Examples
- Simple "Hello World" Server - a simple asynchronous server created using HttpServer
- "Hello World" Server with Pre-defined Launcher - HTTP module provides you with some pre-defined launchers, which are very easy to use for creating servers.
- Custom Server - example of creating a server from scratch using Launcher.
- Multithreaded Server Example - HTTP multithreaded server example.
- Request Parameters Example - example of processing requests with parameter.
- Static Servlet Example - example of StaticServlet utilizing
- Routing Servlet Example - example of
RoutingServlet
usage for creating servlet tree. - Dynamic Routing Examples - various examples of routing based on host, cookie, mime-type, etc.
- Routing Servlet Multibinder - example of using Multibinder to merge conflicting
RoutingServlet
s - Blocking Servlet Example - example of handling complex operations on server in a new thread.
- File Upload Example - example of uploading a file from client local storage to server.
- Client Example - creating an HTTP client utilizing Launcher.
- Multipart Data Handling Example - simple server that handles Multipart/form-data
- WebSocket Echo Server Example - creating a basic server with WebSocket communication protocol.
- WebSocket Client Example - setting up a client with WebSocket communication protocol.
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 v6.0-beta2. Before running the examples, build the project. These examples are located at activej/examples/core/http
Simple "Hello World" Server
HelloWorldExample uses the HttpServer class of the HTTP module. It is a non-blocking server that runs in an eventloop reactor:
public static void main(String[] args) throws IOException {
Eventloop eventloop = Eventloop.create();
HttpServer server = HttpServer.builder(eventloop,
request -> HttpResponse.ok200()
.withPlainText("Hello world!")
.toPromise())
.withListenPort(8080)
.build();
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 the server receives a request, it sends back a Promise of greeting response.
To add support for HTTPS to HttpServer
you need to call withSslListenAddress
or withSslListenAddresses
builder 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.
"Hello World" Server with pre-defined Launcher
Launchers manage the lifecycle of applications and allow you to create applications in a simple way:
public final class HttpHelloWorldExample extends HttpServerLauncher {
@Provides
AsyncServlet servlet() {
return request -> HttpResponse.ok200()
.withPlainText("Hello World")
.toPromise();
}
public static void main(String[] args) throws Exception {
Launcher launcher = new HttpHelloWorldExample();
launcher.launch(args);
}
}
All you have to do is provide a servlet that handles the requests and launch the application. HttpServerLauncher will take care of the rest.
Custom Server
With Launcher you can easily create HTTP servers from scratch. In this example, we create a simple server that sends a greeting:
public final class CustomHttpServerExample extends Launcher {
private static final int PORT = 8080;
@Provides
NioReactor reactor() {
return Eventloop.create();
}
@Provides
AsyncServlet servlet() {
return request -> HttpResponse.ok200()
.withPlainText("Hello from HTTP server")
.toPromise();
}
@Provides
@Eager
HttpServer server(NioReactor reactor, AsyncServlet servlet) {
return HttpServer.builder(reactor, servlet)
.withListenPort(PORT)
.build();
}
@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 a reactor, a servlet, and a reactive server itself. Then, we override getModule method to provide our server with configs and ServiceGraphModule to build the service dependency graph.
Finally, we override the Launcher
's main method run()
and then define the main
method of the example.
To check how the example works, open your favorite browser and go to localhost:8080
Multithreaded Server Example
In this example we use the predefined 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 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")
.toPromise();
}
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
Request Parameters Example
This example demonstrates handling of requests with parameters which are received with methods getPostParameters
and getQueryParameter
@Provides
IStaticLoader staticLoader(Reactor reactor, Executor executor) {
return IStaticLoader.ofClassPath(reactor, executor, RESOURCE_DIR);
}
@Provides
AsyncServlet servlet(Reactor reactor, IStaticLoader staticLoader) {
return RoutingServlet.builder(reactor)
.with(POST, "/hello", request -> request.loadBody()
.then($ -> {
String name = request.getPostParameters().get("name");
return HttpResponse.ok200()
.withHtml("<h1><center>Hello from POST, " + name + "!</center></h1>")
.toPromise();
}))
.with(GET, "/hello", request -> {
String name = request.getQueryParameter("name");
return HttpResponse.ok200()
.withHtml("<h1><center>Hello from GET, " + name + "!</center></h1>")
.toPromise();
})
.with("/*", StaticServlet.builder(reactor, staticLoader)
.withIndexHtml()
.build())
.build();
}
To check how the example works, open your favorite browser and go to localhost:8080
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 the static/site
directory.
@Provides
IStaticLoader staticLoader(Reactor reactor, Executor executor) {
return IStaticLoader.ofClassPath(reactor, executor, "static/site");
}
@Provides
AsyncServlet servlet(Reactor reactor, IStaticLoader staticLoader) {
return StaticServlet.builder(reactor, staticLoader)
.withIndexHtml()
.build();
}
To check how the example works, open your favorite browser and go to localhost:8080
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 with
of RoutingServet
's builder:
.with(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>
""")
.toPromise())
- 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(Reactor reactor) {
return RoutingServlet.builder(reactor)
//[START REGION_2]
.with(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>
""")
.toPromise())
//[END REGION_2]
.with(GET, "/path1", request ->
HttpResponse.ok200()
.withHtml("""
<h1>Hello from the first path!</h1>
<a href="/">Go home</a>
""")
.toPromise())
.with(GET, "/path2", request ->
HttpResponse.ok200()
.withHtml("""
<h1>Hello from the second path!</h1>
<a href="/">Go home</a>
""")
.toPromise())
//[START REGION_3]
.with(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>
"""
.replace("$userId", userId))
.toPromise();
})
//[END REGION_3]
//[START REGION_4]
.with("/*", request ->
HttpResponse.ofCode(404)
.withHtml("""
<h1>404</h1><p>Path '$path' not found</p>
<a href="/">Go home</a>
"""
.replace("$path", request.getRelativePath()))
.toPromise())
.build();
//[END REGION_4]
}
You can map path parameters with /:param
syntax:
.with(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>
"""
.replace("$userId", userId))
.toPromise();
})
Path parameters can be retrieved by calling HttpRequest#getPathParameter
method and passing the name of the path parameter.
You may also use wildcard route *
:
.with("/*", request ->
HttpResponse.ofCode(404)
.withHtml("""
<h1>404</h1><p>Path '$path' not found</p>
<a href="/">Go home</a>
"""
.replace("$path", request.getRelativePath()))
.toPromise())
.build();
*
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
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 can provide RoutingServlet
s in different modules.
Such bindings would result in DI throwing an exception because of conflicting RoutingServlet.class
keys. However, you can instruct the DI to resolve conflicts by merging RoutingServlet
s into a single RoutingServlet
that contains all 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(Reactor reactor) {
return RoutingServlet.builder(reactor)
.with(GET, "/a", request -> HttpResponse.ok200()
.withPlainText("Hello from '/a' path\n")
.toPromise())
.with(GET, "/b", request -> HttpResponse.ok200()
.withPlainText("Hello from '/b' path\n")
.toPromise())
.with(GET, "/", request -> HttpResponse.ok200()
.withPlainText("Hello from '/' path\n")
.toPromise())
.build();
}
}
private static final class ModuleB extends AbstractModule {
@Provides
RoutingServlet servlet(Reactor reactor) {
return RoutingServlet.builder(reactor)
.with(GET, "/a/b", request -> HttpResponse.ok200()
.withPlainText("Hello from '/a/b' path\n")
.toPromise())
.with(GET, "/b/a", request -> HttpResponse.ok200()
.withPlainText("Hello from '/b/a' path\n")
.toPromise())
.with(GET, "/d", request -> HttpResponse.ok200()
.withPlainText("Hello from '/d' path\n")
.toPromise())
.build();
}
}
private static final class ModuleC extends AbstractModule {
@Provides
RoutingServlet servlet(Reactor reactor) {
return RoutingServlet.builder(reactor)
.with(GET, "/a/c", request -> HttpResponse.ok200()
.withPlainText("Hello from '/a/c' path\n")
.toPromise())
.with(GET, "/b/c", request -> HttpResponse.ok200()
.withPlainText("Hello from '/b/c' path\n")
.toPromise())
.with(POST, "/d", request -> HttpResponse.ok200()
.withPlainText("Hello from POST '/d' path\n")
.toPromise())
.build();
}
}
Next, we define a Multibinder which merges conflicting servlets:
public static final Multibinder<RoutingServlet> SERVLET_MULTIBINDER = Multibinders.ofBinaryOperator(RoutingServlet::merge);
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 can open a web browser and visit any of the specified routes to make sure that routing is working properly:
If you try to merge RoutingServlets that have identical routes mapped, an exception would be thrown
Blocking Servlet Example
Shows how to create a new thread for processing some complex operations on a BlockingServlet
@Provides
AsyncServlet servlet(Reactor reactor, Executor executor) {
return RoutingServlet.builder(reactor)
.with("/", request -> HttpResponse.ok200()
.withHtml("<a href='hardWork'>Do hard work</a>")
.toPromise())
.with("/hardWork", AsyncServlet.ofBlocking(executor, request -> {
Thread.sleep(2000); //Hard work
return HttpResponse.ok200()
.withHtml("Hard work is done")
.build();
}))
.build();
}
To check how the example works, open your favorite browser and go to localhost:8080
File Upload Example
In this example user uploads a file from local storage to the server:
@Provides
IStaticLoader staticLoader(Reactor reactor, Executor executor) {
return IStaticLoader.ofClassPath(reactor, executor, "static/multipart/");
}
@Provides
AsyncServlet servlet(Reactor reactor, IStaticLoader staticLoader, Executor executor) {
return RoutingServlet.builder(reactor)
.with(GET, "/*", StaticServlet.builder(reactor, staticLoader)
.withIndexHtml()
.build())
.with(POST, "/test", request ->
request.handleMultipart(AsyncMultipartDataHandler.file(fileName -> ChannelFileWriter.open(executor, path.resolve(fileName))))
.then($ -> HttpResponse.ok200()
.withPlainText("Upload successful")
.toPromise()))
.build();
}
To check how the example works, open your favorite browser and go to localhost:8080
Client Example
This example shows how to create an HTTP client using Launcher, predefined HttpClient, and DnsClient (maps given domains to the corresponding IP addresses):
@Provides
IHttpClient client(NioReactor reactor, IDnsClient dnsClient) {
return HttpClient.builder(reactor)
.withDnsClient(dnsClient)
.build();
}
@Provides
IDnsClient dnsClient(NioReactor reactor, Config config) {
return DnsClient.builder(reactor)
.withDnsServerAddress(config.get(ofInetAddress(), "dns.address"))
.withTimeout(config.get(ofDuration(), "dns.timeout"))
.build();
}
To add support for HTTPS to HttpClient
you need to call withSslEnabled
builder 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.builder()
.withEffectiveConfigLogger()
.build());
}
@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 = reactor.submit(() ->
httpClient.request(HttpRequest.get(url).build())
.then(response -> response.loadBody())
.map(body -> body.getString(UTF_8))
);
System.out.println("HTTP response: " + future.get());
System.out.println();
}
reactor.submit
submits request sending and response receiving to the reactor. So, our main thread will wait until
future in the reactor 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
Multipart Data Handling Example
POST requests can sometimes be encoded as Multipart/form-data. Such requests may contain multiple fields and files. The HttpRequest#handleMultipart
method can be used to handle a request containing multipart data. You need to pass an instance of MultipartDataHandler
to this method. There MultipartDataHandler
class contains several common handlers. You can use them to collect fields to a map, send a file to some ChannelConsumer<ByteBuf>
, etc. Or you can 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(Reactor reactor) {
return RoutingServlet.builder(reactor)
.with(POST, "/handleMultipart", request -> {
Map<String, String> fields = new HashMap<>();
return request.handleMultipart(AsyncMultipartDataHandler.fieldsToMap(fields, this::upload))
.then($ -> {
logger.info("Received fields: {}", fields);
logger.info("Uploaded {} files total", fileUploadsCount);
return HttpResponse.ok200().toPromise();
});
})
.build();
}
To upload the received file to a file system we will use a ChannelFileWritter:
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.
WebSocket Pong Server
Let's create a "Pong" WebSocket server. To do this we need to
provide a RoutingServlet
and use withWebSocket method to map a Consumer of WebSocket as a servlet on /
path.
Our server will simply receive the messages, print them out, and send back the "Pong" message.
@Provides
AsyncServlet servlet(Reactor reactor) {
return RoutingServlet.builder(reactor)
.withWebSocket("/", webSocket -> webSocket.readMessage()
.whenResult(message -> System.out.println("Received:" + message.getText()))
.then(() -> webSocket.writeMessage(Message.text("Pong")))
.whenComplete(webSocket::close))
.build();
}
WebSocket Ping Client
Now let's create a client that will send a "Ping" message to server via a 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 = reactor.submit(() -> {
System.out.println("Sending: Ping");
return webSocketClient.webSocketRequest(HttpRequest.get(url).build())
.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 using lambda. Here we call IWebSocketClient.webSocketRequest
that sends a request and returns a Promise of a IWebSocket
. Then we create a Function
that sends a "Ping" message and receives a response from server.