Die Ausgabe des Logos von Jooq verhindern

Beim Starten eines Queries wird normalerweise das Logo von Jooq ausgegeben. Das ist auch nicht weiter schlimm, es ist nur beim Debuggen der Anwendung manchmal ein wenig störend, weil es viel Platz auf der Konsole einnimmt.

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@  @@        @@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@        @@@@@@@@@@
@@@@@@@@@@@@@@@@  @@  @@    @@@@@@@@@@
@@@@@@@@@@  @@@@  @@  @@    @@@@@@@@@@
@@@@@@@@@@        @@        @@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@        @@        @@@@@@@@@@
@@@@@@@@@@    @@  @@  @@@@  @@@@@@@@@@
@@@@@@@@@@    @@  @@  @@@@  @@@@@@@@@@
@@@@@@@@@@        @@  @  @  @@@@@@@@@@
@@@@@@@@@@        @@        @@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@  @@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  Thank you for using jOOQ 3.11.9

Dank geht an dieser Stelle an Lukas Eder für dieses tolle Werkzeug.

Wie kann nun das Logo abgeschaltet werden? Dazu muss nur die Property org.jooq.no-logo auf true gesetzt werden. Ich löse das in dem ich eine Klasse JooqConfig mit einem Bean erstelle. Die Komponente wird so beim Component Scan gefunden und die Property gesetzt, sodass beim Starten kein Logo angezeigt wird.

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class JooqConfig {
    @Bean
    private void disableLogo() {
        System.setProperty("org.jooq.no-logo", "true");
    }
}

Credentials auslagern

Neben der Möglichkeit die Credentials mit dem Gradle Plugin Credentials zu verwalten, kann man auch zum Beispiel dieses in eine weitere Datei auszulagern. Hierfür bietet sich YAML an.

Hier die jooq.yaml

---
jdbc:
  password: secret
  username: usr

In dem Buildscript jooq.gradle lesen wir die Daten aus dem YAML ein. Das SnakeYaml nutze ich hierfür um die cfg zu definieren.

...
jooq {
  def cfg = new org.yaml.snakeyaml.Yaml().load(new File("" + rootProject.projectDir + "/jooq.yml").newInputStream())
...

Weiter unten im Abschnitt xxx können jetzt die Credentials aus dem jooq.yaml angegeben werden…

...
    jdbc {
      driver = 'org.mariadb.jdbc.Driver'
      url = 'jdbc:mariadb://server:3306/database'
      username = cfg.jdbc.username
      password = cfg.jdbc.password
    }
...

Projekt reactor

Das Spring Framework nutzt für den Starter spring-boot-starter-webflux das Projekt reaktor. Dieses verleiht dem Spring Framework reaktive Streams. Das Framework Reactor ist von Hause aus Multithreading fähig, so dass es dem Entwickler hier hilft.

Warum reaktiv?

Meine Anwendung ist doch asynchron und verwendet Threads! Warum soll sie jetzt auf einmal reaktiv sein? Was ist der Unterschied zwischen asynchron und reaktiv? Diese Fragen stellte ich mir auch. Nun ja, einer der Unterschiede ist, dass das Framework ein Backpreasure kennt. Dieser “Gegendruck” verhindert zum Beispiel das ein Subscriber mit den vom Publisher gelieferten Daten überfordert wird.

Mono und Flux

Mono hat genau 0 … 1 Elemente und Flux hat 0 … N Elemente.

Hot und Cold Streams

Bei Hot-Streams ist die Anzahl der Elemente unbekannt und fortwährend. Man stelle sich zum Beispiel eine Datenquelle wie einen Temperatursensor vor. Solange die Anwendung läuft, können wir Messwerte abfragen und das unendlich.

Nimmt man als Quelle für einen reaktiven Stream zum Beispiel eine Liste, dann ist die Anzahl der Elemente bekannt und endlich. In der Sprache von reaktor heißt das Cold-Stream.

Es gibt noch einen weiteren unterschied zwischen Hot- und Cold-Streams: Sind Elemente in einem Hot-Stream einmal konsumiert durch einen Subscriber, dann sind Sie weg. Schreibt man sich bei einem Cold-Stream ein zweites mal ein, dann werden erneut alle Elemente publiziert.

Dependencies

implementation('io.projectreactor:reactor-core')


dependencyManagement {
     imports {
          mavenBom "io.projectreactor:reactor-bom:Californium-RELEASE"
     }
}

Wie mache ich aus einem blockierenden Methodenaufruf ein reaktiven Stream?

Mit der Methode .fromCallable() von Mono kann man einen blockierende Aufruf als Lambda Funktion übergeben und erhält unmittelbar ein Mono Objekt zurück. Als kurzes Beispiel wird hier eine blockierende Methode getInfo() gewrappt.

/**
    * Wrap blocking service call getInfo
    * 
    * @param Long itemId
    * @return Mono<Info>
    */
private Mono<Info> getInfo(Long id) {
        Mono<Info> blockingWrapper = Mono.fromCallable(() -> { 
                return service.getInfo(id);
        });

        return blockingWrapper.subscribeOn(Schedulers.elastic());
}

Das ganze kann man jetzt in einen Stream aus id verpacken.

private Flux<Info> getInfos(Flux<Long> ids) {
        return ids.flatMap(id -> getInfo(id), 2); // max 2 concurrent
}

Begrenzung der Aufrufe

In dem letzten Abschnitt haben wir gesehen wie man einen blockierenden Aufruf wrappen kann. In reaktiven Streams ist es so, dass keine Daten produziert werden solange kein Subscriber vorhanden ist. Wenn jetzt ein Subscriber für den Flux von getInfos() aboniert, dann werden alle id parallel im Excecutor ThreadPool abgfragt. Dieses kann zum Beispiel bei teuren Funktionen wie zum Beispiel Rest Calls zu einer Überlastung führen. Daher muss hier ein weiterer Parameter in flatMap mit angegeben werden. Damit kann man die Anzahl der Concurrent Calls begrenzen. Hier in diesem Beispiel sind immer maximal 2 konkurierende Aufruf gleichzeitig.

Codepfade

In einem Projekt das für Eingangsdaten sehr viele unterschiedliche Parser verwenden muss, ist es nur natürlich das wenn dieses in einer Klasse geschehen würde, dass das sehr unübersichtlich wird. Daher ist es besser, wenn man die Ganzheit in kleinere separate Parserklassen splittet.

Vor Spring

Nach diesem Schema bin ich in dem Projekt auch vorgegangen und habe für jede Gruppe einen eigenen Parser geschrieben. Alle Parser implementieren das Interface Parser, sodass sich folgender Code ergab:

//
// Instantiate class and execute parse
//
try {
    Class<?> c = Class.forName("de.pfau.parser.Parser00001");

    try {
        Parser p = (Parser) c.getDeclaredConstructor().newInstance();
        autowireCapableBeanFactory.autowireBean(p);

        p.parse();
    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
        throw new Exception ...
    } 
} catch (ClassNotFoundException e) {
    log...
}

Da das Projekt noch aus einer Zeit stammt in der es noch nicht mit Spring kontakt hatte, kommt hier die newInstance() Methode zum Einsatz. Damit in den Klassen auch das Autowiring weiterhin funktioniert, muss nach der Verwendung von DI mit autowireCapableBeanFactory.autowireBean(p) Spring hierüber in Kenntnis gesetzt werden. Ansonsten haben die Autowired Felder null Werte.

Die Umsetzung mit Spring

Spring bringt hierfür Frist Class Support mit, da das grundlegend für das Dependency Injection ist.

Mit dem ClassPathScanningCandidateComponentProvider kann man den Classpath durchsuchen. Beim Instanzieren des Objektes kann ein Parameter useDefaultFilters angegeben werden. Setzt man ihn auf false, dann hat man eine leere Filterkette.

ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);

Als nächstes fügen wir einen neuen Includefilter ein. Es gibt verschiedene Filter die eingefügt werden können (siehe TypeFilter). Für meine Zwecke eignet sich hier der RegexPatternTypeFilter. Als Parameter übergeben wir einen kompilierten RegEx Ausdruck, sodass Standard Java RegEx zum tragen kommt.

Die Klassen heißen alle ParserXXXXX. Wobei XXXXX für fünf stellige Dezimalzahl steht.

scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile("de.pfau.parser.*Parser\\d{5}")));

Über die Methode scanner.findCandidateComponents kann man eine gefilterte Liste der Kandidaten erhalten. Der Rest ist identisch zur alten Version.

Damit das Autowiring von autowireCapableBeanFactory funktioniert, darf der Code nicht im Konstruktor ausgeführt werden. Hierzu verwende ich die @PostConstruct Annotation. Die Instanzen der Parser werden in einer HashMap gespeichert, um so später darauf zuzugreifen.

ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);

scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile("de.pfau.parser.*Parser\\d{5}")));

for (BeanDefinition bd : scanner.findCandidateComponents("de.pfau.parser")) {
    //
    // Instantiate parser and parse loaded document
    //
    try {
        var classname = bd.getBeanClassName();
        Class<?> c = Class.forName(classname);

        try {
            Parser p = (Parser) c.getDeclaredConstructor().newInstance();
            autowireCapableBeanFactory.autowireBean(p);
            map.put(classname, p);

            log.info("parser " + classname + " successfuly loaded");
        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
            throw new Exception...
        } 
    } catch (ClassNotFoundException e) {
        log.severe("could not load class");
    }
}

Hat man einen Web Server z.B. Tomcat im Classpath, dann startet Spring Boot diesen automatisch. Es kann aber sein, das man die selbe Codebasis für zwei Anwendungen benutzt. Es lässt sich das Starten des Webservers steuern. Zum einen über die application.properties und programmatisch.

Spring Boot 2.x

Hier über die Properties:
spring.main.web-application-type=none

oder

public static void main(String[] args) {
    new SpringApplicationBuilder(Application.class)
        .web(WebApplicationType.NONE) 
        .run(args);
}

Spring Boot 1.x

Ab 2.0 haben sie die Bezeichnungen geändert. Zuvor musste die web-environment auf false gesetzt werden.

spring.main.web-environment=false

Am 20.12.2018 wurde die aktuellste Version der Spring Tools Suite 4.1.0 veröffentlicht.

Changelog für Spring Tools Suite 4.1.0

Wie immer wurden Fehler behoben und Aktualisierungen eingefügt. Nennenswertes ist diesmal das Eclipse Update. Eclipse erscheint seit einiger Zeit in einem 3 monatigen Rhythmus und so war Eclipse bei der Aktualisierung auch mit dabei. Spring Tools Suite basiert jetzt auf Eclipse 2018-12.

Weitere Änderungen liste ich hier kurz auf:

Spring Boot

  • Mit diesem Release ist erstmals eine Unterstützung für die Autovervollständigung (content-assist) für Spring Data Repository definitionen mit dabei.
  • Die Live Hover Hyperlinks zu den Javatypen funktionieren auch für Projekte die Java 9 oder höher verwenden.
  • Es wurde ein Problem mit beendeten Anwendungen behoben, wenn diese mit JMX über einen SSH Tunnel verbunden sind.

Eclipse

  • Eclipse Javadoc Live Hover ist nun auch ohne eine Abhägigkeit zum spring-boot-actuator wieder verfügbar.
  • Java 11 Support ohne Update vom Eclipse Marketplace
  • Verbesserter Kompatibilitätscheck von JDK Versionen
  • Live Reueast von Reactiven WebFlux Anwendungen können nun auch im Boot Dashboard angezeigt werden.
  • Fehlerbeseitung beim kommentieren mit Hilfe des Shortcuts

Neue Features in Spring Boot 2.1.1

Der Actuator Endpoint besitzt jetzt health check für Elesticsearch REST Clients. Das Logging für Serlet und Filter Registration wurde verbessert und den restlichen Loggingeinträgen angepasst. Die Konfiguration des LoggingApplicationListeners ignoriert jetzt zusätzliche Leerzeichen (trim) in der Angabe des LoggingLevels. SAP HANA gehört nun zu den bekannten Datenbanken. Hibernate 5.2 kann auch wieder mit Spring Boot 2.1 verwendet werden, wenn dieses als expliziete Abhängigkeit angegeben wird. Zuvor gab es eine ClassNotFoundException. Spring Boot verwendet standardmäßig Hibernate 5.3.

Caused by: java.lang.ClassNotFoundException: org.hibernate.resource.beans.container.spi.BeanContainer
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    ... 45 common frames omitted

Es kann die SSL Verschlüsselung auf dem Management Port 8081. Der Spring Boot Classloader kann jetzt mit den in Java 9 eingeführten MultiRelease Jars umgehen und diese laden wenn sie in einem ausführbaren Jar sind.

Bug Fixes

Natürlich wurden auch in diesem Release jede Menge Fehler behoben. Unter anderem kann WebFlux, die reaktiven Implementation von Web MVC, nun Fehlerseiten für HTTP Status Codes rendern. Anwendungen die als Abhängigkeiten spring-boot-starter-web und spring-boot-startet-jersey hatten, konnten nicht gestartet werden, weil der Startvorgang mit einer einer Fehlermeldung quittiert wurde.

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'requestContextFilter', defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class], could not be registered. A bean with that name has already been defined in class path resource [org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

Mit Sprng Boot 2.1 wurde das Verhalten von Bean definition overriding geändert, sodass es jetzt zu der Fehlermeldung kommt.

Ein Stacktrace auf der Whitelabel Fehlerrseite wird nun korrekt dargestellt. Das DIV Elemnent wird nun mit style=’white-space:pre-wrap;’ versehen.

Auch das Logging für das Endpoint Mapping wurde verbessert.

Dokumentation von Spring Boot

Wie immer wurde kräftig an der Dokumentation gearbeitet und somit entfällt auch diesmal ein Großteil der gelösten Tickets auf die Java Dokumentation.

Dependencies Upgrades

Hier einige der wichtiges Updates…
Upgrade to Flyway 5.2.3
Upgrade to Jetty Reactive Httpclient 1.0.2
Upgrade to Byte Buddy 1.9.5
Upgrade to Undertow 2.0.16.Final
Upgrade to Rxjava2 2.2.4
Upgrade to Netty Tcnative 2.0.20.Final
Upgrade to Micrometer 1.1.1
Upgrade to Junit Jupiter 5.3.2
Upgrade to Spring Session Bean-SR1
Upgrade to Spring Security 5.1.2.RELEASE
Upgrade to Spring Kafka 2.2.2.RELEASE
Upgrade to Spring Integration 5.1.1.RELEASE
Upgrade to Lombok 1.18.4
Upgrade to Mockito 2.23.4
Upgrade to Jooq 3.11.7
Upgrade to Tomcat 9.0.13
Upgrade to Kafka 2.0.1
Upgrade to Reactor Californium-SR3
Upgrade to Spring Data Lovelace-SR3
Upgrade to Spring Framework 5.1.3

Am 30.11.2018 wurde das Update von der Spring Tools Suite 4.0.2 veröffentlicht. Es enthält neben dem Bugfix für fehlende Context-Path Unterstützung (siehe Liver Hover fehlerhaft) Verbesserungen für die Navigation in application.properties und application.yaml Dateien, Vereinheitlichung der Autovervollständigung in Property und YAML, Autovervollständigung im Config-Editor in der Boot Dash View, Verbesserte Anzeige der Live Hover bei functional configuration für WebFlux, Unterstützung für Logging Groups in Property und YAML Editoren.

Außerdem wurden eine Reihe von Fehlern beseitigt, die die Stabilität des Language Server beeinträchtigten. So wurden hänger und zähflüssige Antwortzeiten beseitigt. Es wurde QuickFix mit CTRL + F1 in Editoren die das LSP (Language Server Protocol) verwenden behoben.

Alles passt zusammen

Nun fügen sich alle Puzzelteile zusammen und es läßt sich unter dem aktuellen LTS von Java, dem JDK 11, ein Beispiel mit Hibernate mit SQLite Datenbank, Jooq und Spring Boot 2.1 unter der Verwendung von Gradle als Buildsystem umsetzen. Bis hierhin gab es einige Baustellen, die im Zuge der Modularisierung (Projekt Jigsaw), noch zu beseitigen waren. Jetzt ist eine Umsetzung mit den aktuellsten Versionen möglich und dieses möchte ich hier vorstellen.

Hier die benötigten Komponenten im Einzelnen:

  • Spring Boot 2.1
  • Spring Data JPA (spring-boot-starter-data-jpa)
  • Jooq 3.11.4 (im Spring Boot Starter definiert)
  • Gradle 5.0-RC4
  • sqlite-dialect (Hibernate Dialekt)
  • sqlite-jdbc (Treiber)

Die Demo

Die Anwendung erstellt zwei Invocation Objekte und persistiert sie mit JPA (Hibernate) in der Datenbank. Im Anschluss wird mit Hilfe von Jooq die Tabelle aus SQLite Datenbank wieder gelesen und in Tabellenform auf der Konsole ausgegeben. Das Ganze passiert in der Klasse InitDatabase, die das CommandLineRunner Interface implemenmtiert und somit beim Start der Anwendung ausgeführt wird. Die SQLite Datenbank legt die Daten in der Datei application.db ab. Eine initiale Version ist im Git Repository vorhanden (s.u.).

Ausführen der Anwendung

Der Build wird wie immer mit dem Dreisatz git clone, cd, ./gradlew clean build bootRun angestoßen.

git clone https://mrpeacockgit.duckdns.org:443/Public/spring-hibernate-jooq-sqlite-demo.git
cd spring-hibernate-jooq-sqlite-demo/
./gradlew clean build bootRun

Hier sehen wir den schön formatierten Output von Jooq auf der Konsole.

ACHTUNG: Es muss der Gradle Wrapper unter JDK 11 ausgeführt werden, weil Java 11 als Source- und TargetCompatibility angegeben ist. Wird Gradle z.B. unter Java 8 ausgeführt, dann bricht Gradle den Buildvorgang mit > Could not target platform: ‘Java SE 11’ using tool chain: ‘JDK 8 (1.8)’ ab. Hat man keine JDK 11 zur Hand, dann kann Source und Target auf Java 8 reduziert werden, da keine Sprachfeatures von Java 11 genutzt werden.

Import in Eclipse

Damit in Eclipse die Anwendung fehlerfrei ist, müssen 2 Schritte ausgeführt werden.

  • Generierung des Java Codes. Es muss die Generierung der Java Quelltexte, aus dem gegebenen Datenbankschema mit Hilfe des Jooq Code Generators, durchgeführt werden. Der Task generateSpringhibernatesqliteJooqSchemaSource wird von dem Jooq Plugin bereitgestellt.

  • Damit die statischen Importe gefunden werden, müssen sie auch im Eclipse Buildpath vorhanden sein. Dazu muss einmalig der Task eclipse ausgeführt werden.

Jetzt sollte das Projekt fehlerfrei im Eclipse sein.

Die Entität

Die Entity die hier verwendet wird, ist der Demo bedingt, sehr einfach gehalten. Wie immer verwende ich das Project Lombok, um den Code sauber von Boilerplate Code zu halten.

@Entity
@Data
public class Invocation {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private String name;
}

GenerationType

Der GenerationType muss hier auf AUTO stehen, ansonsten wird der Start der Anwendung mit einer Exception quittiert.

Caused by: org.sqlite.SQLiteException: [SQLITE_CONSTRAINT]  Abort due to constraint violation (NOT NULL constraint failed: invocation.id)

Hibernate Schema Erzeugung / Jooq Code Generation

Hier haben wir ein Henne-/Ei-Problem! Der Code Generator von Jooq benötigt eine Datenbank aus der er das Schema ausliest, um den Code zu erzeugen. Im Quelltext wird aber bereits per Import auf generierten Code zugegriffen, was zu einer Fehlermeldung beim Kompilieren während des Builds führen würde. Da die Code Generierung vor der Kompilierung erfolgt, habe ich eine leere Datenbank mit in das Git Repository aufgenommen. Somit kann die Anwendung über den Gradle Build gebaut und auch ausgeführt werden.

Problem der automatischen Schema Generierung

Anhand des oben beschriebenen Henne-/Ei-Problems, sollte eigentlich jedem schnell klar werden, dass das keine Lösung für große Anwendungen ist. Es sollte hier besser auf andere Lösungsansätze ausgewichen werden. Z.B. Flyway als Datenbankschemamigrationstool oder einfach die Verwendung von Spring Boot Bordmitteln (Spring liest schema.sql ein und verarbeitet die SQL Anweisungen).

Jooq Konfiguration

Für das Erstellen der Konfiguration Jooq Code Generator wird das Plugin von Etienne Struder verwendet. Hier in den Beispiel ist nur eine Grundkonfiguration vorgenommen worden. Alle Einstellungsmöglichkeiten die möglich sind, lassen sich über das XSD für Jooq Code Generator erfahren. Das lesen und verstehen des recht umfangreichen XSD ist nicht einfach. Man kann aber z.B. über Visual XSD sich das XSD visualisieren lassen und so den Aufbau schneller verstehen.

Autovervollständigung in Eclipse

Bei der Eingabe in Eclipse werden nicht automatisch die statischen Imports für die generierten Tabellen von Jooq angezeigt. Hier muss man in Eclipse erst den Umweg über eine nicht statischen Import nehmen, um ihn dann per STRG + 1 in einen statischen Import zu überführen.

Beispiel

In der Klasse InitDatabase die den CommadLineRunner implementiert, möchten wir alle Invocations aus der Datenbank listen.

private void listInvocations() {
        ctx.select()
            .from(INVOCATION)
            .fetch()
            .forEach(System.out::println);
    }

INVOCATION ist hier über den statischen Import

import static db.Tables.INVOCATION;

bekannt. Damit die Autovervollständigung funktioniert, muss der Import vorhanden sein. Das heißt erst nach dem man den Import eingefügt hat, lässt sich mit STRG + SPACE das Code-Fragment .from(INVOCATION) einfügen. Das ist nicht sehr effektiv, da man immer erst den Import einfügen muss.

Einstellung der Favoriten

Der Generator von Jooq ist so konfiguriert, dass die Tabelle aus der Datenbank im Java Package de.Tables.* gelistet werden. Die Quelltexte werden außerhalb von main in src/db gespeichert, so dass jederzeit durch löschen von dem Verzeichnis src/db die generierten Quelltext sauber neu erstellt werden können, ohne das alte Artefakte noch verhanden sind.

Man kann Eclipse anweisen, bestimmte static Members anzuzeigen, auch wenn das Import noch fehlt. Und genau das führt hier zum Ziel und erleichtert die Eingabe von Jooq Queries mit der hervorragenden DSL enorm.

In den Präferenzen von Eclipse wählen Sie java –> Editor –> Content Assist –> Favorites und erstellen einen neuen Typen. Hier ist es *db.Tables.**.

Jetzt kann die Eingabe vervollständigt werden, ohne das der Import zuvor vorhanden war

Hier in diesem Betrag gehe ich auf ein paar grundlegende Sicherheitsmaßnahmen ein. Ziel ist es einige wichtige Maßnahmen zum Schutz der Daten vorzustellen und wie diese mit Spring Security genutzt werden können.

CSRF

Cross Site Request Forgery kann man frei mit Seitenübergreifende Anfragenfälschung übersetzen. Um zu verhindern das ein Angreifer Anfragen an die Anwendung stellt, muss man sicherstellen dass der Link auf den der Benutzer geklickt hat nicht untergeschoben worden ist. Dieses kann man recht einfach erreichen, in dem man ein Geheimnis in der Webseite unterbringt. Dieses Geheimnis, es handelt sich hierbei um das sogenannte CSRF-Token, kennt nur die Anwendung (Server) und der Anwender (Client). Das Token hat eine kurz Lebensdauer und wird als hidden input field mit ausgegeben. Hier am Beispiel in Verwendung mit Thymeleaf Templating Engine.

<input
  type="hidden"
  th:name="${_csrf.parameterName}"
  th:value="${_csrf.token}" />

Es ist also nicht sehr kompliziert, aber dennoch eine Wirkungsvolle Waffe gegen mögliche Angreifer. Alle Anwendungen sollten also möglichst CSRF verhindern. Weitere Sicherungsmaßnahmen befinden sich gut beschrieben in der Referenzdokumentation von Spring Boot Security.

Hinweis: Ab Thymeleaf 2.1 und gesetzter @EnableWebSecurity fügt Thymeleaf dieses automatisch ein.

CSRF deaktivieren

Per default ist CSRF aktiviert. Das heißt das Seiten die von der Anwendung ausgeliefert werden, auch das Token beinhalten müssen, da sonst zu recht ein 403 Fehler angezeigt wird. In dem WebSecurityConfigurerAdapter kann man CSRF deaktivieren.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
            .disable() // TODO remove and secure template with CSRF token
    ...
}

Die Deaktivierung, auch wenn es nur in der Entwicklung ist, ist nicht zu empfehlen. Sicherheit in Anwendungen sollten von Anfang an integraler Bestandteil der Anwendung sein und nicht im Nachhinein übergestülpt werden. Daher rate ich von dieser Vorgehensweise hier ausdrücklich ab.

Dependency

Es muss nur der Starter spring-boot-starter-security eingebunden werden. Dieses ist die einzige Abhängigkeit, um die Anwendung mit Spring Security abzusichern.

dependencies {
    // Spring Boot
    ...    
    implementation('org.springframework.boot:spring-boot-starter-security')
    ...
}

Sicherung auf Methoden Level

Mit welchen Einstellungen lässt sich die Methodensicherung konfigurieren?

Grundsätzlich erfolgt die Steuerung über Annotationen. Welche Annotationen Wirkung haben, hängt davon ab welche Sicherungsmaßnahmen aktiviert sind. In der @EnableGlobalMethodeSecurity können als Paramter folgende Annotationen gesteuert werden:

  • securedEnabled

Methoden oder Klassen können nun mit @Secure() annotiert werden.

  • jsr250Enaabled Die Annotaion jsr250Enabled ist das Equivalent zur Spring Annotation Secured

  • order

  • prePostEnabled

Wenn prePostEnabled wahr ist, dann lassen sich Methoden vor Aufruf oder bevor die Rückgabewerte übergeben werden prüfen.

Beispiel

Wie sieht nun eine typische Konfiguration aus? Hier nachfolgend eine mit @Configuration annotierte Konfiguration, die secured ermöglicht und prePost aktiviert.

@Configuration
@EnableGlobalMethodSecurity(securedEnabled=true, prePostEnabled=true)
public class EnableMethodSecurity extends GlobalMethodSecurityConfiguration {

}

Kurztipp für die Eingabe

Legt man die Klasse mit CTRL + n –> class neu an, dann kann man direkt GMSC + STRG + SPACE eingeben und Eclipse expandiert dann zur gewünschten Klasse GlobalMethodSecurityConfiguration. Man muss also nur die Anfangsbuchstaben wählen und dann mit STRG + SPACE die Autovervollständigung akitivieren.

Den Ausdruck hasPermission() implementieren

Neben dem Standard hasRole(“”) kann man in Spring auch eigene Ausdrücke implementieren. Hier zeige ich an dem Beispiel, wie man die hasPermissions() implementieren kann.

Dazu muss die Klasse das Interface PermissionEvaluator implementieren. Zunächst das Grundgerüst:

@Override
public boolean hasPermission(Authentication authentication,
                             Object targetDomainObject,
                             Object permissionObject) {
    if(authentication==null ||
            targetDomainObject==null ||
            !(permissionObject instanceof String)) {

        log.info("Permission denied");

        return false;
    }

    String targetType = targetDomainObject.getClass()
                            .getSimpleName()
                            .toUpperCase();
    String permission = permissionObject
                            .toString()
                            .toUpperCase();

    return hasPrivilege(authentication, targetType, permission);
}

@Override
public boolean hasPermission(Authentication authentication,
                             Serializable targetId,
                             String targetType,
                             Object permissionObject) {
    if ((authentication == null) || 
        (targetType == null) || 
        !(permissionObject instanceof String)) {
        return false;
    }

    String permission = permissionObject
                            .toString()
                            .toUpperCase();

    return hasPrivilege(authentication, targetType, permission);
}

/**
    * Check privilege on given constraints
    * 
    * @param authentication
    * @param targetType
    * @param permission
    * 
    * @return true if access is granted, otherwise false
    */
private boolean hasPrivilege(Authentication authentication,
                             String targetType,
                             String permission) {
    for (final GrantedAuthority grantedAuth : authentication.getAuthorities()) {
        if (grantedAuth.getAuthority().startsWith(targetType)) {
            if (grantedAuth.getAuthority().contains(permission)) {
                return true;
            }
        }
    }
    return false;
}

Methoden absichern

@Controller
public class CustomerController {
    /**
     * Show table with all Customers. This endpoint is protected
     * by hasPermission expression evaluation in PreAuthorize() annotation.
     */
    @GetMapping("/allcustomersHP")
    @PreAuthorize("hasPermission(#model, 'String', 'xxx')")
    public String allCustomersHP(Model model) {
        model.addAttribute("customers", customerService.findAll());
        return "customerListing";
    }

ACHUTNG: Damit @PreAuthorize ausgeführt und somit auch der Ausdruck hasPermission() evaluiert wird, muss in der Konfiguration prePostEnabled=true gesetzt werden. Siehe oben.

Benutzerdefinierte Ausdrücke für die Validierung

Es lassen sich nicht immer alle Umgebungen auf die hasPermission() abbilden, was dazu führen kann das die Funktion, die ja nur die Permission klären soll, auch andere Zwecke erfüllt. Diese Zweckentfremdung führt früher oder später zu Problemen.

Gegeben sei ein größerer Betrieb mit vielen Organisationseinheiten. Dann macht es Sinn zum Beispiel den Zugriff auf die Umsatzzahlen, einer bestimmten Gruppe von Mitarbeiten zu gewähren. Hier wäre es die Controllingabteilung.

Anstelle mit hasPermission auf isController oder ähnliches zu Prüfen, kann man auch gleich die Organisationeinheiten mit in die Benutzerdaten integrieren und z.B. mit einer Funktion isMemberOf(String organization) prüfen. Dazu muss eine Klasse den SecurityExpressionRoot erweitern. Dieses Thema werde ich in einem weiterem Beitrag ausführlich behandeln.

Sicherheitsmaßnahmen komplett deaktivieren

Wenn sich Spring Security im Classpath befindet, dann werden alle Endpoints (bis auf 2 Actuator /health, /info) gesichert und sind ohne weiteres Zutun nicht mehr erreichbar. Und es ist auch gut so, dass das Spring Team dieses konservative Standardverhalten gewählt hat.

Möchte man zum lokalen Testen nicht jedesmal die Dependencies anpassen, so kann man zum Beispiel auch eine 2. Konfiguration anlegen, die nur bei einem bestimmten Profil aktiviert wird. Überschreibt man die beiden Methodensignaturen configure(final AuthenticationManagerBuilder) und configure(final HttpSecurity), dann hat man den Effekt, dass Spring Security nicht im Classpath vorhanden sei.

@Profile("localdebug")
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {}

  @Override
  protected void configure(final HttpSecurity http) throws Exception {}
}

Vorwort

In diesem Beitrag zeige ich wie man einen reactive RestController erstellt. Es wird die gesamte Anwendung reaktiv sein, d.h. wir verwenden hier den spring-boot-starter-data-mongodb-reactive Client mit seiner reaktiven Repository Schnittstelle.

Das Beispiel ist mit Absicht extrem einfach gehalten, um das Wesentliche besser darzustellen. In dem Beispiel soll ein RestController eine API mit den üblichen CRUD Methoden zur Verfügung stellen. Es soll in der Datenbank für Reservierungen geschaffen werden. Die Konfiguration wird nicht klassisch mit Annotationen durchgeführt, sondern ich zeige hier die Anwendung von functional configuration zum Setzen der Routen zu den entsprechenden Handlermethoden. Die Anwendung soll es ermöglichen eine Reservierung zu erstellen, lesen, löschen und zu ändern.

Die Dependencies

Um das Beispiel umzusetzen benötigen wir Abhängigkeiten für spring-boot-starter-data-mongodb-reactive und spring-boot-starter-webflux. Die weitere Abhängigkeiten (s.u.) sind optional, aber sollten dennoch mit eingebunden werden, da sie das Leben doch vereinfachen können.

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
    compile('org.springframework.boot:spring-boot-starter-webflux')
    compile('org.springframework.boot:spring-boot-actuator')
    runtime('org.springframework.boot:spring-boot-devtools')

    // Project Lombok
    // Since Gradle warns if an AnnotationProcessor is found on classpath, put 
    // it into annotationProcessor directive
    compileOnly('org.projectlombok:lombok')
    annotationProcessor('org.projectlombok:lombok')

    // Testing
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('io.projectreactor:reactor-test')

    // flapdoodle runs a temporary embedded instance of mongodb
    testRuntime('de.flapdoodle.embed:de.flapdoodle.embed.mongo:2.0.1')
}

Project Lombok

Ich mag keinen Boilerplate Code, da er gerade bei Entitäten den eigentlichen Verwendungszweck verdeckt. Daher bin ich ein großer Fan von dem Project Lombok und setze es hier in diesem Beispiel auch ein.

Flapdoodle

Zusätzlich für die Entwicklung und das Unittesting binde ich hier Flapdoodle ein. Flapdoodle kann eine temporäre embedded Version von MongoDB starten. Dazu lädt es ggf. MongoDB herunter und startet für jeden Test eine temporäre Instanz der MongoDB Datenbank. Es wird immer mit einer leeren Datenbank gestartet, so dass hier im Reinraum getestet werden kann, ohne das man zuvor aufräumen muss.

Actuator

Ich habe hier auch spring-boot-actuator mit aufgenommen, damit ich in der Spring Tool Suite 4 die Endpoints in den Properties der Anwendung angezeigt bekomme, bzw. die Live Hover Informationen nutzen kann (siehe Bug in Live Hover).

Die Klasse Reservation

Die Klasse Reservation bildet eine Reservierung ab. Da es hier nur um eine Demonstration handelt, ist hier natürlich genügend Spielraum vorhanden für Erweiterungen. Wie bereits beschrieben nutze ich hier Lombok Annotationen, um möglichst sauber von Boilerplate Code zu arbeiten

Die Klasse Reservation ist also kurz und knackig.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document
class Reservation {
    private String id;

    private String reservationName;
}

Die Annotation @Document beschreibt des sich um ein MongoDB Dokument handelt. Ein Dokument hält einen Datensatz. In MongoDB werden als JSON Objekte übergeben und dann intern als BSON abgespeichert.

Mono und Flux

Bei der reaktiven Programmierung gibt es die beiden Klassen Mono und Flux die das Publisher Interface implementieren. Beide Implementierungen liefern einen Stream von Elementen mit…

Mono genau 0 oder 1 Elemente
Flux 0 oder 1...n Elemente

Die Konfiguration der Routen

Die Klasse ReservationRouter übernimmt das Routing der Request zu den entsprechenden Handlermethoden in der Klasse RequestHandler.

Ein GET Aufruf auf /reservations gibt JSON mit allen Reservierungen aus der Datenbank zurück

Ein GET Aufruf auf /reservation/{id} lädt genau eine Reservierung mit der angegebenen Id

Ein DELETE Aufruf auf /reservation/{id} löscht den referenzierten Datensatz

Ein POST Aufruf auf /reservation erzeugt eine neue Reservierung

Ein PUT Aufruf auf /reservation/{id} modifiziert den referenzierten Datensatz

Die statische Methode i macht die Aufrufe CaseInsensitiv.

@Configuration
public class ReservationRouter {
    @Bean
    RouterFunction<ServerResponse> routes(RequestHandler handler) {
        return route(i(GET("/reservations")), handler::all)
            .andRoute(i(GET("/reservation/{id}")), handler::getById)
            .andRoute(i(DELETE("/reservation/{id}")), handler::deleteById)
            .andRoute(i(POST("/reservation")), handler::create)
            .andRoute(i(PUT("/reservation/{id}")), handler::updateById)
            ;
    }

    private static RequestPredicate i(RequestPredicate target) {
        return new CaseInsensitiveRequestPredicate(target);
    }
}

Die statische Methode route definiert nun mit den angegebenen RequestPredicates (GET, DELETE, POST und PUT) und einem Patternstring, welche Handlermethode den Request bearbeitet.

Warum muss ich schon wieder eine neue Methode zur Konfiguration lernen?

Sicherlich haben Sie sich auch schon gefragt, Moment Mal, das geht doch auch alles mit den bewährten Annotationen. Ja, das stimmt und daran gibt es auch nicht auszusetzen. Es gibt allerdings einen triftigen Grund warum man sich de functional configuration annehmen sollte. Es ist die Performance. Es lassen sich die Startupzeiten mit dem neuen funktionalen Ansatz drastisch, gegenüber der klassischen Methode mit Annotationen, verkürzen. Dieses ist insbesondere im Microservice Bereich, wo schnell mal ein paar zusätzliche Instanzen gespawnt werden müssen, wichtig.

Die Handler Klasse

Die Handlerklasse RequestHandler verarbeitet nun die ankommenden Requests und führt Aktionen auf dem Service durch. Die Methode getById und deleteById werden direkt durchgereicht an den Service. Es wird nur die Pathvariable id extrahiert und dem Service als Parameter übergeben.

Die Handlermethoden liefern ein Mono<ServerResponse> zurück. Spannend wird es der Aufruf von create hier wird der Übergebene ServerRequest in eine Reservation.class gemappt und der Service soll die Reservierung anlegen.

Zu bemerken ist, dass das reaktive Paradigma hier in beide Richtungen eingehalten wird. Das Anfragen die und auch Antworten als Mono oder Flux zwischen dem Server und dem Service ausgetauscht werden. Der hier gezeigte Handler ist vollkommen Asynchron und auch reaktiv, da hier keine blockierenden Operationen ausgeführt werden.

@Component
public class RequestHandler {
    private ReservationService service;

    public RequestHandler(ReservationService service) {
        this.service = service;
    }

    Mono<ServerResponse> getById(ServerRequest r) {
        return defaultReadResponse(service.getById(id(r)));
    }

    Mono<ServerResponse> deleteById(ServerRequest r) {
        return defaultReadResponse(service.deleteById(id(r)));
    }

    Mono<ServerResponse> create(ServerRequest r) {
        Flux<Reservation> flux = r
                .bodyToFlux(Reservation.class)
                .flatMap(toWrite -> service.create(toWrite.getReservationName()));
        return defaultWriteResponse(flux);
    }

...

    private static Mono<ServerResponse> defaultWriteResponse(Publisher<Reservation> reservations) {
        return Mono
                .from(reservations)
                .flatMap(r -> ServerResponse.created(URI.create("/reservation/" + r.getId()))
                .contentType(MediaType.APPLICATION_JSON_UTF8).build());
    }

    private static Mono<ServerResponse> defaultReadResponse(Publisher<Reservation> publisher) {
        return ServerResponse
                .ok()
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(publisher, Reservation.class);
    }

    private static String id(ServerRequest r) {
        return r.pathVariable("id");
    }
}

Der Service

Im vorherigen Abschnitt haben wir beleuchtet wie die Anfragen im Handler verarbeitet werden. Der Service bildet hier fast 1:1 das Repository ab.

@Service
public class ReservationService {
    private ReservationRepository repo;

    public ReservationService(ReservationRepository repo) {
        this.repo = repo;
    }

    public Mono<Reservation> getById(String id) {
        return repo.findById(id);
    }

    public Flux<Reservation> findAll() {
        return repo.findAll();
    }

    public Mono<Reservation> deleteById(String id) {
        return repo.findById(id)
                .flatMap(
                        r -> repo.deleteById(r.getId())
                        .thenReturn(r)
                );
    }

    public Mono<Reservation> create(String reservationName) {
        return repo.save(new Reservation(null, reservationName));
    }
}

Der Service greift auf das reaktive Repository ReservationRepository zu und es werden entsprechend Mono oder Flux<reservation> zurückgegeben. Bei der findAll() haben wir einen Stream mit allen Datensätzen aus der Datenbank. Deshalb kommt hier Flux als Implementierung des Publisher Interfaces zum Einsatz, weil es 0 bis n Datensätze sein können.

Das ReservationRepository

Das Repository ist von Typ ReactiveMongoRepository und stellt die CRUD Methoden zur Verfügung. Wir verwenden hier derived queries, um eine zusätzliche Anfrage für den Service bereitzustellen. Mit findBy+Property kann Spring Data aus dem Methodennamen den gewünschten Query erzeugen. Das heißt wir suchen mit findByReservationName(String name) nach einem Datensatz wo die Property ReservationName dem übergebenen Parameter entspricht.

Dieses ist aber ein Standard Feature von Spring Data und hat nichts mit der reaktiven Implementierung zu tun. Ansonsten gibt es nichts besonderes in dem Interface.

public interface ReservationRepository extends ReactiveMongoRepository<Reservation, String> {
    Flux<Reservation> findByReservationName(String name);
}

Mit curl die Rest API aufrufen

Das Testen der Anwendung möchte ich hier mit dem Kommandozeilenprogramm curl demonstrieren. Es sind standard HTTP Anfragen an den Server, so dass ich hier nur kurz insert und delete zeige.

Einfügen einer Reservierung

curl -i -X POST -H 'Content-Type: application/json' -d '{ "reservationName": "1. Test" }' http://localhost:8080/reservation

Es wird eine Antwort wie zum Beispiel…

HTTP/1.1 201 Created
Content-Type: application/json;charset=UTF-8
Location: /reservation/5bf68a11dbcbb837d33f4be6
content-length: 0

erzeugt, die das erfolgreiche Anlegen des Datensatze bestätigen.

Löschen einer Reservierung

curl -i -X DELETE http://localhost:8080/reservation/5bf3eda398947a5158e60200