Embracing a Modular Java Platform: Apache CXF on Java 10
Want to learn more about using Apache CXF in Java 10? Check out this tutorial on Apache CXF, Project Jigsaw, and JAR files to learn more.
Join the DZone community and get the full member experience.
Join For FreeIt's been almost a year since the Java 9 release finally delivered Project Jigsaw to the masses. It was a long, long journey, but it is finally here, so what has changed?
By and large, Project Jigsaw is a disruptive change, and there are many reasons why. Although, for the most part, all of our existing applications 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 take a lot of time to convert them to Java modules, and many will probably not even make it. This path is thorny, but there are certain things that are already possible. In this rather short post, we are going to learn how to use Apache CXF project to build JAX-RS 2.1 Web APIs in a truly modular fashion using latest JDK 10.
Since the 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 is 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>
The next step is to create the module-info.java
and list the name of our module —in this case, com.example.cxf
. Among other things, we will require all the modules it needs in order to be functional.
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 the Apache CXF distribution (the complete list is available here), 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 with 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 broadcast every new person added through the API. Here is the implementation snippet demonstrating 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, we need to run it using the 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 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 it's 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 stages of the JPMS adoption, but this is just a beginning. When every library and every framework are adding the module-info.java
to their artifacts (JARs) and 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.
Published at DZone with permission of Andriy Redko, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments