これの続き。
この記事の終わりの辺りで、
- GensonをJerseyで使おう!
- あっ、それなら今超絶流行りのPayara Microでやってみよう!
という流れになったので、Payara MicroでGensonを使えるようにするまでの話。
そもそもPayara Microって?
はすぬまさんに聞いて下さい。
はすぬまさんのこの辺りの記事がグーグルでもトップに出てきます。
あとはかずひらさんがやたらと詳しい(と勝手に思ってる)。
Payara MicroのJAX-RSのJSONシリアライザー
- Jackson
- MOXy
の順番で使われるっぽい。
- payara-micro.jarの中に含まれている
META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable
にこの順番で認識するような書き方してあった payara-micro.jar
のMETA-INF/services/javax.ws.rs.ext.MessageBodyWriter
とMETA-INF/services/javax.ws.rs.ext.MessageBodyReader
でJacksonが指定されていた
まあ、実際に作ったwarファイルをデプロイした時に、genson.jar
のMETA-INF/services
の中にjavax.ws.rs.ext.MessageBodyWriter
があるのに、GensonのJSONシリアライザーのクラスGensonJsonConverter
が全然使われていないことから、stackoverflowを調べていくうちに、glassfishはservice loaderに登録されたら変更されない的なことが書かれているQAを見つけたので(要出典)、payara-micro.jar
の中身を見たらそうなってたというだけでした。
いや、もうJacksonもMOXyも使わせない
僕はgradleっ子なので、payara-micro.jarをダウンロードしてコピーする際に、その辺のファイルの書き換えをgradleにさせます。
作戦は次のとおり
- jarファイルの中からファイルを取り除いたりするので、ベースとなるタスクは
Copy
ではなくJar
タスク - payara-micro.jarの中から邪魔なservice loader用のファイルを取り除く
- 使わないし、どうせだからjarのサイズを小さくするためにJacksonを取り除く
- payara-micro.jarの
org.glassfish.jersey.internal.spi.AutoDiscoverable
にはserver sent eventの実装クラスの指定があるので、単純には取り除けないが、genson自体にもorg.glassfish.jersey.internal.spi.AutoDiscoverable
があるのでこいつは自作する
dependency戦略
で、この作戦を満たすための依存性の戦略は次のようになります。
- gensonは
provided
にする - payara-micro自体はコンパイルには不要なので、単純にコピー専用に指定する
というわけで、dependenciesは次のような記述になりました。
ext { payaraVersion = '4.1.152.1' } configurations { genson payaraMicro } dependencies { genson 'com.owlike:genson:1.3' payaraMicro "fish.payara.extras:payara-micro:${payaraVersion}" providedCompile 'javax:javaee-api:7.0' providedCompile configurations.genson }
作戦の4.org.glassfish.jersey.internal.spi.AutoDiscoverable
を満たす
org.glassfish.jersey.internal.spi.AutoDiscoverable
ファイルは自分で出力します。
- 外していいのは
JacksonFeature
とMOXy
なんとか SseAutoDiscoverable
とServerFiltersAutoDiscoverable
は残す- gensonの
JerseyAutoDiscoverable
を含むようにする - このファイルはほとんど変わることないし、できれば
clean
タスクをやった後以外はUP-TO-DATE
にしたい - 他のタスクから成果物のファイル名を取得できるようにしたい
この辺を考慮すると、AutoDiscoverable
を出力するタスクは次のようになります。
ext { autoDiscoverable = 'org.glassfish.jersey.internal.spi.AutoDiscoverable' } import java.nio.file.FIles task generateAutoDiscoverable { // ファイルの設定 def metaInf = file("${buildDir}/${name}/META-INF").toPath() def outFile = file("${buildDir}/${name}/META-INF/${autoDiscoverable}") // 書き出す内容 def contents = $/com.owlike.genson.ext.jaxrs.JerseyAutoDiscoverable org.glassfish.jersey.media.sse.internal.SseAutoDiscoverable org.glassfish.jersey.server.filter.internal.ServerFiltersAutoDiscoverable /$ // タスクの出力ファイルを他のタスクから参照可能にする outputs.files outFile // 不要なタスク実行を避ける outputs.upToDateWhen { if (outFile.exists()) { return outFile.text == contents } else { return false } } // タスクの処理 : ファイルの書き出し doLast { if (!Files.exists(metaInf)) { Files.createDirectories(metaInf) } outFile.write(contents, 'UTF-8') } }
これで、
- 他のタスクから成果物を参照できて
- 不要なタスク実行を避けられて
- 必要なクラスだけをservice loaderに読み込ませるファイルが出力出来ました。
作戦の1.を満たす
タスクはJar
なので、このようなタスクになります。
また、jarをコピーする際に、出力したorg.glassfish.jersey.internal.spi.AutoDiscoverable
ファイルを参照するために先ほどのタスクの後に実行されるようにします。
task copyPayaraMicro(type: Jar, dependsOn: 'generateAutoDiscoverable') {
}
作戦の2.、3.を満たす
取り除きたいのは
- payara-microにある不要な
META-INF/services
の下のファイル - payara-microにあるJacksonのクラス群
- gensonにあるgensonだけの設定しかない
org.glassfish.jersey.internal.spi.AutoDiscoverable
ですので、from
とzipTree(File).matching(Closure)
を使ってうまく取り除いていきます。
task copyPayaraMicro(type: Jar, dependsOn: 'generateAutoDiscoverable') { // payara-microをコピーする from configurations.payaraMicro.collect { it.isDirectory() ? it : zipTree(it).matching { // Jacksonは要らない子 exclude '**/com/fasterxml/jackson/**' // MessageBodyWriter/Readerはgensonのを使う exclude '**/META-INF/services/javax.ws.rs.ext.Message*' // AutoDiscoverableは自前のを使う exclude '**/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable' } } // gensonをコピーする from configurations.genson.collect { it.isDirectory() ? it : zipTree(it).matching { // AutoDiscoverableは自前のを使う exclude '**/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable' } } }
自作したAutoDiscoverable
を使うために、タスクgenerateAutoDiscoverable
の成果物をコピーをします。
task copyPayaraMicro(type: Jar, dependsOn: 'generateAutoDiscoverable') { // 上で書いたやつは省略 // 自前で書きだしたAutoDiscoverableをMETA-INF/servicesにコピー from(tasks.generateAutoDiscoverable.outputs.files) { into 'META-INF/services' } }
あと、Jarをアンアーカイブしてからアーカイブするタスクはくっそ時間がかかるので、これも不要なときはUP-TO-DATE
するようにします。
ext { payaraVersion = '4.1.152.1' payaraDir = "build/payara-micro" payaraMicroJar = 'payara-micro.jar' payaraVersionFile = file("${payaraDir}/payaraVersion") } task copyPayaraMicro(type: Jar, dependsOn: 'generateAutoDiscoverable') { // 上で書いたやつは省略 // UP-TO-DATEの条件 outputs.upToDateWhen { if (payaraVersionFile.exists()) { return payaraVersionFile.text == payaraVersion } else { return false } } // payara-microのバージョンをテキストファイルに書いておく。 // payara-microのバージョンが変わったらバージョンの上がったやつがコピーされるようになる doLast { if (!file(payaraDir).exists()) { file(payaraDir).mkdir() } payaraVersionFile.write(payaraVersion, 'UTF-8') } }
あと、注意しないといけないのは、Jar
タスクでコピーした場合、META-INF/MANIFEST.MF
はなくなるので、これも自分で書き出すようにしておかないといけないです。
task copyPayaraMicro(type: Jar, dependsOn: 'generateAutoDiscoverable') { // 上で書いたやつは省略 manifest { attributes 'Main-Class': 'fish.payara.micro.PayaraMicro', 'Bundle-SymbolicName': 'fish.payara.micro' } }
まあ、必要なのはMain-Class
属性だけだったりする。
これでcopyPayaraMicro
タスクを実行すれば、俺好みのjarが出来上がりますが、まあ、ここまでやれば、どうせwar
タスクも実行することになるので、それらをまとめあげて、かつ実行コマンドを表示してくれるようなタスクを作ってしまいましょう。
war { archiveName = "${project.name}.war" } task stage(dependsOn: ['war', 'copyPayaraMicro']) { doLast { def dir = file(payaraDir).toPath() def name = Files.list(dir).map { it.fileName.toString() }.filter{ it.endsWith('.jar') }.findAny().orElse(payaraMicroJar) println "You can run app with command..." println "java -jar ${payaraDir}/${name} --deploy build/libs/${project.name}.war" } }
よし、おもむろに実行じゃ
適当なJAX-RSアプリケーションを作ります。
@ApplicationPath("contents") public class JaxRsApp extends Application { }
@Path("run") @RequestScoped public class RunRecordResource { private static final Logger LOGGER = LoggerFactory.getLogger(RunRecordResource.class); @Inject @ZoneQualifier private ZoneId zone; @GET @Produces("application/json") public RunRecord rec(@QueryParam("metres") Integer metres) { LocalDateTime now = LocalDateTime.now(zone); ZoneOffset offset = zone.getRules().getOffset(now); Instant instant = now.toInstant(offset); Date date = Date.from(instant); RunRecord runRecord = new RunRecord(metres, date); LOGGER.debug("request {}", runRecord); return runRecord; } public void setZone(ZoneId zone) { this.zone = zone; } }
@ApplicationScoped public class ZoneProducer { @Produces @ZoneQualifier public ZoneId getZone() { return ZoneId.of("Asia/Tokyo"); } }
これをビルドして、実行します。
$ gradle --daemon clean stage :clean :generateAutoDiscoverable :copyPayaraMicro :compileJava :processResources :classes :war :stage You can run app with command... java -jar build/payara-micro/payara-micro.jar --deploy build/libs/genson-sample.war BUILD SUCCESSFUL Total time: 33.518 secs $ java -jar build/payara-micro/payara-micro.jar --deploy build/libs/genson-sample.war ...省略... [2015-07-27T00:15:35.887+0900] [Payara 4.1] [INFO] [] [fish.payara.micro.PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1437923735887] [levelValue: 800] Deployed 1 wars
というわけで、アクセスしてみます。
$ curl -X GET 'http://localhost:8080/genson-sample/contents/run?metres=200' {"date":1437928696467,"metres":200} $
う、日付がlong
で返ってきおった(´・ω・`)
GensonはデフォルトではuseDateAsTimestamp
がtrue
に設定されているので、Date
をフォーマットしたい場合は、もう少しいろいろといじらないといけないようです(具体的にはMessageBodyWriter
をMETA-INF/services
にいれずにResourceConfig
でゴニョるか、サブプロジェクトを作って、そいつにお好みのGensonを生成させるか)。
どちらかだろうなーと思いつつ、眠くて適当になり始めたので、それについては、解決できたらブログ書きます。
もっと簡単な方法あるんだろうけど、おっさんで勉強嫌いでスキル低いので、膨大なスクリプトになった。人生後悔してる。
なおビルドスクリプトの全体は、こちらからどうぞ。
payara-microのJSONシリアライザーを変更するビルドスクリプト · GitHub
おわり