The Java Module System
The course is part of this learning path
This training course provides you with a deep dive into the Java Module System, a new feature provided as part of Java 9. We’ll review the many benefits associated with the new Module System, and how it is used to package application components into smaller and more manageable items.
What you'll learn:
- What the Java Module System is and how to implement it
- The benefits of working and developing with modules
- The differences between JDK modules and Public API modules
- How to implement and package your own modules
- A basic understanding of the Java programming language
- A basic understanding of software development
- A basic understanding of the software development life cycle
- Software Engineers interested in advancing their Java skills
- Software Architects interested in using advanced features of Java to design and build both applications and frameworks
- Anyone interested in advanced Java application development and associated tooling
- Anyone interested in understanding the advanced areas and features of the Java SDK
- [Jeremy Cook] Okay, welcome back. In this lecture we'll explore the Java Module System and how it can and should be used within Java. In particular, we'll review the following topics: we'll introduce you to Project Jigsaw, the Java 9 platform modules, how to define application modules, defining module dependencies, looking at implicit dependencies, implied readability, and finally exporting packages. At the conclusion of this lecture you should be able to perform each of the items listed above. Take a moment to rate yourself on each of these items, on a scale of one through to five. At the conclusion of this lecture these objectives will be reviewed. You should rate yourself again to see how much benefit you received from this lecture. So with Java 9, the module system design was introduced as part of Project Jigsaw, with the primary goal being a new module system that would allow the JDK and other large legacy code bases to be modularized. The new module system would help developers to maintain dependency management between libraries within applications for both the Java SE and EE Platforms. Some of the other goals of Project Jigsaw are: to implement a module system within Java; making Java SE more scalable to scale down the runtime when needed; defining a reliable configuration system allowing modules to define their own dependencies; to support strong encapsulation, allowing modules only to export specific packages; to improve maintainability of the Java SE platform, encapsulating internal APIs; improving the security of the Java SE platform, ensuring critical code is always hidden; and to simplify the development and maintenance of large applications. Therefore as you can see, a wide range of goals were defined for Project Jigsaw. But in summary, the key motivations for Project Jigsaw were to improve and simplify the development and maintenance of Java-based applications, whilst improving the overall performance and security of the platform. Java 9 Platform Modules, the complete Java Development Kit has been divided into modules. So instead of having a single JAR file containing all the technologies that make up the JDK, Java 9 is made up of a large number of modules. Each one of these modules contains the APIs for only a single technology. When developing a Java application, developers can now pick and choose which platform modules are needed to support their code. Naturally, the platform module also requires dependencies on other modules in order to fulfill its task. For example the java.lang.String class resides in the java.base package. Classes to perform XML operations reside in the java.xml package, but these classes do need to be able to use the String class, as a result the java.xml module has a dependency on the java.base module. The module graph is hierarchical, a module might have dependencies on other modules, but these dependencies are always one-way. The entire graph does not contain circular dependencies. To separate the classes that make up the development kit from the classes that should be used to write Java applications, all classes that are part of the JDK are defined in modules of which the name starts with jdk, while all the public APIs of Java 9 are defined in modules of which the name starts with java. The java.base module contains all the fundamental classes of the platform. It contains the java.lang package, but also packages such as java.util and java.io. Since all classes in Java extend java.lang.Object, and that Object class resides in the java.base package, all the modules in Java have an implicit dependency on this java.base module. Until the introduction of Java 9, dependencies between packages were never clear. For an example, when an JAXWS application was being developed, a large number of packages were also required to exist on the classpath. The JAXWS package has a dependency on the bean package, which can be explained by the fact that JAXWS relies on JavaBeans and reflection to manage the state of some objects. But why also the dependency on the desktop package? Well, in an earlier version of Java, someone decided that a bean could be represented in a builder-tool by an icon. In order to support this behavior, a getIcon method was defined on the BeanInfo class. The return type of this method is an instance of java.awt.Image. So when a JAXWS project needed to be developed, the java.awt package needed to be available on the classpath. In Java 9, the dependencies between modules have now been simplified. The java.xml.ws module defines the JAXWS APIs and defines the modules it requires in order to perform its job. So let's now consider modular development. Modularity allows development of several unique modules instead of one large monolithic deployment unit. Each module should define an autonomous unit of deployment, this provides loose coupling, should have a unique identity name, should be discoverable, should provide meta-data describing dependencies, should define a public interface or contract, and should hide the details about its implementation. When developing applications in Java 9, these can now be defined developed using modules. Each one of these application modules will then rely on one or more platform modules. Each module that is being developed should be considered an autonomous unit of development. It will be exposing a public interface while completely hiding its internal implementation. Defining application modules, when developing modular-based applications, you will define your classes in packages, just like you have done before. But now you will add another layer of grouping where multiple packages are being grouped into a single module. A module should be a cohesive collection of resources. Besides the Java classes, it should also contain the resources and configuration files it requires. Once a module is compiled, the resources can be assembled in a single JAR file. This library should now, like any other JAR file, contain all the resources it requires to do its job. But modules are more than just a JAR file containing resources. The module contains a descriptor which defines the unique name of the module itself, but more importantly, it defines the modules it depends on. This way all the modules that are required at compile and runtime can be determined by inspecting the descriptors of the modules. The module descriptor also defines a list of the packages that it exports. Most modules export a subset of the java packages it contains, thereby hiding some of the implementation details. Creating a module, a module is defined by adding a file named module-info.java to the source root within the default package. This is the module declaration file for this module. Every module must at least define its unique name. It is recommend to have the name of the module be a prefix of the package names it contains. This module descriptor is going to contain a number of reserved words, like the word module. It should be noted that these words are only reserved within the context of this module descriptor and are not reserved words in the programming language. Implicit dependencies, since the java.base module contains the Object class, all application modules need at least a dependency on this module. The dependency on java.base is the only dependency that is implicit and does not need to be defined in the module descriptor. Similar to the import of the java.lang package in source code, the dependency can be made explicit, but it is not needed. When classes are needed that are not present in the java.base module, a dependency between the application module and the module in which the class resides must be explicitly declared in the module-info.java file. Keep in mind that when a dependency is declared on a particular module, only the exported packages of that other module can be accessed. In other words, when there is a dependency on a module, only the public API of that module can be used. Defining module dependencies, a dependency on another module must be defined in the module-info.java file. The dependency is defined using the requires keyword and the unique name of the other module. The dependency graph, when all modules define their dependencies in the module descriptor, the entire dependency graph can easily be constructed. By doing so, we can determine what platform modules are really needed at compile and runtime. Readability and accessibility, every module defines which other modules it requires at compile-time and runtime. When a module requires a different module we can say that the module reads the public API from the other module. When a module called application requires the service module, the public API defined by the service module is readable by the application module. The service module in turn requires the data module, so the service module does read the public API of the data module. As a result of the dependency between the service and the data module, the application module has an implicit dependency on the data module. Even though the service and data module must both be available at runtime, the application module does not have access to the public API of the data module. In other words, the application module cannot read the public API of the data module. Transitive dependencies, a lot of the dependencies between two modules are completely encapsulated. A module uses the public API of another module to fulfill its task. However, some modules expose a public API that relies on types that are defined in a module it depends on. In the example shown here, does the getFlightInformation method in the service module return a List of Flight instances? The Flight type is defined in the data module on which the service module depends. When the application module, which only depends on the service module, now invokes the getFlightInformation method, it receives a List of types that it is not familiar with. The application module cannot read the data model and as a result it does not know the flight type, resulting in a compilation error. Implied readability, when a module, for example the service module, reads another module, in this case the data module, it can grant that readability to all modules that it is readable by. By defining its dependency as transitive, it specifies that the module it depends on is also readable by all modules that are depending on this module. An alternative would be to make the application module depend on both the service module and the data module, however this would make the dependency module fragile and complex. When a module exposes a public API that references types that are defined in a second module, it is recommended to define that dependency as transitive. The java.sql Platform module has transitive dependencies on both java.logging and java.xml. An application module that defines a dependency on java.sql can automatically read the public APIs of these other two modules. Exports and accessibility, in Java 9, a type that is defined as public is no longer visible to the entire world. Only when the package in which the type is defined is explicitly exported by the module does it become eligible for use. Before it can be used, another module must explicitly define its dependency on the module. In a way this makes the public type more hidden than the package private default access modifier used in earlier versions of Java. Command line arguments can be used to override this behavior. Adding exports to a module, by default, all packages that are contained within the module are considered to be private. Every package that contains types which can be used by other modules must be explicitly declared in the module-info.java file. It is not possible to make a complete hierarchy of packages public. Each individual package must be explicitly declared in the module descriptor. By taking this approach it becomes possible to only expose the abstract interface of an implementation while hiding the implementation class. The module system is very strict about resolving the dependencies between modules. It will make sure that a dependency is fulfilled by exactly one other module, and unlike the classpath does not rely on the order in which modules are loaded to select one module over the other. The module system does not allow circular references to be defined between modules, all dependencies must be one-way. When multiple modules contain the same package definition, the module system will make sure these packages do not interfere with each other. When exporting packages within the module descriptor, the module system will check to make sure the package you are defining exists within the module and it contains types that are to be exported. By applying these constraints to the modules and making sure all dependencies are resolved properly, we end up with a reliable configuration of our application. When any of the constraints are violated, the system will not compile or launch. Developers familiar with working with the classpath in previous versions of the JDK will realize what a huge improvement this is. Additionally, missing libraries at runtime as a result of changing the classpath order are finally a thing of the past. Okay before we complete this lecture, pause this video and consider the following questions to test yourself on the content that we have just provided. Write down your answers for each question and then resume the video to compare answers. Okay the answers to the above questions are, one, makes Java SE more scalable, defines a reliable configuration system, supports strong encapsulation, improves both security and maintainability, and simplifies development and maintenance of large applications. Two, must have a unique identity, must be discoverable, provide meta-data describing its dependencies, define a public interface, and hide details about its implementation. Three, module-info.java. Four, the name of the module and the dependencies of the module and the packages it exports. Five, in the module descriptor you use the requires keyword to reference another module name. Six, when a Module A depends on Module B and Module A exposes a public API that relies on types defined in Module B, it will make sure that all public types defined in Module B become readable to any other module that reads module A. Seven, by defining your dependency as transitive. Eight, you don't, you only export the public interface. Nine, you cannot export a single type, only a package.
About the Author
Jeremy is the DevOps Content Lead at Cloud Academy where he specializes in developing technical training documentation for DevOps.
He has a strong background in software engineering, and has been coding with various languages, frameworks, and systems for the past 20+ years. In recent times, Jeremy has been focused on DevOps, Cloud, Security, and Machine Learning.
Jeremy holds professional certifications for both the AWS and GCP cloud platforms.