Wenn Sie jemals versucht haben, eine bestimmte Funktion als wiederverwendbares Modul in Mendix zu implementieren, ist die Wahrscheinlichkeit groß, dass Sie einige Jar-Abhängigkeiten hinzufügen mussten. Es gibt mehrere Möglichkeiten, dieses Ziel zu erreichen. Aber es gibt eine Methode, die meiner Meinung nach am besten funktioniert, weil sie mehrere Probleme gleichzeitig angeht.
Aber lassen Sie mich zunächst die spielverderbenden oder einfach lästigen Probleme aufzählen, auf die ich beim Versuch, ein wiederverwendbares Modul zu implementieren, gestoßen bin:
- Wenn Sie eine jar-Datei in Ihrem Modul verwenden wollen, z.B. pdfbox v2.3, aber eine andere Version dieser jar-Datei, z.B. v1.8, bereits in einem anderen Modul Ihrer Anwendung verwendet wird, haben Sie Pech. Soweit ich weiß, ist dies einfach nicht möglich. Der Klassenlader nimmt nur eine Version auf und überlässt es einem der beiden Module, mit der falschen Version umzugehen, was normalerweise dazu führt, dass die Anwendung hängt oder abstürzt. Zu allem Übel tritt dies nur auf, wenn eine bestimmte Java-Aktion mit einer konfliktbehafteten Abhängigkeit zur Laufzeit aufgerufen wird.
- Wenn Sie ein Mendix-Modul pflegen und eine Jar-Datei darin aktualisieren müssen, müssen Sie sicherstellen, dass jeder das alte Jar beim Update löscht. Andernfalls haben Sie am Ende zwei Jars mit unterschiedlichen Versionen in Ihrem Classloader-Pfad, was wiederum dazu führen kann, dass die Anwendung nicht funktioniert. Weitere Informationen zu diesem speziellen Problem finden Sie in einem Beitrag im Ideenforum.
- Mendix verlangt von Ihnen, dass Sie transitive Abhängigkeiten manuell verwalten. Das bedeutet in der Regel, dass Sie den Code ausführen müssen, um zu sehen, welche Klassen fehlen, dann die richtige Jar-Datei finden und sie manuell zum Projekt hinzufügen. Dann spülen und wiederholen Sie den Vorgang, bis keine "NoClassFound"-Fehler mehr auftreten.
- Zeitverschwendung beim Exportieren von Modulen ist mein nächster Kritikpunkt. Es gibt zu viele Jars, die man ein- und ausschließen muss, besonders wenn man so etwas wie Community Commons oder das Rest-Modul in seinem Projekt verwendet. Natürlich könnte man das Problem einfach durch eine Auswahl-/Abwahl-Schaltfläche im Dialogfeld für den Export von Abhängigkeiten lösen, aber das ist nicht der Punkt dieses Beitrags. So oder so, es geht Zeit verloren.
Eine Möglichkeit, mit all diesen Problemen effektiv umzugehen, ist der Einsatz eines Build-Tools, das ein so genanntes Fat-Jar erzeugt, eine einzelne Jar-Datei, die alle Abhängigkeiten enthält. Dies allein löst mehrere Probleme aus der vorherigen Liste, einschließlich: Die Fat-Jar-Datei wird automatisch aktualisiert, wenn das Modul aktualisiert wird, wodurch Punkt 2 gelöst wird; das Build-Tool kann sich um alle transitiven Abhängigkeiten kümmern, wodurch Punkt 3 gelöst wird, und schließlich besteht die einzige Abhängigkeit in einer einzigen Jar-Datei, wodurch Punkt 4 eliminiert wird.
Das erste Problem, das wir erwähnt haben, kann mit einer Technik namens Shadowing gelöst werden . Beim Shadowing werden Muster in Klassennamen durch eine bestimmte Zeichenfolge ersetzt. Sie können zum Beispiel org.json durch community.commons.org.json ersetzen . Dadurch kann der Java Classloader zwei Versionen der org.json-Bibliothek laden, da sie unterschiedliche Klassennamen haben.
Fallbeispiel: community commons
Welches Modul eignet sich besser zur Demonstration der oben beschriebenen Techniken als die Community Commons? In seinem aktuellen Zustand hat es etwa 20 Abhängigkeiten (siehe Scrollbalken).
In diesem Fall werde ich Gradle verwenden. Sie können das Gleiche aber auch mit anderen Werkzeugen wie Maven oder JarJarLinks machen.
Hinzufügen von Abhängigkeiten zu Gradle
Zuerst habe ich das Gradle Eclipse Plugin installiert, nachdem ich es vom Eclipse Marketplace heruntergeladen hatte. Dann habe ich ein neues Gradle-Projekt erstellt. Die wichtigste Datei in jedem Gradle-Projekt ist das Gradle-Build-Skript, in dem viele Optionen angegeben werden können, z. B. welche Java-Version verwendet werden soll. Die Abhängigkeiten für ein Gradle-Projekt werden ebenfalls im Build-Skript definiert. Wie Sie unten sehen können, ist meine erste Iteration des Build-Skripts größtenteils Standardmaterial mit Ausnahme des Shadowing-Tools, das ich hinzugefügt habe:
buildscript {repositories
{mavenCentral
()//hier nach Abhängigkeiten suchen}dependencies
{classpath
"com.github.jengelman.gradle.plugins:shadow:2.0.0"
//das ist das Tool, mit dem wir das fat jar bauen und shadowen werden}}
plugins {id
'com.github.johnrengelman.shadow' version '2.0.0'
id 'java'
}
Gruppe 'com.mendix.community-commons
'Version '1.0.0'
plugin anwenden: 'java
'apply plugin: 'maven'
apply plugin: 'eclipse'
task wrapper(type: Wrapper) {gradleVersion
= '3.0'
}
compileJava {sourceCompatibility
= 1.8targetCompatibility
= 1.8}
Repositories {mavenLocal
()
mavenCentral()
}
abhängigkeiten {}
So weit, so gut. Als nächstes begann ich mit dem Hinzufügen der Abhängigkeiten aus dem Community Commons Modul:
Abhängigkeiten {//
https://mvnrepository.com/artifact/org.owasp.antisamy/antisamycompile group: 'org.owasp.antisamy', name: 'antisamy', version: '1.5.3'
// https://
mvnrepository.com/artifact/com.google.guava/guavacompile group: 'com.google.guava', name: 'guava', version: '14.0.1'
// https://
mvnrepository.com/artifact/commons-codec/commons-codeccompile group: 'commons-codec', name: 'commons-codec', version: '1.10'
// https://
mvnrepository.com/artifact/org.apache.pdfbox/jempboxcompile group: 'org.apache.pdfbox', name: 'jempbox', version: '1.8.5'
// https://
mvnrepository.com/artifact/joda-time/joda-timecompile group: 'joda-time', name: 'joda-time', version: '2.9.6'
// https://
mvnrepository.com/artifact/commons-fileupload/commons-fileuploadcompile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.2.1'
// https://
mvnrepository.com/artifact/commons-io/commons-iocompile group: 'commons-io', name: 'commons-io', version: '2.3'
// https://
mvnrepository.com/artifact/org.apache.commons/commons-lang3compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0'
compile group: 'org.apache.servicemix.bundles', name: 'org.apache.servicemix.bundles.batik', version: '1.8_1'
// https://
mvnrepository.com/artifact/org.apache.pdfbox/pdfboxcompile group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.3'
// https://
mvnrepository.com/artifact/xerces/xercesImplcompile group: 'xerces', name: 'xercesImpl', version: '2.8.1'
}
Mir ist aufgefallen, dass einige der Abhängigkeiten nicht in Maven Central aufgeführt sind (oder ich konnte sie zumindest nicht finden). Kein Problem - ich habe die jar-Dateien aus dem Community-Commons-Projekt auf GitHub. Ich habe einen Ordner libs in meinem Gradle-Projekt erstellt und dann com.springsource.org.apache.batik.css-1.7.0.jar und nekohtml.jar hinzugefügt. Dann fügte ich die folgende Zeile zu meinen Abhängigkeiten hinzu, die, wie zu erwarten, alle jar-Dateien aus dem libs Ordner zum gradle-Projekt hinzufügt.
compile fileTree(dir: 'libs', include: '*.jar')
Damit haben wir nun alle Java-Abhängigkeiten aufgelöst.
Der Umgang mit Mendix-Klassen
Meine Grundprämisse ist es, mit der Java-Aktion nur eine entsprechende Funktion aus dem von mir erstellten fat jar aufzurufen. Dies ist bei den meisten Java-Actions in Community Commons bereits der Fall, d.h. der eigentliche Implementierungscode befindet sich nicht innerhalb der Java-Action-Klasse. Stattdessen wird die Ausführung an eine andere Klasse delegiert. Ich kopierte die Klassen aus dem communitycommons-Paket , in denen die Logik implementiert ist, insbesondere ConversationLog, DateTime, ORM, StringUtils usw., in mein Gradle-Projekt. Ich erstellte das Projekt und aktualisierte den Build-Pfad in Eclipse. Erfolg ... zumindest etwas. Die Abhängigkeiten werden geladen und von Eclipse erkannt, aber ich kann einige fehlende Klassen sehen. Die meisten der fehlenden Klassen stammen aus der Mendix-API, die in der Community in Form von IContext, IMendixObject, Core, etc. ausgiebig genutzt wird. Ich habe sie ebenfalls zu den Abhängigkeiten hinzugefügt.
compile files('C:/Program Files/Mendix/7.2.0/runtime/bundles/com.mendix.public-api.jar'
)compile files('C:/Program Files/Mendix/7.2.0/runtime/bundles/com.mendix.logging-api.jar')
Ich muss diese Klassen bei der Entwicklung in Eclipse einbinden, da der Compiler sie erkennen muss. Andernfalls wird das Projekt nicht kompiliert. Ich möchte jedoch nicht, dass die Mendix-API-Klassen in mein Fat-Jar aufgenommen werden, also habe ich sie ausgeschlossen.
shadowJar {Abhängigkeiten
{Ausschluss
'com/mendix/**'
}}
Rebuild und Refresh, viele nicht erkannte Klassen sind jetzt ok. Hier ist ein Screenshot meines gradle-Projekts zu diesem Zeitpunkt:
Aber ich kann immer noch ein paar Importe sehen, die nicht erkannt werden. Dabei handelt es sich um Aufzählungen und Proxyklassen aus dem Mendix Modeler, wie z.B. system.proxies.FileDocument. Ich wusste nicht, wie ich sie in die Liste der Abhängigkeiten aufnehmen sollte, also beschloss ich, sie zu verpacken. Ich deklarierte ein Interface IFileDocument mit dem folgenden Code:
public interface IFileDocument {public
IMendixObject getMendixObject();
public boolean getHasContents();
}
Ich werde mit dieser Schnittstelle nur innerhalb des Fat Jar arbeiten. Dann habe ich eine Methode hinzugefügt, um ein system.proxies.FileDocument in ein org.community-commons.main.IFileDocument zu konvertieren . Jedes Mal, wenn eine Java-Aktion eine Methode aus meiner Fat-Jar-Datei aufrufen muss, die mit Dateien arbeitet, führe ich eine Konvertierung durch und übergebe dann die Schnittstelle.
Damit bleiben nur noch die Aufzählungen und die system.proxies.Language übrig. Für die Aufzählungen, z.B. communitycommons.proxies.LogLevel, habe ich eine ähnliche Wrapping-Methode verwendet wie für das FileDocument. Für die Sprache habe ich mich entschieden, den gesamten Code (alle vier Zeilen) in der Java-Aktion zu belassen. Es gibt keine weiteren externen Abhängigkeiten, warum also die Mühe? Bei Bedarf kann er jedoch auf ähnliche Weise verpackt werden. Ich habe ein weiteres Rebuild durchgeführt und das Fat Jar zum Projekt hinzugefügt. Nachdem alles kompiliert wurde, sind dies die Abhängigkeiten, die im Modeler verbleiben.
Lassen Sie mich hier einen kleinen Exkurs über das Wrapping von Mendix-Proxy-Klassen machen. Diese Lösung ist eindeutig nicht das, was Sie normalerweise tun möchten. Andererseits handelt es sich dabei um eine weitgehend mechanische Arbeit. So wie ich die Dinge sehe, bedeutet dies, dass es einen Weg gibt, dies zu automatisieren. Es kann ein Werkzeug entwickelt werden, das Code für die Wrapper-Klassen generiert. Man könnte sogar noch einen Schritt weiter gehen und das Werkzeug alle Verwendungen von Proxy-Klassen durch die entsprechenden Wrapper-Klassen ersetzen lassen.
Hinzufügen von Ressourcen zu einem Fat Jar
Wie Sie auf dem Screenshot sehen können, haben wir es geschafft, die meisten Dateien loszuwerden, mit Ausnahme der antisamy XML-Dateien. Ich könnte sie einfach so lassen, wie sie sind, und alles würde gut funktionieren, aber sie stören mich wirklich. Der Gründlichkeit halber zeige ich, wie beliebige Ressourcendateien in das fat jar aufgenommen werden können. Aber das ist natürlich optional.
Wie Sie wahrscheinlich bereits wissen, ist eine jar-Datei nur eine Zip-Datei, was bedeutet, dass wir jede beliebige Datei darin aufnehmen können. Tatsächlich betrachtet Gradle alle Dateien, die sich in einem Ordner namens resources befinden, automatisch als Ressourcendateien und fügt sie dem jar hinzu. Ich habe einen solchen Ordner unter src/main angelegt und alle XML-Dateien dorthin kopiert. Um zu testen, ob die Ressourcen wirklich hinzugefügt wurden, habe ich einen Rebuild durchgeführt und dann die Jar-Datei extrahiert. Ich konnte sehen, dass alle XML-Dateien wirklich in der Fat-Jar-Datei enthalten sind.
Als nächstes musste ich die Art und Weise ändern, wie die XML-Dateien gelesen werden. Zuvor wurden sie wie normale Dateien aus dem Ressourcenordner geladen. Dies ist jedoch nicht mehr möglich, wenn sie in eine Jar-Datei verpackt sind. Stattdessen sollten Sie den Classloader verwenden, um sie als Ressourcen zu laden. Ich habe die folgenden Zeilen in StringUtils.XSSSanitize (das Teil der Community Commons ist) geändert:
String filename = Core.getConfiguration().getResourcesPath() + File.separator+
"communitycommons" + File.separator + "antisamy "
+ File.separator + "antisamy-" + policyString + "-1.4.4.xml";
Policy p = Policy.getInstance(Dateiname);
Anstelle eines Dateinamens verwende ich den Classloader, um den Inhalt der Datei als Stream zu erhalten:
Schließlich bleiben nur noch drei Abhängigkeiten übrig:
- java, das die Klassen für die Schnittstelle mit dem Fat Jar enthält,
- die Fat-Jar-Datei und
- txt - eine Lizenzdatei.
Da ich keine Möglichkeit sehe, die Anzahl der Abhängigkeiten weiter zu reduzieren, können wir nun zum nächsten Thema übergehen:
Shadowing
Der Einfachheit halber werde ich einfach allen externen Klassen das Präfix cc_ voranstellen und den folgenden Code verwenden:

shadowJar {relocate
('org.apache', 'cc_org.apache')re
locate
('org.cyberneko', 'cc_org.cyberneko')
relocate('org.joda', 'cc_org.joda')
relocate('org.owasp', 'cc_org.owasp')
relocate('org.w3c', 'cc_org.w3c')relocate
('org.xml', 'cc_org.xml')
relocate('javax', 'cc_javax')
relocate('java_cup', 'cc_java_cup')
relocate('com.google', 'cc_com.google')
dependencies {exclude
'com/mendix/**'
}}
Gradle ersetzt automatisch die passenden Klassennamen sowohl in den Dateien, in denen diese Klassen definiert sind, als auch in den Dateien, in denen diese Klassen verwendet werden.
Beachten Sie, dass ich das org.community_commons-Paket ausgelassen habe. Diese Klassen beinhalten keine Abhängigkeiten von Dritten, so dass keine Gefahr besteht, dass Konflikte entstehen.
Wenn ich das jar-Archiv öffne, kann ich bestätigen, dass alle Klassen, die aus einer externen Abhängigkeit stammen, tatsächlich umbenannt sind. Das ist alles, was nötig ist. Jetzt können Entwickler, die Community Commons in ihrem Projekt verwenden, Bibliotheken nutzen, die auch vom Community Commons-Modul verwendet werden, ohne sich Gedanken über Konflikte zu machen.
Nehmen wir zum Beispiel an, wir wollen die pdfbox-Bibliothek von Apache verwenden, die auch von community commons verwendet wird, weil es eine neuere Version mit einer Funktion gibt, die uns gefällt, oder weil es eine Komponente gibt, die von einer früheren Version von pdfbox abhängig ist. Das ist jetzt möglich. Wir können die pdfbox.jar zum userlib-Ordner hinzufügen und sie in unserem Code verwenden, ohne auf Probleme mit Abhängigkeiten zu stoßen.
An dieser Stelle möchte ich betonen, dass es zwar technisch möglich ist, Schattenklassen in Ihrem Mendix-Projekt zu verwenden, wie in diesem Screenshot gezeigt, dies aber unbedingt vermieden werden sollte. Es gibt keine Garantie, dass die Schattenklasse nicht entfernt/umbenannt wird und somit nicht mehr verfügbar ist, wenn das Modul aktualisiert wird.
Abschließende Überlegungen
Die korrekte Paketierung und Schattierung von Abhängigkeiten trägt wesentlich dazu bei, Kopfschmerzen bei der Verwendung von Mendix-Modulen zu vermeiden. Ich hoffe, dass diese Beispiele die Entwickler von App-Store-Modulen motivieren werden, ihre internen Abhängigkeiten so zu verwalten, dass das Leben aller Beteiligten etwas einfacher wird. Wenn es genug Interesse und Unterstützung aus der Community gibt, wird der Prozess der Shadowing-Abhängigkeiten von jar-Dateien vielleicht auch Teil eines zukünftigen Mendix Best Practices Dokuments für Module - oder noch besser, wir bekommen ein integriertes Build/Shadowing-Tool innerhalb des Mendix Modelers.
Ich hoffe, Sie haben diesen Beitrag interessant gefunden. Wenn Sie Anmerkungen oder Ideen haben, wie dieser Beitrag oder der darin enthaltene Code verbessert werden kann, kontaktieren Sie mich bitte. Sie können auch das komplette Projekt auf Github einsehen.
Viel Spaß beim Codieren!
-Andrej Gajduk