Skip to content

13. [Cours]: Een database op het web beschikbaar maken met Spring MVC

Trefwoorden: meerlaagse architectuur, Spring, afhankelijkheidsinjectie, webservice / jSON, client / server

13.1. Support

 

De projecten van dit hoofdstuk zijn te vinden in de map [support / chap-13]. Met het script SQL [dbintrospringdata.sql] kunt u de basis MySQL aanmaken die nodig is voor de tests.

13.2. De rol van Spring MVC in een webapplicatie

Laten we Spring MVC in de context van de ontwikkeling van een webapplicatie plaatsen. Meestal wordt deze gebouwd op basis van een meerlaagse architectuur, zoals de volgende:

  • de [Web]-laag is de laag die in contact staat met de gebruiker van de webapplicatie. De gebruiker communiceert met de webapplicatie via webpagina’s die in een browser worden weergegeven. Spring MVC bevindt zich in deze laag en uitsluitend in deze laag;
  • de laag [métier] implementeert de bedrijfsregels van de applicatie, zoals de berekening van een salaris of een factuur. Deze laag maakt gebruik van gegevens die afkomstig zijn van de gebruiker via de laag [Web] en van de laag SGBD via de laag [DAO];
  • de laag [DAO] (Data Access Objects), de laag [ORM] (Object Relational Mapper) en de driver JDBC beheren de toegang tot de gegevens van de SGBD. De laag [ORM] vormt een brug tussen de objecten die door de laag [DAO] worden beheerd en de rijen en kolommen van de tabellen in een relationele database. De specificatie JPA (Java Persistence API) maakt het mogelijk om abstractie te maken van de gebruikte ORM, mits deze de specificaties implementeert. Dat is hier het geval en we zullen de ORM-laag voortaan de JPA-laag noemen;
  • de integratie van de lagen wordt verzorgd door het Spring-framework;

13.3. Het ontwikkelingsmodel van Spring MVC

Spring MVC implementeert het zogenaamde MVC-architectuurmodel (Model – View – Controller) op de volgende manier:

De verwerking van een verzoek van een klant verloopt als volgt:

  1. verzoek – de aangevraagde URL hebben de vorm http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... De [Front Controller] gebruikt een configuratiebestand of Java-annotaties om het verzoek naar de juiste controller en de juiste actie binnen die controller te „routeren“. Hiervoor gebruikt hij het veld [Action] van de URL. De rest van de URL [/param1/param2/...] bestaat uit optionele parameters die aan de actie worden doorgegeven. De C van MVC is hier de tekenreeks [Front Controller, Contrôleur, Action]. Als geen enkele controller de gevraagde actie kan verwerken, zal de webserver antwoorden dat de gevraagde URL niet is gevonden.
  2. verwerking
    • De gekozen actie kan gebruikmaken van de parameters parami die de [Front Controller] aan haar heeft doorgegeven. Deze kunnen uit verschillende bronnen afkomstig zijn:
    • het pad [/param1/param2/...] van de URL,
    • de parameters [p1=v1&p2=v2] van de URL,
    • van parameters die door de browser bij het verzoek zijn meegestuurd;
    • bij de verwerking van het verzoek van de gebruiker kan de actie de lagen [métier] en [2b] nodig hebben. Zodra het verzoek van de klant is verwerkt, kan dit verschillende reacties oproepen. Een klassiek voorbeeld is:
    • een foutpagina als het verzoek niet correct kon worden verwerkt
    • een bevestigingspagina in het andere geval
    • de actie vraagt om een bepaalde weergave [3] weer te geven. Deze weergave toont gegevens die het model van de weergave worden genoemd. Dit is de M van MVC. De actie zal dit model M [2c] aanmaken en vragen om een weergave V weer te geven [3];
  3. antwoord – de gekozen weergave V gebruikt het door de actie opgebouwde model M om de dynamische delen van het antwoord HTML te initialiseren dat zij naar de client moet verzenden, en verstuurt vervolgens dit antwoord.

Voor een webservice / jSON is de voorgaande architectuur enigszins aangepast:

  • in [4a] wordt het model, dat een Java-klasse is, door een bibliotheek jSON omgezet in de tekenreeks jSON;
  • in [4b] wordt deze tekenreeks jSON naar de browser verzonden;

Laten we nu eens nader ingaan op het verband tussen de webarchitectuur MVC en de gelaagde architectuur. Afhankelijk van de definitie die men aan het model geeft, zijn deze twee concepten al dan niet met elkaar verbonden. Laten we een eengelaagde Spring-webapplicatie MVC als voorbeeld nemen:

Als we de laag [Web] implementeren met Spring MVC, hebben we weliswaar een webarchitectuur MVC, maar geen meerlaagse architectuur. Hier zorgt de laag [web] voor alles: presentatie, bedrijfslogica, toegang tot gegevens. Dit werk wordt uitgevoerd door de acties.

Laten we nu eens kijken naar een meerlaagse webarchitectuur:

De laag [Web] kan worden geïmplementeerd zonder framework en zonder het model MVC te volgen. We hebben dan wel een meerlaagse architectuur, maar de weblaag implementeert het model MVC niet.

Bijvoorbeeld: in de .NET-omgeving kan de hierboven genoemde [Web]-laagbovenstaande laag worden geïmplementeerd met ASP.NET en MVC, waardoor we een gelaagde architectuur krijgen met een [Web]-laag van het type MVC. Zodra dit is gebeurd, kan deze laag ASP.NET MVC worden vervangen door een klassieke laag ASP.NET (WebForms), terwijl de rest (bedrijfslaag, DAO, ORM) ongewijzigd. We hebben dan een gelaagde architectuur met een laag [Web] die niet langer van het type MVC is.

In MVC hebben we aangegeven dat het model M dat van de weergave V is, c.a.d. De verzameling gegevens die door de weergave V wordt weergegeven. Er wordt een andere definitie van het model M van MVC gegeven:

Veel auteurs zijn van mening dat wat zich rechts van de laag [Web] bevindt, het model M van MVC vormt. Om dubbelzinnigheden te voorkomen, kan men spreken van:

  • het domeinmodel wanneer men alles bedoelt wat zich rechts van de laag [Web] bevindt
  • het model van de weergave wanneer we verwijzen naar de gegevens die door een weergave V worden getoond

In het vervolg zal de term "M-model" uitsluitend verwijzen naar het model van een weergave V.

13.4. Een webproject / jSON met Spring MVC

De website [http://spring.io/guides] biedt starttutorials om het Spring-ecosysteem te ontdekken. We volgen er een om de Maven-configuratie te ontdekken die nodig is voor een Spring-project MVC.

13.4.1. Het demonstratieproject

  • in [1] importeren we een van de Spring-handleidingen;
  • in [2] kiezen we het voorbeeld [Rest Service];
  • in [3] kiezen we het Maven-project;
  • in [4] kiezen we de definitieve versie van de handleiding;
  • in [5], valideren we;
  • in [6]: het geïmporteerde project;

Webservices die via standaard URL toegankelijk zijn en jSON-tekst leveren, worden vaak REST-services (REpresentational State Transfer) genoemd. Een service wordt als RESTful beschouwd als deze aan bepaalde regels voldoet.

Laten we nu het geïmporteerde project bekijken, te beginnen met de Maven-configuratie.

13.4.2. Maven-configuratie

Het bestand [pom.xml] ziet er als volgt uit:


<?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>
  • regels 6-8: de eigenschappen van het Maven-project. Er ontbreekt een tag [<packaging>] die het type aangeeft van het bestand dat door de Maven-compilatie wordt geproduceerd. Bij gebrek hieraan wordt het type [jar] gebruikt. De applicatie is dus een uitvoerbaar consoleprogramma, en geen webapplicatie, waarvoor de packaging dan [war] zou zijn;
  • regels 10-14: het Maven-project heeft een bovenliggend project met de naam [spring-boot-starter-parent]. Dit project definieert het grootste deel van de afhankelijkheden van het project. Deze kunnen voldoende zijn, in welk geval er geen extra afhankelijkheden worden toegevoegd, of onvoldoende, in welk geval de ontbrekende afhankelijkheden worden toegevoegd;
  • regels 17-20: het artefact [spring-boot-starter-web] bevat de bibliotheken die nodig zijn voor een Spring-project MVC van het type webservice, waarbij geen weergaven worden gegenereerd. Dit artefact bevat een zeer groot aantal bibliotheken, waaronder die van een ingebouwde Tomcat-server. Op deze server wordt de applicatie uitgevoerd;

De bibliotheken die bij deze configuratie worden meegeleverd, zijn zeer talrijk:

Hierboven ziet u de drie archiefbestanden van de Tomcat-server.

13.4.3. De architectuur van een Spring-service [web / jSON]

Voor een webservice / jSON implementeert Spring MVC het model MVC als volgt:

  • in [4a] wordt het model, dat een Java-klasse is, door een bibliotheek jSON omgezet in de tekenreeks jSON;
  • in [4b] wordt deze tekenreeks jSON naar de browser verzonden;

13.4.4. De C-controller

  

De geïmporteerde applicatie heeft de volgende controller:


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));
    }
}
  • regel 9: de annotatie [@RestController] maakt van de klasse [GreetingController] een Spring-controller, d.w.z. dat de methoden ervan worden geregistreerd om URL te verwerken. We hebben de vergelijkbare annotatie [@Controller] al gezien. Het resultaat van de methoden van die controller was een type [String], wat de naam was van de weer te geven weergave. Hier is het anders. De methoden van een controller van het type [@RestController] retourneren objecten die worden geserialiseerd om naar de browser te worden verzonden. Het type serialisatie dat wordt toegepast, hangt af van de configuratie van Spring MVC. Hier worden ze geserialiseerd naar jSON. Het is de aanwezigheid van een bibliotheek jSON in de afhankelijkheden van het project die ervoor zorgt dat Spring Boot het project via autoconfiguratie op deze manier instelt;
  • regel 14: de annotatie [@RequestMapping] geeft aan welk URL door de methode wordt verwerkt, in dit geval het URL [/greeting];
  • regel 15: we hebben de annotatie [@RequestParam] al uitgelegd. Het resultaat dat door de methode wordt geretourneerd, is een object van het type [Greeting].
  • regel 12: een long integer van het type ‘atomic’. Dit betekent dat het gelijktijdige toegang ondersteunt. Meerdere threads kunnen tegelijkertijd de variabele [counter] willen verhogen. Dit verloopt op een veilige manier. Een thread kan de waarde van de teller pas lezen als de thread die deze aan het wijzigen is, zijn wijziging heeft voltooid.

13.4.5. Het M-model

Het M-model dat door de vorige methode wordt gegenereerd, is het volgende object [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;
    }
}

De transformatie jSON van dit object zal de tekenreeks {"id":n,"content":"tekst"} genereren. Uiteindelijk zal de tekenreeks jSON die door de methode van de controller wordt gegenereerd, de volgende vorm hebben:

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

of

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

13.4.6. Uitvoering

  

De klasse [Application.java] is de uitvoerbare klasse van het project. De code ervan is als volgt:


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);
    }

}

We zijn deze code al tegengekomen en hebben deze uitgelegd in het vorige voorbeeld.

13.4.7. Het project uitvoeren

Laten we het project uitvoeren:

 

We krijgen de volgende console-logs:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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/**] naar handler van het type [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 [/**] onto handler van het type [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)
  • regel 13: de Tomcat-server start op poort 8080 (regel 12);
  • regel 17: de servlet [DispatcherServlet] is aanwezig;
  • regel 20: de methode [GreetingController.greeting] is gedetecteerd;

Om de webapplicatie te testen, roepen we de URL [http://localhost:8080/greeting] op:

 

We ontvangen inderdaad de verwachte tekenreeks jSON. Het kan interessant zijn om de door de server verzonden headers HTTP te bekijken. Hiervoor gebruiken we de Chrome-extensie met de naam [Advanced Rest Client] (Chrome / Ctrl-T / Menu [Applications] / [Advanced Rest Client] – zie bijlagen, paragraaf 22.5):

  • in [1], de gevraagde URL;
  • in [2] wordt de methode GET gebruikt;
  • in [3], het antwoord jSON;
  • in [4] heeft de server aangegeven dat hij een antwoord in het formaat jSON verstuurde;
  • in [5] wordt hetzelfde URL opgevraagd, maar ditmaal met een POST;
  • in [7] wordt de informatie in de vorm [urlencoded] naar de server verzonden;
  • in [6], de parameter name met zijn waarde;
  • in [8] geeft de browser aan de server door dat hij informatie [urlencoded] verstuurt;
  • in [9], het antwoord jSON van de server;

13.4.8. Een uitvoerbaar archief maken

We maken nu een uitvoerbaar archief:

  • in [1]: we voeren een Maven-doel uit;
  • in [2]: er zijn twee doelen (goals): [clean] om de map [target] uit het Maven-project te verwijderen, [package] om deze opnieuw te genereren;
  • in [3]: de gegenereerde map [target] wordt in deze map geplaatst;
  • in [4]: het doel wordt gegenereerd;

In de logbestanden die in de console verschijnen, is het belangrijk dat de plug-in [spring-boot-maven-plugin] wordt weergegeven. Deze zorgt voor het genereren van het uitvoerbare archief.

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

Met een console gaan we naar de gegenereerde map:


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
...
  • regel 5: het gegenereerde archief;

Dit archief wordt als volgt uitgevoerd:


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)
...

Nu de webapplicatie is gestart, kun je deze openen met een browser:

 

13.4.9. De applicatie op een Tomcat-server implementeren

Net als bij het vorige project passen we het bestand [pom.xml] als volgt aan:


<?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>
  • regel 9: hier moet worden aangegeven dat er een WAR-archief (Web ARchive) wordt gegenereerd;

Daarnaast moet de webapplicatie worden geconfigureerd. Aangezien het bestand [web.xml] ontbreekt, gebeurt dit met een klasse die afstamt van [SpringBootServletInitializer]:

  

De klasse [ApplicationInitializer] ziet er als volgt uit:


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);
    }

}
  • regel 6: de klasse [ApplicationInitializer] is een uitbreiding van de klasse [SpringBootServletInitializer];
  • regel 9: de methode [configure] wordt opnieuw gedefinieerd (regel 8);
  • regel 10: hier wordt de klasse opgegeven die het project configureert;

Om het project uit te voeren, kunt u als volgt te werk gaan:

  • in [1-2] voert u het project uit op een van de servers die zijn geregistreerd in IDE Eclipse;

Zodra dit is gebeurd, kun je de URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in een browser opvragen:

 

13.4.10. Conclusie

We hebben een type Spring-project geïntroduceerd waarbij de webapplicatie een stream naar de browser stuurt. We gaan nu een webapplicatie / jSON ontwikkelen om de database [dbintrospringdata], die in de tutorial [Introduction à Spring Data] is behandeld, op het web beschikbaar te maken.

13.5. De database [dbintrospringdata] op het web beschikbaar maken

13.5.1. Architectuur van de webservice / jSON

We gaan de volgende architectuur opzetten:

De lagen [DAO] en [JPA] worden geïmplementeerd door de applicatie die in de tutorial [Introduction à Spring Data] is geschreven.

13.5.2. Installatie van de database

  

Met het script SQL [dbintrospringdata.sql] kunt u de database MySQL aanmaken die nodig is voor de tests.

13.5.3. Het Eclipse-project van de webservice / jSON

Het Eclipse-project van de webservice / jSON ziet er als volgt uit:

  

Dit is een Maven-project waarvan het bestand [pom.xml] als volgt is:


<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>
  • regels 11-15: het bovenliggende Maven-project dat al wordt gebruikt voor de laag [DAO];
  • regels 18-22: de afhankelijkheid van de laag [DAO];
  • regels 23-26: de afhankelijkheid van het artefact [spring-boot-starter-web]. Dit artefact bevat alle afhankelijkheden die nodig zijn voor het aanmaken van een webservice / jSON. Het bevat echter ook overbodige bibliotheken. Een nauwkeurigere configuratie zou dus nodig zijn. Maar deze configuratie is handig om mee te beginnen;
  • regels 28-30: de afhankelijkheid van het artefact [spring-boot-starter] maakt het mogelijk om Spring Boot-annotaties te beheren;

De afhankelijkheden die deze configuratie met zich meebrengt, zijn de volgende:

  • in [1] zien we dat Eclipse de afhankelijkheid van het projectarchief [intro-spring-data-01] heeft gedetecteerd;

De bovenstaande afhankelijkheden gelden zowel voor de laag [DAO] als voor de laag [web].

13.5.3.1. Configuratie van de laag [web]

De laag [web] wordt geconfigureerd via een bestand [AppConfig]:

  

De klasse [WebConfig] configureert de laag [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 {

    // -------------------------------- configuratie van de [web]-laag
    @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);
    }

    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // resultaat
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // resultaat
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // resultaat
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // resultaat
        return mapper;
    }
}
  • regel 18: de annotatie [@EnableWebMvc] leidt tot automatische configuraties voor het Spring-framework MVC;
  • regel 19: de klasse [WebConfig] breidt de Spring-klasse [WebMvcConfigurerAdapter] uit om bepaalde beans te herdefiniëren (regels 26-40);
  • regels 22-23: injectie van de Spring-context;
  • regels 25-29: definitie van de servlet van het Spring-framework MVC, die de verzoeken HTTP naar de juiste controller en methode doorstuurt. [DispatcherServlet] is een Spring-klasse;
  • regels 31-34: hier wordt aangegeven dat deze servlet alle URL-verzoeken verwerkt;
  • regels 36-39: de aanwezigheid van deze bean zorgt ervoor dat de Tomcat-server in de projectarchieven wordt geactiveerd. Deze wacht op verzoeken op poort 8080;
  • regels 42-91: beans die zullen worden gebruikt om jSON-filters te beheren;
  • regels 42-45: een mapper jSON zonder filters;
  • regels 47-57: de mapper jSON waarmee een categorie met bijbehorende producten kan worden weergegeven. Merk op dat wanneer een categorie met bijbehorende producten wordt opgevraagd, zowel het filter jSON van de klasse [Categorie] als dat van de klasse [Produit] moet worden geconfigureerd. Dit is altijd het geval. Wanneer een klasse wordt geserialiseerd/deserialiseerd in jSON, moet het filter jSON van de klasse worden geconfigureerd, evenals die van alle afhankelijkheden die daarin moeten worden opgenomen;
  • regels 59-69: de mapper jSON waarmee een product met zijn categorie kan worden weergegeven;
  • regels 71-80: de mapper jSON waarmee een categorie zonder bijbehorende producten kan worden weergegeven;
  • regels 82-91: de mapper jSON waarmee een product zonder de bijbehorende categorie kan worden weergegeven;

De klasse [AppConfig] configureert de gehele applicatie, d.w.z. de lagen [web] en [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 {

}
  • regel 9: we importeren de beans van de laag [DAO] en die van de laag [web];
  • regel 8: geeft aan in welke pakketten andere Spring-beans te vinden zijn;

Merk op dat we nergens de annotatie [@EnableAutoConfiguration] hebben gebruikt. We hebben er de voorkeur aan gegeven de configuratie zelf te beheren.

13.5.4. Het applicatiemodel

  

De klasse [ApplicationModel] ziet er als volgt uit:


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 {

    // de laag [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);
    }

}
  • regel 12: de klasse is een Spring-singleton;
  • regel 13: die de interface [IDao] van de laag [DAO] implementeert;
  • regels 16-17: injectie van een referentie op de laag [DAO];
  • regels 19-99: implementatie van de interface [IDao];

De architectuur van de weblaag ontwikkelt zich als volgt:

  • in [2b] communiceren de methoden van de controller(s) met het singleton [ApplicationModel];

Deze strategie biedt flexibiliteit bij het beheer van een eventuele cache. De klasse [ApplicationModel] kan worden gebruikt om informatie op te slaan die is verkregen uit de laag [DAO], of om configuratiegegevens op te slaan. Dit kan nuttig zijn wanneer men geen controle heeft over de laag [DAO]. Deze cachestrategie kan in de loop van de tijd veranderen. De wijzigingen hebben geen invloed op de code van de controller(s).

13.5.5. De controller

  

We hebben hier slechts één controller, de klasse [MyController].

13.5.5.1. De door deze controller blootgestelde URL

De URL die door deze controller worden weergegeven, zijn de volgende:


    @RequestMapping(value = "/addProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addProduits(HttpServletRequest request) {
...
    }
Voegt producten toe aan de database. Deze worden gepost. Het antwoord is de tekenreeks jSON, de lijst met toegevoegde producten met hun primaire sleutel.

    @RequestMapping(value = "/deleteAllProduits",
method = RequestMethod.GET)
    public String deleteAllProduits() {
..
    }
Verwijdert alle producten uit de database.

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

Werkt producten in de database bij. Deze worden gepost. Het antwoord is de tekenreeks jSON uit de lijst met bijgewerkte producten.

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

Haal de tekenreeks jSON op voor alle producten.

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

Voegt categorieën toe aan de database. Deze worden gepost. Het antwoord is de tekenreeks jSON uit de lijst met toegevoegde categorieën, inclusief hun primaire sleutel. Als de categorieën producten bevatten, worden deze ook aan de database toegevoegd.

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

Verwijdert alle categorieën uit de database, evenals alle producten daarin. Daarna is de database leeg.

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

Werkt de categorieën in de database bij. Deze worden verzonden. Het antwoord is de lijst met bijgewerkte categorieën. Als de categorieën producten bevatten, worden deze ook in de database bijgewerkt. Levert de tekenreeks jSON op van de gewijzigde categorieën;

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

Haal de tekenreeks jSON uit alle categorieën op.

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

Haal de tekenreeks jSON op van een product dat wordt aangeduid met zijn id, samen met de categorie.

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

Haal de tekenreeks jSON op van een product dat wordt aangeduid met zijn id, zonder de categorie.

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

Haal de tekenreeks jSON op van een product dat bij naam wordt aangeduid, samen met de categorie.

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

Haal de tekenreeks jSON op van een product dat bij naam wordt aangeduid, zonder de categorie.

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

Haal de tekenreeks jSON op uit een categorie die wordt aangeduid door haar id, samen met de bijbehorende producten.

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

Haal de tekenreeks jSON op uit een categorie die bij naam wordt aangeduid, samen met de bijbehorende producten.

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

Haal de tekenreeks jSON op uit een categorie die bij naam wordt aangeduid, zonder de producten ervan.

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

Haal de tekenreeks jSON op van een categorie die wordt aangeduid door haar id, zonder de bijbehorende producten.

De weergegeven URL komen overeen met de methoden van de interface [IDao] van de laag [DAO]. De methoden van de webservice / jSON zijn allemaal volgens hetzelfde patroon opgebouwd. We zullen er een paar bekijken.

13.5.5.2. Het skelet van de controller

Het skelet van de controller ziet er als volgt uit:


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 {

    // Spring-afhankelijkheden
    @Autowired
    private ApplicationModel application;

    // filters 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;

    // de klasse [MyController] is een singleton en wordt slechts één keer geïnstantieerd, namelijk de 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 {
        ...
    }

  • regel 28: de annotatie [@Controller] maakt van de klasse een Spring-component;
  • regels 32-33: injectie van een referentie naar de klasse [ApplicationModel];
  • regels 36-50: injectie van verwijzingen naar de mappers jSON;
  • regel 58: de blootgestelde URL is [/addProduits]. De client moet een methode [POST] gebruiken om zijn verzoek te doen (method = RequestMethod.POST). Hij moet de verzonden waarde versturen in de vorm van een tekenreeks jSON (consumes = "application/json; charset=UTF-8"). De methode stuurt zelf het antwoord terug naar de klant (regel 59). Dit zal een tekenreeks zijn (regel 60). De header HTTP [Content-type : application/json; charset=UTF-8] wordt naar de client verzonden om aan te geven dat deze een tekenreeks jSON zal ontvangen (regel 58);
  • regel 60: de methode [addProduits] retourneert de tekenreeks jSON uit de lijst met producten die aan de database zijn toegevoegd;

13.5.5.3. Het antwoord van de methoden van de controller

Alle methoden van de controller retourneren het volgende antwoord van het type [Response]:

  

package spring.webjson.service;

import java.util.List;

public class Response<T> {

    // ----------------- eigenschappen
    // status van de bewerking
    private int status;
    // eventuele foutmeldingen
    private List<String> messages;
    // de inhoud van het antwoord
    private T body;

    // constructors
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters en setters
    ...
}
  • regel 5: het antwoord bevat een type T;
  • regel 13: het antwoord van het type T;
  • regels 9-11: het is mogelijk dat een methode een uitzondering tegenkomt. In dat geval zal deze een antwoord retourneren met:
    • regel 9: status!=0;
    • regel 11: de lijst met opgetreden fouten;

13.5.5.4. L'URL [/addProduits]

L'URL [/addProduits] wordt verwerkt door de volgende methode:


@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 {
        // antwoord
        Response<List<Produit>> response;
        try {
            // de verzonden waarde wordt opgehaald
            String body = CharStreams.toString(request.getReader());
            List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
            });
            // we herstellen de koppeling tussen producten en categorieën
            for (Produit produit : produits) {
                produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
            }
            // de producten worden opgeslagen
            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);
        }
        // antwoord jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
    }
  • regel 3: de methode accepteert als parameter [HttpServletRequest request], waarin alle informatie over het verzoek van de klant is opgenomen;
  • regel 5: het antwoord dat naar de klant wordt verzonden: een lijst met producten;
  • regel 8: de verzonden waarde wordt opgehaald. De klasse [CharStreams] behoort tot de bibliotheek [Google Guava], waarvan de verwijzing is toegevoegd in het bestand [pom.xml]. We ontvangen de door de klant verzonden tekenreeks jSON. Deze moet worden gedeserialiseerd om er iets mee te kunnen doen;
  • regels 8-10: de deserialisatie wordt uitgevoerd. We krijgen een lijst met producten waarbij elk product een veld [categorie=null] heeft;
  • regels 12-14: het veld [categorie] van alle producten in de lijst wordt opnieuw ingesteld. Hiervoor wordt het veld [idCategorie] van het product gebruikt, dat zelf al is geïnitialiseerd;
  • regel 16: de producten worden in de database ingevoerd;
  • regel 17: het object [response] wordt geïnitialiseerd met de productlijst;
  • regels 18-19: gevallen waarin de methode een uitzondering van de laag [DAO] tegenkomt. Het antwoord wordt geïnitialiseerd met [status=1000] (foutcode) [messages=e1.getMessages()], d.w.z. dat de lijst met fouten die aan de serverzijde zijn opgetreden, naar de client wordt verzonden;
  • regels 20-21: het geval waarin de methode een ander type uitzondering tegenkomt. Het antwoord wordt geïnitialiseerd met [status=1000] (foutcode) [messages=getErreursForException(e)], waarbij [getErreursForException] een privémethode van de klasse is die de lijst met fouten teruggeeft die verband houden met de uitzonderingen in de uitzonderingsstack van e, en [body=null];
  • regel 24: de tekenreeks jSON wordt teruggestuurd als antwoord;

13.5.5.5. De URL [/getAllProduits]

De URL [/getAllProduits] wordt verwerkt door de volgende methode:


    @RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits() throws JsonProcessingException {
        // antwoord
        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);
        }
        // antwoord jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
  • regel 1: de URL [/getAllProduits] wordt opgevraagd met een bewerking [GET]. Dit levert jSON op;
  • regel 2: de methode stuurt zelf het antwoord jSON naar de client;
  • regel 5: de methode verstuurt de tekenreeks jSON van het type [Response<List<Produit>>];
  • regel 7: de producten worden opgevraagd zonder hun categorie;
  • regels 8-12: in geval van een fout wordt het antwoord geïnitialiseerd met een foutcode en foutmeldingen;
  • regel 14: het antwoord jSON wordt naar de klant verzonden;

13.5.5.6. Conclusion

We zullen de andere methoden van de controller niet bespreken. Ze lijken op een van de twee methoden die we zojuist hebben besproken.

13.5.6. De uitvoeringsklasse van de webservice / jSON

  

De klasse [Boot] is de uitvoerbare klasse van het project:


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);
    }
}
  • regel 10: de statische methode [SpringApplication.run] wordt uitgevoerd. De klasse [SpringApplication] is een klasse van het project [Spring Boot] (regel 3). Er worden twee parameters aan doorgegeven:
    • [AppConfig.class]: de klasse die de gehele applicatie configureert;
    • [args]: eventuele argumenten die worden doorgegeven aan de methode [main] op regel 9. Deze parameter wordt hier niet gebruikt;

Wanneer deze klasse wordt uitgevoerd, zijn de volgende logbestanden beschikbaar:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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/**] onto handler van het type [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 [/**] onto handler van het type [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)
  • regels 17-19: opstarten van de Tomcat-server die de webservice / jSON gaat uitvoeren;
  • regels 25-33: opbouw van de laag [DAO];
  • regels 32-51: de blootgestelde URL worden gedetecteerd;

13.5.7. Tests van de webservice / jSON

Om de tests uit te voeren, genereren we de database MySQL [dbintrospringdata] op basis van het script SQL [dbintrospringdata.sql]:

  

Vervolgens gebruiken we de client [Advanced Rest Client] (zie paragraaf 22.5) om de URL-gegevens op te vragen die door de webservice / jSON worden aangeboden (de webservice / jSON moet gestart zijn).

  • in [1-3] vragen we de URL [/getAllCategories] via een commando HTTP GET;

We krijgen het volgende antwoord:

  • in [1], het verzoek HTTP van de client;
  • in [2], het antwoord HTTP van de server;
  • in [3] geeft de status [200 OK] aan dat de server het verzoek correct heeft verwerkt;
  • in [4], het antwoord jSON van de server;

Het volledige antwoord jSON luidt als volgt:


{"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 betekent dat er geen fouten aan de serverzijde zijn opgetreden;
  • berichten: null betekent dat er geen foutmeldingen zijn;
  • body: is de hoofdtekst van het antwoord, in dit geval de lijst met categorieën en hun producten. Er zijn twee categorieën met elk 5 producten;

We gaan aan de categorie [categorie1] het product [produit15] toevoegen. Hiervoor gebruiken we de URL [/addCategories] met de volgende code:


@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);
        // de categorieën worden behouden
        try {
            // de opgeslagen waarde wordt opgehaald
            String body = CharStreams.toString(request.getReader());
            mapper.setFilters(jsonFilterCategorieWithProduits);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // de koppeling tussen producten en categorieën wordt hersteld
            for (Categorie categorie : categories) {
                Set<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    for (Produit produit : categorie.getProduits()) {
                        produit.setCategorie(categorie);
                    }
                }
            }
            // de categorieën worden opgeslagen
            application.addCategories(categories);
            response = new Response<List<Categorie>>(0, null, categories);
        } catch (Exception e) {
            response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
        }
        // antwoord jSON
        return mapper.writeValueAsString(response);
    }
  • regel 1: de klant moet een POST opgeven en de verzonden waarde moet een tekenreeks jSON zijn;
  • regels 9-12: de verzonden waarde moet een lijst zijn van categorieën met de bijbehorende producten;

We gaan een categorie [categorie2] aanmaken met een product [produit21]. De te verzenden tekenreeks jSON is dan als volgt:

[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]

Het verzoek aan de webservice /jSON wordt als volgt gedaan:

  • in [1], de gevraagde URL;
  • in [2] wordt deze aangevraagd via een bewerking POST;
  • in [3] wordt de string jSON verzonden;
  • in [4] wordt aan de server aangegeven dat jSON naar de server wordt verzonden;

Het antwoord van de server is als volgt:

  • in [1] zien we dat zowel de categorie als het product nu een primaire sleutel hebben, wat aangeeft dat ze waarschijnlijk in de database zijn ingevoerd. We gaan dit controleren met behulp van URL en [/getCategorieByNameWithProduits/categorie2]:

We krijgen het volgende resultaat:

We hebben inderdaad de categorie [categorie2] met het enige product [produit21] gevonden. We kunnen ook alleen het product opvragen. Laten we daarvoor de URL [/getProduitByIdWithoutCategorie/1859] gebruiken:

We krijgen het volgende resultaat:

Alle bewerkingen met [GET] kunnen in een gewone browser worden uitgevoerd:

 

De lezer wordt uitgenodigd om de andere URL-bewerkingen van de webservice / json te testen.

13.6. Een client die is geprogrammeerd voor de webservice / jSON

Nu de [dbintrospringdata]-database op het web beschikbaar is, gaan we een applicatie schrijven die hiervan gebruikmaakt. We krijgen dan de volgende client/server-architectuur:

De clienttoepassing zal uit twee lagen bestaan:

  • een [DAO] [2]-laag om te communiceren met de webapplicatie / jSON die de database beschikbaar stelt;
  • een testlaag JUnit [1] om te controleren of de client en de server naar behoren functioneren;

13.6.1. Het Eclipse-project

Het Eclipse-project van de client is als volgt:

  
  • de map [src/main/java] implementeert de laag [DAO];
  • de map [src/test/java] implementeert de tests JUnit;

13.6.2. Maven-configuratie van het project

Het project is een Maven-project dat is geconfigureerd via het volgende bestand [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>
        <!-- bibliotheek jSON die door Spring wordt gebruikt -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- component die door Spring wordt gebruikt 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>
        <!-- logboekbibliotheek -->
        <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>
    <!-- plugins -->
    <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>
  • regels 14-18: het bovenliggende Maven-project [spring-boot-starter-parent] waarmee we een aantal afhankelijkheden kunnen definiëren zonder hun versies, aangezien deze in het bovenliggende project zijn gedefinieerd;
  • regels 22-25: hoewel we geen webapplicatie schrijven, hebben we de afhankelijkheid [spring-web] nodig, die de klasse [RestTemplate] meebrengt waarmee we eenvoudig kunnen koppelen aan een webapplicatie / jSON;
  • regels 27-34: een bibliotheek jSON;
  • regels 36-39: een afhankelijkheid waarmee we een timeout kunnen koppelen aan de HTTP-verzoeken van de client. Een timeout is de maximale wachttijd voor het antwoord van de server. Als deze tijd wordt overschreden, meldt de client een timeout-fout door een uitzondering te genereren;
  • regels 41-46: de Google Guava-bibliotheek die in de test JUnit wordt gebruikt. Om deze reden hebben we het bereik ervan ingesteld op [test] (regel 45). Dit betekent dat deze afhankelijkheid alleen wordt opgenomen bij het uitvoeren van code uit de branch [src/test/java];
  • regels 48-51: de logboekbibliotheek;
  • regels 52-63: de afhankelijkheid voor de tests JUnit. Deze voegt met name de bibliotheek JUnit 4 toe die nodig is voor de tests. Deze afhankelijkheden hebben het attribuut [<scope>test</scope>], wat aangeeft dat ze alleen nodig zijn voor de testfase. Ze worden niet opgenomen in het uiteindelijke projectarchief;

13.6.3. Implementatie van de laag [DAO]

  
  • het pakket [spring.client.config] bevat de Spring-configuratie van de laag [DAO];
  • het pakket [spring.client.dao] bevat de implementatie van de laag [DAO];
  • het pakket [spring.client.entities] bevat de objecten die worden uitgewisseld met de webservice / jSON;

13.6.3.1. Configuration

  

De klasse [DaoConfig] zorgt voor de Spring-configuratie van de laag [DAO]. De code ervan is als volgt:


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 {

    // constanten
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // Aanmaken van de component RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // time-out van de communicatie
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // resultaat
        return restTemplate;
    }

    @Bean
    public int timeout() {
        return TIMEOUT;
    }

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }

    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // resultaat
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // resultaat
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // resultaat
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // resultaat
        return mapper;
    }
}
  • regel 13: de klasse is een Spring-configuratieklasse – Spring-componenten zijn te vinden in het pakket [spring.client.dao];
  • regel 17: er wordt een timeout van één seconde (1000 ms) ingesteld;
  • regels 32-35: de bean die deze waarde retourneert;
  • regel 18: de URL van de webservice / jSON;
  • regels 37-40: de bean die deze waarde retourneert;
  • regels 20-30: de configuratie van de klasse [RestTemplate] die de communicatie met de webservice / jSON verzorgt. Wanneer deze niet hoeft te worden geconfigureerd, kan deze in de code worden gebruikt met een eenvoudige [new RestTemplate()]. Hier willen we de timeout instellen voor de communicatie met de webservice / jSON. De bean [timeout] uit regel 36 wordt doorgegeven als parameter aan de methode [restTemplate] uit regel 24;
  • regel 23: de component [HttpComponentsClientHttpRequestFactory] is de component waarmee we de timeout voor de uitwisselingen kunnen instellen (regels 29-30);
  • regel 24: de klasse [RestTemplate] is opgebouwd met deze component. Aangezien deze klasse op deze component steunt om te communiceren met de webservice / jSON, zullen de uitwisselingen inderdaad onderworpen zijn aan de timeout;
  • de client en de server zullen tekstregels uitwisselen. Een converter zorgt ervoor dat een object naar tekst wordt geserialiseerd en omgekeerd dat tekst naar een object wordt gedeserialiseerd. Er kunnen meerdere converters aan de klasse [RestTemplate] zijn gekoppeld en welke op een bepaald moment wordt gekozen, hangt af van de HTTP-headers die door de server worden verzonden. In dit geval hebben we geen converter. De component [RestTemplate] zal dus niet proberen de volgende twee elementen op enigerlei wijze te converteren:
    • de verzonden tekst;
    • de tekst die als antwoord is ontvangen;

Deze teksten zijn jSON-strings die dus door de component [RestTemplate] ongewijzigd worden gelaten. Wij, als ontwikkelaars, zullen zelf de nodige jSON-serialisaties en -deserialisaties uitvoeren. Dit komt doordat de filters die op de verzonden waarde en het ontvangen antwoord moeten worden toegepast, verschillend kunnen zijn en de ervaring leert dat het eenvoudiger is om deze zelf te beheren dan te proberen de component [RestTemplate] zo te configureren dat deze de juiste converter jSON gebruikt;

  • regels 42-92: definiëren jSON-filters. Dit zijn dezelfde als die van de server, die in paragraaf 13.5.3.1 worden gepresenteerd en uitgelegd;
  • regels 43-46: een mapper jSON zonder filters;
  • regels 64-68: een mapper jSON om een categorie zonder producten te krijgen;
  • regels 48-58: een mapper jSON om een categorie met de bijbehorende producten te krijgen;
  • regels 83-92: een mapper jSON om een product zonder categorie te krijgen;
  • regels 60-70: een mapper jSON om een product met zijn categorie te krijgen;

Al deze beans zullen beschikbaar zijn voor de codes van de laag [DAO] en voor de Junit-test.

13.6.3.2. De entiteiten

  

De entiteiten die door de laag [DAO] worden verwerkt, zijn de entiteiten die deze uitwisselt met de webservice /jSON. Dit zijn de artikelen en de producten. Aan de serverzijde hadden deze entiteiten persistentie-annotaties JPA. Hier zijn deze annotaties verwijderd. Ter herinnering geven we de code van de entiteiten nogmaals weer:

[AbstractEntity]


package spring.client.entities;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public abstract class AbstractEntity {
    // eigenschappen
    protected Long id;
    protected Long version;

    // constructors
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // herdefinitie van [equals] en [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();
    }

    // handtekening jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    // getters en setters
...
}

[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 {

    // eigenschappen
    private String nom;

    // gerelateerde producten
    public Set<Produit> produits = new HashSet<Produit>();

    // constructors
    public Categorie() {

    }

    public Categorie(String nom) {
        this.nom = nom;
    }

    // methoden
    public void addProduit(Produit produit) {
        // het product toevoegen
        produits.add(produit);
        // de categorie instellen
        produit.setCategorie(this);
    }

    // getters en setters
    ...
}

[Produit]


package spring.webjson.client.entities;

import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {

    // de naam
    private String nom;
   // het categorienummer
    private Long idCategorie;
   // de prijs
    private double prix;
   // de beschrijving
    private String description;

    // de categorie
    private Categorie categorie;

    // fabrikanten
    public Produit() {

    }

    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }

    // getters en setters
...
}

13.6.3.3. De klasse [DaoException]

 

Wanneer de laag [DAO] een fout tegenkomt, genereert deze een uitzondering van het type [DaoException]. Deze klasse wordt aan de serverzijde gebruikt en wordt beschreven in paragraaf 11.3.7.

13.6.3.4. De interface van de laag [DAO]

 

De laag [DAO] biedt de interface [IDao] die in paragraaf 11.3.7 wordt beschreven.


package spring.client.dao;

import java.util.List;

import spring.client.entities.Categorie;
import spring.client.entities.Produit;

public interface IDao {

    // een productlijst toevoegen
    public List<Produit> addProduits(List<Produit> produits);

    // alle producten verwijderen
    public void deleteAllProduits();

    // een productlijst bijwerken
    public List<Produit> updateProduits(List<Produit> produits);

    // alle producten ophalen
    public List<Produit> getAllProduits();

    // een lijst met categorieën toevoegen
    public List<Categorie> addCategories(List<Categorie> categories);

    // alle categorieën verwijderen
    public void deleteAllCategories();

    // een lijst met categorieën bijwerken
    public List<Categorie> updateCategories(List<Categorie> categories);

    // alle categorieën ophalen
    public List<Categorie> getAllCategories();

    // een specifiek product
    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // een specifieke categorie
    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);

}

13.6.3.5. Het antwoord van de webservice / jSON

  

We hebben gezien dat alle URL van de webservice / jSON een type [Response] opleverden, zoals gedefinieerd in paragraaf 13.5.5.3. We nemen deze klasse hier nogmaals op:


package spring.client.dao;

import java.util.List;

public class Response<T> {

    // ----------------- eigenschappen
    // status van de bewerking
    private int status;
    // eventuele foutmeldingen
    private List<String> messages;
    // de inhoud van het antwoord
    private T body;

    // constructors
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters en setters
    ...
}

13.6.3.6. Implementatie van de uitwisselingen met de webservice / jSON

  

De klasse [AbstractDao] implementeert de uitwisselingen met de webservice / 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 {

    // gegevens
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // algemene aanvraag
    protected String getResponse(String url, String jsonPost) {

        // URL: URL om contact op te nemen
        // jsonPost: de waarde jSON moet worden verzonden
        try {
            // verzoek uitvoeren
            RequestEntity<?> request;
            if (jsonPost != null) {
                // verzoek POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // verzoek GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // de query wordt uitgevoerd
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • regels 15-16: injectie van de component [RestTemplate] die de communicatie met de server verzorgt;
  • regels 17-18: injectie van de URL van de webservice / jSON;

De implementatie van de communicatiemethoden met de server is ondergebracht in de methode [getResponse]:

  • regel 21: de methode ontvangt 2 parameters:
    • [url]: de aangevraagde URL;
    • [jsonPost]: de te verzenden tekenreeks jSON, anders null. Als [jsonPost==null], wordt het verzoek van URL gedaan met een GET, anders met een POST;
  • regel 38: de instructie die het verzoek naar de server stuurt en het antwoord ontvangt. De component [RestTemplate] biedt een groot aantal methoden voor communicatie met de server. We hebben hier gekozen voor de methode [exchange], maar er zijn ook andere;
  • regels 27-36: we moeten het verzoek van het type [RequestEntity] opstellen. Dit verschilt naargelang men een GET of een POST gebruikt om het verzoek te doen;
  • regels 30-31: de query voor een GET. De klasse [RequestEntity] biedt statische methoden om de verzoeken GET, POST, HEAD, ... aan te maken Met de methode [RequestEntity.get] kan een verzoek GET worden aangemaakt door de verschillende methoden die dit verzoek opbouwen aan elkaar te koppelen:
    • de methode [RequestEntity.get] accepteert als parameter de doel-URL in de vorm van een URI-instantie,
    • met de methode [accept] kunnen de elementen van de header HTTP [Accept] worden gedefinieerd. Hier geven we aan dat we het type [application/json] accepteren dat de server zal verzenden;
    • de methode [build] gebruikt deze verschillende gegevens om het type [RequestEntity] van het verzoek samen te stellen;
  • regels 34-35: het verzoek voor een POST. Met de methode [RequestEntity.post] kan een verzoek van het type POST worden aangemaakt door de verschillende methoden die dit verzoek samenstellen achter elkaar te plaatsen:
    • de methode [RequestEntity.post] accepteert als parameter de doel-URL in de vorm van een URI-instantie,
    • de methode [header] definieert een header HTTP. Hier sturen we de header [Content-Type: application/json] naar de server om aan te geven dat de verzonden waarde in de vorm van een tekenreeks jSON zal worden ontvangen;
    • met de methode [accept] geven we aan dat we het type [application/json] accepteren dat de server zal verzenden;
    • de methode [body] stelt de verzonden waarde vast. Dit is de vierde parameter van de generieke methode [getResponse] (regel 1);
  • regel 38: de methode [RestTemplate].exchange retourneert een type [ResponseEntity<String>] dat het volledige antwoord van de server omvat: de headers HTTP en de hoofdtekst van het document. Met de methode [ResponseEntity].getBody() kan deze inhoud worden opgehaald, die het antwoord van de server vertegenwoordigt, in dit geval een tekenreeks;

13.6.3.7. Implementatie van de interface [IDao]

  

De klasse [Dao] implementeert de interface [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;

    // filters 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) {
        // ----------- producten toevoegen (zonder hun categorie)
        ...
}
  • regel 17: de klasse [Dao] is een Spring-component waarin dus andere Spring-componenten kunnen worden geïnjecteerd;
  • regel 18: de klasse [Dao] is een uitbreiding van de klasse [AbstractDao] die we zojuist hebben bekeken en implementeert de interface [IDao];
  • regels 20-21: we injecteren de Spring-context om toegang te krijgen tot de beans;
  • regels 24-38: invoeging van de mappers jSON die zijn gedefinieerd in de klasse [AppConfig], zoals beschreven in paragraaf 13.6.2;

De implementaties van de verschillende methoden van de interface [IDao] volgen allemaal hetzelfde patroon. We zullen twee methoden presenteren: de ene is gebaseerd op een bewerking [POST], de andere op een bewerking [GET].

Een voorbeeld van [GET]: [getCategorieByNameWithProduits]


@Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        // ----------- een categorie op naam ophalen, samen met de bijbehorende producten
        try {
            // query
            Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
                    new TypeReference<Response<Categorie>>() {
                    });
            // fout?
            if (response.getStatus() != 0) {
                // er wordt 1 uitzondering gegenereerd
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // de kern van het antwoord van de server wordt teruggestuurd
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(113, e2);
        }
    }
  • regel 7: de methode [getResponse] van de bovenliggende klasse wordt aangeroepen. Deze methode zorgt voor de communicatie met de webservice / jSON. De parameters zijn als volgt:

getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
  • (vervolg)
    • de URL van de opgevraagde webservice [/getCategorieByNameWithProduits/nom];
    • de verzonden waarde. Hier is er geen;

De methode [getResponse] retourneert een String-type, namelijk het antwoord jSON dat door de server is verzonden. Dit antwoord jSON wordt als volgt gedeserialiseerd:


jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<Categorie>>() {
});

omdat de tekenreeks jSON de serialisatie is van een type [Response<Categorie>];

  • regels 11-17: we controleren de status van het antwoord. Als de status niet 0 is, betekent dit dat er een fout aan de serverzijde is opgetreden. We genereren dan een uitzondering (regel 13), waarbij we de informatie uit het antwoord (status en lijst met foutmeldingen) overnemen;
  • regel 16: als er geen fout aan de serverzijde is opgetreden, wordt de body van het type [Response<Categorie>] geretourneerd, d.w.z. de gevraagde categorie;
  • regels 18-19: de in regel 16 gegenereerde uitzondering wordt afgehandeld;
  • regels 20-22: verwerken alle andere uitzonderingen;

Een voorbeeld van [POST]: [addCategories]


@Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        // ----------- categorieën toevoegen (met hun producten)
        try {
            // verzoek
            Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
                    new TypeReference<Response<List<Categorie>>>() {
                    });
            // fout?
            if (response.getStatus() != 0) {
                // er wordt 1 uitzondering gegenereerd
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // de kern van het serverantwoord wordt weergegeven
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(104, e2);
        }
    }
  • regel 2: de methode [addCategories] wordt gebruikt om de als parameter doorgegeven categorieën in de database op te slaan. Deze methode voegt de primaire sleutels toe aan deze categorieën. Als de categorieën samen met producten worden doorgegeven, worden deze ook opgeslagen;
  • regel 7: de methode [getResponse] van het bovenliggende object wordt aangeroepen om gegevens uit te wisselen met de webservice / jSON;
    • de eerste parameter is de URL [/addCategories];
    • de tweede parameter is de verzonden waarde, in dit geval de lijst met categorieën die moeten worden opgeslagen;

getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))

De verkregen tekenreeks jSON wordt vervolgens gedeserialiseerd om het verwachte type [Response<List<Categorie>] te verkrijgen:


Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<List<Categorie>>>() {
                    });
  • regels 11-17: verwerking van het antwoord van de server (fout of geen fout);
  • regels 20-22: afhandeling van uitzonderingen;

Alle andere methoden volgen het stramien van de twee hierboven beschreven methoden.

13.6.4. De test JUnit

Laten we terugkeren naar de client/server-architectuur die momenteel wordt opgebouwd:

We hebben een laag [DAO] [2] gebouwd met dezelfde interface als de laag [DAO] [4]. Om de laag [DAO] [2] te testen, kunnen we dus de test JUnit gebruiken die is gebruikt om de laag [DAO] [4] te testen. Ter herinnering: deze luidt als volgt:

  

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 {

    // laag [DAO]
    @Autowired
    private IDao dao;

    // filters 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() {
        // de database wordt vóór elke test opgeschoond
        log("Vidage de la base de données", 1);
        // de tabel [CATEGORIES] wordt geleegd – als gevolg daarvan wordt de tabel [PRODUITS] ook geleegd
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // de tabellen worden gevuld
        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);
        }
        // de categorie toevoegen – hierdoor worden ook de producten ingevoerd
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // lijst met categorieën
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // lijst met producten
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // enkele controles
        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);
        // we voeren de invoeging uit
        Categorie categorie = new Categorie("cat1");
        categorie.addProduit(new Produit("x", 1.0, ""));
        categorie.addProduit(new Produit("x", 1.0, ""));
        // categorie toevoegen – de producten worden vervolgens automatisch ook toegevoegd
        try {
            categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
        } catch (DaoException e) {
            show("Les erreurs suivantes se sont produites :", e.getErreurs());
        }
        // controles
        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);
        // controles
        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);
        // controle
        affiche(produits, jsonMapperProduitWithoutCategorie);
    }

    // -------------- privé-methoden
    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;
    }

    // weergave van een element van het type T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // weergave van een lijst met elementen van het type 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) {
        // bericht weergeven
        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) {
        // titel
        System.out.println(String.format("%s : ", title));
        // berichten
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}

De uitvoering is geslaagd en levert de volgende resultaten op de console op:


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