CDI1.1からのデフォルト化で少し困ったこと

この記事はGlassFish Advent Calendar 2013の21日目の記事です。

昨日はHASUNUMA Kenji(@btnrouge)さんのGlassFish 4.0のCDIに潜む罠でした。
明日はMako Yanagisawaさんです。よろしくお願いします。


以前の記事にも書きましたが、2013年5月にリリースされた仕様 Java EE 7 からは、beans.xmlをWARなどのアーカイブなどに含めなくても、@RequestScopedなどのスコープ関連のアノテーションが付与されたクラスを含むアーカイブではデフォルトでCDIが有効になっています。今回はこのデフォルト化によって少し困ったことを紹介しようと思います。

guava-14.0.1でデプロイエラーになる

GlassFish4では、Googleが開発している便利なライブラリguava-librariesのバージョン14をWEB-INF/libに含めてデプロイすると、以下のようなデプロイエラーがCDI実装であるWeldから出力されます。

org.jboss.weld.exceptions.DeploymentException: WELD-001408 Unsatisfied dependencies for type … @Inject com.google.common.util.concurrent.ServiceManager(Set<Service>)]

エラーメッセージによると、guavaの並行処理に関するServiceManagerクラスに@Injectがあるけど、インジェクションしたいSetがどれかわからないよとのことです。guava14のServiceManagerのソースを見てみると、以下のようになっています。

import javax.inject.Singleton;
import javax.inject.Inject;

@Bata
@Singleton
public final class ServiceManager {
    ...
    @Inject ServiceManager(Set<Service> services) {...}

スコープに関するアノテーション@Singletonが付いている*1ので、Java EE 7(CDI1.1)の環境ではアーカイブguava-14.0.1.jarはCDIが有効になっているとみなされます。さらにコンストラクタで@Injectによって引数のインジェクションを試みていますが、型が特定できずに失敗しています。後述するように現在では対処版がリリースされていますが、試しに@Singletonをコメントアウトして再ビルドすると、guavaはCDIの有効対象アーカイブとならず、デプロイに成功します。

対処

この事象guavaのissueに登録されていたので確認してみます。

GlassFish4(CDI1.1)環境で使う場合は、現在の最新版のリリース版であるguava15に差し替えることで事象は解決します。

その後、色々な対処を経て、最終的にまだ正式リリースなっていないguava16からはアノテーションが削除されて、GuiceなどのDIコンテナと組み合わせてインジェクションするコンストラクタが廃止されています。

最終的にアノテーションを削除するに至ったときには、少なくともGuava側はあまり良い気分ではなかったようです。

It's not because we believe it's wrong for Guava to use the annotations;

https://code.google.com/p/guava-libraries/issues/detail?id=1433#c46

@javax.inject.Inject や @javax.inject.Singleton などのアノテーションはSpringを作ったのRod Johnson氏とGuiceのBob Lee氏が中心に策定したDependency Injection for Java(JSR-330)という、JBoss Seemの流れをくむCDI(JSR-299)とは別の仕様で定義されています。CDIがJSR-330で定義されたアノテーションに対応したことで、普段CDIを使うときにはそもそも別の仕様であったことを意識することはほとんどありません。

事象はJSR-330のアノテーションのみ使われている既存のライブラリが、CDI1.1環境で動かないという事象でした。対処はguava側で盛り込まれています。

CDIかguavaどちらが修正すべきなのかは難しい問題です。CDIのような仕様を変更するには手間がかかりそうです。かといって、JSR-330に沿ってライブラリを作ったところ『CDIで動かないからどうにかして!』と要望をもらったら、あまり嬉しい話とは感じられないのは納得できます。ユーザとしてはお互い仲良くしてくれるのが一番嬉しいです。

ライブラリ内で@javax.inject.Singletonがある場合の対処方法

当初はGuiceやSpringで使うことを意識して@Singletonを含めていて、その後 Java EE 環境で動かしたいということは、自作ライブラリなどでも起こりうる要望だと思います。guavaの例の通り、そのままでは Java EE 環境では動きません。

guavaの対応を確認すると、色々な対処方法があるようなので以下にまとめてみます。

対処1. bean-discovery-mode="none"を含んだbean.xmlを追加

guava-15.0.jarが取っている手段がこのパターンです。bean-discovery-mone="none"を設定したbeans.xmlがMETA-INF直下に含まれています。guavaと同じようにライブラリ(Webアプリ側ではない)に対してbeans.xmlを追加すると自作ライブラリでも同様の効果が得られると思います。

<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_1_1.xsd"
    bean-discovery-mode="none">
</beans>

この設定により、guava-15.0.jarの中はCDIが無効化されます。アプリケーションに含まれるクラス間の連携ではCDIは問題なく使えます。

しかし、この対処を盛り込むと困ったことに、CDI1.1では動くが、CDI1.0(GlassFish3.x, JBossAS7.x)でデプロイに失敗するといった欠点があります。追加したbean.xmlファイルが、CDI1.0環境ではbean-discovery-modeに対応していないので、『beans.xmlがあるのでCDIが有効化された』と解釈されてしまうためです。

guavaの場合は、この問題に対処するため、CDI1.1環境で動くためにbean.xmlを含んだ guava-15.0.jar と、CDI1.0環境で動くためにbean.xmlを抜いた guava-15.0-cdi1.0.jar をリリースしているようです。

Guava Release 15.0: Release Notes
http://code.google.com/p/guava-libraries/wiki/Release15

対処2. weld:excludeをbeans.xmlを追加

GlassFishissueで紹介されていたやり方で、以下のようなbean.xmlを同じくライブラリに対して追加します。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:weld="http://jboss.org/schema/weld/beans"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd
                           http://jboss.org/schema/weld/beans http://jboss.org/schema/weld/beans_1_1.xsd">

    <weld:scan>
        <weld:exclude name="com.google.**"/>
    </weld:scan>
</beans>

guavaに盛り込む場合はCDI対象外指定を"com.google"以下全てのクラスとしていますが、パッケージ名は自作ライブラリに合わせて適宜変更してください。このやり方はだと、同じjarライブラリでCDI1.1/CDI1.0ともにデプロイに成功しますが、CDI実装のWeldスキーマに依存してしまう欠点があります。GlassFishやJBossAS/WildFlyCDI実装にWeldを使っているため問題なく動いていますが、Weld以外を使用しているアプリケーションサーバでうまく動かないと思います。

対処3. GlassFish4のimplicitCdiEnabled=false

GlassFish4から使用可能なオプションで、beans.xmlを含めていないアーカイブの暗黙的なCDIの有効化を止めることができます。

asadmin deploy --property implicitCdiEnabled=false <archive>

この対処を盛り込むと、@Singletonがライブラリに含まれていたとしても、ライブラリにbeans.xmlなどを追加せずにCDIを無効化することが可能です。しかし、このやり方にも欠点はあり、アプリケーション側でCDIのデフォルト有効化が効かなくなってしまい、例えCDI1.1のデフォルト挙動であるアノテーション付きクラスのみをインジェクションしたい場合でも、bean-discovery-mode="annotated"を含んだbeans.xmlをアプリケーション側(warなど)に明示的に含める対応が必要になります。

2014/01/12 追記
このオプションは2014/1現在、まだ正式リリースされていないGlassFish4.0.1から使えるようになります。挙動についてはnightly-buildなどで確認することができます。恐らくこのissueが該当の対処。

対処4. ライブラリ内での@Singletonをあきらめる

Guava16で最終的に取られた手段です。JSR-330関連のアノテーションをライブラリから削除します。Guavaの場合は、対象のクラスが@Betaでマークされており、互換性が強く確保されていなかった機能なので削除する対応を行っていました。互換性がなくなる対処なので、対象のライブラリが広く配られており、かつGuiceやSpring上で使われているケースが多い場合はこの手段の選択は難しいと思います。

まとめ

@javax.inject.Singletonが含まれるライブラリのCDI1.1環境におけるデプロイエラー問題は、どの対処もピリっとしない悩ましい問題です。しいていうと、GlassFishおよびJBossAS/WildFlyなどのCDI実装にWeldを使っているAPサーバが多い組織では、対処2として紹介したweld:exclude設定を書いたbeans.xmlをライブラリに盛り込むことが最善かと思います。

余談

本当はJersey2.3から大きく見直されているOAuth Supportについて書こうと思っていたのですが、マニュアルの通りにpom.xmlを書くとOAuthアーティファクトの推移的依存先にguava-14.01があり、これが原因で色々とハマってしまいました。OAuthについては、また機会を見つけて書こうと思います。



2015/1/12 追記

CDI1.2より@javax.inject.Singletonはスキャン対象Beanから外れました。よってGlassFish4.1およびWildFly8.2以降では、上記のような問題は発生なくなりました。詳しくは、こちらの記事にまとめています。

*1:Singletonはインスタンスが1つしかないことを示しているので、生存期間を示すスコープと分けられて、CDIの仕様では疑似スコープと呼ばれています