18. Клієнт, запрограмований для веб-сервісу / jSON
Тепер, коли база даних [dbproduitscategories] доступна в Інтернеті, ми напишемо додаток, який її використовуватиме. У результаті ми отримаємо таку архітектуру «клієнт-сервер»:
![]() |
Клієнтський додаток матиме три рівні:
- шар [Client HTTP] [3] для взаємодії з веб-додатком / jSON, який надає доступ до бази даних;
- шар [DAO] [2], який матиме той самий інтерфейс, що й шар [DAO] [4];
- тестовий рівень JUnit [1] для перевірки того, що клієнт і сервер працюють належним чином;
18.1. Проєкт Eclipse
Проект Eclipse клієнта виглядає наступним чином:
![]() |
![]() | ![]() | ![]() |
![]() | ![]() |
- пакет [spring.webjson.client.config] містить конфігурацію Spring для шару [DAO];
- пакет [spring.webjson.client.dao] містить реалізацію шару [DAO];
- пакет [spring.webjson.client.entities] містить об’єкти, що обмінюються з веб-сервісом / jSON. Ми знаємо їх усі;
- пакет [spring.webjson.client.infrastructure] містить класи винятків, що використовуються в проєкті. Ми знаємо їх усі;
18.2. Конфігурація проекту в Maven
Проєкт є проєктом Maven, налаштованим за допомогою такого файлу [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>
<!-- бібліотека jSON, що використовується 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>
<!-- компонент, що використовується 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>
<!-- бібліотека логів -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- плагіни -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядки 16–20: батьківський проект Maven [spring-boot-starter-parent], який дозволяє нам визначити певну кількість залежностей без зазначення їх версій, оскільки версії визначені в батьківському проекті;
- рядки 24–27: хоча ми й не пишемо веб-додаток, нам потрібна залежність [spring-web], яка містить клас [RestTemplate], що дозволяє легко взаємодіяти з веб-додатком / jSON;
- рядки 29–36: бібліотека jSON;
- рядки 38–41: залежність, яка дозволить нам прив’язати timeout до запитів HTTP від клієнта. timeout — це максимальний час очікування відповіді від сервера. Після закінчення цього часу клієнт повідомляє про помилку timeout, генеруючи виняток;
- рядки 43–48: бібліотека Google Guava;
- рядки 50–53: бібліотека логів;
- рядки 54–64: залежність для тестування JUnit. Вона, зокрема, включає бібліотеку JUnit 4, необхідну для тестування. Ці залежності мають атрибут [<scope>test</scope>], що вказує на те, що вони потрібні лише для етапу тестування. Вони не включаються до остаточного архіву проєкту;
18.3. Налаштування Spring
![]() |
Клас [AppConfig] здійснює конфігурацію Spring для клієнта HTTP. Його код такий:
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 {
// константи
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8081";
// фільтри 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) {
// створення компонента RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// конвертер jSON
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(new MappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters(messageConverters);
// тайм-аут обміну даними
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// результат
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
}
- рядок 20: клас є класом конфігурації Spring;
- рядок 21: інші компоненти Spring слід шукати в пакеті [spring.webjson.client.dao];
- рядок 25: встановлюється timeout тривалістю одну секунду (1000 мс);
- рядки 88–91: бін, який повертає це значення;
- рядок 26: URL веб-сервісу / jSON;
- рядки 93–96: bean, який повертає це значення;
- рядки 72–86: конфігурація класу [RestTemplate], який забезпечує обмін даними з веб-сервісом / jSON. Якщо його не потрібно конфігурувати, у коді його можна вказати просто як [new RestTemplate()]. Тут ми хочемо встановити timeout для обміну даними з веб-сервісом / jSON. Bean [timeout] у рядку 89 передається як параметр методу [restTemplate] у рядку 73;
- рядок 75: компонент [HttpComponentsClientHttpRequestFactory] дозволяє нам встановити timeout для обміну даними (рядки 82–83);
- рядок 76: клас [RestTemplate] побудовано на основі цього компонента. Оскільки він використовує цей компонент для взаємодії з веб-сервісом / jSON, обмін даними буде передано саме через timeout;
- рядки 78–80: до класу [RestTemplate] прив’язується конвертер jSON. Ми вже згадували про це під час розгляду веб-сервісу. Клієнт і сервер обмінюються рядками тексту. Конвертер відповідає за серіалізацію об’єкта в текст і, навпаки, за десеріалізацію тексту в об’єкт. До класу [RestTemplate] може бути прив’язано кілька конвертерів, і вибір конкретного конвертера в певний момент залежить від заголовків HTTP, надісланих сервером. У даному випадку ми маємо лише один конвертер jSON, оскільки обмінювані рядки тексту мають формат jSON;
- рядки 82–83: встановлюються параметри timeout для обміну даними;
- рядки 28–70: визначають фільтри jSON. Вони такі самі, як і на сервері, описані в розділі 17.3.2.1;
- рядки 29–32: bean [jsonMapper] є маппером jSON конвертера [MappingJackson2HttpMessageConverter], який ми пов’язали з класом [RestTemplate]. Він нам потрібен у визначенні фільтрів jSON;
- рядки 34–41: бін, що визначає фільтр jSON [catégorie sans ses produits]. Метод [jsonMapperShortCategorie] отримує як параметр бін [restTemplate], визначений у рядку 73;
- рядок 37: викликається метод [jsonMapper] з рядка 30 для отримання маппера jSON;
- рядки 38–39: встановлюється фільтр, щоб отримати категорію без її товарів;
- рядок 40: повертається маппер jSON у такому налаштованому вигляді;
- рядки 42–51: фільтр jSON для отримання категорії разом із її товарами;
- рядки 53–60: фільтр jSON для отримання товару без його категорії;
- рядки 62–70: фільтр jSON для отримання товару разом із його категорією;
Усі ці «beans» будуть доступні для кодів шару [DAO], а також для тестів JUnit.
18.4. Реалізація клієнта HTTP
![]() |
Вище показано, як шар [Client HTTP] взаємодіє з веб-сервісом, який ми щойно створили. Тепер ми його розглянемо.
![]() |
Клас [Client] реалізує обмін даними з веб-сервісом / jSON. Він реалізує наступний інтерфейс [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);
}
Інтерфейс має лише один метод [getResponse]:
- рядок 6: метод [getResponse] є узагальненим методом, що параметризується двома типами:
- [T1]: це тип відповіді, яку очікують від сервера в [Response<T1>], наприклад [List<Categorie>],
- [T2] — це тип параметра jSON, що надсилається операціями POST, наприклад [List<Produit>];
- рядок 6: метод [getResponse] повертає результат типу T1, наприклад [List<Categorie>];
- рядок 6: параметри [getResponse] такі:
- [String url]: URL, що підлягає запиту;
- [HttpMethod method]: метод HTTP запиту, GET або POST залежно від випадку,
- [int errStatus]: код помилки, який слід використовувати в класі [DaoException], якщо під час зв’язку з сервером сталася помилка,
- [T2 body]: значення, яке слід відправити, якщо існує POST;
Клас [Client] реалізує інтерфейс [IClient] наступним чином:
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 {
// введення
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// локальний
private String simpleClassName = getClass().getSimpleName();
// загальний запит
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
}
// список повідомлень про помилки винятку
protected List<String> getMessagesForException(Exception exception) {
...
}
}
- рядок 18: клас [Client] є компонентом Spring, який, отже, може бути ін'єктований в інші компоненти Spring;
- рядки 22–23: ін’єкція біна [RestTemplate], визначеного в [AppConfig] (див. параграф 18.3), який забезпечує зв’язок із сервером;
- рядки 24–25: ін’єкція URL із веб-сервісу / jSON, визначеного в [AppConfig] (див. параграф 18.3);
- рядки 37–39: приватний метод [getMessagesForException] — це допоміжний метод, що дозволяє отримати список повідомлень про помилки, що містяться у винятковій ситуації. Ми вже неодноразово зустрічали його;
Продовжимо:
// загальний запит
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
// відповідь сервера
ResponseEntity<Response<T1>> response;
try {
// готується запит
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);
}
// виконується запит
response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
});
} catch (Exception e) {
// інкапсуляція винятку
throw new DaoException(errStatus, e, simpleClassName);
}
...
}
- рядок 18: інструкція, яка надсилає запит на сервер і отримує відповідь. Компонент [RestTemplate] пропонує значну кількість методів взаємодії з сервером, але лише метод [exchange] підтримує загальні параметри. Саме тому його й було обрано. Другий параметр визначає тип очікуваної відповіді. Перший параметр — це запит типу [RequestEntity] (рядок 8). Результат методу [exchange] має тип [ResponseEntity<Response<T1>>] (рядок 5). Тип [ResponseEntity] інкапсулює повну відповідь сервера, заголовки HTTP та документ, надісланий сервером. Аналогічно, тип [RequestEntity] інкапсулює весь запит клієнта, включаючи заголовки HTTP та можливе значення, надіслане методом POST;
- рядки 8–16: нам потрібно побудувати запит типу [RequestEntity]. Він відрізняється залежно від того, чи використовується для формування запиту GET чи POST;
- рядок 10: запит для GET. Клас [RequestEntity] надає статичні методи для створення запитів GET, POST, HEAD,... Метод [RequestEntity.get] дозволяє створити запит GET шляхом ланцюгового виклику різних методів, що формують цей запит:
- метод [RequestEntity.get] приймає як параметр цільовий URL у вигляді екземпляра URI,
- метод [accept] дозволяє визначити елементи заголовка HTTP [Accept]. Тут ми вказуємо, що приймаємо тип [application/json], який надішле сервер;
- метод [build] використовує цю інформацію для формування типу запиту [RequestEntity];
- рядок 14: запит на POST. Метод [RequestEntity.post] дозволяє створити запит POST шляхом послідовного виклику різних методів, що формують його:
- метод [RequestEntity.post] приймає як параметр цільовий URL у вигляді екземпляра URI,
- метод [header] визначає заголовок HTTP. Тут на сервер надсилається заголовок [Content-Type: application/json], щоб повідомити йому, що відправлене значення надійде у вигляді рядка jSON;
- метод [accept] дозволяє вказати, що ми приймаємо тип [application/json], який надішле сервер;
- метод [body] встановлює значення, що надсилається. Воно є четвертим параметром загального методу [getResponse] (рядок 1);
- рядки 20–23: якщо трапляється помилка зв’язку з сервером, генерується виняток типу [DaoException] із кодом помилки, який відповідає параметру [errStatus], переданому як третій параметр у загальному методі [getResponse] (рядок 3);
Метод [getResponse] продовжується наступним чином:
// загальний запит
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
// отримано тіло відповіді
Response<T1> entity = response.getBody();
int status = entity.getStatus();
// чи є помилки на стороні сервера?
if (status != 0) {
// створюється виняток
throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
} else {
// все гаразд
return entity.getBody();
}
}
- рядок 4: ми отримали відповідь від сервера. Вона має тип [ResponseEntity<Response<T1>>] (рядок 5 попереднього розглянутого коду), де клас [Response] — це клас, який вже використовується на стороні сервера:
package spring.webjson.client.dao;
public class Response<T> {
// ----------------- властивості
// статус операції
private int status;
// можливе виключення
private String exception;
// тіло відповіді
private T body;
// конструктори
public Response() {
}
public Response(int status, String exception, T body) {
this.status = status;
this.exception = exception;
this.body = body;
}
// методи getter та setter
...
}
Повернемося до методу [getResponse]:
- рядок 6: ми отримуємо документ типу [Response<T1>], вкладений у відповідь. Цей тип має поля [int status, String exception, T1 body];
- рядок 7: ми отримуємо [status] з відповіді, який є кодом помилки;
- рядки 9–12: якщо сталася помилка, то генеруємо виняток, що містить обидві інформації [status, exception] із відповіді сервера;
- рядок 14: в іншому випадку ми повертаємо тип [T1], що міститься у відповіді типу [Response<T1>];
Клас [Client] є загальним. Його можна використовувати для будь-якого веб-клієнта / jSON.
18.5. Реалізація рівня [Dao]
![]() |
![]() |
18.5.1. Клас [AbstractDao]
Клієнтський рівень [DAO] має той самий інтерфейс, що й серверний рівень [DAO] (див. розділ 4.7):
package spring.webjson.client.dao;
import java.util.List;
import spring.webjson.client.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// список усіх сутностей T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// окремих сутностей — коротка версія
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);
// окремих сутностей — розширена версія
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);
// оновлення декількох сутностей
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// видалення всіх об’єктів
public void deleteAllEntities();
// видалення декількох об’єктів
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);
}
Клас [AbstractDao] реалізує інтерфейс [IDao]. Це клас, аналогічний однойменному класу на стороні сервера (див. параграф 4.8). Він слугує батьківським класом для класів [DaoCategorie] та [DaoProduit]. Він не є ідентичним з двох причин:
- на стороні сервера клас [AbstractDao] обробляє інформацію:
// вставки
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
яка нам тут не потрібна.
- На стороні сервера клас [AbstractDao] використовує анотації [@Transactional] для інкапсуляції кожного методу в транзакцію. На стороні клієнта немає бази даних, якою потрібно керувати. Тому ця анотація зникає;
Клас [AbstractDao] обмежується перевіркою правильності параметрів виклику методів інтерфейсу [IDao] перед тим, як делегувати виклик дочірнім класам:
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> {
// локальний
protected String simpleClassName = getClass().getSimpleName();
@Override
public List<T1> getShortEntitiesById(Iterable<Long> ids) {
// правильність аргументу
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// результат
return getShortEntitiesById(Lists.newArrayList(ids));
}
@Override
public List<T1> getShortEntitiesById(Long... ids) {
// правильність аргументу
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// результат
return getShortEntitiesById(Lists.newArrayList(ids));
}
...
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
...
}
// приватні методи ----------------------------------------------
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
// елементи null?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
simpleClassName);
}
// порожні елементи?
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>();
}
}
// результат за замовчуванням
return null;
}
@SuppressWarnings("unchecked")
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
// елементи null?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
}
// порожні елементи?
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>();
}
}
// результат за замовчуванням
return null;
}
// захищені методи ----------------------------------------------
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. Клас [DaoCategorie]
![]() |
Клас [DaoCategorie] має такий вигляд:
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;
...
}
- рядок 19: клас [DaoClient] є компонентом Spring, у який можна ін'єктувати інші компоненти Spring;
- рядок 20: клас [DaoClient] успадковує клас [AbstractDao<Categorie>], який ми щойно розглянули, і, отже, реалізує інтерфейс [IDao<Categorie>];
- рядки 22–23: вводиться контекст Spring, щоб отримати доступ до його бінів;
- рядки 24–25: ін’єктується клієнт HTTP, який ми щойно створили;
Реалізації різних методів інтерфейсу [DaoCategorie] всі побудовані за однаковою схемою. Ми розглянемо три методи: один базується на операції [GET], а два інші — на операції [POST].
18.5.2.1. Метод [getAllLongEntities]
Метод [getAllLongEntities] створює розгорнуту версію всіх категорій у базі даних:
@Override
public List<Categorie> getAllLongEntities() {
try {
// фільтри jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// отримати всі категорії
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// список категорій List<Categorie>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// відновити зв'язок «товар» → «категорія»
return linkCategorieWithProduits(categories);
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(233, e2, simpleClassName);
}
}
- рядок 2: метод повертає список категорій у їхніх повних версіях;
- рядок 5: мапер jSON, який дозволить серіалізувати відправлене значення (його немає) та десеріалізувати відповідь, повернуту класом [Client] (категорії у повних назвах);
- рядок 7: викликається метод [getResponse] класу [Client]. Саме цей метод забезпечує обмін даними з веб-сервісом /jSON. Його параметри такі:
- URL — номер запитуваного сервісу [/getAllLongCategories];
- метод [GET], який слід використовувати;
- код помилки, який слід використовувати у разі помилки (232);
- значення, що надсилається. У даному випадку його немає;
- рядок 7: у виразі [client.<List<Categorie>, Void>] вказуються фактичні параметри загальних типів [T1, T2] методу [getResponse]. Нагадаємо, що [T1] — це тип очікуваної відповіді, а [T2] — тип переданого значення. Тут очікується результат типу [List<Categorie>], а значення, що передається, типу [Void] відсутнє;
- рядок 7: результат, повернений методом [getResponse], зберігається в об’єкті типу [Object]. Це трохи дивно, оскільки очікується тип [List<Categorie>]. Це пов’язано з тим, що метод [getResponse], який працює з генеричними типами [T1, T2], завжди повертає тип [java.util.LinkedHashMap], який потім потрібно обробити, щоб отримати правильний тип;
- рядок 9: повертається список категорій. Для цього об’єкт [map] [mapper.writeValueAsString(map)] серіалізується у рядок jSON, який потім ресеріалізується у тип [List<Categorie>];
- рядок 13: отримано список категорій, деякі з яких можуть містити товари. Отримуємо скорочену версію цих товарів. Отже, під час десеріалізації створені об’єкти [Produit] мають поле [categorie==null]. Метод [linkCategorieWithProduits] відновлює зв’язок між [Produit] та його [Categorie];
- рядки 14–15: зупиняється виняток типу [DaoException], який міг би бути запущений методом [getResponse], щоб одразу ж запустити його знову. Ця дивна поведінка зумовлена тим, що якщо цього не зробити, виняток типу [DaoException] буде перехоплений рядками 16–18, а цього ми не хочемо;
- рядки 16–18: ми зупиняємо всі інші винятки, щоб інкапсулювати їх у тип [DaoException]. Нагадуємо, що рівень [DAO] повинен генерувати лише цей тип винятку;
Метод [linkCategorieWithProduits], який відновлює зв’язки між сутностями [Produit] та [Categorie], виглядає наступним чином:
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. Управління фільтрами jSON
Повернемося до управління фільтрами jSON у попередньому методі [getAllLongEntities]:
@Override
public List<Categorie> getAllLongEntities() {
try {
// фільтри jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// отримати всі категорії
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// список категорій List<Categorie>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
- рядок 5: із контексту Spring отримуємо мапер jSON, здатний обробляти повні назви категорій. Повернемося до визначення цього мапера у конфігурації Spring [AppConfig]:
// фільтри 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) {
...
}
- біан [jsonMapperLongCategorie], який запитується методом [getAlllongEntities], — це біан із рядків 7–15;
- рядок 10: маппер надається методом [jsonMapper] із рядків 2–5. Бачимо, що цей маппер jSON належить об’єкту [RestTemplate], який керує обміном даними HTTP між клієнтом і сервером. Саме цей маппер використовується за замовчуванням для:
- серіалізації значення, надісланого на сервер;
- десеріалізації відповіді, надісланої сервером;
Повернемося до коду [getAllLongEntities]:
// фільтри jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// отримати всі категорії
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// список категорій List<Категорія>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// відновити зв'язок «товар» —> «категорія»
return linkCategorieWithProduits(categories);
- рядок 2: отримуємо маппер [jsonMapperLongCategorie] із контексту Spring;
- рядок 4: виконується метод [getResponse]. При цьому відбувається:
- автоматична серіалізація відправленого значення (тут її немає);
- автоматична десеріалізація отриманої відповіді, в даному випадку типу List<Categorie>. Оскільки суть [Categorie] має фільтр jSON [jsonFilterCategorie], його потрібно було обробити. Це є причиною рядка 2;
- рядок 6: результат проходить другу серіалізацію/десеріалізацію за допомогою того самого маппера, щоб отримати тип List<Categorie>. У рядку 4 тип, що повертається [getResponse], є типом [Object];
У наступних методах слід пам’ятати, що мапер jSON, запитаний у контексті Spring, використовується як для відправленого значення (серіалізація), так і для отриманого значення (десеріалізація). Якщо одне або обидва значення мають фільтр jSON, їх потрібно налаштувати. Отже, мапер може мати до двох налаштованих фільтрів. У наведеному нижче прикладі такого ніколи не трапляється. Або відправлене значення не має фільтра (List<Long>, List<String>), або його не має отримане значення (List<CoreCategorie>, List<CoreProduit>). Сутності, що мають фільтр jSON, — це виключно [Categorie] та [Produit].
18.5.2.3. Метод [getShortEntitiesById]
Метод [getShortEntitiesById] повертає скорочені версії категорій, первинні ключі яких він отримує як параметри:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
// фільтри jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// отримати категорію без її товарів
Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
// категорія
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(223, e2, simpleClassName);
}
}
- рядок 5: мапер jSON, який дозволить серіалізувати відправлене значення (список первинних ключів) та десеріалізувати відповідь, повернуту класом [Client] (категорії у їхніх скорочених версіях). Обраний фільтр не матиме жодного впливу на відправлене значення, оскільки для елементів відправленого списку фільтр відсутній;
- рядок 7: викликається метод [getResponse] батьківського класу. Саме цей метод забезпечує обмін даними з веб-сервісом /jSON. Його параметри такі:
- URL запитуваного сервісу [/getShortCategoriesById];
- метод [POST], який слід використовувати;
- код помилки, який слід використовувати у разі помилки (204);
- значення, що надсилається. У даному випадку це список первинних ключів;
- рядок 7: у виразі [client.<List<Categorie>, List<Long>>] вказуються фактичні параметри загальних типів [T1, T2] методу [getResponse]. Нагадаємо, що [T1] — це тип очікуваної відповіді, а [T2] — тип переданого значення. Тут очікується результат типу [List<Categorie>], а передане значення — це список первинних ключів типу [List<Long>];
- рядок 7: результат, повернений методом [getResponse], поміщається в об’єкт типу [Object];
- рядок 9: повертається список категорій. Для цього об’єкт [map] [mapper.writeValueAsString(map)] серіалізується у рядок jSON, який потім ресеріалізується у тип [List<Categorie>];
18.5.2.4. Метод [saveEntities]
Метод [saveEntities] зберігає категорії в базі даних. Його код такий:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// фільтри jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// додати категорії
Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
entities);
// список доданих основних категорій
List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<CoreCategorie>>() {
});
// категорії оновлюються на основі отриманої інформації
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);
}
}
- рядок 2: метод [saveEntities] слугує для збереження в базі даних категорій, переданих як параметри. Він збагачує ці категорії їхніми первинними ключами. Якщо категорії передаються разом із товарами, то вони також зберігаються;
- рядок 5: мапер jSON, який дозволить серіалізувати відправлене значення (список категорій у розгорнутому вигляді) та десеріалізувати відповідь, повернуту класом [Client] (об’єкти [CoreCategorie]). Обраний фільтр не матиме жодного впливу на результат, оскільки елементи списку, отриманого у відповіді, не мають фільтра;
- рядок 7: викликається метод [getResponse] батьківського класу для обміну даними з веб-сервісом / jSON;
- перший параметр — це URL [/saveCategories];
- другий параметр — метод HTTP, який слід використовувати, у даному випадку — [POST];
- третій параметр — код помилки, який слід використовувати у разі помилки (200);
- останній параметр — це значення, що передається; у даному випадку це список категорій, які потрібно зберегти;
- рядок 7: загальні параметри [T1, T2] методу [getResponse] тут мають значення [List<CoreCategorie>, List<Categorie>]. Перший тип — це тип очікуваної відповіді, другий — тип відправленого значення;
- рядок 7: отриману відповідь поміщаємо у тип [Object];
- рядок 9: відтворюється відповідь типу [List<CoreCategorie>]. Відповідь, яку слід повернути, має тип [List<Categorie>] (рядок 2), а не [List<CoreCategorie>]. Отримана відповідь — це список первинних ключів збережених категорій та товарів;
- рядки 14–28: отримані первинні ключі прив’язуються до категорій та товарів (рядки 17, 23, 24). Крім того, відтворюються зв’язки [Produit] --> [Categorie] (рядки 24–25);
Усі інші методи слідують тій самій схемі.
18.6. Тест JUnit
Повернемося до архітектури «клієнт/сервер», яку ми зараз будуємо:
![]() |
Ми створили шар [DAO] [2] з тим самим інтерфейсом, що й шар [DAO] [4]. Отже, для тестування шару [DAO] [2] можна використовувати тести JUnit, які використовувалися для тестування шару [DAO] [4]:
![]() |
Ці три тести виконуються на основі таких конфігурацій виконання:
![]() | ![]() |
![]() |
Результати трьох тестів такі:
![]() |
![]() |
- у [1] — тест [JUnitTestCheckArguments];
- у [2] — тест [JUnitTestDao];
- у [3] — тест [JUnitTestPushTheLimits], виконаний на стороні клієнта (проект [spring-webjson-client-generic]);
- у [3] — тест [JUnitTestPushTheLimits], виконаний на стороні сервера (проект [spring-jdbc-generic-04]). Можна констатувати, що мережевий рівень спричиняє дуже незначне уповільнення порівняно з тим, яке спричиняється доступом до SGBD;
18.7. Реалізація веб-сервісу / jSON / JPA / Hibernate
Тепер розглянемо таку архітектуру:
![]() |
Зміна знаходиться у [1]. Серверний рівень [DAO] базується на реалізації JPA. Спочатку ми використаємо реалізацію JPA / Hibernate.
18.7.1. Проєкт Eclipse
Наразі в Eclipse завантажено такі проекти:
![]() |
Проєкт [spring-webjson-server-jdbc-generic] базувався на проєкті [spring-jdbc-generic-04], який налаштовує рівень DAO / JDBC для доступу до SGBD та MySQL. Ми створимо новий проєкт [spring-webjson-server-jpa-generic], який, у свою чергу, базуватиметься на проєкті [spring-jpa-generic], що налаштовує рівень DAO / JPA / JDBC для доступу до SGBD та MySQL. Ми знаємо, що в обох випадках рівень [DAO] реалізує той самий інтерфейс [IDao]. Отже, код рівня [web] не змінюється.
Ми можемо створити проект [spring-webjson-server-jpa-generic] шляхом копіювання та вставлення з проекту [spring-webjson-server-jdbc-generic]:
![]() |
- у [1], вказавши папку, спеціально створену для нового проєкту;
![]() |
Необхідно внести три типи змін. Перші знаходяться у файлі [pom.xml], що містить конфігурацію Maven для проекту:
<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>
<!-- веб-шар -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- шар [DAO] -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- плагіни -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядок 5: змінюємо назву артефакту Maven;
- рядки 24–28: залежність тепер вказується на проект [spring-jpa-generic], а не на [spring-jdbc-generic-04];
У підсумку залежності мають такий вигляд:
![]() |
Після цього вирішуються всі проблеми з імпортом, що виникли в різних класах. Наприклад, сутності [Produit, Categorie] тепер слід шукати не в проєкті [spring-jdbc-generic-04], а в проєкті [spring-jpa-generic]. Достатньо ввести [Ctrl-Maj-O] у коді класу, щоб перегенерувати імпорти.
Останню зміну потрібно внести у файл конфігурації [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 {
}
- рядок 9: тепер імпортується конфігурація проєкту [spring-jpa-generic], а не проєкту [spring-jdbc-generic-04];
Тепер все готово. Запускаємо веб-сервіс із конфігурацією [spring-webjson-server-jpa-generic-hibernate-eclipselink]:
![]() | ![]() |
Потім виконуємо три тести загального клієнта [spring-webjson-client-generic]:
![]() |
![]() |
- у [1] — тест [JUnitTestCheckArguments] (конфігурація виконання [spring-webjson-client-generic-JUnitTestCheckArguments]);
- у [2] — тест [JUnitTestDao] (конфігурація виконання [spring-webjson-client-generic-JUnitTestDao]);
- у [3] — тест [JUnitTestPushTheLimits], виконаний на стороні клієнта (конфігурація виконання [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- у [4] — тест [JUnitTestPushTheLimits], виконаний на стороні сервера (конфігурація виконання [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);
18.7.2. Чому це працює?
Це працює, і все ж, якщо уважно придивитися до коду, дивно, що це взагалі працює. Хоча шари [DAO], реалізовані проектами [spring-jdbc-generic-04] та [spring-jpa-generic], дійсно мають однаковий інтерфейс, вони не оперують тими самими сутностями [Categorie] та [Produit]: у проєкті [spring-jpa-generic] ці сутності мають додаткове поле [EntityType entityType], яке може приймати два можливі значення:
- EntityType.POJO: об’єкт є звичайним, усі його поля можна вільно використовувати;
- EntityType.PROXY: суть є об’єктом PROXY, що відображається шаром [JPA]. У цьому випадку деякі поля (а точніше, методи доступу до цих полів) не поводяться звичним чином, тому встановлено такі правила:
- якщо [Categorie.entityType==EntityType.PROXY], то не слід використовувати метод [getProduits];
- якщо [Produit.entityType==EntityType.PROXY], то не слід використовувати метод [getCategorie];
Однак щойно ми перенесли проєкт [spring-webjson-server-jdbc-generic] на [spring-webjson-server-jpa-generic] без змін у коді. Як це можливо?
Розглянемо код методу [saveCategories]:
@RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
// отримуємо значення, надіслане методом POST
String body = CharStreams.toString(request.getReader());
// десеріалізуємо його
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// зберігаємо категорії
categories = daoCategorie.saveEntities(categories);
...
}
- рядок 8: об’єкт List<Categorie> створюється на основі рядка jSON:
- у відправленому значенні товари не мають поля [categorie]. Дійсно, немає сенсу відправляти це поле. Якби його передали, то під час десеріалізації був би створений об’єкт [Produit] із полем [categorie], яке вказує на щойно створений об’єкт [Categorie]. Для n продуктів таким чином було б створено n об’єктів [Categorie], тоді як потрібен лише один. Крім того, поле [categorie] продуктів не вказуватиме на правильний об’єкт [Categorie], до якого вони належать. Тому тут продукти мають поле [categorie==null];
- у класах [Categorie] та [Produit] поле [EntityType entityType] визначено наступним чином:
protected EntityType entityType = EntityType.POJO;
Отже, сутності [Categorie] та [Produit], створені в результаті серіалізації, мають тип POJO.
- рядок 11: зберігаються категорії. Тут це не повинно працювати. Дійсно, якщо в реалізації JDBC поле [Produit.categorie] не використовується для збереження (використовується поле [idCategorie]), то для реалізації JPA воно є абсолютно необхідним. Це поле має вказувати на суть [Categorie], проте тут воно має значення null.
Розглянемо код методу [DaoCategorie.saveEntities] з рівня [DAO / JPA]:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// фіксуємо товари, які будуть додані
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);
}
// при цьому відновлюємо (за потреби) зв’язок «товар» → «категорія»
produit.setCategorie(categorie);
}
}
}
// зберігаємо категорії / товари
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// оновлюємо поле [idCategorie] для доданих товарів
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// результат
return categories;
}
- рядки 13–14: бачимо, що зв’язок [Produit] --> [Categorie] відновлено для сутностей POJO (рядок 8), що й відбувається в даному випадку. Це пояснює, чому збереження категорій спрацювало. Такий підхід корисний і в інших ситуаціях: ніколи не можна бути впевненим, що користувач правильно пов’язав товари з категоріями. Тому ми робимо це за нього;
Тепер розглянемо метод [ProduitController.saveProduits], який зберігає товари:
@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
...
// отримуємо значення, що було відправлено
String body = CharStreams.toString(request.getReader());
// здійснюється десеріалізація
ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
});
// зберігаються товари
produits = daoProduit.saveEntities(produits);
List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
for (Produit produit : produits) {
coreProduits.add(new CoreProduit(produit.getId()));
}
// повертається відповідь
return new Response<List<CoreProduit>>(0, null, coreProduits);
...
}
- рядок 8: об’єкт List<Продукт> відтворюється на основі надісланого значення. З причин, пояснених вище, кожен об’єкт [Produit] матиме поле:
- [EntityType entityType], що дорівнює [EntityType.POJO];
- [Categorie categorie] дорівнює null;
- рядок 11: збереження продуктів має завершитися невдачею. Адже для JPA збереження продукту можливе лише в тому випадку, якщо його поле [categorie] вказує на суть [Categorie];
Розглянемо код методу [DaoProduit.saveEntities] шару [DAO / JPA]:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// відновлюємо (за потреби) зв’язок між товаром та його категорією
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// зберігаються товари
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
- рядки 3–8: для кожного [Produit] типу POJO створюється посилання на об’єкт [Categorie] із правильним первинним ключем та версією, відмінною від null. Цього достатньо, щоб рівень JPA правильно зберег продукту;
Розглянемо останній момент. Об’єкти [Categorie] та [Produit] мають додаткове поле [EntityType entityType], яке буде серіалізовано в jSON під час надсилання цих об’єктів клієнту. Це можна перевірити за допомогою [Advanced Rest Client]:
![]() |
На стороні клієнта сутності [Categorie] та [Produit] були визначені без поля [EntityType entityType]. Це нормально, оскільки об’єкти [Categorie] та [Produit] серіалізуються без їхніх частин PROXY, [Categorie.produits] та [Produit.categorie]. Отже, на стороні клієнта поняття сутності PROXY відсутнє. Є лише звичайні об’єкти.
На стороні клієнта рядок jSON [1] приймається наступним методом [DaoCategorie.getAllShortEntities]:
@Override
public List<Categorie> getAllShortEntities() {
...
// фільтри jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// отримати всі категорії
Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
// список категорій List<Categorie>
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
...
}
- рядок 5: налаштовується мапер jSON об’єкта [RestTemplate] таким чином, щоб обробляти фільтри jSON та [jsonFilterCategorie] об’єктаоб’єкта [Categorie] та фільтр [jsonFilterProduit] об’єкта [Produit];
- рядок 7: відправлене значення (тут його немає) та отримане значення (List<Categorie>) серіалізуються/десеріалізуються за допомогою цього маппера. Можна помітити, що наявність поля [entityType] у отриманому ланцюжку jSON, хоча це поле не існує в сутностях [Categorie] та [Produit] на стороні клієнта, не викликає помилки. Воно ігнорується. Якби це викликало помилку, ми б змінили фільтри на стороні клієнта, щоб воно ігнорувалося.
18.8. Реалізація веб-сервісу / jSON / JPA / EclipseLink
Щоб реалізувати веб-сервіс / jSON / JPA / EclipseLink, достатньо змінити реалізацію JPA:
![]() |
Примітка: натисніть Alt+F5, а потім перегенеруйте всі проекти Maven.
Веб-сервіс запускатиметься з конфігурацією виконання [spring-webjson-server-jpa-generic-hibernate-eclipselink], яка вже використовувалася для Hibernate. Після цього виконайте три тести загального клієнта [spring-webjson-client-generic]:
![]() |
![]() |
- у [1] — тест [JUnitTestCheckArguments];
- у [2] — тест [JUnitTestDao];
- у [3] — тест [JUnitTestPushTheLimits], виконаний на стороні клієнта (проект [spring-webjson-client-generic]);
- у [4] — тест [JUnitTestPushTheLimits], виконаний на стороні сервера (конфігурація виконання [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);
18.9. Реалізація веб-сервісу / jSON / JPA / OpenJpa
Щоб реалізувати веб-сервіс / jSON / JPA / OpenJpa, достатньо змінити реалізацію JPA:
![]() |
Примітка: натисніть Alt+F5, а потім перегенеруйте всі проекти Maven.
Веб-сервіс запускається з конфігурацією виконання [spring-webjson-server-jpa-generic-openpa]:
![]() | ![]() |
Після цього виконайте три тести загального клієнта [spring-webjson-client-generic]:
![]() |
![]() |
- у [1] — тест [JUnitTestCheckArguments] (конфігурація виконання [spring-webjson-client-generic-JUnitTestCheckArguments]);
- у [2] — тест [JUnitTestDao] (конфігурація виконання [spring-webjson-client-generic-JUnitTestDao]);
- у [3] — тест [JUnitTestPushTheLimits], виконаний на стороні клієнта (конфігурація виконання [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- у [4] — тест [JUnitTestPushTheLimits], що виконується на стороні сервера (конфігурація виконання [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);
Щоб тести запрацювали, довелося внести зміни в рівень DAO / JPA. Справа в тому, що з незрозумілих причин методи [DaoCategorie.saveEntities] та [DaoProduit.saveEntities] видавали помилку під час заповнення бази даних, вказуючи, що відокремлені елементи не можуть бути збережені. Відокремлений елемент — це елемент, який має:
- первинний ключ, відмінний від null;
- версію, відмінну від null;
Жоден із цих випадків не перевірявся. Не знаючи, де шукати, я продублював сутності, що підлягали збереженню, у новому списку, і там тести пройшли успішно. Цю зміну можна було б здійснити або:
- у шарі [DAO / JPA];
- у шарі [web], який створює об’єкти для збереження;
Я вирішив зробити це в шарі [DAO / JPA]. Звичайно, це призводить до втрати продуктивності, але вона абсолютно незначна порівняно з часом відгуку шару SGBD. Зміни такі:
У класі [DaoCategorie] проекту [spring-jpa-generic]:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// ***************************************************************************************
// клонувати список категорій — іноді потрібно для OpenJpa — помилка не зрозуміла
// ***************************************************************************************
List<Categorie> categories2 = new ArrayList<Categorie>();
for (Categorie categorie : categories) {
// категорія
Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
EntityType categorieType = categorie.getEntityType();
categorie2.setEntityType(categorieType);
categories2.add(categorie2);
// товари
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);
}
}
// відзначаємо товари, які будуть додані
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);
}
// скористаємося нагодою, щоб відновити (за потреби) зв’язок «товар» → «категорія»
produit.setCategorie(categorie);
}
}
}
// зберігаємо категорії / товари
try {
categoriesRepository.save(categories2);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// оновлюємо поле [idCategorie] для доданих товарів
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// результат
return categories2;
}
- рядки 3–25: список [categories], отриманий як параметр (рядок 2), дублюється у списку [categories2] (рядок 6). Саме цей список зберігається та повертається викликувачу (рядок 52). Це має важливий наслідок: повертається список, відмінний від того, що передано в параметрах, і тому там, де раніше можна було написати:
тепер потрібно писати:
List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// обробка [categories]
У класі [DaoProduit] проекту [spring-jpa-generic] метод [saveEntities] змінюється аналогічним чином:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// ***************************************************************************************
// клонується список товарів — іноді це потрібно для OpenJpa — помилка не врахована
// ***************************************************************************************
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);
}
// відновлюємо (за потреби) зв’язок між товаром та його категорією
for (Produit produit : produits2) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// зберігаємо товари
try {
return Lists.newArrayList(produitsRepository.save(produits2));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
18.10. Реалізація веб-сервісу / jSON / JPA / EclipseLink / PostgreSQL
Для реалізації веб-сервісу / jSON / JPA / EclipseLink / PostgresQL необхідно встановити:
- проект [postgresql-config-jdbc] для налаштування шару JDBC з PostgreSQL;
- проект [postresql-config-jpa-eclipselink] для налаштування шару JPA з PostgreSQL;
- натисніть Alt-F5 і перегенеруйте всі проекти Maven;
![]() |
Запускаємо SGBD та PostgreSQL, а також запускаємо веб-сервіс із конфігурацією виконання [spring-webjson-server-jpa-generic-hibernate-eclipselink], яка вже використовувалася раніше. Після цього виконуємо три тести загального клієнта [spring-webjson-client-generic]:
![]() |
![]() |
- у [1] — тест [JUnitTestCheckArguments] (конфігурація виконання [spring-webjson-client-generic-JUnitTestCheckArguments]);
- у [2] — тест [JUnitTestDao] (конфігурація виконання [spring-webjson-client-generic-JUnitTestDao]);
- у [3] — тест [JUnitTestPushTheLimits], виконаний на стороні клієнта (конфігурація виконання [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- у [4] — тест [JUnitTestPushTheLimits], виконаний на стороні сервера (конфігурація виконання [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);







































