Thursday, October 27, 2022

JDK 19: revolution is looming!

The JDK-19 landed last month and may look like yet another routine release except it is not: Project Loom has finally delivered first bits! Alhough still in half-incubation / half-preview, it is a really big deal to the future of OpenJDK platform. Interestingly enough, a vast majority of the JDK-19 JEPs are either preview or incubating features. So, what JDK-19 has to offer?

From the security perspective, a few notable changes to mention (but feel free to go over much more detailed overview in the JDK 19 Security Enhancements post by Sean Mullan):

If you are one of the early adopters, please note that JDK-19 introduced a native memory usage regression very late in the release process so that the fix could not be integrated any more (JDK-8292654). The regression has already been addressed in JDK 19.0.1 and later.

With that, hope you are as excited as I am about JDK-19 release, the peaceful revolution is looming! And the JDK-20 early access builds are already there, looking very promising!

I 🇺🇦 stand 🇺🇦 with 🇺🇦 Ukraine.

Tuesday, August 30, 2022

Quick, somewhat naïve but still useful: benchmarking HTTP services from the command line

Quite often, while developing HTTP services, you may find yourself looking for a quick and easy way to throw some load at them. The tools like Gatling and JMeter are the golden standard in the open-source world, but developing meaningful load test suites in both of them may take some. It is very likely you are going to end up with one of these but during the development, using more approachable tooling gives the invaluable feedback much faster.

The command line HTTP load testing tools is what we are going to talk about. There are a lot of them, but we will focus on just a few: ab, vegeta, wrk, wrk2, and rewrk. And since HTTP/2 is getting more and more traction, the tools that support both will be highlighted and awarded with bonus points. The sample HTTP service we are going to run tests against is exposing only single GET /services/catalog endpoint over HTTP/1.1, HTTP/2 and HTTP/2 over clear text.

Let us kick off with ab, the Apache HTTP server benchmarking tool: one of the oldest HTTP load testing tools out there. It is available on most Linux distributions and supports only HTTP/1.x (in fact, it does not implement HTTP/1.x fully). The standard set of parameters like desired number of requests, concurrency and timeout is supported.

$> ab -n 1000 -c 10 -s 1 -k  http://localhost:19091/services/catalog

This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
...
Completed 1000 requests
Finished 1000 requests

Server Software:
Server Hostname:        localhost
Server Port:            19091

Document Path:          /services/catalog
Document Length:        41 bytes

Concurrency Level:      10
Time taken for tests:   51.031 seconds
Complete requests:      1000
Failed requests:        0
Keep-Alive requests:    0
Total transferred:      146000 bytes
HTML transferred:       41000 bytes
Requests per second:    19.60 [#/sec] (mean)
Time per request:       510.310 [ms] (mean)
Time per request:       51.031 [ms] (mean, across all concurrent requests)
Transfer rate:          2.79 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       5
Processing:     3  498 289.3    497    1004
Waiting:        2  498 289.2    496    1003
Total:          3  499 289.3    497    1004

Percentage of the requests served within a certain time (ms)
  50%    497
  66%    645
  75%    744
  80%    803
  90%    914
  95%    955
  98%    979
  99%    994
 100%   1004 (longest request)

The report is pretty comprehensive but if your service is talking over HTTP/2 or is using HTTPS with self-signed certificates (not unusual in development), you are out of luck.

Let us move on to a bit more advanced tools, or to be precise - a family of tools, inspired by wrk: a modern HTTP benchmarking tool. There are no official binary releases of wrk available so you are better off building the bits from the sources yourself.

$> wrk -c 50 -d 10 -t 5 --latency --timeout 5s https://localhost:19093/services/catalog

Running 10s test @ https://localhost:19093/services/catalog
  5 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   490.83ms  295.99ms   1.04s    57.06%
    Req/Sec    20.79     11.78    60.00     81.29%
  Latency Distribution
     50%  474.64ms
     75%  747.03ms
     90%  903.44ms
     99%  999.52ms
  978 requests in 10.02s, 165.23KB read
Requests/sec:     97.62
Transfer/sec:     16.49KB

Capability-wise, it is very close to ab with slightly better HTTPS support. The report is as minimal as it could get, however the distinguishing feature of the wrk is the ability to use LuaJIT scripting to perform HTTP request generation. No HTTP/2 support though.

wrk2 is an improved version of wrk (and is based mostly on its codebase) that was modified to produce a constant throughput load and accurate latency details. Unsurprisingly, you have to build this one from the sources as well (and, the binary name is kept as wrk).

$> wrk -c 50 -d 10 -t 5 -L -R 100 --timeout 5s https://localhost:19093/services/catalog

Running 10s test @ https://localhost:19093/services/catalog
  5 threads and 50 connections
  Thread calibration: mean lat.: 821.804ms, rate sampling interval: 2693ms
  Thread calibration: mean lat.: 1077.276ms, rate sampling interval: 3698ms
  Thread calibration: mean lat.: 993.376ms, rate sampling interval: 3282ms
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   933.61ms  565.76ms   3.35s    70.42%
    Req/Sec       -nan      -nan   0.00      0.00%
  Latency Distribution (HdrHistogram - Recorded Latency)
 50.000%  865.79ms
 75.000%    1.22s
 90.000%    1.69s
 99.000%    2.61s
 99.900%    3.35s
 99.990%    3.35s
 99.999%    3.35s
100.000%    3.35s

  Detailed Percentile spectrum:
       Value   Percentile   TotalCount 1/(1-Percentile)

      28.143     0.000000            1         1.00
     278.015     0.100000           36         1.11
     426.751     0.200000           71         1.25
                       ....
    2969.599     0.996875          354       320.00
    3348.479     0.997266          355       365.71
    3348.479     1.000000          355          inf
#[Mean    =      933.606, StdDeviation   =      565.759]
#[Max     =     3346.432, Total count    =          355]
#[Buckets =           27, SubBuckets     =         2048]
----------------------------------------------------------
  893 requests in 10.05s, 150.87KB read
Requests/sec:     88.85
Transfer/sec:     15.01KB

Besides these noticeable enhancements, the feature set of wrk2 is largely matching the wrk's one, so HTTP/2 is out of the picture. But do not give up just yet, we are not done.

The most recent addition to wrk's family is rewrk, a more modern HTTP framework benchmark utility, which could be thought of as wrk rewritten in beloved Rust with HTTP/2 support backed in.

$> rewrk -c 50 -d 10s -t 5 --http2 --pct --host https://localhost:19093/services/catalog

Beginning round 1...
Benchmarking 50 connections @ https://localhost:19093/services/catalog for 10 second(s)
  Latencies:
    Avg      Stdev    Min      Max
    494.62ms  281.78ms  5.56ms   1038.28ms
  Requests:
    Total:   972   Req/Sec:  97.26
  Transfer:
    Total: 192.20 KB Transfer Rate: 19.23 KB/Sec
+ --------------- + --------------- +
|   Percentile    |   Avg Latency   |
+ --------------- + --------------- +
|      99.9%      |    1038.28ms    |
|       99%       |    1006.26ms    |
|       95%       |    975.33ms     |
|       90%       |    942.94ms     |
|       75%       |    859.73ms     |
|       50%       |    737.45ms     |
+ --------------- + --------------- +

As you may have noticed, the report is very similar to the one produced by wrk. In my opinion, this is a tool which has a perfect balance of features, simplicity and insights, at least while you are in the middle of development.

The last one we are going to look at is vegeta, the HTTP load testing tool and library, written in Go. It supports not only HTTP/2, but HTTP/2 over clear text and has powerful reporting built-in. It heavily uses pipes for composing different steps together, for example:

$> echo "GET https://localhost:19093/services/catalog" | 
   vegeta attack -http2 -timeout 5s -workers 10 -insecure -duration 10s | 
   vegeta report

Requests      [total, rate, throughput]         500, 50.10, 45.96
Duration      [total, attack, wait]             10.88s, 9.98s, 899.836ms
Latencies     [min, mean, 50, 90, 95, 99, max]  14.244ms, 529.886ms, 537.689ms, 918.294ms, 962.448ms, 1.007s, 1.068s
Bytes In      [total, mean]                     20500, 41.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:500
Error Set:

You could also ask for histograms (with customizable reporting buckets), not to mention beautiful plots with plot command:

$> echo "GET http://localhost:19092/services/catalog" | 
   vegeta attack -h2c -timeout 5s -workers 10 -insecure -duration 10s | 
   vegeta report  -type="hist[0,200ms,400ms,600ms,800ms]"

Bucket           #    %       Histogram
[0s,     200ms]  86   17.20%  ############
[200ms,  400ms]  102  20.40%  ###############
[400ms,  600ms]  98   19.60%  ##############
[600ms,  800ms]  113  22.60%  ################
[800ms,  +Inf]   101  20.20%  ###############

Out of all above, vegeta is clearly the most powerful tool in terms of capabilities and reporting. It has all the chances to become the one-stop HTTP benchmarking harness even for production.

So we walked through a good number of tools, which one is yours? My advice would be: for HTTP/1.x, use ab; for basic HTTP/2 look at rewrk; and if none of these fit, turn to vegeta. Once your demands and sophistication of the load tests grow, consider Gatling or JMeter.

With that, Happy HTTP Load Testing!

Saturday, May 28, 2022

The lost art of learning ... (a new programming language)

I remember the time, 20 years ago, when as a junior software developer I was driven by constant, non-stoppable desire to learn new things: new features, techniques, frameworks, architectures, styles, practices, ... and surely, new programming languages. This curiosity, hunger to learn, drove me to consume as many books as possible and to hunt around the clock for the scanty online publications available at that time.

Year after year, the accumulated knowledge backed by invaluable experience led me to settle on proven and battle-tested (in production) choices, which have worked, work these days and I am sure will work in the future. For me, the foundation was set in Java (later Scala as well) and JVM platform in general. This is where my expertise lays in, but don't get me wrong, I still try to read a lot, balancing on the edge between books and enormous amount of the blog posts, articles, talks and podcast. How many programming languages I was able to master for the last, let say, 10 years? If we count the JVM ecosystem only, Scala and Groovy would make the list, but otherwise - none, zero, 0. The pace of learning on this front has slowed for me, significantly ...

Why is that? Did I get burned out? Nope (thanks God). Fed up by development? Nope, hopefully never. Opionated? Yes, certainly. Older? Definitely. Less curious? Not really. To be fair, there are so many things going on in the JVM ecosystem, so many projects are being developed by the community, it has become increasingly difficult to find the time for something else! And frankly, why bother if you could do Java for the next 20 years without any fear of it disappearing or becoming irrelevant.

JVM is amazing platform but its fundamental principles heavily impact certain design aspects of any programming language built on top of it. It is good and bad (everything is a trade-off), but with the time, you as a developer get used to it, questioning if anything else makes sense. As many others, I ended up in the bubble which has to be blown up. How? I decided to learn Rust, and go way beyond the basics.

But no worries, I do not intend to sell you Rust but rather share an alarming discovery: the lost art of learning a new programming language. It all started as usual, crunching through the blogs, people's and companies' experiences and finally getting the book, Programming Rust: Fast, Safe Systems Development (really good one but the official The Rust Programming Language is equally awesome). A few months (!) later, I was done reading, feeling ready for delivering cool, production-ready Rust applications, or so I thought ... The truth - I was far, very far from it ... and those are the things I did wrong.

  • Rust is concise (at least, to my taste), modern and powerful, but it is not a simple language. If some language aspects are not clear, do not skip over but try to understand them. If book is not doing sufficiently good job at it, look around till you get them, otherwise you risk to miss even more. I did not do that (overly confident that nothing could be harder than learning functional Scala) and at some point I was somewhat lost.

  • In the same vein, Rust has unique perspective on many things, including concurrency and memory management, and investing the time to understand what those are is a must (the Java/JVM background is not very helpful here). Essentially, you have to "unlearn" in order to grasp certain new concepts, this is not easy and confuses the brain.

  • Stay very focused and methodical, try to read at least few pages **every day**. Yes, we are all super busy, always distracted, have families and started to travel again, ... yada yada yada. Also, Programming Rust: Fast, Safe Systems Development is not a slim book: ~800 pages! Irregular, long breaks in reading were killing my progress.

  • Practice, practice and practice. If you have some pet projects in mind, this is great, if not - just come up with anything ad-hoc but try to use the things you have just read about. Learning the "theory" is great, but mastering the programming language is all about using it every day to solve problems. I screwed there epically by following "bed time story" style ...

  • Learn the coding practices and patterns by looking into the language's ecosystem, in this regard a curated list of Rust code and resources is super useful.

  • Subscribe to newsletters to stay up to date and be aware what is going on in the core development and in the community. In case of Rust, the This Week in Rust turned out to be invaluable.

  • Watch talks and presentations, luckily most of the conferences do publish all their videos online these days. I stumbled upon the official Rust YouTube channel and since then follow it closely.

Learning new programming language is rewarding experience, in particular when it happens to be Rust, a most beloved programming language for the last six years. But not only because of that, Rust is perfect fit for a system programming, something Java/JVM is inherently not suited very well (yes, it is better now with JEP 330: Launch Single-File Source-Code Programs, jbang and GraalVM native images but still far from being perfect for a task). From the other side, it is quite challenging when the programming language you are learning is not part of your day-to-day job.

As of this moment, I am not sure if Rust ever becomes my primary programming language but one thing I do know: I think every passionate software developer should strive to learn at least one new programming language every 1-2 years (we have so many good ones). It is amazing mental exercice and I used to follow this advice back in the days but lost the sight of it along the journey. I really should have not.

Happy learning and peace to everyone!

Friday, March 25, 2022

The show must go on: JDK-18 is here!

It feels like JDK-17 release just landed not so long ago, but there is a new one already out: please welcome JDK-18! Although it is not an LTS release, there are quite a few interesting features to talk about (the preview and incubator ones are not included).

The JDK-18 comes in with a large number of the security enhancements, including further work on phasing out SecurityManager and its APIs:

And while we are enjoying the perils of JDK-18, the early access builds of JDK-19 are already available. There are not much details of what is coming just yet, but may be Project Loom would finally make its appearance in some form or another? Who knows ...

Peace to everyone!

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.