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.

No comments: