Skip to content

2. De Spring 4-server

In de bovenstaande architectuur gaan we nu in op de opbouw van de webservice / JSON, gebouwd met het Spring 4-framework. We zullen dit in verschillende stappen schrijven:

  • eerst de lagen [métier] en [DAO] (Data Access Object). Hier gebruiken we Spring Data;
  • vervolgens de webservice JSON zonder authenticatie. Hiervoor gebruiken we Spring MVC;
  • daarna voegen we het authenticatiegedeelte toe met Spring Security.

We beginnen met het toelichten van de structuur van de database waarop de applicatie is gebaseerd.

2.1. De database

De database die hierna [dbrdvmedecins] wordt genoemd, is een MySQL5-database met de volgende tabellen:

  

De afspraken worden beheerd door de volgende tabellen:

  • [medecins]: bevat de lijst met artsen van de praktijk;
  • [clients]: bevat de lijst met patiënten van de praktijk;
  • [creneaux]: bevat de beschikbare tijdvakken van elke arts;
  • [rv]: bevat de lijst met afspraken van de artsen.

De tabellen [roles], [users] en [users_roles] zijn tabellen die verband houden met de authenticatie. In eerste instantie zullen we hier geen aandacht aan besteden.

De relaties tussen de tabellen die de afspraken beheren, zijn als volgt:

 
  • een tijdslot behoort toe aan een arts – een arts heeft 0 of meerdere tijdslots;
  • een afspraak brengt zowel een klant als een arts samen via een tijdslot van die arts;
  • een klant heeft 0 of meerdere afspraken;
  • aan een tijdslot zijn 0 of meerdere afspraken gekoppeld (op verschillende dagen).

2.1.1. De tabel [MEDECINS]

Deze bevat informatie over de artsen die door de applicatie [RdvMedecins] worden beheerd.

  • ID: identificatienummer van de arts – primaire sleutel van de tabel
  • VERSION: identificatienummer van de versie van de rij in de tabel. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de achternaam van de arts
  • PRENOM: zijn of haar voornaam
  • TITRE: zijn/haar aanspreektitel (mevrouw, mevrouw, meneer)

2.1.2. De tabel [CLIENTS]

De cliënten van de verschillende artsen worden opgeslagen in de tabel [CLIENTS]:

  • ID: identificatienummer van de klant – primaire sleutel van de tabel
  • VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt met 1 verhoogd telkens wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de naam van de klant
  • PRENOM: de voornaam
  • TITRE: zijn/haar aanspreektitel (mevrouw, meisje, heer)

2.1.3. De tabel [CRENEAUX]

Deze tabel geeft een overzicht van de tijdvakken waarin de RV mogelijk zijn:

  • ID: nummer dat het tijdvak identificeert – primaire sleutel van de tabel (regel 8)
  • VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • ID_MEDECIN: nummer dat de arts identificeert aan wie dit tijdslot toebehoort – vreemde sleutel op de kolom MEDECINS (ID).
  • HDEBUT: starttijd van het tijdvak
  • MDEBUT: minuten begin van het tijdvak
  • HFIN: einduur van het tijdvak
  • MFIN: minuten einde tijdslot

De tweede regel van de tabel [CRENEAUX] (zie [1] hierboven) geeft bijvoorbeeld aan dat tijdvak nr. 2 om 8.20 uur begint en om 8.40 uur eindigt en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER).

2.1.4. De tabel [RV]

Deze tabel geeft een overzicht van de RV die voor elke arts zijn vastgelegd:

  • ID: nummer dat de RV op unieke wijze identificeert – primaire sleutel
  • JOUR: dag van RV
  • ID_CRENEAU: tijdvak van RV – externe sleutel op het veld [ID] van de tabel [CRENEAUX] – bepaalt zowel het tijdvak als de betreffende arts.
  • ID_CLIENT: nummer van de klant voor wie de reservering is gemaakt – externe sleutel op het veld [ID] van de tabel [CLIENTS]

Deze tabel heeft een uniekheids -beperking op de waarden van de gekoppelde kolommen (JOUR, ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Als een rij in de tabel [RV] de waarde (JOUR1, ID_CRENEAU1) heeft voor de kolommen (JOUR, ID_CRENEAU), dan mag deze waarde nergens anders voorkomen. Anders zou dit betekenen dat er twee RV’en tegelijkertijd voor dezelfde arts zijn vastgelegd. Vanuit het oogpunt van Java-programmering start de JDBC-driver van de database een SQLException wanneer dit zich voordoet.

De regel met id gelijk aan 3 (zie [1] hierboven) betekent dat er op 23/08/2006 een RV is geboekt voor tijdvak nr. 20 en klant nr. 4. Uit de tabel [CRENEAUX] blijkt dat tijdvak nr. 20 overeenkomt met het tijdvak 16.20 - 16.40 uur en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER). Uit de tabel [CLIENTS] blijkt dat klant nr. 4 mevrouw Brigitte BISTROU is.

2.2. Inleiding tot Spring Data

We gaan de laag [DAO] van het project implementeren met Spring Data, een onderdeel van het Spring-ecosysteem.

Op de website van Spring zijn talrijke tutorials te vinden om aan de slag te gaan met Spring [http://spring.io/guides]. We gaan er een gebruiken om Spring Data te introduceren. Hiervoor gebruiken we de Spring Tool Suite (STS).

  • in [1] importeren we een van de tutorials uit [spring.io/guides];
  • in [2] kiezen we de tutorial [Accessing Data Jpa] die laat zien hoe je met Spring Data toegang krijgt tot een database;
  • in [3] kiezen we een project dat door Maven is geconfigureerd;
  • in [4] kan de tutorial in twee vormen worden aangeboden: [initial], een lege versie die je vult terwijl je de tutorial volgt, of [complete], de definitieve versie van de tutorial. We kiezen voor deze laatste;
  • in [5] kun je ervoor kiezen om de tutorial in een browser te bekijken;
  • in [6], het uiteindelijke project.

2.2.1. De Maven-configuratie van het project

De Maven-afhankelijkheden van het project worden geconfigureerd in het bestand [pom.xml]:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- gebruik UTF-8 voor alles -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • regels 5-9: definiëren een bovenliggend Maven-project. Dit project bepaalt het grootste deel van de afhankelijkheden van het project. Deze kunnen voldoende zijn, in welk geval er niets wordt toegevoegd, of onvoldoende, in welk geval de ontbrekende afhankelijkheden worden toegevoegd;
  • regels 12-15: definiëren een afhankelijkheid van [spring-boot-starter-data-jpa]. Dit artefact bevat de klassen van Spring Data;
  • regels 16-19: definiëren een afhankelijkheid van SGBD en H2, waarmee in-memory-databases kunnen worden aangemaakt en beheerd.

Laten we eens kijken naar de klassen die door deze afhankelijkheden worden geleverd:

Het zijn er heel veel:

  • sommige behoren tot het Spring-ecosysteem (die welke beginnen met spring);
  • andere behoren tot het Hibernate-ecosysteem (hibernate, jboss), waarvan we hier de implementatie JPA gebruiken;
  • weer andere zijn testbibliotheken (junit, hamcrest);
  • weer andere zijn logboekbibliotheken (log4j, logback, slf4j);

We zullen ze allemaal behouden. Voor een applicatie in productie zouden we alleen die moeten behouden die noodzakelijk zijn.

Op regel 26 van het bestand [pom.xml] staat de volgende regel:


<start-class>hello.Application</start-class>

Deze regel houdt verband met de volgende regels:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Regels 6-9: met de plug-in [spring-boot-maven-plugin] kan het uitvoerbare JAR-bestand van de applicatie worden gegenereerd. Regel 26 van het bestand [pom.xml] verwijst dan naar de uitvoerbare klasse van dit JAR-bestand.

2.2.2. De laag [JPA]

Toegang tot de database verloopt via een laag [JPA], Java Persistence API:

  

De applicatie is eenvoudig en beheert klanten [Customer]. De klasse [Customer] maakt deel uit van de laag [JPA] en ziet er als volgt uit:


package hello;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

Een klant heeft een ID [id], een voornaam [firstName] en een achternaam [lastName]. Elk exemplaar [Customer] vertegenwoordigt een rij in een databasetabel.

  • regel 8: annotatie JPA, waardoor de persistentie van de instanties [Customer] (Create, Read, Update, Delete) wordt beheerd door een implementatie JPA. Uit de Maven-afhankelijkheden blijkt dat de implementatie JPA / Hibernate wordt gebruikt;
  • regels 11-12: annotaties JPA die het veld [id] koppelen aan de primaire sleutel van de tabel [Customer]. Regel 12 geeft aan dat de implementatie JPA de methode voor het genereren van de primaire sleutel zal gebruiken die eigen is aan de gebruikte SGBD, in dit geval H2;

Er zijn geen andere annotaties voor JPA. Er worden dan standaardwaarden gebruikt:

  • de tabel van [Customer] krijgt de naam van de klasse, d.w.z. [Customer];
  • de kolommen van deze tabel krijgen de naam van de velden van de klasse: [id, firstName, lastName], waarbij hoofdletters en kleine letters in de naam van een tabelkolom niet van belang zijn;

Opgemerkt moet worden dat de gebruikte implementatie JPA op geen enkel moment bij naam wordt genoemd.

2.2.3. De laag [DAO]

  

De klasse [CustomerRepository] implementeert de laag [DAO]. De code ervan is als volgt:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Het is dus een interface en geen klasse (regel 7). Het is een uitbreiding van de interface [CrudRepository], een interface van Spring Data (regel 5). Deze interface wordt gedefinieerd door twee typen: het eerste is het type van de beheerde elementen, in dit geval het type [Customer], het tweede is het type van de primaire sleutel van de beheerde elementen, in dit geval het type [Long]. De interface [CrudRepository] ziet er als volgt uit:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

Deze interface definieert de bewerkingen CRUD (Create – Read – Update – Delete) die kunnen worden uitgevoerd op een type JPA T:

  • regel 8: met de methode save kan een entiteit T in de database worden opgeslagen. Hiermee wordt de entiteit opgeslagen met de primaire sleutel die door de SGBD is toegekend. Deze methode maakt het ook mogelijk om een entiteit T bij te werken die wordt geïdentificeerd door de primaire sleutel id. De keuze voor de ene of de andere actie hangt af van de waarde van de primaire sleutel id: als deze null is, vindt de opslagbewerking plaats, anders de bijwerkingsbewerking;
  • regel 10: idem, maar dan voor een lijst met entiteiten;
  • regel 12: met de methode findOne kan een entiteit T worden opgehaald die wordt geïdentificeerd door de primaire sleutel id;
  • regel 22: met de methode delete kan een entiteit T, geïdentificeerd door de primaire sleutel id, worden verwijderd;
  • regels 24-28: varianten van de methode [delete];
  • regel 16: met de methode [findAll] kunnen alle opgeslagen entiteiten T worden opgehaald;
  • regel 18: idem, maar beperkt tot de entiteiten waarvan de lijst met identificatiecodes is doorgegeven;

Laten we teruggaan naar de interface [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • met regel 9 kan een [Customer] worden opgezocht op basis van de naam [lastName];

En dat is alles voor de laag [DAO]. Er is geen implementatieklasse voor de vorige interface. Deze wordt tijdens de uitvoering gegenereerd door [Spring Data]. De methoden van de interface [CrudRepository] worden automatisch geïmplementeerd. Voor de methoden die aan de interface [CustomerRepository] zijn toegevoegd, hangt het ervan af. Laten we teruggaan naar de definitie van [Customer]:


    private long id;
    private String firstName;
private String lastName;

De methode op regel 9 wordt automatisch geïmplementeerd door [Spring Data], omdat deze verwijst naar het veld [lastName] (regel 3) van [Customer]. Wanneer Spring Data een methode [findBySomething] tegenkomt in de te implementeren interface, implementeert het deze via de volgende JPQL-query (Java Persistence Query Language):

select t from T t where t.something=:value

Het type T moet dus een veld hebben met de naam [something]. Zo wordt de methode

List<Customer> findByLastName(String lastName);

zal worden geïmplementeerd met code die er ongeveer zo uitziet:

return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()

waarbij [em] verwijst naar de persistentiecontext JPA. Dit is alleen mogelijk als de klasse [Customer] een veld heeft met de naam [lastName], wat het geval is.

Kortom, in eenvoudige gevallen stelt Spring Data ons in staat om de laag [DAO] te implementeren met een eenvoudige interface.

2.2.4. De laag [console]

  

De klasse [Application] ziet er als volgt uit:


package hello;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // sla een paar klanten op
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // haal alle klanten op
        Iterable<Customer> customers = repository.findAll();
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
        System.out.println();

        // haal een individuele klant op via ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // klanten op achternaam ophalen
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }

        context.close();
    }

}
  • regel 10: geeft aan dat de klasse dient om Spring te configureren. Recente versies van Spring kunnen namelijk in Java worden geconfigureerd in plaats van in XML. Beide methoden kunnen tegelijkertijd worden gebruikt. In de code van een klasse met de annotatie [Configuration] vinden we normaal gesproken Spring-beans, d.w.z. definities van klassen die geïnstantieerd moeten worden. Hier is geen enkele bean gedefinieerd. We moeten hier nogmaals benadrukken dat wanneer we met een SGBD werken, diverse Spring-beans gedefinieerd moeten worden:
    • een [EntityManagerFactory] die de te gebruiken implementatie JPA definieert,
    • een [DataSource] die de te gebruiken gegevensbron definieert,
    • een [TransactionManager] die de te gebruiken transactiebeheerder definieert;

Hier is geen van deze beans gedefinieerd.

  • regel 11: de annotatie [EnableAutoConfiguration] is een annotatie afkomstig uit het project [Spring Boot] (regels 5-6). Deze annotatie vraagt Spring Boot via de klasse [SpringApplication] (regel 16) om de applicatie te configureren op basis van de bibliotheken die in het Classpath worden aangetroffen. Omdat de Hibernate-bibliotheken in het Classpath staan, zal de bean [entityManagerFactory] met Hibernate worden geïmplementeerd. Omdat de bibliotheek SGBD in het classpath staat, wordt de bean H2 geïmplementeerd met [dataSource]. In de bean [dataSource] moeten ook de gebruiker en het wachtwoord worden gedefinieerd. Hier zal Spring Boot de standaardbeheerder van H2 gebruiken, die geen wachtwoord heeft. Omdat de bibliotheek [spring-tx] in het classpath staat, wordt de transactiebeheerder van Spring gebruikt.

Bovendien wordt de map waarin de klasse [Application] zich bevindt, gescand op zoek naar beans die impliciet door Spring worden herkend of expliciet zijn gedefinieerd door middel van Spring-annotaties. Zo worden de klassen [Customer] en [CustomerRepository] geïnspecteerd. Omdat de eerste de annotatie [@Entity] heeft, wordt deze gecatalogiseerd als een entiteit die door Hibernate moet worden beheerd. Omdat de tweede de interface [CrudRepository] uitbreidt, wordt deze geregistreerd als een Spring-bean.

Laten we de regels 16-17 van de code eens bekijken:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
  • regel 1: de statische methode [run] van de klasse [SpringApplication] uit het Spring Boot-project wordt uitgevoerd. De parameter hiervan is de klasse die een annotatie [Configuration] of [EnableAutoConfiguration] heeft. Alles wat hierboven is uitgelegd, zal dan plaatsvinden. Het resultaat is een Spring-applicatiecontext, d.w.z. een verzameling beans die door Spring worden beheerd;
  • regel 17: we vragen deze Spring-context om een bean die de interface [CustomerRepository] implementeert. We halen hier de klasse op die door Spring Data is gegenereerd om deze interface te implementeren.

De volgende bewerkingen maken uitsluitend gebruik van de methoden van de bean die de interface [CustomerRepository] implementeert. Let op regel 50: de context wordt afgesloten. De console-uitvoer is als volgt:

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

2014-06-05 16:23:13.877  INFO 11664 --- [           main] hello.Application                        : Toepassing starten op Gportpers3 met PID 11664 (D:\Temp\wksSTS\gs-accessing-data-jpa-complete\target\classes gestart door ST in D:\Temp\wksSTS\gs-accessing-data-jpa-complete)
2014-06-05 16:23:13.936  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext : QZXW2HTMLCb3JnLnNwcmluZ2ZyYW1ld29yay5jb250ZXh0LmFubm90YXRpb24uQW5ub3RhdGlvbkNvbmZpZ0FwcGxpY2F0aW9uQ29udGV4dAZQ wordt vernieuwdX@331a8fa0: startdatum [Thu Jun 05 16:23:13 CEST 2014]; basis van de context hiërarchie
2014-06-05 16:23:15.424  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : De container JPA EntityManagerFactory wordt aangemaakt voor de persistentie-eenheid 'default'
2014-06-05 16:23:15.518  INFO 11664 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Verwerking van PersistenceUnitInfo [
    name: default
    ...]
2014-06-05 16:23:15.690  INFO 11664 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.1.Final}
2014-06-05 16:23:15.692  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties niet gevonden
2014-06-05 16:23:15.694  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Naam bytecode-provider: javassist
2014-06-05 16:23:15.988  INFO 11664 --- [        main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
2014-06-05 16:23:16.078  INFO 11664 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Dialect gebruikt: org.hibernate.dialect.H2Dialect
2014-06-05 16:23:16.300  INFO 11664 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Met behulp van ASTQueryTranslatorFactory
2014-06-05 16:23:16.613  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Hbm2ddl-schema-export wordt uitgevoerd
Hibernate: drop table customer if exists
Hibernate: create table customer (id bigint generated by default as identity, first_name varchar(255), last_name varchar(255), primary key (id))
2014-06-05 16:23:16.619  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Exportschema voltooid
2014-06-05 16:23:17.074  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Beans registreren voor JMX-blootstelling bij het opstarten
2014-06-05 16:23:17.094  INFO 11664 --- [           main] hello.Application                        : Toepassing gestart in 3,906 seconden (JVM draait al 5,013)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as last_nam3_0_ from customer customer0_
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Hibernate: select customer0_.id as id1_0_0_, customer0_.first_name as first_na2_0_0_, customer0_.last_name as last_nam3_0_0_ from customer customer0_ where customer0_.id=?
Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as last_nam3_0_ from customer customer0_ where customer0_.last_name=?
Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2014-06-05 16:23:17.330  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: startdatum [Thu Jun 05 16:23:13 CEST 2014]; basis van de context hiërarchie
2014-06-05 16:23:17.332  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : JMX-blootgestelde beans bij het afsluiten deregistreren
2014-06-05 16:23:17.333  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : JPA EntityManagerFactory wordt gesloten voor persistentie-eenheid 'default'
2014-06-05 16:23:17.334  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Hbm2ddl-schema-export wordt uitgevoerd
Hibernate: drop table customer if exists
2014-06-05 16:23:17.336  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Exportschema voltooid
  • regels 1-8: het logo van het Spring Boot-project;
  • regel 9: de klasse [hello.Application] wordt uitgevoerd;
  • regel 10: [AnnotationConfigApplicationContext] is een klasse die de Spring-interface [ApplicationContext] implementeert. Het is een bean-container;
  • regel 11: de bean [entityManagerFactory] wordt geïmplementeerd met de klasse [LocalContainerEntityManagerFactory], een Spring-klasse;
  • regel 12: [hibernate] verschijnt. Er is gekozen voor deze implementatie, JPA;
  • regel 19: een Hibernate-dialect is de variant SQL, die samen met SGBD moet worden gebruikt. Hier geeft het dialect [H2Dialect] aan dat Hibernate gaat werken met SGBD en H2;
  • regels 22-24: de tabel [CUSTOMER] wordt aangemaakt. Dit betekent dat Hibernate is geconfigureerd om tabellen te genereren op basis van de definities JPA, in dit geval de definitie JPA van de klasse [Customer];
  • regels 27-32: Hibernate-logbestanden die de invoegingen van rijen in de tabel [CUSTOMER] weergeven. Dit betekent dat Hibernate is geconfigureerd om logbestanden te genereren;
  • regels 35-39: de vijf ingevoerde klanten;
  • regels 42-44: resultaat van de methode [findOne] van de interface;
  • regels 47-50: resultaten van de methode [findByLastName];
  • regels 51 en volgende: logberichten over het afsluiten van de Spring-context.

2.2.5. Handmatige configuratie van het Spring Data-project

We dupliceren het vorige project in het project [gs-accessing-data-jpa-2]:

  

In dit nieuwe project gaan we niet vertrouwen op de automatische configuratie door Spring Boot. We gaan dit handmatig doen. Dit kan handig zijn als de standaardconfiguraties niet aan onze eisen voldoen.

Allereerst gaan we de benodigde afhankelijkheden expliciet opgeven in het bestand [pom.xml]:


<dependencies>
        <!-- Spring Core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <!-- Spring-transacties -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.5.2.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.0.2.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.178</version>
        </dependency>
        <!-- Commons DBCP -->
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>
    </dependencies>
  • regels 3-17: de basisbibliotheken van Spring;
  • regels 19-28: de Spring-bibliotheken voor het beheren van transacties met een database;
  • regels 30-34: Spring Data, gebruikt om toegang te krijgen tot de database;
  • regels 36-40: Spring Boot om de applicatie te starten;
  • regels 48-52: de SGBD H2;
  • regels 54-63: databases worden vaak gebruikt met pools van open verbindingen, waardoor herhaaldelijk openen en sluiten van verbindingen wordt voorkomen. Hier wordt de implementatie van [commons-dbcp] gebruikt;

Nog steeds in [pom.xml] wordt de naam van de uitvoerbare klasse gewijzigd:


    <properties>
...
        <start-class>demo.console.Main</start-class>
</properties>

In het nieuwe project blijven de entiteit [Customer] en de interface [CustomerRepository] ongewijzigd. We gaan de klasse [Application] wijzigen, die in twee klassen wordt opgesplitst:

  • [Config], de configuratieklasse:
  • [Main], de uitvoerbare klasse;
  

De uitvoerbare klasse [Main] is dezelfde als voorheen, maar dan zonder de configuratie-annotaties:


package demo.console;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;

public class Main {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
...

        context.close();
    }

}
  • regel 12: de klasse [Main] heeft geen configuratie-annotaties meer;
  • regel 16: de applicatie wordt gestart met Spring Boot. De parameter [Config.class] is de nieuwe configuratieklasse van het project;

De klasse [Config] die het project configureert, is als volgt:


package demo.config;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
    // de gegevensbron H2
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    // de provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("demo.entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Transactiemanager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • regel 22: de annotatie [@Configuration] maakt van de klasse [Config] een Spring-configuratieklasse;
  • regel 21: de annotatie [@EnableJpaRepositories] maakt het mogelijk om de mappen aan te wijzen waarin de Spring Data-interfaces [CrudRepository] zich bevinden. Deze interfaces worden Spring-componenten en zijn beschikbaar in de Spring-context;
  • regel 20: de annotatie [@EnableTransactionManagement] geeft aan dat de methoden van de interfaces [CrudRepository] binnen een transactie moeten plaatsvinden;
  • regel 19: met de annotatie [@EntityScan] kunnen de mappen worden opgegeven waarin naar de entiteiten JPA moet worden gezocht. Hier is deze annotatie uitgecommentarieerd, omdat deze informatie al expliciet in regel 50 is vermeld. Deze annotatie zou aanwezig moeten zijn als de modus [@EnableAutoConfiguration] wordt gebruikt en de entiteiten JPA zich niet in dezelfde map bevinden als de configuratieklasse;
  • regel 18: met de annotatie [@ComponentScan] kunnen de mappen worden opgegeven waarin naar Spring-componenten moet worden gezocht. Spring-componenten zijn klassen die zijn gemarkeerd met Spring-annotaties zoals @Service, @Component, @Controller, ... Hier zijn er geen andere dan die welke zijn gedefinieerd binnen de klasse [Config], dus is de annotatie uitgecommentarieerd;
  • regels 25-33: definiëren de gegevensbron, de database H2. Het is de annotatie @Bean op regel 25 die ervoor zorgt dat het door deze methode aangemaakte object een door Spring beheerde component wordt. De naam van de methode kan hier willekeurig zijn. Deze moet echter [dataSource] heten als EntityManagerFactory uit regel 47 ontbreekt en via autoconfiguratie wordt gedefinieerd;
  • regel 29: de database krijgt de naam [demo] en wordt gegenereerd in de projectmap;
  • regels 36-43: definiëren de gebruikte implementatie JPA, in dit geval een Hibernate-implementatie. De naam van de methode kan hier willekeurig zijn;
  • regel 39: geen logbestanden voor SQL;
  • regel 30: de database wordt aangemaakt als deze nog niet bestaat;
  • regels 46-54: definiëren de EntityManagerFactory die de persistentie JPA zal beheren. De methode moet verplicht [entityManagerFactory] heten;
  • regel 47: de methode ontvangt twee parameters van het type van de twee eerder gedefinieerde beans. Deze worden vervolgens geconstrueerd en door Spring als parameters van de methode geïnjecteerd;
  • regel 49: stelt de gebruikte implementatie JPA vast;
  • regel 50: hier worden de mappen opgegeven waarin de entiteiten JPA te vinden zijn;
  • regel 51: stelt de te beheren gegevensbron vast;
  • regels 57-62: de transactiebeheerder. De methode moet verplicht [transactionManager] heten. Deze ontvangt als parameter de bean uit de regels 46-54;
  • regel 60: de transactiebeheerder wordt gekoppeld aan EntityManagerFactory;

De voorgaande methoden kunnen in willekeurige volgorde worden gedefinieerd.

Het uitvoeren van het project levert dezelfde resultaten op. Er verschijnt een nieuw bestand in de projectmap, namelijk de databasebestand H2:

  

Ten slotte kunnen we Spring Boot achterwege laten. We maken een tweede uitvoerbare klasse aan, [Main2]:

  

De klasse [Main2] bevat de volgende code:


package demo.console;

import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;

public class Main2 {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
....

        context.close();
    }

}
  • regel 15: de configuratieklasse [Config] wordt nu gebruikt door de Spring-klasse [AnnotationConfigApplicationContext]. Op regel 5 is te zien dat er nu geen afhankelijkheid meer is van Spring Boot.

De uitvoering levert dezelfde resultaten op als voorheen.

2.2.6. Een uitvoerbaar archief maken

Om een uitvoerbaar archief van het project te maken, kun je als volgt te werk gaan:

  • in [1]: er wordt een uitvoeringsconfiguratie aangemaakt;
  • in [2]: van het type [Java Application]
  • in [3]: geeft het uit te voeren project aan (gebruik de knop Browse);
  • in [4]: geeft de uit te voeren klasse aan;
  • in [5]: de naam van de uitvoeringsconfiguratie – kan willekeurig zijn;
  • in [6]: het project wordt geëxporteerd;
  • in [7]: in de vorm van een uitvoerbaar JAR-archief;
  • in [8]: geeft het pad en de naam aan van het uit te voeren bestand dat moet worden aangemaakt;
  • in [9]: de naam van de uitvoerconfiguratie die in [5] is aangemaakt;

Zodra dit is gebeurd, openen we een console in de map die het uitvoerbare archief bevat:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-2.jar

Het archief wordt als volgt uitgevoerd:


.....\dist>java -jar gs-accessing-data-jpa-2.jar

De resultaten die in de console worden weergegeven, zijn als volgt:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder voor meer informatie.
juin 12, 2014 9:48:38 AM org.hibernate.ejb.HibernatePersistence logDeprecation
WARN: HHH015016: Encountered a deprecated javax.persistence.spi.PersistenceProvider [org.hibernate.ejb.HibernatePersistence]; use [org.hibernate.jpa.HibernatePersistenceProvider] instead.
juin 12, 2014 9:48:38 AM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
        name: default
        ...]
juin 12, 2014 9:48:38 AM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.4.Final}
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 12, 2014 9:48:39 AM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
juin 12, 2014 9:48:39 AM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
juin 12, 2014 9:48:39 AM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

2.2.7. Een nieuw Spring Data-project aanmaken

Om een Spring Data-projectsjabloon aan te maken, kun je als volgt te werk gaan:

  • in [1] maakt u een nieuw project aan;
  • in [2]: van het type [Spring Starter Project];
  • het gegenereerde project is een Maven-project. In [3] geef je de naam van de projectgroep op;
  • in [4]: geef je de naam op van het artefact (hier een jar) dat bij het bouwen van het project wordt aangemaakt;
  • in [5]: hier wordt het pakket van de uitvoerbare klasse opgegeven die in het project zal worden aangemaakt;
  • in [6]: de Eclipse-naam van het project – deze kan willekeurig zijn (hoeft niet identiek te zijn aan [4]);
  • in [7]: hier wordt aangegeven dat er een project met een laag [JPA] wordt aangemaakt. De benodigde afhankelijkheden voor een dergelijk project worden vervolgens opgenomen in het bestand [pom.xml];
  • in [8]: het aangemaakte project;

Het bestand [pom.xml] bevat de benodigde afhankelijkheden voor een project JPA:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
        <relativePath/> <!-- zoek de bovenliggende entiteit op in de repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • regels 9-12: de benodigde afhankelijkheden voor JPA – zullen [Spring Data] bevatten;
  • regels 13-17: de benodigde afhankelijkheden voor de JUnit-tests die met Spring zijn geïntegreerd;

De uitvoerbare klasse [Application] doet niets, maar is vooraf geconfigureerd:


package istia.st;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

De testklasse [ApplicationTests] doet niets, maar is vooraf geconfigureerd:


package istia.st;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
  • regel 9: de annotatie [@SpringApplicationConfiguration] maakt het mogelijk om het configuratiebestand [Application] te gebruiken. De testklasse zal zo profiteren van alle beans die in dit bestand worden gedefinieerd;
  • regel 8: de annotatie [@RunWith] maakt de integratie van Spring met JUnit mogelijk: de klasse kan worden uitgevoerd als een JUnit-test. [@RunWith] is een annotatie JUnit (regel 4), terwijl de klasse [SpringJUnit4ClassRunner] een Spring-klasse is (regel 6);

Nu we een applicatieskelet JPA hebben, kunnen we dit aanvullen om de persistentielag van de server voor onze afspraakbeheerapplicatie te implementeren.

2.3. Het Eclipse-project van de server

  

De belangrijkste onderdelen van het project zijn:

  • [pom.xml]: het Maven-configuratiebestand van het project;
  • [rdvmedecins.entities]: de entiteiten JPA;
  • [rdvmedecins.repositories]: de Spring Data-interfaces voor toegang tot de entiteiten JPA;
  • [rdvmedecins.metier]: de laag [métier];
  • [rdvmedecins.domain]: de entiteiten die door de laag worden beheerd [métier];
  • [rdvmdecins.config]: de configuratieklassen van de persistentielaag;
  • [rdvmedecins.boot]: een eenvoudige console-applicatie;

2.4. De Maven-configuratie

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


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring4.rdvmedecins</groupId>
    <artifactId>rdvmedecins-metier-dao</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
    </dependencies>
    <properties>
        <!-- gebruik UTF-8 voor alles -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>istia.st.spring.data.main.Application</start-class>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>
  • regels 8-12: het project is gebaseerd op het bovenliggende project [spring-boot-starter-parent]. Voor afhankelijkheden die al in het bovenliggende project aanwezig zijn, wordt geen versie opgegeven. De versie die in het bovenliggende project is gedefinieerd, wordt gebruikt. Andere afhankelijkheden worden op de gebruikelijke manier gedeclareerd;
  • regels 14-17: voor Spring Data;
  • regels 18-22: voor de tests JUnit;
  • regels 23-26: stuurprogramma JDBC van SGBD MySQL5;
  • regels 27-34: Commons-verbindingspool DBCP;
  • regels 35-38: Jackson-bibliotheek voor het beheer van JSON;
  • regels 39-43: Google-bibliotheek voor het beheer van collecties;

Versie 1.1.0.RC1 van [spring-boot-starter-parent] maakt gebruik van de volgende versies van de bibliotheken:

<activemq.version>5.9.1</activemq.version>
    <aspectj.version>1.8.0</aspectj.version>
    <codahale-metrics.version>3.0.2</codahale-metrics.version>
    <commons-beanutils.version>1.9.1</commons-beanutils.version>
    <commons-collections.version>3.2.1</commons-collections.version>
    <commons-dbcp.version>1.4</commons-dbcp.version>
    <commons-digester.version>2.1</commons-digester.version>
    <commons-pool.version>1.6</commons-pool.version>
    <commons-pool2.version>2.2</commons-pool2.version>
    <crashub.version>1.3.0-beta20</crashub.version>
    <flyway.version>3.0</flyway.version>
    <freemarker.version>2.3.20</freemarker.version>
    <gemfire.version>7.0.2</gemfire.version>
    <gradle.version>1.6</gradle.version>
    <groovy.version>2.3.2</groovy.version>
    <h2.version>1.3.175</h2.version>
    <hamcrest.version>1.3</hamcrest.version>
    <hibernate-entitymanager.version>4.3.1.Final</hibernate-entitymanager.version>
    <hibernate-jpa-api.version>1.0.1.Final</hibernate-jpa-api.version>
    <hibernate-validator.version>5.0.3.Final</hibernate-validator.version>
    <hibernate.version>4.3.1.Final</hibernate.version>
    <hikaricp.version>1.3.8</hikaricp.version>
    <hornetq.version>2.4.1.Final</hornetq.version>
    <hsqldb.version>2.3.2</hsqldb.version>
    <httpasyncclient.version>4.0.1</httpasyncclient.version>
    <httpclient.version>4.3.3</httpclient.version>
    <jackson.version>2.3.3</jackson.version>
    <java.version>1.6</java.version>
    <javassist.version>3.18.1-GA</javassist.version>
    <jedis.version>2.4.1</jedis.version>
    <jetty-jsp.version>2.2.0.v201112011158</jetty-jsp.version>
    <jetty.version>8.1.14.v20131031</jetty.version>
    <joda-time.version>2.3</joda-time.version>
    <jolokia.version>1.2.0</jolokia.version>
    <jstl.version>1.2</jstl.version>
    <junit.version>4.11</junit.version>
    <liquibase.version>3.0.8</liquibase.version>
    <log4j.version>1.2.17</log4j.version>
    <logback.version>1.1.2</logback.version>
    <mockito.version>1.9.5</mockito.version>
    <mongodb.version>2.12.1</mongodb.version>
    <mysql.version>5.1.30</mysql.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <reactor.version>1.1.1.RELEASE</reactor.version>
    <servlet-api.version>3.0.1</servlet-api.version>
    <slf4j.version>1.7.7</slf4j.version>
    <snakeyaml.version>1.13</snakeyaml.version>
    <solr.version>4.7.2</solr.version>
    <spock.version>0.7-groovy-2.0</spock.version>
    <spring-amqp.version>1.3.4.RELEASE</spring-amqp.version>
    <spring-batch.version>3.0.0.RELEASE</spring-batch.version>
    <spring-boot.version>1.1.0.RC1</spring-boot.version>
    <spring-data-releasetrain.version>Dijkstra-RELEASE</spring-data-releasetrain.version>
    <spring-hateoas.version>0.12.0.RELEASE</spring-hateoas.version>
    <spring-integration.version>4.0.2.RELEASE</spring-integration.version>
    <spring-loaded.version>1.2.0.RELEASE</spring-loaded.version>
    <spring-mobile.version>1.1.1.RELEASE</spring-mobile.version>
    <spring-security-jwt.version>1.0.2.RELEASE</spring-security-jwt.version>
    <spring-security.version>3.2.4.RELEASE</spring-security.version>
    <spring-social-facebook.version>1.1.1.RELEASE</spring-social-facebook.version>
    <spring-social-linkedin.version>1.0.1.RELEASE</spring-social-linkedin.version>
    <spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version>
    <spring-social.version>1.1.0.RELEASE</spring-social.version>
    <spring.version>4.0.5.RELEASE</spring.version>
    <thymeleaf-extras-springsecurity3.version>2.1.1.RELEASE</thymeleaf-extras-springsecurity3.version>
    <thymeleaf-layout-dialect.version>1.2.4</thymeleaf-layout-dialect.version>
    <thymeleaf.version>2.1.3.RELEASE</thymeleaf.version>
    <tomcat.version>7.0.54</tomcat.version>
    <velocity-tools.version>2.0</velocity-tools.version>
    <velocity.version>1.7</velocity.version>

2.5. De entiteiten JPA

De entiteiten JPA zijn de objecten waarin de rijen van de databasetabellen worden ingekapseld.

  

De klasse [AbstractEntity] is de bovenliggende klasse van de entiteiten [Personne, Creneau, Rv]. De definitie ervan is als volgt:


package rdvmedecins.entities;

import java.io.Serializable;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

@MappedSuperclass
public class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected Long id;
    @Version
    protected Long version;

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    // initialisatie
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

    @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id == other.id;
    }

    // getters en setters
    ..
}
  • regel 11: de annotatie [@MappedSuperclass] geeft aan dat de geannoteerde klasse de bovenliggende klasse is van de entiteiten JPA en [@Entity];
  • regels 15-17: definiëren de primaire sleutel [id] van elke entiteit. Het is de annotatie [@Id] die het veld [id] tot primaire sleutel maakt. De annotatie [@GeneratedValue(strategy = GenerationType.AUTO)] geeft aan dat de waarde van deze primaire sleutel wordt gegenereerd door SGBD en dat er geen generatiemodus wordt opgelegd;
  • regels 18-19: definiëren de versie van elke entiteit. De implementatie JPA zal dit versienummer verhogen telkens wanneer de entiteit wordt gewijzigd. Dit nummer dient om te voorkomen dat de entiteit gelijktijdig door twee verschillende gebruikers wordt bijgewerkt: twee gebruikers, U1 en U2, lezen de entiteit E met een versienummer gelijk aan V1. U1 wijzigt E en slaat deze wijziging op in de database: het versienummer verandert dan in V1+1. U2 wijzigt op zijn beurt E en slaat deze wijziging op in de database: er wordt een uitzondering gegenereerd omdat het een andere versie (V1) heeft dan die in de database (V1+1);
  • regels 29-33: met de methode [build] kunnen de twee velden van [AbstractEntity] worden geïnitialiseerd. Deze methode retourneert de referentie van de aldus geïnitialiseerde instantie [AbstractEntity];
  • regels 36-44: de methode [equals] van de klasse wordt opnieuw gedefinieerd: twee entiteiten worden als gelijk beschouwd als ze dezelfde klassenaam en dezelfde id-identificatie hebben;

De entiteit [Personne] is de bovenliggende klasse van de entiteiten [Medecin] en [Client]:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public class Personne extends AbstractEntity {
    private static final long serialVersionUID = 1L;
    // attributen van een persoon
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;

    // standaardconstructor
    public Personne() {
    }

    // constructor met parameters
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }

    // getters en setters
    ...
}
  • regel 6: de annotatie [@MappedSuperclass] geeft aan dat de geannoteerde klasse de bovenliggende klasse is van de entiteiten JPA en [@Entity];
  • regels 10-15: een persoon heeft een aanspreektitel (Melle), een voornaam (Jacqueline) en een achternaam (Tatou). Er wordt geen informatie gegeven over de kolommen van de tabel. Deze zullen daarom standaard dezelfde namen dragen als de velden;

De entiteit [Medecin] is als volgt:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "medecins")
public class Medecin extends Personne {

    private static final long serialVersionUID = 1L;

    // standaardconstructor
    public Medecin() {
    }

    // constructor met parameters
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }

}
  • regel 6: de klasse is een entiteit JPA;
  • regel 7: gekoppeld aan de tabel [MEDECINS] in de database;
  • regel 8: de entiteit [Medecin] is afgeleid van de entiteit [Personne];

Een arts kan als volgt worden aangemaakt:

Medecin m=new Medecin("Mr","Paul","Tatou");

Als men bovendien een identificatiecode en een versie wil toewijzen, kan men het volgende schrijven:

Medecin m=new Medecin("Mr","Paul","Tatou").build(10,1);

waarbij de methode [build] dezelfde is als die gedefinieerd in [AbstractEntity].

De entiteit [Client] is als volgt:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "clients")
public class Client extends Personne {

    private static final long serialVersionUID = 1L;

    // standaardconstructor
    public Client() {
    }

    // constructor met parameters
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    // identiteit
    public String toString() {
        return String.format("Client[%s]", super.toString());
    }

}
  • regel 6: de klasse is een entiteit JPA;
  • regel 7: gekoppeld aan de tabel [CLIENTS] in de database;
  • regel 8: de entiteit [Client] is afgeleid van de entiteit [Personne];

De entiteit [Creneau] is als volgt:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // kenmerken van een tijdvak van RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // een tijdvak is gekoppeld aan een arts
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // vreemde sleutel
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;

    // standaardconstructeur
    public Creneau() {
    }

    // fabrikant met parameters
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }

    // toString
    public String toString() {
        return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
    }

    // vreemde sleutel
    public long getIdMedecin() {
        return idMedecin;
    }

    // setters - getters
    ...
}
  • regel 10: de klasse is een entiteit JPA;
  • regel 11: gekoppeld aan de tabel [CRENEAUX] in de database;
  • regel 12: de entiteit [Creneau] is afgeleid van de entiteit [AbstractEntity] en erft dus de identificatiecode [id] en de versie [version];
  • regel 16: starttijd van het tijdslot (14);
  • regel 17: startminuten van het tijdvak (20);
  • regel 18: eindtijd van het tijdvak (14);
  • regel 19: eindminuten van het tijdslot (40);
  • regels 22-24: de arts die eigenaar is van het tijdvak. De tabel [CRENEAUX] heeft een vreemde sleutel naar de tabel [MEDECINS]. Deze relatie wordt weergegeven door de regels 22-24;
  • regel 22: de annotatie [@ManyToOne] geeft aan dat er een relatie is van meerdere (tijdvakken) naar één (arts). Het attribuut [fetch=FetchType.LAZY] geeft aan dat wanneer een entiteit [Creneau] wordt opgevraagd bij de persistentiecontext en deze in de database moet worden opgezocht, de entiteit [Medecin] niet mee wordt teruggegeven. Het voordeel van deze modus is dat de entiteit [Medecin] alleen wordt opgezocht als de ontwikkelaar daarom vraagt. Zo wordt geheugen bespaard en worden de prestaties verbeterd;
  • regel 23: geeft de naam aan van de vreemde-sleutelkolom in de tabel [CRENEAUX];
  • regels 27-28: de vreemde sleutel in de tabel [MEDECINS];
  • regel 27: de kolom [ID_MEDECIN] is al gebruikt in regel 23. Dit betekent dat deze op twee verschillende manieren kan worden gewijzigd, wat niet is toegestaan volgens de norm JPA. We voegen daarom de attributen [insertable = false, updatable = false] toe, waardoor de kolom alleen nog maar kan worden gelezen;

De entiteit [Rv] is als volgt:


package rdvmedecins.entities;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // kenmerken van een Rv
    @Temporal(TemporalType.DATE)
    private Date jour;

    // een rv is gekoppeld aan een klant
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // een RV is gekoppeld aan een tijdslot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // vreemde sleutels
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

    // standaardfabrikant
    public Rv() {
    }

    // met parameters
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }

    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }

    // vreemde sleutels
    public long getIdCreneau() {
        return idCreneau;
    }

    public long getIdClient() {
        return idClient;
    }

    // getters en setters
...
}
  • regel 14: de klasse is een entiteit JPA;
  • regel 15: gekoppeld aan de tabel [RV] in de database;
  • regel 16: de entiteit [Rv] is afgeleid van de entiteit [AbstractEntity] en erft dus de identificatiecode [id] en de versie [version];
  • regel 21: de datum van de afspraak;
  • regel 20: het Java-type [Date] bevat zowel een datum als een tijdstip. Hier wordt aangegeven dat alleen de datum wordt gebruikt;
  • regels 24-26: de klant voor wie deze afspraak is gemaakt. De tabel [RV] heeft een vreemde sleutel naar de tabel [CLIENTS]. Deze relatie wordt weergegeven door de regels 24-26;
  • regels 29-31: het tijdvak van de afspraak. De tabel [RV] heeft een vreemde sleutel naar de tabel [CRENEAUX]. Deze relatie wordt weergegeven door de regels 29-31;
  • regels 34-35: de vreemde sleutel [idClient];
  • regels 36-37: de vreemde sleutel [idCreneau];

2.6. De laag [DAO]

We gaan de laag [DAO] implementeren met Spring Data:

  

De laag [DAO] is geïmplementeerd met vier Spring Data-interfaces:

  • [ClientRepository]: biedt toegang tot de entiteiten JPA en [Client];
  • [CreneauRepository]: geeft toegang tot de entiteiten JPA en [Creneau];
  • [MedecinRepository]: geeft toegang tot de entiteiten JPA en [Medecin];
  • [RvRepository]: geeft toegang tot de entiteiten JPA en [Rv];

De interface [MedecinRepository] is als volgt:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Medecin;

public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • regel 7: de interface [MedecinRepository] neemt alleen de methoden van de interface [CrudRepository] over, zonder er andere aan toe te voegen;

De interface [ClientRepository] is als volgt:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Client;

public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • regel 7: de interface [ClientRepository] neemt alleen de methoden van de interface [CrudRepository] over, zonder er andere aan toe te voegen;

De interface [CreneauRepository] is als volgt:


package rdvmedecins.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Creneau;

public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // lijst met spreekuren van een arts
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • regel 8: de interface [CreneauRepository] erft de methoden van de interface [CrudRepository];
  • regels 10-11: met de methode [getAllCreneaux] kunnen de beschikbare tijdvakken van een arts worden opgevraagd;
  • regel 11: de parameter is de identificatiecode van de arts. Het resultaat is een lijst met beschikbare tijdvakken in de vorm van een object [Iterable<Creneau>];
  • regel 10: met de annotatie [@Query] kan de query JPQL (Java Persistence Query Language) worden gespecificeerd die de methode implementeert. De parameter [?1] wordt vervangen door de parameter [idMedecin] van de methode;

De interface [RvRepository] is als volgt:


package rdvmedecins.repositories;

import java.util.Date;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Rv;

public interface RvRepository extends CrudRepository<Rv, Long> {

    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
  • regel 10: de interface [RvRepository] erft de methoden van de interface [CrudRepository];
  • regels 12-13: met de methode [getRvMedecinJour] kunnen de afspraken van een arts voor een bepaalde dag worden opgehaald;
  • regel 13: de parameters zijn de ID van de arts en de dag. Het resultaat is een lijst met afspraken in de vorm van een [Iterable<Rv>]-object;
  • regel 12: met de annotatie [@Query] kan de query JPQL worden gespecificeerd die de methode implementeert. De parameter [?1] wordt vervangen door de parameter [idMedecin] van de methode en de parameter [?2] wordt vervangen door de parameter [jour] van de methode. De volgende query JPQL volstaat niet:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

omdat de velden van de klasse Rv, van het type [Client] en [Creneau], worden verkregen in de modus [FetchType.LAZY], wat betekent dat ze expliciet moeten worden opgevraagd om te worden verkregen. Dit gebeurt in de query JPQL met de syntaxis [left join fetch entité], die vraagt om een join uit te voeren met de tabel waarnaar de vreemde sleutel verwijst, om de entiteit waarnaar wordt verwezen op te halen;

2.7. De laag [métier]

  
  • [IMetier] is de interface van de laag [métier] en [Metier] is de implementatie ervan;
  • [AgendaMedecinJour] en [CreneauMedecinJour] zijn twee bedrijfsentiteiten;

2.7.1. De entiteiten

De entiteit [CreneauMedecinJour] koppelt een tijdslot aan de eventuele afspraak die binnen dat tijdslot is gemaakt:


package rdvmedecins.domain;

import java.io.Serializable;

import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // velden
    private Creneau creneau;
    private Rv rv;

    // constructors
    public CreneauMedecinJour() {

    }

    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
        this.rv=rv;
    }

    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau, rv);
    }

    // getters en setters
...
}
  • regel 12: het tijdvak;
  • regel 13: de eventuele afspraak – anders null;

De entiteit [AgendaMedecinJour] is de agenda van een arts voor een bepaalde dag, d.w.z. de lijst met zijn afspraken:


package rdvmedecins.domain;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;

import rdvmedecins.entities.Medecin;

public class AgendaMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // velden
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // constructors
    public AgendaMedecinJour() {

    }

    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }

    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }

    // getters en setters
...
}
  • regel 13: de arts;
  • regel 14: de dag in de agenda;
  • regel 15: zijn tijdvakken met of zonder afspraak;

2.7.2. De dienst

De interface van de laag [métier] is als volgt:


package rdvmedecins.metier;

import java.util.Date;
import java.util.List;

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;

public interface IMetier {

    // klantenlijst
    public List<Client> getAllClients();

    // lijst met artsen
    public List<Medecin> getAllMedecins();

    // lijst met tijdvakken van een arts
    public List<Creneau> getAllCreneaux(long idMedecin);

    // lijst met afspraken van een arts op een bepaalde dag
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);

    // een klant zoeken op basis van zijn ID
    public Client getClientById(long id);

    // een klant zoeken op basis van zijn ID
    public Medecin getMedecinById(long id);

    // een afspraak zoeken op basis van de id
    public Rv getRvById(long id);

    // een tijdslot zoeken op basis van de id
    public Creneau getCreneauById(long id);

    // een RV toevoegen
    public Rv ajouterRv(Date jour, Creneau créneau, Client client);

    // een RV verwijderen
    public void supprimerRv(Rv rv);

    // beroep
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);

}

De opmerkingen leggen de functie van elke methode uit.

De implementatie van de interface [IMetier] is de volgende klasse [Metier]:


package rdvmedecins.metier;

import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;

import com.google.common.collect.Lists;

@Service("métier")
public class Metier implements IMetier {

    // repositories
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;

    // implementatie interface
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }

    @Override
    public List<Medecin> getAllMedecins() {
        return Lists.newArrayList(medecinRepository.findAll());
    }

    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
    }

    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
    }

    @Override
    public Client getClientById(long id) {
        return clientRepository.findOne(id);
    }

    @Override
    public Medecin getMedecinById(long id) {
        return medecinRepository.findOne(id);
    }

    @Override
    public Rv getRvById(long id) {
        return rvRepository.findOne(id);
    }

    @Override
    public Creneau getCreneauById(long id) {
        return creneauRepository.findOne(id);
    }

    @Override
    public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
        return rvRepository.save(new Rv(jour, client, créneau));
    }

    @Override
    public void supprimerRv(Rv rv) {
        rvRepository.delete(rv.getId());
    }

    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
    ...
    }

}
  • regel 24: de annotatie [@Service] is een Spring-annotatie die de geannoteerde klasse tot een door Spring beheerde component maakt. Een component kan al dan niet een naam krijgen. Deze heeft de naam [métier];
  • regel 25: de klasse [Metier] implementeert de interface [IMetier];
  • regel 28: de annotatie [@Autowired] is een Spring-annotatie. De waarde van het veld waarop deze annotatie is aangebracht, wordt door Spring geïnitialiseerd (geïnjecteerd) met de referentie van een Spring-component van het opgegeven type of met de opgegeven naam. Hier specificeert de annotatie [@Autowired] geen naam. Er zal dus een injectie op basis van het type plaatsvinden;
  • regel 29: het veld [medecinRepository] wordt geïnitialiseerd met de referentie van een Spring-component van het type [MedecinRepository]. Dit is de referentie van de klasse die door Spring Data wordt gegenereerd om de interface [MedecinRepository] te implementeren, die we al eerder hebben besproken;
  • regels 30-35: dit proces wordt herhaald voor de drie andere besproken interfaces;
  • regels 39-41: implementatie van de methode [getAllClients];
  • regel 40: we gebruiken de methode [findAll] van de interface [ClientRepository]. Deze methode retourneert een type [Iterable<Client>] dat we met de statische methode [Lists.newArrayList] omzetten naar [List<Client>]. De klasse [Lists] is gedefinieerd in de Google Guava-bibliotheek. In [pom.xml] is deze afhankelijkheid geïmporteerd:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
  • regels 38-86: de methoden van de interface [IMetier] worden geïmplementeerd met behulp van de klassen van de laag [DAO];

Alleen de methode op regel 88 is specifiek voor de laag [métier]. Deze is hier geplaatst omdat ze een bedrijfsspecifieke verwerking uitvoert die meer is dan alleen toegang tot de gegevens. Zonder deze methode was er geen reden om een laag [métier] aan te maken. De methode [getAgendaMedecinJour] is als volgt:


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // lijst met beschikbare tijdvakken van de arts
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // lijst met afspraken van dezelfde arts voor dezelfde dag
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // er wordt een woordenboek aangemaakt op basis van de gemaakte afspraken
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // de agenda voor de gevraagde dag wordt aangemaakt
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // de arts
        agenda.setMedecin(getMedecinById(idMedecin));
        // de dag
        agenda.setJour(jour);
        // de reserveringsslots
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // invullen van de reserveringsslots
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // regel i agenda
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // tijdvak
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // is het tijdvak vrij of gereserveerd?
            if (hReservations.containsKey(idCreneau)) {
                // het tijdvak is bezet – de reservering wordt genoteerd
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // het resultaat wordt weergegeven
        return agenda;
    }

De lezer wordt verzocht de commentaren te lezen. Het algoritme is als volgt:

  • alle tijdvakken van de opgegeven arts worden opgehaald;
  • we halen al zijn afspraken voor de aangegeven dag op;
  • met deze twee gegevens kan worden vastgesteld of een tijdslot vrij of bezet is;

2.8. De configuratie van het project

  

De klasse [DomainAndPersitenceConfig] configureert het gehele project:


package rdvmedecins.config;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {

    // de gegevensbron MySQL
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        return dataSource;
    }

    // de provider JPA – is niet nodig als men tevreden is met de standaardwaarden die door Spring Boot worden gebruikt
    // hier definiëren we deze om de logs in of uit te schakelen SQL
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    // de EntityManagerFactory en de TransactionManager worden door Spring Boot met standaardwaarden gedefinieerd

}
  • regel 45: we gaan de beans [EntityManagerFactory] en [TransactionManager] niet definiëren. Hiervoor maken we gebruik van de annotatie [@EnableAutoConfiguration] van Spring Boot (regel 17);
  • regels 24-32: hierin wordt de gegevensbron MySQL5 gedefinieerd. Dit is een bean die doorgaans niet door Spring Boot kan worden afgeleid;
  • regels 36-43: we configureren ook de implementatie JPA om het Hibernate-attribuut [showSql] op ‘false’ te zetten (regel 39). Standaard staat dit op ‘true’;
  • op dit moment zijn de enige componenten die door Spring worden beheerd de beans op de regels 25 en 37, plus de beans [EntityManagerFactory] en [TransactionManager] via automatische configuratie. We moeten de beans van de lagen [métier] en [DAO] toevoegen;
  • regel 16 voegt de interfaces van het pakket [rdvmdecins.repositories], die erven van de interface [CrudRepository], toe aan de Spring-context;
  • regel 18 voegt aan de Spring-context alle klassen van het pakket [rdvmedecins] en de daarvan afgeleide klassen met een Spring-annotatie toe. In het pakket [rdvmdecins.metier] wordt de klasse [Metier] met de bijbehorende annotatie [@Service] gevonden en toegevoegd aan de Spring-context;
  • regel 45: Spring Boot definieert standaard een bean [entityManagerFactory]. Aan deze bean moet worden aangegeven waar de entiteiten JPA zich bevinden die hij moet beheren. Dit gebeurt op regel 19;
  • regel 20: geeft aan dat de methoden van de interfaces die van de interface [CrudRepository] erven, binnen een transactie moeten worden uitgevoerd;

2.9. De tests van de laag [métier]

  

De klasse [rdvmedecins.tests.Metier] is een Spring-testklasse / JUnit 4:


package rdvmedecins.tests;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;

@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {

    @Autowired
    private IMetier métier;

    @Test
    public void test1(){
        // klantenweergave
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // weergave van artsen
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // weergave van de afspraken van een arts
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // lijst met afspraken van een arts op een bepaalde dag
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // een RV toevoegen
        Rv rv = null;
        Creneau créneau = creneaux.get(2);
        Client client = clients.get(0);
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // controle
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // een RV toevoegen in hetzelfde tijdvak op dezelfde dag
        // moet een uitzondering veroorzaken
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // de fout wordt genoteerd
            erreur = true;
        }
        // er wordt gecontroleerd of er een fout is opgetreden
        Assert.assertTrue(erreur);
        // lijst met RV
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // agenda weergeven
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // een RV verwijderen
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // controle
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }

    // hulpprogramma - toont de items in een verzameling
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }

}
  • regel 22: met de annotatie [@SpringApplicationConfiguration] kan het eerder besproken configuratiebestand [DomainAndPersistenceConfig] worden gebruikt. De testklasse profiteert zo van alle beans die in dit bestand zijn gedefinieerd;
  • regel 23: de annotatie [@RunWith] maakt de integratie van Spring met JUnit mogelijk: de klasse kan nu worden uitgevoerd als een JUnit-test. [@RunWith] is een annotatie JUnit (regel 9), terwijl de klasse [SpringJUnit4ClassRunner] een Spring-klasse is (regel 12);
  • regels 26-27: injectie in de testklasse van een verwijzing naar de laag [métier];
  • veel tests zijn slechts eenvoudige visuele tests:
    • regels 32-33: lijst met klanten;
    • regels 35-36: lijst met artsen;
    • regels 39-40: lijst met afspraken van een arts;
    • regel 43: lijst met afspraken van een arts;
  • regel 50: toevoegen van een nieuwe afspraak. De methode [ajouterRv] geeft de afspraak weer met aanvullende informatie, namelijk de primaire sleutel id;
  • regel 53: deze primaire sleutel wordt gebruikt om de afspraak in de database op te zoeken;
  • regel 54: er wordt gecontroleerd of de gezochte afspraak en de gevonden afspraak dezelfde zijn. Ter herinnering: de methode [equals] van de entiteit [Rv] is opnieuw gedefinieerd: twee afspraken zijn gelijk als ze dezelfde id hebben. Dit toont aan dat de toegevoegde afspraak daadwerkelijk in de database is opgeslagen;
  • regels 61-73: er wordt geprobeerd dezelfde afspraak een tweede keer toe te voegen. Dit moet door de SGBD worden afgewezen, omdat er een uniekheidsvoorwaarde geldt:

CREATE TABLE IF NOT EXISTS `rv` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `JOUR` date NOT NULL,
  `ID_CLIENT` bigint(20) NOT NULL,
  `ID_CRENEAU` bigint(20) NOT NULL,
  `VERSION` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`ID`),
  UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
  KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
  KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;

Regel 8 hierboven geeft aan dat de combinatie [JOUR, ID_CRENEAU] uniek moet zijn, waardoor het niet mogelijk is om twee afspraken op dezelfde dag in hetzelfde tijdslot te plaatsen.

  • regel 73: er wordt gecontroleerd of er daadwerkelijk een uitzondering is opgetreden;
  • regel 77: de agenda wordt opgevraagd van de arts voor wie zojuist een afspraak is toegevoegd;
  • regel 79: er wordt gecontroleerd of de toegevoegde afspraak daadwerkelijk in zijn agenda staat;
  • regel 82: de toegevoegde afspraak wordt verwijderd;
  • regel 84: we zoeken de verwijderde afspraak op in de database;
  • regel 85: we controleren of we een pointer null hebben opgehaald, wat aangeeft dat de gezochte afspraak niet bestaat;

De test is geslaagd:

 

2.10. Het consoleprogramma

  

Het consoleprogramma is eenvoudig. Het laat zien hoe je een externe sleutel kunt ophalen:


package rdvmedecins.boot;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;

public class Boot {
    // opstarten
    public static void main(String[] args) {
        // de configuratie wordt voorbereid
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // de configuratie wordt gestart
        ConfigurableApplicationContext context = app.run(args);
        // bedrijfsactiviteit
        IMetier métier = context.getBean(IMetier.class);
        try {
            // een RV toevoegen
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // controle
            créneau = métier.getCreneauById(1L);
            long idMedecin = créneau.getIdMedecin();
            display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
        } catch (Exception ex) {
            System.out.println("Exception : " + ex.getCause());
        }
        // de Spring-context sluiten
        context.close();
    }

    // hulpprogramma - toont de elementen van een verzameling
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }

}

Het programma voegt een afspraak toe en controleert vervolgens of deze is toegevoegd.

  • regel 19: de klasse [SpringApplication] maakt gebruik van de configuratieklasse [DomainAndPersistenceConfig];
  • regel 20: verwijdering van de opstartlogs van de applicatie;
  • regel 22: de klasse [SpringApplication] wordt uitgevoerd. Deze retourneert een Spring-context, d.w.z. de lijst met geregistreerde beans;
  • regel 24: er wordt een verwijzing opgehaald naar de bean die de interface [IMetier] implementeert. Het gaat dus om een verwijzing naar de laag [métier];
  • regels 27-31: toevoeging van een nieuwe afspraak voor vandaag, voor klant nr. 1 in tijdvak nr. 1. De klant en het tijdvak zijn volledig verzonnen om aan te tonen dat alleen de identificatiecodes worden gebruikt. We hebben hier de versie geïnitialiseerd, maar we hadden er ook zomaar iets kunnen invullen. Deze wordt hier niet gebruikt;
  • regel 34: we willen weten welke arts tijdvak nr. 1 heeft. Daarvoor moeten we in de database tijdvak nr. 1 opzoeken. Omdat we in de modus [FetchType.LAZY] zitten, wordt de arts niet samen met het tijdvak opgehaald. We hebben er echter voor gezorgd dat er een veld [idMedecin] in de entiteit [Creneau] is opgenomen om de primaire sleutel van de arts op te halen;
  • regel 35: we halen de primaire code van de arts op;
  • regel 36: de lijst met afspraken van de arts wordt weergegeven;

De console-uitvoer is als volgt:

1
2
3
4
Ajout d'un Rv le [10/06/2014] dans le créneau 1 pour le client 1
Rv ajouté = Rv[113, Tue Jun 10 16:51:01 CEST 2014, 1, 1]
Liste des rendez-vous
Rv[113, 2014-06-10, 1, 1]

2.11. Inleiding tot Spring MVC

We gaan nu verder met het bouwen van de weblaag. Deze bestaat voornamelijk uit methoden die specifieke URL verwerken en reageren met een regel tekst in het JSON-formaat (Javascript Object Notation). Deze weblaag is een webinterface die soms een API-web wordt genoemd. We gaan deze interface implementeren met Spring MVC, een andere tak van het Spring-ecosysteem. We beginnen met het bestuderen van een van de handleidingen die te vinden zijn op [http://spring.io].

2.11.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. In dit document zal ik de service die we gaan bouwen simpelweg een / JSON-webservice noemen. Een service wordt als RESTful beschouwd als deze aan bepaalde regels voldoet. Ik heb niet geprobeerd om aan deze regels te voldoen.

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

2.11.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.1.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>http://repo.spring.io/release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>http://repo.spring.io/release</url>
        </pluginRepository>
    </pluginRepositories>
</project>
  • regels 10-14: net als in het project [Spring Data] vinden we hier het bovenliggende project [Spring Boot];
  • regels 17-20: het artefact [spring-boot-starter-web] bevat de bibliotheken die nodig zijn voor een Spring-project MVC. Het bevat met name een ingebouwde Tomcat-server. Op deze server wordt de applicatie uitgevoerd;
  • regels 21-24: de Jackson-bibliotheek zorgt voor de omzetting van een Java-object naar een tekenreeks en omgekeerd;

Deze configuratie bevat een groot aantal bibliotheken:

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

2.11.3. De architectuur van een Spring-service REST

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 [Dispatcher Servlet] is de Spring-klasse die de binnenkomende URL verwerkt. Deze „routeert“ de URL naar de actie die deze moet verwerken. Deze acties zijn methoden van specifieke klassen die [Contrôleurs] worden genoemd. De C van MVC is hier de tekenreeks [Dispatcher Servlet, Contrôleur, Action]. Als er geen actie is geconfigureerd om de binnenkomende URL te verwerken, zal de servlet [Dispatcher Servlet] antwoorden dat de gevraagde URL niet is gevonden (fout 404 NOT FOUND);
  1. verwerking
  • de gekozen actie kan gebruikmaken van de parameters parami die de servlet [Dispatcher Servlet] 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 samen met het verzoek zijn verzonden;
  • bij de verwerking van het verzoek van de gebruiker kan de actie de laag [metier] [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];
  1. 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 omgezet in de tekenreeks JSON;
  • in [4b] wordt deze tekenreeks JSON naar de browser verzonden;

2.11.4. De C-controller

  

De geïmporteerde applicatie heeft de volgende controller:


package hello;

import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping("/greeting")
    public @ResponseBody
    Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}
  • regel 9: de annotatie [@Controller] maakt van de klasse [GreetingController] een Spring-controller, d.w.z. dat de methoden ervan zijn geregistreerd om URL te verwerken;
  • regel 15: de annotatie [@RequestMapping] geeft aan welke URL de methode verwerkt, in dit geval de URL [/greeting]. We zullen later zien dat deze URL kan worden geconfigureerd en dat het mogelijk is om deze parameters op te halen;
  • regel 16: de annotatie [@ResponseBody] geeft aan dat de methode geen sjabloon voor een weergave genereert (JSP, JSF, Thymeleaf, ...) dat vervolgens naar de clientbrowser wordt verzonden, maar genereert zelf het antwoord dat naar de browser wordt gestuurd. Hier genereert de methode een object van het type [Greeting] (regel 18). Hoewel dit hier niet direct zichtbaar is, wordt dit object eerst omgezet naar JSON voordat het naar de browser wordt verzonden. 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 17: de methode [greeting] heeft een parameter [String name]. De annotatie [@RequestParam(value = "name", required = false, defaultValue = "World"] geeft aan dat deze parameter moet worden geïnitialiseerd met een parameter met de naam [name](@RequestParam(value = "name"). Deze kan de parameter zijn van een GET of een POST. Deze parameter is niet verplicht (required = false). In het laatste geval wordt de parameter [name] van de methode geïnitialiseerd met de waarde [World] (defaultValue = "World").

2.11.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"} opleveren. 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!"}

2.11.6. Projectconfiguratie

  

Het project wordt geconfigureerd door de volgende klasse [Application]:


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);
    }
}
  • regel 11: vreemd genoeg is deze klasse uitvoerbaar met een methode [main] die specifiek is voor console-applicaties. Dat is inderdaad het geval. De klasse [SpringApplication] op regel 12 start de Tomcat-server die in de afhankelijkheden staat en implementeert de service REST daarop;
  • regel 4: we zien dat de klasse [SpringApplication] behoort tot het project [Spring Boot];
  • regel 12: de eerste parameter is de klasse die het project configureert, de tweede eventuele parameters;
  • regel 8: de annotatie [@EnableAutoConfiguration] vraagt Spring Boot om het project te configureren;
  • regel 7: de annotatie [@ComponentScan] zorgt ervoor dat de map die de klasse [Application] bevat, wordt doorzocht naar Spring-componenten. Er wordt er één gevonden: de klasse [GreetingController], die de annotatie [@Controller] heeft, waardoor deze een Spring-component is;

2.11.7. Het project uitvoeren

Laten we het project uitvoeren:

 

We krijgen de volgende console-logs:

____ _ __ _ _

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

2014-06-11 14:31:36.435  INFO 11744 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 11744 (D:\Temp\wksSTS\gs-rest-service-complete\target\classes started by ST in D:\Temp\wksSTS\gs-rest-service-complete)
2014-06-11 14:31:36.473  INFO 11744 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7684af0b: startup date [Wed Jun 11 14:31:36 CEST 2014]; root of context hierarchy
2014-06-11 14:31:36.966  INFO 11744 --- [           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-06-11 14:31:37.760  INFO 11744 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-06-11 14:31:37.955  INFO 11744 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-06-11 14:31:37.956  INFO 11744 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-06-11 14:31:38.053  INFO 11744 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-06-11 14:31:38.054  INFO 11744 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1584 ms
2014-06-11 14:31:38.596  INFO 11744 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-06-11 14:31:38.598  INFO 11744 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-06-11 14:31:38.919  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.125  INFO 11744 --- [           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-06-11 14:31:39.129  INFO 11744 --- [           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-06-11 14:31:39.130  INFO 11744 --- [           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-06-11 14:31:39.160  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] naar handler van het type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.160  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler van het type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.448  INFO 11744 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-11 14:31:39.490  INFO 11744 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-11 14:31:39.492  INFO 11744 --- [           main] hello.Application                        : Started Application in 3.45 seconds (JVM running for 3.93)
  • regel 12: de Tomcat-server start op poort 8080 (regel 11);
  • regel 16: de servlet [DispatcherServlet] is aanwezig;
  • regel 19: 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-plug-in met de naam [Advanced Rest Client] (zie bijlagen):

  • in [1], de aangevraagde 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;

2.11.8. Een uitvoerbaar archief maken

Het is mogelijk om buiten Eclipse een uitvoerbaar archief aan te maken. De benodigde configuratie staat in het bestand [pom.xml]:


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
</build>
  • de regels 9-12 definiëren de plug-in die het uitvoerbare archief gaat maken;
  • regel 3 definieert de uitvoerbare klasse van het project;

Dit gaat als volgt:

  • 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 aangemaakt;
  • in [4]: het doelbestand wordt gegenereerd;

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

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

Ga met een console naar de gegenereerde map:

1
2
3
4
5
6
7
8
9
D:\Temp\wksSTS\gs-rest-service-complete\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:

 

2.11.9. De applicatie op een Tomcat-server implementeren

Hoewel Spring Boot erg handig is in de ontwikkelingsmodus, zal een applicatie in productie waarschijnlijk op een echte Tomcat-server worden geïmplementeerd. Dit gaat als volgt:

Pas 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>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>
....
</project>

De wijzigingen moeten op twee plaatsen worden aangebracht:

  • regel 9: hier moet worden aangegeven dat er een WAR-archief (Web ARchive) wordt gegenereerd;
  • regels 26-30: voeg een afhankelijkheid toe voor het artefact [spring-boot-starter-tomcat]. Dit artefact voegt alle Tomcat-klassen toe aan de afhankelijkheden van het project;
  • regel 29: dit artefact is [provided], wat betekent dat de bijbehorende archieven niet in de gegenereerde WAR worden opgenomen. Deze archieven zijn namelijk te vinden op de Tomcat-server waarop de applicatie zal draaien;

Bovendien moet de webapplicatie worden geconfigureerd. Bij gebrek aan het bestand [web.xml] 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: de klasse die het project configureert, wordt opgegeven;

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

  • in [1] voert men het project uit op een van de servers die zijn geregistreerd in de IDE Eclipse;
  • in [2] kiest men [tc Server Developer], dat standaard aanwezig is. Dit is een variant van Tomcat;

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

 

We weten nu hoe we een WAR-archief moeten genereren. Vervolgens gaan we verder met Spring Boot en het uitvoerbare JAR-archief daarvan.

2.11.10. Een nieuw webproject aanmaken

Om een nieuw webproject te bouwen, kun je als volgt te werk gaan:

  • in [1]: Bestand / Nieuw / Spring Starter-project
  • in [2]: selecteer [Web]. We selecteren geen view-bibliotheken, omdat er in een webservice / JSON geen views zijn;
  • het aangemaakte project wordt een Maven-project. In [3] voer je de groep in van het Maven-artefact dat wordt aangemaakt, in [4] de naam van het artefact;
  • in [5] voer je de naam in van een pakket waarin Spring de configuratieklasse van het project zal plaatsen;
  • in [6] geef je het Eclipse-project een naam – deze mag afwijken van [4];
 

2.12. De laag [web]

  

We gaan de weblaag in verschillende stappen opbouwen:

  • stap 1: een operationele weblaag zonder authenticatie;
  • stap 2: authenticatie implementeren met Spring Security;
  • stap 3: implementatie van CORS [Cross-origin resource sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. De client van onze webservice zal een Angular-webclient zijn die niet noodzakelijkerwijs tot hetzelfde domein behoort als onze webservice. Standaard heeft deze dan geen toegang, tenzij de webservice dit toestaat. We zullen zien hoe;

2.12.1. Maven-configuratie

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


<modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring4.mvc</groupId>
    <artifactId>rdvmedecins-webapi-v1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rdvmedecins-webapi-v1</name>
    <description>Gestion de RV Médecins</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>istia.st.spring4.rdvmedecins</groupId>
            <artifactId>rdvmedecins-metier-dao</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
  • regels 7-11: het bovenliggende Maven-project;
  • regels 13-16: de afhankelijkheden voor een Spring-project MVC;
  • regels 17-21: de afhankelijkheden van het lagenproject [métier, DAO, JPA];

2.12.2. De interface van de webservice

  • in [1], hierboven, kan de browser slechts een beperkt aantal URL opvragen met een specifieke syntaxis;
  • in [4] ontvangt hij een antwoord JSON;

De antwoorden van onze webservice zullen allemaal dezelfde vorm hebben, die overeenkomt met de transformatie JSON van een object van het type [Reponse], namelijk:


package rdvmedecins.web.models;

public class Reponse {

    // ----------------- eigenschappen
    // status van de transactie
    private int status;
    // het antwoord JSON
    private Object data;

    // ---------------fabrikanten
    public Reponse() {
    }

    public Reponse(int status, Object data) {
        this.status = status;
        this.data = data;
    }

    // methoden
    public void incrStatusBy(int increment) {
        status += increment;
    }

    // ----------------------getters en setters
...
}
  • regel 7: foutcode van het antwoord 0: OK, anders: KO;
  • regel 9: de inhoud van het antwoord;

Hieronder vindt u de schermafbeeldingen die de interface van de webservice / JSON illustreren:

Lijst van alle patiënten van de artsenpraktijk [/getAllClients]

Lijst van alle artsen van de medische praktijk [/getAllMedecins]

Lijst met de spreekuren van een arts [/getAllCreneaux/{idMedecin}]

Lijst met afspraken van een arts [/getRvMedecinJour/{idMedecin}/{jjjj-mm-dd}

Agenda van een arts [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]

Om een afspraak toe te voegen of te verwijderen gebruiken we de Chrome-extensie [Advanced Rest Client], omdat deze handelingen met een POST worden uitgevoerd.

Een afspraak toevoegen [/ajouterRv]

  • in [0], de URL van de webservice;
  • in [1] wordt de methode POST gebruikt;
  • in [2], de tekst JSON van de informatie die in de vorm {dag, idClient, idCreneau} naar de webservice wordt verzonden;
  • in [3] geeft de client aan de webservice aan dat hij informatie in het formaat JSON verstuurt;

Het antwoord is dan als volgt:

  • in [4]: de client stuurt de header waarmee hij aangeeft dat de gegevens die hij verstuurt in het formaat JSON zijn;
  • in [5]: de webservice antwoordt dat hij ook JSON verstuurt;
  • in [6]: het antwoord JSON van de webservice. Het veld [data] bevat de vorm JSON van de toegevoegde afspraak;

De aanwezigheid van de nieuwe afspraak kan worden gecontroleerd:

Een afspraak verwijderen [/supprimerRv]

  • in [1], de URL van de webservice;
  • in [2] wordt de methode POST gebruikt;
  • in [3], de tekst JSON van de informatie die in de vorm {idRv} naar de webservice wordt verzonden;
  • in [4] geeft de client aan de webservice aan dat hij informatie JSON verstuurt;

Het antwoord luidt dan als volgt:

  • in [5]: het veld [status] is 0, wat aangeeft dat de bewerking is geslaagd;

Het verwijderen van de afspraak kan worden gecontroleerd:

Hierboven is de afspraak van patiënt [Mme GERMAN] niet meer aanwezig.

Via de webservice kunnen entiteiten ook op basis van hun ID worden opgehaald:

Al deze URL-entiteiten worden verwerkt door de controller [RdvMedecinsController], die we nu zullen bespreken.

2.12.3. Het raamwerk van de controller [RdvMedecinsController]

  

De controller [RdvMedecinsController] ziet er als volgt uit:


package rdvmedecins.web.controllers;

import java.text.ParseException;
...

@RestController
public class RdvMedecinsController {

    @Autowired
    private ApplicationModel application;
    private List<String> messages;

    @PostConstruct
    public void init() {
        // foutmeldingen van de applicatie
        messages = application.getMessages();
    }

    // lijst met artsen
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
...
    }

    // lijst met klanten
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients() {
...
    }

    // lijst met beschikbare tijdvakken van een arts
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
    }

    // lijst met afspraken van een arts
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
            @PathVariable("jour") String jour) {
...
    }

    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
    }

    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
    }

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(
            @PathVariable("idMedecin") long idMedecin,
            @PathVariable("jour") String jour) {
...
    }
}
  • regel 6: de annotatie [@RestController] maakt van de klasse [RdvMedecinsController] een Spring-controller. Bovendien zorgt dit er ook voor dat de methoden die URL verwerken, een antwoord genereren dat automatisch wordt omgezet in JSON;
  • regels 9-10: hier wordt door Spring een object van het type [ApplicationModel] geïnjecteerd;
  • regel 13: de annotatie [@PostConstruct] markeert een methode die direct na het instantiëren van de klasse moet worden uitgevoerd. Wanneer deze wordt uitgevoerd, zijn de door Spring geïnjecteerde objecten beschikbaar;
  • alle methoden retourneren een object van het type [Reponse], namelijk:

package rdvmedecins.web.models;

public class Reponse {

    // ----------------- eigenschappen
    // status van de bewerking
    private int status;
    // het antwoord
    private Object data;
...
}

Dit object wordt geserialiseerd naar JSON voordat het naar de browser van de klant wordt verzonden;

  • regel 20: de annotatie [@RequestMapping] bepaalt de voorwaarden voor het aanroepen van de methode. Hier verwerkt de methode een verzoek GET van de URL [/getAllMedecins]. Als deze URL zou worden aangevraagd door een POST, zou deze worden geweigerd en zou Spring MVC een foutcode HTTP naar de webclient sturen;
  • regel 32: de URL wordt ingesteld door {idMedecin}. Deze parameter wordt opgehaald met de annotatie [@PathVariable] op regel 33;
  • regel 33: de enige parameter [long idMedecin] krijgt zijn waarde van de parameter {idMedecin} van de URL [@PathVariable("idMedecin")]. De parameter in URL en die van de methode kunnen verschillende namen hebben. Hierbij moet worden opgemerkt dat [@PathVariable("idMedecin")] van het type String is (de gehele URL is een String), terwijl de parameter [long idMedecin] van het type [long] is. De typewijziging gebeurt automatisch. Er wordt een foutcode HTTP geretourneerd als deze typewijziging mislukt;
  • regel 65: de annotatie [@RequestBody] verwijst naar de body van de aanvraag. In een verzoek van het type GET is er vrijwel nooit een hoofdtekst (maar het is mogelijk er een op te nemen). In een verzoek van het type POST is er meestal wel een (maar het is mogelijk er geen op te nemen). Voor de URL [ajouterRv] verstuurt de webclient in zijn POST de volgende tekenreeks JSON:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

De syntaxis [@RequestBody PostAjouterRv post] (regel 65) in combinatie met het feit dat de methode de JSON [consumes = "application/json; charset=UTF-8"] op regel 64 verwacht, zorgt ervoor dat de door de webclient verzonden tekenreeks JSON wordt gedeserialiseerd tot een object van het type [PostAjouter]. Dit is het volgende:


package rdvmedecins.web.models;

public class PostAjouterRv {

    // gegevens van de post
    private String jour;
    private long idClient;
    private long idCreneau;

    // getters en setters
    ...
}

Ook hier vinden de benodigde typewijzigingen automatisch plaats;

  • in de regels 69-70 vinden we een soortgelijk mechanisme voor de URL [/supprimerRv]. De verzonden tekenreeks JSON is als volgt:
{"idRv":116}

en het type [PostSupprimerRv] is als volgt:


package rdvmedecins.web.models;

public class PostSupprimerRv {

    // postgegevens
    private long idRv;

    // getters en setters
    ...
}

2.12.4. De sjablonen van de webservice

  

We hebben de modellen [Reponse, PostAjouterRv, PostSupprimerRv] al voorgesteld. Het model [ApplicationModel] is het volgende:


package rdvmedecins.web.models;

import java.util.Date;
...

@Component
public class ApplicationModel implements IMetier {

    // de laag [métier]
    @Autowired
    private IMetier métier;

    // gegevens afkomstig van de laag [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    // foutmeldingen
   private List<String> messages;

    @PostConstruct
    public void init() {
        // artsen en klanten worden opgehaald
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }

    // getter
    public List<String> getMessages() {
        return messages;
    }

    // ------------------------- interface van de laag [métier]
    @Override
    public List<Client> getAllClients() {
        return clients;
    }

    @Override
    public List<Medecin> getAllMedecins() {
        return médecins;
    }

    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return métier.getAllCreneaux(idMedecin);
    }

    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return métier.getRvMedecinJour(idMedecin, jour);
    }

    @Override
    public Client getClientById(long id) {
        return métier.getClientById(id);
    }

    @Override
    public Medecin getMedecinById(long id) {
        return métier.getMedecinById(id);
    }

    @Override
    public Rv getRvById(long id) {
        return métier.getRvById(id);
    }

    @Override
    public Creneau getCreneauById(long id) {
        return métier.getCreneauById(id);
    }

    @Override
    public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
        return métier.ajouterRv(jour, creneau, client);
    }

    @Override
    public void supprimerRv(Rv rv) {
        métier.supprimerRv(rv);
    }

    @Override
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        return métier.getAgendaMedecinJour(idMedecin, jour);
    }

}
  • regel 6: de annotatie [@Component] maakt van de klasse [ApplicationModel] een Spring-component. Net als alle Spring-componenten die we tot nu toe hebben gezien (met uitzondering van @Controller), wordt er slechts één object van dit type geïnstantieerd (singleton);
  • regel 7: de klasse [ApplicationModel] implementeert de interface [IMetier];
  • regels 10-11: er wordt door Spring een verwijzing naar de laag [métier] geïnjecteerd;
  • regel 19: de annotatie [@PostConstruct] zorgt ervoor dat de methode [init] direct na het instantiëren van de klasse [ApplicationModel] wordt uitgevoerd;
  • regels 23-24: de lijsten met artsen en klanten worden opgehaald uit de laag [métier];
  • regel 26: als er een uitzondering optreedt, worden de berichten uit de uitzonderingsstack opgeslagen in het veld van regel 17;

De klasse [ApplicationModel] zal voor twee doeleinden worden gebruikt:

  • als cache om de lijsten met artsen en patiënten (klanten) op te slaan;
  • als enige interface voor de controllers;

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 de cache. Momenteel worden de afspraken van de artsen niet in de cache opgeslagen. Om dit wel te doen, volstaat het om de klasse [ApplicationModel] aan te passen. Dit heeft geen invloed op de controller, die de methode [List<Creneau> getAllCreneaux(long idMedecin)] blijft gebruiken zoals voorheen. Het is de implementatie van deze methode in [ApplicationModel] die zal worden gewijzigd.

2.12.5. De klasse Static

De klasse [Static] bevat een reeks statische hulpprogramma's die geen „bedrijfsspecifieke“ of „webgerichte“ aspecten hebben:

  

De code ervan is als volgt:


package rdvmedecins.web.helpers;

import java.text.SimpleDateFormat;
...

public class Static {

    public Static() {
    }

    // lijst met foutmeldingen van een uitzondering
    public static List<String> getErreursForException(Exception exception) {
        // de lijst met foutmeldingen van de uitzondering ophalen
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }

    // Object-to-Map-mappers
    // --------------------------------------------------------
....
}
  • regel 12: de methode [Static.getErreursForException] die (op regel 8 hieronder) is gebruikt in de methode [init] van de klasse [ApplicationModel]:

    @PostConstruct
    public void init() {
        // de artsen en klanten ophalen
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

De methode maakt een object [List<String>] aan met de foutmeldingen [exception.getMessage()] van een uitzondering [exception] en van de uitzonderingen die deze bevat, [exception.getCause()].

De klasse [Static] bevat nog andere hulpprogramma's, waar we op terugkomen zodra we ze tegenkomen.

We gaan nu de verwerking van de URL van de webservice in detail bespreken. Bij deze verwerking zijn drie hoofdklassen betrokken:

  • de controller [RdvMedecinsController];
  • de klasse met hulpprogramma's [Static];
  • de cacheklasse [ApplicationModel];
  

2.12.6. De methode [init] van de controller

De controller [RdvMedecinsController] (zie paragraaf 2.12.3) heeft een methode [init] die direct na het instantiëren ervan wordt uitgevoerd:


    @Autowired
    private ApplicationModel application;
    private List<String> messages;

    @PostConstruct
    public void init() {
        // foutmeldingen van de applicatie
        messages = application.getMessages();
}
  • regel 8: de foutmeldingen die zijn opgeslagen in de cache-applicatie [ApplicationModel] worden lokaal opgeslagen in het veld op regel 3. Hierdoor kunnen de methoden vaststellen of de applicatie correct is geïnitialiseerd.

2.12.7. De URL [/getAllMedecins]

De URL [/getAllMedecins] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:


    // lijst met artsen
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lijst met artsen
        try {
            return new Reponse(0, application.getAllMedecins());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}
  • regel 5: er wordt gecontroleerd of de applicatie correct is geïnitialiseerd (messages==null). Als dat niet het geval is, wordt een antwoord teruggestuurd met status=-1 en data=messages;
  • regel 10: anders sturen we de lijst met artsen terug met een status gelijk aan 0. De methode [application.getAllMedecins()] genereert geen uitzondering, omdat deze alleen een lijst retourneert die in de cache staat. Toch behouden we deze uitzonderingsafhandeling voor het geval de artsen niet meer in de cache worden opgeslagen;

We hebben het geval waarin de applicatie niet correct is geïnitialiseerd nog niet geïllustreerd. Laten we SGBD en MySQL5 stoppen, de webservice starten en vervolgens URL en [/getAllMedecins] opvragen:

Image

We krijgen inderdaad een foutmelding. In een normale situatie krijgen we het volgende scherm te zien:

2.12.8. De URL [/getAllClients]

De URL [/getAllClients] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:


    // lijst met klanten
    @RequestMapping(value = "/getAllClients")
    public Reponse getAllClients() {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lijst met klanten
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}

Deze methode is vergelijkbaar met de eerder besproken methode [getAllMedecins]. De verkregen resultaten zijn als volgt:

2.12.9. De URL [/getAllCreneaux/{idMedecin}]

De URL [/getAllCreneaux/{idMedecin}] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:


// lijst met spreekuren van een arts
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // de arts wordt opgehaald
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // afspraken van de arts
        List<Creneau> créneaux = null;
        try {
            créneaux = application.getAllCreneaux(médecin.getId());
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // antwoord wordt teruggestuurd
        return new Reponse(0, Static.getListMapForCreneaux(créneaux));
    }
  • regel 9: de arts die wordt geïdentificeerd door de parameter [id] wordt opgevraagd via een lokale methode:

    private Reponse getMedecin(long id) {
        // de arts ophalen
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // bestaande arts?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Deze methode keert terug met een status in [0,1,2]. Laten we terugkeren naar de code van de methode [getAllCreneaux]:

  • regels 10-12: als status!=0, wordt het antwoord onmiddellijk teruggestuurd;
  • regel 13: we halen de arts op;
  • regel 17: we halen de beschikbare tijdvakken van deze arts op;
  • regel 22: we sturen als antwoord een object [Static.getListMapForCreneaux(créneaux)];

Laten we de definitie van de klasse [Creneau] nog eens bekijken:


@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // kenmerken van een tijdvak van RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // een tijdvak is gekoppeld aan een arts
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // vreemde sleutel
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • regel 13: de arts wordt opgezocht in de modus [FetchType.LAZY];

Laten we nog eens kijken naar de aanvraag JPQL die de methode [getAllCreneaux] implementeert in de laag [DAO]:


@Query("select c from Creneau c where c.medecin.id=?1")

De notatie [c.medecin.id] dwingt de join tussen de tabellen [CRENEAUX] en [MEDECINS] af. De query levert dus alle afspraken van de arts op, waarbij in elk daarvan de arts wordt vermeld. Wanneer we deze afspraken serialiseren naar JSON, verschijnt in elk daarvan de tekenreeks JSON van de arts. Dit is overbodig. In plaats van een object [Creneau] te serialiseren, gaan we dus een object [Map] serialiseren, waarin we alleen de gewenste velden opnemen.

Laten we teruggaan naar de code die we eerder hebben bekeken:


// het antwoord wordt teruggestuurd
return new Reponse(0, Static.getListMapForCreneaux(créneaux));

De methode [Static.getListMapForCreneaux] is als volgt:


    // List<Creneau> --> List<Map>
    public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
        // lijst met woordenboeken <String,Object>
        List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
        for (Creneau créneau : créneaux) {
            liste.add(Static.getMapForCreneau(créneau));
        }
        // de lijst wordt geretourneerd
        return liste;
}

en de methode [Static.getMapForCreneau] is als volgt:


    // Creneau --> Map
    public static Map<String, Object> getMapForCreneau(Creneau créneau) {
        // moet er iets gebeuren?
        if (créneau == null) {
            return null;
        }
        // woordenboek <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", créneau.getId());
        hash.put("hDebut", créneau.getHdebut());
        hash.put("mDebut", créneau.getMdebut());
        hash.put("hFin", créneau.getHfin());
        hash.put("mFin", créneau.getMfin());
        // we geven het woordenboek weer
        return hash;
}
  • regel 8: er wordt een woordenboek aangemaakt;
  • regels 9-13: we voegen de velden toe die we willen behouden in de string JSON. Het veld [medecin] staat er niet in;
  • regel 15: dit woordenboek wordt teruggegeven;

De verkregen resultaten zijn als volgt:

of deze, als het tijdslot niet bestaat:

of deze in geval van een fout bij het benaderen van de database:

2.12.10. De URL [/getRvMedecinJour/{idMedecin}/{jour}]

De URL [/getRvMedecinJour/{idMedecin}/{jour}] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:


// lijst met afspraken van een arts
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we controleren de datum
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(3, null);
        }
        // de arts ophalen
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // lijst met zijn afspraken
        List<Rv> rvs = null;
        try {
            rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // het antwoord wordt teruggestuurd
        return new Reponse(0, Static.getListMapForRvs(rvs));
}
  • regel 31: er wordt een List<Map<String,Object>>-object geretourneerd in plaats van een List<Rv>-object. Laten we de definitie van de klasse [Rv] nog eens bekijken:

@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // kenmerken van een afspraak
    @Temporal(TemporalType.DATE)
    private Date jour;

    // een afspraak is gekoppeld aan een klant
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // een afspraak is gekoppeld aan een tijdslot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // vreemde sleutels
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

...

}
  • regel 11: de klant wordt opgezocht met de modus [FetchType.LAZY];
  • regel 18: het tijdvak wordt opgezocht met de modus [FetchType.LAZY];

Laten we nog eens kijken naar de query JPQL die de afspraken opzoekt:


@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")

Er worden expliciet join-bewerkingen uitgevoerd om de velden [client] en [creneau] op te halen. Bovendien krijgen we door de join [cr.medecin.id=?1] ook de arts. De arts zal dus in de string JSON van elke afspraak verschijnen. Deze dubbele informatie is echter overbodig. Laten we teruggaan naar de code van de methode:

  • regel 31: we stellen zelf het woordenboek samen dat moet worden geserialiseerd in JSON;

Het woordenboek dat voor een afspraak wordt samengesteld, is als volgt:


    // Rv --> Kaart
    public static Map<String, Object> getMapForRv(Rv rv) {
        // moet er iets gebeuren?
        if (rv == null) {
            return null;
        }
        // woordenboek <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", rv.getId());
        hash.put("client", rv.getClient());
        hash.put("creneau", getMapForCreneau(rv.getCreneau()));
        // we geven het woordenboek terug
        return hash;
}
  • regel 11: we nemen het woordenboek over van het object [Creneau] dat we eerder hebben gepresenteerd;

De verkregen resultaten zijn als volgt:

of deze, met een onjuiste dag:

of deze met een onjuiste arts:

2.12.11. De URL [/getAgendaMedecinJour/{idMedecin}/{jour}]

De URL [/getAgendaMedecinJour/{idMedecin}/{jour}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we controleren de datum
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
        }
        // de arts ophalen
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // zijn agenda ophalen
        AgendaMedecinJour agenda = null;
        try {
            agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // ok
        return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
    }
}
  • op regel 30 wordt een object van het type List<Map<String,Object>> geretourneerd.

De methode [Static.getMapForAgendaMedecinJour] is als volgt:


    // AgendaMedecinJour --> Kaart
    public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
        // iets te doen?
        if (agenda == null) {
            return null;
        }
        // woordenboek <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("medecin", agenda.getMedecin());
        hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
        List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
        for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
            créneaux.add(getMapForCreneauMedecinJour(créneau));
        }
        hash.put("creneauxMedecin", créneaux);
        // we geven het woordenboek weer
        return hash;
}

Het opgebouwde woordenboek heeft drie velden:

  • [medecin]: de arts die eigenaar is van de agenda. Deze informatie is behouden omdat ze slechts één keer voorkomt, terwijl ze in de voorgaande gevallen in elke JSON-string werd herhaald;
  • [jour]: de dag van de agenda;
  • [creneauxMedecin]: de lijst met beschikbare tijdvakken van de arts, met een eventuele afspraak in dat tijdvak;

De methode [getMapForCreneauMedecinJour] die op regel 13 wordt gebruikt, is als volgt:


    // CreneauMedecinJour --> map
    public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
        // moet er iets gebeuren?
        if (créneau == null) {
            return null;
        }
        // woordenboek <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
        hash.put("rv", getMapForRv(créneau.getRv()));
        // we geven het woordenboek terug
        return hash;
}
  • regels 9-10: we gebruiken de reeds besproken woordenboeken voor de typen [Creneau] en [Rv], die dus geen [Medecin]-object bevatten;

De verkregen resultaten zijn als volgt:

of deze, als de dag onjuist is:

of deze als het artsnummer ongeldig is:

2.12.12. De URL [/getMedecinById/{id}]

De URL [/getMedecinById/{id}] wordt verwerkt volgens de volgende methode van de [RdvMedecinsController]-controller:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we halen de arts op
        return getMedecin(id);
}

Regel 8, de methode [getMedecin] is als volgt:


    private Reponse getMedecin(long id) {
        // de arts ophalen
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // Bestaat de arts?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

De verkregen resultaten zijn als volgt:

of deze, als het artsnummer onjuist is:

2.12.13. De URL [/getClientById/{id}]

De URL [/getClientById/{id}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // de klant wordt opgehaald
        return getClient(id);
}

Regel 8, de methode [getClient] is als volgt:


    private Reponse getClient(long id) {
        // de klant ophalen
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // bestaande klant?
        if (client == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, client);
}

De verkregen resultaten zijn als volgt:

of deze, als het klantnummer onjuist is:

2.12.14. De URL [/getCreneauById/{id}]

De URL [/getCreneauById/{id}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // het tijdvak wordt opgehaald
        Reponse réponse = getCreneau(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
        }
        // resultaat
        return réponse;
}

Regel 8, de methode [getCreneau] is als volgt:


    private Reponse getCreneau(long id) {
        // het tijdvak wordt opgehaald
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // bestaand tijdvak?
        if (créneau == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, créneau);
}

De verkregen resultaten zijn als volgt:

of deze, als het slotnummer onjuist is:

2.12.15. De URL [/getRvById/{id}]

De URL [/getRvById/{id}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // de afspraak wordt opgehaald
        Reponse réponse = getRv(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
        }
        // resultaat
        return réponse;
}

Regel 8, de methode [getRv] is als volgt:


    private Reponse getRv(long id) {
        // Rv wordt opgehaald
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // Bestaat Rv?
        if (rv == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, rv);
}

Regel 10: de methode [Static.getMapForRv2] is als volgt:


// Rv --> Kaart
    public static Map<String, Object> getMapForRv2(Rv rv) {
        // moet er iets gebeuren?
        if (rv == null) {
            return null;
        }
        // woordenboek <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", rv.getId());
        hash.put("idClient", rv.getIdClient());
        hash.put("idCreneau", rv.getIdCreneau());
        // we geven het woordenboek terug
        return hash;
    }

De verkregen resultaten zijn als volgt:

of deze, als het afsprachnummer onjuist is:

2.12.16. De URL [/ajouterRv]

De URL [/ajouterRv] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we halen de verzonden waarden op
        String jour = post.getJour();
        long idCreneau = post.getIdCreneau();
        long idClient = post.getIdClient();
        // de datum controleren
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(6, null);
        }
        // het tijdvak wordt opgehaald
        Reponse réponse = getCreneau(idCreneau);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Creneau créneau = (Creneau) réponse.getData();
        // de klant wordt opgehaald
        réponse = getClient(idClient);
        if (réponse.getStatus() != 0) {
            réponse.incrStatusBy(2);
            return réponse;
        }
        Client client = (Client) réponse.getData();
        // de afspraak wordt toegevoegd
        Rv rv = null;
        try {
            rv = application.ajouterRv(jourAgenda, créneau, client);
        } catch (Exception e1) {
            return new Reponse(5, Static.getErreursForException(e1));
        }
        // het antwoord wordt teruggestuurd
        return new Reponse(0, Static.getMapForRv(rv));
    }

Dit is niets nieuws. Op regel 41 wordt de afspraak teruggegeven die op regel 36 is toegevoegd.

De verkregen resultaten zien er als volgt uit met de client [Advanced Rest Client]:

of zo, als je bijvoorbeeld een niet-bestaand tijdvaknummer opgeeft:

2.12.17. De URL [/supprimerRv]

De URL [/supprimerRv] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
        // status van de applicatie
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // de verzonden waarden worden opgehaald
        long idRv = post.getIdRv();
        // de Rv wordt opgehaald
        Reponse réponse = getRv(idRv);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        // verwijdering van de Rv
        try {
            application.supprimerRv(idRv);
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // ok
        return new Reponse(0, null);
    }

De verkregen resultaten van de zijn als volgt:

of deze, als het afsprachnummer niet bestaat:

We zijn klaar met de controller. Nu gaan we bekijken hoe we het project moeten configureren.

2.12.18. Configuratie van de webservice

  

De configuratieklasse [AppConfig] ziet er als volgt uit:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {

}
  • regel 9: we schakelen over naar de modus [AutoConfiguration], zodat Spring Boot het project kan configureren op basis van de archieven die het in het classpath van het project aantreft;
  • regel 10: er wordt aangegeven dat de Spring-componenten moeten worden gezocht in het pakket [rdvmedecins.web] en de onderliggende pakketten. Op deze manier worden de componenten gevonden:
    • [@RestController RdvMedecinsController] in het pakket [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] in het pakket [rdvmedecins.web.models];
  • regel 11: de klasse [DomainAndPersistenceConfig] wordt geïmporteerd, die het project [rdvmedecins-metier-dao] configureert om toegang te krijgen tot de beans van dit project;

2.12.19. De uitvoerbare klasse van de webservice

  

De klasse [Boot] ziet er als volgt uit:


package rdvmedecins.web.boot;

import org.springframework.boot.SpringApplication;

import rdvmedecins.web.config.AppConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

Op regel 10 wordt de statische methode [SpringApplication.run] uitgevoerd met als eerste parameter de projectconfiguratieklasse [AppConfig]. Deze methode voert de automatische configuratie van het project uit, start de in de afhankelijkheden ingebouwde Tomcat-server en implementeert daar de controller [RdvMedecinsController].

De logbestanden bij de uitvoering zijn de volgende:

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

2014-06-12 17:30:41.261  INFO 9388 --- [           main] rdvmedecins.web.boot.Boot                : Starting Boot on Gportpers3 with PID 9388 (D:\data\istia-1314\polys\istia\angularjs-spring4\dvp\rdvmedecins-webapi\target\classes started by ST)
2014-06-12 17:30:41.306  INFO 9388 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@a1e932e: startup date [Thu Jun 12 17:30:41 CEST 2014]; root of context hierarchy
2014-06-12 17:30:42.058  INFO 9388 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'org.springframework.boot.autoconfigure.AutoConfigurationPackages': replacing [Generic bean: class [org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null] with [Generic bean: class [org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null]
2014-06-12 17:30:42.866  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$fd7a7b18] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:42.900  INFO 9388 --- [           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)
2014-06-12 17:30:42.915  INFO 9388 --- [           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)
2014-06-12 17:30:42.920  INFO 9388 --- [           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)
2014-06-12 17:30:43.164  INFO 9388 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-06-12 17:30:43.403  INFO 9388 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-06-12 17:30:43.403  INFO 9388 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.52
2014-06-12 17:30:43.582  INFO 9388 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-06-12 17:30:43.582  INFO 9388 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2279 ms
2014-06-12 17:30:44.117  INFO 9388 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-06-12 17:30:44.119  INFO 9388 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-06-12 17:30:44.662  INFO 9388 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2014-06-12 17:30:44.707  INFO 9388 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2014-06-12 17:30:44.839  INFO 9388 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.1.Final}
2014-06-12 17:30:44.842  INFO 9388 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2014-06-12 17:30:44.844  INFO 9388 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2014-06-12 17:30:45.189  INFO 9388 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
2014-06-12 17:30:45.616  INFO 9388 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2014-06-12 17:30:45.783  INFO 9388 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2014-06-12 17:30:46.729  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:46.825  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllMedecins],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins()
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getMedecinById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCreneauById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getClientById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getRvById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllClients],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllClients()
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/ajouterRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv)
2014-06-12 17:30:46.828  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/supprimerRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv)
2014-06-12 17:30:46.851  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] naar handler van het type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:46.851  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler van het type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:47.131  INFO 9388 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-12 17:30:47.169  INFO 9388 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-12 17:30:47.170  INFO 9388 --- [           main] rdvmedecins.web.boot.Boot                : Started Boot in 6.302 seconds (JVM running for 6.906)
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-06-12 17:30:55.538  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms
  • regel 17: de Tomcat-server start op;
  • regels 23-31: de [métier, DAO, JPA]-lagen worden geïnitialiseerd;
  • regel 34: de methode die URL [/getRvMedecinJour/{idMedecin}/{jour}] verwerkt, is gedetecteerd. Dit proces van het detecteren van de methoden van de controller herhaalt zich tot regel 44;
  • regel 52: de Spring-servlet MVC [DispatcherServlet] is klaar om verzoeken van webclients te beantwoorden;

We hebben nu een werkende webservice die via een webclient kan worden geraadpleegd. We gaan nu kijken naar het beveiligen van deze service: we willen dat alleen bepaalde personen de afspraken van de artsen kunnen beheren. Hiervoor gaan we het Spring Security-framework gebruiken, een onderdeel van het Spring-ecosysteem.

2.13. Inleiding tot Spring Security

We gaan opnieuw een Spring-gids importeren door de onderstaande stappen 1 tot en met 3 te volgen:

  

Het project bestaat uit de volgende onderdelen:

  • in de map [templates] bevinden zich de pagina's HTML van het project;
  • [Application]: is de uitvoerbare klasse van het project;
  • [MvcConfig]: is de configuratieklasse van Spring MVC;
  • [WebSecurityConfig]: is de configuratieklasse voor Spring Security;

2.13.1. Maven-configuratie

Het project [3] is een Maven-project. Laten we het bestand [pom.xml] bekijken om de afhankelijkheden te zien:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
</dependencies>
  • regels 1-5: het project is een Spring Boot-project;
  • regels 8-11: afhankelijkheid van het framework [Thymeleaf] waarmee dynamische HTML-pagina's kunnen worden gebouwd. Dit framework kan de JSP-pagina’s (Java Server Pages) vervangen, die tot voor kort standaard werden gebruikt, evenals het Spring-viewframework MVC;
  • regels 12-15: afhankelijkheid van het Spring Security-framework;

2.13.2. De Thymeleaf-weergaven

  

De weergave [home.html] ziet er als volgt uit:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • De attributen [th:xx] zijn Thymeleaf-attributen. Ze worden door Thymeleaf geïnterpreteerd voordat de pagina HTML naar de klant wordt verzonden. De klant ziet ze niet;
  • regel 12: het attribuut [th:href="@{/hello}"] genereert het attribuut [href] van de tag <a>. De waarde [@{/hello}] genereert het pad [<context>/hello], waarbij [context] de context van de webapplicatie is;

De gegenereerde code HTML is als volgt:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>
    <p>
        Click <a href="/hello">here</a> to see a greeting.
    </p>
</body>
</html>
  • regel 10: de context van de applicatie is de root /;

De weergave [hello.html] is als volgt:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • regel 9: Het attribuut [th:inline="text"] genereert de tekst van de tag <h1>. Deze tekst bevat een $-uitdrukking die moet worden geëvalueerd. Het element [[${#httpServletRequest.remoteUser}]] is de waarde van het attribuut [RemoteUser] van de huidige aanvraag HTTP. Dit is de naam van de aangemelde gebruiker;
  • regel 10: een formulier HTML. Het attribuut [th:action="@{/logout}"] genereert het attribuut [action] van de tag [form]. De waarde [@{/logout}] genereert het pad [<context>/logout], waarbij [context] de context van de webapplicatie is;

De gegenereerde code HTML is als volgt:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1>Hello user!</h1>
    <form method="post" action="/logout">
        <input type="submit" value="Sign Out" />
    <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
</body>
</html>
  • regel 8: de vertaling van Hello [[${#httpServletRequest.remoteUser}]]!;
  • regel 9: de vertaling van @{/logout};
  • regel 11: een verborgen veld met de naam (attribuut name) _csrf;

De laatste weergave [login.html] is als volgt:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • regel 9: het attribuut [th:if="${param.error}"] zorgt ervoor dat de tag <div> alleen wordt gegenereerd als de URL die de inlogpagina weergeeft, de parameter [error] (http://context/login?error) bevat;
  • regel 10: het attribuut [th:if="${param.logout}"] zorgt ervoor dat de tag <div> alleen wordt gegenereerd als de URL, die de inlogpagina weergeeft, de parameter [logout] (http://context/login?logout) bevat;
  • regels 11-23: een formulier HTML;
  • regel 11: het formulier wordt verzonden naar URL [<context>/login], waarbij <context> de context van de webapplicatie is;
  • regel 13: een invoerveld met de naam [username];
  • regel 17: een invoerveld met de naam [password];

De gegenereerde code HTML is als volgt:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>

    <form method="post" action="/login">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
</body>
</html>

Op regel 21 is te zien dat Thymeleaf een verborgen veld met de naam [_csrf] heeft toegevoegd.

2.13.3. Spring-configuratie MVC

  

De klasse [MvcConfig] configureert het Spring-framework MVC:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
  • regel 7: de annotatie [@Configuration] maakt van de klasse [MvcConfig] een configuratieklasse;
  • regel 8: de klasse [MvcConfig] breidt de klasse [WebMvcConfigurerAdapter] uit om bepaalde methoden ervan opnieuw te definiëren;
  • regel 10: herdefinitie van een methode van de bovenliggende klasse;
  • regels 11-16: met de methode [addViewControllers] kunnen URL worden gekoppeld aan weergaven HTML. De volgende koppelingen worden hier gemaakt:
URL
weergave
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

De extensie [html] en de map [templates] zijn de standaardwaarden die door Thymeleaf worden gebruikt. Deze kunnen via de configuratie worden gewijzigd. De map [templates] moet zich in de root van het classpath van het project bevinden:

Boven [1] zijn de mappen [main] en [resources] beide bronmappen (source folders). Dit betekent dat hun inhoud zich in de root van het classpath van het project bevindt. Dus in [2] bevinden de mappen [hello] en [templates] zich in de root van het classpath.

2.13.4. Configuratie van Spring Security

  

De klasse [WebSecurityConfig] configureert het Spring Security-framework:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • regel 9: de annotatie [@Configuration] maakt van de klasse [WebSecurityConfig] een configuratieklasse;
  • regel 10: de annotatie [@EnableWebSecurity] maakt van de klasse [WebSecurityConfig] een Spring Security-configuratieklasse;
  • regel 11: de klasse [WebSecurity] breidt de klasse [WebSecurityConfigurerAdapter] uit om bepaalde methoden ervan opnieuw te definiëren;
  • regel 12: herdefinitie van een methode van de bovenliggende klasse;
  • regels 13-16: de methode [configure(HttpSecurity http)] wordt opnieuw gedefinieerd om de toegangsrechten voor de verschillende URL-klassen van de applicatie vast te leggen;
  • regel 14: met de methode [http.authorizeRequests()] kunnen URL'en aan toegangsrechten worden gekoppeld. Hierin worden de volgende koppelingen gemaakt:
URL
regel
code
/, /home
toegang zonder authenticatie

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
alleen toegang na authenticatie
http.anyRequest().authenticated();
  • regel 15: definieert de authenticatiemethode. De authenticatie gebeurt via een formulier dat voor iedereen toegankelijk is: URL [/login] [http.formLogin().loginPage("/login").permitAll()]. Ook het uitloggen (logout) is voor iedereen toegankelijk.
  • regels 19-21: herdefiniëren de methode [configure(AuthenticationManagerBuilder auth)] die de gebruikers beheert;
  • regel 20: de authenticatie vindt plaats met ‘vast’ gedefinieerde gebruikers [auth.inMemoryAuthentication()]. Een gebruiker wordt hier gedefinieerd met de login [user], het wachtwoord [password] en de rol [USER]. Aan gebruikers met dezelfde rol kunnen dezelfde rechten worden toegekend;

2.13.5. Uitvoerbare klasse

  

De klasse [Application] ziet er als volgt uit:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }

}
  • regel 8: de annotatie [@EnableAutoConfiguration] vraagt Spring Boot (regel 3) om de configuratie uit te voeren die de ontwikkelaar niet expliciet heeft uitgevoerd;
  • regel 9: maakt van de klasse [Application] een Spring-configuratieklasse;
  • regel 10: vraagt om de map van de klasse [Application] te scannen op zoek naar Spring-componenten. De twee klassen [MvcConfig] en [WebSecurityConfig] worden op deze manier gedetecteerd omdat ze de annotatie [@Configuration] hebben;
  • regel 13: de methode [main] van de uitvoerbare klasse;
  • regel 14: de statische methode [SpringApplication.run] wordt uitgevoerd met de configuratieklasse [Application] als parameter. We zijn dit proces al eerder tegengekomen en weten dat de Tomcat-server die in de Maven-afhankelijkheden van het project is opgenomen, zal worden gestart en dat het project daarop zal worden geïmplementeerd. We hebben gezien dat vier URL-bestanden werden beheerd door [/, /home, /login, /hello] en dat sommige werden beschermd door toegangsrechten.

2.13.6. Testen van de applicatie

Laten we beginnen met het opvragen van de URL [/], een van de vier geaccepteerde URL-bestanden. Deze is gekoppeld aan de weergave [/templates/home.html]:

 

De aangevraagde URL [/] is voor iedereen toegankelijk. Daarom hebben we deze verkregen. De link [here] is als volgt:

Click <a href="/hello">here</a> to see a greeting.

De URL [/hello] wordt opgevraagd wanneer je op de link klikt. Deze is beveiligd:

URL
regel
code
/, /home
toegang zonder authenticatie

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
alleen toegang na authenticatie
http.anyRequest().authenticated();

Je moet geauthenticeerd zijn om deze te verkrijgen. Spring Security zal de browser van de klant dan doorverwijzen naar de authenticatiepagina. Volgens de getoonde configuratie is dit de pagina URL [/login]. Deze is voor iedereen toegankelijk:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

We krijgen dus [1]:

De broncode van de verkregen pagina is als volgt:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • op regel 7 verschijnt een verborgen veld dat niet in de oorspronkelijke pagina [login.html] voorkomt. Dit is door Thymeleaf toegevoegd. Deze code, genaamd CSRF (Cross Site Request Forgery), is bedoeld om een beveiligingslek te verhelpen. Dit token moet samen met de authenticatie naar Spring Security worden teruggestuurd, zodat deze wordt geaccepteerd;

We herinneren ons dat alleen de gebruiker user/password door Spring Security wordt herkend. Als we iets anders invoeren in [2], krijgen we dezelfde pagina te zien met een foutmelding in [3]. Spring Security heeft de browser doorgestuurd naar URL [http://localhost:8080/login?error]. De aanwezigheid van de parameter [error] heeft ervoor gezorgd dat de tag werd weergegeven:


<div th:if="${param.error}">Invalid username and password.</div>

Laten we nu de verwachte waarden voor gebruikersnaam/wachtwoord [4] invoeren:

  • in [4], loggen we in;
  • bij [5] leidt Spring Security ons om naar URL en [/hello], omdat dit de URL is die we opvroegen toen we werden doorgestuurd naar de inlogpagina. De identiteit van de gebruiker werd weergegeven in de volgende regel van [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

De pagina [5] toont het volgende formulier:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

Wanneer u op de knop [Sign Out] klikt, wordt er een POST uitgevoerd op de URL [/logout]. Deze is, net als de URL en [/login], voor iedereen toegankelijk:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

In onze koppeling URL / views hebben we niets gedefinieerd voor de URL en [/logout]. Wat gaat er gebeuren? Laten we het eens proberen:

  • in [6] klikken we op de knop [Sign Out];
  • in [7] zien we dat we zijn doorgestuurd naar de URL [http://localhost:8080/login?logout]. Het is Spring Security die om deze omleiding heeft gevraagd. Door de aanwezigheid van de parameter [logout] in URL werd de volgende regel in de weergave getoond:

<div th:if="${param.logout}">You have been logged out.</div>

2.13.7. Conclusie

In het vorige voorbeeld hadden we eerst de webapplicatie kunnen schrijven en deze daarna kunnen beveiligen. Spring Security is niet ingrijpend. Het is mogelijk om de beveiliging van een reeds geschreven webapplicatie in te stellen. Daarnaast hebben we het volgende ontdekt:

  • het is mogelijk om een authenticatiepagina te definiëren;
  • de authenticatie moet vergezeld gaan van het door Spring Security verstrekte token CSRF;
  • als de authenticatie mislukt, wordt men doorgestuurd naar de authenticatiepagina, met bovendien een parameter ‘error’ in het token URL;
  • als de authenticatie slaagt, wordt men doorgestuurd naar de pagina die werd opgevraagd op het moment dat de authenticatie plaatsvond. Als de authenticatiepagina rechtstreeks wordt opgevraagd zonder tussenpagina, dan leidt Spring Security ons om naar de URL [/] (dit geval is niet behandeld);
  • je meldt je af door de pagina URL [/logout] op te vragen met een POST. Spring Security leidt ons vervolgens door naar de authenticatiepagina met de parameter logout in de URL;

Al deze conclusies zijn gebaseerd op het standaardgedrag van Spring Security. Dit gedrag kan via de configuratie worden gewijzigd door bepaalde methoden van de klasse [WebSecurityConfigurerAdapter] opnieuw te definiëren.

De vorige tutorial zal ons verderop weinig helpen. We gaan namelijk gebruikmaken van:

  • een database om gebruikers, hun wachtwoorden en hun rollen op te slaan;
  • authenticatie via headers HTTP;

Er zijn vrij weinig tutorials te vinden voor wat we hier willen doen. De oplossing die wordt voorgesteld, is een samenvoeging van code die hier en daar is gevonden.

2.14. Beveiliging instellen voor de webservice voor afspraken

2.14.1. De database

De database [rdvmedecins] wordt aangepast om rekening te houden met gebruikers, hun wachtwoorden en hun rollen. Er komen drie nieuwe tabellen bij:

Image

Tabel [USERS]: de gebruikers

  • ID: primaire sleutel;
  • VERSION: versiekolom van de rij;
  • IDENTITY: een beschrijvende identiteit van de gebruiker;
  • LOGIN: de gebruikersnaam van de gebruiker;
  • PASSWORD: het wachtwoord van de gebruiker;

In de tabel USERS worden wachtwoorden niet in leesbare vorm opgeslagen:

 

Het algoritme dat de wachtwoorden versleutelt, is het algoritme BCRYPT.

Tabel [ROLES]: de rollen

  • ID: primaire sleutel;
  • VERSION: versiekolom van de rij;
  • NAME: rolnaam. Standaard verwacht Spring Security namen in de vorm ROLE_XX, bijvoorbeeld ROLE_ADMIN of ROLE_GUEST;
 

Tabel [USERS_ROLES]: koppelingstabel USERS / ROLES

Een gebruiker kan meerdere rollen hebben, en een rol kan meerdere gebruikers omvatten. Er is sprake van een veel-op-veel-relatie, die wordt weergegeven door de tabel [USERS_ROLES].

  • ID: primaire sleutel;
  • VERSION: versiekolom van de rij;
  • USER_ID: gebruikers-ID;
  • ROLE_ID: rol-ID;
 

Omdat we de database wijzigen, moeten alle lagen van het project [métier, DAO, JPA] worden aangepast:

2.14.2. Het nieuwe Eclipse-project [métier, DAO, JPA]

We dupliceren het oorspronkelijke project [rdvmedecins-metier-dao] naar [rdvmedecins-metier-dao-v2]:

  • naar [1]: het nieuwe project;
  • in [2]: de wijzigingen die voortvloeien uit de beveiligingsmaatregelen zijn gebundeld in één enkel pakket, [rdvmedecins.security]. Deze nieuwe elementen behoren tot de lagen [JPA] en [DAO], maar voor het gemak heb ik ze in één pakket samengebracht.

2.14.3. De nieuwe entiteiten [JPA]

De laag JPA definieert drie nieuwe entiteiten:

  

De klasse [User] is de afspiegeling van de tabel [USERS]:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // eigenschappen
    private String identity;
    private String login;
    private String password;

    // fabrikant
    public User() {
    }

    public User(String identity, String login, String password) {
        this.identity = identity;
        this.login = login;
        this.password = password;
    }

    // identiteit
    @Override
    public String toString() {
        return String.format("User[%s,%s,%s]", identity, login, password);
    }

    // getters en setters
....
}
  • regel 9: de klasse is een uitbreiding van de klasse [AbstractEntity] die al voor de andere entiteiten wordt gebruikt;
  • regels 13-15: er worden geen namen voor de kolommen opgegeven omdat ze dezelfde naam hebben als de velden waaraan ze zijn gekoppeld;

De klasse [Role] is een afspiegeling van de tabel [ROLES]:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    // eigenschappen
    private String name;

    // fabrikanten
    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    // identiteit
    @Override
    public String toString() {
        return String.format("Role[%s]", name);
    }

    // getters en setters
...
}

De klasse [UserRole] is de weergave van de tabel [USERS_ROLES]:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    // een UserRole verwijst naar een User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // een UserRole verwijst naar een rol
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // getters en setters
...
}
  • regels 15-17: geven de externe sleutel weer van de tabel [USERS_ROLES] naar de tabel [USERS];
  • regels 19-21: definiëren de vreemde sleutel van de tabel [USERS_ROLES] naar de tabel [ROLES];

2.14.4. Wijzigingen in de laag [DAO]

De laag [DAO] wordt uitgebreid met drie nieuwe [Repository]:

  

De interface [UserRepository] beheert de toegang tot de entiteiten [User]:


package rdvmedecins.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {

    // lijst met rollen van een gebruiker die wordt geïdentificeerd aan de hand van zijn id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lijst met rollen van een gebruiker die wordt geïdentificeerd aan de hand van zijn login en wachtwoord
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // zoeken naar een gebruiker op basis van zijn login
    User findUserByLogin(String login);
}
  • regel 9: de interface [UserRepository] breidt de interface [CrudRepository] van Spring Data uit (regel 4);
  • regels 12-13: met de methode [getRoles(User user)] kunnen alle rollen worden opgehaald van een gebruiker die is geïdentificeerd aan de hand van zijn [id]
  • regels 16-17: idem, maar dan voor een gebruiker die wordt geïdentificeerd aan de hand van zijn login en wachtwoord;

De interface [RoleRepository] beheert de toegang tot de entiteiten [Role]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // zoeken naar een rol op naam
    Role findRoleByName(String name);

}
  • regel 5: de interface [RoleRepository] breidt de interface [CrudRepository] uit;
  • regel 8: men kan een rol op naam zoeken;

De interface [userRoleRepository] beheert de toegang tot de entiteiten [UserRole]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • regel 5: de interface [UserRoleRepository] breidt alleen de interface [CrudRepository] uit zonder er nieuwe methoden aan toe te voegen;

2.14.5. De klassen voor het beheer van gebruikers en rollen

  

Spring Security vereist het aanmaken van een klasse die de volgende interface [UsersDetail] implementeert:

 

Deze interface wordt hier geïmplementeerd door de klasse [AppUserDetails]:


package rdvmedecins.security;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // eigenschappen
    private User user;
    private UserRepository userRepository;

    // constructors
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLogin();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // getters en setters
    ...
}
  • regel 10: de klasse [AppUserDetails] implementeert de interface [UserDetails];
  • regels 15-16: de klasse kapselt een gebruiker in (regel 15) en de repository waarmee de details van deze gebruiker kunnen worden opgehaald (regel 16);
  • regels 22-25: de constructor die de klasse instantiëert met een gebruiker en diens repository;
  • regels 28-35: implementatie van de methode [getAuthorities] van de interface [UserDetails]. Deze methode moet een verzameling elementen van het type [GrantedAuthority] of een afgeleid type samenstellen. Hier gebruiken we het afgeleide type [SimpleGrantedAuthority] (regel 32) dat de naam van een van de rollen van de gebruiker uit regel 15 omvat;
  • regels 31-33: we doorlopen de lijst met rollen van de gebruiker uit regel 15 om een lijst met elementen van het type [SimpleGrantedAuthority] samen te stellen;
  • regels 38-40: implementeren de methode [getPassword] van de interface [UserDetails]. Het wachtwoord van de gebruiker uit regel 15 wordt weergegeven;
  • regels 38-40: implementeren de methode [getUserName] van de interface [UserDetails]. De gebruikersnaam uit regel 15 wordt geretourneerd;
  • regels 47-50: het account van de gebruiker verloopt nooit;
  • regels 52-55: het account van de gebruiker wordt nooit geblokkeerd;
  • regels 57-60: de inloggegevens van de gebruiker verlopen nooit;
  • regels 62-65: het account van de gebruiker is altijd actief;

Spring Security vereist ook dat er een klasse bestaat die de interface [AppUserDetailsService] implementeert:

 

Deze interface wordt geïmplementeerd door de volgende klasse [AppUserDetails]:


package rdvmedecins.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // de gebruiker zoeken op basis van zijn login
        User user = userRepository.findUserByLogin(login);
        // gevonden?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // de gegevens van de gebruiker weergeven
        return new AppUserDetails(user, userRepository);
    }

}
  • regel 9: de klasse wordt een Spring-component en is dus beschikbaar in de context;
  • regels 12-13: de component [UserRepository] wordt hier geïnjecteerd;
  • regels 16-25: implementatie van de methode [loadUserByUsername] van de interface [UserDetailsService] (regel 10). De parameter is de gebruikersnaam;
  • regel 18: de gebruiker wordt opgezocht aan de hand van zijn gebruikersnaam;
  • regels 20-22: als de gebruiker niet wordt gevonden, wordt er een uitzondering gegenereerd;
  • regel 24: er wordt een object [AppUserDetails] aangemaakt en weergegeven. Dit is inderdaad van het type [UserDetails] (regel 16);

2.14.6. Tests van de laag [DAO]

  

Allereerst maken we een uitvoerbare klasse [CreateUser] die een gebruiker met een rol kan aanmaken:


package rdvmedecins.security;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;

public class CreateUser {

    public static void main(String[] args) {
        // syntaxis: login wachtwoord roleName

        // er zijn drie parameters nodig
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // de parameters worden opgehaald
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // Spring-context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // bestaat de rol al?
        Role role = roleRepository.findRoleByName(roleName);
        // als deze niet bestaat, maken we deze aan
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // Bestaat de gebruiker al?
        User user = userRepository.findUserByLogin(login);
        // als deze niet bestaat, maken we deze aan
        if (user == null) {
            // het wachtwoord wordt gehasht met bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // de gebruiker wordt opgeslagen
            user = userRepository.save(new User(login, login, crypt));
            // we leggen de koppeling met de rol aan
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // de gebruiker bestaat al – heeft hij de gevraagde rol?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // als deze niet wordt gevonden, wordt de koppeling met de rol aangemaakt
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // Spring-context afsluiten
        context.close();
    }

}
  • regel 17: de klasse verwacht drie argumenten die een gebruiker definiëren: zijn login, zijn wachtwoord en zijn rol;
  • regels 25-27: de drie parameters worden opgehaald;
  • regel 29: de Spring-context wordt opgebouwd op basis van de configuratieklasse [DomainAndPersistenceConfig]. Deze klasse bestond al in het vorige project. Ze moet als volgt worden aangepast:

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
  • regel 1: er moet worden aangegeven dat er nu [Repository]-componenten in het pakket [rdvmedecins.security] zitten;
  • regel 4: er moet worden aangegeven dat er nu entiteiten JPA in het pakket [rdvmedecins.security] zitten;

Laten we teruggaan naar de code voor het aanmaken van een gebruiker:

  • regels 30-32: we halen de referenties op van de drie [Repository] die we kunnen gebruiken om de gebruiker aan te maken;
  • regel 34: we controleren of de rol al bestaat;
  • regels 36-38: als dat niet het geval is, maken we deze aan in de database. De rol krijgt een naam in de vorm van [ROLE_XX];
  • regel 40: we controleren of de login al bestaat;
  • regels 42-49: als de login niet bestaat, wordt deze in de database aangemaakt;
  • regel 44: het wachtwoord wordt versleuteld. Hiervoor wordt de klasse [BCrypt] van Spring Security gebruikt (regel 4). We hebben dus de bibliotheken van dit framework nodig. Het bestand [pom.xml] bevat een nieuwe afhankelijkheid:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • regel 46: de gebruiker wordt in de database opgeslagen;
  • regel 48: evenals de relatie die hem aan zijn rol koppelt;
  • regels 51-57: in het geval dat de login al bestaat – wordt dan gekeken of de rol die we hem willen toewijzen al tussen zijn rollen voorkomt;
  • regel 59-61: als de gezochte rol niet is gevonden, wordt er een rij aangemaakt in de tabel [USERS_ROLES] om de gebruiker aan zijn rol te koppelen;
  • er is geen bescherming ingebouwd tegen mogelijke uitzonderingen. Dit is een ondersteunende klasse om snel een gebruiker met een rol aan te maken.

Wanneer de klasse wordt uitgevoerd met de argumenten [x x guest], krijgt men in de database de volgende resultaten:

Tabel [USERS]

Tabel
 

Tabel [ROLES]

 

Tabel [USERS_ROLES]

 

Laten we nu eens kijken naar de tweede klasse [UsersTest], die een test is van JUnit:

  

package rdvmedecins.security;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import rdvmedecins.config.DomainAndPersistenceConfig;

import com.google.common.collect.Lists;

@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    @Test
    public void findAllUsersWithTheirRoles() {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(user);
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }

    @Test
    public void findUserByLogin() {
        // de gebruiker wordt opgehaald: [admin]
        User user = userRepository.findUserByLogin("admin");
        // we controleren of zijn wachtwoord [admin] is
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // de rol van admin / admin wordt gecontroleerd
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // we halen de gebruiker [admin] op
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // we controleren of het wachtwoord [admin] is
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // de rol van admin / admin wordt gecontroleerd
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // hulpprogramma - toont de items in een verzameling
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • regels 27-34: visuele test. We geven alle gebruikers met hun rollen weer;
  • regels 36-46: we controleren of de gebruiker [admin] het wachtwoord [admin] en de rol [ROLE_ADMIN] heeft, met behulp van de repository [UserRepository];
  • regel 41: [admin] is het wachtwoord in leesbare tekst. In de database is het versleuteld volgens het algoritme BCrypt. Met de methode [ BCrypt.checkpw] kan worden gecontroleerd of het versleutelde wachtwoord in de database inderdaad overeenkomt met het wachtwoord in de database;
  • regels 48-59: er wordt gecontroleerd of de gebruiker [admin] het wachtwoord [admin] en de rol [ROLE_ADMIN] heeft, met behulp van de service [appUserDetailsService];

De tests worden met succes uitgevoerd met de volgende logbestanden:

User[guest,guest,$2a$10$Gzyp54mvkgMH0SPQkXo.Zeu.DvJ/Ql50PRXLf2FkolMTs7fr6A2J2]
Roles :
Role[ROLE_GUEST]
User[admin,admin,$2a$10$m79V6MKt9GPDdpjSulyqReqUioqYwXy8ollt/.ia15FhX2fym3AE6]
Roles :
Role[ROLE_ADMIN]
User[user,user,$2a$10$ph5y/1H89YC11oGVLB49fON.dZwnu44bAOKMK1FFl//xjAvsr/Ese]
Roles :
Role[ROLE_USER]
User[x,x,$2a$10$dAKd2SuQplR1iFhoBUUFs.XiA0lYxNqOmrkv97Gbr5KBoHzEi/5HG]
Roles :
Role[ROLE_GUEST]

2.14.7. Tussentijdse conclusie

De toevoeging van de benodigde klassen aan Spring Security kon worden uitgevoerd met slechts enkele aanpassingen aan het oorspronkelijke project. Laten we deze nog eens op een rijtje zetten:

  • toevoeging van een afhankelijkheid van Spring Security in het bestand [pom.xml];
  • het aanmaken van drie extra tabellen in de database;
  • het aanmaken van entiteiten JPA en Spring-componenten in het pakket [rdvmedecins.security];

Dit zeer gunstige scenario vloeit voort uit het feit dat de drie aan de database toegevoegde tabellen onafhankelijk zijn van de bestaande tabellen. We hadden ze zelfs in een aparte database kunnen plaatsen. Dit was mogelijk omdat we hadden besloten dat een gebruiker een bestaan had dat onafhankelijk was van artsen en klanten. Als deze laatste potentiële gebruikers waren geweest, zouden er koppelingen moeten worden gemaakt tussen de tabel [USERS] en de tabellen [MEDECINS] en [CLIENTS]. Dit zou dan een aanzienlijke impact hebben gehad op het bestaande project.

2.14.8. Het Eclipse-project van de laag [web]

Het vorige project [rdvmedecins-webapi] is gedupliceerd in het project [rdvmedecins-webapi-v2] [1]:

De enige wijzigingen moeten worden aangebracht in het pakket [rdvmedecins.web.config], waar Spring Security moet worden geconfigureerd. We zijn al een configuratieklasse van Spring Security tegengekomen:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

We volgen dezelfde aanpak:

  • regel 11: een klasse definiëren die de klasse [WebSecurityConfigurerAdapter] uitbreidt;
  • regel 13: definieer een methode [configure(HttpSecurity http)] die de toegangsrechten tot de verschillende URL van de webservice vastlegt;
  • regel 19: een methode [configure(AuthenticationManagerBuilder auth)] definiëren die de gebruikers en hun rollen vastlegt;

De configuratie van Spring Security wordt verzorgd door de klasse [SecurityConfig]:


package rdvmedecins.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import rdvmedecins.security.AppUserDetailsService;

@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // de authenticatie wordt uitgevoerd door de bean [appUserDetailsService]
        // het wachtwoord wordt versleuteld met het Bcrypt-hash-algoritme
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // het wachtwoord wordt verzonden via de header Authorization: Basic xxxx
        http.httpBasic();
        // alleen de rol ADMIN kan de applicatie gebruiken
        http.authorizeRequests() //
                .antMatchers("/", "/**") // alle URL
                .hasRole("ADMIN");
    }
}
  • regels 14-15: de annotaties uit het voorbeeld zijn overgenomen;
  • regels 17-18: de klasse [AppUserDetails], die gebruikers toegang geeft tot de applicatie, wordt geïnjecteerd;
  • regels 20-21: de methode [configure(HttpSecurity http)] definieert de gebruikers en hun rollen. Deze methode ontvangt als parameter een type [AuthenticationManagerBuilder]. Deze parameter wordt aangevuld met twee gegevens:
    • een verwijzing naar de service [appUserDetailsService] uit regel 18, die geregistreerde gebruikers toegang verleent. Hierbij moet worden opgemerkt dat niet wordt vermeld dat ze in een database zijn opgeslagen. Ze zouden dus in een cache kunnen staan, door een webservice kunnen worden geleverd, ...
    • het type versleuteling dat voor het wachtwoord wordt gebruikt. Ter herinnering: we hebben hier het algoritme BCrypt gebruikt;
  • regels 27-40: de methode [configure(HttpSecurity http)] definieert de toegangsrechten voor de URL van de webservice;
  • regel 30: in het inleidende project hebben we gezien dat Spring Security standaard een CSRF-token (Cross Site Request Forgery) beheert dat de gebruiker die zich wilde authenticeren, naar de server moest terugsturen. Hier is dit mechanisme uitgeschakeld;
  • regel 32: we schakelen de authenticatiemodus via de header HTTP in. De client moet de volgende header HTTP verzenden:
Authorization:Basic code

waarbij ‘code’ de Base64-codering is van de tekenreeks ‘login:password’. De Base64-codering van de tekenreeks admin:admin is bijvoorbeeld YWRtaW46YWRtaW4=. De gebruiker met de login [admin] en het wachtwoord [admin] zal dus de volgende header HTTP verzenden om zich te authenticeren:

Authorization:Basic YWRtaW46YWRtaW4=
  • regels 34-36: geven aan dat alle URL van de webservice toegankelijk zijn voor gebruikers met de rol [ROLE_ADMIN]. Dit betekent dat een gebruiker die deze rol niet heeft, geen toegang heeft tot de webservice;

De klasse [AppConfig], die de gehele applicatie configureert, verandert als volgt:

  

package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {

}
  • De wijziging vindt plaats op regel 11: er wordt aangegeven dat er nu twee configuratiebestanden moeten worden gebruikt: [DomainAndPersistenceConfig] en [SecurityConfig].

2.14.9. Testen van de webservice

We gaan de webservice testen met de Chrome-client [Advanced Rest Client]. We moeten de authenticatie-header HTTP specificeren:

Authorization:Basic code

waarbij [code] de Base64-code is van de tekenreeks [login:password]. Om deze code te genereren, kun je het volgende programma gebruiken:

  

package rdvmedecins.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // er worden twee argumenten verwacht: login en wachtwoord
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // de twee argumenten worden opgehaald
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // de tekenreeks wordt gecodeerd
        byte[] data = Base64.encode(chaîne.getBytes());
        // de Base64-codering wordt weergegeven
        System.out.println(new String(data));
    }

}

Als we dit programma uitvoeren met de twee argumenten [admin admin]:

  

krijgen we het volgende resultaat:

YWRtaW46YWRtaW4=

Nu we weten hoe we de authenticatie-header HTTP kunnen genereren, starten we de nu beveiligde webservice. Vervolgens vragen we met de Chrome-client [Advanced Rest Client] de lijst met alle artsen op:

  • in [1] vragen we de URL van de artsen op;
  • in [2], met een methode GET;
  • in [3] geven we de header HTTP van de authenticatie op. De code [YWRtaW46YWRtaW4=] is de Base64-codering van de tekenreeks [admin:admin];
  • in [4] verzenden we het commando HTTP;

Het antwoord van de server is als volgt:

  • in [1], de authenticatieheader HTTP;
  • in [2] stuurt de server een antwoord terug: JSON;
  • in [3], de lijst met artsen.

Laten we nu een verzoek HTTP proberen met een onjuiste authenticatieheader. Het antwoord is dan als volgt:

  • in [1] en [3]: de authenticatieheader HTTP;
  • in [2]: het antwoord van de webservice;

Laten we nu de gebruiker user / user proberen. Deze bestaat wel, maar heeft geen toegang tot de webservice. Als we het Base64-encoderingsprogramma uitvoeren met de twee argumenten [user user]:

  

krijgen we het volgende resultaat:

dXNlcjp1c2Vy
  • in [1] en [3]: de authenticatie-header HTTP;
  • in [2]: het antwoord van de webservice. Dit verschilt van het vorige, dat [401 Unauthorized] was. Deze keer heeft de gebruiker zich correct geauthenticeerd, maar beschikt hij niet over voldoende rechten om toegang te krijgen tot URL;

2.15. Conclusion

Laten we nog eens de algemene architectuur van onze client/server-applicatie bekijken:

Een beveiligde webservice is nu operationeel. We zullen zien dat deze moet worden aangepast vanwege problemen die aan het licht zullen komen bij het bouwen van de Angular-client JS. Maar we wachten tot het probleem zich voordoet om het op te lossen. We gaan nu de Angular-client bouwen die een webinterface biedt voor het beheren van de afspraken van artsen.