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:
- 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.
- 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.