Erhältlich ab: August 2013
Endlich ist es wieder so weit: nach drei Jahren Abstinenz hat es Android wieder auf das Cover des Java Magazins geschafft. Die damals gestellte Frage „Wie hebt sich Android von Java ME ab?“ kann mittlerweile leicht beantwortet werden: Android hatte Erfolg.
So viel Erfolg, dass es inzwischen zum Vorbild für andere mobile Betriebssysteme werden konnte. Ja, Sie haben schon richtig gehört: Im Internet unkt man bereits, das neue Apple iOS 7 habe Features eingeführt, die Android teils schon seit langer Zeit bereitstellt (http://bit.ly/15RvMks). Nehmen wir beispielsweise das neue Command Center in iOS 7, das mit einer Swipe-Geste aufgerufen wird, genau wie das Panel, das seit Android 4.2 aus der Notification Bar heraus aufrufbar ist. Oder nehmen wir das Multi-Tasking-Feature, die Möglichkeit, durch eine Preview aller geöffneten Apps zu swipen. Apple hatte bislang die Multi-Tasking-Bar immerhin in Form von zuletzt geöffneten Apps mit Icons dargestellt, also gilt dieser Vergleich nur teilweise. Aber haben Sie schon den neuen Lock-Screen von iOS 7 gesehen? Zumindest dieser beweist, dass die Zeit des hässlichen Entleins Android vorbei ist. Seit dem Meilenstein 4.0 ist Android so sexy geworden, dass es Maßstäbe setzen kann. Das hätte man sich vor drei Jahren noch nicht träumen lassen, oder?
Letzten Monat haben wir Ihnen bereits einen Einblick in das neue Android Studio gegeben, Googles neues Tool für Android-Entwickler. Es ist noch lang kein erwachsenes Projekt, doch zeigt die enorme Resonanz in der Community, dass Android Studio einen Nerv getroffen hat: immer mehr Entwickler suchen immer besseres Tooling für ihre Entwicklung. Und immer mehr Entwickler scheinen mit dem Status quo der Android-Entwicklung mit Eclipse unzufrieden. Google kennt die Probleme und hat sich mit JetBrains zusammengetan, um Android Studio zu entwickeln. Alle Probleme wurden aber anscheinend nicht gelöst, so sollen die alten Emulatoren im Release enthalten sein, was zu denselben Performanceproblemen führt wie in Eclipse (http://www.techwell.com/2013/06/good-and-bad-android-studio). Bis zum ersten echten Release kann das aber schon wieder ganz anders aussehen. In unserem heutigen Heftschwerpunkt zeigen wir, wo Android Studio seine Wurzeln hat: in der IntelliJ IDEA.
So oder so kann Android Studio ein wichtiger Meilenstein in Richtung Android First sein – der Punkt, an dem die Android-Entwicklung für Unternehmen und Entwickler zur Priorität wird und nicht mehr zum „zweiten Weg neben iOS“.
Im September gehen wir auf Road Tour! Java 8 steht vor der Tür und mit diesem Release auch die wichtigen Lambda-Ausdrücke. Gemeinsam mit unseren Topreferenten der JAX, Angelika Langer, Paul Sandoz, Klaus Kreft und Dalibor Topic reisen wir durch die Republik und machen Sie fit für Lambda und Java 8. Die Hacking-Events sind kostenlos, mehr Infos finden Sie hier im Heft auf Seite 14/15 und natürlich im Internet unter www.jaxenter.de/lambdatour. Um Updates zur Tour auf schnellstem Weg zu erfahren, empfehle ich Ihnen, dem Twitter- oder Google+-Account des Java Magazins zu folgen – hier werden wir als Erstes informieren, wenn eine neue Stadt, eine neue Location etc. feststeht!
Den Höhepunkt der Lambda Tour bildet der Java Core Day auf der W-JAX in München. Angelika Langer führt als Moderatorin durch den Tag, und auch hier gibt es wieder einiges zu Lambdas und Java 8 zu erfahren. Die Anmeldung zur W-JAX ist über http://jax.de/tickets möglich.
In diesem Sinne wünsche ich Ihnen viel Spaß bei der Lektüre dieser Ausgabe und hoffe, wir sehen uns „on the road“!
In jedem neuen Softwareprojekt wird, oder zumindest sollte es so sein, der Qualitätssicherung von Beginn an ein hoher Stellenwert zugeschrieben. Dies umfasst nicht nur eine hohe Testabdeckung der fachlichen Logik, sondern spiegelt sich auch im Aufbau des Quellcodes selbst wider. Qualitätssicherung in Alt- und Wartungsprojekten, die bereits ein Jahrzehnt oder länger existieren, ist hingegen eine zeitintensive Mammutaufgabe, deren Einführung gut überlegt sein will. Allgemeine Probleme und erste Lösungsansätze mit Checkstyle, PMD und einem Code Formatter stehen im Fokus des ersten Teils dieser Artikelserie.
Sobald alle Stories abgeschlossen, Features programmiert, Testfälle bestanden sind und die Software erfolgreich beim Kunden ihren Dienst verrichtet, beginnt die längste und wohl zäheste Phase im Softwareleben: die Wartung. Diese ist meist durch das Beheben von Fehlern gekennzeichnet (Abb. 1). Dabei kann zu Beginn noch auf das umfassende Fachwissen der ursprünglichen Entwickler zurückgegriffen werden, die nicht nur bestimmte Architekturentscheidungen getroffen haben, sondern auch die fachliche Qualifikation besitzen, auftretende Fehler schnell zu identifizieren und zu beheben. Mit der Zeit jedoch geht dieses Wissen verloren, sei es durch die Bearbeitung von zahlreichen neuen Projekten oder durch die Fluktuation bei den Mitarbeitern selbst. Neue, motivierte Entwickler kommen mit der nun schon in die Jahre gekommenen Software in Kontakt und müssen die aktuellen Fehler bearbeiten oder sogar neue Erweiterungen implementieren und testen.
Am einfachsten und für die neuen Mitarbeiter wahrscheinlich auch am reizvollsten wäre eine komplette Neuimplementierung der Anwendung. Nicht nur könnte man so alle alten Fehler korrigieren; es bestünde auch noch die Möglichkeit, moderne Technologien einzusetzen, neue Frameworks zu evaluieren und aktuelle Entwurfsmuster (Design-Patterns) in die Architektur mit einfließen zu lassen. Insgesamt, so die Intention, würde die Anwendung schneller reagieren, besser aussehen, Erweiterungen wären einfacher zu realisieren und das verloren gegangene Fachwissen im Unternehmen wäre wiederhergestellt.
Die Realität sieht jedoch anders aus. Anwendungen, die bereits seit Jahren erfolgreich und ohne größere Probleme in Unternehmen eingesetzt werden, werden nicht einfach ausgetauscht. Dabei spielen nicht nur die auftretenden Kosten einer kompletten Neuentwicklung eine Rolle. Hinzu kommt, dass der Kunde schlicht keinen Sinn darin sieht, ein Produkt ein zweites Mal entwickeln zu lassen, das bereits existiert, tief in die eigene Systemlandschaft integriert ist und mit dem die Mitarbeiter nach langer Einarbeitungszeit vertraut sind. Getreu dem Motto „Never change a running system“.
Die Hauptaufgabe der neuen Entwickler ist es deshalb, das alte System am Leben zu erhalten. Um Fehler zu beheben, muss man die grundlegende Architektur kennenlernen, sich durch Dutzende Klassen debuggen, Kommentare studieren und vielleicht sogar mehrere hundert Seiten lange Dokumentationshandbücher wälzen. Es muss sich schlicht eingearbeitet und das Projekt kennengelernt werden. Dass man nicht die erste Person ist, die diese Wege beschreitet, wird spätestens am Quellcode deutlich. Wenn viele Entwickler zu unterschiedlichen Zeiten an einem Projekt arbeiten, ohne dass eine einheitliche Formatierung definiert ist, gibt es mindestens doppelt so viele Codestile. Ebenso können mit der Zeit verschiedene Architekturmuster bei der Erweiterung eingesetzt worden sein. Als engagierter Mitarbeiter verspürt man nun natürlich den Drang, diese Dinge zu ändern, die Codestile zu vereinheitlichen und Methoden oder ganze Klassen einem Refactoring zu unterziehen, um die unter Umständen verloren gegangene Architektur wieder hervorzuheben. Bevor aber begonnen und versucht wird, die Applikation an mehreren Fronten zu verbessern, sollte zunächst folgende Frage gestellt werden: Ist die Verbesserung der Codequalität für mein Projekt sinnvoll und überhaupt umsetzbar?
Um diese Frage zu beantworten, muss zuerst Klarheit geschaffen werden, ob die benötigte Zeit genehmigt wird. Die Codequalität in einem Altprojekt zu verbessern, ist zeit- und arbeitsintensiv, besonders dann, wenn nebenbei noch die tägliche Arbeit verrichtet werden muss. Ein ausreichend großes Zeitbudget, verbunden mit der nötigen Ausdauer des dazugehörigen Personals, bilden die Grundvoraussetzungen für die Phase der Qualitätsverbesserung – zwei Ressourcen, die eigentlich immer zu knapp bemessen sind. Ein weiterer wichtiger Punkt ist die Planungssicherheit in Bezug auf die weitere Nutzung der Anwendung beim Kunden für die nächsten Jahre. Der eigentliche Sinn, die Codequalität zu steigern, besteht darin, die spätere Erweiterung und Fehlerbehebung zu vereinfachen und zu beschleunigen. Besitzt man nun neben dem eigenen Willen, etwas zu verbessern, auch noch ein gewisses Kontingent an Zeit und Budget, könnte die eigentliche Arbeit doch beginnen, oder?
Nein. Änderungen an einem Altprojekt, sei es durch unsachgemäße, manuell durchgeführte Codeformatierung oder durch Refactoring, bergen immer Gefahren. So kann allein das falsche Einfügen von Klammern in if-else-Anweisungen die gesamte Programmlogik verändern. Vielleicht mag so ein Fehler auf den ersten Blick banal erscheinen und erfahrenen Entwicklern in normalen Projekten auch nicht passieren. Aber bei Altprojekten ist es nicht unüblich, tausende Zeilen schlecht formatierter Klassen zu bearbeiten. Wenn dann auch noch gleichzeitig beim Kunden ein Fehler im Produktivsystem zu beheben ist, kann schnell der Überblick verloren gehen. Im Nachhinein die entsprechende Codezeile zu identifizieren kann sehr aufwändig und mitunter auch frustrierend sein. Wenn schon das Verändern von einzelnen Zeilen eine gewisse Gefahr birgt, muss beim Refactoring von Methoden oder ganzen Klassen erst recht vorsichtig agiert werden. Alter Code enthält zahlreiche, unkommentierte Bugfixes und Änderungen in der Fachlogik, die in den Dokumentationen und Handbüchern nicht kommentiert sind. Zusätzlich besteht die Gefahr, dass neue Fehler eingebaut, aber erst im Betrieb sichtbar werden. Fehlen dazu auch noch automatisierte Tests, ist ein Refactoring recht kompliziert. Es sollte dann nur in den wenigsten Fällen erfolgen und einen triftigen Grund voraussetzen. Weiterhin müssten bei einem umfassenden Refactoring die nötigen regressiven Integrationstests ausgearbeitet und implementiert werden, um eine vollständige Funktionsabdeckung aller Sonderfälle zu gewährleisten. Bei dem Einsatz neuer Technologien und Frameworks ist in der Planung der zusätzliche Zeitaufwand für das Lösen unbekannter Probleme mit einzubeziehen.
Bevor also die Codequalität verbessert werden kann, benötigt man die erforderliche Erfahrung mit dem Altsystem, um mögliche Auswirkungen abzuschätzen. Eine intensive Einarbeitung ist somit unerlässlich. Wurde der Prozess der Codequalitätsverbesserung erst begonnen, muss der Entwickler fokussiert auf sein Ziel sein und motiviert bleiben, da das Verändern von altem Code für die meisten nicht zu den interessantesten Aufgaben in der Softwareentwicklung gehört. Hat man schlussendlich die Phase erreicht, um alle Änderungen in das Produktivsystem zu überführen, sind eine umfassende Qualitätssicherung und die Beachtung von ITIL-Prozessen zur Vermeidung von Fehlern und Ausfällen essenziell.
Ein erster Schritt zur Verbesserung der Codequalität ist die Überprüfung und Anpassung der Software mit dem statischen Codeanalysetool Checkstyle [2]. Dabei setzt Checkstyle auf einen fest definierten Regelsatz, um einen einheitlichen Programmierstil sicherzustellen. Es existieren Regeln für verschiedene Komponenten, wie für:
Annotationen
Klassendesign
Namenskonventionen
Verwendung von Leerzeichen
und viele weitere (http://checkstyle.sourceforge.net/checks.html)
Checkstyle kann dabei als Plug-in direkt in Eclipse, Hudson und Jenkins integriert werden. In Eclipse bietet sich dem Entwickler die Möglichkeit, seinen geschriebenen Quellcode sofort auf Regelverstöße zu überprüfen und zu ändern. Eine Einbindung von Checkstyle in den Continuous-Integration-Prozess und damit in die Daily Builds bietet den Vorteil, dass das gesamte Projektteam auf die Fehler aufmerksam wird und diese sofort beheben kann.
Die eigentliche Hauptaufgabe ist es aber nun, den entsprechenden Regelsatz für das Altprojekt zu definieren. Bei neuen Softwareprojekten werden die Regeln entweder vom Kunden vorgegeben oder im Projektteam aufgestellt. Die spätere Software wird damit auf Basis dieser Regeln programmiert. Bei einem Altprojekt ist das der falsche Ansatz. Hier liegt ein über die Jahre gewachsenes System vor, bei dem zahlreiche Entwickler bereits ihre Vorstellungen eines guten (oder schlechten) Programmierstils umgesetzt haben. Würde der Standardregelsatz oder der eines aktuellen Projekts verwendet, wären die Regelverletzungen zu zahlreich, um sie zu beheben. Eine Anwendung der Standardregeln von Checkstyle in meinem eigenen Wartungsprojekt führte zu dem Ergebnis, dass über 80 000 Verstöße vorlagen. Kein Entwickler hätte wohl die Zeit oder die Motivation, so umfangreiche Veränderungen vorzunehmen. Wahrscheinlich könnte danach niemand mehr die Korrektheit der Software gewährleisten. Wie aber ist es dann möglich, Checkstyle in einem Altprojekt einzusetzen?
Die Lösung ist, die Regeln basierend auf der Anwendung zu erstellen. Auf den ersten Blick sieht dieses Vorgehen nicht sehr vielversprechend und erfolgreich aus. Man möchte meinen, dass Regeln, die das aktuelle Altsystem widerspiegeln und dessen Fehler nicht aufzeigen, überflüssig wären. Der Vorteil wird erst durch den kombinierten Einsatz mit einem Codeformatierungstool ersichtlich. Dazu sollte die Konzentration zuerst auf die Regeln gelegt werden, die sich mit der reinen Codeformatierung befassen, wie der Überprüfung von Codeblöcken und der Verwendung von Leerzeichen. Hier kann der aktuelle Entwickler oder das Projektteam, unabhängig vom bisher eingesetzten Programmierstil, die eigenen Vorstellungen umsetzen. Eine einheitliche Klammersetzung bei Methodendefinitionen und Anweisungen erhöhen die Lesbarkeit enorm. Überhaupt ist eine konsequente Klammersetzung (auch bei einzeiligen Anweisungen) durchaus sinnvoll, da Methoden in Altprojekten oft die Eigenschaft haben, mehrere hundert Zeilen lang zu sein. Dadurch erhöht sich nicht nur die Übersichtlichkeit. Der Quellcode kann auch einen Teil seiner Komplexität verlieren, weil zum Beispiel Berechnungen für eventuelle Sonderfälle klarer abgegrenzt sind. Weitaus vorsichtiger müssen die Regeln behandelt werden, die folgende Aspekte überprüfen:
Klassendesign
Zugriffsmodifikatoren
Größe von Dateien, Klassen und Methoden
Namenskonventionen
Java-Kommentare
Hier empfiehlt sich ein prototypischer Testlauf mit den Standardvorgaben der Regeln. Das Ergebnis ist in den meisten Fällen wohl katastrophal, da mit Sicherheit gegen fast alle Regeln wiederholt verstoßen wird. Prädestiniert sind dabei besonders die Größenüberprüfungen und die Zugriffsmodifikatoren. Es werden zahlreiche Methoden und Klassen existieren, die die vorgegebene maximale Größe weit überschreiten oder Redundanz bei den Zugriffsmodifikatoren aufweisen (so sind z. B. Methoden in Interfaces automatisch public und abstract). Hier stehen einem nun drei Lösungsmöglichkeiten zur Verfügung: Zum einen kann man die Regel anpassen, etwa indem man die maximal erlaubte Methodenlänge erhöht, zum anderen den Code, indem zum Beispiel die Methode refactored wird. Oder man entfernt die Regel aus dem Regelsatz. Das umfassende Refactoring aufgrund von Checkstyle ist meist keine Option. Würde dieser Ansatz dauerhaft verfolgt, wäre die aufzuwendende Zeit wohl selbst vor dem eigenen Management kaum zu vertreten und sollte deshalb am besten unterbleiben. Nur wenn sich der manuell zu tätigende Aufwand in Grenzen hält und die Codequalitätsverbesserung signifikant ist, kann in einigen Fällen ein Refactoring durchgeführt werden. Die Anpassung der Regel hört sich logischer an, ist aber auch nur begrenzt umsetzbar. Eine maximale Methodenlänge von 1 000 Zeilen würde zwar den alten Code nun regelkonform erscheinen lassen, hätte aber keinen Mehrwert für neu geschriebenen Quellcode, weil dieser die Regel immer erfüllen würde. Kann also weder der Quellcode noch die Regel angepasst werden, ist das einzig sinnvolle Mittel, die Überprüfung aus dem Regelsatz zu entfernen. Warnungen, die nicht behoben werden können, demotivieren, erfüllen keinen Zweck und sind daher zu vermeiden. Weiterhin könnten Warnungen, die neuen Quellcode betreffen, aus der schieren Masse nur schwer identifiziert werden.
Der Regelsatz für die Java-Kommentare ist wohl nur in den wenigsten Altprojekten noch einsetzbar. Zu häufig werden Kommentare überhaupt nicht geschrieben oder wichtige Bugfixes im Quellcode nicht kenntlich gemacht. Sollten Sie aber dennoch das Glück haben, durchgehend Javadoc-Kommentare in Ihrem Projekt zu besitzen, führen Sie diese weiter, aktualisieren Sie sie und sichern Sie sich zusätzlich mit möglichst vielen Checkstyle-Regeln ab (Listing 1.). Der Quellcode und die dazugehörigen Kommentare sind meist der erste und manchmal auch der einzige Einstiegspunkt bei Problemen in einem Altprojekt. Javadoc und zusätzliche Kommentare helfen auch, schwer verständlichen Quellcode zu begreifen. Eine weitere wichtige Regel für ein Altprojekt ist das Verbieten der Annotation @unused (Listing 2). Mit diesem simplen Mechanismus kann relativ einfach sichergestellt werden, dass als unbenutzt deklarierter Code gelöscht wird. Nach einem gewissen Zeitraum wird kein Entwickler solchen Code wiederverwenden, weil die technischen und/oder fachlichen Gründe, die dazu führten, dass der Quellcode keine Verwendung mehr findet, unklar sind.
Listing 1
<!-- Überprüft Javadoc bei public – Klassen und Interfaces -->
<module name="JavadocType">
<property name="scope" value="public"/>
</module>
<!-- Überprüft Javadoc bei public – Methoden, dabei brauchen RuntimeExceptions nicht beschrieben werden-->
<module name="JavadocMethod">
<property name="scope" value="public"/>
<property name="allowUndeclaredRTE" value="true"/>
</module>
<!-- Überprüft Javadoc bei private, protected und public – Variablen -->
<module name="JavadocVariable"/>
Listing 2
<!-- Erlaubt alle Warnungen, bis auf @unused -->
<module name="SuppressWarnings">
<property name="format" value="^unused$"/>
</module>
Bei der Erstellung eines brauchbaren Checkstyle-Regelsatzes für Altprojekte gibt es kein Patentrezept. Vielmehr werden sich die Regeln von Projekt zu Projekt unterscheiden, basierend auf dem zugrunde liegenden Quellcode. Um trotzdem eine Codequalitätsverbesserung mit Checkstyle zu erreichen, ist ein hohes Maß an Erfahrung mit dem Altprojekt vonnöten, sowie Zeit, um abzuwägen, ob die einzelnen Regeln anwendbar sind. Hat man sich schließlich auf einen Regelsatz geeinigt, muss sichergestellt werden, dass alle Klassen eine regelkonforme Formatierung aufweisen. Dies kann mit einem Codeformatierungstool erreicht werden, wie es beispielsweise in Eclipse standardmäßig integriert ist (Abb. 2, Window | Preferences | Java | Code Style | Formatter).
In den angezeigten Menüpunkten können die durch Checkstyle festgelegten Regeln in das entsprechende Profil eingetragen (Abb. 3) und nach erfolgreichem Anlegen (oder Editieren) die Formatierung auf das gesamte Projekt angewendet werden. Für die verbliebenen Warnungen kann entweder eine manuelle Anpassung oder die Entfernung aus dem Regelsatz erfolgen.
Neben Checkstyle kann eine weitere Überprüfung des Altprojekts mit PMD [1] erfolgen. Während Checkstyle sein Hauptaugenmerk auf die Formatierung des Quellcodes richtet, analysiert PMD den Programmierstil, wie das Design der Klassen, und gibt Empfehlungen auf Basis von Best Practices. PMD ist ebenfalls als Eclipse-, Jenkins- und Hudson-Plug-in [4] verfügbar. Im Gegensatz zu Checkstyle (in Verbindung mit einem Codeformatierungstool) besteht hier jedoch kaum die Möglichkeit, automatisiert Änderungen vorzunehmen und das Altprojekt an die PMD-Regeln anzugleichen. Um manuelle Anpassungen zu vermeiden, empfiehlt sich eine alternative Vorgehensweise. Anstatt aufwändig alle Regeln wie bei Checkstyle auf deren mögliches Verbesserungspotenzial für den Quellcode zu überprüfen, sollte der PMD-Regelsatz eines ähnlichen, aktuellen Softwareprojekts als Grundlage dienen. Eine erste Filterung von wichtigen Regeln wurde von dem dortigen Projektteam bereits getroffen. Gleichwohl müssen die Regeln natürlich ein zweites Mal überprüft werden. Hierfür bietet sich die direkte Darstellung in Eclipse an (Abb. 4, Window | Preferences | PMD | Rules Configuration). Nicht nur, dass die Regeln nach Kategorien sortiert sind, auch eine kurze Beschreibung wird angezeigt, die meist neben dem aussagekräftigen Regelnamen selbst ausreicht, um den Zweck der Überprüfung zu verstehen. Durch ausreichend Erfahrung mit dem Quellcode des Altprojekts sollte es dem Entwickler möglich sein, alle Regeln für das Altprojekt ohne umfangreiche Analyse und Suche in eine der folgenden drei Kategorien einzuteilen:
Sinnvolle und zutreffende Regeln
Zu entfernende Regeln
Regeln, die möglicherweise entfernt werden sollten und/oder bei denen eine weitere Verifizierung sinnvoll erscheint
Dabei können die letzten zwei Listen recht schnell anwachsen. Sollten Regeln in die letzte Kategorie einsortiert werden, muss genau abgewogen werden, inwieweit manuelle Formatierungen oder Refactorings sinnvoll und zeitlich machbar sind. Besonders Regeln der folgenden Kategorien sollten besser entfernt werden, als den Versuch zu unternehmen, das Altprojekt anzupassen:
Design
Migration
Quellcodegröße
Als Vorzeigebeispiele können hier wiederum die Regeln genannt werden, die die Länge von Methoden und Klassen oder die Anzahl der Übergabeparameter einer Methode überprüfen. Berechtige Ausnahmen bilden, wie bei Checkstyle, die Regeln, die unbenutzten Code identifizieren (Listing 3). Nicht nur, dass das Löschen zügig erledigt ist, es wird ebenfalls die Übersichtlichkeit in den Klassen verbessert und die Einarbeitung für neue Mitarbeiter erleichtert. Analog zu Checkstyle gibt es auch für PMD keinen allgemeingültigen Regelsatz. Vielmehr muss man darauf vorbereitet sein, eine hohe Anzahl an Regeln zu entfernen. Dadurch kann aber immer noch eine, wenn vielleicht auch nur geringe, Codequalitätsverbesserung erreicht werden.
Listing 3
<!-- Identifiziert unbenutzten Quellcode -->
<rule ref="ruleset/unusedcode.xml/UnusedPrivateField"/>
<rule ref="ruleset/unusedcode.xml/UnusedLocalVariable"/>
<rule ref="ruleset/unusedcode.xml/UnusedPrivateMethode"/>
<rule ref="ruleset/unusedcode.xml/UnusedFormalParameter"/>
Im zweiten Teil der Artikelserie zeige ich, wie mit dem Framework Mockito [5] die Testabdeckung von neuem und altem Quellcode erhöht und verbessert werden kann. Weiterhin erkläre ich, wie man am besten mit alten fachlichen und technischen Fehlern umgeht und warum eine umfangreiche Dokumentation nicht nur der Zierde dient. Abschließend werden Möglichkeiten aufgezeigt, um die verbesserte Codequalität auch bei zukünftigen Erweiterungen und Fehlerbehebungen zu erhalten.
Daniel Winter studierte Informatik an der FH Zittau/Görlitz und arbeitet seit 2011 als Consultant für Softwareentwicklung bei der Saxonia Systems AG. Sein aktueller Fokus liegt in den Möglichkeiten der Codequalitätsverbesserung bei Projekten jedweder Art. Ausgiebige Erfahrungen in diesem Bereich sammelte er bei der Wartung einer Leasing-Refinanzierungssoftware.
[1] PMD: http://pmd.sourceforge.net/
[2] Checkstyle: http://checkstyle.sourceforge.net/
[3] Checkstyle Eclipse – Plug-in: http://eclipse-cs.sourceforge.net/
[4] Checkstyle Jenkins – Plug-in: https://wiki.jenkins-ci.org/display/JENKINS/Checkstyle+Plugin
[5] Mockito: http://code.google.com/p/mockito/
Die Kombination von Querydsl und Spring Data ist derzeit konkurrenzlos. Kein anderes Framework schafft es, die Menge an Boilerplate-Code so konsequent zu reduzieren und dabei ein so elegantes API bereitzustellen. Getreu dem Prinzip: „Die beste Zeile Code ist die, die man erst gar nicht schreibt“ enthalten Repositories am Ende nur das, was sie auch wirklich brauchen. Für den Entwickler bedeutet das: mehr Spaß bei der Arbeit mit relationalen Datenbanken.
Ziel dieses Artikels ist, zu zeigen, wie die Verwendung von Spring Data in Verbindung mit JPA 2 und Querydsl das Leben extrem erleichtern kann. Vorausgesetzt wird, dass man weiß, wie man einen ORM (Object Relational Mapper) aufsetzt und die Konzepte hinter JPA 2 (Annotationen, EntityManager usw.) beherrscht. Den vollständigen Code zu diesem Artikel samt der zugehörigen ORM-Konfiguration findet man auf GitHub [1].
ORMs sind nicht aus der Softwareentwicklung wegzudenken. In Sachen Performance und Verwendung von speziellen DB-Features hat sich hier einiges getan. Auf der Strecke blieben dabei notwendige Weiterentwicklungen des API. Es mag ein subjektiver Eindruck sein, aber seit einer gefühlten Ewigkeit waren wir gezwungen, Mapping-Informationen in XML-Dateien abzulegen und Querys mühsam per Hand zu schreiben und zu pflegen.
Typsicherheit war hier schon immer ein Fremdwort, und die Unmengen an Boilerplate-Code waren nur schwer zu verstecken. Ständig hat man aus älteren Projekten DAO-Implementierungen kopiert oder sich zum x-ten Mal über einen Tippfehler in der Query geärgert.
Zugegeben, die Sache mit den XML-Mapping-Informationen sind wir mittlerweile losgeworden, auch wenn es immer noch genügend Leute gibt, die sich dagegen wehren. Die fehlende Typsicherheit ist das größte Problem. Egal, wie intelligent eine IDE mit der Erzeugung einer Query umgeht, er bleibt ein String, und Probleme werden im schlimmsten Fall erst erkannt, wenn es schon zu spät ist. Wer jetzt das Criteria-API anführen möchte, sei auf später vertröstet.
Bei Spring Data handelt es sich um ein „Umbrella“-Projekt ähnlich Spring Security oder Spring Social. Ziel war es, die Verwendung verschiedenster Data Stores so einfach wie möglich zu gestalten, ohne dabei Funktionalitäten zu beschränken. Dabei legte man besonderen Wert darauf, die Unterschiede der Stores nicht zu verwischen. Schließlich möchte man keine relationale Algebra auf Neo4j abbilden, sondern mit Graphen arbeiten.
Das so entstandene Projekt bietet Anbindungen für verschiedenste dokumentenorientierte Datenbanken, Key-Value Stores und andere alternative Konzepte. Daneben hat man auch daran gedacht, sich um den immer noch dominantesten Anteil unter den Data Stores zu kümmern: die relationale Datenbank.
ORM ist für viele Datenbankanwendungen das Mittel der Wahl, wenn es darum geht, von Java aus mit einer DB zu interagieren. Über den richtigen ORM lässt sich vortrefflich streiten. Glücklicherweise will sich Spring Data in diese Diskussion nicht einmischen. Man setzte auf die Verwendung von JPA 2 als Basis. Durch diesen Standard war es möglich, fast vollkommen unabhängig von einer konkreten Implementierung zu bleiben. JPA 2 ist aber bei Weitem nicht perfekt, und man ist immer noch an einigen Ecken dazu gezwungen, Vendor-Erweiterungen zu verwenden. Für die Aufgaben von Spring Data benötigt man aber keine davon.
Details zur Konfiguration von Hibernate findet man, wie eingangs erwähnt, im Beispielprojekt. Um nun Spring Data verwenden zu können, muss man die entsprechende Abhängigkeit im Build eintragen:
org.springframework.data:spring-data-jpa:1.3.1.RELEASE
Nachdem diese angezogen wurde, muss noch der Spring-Applikationskontext erweitert werden. Hier hat man sich sehr stark am Context Component Scan-Mechanismus orientiert. Spring Data benötigt nur die Information, in welchen Packages es nach Repositories zu suchen hat. Folgendermaßen wird dies mit Java Config erreicht:
...
@EnableJpaRepositories("com.senacor.repository")
public class MyConfiguration {
Für diejenigen, die kein Java Config einsetzen, zeigt Listing 1 das äquivalente Beispiel in XML.
Listing 1
...
<beans
...
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="
...
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
...>
<jpa:repositories base-package=" com.senacor.repository " />
</beans>
In beiden Fällen weise ich Spring Data an, im Package com.senacor.repository nach Repository-Definitionen zu suchen. Jetzt wird es Zeit, das erste Repository zu definieren.
Repositories sind der Dreh- und Angelpunkt aller Spring-Data-Projekte. Sie sind die Abstraktion zum Zugriff auf den jeweiligen Data Store. Im Fall von JPA 2 kapselt ein Repository den EntityManager und stellt verschiedene Kombinationen von Methoden zum Umgang mit selbigem bereit. Am besten lässt sich das mit einem Beispiel erklären. Anhand der UserEntity aus Listing 2 werde ich den Repository-Mechanismus näher erklären.
Listing 2
@Entity
public class UserEntity implements Serializable {
@Id
private Long id;
private String firstname;
private String lastname;
@Temporal(TemporalType.DATE)
private Date birthday;
...
Um ein CrudRepository (Create, Retrieve, Update, Delete) zu erzeugen, muss ich nur das entsprechende Interface (Listing 3) ableiten.
Listing 3
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
Das Listing wird dann in einem der von mir festgelegten Packages (s. o.) abgelegt:
public interface UserEntityRepository extends CrudRepository<UserEntity, Long>{}
Von jetzt an lässt sich mein UserEntityRepository durch Einsatz von @Autowired an beliebiger Stelle in der Anwendung verwenden:
@Autowired
UserEntityRepository userEntityrepository;
...
Iterable<UserEntity> allEntities = userEntityrepository.findAll();
...
Das war’s auch schon. Von nun an sind so alle essenziellen CRUD-Operationen für die UserEntity verfügbar.
Eine Zeile Code (wenn man die Imports unterschlägt), um ein Repository zu erzeugen – da wird so mancher böse Magie vermuten. Entzaubern wir das Ganze: Spring Data setzt, genau wie Spring, sehr stark auf die Verwendung von Proxies. Für jedes Interface, das von org.springframework.data.repository.Repository ableitet, wird somit ein entsprechender Proxy erzeugt. Dessen Aufgabe ist es, Aufrufe am Interface an ihre Entsprechung am SimpleJpaRepository durchzuleiten. Das SimpleJpaRepository stellt alles an benötigten Methoden und Funktionalitäten bereit. Die Repository-Interfaces werden genutzt, um immer nur das bereitzustellen, was auch tatsächlich benötigt wird. Das Ganze wird mit einem Beispiel etwas klarer.
Wie wäre es z. B. mit einem ReadOnly-Repository für meine UserEntity? Dazu werfen wir einen Blick in die Definition des CrudRepository (Listing 3, eine schamlose Kopie aus den Spring-Quellen).
Mit diesem Wissen kann ich mein eigenes ROCrudRepository (Listing 4) erzeugen. Wichtig ist hierbei die Verwendung von @NoRepositoryBean. Diese weist Spring Data das Interface beim Scan zu ignorieren, obwohl es von Repository ableitet.
Listing 4
@NoRepositoryBean
public interface RORepository<T, ID extends Serializable> extends Repository<T, ID> {
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
}
Der Vollständigkeit halber ist hier die Verwendung des neuen Repository-Typen:
public interface ReadOnlyUserEntityRepository extends RORepository<UserEntity, Long>{}
Repositories sind also extrem flexibel an den jeweiligen Anwendungsfall anpassbar. Sie sind typsicher und bieten bis auf eine „Kleinigkeit“ alles, was man braucht.
Was noch fehlt, ist die Möglichkeit, eigene Abfragen zu formulieren. Hier bietet Spring Data mehrere Wege. Für die folgenden Beispiele soll eine Query erzeugt werden, die alle User mit einem bestimmten Nachnamen und einem teilweise angegebenen Vornamen liefert:
select user from User user where user.firstname like :firstname and user.lastname = :lastname
Die einfachste Variante, sie zu erzeugen, ist, sie gar nicht erst selbst zu schreiben, sondern die Arbeit Spring Data zu überlassen. Bereits erklärt wurde, wie die Methoden aus den Repository-Interfaces auf das SimpleJpaRepository abgebildet werden. Für alle Methoden, die keine Entsprechung an selbigem haben oder anderweitig implementiert wurden, erzeugt Spring Data eine Query. Der Mechanismus funktioniert dabei wie folgt:
Eine Methode, die mit findBy beginnt, wird automatisch als Kandidat erkannt.
Bei allen anderen Methoden überprüft man, ob sie mit dem Namen einer Property der Entität beginnen.
Die Parameter der Methode werden entsprechend der Reihenfolge als Queryparameter interpretiert.
Eine solche Methode sieht wie folgt aus:
public interface UserEntityRepository extends CrudRepository<UserEntity, Long>{
List<User> findByFirstnameLikeAndLastnameEquals(String firstname, String lastname);
}
Alternativ hätte ich auch das findBy weglassen können, ich bevorzuge aber explizite Methodennamen. Ob man diese Variante insgesamt mag oder nicht, ist Geschmackssache. Aber es gibt noch mehr Möglichkeiten, mein Ziel zu erreichen.
Manchmal benötigt man nur einen Tick mehr Kontrolle über die erzeugte Query. Diese liefert Variante zwei. Hierbei werden die Query und ihre Parameter durch die Verwendung von @Query- und @Param-Annotationen erzeugt:
public interface UserEntityRepository extends CrudRepository<UserEntity,Long>{
@Query("select user from UserEntity user where user.firstname like :firstname% and user.lastname = :lastname")
List<UserEntity> findWhereFirstnameLikeAndLastnameEquals(@Param("firstname") String firstname, @Param("lastname") String lastname);
}
So können deutlich komplexere Querys formuliert werden, ohne dass man signifikant mehr Code benötigen wurde. Für die ganz harten Fälle gibt es dann noch eine dritte Variante. Bei dieser übernehme ich die Queryerzeugung komplett selbst. Allerdings muss ich hierfür die bisher vorgestellten Mechanismen umgehen. Dies gelingt durch die Auslagerung der entsprechenden Methoden in ein separates Interface:
public interface UserEntityRepositoryCustom {
List<UserEntity> findByFirstnameLikeAndLastnameEquals(String firstname,String lastname);
}
Daraufhin wird eine Implementierung bereitgestellt (Listing 5).
Listing 5
public class UserEntityRepositoryImpl implements UserEntityRepositoy{
@PersistenceContext
EntityManager em;
List<UserEntity> findByFirstnameLikeAndLastnameEquals(String firstname, String lastname) {
Query q = em.createQuery("select user from UserEntity user where user.firstname like :firstname% and user.lastname = :lastname");
q.setParameter("firstname", firstname);
q.setParameter("lastname", lastname);
return q.getResultList();
}
}
public interface UserEntityRepository extends CrudRepository<UserEntity,Long>, UserEntityRepositoryCustom {}
Eine Sache stört allerdings an allen bisher vorgestellten Varianten: Sie sind weder Typ- noch Refactoring-sicher. Der informierte ORM-Entwickler wird jetzt natürlich den Finger erheben und anmerken, dass ich Variante drei durch die Verwendung des JPA-2-Criteria-API typsicher machen könnte. Die schreckgeweiteten Augen seiner Kollegen werden ihn dann schnell seinen Finger wieder sinken lassen. Wer schon mal mit diesem API-Verbrechen zu tun hatte, wird verstehen, weshalb ich sie für ORM meide wie Struts für Webanwendungen. So gut die Ideen auch sein mögen, die in ihr stecken, so sehr verdarben mir die Unmengen an Boilerplate und das akademische API-Design den Spaß an der Arbeit damit (dieser Satz sollte auf keinen Fall als Aufwertung von Struts gewertet werden). Glücklicherweise gibt es seit einiger Zeit eine großartige Alternative: Mit Querydsl wird es zum Kinderspiel, alle meine Anforderungen zu erfüllen.
Zwei Abhängigkeiten sind notwendig, um Querydsl nutzen zu können:
com.mysema.querydsl:querydsl-jpa:2.9.0
com.mysema.querydsl:querydsl-apt:2.9.0
Die Erste ist das eigentliche API zum Umgang mit JPA 2 (es gibt noch eine ganze Reihe anderer Anbindungen, die allerdings nicht in den Rahmen dieses Artikels passen). Bei der Zweiten handelt es sich um den JPA Annotation Processor. Dieser wird benötigt, um die statischen Metainformationen zur Build-Zeit zu erzeugen. Wie, das sieht man in Listing 6 und 7. Sie zeigen die Verwendung mit Gradle, respektive Maven.
Listing 6
dependencies {
compile libs.jpa20
compile libs.querydsl_apt
compile libs.commons_lang
}
def generatedDir = "$projectDir/src/main/generated"
sourceSets {
generated.java.srcDirs = [generatedDir]
}
configurations {
querydslapt
}
task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
source = sourceSets.main.java
classpath = configurations.compile + configurations.querydslapt
options.compilerArgs = [
"-proc:only",
"-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
]
destinationDir = new File(generatedDir)
dependencyCacheDir = new File("$buildDir/dependencyCacheDir")
}
Listing 7
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>maven-apt-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>process</goal>
<goal>test-process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
Bei den statischen Metainformationen handelt es sich um die so genannten Queryobjekte.
Nach dem Lauf des JPAAnnotationProcessor findet man für jede Entität eine weitere Klasse mit dem gleichen Namen und einem vorangestellten „Q“ (in meinem Beispiel wird zur existierenden UserEntity die QUserEntity erzeugt). Sie enthält statt der Entity Properties eine Pfadbeschreibung in Form von public static Members. So gibt es statt (get/set)Firstname eine firstname-Methode. Diese werden wiederum in Querys eingesetzt und liefern uns alles, was wir brauchen, um mithilfe des Builder Patterns eine Query aufzubauen (Listing 8).
Listing 8
public class UserEntityRepositoryImpl implements UserEntityRepositoy{
@PersistenceContext
EntityManager em;
List<User> findWhereFirstnameLikeAndLastnameEquals(String firstname,String lastname) {
JPAQuery query = new JPAQuery(entityManager);
QUserEntity qUserEntity = QUserEntity.userEntity;
return query.from(qUserEntity).where(qUserEntity.firstname.like(firstname).and(qUserEntity.lastname.eq(lastname))).list(qUserEntity);
}
}
Diese Lösung ist typsicher, Refactoring-sicher und gut lesbar. Ganz zufrieden bin ich aber immer noch nicht. Auch für einfache Querys ist immer noch vergleichsweise viel Code nötig. Das geht noch besser.
Fangen wir mit dem UserEntityRepository noch mal von vorne an und fügen das Interface QueryDslPredicateExecutor hinzu:
public interface UserEntityRepository extends CrudRepository<UserEntity,Long>, QueryDslPredicateExecutor<UserEntity>{
}
Durch diese kleine Änderung bekommen wir einen ganzen Satz neuer Methoden:
public interface QueryDslPredicateExecutor<T> {
T findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
long count(Predicate predicate);
}
Diese enthalten in ihrer Signatur das Predicate-Objekt, bei dem es sich um eine Repräsentation der where Clause handelt. Mithilfe der Q-Objekte ist der Aufbau der Prädikate sehr einfach, und wir können die folgende Beispielquery erzeugen:
@Autowired
UserEntityRepository userEntityrepository;
...
QUserEntity qUserEntity = QUserEntity.userEntity;
userEntityrepository.findAll(qUserEntity.firstname.like(firstname).and(qUserEntity.lastname.eq(lastname)));
...
Und mit diesem Beispiel bin ich da angekommen, wo ich hin wollte. Querys lassen sich durch einfaches Betätigen der Auto-Complete-Funktion erstellen. Änderungen an den Entitäten führen zu Build-Fehlern, und ihre Auswirkungen werden frühzeitig erkannt. Der komplette Zugriff auf die Datenbank wurde gegenüber reinem JPA 2 mit dem Criteria-API deutlich entschlackt.
Ich gebe es zu: Ich bin schon immer ein Spring-Fan und hatte noch nie ein Problem damit, statt des gesetzten den gelebten Standard zu verwenden. Nach der Enttäuschung, die JPA 1 darstellte, war ich dann in vielen Punkten doch angenehm von JPA 2 überrascht. Das änderte sich schnell, als ich das Criteria-API das erste Mal in einem Projekt einsetzen durfte.
Die zugrunde liegenden Ideen waren ja sehr gut – aber was nützen alle guten Ideen, wenn sie sich hinter einem derart hässlichen API verstecken? Der Wechsel zu Querydsl war die logische Konsequenz, und ich habe es bisher in keinem Projekt bereut. Im Gegenteil: Neue Entwickler kommen mit diesen Konzepten deutlich besser zurecht und finden einen schnelleren Einstieg in das Arbeiten mit ORMs.
Die Kombination von Querydsl und Spring Data ist in meinen Augen derzeit konkurrenzlos. Kein anderes Framework schafft es, die Menge an Boilerplate-Code so konsequent zu reduzieren und dabei ein so elegantes API bereitzustellen. Getreu dem Prinzip: „Die beste Zeile Code ist die, die man erst gar nicht schreibt“ enthalten Repositories am Ende nur das, was sie auch wirklich brauchen.
Wie bereits erwähnt, gibt es eine Vielzahl verschiedener Integrationen im Spring-Data-Projekt, und ich kann jedem Entwickler nur empfehlen, sich mit diesen zu beschäftigen. Ich selbst verwende derzeit die Integration von Neo4j und MongoDB und bin mehr als zufrieden. Die aktive Community beschert uns hier eine wachsende Anzahl von Optionen. Kompliment an die Teams von Spring Data und Querydsl!
Jochen Mader ist Chief Developer bei der Senacor Technologies AG, wo er sich mit der Umsetzung komplexer Mehrschichtenanwendungen beschäftigt. Angetrieben durch seine Erfahrungen aus Codereviews ist er immer auf der Suche nach Techniken, um Copy and Paste und Boilerplate-Code vollständig in Projekten zu eliminieren.
[1] http://github.com/codepitbull/javamagazin-spring-data-jpa-querydsl