Saturday, April 23, 2022

Java Tutorial: Modules

Chapters

Java Modules

In java 9, java introduces Java modules. Java modules or Java platform module system (JPMS) is akin to Java packages but it's much secure than Java packages. A module is used to manage packages and communicate with another module. These are the benefits that we can get from modules:
  • Freedom to choose which packages to be distributed.
  • Strong encapsulation.
  • Missing classes can be detected early at startup.
These are the cons of modules:
  • No mutual dependency. It means that if a module requires the other module, the other module can't require the module back.
  • Split package not allowed. A split package happens if two modules export the same package, splitting the content of the package between modules.
  • Increased complexity. Unlike creating java packages, creating modules and linking them are more complex.
Java modules are divided into different types:

System Modules. These modules are provided by java. In java 9, java changes its package structure and moves predefined packages in modules. We can check this modules out by typing this command on your terminal or cmd(Windows): java --list-modules

Application Modules. These are user-made modules.

Automatic Modules. These modules contain Unofficial modules content of existing JAR files that don't have module-info.class. module-info.class holds information about how modules operate. This module will be automatically created if an existing JAR file without module-info.class is included in a module path.

Its name will be derived from the name of the JAR file. This module will be granted full read access to every module loaded by the module path. This module is useful when migrating old existing JAR files to modularity.

Unnamed Modules. These are modules that contain JAR files content loaded in classpath instead of module path. This module is useful for maintaining backward compatibility with written code in java 8 and below.

Modules can be distributed via JAR files or "exploded" directory tree corresponding to a package hierarchy. Now, let's discuss how to create a module. First off, we need a module descriptor. This descriptor is a java file named as module-info.java. Module descriptor holds information about how a module operates.

This is the structure of a module descriptor:
module module-name{
  //Directives...
}
module is a keyword, module-name is the name of a module block. Directives are statements that define how packages are used. Module descriptor must be placed at the root of packages. In the tutorial, we're gonna create modules using a terminal or command prompt on windows. For production, it's better to use IDEs to make things more easier.

Take note that this is an introductory tutorial. More information can be found in this article.

exports and requires Directives

exports and requires Keywords are keywords used in directive statements. By default, all packages in a module can't be accessed by other modules. Exports directive allows modules to access named package's public classes and their members and exported packages. Take note that export directive doesn't allow other modules to access non-public members of a class in the named package via reflection, even we invoke setAccessible method of java reflection.
Here's the syntax for exports directive: exports package-name;

requires keyword adds a dependency to a module. requires keyword will look for the named module of requires directive in module graph. Java compiler will complain if the named module can't be found.
Here's the syntax for requires directive: requires module-name;

Now, let's test these directives. First off, create this file structure:
|company.modules
  |resources
    |Consumer.java
  |module-info.java
|main.app
  |main
    |Main.java
  |module-info.java
company.modules and main.app are module packages. As you can see, each module has module-info.java. Also, if a module package has two or more words, It's recommended to separate them with dots(.) not underscores(_). resources and main are regular packages and they contain classes. Next, write this code to module-info.java in company.modules directory:
module company.modules{
  exports resources;
}
Write this code to module-info.java in main.app directory:
module main.app{
  requires company.modules;
}
As you can see, module names are equivalent to module packages. Write this code to Consumer.java:
package resources;

public class Consumer{
  public Consumer(){
    System.out.println("Consumer Created!");
  }
}
Write this code to Main.java:
package main;

import resources.Consumer;

public class Main{
	
	public static void main(String[] args){
		Consumer consumer = new Consumer();
	}
}
Now, let's start creating the modules. First off, let's build company.modules. Open your terminal(or cmd if you're using windows) in the directory where the module packages reside and type this command to compile Consumer.java:

javac -d output company.modules\resources\Consumer.java

After that, type this command to compile module-info.java in company.modules directory:

javac -d output company.modules\module-info.java

-d means destination directory. output is the destination directory, java will create one if the directory doesn't exist.

Next, let's put the compiled file to a JAR file. Type this command to create a JAR file and put the compiled files above in the JAR:

jar -c -f build\company.modules.jar -C output .

-c means create a JAR. -f means the location of the JAR file that is going to be created. Java will create necessary directories if directories in the location path don't exist. -C means the location of the content that is going to be put in the JAR file. output is the source location and . means include files starting from the root of output. Take note that a JAR file is limited to one module.

Next, let's build main.app module. First off, type this commands to compile module-info.java in main.app and Main.java:

javac --module-path build -d output2 main.app\module-info.java
javac --module-path build -d output2 main.app\main\Main.java

--module-path in javac command refers to a path where modules are located. company.modules is in build directory and since that module is an application module, we need to explicitly tell to the compiler where company.modules is located in order to compile the files. Otherwise, the compiler will complain.

Next, it's time to create a JAR file and put the compiled files in it. Type this command to create a JAR file:

jar -c -f build\main.app.jar -e main.Main -C output2 .

-e means location of a class with main method or the entry point. In the example above, Main.java is the entry point and it's located at main package.

Next, let's run our program. To run the program. Type this command:

java --module-path build -m main.app

--module-path in java command refers to a path where the module we wanna run is located. -m refers to the module with main class or entry point. If you successfully ran the program, the result is going to be:

Consumer Created!

You may repeat the steps if you failed to run the program. More details about java commands can be found in this article.

requires keyword has two additional variants:

requires static
requires transitive

requires static adds an optional dependency to a module. For example, take a look at this module descriptor:

module main.app{ requires work.modules; requires static optional.modules; }

During compilation, optional.modules will be checked. During runtime, optional.modules won't be checked. However, if optional.modules becomes present in the module graph(e.g. added manually with --add-modules), modules that have optional dependency to optional.modules can read the module.

One example where requires static can be used is when we set a maven dependency to 'compile' or 'provided' scope. For example, Lombok library is usually provided by a server. Thus, this dependency's scope is usually set to 'provided'. 'provided' scope excludes the dependency in our built project because it's expected to be provided by other sources such as servers.

requires transitive allows a module to read a module that's required by another module. This is called implied readability. For example:

module work.modules{
  exports resources;
}

module company.modules{
  exports inventory;
  requires transitive work.modules;
}

module main.app{
  requires company.modules;
}
In the example above, main.app can read work.modules without explicit requires directive coming from main.app.

exports keyword also has a variant. exports... to is more restrictive than exports. exports... to only allows packages to be exported to selected modules separated with comma(,). For example:
module work.modules{
  exports resources to company.modules, main.app;
}

uses and provides... with Directives

uses and provides... with directives can be used for creating services and service providers. In java, a service a service is a well-known interface or class for which zero, one, or many service providers exist. A service provider (or just provider) is a class that implements or subclasses the well-known interface or class.

Take note that interface or abstract class of a service is used in uses directive not the implementation. Let's create a service. first off, create this file structure:
|company.modules
  |resources
    |ButtonInterface.java
    |ButtonFactory.java
  |module-info.java
|main.app
  |main
    |Main.java
  |module-info.java
After that, create an interface.
package resources;

public interface ButtonInterface{
  void create(String button);
}
Next, create a concrete class that implements ButtonInterface.
package resources;

public class ButtonFactory implements ButtonInterface{
  
  @Override
  public void create(String button){
    System.out.println(button + " created!");
  }
}
In the main method, we can use the service via ServiceLoader.
package main;

import java.util.ServiceLoader;
import resources.ButtonInterface;

public class Main{
  
  public static void main(String[] args){
    ButtonInterface btn = 
    ServiceLoader.load(ButtonInterface.class).
    iterator().next();
    btn.create("Sample Button");
  }
}
Now, go to module-info.java in company.modules and type these directives:

exports resources;
provides resources.ButtonInterface with resources.ButtonFactory;


company.modules is providing the service we created. Thus, it's the service provider. Next, go to module-info.java in company.modules in main.app and type these directives:

requires company.modules;
uses resources.ButtonInterface;

uses directive is in main.app is the user of service. Next, compile the classes in resources package and also the module-info.java in company.modules.
javac -d output company.modules\resources\ButtonInterface.java
javac -d output -cp output company.modules\resources\ButtonFactory.java
jar -c -f build\company.modules.jar -C output .
I added -cp because ButtonFactory.java would look for .class file of ButtonInterface.java. Next, let's compile the classes in main.app package and also the module-info.java in main.app.
javac -d output2 --module-path build main.app\module-info.java
javac -d output2 --module-path build main.app\main\Main.java
jar -c -f build\main.app.jar -e main.Main -C output2 .
Then, run Main.java:

java --module-path build -m main.app

The result is going to be: Sample Button Created!

opens Directive and open Modifier

opens directive and open modifier allow non-public members of a class in a package to be accessed via reflection. open is a modifier that we can add before the module name. For example:
open module my.module{
  exports package1;
  exports package2;
  exports package3;
}
All non-public members of classes in exported packages of my.module can be accessed via reflection. If we don't want our module to be fully exposed, we can use the opens directive. For example:
module my.module{
  exports package1;
  exports package2;
  exports package3;
  
  opens package1;
}
In the example above, only the non-public members of classes of package1 are accessible via reflection. If we want to select which modules can access non-public members of classes of a package via reflection, we can use opens... to directive. For example:
module my.module{
  exports package1;
  opens package1 to module1, module2;
}

No comments:

Post a Comment