使用简单 Web 服务器

简单 Web 服务器已 添加到 JDK 18 中的 jdk.httpserver 模块。它是一个极简的 HTTP 静态文件服务器,旨在用于原型制作、测试和调试。本文探讨了简单 Web 服务器一些不太明显但有趣的编程应用。

简介

简单 Web 服务器通过在命令行上使用 jwebserver 运行。它通过 HTTP/1.1 在单个目录层次结构中提供静态文件;不支持动态内容和其他 HTTP 版本。除了命令行工具之外,简单 Web 服务器还提供了一个 API,用于以编程方式创建和自定义服务器及其组件。此 API 扩展了 com.sun.net.httpserver 包,该包自 2006 年以来已包含在 JDK 中并得到官方支持。

本文重点介绍简单 Web 服务器 API,并描述了几种使用服务器及其组件的方法,这些方法超出了 jwebserver 工具的常见用法。具体来说,将探讨以下应用程序

  • 创建内存文件服务器,
  • 提供 zip 文件系统,
  • 提供 JRT 目录,
  • 将文件处理程序与罐头响应处理程序结合使用。

创建内存文件服务器

简单 Web 服务器通常用于在本地默认文件系统上提供目录层次结构。例如,命令 jwebserver 提供当前工作目录的文件。虽然这适用于某些用例(例如通过网络共享和浏览文件),但它可能会妨碍其他用例。让我们以 API 存根 为例,其中使用模拟目录结构来模拟预期的响应模式。在这种情况下,最好避开任何文件系统操作,而使用内存文件系统,以避免繁琐的创建和后续删除测试资源。

幸运的是,简单 Web 服务器(更确切地说,是它的文件处理程序)支持非默认文件系统路径 - 唯一的要求是路径的文件系统实现 java.nio.file API。Google 的 Java 内存文件系统 Jimfs 正好满足这一要求,这意味着我们可以使用它创建内存资源并通过简单 Web 服务器提供这些资源。以下是如何做到这一点

package org.example;

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;

/*
 * A Simple Web Server as in-memory server, using Jimfs.
 */
public class SWSJimFS {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);

    /* Creates an in-memory directory hierarchy and starts a Simple Web Server
     * to serve it.
     *
     * Upon receiving a GET request, the server sends a response with a status
     * of 200 if the relative URL matches /some/thing or /some/other/thing.
     * Query parameters are ignored. The body of the response will be a directory
     * listing in html and a Content-type header will be sent with a value of
     * "text/html; charset=UTF-8".
     */
    public static void main( String[] args ) throws Exception {
        Path root = createDirectoryHierarchy();
        var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
        server.start();
    }

    private static Path createDirectoryHierarchy() throws IOException {
        FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
        Path root = fs.getPath("/");

        /* Create directory hierarchy:
         *    |-- root
         *        |-- some
         *            |-- thing
         *            |-- other
         *                |-- thing
         */

        Files.createDirectories(root.resolve("some/thing"));
        Files.createDirectories(root.resolve("some/other/thing"));
        return root;
    }
}

结果是一个简洁的运行时解决方案,没有任何实际的文件系统操作。虽然示例仅为简洁起见创建了一些测试目录,但如果需要,可以使用 Files::write 轻松创建带有内容的模拟文件。

提供 Zip 文件系统

另一个有趣的用例是提供 zip 文件系统的内容。在这种情况下,其根条目的路径将传递到 Simple Web Server。

package org.example;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import com.sun.net.httpserver.SimpleFileServer;

import static java.nio.file.StandardOpenOption.CREATE;

/*
 * A Simple Web Server that serves the contents of a zip file system.
 */
public class SWSZipFS {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);
    static final Path CWD = Path.of(".").toAbsolutePath();

    /* Creates a zip file system and starts a Simple Web Server to serve its
     * contents.
     *
     * Upon receiving a GET request, the server sends a response with a status
     * of 200 if the relative URL matches /someFile.txt, otherwise a 404 response
     * is sent. Query parameters are ignored.
     * The body of the response will be the content of the file "Hello world!"
     * and a Content-type header will be sent with a value of "text/plain".
     */
    public static void main( String[] args ) throws Exception {
        Path root = createZipFileSystem();
        var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, SimpleFileServer.OutputLevel.VERBOSE);
        server.start();
    }

    private static Path createZipFileSystem() throws Exception {
        var path = CWD.resolve("zipFS.zip").toAbsolutePath().normalize();
        var fs = FileSystems.newFileSystem(path, Map.of("create", "true"));
        assert fs != FileSystems.getDefault();
        var root = fs.getPath("/");  // root entry

        /* Create zip file system:
         *    |-- root
         *        |-- aFile.txt
         */

        Files.writeString(root.resolve("someFile.txt"), "Hello world!", CREATE);
        return root;
    }
}

提供 Java 运行时目录

有时,检查远程系统运行时映像中的类文件可能会有所帮助,主要是为了诊断。通过启动一个 Simple Web Server 来轻松实现这一点,该服务器提供给定运行时映像的模块目录。此目录为映像中的每个模块包含一个子目录。要以编程方式访问它,将加载 jrt:/ 文件系统,然后使用它来检查目录并检索运行时可用的类文件。(有关 jrt 文件系统的更多详细信息,请参阅 JEP 220。)

package org.example;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.FileSystems;
import com.sun.net.httpserver.SimpleFileServer;

import static com.sun.net.httpserver.SimpleFileServer.OutputLevel;

public class SWSJRT {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);

    public static void main( String[] args ) {
        var fs = FileSystems.getFileSystem(URI.create("jrt:/"));
        var root = fs.getPath("modules").toAbsolutePath();
        var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
        server.start();
    }
}

然后可以通过网络从另一台计算机请求类和资源文件以进行本地检查。以下是使用 curl 请求 java.base/java/lang/Object.class 文件的示例

$ curl -OL http://<address>:<port>/java.base/java/lang/Object.class

将文件处理程序与罐头响应处理程序结合使用

在了解了 Simple Web Server 的这些不同应用程序后,让我们转向其核心组件文件处理程序,并探讨另一个有趣的用例。具体来说,如果有人想要补充处理程序以支持 GET 和 HEAD 以外的请求方法,该怎么办?

如果接收到 GET 或 HEAD 以外的方法的请求,文件处理程序会生成 501 - Not Implemented405 - Not Allowed。但是,可能需要不同的响应的情况。为此,可以使用 HttpHandlers::handleOrElse 将文件处理程序与条件罐头响应处理程序结合使用

package org.example;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.util.function.Predicate;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpHandlers;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.Request;
import com.sun.net.httpserver.SimpleFileServer;

import static com.sun.net.httpserver.SimpleFileServer.OutputLevel;

/*
 * HttpServer with a handler that combines Simple Web Server's file handler with
 * a canned response handler for DELETE requests.
 */
public class SWSHandlerWithDeleteHandler {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);
    static final Path CWD = Path.of(".").toAbsolutePath();


    /* Creates an HttpServer with a conditional handler that combines two
     * handlers: (1) A file handler for the current working directory and
     * (2) a canned response handler for DELETE requests.
     *
     * If a DELETE request is received, the server sends a 204 response.
     * The body of the response will be empty.
     * All other requests are handled by the file handler, equivalent to the
     * previous example.
     */
    public static void main( String[] args ) throws Exception {
        var fileHandler = SimpleFileServer.createFileHandler(CWD);
        var deleteHandler = HttpHandlers.of(204, Headers.of(), "");
        Predicate<Request> IS_DELETE = r -> r.getRequestMethod().equals("DELETE");
        var handler = HttpHandlers.handleOrElse(IS_DELETE, deleteHandler, fileHandler);
    
        var outputFilter = SimpleFileServer.createOutputFilter(System.out, OutputLevel.VERBOSE);
        var server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler, outputFilter);
        server.start();
    }
}

此处创建了一个 fileHandler 和一个 deleteHandler,具有预设的响应状态(代码 204,没有其他标头,没有正文)。然后将这两个处理程序组合到第三个 handler 中,该处理程序根据请求方法委派传入的请求。如果方法为“DELETE”,则将处理委托给 deleteHandler,否则将委托给 fileHandler。请注意,我们还通过将 Simple Web Server 的输出过滤器添加到服务器实例中来使用它,以便详细记录到 System.out

此机制还可用于根据其他请求状态(如请求 URI 或请求标头)补充或覆盖文件处理程序的行为。因此,HttpHandlers::handleOrElse 是一个强大的 API 点,可以针对特定用例定制处理程序行为。

结论

虽然 jwebserver 工具在很多情况下都足够用了,但 Simple Web Server API 可以帮助解决一些不太常见的情况和极端情况;本文展示了一些此类情况。Simple Web Server 的设计目的是让原型制作、调试和测试变得更容易 - 我们希望最小命令行工具和灵活 API 的结合能够实现这一目标。