Teil 1 von 3 aus der Serie "Maven to the rescue"

In den meisten Projekten werden Bibliotheken samt Abhängigkeiten aus verschiedenen Quellen und mit verschiedenen Versionen eingebunden, was dazu führen kann, dass gleiche Klassen in verschiedenen Implementierungen als Klassenduplikate im Klassenpfad landen. Das duplicate-finder-maven-plugin hilft hier.

Klassenkampf

Wer beispielsweise einen Webservice mit Apache CXF und jaxws in einem Projekt einsetzt, erhält direkt mehrere Implementierungen der Activation-API mitgeliefert, die Version javax.activation-api:javax.activation von der Bibliothek javax.xml.ws:jaxws-api und die Version jakarta.activation:jakarta.activation-api von der Bibliothek org.apache.cxf:cxf-core.

Das Jakarta EE-Projekt (nicht zu Verwechseln mit dem aufgelösten Apache Jakarta Projekt) hat einen schnelleren Lebenszyklus als Oracle und aktualisiert diverse Standard-APIs rund um Java EE schneller als Oracle selber und sollte eigentlich sogar die Weiterenwicklung übernehmen. Sollte, weil Oracle sich nun dagegen ausgesprochen hat, dass Jakarta EE das package javax verwendet.

Unter dem Strich gibt es aber nicht nur von Jakarta EE Bibliotheken, die mit unterschiedlicher Group-ID, aber gleichen Paketpfaden wie andere Bibliotheken arbeiten. Man denke da an das Projekt Apache Log4j, bei der Bridge-Libraries immer wieder die gleichen Klassen (z. B. der commons-logging Bibliothek) Implementieren, um das Logging von anderen Frameworks auf das Log4j System umzulenken.

In all diesen Fällen können mehrere Implementierungen einer Klasse im Klassenpfad liegen. Solange die Klassen identisch sind, ist dies kein größeres Problem, aber wenn zwei unterschiedliche Implementierungen ein und derselben Klasse vorliegen, ist es fast zufällig, welche dieser beiden Implementierungen der Classloader zur Kompilier- und Laufzeit anzieht. Die Folge sind undeterministische Fehler in der Anwendung, Exceptions über fehlende Klassen oder Methoden und unterschiedliches Laufverhalten auf verschiedenen Systemen und instanzen.

Duplikate finden

Die standardmäßig ausgeführten Build-Plugins von Maven erkennen solche Konflikte nicht, denn die verschiedenen Implementierungen haben üblicherweise auch unterschiedliche Gruppen-IDs und Maven prüft initial nur auf Duplikate von Gruppen-IDs + Artifact-IDs.

Glücklicherweise gibt es hierfür ein sehr gutes Plugin, welches nach dem Zusammenstellen aller Dependencies eine Prüfung auf alle Bibliotheken und Klassen durchführt und Duplikate meldet: org.basepom.maven:duplicate-finder-maven-plugin

<!-- Plugin zum Finden redundanter Klassen: https://github.com/basepom/duplicate-finder-maven-plugin -->
<plugin>
    <groupId>org.basepom.maven</groupId>
    <artifactId>duplicate-finder-maven-plugin</artifactId>
    <version>1.3.0</version>
    <executions>
        <execution>
            <id>default</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <printEqualFiles>false</printEqualFiles>
        <failBuildInCaseOfDifferentContentConflict>false</failBuildInCaseOfDifferentContentConflict>
        <failBuildInCaseOfEqualContentConflict>false</failBuildInCaseOfEqualContentConflict>
        <!-- Im Fall eines Konflikts soll der Build fehlschlagen. -->
        <failBuildInCaseOfConflict>true</failBuildInCaseOfConflict>
        <checkCompileClasspath>true</checkCompileClasspath>
        <checkRuntimeClasspath>true</checkRuntimeClasspath>
        <checkTestClasspath>true</checkTestClasspath>
        <skip>false</skip>
        <quiet>true</quiet>
        <preferLocal>true</preferLocal>
        <useResultFile>true</useResultFile>
        <resultFileMinClasspathCount>2</resultFileMinClasspathCount>
        <resultFile>${project.build.directory}/duplicate-finder-result.xml</resultFile>
        <includeBootClasspath>false</includeBootClasspath>
        <bootClasspathProperty>sun.boot.class.path</bootClasspathProperty>
        <useDefaultResourceIgnoreList>true</useDefaultResourceIgnoreList>
        <includePomProjects>false</includePomProjects>
        <!-- Konflikt-Dependencies die sich nicht beheben lassen,
          da z. B. die Bibliotheken dringend benötigt werden und nicht ausgetauscht
          oder entfernen werden können. -->
        <ignoredDependencies>
            <dependency>
                <groupId>com.rometools</groupId>
                <artifactId>rome-utils</artifactId>
            </dependency>
        </ignoredDependencies>
        <!-- Konflikt-Resourcen die sich nicht beheben lassen,
          da die Resourcen dringend benötigt wird und nicht ausgetauscht oder entfernen
          werden können, oder nicht relevant sind. -->
        <ignoredResourcePatterns>
            <ignoredResourcePattern>log4j2.xml</ignoredResourcePattern>
            <ignoredResourcePattern>mozilla/public-suffix-list.txt</ignoredResourcePattern>
            <ignoredResourcePattern>.*\.properties</ignoredResourcePattern>
            <ignoredResourcePattern>.*\.txt</ignoredResourcePattern>
            <ignoredResourcePattern>.*\.html</ignoredResourcePattern>
        </ignoredResourcePatterns>
    </configuration>
</plugin>

Die Ausgabe des Plugins verrät, welche Klassen und Ressourcen mehrfach im Klassenpfad existieren:

[INFO] --- duplicate-finder-maven-plugin:1.3.0:check (default) @ MyProject ---
[WARNING] Found duplicate (but equal) classes in [jakarta.xml.bind:jakarta.xml.bind-api:2.3.2, javax.xml.bind:jaxb-api:2.3.1]:
[WARNING]   javax.xml.bind.DatatypeConverterInterface
[WARNING]   javax.xml.bind.Element
[WARNING]   javax.xml.bind.JAXBContextFactory
[WARNING]   javax.xml.bind.Marshaller
[WARNING]   javax.xml.bind.NotIdentifiableEvent
[WARNING]   javax.xml.bind.ParseConversionEvent
[WARNING]   javax.xml.bind.PrintConversionEvent
...
[WARNING] Found duplicate (but equal) classes in [com.sun.istack:istack-commons-runtime:3.0.8, com.sun.xml.bind:jaxb-core:2.3.0.1]:
[WARNING]   com.sun.istack.Builder
[WARNING]   com.sun.istack.Interned
[WARNING]   com.sun.istack.NotNull
[WARNING]   com.sun.istack.Nullable
[WARNING]   com.sun.istack.Pool
[WARNING]   com.sun.istack.localization.Localizable
[WARNING] Found duplicate and different classes in [jakarta.xml.bind:jakarta.xml.bind-api:2.3.2, javax.xml.bind:jaxb-api:2.3.1]:
[WARNING]   META-INF.versions.9.javax.xml.bind.ModuleUtil
[WARNING]   javax.xml.bind.Binder
[WARNING]   javax.xml.bind.ContextFinder
[WARNING]   javax.xml.bind.DataBindingException
[WARNING]   javax.xml.bind.DatatypeConverter
[WARNING]   javax.xml.bind.DatatypeConverterImpl
[WARNING]   javax.xml.bind.GetPropertyAction
[WARNING]   javax.xml.bind.JAXB
[WARNING]   javax.xml.bind.JAXBContext
[WARNING]   javax.xml.bind.JAXBElement
...
[WARNING] Found duplicate and different classes in [com.sun.istack:istack-commons-runtime:3.0.8, com.sun.xml.bind:jaxb-core:2.3.0.1]:
[WARNING]   com.sun.istack.ByteArrayDataSource
[WARNING]   com.sun.istack.FinalArrayList
[WARNING]   com.sun.istack.FragmentContentHandler
[WARNING]   com.sun.istack.SAXException2
[WARNING]   com.sun.istack.SAXParseException2
[WARNING]   com.sun.istack.XMLStreamException2
...
[WARNING] Found duplicate and different classes in [com.sun.xml.bind:jaxb-core:2.3.0.1, com.sun.xml.bind:jaxb-impl:2.3.2, org.glassfish.jaxb:jaxb-runtime:2.3.2]:
[WARNING]   com.sun.xml.bind.Messages
[WARNING]   com.sun.xml.bind.Util
[WARNING]   com.sun.xml.bind.WhiteSpaceProcessor
[WARNING]   com.sun.xml.bind.api.impl.NameConverter
[WARNING]   com.sun.xml.bind.api.impl.NameUtil
[WARNING]   com.sun.xml.bind.marshaller.DataWriter
[WARNING]   com.sun.xml.bind.marshaller.DumbEscapeHandler
...

In diesem Beispiel zeigt die Ausgabe, dass es gleiche Klassen mit gleichem Inhalt in unterschiedlichen Dependencies gibt (Found duplicate (but equal) classes) und gleiche Klassen mit unterschiedlichem Inhalt (Found duplicate and different classes).
Beide sollten entsprechend behandelt werden.

Ausschluss

Entsprechend muss man die angezogenen Bibliotheken ausdünnen und sich bei Konflikten für eine Version entscheiden.

Dies wird dann mit exclusions repariert:

<dependency>
    <groupId>javax.xml.ws</groupId>
    <artifactId>jaxws-api</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>javax.activation-api</artifactId>
            <groupId>javax.activation</groupId>
        </exclusion>
        <exclusion>
            <artifactId>jaxb-api</artifactId>
            <groupId>javax.xml.bind</groupId>
        </exclusion>
    </exclusions>
</dependency>

Dabei ist es recht hilfreich, die Abhängigkeiten der verschiedenen Großprojekte zu kennen. So hat beispielsweise Jakarta EE derzeit die neueren Versionen der J2EE-Bibliotheken und sollte den javax-Bibliotheken vorgezogen werden. In wenigen Fällen ist es aber so, dass andere Bibliotheken Referenzen auf ältere javax-Bibliotheken haben und ohne diese nicht funktionieren. Es bleibt also nichts anderes übrig, als die Anwendung der “trial and error” Methode. Die Wahl auf die neueren Jakarta EE Bibliotheken gilt aber nur solange, bis letzten Endes geklärt ist, wer und wie in Zukunft die J2EE-APIs verwaltet und publiziert.

Bei Log4j deuten gleiche Klassen darauf hin, dass man zwei sich gegenseitig widersprechende Bridges eingebunden hat. Hier sollte man nochmal prüfen, welche der Bridges die richtige ist.

Hibernate bringt von Haus aus eine eigene JTA-API mit Implementierung mit sich, welche aktueller als andere Implementierungen, wie Geronimo oder javax ist.

Apache CXF ist anscheinend so komplex, dass verschiedene Komponenten durchaus mal verschiedene Bibliotheken mit gleichem Inhalt oder Funktion verwenden.

Fazit

Seit einiger Zeit habe ich dieses Plugin standardmäßig in der Parent-POM meiner Projekte drin. Vor noch ca. 8 Jahren hatte ich in einem größeren Projekt öfter mal Probleme mit konkurrierenden Versionen. Inzwischen kann ich alles “sauber” pflegen. Änderungen in einer der Dependencies werden automatisch geprüft und bei potentiellen Problemen aufgezeigt.

Referenzen & weiterführende Links

Series NavigationSicherheitslücken an der Wurzel packen >>