GradleでProGuardを使う

Gradleでは[ProGuard](https://www.guardsquare.com/en/products/proguard/manual)を使ったことがなかったので、やり方をメモ。

注意: この記事はAndroidについてでは*ありません*。GradleでProGuardというとAndroid向けの記事ばっかり出てきますが、これは違います。


## `build.gradle`

```gradle
buildscript {
    dependencies {
        classpath(
                'net.sf.proguard:proguard-gradle:6.0.3'
        )
    }
}

// 中略...

jar {
    manifest {
        attributes 'Main-Class': 'rip.deadcode.Main'  // 自分のMainクラスに合わせ修正
    }
    // Fat Jarを作る
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
    exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
}

task proguard(type: proguard.gradle.ProGuardTask, dependsOn: jar) {

    def javaHome = System.getProperty('java.home')

    // Shrink対象のJAR
    injars jar.archivePath

    libraryjars files(
            "${javaHome}/lib/rt.jar",  // Java SEランタイム
            "${javaHome}/lib/jce.jar"  // cryptoモジュール
    )

    // Fat JARを使わない場合、依存ライブラリーをlibraryjarsに追加する
//    libraryjars configurations.compile.files

    // 出力先 お好みでどうぞ
    outjars("${jar.destinationDir}/proguarded.jar")

    // Shrinkしないクラス
    keep("public class ${jar.manifest.attributes['Main-Class']} { public static void main(java.lang.String[]); }")

    dontwarn("ch.qos.logback.**")
    dontwarn('afu.org.checkerframework.**')
    dontwarn("org.checkerframework.**")
    dontwarn('org.slf4j.**')
}

// assemble実行時にProGuard
assemble.dependsOn(proguard)
```


## ポイント

* buildscriptのクラスパスに`net.sf.proguard:proguard-gradle:6.0.3`を追加する。公式のサンプルではローカルのJARを参照していたが、それはさすがに……。これが公式のJARなのかは調べきれなかったので、不安な方は各自のリポジトリで。
* `proguard.gradle.ProGuardTask`で実行できる。ほとんどの場合シュリンクする対象はプロジェクトのJARなので、ここではjarタスクに依存させている。
* `injars`に`jar.archivePath`を指定して、jarタスクが生成したJARを対象にする。
* `libraryjars`にはJDKのJARを追加する必要がある。ここでは`JAVA_HOME`からJARのパスを取得しているが、ビルド時のJVMとは別のJVMで実行する場合、そちらを参照させる必要がある。
* 今回はライブラリー等も一つのJARにまとめている。依存ライブラリーをビルドするJARに含めない場合、依存ライブラリーのJARも`libraryjars`に指定する必要がある。とはいえライブラリーを作る場合は特にシュリンクの必要はないわけで……。
* 出力先を指定する`outjars`はお好みで。例では入力のJARと同じ個所に出力する。
* `keep`でシュリンクの対象にしないクラスを指定する。たいていの場合`main`メソッドが対象になる。そのほかリフレクションで不具合が出る場合なども、ここで指定する。
* `dontwarn`で、クラスがクラスパス上で見つからない場合の警告を抑制する。普通は起こらないかと思いきや、SLF4Jなど、「クラスパスに〇〇がいる場合」の動作があるようなライブラリーは結構あったりする。
* `assemble`タスクに今回作ったタスクを依存させ、ビルド時にProGuardが実行されるようにする。
* [ソース](https://bitbucket.org/minebreaker_tf/teach-yourself-proguard)


## AWS LambdaでProGuard

コールドスタート時の速度改善のためにもできる限り不必要なコードは取り除きたいところですが、いろいろと設定が必要になります。

```gradle
keep("public class rip.deadcode.bot.Application { *; }")  // RequestHandlerを実装しているメソッド
keep("public interface com.amazonaws.services.lambda.runtime.RequestHandler { *; }")
keep("class com.amazonaws.** { *; }")
keep("class com.fasterxml.** { *; }")
keepattributes("Signature,*Annotation*")
```

* `keep`にいろいろ指定する必要あり
    * `RequestHandler`を実装しているメソッド。エントリーポイントとなるため必要。
    * `RequestHandler`インターフェース自体。名前が変わると正しくラムダ関数として認識されない模様。
    * `org.apache.commons.logging`。ハンドラーに渡されるコンテキストがもつロガーとの間でコンフリクトする。
    * `com.amazonaws.**`と`com.fasterxml.**`。シリアライズ関係で不具合が起きる。もっとスコープは縮められるとおもうが、デバッグ困難なエラーが多く断念。識者の調査が待たれる。実際このせいでファイルサイズがそこそこ大きくなってしまい悲しい。
* `dontwarn`に`org.apache.commons.logging.**`と`org.joda.time.**`を追加。Joda TimeはJava 8に移行してほしいところだが、AWSほどの巨大サービスにもなるとそうもいかないんだろうなぁと。

`RequestStreamHandler`なら今回の多くの部分が問題にならないと思われるので、横着せずそちらを使うべきかもしれない。