The REST architectural pattern is widely adopted when it comes to creating web services. The term was first introduced by Roy Fielding in his dissertation and describes a way for clients to query and manipulate the resources of a server. With Jakarta RESTful Web Services (JAX-RS), formerly known as Java API for RESTful Web Services, we have a standardized approach to create such web services. This specification is also part of the MicroProfile project since day one.
Learn more about the Jakarta RESTful Web Services (JAX-RS) specification, its annotations and how to use it in this blog post. Please note that I won't cover every aspect of this spec (as it is quite large) and rather concentrate on the most important parts.
Specification profile: Jakarta RESTful Web Services (JAX-RS)
- Current version: 2.1
- GitHub repository
- Specification homepage
- Basic use case: develop web services following the Representational State Transfer (REST) pattern
Bootstrap a JAX-RS application
Bootstrapping a JAX-RS application is simple. The main mechanism is to provide a subclass of javax.ws.rs.core.Application
on your classpath:
1 2 3 | @ApplicationPath("resources") public class JAXRSApplication extends Application { } |
With @ApplicationPath
you can specify the path prefix all of your REST endpoints should share. This might be /api
or /resources
. Furthermore, you can override the methods of Application
and register for example all your resources classes, providers and features manually (getClasses()
method), but you don't have to.
Create REST endpoints
Most of the time you'll use JAX-RS to expose resources of your server on a given path and for a specific HTTP method. The specification provides an annotation to map each HTTP method (GET, PUT, POST, DELETE …) to a Java method. Using the @Path
annotation you can specify which path to map and also specify path variables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @Path("books") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class BookResource { @GET @Path("/{id}") public Response getBookById(@PathParam("id") Long id, @QueryParam("title") @DefaultValue("") String title) { // ... } @POST public Response getBookById(Book bookToStore, @Context UriInfo uriInfo) { // ... } @DELETE @Path("/{id}") public Response deleteBook(@PathParam("id") Long id, @HeaderParam("User-Agent") String userAgent) { // ... } } |
In the example above you see that the whole class is mapped to the path /books
with different HTTP methods. @PathParam
is used to get the value of a path variable and @QueryParam
for retrieving query parameters of a URL (e.g. ?order=DESC
). In addition, you can inject further classes into your JAX-RS method and get access e.g. to the HttpServlet
, UriInfo
and HTTP headers of the request (@HeaderParam("nameOfHeader")
).
Next, JAX-RS offers annotations for content-negotiation: @Consumes
and @Produces
. In the example above, I'm adding these annotations on class-level, so all methods (which don't specify their own @Produces
/@Consumes
) inherit the rules to accept only JSON requests and produces only JSON responses.
In the case, your client sends a payload in the HTTP body (e.g. creating a new book – @POST
in the example above), you can map the payload to a Java POJO. For JSON payloads, JSON-B is used in the background and for not default payload types (e.g. binary protobuf payload to POJO) you have to register your own MessageBodyReader
and MessageBodyWriter
.
The specification defines a standard set of entity providers, which are supported out-of-the-box (e.g. String
for text/plain
, byte[]
for */*
, File
for */*
, MultivaluedMap<String, String>
for application/x-www-form-urlencoded
, etc.).
Alongside synchronous and blocking REST endpoints, the specification also supports asynchronous ones:
1 2 3 4 5 6 7 | @GET @Path("async") public void getBooksAsync(@Suspended final AsyncResponse asyncResponse) { // do long-running task with e.g. @Asynchronous annotation // form MicroProfile Fault Tolerance or from EJB asyncResponse.resume(this.bookStore); } |
If you don't specify any other lifecycle (e.g. with @Singleton
from EJB or a CDI scope) the JAX-RS runtime instantiates a new instance for each request for this resource.
Access external resources
The JAX-RS specification also provides a convenient way to access external resources (e.g. REST endpoints of other services) as a client. We can construct such a client with the ClientBuilder
from JAX-RS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @PostConstruct public void initClient() { ClientBuilder clientBuilder = ClientBuilder.newBuilder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .register(UserAgentClientFilter.class) .register(ClientLoggingResponseFilter.class); this.client = clientBuilder.build(); } @PreDestroy public void tearDown() { this.client.close(); } |
This ClientBuilder
allows you to specify metadata like the connect and read timeouts, but also register several features (like you'll see in the next chapter). Make sure to not construct a new Client
for every request, as they are heavy-weight objects:
Clients are heavy-weight objects that manage the client-side communication infrastructure. Initialization as well as disposal of a {@code Client} instance may be a rather expensive operation. It is therefore advised to construct only a small number of {@code Client} instances in the application. Client instances must be {@link #close() properly closed} before being disposed to avoid leaking resources.
Javadoc of the Client class
Once you have an instance of a Client
, you can now specify the external resources and create a WebTarget
instance for each target you want to access:
1 | WebTarget quotesApiTarget = client.target("https://quotes.rest").path("qod"); |
With this WebTarget
instance you can now perform any HTTP operation, set additional header/cookies, set the request body and specify the response type:
1 2 3 4 5 6 | JsonObject quoteApiResult = this.quotesApiTarget .request() .header("X-Foo", "bar") .accept(MediaType.APPLICATION_JSON) .get() .readEntity(JsonObject.class); |
Furthermore, JAX-RS offer reactive support for requesting external resources with .rx()
:
1 2 3 4 5 6 | CompletionStage<JsonObject> rxQuoteApiResult = this.quotesApiTarget .request() .header("X-Foo", "bar") .accept(MediaType.APPLICATION_JSON) .rx() .get(JsonObject.class); |
Intercept the request and response flow
There are various entry points to intercept the flow of a JAX-RS resource and including client requests. To give you an idea of how the overall architecture looks like, have a look at the following image:
As you see in the image above, there are several ways to apply cross-cutting logic to your JAX-RS resource method or client. I'll not cover all of the filters/readers/interceptors in this blog post, as you'll find a perfect documentation in the Jersey user guide. I'll just have a look at the most common ones.
To register your implementations, you either do it manually with your JAX-RS configuration class (see the first chapter), the .register()
method of the ClientBuilder
, or use @Provider
to register it globally.
First, you can apply a filter that executes before JAX-RS maps an incoming request to your resource method. For this, you need the @PreMatching
annotation and can do evil things like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Provider @PreMatching public class HttpMethodModificationFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { if(requestContext.getMethod().equalsIgnoreCase("DELETE")) { requestContext.setMethod("GET"); } } } |
Next, you can add e.g. common headers to the response of your resource method with a ContainerResponseFilter
:
1 2 3 4 5 6 7 8 9 10 | @Priority(100) @Provider public class XPoweredByResponseHeaderFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { responseContext.getHeaders().add("X-Powered-By", "MicroProfile"); } } |
With @Priority
you can set the order of your filter once you use multiple and rely on execution in order.
For the client-side, we can add a filter to first log all HTTP headers of the incoming response with @ClientResponseFilter
:
1 2 3 4 5 6 7 8 9 10 | @Provider public class ClientLoggingResponseFilter implements ClientResponseFilter { @Override public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { System.out.println("Response filter for JAX-RS Client"); responseContext.getHeaders().forEach((k, v) -> System.out.println(k + ":" + v)); } } |
YouTube video for using JAX-RS 2.1
Watch the following YouTube video of my Getting started with Eclipse MicroProfile series to see JAX-RS 2.1 in action:
You can find the source code with further instructions to run this example on GitHub.
Have fun using JAX-RS,
Phil