HTTP

Concise and more efficient alternative to Spring, Jetty, Vert.x, and other similar solutions built on their top. ActiveJ HTTP significantly outperforms them, which is proven by benchmarks. This component can be used separately from ActiveJ platform.

Features

Provides tools for building HTTP servers and clients with asynchronous I/O in a simple and convenient way:

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);
	}
}

Legacy-free approach, designed to be asynchronous

  • no legacy layers of adapters
  • uses low-overhead, but high-level abstractions: AsyncServlet, Promises and CSP channels
  • can be used as an application web server: supports externally provided ActiveJ Inject (DI) Modules with business logic and AsyncServlets

Simple AsyncServlet interface

RoutingServlet for building servlet routing

  • flexible mapping of HTTP paths and methods to AsyncServlets (including other RoutingServlets)
  • support of path parameters (like /users/:id) and relative paths
@Provides
AsyncServlet servlet() {
	return RoutingServlet.create()
			//[START REGION_2]
			.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>" +
									"<a href=\"/user/0\"> Data for user with ID 0 </a><br>" +
									"<br>" +
									"<a href=\"/path3\"> Non existent </a>"))
			//[END REGION_2]
			.map(GET, "/path1", request ->
					HttpResponse.ok200()
							.withHtml("<h1>Hello from the first path!</h1>" +
									"<a href=\"/\">Go home</a>"))
			.map(GET, "/path2", request ->
					HttpResponse.ok200()
							.withHtml("<h1>Hello from the second path!</h1>" +
									"<a href=\"/\">Go home</a>"))

			//[START REGION_3]
			.map(GET, "/user/:user_id", request -> {
				String userId = request.getPathParameter("user_id");
				return HttpResponse.ok200()
						.withHtml("<h1>You have requested data for user with ID: " + userId + "</h1>" +
								"<h3>Try changing URL after <i>'.../user/'</i> to get data for users with different IDs</h3>");
			})
			//[END REGION_3]

			//[START REGION_4]
			.map("/*", request ->
					HttpResponse.ofCode(404)
							.withHtml("<h1>404</h1><p>Path '" + request.getRelativePath() + "' not found</p>" +
									"<a href=\"/\">Go home</a>"));
			//[END REGION_4]
}

AsyncServletDecorators for pre- and post- processing of HttpRequest and HttpResponse:

  • HttpRequest and HttpResponse listeners
  • mappers of HttpExceptions to HttpResponse (to render application errors across entire servlet tree in a consistent manner)
  • HttpRequest body preload
  • functional composition of AsyncServletDecorators
  • can be compared to Node.js ‘middleware’ pre- and post- filters, but with heavy emphasis on functional Java 8+ programming style

HttpDecoder mini-framework:

  • brief DSL for building user-defined reusable decoders of HttpRequests into structured app-specific POJO classes
  • built-in support of user-provided validators and error messages
  • error messages can be fully localized, while being template engines-friendly

Highly optimized and GC-friendly:

  • automatic recycling of ByteBufs associated with HttpRequest/HttpResponse and also ByteBufs received from Net async sockets
  • optimized headers multimap and internal URL representation with low yield of garbage objects
  • specialized headers subclasses render their content directly into ByteBuf, without intermediate garbage objects

Benchmarks

We’ve measured HTTP GET request performance using JMH as the benchmark tool:

Time: 29112ms; Average time: 5822.4ms; Best time: 5757ms; Worst time: 5892ms; Requests per second: 171750

ActiveJ result is 171.8K RPS on a single CPU. Not only is it faster than highly-specialized Vert.x (162.9K RPS), but also with 50% less CPU load.

When using HTTP pipelining, performance reaches 1.5M RPS.

ActiveJ has also placed first in the-benchmarker’s web frameworks benchmark as per 2021-01-08.

You can find HTTP benchmark sources on GitHub

Examples

Note: To run the examples, you need to clone ActiveJ from GitHub:
$ git clone https://github.com/activej/activej
And import it as a Maven project. Check out tag v4.1. Before running the examples, build the project.
These examples are located at activej -> examples -> core -> http

Simple “Hello World” Server

HelloWorldExample uses AsyncHttpServer class of HTTP module. It is a non-blocking server, which works in an eventloop:

public static void main(String[] args) throws IOException {
	Eventloop eventloop = Eventloop.create();
	AsyncHttpServer server = AsyncHttpServer.create(eventloop,
			request -> HttpResponse.ok200()
					.withPlainText("Hello world!"))
			.withListenPort(8080);

	server.listen();

	System.out.println("Server is running");
	System.out.println("You can connect from browser by visiting 'http://localhost:8080/'");

	eventloop.run();
}

This server runs in the provided eventloop and waits for connections on port 8080. When server receives a request, it sends back a Promise of greeting response.

Note: To add support for HTTPS to AsyncHttpServer you need to call withSslListenAddress or withSslListenAddresses method and pass SSLContext, Executor, and a port or address for the server to be listening on.

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

“Hello World” Server with pre-defined Launcher

Launchers manage application lifecycle and allow to create applications in a simple manner:

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);
	}
}

All you need to do is provide a servlet which processes the requests and launch the application. HttpServerLauncher will take care of everything else.

See full example on GitHub

Custom Server

With Launcher you can easily create HTTP servers from scratch. In this example we’re creating a simple server which sends a greeting:

public final class CustomHttpServerExample extends Launcher {
	private static final int PORT = 8080;

	@Provides
	Eventloop eventloop() {
		return Eventloop.create();
	}

	@Provides
	AsyncServlet servlet() {
		return request -> HttpResponse.ok200()
				.withPlainText("Hello from HTTP server");
	}

	@Provides
	@Eager
	AsyncHttpServer server(Eventloop eventloop, AsyncServlet servlet) {
		return AsyncHttpServer.create(eventloop, servlet).withListenPort(PORT);
	}

	@Override
	protected Module getModule() {
		return ServiceGraphModule.create();
	}

	@Override
	protected void run() throws Exception {
		logger.info("HTTP Server is now available at http://localhost:" + PORT);
		awaitShutdown();
	}

	public static void main(String[] args) throws Exception {
		Launcher launcher = new CustomHttpServerExample();
		launcher.launch(args);
	}
}

First, we provide an eventloop, a servlet, and an async server itself. Then, we override getModule method to provide our server with configs and ServiceGraphModule for building dependency graph of services.

Finally, we override Launcher main method run() and then define main method of the example.

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

Multithreaded Server Example

In this example we are using pre-defined MultithreadedHttpServerLauncher to create a multithreaded HTTP server. By default, there will be 4 worker servlets with workerIds. Each of them sends back a greeting and number of the worker that served the connection:

public final class MultithreadedHttpServerExample extends MultithreadedHttpServerLauncher {
	@Provides
	@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);
	}
}

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

Request Parameters Example

This example represents requests with parameters which are received with methods getPostParameters and getQueryParameter

@Provides
AsyncServlet servlet(Executor executor) {
	return RoutingServlet.create()
			.map(POST, "/hello", loadBody()
					.serve(request -> {
						String name = request.getPostParameters().get("name");
						return HttpResponse.ok200()
								.withHtml("<h1><center>Hello from POST, " + name + "!</center></h1>");
					}))
			.map(GET, "/hello", request -> {
				String name = request.getQueryParameter("name");
				return HttpResponse.ok200()
						.withHtml("<h1><center>Hello from GET, " + name + "!</center></h1>");
			})
			.map("/*", StaticServlet.ofClassPath(executor, RESOURCE_DIR)
					.withIndexHtml());
}

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

Static Servlet Example

Shows how to set up and utilize StaticServlet to create servlets with some static content, in our case it will get content from static/site directory.

@Provides
AsyncServlet servlet(Executor executor) {
	return StaticServlet.ofClassPath(executor, "static/site")
			.withIndexHtml();
}

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

Servlet Decorator Example

Shows basic functionality of AsyncServletDecorator class. It creates a wrap over AsyncServlets and adds behaviour for particular events, for example, exception handling or processing received responses. In the example, we made loading of request body default on the servlet using loadBody():

@Provides
AsyncServlet servlet(Executor executor) {
	return loadBody().serve(
			RoutingServlet.create()
					.map(GET, "/", StaticServlet.ofClassPath(executor, "static/wrapper")
							.withMappingTo("page.html"))
					.map(POST, "/", request -> {
						String text = request.getPostParameter("text");
						if (text == null) {
							return HttpResponse.redirect302("/");
						}
						return HttpResponse.ok200().withPlainText("Message: " + text);
					})
					.map(GET, "/failPage", request -> {
						throw new RuntimeException("fail");
					})
					.then(catchRuntimeExceptions())
					.then(mapException(e -> HttpResponse.ofCode(404).withPlainText("Error: " + e))));
}

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

Routing Servlet Example

Represents how to set up servlet routing tree. This process resembles Express approach. To add a route to a RoutingServlet, you should use method map:

.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>" +
						"<a href=\"/user/0\"> Data for user with ID 0 </a><br>" +
						"<br>" +
						"<a href=\"/path3\"> Non existent </a>"))
  • method (optional) is one of the HTTP methods (GET, POST, etc)
  • path is the path on the server
  • servlet defines the logic of request processing.

The whole servlet tree will look as follows:

@Provides
AsyncServlet servlet() {
	return RoutingServlet.create()
			//[START REGION_2]
			.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>" +
									"<a href=\"/user/0\"> Data for user with ID 0 </a><br>" +
									"<br>" +
									"<a href=\"/path3\"> Non existent </a>"))
			//[END REGION_2]
			.map(GET, "/path1", request ->
					HttpResponse.ok200()
							.withHtml("<h1>Hello from the first path!</h1>" +
									"<a href=\"/\">Go home</a>"))
			.map(GET, "/path2", request ->
					HttpResponse.ok200()
							.withHtml("<h1>Hello from the second path!</h1>" +
									"<a href=\"/\">Go home</a>"))

			//[START REGION_3]
			.map(GET, "/user/:user_id", request -> {
				String userId = request.getPathParameter("user_id");
				return HttpResponse.ok200()
						.withHtml("<h1>You have requested data for user with ID: " + userId + "</h1>" +
								"<h3>Try changing URL after <i>'.../user/'</i> to get data for users with different IDs</h3>");
			})
			//[END REGION_3]

			//[START REGION_4]
			.map("/*", request ->
					HttpResponse.ofCode(404)
							.withHtml("<h1>404</h1><p>Path '" + request.getRelativePath() + "' not found</p>" +
									"<a href=\"/\">Go home</a>"));
			//[END REGION_4]
}

You can map path parameters with /:param syntax:

.map(GET, "/user/:user_id", request -> {
	String userId = request.getPathParameter("user_id");
	return HttpResponse.ok200()
			.withHtml("<h1>You have requested data for user with ID: " + userId + "</h1>" +
					"<h3>Try changing URL after <i>'.../user/'</i> to get data for users with different IDs</h3>");
})

Path parameters can be retrieved by calling HttpRequest#getPathParameter method and passing the name of the path parameter.

You may also use wildcard route *:

.map("/*", request ->
		HttpResponse.ofCode(404)
				.withHtml("<h1>404</h1><p>Path '" + request.getRelativePath() + "' not found</p>" +
						"<a href=\"/\">Go home</a>"));

* states that whichever next path segment is received, it will be processed by this servlet.

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

Dynamic Routing Examples

A few examples that demonstrate how to route HTTP requests by some dynamic values rather than by predefined paths:

Routing Servlet Multibinder

Using Dependency Injection you may provide RoutingServlets in different modules. Such bindings would result in DI throwing an exception for conflicting keys RoutingServlet.class. However, you may instruct the DI to resolve conflicts by merging RoutingServlets into a single RoutingServlet that contains all of the routes specified in other routing servlets.

In the example we declare several modules that provide RoutingServlet with different routes:

private static final class ModuleA extends AbstractModule {
	@Provides
	RoutingServlet servlet() {
		return RoutingServlet.create()
				.map(GET, "/a", request -> HttpResponse.ok200().withPlainText("Hello from '/a' path\n"))
				.map(GET, "/b", request -> HttpResponse.ok200().withPlainText("Hello from '/b' path\n"))
				.map(GET, "/", request -> HttpResponse.ok200().withPlainText("Hello from '/' path\n"));
	}
}
private static final class ModuleB extends AbstractModule {
	@Provides
	RoutingServlet servlet() {
		return RoutingServlet.create()
				.map(GET, "/a/b", request -> HttpResponse.ok200().withPlainText("Hello from '/a/b' path\n"))
				.map(GET, "/b/a", request -> HttpResponse.ok200().withPlainText("Hello from '/b/a' path\n"))
				.map(GET, "/d", request -> HttpResponse.ok200().withPlainText("Hello from '/d' path\n"));
	}
}
private static final class ModuleC extends AbstractModule {
	@Provides
	RoutingServlet servlet() {
		return RoutingServlet.create()
				.map(GET, "/a/c", request -> HttpResponse.ok200().withPlainText("Hello from '/a/c' path\n"))
				.map(GET, "/b/c", request -> HttpResponse.ok200().withPlainText("Hello from '/b/c' path\n"))
				.map(POST, "/d", request -> HttpResponse.ok200().withPlainText("Hello from POST '/d' path\n"));
	}
}

Next, we define a Multibinder which merges conflicting servlets:

public static final Multibinder<RoutingServlet> SERVLET_MULTIBINDER = Multibinders.ofBinaryOperator((servlet1, servlet2) ->
		servlet1.merge(servlet2));

At last, we override HttpServerLauncher#getBusinesLogicModule method to provide a combined DI Module that contains ModuleA, ModuleB, ModuleC as well as an installed Multibinder for DI Key RoutingServlet.class.

If we launch the example, we can see that there are no conflicts as conflicting servlets where successfully merged together. We may open a web browser and visit any of the specified routes to ensure that routing works correctly:

Note, that if you try to merge RoutingServlets that have identical routes mapped, an exception would be thrown

See full example on GitHub

Blocking Servlet Example

Shows how to create a new thread for processing some complex operations on a BlockingServlet.

@Provides
AsyncServlet servlet(Executor executor) {
	return RoutingServlet.create()
			.map("/", request -> HttpResponse.ok200()
					.withHtml("<a href='hardWork'>Do hard work</a>"))
			.map("/hardWork", AsyncServlet.ofBlocking(executor, request -> {
				Thread.sleep(2000); //Hard work
				return HttpResponse.ok200()
						.withHtml("Hard work is done");
			}));
}

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

File Upload Example

In this example user uploads a file from local storage to the server:

@Provides
AsyncServlet servlet(Executor executor) {
	return RoutingServlet.create()
			.map(GET, "/*", StaticServlet.ofClassPath(executor, "static/multipart/")
					.withIndexHtml())
			.map(POST, "/test", request ->
					request.handleMultipart(MultipartDataHandler.file(fileName -> ChannelFileWriter.open(executor, path.resolve(fileName))))
							.map($ -> HttpResponse.ok200().withPlainText("Upload successful")));
}

To check how the example works, open your favorite browser and go to localhost:8080.

See full example on GitHub

Client Example

This example shows how to create an HTTP client using Launcher, pre-defined AsyncHttpClient, and AsyncDnsClient (maps given domains to the corresponding IP addresses):

@Provides
AsyncHttpClient client(Eventloop eventloop, AsyncDnsClient dnsClient) {
	return AsyncHttpClient.create(eventloop)
			.withDnsClient(dnsClient);
}

@Provides
AsyncDnsClient dnsClient(Eventloop eventloop, Config config) {
	return RemoteAsyncDnsClient.create(eventloop)
			.withDnsServerAddress(config.get(ofInetAddress(), "dns.address"))
			.withTimeout(config.get(ofDuration(), "dns.timeout"));
}
Note: To add support for HTTPS to AsyncHttpClient you need to call withSslEnabled method and pass SSLContext and Executor.

Override Launcher getModule method to provide needed configs and ServiceGraph dependency graph:

@Override
protected Module getModule() {
	return combine(
			ServiceGraphModule.create(),
			ConfigModule.create()
					.withEffectiveConfigLogger());
}

@Provides
Config config() {
	return Config.create()
			.with("dns.address", "8.8.8.8")
			.with("dns.timeout", "5 seconds")
			.overrideWith(Config.ofSystemProperties("config"));
}

Since our client extends Launcher, it overrides method run which defines the main functionality. In our case, it sends a request, waits for server response (either successful or failed) and then processes it:

@Override
protected void run() throws ExecutionException, InterruptedException {
	String url = args.length != 0 ? args[0] : "http://127.0.0.1:8080/";
	System.out.println("\nHTTP request: " + url);
	CompletableFuture<String> future = eventloop.submit(() ->
			httpClient.request(HttpRequest.get(url))
					.then(HttpMessage::loadBody)
					.map(body -> body.getString(UTF_8))
	);
	System.out.println("HTTP response: " + future.get());
	System.out.println();
}

eventloop.submit submits request sending and response receiving to the eventloop thread. So, our main thread will wait until future in the eventloop thread will return a result and only then the response will be printed out.

To check how the client works, launch Simple “Hello World” Server or Custom HTTP server and then run ClientExample.

See full example on GitHub

Multipart Data Handling Example

POST requests sometimes may be encoded as Multipart/form-data. Such requests may contain multiple fields and files. To handle a request containing multipart data you may use HttpRequest#handleMultipart method. You need to pass an instance of MultipartDataHandler to this method. There are several common handlers in MultipartDataHandler class. You can use them to collect fields to a map, send a a file to some ChannelConsumer<ByteBuf>, etc. Or you may write your own MultipartDataHandler if that is not enough.

In this example we will collect fields to a map end upload received files to some directory. After this we will log the collected fields and the number of uploaded files:

@Provides
AsyncServlet servlet() {
	return RoutingServlet.create()
			.map(POST, "/handleMultipart", request -> {
				Map<String, String> fields = new HashMap<>();

				return request.handleMultipart(MultipartDataHandler.fieldsToMap(fields, this::upload))
						.map($ -> {
							logger.info("Received fields: " + fields);
							logger.info("Uploaded " + fileUploadsCount + " files total");
							return HttpResponse.ok200();
						});
			});
}

To upload the received file to a file system we will use a ChannelFileWritter:

@NotNull
private Promise<ChannelConsumer<ByteBuf>> upload(String filename) {
	logger.info("Uploading file '{}' to {}", filename, path);
	return ChannelFileWriter.open(executor, path.resolve(filename))
			.map(writer -> writer.withAcknowledgement(ack ->
					ack.whenResult(() -> {
						logger.info("Upload of file '{}' finished", filename);
						fileUploadsCount++;
					})));
}

The Multipart/form-data request is manually forged and contains several fields and files. After running the example you should see a similar logging output:

Uploading file 'data.txt' to /tmp/multipart-data-files4909047508332989372
Upload of file 'data.txt' finished
Uploading file 'key.txt' to /tmp/multipart-data-files4909047508332989372
Upload of file 'key.txt' finished
Received fields: {last name=Johnson, first name=Alice, id=12345}
Uploaded 2 files total

You may inspect the directory from logging output to ensure the files are uploaded.

See full example on GitHub

WebSocket Pong Server

Let’s create a “Pong” WebSocket server. For this purpose we need to provide a RoutingServlet and use mapWebSocket method to map a Consumer of WebSocket as a servlet on /path. Our server will simply accept messages, print them out, and stream back a “Pong” message.

@Provides
AsyncServlet servlet() {
	return RoutingServlet.create()
			.mapWebSocket("/", webSocket -> webSocket.readMessage()
					.whenResult(message -> System.out.println("Received:" + message.getText()))
					.then(() -> webSocket.writeMessage(Message.text("Pong")))
					.whenComplete(webSocket::close));
}

See full example on GitHub

WebSocket Ping Client

Now let’s create a client that will send a “Ping” message to server via WebSocket connection.

@Override
protected void run() throws ExecutionException, InterruptedException {
	String url = args.length != 0 ? args[0] : "ws://127.0.0.1:8080/";
	System.out.println("\nWeb Socket request: " + url);
	CompletableFuture<?> future = eventloop.submit(() -> {
		System.out.println("Sending: Ping");
		return httpClient.webSocketRequest(HttpRequest.get(url))
				.then(webSocket -> webSocket.writeMessage(Message.text("Ping"))
						.then(webSocket::readMessage)
						.whenResult(message -> System.out.println("Received: " + message.getText()))
						.whenComplete(webSocket::close));
	});
	future.get();
}

First, we create a supplier and override its get method via lambda. Here we call AsyncHttpClient.webSocketRequest that sends a request and returns a Promise of a WebSocket. Then we create a Function that sends a “Ping” message and receives a response from server.

See full example on GitHub