Skip to content
This repository has been archived by the owner on Oct 7, 2024. It is now read-only.
/ reactive-wizard Public archive

Reactive non-blocking web applications made really easy with JAX-RS and Project Reactor.

License

Notifications You must be signed in to change notification settings

FortnoxAB/reactive-wizard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Reactive Wizard

The Reactive Wizard project makes it easy to build performant and scalable web applications that harness the power of Project Reactor and Netty.

Build Status

TL; DR;

@Path("/helloworld")
public class HelloWorldResource {

    @Inject
    public HelloWorldResource() {
    }

    @GET
    public Mono<String> greeting() {
        return just("Hello world!");
    }
}

Purpose

Using a standard web technology (like Spring/Dropwizard/Servlets) with blocking code means that your system is using thread-per-request, which means that it will not scale well. The problem is not so much with a high request-per-second but rather about when some external resource (database/external service) is slow. When that happens, you will quickly get a thread starvation, and then it's game over. With blocking code, restarting the server would just give you seconds of uptime if the external resource is still slow or offline.

It is safe to say that blocking I/O is not optimal in terms of taking advantage of available processing power. Blocking I/O (often referred to as synchronous I/O) means that a process (or thread) must finish before it can be used again. Processes that use blocking I/O spend lots of time just waiting for input and output operations to complete. On the other hand, non-blocking I/O (or asynchronous I/O) permits processing to continue before I/O operations complete, which translates to less idle system resources and more throughput. Non-blocking I/O is supported out of the box in Reactive Wizard via Netty.

A natural fit for this type of I/O is Project Reactor, which lets you compose non-blocking and event-based applications using the Flux/Mono patterns.

We think that building non-blocking web applications with the above technologies should be easy. That is why Reactive Wizard supports JAX-RS annotations on class methods returning Flux/Mono. Scroll down a bit more for an example!

Hello world example

This small example explains how to get going with a simple hello world resource. The example demonstrate how JAX-RS annotations can be used to fire up a Reactor Netty powered REST API.

1. Add Reactive Wizard as a dependency

Create a new Maven project and add Reactive Wizard to the dependencies section of your pom.xml file. Set the version element to match the latest released version to make use of the most up-to-date stable version.

    <properties>
        <log4j.version>2.19.0</log4j.version>
        <reactivewizard.version>14.21.0</reactivewizard.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>se.fortnox.reactivewizard</groupId>
            <artifactId>reactivewizard-jaxrs</artifactId>
            <version>${reactivewizard.version}</version>
        </dependency>
        <dependency>
            <groupId>se.fortnox.reactivewizard</groupId>
            <artifactId>reactivewizard-bootstrap</artifactId>
            <version>${reactivewizard.version}</version>
        </dependency>
        <dependency>
            <groupId>se.fortnox.reactivewizard</groupId>
            <artifactId>reactivewizard-server</artifactId>
            <version>${reactivewizard.version}</version>
        </dependency>
        <dependency>
            <groupId>se.fortnox.reactivewizard</groupId>
            <artifactId>reactivewizard-dbmigrate</artifactId>
            <version>${reactivewizard.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>${log4j.version}</version>
        </dependency>
    </dependencies>

2. Add resource class

Create a new class in your project and name it HelloWorldResource in the package foo.bar. Alter the contents of the file to match the following below:

package foo.bar;

import reactor.core.publisher.Mono;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import static reactor.core.publisher.Mono.just;

@Path("/helloworld")
public class HelloWorldResource {

    @Inject
    public HelloWorldResource() {
    }

    @GET
    public Mono<String> greeting() {
        return just("Hello world!");
    }
}

3. Create fatjar

All needed code to run the application is now in place, but we can make things even easier to deploy by packaging everything into a fatjar. This means that all jar files - including dependencies - are placed in a single jar file that we can execute. Somewhere under the project element in pom.xml, paste the following into that file. This instructs Maven to build a fatjar for you.

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <finalName>hello-world-fatjar</finalName>
                    <appendAssemblyId>false</appendAssemblyId>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>se.fortnox.reactivewizard.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

4. Build and run the project

All is set to build and run! Fire up a shell, navigate to the root of your project and build it:

mvn clean install

The fatjar can then be run by invoking the following command:

java -jar target/hello-world-fatjar.jar

To test your service, invoke the following command from your shell (using cURL). Another test would be to navigate to the URL via your web browser.

 curl http://localhost:8080/helloworld
"Hello world!"

This concludes the hello world example.

Defining your API separate from your implementation

You don't have to do this, but it's quite nice if you're building microservices and want to easily make calls between services.

Create one maven module "api" and one "impl", and separate your JAX-RS annotations from your logic:

/api/src/main/foo/bar/HelloWorldResource.java:

@Path("/helloworld")
public interface HelloWorldResource {
    @GET
    Mono<String> greeting(@QueryParam("name") String name);
}

/impl/src/main/foo/bar/HelloWorldResourceImpl.java:

public class HelloWorldResourceImpl implements HelloWorldResource {

    @Inject
    public HelloWorldResource() {
    }

    @Override
    public Mono<String> greeting(String name) {
        return just(format("Hello %s!", name));
    }
}

Now you have one java definition of your rest API, in a separate module from your implementation of it. This allows us to distribute the api module as a maven dependency to anyone who wants to call our service. More about that in the next section.

You can use most of the JAX-RS annotations to describe your api.

Calling external services

Let's say you want to call HelloWorldResource from AnotherResource, in another service. Just add a maven dependency to the api module of the hello world service, and you have access to it's REST API through the class defined above. Just inject that interface and use it!

/impl/src/main/another/service/AnotherResourceImpl.java:

public class AnotherResourceImpl implements AnotherResource {
    private final HelloWorldResource helloWorldResource;

    @Inject
    public AnotherResourceImpl(HelloWorldResource helloWorldResource) {
        this.helloWorldResource = helloWorldResource;
    }

    @Override
    public Mono<String> doStuff() {
        return helloWorldResource.greeting("AnotherService");
    }
}

Need some config as well: config.yml:

httpClient: https://urltoserverroutingtoallservices

Pass the config file name as the last argument to the system when starting it.

Wtf?! How is that possible? We have not defined that HelloWorldResource should have any implementation? Read on.

Binding. Magic.

We believe that less code means less errors. So the usually needed code for binding interfaces to its implementations has been removed. When the system starts, it will automatically bind all resource implementations to the webserver. All JAX-RS interfaces that lack an implementation are assumed to be remote, and are bound to our http client. That means:

  1. You are not aware of if an injected interface has a local or remote implementation (and thus not whether it will be called directly or via http).
  2. You can choose late in your development process to distribute the system over multiple machines (containers) or co-locate multiple services in one binary, by just creating a fat-jar of multiple services, resulting in calls becoming local instead of remote.
  3. You can mock stuff really easily in your tests, because everything that is injected is an interface.

Also, we bind all classes implementing an interface, and having an @Inject annotated constructor, to the interfaces it implements.

If you want to do some magic yourself you can just add a class implementing AutoBindModule.

But we have more magic up our sleeves...

Database access

public interface UnicornDAO {
    @Query("SELECT name, age FROM unicorn")
    Flux<Unicorn> selectAllUnicorns();

    @Update("INSERT INTO unicorn (name, age) VALUES (:unicorn.name, :unicorn.age)")
    Mono<Integer> insertUnicorn(Unicorn unicorn);
}

And then just inject that interface:

public class ResourceUsingDatabaseImpl implements ResourceUsingDatabase {
    private final UnicornDAO unicornDAO;

    @Inject
    public ResourceUsingDatabaseImpl(UnicornDAO unicornDAO) {
        this.unicornDAO = unicornDAO;
    }

    @Override
    public Flux<Unicorn> getAllUnicorns() {
        return unicornDAO.selectAllUnicorns();
    }
}
database:
  user: myusername
  password: supersecret
  schema: optional_schema
  url: jdbc:postgresql://host/database

...and you now have non-blocking access to the database.

Database migrations

We use liquibase for creating and migrating the database. Just put your migrations.xml in src/main/resources and we will find it. You also need to configure how liquibase should connect to your database:

liquibase-database:
  user: myusername
  password: supersecret
  schema: optional_schema
  url: jdbc:postgresql://host/database

You run the migrations like this:

java -jar myapplication.jar db-migrate config.yml

Or, if you want to run the system after migration instead of quitting:

java -jar myapplication.jar db-migrate-run config.yml

If you have migrations in multiple modules, use the XmlAppendingTransformer with the shade plugin when building your fatjar.

Database transactions

You create and run a transaction like this:

public class ResourceUsingDatabaseImpl implements ResourceUsingDatabase {
    private final UnicornDAO unicornDAO;
    private final DaoTransactionsFlux daoTransactions;

    @Inject
    public ResourceUsingDatabaseImpl(UnicornDAO unicornDAO, DaoTransactionsFlux daoTransactions) {
        this.unicornDAO = unicornDAO;
        this.daoTransactions = daoTransactions;
    }

    @Override
    public Mono<Void> createSomeUnicorns() {
        List<Mono<Void>> transaction = new ArrayList<>();
        transaction.add(unicornDAO.insertUnicorn(new Unicorn(){{
            setName("Rainbow");
            setAge(7);
        }}));
        transaction.add(unicornDAO.insertUnicorn(new Unicorn(){{
            setName("Sky");
            setAge(9);
        }}));
        return daoTransactions.executeTransaction(transaction);
    }
}

Since we don't use thread-per-request, we can not do what you normally do, which is to allocate a database transaction to a thread. We could get you a connection to hold on to, but then you would block that connection from being used by others, and your database would become the next bottleneck. Therefore, in our transactions, you can not have selects, you cannot get any data out, you can only have multiple insert/update/delete added to the transaction, and they will be run as one single atomic batch. This allows us to only use the connection when it is actually needed, and then give it back to the pool for another request to use.

This imposes a problem, as you cannot do the following in a transaction:

INSERT INTO parent (name) VALUES ('parent name');
parentId = <get the id of the inserted parent>
INSERT INTO child (name, parentId) VALUES ('parent name', parentId);

We have two alternatives for solving this:

  1. Generate all id's in Java code (UUID perhaps?)
  2. Determine the next id with a select, use that id in an "optimistic" transaction. If someone else took it, try again.

Config

You can create your own config section in your config file:

myCustomConfig:
  number: 666
@Config("myCustomConfig")
public class MyCustomConfig {
    private Integer number;

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }
}

Just inject that class wherever it is needed, and it will be populated from your configuration file.

Logging

We use slf4j, which means that you can choose logging framework, e.g. log4j:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${log4j.version}</version>
</dependency>

Don'ts

  • NEVER ever call .block*() in any code that is not a test. Since you have as many threads as you have cores, you will get thread starvation in no time. If you feel the urge to call .block*() you need to go and sharpen your Project Reactor skills instead.
  • NEVER use external libraries that blocks the code (by reading from disk or network or doing other blocking operations). If you need to use such code it must be running in a separate thread pool.

Licence

MIT