🧠 Java level: expert

πŸ• Reading time: 5 minutes

πŸ“Œ Today we're exploring a little-known topic in Java: how to mock the behaviour of a private static final Supplier, which is immutable, using reflection and the powerful (but risky) Unsafe class to ignore the JVM's standard rules. static final fields are, in fact, typically untouchable once they've been initialised. In some cases, though, especially in tests, you might need to overwrite these values to simulate particular behaviours – for instance, in tests that require explicit control over time.

In our example, we're mocking a private static final Supplier<LocalDateTime> inside a LibraryService class, which is supposed to return the current time.

πŸ” Let's dig a bit deeper

1. We create a lambda that always returns the predetermined value:

final Supplier<LocalDateTime> mockSupplier = () -> LocalDateTime.parse("2024-08-08T12:00:00");

2. We access the private field via reflection and bypass the access rules (the field is private):

final Field field = LibraryService.class.getDeclaredField("getCurrentDateTime");
field.setAccessible(true);

3. We use the Unsafe class to modify the final field. The Unsafe class is an internal class of sun.misc that allows very low-level operations (normally not permitted by the JVM), such as memory manipulation. It's a class that lets you perform unsafe operations (hence the name), like modifying final fields.

final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); // get the Unsafe instance
unsafeField.setAccessible(true);                                      // bypass the access rules
final Unsafe unsafe = (Unsafe) unsafeField.get(null);                 // get the instance of the private theUnsafe field

4. We replace the value of the static final field:

final Object staticFieldBase = unsafe.staticFieldBase(field);          // object the static field is defined on
final long staticFieldOffset = unsafe.staticFieldOffset(field);        // offset of the getCurrentDateTime field
unsafe.putObject(staticFieldBase, staticFieldOffset, mockSupplier);    // replace the value with the mockSupplier

5. We handle any errors related to reflection or field access with a simple try-catch block.

πŸ“‘ The complete method

Putting all the steps together, here's what the mockGetCurrentDateTime method looks like inside a test, where the original field is defined in the LibraryService class:

//supplier from the LibraryService class whose behavior we want to mock
private static final Supplier<LocalDateTime> getCurrentDateTime = LocalDateTime::now;

//method to mock the static final field getCurrentDateTime in Java > 11
private void mockGetCurrentDateTime() {
    //create a mock of the Supplier that returns a specific LocalDateTime
    final Supplier<LocalDateTime> mockSupplier = () -> LocalDateTime.parse("2024-08-08T12:00:00");
    try {
        //get the static final field 'getCurrentDateTime' from the LibraryService class
        final Field field = LibraryService.class.getDeclaredField("getCurrentDateTime");
        //make the private field accessible using reflection
        field.setAccessible(true);
        //get the instance of Unsafe, which allows bypassing JVM restrictions
        final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        //make the private 'theUnsafe' field accessible
        unsafeField.setAccessible(true);
        final Unsafe unsafe = (Unsafe) unsafeField.get(null);
        //calculate the memory location of the static field
        final Object staticFieldBase = unsafe.staticFieldBase(field);
        final long staticFieldOffset = unsafe.staticFieldOffset(field);
        //overwrite the value of the static final field with the mockSupplier
        unsafe.putObject(staticFieldBase, staticFieldOffset, mockSupplier);
    } catch (final NoSuchFieldException | IllegalAccessException exception) {
        //handle exceptions related to reflection or private field access
        throw new RuntimeException(exception);
    }
}

⚠️ Watch out

Using Unsafe is extremely powerful, but it comes with risks. Manipulating final or private fields can cause security, stability and compatibility issues with future JVM versions. So this technique should be used with caution and limited to test scenarios, where you need to simulate specific behaviours.

See you at the next pill! β˜•