Define modules and their dependencies, expose module content including for reflection. Define services, producers, and consumers.
Compile Java code, produce modular and non-modular jars, runtime images, and implement migration using unnamed and automatic modules.
jdeps
jmod
jlink
One of the most significant changes introduced in Java 9 was the Java Platform Module System (JPMS). But what exactly is JPMS, and why should we care about it?
Let’s start by understanding what a module is in this context.
A module in Java is like a section in a well-organized library. Each module has a clear label (its name) and contains specific books (Java packages). However, you can’t access any book without a library card (a dependency declaration) for that specific section.
In this example:
module com.myapp.core {
requires java.base;
exports com.myapp.core.api;
}
We’re declaring a module named com.myapp.core
. It requires the java.base
module (like having a library card for the basic Java section) and exports the com.myapp.core.api
package (making some of its books available to other modules).
While packages group related classes, modules take this concept further by grouping related packages and explicitly declaring their dependencies and exposed APIs.
Consider the benefits of using JPMS:
JPMS includes:
module-info.java
file: Defines your module, its dependencies, and what it exports.module
, requires
, exports
, opens
, uses
, and provides
.jlink
, for creating custom runtime images.Here’s a more complex sample module-info.java
file:
module com.myapp.core {
requires java.base;
requires java.sql;
exports com.myapp.core.api;
exports com.myapp.core.util to com.myapp.plugin;
opens com.myapp.core.model;
uses com.myapp.core.spi.Plugin;
provides com.myapp.core.spi.Logger
with com.myapp.core.logging.FileLogger;
}
This module declaration shows:
While you can still use the classpath, you’d miss out on the benefits of JPMS. The classpath is like a big, unorganized pile of books, while the module path is a well-organized library with controlled access and clear dependencies.
Even in small projects, modules can improve encapsulation and maintainability. Consider this small example:
// In module com.myapp.core
module com.myapp.core {
exports com.myapp.core.api;
}
package com.myapp.core.api;
public interface UserService {
User getUser(String id);
}
package com.myapp.core.internal;
class UserServiceImpl implements UserService {
public User getUser(String id) {
// Implementation
}
}
// In module com.myapp.web
module com.myapp.web {
requires com.myapp.core;
}
package com.myapp.web;
import com.myapp.core.api.UserService;
// import com.myapp.core.internal.UserServiceImpl; // This would cause a compile-time error
public class UserController {
private UserService userService;
// ...
}
In this example, the web
module can only access the api
package of the core
module, not its internal implementation.
Modules provide tools to enforce and express the architecture of your system at the language and JVM level.
Consider this: Would you rather have a big box of unsorted LEGO bricks or neatly organized sets with clear instructions? Both approaches can build amazing things, but one makes the process much smoother and less error-prone.
Now that we’ve got a grasp on what modules are and why they’re useful, let’s dive into the different types of modules in JPMS. Just like how not all books in a library are created equal, not all modules are the same either. JPMS introduces three types of modules:
Named modules are like the properly cataloged books in our library, with a clear title, author information, and a spot on the shelf. In Java terms, a named module is defined by a module-info.java
file at the root of your module.
Here’s an example of the content of this file:
module com.myapp.core {
requires java.base;
exports com.myapp.core.api;
}
This module-info.java
file is the ID card of your module. It gives your module a name (com.myapp.core
in this case), lists its dependencies (requires java.base
), and declares what parts of itself it’s willing to share with other modules (exports com.myapp.core.api
).
Named modules are the most powerful and flexible type of module. They give you full control over your module’s dependencies and what it exposes to the outside world. If you’re starting a new project or refactoring an existing one to use JPMS, named modules are what you’ll be working with most of the time.
But what about all those third-party libraries that haven’t been modularized? This is where automatic modules come in. They’re like the books in our library that don’t have a proper catalog entry yet, but we still want to be able to check them out.
When you put a non-modular JAR file on the module path, the Java runtime automatically treats it as a module. This module is called an automatic module.
In this example:
module com.myapp.core {
requires java.base;
requires commons.lang; // This is an automatic module
exports com.myapp.core.api;
}
commons.lang
is an automatic module. We can require it just like we would a named module, even though it doesn’t have a module-info.java
file.
But how does Java determine the name of an automatic module? Well, the process goes something like this:
Automatic-Module-Name
entry in the JAR’s MANIFEST.MF
file. If it’s there, that’s the module name.For example:
commons-lang3-3.14.jar
becomes the automatic module commons.lang3
guava-33.2.1-jre.jar
becomes guava
You can see this in action using the jar
command:
$ jar --describe-module --file=guava-28.0-jre.jar
No module descriptor found. Derived automatic module.
Automatic module name: guava
...
Last but not least, we have unnamed modules. These are like a miscellaneous box in the library where all loose papers and bookmarks that don’t fit anywhere else are stored.
When you run your application on the classpath (not the module path), all the code that is not part of a named module or automatic module ends up in one big unnamed module. This unnamed module reads all other modules, which means it can access all packages exported by all other modules.
In this command:
java -cp app.jar:lib/* com.myapp.Main
app.jar
and everything in the lib
directory will be part of the unnamed module.
The unnamed module is important for backward compatibility, allowing existing Java code to run without modification on Java 9 and later versions. However, code in the unnamed module doesn’t get the benefits of strong encapsulation that named modules provide.
Here’s a quick comparison:
Module Type | Has module-info.java | On Module Path | On Classpath |
---|---|---|---|
Named | Yes | Yes | No |
Automatic | No | Yes | No |
Unnamed | No | No | Yes |
Understanding these different types of modules is key to working effectively with JPMS. Named modules give you the most control and benefits, automatic modules help you integrate non-modular libraries, and unnamed modules ensure your existing code keeps running.
Here’s a diagram that summarizes the types of modules:
┌─────────────────────────────────────────────────────────┐
│ Java Module Types │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Named │ │ Automatic │ │ Unnamed │ │
│ │ Module │ │ Module │ │ Module │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ - Explicit │ │ - No module-│ │ - No module-│ │
│ │ module- │ │ info.java │ │ info.java │ │
│ │ info.java │ │ - On module │ │ - Not on │ │
│ │ - Defined │ │ path │ │ module │ │
│ │ exports │ │ - Name │ │ path │ │
│ │ - Defined │ │ derived │ │ - Implicitly│ │
│ │ requires │ │ from JAR │ │ exports │ │
│ │ │ │ filename │ │ all pkgs │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Use for: Use for: Use for: │
│ - New Java 9+ - Legacy JARs - Class path │
│ projects - Transition - Compatibility │
│ - Full module - Third-party - Non-modular │
│ control libraries code │
│ │
└─────────────────────────────────────────────────────────┘
Key Points:
- Named modules offer full control over exports and requires
- Automatic modules bridge between modular and non-modular code
- Unnamed modules provide backwards compatibility
Now that we’ve explored the types of modules, let’s create one ourselves. Creating a module is like setting up a new section in your library. We need to decide on its structure, what books (classes) it will contain, and what rules (module-info.java
) will govern its use.
The directory structure for a module is straightforward, but it’s important to get it right. Here’s a typical layout:
mymodule/
├── src/
│ ├── module-info.java
│ ├── com/
│ │ └── mycompany/
│ │ └── mymodule/
│ │ ├── MyClass.java
│ │ └── AnotherClass.java
│ └── resources/
│ └── config.properties
Let’s break it down:
mymodule/
) is typically named after your module.src/
directory. This is where all our source files live.module-info.java
file sits directly under src/
. This is important, it defines our module.com.mycompany.mymodule
) is represented by nested directories under src/
.This structure might look familiar, it’s very similar to how we organized non-modular Java projects. The key difference is the presence of the module-info.java
file.
Now, let’s look at what goes inside our Java files. Here’s an example of what MyClass.java
might look like:
package com.mycompany.mymodule;
public class MyClass {
public void doSomething() {
System.out.println("MyClass is doing something!");
}
}
This is just a regular Java class. However, the package
declaration at the top is important, as it determines where this class fits in our module’s structure.
Here’s AnotherClass.java
:
package com.mycompany.mymodule;
public class AnotherClass {
private MyClass myClass = new MyClass();
public void doSomethingElse() {
System.out.println("AnotherClass is doing something else!");
myClass.doSomething();
}
}
Again, this is a standard Java class. Notice how it can use MyClass
without any special import because they’re in the same package.
module-info.java
FileThis file is what turns our collection of packages into a proper module. It’s like the card catalog for our library section, defining what’s available and what’s needed.
Here’s what a basic module-info.java
might look like:
module com.mycompany.mymodule {
exports com.mycompany.mymodule;
requires java.base;
}
Let’s break this down:
module com.mycompany.mymodule
: This declares our module name. By convention, this often matches our root package name.exports com.mycompany.mymodule
: This line makes our package accessible to other modules. Without this, our classes would be hidden from the outside world.requires java.base
: This declares a dependency on the base Java module. Actually, this line is optional, all modules implicitly require java.base
.But we can get more sophisticated. Let’s say we want to use a logging framework and provide a service:
module com.mycompany.mymodule {
exports com.mycompany.mymodule;
requires java.base;
requires org.apache.logging.log4j;
provides com.mycompany.service.MyService
with com.mycompany.mymodule.MyServiceImpl;
uses com.mycompany.service.AnotherService;
}
Here, we’re requiring the Log4j module, providing an implementation of MyService
, and declaring that we’ll be using AnotherService
(which will be provided by some other module).
Isn’t this module-info.java
file a bit like the nutrition label on a food package? It tells you what’s inside (exports), what it needs (requires), what it can do for you (provides), and what it expects to use (uses).
One thing to watch out for: if you’re using an IDE, make sure it’s set up to work with Java modules. Some IDEs might create a module-info.java
file automatically when you create a new module, while others might require you to create it manually.
For example, assuming there’s a Main
class like this:
package com.mycompany.mymodule;
public class Main {
public static void main(String[] args) {
System.out.println("Main class is running!");
MyClass myClass = new MyClass();
myClass.doSomething();
AnotherClass anotherClass = new AnotherClass();
anotherClass.doSomethingElse();
}
}
Here’s how you might compile and run this module from the command line:
javac -d mods/com.mycompany.mymodule
src/module-info.java
src/com/mycompany/mymodule/*.java
java --module-path mods -m com.mycompany.mymodule/com.mycompany.mymodule.Main
The first command compiles our module, and the second runs it. Notice how we specify the module path (--module-path mods
) and the main class (-m com.mycompany.mymodule/com.mycompany.mymodule.Main
). Also, for both javac
and java
commands, you can use the shorter -p
option instead of --module-path
.
Now that we’ve set up our module’s structure, let’s review the module declaration itself in the module-info.java
file.
The exports
keyword is used to make our module’s packages accessible to other modules. Here’s an example:
module com.mycompany.mymodule {
exports com.mycompany.mymodule.api;
}
In this example, we’re making the com.mycompany.mymodule.api
package available for other modules to use. Any public types in this package can now be accessed by other modules that require it.
But what if we want to be more selective? Java modules allow for that too:
module com.mycompany.mymodule {
exports com.mycompany.mymodule.api to com.mycompany.anothermodule, com.mycompany.yetanothermodule;
}
This declaration exports the package, but only to the specified modules. It’s a way to control access to your module’s internals.
Modules add an extra layer of access control on top of Java’s existing public
, protected
, package-private, and private
modifiers. Here’s how it works:
protected
, package-private, private
) still apply as usual.Let’s see this in action:
// In module com.mycompany.mymodule
module com.mycompany.mymodule {
exports com.mycompany.mymodule.api;
}
// In package com.mycompany.mymodule.api
public class PublicAPI {
public void doSomething() { ... }
}
// In package com.mycompany.mymodule.internal
public class InternalClass {
public void doSomethingElse() { ... }
}
// In another module
import com.mycompany.mymodule.api.PublicAPI; // This works
import com.mycompany.mymodule.internal.InternalClass; // This fails!
Even though InternalClass
is public, it can’t be accessed from outside the module because its package is not exported. It’s like having a public reading room that’s only accessible to staff members.
The requires
keyword is how we declare dependencies on other modules. Consider this example:
module com.mycompany.mymodule {
requires java.sql;
}
This tells the Java runtime that our module depends on the java.sql
module.
But what if we’re building on top of another module and want to expose its functionality through our module? That’s where requires transitive
comes in:
module com.mycompany.mymodule {
requires transitive java.sql;
}
Now, any module that requires our module will automatically require java.sql
too. It’s like saying “if you’re checking out books from our section, you’ll also get a library card for the SQL section.”
This is particularly useful when you’re creating an API that builds on another module. Your users don’t need to know about the underlying dependencies, they just require your module, and everything else comes along.
Sometimes, we need to allow reflective access to a package at runtime, even if it’s not exported. This is where the opens
keyword comes in handy. Consider this example:
module com.mycompany.mymodule {
opens com.mycompany.mymodule.internal;
}
This allows reflective access to all types of the package at runtime, but doesn’t allow compile-time access from other modules.
You can also open a package to specific modules:
module com.mycompany.mymodule {
opens com.mycompany.mymodule.internal to com.mycompany.testmodule;
}
This is particularly useful for testing frameworks or dependency injection libraries that need to access your module’s internals.
If you need to open all packages in your module for reflection, you can use the open
keyword on the module declaration itself:
open module com.mycompany.mymodule {
// module declarations
}
Isn’t this module system a bit like setting up security clearances in a classified library? You have public sections (exported packages), restricted sections (non-exported packages), special access privileges (opens), and even transitive security clearances (requires transitive). It gives you fine-grained control over who can access what in your codebase.
Here’s a more complex example putting it all together:
module com.mycompany.mymodule {
exports com.mycompany.mymodule.api;
exports com.mycompany.mymodule.util to com.mycompany.partnermodule;
requires java.base; // This is implicit
requires transitive com.mycompany.commonmodule;
requires org.apache.logging.log4j;
opens com.mycompany.mymodule.internal to org.junit.jupiter.api;
}
This module exports one package globally and another to a specific module, requires several modules (one transitively), and opens a package for testing.
Now that we’ve explored creating our own modules and services, let’s look at the modules that come built into the Java platform. These built-in modules provide essential services and resources for everything else to build upon.
Modules that start with java
are the core modules of the Java SE Platform. These modules contain the fundamental APIs that most Java applications rely on.
Here are some of the most commonly used java
modules:
java.base
: This is the foundational module of the Java SE Platform. It’s automatically required by all other modules, just like how every section of a library relies on basic organizational principles.
// You don't need to explicitly require java.base
module com.mycompany.app {
// java.base is implicitly required
}
java.sql
: Provides the API for accessing and processing data stored in a data source (usually a relational database) using the Java programming language.
module com.mycompany.app {
requires java.sql;
}
java.xml
: Contains the APIs for processing XML.
module com.mycompany.app {
requires java.xml;
}
java.desktop
: Defines the APIs for creating rich desktop applications, including AWT and Swing.
module com.mycompany.app {
requires java.desktop;
}
java.logging
: Provides the classes and interfaces of the Java Logging API.
module com.mycompany.app {
requires java.logging;
}
These java
modules provide the core functionality that most Java applications rely on. They’re stable, well-documented, and form the backbone of the Java ecosystem.
Modules that start with jdk
are also part of the Java Development Kit, but they’re not considered part of the core Java SE Platform specification. They provide additional tools and APIs that are useful for certain types of systems but aren’t necessary for every application.
Here are some examples of jdk
modules:
jdk.httpserver
: Provides a simple HTTP server API.
module com.mycompany.app {
requires jdk.httpserver;
}
jdk.jshell
: Contains the JShell API, which allows you to create an interactive Java shell.
module com.mycompany.app {
requires jdk.jshell;
}
jdk.security.auth
: Provides implementations of the javax.security.auth.* interfaces.
module com.mycompany.app {
requires jdk.security.auth;
}
It’s important to note that while java
modules are guaranteed to be available in all Java SE implementations, jdk
modules might not be available. They’re part of the JDK but not part of the Java SE specification. This means that if you’re using a jdk
module, your code might not be portable across all Java SE implementations.
Here’s an example of how you might use both types of modules:
module com.mycompany.app {
requires java.base; // This is implicit
requires java.sql; // For database operations
requires java.logging; // For logging
requires jdk.httpserver; // To create a simple HTTP server
exports com.mycompany.app.api;
}
In this module declaration, we’re using both java
and jdk
modules. We’re relying on core Java functionality for database operations and logging, but we’re also using the JDK’s simple HTTP server for some additional functionality.
While IDEs are great for productivity, understanding how to use javac
and java
commands is important. It’s like knowing how to cook a meal from scratch instead of just reheating pre-made dishes.
There are several good reasons to learn how to compile and run Java code from the command line:
Think of it as learning to change a tire. You might not need to do it often, but when you do, you’ll be glad you know how.
javac
Let’s start with the basics. Here’s how you compile a simple Java file:
javac MyClass.java
This compiles MyClass.java
in the default package. But what about when you have packages?
javac com/mycompany/myapp/MyClass.java
This compiles MyClass.java
in the com.mycompany.myapp
package.
Typing out every file name can get tedious. Thankfully, you can use wildcards:
javac com/mycompany/myapp/*.java
This compiles all .java
files in the com/mycompany/myapp
directory.
Often, we want to keep our source files separate from our compiled classes. The -d
option lets us specify an output directory:
javac -d bin com/mycompany/myapp/*.java
This compiles all .java
files and puts the resulting .class
files in the bin
directory, maintaining the package structure.
When our code depends on external libraries, we need to tell the compiler where to find them. That’s where the classpath option comes in:
javac -cp lib/dependency.jar com/mycompany/myapp/*.java
This tells the compiler to look for classes in dependency.jar
while compiling our code. You can specify multiple JAR files or directories by separating them with a colon (:
) on Unix-like systems or a semicolon (;
) on Windows.
Speaking of JAR files, here’s how you compile against multiple JARs:
javac -cp lib/dependency1.jar:lib/dependency2.jar com/mycompany/myapp/*.java
This compiles our code using classes from both dependency1.jar
and dependency2.jar
.
When working with modules, we need to specify the module path:
javac --module-path mods -d out src/module-info.java src/com/mycompany/myapp/*.java
This compiles our module, looking for dependencies in the mods
directory and outputting to the out
directory.
java
Once you’ve compiled a class, you can run it with:
java com.mycompany.myapp.MyClass
This runs MyClass
in the com.mycompany.myapp
package. Note that we don’t include the .class
extension.
Just like with compilation, we might need to specify a classpath when running our code:
java -cp bin:lib/dependency.jar com.mycompany.myapp.MyClass
This runs MyClass
, looking for classes in both the bin
directory and dependency.jar
.
To run a modular application, we use the --module-path
and -m
options:
java --module-path out:mods -m com.mycompany.myapp/com.mycompany.myapp.MyClass
This runs MyClass
from the com.mycompany.myapp
module, looking for modules in the out
and mods
directories.
jar
Often, you’ll want to package your application into a JAR file. The jar
command helps you do this:
jar -cvf myapp.jar -C bin .
Let’s break this down:
-c
or --create
: Create a new archive-v
or --verbose
: Generate verbose output-f
or --file
: Specify the archive file name-C bin
: Change to the bin
directory before adding files.
: Add all files in the current directory (which is now bin
)With a modular application, you can package your module into a modular JAR:
jar --create --file mods/com.mycompany.myapp.jar --main-class com.mycompany.myapp.MyClass -C out .
This creates a modular JAR file, optionally specifying MyClass
as the main class.
Here’s a more complex example that ties it all together:
# Compile the module
javac --module-path mods -d out \
src/module-info.java \
src/com/mycompany/myapp/*.java
# Package the module
jar --create --file mods/com.mycompany.myapp.jar \
--main-class com.mycompany.myapp.MyClass \
-C out .
# Run the module
java --module-path mods \
-m com.mycompany.myapp/com.mycompany.myapp.MyClass
This sequence compiles the module, packages it into a JAR, and then runs it.
By convention, we store the compiled modules in a mods
directory. When we use --module-path mods
in our Java commands, we’re telling Java to look for modules in this mods
directory.
Up until now, we’ve been working with a single module, which is like organizing a single shelf in our library. But real-world applications often require multiple modules working together, more akin to organizing an entire library with multiple sections.
Designing a multi-module application is like planning the layout of a large library. You need to think about how different sections (modules) will interact, what resources they’ll share, and how to organize them for easy navigation and maintenance.
Here’s a simple example of a multi-module application structure:
myapp/
├── core/
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── com/mycompany/core/
│ └── test/
├── api/
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── com/mycompany/api/
│ └── test/
└── app/
└── src/
├── main/
│ └── java/
│ ├── module-info.java
│ └── com/mycompany/app/
└── test/
In this structure:
core
contains the core functionality and domain logicapi
defines the public interfaces for your applicationapp
is the main application that ties everything togetherWhen working with multiple modules, it’s important to understand the dependencies between them.
Let’s look at how we might define the dependencies for our example:
// core/src/main/java/module-info.java
module com.mycompany.core {
exports com.mycompany.core;
}
// api/src/main/java/module-info.java
module com.mycompany.api {
requires com.mycompany.core;
exports com.mycompany.api;
}
// app/src/main/java/module-info.java
module com.mycompany.app {
requires com.mycompany.core;
requires com.mycompany.api;
}
In this setup, both api
and app
depend on core
, and app
also depends on api
. This creates a hierarchy of dependencies that impacts how you develop and maintain your application:
core
can affect both api
and app
.api
can affect app
, but not core
.app
don’t directly affect the other modules.However, this dependency structure helps enforce a clean architecture, preventing lower-level modules from depending on higher-level ones.
When organizing code across modules, think about separation of concerns and information hiding. Each module should have a clear, focused purpose, and should only expose what’s necessary for other modules to use.
Here’s an example of how you might organize some classes:
// In core module
public class User { ... }
public class UserService { ... }
// In api module
public interface UserAPI { ... }
// In app module
public class UserController { ... }
The core
module defines the fundamental domain objects and services. The api
module defines the public interfaces that other parts of the application (or external systems) will use. The app
module contains the application-specific logic that ties everything together.
This organization allows you to change the internals of the core
module without affecting clients of the api
, as long as the api
remains stable.
That said, deciding on the right level of granularity for your modules can be challenging. Too few modules and you lose the benefits of modularization; too many and you introduce unnecessary complexity. Here are some best practices:
Single Responsibility Principle: Each module should have one, and only one, reason to change.
Encapsulation: Modules should hide their internals and expose only what’s necessary.
Stable Dependencies: Modules should depend on modules that are more stable than they are.
Reusability: If a set of functionality might be useful in other contexts, consider making it a separate module.
Size: While there’s no hard and fast rule, modules that are too large become unwieldy, while modules that are too small can lead to dependency hell. Aim for modules that can be reasonably understood and maintained by a small team.
Here’s an example of refactoring our earlier structure, using another approach to improve granularity:
myapp/
├── core/
│ ├── domain/
│ └── services/
├── api/
│ ├── internal/
│ └── public/
├── infrastructure/
│ ├── persistence/
│ └── messaging/
└── app/
├── web/
└── cli/
In this refactored structure:
core
into domain
and services
to separate entities from business logic.api
is divided into internal
(for use within the application) and public
(for external consumers).infrastructure
module to handle cross-cutting concerns.app
is split into web
and cli
for different user interfaces.This granularity allows for more focused modules, each with a clear responsibility, while still maintaining a manageable overall structure.
When working with multiple modules, communication between them becomes important. In Java, inter-module communication typically happens through well-defined APIs.
Here’s how you might set this up:
// In api module
module com.mycompany.api {
exports com.mycompany.api;
}
public interface UserService {
User getUser(String id);
void updateUser(User user);
}
// In core module
module com.mycompany.core {
requires com.mycompany.api;
provides com.mycompany.api.UserService
with com.mycompany.core.UserServiceImpl;
}
public class UserServiceImpl implements UserService {
public User getUser(String id) { ... }
public void updateUser(User user) { ... }
}
// In app module
module com.mycompany.app {
requires com.mycompany.api;
uses com.mycompany.api.UserService;
}
public class UserController {
@Inject
private UserService userService;
public void handleUserUpdate(String id, UserUpdateRequest request) {
User user = userService.getUser(id);
// Update user based on request
userService.updateUser(user);
}
}
In this setup:
api
module defines the UserService
interface.core
module provides an implementation of UserService
.app
module uses the UserService
without knowing about its implementation.This approach allows modules to communicate through well-defined interfaces, promoting loose coupling and making it easier to change implementations without affecting other modules.
As your application grows, you might encounter conflicts between modules. Here are some common conflicts and how to resolve them:
Version Conflicts: When two modules require different versions of the same dependency.
Solution: Use the requires
directive with a specific version, or use build tools like Maven or Gradle to manage versions.
module com.mycompany.moduleA {
requires com.fasterxml.jackson.databind;
}
module com.mycompany.moduleB {
requires com.fasterxml.jackson.databind@2.11.0;
}
Split Packages: When classes in the same package are spread across multiple modules. Solution: Refactor your code to ensure each package is contained within a single module.
Naming Conflicts: When two modules export the same package name. Solution: Rename one of the packages to ensure uniqueness across your application.
Cyclic Dependencies: When modules depend on each other in a circular manner. Solution: Introduce a new module that both can depend on, or use the Service Provider Interface (SPI) pattern.
// Before (cyclic dependency)
module com.mycompany.moduleA {
requires com.mycompany.moduleB;
}
module com.mycompany.moduleB {
requires com.mycompany.moduleA;
}
// After (using SPI)
module com.mycompany.api {
exports com.mycompany.api;
}
module com.mycompany.moduleA {
requires com.mycompany.api;
provides com.mycompany.api.ServiceA with com.mycompany.moduleA.ServiceAImpl;
}
module com.mycompany.moduleB {
requires com.mycompany.api;
uses com.mycompany.api.ServiceA;
}
To better understand the solution, let’s review services in more detail.
Let’s dive into one of the most powerful features of the Java Module System: services. Services allow us to create flexible, extensible applications by decoupling interfaces from their implementations.
In the context of the Java Module System, a service is a well-defined set of programming interfaces and classes that provide access to some specific application functionality or feature. It’s like a specialized department in our library that provides a specific service, say, book restoration.
The service model consists of three main components:
This separation allows for loose coupling between modules. The consumer doesn’t need to know about the specific implementation of the service, just the interface it uses.
Let’s start by declaring our Service Provider Interface. We’ll use the UserService
from the previous section as our sample service:
// In the api module
module com.mycompany.api {
exports com.mycompany.api;
}
package com.mycompany.api;
public interface UserService {
User getUser(String id);
void updateUser(User user);
}
This UserService
interface defines the contract for the user management service. Any module that implements this interface can provide user management functionality.
Now that we have our Service Provider Interface, we need a way to discover and load implementations of this service. This is where a Service Locator comes in. In Java 9 and above, we can use the ServiceLoader
class for this purpose.
Here’s how we might create a UserServiceLocator
:
// In the app module
module com.mycompany.app {
requires com.mycompany.api;
uses com.mycompany.api.UserService;
}
package com.mycompany.app;
import com.mycompany.api.UserService;
import java.util.ServiceLoader;
public class UserServiceLocator {
private static final ServiceLoader<UserService> loader = ServiceLoader.load(UserService.class);
public static UserService getUserService() {
return loader.findFirst().orElseThrow(() -> new IllegalStateException("No UserService implementation found"));
}
}
Let’s break this down:
ServiceLoader.load()
method to create a ServiceLoader
for our UserService
interface.getUserService()
method uses findFirst()
to get the first available implementation of UserService
.IllegalStateException
.Note the uses
directive in the module declaration. This tells the module system that this module will be using the UserService
service.
This approach provides several benefits:
app
module doesn’t need to know about the specific implementation of UserService
.UserService
without changing the consuming code.UserService
, extending the functionality of our application.Here’s how we might use this in our UserController
:
package com.mycompany.app;
public class UserController {
private final UserService userService;
public UserController() {
this.userService = UserServiceLocator.getUserService();
}
public void handleUserUpdate(String id, UserUpdateRequest request) {
User user = userService.getUser(id);
// Update user based on request
userService.updateUser(user);
}
}
In this setup, UserController
doesn’t need to know anything about how UserService
is implemented or where it comes from. It just uses the service locator to get an instance and then uses it.
However, we haven’t actually provided an implementation yet. Let’s do that now.
First, we’ll create an implementation of our UserService
:
// In the core module
package com.mycompany.core;
import com.mycompany.api.User;
import com.mycompany.api.UserService;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class UserServiceImpl implements UserService {
private final Map<String, User> users = new HashMap<>();
@Override
public User getUser(String id) {
return users.get(id);
}
@Override
public void updateUser(User user) {
if (user.getId() == null) {
user.setId(UUID.randomUUID().toString());
}
users.put(user.getId(), user);
}
}
Now, we need to tell the module system that this implementation provides the UserService
. We do this in the module-info.java
file of the core module:
module com.mycompany.core {
requires com.mycompany.api;
provides com.mycompany.api.UserService with com.mycompany.core.UserServiceImpl;
}
The provides ... with
clause tells the module system that this module provides an implementation of UserService
using the UserServiceImpl
class.
Now, when the ServiceLoader
in our UserServiceLocator
looks for implementations of UserService
, it will find and use this UserServiceImpl
.
This separation of interface and implementation gives us incredible flexibility. We could easily swap out our UserServiceImpl
for a different implementation without having to change any of the consuming code. Maybe one that uses a database instead of an in-memory map:
// In the core module
package com.mycompany.core;
import com.mycompany.api.User;
import com.mycompany.api.UserService;
import java.sql.*;
import java.util.UUID;
public class DatabaseUserServiceImpl implements UserService {
private static final String DB_URL = "jdbc:sqlite:users.db";
@Override
public User getUser(String id) {
String sql = "SELECT * FROM users WHERE id = ?";
// ...
}
@Override
public void updateUser(User user) {
String sql = "INSERT OR REPLACE INTO users(id, username, email, active) VALUES(?,?,?,?)";
// ...
}
}
Now, to use this new implementation, we only need to change the provides
clause in our module-info.java
file:
module com.mycompany.core {
requires com.mycompany.api;
requires java.sql; // We need this for JDBC
provides com.mycompany.api.UserService with com.mycompany.core.DatabaseUserServiceImpl;
}
That’s it! We’ve now swapped out our in-memory implementation for a database-backed one. The beauty of this approach is that we didn’t have to change any code in the UserController
or any other consuming classes. They’re still working with the UserService
interface, unaware that the underlying implementation has changed.
To add another implementation of the UserService
without replacing the existing one, we’ll first modify the core module to include both implementations:
// In the core module
module com.mycompany.core {
requires com.mycompany.api;
requires java.sql; // We need this for JDBC
provides com.mycompany.api.UserService with
com.mycompany.core.UserServiceImpl,
com.mycompany.core.DatabaseUserServiceImpl;
}
Now, the module system knows that there are two providers for UserService
.
Next, we can update the UserServiceLocator
to return all available implementations:
// In the app module
module com.mycompany.app {
requires com.mycompany.api;
uses com.mycompany.api.UserService;
}
package com.mycompany.app;
import com.mycompany.api.UserService;
import java.util.ServiceLoader;
import java.util.List;
import java.util.stream.Collectors;
public class UserServiceLocator {
private static final ServiceLoader<UserService> loader = ServiceLoader.load(UserService.class);
public static List<UserService> getUserServices() {
return loader.stream()
.map(ServiceLoader.Provider::get)
.collect(Collectors.toList());
}
}
This method returns a list of all available UserService
implementations.
Now, we can update the UserController
to use, for example, all available services:
package com.mycompany.app;
import com.mycompany.api.User;
import com.mycompany.api.UserService;
import java.util.List;
public class UserController {
private final List<UserService> userServices;
public UserController() {
this.userServices = UserServiceLocator.getUserServices();
}
public void handleUserUpdate(String id, UserUpdateRequest request) {
for (UserService userService : userServices) {
User user = userService.getUser(id);
// Update user based on request
userService.updateUser(user);
}
}
}
In this setup, UserController
will iterate through all available UserService
implementations and call the getUser
and updateUser
methods on each.
This service-oriented approach allows us to build more modular, flexible applications. A well-designed application using the Java Module System’s service feature can easily extend and modify its functionality over time.
As your modular application grows, you might need to inspect your modules to understand their structure, dependencies, and how they’re being resolved. Java provides several command-line tools to help with this.
Let’s start with describing a module. The java
command with the --describe-module
(or -d
) option allows us to see details about a specific module:
java --describe-module java.sql
This might output something like:
java.sql@18.0.3
exports java.sql
exports javax.sql
requires java.logging transitive
requires java.transaction.xa transitive
requires java.base mandated
requires java.xml transitive
uses java.sql.Driver
This tells us what packages the module exports, what other modules it requires, and what services it uses. It’s a quick way to get an overview of a module’s structure and dependencies.
You can also describe modules that aren’t part of the Java runtime. For example, if you have a com.mycompany.core
module in a JAR file:
java --module-path mods --describe-module com.mycompany.core
This might output:
com.mycompany.core@1.0
requires java.base mandated
requires com.mycompany.api
provides com.mycompany.api.UserService with com.mycompany.core.DatabaseUserServiceImpl
Sometimes, you might want to see all the modules available to your application. You can do this with the --list-modules
option:
java --list-modules
This will list all modules in the Java runtime. If you want to include your own modules, you can use:
java --module-path mods --list-modules
This will list both the Java runtime modules and any modules in the mods
directory (by convention, the directory where you store the compiled modules).
When you’re dealing with complex module dependencies, it can be helpful to see how the module system resolves these dependencies. You can do this with the --show-module-resolution
option:
java --show-module-resolution --module-path mods -m com.mycompany.app/com.mycompany.app.Main
This will show detailed information about how each module is resolved as the application starts up. It’s particularly useful for debugging issues with module dependencies.
jar
CommandWhile the java
command is great for describing modules at runtime, sometimes you’ll want to inspect a module without running it. The jar
command can help with this:
jar --describe-module --file mods/com.mycompany.core.jar
This might output something like:
com.mycompany.core jar:file:///.../mods/com.mycompany.core.jar/!module-info.class
requires java.base mandated
requires com.mycompany.api
provides com.mycompany.api.UserService with com.mycompany.core.DatabaseUserServiceImpl
This provides similar information to the java --describe-module
command, but it works directly on the JAR file without needing to set up the module path.
Here’s a more complex example. Let’s say we have a multi-module application and want to understand how all the pieces fit together:
# List all modules
java --module-path mods --list-modules
# Describe each of our modules
java --module-path mods --describe-module com.mycompany.api
java --module-path mods --describe-module com.mycompany.core
java --module-path mods --describe-module com.mycompany.app
# Show module resolution for our main application
java --show-module-resolution --module-path mods -m com.mycompany.app/com.mycompany.app.Main
# Describe our core module JAR file
jar --describe-module --file mods/com.mycompany.core.jar
By running these commands, you can get a comprehensive view of your application’s module structure, from the high-level list of all modules, through the details of each module, to the step-by-step resolution process when you run your application.
jdeps
jdeps
is a tool that provides powerful capabilities for analyzing and visualizing dependencies at both the module and class level. It allows you to examine the relationships between modules, packages, and classes, enabling you to make informed decisions about the structure and organization of your codebase.
To get started with jdeps
, let’s explore its basic syntax and common options. The general format for running jdeps
is as follows:
jdeps [options] path
Here, path
represents the location of the Java class files, JAR files, or directories you want to analyze. The options
allow you to customize the behavior of jdeps according to your specific needs.
These are some of the most important general options:
-dotoutput dir or --dot-output dir
: Specifies the destination directory for DOT file output. If this option is specified, then the jdeps
command generates one .dot file for each analyzed archive named archive-file-name.dot
that lists the dependencies, and also a summary file named summary.dot
that lists the dependencies among the archive files.-s or -summary
: Prints a dependency summary only.-v or -verbose
: Prints all class-level dependencies. This is equivalent to -verbose:class -filter:none
-verbose:package
: Prints package-level dependencies excluding, by default, dependences within the same package.-verbose:class
: Prints class-level dependencies excluding, by default, dependencies within the same archive.-apionly or --api-only
: Restricts the analysis to APIs, for example, dependences from the signature of public and protected members of public classes including field type, method parameter types, returned type, and checked exception types.-jdkinternals or --jdk-internals
: Finds class-level dependences in the JDK internal APIs. By default, this option analyzes all classes specified in the --classpath
option and input files unless you specified the -include
option. You can’t use this option with the -p
, -e
, and -s
options.-cp path, -classpath path, or --class-path path
: Specifies where to find class files.--module-path module-path
: Specifies the module path.--add-modules module-name[, module-name...]
: Adds modules to the root set for analysis.-q or -quiet
: Doesn’t show missing dependencies from -generate-module-info
output.These are the module dependence analysis options:
-m module-name or --module module-name
: Specifies the root module for analysis.--generate-module-info dir
: Generates module-info.java
under the specified directory. The specified JAR files will be analyzed. This option cannot be used with –dot-output or --class-path
options. Use the --generate-open-module
option for open modules.--generate-open-module dir
: Generates module-info.java
for the specified JAR files under the specified directory as open modules. This option cannot be used with the --dot-output
or --class-path
options.--check module-name [, module-name...]
: Analyzes the dependence of the specified modules. It prints the module descriptor, the resulting module dependences after analysis and the graph after transition reduction. It also identifies any unused qualified exports.--list-deps
: Lists the module dependences and also the package names of JDK internal APIs (if referenced). This option transitively analyzes libraries on class path and module path if referenced. Use --no-recursive
option for non-transitive dependency analysis.--list-reduced-deps
: Same as --list-deps
without listing the implied reads edges from the module graph. If module M1 reads M2, and M2 requires transitive on M3, then M1 reading M3 is implied and is not shown in the graph.--print-module-deps
: Same as –list-reduced-deps with printing a comma-separated list of module dependences. The output can be used by jlink –add-modules to create a custom image that contains those modules and their transitive dependences.--ignore-missing-deps
: Ignore missing dependences.These are the options to filter dependences:
-p pkg_name, -package pkg_name, or --package pkg_name
: Finds dependences matching the specified package name. You can specify this option multiple times for different packages. The -p
and -e
options are mutually exclusive.-e regex, -regex regex, or --regex regex
: Finds dependences matching the specified pattern. The -p
and -e
options are mutually exclusive.--require module-name
: Finds dependences matching the given module name (may be given multiple times). The --package
, --regex
, and --require
options are mutually exclusive.-f regex or -filter regex
: Filters dependences matching the given pattern. If give multiple times, the last one will be selected.-filter:package
: Filters dependences within the same package. This is the default.-filter:archive
: Filters dependences within the same archive.-filter:module
: Filters dependences within the same module.-filter:none
: No -filter:package
and -filter:archive
filtering. Filtering specified via the -filter
option still applies.--missing-deps
: Finds missing dependences. This option cannot be used with -p
, -e
and -s
options.And these are the options to filter classes to be analyzed:
-include regex
: Restricts analysis to the classes matching pattern. This option filters the list of classes to be analyzed. It can be used together with -p
and -e
, which apply the pattern to the dependencies.-P or -profile
: Shows the profile containing a package.-R or --recursive
: Recursively traverses all run-time dependences. The -R
option implies -filter:none
. If -p
, -e
, or -f
options are specified, only the matching dependences are analyzed.--no-recursive
: Do not recursively traverse dependences.
– I or --inverse
: Analyzes the dependences per other given options and then finds all artifacts that directly and indirectly depend on the matching nodes. This is equivalent to the inverse of the compile-time view analysis and the print dependency summary. This option must be used with the --require
, --package
, or --regex
options.--compile-time
: Analyzes the compile-time view of transitive dependencies, such as the compile-time view of the -R
option. Analyzes the dependences per other specified options. If a dependency is found from a directory, a JAR file or a module, all classes in that containing archive are analyzed.One of the primary use cases of jdeps
is analyzing module dependencies. By running jdeps
on a module, you can obtain a detailed report of the modules it depends on and the packages it uses from each module:
jdeps --module-path mods --add-modules com.example.myapp mymodule.jar
In this example, we specify the module path using the --module-path
option, which points to the directory containing the module definitions. The --add-modules
option is used to specify the main module of our application. Finally, we provide the path to the JAR file representing our module.
jdeps
will analyze the dependencies and generate a report that looks something like this:
mymodule.jar -> java.base
com.example.myapp -> java.io
com.example.myapp -> java.lang
com.example.myapp -> java.util
mymodule.jar -> java.desktop
com.example.myapp -> java.awt
com.example.myapp -> javax.swing
This report shows the dependencies of mymodule.jar
on other modules, such as java.base
and java.desktop
. It also lists the specific packages within mymodule.jar
that depend on packages from those modules.
In addition to module-level analysis, jdeps
allows you to examine dependencies at the class level. By running jdeps
on individual class files or directories containing class files, you can gain insights into the relationships between classes and packages.
Consider this example:
jdeps --verbose --class-path lib/* com/example/MyClass.class
Here, we use the --class-path
option to specify the classpath containing the required dependencies. The --verbose
flag provides more detailed output, showing the specific classes and members being used.
The class-level dependency report generated by jdeps
will include information like this:
com.example.MyClass -> java.lang.Object
com.example.MyClass -> java.lang.String
com.example.MyClass -> java.util.ArrayList
com.example.MyClass -> java.util.List
com.example.MyClass -> com.example.HelperClass
This report indicates that MyClass
depends on classes from the java.lang
and java.util
packages, as well as another class named HelperClass
from the same package.
jdeps
also provides the ability to generate comprehensive dependency reports in various formats. By using the --dot-output
option, you can generate a DOT file that visualizes the dependencies as a graph. This graphical representation can be extremely helpful in understanding complex dependency structures and identifying potential issues.
jdeps --dot-output docs --module-path mods --add-modules com.example.myapp mymodule.jar
In this example, jdeps
will generate a DOT file named after the module in the docs
directory. You can then use tools like Graphviz to render the DOT file into a visual graph.
Another useful feature of jdeps
is its ability to identify usage of internal APIs. The --jdk-internals
flag helps you detect and analyze the use of internal JDK APIs within your code. This is important because relying on internal APIs can lead to compatibility issues and unexpected behavior when upgrading to newer Java versions.
jdeps --jdk-internals --class-path lib/* com/example/MyClass.class
If MyClass
uses any internal JDK APIs, jdeps
will report them in the output, allowing you to take necessary actions to refactor or remove the dependencies on internal APIs.
If you provide the path to a JAR file or directory, jdeps
will recursively analyze all the classes within it and generate a comprehensive dependency report.
Here’s an example:
jdeps --recursive lib/myapp.jar
The --recursive
option ensures that jdeps
traverses all the classes and nested directories within the specified JAR file or directory, providing a complete picture of the dependencies.
jdeps
also offers a feature called recursive dependency analysis, which allows you to understand the transitive dependencies of your code. By analyzing not only the direct dependencies but also the dependencies of those dependencies, jdeps
helps you identify potential issues and conflicts.
Consider this example:
jdeps --recursive --module-path mods --add-modules com.example.myapp mymodule.jar
With the --recursive
flag, jdeps
will traverse the entire dependency graph, starting from the specified module or JAR file. It will generate a report that includes all the transitive dependencies, giving you a comprehensive view of your project’s dependency structure.
jmod
jmod
is a command-line tool that operates on a file format called JMOD. JMOD files are similar to JAR files in the way that they package Java classes, resources, and metadata. However, JMOD files are specifically designed to work with the module system and offer additional capabilities compared to traditional JAR files.
The JMOD file format is optimized for the JPMS and serves as a container for modular content. It encapsulates not only the compiled Java classes and resources but also includes module descriptors, native libraries, and other module-specific information. JMOD files have the .jmod
file extension and follow a specific directory structure to organize their contents.
JMOD files do not replace JAR files.
JAR (Java Archive) files are the most common and widely used format for packaging Java classes and resources. They are essentially zip files that contain compiled Java classes, metadata, and resources. Additionally, JAR files can be placed on the classpath for easy access by Java programs.
There are some key differences between JAR and JMOD files:
Modularity: JMOD files are primarily used for modular Java development, whereas JAR files are used for both modular and non-modular code.
Native code: JMOD files can include native libraries and executables, which is not possible with JAR files.
Versioning: JMOD files support module versioning through the --module-version
option, allowing for better version management.
Optimization: JMOD files are optimized for the module system and provide better performance and encapsulation compared to JAR files.
Usage: JAR files are widely used for distributing libraries and applications, while JMOD files are mainly used for creating and packaging modules.
So, when should you use JMOD files instead of JAR files? Here are some guidelines:
If you are developing a modular Java application using the JPMS, JMOD files are the recommended format for packaging your modules.
If your module requires native libraries or executables, JMOD files provide a convenient way to include them alongside your Java code.
If you need to create a custom runtime image or a JRE (Java Runtime Environment) specific to your application, JMOD files are used as the input to the jlink
tool for creating optimized runtime images.
However, if you are developing a non-modular Java application or a library that needs to be compatible with older versions of Java, JAR files are still the preferred choice.
One of the key advantages of JMOD files is their ability to include native libraries and executables. This is particularly useful for modules that have platform-specific dependencies or require native code integration. By packaging native libraries within the JMOD file, the module can be easily distributed and deployed across different platforms.
JMOD files also support versioning, allowing modules to specify their version information. This is important for managing dependencies and ensuring compatibility between various versions of modules. The module descriptor in the module-info.class
file can include version-related annotations to provide version metadata.
This is the basic syntax of the jmod
command:
jmod (create|extract|list|describe|hash) [options] jmod-file
The main operation modes are:
create
: Creates a new JMOD archive file.extract
: Extracts all the files from the JMOD archive file.list
: Prints the names of all the entries.describe
: Prints the module details.hash
: Determines leaf modules and records the hashes of the dependencies that directly and indirectly require them.These are the most important options:
--class-path path
: Specifies the location of application JAR files or a directory containing classes to copy into the resulting JMOD file.--cmds path
: Specifies the location of native commands to copy into the resulting JMOD file.--config path
: Specifies the location of user-editable configuration files to copy into the resulting JMOD file.--dir path
: Specifies the location where jmod puts extracted files from the specified JMOD archive.--dry-run
: Performs a dry run of hash mode. It identifies leaf modules and their required modules without recording any hash values.--hash-modules regex-pattern
: Determines the leaf modules and records the hashes of the dependencies directly and indirectly requiring them, based on the module graph of the modules matching the given regex-pattern. The hashes are recorded in the JMOD archive file being created, or a JMOD archive or modular JAR on the module path specified by the jmod hash
command.--help or -h
: Prints a usage message.--libs path
: Specifies location of native libraries to copy into the resulting JMOD file.--main-class class-name
: Specifies main class to record in the module-info.class
file.--module-version ersion
: Specifies the module version to record in the module-info.class
file.--module-path path or -p path
: Specifies the module path. This option is required if you also specify --hash-modules
.--target-platform platform
: Specifies the target platform.--version
: Prints version information of the jmod
command.Here are some examples that demonstrate the basic usage of each operation mode:
jmod create \
--class-path classes \
--main-class com.example.Main \
--module-version 1.0 \
--module-path lib \
mymodule.jmod
This command creates a new JMOD archive file named mymodule.jmod
. It includes classes from the classes
directory, sets the main class to com.example.Main
, specifies the module version as 1.0, and uses the lib
directory as the module path.
jmod extract --dir extracted_files mymodule.jmod
This command extracts all files from mymodule.jmod
into a directory named extracted_files
.
jmod list mymodule.jmod
This command prints the names of all entries in mymodule.jmod
.
jmod describe mymodule.jmod
This command prints the module details of mymodule.jmod
.
jmod hash --module-path lib \
--hash-modules java.base \
mymodule.jmod
This command determines leaf modules and records the hashes of dependencies that directly and indirectly require them. It uses the lib
directory as the module path and considers modules matching the pattern java.base
.
When working with JMOD files, there are some best practices to keep in mind:
Use descriptive and meaningful names for your JMOD files, following the naming conventions for modules.
Include a module-info.java
file in your module’s source code to define the module’s name, dependencies, and exported packages.
Organize your module’s classes, resources, and native libraries in the appropriate directories within the JMOD file.
Use the --module-version
option when creating JMOD files to specify the version of your module.
Store JMOD files in a separate directory structure, separate from your source code and other project artifacts.
Use the jmod tool to create, extract, and manipulate JMOD files as needed.
When distributing your modular application, consider using jlink
to create optimized runtime images that include only the necessary modules.
While JMOD files offer many benefits for modular Java development, there are some limitations and considerations to keep in mind:
JMOD files are specific to the Java Platform Module System and are not backward compatible with older versions of Java.
Not all Java libraries and frameworks are modularized or provide JMOD files. You may need to rely on traditional JAR files for dependencies that are not yet modularized.
The tooling and build systems for modular Java development are still evolving, and there may be some learning curve and configuration required to fully utilize JMOD files in your project.
JMOD files are not intended to be used as a distribution format for end-users. They are typically used as an intermediate format for creating runtime images or integrating with build tools.
jlink
Traditionally, Java applications have relied on the Java Runtime Environment (JRE) to execute. The JRE includes a wide range of modules and libraries, many of which may not be necessary for a particular application. This can lead to larger distribution sizes and potentially unnecessary dependencies.
With jlink
, we can create custom runtime images that include only the modules required by our application. These custom runtime images are self-contained and can be distributed as standalone executables. They provide several benefits, such as reducing distribution size, improving startup time, and enhancing security by minimizing the attack surface.
jlink
To create a custom runtime image using jlink
, we use the following basic syntax:
jlink [options] --module-path <modulepath> --add-modules <modules>
Let’s break down the key components of the jlink
command:
[options]
: Additional options to configure the behavior of jlink
, such as compression, debugging, and more.--module-path <modulepath>
: Specifies the module path where the required modules can be found. This includes the application modules and any dependencies.--add-modules <modules>
: Specifies the modules to be included in the runtime image. This can be a comma-separated list of module names or the keyword ALL-MODULE-PATH
to include all modules found on the module path.One of the primary goals of creating custom runtime images is to minimize the size and include only the necessary modules. jlink
provides options to create a minimal runtime that includes only the essential modules required for our application to run.
Here are some of the most important options:
--add-modules mod [, mod...]
: Adds the named modules, mod
, to the default set of root modules. The default set of root modules is empty.--bind-services
: Link service provider modules and their dependencies.-c ={0|1|2} or --compress={0|1|2}
: Enable compression of resources.--endian {little|big}
: Specifies the byte order of the generated image. The default value is the format of your system’s architecture.-h or --help
: Prints the help message.--ignore-signing-information
: Suppresses a fatal error when signed modular JARs are linked in the runtime image. The signature-related files of the signed modular JARs aren’t copied to the runtime image.--launcher command=module or --launcher command=module/main
: Specifies the launcher command name for the module or the command name for the module and main class (the module and the main class names are separated by a slash character).--limit-modules mod [, mod...]
: Limits the universe of observable modules to those in the transitive closure of the named modules, mod, plus the main module, if any, plus any further modules specified in the --add-modules
option.--list-plugins
: Lists available plug-ins, which you can access through command-line options.-p or --module-path modulepath
: Specifies the module path. If this option is not specified, then the default module path is $JAVA_HOME/jmods
. This directory contains the java.base
module and the other standard and JDK modules. If this option is specified but the java.base
module cannot be resolved from it, then the jlink command appends $JAVA_HOME/jmods
to the module path.--output path
: Specifies the location of the generated runtime image.--suggest-providers [name, ...]
: Suggest providers that implement the given service types from the module path.--version
: Prints version information.To create a minimal runtime, we can use the following command:
jlink --module-path <modulepath> \
--add-modules <modules> \
--compress 2 \
--strip-debug \
--no-header-files \
--no-man-pages \
--output <path>
In this command, we use several options to optimize the runtime image:
--compress 2
: Enables compression of the generated runtime image, reducing its size.--strip-debug
: Removes debug information from the runtime image, further reducing its size.--no-header-files
: Excludes header files from the runtime image.--no-man-pages
: Excludes manual pages from the runtime image.By specifying only the necessary modules with --add-modules
and using these optimization options, we can create a minimal runtime image that is tailored to our application’s specific requirements.
jlink
also allows us to explicitly include or exclude modules from the runtime image. This gives us fine-grained control over the modules that are packaged into the image.
To include specific modules, we can use the --add-modules
option followed by a comma-separated list of module names. For example:
jlink --module-path <modulepath> \
--add-modules module1,module2,module3 \
--output <path>
This command will create a runtime image that includes only module1
, module2
, and module3
, along with their transitive dependencies.
On the other hand, if we want to exclude certain modules from the runtime image, we can use the --exclude-modules
option followed by a comma-separated list of module names. For example:
jlink --module-path <modulepath> \
--add-modules ALL-MODULE-PATH \
--exclude-modules module4,module5 \
--output <path>
In this case, jlink will include all modules found on the module path except for module4
and module5
.
Plugins are additional components that extend the functionality of the jlink
tool. They allow developers to customize the creation of runtime images in various ways, such as optimizing the generated image, adding or removing resources, and configuring how the image is laid out.
If you execute:
jlink --list-plugins
You’ll get the list of all available plugins. For example:
--add-options <options>
: Prepend the specified <options>
string, which may include whitespace, before any other options when invoking the virtual machine in the resulting image.--compress <compress>
: Compression to use in compressing resources. Accepted values are zip-[0-9]
, where zip-0
provides no compression, and zip-9
provides the best compression. Default is zip-6
.--exclude-files <pattern-list>
: Specify files to exclude. For example: **.java
, glob:/java.base/lib/client/**
--exclude-jmod-section <section-name>
: Specify a JMOD section to exclude. Where <section-name>
is man
or headers
.--exclude-resources <pattern-list>
: Specify resources to exclude. For example: **.jcov
, glob:**/META-INF/**
--include-locales <langtag>[,<langtag>]*
: BCP 47 language tags separated by a comma, allowing locale matching defined in RFC 4647. For example: en
, ja
, *-IN
--strip-debug
: Strip debug information from the output image--strip-java-debug-attributes
: Strip Java debug attributes from classes in the output image--strip-native-commands
: Exclude native commands (such as java/java.exe
) from the image.--vm <client|server|minimal|all>
: Select the HotSpot VM in the output image. Default is all
.Here are some examples of how you might use these plugins with the jlink
command:
# Create a runtime image with maximum compression,
# exclude specific files, and strip debug information
jlink --module-path $JAVA_HOME/jmods \
--add-modules java.base \
--compress zip-9 \
--exclude-files "**.java,glob:/java.base/lib/client/**" \
--strip-debug \
--output custom-runtime-image
# Create a runtime image that includes only the
# specified locales and uses the server VM
jlink --module-path $JAVA_HOME/jmods \
--add-modules java.base \
--include-locales en,ja \
--vm server \
--output custom-runtime-image
In addition to creating a minimal runtime image, jlink
provides options to further optimize the generated runtime. These optimizations can help reduce the size of the runtime image and improve its performance.
One important optimization is compression. By default, jlink
does not compress the generated runtime image. However, we can enable compression using the --compress
option followed by a compression level. The compression level can be set to 0 (no compression), 1 (constant string sharing), or 2 (ZIP compression). For example:
jlink --module-path <modulepath>
--add-modules <modules> \
--compress 2 \
--output <path>
Using --compress 2
applies ZIP compression to the generated runtime image, significantly reducing its size.
Another optimization is to strip debug information from the runtime image. Debug information is useful during development but is not necessary for production deployments. We can remove debug information using the --strip-debug
option:
jlink --module-path <modulepath>
--add-modules <modules> \
--strip-debug \
--output <path>
This way, we can further reduce the size of the runtime image.
Migrating an existing application to use modules can be a challenging task. It’s doable, but requires careful planning and execution.
Before embarking on the migration process, it’s important to understand how the packages and libraries in the existing application are structured. This involves analyzing the codebase and identifying the dependencies between different parts of the application.
One approach to gain insights into the application structure is to use jdeps
. By running jdeps
on the application’s JAR files or class files, we can generate a dependency report that provides valuable information about the relationships between packages and classes.
Here’s an example of running jdeps
on an application JAR file:
jdeps -s -recursive application.jar
The -s
option generates a summary output, and the -recursive
option analyzes all dependent JAR files as well.
The output of jdeps will give us an overview of the packages and their dependencies. It will highlight any dependencies on JDK internal APIs, which is important to note as these APIs may not be accessible in future Java versions.
If you want more detail, you can use the -verbose
option:
jdeps -verbose application.jar
The output will show the dependencies between packages and classes, as well as the dependencies on external libraries.
It’s important to identify and resolve any circular dependencies or unnecessary dependencies at this stage. Circular dependencies can cause issues when modularizing the application, as modules cannot have cyclic dependencies. Unnecessary dependencies can bloat the application and make it harder to modularize effectively.
Now that we have a map of the application dependencies, it’s time to start planning our migration. One common strategy is to split our big project into smaller, more manageable modules. This process involves identifying logical boundaries within the application and separating the code into distinct modules based on functionality and dependencies.
JPMS gives us a few tools to ease this transition: unnamed modules and automatic modules.
Let’s say we have a big monolithic application called BigApp
. We might start by putting it on the module path without defining a module-info.java
file:
java --module-path BigApp.jar
--add-modules ALL-UNNAMED
This puts BigApp
into an unnamed module. It’s not a proper JPMS module yet, but it’s a start. We can now start breaking BigApp
into smaller, more manageable pieces.
For third-party libraries that aren’t yet modularized, we can use automatic modules. Let’s say we’re using a library named CoolLib
. We can put it on the module path:
java --module-path BigApp.jar:CoolLib.jar
--add-modules ALL-UNNAMED
Now CoolLib
becomes an automatic module. The module system derives its name from the JAR filename, and it exports all its packages.
But remember, these are temporary solutions. Our end goal is to have proper, explicit modules for everything.
When splitting the project into modules, it’s important to consider the dependencies between the modules. Aim to minimize the coupling between modules and promote loose coupling with well-defined interfaces and APIs.
Here are some strategies for properly splitting a big project into modules:
Package-based splitting: One approach is to create modules based on the existing package structure. Each package or a group of related packages can be converted into a separate module. This helps in maintaining a clear separation of concerns and encapsulation.
Layered architecture: If the application follows a layered architecture (presentation layer, business logic layer, data access layer), each layer can be split into its own module. This allows for better modularity and easier maintenance of each layer independently.
Feature-based splitting: Another approach is to split the application based on its features or functional areas. Each major feature or functionality can be encapsulated within its own module, promoting reusability and maintainability.
Dependency-based splitting: Analyzing the dependencies between different parts of the application can help identify natural module boundaries. Strongly coupled components can be grouped together into a module, while loosely coupled components can be split into separate modules.
But you might be wondering what strategies can we take for the migration in general. Here are a few approaches:
Incremental migration: In this approach, the migration is done gradually, one module at a time. Start by identifying a suitable module to migrate first, typically one with minimal dependencies on other parts of the application. Once the module is successfully migrated, move on to the next module, and so on.
Bottom-up migration: This strategy involves starting the migration from the lowest-level modules and gradually moving up the dependency hierarchy. Begin by modularizing the modules that have no dependencies on other modules, and then proceed to modules that depend on already modularized modules.
Top-down migration: In contrast to the bottom-up approach, the top-down migration starts with the high-level modules and works its way down the dependency chain. This strategy is useful when the high-level modules have a clear separation of concerns and can be easily modularized.
Parallel development: If time and resources permit, parallel development can be employed. In this approach, a separate branch or codebase is created for the modularized version of the application, while the existing non-modularized version continues to be maintained. Development can proceed simultaneously on both versions, gradually migrating modules to the modularized branch.
For example, we might start modularizing a part of our application using a bottom-up approach:
module com.myapp.core {
requires java.base;
requires com.coollib; // This is our automatic module
exports com.myapp.core.api;
}
This module-info.java
file defines a new module com.myapp.core
. We’re starting with a core module that likely has fewer dependencies, which is characteristic of the bottom-up approach. It requires the java.base
module (which is implicit but we’re being explicit here) and CoolLib
, which is an automatic module. It also exports a package com.myapp.core.api
for other modules to use.
As we create more modules, we’ll need to think carefully about our module boundaries.
One important thing to keep in mind is that while we’re in this phase, we might need to open up more than we’d like. For example:
open module com.myapp.core {
requires java.base;
requires com.coollib;
exports com.myapp.core.api;
}
By making this an open module, we allow deep reflection into all its packages. It’s not ideal for security, but it might be necessary during the migration to keep things working. As we progress in our migration, we’ll want to tighten these permissions, exporting and opening only what’s necessary.
Remember, migration is a process. It’s okay to use unnamed and automatic modules as stepping stones. The key is to have a clear migration plan and to move steadily towards a fully modularized system. Breaking down the migration process into smaller, manageable tasks helps in tracking progress and identifying any challenges or roadblocks along the way.
A module in Java is a named, self-describing collection of code and data. It’s defined in a module-info.java
file.
module
: Declares the module namerequires
: Specifies module dependenciesexports
: Makes packages accessible to other modulesprovides
: Declares service implementationsuses
: Indicates that a module uses a servicemodule-info.java
fileKey benefits of JPMS include improved encapsulation, clearer dependencies, better performance, and enhanced security.
The exports
keyword controls which packages are accessible to other modules. You can also use exports...to
to limit access to specific modules.
The requires
keyword declares module dependencies. Use requires transitive
to make a module’s dependencies available to modules that depend on it.
The opens
keyword allows reflective access to a package at runtime.
The provides...with
clause in a module declaration specifies that a module provides a service implementation.
The uses
clause indicates that a module consumes a service.
ServiceLoader
is used to discover and load service implementations at runtime.
Built-in Java modules start with java
(core SE Platform) or jdk
(additional JDK-specific APIs).
When designing multi-module applications, consider separation of concerns, encapsulation, stable dependencies, reusability, and appropriate module size.
Compile modules using javac
with the --module-path
option. Run modular applications using java
with --module-path
and -m
options.
Use the jar
command to package modules into modular JAR files.
Resolve conflicts between modules by managing versions, avoiding split packages, ensuring unique package names across modules, and breaking cyclic dependencies.
The java
command with --describe-module
option provides details about a specific module, including exports, requirements, and services.
--list-modules
option lists all available modules in the Java runtime and custom modules.
--show-module-resolution
option helps debug complex module dependencies by showing how each module is resolved.
The jar
command can be used to inspect modules without running them, using --describe-module
option.
jdeps
is a tool for analyzing and visualizing dependencies at both module and class levels.
jdeps [options] path
. Here are some of its most important options:
--dot-output
option generates a DOT file for visualizing dependency graphs.--jdk-internals
flag helps identify usage of internal JDK APIs.--recursive
option provides transitive dependency analysis.JMOD files are designed for the Java Platform Module System (JPMS) and have a .jmod
extension.
JMOD files can include compiled classes, resources, native libraries, and module descriptors.
jmod
has several modes: create, extract, describe, list, and hash.
Best practices include using descriptive names, including module-info.java
, and organizing module contents properly.
jlink
creates custom runtime images that include only required modules.
This is the basic syntax of jlink
: jlink [options] --module-path <modulepath> --add-modules <modules>
Plugins can extend jlink
functionality for additional optimizations or customizations.
Options like --compress
, --strip-debug
, --no-header-files
, and --no-man-pages
help optimize the runtime image.
Use jdeps
to analyze existing application structure and dependencies before migration.
Strategies for splitting projects into modules: package-based, layered architecture, feature-based, and dependency-based.
Migration approaches: incremental, bottom-up, top-down, and parallel development.
Unnamed modules and automatic modules can be used as temporary solutions during migration.
Consider using open modules during migration to allow reflection, but aim to tighten permissions as the migration progresses.
Migration is a process; it’s okay to use unnamed and automatic modules as stepping stones towards full modularization.
1. Which of the following are types of modules in the Java Platform Module System (JPMS)? (Choose all that apply.)
A) Automatic module
B) Default module
C) Unnamed module
D) Core module
E) Primary module
2. Which of the following is the correct way to declare a module named com.example
in the Java Platform Module System (JPMS)?
A) module com.example { exports com.example.api; }
B) declare module com.example { }
C) create module com.example { requires java.base; }
D) module com.example { }
E) module com.example requires java.base;
3. Which of the following access control statements correctly restricts access to the com.example.internal
package so that it is only accessible to the com.example.client
module?
A) module com.example { exports com.example.internal to com.example.client; }
B) module com.example { opens com.example.internal to com.example.client; }
C) module com.example { requires com.example.internal; }
D) module com.example { provides com.example.internal to com.example.client; }
E) module com.example { uses com.example.internal; }
4. Given the following module declarations, which statement is correct regarding the accessibility of the com.example.api
package for deep reflection by the com.example.client
module?
module com.example {
exports com.example.api;
opens com.example.internal to com.example.client;
}
module com.example.client {
requires com.example;
}
A) The com.example.client
module can access the com.example.api
package for deep reflection.
B) The com.example.client
module cannot access the com.example.api
package for deep reflection.
C) The com.example.api
package is opened to all modules for deep reflection.
D) The com.example.internal
package is exported to the com.example.client
module.
E) The com.example.api
package is exported to the com.example.client
module for deep reflection.
5. Which of the following statements is correct regarding core Java modules and their functionalities?
A) The java.base
module provides the Swing and AWT libraries for building graphical user interfaces.
B) The java.logging
module is responsible for handling collections, including lists, sets, and maps.
C) The java.desktop
module provides the classes for implementing standard input and output streams.
D) The java.xml
module includes the classes for processing XML documents.
E) The java.naming
module provides APIs for accessing and processing annotations.
6. Which of the following command-line statements correctly compiles the module located in the src/com.example
directory and outputs the compiled module to the out
directory?
A) javac -d out src/com.example/module-info.java src/com.example/com/example/*.java
B) javac -sourcepath src -d out com.example/module-info.java com.example/com/example/*.java
C) javac -d out --module-source-path src -m com.example
D) javac -modulepath out -d src src/com.example/module-info.java src/com.example/com/example/*.java
E) javac --module-path src --module com.example -d out
7. Given the following multi-module application structure, which command compiles both modules correctly?
src/
├── com.foo/
│ ├── module-info.java
│ └── com/foo/Foo.java
└── com.bar/
├── module-info.java
└── com/bar/Bar.java
A) javac --module-source-path src -d out $(find src -name "*.java")
B) javac -d out --module com.foo,com.bar --module-source-path src
C) javac -sourcepath src -d out src/com.foo/module-info.java src/com.foo/com/foo/*.java src/com.bar/module-info.java src/com.bar/com/bar/*.java
D) javac -modulepath src -d out src/com.foo/*.java src/com.bar/*.java
E) javac --module-source-path src/com.foo,src/com.bar -d out
8. Which of the following statements correctly specifies a service provider implementation for the service com.example.Service
in the module-info.java
of the com.provider
module?
A) requires com.example.Service with com.provider.ServiceImpl;
B) exports com.example.Service with com.provider.ServiceImpl;
C) provides com.example.Service with com.provider.ServiceImpl;
D) uses com.example.Service with com.provider.ServiceImpl;
9. Which of the following command-line statements correctly describes the com.example
module using the --describe-module
option?
A) java --describe-module com.example/module-info.java
B) javac --describe-module com.example
C) jar --describe-module com.example
D) java --describe-module com.example
10. Which of the following command-line statements correctly uses jdeps
to analyze the dependencies of a JAR file named example.jar
? (Choose all that apply)
A) jdeps --list-deps example.jar
B) jdeps -verbose example.jar
C) jdeps -s example.jar
D) jdeps --check example.jar
11. Which of the following command-line statements correctly creates a JMOD file from the contents of the mods/com.example
directory?
A) jmod create --class-path mods/com.example --output com.example.jmod
B) jmod --create --class-path mods/com.example --output com.example.jmod
C) jmod --create --dir mods/com.example --output com.example.jmod
D) jmod create --dir mods/com.example --output com.example.jmod
12. Which of the following command-line statements correctly creates a custom runtime image using the jlink
tool with the modules java.base
and com.example
and outputs it to the myimage
directory?
A) jlink --module-path java.base:com.example --output myimage
B) jlink --module-path mods --add-modules java.base,com.example --output myimage
C) jlink --add-modules java.base,com.example --image myimage
D) jlink --modules java.base,com.example --dir myimage
13. Which of the following statements is correct regarding the migration of a legacy application to the Java Platform Module System using unnamed and automatic modules?
A) An unnamed module can depend on named modules and other unnamed modules.
B) Automatic modules must have a module-info.java
file to be placed on the module path.
C) Unnamed modules can export their packages to named modules using module-info.java
.
D) An automatic module is created when a JAR file without a module-info.java
is placed on the module path, and it can read all other modules.
Do you like what you read? Would you consider?
Do you have a problem or something to say?