Skip to content

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;

Отримано такий результат: