舞台裏

Qiita が表でこっちが裏。こっそりやっていく。

JJUG CCC 2017 Fall でしゃべってきた

JJUG CCC 2017 Fall に行って、1セッション・1LT をしゃべってきたので、その報告というか裏話というか。

Spring Security にできること・できないこと

qiita.com

こちらは半年前のKANJAVA PARTY で一度話したやつでした。

もともと 2017 Spring でも何か発表できないかなぁと考えていたのですが、ネタが思いつかず CfP の期限も過ぎたので、 Spring での登壇は断念しました。
しかし、同じころ KANJAVA PARTY での発表が決まり、こちらはネタが思いついて登壇することになりました。
そして、これをそのまま CCC 2017 Fall に応募して当選すれば、資料はできてるし一回発表したから感覚もつかめているしで、すごい楽な気持ちで参加できるんじゃないかなぁと思い CfP に応募しました。

結果 CfP は無事通り、思惑通り?去年の初登壇と比べるとかなり楽な気持ちで発表に挑むことができました。
(一応、大阪での発表の際にいくつか質問があったので、その分の加筆修正は入れていました)

大阪での発表のときは、急いで話しすぎたため予定より大幅に時間を残してセッションを終了してしまうという失敗がありました。
なので、今回は時間配分を考え、なるべくゆっくり話すようにして時間通りに終わらせよう、というリベンジ的な目標がありました(大阪での発表もそうしとけよという話ではある) 。
結果は、5分残しの40分で終えられたので、目標は達成できたかなぁと思ってほっとしてます。

ありがとうございます、ありがとうございます!

Spring Boot の容量を減らしてみた (懇親会 LT)

qiita.com

Spring Security のやつは再演だったので、個人的にはこっちのほうが本番でした。

実はこのネタはもともと KANJAVA PARTY の懇親会 LT でやろうと思って作っていたのですが、気付いたときには LT の発表枠が埋まっていてお蔵入りになっていました。

しかし、いずれはどこかでやりたいと思っていたので、 CCC の CfP に出すときに LT 枠で応募することにしました。
結果は LT も含め両方とも当選したので、半年の充電期間を経て日の目を見ることができました。

苦労話

LT では時間が無くて話すことはできませんでしたが、あのネタの実現にはかなり苦労しました。
この苦労話はそれはそれで頭おかしいので、せっかくなのでこちらのブログ記事で補完したいと思います。

ソースコードの収集!

まずはなによりソースコードの収集が必要です。
Spring Boot の Hello World の依存関係は、 Gradle だと以下のようになります。

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
}

当たり前ですが、必要なソースはこれだけではないです。
推移的な依存関係の解決によって、実際は↓のライブラリが必要になります。

\--- org.springframework.boot:spring-boot-starter-web: -> 1.5.6.RELEASE
     +--- org.springframework.boot:spring-boot-starter:1.5.6.RELEASE
     |    +--- org.springframework.boot:spring-boot:1.5.6.RELEASE
     |    |    +--- org.springframework:spring-core:4.3.10.RELEASE
     |    |    \--- org.springframework:spring-context:4.3.10.RELEASE
     |    |         +--- org.springframework:spring-aop:4.3.10.RELEASE
     |    |         |    +--- org.springframework:spring-beans:4.3.10.RELEASE
     |    |         |    |    \--- org.springframework:spring-core:4.3.10.RELEASE
     |    |         |    \--- org.springframework:spring-core:4.3.10.RELEASE
     |    |         +--- org.springframework:spring-beans:4.3.10.RELEASE (*)
     |    |         +--- org.springframework:spring-core:4.3.10.RELEASE
     |    |         \--- org.springframework:spring-expression:4.3.10.RELEASE
     |    |              \--- org.springframework:spring-core:4.3.10.RELEASE
     |    +--- org.springframework.boot:spring-boot-autoconfigure:1.5.6.RELEASE
     |    |    \--- org.springframework.boot:spring-boot:1.5.6.RELEASE (*)
     |    +--- org.springframework.boot:spring-boot-starter-logging:1.5.6.RELEASE
     |    |    +--- ch.qos.logback:logback-classic:1.1.11
     |    |    |    +--- ch.qos.logback:logback-core:1.1.11
     |    |    |    \--- org.slf4j:slf4j-api:1.7.22 -> 1.7.25
     |    |    +--- org.slf4j:jcl-over-slf4j:1.7.25
     |    |    |    \--- org.slf4j:slf4j-api:1.7.25
     |    |    +--- org.slf4j:jul-to-slf4j:1.7.25
     |    |    |    \--- org.slf4j:slf4j-api:1.7.25
     |    |    \--- org.slf4j:log4j-over-slf4j:1.7.25
     |    |         \--- org.slf4j:slf4j-api:1.7.25
     |    +--- org.springframework:spring-core:4.3.10.RELEASE
     |    \--- org.yaml:snakeyaml:1.17
     +--- org.springframework.boot:spring-boot-starter-tomcat:1.5.6.RELEASE
     |    +--- org.apache.tomcat.embed:tomcat-embed-core:8.5.16
     |    +--- org.apache.tomcat.embed:tomcat-embed-el:8.5.16
     |    \--- org.apache.tomcat.embed:tomcat-embed-websocket:8.5.16
     |         \--- org.apache.tomcat.embed:tomcat-embed-core:8.5.16
     +--- org.hibernate:hibernate-validator:5.3.5.Final
     |    +--- javax.validation:validation-api:1.1.0.Final
     |    +--- org.jboss.logging:jboss-logging:3.3.0.Final -> 3.3.1.Final
     |    \--- com.fasterxml:classmate:1.3.1 -> 1.3.3
     +--- com.fasterxml.jackson.core:jackson-databind:2.8.9
     |    +--- com.fasterxml.jackson.core:jackson-annotations:2.8.0
     |    \--- com.fasterxml.jackson.core:jackson-core:2.8.9
     +--- org.springframework:spring-web:4.3.10.RELEASE
     |    +--- org.springframework:spring-aop:4.3.10.RELEASE (*)
     |    +--- org.springframework:spring-beans:4.3.10.RELEASE (*)
     |    +--- org.springframework:spring-context:4.3.10.RELEASE (*)
     |    \--- org.springframework:spring-core:4.3.10.RELEASE
     \--- org.springframework:spring-webmvc:4.3.10.RELEASE
          +--- org.springframework:spring-aop:4.3.10.RELEASE (*)
          +--- org.springframework:spring-beans:4.3.10.RELEASE (*)
          +--- org.springframework:spring-context:4.3.10.RELEASE (*)
          +--- org.springframework:spring-core:4.3.10.RELEASE
          +--- org.springframework:spring-expression:4.3.10.RELEASE (*)
          \--- org.springframework:spring-web:4.3.10.RELEASE (*)

これらのソースを手で入手するのはどう考えても現実的ではないです。
そこで、 Gradle の依存 jar を落としてくる仕組みを流用してソースコードを入手できないか調べたのですが、意外と直接的にそういうことができる仕組みはなさそうでした。

なので、依存 jar がキャッシュされているフォルダからソースコードをかき集めてくるタスクを作ることで解決しました。
(なんか、 Maven だとソースを入手する簡単な方法があるらしい)

import groovy.io.FileType

task('collectSource', dependsOn: 'eclipse') { // eclipse タスクがソースコードを落としてくるので dependsOn に設定しておくしておく
    configurations.runtime.each { jarFile ->
        jarFile.parentFile.parentFile.eachFileRecurse(FileType.FILES) { file ->
            if (file.name =~ /^.*\.jar$/ && file.name.contains('source')) {
                copy {
                    from file
                    into "$buildDir/dependencies/packaged/source"
                }
            }
        }
    }
}

コンパイルが通らない!

ソースコードが入手できたので、これで解決かと思ったら、そうは問屋はおろさないです。

推移的な依存関係の解決で入手したソースコードコンパイルすると、必要なソースがないためコンパイルが通りません。
例えば、 Spring Boot のソースコードには AutoConfiguration クラスが入っているため、実行時には使用しなくても様々なライブラリと依存しています。

それ以外にも、依存するソースがないためコンパイルエラーになるソースは多くありました。

それらを全て洗い出して、必要なソースコードを集めるのは大変すぎます。
なので、コンパイルエラーになるソースコードコンパイルをあきらめて、 class ファイルを jar のなかに置くようにしました。

つまり、改良版 jar の中身は全てが java ファイルではなく、一部のコンパイルができなかったクラスは class ファイルになっています。

ちなみに、 java ファイルと class ファイルは1対1の関係になっていません。
インナークラスがあると、インナークラスは $ で区切られた別 class ファイルとして出力されるからです(例えば Map.java からは Map.classMap$Entry.class が出力される)。

その辺も考慮したうえで、ファイルの差し替えが必要でした。

一回のコンパイルでは終わらない!

前述のように、コンパイルエラーになったソースは class ファイルに差し替える必要があるわけですが、コンパイルエラーの数が多いと一度のコンパイルで全てのエラーを出し切ることはできませんでした。
(もしかしたらオプションか何かで調整できたのかもしれませんが)

仕方ないので、

  1. コンパイルする
  2. エラーになった java ソースを class に置き換える
  3. 1, 2 をコンパイルエラーが無くなるまで繰り返す

というループをだいたい 40 回くらい繰り返す必要がありました。

実行時に NoClassDefError!

ここまで頑張ってコンパイルしても、実行すると NoClassDefError が発生しました。

原因は、 jar ファイルの中には class ファイルが入っているのに、 Maven リポジトリから落としてきたソースファイルの中には対応する java ファイルが存在しない、というライブラリが存在したからでした。

なぜそういう状態なのかは分からないですが、仕方ないのでそういうクラスは発見しだい class ファイルを配置するようにしました。

Slf4J の罠!

Slf4J の slf4j-api-x.x.x.jar には、 UnsupportedOperationException をスローする実装クラスが用意されています。

org.slf4j.impl.StaticLoggerBinder.java

...
public class StaticLoggerBinder {

    ...

    private StaticLoggerBinder() {
        throw new UnsupportedOperationException("This code should have never made it into slf4j-api.jar");
    }

    public ILoggerFactory getLoggerFactory() {
        throw new UnsupportedOperationException("This code should never make it into slf4j-api.jar");
    }

    public String getLoggerFactoryClassStr() {
        throw new UnsupportedOperationException("This code should never make it into slf4j-api.jar");
    }
}

一方、 Logbacklogback-classic-x.x.x.jar にも同じ FQCNソースコードが配置されています。

...
public class StaticLoggerBinder implements LoggerFactoryBinder {

    ...

    public ILoggerFactory getLoggerFactory() {
        if (!initialized) {
            return defaultLoggerContext;
        }

        if (contextSelectorBinder.getContextSelector() == null) {
            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
        }
        return contextSelectorBinder.getContextSelector().getLoggerContext();
    }

    public String getLoggerFactoryClassStr() {
        return contextSelectorBinder.getClass().getName();
    }

}

こちらは処理が実装されています。

依存ライブラリのソースコードをかき集めるときに、いったん1か所(フォルダ)にソースコードをかき集める処理をしていました。
この問題に気付かなかったときは、 Slf4J が持つソースコードLogbackソースコードを上書きしてしまっていたため、実行時に UnsupportedOperationException がスローされてしまいました。

仕方ないので、ソースコードをかき集める Gradle のタスクの中で、 Slf4J より Logback のソースが優先されるように調整することにしました。

ソースコードの圧縮!

ソースコードにはコメントやスペース、改行という無駄な記述が多くあります。

これを除去するために、最初はソースを1文字ずつ読み込みながら不要と思われる部分を除去する処理を実装していました。

しかし、それも限界を感じたので JavaParser という構文解析ライブラリを使用することにしました。
これによって、コメントや private 修飾子を確実に削除できるようになりました。

ただ、空白スペースと改行の扱いについては、 JavaParser が標準で用意している仕組みだけでは足りませんでした。

JavaParser には PrettyPrintVisitor という AST を綺麗に整形した Java ソースに戻すためのクラスが用意されています。
これにはインデントサイズや改行文字を指定する方法が提供されています。

それぞれに空文字と空白スペースを設定することで、インデントと改行を消すことができます。 しかし、より細かいスペースの除去はうまくいきません。

たとえば、

    public String method(int aaa, int bbb) throws Exception {
        ...
    }

というコードがあった場合、インデントと改行の削除だけだと int aaa, int bbb のカンマの後ろのスペースや、 ) throws Exception {throws の前と Exception の後ろのスペースは削除できません。

これらのスペースは、上述の PrettyPrintVisitor の中にハードコーディングされているため、標準で用意されている API では変更できません。

仕方ないので、 PrettyPrintVisitor を継承して除去可能なスペースを全て除去する独自のクラスを作成することにしました。


とまぁ、いろいろな壁を乗り越えた結果、なんとかあの jar ファイルを作成することができました。

なお、この jar を作成するための手順を何度も手で実行するのはあまりにも大変なので、ツール化して簡単に実行できるようにしています。

lightweight-jar | GitHub

以上、 Spring Boot の容量を減らすために四苦八苦した話でした。


ところで、今回会場が一方通行になったことで前回までの混雑が驚くほど解消されてて衝撃的でした。