Accessing REST Resources with the Jakarta REST Client API
This chapter describes the Jakarta REST Client API and includes examples of how to access REST resources using the Java programming language.
Jakarta REST provides a client API for accessing REST resources from other Java applications.
Overview of the Client API
The Jakarta REST Client API provides a high-level API for accessing any REST resources, not just Jakarta REST services.
The Client API is defined in the jakarta.ws.rs.client
package.
Creating a Basic Client Request Using the Client API
The following steps are needed to access a REST resource using the Client API.
-
Obtain an instance of the
jakarta.ws.rs.client.Client
interface. -
Configure the
Client
instance with a target. -
Create a request based on the target.
-
Invoke the request.
The Client API is designed to be fluent, with method invocations chained together to configure and submit a request to a REST resource in only a few lines of code.
Client client = ClientBuilder.newClient();
String name = client.target("http://example.com/webapi/hello")
.request(MediaType.TEXT_PLAIN)
.get(String.class);
In this example, the client instance is first created by calling the jakarta.ws.rs.client.ClientBuilder.newClient
method.
Then, the request is configured and invoked by chaining method calls together in one line of code.
The Client.target
method sets the target based on a URI.
The jakarta.ws.rs.client.WebTarget.request
method sets the media type for the returned entity.
The jakarta.ws.rs.client.Invocation.Builder.get
method invokes the service using an HTTP GET
request, setting the type of the returned entity to String
.
Obtaining the Client Instance
The Client
interface defines the actions and infrastructure a REST client requires to consume a RESTful web service.
Instances of Client
are obtained by calling the ClientBuilder.newClient
method.
Client client = ClientBuilder.newClient();
Use the close
method to close Client
instances after all the invocations for the target resource have been performed:
Client client = ClientBuilder.newClient();
...
client.close();
Client
instances are heavyweight objects.
For performance reasons, limit the number of Client
instances in your application, as the initialization and destruction of these instances may be expensive in your runtime environment.
Setting the Client Target
The target of a client, the REST resource at a particular URI, is represented by an instance of the jakarta.ws.rs.client.WebTarget
interface.
You obtain a WebTarget
instance by calling the Client.target
method and passing in the URI of the target REST resource.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi");
For complex REST resources, it may be beneficial to create several instances of WebTarget
.
In the following example, a base target is used to construct several other targets that represent different services provided by a REST resource.
Client client = ClientBuilder.newClient();
WebTarget base = client.target("http://example.com/webapi");
// WebTarget at http://example.com/webapi/read
WebTarget read = base.path("read");
// WebTarget at http://example.com/webapi/write
WebTarget write = base.path("write");
The WebTarget.path
method creates a new WebTarget
instance by appending the current target URI with the path that was passed in.
Setting Path Parameters in Targets
Path parameters in client requests can be specified as URI template parameters, similar to the template parameters used when defining a resource URI in a Jakarta REST service.
Template parameters are specified by surrounding the template variable with braces ({}
).
Call the resolveTemplate
method to substitute the {username}
, and then call the queryParam
method to add another variable to pass.
WebTarget myResource = client.target("http://example.com/webapi/read")
.path("{userName}")
.resolveTemplate("userName", "janedoe")
.queryParam("chapter", "1");
// http://example.com/webapi/read/janedoe?chapter=1
Response response = myResource.request(...).get();
Invoking the Request
After setting and applying any configuration options to the target, call one of the WebTarget.request
methods to begin creating the request.
This is usually accomplished by passing to WebTarget.request
the accepted media response type for the request either as a string of the MIME type or using one of the constants in jakarta.ws.rs.core.MediaType
.
The WebTarget.request
method returns an instance of jakarta.ws.rs.client.Invocation.Builder
, a helper object that provides methods for preparing the client request.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
Invocation.Builder builder = myResource.request(MediaType.TEXT_PLAIN);
Using a MediaType
constant is equivalent to using the string defining the MIME type.
Invocation.Builder builder = myResource.request("text/plain");
After setting the media type, invoke the request by calling one of the methods of the Invocation.Builder
instance that corresponds to the type of HTTP request the target REST resource expects.
These methods are:
-
get()
-
post()
-
delete()
-
put()
-
head()
-
options()
For example, if the target REST resource is for an HTTP GET request, call the Invocation.Builder.get
method.
The return type should correspond to the entity returned by the target REST resource.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
String response = myResource.request(MediaType.TEXT_PLAIN)
.get(String.class);
If the target REST resource is expecting an HTTP POST request, call the Invocation.Builder.post
method.
Client client = ClientBuilder.newClient();
StoreOrder order = new StoreOrder(...);
WebTarget myResource = client.target("http://example.com/webapi/write");
TrackingNumber trackingNumber = myResource.request(MediaType.APPLICATION_XML)
.post(Entity.xml(order), TrackingNumber.class);
In the preceding example, the return type is a custom class and is retrieved by setting the type in the Invocation.Builder.post(Entity<?> entity, Class<T> responseType)
method as a parameter.
If the return type is a collection, use jakarta.ws.rs.core.GenericType<T>
as the response type parameter, where T
is the collection type:
List<StoreOrder> orders = client.target("http://example.com/webapi/read")
.path("allOrders")
.request(MediaType.APPLICATION_XML)
.get(new GenericType<List<StoreOrder>>() {});
This preceding example shows how methods are chained together in the Client API to simplify how requests are configured and invoked.
Using the Client API in the Jakarta REST Example Applications
The rsvp
and customer
examples use the Client API to call Jakarta REST services.
This section describes how each example application uses the Client API.
The Client API in the rsvp Example Application
The rsvp
application allows users to respond to event invitations using Jakarta REST resources, as explained in The rsvp Example Application.
The web application uses the Client API in CDI backing beans to interact with the service resources, and the Facelets web interface displays the results.
The StatusManager
CDI backing bean retrieves all the current events in the system.
The client instance used in the backing bean is obtained in the constructor:
public StatusManager() {
this.client = ClientBuilder.newClient();
}
The StatusManager.getEvents
method returns a collection of all the current events in the system by calling the resource at http://localhost:8080/rsvp/webapi/status/all, which returns an XML document with entries for each event.
The Client API automatically unmarshals the XML and creates a List<Event>
instance.
public List<Event> getEvents() {
List<Event> returnedEvents = null;
try {
returnedEvents = client.target(baseUri)
.path("all")
.request(MediaType.APPLICATION_XML)
.get(new GenericType<List<Event>>() {
});
if (returnedEvents == null) {
logger.log(Level.SEVERE, "Returned events null.");
} else {
logger.log(Level.INFO, "Events have been returned.");
}
} catch (WebApplicationException ex) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
...
return returnedEvents;
}
The StatusManager.changeStatus
method is used to update the attendee’s response.
It creates an HTTP POST
request to the service with the new response.
The body of the request is an XML document.
public String changeStatus(ResponseEnum userResponse,
Person person, Event event) {
String navigation;
try {
logger.log(Level.INFO,
"changing status to {0} for {1} {2} for event ID {3}.",
new Object[]{userResponse,
person.getFirstName(),
person.getLastName(),
event.getId().toString()});
client.target(baseUri)
.path(event.getId().toString())
.path(person.getId().toString())
.request(MediaType.APPLICATION_XML)
.post(Entity.xml(userResponse.getLabel()));
navigation = "changedStatus";
} catch (ResponseProcessingException ex) {
logger.log(Level.WARNING, "couldn''t change status for {0} {1}",
new Object[]{person.getFirstName(),
person.getLastName()});
logger.log(Level.WARNING, ex.getMessage());
navigation = "error";
}
return navigation;
}
The Client API in the customer Example Application
The customer
example application stores customer data in a database and exposes the resource as XML, as explained in The customer Example Application.
The service resource exposes methods that create customers and retrieve all the customers.
A Facelets web application acts as a client for the service resource, with a form for creating customers and displaying the list of customers in a table.
The CustomerBean
stateless session bean uses the Jakarta REST Client API to interface with the service resource.
The CustomerBean.createCustomer
method takes the Customer
entity instance created by the Facelets form and makes a POST call to the service URI.
public String createCustomer(Customer customer) {
if (customer == null) {
logger.log(Level.WARNING, "customer is null.");
return "customerError";
}
String navigation;
Response response =
client.target("http://localhost:8080/customer/webapi/Customer")
.request(MediaType.APPLICATION_XML)
.post(Entity.entity(customer, MediaType.APPLICATION_XML),
Response.class);
if (response.getStatus() == Status.CREATED.getStatusCode()) {
navigation = "customerCreated";
} else {
logger.log(Level.WARNING,
"couldn''t create customer with id {0}. Status returned was {1}",
new Object[]{customer.getId(), response.getStatus()});
FacesContext context = FacesContext.getCurrentInstance();
context.addMessage(null,
new FacesMessage("Could not create customer."));
navigation = "customerError";
}
return navigation;
}
The XML request entity is created by calling the Invocation.Builder.post
method, passing in a new Entity
instance from the Customer
instance, and specifying the media type as MediaType.APPLICATION_XML
.
The CustomerBean.retrieveCustomer
method retrieves a Customer
entity instance from the service by appending the customer’s ID to the service URI.
public String retrieveCustomer(String id) {
String navigation;
Customer customer =
client.target("http://localhost:8080/customer/webapi/Customer")
.path(id)
.request(MediaType.APPLICATION_XML)
.get(Customer.class);
if (customer == null) {
navigation = "customerError";
} else {
navigation = "customerRetrieved";
}
return navigation;
}
The CustomerBean.retrieveAllCustomers
method retrieves a collection of customers as a List<Customer>
instance.
This list is then displayed as a table in the Facelets web application.
public List<Customer> retrieveAllCustomers() {
List<Customer> customers =
client.target("http://localhost:8080/customer/webapi/Customer")
.path("all")
.request(MediaType.APPLICATION_XML)
.get(new GenericType<List<Customer>>() {
});
return customers;
}
Because the response type is a collection, the Invocation.Builder.get
method is called by passing in a new instance of GenericType<List<Customer>>
.
Advanced Features of the Client API
This section describes some of the advanced features of the Jakarta REST Client API.
Configuring the Client Request
Additional configuration options may be added to the client request after it is created but before it is invoked.
Setting Message Headers in the Client Request
You can set HTTP headers on the request by calling the Invocation.Builder.header
method.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
String response = myResource.request(MediaType.TEXT_PLAIN)
.header("myHeader", "The header value")
.get(String.class);
If you need to set multiple headers on the request, call the Invocation.Builder.headers
method and pass in a jakarta.ws.rs.core.MultivaluedMap
instance with the name-value pairs of the HTTP headers.
Calling the headers
method replaces all the existing headers with the headers supplied in the MultivaluedMap
instance.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
MultivaluedMap<String, Object> myHeaders =
new MultivaluedMap<>("myHeader", "The header value");
myHeaders.add(...);
String response = myResource.request(MediaType.TEXT_PLAIN)
.headers(myHeaders)
.get(String.class);
The MultivaluedMap
interface allows you to specify multiple values for a given key.
MultivaluedMap<String, Object> myHeaders =
new MultivaluedMap<String, Object>();
List<String> values = new ArrayList<>();
values.add(...);
myHeaders.add("myHeader", values);
Setting Cookies in the Client Request
You can add HTTP cookies to the request by calling the Invocation.Builder.cookie
method, which takes a name-value pair as parameters.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
String response = myResource.request(MediaType.TEXT_PLAIN)
.cookie("myCookie", "The cookie value")
.get(String.class);
The jakarta.ws.rs.core.Cookie
class encapsulates the attributes of an HTTP cookie, including the name, value, path, domain, and RFC specification version of the cookie.
In the following example, the Cookie
object is configured with a name-value pair, a path, and a domain.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
Cookie myCookie = new Cookie("myCookie", "The cookie value",
"/webapi/read", "example.com");
String response = myResource.request(MediaType.TEXT_PLAIN)
.cookie(myCookie)
.get(String.class);
Adding Filters to the Client
You can register custom filters with the client request or the response received from the target resource.
To register filter classes when the Client
instance is created, call the Client.register
method.
Client client = ClientBuilder.newClient().register(MyLoggingFilter.class);
In the preceding example, all invocations that use this Client
instance have the MyLoggingFilter
filter registered with them.
You can also register the filter classes on the target by calling WebTarget.register
.
Client client = ClientBuilder.newClient().register(MyLoggingFilter.class);
WebTarget target = client.target("http://example.com/webapi/secure")
.register(MyAuthenticationFilter.class);
In the preceding example, both the MyLoggingFilter
and MyAuthenticationFilter
filters are attached to the invocation.
Request and response filter classes implement the jakarta.ws.rs.client.ClientRequestFilter
and jakarta.ws.rs.client.ClientResponseFilter
interfaces, respectively.
Both of these interfaces define a single method, filter
.
All filters must be annotated with jakarta.ws.rs.ext.Provider
.
The following class is a logging filter for both client requests and client responses.
@Provider
public class MyLoggingFilter implements ClientRequestFilter,
ClientResponseFilter {
static final Logger logger = Logger.getLogger(...);
// implement the ClientRequestFilter.filter method
@Override
public void filter(ClientRequestContext requestContext)
throws IOException {
logger.log(...);
...
}
// implement the ClientResponseFilter.filter method
@Override
public void filter(ClientRequestContext requestContext,
ClientResponseContext responseContext) throws IOException {
logger.log(...);
...
}
}
If the invocation must be stopped while the filter is active, call the context object’s abortWith
method, and pass in a jakarta.ws.rs.core.Response
instance from within the filter.
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
...
Response response = new Response();
response.status(500);
requestContext.abortWith(response);
}
Asynchronous Invocations in the Client API
In networked applications, network issues can affect the perceived performance of the application, particularly in long-running or complicated network calls. Asynchronous processing helps prevent blocking and makes better use of an application’s resources.
In the Jakarta REST Client API, the Invocation.Builder.async
method is used when constructing a client request to indicate that the call to the service should be performed asynchronously.
An asynchronous invocation returns control to the caller immediately, with a return type of java.util.concurrent.Future<T>
(part of the Java SE concurrency API) and with the type set to the return type of the service call.
Future<T>
objects have methods to check if the asynchronous call has been completed, to retrieve the final result, to cancel the invocation, and to check if the invocation has been cancelled.
The following example shows how to invoke an asynchronous request on a resource.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
Future<String> response = myResource.request(MediaType.TEXT_PLAIN)
.async()
.get(String.class);
Using Custom Callbacks in Asynchronous Invocations
The InvocationCallback
interface defines two methods, completed
and failed
, that are called when an asynchronous invocation either completes successfully or fails, respectively.
You may register an InvocationCallback
instance on your request by creating a new instance when specifying the request method.
The following example shows how to register a callback object on an asynchronous invocation.
Client client = ClientBuilder.newClient();
WebTarget myResource = client.target("http://example.com/webapi/read");
Future<Customer> fCustomer = myResource.request(MediaType.TEXT_PLAIN)
.async()
.get(new InvocationCallback<Customer>() {
@Override
public void completed(Customer customer) {
// Do something with the customer object
}
@Override
public void failed(Throwable throwable) {
// handle the error
}
});
Using Reactive Approach in Asynchronous Invocations
Using custom callbacks in asynchronous invocations is easy in simple cases and when there are many independent calls to make. In nested calls, using custom callbacks becomes very difficult to implement, debug, and maintain.
Jakarta REST defines a new type of invoker called as RxInvoker
and a default implementation of this type is CompletionStageRxInvoker
.
The new rx
method is used as in the following example:
CompletionStage<String> csf = client.target("forecast/{destination}")
.resolveTemplate("destination","mars")
.request().rx().get(String.class);
csf.thenAccept(System.out::println);
In the example, an asynchronous processing of the interface CompletionStage<String>
is created and waits till it is completed and the result is displayed.
The CompletionStage
that is returned can then be used only to retrieve the result as shown in the above example or can be combined with other completion stages to ease and improve the processing of asynchronous tasks.
Using Server-Sent Events
Server-sent Events (SSE) technology is used to asynchronously push notifications to the client over standard HTTP or HTTPS protocol. Clients can subscribe to event notifications that originate on a server. Server generates events and sends these events back to the clients that are subscribed to receive the notifications. The one-way communication channel connection is established by the client. Once the connection is established, the server sends events to the client whenever new data is available.
The communication channel established by the client lasts till the client closes the connection and it is also re-used by the server to send multiple events from the server.
Overview of the SSE API
The SSE API is defined in the jakarta.ws.rs.sse
package that includes the interfaces SseEventSink
, SseEvent
, Sse
, and SseEventSource
.
To accept connections and send events to one or more clients, inject an SseEventSink
in the resource method that produces the media type text/event-stream
.
The following example shows how to accept the SSE connections and to send events to the clients:
@GET
@Path("eventStream")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void eventStream(@Context SseEventSink eventSink, @Context Sse sse) {
executor.execute(() -> {
try (SseEventSink sink = eventSink) {
eventSink.send(sse.newEvent("event1"));
eventSink.send(sse.newEvent("event2"));
eventSink.send(sse.newEvent("event3"));
}
});
}
The SseEventsink
is injected into the resource method and the underlying client connection is kept open and used to send events.
The connection persists until the client disconnects from the server.
The method send
returns an instance of CompletionStage<T>
which indicates the action of asynchronously sending a message to a client is enabled.
The events that are streamed to the clients can be defined with the details such as event
, data
, id
, retry
, and comment
.
Broadcasting Using SSE
Broadcasting is the action of sending events to multiple clients simultaneously.
Jakarta REST SSE API provides SseBroadcaster
to register all SseEventSink
instances and send events to all registered event outputs.
The life-cycle and scope of an SseBroadcaster
is fully controlled by applications and not the Jakarta REST runtime.
The following example show the use of broadcasters:
@Path("/")
@Singleton
public class SseResource {
@Context
private Sse sse;
private volatile SseBroadcaster sseBroadcaster;
@PostConstruct
public init() {
this.sseBroadcaster = sse.newBroadcaster();
}
@GET
@Path("register")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void register(@Context SseEventSink eventSink) {
eventSink.send(sse.newEvent("welcome!"));
sseBroadcaster.register(eventSink);
}
@POST
@Path("broadcast")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public void broadcast(@FormParam("event") String event) {
sseBroadcaster.broadcast(sse.newEvent(event));
}
}
@Singleton
annotation is defined for the resource class restricting the creation of multiple instances of the class.
The register
method on a broadcaster is used to add a new SseEventSink
; the broadcast
method is used to send an SSE event to all registered clients.
Listening and Receiving Events
Jakarta REST SSE provides the SseEventSource
interface for the client to subscribe to notifications.
The client can get asynchronously notified about incoming events by invoking one of the subscribe
methods in jakarta.ws.rs.sse.SseEventSource
.
The following example shows how to use the SseEventSource
API to open an SSE connection and read some of the messages for a period:
WebTarget target = client.target("http://...");
try (SseEventSource source = SseEventSource.target(target).build()) {
source.register(System.out::println);
source.open();
Thread.sleep(500); // Consume events for just 500 ms
source.close();
} catch (InterruptedException e) {
// falls through
}