HTTP Module

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 -> Promise.of(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: AsyncServlets, Promises and CSP channels
  • can be used as application web server: support of externally provided DI Modules with business logic and AsyncServlets

Simple AsyncServlet interface

  • basically, it’s just an async function, mapping HttpRequest to Promise<HttpResponse>
  • collection of pre-defined AsyncServlets out of the box (StaticServlet, BlockingServlet, RoutingServlet etc.)
  • extensive support of functional composition of AsyncServlets

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 -> Promise.of(
					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=\"/path3\"> Path 3 </a>")))
			//[END REGION_2]
			.map(GET, "/path1", request -> Promise.of(
					HttpResponse.ok200()
							.withHtml("<h1>Hello form the first path!</h1>" +
									"<a href=\"/\">Go home</a>")))
			.map(GET, "/path2", request -> Promise.of(
					HttpResponse.ok200()
							.withHtml("<h1>Hello from the second path!</h1>" +
									"<a href=\"/\">Go home</a>")))
			//[START REGION_3]
			.map("/*", request -> Promise.of(
					HttpResponse.ofCode(404)
							.withHtml("<h1>404</h1><p>Path '" + request.getRelativePath() + "' not found</p>" +
									"<a href=\"/\">Go home</a>")));
			//[END REGION_3]
}

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 and HttpResponse and also ByteBufs received from AsyncSocket
  • 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

You can add HTTP module to your project by inserting dependency in pom.xml:

<dependency>
    <groupId>io.datakernel</groupId>
    <artifactId>datakernel-http</artifactId>
    <version>3.0.0-beta1</version>
</dependency>

Examples

Note: To run the examples, you need to clone DataKernel from GitHub:
$ git clone https://github.com/softindex/datakernel
And import it as a Maven project. Before running the examples, build the project.

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 -> Promise.of(
					HttpResponse.ok200()
							.withBody(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();
}

So, 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.

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 simply and minimalistically:

public final class HttpHelloWorldExample extends HttpServerLauncher {
	@Provides
	AsyncServlet servlet() {
		return request -> Promise.of(ok200().withPlainText("Hello World"));
	}

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

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

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;

	@Inject
	private AsyncHttpServer server;

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

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

	@Provides
	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 {
		awaitShutdown();
	}

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

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

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 the number of worker which served the connection:

public final class MultithreadedHttpServerExample extends MultithreadedHttpServerLauncher {
	@Provides
	@Worker
	AsyncServlet servlet(@WorkerId int workerId) {
		return request -> Promise.of(
				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

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 Promise.of(
								HttpResponse.ok200()
										.withHtml("<h1><center>Hello from POST, " + name + "!</center></h1>"));
					}))
			.map(GET, "/hello", request -> {
				String name = request.getQueryParameter("name");
				return Promise.of(
						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 Promise.of(
									HttpResponse.redirect302("/"));
						}
						return Promise.of(
								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 rout to RoutingServlet, you should use method map:

.map(GET, "/", request -> Promise.of(
		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=\"/path3\"> Path 3 </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.

So, the whole servlet tree will look as follows:

@Provides
AsyncServlet servlet() {
	return RoutingServlet.create()
			//[START REGION_2]
			.map(GET, "/", request -> Promise.of(
					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=\"/path3\"> Path 3 </a>")))
			//[END REGION_2]
			.map(GET, "/path1", request -> Promise.of(
					HttpResponse.ok200()
							.withHtml("<h1>Hello form the first path!</h1>" +
									"<a href=\"/\">Go home</a>")))
			.map(GET, "/path2", request -> Promise.of(
					HttpResponse.ok200()
							.withHtml("<h1>Hello from the second path!</h1>" +
									"<a href=\"/\">Go home</a>")))
			//[START REGION_3]
			.map("/*", request -> Promise.of(
					HttpResponse.ofCode(404)
							.withHtml("<h1>404</h1><p>Path '" + request.getRelativePath() + "' not found</p>" +
									"<a href=\"/\">Go home</a>")));
			//[END REGION_3]
}

Note this request:

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

* states, that whichever path until the next / 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.

Blocking Servlet Example

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

@Provides
AsyncServlet servlet(Executor executor) {
	return RoutingServlet.create()
			.map("/", request -> Promise.of(
					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 some 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.getFiles(name -> ChannelFileWriter.open(executor, path.resolve(name)))
							.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

ClientExample is an example of creating an HTTP client. It extends Launcher and utilizes pre-defined AsyncHttpClient and AsyncDnsClient (can resolve given domain names into their respective 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"));
}

Next, getModule provides with needed configs and dependency graph ServiceGraph:

@Override
protected Module getModule() {
	return combine(
			ServiceGraphModule.create(),
			ConfigModule.create()
					.printEffectiveConfig()
					.rebindImport(new Key<CompletionStage<Void>>() {}, new Key<CompletionStage<Void>>(OnStart.class) {})
	);
}

@Provides
Config config() {
	return Config.create()
			.with("dns.address", "8.8.8.8")
			.with("dns.timeout", "5 seconds")
			.overrideWith(Config.ofProperties(System.getProperties()).getChild("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("HTTP 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());
}

eventloop.submit submits request sending and response receiving to eventloop thread. So, our main thread will wait until future in 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 Server Scratch and then run ClientExample.

See full example on GitHub.