Wednesday, January 27, 2021

JEP-396 and You: strong encapsulation of the JDK internals is the default

Since the inception of the Project Jigsaw, one of its goals was to encapsulate most of the JDK internal APIs in order to give the contributors a freedom to move Java forward at faster pace. JEP-260, delivered along JDK 9 release was a first step in this direction. Indeed, the famous WARNING messages like the ones below

...
WARNING: An illegal reflective access operation has occurred
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
...

started to appear all over the place. Intended to give the developers and maintainers the time to switch to the alternative, publicly available APIs, they rather provoked the opposite: most just got used to them. Well, if nothing breaks, why bother?

But ... the things are going to change very soon. I think many of you have seen the code which tries to do clever things by gaining the access to the private method or fields of the classes from standard library. One of the notable examples I have seen often enough is overcoming ThreadPoolExecutor's core pool size / maximum pool size semantics (if curious, please read the documentations and complementaty material) by invoking its internal addWorker method. The example below is a simplified illustration of this idea (please, do not do that, ever).

final ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue>Runnable<());

final Runnable task = ...
executor.submit(task);
    
// Queue is empty, enough workers have been created
if (executor.getQueue().isEmpty()) {
    return;
}
    
// Still have some room to go
if (executor.getActiveCount() < executor.getCorePoolSize()) {
    return;
}
    
// Core pool is full but not maxed out, let us add more workers
if (executor.getActiveCount() < executor.getMaximumPoolSize()) {
    final Method addWorker = ThreadPoolExecutor.class.getDeclaredMethod(
        "addWorker", Runnable.class, Boolean.TYPE);
    addWorker.setAccessible(true);
    addWorker.invoke(executor, null, Boolean.FALSE);
}

To be fair, this code works now on JDK 8, JDK 11 or JDK 15: find a private method, make it accessible, good to go. But it will not going to work smoothly in soon to be out JDK 16 and onwards, generating the InaccessibleObjectException exception at runtime upon setAccessible invocation.

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make private boolean java.util.concurrent.ThreadPoolExecutor.addWorker(java.lang.Runnable,boolean) accessible: module java.base does not "opens java.util.concurrent" to unnamed module @72ea2f77
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357)
        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)
        ...

So what is happening here? The new JEP-396 continues the endeavours of JEP-260 by strongly encapsulating JDK internals by default. It has been integrated into JDK 16 and JDK 17 early builds which essentially means, no abusive access is going to be allowed anymore. Arguably, this is the right move, though it is very likely to be a disruptive one.

Should you be worrying? It is a good question: if you do not use any internal JDK APIs directly, it is very likely one of the libraries you depend upon may not play by the rules (or may not be ready for rule changes). Hopefully by the time JDK 16 is released, the ecosystem will be in a good shape. There is never good time, we were warned for years and the next milestone is about to be reached. If you could help your favorite library or framework, please do.

The complete list of the exported packages that will no longer be open by default was conveniently made available here, a couple to pay attention to:

java.beans.*
java.io
java.lang.*
java.math
java.net
java.nio.*
java.rmi.*
java.security.*
java.sql
java.text.*
java.time.*
java.util.*

Last but not least, you still could overturn the defaults using the --add-opens command line options, but please use it with great caution (or better, do not use it at all):

$ java --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED ...

Please be proactive and test with latest JDKs in advance, luckily the early access builds (JDK 16, JDK 17) are promptly available to everyone.

No comments: