Skip to content

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:

  1. żą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.
  2. 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];
  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ć:

{"id":2,"content":"Hello, World!"}

lub

{"id":2,"content":"Hello, John!"}

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:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.9.RELEASE)

2014-11-28 15:22:55.005  INFO 3152 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046  INFO 3152 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762  INFO 3152 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567  INFO 3152 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738  INFO 3152 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-11-28 15:22:56.740  INFO 3152 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869  INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870  INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478  INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481  INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] do obsługi typu [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] na handler typu [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231  INFO 3152 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318  INFO 3152 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319  INFO 3152 --- [           main] hello.Application                        : Started Application in 3.788 seconds (JVM running for 4.424)
  • 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.

[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

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:


    @RequestMapping(value = "/addProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addProduits(HttpServletRequest request) {
...
    }
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.

    @RequestMapping(value = "/deleteAllProduits",
method = RequestMethod.GET)
    public String deleteAllProduits() {
..
    }
Usuwa wszystkie produkty z bazy danych.

    @RequestMapping(value = "/updateProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String updateProduits(HttpServletRequest request) {
..
    }

Aktualizuje produkty w bazie danych. Zostają one opublikowane. Odpowiedzią jest ciąg znaków jSON z listy zaktualizowanych produktów.

    @RequestMapping(value = "/getAllProduits",
method = RequestMethod.GET)
    public String getAllProduits() {
..
    }

Pobiera ciąg znaków jSON dla wszystkich produktów.

    @RequestMapping(value = "/addCategories",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addCategories(HttpServletRequest request) {
..
    }

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.

    @RequestMapping(value = "/deleteAllCategories",
method = RequestMethod.GET)
    public String deleteAllCategories() {
...
    }

Usuwa wszystkie kategorie z bazy danych wraz ze wszystkimi produktami w nich zawartymi. Po wykonaniu tej operacji baza danych jest pusta.

    @RequestMapping(value = "/updateCategories",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String updateCategories
(HttpServletRequest request) {
...
    }

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;

    @RequestMapping(value = "/getAllCategories",
method = RequestMethod.GET)
    public String getAllCategories() {
...
    }

Pobiera ciąg znaków jSON ze wszystkich kategorii.

    @RequestMapping(value = "/getProduitByIdWithCategorie/{idProduit}",
method = RequestMethod.GET)
    public String getProduitByIdWithCategorie
(@PathVariable("idProduit") Long idProduit) {
...
    }

Pobiera ciąg znaków jSON dla produktu określonego przez jego identyfikator wraz z kategorią.

    @RequestMapping(value = "/getProduitByIdWithoutCategorie/{idProduit}",
method = RequestMethod.GET)
    public String getProduitByIdWithoutCategorie
@PathVariable("idProduit") Long idProduit) {
...
    }

Pobiera ciąg znaków jSON dla produktu określonego przez jego identyfikator, bez podawania kategorii.

    @RequestMapping(value = "/getProduitByNameWithCategorie/{nom}",
method = RequestMethod.GET)
    public String getProduitByNameWithCategorie(
@PathVariable("nom") String nom) {
...
    }

Pobiera ciąg znaków jSON dla produktu określonego przez jego nazwę wraz z kategorią.

    @RequestMapping(value = "/getProduitByNameWithoutCategorie/{nom}",
method = RequestMethod.GET)
    public String getProduitByNameWithoutCategorie
(@PathVariable("nom") String nom) {
...
    }

Pobiera ciąg znaków jSON dla produktu określonego przez jego nazwę, bez podawania kategorii.

    @RequestMapping(value = "/getCategorieByIdWithProduits/{idCategorie}",
 method = RequestMethod.GET)
    public String getCategorieByIdWithProduits
@PathVariable("idCategorie") Long idCategorie) {
...
    }

Pobiera ciąg znaków jSON z kategorii określonej przez jej identyfikator wraz z jej produktami.

    @RequestMapping(value = "/getCategorieByNameWithProduits/{nom}",
method = RequestMethod.GET)
    public String getCategorieByNameWithProduits
(@PathVariable("nom") String nom) {
...
    }

Pobiera ciąg znaków jSON z kategorii określonej jej nazwą wraz z jej produktami.

    @RequestMapping(value = "/getCategorieByNameWithoutProduits/{nom}",
method = RequestMethod.GET)
    public String getCategorieByNameWithoutProduits(
@PathVariable("nom") String nom) {
...
    }

Pobiera ciąg znaków jSON z kategorii określonej jej nazwą, bez produktów z tej kategorii.

    @RequestMapping(value = "/getCategorieByIdWithoutProduits/{idCategorie}",
method = RequestMethod.GET)
    public String getCategorieByIdWithoutProduits(
@PathVariable("idCategorie") Long idCategorie) {
...
    }

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:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-24 16:22:46.608  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Starting Boot on Gportpers3 with PID 9492 (D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02\target\classes started by ST in D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02)
2015-03-24 16:22:46.654  INFO 9492 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:47.521  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2015-03-24 16:22:47.569  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'entityManagerFactory': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=spring.data.config.DaoConfig; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class spring.data.config.DaoConfig] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=true; factoryBeanName=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]]
2015-03-24 16:22:48.137  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$405db6ba] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.162  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.172  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.178  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.586  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2015-03-24 16:22:48.850  INFO 9492 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2015-03-24 16:22:48.852  INFO 9492 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.20
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2342 ms
2015-03-24 16:22:49.645  INFO 9492 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2015-03-24 16:22:49.650  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'characterEncodingFilter' to: [/*]
2015-03-24 16:22:49.651  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015-03-24 16:22:50.380  INFO 9492 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-24 16:22:50.392  INFO 9492 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-24 16:22:50.478  INFO 9492 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-24 16:22:50.480  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-24 16:22:50.483  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-24 16:22:50.697  INFO 9492 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-24 16:22:50.806  INFO 9492 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2015-03-24 16:22:51.058  INFO 9492 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-24 16:22:52.581  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:52.654  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.addProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.updateProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.getAllProduits()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.getAllCategories()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.addCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.updateCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithoutProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithoutProduits(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithoutCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithoutCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithCategorie(java.lang.Long)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithProduits(java.lang.String)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllCategories()
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithoutProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithoutProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllProduits()
2015-03-24 16:22:52.658  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithoutCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithoutCategorie(java.lang.Long)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.691  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] do modułu obsługi typu [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.692  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] do obsługi typu [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.742  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:53.001  INFO 9492 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2015-03-24 16:22:53.106  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-24 16:22:53.108  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Started Boot in 6.752 seconds (JVM running for 7.433)
  • 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