In the part 1 we’ve seen how we can use Simple Web Server (SWS) from the command line during writing or debugging web applications. While such usage should be enough for a lot of use-cases, from time to time we’ll need more control over which HTTP methods, headers, MIME types etc. That’s why in Java 18 there is a possibility to use SWS programmatically, i.e. to embed it into our applications. For that, we can use some existing classes (i.e. pre Java 18) and also some new ones. Let’s see the basic programmatic usage of Simple Web Server (SWS) in action!
Minimal file server
To start a minimal working file server we’ll use a new class SimpleFileServer
from the com.sun.net.httpserver
package. It exposes just 3 static methods for creating a server, a handler and a filter. We can create and start a minimal server like this:
1public void minimalFileServer(int port, String path) {
2 var server = SimpleFileServer.createFileServer(
3 new InetSocketAddress(port),
4 Path.of(path),
5 SimpleFileServer.OutputLevel.VERBOSE
6 );
7 server.start();
8}
We just need to provide a port it will listen to and a path to the directory from which we want to serve files. It is essentially an equivalent of CLI tool jwebserver
used like jwebserver --port PORT --directory PATH --output verbose
. That means that it supports (only) GET and HEAD methods and that it will print a detailed log of communication on System.out
. Also, given path will be served as a root context. This simply means that if we call our method like app.minimalFileServer(7001, "/Users/ivanmilosavljevic/tmp");
and then visit http://localhost:7001
in the browser, we’ll see either a directory listing of our path (if the directory doesn’t contain index.html
file) or contents of index.html
file. All other URLs that we try to visit will return 404.
This is neat but we didn’t need to fire the whole application for that. Let’s add some functionality to it. We’ll start with adding a context to our server so it can know which handler to call for which URL path.
File server with additional context
To add a context, we shall use createFileHandler
method of a SimpleFileServer
class:
1public void fileServerWithAdditionalContext(int port, String path) {
2 var server = SimpleFileServer.createFileServer(
3 new InetSocketAddress(port),
4 Path.of(path),
5 SimpleFileServer.OutputLevel.VERBOSE
6 );
7 var handler = SimpleFileServer.createFileHandler(Path.of("/Users/ivanmilosavljevic/dotfilesMac"));
8 server.createContext("/browse", handler);
9 server.start();
10}
If we now start it by calling app.fileServerWithAdditionalContext(7002, "/Users/ivanmilosavljevic/tmp");
, our server will answer on two URLs: the root one http://localhost:7002
and the additional one http://localhost:7002/browse/
. But we still support only GET and HEAD methods. Let’s see how we can respond to POST, and also add some pre- and post-processing of incoming requests.
File server with custom handler and filter (the “old” way)
OK, it’s not fair to call this the old way because it still works properly and is, in fact, more powerful than the “new” way of using class HttpHandlers
and an interface Request
, which were introduced in Java 18. I just wanted to make a distinction between classes introduced all the way back in Java 1.6. This is a way to create an HttpServer
with a custom handler and filter:
1public void serverWithCustomHandler(int port, String path) throws IOException {
2 var server = HttpServer.create(
3 new InetSocketAddress(port),
4 10,
5 "/store", //this URI will be handled by given Handler and Filter
6 new MultiMethodHandler(),
7 new ConsoleLogFilter()
8 );
9 var handler = SimpleFileServer.createFileHandler(Path.of(path));
10 server.createContext("/browse", handler);
11 server.start();
12}
Note that we are using HttpServer
class here to create a server, but calling its newly introduced create
function. We also use SimpleFileServer
to create a handler for a context. This is to show that we can mix and match these two ways. If we call this method like app.minimalFileServer(7003, "/Users/ivanmilosavljevic/tmp");
, our server will respond on URLs http://localhost:7003/store/
and http://localhost:7003/browse/
. The latter one will respond to GET and HEAD requests (and return HTTP status 405 for all the others). But what about the former one?
It will respond to anything we define in the MultiMethodHandler
class. That class can look like this:
1static class MultiMethodHandler implements HttpHandler {
2 @Override
3 public void handle(HttpExchange exchange) throws IOException {
4 String requestMethod = exchange.getRequestMethod();
5 switch (requestMethod) {
6 case "GET" -> handleGet(exchange);
7 case "POST" -> handlePost(exchange);
8 default -> handleError(exchange);
9 }
10 }
11
12 private void handleGet(HttpExchange exchange) throws IOException {
13 var responseBytes = "<html><body><h1>I'm a teapot</h1></body></html>".getBytes(StandardCharsets.UTF_8);
14 exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
15 exchange.getResponseHeaders().set("Content-Length", Integer.toString(responseBytes.length));
16 exchange.sendResponseHeaders(418, responseBytes.length);
17 try (OutputStream os = exchange.getResponseBody()) {
18 os.write(responseBytes);
19 }
20 }
21
22 private void handlePost(HttpExchange exchange) throws IOException {
23 var responseBytes = "OK".getBytes(StandardCharsets.UTF_8);
24 exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
25 exchange.getResponseHeaders().set("Content-Length", Integer.toString(responseBytes.length));
26 exchange.sendResponseHeaders(200, responseBytes.length);
27 try (OutputStream os = exchange.getResponseBody()) {
28 os.write(responseBytes);
29 }
30 }
31
32 private void handleError(HttpExchange exchange) throws IOException {
33 var responseBytes = "<html><body>Error 451</body></html>".getBytes(StandardCharsets.UTF_8);
34 exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
35 exchange.getResponseHeaders().set("Content-Length", Integer.toString(responseBytes.length));
36 exchange.sendResponseHeaders(451, responseBytes.length);
37 try (OutputStream os = exchange.getResponseBody()) {
38 os.write(responseBytes);
39 }
40 }
41}
Method handle
defines which HTTP methods will we respond to. In this case, we opted for GET and POST. All others will be handled by handleError
which will just return 451. Of course, these handler methods are quite simplistic, but even in a real-world usage they might be just a tad more complicated. For example, you might return some hard-coded JSON or a similar response. Remember that we’re not trying to create a production HTTP server here, just something to aid in developing.
For the sake of completion, this is how a ConsoleLogFilter
might look like:
1static class ConsoleLogFilter extends Filter {
2 private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z");
3
4 @Override
5 public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
6 String msg = "[" + OffsetDateTime.now().format(FORMATTER) + "] " +
7 exchange.getRequestMethod() + ' ' + exchange.getRequestURI() +
8 ' ' + exchange.getRemoteAddress();
9 System.out.println(msg);
10 exchange.getResponseHeaders().put("X-Custom-Header", List.of("filtered"));
11 chain.doFilter(exchange);
12 }
13
14 @Override
15 public String description() {
16 return "Primitive console logging filter";
17 }
18}
When one if the URLs are hit, this filter will log something like this to the standard out [17/Feb/2022:13:45:03 +0100] GET /store/ /[0:0:0:0:0:0:0:1]:57247
.
File server with custom handler and filter (the “new” way)
Finally, let’s see how to create and combine handlers using the new HttpHandlers
class:
1public void serverWithCustomHandlerNewWay(int port) throws IOException {
2 var okHandler = HttpHandlers.of(200, Headers.of("X-method", "PUT"), "Hard-coded response body");
3 var errHandler = HttpHandlers.of(500, Headers.of("X-method", "not PUT"), "Ain't gonna work");
4 var combinedHandler = HttpHandlers.handleOrElse(
5 r -> r.getRequestMethod().equals("PUT"),
6 okHandler,
7 errHandler
8 );
9 var server = HttpServer.create(
10 new InetSocketAddress(port),
11 10,
12 "/",
13 combinedHandler
14 );
15 server.start();
16}
Here we create two handlers: okHandler
and errHandler
using static factory method of
, and combine them using handleOrElse
function from HttpHandlers
class. It behaves as an if
statement: if our predicate on Request
resolves to true
we’ll invoke first handler, else we’ll invoke the second one. If we call this method like app.serverWithCustomHandler(7004);
, our server will respond on URL http://localhost:7004
but only to PUT requests (and return HTTP status 500 for all the others).
Let’s use awesome CLI tool httpie to issue a few requests to our endpoint. First we’ll try PUT and see that it returns 200:
1$ http PUT http://localhost:7004/
2HTTP/1.1 200 OK
3Content-length: 24
4Date: Tue, 05 Apr 2022 17:31:34 GMT
5X-method: PUT
6
7Hard-coded response body
Now let’s try GET which will return 500:
1$ http GET http://localhost:7004/
2HTTP/1.1 500 Internal Server Error
3Content-length: 16
4Date: Tue, 05 Apr 2022 17:31:20 GMT
5X-method: not PUT
6
7Ain't gonna work
HEAD will also return 500:
1$ http HEAD http://localhost:7004/
2HTTP/1.1 500 Internal Server Error
3Content-length: 16
4Date: Tue, 05 Apr 2022 17:35:57 GMT
5X-method: not PUT
Summary
We have seen how to use SWS programmatically and how to enhance its capabilities compared to the CLI tool jwebserver
. Whether you want to respond to additional HTTP methods, modify requests or responses, or add custom logging, new (and old) classes we mentioned got you covered.
Dear fellow developer, thank you for reading this article about the programmatic usage of SWS. Until next time, TheJavaGuy saluts you!
Comments