payara-microのJSONシリアライザーをGensonにするために、なかなかハマった件
これの続き。
この記事の終わりの辺りで、
- 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
おわり