Skip to content

18. Klient zaprogramowany dla serwisu internetowego / jSON

Teraz, gdy baza danych [dbproduitscategories] jest dostępna w sieci, napiszemy aplikację, która z niej korzysta. Uzyskamy wówczas następującą architekturę klient-serwer:

Aplikacja kliencka będzie składać się z trzech warstw:

  • warstwa [Client HTTP] [3] do komunikacji z aplikacją internetową / jSON, która udostępnia bazę danych;
  • warstwa [DAO] [2], która będzie miała ten sam interfejs co warstwa [DAO] [4];
  • warstwa testowa JUnit [1] służąca do sprawdzania, czy klient i serwer działają prawidłowo;

18.1. Projekt Eclipse

Projekt Eclipse klienta wygląda następująco:

 
  • pakiet [spring.webjson.client.config] zawiera konfigurację Spring dla warstwy [DAO];
  • pakiet [spring.webjson.client.dao] zawiera implementację warstwy [DAO];
  • pakiet [spring.webjson.client.entities] zawiera obiekty wymieniane z serwisem internetowym / jSON. Znamy je wszystkie;
  • pakiet [spring.webjson.client.infrastructure] zawiera klasy wyjątków używane w projekcie. Znamy je wszystkie;

18.2. Konfiguracja projektu w Maven

Projekt jest projektem Maven skonfigurowanym przez następujący plik [pom.xml]:


<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>
        <!-- biblioteka jSON wykorzystywana przez Spring -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- komponent używany przez Spring 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>
        <!-- biblioteka logów -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Test Spring Boot -->
        <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>
    <!-- wtyczki -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersze 16–20: nadrzędny projekt Maven [spring-boot-starter-parent], który pozwala nam zdefiniować pewną liczbę zależności bez podawania ich wersji, ponieważ jest ona określona w projekcie nadrzędnym;
  • wiersze 24–27: chociaż nie tworzymy aplikacji internetowej, potrzebujemy zależności [spring-web], która zawiera klasę [RestTemplate], umożliwiającą łatwą integrację z aplikacją internetową / jSON;
  • wiersze 29–36: biblioteka jSON;
  • wiersze 38–41: zależność, która pozwoli nam przypisać klasę timeout do żądań HTTP wysyłanych przez klienta. timeout to maksymalny czas oczekiwania na odpowiedź serwera. Po upływie tego czasu klient zgłasza błąd timeout, generując wyjątek;
  • wiersze 43–48: biblioteka Google Guava;
  • wiersze 50–53: biblioteka logów;
  • wiersze 54–64: zależność testowa o numerze JUnit. Zawiera ona w szczególności bibliotekę JUnit 4, niezbędną do przeprowadzenia testów. Zależności te mają atrybut [<scope>test</scope>], wskazujący, że są one potrzebne wyłącznie na etapie testów. Nie są one dołączane do ostatecznego archiwum projektu;

18.3. Konfiguracja Spring

  

Klasa [AppConfig] odpowiada za konfigurację Spring klienta HTTP. Jej kod wygląda następująco:


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 {

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

    // filtry 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) {
        // tworzenie komponentu RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // konwerter jSON
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        // limit czasu wymiany danych
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // wynik
        return restTemplate;
    }

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

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
}
  • wiersz 20: klasa ta jest klasą konfiguracyjną Spring;
  • wiersz 21: pozostałe komponenty Springa należy szukać w pakiecie [spring.webjson.client.dao];
  • wiersz 25: ustawia się timeout na jedną sekundę (1000 ms);
  • wiersze 88–91: bean zwracający tę wartość;
  • wiersz 26: URL usługi internetowej / jSON;
  • wiersze 93–96: bean zwracający tę wartość;
  • wiersze 72–86: konfiguracja klasy [RestTemplate], która zapewnia komunikację z serwisem internetowym / jSON. Gdy nie ma potrzeby jej konfigurowania, można ją wykorzystać w kodzie poprzez proste użycie [new RestTemplate()]. W tym przypadku chcemy ustawić timeout dla komunikacji z serwisem internetowym / jSON. Bean [timeout] z wiersza 89 jest przekazywany jako parametr do metody [restTemplate] z wiersza 73;
  • wiersz 75: komponent [HttpComponentsClientHttpRequestFactory] umożliwia nam ustalenie timeout dla wymiany danych (wiersze 82–83);
  • wiersz 76: klasa [RestTemplate] jest tworzona przy użyciu tego komponentu. Ponieważ opiera się ona na tym komponencie w celu komunikacji z usługą internetową / jSON, wymiany danych będą rzeczywiście podlegać timeout;
  • wiersze 78–80: do klasy [RestTemplate] przypisano konwerter jSON. Omówiliśmy to już podczas analizy serwisu internetowego. Klient i serwer wymieniają między sobą wiersze tekstu. Konwerter zajmuje się serializacją obiektu do postaci tekstowej oraz, odwrotnie, deserializacją tekstu do postaci obiektu. Z klasą [RestTemplate] może być powiązanych kilka konwerterów, a wybór konkretnego konwertera w danym momencie zależy od nagłówków HTTP wysyłanych przez serwer. W tym przypadku mamy tylko jeden konwerter jSON, ponieważ wymieniane wiersze tekstu są typu jSON;
  • wiersze 82–83: ustalamy nagłówki timeout dla wymiany danych;
  • wiersze 28–70: definiują filtry jSON. Są to te same filtry, co na serwerze, przedstawione w paragrafie 17.3.2.1;
  • wiersze 29–32: bean [jsonMapper] jest mapperem jSON konwertera [MappingJackson2HttpMessageConverter], który powiązaliśmy z klasą [RestTemplate]. Potrzebujemy go w definicji filtrów jSON;
  • wiersze 34–41: bean definiujący filtr jSON [catégorie sans ses produits]. Metoda [jsonMapperShortCategorie] przyjmuje jako parametr bean [restTemplate] zdefiniowany w wierszu 73;
  • wiersz 37: wywołuje się metodę [jsonMapper] z wiersza 30 w celu pobrania mapera jSON;
  • wiersze 38–39: ustawia się filtr, aby uzyskać kategorię bez jej produktów;
  • wiersz 40: zwracamy mapper jSON w takiej konfiguracji;
  • wiersze 42–51: filtr jSON, aby uzyskać kategorię wraz z jej produktami;
  • wiersze 53–60: filtr jSON, aby uzyskać produkt bez kategorii;
  • wiersze 62–70: filtr jSON, aby uzyskać produkt wraz z kategorią;

Wszystkie te obiekty typu „bean” będą dostępne dla kodów warstwy [DAO], a także dla testów JUnit.

18.4. Wdrożenie klienta HTTP

Powyżej przedstawiono warstwę [Client HTTP], która komunikuje się z właśnie zbudowaną przez nas usługą internetową. Przyjrzyjmy się jej teraz.

  

Klasa [Client] realizuje komunikację z usługą internetową / jSON. Implementuje ona następujący interfejs [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);
}

Interfejs posiada tylko jedną metodę [getResponse]:

  • wiersz 6: metoda [getResponse] jest metodą generyczną, której parametry określają dwa typy:
    • [T1]: jest to typ odpowiedzi oczekiwanej od serwera w [Response<T1>], na przykład [List<Categorie>],
    • [T2]: jest to typ parametru jSON wysyłanego przez operacje POST, na przykład [List<Produit>];
  • wiersz 6: metoda [getResponse] zwraca wynik typu T1, na przykład [List<Categorie>];
  • wiersz 6: parametry metody [getResponse] są następujące:
    • [String url]: URL, który ma zostać zapytany;
    • [HttpMethod method]: metoda HTTP zapytania, GET lub POST w zależności od sytuacji,
    • [int errStatus]: kod błędu do wykorzystania w klasie [DaoException], jeśli wystąpi błąd podczas komunikacji z serwerem,
    • [T2 body]: wartość, którą należy przesłać, jeśli istnieje POST;

Klasa [Client] implementuje interfejs [IClient] w następujący sposób:


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 {

    // wstrzyknięcia
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

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

    // ogólne zapytanie
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
    }

    // lista komunikatów o błędach związanych z wyjątkiem
    protected List<String> getMessagesForException(Exception exception) {
    ...
    }
}
  • wiersz 18: klasa [Client] jest komponentem Springa, który może być wstrzykiwany do innych komponentów Springa;
  • wiersze 22–23: wstrzyknięcie fasoli [RestTemplate] zdefiniowanej w [AppConfig] (patrz punkt 18.3), która zapewnia komunikację z serwerem;
  • wiersze 24–25: wstrzyknięcie komponentu URL usługi internetowej / jSON zdefiniowanego w [AppConfig] (patrz punkt 18.3);
  • wiersze 37–39: prywatna metoda [getMessagesForException] jest metodą pomocniczą służącą do uzyskania listy komunikatów o błędach zawartych w wyjątku. Spotkaliśmy się z nią już kilkakrotnie;

Kontynuujmy:


    // zapytanie ogólne
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
        // odpowiedź serwera
        ResponseEntity<Response<T1>> response;
        try {
            // przygotowuje się żądanie
            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);
            }
            // wykonywanie zapytania
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // kapsułkowanie wyjątku
            throw new DaoException(errStatus, e, simpleClassName);
        }
        ...
}
  • wiersz 18: instrukcja, która wysyła zapytanie do serwera i odbiera jego odpowiedź. Komponent [RestTemplate] oferuje wiele metod komunikacji z serwerem, ale tylko metoda [exchange] akceptuje parametry generyczne. Z tego powodu została ona wybrana. Drugi parametr określa typ oczekiwanej odpowiedzi. Pierwszym parametrem jest żądanie typu [RequestEntity] (wiersz 8). Wynik metody [exchange] ma typ [ResponseEntity<Response<T1>>] (wiersz 5). Typ [ResponseEntity] zawiera pełną odpowiedź serwera, nagłówki HTTP oraz dokument wysłany przez serwer. Podobnie typ [RequestEntity] zawiera całe żądanie klienta, w tym nagłówki HTTP oraz ewentualną wartość przesłaną metodą POST;
  • wiersze 8–16: musimy skonstruować żądanie typu [RequestEntity]. Różni się ono w zależności od tego, czy do wysłania żądania używamy typu GET, czy POST;
  • wiersz 10: zapytanie dla GET. Klasa [RequestEntity] udostępnia metody statyczne do tworzenia zapytań GET, POST, HEAD,... Metoda [RequestEntity.get] umożliwia utworzenie zapytania GET poprzez łańcuchowanie różnych metod, które je tworzą:
    • metoda [RequestEntity.get] przyjmuje jako parametr docelowy obiekt URL w postaci instancji URI,
    • metoda [accept] pozwala zdefiniować elementy nagłówka HTTP [Accept]. W tym miejscu wskazujemy, że akceptujemy typ [application/json], który wyśle serwer;
    • metoda [build] wykorzystuje te różne informacje do skonstruowania typu [RequestEntity] żądania;
  • wiersz 14: żądanie dotyczące typu POST. Metoda [RequestEntity.post] pozwala utworzyć żądanie typu POST poprzez połączenie różnych metod, które je tworzą:
    • metoda [RequestEntity.post] przyjmuje jako parametr docelowy URL w postaci instancji URI,
    • metoda [header] definiuje nagłówek HTTP. W tym przypadku wysyłamy do serwera nagłówek [Content-Type: application/json], aby poinformować go, że wysyłana wartość dotrze do niego w postaci ciągu znaków jSON;
    • metoda [accept] pozwala wskazać, że akceptujemy typ [application/json], który wyśle serwer;
    • metoda [body] ustala wartość wysyłaną metodą POST. Jest to czwarty parametr metody generycznej [getResponse] (wiersz 1);
  • wiersze 20–23: jeśli wystąpi błąd komunikacji z serwerem, generowany jest wyjątek typu [DaoException] z kodem błędu równym parametrowi [errStatus], przekazanym jako trzeci parametr metody generycznej [getResponse] (wiersz 3);

Metoda [getResponse] przebiega dalej w następujący sposób:


// żądanie ogólne
    @Override
    public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
    ...
        // pobieranie treści odpowiedzi
        Response<T1> entity = response.getBody();
        int status = entity.getStatus();
        // czy wystąpiły błędy po stronie serwera?
        if (status != 0) {
            // tworzymy wyjątek
            throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
        } else {
            // w porządku
            return entity.getBody();
        }
    }
  • wiersz 4: otrzymaliśmy odpowiedź z serwera. Jest to typ [ResponseEntity<Response<T1>>] (wiersz 5 poprzedniego analizowanego kodu), gdzie klasa [Response] jest klasą już wykorzystaną po stronie serwera:

package spring.webjson.client.dao;

public class Response<T> {

    // ----------------- właściwości
    // status operacji
    private int status;
    // ewentualny wyjątek
    private String exception;
    // treść odpowiedzi
    private T body;

    // konstruktory
    public Response() {

    }

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

    // metody pobierające i ustawiające
...
}

Wróćmy do metody [getResponse]:

  • wiersz 6: pobieramy dokument typu [Response<T1>] zawarty w odpowiedzi. Ten typ posiada pola [int status, String exception, T1 body];
  • wiersz 7: pobieramy z odpowiedzi [status], który jest kodem błędu;
  • wiersze 9–12: jeśli wystąpi błąd, to generujemy wyjątek zawierający dwie informacje [status, exception] z odpowiedzi serwera;
  • wiersz 14: w przeciwnym razie zwracamy typ [T1] zawarty w odpowiedzi typu [Response<T1>];

Klasa [Client] ma charakter ogólny. Można ją wykorzystać dla dowolnego klienta internetowego / jSON.

18.5. Implementacja warstwy [Dao]

  

18.5.1. Klasa [AbstractDao]

Warstwa po stronie klienta [DAO] posiada ten sam interfejs, co warstwa po stronie serwera [DAO] (patrz punkt 4.7):


package spring.webjson.client.dao;

import java.util.List;

import spring.webjson.client.entities.AbstractCoreEntity;

public interface IDao<T extends AbstractCoreEntity> {

    // lista wszystkich encji T
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // poszczególnych encji – wersja skrócona
    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);

    // poszczególnych encji – wersja długa
    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);

    // aktualizacja wielu elementów
    public List<T> saveEntities(Iterable<T> entities);

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

    // usunięcie wszystkich elementów
    public void deleteAllEntities();

    // usunięcie wielu elementów
    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);
}

Klasa [AbstractDao] implementuje interfejs [IDao]. Jest to klasa analogiczna do klasy o tej samej nazwie po stronie serwera (patrz punkt 4.8). Służy ona jako klasa nadrzędna dla klas [DaoCategorie] i [DaoProduit]. Nie jest ona identyczna z dwóch powodów:

  • po stronie serwera klasa [AbstractDao] obsługuje informację:

    // wstawki
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

których tutaj nie potrzebujemy.

  • Po stronie serwera klasa [AbstractDao] wykorzystuje adnotacje [@Transactional] do enkapsulacji każdej metody w transakcji. Po stronie klienta nie ma bazy danych do zarządzania. Adnotacja ta zatem znika;

Klasa [AbstractDao] ogranicza się do sprawdzania poprawności parametrów wywołania metod interfejsu [IDao] przed przekazaniem wywołania do klas potomnych:


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> {

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

    @Override
    public List<T1> getShortEntitiesById(Iterable<Long> ids) {
        // poprawność argumentu
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // wynik
        return getShortEntitiesById(Lists.newArrayList(ids));
    }

    @Override
    public List<T1> getShortEntitiesById(Long... ids) {
        // poprawność argumentu
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // wynik
        return getShortEntitiesById(Lists.newArrayList(ids));
    }
...
    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
        ...
    }

    // metody prywatne ----------------------------------------------
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
        // elementy o wartości null?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
                    simpleClassName);
        }
        // puste elementy?
        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>();
            }
        }
        // wynik domyślny
        return null;
    }

    @SuppressWarnings("unchecked")
    private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
        // elementy null?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
        }
        // elementy puste?
        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>();
            }
        }
        // wynik domyślny
        return null;
    }

    // metody chronione ----------------------------------------------
    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. Klasa [DaoCategorie]

  

Klasa [DaoCategorie] ma następującą postać:


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;

...
}
  • wiersz 19: klasa [DaoClient] jest komponentem Springa, do którego można wstrzykiwać inne komponenty Springa;
  • wiersz 20: klasa [DaoClient] rozszerza klasę [AbstractDao<Categorie>], którą właśnie omówiliśmy, i w związku z tym implementuje interfejs [IDao<Categorie>];
  • wiersze 22–23: wstrzykujemy kontekst Springa, aby uzyskać dostęp do jego beanów;
  • wiersze 24–25: wstrzykujemy klienta HTTP, którego właśnie utworzyliśmy;

Implementacje poszczególnych metod interfejsu [DaoCategorie] są zgodne z tym samym schematem. Przedstawimy trzy metody: jedną opartą na operacji [GET], a dwie pozostałe na operacji [POST].

18.5.2.1. Metoda [getAllLongEntities]

Metoda [getAllLongEntities] generuje pełną wersję wszystkich kategorii w bazie danych:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtry jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // pobierz wszystkie kategorie
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // lista kategorii List<Kategoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // ponowne utworzenie powiązania produkt --> kategoria
            return linkCategorieWithProduits(categories);
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(233, e2, simpleClassName);
        }
}
  • wiersz 2: metoda zwraca listę kategorii w ich pełnych wersjach;
  • wiersz 5: mapper jSON, który umożliwi serializację wysłanej wartości (nie ma żadnej) oraz deserializację odpowiedzi zwracanej przez klasę [Client] (kategorie w pełnych wersjach);
  • wiersz 7: wywoływana jest metoda [getResponse] klasy [Client]. To właśnie ta metoda zapewnia komunikację z serwisem internetowym / jSON. Jej parametry są następujące:
    • URL serwisu, do którego kierowane jest zapytanie [/getAllLongCategories];
    • metoda [GET], która ma zostać zastosowana;
    • kod błędu, który należy zastosować w przypadku wystąpienia błędu (232);
    • wartość wysyłana. W tym przypadku nie ma żadnej;
  • wiersz 7: w wyrażeniu [client.<List<Categorie>, Void>] określa się rzeczywiste parametry typów generycznych [T1, T2] metody [getResponse]. Przypominamy, że [T1] jest typem oczekiwanej odpowiedzi, a [T2] typem wartości przekazanej. W tym przypadku oczekuje się wyniku typu [List<Categorie>] i nie ma wartości wysyłanej typu [Void];
  • wiersz 7: wynik zwracany przez metodę [getResponse] jest umieszczany w obiekcie typu [Object]. Jest to nieco dziwne, ponieważ oczekujemy typu [List<Categorie>]. Wynika to z faktu, że metoda [getResponse], działająca z typami generycznymi [T1, T2], zawsze zwraca typ [java.util.LinkedHashMap], który należy następnie przetworzyć, aby uzyskać właściwy typ;
  • wiersz 9: zwracamy listę kategorii. W tym celu serializujemy obiekt [map] [mapper.writeValueAsString(map)] do ciągu znaków jSON, który następnie reserializujemy do typu [List<Categorie>];
  • wiersz 13: otrzymano listę kategorii, z których niektóre mogą zawierać produkty. Otrzymujemy skróconą wersję tych produktów. Dlatego po deserializacji utworzone obiekty [Produit] posiadają pole [categorie==null]. Metoda [linkCategorieWithProduits] odtwarza powiązanie między obiektem [Produit] a jego obiektem [Categorie];
  • wiersze 14–15: zatrzymuje się wyjątek typu [DaoException], który mogła wygenerować metoda [getResponse], aby natychmiast go ponownie wygenerować. To dziwne zachowanie wynika z faktu, że gdyby tego nie zrobiono, wyjątek typu [DaoException] zostałby zatrzymany przez wiersze 16–18, a tego nie chcemy;
  • wiersze 16–18: zatrzymujemy wszystkie inne wyjątki, aby zamknąć je w typie [DaoException]. Przypominamy, że warstwa [DAO] powinna generować wyłącznie ten typ wyjątku;

Metoda [linkCategorieWithProduits], która odtwarza powiązania między encjami [Produit] a encjami [Categorie], wygląda następująco:


    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. Zarządzanie filtrami jSON

Wróćmy do zarządzania filtrami jSON w poprzedniej metodzie [getAllLongEntities]:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            // filtry jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // pobierz wszystkie kategorie
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // lista kategorii List<Kategoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });

  • wiersz 5: z kontekstu Spring pobieramy mapper jSON, który obsługuje długie wersje kategorii. Przyjrzyjmy się definicji tego mappera w konfiguracji Spring [AppConfig]:

// filtry 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) {
    ...
    }

  • bean [jsonMapperLongCategorie] wywoływany przez metodę [getAlllongEntities] to bean z wierszy 7–15;
  • wiersz 10: mapper jest dostarczany przez metodę [jsonMapper] z wierszy 2–5. Widać, że ten mapper jSON należy do obiektu [RestTemplate], który zarządza wymianą danych HTTP między klientem a serwerem. To właśnie ten mapper jest domyślnie używany do:
    • serializację wartości wysyłanej do serwera;
    • deserializację odpowiedzi zwróconej przez serwer;

Wróćmy do kodu obiektu [getAllLongEntities]:


            // filtry jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // pobierz wszystkie kategorie
            Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
            // lista kategorii List<Kategoria>
            List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<Categorie>>() {
                    });
            // ponownie tworzymy powiązanie produkt –> kategoria
return linkCategorieWithProduits(categories);
  • wiersz 2: pobieramy mapper [jsonMapperLongCategorie] z kontekstu Spring;
  • wiersz 4: wykonywana jest metoda [getResponse]. Następuje wówczas:
    • automatyczna serializacja wartości przesłanej w żądaniu (w tym przypadku nie ma żadnej);
    • automatyczna deseryalizacja otrzymanej odpowiedzi, w tym przypadku typu List<Categorie>. Wynika to z faktu, że entyteta [Categorie] posiada filtr jSON [jsonFilterCategorie], który należało obsłużyć. To jest powód linii 2;
  • wiersz 6: wynik przechodzi drugą serializację/deserializację przy użyciu tego samego mapera, aby odzyskać typ List<Categorie>. W wierszu 4 typ zwracany przez [getResponse] to typ [Object];

W kolejnych metodach należy pamiętać, że mapper jSON, o który zwrócono się do kontekstu Spring, jest używany zarówno dla wartości wysyłanej (serializacja), jak i dla wartości odbieranej (deserializacja). Jeśli jedna lub obie wartości mają filtr jSON, należy je skonfigurować. Mapper może zatem mieć skonfigurowane maksymalnie dwa filtry. W poniższym przykładzie sytuacja taka nigdy nie występuje. Albo wartość wysyłana nie ma filtra (List<Long>, List<String>), albo to wartość odbierana nie ma filtra (List<CoreCategorie>, List<CoreProduit>). Entności posiadające filtr jSON to wyłącznie [Categorie] i [Produit].

18.5.2.3. Metoda [getShortEntitiesById]

Metoda [getShortEntitiesById] zwraca skrócone wersje kategorii, których klucze główne otrzymuje jako parametry:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            // filtry jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // pobierz kategorię bez jej produktów
            Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
            // kategoria
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(223, e2, simpleClassName);
        }
}
  • wiersz 5: mapper jSON, który umożliwi serializację wysyłanej wartości (listy kluczy głównych) oraz deserializację odpowiedzi zwracanej przez klasę [Client] (kategorie w ich skróconych wersjach). Wybrany filtr nie będzie miał żadnego wpływu na wysyłaną wartość, ponieważ dla elementów wysyłanej listy nie ma filtra;
  • wiersz 7: wywoływana jest metoda [getResponse] klasy nadrzędnej. To właśnie ta metoda zapewnia komunikację z serwisem internetowym / jSON. Jej parametry są następujące:
    • URL serwisu, do którego kierowane jest zapytanie [/getShortCategoriesById];
    • metoda [POST], która ma zostać użyta;
    • kod błędu, który należy zastosować w przypadku wystąpienia błędu (204);
    • wartość przesłana. W tym przypadku jest to lista kluczy głównych;
  • wiersz 7: w wyrażeniu [client.<List<Categorie>, List<Long>>] określa się rzeczywiste parametry typów generycznych [T1, T2] metody [getResponse]. Przypominamy, że [T1] jest typem oczekiwanej odpowiedzi, a [T2] typem wartości przekazywanej. W tym przypadku oczekuje się wyniku typu [List<Categorie>], a wartością wysyłaną jest lista kluczy głównych typu [List<Long>];
  • wiersz 7: wynik zwracany przez metodę [getResponse] jest umieszczany w obiekcie typu [Object];
  • wiersz 9: zwracana jest lista kategorii. W tym celu obiekt [map] [mapper.writeValueAsString(map)] jest serializowany do ciągu znaków jSON, który następnie jest ponownie serializowany do typu [List<Categorie>];

18.5.2.4. Metoda [saveEntities]

Metoda [saveEntities] zapisuje kategorie w bazie danych. Jej kod wygląda następująco:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // filtry jSON
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            // dodaj kategorie
            Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
                    entities);
            // lista dodanych głównych kategorii
            List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
                    new TypeReference<List<CoreCategorie>>() {
                    });
            // aktualizujemy kategorie na podstawie otrzymanych informacji
            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);
        }
    }
  • wiersz 2: metoda [saveEntities] służy do zapisywania w bazie danych kategorii przekazanych jako parametry. Dodaje do tych kategorii ich klucze główne. Jeśli kategorie są przekazywane wraz z produktami, te również są zapisywane;
  • wiersz 5: mapper jSON, który umożliwi serializację wysyłanej wartości (lista kategorii w pełnych wersjach) oraz deserializację odpowiedzi zwracanej przez klasę [Client] (obiekty [CoreCategorie]). Wybrany filtr nie będzie miał żadnego wpływu na wynik, ponieważ elementy listy otrzymanej w odpowiedzi nie są filtrowane;
  • wiersz 7: wywołuje się metodę [getResponse] klasy nadrzędnej w celu przeprowadzenia komunikacji z serwisem internetowym / jSON;
    • pierwszym parametrem jest URL [/saveCategories];
    • drugim parametrem jest metoda HTTP, którą należy zastosować, w tym przypadku [POST];
    • trzecim parametrem jest kod błędu, który należy zastosować w przypadku wystąpienia błędu (200);
    • ostatnim parametrem jest wartość wysyłana, w tym przypadku lista kategorii do zapisania;
  • wiersz 7: parametry ogólne [T1, T2] metody [getResponse] mają tutaj wartość [List<CoreCategorie>, List<Categorie>]. Pierwszy typ odpowiada oczekiwanej odpowiedzi, a drugi – typowi wysłanej wartości;
  • wiersz 7: otrzymaną odpowiedź umieszczamy w typie [Object];
  • wiersz 9: odtwarzamy odpowiedź typu [List<CoreCategorie>]. Odpowiedź, którą należy zwrócić, jest typu [List<Categorie>] (wiersz 2), a nie [List<CoreCategorie>]. Otrzymana odpowiedź to lista kluczy głównych zachowanych kategorii i produktów;
  • wiersze 14–28: otrzymane klucze główne są przypisywane do kategorii i produktów (wiersze 17, 23, 24). Ponadto odtwarza się powiązania [Produit] --> [Categorie] (wiersze 24–25);

Wszystkie pozostałe metody przebiegają według tego samego schematu.

18.6. Test JUnit

Wróćmy do budowanej obecnie architektury klient-serwer:

Stworzyliśmy warstwę [DAO] [2] z tym samym interfejsem, co warstwa [DAO] [4]. Aby przetestować warstwę [DAO] [2], można zatem wykorzystać testy JUnit, które posłużyły do przetestowania warstwy [DAO] [4]:

  

Te trzy testy są uruchamiane na podstawie następujących konfiguracji uruchomieniowych:

 

Wyniki tych trzech testów są następujące:

  • w [1] – test [JUnitTestCheckArguments];
  • w [2] – test [JUnitTestDao];
  • w [3] – test [JUnitTestPushTheLimits] przeprowadzony po stronie klienta (projekt [spring-webjson-client-generic]);
  • w [3] – test [JUnitTestPushTheLimits] przeprowadzony po stronie serwera (projekt [spring-jdbc-generic-04]). Można zauważyć, że warstwa sieciowa powoduje bardzo niewielkie spowolnienie w porównaniu z tym spowodowanym przez dostęp do SGBD;

18.7. Implementacja serwisu internetowego / jSON / JPA / Hibernate

Skupiamy się teraz na następującej architekturze:

Modyfikacja znajduje się w [1]. Warstwa serwera [DAO] opiera się na implementacji JPA. Najpierw wykorzystamy implementację JPA / Hibernate.

18.7.1. Projekt Eclipse

Na razie w Eclipse załadowano następujące projekty:

  

Projekt [spring-webjson-server-jdbc-generic] opierał się na projekcie [spring-jdbc-generic-04], który konfiguruje warstwę DAO / JDBC, umożliwiającą dostęp do SGBD i MySQL. Utworzymy nowy projekt [spring-webjson-server-jpa-generic], który będzie oparty na projekcie [spring-jpa-generic], konfigurującym warstwę DAO / JPA / JDBC, umożliwiającą dostęp do SGBD i MySQL. Wiemy, że w obu przypadkach warstwa [DAO] implementuje ten sam interfejs [IDao]. Kod warstwy [web] pozostaje zatem niezmieniony.

Możemy utworzyć projekt [spring-webjson-server-jpa-generic] poprzez skopiowanie i wklejenie projektu [spring-webjson-server-jdbc-generic]:

  • na [1], wskazując folder utworzony specjalnie dla nowego projektu;
  

Należy wprowadzić trzy rodzaje zmian. Pierwsze z nich dotyczą pliku [pom.xml] zawierającego konfigurację Maven projektu:


<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>
        <!-- warstwa internetowa -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- warstwa [DAO] -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- wtyczki -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersz 5: zmieniamy nazwę artefaktu Maven;
  • wiersze 24–28: zależność odnosi się teraz do projektu [spring-jpa-generic], a nie do [spring-jdbc-generic-04];

Ostatecznie zależności są następujące:

  

Po wykonaniu tej czynności rozwiązujemy wszystkie problemy z importami, które pojawiły się w różnych klasach. Na przykład elementy [Produit, Categorie] nie są już szukane w projekcie [spring-jdbc-generic-04], ale w projekcie [spring-jpa-generic]. Wystarczy wpisać [Ctrl-Maj-O] w kodzie klasy, aby ponownie wygenerować importy.

Ostatnią zmianę należy wprowadzić w pliku konfiguracyjnym [AppConfig]:


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 {

}
  • wiersz 9: teraz importujemy konfigurację projektu [spring-jpa-generic], a nie już projektu [spring-jdbc-generic-04];

Po wykonaniu tych czynności wszystko jest gotowe. Uruchamiamy usługę internetową z konfiguracją [spring-webjson-server-jpa-generic-hibernate-eclipselink]:

Następnie uruchamiamy trzy testy klienta generycznego [spring-webjson-client-generic]:

  • w [1] – test [JUnitTestCheckArguments] (konfiguracja uruchomienia [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • w [2] – test [JUnitTestDao] (konfiguracja wykonania [spring-webjson-client-generic-JUnitTestDao]);
  • w [3] – test [JUnitTestPushTheLimits] przeprowadzony po stronie klienta (konfiguracja wykonania [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • w [4] test [JUnitTestPushTheLimits] uruchomiony po stronie serwera (konfiguracja wykonania [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.7.2. Dlaczego to działa?

To działa, a jednak gdy przyjrzymy się uważnie kodowi, zaskakujące jest, że w ogóle działa. Chociaż warstwy [DAO] zaimplementowane przez projekty [spring-jdbc-generic-04] i [spring-jpa-generic] mają wprawdzie ten sam interfejs, nie obsługują one tych samych encji [Categorie] i [Produit]: w projekcie [spring-jpa-generic] encje te posiadają dodatkowe pole [EntityType entityType], które może przyjmować dwie wartości:

  • EntityType.POJO: jednostka jest zwykłym obiektem, którego wszystkie pola można swobodnie wykorzystywać;
  • EntityType.PROXY: encja jest obiektem PROXY renderowanym przez warstwę [JPA]. W tym przypadku niektóre pola (a właściwie metody pobierające tych pól) nie zachowują się w zwykły sposób, dlatego ustalono następujące zasady:
    • jeśli [Categorie.entityType==EntityType.PROXY], to nie należy używać metody [getProduits];
    • jeśli [Produit.entityType==EntityType.PROXY], to nie należy używać metody [getCategorie];

Jednak właśnie przeniesiono projekt [spring-webjson-server-jdbc-generic] do [spring-webjson-server-jpa-generic] bez wprowadzania zmian w kodzie. Jak to możliwe?

Przyjrzyjmy się kodowi metody [saveCategories]:


    @RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
            // pobieramy wartość przesłaną w żądaniu POST
            String body = CharStreams.toString(request.getReader());
            // deserializujemy ją
            ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // zapisujemy kategorie
            categories = daoCategorie.saveEntities(categories);
            ...
}
  • wiersz 8: obiekt List<Categorie> jest tworzony na podstawie ciągu znaków jSON:
    • w wartości wysyłanej produkty nie mają pola [categorie]. Wysyłanie tego pola jest bowiem zbędne. Gdybyśmy je przesłali, deserializacja utworzyłaby obiekt [Produit] posiadający pole [categorie] wskazujące na nowo utworzony obiekt [Categorie]. W przypadku n produktów powstałoby w ten sposób n obiektów [Categorie], podczas gdy potrzebny jest tylko jeden. Ponadto pole [categorie] produktów nie wskazywałoby na właściwy obiekt [Categorie], do którego należą. Dlatego w tym przypadku produkty mają pole [categorie==null];
    • w klasach [Categorie] i [Produit] pole [EntityType entityType] jest zdefiniowane w następujący sposób:

    protected EntityType entityType = EntityType.POJO;

Zatem encje [Categorie] i [Produit] utworzone w wyniku serializacji mają wszystkie typ POJO.

  • wiersz 11: zapisujemy kategorie. Tutaj to nie powinno działać. W rzeczywistości, jeśli w implementacji JDBC pole [Produit.categorie] nie jest potrzebne do zapisu (używane jest pole [idCategorie]), to w przypadku implementacji JPA jest ono absolutnie niezbędne. Pole to powinno wskazywać na encję [Categorie], a tutaj ma wartość null.

Przyjrzyjmy się kodowi metody [DaoCategorie.saveEntities] w warstwie [DAO / JPA]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // odnotowujemy produkty, które zostaną dodane
        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);
                    }
                    // przy okazji przywracamy (w razie potrzeby) relację produkt --> kategoria
                    produit.setCategorie(categorie);
                }
            }
        }
        // zapisujemy kategorie / produkty
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // aktualizujemy pole [idCategorie] dla dodanych produktów
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // wynik
        return categories;
    }
  • wiersze 13–14: widać, że powiązanie [Produit] --> [Categorie] zostało przywrócone dla encji POJO (wiersz 8), co ma miejsce w tym przypadku. To wyjaśnia, dlaczego zachowanie kategorii zadziałało. Taki scenariusz jest przydatny również w innych okolicznościach: nigdy nie ma pewności, czy użytkownik poprawnie powiązał produkty z kategoriami. Dlatego robimy to za niego;

Teraz przyjrzyjmy się metodzie [ProduitController.saveProduits], która zapisuje produkty:


@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
    ...
            // pobieramy zapisaną wartość
            String body = CharStreams.toString(request.getReader());
            // deserializujemy ją
            ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
            List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
            });
            // produkty są nadal dostępne
            produits = daoProduit.saveEntities(produits);
            List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
            for (Produit produit : produits) {
                coreProduits.add(new CoreProduit(produit.getId()));
            }
            // zwracamy odpowiedź
            return new Response<List<CoreProduit>>(0, null, coreProduits);
...
    }
  • wiersz 8: obiekt List<Produkt> jest odtwarzany na podstawie przesłanej wartości. Z powodów wyjaśnionych powyżej każdy obiekt [Produit] będzie miał pole:
    • [EntityType entityType] równe [EntityType.POJO];
    • [Categorie categorie] równe null;
  • wiersz 11: zapisanie produktów w bazie danych powinno zakończyć się niepowodzeniem. W rzeczywistości, w przypadku JPA zapisanie produktu w bazie danych jest możliwe tylko wtedy, gdy jego pole [categorie] wskazuje na encję [Categorie];

Przyjrzyjmy się kodowi metody [DaoProduit.saveEntities] warstwy [DAO / JPA]:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // przywracamy (w razie potrzeby) powiązanie między produktem a jego kategorią
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // zapisujemy produkty
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}
  • wiersze 3–8: dla każdego obiektu typu [Produit] typu POJO tworzone jest powiązanie z obiektem [Categorie] posiadającym właściwy klucz główny i wersję inną niż null. To wystarczy, aby warstwa JPA poprawnie zachowała produkt;

Przyjrzyjmy się jeszcze jednej kwestii. Obiekty [Categorie] i [Produit] posiadają dodatkowe pole [EntityType entityType], które zostanie zserializowane do postaci jSON, gdy obiekty te zostaną wysłane do klienta. Można to sprawdzić na przykładzie [Advanced Rest Client]:

Po stronie klienta obiekty [Categorie] i [Produit] zostały zdefiniowane bez pola [EntityType entityType]. Jest to normalne, ponieważ obiekty [Categorie] i [Produit] są serializowane bez części PROXY, [Categorie.produits] i [Produit.categorie]. Po stronie klienta nie ma zatem pojęcia encji o nazwie PROXY. Istnieją jedynie zwykłe obiekty.

Po stronie klienta ciąg jSON [1] jest odbierany przez następującą metodę [DaoCategorie.getAllShortEntities]:


    @Override
    public List<Categorie> getAllShortEntities() {
...
            // filtry jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // pobieranie wszystkich kategorii
            Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
            // lista kategorii List<Kategoria>
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
...
}
  • wiersz 5: konfiguruje się mapper jSON obiektu [RestTemplate] tak, aby obsługiwał filtry jSON i [jsonFilterCategorie] obiektuobiektu [Categorie] oraz filtra [jsonFilterProduit] obiektu [Produit];
  • wiersz 7: wartość wysyłana (tutaj jej nie ma) oraz wartość odbierana (List<Categorie>) są serializowane/deserializowane za pomocą tego mapera. Można zauważyć, że obecność pola [entityType] w otrzymanym łańcuchu jSON, mimo że pole to nie istnieje w encjach [Categorie] i [Produit] po stronie klienta, nie powoduje błędu. Jest ono ignorowane. Gdyby spowodowało błąd, zmodyfikowalibyśmy filtry po stronie klienta, aby zostało zignorowane.

Aby zaimplementować usługę internetową / jSON / JPA / EclipseLink, wystarczy zmienić implementację JPA:

  

Uwaga: należy nacisnąć klawisze Alt+F5, a następnie ponownie wygenerować wszystkie projekty Maven.

Usługę internetową uruchomimy przy użyciu konfiguracji uruchomieniowej [spring-webjson-server-jpa-generic-hibernate-eclipselink], która była już używana dla Hibernate. Po wykonaniu tej czynności należy uruchomić trzy testy klienta generycznego [spring-webjson-client-generic]:

  • w [1] – test [JUnitTestCheckArguments];
  • w [2] – test [JUnitTestDao];
  • w [3] – test [JUnitTestPushTheLimits] przeprowadzony po stronie klienta (projekt [spring-webjson-client-generic]);
  • w [4] – test [JUnitTestPushTheLimits] przeprowadzony po stronie serwera (konfiguracja uruchomienia [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);

18.9. Wdrożenie usługi internetowej / jSON / JPA / OpenJpa

Aby zaimplementować usługę internetową / jSON / JPA / OpenJpa, wystarczy zmienić implementację JPA:

  

Uwaga: należy nacisnąć klawisze Alt+F5, a następnie ponownie wygenerować wszystkie projekty Maven.

Usługę internetową uruchomimy przy użyciu konfiguracji uruchomieniowej [spring-webjson-server-jpa-generic-openpa]:

Po wykonaniu tej czynności należy przeprowadzić trzy testy klienta generycznego [spring-webjson-client-generic]:

  • w [1] – test [JUnitTestCheckArguments] (konfiguracja uruchomienia [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • w [2] – test [JUnitTestDao] (konfiguracja wykonania [spring-webjson-client-generic-JUnitTestDao]);
  • w [3] – test [JUnitTestPushTheLimits] przeprowadzony po stronie klienta (konfiguracja wykonania [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • w [4] test [JUnitTestPushTheLimits] uruchomiony po stronie serwera (konfiguracja wykonania [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);

Aby testy działały, konieczne było wprowadzenie zmian w warstwie DAO / JPA. W rzeczywistości, z niezrozumiałych przyczyn, metody [DaoCategorie.saveEntities] i [DaoProduit.saveEntities] zawiodły podczas wypełniania bazy danych, wskazując, że elementy odłączone nie mogą zostać zapisane. Element odłączony to element, który posiada:

  • klucza głównego innego niż null;
  • wersję inną niż null;

Żadna z tych sytuacji nie była sprawdzana. Nie wiedząc, gdzie szukać przyczyny, skopiowałem elementy, które miały zostać zapisane, do zupełnie nowej listy i wtedy testy zakończyły się powodzeniem. Modyfikację tę można było wprowadzić albo:

  • w warstwie [DAO / JPA];
  • w warstwie [web], która tworzy elementy do zapisania;

Zdecydowałem się to zrobić w warstwie [DAO / JPA]. Oczywiście wiąże się to z pewną utratą wydajności, ale jest ona całkowicie nieistotna w porównaniu z czasem odpowiedzi warstwy SGBD. Zmiany są następujące:

W klasie [DaoCategorie] projektu [spring-jpa-generic]:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // ***************************************************************************************
        // klonowanie listy kategorii — czasami konieczne dla OpenJpa — błąd nieznany
        // ***************************************************************************************
        List<Categorie> categories2 = new ArrayList<Categorie>();
        for (Categorie categorie : categories) {
            // kategoria
            Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
            EntityType categorieType = categorie.getEntityType();
            categorie2.setEntityType(categorieType);
            categories2.add(categorie2);
            // produkty
            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);
            }
        }
        // odnotowuje się produkty, które mają zostać dodane
        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);
                    }
                    // przy okazji przywracamy (w razie potrzeby) relację produkt --> kategoria
                    produit.setCategorie(categorie);
                }
            }
        }
        // zapisujemy kategorie / produkty
        try {
            categoriesRepository.save(categories2);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // aktualizujemy pole [idCategorie] dla dodanych produktów
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // wynik
        return categories2;
    }
  • wiersze 3–25: lista [categories] przekazana jako parametr (wiersz 2) jest duplikowana w liście [categories2] (wiersz 6). To właśnie ta lista jest zapisywana i zwracana wywołującemu (wiersz 52). Ma to istotną konsekwencję: zwracana jest lista inna niż ta przekazana w parametrach, a zatem tam, gdzie wcześniej można było napisać:
List<Categorie> categories=...
daoCategorie.saveEntities(categories)
// przetwarzanie [categories]

teraz należy zapisać:


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

W klasie [DaoProduit] projektu [spring-jpa-generic] metoda [saveEntities] została zmieniona w podobny sposób:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // ***************************************************************************************
        // klonowanie listy produktów — czasami konieczne dla OpenJpa — błąd nie uwzględniony
        // ***************************************************************************************
        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);
        }

        // przywracamy (w razie potrzeby) powiązanie między produktem a jego kategorią
        for (Produit produit : produits2) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // zapisuje się produkty
        try {
            return Lists.newArrayList(produitsRepository.save(produits2));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
}

Aby zaimplementować usługę internetową / jSON / JPA / EclipseLink / PostgresQL, należy zainstalować:

  • projekt [postgresql-config-jdbc] konfigurujący warstwę JDBC projektu PostgreSQL;
  • projekt [postresql-config-jpa-eclipselink] do konfiguracji warstwy JPA z projektu PostgreSQL;
  • naciśnij Alt-F5 i zregeneruj wszystkie projekty Maven;
  

Uruchamiamy SGBD PostgreSQL i uruchamiamy serwis internetowy z konfiguracją uruchomieniową [spring-webjson-server-jpa-generic-hibernate-eclipselink], która była już wcześniej używana. Następnie uruchamiamy trzy testy klienta generycznego [spring-webjson-client-generic]:

  • w [1] – test [JUnitTestCheckArguments] (konfiguracja uruchomienia [spring-webjson-client-generic-JUnitTestCheckArguments]);
  • w [2] – test [JUnitTestDao] (konfiguracja wykonania [spring-webjson-client-generic-JUnitTestDao]);
  • w [3] – test [JUnitTestPushTheLimits] przeprowadzony po stronie klienta (konfiguracja wykonania [spring-webjson-client-generic-JUnitTestPushTheLimits]);
  • w [4] test [JUnitTestPushTheLimits] uruchomiony po stronie serwera (konfiguracja wykonania [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);