こんにちは、サーバーで Java を書いている持田です。
direct では現在サーバーに Jersey という Web フレームワークのバージョン 1 のものを使っています。 こちらは REST API に関する Java 標準仕様の JSR(Java Specification Request)-311(2008 年) の参照実装となるライブラリーです。 これからの数回にわたって、 Jersey 1 から Jersey 2 への移行を焦点に技術的なメモを書いておこうと思います。
第 1 回は、 Jersey 2 + HK2 + Guice の連携についてです。
====
TL;DR;
- jersey-guice から Jersey2 + HK2 + Guice へ移行するために膨大な修正が必要になるかと覚悟していたけど、ほとんど修正が必要なさそうだということがわかった
移行が必要になった経緯
現在 direct で使っている Java のバージョンは 8 です。 これを Java 11 にあげるためにアプリケーションを検証していたところ、 Jersey 1 が内部に抱えている ASM という動的バイトコード生成ライブラリーが Java11 でコンパイルしたクラスファイルを解釈できないことがわかりました。 そこで、 Jersey 1 を Jersey 2 にアップグレードする必要が生じました。
現在のコード
現在の direct では、 jersey-guice
というライブラリーを用いています。 このライブラリーを用いて以下の処理を行います。これにより、 Jersey 1 と Guice が連携できます。
JerseyServletModule
を継承したクラスを使ってクラスのバインドをおこなう。GuiceServletContextListener
を継承したクラスをweb.xml
のservlet-listener
に登録して、上述のクラスを使ってInjector
を生成する- リクエストのパス
/*
に対して、GuiceFilter
をマッピングする。
これらはこのようなコードになります。
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"> <filter> <filter-name>guiceFilter</filter-name> <filter-class>com.google.inject.servlet.GuiceFilter</filter-class> </filter> <filter-mapping> <filter-name>guiceFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <listener> <listener-class>com.example.AppServletContextListener</listener-class> </listener> </web-app>
GuiceServletContextListener 継承クラス
class AppServletContextListener extends GuiceServletContextListener { private static final Logger logger = LoggerFactory.getLogger(AppServletContextListener.class); @Override public void contextInitialized(final ServletContextEvent sce) { logger.info("context-initialized"); super.contextInitialized(sce); // some configuration } @Override public void contextDestroyed(final ServletContextEvent sce) { logger.info("context-destroyed"); super.contextDestroyed(sce); // some after execution code } @Override protected Injector getInjector() { return Guice.createInjector(new AllModule()); } }
JerseyServletModule 継承クラス
class AllModule extends JerseyServletModule { @Override protected void configureServlets() { // ... bind など ... filter("/*").through(MyFilter.class); filter("/*").through(GuiceContainer.class, Collections.singletonMap(JSONConfiguration.FEATURE_POJO_MAPPING, "true")); } }
Jersey2 への移行の調査
上述のコードが Jersey2 に移行すると、以下の変更が求められます。
- Jersey2 には
jersey-guice
に相当するライブラリーが存在しないため、JerseyServletModule
サブクラスを別のクラスのサブクラスにして、同等の処理を提供する。 JerseyServletModule
サブクラスのfilter
で指定しているGuiceContainer
もjersey-guice
に含まれるクラスであるため、何らかの対策が必要そうです。- Jersey2 には HK2 という DI コンテナが標準でついており、 HK2 と Guice との間を取り持つためのブリッジが必要になる。
- HK2 と Guice の連携自体は巷にあふれる情報から
ResourceConfig
クラスで連携設定をすることはわかりますが、 現状のGuiceServletContextListener
の継承クラスで生成したInjector
をどのようにResourceConfig
クラスで取り出すのか調べる必要がありそうです。
- HK2 と Guice の連携自体は巷にあふれる情報から
以上から、既存のクラスがどのような処理を提供しているのかを把握することが移行のための第一歩となりそうです。
JerseyServletModule の継承クラスの移行方法を調べる
JerseyServletModule
は ServletModule
の継承クラスで、それに加えて GuiceContainer
に Jersey 1 の設定情報などを渡しているクラスとなっています。 そのため、移行にあたっては…
JerseyServletModule
をServletModule
に置き換える。- 設定情報などは
ResourceConfig
で設定するため、ResourceConfig
に Jersey2 との互換性を考慮して移設する必要がある。 filter
で指定しているGuiceContainer
については別途調査(後述)。
といったことがわかりました。
GuiceContainer の指定をどうするか調べる
GuiceContainer
のコードを読んだところ以下のことがわかります。
- Jersey1 の
ServletContainer
の継承クラスである。 ResourceConfig
や、WebApplication
といったアプリケーションを表すクラスを返している。
これらが Jersey2 でどのようなクラスに対応しているのか調べたところ、 ServletContainer
という名前の同じクラスが Jersey2 にあることがわかりました。 では、これを ServletModule
の filter
メソッドで指定すればよいのかというと、そうではないようです(実験した結果わかった)。
ServletContainer
のインスタンスは JerseyServletContainerInitializer
というクラスが生成しているようです。 このクラスはサーブレット API が提供している ServletContainerInitializer
インターフェースを実装したクラスで、 サーブレットAPIが検出・生成・実行しています。 なお、この JerseyServletContainerInitializer
というクラスですが、 サーブレットAPIが検出した @Provider
/@Path
/@ApplicationPath
といったアノテーションが付与されているクラスや Application
クラスの継承クラスを受け取って、リソース情報の組み立てなどを行っています。
以上の調査から、
ServletModule
でfilter
に指定しなくてよい
ということがわかりました。
GuiceServletContextListener で生成した Injector を ResourceConfig 継承クラスで取り出す方法を調べる
GuiceServletContextListener
にてアプリケーションプログラマーは Injector
を生成しますが、利用目的を確認するために GuiceServletContextListener
のコードを読んでみたところ、 GuiceServletContextListener
は ServletContext
に Injector
を収めていることがわかりました。
ResourceConfig
継承クラスのインスタンスに ServiceLocator
を取るように次のようなコンストラクターを設けます。
public class MyApp extends ResourceConfig { @Inject public MyApp(ServiceLocator serviceLocator) { // ... 省略 } }
この ServiceLocator
から ServletContext
が取得できないか試してみたところ、取得できることがわかりました。
public class MyApp extends ResourceConfig { @Inject public MyApp(ServiceLocator serviceLocator) { ServletContext servletContext = serviceLocator.getService(ServletContext.class); // ... 省略 } }
さらに、 Injector
を ServletContext
から取れるか確認してみました。
public class MyApp extends ResourceConfig { @Inject public MyApp(ServiceLocator serviceLocator) { ServletContext servletContext = serviceLocator.getService(ServletContext.class); Injector injector = (Injector) servletContext.getAttribute(Injector.class.getName()); // ... 省略 } }
以上の調査から、 ResourceConfig
の継承クラスにて ServiceLocator
が保持している ServletContext
から GuiceServletContextListener
で生成した Injector
を取り出せることがわかりました。
変更後のコード
web.xml
変更なし
GuiceServletContextListener 継承クラス
変更なし
JerseyServletModule 継承クラス
class AllModule extends ServletModule { @Override protected void configureServlets() { // ... bind など ... filter("/*").through(MyFilter.class); // ... filter(GuiceContainer.class) を削除 } }
ResourceConfig 継承クラス
新規作成
public class MyApp extends ResourceConfig { @Inject public MyApp(ServiceLocator serviceLocator) { ServletContext servletContext = serviceLocator.getService(ServletContext.class); Injector injector = (Injector) servletContext.getAttribute(Injector.class.getName()); GuiceBridge.getGuiceBridge().initializeGuiceBridge(serviceLocator); GuiceIntoHK2Bridge bridge = serviceLocator.getService(GuiceIntoHK2Bridge.class); bridge.bridgeGuiceInjector(injector); // 設定等(省略) } }
まとめ
オリンピック目前の 2019 年ですが、 2013 年の技術について調査しました。
- Jersey1 は内部に抱えている ASM のバージョンが古いため Java11 でコンパイルしたアプリケーションを動かせない。
com.sun.jersey
以下のパッケージを利用していなければ、コードの移行のコストは(ここまでのところ)かからなそう。- 移行する際には既存のAPIがどのような役割を果たしているかを確認するのが正解への近道であるように思われる。
第 2 回は Jersey2 で JSON のシリアライズに Jackson1 または Jettison を使う場合の設定方法の覚書を記述する予定です。
現在、 direct 開発部では Java を書きたいエンジニアや、 AWS 上で動くサービスを安定運用したいエンジニアや、 Kotlin で Android アプリを書きたいエンジニアを募集しています。 気になるぞという方はこちらからお申込みいただけます。 みなさんも、一緒に direct を盛り上げていきませんか?ご応募お待ちしております。
参考
「Jersey2 HK2 Guice」で検索すると、おそらく山のようにエントリーが見つかります。 このエントリーもその山の中に埋もれるものとなるでしょう。
山の中には僕がおかしいのか、それとも記述がおかしいのかうまく行かないケースが多かったのですが、 うらがみさんの GitHub でアーカイブになっているレポジトリーが最も参考になりました。