18. Een client die is geprogrammeerd voor de webservice / jSON
Nu de database [dbproduitscategories] 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 drie lagen bestaan:
- een laag [Client HTTP] [3] om te communiceren met de webapplicatie / jSON die de database beschikbaar stelt;
- een laag [DAO] [2] die dezelfde interface zal bieden als de laag [DAO] [4];
- een testlaag JUnit [1] om te controleren of de client en de server naar behoren functioneren;
18.1. Het Eclipse-project
Het Eclipse-project van de client is als volgt:
![]() |
![]() | ![]() | ![]() |
![]() | ![]() |
- het pakket [spring.webjson.client.config] bevat de Spring-configuratie van de laag [DAO];
- het pakket [spring.webjson.client.dao] bevat de implementatie van de laag [DAO];
- het pakket [spring.webjson.client.entities] bevat de objecten die worden uitgewisseld met de webservice / jSON. We kennen ze allemaal;
- het pakket [spring.webjson.client.infrastructure] bevat de uitzonderingsklassen die door het project worden gebruikt. We kennen ze allemaal;
18.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>dvp.spring.database</groupId>
<artifactId>spring-webjson-client-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<name>spring-webjson-client-generic</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.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>
<!-- door Spring gebruikte component 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>
</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>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- regels 16-20: het bovenliggende Maven-project [spring-boot-starter-parent] waarmee we een aantal afhankelijkheden kunnen definiëren zonder hun versie, aangezien deze in het bovenliggende project is gedefinieerd;
- regels 24-27: 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 29-36: een bibliotheek jSON;
- regels 38-41: een afhankelijkheid waarmee we een timeout kunnen koppelen aan de verzoeken HTTP 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 43-48: de Google Guava-bibliotheek;
- regels 50-53: de logboekbibliotheek;
- regels 54-64: de afhankelijkheid voor de tests JUnit. Deze bevat met name de bibliotheek JUnit 4 die nodig is voor de tests. Deze afhankelijkheden hebben het kenmerk [<scope>test</scope>], wat aangeeft dat ze alleen nodig zijn voor de testfase. Ze worden niet opgenomen in het uiteindelijke archief van het project;
18.3. Spring-configuratie
![]() |
De klasse [AppConfig] zorgt voor de Spring-configuratie van de client HTTP. De code ervan is als volgt:
package spring.webjson.client.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
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;
@Configuration
@ComponentScan({ "spring.webjson.client.dao" })
public class AppConfig {
// constanten
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8081";
// filters jSON
@Bean
public ObjectMapper jsonMapper(RestTemplate restTemplate) {
return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
public RestTemplate restTemplate(int timeout) {
// component aanmaken RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// converter jSON
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(new MappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters(messageConverters);
// time-out bij uitwisselingen
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// resultaat
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
}
- regel 20: de klasse is een Spring-configuratieklasse;
- regel 21: andere Spring-componenten moeten worden opgehaald uit het pakket [spring.webjson.client.dao];
- regel 25: er wordt een timeout van één seconde (1000 ms) ingesteld;
- regels 88-91: de bean die deze waarde retourneert;
- regel 26: de URL van de webservice / jSON;
- regels 93-96: de bean die deze waarde retourneert;
- regels 72-86: 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] op regel 89 wordt doorgegeven als parameter aan de methode [restTemplate] op regel 73;
- regel 75: de component [HttpComponentsClientHttpRequestFactory] is de component waarmee we de timeout voor de uitwisselingen kunnen instellen (regels 82-83);
- regel 76: de klasse [RestTemplate] wordt opgebouwd met deze component. Aangezien deze klasse op deze component steunt om te communiceren met de webservice / jSON, zullen de uitwisselingen inderdaad worden onderworpen aan de timeout;
- regels 78-80: aan de klasse [RestTemplate] wordt een converter jSON gekoppeld. We hebben dit al besproken bij de bespreking van de webservice. De client en de server wisselen tekstregels uit. 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. Hier hebben we slechts één converter jSON, aangezien de uitgewisselde tekstregels van het type jSON zijn;
- regels 82-83: hier worden de timeout-instellingen voor de uitwisselingen vastgelegd;
- regels 28-70: hier worden jSON-filters gedefinieerd. Dit zijn dezelfde als die van de server, zoals beschreven in paragraaf 17.3.2.1;
- regels 29-32: de bean [jsonMapper] is de mapper jSON van de converter [MappingJackson2HttpMessageConverter] die we hebben gekoppeld aan de klasse [RestTemplate]. We hebben deze nodig in de definitie van de filters jSON;
- regels 34-41: een bean die het filter jSON [catégorie sans ses produits] definieert. De methode [jsonMapperShortCategorie] ontvangt als parameter de bean [restTemplate] die in regel 73 is gedefinieerd;
- regel 37: de methode [jsonMapper] uit regel 30 wordt aangeroepen om de mapper jSON op te halen;
- regels 38-39: het filter wordt ingesteld om een categorie zonder producten weer te geven;
- regel 40: de mapper jSON wordt op deze manier geconfigureerd;
- regels 42-51: het filter jSON om een categorie met de bijbehorende producten te krijgen;
- regels 53-60: het filter jSON om een product zonder zijn categorie te krijgen;
- regels 62-70: het filter 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 tests JUnit.
18.4. Implementatie van de client HTTP
![]() |
Hierboven is de laag [Client HTTP] te zien die communiceert met de webservice die we zojuist hebben gebouwd. We gaan deze nu nader bekijken.
![]() |
De klasse [Client] implementeert de communicatie met de webservice / jSON. Deze klasse implementeert de volgende interface [IClient]:
package spring.webjson.client.dao;
import org.springframework.http.HttpMethod;
public interface IClient {
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body);
}
De interface heeft slechts één methode, [getResponse]:
- regel 6: de methode [getResponse] is een generieke methode die wordt gedefinieerd door twee typen:
- [T1]: is het type antwoord dat van de server wordt verwacht in [Response<T1>], bijvoorbeeld [List<Categorie>],
- [T2]: is het type van de parameter jSON die wordt verzonden door de bewerkingen POST, bijvoorbeeld [List<Produit>];
- regel 6: de methode [getResponse] levert een resultaat op van het type T1, bijvoorbeeld [List<Categorie>];
- regel 6: de parameters van [getResponse] zijn als volgt:
- [String url]: de URL die moet worden opgevraagd;
- [HttpMethod method]: methode HTTP van de aanvraag, GET of POST, al naar gelang het geval;
- [int errStatus]: foutcode die moet worden gebruikt in de klasse [DaoException], indien er een fout optreedt tijdens de communicatie met de server,
- [T2 body]: de waarde die moet worden verzonden indien POST aanwezig is;
De klasse [Client] implementeert de interface [IClient] als volgt:
package spring.webjson.client.dao;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import spring.webjson.client.infrastructure.DaoException;
@Component
public class Client implements IClient {
// injecties
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// lokaal
private String simpleClassName = getClass().getSimpleName();
// algemeen verzoek
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
}
// lijst met foutmeldingen van een uitzondering
protected List<String> getMessagesForException(Exception exception) {
...
}
}
- regel 18: de klasse [Client] is een Spring-component die dus in andere Spring-componenten kan worden geïnjecteerd;
- regels 22-23: injectie van de bean [RestTemplate], gedefinieerd in [AppConfig] (zie paragraaf 18.3), die zorgt voor de communicatie met de server;
- regels 24-25: injectie van de URL van de webservice / jSON, gedefinieerd in [AppConfig] (zie paragraaf 18.3);
- regels 37-39: de privémethode [getMessagesForException] is een hulpprogramma waarmee de lijst met foutmeldingen in een uitzondering kan worden opgehaald. We zijn deze methode al meerdere keren tegengekomen;
Laten we verdergaan:
// algemeen verzoek
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
// het antwoord van de server
ResponseEntity<Response<T1>> response;
try {
// het verzoek wordt voorbereid
RequestEntity<?> request = null;
if (method == HttpMethod.GET) {
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
if (method == HttpMethod.POST) {
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(body);
}
// het verzoek wordt uitgevoerd
response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
});
} catch (Exception e) {
// de uitzondering wordt ingekapseld
throw new DaoException(errStatus, e, simpleClassName);
}
...
}
- regel 18: 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, maar alleen de methode [exchange] ondersteunt generieke parameters. Daarom is voor deze methode gekozen. De tweede parameter bepaalt het type van het verwachte antwoord. De eerste parameter is het verzoek van het type [RequestEntity] (regel 8). Het resultaat van de methode [exchange] is van het type [ResponseEntity<Response<T1>>] (regel 5). Het type [ResponseEntity] omvat het volledige antwoord van de server, inclusief de headers HTTP en het door de server verzonden document. Evenzo omvat het type [RequestEntity] het volledige verzoek van de client, inclusief de headers HTTP en de eventuele verzonden waarde;
- regels 8-16: we moeten het verzoek van het type [RequestEntity] samenstellen. Dit verschilt naargelang we een GET of een POST gebruiken om het verzoek te doen;
- regel 10: 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 van het type 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;
- regel 14: 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 aan elkaar te koppelen:
- 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);
- regels 20-23: als er een communicatiefout met de server optreedt, wordt er een uitzondering van het type [DaoException] gegenereerd met als foutcode de parameter [errStatus] die als derde parameter wordt doorgegeven aan de generieke methode [getResponse] (regel 3);
De methode [getResponse] verloopt als volgt:
// generiek verzoek
@Override
public <T1, T2> T1 getResponse(String url, HttpMethod method, int errStatus, T2 body) {
...
// de hoofdtekst van het antwoord wordt opgehaald
Response<T1> entity = response.getBody();
int status = entity.getStatus();
// fouten aan de serverzijde?
if (status != 0) {
// er wordt een uitzondering gegenereerd
throw new DaoException(status, new RuntimeException(entity.getException()), simpleClassName);
} else {
// het is in orde
return entity.getBody();
}
}
- regel 4: we hebben het antwoord van de server ontvangen. Dit is van het type [ResponseEntity<Response<T1>>] (regel 5 van de eerder besproken code), waarbij de klasse [Response] de klasse is die al aan de serverzijde wordt gebruikt:
package spring.webjson.client.dao;
public class Response<T> {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// de eventuele uitzondering
private String exception;
// de inhoud van het antwoord
private T body;
// constructors
public Response() {
}
public Response(int status, String exception, T body) {
this.status = status;
this.exception = exception;
this.body = body;
}
// getters en setters
...
}
Laten we teruggaan naar de methode [getResponse]:
- regel 6: we halen het document van het type [Response<T1>] op dat in het antwoord is ingekapseld. Dit type heeft de velden [int status, String exception, T1 body];
- regel 7: we halen de [status] uit het antwoord op, wat een foutcode is;
- regels 9-12: als er een fout is, genereren we een uitzondering met de twee gegevens [status, exception] uit het antwoord van de server;
- regel 14: anders geven we het type [T1] terug dat in het antwoord van het type [Response<T1>] is opgenomen;
De klasse [Client] is algemeen. Deze kan worden gebruikt voor elke webclient / jSON.
18.5. Implementatie van de laag [Dao]
![]() |
![]() |
18.5.1. De klasse [AbstractDao]
De client-side laag [DAO] heeft dezelfde interface als de server-side laag [DAO] (zie paragraaf 4.7):
package spring.webjson.client.dao;
import java.util.List;
import spring.webjson.client.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// lijst van alle T-entiteiten
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// van specifieke entiteiten – korte versie
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// van specifieke entiteiten – lange versie
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// meerdere entiteiten bijwerken
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// verwijdering van alle entiteiten
public void deleteAllEntities();
// verwijdering van meerdere entiteiten
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
De klasse [AbstractDao] implementeert de interface [IDao]. Dit is een klasse die vergelijkbaar is met de gelijknamige klasse aan de serverzijde (zie paragraaf 4.8). Ze fungeert als bovenliggende klasse voor de klassen [DaoCategorie] en [DaoProduit]. Ze is om twee redenen niet identiek:
- aan de serverzijde beheert de klasse [AbstractDao] één stuk informatie:
// toevoegingen
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
die we hier niet nodig hebben.
- Aan de serverzijde gebruikt de klasse [AbstractDao] annotaties [@Transactional] om elke methode in een transactie in te kapselen. Aan de clientzijde is er geen database om te beheren. Deze annotatie verdwijnt dus;
De klasse [AbstractDao] beperkt zich tot het controleren van de geldigheid van de aanroepparameters van de methoden van de interface [IDao], alvorens de aanroep door te geven aan de onderliggende klassen:
package spring.webjson.client.dao;
import java.util.ArrayList;
import java.util.List;
import spring.webjson.client.entities.AbstractCoreEntity;
import spring.webjson.client.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {
// lokaal
protected String simpleClassName = getClass().getSimpleName();
@Override
public List<T1> getShortEntitiesById(Iterable<Long> ids) {
// geldigheid van het argument
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// resultaat
return getShortEntitiesById(Lists.newArrayList(ids));
}
@Override
public List<T1> getShortEntitiesById(Long... ids) {
// geldigheid van het argument
List<T1> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// resultaat
return getShortEntitiesById(Lists.newArrayList(ids));
}
...
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T1... entities) {
...
}
// privé-methoden ----------------------------------------------
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T3> elements) {
// nul-elementen?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),
simpleClassName);
}
// lege elementen?
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),simpleClassName);
} else {
return new ArrayList<T1>();
}
}
// standaardresultaat
return null;
}
@SuppressWarnings("unchecked")
private <T3> List<T1> checkNullOrEmptyArgument(boolean checkEmpty, T3... elements) {
// null-elementen?
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"),simpleClassName);
}
// lege elementen?
if (elements.length == 0) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("L'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T1>();
}
}
// standaardresultaat
return null;
}
// beschermde methoden ----------------------------------------------
abstract protected List<T1> getShortEntitiesById(List<Long> ids);
abstract protected List<T1> getShortEntitiesByName(List<String> names);
abstract protected List<T1> getLongEntitiesById(List<Long> ids);
abstract protected List<T1> getLongEntitiesByName(List<String> names);
abstract protected List<T1> saveEntities(List<T1> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
18.5.2. De klasse [DaoCategorie]
![]() |
De klasse [DaoCategorie] ziet er als volgt uit:
package spring.webjson.client.dao;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import spring.webjson.client.entities.Categorie;
import spring.webjson.client.entities.CoreCategorie;
import spring.webjson.client.entities.CoreProduit;
import spring.webjson.client.entities.Produit;
import spring.webjson.client.infrastructure.DaoException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ApplicationContext context;
@Autowired
private IClient client;
...
}
- regel 19: de klasse [DaoClient] is een Spring-component waarin dus andere Spring-componenten kunnen worden geïnjecteerd;
- regel 20: de klasse [DaoClient] is een uitbreiding van de klasse [AbstractDao<Categorie>] die we zojuist hebben bekeken en implementeert dus de interface [IDao<Categorie>];
- regels 22-23: we injecteren de Spring-context om toegang te krijgen tot de beans;
- regels 24-25: we injecteren de client HTTP die we zojuist hebben gebouwd;
De implementaties van de verschillende methoden van de interface [DaoCategorie] volgen allemaal hetzelfde patroon. We zullen drie methoden bespreken: één die is gebaseerd op een bewerking [GET], en de andere twee op een bewerking [POST].
18.5.2.1. De methode [getAllLongEntities]
De methode [getAllLongEntities] genereert de lange versie van alle categorieën in de database:
@Override
public List<Categorie> getAllLongEntities() {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// alle categorieën ophalen
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// de lijst met categorieën List<Categorie>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// de koppeling product --> categorie opnieuw maken
return linkCategorieWithProduits(categories);
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(233, e2, simpleClassName);
}
}
- regel 2: de methode geeft de lijst met categorieën in hun lange versies weer;
- regel 5: de mapper jSON waarmee de verzonden waarde (er is er geen) kan worden geserialiseerd en het antwoord van de klasse [Client] (categorieën in hun volledige vorm) kan worden gedeserialiseerd;
- regel 7: de methode [getResponse] van de klasse [Client] wordt aangeroepen. Deze methode zorgt voor de communicatie met de webservice / jSON. De parameters zijn als volgt:
- de URL van de opgevraagde webservice [/getAllLongCategories];
- de te gebruiken methode [GET];
- de foutcode die moet worden gebruikt als er een fout optreedt (232);
- de verzonden waarde. Hier is er geen;
- regel 7: in de uitdrukking [client.<List<Categorie>, Void>] worden de daadwerkelijke parameters van de generieke typen [T1, T2] van de methode [getResponse] aangeduid. Ter herinnering: [T1] is het type van het verwachte antwoord en [T2] het type van de doorgegeven waarde. Hier wordt een resultaat van het type [List<Categorie>] verwacht en is er geen verzonden waarde van het type [Void];
- regel 7: het resultaat van de methode [getResponse] wordt opgeslagen in een object van het type [Object]. Dat is een beetje vreemd, aangezien we een type [List<Categorie>] verwachten. Dit komt doordat de methode [getResponse], die werkt met generieke typen [T1, T2], systematisch een type [java.util.LinkedHashMap] retourneert, dat vervolgens moet worden gebruikt om het juiste type te genereren;
- regel 9: de lijst met categorieën wordt weergegeven. Hiervoor serialiseren we het object [map] [mapper.writeValueAsString(map)] naar een tekenreeks jSON, die we vervolgens opnieuw serialiseren naar een type [List<Categorie>];
- regel 13: we hebben een lijst met categorieën ontvangen, waarvan sommige producten kunnen bevatten. We ontvangen de verkorte versie van deze producten. Wanneer ze dus worden gedeserialiseerd, hebben de aangemaakte objecten [Produit] het veld [categorie==null]. De methode [linkCategorieWithProduits] herstelt de koppeling tussen een [Produit] en zijn [Categorie];
- regels 14-15: de uitzondering van het type [DaoException] die de methode [getResponse] had kunnen genereren, wordt onderbroken om deze onmiddellijk opnieuw te genereren. Dit vreemde gedrag is te wijten aan het feit dat, als we dit niet doen, de uitzondering van het type [DaoException] door de regels 16-18 wordt onderbroken, en dat willen we niet;
- regels 16-18: alle andere uitzonderingen worden opgevangen om ze in te kapselen in een type [DaoException]. Ter herinnering: de laag [DAO] mag alleen dit type uitzondering genereren;
De methode [linkCategorieWithProduits], die de koppelingen tussen entiteiten van het type [Produit] en entiteiten van het type [Categorie] opnieuw tot stand brengt, is als volgt:
private List<Categorie> linkCategorieWithProduits(List<Categorie> categories) {
for (Categorie categorie : categories) {
List<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : produits) {
produit.setCategorie(categorie);
}
}
}
return categories;
}
18.5.2.2. Beheer van filters jSON
Laten we terugkomen op het beheer van de filters jSON in de vorige methode [getAllLongEntities]:
@Override
public List<Categorie> getAllLongEntities() {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// alle categorieën ophalen
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// de lijst met categorieën List<Categorie>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
- regel 5: we halen uit de Spring-context een mapper jSON op die de lange versies van de categorieën kan verwerken. Laten we nog eens kijken naar de definitie van deze mapper in de Spring-configuratie [AppConfig]:
// filters jSON
@Bean
public ObjectMapper jsonMapper(RestTemplate restTemplate) {
return ((MappingJackson2HttpMessageConverter) (restTemplate.getMessageConverters().get(0))).getObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie(RestTemplate restTemplate) {
ObjectMapper jsonMapper = jsonMapper(restTemplate);
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
public RestTemplate restTemplate(int timeout) {
...
}
- de bean [jsonMapperLongCategorie] die door de methode [getAlllongEntities] wordt aangevraagd, is de bean uit de regels 7-15;
- regel 10: de mapper wordt geleverd door de methode [jsonMapper] uit de regels 2-5. We zien dat deze mapper jSON die is van het object [RestTemplate], dat de uitwisselingen HTTP tussen de client en de server beheert. Deze mapper wordt standaard gebruikt om:
- het serialiseren van de naar de server verzonden waarde;
- het deserialiseren van het door de server teruggestuurde antwoord;
Laten we teruggaan naar de code van [getAllLongEntities]:
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// alle categorieën ophalen
Object map = client.<List<Categorie>, Void> getResponse("/getAllLongCategories", HttpMethod.GET, 232, null);
// de lijst met categorieën List<Categorie>
List<Categorie> categories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<Categorie>>() {
});
// de koppeling product --> categorie opnieuw maken
return linkCategorieWithProduits(categories);
- regel 2: we halen de mapper [jsonMapperLongCategorie] op uit de Spring-context;
- regel 4: de methode [getResponse] wordt uitgevoerd. Er vindt dan het volgende plaats:
- automatische serialisatie van de verzonden waarde (die is hier niet aanwezig);
- automatische deserialisatie van het ontvangen antwoord, in dit geval van het type List<Categorie>. Omdat de entiteit [Categorie] een filter jSON [jsonFilterCategorie] heeft, moest dit worden afgehandeld. Dat is de reden voor regel 2;
- regel 6: het resultaat ondergaat een tweede serialisatie/deserialisatie met dezelfde mapper om het type List<Categorie> te verkrijgen. Regel 4: het type dat door [getResponse] wordt geretourneerd, is een type [Object];
In de volgende methoden moeten we in gedachten houden dat de mapper jSON, die bij de Spring-context wordt opgevraagd, zowel voor de verzonden waarde (serialisatie) als voor de ontvangen waarde (deserialisatie) wordt gebruikt. Als een of beide waarden een filter jSON hebben, moeten deze worden geconfigureerd. De mapper kan dus maximaal twee geconfigureerde filters hebben. In het onderstaande voorbeeld komt dit nooit voor. Ofwel heeft de verzonden waarde geen filter (List<Long>, List<String>), ofwel heeft de ontvangen waarde er geen (List<CoreCategorie>, List<CoreProduit>). De entiteiten met een filter jSON zijn uitsluitend [Categorie] en [Produit].
18.5.2.3. De methode [getShortEntitiesById]
De methode [getShortEntitiesById] genereert de verkorte versies van de categorieën waarvan de primaire sleutels als parameters worden doorgegeven:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// een categorie ophalen zonder de bijbehorende producten
Object map = client.<List<Categorie>, List<Long>> getResponse("/getShortCategoriesById", HttpMethod.POST, 204, ids);
// de categorie
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(223, e2, simpleClassName);
}
}
- regel 5: de mapper jSON die het mogelijk maakt de geposte waarde (een lijst met primaire sleutels) te serialiseren en het antwoord van de klasse [Client] (categorieën in hun verkorte versies) te deserialiseren. Het gekozen filter heeft geen invloed op de verzonden waarde, aangezien er voor de elementen van de verzonden lijst geen filter is;
- 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:
- de URL van de opgevraagde webservice [/getShortCategoriesById];
- de te gebruiken methode [POST];
- de foutcode die moet worden gebruikt als er een fout optreedt (204);
- de verzonden waarde. In dit geval is dat een lijst met primaire sleutels;
- regel 7: in de uitdrukking [client.<List<Categorie>, List<Long>>] worden de daadwerkelijke parameters van de generieke typen [T1, T2] van de methode [getResponse] aangeduid. Ter herinnering: [T1] is het type van het verwachte antwoord en [T2] het type van de doorgegeven waarde. Hier wordt een resultaat van het type [List<Categorie>] verwacht en is de verzonden waarde een lijst met primaire sleutels van het type [List<Long>];
- regel 7: het resultaat van de methode [getResponse] wordt opgeslagen in een object van het type [Object];
- regel 9: de lijst met categorieën wordt geretourneerd. Hiervoor wordt het object [map] [mapper.writeValueAsString(map)] geserialiseerd tot een tekenreeks jSON, die vervolgens opnieuw wordt geserialiseerd naar het type [List<Categorie>];
18.5.2.4. De methode [saveEntities]
De methode [saveEntities] slaat categorieën op in de database. De code ervan is als volgt:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
// categorieën toevoegen
Object map = client.<List<CoreCategorie>, List<Categorie>> getResponse("/saveCategories", HttpMethod.POST, 200,
entities);
// de lijst met toegevoegde hoofdcategorieën
List<CoreCategorie> coreCategories = mapper.readValue(mapper.writeValueAsString(map),
new TypeReference<List<CoreCategorie>>() {
});
// de categorieën worden bijgewerkt met de ontvangen informatie
for (int i = 0; i < entities.size(); i++) {
Categorie categorie = entities.get(i);
CoreCategorie coreCategorie = coreCategories.get(i);
categorie.setId(coreCategorie.getId());
List<Produit> produits = categorie.getProduits();
if (produits != null) {
List<CoreProduit> coreProduits = coreCategorie.getCoreProduits();
for (int j = 0; j < produits.size(); j++) {
Produit produit = produits.get(j);
produit.setId(coreProduits.get(j).getId());
produit.setIdCategorie(categorie.getId());
produit.setCategorie(categorie);
}
}
}
return entities;
} catch (DaoException e1) {
throw e1;
} catch (Exception e2) {
throw new DaoException(220, e2, simpleClassName);
}
}
- regel 2: de methode [saveEntities] dient 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 eveneens opgeslagen;
- regel 5: de mapper jSON, waarmee de verzonden waarde (een lijst met categorieën in hun volledige vorm) kan worden geserialiseerd en het antwoord van de klasse [Client] (objecten van het type [CoreCategorie]) kan worden gedeserialiseerd. Het gekozen filter heeft geen invloed op het resultaat, aangezien de elementen van de lijst die als antwoord wordt ontvangen geen filter hebben;
- regel 7: de methode [getResponse] van de bovenliggende klasse wordt aangeroepen om de communicatie met de webservice / jSON te verzorgen;
- de eerste parameter is de URL [/saveCategories];
- de tweede parameter is de te gebruiken methode HTTP, in dit geval een [POST];
- de derde parameter is de foutcode die moet worden gebruikt als er een fout optreedt (200);
- de laatste parameter is de verzonden waarde, in dit geval de lijst met categorieën die moeten worden opgeslagen;
- regel 7: de generieke parameters [T1, T2] van de methode [getResponse] zijn hier [List<CoreCategorie>, List<Categorie>]. Het eerste type is dat van het verwachte antwoord, het tweede het type van de verzonden waarde;
- regel 7: het verkregen antwoord wordt in een type [Object] geplaatst;
- regel 9: het antwoord van het type [List<CoreCategorie>] wordt gereconstrueerd. Het te retourneren antwoord is van het type [List<Categorie>] (regel 2) en niet [List<CoreCategorie>]. Het ontvangen antwoord is de lijst met primaire sleutels van de opgeslagen categorieën en producten;
- regels 14-28: de ontvangen primaire sleutels worden toegewezen aan de categorieën en producten (regels 17, 23, 24). Daarnaast worden de koppelingen [Produit] --> [Categorie] (regels 24-25) gereconstrueerd;
Alle andere methoden volgen hetzelfde stramien.
18.6. 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 tests JUnit gebruiken die zijn gebruikt om de laag [DAO] [4] te testen:
![]() |
Deze drie tests worden uitgevoerd op basis van de volgende uitvoeringsconfiguraties:
![]() | ![]() |
![]() |
De resultaten van de drie tests zijn als volgt:
![]() |
![]() |
- in [1], de test [JUnitTestCheckArguments];
- in [2], de test [JUnitTestDao];
- in [3], de test [JUnitTestPushTheLimits] uitgevoerd aan de clientzijde (project [spring-webjson-client-generic]);
- in [3], de test [JUnitTestPushTheLimits] die aan de serverzijde wordt uitgevoerd (project [spring-jdbc-generic-04]). We stellen vast dat de netwerklaag zeer weinig vertraging veroorzaakt in vergelijking met de vertraging die wordt veroorzaakt door de toegang tot SGBD;
18.7. Implementatie van de webservice / jSON / JPA / Hibernate
We richten ons nu op de volgende architectuur:
![]() |
De wijziging bevindt zich in [1]. De [DAO]-laag van de server is gebaseerd op een implementatie van JPA. We gaan eerst een implementatie JPA / Hibernate gebruiken.
18.7.1. Het Eclipse-project
Op dit moment zijn de volgende projecten in Eclipse geladen:
![]() |
Het project [spring-webjson-server-jdbc-generic] was gebaseerd op het project [spring-jdbc-generic-04], dat de laag DAO / JDBC voor toegang tot SGBD en MySQL. We gaan een nieuw project [spring-webjson-server-jpa-generic] aanmaken, dat op zijn beurt zal voortbouwen op het project [spring-jpa-generic], dat de laag DAO / JPA / JDBC voor toegang tot SGBD en MySQL. We weten dat in beide gevallen de laag [DAO] dezelfde interface [IDao] implementeert. De code van de laag [web] verandert dus niet.
We kunnen het project [spring-webjson-server-jpa-generic] aanmaken door het project [spring-webjson-server-jdbc-generic] te kopiëren en te plakken:
![]() |
- naar [1], en een map aanwijzen die speciaal voor het nieuwe project is aangemaakt;
![]() |
Er moeten drie soorten wijzigingen worden aangebracht. De eerste staan in het Maven-configuratiebestand [pom.xml] van het project:
<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>dvp.spring.database</groupId>
<artifactId>spring-webjson-server-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-webjson-server-jpa-generic</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- weblaag -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- laag [DAO] -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- plug-ins -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- regel 5: de naam van het Maven-artefact wordt gewijzigd;
- regels 24-28: de afhankelijkheid verwijst nu naar het project [spring-jpa-generic] en niet meer naar [spring-jdbc-generic-04];
Uiteindelijk zijn de afhankelijkheden als volgt:
![]() |
Zodra dit is gebeurd, worden alle importproblemen opgelost die in de verschillende klassen zijn opgetreden. Zo hoeven de entiteiten [Produit, Categorie] bijvoorbeeld niet meer in het project [spring-jdbc-generic-04] te worden gezocht, maar in het project [spring-jpa-generic]. Door in de code van een klasse [Ctrl-Maj-O] in te voeren, worden de imports automatisch opnieuw gegenereerd.
De laatste wijziging moet worden aangebracht in het configuratiebestand [AppConfig]:
package spring.webjson.server.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan(basePackages = { "spring.webjson.server.service" })
@Import({ spring.data.config.AppConfig.class, WebConfig.class })
public class AppConfig {
}
- regel 9: we importeren nu de configuratie van het project [spring-jpa-generic] en niet langer die van het project [spring-jdbc-generic-04];
Nu dit is gebeurd, zijn we klaar. We starten de webservice met de configuratie [spring-webjson-server-jpa-generic-hibernate-eclipselink]:
![]() | ![]() |
Vervolgens voeren we de drie tests van de generieke client [spring-webjson-client-generic] uit:
![]() |
![]() |
- in [1], de test [JUnitTestCheckArguments] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestCheckArguments]);
- in [2], de test [JUnitTestDao] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestDao]);
- in [3], de client-side uitgevoerde test [JUnitTestPushTheLimits] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- in [4], de test [JUnitTestPushTheLimits] uitgevoerd aan de serverzijde (uitvoeringsconfiguratie [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);
18.7.2. Waarom werkt dit?
Het werkt, en toch is het, als je goed naar de code kijkt, verbazingwekkend dat het werkt. Hoewel de [DAO]-lagen die door de projecten [spring-jdbc-generic-04] en [spring-jpa-generic] zijn geïmplementeerd weliswaar dezelfde interface hebben, dan werken ze niet met dezelfde entiteiten [Categorie] en [Produit]: in het project [spring-jpa-generic] hebben deze entiteiten een extra veld [EntityType entityType] dat twee mogelijke waarden heeft:
- EntityType.POJO: de entiteit is een normaal object waarvan alle velden vrij kunnen worden gebruikt;
- EntityType.PROXY: de entiteit is een PROXY-object dat wordt weergegeven door de laag [JPA]. In dit geval gedragen bepaalde velden (eigenlijk de getters van deze velden) zich niet zoals gewoonlijk en zijn de volgende regels vastgesteld:
- als [Categorie.entityType==EntityType.PROXY], dan mag de methode [getProduits] niet worden gebruikt;
- als [Produit.entityType==EntityType.PROXY], dan mag de methode [getCategorie] niet worden gebruikt;
Maar we hebben zojuist het project [spring-webjson-server-jdbc-generic] overgezet naar [spring-webjson-server-jpa-generic] zonder de code te wijzigen. Hoe is dat mogelijk?
Laten we de code van de methode [saveCategories] eens bekijken:
@RequestMapping(value = "/saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request) {
...
// de verzonden waarde wordt opgehaald
String body = CharStreams.toString(request.getReader());
// de waarde wordt gedeserialiseerd
ObjectMapper mapper = context.getBean("jsonMapperLongCategorie", ObjectMapper.class);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// de categorieën worden opgeslagen
categories = daoCategorie.saveEntities(categories);
...
}
- regel 8: er wordt een List<Categorie>-object aangemaakt op basis van een tekenreeks jSON:
- in de verzonden waarde hebben de producten geen veld [categorie]. Het is inderdaad niet nodig om dit veld te verzenden. Als we het wel zouden verzenden, zou de deserialisatie een object [Produit] aanmaken met een veld [categorie] dat verwijst naar een zojuist aangemaakt object [Categorie]. Voor n producten zouden er dan n objecten [Categorie] worden aangemaakt, terwijl er maar één nodig is. Bovendien zou het veld [categorie] van de producten niet verwijzen naar het juiste object [Categorie], namelijk het object waartoe ze behoren. Daarom hebben de producten hier een veld [categorie==null];
- in de klassen [Categorie] en [Produit] is het veld [EntityType entityType] als volgt gedefinieerd:
protected EntityType entityType = EntityType.POJO;
De entiteiten [Categorie] en [Produit] die door de serialisatie zijn aangemaakt, hebben dus allemaal het type POJO.
- regel 11: de categorieën worden opgeslagen. Dit zou hier niet moeten werken. Als in de implementatie JDBC het veld [Produit.categorie] niet nodig is voor het opslaan (het veld [idCategorie] wordt gebruikt), is het voor de implementatie JPA absoluut noodzakelijk. Dit veld moet verwijzen naar een entiteit [Categorie], maar hier is de waarde null.
Laten we de code van de methode [DaoCategorie.saveEntities] van de laag [DAO / JPA] eens bekijken:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// we noteren de producten die zullen worden toegevoegd
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// we maken van de gelegenheid gebruik om (indien nodig) de relatie product --> categorie te herstellen
produit.setCategorie(categorie);
}
}
}
// de categorieën en producten worden opgeslagen
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// het veld [idCategorie] van de toegevoegde producten wordt bijgewerkt
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// resultaat
return categories;
}
- regels 13-14: we zien dat de koppeling [Produit] --> [Categorie] wordt hersteld voor de entiteiten POJO (regel 8), wat hier het geval is. Dit verklaart waarom het behoud van de categorieën heeft gewerkt. Dit scenario is ook in andere omstandigheden nuttig: men kan er nooit zeker van zijn dat de gebruiker de producten correct aan de categorieën heeft gekoppeld. Daarom doen we dit in zijn plaats;
Laten we nu eens kijken naar de methode [ProduitController.saveProduits] die producten opslaat:
@RequestMapping(value = "/saveProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Response<List<CoreProduit>> saveProduits(HttpServletRequest request) {
...
// de verzonden waarde wordt opgehaald
String body = CharStreams.toString(request.getReader());
// de waarde wordt gedeserialiseerd
ObjectMapper mapper = context.getBean("jsonMapperShortProduit", ObjectMapper.class);
List<Produit> produits = mapper.readValue(body, new TypeReference<List<Produit>>() {
});
// de producten blijven bestaan
produits = daoProduit.saveEntities(produits);
List<CoreProduit> coreProduits = new ArrayList<CoreProduit>();
for (Produit produit : produits) {
coreProduits.add(new CoreProduit(produit.getId()));
}
// het antwoord wordt weergegeven
return new Response<List<CoreProduit>>(0, null, coreProduits);
...
}
- regel 8: er wordt een List<Product>-object gereconstrueerd op basis van de verzonden waarde. Om de eerder uitgelegde redenen zal elk [Produit]-object een veld hebben:
- [EntityType entityType] gelijk aan [EntityType.POJO];
- [Categorie categorie] is gelijk aan null;
- regel 11: het opslaan van de producten zou moeten mislukken. Met JPA is het opslaan van een product namelijk alleen mogelijk als het veld [categorie] verwijst naar een entiteit [Categorie];
Laten we eens kijken naar de code van de methode [DaoProduit.saveEntities] van de laag [DAO / JPA]:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// de koppeling tussen een product en zijn categorie wordt (indien nodig) hersteld
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// de producten worden opgeslagen
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
- regels 3-8: voor elke [Produit] van het type POJO wordt een koppeling aangemaakt naar een object [Categorie] met de juiste primaire sleutel en een versie die niet null is. Dit is voldoende om ervoor te zorgen dat de laag JPA het product correct bewaart;
Laten we nog een laatste punt bekijken. De objecten [Categorie] en [Produit] hebben een extra veld [EntityType entityType] dat zal worden geserialiseerd naar jSON wanneer deze objecten naar de client worden verzonden. Dit kunnen we controleren met [Advanced Rest Client]:
![]() |
Aan de kant van de client zijn de entiteiten [Categorie] en [Produit] gedefinieerd zonder het veld [EntityType entityType]. Dit is normaal, aangezien de objecten [Categorie] en [Produit] worden geserialiseerd zonder hun onderdelen PROXY, [Categorie.produits] en [Produit.categorie]. Aan de clientzijde bestaat er dus geen entiteit met de naam PROXY. Er zijn alleen normale objecten.
Aan de clientzijde wordt de reeks jSON [1] ontvangen door de volgende methode [DaoCategorie.getAllShortEntities]:
@Override
public List<Categorie> getAllShortEntities() {
...
// filters jSON
ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
// alle categorieën ophalen
Object map = client.<List<Categorie>, Void> getResponse("/getAllShortCategories", HttpMethod.GET, 202, null);
// de lijst met categorieën List<Categorie>
return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
});
...
}
- regel 5: de mapper jSON van het object [RestTemplate] wordt zo geconfigureerd dat deze de filters jSON en [jsonFilterCategorie] van hetobject [Categorie] en het filter [jsonFilterProduit] van het object [Produit];
- regel 7: de verzonden waarde (die hier ontbreekt) en de ontvangen waarde (List<Categorie>) worden met deze mapper geserialiseerd/deserialiseerd. We zien dat de aanwezigheid van het veld [entityType] in de ontvangen reeks jSON, terwijl dit veld niet bestaat in de entiteiten [Categorie] en [Produit] aan de clientzijde, geen fout veroorzaakt. Het wordt genegeerd. Als het een fout had veroorzaakt, zouden we de filters aan de clientzijde hebben aangepast om het te negeren.
18.8. Implementatie van de webservice / jSON / JPA / EclipseLink
Om de webservice / jSON / JPA / EclipseLink te implementeren, volstaat het om de implementatie van JPA aan te passen:
![]() |
Opmerking: druk op Alt+F5 en genereer vervolgens alle Maven-projecten opnieuw.
We starten de webservice met de uitvoeringsconfiguratie [spring-webjson-server-jpa-generic-hibernate-eclipselink] die al voor Hibernate wordt gebruikt. Voer daarna de drie tests van de generieke client [spring-webjson-client-generic] uit:
![]() |
![]() |
- in [1], de test [JUnitTestCheckArguments];
- in [2], de test [JUnitTestDao];
- in [3], de test [JUnitTestPushTheLimits] uitgevoerd aan de clientzijde (project [spring-webjson-client-generic]);
- in [4], de test [JUnitTestPushTheLimits] die aan de serverzijde wordt uitgevoerd (uitvoeringsconfiguratie [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);
18.9. Implementatie van de webservice / jSON / JPA / OpenJpa
Om de webservice / jSON / JPA / OpenJpa te implementeren, volstaat het om de implementatie JPA te wijzigen:
![]() |
Opmerking: druk op Alt+F5 en genereer vervolgens alle Maven-projecten opnieuw.
We starten de webservice met de uitvoeringsconfiguratie [spring-webjson-server-jpa-generic-openpa]:
![]() | ![]() |
Voer vervolgens de drie tests van de generieke client [spring-webjson-client-generic] uit:
![]() |
![]() |
- in [1], de test [JUnitTestCheckArguments] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestCheckArguments]);
- in [2], de test [JUnitTestDao] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestDao]);
- in [3], de client-side uitgevoerde test [JUnitTestPushTheLimits] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- in [4], de test [JUnitTestPushTheLimits] uitgevoerd aan de serverzijde (uitvoeringsconfiguratie [spring-jpa-generic-JUnitTestPushTheLimits-openpa]);
Om de tests te laten werken, moesten er wijzigingen worden aangebracht in de laag DAO / JPA. Om onbegrijpelijke redenen liepen de methoden [DaoCategorie.saveEntities] en [DaoProduit.saveEntities] namelijk vast bij het vullen van de database, waarbij werd aangegeven dat losstaande elementen niet konden worden opgeslagen. Een losstaand element is een element dat ofwel:
- een primaire sleutel die niet null is;
- een versie heeft die niet null is;
Geen van beide gevallen werd gecontroleerd. Omdat ik niet wist waar ik moest zoeken, heb ik de entiteiten die bewaard moesten worden gedupliceerd in een geheel nieuwe lijst en daar werkten de tests wel. Deze wijziging had ook kunnen worden doorgevoerd:
- in de laag [DAO / JPA];
- in de laag [web], die de te bewaren entiteiten aanmaakt;
Ik heb ervoor gekozen om dit in de laag [DAO / JPA] te doen. Er is natuurlijk sprake van prestatieverlies, maar dat is volstrekt verwaarloosbaar in vergelijking met de responstijden van SGBD. De wijzigingen zijn als volgt:
In de klasse [DaoCategorie] van het project [spring-jpa-generic]:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// ***************************************************************************************
// de lijst met categorieën klonen -- soms nodig voor OpenJpa -- bug niet begrepen
// ***************************************************************************************
List<Categorie> categories2 = new ArrayList<Categorie>();
for (Categorie categorie : categories) {
// categorie
Categorie categorie2 = new Categorie(categorie.getId(), categorie.getVersion(), categorie.getNom(), null);
EntityType categorieType = categorie.getEntityType();
categorie2.setEntityType(categorieType);
categories2.add(categorie2);
// producten
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
List<Produit> produits2 = new ArrayList<Produit>();
for (Produit produit : produits) {
Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(),
produit.getIdCategorie(), produit.getPrix(), produit.getDescription(), produit.getCategorie());
produit2.setEntityType(produit.getEntityType());
produits2.add(produit2);
}
categorie2.setProduits(produits2);
}
}
// de producten die zullen worden toegevoegd, worden genoteerd
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories2) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// we maken van de gelegenheid gebruik om (indien nodig) de relatie product --> categorie te herstellen
produit.setCategorie(categorie);
}
}
}
// de categorieën en producten worden opgeslagen
try {
categoriesRepository.save(categories2);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// het veld [idCategorie] van de toegevoegde producten wordt bijgewerkt
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// resultaat
return categories2;
}
- regels 3-25: de lijst [categories] die als parameter wordt ontvangen (regel 2) wordt gedupliceerd in de lijst [categories2] (regel 6). Het is deze lijst die wordt opgeslagen en aan de aanroeper wordt teruggegeven (regel 52). Dit heeft een belangrijk gevolg: er wordt een andere lijst teruggegeven dan de lijst die als parameter is doorgegeven, en dus waar men voorheen kon schrijven:
moet nu het volgende worden geschreven:
List<Categorie> categories=...
categories=daoCategorie.saveEntities(categories)
// verwerking van [categories]
In de klasse [DaoProduit] van het project [spring-jpa-generic] wordt de methode [saveEntities] op dezelfde manier aangepast:
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// ***************************************************************************************
// de productlijst wordt gekloond -- soms nodig voor OpenJpa -- bug niet meegerekend
// ***************************************************************************************
List<Produit> produits2 = new ArrayList<Produit>();
for (Produit produit : entities) {
Produit produit2 = new Produit(produit.getId(), produit.getVersion(), produit.getNom(), produit.getIdCategorie(),
produit.getPrix(), produit.getDescription(), produit.getCategorie());
produit2.setEntityType(produit.getEntityType());
produits2.add(produit2);
}
// de koppeling tussen een product en zijn categorie wordt (indien nodig) hersteld
for (Produit produit : produits2) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// de producten worden opgeslagen
try {
return Lists.newArrayList(produitsRepository.save(produits2));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
18.10. Implementatie van de webservice / jSON / JPA / EclipseLink / PostgreSQL
Om de webservice / jSON / JPA / EclipseLink / PostgresQL te implementeren, moet u het volgende installeren:
- het configuratieproject [postgresql-config-jdbc] voor de configuratielaag JDBC van PostgreSQL;
- het configuratieproject [postresql-config-jpa-eclipselink] van de laag JPA van PostgreSQL;
- druk op Alt-F5 en genereer alle Maven-projecten opnieuw;
![]() |
We starten SGBD en PostgreSQL en starten de webservice met de eerder gebruikte uitvoeringsconfiguratie [spring-webjson-server-jpa-generic-hibernate-eclipselink]. Vervolgens voer je de drie tests van de generieke client [spring-webjson-client-generic] uit:
![]() |
![]() |
- in [1], de test [JUnitTestCheckArguments] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestCheckArguments]);
- in [2], de test [JUnitTestDao] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestDao]);
- in [3], de client-side uitgevoerde test [JUnitTestPushTheLimits] (uitvoeringsconfiguratie [spring-webjson-client-generic-JUnitTestPushTheLimits]);
- in [4], de test [JUnitTestPushTheLimits] uitgevoerd aan de serverzijde (uitvoeringsconfiguratie [spring-jpa-generic-JUnitTestPushTheLimits-hibernate-eclipselink]);







































