14. [TD]: Публікація шару [metier] у веб-середовищі
Ключові слова: багатошарова архітектура, Spring, ін’єкція залежностей, веб-сервіс / jSON, клієнт / сервер.
Повернемося до поточної архітектури додатка TD:
![]() |
Ми будемо розвивати цю архітектуру до такої:
![]() |
з метою оприлюднення в Інтернеті інтерфейсу [IMetier] бізнес-шару. Для цього ми будемо дотримуватися методології, описаної в розділі 13.5.
14.1. Support
![]() |
Проєкти цього розділу можна знайти в папці [support / chap-14].
14.2. Проєкт Eclipse для шару [métier]
![]() |
![]() |
14.2.1. Конфігурація Maven
Проєкт шару [métier] — це проєкт Maven, налаштований за допомогою такого файлу [pom.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.elections</groupId>
<artifactId>elections-metier-dao-spring-data</artifactId>
<version>0.1.0</version>
<!-- залежності -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- шар [DAO] -->
<dependency>
<groupId>istia.st.elections</groupId>
<artifactId>elections-dao-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
<!-- Тестування Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<!-- використовувати UTF-8 для всього -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядки 18–22: залежність від шару [DAO], створеного в розділі 12;
- рядки 23–34: залежності, необхідні для тестування;
14.2.2. Конфігурація Spring
![]() |
Проєкт шару [métier] — це проєкт Spring, налаштований за допомогою такого файлу [MetierConfig]:
package elections.metier.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import elections.dao.config.DaoConfig;
@Import({ DaoConfig.class })
@ComponentScan({ "elections.metier.service" })
public class MetierConfig {
}
- тут ми не використовуємо нотацію [@Configuration], яка робить клас класом конфігурації Spring. Наявність анотацій [@Import, @ComponentScan] автоматично робить його класом конфігурації;
- рядок 8: імпортуємо файл конфігурації шару [DAO]. Після цього стають доступними всі біни, визначені в цьому файлі;
- рядок 9: інші біни Spring слід шукати в папці [elections.metier.service];
14.2.3. Реалізація шару [métier]
![]() |
Реалізація шару [métier] відповідає тому, що було визначено в розділі 8.5.
14.2.4. Тестування шару [métier]
![]() |
Тестовий клас відповідає опису, наведеному в розділі 8.6.
Завдання: реалізуйте проект шару [métier] та пройдіть його модульне тестування. Створіть архів шару у локальному репозиторії Maven (run as/ Maven / install).
14.3. Проєкт Eclipse шару [web]
![]() |
Веб-шар — це шар Spring MVC:
![]() |
Проєкт Eclipse має таку структуру:
![]() | ![]() |
- [Boot.java] — це клас, який запускає веб-сервіс;
- [WebConfig.java] — це клас конфігурації веб-сервісу;
- [Response.java] — це відповідь, що надається різними екземплярами URL веб-сервісу;
- [ElectionsController] — клас реалізації веб-сервісу;
14.4. Конфігурація 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.elections</groupId>
<artifactId>elections-webjson-metier-dao-spring-data</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>elections-webjson-metier-dao-spring-data</name>
<description>couche métier exposée comme un service web / jSON</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.elections</groupId>
<artifactId>elections-metier-dao-spring-data</artifactId>
<version>0.1.0</version>
</dependency>
<!-- шар MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- рядки 19–23: залежність від архіву шару [métier]. Це той архів, який ми створили в параграфі 14;
- рядки 25–28: залежність від додатка Spring MVC;
14.5. Конфігурація Spring
![]() |
Клас [WebConfig] налаштовує веб-сервіс:
package elections.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import elections.metier.config.MetierConfig;
@EnableWebMvc
@Import({ MetierConfig.class })
@ComponentScan({ "elections.webjson.service" })
public class WebConfig {
// -------------------------------- конфігурація рівня [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
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- значення цієї конфігурації було наведено в розділі 13.5.3.1. Ми пояснюємо лише нововведення:
- рядок 22: імпортується файл конфігурації шару [métier], щоб отримати доступ до всіх його бінів;
- рядок 23: вказується, що інші компоненти (beans) знаходяться у папці [elections.webjson.server.service];
14.6. Клас запуску веб-сервісу
![]() |
Клас [Boot] запускає веб-сервіс наступним чином:
package elections.webjson.boot;
import org.springframework.boot.SpringApplication;
import elections.webjson.config.WebConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(WebConfig.class, args);
}
}
- рядок 10: статичний метод [SpringApplication.run] використовуватиме файл конфігурації [WebConfig]. Завдяки анотації [@EnableAutoConfiguration] Spring Boot запустить сервер Tomcat і розгорне на ньому веб-сервіс;
14.7. Відповідь веб-сервісу URL
![]() |
Усі URL веб-сервісу / jSON надсилають однаковий тип відповіді:
package elections.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
...
}
Цей клас було представлено та розглянуто в розділі 13.5.5.3.
14.8. Реалізація веб-сервісу / jSON
![]() |
Веб-сервіс / jSON реалізовано за допомогою наступного класу [ElectionsController]:
package elections.webjson.service;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.databind.ObjectMapper;
import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.metier.service.IElectionsMetier;
@Controller
public class ElectionsController {
// залежності Spring
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private IElectionsMetier metier;
@RequestMapping(value = "/getElectionsConfig", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getElectionsConfig() throws JsonProcessingException {
// відповідь
Response<ElectionsConfig> response;
try {
response = new Response<>(0, null,
new ElectionsConfig(metier.getNbSiegesAPourvoir(), metier.getSeuilElectoral()));
} catch (ElectionsException e1) {
response = new Response<>(e1.getCode(), e1.getErreurs(), null);
} catch (RuntimeException e2) {
response = new Response<>(1000, getErreursForException(e2), null);
}
// відповідь
return jsonMapper.writeValueAsString(response);
}
@RequestMapping(value = "/getListesElectorales", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getListesElectorales() throws JsonProcessingException {
throw new UnsupportedOperationException("Not supported yet");
}
@RequestMapping(value = "/setListesElectorales", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String setListesElectorales(HttpServletRequest request) throws JsonProcessingException {
throw new UnsupportedOperationException("Not supported yet");
}
@RequestMapping(value = "/calculerSieges", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String calculerSieges(HttpServletRequest request) throws JsonProcessingException {
throw new UnsupportedOperationException("Not supported yet");
}
// приватні методи -----------------------------
// список повідомлень про помилки RuntimeException
private List<String> getErreursForException(Exception e) {
// отримуємо список повідомлень про помилки винятку
Throwable cause = e;
List<String> erreurs = new ArrayList<>();
while (cause != null) {
// отримуємо повідомлення, лише якщо воно !=null і не порожнє
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// наступна причина
cause = cause.getCause();
}
return erreurs;
}
}
Завдання: слідуючи вказівкам, наведеним у розділі 13.5.5, доповніть код класу [ElectionsController].
Примітки:
- тут немає фільтрів jSON, оскільки таблиці [CONF] та [LISTES] не пов’язані між собою відношенням зовнішнього ключа, що значно спрощує код веб-сервісу;
- не забувайте про різні необхідні анотації Spring;
- URL отримають імена відповідних методів;
- метод [setListeElectorales] викликається з операцією [POST]. Відправлене значення — це масив списків-конкурентів (типу ListeElectorale[]) з їхніми атрибутами [sieges, voix, elimine], які потрібно записати в базу даних. Цей метод повертає тип [Response<Void>] з полем [status=0], якщо помилок не сталося, в іншому випадку — інше значення;
- метод [calculerSieges] викликається з операцією [POST]. Переданим значенням є масив списків кандидатів (типу ListeElectorale[]) з їхніми атрибутами [nom, voix]. Цей метод повертає тип [Response<ListeElectorale[]>], тілом якого є виборчі списки з ініціалізованими полями [sieges, elimine];
14.9. Tests
Після запуску веб-сервісу виконайте наступні тести, щоб переконатися у правильній роботі веб-сервісу за допомогою утиліти [Advanced Rest Client]:
![]() |
Відповідь jSON на попередній запит має такий вигляд: [1]:
![]() |
1 ![]() | 2 ![]() |
У [2] скопіюйте відповідь у буфер обміну, а потім вставте її в будь-який текстовий редактор [3]:
![]() |
Виділіть значення поля [body] і, наприклад, змініть голоси у списках. Нижче [4] ми встановлюємо 100 голосів для всіх списків:
![]() |
Переконайтеся, що ваш рядок jSON починається з [ et se termine par ]. Ці символи слугують для обмеження масиву jSON. У поле [5] вставте наведений вище рядок jSON. Це буде значення, яке буде відправлено для наступного URL. Для цього потрібно вибрати метод HTTP [POST] [7].
![]() |
- у [6] запитуйте URL [setListesElectorales]. Цей URL запитується разом із POST. Значенням, що вноситься, є масив jSON списків учасників, результати яких потрібно записати в базу даних;
Отримуємо такий результат:
![]() |
Поле [status=0] вказує, що помилок не сталося. Щоб переконатися в цьому, знову запишіть списки учасників змагань і перевірте, чи були враховані зміни, які ви внесли до списків:
![]() |
Виконуємо [POST] ще раз, щоб підрахувати кількість місць, отриманих списками:
![]() |
- у [1]: URL з розрахунку місць;
- у [2]: створюємо [POST];
- у [3]: списки кандидатів. У поле [voix] вводяться значення з TD, усі значення в [sieges] встановлюються на 0, усі значення в полях [elimine] встановлюються на false;
Отримано такий результат:
![]() |

























