L is B BLOG

株式会社L is Bの社員ブログです。会社の取り組みや技術ブログを発信しています。

Jersey1(jersey-guice) から Jersey2 (Jersey2-HK2-Guice) への移行に関する手記〜その1

こんにちは、サーバーで Java を書いている持田です。

f:id:mike_neck:20190808200556p:plain

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.xmlservlet-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 に移行すると、以下の変更が求められます。

  1. Jersey2 には jersey-guice に相当するライブラリーが存在しないため、 JerseyServletModule サブクラスを別のクラスのサブクラスにして、同等の処理を提供する。
  2. JerseyServletModule サブクラスの filter で指定している GuiceContainerjersey-guice に含まれるクラスであるため、何らかの対策が必要そうです。
  3. Jersey2 には HK2 という DI コンテナが標準でついており、 HK2 と Guice との間を取り持つためのブリッジが必要になる。
    • HK2 と Guice の連携自体は巷にあふれる情報から ResourceConfig クラスで連携設定をすることはわかりますが、 現状の GuiceServletContextListener の継承クラスで生成した Injector をどのように ResourceConfig クラスで取り出すのか調べる必要がありそうです。

以上から、既存のクラスがどのような処理を提供しているのかを把握することが移行のための第一歩となりそうです。

JerseyServletModule の継承クラスの移行方法を調べる

JerseyServletModuleServletModule の継承クラスで、それに加えて GuiceContainer に Jersey 1 の設定情報などを渡しているクラスとなっています。 そのため、移行にあたっては…

  • JerseyServletModuleServletModule に置き換える。
  • 設定情報などは ResourceConfig で設定するため、 ResourceConfig に Jersey2 との互換性を考慮して移設する必要がある。
  • filter で指定している GuiceContainer については別途調査(後述)。

といったことがわかりました。

GuiceContainer の指定をどうするか調べる

GuiceContainer のコードを読んだところ以下のことがわかります。

  • Jersey1 の ServletContainer の継承クラスである。
  • ResourceConfig や、 WebApplication といったアプリケーションを表すクラスを返している。

これらが Jersey2 でどのようなクラスに対応しているのか調べたところ、 ServletContainer という名前の同じクラスが Jersey2 にあることがわかりました。 では、これを ServletModulefilter メソッドで指定すればよいのかというと、そうではないようです(実験した結果わかった)。

ServletContainerインスタンスJerseyServletContainerInitializer というクラスが生成しているようです。 このクラスはサーブレット API が提供している ServletContainerInitializer インターフェースを実装したクラスで、 サーブレットAPIが検出・生成・実行しています。 なお、この JerseyServletContainerInitializer というクラスですが、 サーブレットAPIが検出した @Provider/@Path/@ApplicationPath といったアノテーションが付与されているクラスや Application クラスの継承クラスを受け取って、リソース情報の組み立てなどを行っています。

以上の調査から、

  • ServletModulefilter に指定しなくてよい

ということがわかりました。

GuiceServletContextListener で生成した InjectorResourceConfig 継承クラスで取り出す方法を調べる

GuiceServletContextListener にてアプリケーションプログラマーInjector を生成しますが、利用目的を確認するために GuiceServletContextListener のコードを読んでみたところ、 GuiceServletContextListenerServletContextInjector を収めていることがわかりました。

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);
    // ... 省略
  }
}

さらに、 InjectorServletContext から取れるか確認してみました。

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アーカイブになっているレポジトリーが最も参考になりました。