Skip to content

18. Ein für den Webdienst / jSON programmierter Client

Da die Basis [dbproduitscategories] nun im Web verfügbar ist, werden wir eine Anwendung schreiben, die diese nutzt. Wir erhalten dann die folgende Client-Server-Architektur:

Die Client-Anwendung wird drei Schichten umfassen:

  • eine Schicht [Client HTTP] [3] zur Kommunikation mit der Webanwendung / jSON, die die Datenbank bereitstellt;
  • eine Schicht [DAO] [2], die dieselbe Schnittstelle wie die Schicht [DAO] [4] bereitstellt;
  • eine Testschicht JUnit [1], um zu überprüfen, ob Client und Server ordnungsgemäß funktionieren;

18.1. Das Eclipse-Projekt

Das Eclipse-Projekt des Clients sieht wie folgt aus:

 
  • Das Paket [spring.webjson.client.config] enthält die Spring-Konfiguration der Schicht [DAO];
  • das Paket [spring.webjson.client.dao] enthält die Implementierung der Schicht [DAO];
  • Das Paket [spring.webjson.client.entities] enthält die Objekte, die mit dem Webdienst /jSON ausgetauscht werden. Wir kennen sie alle;
  • Das Paket [spring.webjson.client.infrastructure] enthält die vom Projekt verwendeten Ausnahmeklassen. Wir kennen sie alle;

18.2. Maven-Konfiguration des Projekts

Das Projekt ist ein Maven-Projekt, das durch die folgende Datei „[pom.xml]“ konfiguriert wird:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-webjson-client-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <description>Client console du serveur web / jSON</description>
    <name>spring-webjson-client-generic</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <!-- von Spring verwendete Bibliothek jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- von Spring verwendete Komponente RestTemplate -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Protokollbibliothek -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot-Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- Plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 16–20: Das übergeordnete Maven-Projekt [spring-boot-starter-parent], mit dem wir eine Reihe von Abhängigkeiten ohne Angabe der Versionen definieren können, da diese im übergeordneten Projekt festgelegt sind;
  • Zeilen 24–27: Obwohl wir keine Webanwendung schreiben, benötigen wir die Abhängigkeit [spring-web], die die Klasse [RestTemplate] mit sich bringt, welche eine einfache Anbindung an eine Webanwendung / jSON ermöglicht;
  • Zeilen 29–36: eine Bibliothek jSON;
  • Zeilen 38–41: eine Abhängigkeit, die es uns ermöglicht, den Client-Anfragen HTTP einen timeout zuzuweisen. Ein timeout ist eine maximale Wartezeit auf die Antwort des Servers. Nach Ablauf dieser Zeit meldet der Client einen Fehler vom Typ timeout, indem er eine Ausnahme auslöst;
  • Zeilen 43–48: die Google Guava-Bibliothek;
  • Zeilen 50–53: die Logging-Bibliothek;
  • Zeilen 54–64: die Abhängigkeit für die Tests JUnit. Sie enthält insbesondere die für die Tests erforderliche Bibliothek JUnit 4. Diese Abhängigkeiten tragen das Attribut [<scope>test</scope>], das angibt, dass sie nur für die Testphase benötigt werden. Sie sind nicht im endgültigen Projektarchiv enthalten;

18.3. Spring-Konfiguration

  

Die Klasse [AppConfig] übernimmt die Spring-Konfiguration des Clients HTTP. Ihr Code lautet wie folgt:


package spring.webjson.client.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@Configuration
@ComponentScan({ "spring.webjson.client.dao" })
public class AppConfig {

    // Konstanten
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8081";

    // Filter jSON
    @Bean
    public ObjectMapper jsonMapper(RestTemplate restTemplate) {
        return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortProduit(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongProduit(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // Erstellung der Komponente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // Konverter jSON
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        // Zeitüberschreitung beim Datenaustausch
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // Ergebnis
        return restTemplate;
    }

    @Bean
    public int timeout() {
        return TIMEOUT;
    }

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
}
  • Zeile 20: Die Klasse ist eine Spring-Konfigurationsklasse;
  • Zeile 21: Weitere Spring-Komponenten sind im Paket [spring.webjson.client.dao] zu suchen;
  • Zeile 25: Es wird ein timeout von einer Sekunde (1000 ms) festgelegt;
  • Zeilen 88–91: Das Bean, das diesen Wert zurückgibt;
  • Zeile 26: der URL des Webdienstes / jSON;
  • Zeilen 93–96: Die Bean, die diesen Wert zurückgibt;
  • Zeilen 72–86: die Konfiguration der Klasse [RestTemplate], die den Datenaustausch mit dem Webdienst / jSON gewährleistet. Wenn keine Konfiguration erforderlich ist, kann sie im Code einfach durch [new RestTemplate()] angegeben werden. Hier möchten wir den timeout für den Datenaustausch mit dem Webdienst / jSON festlegen. Die Bean [timeout] aus Zeile 89 wird als Parameter an die Methode [restTemplate] aus Zeile 73 übergeben;
  • Zeile 75: Die Komponente [HttpComponentsClientHttpRequestFactory] ermöglicht es uns, den timeout für den Datenaustausch festzulegen (Zeilen 82–83);
  • Zeile 76: Die Klasse [RestTemplate] wird mit dieser Komponente erstellt. Da sie sich auf diese stützt, um mit dem Webdienst /jSON zu kommunizieren, werden die Datenaustausche tatsächlich an die Klasse timeout übergeben;
  • Zeilen 78–80: Der Klasse [RestTemplate] wird ein Konverter jSON zugeordnet. Darauf sind wir bereits bei der Betrachtung des Webdienstes eingegangen. Client und Server tauschen Textzeilen aus. Ein Konverter sorgt dafür, ein Objekt in Text zu serialisieren und umgekehrt einen Text in ein Objekt zu deserialisieren. Der Klasse [RestTemplate] können mehrere Konverter zugeordnet sein, und welcher zu einem bestimmten Zeitpunkt ausgewählt wird, hängt von den vom Server gesendeten Headern HTTP ab. Hier haben wir nur einen Konverter jSON, da die ausgetauschten Textzeilen vom Typ jSON sind;
  • Zeilen 82–83: Hier werden die timeout-Parameter für den Datenaustausch festgelegt;
  • Zeilen 28–70: Definieren jSON-Filter. Diese entsprechen denen des Servers, die in Abschnitt 17.3.2.1 vorgestellt wurden;
  • Zeilen 29–32: Die Bean [jsonMapper] ist der Mapper jSON des Konverters [MappingJackson2HttpMessageConverter], den wir der Klasse [RestTemplate] zugeordnet haben. Wir benötigen ihn in der Definition der Filter jSON;
  • Zeilen 34–41: Ein Bean, das den Filter jSON [catégorie sans ses produits] definiert. Die Methode [jsonMapperShortCategorie] erhält als Parameter das in Zeile 73 definierte Bean [restTemplate];
  • Zeile 37: Die Methode [jsonMapper] aus Zeile 30 wird aufgerufen, um den Mapper jSON abzurufen;
  • Zeilen 38–39: Der Filter wird so festgelegt, dass eine Kategorie ohne ihre Produkte angezeigt wird;
  • Zeile 40: Der Mapper jSON wird entsprechend konfiguriert;
  • Zeilen 42–51: Der Filter jSON, um eine Kategorie mit ihren Produkten zu erhalten;
  • Zeilen 53–60: Der Filter jSON, um ein Produkt ohne seine Kategorie zu erhalten;
  • Zeilen 62–70: Der Filter jSON, um ein Produkt mit seiner Kategorie anzuzeigen;

Alle diese Beans stehen für die Codes der Ebene [DAO] sowie für die Tests JUnit zur Verfügung.

18.4. Implementierung des Clients HTTP

Oben ist die Schicht [Client HTTP] dargestellt, die mit dem soeben erstellten Webdienst kommuniziert. Diese betrachten wir nun genauer.

  

Die Klasse [Client] implementiert den Datenaustausch mit dem Webservice / jSON. Sie implementiert die folgende Schnittstelle [IClient]:


package spring.webjson.client.dao;

import org.springframework.http.HttpMethod;

public interface IClient {
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body);
}

Die Schnittstelle verfügt nur über eine Methode [getResponse]:

  • Zeile 6: Die Methode [getResponse] ist eine generische Methode, die durch zwei Typen parametrisiert wird:
    • [T1]: ist der vom Server in [Response<T1>] erwartete Antworttyp, zum Beispiel [List<Categorie>],
    • [T2]: ist der Typ des Parameters jSON, der von den Operationen POST übermittelt wird, zum Beispiel [List<Produit>];
  • Zeile 6: Die Methode [getResponse] liefert ein Ergebnis vom Typ T1, zum Beispiel [List<Categorie>];
  • Zeile 6: Die Parameter von [getResponse] lauten wie folgt:
    • [String url]: das abzufragende URL;
    • [HttpMethod method]: Methode HTTP der Abfrage, je nach Fall GET oder POST,
    • [int errStatus]: Fehlercode, der in der Klasse [DaoException] zu verwenden ist, falls bei der Kommunikation mit dem Server ein Fehler auftritt,
    • [T2 body]: Der Wert, der gesendet werden soll, falls POST vorhanden ist;

Die Klasse [Client] implementiert die Schnittstelle [IClient] wie folgt:


package spring.webjson.client.dao;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import spring.webjson.client.infrastructure.DaoException;

@Component
public class Client implements IClient {

    // Einspeisungen
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // lokal
    private String simpleClassName = getClass().getSimpleName();

    // Generische Anfrage
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
    }

    // Liste der Fehlermeldungen einer Ausnahme
    protected List<String> getMessagesForException(Exception exception) {
    ...
    }
}
  • Zeile 18: Die Klasse [Client] ist eine Spring-Komponente und kann daher in andere Spring-Komponenten injiziert werden;
  • Zeilen 22–23: Injektion des Beans [RestTemplate], das in [AppConfig] definiert ist (siehe Abschnitt 18.3) und die Kommunikation mit dem Server gewährleistet;
  • Zeilen 24–25: Injektion des Webdienstes URL / jSON, der in [AppConfig] definiert ist (siehe Abschnitt 18.3);
  • Zeilen 37–39: Die private Methode [getMessagesForException] ist eine Hilfsmethode, mit der die Liste der in einer Ausnahme enthaltenen Fehlermeldungen abgerufen werden kann. Wir sind ihr bereits mehrfach begegnet;

Weiter geht’s:


    // generische Anfrage
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // die Antwort des Servers
        ResponseEntity<Response<T1>> response;
        try {
            // Die Anfrage wird vorbereitet
            RequestEntity<?> request = null;
            if (method == HttpMethod.GET) {
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            if (method == HttpMethod.POST) {
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(body);
            }
            // Die Abfrage wird ausgeführt
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // Die Ausnahme wird gekapselt
            throw new DaoException(errStatus, e, simpleClassName);
        }
        ...
}
  • Zeile 18: Die Anweisung, die die Anfrage an den Server sendet und dessen Antwort empfängt. Die Komponente [RestTemplate] bietet eine Vielzahl von Methoden für den Austausch mit dem Server, doch nur die Methode [exchange] akzeptiert generische Parameter. Aus diesem Grund wurde sie ausgewählt. Der zweite Parameter legt den Typ der erwarteten Antwort fest. Der erste Parameter ist die Anfrage vom Typ [RequestEntity] (Zeile 8). Das Ergebnis der Methode [exchange] ist vom Typ [ResponseEntity<Response<T1>>] (Zeile 5). Der Typ [ResponseEntity] kapselt die vollständige Antwort des Servers, einschließlich der Header HTTP und des vom Server gesendeten Dokuments. Ebenso kapselt der Typ [RequestEntity] die gesamte Anfrage des Clients, einschließlich der Header HTTP und des eventuell übermittelten POST-Werts;
  • Zeilen 8–16: Wir müssen die Anfrage vom Typ [RequestEntity] erstellen. Diese unterscheidet sich je nachdem, ob ein GET oder ein POST für die Anfrage verwendet wird;
  • Zeile 10: Die Abfrage für ein GET. Die Klasse [RequestEntity] bietet statische Methoden zum Erstellen der Abfragen GET, POST, HEAD, ... Die Methode [RequestEntity.get] ermöglicht es, eine Abfrage vom Typ GET zu erstellen, indem die verschiedenen Methoden, aus denen diese besteht, miteinander verkettet werden:
    • Die Methode [RequestEntity.get] akzeptiert als Parameter die Ziel-URL in Form einer Instanz von URI,
    • die Methode [accept] ermöglicht die Definition der Elemente des Headers HTTP [Accept]. Hier geben wir an, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
    • die Methode [build] verwendet diese verschiedenen Informationen, um den Typ [RequestEntity] der Anfrage zu erstellen;
  • Zeile 14: die Anfrage für einen POST. Mit der Methode [RequestEntity.post] lässt sich eine Anfrage vom Typ POST erstellen, indem die verschiedenen Methoden, aus denen diese besteht, miteinander verkettet werden:
    • Die Methode [RequestEntity.post] akzeptiert als Parameter das Ziel-URL in Form einer Instanz von URI,
    • die Methode [header] definiert einen Header HTTP. Hier wird der Header [Content-Type: application/json] an den Server gesendet, um ihm mitzuteilen, dass der übermittelte Wert in Form einer Zeichenkette jSON bei ihm eintreffen wird;
    • die Methode [accept] ermöglicht es, anzugeben, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
    • Die Methode [body] legt den übermittelten Wert fest. Dieser ist der vierte Parameter der generischen Methode [getResponse] (Zeile 1);
  • Zeilen 20–23: Tritt ein Kommunikationsfehler mit dem Server auf, wird eine Ausnahme vom Typ [DaoException] ausgelöst, deren Fehlercode der Parameter [errStatus] ist, der als dritter Parameter an die generische Methode [getResponse] (Zeile 3) übergeben wurde;

Die Methode [getResponse] wird wie folgt fortgesetzt:


// Generische Anfrage
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
        // Der Antworttext wird abgerufen
        Response<T1> entity = response.getBody();
        int status = entity.getStatus();
        // Fehler auf Serverseite?
        if (status != 0) {
            // Es wird eine Ausnahme ausgelöst
            throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
        } else {
            // Alles in Ordnung
            return entity.getBody();
        }
    }
  • Zeile 4: Wir haben die Antwort vom Server erhalten. Sie ist vom Typ [ResponseEntity<Response<T1>>] (Zeile 5 des zuvor untersuchten Codes), wobei die Klasse [Response] die bereits serverseitig verwendete Klasse ist:

package spring.webjson.client.dao;

public class Response<T> {

    // ----------------- Eigenschaften
    // Status der Operation
    private int status;
    // die eventuelle Ausnahme
    private String exception;
    // der Antworttext
    private T body;

    // Konstruktoren
    public Response() {

    }

    public Response(int status, String exception, T body) {
        this.status = status;
        this.exception = exception;
        this.body = body;
    }

    // Getter und Setter
...
}

Kehren wir zur Methode [getResponse] zurück:

  • Zeile 6: Wir extrahieren das in der Antwort enthaltene Dokument vom Typ [Response<T1>]. Dieser Typ enthält die Felder [int status, String exception, T1 body];
  • Zeile 7: Wir extrahieren das [status] aus der Antwort, bei dem es sich um einen Fehlercode handelt;
  • Zeilen 9–12: Liegt ein Fehler vor, wird eine Ausnahme ausgelöst, die die beiden Informationen [status, exception] aus der Serverantwort enthält;
  • Zeile 14: Andernfalls geben wir den Typ [T1] zurück, der in der Antwort vom Typ [Response<T1>] enthalten ist;

Die Klasse [Client] ist allgemein gehalten. Sie kann für jeden Web-Client / jSON verwendet werden.

18.5. Implementierung der Schicht [Dao]

  

18.5.1. Die Klasse [AbstractDao]

Die clientseitige Schicht [DAO] weist dieselbe Schnittstelle auf wie die serverseitige Schicht [DAO] (siehe Abschnitt 4.7):


package spring.webjson.client.dao;

import java.util.List;

import spring.webjson.client.entities.AbstractCoreEntity;

public interface IDao<T extends AbstractCoreEntity> {

    // Liste aller T-Entitäten
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // bestimmter Entitäten – Kurzfassung
    public List<T> getShortEntitiesById(Iterable<Long> ids);

    public List<T> getShortEntitiesById(Long... ids);

    public List<T> getShortEntitiesByName(Iterable<String> names);

    public List<T> getShortEntitiesByName(String... names);

    // bestimmter Entitäten – Langfassung
    public List<T> getLongEntitiesById(Iterable<Long> ids);

    public List<T> getLongEntitiesById(Long... ids);

    public List<T> getLongEntitiesByName(Iterable<String> names);

    public List<T> getLongEntitiesByName(String... names);

    // Aktualisierung mehrerer Entitäten
    public List<T> saveEntities(Iterable<T> entities);

    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);

    // Löschen aller Entitäten
    public void deleteAllEntities();

    // Löschen mehrerer Entitäten
    public void deleteEntitiesById(Iterable<Long> ids);

    public void deleteEntitiesById(Long... ids);

    public void deleteEntitiesByName(Iterable<String> names);

    public void deleteEntitiesByName(String... names);

    public void deleteEntitiesByEntity(Iterable<T> entities);

    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}

Die Klasse [AbstractDao] implementiert die Schnittstelle [IDao]. Es handelt sich um eine Klasse, die der gleichnamigen serverseitigen Klasse entspricht (siehe Abschnitt 4.8). Sie dient als übergeordnete Klasse für die Klassen [DaoCategorie] und [DaoProduit]. Aus zwei Gründen ist sie nicht identisch:

  • Auf der serverseitigen Seite verwaltet die Klasse [AbstractDao] eine Information:

    // Einfügungen
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

die wir hier nicht benötigen.

  • Auf der Serverseite verwendet die Klasse [AbstractDao] die Annotation [@Transactional], um jede Methode in eine Transaktion zu kapseln. Auf der Clientseite gibt es keine Datenbank, die verwaltet werden muss. Diese Annotation entfällt daher;

Die Klasse [AbstractDao] beschränkt sich darauf, die Gültigkeit der Aufrufparameter der Methoden der Schnittstelle [IDao] zu überprüfen, bevor sie den Aufruf an die untergeordneten Klassen weiterleitet:


package spring.webjson.client.dao;

import java.util.ArrayList;
import java.util.List;

import spring.webjson.client.entities.AbstractCoreEntity;
import spring.webjson.client.infrastructure.MyIllegalArgumentException;

import com.google.common.collect.Lists;

public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {

    // lokal
    protected String simpleClassName = getClass().getSimpleName();

    @Override
    public List<T1> getShortEntitiesById(Iterable<Long> ids) {
        // Gültigkeit des Arguments
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // Ergebnis
        return getShortEntitiesById(Lists.newArrayList(ids));
    }

    @Override
    public List<T1> getShortEntitiesById(Long... ids) {
        // Gültigkeit des Arguments
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // Ergebnis
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
...
    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
        ...
    }

    // private Methoden ----------------------------------------------
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
        // Null-Elemente?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
                    simpleClassName);
        }
        // Leere Elemente?
        if (!elements.iterator().hasNext()) {
            if (checkEmpty) {
                throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),simpleClassName);
            } else {
                return new ArrayList<T1>();
            }
        }
        // Standard-Ergebnis
        return null;
    }

    @SuppressWarnings("unchecked")
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
        // Null-Elemente?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
        }
        // leere Elemente?
        if (elements.length == 0) {
            if (checkEmpty) {
                throw new MyIllegalArgumentException(223, new RuntimeException("L'argument ne peut être une liste vide"),
                        simpleClassName);
            } else {
                return new ArrayList<T1>();
            }
        }
        // Standardwert
        return null;
    }

    // geschützte Methoden ----------------------------------------------
    abstract protected List<T1> getShortEntitiesById(List<Long> ids);

    abstract protected List<T1> getShortEntitiesByName(List<String> names);

    abstract protected List<T1> getLongEntitiesById(List<Long> ids);

    abstract protected List<T1> getLongEntitiesByName(List<String> names);

    abstract protected List<T1> saveEntities(List<T1> entities);

    abstract protected void deleteEntitiesById(List<Long> ids);

    abstract protected void deleteEntitiesByName(List<String> names);
}

18.5.2. Die Klasse [DaoCategorie]

  

Die Klasse [DaoCategorie] sieht wie folgt aus:


package spring.webjson.client.dao;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;

import spring.webjson.client.entities.Categorie;
import spring.webjson.client.entities.CoreCategorie;
import spring.webjson.client.entities.CoreProduit;
import spring.webjson.client.entities.Produit;
import spring.webjson.client.infrastructure.DaoException;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    @Autowired
    private ApplicationContext context;
    @Autowired
    private IClient client;

...
}
  • Zeile 19: Die Klasse [DaoClient] ist eine Spring-Komponente, in die daher andere Spring-Komponenten injiziert werden können;
  • Zeile 20: Die Klasse [DaoClient] erweitert die soeben betrachtete Klasse [AbstractDao<Categorie>] und implementiert somit die Schnittstelle [IDao<Categorie>];
  • Zeilen 22–23: Wir injizieren den Spring-Kontext, um Zugriff auf dessen Beans zu erhalten;
  • Zeilen 24–25: Wir injizieren den Client HTTP, den wir gerade erstellt haben;

Die Implementierungen der verschiedenen Methoden der Schnittstelle [DaoCategorie] folgen alle demselben Schema. Wir stellen drei Methoden vor, von denen eine auf einer Operation [GET] basiert, die beiden anderen auf einer Operation [POST].

18.5.2.1. Die Methode [getAllLongEntities]

Die Methode [getAllLongEntities] gibt die Langform aller Kategorien in der Datenbank zurück:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // Filter jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // Alle Kategorien abrufen
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // die Liste der Kategorien List<Kategorie>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // Produkt-Kategorie-Zuordnung neu erstellen
            return linkCategorieWithProduits(categories);
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(233, e2, simpleClassName);
        }
}
  • Zeile 2: Die Methode gibt die Liste der Kategorien in ihrer Langform zurück;
  • Zeile 5: Der Mapper jSON, der die Serialisierung des gesendeten Werts (es gibt keinen) und die Deserialisierung der von der Klasse [Client] zurückgegebenen Antwort (Kategorien in ihrer Langform) ermöglicht;
  • Zeile 7: Es wird die Methode [getResponse] der Klasse [Client] aufgerufen. Diese Methode sorgt für den Austausch mit dem Webdienst / jSON. Ihre Parameter lauten wie folgt:
    • die URL des abgefragten Dienstes [/getAllLongCategories];
    • die zu verwendende Methode [GET];
    • der im Fehlerfall zu verwendende Fehlercode (232);
    • der übermittelte Wert. Hier gibt es keinen;
  • Zeile 7: Im Ausdruck [client.<List<Categorie>, Void>] werden die tatsächlichen Parameter der generischen Typen [T1, T2] der Methode [getResponse] angegeben. Zur Erinnerung: [T1] ist der Typ der erwarteten Antwort und [T2] der Typ des übergebenen Werts. Hier wird ein Ergebnis vom Typ [List<Categorie>] erwartet, und es gibt keinen übermittelten Wert vom Typ [Void];
  • Zeile 7: Das von der Methode [getResponse] zurückgegebene Ergebnis wird in ein Objekt vom Typ [Object] gespeichert. Das ist etwas seltsam, da eigentlich ein Typ [List<Categorie>] erwartet wird. Dies liegt daran, dass die Methode [getResponse], die mit generischen Typen vom Typ [T1, T2] arbeitet, systematisch einen Typ [java.util.LinkedHashMap] zurückgibt, der dann weiterverarbeitet werden muss, um den richtigen Typ zu erhalten;
  • Zeile 9: Die Liste der Kategorien wird zurückgegeben. Dazu wird das Objekt [map] [mapper.writeValueAsString(map)] in eine Zeichenkette jSON serialisiert, die anschließend wieder in einen Typ [List<Categorie>] deserialisiert wird;
  • Zeile 13: Es wurde eine Liste von Kategorien empfangen, von denen einige Produkte enthalten können. Man erhält die Kurzform dieser Produkte. Wenn sie also deserialisiert werden, verfügen die erstellten Objekte [Produit] über ihr Feld [categorie==null]. Die Methode [linkCategorieWithProduits] stellt die Verbindung zwischen einem [Produit] und seinem [Categorie] wieder her;
  • Zeilen 14–15: Die Ausnahme vom Typ [DaoException], die die Methode [getResponse] möglicherweise ausgelöst hätte, wird abgefangen, um sie sofort erneut auszulösen. Dieses seltsame Verhalten ist darauf zurückzuführen, dass andernfalls die Ausnahme vom Typ [DaoException] durch die Zeilen 16–18 abgefangen würde, was wir nicht wollen;
  • Zeilen 16–18: Alle anderen Ausnahmen werden abgefangen, um sie in einen Typ [DaoException] zu kapseln. Zur Erinnerung: Die Schicht [DAO] darf nur diesen Ausnahmetyp auslösen;

Die Methode [linkCategorieWithProduits], die die Verknüpfungen zwischen den Entitäten [Produit] und [Categorie] wiederherstellt, lautet wie folgt:


    private List<Categorie> linkCategorieWithProduits(List<Categorie> categories) {
        for (Categorie categorie : categories) {
            List<Produit> produits = categorie.getProduits();
            if (produits != null) {
                for (Produit produit : produits) {
                    produit.setCategorie(categorie);
                }
            }
        }
        return categories;
}

18.5.2.2. Filterverwaltung jSON

Kehren wir zur Filterverwaltung jSON in der vorherigen Methode [getAllLongEntities] zurück:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // Filter jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // Alle Kategorien abrufen
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // die Liste der Kategorien List<Kategorie>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });

  • Zeile 5: Aus dem Spring-Kontext wird ein Mapper jSON abgerufen, der die Langformen der Kategorien verarbeiten kann. Kehren wir zur Definition dieses Mappers in der Spring-Konfiguration [AppConfig] zurück:

// Filter jSON
    @Bean
    public ObjectMapper jsonMapper(RestTemplate restTemplate) {
        return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
        ObjectMapper jsonMapper = jsonMapper(restTemplate);
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
}
    @Bean
    public RestTemplate restTemplate(int timeout) {
    ...
    }

  • Das von der Methode [getAlllongEntities] angeforderte Bean [jsonMapperLongCategorie] ist das Bean aus den Zeilen 7–15;
  • Zeile 10: Der Mapper wird von der Methode [jsonMapper] in den Zeilen 2–5 bereitgestellt. Man sieht, dass dieser Mapper jSON zu dem Objekt [RestTemplate] gehört, das den Datenaustausch HTTP zwischen Client und Server verwaltet. Dieser Mapper wird standardmäßig verwendet, um:
    • die an den Server gesendete Wert zu serialisieren;
    • die vom Server zurückgesendete Antwort zu deserialisieren;

Kehren wir zum Code von [getAllLongEntities] zurück:


            // Filter jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // Alle Kategorien abrufen
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // die Liste der Kategorien List<Kategorie>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // Produkt-Kategorie-Verknüpfung neu erstellen
return linkCategorieWithProduits(categories);
  • Zeile 2: Der Mapper [jsonMapperLongCategorie] wird aus dem Spring-Kontext abgerufen;
  • Zeile 4: Die Methode [getResponse] wird ausgeführt. Dabei erfolgt:
    • eine automatische Serialisierung des gesendeten Werts (hier gibt es keinen);
    • automatische Deserialisierung der empfangenen Antwort, in diesem Fall vom Typ `List<Categorie>`. Da die Entität `[Categorie]` einen Filter `jSON [jsonFilterCategorie]` besitzt, musste dieser verarbeitet werden. Das ist der Grund für Zeile 2;
  • Zeile 6: Das Ergebnis durchläuft eine zweite Serialisierung/Deserialisierung mit demselben Mapper, um den Typ `List<Categorie>` wiederherzustellen. In Zeile 4 ist der von `[getResponse]` zurückgegebene Typ ein Typ `[Object]`;

In den folgenden Methoden ist zu beachten, dass der vom Spring-Kontext angeforderte Mapper jSON sowohl für den gesendeten Wert (Serialisierung) als auch für den empfangenen Wert (Deserialisierung) verwendet wird. Wenn einer oder beide Werte einen Filter vom Typ jSON haben, müssen diese konfiguriert werden. Der Mapper kann somit bis zu zwei konfigurierte Filter haben. Im Folgenden tritt dies nie auf. Entweder hat der gesendete Wert keinen Filter (List<Long>, List<String>), oder der empfangene Wert hat keinen (List<CoreCategorie>, List<CoreProduit>). Die Entitäten mit dem Filter jSON sind ausschließlich [Categorie] und [Produit].

18.5.2.3. Die Methode [getShortEntitiesById]

Die Methode [getShortEntitiesById] gibt die Kurzformen der Kategorien zurück, deren Primärschlüssel sie als Parameter erhält:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            // Filter jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // Eine Kategorie ohne ihre Produkte abrufen
            Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
            // die Kategorie
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(223, e2, simpleClassName);
        }
}
  • Zeile 5: Der Mapper jSON, der die Serialisierung des übermittelten Werts (eine Liste von Primärschlüsseln) und die Deserialisierung der von der Klasse [Client] zurückgegebenen Antwort (Kategorien in ihren Kurzversionen) ermöglicht. Der ausgewählte Filter hat keinerlei Auswirkungen auf den gesendeten Wert, da für die Elemente der gesendeten Liste kein Filter vorhanden ist;
  • Zeile 7: Die Methode [getResponse] der übergeordneten Klasse wird aufgerufen. Diese Methode sorgt für den Austausch mit dem Webdienst /jSON. Ihre Parameter lauten wie folgt:
    • die URL des abgefragten Dienstes [/getShortCategoriesById];
    • die zu verwendende Methode [POST];
    • der im Fehlerfall zu verwendende Fehlercode (204);
    • der übermittelte Wert. In diesem Fall handelt es sich um eine Liste von Primärschlüsseln;
  • Zeile 7: Im Ausdruck [client.<List<Categorie>, List<Long>>] werden die effektiven Parameter der generischen Typen [T1, T2] der Methode [getResponse] bezeichnet. Zur Erinnerung: [T1] ist der Typ der erwarteten Antwort und [T2] der Typ des übermittelten Werts. Hier wird ein Ergebnis vom Typ [List<Categorie>] erwartet, und der übergebene Wert ist eine Liste von Primärschlüsseln vom Typ [List<Long>];
  • Zeile 7: Das von der Methode [getResponse] zurückgegebene Ergebnis wird in ein Objekt vom Typ [Object] gespeichert;
  • Zeile 9: Die Liste der Kategorien wird zurückgegeben. Dazu wird das Objekt [map] [mapper.writeValueAsString(map)] in eine Zeichenkette jSON serialisiert, die anschließend wieder in einen Typ [List<Categorie>] deserialisiert wird;

18.5.2.4. Die Methode [saveEntities]

Die Methode [saveEntities] speichert Kategorien in der Datenbank. Ihr Code lautet wie folgt:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // Filter jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // Kategorien hinzufügen
            Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
                    entities);
            // Liste der hinzugefügten Kernkategorien
            List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<CoreCategorie>>() {
                    });
            // Die Kategorien werden mit den erhaltenen Informationen aktualisiert
            for (int i = 0; i < entities.size(); i++) {
                Categorie categorie = entities.get(i);
                CoreCategorie coreCategorie = coreCategories.get(i);
                categorie.setId(coreCategorie.getId());
                List<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    List<CoreProduit> coreProduits = coreCategorie.getCoreProduits();
                    for (int j = 0; j < produits.size(); j++) {
                        Produit produit = produits.get(j);
                        produit.setId(coreProduits.get(j).getId());
                        produit.setIdCategorie(categorie.getId());
                        produit.setCategorie(categorie);
                    }
                }
            }
            return entities;
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(220, e2, simpleClassName);
        }
    }
  • Zeile 2: Die Methode [saveEntities] dient dazu, die als Parameter übergebenen Kategorien in der Datenbank zu speichern. Sie speichert diese Kategorien, ergänzt um ihre Primärschlüssel. Werden die Kategorien zusammen mit Produkten übergeben, werden diese ebenfalls gespeichert;
  • Zeile 5: Der Mapper jSON ermöglicht die Serialisierung des gesendeten Werts (eine Liste von Kategorien in ihrer Langform) und die Deserialisierung der von der Klasse [Client] zurückgegebenen Antwort (Objekte vom Typ [CoreCategorie]). Der ausgewählte Filter hat keinerlei Auswirkungen auf das Ergebnis, da die Elemente der als Antwort empfangenen Liste keinen Filter haben;
  • Zeile 7: Die Methode [getResponse] der übergeordneten Klasse wird aufgerufen, um den Datenaustausch mit dem Webdienst / jSON durchzuführen;
    • Der erste Parameter ist URL [/saveCategories];
    • der zweite Parameter ist die zu verwendende Methode HTTP, in diesem Fall [POST];
    • der dritte Parameter ist der im Fehlerfall zu verwendende Fehlercode (200);
    • der letzte Parameter ist der übermittelte Wert, hier die Liste der zu speichernden Kategorien;
  • Zeile 7: Die generischen Parameter [T1, T2] der Methode [getResponse] lauten hier [List<CoreCategorie>, List<Categorie>]. Der erste Typ ist der der erwarteten Antwort, der zweite der Typ des übermittelten Werts;
  • Zeile 7: Die erhaltene Antwort wird in einen Typ [Object] abgelegt;
  • Zeile 9: Die Antwort vom Typ [List<CoreCategorie>] wird rekonstruiert. Die zurückzugebende Antwort ist vom Typ [List<Categorie>] (Zeile 2) und nicht vom Typ [List<CoreCategorie>]. Die empfangene Antwort ist die Liste der Primärschlüssel der gespeicherten Kategorien und Produkte;
  • Zeilen 14–28: Die empfangenen Primärschlüssel werden den Kategorien und Produkten zugeordnet (Zeilen 17, 23, 24). Außerdem werden die Verknüpfungen [Produit] --> [Categorie] (Zeilen 24–25) rekonstruiert;

Alle anderen Methoden folgen dem gleichen Schema.

18.6. Der Test JUnit

Kehren wir zur Client-Server-Architektur zurück, die gerade aufgebaut wird:

Wir haben eine Schicht [DAO] [2] mit derselben Schnittstelle wie die Schicht [DAO] [4] erstellt. Zum Testen der Schicht [DAO] [2] können daher die Tests JUnit verwendet werden, die bereits zum Testen der Schicht [DAO] [4] dienten:

  

Diese drei Tests werden anhand der folgenden Ausführungskonfigurationen ausgeführt:

 

Die Ergebnisse der drei Tests lauten wie folgt:

  • in [1], der Test [JUnitTestCheckArguments];
  • in [2] der Test [JUnitTestDao];
  • in [3] der clientseitig ausgeführte Test [JUnitTestPushTheLimits] (Projekt [spring-webjson-client-generic]);
  • in [3] der serverseitig ausgeführte Test [JUnitTestPushTheLimits] (Projekt [spring-jdbc-generic-04]). Es ist festzustellen, dass die Netzwerkschicht im Vergleich zu den durch den Zugriff auf SGBD verursachten Verzögerungen nur sehr geringe Verzögerungen mit sich bringt;

18.7. Implementierung des Webdienstes / jSON / JPA / Hibernate

Wir befassen uns nun mit der folgenden Architektur:

Die Änderung befindet sich in [1]. Die Serverschicht [DAO] basiert auf einer Implementierung JPA. Zunächst werden wir eine Implementierung JPA / Hibernate verwenden.

18.7.1. Das Eclipse-Projekt

Derzeit sind folgende Projekte in Eclipse geladen:

  

Das Projekt [spring-webjson-server-jdbc-generic] basierte auf dem Projekt [spring-jdbc-generic-04], das die Schicht DAO / JDBC für den Zugriff auf SGBD und MySQL konfiguriert. Wir werden ein neues Projekt [spring-webjson-server-jpa-generic] erstellen, das sich wiederum auf das Projekt [spring-jpa-generic] stützt, welches die Zugriffsebene DAO / JPA / JDBC für den Zugriff auf SGBD und MySQL konfiguriert. Wir wissen, dass in beiden Fällen die Schicht [DAO] dieselbe Schnittstelle [IDao] implementiert. Der Code der Schicht [web] ändert sich daher nicht.

Wir können das Projekt [spring-webjson-server-jpa-generic] durch Kopieren und Einfügen aus dem Projekt [spring-webjson-server-jdbc-generic] erstellen:

  • in [1], einen speziell für das neue Projekt erstellten Ordner angeben;
  

Es sind drei Arten von Änderungen vorzunehmen. Die ersten befinden sich in der Maven-Konfigurationsdatei [pom.xml] des Projekts:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-webjson-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-webjson-server-jpa-generic</name>
    <description>démo spring mvc</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Web-Ebene -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Schicht [DAO] -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- Plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeile 5: Der Name des Maven-Artefakts wird geändert;
  • Zeilen 24–28: Die Abhängigkeit bezieht sich nun auf das Projekt [spring-jpa-generic] und nicht mehr auf [spring-jdbc-generic-04];

Letztendlich lauten die Abhängigkeiten wie folgt:

  

Damit sind alle Importprobleme behoben, die in den verschiedenen Klassen aufgetreten sind. Beispielsweise müssen die Entitäten [Produit, Categorie] nicht mehr im Projekt [spring-jdbc-generic-04], sondern im Projekt [spring-jpa-generic] gesucht werden. Es reicht aus, im Code einer Klasse „[Ctrl-Maj-O]“ einzufügen, um die Importe neu zu generieren.

Die letzte Änderung muss in der Konfigurationsdatei [AppConfig] vorgenommen werden:


package spring.webjson.server.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@ComponentScan(basePackages = { "spring.webjson.server.service" })
@Import({ spring.data.config.AppConfig.class, WebConfig.class })
public class AppConfig {

}
  • Zeile 9: Es wird nun die Konfiguration des Projekts [spring-jpa-generic] importiert und nicht mehr die des Projekts [spring-jdbc-generic-04];

Damit sind wir bereit. Wir starten den Webdienst mit der Konfiguration [spring-webjson-server-jpa-generic-hibernate-eclipselink]:

Anschließend führen wir die drei Tests des generischen Clients [spring-webjson-client-generic] aus:

  • in [1] den Test [JUnitTestCheckArguments] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], der Test [JUnitTestDao] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], der clientseitig ausgeführte Test [JUnitTestPushTheLimits] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], der serverseitig ausgeführte Test [JUnitTestPushTheLimits] (Ausführungskonfiguration [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.7.2. Warum funktioniert das?

Es funktioniert, und doch ist es erstaunlich, dass es funktioniert, wenn man sich den Code genauer ansieht. Zwar weisen die durch die Projekte [spring-jdbc-generic-04] und [spring-jpa-generic] implementierten Schichten [DAO] zwar dieselbe Schnittstelle auf, bearbeiten sie jedoch nicht dieselben Entitäten [Categorie] und [Produit]: Im Projekt [spring-jpa-generic] verfügen diese Entitäten über ein zusätzliches Feld [EntityType entityType], das zwei mögliche Werte annimmt:

  • EntityType.POJO: Die Entität ist ein normales Objekt, dessen Felder frei genutzt werden können;
  • EntityType.PROXY: Die Entität ist ein PROXY-Objekt, das von der Schicht [JPA] gerendert wird. In diesem Fall verhalten sich bestimmte Felder (genauer gesagt die Getter dieser Felder) nicht wie gewohnt, weshalb folgende Regeln festgelegt wurden:
    • Wenn [Categorie.entityType==EntityType.PROXY], dann darf die Methode [getProduits] nicht verwendet werden;
    • Wenn [Produit.entityType==EntityType.PROXY], dann darf die Methode [getCategorie] nicht verwendet werden;

Nun wurde das Projekt [spring-webjson-server-jdbc-generic] jedoch ohne Codeänderungen auf [spring-webjson-server-jpa-generic] portiert. Wie ist das möglich?

Sehen wir uns den Code der Methode [saveCategories] an:


    @RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
            // Der übermittelte Wert wird abgerufen
            String body = CharStreams.toString(request.getReader());
            // man deserialisiert ihn
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // Die Kategorien werden gespeichert
            categories = daoCategorie.saveEntities(categories);
            ...
}
  • Zeile 8: Aus einer Zeichenkette wird ein Objekt vom Typ `List<Categorie>` erstellt jSON:
    • Im übermittelten Wert haben die Produkte kein Feld [categorie]. Es ist in der Tat unnötig, dieses Feld zu übermitteln. Würde man es übermitteln, würde die Deserialisierung ein Objekt [Produit] erstellen, das ein Feld [categorie] enthält, das auf ein neu erstelltes Objekt [Categorie] verweist. Bei n Produkten würden somit n Objekte vom Typ [Categorie] erstellt, obwohl nur eines benötigt wird. Außerdem würde das Feld [categorie] der Produkte nicht auf das richtige Objekt [Categorie] verweisen, zu dem sie gehören. Daher haben die Produkte hier ein Feld [categorie==null];
    • in den Klassen [Categorie] und [Produit] ist das Feld [EntityType entityType] wie folgt definiert:

    protected EntityType entityType = EntityType.POJO;

Somit haben die durch die Serialisierung erzeugten Entitäten [Categorie] und [Produit] alle den Typ POJO.

  • Zeile 11: Die Kategorien werden persistiert. Hier sollte es nicht funktionieren. Denn wenn in der Implementierung JDBC das Feld [Produit.categorie] für die Persistenz nicht benötigt wird (es wird das Feld [idCategorie] verwendet), ist es für die Implementierung JPA jedoch unbedingt erforderlich. Dieses Feld muss auf eine Entität [Categorie] verweisen, hier ist jedoch der Wert null angegeben.

Sehen wir uns den Code der Methode [DaoCategorie.saveEntities] der Schicht [DAO / JPA] an:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // die Produkte, die eingefügt werden sollen, werden notiert
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // Man nutzt die Gelegenheit, um (falls erforderlich) die Beziehung „Produkt → Kategorie“ wiederherzustellen
                    produit.setCategorie(categorie);
                }
            }
        }
        // Die Kategorien und Produkte werden gespeichert
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // Das Feld [idCategorie] der eingefügten Produkte wird aktualisiert
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // Ergebnis
        return categories;
    }
  • Zeilen 13–14: Man sieht, dass die Verknüpfung [Produit] --> [Categorie] für die Entitäten POJO (Zeile 8) wiederhergestellt wird, was hier der Fall ist. Das erklärt, warum die Persistenz der Kategorien funktioniert hat. Dieser Fall ist auch unter anderen Umständen nützlich: Man kann nie sicher sein, dass der Benutzer die Produkte korrekt mit den Kategorien verknüpft hat. Also übernimmt man das für ihn;

Betrachten wir nun die Methode [ProduitController.saveProduits], die die Produkte persistiert:


@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
    ...
            // Der gesendete Wert wird abgerufen
            String body = CharStreams.toString(request.getReader());
            // Er wird deserialisiert
            ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
            List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
            });
            // Die Produkte werden gespeichert
            produits = daoProduit.saveEntities(produits);
            List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
            for (Produit produit : produits) {
                coreProduits.add(new CoreProduit(produit.getId()));
            }
            // die Antwort wird zurückgegeben
            return new Response<List<CoreProduit>>(0, null, coreProduits);
...
    }
  • Zeile 8: Aus dem übermittelten Wert wird ein Objekt vom Typ `List<Produkt>` rekonstruiert. Aus den zuvor erläuterten Gründen wird jedes Objekt `[Produit]` ein Feld enthalten:
    • [EntityType entityType] ist gleich [EntityType.POJO];
    • [Categorie categorie] ist gleich null;
  • Zeile 11: Die Persistenz der Produkte dürfte fehlschlagen. Denn bei JPA ist die Persistenz eines Produkts nur möglich, wenn sein Feld [categorie] auf eine Entität [Categorie] verweist;

Sehen wir uns den Code der Methode [DaoProduit.saveEntities] der Schicht [DAO / JPA] an:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // (falls erforderlich) wird die Verknüpfung zwischen einem Produkt und seiner Kategorie wiederhergestellt
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // Die Produkte werden weiterhin angeboten
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}
  • Zeilen 3–8: Für jedes [Produit] vom Typ POJO wird eine Verknüpfung zu einem Objekt [Categorie] mit dem richtigen Primärschlüssel und einer Version erstellt, die nicht null ist. Dies reicht aus, damit die Ebene JPA das Produkt korrekt beibehält;

Betrachten wir noch einen letzten Punkt. Die Objekte [Categorie] und [Produit] verfügen über ein zusätzliches Feld [EntityType entityType], das in jSON serialisiert wird, wenn diese Objekte an den Client gesendet werden. Dies lässt sich anhand von [Advanced Rest Client] überprüfen:

Auf der Client-Seite wurden die Entitäten [Categorie] und [Produit] ohne das Feld [EntityType entityType] definiert. Dies ist normal, da die Objekte [Categorie] und [Produit] ohne ihren Teil PROXY, [Categorie.produits] und [Produit.categorie] serialisiert werden. Auf der Client-Seite gibt es daher keine Entität mit der Bezeichnung PROXY. Es gibt nur normale Objekte.

Auf der Client-Seite wird die Zeichenfolge jSON [1] von der folgenden Methode [DaoCategorie.getAllShortEntities] empfangen:


    @Override
    public List<Categorie> getAllShortEntities() {
...
            // Filter jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // Alle Kategorien anzeigen
            Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
            // die Liste der Kategorien List<Kategorie>
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
...
}
  • Zeile 5: Der Mapper jSON des Objekts [RestTemplate] wird so konfiguriert, dass er die Filter jSON und [jsonFilterCategorie] desObjekts [Categorie] sowie den Filter [jsonFilterProduit] des Objekts [Produit] zu verarbeiten;
  • Zeile 7: Der gesendete Wert (hier gibt es keinen) und der empfangene Wert (List<Categorie>) werden mit diesem Mapper serialisiert bzw. deserialisiert. Es ist festzustellen, dass das Vorhandensein des Feldes [entityType] in der empfangenen Zeichenfolge jSON, obwohl dieses Feld in den Entitäten [Categorie] und [Produit] auf der Client-Seite nicht existiert, keinen Fehler auslöst. Es wird ignoriert. Hätte es einen Fehler ausgelöst, hätten wir die Filter auf der Client-Seite so angepasst, dass es ignoriert wird.

Um den Webservice / jSON / JPA / EclipseLink zu implementieren, muss lediglich die Implementierung von JPA geändert werden:

  

Hinweis: Drücken Sie Alt+F5 und generieren Sie anschließend alle Maven-Projekte neu.

Der Webdienst wird mit der Ausführungskonfiguration [spring-webjson-server-jpa-generic-hibernate-eclipselink] gestartet, die bereits für Hibernate verwendet wurde. Anschließend führen Sie die drei Tests des generischen Clients [spring-webjson-client-generic] aus:

  • in [1] den Test [JUnitTestCheckArguments];
  • in [2], der Test [JUnitTestDao];
  • in [3] der clientseitig ausgeführte Test [JUnitTestPushTheLimits] (Projekt [spring-webjson-client-generic]);
  • in [4] der serverseitig ausgeführte Test [JUnitTestPushTheLimits] (Ausführungskonfiguration [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.9. Implementierung des Web-Service / jSON / JPA / OpenJpa

Um den Webservice / jSON / JPA / OpenJpa zu implementieren, muss lediglich die Implementierung JPA geändert werden:

  

Hinweis: Drücken Sie Alt+F5 und generieren Sie anschließend alle Maven-Projekte neu.

Der Webdienst wird mit der Ausführungskonfiguration [spring-webjson-server-jpa-generic-openpa] gestartet:

Anschließend führen Sie die drei Tests des generischen Clients [spring-webjson-client-generic] aus:

  • in [1] den Test [JUnitTestCheckArguments] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], der Test [JUnitTestDao] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], der clientseitig ausgeführte Test [JUnitTestPushTheLimits] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], der serverseitig ausgeführte Test [JUnitTestPushTheLimits] (Ausführungskonfiguration [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);

Um die Tests zum Laufen zu bringen, mussten Änderungen an der Ebene DAO / JPA vorgenommen werden. Tatsächlich kam es aus unerklärlichen Gründen bei den Methoden [DaoCategorie.saveEntities] und [DaoProduit.saveEntities] beim Befüllen der Datenbank zu Fehlern, wobei angezeigt wurde, dass losgelöste Elemente nicht persistent gespeichert werden könnten. Ein losgelöstes Element ist ein Element, das entweder:

  • einen Primärschlüssel, der nicht null ist;
  • eine Version, die nicht null ist;

Keiner der beiden Fälle wurde überprüft. Da ich nicht wusste, wo ich suchen sollte, habe ich die zu speichernden Entitäten in eine völlig neue Liste dupliziert, und dort funktionierten die Tests. Diese Änderung hätte entweder erfolgen können:

  • in der Schicht [DAO / JPA];
  • in der Ebene [web], die die zu speichernden Entitäten erstellt;

Ich habe mich dafür entschieden, dies in der Schicht [DAO / JPA] vorzunehmen. Es kommt natürlich zu einem Leistungsverlust, der jedoch im Vergleich zu den Antwortzeiten von SGBD völlig vernachlässigbar ist. Die Änderungen lauten wie folgt:

In der Klasse [DaoCategorie] des Projekts [spring-jpa-generic]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // ***************************************************************************************
        // Die Liste der Kategorien wird geklont – manchmal erforderlich für OpenJpa – Fehler nicht berücksichtigt
        // ***************************************************************************************
        List<Categorie> categories2 = new ArrayList<Categorie>();
        for (Categorie categorie : categories) {
            // Kategorie
            Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
            EntityType categorieType = categorie.getEntityType();
            categorie2.setEntityType(categorieType);
            categories2.add(categorie2);
            // Produkte
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                List<Produit> produits2 = new ArrayList<Produit>();
                for (Produit produit : produits) {
                    Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(),
                            produit.getIdCategorie(), produit.getPrix(), produit.getDescription(), produit.getCategorie());
                    produit2.setEntityType(produit.getEntityType());
                    produits2.add(produit2);
                }
                categorie2.setProduits(produits2);
            }
        }
        // Die Produkte, die eingefügt werden sollen, werden notiert
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories2) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // Man nutzt die Gelegenheit, um (falls erforderlich) die Beziehung „Produkt → Kategorie“ wiederherzustellen
                    produit.setCategorie(categorie);
                }
            }
        }
        // Die Kategorien/Produkte werden gespeichert
        try {
            categoriesRepository.save(categories2);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // Das Feld [idCategorie] der eingefügten Produkte wird aktualisiert
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // Ergebnis
        return categories2;
    }
  • Zeilen 3–25: Die als Parameter übergebene Liste [categories] (Zeile 2) wird in die Liste [categories2] (Zeile 6) dupliziert. Diese Liste wird gespeichert und an den Aufrufer zurückgegeben (Zeile 52). Dies hat eine wichtige Konsequenz: Es wird eine andere Liste zurückgegeben als die, die als Parameter übergeben wurde. Daher muss an der Stelle, an der zuvor Folgendes geschrieben werden konnte:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// Auswertung von [categories]

muss nun geschrieben werden:


List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// Auswertung von [categories]

In der Klasse [DaoProduit] des Projekts [spring-jpa-generic] wird die Methode [saveEntities] in ähnlicher Weise geändert:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // ***************************************************************************************
        // Die Produktliste wird geklont – manchmal erforderlich für OpenJpa – Fehler nicht berücksichtigt
        // ***************************************************************************************
        List<Produit> produits2 = new ArrayList<Produit>();
        for (Produit produit : entities) {
            Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(), produit.getIdCategorie(),
                    produit.getPrix(), produit.getDescription(), produit.getCategorie());
            produit2.setEntityType(produit.getEntityType());
            produits2.add(produit2);
        }

        // Die Verknüpfung zwischen einem Produkt und seiner Kategorie wird (falls erforderlich) wiederhergestellt
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // Die Produkte werden gespeichert
        try {
            return Lists.newArrayList(produitsRepository.save(produits2));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}

Zur Implementierung des Web-Service / jSON / JPA / EclipseLink / PostgresQL müssen folgende Komponenten installiert werden:

  • das Projekt [postgresql-config-jdbc] zur Konfiguration der Schicht JDBC von PostgreSQL;
  • das Projekt [postresql-config-jpa-eclipselink] zur Konfiguration der Ebene JPA von PostgreSQL;
  • drücken Sie Alt-F5 und regenerieren Sie alle Maven-Projekte;
  

Starten Sie SGBD und PostgreSQL und starten Sie den Webdienst mit der zuvor bereits verwendeten Ausführungskonfiguration [spring-webjson-server-jpa-generic-hibernate-eclipselink]. Anschließend führen wir die drei Tests des generischen Clients [spring-webjson-client-generic] aus:

  • in [1] den Test [JUnitTestCheckArguments] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • in [2], der Test [JUnitTestDao] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestDao]);
  • in [3], der clientseitig ausgeführte Test [JUnitTestPushTheLimits] (Ausführungskonfiguration [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • in [4], der serverseitig ausgeführte Test [JUnitTestPushTheLimits] (Ausführungskonfiguration [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);