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.
18.8. Implementacja usługi internetowej / jSON / JPA / EclipseLink
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ć:
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);
}
}
18.10. Implementacja usługi internetowej / jSON / JPA / EclipseLink / PostgreSQL
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]);







































