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

# 確認環境

JBoss EAP 6.4.0, 7.0.0, 7.1.0beta1


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

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

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


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

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

`{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)の[ブランクプロジェクト](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/ImplementationAtEachLayer/CreateWebApplicationProject.html)を作成し、`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](http://localhost:8080/todo)にアクセスしてみます。

![pivotal_result.PNG](https://qiita-image-store.s3.amazonaws.com/0/162142/e5a92f03-7271-11eb-a057-57d797081fc4.png)

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

![jboss7_result.PNG](https://qiita-image-store.s3.amazonaws.com/0/162142/0265ef0c-e6c1-0836-bf03-bb3cdef3d7f6.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);`の部分です。

このエラーは[報告されていて](https://github.com/FasterXML/jackson-databind/issues/1130)、jackson-databindの2.7.2で既に修正されています。
TERASOLUNA 5.3.0が利用するjackson-databindのバージョンは[スタック一覧](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/Overview/FrameworkStack.html)にあるように、2.8.5なのですが、なぜこのような問題が起こったのでしょうか？

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

参考：[Wildfly 9 - How do I exclude Jackson](https://stackoverflow.com/questions/37060294/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](https://qiita-image-store.s3.amazonaws.com/0/162142/7e9574b5-f8df-0bde-003a-5bfd804fdd64.png)

やったぜ☆