© 2019-2020 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.

This entity is also available as a PDF download.

1. Introduction

RxMicro is a modern, JVM-based, full stack framework designed to develop distributed reactive applications that use a microservice architecture.

The RxMicro framework provides developers with a convenient tool to focus on writing an application business logic. Meanwhile, routine and standard operations, which are the prerequisite for launching an application, are delegated to the framework.

The RxMicro framework is small and lightweight. Even though the RxMicro framework is designed to create microservices, a developer can easily use separate RxMicro modules to develop any type of application using a reactive approach.

The RxMicro framework is a framework that uses reactive programming as the main and only approach when designing microservices.

Any blocking operations are not supported!

When developing a project using the RxMicro framework, use only non-blocking drivers to interact with databases, network connections and files. Otherwise Your project will work too slow, and won’t be able to process a large number of clients' requests.

1.1. RxMicro Features

The RxMicro framework provides the following feature set:

  1. Declarative programming using annotations.

  2. Reactive programming using common libraries:

  3. Configuring using java configuration, annotations, files, system properties and environment variables.

  4. Declarative handlers of HTTP requests to the microservice.

    1. Request routing based on HTTP method, URL path and HTTP body analysis.

    2. HTTP header processing.

    3. HTTP parameter processing.

    4. Path variable support.

    5. Automatic conversion of request data into Java model and vice versa.

    6. Built-in validation of requests and responses.

    7. Static configuration support.

    8. Handler versioning support.

    9. Cross-Origin Resource Sharing (CORS) support.

    10. Support for request identification during inter-service interaction.

  5. Declarative REST client.

    1. HTTP header processing.

    2. HTTP parameter processing.

    3. Path variable support.

    4. Automatic conversion of request data into Java model and vice versa.

    5. Built-in validation of requests and responses.

    6. Static configuration support.

    7. REST client versioning support.

    8. Request timeout.

    9. Automatic redirection support.

    10. Customization option for standard client implementation.

  6. Contexts and Dependency Injection (CDI).

    1. Dependencies can be explicitly managed without using CDI.

    2. Dependency injection by fields, methods and constructors.

    3. Qualifier support.

    4. Factory method support.

    5. Post construct method support.

    6. Class factory support.

    7. Optional injection.

    8. Resource injection.

    9. Multibinder support.

    10. Dependency injection using JEE and Spring style.

  7. Generation of REST-based microservice documentation.

    1. Documenting with annotations.

    2. asciidoc support (widely used and multifunctional documenting format).

    3. Configuration of the project documentation standard sections.

  8. Data Repositories.

    1. Postgre SQL Data Repositories.

      1. SELECT, INSERT, UPDATE, DELETE operation support.

      2. Auto-generated primary key support.

      3. Composite primary key support.

      4. Transaction support.

      5. Variable support in SQL query.

      6. Customized SELECT queries support.

      7. Possibility to customize a standard repository implementation.

      8. Access to a low-level API.

      9. Auto registration of enum codecs.

    2. Mongo Data Repositories.

      1. find, aggregate, distinct, insert, update, delete, countDocuments and estimatedDocumentCount operation support.

      2. Auto-generated entity id support.

      3. Query parameter logging.

      4. Possibility to customize a standard repository implementation.

      5. Access to a low-level API.

  9. Monitoring

    1. Health checks.

    2. Request tracing.

  10. Testing.

    1. Component testing.

    2. REST-based microservices testing.

    3. Integration testing.

    4. Support for alternatives and mocks.

    5. Integration with JUnit 5 and Mockito frameworks.

    6. Integration with DBUnit framework.

  11. Monitoring.

    1. Health checks.

    2. Request tracing.

  12. Integration with other Java libraries and frameworks.

1.2. RxMicro Benefits

The RxMicro framework provides the following benefits:

  • Declarative programming using annotations.

  • CDI by demand.

  • Human readable generated code.

  • Verifier of the redundant and inefficient source code.

  • Runtime without reflection.

  • Fast startup time.

  • Reduced memory footprint.

These benefits are gained due to:

  • using of Java annotation processors, which generates standard code based on RxMicro Annotations;

  • replacing standard Java libraries that require reflection for their work with analogs that do not need reflection;

  • using of Netty as the primary NIO framework for non-blocking asynchronous IO operations;

  • generation of low-level code avoiding unnecessary abstractions and proxies.

1.3. Requirements

The RxMicro framework requires JDK 11 LTS or higher.

To succeed in studying this guide, it is assumed that the reader is familiar with the following technologies:

The RxMicro framework uses the following Java modules:

  • Common module(s):

  • The rxmicro.logger module requires the following module(s):

  • REST client and REST based microservice test modules require the following module(s):

  • The rxmicro.data.r2dbc.postgresql module requires the following module(s):

  • Netty requires the the following module(s):

    • jdk.unsupported.

2. What are Microservices?

Microservices - also known as the microservice architecture - is an architectural style that structures an application as a collection of services that are:

  • Highly maintainable and testable.

  • Loosely coupled.

  • Independently deployable.

  • Organized around business capabilities.

  • Owned by a small team.

(Read more at https://microservices.io/…​)

Thus, a microservice project consists of several microservices. Each microservice must fulfill only one business task.

Let’s look at a microservice that displays the current date and time in UTC format:

public final class MicroService1 {

    public static void main(final String[] args) {
        System.out.println(Instant.now());
    }
}

Does this microprogram constitute a microservice?
Yes, since this microprogram fulfills a business task.
Unfortunately, this program has a serious disadvantage: it interacts with clients through the console. Therefore, only a client’s program with a console interface launched in a session of the current logged-in OS user will be able to interact with this microservice! This restriction makes it impossible to scale this microservice!

Can we improve this situation? Yes, we can:

public final class MicroService2 {

    public static void main(final String[] args) throws Exception {
        Files.write(
                Paths.get("/var/microservice/now-instant.txt"),
                Instant.now().toString().getBytes(UTF_8)
        );
    }
}

This microservice uses a file system to interact with client’s programs. In this way, the only requirement for the client’s program is to be run on the same computer on which the microservice is running. The situation has improved, but it is still impossible to scale this microservice horizontally!

Can we improve this situation? Yes, we can:

public final class MicroService3 {

    public static void main(final String[] args) throws Exception {
        try (final ServerSocket serverSocket = new ServerSocket(8080)) {
            try (final Socket clientSocket = serverSocket.accept()) {
                try (final OutputStream out = clientSocket.getOutputStream()) {
                    // read command from input stream
                    out.write(Instant.now().toString().getBytes(UTF_8));
                }
            }
        }
    }
}

Now, the microservice uses the network to interact with clients. This implementation of the microservice is scalable as the microservice can now be run on several networked computers. The situation has improved markedly, but there are problems with networking:

  • Presence of firewalls.

  • The need to create an interaction protocol.

  • Independence from the programming language is an important criterion for the interaction protocol.

Can we improve this situation? Yes, for this purpose You can use the HTTP protocol with the REST architecture style:

public final class MicroService4 {

    public static void main(final String[] args) throws IOException {
        final HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8080), 0);
        server.createContext("/now-instant", exchange -> {
            final String content = Instant.now().toString();
            exchange.sendResponseHeaders(200, content.length());
            exchange.getResponseHeaders().add("Content-Type", "text/txt");
            try (final OutputStream body = exchange.getResponseBody()) {
                body.write(content.getBytes(UTF_8));
            }
        });
        server.start();
    }
}

That’s why microservices are often referred to as REST-based microservices

For simple tasks, the entire logic of the microservice can be found in one class, which is often called microservice. If a microservice has to solve a complex task, then this microservice is divided into two logical components:

  1. REST controller, the main task of which is:

    1. to accept HTTP requests;

    2. to validate HTTP requests;

    3. to convert HTTP requests into Java models;

    4. to invoke request handlers;

    5. once the response model is received, convert it to an HTTP response.

  2. Business service, the main task of which is:

    1. if the task is of medium complexity, then independently calculate the result and return it to the REST controller;

    2. if it is a high-complexity task, then decompose it into sub-tasks and delegate its execution to other microservices. After all sub-tasks have been completed, merge the result and return it to the REST controller.

Therefore, the following is implied in this guide:

  1. If You find the term microservice, it means REST-based microservice, unless stated otherwise!

  2. If You find the term REST controller, it means a logical component of the microservice that performs its direct functions!

3. Quick Start

This section describes in detail the steps to be taken in order to create the REST-based microservice that returns the "Hello World!" message, using the RxMicro framework.

In order to successfully execute these instructions, You need to install JDK 11 LTS or higher on Your computer. For Your convenience it is also recommended to use a modern IDE, for example IntelliJ IDEA.

The features of the IntelliJ IDEA Community Edition version are enough for a complete and convenient work on a project that uses the RxMicro framework.

The RxMicro framework consists of several dozens of modules, so for convenient handling it is recommended to install maven on Your computer.

Any modern IDE for Java (IntelliJ IDEA, Eclipse, NetBeans) already contains a built-in maven, so there is no need in its additional installation on Your computer.

To run maven commands, You can use Your IDE instead of the terminal.

3.1. Creating a Project

For creating a project, it is recommended to use a modern IDE, for example IntelliJ IDEA

3.1.1. Using the IntelliJ IDEA

To create a new project, proceed as follows: FileNewProject or Create a New Project.

create maven project step1
Figure 1. Creating the simplest project in IntelliJ IDEA: Choosing a project type.

In the appeared dialog box select the Maven type, make sure that Project SDK version 11 or higher will be used, remove the Create from archetype checkbox and click Next.

create maven project step2
Figure 2. Creating the simplest project in IntelliJ IDEA: Basic settings.

In the appeared dialog box type Name, Location (if the default value is wrong) and GroupId (if the default value is wrong), and click Finish.

As a result, IntelliJ IDEA will generate the following project template using maven settings:

create maven project step3
Figure 3. Creating the simplest project in IntelliJ IDEA: Basic project template.

After creating the standard template, activate the Enable Auto-Import option.

If for some reason the IntelliJ IDEA or another IDE You use for Java coding (e.g. Eclipse or NetBeans) has generated another project template, delete unnecessary files, create un-created folders and leave only the following sections in pom.xml: modelVersion, groupId, artifactId, version.

As a result, Your template should fully match the template: Figure 3, “Creating the simplest project in IntelliJ IDEA: Basic project template.”.

3.1.2. Using the Terminal

It is possible to create a new maven project without using the IDE.

If You don’t intend to write the source code of a project in notepad, but rather use the IDE to do this, You should directly create a maven project using the IDE.

To do this, open the terminal and run the following command:

mvn archetype:generate -DgroupId=io.rxmicro.examples -DartifactId=quick-start -DinteractiveMode=false

In order to run maven commands in the terminal it is necessary to:

  • Install JDK and maven on Your computer;

  • Set the JAVA_HOME environment variable;

  • Add the path to $MAVEN_HOME/bin folder to the $PATH environment variable.

A detailed instruction on the maven installation for its further use in the terminal can be found at the following link: Installing Apache Maven.

As a result, the quick-start folder with the basic project template will be created in the current folder. After that, the created project must be imported into the IDE.

By default, mvn archetype:generate doesn’t generate an empty project, but a project with two App and AppTest classes, as well as a connected junit library of 3.8.1 version in pom.xml. The specified classes and dependencies should be deleted before performing the following steps of this guide.

As a result, Your template should fully match the template: Figure 3, “Creating the simplest project in IntelliJ IDEA: Basic project template.”.

3.1.3. Using Other IDE

Creating the simplest project with other IDEs does not differ much from creating it with IntelliJ IDEA. When creating, You should also specify maven archetype, groupId, artifactId and version.

The main thing is that after creation Your project template should fully match the template: Figure 3, “Creating the simplest project in IntelliJ IDEA: Basic project template.”.

3.2. Configuring the Project

Before writing the code of a REST-based microservice, You should configure pom.xml of Your project by performing the following steps:

  1. Define the versions of used libraries.

  2. Add the required dependencies to the pom.xml.

  3. Configure the maven-compiler-plugin.

3.2.1. Definition the Versions of the Used Libraries

To make further updating of library versions convenient, it is recommended to use maven properties:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <rxmicro.version>0.10-SNAPSHOT</rxmicro.version> (1)
    <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version> (2)
</properties>

3.2.2. Adding the Required Dependencies

Before using RxMicro modules, the following dependencies must be added to the project:

<dependencies>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-rest-server-netty</artifactId> (1)
        <version>${rxmicro.version}</version>
    </dependency>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-rest-server-exchange-json</artifactId> (2)
        <version>${rxmicro.version}</version>
    </dependency>
</dependencies>
1 Library for building REST-based microservices based on HTTP server that uses Netty;
2 Library for converting Java model to JSON format and vice versa;

3.2.3. Configuring the maven-compiler-plugin

Since the RxMicro framework uses the Java annotation processors, You need to set up maven-compiler-plugin:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version>
    <configuration>
        <release>11</release> (1)
        <annotationProcessorPaths>
            <annotationProcessorPath>
                <groupId>io.rxmicro</groupId>
                <artifactId>rxmicro-annotation-processor</artifactId> (2)
                <version>${rxmicro.version}</version>
            </annotationProcessorPath>
        </annotationProcessorPaths>
    </configuration>
    <executions>
        <execution>
            <id>source-compile</id>
            <goals>
                <goal>compile</goal>
            </goals>
            <configuration>
                <annotationProcessors>
                    <annotationProcessor>
                        io.rxmicro.annotation.processor.RxMicroAnnotationProcessor (3)
                    </annotationProcessor>
                </annotationProcessors>
                <generatedSourcesDirectory>
                    ${project.build.directory}/generated-sources/ (4)
                </generatedSourcesDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>
1 The RxMicro framework requires a Java compiler of v11 or higher;
2 The annotation processor library, that will handle all RxMicro Annotations;
3 The annotation processor class, that handles the launch configuration;
4 Location of the generated Java classes by the RxMicro Annotation Processor;

3.2.4. The Final Version of pom.xml File

After all the above changes, the final version of the pom.xml file should look like:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.rxmicro.examples</groupId>
    <artifactId>quick-start</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <rxmicro.version>0.10-SNAPSHOT</rxmicro.version>
        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>${maven-compiler-plugin.version}</version>
                    <configuration>
                        <release>11</release>
                        <annotationProcessorPaths>
                            <annotationProcessorPath>
                                <groupId>io.rxmicro</groupId>
                                <artifactId>rxmicro-annotation-processor</artifactId>
                                <version>${rxmicro.version}</version>
                            </annotationProcessorPath>
                        </annotationProcessorPaths>
                    </configuration>
                    <executions>
                        <execution>
                            <id>source-compile</id>
                            <goals>
                                <goal>compile</goal>
                            </goals>
                            <configuration>
                                <annotationProcessors>
                                    <annotationProcessor>
                            io.rxmicro.annotation.processor.RxMicroAnnotationProcessor
                                    </annotationProcessor>
                                </annotationProcessors>
                                <generatedSourcesDirectory>
                                    ${project.build.directory}/generated-sources/
                                </generatedSourcesDirectory>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

    <dependencies>
        <dependency>
            <groupId>io.rxmicro</groupId>
            <artifactId>rxmicro-rest-server-netty</artifactId>
            <version>${rxmicro.version}</version>
        </dependency>
        <dependency>
            <groupId>io.rxmicro</groupId>
            <artifactId>rxmicro-rest-server-exchange-json</artifactId>
            <version>${rxmicro.version}</version>
        </dependency>
    </dependencies>
</project>

3.3. Creating the Source Code

The source code of the simplest REST-based microservice consists of one module, one package and two classes. The source code of each of these components is described below.

3.3.1. A module-info.java Descriptor

Java 9 has introduced the JPMS.

Therefore, the RxMicro framework, which requires the use of JDK 11 or higher, requires a module-info.java descriptor for any of Your microservice projects.

module examples.quick.start {
    requires rxmicro.rest.server.netty;         (1)
    requires rxmicro.rest.server.exchange.json; (2)
}
1 Module for building REST-based microservices based on HTTP server that uses Netty, with all required transitive dependencies.
2 Module for converting a Java model to JSON format and vice versa, with all required transitive dependencies.

Usually module-info.java depends on all RxMicro modules listed in the dependencies section of the pom.xml file of any of Your microservice projects. Therefore, it is enough to duplicate all the dependencies in module-info.java.

Thanks to the transitive dependencies of the RxMicro framework, the number of modules required has been greatly reduced. Only basic RxMicro modules must be specified!

3.3.2. An HTTP Response Model Class

package io.rxmicro.examples.quick.start;

import static java.util.Objects.requireNonNull;

@SuppressWarnings("SameParameterValue")
final class Response {

    final String message;

    Response(final String message) {
        this.message = requireNonNull(message);
    }
}

According to the specification, JSON format supports the following data types: object, array and primitives: strings, logical type, numeric type and null.

To simplify communication between REST-based microservices, the RxMicro framework supports only JSON object as a return type of any REST-based microservice. Thus, any REST-based microservice built via the RxMicro framework can return only JSON objects. In case You need to return a primitive or an array, You need to create a wrapper class.

Therefore, to display the "Hello World!" string, it is necessary to create a Response wrapper class, which is a wrapper above the string data type.

3.3.3. A REST-Based Microservice Class

package io.rxmicro.examples.quick.start;

import io.rxmicro.rest.method.GET;
import io.rxmicro.rest.server.RxMicroRestServer;

import java.util.concurrent.CompletableFuture;

public final class HelloWorldMicroService {

    @GET("/")
    CompletableFuture<Response> sayHelloWorld() {       (1)
        return CompletableFuture.supplyAsync(() ->
                new Response("Hello World!"));          (2)
    }

    public static void main(final String[] args) {              (3)
        RxMicroRestServer.startRestServer(HelloWorldMicroService.class);  (4)
    }
}
1 REST-based microservice contains a handler of HTTP GET method: sayHelloWorld, which doesn’t accept any parameters and returns a CompletableFuture reactive type.
2 The CompletableFuture.supplyAsync() static method is used to create an object of CompletableFuture class.
3 To launch a REST-based microservice the main method is used.
4 The launch is performed using the RxMicroRestServer.startRestServer(Class<?>) static method, which requires the REST-based microservice class as parameter.

Note that the HTTP request handler method doesn’t need to be public.

The protected and <default> modifiers are also supported by the RxMicro framework.

3.3.4. A Structure of the Microservice Project

The above-mentioned components of the microservice project should be located in the project according to the following screenshot:

project structure
Figure 4. A structure of the microservice project

3.4. Compiling the Project

3.4.1. Using the maven

To compile a project using the maven, open the terminal in the project root folder and proceed with the following command:

mvn clean compile

In order to run maven commands in the terminal it is necessary to:

  • Install JDK and maven on Your computer;

  • Set the JAVA_HOME environment variable;

  • Add the path to $MAVEN_HOME/bin folder to the $PATH environment variable.

A detailed instruction on the maven installation for its further use in the terminal can be found at the following link: Installing Apache Maven.

It is possible to compile the project with maven even without using the terminal. Since any modern IDE for Java contains built-in maven, You can use this built-in maven tool.

To do this, open the maven panel and execute the specified commands with a mouse or touchpad manipulator. For example, the maven panel in IntelliJ IDEA looks like:

maven panel
Figure 5. Maven panel in IntelliJ IDEA

After successful compilation, the RxMicro Annotation Processor work results are displayed in the terminal:

[INFO] ------------------------------------------------------------------------
[INFO]  RX-MICRO ANNOTATIONS PROCESSING
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] Current environment context is:
  RxMicro version: 0.10-SNAPSHOT
  Current module:
    examples.quick.start
  Available RxMicro modules:
    rxmicro.common;
    rxmicro.reflection;
    rxmicro.model;
    rxmicro.runtime;
    rxmicro.config;
    rxmicro.logger;
    rxmicro.files;
    rxmicro.http;
    rxmicro.rest;
    rxmicro.rest.server;
    rxmicro.rest.server.netty;
    rxmicro.json;
    rxmicro.exchange.json;
    rxmicro.rest.server.exchange.json;
  Include packages: <none>
  Exclude packages: <none>
[INFO] Found the following REST controllers:
  io.rxmicro.examples.quick.start.HelloWorldMicroService:
    'GET /' -> sayHelloWorld();
[INFO] Generating java classes...
[INFO] All java classes generated successful in 0.031 seconds. (1)
[INFO] ------------------------------------------------------------------------
[INFO] Annotations processing completed successful.
[INFO] ------------------------------------------------------------------------
1 The given information indicates that all files needed to run the microservice have been generated.

In the target folder You can find all generated and compiled classes of the microservice project.

To understand how the RxMicro framework works, please go to Section 4.1, “How It Works?” section.

3.4.2. Using the IntelliJ IDEA

The IntelliJ IDEA allows annotation processors to be launched automatically when building a project. So if You want to compile a microservice project using IntelliJ IDEA rather than maven, You need to set up the Annotation Processors section in the IntelliJ IDEA.

3.4.2.1. Enable Annotation Processing

To enable annotation processing while building a project with IntelliJ IDEA, You need to set up the Annotation Processors section. To do so, open the menu: FileSettings and get to the tab:`Build, Execution, Deployment` → CompilerAnnotation Processors.

idea annotation processing
Figure 6. Required Annotation Processing settings

Make sure that all Your settings of the Annotation Processors section correspond to the settings shown in the figure above.

3.4.2.2. Rebuilding the Project

After setting up the Annotation Processors section, the project must be rebuilt. To do so, run the following command from the main menu: BuildRebuild project.

3.5. Starting the Microservice

3.5.1. Using the IDE:

You can run the REST-based microservice using the IntelliJ IDEA launch context menu

run using ide
Figure 7. Run the REST-based micro service using IntelliJ IDEA context menu.

If You get the following error while starting the REST-based HelloWorldMicroService microservice:

kotlin error fix

rebuild the project!

(To do this, run the command Rebuild project from the main menu: BuildRebuild project.)

After starting, the console will display the following information:

2020-02-02 20:14:11.707 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:8080 using NETTY transport in 500 millis (1)
1 The Server started in …​ millis message means that the RxMicro HTTP server has been successfully started.

If an error occurs during the starting process, the console will display a stack trace of this error.

3.5.2. Using the Terminal:

Go to the target folder of the microservice project, open the terminal in this folder and run the following command:

java -Dfile.encoding=UTF-8 -p ./classes:$M2_REPO/io/rxmicro/rxmicro-rest-server-netty/0.10-SNAPSHOT/rxmicro-rest-server-netty-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-rest-server/0.10-SNAPSHOT/rxmicro-rest-server-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-rest/0.10-SNAPSHOT/rxmicro-rest-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-model/0.10-SNAPSHOT/rxmicro-model-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-http/0.10-SNAPSHOT/rxmicro-http-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-logger/0.10-SNAPSHOT/rxmicro-logger-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-common/0.10-SNAPSHOT/rxmicro-common-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-config/0.10-SNAPSHOT/rxmicro-config-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-runtime/0.10-SNAPSHOT/rxmicro-runtime-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-files/0.10-SNAPSHOT/rxmicro-files-0.10-SNAPSHOT.jar:$M2_REPO/io/netty/netty-codec-http/4.1.69.Final/netty-codec-http-4.1.69.Final.jar:$M2_REPO/io/netty/netty-common/4.1.69.Final/netty-common-4.1.69.Final.jar:$M2_REPO/io/netty/netty-buffer/4.1.69.Final/netty-buffer-4.1.69.Final.jar:$M2_REPO/io/netty/netty-codec/4.1.69.Final/netty-codec-4.1.69.Final.jar:$M2_REPO/io/netty/netty-handler/4.1.69.Final/netty-handler-4.1.69.Final.jar:$M2_REPO/io/netty/netty-transport/4.1.69.Final/netty-transport-4.1.69.Final.jar:$M2_REPO/io/netty/netty-resolver/4.1.69.Final/netty-resolver-4.1.69.Final.jar:$M2_REPO/io/rxmicro/rxmicro-rest-server-exchange-json/0.10-SNAPSHOT/rxmicro-rest-server-exchange-json-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-exchange-json/0.10-SNAPSHOT/rxmicro-exchange-json-0.10-SNAPSHOT.jar:$M2_REPO/io/rxmicro/rxmicro-json/0.10-SNAPSHOT/rxmicro-json-0.10-SNAPSHOT.jar -m examples.quick.start/io.rxmicro.examples.quick.start.HelloWorldMicroService

It is assumed that the M2_REPO environment variable is set on Your computer. This variable must contain a path to the maven local repository.

By default, the local repository is located in the $HOME/.m2/repository folder, where $HOME is the home directory (i.e. System.getProperty("user.home")):

  • for Linux platform the $HOME directory is /home/$USERNAME;

  • for MacOS platform the $HOME directory is /Users/$USERNAME;

  • for Windows platform the $HOME directory is C:\Documents and Settings\%USERNAME% or C:\Users\%USERNAME%.

The above example of launching a microservice project using a terminal won’t work on Windows OS.

Inoperability is caused by the use of different special symbols on Unix (Linux and MacOS) and Windows platforms:

Therefore, in order to launch a microservice project on the Windows OS, it is necessary to replace all special symbols.

After starting, the console will display the following information:

java -p ./classes: \
        $M2_REPO/io/rxmicro/rxmicro-rest-server-netty/0.10-SNAPSHOT/rxmicro-rest-server-netty-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-rest-server/0.10-SNAPSHOT/rxmicro-rest-server-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-rest/0.10-SNAPSHOT/rxmicro-rest-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-model/0.10-SNAPSHOT/rxmicro-model-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-http/0.10-SNAPSHOT/rxmicro-http-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-logger/0.10-SNAPSHOT/rxmicro-logger-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-common/0.10-SNAPSHOT/rxmicro-common-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-config/0.10-SNAPSHOT/rxmicro-config-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-runtime/0.10-SNAPSHOT/rxmicro-runtime-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-files/0.10-SNAPSHOT/rxmicro-files-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-rest-server-exchange-json/0.10-SNAPSHOT/rxmicro-rest-server-exchange-json-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-exchange-json/0.10-SNAPSHOT/rxmicro-exchange-json-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/rxmicro/rxmicro-json/0.10-SNAPSHOT/rxmicro-json-0.10-SNAPSHOT.jar: \
        $M2_REPO/io/netty/netty-codec-http/4.1.69.Final/netty-codec-http-4.1.69.Final.jar: \
        $M2_REPO/io/netty/netty-common/4.1.69.Final/netty-common-4.1.69.Final.jar: \
        $M2_REPO/io/netty/netty-buffer/4.1.69.Final/netty-buffer-4.1.69.Final.jar: \
        $M2_REPO/io/netty/netty-codec/4.1.69.Final/netty-codec-4.1.69.Final.jar: \
        $M2_REPO/io/netty/netty-handler/4.1.69.Final/netty-handler-4.1.69.Final.jar: \
        $M2_REPO/io/netty/netty-transport/4.1.69.Final/netty-transport-4.1.69.Final.jar: \
        $M2_REPO/io/netty/netty-resolver/4.1.69.Final/netty-resolver-4.1.69.Final.jar \
    -m examples.quick.start/io.rxmicro.examples.quick.start.HelloWorldMicroService

2020-02-02 20:14:11.707 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:8080 using NETTY transport in 500 millis (1)
1 The Server started in …​ millis message means that the RxMicro HTTP server has been successfully started.

When starting the microservice via the terminal, it’s quite inconvenient to list all dependencies and their versions. To solve this problem, You can use the maven-dependency-plugin, which can copy all project dependencies. To activate the maven-dependency-plugin, You must add it to pom.xml:

<plugin>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>${maven-dependency-plugin.version}</version>(1)
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>package</phase>(2)
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.directory}/lib</outputDirectory>(3)
                <includeScope>compile</includeScope>(4)
            </configuration>
        </execution>
    </executions>
</plugin>
1 The latest stable version of the maven-dependency-plugin.
2 The plugin is invoked during the package phase.
3 Target folder all dependencies should be copied to.
(In the example above, this is the target/lib folder.)
4 This setting specifies what scope of dependencies should be copied.
(This option allows excluding libraries required for testing or libraries, those already present on the client’s computer.)

After adding the plugin, You need to execute the command:

mvn clean package

As a result of running the command, the maven-dependency-plugin will copy all the dependencies to the target/lib folder:

maven dependency plugin results
Figure 8. The required dependencies for the simplest REST-based micro service.

Now You can simplify the start command
(Instead of listing all the libraries, specify the lib folder):

java -p ./classes:lib -m examples.quick.start/io.rxmicro.examples.quick.start.HelloWorldMicroService

The above example of launching a microservice project using a terminal won’t work on Windows OS.

Inoperability is caused by the use of different special symbols on Unix (Linux and MacOS) and Windows platforms:

Therefore, in order to launch a microservice project on the Windows OS, it is necessary to replace all special symbols.

3.6. Verifying the Microservice

To receive the "Hello World!" message from the created REST-based microservice, execute GET request to localhost:8080 endpoint:

:$ curl -v localhost:8080

* Rebuilt URL to: localhost:8080/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)

> GET / HTTP/1.1 (1)
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: /
>

< HTTP/1.1 200 OK (2)
< Server: RxMicro-NettyServer/0.10-SNAPSHOT
< Date: Thu, 2 Jan 2020 11:48:13 GMT
< Content-Type: application/json
< Content-Length: 25
< Request-Id: 62jJeu8x1310662
<
* Connection #0 to host localhost left intact
{"message":"Hello World!"} (3)
1 curl sends a GET request.
2 HTTP server successfully returns a response.
3 The HTTP body contains a JSON response with the "Hello World!" message.

Therefore, the created REST-based microservice works correctly!

You can also use Your favorite browser instead of curl to verify if the REST-based microservice is working correctly.

3.7. Automated Test

The RxMicro framework provides modules for effective writing of any type of tests. Among all supported test types, a REST-based microservice test is required for the current project.

3.7.1. Configuring the Project

Before writing a REST-based microservice test, You need to configure pom.xml of Your project by performing the following steps:

  1. Add the required dependencies to pom.xml.

  2. Configure the maven-compiler-plugin.

  3. Configure the maven-surefire-plugin.

3.7.1.1. Adding the Required Dependencies

Before using RxMicro modules for testing, You need to add the following dependencies to the project:

<dependencies>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-test-junit</artifactId> (1)
        <version>${rxmicro.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-rest-client-exchange-json</artifactId> (2)
        <version>${rxmicro.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>
1 Unit testing library based on the JUnit 5 framework
2 Library for Java model conversion to JSON format and vice versa on the HTTP client side;

The REST-based microservice testing process consists in launching the REST-based microservice and sending a request to the microservice via HTTP client.
(Therefore, in maven dependencies it’s necessary to add the library supporting the JSON format on the HTTP client side (rxmicro-rest-client-exchange-json)).
After receiving a response from the microservice, the response is compared to the expected one.

3.7.1.2. Configuring the maven-compiler-plugin

Since the RxMicro framework uses the Java annotation processors, You need to configure maven-compiler-plugin:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version>
    <configuration>
        <release>11</release>
        <annotationProcessorPaths>
            <annotationProcessorPath>
                <groupId>io.rxmicro</groupId>
                <artifactId>rxmicro-annotation-processor</artifactId>
                <version>${rxmicro.version}</version>
            </annotationProcessorPath>
        </annotationProcessorPaths>
    </configuration>
    <executions>
        <execution>
            <id>source-compile</id>
            <goals>
                <goal>compile</goal>
            </goals>
            <configuration>
                <annotationProcessors>
                    <annotationProcessor>
            io.rxmicro.annotation.processor.RxMicroAnnotationProcessor
                    </annotationProcessor>
                </annotationProcessors>
                <generatedSourcesDirectory>
                    ${project.build.directory}/generated-sources/
                </generatedSourcesDirectory>
            </configuration>
        </execution>
        <execution>
            <id>test-compile</id> (1)
            <goals>
                <goal>testCompile</goal>
            </goals>
            <configuration>
                <annotationProcessors>
                    <annotationProcessor>
            io.rxmicro.annotation.processor.RxMicroTestsAnnotationProcessor (2)
                    </annotationProcessor>
                </annotationProcessors>
                <generatedTestSourcesDirectory>
                    ${project.build.directory}/generated-test-sources/ (3)
                </generatedTestSourcesDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>
1 The tests require a separate configuration, so a new execution must be added.
2 The annotation processor class that handles test configuration.
3 Location of Java-generated classes by the test annotation processor.

To learn more about how the RxMicro Annotation Processor works in the test environment, please go to the following section: Section 14.4, “How It Works”.

3.7.1.3. Configuring the maven-surefire-plugin

For a successful tests launch while building a project with maven it is necessary to update maven-surefire-plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${maven-surefire-plugin.version}</version> (1)
    <configuration>
        <properties>
            <!-- https://junit.org/junit5/docs/5.5.1/api/org/junit/jupiter/api/Timeout.html -->
            <configurationParameters>
                junit.jupiter.execution.timeout.default = 60                (2)
                junit.jupiter.execution.timeout.mode = disabled_on_debug    (3)
                junit.jupiter.execution.parallel.enabled = false            (4)
            </configurationParameters>
        </properties>
    </configuration>
</plugin>
1 Last stable version of maven-surefire-plugin.
(The plugin version must be 2.22.1 or higher, otherwise maven will ignore the tests!.)
2 In case of an error in the code which uses reactive programming, an infinite function execution may occur. In order to detect such cases, it is necessary to set a global timeout for all methods in the tests. (By default, timeout is set in seconds. More detailed information on timeouts configuration is available in official JUnit5 documentation.)
3 While debugging, timeouts can be turned off.
4 This property is useful for the tests debugging from IDE or maven.
(By setting this property the speed of test performance will decrease, so use this property for debugging only!)

3.7.2. Creating a Test Class

REST-based microservice test is a one class containing one test method:

package io.rxmicro.examples.quick.start;

import io.rxmicro.test.BlockingHttpClient;
import io.rxmicro.test.ClientHttpResponse;
import io.rxmicro.test.junit.RxMicroRestBasedMicroServiceTest;
import org.junit.jupiter.api.Test;

import static io.rxmicro.test.json.JsonFactory.jsonObject;
import static org.junit.jupiter.api.Assertions.assertEquals;

@RxMicroRestBasedMicroServiceTest(HelloWorldMicroService.class) (1)
final class HelloWorldMicroServiceTest {

    private BlockingHttpClient blockingHttpClient; (2)

    @Test
    void Should_return_Hello_World_message() {
        final ClientHttpResponse response = blockingHttpClient.get("/"); (3)
        assertEquals(
                jsonObject("message", "Hello World!"), (4)
                response.getBody()
        );
        assertEquals(200, response.getStatusCode()); (5)
    }
}
1 The RxMicro Test Annotation indicating which microservice should be run for testing.
2 The BlockingHttpClient is a basic HTTP client interface designed for use in tests. This interface allows executing blocking requests to the microservice via the HTTP protocol. This field is initialized automatically when running the test with reflection. Upon initialization it refers to the test HTTP server that was automatically started for the test.
3 Blocking request to the microservice.
4 Comparing the contents of an HTTP body with an expected value.
5 Comparing the HTTP status code with an expected value.

For low-level and effective work with JSON format, the RxMicro framework provides a separate rxmicro.json module.

To get a common idea of the capabilities of this module, which are required when writing tests, go to the following section: Section 4.10, “JSON”.

In microservice tests it is recommended to compare the HTTP request body before comparing the HTTP status, when the microservice constantly returns a text error message! (This will make it easier to understand the error in case it occurred during the testing.)

If the microservice returns ONLY the status when an error occurs, the HTTP body comparison should be skipped!

For further information on how to test REST-based microservices, go to the following section: Section 14.6, “REST-based Microservice Testing”.

3.7.3. Starting the Test Class

To start the tests, You need to run the command:

mvn clean test

After starting, the console will display the following information:

...
[INFO] ------------------------------------------------------------------------
[INFO]  RX-MICRO ANNOTATIONS PROCESSING (1)
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] Current environment context is:
  RxMicro version: 0.10-SNAPSHOT
  Current module:
    examples.quick.start
  Available RxMicro modules:
    ...
  Include packages: <none>
  Exclude packages: <none>
[INFO] Found the following REST controllers:
  io.rxmicro.examples.quick.start.HelloWorldMicroService:
    'GET /' -> sayHelloWorld();
[INFO] Generating java classes...
[INFO] All java classes generated successful in 0.030 seconds. (2)
[INFO] ------------------------------------------------------------------------
[INFO] Annotations processing completed successful.
[INFO] ------------------------------------------------------------------------
...
[INFO] ------------------------------------------------------------------------
[INFO]  RX-MICRO TEST ANNOTATIONS PROCESSING (3)
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] Current environment context is:
  RxMicro version: 0.10-SNAPSHOT
  Current module:
    examples.quick.start
  Available RxMicro modules:
    ...
  Include packages: <none>
  Exclude packages: <none>
[INFO] Generating java classes...
[INFO] Test fixer class generated successfully: rxmicro.$$RestBasedMicroServiceTestFixer (4)
[INFO] All java classes generated successful in 0.009 seconds. (4)
[INFO] ------------------------------------------------------------------------
[INFO] Annotations processing completed successful.
[INFO] ------------------------------------------------------------------------
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
...
[INFO] Fix the environment for REST based microservice test(s)... (5)
[INFO] opens examples.quick.start/rxmicro to ALL-UNNAMED (5)
[INFO] opens examples.quick.start/io.rxmicro.examples.quick.start to ALL-UNNAMED (5)
[INFO] opens examples.quick.start/rxmicro to rxmicro.reflection (5)
[INFO] opens examples.quick.start/io.rxmicro.examples.quick.start to rxmicro.reflection (5)
[INFO] opens rxmicro.rest.server.netty/io.rxmicro.rest.server.netty.local to ALL-UNNAMED (5)
[INFO] opens rxmicro.runtime/io.rxmicro.runtime.local to ALL-UNNAMED (5)
[INFO] opens rxmicro.runtime/io.rxmicro.runtime.local.error to ALL-UNNAMED (5)
[INFO] opens rxmicro.runtime/io.rxmicro.runtime.local.provider to ALL-UNNAMED (5)
[INFO] opens rxmicro.config/io.rxmicro.config.local to ALL-UNNAMED (5)
[INFO] opens rxmicro.rest.server/io.rxmicro.rest.server.local.model to ALL-UNNAMED (5)
[INFO] opens rxmicro.rest.server/io.rxmicro.rest.server.local.component to ALL-UNNAMED (5)
[INFO] opens rxmicro.common/io.rxmicro.common.local to ALL-UNNAMED (5)
[INFO] opens rxmicro.http/io.rxmicro.http.local to ALL-UNNAMED (5)
[INFO] Running io.rxmicro.examples.quick.start.HelloWorldMicroServiceTest (6)
[INFO] ...NettyServer: Server started at 0.0.0.0:38751 using NETTY transport. (7)
[INFO] ...Router: Mapped "GET '/'  onto ...HelloWorldMicroService.sayHelloWorld()
[INFO] ...Router: Mapped "GET '/bad-request'  onto ...BadHttpRequestRestController.handle(...)
[INFO] ...HelloWorldMicroServiceTest: JdkHttpClient released (8)
[INFO] ...NettyServer: Retrieved shutdown request ...
[INFO] ...NettyServer: Server stopped (9)
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 (10)
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
1 Starting of the RxMicro Annotation Processor.
2 RxMicro Annotation Processor has successfully completed its work.
3 Starting of the RxMicro Tests Annotation Processor.
4 RxMicro Tests Annotation Processor has successfully completed its work.
5 For the test configuration, missing exports were automatically added using the capabilities of the java.lang.Module class.
6 REST-based microservice test starting.
7 HTTP server has started automatically on random free port.
8 The resources of the BlockingHttpClient component have been released.
9 HTTP server has stopped successfully.

For further information on how the RxMicro Annotation Processor works for the test environment, go to the following section: Section 14.4, “How It Works”.

3.8. The Project at the Github

The REST-based HelloWorldMicroService microservice project is available at the following link:

The pom.xml configuration file is a reference configuration and can be copied to Your next project using the RxMicro framework.

DON’T FORGET to remove the link to the parent project:

<parent>
    <artifactId>examples</artifactId>
    <groupId>io.rxmicro</groupId>
    <version>0.10-SNAPSHOT</version>
</parent>

and add the properties section describing the versions of the libraries used:

<properties>
    <rxmicro.version>0.10-SNAPSHOT</rxmicro.version> (1)

    <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version> (2)
    <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version> (3)
    <maven-dependency-plugin.version>3.2.0</maven-dependency-plugin.version> (4)
</properties>

4. Core Concepts

This section will describe the basic working concepts of the RxMicro framework.

4.1. How It Works?

The RxMicro framework uses the Java annotation processor, which generates standard code using RxMicro Annotations.

Thus, the RxMicro framework is a framework of declarative programming.

Using the RxMicro framework, the developer focuses on writing the business logic of a microservice. Then he configures the desired standard behavior with RxMicro Annotations. When compiling a project, the RxMicro Annotation Processor generates additional classes. Generated classes contain a standard logic that ensures the functionality of the created microservice.

4.1.1. A Common Work Schema

The common work schema can be presented as follows:

core schema
Figure 9. The RxMicro framework common work schema.

While solving a business task, the developer writes Micro service source code. Then the developer configures the desired standard microservice behavior via RxMicro Annotations. After that, the developer compiles the project.

Since the RxMicro Annotation Processor is configured in maven, when compiling a project this processor handles the source code of the microservice and generates the additional classes: Micro service generated code. After that, the compiler compiles the source and generated microservice codes: Micro service byte code and Micro service generated byte code.

The compiled source and generated codes along with the RxMicro runtime libraries perform useful work.

All RxMicro Annotations are only available during compilation!

The compiled byte code of microservices does not contain RxMicro Annotations:

rxmicro annotations not available in runtime
Figure 10. Decompiled REST-based microservice class.

Therefore, libraries with annotation classes may be missing while REST-based microservice is running.

It is especially important for features which are performed only at the build time (for example, generation of the project documentation).

4.1.2. Generating of Additional Classes.

Let’s have a look at the RxMicro framework common work schema, by the example of the REST-based microservice project, which displays the "Hello World!" message in JSON format.
(This project was considered in the Section 3, “Quick Start” section.)

While implementing a business task (in this example, it’s a task of displaying the "Hello World!" message in JSON format) the developer wrote the following Micro service source code:

micro src

In order to inform the RxMicro framework about the need to generate additional classes by which a written Micro service source code can be built into an HTTP server to handle the desired HTTP requests, the developer added the following RxMicro Annotation:

@GET("/")

Since the RxMicro Annotation Processor is configured in maven:

<configuration>
    <annotationProcessors>
        <annotationProcessor>
            io.rxmicro.annotation.processor.RxMicroAnnotationProcessor
        </annotationProcessor>
    </annotationProcessors>
</configuration>

then when compiling a project this processor handles the source code of the REST-based microservice and generates the Micro service generated code additional classes:

micro gen src

After the source code of additional classes was successfully generated by the RxMicro Annotation Processor, the compiler compiles:

  • REST-based microservice source code in Micro service byte code:

micro byte
  • Generated code of additional classes in Micro service byte code:

micro gen byte

As a result of the compiler’s work, the REST-based microservice byte code and the byte code of the generated additional classes will be stored jointly in the same jar archive:

all byte

For successful start of the compiled classes, the RxMicro runtime libraries are required:

rxmicro runtime libs

The Micro service byte code, Micro service byte code and RxMicro runtime libraries are the program components of microservice, which perform useful work.

Below we will look closely at each generated additional class and what functions it performs.

The names of all classes generated by the RxMicro framework start with the $$ prefix.

4.1.2.1. An Additional Class for the REST Controller.

Any REST-based microservice, contains at least one REST controller. For the simplest project, REST-based microservice and REST controller are the same class.

Therefore, when analyzing such projects, such terms as REST controller, REST-based microservice and microservice are synonymous, because physically they are the same class.

The considered REST-based microservice, which displays the "Hello World!" message, is the simplest project, therefore the HelloWorldMicroService class is a REST controller.

For more information on the differences between REST controller, REST-based microservice and microservice, refer to the {microservice}.html section.

For each REST controller class the RxMicro framework generates an additional class that performs the following functions:

  • Creates a REST controller object.
    (In case of rxmicro.cdi module activation, after creation it also injects the required dependencies.)

  • Creates ModelReader objects that convert HTTP request parameters, headers and body to Java model.

  • Creates ModelWriter objects that convert the Java response model to HTTP response headers and body;

  • Registers all HTTP request handlers of current REST controller in the router.

  • When receiving an HTTP request via the ModelReader object, converts the HTTP request to the Java request model and invokes the corresponding REST controller handler.

  • After receiving the resulting Java response model via the ModelWriter object, converts the Java model into an HTTP response and sends the response to the client.

Such an additional class for the HelloWorldMicroService class is the $$HelloWorldMicroService class:

public final class $$HelloWorldMicroService extends AbstractRestController {

    private HelloWorldMicroService restController;

    private $$ResponseModelWriter responseModelWriter;

    @Override
    protected void postConstruct() {
        restController = new HelloWorldMicroService(); (1)
        responseModelWriter =
                new $$ResponseModelWriter(restServerConfig.isHumanReadableOutput()); (2)
    }

    @Override
    public void register(final RestControllerRegistrar registrar) { (3)
        registrar.register(
                this,
                new Registration(
                        "",
                        "sayHelloWorld()",
                        this::sayHelloWorld, (4)
                        false,
                        new ExactUrlRequestMappingRule( (5)
                                "GET",
                                "/",
                                false
                        )
                )
        );
    }

    private CompletionStage<HttpResponse> sayHelloWorld(final PathVariableMapping mapping,
                                                        final HttpRequest request) {
        final HttpHeaders headers = HttpHeaders.of();
        return restController.sayHelloWorld() (6)
                .thenApply(response -> buildResponse(response, 200, headers)); (7)
    }

    private HttpResponse buildResponse(final Response model,
                                       final int statusCode,
                                       final HttpHeaders headers) {
        final HttpResponse response = httpResponseBuilder.build();
        response.setStatus(statusCode);
        response.setOrAddHeaders(headers);
        responseModelWriter.write(model, response); (8)
        return response;
    }

}
1 The $$HelloWorldMicroService component creates an instance of the REST controller class.
2 The $$HelloWorldMicroService component creates an instance of the ModelWriter that converts the Java response model to the HTTP response headers and body.
3 The $$HelloWorldMicroService component registers all HTTP request handlers of the current REST controller.
4 The registration object contains a reference to the HTTP request handler of the current REST controller.
5 The registration object contains a rule, according to which the router determines whether to invoke this HTTP request handler.
6 When receiving HTTP request, the $$HelloWorldMicroService invokes REST controller method.
7 After invoking the REST controller method, an asynchronous result handler is added.
(When using the reactive approach, the current thread cannot be blocked, so the thenApply method is used for delayed result handling.)
8 After receiving the Java response model object, the result handler creates an HTTP response based on the data received from the model, which is subsequently sent to the client.
4.1.2.2. An ModelWriter Class.

To convert a Java model to an HTTP response, You will need a separate component that performs the following functions:

  • Defines in what format to return an HTTP response depending on the project settings.

  • Creates converter objects that support the specified messaging format.

  • When converting a Java model to an HTTP response, manages the conversion process by delegating invocations to the appropriate components.

Such a separate component for the Response model class is the $$ResponseModelWriter class:

The code of the $$ResponseModelWriter generated class depends on the response model class structure, and the format used for message exchange with the client.

Since the format of message exchange with the client is set in module-info.java of the project (requires rxmicro.rest.server.exchange.json;), and is the configuration for all REST controllers and all their handlers, then within the current project, the $$ResponseModelWriter will depend only on the response model class structure.

Therefore, if several handlers from different REST controllers will return the Response class model, only one $$ResponseModelWriter class will be generated. As a result, in each additional REST controller class, the instance of this class will be used.

public final class $$ResponseModelWriter extends ModelWriter<Response> {

    private final $$ResponseModelToJsonConverter responseModelToJsonConverter; (1)

    private final ExchangeDataFormatConverter<Object> exchangeDataFormatConverter; (2)

    private final String outputMimeType;

    public $$ResponseModelWriter(final boolean humanReadableOutput) {
        exchangeDataFormatConverter =
            new JsonExchangeDataFormatConverter(humanReadableOutput); (3)
        responseModelToJsonConverter = new $$ResponseModelToJsonConverter();
        outputMimeType = exchangeDataFormatConverter.getMimeType();
    }

    @Override
    public void write(final Response model,
                      final HttpResponse response) {
        response.setHeader(HttpHeaders.CONTENT_TYPE, outputMimeType); (4)
        final Map<String, Object> json = responseModelToJsonConverter.toJsonObject(model); (5)
        response.setContent(exchangeDataFormatConverter.toBytes(json)); (6)
    }

}
1 Since the JSON message exchange format is specified in the settings, a component that can convert the Java response model to a JSON response model is required. (This task is specific for each response model, so to avoid using reflection, You need to generate a separate converter component.)
2 To convert any low-level model (in this example, it’s a JSON response model) into a byte array, You also need a separate converter component.
3 Since the JSON messaging format is specified in the settings, it is assumed that the JSON model will be converted to an byte array, which will be created from the Java response model.
4 Since the JSON message exchange format is specified in the settings, it is necessary to set the HTTP header: Content-Type = application/json.
5 When the HTTP response is formed, it is necessary to convert Java response model to JSON model.
6 The last step is to convert the JSON model to a byte array, that will be written to the HTTP response body.
4.1.2.3. A Java Model Converter.

To avoid using reflection, You need a component that can convert Java model to JSON model.

This component must support the following functions:

  • Convert Java model to JSON model of any complexity.

  • Support all possible class field access models to be an all-purpose tool.
    (Supported class field access models are described in details in the Section 4.7, “Encapsulation”.)

Such a separate component for the Response model class is the $$ResponseModelToJsonConverter class:

public final class $$ResponseModelToJsonConverter extends ModelToJsonConverter<Response> {

    @Override
    (1)
    public Map<String, Object> toJsonObject(final Response model) {
        final JsonObjectBuilder builder = new JsonObjectBuilder();
        putValuesToBuilder(model, builder);
        return builder.build();
    }

    public void putValuesToBuilder(final Response model,
                                   final JsonObjectBuilder builder) {
        builder.put("message", model.message); (2)
    }
}
1 JSON object is presented as Map<String, Object>.
(More information about JSON format support by the RxMicro framework can be found in the Section 4.10, “JSON”.)
2 The value of the message field is read from the Java model by direct reference to the field.
(Supported class field access models are described in details in the Section 4.7, “Encapsulation”.)
4.1.2.4. An Aggregator of the REST Controllers.

To integrate developer code into the RxMicro framework infrastructure, You need aggregators.

The aggregators perform the following functions:

  • Register all generated additional classes for REST controllers;

  • Customize the runtime environment;

The aggregators are invoked by the RxMicro framework using reflection.
(That' s why aggregators have a permanent and predefined names and are located in the special package: rxmicro.)

An Aggregator of the REST Controllers for any project is always the rxmicro.$$RestControllerAggregatorImpl class:

package rxmicro; (1)

public final class $$RestControllerAggregatorImpl extends RestControllerAggregator { (2)

    static {
        $$EnvironmentCustomizer.customize(); (3)
    }

    protected List<AbstractMicroService> listAllRestControllers() {
        return List.of(
                new io.rxmicro.examples.quick.start.$$HelloWorldMicroService() (4)
        );
    }

}
1 All aggregators are always generated in the special package: rxmicro.
2 The predefined name of the REST controller aggregator class is always $$RestControllerAggregatorImpl.
3 When the aggregator class is loaded by the RxMicro framework, the component of the current environment customization is invoked.
4 The aggregator registers all generated additional classes for REST controllers;
4.1.2.5. An Environment Customizer.

Java 9 has introduced the JPMS.

This system requires that a developer configures access to classes in the module-info.java file of the microservice project.

To enable the RxMicro framework to load aggregator classes, You must export the rxmicro package to the rxmicro.reflection module:

module examples.quick.start {
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;

    exports rxmicro to rxmicro.reflection; (1)
}
1 Allow access of reflection util classes from the rxmicro.reflection module to all classes from the rxmicro package.

But the rxmicro package is created automatically and after deleting all the generated files, it won’t be possible to compile the module-info.java because of the following error:

package is empty or does not exist: rxmicro.

To solve this problem, the RxMicro framework generates the rxmicro.$$EnvironmentCustomizer class:

package rxmicro; (1)

final class $$EnvironmentCustomizer {

    static {
        exportTheRxmicroPackageToReflectionModule(); (2)
        // All required customization must be here
    }

    public static void customize() {
        //do nothing. All customization is done at the static section
    }

    private static void exportTheRxmicroPackageToReflectionModule() {
        final Module currentModule = $$EnvironmentCustomizer.class.getModule();
        currentModule.addExports("rxmicro", RX_MICRO_REFLECTION_MODULE); (3)
    }

    private $$EnvironmentCustomizer() {
    }
}
1 All customizers are always generated in the special package: rxmicro.
2 When the class is loaded, the exportTheRxmicroPackageToReflectionModule() static method is invoked.
3 In this method body, the export of the rxmicro package to the rxmicro.reflection module is performed dynamically using the capabilities of the java.lang.Module class.

Due to this additional class, all necessary settings for the JPMS are created automatically.

If the RxMicro framework needs additional automatic settings for its correct work, these settings will be automatically added by the RxMicro Annotation Processor to the rxmicro.$$EnvironmentCustomizer class.

4.1.3. Using the Debug Mode

To get a better idea of how the RxMicro framework works, You can use the debug mode.

To do this, set breakpoints and start the microservice in debug mode.

micro service start debug
Figure 11. Starting the microservice in debug mode.

The code generated by the RxMicro Annotation Processor is a usual source code. So You can use breakpoints for this code as well as for Your source code.

micro service handle request debug
Figure 12. HTTP request handling by microservice in debug mode.

4.2. RxMicro Annotation Processor Options

The RxMicro Annotation Processor supports the following options:

Table 1. Options supported by the RxMicro Annotation Processor.
Option Description Type Default value

RX_MICRO_MAX_JSON_NESTED_DEPTH

maximum stack size for recursive invocations when analyzing models containing JSON nested objects.

positive int

20

RX_MICRO_LOG_LEVEL

RxMicro Annotation Processor logging level.

Enum {OFF, INFO, DEBUG}

INFO

RX_MICRO_DOC_DESTINATION_DIR

the resulting directory for generated documentation.

String

Asciidoc: ./src/main/asciidoc

RX_MICRO_BUILD_UNNAMED_MODULE

the unnamed module support for a microservice project.

boolean

false

RX_MICRO_DOC_ANALYZE_PARENT_POM

this option allows analyzing parent pom.xml if child pom.xml does not contain required property.

boolean

true

RX_MICRO_STRICT_MODE

activates additional validation rules during compilation process.

The RxMicro team strong recommends enabling the strict mode for your production code.

boolean

false

These options are set using the compiler arguments in maven-compiler-plugin:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version>
    <configuration>
        <release>11</release>
        <compilerArgs>
            <arg>-ARX_MICRO_MAX_JSON_NESTED_DEPTH=20</arg>
            <arg>-ARX_MICRO_LOG_LEVEL=INFO</arg>
            <arg>-ARX_MICRO_DOC_DESTINATION_DIR=./src/main/asciidoc</arg>
            <arg>-ARX_MICRO_BUILD_UNNAMED_MODULE=false</arg>
            <arg>-ARX_MICRO_DOC_ANALYZE_PARENT_POM=true</arg>
            <arg>-ARX_MICRO_STRICT_MODE=false</arg>
        </compilerArgs>
    </configuration>
</plugin>

Note that it is necessary to add the -A prefix before setting the value of the option.

The common format is as follows: -A${name}=${value}. For example: -ARX_MICRO_LOG_LEVEL=OFF

4.3. Don’t Block Current Thread!

In modern computer architecture, IO operations are the slowest ones. As a result, when using multithreading programming model, the use of CPU and RAM is extremely inefficient. For a single-user monolithic application such inefficiency is imperceptible. But for a multi-user distributed application with a much higher number of IO operations, this problem generates huge financial costs for additional hardware and coordination between client data streams (Read more: C10k problem).

Therefore, the Event-driven architecture (EDA) is used for efficient use of the hardware resources of a multi-user distributed application.

The most popular Java framework that uses Event-driven architecture for IO operations is Netty. To write efficient programs using Netty, it is necessary to comply with certain rules and restrictions. The most important of these is the following requirement: Don’t block current thread!

The RxMicro framework is a framework that runs on Netty. Therefore, all requirements for applications that utilize Netty also cover the RxMicro framework.

4.3.1. Prohibited Operations

Consequently, when writing microservice applications based on the RxMicro framework, the following operations are prohibited:

  • Data reading from a socket or file in a blocking style using java.io.InputStream and child classes.

  • Data writing to a socket or file in a blocking style using java.io.OutputStream and child classes.

  • Interaction with a database using of the blocking driver (all JDBC drivers).

  • Waiting on a lock or a monitor (java.util.concurrent.locks.Lock, Object.wait).

  • Putting the thread into sleep mode (Thread.sleep, TimeUnit.sleep).

  • Any other blocking operations.

The absence of blocking operations in the microservice allows handling many concurrent connections, using a small number of threads and, as a result, to effectively use hardware resources of the computer.

Therefore, when designing microservices via the RxMicro framework, You must follow by the following rule:

When implementing a microservice, if the result can be obtained immediately, it must be returned immediately.

Otherwise, You must return Publisher or CompletableFuture, which will generate the result later.

4.4. Reactive Libraries Support

The RxMicro framework supports the following reactive programming libraries:

4.4.1. Expected Business Process Results

When writing reactive programs, the following 4 expected results of any business process are possible:

  1. It is important to complete the business process, but the result is missing or unimportant.

  2. The business process returns the result in a single instance or nothing.

  3. The business process returns the required result in a single instance.

  4. The business process returns the result as (0 .. n) object stream.

When writing a business service using reactive libraries, it is recommended to comply with the following agreements:

Table 2. Which class from a reactive library must be choose?
Reactive Library java.util.concurrent Project Reactor RxJava3

Without result

CompletableFuture<Void> CompletionStage<Void>

Mono<Void>

Completable

Optional result

CompletableFuture<Optional<MODEL>> CompletionStage<Optional<MODEL>>

Mono<MODEL>

Maybe<MODEL>

Required result

CompletableFuture<MODEL> CompletionStage<MODEL>

Mono<MODEL>

Single<MODEL>

Stream result

CompletableFuture<List<MODEL>> CompletionStage<List<MODEL>>

Flux<MODEL>, Mono<List<MODEL>>

Flowable<MODEL>, Single<List<MODEL>>

The following types of results Flux<MODEL> and Mono<List<MODEL>>, as well as Flowable<MODEL>, Single<List<MODEL>> are not absolutely equivalent!

For the Flux<MODEL> and Flowable<MODEL> types, the result handler can be invoked before all data is received from a data source, e.g. from a database.

Whereas for Mono<List<MODEL>> and Single<List<MODEL>> the result handler is invoked only after all the data is received from the data source!

4.4.2. Recommendations for Choosing a Library

General recommendation for choosing a reactive programming library when using the RxMicro framework:

  1. If Your microservice contains simple logic, You can use the lightweight and Java-provided java.util.concurrent library, represented by the CompletableFuture class and the CompletionStage interface.

  2. If Your microservice contains more complex logic, to describe which You need to use complex operators, it is recommended to choose Project Reactor or RxJava.

  3. When choosing between the Project Reactor and RxJava follow the recommendations:

    1. If You are more familiar with the Project Reactor, then use it, otherwise use RxJava.

    2. If You need r2dbc based reactive SQL repositories (rxmicro.data.sql.r2dbc module), then use the Project Reactor.
      (Since r2dbc drivers already use the Project Reactor.)

Thus, when writing microservices via the RxMicro framework, You can use any Java reactive programming library that You prefer!

FYI All libraries support a blocking getting of the result:

public final class BlockingGetResult {

    public static void main(final String[] args) {
        final String blockingResult1 =
                CompletableFuture.completedFuture("Hello")
                        .join();

        final String blockingResult2 =
                Mono.just("Hello")
                        .block();

        final String blockingResult3 =
                Single.just("Hello")
                        .blockingGet();

        System.out.println(blockingResult1);
        System.out.println(blockingResult2);
        System.out.println(blockingResult3);
    }

}

The main need in blocking getting of the result, using reactive programming, arises at Unit testing implementation. Since the main popular frameworks for unit testing (JUnit, TestNG, etc.) use the usual thread (blocking) programming model.

4.5. Base Model

Java applications use java.lang.Object.toString() method very often for debug purposes. Thus a developer must override this method for all model classes in his(her) project.

To help with overriding of this method for all model classes the RxMicro framework introduces the BaseModel class. This class uses the reflection to generate string representation of the model class on fly.

public class CustomModel extends BaseModel {

    String string;

    Integer integer;

    //...

    //toString method not necessary!
}

The reflection mechanism is slow one, so use the generated string representation of the model class only for debug purposes!

According to JPMS requirements all reflection access must be configured at the module-info.java descriptor using exports or opens` instructions. It means that for correct usage the BaseModel feature, it is necessary to add the following instruction:

opens io.rxmicro.examples.base.model.model.package4 to
        rxmicro.reflection;

where io.rxmicro.examples.base.model.model.package4 is the package that contains custom model classes.

But the BaseModel feature is designed for debug purposes, so required exports or opens instructions are added automatically via generated $$EnvironmentCustomizer class if You do not add these instructions manually! So You can use the BaseModel feature without any module-info.java modifications for Your project!

4.6. Strings Formatting

While developing a software product it is necessary to format strings.

For this purpose, Java provides different approaches:

Mono<? extends Result> executeQuery(final Connection connection,
                                    final Long id) {
    final String sql = "SELECT * FROM account WHERE id = $1"; (1)
    SLF4J_LOGGER.info("SQL: {}", sql); (2)
    return Mono.from(connection.createStatement(sql)
            .bind(0, id)
            .execute())
            .onErrorResume(e -> Mono.error(
                    new IllegalArgumentException(
                            String.format(
                                    "SQL '%s' contains syntax error: %s", sql, e.getMessage() (3)
                            ))
                    )
            );
}
1 To generate an SQL query, You need to use $1 placeholder.
(This placeholder depends on the used R2DBC driver. For postgresql, it’s a $1 symbol.)
2 To generate a logging message, You need to use {} placeholder.
(This placeholder depends on the logging library used. For SLF4J, it’s a {} symbol.)
3 To generate an error message, You need to use %s placeholder from a separate utility class, for example String.format.

While writing the code, a developer can easily confuse the required placeholder.

To avoid such a problem, the RxMicro framework recommends using the universal ? placeholder

Mono<? extends Result> executeQuery(final Connection connection,
                                    final Long id) {
    final String sql = "SELECT * FROM account WHERE id = ?"; (1)
    RX_MICRO_LOGGER.info("SQL: ?", sql); (2)
    return Mono.from(connection.createStatement(sql)
            .bind(0, id)
            .execute())
            .onErrorResume(e -> Mono.error(
                    new InvalidStateException(
                            "SQL '?' contains syntax error: ?", sql, e.getMessage()) (3)
                    )
            );
}
1 To generate an SQL query, You need to use ? placeholder.
2 To generate a logging message, You need to use ? placeholder.
3 To generate an error message, You need to use ? placeholder.

4.7. Encapsulation

When designing Java request and response models, there is a need to protect data from unauthorized modification.

4.7.1. A private Modifier Usage

The standard solution to this problem in Java is using the private modifier:

final class Response {

    private final String message; (1)

    Response(final String message) {
        this.message = requireNonNull(message);
    }
}
1 By declaring the message field as private, the developer allows access to this field only from inside the class.

To violate encapsulation principles when necessary, Java provides powerful reflection mechanism.

The RxMicro framework is aware of this mechanism, so when generating a converter, the framework uses it:

import static rxmicro.$$Reflections.getFieldValue; (1)

public final class $$ResponseModelToJsonConverter extends ModelToJsonConverter<Response> {

    @Override
    public Map<String, Object> toJsonObject(final Response model) {
        final JsonObjectBuilder builder = new JsonObjectBuilder();
        putValuesToBuilder(model, builder);
        return builder.build();
    }

    public void putValuesToBuilder(final Response model,
                                   final JsonObjectBuilder builder) {
        builder.put("message", getFieldValue(model, "message")); (2)
    }
}
1 Static import of method that allows reading field value with reflection.
2 Reading the value of the message field from the response model.

Using reflection when converting from Java model to JSON model while processing each request can reduce microservice performance, where this problem can be avoided. Therefore, when compiling this class, the RxMicro Annotation Processor generates a warning message:

[WARNING] Response.java:[27,26] PERFORMANCE WARNING: To read a value from io.rxmicro.example.hello.world.Response.message rxmicro will use the reflection.
It is recommended to add a getter or change the field modifier: from private to default, protected or public.

If the RX_MICRO_STRICT_MODE is set, the RxMicro Annotation Processor throws a compilation error instead of showing the PERFORMANCE WARNING.

By default the reflection usage for model classes is not allowed for strict mode!

4.7.2. A Separate Package Usage

The best and recommended solution to this problem is to create a separate package (e.g. model) and declare all fields of the model classes without any access modifier (i.e. default/package). Under this approach, fields can be accessed only from classes of the same package. And the package contains only classes of models without any business logic:

model package
Figure 13. Recommended structure to support encapsulation in Java models.

Using model class fields without any access modifier (i.e. default/package) allows You to generate a converter that can read or write a value using direct access to the field by . operator.

4.7.3. A getters Usage

If the simplest logic is required when reading a value from a model field, You can use getter.

To do this, declare the field as private and add getter:

public final class Response {

    private final String message;

    public Response(final String message) {
        this.message = requireNonNull(message);
    }

    public String getMessage() {
        if (message.isEmpty()) {
            return "<Empty>";
        } else {
            return message;
        }
    }
}

In this case, getter will be used in the generated converter to get the result:

public final class $$ResponseModelToJsonConverter extends ModelToJsonConverter<Response> {

    @Override
    public Map<String, Object> toJsonObject(final Response model) {
        final JsonObjectBuilder builder = new JsonObjectBuilder();
        putValuesToBuilder(model, builder);
        return builder.build();
    }

    public void putValuesToBuilder(final Response model,
                                   final JsonObjectBuilder builder) {
        builder.put("message", model.getMessage()); (1)
    }
}
1 getter invoking to get the value of the response model field.

4.7.4. Performance Comparison

Performance test source code:

@Warmup(iterations = 5)
@Measurement(iterations = 5)
@Fork(value = 2, jvmArgsAppend = "-server")
@BenchmarkMode({
        Mode.Throughput,
        Mode.AverageTime,
        Mode.SingleShotTime
})
@State(Scope.Benchmark)
@OutputTimeUnit(MILLISECONDS)
@Threads(1)
public class ReadWriteFieldBenchmark {

    private final CustomClass customClass = new CustomClass("text");

    private final Map<Class<?>, Map<String, Field>> cache = new HashMap<>();

    public ReadWriteFieldBenchmark() {
        try {
            final Field field = CustomClass.class.getDeclaredField("value");
            if (!field.canAccess(customClass)) {
                field.setAccessible(true);
            }
            cache.put(CustomClass.class, new HashMap<>(Map.of("value", field)));
        } catch (final NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    @Benchmark
    public void readDirectField() {
        var v = customClass.value;
    }

    @Benchmark
    public void writeDirectField() {
        customClass.value = "string";
    }

    @Benchmark
    public void readUsingGetter() {
        var v = customClass.getValue();
    }

    @Benchmark
    public void writeUsingSetter() {
        customClass.setValue("string");
    }

    @Benchmark // read field value using reflection with field search at the cache
    public void readUsingReflection() throws IllegalAccessException {
        var v = cache.get(CustomClass.class).get("value").get(customClass);
    }

    @Benchmark // write field value using reflection with field search at the cache
    public void writeUsingReflection() throws IllegalAccessException {
        cache.get(CustomClass.class).get("value").set(customClass, "string");
    }
}

Performance test results:

Benchmark                                       Mode  Cnt       Score       Error   Units
ReadWriteFieldBenchmark.readDirectField         thrpt   10  753348.188 ± 90286.947  ops/ms
ReadWriteFieldBenchmark.readUsingGetter         thrpt   10  764112.155 ± 94525.371  ops/ms
ReadWriteFieldBenchmark.readUsingReflection     thrpt   10   26241.478 ±  3838.172  ops/ms
ReadWriteFieldBenchmark.writeDirectField        thrpt   10  344623.904 ± 18961.759  ops/ms
ReadWriteFieldBenchmark.writeUsingReflection    thrpt   10   19430.735 ±  2135.813  ops/ms
ReadWriteFieldBenchmark.writeUsingSetter        thrpt   10  323596.205 ± 28416.707  ops/ms
ReadWriteFieldBenchmark.readDirectField          avgt   10      ≈ 10⁻⁶               ms/op
ReadWriteFieldBenchmark.readUsingGetter          avgt   10      ≈ 10⁻⁶               ms/op
ReadWriteFieldBenchmark.readUsingReflection      avgt   10      ≈ 10⁻⁴               ms/op
ReadWriteFieldBenchmark.writeDirectField         avgt   10      ≈ 10⁻⁶               ms/op
ReadWriteFieldBenchmark.writeUsingReflection     avgt   10      ≈ 10⁻⁴               ms/op
ReadWriteFieldBenchmark.writeUsingSetter         avgt   10      ≈ 10⁻⁶               ms/op
ReadWriteFieldBenchmark.readDirectField            ss   10       0.001 ±     0.001   ms/op
ReadWriteFieldBenchmark.readUsingGetter            ss   10       0.001 ±     0.001   ms/op
ReadWriteFieldBenchmark.readUsingReflection        ss   10       0.008 ±     0.005   ms/op
ReadWriteFieldBenchmark.writeDirectField           ss   10       0.002 ±     0.001   ms/op
ReadWriteFieldBenchmark.writeUsingReflection       ss   10       0.011 ±     0.008   ms/op
ReadWriteFieldBenchmark.writeUsingSetter           ss   10       0.001 ±     0.001   ms/op

Test results show that reading/writing by a direct reference or via getter/setter is performed 28/17 times faster, than when using reflection!

4.7.5. Approach Selection Recommendations

Thus, the RxMicro framework uses the following algorithm to read (write) from the fields of the Java model:

  1. If the field is declared with public, protected or default/package modifiers, the generated converter uses direct access to the field using the . operator;

  2. If the field is declared with the private modifier and the developer created getter/setter, the generated converter uses the getter/setter invocation to get or write the field value;

  3. If the field is declared with the private modifier without getter/setter declaration, the generated converter uses reflection to access the model field.
    (When generating this type of converter the RxMicro Annotation Processor informs the developer about PERFORMANCE WARNING.)

To benefit from the encapsulation advantages when designing microservices via the RxMicro framework, follow the recommendations:

When creating request and response models, use a separate package and default/package access modifier!

If the simplest logic of reading (writing) the value of the model field is required, use getter (setter) and private field access modifier.

Do not use the private modifier to access the model field without getter (setter)!
This approach offers no benefits!

If the RX_MICRO_STRICT_MODE is set, the RxMicro Annotation Processor throws a compilation error instead of showing the PERFORMANCE WARNING.

By default the reflection usage for model classes is not allowed for strict mode!

4.8. Configuration

The RxMicro framework provides the rxmicro.config module for flexible configuration of microservices to any environment. This module provides the following features:

  • Support for different types of configuration sources: files, classpath resources, environment variables, command line arguments, etc.;

  • Inheritance and redefinition of settings from different configuration sources;

  • Changing the order in which the configuration sources are read;

  • Configuration using annotations and Java classes.

4.8.1. Basic Structure of the Config Module

Each class that extends the Config abstract class is a configuration class.

Each configuration class defines its own namespace. Each namespace clearly defines the corresponding configuration class. The namespace is necessary to set the settings of the configuration class fields in text form. (Further details will be described below.)

To work with configurations, the RxMicro framework provides the Configs configuration manager.

To read the current settings, You must use the Configs.getConfig(Class<T>) method:

public final class ReadConfigMicroService {

    @GET("/")
    void readHttpServerPort() {
        final HttpServerConfig config = getConfig(HttpServerConfig.class); (1)
        System.out.println("HTTP server port: " + config.getPort());
    }

    public static void main(final String[] args) {
        startRestServer(ReadConfigMicroService.class);
    }
}
1 Getting the current HTTP server configuration using the Configs.getConfig static method.

To change the standard configuration settings, You must use the Configs.Builder class:

public final class CustomizeConfigMicroService {

    @GET("/")
    void test() {
        // do something
    }

    public static void main(final String[] args) {
        new Configs.Builder()
                .withConfigs(new HttpServerConfig()
                        .setPort(9090)) (1)
                .build(); (2)
        startRestServer(CustomizeConfigMicroService.class); (3)
    }
}
1 Setting the HTTP server custom port.
2 Creating the configuration manager object.
3 REST-based microservice should be started after configuration manager settings, otherwise changes will not take effect.

Each subsequent invocation of the Configs.Builder.build() method overrides all configuration manager settings. (In any microservice project there is only one configuration manager object!)

It means that if the developer creates several Configs.Builder instances, it will be the last invocation of the build method that matters, the others will be ignored.

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.

Settings customization via the Configs.Builder.build() is one of the types of configuration. This type of configuration is called configuration using Java classes.

4.8.2. Configuration Types

The RxMicro framework supports the following configuration types:

  1. Configuration using classpath resources.

  2. Configuration using properties files.

  3. Configuration using environment variables.

  4. Configuration using Java system properties.

  5. Configuration using Java classes.

  6. Configuration using Java annotations.

  7. Configuration using command line arguments.

4.8.2.1. Configuration Using classpath Resources.

The RxMicro framework supports shared and separate classpath resources for the external configuration in relation to the microservice source code.

The only supported classpath resource format is the properties file format.

4.8.2.1.1. Configuration Using Separate classpath Resource.

If the classpath of the current project contains the http-server.properties resource with the following content:

port=9090 (1)
1 Custom port for HTTP server.

then the RxMicro framework when reading the HttpServerConfig class configuration will read this resource:

2020-01-11 12:44:27.518 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (1)
1 The HTTP server has started at the 9090 port instead of the standard 8080 port.

The http-server name is the default namespace for the HttpServerConfig class. That’s why these settings are automatically applied when requesting the HttpServerConfig class configuration.

The default namespace for the configuration class is calculated using the Config.getDefaultNameSpace(Class<? extends Config>) method,

4.8.2.1.2. Configuration Using Shared classpath Resource.

If the classpath of the current project contains the rxmicro.properties resource with the following content:

http-server.port=9090 (1)
1 Custom port for HTTP server.

then the RxMicro framework when reading the HttpServerConfig class configuration will read this resource:

2020-01-11 12:44:27.518 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (1)
1 The HTTP server has started at the 9090 port instead of the standard 8080 port.

The rxmicro name for the properties file is constant. That’s why when requesting any configuration, the RxMicro framework tries to read the content of this file, if it exists.

The rxmicro name is a named constant: Config.RX_MICRO_CONFIG_FILE_NAME,

Since the rxmicro.properties resource is a shared resource for any configuration, You must specify a namespace when specifying the settings.

That’s why when specifying the HTTP server port, You should specify the http-server prefix in the rxmicro.properties file. (When using the http-server.properties file, there was no such need!)

That means

http-server.port=9090

instead of

port=9090
4.8.2.2. Configuration Using properties Files.

Similar to classpath resources, the RxMicro framework also supports shared and separate properties files for the external configuration in relation to the microservice source code.

Configuration files can be located:

  • in the current directory in relation to the microservice;

  • in the $HOME directory:

    • for Linux platform the $HOME directory is /home/$USERNAME;

    • for MacOS platform the $HOME directory is /Users/$USERNAME;

    • for Windows platform the $HOME directory is C:\Documents and Settings\%USERNAME% or C:\Users\%USERNAME%.

  • in the default rxmicro config directory: $HOME/.rxmicro/ (predefined name and location).
    (Using $HOME/.rxmicro/ directory instead of $HOME one allows configuring this directory as docker or kubernetes volume.)

To find out the location of the $HOME directory on Your computer using Java, start the following program:

public final class GetHomeDirectory {

    public static void main(final String[] args) {
        System.out.println(System.getProperty("user.home"));
    }
}

By default, the function of searching and reading configuration files is disabled in the RxMicro framework!

To activate this function, You must use the Configs.Builder class:

new Configs.Builder()
        .withAllConfigSources() (1)
        .build();
1 Activation of all available configuration sources for the current microservice.

Besides activating all available sources, it is possible to activate only configuration files in a given location.

For details on how to do this, go to the Section 4.8.3.2, “Custom Reading Order of Config Sources.”.

4.8.2.2.1. Configuration Using Separate properties File.

If the current directory (or $HOME, or $HOME/.rxmicro/) directory contains the http-server.properties file with the following content

port=9090 (1)
1 Custom port for HTTP server.

then the RxMicro framework when reading the HttpServerConfig class configuration will read this file:

2020-01-11 12:44:27.518 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (1)
1 The HTTP server has started at the 9090 port instead of the standard 8080 port.

The http-server name is the default namespace for the HttpServerConfig class. That’s why these settings are automatically applied when requesting the HttpServerConfig class configuration.

The default namespace for the configuration class is calculated using the Config.getDefaultNameSpace(Class<? extends Config>) method,

4.8.2.2.2. Configuration Using Shared properties File.

If the current directory (or $HOME, or $HOME/.rxmicro/) directory contains the rxmicro.properties file with the following content

http-server.port=9090 (1)
1 Custom port for HTTP server.

then the RxMicro framework when reading the HttpServerConfig class configuration will read this resource:

2020-01-11 12:44:27.518 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (1)
1 The HTTP server has started at the 9090 port instead of the standard 8080 port.

The rxmicro name for the properties file is constant. That’s why when requesting any configuration, the RxMicro framework tries to read the content of this file, if it exists.

The rxmicro name is a named constant: Config.RX_MICRO_CONFIG_FILE_NAME,

Since the rxmicro.properties file is a shared file for any configuration, You must specify a namespace when specifying the settings.

That’s why when specifying the HTTP server port, You should specify the http-server prefix in the rxmicro.properties file. (When using the http-server.properties file, there was no such need!)

That means

http-server.port=9090

instead of

port=9090
4.8.2.3. Configuration Using Environment Variables.

When using environment variables, the format of configurations matches the following format:

export ${name-space}.${property-name} = ${value}:

export http-server.port=9090 (1)

java -p ./classes:lib -m examples.quick.start/io.rxmicro.examples.quick.start.HelloWorldMicroService
2020-01-02 18:49:58.372 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (2)
1 Setting the http-server.port environment variable = 9090 (custom port for HTTP server).
2 The HTTP server has started at the 9090 port instead of the standard 8080 port.

Thus, the format of configurations using environment variables corresponds to the format of rxmicro.properties file or classpath resource.

Allowed characters in environment variable names!

These strings have the form name=value; names shall not contain the character '='. For values to be portable across systems conforming to IEEE Std 1003.1-2001, the value shall be composed of characters from the portable character set (except NUL and as indicated below).

So names may contain any character except = and NUL, but:

Environment variable names used by the utilities in the Shell and Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase letters, digits, and the '' (underscore) from the characters defined in Portable Character Set and do not begin with a digit. Other characters may be permitted by an implementation; applications shall tolerate the presence of such names._

So for such restricted environment the RxMicro framework supports the following format for environment variable mapping as well:

export HTTP_SERVER_PORT=9090 (1)

java -p ./classes:lib -m examples.quick.start/io.rxmicro.examples.quick.start.HelloWorldMicroService
2020-01-02 18:49:58.372 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (2)
1 Setting the HTTP_SERVER_PORT environment variable = 9090 (custom port for HTTP server).
2 The HTTP server has started at the 9090 port instead of the standard 8080 port.

Configuring with environment variables is very convenient when using docker containers!

To protect Your secret data, use configuration via properties files instead of environment variables. The config directory with secret config files (for example $HOME/.rxmicro/) must be mount as external volume using tmpfs file system.

If it is necessary to separate environment variables used for the configuration of the RxMicro environment from other environment variables, You must define the standard environment variable with name RX_MICRO_CONFIG_ENVIRONMENT_VARIABLE_PREFIX!

For example if You runtime contains the following environment variable RX_MICRO_CONFIG_ENVIRONMENT_VARIABLE_PREFIX=MY_RUNTIME_ than it is necessary to use

export MY_RUNTIME_HTTP_SERVER_PORT=9090

instead of

export HTTP_SERVER_PORT=9090

setting for configuring a port for the HTTP server!

4.8.2.4. Configuration Using Java System Properties

When using the Java System Properties, the format of configurations matches the following format:

${name-space}.${property-name} = ${value}:

java -p ./classes:lib \
    -Dhttp-server.port=9090 \ (1)
    -m examples.quick.start/io.rxmicro.examples.quick.start.HelloWorldMicroService

2020-01-02 18:49:58.372 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (2)
1 Setting the http-server.port Java system variable = 9090 (custom port for HTTP server).
2 The HTTP server has started at the 9090 port instead of the standard 8080 port.

Thus, the format of configurations using Java system variables corresponds to the format of configuration using environment variables, and also to the format of rxmicro.properties file or classpath resource.

4.8.2.5. Configuration Using Java Classes.

Configuring with Java classes is the easiest and most explicit configuration method:

public static void main(final String[] args) {
    new Configs.Builder()
            .withConfigs(new HttpServerConfig()
                    .setPort(9090)) (1)
            .build();
    startRestServer(MicroService.class);
}
1 Changing the HTTP server port.
2020-01-02 18:49:58.372 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:9090 using NETTY transport in 500 millis. (1)
1 The HTTP server has started at the 9090 port instead of the standard 8080 port.

The main difference between this type of configuration and the others is that when using Java classes, other configuration sources are always ignored!

Therefore this type is recommended to be used ONLY for test purposes!

(It does not have enough flexibility for the production environment!)

For the production environment, use the configuration with annotations instead of the configuration with Java classes!

4.8.2.6. Configuration Using Java Annotations.

To override the default value, the RxMicro framework provides the @DefaultConfigValue annotation.

@DefaultConfigValue(
        configClass = HttpServerConfig.class,   (1)
        name = "port",                          (2)
        value = "9090"
)
@DefaultConfigValue(
        name = "http-server.host",  (3)
        value = "localhost"
)
module examples.config.annotations { (4)
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;
}
1 When overriding the configuration value, You must specify the configuration class.
2 If the configuration class is specified, the namespace may not be specified.
(It means the field of the specified configuration class.)
3 If no configuration class is specified, the name must contain a namespace.
(The namespace allows You to clearly define to which configuration class the specified setting belongs.)
4 When configuring a microservice project, the annotation must be specified on the module-info.java descriptor.
(A microservice project may contain several classes of REST controllers, so the common settings are configured using the module-info.java descriptor rather than the REST controller class.)

Please, pay attention when overriding the default value with the annotations!

If You make a mistake when specifying a setting name (this refers to the namespace and field name), no error will occur upon starting! The overridden value will be simply ignored!

By overriding the default values using the module-info.java descriptor, You can start the microservice. While reading the configuration of the HttpServerConfig class, the RxMicro framework reads the default values set with annotations:

2020-01-13 13:09:44.236 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at localhost:9090 using NETTY transport in 500 millis. (1)
1 HTTP server started on localhost:9090

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

The main difference between configuring with annotations and configuring with Java classes is support of the settings inheritance and overriding.

In other words, when using configuration via annotations, the RxMicro framework can also read other configuration sources.

When using configuration via Java classes, other configuration sources are always ignored.

For the test environment only, the RxMicro framework provides special @WithConfig annotation. Using this annotation it is convenient to configure the configuration manager for the test environment:

@WithConfig
private static final HttpServerConfig SERVER_CONFIG =
        new HttpServerConfig()
                .setPort(56789);

The source code of the project using the @WithConfig annotation is available at the following link:

@DefaultConfigValue annotation can be applied to override primitive values only:

  • strings,

  • booleans,

  • numbers,

  • dates,

  • times,

  • enums.

If You need to override a complex value, it is necessary to use @DefaultConfigValueSupplier annotation instead.

The source code of the project using the @DefaultConfigValueSupplier annotation 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.8.2.7. Configuration Using Command Line Arguments.

To override configs You can use command line arguments.

This type of configuration has the highest priority and overrides all other types. (Except of configuration using Java classes.)

Configuration using command line arguments is disabled by default.

To enable it is necessary to configure the Configs configuration manager:

public static void main(final String[] args) {
    new Configs.Builder()
            .withCommandLineArguments(args) (1)
            .build();
    startRestServer(MicroService.class);
}
1 Use withCommandLineArguments(args) method to enable the configuration using command line arguments.

For example, If You want to start HTTP server at 9191 port, You can pass the following command line argument:

java -p ./classes:lib -m examples.quick.start/io.rxmicro.examples.quick.start.HelloWorldMicroService http-server.port=9191

Result:

2020-01-02 18:49:58.372 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0: 9191 using NETTY transport in 500 millis.

4.8.3. Config Sources Setting

The RxMicro framework allows You to customize the order in which the configuration sources are read.

With this feature, the RxMicro framework automatically supports inheritance and redefinition of launch configurations.

4.8.3.1. Default Reading Order of Config Sources.

By default, the RxMicro framework reads configuration sources in the following order:

  • Configuration using @DefaultConfigValue annotations;

  • Configuration using the rxmicro.properties classpath resource;

  • Configuration using the separate (${name-space}.properties) classpath resource;

  • Configuration using environment variables;

  • Configuration using Java system variables;

Thus, if there are two classpath resources for a microservice:

The rxmicro.properties resource with the following content:

http-server.port=9090
http-server.host=localhost

and the http-server.properties resource with the following content:

port=9876

then the result will be as follows:

2020-01-11 16:52:26.797 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at localhost:9876 using NETTY transport in 500 millis. (1)
1 HTTP server has started at localhost:9876.

The configuration reading algorithm for the above example is as follows:

  1. By default, the HTTP server should start at 0.0.0.0:8080.

  2. But in the rxmicro.properties classpath resource there is a different IP address and port: localhost:9090.

  3. If the http-server.properties classpath resource had not existed, the HTTP server would have run at localhost:9090.

  4. But in the http-server.properties classpath resource it is specified the 9876 port.

  5. Therefore, when starting, the IP address is inherited from the rxmicro.properties resource and the overridden port value is read from the http-server.properties resource.
    (This behavior corresponds to the order of reading the default configurations.)

4.8.3.2. Custom Reading Order of Config Sources.

To change the order of the configuration reading it is necessary to use the Configs.Builder.withOrderedConfigSources(ConfigSource…​) method:

public static void main(final String[] args) {
    new Configs.Builder()
            .withOrderedConfigSources(
                    SEPARATE_CLASS_PATH_RESOURCE, (1)
                    RXMICRO_CLASS_PATH_RESOURCE   (2)
            )
            .build();
    startRestServer(MicroService.class);
}
1 First, it is necessary to read the configuration from the ${name-space}.properties classpath resource (In our case, it’s a http-server.properties)
2 and then from the rxmicro.properties classpath resource.

Thus, the order of the configuration reading from classpath resources has been changed in comparison with the default order.

When starting the microservice, the settings from the http-server.properties classpath resource will be overwritten by the settings from the rxmicro.properties classpath resource:

2020-01-11 16:52:26.797 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at localhost:9090 using NETTY transport in 500 millis. (1)
1 HTTP server has started at localhost:9090.

The Configs.Builder.withOrderedConfigSources(ConfigSource…​) method allows not only to change the order of reading the configuration sources, but also to activate/deactivate the sources.

In the above example, the RxMicro framework will ignore any configuration sources except classpath resources!

The RxMicro framework also provides other additional methods:

The Configs.Builder.withAllConfigSources() method was used to activate the reading of configurations from properties files in the Section 4.8.2.2, “Configuration Using properties Files.” subsection.

If You plan to use only properties files, it is recommended to specify only these types, excluding all other types:

public static void main(final String[] args) {
    new Configs.Builder()
            .withOrderedConfigSources(
                    RXMICRO_FILE_AT_THE_HOME_DIR,    (1)
                    RXMICRO_FILE_AT_THE_CURRENT_DIR, (2)
                    SEPARATE_FILE_AT_THE_HOME_DIR,   (3)
                    SEPARATE_FILE_AT_THE_CURRENT_DIR (4)
            )
            .build();
    startRestServer(MicroService.class);
}
1 Activation of configuration reading from the $HOME/rxmicro.properties file.
2 Activation of configuration reading from the rxmicro.properties file in the current directory.
3 Activation of configuration reading from the $HOME/${name-space}.properties files (for example, http-server.properties).
4 ctivation of configuration reading from the ${name-space}.properties files (for example, http-server.properties) in the current directory.

The order of reading is set by the argument order of the Configs.Builder.withOrderedConfigSources(ConfigSource…​) method

If You know exactly which configuration sources should be used by the microservice, ALWAYS specify them explicitly!

With this approach, at the microservice starting, the RxMicro framework won’t try to search for non-existent sources, spending precious microseconds!

4.8.3.3. Logging the Config Reading Process

To debug the configuration reading process You can activate the logger:

.level=INFO
io.rxmicro.config.level=DEBUG (1)
1 For all classes and subpackages of the io.rxmicro.config package activate the DEBUG(FINE) logging level.

After activating the logger, the start of the microservice with default settings will be as follows:

[DEBUG] Discovering properties for 'rest-server' namespace from sources: [DEFAULT_CONFIG_VALUES, RXMICRO_CLASS_PATH_RESOURCE, SEPARATE_CLASS_PATH_RESOURCE, ENVIRONMENT_VARIABLES, JAVA_SYSTEM_PROPERTIES]

[DEBUG] Classpath resource not found: rest-server.properties

[DEBUG] All properties discovered for 'rest-server' namespace (1)

[DEBUG] Discovering properties for 'http-server' namespace from sources: [DEFAULT_CONFIG_VALUES, RXMICRO_CLASS_PATH_RESOURCE, SEPARATE_CLASS_PATH_RESOURCE, ENVIRONMENT_VARIABLES, JAVA_SYSTEM_PROPERTIES]

[DEBUG] Discovered properties from 'rxmicro.properties' classpath resource: [http-server.port=9090, http-server.host=localhost] (2)

[DEBUG] Discovered properties from 'http-server.properties' classpath resource: [port=9876] (3)

[DEBUG] All properties discovered for 'http-server' namespace

[DEBUG] Discovering properties for 'netty-rest-server' namespace from sources: [DEFAULT_CONFIG_VALUES, RXMICRO_CLASS_PATH_RESOURCE, SEPARATE_CLASS_PATH_RESOURCE, ENVIRONMENT_VARIABLES, JAVA_SYSTEM_PROPERTIES]

[DEBUG] Classpath resource not found: netty-rest-server.properties

[DEBUG] All properties discovered for 'netty-rest-server' namespace (4)

[INFO] Server started at 0.0.0.0:9876 using NETTY transport in 500 millis. (5)
1 There is no configuration customization for the rest-server namespace, so the default configuration will be used.
2 Configuration reading for the http-server namespace from the rxmicro.properties classpath resource (Read values: http-server.port=9090, http-server.host=localhost).
3 Configuration reading for the http-server namespace from the http-server.properties classpath resource (Read value: port=9876).
4 There is no configuration customization for the netty-rest-server namespace, so the default configuration will be used.
5 HTTP server has started at localhost:9876.

Additional debugging information will show the order of reading the configuration sources and overriding the configuration parameter values!

4.8.4. Dynamic Configuration

If Your runtime environment can contain dynamic properties You can use AsMapConfig configuration:

public final class DynamicAsMapConfig extends AsMapConfig {
}

If the rxmicro.properties classpath resource with the following content exists:

dynamic.bigDecimal=3.1423456676
dynamic.bigInteger=9999999999999999999999999999999999999999999999999
dynamic.boolean=true
dynamic.integer=34
dynamic.string=staging

then the following code will return configured dynamic parameters:

public static void main(final String[] args) {
    final DynamicAsMapConfig config = getConfig(DynamicAsMapConfig.class);

    System.out.println(config.getBigDecimal("bigDecimal"));
    System.out.println(config.getBigInteger("bigInteger"));
    System.out.println(config.getBoolean("boolean"));
    System.out.println(config.getInteger("integer"));
    System.out.println(config.getString("string"));
}

4.8.5. User Defined Configurations

The developer can use the configuration module for custom configurations.

To do this, it is necessary to create a separate class:

public final class BusinessServiceConfig extends Config {

    private boolean production = true;

    public boolean isProduction() {
        return production;
    }

    public BusinessServiceConfig setProduction(final boolean production) {
        this.production = production;
        return this;
    }
}

The custom configuration class 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 Config abstract class.

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

Since this class will be created and initialized by the reflection util classes from rxmicro.reflection module automatically, it is necessary to export the package of the custom config class to this module in the module-info.java descriptor.
(These are the JPMS requirements.)

module examples.config.custom {
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;

    exports io.rxmicro.examples.config.custom to
            rxmicro.reflection;  (1)
}
1 Allow the access of reflection util classes from rxmicro.reflection module to config classes from the io.rxmicro.example.config.custom package.

After these changes, a class of custom configurations is available for use:

final class MicroService {

    @GET("/")
    void test() {
        final BusinessServiceConfig config = getConfig(BusinessServiceConfig.class);
        System.out.println("Production: " + config.isProduction());
    }
}

The production flag can now be set using any type of configuration, for example using the classpath of the business-service.properties resource:

production=false

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.8.6. Supported Parameter Types

The RxMicro framework supports the primitive, custom and container types, which can be config parameters for any configuration.

4.8.6.1. Supported Primitive Parameter Types

The RxMicro framework supports the following primitive Java types:

  • ? extends Enum<?>;

  • boolean;

  • java.lang.Boolean;

  • byte;

  • java.lang.Byte;

  • short;

  • java.lang.Short;

  • int;

  • java.lang.Integer;

  • long;

  • java.lang.Long;

  • java.math.BigInteger;

  • float;

  • java.lang.Float;

  • double;

  • java.lang.Double;

  • java.math.BigDecimal;

  • char;

  • java.lang.Character;

  • java.lang.CharSequence;

  • java.lang.String;

  • java.time.Instant;

  • java.time.LocalDate;

  • java.time.LocalDateTime;

  • java.time.LocalTime;

  • java.time.MonthDay;

  • java.time.OffsetDateTime;

  • java.time.OffsetTime;

  • java.time.Year;

  • java.time.YearMonth;

  • java.time.ZonedDateTime;

  • java.time.Duration;

  • java.time.ZoneOffset;

  • java.time.ZoneId;

  • java.time.Period;

  • java.nio.file.Path;

For temporal classes the RxMicro framework uses parse or of factory method to converts string value to the appropriate Java representation.

4.8.6.2. Supported Custom Parameter Types

The RxMicro framework supports a custom type as valid config parameter type.

For example if Your project contain the following custom type:

public interface CustomType {

    String getValue();
}

then Your custom config class can use this type as valid config parameter type:

public final class ExampleConfig extends Config {

    private CustomType type = () -> "DEFAULT_CONSTANT";

    public CustomType getType() {
        return type;
    }

    public void setType(final CustomType type) {
        this.type = type;
    }
}

Instances of the custom type can be created as:

  • Enum constant:

public enum CustomEnum implements CustomType {

    ENUM_CONSTANT;

    @Override
    public String getValue() {
        return "ENUM_CONSTANT";
    }
}
  • Class public static final constant:

public class CustomClass {

    public static final CustomType CLASS_CONSTANT = () -> "CLASS_CONSTANT";
}
  • Interface constant:

public interface CustomInterface {

    CustomType INTERFACE_CONSTANT = () -> "INTERFACE_CONSTANT";
}
  • Annotation constant:

public @interface CustomAnnotation {

    CustomType ANNOTATION_CONSTANT = () -> "ANNOTATION_CONSTANT";
}

To inform the RxMicro framework which instance must be created and injected to config parameter, it is necessary to use the following syntax:

@${FULL_CLASS_NAME}:${CONSTANT_FIELD_NAME}

For example if Your environment contains the following Java system properties:

System.setProperty(
        "enum-constant.type",
        "@io.rxmicro.examples.config.custom.type._enum.CustomEnum:ENUM_CONSTANT"
);
System.setProperty(
        "class-constant.type",
        "@io.rxmicro.examples.config.custom.type._class.CustomClass:CLASS_CONSTANT"
);
System.setProperty(
        "interface-constant.type",
        "@io.rxmicro.examples.config.custom.type._interface.CustomInterface:INTERFACE_CONSTANT"
);
System.setProperty(
        "annotation-constant.type",
        "@io.rxmicro.examples.config.custom.type._annotation.CustomAnnotation:ANNOTATION_CONSTANT"
);

and Your application read configuration for all configured namespaces:

System.out.println(
        "Default constant: " +
                getConfig(ExampleConfig.class).getType().getValue()
);
System.out.println(
        "Enum constant: " +
                getConfig("enum-constant", ExampleConfig.class).getType().getValue()
);
System.out.println(
        "Class constant: " +
                getConfig("class-constant", ExampleConfig.class).getType().getValue()
);
System.out.println(
        "Interface constant: " +
                getConfig("interface-constant", ExampleConfig.class).getType().getValue()
);
System.out.println(
        "Annotation constant: " +
                getConfig("annotation-constant", ExampleConfig.class).getType().getValue()
);

Result will be the following:

Default constant: DEFAULT_CONSTANT
Enum constant: ENUM_CONSTANT
Class constant: CLASS_CONSTANT
Interface constant: INTERFACE_CONSTANT
Annotation constant: ANNOTATION_CONSTANT

For class or interface or annotation constants the RxMicro framework uses the reflection to read value.
So before using this type of custom type instances don’t forget to export packages to rxmicro.reflection module, because this module contains the reflection util class which is used to read this value:

exports io.rxmicro.examples.config.custom.type._class to
        rxmicro.reflection;
exports io.rxmicro.examples.config.custom.type._interface to
        rxmicro.reflection;
exports io.rxmicro.examples.config.custom.type._annotation to
        rxmicro.reflection;

The RxMicro team recommends using an enum for custom type injection to config instances!

4.8.6.3. Supported Container Parameter Types

The RxMicro framework supports the following container Java types:

  • java.util.List<V>;

  • java.util.Set<V>;

  • java.util.SortedSet<V>;

  • java.util.Map<K, V>;

where K and V can be:

  • java.lang.Boolean;

  • java.lang.Long;

  • java.math.BigDecimal;

  • java.lang.String;

  • CUSTOM_TYPE.

The RxMicro framework uses , character as collection (List, Set, SortedSet, Map.Entry) value delimiter and '=' character as key-value separator:

list=red,green,blue
set=red,green,blue
sorted-set=red,green,blue
map=red=0xFF0000,green=0x00FF00,blue=0x0000FF

The RxMicro framework uses reflection to initialize config instances. But container parametrization types are not available for the reflection reading.

Thus the RxMicro framework tries to guess which type must be created using the following algorithm:

  1. Try to convert to java.lang.Boolean type. If failed then goto 2 step.

  2. Try to convert to java.math.BigDecimal type. If failed then goto 3 step.

  3. Try to convert to java.lang.Long type. If failed then goto 4 step.

  4. Try to convert to CUSTOM_TYPE. If failed then goto 5 step.

  5. Return java.lang.String instance.

This means that if You provide a config list with different types, the RxMicro framework create a java.util.List with different types:

For example:

list=red,1.2,4,true

the result will be:

java.util.List.of(new String("red"), new BigDecimal("1.2"), Long.valueOf(4), Boolean.valueOf(true));

and the ClassCastException will be thrown if Your config parameter is not of java.util.List<java.lang.Object> type.

To avoid the ClassCastException use the following recommendation:

  • For boolean lists use java.util.List<java.lang.Boolean> type.

  • For integer number lists use java.util.List<java.lang.Long> type.

  • For decimal number lists use java.util.List<java.math.BigDecimal> type.

  • For string lists use java.util.List<java.lang.String> type.

  • For custom type lists use java.util.List<CUSTOM_TYPE> type.

DON’T USE ANY OTHER TYPES FOR COLLECTION PARAMETRIZATION!

4.8.7. Configuration Verifiers

If Your runtime has complex configuration the RxMicro team strong recommends enabling runtime strict mode.

If the runtime strict mode activated the RxMicro runtime invokes additional checks to find unused or redundant configurations.

To enable the runtime strict mode set RX_MICRO_RUNTIME_STRICT_MODE environment variable to the true value! (Instead of environment variable You can use Java System property as well.)

If runtime strict mode successful activated the following log message can be found:

[INFO] RxMicroRuntime !!! RxMicro Runtime Strict Mode is activated !!!

4.9. Logger

Logger is an integral component of any software system.

The RxMicro framework provides the rxmicro.logger module for logging important events during the work of microservices.

Creation and usage of a logger in the source code is no different from other logging frameworks:

final class MicroService {

    private static final Logger LOGGER = LoggerFactory.getLogger(MicroService.class); (1)

    @GET("/")
    void test() {
        LOGGER.info("test message"); (2)
    }
}
1 Logger creation for the current microservice class.
2 Logging of a message with INFO level.

The Logger interface is an abstraction over the real logger.

At the moment, there is only one implementation of this interface that delegates logging to the java.logging module.

4.9.1. Logger Configuration

4.9.1.1. Using Configuration File

The main configuration file of the java.logging logger is the jul.properties classpath resource.

If classpath contains the jul.test.properties resource, then this resource overrides all configurations of the jul.properties resource.

This function allows configuring the logger for a test environment.

The jul.properties classpath resource must contain a configuration in the standard format for the java.logging module.

Example of logger configuration:

.level=INFO
io.rxmicro.config.level=DEBUG

The rxmicro.logger module supports all logging levels from the following sets: {OFF, ERROR, WARN, INFO, DEBUG, TRACE, ALL} and {OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL}.

Therefore, in the jul.properties configuration file You can use any logging level. When activating the java.logging logger, the RxMicro framework automatically converts levels from a set of {OFF, ERROR, WARN, INFO, DEBUG, TRACE, ALL} into {OFF, SEVERE, WARNING, INFO, FINE, FINEST, ALL}.

This option makes it possible to use unsupported but widely used in other logging frameworks logging levels for the java.logging logger.

4.9.1.2. Default Reading Order of Config Sources.

By default, the rxmicro.logger module reads configuration sources in the following order (From the lowest to the highest priority):

  • Default config.

  • Configuration from the jul.properties classpath resource if the resource found.

  • Configuration from the jul.test.properties classpath resource if the resource found.

4.9.1.3. Using Additional Config Sources for Logging Configuration

Besides jul.properties and jul.test.properties classpath resources the rxmicro.logger module supports the following configuration sources:

  • Configuration using environment variables;

  • Configuration using Java system properties;

  • Configuration using properties file that is located at the following paths:

    • ./jul.properties;

    • $HOME/jul.properties;

    • $HOME/.rxmicro/jul.properties;

where

  • . - is the current project directory (i.e. System.getProperty("user.dir"));

  • $HOME - is the home directory (i.e. System.getProperty("user.home")):

    • for Linux platform the $HOME directory is /home/$USERNAME;

    • for MacOS platform the $HOME directory is /Users/$USERNAME;

    • for Windows platform the $HOME directory is C:\Documents and Settings\%USERNAME% or C:\Users\%USERNAME%.

To enable the additional config sources use the LoggerConfigSources class:

(1)
static {
    LoggerConfigSources.setLoggerConfigSources(
            LoggerConfigSource.DEFAULT,
            LoggerConfigSource.CLASS_PATH_RESOURCE,
            LoggerConfigSource.TEST_CLASS_PATH_RESOURCE,
            LoggerConfigSource.FILE_AT_THE_HOME_DIR,
            LoggerConfigSource.FILE_AT_THE_CURRENT_DIR,
            LoggerConfigSource.FILE_AT_THE_RXMICRO_CONFIG_DIR,
            LoggerConfigSource.ENVIRONMENT_VARIABLES,
            LoggerConfigSource.JAVA_SYSTEM_PROPERTIES
    );
}

private static final Logger LOGGER = LoggerFactory.getLogger(LoggerConfigSourceTest.class);

public static void main(final String[] args) {
    LOGGER.info("Hello World!");
}
1 - The logger config sources must be configured before the first usage of the LoggerFactory, otherwise these config settings will be ignored.

If You know exactly which configuration sources should be used by the microservice, ALWAYS specify them explicitly!

With this approach, at the microservice starting, the RxMicro framework won’t try to search for non-existent sources, spending precious microseconds!

4.9.1.4. Using Environment Variables And Java System Properties

After activation of the configuration using environment variables and(or) Java system properties, the RxMicro framework parses environment variables and(or) Java system properties.
If Your runtime contains an environment variable and (or) java system property with name that starts with logger. phrase, the rxmicro.logger module interprets it as configuration. For example to enable TRACE logger level for MicroServiceLauncher it is necessary to provide one of the following configurations:

  • Example of the configuration using environment variable:

export logger.io.rxmicro.rest.server.level=TRACE

java -p lib:. -m module/package.MicroServiceLauncher

or

  • Example of the configuration using Java system property:

java -p lib:. -Dlogger.io.rxmicro.rest.server.level=TRACE -m module/package.MicroServiceLauncher

4.9.2. Logger Handler

By default the INFO logging level is activated for the all loggers.

All logger events are sent to the SystemConsoleHandler appender, which outputs:

  • Logger events with ERROR level into System.err;

  • Logger events with other levels into System.out.

The rxmicro.logger module follows the logs recommendations, that are defined at The Twelve-Factor App Manifest and logs messages to System.out/System.err only!

If these functions are not enough You can use any other logging framework: Logback, Apache Log4j 2 and others.

P.S. You can use also the FileHandler, SocketHandler, etc that defined at the java.logging module.

If the SystemConsoleHandler appender must output all logging information into System.out or System.err it is necessary to set stream parameter:

  • To enable System.err output for the all log levels use the following configuration:

io.rxmicro.logger.jul.SystemConsoleHandler.stream=stderror
  • To enable System.out output for the all log levels use the following configuration:

io.rxmicro.logger.jul.SystemConsoleHandler.stream=stdout

To get more information about the configuration of the SystemConsoleHandler component read javadoc for this component:
SystemConsoleHandler.html

4.9.3. Pattern Formatter

The current version of the rxmicro.logger module is supported only one logger formatter: PatternFormatter with the default configuration:

io.rxmicro.logger.jul.PatternFormatter.pattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%p] %c: %m%n

This class supports conversion specifiers that can be used as format control expressions. Each conversion specifier starts with a percent sign % and is followed by optional format modifiers, a conversion word and optional parameters between braces. The conversion word controls the data field to convert, e.g. logger name or date format.

The PatternFormatter supports the following conversion specifiers:

Table 3. Conversion specifiers supported by the PatternFormatter.
Conversion specifiers Description

c{length}
lo{length}
logger{length}

Outputs the name of the logger at the origin of the logging event.
This conversion specifier takes a string as its first and only option.
Currently supported only one of the following options: {short}, {0}, {full}.
{short} is synonym for {0} option.
If no option defined this conversion specifier uses {full} option.

The following table describes option usage results:

Conversion specifier Logger name Result

%logger

package.sub.Bar

package.sub.Bar

%logger{full}

package.sub.Bar

package.sub.Bar

%logger{short}

package.sub.Bar

Bar

%logger{0}

package.sub.Bar

Bar

C{length}
class{length}

Outputs the fully-qualified class name of the caller issuing the logging request.
This conversion specifier takes a string as its first and only option.
Currently supported only one of the following options: {short}, {0}, {full}.
{short} is synonym for {0} option.
If no option defined this conversion specifier uses {full} option.

The following table describes option usage results:

Conversion specifier Logger name Result

%logger

package.sub.Bar

package.sub.Bar

%logger{full}

package.sub.Bar

package.sub.Bar

%logger{short}

package.sub.Bar

Bar

%logger{0}

package.sub.Bar

Bar

Generating the caller class information is not particularly fast. Thus, its use should be avoided unless execution speed is not an issue!

d{pattern}
date{pattern}
d{pattern, timezone}
date{pattern, timezone}

Used to output the date of the logging event.
The date conversion word admits a pattern string as a parameter.
The pattern syntax is compatible with the format accepted by DateTimeFormatter.
If {@code timezone} is specified, this conversion specifier uses ZoneId.of(String) method to parse it, so timezone syntax must be compatible with zone id format.

The following table describes option usage results:

Conversion specifier Result

%date

2020-01-02 03:04:05.123

%date{yyyy-MM-dd}

2020-01-02

%date{HH:mm:ss.SSS}

03:04:05.123

%date{, UTC}

2020-01-02 03:04:05.123

If pattern is missing (For example: %d, %date, %date{, UTC}}, the default pattern will be used: yyyy-MM-dd HH:mm:ss.SSS

F
file`

Outputs the file name of the Java source file where the logging request was issued.

Generating the file information is not particularly fast. Thus, its use should be avoided unless execution speed is not an issue!

L
line`

Outputs the line number from where the logging request was issued.

Generating the file information is not particularly fast. Thus, its use should be avoided unless execution speed is not an issue!

m
mes`
message

Outputs the application-supplied message associated with the logging event.

M
method`

Outputs the method name where the logging request was issued.

Generating the method name is not particularly fast. Thus, its use should be avoided unless execution speed is not an issue.

n

Outputs the platform dependent line separator character or characters.

This conversion word offers practically the same performance as using non-portable line separator strings such as \n, or \r\n. Thus, it is the preferred way of specifying a line separator.

p
le`
level

Outputs the level of the logging event.

r
relative

Outputs the number of milliseconds elapsed since the start of the application until the creation of the logging event.

t
thread

Outputs the name of the thread that generated the logging event.

id
rid
request-id
request_id
requestId

Outputs the request id if specified.

This specifier displays the request id that retrieved from HTTP request if the request tracing is enabled.

For request tracing feature usage Your code must use the overloaded logger methods with RequestIdSupplier argument:

public interface Logger {

    // ...

    void trace(RequestIdSupplier requestIdSupplier, Object... otherArguments);

    void debug(RequestIdSupplier requestIdSupplier, Object... otherArguments);

    void info(RequestIdSupplier requestIdSupplier, Object... otherArguments);

    void warn(RequestIdSupplier requestIdSupplier, Object... otherArguments);

    void error(RequestIdSupplier requestIdSupplier, Object... otherArguments);

    // ...
}

4.9.4. Custom Log Event

The RxMicro framework provides a factory method to build a custom logger event:

private static final Logger LOGGER = LoggerFactory.getLogger(LoggerEventBuilderTest.class);

public static void main(final String[] args) {
    final LoggerEventBuilder builder = LoggerFactory.newLoggerEventBuilder();
    builder.setMessage("Hello World!");
    LOGGER.info(builder.build());
}

A custom logger event can be useful if Your log message depends on some parameter.
For example, the following methods implement the same logic:

public void log1(final Throwable throwable,
                 final boolean withStackTrace) {
    final LoggerEventBuilder builder = LoggerFactory.newLoggerEventBuilder()
            .setMessage("Some error message: ?", throwable.getMessage());
    if (withStackTrace) {
        builder.setThrowable(throwable);
    }
    LOGGER.error(builder.build());
}

public void log2(final Throwable throwable,
                 final boolean withStackTrace) {
    if (withStackTrace) {
        LOGGER.error(throwable, "Some error message: ?", throwable.getMessage());
    } else {
        LOGGER.error("Some error message: ?", throwable.getMessage());
    }
}

Besides that a custom logger event allows customizing the following auto detect parameters:

  • Thread id;

  • Thread name;

  • Source class name;

  • Source method name;

  • Source file name;

  • Source line number.

LOGGER.info(
        LoggerFactory.newLoggerEventBuilder()
                .setMessage("Some error message")
                .setThreadName("Test-Thread-Name")
                .setThreadId(34L)
                .setStackFrame("package.Class", "method", "Test.java", 85)
                .setThrowable(throwable)
                .build()
);

4.9.5. Multiline Logs Issue For Docker Environment

By default the docker log driver does not support multiline log data.
It means that if Your microservice prints a stacktrace of any exception to the System.out or System.err each stack trace element will be processed as separate log event.

There are several standard solutions to this problem. The RxMicro framework adds the one of them.

4.9.5.1. Solution From RxMicro Framework

If Your logger configuration contains the following setting:

io.rxmicro.logger.jul.PatternFormatter.singleLine=true

than all multiline log events are processed by the RxMicro framework as single line ones:
i.e. the PatternFormatter component replaces '\n' character by the "\\n" string before printing it to the System.out or System.err.
(For Windows platform the "\r\n"" character combination will be replaced by "\\r\\n" string!)

4.9.5.2. How To View Original Log Events?

To view original logs You can use the sed util:

docker logs microservice-container | sed -e 's/\\n/\n/g'

or

kubectl logs microservice-pod | sed -e 's/\\n/\n/g'

To view original logs on Your log aggregation tool if fluentd open source data collector is used, it is necessary to add the following filter:

<filter exampleTag>
    @type record_transformer
    enable_ruby true
    <record>
        # --- Replace "\n" string by '\n' character ---
        log ${record["log"].gsub("\\n", "\n")}
    </record>
</filter>

FYI: This filter requires that your log messages are parsed and converted to the json property with log name before invocation of this filter!

4.10. JSON

JSON is a widely used message exchange format for distributed applications.

The RxMicro framework provides the rxmicro.json module for low-level and efficient work with this format.

Unfortunately JSON format is defined in at least seven different documents:

(Read more at Parsing JSON is a Minefield article).

So the RxMicro framework implementation of JSON parser can be described using the following test suites:

The RxMicro framework uses classes from the rxmicro.json module when automatically converting Java models to JSON format and vice versa.

Therefore, a developer should not explicitly use this module!

However, the common idea about the capabilities of this module is needed for correct test writing!

4.10.1. A Mapping Between JSON and Java Types

Since this module is used automatically, it is optimized for machine operations. Therefore, this module doesn’t provide separate classes for JSON types. Instead, standard Java classes are used:

Table 4. Mapping table between JSON and Java types.
JSON type Java type

object

java.util.Map<String, Object>

array

java.util.List<Object>

boolean

java.lang.Boolean

null

null

string

java.lang.String

number

io.rxmicro.json.JsonNumber

Table 5. Mapping table between Java and JSON types.
Java type JSON type

java.util.Map<String, Object>

object

java.util.List<Object>

array

java.lang.Boolean

boolean

null

null

java.lang.String

string

? extends java.lang.Number

number

io.rxmicro.json.JsonNumber

number

Any Java Class

string

4.10.2. rxmicro.json Module Usage

To write tests correctly, it is necessary to have a common idea about the rxmicro.json module.

Let’s look at a microservice that returns the result in JSON object format:

final class MicroService {

    @GET("/")
    CompletionStage<Response> produce() {
        return completedStage(new Response());
    }
}

The Response class - Java model of HTTP response in JSON format:

public final class Response {

    final Child child = new Child(); (1)

    final List<Integer> values = List.of(25, 50, 75, 100); (2)

    final String string = "text"; (3)

    final Integer integer = 10; (4)

    final BigDecimal decimal = new BigDecimal("0.1234"); (5)

    final Boolean logical = true; (6)
}
1 Nested JSON object.
2 JSON numeric array.
3 String data type.
4 Integer numeric data type.
5 Floating-point numeric data type.
6 Logical data type.

The Child class - Java model of the nested JSON object:

public final class Child {

    final Integer integer = 20;
}

For simplicity, each response model field is immediately initialized with test data.

As a result, the microservice returns the following JSON object:

{
  "child": {
    "integer": 20
  },
  "values": [
    25,
    50,
    75,
    100
  ],
  "string": "text",
  "integer": 10,
  "decimal": 0.1234,
  "logical": true
}

While writing REST-based microservice test or integration test, it is necessary to compare the expected JSON response with the one returned by REST-based microservice. (The response returned by the microservice is available through the ClientHttpResponse.getBody()method):

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {
    //..
    @Test
    void Should_return_Response() {
        final ClientHttpResponse response = httpClient.get("/").join();
        final Object actualBody = response.getBody(); (1)
        // ...
    }
}
1 The ClientHttpResponse.getBody() method returns the HTTP response body. Since the REST-based microservice returns a JSON object, this method returns the result as the java.util.Map<String, Object> object.

Therefore, to compare JSON objects, You need to create the java.util.Map<String, Object> object containing the expected properties of the JSON object:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {
    //..
    @Test
    void Should_return_Response() {
        final ClientHttpResponse response = httpClient.get("/").join();
        final Object actualBody = response.getBody(); (1)
        final Object expectedBody = Stream.of(
                Map.entry("child", Map.of(
                        "integer", new io.rxmicro.json.JsonNumber("20")
                )),
                Map.entry("values", List.of(
                        new io.rxmicro.json.JsonNumber("25"),
                        new io.rxmicro.json.JsonNumber("50"),
                        new io.rxmicro.json.JsonNumber("75"),
                        new io.rxmicro.json.JsonNumber("100")
                )),
                Map.entry("string", "text"),
                Map.entry("integer", new io.rxmicro.json.JsonNumber("10")),
                Map.entry("decimal", new io.rxmicro.json.JsonNumber("0.1234")),
                Map.entry("logical", true)
        ).collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (u, v) -> u, LinkedHashMap::new
        )); (2)
        assertEquals(expectedBody, actualBody); (3)
    }
}
1 A JSON object returned by microservice.
2 Expected JSON object.
3 Comparison of JSON objects using the java.lang.Object.equals() method.

According to the JSON specification, the property order in JSON object is undefined.

Thus, the following JSON objects:

{
    "firstname" : "David",
    "lastname" : "Smith"
}

and

{
    "lastname" : "Smith",
    "firstname" : "David"
}

are considered to be the same.

The RxMicro framework always arranges JSON object properties!

Thus, the order of JSON properties always corresponds to the order of fields in the Java model!

This allows You to compare JSON objects in the form of java.util.Map<String, Object> using the java.lang.Object.equals() method.

If Your microservice returns the JSON object with unordered properties, use JsonFactory.orderedJsonObject(Object jsonObject) method to sort properties before comparison!

For convenient creation of the expected JSON object, it is recommended to use the JsonFactory utility class. This class arranges JSON properties and automatically converts all java.lang.Number types into the io.rxmicro.json.JsonNumber type:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @Test
    void Should_return_Response() {
        final ClientHttpResponse response = blockingHttpClient.get("/");

        final Object actualBody = response.getBody(); (1)
        final Object expectedBody = jsonObject(
                "child", jsonObject("integer", 20), (4)
                "values", jsonArray(25, 50, 75, 100),   (5)
                "string", "text",
                "integer", 10,
                "decimal", new BigDecimal("0.1234"),
                "logical", true
        ); (2)
        assertEquals(expectedBody, actualBody); (3)
    }
}
1 A JSON object returned by microservice.
2 Expected JSON object.
3 Comparison of JSON objects using the java.lang.Object.equals() method.
4 To create JSON object, it is recommended to use the JsonFactory.jsonObject method.
5 To create JSON array, it is recommended to use the JsonFactory.jsonArray method.

The ClientHttpResponse interface besides body() method also contains bodyAsBytes() and bodyAsString() ones.

Therefore in the test You can compare JSON objects by comparing their string representation or using a third-party Java library that supports the JSON format. (For example, JSON-java, Java API for JSON Processing, etc.)

Thus, the RxMicro framework recommends using the rxmicro.json module to compare JSON objects, but at the same time provides an opportunity to use any other JSON framework!

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

4.10.3. Json Wrappers

If you want to work with JSON format using usual Java style, the RxMicro framework provides wrapper classes for JSON object and array:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTestWithWrappers {

    private BlockingHttpClient blockingHttpClient;

    static Stream<Function<ClientHttpResponse, JsonObject>> toJsonObjectWrapperConverterProvider() {
        return Stream.of(
                (1)
                response -> JsonWrappers.toJsonObject(response.getBody()),
                (2)
                response -> JsonWrappers.readJsonObject(response.getBodyAsString(UTF_8))
        );
    }

    @ParameterizedTest
    @MethodSource("toJsonObjectWrapperConverterProvider")
    void Should_return_Response(final Function<ClientHttpResponse, JsonObject> converter) {
        final ClientHttpResponse response = blockingHttpClient.get("/");

        final JsonObject actualBody = converter.apply(response);

        assertEquals(
                new JsonObject()
                        .set("integer", 20),
                actualBody.getJsonObject("child")
        );
        assertEquals(
                new JsonArray()
                        .add(25).add(50).add(75).add(100),
                actualBody.getJsonArray("values")
        );
        assertEquals(
                "text",
                actualBody.getString("string")
        );
        assertEquals(
                10,
                actualBody.getNumber("integer").intValueExact()
        );
        assertEquals(
                new BigDecimal("0.1234"),
                actualBody.getNumber("decimal").bigDecimalValueExact()
        );
        assertTrue(
                actualBody.getBoolean("logical")
        );
    }
}
1 - To convert a internal view of JSON object to the JSON wrapper use JsonWrappers.toJsonObject method.
2 - To convert a string view of JSON object to the JSON wrapper use JsonWrappers.readJsonObject method.

4.11. Native Transports

The RxMicro framework uses Netty to perform asynchronous non-blocking IO operations. To increase productivity, Netty allows the use of native transports.

To enable native transports feature, add one of the following dependencies:

<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-netty-native-linux</artifactId>
    <version>${rxmicro.version}</version>
</dependency>
<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-netty-native-osx</artifactId>
    <version>${rxmicro.version}</version>
</dependency>
<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-netty-native</artifactId>
    <version>${rxmicro.version}</version>
</dependency>
<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-netty-native-all</artifactId>
    <version>${rxmicro.version}</version>
</dependency>
  • The rxmicro-netty-native-linux dependency adds the netty-transport-native-epoll artifact;

  • The rxmicro-netty-native-osx dependency adds the netty-transport-native-kqueue artifact;

  • The rxmicro-netty-native dependency activates native transports for the current platform:

    • in case of the Linux current platform, the netty-transport-native-epoll artifact is added;

    • in case of the MacOS current platform, the netty-transport-native-kqueue artifact is added;

    • otherwise native transports is not activated for the current platform;

  • the rxmicro-netty-native-all dependency adds the netty-transport-native-epoll and netty-transport-native-kqueue artifacts.

Adding a dependency to the microservice project:

<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-netty-native-linux</artifactId>
    <version>${rxmicro.version}</version>
</dependency>

instead of

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-epoll</artifactId>
    <version>${netty.version}</version>
    <classifier>linux-x86_64</classifier>
</dependency>

allows using the Netty native transports library version compatible with all other Netty libraries used by the RxMicro framework.

Therefore, the rxmicro-netty-native-…​ modules don’t contain any logic. They just add Netty native transports libraries of the correct version.

If native transports has been successfully activated, the information message about the start of the HTTP server will display the corresponding type of transport.

2020-02-02 20:14:11.707 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:8080 using EPOLL transport in 500 millis. (1)
1 The using EPOLL transport message format means that Netty will use the netty-transport-native-epoll library.
2020-02-02 20:14:11.707 [INFO] io.rxmicro.rest.server.netty.internal.component.NettyServer :
Server started at 0.0.0.0:8080 using KQUEUE transport in 500 millis. (1)
1 The using KQUEUE transport message format means that Netty will use the netty-transport-native-kqueue library.

5. REST Controller

REST Controller is a class that contains at least one declarative HTTP request handler.

Each REST-based microservice project must contain at least one REST Controller.
(The REST Controller is the entry point for the client.)

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

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

5.1. REST Controller Implementation Requirements.

5.1.1. REST Controller Class Requirements.

REST Controller is a Java class:

import io.rxmicro.rest.method.GET;

final class MicroService {

    @GET("/")
    void requestHandler() {

    }
}

that must meet the following requirements:

  1. The class must extend the java.lang.Object one.

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

  3. The class couldn’t be a nested one.

  4. The class must contain an accessible (not private) constructor without parameters.
    (The last requirement can be ignored if Your project depends on rxmicro.cdi module.)

A REST Controller class must be a public one, only if it contains the method: public static void main(final String[] args). Otherwise, a REST Controller class can be nonpublic.

5.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 synchronized one.

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

  5. The method couldn’t be a native one.

  6. The method must be annotated by at least one of the following annotations:

  7. The method must return a void type or one of the following reactive types:

  8. 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: Table 2, “Which class from a reactive library must be choose?”)).

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 microservices without parameters.)

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

  3. Custom class modeling an HTTP request.
    (This type is recommended for microservices, 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.

5.2. RxMicro Annotations

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

Table 6. Supported RxMicro Annotations.
Annotation Description

@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 controller.

@Version

Denotes a version of the REST controller.

@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 response, created by the request handler.

@SetHeader

Denotes a static HTTP header that must be set to the response, created by the request handler.

@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.)

@PathVariable

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

@RemoteAddress

Declares the Java model field as a field, in which the RxMicro framework must inject the remote client connection address.

@RequestMethod

Declares the Java model field as a field, in which the RxMicro framework must inject a method of the received request.

(This feature is useful for request logging when one handler supports different HTTP methods.)

@RequestUrlPath

Declares the Java model field as a field, in which the RxMicro framework must inject URL path of the received request.

(This feature is useful for request logging using path-variables.)

@RequestBody

Declares the Java model field as a field, in which the RxMicro framework must inject a body of the received request.

@ResponseStatusCode

Indicates to the RxMicro framework that the value of the Java model field should be used as a status code to be sent to the client.

@ResponseBody

Indicates to the RxMicro framework that the value of the Java model field should be used as a body to be sent to the client.

@RequestId

Declares the Java model field as a field, in which the RxMicro framework must inject a unique request ID.

@SetStatusCode

Declares a status code, which should be sent to the client in case of successful execution of the HTTP request handler.

@NotFoundMessage

Declares a message returned by the handler in case of no result.

@RestServerGeneratorConfig

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

@EnableCrossOriginResourceSharing

Activates the Cross Origin Resource Sharing for all request handlers in the REST controller.

5.3. Return Types

The HTTP request handler supports two categories of returned results:

  • HTTP response without body;

  • HTTP response with body;

5.3.1. Supported Return Types for HTTP Response without Body

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

final class RestControllerWithoutBody {

    (1)
    @GET("/void/void1")
    void void1() {
        //do something
    }

    @GET("/void/void2")
    void void2(final Request request) {
        //do something with request
    }

    @GET("/void/void3")
    void void3(final String requestParameter) {
        //do something with requestParameter
    }

    (2)

    @GET("/jse/completedFuture1")
    CompletableFuture<Void> completedFuture1() {
        return CompletableFuture.completedFuture(null);
    }

    @GET("/jse/completedFuture2")
    CompletableFuture<Void> completedFuture1(final Request request) {
        return CompletableFuture.completedFuture(null);
    }

    @GET("/jse/completedFuture3")
    CompletableFuture<Void> completedFuture1(final String requestParameter) {
        return CompletableFuture.completedFuture(null);
    }

    (3)

    @GET("/jse/completionStage1")
    CompletionStage<Void> completionStage1() {
        return CompletableFuture.completedStage(null);
    }

    @GET("/jse/completionStage2")
    CompletionStage<Void> completionStage2(final Request request) {
        return CompletableFuture.completedStage(null);
    }

    @GET("/jse/completionStage3")
    CompletionStage<Void> completionStage3(final String requestParameter) {
        return CompletableFuture.completedStage(null);
    }

    (4)

    @GET("/spring-reactor/mono1")
    Mono<Void> mono1() {
        return Mono.just("").then();
    }

    @GET("/spring-reactor/mono2")
    Mono<Void> mono2(final String requestParameter) {
        return Mono.just("").then();
    }

    @GET("/spring-reactor/mono3")
    Mono<Void> mono4(final Request request) {
        return Mono.just("").then();
    }

    (5)

    @GET("/rxjava3/completable1")
    Completable completable1() {
        return Completable.complete();
    }

    @GET("/rxjava3/completable2")
    Completable completable2(final Request request) {
        return Completable.complete();
    }

    @GET("/rxjava3/completable3")
    Completable completable3(final String requestParameter) {
        return Completable.complete();
    }
}
1 The void type is supported mainly for test purposes.
(The void type can also be applied when the request handler does not use any blocking operations. In cases where the request handler uses blocking operations, one of the reactive types must be used instead of the void type.)
2 The CompletableFuture<Void> type is recommended when using the java.util.concurrent library.
3 Instead of the CompletableFuture<Void> type, can also be used the CompletionStage<Void> type.
4 When using the Project Reactor library, only the Mono<Void> type can be used.
5 When using the RxJava library, only the Completable type can be used.

All the above mentioned handlers return an HTTP response without body:

@RxMicroRestBasedMicroServiceTest(RestControllerWithoutBody.class)
final class RestControllerWithoutBodyTest {

    private BlockingHttpClient blockingHttpClient;

    @ParameterizedTest
    @ValueSource(strings = {
            "/void/void1",
            "/void/void2",
            "/void/void3",

            "/jse/completedFuture1",
            "/jse/completedFuture2",
            "/jse/completedFuture3",

            "/jse/completionStage1",
            "/jse/completionStage2",
            "/jse/completionStage3",

            "/spring-reactor/mono1",
            "/spring-reactor/mono2",
            "/spring-reactor/mono3",

            "/rxjava3/completable1",
            "/rxjava3/completable2",
            "/rxjava3/completable3"
    })
    void Should_support_HTTP_responses_without_body(final String urlPath) {
        final ClientHttpResponse response = blockingHttpClient.get(urlPath);

        assertTrue(response.isBodyEmpty(), "Body not empty: " + response.getBody()); (1)
    }
}
1 When testing all RestControllerWithoutBody class handlers, each handler returns an HTTP response without body.

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

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

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.3.2. Supported Return Types for HTTP Response with Body

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

final class RestControllerWithBody {

    (1)

    @GET("/jse/completedFuture1")
    CompletableFuture<Response> completedFuture1() {
        return CompletableFuture.completedFuture(new Response());
    }

    @GET("/jse/completedFuture2")
    CompletableFuture<Response> completedFuture2(final Request request) {
        return CompletableFuture.completedFuture(new Response());
    }

    @GET("/jse/completedFuture3")
    CompletableFuture<Response> completedFuture3(final String requestParameter) {
        return CompletableFuture.completedFuture(new Response());
    }

    @GET("/jse/completedFuture4")
    CompletableFuture<Optional<Response>> completedFuture4() {
        return CompletableFuture.completedFuture(Optional.of(new Response()));
    }

    @GET("/jse/completedFuture5")
    CompletableFuture<Optional<Response>> completedFuture5(final Request request) {
        return CompletableFuture.completedFuture(Optional.of(new Response()));
    }

    @GET("/jse/completedFuture6")
    CompletableFuture<Optional<Response>> completedFuture6(final String requestParameter) {
        return CompletableFuture.completedFuture(Optional.of(new Response()));
    }

    (2)

    @GET("/jse/completionStage1")
    CompletionStage<Response> completionStage1() {
        return CompletableFuture.completedStage(new Response());
    }

    @GET("/jse/completionStage2")
    CompletionStage<Response> completionStage2(final Request request) {
        return CompletableFuture.completedStage(new Response());
    }

    @GET("/jse/completionStage3")
    CompletionStage<Response> completionStage3(final String requestParameter) {
        return CompletableFuture.completedStage(new Response());
    }

    @GET("/jse/completionStage4")
    CompletionStage<Optional<Response>> completionStage4() {
        return CompletableFuture.completedStage(Optional.of(new Response()));
    }

    @GET("/jse/completionStage5")
    CompletionStage<Optional<Response>> completionStage5(final Request request) {
        return CompletableFuture.completedStage(Optional.of(new Response()));
    }

    @GET("/jse/completionStage6")
    CompletionStage<Optional<Response>> completionStage6(final String requestParameter) {
        return CompletableFuture.completedStage(Optional.of(new Response()));
    }

    (3)

    @GET("/spring-reactor/mono1")
    Mono<Response> mono1() {
        return Mono.just(new Response());
    }

    @GET("/spring-reactor/mono2")
    Mono<Response> mono2(final Request request) {
        return Mono.just(new Response());
    }

    @GET("/spring-reactor/mono3")
    Mono<Response> mono3(final String requestParameter) {
        return Mono.just(new Response());
    }

    (4)

    @GET("/rxjava3/single1")
    Single<Response> single1() {
        return Single.just(new Response());
    }

    @GET("/rxjava3/single2")
    Single<Response> single2(final Request request) {
        return Single.just(new Response());
    }

    @GET("/rxjava3/single3")
    Single<Response> single3(final String requestParameter) {
        return Single.just(new Response());
    }

    @GET("/rxjava3/maybe1")
    Maybe<Response> maybe1() {
        return Maybe.just(new Response());
    }

    @GET("/rxjava3/maybe2")
    Maybe<Response> maybe2(final Request request) {
        return Maybe.just(new Response());
    }

    @GET("/rxjava3/maybe3")
    Maybe<Response> maybe3(final String requestParameter) {
        return Maybe.just(new Response());
    }
}
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, the Single<MODEL> or Maybe<MODEL> can be used.

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

All the above mentioned handlers return an HTTP response with body:

@RxMicroRestBasedMicroServiceTest(RestControllerWithBody.class)
final class RestControllerWithBodyTest {

    private BlockingHttpClient blockingHttpClient;

    @ParameterizedTest
    @ValueSource(strings = {
            "/jse/completedFuture1",
            "/jse/completedFuture2",
            "/jse/completedFuture3",
            "/jse/completedFuture4",
            "/jse/completedFuture5",
            "/jse/completedFuture6",

            "/jse/completionStage1",
            "/jse/completionStage2",
            "/jse/completionStage3",
            "/jse/completionStage4",
            "/jse/completionStage5",
            "/jse/completionStage6",

            "/spring-reactor/mono1",
            "/spring-reactor/mono2",
            "/spring-reactor/mono3",

            "/rxjava3/single1",
            "/rxjava3/single2",
            "/rxjava3/single3",

            "/rxjava3/maybe1",
            "/rxjava3/maybe2",
            "/rxjava3/maybe3"
    })
    void Should_support_HTTP_responses_with_body(final String urlPath) {
        final ClientHttpResponse response = blockingHttpClient.get(urlPath);

        assertEquals(
                jsonObject("message", "Hello World!"), (1)
                response.getBody()
        );
    }
}
1 When testing all RestControllerWithBody class handlers, each handler returns an HTTP response containing a JSON object, with the Hello World! message.

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. Routing of Requests

The RxMicro framework supports request routing based on HTTP method, URL Path and HTTP body:

5.4.1. Routing of Requests Based on HTTP Method

The RxMicro framework allows handling identical HTTP requests, differing only in HTTP method, by different handlers:

final class RoutingUsingHTTPMethod {

    @GET("/")
    void get() {
        System.out.println("GET");
    }

    @HEAD("/")
    void head() {
        System.out.println("HEAD");
    }

    @OPTIONS("/")
    void options() {
        System.out.println("OPTIONS");
    }
}
@RxMicroRestBasedMicroServiceTest(RoutingUsingHTTPMethod.class)
final class RoutingUsingHTTPMethodTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @ParameterizedTest
    @ValueSource(strings = {"GET", "HEAD", "OPTIONS"})
    void Should_route_to_valid_request_handler(final String method) {
        blockingHttpClient.send(method, "/");
        assertEquals(method, systemOut.asString());
    }
}

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.2. Routing of Requests Based on URL Path

The RxMicro framework allows handling HTTP requests on different URL Paths by different handlers:

final class RoutingUsingUrlPath {

    @GET("/urlPath1")
    void get1() {
        System.out.println("/urlPath1");
    }

    @GET("/urlPath2")
    void get2() {
        System.out.println("/urlPath2");
    }

    @GET("/${type}")
    void get3(final @PathVariable String type) { (1)
        System.out.println("/${type}: " + type);
    }
}
1 In addition to static URL Paths, the RxMicro framework also supports path variables.
@RxMicroRestBasedMicroServiceTest(RoutingUsingUrlPath.class)
final class RoutingUsingUrlPathTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @ParameterizedTest
    @CsvSource({
            "/urlPath1,     /urlPath1",
            "/urlPath2,     /urlPath2",
            "/urlPath3,     /${type}: urlPath3",
            "/put,          /${type}: put",
            "/get,          /${type}: get"
    })
    void Should_route_to_valid_request_handler(final String urlPath,
                                               final String expectedOut) {
        blockingHttpClient.get(urlPath);
        assertEquals(expectedOut, systemOut.asString());
    }
}

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.3. Routing of Requests Based on HTTP Body

Some HTTP methods allows transferring request parameters both in the start line and in the request body.

Therefore, in addition to standard routing types, the RxMicro framework also supports routing based on the HTTP body availability:

final class RoutingUsingHttpBody {

    @GET("/")
    @HEAD("/")
    @OPTIONS("/")
    @DELETE("/")
    @PATCH("/")
    @POST(value = "/", httpBody = false)
    @PUT(value = "/", httpBody = false)
    void handleRequestsWithoutBody(final String parameter) { (1)
        System.out.println("without body");
    }

    @PATCH(value = "/", httpBody = true)
    @POST("/")
    @PUT("/")
    void handleRequestsWithBody(final String parameter) { (2)
        System.out.println("with body");
    }
}
1 The request handler with parameters transferred in the start line.
2 The request handler with parameters transferred in the request body.

For the GET, HEAD, OPTIONS and DELETE HTTP methods, the request parameters are always transferred in the start line.

For the POST, PUT and PATCH HTTP methods, the request parameters can be transferred both in the start line and in the request body. (To configure the parameter transfer type, the httpBody option is used.)

@RxMicroRestBasedMicroServiceTest(RoutingUsingHttpBody.class)
final class RoutingUsingHttpBodyTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @ParameterizedTest
    @ValueSource(strings = {"GET", "HEAD", "POST", "PUT", "PATCH", "OPTIONS", "DELETE"})
    void Should_route_to_handleRequestsWithoutBody(final String method) {
        blockingHttpClient.send(
                method,
                "/?parameter=test" (1)
        );
        assertEquals("without body", systemOut.asString());
    }

    @ParameterizedTest
    @ValueSource(strings = {"POST", "PUT", "PATCH"})
    void Should_route_to_handleRequestsWithBody(final String method) {
        blockingHttpClient.send(
                method,
                "/",
                jsonObject("parameter", "test") (2)
        );
        assertEquals("with body", systemOut.asString());
    }
}
1 Parameter transfer in the start line.
2 Parameter transfer in the request body as JSON object.

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. Not Found Logic

The RxMicro framework supports Not Found Logic for HTTP request handlers.

To activate this feature it’s necessary to return a reactive type that supports optional result:

final class NotFoundMicroService {

    @GET("/get1")
    CompletableFuture<Optional<Response>> getOptional1(final Boolean found) { (1)
        return completedFuture(found ? Optional.of(new Response()) : Optional.empty());
    }

    @GET("/get2")
    Mono<Response> getOptional2(final Boolean found) { (2)
        return found ? Mono.just(new Response()) : Mono.empty();
    }

    @GET("/get3")
    Maybe<Response> getOptional3(final Boolean found) { (3)
        return found ? Maybe.just(new Response()) : Maybe.empty();
    }
}
1 When using CompletableFuture/CompletionStage it is necessary to use the java.util.Optional contract, since the CompletableFuture/CompletionStage reactive types do not support optional result by default.
2 Unlike CompletableFuture/CompletionStage, the Mono reactive type supports optional result.
3 Unlike CompletableFuture/CompletionStage, the Maybe reactive type also supports optional result.

When handling requests, the RxMicro framework checks the handler result:

  • If the handler returns a response model, the RxMicro framework will convert it to an HTTP response with the 200 status and JSON representation of this model.

  • If the handler returns an empty result, the RxMicro framework generates an HTTP response with the 404 and the standard "Not Found" error message.

@RxMicroRestBasedMicroServiceTest(NotFoundMicroService.class)
final class NotFoundMicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @ParameterizedTest
    @ValueSource(strings = {"/get1", "/get2", "/get3"})
    void Should_return_found_response(final String urlPath) {
        final ClientHttpResponse response = blockingHttpClient.get(urlPath + "?found=true");

        assertEquals(jsonObject("message", "Hello World!"), response.getBody()); (1)
        assertEquals(200, response.getStatusCode()); (1)
    }

    @ParameterizedTest
    @ValueSource(strings = {"/get1", "/get2", "/get3"})
    void Should_return_not_found_response(final String urlPath) {
        final ClientHttpResponse response = blockingHttpClient.get(urlPath + "?found=false");

        assertEquals(jsonErrorObject("Not Found"), response.getBody()); (2)
        assertEquals(404, response.getStatusCode()); (2)
    }
}
1 In case there is a result, the result is returned in JSON format, and the HTTP response status is 200.
2 If there is no result, a standard error message is returned, and the HTTP response status is 404.

The RxMicro framework provides an option to customize the Not Found message:

final class CustomizeNotFoundMicroService {

    @GET("/")
    (1)
    @NotFoundMessage("Custom not found message")
    CompletableFuture<Optional<Response>> getOptional1(final Boolean found) {
        return completedFuture(found ? Optional.of(new Response()) : Optional.empty());
    }
}
1 The @NotFoundMessage annotation allows You to specify a message that will be displayed to the client in case of missing result instead of the standard "Not Found" message.
@Test
void Should_return_custom_not_found_message() {
    final ClientHttpResponse response = blockingHttpClient.get("/?found=false");

    assertEquals(jsonErrorObject("Custom not found message"), response.getBody()); (1)
    assertEquals(404, response.getStatusCode()); (2)
}
1 If there is no result, the @NotFoundMessage message, which is set by the annotation, is returned.
2 The HTTP status is 404.
(The RxMicro framework does not allow overriding the HTTP status for Not Found logic!)

For more control over the HTTP response generated in case of an error, use exception instead of Not Found Logic feature!

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.6. Exceptions Usage

Each reactive type of returned request handler result supports two states:

  • Successful completion signal.

  • Completion signal with an error.

The feature of a successful completion signal consists in its uniqueness, i.e. if such a signal has occurred, it ensures successful completion of the business task. The feature of a completion signal with an error is that different types of errors may occur during the execution of the business task:

  • validation error;

  • data source connection error;

  • computational algorithm error, etc.

It means that each request handler can return only one successful result and several results with errors.

So the RxMicro framework introduces the error concept. An error means any unsuccessful result.

For simplified error handling, the RxMicro framework recommends using HTTP status codes for each error category!

In case the HTTP code status is not sufficient, the RxMicro framework recommends using an additional text description.

For this purpose, the RxMicro framework defines a standard JSON model which is returned in case of any error:

{
  "message": "Not Found"
}

Thus, in case of an error, the client determines the error category basing on HTTP status code analysis. For more information, the client should analyze a text message.

5.6.1. Basic Class of Exceptions

When handling an HTTP request, the RxMicro framework defines the HttpErrorException base exception class.

All custom exception types must extend this base class!

For all child classes which extend the HttpErrorException class, when creating an exception instance the stack trace is not filled, as this information is redundant.

(This behavior is achieved by using the protected RuntimeException(String, Throwable, boolean, boolean) protected constructor.)

5.6.2. Using the User Defined Exceptions

If the request handler must return an HTTP status code other than the successful one, a separate exception class must be created:

public final class CustomNotFoundException extends HttpErrorException { (1)

    private static final int STATUS_CODE = 404; (2)

    public CustomNotFoundException(final String message) { (3)
        super(STATUS_CODE, message);
    }
}
1 The custom exception class must extend the HttpErrorException class.
2 Each class must define the static int STATUS_CODE field with a status code that identifies the error category. (This name is constant and is used by the rxmicro.documentation module when building the project documentation.)
3 If You want to display a detailed message to the client, You need to add the constructor parameter.
(If You want to return to the client only the HTTP status without the HTTP body, then create a constructor without parameters.)

The RxMicro framework does not support null as a business value!

So if the null value is passed to the exception constructor as a message, then the RxMicro framework will return an HTTP response without a message body!
(i.e. the new CustomNotFoundException(null) and new CustomNotFoundException() constructions are identical in terms of the RxMicro framework!)

You can use two approaches to return an HTTP status code other than successful one:

final class MicroService {

    @PUT(value = "/business-object-1", httpBody = false)
    CompletableFuture<Void> updateObject1(final Integer id) {
        return failedFuture(new CustomNotFoundException("Object not found by id=" + id)); (1)
    }

    @PUT(value = "/business-object-2", httpBody = false)
    CompletableFuture<Void> updateObject2(final Integer id) {
        throw new CustomNotFoundException("Object not found by id=" + id); (2)
    }
}
1 Use the reactive type method to generate a completion signal with an error.
2 Create and throw an exception instance.

From the client’s point of view, the two above-mentioned approaches are exactly the same:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @ParameterizedTest
    @ValueSource(strings = {"/business-object-1", "/business-object-2"})
    void Should_return_404(final String urlPath) {
        final ClientHttpResponse response = blockingHttpClient.put(urlPath + "?id=0");

        assertEquals(jsonErrorObject("Object not found by id=0"), response.getBody()); (1)
        assertEquals(404, response.getStatusCode()); (2)
    }
}
1 As a detailed error message, the message passed to the exception constructor is returned.
(The jsonErrorObject("…​") utility method is synonymous with the jsonObject("message", "…​") method.)
2 The returned status code equals the status code defined in the exception class in the int STATUS_CODE field.

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.6.3. Error Signal Generation Methods

To return an HTTP response with an error it is necessary to use the appropriate reactive type method to generate the error signal.

Table 7. Error signal generation method.
Reactive type Static method

CompletableFuture

CompletableFuture.failedFuture

CompletionStage

CompletableFuture.failedStage

Mono

Mono.error

Completable

Completable.error

Single

Single.error

Maybe

Maybe.error

Using the error signal generation method instead of throwing an exception instance requires writing more code but saves processor resources!

So it’s up to You to determine which method to use in Your project!

5.6.4. Predefined Classes of Exceptions

The RxMicro framework defines the following predefined exception classes:

Table 8. List of predefined exception classes.
Exception class Status code Description

RedirectException

300..399

A base class to inform the client about the need to perform redirect.

(Instead of using a base class, it is recommended to use one of the following child ones: TemporaryRedirectException or PermanentRedirectException.)

TemporaryRedirectException

307

A class that signals the need to perform Temporary Redirect.

PermanentRedirectException

308

A class that signals the need to perform Permanent Redirect.

ValidationException

400

A class signaling that the client has sent a bad request.

InternalHttpErrorException

500

A class signaling that an internal error has occurred during execution.

UnexpectedResponseException

500

A class signaling that the HTTP response, generated by the request handler, contains validation errors.

HttpClientTimeoutException

504

A class signaling that the HTTP client didn’t receive a response from the server in given time.

5.7. Overriding the Status Code

By default, if there are no errors during HTTP request handling, the HTTP server returns the 200 status code. If You need to change the status code returned by default, You must use the @SetStatusCode annotation:

final class MicroService {

    @GET("/200")
    void success() {
    }

    @GET("/307")
    @SetStatusCode(307)
    void permanentRedirect() {
    }

    @GET("/404")
    @SetStatusCode(404)
    void notFound() {
    }

    @GET("/500")
    @SetStatusCode(500)
    void internalError() {
    }
}
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @ParameterizedTest
    @ValueSource(ints = {200, 307, 404, 500})
    void Should_return_valid_status_codes(final int expectedStatus) {
        final ClientHttpResponse response = blockingHttpClient.get("/" + expectedStatus);

        assertEquals(expectedStatus, response.getStatusCode());
    }
}

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.8. Redirecting of Requests

The RxMicro framework supports request redirection:

final class MicroService {

    @PUT(value = "/old-url")
    CompletableFuture<Void> redirect() {
        return failedFuture(new PermanentRedirectException("/new-url")); (1)
    }

    (2)
    @PUT(value = "/new-url")
    void put(final String parameter) {
        System.out.println(parameter);
    }
}
1 To redirect a request, You need to return the PermanentRedirectException instance, indicating a new URL Path.
2 Once the HTTP response with request redirection is returned, the HTTP client will automatically repeat the request to a new URL Path.

If temporary redirection is required, the TemporaryRedirectException class must be used instead of the PermanentRedirectException class!

The predefined redirection classes (TemporaryRedirectException and PermanentRedirectException) return the 307 and 308 HTTP statuses.

According to the HTTP protocol rules, the HTTP method is preserved at such redirection!

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    (1)
    @BlockingHttpClientSettings(followRedirects = ENABLED)
    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @Test
    void Should_redirect() {
        blockingHttpClient.put(
                "/old-url",
                jsonObject("parameter", "test")
        ); (2)

        assertEquals("test", systemOut.asString()); (3)
    }
}
1 By default, the predefined HTTP client, that is used to perform test requests to the microservice, does not support redirection. Therefore, automatic redirection must be activated using the @BlockingHttpClientSettings(followRedirects = ENABLED) setting.
2 The PUT request to the /put1 URL Path is performed.
3 Consequently, the MicroService.put handler is automatically performed after the MicroService.redirect handler.

Besides using the PermanentRedirectException/TemporaryRedirectException exceptions, a custom model class or the @SetStatusCode and @SetHeader annotation composition can be used for redirection:

@PUT(value = "/old-url1")
CompletableFuture<RedirectResponse> redirect1() {
    return completedFuture(new RedirectResponse("/new-url")); (1)
}

@PUT(value = "/old-url2")
(2)
@SetStatusCode(307)
@SetHeader(name = LOCATION, value = "/new-url")
void redirect2() {
}
1 Using a custom model class with the Location header.
2 Using the @SetStatusCode and @SetHeader annotation composition.

FYI: The RedirectResponse model class:

public final class RedirectResponse {

    @ResponseStatusCode
    final Integer status = 307;

    @Header(LOCATION)
    final String location;

    public RedirectResponse(final String location) {
        this.location = requireNonNull(location);
    }
}

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.9. HTTP Headers Support

5.9.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:

final class SimpleUsageMicroService {

    @GET("/get1")
    CompletableFuture<Response> get1(final Request request) { (1)
        return completedFuture(new Response(request));
    }

    (2)
    @GET("/get2")
    (3)
    @HeaderMappingStrategy
    CompletableFuture<Response> get2(final @Header String endpointVersion,          (4)
                                     final @Header("UseProxy") Boolean useProxy) {  (5)
        return completedFuture(new Response(endpointVersion, useProxy));
    }
}
1 If a separate model class has been created for an HTTP request, then this class must be passed to 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, from the client’s point of view the two above-mentioned handlers are absolutely equal:

@RxMicroRestBasedMicroServiceTest(SimpleUsageMicroService.class)
final class SimpleUsageMicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @ParameterizedTest
    @ValueSource(strings = {"/get1", "/get2"})
    void Should_process_HTTP_headers(final String path) {
        final ClientHttpResponse response = blockingHttpClient.get(
                path, (1)
                HttpHeaders.of(
                        "Endpoint-Version", "v1",   (2)
                        "UseProxy", true            (3)
                )
        );

        final HttpHeaders responseHeaders = response.getHeaders();
        assertEquals("v1", responseHeaders.getValue("Endpoint-Version"));   (4)
        assertEquals("true", responseHeaders.getValue("UseProxy"));         (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.

5.9.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.

final class ListHeaderMicroService {

    @GET("/")
    @HeaderMappingStrategy
    void consume(final @Header List<Status> supportedStatuses) {
        System.out.println(supportedStatuses);
    }
}
@RxMicroRestBasedMicroServiceTest(ListHeaderMicroService.class)
final class ListHeaderMicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    static Stream<Arguments> supportedListProvider() {
        return Stream.of(
                arguments("created|approved|rejected"),                         (1)
                arguments(new Status[]{created, approved, rejected}, 0),        (2)
                arguments(new String[]{"created", "approved", "rejected"}, 0),  (3)
                arguments(List.of(created, approved, rejected)),                (4)
                arguments(List.of("created", "approved", "rejected")),          (5)
                arguments(HttpHeaders.of(                                       (6)
                        "Supported-Statuses", "created",
                        "Supported-Statuses", "approved",
                        "Supported-Statuses", "rejected"
                )),
                arguments(HttpHeaders.of(                                       (6)
                        "Supported-Statuses", "created|approved",
                        "Supported-Statuses", "rejected"
                ))
        );
    }

    @ParameterizedTest
    @MethodSource("supportedListProvider")
    void Should_support_list_headers(final Object headerValue) {
        blockingHttpClient.get(
                "/",
                headerValue instanceof HttpHeaders ?
                        (HttpHeaders) headerValue :
                        HttpHeaders.of("Supported-Statuses", headerValue) (7)
        );

        assertEquals(
                "[created, approved, rejected]", (8)
                systemOut.asString()
        );
    }
}
1 If the HTTP header is a list of values, the list elements are transferred via the HTTP protocol as a string separated by the | symbol.
(For HTTP response headers it is possible to activate the repeating headers mode.)
2 Besides transferring the list using a comma-separated string, the BlockingHttpClient component also supports:
an array of enumerations,
3 an array of string values,
4 a list of enumerations,
5 a list of string values.
(The BlockingHttpClient component converts these types to a comma-separated string automatically!)
6 Besides transferring HTTP headers as a comma-separated string, the BlockingHttpClient component also supports repeatable HTTP headers with different values.
7 All specified value types for the HTTP header, which is a list of values, are transferred as the java.lang.Object type. (The BlockingHttpClient component automatically converts them to a comma-separated string and transfers via the HTTP protocol.)
8 The RxMicro framework converts different types to a list of enumerations and displays it in the console.

5.9.3. Static HTTP headers

For setting of the static headers, the RxMicro framework provides the @AddHeader and @SetHeader annotations:

(1)
@SetHeader(name = "Mode", value = "Demo")
final class StaticHeadersMicroService {

    @GET("/get1")
    void get1() {
    }

    @GET("/get2")
    (2)
    @SetHeader(name = "Debug", value = "true")
    void get2() {
    }
}
1 If the HTTP header is set for the REST controller, it is added to the HTTP responses for each handler.
2 If the HTTP header is set for the handler, it is added to the HTTP responses only for that handler.

From the client’s point of view, static headers do not differ from any others:

@Test
void Should_use_parent_header_only() {
    final ClientHttpResponse response = blockingHttpClient.get("/get1");

    assertEquals("Demo", response.getHeaders().getValue("Mode"));
}

@Test
void Should_use_parent_and_child_headers() {
    final ClientHttpResponse response = blockingHttpClient.get("/get2");

    assertEquals("Demo", response.getHeaders().getValue("Mode"));
    assertEquals("true", response.getHeaders().getValue("Debug"));
}

In order to understand the differences between the @AddHeader and @SetHeader annotations, take a look to 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.9.4. Repeating HTTP headers

If the HTTP header of an HTTP response 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:

@Header
List<Status> singleHeader = List.of(created, approved, rejected);

@Header
@RepeatHeader
List<Status> repeatingHeader = List.of(created, approved, rejected);

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

For request 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:

assertEquals(
        "created|approved|rejected", (1)
        response.getHeaders().getValue("Single-Header")
);
assertEquals(
        List.of("created", "approved", "rejected"), (2)
        response.getHeaders().getValues("Repeating-Header")
);
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.10. HTTP Parameters Handling

5.10.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:

final class SimpleUsageMicroService {

    @GET("/get1")
    @POST("/post1")
    CompletableFuture<Response> get1(final Request request) { (1)
        return completedFuture(new Response(request));
    }

    (2)
    @GET("/get2")
    @POST("/post2")
    (3)
    @ParameterMappingStrategy
    CompletableFuture<Response> get2(final String endpointVersion,                      (4)
                                     final @Parameter("use-Proxy") Boolean useProxy) {  (5)
        return completedFuture(new Response(endpointVersion, useProxy));
    }
}
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!

Therefore, if the parameters are the same, it is possible to use the same handler with the specified request model to handle both GET (parameters are transferred in the start line) and POST (parameters are transferred in the HTTP body) request.

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, from the client’s point of view the two above-mentioned handlers are absolutely equal to GET and POST requests respectively:

@RxMicroRestBasedMicroServiceTest(SimpleUsageMicroService.class)
final class SimpleUsageMicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @ParameterizedTest
    @ValueSource(strings = {"/get1", "/get2"})
    void Should_process_HTTP_query_params(final String path) {
        final ClientHttpResponse response = blockingHttpClient.get(
                path, (1)
                QueryParams.of(
                        "endpoint_version", "v1",   (2)
                        "use-Proxy", true           (3)
                )
        );

        assertHttpBody(response);
    }

    @ParameterizedTest
    @ValueSource(strings = {"/post1", "/post2"})
    void Should_process_HTTP_body_params(final String path) {
        final ClientHttpResponse response = blockingHttpClient.post(
                path, (1)
                jsonObject(
                        "endpoint_version", "v1",   (2)
                        "use-Proxy", true           (3)
                )
        );

        assertHttpBody(response);
    }

    private void assertHttpBody(final ClientHttpResponse response) {
        assertEquals(
                jsonObject(
                        "endpoint_version", "v1", (4)
                        "use-Proxy", true         (4)
                ),
                response.getBody()
        );
    }
}
1 When performing a request to different URL Paths, the result is the same. (Despite the differences in parameter transfer for GET and `POST`requests.)
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 @ParameterMappingStrategy annotation.)
3 The use-Proxy name corresponds to the useProxy field of the request model, since this name is specified in the @Parameter annotation.
4 After executing the request in the resulting HTTP response, the endpoint_version and use-Proxy HTTP parameters 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.

5.10.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.

final class ListQueryParamMicroService {

    @GET("/")
    @ParameterMappingStrategy
    void consume(final List<Status> supportedStatuses) {
        System.out.println(supportedStatuses);
    }
}
@RxMicroRestBasedMicroServiceTest(ListQueryParamMicroService.class)
final class ListQueryParamMicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    static Stream<Arguments> supportedListProvider() {
        return Stream.of(
                arguments("created|approved|rejected"),                         (1)
                arguments(new Status[]{created, approved, rejected}, 0),        (2)
                arguments(new String[]{"created", "approved", "rejected"}, 0),  (3)
                arguments(List.of(created, approved, rejected)),                (4)
                arguments(List.of("created", "approved", "rejected")),          (5)
                arguments(QueryParams.of(                                       (6)
                        "supported_statuses", "created",
                        "supported_statuses", "approved",
                        "supported_statuses", "rejected"
                )),
                arguments(QueryParams.of(                                       (0)
                        "supported_statuses", "created|approved",
                        "supported_statuses", "rejected"
                ))
        );
    }

    @ParameterizedTest
    @MethodSource("supportedListProvider")
    void Should_support_list_headers(final Object queryParamValue) {
        blockingHttpClient.get(
                "/",
                queryParamValue instanceof QueryParams ?
                        (QueryParams) queryParamValue :
                        QueryParams.of("supported_statuses", queryParamValue) (7)
        );

        assertEquals(
                "[created, approved, rejected]", (8)
                systemOut.asString()
        );
    }
}
1 If the HTTP parameter is a list of values, the list elements are transferred via the HTTP protocol as a string separated by the | symbol.
2 Besides transferring the list using a comma-separated string, the BlockingHttpClient component also supports:
an array of enumerations,
3 an array of string values,
4 a list of enumerations,
5 a list of string values.
(The BlockingHttpClient component converts these types to a comma-separated string automatically!)
6 Besides transferring HTTP parameters as a comma-separated string, the BlockingHttpClient component also supports repeatable query params with different values.
7 All specified value types for the HTTP parameter, which is a list of values, are transferred as the java.lang.Object type. (The BlockingHttpClient component automatically converts them to a comma-separated string and transfers via the HTTP protocol.)
8 The RxMicro framework converts different types to a list of enumerations and displays it in the console.

5.10.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;
}

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

@ParameterMappingStrategy
public final class ComplexRequest {

    Integer integerParameter; (1)

    Status enumParameter; (1)

    List<Status> enumsParameter; (2)

    NestedModel nestedModelParameter; (3)

    List<NestedModel> nestedModelsParameter; (4)
}
@ParameterMappingStrategy
public final class ComplexResponse {

    final Integer integerParameter; (1)

    final Status enumParameter; (1)

    final List<Status> enumsParameter; (2)

    final NestedModel nestedModelParameter; (3)

    final List<NestedModel> nestedModelsParameter; (4)

    public ComplexResponse(final ComplexRequest request) {
        this.integerParameter = request.integerParameter;
        this.enumParameter = request.enumParameter;
        this.enumsParameter = request.enumsParameter;
        this.nestedModelParameter = request.nestedModelParameter;
        this.nestedModelsParameter = request.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:

final class ComplexModelMicroService {

    @POST("/")
    CompletableFuture<ComplexResponse> handle(final ComplexRequest request) {
        return completedFuture(new ComplexResponse(request));
    }
}

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

@RxMicroRestBasedMicroServiceTest(ComplexModelMicroService.class)
final class ComplexModelMicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @Test
    void Should_support_complex_models() {
        final Object jsonObject = jsonObject(
                "integer_parameter", 1,
                "enum_parameter", created,
                "enums_parameter", jsonArray(created, approved),
                "nested_model_parameter", jsonObject(
                        "string_parameter", "test",
                        "big_decimal_parameter", new BigDecimal("3.14"),
                        "instant_parameter", Instant.now()
                ),
                "nested_models_parameter", jsonArray(
                        jsonObject(
                                "string_parameter", "test1",
                                "big_decimal_parameter", new BigDecimal("1.1"),
                                "instant_parameter", Instant.now()
                        ),
                        jsonObject(
                                "string_parameter", "test2",
                                "big_decimal_parameter", new BigDecimal("1.2"),
                                "instant_parameter", Instant.now()
                        )
                )
        );
        final ClientHttpResponse response = blockingHttpClient.post("/", jsonObject);

        assertEquals(jsonObject, response.getBody());
    }
}

For automatic conversion of Java types to JSON types, it is recommended to use the JsonFactory test class from the rxmicro-test-json module.

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.11. The path variables Support

5.11.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:

final class MicroService {

    (1)
    @GET("/1/${category}/${class}/${subType}")
    @GET("/1/${category}/${class}_${subType}")
    @GET("/1/${category}-${class}-${subType}")
    @GET("/1-${category}-${class}-${subType}")
    void consume(final Request request) {
        System.out.println(format(
                "?-?-?",
                request.getCategory(), request.getType(), request.getSubType()
        ));
    }

    (1)
    @GET("/2/${category}/${class}/${subType}")
    @GET("/2/${category}/${class}_${subType}")
    @GET("/2/${category}-${class}-${subType}")
    @GET("/2-${category}-${class}-${subType}")
    void consume(final @PathVariable String category,       (2)
                 final @PathVariable("class") String type,  (3)
                 final @PathVariable String subType) {
        System.out.println(format(
                "?-?-?",
                category, type, 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 selects the required HTTP request handler.)
2 In order to declare a method parameter as path variable, You must use the @PathVariable annotation.
3 Using @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, from the client’s point of view the two above-mentioned handlers are absolutely equal:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @ParameterizedTest
    @CsvSource({
            "/1/category/class/subType,     category-class-subType",
            "/1/category/class_subType,     category-class-subType",
            "/1/category-class-subType,     category-class-subType",
            "/1-category-class-subType,     category-class-subType",

            "/2/category/class/subType,     category-class-subType",
            "/2/category/class_subType,     category-class-subType",
            "/2/category-class-subType,     category-class-subType",
            "/2-category-class-subType,     category-class-subType",

            "/1/5/6/7,                      5-6-7",
            "/1/5/6_7,                      5-6-7",
            "/1/5-6-7,                      5-6-7",
            "/1-5-6-7,                      5-6-7",

            "/2/5/6/7,                      5-6-7",
            "/2/5/6_7,                      5-6-7",
            "/2/5-6-7,                      5-6-7",
            "/2-5-6-7,                      5-6-7",
    })
    void Should_support_path_variables(final String path,           (1)
                                       final String expectedOut) {  (2)
        blockingHttpClient.get(path);

        assertEquals(expectedOut, systemOut.asString());
    }
}
1 The current path, which corresponds to one of the specified path templates.
2 The expected string containing all extracted path variables from the current path, displayed in the console.

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.11.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!

5.12. Support of Internal Data Types

5.12.1. Basic Rules

The RxMicro framework works with internal data. Due to this, the developer can extract additional information about the HTTP request and gain more control over the generated HTTP response.

To work with internal data, it is necessary to create request and response models:

public final class Request {

    (1)
    @RemoteAddress
    String remoteAddress1;

    (2)
    @RequestUrlPath
    String urlPath;

    (3)
    @RequestMethod
    String method;

    (4)
    HttpVersion httpVersion;

    (5)
    HttpHeaders headers;

    (6)
    @RequestBody
    byte[] bodyBytes;

    public String getRemoteAddress1() {
        return remoteAddress1;
    }

    public String getUrlPath() {
        return urlPath;
    }

    public String getMethod() {
        return method;
    }

    public HttpVersion getHttpVersion() {
        return httpVersion;
    }

    public HttpHeaders getHeaders() {
        return headers;
    }

    public byte[] getBodyBytes() {
        return bodyBytes;
    }
}
1 To extract the remote address of the client connection, it is necessary to use the @RemoteAddress annotation.
2 To extract the current URL path, it is necessary to use the @RequestUrlPath annotation.
(This feature is useful for request logging using path-variables.)
3 To extract the current HTTP method, it is necessary to use the @RequestMethod annotation.
(This feature is useful for request logging when one handler supports different HTTP methods.)
4 To extract the HTTP protocol version, it is necessary to use the HttpVersion type.
5 To extract all HTTP headers, it is necessary to use the HttpHeaders type.
6 To extract the request body content as a byte array, it is necessary to use the @RequestBody annotation.
public final class Response {

    (1)
    @ResponseStatusCode
    final Integer status;

    (2)
    final HttpVersion version;

    (3)
    final HttpHeaders headers;

    (4)
    @ResponseBody
    final byte[] body;

    public Response(final Integer status,
                    final HttpVersion version,
                    final HttpHeaders headers,
                    final byte[] body) {
        this.status = status;
        this.version = version;
        this.headers = headers;
        this.body = body;
    }
}
1 To override the HTTP status code, it is necessary to use the @ResponseStatusCode annotation.
2 To set 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 set all HTTP headers, it is necessary to use the HttpHeaders type.
4 To set 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 an HTTP request handler that uses the following models:

final class MicroService {

    @POST("/")
    CompletableFuture<Response> handle(final Request request) {
        return completedFuture(new Response(
                201,
                request.getHttpVersion(),
                HttpHeaders.of(
                        "Remote-Address", request.getRemoteAddress1(),
                        "Url-Path", request.getUrlPath(),
                        "Method", request.getMethod(),
                        "Custom-Header", request.getHeaders().getValue("Custom-Header")
                ),
                request.getBodyBytes()
        ));
    }
}

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

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @Test
    void Should_support_internal_types() {
        final Object body = jsonObject("message", "Hello World!");
        final ClientHttpResponse response = blockingHttpClient.post(
                "/",
                HttpHeaders.of("Custom-Header", "Custom-Value"),
                body
        );

        assertEquals(body, response.getBody());
        assertEquals(201, response.getStatusCode());
        assertSame(HTTP_1_1, response.getVersion());
        final HttpHeaders responseHeaders = response.getHeaders();
        final String remoteAddress = responseHeaders.getValue("Remote-Address");
        assertTrue(
                remoteAddress.contains("127.0.0.1"),
                "Invalid Remote-Address: " + remoteAddress
        );
        assertEquals("/", responseHeaders.getValue("Url-Path"));
        assertEquals("POST", responseHeaders.getValue("Method"));
        assertEquals("Custom-Value", responseHeaders.getValue("Custom-Header"));
    }
}

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.12.2. Supported Internal Data Types

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

HttpRequest

None

-

HTTP request model

HttpVersion

None

-

HTTP protocol version

HttpHeaders

None

-

HTTP header model

SocketAddress

@RemoteAddress

No

Remote client address

String

@RemoteAddress

Yes

Remote client address

String

@RequestUrlPath

Yes

URL Path of the current request

String

@RequestMethod

Yes

HTTP request method

byte[]

@RequestBody

Yes

HTTP request body content

Currently only 1.0 and 1.1 HTTP versions are supported.

Table 10. 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

5.13. Versioning of REST Controllers

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

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

  • Versioning based on URL Path fragment analysis.

5.13.1. Versioning Based on HTTP Header Analysis

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

@Version(value = "v1", strategy = Version.Strategy.HEADER) (1)
final class OldMicroService {

    @PATCH("/patch")
    void update() {
        System.out.println("old");
    }
}
1 REST controller of the old v1 version, using the Version.Strategy.HEADER strategy;
@Version(value = "v2", strategy = Version.Strategy.HEADER) (1)
final class NewMicroService {

    @PATCH("/patch")
    void update() {
        System.out.println("new");
    }
}
1 REST controller of the new v2 version, using the Version.Strategy.HEADER strategy;

Note that the rules for defining a handler are the same for two different classes!

The correct selection of the appropriate REST controller handler can be checked with the following test:

@RxMicroRestBasedMicroServiceTest({OldMicroService.class, NewMicroService.class}) (1)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @ParameterizedTest
    @CsvSource({
            "v1,    old",
            "v2,    new"
    })
    void Should_route_to_valid_request_handler(final String versionHeaderValue,
                                               final String expectedOut) {
        blockingHttpClient.patch(
                "/patch",    (2)
                HttpHeaders.of(
                        API_VERSION, (3)
                        versionHeaderValue
                )
        );

        assertEquals(expectedOut, systemOut.asString());
    }
}
1 The @RxMicroRestBasedMicroServiceTest annotation allows You to run several REST controllers in test mode.
2 The test runs the PATCH request to a URL Path: /.
3 To specify the handler version, the standard Api-Version HTTP header is used.

If only REST controllers of a certain version need to be tested, then the BlockingHttpClient component can be set up for operation of only certain versions of REST controller using the @BlockingHttpClientSettings annotation.

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.13.2. Versioning Based on URL Path Fragment Analysis

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

@Version(value = "v1", strategy = Version.Strategy.URL_PATH) (1)
final class OldMicroService {

    @PATCH("/patch")
    void update() {
        System.out.println("old");
    }
}
1 REST controller of the old v1 version, using the Version.Strategy.URL_PATH strategy;
@Version(value = "v2", strategy = Version.Strategy.URL_PATH) (1)
final class NewMicroService {

    @PATCH("/patch")
    void update() {
        System.out.println("new");
    }
}
1 REST controller of the new v2 version, using the Version.Strategy.URL_PATH strategy;

Note that the rules for defining a handler are the same for two different classes!

The correct selection of the appropriate REST controller handler can be checked with the following test:

@RxMicroRestBasedMicroServiceTest({OldMicroService.class, NewMicroService.class}) (1)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @ParameterizedTest
    @CsvSource({
            "/v1,    old",
            "/v2,    new"
    })
    void Should_route_to_valid_request_handler(final String urlVersionPath,
                                               final String expectedOut) {
        blockingHttpClient.patch(
                urlVersionPath + "/patch"    (2)
        );

        assertEquals(expectedOut, systemOut.asString());
    }
}
1 The @RxMicroRestBasedMicroServiceTest annotation allows You to run several REST controllers in test mode.
2 The test runs the PATCH request to different URL Paths;

If only REST controllers of a certain version need to be tested, then the BlockingHttpClient component can be set up for operation of only certain versions of REST controller using the @BlockingHttpClientSettings annotation.

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.14. Base URL Path for All Handlers

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

@BaseUrlPath("base-url-path")
final class MicroService {

    @GET("/path")
    void path() {
    }
}
@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @Test
    void Should_support_base_url() {
        final ClientHttpResponse response = blockingHttpClient.get("/base-url-path/path");

        assertEquals(200, response.getStatusCode());
    }
}

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.15. CORS Support

The RxMicro framework supports the Cross Origin Resource Sharing (CORS).

To activate this function it is necessary to add the @EnableCrossOriginResourceSharing annotation:

@EnableCrossOriginResourceSharing (1)
final class MicroService {

    @PATCH("/")
    void handle() {
        System.out.println("handle");
    }
}
1 The Cross Origin Resource Sharing (CORS) feature is activated for all handlers of the specified REST controller.

When activating this feature, the RxMicro framework automatically adds a standard handler:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @Test
    void Should_handle_PATCH_request() {
        blockingHttpClient.patch("/");

        assertEquals("handle", systemOut.asString());
    }

    (1)
    @Test
    void Should_support_CORS_Options_request() {
        final ClientHttpResponse response = blockingHttpClient.options(
                "/",
                HttpHeaders.of(
                        ORIGIN, "test.rxmicro.io",
                        ACCESS_CONTROL_REQUEST_METHOD, PATCH
                )
        );

        assertEquals(ORIGIN, response.getHeaders().getValue(VARY));
        assertEquals("*", response.getHeaders().getValue(ACCESS_CONTROL_ALLOW_ORIGIN));
        assertEquals(PATCH.name(), response.getHeaders().getValue(ACCESS_CONTROL_ALLOW_METHODS));
    }
}
1 The standard handler can handle OPTIONS requests with additional HTTP headers.

For more information on the Cross Origin Resource Sharing (CORS) support, check out the following examples:

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.16. Request ID

The Request Id feature described at Monitoring section.

5.17. Configuring the Code Generation Process

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

import io.rxmicro.rest.model.GenerateOption;
import io.rxmicro.rest.model.ServerExchangeFormatModule;
import io.rxmicro.rest.server.RestServerGeneratorConfig;

@RestServerGeneratorConfig(
        exchangeFormat = ServerExchangeFormatModule.AUTO_DETECT, (1)
        generateRequestValidators = GenerateOption.AUTO_DETECT,  (2)
        generateResponseValidators = GenerateOption.DISABLED     (3)
)
module rest.controller.generator {
    requires rxmicro.rest.server.netty;
}
1 The exchangeFormat parameter allows You to specify a format for message exchange with a client.
(By default, it is used the message exchange format 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 parameter, You need to specify which module should be used for REST controllers.)
2 The generateRequestValidators parameter allows enabling/disabling the option of generating HTTP request validators for all handlers 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.)
3 The generateResponseValidators parameter allows enabling/disabling the option of generating HTTP response validators for all handlers in the project. (The DISABLED value means that validators won’t be generated by the RxMicro Annotation Processor.)

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

(HTTP response validation can be useful for identifying errors in business task implementation algorithms. For example, instead of returning an incorrect response model to a client, the microservice 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 response validators are generated but not invoked! To activate the validation of responses it is necessary to set enableAdditionalValidations property:

new Configs.Builder()
        .withConfigs(new RestServerConfig()
                .setEnableAdditionalValidations(true)) (1)
        .build();
1 Enables the response validators

or

export rest-server.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!

6. REST Client

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!)

6.1. REST Client Implementation Requirements

6.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.

6.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: Table 2, “Which class from a reactive library must be choose?”)).

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.

6.2. RxMicro Annotations

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

Table 11. 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 controller.

@Version

Denotes a version of the REST controller.

@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 response, created by the request handler.

@SetHeader

Denotes a static HTTP header that must be set to the response, created by the request handler.

@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.)

@PathVariable

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

@RemoteAddress

Declares the Java model field as a field, in which the RxMicro framework must inject the remote client connection address.

@RequestMethod

Declares the Java model field as a field, in which the RxMicro framework must inject a method of the received request.

(This feature is useful for request logging when one handler supports different HTTP methods.)

@RequestUrlPath

Declares the Java model field as a field, in which the RxMicro framework must inject URL path of the received request.

(This feature is useful for request logging using path-variables.)

@RequestBody

Declares the Java model field as a field, in which the RxMicro framework must inject a body of the received request.

@ResponseStatusCode

Indicates to the RxMicro framework that the value of the Java model field should be used as a status code to be sent to the client.

@ResponseBody

Indicates to the RxMicro framework that the value of the Java model field should be used as a body to be sent to the client.

@RequestId

Declares the Java model field as a field, in which the RxMicro framework must inject a unique request ID.

@SetStatusCode

Declares a status code, which should be sent to the client in case of successful execution of the HTTP request handler.

@NotFoundMessage

Declares a message returned by the handler in case of no result.

@RestServerGeneratorConfig

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

@EnableCrossOriginResourceSharing

Activates the Cross Origin Resource Sharing for all request handlers in the REST controller.

@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.

6.3. HTTP Request Handler Return Types

The HTTP request handler supports two categories of returned results:

  • HTTP response without body;

  • HTTP response with body;

6.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.

6.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.

6.4. HTTP Headers Handling

6.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.

6.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.

6.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.

6.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.

6.5. HTTP Parameters Handling

6.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.

6.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.

6.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.

6.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.

6.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.6. The path variables Support

6.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.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!

6.7. Support of Internal Data Types

6.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.

6.7.2. Supported Internal Data Types

Table 12. 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.

6.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.

6.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.

6.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.

6.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.

6.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.

6.11. Request ID

The Request Id feature described at Monitoring section.

6.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.

6.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!

7. Validation

The RxMicro framework supports validation of HTTP requests and responses.

All classes and annotations required for data validation are available in the rxmicro.validation module.

7.1. Preparatory Steps

To activate the validation module in a microservices project, the following steps must be taken:

  1. Add the required dependencies to pom.xml.

  2. Add the rxmicro.validation module to module-info.java.

7.1.1. Adding the Required Dependencies:

To activate the validation module in a microservices project, it is necessary to add the rxmicro-validation library:

<dependencies>
    <dependency>
        <groupId>io.rxmicro</groupId>
        <artifactId>rxmicro-validation</artifactId>
        <version>${rxmicro.version}</version>
    </dependency>
</dependencies>

7.1.2. Adding the rxmicro.validation Module to module-info.java

module example {
    requires rxmicro.validation; (1)
}
1 Adding the request and response validation module.

7.2. Built-in Constraints.

The RxMicro framework supports the following built-in constraints, that are available at the io.rxmicro.validation.constraint package:

Table 13. Supported Built-in Constraints.
Annotation Supported Type Description

@AllowEmptyString

java.lang.String

The annotated element may be optional, i.e. empty string BUT must be not null!

@AssertFalse

java.lang.Boolean

The annotated element must be false.

@AssertTrue

java.lang.Boolean

The annotated element must be true.

@Base64URLEncoded

java.lang.String

The annotated element must be a valid Base64 string:

@CountryCode

java.lang.String

The annotated element must be a valid country code:

@DigitsOnly

java.lang.String

The annotated element must be a string value with digit characters only.

@Email

java.lang.String

The annotated element must be a well-formed email address.

@EndsWith

java.lang.String

The annotated element value must end with the provided suffix.

@Enumeration

The annotated element must be an element of the predefined enumeration.

This validation rule is useful when a Java enum type is not applicable. For example: if an enum item name equals to a Java keyword. To solve this issue use @Enumeration annotation, otherwise use a Java enum.

@Future

java.time.Instant

The annotated element must be an instant in the future.

@FutureOrPresent

java.time.Instant

The annotated element must be an instant in the present or in the future.

@HostName

java.lang.String

The annotated element must be a valid host name.

@IP

java.lang.String

The annotated element must be a valid IP address:

@Lat

java.math.BigDecimal

The annotated element must be a valid latitude coordinate.

@LatinAlphabetOnly

java.lang.String

The annotated element must be a string with latin alphabet letters only.

@Length

java.lang.String

The annotated element must have the expected string length.

@Lng

java.math.BigDecimal

The annotated element must be a valid longitude coordinate.

@Lowercase

java.lang.String

The annotated element must a lowercase string.

@MaxDouble

The annotated element must be a double whose value must be lower to the specified maximum.

@MaxInt

The annotated element must be a byte or short or integer or long whose value must be lower or equal to the specified maximum.

@MaxLength

java.lang.String

The annotated element must have a string length whose value must be lower or equal to the specified maximum.

@MaxNumber

The annotated element must be a number whose value must be lower or equal to the specified maximum.

@MaxSize

The annotated element must have a list size whose value must be lower or equal to the specified maximum.

@MinDouble

The annotated element must be a double whose value must be higher or equal to the specified minimum.

@MinInt

The annotated element must be a byte or short or integer or long whose value must be higher or equal to the specified minimum.

@MinLength

java.lang.String

The annotated element must have a string length whose value must be higher or equal to the specified minimum.

@MinNumber

The annotated element must be a number whose value must be higher or equal to the specified minimum.

@MinSize

The annotated element must have a list size whose value must be higher or equal to the specified minimum.

@Nullable

? extends java.lang.Object

The annotated element may be optional, i.e. null.

@NullableArrayItem

The annotated array element may be optional, i.e. null.

@Numeric

java.math.BigDecimal

The annotated element must be a decimal within accepted range (scale and precision).

@Past

java.time.Instant

The annotated element must be an instant in the past.

@PastOrPresent

java.time.Instant

The annotated element must be an instant in the past or in the present.

@Pattern

java.lang.String

The annotated String must match the specified regular expression. The regular expression follows the Java regular expression conventions.

(See java.util.regex.Pattern).

@Phone

java.lang.String

The annotated element must be a valid phone number.

@Size

The annotated element must have the expected list size.

@Skype

java.lang.String

The annotated element must be a valid skype number.

@StartsWith

java.lang.String

The annotated element value must start with the provided prefix.

@SubEnum

? extends java.lang.Enum

The annotated element must be an enumeration with predefined sub sequence.

@Telegram

java.lang.String

The annotated element must be a valid telegram number.

@TruncatedTime

java.time.Instant

The annotated element must be an instant with truncated time value.

@UniqueItems

java.util.List

The annotated element must contain unique items.

@Uppercase

java.lang.String

The annotated element must an uppercase string.

@URI

java.lang.String

The annotated element must be a valid java.net.URI address.

(See Uniform Resource Identifier)

@URLEncoded

java.lang.String

The annotated element must be a valid URL encoded value.

@Viber

java.lang.String

The annotated element must be a valid viber number.

@WhatsApp

java.lang.String

The annotated element must be a valid whatsapp number.

The RxMicro framework analyzes built-in constraints when drawing up the project documentation.

Therefore, a properly selected annotation, in addition to its main purpose, makes it possible to automatically generate more accurate project documentation!

7.3. HTTP Requests Server Validation

After activation of the rxmicro.validation module in the module-info.java descriptor

module examples.validation.server.basic {
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;
    requires rxmicro.validation; (1)
}
1 rxmicro.validation is a module for request and response validation.

the developer can use built-in constraints to validate HTTP request parameters:

final class MicroService {

    @PUT(value = "/", httpBody = false)
    void consume(final @Email String email) { (1)
        System.out.println("Email: " + email);
    }

}
1 The email parameter is annotated by the @Email annotation.

Due to this annotation the RxMicro Annotation Processor will automatically generate a validator for email HTTP parameter:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    private SystemOut systemOut;

    @ParameterizedTest
    @CsvSource({
            "/,                     Parameter \"email\" is required!",  (1)
            "/?email=,              Parameter \"email\" is required!",
            "/?email=rxmicro,       Invalid parameter \"email\": Expected a valid email format!",
            "/?email=rxmicro.io,    Invalid parameter \"email\": Expected a valid email format!",
            "/?email=@rxmicro.io,   Invalid parameter \"email\": Expected a valid email format!",
            "/?email=rxmicro.io@,   Invalid parameter \"email\": Expected a valid email format!",
            "/?email=@.rxmicro.io,  Invalid parameter \"email\": Expected a valid email format!",
            "/?email=rxmicro.io@.,  Invalid parameter \"email\": Expected a valid domain name!"
    })
    void Should_return_invalid_request_status(final String path,
                                              final String expectedErrorMessage) {
        final ClientHttpResponse response = blockingHttpClient.put(path);

        assertEquals(jsonErrorObject(expectedErrorMessage), response.getBody()); (2)
        assertEquals(400, response.getStatusCode()); (2)
        assertTrue(systemOut.isEmpty(), "System.out is not empty: " + systemOut.asString()); (3)
    }

    @Test
    void Should_handle_request() {
        final ClientHttpResponse response = blockingHttpClient.put("/?email=welcome@rxmicro.io");

        assertEquals("Email: welcome@rxmicro.io", systemOut.asString()); (4)
        assertEquals(200, response.getStatusCode()); (4)
    }

}
1 When activating the rxmicro.validation module, all query parameters are automatically considered as required.
(Therefore the RxMicro Annotation Processor automatically adds a required validator for each parameter. If the parameter should be optional, the model field should be annotated with the @Nullable annotation.)
2 If request parameters are invalid, HTTP server automatically returns a status code 400 and JSON object of standard structure with detailed error description.
3 If an HTTP request validation error occurs, the request handler isn’t invoked from the REST controller.
4 If the request parameters are valid, control is transferred to the request handler from the REST controller.

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.4. HTTP Responses Server Validation

In addition to validating HTTP requests, the RxMicro framework also provides the option to validate HTTP responses.

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

If current implementation of the business task doesn’t contain any errors, then HTTP response validation will consume computing resources senselessly. In this case, HTTP response validation must be disabled!

By default the response validators are generated but not invoked!

To activate the validation of responses it is necessary to set enableAdditionalValidations property:

new Configs.Builder()
        .withConfigs(new RestServerConfig()
                .setEnableAdditionalValidations(true)) (1)
        .build();
1 Enables the response validators

or

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

or using any other supported config types

The HTTP response model class can contain any built-in or custom constraint annotations:

public final class Response {

    final String message; (1)

    public Response(final String message) {
        this.message = message;
    }

}
1 In this example, the message field doesn’t explicitly contain any constraint annotations. But since to the module-info.java descriptor was added the rxmicro.validation module, then all model fields not marked with the @Nullable annotation are automatically required. (In other words, such fields are implicitly marked by a virtual constraint annotation @Required.)

To emulate an incorrect business value, null value is passed in the request handler to the message field:

final class MicroService {

    @GET("/")
    CompletableFuture<Response> get() {
        return CompletableFuture.completedFuture(new Response(null));
    }

}

In case of invalid HTTP response, the RxMicro framework returns HTTP response with a status 500 and standard error model:

@RxMicroRestBasedMicroServiceTest(MicroService.class)
final class MicroServiceTest {

    private BlockingHttpClient blockingHttpClient;

    @Test
    void Should_produce_internal_error() {
        final ClientHttpResponse response = blockingHttpClient.get("/");

        assertEquals(
                jsonErrorObject("Response is invalid: Parameter \"message\" is required!"),
                response.getBody()
        );
        assertEquals(500, response.getStatusCode());
    }

}

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.5. HTTP Responses Client Validation

After activation of the rxmicro.validation module in the module-info.java descriptor

module examples.validation.client.basic {
    requires rxmicro.rest.client;
    requires rxmicro.rest.client.exchange.json;
    requires rxmicro.validation; (1)
}
1 rxmicro.validation is a module for request and response validation.

the developer can use built-in constraints to validate HTTP request parameters:

public final class Response {

    (1)
    @Email
    String email;

    public String getEmail() {
        return email;
    }

}
1 The email parameter is annotated by the @Email annotation.

After setting up the model class, this model must be used in the REST client:

@RestClient
public interface RESTClient {

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

}

When converting the content of an HTTP response to the Response class object, the validator will be invoked automatically:

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

    private static final HttpRequestMock HTTP_REQUEST_MOCK = new HttpRequestMock.Builder()
            .setMethod(HttpMethod.GET)
            .setPath("/")
            .build();

    private RESTClient restClient;

    @Alternative
    @Mock
    private HttpClientFactory httpClientFactory;

    private void prepareValidResponse() {
        prepareHttpClientMock(
                httpClientFactory,
                HTTP_REQUEST_MOCK,
                jsonObject("email", "welcome@rxmicro.io") (1)
        );
    }

    @Test
    @BeforeThisTest(method = "prepareValidResponse")
    void Should_return_received_email() {
        final Response response = restClient.get().join();
        assertEquals("welcome@rxmicro.io", response.getEmail()); (1)
    }

    private void prepareInvalidResponse() {
        prepareHttpClientMock(
                httpClientFactory,
                HTTP_REQUEST_MOCK,
                jsonObject("email", "rxmicro.io") (2)
        );
    }

    @Test
    @BeforeThisTest(method = "prepareInvalidResponse")
    void Should_throw_UnexpectedResponseException() {
        final Throwable throwable =
                (4)
                getRealThrowable(assertThrows(RuntimeException.class, () -> restClient.get().join())); (2)
        assertEquals(UnexpectedResponseException.class, throwable.getClass()); (3)
        assertEquals(
                "Response is invalid: " +
                        "Invalid parameter \"email\": Expected a valid email format!", (3)
                throwable.getMessage()
        );
    }

}
1 If the REST client receives a valid HTTP response, no errors will occur.
2 If the email field is incorrect in the HTTP response, the REST client will return an error signal.
3 If the HTTP response is incorrect, the UnexpectedResponseException class exception is returned with a detailed text message.
4 Since CompletableFuture when receiving an error signal ALWAYS returns the CompletionException class exception, to get the original exception class, which was thrown during the lazy evaluation, it is necessary to use the Exceptions.getRealThrowable(Throwable) utility method!

Check out the following example to find out the features of the RxMicro framework for HTTP request validation in REST clients:

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.6. HTTP Requests Client Validation

In addition to validating HTTP responses, the RxMicro framework also provides the option to validate HTTP requests.

(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.)

If current implementation of the business task doesn’t contain any errors, then HTTP request validation will consume computing resources senselessly. In this case, HTTP request validation must be disabled!

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

7.7. Creating Custom Constraints

If built-in constraints are not enough, the developer can create custom constraint. To do so, the following steps must be taken:

  1. Create a constraint annotation.

  2. Implement a validator.

A validation annotation is an annotation that meets the following requirements:

(1)
@Retention(CLASS)
(2)
@Target({FIELD, METHOD, PARAMETER})
(3)
@ConstraintRule(
        supportedTypes = {
                BigDecimal.class                        (4)
        },
        validatorClass = {
                ExpectedZeroConstraintValidator.class   (5)
        }
)
public @interface ExpectedZero {

    boolean off() default false; (6)

}
1 The annotation is only available at the compilation level.
2 This annotation allows validating class fields, class methods (setters and getters) and method parameters.
3 The @ConstraintRule annotation is used to indicate:
4 data type;
5 validator class.
6 Each constraint annotation requires a required boolean off() default false; parameter, that allows You to disable the validator.
(This feature is useful for model inheritance when a parameter from a child class should not be validated and a parameter from a parent class should be validated!)

Validator is a class that meets the following requirements:

public final class ExpectedZeroConstraintValidator
        implements ConstraintValidator<BigDecimal> { (1)

    @Override
    public void validate(final BigDecimal value,
                         final HttpModelType httpModelType,
                         final String modelName) throws ValidationException {
        if (value != null) { (2)
            if (value.compareTo(BigDecimal.ZERO) != 0) { (3)
                throw new ValidationException(
                        "Invalid ? \"?\": Expected a zero value!",
                        httpModelType, modelName
                );
            }
        }
    }

}
1 The validator class must implement the ConstraintValidator interface parameterized by the data type.
(If constraint annotation can be applied to different data types, a separate validator class must be created for each data type.)
2 Validator must ignore null values.
(To check for null there is a predefined validator.)
3 If the parameter is incorrect, the ValidationException exception with a clear error message must be thrown.

Using a custom validator is no different from using a predefined validator:

final class MicroService {

    @PATCH("/")
    void consume(final @ExpectedZero BigDecimal value) {
        System.out.println(value);
    }

}
@ParameterizedTest
@CsvSource({
        "/,                     Parameter \"value\" is required!",
        "/?value=,              Parameter \"value\" is required!",
        "/?value=1.23,          Invalid parameter \"value\": Expected a zero value!",
        "/?value=-3.45,         Invalid parameter \"value\": Expected a zero value!"
})
void Should_return_invalid_request_status(final String path,
                                          final String expectedErrorMessage) {
    final ClientHttpResponse response = blockingHttpClient.patch(path);

    assertEquals(jsonErrorObject(expectedErrorMessage), response.getBody());
    assertEquals(400, response.getStatusCode());
    assertEquals("", systemOut.asString());
}

@ParameterizedTest
@ValueSource(strings = {"0", "0.0", "0.000", "-0.0"})
void Should_handle_request(final String value) {
    final ClientHttpResponse response = blockingHttpClient.patch("/?value=" + value);

    assertEquals(new BigDecimal(value).toString(), systemOut.asString());
    assertEquals(200, response.getStatusCode());
}

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.8. Disabling Validation

To disable the generation of validators, You must perform one of the following steps:

  1. Delete the rxmicro.validation module from the module-info.java descriptor.

  2. Use GenerateOption.DISABLED options to disable specific categories of validators.

  3. Use the @DisableValidation annotation to disable validators of a selected group of classes in the project.

7.8.1. Removing the rxmicro.validation Module

The easiest and fastest way to disable the generation of validators for all classes in the current module is to remove the rxmicro.validation module from the module-info.java descriptor.

After deleting the rxmicro.validation module DON’T FORGET to recompile the whole project for the changes to take effect: mvn clean compile!

7.8.2. Using GenerateOption.DISABLED Option

To disable the generation of validators by category, it is necessary to use annotations:

@RestServerGeneratorConfig(
        generateRequestValidators = GenerateOption.DISABLED,    (1)
        generateResponseValidators = GenerateOption.DISABLED    (2)
)
@RestClientGeneratorConfig(
        generateRequestValidators = GenerateOption.DISABLED,    (3)
        generateResponseValidators = GenerateOption.DISABLED    (4)
)
module examples.validation {
    requires rxmicro.rest.server;
    requires rxmicro.rest.client;
    requires rxmicro.validation;
}
1 Validators for all models of HTTP requests in the current project won’t be generated.
2 Validators for all models of HTTP responses in the current project won’t be generated.
3 Validators for all models of HTTP requests in the current project won’t be generated.
4 Validators for all models of HTTP responses in the current project won’t be generated.

Upon activation of the rxmicro.validation module, by default the RxMicro framework generates only HTTP request validators for REST controllers and HTTP response validators for REST clients!

All other categories of validators must be manually activated using the @RestServerGeneratorConfig and @RestClientGeneratorConfig annotations!

After changing the settings using the @RestServerGeneratorConfig and @RestClientGeneratorConfig annotations DON’T FORGET to recompile the whole project for the changes to take effect: mvn clean compile!

7.8.3. Using @DisableValidation Annotation

The @DisableValidation annotation provides an opportunity to disable the generation of validators for the selected group of classes in the project:

  1. If a model class is annotated by this annotation, then only for this model class the validator won’t be generated.

  2. If this annotation annotates the package-info.java class, then for all classes from the specified package and all its subpackages no validators will be generated.

  3. If this annotation annotates the module-info.java descriptor, then for all classes in the current module no validators will be generated.
    (This behavior is similar to the removal of the rxmicro.validation module from the module-info.java descriptor.)

After adding the @DisableValidation annotation DON’T FORGET to recompile the whole project for the changes to take effect: mvn clean compile!

8. REST-based Microservice Documentation

REST-based microservice documentation describes rules of interaction with REST-based microservices for fast and easy integration with them.

Currently, the RxMicro framework only supports specialized documentation format based on the AsciiDoc format.

8.1. Basic Usage

Default settings allow You to generate REST-based microservice documentation using minimal configurations.
(This advantage is achieved through close integration with other RxMicro modules.)

8.1.1. Min Settings

To generate REST-based microservice documentation during the compilation of the microservice project, the following two steps must be done:

  • Add the rxmicro-documentation-asciidoctor dependency to pom.xml of the microservice project:

<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-documentation-asciidoctor</artifactId>
    <version>${rxmicro.version}</version>
</dependency>
  • Add the rxmicro.documentation.asciidoctor module to the module-info.java descriptor of the microservice project:

module examples.documentation.asciidoctor.quick.start {
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;
    requires static rxmicro.documentation.asciidoctor; (1)
}
1 The rxmicro.documentation.asciidoctor must be added using the static modifier.
(For more information on the benefits of the static modifier, refer to the Section 8.1.3, “Asciidoctor-dependency-plugin Settings”.)

After performing these steps during compiling the microservice project, the RxMicro Annotation Processor will generate the ExamplesDocumentationAsciidoctorQuickStartDocumentation.adoc file. The generated file will contain REST-based microservice documentation in the AsciiDoc format, and by default will be located in the ./src/main/asciidoc folder.

In terms of the version control system, the ./src/main/asciidoc folder is the source code, so it will be controlled by this system. Therefore, if REST-based microservice documentation is to be dynamically generated, it is necessary to:

  • specify that files in the ./src/main/asciidoc folder must be ignored by the version control system;

  • or configure the rxmicro.documentation.asciidoctor module, so that it generates REST-based microservice documentation in another folder, for example in the target/docs.
    (To configure the folder in which REST-based microservice documentation should be generated, refer to the core.html.)

Using the ./src/main/asciidoc folder by default provides the following benefits:

  • The asciidoctor-maven-plugin, which converts the AsciiDoc format to HTML or PDF , by default reads files from the ./src/main/asciidoc folder.

  • Automatic building of REST-based microservice documentation can be used as an initial version of the document. After formation of the initial version, the generation can be disabled and all changes to the documentation will be made manually by the developer and stored in the version control system.

  • Compared to the previous version, a more efficient way to keep REST-based microservice documentation up to date is to divide all documentation into fragments. Static fragments (business task description, charts and figures) will be created manually by the developer. Dynamic fragments will be generated by the RxMicro Annotation Processor at each project compilation. As a result, a final document containing static and dynamic fragments will be compiled from these fragments.
    (When using this approach, do not forget to add the all dynamic fragments to ignore list for Your version control system!)

8.1.2. Asciidoctor-maven-plugin Settings

The asciidoctor-maven-plugin plugin allows You to convert documentation from the AsciiDoc format to HTML, PDF and other formats.

<plugin>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId> (1)
    <version>${asciidoctor-maven-plugin.version}</version>
    <executions>
        <execution>
            <id>output-html5</id>
            <phase>package</phase> (2)
            <goals>
                <goal>process-asciidoc</goal>
            </goals>
            <configuration>
                <backend>html5</backend> (3)
                <sourceHighlighter>highlight.js</sourceHighlighter> (4)
                <preserveDirectories>true</preserveDirectories>
                <relativeBaseDir>true</relativeBaseDir>
                <attributes> (5)
                    <icons>font</icons>
                    <pagenums/>
                    <toclevels>3</toclevels>
                </attributes>
            </configuration>
        </execution>
    </executions>
</plugin>
1 The latest stable version of the asciidoctor-maven-plugin.
2 The asciidoctor-maven-plugin must convert REST-based microservice documentation at the package phase.
3 Using the backend directive, You can specify in which format the AsciiDoc document should be converted.
4 If the AsciiDoc document contains examples of source code in a programming language, You need to add the js library for highlighting the syntax of that language.
5 Using the attributes directive, it is possible to override the attributes of the AsciiDoc document.

For more information about the asciidoctor-maven-plugin plugin features, refer to the documentation:

8.1.3. Asciidoctor-dependency-plugin Settings

REST-based microservice documentation is generated during the compilation process, therefore the libraries used to configure the REST-based microservice documentation generation process are not required in runtime. Therefore, the maven-dependency-plugin should not copy artifacts related to the generation of REST-based microservice documentation to the lib folder:

<plugin>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>${maven-dependency-plugin.version}</version>
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.directory}/lib</outputDirectory>
                <includeScope>compile</includeScope>
                <excludeArtifactIds> (1)
                    rxmicro-documentation,
                    rxmicro-documentation-asciidoctor
                </excludeArtifactIds>
            </configuration>
        </execution>
    </executions>
</plugin>
1 The rxmicro-documentation and rxmicro-documentation-asciidoctor artifacts are required only during the compilation process.

In order that in runtime no errors occur when excluding the artifacts required for REST-based microservice documentation generation, it is required to use the static modifier in the module-info.java descriptor: requires static rxmicro.documentation.asciidoctor;.

Without this modifier the following error will occur when starting the microservice: Module rxmicro.documentation.asciidoctor not found!

When compiling a project, the RxMicro framework will generate the following project documentation according to the settings of the rxmicro.documentation.asciidoctor module defined in this section:

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. RxMicro Annotations

The RxMicro framework supports the following RxMicro Annotations:

Table 14. Supported RxMicro Annotations.
Annotation Description

@Author

Denotes the author of the generated REST-based microservice documentation.

(Allows You to override the author specified in the developer directive to pom.xml.)

@BaseEndpoint

Denotes the basic endpoint in the generated REST-based microservice documentation.

(Allows You to override the basic endpoint specified in the url directive to pom.xml.)

@Description

Denotes the description of the generated REST-based microservice documentation.

(Allows You to override the description specified in the description directive to pom.xml.)

In addition to the description of all REST-based microservice documentation, this annotation also allows You to specify a description of separate elements: sections, model fields, etc.

@DocumentationDefinition

A composite annotation that specifies the settings for generating a whole document.

@DocumentationVersion

Denotes the version of REST-based microservice in the generated REST-based microservice documentation.

(Allows You to override the version of REST-based microservice specified in the version directive to pom.xml.)

@Example

Denotes the model field value used as an example in the generated REST-based microservice documentation.

@IncludeDescription

Denotes the AsciiDoc fragment to be imported into the generated REST-based microservice documentation.

In addition to the description of all REST-based microservice documentation, this annotation also allows You to specify the AsciiDoc fragment for separate elements: sections, model fields, etc.

@IntroductionDefinition

A composite annotation that specifies the settings for generating the Introduction section.

@License

Denotes the license of REST-based microservice in the generated REST-based microservice documentation.

(Allows You to override the license of REST-based microservice specified in the license directive to pom.xml.)

@ModelExceptionErrorResponse

Denotes the exception class to be analyzed by the RxMicro Annotation Processor for generating the unsuccessful HTTP response description of REST-based microservice.

@ResourceDefinition

A composite annotation that specifies the settings for generating the ResourceDefinition section.

@ResourceGroupDefinition

A composite annotation that specifies the settings for generating the ResourceGroupDefinition section.

@SimpleErrorResponse

Contains metadata about the unsuccessful HTTP response of REST-based microservice.

@Title

Denotes the name of the generated REST-based microservice documentation.

(Allows You to override the name of the generated REST-based microservice documentation specified in the name directive to pom.xml.)

@DocumentAttributes

Allows You to specify AsciiDoc attributes for the generated REST-based microservice documentation.

8.3. @Example and @Description Usage

Using the @Example annotation, the developer can specify the data that is as close to the real data as possible, which will be used to build examples of usage in REST-based microservice documentation.

Using the @Description annotation, the developer can specify the detailed description of separate model fields to be used in building REST-based microservice documentation.

public final class Echo {

    @Example("EchoExample")
    @Description("EchoDescription")
    String echo;
}
final class MicroService {

    @GET("/")
    @POST("/")
    @POST(value = "/", httpBody = false)
    CompletableFuture<Echo> echo(final Echo echo) {
        return completedFuture(echo);
    }
}

When compiling a project, the RxMicro framework will generate the following project documentation according to the settings of the rxmicro.documentation.asciidoctor module defined in this section:

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.4. Sections Customization

Using the following composite annotations:

the developer can regroup standard sections of REST-based microservice documentation, as well as add his own fragments as sections.

@DocumentationDefinition(
        withGeneratedDate = false,
        introduction = @IntroductionDefinition(
                sectionOrder = {
                        IntroductionDefinition.Section.LICENSES,
                        IntroductionDefinition.Section.SPECIFICATION,
                        IntroductionDefinition.Section.CUSTOM_SECTION
                },
                customSection = {
                        "${PROJECT-DIR}/src/main/asciidoc/_fragment/" +
                                "custom-introduction-content.asciidoc"
                },
                includeMode = IncludeMode.INLINE_CONTENT
        ),
        resourceGroup = @ResourceGroupDefinition(
                sectionOrder = {
                        ResourceGroupDefinition.Section.CUSTOM_SECTION
                },
                customSection = {
                        "${PROJECT-DIR}/src/main/asciidoc/_fragment/" +
                                "custom-resource-group-content.asciidoc"
                },
                includeMode = IncludeMode.INLINE_CONTENT
        ),
        resource = @ResourceDefinition(
                withInternalErrorResponse = false,
                withJsonSchema = false,
                withRequestIdResponseHeader = false,
                withQueryParametersDescriptionTable = false,
                withBodyParametersDescriptionTable = false
        )
)
module examples.documentation.asciidoctor.custom.sections {
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;
    requires static rxmicro.documentation.asciidoctor;
}

Besides generating the final AsciiDoc document, the RxMicro Annotation Processor allows You to generate separate dynamic fragments.

For more information on this feature, check out the following example:

When compiling a project, the RxMicro framework will generate the following project documentation according to the settings of the rxmicro.documentation.asciidoctor module defined in this section:

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.5. Integration with rxmicro.validation Module

The rxmicro.documentation.asciidoctor module is integrated with the rxmicro.validation module. Thanks to this integration, when building the REST-based microservice documentation, the RxMicro framework analyzes all available built-in constraints for automatic generation of model fields description.

@DocumentationDefinition(
        introduction = @IntroductionDefinition(sectionOrder = {}),
        withGeneratedDate = false
)
module examples.documentation.asciidoctor.validation {
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;
    requires rxmicro.validation;
    requires static rxmicro.documentation.asciidoctor;
}
final class MicroService {

    @PUT("/")
    void consume(final @Phone String phone) { (1)
        // do something
    }
}
1 The phone parameter must be validated via built-in constraint for phones.

Thus, when building the REST-based microservice documentation, the RxMicro framework will automatically generate a description for the phone parameter:

validation integration
Figure 14. The phone field description, formed based on built-in constraints analysis

When compiling a project, the RxMicro framework will generate the following project documentation according to the settings of the rxmicro.documentation.asciidoctor module defined in this section:

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.6. REST-based Microservice Metadata Configuration

To obtain metadata while building REST-based microservice documentation, the RxMicro framework can use:

  • The pom.xml file;

  • The rxmicro.documentation module annotations.

The REST-based microservice metadata is:

  • the REST-based microservice name;

  • the REST-based microservice description;

  • the REST-based microservice version;

  • the list of licenses that cover the REST-based microservice;

  • the list of REST-based microservice developers;

  • the REST-based microservice basic endpoint.

8.6.1. Using pom.xml

To specify the metadata needed to generate the REST-based microservice documentation, You can use the pom.xml file

<project>
    <name>Metadata Pom Xml</name>
    <description>*Project* _Description_</description>
    <developers>
        <developer>
            <name>Richard Hendricks</name>
            <email>richard.hendricks@piedpiper.com</email>
        </developer>
        <developer>
            <name>Bertram Gilfoyle</name>
            <email>bertram.gilfoyle@piedpiper.com</email>
        </developer>
        <developer>
            <name>Dinesh Chugtai</name>
            <email>dinesh.chugtai@piedpiper.com</email>
        </developer>
    </developers>
    <url>https://api.rxmicro.io</url>
    <licenses>
        <license>
            <name>Apache License 2.0</name>
            <url>https://github.com/rxmicro/rxmicro/blob/master/LICENSE</url>
        </license>
    </licenses>
</project>

When compiling a project, the RxMicro framework will generate the following project documentation according to the settings of the rxmicro.documentation.asciidoctor module defined in this section:

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.6.2. Using Annotations

To specify the metadata needed to generate the REST-based microservice documentation, You can use the rxmicro.documentation module annotations.

@Title("Metadata Annotations")
@Description("*Project* _Description_")
@DocumentationVersion("0.0.1")
@Author(
        name = "Richard Hendricks",
        email = "richard.hendricks@piedpiper.com"
)
@Author(
        name = "Bertram Gilfoyle",
        email = "bertram.gilfoyle@piedpiper.com"
)
@Author(
        name = "Dinesh Chugtai",
        email = "dinesh.chugtai@piedpiper.com"
)
@BaseEndpoint("https://api.rxmicro.io")
@License(
        name = "Apache License 2.0",
        url = "https://github.com/rxmicro/rxmicro/blob/master/LICENSE"
)
@DocumentationDefinition(
        introduction = @IntroductionDefinition(
                sectionOrder = {
                        IntroductionDefinition.Section.BASE_ENDPOINT,
                        IntroductionDefinition.Section.LICENSES
                }
        ),
        resource = @ResourceDefinition(
                withInternalErrorResponse = false
        ),
        withGeneratedDate = false
)
module examples.documentation.asciidoctor.metadata.annotations {
    requires rxmicro.rest.server.netty;
    requires rxmicro.rest.server.exchange.json;
    requires static rxmicro.documentation.asciidoctor;
}

When compiling a project, the RxMicro framework will generate the following project documentation according to the settings of the rxmicro.documentation.asciidoctor module defined in this section:

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.7. Error Documentation

To describe unsuccessful HTTP responses, the rxmicro.documentation module provides two annotations:

Unresolved directive in _fragment/_project-documentation/errors.adoc - include::../../../../../../examples/group-documentation-asciidoctor/documentation-asciidoctor-errors/src/main/java/io/rxmicro/examples/documentation/asciidoctor/errors/ProxyMicroService.java[tags=content]
1 The @SimpleErrorResponse annotation allows You to explicitly specify the HTTP response description.
2 The status parameter describes the HTTP status code.
3 The description parameter describes the text description.
4 The exampleErrorMessage parameter denotes the value used as an example in the generated REST-based microservice documentation.
5 The @ModelExceptionErrorResponse annotation allows You to specify the exception class of the standard
6 or custom type.

When using the custom exception type, this class contains all necessary parameters for building REST-based microservice documentation:

Unresolved directive in _fragment/_project-documentation/errors.adoc - include::../../../../../../examples/group-documentation-asciidoctor/documentation-asciidoctor-errors/src/main/java/io/rxmicro/examples/documentation/asciidoctor/errors/model/NotAcceptableException.java[tags=content]
1 The required STATUS_CODE static field describes the HTTP status code.
2 The @Description annotation describes the text description.
3 The @Example annotation describes the value used as an example in the generated REST-based microservice documentation.

The custom exception class can contain not string parameter(s). For such classes the RxMicro framework returns custom JSON model instead of standard one.

Unresolved directive in _fragment/_project-documentation/errors.adoc - include::../../../../../../examples/group-documentation-asciidoctor/documentation-asciidoctor-errors/src/main/java/io/rxmicro/examples/documentation/asciidoctor/errors/model/CustomErrorModelException.java[tags=content]
1 The @Description annotation describes the text description.
2 The required STATUS_CODE static field describes the HTTP status code.
3 The custom field can contain @Example and @Description annotations.
4 The custom field(s) must be initialized via constructor.
5 For custom exception classes with custom field(s) the RxMicro Annotation Processor does not generate Writer, so You must override the getResponseData method manually!
6 Overridden method must return values from all declared fields at custom exception class.

If RX_MICRO_STRICT_MODE option is enabled, the RxMicro Annotation Processor validates the overidden getResponseData method and throws compilation error if some field values not returned!

When compiling a project, the RxMicro framework will generate the following project documentation according to the settings of the rxmicro.documentation.asciidoctor module defined in this section:

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. Postgre SQL Data Repositories

The RxMicro framework supports creation of dynamic repositories for interaction with databases.

To interact with PostgreSQL DB, using the reactive R2DBC PostgreSQL driver, the RxMicro framework provides the rxmicro.data.sql.r2dbc.postgresql module.

9.1. Basic Usage

To use the rxmicro.data.sql.r2dbc.postgresql module in the project, the following two steps must be taken:

  • Inject the rxmicro.data.sql.r2dbc.postgresql dependency to the pom.xml file:

<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-data-sql-r2dbc-postgresql</artifactId>
    <version>${rxmicro.version}</version>
</dependency>
  • Add the rxmicro.data.sql.r2dbc.postgresql module to the module-info.java descriptor:

module examples.data.r2dbc.postgresql.basic {
    requires rxmicro.data.sql.r2dbc.postgresql;
}

By default, the reactive R2DBC PostgreSQL driver uses the Project Reactor library, so when adding the rxmicro.data.sql.r2dbc.postgresql module, the reactor.core module is automatically added and available for usage!

After adding the rxmicro.data.sql.r2dbc.postgresql module, You can create a data model class and dynamic repository:

@Table
(1)
@ColumnMappingStrategy
public final class Account {

    @Column(length = Column.UNLIMITED_LENGTH)
    String firstName;

    @Column(length = Column.UNLIMITED_LENGTH)
    String lastName;

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}
1 The @ColumnMappingStrategy annotation sets the strategy of forming column names of the relational database table based on the analysis of the Java model class field names.
(Thus, the firstName field corresponds to the first_name column, and the lastName field corresponds to the last_name column.)
(1)
@PostgreSQLRepository
public interface DataRepository {

    (2)
    @Select("SELECT * FROM ${table} WHERE email = ?")
    Mono<Account> findByEmail(String email);
}
1 In order for a standard interface to be recognized by the RxMicro framework as a dynamic repository for interaction with PostgreSQL DB, this interface should be annotated by @PostgreSQLRepository annotation.
2 The dynamic repository may contain methods that form a query to the PostgreSQL DB.
(The query that used for a request for data uses the SQL and is specified in the annotation parameters.)

Since the dynamic repository is a RxMicro component, for its testing You need to use the microservice component testing approach:

The common approach recommended for testing dynamic repositories, that interact with PostgreSQL DB, is described in the Section 9.3.2, “Test Templates”.

@Testcontainers
@RxMicroComponentTest(DataRepository.class)
final class DataRepositoryTest {

    @Container
    private final GenericContainer<?> postgresqlTestDb =
            new GenericContainer<>("rxmicro/postgres-test-db")
                    .withExposedPorts(5432);

    @WithConfig
    private final PostgreSQLConfig config = new PostgreSQLConfig()
            .setDatabase("rxmicro")
            .setUser("rxmicro")
            .setPassword("password");

    private DataRepository dataRepository;

    @BeforeEach
    void beforeEach() {
        config
                .setHost(postgresqlTestDb.getContainerIpAddress())
                .setPort(postgresqlTestDb.getFirstMappedPort());
    }

    @Test
    void Should_find_account() {
        final Account account = requireNonNull(
                dataRepository.findByEmail("richard.hendricks@piedpiper.com").block()
        );

        assertEquals("Richard", account.getFirstName());
        assertEquals("Hendricks", account.getLastName());
    }
}

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.2. RxMicro Annotations

The RxMicro framework supports the following RxMicro Annotations:

Table 15. Supported RxMicro Annotations.
Annotation Description

@Column

Sets mapping between the column name in the PostgreSQL DB table and the Java model class field name.

(By default, the RxMicro framework uses the Java model class field name as the column name in the PostgreSQL DB table. If the name should differ for some reason, (for example, as a column name in the PostgreSQL DB table the keyword Java is used), it should be specified using this annotation!)

Required length parameter must contain the max character count that can be stored to table column. If actual length will be more that expected length, value will be trimmed automatically.

The nullable parameter does not disable any validation. It used as the descriptive characteristic only.

@ColumnMappingStrategy

Sets the strategy of column name formation in the PostgreSQL DB table, based on the analysis of the Java model class field names.

(If this annotation annotates the Java model class, then the set strategy will be used for all fields in this class. For example, if You set the default LOWERCASE_WITH_UNDERSCORED strategy, then the parentId field in the Java class will correspond to the parent_id column in the PostgreSQL DB table.)

@DataRepositoryGeneratorConfig

Allows You to configure the repository generation process.

@RepeatParameter

Allows setting mapping between one method parameter marked with this annotation and several universal placeholders that are used in the query to PostgreSQL DB.

@Select

Denotes a repository method that must execute a SELECT SQL operation

@CustomSelect

Denotes a string parameter of repository method, the value of that must be used as custom SELECT.

@Insert

Denotes a repository method that must execute a INSERT SQL operation

@Update

Denotes a repository method that must execute a UPDATE SQL operation

@Delete

Denotes a repository method that must execute a DELETE SQL operation

@ExpectedUpdatedRowsCount

Enables validation for updated rows count during DML operation, like Insert, Update and Delete operations.

If current database has invalid state the InvalidDatabaseStateException will be thrown!

@NotInsertable

Denotes a model field, the value of that ignored during INSERT SQL operation.

@NotUpdatable

Denotes a model field, the value of that ignored during UPDATE SQL operation.

@PrimaryKey

Denotes a model field that must be used as primary key.

@Schema

Denotes a schema of a database table.

@SequenceGenerator

Denotes a sequence that must be used to get the next unique value for model field.

@Table

Denotes a table name for entity.

@EnumType

Denotes a db type name for enum.

@VariableValues

Denotes a storage with the values of the predefined variables.

@PostgreSQLRepository

Denotes that an interface is a dynamic generated PostgreSQL data repository.

@PartialImplementation

Denotes an abstract class that contains a partial implementation of the annotated by this annotation a PostgreSQL Data Repository interface.

9.3. Repositories Testing

For successful functional testing of dynamic repositories, that interact with PostgreSQL DB, it is required:

  • Presence of a script that creates a test database.

  • Mechanism for preparing a database for testing: creating a database before starting the test and deleting a database after completing the test.

9.3.1. Test Database

A test database was created for testing the rxmicro.data.sql.r2dbc.postgresql module features, which are described in this section.

The test database contains three tables: account, product and order:

rxmicro test db er diagram
Figure 15. ER-Diagram for Test Database

SQL scripts for creating a test database are available at the following link:

The following classes of Java models correspond to the tables created in the test database:

@Table
@ColumnMappingStrategy
public final class Account {

    @PrimaryKey
    @SequenceGenerator
    Long id;

    @Column(length = Column.UNLIMITED_LENGTH)
    @NotUpdatable
    String email;

    @Column(length = Column.UNLIMITED_LENGTH)
    String firstName;

    @Column(length = Column.UNLIMITED_LENGTH)
    String lastName;

    BigDecimal balance;

    Role role;
}
@Table
@ColumnMappingStrategy
public final class Order {

    @PrimaryKey
    Long id;

    Long idAccount;

    Integer idProduct;

    Integer count;

    @NotInsertable
    Instant created;
}
@Table
@ColumnMappingStrategy
public final class Product {

    @PrimaryKey(autoGenerated = false)
    Integer id;

    @Column(length = Column.UNLIMITED_LENGTH)
    String name;

    BigDecimal price;

    Integer count;
}
public enum Role {

    CEO,

    Lead_Engineer,

    Systems_Architect
}

For ease of studying the rxmicro.data.sql.r2dbc.postgresql module, You can use the ready-made PostgreSQL DB image with the rxmicro/postgres-test-db test database.

The source code of the project used as a base for building this docker image, is available at the following link:

9.3.2. Test Templates

As a mechanism for preparing a database for testing (creating a database before starting the test and deleting a database after completing the test), it is most convenient to use docker.

To start docker containers in the functional test it is convenient to use the Testcontainers Java library:

(1)
@Testcontainers
(2)
@RxMicroComponentTest(DataRepository.class)
final class DataRepositoryTestTemplate1 {

    (3)
    @Container
    private static final GenericContainer<?> POSTGRESQL_TEST_DB =
            new GenericContainer<>("rxmicro/postgres-test-db")
                    .withExposedPorts(5432); (4)

    (5)
    @WithConfig
    private static final PostgreSQLConfig CONFIG = new PostgreSQLConfig()
            .setDatabase("rxmicro")
            .setUser("rxmicro")
            .setPassword("password"); (6)

    @BeforeAll
    static void beforeAll() {
        POSTGRESQL_TEST_DB.start(); (7)
        CONFIG
                .setHost(POSTGRESQL_TEST_DB.getContainerIpAddress()) (8)
                .setPort(POSTGRESQL_TEST_DB.getFirstMappedPort());
    }

    private DataRepository dataRepository; (9)

    // ... test methods must be here

    @AfterAll
    static void afterAll() {
        POSTGRESQL_TEST_DB.stop(); (10)
    }
}
1 The @Testcontainers annotation activates the start and stop of the docker containers to be used in this test.
2 Since the dynamic repository is a RxMicro component, for its testing You need to use the microservice component testing approach.
3 The @Container annotation indicates the docker container that will be used in this test. As an image on the basis of which it is necessary to create the docker container, the PostgreSQL DB ready-made image with the rxmicro/postgres-test-db test database is used.
4 When starting the docker container, You need to open the standard port for PostgreSQL DB.
5 Using the @WithConfig annotation, the configuration available only during the test is declared.
6 Setting up the configuration to interact with the test database.
7 Before running all tests, You must start the docker container.
8 After starting the docker container, You need to read the random IP address and port that will be used when connecting to the running docker container.
9 When testing microservice components, it is necessary to specify a reference to the component in which the RxMicro framework will inject the tested component.
10 After completing all the tests, You must stop the docker container.

The main advantage of this template is the speed of testing. Since the docker container is created once before starting all test methods, the total runtime of all test methods is reduced. The main disadvantage of this template is that if any test method changes the PostgreSQL DB state, the following test method may end with an error.

Therefore, this functional test template should be used for queries to PostgreSQL DB that do not change the database state!

If You need to test methods that change the PostgreSQL DB state, You should use another template:

(1)
@Testcontainers
(2)
@RxMicroComponentTest(DataRepository.class)
final class DataRepositoryTestTemplate2 {

    (3)
    @Container
    private final GenericContainer<?> postgresqlTestDb =
            new GenericContainer<>("rxmicro/postgres-test-db")
                    .withExposedPorts(5432); (4)

    (5)
    @WithConfig
    private final PostgreSQLConfig config = new PostgreSQLConfig()
            .setDatabase("rxmicro")
            .setUser("rxmicro")
            .setPassword("password"); (6)

    private DataRepository dataRepository; (7)

    @BeforeEach
    void beforeEach() {
        config
                .setHost(postgresqlTestDb.getContainerIpAddress()) (8)
                .setPort(postgresqlTestDb.getFirstMappedPort());
    }

    // ... test methods must be here
}
1 The @Testcontainers annotation activates the start and stop of the docker containers to be used in this test.
2 Since the dynamic repository is a RxMicro component, for its testing You need to use the microservice component testing approach
3 The @Container annotation indicates the docker container that will be used in this test. As an image on the basis of which it is necessary to create the docker container, the PostgreSQL DB ready-made image with the rxmicro/postgres-test-db test database is used.
4 When starting the docker container, You need to open the standard port for PostgreSQL DB.
5 Using the @WithConfig annotation, the configuration available only during the test is declared.
6 Setting up the configuration to interact with the test database.
7 When testing microservice components, it is necessary to specify a reference to the component in which the RxMicro framework will inject the tested component.
8 After starting the docker container, You need to read the random IP address and port that will be used when connecting to the running docker container.

This template for each test method will create and drop the docker container, which may increase the total runtime of all test methods.

Therefore, select the most appropriate functional test template based on the requirements of the tested functionality!

The Testcontainers library starts the docker container before running the test and stops the docker container automatically after completing the test!

So You should start and stop the docker container manually only if You want to use one docker container for all test methods!

9.4. DataBase Models

The RxMicro framework supports the following database model types:

9.4.1. Primitives

A primitive is a supported Java type that can be mapped to database table column.

The rxmicro.data.sql.r2dbc.postgresql module supports the following primitive type:

  • ? 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.Intant;

  • java.time.LocalTime;

  • java.time.LocalDate;

  • java.time.LocalDateTime;

  • java.time.OffsetDateTime;

  • java.time.ZonedDateTime;

  • java.net.InetAddress;

  • java.util.UUID;

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!

9.4.2. Entities

An entity is a composition of primitives only.

For example:

@Table
@ColumnMappingStrategy
public final class Account {

    @PrimaryKey
    @SequenceGenerator
    Long id;

    @Column(length = Column.UNLIMITED_LENGTH)
    @NotUpdatable
    String email;

    @Column(length = Column.UNLIMITED_LENGTH)
    String firstName;

    @Column(length = Column.UNLIMITED_LENGTH)
    String lastName;

    BigDecimal balance;

    Role role;
}

9.5. Universal Placeholder

The RxMicro framework recommends using the universal placeholder (?) as parameter value placeholder in the SQL queries:

@Select("SELECT * FROM ${table} WHERE email=?")
Mono<Account> findByEmail(String email);

If this method invoked with the following parameter:
repository.findByEmail("welcome@rxmicro.io");,

the RxMicro framework will generate the following PostgreSQL DB query:
SELECT * FROM account WHERE email='welcome@rxmicro.io'.

9.6. @RepeatParameter Annotation

The universal placeholder (?) is the simplest type of placeholders.

But unfortunately, it has one disadvantage: if a query parameter must be repeated, a developer must define a copy of this parameter:

@Select("SELECT * FROM ${table} WHERE firstName=? OR lastName=?")
Mono<Account> findByFirstOrLastNames(String name1, String name2);

The @RepeatParameter annotation fixes this disadvantage.

The following code is an equivalent to the code with a copy of the name parameter:

@Select("SELECT * FROM ${table} WHERE firstName=? OR lastName=?")
Mono<Account> findByFirstOrLastNames(@RepeatParameter(2) String name);

9.7. SQL Operations

9.7.1. @Select

The rxmicro.data.sql.r2dbc.postgresql module supports the SELECT SQL operation.

9.7.1.1. Returning Types Support
9.7.1.1.1. Reactive Types Support

PostgreSQL Data Repositories that generated by the RxMicro frameworks support the following return reactive types:

@PostgreSQLRepository
public interface SelectSingleDataRepository {

    @Select("SELECT * FROM ${table} WHERE email = ?")
    Mono<Account> findByEmail1(String email);

    @Select("SELECT * FROM ${table} WHERE email = ?")
    CompletableFuture<Account> findByEmail2(String email);

    @Select("SELECT * FROM ${table} WHERE email = ?")
    CompletionStage<Account> findByEmail3(String email);

    @Select("SELECT * FROM ${table} WHERE email = ?")
    CompletableFuture<Optional<Account>> findByEmail4(String email);

    @Select("SELECT * FROM ${table} WHERE email = ?")
    CompletionStage<Optional<Account>> findByEmail5(String email);

    @Select("SELECT * FROM ${table} WHERE email = ?")
    Single<Account> findByEmail6(String email);

    @Select("SELECT * FROM ${table} WHERE email = ?")
    Maybe<Account> findByEmail7(String email);
}
@PostgreSQLRepository
public interface SelectManyDataRepository {

    @Select("SELECT * FROM ${table} ORDER BY id")
    Mono<List<Account>> findAll1();

    @Select("SELECT * FROM ${table} ORDER BY id")
    Flux<Account> findAll2();

    @Select("SELECT * FROM ${table} ORDER BY id")
    CompletableFuture<List<Account>> findAll3();

    @Select("SELECT * FROM ${table} ORDER BY id")
    CompletionStage<List<Account>> findAll4();

    @Select("SELECT * FROM ${table} ORDER BY id")
    Single<List<Account>> findAll5();

    @Select("SELECT * FROM ${table} ORDER BY id")
    Flowable<Account> findAll6();
}

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.7.1.1.2. Model Types Support

PostgreSQL Data Repositories that generated by the RxMicro frameworks support the following return model types:

@PostgreSQLRepository
@VariableValues({
        "${table}", "account"
})
public interface SelectSingleDataRepository {

    @Select("SELECT * FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<Account> findSingleAccount();

    @Select("SELECT first_name, last_name FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<EntityFieldMap> findSingleEntityFieldMap();

    @Select("SELECT first_name, last_name FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<EntityFieldList> findSingleEntityFieldList();

    @Select("SELECT email FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<String> findSingleEmail();

    @Select("SELECT role FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<Role> findSingleRole();

    @Select("SELECT balance FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<BigDecimal> findSingleBalance();
}
@PostgreSQLRepository
public interface SelectManyDataRepository {

    @Select("SELECT first_name, last_name FROM ${table} ORDER BY id")
    CompletableFuture<List<Account>> findAllAccounts();

    @Select(
            value = "SELECT first_name, last_name FROM ${table} ORDER BY id",
            entityClass = Account.class
    )
    CompletableFuture<List<EntityFieldMap>> findAllEntityFieldMapList();

    @Select(
            value = "SELECT first_name, last_name FROM ${table} ORDER BY id",
            entityClass = Account.class
    )
    CompletableFuture<List<EntityFieldList>> findAllEntityFieldList();

    @Select(
            value = "SELECT email FROM ${table} ORDER BY id",
            entityClass = Account.class
    )
    CompletableFuture<List<String>> findAllEmails();

    @Select(
            value = "SELECT DISTINCT role FROM ${table} ORDER BY role",
            entityClass = Account.class
    )
    CompletableFuture<List<Role>> findAllRoles();

    @Select(
            value = "SELECT DISTINCT balance FROM ${table} ORDER BY balance",
            entityClass = Account.class
    )
    CompletableFuture<List<BigDecimal>> findAllBalances();
}

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.7.1.1.3. All Supported Return Types

For more information, we recommend that You familiarize yourself with the following examples:

9.7.1.2. WHERE, ORDER BY and Other SELECT Operations

The rxmicro.data.sql.r2dbc.postgresql module supports all SQL nested operators that are supported by the SELECT operation:

  • WHERE operator:

@PostgreSQLRepository
public interface SelectByFilterRepository {

    @Select("SELECT * FROM ${table} WHERE role=?")
    CompletableFuture<List<Account>> findByRole(Role role);

    @Select("SELECT * FROM ${table} WHERE first_name=? OR first_name=? OR first_name=?")
    CompletableFuture<List<Account>> findByFirstName(
            String firstName1, String firstName2, String firstName3
    );

    @Select("SELECT * FROM ${table} WHERE balance BETWEEN ? AND ?")
    CompletableFuture<List<Account>> findByBalance(BigDecimal minBalance, BigDecimal maxBalance);

    @Select("SELECT * FROM ${table} WHERE first_name=? OR last_name=?")
    CompletableFuture<List<Account>> findByFirstOrLastName(String name1, String name2);

    @Select("SELECT * FROM ${table} WHERE first_name ILIKE ? OR last_name ILIKE ?")
    CompletableFuture<List<Account>> findByFirstOrLastName(@RepeatParameter(2) String name);
}
  • IN operator:

@PostgreSQLRepository
public interface SelectByFilterUsingINOperatorRepository {

    @Select("SELECT * FROM ${table} WHERE role IN ('CEO'::role, 'Systems_Architect'::role)")
    CompletableFuture<List<Account>> findByRole();

    @Select("SELECT * FROM ${table} WHERE email NOT IN (SELECT email FROM blocked_accounts)")
    CompletableFuture<List<Account>> findNotBlockedAccount();

    @Select("SELECT * FROM ${table} WHERE email in ?")
    CompletableFuture<Optional<Account>> findByEmail(String email);

    @Select("SELECT * FROM ${table} WHERE email in ?")
    CompletableFuture<List<Account>> findByEmail(List<String> emails);

    @Select("SELECT * FROM ${table} WHERE role in (?)")
    CompletableFuture<List<Account>> findByRole(Role role);

    @Select("SELECT * FROM ${table} WHERE role in (?)")
    CompletableFuture<List<Account>> findByRole(List<Role> roles);

    @Select("SELECT * FROM ${table} WHERE balance in (?)")
    CompletableFuture<List<Account>> findByBalance(BigDecimal balance);

    @Select("SELECT * FROM ${table} WHERE balance in ?")
    CompletableFuture<List<Account>> findByBalance(List<BigDecimal> balances);
}
  • ORDER BY operator:

@PostgreSQLRepository
public interface SelectOrderedDataRepository {

    @Select("SELECT * FROM ${table} ORDER BY id")
    CompletableFuture<List<Account>> findAllOrderedById();

    @Select("SELECT * FROM ${table} ORDER BY ( id ? )")
    CompletableFuture<List<Account>> findAllOrderedById(SortOrder sortOrder);

    @Select("SELECT * FROM ${table} ORDER BY ? ?")
    CompletableFuture<List<Account>> findAllOrderedBy(String columnName, SortOrder sortOrder);

    @Select("SELECT * FROM ${table} ORDER BY (id ?, email ?) LIMIT 10")
    CompletableFuture<List<Account>> findAllOrderedByIdAndEmail(
            @RepeatParameter(2) SortOrder sortOrder
    );
}
  • LIMIT and/or OFFSET operator(s):

@PostgreSQLRepository
public interface SelectLimitedDataRepository {

    @Select("SELECT * FROM ${table} ORDER BY id LIMIT 2")
    CompletableFuture<List<Account>> findFirst2Accounts();

    @Select("SELECT * FROM ${table} ORDER BY id LIMIT ?")
    CompletableFuture<List<Account>> findAccounts(int limit);

    @Select("SELECT * FROM ${table} ORDER BY id LIMIT ? OFFSET ?")
    CompletableFuture<List<Account>> findAccounts(int limit, int offset);

    @Select("SELECT * FROM ${table} ORDER BY id LIMIT ? OFFSET ?")
    CompletableFuture<List<Account>> findAccounts(Pageable pageable);
}
  • Composition of WHERE, ORDER BY, LIMIT and OFFSET operators:

@PostgreSQLRepository
public interface SelectComplexDataRepository {

    @Select("SELECT * FROM ${table} WHERE " +
            "first_name ILIKE ? AND role IN (?) AND balance < ? " +
            "ORDER BY (id ?, email ?) " +
            "LIMIT ? " +
            "OFFSET ?")
    CompletableFuture<List<Account>> find01(
            String firstNameTemplate,
            List<Role> roles,
            BigDecimal balance,
            @RepeatParameter(2) SortOrder sortOrder,
            int limit,
            int offset
    );
}
  • etc.

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.7.1.3. Selected Projections

The rxmicro.data.sql.r2dbc.postgresql module supports projections from selected table(s).

To use projections, developer must specify required columns at SELECT query:

@PostgreSQLRepository
public interface SelectProjectionDataRepository {

    @Select("SELECT * FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<Account> findAllColumns();

    @Select("SELECT id, email, first_name, last_name, balance FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<Account> findAllColumnsExceptRole1();

    @Select("SELECT id, email, last_name, first_name, balance FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<Account> findAllColumnsExceptRole2();

    @Select("SELECT 1 as id, " +
            "'richard.hendricks@piedpiper.com' as email, " +
            "'Hendricks' as last_name, " +
            "'Richard' as first_name, " +
            "70000.00 as balance")
    CompletableFuture<Account> findAllColumnsExceptRole3();

    @Select("SELECT first_name, last_name FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<Account> findFirstAndLastName();

    @Select("SELECT id, " +
            "'***@***' as email, " +
            "upper(last_name) as last_name, " +
            "first_name, " +
            "(20000 + 50000.00) as balance " +
            "FROM ${table} " +
            "WHERE email='richard.hendricks@piedpiper.com'")
    CompletableFuture<Account> findModifiedColumns();
}

For each nonstandard projection, the RxMicro framework generates a separate converter method.

For example for SelectProjectionDataRepository the RxMicro framework generates the following converter:

public final class $$AccountEntityFromDBConverter
        extends EntityFromDBConverter<Row, RowMetadata, Account> {

    (1)
    public Account fromDB(final Row dbRow,
                          final RowMetadata metadata) {
        final Account model = new Account();
        model.id = dbRow.get(0, Long.class);
        model.email = dbRow.get(1, String.class);
        model.firstName = dbRow.get(2, String.class);
        model.lastName = dbRow.get(3, String.class);
        model.balance = dbRow.get(4, BigDecimal.class);
        model.role = toEnum(Role.class, dbRow.get(5, String.class), "role");
        return model;
    }

    public Account fromDBFirst_nameLast_name(final Row dbRow,
                                             final RowMetadata metadata) {
        final Account model = new Account();
        model.firstName = dbRow.get(0, String.class);
        model.lastName = dbRow.get(1, String.class);
        return model;
    }

    public Account fromDBIdEmailFirst_nameLast_nameBalance(final Row dbRow,
                                                           final RowMetadata metadata) {
        final Account model = new Account();
        model.id = dbRow.get(0, Long.class);
        model.email = dbRow.get(1, String.class);
        model.firstName = dbRow.get(2, String.class);
        model.lastName = dbRow.get(3, String.class);
        model.balance = dbRow.get(4, BigDecimal.class);
        return model;
    }

    public Account fromDBIdEmailLast_nameFirst_nameBalance(final Row dbRow,
                                                           final RowMetadata metadata) {
        final Account model = new Account();
        model.id = dbRow.get(0, Long.class);
        model.email = dbRow.get(1, String.class);
        model.lastName = dbRow.get(2, String.class);
        model.firstName = dbRow.get(3, String.class);
        model.balance = dbRow.get(4, BigDecimal.class);
        return model;
    }
}
1 It is standard converter example.
(This converter is a standard one, because an order of the selected columns is defined by the order of fields of Java model class. (Account class for current 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.

9.7.1.4. Custom Select

The rxmicro.data.sql.r2dbc.postgresql module introduces a @CustomSelect annotation that allows working with a Custom SELECT.

The Custom SELECT is a string parameter that sends to a repository method and contains a SQL, built dynamically during an execution of a microservice:

@PostgreSQLRepository
public interface CustomSelectRepository {

    @Select
    CompletableFuture<List<EntityFieldMap>> findAll(
            @CustomSelect String sql (1)
    );

    @Select
    CompletableFuture<Optional<Account>> findAccount(
            @CustomSelect(supportUniversalPlaceholder = false) String sql, (2)
            String firstName
    );

    @Select
    CompletableFuture<Optional<Account>> findFirstAndLastName(
            @CustomSelect(selectedColumns = {"first_name", "last_name"}) String sql, (3)
            String firstName
    );

    @Select
    CompletableFuture<Optional<Account>> findLastAndFirstName(
            @CustomSelect(selectedColumns = {"last_name", "first_name"}) String sql, (4)
            String firstName
    );
}
1 This is example of a repository method that can execute any SELECT query.
2 This repository method selects all columns defined at Account entity.
(Disabling of universal placeholder means that developer must use postgres specific placeholder ($1, $2, etc) instead of universal placeholder (?). Otherwise error will be thrown!)
3 This repository method selects only selected columns (first_name and last_name) from account table.
(This method supports universal placeholder!)
4 This repository method selects only selected columns (last_name and first_name) from account table.
(This method supports universal placeholder!)

Using of the Custom SELECT feature requires that dynamic built SQL contains the predefined selected columns, otherwise the model converter will convert a table row to the instance of Java class incorrectly!

The following test describes how the Custom SELECT feature can be tested:

@Test
void findAll() {
    final List<EntityFieldMap> entityFieldMaps = dataRepository.findAll(
            "SELECT email, first_name, last_name FROM account WHERE id = 1"
    ).join();
    assertEquals(
            List.of(
                    orderedMap(
                            "email", "richard.hendricks@piedpiper.com",
                            "first_name", "Richard",
                            "last_name", "Hendricks"
                    )
            ),
            entityFieldMaps
    );
}

@Test
void findAccount() {
    final Optional<Account> optionalAccount = dataRepository.findAccount(
            "SELECT * FROM account WHERE first_name = $1",
            "Richard"
    ).join();
    assertEquals(
            Optional.of(
                    new Account(
                            1L,
                            "richard.hendricks@piedpiper.com",
                            "Richard",
                            "Hendricks",
                            new BigDecimal("70000.00")
                    )
            ),
            optionalAccount
    );
}

@Test
void findFirstAndLastName() {
    final Optional<Account> optionalAccount = dataRepository.findFirstAndLastName(
            "SELECT first_name, last_name FROM account WHERE first_name = ?",
            "Richard"
    ).join();
    assertEquals(
            Optional.of(new Account("Richard", "Hendricks")),
            optionalAccount
    );
}

@Test
void findLastAndFirstName() {
    final Optional<Account> optionalAccount = dataRepository.findLastAndFirstName(
            "SELECT last_name, first_name FROM account WHERE first_name = ?",
            "Richard"
    ).join();
    assertEquals(
            Optional.of(new Account("Richard", "Hendricks")),
            optionalAccount
    );
}

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.7.2. @Insert

The rxmicro.data.sql.r2dbc.postgresql module supports the INSERT SQL operation:

@PostgreSQLRepository
public interface DataRepository {

    @Insert
    CompletableFuture<Boolean> insert1(Account account);

    @Insert
    CompletableFuture<Account> insert2(Account account);

    @Insert("INSERT INTO ${table} VALUES(nextval('account_seq'),?,?,?,?,?)")
    (1)
    @VariableValues({
            "${table}", "account"
    })
    CompletableFuture<Integer> insert3(
            String email, String firstName, String lastName, BigDecimal balance, Role role
    );

    @Insert("INSERT INTO ${table} VALUES(nextval('account_seq'),?,?,?,?,?) RETURNING *")
    CompletableFuture<Account> insert4(
            String email, String firstName, String lastName, BigDecimal balance, Role role
    );

    @Insert(
            value = "INSERT INTO ${table} VALUES(nextval('account_seq'),?,?,?,?,?)",
            entityClass = Account.class
    )
    CompletableFuture<Integer> insert5(
            String email, String firstName, String lastName, BigDecimal balance, Role role
    );

    @Insert(
            value = "INSERT INTO ${table} VALUES(nextval('account_seq'),?,?,?,?,?) RETURNING *",
            entityClass = Account.class
    )
    CompletableFuture<EntityFieldMap> insert6(
            String email, String firstName, String lastName, BigDecimal balance, Role role
    );

    @Insert("INSERT INTO ${table}(${inserted-columns}) VALUES(${values}) " +
            "RETURNING ${returning-columns}")
    CompletableFuture<AccountResult> insert7(Account account);

    @Insert("INSERT INTO ${table}(${inserted-columns}) VALUES(${values}) " +
            "ON CONFLICT (${id-columns}) DO UPDATE SET ${on-conflict-update-inserted-columns}" +
            "RETURNING ${returning-columns}")
    CompletableFuture<AccountResult> insert8(Account account);

    @Insert("INSERT INTO ${table}(${inserted-columns}) VALUES(${values}) " +
            "ON CONFLICT (${id-columns}) DO UPDATE SET ${on-conflict-update-inserted-columns}")
    CompletableFuture<Void> insert9(Account account);

    @Insert("INSERT INTO ${table}(${inserted-columns}) VALUES(${values}) " +
            "ON CONFLICT (${id-columns}) DO NOTHING")
    CompletableFuture<Void> insert10(Account account);

    @Insert("INSERT INTO ${table} SELECT * FROM dump RETURNING *")
    CompletableFuture<List<Account>> insertMany1();

    @Insert("INSERT INTO account SELECT * FROM dump")
    CompletableFuture<Integer> insertMany2();
}
1 The variable values are used to resolve predefined variables at the SQL query.
(Read more about the algorithm of the variables resolving at Section 9.8, “Variables Support”.)

For more information, we recommend that You familiarize yourself with the following examples:

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.7.3. @Update

The rxmicro.data.sql.r2dbc.postgresql module supports the UPDATE SQL operation:

@PostgreSQLRepository
public interface DataRepository {

    @Update
    CompletableFuture<Boolean> update1(Account account);

    @Update("UPDATE ${table} SET first_name=?, last_name=? WHERE id=?")
    (1)
    @VariableValues({
            "${table}", "account"
    })
    CompletableFuture<Integer> update2(String firstName, String lastName, Long id);

    @Update("UPDATE ${table} SET first_name=?, last_name=? WHERE ${by-id-filter} RETURNING *")
    CompletableFuture<Account> update3(String firstName, String lastName, Long id);

    @Update(
            value = "UPDATE ${table} SET first_name=?, last_name=? " +
                    "WHERE id = ?",
            entityClass = Account.class
    )
    CompletableFuture<Integer> update4(String firstName, String lastName, Long id);

    @Update(
            value = "UPDATE ${table} SET first_name=?, last_name=? " +
                    "WHERE ${by-id-filter} RETURNING *",
            entityClass = Account.class
    )
    CompletableFuture<EntityFieldMap> update5(String firstName, String lastName, Long id);
}
1 The variable values are used to resolve predefined variables at the SQL query.
(Read more about the algorithm of the variables resolving at Section 9.8, “Variables Support”.)

For more information, we recommend that You familiarize yourself with the following examples:

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.7.4. @Delete

The rxmicro.data.sql.r2dbc.postgresql module supports the DELETE SQL operation:

@PostgreSQLRepository
public interface DataRepository {

    @Delete
    CompletableFuture<Boolean> delete1(Account account);

    @Delete("DELETE FROM ${table} WHERE balance < ?")
    (1)
    @VariableValues({
            "${table}", "account"
    })
    CompletableFuture<Integer> delete2(BigDecimal minRequiredBalance);

    @Delete("DELETE FROM ${table} WHERE ${by-id-filter} RETURNING *")
    CompletableFuture<Account> delete3(Long id);

    @Delete(entityClass = Account.class)
    CompletableFuture<Integer> delete4(Long id);

    @Delete(
            value = "DELETE FROM ${table} WHERE ${by-id-filter} RETURNING *",
            entityClass = Account.class
    )
    CompletableFuture<EntityFieldMap> delete5(Long id);
}
1 The variable values are used to resolve predefined variables at the SQL query.
(Read more about the algorithm of the variables resolving at Section 9.8, “Variables Support”.)

For more information, we recommend that You familiarize yourself with the following examples:

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.8. Variables Support

When building SQL queries, sometimes it is necessary to specify the table name as a string constant. This feature provides the developer with more flexibility: the table names may vary depending on the environment.

For better readability of SQL query, the RxMicro framework recommends using predefined variables instead of string concatenation:

(1)
public static final String TABLE_NAME = "table1";

(2)
@Select("SELECT id, value FROM " + TABLE_NAME + " WHERE id = ?")
CompletableFuture<EntityFieldMap> findById1(long id);

(3)
@Select("SELECT id, value FROM ${table} WHERE id = ?")
@VariableValues({
        "${table}", TABLE_NAME
})
CompletableFuture<EntityFieldMap> findById2(long id);
1 String constant with table name.
2 When strings are concatenated, the readability of an entire SQL query gets worse.
3 Instead of string concatenation, the RxMicro framework recommends using predefined variables.

All predefined variables supported by the RxMicro framework are declared in the SupportedVariables class

To determine the value of the predefined variable used in the query specified for the repository method, the RxMicro framework uses the following algorithm:

  1. If the repository method returns or accepts the entity model as a parameter, the entity model class is used to define the variable value.

  2. Otherwise, the RxMicro framework analyzes the optional entityClass parameter defined in the @Select, @Insert, @Update and @Delete annotations.

  3. If the optional entityClass parameter is set, the class specified in this parameter is used to define the variable value.

  4. If the optional entityClass parameter is missing, the RxMicro framework tries to extract the variable value from the @VariableValues annotation, which annotates this repository method.

  5. If the repository method is not annotated with the @VariableValues annotation or the @VariableValues annotation does not contain the value of a predefined variable, then the RxMicro framework tries to extract the value of this variable from the @VariableValues annotation, which annotates the repository interface.

  6. If the variable value is undefined in all specified places, then the RxMicro framework notifies the developer about the error.

@PostgreSQLRepository
@VariableValues({
        "${table}", SelectDataRepository.GLOBAL_TABLE
})
public interface SelectDataRepository {

    public static final String GLOBAL_TABLE = "global_table";

    public static final String ENTITY_TABLE = "entity_table";

    public static final String LOCAL_TABLE = "local_table";

    (1)
    @Select("SELECT * FROM ${table}")
    CompletableFuture<List<Entity>> findFromEntityTable1();

    (2)
    @Select(value = "SELECT * FROM ${table}", entityClass = Entity.class)
    CompletableFuture<List<EntityFieldMap>> findFromEntityTable2();

    (3)
    @Select("SELECT * FROM ${table}")
    CompletableFuture<List<EntityFieldMap>> findFromGlobalTable();

    (4)
    @Select("SELECT * FROM ${table}")
    @VariableValues({
            "${table}", SelectDataRepository.LOCAL_TABLE
    })
    CompletableFuture<List<EntityFieldMap>> findFromLocalTable();
}
1 The ${table} variable value will be equal to entity_table.
(The variable value is read from the Entity class, which is returned by this method.)
2 The ${table} variable value will be equal to entity_table.
(The variable value is read from the Entity class, since this class is specified in the entityClass parameter.)
3 The ${table} variable value will be equal to global_table.
(The variable value is read from the @VariableValues annotation, which annotates the repository interface.)
4 The ${table} variable value will be equal to local_table.
(The variable value is read from the @VariableValues annotation, which annotates the repository method.)

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.9. Primary Keys Support

The rxmicro.data.sql.r2dbc.postgresql module supports four types of the primary keys:

  • Auto generated primary key. (SERIAL type.)
    (A uniqueness of this type of primary key is controlled by the database server!):

@PrimaryKey
Long id;
@PrimaryKey
@SequenceGenerator
Long id;
  • Manually set primary key.
    (A developer must control a uniqueness of this type of primary key!):

@PrimaryKey(autoGenerated = false)
Integer id;
  • Complex primary key:
    (A developer must control a uniqueness of this type of primary key!):

@PrimaryKey(autoGenerated = false)
Long idCategory;

@PrimaryKey(autoGenerated = false)
@Column(length = Column.UNLIMITED_LENGTH)
String idType;

@PrimaryKey(autoGenerated = false)
Role idRole;

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.10. @ExpectedUpdatedRowsCount annotation

Enables validation for updated rows count during DML operation, like Insert, Update and Delete operations. This annotation adds additional runtime validator that validates the actual updated rows during SQL operation.

If current database has invalid state the InvalidDatabaseStateException will be thrown!

The following examples demonstrate the @ExpectedUpdatedRowsCount annotation usage:

@ExpectedUpdatedRowsCount(10)
@Insert("INSERT INTO ${table} SELECT * FROM dump")
Mono<Integer> insert12();

@ExpectedUpdatedRowsCount(1)
@Insert("INSERT INTO ${table} VALUES(nextval('account_seq'),?,?)")
Mono<Boolean> insert13(String firstName, String lastName);

@ExpectedUpdatedRowsCount(1)
@Insert("INSERT INTO ${table} VALUES(nextval('account_seq'),?,?) RETURNING *")
Mono<Account> insert14(String firstName, String lastName);

@ExpectedUpdatedRowsCount(0)
@Insert("INSERT INTO ${table} VALUES(nextval('account_seq'),?,?) RETURNING *")
Mono<Account> insert15(String firstName, String lastName);
@ExpectedUpdatedRowsCount(10)
@Update("UPDATE ${table} SET first_name=?, last_name=? WHERE email=?")
Mono<Integer> update12(String firstName, String lastName, String email);

@ExpectedUpdatedRowsCount(1)
@Update("UPDATE ${table} SET first_name=?, last_name=? WHERE id=?")
Mono<Boolean> update13(String firstName, String lastName, Long id);

@ExpectedUpdatedRowsCount(1)
@Update("UPDATE ${table} SET first_name=?, last_name=? WHERE ${by-id-filter} RETURNING *")
Mono<Account> update14(String firstName, String lastName, Long id);

@ExpectedUpdatedRowsCount(0)
@Update("UPDATE ${table} SET first_name=?, last_name=? WHERE ${by-id-filter} RETURNING *")
Mono<Account> update15(String firstName, String lastName, Long id);
@ExpectedUpdatedRowsCount(1)
@Delete(entityClass = Account.class)
Mono<Void> delete11(Long id);

@ExpectedUpdatedRowsCount(10)
@Delete("DELETE FROM ${table} WHERE first_name ILIKE ? OR last_name ILIKE ?")
Mono<Integer> delete12(Transaction transaction, @RepeatParameter(2) String name);

@ExpectedUpdatedRowsCount(1)
@Delete(entityClass = Account.class)
Mono<Boolean> delete13(Long id);

@ExpectedUpdatedRowsCount(1)
@Delete("DELETE FROM ${table} WHERE ${by-id-filter} RETURNING *")
Mono<Account> delete14(Long id);

@ExpectedUpdatedRowsCount(0)
@Delete("DELETE FROM ${table} WHERE ${by-id-filter} RETURNING *")
Mono<Account> delete15(Long id);

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.11. Transactions Support

9.11.1. DataBase Transactions

To work with database transactions the RxMicro framework introduces a basic transaction model:

public interface Transaction {

    ReactiveType commit();

    ReactiveType rollback();

    ReactiveType create(SavePoint savePoint);

    ReactiveType release(SavePoint savePoint);

    ReactiveType rollback(SavePoint savePoint);

    IsolationLevel getIsolationLevel();

    ReactiveType setIsolationLevel(IsolationLevel isolationLevel);
}

where ReactiveType can be Mono<Void>, Completable or CompletableFuture<Void>.

This basic transaction model has adaptation for all supported reactive libraries:

import io.rxmicro.data.sql.model.reactor.Transaction;

@PostgreSQLRepository
public interface BeginReactorTransactionRepository {

    Mono<Transaction> beginTransaction();

    Mono<Transaction> beginTransaction(IsolationLevel isolationLevel);
}
import io.rxmicro.data.sql.model.rxjava3.Transaction;

@PostgreSQLRepository
public interface BeginRxJava3TransactionRepository {

    Single<Transaction> beginTransaction();

    Single<Transaction> beginTransaction(IsolationLevel isolationLevel);
}
import io.rxmicro.data.sql.model.completablefuture.Transaction;

@SuppressWarnings("unused")
@PostgreSQLRepository
public interface BeginCompletableFutureTransactionRepository {

    CompletionStage<Transaction> beginTransaction1();

    CompletionStage<Transaction> beginTransaction1(IsolationLevel isolationLevel);

    CompletableFuture<Transaction> beginTransaction2();

    CompletableFuture<Transaction> beginTransaction2(IsolationLevel isolationLevel);
}

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.11.2. Concurrent Access Example

The following example demonstrates how developer can use the RxMicro framework to build microservice that requires concurrent access:

@PostgreSQLRepository
public interface ConcurrentRepository {

    Mono<Transaction> beginTransaction();

    @Select("SELECT * FROM ${table} WHERE id=? FOR UPDATE")
    Mono<Account> findAccountById(Transaction transaction, long id);

    @Select("SELECT * FROM ${table} WHERE id=? FOR UPDATE")
    Mono<Product> findProductById(Transaction transaction, int id);

    @Update(value = "UPDATE ${table} SET balance=? WHERE id=?", entityClass = Account.class)
    Mono<Void> updateAccountBalance(Transaction transaction, BigDecimal balance, long id);

    @Update(value = "UPDATE ${table} SET count=? WHERE id=?", entityClass = Product.class)
    Mono<Void> updateProductCount(Transaction transaction, int count, long id);

    @Insert
    Mono<Order> createOrder(Transaction transaction, Order order);
}
public final class ConcurrentBusinessService {

    private final ConcurrentRepository repository = getRepository(ConcurrentRepository.class);

    /**
     * @return order id if purchase is successful or
     *         error signal if:
     *                  - account not found or
     *                  - product not found or
     *                  - products ran out or
     *                  - money ran out
     */
    public Mono<Long> tryToBuy(final long idAccount,
                               final int idProduct,
                               final int count) {
        return repository.beginTransaction()
                .flatMap(transaction -> repository.findAccountById(transaction, idAccount)
                        .flatMap(account -> repository.findProductById(transaction, idProduct)
                                .flatMap(product ->
                                        tryToBuy(transaction, account, product, count))
                                .switchIfEmpty(Mono.error(() ->
                                        // product not found
                                        new ProductNotFoundException(idProduct))))
                        // account not found
                        .switchIfEmpty(Mono.error(() -> new AccountNotFoundException(idAccount)))
                        .onErrorResume(transaction.createRollbackThenReturnErrorFallback())
                );
    }

    private Mono<Long> tryToBuy(final Transaction transaction,
                                final Account account,
                                final Product product,
                                final int count) {
        if (count <= product.getCount()) {
            final BigDecimal cost = product.getPrice().multiply(BigDecimal.valueOf(count));
            if (cost.compareTo(account.getBalance()) <= 0) {
                return buy(transaction, account, product, count, cost);
            } else {
                // money ran out
                return Mono.error(new NotEnoughFundsException(cost, account.getBalance()));
            }
        } else {
            // products ran out
            return Mono.error(new NotEnoughProductCountException(count, product.getCount()));
        }
    }

    // purchase is successful, returns order id
    private Mono<Long> buy(final Transaction transaction,
                           final Account account,
                           final Product product,
                           final int count,
                           final BigDecimal cost) {
        final int newProductCount = product.getCount() - count;
        final BigDecimal newBalance = account.getBalance().subtract(cost);
        final Order order = new Order(account.getId(), product.getId(), count);

        return repository.updateProductCount(transaction, newProductCount, product.getId())
                .then(repository.updateAccountBalance(transaction, newBalance, account.getId())
                        .then(repository.createOrder(transaction, order)
                                .map(Order::getId)
                                .flatMap(id -> transaction.commit()
                                        .thenReturn(id))
                        )
                );
    }
}

For more information, we recommend that You familiarize yourself with the following examples:

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.12. Partial Implementation

If the Postgre SQL data repository 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 methods for the Postgre SQL data repository 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 method(s) for Postgre SQL data repository:

@PostgreSQLRepository
(1)
@PartialImplementation(AbstractDataRepository.class)
public interface DataRepository {

    @Select("SELECT 1 + 1")
    CompletableFuture<Long> generatedMethod();

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

An AbstractDataRepository contains the following content:

public abstract class AbstractDataRepository extends AbstractPostgreSQLRepository
        implements DataRepository {

    protected AbstractDataRepository(final Class<?> repositoryClass) {
        super(repositoryClass);
    }

    @Override
    public CompletableFuture<Long> userDefinedMethod() {
        return CompletableFuture.completedFuture(100L);
    }
}

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 AbstractPostgreSQLRepository one.

  3. The class must implement the PostgreSQL data repository interface.

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

In terms of infrastructure, the repository methods generated and defined by the developer for Postgre SQL data repository do not differ:

@Test
void generatedMethod() {
    assertEquals(2L, dataRepository.generatedMethod().join());
}

@Test
void userDefinedMethod() {
    assertEquals(100L, dataRepository.userDefinedMethod().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.13. Logging

PostgreSQL Data Repositories use the R2DBC PostgreSQL Driver, so in order to activate database request logging, You must configure the R2DBC PostgreSQL Driver Logger:

For example, if to the classpath of the current project add the jul.properties resource:

io.r2dbc.postgresql.QUERY.level=TRACE

,then PostgreSQL Data Repositories will generate request logs to the database while working:

[DEBUG] io.r2dbc.postgresql.QUERY : Executing query: SHOW TRANSACTION ISOLATION LEVEL
[DEBUG] io.r2dbc.postgresql.QUERY : Executing query: SELECT 2+2
[DEBUG] io.r2dbc.postgresql.QUERY : Executing query: SELECT first_name, last_name FROM account WHERE email = $1

10. Mongo Data Repositories

The RxMicro framework supports creation of dynamic repositories for interaction with databases.

To interact with Mongo DB, the RxMicro framework provides the rxmicro.data.mongo module.

10.1. Basic Usage

To use the rxmicro.data.mongo module in the project, the following two steps must be taken:

  • Inject the rxmicro.data.mongo dependency to the pom.xml file:

<dependency>
    <groupId>io.rxmicro</groupId>
    <artifactId>rxmicro-data-mongo</artifactId>
    <version>${rxmicro.version}</version>
</dependency>
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId> (1)
    <version>${projectreactor.version}</version>
</dependency>
1 Besides the rxmicro.data.mongo dependency, it is also recommended to add the reactive programming library.

Instead of adding a third-party reactive programming library, You can also use the java.util.concurrent library built into the JDK, but often in practice the java.util.concurrent library’s features are not enough.

Therefore, it is recommended to use the Project Reactor or the RxJava library!

  • Add the rxmicro.data.mongo module to the module-info.java descriptor:

module examples.data.mongo.basic {
    requires rxmicro.data.mongo;
    requires reactor.core;       (1)
}
1 When using a third-party reactive programming library, do not forget to add the corresponding module.

Once the rxmicro.data.mongo module is added, You can create a data model class and a dynamic repository:

public final class Account {

    String firstName;

    String lastName;

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}
(1)
@MongoRepository(collection = "account")
public interface DataRepository {

    (2)
    @Find(query = "{email: ?}")
    Mono<Account> findByEmail(String email);
}
1 In order for a standard interface to be recognized by the RxMicro framework as a dynamic repository for interaction with Mongo DB, this interface should be annotated with the @MongoRepository annotation.
2 The dynamic repository may contain methods that form a query to the Mongo DB. (The query that used for a request for data uses the JSON format (Specialized request format for Mongo DB) and is specified in the annotation parameters. For each operation supported by Mongo DB, the RxMicro framework defines a separate annotation: The @Find annotation describes the db.collection.find()operation.)

Since the dynamic repository is a RxMicro component, for its testing You need to use the microservice component testing approach:

The common approach recommended for testing dynamic repositories, that interact with Mongo DB, is described in the Section 10.3.2, “Test Templates”.

@Testcontainers
@RxMicroComponentTest(DataRepository.class)
final class DataRepositoryTest {

    @Container
    private final GenericContainer<?> mongoTestDb =
            new GenericContainer<>("rxmicro/mongo-test-db")
                    .withExposedPorts(27017);

    @WithConfig
    private final MongoConfig mongoConfig = new MongoConfig()
            .setDatabase("rxmicro");

    private DataRepository dataRepository;

    @BeforeEach
    void beforeEach() {
        mongoConfig
                .setHost(mongoTestDb.getContainerIpAddress())
                .setPort(mongoTestDb.getFirstMappedPort());
    }

    @Test
    void Should_find_account() {
        final Account account =
                dataRepository.findByEmail("richard.hendricks@piedpiper.com")
                        .blockOptional()
                        .orElseThrow();

        assertEquals("Richard", account.getFirstName());
        assertEquals("Hendricks", account.getLastName());
    }
}

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.2. RxMicro Annotations

The RxMicro framework supports the following RxMicro Annotations:

Table 16. Supported RxMicro Annotations.
Annotation Description

@Column

Sets mapping between the field name in the Mongo DB document and the Java model class field name.

(By default, the RxMicro framework uses the Java model class field name as the field name in the Mongo DB document. If the name should differ for some reason, (for example, as a field name in the Mongo DB document the keyword Java is used), it should be specified using this annotation!)

@ColumnMappingStrategy

Sets the strategy of field name formation in the Mongo DB document, based on the analysis of the Java model class field names.

(If this annotation annotates the Java model class, then the set strategy will be used for all fields in this class. For example, if You set the default LOWERCASE_WITH_UNDERSCORED strategy, then the parentId field in the Java class will correspond to the parent_id field in the Mongo DB document.)

@DataRepositoryGeneratorConfig

Allows You to configure the repository generation process.

@RepeatParameter

Allows setting mapping between one method parameter marked with this annotation and several universal placeholders that are used in the request to Mongo DB.

@Find

Denotes a repository method that must execute a db.collection.find() operation.

@Aggregate

Denotes a repository method that must execute a db.collection.aggregate() operation.

@Distinct

Denotes a repository method that must execute a db.collection.distinct() operation.

@CountDocuments

Denotes a repository method that must execute a db.collection.countDocuments() operation.

@EstimatedDocumentCount

Denotes a repository method that must execute a db.collection.estimatedDocumentCount() operation.

@Insert

Denotes a repository method that must execute a db.collection.insertOne() operation.

@Update

Denotes a repository method that must execute a db.collection.updateOne() operation.

@Delete

Denotes a repository method that must execute a db.collection.deleteOne() operation.

@DocumentId

Denotes a model field that must be used as document unique identifier.

@MongoRepository

Denotes that an interface is a dynamic generated Mongo data repository.

@PartialImplementation

Denotes an abstract class that contains a partial implementation of the annotated by this annotation a Mongo Data Repository interface.

10.3. Repositories Testing

For successful functional testing of dynamic repositories, that interact with Mongo DB, it is required:

  • Presence of a script that creates a test database.

  • Mechanism for preparing a database for testing: creating a database before starting the test and deleting a database after completing the test.

10.3.1. Test Database

A test database was created for testing the rxmicro.data.mongo, module features, which are described in this section.

The test database consists of one account collection, which contains 6 documents describing the accounts of the test users:

mongo test db
Figure 16. Mongo Test Database

Scripts for creating a test database are available at the following link:

The following classes of Java models correspond to the documents created in the test database:

public class Account {
    @DocumentId
    Long id;

    String email;

    String firstName;

    String lastName;

    BigDecimal balance;

    Role role;
}
public enum Role {

    CEO,

    Systems_Architect,

    Lead_Engineer,

    Engineer
}

For ease of studying the rxmicro.data.mongo module, You can use the ready-made Mongo DB image with the rxmicro/mongo-test-db test database.

The source code of the project used as a base for building this docker image, is available at the following link:

10.3.2. Test Templates

As a mechanism for preparing a database for testing (creating a database before starting the test and deleting a database after completing the test), it is most convenient to use docker.

To start docker containers in the functional test it is convenient to use the Testcontainers Java library:

(1)
@Testcontainers
(2)
@RxMicroComponentTest(DataRepository.class)
final class DataRepositoryTestTemplate1 {

    (3)
    @Container
    private static final GenericContainer<?> MONGO_TEST_DB =
            new GenericContainer<>("rxmicro/mongo-test-db")
                    .withExposedPorts(27017); (4)

    (5)
    @WithConfig
    private static final MongoConfig MONGO_CONFIG = new MongoConfig()
            .setDatabase("rxmicro"); (6)

    @BeforeAll
    static void beforeAll() {
        MONGO_TEST_DB.start(); (7)
        MONGO_CONFIG
                .setHost(MONGO_TEST_DB.getContainerIpAddress()) (8)
                .setPort(MONGO_TEST_DB.getFirstMappedPort());
    }

    private DataRepository dataRepository; (9)

    // ... test methods must be here

    @AfterAll
    static void afterAll() {
        MONGO_TEST_DB.stop(); (10)
    }
}
1 The @Testcontainers annotation activates the start and stop of the docker containers to be used in this test.
2 Since the dynamic repository is a RxMicro component, for its testing You need to use the microservice component testing approach.
3 The @Container annotation indicates the docker container that will be used in this test. As an image on the basis of which it is necessary to create the docker container, the Mongo DB ready-made image with the rxmicro/mongo-test-db test database is used.
4 When starting the docker container, You need to open the standard port for Mongo DB.
5 Using the @WithConfig annotation, the configuration available only during the test is declared.
6 Setting up the configuration to interact with the test database.
7 Before running all tests, You must start the docker container.
8 After starting the docker container, You need to read the random IP address and port that will be used when connecting to the running docker container.
9 When testing microservice components, it is necessary to specify a reference to the component in which the RxMicro framework will inject the tested component.
10 After completing all the tests, You must stop the docker container.

The main advantage of this template is the speed of testing. Since the docker container is created once before starting all test methods, the total runtime of all test methods is reduced. The main disadvantage of this template is that if any test method changes the Mongo DB state, the following test method may end with an error.

Therefore, this functional test template should be used for queries to Mongo DB that do not change the database state!

If You need to test methods that change the Mongo DB state, You should use another template:

(1)
@Testcontainers
(2)
@RxMicroComponentTest(DataRepository.class)
final class DataRepositoryTestTemplate2 {

    (3)
    @Container
    private final GenericContainer<?> mongoTestDb =
            new GenericContainer<>("rxmicro/mongo-test-db")
                    .withExposedPorts(27017); (4)

    (5)
    @WithConfig
    private final MongoConfig mongoConfig = new MongoConfig()
            .setDatabase("rxmicro"); (6)

    private DataRepository dataRepository; (7)

    @BeforeEach
    void beforeEach() {
        mongoConfig
                .setHost(mongoTestDb.getContainerIpAddress()) (8)
                .setPort(mongoTestDb.getFirstMappedPort());
    }

    // ... test methods must be here
}
1 The @Testcontainers annotation activates the start and stop of the docker containers to be used in this test.
2 Since the dynamic repository is a RxMicro component, for its testing You need to use the microservice component testing approach
3 The @Container annotation indicates the docker container that will be used in this test. As an image on the basis of which it is necessary to create the docker container, the Mongo DB ready-made image with the rxmicro/mongo-test-db test database is used.
4 When starting the docker container, You need to open the standard port for Mongo DB.
5 Using the @WithConfig annotation, the configuration available only during the test is declared.
6 Setting up the configuration to interact with the test database.
7 When testing microservice components, it is necessary to specify a reference to the component in which the RxMicro framework will inject the tested component.
8 After starting the docker container, You need to read the random IP address and port that will be used when connecting to the running docker container.

This template for each test method will create and drop the docker container, which may increase the total runtime of all test methods.

Therefore, select the most appropriate functional test template based on the requirements of the tested functionality!

The Testcontainers library starts the docker container before running the test and stops the docker container automatically after completing the test!

So You should start and stop the docker container manually only if You want to use one docker container for all test methods!

10.4. Universal Placeholder

The RxMicro framework recommends using the universal placeholder (?) as parameter value placeholder in the Mongo DB queries:

@Find(query = "{email: ?}")
Mono<Account> findByEmail(String email);

If this method invoked with the following parameter:
repository.findByEmail("welcome@rxmicro.io");,

the RxMicro framework will generate the following Mongo DB query:
{email: "welcome@rxmicro.io"}.

10.5. @RepeatParameter Annotation

The universal placeholder (?) is the simplest type of placeholders.

But unfortunately, it has one disadvantage: if a query parameter must be repeated, a developer must define a copy of this parameter:

@Find(query = "{$or: [{firstName: ?}, {lastName: ?}]}")
Mono<Account> findByFirstOrLastNames(String name1, String name2);

The @RepeatParameter annotation fixes this disadvantage.

The following code is an equivalent to the code with a copy of the name parameter:

@Find(query = "{$or: [{firstName: ?}, {lastName: ?}]}")
Mono<Account> findByFirstOrLastNames(@RepeatParameter(2) String name);

10.6. Mongo Operations

10.6.1. @Find

The rxmicro.data.mongo module supports the db.collection.find() operation:

@Find(query = "{_id: ?}")
Mono<Account> findById(long id);

@Find(query = "{_id: ?}", projection = "{firstName: 1, lastName: 1}")
Mono<Account> findWithProjectionById(long id);

@Find(query = "{role: ?}", sort = "{role: 1, balance: 1}")
Flux<Account> findByRole(Role role, Pageable pageable);

@Find(query = "{role: ?}", sort = "{role: ?, balance: ?}")
Flux<Account> findByRole(Role role, @RepeatParameter(2) SortOrder sortOrder, Pageable pageable);

For more information, we recommend that You familiarize yourself with the following examples:

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.