Skip to content

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] на стороні клієнта, не викликає помилки. Воно ігнорується. Якби це викликало помилку, ми б змінили фільтри на стороні клієнта, щоб воно ігнорувалося.

Щоб реалізувати веб-сервіс / 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=...
daoCategorie.saveEntities(categories)
// обробка [categories]

тепер потрібно писати:


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);
        }
}

Для реалізації веб-сервісу / 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]);