13. [Cours]: Публікація бази даних у веб-середовищі за допомогою Spring MVC
Ключові слова: багаторівнева архітектура, Spring, ін’єкція залежностей, веб-сервіс / jSON, клієнт / сервер
13.1. Support
![]() | ![]() |
Проєкти цього розділу можна знайти в папці [support / chap-13]. Скрипт SQL [dbintrospringdata.sql] дозволяє створити базу MySQL, необхідну для тестування.
13.2. Місце Spring MVC у веб-додатку
Розглянемо роль Spring MVC у розробці веб-додатку. Найчастіше такий додаток будується на багатошаровій архітектурі, наприклад, такій:
![]() |
- шар [Web] — це шар, що взаємодіє з користувачем веб-додатка. Користувач взаємодіє з веб-додатком через веб-сторінки, що відображаються у браузері. Саме в цьому шарі, і лише в ньому, розташований Spring MVC;
- шар [métier] реалізує правила управління додатком, такі як розрахунок заробітної плати або рахунку-фактури. Цей рівень використовує дані, що надходять від користувача через рівень [Web], а також дані з SGBD через рівень [DAO];
- шар [DAO] (об’єкти доступу до даних), шар [ORM] (об’єктно-реляційний мапер) та драйвер JDBC керують доступом до даних з SGBD. Рівень [ORM] забезпечує зв’язок між об’єктами, що обробляються рівнем [DAO], та рядками й стовпцями таблиць реляційної бази даних. Специфікація JPA (Java Persistence API) дозволяє абстрагуватися від використовуваного ORM, якщо він реалізує ці специфікації. Саме так і буде в даному випадку, і відтепер ми будемо називати шар ORM шаром JPA;
- інтеграція шарів здійснюється фреймворком Spring;
13.3. Модель розробки Spring MVC
Spring MVC реалізує так звану архітектурну модель MVC (Модель – Вигляд – Контролер) наступним чином:
![]() |
Обробка запиту клієнта відбувається наступним чином:
- запит — запитувані URL мають вигляд http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&.... [Front Controller] використовує файл конфігурації або анотації Java для «маршрутизації» запиту до відповідного контролера та відповідної дії в межах цього контролера. Для цього він використовує поле [Action] у URL. Решта URL [/param1/param2/...] складається з необов’язкових параметрів, які будуть передані до дії. Змінна C у MVC тут є рядком [Front Controller, Contrôleur, Action]. Якщо жоден контролер не може обробити запитувану дію, веб-сервер відповість, що запитувана дія URL не знайдена.
- Обробка
- обрана дія може використовувати параметри parami, які їй передала [Front Controller]. Вони можуть походити з кількох джерел:
- з шляху [/param1/param2/...], що вказує на URL,
- з параметрів [p1=v1&p2=v2] від URL,
- з параметрів, надісланих браузером разом із запитом;
- під час обробки запиту користувача дії може знадобитися рівень [métier] [2b]. Після обробки запиту клієнта це може викликати різні відповіді. Класичним прикладом є:
- сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
- сторінка підтвердження в іншому випадку
- дія вимагає відображення певного виду [3]. Цей вид відображатиме дані, які називаються моделлю виду. Це «М» у MVC. Дія створить цю модель M [2c] і вимагатиме відображення виду V [3];
- відповідь — обраний вигляд V використовує модель M, створену дією, для ініціалізації динамічних частин відповіді HTML, яку він повинен надіслати клієнту, а потім надсилає цю відповідь.
Для веб-сервісу / jSON попередня архітектура дещо змінена:
![]() |
- у [4a] модель, яка є класом Java, перетворюється на рядок jSON за допомогою бібліотеки jSON;
- у [4b] цей рядок jSON надсилається до браузера;
Тепер уточнимо зв’язок між веб-архітектурою MVC та багаторівневою архітектурою. Залежно від того, як ми визначаємо цю модель, ці два поняття можуть бути пов’язані або ні. Розглянемо однорівневий веб-додаток Spring MVC:
![]() |
Якщо ми реалізуємо шар [Web] за допомогою Spring MVC, ми отримаємо веб-архітектуру MVC, але не багатошарову архітектуру. У цьому випадку шар [web] буде відповідати за все: представлення, бізнес-логіку, доступ до даних. Цю роботу виконуватимуть дії.
Тепер розглянемо багаторівневу веб-архітектуру:
![]() |
Рівень [Web] можна реалізувати без фреймворку та без дотримання моделі MVC. У цьому випадку ми дійсно маємо багаторівневу архітектуру, але веб-рівень не реалізує модель MVC.
Наприклад, у середовищі .NET цей шар [Web]може бути реалізований за допомогою ASP.NET та MVC, і тоді ми отримуємо багатошарову архітектуру з шаром [Web] типу MVC. Зробивши це, можна замінити цей шар ASP.NET MVC на класичний шар ASP.NET (WebForms), при цьому залишивши решту (бізнес-логіка, DAO, ORM) без змін. У результаті ми отримуємо багаторівневу архітектуру з рівнем [Web], який більше не належить до типу MVC.
У MVC ми зазначили, що модель M відповідає представленню V, c.a.d, — сукупності даних, що відображаються представленням V. Наводимо ще одне визначення моделі M для MVC:
![]() |
Багато авторів вважають, що те, що знаходиться праворуч від шару [Web], утворює модель M для MVC. Щоб уникнути двозначностей, можна говорити про:
- моделі домену, коли мається на увазі все, що знаходиться праворуч від шару [Web]
- про модель подання, коли йдеться про дані, що відображаються поданням V
Надалі термін «модель M» позначатиме виключно модель подання V.
13.4. Веб-проект / jSON із Spring MVC
Сайт [http://spring.io/guides] пропонує навчальні посібники для початківців, щоб ознайомитися з екосистемою Spring. Ми будемо слідувати одному з них, щоб дізнатися про конфігурацію Maven, необхідну для проекту Spring MVC.
13.4.1. Демонстраційний проєкт
![]() |
- у [1] ми імпортуємо один із посібників Spring;
![]() |
- у [2] ми обираємо приклад [Rest Service];
- у [3] вибираємо проект Maven;
- у [4] вибираємо остаточну версію посібника;
- у [5] підтверджуємо;
- у [6] — імпортований проєкт;
Веб-сервіси, доступні через стандартні URL і які надають текст jSON, часто називають сервісами REST (REpresentational State Transfer). Сервіс вважається Restful, якщо він дотримується певних правил.
Тепер розглянемо імпортований проєкт, спочатку його конфігурацію Maven.
13.4.2. Конфігурація Maven
Файл [pom.xml] має такий вигляд:
<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
- рядки 6–8: властивості проекту Maven. Відсутній тег [<packaging>], що вказує тип файлу, який створюється під час компіляції Maven. За його відсутності використовується тип [jar]. Отже, додаток є консольним виконуваним файлом, а не веб-додатком, для якого тип упаковки був би [war];
- рядки 10–14: проект Maven має батьківський проект [spring-boot-starter-parent]. Саме він визначає основну частину залежностей проекту. Їх може бути достатньо, і в цьому випадку додаткові залежності не додаються, або ж їх може не вистачати, і тоді додаються відсутні залежності;
- рядки 17–20: артефакт [spring-boot-starter-web] містить бібліотеки, необхідні для проекту Spring MVC типу веб-сервісу, у якому не генеруються візуальні елементи. Цей артефакт містить дуже велику кількість бібліотек, зокрема бібліотеки вбудованого сервера Tomcat. Саме на цьому сервері буде виконуватися додаток;
Бібліотек, що входять до складу цієї конфігурації, дуже багато:
![]() | ![]() |
Вище показано три архіви сервера Tomcat.
13.4.3. Архітектура сервісу Spring [web / jSON]
Для веб-сервісу / jSON Spring MVC реалізує модель MVC наступним чином:
![]() |
- у [4a] модель, яка є класом Java, перетворюється на рядок jSON за допомогою бібліотеки jSON;
- у [4b] цей рядок jSON надсилається до браузера;
13.4.4. Контролер C
![]() |
Імпортований додаток має такий контролер:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- рядок 9: анотація [@RestController] перетворює клас [GreetingController] на контролер Spring, тобто його методи реєструються для обробки URL. Ми вже бачили подібну анотацію [@Controller]. Результатом роботи методів цього контролера був тип [String], який являв собою ім’я поданого виду. Тут ситуація інша. Методи контролера типу [@RestController] повертають об’єкти, які серіалізуються для відправки до браузера. Тип серіалізації залежить від конфігурації Spring MVC. У цьому випадку вони будуть серіалізовані у формат jSON. Саме наявність бібліотеки jSON серед залежностей проєкту змушує Spring Boot шляхом автоконфігурації налаштувати проєкт саме таким чином;
- рядок 14: анотація [@RequestMapping] вказує на URL, який обробляє метод, у даному випадку URL [/greeting];
- рядок 15: ми вже пояснювали анотацію [@RequestParam]. Результатом, що повертається методом, є об’єкт типу [Greeting].
- рядок 12: ціле число типу «long» атомарного типу. Це означає, що воно підтримує паралельний доступ. Кілька потоків можуть одночасно намагатися збільшити значення змінної [counter]. Це відбуватиметься коректно. Потік може зчитати значення лічильника лише тоді, коли потік, який його змінює, завершив свою операцію.
13.4.5. Модель M
Модель M, створена за допомогою попереднього методу, є таким об’єктом [Greeting]:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
Перетворення jSON цього об’єкта створить рядок {"id":n,"content":"текст"}. У підсумку рядок jSON, створений методом контролера, матиме такий вигляд:
або
13.4.6. Виконання
![]() |
Клас [Application.java] є виконуваним класом проекту. Його код такий:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Ми вже зустрічали та пояснювали цей код у попередньому прикладі.
13.4.7. Виконання проекту
Запустимо проект:
![]() |
Отримуємо такі записи консолі:
- рядок 13: сервер Tomcat запускається на порту 8080 (рядок 12);
- рядок 17: сервлет [DispatcherServlet] присутній;
- рядок 20: виявлено метод [GreetingController.greeting];
Щоб протестувати веб-додаток, надсилаємо запит на URL [http://localhost:8080/greeting]:
![]() | ![]() |
Ми отримуємо очікуваний рядок jSON. Може бути цікаво подивитися на заголовки HTTP, надіслані сервером. Для цього ми скористаємося розширенням для Chrome під назвою [Advanced Rest Client] (Chrome / Ctrl-T / Меню [Applications] / [Advanced Rest Client] — див. Додатки, параграф 22.5):
![]() |
- у [1] — запитуване URL;
- у [2] використовується метод GET;
- у [3] — відповідь jSON;
- у запиті [4] сервер вказав, що надсилає відповідь у форматі jSON;
- у [5] запитується той самий URL, але цього разу з POST;
- у [7] інформація надсилається на сервер у формі [urlencoded];
- у [6] — параметр name із його значенням;
- у [8] браузер повідомляє серверу, що надсилає йому інформацію [urlencoded];
- у [9] — відповідь jSON від сервера;
13.4.8. Створення виконуваного архіву
Тепер ми створюємо виконуваний архів:
![]() |
![]() |
- у [1]: виконується ціль Maven;
- у [2]: є дві цілі (goals): [clean] для видалення папки [target] з проекту Maven, [package] для її повторного створення;
- у [3]: папка [target], що буде згенерована, розміститься в цій папці;
- у [4]: генерується ціль;
У журналах, що з’являються в консолі, важливо побачити плагін [spring-boot-maven-plugin]. Саме він генерує виконуваний архів.
За допомогою консолі перейдіть до створеної папки:
D:\Temp\wksSTS\gs-rest-service\target>dir
...
11/06/2014 15:30 <DIR> classes
11/06/2014 15:30 <DIR> generated-sources
11/06/2014 15:30 11 073 572 gs-rest-service-0.1.0.jar
11/06/2014 15:30 3 690 gs-rest-service-0.1.0.jar.original
11/06/2014 15:30 <DIR> maven-archiver
11/06/2014 15:30 <DIR> maven-status
...
- рядок 5: створений архів;
Цей архів запускається наступним чином:
D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.0.RELEASE)
2014-06-11 15:32:47.088 INFO 4972 --- [ main] hello.Application
: Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...
Тепер, коли веб-додаток запущено, його можна відкрити у браузері:
![]() |
13.4.9. Розгортання додатка на сервері Tomcat
Як і в попередньому проєкті, ми змінюємо файл [pom.xml] наступним чином:
<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
...
</project>
- рядок 9: потрібно вказати, що буде створено архів war (Web ARchive);
Крім того, потрібно налаштувати веб-додаток. За відсутності файлу [web.xml] це робиться за допомогою класу, що успадковує [SpringBootServletInitializer]:
![]() |
Клас [ApplicationInitializer] має такий вигляд:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- рядок 6: клас [ApplicationInitializer] розширює клас [SpringBootServletInitializer];
- рядок 9: метод [configure] перевизначено (рядок 8);
- рядок 10: вказується клас, який налаштовує проект;
Щоб запустити проект, можна зробити наступне:
![]() |
- у [1-2] запускаємо проект на одному із серверів, зареєстрованих у IDE Eclipse;
Після цього можна завантажити URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] у браузері:
![]() |
13.4.10. Висновок
Ми розглянули тип проектів Spring, у яких веб-додаток надсилає потік даних до браузера. Тепер ми розробимо веб-додаток / jSON для оприлюднення в Інтернеті бази даних [dbintrospringdata], яку ми вивчали в підручнику [Introduction à Spring Data].
13.5. Оприлюднення бази даних [dbintrospringdata] у мережі Інтернет
13.5.1. Архітектура веб-сервісу / jSON
Ми реалізуємо таку архітектуру:
![]() |
Рівні [DAO] та [JPA] реалізовано за допомогою програми, написаної в посібнику [Introduction à Spring Data].
13.5.2. Встановлення бази даних
![]() |
Скрипт SQL [dbintrospringdata.sql] дозволяє створити базу даних MySQL, необхідну для тестування.
13.5.3. Проєкт Eclipse веб-сервісу / jSON
Проєкт Eclipse веб-сервісу / jSON виглядає наступним чином:
![]() |
Це проект 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>istia.st.webjson</groupId>
<artifactId>intro-server-webjson01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-server-webjson01</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядки 11–15: батьківський проект Maven, який вже використовувався для шару [DAO];
- рядки 18–22: залежність від шару [DAO];
- рядки 23–26: залежність від артефакту [spring-boot-starter-web]. Цей артефакт містить усі залежності, необхідні для створення веб-сервісу / jSON. Він також містить непотрібні бібліотеки. Тому потрібна більш точна конфігурація. Але ця конфігурація зручна для початку;
- рядки 28–30: залежність від артефакту [spring-boot-starter] дозволяє керувати анотаціями Spring Boot;
Залежності, що додаються цією конфігурацією, такі:
![]() |
- у [1] видно, що Eclipse виявив залежність від архіву проєкту [intro-spring-data-01];
Вищезазначені залежності є одночасно залежностями як шару [DAO], так і шару [web].
13.5.3.1. Налаштування шару [web]
Рівень [web] налаштовується за допомогою файлу [AppConfig]:
![]() |
Клас [WebConfig] налаштовує шар [web]:
package spring.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
// -------------------------------- конфігурація рівня [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// фільтри jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// результат
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// результат
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// результат
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// результат
return mapper;
}
}
- рядок 18: анотація [@EnableWebMvc] ініціює автоматичну конфігурацію фреймворку Spring MVC;
- рядок 19: клас [WebConfig] розширює клас Spring [WebMvcConfigurerAdapter], щоб перевизначити деякі його біни (рядки 26–40);
- рядки 22–23: ін’єкція контексту Spring;
- рядки 25–29: визначення сервлету фреймворку Spring MVC, який маршрутизує запити HTTP до відповідного контролера та методу. [DispatcherServlet] — це клас Spring;
- рядки 31–34: вказується, що цей сервлет обробляє всі запити URL;
- рядки 36–39: саме наявність цього біна активує сервер Tomcat, що міститься в архівах проєкту. Він очікуватиме запитів на порту 8080;
- рядки 42–91: біни, які використовуватимуться для управління фільтрами jSON;
- рядки 42–45: мапер jSON без фільтрів;
- рядки 47–57: маппер jSON, який дозволяє відображати категорію разом із її товарами. Слід зауважити, що при запиті категорії разом із її товарами необхідно налаштувати як фільтр jSON класу [Categorie], так і фільтр класу [Produit]. Так відбувається завжди. Під час серіалізації/десеріалізації класу в jSON необхідно налаштувати фільтр jSON цього класу та фільтри всіх залежностей, які мають бути включені до нього;
- рядки 59–69: мапер jSON, який дозволяє отримати товар разом із його категорією;
- рядки 71–80: маппер jSON, який дозволяє отримати категорію без її товарів;
- рядки 82–91: маппер jSON, який дозволяє отримати товар без його категорії;
Клас [AppConfig] налаштовує весь додаток, тобто шари [web] та [DAO]:
package spring.webjson.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import spring.data.config.DaoConfig;
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
}
- рядок 9: імпортуються біни з шару [DAO] та з шару [web];
- рядок 8: вказує, у яких пакетах можна знайти інші біни Spring;
Слід зауважити, що ми ніде не використовували анотацію [@EnableAutoConfiguration]. Ми вважали за краще самостійно контролювати конфігурацію.
13.5.4. Модель додатка
![]() |
Клас [ApplicationModel] має такий вигляд:
package spring.webjson.models;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
@Component
public class ApplicationModel implements IDao {
// шар [DAO]
@Autowired
private IDao dao;
@Override
public void addProduits(List<Produit> produits) {
dao.addProduits(produits);
}
@Override
public void deleteAllProduits() {
dao.deleteAllProduits();
}
@Override
public void updateProduits(List<Produit> produits) {
dao.updateProduits(produits);
}
@Override
public List<Produit> getAllProduits() {
return dao.getAllProduits();
}
@Override
public void addCategories(List<Categorie> categories) {
dao.addCategories(categories);
}
@Override
public void deleteAllCategories() {
dao.deleteAllCategories();
}
@Override
public void updateCategories(List<Categorie> categories) {
dao.updateCategories(categories);
}
@Override
public List<Categorie> getAllCategories() {
return dao.getAllCategories();
}
@Override
public Produit getProduitByIdWithCategorie(Long idProduit) {
return dao.getProduitByIdWithCategorie(idProduit);
}
@Override
public Produit getProduitByNameWithCategorie(String nom) {
return dao.getProduitByNameWithCategorie(nom);
}
@Override
public Categorie getCategorieByIdWithProduits(Long idCategorie) {
return dao.getCategorieByIdWithProduits(idCategorie);
}
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
return dao.getCategorieByNameWithProduits(nom);
}
@Override
public Produit getProduitByIdWithoutCategorie(Long idProduit) {
return dao.getProduitByIdWithoutCategorie(idProduit);
}
@Override
public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
return dao.getCategorieByIdWithoutProduits(idCategorie);
}
@Override
public Produit getProduitByNameWithoutCategorie(String nom) {
return dao.getProduitByNameWithoutCategorie(nom);
}
@Override
public Categorie getCategorieByNameWithoutProduits(String nom) {
return dao.getCategorieByNameWithoutProduits(nom);
}
}
- рядок 12: клас є синглтоном Spring;
- рядок 13: який реалізує інтерфейс [IDao] шару [DAO];
- рядки 16–17: ін'єкція посилання на рівень [DAO];
- рядки 19–99: реалізація інтерфейсу [IDao];
Архітектура веб-шару розвивається наступним чином:
![]() |
- у [2b] методи контролера (або контролерів) взаємодіють із синглтоном [ApplicationModel];
Ця стратегія забезпечує гнучкість у керуванні можливим кешем. Клас [ApplicationModel] може використовуватися для зберігання інформації, отриманої з рівня [DAO], або даних конфігурації. Це може бути корисно, коли немає контролю над рівнем [DAO]. Ця стратегія кешування може змінюватися з часом. Зміни не матимуть жодного впливу на код контролера або контролерів.
13.5.5. Контролер
![]() |
![]() |
Тут у нас є лише один контролер — клас [MyController].
13.5.5.1. Виведені URL
URL, представлені цим контролером, є такими:
| Додає товари до бази даних. Вони опубліковані. Відповіддю є рядок jSON — список доданих товарів із їхніми первинними ключами. |
| Видалити всі товари з бази даних. |
| Оновлює товари в базі даних. Вони опубліковані. Відповіддю є рядок jSON зі списку оновлених товарів. |
| Отримує рядок jSON для всіх товарів. |
| Додає категорії до бази даних. Вони публікуються. Відповіддю є рядок jSON зі списком доданих категорій та їх первинними ключами. Якщо категорії містять товари, вони також додаються до бази даних. |
| Видаляє всі категорії з бази даних, а також усі товари з цих категорій. Після цього база даних стає порожньою. |
| Оновлює категорії в базі даних. Вони публікуються. У відповіді надається список оновлених категорій. Якщо категорії містять товари, вони також оновлюються в базі даних. Повертає рядок jSON зі зміненими категоріями; |
| Отримує рядок jSON з усіх категорій. |
| Отримує рядок jSON для товару, визначеного за його ідентифікатором, разом із його категорією. |
| Отримує рядок jSON для товару, визначеного за його ідентифікатором, без зазначення категорії. |
| Отримує рядок jSON для товару, позначеного його назвою, разом із його категорією. |
| Отримує рядок jSON для товару, позначеного його назвою, без зазначення категорії. |
| Отримує рядок jSON із категорії, позначеної її ідентифікатором, разом із її товарами. |
| Отримує рядок jSON із категорії, позначеної її назвою, разом із її товарами. |
| Отримує рядок jSON із категорії, позначеної її назвою, без її товарів. |
| Отримує рядок jSON для категорії, визначеної за її ідентифікатором, без її товарів. |
Наведені URL відповідають методам інтерфейсу [IDao] рівня [DAO]. Усі методи веб-сервісу / jSON побудовані за одним і тим самим зразком. Розглянемо деякі з них.
13.5.5.2. Скелет контролера
Структура контролера така:
package spring.webjson.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
@Controller
public class MyController {
// залежності Spring
@Autowired
private ApplicationModel application;
// фільтри jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
// клас [MyController] є синглтоном і інстанціюється лише один раз після створення біна
public MyController() {
// System.out.println("MyController");
}
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
...
}
- рядок 28: анотація [@Controller] робить клас компонентом Spring;
- рядки 32–33: ін’єкція посилання на клас [ApplicationModel];
- рядки 36–50: ін’єкції посилань на маппери jSON;
- рядок 58: експонований URL — це [/addProduits]. Клієнт повинен використовувати метод [POST] для надсилання запиту (method = RequestMethod.POST). Він повинен надіслати значення у вигляді рядка jSON (consumes = "application/json; charset=UTF-8"). Метод сам повертає відповідь клієнту (рядок 59). Це буде рядок символів (рядок 60). Заголовок HTTP [Content-type : application/json; charset=UTF-8] буде надіслано клієнту, щоб повідомити його про те, що він отримає рядок jSON (рядок 58);
- рядок 60: метод [addProduits] повертає рядок jSON зі списку товарів, доданих до бази;
13.5.5.3. Відповідь методів контролера
Усі методи контролера повертають відповідь типу [Response], а саме:
![]() |
package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- властивості
// статус операції
private int status;
// можливі повідомлення про помилки
private List<String> messages;
// тіло відповіді
private T body;
// конструктори
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// методи getter та setter
...
}
- рядок 5: відповідь інкапсулює тип T;
- рядок 13: відповідь типу T;
- рядки 9–11: можливе виникнення винятку під час виконання методу. У цьому випадку метод поверне відповідь із:
- рядок 9: status!=0;
- рядок 11: список виявлених помилок;
13.5.5.4. L'URL [/addProduits]
L'URL [/addProduits] обробляється наступним методом:
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
// відповідь
Response<List<Produit>> response;
try {
// отримуємо відправлене значення
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// відновлюємо зв’язок між товарами та категоріями
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// зберігаємо товари
application.addProduits(produits);
response = new Respon se<List<Produit>>(0, null, produits);
} catch (DaoException e1) {
response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
}
// відповідь jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- рядок 3: метод приймає як параметр [HttpServletRequest request], який містить всю інформацію про запит клієнта;
- рядок 5: відповідь, яка буде надіслана клієнту: список товарів;
- рядок 8: отримуємо значення, надіслане методом POST. Клас [CharStreams] належить до бібліотеки [Google Guava], посилання на яку було додано у файлі [pom.xml]. Отримуємо рядок jSON, надісланий клієнтом. Його потрібно десеріалізувати, щоб щось з ним зробити;
- рядки 8–10: десеріалізація виконана. Отримуємо список товарів, де кожен товар має поле [categorie=null];
- рядки 12–14: скидаємо значення поля [categorie] для всіх товарів у списку. Для цього використовуємо поле [idCategorie] товару, яке вже ініціалізовано;
- рядок 16: товари вносяться до бази даних;
- рядок 17: об’єкт [response] ініціалізується зі списком товарів;
- рядки 18–19: випадок, коли метод стикається з винятком на рівні [DAO]. Відповідь ініціалізується значенням [status=1000] (код помилки) [messages=e1.getMessages()], тобто клієнту передається список помилок, що виникли на стороні сервера;
- рядки 20–21: випадок, коли метод стикається з іншим типом винятку. Відповідь ініціалізується з [status=1000] (код помилки) [messages=getErreursForException(e)], де [getErreursForException] — це приватний метод класу, який повертає список помилок, пов’язаних із винятками зі стека винятків e, та [body=null];
- рядок 24: повертається рядок jSON у відповіді;
13.5.5.5. URL [/getAllProduits]
URL [/getAllProduits] обробляється за допомогою наступного методу:
@RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// відповідь
Response<List<Produit>> response;
try {
response = new Response<List<Produit>>(0, null, application.getAllProduits());
} catch (DaoException e1) {
response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
}
// відповідь jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- рядок 1: URL [/getAllProduits] запитується за допомогою операції [GET]. Вона генерує jSON;
- рядок 2: метод самостійно надсилає відповідь jSON клієнту;
- рядок 5: метод надсилає рядок jSON типу [Response<List<Produit>>];
- рядок 7: запитуються товари без зазначення їхньої категорії;
- рядки 8–12: у разі помилки відповідь ініціалізується кодом та повідомленнями про помилку;
- рядок 14: відповідь jSON надсилається клієнту;
13.5.5.6. Conclusion
Ми не будемо розглядати інші методи контролера. Вони схожі на один із двох методів, які ми щойно розглянули.
13.5.6. Клас виконання веб-сервісу / jSON
![]() |
Клас [Boot] є класом виконання проекту:
package spring.webjson.boot;
import org.springframework.boot.SpringApplication;
import spring.webjson.server.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
- рядок 10: виконується статичний метод [SpringApplication.run]. Клас [SpringApplication] є класом проекту [Spring Boot] (рядок 3). Йому передаються два параметри:
- [AppConfig.class]: клас, що налаштовує весь додаток;
- [args]: можливі аргументи, передані методу [main] у рядку 9. Цей параметр тут не використовується;
Під час виконання цього класу з’являються такі записи в журналі:
- рядки 17–19: запуск сервера Tomcat, який буде виконувати веб-сервіс / jSON;
- рядки 25–33: побудова шару [DAO];
- рядки 32–51: виявлення відкритих URL;
13.5.7. Тестування веб-сервісу / jSON
Для проведення тестування ми генеруємо базу даних MySQL [dbintrospringdata] за допомогою скрипта SQL [dbintrospringdata.sql]:
![]() |
Після цього ми використовуємо клієнт [Advanced Rest Client] (див. параграф 22.5) для запиту до URL, що надаються веб-сервісом / jSON (веб-сервіс / jSON має бути запущений).
![]() |
- у [1-3] ми запитуємо URL [/getAllCategories] за допомогою команди HTTP GET;
Ми отримуємо таку відповідь:
![]() |
- у [1] — запит HTTP від клієнта;
- у [2] — відповідь HTTP від сервера;
- у [3] статус [200 OK] вказує, що сервер успішно обробив запит;
- у [4] — відповідь сервера jSON;
Повна відповідь jSON виглядає так:
{"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
- status:0 означає, що на стороні сервера помилок не було;
- messages: null означає, що повідомлень про помилки немає;
- body: це тіло відповіді, у даному випадку — список категорій із відповідними товарами. Є дві категорії, у кожній з яких по 5 товарів;
Ми додамо до категорії [categorie1] товар [produit15]. Для цього ми використаємо URL [/addCategories], який має такий код:
@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addCategories(HttpServletRequest request) throws JsonProcessingException {
Response<List<Categorie>> response;
ObjectMapper mapper = context.getBean(ObjectMapper.class);
// зберігаються категорії
try {
// отримуємо значення, що було відправлено
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// відновлюємо зв'язок між товарами та категоріями
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// зберігаються категорії
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// відповідь jSON
return mapper.writeValueAsString(response);
}
- рядок 1: клієнт повинен створити POST, а значення, що надсилається, має бути рядком jSON;
- рядки 9–12: значення, що відправляється, має бути списком категорій із відповідними товарами;
Ми створимо категорію [categorie2] із товаром [produit21]. Тоді рядок jSON, який потрібно надіслати, буде таким:
[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]
Запит до веб-сервісу /jSON формується наступним чином:
![]() |
- у [1] — запитується URL;
- у [2] запитується через операцію POST;
- у [3] — надіслано рядок jSON;
- у [4] серверу повідомляється, що йому буде надіслано jSON;
Відповідь сервера така:
![]() |
- у [1] видно, що і категорія, і її товар тепер мають первинний ключ, що свідчить про те, що вони, ймовірно, були внесені до бази даних. Ми перевіримо це, використовуючи URL та [/getCategorieByNameWithProduits/categorie2]:
![]() |
Ми отримуємо такий результат:
![]() |
Ми дійсно отримали категорію [categorie2] з її єдиним товаром [produit21]. Можна також запитати лише товар. Для цього використаємо URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
Ми отримуємо такий результат:
![]() |
Усі операції [GET] можна виконати у звичайному веб-браузері:
![]() |
Читачам пропонується випробувати інші функції URL веб-сервісу / json.
13.6. Клієнт, запрограмований для веб-сервісу / jSON
Тепер, коли база даних [dbintrospringdata] доступна в Інтернеті, ми напишемо додаток, який її використовуватиме. У результаті ми отримаємо таку архітектуру «клієнт-сервер»:
![]() |
Клієнтський додаток матиме два рівні:
- рівень [DAO] [2] для взаємодії з веб-додатком / jSON, який надає доступ до бази даних;
- тестовий рівень JUnit [1] для перевірки того, що клієнт і сервер працюють належним чином;
13.6.1. Проєкт Eclipse
Проект Eclipse клієнта має такий вигляд:
![]() |
- папка [src/main/java] реалізує рівень [DAO];
- папка [src/test/java] реалізує тести JUnit;
13.6.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>istia.st.webjson</groupId>
<artifactId>intro-client-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.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>
<scope>test</scope>
</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>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
<name>intro-client-webjson-01</name>
</project>
- рядки 14–18: батьківський проект Maven [spring-boot-starter-parent], який дозволяє нам визначити певну кількість залежностей без зазначення їхніх версій, оскільки версії визначені в батьківському проекті;
- рядки 22–25: хоча ми й не пишемо веб-додаток, нам потрібна залежність [spring-web], яка містить клас [RestTemplate], що дозволяє легко взаємодіяти з веб-додатком / jSON;
- рядки 27–34: бібліотека jSON;
- рядки 36–39: залежність, яка дозволить нам прив’язати timeout до запитів HTTP від клієнта. timeout — це максимальний час очікування відповіді від сервера. Після закінчення цього часу клієнт повідомляє про помилку timeout, генеруючи виняток;
- рядки 41–46: бібліотека Google Guava, яка використовується в тесті JUnit. З цієї причини ми встановили її область дії на [test] (рядок 45). Це означає, що ця залежність включається лише під час виконання коду з гілки [src/test/java];
- рядки 48–51: бібліотека логів;
- рядки 52–63: залежність для тестування JUnit. Вона, зокрема, включає бібліотеку JUnit 4, необхідну для тестування. Ці залежності мають атрибут [<scope>test</scope>], що вказує на те, що вони потрібні лише для етапу тестування. Вони не включаються до кінцевого архіву проєкту;
13.6.3. Реалізація шару [DAO]
![]() |
![]() |
- пакет [spring.client.config] містить конфігурацію Spring для шару [DAO];
- пакет [spring.client.dao] містить реалізацію шару [DAO];
- пакет [spring.client.entities] містить об’єкти, що обмінюються з веб-сервісом / jSON;
13.6.3.1. Configuration
![]() |
Клас [DaoConfig] здійснює конфігурацію Spring для шару [DAO]. Його код такий:
package spring.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
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;
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
// константи
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// створення компонента RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// тайм-аут обміну даними
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// результат
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// фільтри jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// результат
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// результат
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// результат
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// маппер jSON
ObjectMapper mapper = new ObjectMapper();
// фільтри
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// результат
return mapper;
}
}
- рядок 13: клас є класом конфігурації Spring — компоненти Spring слід шукати в пакеті [spring.client.dao];
- рядок 17: встановлюється timeout тривалістю одну секунду (1000 мс);
- рядки 32–35: bean, який повертає це значення;
- рядок 18: URL веб-сервісу / jSON;
- рядки 37–40: bean, який повертає це значення;
- рядки 20–30: конфігурація класу [RestTemplate], який забезпечує обмін даними з веб-сервісом / jSON. Якщо його не потрібно конфігурувати, у коді його можна вказати просто як [new RestTemplate()]. Тут ми хочемо встановити timeout для обміну даними з веб-сервісом / jSON. Bean [timeout] у рядку 36 передається як параметр методу [restTemplate] у рядку 24;
- рядок 23: компонент [HttpComponentsClientHttpRequestFactory] дозволяє нам задати timeout для обміну даними (рядки 29–30);
- рядок 24: клас [RestTemplate] побудовано на основі цього компонента. Оскільки він використовує цей компонент для взаємодії з веб-сервісом / jSON, обмін даними буде здійснюватися через timeout;
- клієнт і сервер обмінюватимуться текстовими рядками. Конвертер відповідає за серіалізацію об’єкта в текст і, навпаки, за десеріалізацію тексту в об’єкт. З класом [RestTemplate] може бути пов’язано кілька конвертерів, і вибір конкретного конвертера в певний момент залежить від заголовків HTTP, надісланих сервером. У даному випадку конвертер відсутній. Отже, компонент [RestTemplate] не намагатиметься жодним чином конвертувати два наступні елементи:
- опублікований текст;
- текст, отриманий у відповідь;
Ці тексти будуть рядками jSON, які, отже, компонент [RestTemplate] залишить без змін. Саме ми, розробники, будемо виконувати необхідні сериалізацію та десериалізацію jSON. Це пов’язано з тим, що фільтри, які слід застосувати до відправленого значення та отриманої відповіді, можуть відрізнятися, а досвід показує, що простіше керувати ними самостійно, ніж намагатися налаштувати компонент [RestTemplate] так, щоб він використовував правильний конвертер jSON;
- рядки 42–92: визначають фільтри jSON. Вони такі самі, як і серверні, представлені та пояснені в розділі 13.5.3.1;
- рядки 43–46: мапер jSON без фільтрів;
- рядки 64–68: мапер jSON для створення категорії без її товарів;
- рядки 48–58: маппер jSON для отримання категорії разом із її товарами;
- рядки 83–92: маппер jSON для отримання товару без його категорії;
- рядки 60–70: маппер jSON для створення товару разом із його категорією;
Усі ці bean будуть доступні для кодів шару [DAO], а також для тесту Junit.
13.6.3.2. Об’єкти
![]() |
Ентітети, з якими працює шар [DAO], — це ті, якими він обмінюється з веб-сервісом /jSON. Це товари та продукти. На стороні сервера ці сутності мали анотації персистентності JPA. Тут ці анотації було видалено. Нагадуємо код сутностей:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractEntity {
// властивості
protected Long id;
protected Long version;
// конструктори
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// перевизначення [equals] та [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// підпис jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// методи getter та setter
...
}
[Categorie]
package spring.client.entities;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
// властивості
private String nom;
// супутні товари
public Set<Produit> produits = new HashSet<Produit>();
// конструктори
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// методи
public void addProduit(Produit produit) {
// додаємо товар
produits.add(produit);
// визначаємо його категорію
produit.setCategorie(this);
}
// гетери та сеттери
...
}
[Produit]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// назва
private String nom;
// номер категорії
private Long idCategorie;
// ціна
private double prix;
// опис
private String description;
// категорія
private Categorie categorie;
// виробники
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// методи getter та setter
...
}
13.6.3.3. Клас [DaoException]
![]() |
Коли рівень [DAO] зіткнеться з помилкою, він викличе виняток типу [DaoException]. Цей клас використовується на стороні сервера та описаний у розділі 11.3.7.
13.6.3.4. Інтерфейс шару [DAO]
![]() |
Рівень [DAO] має інтерфейс [IDao], описаний у розділі 11.3.7.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// додавання списку товарів
public List<Produit> addProduits(List<Produit> produits);
// видалення всіх товарів
public void deleteAllProduits();
// оновлення списку товарів
public List<Produit> updateProduits(List<Produit> produits);
// отримання всіх товарів
public List<Produit> getAllProduits();
// додавання списку категорій
public List<Categorie> addCategories(List<Categorie> categories);
// видалення всіх категорій
public void deleteAllCategories();
// оновлення списку категорій
public List<Categorie> updateCategories(List<Categorie> categories);
// отримання всіх категорій
public List<Categorie> getAllCategories();
// конкретний товар
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// конкретна категорія
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
13.6.3.5. Відповідь веб-сервісу / jSON
![]() |
Ми бачили, що всі URL веб-сервісу / jSON повертають тип [Response], визначений у розділі 13.5.5.3. Тут ми повторюємо цей клас:
package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- властивості
// статус операції
private int status;
// можливі повідомлення про помилки
private List<String> messages;
// тіло відповіді
private T body;
// конструктори
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// методи getter та setter
...
}
13.6.3.6. Реалізація обміну даними з веб-сервісом / jSON
![]() |
Клас [AbstractDao] реалізує обмін даними з веб-сервісом / jSON:
package spring.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestTemplate;
public abstract class AbstractDao {
// дані
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// загальний запит
protected String getResponse(String url, String jsonPost) {
// URL: URL для зв’язку
// jsonPost: значення jSON, яке потрібно відправити
try {
// виконання запиту
RequestEntity<?> request;
if (jsonPost != null) {
// запит POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// запит GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// виконується запит
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- рядки 15–16: підключення компонента [RestTemplate], який забезпечує зв’язок із сервером;
- рядки 17–18: ін'єкція компонента URL із веб-сервісу / jSON;
Реалізація методів зв’язку із сервером винесена в метод [getResponse]:
- рядок 21: метод отримує 2 параметри:
- [url]: запитуваний URL;
- [jsonPost]: рядок jSON, який потрібно відправити, інакше — null. Якщо [jsonPost==null], запит на URL надсилається з GET, інакше — з POST;
- рядок 38: інструкція, яка надсилає запит до сервера та отримує відповідь. Компонент [RestTemplate] пропонує значну кількість методів взаємодії з сервером. Тут ми обрали метод [exchange], але існують й інші;
- рядки 27–36: нам потрібно сформувати запит типу [RequestEntity]. Він відрізняється залежно від того, чи використовується для формування запиту GET чи POST;
- рядки 30–31: запит для GET. Клас [RequestEntity] надає статичні методи для створення запитів GET, POST, HEAD,... Метод [RequestEntity.get] дозволяє створити запит GET шляхом ланцюгового виклику різних методів, що формують його:
- метод [RequestEntity.get] приймає як параметр цільовий URL у вигляді екземпляра URI,
- метод [accept] дозволяє визначити елементи заголовка HTTP [Accept]. Тут ми вказуємо, що приймаємо тип [application/json], який надішле сервер;
- метод [build] використовує цю інформацію для формування типу запиту [RequestEntity];
- рядки 34–35: запит на POST. Метод [RequestEntity.post] дозволяє створити запит POST шляхом послідовного виклику різних методів, що формують його:
- метод [RequestEntity.post] приймає як параметр цільовий URL у вигляді екземпляра URI,
- метод [header] визначає заголовок HTTP. Тут на сервер надсилається заголовок [Content-Type: application/json], щоб повідомити йому, що відправлене значення надійде у вигляді рядка jSON;
- метод [accept] дозволяє вказати, що ми приймаємо тип [application/json], який надішле сервер;
- метод [body] встановлює значення, що надсилається. Воно є четвертим параметром загального методу [getResponse] (рядок 1);
- рядок 38: метод [RestTemplate].exchange повертає тип [ResponseEntity<String>], який інкапсулює всю відповідь сервера: заголовки HTTP та тіло документа. Метод [ResponseEntity].getBody() дозволяє отримати це тіло, яке представляє відповідь сервера, в даному випадку — рядок символів;
13.6.3.7. Реалізація інтерфейсу [IDao]
![]() |
Клас [Dao] реалізує інтерфейс [IDao]:
package spring.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@Component
public class Dao extends AbstractDao implements IDao {
@Autowired
private ApplicationContext context;
// фільтри jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Override
public List<Produit> addProduits(List<Produit> produits) {
// ----------- додати товари (без їхньої категорії)
...
}
- рядок 17: клас [Dao] є компонентом Spring, у який, отже, можна ін’єктувати інші компоненти Spring;
- рядок 18: клас [Dao] успадковує клас [AbstractDao], який ми щойно розглянули, та реалізує інтерфейс [IDao];
- рядки 20–21: вводиться контекст Spring, щоб отримати доступ до його бінів;
- рядки 24–38: підстановка мапперів jSON, визначених у класі [AppConfig], представленому в розділі 13.6.2;
Реалізації різних методів інтерфейсу [IDao] всі побудовані за однаковою схемою. Ми розглянемо два методи: один базується на операції [POST], інший — на операції [GET].
Приклад [GET]: [getCategorieByNameWithProduits]
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
// ----------- отримати категорію за її назвою разом із товарами
try {
// запит
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// помилка?
if (response.getStatus() != 0) {
// виникає 1 виняток
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// повертається основна частина відповіді сервера
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
- рядок 7: викликається метод [getResponse] батьківського класу. Саме цей метод забезпечує взаємодію з веб-сервісом / jSON. Його параметри такі:
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
- (продовження)
- URL запитуваного сервісу [/getCategorieByNameWithProduits/nom];
- значення, що надсилається. У даному випадку його немає;
Метод [getResponse] повертає тип String, який є відповіддю jSON, надісланою сервером. Цю відповідь jSON десеріалізуємо наступним чином:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
оскільки рядок jSON є серіалізацією типу [Response<Categorie>];
- рядки 11–17: перевіряється статус відповіді. Якщо статус відмінний від 0, це означає, що на стороні сервера сталася помилка. У такому разі генерується виняток (рядок 13) із використанням інформації, що міститься у відповіді (статус та список повідомлень про помилки);
- рядок 16: якщо на стороні сервера помилки не сталося, повертається тіло типу [Response<Categorie>], тобто запитувана категорія;
- рядки 18–19: обробляється виняток, згенерований у рядку 16;
- рядки 20–22: обробка всіх інших винятків;
Приклад [POST]: [addCategories]
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- додати категорії (разом із їхніми товарами)
try {
// запит
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// помилка?
if (response.getStatus() != 0) {
// викликається 1 виняток
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// повертається основна частина відповіді сервера
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- рядок 2: метод [addCategories] слугує для збереження в базі даних категорій, переданих як параметри. Він збагачує ці категорії їхніми первинними ключами. Якщо категорії передаються разом із товарами, то й вони також зберігаються;
- рядок 7: викликається метод [getResponse] батьківського об’єкта для обміну даними з веб-сервісом / jSON;
- перший параметр — це URL [/addCategories];
- другий параметр — це значення, що передається, у даному випадку список категорій, які потрібно зберегти;
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
Отриманий рядок jSON потім десеріалізується для отримання очікуваного типу [Response<List<Categorie>]:
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<List<Categorie>>>() {
});
- рядки 11–17: обробка відповіді сервера (помилка чи ні);
- рядки 20–22: обробка винятків;
Усі інші методи побудовані за зразком двох наведених методів.
13.6.4. Тест JUnit
Повернемося до архітектури «клієнт/сервер», яка зараз створюється:
![]() |
Ми створили шар [DAO] [2] з тим самим інтерфейсом, що й шар [DAO] [4]. Отже, для тестування шару [DAO] [2] можна використовувати тест JUnit, який використовувався для тестування шару [DAO] [4]. Нагадуємо, що він має такий вигляд:
![]() |
package spring.client.junit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// шар [DAO]
@Autowired
private IDao dao;
// фільтри jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Before
public void cleanAndFill() {
// очищення бази даних перед кожним тестом
log("Vidage de la base de données", 1);
// очищається таблиця [CATEGORIES] — каскадно очищається таблиця [PRODUITS]
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// таблиці заповнюються
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// додавання категорії — за каскадним принципом товари також будуть вставлені
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// список категорій
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// список товарів
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// деякі перевірки
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
@Test
public void getCategorieByNameWithProduits() {
log("getCategorieByNameWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals(5, categorie1.getProduits().size());
}
@Test
public void getCategorieByNameWithoutProduits() {
log("getCategorieByNameWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals("categorie1", categorie1.getNom());
}
@Test
public void getCategorieByIdWithProduits() {
log("getCategorieByIdWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getId(), categorie2.getId());
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getCategorieByIdWithoutProduits() {
log("getCategorieByIdWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getProduitByNameWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertNotNull(produit.getCategorie());
}
@Test
public void getProduitByNameWithoutCategorie() {
log("getProduitByNameWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertEquals("produit03", produit.getNom());
}
@Test
public void getProduitByIdWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
}
@Test
public void getProduitByIdWithoutCategorie() {
log("getProduitByIdWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
@Test
public void doInsertsInTransaction() {
log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
// виконуємо вставку
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// додавання категорії — товари також будуть додані послідовно
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// перевірки
List<Categorie> categories = dao.getAllCategories();
Assert.assertEquals(2, categories.size());
List<Produit> produits = dao.getAllProduits();
Assert.assertEquals(10, produits.size());
}
@Test
public void updateDataBase() {
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
Set<Produit> produits = categorie1.getProduits();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
}
List<Produit> produits2 = Lists.newArrayList(produits);
produits2 = dao.updateProduits(produits2);
// перевірки
List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
for (Produit produit2 : produits2) {
Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
}
}
@Test
public void addProduits() throws BeansException, JsonProcessingException {
log("Ajout de deux produits de catégorie [categorie0]", 1);
Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
Long idCategorie = categorie0.getId();
Produit p1 = new Produit("x", 1, "");
p1.setIdCategorie(idCategorie);
p1.setCategorie(categorie0);
Produit p2 = new Produit("y", 1, "");
p2.setIdCategorie(idCategorie);
p2.setCategorie(categorie0);
List<Produit> produits = new ArrayList<Produit>();
produits.add(p1);
produits.add(p2);
produits = dao.addProduits(produits);
// перевірка
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- приватні методи
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
private Categorie findCategorieByName(String nom, List<Categorie> categories) {
for (Categorie categorie : categories) {
if (categorie.getNom().equals(nom)) {
return categorie;
}
}
return null;
}
// відображення елемента типу T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// виведення списку елементів типу T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// виводить повідомлення
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// заголовок
System.out.println(String.format("%s : ", title));
// повідомлення
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
Його виконання пройшло успішно і видало такі результати у консолі:
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites :
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy



























































