Java
jboss
JavaEE
Wildfly

JBossのモジュールとアプリケーションのライブラリの競合を防ぐ方法

確認環境

JBoss EAP 6.4.0, 7.0.0, 7.1.0beta1

モジュールとライブラリの競合が起こる原因

Java EEアプリケーションサーバとしてJBoss EAP(以下JBoss)を使う際の注意点としてライブラリとモジュールの競合があります。
詳細については、開発ガイドの3章クラスローディングとモジュールに記載されていますが、簡潔に説明すると、JBossではアプリケーションのライブラリよりもJBossに同梱されているモジュールが優先してロードされます。(ざっと見た感じ順番を変える設定はなさそうです。)

つまり、アプリケーションがビルド時に使用したライブラリのバージョンと異なるバージョンで実行される可能性があり、起動時や実行時にエラーが発生するかもしれません。(今は大丈夫でもバージョンアップしたときに問題が起こるかも:anguished:

ロードされるモジュールを把握する

JBossのモジュールはデプロイしたものによって自動的にロードされるので、動かしたいアプリケーションを1回デプロイしてみて、何がロードされるか把握しましょう。

{JBOSS_HOME}/standalone/configurationにあるstandalone.xmlのデフォだと115行目くらいにログ出力に関する設定があるので、levelをDEBUGに変更します。

standalone.xml
  <root-logger>
   <level name="DEBUG"/>
   <handlers>
    <handler name="CONSOLE"/>
    <handler name="FILE"/>
   </handlers>
  </root-logger>

この状態でJBossを起動して何かアプリケーションをデプロイしてみてください。
すると{JBOSS_HOME}/standalone/server.logにアプリケーションのライブラリとJBossがロードしたモジュールの一覧が出力されています。
Adding resourceと書かれている部分がwarから読み込まれたライブラリで、
Adding dependency ModuleDependencyと書かれている部分がロードされたモジュールです。
以下は出力例です。

server.log
2017-09-04 18:29:42,597 DEBUG [org.jboss.as.server.deployment] (MSC service thread 1-7) Adding resource "/C:/jboss/standalone/deployments/todo.war/WEB-INF/lib/aopalliance-1.0.jar" to module deployment.todo.war
2017-09-04 18:29:42,648 DEBUG [org.jboss.as.server.deployment] (MSC service thread 1-7) Adding dependency ModuleDependency [identifier=javax.ejb.api, moduleLoader=local module loader @282ba1e (finder: local module finder @13b6d03 (roots: C:\jboss\modules,C:\jboss\modules\system\layers\base)), export=false, optional=false, importServices=true] to module deployment.todo.war

今回は例としてjavax.ejb.apiを除外してみます。

モジュールに関する設定ファイルの作成

WebアプリケーションのWEB-INF直下にjboss-deployment-structure.xmlを配置します。
配置したら以下のように設定します。

jboss-deployment-structure.xml
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2">
    <deployment>
        <exclusions>
            <module name="javax.ejb.api" />
        </exclusions>
    </deployment>
</jboss-deployment-structure>

除外するモジュールを<exclusions>で囲うだけです。
パスから指定する必要がありますが、これは各モジュールのmodule.xmlに書かれています。
では、この状態でJBossを再起動してみます。

ログが長いので割愛しますが、Adding dependency ModuleDependencyの欄にjavax.ejb.apiがないはずです。

その他の設定

jboss-deployment-structure.xmlには、もともとJBossに含まれないモジュールを追加したり、複数のモジュールで構成されるサブシステムごと除外したりできます。ここでは取り上げませんが、設定方法が開発ガイドの3章に書かれていますので参考にしてください。

この投稿に至った経緯

Tomcatで問題なく動くアプリケーションがJBossだとエラーになる原因がわからず四苦八苦したので:dizzy_face:

おまけ

具体的な事例

こっちをメインに書くか悩んだのですが、単にモジュールを除外設定すればいいという話ではなかったので、おまけとして……
TERASOLUNA(5.3.0)のブランクプロジェクトを作成し、HelloController.javaに以下のコードを追加します。

HelloController.java
/** (略 **/
import com.fasterxml.jackson.databind.util.StdDateFormat;

    /** (略 **/

    @RequestMapping(value = "/", method = { RequestMethod.GET, RequestMethod.POST })
    public String home(Locale locale, Model model) {
        logger.info("Welcome home! The client locale is {}.", locale);

        Date date = new Date();
        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
        String formattedDate = dateFormat.format(date);

        // ここから
        Set<DateFormat> set = new HashSet<>();
        set.add(StdDateFormat.instance);
        // ここまで

        model.addAttribute("serverTime", formattedDate);

        return "welcome/home";
    }
}

これをSTS同梱のPivotal tc Server(v3.2)にデプロイしてhttp://localhost:8080/todoにアクセスしてみます。

pivotal_result.PNG

特に問題はありませんね。
では次にJBoss EAP 7.0.0にデプロイしてアクセスしてみましょう。

jboss7_result.PNG

なんかエラーになりました。コンソールにもエラーが出ています。

console.log
15:28:42,923 ERROR [org.terasoluna.gfw.common.exception.ExceptionLogger] (default task-2) [e.xx.fw.9001] UNDEFINED-MESSAGE: java.lang.NullPointerException
    at java.text.DateFormat.hashCode(Unknown Source)
    at java.util.HashMap.hash(Unknown Source)
    at java.util.HashMap.put(Unknown Source)
    at java.util.HashSet.add(Unknown Source)
    at todo.app.welcome.HelloController.home(HelloController.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    (略)

HelloController.javaの38行目はset.add(StdDateFormat.instance);の部分です。

このエラーは報告されていて、jackson-databindの2.7.2で既に修正されています。
TERASOLUNA 5.3.0が利用するjackson-databindのバージョンはスタック一覧にあるように、2.8.5なのですが、なぜこのような問題が起こったのでしょうか?

それは、JBoss EAP 7.0.0に含まれるjackson-databindのバージョンが2.5.4だからです……が、複数のモジュール同士が依存関係にあり、単にjackson-databindを除外してもうまく動かないので、サブシステムJAX-RSを除外します。

参考:Wildfly 9 - How do I exclude Jackson

jboss-deployment-structure.xmlを以下のように設定します。

jboss-deployment-structure.xml
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2">
    <deployment>
        <exclude-subsystems>
            <subsystem name="jaxrs" />
        </exclude-subsystems>
    </deployment>
</jboss-deployment-structure>

では、この状態でJBossを再起動してみます。

jboss7_result.PNG

やったぜ☆