mike-neckのブログ

Java or Groovy or Swift or Golang

payara-microのJSONシリアライザーをGensonにするために、なかなかハマった件

mike-neck.hatenadiary.com

これの続き。

この記事の終わりの辺りで、

  • GensonをJerseyで使おう!
  • あっ、それなら今超絶流行りのPayara Microでやってみよう!

という流れになったので、Payara MicroでGensonを使えるようにするまでの話。


そもそもPayara Microって?

はすぬまさんに聞いて下さい。

www.coppermine.jp

はすぬまさんのこの辺りの記事がグーグルでもトップに出てきます。

あとはかずひらさんがやたらと詳しい(と勝手に思ってる)。

Payara MicroのJAX-RSJSONリアライザー

  1. Jackson
  2. MOXy

の順番で使われるっぽい。

  • payara-micro.jarの中に含まれているMETA-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverableにこの順番で認識するような書き方してあった
  • payara-micro.jarMETA-INF/services/javax.ws.rs.ext.MessageBodyWriterMETA-INF/services/javax.ws.rs.ext.MessageBodyReaderでJacksonが指定されていた

まあ、実際に作ったwarファイルをデプロイした時に、genson.jarMETA-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にさせます。

作戦は次のとおり

  1. jarファイルの中からファイルを取り除いたりするので、ベースとなるタスクはCopyではなくJarタスク
  2. payara-micro.jarの中から邪魔なservice loader用のファイルを取り除く
  3. 使わないし、どうせだからjarのサイズを小さくするためにJacksonを取り除く
  4. 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ファイルは自分で出力します。

  • 外していいのはJacksonFeatureMOXyなんとか
  • SseAutoDiscoverableServerFiltersAutoDiscoverableは残す
  • 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

ですので、fromzipTree(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はデフォルトではuseDateAsTimestamptrueに設定されているので、Dateをフォーマットしたい場合は、もう少しいろいろといじらないといけないようです(具体的にはMessageBodyWriterMETA-INF/servicesにいれずにResourceConfigでゴニョるか、サブプロジェクトを作って、そいつにお好みのGensonを生成させるか)。

どちらかだろうなーと思いつつ、眠くて適当になり始めたので、それについては、解決できたらブログ書きます。


もっと簡単な方法あるんだろうけど、おっさんで勉強嫌いでスキル低いので、膨大なスクリプトになった。人生後悔してる。

なおビルドスクリプトの全体は、こちらからどうぞ。

payara-microのJSONシリアライザーを変更するビルドスクリプト · GitHub

おわり