It's been mostly a year since Java 9 release finally delivered Project Jigsaw to the masses. It was a long, long journey, but it is there, so what has changed? This is a very good question and the answer to it is not obvious and straightforward.
By and large, Project Jigsaw is a disruptive change and there are many reasons why. Although mostly all of our existing application are going to run on Java 10 (to be replaced by JDK 11 very soon) with minimal or no changes, there are deep and profound implications Project Jigsaw brings to the Java developers: embrace the modular applications the Java platform way.
With the myriads of awesome frameworks and libraries out there, it will surely take time, a lot of time, to convert them to Java modules (many will not ever make it). This path is thorny but there are certain things which are already possible even today. In this rather short post we are going to learn how to use terrific Apache CXF project to build JAX-RS 2.1 Web APIs in a truly modular fashion using latest JDK 10.
Since 3.2.5 release, all Apache CXF artifacts have their manifests enriched with an Automatic-Module-Name directive. It does not make them full-fledged modules, but this is a first step in the right direction. So let us get started ...
If you use Apache Maven as the build tool of choice, not much changed here, the dependencies are declared the same way as before.
<dependencies> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxrs</artifactId> <version>3.2.5</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.jaxrs</groupId> <artifactId>jackson-jaxrs-json-provider</artifactId> <version>2.9.6</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>9.4.11.v20180605</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-webapp</artifactId> <version>9.4.11.v20180605</version> </dependency> </dependencies>
The uber-jar or fat-jar packaging are not really applicable to the modular Java applications so we have to collect the modules ourselves, for example at the target/modules folder.
<plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> <configuration> <outputDirectory>${project.build.directory}/modules</outputDirectory> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/modules</outputDirectory> <includeScope>runtime</includeScope> </configuration> </execution> </executions> </plugin>
All good, the next step is to create the module-info.java and list there the name of our module (com.example.cxf in this case) and, among other things, all required modules it needs in order to be functionable.
module com.example.cxf { exports com.example.rest; requires org.apache.cxf.frontend.jaxrs; requires org.apache.cxf.transport.http; requires com.fasterxml.jackson.jaxrs.json; requires transitive java.ws.rs; requires javax.servlet.api; requires jetty.server; requires jetty.servlet; requires jetty.util; requires java.xml.bind; }
As you may spot right away, org.apache.cxf.frontend.jaxrs and org.apache.cxf.transport.http come from Apache CXF distribution (the complete list is available in the documentation) whereas java.ws.rs is the JAX-RS 2.1 API module. After that we could proceed with implementing our JAX-RS resources the same way we did before.
@Path("/api/people") public class PeopleRestService { @GET @Produces(MediaType.APPLICATION_JSON) public Collection<Person> getAll() { return List.of(new Person("John", "Smith", "john.smith@somewhere.com")); } }
This looks easy, how about adding some spicy sauce, like server-sent events (SSE) and RxJava, for example? Let us see how exceptionally easy it is, starting from dependencies.
<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-rs-sse</artifactId> <version>3.2.5</version> </dependency> <dependency> <groupId>io.reactivex.rxjava2</groupId> <artifactId>rxjava</artifactId> <version>2.1.14</version> </dependency>
Also, we should not forget to update our module-info.java by adding the requires directive to these new modules.
module com.example.cxf { ... requires org.apache.cxf.rs.sse; requires io.reactivex.rxjava2; requires transitive org.reactivestreams; ... }
In order to keep things simple, our SSE endpoint would just broadcast every new person added through the API. Here is the implementation snippet which does it.
private SseBroadcaster broadcaster; private Builder builder; private PublishSubject<Person> publisher; public PeopleRestService() { publisher = PublishSubject.create(); } @Context public void setSse(Sse sse) { this.broadcaster = sse.newBroadcaster(); this.builder = sse.newEventBuilder(); publisher .subscribeOn(Schedulers.single()) .map(person -> createEvent(builder, person)) .subscribe(broadcaster::broadcast); } @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response add(@Context UriInfo uriInfo, Person payload) { publisher.onNext(payload); return Response .created( uriInfo .getRequestUriBuilder() .path(payload.getEmail()) .build()) .entity(payload) .build(); } @GET @Path("/sse") @Produces(MediaType.SERVER_SENT_EVENTS) public void people(@Context SseEventSink sink) { broadcaster.register(sink); }
Now when we build it:
mvn clean package
And run it using module path:
java --add-modules java.xml.bind \ --module-path target/modules \ --module com.example.cxf/com.example.Starter
We should be able to give our JAX-RS API a test drive. The simplest way to make sure things work as expected is to navigate in the Google Chrome to the SSE endpoint http://localhost:8686/api/people/sse and add some random people through the POST requests, using the old buddy curl from the command line:
curl -X POST http://localhost:8686/api/people \ -d '{"email": "john@smith.com", "firstName": "John", "lastName": "Smith"}' \ -H "Content-Type: application/json"
curl -X POST http://localhost:8686/api/people \ -d '{"email": "tom@tommyknocker.com", "firstName": "Tom", "lastName": "Tommyknocker"}' \ -H "Content-Type: application/json"
In the Google Chrome we should be able to see raw SSE events, pushed by the server (they are not looking pretty but good enough to illustrate the flow).
So, what about the application packaging? Docker and containers are certainly a viable option, but with Java 9 and above we have another player: jlink. It assembles and optimizes a set of modules and their dependencies into a custom, fully sufficient runtime image. Let us try it out.
jlink --add-modules java.xml.bind,java.management \ --module-path target/modules \ --verbose \ --strip-debug \ --compress 2 \ --no-header-files \ --no-man-pages \ --output target/cxf-java-10-app
Here we are hitting the first wall. Unfortunately, since mostly all the dependencies of our application are automatic modules, it is a problem for jlink and we still have to include module path explicitly when running from the runtime image:
target/cxf-java-10-app/bin/java \ --add-modules java.xml.bind \ --module-path target/modules \ --module com.example.cxf/com.example.Starter
At the end of the day it turned out to be not that scary. We are surely in the very early stage of the JPMS adoption, this is just a beginning. When every library, every framework we are using adds the module-info.java to their artifacts (JARs), making them true modules despite all the quirks, then we could declare a victory. But the small wins are already happening, make one yours!
The complete source of the project is available on Github.