New Feature in Payara Platform 5.182: MicroProfile REST Client

Photo of Ondro Mihályi by Ondro Mihályi

The 5.182 release of Payara Server & Payara Micro (Payara Platform) brings in MicroProfile 1.3. This introduces a couple of updates to some existing specifications, and three new ones: OpenTracing, OpenAPI, and Type-safe REST Client. In this blog, I’ll be covering our implementation of the Type-safe REST Client. 

 

Want to know more about MicroProfile in Payara Server and Payara Micro? Check out these articles on our blog:

MicroProfile REST Client API provides a type-safe way to call remote REST services via a plain Java interface. It extends JAX-RS API by reusing the same annotations that are available to map incoming REST requests to a call of a Java method. But the annotations are applied on a client interface and are interpreted in an opposite direction to allow mapping of Java method call to a REST request.

 

Since JAX-RS provides only a programmatic client API, MicroProfile is a nice addition and allows separating business logic and a configuration of a REST call via an annotated Java interface.

 

You just need to create an interface with a method for each combination of paths, path parameters, request parameters and HTTP method types you need. Then you can just inject the interface:

    @Inject
@RestClient
private HelloService helloService

And then simply call methods on the injected instance and a REST call will be issued behind the scenes:

    helloService.hello("World");

Get Started: Add MicroProfile REST Client API

 

Before using the REST Client API, we need to add its API JAR into our application. With Maven, it's as easy as adding the following dependencies into the pom.xml:

 

<dependency>
       <groupId>org.eclipse.microprofile.rest.client</groupId>
        <artifactId>microprofile-rest-client-api</artifactId>
        <version>1.1</version>
        <scope>provided</scope>
    </dependency>    
    <dependency>
        <groupId>javax.ws.rs</groupId>
        <artifactId>javax.ws.rs-api</artifactId>
        <version>2.0.1</version>
        <scope>provided</scope>
    </dependency>

 

If you're not using Maven, you need to download the dependencies from Maven and add them to your project as provided dependencies (so that they're not added to the built WAR file).

 

Step 1: Declare a REST Client Interface

The first is very simple for a Java programmer: create a plain Java interface with methods that represent future REST calls. For example, let's create a HelloService interface with a single method:

    public interface HelloService {
    String hello(String name);
}

We want to inject this interface using,@Inject @RestClient so we'll mark the interface with the @RegisterRestClient annotation provided by the MicroProfile REST Client API so that the container discovers it and enables for injection:

    @RegisterRestClient
public interface HelloService

Note that injected instances will have @Dependent scope by default, which means a new instance will be created for each injection. If you need to optimize this with a different scope, you can add a CDI scope annotation on the interface.

Now we need to declare the root URL which is common for all methods on the interface. This URL will be appended to a base URL which is configured separately because it depends on the location of the remote REST service. We use the @javax.ws.rs.Path annotation on the interface to define the root URL of "/api/hello":

@Path("/api/hello")
public interface HelloService

We can also define the default mime types produced in a request by the methods and accepted in a response using existing JAX-RS annotations @Consumes and @Produces (these can be overridden by annotations on individual methods):

    @Produces(MediaType.TEXT_PLAIN)
  @Consumes(MediaType.TEXT_PLAIN)
public interface HelloService

We can also define additional JAX-RS providers using the @RegisterProvider annotation from the MicroProfile REST Client API:

    @RegisterProvider(MyClientRequestFilter.class)
public interface HelloService

Step 2: Add Method Arguments

Very often we need some arguments to REST requests. Arguments can be either additional path elements in the URL, query parameters or message body for some HTTP methods. Even here we can reuse the same mapping configuration as in JAX-RS endpoint methods to map a Java method to an HTTP request and response. Let's see how.

 

Each individual method can be bound to a subpath of the path defined for the whole interface. If a @Path annotation is applied to a method, its value is appended to the path. We can use path parameters to define URL segments, which can be mapped to method arguments using @javax.ws.rs.PathParam. For example, we can define a "name" path parameter and map it to an argument like this:

    @Path("{name}")
    String hello(@PathParam("name") String name);

With this code, a call of hello("Payara") will be transformed to an URL like http://localhost/api/hello/Payara.

 

Additional request parameters can be added with arguments annotated with @javax.ws.rs.QueryParam that defines their name in the query. For example, we can add a "verbose" query parameter like this:

    @Path("{name}")
    String hello(@PathParam("name") String name, @QueryParam("verbose") boolean verbose);

And a call of hello("Payara", true) will be transformed to an URL like http://localhost/api/hello/Payara?verbose=true.

For methods that accept a message body (such as POST and PUT), we can send the body as an additional argument. No annotation is needed for this, for example:

    @POST
    String saveGreeting(Greeting greeting);

Step 3: Use the REST Client Interface

An interface annotated with @RegisterRestClient can be injected in any CDI bean. We just need to mark an injection point with the @RestClient qualifier annotation:

    @Inject
@RestClient
private HelloService helloService

With an acquired instance, we can now call any method on the interface to invoke a REST call. However, before we can do that, we need to specify one mandatory configuration option and that's the base URL of a remote service. All the options, including the base URL, can be specified using MicroProfile Config API, which is also included in a full MicroProfile set of APIs and is available in Payara Platform as well. To have a reasonable default value, we'll create a configuration file microprofile-config.properties in a META-INF folder on the classpath and add a default value for the base URL for our HelloService. This file will contain a property composed of the full name of the interface (including its package), "mp-rest" and "url", all separated by "/". For example, a configuration of the base url for our HelloService will look like this:

    fish.payara.restclient.HelloService/mp-rest/url=http://localhost:8080/rest-client

The URL also includes the context path of the remote service and anything that should be prepended before the path specified in the @Path annotation. In result, a URL of the REST service invoked from HelloService would be

 

 http://localhost:8080/rest-client/api/hello.

 

All the other properties are either not needed or have reasonable default values. If you need, you can configure them using MicroProfile Config as well. Instead of the "url" property in the configuration key, you can use one of the following:

  • uri (you can use this property instead of url to set the base URL)
  • scope (set the CDI scope of the injected client proxies)
  • providers (an alternative to using the @RegisterProvider annotation)
  • priority (will override the priority of a specific provider)

For more details about the properties, consult the section "Support for MicroProfile Config" of the MicroProfile REST Client specification document.

 

If we need more control over the creation and configuration of a REST client proxy, we can also use the programmatic approach to creating it. in order to do that, we'll use a builder retrieved by RestClientBuilder.newBuilder(). This builder implements the same javax.ws.rs.core.Configurable interface that is implemented by JAX-RS ClientBuilder. We will, therefore, reuse the JAX-RS ClientBuilder API to configure the created MicroProfile REST Client proxy:

    HelloService helloService = RestClientBuilder.newBuilder()
.baseUri(new URI("http://localhost:8080/rest-client"))
.build(HelloService.class);

Whatever way we retrieve a HelloService proxy, we can then call any method on the interface, like this:

    String greeting = remoteApi.hello("World");

Behind the scenes, the Java method call is transformed to an HTTP call to a remote REST service and a subsequent response is transformed into a value returned from the method.

 

Call a REST Service Asynchronously

MicroProfile REST Client also supports asynchronous invocation of REST services since version 1.1, which is part of MicroProfile 1.4. We can use this API in the Payara Platform since version 5.182, as an addition to MicroProfile 1.3 which only includes support for synchronous REST clients. Full support of MicroProfile 1.4 and 2.0 features is planned for a future version of Payara Platform.

 

Similar to resource methods in the latest JAX-RS 2.1 version, methods in a MicroProfile REST client interface can also return CompletionStage instead of a direct value. This allows delivering the response when it's ready even after a REST client method returns.

 

When a method that returns CompletionStage is called, it will return as soon as possible, without waiting for the REST call to complete. The returned stage is completed (either with a result or exceptionally) when the REST call is completed or timed out. An important aspect of returning CompletionStage is that the calling thread isn't blocked and can continue further processing. The REST call happens in the background, in a separate thread.

 

For example, we can turn our synchronous method hello, which returns a String, into an asynchronous one like this:

 

    @Path("{name}")
    @GET
    CompletionStage<String> helloAsync(@PathParam("name") String name);

 

The asynchronous method can be called in the same way as the synchronous method. However, the method will return as soon s possible, without waiting for a response. The code to execute after the REST call returns a response should be chained using the returned CompletionStage. For example, this code will transform a String response to upper case:

    helloService.helloAsync("World (Async)")
.thenApply(String::toUpperCase);

The code chained to the CompletionStage will be executed in a separate thread managed by the container. If you want to use a custom thread executor, you need to use the programmatic API. While it's not possible to define an executor for injected proxies, it's possible to specify a custom executor using the programmatic API like this:

    HelloService remoteApi = RestClientBuilder.newBuilder()
.baseUri(new URI("http://localhost:8080/rest-client"))
.executorService(customExecutor)
.build(HelloService.class);

NOTE: You should always use a managed executor provided by the container. In Java EE containers, a managed executor can be retrieved by injecting a ManagedExecutorService or created with an injected ManagedThreadFactory.

 

Further Reading

You can find an example Microprofile REST Client application with a full source code in Payara Examples repository.

 

The MicroProfile REST Client specification and API documentation provide more details.

 

If you're not familiar with the JAX-RS API, you can find out more about it at the JAX-RS 2.1 (JSR 370) specification page. If you already know JAX-RS API, MicroProfile REST Client will make it easier and more natural for you to invoke REST services without looking too much into the documentation. If you want to keep playing more with MicroProfile REST Client, I’d recommend looking at the MicroProfile REST Client repository to see how things are changing in MicroProfile REST Client  1.2 and beyond. And as always, feel free to raise enhancement requests or issues on our GitHub Issues page or forum.

 

Comments