Intro to Java's Simple Web Server

Java 18's Simple Web Server lets you use a command-line tool or API to host files and more. Here's how it works.

two people skydiving in tandem
Thinkstock

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.

Screenshot of a Simple Web Server instance. IDG
Figure 1. A Simple Web Server instance running on port 8000.

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:

Copyright © 2023 IDG Communications, Inc.