CLOVER🍀

That was when it all began.

UndertowのResourceManagerで、静的ファイルを公開する

これは、なにをしたくて書いたもの?

  • Undertowには、ResourceManagerというリソースを扱うインターフェースがある
  • ResourceManagerの実装を使うと、静的ファイルをUndertowで公開できるようだ

というわけで、試してみました。

ResourceManager?

このインターフェースですね。

ResourceManager

ドキュメント上でも、少しだけ登場します。

Built in Handlers / Resource

The resource handler is used to serve static resources such as files. This handler takes a ResourceManager instance, that is basically a file system abstraction. Undertow provides file system and class path based resource mangers, as well as a caching resource manager that wraps an existing resource manager to provide in memory caching support.

Handlerとしては、ResourceHandlerを使います。

Resource Handler

ResourceManagerには実装がいくつかあり、

  • 指定したjava.io.Fileを起点に、ファイルシステムからリソースを取得するもの
  • 指定したjava.nio.file.Pathを起点に、ファイルシステムからリソースを取得するもの
  • クラスパスからリソースを取得するもの
  • ResourceManagerをラップし、キャッシュの機能を提供するもの

といったものが存在します。

お題

今回は、簡単なServlet+ResourceManager(ResourceHandler)を使って、動的コンテンツの公開と静的コンテンツの公開の
両方をやってみましょう。

環境

今回の環境は、こちらです。

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-0ubuntu0.18.04.1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

$ mvn -version
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T03:33:14+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-34-generic", arch: "amd64", family: "unix"

準備

Maven依存関係は、こちら。

        <dependency>
          <groupId>io.undertow</groupId>
          <artifactId>undertow-servlet</artifactId>
          <version>2.0.13.Final</version>
        </dependency>

undertow-servletのみでいきます。

あと、静的ファイルは用意するのが面倒だったので、Bootstrapをダウンロードしてきて展開。

Download · Bootstrap

JavaScritやCSSは、Bootstrapのzipを展開後、ディレクトリをひとつ上に移しています。

$ find src/main/resources -type f | perl -wp -e 's!(.+)/(.+)!$1    $2!' | sort
src/main/resources/static    bootstrap-4.1.3-dist.zip
src/main/resources/static    index.html
src/main/resources/static/css    .DS_Store
src/main/resources/static/css    bootstrap-grid.css
src/main/resources/static/css    bootstrap-grid.css.map
src/main/resources/static/css    bootstrap-grid.min.css
src/main/resources/static/css    bootstrap-grid.min.css.map
src/main/resources/static/css    bootstrap-reboot.css
src/main/resources/static/css    bootstrap-reboot.css.map
src/main/resources/static/css    bootstrap-reboot.min.css
src/main/resources/static/css    bootstrap-reboot.min.css.map
src/main/resources/static/css    bootstrap.css
src/main/resources/static/css    bootstrap.css.map
src/main/resources/static/css    bootstrap.min.css
src/main/resources/static/css    bootstrap.min.css.map
src/main/resources/static/js    bootstrap.bundle.js
src/main/resources/static/js    bootstrap.bundle.js.map
src/main/resources/static/js    bootstrap.bundle.min.js
src/main/resources/static/js    bootstrap.bundle.min.js.map
src/main/resources/static/js    bootstrap.js
src/main/resources/static/js    bootstrap.js.map
src/main/resources/static/js    bootstrap.min.js
src/main/resources/static/js    bootstrap.min.js.map

index.htmlは、Getting startedをマネて。

Introduction / Starter template

Bootstrapが提供するJavaScriptCSSは、ローカルファイルを参照するように記述しています。 src/main/resources/static/index.html

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="/static/css/bootstrap.min.css">

    <title>Hello, world!</title>
  </head>
  <body>
    <h1>Hello, world!</h1>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="/static/js/bootstrap.min.js"></script>
  </body>
</html>

Servletを用意

動的コンテンツの配信役として、簡単なServletを作成します。 src/main/java/org/littlewings/undertow/HelloServlet.java

package org.littlewings.undertow;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.getWriter().write("Hello Undertow Servlet!!");
    }
}

本当に、至ってシンプル。

Undertowを起動する

では、このServletとResourceManagerを使って、Undertowを使った起動クラスを作成していきます。

結果は、こちら。 src/main/java/org/littlewings/undertow/Server.java

package org.littlewings.undertow;

import java.nio.file.Paths;
import javax.servlet.ServletException;

import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.resource.ClassPathResourceManager;
import io.undertow.server.handlers.resource.PathResourceManager;
import io.undertow.server.handlers.resource.ResourceManager;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;

public class Server {
    public static void main(String... args) throws ServletException {
        DeploymentInfo deploymentInfo =
                Servlets
                        .deployment()
                        .setClassLoader(Server.class.getClassLoader())
                        .setContextPath("/")
                        .setDeploymentName("app")
                        .addServlets(
                                Servlets.servlet(HelloServlet.class).addMapping("/servlet/hello")
                        );

        DeploymentManager deploymentManager = Servlets.defaultContainer().addDeployment(deploymentInfo);
        deploymentManager.deploy();

        ResourceManager resourceManager = new PathResourceManager(Paths.get("src/main/resources/static"));

        HttpHandler handler =
                Handlers
                        .path(Handlers.redirect("/static"))
                        .addPrefixPath("/static", Handlers.resource(resourceManager))
                        .addPrefixPath("/app", deploymentManager.start());

        Undertow server =
                Undertow
                        .builder()
                        .addHttpListener(8080, "localhost")
                        .setHandler(handler)
                        .build();

        server.start();
    }
}

Servletの登録と、デプロイ。

        DeploymentInfo deploymentInfo =
                Servlets
                        .deployment()
                        .setClassLoader(Server.class.getClassLoader())
                        .setContextPath("/")
                        .setDeploymentName("app")
                        .addServlets(
                                Servlets.servlet(HelloServlet.class).addMapping("/servlet/hello")
                        );

        DeploymentManager deploymentManager = Servlets.defaultContainer().addDeployment(deploymentInfo);
        deploymentManager.deploy();

静的ファイルの取得。今回は、java.nio.file.Pathを起点に書いています。

        ResourceManager resourceManager = new PathResourceManager(Paths.get("src/main/resources/static"));

で、これらを使ってHttpHandlerを作成。

        HttpHandler handler =
                Handlers
                        .path(Handlers.redirect("/static"))
                        .addPrefixPath("/static", Handlers.resource(resourceManager))
                        .addPrefixPath("/app", deploymentManager.start());

Handlers.pathではデフォルトのHttpHandlerを「/」以下に割り当てるのですが、これは「/static」へリダイレクトするように設定。

静的コンテンツは「/static」配下、Servletは「/app」配下というパスの割り当てで公開します。

                        .addPrefixPath("/static", Handlers.resource(resourceManager))
                        .addPrefixPath("/app", deploymentManager.start());

あとは、このHttpHandlerを使ってUndertowを起動。

        Undertow server =
                Undertow
                        .builder()
                        .addHttpListener(8080, "localhost")
                        .setHandler(handler)
                        .build();

        server.start();

確認。

Servlet

$ curl -i localhost:8080/app/servlet/hello
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 24
Date: Sat, 15 Sep 2018 08:26:47 GMT

Hello Undertow Servlet!!

静的コンテンツ。

$ curl -i localhost:8080/static/index.html
HTTP/1.1 200 OK
Connection: keep-alive
Last-Modified: Sat, 15 Sep 2018 08:26:45 GMT
Content-Length: 928
Content-Type: text/html
Accept-Ranges: bytes
Date: Sat, 15 Sep 2018 08:27:11 GMT

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="/static/css/bootstrap.min.css">

    <title>Hello, world!</title>
  </head>
  <body>
    <h1>Hello, world!</h1>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="/static/js/bootstrap.min.js"></script>
  </body>
</html>


$ curl -i -I localhost:8080/static/js/bootstrap.min.js
HTTP/1.1 200 OK
Connection: keep-alive
Last-Modified: Tue, 24 Jul 2018 01:37:42 GMT
Content-Length: 51039
Content-Type: application/javascript
Accept-Ranges: bytes
Date: Sat, 15 Sep 2018 08:28:14 GMT


$ curl -i -I localhost:8080/static/css/bootstrap.min.css
HTTP/1.1 200 OK
Connection: keep-alive
Last-Modified: Tue, 24 Jul 2018 01:37:28 GMT
Content-Length: 140936
Content-Type: text/css
Accept-Ranges: bytes
Date: Sat, 15 Sep 2018 08:28:36 GMT

JavaScriptCSSは結果が長いので、HEADだけ。

動作していますね。

今回はjava.nio.file.Pathを起点にしたResourceManagerを使いましたが、クラスパスから取得する場合は、こんな感じ。

        ResourceManager resourceManager = new ClassPathResourceManager(Server.class.getClassLoader(), "static");

クラスパス上の、あるパスを起点にリソースを取得します。

実行結果は、先ほどと同じなので割愛。

やりたいことは確認できたので、OKです。

DeploymentInfoにResourceManagerを設定する

今回は、HttpHandlerとして個別にResourceHandlerを設定しましたが、ResourceManagerをDeploymentInfoに
含めて設定することも可能なようです。

        DeploymentInfo deploymentInfo =
                Servlets
                        .deployment()
                        .setClassLoader(Server.class.getClassLoader())
                        .setContextPath("/")
                        .setDeploymentName("app")
                        .setResourceManager(new PathResourceManager(Paths.get("src/main/resources/static")))
                        .addServlets(
                                Servlets.servlet(HelloServlet.class).addMapping("/servlet/hello")
                        );

Weldのドキュメントを見ていて、気づきました…。

Servlet containers / Undertow

Content-Type?

ResourceManagerを使うにあたり、Content-Typeがちょっと気になるところだったのですが、割としっかり入って
いました。

これ、どうなっているのかなーと思ったのですが、デフォルトである程度登録されているようです。

https://github.com/undertow-io/undertow/blob/2.0.13.Final/core/src/main/java/io/undertow/util/MimeMappings.java#L37-L150