Rozwiązywanie niezgodności binarnych z maven-shade-plugin
Z Jacek Laskowski - Wiki Projektanta Java EE
Wracając do mojej działalności w Apache OpenEJB zabrałem się za czyszczenie mojego backlogu. Lista nie jest pokaźna, ale niezwykle leciwa. Jednym z zadań było rozwiązanie tematu niezgodności binarnej asm używanego w OpenEJB. Innymi słowy, przypadek zależności asm unaocznia problem różnych wersji tej samej biblioteki, która zmieniła publiczny interfejs (API) między wersjami, co skutkuje, że skorzystanie jednocześnie z nich prowadzi do pojawienia się wyjątku:
java.lang.NoSuchMethodError: org.objectweb.asm.ClassReader.accept(Lorg/objectweb/asm/ClassVisitor;I)V
Wynika to z faktu, że między wersjami ASM 1.5.3 i nowszymi istnieje niezgodność publicznego interfejsu, gdzie metody klasy org.objectweb.asm.ClassReader czy interfejsu org.objectweb.asm.ClassVisitor zmieniły swoje sygnatury (!) Na ten temat natrafili również panowie ze SpringSource - ASM version incompatibilities, using Spring @Autowired with Hibernate. Jednym z rozwiązań było przepakowanie klas i interfejsów asm narzędziem jarjar oznaczone numerem 3. W tym artykule przedstawię rozwiązanie numer 4 z użyciem maven-shade-plugin. Wtyczka maven-shade-plugin pozwala na spakowanie wszystkich zależności projektu w pojedynczy plik jar oraz (właśnie ta funkcjonalność mnie najbardziej interesowała) przenieść wskazane klasy z jednego pakietu do wskazanego z jednoczesną zmianą ich wywołań we wszystkich klasach projektów korzystających z nich.
Najlepszym sposobem na przedstawienie problemu będzie sposób praktyczny - przez przykład. Stworzymy projekt z zależnością ASM 1.5.3 i drugi z ASM 3.1. Następnie stworzymy kolejny, trzeci projekt, który będzie zależny od tych dwóch, wcześniej stworzonych. I właśnie ten trzeci projekt będzie projektem demonstracyjnym - dwie niezgodne binarnie wersje tej samej biblioteki będące przechodnimi zależnościami projektów zależnych prowadzą do sytuacji, w której pojawi się wyjątek NoSuchMethodError. Rozwiązanie to wdrożenie wtyczki maven-shade-plugin w projektach przejściowych, które dopiero stanowią zależności projektu demonstracyjnego.
Do tworzenia projektów skorzystamy z Apache Maven. Kompletny projekt dostępny jest jako asm-shaded.zip.
Spis treści |
Utworzenie struktury projektowej
Na cele naszego doświadczenia utworzymy kilka projektów mavenowych zebranych w jeden zbiorczy projekt macierzysty, który pozwoli nam na wykonywanie poleceń zbiorczo dla wszystkich podprojektów. Typowa konfiguracja w Maven dla wielu projektów będących projektami współpracującymi.
Utworzenie projektu macierzystego - asm-shaded
Tworzymy wspomniany projekt macierzysty asm-shaded.
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.0 \
-DgroupId=pl.jaceklaskowski -DartifactId=asm-shaded -Dversion=1.0
W pliku pom.xml zmieniamy deklarację packaging na pom. W ten sposób stanie się on projektem macierzystym kolejnych.
<packaging>pom</packaging>
Kolejne polecenia wykonujemy z poziomu katalogu projektu asm-shaded.
Utworzenie projektu z ASM 1.5.3 - A-asm153
Pora na kilka podprojektów, z których jako pierwszy będzie A-asm153 z zależnością ASM 1.5.3.
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.0 \
-DgroupId=pl.jaceklaskowski -DartifactId=A-asm153 -Dversion=1.0
W pliku pom.xml deklarujemy zależność asm-1.5.3.
<dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>1.5.3</version> </dependency>
Tworzymy klasę pl.jaceklaskowski.KlasaA korzystającą z funkcjonalności specyficznej dla ASM 1.5.3 - metody public void accept(ClassVisitor classVisitor, boolean skipDebug).
package pl.jaceklaskowski;
import org.objectweb.asm.*;
import java.io.*;
public class KlasaA {
public void wykonajMetodeDostepnaJedynieWAsm153() {
try {
// void accept(ClassVisitor classVisitor, boolean skipDebug) dostępna w ASM 1.5.3 ale nie ASM 3.1
new ClassReader(this.getClass().getName()).accept(new PustyClassVisitor(), true);
} catch (IOException e) {
// I tak się nie wydarzy
e.printStackTrace();
}
}
// Już ClassVisitor jest wyróżnikiem, z jaką wersją pracujemy
private static class PustyClassVisitor implements ClassVisitor {
public void visit(int i, int i1, String s, String s1, String[] strings, String s2) {
}
public void visitInnerClass(String s, String s1, String s2, int i) {
}
public void visitField(int i, String s, String s1, Object o, Attribute attribute) {
}
public CodeVisitor visitMethod(int i, String s, String s1, String[] strings, Attribute attribute) {
return null;
}
public void visitAttribute(Attribute attribute) {
}
public void visitEnd() {
}
}
}
Utworzenie projektu z ASM 3.1 - B-asm31
Następnym podprojektem jest projekt B-asm31 z zależnością ASM 3.1.
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.0 \
-DgroupId=pl.jaceklaskowski -DartifactId=B-asm31 -Dversion=1.0
Definiujemy zależność asm-3.1 w pom.xml:
<dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.1</version> </dependency>
Tworzymy klasę pl.jaceklaskowski.KlasaB korzystającą z funkcjonalności specyficznej dla ASM 3.1 - metody public void accept(ClassVisitor classVisitor, int flags).
package pl.jaceklaskowski;
import org.objectweb.asm.*;
import java.io.*;
public class KlasaB {
public void wykonajMetodeDostepnaJedynieWAsm31() {
try {
// void accept(ClassVisitor classVisitor, boolean skipDebug) dostępna w ASM 3.1, ale brak jej w ASM 1.5.3
new ClassReader(this.getClass().getName()).accept(new PustyClassVisitor(), ClassReader.SKIP_DEBUG);
} catch (IOException e) {
// I tak się nie wydarzy
e.printStackTrace();
}
}
// ClassVisitor jest bardziej rozbudowany w nowszych wersjach niż 1.5.3
private static class PustyClassVisitor implements ClassVisitor {
public void visit(int i, int i1, String s, String s1, String s2, String[] strings) {
}
public void visitSource(String s, String s1) {
}
public void visitOuterClass(String s, String s1, String s2) {
}
public AnnotationVisitor visitAnnotation(String s, boolean b) {
return null;
}
public void visitAttribute(Attribute attribute) {
}
public void visitInnerClass(String s, String s1, String s2, int i) {
}
public FieldVisitor visitField(int i, String s, String s1, String s2, Object o) {
return null;
}
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
return null;
}
public void visitEnd() {
}
}
}
Utworzenie projektu głównego - C
Ostatnim podprojektem jest projekt główny C.
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.0 \
-DgroupId=pl.jaceklaskowski -DartifactId=C -Dversion=1.0
W jego pliku pom.xml definiujemy zależności A-asm153 oraz B-asm31.
<dependency> <groupId>pl.jaceklaskowski</groupId> <artifactId>A-asm153</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>pl.jaceklaskowski</groupId> <artifactId>B-asm31</artifactId> <version>1.0</version> </dependency>
Ostatnim krokiem w zestawianiu środowiska demonstracyjnego jest stworzenie klasy-testu pl.jaceklaskowski.AsmTest, która korzysta z metod udostępnianych przez projekty zależne.
package pl.jaceklaskowski;
public class AsmTest {
public void testAsmBinaryIncompatibility() {
new KlasaA().wykonajMetodeDostepnaJedynieWAsm153();
new KlasaB().wykonajMetodeDostepnaJedynieWAsm31();
}
}
Pierwsze uruchomienie mvn test - java.lang.NoSuchMethodError
Uruchomienie polecenia mvn test uruchomi klasę-test pl.jaceklaskowski.AsmTest, która zgodnie z oczekiwaniami zakończy się błędem java.lang.NoSuchMethodError.
$ cat C/target/surefire-reports/pl.jaceklaskowski.AsmTest.txt
-------------------------------------------------------------------------------
Test set: pl.jaceklaskowski.AsmTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.031 sec <<< FAILURE!
pl.jaceklaskowski.AsmTest.testAsmBinaryIncompatibility() Time elapsed: 0.031 sec <<< FAILURE!
java.lang.NoSuchMethodError: org.objectweb.asm.ClassReader.accept(Lorg/objectweb/asm/ClassVisitor;I)V
at pl.jaceklaskowski.KlasaB.wykonajMetodeDostepnaJedynieWAsm31(KlasaB.java:12)
at pl.jaceklaskowski.AsmTest.testAsmBinaryIncompatibility(AsmTest.java:6)
Wdrożenie maven-shade-plugin
Wdrożenie wtyczki maven-shade-plugin rozpoczynamy od utworzenia dwóch, nowych projektów, których jedynym celem jest zmiana pakietów klas ASM - jeden dla wersji 1.5.3, a drugi dla wersji 3.1 (patrz Relocating Classes w dokumentacji maven-shade-plugin). Nowe projekty stają się projektami przejściowymi z maven-shade-plugin, które deklarujemy jako zależności naszego projektu C zamiast dotychczasowych A-asm135 oraz B-asm31. Technicznie moglibyśmy wykonać "przesłonięcie" pakietu wyłącznie w jednym projekcie zależnym, a jedynie dla porządku robimy to w obu. Chodzi o to, aby nazwy pakietów dla klas ASM, które zmieniły swój publiczny interfejs, np. org.objectweb.asm.ClassReader, były różne dla różnych wersji ASM.
Utworzenie projektu przejściowego A-asm153-shaded
Najpierw tworzymy projekt A-asm153-shaded.
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.0 \
-DgroupId=pl.jaceklaskowski -DartifactId=A-asm153-shaded -Dversion=1.0
a następnie deklarujemy zależność A-asm153 w pom.xml:
<dependency>
<groupId>pl.jaceklaskowski</groupId>
<artifactId>A-asm153</artifactId>
<version>1.0</version>
</dependency>
Ostatnia zmiana w projekcie to dodanie konfiguracji wtyczki maven-shade-plugin w pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.objectweb.asm</pattern>
<shadedPattern>shaded.a.org.objectweb.asm</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Sekcja relocation konfiguruje przesłonięcie pakietów.
Utworzenie projektu przejściowego B-asm31-shaded
Tworzymy projekt B-asm31-shaded.
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.0 \
-DgroupId=pl.jaceklaskowski -DartifactId=B-asm31-shaded -Dversion=1.0
a następnie deklarujemy zależność B-asm31 w pom.xml:
<dependency>
<groupId>pl.jaceklaskowski</groupId>
<artifactId>B-asm31</artifactId>
<version>1.0</version>
</dependency>
Kończymy dodając konfigurację wtyczki maven-shade-plugin do pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.objectweb.asm</pattern>
<shadedPattern>shaded.b.org.objectweb.asm</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Warto zauważyć, że docelowy pakiet klas z org.objectweb.asm w tym projekcie jest inny (shaded.b.org.objectweb.asm) niż ten w projekcie A-asm153-shaded (shaded.a.org.objectweb.asm). To jest kluczowa konfiguracja, która przenosi niezgodne binarnie klasy do różnych pakietów.
Zmiany w projekcie C - dodanie zależności przejściowych
W pom.xml projektu C podmieniamy zależności A-asm153 oraz B-asm31 na odpowiadające im zależności przejściowe, odpowiednio, A-asm153-shaded oraz B-asm31-shaded.
<dependency> <groupId>pl.jaceklaskowski</groupId> <artifactId>A-asm153-shaded</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>pl.jaceklaskowski</groupId> <artifactId>B-asm31-shaded</artifactId> <version>1.0</version> </dependency>
Uruchomienie końcowe z mvn package
Wszystko gotowe na powtórne wykonanie mvn test. Ale co jest?! Wykonanie mvn test kończy się znowu BUILD FAILURE z java.lang.NoSuchMethodError: org.objectweb.asm.ClassReader.accept(Lorg/objectweb/asm/ClassVisitor;I)V?!
Rozwiązaniem jest wykonanie polecenia mvn package, gdyż wynika to z konfiguracji wtyczki maven-shade-plugin w pomach projektów pośrednich, która jest związana z wykonaniem mavenowego etapu package (patrz <phase>package</phase> w pom.xml w projektach A-asm153-shaded i B-asm31-shaded).
Uruchomienie mvn package upewnia nas, że wszystko jest faktycznie w należytym porządku.
$ mvn package ... ------------------------------------------------------- T E S T S ------------------------------------------------------- Running pl.jaceklaskowski.AsmTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.047 sec Running pl.jaceklaskowski.AppTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.031 sec Results : Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [jar:jar] [INFO] Building jar: C:\projs\sandbox\asm-shaded\C\target\C-1.0.jar [INFO] [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary: [INFO] ------------------------------------------------------------------------ [INFO] asm-shaded ............................................ SUCCESS [1.765s] [INFO] A-asm153 .............................................. SUCCESS [1.844s] [INFO] B-asm31 ............................................... SUCCESS [0.360s] [INFO] A-asm153-shaded ....................................... SUCCESS [0.828s] [INFO] B-asm31-shaded ........................................ SUCCESS [0.500s] [INFO] C ..................................................... SUCCESS [0.406s] [INFO] ------------------------------------------------------------------------ [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------
