Intro to Java's Simple Web Server
Intro to Java's Simple Web Server
One of the most handy new features included in the Java 18 release (March 2022) was the new Simple Web Server, which makes it easy to spin up and configure an HTTP file server. It also exposes an API that extends the existing httpserver
package for building simple use cases. The new Simple Web Server is a useful tool that every Java developer should have in their bag of tricks. Let’s check it out!
Simple Web Server on the command line
Java’s new jwebserver
command makes it simple to run a basic web server. It is analogous to the popular SimpleHTTPServer tool in the Python world.
The first step is to make sure you are running Java 18 or a later release. Type java --version
to find out what release you are currently running. I recommend using SDKMan to manage JDK installs. It’s especially useful for juggling multiple versions.
jwebserver basics
The most basic thing you can do with the Java Simple Web Server is to serve the current directory on port 8000. Just enter the command shown in Listing 1.
Listing 1. No-arg web server
$ jwebserver Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::". Serving /home/matthewcarltyson and subdirectories on 127.0.0.1 port 8000 URL http://127.0.0.1:8000/
From there, if you go to your browser and visit localhost:8000
, you’ll see a listing of the file system, as shown in Figure 1.
Configuring the command line
There are several common things you might need to do to fine-tune Simple Web Server on the command-line. For example, you can change the port, the address to bind to (the network interface to listen on), and the directory to serve.
In Listing 2, you can see how to listen on port 8080, on all interfaces, and in the /foo/bar
directory.
Listing 2. Listen on port 8080, all interfaces, /foo/bar
$ jwebserver -b 0.0.0.0 -p 8081 -d /foo/bar You can get a list of all the options with $ jwebserver -h.
As you can see, the jwebserver
command-line tool makes it possible to serve static files using the simplest possible syntax. Next, we’ll take a look at the Simple Web Server API.
Using the Simple Web Server API
The Simple Web Server Javadoc is a good starting point for learning about the API. The SimpleFileServer
class exists in the com.sun.net.httpserver
package. (This package also houses the older, more low-level APIs for building web servers. The httpserver
package extends that functionality for simpler requirements.) The jwebserver
CLI tool uses SimpleFileServer
to do its work, and we can also use it programmatically.
The SimpleFileServer
class only handles GET
and HTTP/1.1. We can do some interesting things with it, though. For example, this introduction to working with Simple Web Server suggests a way to use the Google Java in-memory file system project to fake a file system for the handler.
We’re going to use the idea of an in-memory file system to modify the FileHandler
in SimpleFileServer
to actually serve a virtual file system from memory. Then, we’ll use the httpserver
package to handle a POST
to add a faux file to the faux file system.
Serve a virtual file system from memory
To begin, let’s create a quick Maven project using the following command:
$ mvn archetype:generate -DgroupId=.com.infoworld -DartifactId=jsws -DarchetypeArtifactId=maven-archetype-quickstart
Now, CD into the new /jsws
directory.
Set the compiler and source versions to 18
in the pom.xml, as described here.
Next, add Google jimfs
to the dependencies, as shown in Listing 3.
Listing 3. The google Java in-memory file system dependency
<dependency> <groupId>com.google.jimfs</groupId> <artifactId>jimfs</artifactId> <version>1.3.0</version> </dependency>
Now, we can modify the src/main/java/App.java
file to serve a fake file system. You can see the code to do this in Listing 4.
Listing 4. Serving the in-memory file system with SimpleFileServer
package com.infoworld; import java.util.List; import java.nio.charset.StandardCharsets; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import com.sun.net.httpserver.SimpleFileServer; import static com.sun.net.httpserver.SimpleFileServer.OutputLevel; public class Mem { private static final InetSocketAddress ADDR = new InetSocketAddress("0.0.0.0", 8080); public static void main( String[] args ) throws Exception { Path root = createDirectoryHierarchy(); var server = SimpleFileServer.createFileServer(ADDR, root, OutputLevel.VERBOSE); server.start(); } private static Path createDirectoryHierarchy() throws IOException { FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path root = fs.getPath("/"); Files.createDirectories(root.resolve("test/test2")); Path foo = fs.getPath("/foo"); Files.createDirectory(foo); Path hello = foo.resolve("hello.txt"); Files.write(hello, List.of("hello", "world"), StandardCharsets.UTF_8); return root; } }
The idea in Listing 4 is to simulate the standard local file system API using Google’s open source jimfs library, which implements the java.nio.file
API but does everything in-memory, like a virtual file system. Using the library, you can define your own directory paths and files programmatically. So, we create our own virtual directory structure and hand that off to SimpleFileServer
as the file handler.
We configure the SimpleFileServer
class programmatically:
var server = SimpleFileServer.createFileServer(ADDR, root, OutputLevel.VERBOSE);
This accepts the internet address to bind to, just like we saw from the command line. In this case, we pass in the unspecified interface and port 8080. After that comes the file system root. For this example, we’ll give it the Path
object created by our createDirectoryHierarchy()
method.
The createDirectoryHierarchy()
method uses jimfs
to build a Path
object: FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
. It then populates the Path
with files and directories. The jimfs
API for creating paths and files with content is not hard to grasp; for example, we create one with Path hello = foo.resolve("hello.txt");
. You can use most of the objects like they were just normal Java NIO paths.
Now, if we run this code and visit localhost:8080
, you’ll see the directory listing and be able to browse it and see file contents, just like you would with a normal file server.
Creating a virtual file
Let’s take this idea one step further and add the ability to upload a new file. We can use the com.sun.net.httpserver
package to accept a POST
request that will upload a new file to our in-memory file system. You can see this in Listing 5.
Listing 5. Uploading a new file to the in-memory file system
package com.infoworld; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.util.List; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import com.sun.net.httpserver.SimpleFileServer; import static com.sun.net.httpserver.SimpleFileServer.OutputLevel; public class App { public static void main( String[] args ) throws Exception { // same config... server.start(); // Create the HTTP server with the UploadHandler using the same 'root' path HttpServer httpServer = HttpServer.create(new InetSocketAddress("0.0.0.0", 8081), 0); httpServer.createContext("/upload", new UploadHandler(root)); httpServer.start(); } private static Path createDirectoryHierarchy() throws IOException { // same ... } // Handler to process POST requests and upload files static class UploadHandler implements HttpHandler { private final Path root; public UploadHandler(Path root) { this.root = root; } @Override public void handle(HttpExchange exchange) throws IOException { if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) { String filename = exchange.getRequestHeaders().getFirst("filename"); if (filename != null) { Path newFilePath = root.resolve(filename); try (OutputStream os = Files.newOutputStream(newFilePath)) { exchange.getRequestBody().transferTo(os); } String response = "File uploaded successfully: " + filename; exchange.sendResponseHeaders(200, response.getBytes(StandardCharsets.UTF_8).length); try (OutputStream os = exchange.getResponseBody()) { os.write(response.getBytes(StandardCharsets.UTF_8)); } } else { exchange.sendResponseHeaders(400, -1); // Bad Request } } else { exchange.sendResponseHeaders(405, -1); // Method Not Allowed } } } }
In Listing 5, we keep most of the class the same, but at an HttpServer
instance listening on port 8081. This is configured with our custom uploadHandler
, which takes the uploaded data and uses it to write a new file to the root path that we created in createDirectoryHierarchy
.
To test this out, we can run the whole server cluster with:
$ mvn clean install exec:java -Dexec.mainClass="com.infoworld.Mem"
You can push a new file to the server with a CURL request like the one in Listing 6.
Listing 6. CURL POST a file
$ touch file.txt $ curl -X POST -H "filename: file.txt" -d "@file.txt" http://localhost:8081/upload File uploaded successfully: file.txt
When you reload the localhost:8080/
file listings, you’ll see the new file.txt
and you can click it and view its contents.
Conclusion
Simple Web Server is a welcome addition to the Java toolset. Not only does it make hosting files very simple with the CLI, it introduces some interesting possibilities with its API, especially when used in conjunction with the lower level HttpServer
API.
To learn more, check out these additional resources: