Explore Java 9 module systems and reaction flows
New features of Java 9, Java modularization, Java reaction flow, reactive, jigsaw
Modular system
The Java platform module system (jpms) is a feature in Java 9 and a product of the jigsaw project. In short, it organizes packages and types in a simpler and easier to maintain way.
Until Java 8, the system still faced two problems related to type system:
These are what the Java module system has to deal with. Mark Reinhold, chief architect of Java platform of Oracle, described the goals of Java module system:
Java 9 allows you to define modules using module descriptors.
Module descriptor
The module descriptor is the core of the module system. The declaration of the module description is named module info in the root directory of the module directory hierarchy Java file.
The declaration of the module description starts with the module keyword, followed by the module name. The declaration end tag is a pair of braces containing zero or more modules. You can declare an empty module like this:
The instructions you can list in the module declaration are:
Modular application example
The sample application consists of four modules -- model, service, impl, and client. In the actual project, the module should be named using the reverse domain name mode to avoid name conflict. Simple names are used in this example, which is easy to master. The code of each module is in the SRC root directory, and the compiled files are placed in dist.
Let's start with the model module. This module has only one package with a class in it.
package com.stackify.model;
public class Person {
private int id;
private String name;
public Person(int id,String name) {
this.id = id;
this.name = name;
}
}
The module is declared as follows:
module model {
exports com.stackify.model;
opens com.stackify.model;
}
This module exports com stackify. Model package, and introspection is turned on for the secondary package.
An interface is defined in the service module:
package com.stackify.service;
import com.stackify.model.Person;
public interface AccessService {
public String getName(Person person);
}
Since the service module uses com stackify. Model package, so it must rely on the model module. The configuration is as follows:
module service {
requires transitive model;
exports com.stackify.service;
}
Note the transitional keyword in the declaration. The existence of this keyword indicates that all modules that depend on the service module have automatically obtained the access permission of the model module. In order for accessservice to be accessed by other modules, you must use exports to find the package where it is located.
The impl module provides an implementation for accessing services:
package com.stackify.impl;
import com.stackify.service.AccessService;
import com.stackify.model.Person;
import java.lang.reflect.Field;
public class AccessImpl implements AccessService {
public String getName(Person person) {
try {
return extract(person);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private String extract(Person person) throws Exception {
Field field = person.getClass().getDeclaredField("name");
field.setAccessible(true);
return (String) field.get(person);
}
}
Because of the opens declaration in the model module, accessimpl can reflect the person class. The module declaration of impl module is as follows:
module impl {
requires service;
provides com.stackify.service.AccessService with com.stackify.impl.AccessImpl;
}
The import of the model module in the service module is transitional. Therefore, the impl module only needs to reference the service module to obtain access to the two modules. (service,model)
The provides declaration indicates that the impl module provides an implementation for the accessservice interface, which is the accessimpl class.
The client module needs to make the following declaration to consume this accessservice service:
module client {
requires service;
uses com.stackify.service.AccessService;
}
An example of client using this service is as follows:
package com.stackify.client;
import com.stackify.service.AccessService;
import com.stackify.model.Person;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) throws Exception {
AccessService service = ServiceLoader.load(AccessService.class).findFirst().get();
Person person = new Person(1,"John Doe");
String name = service.getName(person);
assert name.equals("John Doe");
}
}
You can see that the accessimpl implementation class is not used in the main function. In fact, the module system runs based on the users and providers in the module definition The instruction automatically locates the specific implementation of the accessservice class.
Compilation and execution
This section describes the steps to compile and execute the modular application you just saw. Note that you must run all the commands in the project root directory (the parent directory of SRC) in order and display them.
Compile the model module and put the generated class file into the dist directory. The command is:
javac -d dist/model src/model/module-info.java src/model/com/stackify/model/Person.java
Since the service module depends on the model module, when you compile the service module, you need to use the - P instruction to specify the path of the dependent module.
javac -d dist/service -p dist src/service/module-info.java src/service/com/stackify/service/AccessService.java
Similarly, the following command shows how to compile impl and client modules:
javac -d dist/impl -p dist src/impl/module-info.java src/impl/com/stackify/impl/AccessImpl.java
javac -d dist/client -p dist src/client/module-info.java src/client/com/stackify/client/Main.java
Assertion declarations are used in the main class, so you need to enable assertions when executing the main program:
java -ea -p dist -m client/com.stackify.client.Main
Note that you need to add the module name before the main class and pass it to the - M option.
Backward compatibility
Before Java 9, there was no concept of "module" in the declaration of all packages. However, this does not prevent you from deploying these packages on a new modular system. You just need to add it to the classpath. As you did in Java 8, the package will become part of the "unnamed" module.
The "unnamed" module reads all other modules, whether they are in the classpath or the module path. Therefore, programs compiled and run on java8 can also run on java9. However, modules with explicit declarations cannot access "unnamed" modules. Here you need another module - automatic module.
You can convert the ancestral old jar package without module declaration into an automatic module by putting it into the module path. This defines a module whose name is derived from the jar file name. Such an automatic module can access all other modules in the module path and expose its own package. Thus, seamless interoperability between packages is realized, whether there are explicit modules or not.
Reaction flow
Reactive flow is a programming paradigm that allows asynchronous data flows to be processed in a non blocking manner with back pressure. In essence, this mechanism puts the receiver under control so that it can determine the amount of data to be transmitted without having to wait for a response after each request.
The Java platform integrates reaction flow as part of Java 9. This integration allows you to leverage reactive streams in a standard way so that various implementations can work together.
Flow class
Java API encapsulates the interface of reaction flow in flow class -- including publisher, subscriber, subscription and processor.
Publisher provides entries and related control information. This interface only defines one method, the subscribe method. This method adds a subscriber (subscriber) to change the subscriber's listening data and the data transmitted by the publisher.
Subscriber receives data from a publisher. This interface defines four methods:
Subscription is used to control the communication between publishers and subscribers. This interface defines two methods: request and cancel. The request method requests the publisher to publish a specific number of items, and canceling will cause the subscriber to unsubscribe.
Sometimes you may want to manipulate data items as they are transferred from the publisher to the subscriber. You can use processor at this time. This interface extends subscriber and publisher so that they can act as publishers from the perspective of publishers and subscribers from the perspective of subscribers.
Internal implementation
The Java platform provides out of the box implementations for publisher and subscription. The implementation class of publisher interface is submissionpublisher In addition to the methods defined in the publisher interface, this class also has other methods, including:
The implementation class of subscription is a private class intended for use only by submissionpublisher. When you call the subscribe method of submissionpublisher with the subscriber parameter, a subscription object is created and passed to the onsubscribe method of that subscriber.
A simple application
With the ready-made implementation of publisher and subscription, you only need to declare an implementation class of subscriber interface to create a reaction flow application. As follows, this class requires a message of type string:
public class StringSubscriber implements Subscriber<String> {
private Subscription subscription;
private StringBuilder buffer;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
this.buffer = new StringBuilder();
subscription.request(1);
}
public String getData() {
return buffer.toString();
}
// other methods
}
As you can see, stringsubscriber stores the subscription obtained when subscribing to a publisher in its private member variable. At the same time, it uses a buffer member to store the string messages it receives. You can retrieve the buffer data through the GetData () method. The onsubscribe method requests the publisher to issue a single data entry.
The onnext method is defined as follows:
@Override
public void onNext(String item) {
buffer.append(item);
if (buffer.length() < 5) {
subscription.request(1);
return;
}
subscription.cancel();
}
This method receives new messages published by publisher and superimposes them on the previous message. When five messages are received, the subscriber stops receiving them.
The implementation of two unimportant methods, onerror and oncomplete, are as follows:
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Data transfer is complete");
}
This test verifies our implementation:
@Test
public void whenTransferingDataDirectly_thenGettingString() throws Exception {
StringSubscriber subscriber = new StringSubscriber();
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
publisher.subscribe(subscriber);
String[] data = { "0","1","2","3","4","5","6","7","8","9" };
Arrays.stream(data).forEach(publisher::submit);
Thread.sleep(100);
publisher.close();
assertEquals("01234",subscriber.getData());
}
In the above test method, the sleep method does nothing but wait for the asynchronous data transmission to complete.
Application processor
Let's add a processor to make this simple application a little more complex. The processor converts the published string into integer and throws an exception if the conversion fails. After the conversion, the processor forwards the result number to the subscriber. The code implementation of subscriber is as follows:
public class NumberSubscriber implements Subscriber<Integer> {
private Subscription subscription;
private int sum;
private int remaining;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
remaining = 1;
}
public int getData() {
return sum;
}
@Override
public void onNext(Integer item) {
sum += item;
if (--remaining == 0) {
subscription.request(3);
remaining = 3;
}
}
}
The code is similar to the previous subscriber and will not be explained. The implementation of processor is as follows:
public class StringToNumberProcessor extends SubmissionPublisher<Integer> implements Subscriber<String> {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
// other methods
}
In this example, the processor inherits submissionpublisher, so it is an abstract method of the subscriber interface. Other methods are:
@Override
public void onNext(String item) {
try {
submit(Integer.parseInt(item));
} catch (NumberFormatException e) {
closeExceptionally(e);
subscription.cancel();
return;
}
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
closeExceptionally(throwable);
}
@Override
public void onComplete() {
System.out.println("Data conversion is complete");
close();
}
Note that when the publisher closes, the processor also needs to close and issue an oncomplete signal to the subscriber. Similarly, when an error occurs -- whether it is processor or publisher -- the processor itself should notify the subscriber of the error.
You can implement the notification flow by calling the close and closeexceptional methods.
The test cases are as follows:
@Test
public void whenProcessingDataMidway_thenGettingNumber() throws Exception {
NumberSubscriber subscriber = new NumberSubscriber();
StringToNumberProcessor processor = new StringToNumberProcessor();
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
processor.subscribe(subscriber);
publisher.subscribe(processor);
String[] data = { "0","9" };
Arrays.stream(data).forEach(publisher::submit);
Thread.sleep(100);
publisher.close();
assertEquals(45,subscriber.getData());
}
API usage
Through the above application, you can better understand the reaction flow. However, they are by no means guidelines for building reactive programs from scratch. Implementing a reaction flow specification is not easy, because the problem it needs to solve is not simple at all. You should use effective libraries (such as rxjava or project reactor) to write efficient applications.
In the future, when many reactive libraries support Java 9, you can even combine various implementations from different tools to make full use of the API.