🧠 Livello Java: esperto

🕐 Tempo di lettura: 5 minuti

📌 Oggi esploriamo un argomento poco conosciuto in Java: come mockare il comportamento di un Supplier private static final, immutabile, utilizzando reflection e la potente (ma rischiosa) classe Unsafe per ignorare le regole standard della JVM. I campi static final, infatti, sono tipicamente intoccabili dopo la loro inizializzazione. Tuttavia, in alcuni casi, specialmente nei test, potrebbe essere necessario sovrascrivere questi valori per simulare comportamenti particolari, come ad esempio in test che richiedono un controllo esplicito sul tempo.

Nel nostro esempio, stiamo mockando un private static final Supplier<LocalDateTime> all'interno di una classe LibraryService, che dovrebbe restituire l'ora corrente.

🔍 Andiamo più nel dettaglio

1. Creiamo una lambda che restituisca sempre il valore predeterminato:

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

2. Accediamo al campo privato tramite reflection e bypassare le regole di accesso (il campo è privato):

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

3. Usiamo la classe Unsafe per modificare il campo final. La classe Unsafe è una classe interna di sun.misc che permette operazioni a bassissimo livello (normalmente non permesse dalla JVM), come manipolazioni della memoria. È una classe che consente di eseguire operazioni non sicure (da qui il nome), come modificare campi final.

final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); // otteniamo l'istanza di Unsafe
unsafeField.setAccessible(true);                                      // bypassiamo le regole di accesso
final Unsafe unsafe = (Unsafe) unsafeField.get(null);                 // otteniamo l'istanza del campo privato theUnsafe

4. Sostituiamo il valore del campo static final:

final Object staticFieldBase = unsafe.staticFieldBase(field);          // oggetto su cui il campo statico è definito
final long staticFieldOffset = unsafe.staticFieldOffset(field);        // offset del campo getCurrentDateTime
unsafe.putObject(staticFieldBase, staticFieldOffset, mockSupplier);    // rimpiazza il valore con il mockSupplier

5. Gestiamo eventuali errori legati alla reflection o all'accesso ai campi con un semplice blocco try-catch.

📑 Il metodo completo

Mettendo insieme tutti i passi, ecco come si presenta il metodo mockGetCurrentDateTime all'interno di un test, dove il campo originale è definito nella classe LibraryService:

//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);
    }
}

⚠️ Attenzione

L'utilizzo di Unsafe è estremamente potente, ma comporta rischi. Manipolare campi final o privati può causare problemi di sicurezza, stabilità e compatibilità con future versioni della JVM. Pertanto, questa tecnica dovrebbe essere usata con cautela e limitata a scenari di test, dove è necessario simulare comportamenti specifici.

Alla prossima pillola! ☕