Working with the Simple Web Server

原文はこちら。
The original article was written by Julia Boes (Java Core Libraries Engineer, Oracle).
https://inside.java/2021/12/06/working-with-the-simple-web-server/

Simple Web Server は、JDK 18 でjdk.httpserverモジュールに追加されました。これは最小限の静的ファイルのHTTPサーバーで、プロトタイピング、テスト、およびデバッグ用途を目的として設計されています。この記事では、Simple Web Serverの、あまり目立たないが興味深いプログラムのアプリケーションを紹介します。

JEP 408: Simple Web Server
https://openjdk.java.net/jeps/408

Introduction

Simple Web Serverは、コマンドラインからjwebserverで実行します。

The jwebserver Command
https://download.java.net/java/early_access/jdk18/docs/specs/man/jwebserver.html

これは、HTTP/1.1で単一のディレクトリ階層にある静的なファイルを提供します。動的コンテンツと他のHTTPバージョンはサポート対象外です。コマンドラインツールに加えて、Simple Web Serverは、サーバとそのコンポーネントをプログラムで作成およびカスタマイズするための API を提供します。この APIは、2006年からJDKに含まれ、公式にサポートされているcom.sun.net.httpserverパッケージを拡張したものです。

Class SimpleFileServer
https://download.java.net/java/early_access/jdk18/docs/api/jdk.httpserver/com/sun/net/httpserver/SimpleFileServer.html

この記事では、Simple Web Server APIに焦点を当て、jwebserverツールの一般的な使用法を超えて、サーバーとそのコンポーネントを操作するいくつかの方法について説明します。特に、以下のようなアプリケーションを検証しています。

  • インメモリファイルサーバの作成
  • ZIPファイルシステムの提供
  • JRTのディレクトリを提供
  • ファイルハンドラと定型応答ハンドラの組み合わせ

Creating an In-Memory File Server

Simple Web Serverを使って、ローカルのデフォルトファイルシステム上のディレクトリ階層を提供できます。例えば、jwebserverコマンドで、現在の作業ディレクトリのファイルを提供します。これは例えばネットワーク越しのファイルの共有や閲覧用途には適していますが、他の使用例では邪魔になることがあります。例えば、APIのスタブ(模擬ディレクトリ構造)を使って期待される応答パターンをシミュレートする場合を考えてみましょう。

Stubbing
http://wiremock.org/docs/stubbing/

この場合、ファイルシステムの操作を避け、代わりにインメモリファイルシステムで作業するのが、テストリソースの面倒な作成とその後の削除を避ける上で都合がよい可能性があります。

幸運にも、Simple Web Server、より正確にはSimple Web Serverのファイルハンドラが、デフォルトではないファイルシステムパスをサポートしています。その唯一の要件は、パスのファイルシステムがjava.nio.file APIを実装することです。

java.nio.file
https://download.java.net/java/early_access/jdk18/docs/api/java.base/java/nio/file/package-summary.html

Google による Java のインメモリファイルシステム Jimfs はまさにこれを実装しています。つまり、これを使用してインメモリリソースを作成し、Simple Web Server で提供できるのです。以下はその作成例です。

Jimfs
https://github.com/google/jimfs

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を使ってコンテンツのあるモックファイルを簡単に作成できます。

Serving a Zip File System

もう一つの興味深いユースケースは、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;
    }
}

Serving a Java Runtime Directory

主に診断目的で、リモートシステムのランタイムイメージのクラスファイルを検査することが有用な場合があります。これは、指定されたランタイムイメージのモジュールディレクトリを提供するシンプルな Webサーバーを起動することで簡単に実現できます。このディレクトリには、イメージ内の各モジュールに対応する1つのサブディレクトリが含まれています。プログラム的にアクセスするために、jrt:/ ファイルシステムがロードされ、次にディレクトリを検査してランタイムに利用可能なクラスファイルを取得するために使用されます(jrtファイルシステムの詳細については、JEP 220を参照してください)。

JEP 220: Modular Run-Time Images
https://openjdk.java.net/jeps/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

Combining a File Handler With a Canned Response Handler

Simple Web Server のさまざまな応用例を見てきましたが、次はその中核となるファイルハンドラに目を向けて、もう一つ興味深い使用例を考えてみましょう。具体的には、ハンドラを補完してGETHEAD以外のリクエストメソッドをサポートしたい場合はどうすればいいのでしょうか。

GETHEAD以外のメソッドのリクエストを受信した場合、ファイルハンドラは 501 (Not Implemented) または 405 (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();
    }
}

この例では、あらかじめレスポンスの状態(HTTP status 204、追加ヘッダーなし、ボディなし)を設定済みのfileHandlerdeleteHandlerを作成しています。この両ハンドラは3番目のハンドラに結合され、 リクエストメソッドに基づき受信したリクエストを処理します。メソッドがDELETEの場合はdeleteHandlerに、そうでない場合はfileHandlerに処理を委譲しています。また、Simple Web Server の出力フィルターを使っている点にも注意してください。この例ではサーバーインスタンスにフィルターを追加してSystem.out に詳細なログを記録するようにしています。

この機構を使って、リクエスト URI やリクエストヘッダなどの他のリクエスト状態に基づいてファイルハンドラの動作を補完したりオーバーライドしたりもできます。このようにHttpHandlers::handleOrElseは、特定のユースケースに合わせてハンドラの動作を調整するための強力な API ポイントです。

Conclusion

多くの場合、jwebserverツールで十分ですが、Simple Web Server APIはあまり一般的でないシナリオや稀なケースに対して有用です。この記事ではそのいくつかをご紹介しました。Simple Web Serverはプロトタイピング、デバッグ、テストを簡単にするために設計されました。最小限のコマンドラインツールと柔軟なAPIの組み合わせにより、この目標を達成したと考えています。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください