REST Controller

Home

Validation


© 2019-2022 rxmicro.io. Free use of this software is granted under the terms of the Apache License 2.0.

Copies of this entity may be made for Your own use and for distribution to others, provided that You do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

If You find errors or omissions in this entity, please don’t hesitate to submit an issue or open a pull request with a fix.

REST Client is an interface that contains at least one declarative HTTP request handler.

To create REST clients, the RxMicro framework provides the following modules:

Due to transit dependencies only two modules usually need to be added to a project: rxmicro.rest.client.jdk and rxmicro.rest.client.exchange.json.
(All other modules are automatically added to the project!)

1. REST Client Implementation Requirements

1.1. REST Client Interface Requirements

REST Client is a Java interface that annotated by the @RestClient annotation:

import java.util.concurrent.CompletableFuture;

import io.rxmicro.rest.client.RestClient;
import io.rxmicro.rest.method.GET;

@RestClient
public interface RESTClient {

    @GET("/")
    CompletableFuture<Model> getContent();
}

that must meet the following requirements:

  1. The interface must be a public one.

  2. The interface must be annotated by the required @RestClient annotation.

  3. The interface couldn’t extend any other interfaces.

  4. The interface couldn’t be a nested one.

1.2. HTTP Request Handler Requirements

HTTP request handler is a method, that must meet the following requirements:

  1. The method couldn’t be a private one.

  2. The method couldn’t be an abstract one.

  3. The method couldn’t be a static one.

  4. The method must be annotated by only one of the following annotations:

  5. The method must return one of the following reactive types:

  6. If the method returns a reactive type, that this type must be parametrized by a HTTP response model type. (The additional types such as java.lang.Void and java.util.Optional are supported also. (Read more: core.html)).

A REST client can contain static, private and default methods, but such methods couldn’t be HTTP request handlers!

The Flux and Flowable types are developed to handle data stream that may arrive within a certain period of time.

For such cases the HTTP protocol provides the special Transfer-Encoding mechanism.

But this mechanism is not supported by the RxMicro framework yet, so the Flux and Flowable types cannot be used in the HTTP request handler.

The RxMicro framework supports the following parameter types for the HTTP request handler:

  1. Handler without parameters.
    (This type is recommended for the simplest stateless REST clients without parameters.)

  2. List of primitive parameters.
    (This type is recommended for REST clients, the behavior of which depends on 1-2 parameters.)

  3. Custom class modeling an HTTP request.
    (This type is recommended for REST clients, the behavior of which depends on 3 or more parameters.)

When using the Project Reactor and RxJava reactive libraries, You need:

  1. Add dependencies to pom.xml.

  2. Add modules to module-info.java.

Adding dependencies to pom.xml:

<dependencies>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-core</artifactId>
        <version>${projectreactor.version}</version> (1)
    </dependency>
    <dependency>
        <groupId>io.reactivex.rxjava3</groupId>
        <artifactId>rxjava</artifactId>
        <version>${rxjava.version}</version> (2)
    </dependency>
</dependencies>

Adding modules to module-info.java:

module example {
    requires reactor.core; (1)
    requires io.reactivex.rxjava3; (2)
}
1 The reactor.core module when using the Project Reactor library.
2 The io.reactivex.rxjava3 module when using the RxJava library.

2. RxMicro Annotations

The RxMicro framework supports the following RxMicro Annotations, which are used to create and configure REST Clients.

Table 1. Supported RxMicro Annotations.
Annotation Description

@RestClient

Denotes that an interface is a dynamic generated REST client.

@GET

Denotes a method, that must handle a GET HTTP request.

@POST

Denotes a method, that must handle a POST HTTP request.

@PUT

Denotes a method, that must handle a PUT HTTP request.

@DELETE

Denotes a method, that must handle a DELETE HTTP request.

@PATCH

Denotes a method, that must handle a PATCH HTTP request.

@HEAD

Denotes a method, that must handle a HEAD HTTP request.

@OPTIONS

Denotes a method, that must handle a OPTIONS HTTP request.

@BaseUrlPath

Denotes a base URL path for the all request handlers at the REST client.

@Version

Denotes a version of the REST client.

@Header

Denotes that a field of Java model class is a HTTP header.

@HeaderMappingStrategy

Declares a strategy of header name formation based on Java model field name analysis.

(By default, the CAPITALIZE_WITH_HYPHEN strategy is used. Thus, by using this strategy, the Header-Name name header corresponds to the headerName field name.)

@AddHeader

Denotes a static HTTP header that must be added to the request, created by REST client implementation.

@SetHeader

Denotes a static HTTP header that must be set to the request, created by REST client implementation.

@RepeatHeader

Denotes the header, which name needs to be repeated for each element in the list.

(This annotation applies only to fields with the java.util.List<?> type.)

@Parameter

Denotes that a field of Java model class is a HTTP parameter.

@ParameterMappingStrategy

Declares a strategy of parameter name formation based on Java model field name analysis.

(By default, the LOWERCASE_WITH_UNDERSCORED strategy is used. Thus, by using this strategy, the header_name name header corresponds to the headerName field name.)

@AddQueryParameter

Denotes a static query parameter that must be added to the request, created by REST client implementation.

@SetQueryParameter

Denotes a static query parameter that must be set to the request, created by REST client implementation.

@RepeatQueryParameter

Denotes the query parameter, which name needs to be repeated for each element in the list.

(This annotation applies only to fields with the java.util.List<?> type.)

@PathVariable

Denotes that a field of Java model class is a path variable.

@RequestId

Declares the Java model field as a field, that the RxMicro framework must used as a unique request ID and sent it to the server.

@ResponseStatusCode

Indicates to the RxMicro framework that the value of the Java model field should be used as a status code that received from the server.

@ResponseBody

Indicates to the RxMicro framework that the value of the Java model field should be used as a body that received from the server.

@PartialImplementation

Denotes an abstract class that contains a partial implementation of the annotated by this annotation REST client interface.

@RestClientGeneratorConfig

Allows to configure the process of code generation by the RxMicro Annotation Processor for REST clients.

3. HTTP Request Handler Return Types

The HTTP request handler supports two categories of returned results:

  • HTTP response without body;

  • HTTP response with body;

3.1. Supported Return Result Types for HTTP Response without Body

The RxMicro framework supports the following return result types for an HTTP response without body:

@RestClient
public interface RestClientWithoutBody {

    (1)

    @GET("/jse/completedFuture1")
    CompletableFuture<Void> completedFuture1();

    @GET("/jse/completedFuture2")
    CompletableFuture<Void> completedFuture2(final Request request);

    @GET("/jse/completedFuture3")
    CompletableFuture<Void> completedFuture3(final String requestParameter);

    (2)

    @GET("/jse/completionStage1")
    CompletionStage<Void> completionStage1();

    @GET("/jse/completionStage2")
    CompletionStage<Void> completionStage2(final Request request);

    @GET("/jse/completionStage3")
    CompletionStage<Void> completionStage3(final String requestParameter);

    (3)

    @GET("/spring-reactor/mono1")
    Mono<Void> mono1();

    @GET("/spring-reactor/mono2")
    Mono<Void> mono2(final Request request);

    @GET("/spring-reactor/mono3")
    Mono<Void> mono3(final String requestParameter);

    (4)

    @GET("/rxjava3/completable1")
    Completable completable1();

    @GET("/rxjava3/completable2")
    Completable completable2(final Request request);

    @GET("/rxjava3/completable3")
    Completable completable3(final String requestParameter);
}
1 The CompletableFuture<Void> type is recommended when using the java.util.concurrent library.
2 Instead of the CompletableFuture<Void> type, can also be used the CompletionStage<Void> type.
3 When using the Project Reactor library, only the Mono<Void> type can be used.
4 When using the RxJava library, only the Completable type can be used.

All the above mentioned HTTP request handlers shouldn’t throw any exceptions:

@InitMocks
@RxMicroComponentTest(RestClientWithoutBody.class)
final class RestClientWithoutBodyTest {

    private RestClientWithoutBody restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    static Stream<Consumer<RestClientWithoutBody>> clientMethodsProvider() {
        return Stream.of(
                client -> client.completedFuture1().join(),
                client -> client.completedFuture2(new Request("param")).join(),
                client -> client.completedFuture3("param").join(),

                client -> client.completionStage1().toCompletableFuture().join(),
                client -> client.completionStage2(
                        new Request("param")).toCompletableFuture().join(),
                client -> client.completionStage3("param").toCompletableFuture().join(),

                client -> client.mono1().block(),
                client -> client.mono2(new Request("param")).block(),
                client -> client.mono3("param").block(),

                client -> client.completable1().blockingAwait(),
                client -> client.completable2(new Request("param")).blockingAwait(),
                client -> client.completable3("param").blockingAwait()
        );
    }

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder().setAnyRequest().build(),
                true
        );
    }

    @ParameterizedTest
    @MethodSource("clientMethodsProvider")
    @BeforeThisTest(method = "prepare")
    void Should_be_invoked_successfully(final Consumer<RestClientWithoutBody> clientMethod) {
        assertDoesNotThrow(() -> clientMethod.accept(restClient));
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

3.2. Supported Return Result Types for HTTP Response with Body

The RxMicro framework supports the following return result types for an HTTP response with body:

@RestClient
public interface RestClientWithBody {

    (1)

    @GET("/jse/completedFuture1")
    CompletableFuture<Response> completedFuture1();

    @GET("/jse/completedFuture2")
    CompletableFuture<Response> completedFuture2(final Request request);

    @GET("/jse/completedFuture3")
    CompletableFuture<Response> completedFuture3(final String requestParameter);

    (2)

    @GET("/jse/completionStage1")
    CompletionStage<Response> completionStage1();

    @GET("/jse/completionStage2")
    CompletionStage<Response> completionStage2(final Request request);

    @GET("/jse/completionStage3")
    CompletionStage<Response> completionStage3(final String requestParameter);

    (3)

    @GET("/spring-reactor/mono1")
    Mono<Response> mono1();

    @GET("/spring-reactor/mono2")
    Mono<Response> mono2(final Request request);

    @GET("/spring-reactor/mono3")
    Mono<Response> mono3(final String requestParameter);

    (4)

    @GET("/rxjava3/single1")
    Single<Response> single1();

    @GET("/rxjava3/single2")
    Single<Response> single2(final Request request);

    @GET("/rxjava3/single3")
    Single<Response> single3(final String requestParameter);
}
1 The CompletableFuture<MODEL> type is recommended when using the java.util.concurrent library.
2 Instead of the CompletableFuture<MODEL> type, can also be used the CompletionStage<MODEL> type.
3 When using the Project Reactor library, only the Mono<MODEL> type can be used.
4 When using the RxJava library, only the Single<MODEL> type can be used.

Note that the reactive types must be parameterized by the HTTP response model class!

All of the above mentioned handlers return the Response class instance with the Hello World! message:

@InitMocks
@RxMicroComponentTest(RestClientWithBody.class)
final class RestClientWithBodyTest {

    private RestClientWithBody restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    static Stream<Function<RestClientWithBody, Response>> clientMethodsProvider() {
        return Stream.of(
                client -> client.completedFuture1().join(),
                client -> client.completedFuture2(new Request("param")).join(),
                client -> client.completedFuture3("param").join(),

                client -> client.completionStage1().toCompletableFuture().join(),
                client -> client.completionStage2(
                        new Request("param")).toCompletableFuture().join(),
                client -> client.completionStage3("param").toCompletableFuture().join(),

                client -> client.mono1().block(),
                client -> client.mono2(new Request("param")).block(),
                client -> client.mono3("param").block(),

                client -> client.single1().blockingGet(),
                client -> client.single2(new Request("param")).blockingGet(),
                client -> client.single3("param").blockingGet()
        );
    }

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder().setAnyRequest().build(),
                jsonObject("message", "Hello World!"),
                true
        );
    }

    @ParameterizedTest
    @MethodSource("clientMethodsProvider")
    @BeforeThisTest(method = "prepare")
    void Should_return_message_Hello_World(final Function<RestClientWithBody, Response> clientMethod) {
        final Response response = assertDoesNotThrow(() -> clientMethod.apply(restClient));

        assertEquals("Hello World!", response.getMessage());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

4. HTTP Headers Handling

4.1. Basic Rules

The RxMicro framework supports HTTP headers in request and response models:

(1)
@HeaderMappingStrategy
public final class Request {

    (2)
    @Header
    String endpointVersion;

    (3)
    @Header("UseProxy")
    Boolean useProxy;

    public String getEndpointVersion() {
        return endpointVersion;
    }

    public Boolean getUseProxy() {
        return useProxy;
    }
}
(1)
@HeaderMappingStrategy
public final class Response {

    (2)
    @Header
    final String endpointVersion;

    (3)
    @Header("UseProxy")
    final Boolean useProxy;

    public Response(final Request request) {
        this.endpointVersion = request.getEndpointVersion();
        this.useProxy = request.getUseProxy();
    }

    public Response(final String endpointVersion,
                    final Boolean useProxy) {
        this.endpointVersion = endpointVersion;
        this.useProxy = useProxy;
    }
}
1 The @HeaderMappingStrategy annotation allows setting common conversion rules for all header names from HTTP to Java format and vice versa in the current model class.
(By default, the CAPITALIZE_WITH_HYPHEN strategy is used. The field name is used as the basic name, and then, following the rules of the specified strategy, the HTTP header name is generated.)
2 In order to declare a model field as the HTTP header field, it is necessary to use the @Header annotation.
3 Using the @Header annotation, it is possible to specify the HTTP header name, which does not correspond to the used strategy declared by the @HeaderMappingStrategy annotation.

The RxMicro framework uses the following algorithm to define the HTTP header name for the specified model field:

  1. If the field is annotated by the @Header annotation with an explicit indication of the HTTP header name, the specified name is used;

  2. If no HTTP header name is specified in the @Header annotation, the RxMicro framework checks for the @HeaderMappingStrategy annotation above the model class;

  3. If the model class is annotated by the @HeaderMappingStrategy annotation, then the specified naming strategy is used.
    (The field name is used as the basic name, and then, following the rules of the specified strategy, the HTTP header name is generated.)

  4. If the @HeaderMappingStrategy annotation is missing, the model class field name is used as the HTTP header name.

After creating model classes, it is necessary to create a request handler that uses the following models:

@RestClient
public interface SimpleUsageRestClient {

    @GET("/get1")
    CompletableFuture<Response> get1(final Request request); (1)


    (2)
    @GET("/get2")
    (3)
    @HeaderMappingStrategy
    CompletableFuture<Response> get2(final @Header String endpointVersion,        (4)
                                     final @Header("UseProxy") Boolean useProxy); (5)
}
1 If a separate model class has been created for an HTTP request, then this class must be passed by the method parameter that handles the HTTP request.
2 Besides supporting HTTP request model classes, the RxMicro framework supports request handlers that accept HTTP headers as method parameters.
3 To define a common naming strategy for all HTTP headers which are passed as method parameters, the request handler must be annotated by the @HeaderMappingStrategy annotation.
4 To declare a method parameter as an HTTP header field, use the @Header annotation.
5 Using the @Header annotation, it is possible to specify an HTTP header name that does not correspond to the strategy used, declared by the @HeaderMappingStrategy annotation.

The RxMicro framework recommends for request handlers that depend on 3 or more HTTP headers to create separate classes of request model.

Upon implementation of HTTP headers directly into the handler, the code becomes hard to read!

Despite the different approaches to HTTP header handling support, as to the message generated by the HTTP protocol, the two above-mentioned handlers are absolutely equal:

@InitMocks
@RxMicroComponentTest(SimpleUsageRestClient.class)
final class SimpleUsageRestClientTest {

    private static final String ENDPOINT_VERSION = "v1";

    private static final Boolean USE_PROXY = true;

    private SimpleUsageRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    static Stream<Function<SimpleUsageRestClient, Response>> restClientExecutableProvider() {
        return Stream.of(
                client -> client.get1(new Request(ENDPOINT_VERSION, USE_PROXY)).join(),
                client -> client.get2(ENDPOINT_VERSION, USE_PROXY).join()
        );
    }

    private void prepare() {
        final HttpHeaders headers = HttpHeaders.of(
                "Endpoint-Version", ENDPOINT_VERSION, (2)
                "UseProxy", USE_PROXY                 (3)
        );
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setAnyPath()
                        .setHeaders(headers)
                        .build(),
                new HttpResponseMock.Builder()
                        .setHeaders(headers)
                        .build(),
                true
        );
    }

    @ParameterizedTest
    @MethodSource("restClientExecutableProvider")
    @BeforeThisTest(method = "prepare")
    void Should_process_HTTP_headers(
            final Function<SimpleUsageRestClient, Response> clientMethod) {
        final Response response = assertDoesNotThrow(() ->
                clientMethod.apply(restClient)); (1)

        assertEquals(ENDPOINT_VERSION, response.getEndpointVersion()); (4)
        assertEquals(USE_PROXY, response.getUseProxy());               (4)
    }
}
1 When performing a request to different URL Paths, the result is the same.
2 The Endpoint-Version name corresponds to the endpointVersion field of the request model.
(This correspondence is formed basing on the default strategy use, which is defined by the @HeaderMappingStrategy annotation.)
3 The UseProxy name corresponds to the useProxy field of the request model, since this name is specified in the @Header annotation.
4 After executing the request in the resulting HTTP response, the Endpoint-Version and UseProxy headers are equal to v1 and true respectively. (The handler returns the same header values it received from an HTTP request.)

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

4.2. Supported Data Types

The RxMicro framework supports the following Java types, which can be HTTP request model headers:

  • ? extends Enum<?>;

  • java.lang.Boolean;

  • java.lang.Byte;

  • java.lang.Short;

  • java.lang.Integer;

  • java.lang.Long;

  • java.math.BigInteger;

  • java.lang.Float;

  • java.lang.Double;

  • java.math.BigDecimal;

  • java.lang.Character;

  • java.lang.String;

  • java.time.Instant;

For floating point numbers, it is suggested to use the java.math.BigDecimal type instead of java.lang.Float or java.lang.Double.

Using the java.math.BigDecimal allows excluding errors of decimal number notation. This is particularly important if the decimal number is used to represent currency-related data!

and also the java.util.List<T>, java.util.Set<T> and the java.util.Map<String, T> type is parameterized by any of the above mentioned primitive types.

If the java.util.Map<String, T> type is used, then REST model can contain dynamic properties, where keys are the names of properties and values are the properties values.

4.3. Static HTTP Headers

For static header installation, the RxMicro framework provides the @AddHeader and @SetHeader annotations:

@RestClient
(1)
@SetHeader(name = "Mode", value = "Demo")
public interface StaticHeadersRestClient {

    @GET("/get1")
    CompletableFuture<Void> get1();

    @GET("/get2")
    (2)
    @SetHeader(name = "Debug", value = "true")
    CompletableFuture<Void> get2();
}
1 If the HTTP header is set for the REST client, it is added to the HTTP requests for each handler.
2 If the HTTP header is set for the handler, it is added to the HTTP requests only for that handler.

In terms of the HTTP protocol, static headers do not differ from any others:

@InitMocks
@RxMicroComponentTest(StaticHeadersRestClient.class)
final class StaticHeadersRestClientTest {

    private StaticHeadersRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepareParentHeaderOnly() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setPath("/get1")
                        .setHeaders(HttpHeaders.of(
                                "Mode", "Demo"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepareParentHeaderOnly")
    void Should_use_parent_header_only() {
        assertDoesNotThrow(() -> restClient.get1().join());
    }

    private void prepareParentAndChildHeaders() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setPath("/get2")
                        .setHeaders(HttpHeaders.of(
                                "Mode", "Demo",
                                "Debug", "true"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepareParentAndChildHeaders")
    void Should_use_parent_and_child_headers() {
        assertDoesNotThrow(() -> restClient.get2().join());
    }
}

In order to understand the differences between the @AddHeader and @SetHeader annotations, check out the following example:

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

4.4. Repeating HTTP Headers

If the HTTP header of an HTTP request is a list of values, the list elements are transferred by default via the HTTP protocol as a string separated by the | symbol.

If You want the HTTP header to be repeated for each value, You need to use the @RepeatHeader annotation:

@RestClient
public interface RepeatingHeadersRestClient {

    @PUT("/")
    @HeaderMappingStrategy
    CompletableFuture<Void> put(@Header List<Status> singleHeader,
                                @Header @RepeatHeader List<Status> repeatingHeader);
}

The use of the @RepeatHeader annotation is supported for request models only!

For response models it makes no sense, because the RxMicro framework converts any of the supported formats into a list of values.

As a result of converting the Java model to HTTP response, the result will be as follows:

@InitMocks
@RxMicroComponentTest(RepeatingHeadersRestClient.class)
final class RepeatingHeadersRestClientTest {

    private RepeatingHeadersRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(PUT)
                        .setPath("/")
                        .setHeaders(HttpHeaders.of(
                                "Single-Header", "created|approved|rejected", (1)
                                "Repeating-Header", "created",
                                "Repeating-Header", "approved",               (2)
                                "Repeating-Header", "rejected"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_support_repeating_headers() {
        final List<Status> headers = List.of(created, approved, rejected);
        assertDoesNotThrow(() -> restClient.put(headers, headers).join());
    }
}
1 By default, HTTP header list elements are transferred via the HTTP protocol as a string separated by the | symbol.
2 If the field is annotated by the @RepeatHeader annotation, then the header is repeated for each element of the list.

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

5. HTTP Parameters Handling

5.1. Basic Rules

The RxMicro framework supports HTTP parameters in request and response models:

(1)
@ParameterMappingStrategy
public final class Request {

    (2)
    String endpointVersion;

    (3)
    @Parameter("use-Proxy")
    Boolean useProxy;

    public String getEndpointVersion() {
        return endpointVersion;
    }

    public Boolean getUseProxy() {
        return useProxy;
    }
}
(1)
@ParameterMappingStrategy
public final class Response {

    (2)
    final String endpointVersion;

    (3)
    @Parameter("use-Proxy")
    final Boolean useProxy;

    public Response(final Request request) {
        this.endpointVersion = request.getEndpointVersion();
        this.useProxy = request.getUseProxy();
    }

    public Response(final String endpointVersion,
                    final Boolean useProxy) {
        this.endpointVersion = endpointVersion;
        this.useProxy = useProxy;
    }
}
1 The @ParameterMappingStrategy annotation allows setting common conversion rules for all parameter names from HTTP to Java format and vice versa in the current model class. (By default, the LOWERCASE_WITH_UNDERSCORED strategy is used. The field name is used as the basic name, and then, following the rules of the specified strategy, the HTTP parameter name is generated.)
2 In order to declare a model field as the HTTP parameter field, it is necessary to use the @Parameter annotation. (Unlike the @Header annotation, the @Parameter annotation is optional. Thus, if the model field is not annotated by any annotation, then by default it is assumed that there is the @Parameter annotation above the model field.)
3 Using the @Parameter annotation, it is possible to specify the HTTP parameter name, which does not correspond to the used strategy declared by the @ParameterMappingStrategy annotation.

The RxMicro framework uses the following algorithm to define the HTTP parameter name for the specified model field:

  1. If the field is annotated by the @Parameter annotation with an explicit indication of the HTTP parameter name, the specified name is used;

  2. If no HTTP parameter name is specified in the @Parameter annotation, the RxMicro framework checks for the @ParameterMappingStrategy annotation above the model class;

  3. If the model class is annotated by the @ParameterMappingStrategy annotation, then the specified naming strategy is used. (The field name is used as the basic name, and then, following the rules of the specified strategy, the HTTP parameter name is generated.)

  4. If the @ParameterMappingStrategy annotation is missing, the model class field name is used as the HTTP parameter name.

Unlike the @Header annotation, the @Parameter annotation is optional!

Thus, if the model field is not annotated by any annotation, then by default it is assumed that there is the @Parameter annotation above the model field!

After creating model classes, it is necessary to create an HTTP request handler that uses the following models:

@RestClient
public interface SimpleUsageRestClient {

    @GET("/get1")
    CompletableFuture<Response> get1(final Request request); (1)

    (2)
    @GET("/get2")
    (3)
    @ParameterMappingStrategy
    CompletableFuture<Response> get2(final String endpointVersion,        (4)
                                     final @Parameter("use-Proxy") Boolean useProxy); (5)
}
1 If a separate model class has been created for an HTTP request, then this class must be passed by the method parameter that handles the HTTP request.
2 Besides supporting HTTP request model classes, the RxMicro framework supports request handlers that accept HTTP request parameters as method parameters.
3 To define a common naming strategy for all HTTP parameters which are passed as method parameters, the request handler must be annotated by the @ParameterMappingStrategy annotation.
4 To declare a method parameter as an HTTP parameter field, You do not need to use any additional annotations.
5 Using the @Parameter annotation, it is possible to specify an HTTP parameter name that does not correspond to the strategy used, declared by the @ParameterMappingStrategy annotation.

Note that the RxMicro framework does not distinguish whether HTTP request parameters will be transferred to the handler using the start line or HTTP body!

The RxMicro framework recommends for request handlers that depend on 3 or more HTTP parameters to create separate classes of request model.

Upon implementation of HTTP parameters directly into the handler, the code becomes hard to read!

Despite the different approaches to HTTP parameter handling support, as to the message generated by the HTTP protocol, the two above-mentioned handlers are absolutely equal:

@InitMocks
@RxMicroComponentTest(SimpleUsageRestClient.class)
final class SimpleUsageRestClientTest {

    private static final String ENDPOINT_VERSION = "v1";

    private static final Boolean USE_PROXY = true;

    private static final QueryParams QUERY_PARAMETERS = QueryParams.of(
            "endpoint_version", ENDPOINT_VERSION,
            "use-Proxy", USE_PROXY
    );

    private static final Object BODY = jsonObject(
            "endpoint_version", ENDPOINT_VERSION,
            "use-Proxy", USE_PROXY
    );

    private SimpleUsageRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare1() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.GET)
                        .setPath("/get1")
                        .setQueryParameters(QUERY_PARAMETERS)
                        .build(),
                BODY,
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare1")
    void Should_use_handler_with_request_class() {
        final Response response =
                assertDoesNotThrow(() ->
                        restClient.get1(new Request(ENDPOINT_VERSION, USE_PROXY)).join());

        assertEquals(ENDPOINT_VERSION, response.getEndpointVersion());
        assertEquals(USE_PROXY, response.getUseProxy());
    }

    private void prepare2() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.GET)
                        .setPath("/get2")
                        .setQueryParameters(QUERY_PARAMETERS)
                        .build(),
                BODY,
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare2")
    void Should_use_handler_with_request_params() {
        final Response response =
                assertDoesNotThrow(() ->
                        restClient.get2(ENDPOINT_VERSION, USE_PROXY).join());

        assertEquals(ENDPOINT_VERSION, response.getEndpointVersion());
        assertEquals(USE_PROXY, response.getUseProxy());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

5.2. Supported Data Types

The RxMicro framework supports the following Java types, which can be HTTP request model parameters:

  • ? extends Enum<?>;

  • java.lang.Boolean;

  • java.lang.Byte;

  • java.lang.Short;

  • java.lang.Integer;

  • java.lang.Long;

  • java.math.BigInteger;

  • java.lang.Float;

  • java.lang.Double;

  • java.math.BigDecimal;

  • java.lang.Character;

  • java.lang.String;

  • java.time.Instant;

  • ? extends Object;

  • java.util.List<? extends Object>;

  • java.util.Set<? extends Object>;

  • java.util.Map<java.lang.String, ? extends Object>;

For floating point numbers, it is suggested to use the java.math.BigDecimal type instead of java.lang.Float or java.lang.Double.

Using the java.math.BigDecimal allows excluding errors of decimal number notation. This is particularly important if the decimal number is used to represent currency-related data!

and also the java.util.List<T>, java.util.Set<T> and the java.util.Map<String, T> type is parameterized by any of the above mentioned primitive types.

If the java.util.Map<String, T> type is used, then REST model can contain dynamic properties, where keys are the names of properties and values are the properties values.

5.3. Complex Model Support

Unlike HTTP headers, HTTP parameters can be transferred in the request body.

Therefore, besides primitives the RxMicro framework also supports nested JSON objects and arrays.

Therefore, the list of supported types includes the following types:

  • ? extends Object - custom class of the nested Java model;

  • java.util.List<? extends Object> - list of the nested Java model custom classes;

  • java.util.Set<? extends Object> - set of the nested Java model custom classes;

  • java.util.Map<String, ? extends Object> - map of the nested Java model custom classes;

Nested Model Example:

@ParameterMappingStrategy
public final class NestedModel {

    String stringParameter;

    BigDecimal bigDecimalParameter;

    Instant instantParameter;

    public NestedModel(final String stringParameter,
                       final BigDecimal bigDecimalParameter,
                       final Instant instantParameter) {
        this.stringParameter = stringParameter;
        this.bigDecimalParameter = bigDecimalParameter;
        this.instantParameter = instantParameter;
    }

    public NestedModel() {
    }

    @Override
    public int hashCode() {
        return Objects.hash(stringParameter, bigDecimalParameter, instantParameter);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final NestedModel that = (NestedModel) o;
        return stringParameter.equals(that.stringParameter) &&
                bigDecimalParameter.equals(that.bigDecimalParameter) &&
                instantParameter.equals(that.instantParameter);
    }
}

As well as examples of more complex Java models that use a nested model:

@ParameterMappingStrategy
public final class ComplexRequest {

    final Integer integerParameter; (1)

    final Status enumParameter; (1)

    final List<Status> enumsParameter; (2)

    final NestedModel nestedModelParameter; (3)

    final List<NestedModel> nestedModelsParameter; (4)

    public ComplexRequest(final Integer integerParameter,
                          final Status enumParameter,
                          final List<Status> enumsParameter,
                          final NestedModel nestedModelParameter,
                          final List<NestedModel> nestedModelsParameter) {
        this.integerParameter = integerParameter;
        this.enumParameter = enumParameter;
        this.enumsParameter = enumsParameter;
        this.nestedModelParameter = nestedModelParameter;
        this.nestedModelsParameter = nestedModelsParameter;
    }
}
@ParameterMappingStrategy
public final class ComplexResponse {

    Integer integerParameter; (1)

    Status enumParameter; (1)

    List<Status> enumsParameter; (2)

    NestedModel nestedModelParameter; (3)

    List<NestedModel> nestedModelsParameter; (4)

    public Integer getIntegerParameter() {
        return integerParameter;
    }

    public Status getEnumParameter() {
        return enumParameter;
    }

    public List<Status> getEnumsParameter() {
        return enumsParameter;
    }

    public NestedModel getNestedModelParameter() {
        return nestedModelParameter;
    }

    public List<NestedModel> getNestedModelsParameter() {
        return nestedModelsParameter;
    }
}
1 Primitive JSON type field.
2 Primitive JSON array type.
3 Nested JSON object.
4 JSON array of JSON nested objects.

After creating model classes, it is necessary to create a request handler that uses the following models:

@RestClient
public interface ComplexModelRestClient {

    @POST("/")
    CompletableFuture<ComplexResponse> post(ComplexRequest request);
}

Since the request is transferred in an HTTP body, the sent and received JSON objects must be the same:

@InitMocks
@RxMicroComponentTest(ComplexModelRestClient.class)
final class ComplexModelRestClientTest {

    private final Integer integerParameter = 1;

    private final Status enumParameter = created;

    private final List<Status> enumsParameter = List.of(created, approved);

    private final NestedModel nestedModelParameter =
            new NestedModel("test", new BigDecimal("3.14"), Instant.now());

    private final List<NestedModel> nestedModelsParameter = List.of(
            new NestedModel("test1", new BigDecimal("1.1"), Instant.now()),
            new NestedModel("test2", new BigDecimal("1.2"), Instant.now())
    );

    private ComplexModelRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(POST)
                        .setPath("/")
                        .setAnyBody()
                        .build(),
                new HttpResponseMock.Builder()
                        .setReturnRequestBody()
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_support_complex_requests_and_responses() {
        final ComplexResponse response = assertDoesNotThrow(() ->
                restClient.post(new ComplexRequest(
                        integerParameter,
                        enumParameter,
                        enumsParameter,
                        nestedModelParameter,
                        nestedModelsParameter
                )).join());

        assertEquals(integerParameter, response.getIntegerParameter());
        assertEquals(enumParameter, response.getEnumParameter());
        assertEquals(enumsParameter, response.getEnumsParameter());
        assertEquals(nestedModelParameter, response.getNestedModelParameter());
        assertEquals(nestedModelsParameter, response.getNestedModelsParameter());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

5.4. Static Query Parameters

For setting static query parameters, the RxMicro framework provides the @AddQueryParameter and @SetQueryParameter annotations:

@RestClient
(1)
@SetQueryParameter(name = "mode", value = "demo")
public interface StaticQueryParametersRestClient {

    @GET("/get1")
    CompletableFuture<Void> get1();

    @GET("/get2")
    (2)
    @SetQueryParameter(name = "debug", value = "true")
    CompletableFuture<Void> get2();
}
1 If the static query parameter is set for the REST client, it is added to the HTTP requests for each handler.
2 If the static query parameter is set for the handler, it is added to the HTTP requests only for that handler.

In terms of the HTTP protocol, static query parameters do not differ from any standard parameter:

@InitMocks
@RxMicroComponentTest(StaticQueryParametersRestClient.class)
final class StaticQueryParametersRestClientTest {

    private StaticQueryParametersRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepareParentParamOnly() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setPath("/get1")
                        .setQueryParameters(QueryParams.of(
                                "mode", "demo"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepareParentParamOnly")
    void Should_use_parent_param_only() {
        assertDoesNotThrow(() -> restClient.get1().join());
    }

    private void prepareParentAndChildParams() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setPath("/get2")
                        .setQueryParameters(QueryParams.of(
                                "debug", true,
                                "mode", "demo"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepareParentAndChildParams")
    void Should_use_parent_and_child_params() {
        assertDoesNotThrow(() -> restClient.get2().join());
    }
}

In order to understand the differences between the @AddQueryParameter and @SetQueryParameter annotations, check out the following example:

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

5.5. Repeating Query Parameters

If the static query parameter of an HTTP request is a list of values, the list elements are transferred by default via the HTTP protocol as a string separated by the | symbol.

If You want the static query parameter to be repeated for each value, You need to use the @RepeatQueryParameter annotation:

@RestClient
public interface RepeatingQueryParamsRestClient {

    @PUT(value = "/", httpBody = false)
    @ParameterMappingStrategy
    CompletableFuture<Void> put(List<Status> singleHeader,
                                @RepeatQueryParameter List<Status> repeatingHeader);
}

As a result of converting the Java model to HTTP response, the result will be as follows:

@InitMocks
@RxMicroComponentTest(RepeatingQueryParamsRestClient.class)
final class RepeatingQueryParamsRestClientTest {

    private RepeatingQueryParamsRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(PUT)
                        .setPath("/")
                        .setQueryParameters(QueryParams.of(
                                "single_header", "created|approved|rejected", (1)
                                "repeating_header", "created",
                                "repeating_header", "approved",               (2)
                                "repeating_header", "rejected"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_support_repeating_headers() {
        final List<Status> headers = List.of(created, approved, rejected);
        assertDoesNotThrow(() -> restClient.put(headers, headers).join());
    }
}
1 By default, static query parameter list elements are transferred via the HTTP protocol as a string separated by the | symbol.
2 If the field is annotated by the @RepeatQueryParameter annotation, then the static query parameter is repeated for each element of the list.

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

6. The path variables Support

6.1. Basic Rules

The RxMicro framework supports path variables in request models:

public final class Request {

    (1)
    @PathVariable
    String category;

    (2)
    @PathVariable("class")
    String type;

    @PathVariable
    String subType;

    public String getCategory() {
        return category;
    }

    public String getType() {
        return type;
    }

    public String getSubType() {
        return subType;
    }
}
1 In order to declare a model field as the path variable, it is necessary to use the @PathVariable annotation.
2 Using the @PathVariable annotation, it is possible to specify the path variable name.
(If no name is specified, the model field name is used as the path variable name.)

Unlike HTTP headers and parameters that are available also on the client side, path variables are available only within the HTTP request handlers.

So for simplicity it is recommended to always use the model field name as the path variable name!

After creating model classes, it is necessary to create an HTTP request handler that uses the following model:

@RestClient
public interface RESTClient {

    (1)
    @GET("/${category}/${class}-${subType}")
    CompletableFuture<Void> consume(final Request request);

    (1)
    @GET("/${category}/${class}-${subType}")
    CompletableFuture<Void> consume(@PathVariable String category,       (2)
                                    @PathVariable("class") String type,  (3)
                                    @PathVariable String subType);
}
1 When using path variables in request models, be sure to use all path variables in the URL Path. (Based on the analysis of the URL Path, and considering all path variables, the RxMicro framework generates the final URL to which the HTTP request will be sent.)
2 In order to declare a method parameter as path variable, You must use the @PathVariable annotation.
3 Using the @PathVariable annotation it is possible to specify the path variable name, which does not match the name of the method parameter.

The RxMicro framework recommends for request handlers that depend on 3 or more path variables to create separate classes of request model.

Upon implementation of path variables directly into the handler, the code becomes hard to read!

Despite the different approaches to path variables handling support, in terms of the final URL the two above-mentioned handlers are absolutely equal:

@InitMocks
@RxMicroComponentTest(RESTClient.class)
final class RESTClientTest {

    private RESTClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    static Stream<Consumer<RESTClient>> clientMethodsProvider() {
        return Stream.of(
                client -> client.consume(new Request("CATEGORY", "TYPE", "SUB-TYPE")).join(),
                client -> client.consume("CATEGORY", "TYPE", "SUB-TYPE").join()
        );
    }

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.GET)
                        .setPath("/CATEGORY/TYPE-SUB-TYPE") (1)
                        .build(),
                true
        );
    }

    @ParameterizedTest
    @MethodSource("clientMethodsProvider")
    @BeforeThisTest(method = "prepare")
    void Should_return_message_Hello_World(final Consumer<RESTClient> clientMethod) {
        assertDoesNotThrow(() -> clientMethod.accept(restClient));
    }
}
1 As a result of the HTTP request handler execution, the RxMicro framework generates the same URL: /CATEGORY/TYPE-SUB-TYPE

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

6.2. Supported Data Types

The RxMicro framework supports the following Java types, which can be path variables of the request model:

  • ? extends Enum<?>;

  • java.lang.Boolean;

  • java.lang.Byte;

  • java.lang.Short;

  • java.lang.Integer;

  • java.lang.Long;

  • java.math.BigInteger;

  • java.lang.Float;

  • java.lang.Double;

  • java.math.BigDecimal;

  • java.lang.Character;

  • java.lang.String;

  • java.time.Instant;

For floating point numbers, it is suggested to use the java.math.BigDecimal type instead of java.lang.Float or java.lang.Double.

Using the java.math.BigDecimal allows excluding errors of decimal number notation. This is particularly important if the decimal number is used to represent currency-related data!

7. Support of Internal Data Types

7.1. Basic Rules

The RxMicro framework works with internal data. Due to this, the developer can extract additional information about the HTTP response.

To work with internal data, it is necessary to create a response model:

public final class Response {

    (1)
    @ResponseStatusCode
    Integer status;

    (2)
    HttpVersion version;

    (3)
    HttpHeaders headers;

    (4)
    @ResponseBody
    byte[] body;

    public Integer getStatus() {
        return status;
    }

    public HttpVersion getVersion() {
        return version;
    }

    public HttpHeaders getHeaders() {
        return headers;
    }

    public byte[] getBody() {
        return body;
    }
}
1 To get the HTTP status code, it is necessary to use the @ResponseStatusCode annotation.
2 To get the HTTP protocol version, it is necessary to use the HttpVersion type.
(Currently only 1.0 and 1.1 HTTP versions are supported.)
3 To get all HTTP headers, it is necessary to use the HttpHeaders type.
4 To get the response body content as a byte array, it is necessary to use the @ResponseBody annotation.

After creating model classes, it is necessary to create a REST client method that uses this model:

@RestClient
public interface RESTClient {

    @GET("/")
    CompletableFuture<Response> get();
}

After creating the REST client, let’s check that everything works:

@InitMocks
@RxMicroComponentTest(RESTClient.class)
final class RESTClientTest {

    private RESTClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    @Mock
    private HttpHeaders headers;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setPath("/")
                        .build(),
                new HttpResponseMock.Builder()
                        .setStatus(201)
                        .setVersion(HTTP_1_0)
                        .setHeaders(headers)
                        .setBody("<BODY>")
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_support_internals() {
        final Response response = assertDoesNotThrow(() -> restClient.get().join());

        assertEquals(201, response.getStatus());
        assertEquals(HTTP_1_0, response.getVersion());
        assertSame(headers, response.getHeaders());
        assertArrayEquals("<BODY>".getBytes(UTF_8), response.getBody());

    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

7.2. Supported Internal Data Types

Table 2. Supported internal data types for a response model.
Java type RxMicro Annotation Is the annotation required? Description

HttpVersion

None

-

HTTP protocol version

HttpHeaders

None

-

HTTP header model

Integer

@ResponseStatusCode

Yes

HTTP response status code

byte[]

@ResponseBody

Yes

HTTP response body content

Currently only 1.0 and 1.1 HTTP versions are supported.

8. Versioning of REST Clients

The RxMicro framework supports versioning of REST Clients using two strategies:

  • Versioning based on HTTP header analysis with the Api-Version name.

  • Versioning based on URL Path fragment analysis.

8.1. Versioning Based on HTTP Header Analysis

The RxMicro framework allows creating identical REST clients that differ only in version:

@RestClient
@Version(value = "v1", strategy = Version.Strategy.HEADER) (1)
public interface OldRestClient {

    @PATCH("/patch")
    CompletableFuture<Void> update();
}
1 REST client of the old v1 version, using the Version.Strategy.HEADER strategy;
@RestClient
@Version(value = "v2", strategy = Version.Strategy.HEADER) (1)
public interface NewRestClient {

    @PATCH("/patch")
    CompletableFuture<Void> update();
}
1 REST client of the new v2 version, using the Version.Strategy.HEADER strategy;

The correctness of HTTP request generation for these REST clients can be checked with the following tests:

@InitMocks
@RxMicroComponentTest(OldRestClient.class)
final class OldRestClientTest {

    private OldRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.PATCH)
                        .setPath("/patch")
                        .setHeaders(HttpHeaders.of(
                                API_VERSION, "v1"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_add_version_url_path() {
        assertDoesNotThrow(() -> restClient.update().join());
    }
}
@InitMocks
@RxMicroComponentTest(NewRestClient.class)
final class NewRestClientTest {

    private NewRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.PATCH)
                        .setPath("/patch")
                        .setHeaders(HttpHeaders.of(
                                API_VERSION, "v2"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_add_version_url_path() {
        assertDoesNotThrow(() -> restClient.update().join());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

8.2. Versioning Based on URL Path Fragment Analysis

The RxMicro framework allows creating identical REST clients that differ only in version:

@RestClient
@Version(value = "v1", strategy = Version.Strategy.URL_PATH) (1)
public interface OldRestClient {

    @PATCH("/patch")
    CompletableFuture<Void> update();
}
1 REST client of the old v1 version, using the Version.Strategy.URL_PATH strategy;
@RestClient
@Version(value = "v2", strategy = Version.Strategy.URL_PATH) (1)
public interface NewRestClient {

    @PATCH("/patch")
    CompletableFuture<Void> update();
}
1 REST client of the new v2 version, using the Version.Strategy.URL_PATH strategy;

The correctness of HTTP request generation for these REST clients can be checked with the following tests:

@InitMocks
@RxMicroComponentTest(OldRestClient.class)
final class OldRestClientTest {

    private OldRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.PATCH)
                        .setPath("/v1/patch")
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_add_version_url_path() {
        assertDoesNotThrow(() -> restClient.update().join());
    }
}
@InitMocks
@RxMicroComponentTest(NewRestClient.class)
final class NewRestClientTest {

    private NewRestClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.PATCH)
                        .setPath("/v2/patch")
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_add_version_url_path() {
        assertDoesNotThrow(() -> restClient.update().join());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

9. Base URL Path for All Handlers

To configure a base URL Path for all methods sending an HTTP request, the RxMicro framework provides the @BaseUrlPath annotation:

@RestClient
@BaseUrlPath("base-url-path")
public interface RESTClient {

    @GET("/path")
    CompletableFuture<Void> path();
}
@InitMocks
@RxMicroComponentTest(RESTClient.class)
final class RESTClientTest {

    private RESTClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.GET)
                        .setPath("/base-url-path/path")
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_support_base_url() {
        assertDoesNotThrow(() -> restClient.path().join());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

10. Expressions

The RxMicro framework supports expressions for REST clients.

Expressions can be useful to send configuration parameters to the server.

To use expressions You need to create a configuration class:

public final class CustomRestClientConfig extends RestClientConfig {

    private boolean useProxy = true;

    private Mode mode = Mode.PRODUCTION;

    public boolean isUseProxy() {
        return useProxy;
    }

    public CustomRestClientConfig setUseProxy(final boolean useProxy) {
        this.useProxy = useProxy;
        return this;
    }

    public Mode getMode() {
        return mode;
    }

    public CustomRestClientConfig setMode(final Mode mode) {
        this.mode = mode;
        return this;
    }

    @Override
    public CustomRestClientConfig setSchema(final ProtocolSchema schema) {
        return (CustomRestClientConfig) super.setSchema(schema);
    }

    @Override
    public CustomRestClientConfig setHost(final String host) {
        return (CustomRestClientConfig) super.setHost(host);
    }

    @Override
    public CustomRestClientConfig setPort(final int port) {
        return (CustomRestClientConfig) super.setPort(port);
    }

    public enum Mode {

        PRODUCTION,

        TEST
    }
}

, which must meet the following requirements:

  1. The class must be public.

  2. The class must contain a public constructor without parameters.

  3. The class must extend the RestClientConfig class.

  4. To set property values, the class must contain setters.
    (Only those fields, that will contain setters, can be initialized!)

To attach this configuration class to a REST client, You must specify it in the @RestClient annotation parameter:

@RestClient(
        configClass = CustomRestClientConfig.class,    (1)
        configNameSpace = "custom"                     (2)
)
@AddHeader(name = "Use-Proxy", value = "${useProxy}")  (3)
public interface RESTClient {

    @PUT("/")
    @AddHeader(name = "Debug", value = "Use-Proxy=${useProxy}, Mode=${mode}")             (3)
    @AddHeader(name = "Endpoint", value = "Schema=${schema}, Host=${host}, Port=${port}") (3)
    CompletableFuture<Void> put();
}
1 Attaching the configuration class to a REST client.
2 The name space specification for this configuration file.
(For more information on name space, refer to the core.html section)
3 After attaching the configuration class, its properties can be used in expressions.

So dont’t forget to add the following opens instruction to your module-info.java:

exports io.rxmicro.examples.rest.client.expressions to
        rxmicro.reflection;

The functionality of expressions can be demonstrated through the test:

@InitMocks
@RxMicroComponentTest(RESTClient.class)
final class RESTClientTest {

    @WithConfig("custom")
    private static final CustomRestClientConfig config =
            new CustomRestClientConfig()
                    .setHost("rxmicro.io")
                    .setPort(8443)
                    .setSchema(ProtocolSchema.HTTPS)
                    .setUseProxy(false)
                    .setMode(CustomRestClientConfig.Mode.TEST);

    private RESTClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepare() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(HttpMethod.PUT)
                        .setPath("/")
                        .setHeaders(HttpHeaders.of(
                                "Use-Proxy", "false",
                                "Debug", "Use-Proxy=false, Mode=TEST",
                                "Endpoint", "Schema=HTTPS, Host=rxmicro.io, Port=8443"
                        ))
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepare")
    void Should_support_expressions() {
        assertDoesNotThrow(() -> restClient.put().join());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

11. Request ID

The Request Id feature described at Monitoring section.

12. Partial Implementation

If the REST client generated by the RxMicro Annotation Processor contains errors, incorrect or non-optimized logic, the developer can use the Partial Implementation feature.

This feature allows You to implement HTTP request handlers for the REST client on Your own, instead of generating them by the RxMicro framework.

To activate this feature, You need to use the @PartialImplementation annotation, and specify an abstract class that contains a partial implementation of HTTP request handler(s) for REST client:

@RestClient
(1)
@PartialImplementation(AbstractRESTClient.class)
public interface RESTClient {

    @GET("/")
    CompletableFuture<Void> generatedMethod();

    CompletableFuture<Void> userDefinedMethod();
}
1 Using the @PartialImplementation annotation, the AbstractRESTClient class is specified.

An AbstractRESTClient contains the following content:

abstract class AbstractRESTClient extends AbstractRestClient implements RESTClient {

    @Override
    public final CompletableFuture<Void> userDefinedMethod() {
        return CompletableFuture.completedFuture(null);
    }
}

An abstract class that contains a partial implementation must meet the following requirements:

  1. The class must be an abstract one.

  2. The class must extend the AbstractRestClient one.

  3. The class must implement the REST client interface.

  4. The class must contain an implementation of all methods that are not generated automatically.

In terms of infrastructure, the HTTP request handlers generated and defined by the developer for REST client do not differ:

@InitMocks
@RxMicroComponentTest(RESTClient.class)
final class RESTClientTest {

    private RESTClient restClient;

    @Mock(answer = RETURNS_DEEP_STUBS)
    @Alternative
    private HttpClientFactory httpClientFactory;

    private void prepareGeneratedMethod() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setMethod(GET)
                        .setPath("/")
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepareGeneratedMethod")
    void Should_invoke_generated_method() {
        assertDoesNotThrow(() -> restClient.generatedMethod().join());
    }

    private void prepareUserDefinedMethod() {
        prepareHttpClientMock(
                httpClientFactory,
                new HttpRequestMock.Builder()
                        .setAnyRequest()
                        .build(),
                true
        );
    }

    @Test
    @BeforeThisTest(method = "prepareUserDefinedMethod")
    void Should_invoke_user_defined_method() {
        assertDoesNotThrow(() -> restClient.userDefinedMethod().join());

        final HttpClient httpClient = getPreparedHttpClientMock();
        verify(httpClient, never()).sendAsync(anyString(), anyString(), any());
        verify(httpClient, never()).sendAsync(anyString(), anyString(), any(), any());
    }
}

The project source code used in the current subsection is available at the following link:

When compiling, the RxMicro framework searches for RxMicro Annotations in the source code and generates additional classes necessary for the integral work of the microservice.

When changing the RxMicro Annotations in the source code, DON’T FORGET to recompile the ALL source code, not just the changed file, for the changes to take effect: mvn clean compile.

13. Configuring the Code Generation Process

The RxMicro framework provides an option to configure the code generation process for REST client. For this purpose, it is necessary to use the @RestClientGeneratorConfig annotation, that annotates the module-info.java module descriptor:

import io.rxmicro.rest.client.RestClientGeneratorConfig;
import io.rxmicro.rest.model.ClientExchangeFormatModule;
import io.rxmicro.rest.model.GenerateOption;

@RestClientGeneratorConfig(
        exchangeFormat = ClientExchangeFormatModule.AUTO_DETECT,                    (1)
        generateRequestValidators = GenerateOption.DISABLED,                        (2)
        generateResponseValidators = GenerateOption.DISABLED,                       (3)
        requestValidationMode =
                RestClientGeneratorConfig.RequestValidationMode.RETURN_ERROR_SIGNAL (4)
)
module rest.client.generator {
    requires rxmicro.rest.client;
}
1 The exchangeFormat parameter allows You to specify a format for message exchange with a server.
(By default, it is used the message exchange format which added to the module-info.java descriptor. If several modules supporting the message exchange format are added to the module-info.java descriptor, then using the exchangeFormat, You need to specify which module should be used for REST clients.)
2 The generateRequestValidators parameter allows enabling/disabling the option of generating HTTP request validators for all REST client methods in the project.
(The DISABLED value means that validators won’t be generated by the RxMicro Annotation Processor.)
3 The generateResponseValidators parameter allows enabling/disabling the option of generating HTTP response validators for all REST client methods in the project.
(The AUTO_DETECT value means that validators will be generated only if the developer adds the rxmicro.validation module to the module-info.java descriptor.)
4 The requestValidationMode parameter specifies how the HTTP request parameters should be checked.

If the requestValidationMode parameter is equal to the RETURN_ERROR_SIGNAL, then error handling should be performed in reactive style:

restClient.put("not_valid_email")
        .exceptionally(throwable -> {
            // do something with ValidationException
            return null;
        });

If the requestValidationMode parameter is equal to the THROW_EXCEPTION, then it is necessary to catch the exception

try {
    restClient.put("not_valid_email");
} catch (final ValidationException e) {
    // do something with ValidationException
}

The RxMicro team strong recommends using the AUTO_DETECT generate option always! (This is default value for @RestClientGeneratorConfig annotation).

(HTTP request validation can be useful for identifying errors in business task implementation algorithms. For example, instead of returning an incorrect request model to a server, the REST client will throw an error. This approach increases the speed of error search and debugging of the source code that performs the business task.)

FYI: By default the request validators are generated but not invoked! To activate the validation of requests it is necessary to set enableAdditionalValidations property:

new Configs.Builder()
        .withConfigs(new RestClientConfig()
                .setEnableAdditionalValidations(true)) (1)
        .build();
1 Enables the requests validators

or

export rest-client.enableAdditionalValidations=true (1)
1 Enables the response validators

or using any other supported config types

Thus the RxMicro team recommends the following approach:

  • Your project configuration must enable generation of validators for all possible model classes.

  • Your development and staging environment must enable additional validation.

  • Your production environment must disable additional validation!

REST Controller

Home

Validation