Current Tutorial
Circumventing Strong Encapsulation with --add-exports and --add-opens

Circumventing Strong Encapsulation with --add-exports and --add-opens

The module system is very strict about access to internal APIs: If the package isn't exported or opened, access will be denied. But a package can't just be exported or opened by a module's author - there are also the command line flags --add-exports and --add-opens, which allow the module's user to do that as well.

That way, it is possible to write and run code that accesses internals of the app's dependencies or of the JDK APIs. Since this comes with a trade-off between more features or performance (presumably) versus less maintainability or undermined platform integrity, this decision should not be made lightly. And because it ultimately concerns not just the developer but also the user of the resulting app, these command line flags have to be applied at launch time, so the user is aware that the trade-off is being made.

Note: To fully understand this feature, you need a thorough understanding of a few different aspects of the module system, namely its basics, the support for reflection, qualified exports and opens, how to build and launch from the command line, and why strong encapsulation is important.

Exporting Packages with --add-exports

The option --add-exports $MODULE/$PACKAGE=$READING_MODULE, available for the java and javac commands, exports $PACKAGE of $MODULE to $READING_MODULE. Code in $READING_MODULE can hence access all public types and members in $PACKAGE but other modules can not. When setting $READING_MODULE to ALL-UNNAMED, all code from the class path can access that package. In a project that doesn't use modules, you will always use that placeholder - only once your own code runs in modules can you limit exported packages to specific modules.

The space after --add-exports can be replaced with an equal sign =, which helps with some tool configurations (Maven, for example): --add-exports=.../...=....

At Compile Time

As an example, see this code that tries to create an instance of the internal class sun.util.BuddhistCalendar:

BuddhistCalendar calendar = new BuddhistCalendar();

If we compile it like that, we get the following error, either on the import or the line itself if there's no import:

error: package sun.util is not visible
  (package sun.util is declared in module java.base, which does not export it)

The option --add-exports can work around that. If the code above is compiled without module declaration, we need to open the package to ALL-UNNAMED:

javac
    --add-exports java.base/sun.util=ALL-UNNAMED
    Internal.java

If it's in module named com.example.internal, we can be more precise and thus minimize exposure of internals:

javac
    --add-exports java.base/sun.util=com.example.internal
    module-info.java Internal.java

At Run Time

When launching the code (on JDK 17 and higher), we get a run-time error:

java.lang.IllegalAccessError:
    class Internal (in unnamed module @0x758e9812)
    cannot access class sun.util.BuddhistCalendar (in module java.base)
    because module java.base does not export sun.util to unnamed module @0x758e9812

To solve this problem, we need to repeat the --add-exports option at launch time. For code in the class path:

java
    --add-exports java.base/sun.util=ALL-UNNAMED
    --class-path com.example.internal.jar
    com.example.internal.Internal

If it's in module named com.example.internal (that defines a main class), we can again be more precise:

java
    --add-exports java.base/sun.util=com.example.internal
    --module-path com.example.internal.jar
    --module com.example.internal

Opening Packages with --add-opens

The command line option --add-opens $MODULE/$PACKAGE=$REFLECTING_MODULE opens $PACKAGE of $MODULE to $REFLECTING_MODULE. Code in $REFLECTING_MODULE can hence reflectively access all types and members, public and non-public ones, in $PACKAGE but other modules can not. When setting $READING_MODULE to ALL-UNNAMED, all code from the class path can reflectively access that package. In a project that doesn't use modules, you will always use that placeholder - only once your own code runs in modules can you limit opened packages to specific modules.

The space after --add-opens can be replaced with an equal sign =, which helps with some tool configurations: --add-opens=.../...=....

Since --add-opens is bound to reflection, a pure run time concept, it only makes sense for the java command. But given that numerous command line options work across multiple tools, it's helpful to report and explain when an option doesn't and so javac does not reject the option and instead issues the warning that "--add-opens has no effect at compile time".

At Run Time

As an example, see this code in a class Internal that tries to use reflection to create an instance of the internal class sun.util.BuddhistCalendar:

Class.forName("sun.util.BuddhistCalendar").getConstructor().newInstance();

Since the code doesn't compile against the internal class BuddhistCalendar, compilation works without additional command line flags. But on JDK 17 and higher, executing the resulting code leads to an exception at run time:

Exception in thread "main" java.lang.IllegalAccessException:
    class Internal cannot access class sun.util.BuddhistCalendar (in module java.base)
    because module java.base does not export sun.util to unnamed module @1f021e6c
        at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
        at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:489)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)

The option --add-opens can work around that. If the code above is in a JAR on the class path, we need to open the package sun.util to ALL-UNNAMED:

java
    --add-opens java.base/sun.util=ALL-UNNAMED
    --class-path com.example.internal.jar
    com.example.internal.Internal

(Recall from the article on strong encapsulation, that it is not necessary to open the packages sun.misc and sun.reflect because they are exported by jdk.unsupported.)

If it's in module named com.example.internal (that defines a main class), we can be more precise and thus minimize exposure of internals:

java
    --add-opens java.base/sun.util=com.example.internal
    --module-path com.example.internal.jar
    --module com.example.internal

Last update: September 14, 2021


Current Tutorial
Circumventing Strong Encapsulation with --add-exports and --add-opens