You’re tasked with moving an application to JPMS. What’s the command line?
April 14, 2020 | Download a PDF of this article
More quiz questions available here
If you have worked on our quiz questions in the past, you know none of them is easy. They model the difficult questions from certification examinations. The “intermediate” and “advanced” designations refer to the exams rather than to the questions, although in almost all cases, “advanced” questions will be harder. We write questions for the certification exams, and we intend that the same rules apply: Take words at their face value and trust that the questions are not intended to deceive you but to straightforwardly test your knowledge of the ins and outs of the language.
The objective here is to migrate an application developed using a version prior to Java SE 9 to Java SE 11, including top-down and bottom-up migration, and splitting a Java SE 8 application into modules for migration.
Imagine you’re tasked with migrating an application to the Java Platform Module System (JPMS). Assuming the following:
- There are two JAR files:
- The main class is named
app.Main and is in
app.jar uses features in
- You are told this is a bottom-up migration.
Which command line can be used to run the application during migration? Choose one.
Answer. First, let’s look at some background for the Java Platform Module System. Knowing what the system seeks to do will help you understand how to work with it.
JPMS, introduced in Java 9, improves some key architectural aspects of both Java and the JVM. Key among these are provisions for strong encapsulation at the level of individual modules (a module might be considered a reasonable approximation as a library in a JAR file) and better dependency management.
Strong encapsulation allows an individual module to protect its contents from external (mis)use in much the same way that private features of a class are protected, except that a module is typically also protected against the reflection mechanism. In contrast, private elements of a class are generally vulnerable to misuse through reflection.
Dependency management allows the developer, and the runtime system, to ensure that the necessary components are in place and also to avoid cluttering a runtime system with unnecessary components. Note that JPMS is not a build management system.
To be useful in the face of strong encapsulation, a module must permit access to some aspects of itself. This means that a configuration mechanism must be provided to indicate which parts should be accessible. The configuration follows the traditional security mantra: “That which is not expressly permitted is denied.” So there’s no need to say what’s inaccessible explicitly.
To support dependency management, it must be possible to determine what modules are required by any given module.
Now, from this background, what do you need to handle the situation that inevitably arises when an existing project that was created prior to the JPMS is migrated to use modules? One observation is that it’s unlikely to be possible to stop all development and make all the changes at once. Another is that it’s possible some third-party libraries might already be modularized or that some are not yet available in this form.
From this, two broad approaches seem possible (though reality might involve a mixture). You could have libraries that are modularized and want to use them in an application that isn’t yet modularized. The opposite of this would be that you’re busy making the application modular, but the libraries are not yet modularized. The former approach is referred to as bottom-up migration, and the latter is known as top-down migration. Either way, the core Java libraries are already in modules if you’re using Java 9 or newer.
Now, the question states that a bottom-up migration approach is to be used. This means that the core of the application is not modularized but other libraries—in this case,
The terms bottom up and top down don’t figure prominently in any core documentation, but they’re called out in the exam objectives and they are discussed in at least two public Oracle resources, including a discussion in “The State of the Module System,” by Mark Reinhold, and a presentation from JavaOne in 2016, “Advanced Modular Development.”
Both those resources were released significantly before the actual release of Java 9 and some of the details of JPMS changed, but the presentations describe the concepts of migration well.
For this question then, and likely for such a question in the real exam, you should know that bottom up implies:
- Some of the libraries in the system will be in modules and some will not.
- The main application will not (yet) be in a module.
- Over time, more of the code will be moved into modules.
- The main application will be moved to a module last, and at that point the migration will be complete.
Note that in practice, this might not progress in such a clean and predictable fashion.
Perhaps the first issue you must resolve to proceed with this quiz question is determining which, if any, of the JAR files contain modules and which are legacy JAR files.
Because the question doesn’t say, you can’t be 100% sure where you are in the migration (though there is a pretty reasonable interpretation on this point!). However, that is not a problem:
First, the “reasonable interpretation” is that the library is probably a module (otherwise, you really haven’t even started the migration) and that
app.jar (which you were told contains the
app.Main class) probably isn’t a module (otherwise, you have finished the migration).
The second reason it’s not a problem not to know where you are in the migration is that it turns out only one of the commands can possibly work no matter what stage you’re at, and it’ll only work if this interpretation is true. So, let’s proceed on the assumption that
lib.jar is modularized and
app.jar is not, and see where that leads.
To run this code, the JVM needs to be able to assemble some key information:
- Where to find regular classes
- Where to find modules
- What modules to load
- How to start the program
Working on the assumption that the main program is launched from a class loaded from a legacy JAR file, that JAR file must be on the classpath. For this to be true, you expect the
app.jar file to be found on the classpath (specified by one of the following:
--class-path). Options A, B, and C satisfy this condition. Option D puts the
app.jar file on the module path using -
p (which is the short form of
The next thing the JVM needs to know is where to load modules from. Again, for now you’re working on the assumption that
lib.jar is a module. This means there needs to be a module path that includes
lib.jar. Options A, C, and D satisfy this. Option B, however, puts
lib.jar on the classpath, which would be wrong if the assumption is correct.
A key difference between modular and legacy code is that in nonmodular environments, any class that’s on the classpath will be loaded if the running code calls for it, but in a modular environment, the JVM searches only in modules that are in the module graph.
In the module system, the JVM builds a graph of all the modules that it determines are required. You can imagine the graph starting with the module that contains the main class of the application. That module declares that it
requires some other modules, so these are added to the dependency graph. Next, the JVM adds all the modules that those modules
requires. This proceeds transitively to build a graph of all the necessary modules. Of course, the module path must actually lead to those modules too.
Now, to repeat: When the JVM is running and the code calls for a class, the JVM checks only in the modules on this graph. This is somewhat simplified, particularly because as stated, it can’t possibly work with a main class in a nonmodular JAR file. This is because that JAR file doesn’t contain any module descriptor and, therefore, it contains no
requires directives. The essence of the problem is that in the absence of
requires directives, how does it know which modules are needed? So, to run this example, you must somehow indicate the need for the
lib module; otherwise, when
app.Main needs a class from it, the JVM will throw a “class not found” error.
To resolve this error, you must explicitly add the equivalent of a
requires directive from the command line. This is done using the
--add-modules command-line option. This can be used with explicit module names (as exemplified in option B) or with one of three special values:
ALL-MODULE-PATH requires that all the modules that are on the module path be added to the root module set (and, hence, be used to build the graph of all necessary modules). In this case, the effect is to force the
lib module to be available. This command-line option is in option A and option B, and from this, you can conclude that if your assumption is correct, options C and D must be incorrect.
Well, it’s too soon to draw this conclusion with confidence. First, does the rest of option A or option B work? Can you prove that your assumption is correct?
Let’s continue investigating option A. The JVM still needs to know what the program’s entry point is. If
app.jar is a nonmodular JAR file on the classpath, you should simply specify the fully qualified name of the main class at the end of the
java command elements (unless, of course, there are arguments to the program itself). Option A correctly does this, and so if the base assumption is correct, option A will work correctly.
Option B uses
--add-modules and explicitly calls for the module
lib. That part is good, but the
lib.jar file is on the classpath, not the module path. While a regular JAR file can become a module by being placed on the module path, nothing on the classpath ever becomes a named module. So regardless of whether the assumption about
lib.jar being modular and
app.jar not being modular is correct, this use of
--add-modules cannot work. Previously, it was noted that
lib.jar being on the classpath didn’t fit expectations, but now you can see that the elements of option B are inconsistent with each other and, thus, option B is definitely incorrect.
Option C is very similar to option A. It adds
lib.jar to the module path using the shorthand
-p, and it adds
app.jar to the classpath (again, using the shorthand form
-cp). It also invokes the main entry point in the same way as option A. So, this is a promising setup that treats
lib.jar as modular and
app.jar as legacy. However, it does not use
--add-modules, and as a result the classes in
lib.jar won’t be available for loading and the program will fail. It doesn’t matter whether
lib.jar is modular or legacy: If a JAR file is on the module path, its classes are available only if that module makes it onto the module graph. From this, you can conclude that option C is definitely incorrect.
Option D tries to put
app.jar on the module path. Well, if
app.jar were a module containing a module-info, this would work. However, this would also imply that the migration is complete or is in a top-down mode. Because the question clearly indicates that a bottom-up migration is in progress, you must discard this idea. If a legacy
app.jar is added to the module path, it will be treated as an automatic module, but the command line is still lacking
--add-modules. Therefore, option D must be incorrect.
The correct answer is option A.