Showing posts with label jax-rs. Show all posts
Showing posts with label jax-rs. Show all posts

Thursday, December 26, 2024

Simple is finally easy: bootstrapping JAX-RS applications in Java SE environments

It has been a while since Jakarta EE 10 was released but the ecosystem is slowly (but steadily!) catching up. The Apache CXF project landed new 4.1.0 release very recently that delivers Jakarta EE 10 compatibility, specifically implementation of the Jakarta RESTful Web Services 3.1 specification (also known as JAX-RS).

One of the most exciting (in my option) features that Jakarta RESTful Web Services 3.1 includes is bootstrapping JAX-RS applications in Java SE environments. From now on, creating the full-fledged RESTful web services on the JVM becomes not only easy, but very straightforward! In today's post, we are going to build a sample RESTful web service and host it inside the Java SE application, with a catch - no boilerplate allowed.

The PeopleRestService, presented in the snippet below, is a minimalistic example of the typical Jakarta RESTful web service: for a sake of keeping things simple, it does not do anything useful besides returning the predefined data back.

import java.util.Collection;
import java.util.List;

import com.example.jakarta.restful.bootstrap.model.Person;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/people")
public class PeopleRestService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Collection<Person> getPeople() {
        return List.of(new Person("a@b.com", "John", "Smith"));
    }
}

The Person class contains only three fields: email, firstName and lastName.

  
public class Person {
    private String email;
    private String firstName;
    private String lastName;
    // Skipping the getters and setters for brevity
}

Essentially, this is all we need at this point. Now the hardest part, how to expose the PeopleRestService to the outside world? Here is the moment for SeBootstrap to take the stage. Its entire purpose is allowing to startup a JAX-RS application in Java SE environments, without (explicitly) requiring the presence of the web container or application server. How does it look like in practice?

  
import java.util.Set;
import java.util.concurrent.CompletionStage;

import com.example.jakarta.restful.bootstrap.rs.PeopleRestService;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.Application;

public class BootstrapRunner {
    @ApplicationPath("/api")
    public static final class JakartaRestfulApplication extends Application {
        @Override
        public Set<Object> getSingletons() {
            return Set.of(new PeopleRestService());
        }
    }

    public static void main(String[] args) {
        final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
            .builder()
            .property(SeBootstrap.Configuration.PROTOCOL, "http")
            .property(SeBootstrap.Configuration.PORT, 10800)
            .property(SeBootstrap.Configuration.ROOT_PATH, "/")
            .build();
    
        SeBootstrap
            .start(new JakartaRestfulApplication(), configuration)
            .toCompletableFuture()
            .join();
    }
}

As simple as that: pass the port (10800), protocol (HTTP) and root path (/) through SeBootstrap.Configuration along with Application subclass (JakartaRestfulApplication) instance to SeBootstrap::start method. To complete the puzzle, here are all the dependencies that are required by our Java SE application (taken from project's Apache Maven pom.xml file).

  
<dependencies>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-frontend-jaxrs</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-rs-extension-providers</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http-jetty</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>jakarta.json</groupId>
		<artifactId>jakarta.json-api</artifactId>
		<version>2.1.3</version>
	</dependency>
	<dependency>
		<groupId>jakarta.json.bind</groupId>
		<artifactId>jakarta.json.bind-api</artifactId>
		<version>3.0.1</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.5.15</version>
	</dependency>
	<dependency>
		<groupId>org.eclipse</groupId>
		<artifactId>yasson</artifactId>
		<version>3.0.4</version>
	</dependency>
</dependencies>

Nothing special, except may be Eclipse Yasson, the JSON-B implementation provider. It is time to make sure everything actually works!

$ mvn clean package

...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

$ java -jar target/cxf-jakarta-restful-3.1-bootstrap-0.0.1-SNAPSHOT.jar
Dec 24, 2024 2:43:45 P.M. org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be http://:10800/api
14:43:46.129 [onPool-worker-1] INFO rg.eclipse.jetty.server.Server - jetty-12.0.15; built: 2024-11-05T19:44:57.623Z; git: 8281ae9740d4b4225e8166cc476bad237c70213a; jvm 23.0.1+8-FR
14:43:46.288 [onPool-worker-1] INFO jetty.server.AbstractConnector - Started ServerConnector@7b66a8d{HTTP/1.1, (http/1.1)}{:10800}
14:43:46.302 [onPool-worker-1] INFO rg.eclipse.jetty.server.Server - Started oejs.Server@7683694f{STARTING}[12.0.15,sto=0] @1701ms
14:43:46.349 [onPool-worker-1] INFO .server.handler.ContextHandler - Started oeje10s.ServletContextHandler@4b04a638{ROOT,/,b=null,a=AVAILABLE,h=oeje10s.ServletHandler@23ae88f1{STARTED}}

With the application up and running, we are ready to invoke the http://localhost:10800/api/people HTTP endpoint (the only one our Jakarta RESTful web service exposes).

$ curl http://localhost:10800/api/people -iv
* Host localhost:10800 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:10800...
*   Trying 127.0.0.1:10800...
* Connected to localhost (127.0.0.1) port 10800
* using HTTP/1.x
> GET /api/people HTTP/1.1
> Host: localhost:10800
> User-Agent: curl/8.11.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: Jetty(12.0.15)
Server: Jetty(12.0.15)
< Date: Tue, 24 Dec 2024 19:52:10 GMT
Date: Tue, 24 Dec 2024 19:52:10 GMT
< Content-Type: application/json
Content-Type: application/json
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
<

[{"email":"a@b.com","firstName":"John","lastName":"Smith"}]

And here we are, we get the response with our hardcoded list of people, no surprises! I hope you would agree, the bootstrapping process is very simple and easy to follow. Even more, you could integrate SeBootstrap in your test suites as well, thanks to its flexible configuration capabilities, for example:

  
final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
    .builder()
    // Use random free port
    .property(SeBootstrap.Configuration.PORT, SeBootstrap.Configuration.FREE_PORT)
    ...
    .build();

// Start the instance
final Instance instance = SeBootstrap
    .start(new JakartaRestfulApplication(), configuration)
    .toCompletableFuture()
    .join();
    
final SeBootstrap.Configuration actual = instance.configuration();
// Use actual.port(), actual.host(), ...
...

// Stop the instance
instance
    .stop()
    .toCompletableFuture()
    .join();

It is worth to mention that bootstrapping secure Jakarta RESTful Web Services using HTTPS protocol is also supported, for example:

 
final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
    .builder()
    .property(SeBootstrap.Configuration.PROTOCOL, "https")
    .property(SeBootstrap.Configuration.PORT, 10843)
    .property(SeBootstrap.Configuration.ROOT_PATH, "/")
    .sslContext(SSLContext.getDefault()) /* or supply your own */
    .build();

The complete source code of the project is available on Github.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.

Friday, August 30, 2024

Apache CXF at speed of ... native!

GraalVM has been around for quite a while, steadily making big waves in OpenJDK community (looking at you, JEP 483: Ahead-of-Time Class Loading & Linking). It is wonderful piece of JVM engineering that gave birth to new generation of the frameworks like Quarkus, Helidon and Micronaut, just to name a few.

But what about the old players, like Apache CXF? A large number of applications and services were built on top of it, could those benefit from GraalVM, and particularly native image compilation? The answer to this question used to vary a lot, but thanks to steady progress, GraalVM strives to make it as frictionless as possible.

In today's post, we are going to build a sample Jakarta RESTful web service using Apache CXF and Jakarta XML Binding, and compile it to native image with GraalVM Community 21.0.2+13.1.

Let us start off with the data model, which consists of a single POJO, class Customer.

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "Customer")
public class Customer {
    private long id;
    private String name;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The class CustomerResource, a minimal Jakarta RESTful web service implementation, exposes a few endpoints to manage Customers, for simplicity - the state is stored in memory.

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

@Path("/")
@Produces(MediaType.APPLICATION_XML)
public class CustomerResource {
    private final AtomicInteger id = new AtomicInteger();
    private final Map<Long, Customer> customers = new HashMap<>();

    @GET
    @Path("/customers")
    public Collection<Customer> getCustomers() {
        return customers.values();
    }

    @GET
    @Path("/customers/{id}")
    public Response getCustomer(@PathParam("id") long id) {
        final Customer customer = customers.get(id);
        if (customer != null) {
            return Response.ok(customer).build();
        } else {
            return Response.status(Status.NOT_FOUND).build();
        }
    }

    @POST
    @Path("/customers")
    public Response addCustomer(Customer customer) {
        customer.setId(id.incrementAndGet());
        customers.put(customer.getId(), customer);
        return Response.ok(customer).build();
    }

    @DELETE
    @Path("/customers/{id}")
    public Response deleteCustomer(@PathParam("id") long id) {
        if (customers.remove(id) != null) {
            return Response.noContent().build();
        } else {
            return Response.status(Status.NOT_FOUND).build();
        }
    }
}

The last piece we need is to have running web container to host the CustomerResource service. We are going to use Eclipse Jetty but any other HTTP transport supported by Apache CXF will do the job.

import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;

public class Server {
    public static org.apache.cxf.endpoint.Server create() {
        final JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
        sf.setResourceClasses(CustomerResource.class);
        sf.setResourceProvider(CustomerResource.class, new SingletonResourceProvider(new CustomerResource()));
        sf.setAddress("http://localhost:9000/");
        return sf.create();
    }
    
    public static void main(String[] args) throws Exception {
        var server = create();
        server.start();
    }
}

Literally, this is all we need from the implementation perspective. The Apache Maven dependencies list is limited to handful of those:

<dependencies>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http</artifactId>
		<version>4.0.5</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http-jetty</artifactId>
		<version>4.0.5</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-frontend-jaxrs</artifactId>
		<version>4.0.5</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.5.7</version>
	</dependency>
</dependencies>

Cool, so what is next? The GraalVM project provides Native Build Tools to faciliate building native images, including dedicated Apache Maven plugin. However, if we just add the plugin into the build, the resulting native image won't be functionable, even if the build succeeds:

$./target/cxf-jax-rs-graalvm-server

Exception in thread "main" java.lang.ExceptionInInitializerError
        at java.base@21.0.2/java.lang.Class.ensureInitialized(DynamicHub.java:601)
        at com.example.jaxrs.graalvm.Server.create(Server.java:27)
        at com.example.jaxrs.graalvm.Server.main(Server.java:35)
        at java.base@21.0.2/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
Caused by: java.util.MissingResourceException: Can't find bundle for base name org.apache.cxf.jaxrs.Messages, locale en
        at java.base@21.0.2/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2059)
        at java.base@21.0.2/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1697)
        at java.base@21.0.2/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1600)
        at java.base@21.0.2/java.util.ResourceBundle.getBundle(ResourceBundle.java:1283)
        at org.apache.cxf.common.i18n.BundleUtils.getBundle(BundleUtils.java:94)
        at org.apache.cxf.jaxrs.AbstractJAXRSFactoryBean.<clinit>(AbstractJAXRSFactoryBean.java:69)
        ... 4 more

Why is that? GraalVM operates under closed world assumption: all classes and all bytecodes that are reachable at run time must be known at build time. Since a majority of the frameworks, Apache CXF included, does not comply with such assumptions, GraalVM needs some help: tracing agent. The way we are going to let GraalVM capture all necessary metadata is pretty straightforward:

  • add test cases which exercise the service logic (more is better)
  • run test suite using tracing agent instrumentation
  • build the native image using the metadata collected by the tracing agent

If that sounds like a plan to you, let us add the test case first:

import java.io.InputStream;
import java.io.IOException;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.Response;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

public class ServerTest {
    private org.apache.cxf.endpoint.Server server;
    
    @BeforeEach
    public void setUp() {
        server = Server.create();
        server.start();
    }
    
    @Test
    public void addNewCustomer() throws IOException {
        var client = ClientBuilder.newClient().target("http://localhost:9000/customers");
        try (InputStream in = getClass().getResourceAsStream("/add_customer.xml")) {
            try (Response response = client.request().post(Entity.xml(in))) {
                assertThat(response.getStatus(), equalTo(200));
            }
        }
    }

    @Test
    public void listCustomers() {
        var client = ClientBuilder.newClient().target("http://localhost:9000/customers");
        try (Response response = client.request().get()) {
            assertThat(response.getStatus(), equalTo(200));
        }
    }

    @AfterEach
    public void tearDown() {
        server.stop();
        server.destroy();
    }
}

Awesome, with tests in place, we could move on and integrate Native Build Tools into our Apache Maven build. It is established practice to have a dedicated profile for native image since the process could take quite a lot of time (and resources):

<profiles>
	<profile>
		<id>native-image</id>
		<activation>
			<property>
				<name>native</name>
			</property>
		</activation>
		<build>
			<plugins>
				<plugin>
					<groupId>org.graalvm.buildtools</groupId>
					<artifactId>native-maven-plugin</artifactId>
					<extensions>true</extensions>
					<version>0.10.2</version>
					<executions>
						<execution>
							<goals>
								<goal>compile-no-fork</goal>
							</goals>
							<phase>package</phase>
						</execution>
					</executions>
					<configuration>
						<agent>
							<enabled>true</enabled>
							<defaultMode>direct</defaultMode>
							<modes>
								<direct>config-output-dir=${project.build.directory}/native/agent-output</direct>
							</modes>
						</agent>
						<mainClass>com.example.jaxrs.graalvm.Server</mainClass>
						<imageName>cxf-jax-rs-graalvm-server</imageName>
						<buildArgs>
							<buildArg>--enable-url-protocols=http</buildArg>
							<buildArg>--no-fallback</buildArg>
							<buildArg>-Ob</buildArg>
						</buildArgs>
						<metadataRepository>
							<enabled>false</enabled>
						</metadataRepository>
						<resourcesConfigDirectory>${project.build.directory}/native</resourcesConfigDirectory>
					</configuration>
				</plugin>
			</plugins>
		</build>
	</profile>
</profiles>

It may look a bit complicated but fear not. The first thing to notice is that we configure tracing agent in the <agent> ... </agent> section. The captured metadata is going to be dumped into ${project.build.directory}/native/agent-output folder. Later on, the native image builder will refer to it as part of the <resourcesConfigDirectory> ... </resourcesConfigDirectory> configuration option. The profile is activated by the presence of native property.

Time to see each step in action! First thing first, run tests and capture the metadata:

$ mvn clean -Dnative test

...

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  6.977 s
[INFO] Finished at: 2024-08-30T15:50:55-04:00
[INFO] ------------------------------------------------------------------------

If we list the content of the target/native folder, we should see something like that:

$ tree target/native/

target/native/
└── agent-output
    ├── agent-extracted-predefined-classes
    ├── jni-config.json
    ├── predefined-classes-config.json
    ├── proxy-config.json
    ├── reflect-config.json
    ├── resource-config.json
    └── serialization-config.json    

If curious, you could inspect the content of each file, since it is just JSON, but we are going to proceed to the next step right away:

$ mvn -Dnative -DskipTests package

GraalVM Native Image: Generating 'cxf-jax-rs-graalvm-server' (executable)...
========================================================================================================================
Warning: Could not resolve org.junit.platform.launcher.TestIdentifier$SerializedForm for serialization configuration.
Warning: Could not resolve org.junit.platform.launcher.TestIdentifier$SerializedForm for serialization configuration.
[1/8] Initializing...                                                                                    (5.7s @ 0.10GB)
 Java version: 21.0.2+13, vendor version: GraalVM CE 21.0.2+13.1
 Graal compiler: optimization level: b, target machine: x86-64-v3
 C compiler: gcc (linux, x86_64, 9.4.0)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
 2 user-specific feature(s):
 - com.oracle.svm.thirdparty.gson.GsonFeature
 - org.eclipse.angus.activation.nativeimage.AngusActivationFeature
------------------------------------------------------------------------------------------------------------------------
Build resources:
 - 9.99GB of memory (64.5% of 15.49GB system memory, determined at start)
 - 16 thread(s) (100.0% of 16 available processor(s), determined at start)
[2/8] Performing analysis...  [*****]                                                                   (31.2s @ 1.59GB)
   12,363 reachable types   (85.2% of   14,503 total)
   21,723 reachable fields  (63.2% of   34,385 total)
   61,933 reachable methods (57.6% of  107,578 total)
    3,856 types,   210 fields, and 2,436 methods registered for reflection
       62 types,    69 fields, and    55 methods registered for JNI access
        4 native libraries: dl, pthread, rt, z
[3/8] Building universe...                                                                               (4.8s @ 1.85GB)
[4/8] Parsing methods...      [**]                                                                       (3.0s @ 1.16GB)
[5/8] Inlining methods...     [***]                                                                      (2.2s @ 1.35GB)
[6/8] Compiling methods...    [*****]                                                                   (25.4s @ 1.89GB)
[7/8] Layouting methods...    [***]                                                                      (6.2s @ 1.49GB)
[8/8] Creating image...       [***]                                                                      (7.5s @ 2.03GB)
  32.41MB (49.57%) for code area:    38,638 compilation units
  30.78MB (47.08%) for image heap:  325,764 objects and 152 resources
   2.19MB ( 3.35%) for other data
  65.38MB in total

...

========================================================================================================================
Finished generating 'cxf-jax-rs-graalvm-server' in 1m 26s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:30 min
[INFO] Finished at: 2024-08-30T15:58:42-04:00
[INFO] ------------------------------------------------------------------------

And we should end up with a fully functional executable, let us make sure this is the case:

$ ./target/cxf-jax-rs-graalvm-server

Aug 30, 2024 4:03:22 PM org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be http://localhost:9000/
16:03:22.987 [main] INFO  org.eclipse.jetty.server.Server -- jetty-11.0.22; built: 2024-06-27T16:27:26.756Z; git: e711d4c7040cb1e61aa68cb248fa7280b734a3bb; jvm 21.0.2+13-jvmci-23.1-b30
16:03:22.993 [main] INFO  o.e.jetty.server.AbstractConnector -- Started ServerConnector@5725648b{HTTP/1.1, (http/1.1)}{localhost:9000}
16:03:22.994 [main] INFO  org.eclipse.jetty.server.Server -- Started Server@1f9511a6{STARTING}[11.0.22,sto=0] @24ms
16:03:22.994 [main] INFO  o.e.j.server.handler.ContextHandler -- Started o.a.c.t.h.JettyContextHandler@375bd1b5{/,null,AVAILABLE}

If we open up another terminal window and run curl from the command line, we should be hitting the instance of our service and getting successful responses back:

$curl http://localhost:9000/customers -H "Content-Type: application/xml" -d @src/test/resources/add_customer.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Customer>
	<id>3</id>
    <name>Jack</name>
</Customer>

All the credits go to GraalVM team! Before we wrap up, you might be asking yourself if this the only way? And the short answer is "no": ideally, you should be able to add Native Build Tools and be good to go. The GraalVM Reachability Metadata Repository is the place that enables users of GraalVM native image to share and reuse metadata for libraries and frameworks in the Java ecosystem. Sadly, Apache CXF is not there just yet ... as many others.

The complete project sources are available on Github.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.

Thursday, January 20, 2022

So you want to expose your JAX-RS services over HTTP/2 ...

Nonetheless HTTP/2 is about six years old (already!), and HTTP/3 is around the corner, it looks like the majority of the web applications and systems are stuck in time, operating over HTTP/1.x protocol. And we are not even talking about legacy systems, it is not difficult to stumble upon greenfield web applications that ignore the existence of the HTTP/2 in principle. A few years ago the excuses like "immature HTTP/2 support by container of my choice" might have been justified but these days all the major web containers (Jetty, Apache Tomcat, Netty, Undertow) offer a first class HTTP/2 support, so why not use it?

The today's post is all about exposing and consuming your JAX-RS services over HTTP/2 protocol using latest 3.5.0 release of the Apache CXF framework, a compliant JAX-RS 2.1 implementation. Although HTTP/2 does not require encryption, it is absolutely necessary these days for deploying real-world production systems. With that being said, we are going to cover both options: h2c (HTTP/2 over clear text, useful for development) and regular h2 (HTTP/2 over TLS).

Our JAX-RS resource, PeopleResource, exposes only one @GET endpoint with hardcoded response specification (to keeps things simple here):

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.example.model.Person;

import reactor.core.publisher.Flux;

@Path("/people")
public class PeopleResource {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Flux<Person> getPeople() {
        return Flux.just(new Person("a@b.com", "Tom", "Knocker"));
    }
}

The usage of reactive types (Project Reactor in this case) is intentional here since this is most likely what you are going to end up with (but to be fair, not a requirement).

<dependency>
	<groupId>io.projectreactor</groupId>
	<artifactId>reactor-core</artifactId>
	<version>3.4.14</version>
</dependency>

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-rs-extension-reactor</artifactId>
	<version>3.5.0</version>
</dependency>

To be noted, other options like RxJava3 / RxJava2 are also available out of the box. The Person model is as straightforward as it gets:

public class Person {
    private String email;
    private String firstName;
    private String lastName;
    
    // Getters and setters here
}

To benefit from HTTP/2 support, you need to pick your web server/container (Jetty, Netty, or Undertow) and (optionally) include a couple of additional dependencies (which might be specific to server/container and/or JDK version you are using). The official documentation has it covered in great details, for demonstration purposes we are going to use Jetty (9.4.44.v20210927) and run on JDK-17, the latest LTS version of the OpenJDK.

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-transports-http-jetty</artifactId>
	<version>3.5.0</version>
</dependency>

<dependency>
	<groupId>org.eclipse.jetty.http2</groupId>
	<artifactId>http2-server</artifactId>
	<version>9.4.44.v20210927</version>
</dependency>

<dependency>
	<groupId>org.eclipse.jetty</groupId>
	<artifactId>jetty-alpn-server</artifactId>
	<version>9.4.44.v20210927</version>
</dependency>

<dependency>
	<groupId>org.eclipse.jetty</groupId>
	<artifactId>jetty-alpn-java-server</artifactId>
	<version>9.4.44.v20210927</version>
</dependency>

Apache CXF lets you package and run your services as standalone executable JARs (or GraalVM's native images in certain cases), no additional frameworks required besides the main class.

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.jaxrs.utils.JAXRSServerFactoryCustomizationUtils;
import org.apache.cxf.transport.http.HttpServerEngineSupport;

import com.example.rest.PeopleResource;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;

public class ServerStarter {
    public static void main( final String[] args ) throws Exception {
        final Bus bus = BusFactory.getDefaultBus();
        bus.setProperty(HttpServerEngineSupport.ENABLE_HTTP2, true);

        final JAXRSServerFactoryBean bean = new JAXRSServerFactoryBean();
        bean.setResourceClasses(PeopleResource.class);
        bean.setResourceProvider(PeopleResource.class, 
            new SingletonResourceProvider(new PeopleResource()));
        bean.setAddress("http://localhost:19091/services");
        bean.setProvider(new JacksonJsonProvider());
        bean.setBus(bus);
        
        JAXRSServerFactoryCustomizationUtils.customize(bean);
        Server server = bean.create();
        server.start();
    }
}

The key configuration here is HttpServerEngineSupport.ENABLE_HTTP2 property which has to be set to true in order to notify the transport provider of your choice to turn HTTP/2 support on. Without TLS configuration your JAX-RS resources become accessible over h2c (HTTP/2 over clear text), additionally to HTTP/1.1. Let us give it a try right away (please make sure your have JDK-17 available by default).

$ mvn clean package
$ java -jar target/jaxrs-standalone-jetty-http2-0.0.1-SNAPSHOT-h2c.jar

[INFO] 2022-01-16 11:11:16.255 org.apache.cxf.endpoint.ServerImpl -[] Setting the server's publish address to be http://localhost:19091/services
[INFO] 2022-01-16 11:11:16.322 org.eclipse.jetty.util.log -[] Logging initialized @482ms to org.eclipse.jetty.util.log.Slf4jLog
[INFO] 2022-01-16 11:11:16.361 org.eclipse.jetty.server.Server -[] jetty-9.4.44.v20210927; built: 2021-09-27T23:02:44.612Z; git: 8da83308eeca865e495e53ef315a249d63ba9332; jvm 17+35-2724
[INFO] 2022-01-16 11:11:16.449 o.e.jetty.server.AbstractConnector -[] Started ServerConnector@3f4faf53{HTTP/1.1, (h2c, http/1.1)}{localhost:19091}
[INFO] 2022-01-16 11:11:16.449 org.eclipse.jetty.server.Server -[] Started @613ms
[WARN] 2022-01-16 11:11:16.451 o.e.j.server.handler.ContextHandler -[] Empty contextPath
[INFO] 2022-01-16 11:11:16.466 o.e.j.server.handler.ContextHandler -[] Started o.e.j.s.h.ContextHandler@495ee280{/,null,AVAILABLE}
...

It is as simple as that, if you don't believe it, Jetty dumps quite handy message in the console regarding the supported protocols: {HTTP/1.1, (h2c, http/1.1)}. The Swiss army knife of the web developer, curl, is the easiest way to verify things are working as expected.

$ curl http://localhost:19091/services/people --http2 -iv                                                                                                 
...
* Connected to localhost (127.0.0.1) port 19091 (#0)                                                                                                      
> GET /services/people HTTP/1.1
> Host: localhost:19091
> User-Agent: curl/7.71.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA
...
* Mark bundle as not supporting multiuse                                                                                                                  
< HTTP/1.1 101 Switching Protocols 
* Received 101                                                                                                                                            
* Using HTTP2, server supports multi-use                                                                                                                  
* Connection state changed (HTTP/2 confirmed)                                                                                                             
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0                                                                          
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!                                                                                               
< HTTP/2 200                                                                                                                                             
< server: Jetty(9.4.44.v20210927)                                                                                                                       
< date: Sun, 16 Jan 2022 17:08:08 GMT                                                                                                                   
< content-type: application/json
< content-length: 60
...
HTTP/1.1 101 Switching Protocols
HTTP/2 200
server: Jetty(9.4.44.v20210927)                                                                                                                           
date: Sun, 16 Jan 2022 17:08:08 GMT                                                                                                                       
content-type: application/json                                                                                                                            
content-length: 60                                                                                                                                        
                                                                                                                                                          
[{"email":"a@b.com","firstName":"Tom","lastName":"Knocker"}]                                                                                              

There is something interesting happening here. Nonetheless we have asked for HTTP/2, the client connects over HTTP/1.1 first and only than switches the protocol (HTTP/1.1 101 Switching Protocols) to HTTP/2. This is expected for HTTP/2 over clear text (h2c), however we could use HTTP/2 prior knowledge to skip over the protocol upgrade steps.

$ curl http://localhost:19091/services/people --http2-prior-knowledge -iv                                                                 
...
* Connected to localhost (127.0.0.1) port 19091 (#0)                                                                                      
* Using HTTP2, server supports multi-use                                                                                                  
* Connection state changed (HTTP/2 confirmed)                                                                                             
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0                                                          
* Using Stream ID: 1 (easy handle 0x274df30)                                                                                              
> GET /services/people HTTP/2                                                                                                             
> Host: localhost:19091                                                                                                                   
> user-agent: curl/7.71.1                                                                                                                 
> accept: */*                                                                                                                             
>                                                                                                                                         
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!                                                                               
< HTTP/2 200                                                                                                                              
< server: Jetty(9.4.44.v20210927)                                                                                                         
< date: Sun, 16 Jan 2022 17:06:40 GMT                                                                                                     
< content-type: application/json                                                                                                          
< content-length: 60                                                                                                                    
...
HTTP/2 200                                                  
server: Jetty(9.4.44.v20210927)                                                                                                           
date: Sun, 16 Jan 2022 17:06:40 GMT                                                                                                       
content-type: application/json                                                                                                            
content-length: 60                                                                                                                        
                                                                                                                                          
[{"email":"a@b.com","firstName":"Tom","lastName":"Knocker"}]                                                                              

Configuring HTTP/2 over TLS requires just a bit more efforts to setup the certificates and key managers (we are using self-signed certificates issued to localhost, please check Creating sample HTTPS server for fun and profit if you are curious how to generate your own):

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.configuration.jsse.TLSParameterJaxBUtils;
import org.apache.cxf.configuration.jsse.TLSServerParameters;
import org.apache.cxf.configuration.security.KeyManagersType;
import org.apache.cxf.configuration.security.KeyStoreType;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.jaxrs.utils.JAXRSServerFactoryCustomizationUtils;
import org.apache.cxf.transport.http.HttpServerEngineSupport;
import org.apache.cxf.transport.http_jetty.JettyHTTPServerEngineFactory;

import com.example.rest.PeopleResource;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;

public class TlsServerStarter {
    public static void main( final String[] args ) throws Exception {
        final Bus bus = BusFactory.getDefaultBus();
        bus.setProperty(HttpServerEngineSupport.ENABLE_HTTP2, true);
        
        final KeyStoreType keystore = new KeyStoreType();
        keystore.setType("JKS");
        keystore.setPassword("strong-passw0rd-here");
        keystore.setResource("certs/server.jks");
        
        final KeyManagersType kmt = new KeyManagersType();
        kmt.setKeyStore(keystore);
        kmt.setKeyPassword("strong-passw0rd-here");
        
        final TLSServerParameters parameters = new TLSServerParameters();
        parameters.setKeyManagers(TLSParameterJaxBUtils.getKeyManagers(kmt));
        final JettyHTTPServerEngineFactory factory = new JettyHTTPServerEngineFactory(bus);
        factory.setTLSServerParametersForPort("localhost", 19091, parameters);
        
        final JAXRSServerFactoryBean bean = new JAXRSServerFactoryBean();
        bean.setResourceClasses(PeopleResource.class);
        bean.setResourceProvider(PeopleResource.class, 
            new SingletonResourceProvider(new PeopleResource()));
        bean.setAddress("https://localhost:19091/services");
        bean.setProvider(new JacksonJsonProvider());
        bean.setBus(bus);
        
        JAXRSServerFactoryCustomizationUtils.customize(bean);
        Server server = bean.create();
        server.start();
    }
}

Now if we repeat the experiment, the results are going to be quite different.

$ mvn clean package
$ java -jar target/jaxrs-standalone-jetty-http2-0.0.1-SNAPSHOT-h2.jar

[INFO] 2022-01-17 19:06:37.481 org.apache.cxf.endpoint.ServerImpl -[] Setting the server's publish address to be https://localhost:19091/services
[INFO] 2022-01-17 19:06:37.536 org.eclipse.jetty.util.log -[] Logging initialized @724ms to org.eclipse.jetty.util.log.Slf4jLog
[INFO] 2022-01-17 19:06:37.576 org.eclipse.jetty.server.Server -[] jetty-9.4.44.v20210927; built: 2021-09-27T23:02:44.612Z; git: 8da83308eeca865e495e53ef315a249d63ba9332; jvm 17+35-2724
[INFO] 2022-01-17 19:06:37.749 o.e.jetty.server.AbstractConnector -[] Started ServerConnector@163370c2{ssl, (ssl, alpn, h2, http/1.1)}{localhost:19091}
[INFO] 2022-01-17 19:06:37.749 org.eclipse.jetty.server.Server -[] Started @937ms
[WARN] 2022-01-17 19:06:37.752 o.e.j.server.handler.ContextHandler -[] Empty contextPath
[INFO] 2022-01-17 19:06:37.772 o.e.j.server.handler.ContextHandler -[] Started o.e.j.s.h.ContextHandler@403f0a22{/,null,AVAILABLE}
...

The list of the supported protocols listed by Jetty includes few newcomers: {ssl, (ssl, alpn, h2, http/1.1)}. The presence of ALPN (Application-Layer Protocol Negotiation) is very important as it allows the application layer to negotiate which protocol should be selected over a TLS connection. Without further ado, let us see that in action.

$ curl https://localhost:19091/services/people --http2 -k 
  
* Connected to localhost (127.0.0.1) port 19091 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
...
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=XX; ST=XX; L=XX; O=XX; CN=localhost
*  start date: Jan 18 00:16:42 2022 GMT
*  expire date: Nov  7 00:16:42 2024 GMT
*  issuer: C=XX; ST=XX; L=XX; O=XX; CN=localhost
*  SSL certificate verify result: self signed certificate (18), continuing anyway.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
...
> GET /services/people HTTP/2
> Host: localhost:19091
> user-agent: curl/7.71.1
> accept: */*
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200
< server: Jetty(9.4.44.v20210927)
< date: Tue, 18 Jan 2022 00:19:20 GMT
< content-type: application/json
< content-length: 60

HTTP/2 200
server: Jetty(9.4.44.v20210927)
date: Tue, 18 Jan 2022 00:19:20 GMT
content-type: application/json
content-length: 60

[{"email":"a@b.com","firstName":"Tom","lastName":"Knocker"}]

As we can see, the client and server negotiated the protocols from the start and HTTP/2 has been picked, completely bypassing the HTTP/1.1 101 Switching Protocols dance we have seen before.

Hopefully things are looking exciting already, but to be fair, it is very likely that your are already hosting JAX-RS services inside applications powered by widely popular Spring Boot framework. Wouldn't it be awesome to have HTTP/2 support right there? Absolutely, and in fact you don't need anything special from the Apache CXF besides using the provided Spring Boot starters.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter</artifactId>
	<version>2.6.2</version>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jetty</artifactId>
	<version>2.6.2</version>
</dependency>

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
	<version>3.5.0</version>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</exclusion>
	</exclusions>
</dependency>

The application configuration is minimal but still required (although in future it should be completely auto-configurable):

import org.apache.cxf.Bus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.utils.JAXRSServerFactoryCustomizationUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.rest.PeopleResource;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;

@Configuration
public class AppConfig {
    @Bean
    public Server server(Bus bus, PeopleResource service) {
        JAXRSServerFactoryBean bean = new JAXRSServerFactoryBean();
        bean.setBus(bus);
        bean.setServiceBean(service);
        bean.setProvider(new JacksonJsonProvider());
        bean.setAddress("/");
        JAXRSServerFactoryCustomizationUtils.customize(bean);
        return bean.create();
    }
}

Everything else, including TLS configuration, is done through configuration properties, which are usually provided inside application.yml (or externalized altogether):

server:
  port: 19091
  http2:
    enabled: true
---
spring:
  config:
    activate:
      on-profile: h2
server:
  ssl:
    key-store: "classpath:certs/server.jks"
    key-store-password: "strong-passw0rd-here"
    key-password: "strong-passw0rd-here"

The HTTP/2 protocol is enabled by setting server.http2.enabled configuration property to true, the Apache CXF is not involved in any way, it is solely offered by Spring Boot. The TLS/SSL is activated by Spring profile h2, otherwise it runs HTTP/2 over clear text.

$ java -jar  target/jaxrs-spring-boot-jetty-http2-0.0.1-SNAPSHOT.jar
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.2)

[INFO] 2022-01-19 20:08:55.645 o.h.validator.internal.util.Version -[] HV000001: Hibernate Validator 6.2.0.Final
[INFO] 2022-01-19 20:08:55.646 com.example.ServerStarter -[] No active profile set, falling back to default profiles: default
[INFO] 2022-01-19 20:08:56.777 org.eclipse.jetty.util.log -[] Logging initialized @2319ms to org.eclipse.jetty.util.log.Slf4jLog
[INFO] 2022-01-19 20:08:57.008 o.s.b.w.e.j.JettyServletWebServerFactory -[] Server initialized with port: 19091
[INFO] 2022-01-19 20:08:57.011 org.eclipse.jetty.server.Server -[] jetty-9.4.44.v20210927; built: 2021-09-27T23:02:44.612Z; git: 8da83308eeca865e495e53ef315a249d63ba9332; jvm 17+35-2724
[INFO] 2022-01-19 20:08:57.052 o.e.j.s.h.ContextHandler.application -[] Initializing Spring embedded WebApplicationContext
[INFO] 2022-01-19 20:08:57.052 o.s.b.w.s.c.ServletWebServerApplicationContext -[] Root WebApplicationContext: initialization completed in 1352 ms
[INFO] 2022-01-19 20:08:57.237 org.eclipse.jetty.server.session -[] DefaultSessionIdManager workerName=node0
[INFO] 2022-01-19 20:08:57.238 org.eclipse.jetty.server.session -[] No SessionScavenger set, using defaults
[INFO] 2022-01-19 20:08:57.238 org.eclipse.jetty.server.session -[] node0 Scavenging every 660000ms
[INFO] 2022-01-19 20:08:57.245 org.eclipse.jetty.server.Server -[] Started @2788ms
[INFO] 2022-01-19 20:08:57.422 org.apache.cxf.endpoint.ServerImpl -[] Setting the server's publish address to be /
[INFO] 2022-01-19 20:08:58.038 o.e.j.s.h.ContextHandler.application -[] Initializing Spring DispatcherServlet 'dispatcherServlet'
[INFO] 2022-01-19 20:08:58.038 o.s.web.servlet.DispatcherServlet -[] Initializing Servlet 'dispatcherServlet'
[INFO] 2022-01-19 20:08:58.038 o.s.web.servlet.DispatcherServlet -[] Completed initialization in 0 ms
[INFO] 2022-01-19 20:08:58.080 o.e.jetty.server.AbstractConnector -[] Started ServerConnector@ee86bcb{HTTP/1.1, (http/1.1, h2c)}{0.0.0.0:19091}
[INFO] 2022-01-19 20:08:58.081 o.s.b.w.e.jetty.JettyWebServer -[] Jetty started on port(s) 19091 (http/1.1, h2c) with context path '/'
[INFO] 2022-01-19 20:08:58.093 com.example.ServerStarter -[] Started ServerStarter in 2.939 seconds (JVM running for 3.636)
...

The already familiar list of protocols appears in the console: {HTTP/1.1, (http/1.1, h2c)}. To activate HTTP/2 over TLS we could pass --spring.profiles.active=h2 command line argument, for example:

$ java -jar  target/jaxrs-spring-boot-jetty-http2-0.0.1-SNAPSHOT.jar --spring.profiles.active=h2                                                                                       
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.2)

[INFO] 2022-01-19 20:13:17.999 com.example.ServerStarter -[] The following profiles are active: h2
[INFO] 2022-01-19 20:13:17.999 o.h.validator.internal.util.Version -[] HV000001: Hibernate Validator 6.2.0.Final
[INFO] 2022-01-19 20:13:19.124 org.eclipse.jetty.util.log -[] Logging initialized @2277ms to org.eclipse.jetty.util.log.Slf4jLog
[INFO] 2022-01-19 20:13:19.368 o.s.b.w.e.j.JettyServletWebServerFactory -[] Server initialized with port: 19091
[INFO] 2022-01-19 20:13:19.398 org.eclipse.jetty.server.Server -[] jetty-9.4.44.v20210927; built: 2021-09-27T23:02:44.612Z; git: 8da83308eeca865e495e53ef315a249d63ba9332; jvm 17+35-2724
[INFO] 2022-01-19 20:13:19.433 o.e.j.s.h.ContextHandler.application -[] Initializing Spring embedded WebApplicationContext
[INFO] 2022-01-19 20:13:19.433 o.s.b.w.s.c.ServletWebServerApplicationContext -[] Root WebApplicationContext: initialization completed in 1380 ms
[INFO] 2022-01-19 20:13:19.618 org.eclipse.jetty.server.session -[] DefaultSessionIdManager workerName=node0
[INFO] 2022-01-19 20:13:19.618 org.eclipse.jetty.server.session -[] No SessionScavenger set, using defaults
[INFO] 2022-01-19 20:13:19.619 org.eclipse.jetty.server.session -[] node0 Scavenging every 660000ms                                                         [INFO] 2022-01-19 20:13:19.626 org.eclipse.jetty.server.Server -[] Started @2779ms
[INFO] 2022-01-19 20:13:19.823 org.apache.cxf.endpoint.ServerImpl -[] Setting the server's publish address to be /
[INFO] 2022-01-19 20:13:20.394 o.e.j.s.h.ContextHandler.application -[] Initializing Spring DispatcherServlet 'dispatcherServlet'
[INFO] 2022-01-19 20:13:20.394 o.s.web.servlet.DispatcherServlet -[] Initializing Servlet 'dispatcherServlet'
[INFO] 2022-01-19 20:13:20.395 o.s.web.servlet.DispatcherServlet -[] Completed initialization in 1 ms
[INFO] 2022-01-19 20:13:20.775 o.e.jetty.server.AbstractConnector -[] Started SslValidatingServerConnector@7e3181aa{SSL, (ssl, alpn, h2, http/1.1)}{0.0.0.0:19091}
[INFO] 2022-01-19 20:13:20.776 o.s.b.w.e.jetty.JettyWebServer -[] Jetty started on port(s) 19091 (ssl, alpn, h2, http/1.1) with context path '/'
[INFO] 2022-01-19 20:13:20.786 com.example.ServerStarter -[] Started ServerStarter in 3.285 seconds (JVM running for 3.939)
...

And we see {SSL, (ssl, alpn, h2, http/1.1)} this time around. If you would like to repeat the experiment with the curl commands we executed before, feel free to do so, the observed results should be the same. It is worth mentioning that along with Jetty, Spring Boot bakes first-class support for Apache Tomcat, Netty (Reactor Netty to be precise) and Undertow.

Huh, quite likely you are now convinced that HTTP/2 is pretty well supported these days and it is here for you to take advantage of. We have seen Spring Boot and Apache CXF in action, but Quarkus, Micronaut, Helidon (and many others) are on a par with HTTP/2 support, enjoy!

The complete project sources are available on Github.

Wednesday, September 30, 2020

For gourmets and practioners: pick your flavour of the reactive stack with JAX-RS and Apache CXF

When JAX-RS 2.1 specification was released back in 2017, one of its true novelties was the introduction of the reactive API extensions. The industry has acknowledged the importance of the modern programming paradigms and specification essentially mandated the first-class support of the asynchronous and reactive programming for the Client API.

But what about the server side? It was not left out, the JAX-RS 2.1 asynchronous processing model has been enriched with Java 8's CompletionStage support, certainly a step in a right direction. Any existing REST web APIs built on top of the JAX-RS 2.1 implementation (like Apache CXF for example) could benefit from such enhancements right away.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

@Service
@Path("/people")
public class PeopleRestService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public CompletionStage<List<Person>> getPeople() {
        return CompletableFuture
        	.supplyAsync(() -> Arrays.asList(new Person("a@b.com", "Tom", "Knocker")));
    }
}

Udoubtedly, CompletionStage and CompletableFuture are powerful tools but not without own quirks and limitations. The Reactive Streams specification and a number of its implementations offer a considerably better glimpse on how asynchronous and reactive programming should look like on JVM. With that, the logical question pops up: could your JAX-RS web services and APIs take advantage of the modern reactive libraries? And if the answer is positive, what does it take?

If your bets are on Apache CXF, you are certainly well positioned. The latest Apache CXF 3.2.14 / 3.3.7 / 3.4.0 release trains bring a comprehesive support of RxJava3, RxJava2 and Project Reactor. Along this post we are going to see how easy it is to plug your favorite reactive library in and place it at the forefront of your REST web APIs and services.

Since the most applications and services on the JVM are built on top of excellent Spring framework and Spring Boot, we will be developing the reference implementations using those as a foundation. The Spring Boot starter which comes along with Apache CXF distribution is taking care of most of the boring wirings you would have needed to do otherwise.

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
	<version>3.4.0</version>
</dependency>

The Project Reactor is the number one choice as the reactive foundation for Spring-based applications and services, so let us just start from that.

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-rs-extension-reactor</artifactId>
	<version>3.4.0</version>
</dependency>

Great, believe it or not, we are mostly done here. In order to teach Apache CXF to understand Project Reactor types like Mono or/and Flux we need to tune the configuration just a bit using ReactorCustomizer instance.

import org.apache.cxf.Bus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.reactor.server.ReactorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;

@Configuration
public class AppConfig {
    @Bean
    public Server rsServer(Bus bus, PeopleRestService service) {
        final JAXRSServerFactoryBean bean = new JAXRSServerFactoryBean();
        bean.getProperties(true).put("useStreamingSubscriber", true);
        bean.setBus(bus);
        bean.setAddress("/");
        bean.setServiceBean(service);
        bean.setProvider(new JacksonJsonProvider());
        new ReactorCustomizer().customize(bean);
        return bean.create();
    }
}

With such customization in-place, our JAX-RS web services and APIs could freely utilize Project Reactor primitives in a streaming fashion, for example.

import reactor.core.publisher.Flux;

@Service
@Path("/people")
public class PeopleRestService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Flux<Person> getPeople() {
        return Flux.just(new Person("a@b.com", "Tom", "Knocker"));
    }
}

As you probably noticed, the implementation purposely does not do anything complicated. However, once the reactive types are put at work, you could unleash the full power of the library of your choice (and Project Reactor is really good at that).

Now, when you undestand the principle, it comes the turn of the RxJava3, the last generation of the pioneering reactive library for the JVM platform.

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-rs-extension-rx3</artifactId>
	<version>3.4.0</version>
</dependency>

The configuration tuning is mostly identical to the one we have seen with Project Reactor, the customizer instance, ReactiveIOCustomizer, is all that changes.

import org.apache.cxf.Bus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.rx3.server.ReactiveIOCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;

@Configuration
public class AppConfig {
    @Bean
    public Server rsServer(Bus bus, PeopleRestService service) {
        final JAXRSServerFactoryBean bean = new JAXRSServerFactoryBean();
        bean.getProperties(true).put("useStreamingSubscriber", true);
        bean.setBus(bus);
        bean.setAddress("/");
        bean.setServiceBean(service);
        bean.setProvider(new JacksonJsonProvider());
        new ReactiveIOCustomizer().customize(bean);
        return bean.create();
    }
}

The list of supported types includes Flowable, Single and Observable, the equivalent implementation in terms of RxJava3 primitives may look like this.


import io.reactivex.rxjava3.core.Flowable;

@Service
@Path("/people")
public class PeopleRestService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Flowable<Person> getPeople() {
        return Flowable.just(new Person("a@b.com", "Tom", "Knocker"));
    }
}

Pretty simple, isn't it? If you stuck with an older generation, RxJava2, nothing to worry about, Apache CXF has you covered.

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-rt-rs-extension-rx2</artifactId>
	<version>3.4.0</version>
</dependency>

The same configuration trick with applying the customizer (which may look annoying at this point to be fair) is all that is required.

import org.apache.cxf.Bus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.rx2.server.ReactiveIOCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;

@Configuration
public class AppConfig {
    @Bean
    public Server rsServer(Bus bus, PeopleRestService service) {
        final JAXRSServerFactoryBean bean = new JAXRSServerFactoryBean();
        bean.getProperties(true).put("useStreamingSubscriber", true);
        bean.setBus(bus);
        bean.setAddress("/");
        bean.setServiceBean(service);
        bean.setProvider(new JacksonJsonProvider());
        new ReactiveIOCustomizer().customize(bean);
        return bean.create();
    }
}

And we are good to go, ready to use the familiar reactive types Observable, Flowable and Single.

import io.reactivex.Flowable;
import io.reactivex.Observable;

@Service
@Path("/people")
public class PeopleRestService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Observable<Person> getPeople() {
        return Flowable
            .just(new Person("a@b.com", "Tom", "Knocker"))
            .toObservable();
    }
}

Last but not least, if you happens to be using the first generation of RxJava, it is also available with Apache CXF but certainly not recommended for production (as it has EOLed a couple of years ago).

Reactive programming paradigm is steadily getting more and more traction. It is great to see that the ecosystem embraces that and frameworks like Apache CXF are not an exception. If you are looking for robust foundation to build reactive and/or asynchronous REST web APIs on JVM, Apache CXF is worth considering, please give it a try!

The complete source code is available on Github.

Saturday, May 11, 2019

When HTTP status code is not enough: tackling web APIs error reporting

One area of the RESTful web APIs design, quite frequently overlooked, is how to report errors and problems, either related to business or application. The proper usage of the HTTP status codes comes to mind first, and although quite handy, often it is not informative enough. Let us take 400 Bad Request for example. Yes, it clearly states that the request is problematic, but what exactly is wrong?

The RESTful architectural style does not dictate what should be done in this case and so everyone is inventing its own styles, conventions and specifications. It could be as simple as including error message into the response or as shortsighted as copy/pasting long stack traces (in case of Java or .NET, to name a few cultprits). There is no shortage of ideas but luckily, we have at least some guidance available in the form of RFC 7807: Problem Details for HTTP APIs. Despite the fact that it is not an official specification but a draft (still), it outlines the good common principles on the problem at hand and this is what we are going to talk about in this post.

In the nutshell, RFC 7807: Problem Details for HTTP APIs just proposes the error or problem representation (in JSON or XML formats) which may include at least the following details:

  • type - A URI reference that identifies the problem type
  • title - A short, human-readable summary of the problem type
  • status - The HTTP status code
  • detail - A human-readable explanation specific to this occurrence of the problem
  • instance - A URI reference that identifies the specific occurrence of the problem
More importantly, the problem type definitions may extend the problem details object with additional members, contributing to the ones above. As you see, it looks dead simple from the implementation perspective. Even better, thanks to Zalando, we already have the RFC 7807: Problem Details for HTTP APIs implementation for Java (and Spring Web in particular). So ... let us give it a try!

Our imaginary People Management web API is going to be built using the state of the art technology stack, Spring Boot and Apache CXF, the popular web services framework and JAX-RS 2.1 implementation. To keep it somewhat simple, there are only two endpoints which are exposed: registration and lookup by person identifier.

Sweeping aside the tons of issues and business constraints you may run into while developing the real-world services, even with this simple API a few things may go wrong. The first problem we age going to tackle is what if the person you are looking for is not registered yet? Looks like a fit for 404 Not Found, right? Indeed, let us start with our first problem, PersonNotFoundProblem!

public class PersonNotFoundProblem extends AbstractThrowableProblem {
    private static final long serialVersionUID = 7662154827584418806L;
    private static final URI TYPE = URI.create("http://localhost:21020/problems/person-not-found");
    
    public PersonNotFoundProblem(final String id, final URI instance) {
        super(TYPE, "Person is not found", Status.NOT_FOUND, 
            "Person with identifier '" + id + "' is not found", instance, 
                null, Map.of("id", id));
    }
}

It resembles a lot the typical Java exception, and it really is one, since AbstractThrowableProblem is the subclass of the RuntimeException. As such, we could throw it from our JAX-RS API.

@Produces({ MediaType.APPLICATION_JSON, "application/problem+json" })
@GET
@Path("{id}")
public Person findById(@PathParam("id") String id) {
    return service
        .findById(id)
        .orElseThrow(() -> new PersonNotFoundProblem(id, uriInfo.getRequestUri()));
}

If we run the server and just try to fetch the person providing any identifier, the problem detail response is going to be returned back (since the dataset is not pre-populated), for example:

$ curl "http://localhost:21020/api/people/1" -H  "Accept: */*" 

HTTP/1.1 404
Content-Type: application/problem+json

{
    "type" : "http://localhost:21020/problems/person-not-found",
    "title" : "Person is not found",
    "status" : 404,
    "detail" : "Person with identifier '1' is not found",
    "instance" : "http://localhost:21020/api/people/1",
    "id" : "1"
}

Please notice the usage of the application/problem+json media type along with additional property id being included into the response. Although there are many things which could be improved, it is arguably better than just naked 404 (or 500 caused by EntityNotFoundException). Plus, the documentation section behind this type of the problem (in our case, http://localhost:21020/problems/person-not-found) could be consulted in case further clarifications may be needed.

So designing the problems after exceptions is just one option. You may often (and for very valid reasons) restrain from coupling you business logic with unrelated details. In this case, it is perfectly valid to return the problem details as the response payload from the JAX-RS resource. As an example, the registration process may raise NonUniqueEmailException so our web API layer could transform it into appropriate problem detail.

@Consumes(MediaType.APPLICATION_JSON)
@Produces({ MediaType.APPLICATION_JSON, "application/problem+json" })
@POST
public Response register(@Valid final CreatePerson payload) {
    try {
        final Person person = service.register(payload.getEmail(), 
            payload.getFirstName(), payload.getLastName());
            
        return Response
            .created(uriInfo.getRequestUriBuilder().path(person.getId()).build())
            .entity(person)
            .build();

    } catch (final NonUniqueEmailException ex) {
        return Response
            .status(Response.Status.BAD_REQUEST)
            .type("application/problem+json")
            .entity(Problem
                .builder()
                .withType(URI.create("http://localhost:21020/problems/non-unique-email"))
                .withInstance(uriInfo.getRequestUri())
                .withStatus(Status.BAD_REQUEST)
                .withTitle("The email address is not unique")
                .withDetail(ex.getMessage())
                .with("email", payload.getEmail())
                .build())
            .build();
        }
    }

To trigger this issue, it is enough to run the server instance and try to register the same person twice, like we have done below.

$ curl -X POST "http://localhost:21020/api/people" \ 
     -H  "Accept: */*" -H "Content-Type: application/json" \
     -d '{"email":"john@smith.com", "firstName":"John", "lastName": "Smith"}'

HTTP/1.1 400                                                                              
Content-Type: application/problem+json                                                           
                                                                                                                                                                                   
{                                                                                         
    "type" : "http://localhost:21020/problems/non-unique-email",                            
    "title" : "The email address is not unique",                                            
    "status" : 400,                                                                         
    "detail" : "The email 'john@smith.com' is not unique and is already registered",        
    "instance" : "http://localhost:21020/api/people",                                       
    "email" : "john@smith.com"                                                              
}                                                                                         

Great, so our last example is a bit more complicated but, probably, at the same time, the most realistic one. Our web API heavily relies on Bean Validation in order to make sure the input provided by the consumers of the API is valid. How would we represent the validation errors as the problem details? The most straightforward way is to supply the dedicated ExceptionMapper provider, which is the part of the JAX-RS specification. Let us introduce one.

@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
    @Context private UriInfo uriInfo;
    
    @Override
    public Response toResponse(final ValidationException ex) {
        if (ex instanceof ConstraintViolationException) {
            final ConstraintViolationException constraint = (ConstraintViolationException) ex;
            
            final ThrowableProblem problem = Problem
                    .builder()
                    .withType(URI.create("http://localhost:21020/problems/invalid-parameters"))
                    .withTitle("One or more request parameters are not valid")
                    .withStatus(Status.BAD_REQUEST)
                    .withInstance(uriInfo.getRequestUri())
                    .with("invalid-parameters", constraint
                        .getConstraintViolations()
                        .stream()
                        .map(this::buildViolation)
                        .collect(Collectors.toList()))
                    .build();

            return Response
                .status(Response.Status.BAD_REQUEST)
                .type("application/problem+json")
                .entity(problem)
                .build();
        }
        
        return Response
            .status(Response.Status.INTERNAL_SERVER_ERROR)
            .type("application/problem+json")
            .entity(Problem
                .builder()
                .withTitle("The server is not able to process the request")
                .withType(URI.create("http://localhost:21020/problems/server-error"))
                .withInstance(uriInfo.getRequestUri())
                .withStatus(Status.INTERNAL_SERVER_ERROR)
                .withDetail(ex.getMessage())
                .build())
            .build();
    }

    protected Map<?, ?> buildViolation(ConstraintViolation<?> violation) {
        return Map.of(
                "bean", violation.getRootBeanClass().getName(),
                "property", violation.getPropertyPath().toString(),
                "reason", violation.getMessage(),
                "value", Objects.requireNonNullElse(violation.getInvalidValue(), "null")
            );
    }
}

The snippet above distingushes two kind of issues: the ConstraintViolationExceptions indicate the invalid input and are mapped to 400 Bad Request, whereas generic ValidationExceptions indicate the problem on the server side and are mapped to 500 Internal Server Error. We only extract the basic details about violations, however even that improves the error reporting a lot.

$ curl -X POST "http://localhost:21020/api/people" \
    -H  "Accept: */*" -H "Content-Type: application/json" \
    -d '{"email":"john.smith", "firstName":"John"}' -i    

HTTP/1.1 400                                                                    
Content-Type: application/problem+json                                              
                                                                                
{                                                                               
    "type" : "http://localhost:21020/problems/invalid-parameters",                
    "title" : "One or more request parameters are not valid",                     
    "status" : 400,                                                               
    "instance" : "http://localhost:21020/api/people",                             
    "invalid-parameters" : [ 
        {
            "reason" : "must not be blank",                                             
            "value" : "null",                                                           
            "bean" : "com.example.problem.resource.PeopleResource",                     
            "property" : "register.payload.lastName"                                    
        }, 
        {                                                                          
            "reason" : "must be a well-formed email address",                           
            "value" : "john.smith",                                                     
            "bean" : "com.example.problem.resource.PeopleResource",                     
            "property" : "register.payload.email"                                       
        } 
    ]                                                                           
}                                                                               

This time the additional information bundled into the invalid-parameters member is quite verbose: we know the class (PeopleResource), method (register), the method's argument (payload) and the properties (lastName and email) respectively (all that extracted from the property path).

Meaningful error reporting is one of corner stones of the modern RESTful web APIs. Often it is not easy but definitely worth the efforts. The consumers (which often are just other developers) should have a clear understanding of what went wrong and what to do about it. The RFC 7807: Problem Details for HTTP APIs is a step into right direction and libraries like problem and problem-spring-web are here to back you up, please make use of them.

The complete source code is available on Github.

Monday, April 30, 2018

Moving With The Times: Towards OpenAPI v3.0.0 adoption in JAX-RS APIs

It is terrifying to see how fast time passes! The OpenAPI specification 3.0.0, a major revamp of so-get-used-to Swagger specification, has been released mostly one year ago but it took awhile for tooling to catch up. However, with the recent official release of the Swagger Core 2.0.0 things are going to accelerate for sure.

To prove the point, Apache CXF, the well-known JAX-RS 2.1 implementation, is one of the first adopters of the OpenAPI 3.0.0 and in today's post we are going to take a look how easy your JAX-RS 2.1 APIs could benefit from it.

As always, to keep things simple we are going to design a people management web APIs with just a handful set of resources to support it, nothing too exciting here.

POST   /api/people
GET    /api/people/{email}
GET    /api/people
DELETE /api/people/{email}

Our model would consist of a single Person class.

public class Person {
    private String email;
    private String firstName;
    private String lastName;
}

To add a bit of magic, we would be using Spring Boot to get us up and running as fast as possible. With that, let us start to fill in the dependencies (assuming we are using Apache Maven for build management).

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
    <version>3.2.4</version>
</dependency>

In the recent 3.2.x releases Apache CXF introduces a new module cxf-rt-rs-service-description-openapi-v3 dedicated to OpenAPI 3.0.0, based on Swagger Core 2.0.0.

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-service-description-openapi-v3</artifactId>
    <version>3.2.4</version>
</dependency>

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>swagger-ui</artifactId>
    <version>3.13.6</version>
</dependency>

The presence of the Swagger UI is not strictly necessary, but this is exceptionally useful and beautiful tool to explore your APIs (and if it is available on classpath, Apache CXF seamlessly integrates it into your applications as we are going to see in a bit).

The prerequisites are in place, let us do some coding! Before we begin, it is worth to note that the Swagger Core 2.0.0 has many ways to populate the OpenAPI 3.0.0 definitions for your services, including property files, annotations or programmatically. Along this post we are going to use annotations only.

@OpenAPIDefinition(
    info = @Info(
        title = "People Management API",
        version = "0.0.1-SNAPSHOT",
        license = @License(
            name = "Apache 2.0 License",
            url = "http://www.apache.org/licenses/LICENSE-2.0.html"
        )
    )
)
@ApplicationPath("api")
public class JaxRsApiApplication extends Application {
}

Looks pretty simple, the @OpenAPIDefinition sets the top-level definition for all our web APIs. Moving on to the PeopleRestService, we just add the @Tag annotation to, well, tag our API.

@Path( "/people" ) 
@Tag(name = "people")
public class PeopleRestService {
    // ...
}

Awesome, nothing complicated so far. The meaty part starts with web API operation definitions, so let us take a look on the first example, the operation to fetch everyone.

@Produces(MediaType.APPLICATION_JSON)
@GET
@Operation(
    description = "List all people", 
    responses = {
        @ApiResponse(
            content = @Content(
                array = @ArraySchema(schema = @Schema(implementation = Person.class))
            ),
            responseCode = "200"
        )
    }
)
public Collection<Person> getPeople() {
    // ...
}

Quite a few annotations but by and large, looks pretty clean and straightforward. Let us take a look on another one, the endpoint to find the person by its e-mail address.

@Produces(MediaType.APPLICATION_JSON)
@Path("/{email}")
@GET
@Operation(
    description = "Find person by e-mail", 
    responses = {
        @ApiResponse(
            content = @Content(schema = @Schema(implementation = Person.class)), 
            responseCode = "200"
        ),
        @ApiResponse(
            responseCode = "404", 
            description = "Person with such e-mail doesn't exists"
        )
    }
)
public Person findPerson(
        @Parameter(description = "E-Mail address to lookup for", required = true) 
        @PathParam("email") final String email) {
    // ...
}

In the same vein, the operation to remove the person by e-mail looks mostly identical.

@Path("/{email}")
@DELETE
@Operation(
    description = "Delete existing person",
    responses = {
        @ApiResponse(
            responseCode = "204",
            description = "Person has been deleted"
        ),
        @ApiResponse(
            responseCode = "404", 
            description = "Person with such e-mail doesn't exists"
        )
     }
)
public Response deletePerson(
        @Parameter(description = "E-Mail address to lookup for", required = true ) 
        @PathParam("email") final String email) {
    // ...
}

Great, let us wrap up by looking into last, arguably the most interesting endpoint which adds a new person (the choice to use the @FormParam is purely to illustrate the different flavor of the APIs).

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
@POST
@Operation(
    description = "Create new person",
    responses = {
        @ApiResponse(
            content = @Content(
                schema = @Schema(implementation = Person.class), 
                mediaType = MediaType.APPLICATION_JSON
            ),
            headers = @Header(name = "Location"),
            responseCode = "201"
        ),
        @ApiResponse(
            responseCode = "409", 
            description = "Person with such e-mail already exists"
        )
    }
)
public Response addPerson(@Context final UriInfo uriInfo,
        @Parameter(description = "E-Mail", required = true) 
        @FormParam("email") final String email, 
        @Parameter(description = "First Name", required = true) 
        @FormParam("firstName") final String firstName, 
        @Parameter(description = "Last Name", required = true) 
        @FormParam("lastName") final String lastName) {
    // ...
}

If you have an experience with documenting web APIs using older Swagger specifications, you might find the approach quite familiar but more verbose (or better to say, formalized). It is the result of the tremendous work which specification leads and community has done to make it as complete and extensible as possible.

The APIs are defined and documented, it is time to try them out! The missing piece though is the Spring configuration where we would initialize and expose our JAX-RS web services.

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackageClasses = PeopleRestService.class)
public class AppConfig {
    @Autowired private PeopleRestService peopleRestService;
 
    @Bean(destroyMethod = "destroy")
    public Server jaxRsServer(Bus bus) {
        final JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();

        factory.setApplication(new JaxRsApiApplication());
        factory.setServiceBean(peopleRestService);
        factory.setProvider(new JacksonJsonProvider());
        factory.setFeatures(Arrays.asList(new OpenApiFeature()));
        factory.setBus(bus);
        factory.setAddress("/");

        return factory.create();
    }

    @Bean
    public ServletRegistrationBean cxfServlet() {
        final ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new CXFServlet(), "/api/*");
        servletRegistrationBean.setLoadOnStartup(1);
        return servletRegistrationBean;
    }
}

The OpenApiFeature is a key ingredient here which takes care of all the integration and introspection. The Spring Boot application is the last touch to finish the picture.

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

Let us build and run it right away:

mvn clean package 
java -jar target/jax-rs-2.1-openapi-0.0.1-SNAPSHOT.jar

Having the application started, the OpenAPI 3.0.0 specification of our web APIs should be live and available for consumption in the JSON format at:

http://localhost:8080/api/openapi.json

Or in YAML format at:

http://localhost:8080/api/openapi.json

Wouldn't it be great to explore our web APIs and play with it? Since we included Swagger UI dependency, this is no brainer, just navigate to http://localhost:8080/api/api-docs?url=/api/openapi.json:

A special attention to be made to the small icon Swagger UI places alongside your API version, hinting about its conformity to OpenAPI 3.0.0 specification.

Just to note here, there is nothing special in using Spring Boot. In case you are using Apache CXF inside the OSGi container (like Apache Karaf for example), the integration with OpenAPI 3.0.0 is also available (please check out the official documentation and samples if you are interested in the subject).

It all looks easy and simple, but what about migrating to OpenAPI 3.0.0 from older versions of the Swagger specifications? The Apache CXF has an powerful feature to convert older the specifications on the fly but in general the OpenApi.Tools portal is a right place to evaluate your options.

Should you migrate to OpenAPI 3.0.0? I honestly believe you should, at least should try experimenting with it, but please be aware that the tooling is still not mature enough, expect a few roadblocks along the way (which you would be able to overcome by contributing the patches, by the way). But undoubtedly, the future is bright!

The complete project sources are available on Github.