Explore Java 9 module system and reaction flow

Java 9 new features, Java modularization, Java reaction flow Reactive, Jigsaw

catalog

Modular system

Java platform module system (JPMS) is a feature of Java 9, which is the product of 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 systems:

1. All components (mostly Jar packages) are in classpath without any explicit dependency declaration. Build tools such as Maven can help organize these artifacts during development. However, there is no such support tool at runtime. You may end up missing a class in calsspath, or more importantly, there are two versions of the same class. It is difficult to diagnose such an error.

2. Encapsulation is not supported at the API level. All public classes can be accessed in the whole application. In fact, these classes only want to be called by some other classes. On the other hand, private classes and private members are not private, because you can use reflection to bypass access restrictions.

These are what the Java module system has to deal with. Mark Reinhold, chief architect of Oracle's Java platform, describes the goals of the Java module system:

1. Reliable configuration - replace the fragile and error prone classpath mechanism with the method that program components declare explicit dependencies on each other.

2. Powerful encapsulation - allows components to declare which public types are accessible to other components and which are not.

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 specified in the file named module info.java in the root directory of the module directory hierarchy.

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:

module com.stackify { }

The instructions you can list in the module declaration are:

  • require indicates the module it depends on, also known as dependency.
  • transitive is used only with the require instruction, indicating that the specified dependency can also be accessed by this module's dependency.
  • exports declares a package that can be accessed by other modules
  • Openss exposes a package at runtime for reflection API introspection.
  • uses specifies the fully qualified name of the service consumed by this module.
  • provides with – denotes an implementation, specified by the with keyword, for a service, indicated by provides

Modular application example

  • dist
  • src
    • client
      • com
        • stackify
          • client
            • Main.java
      • module-info.java
    • impl
      • com
        • stackify
          • impl
            • AccessImpl.java
      • module-info.java
    • model
      • com
        • stackify
          • model
            • Person.java
      • module-info.java
    • service
      • com
        • stackify
          • service
            • AccessService.java
      • module-info.java

The sample application consists of four modules - model,service,impl,client. The reverse domain name pattern should be used in the actual project to name the module to avoid name conflicts. The simple name is used in this example, which is easy to master. The code of each module is in the src root directory, and the compiled file will be placed in dist.

Let's start with the model module, which has only one package and one 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 the com.stackify.model package and enables introspection 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 the com.stackify.model package, it must rely on the model module. The configuration is as follows:

module service {
    requires transitive model;
    exports com.stackify.service;
}

Note the transitive 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 the AccessService to be accessible by other modules, you must use exports to locate the package it is in.

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 opening 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 transitive, so the impl module only needs to reference the service module to get access to the two modules. (service,model)

The services declaration indicates that the impl module provides an implementation for the AccessService interface, which is the AccessImpl class.

When the client module consumes this access service service, it needs to make the following statement:

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 automatically locates to the specific implementation of the AccessService class based on the users, supplies... Instruction in the module definition at runtime.

Compile and execute

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 (src's parent directory) in order and display them.

The command to compile the model module and put the generated class file into the dist directory 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 the 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 declaration is used in Main class, so you need to enable assertion when executing Main program:

java -ea -p dist -m client/com.stackify.client.Main

Note that you need to precede the Main class with the module name and pass it to the - m option.

Backward compatibility

Before java9, there was no concept of "module" in the declaration of all packages. However, this does not prevent you from deploying these packages on the new modular system. You just need to add it to the classpath, as you did in java8, and the package will become part of the "unnamed" module.

Unnamed modules read all other modules, whether they are in classpath or module path. As a result, programs compiled and run on java8 can also be run on java9. However, modules with explicit declarations cannot access "unnamed" modules. Here you need another module - the automatic module.

You can convert the old jar package that does not have module declaration from the ancestor to automatic module by putting it in the module path. This defines a module whose name comes from the jar filename. 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, no matter whether there are clear modules or not.

Reaction flow

Reaction flow is a programming paradigm that allows asynchronous data flow to be processed in a non blocking manner with back pressure. Essentially, this mechanism puts the receiver under control so that it can determine the amount of data to transmit without having to wait for a response after each request.

The Java platform integrates the 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

The Java api encapsulates the Flow interface in the Flow class -- Publisher,Subscriber,Subscription, and Processor.

Publisher provides entries and related control information. This interface only defines one method, that is, the subscribe method. This method adds a subscriber (subscriber), and changes the subscriber's listening data and the data transmitted by the publisher.

Subscriber receives data from a Publisher. This interface defines four methods:

  • onSubscribe
  • onNext
  • onError
  • onComplete

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 entries, and cancel causes 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 to act as Publisher and Subscriber from the perspective of Publisher.

Internal implementation

The Java platform provides an out of the box implementation for Publisher and Subscription. The implementation class of the Publisher interface is SubmissionPublisher. In addition to the methods defined in the Publisher interface, this class has other methods, including:

  • submit publishes an entry to each subscriber
  • close sends an onComplete signal to each subscriber and forbids subsequent subscriptions

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 that subscriber's onSubscribe method.

A simple application

With the off the shelf implementation of Publisher and Subscription, you only need to declare an implementation class of the Subscriber interface to create a reaction flow application. As follows, this class needs 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 it gets 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. After receiving 5 messages, the subscriber stops receiving them.

The two unimportant methods of onError and onComplete are implemented 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 an 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, no explanation will be made. 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 case, the processor inherits the SubmissionPublisher, so the abstract method of the Subscriber interface. The 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 publisher is closed, the processor also needs to be closed and issue the onComplete signal to the subscriber. Similarly, when an error occurs - either processor or publisher - the processor itself should notify the subscriber of the error.

You can implement the notification flow by calling the close and closeExceptionally 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", "1", "2", "3", "4", "5", "6", "7", "8", "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. It is not easy to implement a reaction flow specification, because the problem it has 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 the most of the API.

summary

This article covers two core technologies in Java 9 - modular systems and reaction flows.

Modular systems are new features and are expected to be widely used in the near future. However, it is inevitable for the whole java world to move towards modular systems, and you should be prepared for this.

Reaction flow has been around for some time, and the introduction of Java 9 helps standardize the paradigm, which may speed up its use.

Original address: https://stackify.com/exploring-java-9-module-system-and-reactive-streams/

Tags: Java Maven Oracle Programming

Posted on Sun, 17 May 2020 21:08:21 -0700 by beachdaze