13. [Cours]: Udostępnianie bazy danych w sieci WWW za pomocą Springa MVC
Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, usługa internetowa / jSON, klient / serwer
13.1. Support
![]() | ![]() |
Projekty z tego rozdziału znajdują się w folderze [support / chap-13]. Skrypt SQL [dbintrospringdata.sql] pozwala utworzyć bazę danych MySQL niezbędną do przeprowadzenia testów.
13.2. Rola Springa MVC w aplikacji internetowej
Umieśćmy Spring MVC w kontekście tworzenia aplikacji internetowej. Najczęściej będzie ona oparta na architekturze wielowarstwowej, takiej jak poniższa:
![]() |
- warstwa [Web] stanowi warstwę kontaktową z użytkownikiem aplikacji internetowej. Użytkownik ten wchodzi w interakcję z aplikacją internetową za pośrednictwem stron internetowych wyświetlanych w przeglądarce. To właśnie w tej warstwie znajduje się Spring MVC i wyłącznie w tej warstwie;
- warstwa [métier] realizuje reguły biznesowe aplikacji, takie jak obliczanie wynagrodzenia lub faktury. Warstwa ta wykorzystuje dane pochodzące od użytkownika za pośrednictwem warstwy [Web] oraz dane z warstw SGBD za pośrednictwem warstwy [DAO];
- warstwa [DAO] (obiekty dostępu do danych), warstwa [ORM] (mapowanie obiektowo-relacyjne) oraz sterownik JDBC zarządzają dostępem do danych z warstwy SGBD. Warstwa [ORM] stanowi pomost między obiektami obsługiwanymi przez warstwę [DAO] a wierszami i kolumnami tabel w relacyjnej bazie danych. Specyfikacja JPA (Java Persistence API) pozwala na abstrakcję od używanego ORM, o ile ten ostatni implementuje te specyfikacje. Tak będzie w tym przypadku i odtąd będziemy nazywać warstwę ORM warstwą JPA;
- integracja warstw odbywa się za pośrednictwem frameworka Spring;
13.3. Model rozwoju Spring MVC
Spring MVC implementuje model architektury zwany MVC (Model – Widok – Kontroler) w następujący sposób:
![]() |
Przetwarzanie żądania klienta przebiega w następujący sposób:
- żądanie – żądane obiekty URL mają postać http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&.... [Front Controller] wykorzystuje plik konfiguracyjny lub adnotacje Java do „przekierowania” żądania do właściwego kontrolera i właściwej akcji w ramach tego kontrolera. W tym celu wykorzystuje pole [Action] z URL. Pozostała część URL i [/param1/param2/...] składa się z opcjonalnych parametrów, które zostaną przekazane do akcji. C w MVC to w tym przypadku ciąg znaków [Front Controller, Contrôleur, Action]. Jeśli żaden kontroler nie jest w stanie obsłużyć żądanej akcji, serwer WWW odpowie, że żądana akcja URL nie została znaleziona.
- Przetwarzanie
- wybrana akcja może wykorzystać parametry parami, które przekazał jej [Front Controller]. Mogą one pochodzić z kilku źródeł:
- ze ścieżki [/param1/param2/...] pliku URL,
- z parametrów [p1=v1&p2=v2] z URL,
- z parametrów przesłanych przez przeglądarkę wraz z żądaniem;
- podczas przetwarzania żądania użytkownika akcja może wymagać warstwy [métier] [2b]. Po przetworzeniu żądania klienta może ono wywołać różne odpowiedzi. Klasycznym przykładem jest:
- strona błędu, jeśli żądanie nie mogło zostać poprawnie przetworzone
- w przeciwnym razie strona potwierdzenia
- akcja żąda wyświetlenia określonego widoku [3]. Widok ten wyświetli dane, które nazywamy modelem widoku. To właśnie litera M w MVC. Akcja utworzy ten model M [2c] i zażąda wyświetlenia widoku V [3];
- odpowiedź – wybrany widok V wykorzystuje model M utworzony przez akcję do zainicjowania dynamicznych części odpowiedzi HTML, którą musi wysłać do klienta, a następnie wysyła tę odpowiedź.
W przypadku usługi internetowej / jSON powyższa architektura ulega niewielkiej modyfikacji:
![]() |
- w [4a] model, który jest klasą Java, jest przekształcany w ciąg znaków jSON przez bibliotekę jSON;
- w [4b] ten ciąg znaków jSON jest wysyłany do przeglądarki;
Teraz wyjaśnijmy związek między architekturą internetową MVC a architekturą warstwową. W zależności od tego, jak zdefiniujemy ten model, te dwie koncepcje mogą być ze sobą powiązane lub nie. Weźmy na przykład jednowarstwową aplikację internetową Spring MVC:
![]() |
Jeśli zaimplementujemy warstwę [Web] przy użyciu Springa MVC, otrzymamy wprawdzie architekturę internetową MVC, ale nie będzie to architektura wielowarstwowa. W tym przypadku warstwa [web] zajmie się wszystkim: prezentacją, logiką biznesową oraz dostępem do danych. Zadania te będą realizowane przez akcje.
Rozważmy teraz wielowarstwową architekturę internetową:
![]() |
Warstwa [Web] może zostać zaimplementowana bez frameworku i bez stosowania się do modelu MVC. Mamy więc architekturę wielowarstwową, ale warstwa internetowa nie implementuje modelu MVC.
Na przykład w środowisku .NET warstwa [Web]może być zaimplementowana przy użyciu ASP.NET i MVC, co daje architekturę warstwową z warstwą [Web] typu MVC. Po wykonaniu tej czynności można zastąpić tę warstwę ASP.NET MVC klasyczną warstwą ASP.NET (WebForms), zachowując jednocześnie pozostałe elementy (biznesową, DAO, ORM) bez zmian. Otrzymujemy wówczas architekturę warstwową z warstwą [Web], która nie jest już typu MVC.
W MVC stwierdziliśmy, że model M odpowiada widokowi V, c.a.d, czyli zbiorowi danych wyświetlanych przez widok V. Podano kolejną definicję modelu M dla MVC:
![]() |
Wielu autorów uważa, że to, co znajduje się po prawej stronie warstwy [Web], tworzy model M dla MVC. Aby uniknąć niejasności, można mówić o:
- o modelu domeny, gdy mamy na myśli wszystko, co znajduje się po prawej stronie warstwy [Web]
- o modelu widoku, gdy mamy na myśli dane wyświetlane przez widok V
W dalszej części termin „model M” będzie odnosił się wyłącznie do modelu widoku V.
13.4. Projekt internetowy / jSON z wykorzystaniem Springa MVC
Strona [http://spring.io/guides] oferuje samouczki dla początkujących, pozwalające zapoznać się z ekosystemem Spring. Skorzystamy z jednego z nich, aby poznać konfigurację Maven niezbędną dla projektu Spring MVC.
13.4.1. Projekt demonstracyjny
![]() |
- w [1] importujemy jeden z przewodników Springa;
![]() |
- w [2] wybieramy przykład [Rest Service];
- w [3] wybieramy projekt Maven;
- w [4] wybieramy ostateczną wersję przewodnika;
- w [5] zatwierdzamy;
- w [6] – zaimportowany projekt;
Usługi internetowe dostępne za pośrednictwem standardu URL i dostarczające tekst jSON są często nazywane usługami REST (REpresentational State Transfer). Usługa jest określana jako RESTful, jeśli spełnia określone zasady.
Przyjrzyjmy się teraz zaimportowanemu projektowi, a najpierw jego konfiguracji Maven.
13.4.2. Konfiguracja Maven
Plik [pom.xml] ma następującą treść:
<?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>
- wiersze 6–8: właściwości projektu Maven. Brakuje tagu [<packaging>] określającego typ pliku generowanego przez kompilację Maven. W przypadku jego braku stosowany jest typ [jar]. Aplikacja jest zatem aplikacją konsolową, a nie aplikacją internetową, w przypadku której pakiet miałby typ [war];
- wiersze 10–14: projekt Maven ma projekt nadrzędny o nazwie [spring-boot-starter-parent]. To właśnie on definiuje większość zależności projektu. Mogą one być wystarczające, w którym to przypadku nie dodaje się żadnych nowych, lub mogą być niewystarczające, w którym to przypadku dodaje się brakujące zależności;
- wiersze 17–20: artefakt [spring-boot-starter-web] zawiera biblioteki niezbędne dla projektu Spring o nazwie MVC, będącego serwisem internetowym, w którym nie generowane są widoki. Ten artefakt zawiera bardzo dużą liczbę bibliotek, w tym biblioteki wbudowanego serwera Tomcat. To właśnie na tym serwerze będzie uruchamiana aplikacja;
Biblioteki dostarczane w ramach tej konfiguracji są bardzo liczne:
![]() | ![]() |
Powyżej widoczne są trzy archiwa serwera Tomcat.
13.4.3. Architektura serwisu Spring [web / jSON]
W przypadku serwisu internetowego / jSON, Spring MVC implementuje model MVC w następujący sposób:
![]() |
- w [4a] model, który jest klasą Java, jest przekształcany w ciąg znaków jSON przez bibliotekę jSON;
- do [4b], ten ciąg znaków jSON jest wysyłany do przeglądarki;
13.4.4. Kontroler C
![]() |
Zaimportowana aplikacja posiada następujący kontroler:
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));
}
}
- wiersz 9: adnotacja [@RestController] sprawia, że klasa [GreetingController] staje się kontrolerem Springa, co oznacza, że jej metody są zarejestrowane do obsługi obiektów typu URL. Widzieliśmy już podobną adnotację [@Controller]. Wynikiem działania metod tego kontrolera był typ [String], który stanowił nazwę widoku do wyświetlenia. Tutaj jest inaczej. Metody kontrolera typu [@RestController] zwracają obiekty, które są serializowane w celu wysłania do przeglądarki. Typ przeprowadzanej serializacji zależy od konfiguracji Springa MVC. W tym przypadku będą one serializowane do formatu jSON. To właśnie obecność biblioteki jSON wśród zależności projektu sprawia, że Spring Boot, poprzez autokonfigurację, skonfiguruje projekt w ten sposób;
- wiersz 14: adnotacja [@RequestMapping] wskazuje na URL, który jest przetwarzany przez metodę, w tym przypadku URL [/greeting];
- wiersz 15: wyjaśniliśmy już adnotację [@RequestParam]. Wynikiem zwracanym przez tę metodę jest obiekt typu [Greeting].
- wiersz 12: liczba całkowita typu long o charakterze atomowym. Oznacza to, że obsługuje ona współbieżność dostępu. Kilka wątków może jednocześnie próbować zwiększyć wartość zmiennej [counter]. Odbędzie się to poprawnie. Wątek może odczytać wartość licznika dopiero wtedy, gdy wątek dokonujący modyfikacji zakończył już swoją operację.
13.4.5. Model M
Model M wygenerowany przez poprzednią metodę to następujący obiekt [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;
}
}
Transformacja jSON tego obiektu utworzy ciąg znaków {"id":n,"content":"tekst"}. Ostatecznie ciąg jSON wygenerowany przez metodę kontrolera będzie miał postać:
lub
13.4.6. Wykonanie
![]() |
Klasa [Application.java] jest klasą wykonywalną projektu. Jej kod wygląda następująco:
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);
}
}
Ten kod omówiliśmy już w poprzednim przykładzie.
13.4.7. Uruchomienie projektu
Uruchommy projekt:
![]() |
Otrzymujemy następujące logi konsoli:
- wiersz 13: serwer Tomcat uruchamia się na porcie 8080 (wiersz 12);
- wiersz 17: serwlet [DispatcherServlet] jest obecny;
- wiersz 20: wykryto metodę [GreetingController.greeting];
Aby przetestować aplikację internetową, wysyłamy żądanie do serwletu URL [http://localhost:8080/greeting]:
![]() | ![]() |
Otrzymujemy oczekiwany ciąg znaków jSON. Warto przyjrzeć się nagłówkom HTTP wysyłanym przez serwer. W tym celu użyjemy rozszerzenia do przeglądarki Chrome o nazwie [Advanced Rest Client] (Chrome / Ctrl-T / Menu [Applications] / [Advanced Rest Client] – patrz załączniki, punkt 22.5):
![]() |
- w [1], żądany plik URL;
- w [2] zastosowano metodę GET;
- w [3] – odpowiedź jSON;
- w przypadku [4] serwer poinformował, że wysyła odpowiedź w formacie jSON;
- w [5] żądana jest ta sama odpowiedź URL, ale tym razem z POST;
- w [7] informacje są wysyłane do serwera w postaci [urlencoded];
- w [6] – parametr „name” wraz z jego wartością;
- w [8] przeglądarka informuje serwer, że przesyła mu dane [urlencoded];
- w [9] – odpowiedź serwera jSON;
13.4.8. Tworzenie archiwum wykonywalnego
Teraz tworzymy archiwum wykonywalne:
![]() |
![]() |
- w [1]: uruchamiamy cel Maven;
- w [2]: występują dwa cele (goals): [clean] w celu usunięcia folderu [target] z projektu Maven oraz [package] w celu jego ponownego wygenerowania;
- w [3]: wygenerowany folder [target] zostanie umieszczony w tym folderze;
- w [4]: generowany jest cel;
W logach wyświetlanych w konsoli ważne jest, aby pojawiła się wtyczka [spring-boot-maven-plugin]. To właśnie ona generuje archiwum wykonywalne.
W konsoli przechodzimy do wygenerowanego folderu:
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
...
- wiersz 5: wygenerowany plik archiwum;
Archiwum to uruchamia się w następujący sposób:
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)
...
Teraz, gdy aplikacja internetowa została uruchomiona, można ją wywołać za pomocą przeglądarki:
![]() |
13.4.9. Wdrożenie aplikacji na serwerze Tomcat
Podobnie jak w przypadku poprzedniego projektu, modyfikujemy plik [pom.xml] w następujący sposób:
<?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>
- wiersz 9: należy wskazać, że zamierzamy wygenerować archiwum WAR (Web ARchive);
Należy również skonfigurować aplikację internetową. W przypadku braku pliku [web.xml] odbywa się to za pomocą klasy dziedziczącej po [SpringBootServletInitializer]:
![]() |
Klasa [ApplicationInitializer] ma następującą postać:
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);
}
}
- wiersz 6: klasa [ApplicationInitializer] rozszerza klasę [SpringBootServletInitializer];
- wiersz 9: metoda [configure] została ponownie zdefiniowana (wiersz 8);
- wiersz 10: podaje się klasę, która konfiguruje projekt;
Aby uruchomić projekt, można postępować w następujący sposób:
![]() |
- w [1-2] uruchamia się projekt na jednym z serwerów zarejestrowanych w IDE Eclipse;
Po wykonaniu tej czynności można wywołać URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] w przeglądarce:
![]() |
13.4.10. Wnioski
Przedstawiliśmy rodzaj projektów Spring, w których aplikacja internetowa wysyła strumień do przeglądarki. Teraz opracujemy aplikację internetową / jSON w celu udostępnienia w sieci bazy danych [dbintrospringdata] omówionej w samouczku [Introduction à Spring Data].
13.5. Udostępnienie bazy danych [dbintrospringdata] w sieci
13.5.1. Architektura serwisu internetowego / jSON
Wdrożymy następującą architekturę:
![]() |
Warstwy [DAO] i [JPA] są zaimplementowane przez aplikację opisaną w samouczku [Introduction à Spring Data].
13.5.2. Instalacja bazy danych
![]() |
Skrypt SQL [dbintrospringdata.sql] umożliwia utworzenie bazy danych MySQL niezbędnej do przeprowadzenia testów.
13.5.3. Projekt Eclipse serwisu internetowego / jSON
Projekt Eclipse usługi internetowej / jSON wygląda następująco:
![]() |
Jest to projekt Maven, którego plik [pom.xml] wygląda następująco:
<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>
- wiersze 11–15: nadrzędny projekt Maven, już wykorzystywany dla warstwy [DAO];
- wiersze 18–22: zależność od warstwy [DAO];
- wiersze 23–26: zależność od artefaktu [spring-boot-starter-web]. Artefakt ten zawiera wszystkie zależności niezbędne do utworzenia serwisu internetowego / jSON. Zawiera on również zbędne biblioteki. Konieczna byłaby zatem bardziej precyzyjna konfiguracja. Jednak ta konfiguracja jest przydatna na początek;
- wiersze 28–30: zależność od artefaktu [spring-boot-starter] umożliwia obsługę adnotacji Spring Boot;
Zależności wynikające z tej konfiguracji są następujące:
![]() |
- w [1] widać, że Eclipse wykryło zależność od archiwum projektu [intro-spring-data-01];
Powyższe zależności dotyczą zarówno warstwy [DAO], jak i warstwy [web].
13.5.3.1. Konfiguracja warstwy [web]
Warstwa [web] jest konfigurowana za pomocą pliku [AppConfig]:
![]() |
Klasa [WebConfig] konfiguruje warstwę [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 {
// -------------------------------- 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);
}
// filtry jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapowanie jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// wynik
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// wynik
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// wynik
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// wynik
return mapper;
}
}
- wiersz 18: adnotacja [@EnableWebMvc] powoduje automatyczne skonfigurowanie frameworka Spring MVC;
- wiersz 19: klasa [WebConfig] rozszerza klasę Spring [WebMvcConfigurerAdapter] w celu przedefiniowania niektórych beanów (wiersze 26–40);
- wiersze 22–23: wstrzyknięcie kontekstu Spring;
- wiersze 25–29: definicja serwletu frameworka Spring MVC, który przekierowuje żądania HTTP do właściwego kontrolera i właściwej metody. [DispatcherServlet] jest klasą Springa;
- wiersze 31–34: wskazano, że ten serwlet obsługuje wszystkie żądania URL;
- wiersze 36–39: to właśnie obecność tej fasoli spowoduje uruchomienie serwera Tomcat znajdującego się w archiwach projektu. Będzie on oczekiwał na żądania na porcie 8080;
- wiersze 42–91: fasole, które będą wykorzystywane do zarządzania filtrami jSON;
- wiersze 42–45: mapper jSON bez filtrów;
- wiersze 47–57: mapper jSON, który umożliwia wyświetlenie kategorii wraz z produktami. Należy zauważyć, że gdy żądamy kategorii wraz z jej produktami, należy skonfigurować zarówno filtr jSON klasy [Categorie], jak i filtr klasy [Produit]. Tak jest zawsze. Podczas serializacji/deserializacji klasy w formacie jSON należy skonfigurować filtr jSON tej klasy oraz filtry wszystkich zależności, które mają zostać w niej uwzględnione;
- wiersze 59–69: mapper jSON, który pozwala na wyświetlenie produktu wraz z jego kategorią;
- wiersze 71–80: mapper jSON, który pozwala uzyskać kategorię bez produktów;
- wiersze 82–91: mapper jSON, który pozwala wyświetlić produkt bez kategorii;
Klasa [AppConfig] konfiguruje całą aplikację, tj. warstwy [web] i [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 {
}
- wiersz 9: importujemy bean z warstwy [DAO] oraz z warstwy [web];
- wiersz 8: wskazuje, w których pakietach znajdują się inne komponenty Spring;
Należy zauważyć, że nigdzie nie użyliśmy adnotacji [@EnableAutoConfiguration]. Zdecydowaliśmy się samodzielnie kontrolować konfigurację.
13.5.4. Model aplikacji
![]() |
Klasa [ApplicationModel] wygląda następująco:
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 {
// warstwa [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);
}
}
- wiersz 12: klasa jest singletonem Springa;
- wiersz 13: która implementuje interfejs [IDao] warstwy [DAO];
- wiersze 16–17: wstrzyknięcie odwołania do warstwy [DAO];
- wiersze 19–99: implementacja interfejsu [IDao];
Architektura warstwy internetowej zmienia się w następujący sposób:
![]() |
- w [2b] metody kontrolera lub kontrolerów komunikują się z singletonem [ApplicationModel];
Strategia ta zapewnia elastyczność w zarządzaniu ewentualną pamięcią podręczną. Klasa [ApplicationModel] może służyć do przechowywania informacji uzyskanych z warstwy [DAO] lub danych konfiguracyjnych. Może to być przydatne, gdy nie mamy kontroli nad warstwą [DAO]. Ta strategia buforowania może ulegać zmianom w miarę upływu czasu. Zmiany te nie będą miały żadnego wpływu na kod kontrolera lub kontrolerów.
13.5.5. Kontroler
![]() |
![]() |
Mamy tu tylko jeden kontroler, klasę [MyController].
13.5.5.1. Eksponowane obiekty URL
Klasy URL udostępniane przez ten kontroler to:
| Dodaje produkty do bazy danych. Zostają one zapisane. Odpowiedzią jest ciąg znaków jSON zawierający listę dodanych produktów wraz z ich kluczami głównymi. |
| Usuwa wszystkie produkty z bazy danych. |
| Aktualizuje produkty w bazie danych. Zostają one opublikowane. Odpowiedzią jest ciąg znaków jSON z listy zaktualizowanych produktów. |
| Pobiera ciąg znaków jSON dla wszystkich produktów. |
| Dodaje kategorie do bazy danych. Są one publikowane. Odpowiedzią jest ciąg znaków jSON zawierający listę dodanych kategorii wraz z ich kluczami głównymi. Jeśli kategorie zawierają produkty, są one również dodawane do bazy danych. |
| Usuwa wszystkie kategorie z bazy danych wraz ze wszystkimi produktami w nich zawartymi. Po wykonaniu tej operacji baza danych jest pusta. |
| Aktualizuje kategorie w bazie danych. Są one publikowane. Odpowiedzią jest lista zaktualizowanych kategorii. Jeśli kategorie zawierają produkty, są one również aktualizowane w bazie danych. Zwraca ciąg znaków jSON zawierający zmodyfikowane kategorie; |
| Pobiera ciąg znaków jSON ze wszystkich kategorii. |
| Pobiera ciąg znaków jSON dla produktu określonego przez jego identyfikator wraz z kategorią. |
| Pobiera ciąg znaków jSON dla produktu określonego przez jego identyfikator, bez podawania kategorii. |
| Pobiera ciąg znaków jSON dla produktu określonego przez jego nazwę wraz z kategorią. |
| Pobiera ciąg znaków jSON dla produktu określonego przez jego nazwę, bez podawania kategorii. |
| Pobiera ciąg znaków jSON z kategorii określonej przez jej identyfikator wraz z jej produktami. |
| Pobiera ciąg znaków jSON z kategorii określonej jej nazwą wraz z jej produktami. |
| Pobiera ciąg znaków jSON z kategorii określonej jej nazwą, bez produktów z tej kategorii. |
| Pobiera ciąg znaków jSON z kategorii określonej przez jej identyfikator, bez produktów. |
Przedstawione URL odpowiadają metodom interfejsu [IDao] warstwy [DAO]. Wszystkie metody serwisu internetowego / jSON są oparte na tym samym wzorcu. Przyjrzyjmy się kilku z nich.
13.5.5.2. Szkielet kontrolera
Szkielet kontrolera wygląda następująco:
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 {
// zależności Spring
@Autowired
private ApplicationModel application;
// filtry 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;
// klasa [MyController] jest singletonem i jest instancjonowana tylko raz po utworzeniu bean
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 {
...
}
- wiersz 28: adnotacja [@Controller] sprawia, że klasa staje się komponentem Spring;
- wiersze 32–33: wstrzyknięcie odniesienia do klasy [ApplicationModel];
- wiersze 36–50: wstrzykiwanie odwołań do mapperów jSON;
- wiersz 58: eksponowaną metodą URL jest [/addProduits]. Klient musi użyć metody [POST], aby wysłać żądanie (method = RequestMethod.POST). Musi wysłać wartość w postaci ciągu znaków jSON (consumes = "application/json; charset=UTF-8"). Metoda sama zwraca odpowiedź do klienta (wiersz 59). Będzie to ciąg znaków (wiersz 60). Nagłówek HTTP [Content-type : application/json; charset=UTF-8] zostanie wysłany do klienta, aby poinformować go, że otrzyma ciąg znaków jSON (wiersz 58);
- wiersz 60: metoda [addProduits] zwraca ciąg znaków jSON z listy produktów dodanych do bazy;
13.5.5.3. Odpowiedź metod kontrolera
Wszystkie metody kontrolera zwracają następującą odpowiedź typu [Response]:
![]() |
package spring.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
...
}
- wiersz 5: odpowiedź zawiera typ T;
- wiersz 13: odpowiedź typu T;
- wiersze 9–11: może się zdarzyć, że metoda napotka wyjątek. W takim przypadku zwróci odpowiedź zawierającą:
- wiersz 9: status!=0;
- wiersz 11: lista napotkanych błędów;
13.5.5.4. L'URL [/addProduits]
L'URL [/addProduits] jest przetwarzana przez następującą metodę:
@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 {
// odpowiedź
Response<List<Produit>> response;
try {
// pobieramy przesłaną wartość
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// przywracamy powiązania między produktami a kategoriami
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// zapisujemy produkty
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);
}
// odpowiedź jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- wiersz 3: metoda przyjmuje jako parametr [HttpServletRequest request], który zawiera wszystkie informacje dotyczące żądania klienta;
- wiersz 5: odpowiedź, która zostanie wysłana do klienta: lista produktów;
- wiersz 8: pobieramy przesłaną wartość. Klasa [CharStreams] należy do biblioteki [Google Guava], do której dodaliśmy odwołanie w pliku [pom.xml]. Otrzymujemy ciąg znaków jSON przesłany przez klienta. Należy go deserializować, aby móc z niego skorzystać;
- wiersze 8–10: przeprowadzono deserializację. Otrzymujemy listę produktów, gdzie każdy produkt posiada pole [categorie=null];
- wiersze 12–14: resetujemy pole [categorie] wszystkich produktów z listy. W tym celu wykorzystujemy pole [idCategorie] produktu, które jest zainicjowane;
- wiersz 16: produkty są wstawiane do bazy danych;
- wiersz 17: obiekt [response] jest inicjowany listą produktów;
- wiersze 18–19: sytuacja, w której metoda napotyka wyjątek warstwy [DAO]. Odpowiedź jest inicjowana wartością [status=1000] (kod błędu) [messages=e1.getMessages()], co oznacza, że do klienta przekazywana jest lista błędów wystąpionych po stronie serwera;
- wiersze 20–21: przypadek, w którym metoda napotyka inny rodzaj wyjątku. Inicjuje się odpowiedź kodem błędu [status=1000] [messages=getErreursForException(e)], gdzie [getErreursForException] jest prywatną metodą klasy, która zwraca listę błędów związanych z wyjątkami ze stosu wyjątków e, oraz [body=null];
- wiersz 24: zwracany jest ciąg znaków jSON z odpowiedzi;
13.5.5.5. URL [/getAllProduits]
URL [/getAllProduits] jest przetwarzane przez następującą metodę:
@RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// odpowiedź
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);
}
// odpowiedź jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- wiersz 1: żądanie dotyczące URL [/getAllProduits] jest wysyłane za pomocą operacji [GET]. Generuje ono jSON;
- wiersz 2: metoda sama wysyła odpowiedź jSON do klienta;
- wiersz 5: metoda wysyła ciąg znaków jSON typu [Response<List<Produit>>];
- wiersz 7: produkty są żądane bez podania ich kategorii;
- wiersze 8–12: w przypadku błędu odpowiedź jest inicjowana kodem i komunikatami o błędzie;
- wiersz 14: odpowiedź o typie jSON jest wysyłana do klienta;
13.5.5.6. Conclusion
Nie będziemy omawiać pozostałych metod kontrolera. Są one podobne do jednej z dwóch metod, które właśnie przedstawiliśmy.
13.5.6. Klasa wykonawcza serwisu internetowego / jSON
![]() |
Klasa [Boot] jest klasą wykonawczą projektu:
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);
}
}
- wiersz 10: wykonywana jest metoda statyczna [SpringApplication.run]. Klasa [SpringApplication] jest klasą projektu [Spring Boot] (wiersz 3). Przekazywane są do niej dwa parametry:
- [AppConfig.class]: klasa konfigurująca całą aplikację;
- [args]: ewentualne argumenty przekazane do metody [main] w wierszu 9. Ten parametr nie jest tutaj wykorzystywany;
Po uruchomieniu tej klasy otrzymujemy następujące logi:
- wiersze 17–19: uruchomienie serwera Tomcat, który będzie obsługiwał usługę internetową / jSON;
- wiersze 25–33: tworzenie warstwy [DAO];
- wiersze 32–51: wykryto ujawnione elementy URL;
13.5.7. Testy serwisu internetowego / jSON
Aby przeprowadzić testy, generujemy bazę danych MySQL [dbintrospringdata] na podstawie skryptu SQL [dbintrospringdata.sql]:
![]() |
Następnie wykorzystujemy klienta [Advanced Rest Client] (patrz punkt 22.5) do wysyłania zapytań do zasobów URL udostępnianych przez serwis internetowy / jSON (usługa internetowa / jSON musi być uruchomiona).
![]() |
- w [1-3] żądamy URL [/getAllCategories] za pomocą polecenia HTTP GET;
Otrzymujemy następującą odpowiedź:
![]() |
- w [1], żądanie klienta HTTP;
- w [2], odpowiedź serwera HTTP;
- w [3] status [200 OK] wskazuje, że serwer poprawnie przetworzył żądanie;
- w [4] – odpowiedź serwera jSON;
Pełna odpowiedź jSON wygląda następująco:
{"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 oznacza, że po stronie serwera nie wystąpiły żadne błędy;
- messages: null oznacza, że nie ma komunikatów o błędach;
- treść: to treść odpowiedzi, w tym przypadku lista kategorii wraz z produktami. Istnieją dwie kategorie, z których każda zawiera 5 produktów;
Dodamy do kategorii [categorie1] produkt [produit15]. W tym celu wykorzystamy URL [/addCategories], który ma następujący kod:
@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);
// zachowuje się kategorie
try {
// pobieramy zapisaną wartość
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// przywracamy powiązania między produktami a kategoriami
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// zapisujemy kategorie
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// odpowiedź jSON
return mapper.writeValueAsString(response);
}
- wiersz 1: klient musi utworzyć POST, a wysyłana wartość musi być ciągiem znaków jSON;
- wiersze 9–12: wartość wysyłana musi być listą kategorii wraz z powiązanymi z nimi produktami;
Utworzymy kategorię o nazwie [categorie2] wraz z produktem o nazwie [produit21]. Ciąg znaków jSON, który należy wysłać, będzie zatem następujący:
[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]
Żądanie do serwisu internetowego /jSON jest formułowane w następujący sposób:
![]() |
- w przypadku [1] – żądany URL;
- w [2] jest ona żądana poprzez operację POST;
- w [3] wysłano ciąg jSON;
- w [4] informuje się serwer, że zostanie mu wysłany ciąg jSON;
Odpowiedź serwera jest następująca:
![]() |
- w [1] widać, że zarówno kategoria, jak i produkt mają teraz klucz główny, co wskazuje, że prawdopodobnie zostały wstawione do bazy danych. Sprawdzimy to, korzystając z URL i [/getCategorieByNameWithProduits/categorie2]:
![]() |
Otrzymujemy następujący wynik:
![]() |
Rzeczywiście uzyskaliśmy kategorię [categorie2] wraz z jej jedynym produktem [produit21]. Można również wyszukać wyłącznie produkt. W tym celu użyjmy URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
Otrzymujemy następujący wynik:
![]() |
Wszystkie operacje [GET] można wykonać w zwykłej przeglądarce:
![]() |
Zachęcamy czytelnika do przetestowania pozostałych operacji URL serwisu internetowego / json.
13.6. Klient zaprogramowany dla serwisu internetowego / jSON
Teraz, gdy baza danych [dbintrospringdata] jest dostępna w sieci, napiszemy aplikację, która z niej korzysta. Uzyskamy wówczas następującą architekturę klient-serwer:
![]() |
Aplikacja kliencka będzie miała dwie warstwy:
- warstwę [DAO] [2] do komunikacji z aplikacją internetową / jSON, która udostępnia bazę danych;
- warstwę testową JUnit [1], służącą do sprawdzania, czy klient i serwer działają prawidłowo;
13.6.1. Projekt Eclipse
Projekt Eclipse klienta wygląda następująco:
![]() |
- folder [src/main/java] implementuje warstwę [DAO];
- folder [src/test/java] implementuje testy JUnit;
13.6.2. Konfiguracja projektu w Maven
Projekt jest projektem Maven skonfigurowanym przez następujący plik [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>
<!-- biblioteka jSON wykorzystywana przez 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>
<!-- komponent używany przez 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>
<!-- biblioteka logów -->
<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>
<!-- wtyczki -->
<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>
- wiersze 14–18: nadrzędny projekt Maven [spring-boot-starter-parent], który pozwala nam zdefiniować pewną liczbę zależności bez podawania ich wersji, ponieważ są one określone w projekcie nadrzędnym;
- wiersze 22–25: chociaż nie tworzymy aplikacji internetowej, potrzebujemy zależności [spring-web], która zawiera klasę [RestTemplate], umożliwiającą łatwą integrację z aplikacją internetową / jSON;
- wiersze 27–34: biblioteka jSON;
- wiersze 36–39: zależność, która pozwoli nam przypisać timeout do żądań HTTP wysyłanych przez klienta. timeout to maksymalny czas oczekiwania na odpowiedź serwera. Po upływie tego czasu klient zgłasza błąd timeout, generując wyjątek;
- wiersze 41–46: biblioteka Google Guava używana w teście JUnit. Z tego powodu ograniczyliśmy jej zakres do [test] (wiersz 45). Oznacza to, że ta zależność jest dołączana wyłącznie podczas wykonywania kodu z gałęzi [src/test/java];
- wiersze 48–51: biblioteka logów;
- wiersze 52–63: zależność testowa JUnit. Dostarcza ona w szczególności bibliotekę JUnit 4, niezbędną do przeprowadzenia testów. Zależności te mają atrybut [<scope>test</scope>] wskazujący, że są one potrzebne wyłącznie na etapie testów. Nie są one dołączane do ostatecznego archiwum projektu;
13.6.3. Implementacja warstwy [DAO]
![]() |
![]() |
- pakiet [spring.client.config] zawiera konfigurację Spring dla warstwy [DAO];
- pakiet [spring.client.dao] zawiera implementację warstwy [DAO];
- pakiet [spring.client.entities] zawiera obiekty wymieniane z usługą internetową / jSON;
13.6.3.1. Configuration
![]() |
Klasa [DaoConfig] odpowiada za konfigurację warstwy [DAO] w Spring. Jej kod wygląda następująco:
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 {
// stałe
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// tworzenie komponentu RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// limit czasu wymiany danych
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// wynik
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// filtry jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// wynik
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// wynik
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// wynik
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filtry
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// wynik
return mapper;
}
}
- wiersz 13: klasa jest klasą konfiguracyjną Springa – komponenty Springa należy szukać w pakiecie [spring.client.dao];
- wiersz 17: ustawia się timeout na jedną sekundę (1000 ms);
- wiersze 32–35: bean zwracający tę wartość;
- wiersz 18: URL usługi internetowej / jSON;
- wiersze 37–40: bean zwracający tę wartość;
- wiersze 20–30: konfiguracja klasy [RestTemplate], która zapewnia komunikację z serwisem internetowym / jSON. Gdy nie ma potrzeby jej konfigurowania, można ją wykorzystać w kodzie poprzez proste użycie [new RestTemplate()]. W tym przypadku chcemy ustawić timeout dla komunikacji z serwisem internetowym / jSON. Bean [timeout] z wiersza 36 jest przekazywany jako parametr do metody [restTemplate] z wiersza 24;
- wiersz 23: komponent [HttpComponentsClientHttpRequestFactory] umożliwia nam ustalenie wartości timeout w ramach wymiany danych (wiersze 29–30);
- wiersz 24: klasa [RestTemplate] jest tworzona przy użyciu tego komponentu. Ponieważ opiera się ona na tym komponencie w celu komunikacji z usługą internetową / jSON, wymiana danych będzie podlegać timeout;
- klient i serwer będą wymieniać między sobą wiersze tekstu. Konwerter zajmuje się serializacją obiektu do postaci tekstowej oraz, odwrotnie, deserializacją tekstu do postaci obiektu. Z klasą [RestTemplate] może być powiązanych kilka konwerterów, a wybór konkretnego konwertera w danym momencie zależy od nagłówków HTTP wysyłanych przez serwer. W tym przypadku nie będziemy mieli żadnego konwertera. W związku z tym komponent [RestTemplate] nie będzie próbował w żaden sposób konwertować dwóch następujących elementów:
- wysłanego tekstu;
- tekst otrzymany w odpowiedzi;
Teksty te będą ciągami znaków jSON, które komponent [RestTemplate] pozostawi w niezmienionej postaci. To my, programiści, zajmiemy się niezbędną serializacją i deserializacją jSON. Wynika to z faktu, że filtry, które należy zastosować do wysłanej wartości i otrzymanej odpowiedzi, mogą się różnić, a doświadczenie pokazuje, że łatwiej jest zarządzać nimi samodzielnie niż próbować skonfigurować komponent [RestTemplate] tak, aby używał właściwego konwertera jSON;
- wiersze 42–92: definiują filtry jSON. Są to te same filtry, co na serwerze, przedstawione i wyjaśnione w paragrafie 13.5.3.1;
- wiersze 43–46: mapper jSON bez filtrów;
- wiersze 64–68: mapper jSON, aby uzyskać kategorię bez produktów;
- wiersze 48–58: mapper jSON, aby uzyskać kategorię wraz z jej produktami;
- wiersze 83–92: mapper jSON, aby uzyskać produkt bez kategorii;
- wiersze 60–70: mapper jSON, aby uzyskać produkt wraz z kategorią;
Wszystkie te obiekty typu „bean” będą dostępne w kodach warstwy [DAO], a także w teście Junit.
13.6.3.2. Elementy
![]() |
Entytety obsługiwane przez warstwę [DAO] to te, które są wymieniane z serwisem internetowym / jSON. Są to artykuły i produkty. Po stronie serwera encje te posiadały adnotacje trwałości JPA. W tym przypadku adnotacje te zostały usunięte. Dla przypomnienia podajemy kod encji:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractEntity {
// właściwości
protected Long id;
protected Long version;
// konstruktorzy
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// przeformułowanie [equals] i [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();
}
// podpis jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// metody pobierające i ustawiające
...
}
[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 {
// właściwości
private String nom;
// produkty powiązane
public Set<Produit> produits = new HashSet<Produit>();
// konstruktory
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// metody
public void addProduit(Produit produit) {
// dodajemy produkt
produits.add(produit);
// ustalamy kategorię
produit.setCategorie(this);
}
// metody pobierające i ustawiające
...
}
[Produit]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// nazwa
private String nom;
// numer kategorii
private Long idCategorie;
// cena
private double prix;
// opis
private String description;
// kategoria
private Categorie categorie;
// producenci
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// metody pobierające i ustawiające
...
}
13.6.3.3. Klasa [DaoException]
![]() |
Gdy warstwa [DAO] napotka błąd, wygeneruje wyjątek typu [DaoException]. Klasa ta jest wykorzystywana po stronie serwera i została opisana w paragrafie 11.3.7.
13.6.3.4. Interfejs warstwy [DAO]
![]() |
Warstwa [DAO] udostępnia interfejs [IDao] opisany w paragrafie 11.3.7.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// dodawanie listy produktów
public List<Produit> addProduits(List<Produit> produits);
// usunięcie wszystkich produktów
public void deleteAllProduits();
// aktualizacja listy produktów
public List<Produit> updateProduits(List<Produit> produits);
// pobieranie wszystkich produktów
public List<Produit> getAllProduits();
// dodanie listy kategorii
public List<Categorie> addCategories(List<Categorie> categories);
// usunięcie wszystkich kategorii
public void deleteAllCategories();
// aktualizacja listy kategorii
public List<Categorie> updateCategories(List<Categorie> categories);
// pobieranie wszystkich kategorii
public List<Categorie> getAllCategories();
// konkretny produkt
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// określona kategoria
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
13.6.3.5. Odpowiedź serwisu internetowego / jSON
![]() |
Zauważyliśmy, że wszystkie elementy URL serwisu internetowego / jSON zwracają typ [Response] zdefiniowany w paragrafie 13.5.5.3. Przytaczamy tutaj tę klasę:
package spring.client.dao;
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
...
}
13.6.3.6. Implementacja wymiany danych z serwisem internetowym / jSON
![]() |
Klasa [AbstractDao] realizuje wymianę danych z serwisem internetowym / 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 {
// dane
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// ogólne zapytanie
protected String getResponse(String url, String jsonPost) {
// adres URL: URL – kontakt
// jsonPost: wartość jSON do wysłania
try {
// wykonanie żądania
RequestEntity<?> request;
if (jsonPost != null) {
// zapytanie POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// zapytanie GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// wykonuje się zapytanie
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- wiersze 15–16: wstrzyknięcie komponentu [RestTemplate], który zapewnia komunikację z serwerem;
- wiersze 17–18: wstrzyknięcie komponentu URL z serwisu internetowego / jSON;
Implementacja metod komunikacji z serwerem została wyodrębniona do metody [getResponse]:
- wiersz 21: metoda przyjmuje 2 parametry:
- [url]: żądana metoda URL;
- [jsonPost]: ciąg jSON do wysłania, w przeciwnym razie null. Jeśli [jsonPost==null], żądanie dotyczące URL jest wysyłane z GET, w przeciwnym razie z POST;
- wiersz 38: instrukcja, która wysyła zapytanie do serwera i odbiera jego odpowiedź. Komponent [RestTemplate] oferuje wiele metod komunikacji z serwerem. Wybraliśmy tutaj metodę [exchange], ale istnieją też inne;
- wiersze 27–36: musimy skonstruować żądanie typu [RequestEntity]. Różni się ono w zależności od tego, czy do wysłania żądania używamy komponentu GET, czy POST;
- wiersze 30–31: zapytanie dotyczące GET. Klasa [RequestEntity] udostępnia metody statyczne do tworzenia zapytań GET, POST, HEAD,... Metoda [RequestEntity.get] umożliwia utworzenie zapytania GET poprzez łańcuchowanie różnych metod, które je tworzą:
- metoda [RequestEntity.get] przyjmuje jako parametr docelowy obiekt URL w postaci instancji URI,
- metoda [accept] pozwala zdefiniować elementy nagłówka HTTP [Accept]. W tym miejscu wskazujemy, że akceptujemy typ [application/json], który wyśle serwer;
- metoda [build] wykorzystuje te różne informacje do skonstruowania typu [RequestEntity] żądania;
- wiersze 34–35: żądanie dotyczące typu POST. Metoda [RequestEntity.post] pozwala utworzyć żądanie typu POST poprzez łańcuchowanie różnych metod, które je tworzą:
- metoda [RequestEntity.post] przyjmuje jako parametr docelowy URL w postaci instancji URI,
- metoda [header] definiuje nagłówek HTTP. W tym przypadku wysyłamy do serwera nagłówek [Content-Type: application/json], aby poinformować go, że wysyłana wartość dotrze do niego w postaci ciągu znaków jSON;
- metoda [accept] pozwala wskazać, że akceptujemy typ [application/json], który wyśle serwer;
- metoda [body] ustala wartość wysyłaną. Jest to czwarty parametr metody generycznej [getResponse] (wiersz 1);
- wiersz 38: metoda [RestTemplate].exchange zwraca typ [ResponseEntity<String>], który zawiera całą odpowiedź serwera: nagłówki HTTP oraz treść dokumentu. Metoda [ResponseEntity].getBody() pozwala uzyskać tę treść, która reprezentuje odpowiedź serwera, w tym przypadku ciąg znaków;
13.6.3.7. Implementacja interfejsu [IDao]
![]() |
Klasa [Dao] implementuje interfejs [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;
// filtry 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) {
// ----------- dodaj produkty (bez kategorii)
...
}
- wiersz 17: klasa [Dao] jest komponentem Springa, do którego można zatem wstrzykiwać inne komponenty Springa;
- wiersz 18: klasa [Dao] rozszerza klasę [AbstractDao], którą właśnie omówiliśmy, i implementuje interfejs [IDao];
- wiersze 20–21: wstrzykujemy kontekst Springa, aby uzyskać dostęp do jego beanów;
- wiersze 24–38: wstrzyknięcie mapowników jSON zdefiniowanych w klasie [AppConfig] przedstawionej w paragrafie 13.6.2;
Wszystkie implementacje poszczególnych metod interfejsu [IDao] mają tę samą strukturę. Przedstawimy dwie metody: jedną opartą na operacji [POST], a drugą na operacji [GET].
Przykład metody [GET]: [getCategorieByNameWithProduits]
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
// ----------- pobierz kategorię określoną przez jej nazwę wraz z produktami
try {
// zapytanie
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// błąd?
if (response.getStatus() != 0) {
// wyzwalany jest 1 wyjątek
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// zwracamy główną część odpowiedzi serwera
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
- wiersz 7: wywoływana jest metoda [getResponse] klasy nadrzędnej. To właśnie ta metoda zapewnia komunikację z serwisem internetowym / jSON. Jej parametry są następujące:
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
- (ciąg dalszy)
- URL serwisu, do którego kierowane jest zapytanie [/getCategorieByNameWithProduits/nom];
- wartość wysłana. W tym przypadku nie ma żadnej;
Metoda [getResponse] zwraca typ String, który jest odpowiedzią jSON wysłaną przez serwer. Odserializowujemy tę odpowiedź jSON w następujący sposób:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
ponieważ ciąg znaków jSON jest serializacją typu [Response<Categorie>];
- wiersze 11–17: sprawdzamy status odpowiedzi. Jeśli status jest różny od 0, oznacza to, że wystąpił błąd po stronie serwera. W takim przypadku generujemy wyjątek (wiersz 13), wykorzystując informacje zawarte w odpowiedzi (status i listę komunikatów o błędach);
- wiersz 16: jeśli nie wystąpił błąd po stronie serwera, zwracamy treść typu [Response<Categorie>], tj. żądaną kategorię;
- wiersze 18–19: obsługujemy wyjątek zgłoszony w wierszu 16;
- wiersze 20–22: obsługują wszystkie pozostałe wyjątki;
Przykład [POST]: [addCategories]
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- dodaj kategorie (wraz z produktami)
try {
// żądanie
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// błąd?
if (response.getStatus() != 0) {
// wyzwalany jest 1 wyjątek
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// zwracamy treść odpowiedzi serwera
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- wiersz 2: metoda [addCategories] służy do zapisania w bazie danych kategorii przekazanych jako parametry. Dodaje do tych kategorii ich klucze główne. Jeśli kategorie są przekazywane wraz z produktami, produkty te są również zapisywane;
- wiersz 7: wywołuje się metodę [getResponse] klasy nadrzędnej w celu przeprowadzenia wymiany danych z serwisem internetowym / jSON;
- pierwszym parametrem jest URL [/addCategories];
- drugim parametrem jest wartość przesłana, w tym przypadku lista kategorii, które mają zostać zachowane;
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
Uzyskany ciąg jSON jest następnie deserializowany w celu uzyskania oczekiwanego typu [Response<List<Categorie>]:
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<List<Categorie>>>() {
});
- wiersze 11–17: obsługa odpowiedzi serwera (błąd lub brak błędu);
- wiersze 20–22: obsługa wyjątków;
Wszystkie pozostałe metody są zgodne ze schematem dwóch przedstawionych metod.
13.6.4. Test JUnit
Wróćmy do tworzonej obecnie architektury klient-serwer:
![]() |
Stworzyliśmy warstwę [DAO] [2] z tym samym interfejsem, co warstwa [DAO] [4]. Aby przetestować warstwę [DAO] [2], można zatem wykorzystać test JUnit, który posłużył do przetestowania warstwy [DAO] [4]. Przypomnijmy, że wygląda on następująco:
![]() |
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 {
// warstwa [DAO]
@Autowired
private IDao dao;
// filtry 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() {
// przed każdym testem czyści się bazę danych
log("Vidage de la base de données", 1);
// opróżnianie tabeli [CATEGORIES] – w efekcie kaskadowym tabela [PRODUITS] zostanie opróżniona
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// wypełnia się tabele
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);
}
// dodajemy kategorię – w wyniku kaskadowego działania produkty również zostaną wstawione
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// lista kategorii
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// lista produktów
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// kilka sprawdzeń
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);
// przeprowadzamy wstawianie
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// dodanie kategorii – produkty również zostaną dodane kaskadowo
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// sprawdzenie
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);
// sprawdzenie
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);
// weryfikacja
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- metody prywatne
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;
}
// wyświetlanie elementu typu T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// wyświetlanie listy elementów typu 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) {
// wyświetla komunikat
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) {
// tytuł
System.out.println(String.format("%s : ", title));
// komunikaty
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
Jego wykonanie zakończyło się powodzeniem i wyświetliło następujące wyniki w konsoli:
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



























































