Eigene Doclets programmatisch ausführen – was ist neu in Java 10?

Das javadoc-Tool hat die Aufgabe, den Java-Quellcode auszulesen und die Javadoc-Kommentare zu extrahieren. Was dann mit den Daten passiert, ist die Aufgabe eines Doclets. Das Standard-Doclet von Oracle erzeugt die bekannte Struktur auf verlinkten HTML-Dateien, aber es lassen sich auch eigene Doclets schreiben, um etwa in Javadoc-Kommentaren enthaltene Links auf Erreichbarkeit zu prüfen oder UML-Diagramme aus den Typbeziehungen zu erzeugen. Hervorzuheben ist JDiff (http://javadiff.sourceforge.net/), was Differenzen in unterschiedlichen Versionen der Java-Dokumentation – wie neu hinzugefügte Methoden – erkennt und meldet.

Die Doclet-API ist Teil vom JDK, und deklariert Typen und Methoden, mit denen sich Module, Pakete, Klassen, Methoden usw. und deren Javadoc-Texte erfragen lassen. Allerdings gibt es zwei Doclet-APIs:

  • Im Paket javadoc.doclet (Modul jdk.javadoc) ist die aktuelle API.
  • Im Paket sun.javadoc ist die mittlerweile deprecated API, allerdings einfacher zu nutzen.

Beispiel

Im folgenden Beispiel wollen wir ein kleines Doclet schreiben, das Klassen, Attribute, Methoden und Konstruktoren ausgibt, die das Javadoc-Tag @since 10 tragen. So lässt sich leicht ermitteln, was in der Version Java 10 alles hinzugekommen ist. Doclets werden normalerweise von der Kommandozeile aufgerufen und dem javadoc-Tool übergeben, doch da es mit der Tool-Schnittstelle auch selbst geht, können wir ein Programm mit main(…)-Methode schreiben, das die Neuerungen auf der Konsole ausgibt.

package com.tutego.insel.tools;




import java.io.IOException;

import java.nio.file.*;

import java.util.Arrays;

import java.util.List;

import java.util.function.Predicate;

import java.util.stream.Collectors;

import javax.tools.*;

import com.sun.javadoc.*;




@SuppressWarnings( "deprecation" )

public class FindSinceTagsInJavadoc {




  public static class TestDoclet {




    public static boolean start( RootDoc root ) {

      Arrays.stream( root.classes() ).forEach( TestDoclet::processClass );

      return true;

    }




    private static void processClass( ClassDoc clazz ) {




      Predicate<Doc> hasSinceTag = doc -> Arrays.stream( doc.tags( "since" ) )

                                            .map( Tag::text ).anyMatch( "10"::equals );




      if ( hasSinceTag.test( clazz ) )

        System.out.printf( "%s -- Neuer Typ%n", clazz );




      Arrays.stream( clazz.fields() ).filter( hasSinceTag )

            .forEach( f -> System.out.printf( "%s -- Neues Attribut%n", f ) );




      Arrays.stream( clazz.methods() ).filter( hasSinceTag )

            .forEach( m -> System.out.printf( "%s -- Neue Methode%n", m ) );




      Arrays.stream( clazz.constructors() ).filter( hasSinceTag )

            .forEach( c -> System.out.printf( "%s -- Neuer Konstruktor%n", c ) );

    }

  }




  public static void main( String[] args ) throws IOException {




    Path zipfile = Paths.get( System.getProperty( "java.home" ), "lib/src.zip" );

    try ( FileSystem srcFs = FileSystems.newFileSystem( zipfile, null ) ) {

      Predicate<Path> filesToIgnore = p ->  p.toString().endsWith( ".java" ) &&

                                          ! p.toString().startsWith( "/javafx" ) &&

                                          ! p.toString().startsWith( "/jdk" ) &&

                                          ! p.toString().endsWith( "module-info.java" ) &&

                                          ! p.toString().endsWith( "package-info.java" );




      List<Path> paths = Files.walk( srcFs.getPath( "/" ) )

                              .filter( filesToIgnore ).collect( Collectors.toList() );




      DocumentationTool tool = ToolProvider.getSystemDocumentationTool();

      try ( StandardJavaFileManager fm = tool.getStandardFileManager( null, null, null ) ) {

        Iterable<? extends JavaFileObject> files = fm.getJavaFileObjectsFromPaths( paths );

        tool.getTask( null, fm, null, TestDoclet.class, List.of( "-quiet" ), files ).call();

      }

    }

  }

}

Unsere main(…)-Methode bindet zunächst das ZIP-Archiv vom JDK mit den Quellen als Dateisystem ein. Im nächsten Schritt durchsuchen wir das Dateisystem nach den passenden Quellcodedateien und sammeln sie in eine Liste von Pfaden. Jetzt kann das DocumentationTool mit unserem Doclet konfiguriert und aufgerufen werden. call() startet das Parsen aller Quellen und führt anschließend zum Aufruf von start(RootDoc) unseres Doclets.

In unserer start(…)-Methode laufen wir über alle ermittelten Typen und rufen dann processClass(ClassDoc) auf. Dort passiert der eigentliche Test. Die Metadaten kommen dabei über diverse XXXDoc-Typen. Ein Predicate zieht den Tag-Test heraus.

JSON-Serialisierung mit JSR 353/374 und JSR 367

Im Internet hat JSON das XML-Format zwecks Objektübertragung zwischen Server und Browser fast vollständig verdrängt. Das liegt daran, dass ein Browser JSON-Strings direkt in JavaScript-Objekte konvertieren kann, XML-Dokumente aber erst aufwändiger verarbeitet werden müssen.

Neben dem Einsatzgebiet im Internet bietet JSON auch ein kompaktes Format, um etwa lokale Konfigurationsdateien zu kodieren.

JSON im Kontext von JavaScript

Nehmen wir folgende Zeile JavaScript-Code, die ein Person-Objekt mit zwei Properties für Name und Alter definiert. Eine Property wird über ein Schlüssel-Wert-Paar beschrieben:

var person = { „name“ : „Michael Jackson“, „age“ : 50 };

Die Definition eines Objekts geschieht in der JSON (JavaScript Object Notation). Als Datentypen unterstützt JSON Zahlen, Wahrheitswerte, Strings, Arrays, null und Objekte – wie unser Beispiel zeigt. Die Deklarationen können geschachtelt sein, um Unterobjekte aufzubauen.

Zum Zugriff auf die JSON-Daten kommt der Punkt zum Einsatz, sodass der Name nach der Auswertung durch person.name zugänglich ist.

Eine Personenbeschreibung wie diese kann in einem String stehen, der von JavaScript zur Laufzeit ausgewertet wird.

var json = ‚person = { „name“ : „Michael Jackson“, „age“ : 50 };‘;

Der Zugriff auf person.name liefert wie vorher den Namen, denn nach der Auswertung mit eval(…) wird JavaScript ein neues Objekt mit person im Kontext anlegen.

JSON ist besonders praktisch, wenn es darum geht, Daten zwischen einem Server und einem Browser mit JavaScript-Interpreter auszutauschen. Denn wenn der String json nicht von Hand mit einem String initialisiert wurde, sondern ein Server die Zeichenkette person = { … }; liefert, haben wir das, was heutzutage in modernen Ajax-Webanwendungen passiert.

Die letzte Frage ist nun, wie elegant der Server Zeichenketten im Datenaustauschformat JSON erzeugt und so Objekte überträgt. Den String per Hand aufzubauen ist eine Lösung, aber es geht besser.

JSON-Verarbeitung mit der Java API for JSON Processing

Zum Verarbeiten von JSON-Dokumenten gibt es in der Java SE keine Standardklassen, sodass sich eine Reihe von Open-Source-Bibliotheken herausgeprägt haben; Jackson (http://tutego.de/go/jackson) gehört zu den populärsten Lösungen. 2013 wurde dann die JSR 353, »Java API for JSON Processing«, verabschiedet, die Teil der Jakarta EE 7 ist, 2014 dann das JSR 374 »Java API for JSON Processing 1.1«.

Wir können die JSON-API in unseren Java SE-Programmen nutzen, müssen dafür aber Java-Archive im Klassenpfad einbinden. Die Referenzimplementierung befindet sich unter https://javaee.github.io/jsonp/. Am einfachsten haben es Maven-Nutzer, sie binden in ihre POM die Abhängigkeiten auf den „Default provider for JSR 374:Java API for Processing JSON“ ein:

<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1</version>
</dependency>

Die Implementierung hat eine Abhängigkeit zur JSON-API javax.json:javax.json-api, die wir dann nicht unbedingt selbst in die POM mit aufnehmen müssen.

Aufbau von JSON-Objekten, Formatieren und Parsen

Der Typ JsonObject ist in der API zentral, denn er definiert ein hierarchisches Model mit den Schlüssel-Wert-Paaren eines JSON-Objekts. Um ein JsonObject aufzubauen, können wir über den JsonObjectBuilder gehen, oder wir lassen den Parser JsonReader aus einer String-Repräsentation ein JsonObject erzeugen. Zum formatierten Schreiben in einen Ausgabestrom können wir einen einfachen JsonWriter von der Json-Klasse holen – es geht aber noch einfacher über toString() – oder über die JsonWriterFactory arbeiten, falls wir eine hübsche eingerückte Ausgabe wünschen. Ein Beispiel:

Point p = new Point( 10, 20 );

JsonObjectBuilder objBuilder = Json.createObjectBuilder()
.add( „x“, p.x )
.add( „y“, p.y );
JsonObject jsonObj = objBuilder.build();

System.out.println( jsonObj );  // {„x“:10,“y“:20}

Json.createWriter( System.out ).write( jsonObj ); // {„x“:10,“y“:20}

Map<String, Boolean> config = new HashMap<>();
config.put( JsonGenerator.PRETTY_PRINTING, true );
JsonWriterFactory writerFactory = Json.createWriterFactory( config );

StringWriter out = new StringWriter();
writerFactory.createWriter( out ).write( jsonObj );
System.out.println( out );  // {\n    „x“: 10, …

JsonReader reader = Json.createReader( new StringReader( out.toString() ) );
System.out.println( reader.readObject().getInt( „x“ ) ); // 10

Soll der assoziierte Wert ein Array sein, so wird das Array mit Json.createArrayBuilder().add(..).add(..) aufgebaut und gefüllt.

JSON-Streaming API

So wie es für XML eine Pull-API gibt, existiert sie auch für JSON-Dokumente; das ist von Vorteil, wenn die Daten sehr umfangreich sind. Ein Beispiel zeigt das sehr gut:

URL url = new URL( „https://data.cityofnewyork.us/api/views/25th-nujf/rows.json?accessType=DOWNLOAD“ );

try ( JsonParser parser = Json.createParser( url.openStream() ) ) {
while ( parser.hasNext() ) {
switch ( parser.next() ) {
case KEY_NAME:
case VALUE_STRING:
System.out.println( parser.getString() );
break;
case VALUE_NUMBER:
System.out.println( parser.getBigDecimal() );
break;
case VALUE_TRUE:
System.out.println( true );
break;
case VALUE_FALSE:
System.out.println( false );
break;
case VALUE_NULL:
case START_ARRAY:
case END_ARRAY:
case START_OBJECT:
case END_OBJECT:
// Ignore
}
}
}

Zum Schreiben gibt es Vergleichbares, einen JsonGenerator; die API ist selbsterklärend:

JsonGenerator gen = Json.createGenerator( System.out );
gen.writeStartArray();
gen.writeStartObject();
gen.write( „x“, „12“ );
gen.write( „y“, „2“ );
gen.writeEnd();
gen.writeStartObject()
.write( „x“, „99“ )
.write( „x“, „123“ )
.writeEnd();
gen.writeEnd().close();
// [{„x“:“12″,“y“:“2″},{„x“:“99″,“x“:“123″}]

Der JsonGenerator ist AutoCloseable, sodass er gut in einem try-mit-Ressoucen-Block eingesetzt werden kann.

Objekt-JSON-Mapping

Das JSR 353 beschreibt kein automatisches Objekt-JSON-Mapping, wie JAXB das Objekt-XML-Mapping ermöglicht. Das wird im Standard JSR 367, »Java API for JSON Binding (JSON-B)« definiert. Die Website http://json-b.net/ liefert Hintergrundinformationen, die Referenzimplementierung ist Eclipse Yasson.

Um das mit einem Beispiel testen zu können, nehmen wie Yasson in unsere POM mit auf:

<dependency>

<groupId>org.eclipse</groupId>

<artifactId>yasson</artifactId>

<version>1.0.1</version>

</dependency>

Die Dependency selbst hat eine Maven Abhängigkeit auf javax.json.bind:javax.json.bind-api, also der eigentliche API.

Zur Abbildung bauen wir uns ein Beispielobjekt und bringen es in das JSON-Format. Im zweiten Schritt übertragen wir den JSON-String wieder auf das Objekt:

public class JsonbDemo {

public static class EvilLaboratory {

public String from;

public double volume;

public boolean didPayElectricityBill;

public List<String> items;

}

public static void main( String[] args ) {

EvilLaboratory lab1 = new EvilLaboratory();

lab1.from = „Frank“;

lab1.didPayElectricityBill = true;

lab1.volume = 12442.33;

lab1.items = List.of( „corpses“, „animals“ );

 

Jsonb jsonbuilder = JsonbBuilder.create();

String jsonString = jsonbuilder.toJson( lab1 );

System.out.println( jsonString );

 

EvilLaboratory lab2 = jsonbuilder.fromJson( jsonString, EvilLaboratory.class );

System.out.printf( „from=%s, volume=%s, didPayElectricityBill=%s, items=%s“,

lab2.from, lab2.volume, lab2.didPayElectricityBill, lab2.items );

}

}

Insel: Apache Commons CSV

Eine freie Java-Bibliothek zum Lesen und Schreiben von CSV-Dokumenten ist Apache Commons CSV (https://commons.apache.org/proper/commons-csv/). Sie verwaltet unterschiedliche CSV-Formate wie Microsoft Excel, RFC 4180.

Auf der Webseite gibt es einen Download-Link für das Java-Archiv, doch am einfachsten ist die Einbindung über die Maven-POM-Datei – wir fügen folgende Abhängigkeit hinzu:

<dependency>

<groupId>org.apache.commons</groupId>

<artifactId>commons-csv</artifactId>

<version>1.5</version>

</dependency>

Über die Abhängigkeit bekommen wir neue Typen im Paket org.apache.commons.csv. Mit dem CSVPrinter lassen sich CSV-Dokumente korrekt erzeugen, und er behandelt auch Fluchtsymbole korrekt. Erzeugen wir ein CSV-Dokument im Speicher:

StringBuilder appendable = new StringBuilder();

CSVFormat csvFormat = CSVFormat.EXCEL.withHeader( „DisplayName“, „ISO3Language“ );

try ( CSVPrinter csvPrinter = new CSVPrinter( appendable, csvFormat ) ) {

for ( Locale l : Arrays.copyOf( Locale.getAvailableLocales(), 5 ) )

csvPrinter.printRecord( l.getDisplayName(), l.getISO3Language() );

}

Starten wir das Programm erscheint auf der Konsole:

DisplayName,ISO3Language

„“,

Norwegisch Nynorsk,nno

Arabisch (Jordanien),ara

Bulgarisch,bul

Kabuverdianu,kea

Lesen wir das Dokument wieder ein und greifen dazu auf die Klasse CSVParser zurück. Es gibt zwei Ansätze:

  1. Da CSVParser ein Iterable<CSVRecord> ist, können wir direkt die Zeilen mit dem erweiterten for Das bietet sich für größere Datenmengen an.
  2. Ist die Anzahl der Daten überschaubar, liefert getRecords() eine gefüllte util.List<CSVRecord> mit alle Zeilen. Das kann praktisch sein, denn eine List hat eine stream()-Methode, sodass die Zeilen praktisch als Stream<CSVRecord> weiterverarbeitet werden können.

Zum Programm:

try ( StringReader reader = new StringReader( appendable.toString() );

CSVParser csvParser = new CSVParser( reader,

CSVFormat.EXCEL.withFirstRecordAsHeader() ) ) {

for ( CSVRecord csvRecord : csvParser )

System.out.printf( „Record=%d – %s, %s%n“,

csvRecord.getRecordNumber(),

csvRecord.get( 0 ), csvRecord.get( 1 ) );

}

Die Ausgabe ist:

Record=1 – ,

Record=2 – Norwegisch Nynorsk, nno

Record=3 – Arabisch (Jordanien), ara

Record=4 – Bulgarisch, bul

Record=5 – Kabuverdianu, kea

Automatisches Feststellen der Typen mit var

Java 10 hat eine Erweiterung gebracht, dass der Variablentyp bei gewissen Deklarationen entfallen kann und wir einfach stattdessen var nutzen können:

var name = „Barack Hussein Obama II“;

var age = 48;

var income = 400000;

var gender = ‚m‘;

var isPresident = false;

Wir sehen, dass im Gegensatz zu unserem vorherigen Beispiel nicht mehr die Variablentypen wie String oder int bei der Variablendeklaration explizit im Code stehen, sondern nur noch var. Das heißt allerdings nicht, dass der Compiler die Typen offen lässt! Der Compiler braucht zwingend die rechte Seite neben dem Gleichheitszeichen, um den Typ feststellen zu können, das nennt sich Local-Variable Type Inference. Daher gibt es in unserem Programm auch eine Unstimmigkeit, nämlich bei var income = 400000, die gut ein Probleme mit var aufzeigt: die Variable ist kein double mehr wie vorher, sondern 400000 ist ein Ganzzahl-Literal, weshalb der Java-Compiler der Variablen income den Typ int gibt.

Die Nutzung von var soll Entwicklern helfen, Code kürzer zu schreiben, insbesondere, wenn der Variablenname schon eindeutig auf den Typ hinweist. Finden wie eine Variable text vor, ist der Typ String naheliegend, genauso wie age ein int ist, oder ein Präfix wie is oder has auf eine boolean-Variable hinweist. Aber wenn var auf die Kosten der Verständlichkeit geht, darf die Abkürzung nicht eingesetzt werden. Auch der Java-Compiler gibt Schranken:

  • var ist nur dann möglich, wenn eine Initialisierung einen Typ vorgibt. Eine Deklaration der Art var age; ohne Initialisierung ist nicht möglich und führt zu einem Compilerfehler.
  • var kann nur bei lokalen Variablen eingesetzt werden, wo der Bereich überschaubar ist. Es gibt aber noch viele weitere Stellen, wo in Java Variablen deklariert werden – dort ist var nicht möglich.

Sprachvergleich: Java ist mit var relativ spät dran.[1] Andere statisch getypte Sprachen bieten die Möglichkeit schon länger, etwa C++ mit auto oder C# auch mit var. Auch JavaScript nutzt var, allerdings in einem völlig anderen Kontext: in JavaScript sind Variablen erst zur Laufzeit getypt, und alle Operationen werden erst zur Ausführungszeit geprüft, während Java die Typsicherheit mit var nicht aufgibt.

[1] http://openjdk.java.net/jeps/286

Inselupdate: Microsoft und Java

In der Anfangszeit verursachte Microsoft einigen Wirbel um Java. Mit Visual J++ (gesprochen »Jay Plus Plus«) bot Microsoft schon früh einen eigenen Java-Compiler (als Teil des Microsoft Development Kits) und mit der Microsoft Java Virtual Machine (MSJVM) eine eigene schnelle Laufzeitumgebung. Das Problem war nur, dass Dinge wie RMI und JNI absichtlich fehlten[1] – JNI wurde 1998 nachgereicht. Entgegen allen Standards führte der J++-Compiler neue Schlüsselwörter wie multicast und delegate ein. Außerdem fügte Microsoft einige neue Methoden und Eigenschaften hinzu, zum Beispiel J/Direct, um der plattformunabhängigen Programmiersprache den Windows-Stempel zu verpassen. Mit J/Direct konnten Programmierer aus Java heraus direkt auf Funktionen der Win32-API zugreifen und damit reine Windows-Programme in Java programmieren. Durch Integration von DirectX sollte die Internet-Programmiersprache Java multimediafähig gemacht werden. Das führte natürlich zu dem Problem, dass Applikationen, die mit J++ erstellt wurden, nicht zwangsläufig auf anderen Plattformen liefen. Sun klagte gegen Microsoft.

Da es Sun in der Vergangenheit finanziell nicht besonders gut ging, pumpte Microsoft im April 2004 satte 1,6 Milliarden US-Dollar in die Firma. Microsoft erkaufte sich damit das Ende der Kartellprobleme und Patentstreitigkeiten. Dass es bis zu dieser Einigung nicht einfach gewesen war, zeigen Aussagen von Microsoft-Projektleiter Ben Slivka über das JDK bzw. die Java Foundation Classes, man müsse sie »bei jeder sich bietenden Gelegenheit anpissen« (»pissing on at every opportunity«).[2]

Im Januar 2004 beendete Microsoft die Arbeit an J++, denn die Energie floss in das .NET-Framework und die .NET-Sprachen. Am Anfang gab es mit J# eine Java-Version, die Java-Programme auf der Microsoft .NET-Laufzeitumgebung CLR ausführt, doch Anfang 2007 wurde auch J# eingestellt. Das freie IKVM.NET (http://www.ikvm.net) ist eine JVM für .NET und verfügt über einen Übersetzer von Java-Bytecode nach .NET-Bytecode, was es möglich macht, Java-Programme unter .NET zu nutzen. Das ist praktisch, denn für Java gibt es eine riesige Anzahl von Programmen, die somit auch für .NET-Entwickler zugänglich sind. Bedauerlicherweise wird das Projekt nicht weiterentwickelt.

Microsoft hat sich lange Zeit aus der Java-Entwicklung nahezu vollständig zurückgezogen. Es waren eher überschaubare Projekte wie der Microsoft JDBC Driver for SQL Server. Das Verhältnis ist heute auch deutlich entspannter, und Microsoft geht wieder einen Schritt auf Java zu. Zu erkennen ist das am Beitritt in die Jakarta EE Working Group und an der Unterstützung von Java-Applikationen in der Windows Cloud Azure.[3] Vielleicht gratuliert Microsoft irgendwann einmal Oracle, wie es auch Linux zum 20. Geburtstag gratuliert hat.[4]

[1] http://www.microsoft.com/presspass/legal/charles.mspx

[2] Würden wir nicht gerade im westlichen Kulturkreis leben, wäre diese Geste auch nicht zwangsläufig unappetitlich. Im alten Mesopotamien steht »pissing on« für »anbeten«. Da jedoch die E‐Mail nicht aus dem Zweistromland kam, bleibt die wahre Bedeutung wohl unserer Fantasie überlassen.

[3] https://azure.microsoft.com/de-de/develop/java/

[4] http://www.youtube.com/watch?v=ZA2kqAIOoZM

Autor für MySQL-Administrationshandbuch gesucht

Der www.rheinwerk-verlag.de ist auf der Suche nach einem Überarbeiter für das Administrationshandbuch zu MySQL (https://www.rheinwerk-verlag.de/mysql_3843/). Das Autorenteam des bestehenden Buchs findet für die anstehende Aktualisierung auf MySQL 8
nicht mehr die Zeit. Interessierte Autoren mögen sich bitte unter melden bei:

Christoph Meister, Lektor Computing
Rheinwerk Verlag GmbH | Rheinwerkallee 4 | 53227 Bonn
Telefon +49 228 42150-45
christoph . meister @ rheinwerk – verlag . de (Space entfernen)

Actuator in Spring Boot

Gesundheitzustand über den Actuator

Das Actuator-Modul von Spring Boot stellt HTTP-Endpunkte bereit, die Auskunft über den Zustand der Anwendung geben und es erlauben, die Anwendung zu beeinflussen.

Eigene Informationen über den „Gesundheitzustand“ lassen sich einbauen.

Die Informationen werden über REST veröffentlicht, können aber auch über JMX abgerufen werden.

Achtung: Actuator 1 und Actuator 2 unterscheiden sich deutlich!

Actuator in der POM

Einbinden wie üblich über die POM:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Da ganze funktioniert nur im Web-Modus, also muss org.springframework.boot:spring-boot-starter-web auch in der POM-Datei sein.

Starte man nun die Anwendung, kann man im Log die neuen Endpunkte sehen.

Aktivierung der Endpunkte

Damit ein Actuator Endpunkt über HTPP verfügbar ist, muss er enabled und exposed sein.

  • Die meisten Endpunkte sind standardmäßig deaktiviert.
  • Man kann nur actuator/health und actuator/health nutzen, und selbst da ist wenig Info vorhanden.

Um mehr Endpunkte freizuschalten setze man die die application.properties:

management.endpoints.web.exposure.include=*

Der Wildcard * aktiviert alle, alternativ zählt man kommasepariert auf.

Auswahl einiger Spring Boot Endpunkte

Die Endpunkte liegen alle eine Ebene unter http://localhost:8080/actuator:

/beans
Alle Spring-Beans
/env
Das Environment-Properties
/health
„health information“
/info
Applikationsinformationen
/loggers
Welche Log-Level für welche Pakete gelten
/metrics
Metriken über die aktuelle Anwendung, es muss eine weitere ID folgen, etwa metrics/http.server.requests
/threaddump
Thread-Dump

Management Endpoint URL

Natürlich lassen sich Dinge wie

  • Server-Port
  • URL-Basispfad
  • freigeschaltete Endpunkte
  • SSL-Verschlüsselung

konfigurieren.

Bin ich gesund?

Unter /health finden sich Informationen über den „Gesundheitszustand“ der Anwendung.

Der Status kann UP oder DOWN sein. Standardmäßig gibt es nur status

Authentifizierung

Bei einer nicht-authentifizierten Verbindung bekommt man nicht mehr zu sehen als den Status.

Um das zu ändern, müssen wir 1. in die POM mit aufnehmen:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Dann 2. in application.properties (zum Testen!):

management.endpoint.health.show-details=always
spring.security.user.name=user
spring.security.user.password=pass

Nach dem Neustart der Anwendung und Neuladen der Webseite erfolgt eine BASIC-Authentifizierung. Geben wir user/pass ein, sehen wir alles.

Nacktes Thymeleaf

Oftmals nutzen die Beispiele im Internet Spring, oder — noch spezieller — Thymeleaf wird als Template-Engine in Spring MVC eingesetzt, daher jetzt nackt, so einfach es geht.

In unsere POM kommt:

<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf -->
<dependency>
 <groupId>org.thymeleaf</groupId>
 <artifactId>thymeleaf</artifactId>
 <version>3.0.9.RELEASE</version>
</dependency>

Wir schreiben das Template demo.html:

<!DOCTYPE html>
<html>
<body>
<p>Hey <span data-th-text="${name}">CHRISTIAN</span></p>
</body>
</html>

Und dann kann ein Java-Programm name füllen:

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templateresolver.FileTemplateResolver;

public class ThymeleafDemo {

 public static void main( String[] args ) {

  FileTemplateResolver resolver = new FileTemplateResolver();
  resolver.setCharacterEncoding( "UTF-8" );

  TemplateEngine templateEngine = new TemplateEngine();
  templateEngine.setTemplateResolver( resolver );

  Context ctx = new Context();
  ctx.setVariable( "name", "tutego" );
 
  String result = templateEngine.process( "demo.html", ctx );
  System.out.println( result );
 }
}

 

Aktuelle offene Java-Seminare Q2/Q3 2018

Spring Boot (›SPRINGBOOT‹)

22.–24. Mai 2018 (KW 21), 11.–13. Juni 2018 (KW 24), 16.–18. Juli (KW 29)

Java Grundlagen (›JAVA1‹)

14.–18. Mai 2018 (KW 20), 25.–29. Juni 2018 (KW 26), 6.–10. August 2018 (KW 32), 17.–21. September 2018 (KW 38), 5.–9. November 2018 (KW 45)

Java für Fortgeschrittene (›JAVA2‹)

28. Mai bis 1. Juni 2018 (KW 22), 9.–13. Juni 2018 (KW 28), 20.–24. August 2018 (KW 34), 8.–12. Oktober 2018 (KW 41), 26.–30. November 2018 (KW 48)

Java für C++/C#-Programmierer

23.–27. April 2018 (KW 17), 11.–15. Juni 2018 (KW 24), 23.–27. Juli 2018 (KW 30), 3.–7. September 2018 (KW 36), 22.–26. Oktober 2018 (KW 43)

Nebenläufige Programmierung mit Threads (›JAVACON‹)

7.–8. Mai (KW 19), 14.–15. Juni (KW 24), 19.–20. Juli (KW 29)

Spring Retry

Das Spring Retry Projekt

Spring Retry ist ein Zusatzprojekt (https://github.com/spring-projects/spring-retry), um Codeblöcke wiederholt auszuführen, wenn sie zu einem Fehler führen.

  • Das ist nützlich beim Ansprechen von Remote-Diensten, die temporär nicht verfügbar sein können.

Als erstes müssen zwei Dependencies in unsere POM:

<dependency>
	<groupId>org.springframework.retry</groupId>
	<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
</dependency>

@Retryable

Schreiben wir eine Bean, die zwei Anläufe braucht, eine Operation durchzuführen.

@Component
class Achiever {

  int tries = 1;

  @Retryable   
  public int tryIt() {   

    System.out.println( "Runde " + tries );
    tries++;

    if ( tries != 3 )
      throw new IllegalStateException( "Runde nicht 3" );

    return tries;
  }
}
Retryable drückt aus, das eine Operation bei Ausnahmen automatisch neu aufgerufen werden soll.
Die Methoden können eine Rückgabe haben, oder auch keine.

@EnableRetry

Damit Spring für die annotierten Methoden einen Proxy baut, müssen wir eine @Configuration mit @EnableRetry annotieren.

Wir können nun den Achiever injizieren:

@Autowired Achiever archiever;

Und die Methode ausführen:

System.out.println( "Vor dem Aufruf" );
int i = archiever.tryIt();
System.out.println( "Nach dem Aufruf ist i=" + i );

Auf der Konsole erscheint keine Exception. Nur:

Vor dem Aufruf
Runde 1
Runde 2
Nach dem Aufruf

Der Annotationsty Retryable

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
	String interceptor() default "";
	Class<? extends Throwable>[] value() default {};
	Class<? extends Throwable>[] include() default {};
	Class<? extends Throwable>[] exclude() default {};
	String label() default "";
	boolean stateful() default false;
	int maxAttempts() default 3;              
	String maxAttemptsExpression() default "";
	Backoff backoff() default @Backoff();
	String exceptionExpression() default "";
}
Die Anzahl der Wiederholungen ist nicht unendlich, Spring stoppt standardmäßig bei 3.

Ausblick

Die Dokumentation unter https://github.com/spring-projects/spring-retry zeit weitere Möglichkeiten auf. Ein zusätzliches Beispiel:

@Retryable
public void tryIO() throws IOException {
  throw new IOException( LocalTime.now().toString() );
}

@Recover  
public void recover( IOException e ) {
  System.err.println( "Aarrrrg: " + e );
}
Die mit @Recover annotierte Methode wird am Ende einer nicht erfolgreichen Aufrufkette aufgerufen.

Caching in Spring Boot nutzen

Optimierung durch Caching

Bei Methoden, die idempotent sind, also zu einem Parameter immer den gleichen Wert liefern, kann Spring einen Cache zur Optimierung einsetzen.

  • Im Grunde ist der Cache ein Assoziativspeicher, der die Methodenparameter als Schlüssel nutzt.

Beispiele:

  • Berechnen einer Prüfsumme
  • Dateiinhalt
  • DNS-Cache

Achtung: Gewisse Cache-Inhalte können nach einer Zeit ungültig werden.

Caching in Spring

Um das Caching zu aktivieren annotiert man

  1. eine @Configuration mit @EnableCaching
  2. eine public (!!!) Methode mit @Cacheable und vergibt einen Cache-Namen.

@Cacheable-Beispiel (1/2)

@Component
class Hash {

  @Cacheable( "md5" )
  public byte[] md5( String text ) {

    System.out.println( "hash: " + text );
    try {
      MessageDigest md = MessageDigest.getInstance( "MD5" );
      return md.digest( text.getBytes( StandardCharsets.UTF_8 ) );
    }
    catch ( NoSuchAlgorithmException e ) {
      throw new IllegalStateException( e );
    }
  }
}

@Cacheable-Beispiel (2/2)

Wir können Hash injizieren

@Autowired
Hash hash;

und nutzen

byte[] md5_1 = hash.md5( "tutego" );
byte[] md5_2 = hash.md5( "tutego" );
System.out.println( Arrays.equals( md5_1, md5_2 ) ); // true

System.out.println( hash.getClass() ); // com.tutego.boot.basic.Hash$$EnhancerBySpringCGLIB$$4548965

Ausblick (1/2)

  • Spring generiert den Schlüssel zum Cache aus den Argumente. Man kann kann eigene KeyGenerator en verwenden.
  • Mit der SpEL lassen sich von Anfrageobejkte zum Beispiel die Schlüssel erfragen, etwa so @Cacheable(cacheNames="books", key="#isbn.rawNumber") .
  • Unter Umständen sollen große Objekte nicht in den Cache, hier kann man eine Bedingung angeben: @Cacheable(cacheNames="book", condition="#name.length() < 32")
  • Ist eine Methode mit @CachePut annotiert, kann man den Cache selbst füllen, bzw. Werte überschreiben.
  • Ist eine Methode mit @CacheEvict annotiert, etwa @CacheEvict(cacheNames="books", allEntries=true) public void clear() wird der Cache gelöscht.

Ausblick (2/2)

Das Spring Framework kann automatisch diverse Caching-Implementierung nutzen.

  • Standardmäßig ist es eine ConcurrentHashMap.
  • Unterstützt werden u.a.: EhCache 2.x, Hazelcast, Infinispan, Couchbase, Redis, Caffeine.
  • Spring erkennt anhand des Eintrags im Klassenpfad, was gewünscht ist.

In der application.[properties|yml] lassen sich dann Dinge wie Lebensdauer, Größe, etc. extern konfigurieren.