Java11環境でSOAPサービスを呼び出そうとしたら大変だった

Java11 上で動かしている JBoss (Red Hat SSO) 環境で、SOAP サービスを呼び出そうとしたら、とても大変だったのでそのまとめです。

環境

  • AdoptOpenJDK Hotspot 11.0.11+9
  • Red Hat SSO 7.4 (JBoss EAP 7.3.7)
  • Apache CXF 3.4.3

ひとつめの問題

Red Hat SSO に、カスタムプロバイダ (認証機能を拡張するモジュール) を載せた状態で当該機能にアクセスした際、以下のような例外が発生しました。

08:03:12,265 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-5) Uncaught server error: javax.xml.ws.WebServiceException: Provider com.sun.xml.internal.ws.spi.ProviderImpl not found
     at javax.xml.ws.spi.FactoryFinder$1.createException(FactoryFinder.java:31)
     at javax.xml.ws.spi.FactoryFinder$1.createException(FactoryFinder.java:28)
     at javax.xml.ws.spi.ServiceLoaderUtil.newInstance(ServiceLoaderUtil.java:73)
     at javax.xml.ws.spi.FactoryFinder.find(FactoryFinder.java:82)
     at javax.xml.ws.spi.Provider.provider(Provider.java:66)
     at javax.xml.ws.Service.<init>(Service.java:82)

...

Caused by: java.lang.ClassNotFoundException: com.sun.xml.internal.ws.spi.ProviderImpl from [Module "deployment.keycloak-server.war" from Service Module Loader]
     at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:255)
     at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:410)
     at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:398)
     at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:116)
     at javax.xml.ws.spi.ServiceLoaderUtil.nullSafeLoadClass(ServiceLoaderUtil.java:60)
     at javax.xml.ws.spi.ServiceLoaderUtil.safeLoadClass(ServiceLoaderUtil.java:93)
     at javax.xml.ws.spi.ServiceLoaderUtil.newInstance(ServiceLoaderUtil.java:71)
     ... 88 more

com.sun.xml.internal.ws.spi.ProviderImpl が見つからないのだそうです。 これは、Java8 以前のバージョンで、rt.jar の中に含まれていたクラスです。 Java9 以降では rt.jar がランタイムに含まれなくなったため、当該クラスが見つからずに例外をスローした、ということのようです。

これを解消するには、com.sun.xml.internal.ws.spi.ProviderImpl に代わるクラスを用意して、用意されたクラスが使用されればよいです。 代わるクラスについては、JEP 320 でも言及されており、以下のような流れで対応します。

  1. com.sun.xml.ws:jaxws-ri:2.3.1pom スコープで依存に追加します (執筆時点で、2.x の最新は 2.3.4 ですが、使用すると別の ClassNotFoundException が発生するため回避します *後述)
  2. com.sun.xml.bind:jaxb-ri:2.3.1pom スコープで依存に追加します (こちらは必要に応じて追加します)
  3. アプリケーションの起動スクリプト等に、システムプロパティ -Djavax.xml.ws.spi.Provider=com.sun.xml.ws.spi.ProviderImpl を設定します。

ふたつめの問題

ここまで対応しても、以下の通り例外は解消されません。しかし着実に前進はしています。 例外内容が変わらないため、効果のない対応だったのではないかと、見誤ってしまいそうになりますが、大丈夫です。

11:01:37,324 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-2) Uncaught server error: javax.xml.ws.WebServiceException: Provider com.sun.xml.ws.spi.ProviderImpl not found
     at javax.xml.ws.spi.FactoryFinder$1.createException(FactoryFinder.java:31)
     at javax.xml.ws.spi.FactoryFinder$1.createException(FactoryFinder.java:28)
     at javax.xml.ws.spi.ServiceLoaderUtil.newInstance(ServiceLoaderUtil.java:73)
     at javax.xml.ws.spi.FactoryFinder.fromSystemProperty(FactoryFinder.java:92)
     at javax.xml.ws.spi.FactoryFinder.find(FactoryFinder.java:69)
     at javax.xml.ws.spi.Provider.provider(Provider.java:66)
     at javax.xml.ws.Service.<init>(Service.java:82)

...

Caused by: java.lang.ClassNotFoundException: com.sun.xml.ws.spi.ProviderImpl from [Module "deployment.keycloak-server.war" from Service Module Loader]
     at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:255)
     at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:410)
     at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:398)
     at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:116)
     at javax.xml.ws.spi.ServiceLoaderUtil.nullSafeLoadClass(ServiceLoaderUtil.java:60)
     at javax.xml.ws.spi.ServiceLoaderUtil.safeLoadClass(ServiceLoaderUtil.java:93)
     at javax.xml.ws.spi.ServiceLoaderUtil.newInstance(ServiceLoaderUtil.java:71)
     ... 89 more

これは、JBoss のクラスローダーに、対応したクラスがロードされていないことに起因します。 ここでは、例外が発生する処理の前後でクラスローダーを切り替えて対応します。 必要なクラスはすでに依存に含まれているため、適切なクラスローダーに切り替えることで、対象のクラスを見つけられるようになります。

今回は、以下のような WebService クラスから Port オブジェクトを取得する処理中に、 例外が発生していたため、この前後でクラスローダーを切り替えます。

変更前は以下のような実装です。

new DemoService(new URL(wsdlLocation)).getDemoPort();

これを以下のように書き換えます。

ClassLoader targetClassLoader = DemoService.class.getClassLoader();
Thread currentThread = Thread.currentThread();
ClassLoader contextClassLoader = currentThread.getContextClassLoader();
try {
    currentThread.setContextClassLoader(targetClassLoader);

    new DemoService(new URL(wsdlLocation)).getDemoPort();
} finally {
    currentThread.setContextClassLoader(contextClassLoader);
}

あとから聞いた話によると、このようなテクニックは、JBoss 他、アプリケーションサーバー内の実装では度々行われているそうで、 アプリケーションサーバー上で動作するユーザーアプリケーションのために、実行環境を整える必要があるような場合に、 同様にクラスローダーを切り替えるのだそうです。

みっつめの問題

これはここまでの流れに従っている場合は発生しません。 [[ひとつめの問題]] の対応時に、依存するモジュールのバージョンを誤っていた場合に発生するものです。

com.sun.xml.ws:jaxws-ri:2.3.1 を誤って、com.sun.xml.ws:jaxws-ri:2.3.4 に依存していた場合に、この問題が発生します。 一見パッチバージョンのアップデートに見える 2.3.4 ですが、後方互換性を失っているようで、以下のような例外が発生してしまいます。

16:59:49,586 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-4) Uncaught server error: java.lang.NoClassDefFoundError: com/oracle/webservices/api/message/AccessorFactory$MethodHandleAccessor
     at com.oracle.webservices.api.message.AccessorFactory.createAccessor(AccessorFactory.java:43)
     at com.oracle.webservices.api.message.BasePropertySet.lambda$parse$0(BasePropertySet.java:176)
     at java.base/java.security.AccessController.doPrivileged(Native Method)
     at com.oracle.webservices.api.message.BasePropertySet.parse(BasePropertySet.java:148)
     at com.oracle.webservices.api.message.BasePropertySet.parse(BasePropertySet.java:130)
     at com.sun.xml.ws.client.RequestContext.<clinit>(RequestContext.java:361)
     at com.sun.xml.ws.client.Stub.<init>(Stub.java:136)
     at com.sun.xml.ws.client.Stub.<init>(Stub.java:213)
     at com.sun.xml.ws.client.Stub.<init>(Stub.java:228)
     at com.sun.xml.ws.client.sei.SEIStub.<init>(SEIStub.java:68)
     at com.sun.xml.ws.client.WSServiceDelegate.getStubHandler(WSServiceDelegate.java:791)
     at com.sun.xml.ws.client.WSServiceDelegate.createEndpointIFBaseProxy(WSServiceDelegate.java:780)
     at com.sun.xml.ws.client.WSServiceDelegate.getPort(WSServiceDelegate.java:422)
     at com.sun.xml.ws.client.WSServiceDelegate.getPort(WSServiceDelegate.java:390)
     at com.sun.xml.ws.client.WSServiceDelegate.getPort(WSServiceDelegate.java:372)
     at javax.xml.ws.Service.getPort(Service.java:139)

...

Caused by: java.lang.ClassNotFoundException: com.oracle.webservices.api.message.AccessorFactory$MethodHandleAccessor
     at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471)
     at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
     at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
     ... 101 more

こちらへの対応は、前述の通り、com.sun.xml.ws:jaxws-ri の依存バージョンを 2.3.1 に修正すればよいです。

まとめ

以上ですべての対応は完了です。 ここまでをまとめると、一部のアプリケーションサーバー上で SOAP 関連のモジュールを動かそうとして例外が発生した場合には、以下を試してみると良いでしょう。

  • com.sun.xml.ws:jaxws-ri:2.3.1pom スコープで依存に追加する
  • com.sun.xml.bind:jaxb-ri:2.3.1pom スコープで依存に追加する
  • システムプロパティ -Djavax.xml.ws.spi.Provider=com.sun.xml.ws.spi.ProviderImpl を設定する
  • 例外が発生する処理の前後でコンテキストクラスローダーをアプリケーション由来のものに切り替える

参考

avatar

Written by yo1000 | YO!CHI KIKUCHI
Loves 🌱 Spring, 🦢 Pelikan fountain pen and 🦁 FINAL FANTASY VIII