Jakarta Servlet 5.0 のFilterを使ってみる

f:id:bau1537:20210925143925j:plain

  • フィルターについて調べてみます。
  • ざっくりとフィルターとは何かというと
    • Servlet にリクエストが到達する、またはレスポンスがクライアントに返される際に、呼び出されるメソッド
    • 引数で渡される HttpServletRequest と HttpServletResponse を変更することができる
    • 引数で渡される FilterChain を呼び出す、呼び出さないを切り替えることで後続のフィルーターまたはサーブレットに処理をさせるかを決定できる
    • どのURLパターンに対して適用するかを決定できる
    • 複数のフィルターをつなぎ合わせて構成できる
  • ざっくりと例を絵にするとこんな感じになるかな?
Client
   │
   │
   ▼
Filter 1  doFilter()
   │
   │
   ▼
Filter 2  doFilter()
   │
   │
   ▼
Filter N  doFilter()
   │
   │
   ▼
Servlet doGet()

環境

  • プロジェクト構成
.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── org
        │       └── example
        │           ├── filter
        │           │   ├── DemoFilter1.java
        │           │   └── DemoFilter2.java
        │           └── servlet
        │               └── DemoServlet.java
        └── webapp
            └── WEB-INF
                └── web.xml

9 directories, 5 files
  • pom
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>servlet</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>Servlet Maven Webapp</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
$ java -jar $JETTY_HOME/start.jar --version

Jetty Server Classpath:
-----------------------
Version Information on 22 entries in the classpath.
Note: order presented here is how they would appear on the classpath.
      changes to the --module=name command line options will be reflected here.
 0:                    (dir) | ${jetty.base}/resources
 1:             2.0.0-alpha1 | ${jetty.home}/lib/logging/slf4j-api-2.0.0-alpha1.jar
 2:                   11.0.6 | ${jetty.home}/lib/logging/jetty-slf4j-impl-11.0.6.jar
 3:                    5.0.2 | ${jetty.home}/lib/jetty-jakarta-servlet-api-5.0.2.jar
 4:                   11.0.6 | ${jetty.home}/lib/jetty-http-11.0.6.jar
 5:                   11.0.6 | ${jetty.home}/lib/jetty-server-11.0.6.jar
 6:                   11.0.6 | ${jetty.home}/lib/jetty-xml-11.0.6.jar
 7:                   11.0.6 | ${jetty.home}/lib/jetty-util-11.0.6.jar
 8:                   11.0.6 | ${jetty.home}/lib/jetty-io-11.0.6.jar
 9:                   11.0.6 | ${jetty.home}/lib/jetty-jndi-11.0.6.jar
10:                   11.0.6 | ${jetty.home}/lib/jetty-security-11.0.6.jar
11:                   11.0.6 | ${jetty.home}/lib/jetty-servlet-11.0.6.jar
12:                   11.0.6 | ${jetty.home}/lib/jetty-webapp-11.0.6.jar
13:                   11.0.6 | ${jetty.home}/lib/jetty-plus-11.0.6.jar
14:                    2.0.0 | ${jetty.home}/lib/jakarta.transaction-api-2.0.0.jar
15:                   11.0.6 | ${jetty.home}/lib/jetty-annotations-11.0.6.jar
16:                      9.1 | ${jetty.home}/lib/annotations/asm-9.1.jar
17:                      9.1 | ${jetty.home}/lib/annotations/asm-analysis-9.1.jar
18:                      9.1 | ${jetty.home}/lib/annotations/asm-commons-9.1.jar
19:                      9.1 | ${jetty.home}/lib/annotations/asm-tree-9.1.jar
20:                    2.0.0 | ${jetty.home}/lib/annotations/jakarta.annotation-api-2.0.0.jar
21:                   11.0.6 | ${jetty.home}/lib/jetty-deploy-11.0.6.jar

動かしてみる

package org.example.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;
import java.io.PrintWriter;

// アノテーションでフィルターであることと、名前を定義
@WebFilter(filterName = "demo-filter-1")
// jakarta.servlet.Filterをimplementsする決まり
public class DemoFilter1 implements Filter {

    // フィルターの初期化時に呼ばれる
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
        System.out.println("DemoFilter init");
    }

    // フィルターの破棄時に呼ばれる
    @Override
    public void destroy() {
        Filter.super.destroy();
        System.out.println("DemoFilter destroy");
    }

    // フィルター処理
    // アクセスログを出力し、 /filter にリクエストが来ている場合は chain を呼び出さずリクエストを返す
    // URLパターンで指定されたものに一致するリクエストが来るとServletよりも前に呼び出される
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        StringBuffer requestURL = ((HttpServletRequest) request).getRequestURL();

        // アクセスログを出力する
        System.out.println("filter 1");
        System.out.println("request url : " + requestURL);

        if (requestURL.toString().endsWith("/filter")) {
            // レスポンスの中身を書き込み、クライアントへ返す
            writeResponse(response);
        } else {
            // 後続のフィルターまたはServletに処理を委譲する
            chain.doFilter(request, response);
        }
    }

    private void writeResponse(ServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        writer.write("Hello from Jakarta Servlet 5 Filter 1");
    }

}
  • 同じ内容で、HTTPレスポンスの内容と、ログの出力内容をちょっと変えたフィルターをもう一つ作る
package org.example.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;
import java.io.PrintWriter;

@WebFilter(filterName = "demo-filter-2")
public class DemoFilter2 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
        System.out.println("DemoFilter init");
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
        System.out.println("DemoFilter destroy");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        StringBuffer requestURL = ((HttpServletRequest) request).getRequestURL();

        System.out.println("filter 2");
        System.out.println("request url : " + requestURL);

        if (requestURL.toString().endsWith("/filter")) {
            writeResponse(response);
        } else {
            chain.doFilter(request, response);
        }
    }

    private void writeResponse(ServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        writer.write("Hello from Jakarta Servlet 5 Filter 2");
    }

}
<?xml version="1.0" encoding="UTF-8" ?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee 
         https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <filter-mapping>
        <filter-name>demo-filter-1</filter-name>
        <!--suppress WebProperties -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>demo-filter-2</filter-name>
        <!--suppress WebProperties -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
  • デプロイしてみる
    • 初期化のログが出てくる
    • 今回フィルターを2つ用意しているので、二行ログが出てくる
DemoFilter init
DemoFilter init
  • リクエストを飛ばす
    • サーブレットまでリクエストが届いた
    • /demo へのリクエストはFilterでchainを呼び出すようにしているので、こうなる
$ curl http://localhost:8080/servlet-1.0-SNAPSHOT/demo
hello from Jakarta Servlet 5 Servlet : service
  • ログはこう出る
    • web.xmlで定義した順に処理されているのがわかる
filter 1
request url : http://localhost:8080/servlet-1.0-SNAPSHOT/demo
filter 2
request url : http://localhost:8080/servlet-1.0-SNAPSHOT/demo
  • 別のリクエストを飛ばす
    • demo-filter-1 がレスポンスを返してくる
    • /filter へのリクエストはFilterでレスポンスを返し、後続に処理を委譲しないようにしている
    • 今回の場合、最初に実行されるフィルターは demo-filter-1 なので、こうなる
$ curl http://localhost:8080/servlet-1.0-SNAPSHOT/demo/filter
Hello from Jakarta Servlet 5 Filter 1
  • ログにはこう出る
filter 1
request url : http://localhost:8080/servlet-1.0-SNAPSHOT/demo/filter

例外を投げてみる

  • Filterで例外が発生するとどうなるんでしょうか
  • init で例外を投げてみます
    • ServletException を投げます
@WebFilter(filterName = "demo-filter-1")
public class DemoFilter1 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
        System.out.println("DemoFilter init");

        throw new ServletException();
    }
  • ログはこう
    • 初期化ログが2つ出ているので、一つのフィルターが死んでもその他のフィルターは初期化される模様
jakarta.servlet.ServletException
    at org.example.filter.DemoFilter1.init(DemoFilter1.java:18)
    at org.eclipse.jetty.servlet.FilterHolder.initialize(FilterHolder.java:133)
    at org.eclipse.jetty.servlet.ServletHandler.lambda$initialize$2(ServletHandler.java:690)
    at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
    at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
    at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
    at org.eclipse.jetty.servlet.ServletHandler.initialize(ServletHandler.java:714)
    at org.eclipse.jetty.servlet.ServletContextHandler.startContext(ServletContextHandler.java:392)
    at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1305)
    at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:891)
    at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:306)
    at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:533)
    at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
    at org.eclipse.jetty.deploy.bindings.StandardStarter.processBinding(StandardStarter.java:40)
    at org.eclipse.jetty.deploy.AppLifeCycle.runBindings(AppLifeCycle.java:183)
    at org.eclipse.jetty.deploy.DeploymentManager.requestAppGoal(DeploymentManager.java:516)
    at org.eclipse.jetty.deploy.DeploymentManager.addApp(DeploymentManager.java:151)
    at org.eclipse.jetty.deploy.providers.ScanningAppProvider.fileChanged(ScanningAppProvider.java:203)
    at org.eclipse.jetty.deploy.providers.WebAppProvider.fileChanged(WebAppProvider.java:398)
    at org.eclipse.jetty.deploy.providers.ScanningAppProvider$1.fileChanged(ScanningAppProvider.java:64)
    at org.eclipse.jetty.util.Scanner$DiscreteListener.pathChanged(Scanner.java:271)
    at org.eclipse.jetty.util.Scanner.reportChange(Scanner.java:881)
    at org.eclipse.jetty.util.Scanner.reportDifferences(Scanner.java:805)
    at org.eclipse.jetty.util.Scanner.scan(Scanner.java:709)
    at org.eclipse.jetty.util.Scanner$ScanTask.run(Scanner.java:145)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
    at java.base/java.lang.Thread.run(Thread.java:831)
DemoFilter init
DemoFilter init
  • リクエストを /demo へ投げると HTTP Status Code は 503 が返ってきました
    • つまり、フィルターが死ぬとどうやらアプリ全体死ぬ感じですかね?
$ curl http://localhost:8080/servlet-1.0-SNAPSHOT/demo
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 503 Service Unavailable</title>
</head>
<body><h2>HTTP ERROR 503 Service Unavailable</h2>
<table>
<tr><th>URI:</th><td>/servlet-1.0-SNAPSHOT/demo</td></tr>
<tr><th>STATUS:</th><td>503</td></tr>
<tr><th>MESSAGE:</th><td>Service Unavailable</td></tr>
<tr><th>SERVLET:</th><td>-</td></tr>
</table>
<hr><a href="https://eclipse.org/jetty">Powered by Jetty:// 11.0.6</a><hr/>

</body>
</html>
  • フィルターが対象とするURLパターン以外だったらリクエスト届くのか?と思い、試してみる
  • web.xmlを書き換え
    • フィルターは /filter のURLのみ処理するようにする
<?xml version="1.0" encoding="UTF-8" ?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee 
         https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <filter-mapping>
        <filter-name>demo-filter-1</filter-name>
        <!--suppress WebProperties -->
        <url-pattern>/filter</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>demo-filter-2</filter-name>
        <!--suppress WebProperties -->
        <url-pattern>/filter</url-pattern>
    </filter-mapping>
</web-app>
  • デプロイして起動し、 /demo へリクエストを投げる
    • フィルターは処理が走らないはず
    • でも HTTP Status Code は 503 が返ってくるんで、やはりサーブレットごと死んでますね
$ curl http://localhost:8080/servlet-1.0-SNAPSHOT/demo
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 503 Service Unavailable</title>
</head>
<body><h2>HTTP ERROR 503 Service Unavailable</h2>
<table>
<tr><th>URI:</th><td>/servlet-1.0-SNAPSHOT/demo</td></tr>
<tr><th>STATUS:</th><td>503</td></tr>
<tr><th>MESSAGE:</th><td>Service Unavailable</td></tr>
<tr><th>SERVLET:</th><td>-</td></tr>
</table>
<hr><a href="https://eclipse.org/jetty">Powered by Jetty:// 11.0.6</a><hr/>

</body>
</html>
  • doFilter で例外が出た場合は?
  • web.xmlは上記の書き換えたバージョンで
  • Filterは以下のように書き換え
package org.example.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;
import java.io.PrintWriter;

@WebFilter(filterName = "demo-filter-1")
public class DemoFilter1 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
        System.out.println("DemoFilter init");
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
        System.out.println("DemoFilter destroy");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        StringBuffer requestURL = ((HttpServletRequest) request).getRequestURL();

        System.out.println("filter 1");
        System.out.println("request url : " + requestURL);


        throw new ServletException();
    }

}
  • デプロイしてリクエストを投げてみる
    • /demo はフィルターが見てないんでふつーに200ですね
$ curl http://localhost:8080/servlet-1.0-SNAPSHOT/demo
hello from Jakarta Servlet 5 Servlet : service
  • /filter にリクエストを投げると、例外が発生して 500 になります
    • ただ、この場合、アプリごと死んでしまうわけではなくて、この後もリクエストを処理してくれます
$ curl -v http://localhost:8080/servlet-1.0-SNAPSHOT/filter
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /servlet-1.0-SNAPSHOT/filter HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 500 Server Error
< Cache-Control: must-revalidate,no-cache,no-store
< Content-Type: text/html;charset=iso-8859-1
< Content-Length: 3740
< Connection: close
< Server: Jetty(11.0.6)
< 
  • つまり init で死ぬと、アプリごと道連れになりますが、 doFilter だと道連れにならなそうって感じですかねー
    • init で失敗して動き続けられても困るんで、そりゃそうなるよねって感じです