BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Painlessly Migrating to Java Jigsaw Modules - a Case Study

Painlessly Migrating to Java Jigsaw Modules - a Case Study

Leia em Português

Key Takeaways

  • Implementing applications in a modular fashion encourages good design practices, such as separation of concerns and encapsulation.
  • The Java Platform Module System (JPMS) lets developers define what the application’s modules are, how they are to be used by other modules, and which other modules they depend upon.
  • It is possible to add JPMS module definitions to applications that were already using a different system to define the application’s modules, e.g. Maven modules or Gradle subprojects.
  • The JDK comes with tools to help developers migrate existing code to JPMS.
  • Application code can still have dependencies upon pre-Java-9 libraries, these jar files are treated as a special "automatic" modules. This makes it easier to migrate gradually to Java 9.

This article demonstrates a case study of the changes needed by a real application in order to make use of the new Java Platform Module System (JPMS).  Note that you do not need to do this in order to use Java 9, but an understanding of the module system (often referred to as Jigsaw) will no doubt, over time, become an important skill for Java developers.

I’ll walk through the steps I took to refactor a Java 8 application, which was already organized in a modular fashion, to use the new Java module system.

Download Java 9

Firstly download and install the latest version of JDK 9. This is currently an early access release (this article uses 9-ea+176). Until the impact of Java 9 on your system is understood, you probably don’t want this to be the default Java version. Instead of updating $JAVA_HOME to point to the new installation, you may wish to create a new environment variable $JAVA9_HOME instead.  I’ll be using this approach throughout this article.

There are plenty of other tutorials that talk about some of the steps you may need to take in order to use Java 9. We will be confining our discussion to the modularization component, but you may want to check out Oracle’s Migration Guide for more information.

Modularity

The feature you’ll hear most about in the context of Java 9 is Project Jigsaw, the introduction of modules to Java. There are lots of tutorials and articles on exactly what this is or how it works, this article will cover how you can migrate your existing code to use the new Java Platform Module System.

Many developers are surprised to learn that they don’t have to add modularity to their own code in order to use Java 9.  The encapsulation of internal APIs is probably one of the features that concerns developers when considering Java 9, but just because that part of Jigsaw may impact developers does not mean that developers need to fully embrace modularity in order to make use of Java 9.

If you do wish to take advantage of the Java Platform Module System (JPMS), there are tools to help you, for example the jdeps dependency analyzer, the Java compiler and your own IDE.

I won’t talk about how to decompose your specific application into modules, this can be hard to achieve if it wasn’t done at the start - (case in point - the many years it took to organize the JDK into modules!) Instead, I’m going to assume your application is already structured into smaller pieces, possibly using Maven modules or Gradle subprojects, or maybe by using sub projects or modules in the IDE.

You’ll find many tutorials, like the Jigsaw quick-start guide, which assume a project structure that looks something like Figure 1.

Figure 1

This structure has a single src directory and a single test directory, and then all modules are subfolders of those.  This is a deviation from the familiar structure of Maven or Gradle, where each module contains its own src and test directory.  Thankfully you do not have to rearrange your whole application that way (with the associated headache of getting your build tools to understand this updated structure).  You can continue with your Maven/Gradle layout and adopt JPMS, as long as you understand the difference in structure in many tutorials. The key is to know which directories are your application’s source roots - in a Maven/Gradle layout, it’s the src and test folders.

Figure 2

The first thing you’ll need to do is place a module-info.java file into the source root directory of your module, defining the name of your module.  You can create this manually, or some IDEs can create this for you. Figure 3 shows a simple module-info.java for my service module:

Figure 3

Note how this lives in the src directory, alongside the folder that’s the root of your directory structure.

Now compile the project in your IDE or navigate to this src directory on the command line and compile the module with something like:

> "%JAVA9_HOME%"\bin\javac -d ..\mods\service module-info.java com\mechanitis\demo\sense\service\*.java com\mechanitis\demo\sense\service\config\*.java

At this point you’ll see a lot of compilation errors (see Figure 4).

Figure 4

There are 27 errors, which may come as a surprise - this project was compiling perfectly and running, and all we did was add a module-info.java file, and now it no longer compiles.  The reason is that we now have to be much more explicit about the modules we want our own module to use.  These required modules can be: modules from the JDK; other modules that we ourselves have created; or modules from external dependencies, which at this point in time will likely be automatic modules.

Here, the jdeps dependency analyzer can help us to identify the modules we’ll need to declare in our module-info.java.  In order to successfully run this, you need a couple of things:

  1. A jar file of your (pre-JPMS) module code, or a directory containing the (pre-JPMS) class files. Note that if you just tried to compile your source files in the last step, you won’t have any working class files at this point. You may need to remove the module-info.java and recompile.
  2. The classpath for your module code.  If you’re used to running inside an IDE, especially if you’re using Maven or Gradle to manage your dependencies, this can be challenging to locate or create. In IntelliJ IDEA, you may be able to identify a working classpath to use by looking at the details inside the run window when you run the application or tests. In Figure 5, I just had to scroll to the right and copy the rest of the line in blue.

Figure 5

Now we have what we need to run jdeps, we’ll use the Java 9 version with appropriate flags:

> "%JAVA9_HOME%"\bin\jdeps --class-path %SERVICE_MODULE_CLASSPATH% out\production\com.mechanitis.demo.sense.service

This last argument is the directory containing the class files for my service module.  When I run this, I get the output in Figure 6.

split package: javax.annotation [jrt:/java.xml.ws.annotation, C:\.m2\...\javax.annotation-api-1.2.jar]

com.mechanitis.sense.service -> java.base
com.mechanitis.sense.service -> java.logging
com.mechanitis.sense.service -> C:\.m2\...\javax-websocket-server-impl-9.4.6.jar
com.mechanitis.sense.service -> C:\.m2\...\javax.websocket-api-1.0.jar
com.mechanitis.sense.service -> C:\.m2\...\jetty-server-9.4.6.jar
com.mechanitis.sense.service -> C:\.m2\...\jetty-servlet-9.4.6.jar
   com.mechanitis.sense.service    -> com.mechanitis.sense.service.config  com.mechanitis.sense.service
   com.mechanitis.sense.service    -> java.io                              java.base
   com.mechanitis.sense.service    -> java.lang                            java.base
   com.mechanitis.sense.service    -> java.lang.invoke                     java.base
   com.mechanitis.sense.service    -> java.net                             java.base
   com.mechanitis.sense.service    -> java.nio.file                        java.base
   com.mechanitis.sense.service    -> java.util                            java.base
   com.mechanitis.sense.service    -> java.util.concurrent                 java.base
   com.mechanitis.sense.service    -> java.util.concurrent.atomic          java.base
   com.mechanitis.sense.service    -> java.util.function                   java.base
   com.mechanitis.sense.service    -> java.util.logging                    java.logging
   com.mechanitis.sense.service    -> java.util.stream                     java.base
   com.mechanitis.sense.service    -> javax.websocket                      javax.websocket-api-1.0.jar
   com.mechanitis.sense.service    -> javax.websocket.server               javax.websocket-api-1.0.jar
   com.mechanitis.sense.service    -> org.eclipse.jetty.server             jetty-server-9.4.6.jar
   com.mechanitis.sense.service    -> org.eclipse.jetty.servlet            jetty-servlet-9.4.6.jar
   com.mechanitis.sense.service    -> org.eclipse.jetty.websocket.server        javax-websocket-server-impl-9.4.6.jar
   com.mechanitis.sense.service    -> org.eclipse.jetty.websocket.server.deploy javax-websocket-server-impl-9.4.6.jar
   com.mechanitis.sense.service.config           -> java.lang              java.base
   com.mechanitis.sense.service.config           -> java.lang.invoke       java.base
   com.mechanitis.sense.service.config           -> javax.websocket        javax.websocket-api-1.0.jar
   com.mechanitis.sense.service.config           -> javax.websocket.server javax.websocket-api-1.0.jar

Figure 6

It warns me about a split package, which is when the same package name appears in two different modules or jar files - in this case, it’s because the same package appears in both java.xml.ws.annotation (from the JDK) and javax.annotation-api.jar. Then it tells me about all the packages my service module uses, and which module/jar file these packages live in.  I can use this information to create a module-info.java file that will work for my module:

module com.mechanitis.demo.sense.service {
   requires java.logging;
   requires javax.websocket.api;
   requires jetty.server;
   requires jetty.servlet;
   requires javax.websocket.server.impl;
}

java.base contains most of the basic JDK stuff, but I don’t need to include it, as it is included by default.  I do need to figure out what the automatic module names are for my external dependencies. 

Automatic modules

Java 9 and the JPMS are designed to take into account the fact that most code, particularly the common libraries that we all use, will likely not use JPMS (i.e. will not be fully modular, with module-info.java defining the dependencies and permissions) right from the start. To bridge the gap and ease migration, your modularised code can still have jar dependencies that are not true modules. Rather these are treated as automatic modules; special modules where you can still access all the packages inside the jar in exactly the same way you did originally, when you put that jar file on the classpath. The only tricky thing for us as developers using automatic modules is figuring out what its name is. By default, the name is more-or-less the jar file name, minus any version number.  So jetty-server-9.4.1.v20170120.jar yields an automatic module named jetty.server (note the use of dots instead of hyphens).

Note: The most up to date versions of Java 9 allow library developers to specify their automatic module name with a JAR-file manifest attribute, ‘Automatic-Module-Name’, so you may need to check the jar file itself to find out what its module name is.

Using Our New Module

Now my code compiles correctly.  We can now turn to the job of migrating another one of our modules to a JPMS module.  Let’s create an empty module-info.java for the user module, Figure 7 shows the new compiler error.

Figure 7

This is simple enough to fix, we just need to update the module-info.java file from the service module to allow other modules to access the service package, by exporting it:

module com.mechanitis.demo.sense.service {
   requires java.logging;
   requires javax.websocket.api;
   requires jetty.server;
   requires jetty.servlet;
   requires javax.websocket.server.impl;
  
   exports com.mechanitis.demo.sense.service;
}

Now recompiling yields a different error:

Error:(3, 33) java: package com.mechanitis.demo.sense.service is not visible

  (package com.mechanitis.demo.sense.service is declared in module com.mechanitis.demo.sense.service, but module com.mechanitis.demo.sense.user does not read it)

Which means we need to update the user module to require the service module:

module com.mechanitis.demo.sense.user {
   requires com.mechanitis.demo.sense.service;
}

Once that is done everything compiles and we can successfully run our user service.

Conclusion

We went through the process of refactoring an existing application to use the Java Platform Module System.  Although there are benefits to splitting an application into modules, migrating existing applications to JMPS is a non-trivial task and should only be attempted if the benefits are clear.

About the Author

Trisha Gee is a Java Champion and has developed Java applications for a range of industries, including finance, manufacturing, software and non-profit, for companies of all sizes.  She has expertise in Java high performance systems, is passionate about enabling developer productivity, and dabbles with open source development. She’s the Java Developer Advocate for JetBrains, which is the perfect excuse to live on the bleeding edge of Java technology.

Rate this Article

Adoption
Style

BT