Moving reflection-based code from Java 8 to 11 and 17
Tags: java, jdk17, jdk11
Published: 2022-02-18 10:18:43

During the migration of one of the libraries from Java 8 to Java 11 and then to Java 17 I ran into a number of problems with the reflection functionality that was used in this library. In my article I'm going to describe these problems and how they can be solved.

## Java 8
In the library reflection mechanism was used to access private fields, methods and constructors of JDK internal classes, for example, to manipulate annotations in runtime. Here are some examples of reflection usages without actual manipulation code:
```java
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

public class ReflectionUsage {

    public static void main(String[] args) throws NoSuchMethodException, NoSuchFieldException, ClassNotFoundException {
        Method declaredAnnotationsMethod = Field.class.getDeclaredMethod("declaredAnnotations");
        declaredAnnotationsMethod.setAccessible(true);

        Field declaredAnnotationsField = Field.class.getDeclaredField("declaredAnnotations");
        declaredAnnotationsField.setAccessible(true);

        Class<?> annotationDataClass = Class.forName("java.lang.Class$AnnotationData");
        Constructor<?> annotationDataConstructor = annotationDataClass.getDeclaredConstructor(Map.class, Map.class, int.class);
        annotationDataConstructor.setAccessible(true);

        Method annotationDataMethod = Class.class.getDeclaredMethod("annotationData");
        annotationDataMethod.setAccessible(true);
    }
}
```

## Java 11
After I migrated the library to Java 11, I started getting warnings like the followings:
```
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by ...ReflectionUsage ... to method java.lang.reflect.Field.declaredAnnotations()
WARNING: Please consider reporting this to the maintainers of ...ReflectionUsage
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
```
These warnings are caused by the modular structure introduced in Java 9: reflective access to internal JDK modules is considered to be `Illegal`, but in Java 9-11 such access is still allowed - it only produces warnings.  
To see all warnings produced due to `Illegal` access of the code, you can use the option: 
```
--illegal-access=warn
```
  
## Java 17 
### InaccessibleObjectException
After I migrated the library to Java 17, I started seeing runtime errors `InaccessibleObjectException` instead of `Illegal` access warnings. An example of `InaccessibleObjectException` stack trace:
```
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make private java.util.Map java.lang.reflect.Field.declaredAnnotations() accessible: module java.base does not "opens java.lang.reflect" to unnamed module @...
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
	at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
	at ...ReflectionUsage.main(ReflectionUsage.java:13)

```
To resolve these exceptions it's required to use the option:
```
--add-opens
```
From the Oracle [article](https://docs.oracle.com/javase/9/migrate/toc.htm#JSMIG-GUID-12F945EB-71D6-46AF-8C3D-D354FD0B1781):

> If you have to allow code on the class path to do deep reflection to access nonpublic members, then use the --add-opens runtime option. 
> Some libraries do deep reflection, meaning setAccessible(true), so they can access all members, including private ones. You can grant this access using the --add-opens option on the java command line. No warning messages are generated as a result of using this option.

So I used the following options for the library:
```
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED
```
   
**NOTE**
In Java 16 and prior it's also possible to fix this error by using the option:
```
--illegal-access=permit
``` 
but its support has been removed in Java 17

### NoSuchFieldException
Fix with `--add-opens` option helped me with `InaccessibleObjectException` but then I started getting `NoSuchFieldException` when trying to access private field `declaredAnnotations` from `java.lang.reflect.Field` class:
```
Exception in thread "main" java.lang.NoSuchFieldException: declaredAnnotations
	at java.base/java.lang.Class.getDeclaredField(Class.java:2610)
	at ...ReflectionUsage.main(ReflectionUsage.java:15)
```
for the sentence:
```java
Field declaredAnnotationsField = Field.class.getDeclaredField("declaredAnnotations");
```
`NoSuchFieldException` was caused by the feature "Filtering for classes with security sensitive fields" introduced in Java 12 [JDK-8210522](https://bugs.openjdk.java.net/browse/JDK-8210522):

> Core reflection has a filtering mechanism to hide security and integrity sensitive fields and methods from Class getXXXField(s) and getXXXMethod(s). 
> The filtering mechanism has been used for several releases to hide security sensitive fields such as System.security and Class.classLoader.
This CSR proposes to extend the filters to hide fields from a number of highly security sensitive classes in java.lang.reflect and java.lang.invoke.

I found two possible workarounds to access private fields of `java.lang.reflect.Field` class (in my case I needed access to `declaredAnnotations` field, but the same approach can be used to access other fields that are not available because of [JDK-8210522](https://bugs.openjdk.java.net/browse/JDK-8210522)) 

**Workaround #1:** use [VarHandle](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/VarHandle.html) ([source](https://stackoverflow.com/a/56043252/5516086)): 
```java
var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
VarHandle declaredAnnotationsField = lookup.findVarHandle(Field.class, "declaredAnnotations", Map.class);

```

**Workaround #2:** invoke `getDeclaredFields0` method for `java.lang.reflect.Field` class using reflection ([source](https://stackoverflow.com/a/69418150/5516086)):  
```java
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0.setAccessible(true);

Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
Field declaredAnnotationsField;
for (Field field : fields) {
    if ("declaredAnnotations".equals(field.getName())) {
        declaredAnnotationsField = field;
        declaredAnnotationsField.setAccessible(true);
        break;
    }
}
``` 

**NOTE**
Main goal of [JDK-8210522](https://bugs.openjdk.java.net/browse/JDK-8210522) feature is to prevent reflective access to several internal fields, so workarounds described above can stop working with any new JDK release so if you have the same problem - probably you need to think about re-writing your code to use a different approach.

## Conclusion
In this article I've covered only reflection related aspects of migration Java applications to the newest LTS releases of Java (JDK 11, JDK 17), but there are certainly many more potential issues and problems during this upgrade.

---
Cover image: Photo by [Andrew Ly](https://unsplash.com/@nineteen?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/mountain-reflection?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)