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.

  1. Obtain an instance of the jakarta.ws.rs.client.Client interface.

  2. Configure the Client instance with a target.

  3. Create a request based on the target.

  4. 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
}