Fat JAR mit gradle

Quelle, ergänzt um, Aufruf dann via

Spring (Boot) erweitern

Angenommen, ich würde Web-Applikationen verkaufen. Zum Teil customized auf Basis desselben Cores, zum Teil unabhängig davon. Mein Code müsste also modular sein; ich möchte Libraries wieder­­verwenden, ganze Features ein- und ausschalten können, vielleicht Teile nur für einen Kunden überschreiben. Wie würde ich das wohl mit Spring Boot machen?

Sucht man im Internet nach Modularisierung von Spring (Boot) Anwendungen, landet man recht schnell bei Microservices:

While there is no precise definition of this architectural style, there are certain common characteristics […]: an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API.

Zuerst mal nützt mir das noch nichts für wiederzuverwendende Libraries. Aber selbst wenn, dann ist mir zugegebenermaßen nicht ganz klar, wie man sich das in einer Webapplikation vorzustellen hat: Kommuniziert jeder Teil der Anwendung (Chat, Kalender, …) nur mit “seinem” Backend, separiert zum Beispiel über Ports? Oder läuft alles über Port 80 an den Server, und der verteilt das intern an den jeweiligen Prozess? So oder so müsste man das anwendungsweit berücksichtigen (richtig?). Nicht falsch verstehen: Ich sehe die Vorteile (schnelles Austauschen eines Service, Ausfallsicherheit der Dach-Anwendung. Skalierbarkeit), <update>Hier ein gutes Tutorial zum Thema “microservices mit Spring, Spring Boot and Spring Cloud”</update> will mich hier aber auf einen anderen Ansatz konzentrieren:

Denn “schöner” (im Sinne von gewohnter) wäre es, wenn man die einzelnen Module zu einer Anwendung zusammenstecken könnte, trotzdem mit den o.g. Eigenschaften… Und letztendlich ist das genau, wie Spring Boot an sich funktioniert: Per Maven (jaja, oder Gradle) konfiguriert man, welche “Starter” man braucht, und plötzlich hat man zB JPA-Support, kann die Adapter aber frei austauschen oder umkonfigurieren. Interessant wäre es “nur” noch, wie die Starter an sich aussehen (wie gut oder schnell lassen sie sich (weiter-)entwickeln?) und ob sie zB auch eigene URL-Endpoints erlauben (nötig für Module mit Frontendanbindung wie Kalender oder Chat).

Vorab – das geht alles.

  • Jeder Starter ist für sich eine vollwertige eigenständige Webanwendung, kann aber trotzdem ohne Änderung als Dependency eingebunden werden. Das macht das Entwickeln extrem komfortabel.
  • Die Dependencies bringen eigene Endpoints mit, die aber ebenfalls dynamisch “woanders hingeroutet” werden können. Konfiguration etc. ist ebenfalls, auch partiell, überschreibbar.
  • Es gibt im einbindenden Projekt keine zwingende Abhängigkeit im Code zum Starter. Man kann aber, und das sogar komfortabel über konditionale Annotations, darauf reagieren, ob eine Dependency da ist oder nicht. Und zB default-Implementierungen aus der Dependency überschreiben.

Im Detail: Hier gibt es ein exzellentes Tutorial, das ich mir erlaubt habe zu übernehmen; hier gibt es meine entzerrte, “white-label”-Implementierung der interessantesten Punkte:

  1. Jeder Starter (“Service A”, “Service B”) ist ein vollständiges Spring Boot Projekt, und standalone lauffähig. (“Some Projekt” ist natürlich auch standalone, allerdings müssen “Service A” und “Service B” in das lokale Repo installiert sein, siehe README)
  2. Wenn ein Starter in ein anderes Projekt eingebunden wird, muss man Spring sagen, welche Klasse die Basis-Konfiguration enthält. Dazu legt man /src/main/resources/META-INF/spring.factories an und hinterlegt die Klasse wie im Beispiel-Code.
  3. Um Namenskonflikte der Beans zu vermeiden, sollte man @ComponentScan.nameGenerator verwenden – zur Not tut es aber auch das name-Feld von @Component wie im Beispiel.
  4. Konfigurationsdateien werden per @PropertySource(“classpath:acme-aservice.properties”) eingebunden. Werte daraus werden per @Value(“${aservice.messages.test1}”) private String test1; injected oder bsplw. per @RequestMapping(“${aservice.mapping.url:/service-a}”) in einem dadurch konfigurierbaren Routing verwendet (nach dem Doppelpunkt folgt der default).
  5. Konfigurationsdateien können von haus aus nur komplett überschrieben werden, indem sie unter demselben Dateinamen im Rahmenprojekt abgelegt werden. In der Beispielimplementierung habe ich eine Methode @PostConstruct public void initialize() in “Some Project”, die das aufbohrt, und das Überschreiben von einzelnen Settings erlaubt, auch wenn diese in einer anders benannten Datei definiert sind.
  6. Das folgende Konstrukt in com.acme.bservice.IndexController erlaubt das Ersetzen von Beispielimplementierungen (MessageProvider ist ein Interface):
        @Autowired(required = false)
         public void setMessageProvider(MessageProvider messageProvider)
    Ersetzt wird der default dann völlig transparent, indem man nur das Interface MessageProvider implementiert und die Implementierung als @Component annotiert.

Für Details der Implementierung siehe Code auf Github – der ist wirklich überschaubar: “Service A” hat zwei Klassen plus eine Config, “Service B” drei Klassen, “Some Project” zwei Klassen plus Config 🙂