Migrating Your Project to Jigsaw Step by Step

Java 9 is out. Let's try it on a simple Spring-based project. To make the exercise bit more challenging, let's try to use the new module system as well. The project is just a simple example using Spring, JDBC, and Shedlock.

  1. Read all available documentation and specifications. Hmm, sounds boring. Skip step one.
  2. Download the JDK and try to run the project. We are lucky, all our dependencies were using only public Java APIs, so it works. In a real-world project, you might not be so lucky. Some libraries are using removed internal APIs that are not available in Java 9. You have to wait for library authors to fix them, which usually is not easy at all. Luckily, that's not our case.

    Our project is running on the Java 9 JVM and we can stop here. We can take advantage of compact strings and other optimizations without going any further. But we want to use new language features as well.
  3. Switch the compiler to Java 9:
    <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
            <source>1.9</source>
            <target>1.9</target>
        </configuration>
    </plugin>

    Still works, cool. We could have used new '--release' option here, but unfortunately, it's not supported by IntelliJ yet. Again, we can stop here. Now we can use all the new language features. But let's try Jigsaw.
  4. Add module-info.java and run it again
  5. module shedlock.example {
    }


    The project does not compile. We get lots of 'package XYZ is not visible' compiler errors. It makes sense — we are using external libraries and we have not declared those dependencies in module-info. Luckily, we have the newest IntelliJ, which will help us to fix it. It's some manual work, and I expect it to become a one-click process in the future, but it's good enough for now. Alternatively, you can use the jdeps tool.

    module shedlock.example {
        requires spring.context;
        requires spring.jdbc;
        requires slf4j.api;
        requires shedlock.core;
        requires shedlock.spring;
        requires HikariCP;
        requires shedlock.provider.jdbc.template;
        requires java.sql;
    }


    Please note that none of the libraries have the module-info declaration. What we see here are automatic modules with names automatically generated from JAR names. Some of the names may change when the library authors release a module-aware version. For example, slf4j.api will most likely be renamed org.slf4j. It's not a problem in a project like this, though. When we upgrade slf4j, we can change the dependency, too.

    But in a library, it's something different.

    Once we declare a dependency, it's really hard to change it without breaking all projects depending on us. That's why Maven is warning us "Required filename-based automodules detected. Please don't publish this project to a public artifact repository!" Now we have all our dependencies declared, and the project compiles! We are done! Right?

    Caused by: 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 module spring.core
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
    at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:198)
    at java.base/java.lang.reflect.Method.setAccessible(Method.java:192)
    at spring.core@4.3.7.RELEASE/org.springframework.cglib.core.ReflectUtils$1.run(ReflectUtils.java:54)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at spring.core@4.3.7.RELEASE/org.springframework.cglib.core.ReflectUtils.<clinit>(ReflectUtils.java:44)
    ... 25 more


    Not so fast.

    Java modules are enforced both at compile time and run time. I have executed the project from IntelliJ, which saw module-info.java and automatically used module-path instead of class-path, which in turn switched on runtime checks. We are getting 'module java.base does not "opens java.lang" to module spring.core'. Spring is trying to use a reflection on a class from the java.base module, and the module system does not want to allow it. Spring 5 supports Java 9, so let's update to newest Spring 5 RC 4 and... does not help.

    Let's downgrade again.

    Shall we read the documentation? No way, StackOverflow to the rescue. We have to open java.base module. Since we are not able to modify it (luckily), the only other way is to use a command line parameter.

  6. Use the --add-opens java.base/java.lang=spring.core command line parameter. Now the spring.core module can access java.base by reflection. Let's execute it and move to the next exception.
    Caused by: java.lang.IllegalAccessException: class org.springframework.cglib.proxy.Enhancer (in module spring.core) cannot access class net.javacrumbs.shedlockexample.SpringConfig$$EnhancerBySpringCGLIB$$81275b04 (in module shedlock.example) because module shedlock.example does not export net.javacrumbs.shedlockexample to module spring.core
    at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
    at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:589)
    at java.base/java.lang.reflect.Field.checkAccess(Field.java:1075)
    at java.base/java.lang.reflect.Field.set(Field.java:778)

    Spring is trying to access classes from our project and we get the error 'module shedlock.example does not export net.javacrumbs.shedlockexample to module spring.core'. Let's do what the JVM tells us and export our only package
    exports net.javacrumbs.shedlockexample to spring.core;

    Still no luck: We get 'module shedlock.example does not "opens net.javacrumbs.shedlockexample" to module spring.core'. We have exported the package for compile time, but not for reflection. Error messages from the module system have been great so far, but they are a bit confusing here. Our code compiles, but at run time, the module system complains that we have not exported our package. Export should only affect compile time, so why are we getting this error at run time?

    The reason is simple: Spring uses CGLIB to generate a subclass of our class, so the difference between compile time and run time is a bit fuzzy. According to the spec, “A normal module grants reflective access to types in only those packages which are explicitly exported or explicitly opened (or both).” Is a class generated in run time a reflective access? Let's try to replace export by open. It still works. A few exceptions later, we end up with a working module declaration.
    module shedlock.example {
        requires spring.context;
        requires spring.jdbc;
        requires slf4j.api;
        requires shedlock.core;
        requires shedlock.spring;
        requires HikariCP;
        requires shedlock.provider.jdbc.template;
        requires java.sql;
        opens net.javacrumbs.shedlockexample to spring.core, spring.beans, spring.context;
    }

  7. Cool, but still not perfect. Imagine that you have more packages in your project and you want to use Spring, Jackson, or other reflection-using libraries in most of them. Opening them one by one sounds like a lot of work. Luckily, we can open the whole module.

  8. Open the module by adding the open keyword.
    open module shedlock.example {
        requires spring.context;
        requires spring.jdbc;
        requires slf4j.api;
        requires shedlock.core;
        requires shedlock.spring;
        requires HikariCP;
        requires shedlock.provider.jdbc.template;
        requires java.sql;
    }

    Let's quote the spec again. “An open module, with the open modifier, grants access at compile time to types in only those packages which are explicitly exported, but grants access at run time to types in all its packages, as if all packages had been exported.” That's exactly what we need.

We are done! It's been an interesting exercise, so what have we learned? First of all, it's possible to migrate your project to modules. Is it worth it? Most likely not. I would recommend waiting until the tools and libraries are ready. It's fun to be on the bleeding edge, but for a production project, I would give it a few months until all the tooling and libraries are Java 9-ready and all the serious bugs are fixed. And maybe a bit more to figure out how Jigsaw fits into the whole ecosystem.

You can see all the steps in the example project.

 

 

 

 

Top