The microprofile-rest-client
quickstart demonstrates the use of the MicroProfile REST Client specification in {productName}.
MicroProfile REST Client provides a type-safe approach to invoke RESTful services over HTTP. It relies on JAX-RS APIs for consistency and easier reuse.
In this quickstart, we have two services — a country server and a country client. The server provides a simple REST interface providing information about some countries. The client consumes this API through the MicroProfile REST Client specification.
We recommend that you follow the instructions that create the application step by step. However, you can also go right to the completed example which is available in this directory.
As this quickstart is about MicroProfile REST Client integration we will just deploy the country server to the {productName} so we can create the client application that will be contacting this server.
cd country-server
mvn clean package wildfly:deploy
Note
|
Make sure that your {productName} server is running. |
You can verify that the server is responding by accessing
http://localhost:8080/country-server/name/France
endpoint using your browser or
curl http://localhost:8080/country-server/name/France
to get some information about France
.
Now we can start creating our MicroProfile REST Client application. In different directory run:
mvn archetype:generate \
-DgroupId=org.wildfly.quickstarts \
-DartifactId=country-client \
-DinteractiveMode=false \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-webapp
cd country-client
Open the project in your favourite IDE.
Open the generated pom.xml
.
The first thing to do is to setup our dependencies. Add the following section to your
pom.xml
:
<dependencyManagement>
<dependencies>
<!-- importing the microprofile BOM adds MicroProfile specs -->
<dependency>
<groupId>org.wildfly.bom</groupId>
<artifactId>wildfly-microprofile</artifactId>
<version>{versionMicroprofileBom}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Now we need to add the following dependencies:
<!-- Import the MicroProfile REST Client API, we use provided scope as the API is included in the server -->
<dependency>
<groupId>org.eclipse.microprofile.rest.client</groupId>
<artifactId>microprofile-rest-client-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Import the MicroProfile Config API, we use provided scope as the API is included in the server -->
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Import the CDI API, we use provided scope as the API is included in the server -->
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Import the Jakarta REST API, we use provided scope as the API is included in the server -->
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<scope>provided</scope>
</dependency>
All dependencies can have provided scope.
As we are going to be deploying this application to the {productName} server, let’s also add a maven plugin that will simplify the deployment operations (you can replace the generated build section):
<build>
<!-- Set the name of the archive -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- Allows to use mvn wildfly:deploy -->
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
As this is a Jakarta REST application we need to also create an application class.
Create org.wildfly.quickstarts.microprofile.rest.client.JaxRsApplication
with the
following content:
Note
|
The new file should be created in
src/main/java/org/quickstarts/microprofile/rest/client/JaxRsApplication.java .
|
package org.wildfly.quickstarts.microprofile.rest.client;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/")
public class JaxRsApplication extends Application {
}
Now we are ready to start working with MicroProfile REST Client.
In this guide, we will be demonstrating how to consume a part of the REST API
supplied by the country-server
service which should be already deployed and running
on your {productName} server (see the previous section).
Our first step is to setup the model we will be using in form of the Country
POJO
which we will use for our JSON transformations. Create the
org.wildfly.quickstarts.microprofile.rest.client.model.Country
class:
package org.wildfly.quickstarts.microprofile.rest.client.model;
public class Country {
public String name;
public String capital;
public String currency;
}
Now we can start working with the MicroProfile REST Client.
Using the MicroProfile REST Client is as simple as creating an interface which uses the
proper JAX-RS and MicroProfile annotations. In our case, the
interface should be created in
org.wildfly.quickstarts.microprofile.rest.client.CountriesServiceClient
:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.wildfly.quickstarts.microprofile.rest.client.model.Country;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@Path("/")
public interface CountriesServiceClient {
@GET
@Path("/name/{name}")
@Produces("application/json")
Country getByName(@PathParam("name") String name);
}
The getByName
method gives our code the ability to query a country by name
from the REST Countries API. The client will handle all the networking and
marshalling leaving our code clean of such technical details.
As you can see, all that our REST client interface uses for now are standard JAX-RS annotations. There are two ways how to expose the REST client to use it in your application: the CDI lookup and the programmatic lookup.
To register our created REST Client interface with the CDI we need to add the
@RegisterRestClient
annotation to its declaration:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.wildfly.quickstarts.microprofile.rest.client.model.Country;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@Path("/")
@RegisterRestClient
public interface CountriesServiceClient {
@GET
@Path("/name/{name}")
@Produces("application/json")
Country getByName(@PathParam("name") String name);
}
Now it’s ready to be injected in any CDI bean but first we must configure the base URL/URI to which REST calls will be made. There are also two options of how to configure it:
-
Directly in the
@RegisterRestClient
via thebaseUri
attribute if the the base URI is static. -
MicroProfile Config if the base URI/URL should be configurable.
Let’s start with the easier definition directly in the @RegisterRestClient
.
Update CountriesServiceClient
:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.wildfly.quickstarts.microprofile.rest.client.model.Country;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@Path("/")
@RegisterRestClient(baseUri = "http://localhost:8080/country-server")
public interface CountriesServiceClient {
@GET
@Path("/name/{name}")
@Produces("application/json")
Country getByName(@PathParam("name") String name);
}
Note
|
We will cover the configuration through the MicroProfile Config in the later section. |
Now we can start using our statically configured REST client.
Create org.wildfly.quickstarts.microprofile.rest.client.CountriesResource
with the
following content:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.wildfly.quickstarts.microprofile.rest.client.model.Country;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/country")
@ApplicationScoped
public class CountriesResource {
@Inject
@RestClient
private CountriesServiceClient countriesServiceClient;
@GET
@Path("/cdi/{name}")
@Produces(MediaType.APPLICATION_JSON)
public Country cdiName(@PathParam("name") String name) {
return countriesServiceClient.getByName(name);
}
}
Note the in addition to the standard CDI @Inject
annotation, we also use the
MicroProfile @RestClient
qualifier annotation for the injection. The
@RestClient
is only required when there can be more produced CDI beans for
the same class.
Now we are ready to build, deploy and test our application:
Build and deploy the application:
$ mvn clean package wildfly:deploy
Now you can access
http://localhost:8080/country-client/country/cdi/France
endpoint using your browser or
curl http://localhost:8080/country-client/country/cdi/France
to get some information about France
.
Congratulations! You are already invoking custom REST client from your JAX-RS endpoint.
To configure the base URI of the REST client dynamically the MicroProfile REST Client uses the MicroProfile Config specification. We already included it in our dependencies so we can start adding the configuration.
First we remove the statically configured base URI from the CountriesServiceClient
:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.wildfly.quickstarts.microprofile.rest.client.model.Country;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@Path("/")
@RegisterRestClient
public interface CountriesServiceClient {
@GET
@Path("/name/{name}")
@Produces("application/json")
Country getByName(@PathParam("name") String name);
}
Note
|
If you are not familiar with the MicroProfile Config specification please see the
microprofile-config guide.
|
The name of the property for the base URI of our REST client needs to follow a
certain convention. Create a new file
src/main/resources/META-INF/microprofile-config.properties
with the following content:
org.wildfly.quickstarts.microprofile.rest.client.CountriesServiceClient/mp-rest/url=http://localhost:8080/country-server
org.wildfly.quickstarts.microprofile.rest.client.CountriesServiceClient/mp-rest/scope=jakarta.inject.Singleton
This configuration means that:
-
all requests performed using
org.wildfly.quickstarts.microprofile.rest.client.CountriesServiceClient
will usehttp://localhost:8080/country-server
as a base URL. Using the configuration above, calling thegetByName
method ofCountriesServiceClient
with a value ofFrance
would result in an HTTP GET request being made tohttp://localhost:8080/country-server/name/France
. -
the default scope of
org.wildfly.quickstarts.microprofile.rest.client.CountriesServiceClient
will be@Singleton
. All supported values are described later. The default scope can also be defined on the interface.
NOTE that org.wildfly.quickstarts.microprofile.rest.client.CountriesServiceClient
must match the fully qualified name of the CountriesServiceClient
we created in
the previous section.
Now we can rebuild and redeploy the application:
$ mvn clean package wildfly:deploy
And repeat the requests to
http://localhost:8080/country-client/country/cdi/France
endpoint using your browser or
curl http://localhost:8080/country-client/country/cdi/France
to get some information about France
. The output should be the same. However,
now we can change the base URL dynamically in the microprofile-config.properties
file without the need to change the code. Again for more information about
MicroProfile Config specification please see the microprofile-config
quickstart or
the specification available at https://github.com/eclipse/microprofile-config. This
is particularly useful when you are developing the application in different
environment than in which it will be running in production (e.g. localhost vs
kubernetes addresses).
The CDI defined interfaces can be configured through the MicroProfile Config
properties to define additional behavior or to override @RegisterRestClient
annotations. Supported properties are:
-
my.package.MyClient/mp-rest/url
- base URL of the target -
my.package.MyClient/mp-rest/uri
- base URI of the target -
my.package.MyClient/mp-rest/scope
- CDI scope to use for injection, default isjakarta.enterprise.context.Dependent
-
my.package.MyClient/mp-rest/providers/my.package.MyProvider/priority
- override the priority of the provider -
my.package.MyClient/mp-rest/connectTimeout
- connection timeout in milliseconds -
my.package.MyClient/mp-rest/readTimeout
- timeout to wait for response in milliseconds
It is also possible to define a custom configuration key in the @RegisterRestClient
annotation. If you would define interface like this:
package my.package;
@RegisterRestClient(configKey="myClient")
public interface MyClient {
// ...
}
then you can define properties like:
myClient/mp-rest/url==...
myClient/mp-rest/scope==...
If you need to create the instance of the REST Client proxy programmatically you can
use the RestClientBuilder
class. Update the CountriesResource
by adding the
following method:
@GET
@Path("/programmatic/{name}")
@Produces(MediaType.APPLICATION_JSON)
public Country programmaticName(@PathParam("name") String name) throws MalformedURLException {
CountriesServiceClient client = RestClientBuilder.newBuilder()
.baseUrl(new URL("http://localhost:8080/country-server"))
.build(CountriesServiceClient.class);
return client.getByName(name);
}
In this method, we use the MicroProfile RestClientBuilder
to programmatically get
access to the new instance of the CountriesServiceClient
. The only required
parameter is the base URL (or URI) which is set by the method baseUrl
. The
returned instance can be used as previously.
Build and deploy the application:
$ mvn clean package wildfly:deploy
Now you can access
http://localhost:8080/country-client/country/programmatic/France
endpoint using your browser or
curl http://localhost:8080/country-client/country/programmatic/France
to get some information about France
. The output will be the same as previously.
The MicroProfile REST Client supports asynchronous REST calls. Let’s see it action
by adding a getByNameAsync
method in out CountriesServiceClient
REST interface:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.wildfly.quickstarts.microprofile.rest.client.model.Country;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import java.util.concurrent.CompletionStage;
@Path("/")
@RegisterRestClient
public interface CountriesServiceClient {
@GET
@Path("/name/{name}")
@Produces("application/json")
Country getByName(@PathParam("name") String name);
@GET
@Path("/name/{name}")
@Produces("application/json")
CompletionStage<Country> getByNameAsync(@PathParam("name") String name);
}
And add the following method to CountriesResource
:
@GET
@Path("/name-async/{name}")
@Produces(MediaType.APPLICATION_JSON)
public CompletionStage<Country> nameAsync(@PathParam("name") String name) {
return countriesServiceClient.getByNameAsync(name);
}
That’s it. Build and redeploy the application:
$ mvn clean package wildfly:deploy
Now you can access
http://localhost:8080/country-client/country/name-async/France
endpoint using your browser or
curl http://localhost:8080/country-client/country/name-async/France
to get some information about France
. The output will be the same as previously
but the method the call is now executed asynchronously. To verify this, you can add
some code between the getByNameAsync
call and the return of the completion stage:
@GET
@Path("/name-async/{name}")
@Produces(MediaType.APPLICATION_JSON)
public CompletionStage<Country> nameAsync(@PathParam("name") String name) {
CompletionStage<Country> completionStage = countriesServiceClient.getByNameAsync(name);
System.out.println("Async request happening now...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return completionStage;
}
MicroProfile REST Client allows you to register custom JAX-RS providers when you are defining the interface or building the client with the builder. The supported providers which can be registered are:
-
ClientResponseFilter
-
ClientRequestFilter
-
MessageBodyReader
-
MessageBodyWriter
-
ParamConverter
-
ReaderInterceptor
-
WriterInterceptor
-
ResponseExceptionMapper
- specific to the MicroProfile REST Client
To declare a provider you can use:
-
@RegisterProvider
on the REST Client interface -
or
RestClientBuilder#register
As ResponseExceptionMapper
is a custom provider of MicroProfile REST Client,
let’s define one in our application. ResponseExceptionMapper
allows you to
transform received Response
object to a Throwable
that will be thrown at the
place of the client invocation. Let’s say that we don’t want to return error to
users in case they pass in a wrong name of the country. Create a new class
org.wildfly.quickstarts.microprofile.rest.client.NotFoundResponseExceptionMapper
:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
public class NotFoundResponseExceptionMapper implements ResponseExceptionMapper<NotFoundException> {
@Override
public boolean handles(int status, MultivaluedMap<String, Object> headers) {
return status == 404;
}
@Override
public NotFoundException toThrowable(Response response) {
return new NotFoundException(response.hasEntity() ? response.readEntity(String.class) : "Resource not found");
}
}
which is a ResponseExceptionMapper
that transforms HTTP status code 404
to
the NotFoundException
. Now we need to register our provided with the
CountriesServiceClient
:
package org.wildfly.quickstarts.microprofile.rest.client;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.wildfly.quickstarts.microprofile.rest.client.model.Country;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import java.util.concurrent.CompletionStage;
@Path("/")
@RegisterRestClient
@RegisterProvider(NotFoundResponseExceptionMapper.class)
public interface CountriesServiceClient {
@GET
@Path("/name/{name}")
@Produces("application/json")
Country getByName(@PathParam("name") String name);
@GET
@Path("/name/{name}")
@Produces("application/json")
CompletionStage<Country> getByNameAsync(@PathParam("name") String name);
}
And the last step is to catch the exception at the code that is invoking
CountriesServiceClient
. Update CountriesResource#cdiName
method:
@GET
@Path("/cdi/{name}")
@Produces(MediaType.APPLICATION_JSON)
public Country cdiName(@PathParam("name") String name) {
try {
return countriesServiceClient.getByName(name);
} catch (NotFoundException e) {
return null;
}
}
Build and redeploy the application:
$ mvn clean package wildfly:deploy
Now you can access
http://localhost:8080/country-client/country/cdi/doesNotExist
endpoint using your browser or
curl http://localhost:8080/country-client/country/cdi/doesNotExist
the application will return a 204 No Content response instead of the 404 error
received from country-server
. You can verify the country-server
response at
http://localhost:8080/country-server/name/doesNotExist
.
MicroProfile REST Client provides you with an option to define REST clients in a clear, declarative, and intuitive way using the same annotations as for your JAX-RS resources. It also allows you to make the HTTP communication on the background transparent for your services with the direct data conversions and exception mappers. You can find more information about the MicroProfile REST Client specification at https://github.com/eclipse/microprofile-rest-client.