14. [TD]: Udostępnienie warstwy [metier] w sieci
Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, usługa internetowa / jSON, klient / serwer.
Wróćmy do obecnej architektury aplikacji TD:
![]() |
Zamierzamy przekształcić tę architekturę w następującą:
![]() |
w celu udostępnienia w sieci interfejsu [IMetier] warstwy biznesowej. W tym celu zastosujemy metodologię opisaną w paragrafie 13.5.
14.1. Support
![]() |
Projekty z tego rozdziału znajdują się w folderze [support / chap-14].
14.2. Projekt Eclipse warstwy [métier]
![]() |
![]() |
14.2.1. Konfiguracja Maven
Projekt warstwy [métier] jest projektem Maven skonfigurowanym za pomocą następującego pliku [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>
<!-- zależności -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- warstwa [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>
<!-- Test Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<!-- użyj UTF-8 do wszystkiego -->
<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>
- wiersze 18–22: zależność od warstwy [DAO] utworzonej w punkcie 12;
- wiersze 23–34: zależności niezbędne do przeprowadzenia testów;
14.2.2. Konfiguracja Spring
![]() |
Projekt warstwy [métier] jest projektem Spring skonfigurowanym przez następujący plik [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 {
}
- Nie stosujemy tutaj notacji [@Configuration], która sprawia, że klasa staje się klasą konfiguracyjną Springa. Obecność adnotacji [@Import, @ComponentScan] automatycznie czyni ją klasą konfiguracyjną;
- wiersz 8: importujemy plik konfiguracyjny warstwy [DAO]. Uzyskujemy w ten sposób dostęp do wszystkich beanów zdefiniowanych w tym pliku;
- wiersz 9: pozostałe komponenty Spring należy pobrać z folderu [elections.metier.service];
14.2.3. Implementacja warstwy [métier]
![]() |
Implementacja warstwy [métier] jest zgodna z opisem w paragrafie 8.5.
14.2.4. Test warstwy [métier]
![]() |
Klasa testowa jest zgodna z opisem zawartym w paragrafie 8.6.
Zadanie do wykonania: zaimplementuj projekt warstwy [métier] i przeprowadź jej test jednostkowy. Wygeneruj archiwum warstwy w lokalnym repozytorium Maven (run as / Maven / install).
14.3. Projekt Eclipse warstwy [web]
![]() |
Warstwa internetowa to warstwa Spring MVC:
![]() |
Projekt Eclipse ma następującą strukturę:
![]() | ![]() |
- [Boot.java] to klasa uruchamiająca usługę internetową;
- [WebConfig.java] to klasa konfiguracyjna serwisu internetowego;
- [Response.java] to odpowiedź generowana przez różne instancje klasy URL serwisu internetowego;
- [ElectionsController] to klasa implementacyjna serwisu internetowego;
14.4. Konfiguracja Maven
Projekt jest projektem Maven skonfigurowanym za pomocą następującego pliku [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>
<!-- warstwa biznesowa -->
<dependency>
<groupId>istia.st.elections</groupId>
<artifactId>elections-metier-dao-spring-data</artifactId>
<version>0.1.0</version>
</dependency>
<!-- warstwa 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>
- wiersze 19–23: zależność od archiwum warstwy [métier]. Jest to archiwum, które utworzyliśmy w punkcie 14;
- wiersze 25–28: zależność od biblioteki Spring o nazwie MVC;
14.5. Konfiguracja Spring
![]() |
Klasa [WebConfig] konfiguruje usługę internetową:
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 {
// -------------------------------- konfiguracja warstwy [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);
}
// mapper jSON
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- znaczenie tej konfiguracji zostało wyjaśnione w paragrafie 13.5.3.1. Wyjaśniamy jedynie nowości:
- wiersz 22: importujemy plik konfiguracyjny warstwy [métier], aby móc korzystać ze wszystkich jej beanów;
- wiersz 23: wskazujemy, że inne komponenty (beans) znajdują się w folderze [elections.webjson.server.service];
14.6. Klasa uruchamiająca usługę internetową
![]() |
Klasa [Boot] uruchamia usługę internetową w następujący sposób:
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);
}
}
- wiersz 10: metoda statyczna [SpringApplication.run] wykorzysta plik konfiguracyjny [WebConfig]. Dzięki adnotacji [@EnableAutoConfiguration] Spring Boot uruchomi serwer Tomcat i wdroży na nim usługę internetową;
14.7. Odpowiedź serwisu internetowego URL
![]() |
Wszystkie URL serwisu internetowego / jSON wysyłają ten sam typ odpowiedzi:
package elections.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- właściwości
// status operacji
private int status;
// ewentualne komunikaty o błędach
private List<String> messages;
// treść odpowiedzi
private T body;
// konstruktory
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// metody pobierające i ustawiające
...
}
Klasa ta została przedstawiona i omówiona w paragrafie 13.5.5.3.
14.8. Implementacja usługi internetowej / jSON
![]() |
Usługa internetowa / jSON jest zaimplementowana przez następującą klasę [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 {
// zależności 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 {
// odpowiedź
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);
}
// odpowiedź
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");
}
// metody prywatne -----------------------------
// lista komunikatów o błędach z wyjątku RuntimeException
private List<String> getErreursForException(Exception e) {
// pobieramy listę komunikatów o błędach z wyjątku
Throwable cause = e;
List<String> erreurs = new ArrayList<>();
while (cause != null) {
// pobieramy komunikat tylko wtedy, gdy !=null i nie jest pusty
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// następna przyczyna
cause = cause.getCause();
}
return erreurs;
}
}
Zadanie: postępując zgodnie z instrukcjami zawartymi w paragrafie 13.5.5, uzupełnij kod klasy [ElectionsController].
Uwagi:
- nie ma tutaj filtrów jSON, ponieważ tabele [CONF] i [LISTES] nie są ze sobą powiązane relacją klucza obcego, co znacznie upraszcza kod serwisu internetowego;
- nie należy zapomnieć o różnych niezbędnych adnotacjach Spring;
- metodom URL nadamy nazwy powiązanych metod;
- metoda [setListeElectorales] jest wywoływana wraz z operacją [POST]. Wartością wysyłaną jest tablica konkurujących list (typu ListeElectorale[]) wraz z ich atrybutami [sieges, voix, elimine], które należy zapisać w bazie danych. Metoda ta zwraca typ [Response<Void>] z polem [status=0], jeśli nie wystąpił żaden błąd, w przeciwnym razie zwraca inną wartość;
- metoda [calculerSieges] jest wywoływana wraz z operacją [POST]. Przesyłaną wartością jest tablica list wyborczych (typu ListeElectorale[]) wraz z ich atrybutami [nom, voix]. Metoda ta zwraca typ [Response<ListeElectorale[]>], którego treścią są listy wyborcze z zainicjowanymi polami [sieges, elimine];
14.9. Tests
Po uruchomieniu serwisu internetowego należy przeprowadzić następujące testy, aby upewnić się, że serwis działa poprawnie przy użyciu narzędzia [Advanced Rest Client]:
![]() |
Odpowiedź jSON na poprzednie żądanie [1] brzmi następująco:
![]() |
1 ![]() | 2 ![]() |
W [2] skopiuj odpowiedź do schowka, a następnie wklej ją do dowolnego edytora tekstu [3]:
![]() |
Wyodrębnij wartość pola [body] i zmień na przykład liczbę głosów na listach. Poniżej, w pliku [4], ustawiono liczbę głosów na wszystkich listach na 100:
![]() |
Sprawdź, czy Twój ciąg znaków jSON zaczyna się od [ et se termine par ]. Znaki te służą do wyznaczenia granic tablicy jSON. W polu [5] wklej powyższy ciąg znaków jSON. Będzie to wartość wysyłana dla następnego pola URL. W tym celu należy wybrać metodę HTTP [POST] [7].
![]() |
- w [6] należy zażądać URL oraz [setListesElectorales]. Ten URL jest żądany wraz z POST. Wartością wprowadzoną jest tabela jSON zawierająca listy zawodników, których wyniki należy zapisać w bazie danych;
Otrzymujemy następujący wynik:
![]() |
Pole [status=0] wskazuje, że nie wystąpił żaden błąd. Aby to sprawdzić, należy ponownie zażądać list konkursowych i upewnić się, że wprowadzone zmiany zostały uwzględnione:
![]() |
Ponownie generujemy [POST], aby obliczyć liczbę mandatów uzyskanych przez listy:
![]() |
- w [1]: URL z obliczeń mandatów;
- w [2]: tworzymy plik [POST];
- w [3]: listy startujące w wyborach. W polu [voix] wpisujemy wartości z pola TD, wszystkie pola [sieges] przyjmują wartość 0, a wszystkie pola [elimine] przyjmują wartość false;
Otrzymany wynik jest następujący:
![]() |

























