読者です 読者をやめる 読者になる 読者になる

WildFly(RESTEasy) で JAX-RS + JSONP のサンプル

@ さんがサンプル書いてくれました。ありがとうございます!
java-examples/jaxrs-jsonp at master · nekop/java-examples · GitHub

WildFly-8.2.0.Final で試しましたがこれで動きました。
annotations="true" で jackson-provider を指定する必要があります。
Enable JSONP · 30dc57c · nekop/java-examples · GitHub

<jboss-deployment-structure>
  <deployment>
    <dependencies>
      <module name="org.jboss.resteasy.resteasy-jackson-provider" annotations="true"/>
    </dependencies>
  </deployment>
</jboss-deployment-structure>

jboss-deployment-structure.xml は ここらへんを参考に
Class Loading in WildFly - WildFly 8 - Project Documentation Editor

RESTEasy は 2.3.6.Final から JSONP 形式をサポートしたようです。
http://docs.jboss.org/resteasy/docs/2.3.6.Final/userguide/html/json.html#JSONP_Support
それまでは JSONP がサポートされていなかったので、Servlet Filter とかで何とかしてあげる必要がありました。
java - How enable JSONP in RESTEasy? - Stack Overflow

ドキュメントの最新 3.0.9.Final*1 では http://docs.jboss.org/resteasy/docs/3.0.9.Final/userguide/html/json.html#JSONP_Support
org.jboss.resteasy.plugins.providers.jackson.JacksonJsonpInterceptor で出来るようになってるよと書かれています。
今回の問題は、WildFly 上の RESTEasy でどうやって JacksonJsonpInterceptor を有効にするのかが焦点でした。

ここからは試した内容を書いてきます。*2
JAX-RS のリソースクラスはこれを使います。

@Path("hello")
public class HelloResource {
  @GET
  @Produces({ MediaType.APPLICATION_JSON })
  public RestMessage hello() {
    System.out.println("HelloResource#hello");
    return new RestMessage("Hello");
  }
}

この状態で JSON で返すリクエスト(/hello) は正常に動きます。
JSONP で返すリクエスト(/hello?calback=hoge) でも JSON と同じ 「{"messaage":"Hello"}」で返ってきてしまいます。
JSONP で投げた時は、「hoge({"messaage":"Hello"})」の形で返ってきてくれるのが期待している結果です。

resteasy-jackson-provider を 明示的に有効にする (annotations 指定無し)

WildFly では、JacksonJsonpInterceptor が含まれている org.jboss.resteasy.resteasy-jackson-provider モジュールがあります。
これを明示的に有効にして試してみましたが結果は変わらず。
jboss-deployment-structure.xml

<?xml version='1.0' encoding='UTF-8'?>
<jboss-deployment-structure>
  <deployment>
    <dependencies>
      <module name="org.jboss.resteasy.resteasy-jackson-provider" />
    </dependencies>
  </deployment>
</jboss-deployment-structure>

結果的にこれが一番近かったのですが、ここからどんどん迷走していきます。。

MediaType を application/javascript にしてみる

@Produces({ "application/javascript" }) にしてみるとそんな MessgeBodyWriter 無いよとエラーが出ました。

WARN  [org.jboss.resteasy.core.ExceptionHandler] (default task-17) Failed executing GET /hello: org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response object of type: jaxrsjsonp.RestMessage of media type: application/javascript

org.jboss.resteasy.reateasy-jackson2-provider にしてみる

resteasy-jackson2-provider にしてみたらいけるかと試してみましたが、結果変わらず。

<?xml version='1.0' encoding='UTF-8'?>
<jboss-deployment-structure>
  <deployment>
    <exclusions>
      <module name="org.jboss.resteasy.resteasy-jackson-provider" />
    </exclusions>
    <dependencies>
      <module name="org.jboss.resteasy.resteasy-jackson2-provider" />
    </dependencies>
  </deployment>
</jboss-deployment-structure>

war に org.jboss.resteasy.reateasy-jackson2-provider を追加する

WildFly のモジュールでは無く、アプリの war に jackson2-provider を入れてみました。
これが結構不思議な動きをします。

<?xml version='1.0' encoding='UTF-8'?>
<jboss-deployment-structure>
  <deployment>
    <exclusions>
      <module name="org.jboss.resteasy.resteasy-jackson-provider" />
      <module name="org.jboss.resteasy.resteasy-jackson2-provider" />
    </exclusions>
  </deployment>
</jboss-deployment-structure>

※war に突っ込んだせいでは無く、jackson2-provider の動きのようです。

MediaType.APPLICATION_JSON(application/json) のとき

JSON で返すリクエスト(/hello) では正常に動きます。
JSONP で返すリクエスト(/hello?calback=hoge) では、レスポンスは JSONP の形で返ってきます*3が、サーバー側でエラーが出ます。

ERROR [io.undertow.request] (default task-19) UT005023: Exception handling request to /jaxrsjsonp/api/hello: org.jboss.resteasy.spi.UnhandledException: Response is committed, can't handle exception
    at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:148) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.SynchronousDispatcher.writeResponse(SynchronousDispatcher.java:432) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:376) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:179) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:220) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51) [resteasy-jaxrs-3.0.10.Final.jar:]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) [jboss-servlet-api_3.1_spec-1.0.0.Final.jar:1.0.0.Final]
    at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:61) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:56) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:45) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:63) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:58) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:70) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.security.handlers.SecurityInitialHandler.handleRequest(SecurityInitialHandler.java:76) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:261) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:247) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:76) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:166) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:197) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:759) [undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [rt.jar:1.8.0_11]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [rt.jar:1.8.0_11]
    at java.lang.Thread.run(Thread.java:745) [rt.jar:1.8.0_11]
Caused by: java.io.IOException: UT010029: Stream is closed
    at io.undertow.servlet.spec.ServletOutputStreamImpl.write(ServletOutputStreamImpl.java:136) [undertow-servlet-1.1.0.Final.jar:1.1.0.Final]
    at org.jboss.resteasy.plugins.server.servlet.HttpServletResponseWrapper$DeferredOutputStream.write(HttpServletResponseWrapper.java:46) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.util.CommitHeaderOutputStream.write(CommitHeaderOutputStream.java:71) [resteasy-jaxrs-3.0.10.Final.jar:]
    at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221) [rt.jar:1.8.0_11]
    at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291) [rt.jar:1.8.0_11]
    at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:295) [rt.jar:1.8.0_11]
    at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141) [rt.jar:1.8.0_11]
    at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229) [rt.jar:1.8.0_11]
    at org.jboss.resteasy.plugins.providers.jackson.Jackson2JsonpInterceptor.aroundWriteTo(Jackson2JsonpInterceptor.java:111) [resteasy-jackson2-provider-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.interception.AbstractWriterInterceptorContext.proceed(AbstractWriterInterceptorContext.java:122) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.security.doseta.DigitalSigningInterceptor.aroundWriteTo(DigitalSigningInterceptor.java:143) [resteasy-crypto-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.interception.AbstractWriterInterceptorContext.proceed(AbstractWriterInterceptorContext.java:122) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.plugins.interceptors.encoding.GZIPEncodingInterceptor.aroundWriteTo(GZIPEncodingInterceptor.java:100) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.interception.AbstractWriterInterceptorContext.proceed(AbstractWriterInterceptorContext.java:122) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.ServerResponseWriter.writeNomapResponse(ServerResponseWriter.java:99) [resteasy-jaxrs-3.0.10.Final.jar:]
    at org.jboss.resteasy.core.SynchronousDispatcher.writeResponse(SynchronousDispatcher.java:427) [resteasy-jaxrs-3.0.10.Final.jar:]
    ... 32 more

閉じてる Stream に対して何かしようとしてる感じ?
割に結果はちゃんと JSONP の形で受け取れています。

MediaType に application/javascript を追加したとき

@Produces({ "application/javascript", MediaType.APPLICATION_JSON })
JSON/JSONP リクエストともに正常に返ってきました。
サーバー側でもエラー出ていません。
ここで一応 JSONP 返せる事が分かり一安心。

ただ、MediaType.APPLICATION_JSON だけで動いかないのかな?ともう少し試してみました。

war に org.jboss.resteasy.reateasy-jackson-provider を追加する

さっきと同様 WildFly のモジュールを切り、war に jackson-provider を突っ込んでみました。
すると、JSON/JSONP リクエストともに正常に返ってきました。
サーバー側でもエラー出ていません。
しかし、WildFly で持ってるのに war側で入れるのは何か変だよなーとなってるところで @nekop さんがサンプルを提供してくれました。

結論

jboss-deployment-structure 大事!!

*1:WildFly-8.2.0.Final の RESTEasy は 3.0.10.Final

*2:もっと色々試したはずなんですが、再現せずなのでこんだけ

*3:Status:200