はじめに
Tomcat + Jersey という構成の Web アプリケーションの動作を理解するために、Tomcat Embed を使って実験をしていきます。
いまどきこんな構成で開発を始めることは少ないかと思いますが、レガシーソフトウェアと戦う人たちの助けになれば幸いです。
関連記事の一覧(予定)
- 埋め込みTomcatでJerseyを動かしてみる ← イマココ
- HK2でDIしてみる
- BeanValidationで入力値検証してみる
- TomcatとJerseyでトランザクション管理してみる
リポジトリ
静的コンテンツを配置する
Tomcat 9.0.x と Jersey 2.x において1最低限必要な依存関係2は以下です。
...
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
}
...
ドキュメントルートとなるディレクトリを作成し、適当な静的ファイルを配置します。本記事では src/main/webapp
としました。
src/main
├── java
│ └── (省略)
│ └── App.java
└── webapp
└── index.html
エントリポイントとなるクラスの main
メソッドで、ドキュメントルートのパスを指定します。
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
が返ってきます。
curl localhost:8080
# <!DOCTYPE html>
# <html>
# <body>
# Hello Tomcat!
# </body>
# </html>
リソースを web.xml で配置する
Jersey による動的コンテンツを追加してみましょう。
...
@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
<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
にアクセスしてみましょう。
curl localhost:8080/basic
# Hello Jersey👕
リソースを ResourceConfig で配置する
リソースの可換性が必要なければ web.xml
よりも Java でリソースを指定するほうが、コンパイル時にリソースを参照できることが保証されるため3、より安全です。
Jersey では、どのリソースをアプリケーションに追加するかを ResouceConfig
のサブクラスに指定できます。ここでは自身の属するパッケージを指定することで、その配下にあるすべてのリソースが自動的にアプリケーションへ追加されます。
...
@ApplicationPath("app")
public class AppConfig extends ResourceConfig {
public AppConfig() {
packages(getClass().getPackage().getName());
}
}
ここで先ほど作成した BasicResource
を含むパッケージを指定した場合、 web.xml
で指定した /basic
と、 AppConfig
で指定したパス app
の両方で BasicResource
に到達できるようになります。
確認してみましょう。
curl localhost:8080/basic
# Hello Jersey👕
curl localhost:8080/app
# Hello Jersey👕
どちらも同じレスポンスが返っていますね。
なお、本記事では web.xml
を併用しましたが、すべて Java で記述する場合には WEB-INF/web.xml
は存在しなくても構いません。
フィルターを設定する
ところで、 localhost:8080/app
に ブラウザから アクセスしてみると、絵文字👕が正しく出力されず以下のように文字化けしてしまいました。
これは HTTP レスポンスに文字セットの指定がない場合、デフォルトの文字セット ISO-8859-1 で解釈されるためです4。
...
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
で作成したフィルターを登録します。
...
@ApplicationPath("app")
public class AppConfig extends ResourceConfig {
public AppConfig() {
packages(getClass().getPackage().getName());
register(AddCharsetUtf8ToResponseFilter.class); // 追加
}
}
アプリケーションを再起動して、localhost:8080/app
に再アクセスしてみましょう。
正しく表示されました。
ここで、フィルターが追加されたのは localhost:8080/app
のみで、 web.xml
で登録した localhost:8080/basic
では文字化けしたままであることから、それぞれが 別のアプリケーションとして 同じリソースを呼び出していることが分かります。
参考
- Chapter 4. Application Deployment and Runtime Environments
- Chapter 10. Filters and Interceptors
- java - Code works with Embedded Apache Tomcat 8 but not with 9. What's changed? - Stack Overflow
-
Tomcat 10.x と Jersey 3.x でも同様ですが、
javax
がjakarta
に変更された影響で、どちらも以前のバージョンとは共存できないようです。また、 Java や XML ファイルにあるjavax
をjakarta
に書き換える必要があります。経緯については Java EEからJakarta EEへ | Oracle Technology Network Japan Blog を参照のこと。 ↩ -
Minimal Embedded Tomcat - A Memorandum に記載の
tomcat-embed-logging-juli
は Tomcat 8.5 以降では不要なようです。 ↩ -
ここで保証されるのはリソースクラスへの参照だけで、リソースに注入される依存関係は実行時にはじめて検証されることに注意。 ↩
-
rfc5987 の冒頭に言及されています。
この対策として、文字セットの指定がない場合には UTF-8 の指定を追加するフィルターを設定してみます5。 ↩ -
この実装アイディアは android - Jersey / Rest default character encoding - Stack Overflow を参考にしました。 ↩