com.kumuluz.ee.rest-client:kumuluzee-rest-client

Implementation of Microprofile Rest Client


Keywords
cloud-native, jakartaee, java, javaee, kumuluzee, microprofile, microservices, rest, rest-client
License
MIT

Documentation

KumuluzEE MicroProfile Rest Client

Build Status

KumuluzEE MicroProfile Rest Client project provides an easy way to define and invoke RESTful services over HTTP.

KumuluzEE MicroProfile Rest Client supports generation of rest clients from simple definitions. APIs are defined using interfaces and well-known JAX-RS annotations. Generated rest clients provide a type-safe way to invoke defined APIs and support a wide variety of providers which allow fine-grained but natural configuration at various stages of requests.

KumuluzEE MicroProfile Rest Client implements MicroProfile Rest Client 1.3.3 API.

Usage

MicroProfile Rest Client can be added via the following Maven dependency:

<dependency>
    <groupId>com.kumuluz.ee.rest-client</groupId>
    <artifactId>kumuluzee-rest-client</artifactId>
    <version>${kumuluzee-rest-client.version}</version>
</dependency>

Defining type-safe API

A type-safe API is defined by creating an interface:

@Path("orders")
@RegisterRestClient
@Dependent
public interface SimpleApi {
	
	@GET
	@Path("{id}")
	Order getById(@PathParam("id") long id);
	
}

Building rest client instance

Defined rest client can be generated programmatically or injected using CDI.

Generating rest client programmatically

Rest client can be built programmatically anywhere in application code.

SimpleApi simpleApi = RestClientBuilder
    .newBuilder()
    .baseUrl(new URL("http://myapi.location.com"))
    .build(SimpleApi.class);

All constructed rest clients implement the Closeable and AutoCloseable interfaces and can thus be manually closed.

Generating rest client with CDI injection

Injection of rest client is supported in CDI beans and offers an approach of generating a rest client with less boilerplate.

@Inject
@RestClient
SimpleApi simpleApi;

If you are using CDI injection to create rest client, you must annotate your interface with @RegisterRestClient. The URL of the api can be supplied with the annotation parameter or using configuration parameter as described below.

Using providers

KumuluzEE Rest Client supports the usage of additional providers, which enable fine-grained control of requests at various stages. Providers are implemented in similar fashion as in JAX-RS specification.

The following providers are supported:

  • ClientRequestFilter - invoked when a request is made.
  • ClientResponseFilter - invoked when a response is received.
  • MessageBodyReader - allows reading the entity from the response.
  • MessageBodyWriter - allows writing the entity to the request.
  • ParamConverter - allows conversion of request/response parameters to and from String.
  • ReaderInterceptor and WriterInterceptor - listeners for when a read/write occurs.
  • ResponseExceptionMapper - maps received Response to a Throwable that is thrown by runtime.
  • AsyncInvocationInterceptorFactory - creates interceptor for manipulating the thread in which asynchronous calls are executed

Provider registration

Providers can be registered either programmatically when building the rest client or with annotations placed on the definition class.

Example of programmatic registration:

SimpleApi simpleApi = RestClientBuilder
    .newBuilder()
    .baseUrl(new URL("http://myapi.location.com"))
    .register(MyProvider.class)
    .build(SimpleApi.class);

Example of registration with annotations:

@Path("orders")
@RegisterRestClient
@RegisterProvider(MyProvider.class)
@Dependent
public interface SimpleApi {
	
	@GET
	@Path("{id}")
	Order getById(@PathParam("id") long id);
	
}

Configuration of rest client definitions

Rest client definitions can be additionally configured using the KumuluzEE configuration. Example configuration for the cdi.api.TodoApi:

kumuluzee:
  rest-client:
    registrations:
      - class: cdi.api.TodoApi
        url: https://jsonplaceholder.typicode.com
        providers: com.example.providers.Provider1,com.example.providers.Provider2
        connect-timeout: 1000
        read-timeout: 5000

The class property identifies the definition of rest client by its class name. The url property defines the base URL for generated rest clients and the providers property is a comma separated list of providers, that are added to the generated rest clients.

Additionally the following configuration keys are supported:

  • connect-timeout - Connection timeout in milliseconds.
  • read-timeout - Read timeout in milliseconds.
  • scope - Fully qualified class name of the desired scope of the rest client.
  • hostname-verifier - Fully qualified class name of the desired implementation of HostnameVerifier.
  • key-store - Location of the client key store. Can point to either a classpath resource (e.g. classpath:/my-keystore.jks) or a file (e.g. file:/home/user/my-keystore.jks).
  • key-store-type - Type of the client key store (JKS by default).
  • key-store-password - Password of the client key store.
  • trust-store - Location of the trust store. Can point to either a classpath resource (e.g. classpath:/my-truststore.jks) or a file (e.g. file:/home/user/my-truststore.jks).
  • trust-store-type - Type of the trust store (JKS by default).
  • trust-store-password - Password of the trust store.

Instead of using fully qualified class names for the configuration a configuration keys can also be used. This is especially useful when multiple client definitions share the same configuration. For example for the following definition:

@RegisterRestClient(configKey="test-client")
public interface TestClient {
  @GET
  Response test();
}

The following configuration can be used (notice that the class key matches the configKey parameter of the @RegisterRestClient annotation):

kumuluzee:
  rest-client:
    registrations:
      - class: test-client
        url: https://my-test-service
        read-timeout: 5000

When using both configuration keys and fully qualified class names for the configuration the fully qualified class name configuration takes precedence.

Making asynchronous requests

In order to make requests asynchronously the method in the API interface should return parameterized type CompletionStage. For example:

@POST
CompletionStage<Void> createCustomerAsynch(Customer customer);

The defined method can then be used to make asynchronous requests. For example:

customerApi.createCustomerAsynch(c)
    .toCompletableFuture().get();

Adding headers on the API interface

Headers can be added to request in multiple ways. You can use the JAX-RS @HeaderParam parameter annotation. For example:

@GET
List<User> getUsers(@HeaderParam("Authorization") String authorization);

If using a parameter for header creation is not desirable using @ClientHeaderParam annotation on API interface or method is also supported. For example:

@POST
@ClientHeaderParam(name="X-Http-Method-Override", value="PUT")
Response sentPUTviaPOST(MyEntity entity);

The value of the @ClientHeaderParam can also use a method reference to generate header values. For example:

@Path("/somePath")
public interface MyClient {
  @POST
  @ClientHeaderParam(name="X-Request-ID", value="{generateRequestId}")
  Response postWithRequestId(MyEntity entity);
  
  @GET
  @ClientHeaderParam(name="CustomHeader", value="{some.pkg.MyHeaderGenerator.generateCustomHeader}", required=false)
  Response getWithoutCustomHeader();
  
  default String generateRequestId() {
    return UUID.randomUUID().toString();
  }
}

The method reference can point to a default method defined in the API or a public static method in a different class. If required parameter is set to true and the generator method throws an exception, the request will fail. If required parameter is set to false and the generator method throws an exception, the request will not fail but (unsuccessfully) generated header will not be sent. Default value is true.

Generator method must have zero arguments or one String argument (name of the header) and should return a String representing the header value or String[] representing multiple header values.

Propagating headers

KumuluzEE Rest Client supports propagation of headers from incoming requests to the outgoing requests. To enable this feature annotate the API interface with the @RegisterClientHeaders annotation. Then specify the headers that should be propagated in the configuration. For example to forward the Authorization header put the following configuration in config.yaml:

kumuluzee:
  rest-client:
    propagate-headers: Authorization

NOTE: Header propagation requires the KumuluzEE Config MicroProfile dependency!

If more control is needed, an implementation of ClientHeadersFactory can be used by registering it in the @RegisterClientHeaders parameter. Example implementation:

public class ExampleHeadersFactory implements ClientHeadersFactory {
    @Override
    public MultivaluedMap<String, String> update(MultivaluedMap<String, String> incomingHeaders,
                                                 MultivaluedMap<String, String> clientOutgoingHeaders) {
        // incomingHeaders - headers from the JAX-RS request
        // clientOutgoingHeaders - headers specified on the API interface
        // should return a map of headers which should actually be sent when making a request
    }
}

Note that this disables the default propagation, header propagation should be handled manually in the factory implementation. This approach also doesn't require the KumuluzEE Config MicroProfile dependency.

Intercepting new client builders

When a new client is being built it can be intercepted with a SPI interface RestClientBuilderListener. This includes the builders that are created in the CDI environment at the start of the application (interfaces annotated with @RegisterRestClient). For example:

public class BuilderListener implements RestClientBuilderListener {

    @Override
    public void onNewBuilder(RestClientBuilder builder) {
        // ...
    }
}

Remember to register the listener in the service file named org.eclipse.microprofile.rest.client.spi.RestClientBuilderListener.

A similar interface supported since 1.2.0 is the RestClientListener interface. The difference between the two is that the RestClientBuilderListener implementations are called when a new builder is created and the RestClientListener implementations are called when the build method is called on the builder. The latter also exposes the service interface class.

Form parameters content type

Rest client will automatically set value of Content-Type header to corresponding values if method contains any of the following arguments:

  • @FormParam: application/x-www-urlencoded
  • @FormDataParam: multipart/form-data

A method cannot contain both type of parameters and will throw IllegalStateException if it does.

Known issues

401 response status throwing ProcessingException

If called service returns status of 401, Jetty's WWWAuthenticationProtocolHandler tries to parse WWW-Authenticate header and if not present, throws ProcessingException, due to which registered ResponseExceptionMappers are not executed.

To prevent this, you can set configuration flag kumuluzee.rest-client.disable-jetty-www-auth to true and handler will be removed from Jetty, allowing you to handle 401 response yourself.

Changelog

Recent changes can be viewed on Github on the Releases Page

Contribute

See the contributing docs

When submitting an issue, please follow the guidelines.

When submitting a bugfix, write a test that exposes the bug and fails before applying your fix. Submit the test alongside the fix.

When submitting a new feature, add tests that cover the feature.

License

MIT