• No results found

MODULARITY IN JAVA

In document Versatile Java - Deepak Mali 351 (Page 185-200)

.Modularity features in Java 9 has come a long way making the process of modularization easier and accessible. Java 9 has taken a bottom up approach imbibing modularity into its core libraries, language and JVM. Modularization involves a large scale change to an application with an enormous impact on the design, compilation, packaging, etc. It‘s more fundamental to approach than merely a new feature.

Meaning of Modularity

Modularity means decomposing an application into self-contained units ,and each unit encompasses code , metadata about the module and metadata describing its relation with other modules within the application .Each such unit could be an business function (e.g. a Product catalogue in an e commerce application )or a technical unit (e.g. database access logic ).Modularity demands strong encapsulation to prevent other modules effecting the internals and establish communication via the public assessors.In such a case the internals can be updated without any regression effects .Each module should be exposed via well-defined interfaces since this is the single point of communication between two modules and any change at the interface level could potentially break the dependent modules.

Also, to ensure that the metadata pointing to the dependencies is well defined, we should be very clear with all the modules dependencies (at times referred as module graph)

Pre-Java9 Era

Packages and access modifiers (public, private,protected) have provided some form of encapsulation and interfaces have offered modularization in pre-Java 9 era. The approach of hiding the details behind the public interfaces have been long seen and acted as a basis for designing modular codebases. For using classes from other packages bundled within an external jar, we use import statements .Not writing proper imports will be a compile time catch , however to know which dependent jars need to be available at runtime when our jar refers them , we restore to tools like Maven and OGSI.

Jars are a close approximation of modules in the pre- Java 9 world .In case our Test Application uses hibernate validator jar which brings further jars through transitive dependency resolution.

Fig 7.1 Modularity pre Java 9

Since the classes bundled with jars have public API‘s or interfaces exposed .In case these public interfaces change, our code will break .The situation describes lack of true encapsulation. When we execute a java program explicitly providing the external dependencies with the –classpath argument, the JVM loads the classes sequentially .In case any class is missed or two different versions of the same jar exists, a runtime exception is bound to happen. A safe bet is to use Java 9 modules.

Java 9 modules

With java 9, the JDK itself is modularized and the applications are built using a modular system.Modules arestrongly encapsulated entitiesand they express the dependency on other modules explicitly in the modular system. Extending it to the above example, the hibernate validator will mark classmate jar, jboss-logmanager jar and validation api jar as part of its module descriptor. Each module will have a publicly accessible part and an encapsulated part .The application can also refer to a jar or module which is part of the JDK e.g. java.sql jar although the application will have default access to platform module (java.base )containing packages like java.lang and java.util .The modular system offer many advantages :

 Explicit dependencies make the encapsulation stronger.Any accidental references to the internal /transitive dependencies are prevented.

 The dependencies are resolved at compile time as well as runtime

 Well defined public interfaces enable teams to work in parallel

 Minimal configuration for modules

Modularizing JDK was a very challenging task .There were many jars bundled with rt.jar (jar all the java applications use)which may or may be used by all the applications .Thus in the cloud solutions deployed over small containers we often face resource constraints. At the same time , we cannot remove these jars blindly in order to support backward compatibility. In the modular Java , application developers can choose to ignore the modules not in use. The task to bring down the monolithic JDK is also challenging because apart from obsolete technologies (COBRA jar) there is an aspect of usability .A web

application may not use any AWT or Swing utility but a thick client application may .Using only the requires platform modules will reduce the security risks as well.The first step towards a more modular JDK was taken in Java 8 with the introduction of compact profiles. A profile defines a subset of packages from the standard library available to applications targeting that profile. Three profiles are defined, imaginatively called compact1, compact2 and compact3. Each of these profiles is a superset of the previous, adding more packages that can be used. The Java compiler and runtime were updated with knowledge of these pre-defined profiles. Java SE Embedded 8 offers low footprint runtimes matching the compact profiles.

The JDK now consists of about 90 platform modules, instead of a monolithic library. Every module constitutes a well-defined piece of functionality of the JDK, ranging from logging to XML support. All modules explicitly define their dependencies on other modules.For example, java.logging has many incoming dependencies, meaning it used by many other platform modules. That makes sense for a central functionality such as logging. Module java.xml.ws (containing the JAX-WS APIs for SOAP WebServices) has many outgoing dependencies, including an unexpected one on java.desktop. Aggregator modules roughly corresponding to the previously discussed compact profiles have been defined as well: java.compact1, java.compact2 and java.compact3.

Decomposing the JDK into modules has been a tremendous amount of work. Splitting up an entangled, organically grown codebase containing tens of thousands of classes into well-defined modules with clear boundaries, while retaining backwards compatibility, takes time. This is one of the reasons it took a long time to get a module system into Java. With over 20 years of legacy accumulated, many dubious dependencies

had to be untangled. Going forward, this effort will definitely pay off in terms of development speed and increased flexibility for the JDK.

Module Descriptors

Now that we have a high level overview of the JDK module structure, let‘s explore how it works. What is a module, and how is it defined? A module has a name, it groups related code and possibly other resources, and is described by a module descriptor. The module descriptor lives in a file called module-info.java. Here‘s an example of the module descriptor for

conventions like reverse DNS notation

(e.g. com.mycompany.project.somemodule) to ensure uniqueness for your own modules. A module descriptor always starts with the module keyword, followed by the name of the module. Then, the body of module-info.java describes other characteristics of the module, if any.

Let‘s move on to the body of the module descriptor for java.prefs. Code in java.prefs uses code from java.xml to load preferences from XML files. This dependency must be expressed in the module descriptor. Without this dependency declaration, the java.prefs module would not compile (or run).

Such a dependency is declared with the requires keyword followed by a module name, in this case java.xml. The implicit dependency on java.base may be added to a module descriptor.

Doing so adds no value, similar to how you can (but generally don‘t) add "import java.lang.String" to code using Strings.

A module descriptor can also contain exports statements. Strong encapsulation is the default for modules. Only when a package is explicitly exported, like java.util.prefs in this example, can it be accessed from other modules. Packages inside a module that are not exported, are inaccessible from other modules by default. Other modules cannot refer to types in encapsulated packages, even if they have dependency on the module.

Readability

A module lists the other modules it depends on to compile and run. Again from the ―State of the Module System‖ This concept of readability is the basis of reliable configuration: When any condition is violated, the module system refuses to compile or launch the code; an immense improvement over the brittle classpath model.

Accessibility

A module lists the packages it exports. A type in one module is only accessible by code in another module if:

the type is public

the containing package is exported by the first module

the second module reads the first

This means that public is no longer really public. A public type in a non-exported package is as hidden from the outside world as a non-public type in an exported package. So "public‖ is even more hidden than package-private types are today, because the module system does not evenpermit access via reflection.

As Jigsaw is currently implemented, command line flags are the only way around this.

So accessibility builds on top of readability and the export clauses, creating the basis for strong encapsulation, where a module author can clearly express which parts of the module‘s API are public and supported.

Creating the first module

Say we have an application that monitors microservices running in our network. It periodically contacts them and uses their responses to update a database table as well as a nifty JavaFX UI. For the time being, let‘s assume that the application is developed as a single project without any dependencies.

Now let‘s switch to Java 9 with Jigsaw! (The early access builds are availale on java.net - the code samples and commands presented in this article were created for build 96 from December 22nd.) While jdeps, the Java dependency analyzer, already exists in JDK 8, we need to use the JDK 9 version so that it knows about modules.

The first thing to note is that we can simply choose to ignore modules. Unless the code is relying on internal APIs or some other JDK implementation details (in which case things will

break), the application can be compiled and executed exactly as with Java 8. Simply add the artifact (and its dependencies if it has any) to the classpath and call main. And voilà, it runs!

To move the code into a module we must create a module descriptor for it. This is a source code file called module-info.java in the source directory‘s root.

module com.infoq.monitor {

// add modules our application requires

// add packages our application exports }

Our application is now a module with the name com.infoq.monitor. To determine which modules it depends on, we can use jdeps:

jdeps-module ServiceMonitor.jar

This will list the packages our application uses and, more

importantly, from which modules they

come: java.base, java.logging, java.sql, javafx.base, javafx.controls, javafx.graphics.

With our dependencies covered we can now think about which packages we might export. Since we are talking about a stand-alone application, we won‘t export anything.

module com.infoq.monitor {

requires java.base;// see more about that below

requires java.logging;

requires java.sql;

requires javafx.base;

requires javafx.controls;

requires javafx.graphics;

// no packages to export }

Compiling is the same as without Jigsaw except we have to include module-info.java in the list of source files:

javac-d classes/com.infoq.monitor ${source files}

Now that all classes were compiled

into classes/com.infoq.monitor, we can create a JAR from them:

jar -c \

--file=mods/com.infoq.monitor.jar \ --main-class=com.infoq.monitor.Monitor \

${compiled class files}

The new --main-class can be used to specify the class containing the application‘s entry point. The result is a so-called modular JAR, which we will discuss further below.

In stark contrast to the old model, there is a whole new sequence to launch the application. We use the new -mp switch to specify where to look for modules, and -m to name the one we want to launch:

java -mp mods -m com.infoq.monitor

Kinds Of Modules

The JDK itself was modularized and consists of about 80 platform modules (have a look at them with java -listmods).

The ones that will be standardized for Java 9 SE are prefixed with ―java.‖, the JDK-specific ones with ―jdk.‖.

Not all Java environments have to contain all platform modules.

On the contrary, one goal of Jigsaw is the scalable platform, which means that it is easy to create a runtime with just the desired modules.

All Java code depends on Object and virtually all code uses basic features like threading and collections. These types can be found in java.base, which plays a special role; it is the only module that the module system inherently knows about, and since all code depends on it, all modules automatically read it.

So in our example above, we would not need to declare a dependency on java.base.

Non-platform modules are called application modules and the module used to launch an application (the one containing main) is the initial module.

Module Jars

As we have seen above, Jigsaw still creates JARs, albeit with the new semantics. If they contain a module-info.class they are called modular JARs, about which ―State of the Module System‖ says:

A modular JAR file is like an ordinary JAR file in all possible ways, except that it also includes a module-info.class file in its root directory.

Modular JARs can be put on the classpath as well as on the module path (see below). This allows projects to release a single artifact and let its users decide between the legacy or the modular approach to application building.

Module Path

Platform modules are part of the currently used environment and hence are easily available. To let the compiler or VM know about application modules, we have to specify a module path with -mp (as we have done above).

The platform modules in the current environment and the application modules from the module path together comprise the universe of observable modules. Given that set and an initial module contained therein the virtual machine can create a module graph.

Module Graph

Starting from the initial application module, the module system resolves all of its transitive dependencies. The result is the module graph where modules are nodes and a dependency of one module on another is a directed edge.

For our example, that looks like this:

Fig 7.2 Module Graph - I

The platform modules are shown in blue, where the brighter modules are the ones we depend on directly and the darker ones transitive dependencies. The ubiquitous java.base is not shown;

remember, all modules implicitly depend on it.

Splitting Modules

With this understanding of how modules are handled by the compiler and virtual machine, lets start to think about how to divide our application into modules.

Our architecture consists of the following parts:

contacting the microservices and creating statistics

updating the database

presenting the JavaFX user interface

wiring the pieces

Let‘s go ahead and create a module for each:

com.infoq.monitor.stats

com.infoq.monitor.db

com.infoq.monitor.ui

com.infoq.monitor

The Jigsaw Quick-Start Guide and the JDK itself propose to have a folder for each module below the project‘s root source folder. In our case this would have the following structure:

Service Monitor └─ src

├─ com.infoq.monitor │ ├─com ...

│ └module-info.java

├─ com.infoq.monitor.db │ ├─com ...

│ └module-info.java

├─ com.infoq.monitor.stats │ ├─com ...

│ └module-info.java └─ com.infoq.monitor.ui

├─com ...

└module-info.java

This tree is shown truncated here, but each ―com‖ directory represents the package of the same name and would contain more subdirectories and eventually the module‘s code.

The statistics module depends on java.base (but as we learned, we don‘t have to list that) and uses Java‘s built-in logging facilities. It‘s public API allows to request aggregated and detailed statistics and is contained in a single package.

module com.infoq.monitor.stats { requires java.logging;

exports com.infoq.monitor.stats.get;

}

To reiterate the accessibility rules: non-public types in com.infoq.monitor.stats.get and all types in other packages are completely hidden from all other modules. Even the exported package is only visible to modules that read this one.

Our database module also logs and obviously requires Java‘s SQL features. Its API consists of a simple writer.

module com.infoq.monitor.db { requires java.logging;

requires java.sql;

exports com.infoq.monitor.db.write;

}

The user interface obviously requires the platform modules that contain the employed JavaFX features. Its API consists of the means to launch the JavaFX UI. This will return a model that uses JavaFX properties. The client can update the model, and the UI will show the new state.

module com.infoq.monitor.ui { requires javafx.base;

requires javafx.controls;

requires javafx.graphics;

exports com.infoq.monitor.ui.launch;

exports com.infoq.monitor.ui.show;

}

Now that we covered the actual functionality, we can turn our attention to the main module, which wires all of the parts together. This module requires each of our three modules, plus Java‘s logging facilities. Since the UI requires its dependent module to work with JavaFX properties, it depends on javafx.base as well. And since the main module is not used by anybody else, it has no API to export.

module com.infoq.monitor { requires com.infoq.monitor.stats;

requires com.infoq.monitor.db;

requires com.infoq.monitor.ui;

requires javafx.base;// to update the UI model requires java.logging;

// no packages to export }

It‘s a little unwieldy that we have to explicitly require javafx.base, but if we don‘t, we couldn‘t call any code from javafx.beans.property, which we must do to work with the model. So in a way com.infoq.monitor.ui is broken without javafx.beans.property. For this case the Jigsaw prototype provides the concept of implied readability, which we will cover shortly.

In document Versatile Java - Deepak Mali 351 (Page 185-200)

Related documents