Showing posts with label native-image. Show all posts
Showing posts with label native-image. Show all posts

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 [email protected]/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 [email protected]/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 [email protected]/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2059)
        at [email protected]/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1697)
        at [email protected]/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1600)
        at [email protected]/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("[email protected]", "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":"[email protected]","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":"[email protected]","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":"[email protected]","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.