Shadingって何
最近、本業の業務がOSSの運用・保守を中心に行う部署になったこともあって、「アプリケーション」開発とは異なる概念に行き合うことが増えてきた。
アプリケーションを開発していたときは、ベースはOSSの組み合わせで実現され、ロジックやデータフローを表現していけばそれで良かった。
しかし、OSSの保守運用(コミッター活動を含む)となると話は別で、ネイティブな言語仕様やOSSが隠してくれていた泥臭い部分の解決について理解することが重要になる。
PR
例えば下記のPR。
https://github.com/apache/pulsar/pull/19458
これは見ての通り、Apache Pulsarで使用されるClient側コードの修正になっている。
修正対象の事象はClassのロードエラーで、アプリケーション開発だとまずお目にかからないエラーだ。なぜならそんなコードはコンパイルできないからである。
しかしOSSでは発生しうる。
この原因になったのがOSS側で行っているshadingである。
クラスパスの解決
shadingの説明に入る前に、Javaにおけるクラスパスの解決についておさらい。
maven, gradleを使用している際に、同名のクラスパスに複数のバージョンが重なっている場合どのように解決されるか?
通常は一番新しいバージョンによって上書きされる。
この上書きは、jarにまとめられる単位で行われるので、自作のコードだけでなく、dependencyすべてが影響を受けるということだ。
これを利用して、脆弱性対応がされていない外部ライブラリの依存を無理やり新しいバージョンに差し替えることも可能である。(正しく動くかは別の話)
OSSにおけるクラスパスの解決
Apache Pulsarなど、独立性が強いOSSにおいては、外部依存のライブラリによって自らのクライアントが使用しているバージョンが変更されることは望ましい挙動ではない。
特に、ClientはSpringFrameworkなどに組み込まれて使われることも一般的であるため、内部で使用している有名どころのOSSなんかはすぐに衝突してしまう。
こうなると、正しい挙動をするかどうかが保証されなくなってしまうし、いちいち正しいバージョンになるように解決することは人手でも自動でも不可能に近い。
(dependency-hell
と呼ばれる)
これを解決する方法の一つがshadingである
shading
shadingは「あるクラスパスを別のクラスパスで表現し直す」機能である。
実際にコードを見てみる。
https://github.com/massakam/pulsar/blob/42c72d1177f2be6ade02f84d5eedb910d51cd891/pulsar-client-shaded/pom.xml
maven-shade-plugin によってshadingを行っているタスクである。
<plugin>
<!-- Shade all the dependencies to avoid conflicts -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>${shadePluginPhase}</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<promoteTransitiveDependencies>true</promoteTransitiveDependencies>
<minimizeJar>false</minimizeJar>
<artifactSet>
<includes>
<include>org.apache.pulsar:pulsar-client-original</include>
<include>org.apache.bookkeeper:*</include>
<include>org.apache.commons:commons-lang3</include>
<include>commons-codec:commons-codec</include>
<include>commons-collections:commons-collections</include>
<include>org.asynchttpclient:*</include>
<include>io.netty:netty-codec-http</include>
<include>io.netty:netty-transport-native-epoll</include>
<include>org.reactivestreams:reactive-streams</include>
<include>com.typesafe.netty:netty-reactive-streams</include>
<include>org.javassist:javassist</include>
<include>com.google.guava:*</include>
<include>org.checkerframework:*</include>
<include>com.google.code.findbugs:*</include>
<include>com.google.errorprone:*</include>
<include>com.google.j2objc:*</include>
<include>com.google.code.gson:gson</include>
<include>com.fasterxml.jackson.*:*</include>
<include>io.netty:*</include>
<include>io.netty.incubator:*</include>
<include>io.perfmark:*</include>
<include>org.eclipse.jetty:*</include>
<include>com.yahoo.datasketches:*</include>
<include>commons-*:*</include>
<include>io.swagger:*</include>
<include>io.airlift:*</include>
<include>org.apache.pulsar:pulsar-common</include>
<include>com.yahoo.datasketches:sketches-core</include>
<include>org.objenesis:*</include>
<include>org.yaml:snakeyaml</include>
<include>org.apache.avro:*</include>
<!-- Avro transitive dependencies-->
<include>com.thoughtworks.paranamer:paranamer</include>
<include>org.apache.commons:commons-compress</include>
<include>org.tukaani:xz</include>
<!-- Issue #6834, Since Netty ByteBuf shaded, we need also shade this module -->
<include>org.apache.pulsar:pulsar-client-messagecrypto-bc</include>
</includes>
<excludes>
<exclude>com.fasterxml.jackson.core:jackson-annotations</exclude>
</excludes>
</artifactSet>
<filters>
<filter>
<artifact>org.apache.pulsar:pulsar-client-original</artifact>
<includes>
<include>**</include>
</includes>
<excludes>
<!-- bouncycastle jars could not be shaded, or the signatures will be wrong-->
<exclude>org/bouncycastle/**</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>org.asynchttpclient</pattern>
<shadedPattern>org.apache.pulsar.shade.org.asynchttpclient</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons</pattern>
<shadedPattern>org.apache.pulsar.shade.org.apache.commons</shadedPattern>
</relocation>
<relocation>
<pattern>io.airlift</pattern>
<shadedPattern>org.apache.pulsar.shade.io.airlift</shadedPattern>
</relocation>
<relocation>
<pattern>com.google</pattern>
<shadedPattern>org.apache.pulsar.shade.com.google</shadedPattern>
<excludes>
<exclude>com.google.protobuf.*</exclude>
</excludes>
</relocation>
<relocation>
<pattern>com.fasterxml.jackson</pattern>
<shadedPattern>org.apache.pulsar.shade.com.fasterxml.jackson</shadedPattern>
<excludes>
<exclude>com.fasterxml.jackson.annotation.*</exclude>
</excludes>
</relocation>
<relocation>
<pattern>io.netty</pattern>
<shadedPattern>org.apache.pulsar.shade.io.netty</shadedPattern>
</relocation>
<relocation>
<pattern>org.checkerframework</pattern>
<shadedPattern>org.apache.pulsar.shade.org.checkerframework</shadedPattern>
</relocation>
<relocation>
<pattern>javax.annotation</pattern>
<shadedPattern>org.apache.pulsar.shade.javax.annotation</shadedPattern>
</relocation>
<relocation>
<pattern>io.swagger</pattern>
<shadedPattern>org.apache.pulsar.shade.io.swagger</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.pulsar.policies</pattern>
<shadedPattern>org.apache.pulsar.shade.org.apache.pulsar.policies</shadedPattern>
</relocation>
<relocation>
<pattern>com.yahoo.datasketches</pattern>
<shadedPattern>org.apache.pulsar.shade.com.yahoo.datasketches</shadedPattern>
</relocation>
<relocation>
<pattern>com.yahoo.sketches</pattern>
<shadedPattern>org.apache.pulsar.shade.com.yahoo.sketches</shadedPattern>
</relocation>
<relocation>
<pattern>org.eclipse.jetty</pattern>
<shadedPattern>org.apache.pulsar.shade.org.eclipse</shadedPattern>
</relocation>
<relocation>
<pattern>org.reactivestreams</pattern>
<shadedPattern>org.apache.pulsar.shade.org.reactivestreams</shadedPattern>
</relocation>
<relocation>
<pattern>com.typesafe</pattern>
<shadedPattern>org.apache.pulsar.shade.com.typesafe</shadedPattern>
</relocation>
<relocation>
<pattern>com.yahoo.memory</pattern>
<shadedPattern>org.apache.pulsar.shade.com.yahoo.memory</shadedPattern>
</relocation>
<relocation>
<pattern>org.objenesis</pattern>
<shadedPattern>org.apache.pulsar.shade.org.objenesis</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml</pattern>
<shadedPattern>org.apache.pulsar.shade.org.yaml</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.avro</pattern>
<shadedPattern>org.apache.pulsar.shade.org.apache.avro</shadedPattern>
<excludes>
<exclude>org.apache.avro.reflect.AvroAlias</exclude>
<exclude>org.apache.avro.reflect.AvroDefault</exclude>
<exclude>org.apache.avro.reflect.AvroEncode</exclude>
<exclude>org.apache.avro.reflect.AvroIgnore</exclude>
<exclude>org.apache.avro.reflect.AvroMeta</exclude>
<exclude>org.apache.avro.reflect.AvroName</exclude>
<exclude>org.apache.avro.reflect.AvroSchema</exclude>
<exclude>org.apache.avro.reflect.Nullable</exclude>
<exclude>org.apache.avro.reflect.Stringable</exclude>
<exclude>org.apache.avro.reflect.Union</exclude>
</excludes>
</relocation>
<!--Avro transitive dependencies-->
<relocation>
<pattern>org.codehaus.jackson</pattern>
<shadedPattern>org.apache.pulsar.shade.org.codehaus.jackson</shadedPattern>
</relocation>
<relocation>
<pattern>com.thoughtworks.paranamer</pattern>
<shadedPattern>org.apache.pulsar.shade.com.thoughtworks.paranamer</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons</pattern>
<shadedPattern>org.apache.pulsar.shade.org.apache.commons</shadedPattern>
</relocation>
<relocation>
<pattern>org.tukaani</pattern>
<shadedPattern>org.apache.pulsar.shade.org.tukaani</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.bookkeeper</pattern>
<shadedPattern>org.apache.pulsar.shade.org.apache.bookkeeper</shadedPattern>
</relocation>
</relocations>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.PluginXmlResourceTransformer" />
</transformers>
</configuration>
</execution>
</executions>
</plugin>
artifactSet.includeはshading対象のクラスパスを示し、relocations.relocationはどのクラスパスをどんなクラスパスに変更するかが記載されている。
ここで重要なのは、shadingの対象にするにはincludeにそのパスを記載する必要があるということと、
relocationは実際にそのクラスファイルが存在するかは気にしていないとうことである。
PRの編集内容がまさにそれを修正している。
PRの修正内容
PRでは、主に < include >com.fasterxml.jackson.*:*</ include >
の追加と、それによって不要になるincludeの削除を行っている。
また、ユーザがアノテーションを使用する都合でjacksonのanotationクラスはexcludeされている。
実際に問題を発生させていたのは、pulsar-client-shaded
で jacksonのdatatypeをinclude指定内にも関わらず、com.fasterxml.jacksonをリロケーションしていることにある。
これによって、リロケーションされたdatatypeは解決できるクラスがなくなってしまったため、例外になっている。
まとめ
shadingは適切に利用すると依存関係の変更からライブラリを守ることができる。
しかし、新しいライブラリを追加したときや、ワイルドカードを使用できないときなどはinclude/excludeとrelocationの設定を正しく行わなければ実行時エラーを招く原因になる。
あまり業務アプリケーションで使用したい機能ではないね。そんなシチュエーションは限られていると思うけど。