Theory:Modules
Java 9 introduced Java modules to break large applications down into smaller, more manageable chunks. Unfortunately, when a codebase grows in size, some things are bound to happen. First, dependency management becomes very difficult if you try to do it manually. Also, the size of your application begins to balloon. These things can complicate the interaction between different parts of a large codebase. With no foolproof way of enforcing private access to classes, a developer working on a distant part of a large application might make a few changes to their code and inadvertently cause breaking changes in yours. The Java Platform Module System (JPMS) is Java 9's attempt at solving these problems.
# Packages VS modules
Since packages and modules have a lot in common, it can be difficult to see the difference between them at first. Think of it like this. Packages group related classes. Usually, they are just folders in your source directory. They were introduced to keep related classes together, but they also serve the additional purpose of distinguishing between classes with the same name. Prepending the package name to a class is a simple way to remove any ambiguity. Packages have existed in Java from the very beginning.
Two additional points to keep in mind about Java packages (we will see how they are relevant in the following sections):
- Packages cannot be deployed by themselves. They contain the source code of an application, but the application still requires the JRE to run it.
- Classes in a package can be accessed via reflection, even if the classes were declared private.
As for Java modules, it is helpful to think of them as large boxes that you put packages into. If the packages are similar or if they logically belong together because they contribute to one functionality, they can be bundled together into a Java module. In fact, starting from Java 9, the Java Platform API has been split into separate modules instead of one monolithic .jar file.
# Application size
Because the Java Platform API is now broken down into modules, your application only needs to include the class files from the modules of the API you used. This has the lovely side effect of shrinking your compiled program file size, making it much easier to use Java programs on resource-limited computers (embedded systems, mobile devices, RaspberryPi, etc.).
While it is beyond the scope of this topic, you can even package the JRE into your module so that it becomes an independent executable (opens new window) file that does not require the user to have installed Java to run it.
# Encapsulation
Java modules also introduce some new ways to choose which parts of the module have their class files visible to other developers and which parts are completely encapsulated as if they were a black box. As a result, it makes it possible to build programs as a collection of loosely coupled modules. This way the developers of one particular module in a large program have no choice but play nice with the developers of other modules, keeping their hands to themselves and only interacting with other modules at the API level.
It is generally easier for developers to update their codebase when they provide an interface for their program but keep the internals hidden. Users and other developers don't need to know how it is structured internally, that is, its implementation. They only need to understand how the outside interface that is visible to them works.
提示
Likewise, when developers provide an interface but restrict access to the implementation, it allows them to optimize, update, or even completely rewrite the implementation. As long as the interface still works as expected, any program using this module as a dependency will still work.
At this point, you may be thinking: couldn't we always do this in Java? How is this a new feature? Technically, yes, you could design an application to be used this way, but the issue was that developers couldn't enforce the encapsulation of the implementation. Other developers could always use reflection to access the internals, even if they were declared private. So Java modules changed the rules of the game by making it so that application developers don't need to trust other developers to use their interface but not mess with the internals. Instead, they can now lock away whichever parts of the implementation they choose.
# Dependency management
Java modules must contain a Module Descriptor file, which is titled module-info.java. While it contains some obvious data, like the module's name, the services it offers, and the services it consumes, it also specifies a few more targeted pieces of information. Oracle (opens new window)summarizes them as such:
- the module’s dependencies (that is, other modules this module depends on);
- the packages it explicitly makes available to other modules (all other packages in the module are implicitly unavailable to other modules);
- to what other modules it allows reflection*.*
It makes sense that modules can depend on other modules, but now developers have to explicitly say which packages of their module are available to other modules. The best part is that developers can now also be selective about which other modules can access it via reflection.
As we mentioned in the introduction, more dependencies to keep track of increase the likelihood that someone will accidentally miss some of them, or that conflicts between dependency versions will arise. While it is true that developers often use build tools like Maven and Gradle to manage dependencies, up until the introduction of Java modules, there was nothing internal to Java that could help with this. For instance, JVM wouldn't detect a missing class file until the program actually tried to use it. However, beginning in Java 9, the JVM will check the dependency graph on startup and throw an error if any dependencies are missing.
# Modules in action
We are going to create a basic module named myModuleName
. The name of our module must be unique. This is why you often see both packages and modules prefixed with a company domain before their names. For example, if your module is named JsonUtil
, there's a fair chance someone else in the world have used that name already or will try to do so in the future. But if you used com.myCompany.JsonUtil
instead, you will likely be safe.
We begin with a java file titled *module-info.java*
, which is placed in the module’s root source directory. In this file, the word module
is followed by the name of our module and a pair of curly braces.
An empty module declaration with a generic company domain would look like this:
module com.myCompany.myModuleName {
}
2
3
We can declare our module's dependencies inside this declaration using requires
. If our module used JavaFX for its GUI, we would need to requires
the appropriate JavaFX modules. In this example, we add the javafx.graphics
module to our *module-info.java*
. There are other modules in javafx
, of course, but we will only take the one we need.
module com.myCompany.myModuleName {
requires javafx.graphics;
}
2
3
If we want to make parts of our module available for other modules to use, we must explicitly do so using the exports
keyword.
module com.myCompany.myModuleName {
exports com.myCompany.myModuleName;
}
2
3
注意
Only the specific packages mentioned will be made available. To access a package's child packages, you need to explicitly export
those as well. For example, other modules now have access to the base package *com.myCompany.myModuleName*
, but they do not have access to *com.myCompany.myModuleName.util*
.
# Example program
Suppose you want to build a program composed of two modules. One module will be the main module and the other will be added as a dependency to it. You created a module of super utilities that could revolutionize the world, but you only want other modules to be able to use its baseUtilities
package. The baseUtilities
package should have access to the other classes to do its job, but no one else will have access to them. To make this happen, we need to export only the package we want to share.
module com.myCompany.superUtilities {
exports com.myCompany.superUtilities.baseUtilities;
}
2
3
The baseUtilities
package has a class called SuperOptimizer
, which we want to use in our program.
package com.myCompany.superUtilities.baseUtilities;
public class SuperOptimizer {
}
2
3
4
5
The main module of our program is com.myCompany.WorldChanger
. It can only change the world if it has access to the SuperUtilities
module, therefore we will need to add it as a dependency. While we're at it, let's export our module so others can use it in their programs too.
module com.myCompany.worldChanger {
requires com.myCompany.superUtilities;
exports com.myCompany.worldChanger;
}
2
3
4
5
提示
Note that while you must be as specific as possible when exporting packages from a module, you can import the module as a whole without specifying any packages. You will, of course, only get access to the packages that have been specifically mentioned in the exports
statement of that module's module-info.java file
.
Now we can write our Java program.
package com.myCompany.worldChanger;
import com.myCompany.superUtilities.baseUtilities.SuperOptimizer;
public class WorldChangerImpl {
public static void main(String[] args) {
SuperOptimizer so = new SuperOptimizer();
// rest of code
}
2
3
4
5
6
7
8
9
10
11
# Conclusion
In this topic, we learned how Java modules help us with dependency management by checking for all the dependencies at startup, rather than waiting until they are used during runtime. Java modules also help developers to have better control over who can access different parts of their code. This helps with privacy, but it also allows developers to make more substantive changes behind the scenes without worrying about breaking others' code. Since the JDK itself is made of modules, we can also shrink the size of our applications just by using the exact modules we need. Now, let's get our hands dirty with some practice!