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

Bookmarks

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

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

  • Recommended direction for introducing modules

    by Christo Zietsman,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Given a muiti-project Gradle build as an example. How would you start introducing modules?

    Would you start with the bottom most (core) projects or with the top most project(s).

    Certain projects may be fringe projects with few dependencies. Should these be tackled early or left till later?

  • Module names

    by Philippe Marschall,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    I'm a bit concerned with the modules names used.

    requires javax.websocket.api


    As far as I know that should be java.websocket. JEP 200 specifies that

    Standard modules, whose specifications are governed by the JCP, must have names starting with the string "java.".

    JSON-P went with "java.json", Bean Validation when with "java.validation". Both of them have been endorsed as good examples by the JPMS-lead. Stephen Colebourne proposed a different naming convention which seems to be followed by open source projects outside of the JCP like Guava and your application modules.


    requires javax.websocket.server.impl

    This seems really unfortunate to depend on an implementation as Jigsaw is about the encapsulation of internals. In addition this seems to be required for the classes in the "org.eclipse.jetty.websocket" package so the module name is really confusing and does not reflect the contents well.


    All of this would be less of an issue if Jigsaw would allow to define dependencies on a package instead of a module, a lesson OSGi learnt the hard way years ago.

  • Re: Recommended direction for introducing modules

    by Trisha Gee,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    I haven't had much of a chance to work with Gradle and Java 9 yet, as full support for Java 9 has only just been introduced in Gradle 4.1. This case study actually was a multi-module Gradle project initially, but when I migrated to Java 9 modules I realised I wanted to restructure the whole project - this is not a Gradle-specific problem but an issue I made for myself due to my deployment limitations.

    As with all questions like this, I think the answer is "it depends" (sorry!). I personally would want to tackle something simple with few dependencies early on to see how difficult the process is. Of course, you have to ask yourself what the value is for each module - if nothing is dependent upon your module, is there any value in turning it into a full-blown JPMS module?

    So I'd start simple and also check there was value in the migration for each module.

    Once you've tackled a couple of modules and are comfortable with the process, my instinct would be to start with the core modules and work outwards. Of course, this could get quite complicated as changes to the core may have an impact that ripples outwards.

    Personally, I'd probably wait to see how others are tackling the problem before taking the plunge with an important, complex application. Of course, someone has to be first...

  • Enterprise standards

    by Jan Nowak,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    I have a strange feeling that this tech will share the same fate of other designs done by the house of IBM, Oracle and the rest. First it will be forcefully used by Oracle, then after many attempts it will be recognized that it is too complicated and doesn't fit nicely with other technologies. Then there will be a huge refactoring and the version 2.0 will come out with Java 10. Just like EJB 1.x, 2.x were pretty much useless and only after huge push from the community the 3.x were somewhat usable.

  • Re: Module names

    by Trisha Gee,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    The module names situation may well improve now that automatic modules can define their module names without having to go all the way to being full JPMS modules. Automatic modules are a necessary evil in order to ease some of the pain of migration, and being able to define a better (or at least more standard) name for them will further ease this pain.

    Depending upon the *.impl* package is, I believe, part of this migration effort. Or it may even be required in this case as I am actually using websockets at a very low level - at least Jigsaw lets me be clear about this dependency (and also highlights my use of it, which should make the developer/architect question if it is needed at all - I see this exposure of what's being used a very useful part of Jigsaw).

    On the other hand, some of these "requires" might even go away by the time the "real" Java 9 comes out. Earlier versions of my module-info files for this project were MUCH more nasty, they needed to "require" pretty much every package that was used, whether directly or indirectly, and this seems to have got much better over the last 6 months of releases. This may improve further before the actual release of Java 9.

  • Re: Enterprise standards

    by Trisha Gee,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    It's difficult to tell at this point, but it's definitely true that developers are wary of adopting something that could require big changes to their architecture, especially if there are no clear gains. When I was working on library code, I definitely saw the need to encapsulate internal APIs, which is a big part of Jigsaw of course. But for enterprise developers, even Oracle's developers are not recommending refactoring existing code bases to use JPMS unless there's a real reason to do so.

    I guess we'll have to wait and see.

  • How to solve java.xml and jsr305 in the same module

    by Djeison Selzlein,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    I have java.xml.ws.annotation (from the JDK) and jsr305 in the same module-info and I get "javax.annotation from both java.annotation and jsr305", would you have any example or tip on how to handle this scenario?

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT