In one of my recent blog posts, I presented Spring's WebClient for RESTful communication. With Java EE we can utilize the JAX-RS Client
and WebTarget
classes to achieve the same. However, if you add the MicroProfile API to your project, you can make use of the MicroProfile Rest Client specification. This API targets the following goal:
The MicroProfile Rest Client provides a type-safe approach to invoke RESTful services over HTTP. As much as possible the MP Rest Client attempts to use JAX-RS 2.0 APIs for consistency and easier re-use.
With MicroProfile 3.3 you'll get the latest version of the Rest Client which is 1.4. RESTful communication becomes quite easy with this specification as you just define the access to an external REST API with an interface definition and JAX-RS annotations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Path("/movies") public interface MovieReviewService { @GET Set<Movie> getAllMovies(); @POST @Path("/{movieId}/reviews") String submitReview(@PathParam("movieId") String movieId, Review review); @PUT @Path("/{movieId}/reviews/{reviewId}") Review updateReview(@PathParam("movieId") String movieId, @PathParam("reviewId") String reviewId, Review review); } |
In this blog post, I'll demonstrate an example usage of MicroProfile Rest Client using Java EE, MicroProfile, Java 8 running on Payara.
System architecture
The project contains two services: order application and user management application. Alongside order data, the order application also stores the id of the user who created this order. To resolve a username from the user id and to create new orders the application accesses the user management application's REST interface:
For simplicity, I keep the implementation simple and store the objects in memory without an underlying database.
MicroProfile project setup
Both applications were created with my Java EE 8 and MicroProfile Maven archetype and contain just both APIs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.eclipse.microprofile</groupId> <artifactId>microprofile</artifactId> <version>3.2</version> <type>pom</type> <scope>provided</scope> </dependency> </dependencies> |
The order application has two JAX-RS endpoints, one for reading orders by their id and one for creating an order:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Path("orders") @Produces("application/json") @Consumes("application/json") public class OrderResource { @Inject private OrderService orderService; @GET @Path("/{id}") public JsonObject getOrderById(@PathParam("id") Integer id) { return orderService.getOrderById(id); } @POST public Response createNewOrder(JsonObject order, @Context UriInfo uriInfo) { Integer newOrderId = this.orderService.createNewOrder(new Order(order)); UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder(); uriBuilder.path(Integer.toString(newOrderId)); return Response.created(uriBuilder.build()).build(); } } |
The user management application has also two JAX-RS endpoints to resolve a username by its id and to create a new user. Both of these endpoints are required for the order application to work properly and are synchronously called:
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 33 34 35 36 37 38 39 40 41 42 43 | @Path("users") @Consumes("application/json") @Produces("application/json") @ApplicationScoped public class UserResource { private ConcurrentHashMap<Integer, String> userDatabase; private Faker randomUser; @PostConstruct public void init() { this.userDatabase = new ConcurrentHashMap<>(); this.userDatabase.put(1, "Duke"); this.userDatabase.put(2, "John"); this.userDatabase.put(3, "Tom"); this.randomUser = new Faker(); } @GET @Path("/{userId}") public JsonObject getUserById(@PathParam("userId") Integer userId, @HeaderParam("X-Request-Id") String requestId, @HeaderParam("X-Application-Name") String applicationName) { System.out.println( String.format("External system with name '%s' " + "and request id '%s' trying to access " + "user with id '%s'", applicationName, requestId, userId)); return Json .createObjectBuilder() .add("username", this.userDatabase.getOrDefault(userId, "Default User")) .build(); } @POST @RolesAllowed("ADMIN") public void createNewUser(JsonObject user) { this.userDatabase .put(user.getInt("userId"), this.randomUser.name().firstName()); } } |
For a more advanced use case, I'm tracking the access to /users/{userId}
by printing two custom HTTP headers X-Request-Id
and X-Application-Name
. In addition, posting a new user requires authentication and authorization which is basic authentication for which I'm using the Java EE 8 Security API.
Invoke RESTful services over HTTP with Rest Client
The REST access to the user management app is specified with a Java interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @RegisterRestClient @Path("/resources/users") @Produces("application/json") @Consumes("application/json") @ClientHeaderParam(name = "X-Application-Name", value = "ORDER-MGT-APP") public interface UserManagementApplicationClient { @GET @Path("/{userId}") JsonObject getUserById(@HeaderParam("X-Request-Id") String requestIdHeader, @PathParam("userId") Integer userId); @POST @ClientHeaderParam(name = "Authorization", value = "{generateAuthHeader}") Response createUser(JsonObject user); default String generateAuthHeader() { return "Basic " + new String(Base64.getEncoder().encode("duke:SECRET".getBytes())); } } |
Every method of this interface represents one REST endpoint of the external service. With the common JAX-RS annotations like @GET
, @POST
, @Path
, and @PathParam
you can specify the HTTP method and URL parameters. The return type of the method represents the HTTP response body which is deserialized using the MessageBodyReader
which is makes use of JSON-B for application/json
. For sending data alongside the HTTP request body, you can add a POJO as the method argument.
Furthermore, you can add HTTP headers to your calls by using either @HeaderParam
or @ClientHeaderParam
. With @HeaderParam
you mark a method argument as an HTTP header and can pass it from outside to the Rest Client. The @ClientHeaderParam
on the other side does not modify the method signature with an additional argument and retrieves its value either by config, by a hardcoded string, or by calling a method. In this example, I'm using it to add the X-Application-Name
header to every HTTP request and for the authorization header which is required for basic auth. You can use this annotation on both class and method level.
Rest Client configuration and CDI integration
To integrate this Rest Client with CDI and make it injectable, you can register the client with @RegisterRestClient
. Any other bean can now inject the Rest Client with the following code:
1 2 3 | @Inject @RestClient private UserManagementApplicationClient userManagementApplicationClient; |
The URL of the remote service is configured either with the @RegisterRestClient(baseUri="http://somedomain/api")
annotation or using the MicroProfile Config API. For this example, I'm using the configuration approach with a microprofile-config.properties
file:
1 2 3 | de.rieckpil.blog.order.control.UserManagementApplicationClient/mp-rest/url=http://user-management-application:8080 de.rieckpil.blog.order.control.UserManagementApplicationClient/mp-rest/connectTimeout=3000 de.rieckpil.blog.order.control.UserManagementApplicationClient/mp-rest/readTimeOut=3000 |
Besides the URL you can configure the HTTP connect and read timeouts for this Rest Client and specify JAX-RS providers to intercept the requests/responses. For more information have a look at the specification document.
As I'm using docker-compose
for deploying the two applications, I can use the name of the user app for external access:
1 2 3 4 5 6 7 8 9 10 | version: '3' services: order-application: build: order-application ports: - "8080:8080" links: - user-management-application user-management-application: build: user-management-application |
For further information have a look at the GitHub repository of this specification and the release page to get the latest specification documents.
You can find the source code with a step-by-step guide to start the two applications on GitHub.
For more information on Eclipse MicroProfile, take a look at my Getting Started with Eclipse MicroProfile Course Bundle.
Have fun using the MicroProfile Rest Client API,
Phil