In a distributed system your services usually communicate via HTTP and expose REST APIs. External clients or other services in your system consume these endpoints on a regular basis to e.g. fetch data from a different part of the domain. If you are using Java EE you can utilize the JAX-RS WebTarget
and Client
for this kind of communication. With the MicroProfile Rest Client specification, you'll get a more advanced and simpler way of creating these RESTful clients. You just declare interfaces and use a more declarative approach (like you might already know it from the Feign library).
Learn more about the MicroProfile Rest Client specification, its annotations, and how to use it in this blog post.
Specification profile: MicroProfile Rest Client
- Current version: 1.4
- GitHub repository
- Latest specification document
- Basic use case: Provide a type-safe approach to invoke RESTful services over HTTP.
Defining the RESTful client
For defining the Rest Client you just need a Java interface and model the remote REST API using JAX-RS annotations:
1 2 3 4 5 6 7 8 9 10 11 | public interface JSONPlaceholderClient { @GET @Path("/posts") JsonArray getAllPosts(); @POST @Path("/posts") Response createPost(JsonObject post); } |
You can specify the response type with a specific POJO (JSON-B will then try to deserialize the HTTP response body) or use the generic Response
class of JAX-RS.
Furthermore, you can indicate an asynchronous execution, if you use CompletionStage<T>
as the method return type:
1 2 3 | @GET @Path("/posts/{id}") CompletionStage<JsonObject> getPostById(@PathParam("id") String id); |
Path variables and query parameters for the remote endpoint can be specified with @PathParam
and @QueryParam
:
1 2 3 4 5 6 7 | @GET @Path("/posts") JsonArray getAllPosts(@QueryParam("orderBy") String orderDirection); @GET @Path("/posts/{id}/comments") JsonArray getCommentsForPostByPostId(@PathParam("id") String id); |
You can define the media type of the request and the expected media type of the response on either interface level or for each method separately:
1 2 3 4 5 6 7 8 9 10 | @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public interface JSONPlaceholderClient { @GET @Produces(MediaType.APPLICATION_XML) // overrides the JSON media type only for this method @Path("/posts/{id}") CompletionStage<JsonObject> getPostById(@PathParam("id") String id); } |
If you have to declare specific HTTP headers (e.g. for authentication), you can pass them either to the method with @HeaderParam
or define them with @ClientHeaderParam
(static value or refer to a method):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @ClientHeaderParam(name = "X-Application-Name", value = "MP-blog") public interface JSONPlaceholderClient { @PUT @ClientHeaderParam(name = "Authorization", value = "{generateAuthHeader}") @Path("/posts/{id}") Response updatePostById(@PathParam("id") String id, JsonObject post, @HeaderParam("X-Request-Id") String requestIdHeader); default String generateAuthHeader() { return "Basic " + new String(Base64.getEncoder().encode("duke:SECRET".getBytes())); } } |
Specifying multiple HTTP headers
If you want to generate multiple HTTP headers or propagate HTTP headers from an incoming JAX-RS request (e.g. pass the Authorization header to a downstream system), you can use the ClientHeadersFactory
.
This interface specifies one method that returns the final HTTP headers for an outgoing client call. The headers might still be manipulated by a filter or any other mechanism before sending the client request.
While implementing this method you get two arguments passed to the update
method. First, yo get passed incoming headers if the Rest Client is used as part of a JAX-RS request. These might be empty.
Furthermore, you have access to the HTTP headers you specified already at your interface while using e.g. @ClientHeaderParam
:
1 2 3 4 5 | pubic interface ClientHeadersFactory { MultivaluedMap<String, String> update( MultivaluedMap<String, String> incomingHeaders, MultivaluedMap<String, String> clientOutgoingHeaders); } |
Inside your implementation, you can now define logic for the outgoing HTTP headers. As an example I'm merging the incoming headers with the client outgoing headers and add three more headers manually:
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 27 28 29 30 31 32 | @ApplicationScoped public class GlobalClientHeaders implements ClientHeadersFactory { @Inject @ConfigProperty(name = "secrets.value") private String secretValue; @Override public MultivaluedMap<String, String> update( MultivaluedMap<String, String> incomingHeaders, MultivaluedMap<String, String> clientOutgoingHeaders) { System.out.println("--- Incoming headers of the JAX-RS environment"); incomingHeaders.forEach((k, v) -> System.out.println(k + ":" + v)); System.out.println("--- Specified outgoing headers of the Rest Client"); clientOutgoingHeaders.forEach((k, v) -> System.out.println(k + ":" + v)); MultivaluedMap<String, String> resultHeader = new MultivaluedHashMap(); resultHeader.putAll(incomingHeaders); resultHeader.putAll(clientOutgoingHeaders); resultHeader.add("X-Secret-Header", secretValue); resultHeader.add("X-Global-Header", "duke"); resultHeader.add("X-Special-Header", "MicroProfile"); System.out.println("--- Header of the Rest Client after merging"); resultHeader.forEach((k, v) -> System.out.println(k + ":" + v)); return resultHeader; } } |
Besides the benefit of propagating HTTP headers of a JAX-RS request, you can use @Inject
here if your implementation is managed by CDI. This allows you to inject secrets for example or any other CDI bean to calculate a header value.
Finally, you have to register your factory implementation using @RegisterClientHeaders(NameOfFactoryImpl.class)
on your Rest Client interface.
1 2 3 4 | @RegisterRestClient @RegisterClientHeaders(GlobalClientHeaders.class) public interface JSONPlaceholderClient { } |
You can get further information on using the ClientHeadersFactory
interface in the MicroProfile Rest Client 1.4 update video.
Using the client interface
Once you define your Rest Client interface you have two ways of using them. First, you can make use of the programmatic approach using the RestClientBuilder
. With this builder we can set the base URI, define timeouts and register JAX-RS features/provider like ClientResponseFilter
, MessageBodyReader
, ReaderInterceptor
etc.
1 2 3 4 5 6 7 8 | JSONPlaceholderClient jsonApiClient = RestClientBuilder.newBuilder() .baseUri(new URI("https://jsonplaceholder.typicode.com")) .register(ResponseLoggingFilter.class) .connectTimeout(2, TimeUnit.SECONDS), .readTimeout(2, TimeUnit.SECONDS) .build(JSONPlaceholderClient.class); jsonApiClient.getPostById("1").thenAccept(System.out::println); |
In addition to this, we can use CDI to inject the Rest Client. To register the interface as a CDI managed bean during runtime, the interface requires the @RegisterRestClient
annotation:
1 2 3 4 5 | @RegisterRestClient @RegisterProvider(ResponseLoggingFilter.class) public interface JSONPlaceholderClient { } |
With the @RegisterProvider
you can register further JAX-RS provider and features as you've seen it in the programmatic approach. If you don't specify any scope for the interface, the @Dependent
scope will be used by default. With this scope, your Rest Client bean is bound (dependent) to the lifecycle of the injector class.
You can now use it as any other CDI bean and inject it into your classes. Make sure to add the CDI qualifier @RestClient
to the injection point:
1 2 3 4 5 6 7 8 | @ApplicationScoped public class PostService { @Inject @RestClient JSONPlaceholderClient jsonPlaceholderClient; } |
Further configuration for the Rest Client
If you use the CDI approach, you can make use of MicroProfile Config to further configure the Rest Client. You can specify the following properties with MicroProfile Config:
- Base URL (
.../mp-rest/url
) - Base URI (
.../mp-rest/uri
) - The CDI scope of the client as a fully qualified class name (
.../mp-rest/scope
) - JAX-RS provider as a comma-separated list of fully qualified class names (
../mp-rest/providers
) - The priority of a registered provider (
.../mp-rest/providers/com.acme.MyProvider/priority
) - Connect and read timeouts (
.../mp-rest/connectTimeout
and.../mp-rest/readTimeout
)
You can specify these properties for each client individually as you have to specify the fully qualified class name of the Rest Client for each property:
1 2 3 | de.rieckpil.blog.JSONPlaceholderClient/mp-rest/url=https://jsonplaceholder.typicode.com de.rieckpil.blog.JSONPlaceholderClient/mp-rest/connectTimeout=3000 de.rieckpil.blog.JSONPlaceholderClient/mp-rest/readTimeout=3000 |
YouTube video for using MicroProfile Rest Client
Watch the following YouTube video of my Getting started with MicroProfile series to see MicroProfile Rest Client in action:
You can find the source code with further instructions to run this example on GitHub.
Have fun using MicroProfile Rest Client,
Phil