CLOVER🍀

That was when it all began.

組み込みJettyで遊ぼう

最近、ちょっと組み込みのJettyを触っている時間が増えたので、ちょっとまとめておこうかなと思います。

Jettyを組み込みの形で使って、以下のことを試してみたいと思います。

利用するJettyのバージョンは「9.4.8.v20171121」とし、参考にするドキュメントおよびリソースはこちらを。

https://www.eclipse.org/jetty/documentation/9.4.8.v20171121/embedded-examples.html

https://github.com/jetty-project/embedded-jetty-jsp/blob/master/src/main/java/org/eclipse/jetty/demo/Main.java

確認はテストコードで行うので、各プロジェクトのMaven依存関係にはいつも以下が入っているものとします。

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.0.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.9.0</version>
            <scope>test</scope>
        </dependency>

Servletを使う

まずは、単純にServletだけを使う方法。

Embedded Examples / Minimal Servlet

必要なMaven依存関係は、こちら。

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>

簡単なServletを用意しましょう。
src/test/java/org/littlewings/embedded/jetty/HelloServlet.java

package org.littlewings.embedded.jetty;

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
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.getWriter().write("Hello Servlet!!");
    }
}

Jettyを使った確認コードは、こちら。
src/test/java/org/littlewings/embedded/jetty/SimpleJettyServletTest.java

package org.littlewings.embedded.jetty;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SimpleJettyServletTest {
    @Test
    public void test() throws Exception {
        Server server = new Server(8080);

        ServletHandler handler = new ServletHandler();
        handler.addServletWithMapping(HelloServlet.class, "/hello");

        server.setHandler(handler);

        server.start();

        HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080/hello").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Servlet!!");
        }
        conn.disconnect();

        server.stop();
    }
}

まず、Serverのインスタンスを作成。この時、リッスンポートを指定します。

        Server server = new Server(8080);

ServletHandlerを生成して、マッピングするURLとともにSerlvetのクラスを登録し、Serverへ設定します。

        ServletHandler handler = new ServletHandler();
        handler.addServletWithMapping(HelloServlet.class, "/hello");

        server.setHandler(handler);

あとは、Server#start、stopで起動/停止です。

        server.start();

        /////

        server.stop();

簡単ですね。

確認。

        HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080/hello").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Servlet!!");
        }
        conn.disconnect();

これで、Servletが動かせましたよ、と。

Webアプリケーション(Servletアノテーション / web.xmlレス)を使う

続いて、Webアプリケーション(アノテーション駆動のServletのみ)を動かしてみます。

必要なMaven依存関係は、こちら。

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-annotations</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>

「jetty-webapp」は、「jetty-annotations」に実は含まれていたりするのですが…。

@WebServletアノテーションを付与したServletを作成します。
src/test/java/org/littlewings/embedded/jetty/HelloServlet.java

package org.littlewings.embedded.jetty;

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

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

Webアプリケーションのルートディレクトリが必要なので、今回は「src/test/webapp」とします。

$ mkdir -p src/test/webapp/WEB-INF

@WebServletなServletを認識させるには、クラスファイルをWEB-INF/classesに配置する必要があるようなので、コンパイル結果を
「src/test/webapp/WEB-INF/classes」に出力するように設定しておきます。

    <build>
        <testOutputDirectory>src/test/webapp/WEB-INF/classes</testOutputDirectory>
    </build>

なんとなく、テキストファイルも置いてみます。
src/test/webapp/text-file.txt

Hello Jetty!!


src/test/java/org/littlewings/embedded/jetty/SimpleJettyWebappTest.java

package org.littlewings.embedded.jetty;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SimpleJettyWebappTest {
    @Test
    public void test() throws Exception {
        Server server = new Server(8080);

        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setWar("src/test/webapp");

        webapp.setConfigurations(new Configuration[]{
                new WebXmlConfiguration(),
                new AnnotationConfiguration(),
                new WebInfConfiguration()
        });

        server.setHandler(webapp);

        server.start();

        HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080/hello").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Servlet!!");
        }
        conn.disconnect();

        HttpURLConnection connPlainText = (HttpURLConnection) URI.create("http://localhost:8080/text-file.txt").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connPlainText.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Jetty!!");
        }
        connPlainText.disconnect();

        server.stop();
    }
}

先ほどの最小構成のServletの時から変わったことは、WebAppContextを使用するようになったことですね。

        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setWar("src/test/webapp");

        webapp.setConfigurations(new Configuration[]{
                new WebXmlConfiguration(),
                new AnnotationConfiguration(),
                new WebInfConfiguration()
        });

        server.setHandler(webapp);

コンテキストパス、WARの位置(今回はディレクトリですが)を指定して、Server#setHandlerで登録します。

あと、Servletを認識させるには、このあたりの設定が必要です。

        webapp.setConfigurations(new Configuration[]{
                new WebXmlConfiguration(),
                new AnnotationConfiguration(),
                new WebInfConfiguration()
        });

確認。

        HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080/hello").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Servlet!!");
        }
        conn.disconnect();

        HttpURLConnection connPlainText = (HttpURLConnection) URI.create("http://localhost:8080/text-file.txt").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connPlainText.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Jetty!!");
        }
        connPlainText.disconnect();

動いていますよ、と。

Webアプリケーション(Servlet+web.xml)を使う

今度は、web.xmlを使ってちょっとレガシー気味なWebアプリケーション…Servletのみですが…を書いてみます。

Maven依存関係は、「jetty-webapp」のみがあればOKです。

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>

また、Webアプリケーションのルートディレクトリは先ほどと変更しませんが、「WEB-INF/classes」へのコンパイル結果出力設定は不要です。

Servletは、シンプルなものに戻します。
src/test/java/org/littlewings/embedded/jetty/HelloServlet.java

package org.littlewings.embedded.jetty;

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
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.getWriter().write("Hello Servlet!!");
    }
}

これをマッピングするweb.xmlを作成。
src/test/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>org.littlewings.embedded.jetty.HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

テキストファイルも置いていますが、内容一緒なので省略…。

src/test/java/org/littlewings/embedded/jetty/SimpleJettyWebappLegacyTest.java

package org.littlewings.embedded.jetty;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SimpleJettyWebappLegacyTest {
    @Test
    public void test() throws Exception {
        Server server = new Server(8080);

        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setWar("src/test/webapp");

        // webapp.addServlet(HelloServlet.class, "/hello");

        server.setHandler(webapp);

        server.start();

        HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080/hello").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Servlet!!");
        }
        conn.disconnect();

        HttpURLConnection connPlainText = (HttpURLConnection) URI.create("http://localhost:8080/text-file.txt").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connPlainText.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello Jetty!!");
        }
        connPlainText.disconnect();

        server.stop();
    }
}

今回は、グッとシンプルになります。Configurationは不要です。

        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setWar("src/test/webapp");

        // webapp.addServlet(HelloServlet.class, "/hello");

        server.setHandler(webapp);

web.xmlServletマッピングを書かない場合は、コメントアウトしてある箇所のようにServletを個別にWebAppContext#addServletで登録すると良いでしょう。

確認結果は同じなので、割愛。

JSPを使う

JSPを使ってみます。

参考にしたドキュメントは、こちら。これは、WARファイルをデプロイする例ではありますが…。
Embedded Examples / Web Application with JSP

必要なMaven依存関係は、こちら。

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-annotations</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>apache-jsp</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>

単純なJSP
src/test/webapp/hello.jsp

Hello JSP!!

EL式を使ったJSPを用意してみます。
src/test/webapp/el.jsp

${5 + 3}

WEB-INFディレクトリは用意しません。

コードは、こちら。
src/test/java/org/littlewings/embedded/jetty/SimpleJettyJspTest.java

package org.littlewings.embedded.jetty;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.plus.webapp.EnvConfiguration;
import org.eclipse.jetty.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.FragmentConfiguration;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SimpleJettyJspTest {
    @Test
    public void test() throws Exception {
        Server server = new Server(8080);

        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setWar("src/test/webapp");

        webapp.setConfigurations(new Configuration[] {
                new WebXmlConfiguration(),
                new AnnotationConfiguration(),
                new WebInfConfiguration()
        });

        server.setHandler(webapp);

        server.start();

        HttpURLConnection connJsp = (HttpURLConnection) URI.create("http://localhost:8080/hello.jsp").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connJsp.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello JSP!!");
        }
        connJsp.disconnect();

        HttpURLConnection connEl = (HttpURLConnection) URI.create("http://localhost:8080/el.jsp").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connEl.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("8");
        }
        connEl.disconnect();

        server.stop();
    }
}

Jettyの設定は、web.xmlを省略した場合のコードと同じですね。

このあたりの設定を削ると、今回の構成ではJSPが動かなくなります…。

        webapp.setConfigurations(new Configuration[] {
                new WebXmlConfiguration(),
                new AnnotationConfiguration(),
                new WebInfConfiguration()
        });

別解としては、これでも動くようですが。

        webapp.setConfigurations(new Configuration[] {
                new AnnotationConfiguration(),
                new WebInfConfiguration()
        });

        ServletHolder jspHolder = new ServletHolder("jsp", JettyJspServlet.class);
        webapp.addServlet(jspHolder, "*.jsp");

また、JSPServletコンパイルされるわけですが、その時に使うディレクトリはデフォルトでは「java.io.tmpdir」配下が使われます。

こんな感じに。

$ ll -d /tmp/jetty*
drwxrwxr-x 3 xxxxx xxxxx 4096  217 19:30 /tmp/jetty-0.0.0.0-8080-webapp-_-any-2499022055629159289.dir/
drwxrwxr-x 3 xxxxx xxxxx 4096  217 19:09 /tmp/jetty-0.0.0.0-8080-webapp-_-any-4228262766737794196.dir/
drwxrwxr-x 3 xxxxx xxxxx 4096  217 19:05 /tmp/jetty-0.0.0.0-8080-webapp-_-any-456663689543055411.dir/
drwxrwxr-x 3 xxxxx xxxxx 4096  217 19:29 /tmp/jetty-0.0.0.0-8080-webapp-_-any-7594933357262438255.dir/

このディレクトリは、Server#stop時には削除されます。

ディレクトリを変更する場合は、以下のように設定すればOKです。

        webapp.setAttribute("javax.servlet.context.tempdir", new File("..."));

確認。

        HttpURLConnection connJsp = (HttpURLConnection) URI.create("http://localhost:8080/hello.jsp").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connJsp.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello JSP!!");
        }
        connJsp.disconnect();

        HttpURLConnection connEl = (HttpURLConnection) URI.create("http://localhost:8080/el.jsp").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connEl.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("8");
        }
        connEl.disconnect();

JSP、EL式も含めて動きましたよ、と。

JSTLを使う

最後に、JSTLを使ってみましょう。

必要なMaven依存関係は、こちらになります。

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-annotations</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>apache-jsp</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>apache-jstl</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>

簡単なJSTLのカスタムタグを使ったJSPを用意。
src/test/webapp/jstl.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><c:out value="Hello JSTL!!"/>

Java側のコードは、JSPを使う時と変わりません。
src/test/java/org/littlewings/embedded/jetty/SimpleJettyJstlTest.java

package org.littlewings.embedded.jetty;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.plus.webapp.EnvConfiguration;
import org.eclipse.jetty.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.FragmentConfiguration;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SimpleJettyJstlTest {
    @Test
    public void test() throws Exception {
        Server server = new Server(8080);

        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setWar("src/test/webapp");

        webapp.setConfigurations(new Configuration[] {
                new WebXmlConfiguration(),
                new AnnotationConfiguration(),
                new WebInfConfiguration()
        });

        server.setHandler(webapp);

        server.start();

        HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080/jstl.jsp").toURL().openConnection();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            assertThat(reader.readLine()).isEqualTo("Hello JSTL!!");
        }
        conn.disconnect();

        server.stop();
    }
}

実行時に、JSTLURIが解決できないとかいうトラブルに遭った場合は、次のような調性を行うことになります。

        // Set Classloader of Context to be sane (needed for JSTL)
        // JSP requires a non-System classloader, this simply wraps the
        // embedded System classloader in a way that makes it suitable
        // for JSP to use
        ClassLoader jspClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
        webapp.setClassLoader(jspClassLoader);

        // Set the ContainerIncludeJarPattern so that jetty examines these
        // container-path jars for tlds, web-fragments etc.
        // If you omit the jar that contains the jstl .tlds, the jsp engine will
        // scan for them instead.
        webapp.setAttribute(
                "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
                ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$" );

参考)
Embedded Examples / Web Application with JSP
https://github.com/jetty-project/embedded-jetty-jsp/blob/master/src/main/java/org/eclipse/jetty/demo/Main.java#L194-L199

とりあえず、こんなところで。