Das Spring-Framework ist bereits seit vielen Jahren ein beliebtes Framework für komplexe Java-Anwendungen im betrieblichen Umfeld. Die erste Version von Spring hat Rod Johnson bereits in 2002 vorgestellt. Das Spring-Framework hat sich im Laufe der Jahre mehrfach gewandelt und sich an moderne Programmierparadigmen, Entwurfs- und Architekturmuster angepasst. Heute wird Spring von Pivotal Software weiterentwickelt.

Inversion of Control

Im Kapitel zu Dependency Injection ist bereits das grundlegende Paradigma eines jeden Frameworks genannt worden, nämlich Inversion of Control (IoC). Ein Framework gibt einer Anwendung eine grundlegende Struktur vor, also eine Architektur, und steuert den Kontrollfluss der Anwendung. Der Anwendungsentwickler implementiert fachliche Methoden, die er beim Framework an vordefinierten Stellen registriert. Das Framework übernimmt die Steuerung des Kontrollflusses zur Laufzeit und wird die fachlichen Methoden/Anweisungen – ausgelöst durch bestimmte Ereignisse – aufrufen. IoC heißt also, dass die eigenen fachlichen Methoden/Anweisungen in definierte Callback-Methoden eingebettet und dort passiv aufgerufen werden, anstatt dass der Anwendungsentwickler aktiv den Kontrollfluss der Anwendung steuert. Das Spring-Framework hat maßgeblich zur Verbreitung des IoC-Paradigmas beigetragen. Das Spring-Framework selbst übernimmt die Aufgabe des IoC-Containers und verwaltet sogenannte Beans. Alle Klassen, die mit der Spring-Annotation @Component oder einer ihrer Spezialisierungen wie @Service, @Repository oder @Controller versehen sind, werden von Spring automatisch als Beans erkannt und registriert. Die registrierten Beans können zur Laufzeit über das Singleton-Objekt ApplicationContext erreicht werden (s. Kapitel Dependency Injection).

REST und ORM mit Spring

Es soll im Folgenden eine REST-API mit Spring Data entwickelt werden, die die verwalteten Objekte mittels JPA in einem relationalen Datenbanksystem speichert. Als Datenbanksystem wird MariaDB ausgewählt. Damit ergeben sich für das Projekt folgende Abhängigkeiten:

Bevor wir im Folgenden eigene Beispiele vorstellen, soll an dieser Stelle auf die guten Tutorials auf der Spring-Webseite hingewiesen werden:

Die zu entwickelnde REST-API soll Objekte zu den folgenden Entitätsklassen bereitstellen: User, Product und Booking. Der Zusammenhang der Klassen ist in der nächsten Abbildung veranschaulicht. Die API bildet die Datenhaltung für eine minimale Shop-Anwendung ab, in der authentifizierte Anwender (User) Produkte (Product) aus einem Produktkatalog auswählen und für diese beliebig viele Buchungen (Booking) anlegen und wieder entfernen können. Die hier entwickelte API soll in den folgenden Kapiteln zu den UI-Frameworks als gemeinsame Basis genutzt werden, d.h. die später vorgestellten Clients in JavaFX und JQuery/Angular greifen jeweils auf diese API zu.

@Entity @Getter @Setter
public class Product extends IdentifiedEntity {

	@Column(length = 50, nullable = false)
	String title;

	@Lob
	String description;

	@Lob
	@JsonIgnore
	byte[] image;

	@Column(nullable = false)
	Integer price;

	Boolean instock;
}
@Entity @Getter @Setter
public class Booking extends IdentifiedEntity {

    @Temporal(TemporalType.DATE)
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Europe/Berlin")
    Date date = new Date();

    @Column(name = "bookingprice", nullable = false)
    Integer bookingPrice;	
	
    @ManyToOne
    @JoinColumn(name = "user_id")
    User user;

    @ManyToOne
    @JoinColumn(name = "product_id")
    Product product;
}
@Entity @Getter @Setter
public class User extends IdentifiedEntity {

    @Column(length = 20, nullable = false)
    String name;

    @Column(length = 100, nullable = false)
    String email;

    @Column(name = "password_hash", length = 100, nullable = false)
    String passwordHash;

    @OneToMany(mappedBy = "user")
    List<Booking> bookings;
}
@MappedSuperclass @Getter @Setter
public abstract class IdentifiedEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    Integer id;
}

Der einfachste Weg mit Spring Data eine REST-API zu erzeugen, ist es das CrudRepository-Interface zu erweitern. In dem folgenden Code-Beispiel wird dieses Vorgehen demonstriert, indem das leere Interface BookingRepository<Booking, Integer> das generische CrudRepository<T, ID> erweitert und dadurch die in der CrudRepository-Dokumentation genannten Methoden erhält. Das Framework erstellt zur Laufzeit eine passende Implementierung für das BookingRepository<Booking, Integer>, wobei Booking die zu verwaltende Entitätsklasse angibt und Integer den Typ des Id-Attributs dieser Klasse.

import org.springframework.data.repository.CrudRepository;
// ...

public interface BookingRepository extends CrudRepository<Booking, Integer> {
}

Für jedes konkrete CrudRepository werden automatisch sogenannte CRUD-Endpunkte (Create, Read, Update und Delete) in der REST-API erzeugt, d.h. in diesem Fall werden unter dem Pfad /bookings die HTTP-Methoden POST und GET unterstützt und unter dem Pfad /bookings/<id> die HTTP-Methoden GET, PUT und DELETE. Folgend wird ein HTTP-Request und die zugehörige HTTP-Response der mit Spring Data REST generierten REST-API dargestellt. Es wird mittels POST eine neue Buchung angelegt, wobei auf bereits vorab existierende Ressourcen – wie hier das gebuchte Produkt und der buchende User – über deren URI verwiesen werden kann.

Der Body der HTTP-Response enthält zwar das Datum und den Preis zu einer Buchung, aber nicht die neu vergebene Id und keine weiteren Attribute des referenzierten Produkts und des referenzierten Users. Die Id wird über den HTTP-Header Location bekanntgegeben. Die Referenzen auf Produkt und User können über die angegebenen Links aufgelöst werden. Spring Data REST erlaubt alternativ die Erstellung von Projektionen für Entitätsklassen, über die deren Ausgabe angepasst werden kann. Das folgende Code-Beispiel ergänzt die Ausgabe einer Buchung um die Details des gebuchten Produkts, so dass diese nicht über einen weiteren HTTP-Request angefragt werden müssen.

@Projection(name = "inlineProduct", types = {Booking.class})
public interface BookingInlineProduct {
    String getDate();
    int getBookingPrice();
    Product getProduct();
}
@RepositoryRestResource(excerptProjection = BookingInlineProduct.class)
public interface BookingRepository extends CrudRepository<Booking, Integer> {
}

Dass die HTTP-Response nicht nur die Daten selbst, sondern auch weiterführende Links zur Navigation innerhalb der API enthält, entspricht dem sogenannten HATEOAS-Prinzip: Hypermedia As The Engine Of Application State. Da der Client bei einer REST-API (im Gegensatz zur WSDL bei SOAP-Webservices) keine formale Schnittstellenbeschreibung erhält, sind die Entwickler, die eine REST-API aufrufen möchten, auf eine gute Dokumentation angewiesen, in der die mögliche Parametrisierung der API-Endpunkte, die über die Konventionen hinausgeht, beschrieben wird. Die grundlegende Idee von HATEOAS ist es, diese Dokumentation in die API selbst einzubetten. Daher werden neben den angefragten Daten auch Links zurückgegeben, die potentielle Transitionen ausgehend von der aktuellen Anfrage zur nächsten Anfrage angeben. Die REST-API wird auf diese Weise als Zustandsautomat betrachtet, durch den mittels Hypermedia (= Repräsentation einer Ressource inkl. Links) navigiert werden kann. Die nächste Abbildung zeigt einen kleinen Ausschnitt einer REST-API als Zustandsautomat.

In der Repräsentation einer Ressource im HTML-Format sind wir Links über die Attribute href oder src gewohnt. Gemäß HATEOAS sind vergleichbare Hyperlinks auch bei anderen Formaten wie JSON oder XML zu ergänzen. Entsprechend hat Leonard Richardson ein nach ihm benanntes Reifegrad-Modell für REST-APIs entwickelt, das in der folgenden Abbildung dargestellt ist.

In einem Interface, das das CrudRepository erweitert, können zusätzlich eigene Methoden spezifiziert werden. Für diese Query Methods existieren leicht interpretierbare Bezeichnungskonventionen, entsprechend derer eine passende Implementierung durch das Framework generiert wird. Falls das Repository mittels JPA an eine relationale Datenbank gebunden ist, werden z.B. je nach Methodenbezeichnung bestimmte JPQL-Anfragen erzeugt. In der Dokumentation zu Spring Data JPA ist genau beschrieben, wie die Query Methods zu bezeichnen sind und welche JPQL-Anfrage sich daraus ergibt. Das im nächsten Code-Beispiel dargestellte Interface ProductRepository ergänzt z.B. Methoden zur Filterung der ausgegebenen Produkte über ihren Titel, ihre Beschreibung (Volltextsuche) und ihren Preis (Angabe einer Obergrenze).

import org.springframework.data.repository.PagingAndSortingRepository;
// ...

public interface ProductRepository extends PagingAndSortingRepository<Product, Integer> {

    @RestResource(path = "byTitle")
    List<Product> findByTitle(@Param(value = "title") String title);

    @RestResource(path = "inDescription")
    List<Product> findByDescriptionContainingIgnoreCase(@Param(value = "pattern") String pattern);

    @RestResource(path = "maxPrice")
    List<Product> findByPriceLessThanEqual(@Param(value = "price") Integer price);
}

Das folgende Video demonstriert die resultierende REST-API dieses Kapitels, indem exemplarische Anfragen mittels Postman ausgeführt werden.

Eine Erweiterung des CrudRepository ist das oben bereits verwendete PagingAndSortingRepository, das entsprechend seiner Bezeichnung erlaubt, die Menge der Elemente, die mittels der GET-Methode für eine Collection-Ressource zurückgeben werden, über URL-Parameter in paginierte Abschnitte zu teilen und zu sortieren. Der folgend dargestellte HTTP-Request enthält URL-Parameter zur Paginierung und zur Sortierung, so dass in der zugehörigen HTTP-Response nur 3 von 15 Produkten ausgegeben werden. Die weiterführenden Links zur Navigation durch die API gemäß des HATEOAS-Prinzips sind deutlich zu erkennen.

Die automatisch von Spring generierten Ressourcen-Methoden für ein CrudRepository können über verschiedene Wege angepasst werden. Es kann eine Klasse angelegt werden, die das Interface implementiert und die Methoden überschreibt. Alternativ kann eine Klasse, versehen mit der Annotation @RestController, angelegt und auf dem Pfad der Ressource registriert werden. Beide Wege erlauben das Verhalten der HTTP-Methoden individuell anzupassen. Im folgenden Code-Beispiel überschreibt die Klasse UserController die auf dem Pfad /users registrierten Methoden für POST und GET des Interface UserRepository (Zeilen 4-5). Bei einer GET-Anfrage werden z.B. nur noch die Namen der vorhandenen User ohne weitere Attribute der Klasse ausgegeben (Zeilen 22-26). Die Methode findByName im Interface UserRepository ist ohnehin nicht Teil der REST-API, da sie nicht mit einer Annotation @RestResource versehen ist. Im UserRepository werden explizit noch die Methoden GET und DELETE für den Pfad /users/<id> deaktiviert.

import org.springframework.web.bind.annotation.RestController;
// ...

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    // registers a new user
    @PostMapping
    public void register(@RequestBody User user) {
        user.setPasswordHash(bCryptPasswordEncoder.encode(user.getPasswordHash()));
        userRepository.save(user);
    }

    // provides only the user names
    @GetMapping
    public List<String> getUserNames() {
        Stream<User> users = StreamSupport.stream(userRepository.findAll().spliterator(), false);
        return users.map(User::getName).sorted().collect(Collectors.toList());
    }
}
public interface UserRepository extends CrudRepository<User, Integer> {
   User findByName(@Param(value = "name") String name);
   
   @Override
   @RestResource(exported = false)
   Optional<User> findById(Integer id);   
   
   @Override
   @RestResource(exported = false)
   void delete(User user);  
}

Für ausgewählte Endpunkte der entwickelten REST-API kann der Zugriff mittels Spring Security abgesichert werden. Falls JSON Web Tokens (JWT) zur Zugangskontrolle eingesetzt werden sollen, findet sich im Blog von Auth0 eine umfassende Einführung, an der sich auch die Implementierung im Package shop.security des begleitenden Projekts orientiert. Es entstehen dadurch Abhängigkeiten des Projekts zu Spring Boot Security Starter und Java JWT.

Reaktive Web-Anwendungen mit Spring

Das ursprüngliche Spring-Framework zur Entwicklung von Web-Anwendungen auf Basis der Servlet API nennt sich Spring MVC. Neu in der aktuellen Version 5 des Spring-Frameworks ist die Unterstützung der Reactive Streams API auf Basis der Bibliothek Reactor. Dieser alternative Architekturansatz nennt sich Spring WebFlux und ist auf asynchrone und nicht-blockierende Kommunikation ausgerichtet. Die folgende Definition aus der Spring-Dokumentation erklärt, was wir unter dem Begriff Reaktive Programmierung verstehen.

"The term, “reactive,” refers to programming models that are built around reacting to change — network components reacting to I/O events, UI controllers reacting to mouse events, and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available. There is also another important mechanism that we on the Spring team associate with “reactive” and that is non-blocking back pressure. In synchronous, imperative code, blocking calls serve as a natural form of back pressure that forces the caller to wait. In non-blocking code, it becomes important to control the rate of events so that a fast producer does not overwhelm its destination." (Spring Docs)

Voraussetzung ist eine reaktive Datenquelle, die als Publisher bei ihr registrierte Subscriber über neue oder veränderte Daten benachrichtigen kann, wie z.B. MongoDB, Redis oder Cassandra. JDBC ist nicht darauf ausgelegt. Reaktive Programmierung ist insbesondere für Anwendungsfälle sinnvoll, in denen kontinuierliche, unbegrenzte Datenströme verarbeitet und in einem User Interface dargestellt werden müssen. Die folgende Abbildung stellt Spring MVC und Spring WebFlux einander gegenüber.

Pivotal Software: Spring Framework 5, https://spring.io/.

Spring WebFlux übernimmt die Begriffe Flux und Mono aus der Bibliothek Reactor. Ein Flux ist ein generischer Datenstrom (Stream) der 0..N Elemente emittieren kann, ein Mono hingegen nur 0..1 Element. Die Klassen reactor.core.publisher.Flux und reactor.core.publisher.Mono implementieren das Interface org.reactivestreams.Publisher, das lediglich die Methode void subscribe(Subscriber subscriber) spezifiziert. Ein Nachrichtenempfänger (Subscriber) kann sich demzufolge bei einem Flux- oder einem Mono-Objekt registrieren, um kontinuierlich mit neuen Nachrichten versorgt zu werden. Spring ermöglicht es, Flux- oder Mono-Objekte einem Client über eine API zur Verfügung zu stellen, und erzeugt dazu eine EventSource gemäß des Protokolls Server-Sent Events (SSE). Das folgende Code-Beispiel verdeutlicht wie mittels Spring WebFlux ein endlicher Datenstrom (Stream) erzeugt wird, in dem verzögert bis zu einem bestimmten Zielwert hochgezählt wird. Der Datenstrom endet, wenn der Zielwert erreicht ist.

import reactor.core.publisher.Flux;
// ...

@RestController
@RequestMapping("/count")
public class CounterController {

    @GetMapping(path = "/{number}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    @CrossOrigin
    public Flux<Integer> countToNumber(@PathVariable("number") int number) {

        Flux<Integer> numbers = Flux.create(sink -> {

            Runnable counter = () -> {
                for (int i = 1; i <= number; i++) {
                    sink.next(i); // send number to client
                    try {
                        TimeUnit.SECONDS.sleep(1); // delay counting
                    } catch (InterruptedException e) { }
                }
                sink.complete(); // counting is completed, stream ends
            };

            new Thread(counter).start();
        });
        return numbers;
    }
}

Im nächsten Code-Beispiel soll die Quelle eines Datenstroms ein MongoDB-Datenbanksystem sein. Jedes Mal, wenn neue Dokumente in eine Collection innerhalb der MongoDB-Datenbank eingefügt werden, soll die serverseitige Komponente auf Basis von Spring WebFlux einen registrierten Client darüber benachrichtigen. Die Dokumente könnten z.B. Tweets sein, die aus einer unendlichen externen Quelle bezogen werden. Die Architektur entspricht damit durchgängig dem reaktiven Programmiermodell.

import reactor.core.publisher.Flux;
// ...

@RestController
public class TweetController {

    @Autowired
    TweetRepository tweetRepository;

    @GetMapping("/tweets") @CrossOrigin
    public Flux<Tweet> getTweets() {
        return tweetRepository.findTweetsBy(); // return infinite stream
    }
}
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.data.mongodb.repository.Tailable;
// ...

public interface TweetRepository extends ReactiveMongoRepository<Tweet, String> {

    @Tailable
    Flux<Tweet> findTweetsBy(); // infinite stream as annotated by @Tailable
}
import org.springframework.data.mongodb.core.mapping.Document;
// ...

@Document(collection = Tweet.COLLECTION_NAME) @Getter @Setter
public class Tweet {

	public final static String COLLECTION_NAME = "tweet";

	@Id
	String id;
	String text;
	Date date;

	public Tweet(String text) {
		this.text = text;
		this.date = new Date();
	}
}
@SpringBootApplication
public class TweetStarter implements CommandLineRunner {

    @Autowired
    TweetRepository tweetRepository;

    @Autowired
    ReactiveMongoTemplate reactiveMongoTemplate;

    public static void main(String[] args) {
        SpringApplication.run(TweetStarter.class, args);
    }

    @Override
    public void run(String... args) throws Exception {

        // create a capped collection which supports tailable cursors, limited to 5 documents here
        reactiveMongoTemplate.dropCollection(Tweet.COLLECTION_NAME).block();
        reactiveMongoTemplate.createCollection(Tweet.COLLECTION_NAME, 
          CollectionOptions.empty().capped().size(4096).maxDocuments(5)).block();

        // prepare HTTP request for new lorem ipsum tweet
        HttpClient client = HttpClient.newHttpClient();
        String loremIpsumApi = "https://baconipsum.com/api/?type=all-meat&sentences=1&start-with-lorem=1&format=text";
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(loremIpsumApi)).build();

        // post a new tweet every 3 seconds
        Runnable changePrices = () -> {
            try {
                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                Tweet tweet = new Tweet(response.body());
                tweetRepository.save(tweet).block();
            } catch (InterruptedException | IOException e) { }
        };
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(changePrices, 0, 3, TimeUnit.SECONDS);
    }
}

Die gezeigten Code-Beispiele zum Umgang mit reaktiven Streams in Spring finden sich im Verzeichnis /remote/spring-webflux des Modul-Repository.