(j)Box2d

Für unser neues Android-Gameprojekt (ist noch unter höchster Geheimhaltungsstufe) brauchte ich eine Physik-Engine. Natürlich trifft man als allererstes auf Box2D bzw. deren Java-Version jBox2D.

Ich griff zu letzterer, nachdem ich irgendwo gelesen hatte, der Performanceunterschied sei nicht allzu groß. Dass das Projekt seit 2014 kein Release mehr gesehen hat (aber durchaus Commits!), fand ich nicht weiter schlimm, es ist stabil, gut getestet, von recht hoher Codequalität und da die Physik selten Features hinzugewinnt, muss auch so eine Library nicht dauernd irgendwas nachziehen.

Ich litt etwas unter einem gewissen Mangel an Beispielcode – die meisten Programmierer verwenden heutzutage ja multiplattformhalber Unity und mühen sich nicht mit Android-Libraries ab. Ich bin da speziell – durch konsequente Nichtunterstützung von iPhones habe ich den Apfelkonzern schon an den Rand des Ruins gebracht.

Bei Levels mit etwas mehr physikalischen Objekten darin hatte die Engine merklich Probleme, die Framerate von mindestens 25fps zu halten, jedenfalls in Momenten mit vielen Kollisionen; teils flogen auch mal Teile durcheinander durch. Nun muss man wissen, dass jBox2Ds Funktion world.step() (die für die Physik zuständig ist) single-threaded ist, aber Phones und Tablets heutzutagen auch schon mit 2, 4 oder 8 Kernen daher kommen. Da wird eine Menge Potenzial nicht genutzt. Zwar gilt für die native C++-Version dasselbe, aber Maschinensprache ist eben per se schneller als Java.

Der große Aha-Effekt kam, als ich mir probeweise ein chinesisches Billig-Tablet beschaffte (XGody, 10 Zoll, knappe 70 Euro bei ebay, aber sogar mit OTG-Kabel in der Schachtel (zur hypercoolen Verwendung solcher Kabel in Kürze mehr an dieser Stelle)). Die Framerate sank bei Action im Bild auf unter 5 fps. Autsch.

Eine schnelle Messung ergab: Die Grafik ist nicht schuld, das komplette Zeichnen inklusive Debug-Hilfslinien etc. dauert (im TextureView) nur 25ms. Aber world.step() brauchte 40ms und mehr.

Also ging ich daran, jBox2D durch box2D zu ersetzen. Nun darf man sich das nicht zu einfach vorstellen, denn die Bibliothek gibt es nur im Quellcode, und übrigens auch auf dem Stand von 2014, wie gesagt: Seither hat sich an der Physik dieses Universums nichts verändert.

Die einzige praktikable Option heißt libgdx: Diese Multiplattform-Gameengine verfügt nämlich über ein Erweiterungsmodul, das die native box2D-Bibliothek enthält, und zwar verpackt in einen dünnen Java-Wrapper. Wie sich herausstellte, ließ sich jBox2D fast 1:1 durch libgdx-box2D ersetzen. Fast.

Ein paar Klassen heißen in box2D anders, z.B. Vector2 statt Vec2. Einige Attribute sind nur mit Gettern und nicht direkt zugreifbar. Und body.userdata lässt sich nicht über BodyDef setzen, nur direkt im Body. Eine Klasse AABB (zur Kollisionsberechnung) gibt es nicht, world.QueryAABB() nimmt direkt die Koordinaten des Rechtecks entgegen. Alles recht schmerzfrei.

Natürlich wollte ich nicht das ganze Projekt auf libgdx umstellen, deshalb konnte ich nicht den libgdx-eigenen Project-Wizard verwenden, der auf Wunsch direkt ein Gradle-Projekt mit Box2D-Unterstützung erzeugt. Und wie man das per Hand hinbekommt, steht nirgendwo. Also war probieren angesagt. Entscheidend sind ein paar Änderungen in der build.gradle. Als da wären:

android {
  ...
  defaultConfig {
    ...
    ndk {
      abiFilters "armeabi-v7a", "x86", "armeabi", "x86_64", "arm64-v8a"
    }
  }
}

Dies sorgt dafür, dass die App später die aufgeführten Architekturen unterstützt. Wichtig dabei: Ab 1. August ist Unterstützung der 64-Bit-Architekturen seitens Google Play obligatorisch!

sourceSets {
        main {
            jniLibs.srcDirs = ['lib']
        }
    }

Hiermit wird das Verzeichnis für die nativen Libs festgelegt. Aber woher kommen die?

task copyAndroidNatives {
    doFirst {
        file("lib/armeabi/").mkdirs()
        file("lib/armeabi-v7a/").mkdirs()
        file("lib/arm64-v8a/").mkdirs()
        file("lib/x86_64/").mkdirs()
        file("lib/x86/").mkdirs()

        configurations.natives.files.each { jar ->
            def outputDir = null
            if (jar.name.endsWith("natives-arm64-v8a.jar")) outputDir = file("lib/arm64-v8a")
            if (jar.name.endsWith("natives-armeabi-v7a.jar")) outputDir = file("lib/armeabi-v7a")
            if(jar.name.endsWith("natives-armeabi.jar")) outputDir = file("lib/armeabi")
            if(jar.name.endsWith("natives-x86_64.jar")) outputDir = file("lib/x86_64")
            if(jar.name.endsWith("natives-x86.jar")) outputDir = file("lib/x86")
            if(outputDir != null) {
                copy {
                    from zipTree(jar)
                    into outputDir
                    include "*.so"
                }
            }
        }
    }
}

tasks.whenTaskAdded { packageTask ->
    if (packageTask.name.contains("package")) {
        packageTask.dependsOn 'copyAndroidNatives'
    }
}

So wird Gradle beigebracht, die nativen Bibliotheken an Ort und Stelle abzulegen. (Man kann das später in Android Studio mit Analyze/APK… nachschauen.)

Fehlt noch:

configurations {
    natives
}
dependencies {
   ...
    implementation "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
    natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-armeabi"
    natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-armeabi-v7a"
    natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-arm64-v8a"
    natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-x86"
    natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-x86_64"
}

Tatsächlich binde ich hier NUR gdx-box2d ein, und nicht die Hauptbibliothek libgdx selbst, die ich nicht brauche.

Die zu verwendende gdxVersion lege ich in der hierarchisch obersten build.gradle fest:

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
    ext {
        gdxVersion = '1.9.9'
    }
}

Und nun zur Pointe: Die native Version der Bibliothek ist grob geschätzt um einen Faktor 25 schneller. Sogar das lahme-Ente-Tablet aus China erreicht damit eine spielbare Framerate:

Mit ins Bild geschummelt hat sich das mitgelieferte OTG-Kabel

Ein schnelleres Tablet oder Phone kommt locker auf 1ms Rechenaufwand für die Physik (also world.step()), während das Zeichnen „nur“ um einen Faktor 3 schneller ist. Aber da lässt sich sicher auch noch was optimieren, das soll ein andermal erzählt werden.

Der Teufel steckt aber mal wieder im Detail. Während jBox2D und box2d selbst ihre physikalischen Objekte (bodies) in verketteten Listen verwalten, benutzt der libgdx-Wrapper zusätzlich eine LongMap. Deren Schlüssel sind aber nicht sortiert, daher geht die Reihenfolge der Bodies zwischen Hinzufügen und Zeichnen verloren! Da keine Möglichkeit vorgesehen ist, eine z-Ebene mitzugeben, kann es passieren, dass Objekte vor anderen (also später) gezeichnet werden, die eigentlich dahinter gezeichnet werden sollen (also früher). Da hat wohl jemand tief geschlafen, als er den Wrapper implementiert hat. Mal sehen, wie ich das löse. Es bleibt spannend.

TILT: Hier geht die Reihenfolge verloren, in der die Bodys hinzugefügt werden, da der Schlüssel der verwendeten LongMap keine Reihenfolge kennt (World.java).

Fazit: Finger weg von jBox2D, sondern gleich die native Version nehmen. Schneller ist schöner.

Ich bedanke mich für die Aufmerksamkeit und gehe mal 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.

Android: Speichern ohne Genehmigung

Ein Appetizer aus meinem in Kürze erscheinenden Buch: Android-Apps entwickeln für Einsteiger (8. Auflage).

Von Ihrem PC kennen Sie den »Speichern unter«-Dialog. Jede installierte Anwendung funktioniert in dieser Hinsicht gleich: Sie überlässt Ihnen als Nutzer die Entscheidung, wo ein Dokument landen soll und unter welchem Namen.

Tatsächlich existiert ein solcher Dialog auch unter Android, bloß verwenden ihn nur wenige Apps. Dabei hat das sich dahinter verbergende Storage Access Framework sogar einen immensen Vorteil: Es kommt ohne die Genehmigung zum Speichern von Dateien auf der SD-Karte aus!

Lassen Sie uns als Beispiel einen Mini-Editor schreiben, der es dem Nutzer erlaubt, einen Text einzugeben und ihn mittels Storage Access Framework zu speichern. Dieser bescheidene Funktionsumfang rechtfertigt eigentlich nur den Projektnamen MiniMiniEditor.

Achten Sie darauf, beim Anlegen des Projekts die minSdkVersion auf 19 zu stellen (Android 4.4), denn zuvor existierte das Storage Access Framework noch nicht.

Schalten Sie außerdem die Unterstützung für Java 8 ein, da die folgenden Codezeilen eine Methodenreferenz verwenden.

Verwenden Sie im Layout activity_main.xml ein vertikales LinearLayout, und setzen Sie einen Speichern-Button und ein EditText hinein. Damit letztere View den gesamten verfügbaren Platz ausfüllt und mehrzeiligen Text entgegennimmt, setzen Sie die Attribute wie folgt:

 <EditText android:id="@+id/text"  android:layout_width="match_parent" android:layout_height="0dp"  android:gravity="top|left"  ndroid:layout_weight="1" android:inputType="textMultiLine" />

In der Klasse MainActivity verknüpfen Sie zunächst den EditText mit der festgelegten ID text mit einem Attribut editText und den Button mit einem Listener:

editText = findViewById(R.id.text); 
findViewById(R.id.save).setOnClickListener(this::onSaveClicked);

Wenn der Nutzer auf den Speichern-Button drückt, basteln Sie ein spezielles Intent-Objekt:

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); 
intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TITLE, "");
startActivityForResult(intent, REQUESTCODE_SAVE);

Entscheidend ist hier die Action ACTION_CREATE_DOCUMENT. Als EXTRA_TITLE können Sie einen Dateinamen vorgeben, den der Nutzer noch ändern kann.

Was Sie hier nicht sehen, ist der eingegebene Text: Der wird nicht etwa an den Intent gehängt, sondern in einem zweiten Schritt gespeichert. Dadurch erhält die Dateiauswahl nie Zugriff auf die fraglichen Daten. Für das eigentliche Speichern sind Sie selbst zuständig.

Deshalb starten Sie den Intent mit startActivityForResult() und übergeben einen frei definierbaren Request-Code.

Der Storage Access Provider horcht mit einem Intent-Filter darauf und präsentiert dem Nutzer einen Auswahldialog.

Standardmäßig zeigt der Speichern unter-Dialog nicht viele mögliche Ziele an: meist nur das Downloads-Verzeichnis und Ihr Google Drive, falls vorhanden. Aber hinter dem Zahnrad rechts oben erreichen Sie den Einstellungen-Dialog, wo Sie mit Erweiterte Geräte anzeigen dafür sorgen können, dass auch Ihr interner Speicher sowie eine eventuell eingesetzte SD-Karte erscheinen. Die meisten neueren Android-Geräte zeigen hier auch USB-Speichermedien an, die Sie einstöpseln.

Sobald der Nutzer das gewünschte Ziel ausgewählt hat, sendet das Storage Access Framework Ihrer Activity ein Ergebnis. Das nehmen Sie entgegen, indem Sie die Methode onActivityResult() überschreiben:

 public void onActivityResult(int requestCode, int resultCode, Intent resultData) { }

Da an dieser Stelle grundsätzlich ganz unterschiedliche Mitteilungen eintreffen können, müssen Sie den requestCode mit Ihrer Definition vergleichen. Der resultCode verrät Ihnen, ob der Nutzer den Vorgang erfolgreich beendet hat – nur dann möchten Sie etwas speichern:

if(requestCode==REQUESTCODE_SAVE
&& resultCode==Activity.RESULT_OK) {
}

Das Storage Access Framework übergibt Ihnen im Intent resultData das Ziel für Ihre Datei, und zwar in Form einer URI:

Uri
uri = resultData.getData();

Diese URI zeigt auf einen Dateipfad, aber dessen genauer Ort muss Sie nicht interessieren. Sie verwenden lediglich den ContentResolver, um sich einen OutputStream zu beschaffen, der Ihre Daten an die richtige Stelle schreibt. Das Storage Access Framework sorgt dafür, dass Sie temporär die nötigen Schreibrechte besitzen, obwohl Ihre App keinerlei Genehmigung zum Schreiben auf den Datenträger besitzt.

Der Schreibvorgang funktioniert im Fall von Textdaten am einfachsten über einen PrintWriter:

 try {
OutputStream stream = getContentResolver().openOutputStream(uri);
PrintWriter writer = new PrintWriter(stream);
writer.write(editText.getText().toString());
writer.flush();
stream.close();
} catch (java.io.IOException e) {
Log.e(getLocalClassName(),"caught IOException",e);
}

Das war schon alles. Natürlich können Sie dem Nutzer jetzt noch eine Erfolgsmeldung zeigen (und im Fehlerfall eine Fehlermeldung), aber das überlasse ich Ihnen.

Das Storage Access Framework kann auch dazu dienen, vorhandene Dateien zum Öffnen auszuwählen, z. B? Mediendateien. Als Intent-Action verwenden Sie dann ACTION_OPEN_DOCUMENT, der Rest funktioniert analog – ebenfalls ohne irgendwelche Permissions. Letztlich entscheidet der Nutzer im Einzelfall darüber, auf welche Dateien Ihre App zugreifen darf, deshalb ist die allgemeine Permission verzichtbar – das ist durchaus manchmal ein Vorteil, denn viele skeptische Nutzer installieren nur Apps, die möglichst wenige Genehmigungen einfordern.

Diese und viele andere nützliche Tipps finden Sie in meinem Buch: Android-Apps entwickeln für Einsteiger (8. Auflage), erschienen im Rheinwerk-Verlag

GIMP Beautify

Nur für GIMP 2.8 und leider nicht mehr weiterentwickelt: Die Filter-Suite „Beautify“. Leider erhöhter Schwierigkeitsgrad, man muss sie selbst kompilieren. Unter Linux geht das so:

sudo apt-get install libgimp2.0-dev
git clone https://github.com/hejiann/beautify
cd beautify
make
make userinstall

Dann noch die Texturen runterladen und in den richtigen Ordner entpacken (steht in der Wiki des Projekts). Die Filter sind dann zu finden unter Filter/Beautify.

Die Belohnung: Mit wenigen Klicks werden aus Fotos coole Hingucker. Free&Open source.

DeltaCam auf F-Droid

Nach und nach bringe ich einige meiner Open Source-Apps bei F-Droid.org online. Den Anfang macht die DeltaCam, die Bewegungen im Bild festhält. So hinterlassen laufende Ameisen schwarze Spuren auf dem Foto.

Es ist wirklich nicht ganz trivial, eine App bei F-Droid online zu bringen. Deshalb wird es in der nächsten (8.) Auflage von Android Apps entwickeln für Einsteiger ein eigenes Kapitel mit einer Schritt-für-Schritt-Anleitung geben.

Als nächstes soll der „Malkasten für Kids“ erscheinen. Wird aber noch ein paar Tage dauern.

Ach ja: F-Droid kann man als zusätzliche Shop-App installieren. Alle angebotenen Apps sind kostenlos und (soweit ich weiß) werbefrei. Eine Wohltat! Und somit die einzige echte Alternative zu Google Play, zumindest für einfache Apps wie Notizblock, Karten, Dateimanager, Mail, Multimedia, Cryptowährungen, Cloud-Synchronisation.

Mining auf Android

Ein Artikel in der letzten c’t hat mich dazu gebracht, ein brandaktuelles Thema in Angriff zu nehmen: Crypto-Currency-Mining auf Android.

Android-Geräte sind natürlich viel zu rechenschwach, um relevante Mengen Kryptowährung zu erzeugen. Aber man stelle sich vor: Ein Spiel finanziert sich nicht über Werbung, sondern über Mining. Der Spieler bezahlt für das Spiel (oder ingame-Items) nicht mit Geld oder durch Genervtwerden mit Werbung, sondern schlicht mit Akkuladung – denn die verbraucht das Mining natürlich. Wenn dann nicht 10 oder 100 oder 1000, sondern 10000 Spieler mal hier, mal da einige Minuten zocken und solange Mining betreiben, kommen ein paar digitale Münzen zusammen.

Dieses Konzept habe ich nun umgesetzt, indem ich die für ARM64-Architekturen verfügbare Version des Miners xmrig in ein Android-Projekt gepackt habe. Wer sich für die Details interessiert, kann den unter GPLv3 veröffentlichten Code anschauen. (Übrigens erlaubt GPLv3, eine Binärdatei auch in proprietärer, kommerzieller Software mitzuliefern, solange sie nicht direkt verlinkt ist – also ist der Vertrieb einer App wie das als Beispiel genannte Game erlaubt).

Oder einfach die App ausprobieren (Installation aus Drittquellen muss auf dem Android-Gerät erlaubt sein, funktioniert nur auf ARM64-Geräten): MoneroMiner.apk

Standardmäßig ist übrigens meine gähnend leere Monero-Wallet eingestellt – man muss also keine eigene haben, um die Technologie auszuprobieren.

Schließlich sei vermerkt, dass eine solche App wirklich ziemlich heißer Scheiß ist, ich bin wohl einer der ersten, der sowas gebaut hat. Daher gönne ich mir jetzt ein schönes Wochenende!

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.