Lombok 1.18.12 erschienen

Die neue Version 1.18.12 von Project Lombok ist veröffentlicht worden.

Typ Beschreibung
PLATFORM Support für JDK13 (yield in Switch Ausdrücken)
PLATFORM Support for JDK14 (Mit pattern match instanceof Ausdrücken).
FEATURE In der lombok.config können jetzt weitere config Dateien eingebunden werden und das sogar von .zip oder .jar Dateien.
FEATURE Mit @Builder(setterPrefix = „set“) lässt sich das Prefix für die Setter Methoden ändern. Dieses wird aber nicht empfohlen, aber man hat die Möglcihkeit, falls es eine Library so benötigt.
FEATURE Wenn man @Builder’s @Singular verwendet, dann wird auch ein Plural generiert.
FEATURE Lombok kann nullity annotations eingfügen wo es relevant ist. Dei Einstellung wird in der lombok.config vorgenommen und wird auf die return value von toString, withX, chainable setX, static constructors, build, builder, usw und dem Parameter von equals, canEqual.
BUGFIX Fix für das Sonarplugin
BUGFIX lombok.experimental.Wither wurde in lombok.With umbenannt.

Alle Änderungen sind im Changelog zu finden.

Spring Boot

Ab dem Release 2.3.0 M2 ist das Update von Lombok 1.18.12 enthalten.

Die Lombok Anwendung kann nicht gestartet werden

Wer neuere Java Features einsetzt und daher neuere Java Versionen verwendet, kommt oftmals mit neuen Fehler die mit dem Modulsystem von dem Projekt Jigsaw zusammenhängen.

Ich verwende gerne das Lombok und auch hier zeigen sich immer noch einmal hier und dort einige Fehler die auftauchen können. Sollte Euer Programm mit folgender Fehlermeldung abbrechen:

Error occurred during initialization of boot layer
java.lang.module.ResolutionException: Module lombok does not read a module that exports org.mapstruct.ap.spi

So fehlt Lombok eine Dependency. Es muss org.mapstruct:mapstruct-processor mit aufgenommen werden. Siehe issue 2125

dependencies {
    // fix for Lombok see https://github.com/rzwitserloot/lombok/issues/2125
    implementation 'org.mapstruct:mapstruct-processor:1.3.1.Final'
}

Jetzt sollte das Programm inklusive Lombok normal gestartet werden können.

Neue Version 1.18.6 von Lombok

Am 12.02.2019 ist die Neue Version von Lombok erschienen. Neben einigen Bugfixes enthält das Update initialen Support für die EA von JDK 12 und weiteren neuen Features wie zum Beispiel die Generierung von Javadocs.

Die wichtigsten Änderungen im Einzelnen

  • FEATURE: Javadoc an Feldern werden nun auch an den Builders Setter Methoden kopiert.
  • FEATURE: Es gibt jetzt einen Konfigurationseinstellung um das Verhalten von toString() die den super() Konstruktor aufruft zu beeinflussen
  • ENHANCEMENT: Verbesserte Konsolenausgabe wenn toString auf Enums angewandt wird. Es wird nun der Name der Konstanten ausgegeben
  • Unterstütung für JDK12. Aufgrund von Änderungen an Switchstatements gab es Probleme mit Lombok
  • BUGFIX: @Delegate in Zusammenhang mit @NonNull Annotation verursacht nun unter JDK8 keinen Fehler mehr
    BUGFIX: Delombok funktionierte seit der Version 1.18.4 nicht korrekt, weil ein NoClassDefFoundError Fehler ausgegeben worden ist

Die aktuelle Version von Lombok.jar befindet sich wie immer im Downloadbereich von dem Projekt.

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

Lombok 1.18.4

Am 30.10.2018 ist die neue Version 1.18.4 von Lombok erschienen. Wie immer sind einige Bugs in Zusammenhang mit Java Versionen größer 9 behoben worden. Es sind aber auch inkompatible Änderungen vorgenommen worden. Das Jar kann von hier runtergeladen werden.

Lombok unterstützt nun Eclipse Photon. Der lombok.patcher verwendete OpCodes.ASM4, welches nun nicht mehr von Eclipse verwendet wird. Eclipse verwendet nun invoke dynamic.

Inkompatible Änderungen

  • FieldNameConstants wurden aufgrund eines Tickets neu redesigned. Das Feature bleibt weiterhin als experimentel gekennzeichnet, da noch nicht klar ist, ob es wirklich eine Verbesserung der Lesbarkeit mit sich bringt. Dokumentation FieldNameConstants.

  • Lombok kopiert nun immer bestimmte Annotationen, damit das Verhalten nicht verloren geht. Z.B. @NonNull.

Weitere Änderungen und neue Features

Alle Änderungen sind im https://projectlombok.org/changelog zu entnehmen.

Ein aktueller Status von dem Projekt Lombok

Hier ein kurzer Zwischenbericht über den aktuellen Stand JDK, Eclipse (STS4 Version), Gradle mit Lombok.

JDK10

Als Vorbereitung Java 10 installieren…

yaourt --noconfirm -S jre10-openjdk-headless jre10-openjdk jdk10-openjdk openjdk10-doc openjdk10-src

08.08.2018

Eclipse STS4 in M12 ist entpackt. Ich habe die aktuellste Version von Lombok mit dem integrieren Installer installiert. Auf dem Arch Linux System wird hier der absolute Pfad eingetragen. Leider gibt es folgenden Fehler:

16.08.2018

Hurra! Es ist geschafft. Endlich läuft Eclipse mit Gradle und Lombok unter einer Java Version > 8 !!! Aktuell verwende ich das letzte Milestone-Release M14 der STS14 mit der letzten Version v1.18.2 und der letzten Gradle Version 4.9.

JDK11

16.08.2018

Achtung: es gibt kein offizielles Paket für die EA-Version. Man findet sie auch nicht so leicht im AUR, da dass Paket jdk-devel heißt. Besser wäre jdk11-ea. Dann würde mann es mit yaourt -s jdk11 auch als EA finden.

yaourt --noconfirm -S jdk-devel
sudo archlinux-java set java-11-jdk

Auch hier gibt es offenbar keine Probleme mehr…

Lombok

Ist Lombok unter Eclipse installiert und läuft Eclipse auf einer VM größer Java 8, dann kommt es beim aktualisieren des Gradle Modells unter Eclipse zu einer Fehlermeldung die erst einmal keinen direkten Rückschluss auf Lombok zulässt.

Gradle Buildfehler

Ist Lombok im Klassenpfad der VM von Eclipse geladen, dann kommt es zu folgender Fehlermeldung wenn man ein Gradle Projekt in der Workbench aktualisiert.

Errors occurred during the build.
    Errors running builder 'Java Builder' on project 'tornet'.
    Unknown constant pool type 19

Startet man Eclipse auf der Konsole, so kann man zumindest den Stacktrace zu dem BuildError sehen.

java.lang.AssertionError: Unknown constant pool type 19
        at lombok.bytecode.ClassFileMetaData.<init>(ClassFileMetaData.java:104)
        at lombok.bytecode.PreventNullAnalysisRemover.applyTransformations(PreventNullAnalysisRemover.java:42)
        at lombok.core.PostCompiler.applyTransformations(PostCompiler.java:43)
        at lombok.eclipse.agent.PatchFixesShadowLoaded.runPostCompiler(PatchFixesShadowLoaded.java:41)
        at jdk.internal.reflect.GeneratedMethodAccessor44.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at lombok.launch.PatchFixesHider$Util.invokeMethod(PatchFixesHider.java:115)
        at lombok.launch.PatchFixesHider$LombokDeps.runPostCompiler(PatchFixesHider.java:155)
        at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.writeClassFileContents(AbstractImageBuilder.java:880)
        at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.writeClassFile(AbstractImageBuilder.java:871)
        at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.acceptResult(AbstractImageBuilder.java:200)
        at org.eclipse.jdt.internal.compiler.Compiler.processCompiledUnits(Compiler.java:615)
        at org.eclipse.jdt.internal.compiler.Compiler.compile(Compiler.java:472)
        at org.eclipse.jdt.internal.compiler.Compiler.compile(Compiler.java:423)
        at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.compile(AbstractImageBuilder.java:383)
        at org.eclipse.jdt.internal.core.builder.BatchImageBuilder.compile(BatchImageBuilder.java:211)
        at org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.compile(AbstractImageBuilder.java:315)
        at org.eclipse.jdt.internal.core.builder.BatchImageBuilder.build(BatchImageBuilder.java:76)
        at org.eclipse.jdt.internal.core.builder.JavaBuilder.buildAll(JavaBuilder.java:259)
        at org.eclipse.jdt.internal.core.builder.JavaBuilder.build(JavaBuilder.java:182)
        at org.eclipse.core.internal.events.BuildManager$2.run(BuildManager.java:795)
        at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:42)
        at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:216)
        at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:259)
        at org.eclipse.core.internal.events.BuildManager$1.run(BuildManager.java:312)
        at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:42)
        at org.eclipse.core.internal.events.BuildManager.basicBuild(BuildManager.java:315)
        at org.eclipse.core.internal.events.BuildManager.basicBuildLoop(BuildManager.java:367)
        at org.eclipse.core.internal.events.BuildManager.build(BuildManager.java:388)
        at org.eclipse.core.internal.events.AutoBuildJob.doBuild(AutoBuildJob.java:142)
        at org.eclipse.core.internal.events.AutoBuildJob.run(AutoBuildJob.java:232)
        at org.eclipse.core.internal.jobs.Worker.run(Worker.java:60)

Diesen kann man sehen das es zu einem Assertion Error kommt.

Lösung

Zur Zeit ist mir noch keine Lösung hierzu bekannt und es hilft nur den Java Agenten von Lombok aus der Eclipse.ini auszukommentieren.

-vmargs
--add-modules=ALL-SYSTEM
-Xms40m
-Xmx1200m
# build error in projects...
#-javaagent:lombok.j