OOP

Java Hidden Classes

In Java, Hidden Classes are a feature introduced in Java 15 as part of the JEP 371 proposal.

They are not meant for direct use by typical application developers but are more relevant to frameworks, libraries, and advanced use cases such as dynamic proxies and runtime code generation.

Hidden classes are non-discoverable, meaning they do not appear in the system dictionary, making them ideal for temporary, dynamically generated classes that should not be accessible through the usual reflection mechanisms.

Key Characteristics of Hidden Classes

  1. Non-discoverable: Hidden classes are not visible in the usual class namespaces, so they cannot be accessed by name after creation.
  2. Dynamic Generation: They are typically generated at runtime, not defined in source code.
  3. Temporary Usage: They are often used in frameworks and libraries where a temporary, disposable class is needed.
  4. Enhanced Security: Since hidden classes are isolated, they improve security and encapsulation by hiding implementation details from the global scope.

Creating Hidden Classes

To create a hidden class, we use the java.lang.invoke API, specifically MethodHandles and Lookup. We use Lookup.defineHiddenClass to define a hidden class at runtime.

Example Scenarios

Let’s look at some practical scenarios where hidden classes can be beneficial, along with examples.

Example 1: Creating a Simple Hidden Class

In this example, we’ll dynamically define a hidden class and invoke a method from it.

Code Example

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.MethodHandle;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;

public class HiddenClassExample {
    public static void main(String[] args) throws Throwable {
        // Load bytecode for the hidden class (assuming it's pre-compiled)
        byte[] bytecode = Files.readAllBytes(Paths.get("MyHiddenClass.class"));

        // Use a Lookup to define a hidden class
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        Class<?> hiddenClass = lookup.defineHiddenClass(bytecode, true, MethodHandles.Lookup.ClassOption.NESTMATE).lookupClass();

        // Create a MethodHandle for the method we want to invoke
        MethodHandle hiddenMethod = lookup.findStatic(hiddenClass, "hiddenMethod", MethodType.methodType(void.class));

        // Invoke the hidden method
        hiddenMethod.invoke();
    }
}

// Hidden Class (pre-compiled and loaded as bytecode)
public class MyHiddenClass {
    public static void hiddenMethod() {
        System.out.println("Hello from the hidden class!");
    }
}

Explanation

  1. Load Bytecode: We load the bytecode of the hidden class (e.g., MyHiddenClass.class) using Files.readAllBytes.
  2. Define Hidden Class: We define the class as hidden using lookup.defineHiddenClass. The ClassOption.NESTMATE option makes it a nestmate of the current class, allowing access to its private members if needed.
  3. Invoke Method: We create a MethodHandle to the hiddenMethod and then invoke it.

Note: Hidden classes are defined using bytecode rather than source code, so MyHiddenClass.class needs to be compiled separately.

Example 2: Using Hidden Classes in a Proxy Pattern

Hidden classes are commonly used in frameworks for creating dynamic proxies, where runtime-generated classes can implement or extend interfaces without being part of the main application’s codebase.

Code Example

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;

interface Service {
    void execute();
}

public class HiddenProxyExample {
    public static void main(String[] args) throws Throwable {
        byte[] bytecode = Files.readAllBytes(Paths.get("ServiceProxy.class"));

        MethodHandles.Lookup lookup = MethodHandles.lookup();
        Class<?> proxyClass = lookup.defineHiddenClass(bytecode, true, MethodHandles.Lookup.ClassOption.NESTMATE).lookupClass();

        MethodHandle constructor = lookup.findConstructor(proxyClass, MethodType.methodType(void.class));
        Service proxyInstance = (Service) constructor.invoke();

        proxyInstance.execute();
    }
}

// Proxy implementation (compiled separately)
public class ServiceProxy implements Service {
    @Override
    public void execute() {
        System.out.println("Executing service via proxy.");
    }
}

Explanation

  1. Define the Interface: We define a Service interface with an execute() method.
  2. Hidden Proxy Class: The ServiceProxy class implements the Service interface and provides the execute method’s implementation.
  3. Dynamic Proxy Creation: Using MethodHandles.Lookup, we load and create an instance of the hidden ServiceProxy class at runtime.
  4. Invoke Method: Finally, we call the execute method through the Service interface, executing it dynamically.

Note: As in the previous example, ServiceProxy.class should be compiled in advance and provided as bytecode.

Example 3: Using Hidden Classes with Method Handles for Advanced Reflection

Hidden classes can work with MethodHandles to execute code that should be encapsulated and invisible to the outer application. Here, we’ll demonstrate creating a hidden class to encapsulate sensitive computations.

Code Example

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;

public class HiddenComputeExample {
    public static void main(String[] args) throws Throwable {
        byte[] bytecode = Files.readAllBytes(Paths.get("ComputeClass.class"));

        MethodHandles.Lookup lookup = MethodHandles.lookup();
        Class<?> computeClass = lookup.defineHiddenClass(bytecode, true, MethodHandles.Lookup.ClassOption.NESTMATE).lookupClass();

        MethodHandle computeMethod = lookup.findStatic(computeClass, "compute", MethodType.methodType(int.class, int.class, int.class));
        int result = (int) computeMethod.invoke(10, 20);

        System.out.println("Computation result: " + result);
    }
}

// ComputeClass.java (compiled separately)
public class ComputeClass {
    public static int compute(int a, int b) {
        return a + b;
    }
}

Explanation

  1. Hidden Compute Class: We create a ComputeClass that provides a compute method to add two numbers.
  2. Defining Hidden Class: The MethodHandles.Lookup is used to define the ComputeClass as hidden.
  3. Method Invocation: We retrieve a MethodHandle for the compute method and invoke it with the specified arguments, obtaining the computed result.

This example demonstrates how hidden classes can securely encapsulate computations or sensitive logic, shielding them from access via regular reflection.

Example 4: Conditional Access to Hidden Classes

Hidden classes can restrict their access within certain conditions, providing conditional behavior based on runtime data.

Code Example

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;

public class ConditionalAccessExample {
    public static void main(String[] args) throws Throwable {
        if (System.getenv("ALLOW_ACCESS") != null) {
            byte[] bytecode = Files.readAllBytes(Paths.get("ConditionClass.class"));

            MethodHandles.Lookup lookup = MethodHandles.lookup();
            Class<?> conditionClass = lookup.defineHiddenClass(bytecode, true, MethodHandles.Lookup.ClassOption.NESTMATE).lookupClass();

            MethodHandle conditionMethod = lookup.findStatic(conditionClass, "conditionalAccess", MethodType.methodType(void.class));
            conditionMethod.invoke();
        } else {
            System.out.println("Access not allowed.");
        }
    }
}

// ConditionClass.java (compiled separately)
public class ConditionClass {
    public static void conditionalAccess() {
        System.out.println("Conditional access granted.");
    }
}

Explanation

  1. Environment-based Access Control: Before loading the hidden class, the program checks if an environment variable ALLOW_ACCESS is set.
  2. Defining Hidden Class Conditionally: If the condition is met, ConditionClass is defined and the method conditionalAccess is invoked.
  3. Encapsulation: This approach shows how to restrict access to hidden class functionality based on runtime conditions.

Key Points to Remember about Hidden Classes

  1. Usage: Hidden classes are designed primarily for frameworks and libraries rather than application code.
  2. Method Handles: Hidden classes work closely with MethodHandles and Lookup to define classes and invoke their methods dynamically.
  3. Isolation: Since hidden classes are non-discoverable, they help keep sensitive or temporary classes encapsulated.
  4. No Direct Reference: Hidden classes cannot be referenced directly by name after their creation, enhancing security and encapsulation.

Summary

Java’s hidden classes provide a way to define classes dynamically at runtime, without polluting the global namespace.

They are useful for cases where a class is needed temporarily and should not be accessible outside its immediate context.

Hidden classes are particularly beneficial for frameworks and libraries that need to generate classes on the fly, allowing for encapsulation, security, and advanced reflective operations through MethodHandles.

Related posts

Java sealed classes

Java Static Classes

Java inner classes