Java 9 introduces modules to the Java programming language and its runtime. Despite this feature being optional, due to the modularization of the standard library existing applications might behave differently when running on a version 9 JVM. Furthermore, because of changes in the runtime, existing libraries and frameworks might not yet correctly process your modularized code. As a result, updating to a Java 9 VM and taking Java 9 into brings its challanges.
This talk discusses the practical implications of module boundaries and analyzes new limitations Java 9 imposes on the reflection API. This talk explains how reflection is used in popular frameworks like Spring and Hibernate and explains why existing applications might break or change their behavior when facing modularized code. Finally, this talk showcases alternatives to now failing Java programming patterns and weights their robustness with regard to the Java releases 10 and upward.
The presenter is an active contributor to open source and helped to migrate many popular Java libraries to supporting Java 9. As a consequence, he as been working with Java 9 for almost two years.
4. Scope
Task module descriptor
module descriptor
deployment descriptor
build execution descriptor
API for custom tasks
runtime
compile-time
build-time
Type declarative declarative/programmatic
Unit Java package (sub-)project
5. api/Foo.class internal/Qux.class api/Bar.class internal/Baz.class
api/Foo.class internal/Qux.class api/Bar.class internal/Baz.class
(system) class path
Modularity prior to Java 9 is emulated by class loader hierarchies. By tweaking class
loaders to define multiple parents, it is also possible to run several versions of the
same module (e.g. OSGi).
6. api/Foo.class internal/Qux.class api/Bar.class internal/Baz.class
api/Foo.class internal/Qux.class api/Bar.class internal/Baz.class
module loader
example.foo
example.bar
“example.bar reads example.foo”
The integrity of the module graph is verified by the Java 9 runtime. Jars without a
module-info.class are typically contained by the unnamed module that reads all modules.
7. module example.qux {
exports example.bar;
}
module example.foo {
requires example.foo;
exports api;
}
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>qux</artifactId>
<version>1</version>
</project>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>bar</artifactId>
<version>1</version>
</project>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>foo</artifactId>
<version>1</version>
</project>
module example.bar {
requires example.foo;
}
module example.bar {
requires transitive
example.foo;
}
Imports are non-transitive by default (what is a good default for a runtime module system).
9. module example.bar {
requires example.foo;
requires java.xml;
}
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>foo</artifactId>
<version>1</version>
</project>
module java.base {
exports java.lang;
exports java.io;
exports java.util;
// ...
}
module java.xml {
exports org.xml.sax;
// ...
}
Modules can use non-modularized code as automatic modules (referenced by jar name).
Automatic modules export every package and import any module.
Automatic modules should only be used locally to avoid depending on a name.
example-foo-1.jar
10. module java.base {
exports java.lang;
exports java.io;
exports java.util;
// ...
}
module java.xml {
exports org.xml.sax;
// ...
}
module example.foo {
exports api;
// ...
}
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>bar</artifactId>
<version>1</version>
</project>
Non-modularized code can still run on the class path within the unnamed module.
The unnamed module imports any module but does not export any packages.
13. module java.base {
exports java.lang;
exports java.io;
exports java.util;
// ...
}
import sun.misc.Unsafe
module jdk.unsupported {
exports sun.misc;
// ...
}
module example.bar {
requires jdk.unsupported;
}
Despite its name, the jdk.unsupported module is also contained in regular JVMs.
14. module sample.app {
requires foo.bar.qux;
requires some.lib;
}
java -p foo-bar-1.0.jar Main
java -p foo-bar-qux.jar Main
name version
module sample.app {
requires foo.bar.qux;
}
module some.lib {
requires foo.bar;
}
Automatic module names are instable because of:
1. The inpredictability of a jar's file name
2. The inpredictability of a dependencys future module name
Module names should follow the reverse DNS convention known from package names.
Ideally, a module name should be equal to the module's root package.
Avoid an impedance mismatch between module names and (Maven) artifact ids.
Automatic module names and module naming convention.
throws ResolutionException
Module-Name: foo.bar.qux
15. class foo.Bar class pkg.Main
Loading a class from the class path.
java
–cp first.jar:second.jar:third.jar
pkg.Main
Class<?> load(String className) {
for (JarFile jarFile : getClassPath()) {
if (jarFile.contains(className)) {
return jarFile.load(className);
}
}
throw new ClassNotFoundException(className);
}
class foo.Bar
Packages can be split among jars unless they are sealed. Yet, sealing is vulnerable.
search
order
16. Loading a class from the module path.
Map<String, Module> packageToModule;
Class<?> load(String className) {
Module module = packageToModule.get(pkgName(className));
if (module != null && module.contains(className)) {
return module.load(className);
}
throw new ClassNotFoundException(className);
}
example.foo example.bar
Module-Package-ownership: split packages or concealed packages are no longer permitted.
java
–p first.jar:second.jar:third.jar
–m example.foo/pkg.Main
18. package library;
class Api {
@Deprecated
String foo() {
return "foo";
}
String bar() {
return "bar";
}
}
package library;
class Api {
String foo() {
return "foo";
}
}
Dealing with "jar hell"
package library;
class Api {
String bar() {
return "bar";
}
}
library-1.0.jar library-2.0.jar
Api api = new Api();
api.foo();
second-library-1.0.jar
Api api = new Api();
api.bar();
third-library-1.0.jar
throws NoSuchMethodError
19. package library;
class Api {
String foo() {
return "foo";
}
}
package library;
class Api2 {
String bar() {
return "bar";
}
}
library-1.0.jar library-2.0.jar
Class<?> entryPoint = ModuleLayer.empty()
.defineModules(
Configuration.resolve(ModuleFinder.of(modsDir),
Collections.emptyList(),
ModuleFinder.of(), // no late resolved modules
Collections.singleton("my.module")),
name -> makeClassLoader(name)
).layer().findLoader("my.module").loadClass("my.Main");
With little tweaking, the JPMS is however already capable of isolating packages:
20. Jars, modular jars and jmods.
javac
-d target
src/module-info.java src/pkg/Main.java
jar
--create
--file lib/example.foo.jar
--main-class=com.greetings.Main
-C target
.
jmod
create
--class-path target
--main-class=com.greetings.Main
lib/example.foo.jmod
JMOD
A jar file containing a module-info.class is considered a modular jar.
Both the jar and the jmod tool support a --module-version parameter.
Versions are however not currently a part of JPMS, “jar hell” is therefore not averted.
21. Linking modules prior to execution.
jlink
–-module-path ${JAVA_HOME}/jmods:mods
--add-modules example.foo
--launcher run=example.foo/pkg.Main
--output myapp
/myapp
/bin
java
run
/conf
/include
/legal
/lib
release./myapp/bin/run
Hello world!
du –hs myapp
44M myapp
./myapp/bin/java --list-modules
example.foo
java.base@9
Jlink (as of today) can only process jmods and modular jars.
22. package api;
public class SomeClass {
public void foo() {
/* do something */
}
private void bar() {
/* do something */
}
}
Method foo = SomeClass.class.getMethod("foo");
foo.invoke(new SomeClass());
Method bar = SomeClass.class.getDeclaredMethod("bar");
bar.setAccessible(true); // check against security manager
bar.invoke(new SomeClass());
Reflection and breaking encapsulation
module example.foo {
opens api;
}
open module example.foo { }
JVM modules do not open any of their packages (unless specified on the command line)!
java --add-opens
java --illegal-access=<permit,deny>
Module::addOpens
Instrumentation::redefineModule
24. module java.base {
exports java.lang;
// ...
}
module my.library { }
Field value = String.class.getDeclaredField("value");
value.setAccessible(true); // java.lang is not 'open'
Reflection against closed APIs is not discovered by the jdeps tool.
An existing class-path application that compiles and
runs on Java SE 8 will, thus, compile and run in
exactly the same way on Java SE 9, so long as it only
uses standard, non-deprecated Java SE APIs.
An existing class-path application that compiles and
runs on Java SE 8 will, thus, compile and run in
exactly the same way on Java SE 9, so long as it only
uses standard, non-deprecated Java SE APIs,
excluding the use of reflection on non-public members.
JPMS
group
25. package java.lang;
public class ClassLoader {
protected Class<?> defineClass(String name, byte[] b,
int off, int len) {
/* define class and return */
}
}
public class MyBean
{
public void foo() { }
void bar() { }
}
public class ProxyBean extends MyBean
{
@Override public void foo() { }
@Override void bar() { }
}
MyBean mock = Mockito.mock(MyBean.class);
MyBean persisted = entityManager.merge(myBean);
MyBean bean = applicationContext.getBean(MyBean.class);
ClassLoader proxyLoader = new ProxyClassLoader(parent);
Class<?> proxy = proxyLoader.loadClass("ProxyBean");
MethodHandles.Lookup lookup = MethodHandles.lookup();
lookup.defineClass(classFile);
MethodHandle bar = lookup.findVirtual(MyBean.class,
"bar",
MethodType.methodType(void.class);
bar.invoke(new MyBean());
MyBean mock = Mockito.mock(
MyBean.class, MethodHandles.lookup());
26. module my.library {
requires jdk.unsupported;
}
module jdk.unsupported {
exports sun.misc;
opens sun.misc;
// ...
}
module java.base {
exports java.lang;
// ...
}
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
unsafe.defineClass( ... );
package java.lang;
public class Accessor {
public void access() {
// within module
}
}
module my.library { }
Field value = String.class.getDeclaredField("value");
value.setAccessible(true); // java.lang is not 'open'
28. public class SomeHttpClient {
void call(String url) {
try {
Class.forName("org.apache.http.client.HttpClient");
ApacheDispatcher.doCall(HttpClients.createDefault());
} catch (ClassNotFoundException ignored) {
JVMDispatcher.doCall(new URL(url).openConnection());
}
}
}
module library.http {
requires static
httpclient;
}
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>2.0</version>
<optional>true</optional>
</dependency>
</dependencies>
Could be substituted by using a service loader and modules that implement the binding.
29. module my.app {
requires spring;
opens beans;
}
module my.app {
requires spring;
requires careless.lib;
opens beans to spring;
}
module my.app {
requires spring;
requires careless.lib;
opens beans;
}
module spring {
// ...
}
module careless.lib {
// has vulnerability
}
class Util {
void doCall(String httpVal)
throws Exception {
Class.forName(httpVal)
.getMethod("apply")
.invoke(null);
}
}
It is also possible to qualify exports statements for the creation of friend modules.
This can be useful for priviledged modules within the same module family.
30. module my.app {
requires web.framework;
opens app;
}
URL ressource = getClass().getResource("/foo/bar.txt");
Resources are only visible to other modules if:
1. They reside in an exported package
2. Belong to the module conducting the lookup
3. Reside in a folder that is not a legal package-name (e.g. META-INF)
4. End with .class
/app/MyWebPage.java
/app/MyWebPage.html
abstract class BaseWebPage {
URL findTemplate() {
String name = getClass()
.getSimpleName() + ".html";
return getClass()
.getResource(name);
}
}
module web.framework {
// ...
}
module my.app {
requires web.framework;
}
class MyWebPage
extends BaseWebPage {
/* ... */
}
35. Java platform module system: the controversy
Are modules even (still/even) useful?
Considering microservices, modules are often the better option, both considering
maintainability and performance. We have always used modules with Maven; adding
first-level support offers a chance to better interact with them at runtime.
Think of what we could have instead of Jigsaw in the time it took!
Remember the last time you had to defend a refactoring/maintenance release?
You should always clean up before you expand your feature set.
Java modules breaks all the things!
The "growth only" model is not sustainable if Java should have a future. The JVM
must at some point apply breaking changes to survive/strive.
To make full use of Java, some code needs to cross module boundaries!
Nobody relies on internal APIs for laziness but because it is necessary. Without the
openness of sun.misc.Unsafe in the past, a lot of libraries that make the JVM such
an attractive choice would not exist today. The standard library only offers basic
functionality by design. Especially test libraries and tooling have a legitimate wish
to "cross the line". As of today, any code can however still use Unsafe to leverage
low-level functionality. The standard library still relies heavily on qualified exports!