Creating High-Performance Web Applications with ActiveJ
In this article, we will create several web applications using a single full-stack ActiveJ framework.
1. Why using ActiveJ?
Most of the Java frameworks have common drawbacks:
- Excessive layers of abstractions that hide the legacy staff
- To much overhead, and thus lack of performance and flexibility
- Framework specifications are elevated over business logic
ActiveJ’s main concept is to overcome these drawbacks. For this purpose the framework was built from the ground up without third-party dependencies. This made the technology flexible, lightweight, and extremely concise. Now it’s your turn to test it out!
ActiveJ has a lot of components: from async core to high-level abstractions for developing distributed storage. But in this tutorial we will only consider the core components of the framework, particularly HTTP and ActiveInject.
2. What is ActiveJ HTTP and ActiveInject?
ActiveJ HTTP component provides high-performance HTTP clients, servers and servlets. This component is fully asynchronous and has minimum overhead. Moreover, it is flexible and suitable for developing both microservices architecture and complex web applications. In some microservices scenarios ActiveJ is 30% faster than highly-specialized multi-threaded Vert.x even on a single core and with 50% less overall CPU load. You can find benchmark sources here.
Yet, if your microservices architecture needs even more performance to process dozens of millions of requests per second, you should check out the ActiveJ RPC component.
ActiveInject is a lightweight and powerful DI library. It has a wide range of features: support of nested scopes, singletons and transient bindings, modules, optimized multi-threaded and single-threaded Injectors, etc. At the same time, ActiveInject is 6 times faster than Guice and hundreds of times faster than Spring DI.
3. Creating single/multi-threaded web applications
Let’s start with a simple “Hello World” HTTP server example. First, you’ll need to import a Maven ActiveJ component to your project:
<dependencies> <dependency> <groupId>io.activej</groupId> <artifactId>activej-launchers-http</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> </dependencies>
logback-classic
dependency is optional. activej-launchers-http
includes pre-defined launchers for HTTP servers. They take care of application lifecycle, dependencies and logging. The activej-launchers-http
component contains an HttpServerLauncher
class which we’ll use as a superclass of our sample server. This dependency will automatically import HTTP component and ActiveInject to your project as well.
Now the whole code of your server will look as follows:
public final class HttpHelloWorldExample extends HttpServerLauncher { @Provides AsyncServlet servlet() { return request -> HttpResponse.ok200().withPlainText("Hello World"); } public static void main(String[] args) throws Exception { Launcher launcher = new HttpHelloWorldExample(); launcher.launch(args); } }
That’s it, no additional configurations are required. Extending the HttpServerLauncher
allows us to use ActiveInject to specify the root HTTP listener AsyncServlet
via @Provides
annotation. The launch
method of the superclass takes care of managing the app’s lifecycle. That’s how simply you create a fully-functioning HTTP server that is ready for highload scenarios out of the box.
This and all the subsequent examples can be tested by running the main method and going to http://localhost:8080.
Another predefined HTTP launcher is named MultithreadedHttpServerLauncher
. It allows us to simply create multi-threaded servers. Similarly to the previous example, we only need to extend the launcher and provide a servlet:
public final class MultithreadedHttpServerExample extends MultithreadedHttpServerLauncher { @Provide @Worker AsyncServlet servlet(@WorkerId int workerId) { return request -> HttpResponse.ok200() .withPlainText("Hello from worker server #" + workerId + "\n"); } public static void main(String[] args) throws Exception { MultithreadedHttpServerExample example = new MultithreadedHttpServerExample(); example.launch(args); } }
Now, your server will automatically distribute requests between threads. Note the @Worker
annotation. It defines the components you wish to put into a separate worker thread. By default, MultithreadedHttpServerLauncher
creates 4 workers. Yet, you can use this launcher as a reference and extend or modify it according to your business logic needs.
4. More specific use cases
In both examples we used a basic AsyncServlet
class for setting up servlets. But ActiveJ HTTP has other more specific solutions, for example:
RoutingServlet
for handy and efficient routingStaticServlet
to load static content from the provided directoryBlockingServlet
a blocking version of theAsyncServlet
AsyncServletDecorator
allows transformations ofAsyncServlet
WebSocketServlet
a servlet that can handle websocket requests
Let’s test out some of them.
RoutingServlet
allows to easily create trees of servlets to route requests between them by the HTTP paths:
RoutingServletExample extends HttpServerLauncher {
@Provides
AsyncServlet servlet() {
return RoutingServlet.create()
.map(GET, "/", request ->
HttpResponse.ok200()
.withHtml("<h1>Go to some pages</h1>" +
"<a href=\"/path1\"> Path 1 </a><br>" +
"<a href=\"/path2\"> Path 2 </a><br>"))
.map(GET, "/path1", request ->
HttpResponse.ok200()
.withHtml("<h1>Hello from Path 1!</h1>" +
"<a href=\"/\">Go home</a>"))
.map("/*", request ->
HttpResponse.ofCode(404)
.withHtml("<h1>404</h1><p>Path '" + request.getRelativePath() + "' not found</p>" +
"<a href=\"/\">Go home</a>"));
}
public static void main(String[] args) throws Exception {
Launcher launcher = new RoutingServletExample();
launcher.launch(args);
}
}
We use method map
to add a route to the RoutingServlet
and set up the following parameters:
method
(optional) – one of the HTTP methods (GET
,POST
, etc)path
– the path on the serverservlet
– defines the logic of request processing.
Another AsyncServlet
implementation is StaticServlet
. It allows us to use static content from a predefined source, for example, local filestorage:
public final class StaticServletExample extends HttpServerLauncher { @Provides Executor executor() { return newSingleThreadExecutor(); } @Provides AsyncServlet servlet(Executor executor) { return StaticServlet.ofClassPath(executor, "static/site") .withIndexHtml(); } public static void main(String[] args) throws Exception { Launcher launcher = new StaticServletExample(); launcher.launch(args); } }
You can place any HTML code in your index.html file in the static/site directory and it will be used as a homepage when you launch the example.
ActiveJ HTTP component has support for WebSocket connection. Just like in the previous examples, the implementation is pretty straightforward, you need to extend HttpServerLauncher
and provide an AsyncServlet
:
@Provides AsyncServlet servlet() { return RoutingServlet.create() .mapWebSocket("/", webSocket -> webSocket.readMessage() .then(message -> webSocket.writeMessage(Message.text("Received " + message.getText()))) .whenComplete(webSocket::close)); }
mapWebSocket
method maps a given Consumer
of a WebSocket
as a WebSocketServlet
on path '/'
. You can find an example of ActiveJ WebSocket client implementation here.
5. Summing up
We’ve covered only a tiny scope of ActiveJ features yet they show the streamlined programming approach of this framework. ActiveJ has much more to offer: lightning-fast bytecode generation, serialization, optimizational tools, RPC implementation, and others.
Basically, you can create almost any application using a single high-performance and holistic framework.
You win some, you lose some.