[Jersey]埋め込みTomcatでJerseyを動かしてみる

# はじめに

Tomcat + Jersey という構成の Web アプリケーションの動作を理解するために、[Tomcat Embed](https://mvnrepository.com/artifact/org.apache.tomcat.embed) を使って実験をしていきます。
いまどきこんな構成で開発を始めることは少ないかと思いますが、レガシーソフトウェアと戦う人たちの助けになれば幸いです。

## 関連記事の一覧（予定）

- 埋め込みTomcatでJerseyを動かしてみる ← イマココ
- [HK2でDIしてみる](https://qiita.com/atti/items/3f6f43c5168323344427)
- [BeanValidationで入力値検証してみる](https://qiita.com/atti/items/2267a4b52b033dfabcca)
- [TomcatとJerseyでトランザクション管理してみる](https://qiita.com/atti/items/4d682f01cf6018172936)

## リポジトリ

https://github.com/at6ue/tomcat_embed-jersey

# 静的コンテンツを配置する

Tomcat 9.0.x と Jersey 2.x において[^1]最低限必要な依存関係[^2]は以下です。

```gradle:build.gradle
...
dependencies {
    implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: TOMCAT_VERSION
    implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: TOMCAT_VERSION
    implementation group: 'org.glassfish.jersey.containers', name: 'jersey-container-servlet', version: JERSEY_VERSION
    // ↓ Jersey 2.26 以降で必要
    implementation group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: JERSEY_VERSION
}
...
```

[^1]: Tomcat 10.x と Jersey 3.x でも同様ですが、 `javax` が `jakarta` に変更された影響で、どちらも以前のバージョンとは共存できないようです。また、 Java や XML ファイルにある `javax` を `jakarta` に書き換える必要があります。経緯については [Java EEからJakarta EEへ | Oracle Technology Network Japan Blog](https://blogs.oracle.com/otnjp/transition-from-java-ee-to-jakarta-ee-ja) を参照のこと。

[^2]: [Minimal Embedded Tomcat - A Memorandum](https://blog1.mammb.com/entry/2015/03/26/003301) に記載の `tomcat-embed-logging-juli` は Tomcat 8.5 以降では不要なようです。

ドキュメントルートとなるディレクトリを作成し、適当な静的ファイルを配置します。本記事では `src/main/webapp` としました。

```:ファイル構成
src/main
├── java
│   └── （省略）
│　　　　　　　　　    　└── App.java
└── webapp
    └── index.html
```

エントリポイントとなるクラスの `main` メソッドで、ドキュメントルートのパスを指定します。

```java:App.java
public class App {
    public static void main(String[] args) throws Exception {
        Tomcat tomcat = new Tomcat();
        tomcat.getConnector(); // Tomcat 9 以降では必須
        var context = tomcat.addWebapp("", Paths.get("src/main/webapp/").toAbsolutePath().toString());
        tomcat.start();
        tomcat.getServer().await();
    }
}
```

作成したクラスを実行してみましょう。このとき、Java アプリケーションの作業ディレクトリから、指定した相対パスでドキュメントルートに到達できることに注意してください。
Tomcat のデフォルトポート 8080 にアクセスすると、 `index.html` が返ってきます。

```bash:Terminal
curl localhost:8080
# <!DOCTYPE html>
# <html>
#   <body>
#     Hello Tomcat!
#   </body>
# </html>
```

# リソースを web.xml で配置する

Jersey による動的コンテンツを追加してみましょう。

```java:BasicResource.java
...
@Path("")
public class BasicResource {
    @GET
    public String get() {
        return "Hello Jersey👕";
    }
}
```

ドキュメントルートに `WEB-INF/web.xml` を追加し、作成した `BasicResource` の完全修飾名を記載します。

```:ファイル構成
src/main
├── （省略）
└── webapp
    ├── WEB-INF
    │   └── web.xml
    └── index.html
```

```xml:web.xml
<web-app version="3.0"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <servlet>
    <servlet-name>Basic</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>jersey.config.server.provider.classnames</param-name>
      <param-value>com.github.at6ue.jersey.basic.BasicResource</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>Basic</servlet-name>
    <url-pattern>/basic</url-pattern>
  </servlet-mapping>
</web-app>
```

アプリケーションを再起動して、`/basic` にアクセスしてみましょう。

```bash:Terminal
curl localhost:8080/basic
# Hello Jersey👕
```

# リソースを ResourceConfig で配置する

リソースの可換性が必要なければ `web.xml` よりも Java でリソースを指定するほうが、コンパイル時にリソースを参照できることが保証されるため[^3]、より安全です。
[^3]: ここで保証されるのはリソースクラスへの参照だけで、リソースに注入される依存関係は実行時にはじめて検証されることに注意。

Jersey では、どのリソースをアプリケーションに追加するかを `ResouceConfig` のサブクラスに指定できます。ここでは自身の属するパッケージを指定することで、その配下にあるすべてのリソースが自動的にアプリケーションへ追加されます。

```java:AppConfig.java
...
@ApplicationPath("app")
public class AppConfig extends ResourceConfig {
    public AppConfig() {
        packages(getClass().getPackage().getName());
    }
}
```

ここで先ほど作成した `BasicResource` を含むパッケージを指定した場合、 `web.xml` で指定した `/basic` と、 `AppConfig` で指定したパス `app` の両方で `BasicResource` に到達できるようになります。
確認してみましょう。

```bash:Terminal
curl localhost:8080/basic
# Hello Jersey👕
curl localhost:8080/app
# Hello Jersey👕
```

どちらも同じレスポンスが返っていますね。
なお、本記事では `web.xml` を併用しましたが、すべて Java で記述する場合には `WEB-INF/web.xml` は存在しなくても構いません。

# フィルターを設定する

ところで、 `localhost:8080/app` に **ブラウザから** アクセスしてみると、絵文字👕が正しく出力されず以下のように文字化けしてしまいました。
![Firefox で表示したところ "Hello JerseyðŸ‘•" などと表示される](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591453/f59bd110-6068-0596-487d-f1aa8fbf7d9f.png)
これは HTTP レスポンスに文字セットの指定がない場合、デフォルトの文字セット ISO-8859-1 で解釈されるためです[^4]。
[^4]: [rfc5987](https://datatracker.ietf.org/doc/html/rfc5987) の冒頭に言及されています。
この対策として、文字セットの指定がない場合には UTF-8 の指定を追加するフィルターを設定してみます[^5]。

[^5]: この実装アイディアは [android - Jersey / Rest default character encoding - Stack Overflow](https://stackoverflow.com/a/20569571) を参考にしました。

```java:AddCharsetUtf8ToResponseFilter.java
...
public class AddCharsetUtf8ToResponseFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        var mediaType = responseContext.getMediaType();
        if (mediaType == null || mediaType.toString().contains(MediaType.CHARSET_PARAMETER)) {
            return;
        }
        responseContext.getHeaders().putSingle("Content-Type", mediaType.toString() + ";charset=utf-8");
    }
}
```

`AppConfig` で作成したフィルターを登録します。

```java:AppConfig.java
...
@ApplicationPath("app")
public class AppConfig extends ResourceConfig {
    public AppConfig() {
        packages(getClass().getPackage().getName());
        register(AddCharsetUtf8ToResponseFilter.class); // 追加
    }
}
```

アプリケーションを再起動して、`localhost:8080/app` に再アクセスしてみましょう。

!["Hello Jersey👕" と表示される](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591453/3642c1a3-f8b7-64ab-7833-59a30d0986ab.png)

正しく表示されました。
ここで、フィルターが追加されたのは `localhost:8080/app` のみで、 `web.xml` で登録した `localhost:8080/basic` では文字化けしたままであることから、それぞれが **別のアプリケーションとして** 同じリソースを呼び出していることが分かります。


# 参考

- [Chapter 4. Application Deployment and Runtime Environments](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/deployment.html#deployment.servlet)
- [Chapter 10. Filters and Interceptors](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/filters-and-interceptors.html#d0e9587)
- [java - Code works with Embedded Apache Tomcat 8 but not with 9. What's changed? - Stack Overflow](https://stackoverflow.com/questions/48998387/code-works-with-embedded-apache-tomcat-8-but-not-with-9-whats-changed)