Java 16 and 17
  • 24-Apr-2023
Lightrun Team
Author Lightrun Team
Share
Java 16 and 17

Java 16 and 17 compatibility

Lightrun Team
Lightrun Team
24-Apr-2023

Explanation of the problem

When upgrading to Java 16, an exception occurred with the following message: Exception in thread “main” java.lang.ExceptionInInitializerError at com.example.main(Example.java:10). In an attempt to resolve the issue, the VM option –add-opens java.base/java.lang=ALL-UNNAMED was added, but it did not solve the problem.

The exception was caused by a net.sf.cglib.core.CodeGenerationException with the message: java.lang.reflect.InaccessibleObjectException –> Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not “opens java.lang” to unnamed module @39aeed2f. This error occurred when trying to define a protected final java.lang.Class using the defineClass method of java.lang.ClassLoader.

The java.lang.reflect.InaccessibleObjectException error message indicated that it was not possible to make the protected final java.lang.Class method accessible. This was due to the java.base module not opening java.lang to the unnamed module @39aeed2f. The error occurred in the net.sf.cglib.core.ReflectUtils.defineClass method, which was called from the AbstractClassGenerator.generate method. The code blocks included in the exception provide additional information about the code paths leading to the error.

Troubleshooting with the Lightrun Developer Observability Platform

 

Getting a sense of what’s actually happening inside a live application is a frustrating experience, one that relies mostly on querying and observing whatever logs were written during development.
Lightrun is a Developer Observability Platform, allowing developers to add telemetry to live applications in real-time, on-demand, and right from the IDE.

  • Instantly add logs to, set metrics in, and take snapshots of live applications
  • Insights delivered straight to your IDE or CLI
  • Works where you do: dev, QA, staging, CI/CD, and production

Start for free today

Problem solution for Java 16 and 17 compatibility

The answers provided shed light on an important issue with using unmaintained libraries like cglib, particularly in newer JDKs. The specific problem at hand is that cglib generates bytecode that attempts to call protected methods that the JVM is not allowed to access, which results in runtime exceptions. This issue becomes more pronounced in newer JDKs like JDK 17 and above, where access restrictions are enforced more strictly. Therefore, developers should be cautious when using such libraries and consider alternative libraries like ByteBuddy, which are actively maintained and offer better compatibility with newer JDKs.

ByteBuddy is a popular alternative to cglib that provides an API for generating Java classes at runtime. It offers a more modern and streamlined approach to generating bytecode that does not rely on any hacks or workarounds. Furthermore, ByteBuddy offers a simpler and more intuitive API that makes it easier to use than cglib. Instead of generating bytecode that tries to access protected methods, ByteBuddy generates bytecode that relies on public methods, which are guaranteed to continue working in the future. This approach ensures that the code remains functional and maintainable even as the underlying libraries or JDKs evolve.

To ensure that code remains functional and maintainable, it is essential to prioritize writing clean and robust code. This includes avoiding hacks or workarounds and using libraries that are actively maintained and offer better compatibility with newer JDKs. Furthermore, developers should always keep an eye on the latest developments in the Java ecosystem and adjust their code accordingly. This may involve migrating to newer libraries or updating their code to comply with new security or performance requirements. By following these best practices, developers can ensure that their code remains resilient and adaptable over time.

// Example of using cglib to generate bytecode that accesses protected methods
public class Example {
  public static void main(String[] args) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(MyClass.class);
    enhancer.setCallback(new MyMethodInterceptor());
    MyClass proxy = (MyClass) enhancer.create();
    proxy.myProtectedMethod(); // This call will fail in newer JDKs
  }
}

// Example of using ByteBuddy to generate bytecode that relies on public methods
public class Example {
  public static void main(String[] args) throws Exception {
    Class<?> dynamicType = new ByteBuddy()
      .subclass(MyClass.class)
      .method(ElementMatchers.named("myPublicMethod"))
      .intercept(MethodDelegation.to(MyMethodInterceptor.class))
      .make()
      .load(Example.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
      .getLoaded();
    MyClass proxy = (MyClass) dynamicType.newInstance();
    proxy.myPublicMethod(); // This call is guaranteed to work
  }
}

Other popular problems with Java 16 and 17 compatibility

 

Problem: The removal of the Nashorn JavaScript engine

In previous versions of Java, Nashorn was the default JavaScript engine, but it has been removed in Java 16 and 17. This means that applications that rely on Nashorn for JavaScript execution will not work in the newer versions of Java. Developers can resolve this issue by migrating to a different JavaScript engine such as GraalVM or Rhino, which are fully supported in Java 16 and 17.

Solution:

To migrate from Nashorn to GraalVM or Rhino, developers must first ensure that their application does not use any Nashorn-specific APIs. They can then update their code to use the GraalVM or Rhino JavaScript engine. GraalVM is a high-performance, polyglot virtual machine that supports JavaScript, Python, Ruby, and other languages. Rhino is a pure-Java implementation of JavaScript that is compatible with the ECMAScript 5.1 standard. Both engines can be used as drop-in replacements for Nashorn and provide similar functionality.

Problem: The removal of certain APIs

In Java 16, the java.util.jar.Pack200 class was removed, which may cause issues for applications that rely on this class for compression and decompression of JAR files. Additionally, in Java 17, the java.security.acl package was removed, which may impact applications that use this package for access control lists.

Solution:

To resolve issues related to removed APIs, developers must update their code to use alternative APIs that provide similar functionality. In the case of java.util.jar.Pack200, developers can use the java.util.zip package for compression and decompression of JAR files. For applications that use the java.security.acl package, developers can use the java.nio.file.attribute package or other access control mechanisms provided by their operating system.

Problem: Changes in the default garbage collector

In Java 16, the default garbage collector was changed from ParallelGC to G1GC. While G1GC provides better performance for most applications, some applications may experience issues due to its different behavior compared to ParallelGC. For example, applications that rely on short-lived objects may experience longer pauses with G1GC.

Solution:

To resolve issues related to the default garbage collector, developers can experiment with different garbage collectors and their configuration options to find the best fit for their application. They can also use tools such as jstat and jconsole to monitor the performance of the garbage collector and optimize its behavior. Additionally, developers can consider using libraries and frameworks that are optimized for G1GC, such as Micronaut, which provides built-in support for G1GC and other garbage collectors.

A brief introduction of Cglib

Cglib is a widely-used code generation library for Java applications. It is an open-source library that provides high-level API for generating and transforming Java bytecode at runtime. The library is used for a variety of tasks such as dynamic proxy creation, method interception, and class enhancement. Cglib uses a technique called byte-code manipulation to generate new classes at runtime. This allows developers to add new functionality to existing classes without modifying the original code.

Cglib is often used in Java frameworks such as Spring and Hibernate to implement aspects of their functionality, such as transaction management and lazy loading. The library provides a powerful and flexible way to modify existing classes and create new ones at runtime. This flexibility, however, comes with a cost. Bytecode manipulation can introduce performance overhead and increase the complexity of the application. Additionally, the use of Cglib can make the application harder to maintain and debug, as the generated code is often opaque to the developer. Despite these potential drawbacks, Cglib remains a popular choice for dynamic code generation in Java applications.

Most popular use cases for Cglib

  1. Cglib can be used to generate dynamic proxy classes that allow for the interception and modification of method calls at runtime. This is achieved through the creation of a new class that extends the target class and overrides its methods with a call to a user-defined method interceptor. The interceptor can be used to perform tasks such as logging, caching, and security checks without modifying the original source code.
    public class MyInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            // Perform actions before method execution
            Object result = proxy.invokeSuper(obj, args);
            // Perform actions after method execution
            return result;
        }
    }
    
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(MyClass.class);
    enhancer.setCallback(new MyInterceptor());
    MyClass myObject = (MyClass) enhancer.create();
    
  2. Cglib can also be used to generate new classes at runtime based on existing class definitions. This feature can be used to create classes that implement interfaces or extend existing classes, providing a flexible and dynamic way to generate new code on the fly. This is particularly useful for generating code in response to user input or changing runtime conditions.
    public class MyGenerator implements ClassGenerator {
        public void generateClass(ClassWriter writer) {
            // Define class structure and bytecode instructions
        }
    }
    
    DefaultGeneratorStrategy strategy = new DefaultGeneratorStrategy();
    MyGenerator generator = new MyGenerator();
    byte[] bytecode = strategy.generate(generator).toByteArray();
    Class<?> myClass = new MyClassLoader().defineClass("MyClass", bytecode);
    
  3. Cglib can also be used to generate new classes at runtime based on existing class definitions. This feature can be used to create classes that implement interfaces or extend existing classes, providing a flexible and dynamic way to generate new code on the fly. This is particularly useful for generating code in response to user input or changing runtime conditions.
    FastClass fastClass = FastClass.create(MyClass.class);
    FastMethod fastMethod = fastClass.getMethod("myMethod", new Class[]{String.class});
    Object result = fastMethod.invoke(myObject, new Object[]{"myArg"});
    
Share

It’s Really not that Complicated.

You can actually understand what’s going on inside your live applications.

Try Lightrun’s Playground

Lets Talk!

Looking for more information about Lightrun and debugging?
We’d love to hear from you!
Drop us a line and we’ll get back to you shortly.

By submitting this form, I agree to Lightrun’s Privacy Policy and Terms of Use.