Skip to content

2. Serwer Spring 4

W powyższej architekturze przechodzimy teraz do tworzenia serwisu internetowego / JSON zbudowanego przy użyciu frameworka Spring 4. Napiszemy go w kilku etapach:

  • najpierw warstwy [métier] i [DAO] (Data Access Object). Wykorzystamy tu Spring Data;
  • następnie serwis internetowy JSON bez uwierzytelniania. Wykorzystamy tu Spring MVC;
  • następnie dodamy moduł uwierzytelniania przy użyciu Spring Security.

Zaczniemy od wyjaśnienia struktury bazy danych stanowiącej podstawę aplikacji.

2.1. Baza danych

Baza danych, zwana dalej [dbrdvmedecins] , to baza danych MySQL5 zawierająca następujące tabele:

  

Wizyty są zarządzane przez następujące tabele:

  • [medecins]: zawiera listę lekarzy w gabinecie;
  • [clients]: zawiera listę pacjentów przychodni;
  • [creneaux]: zawiera przedziały czasowe dla każdego z lekarzy;
  • [rv]: zawiera listę wizyt u lekarzy.

Tabele [roles], [users] i [users_roles] są tabelami związanymi z uwierzytelnianiem. Na razie nie będziemy się nimi zajmować.

Powiązania między tabelami zarządzającymi wizytami są następujące:

 
  • przedział czasowy należy do lekarza – jeden lekarz ma 0 lub więcej przedziałów czasowych;
  • wizyta łączy zarówno pacjenta, jak i lekarza poprzez przedział czasowy tego ostatniego;
  • klient ma 0 lub więcej wizyt;
  • z jednym przedziałem czasowym powiązana jest 0 lub więcej wizyt (w różnych dniach).

2.1.1. Tabela [MEDECINS]

Zawiera informacje o lekarzach zarządzanych przez aplikację [RdvMedecins].

  • ID: numer identyfikacyjny lekarza – klucz główny tabeli
  • VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
  • NOM: nazwisko lekarza
  • PRENOM: jego imię
  • TITRE: jego tytuł (panna, pani, pan)

2.1.2. Tabela [CLIENTS]

Pacjenci poszczególnych lekarzy są zapisani w tabeli [CLIENTS]:

  • ID: numer identyfikacyjny pacjenta – klucz główny tabeli
  • VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
  • NOM: nazwisko klienta
  • PRENOM: imię klienta
  • TITRE: tytuł (panna, pani, pan)

2.1.3. Tabela [CRENEAUX]

Zawiera listę przedziałów czasowych, w których możliwe jest użycie RV:

  • ID: numer identyfikujący przedział czasowy – klucz główny tabeli (wiersz 8)
  • VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
  • ID_MEDECIN: numer identyfikujący lekarza, do którego należy ten przedział czasowy – klucz obcy w kolumnie MEDECINS (ID).
  • HDEBUT: godzina rozpoczęcia terminu
  • MDEBUT: minuty początku przedziału czasowego
  • HFIN: godzina zakończenia przedziału czasowego
  • MFIN: minuty zakończenia przedziału czasowego

Drugi wiersz tabeli [CRENEAUX] (por. [1] powyżej) wskazuje na przykład, że przedział nr 2 rozpoczyna się o godz. 8:20 i kończy o godz. 8:40 oraz należy do lekarza nr 1 (pani Marie PELISSIER).

2.1.4. Tabela [RV]

Zawiera listę RV przypisanych każdemu lekarzowi:

  • ID: numer jednoznacznie identyfikujący RV – klucz główny
  • JOUR: dzień z RV
  • ID_CRENEAU: przedział czasowy z RV – klucz obcy w polu [ID] tabeli [CRENEAUX] – określa zarówno przedział czasowy, jak i odpowiedniego lekarza.
  • ID_CLIENT: numer klienta, dla którego dokonano rezerwacji – klucz obcy w polu [ID] tabeli [CLIENTS]

Ta tabela posiada , która wymusza unikalność wartości połączonych kolumn (JOUR, ID_CRENEAU):

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

Jeśli wiersz w tabeli [RV] ma wartość (JOUR1, ID_CRENEAU1) w kolumnach (JOUR, ID_CRENEAU), to wartość ta nie może występować nigdzie indziej. W przeciwnym razie oznaczałoby to, że dwa rekordy o wartości RV zostały zarejestrowane w tym samym czasie dla tego samego lekarza. Z punktu widzenia programowania w Javie sterownik bazy danych o wartości JDBC uruchamia sterownik o wartości SQLException, gdy wystąpi taka sytuacja.

Wiersz o wartości id równej 3 (por. [1] powyżej) oznacza, że rezerwacja o numerze RV została dokonana na przedział czasowy nr 20 dla klienta nr 4 w dniu 23.08.2006 r. Z tabeli [CRENEAUX] wynika, że przedział nr 20 odpowiada przedziałowi czasowemu 16:20–16:40 i należy do lekarza nr 1 (pani Marie PELISSIER). Z tabeli [CLIENTS] wynika, że klient nr 4 to panna Brigitte BISTROU.

2.2. Wprowadzenie do Spring Data

Zaimplementujemy warstwę [DAO] projektu przy użyciu Spring Data, gałęzi ekosystemu Spring.

Na stronie Springa znajduje się wiele samouczków dotyczących rozpoczęcia pracy ze Springiem [http://spring.io/guides]. Wykorzystamy jeden z nich, aby zapoznać się z Spring Data. W tym celu użyjemy Spring Tool Suite (STS).

  • w [1] importujemy jeden z samouczków z [spring.io/guides];
  • w [2] wybieramy samouczek [Accessing Data Jpa], który pokazuje, jak uzyskać dostęp do bazy danych za pomocą Spring Data;
  • w [3] wybieramy projekt skonfigurowany przez Maven;
  • w [4] samouczek może być udostępniony w dwóch formach: [initial], czyli pusta wersja, którą wypełnia się zgodnie z samouczkiem, lub [complete], czyli ostateczna wersja samouczka. Wybieramy tę drugą;
  • w pliku [5] można wybrać opcję wyświetlenia samouczka w przeglądarce;
  • w pliku [6] znajduje się gotowy projekt.

2.2.1. Konfiguracja projektu w Maven

Zależności Maven projektu są skonfigurowane w pliku [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>
        <!-- użyj UTF-8 do wszystkiego -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • wiersze 5–9: definiują nadrzędny projekt Maven. To właśnie on określa większość zależności projektu. Mogą one być wystarczające, w którym to przypadku nie dodaje się żadnych nowych, lub nie, w którym to przypadku dodaje się brakujące zależności;
  • wiersze 12–15: definiują zależność od pliku [spring-boot-starter-data-jpa]. Ten artefakt zawiera klasy Spring Data;
  • wiersze 16–19: definiują zależność od artefaktu SGBD oraz H2, który umożliwia tworzenie i zarządzanie bazami danych w pamięci.

Przyjrzyjmy się klasom dostarczanym przez te zależności:

Jest ich bardzo wiele:

  • niektóre należą do ekosystemu Spring (te zaczynające się od „spring”);
  • inne należą do ekosystemu Hibernate (hibernate, jboss), z którego implementacją JPA korzystamy tutaj;
  • inne to biblioteki testowe (junit, hamcrest);
  • inne to biblioteki logowania (log4j, logback, slf4j);

Zachowamy je wszystkie. W przypadku aplikacji produkcyjnej należałoby zachować tylko te, które są niezbędne.

W wierszu 26 pliku [pom.xml] znajduje się następujący wiersz:


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

Ten wiersz jest powiązany z następującymi wierszami:


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

W wierszach 6–9 wtyczka [spring-boot-maven-plugin] umożliwia wygenerowanie pliku jar zawierającego kod wykonywalny aplikacji. Wiersz 26 pliku [pom.xml] określa zatem klasę wykonywalną tego pliku jar.

2.2.2. Warstwa [JPA]

Dostęp do bazy danych odbywa się za pośrednictwem warstwy [JPA], Java Persistence API:

  

Aplikacja jest prosta i obsługuje klientów [Customer]. Klasa [Customer] należy do warstwy [JPA] i ma następującą postać:


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

}

Klient ma identyfikator [id], imię [firstName] i nazwisko [lastName]. Każda instancja [Customer] reprezentuje wiersz w tabeli bazy danych.

  • wiersz 8: adnotacja JPA, która powoduje, że trwałość instancji [Customer] (tworzenie, odczyt, aktualizacja, usunięcie) będzie zarządzana przez implementację JPA. Z zależności Maven wynika, że wykorzystywana jest implementacja JPA / Hibernate;
  • wiersze 11–12: adnotacje JPA, które powiązują pole [id] z kluczem głównym tabeli [Customer]. Wiersz 12 wskazuje, że implementacja JPA będzie korzystać z metody generowania klucza głównego właściwej dla używanego SGBD, w tym przypadku H2;

Nie ma innych adnotacji dotyczących JPA. W takim przypadku zostaną zastosowane wartości domyślne:

  • tabela o nazwie [Customer] będzie nosiła nazwę klasy, tj. [Customer];
  • kolumny tej tabeli będą nosiły nazwy pól klasy: [id, firstName, lastName], przy czym w nazwie kolumny tabeli nie jest brana pod uwagę wielkość liter;

Należy zauważyć, że w żadnym momencie nie podano nazwy używanej implementacji JPA.

2.2.3. Warstwa [DAO]

  

Klasa [CustomerRepository] implementuje warstwę [DAO]. Jej kod wygląda następująco:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Jest to zatem interfejs, a nie klasa (wiersz 7). Rozszerza on interfejs [CrudRepository], który jest interfejsem Spring Data (wiersz 5). Interfejs ten jest definiowany przez dwa typy: pierwszy to typ zarządzanych elementów, w tym przypadku typ [Customer], a drugi to typ klucza głównego zarządzanych elementów, w tym przypadku typ [Long]. Interfejs [CrudRepository] ma następującą postać:


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

Interfejs ten definiuje operacje CRUD (Create – Read – Update – Delete), które można wykonać na typie JPA T:

  • wiersz 8: metoda save pozwala na zapisanie encji T w bazie danych. Zapisuje ona encję przy użyciu klucza głównego nadanego jej przez SGBD. Pozwala również na aktualizację encji T identyfikowanej za pomocą jej klucza głównego id. Wybór jednej lub drugiej operacji zależy od wartości klucza głównego id: jeśli ma on wartość null, wykonywana jest operacja zapisania, w przeciwnym razie – operacja aktualizacji;
  • wiersz 10: to samo, ale dla listy encji;
  • wiersz 12: metoda findOne pozwala na odzyskanie encji T identyfikowanej przez jej klucz pierwotny id;
  • wiersz 22: metoda delete pozwala usunąć encję T identyfikowaną przez jej klucz pierwotny id;
  • wiersze 24–28: warianty metody [delete];
  • wiersz 16: metoda [findAll] pozwala odzyskać wszystkie zapisane encje T;
  • wiersz 18: to samo, ale ograniczone do encji, dla których przekazano listę identyfikatorów;

Wróćmy do interfejsu [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);
}
  • wiersz 9 pozwala odnaleźć obiekt [Customer] na podstawie jego nazwy [lastName];

I to wszystko, jeśli chodzi o warstwę [DAO]. Nie ma klasy implementującej poprzedni interfejs. Jest ona generowana w czasie wykonywania przez [Spring Data]. Metody interfejsu [CrudRepository] są implementowane automatycznie. W przypadku metod dodanych do interfejsu [CustomerRepository] sytuacja jest różna. Wróćmy do definicji [Customer]:


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

Metoda w wierszu 9 jest automatycznie implementowana przez [Spring Data], ponieważ odwołuje się do pola [lastName] (wiersz 3) z [Customer]. Gdy Spring Data napotka metodę [findBySomething] w interfejsie do zaimplementowania, implementuje ją za pomocą następującego zapytania JPQL (Java Persistence Query Language):

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

Typ T musi zatem posiadać pole o nazwie [something]. W ten sposób metoda

List<Customer> findByLastName(String lastName);

zostanie zaimplementowana za pomocą kodu podobnego do poniższego:

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

gdzie [em] oznacza kontekst trwałości JPA. Jest to możliwe tylko wtedy, gdy klasa [Customer] posiada pole o nazwie [lastName], co ma miejsce.

Podsumowując, w prostych przypadkach Spring Data pozwala nam zaimplementować warstwę [DAO] za pomocą prostego interfejsu.

2.2.4. Warstwa [console]

  

Klasa [Application] wygląda następująco:


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

        // zapisz kilku klientów
        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"));

        // pobierz wszystkich klientów
        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();

        // pobierz pojedynczego klienta za pomocą ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // pobierz klientów według nazwiska
        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();
    }

}
  • Wiersz 10: wskazuje, że klasa służy do konfiguracji Springa. Najnowsze wersje Springa można bowiem konfigurować w Javie, a nie w XML. Obie metody mogą być stosowane jednocześnie. W kodzie klasy opatrzonej adnotacją [Configuration] zazwyczaj znajdują się bean'y Springa, czyli definicje klas, które mają zostać zainicjowane. W tym przypadku nie zdefiniowano żadnego bean'a. Należy tutaj przypomnieć, że podczas pracy z SGBD należy zdefiniować różne bean'y Springa:
    • plik [EntityManagerFactory], który określa implementację JPA, z której należy korzystać,
    • plik [DataSource], który określa źródło danych, z którego należy korzystać,
    • [TransactionManager], który definiuje menedżera transakcji, z którego należy korzystać;

W tym przypadku żadna z tych fasoli nie jest zdefiniowana.

  • w wierszu 11: adnotacja [EnableAutoConfiguration] pochodzi z projektu [Spring Boot] (wiersze 5–6). Ta adnotacja nakazuje Spring Boot, za pośrednictwem klasy [SpringApplication] (wiersz 16), skonfigurowanie aplikacji zgodnie z bibliotekami znalezionymi w jej ścieżce Classpath. Ponieważ biblioteki Hibernate znajdują się w ścieżce Classpath, bean [entityManagerFactory] zostanie zaimplementowany z wykorzystaniem Hibernate. Ponieważ biblioteka SGBD H2 znajduje się w ścieżce Classpath, bean [dataSource] zostanie zaimplementowany przy użyciu H2. W beanie [dataSource] należy również zdefiniować użytkownika i jego hasło. W tym przypadku Spring Boot użyje domyślnego administratora z klasy H2, który nie ma hasła. Ponieważ biblioteka [spring-tx] znajduje się w ścieżce Classpath, zostanie użyty menedżer transakcji Springa.

Ponadto katalog, w którym znajduje się klasa [Application], zostanie przeszukany w poszukiwaniu beanów rozpoznawanych niejawnie przez Spring lub zdefiniowanych jawnie za pomocą adnotacji Spring. W ten sposób zostaną sprawdzone klasy [Customer] i [CustomerRepository]. Ponieważ pierwsza z nich posiada adnotację [@Entity], zostanie skatalogowana jako encja zarządzana przez Hibernate. Ponieważ druga rozszerza interfejs [CrudRepository], zostanie zarejestrowana jako bean Spring.

Przyjrzyjmy się wierszom 16–17 kodu:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
  • wiersz 1: wykonywana jest metoda statyczna [run] klasy [SpringApplication] z projektu Spring Boot. Jej parametrem jest klasa posiadająca adnotację [Configuration] lub [EnableAutoConfiguration]. Następnie nastąpi wszystko, co zostało wyjaśnione wcześniej. Wynikiem jest kontekst aplikacji Spring, czyli zbiór beanów zarządzanych przez Spring;
  • wiersz 17: zwracamy się do tego kontekstu Springa o bean implementujący interfejs [CustomerRepository]. Pobieramy tutaj klasę wygenerowaną przez Spring Data w celu implementacji tego interfejsu.

Kolejne operacje polegają jedynie na wykorzystaniu metod bean’a implementującego interfejs [CustomerRepository]. Należy zwrócić uwagę, że w wierszu 50 kontekst zostaje zamknięty. Wyniki wyświetlane w konsoli są następujące:

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

2014-06-05 16:23:13.877  INFO 11664 --- [           main] hello.Application                        : Uruchamianie aplikacji na Gportpers3 z PID 11664 (D:\Temp\wksSTS\gs-accessing-data-jpa-complete\target\classes uruchomione przez ST w D:\Temp\wksSTS\gs-accessing-data-jpa-complete)
2014-06-05 16:23:13.936  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext: Odświeżanie org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: data uruchomienia [Thu Jun 05 16:23:13 CEST 2014]; korzeń hierarchii kontekstowej
2014-06-05 16:23:15.424  INFO 11664 --– [           main] j.LocalContainerEntityManagerFactoryBean: Tworzenie kontenera JPA o nazwie EntityManagerFactory dla jednostki trwałości „default”
2014-06-05 16:23:15.518  INFO 11664 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Przetwarzanie 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 nie znaleziono
2014-06-05 16:23:15.694  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Nazwa dostawcy kodu bajtowego: 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: Korzystanie z dialektu: org.hibernate.dialect.H2Dialect
2014-06-05 16:23:16.300  INFO 11664 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Korzystanie z ASTQueryTranslatorFactory
2014-06-05 16:23:16.613  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Uruchamianie eksportu schematu hbm2ddl
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: Schemat eksportu zakończony
2014-06-05 16:23:17.074  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Rejestrowanie komponentów typu bean w celu udostępnienia JMX podczas uruchamiania
2014-06-05 16:23:17.094  INFO 11664 --- [           main] hello.Application                        : Uruchomiono aplikację w 3,906 sekundy (JVM działa od 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 : Zamknięcie org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: data uruchomienia [Thu Jun 05 16:23:13 CEST 2014]; korzeń hierarchii kontekstowej
2014-06-05 16:23:17.332  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Wyrejestrowywanie komponentów udostępnionych przez JMX podczas wyłączania
2014-06-05 16:23:17.333  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : Zamykanie JPA EntityManagerFactory dla jednostki trwałości „default”
2014-06-05 16:23:17.334  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Uruchamianie eksportu schematu hbm2ddl
Hibernate: drop table customer if exists
2014-06-05 16:23:17.336  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schemat eksportu zakończony
  • wiersze 1–8: logo projektu Spring Boot;
  • wiersz 9: uruchamiana jest klasa [hello.Application];
  • wiersz 10: [AnnotationConfigApplicationContext] to klasa implementująca interfejs [ApplicationContext] biblioteki Spring. Jest to kontener beanów;
  • wiersz 11: bean [entityManagerFactory] jest zaimplementowany za pomocą klasy [LocalContainerEntityManagerFactory], klasy biblioteki Spring;
  • wiersz 12: pojawia się [hibernate]. Wybrano właśnie tę implementację JPA;
  • wiersz 19: dialektem Hibernate jest wariant SQL, który ma być używany wraz z SGBD. W tym przypadku dialekt [H2Dialect] wskazuje, że Hibernate będzie współpracować z SGBD i H2;
  • wiersze 22–24: tworzona jest tabela [CUSTOMER]. Oznacza to, że Hibernate został skonfigurowany do generowania tabel na podstawie definicji JPA, w tym przypadku definicji JPA klasy [Customer];
  • wiersze 27–32: logi Hibernate pokazujące wstawianie wierszy do tabeli [CUSTOMER]. Oznacza to, że Hibernate został skonfigurowany do generowania logów;
  • wiersze 35–39: pięciu wstawionych klientów;
  • wiersze 42–44: wynik metody [findOne] z interfejsu;
  • wiersze 47–50: wyniki metody [findByLastName];
  • wiersze 51 i kolejne: logi zamknięcia kontekstu Spring.

2.2.5. Ręczna konfiguracja projektu Spring Data

Duplikujemy poprzedni projekt, tworząc projekt [gs-accessing-data-jpa-2]:

  

W tym nowym projekcie nie będziemy polegać na automatycznej konfiguracji dokonywanej przez Spring Boot. Zrobimy to ręcznie. Może to być przydatne, jeśli domyślne ustawienia nam nie odpowiadają.

Najpierw określimy niezbędne zależności w pliku [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>
        <!-- Transakcje Spring -->
        <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 Baza danych -->
        <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>
  • wiersze 3–17: podstawowe biblioteki Springa;
  • wiersze 19–28: biblioteki Spring do obsługi transakcji z bazą danych;
  • wiersze 30–34: Spring Data służący do dostępu do bazy danych;
  • wiersze 36–40: Spring Boot do uruchamiania aplikacji;
  • wiersze 48–52: SGBD H2;
  • wiersze 54–63: bazy danych są często używane z pulami otwartych połączeń, co pozwala uniknąć ciągłego otwierania i zamykania połączeń. W tym przypadku zastosowano implementację z pliku [commons-dbcp];

Również w pliku [pom.xml] zmienia się nazwę klasy wykonywalnej:


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

W nowym projekcie jednostka [Customer] oraz interfejs [CustomerRepository] pozostają bez zmian. Zmienimy klasę [Application], która zostanie podzielona na dwie klasy:

  • [Config], która będzie klasą konfiguracyjną:
  • [Main], która będzie klasą wykonalną;
  

Klasa wykonywalna [Main] jest taka sama jak poprzednio, bez adnotacji konfiguracyjnych:


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

}
  • wiersz 12: klasa [Main] nie zawiera już adnotacji konfiguracyjnych;
  • wiersz 16: aplikacja jest uruchamiana za pomocą Spring Boot. Parametr [Config.class] to nowa klasa konfiguracyjna projektu;

Klasa [Config], która konfiguruje projekt, wygląda następująco:


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 {
    // źródło danych 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;
    }

    // dostawca 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();
    }

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

}
  • wiersz 22: adnotacja [@Configuration] sprawia, że klasa [Config] staje się klasą konfiguracyjną Spring;
  • wiersz 21: adnotacja [@EnableJpaRepositories] pozwala wskazać katalogi, w których znajdują się interfejsy Spring Data [CrudRepository]. Interfejsy te staną się komponentami Spring i będą dostępne w jego kontekście;
  • wiersz 20: adnotacja [@EnableTransactionManagement] wskazuje, że metody interfejsów [CrudRepository] muszą być wykonywane w ramach transakcji;
  • wiersz 19: adnotacja [@EntityScan] pozwala określić katalogi, w których należy szukać encji JPA. W tym przypadku została ona skomentowana, ponieważ informacja ta została podana wyraźnie w wierszu 50. Adnotacja ta powinna być obecna, jeśli stosowany jest tryb [@EnableAutoConfiguration], a elementy JPA nie znajdują się w tym samym katalogu co klasa konfiguracyjna;
  • wiersz 18: adnotacja [@ComponentScan] pozwala na wyliczenie folderów, w których należy szukać komponentów Spring. Komponenty Spring to klasy oznaczone adnotacjami Spring, takimi jak @Service, @Component, @Controller itp. W tym przypadku nie ma innych komponentów poza tymi zdefiniowanymi w klasie [Config], dlatego adnotacja została skomentowana;
  • wiersze 25–33: definiują źródło danych, bazę danych H2. To właśnie adnotacja @Bean w wierszu 25 sprawia, że obiekt utworzony przez tę metodę staje się komponentem zarządzanym przez Spring. Nazwa metody może być tutaj dowolna. Musi ona jednak nosić nazwę [dataSource], jeśli metoda EntityManagerFactory z wiersza 47 nie istnieje i jest definiowana przez autokonfigurację;
  • wiersz 29: baza danych będzie nosić nazwę [demo] i zostanie wygenerowana w folderze projektu;
  • wiersze 36–43: definiują używaną implementację JPA, w tym przypadku implementację Hibernate. Nazwa metody może być dowolna;
  • wiersz 39: brak logów SQL;
  • wiersz 30: baza danych zostanie utworzona, jeśli jeszcze nie istnieje;
  • wiersze 46–54: definiują metodę EntityManagerFactory, która będzie zarządzać trwałością JPA. Metoda ta musi obowiązkowo nosić nazwę [entityManagerFactory];
  • wiersz 47: metoda otrzymuje dwa parametry o typach odpowiadających dwóm wcześniej zdefiniowanym beanom. Zostaną one następnie skonstruowane, a następnie wstrzyknięte przez Spring jako parametry metody;
  • wiersz 49: określa używaną implementację JPA;
  • wiersz 50: określa katalogi, w których znajdują się encje JPA;
  • wiersz 51: określa źródło danych, które ma być obsługiwane;
  • wiersze 57–62: menedżer transakcji. Metoda musi obowiązkowo nosić nazwę [transactionManager]. Jako parametr otrzymuje ona bean z wierszy 46–54;
  • wiersz 60: menedżer transakcji jest powiązany z EntityManagerFactory;

Powyższe metody można zdefiniować w dowolnej kolejności.

Uruchomienie projektu daje te same wyniki. W folderze projektu pojawia się nowy plik – plik bazy danych o nazwie H2:

  

Wreszcie można zrezygnować z Spring Boot. Tworzymy drugą klasę wykonalną o nazwie [Main2]:

  

Klasa [Main2] zawiera następujący kod:


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

}
  • wiersz 15: klasa konfiguracyjna [Config] jest teraz wykorzystywana przez klasę Spring [AnnotationConfigApplicationContext]. W wierszu 5 widać, że nie ma już zależności od Spring Boot.

Wykonanie daje takie same wyniki jak poprzednio.

2.2.6. Tworzenie archiwum wykonywalnego

Aby utworzyć archiwum wykonywalne projektu, można postępować w następujący sposób:

  • w [1]: tworzy się konfigurację wykonania;
  • w [2]: typu [Java Application]
  • w [3]: określa projekt do wykonania (należy użyć przycisku Browse);
  • w [4]: określa klasę do wykonania;
  • w [5]: nazwa konfiguracji uruchomienia – może być dowolna;
  • w [6]: eksportujemy projekt;
  • w [7]: w postaci archiwum wykonywalnego JAR;
  • w [8]: podaje ścieżkę i nazwę pliku wykonywalnego, który ma zostać utworzony;
  • w [9]: nazwa konfiguracji uruchomieniowej utworzonej w [5];

Po wykonaniu tych czynności otwieramy konsolę w folderze zawierającym archiwum pliku wykonywalnego:

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

Archiwum uruchamia się w następujący sposób:


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

W konsoli pojawiają się następujące wyniki:

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  więcej szczegółów.
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. Tworzenie nowego projektu Spring Data

Aby utworzyć szkielet projektu Spring Data, można postępować w następujący sposób:

  • w [1] tworzymy nowy projekt;
  • w [2]: typu [Spring Starter Project];
  • w [3] podajemy nazwę grupy projektu;
  • w pliku [4]: podajemy nazwę artefaktu (w tym przypadku pliku jar), który zostanie utworzony podczas kompilacji projektu;
  • w polu [5]: należy podać pakiet klasy wykonywalnej, która zostanie utworzona w projekcie;
  • w [6]: nazwa projektu w Eclipse – może być dowolna (nie musi być identyczna z [4]);
  • w polu [7]: należy wskazać, że zamierza się utworzyć projekt posiadający warstwę [JPA]. Niezbędne dla takiego projektu zależności zostaną wówczas uwzględnione w pliku [pom.xml];
  • w pliku [8]: utworzony projekt;

Plik [pom.xml] zawiera zależności niezbędne dla projektu JPA:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
        <relativePath/> <!-- wyszukaj element nadrzędny w repozytorium -->
    </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>
  • wiersze 9–12: zależności niezbędne dla pliku JPA – będą obejmować plik [Spring Data];
  • wiersze 13–17: zależności niezbędne dla testów JUnit zintegrowanych ze Springiem;

Klasa wykonywalna [Application] nie wykonuje żadnych czynności, ale jest wstępnie skonfigurowana:


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

Klasa testowa [ApplicationTests] nie wykonuje żadnych czynności, ale jest wstępnie skonfigurowana:


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() {
    }

}
  • wiersz 9: adnotacja [@SpringApplicationConfiguration] umożliwia wykorzystanie pliku konfiguracyjnego [Application]. Dzięki temu klasa testowa będzie korzystać ze wszystkich beanów zdefiniowanych w tym pliku;
  • wiersz 8: adnotacja [@RunWith] umożliwia integrację Springa z JUnit: klasa będzie mogła zostać uruchomiona jako test JUnit. [@RunWith] jest adnotacją JUnit (wiersz 4), podczas gdy klasa [SpringJUnit4ClassRunner] jest klasą Spring (wiersz 6);

Teraz, gdy mamy już szkielet aplikacji JPA, możemy go uzupełnić, aby napisać projekt warstwy trwałości serwera naszej aplikacji do zarządzania terminami.

2.3. Projekt serwera w Eclipse

  

Główne elementy projektu to:

  • [pom.xml]: plik konfiguracyjny Maven projektu;
  • [rdvmedecins.entities]: encje JPA;
  • [rdvmedecins.repositories]: interfejsy Spring Data służące do dostępu do encji JPA;
  • [rdvmedecins.metier]: warstwa [métier];
  • [rdvmedecins.domain]: encje obsługiwane przez warstwę [métier];
  • [rdvmdecins.config]: klasy konfiguracyjne warstwy trwałości;
  • [rdvmedecins.boot]: prosta aplikacja konsolowa;

2.4. Konfiguracja Maven

Plik [pom.xml] projektu wygląda następująco:


<?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>
        <!-- użyj UTF-8 do wszystkiego -->
        <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>
  • wiersze 8–12: projekt opiera się na projekcie nadrzędnym [spring-boot-starter-parent]. W przypadku zależności już obecnych w projekcie nadrzędnym nie podaje się wersji. Zastosowana zostanie wersja zdefiniowana w projekcie nadrzędnym. Pozostałe zależności deklaruje się w standardowy sposób;
  • wiersze 14–17: dla Spring Data;
  • wiersze 18–22: dla testów JUnit;
  • wiersze 23–26: sterownik JDBC dla SGBD i MySQL5;
  • wiersze 27–34: pula połączeń Commons DBCP;
  • wiersze 35–38: biblioteka Jackson do zarządzania JSON;
  • wiersze 39–43: biblioteka Google do zarządzania zbiorami;

Wersja 1.1.0.RC1 biblioteki [spring-boot-starter-parent] wykorzystuje następujące wersje bibliotek:

<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. Elementy JPA

Entities JPA to obiekty, które będą zawierać wiersze tabel bazy danych.

  

Klasa [AbstractEntity] jest klasą nadrzędną dla encji [Personne, Creneau, Rv]. Jej definicja jest następująca:


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

    // inicjalizacja
    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;
    }

    // metody pobierające i ustawiające
    ..
}
  • wiersz 11: adnotacja [@MappedSuperclass] wskazuje, że klasa, do której odnosi się adnotacja, jest klasą nadrzędną dla encji JPA i [@Entity];
  • wiersze 15–17: definiują klucz podstawowy [id] dla każdej jednostki. To adnotacja [@Id] sprawia, że pole [id] staje się kluczem podstawowym. Adnotacja [@GeneratedValue(strategy = GenerationType.AUTO)] wskazuje, że wartość tego klucza głównego jest generowana przez SGBD i że nie narzucono żadnego trybu generowania;
  • wiersze 18–19: definiują wersję każdej encji. Implementacja JPA będzie zwiększać ten numer wersji za każdym razem, gdy encja zostanie zmodyfikowana. Numer ten służy do zapobiegania jednoczesnej aktualizacji encji przez dwóch różnych użytkowników: dwaj użytkownicy U1 i U2 odczytują encję E o numerze wersji równym V1. U1 modyfikuje E i zapisuje tę zmianę w bazie danych: numer wersji zmienia się wówczas na V1+1. U2 z kolei modyfikuje E i zapisuje tę zmianę w bazie danych: otrzyma wyjątek, ponieważ posiada wersję (V1) inną niż ta w bazie danych (V1+1);
  • wiersze 29–33: metoda [build] pozwala zainicjować dwa pola obiektu [AbstractEntity]. Metoda ta zwraca odwołanie do tak zainicjowanej instancji [AbstractEntity];
  • wiersze 36–44: metoda [equals] klasy została przedefiniowana: dwa obiekty uznaje się za równe, jeśli mają tę samą nazwę klasy i ten sam identyfikator id;

Entyteta [Personne] jest klasą nadrzędną dla entytet [Medecin] i [Client]:


package rdvmedecins.entities;

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

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

    // konstruktor domyślny
    public Personne() {
    }

    // konstruktor z parametrami
    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);
    }

    // metody pobierające i ustawiające
    ...
}
  • wiersz 6: adnotacja [@MappedSuperclass] wskazuje, że klasa, której dotyczy adnotacja, jest klasą nadrzędną względem jednostek JPA i [@Entity];
  • wiersze 10–15: osoba posiada tytuł (Melle), imię (Jacqueline) oraz nazwisko (Tatou). Nie podano żadnych informacji dotyczących kolumn tabeli. Domyślnie będą one więc nosiły te same nazwy co pola;

Entyteta [Medecin] ma następujący wygląd:


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;

    // konstruktor domyślny
    public Medecin() {
    }

    // konstruktor z parametrami
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

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

}
  • wiersz 6: klasą jest encja JPA;
  • wiersz 7: powiązana z tabelą [MEDECINS] w bazie danych;
  • wiersz 8: encja [Medecin] wywodzi się z encji [Personne];

Dane lekarza można zainicjować w następujący sposób:

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

Jeśli dodatkowo chcemy przypisać mu identyfikator i wersję, możemy zapisać:

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

gdzie metoda [build] jest tą samą metodą, która została zdefiniowana w [AbstractEntity].

Entyteta [Client] ma następującą postać:


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;

    // konstruktor domyślny
    public Client() {
    }

    // konstruktor z parametrami
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    // tożsamość
    public String toString() {
        return String.format("Client[%s]", super.toString());
    }

}
  • wiersz 6: klasa jest encją JPA;
  • wiersz 7: powiązana z tabelą [CLIENTS] w bazie danych;
  • wiersz 8: encja [Client] wywodzi się z encji [Personne];

Entyteta [Creneau] ma następujący wygląd:


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;
    // charakterystyka terminu z RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // slot jest powiązany z lekarzem
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

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

    // domyślny konstruktor
    public Creneau() {
    }

    // konstruktor z parametrami
    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);
    }

    // klucz obcy
    public long getIdMedecin() {
        return idMedecin;
    }

    // metody ustawiające i pobierające
    ...
}
  • wiersz 10: klasa jest encją JPA;
  • wiersz 11: powiązana z tabelą [CRENEAUX] w bazie danych;
  • wiersz 12: encja [Creneau] wywodzi się z encji [AbstractEntity] i w związku z tym dziedziczy identyfikator [id] oraz wersję [version];
  • wiersz 16: godzina rozpoczęcia przedziału czasowego (14);
  • wiersz 17: minuty rozpoczęcia przedziału czasowego (20);
  • wiersz 18: godzina zakończenia przedziału czasowego (14);
  • wiersz 19: minuty zakończenia przedziału czasowego (40);
  • wiersze 22–24: lekarz, do którego należy termin. Tabela [CRENEAUX] posiada klucz obcy do tabeli [MEDECINS]. Relacja ta jest odzwierciedlona w wierszach 22–24;
  • wiersz 22: adnotacja [@ManyToOne] wskazuje na relację „wiele (przedziałów czasowych) do jednego (lekarza)”. Atrybut [fetch=FetchType.LAZY] wskazuje, że gdy żądamy encji [Creneau] z kontekstu trwałości i musi ona zostać wyszukana w bazie danych, wówczas encja [Medecin] nie jest zwracana wraz z nią. Zaletą tego trybu jest to, że encja [Medecin] jest wyszukiwana tylko wtedy, gdy programista tego zażąda. Pozwala to zaoszczędzić pamięć i zwiększyć wydajność;
  • wiersz 23: wskazuje nazwę kolumny klucza obcego w tabeli [CRENEAUX];
  • wiersze 27–28: klucz obcy w tabeli [MEDECINS];
  • wiersz 27: kolumna [ID_MEDECIN] została już wykorzystana w wierszu 23. Oznacza to, że może być modyfikowana na dwa różne sposoby, czego nie dopuszcza standard JPA. Dodajemy zatem atrybuty [insertable = false, updatable = false], co sprawia, że kolumna ta może być jedynie odczytywana;

Entyteta [Rv] ma następującą postać:


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;

    // cechy obiektu Rv
    @Temporal(TemporalType.DATE)
    private Date jour;

    // RV jest powiązany z klientem
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // RV jest powiązany z przedziałem czasowym
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // klucze zewnętrzne
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

    // domyślny producent
    public Rv() {
    }

    // z parametrami
    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);
    }

    // klucze obce
    public long getIdCreneau() {
        return idCreneau;
    }

    public long getIdClient() {
        return idClient;
    }

    // metody pobierające i ustawiające
...
}
  • wiersz 14: klasa jest encją JPA;
  • wiersz 15: powiązana z tabelą [RV] w bazie danych;
  • wiersz 16: encja [Rv] wywodzi się z encji [AbstractEntity] i w związku z tym dziedziczy identyfikator [id] oraz wersję [version];
  • wiersz 21: data spotkania;
  • wiersz 20: typ [Date] w Javie zawiera zarówno datę, jak i godzinę. W tym przypadku zaznaczono, że wykorzystywana jest wyłącznie data;
  • wiersze 24–26: klient, dla którego umówiono to spotkanie. Tabela [RV] posiada klucz obcy do tabeli [CLIENTS]. Relacja ta jest odzwierciedlona w wierszach 24–26;
  • wiersze 29–31: przedział czasowy spotkania. Tabela [RV] posiada klucz obcy do tabeli [CRENEAUX]. Relacja ta jest odzwierciedlona w wierszach 29–31;
  • wiersze 34–35: klucz obcy [idClient];
  • wiersze 36–37: klucz obcy [idCreneau];

2.6. Warstwa [DAO]

Zaimplementujemy warstwę [DAO] przy użyciu Spring Data:

  

Warstwa [DAO] jest zaimplementowana za pomocą czterech interfejsów Spring Data:

  • [ClientRepository]: zapewnia dostęp do encji JPA i [Client];
  • [CreneauRepository]: zapewnia dostęp do jednostek JPA i [Creneau];
  • [MedecinRepository]: zapewnia dostęp do jednostek JPA i [Medecin];
  • [RvRepository]: zapewnia dostęp do jednostek JPA i [Rv];

Interfejs [MedecinRepository] wygląda następująco:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Medecin;

public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • wiersz 7: interfejs [MedecinRepository] po prostu dziedziczy metody z interfejsu [CrudRepository], nie dodając żadnych nowych;

Interfejs [ClientRepository] ma następującą postać:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Client;

public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • wiersz 7: interfejs [ClientRepository] ogranicza się do dziedziczenia metod z interfejsu [CrudRepository] bez dodawania żadnych nowych;

Interfejs [CreneauRepository] ma następującą postać:


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> {
    // lista terminów wizyt u lekarza
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • wiersz 8: interfejs [CreneauRepository] dziedziczy metody z interfejsu [CrudRepository];
  • wiersze 10–11: metoda [getAllCreneaux] pozwala uzyskać terminy wizyt u lekarza;
  • wiersz 11: parametrem jest identyfikator lekarza. Wynikiem jest lista terminów w postaci obiektu [Iterable<Creneau>];
  • wiersz 10: adnotacja [@Query] pozwala określić zapytanie JPQL (Java Persistence Query Language), które implementuje tę metodę. Parametr [?1] zostanie zastąpiony parametrem [idMedecin] tej metody;

Interfejs [RvRepository] ma następujący wygląd:


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);
}
  • wiersz 10: interfejs [RvRepository] dziedziczy metody z interfejsu [CrudRepository];
  • wiersze 12–13: metoda [getRvMedecinJour] pozwala uzyskać terminy wizyt u lekarza na dany dzień;
  • wiersz 13: parametrami są identyfikator lekarza i dzień. Wynikiem jest lista wizyt w postaci obiektu [Iterable<Rv>];
  • wiersz 12: adnotacja [@Query] pozwala określić zapytanie JPQL, które implementuje tę metodę. Parametr [?1] zostanie zastąpiony parametrem [idMedecin] tej metody, a parametr [?2] zostanie zastąpiony parametrem [jour] tej metody. Nie wystarczy użyć następującego zapytania JPQL:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

ponieważ pola klasy Rv o typach [Client] i [Creneau] są pobierane w trybie [FetchType.LAZY], co oznacza, że należy je wyraźnie zażądać, aby je uzyskać. Odbywa się to w zapytaniu JPQL przy użyciu składni [left join fetch entité], która wymaga wykonania połączenia z tabelą, na którą wskazuje klucz obcy, w celu pobrania wskazanej encji;

2.7. Warstwa [métier]

  
  • [IMetier] stanowi interfejs warstwy [métier], a [Metier] jest jej implementacją;
  • [AgendaMedecinJour] i [CreneauMedecinJour] to dwie jednostki biznesowe;

2.7.1. Entities

Entyteta [CreneauMedecinJour] łączy przedział czasowy z ewentualnym spotkaniem umówionym w tym przedziale:


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;
    // pola
    private Creneau creneau;
    private Rv rv;

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

    // metody pobierające i ustawiające
...
}
  • wiersz 12: przedział czasowy;
  • wiersz 13: ewentualna wizyta – w przeciwnym razie null;

Entyteta [AgendaMedecinJour] to kalendarz lekarza na dany dzień, tj. lista jego wizyt:


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;
    // pola
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // konstruktory
    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());
    }

    // metody pobierające i ustawiające
...
}
  • wiersz 13: lekarz;
  • wiersz 14: dzień w terminarzu;
  • wiersz 15: jego przedziały czasowe z wizytami lub bez;

2.7.2. Usługa

Interfejs warstwy [métier] wygląda następująco:


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 {

    // lista klientów
    public List<Client> getAllClients();

    // lista lekarzy
    public List<Medecin> getAllMedecins();

    // lista terminów wizyt u lekarza
    public List<Creneau> getAllCreneaux(long idMedecin);

    // lista wizyt u lekarza w danym dniu
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);

    // znalezienie klienta na podstawie jego identyfikatora
    public Client getClientById(long id);

    // znalezienie klienta na podstawie jego identyfikatora
    public Medecin getMedecinById(long id);

    // znalezienie wizyty na podstawie jej identyfikatora
    public Rv getRvById(long id);

    // znalezienie przedziału czasowego na podstawie jego identyfikatora
    public Creneau getCreneauById(long id);

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

    // usunąć RV
    public void supprimerRv(Rv rv);

    // zawód
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);

}

Komentarze wyjaśniają rolę każdej z metod.

Implementacją interfejsu [IMetier] jest następująca klasa [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 {

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

    // wdrożenie interfejsu
    @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) {
    ...
    }

}
  • wiersz 24: adnotacja [@Service] jest adnotacją Springa, która sprawia, że klasa z tą adnotacją staje się komponentem zarządzanym przez Springa. Komponentowi można nadać nazwę lub nie. Ten nosi nazwę [métier];
  • wiersz 25: klasa [Metier] implementuje interfejs [IMetier];
  • wiersz 28: adnotacja [@Autowired] jest adnotacją Springa. Wartość pola opatrzonego tą adnotacją zostanie zainicjowana (wstrzyknięta) przez Springa za pomocą odwołania do komponentu Springa o określonym typie lub nazwie. W tym przypadku adnotacja [@Autowired] nie określa nazwy. Zostanie zatem wykonane wstrzyknięcie według typu;
  • wiersz 29: pole [medecinRepository] zostanie zainicjowane odniesieniem do komponentu Spring typu [MedecinRepository]. Będzie to odniesienie do klasy wygenerowanej przez Spring Data w celu implementacji interfejsu [MedecinRepository], który już przedstawiliśmy;
  • wiersze 30–35: proces ten powtarza się dla pozostałych trzech analizowanych interfejsów;
  • wiersze 39–41: implementacja metody [getAllClients];
  • wiersz 40: wykorzystujemy metodę [findAll] z interfejsu [ClientRepository]. Metoda ta zwraca typ [Iterable<Client>], który przekształcamy na [List<Client>] za pomocą metody statycznej [Lists.newArrayList]. Klasa [Lists] jest zdefiniowana w bibliotece Google Guava. W klasie [pom.xml] zaimportowano tę zależność:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
  • wiersze 38–86: metody interfejsu [IMetier] są zaimplementowane przy pomocy klas warstwy [DAO];

Jedynie metoda z linii 88 jest specyficzna dla warstwy [métier]. Została umieszczona właśnie tutaj, ponieważ realizuje ona operację biznesową, która wykracza poza zwykły dostęp do danych. Bez tej metody nie było powodu, aby tworzyć warstwę [métier]. Metoda [getAgendaMedecinJour] wygląda następująco:


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // lista terminów wizyt u lekarza
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // lista rezerwacji tego samego lekarza na ten sam dzień
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // tworzymy słownik na podstawie dokonanych rezerwacji
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // tworzy się kalendarz na wybrany dzień
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // lekarz
        agenda.setMedecin(getMedecinById(idMedecin));
        // dzień
        agenda.setJour(jour);
        // przedziały czasowe rezerwacji
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // wypełnianie przedziałów czasowych rezerwacji
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // wiersz w kalendarzu
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // przedział czasowy
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // czy przedział czasowy jest wolny czy zarezerwowany?
            if (hReservations.containsKey(idCreneau)) {
                // przedział jest zajęty – odnotowuje się rezerwację
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // zwracamy wynik
        return agenda;
    }

Zachęcamy czytelnika do zapoznania się z komentarzami. Algorytm wygląda następująco:

  • pobiera się wszystkie przedziały czasowe wskazanego lekarza;
  • pobiera się wszystkie jego wizyty na wskazany dzień;
  • na podstawie tych dwóch informacji można stwierdzić, czy dany przedział czasowy jest wolny, czy zajęty;

2.8. Konfiguracja projektu

  

Klasa [DomainAndPersitenceConfig] konfiguruje cały projekt:


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 {

    // źródło danych 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;
    }

    // dostawca JPA – nie jest konieczny, jeśli wystarczają wartości domyślne używane przez Spring Boot
    // tutaj definiujemy go w celu włączenia/wyłączenia logów SQL
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory oraz TransactionManager są definiowane z wartościami domyślnymi przez Spring Boot

}
  • wiersz 45: nie będziemy definiować beanów [EntityManagerFactory] i [TransactionManager]. W tym celu wykorzystamy adnotację [@EnableAutoConfiguration] z Spring Boot (wiersz 17);
  • wiersze 24–32: definiują źródło danych MySQL5. Jest to bean, którego Spring Boot zazwyczaj nie jest w stanie rozpoznać;
  • wiersze 36–43: konfigurujemy również implementację JPA, aby ustawić atrybut [showSql] w Hibernate na „false” (wiersz 39). Domyślnie ma on wartość „true”;
  • na razie jedynymi komponentami zarządzanymi przez Spring są bean'y z wierszy 25 i 37 oraz bean'y [EntityManagerFactory] i [TransactionManager], które są konfigurowane automatycznie. Musimy dodać fasole z warstw [métier] i [DAO];
  • w wierszu 16 dodano do kontekstu Springa interfejsy z pakietu [rdvmdecins.repositories], które dziedziczą po interfejsie [CrudRepository];
  • w wierszu 18 dodano do kontekstu Springa wszystkie klasy z pakietu [rdvmedecins] oraz ich klasy potomne posiadające adnotację Springa. W pakiecie [rdvmdecins.metier] zostanie znaleziona klasa [Metier] wraz z jej adnotacją [@Service] i dodana do kontekstu Spring;
  • wiersz 45: Spring Boot domyślnie zdefiniuje bean [entityManagerFactory]. Należy wskazać temu beanowi, gdzie znajdują się encje JPA, którymi ma zarządzać. Odbywa się to w wierszu 19;
  • wiersz 20: wskazuje, że metody interfejsów dziedziczących po interfejsie [CrudRepository] muszą być wykonywane w ramach transakcji;

2.9. Testy warstwy [métier]

  

Klasa [rdvmedecins.tests.Metier] jest klasą testową Spring / 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(){
        // wyświetlanie klientów
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // wyświetlanie lekarzy
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // wyświetlanie terminów wizyt u lekarza
        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);
        // lista wizyt u lekarza w danym dniu
        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));
        // dodaj RV
        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);
        // weryfikacja
        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));
        // dodanie RV w tym samym terminie tego samego dnia
        // powinno spowodować wyjątek
        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();
            }
            // odnotowuje się błąd
            erreur = true;
        }
        // sprawdzamy, czy wystąpił błąd
        Assert.assertTrue(erreur);
        // lista RV
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // wyświetlanie kalendarza
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // usunięcie RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // weryfikacja
        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));
    }

    // metoda pomocnicza – wyświetla elementy kolekcji
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }

}
  • wiersz 22: adnotacja [@SpringApplicationConfiguration] umożliwia wykorzystanie omówionego wcześniej pliku konfiguracyjnego [DomainAndPersistenceConfig]. Dzięki temu klasa testowa korzysta ze wszystkich beanów zdefiniowanych w tym pliku;
  • wiersz 23: adnotacja [@RunWith] umożliwia integrację Springa z JUnit: klasa będzie mogła zostać uruchomiona jako test JUnit. [@RunWith] jest adnotacją JUnit (wiersz 9), podczas gdy klasa [SpringJUnit4ClassRunner] jest klasą Spring (wiersz 12);
  • wiersze 26–27: wstrzyknięcie do klasy testowej odwołania do warstwy [métier];
  • wiele testów to jedynie proste testy wizualne:
    • wiersze 32–33: lista klientów;
    • wiersze 35–36: lista lekarzy;
    • wiersze 39–40: lista terminów wizyt u lekarza;
    • wiersz 43: lista wizyt danego lekarza;
  • wiersz 50: dodanie nowej wizyty. Metoda [ajouterRv] zwraca wizytę wraz z dodatkową informacją – jej kluczem głównym id;
  • wiersz 53: używa się tego klucza głównego do wyszukania wizyty w bazie danych;
  • wiersz 54: sprawdzamy, czy szukana wizyta i znaleziona wizyta to ta sama. Przypominamy, że metoda [equals] encji [Rv] została przedefiniowana: dwa terminy są identyczne, jeśli mają ten sam identyfikator id. W tym przypadku pokazuje to, że dodany termin został rzeczywiście zapisany w bazie danych;
  • wiersze 61–73: próbujemy dodać ten sam termin po raz drugi. Musi to zostać odrzucone przez SGBD, ponieważ obowiązuje ograniczenie unikalności:

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 ;

Wiersz 8 powyżej wskazuje, że kombinacja [JOUR, ID_CRENEAU] musi być unikalna, co uniemożliwia umieszczenie dwóch spotkań tego samego dnia w tym samym przedziale czasowym.

  • wiersz 73: sprawdzamy, czy rzeczywiście wystąpił wyjątek;
  • wiersz 77: pobierany jest kalendarz lekarza, dla którego właśnie dodano wizytę;
  • wiersz 79: sprawdzamy, czy dodana wizyta rzeczywiście znajduje się w jego kalendarzu;
  • wiersz 82: usuwa się dodaną wizytę;
  • wiersz 84: wyszukuje się w bazie usuniętą wizytę;
  • wiersz 85: sprawdzamy, czy uzyskano wskaźnik null, co oznacza, że szukana wizyta nie istnieje;

Test zakończył się powodzeniem:

 

2.10. Program wyświetla na konsoli

  

Program konsolowy jest prosty. Pokazuje, jak odzyskać klucz zewnętrzny:


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 {
    // uruchamianie
    public static void main(String[] args) {
        // przygotowujemy konfigurację
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // uruchamiamy ją
        ConfigurableApplicationContext context = app.run(args);
        // zadanie biznesowe
        IMetier métier = context.getBean(IMetier.class);
        try {
            // dodaj RV
            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));
            // weryfikacja
            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());
        }
        // zamknięcie kontekstu Spring
        context.close();
    }

    // metoda pomocnicza – wyświetla elementy kolekcji
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }

}

Program dodaje spotkanie, a następnie sprawdza, czy zostało ono dodane.

  • wiersz 19: klasa [SpringApplication] będzie korzystać z klasy konfiguracyjnej [DomainAndPersistenceConfig];
  • wiersz 20: usunięcie logów uruchomienia aplikacji;
  • wiersz 22: uruchamiana jest klasa [SpringApplication]. Zwraca ona kontekst Spring, tj. listę zarejestrowanych beanów;
  • wiersz 24: pobierane jest odwołanie do bean’a implementującego interfejs [IMetier]. Jest to zatem odwołanie do warstwy [métier];
  • wiersze 27–31: dodanie nowego terminu na dzisiaj dla klienta nr 1 w przedziale nr 1. Klient i przedział zostały stworzone od podstaw, aby pokazać, że wykorzystywane są wyłącznie identyfikatory. Zainicjowano tutaj wersję, ale można było wpisać dowolną wartość. Nie jest ona tutaj wykorzystywana;
  • wiersz 34: chcemy poznać lekarza, który ma przedział nr 1. W tym celu musimy pobrać z bazy przedział nr 1. Ponieważ znajdujemy się w trybie [FetchType.LAZY], lekarz nie jest pobierany wraz z przedziałem. Zadbaliśmy jednak o to, aby w encji [Creneau] przewidzieć pole [idMedecin] w celu pobrania klucza głównego lekarza;
  • wiersz 35: pobieramy numer rejestracyjny lekarza;
  • wiersz 35: wyświetla się lista wizyt lekarza;

Wyniki wyświetlane w konsoli są następujące:

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. Wprowadzenie do Spring MVC

Przechodzimy teraz do tworzenia warstwy internetowej. Składa się ona głównie z metod, które przetwarzają konkretne URL i zwracają w odpowiedzi wiersz tekstu w formacie JSON (Javascript Object Notation). Ta warstwa internetowa to interfejs internetowy, który czasami nazywany jest API web. Zaimplementujemy ten interfejs za pomocą Spring MVC, kolejnej gałęzi ekosystemu Spring. Zaczniemy od zapoznania się z jednym z przewodników dostępnych na [http://spring.io].

2.11.1. Projekt demonstracyjny

  • w [1] importujemy jeden z przewodników Springa;
  • w [2] wybieramy przykład [Rest Service];
  • w pliku [3] wybieramy projekt Maven;
  • w [4] wybieramy ostateczną wersję przewodnika;
  • w [5] zatwierdzamy;
  • w [6] – zaimportowany projekt;

Usługi internetowe dostępne za pośrednictwem standardów URL i dostarczające tekst JSON są często nazywane usługami REST (REpresentational State Transfer). W niniejszym dokumencie będę nazywał usługę, którą zamierzamy stworzyć, po prostu usługą internetową / JSON. Usługa jest określana jako Restful, jeśli spełnia określone zasady. Nie starałem się ich przestrzegać.

Przyjrzyjmy się teraz zaimportowanemu projektowi, a najpierw jego konfiguracji Maven.

2.11.2. Konfiguracja Maven

Plik [pom.xml] ma następującą treść:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.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>
  • wiersze 10–14: podobnie jak w projekcie [Spring Data], znajduje się tu projekt nadrzędny [Spring Boot];
  • wiersze 17–20: artefakt [spring-boot-starter-web] zawiera biblioteki niezbędne dla projektu Spring MVC. W szczególności zawiera wbudowany serwer Tomcat. To właśnie na tym serwerze będzie uruchamiana aplikacja;
  • wiersze 21–24: biblioteka Jackson obsługuje JSON: przekształcanie obiektu Java na ciąg znaków JSON i odwrotnie;

Biblioteki dostarczane w ramach tej konfiguracji są bardzo liczne:

Powyżej widoczne są trzy archiwa serwera Tomcat.

2.11.3. Architektura serwisu Spring REST

Spring MVC implementuje model architektury zwany MVC (Model – Widok – Kontroler) w następujący sposób:

Przetwarzanie żądania klienta przebiega w następujący sposób:

  1. żądanie – żądane obiekty URL mają postać http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] to klasa Springa, która przetwarza przychodzące obiekty URL. „Przekierowuje” ona URL do akcji, która ma ją przetworzyć. Akcje te są metodami określonych klas o nazwach [Contrôleurs]. Litera „C” w MVC oznacza tutaj ciąg znaków [Dispatcher Servlet, Contrôleur, Action]. Jeśli nie skonfigurowano żadnej akcji do przetworzenia przychodzącego URL, serwlet [Dispatcher Servlet] odpowie, że żądany URL nie został znaleziony (błąd 404 NOT FOUND);
  1. przetwarzanie
  • wybrana akcja może wykorzystać parametry parami, które przekazała jej serwlet [Dispatcher Servlet]. Mogą one pochodzić z różnych źródeł:
    • ze ścieżki [/param1/param2/...] serwletu URL,
    • z parametrów [p1=v1&p2=v2] serwletu URL,
    • z parametrów przesłanych przez przeglądarkę wraz z żądaniem;
  • podczas przetwarzania żądania użytkownika akcja może wymagać warstwy [metier] [2b]. Po przetworzeniu żądania klienta może ono wywołać różne odpowiedzi. Klasycznym przykładem jest:
    • strona błędu, jeśli żądanie nie mogło zostać poprawnie przetworzone
    • w przeciwnym razie strona potwierdzenia
  • akcja żąda wyświetlenia określonego widoku [3]. Widok ten wyświetli dane, które nazywamy modelem widoku. To właśnie litera M w MVC. Akcja utworzy ten model M [2c] i zażąda wyświetlenia widoku V [3];
  1. odpowiedź – wybrany widok V wykorzystuje model M utworzony przez akcję do zainicjowania dynamicznych części odpowiedzi HTML, którą musi wysłać do klienta, a następnie wysyła tę odpowiedź.

W przypadku usługi internetowej / JSON powyższa architektura ulega niewielkiej modyfikacji:

  • w [4a] model, który jest klasą Java, jest przekształcany w ciąg znaków JSON przez bibliotekę JSON;
  • w [4b] ten ciąg znaków JSON jest wysyłany do przeglądarki;

2.11.4. Kontroler C

  

Zaimportowana aplikacja posiada następujący kontroler:


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));
    }
}
  • wiersz 9: adnotacja [@Controller] sprawia, że klasa [GreetingController] staje się kontrolerem Springa, tzn. jej metody są zarejestrowane do obsługi obiektów typu URL;
  • wiersz 15: adnotacja [@RequestMapping] wskazuje obiekt URL, który jest przetwarzany przez metodę, w tym przypadku obiekty URL i [/greeting]. W dalszej części zobaczymy, że ten URL można skonfigurować i że możliwe jest pobranie tych parametrów;
  • wiersz 16: adnotacja [@ResponseBody] wskazuje, że metoda nie generuje szablonu dla widoku (JSP, JSF, Thymeleaf, ...), który zostanie następnie wysłany do przeglądarki klienta, ale sama generuje odpowiedź wysyłaną do przeglądarki. W tym przypadku generuje obiekt typu [Greeting] (wiersz 18). Choć nie widać tego w tym miejscu, obiekt ten zostanie najpierw przekształcony w JSON, a dopiero potem wysłany do przeglądarki. To właśnie obecność biblioteki JSON wśród zależności projektu sprawia, że Spring Boot, poprzez autokonfigurację, skonfiguruje projekt w ten sposób;
  • wiersz 17: metoda [greeting] ma parametr [String name]. Adnotacja [@RequestParam(value = "name", required = false, defaultValue = "World"] wskazuje, że parametr ten musi zostać zainicjowany za pomocą parametru o nazwie [name](@RequestParam(value = "name"). Ten ostatni może być parametrem metody GET lub POST. Ten parametr nie jest obowiązkowy (required = false). W tym ostatnim przypadku parametr [name] metody zostanie zainicjowany wartością [World] (defaultValue = „World”).

2.11.5. Szablon M

Szablon M wygenerowany przez poprzednią metodę to następujący obiekt [Greeting]:

  

package hello;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

Transformacja JSON tego obiektu utworzy ciąg znaków {"id":n,"content":"tekst"}. Ostatecznie ciąg znaków JSON wygenerowany przez metodę kontrolera będzie miał postać:

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

lub

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

2.11.6. Konfiguracja projektu

  

Projekt jest skonfigurowany przez następującą klasę [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);
    }
}
  • wiersz 11: co ciekawe, ta klasa jest uruchamiana za pomocą metody [main], charakterystycznej dla aplikacji konsolowych. Tak właśnie jest. Klasa [SpringApplication] z wiersza 12 uruchomi serwer Tomcat znajdujący się w zależnościach i wdroży na nim usługę REST;
  • wiersz 4: widać, że klasa [SpringApplication] należy do projektu [Spring Boot];
  • wiersz 12: pierwszy parametr to klasa konfigurująca projekt, drugi to ewentualne parametry;
  • wiersz 8: adnotacja [@EnableAutoConfiguration] nakazuje Spring Boot przeprowadzenie konfiguracji projektu;
  • wiersz 7: adnotacja [@ComponentScan] powoduje, że katalog zawierający klasę [Application] zostanie przeszukany w celu znalezienia komponentów Spring. Zostanie znaleziony komponent – klasa [GreetingController], która posiada adnotację [@Controller], dzięki czemu staje się komponentem Spring;

2.11.7. Uruchomienie projektu

Uruchommy projekt:

 

Otrzymujemy następujące logi konsoli:

____ _ __ _ _

 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 [/**] do obsługi typu [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/**] do obsługi typu [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)
  • wiersz 12: serwer Tomcat uruchamia się na porcie 8080 (wiersz 11);
  • wiersz 16: serwlet [DispatcherServlet] jest obecny;
  • wiersz 19: wykryto metodę [GreetingController.greeting];

Aby przetestować aplikację internetową, wysyłamy żądanie do serwletu URL [http://localhost:8080/greeting]:

 

Otrzymujemy oczekiwany ciąg znaków JSON. Interesujące może być przyjrzenie się nagłówkom HTTP wysyłanym przez serwer. W tym celu użyjemy wtyczki do przeglądarki Chrome o nazwie [Advanced Rest Client] (patrz załączniki):

  • w [1], żądany URL;
  • w [2] zastosowano metodę GET;
  • w [3] – odpowiedź JSON;
  • w [4] serwer poinformował, że wysyła odpowiedź w formacie JSON;
  • w [5] żądana jest ta sama odpowiedź URL, ale tym razem z POST;
  • w [7] informacje są wysyłane do serwera w postaci [urlencoded];
  • w [6] – parametr „name” wraz z jego wartością;
  • w [8] przeglądarka informuje serwer, że wysyła mu informacje [urlencoded];
  • w [9] – odpowiedź serwera JSON;

2.11.8. Tworzenie archiwum wykonywalnego

Możliwe jest utworzenie archiwum wykonywalnego poza środowiskiem Eclipse. Niezbędna konfiguracja znajduje się w pliku [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>
  • wiersze 9–12 definiują wtyczkę, która utworzy archiwum wykonywalne;
  • wiersz 3 określa klasę wykonywalną projektu;

Postępuje się w następujący sposób:

  • w pliku [1]: uruchamia się cel Maven;
  • w [2]: istnieją dwa cele (goals): [clean] do usunięcia folderu [target] z projektu Maven oraz [package] do jego ponownego wygenerowania;
  • w [3]: wygenerowany folder [target] zostanie umieszczony w tym folderze;
  • w [4]: generowany jest plik docelowy;

W logach wyświetlanych w konsoli ważne jest, aby pojawiła się wtyczka [spring-boot-maven-plugin]. To właśnie ona generuje archiwum wykonywalne.

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

W konsoli przechodzimy do wygenerowanego folderu:

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
...
  • wiersz 5: wygenerowany plik archiwum;

Archiwum to uruchamia się w następujący sposób:

D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar

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

2014-06-11 15:32:47.088  INFO 4972 --- [           main] hello.Application
                  : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...

Teraz, gdy aplikacja internetowa została uruchomiona, można ją wywołać za pomocą przeglądarki:

 

2.11.9. Wdrożenie aplikacji na serwerze Tomcat

Chociaż Spring Boot okazuje się bardzo przydatny w trybie programowania, prawdopodobnie aplikacja w środowisku produkcyjnym zostanie wdrożona na prawdziwym serwerze Tomcat. Oto jak to zrobić:

Zmodyfikuj plik [pom.xml] w następujący sposób:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>

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

Zmiany należy wprowadzić w dwóch miejscach:

  • wiersz 9: należy wskazać, że zamierzamy wygenerować archiwum WAR (Web ARchive);
  • wiersze 26–30: należy dodać zależność od artefaktu [spring-boot-starter-tomcat]. Artefakt ten wprowadza wszystkie klasy Tomcata do zależności projektu;
  • wiersz 29: artefaktem tym jest [provided], co oznacza, że odpowiednie archiwa nie zostaną umieszczone w wygenerowanym pliku WAR. Archiwa te będą bowiem dostępne na serwerze Tomcat, na którym będzie działać aplikacja;

Należy ponadto skonfigurować aplikację internetową. W przypadku braku pliku [web.xml] odbywa się to za pomocą klasy dziedziczącej po [SpringBootServletInitializer]:

  

Klasa [ApplicationInitializer] ma następującą postać:


package hello;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class ApplicationInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

}
  • wiersz 6: klasa [ApplicationInitializer] rozszerza klasę [SpringBootServletInitializer];
  • wiersz 9: metoda [configure] została ponownie zdefiniowana (wiersz 8);
  • wiersz 10: podano klasę konfigurującą projekt;

Aby uruchomić projekt, można postępować w następujący sposób:

  • w [1] uruchamia się projekt na jednym z serwerów zarejestrowanych w Eclipse w IDE;
  • w [2] wybieramy [tc Server Developer], który jest domyślnie dostępny. Jest to wariant serwera Tomcat;

Po wykonaniu tych czynności można wywołać URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] w przeglądarce:

 

Wiemy już, jak wygenerować archiwum WAR. W dalszej części będziemy kontynuować pracę ze Spring Boot i jego wykonywalnym archiwum JAR.

2.11.10. Tworzenie nowego projektu internetowego

Aby utworzyć nowy projekt internetowy, można postępować w następujący sposób:

  • w [1]: Plik / Nowy / Projekt Spring Starter
  • w [2]: wybierz [Web]. Nie wybieramy bibliotek widoków, ponieważ w usłudze internetowej / JSON nie ma widoków;
  • utworzony projekt będzie projektem Maven. W polu [3] należy podać grupę artefaktu Maven, który zostanie utworzony, a w polu [4] – nazwę artefaktu;
  • w polu [5] należy wpisać nazwę pakietu, w którym Spring umieści klasę konfiguracyjną projektu;
  • w polu [6] należy podać nazwę projektu Eclipse – może ona różnić się od wartości podanej w polu [4];
 

2.12. Warstwa [web]

  

Warstwę internetową zbudujemy w kilku etapach:

  • etap 1: warstwa internetowa działająca bez uwierzytelniania;
  • etap 2: wdrożenie uwierzytelniania za pomocą Spring Security;
  • etap 3: wdrożenie 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)]. Klientem naszej usługi internetowej będzie aplikacja internetowa oparta na Angularze, która niekoniecznie będzie należała do tej samej domeny co nasza usługa internetowa. Domyślnie nie będzie miał on do niej dostępu, chyba że usługa internetowa mu to zezwoli. Zobaczymy, jak to zrobić;

2.12.1. Konfiguracja Maven

Plik [pom.xml] projektu wygląda następująco:


<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>
  • wiersze 7–11: nadrzędny projekt Maven;
  • wiersze 13–16: zależności dla projektu Spring o nazwie MVC;
  • wiersze 17–21: zależności od projektu warstw [métier, DAO, JPA];

2.12.2. Interfejs usługi internetowej

  • w [1], jak pokazano powyżej, przeglądarka może wysyłać zapytania tylko do ograniczonej liczby URL przy użyciu ściśle określonej składni;
  • w [4] otrzymuje odpowiedź JSON;

Wszystkie odpowiedzi naszej usługi internetowej będą miały tę samą formę, odpowiadającą przekształceniu JSON obiektu typu [Reponse] o następującej postaci:


package rdvmedecins.web.models;

public class Reponse {

    // ----------------- właściwości
    // status operacji
    private int status;
    // odpowiedź JSON
    private Object data;

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

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

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

    // ----------------------metody pobierające i ustawiające
...
}
  • wiersz 7: kod błędu odpowiedzi 0: OK, w pozostałych przypadkach: KO;
  • wiersz 9: treść odpowiedzi;

Poniżej przedstawiamy zrzuty ekranu ilustrujące interfejs serwisu internetowego / JSON:

Lista wszystkich pacjentów gabinetu lekarskiego [/getAllClients]

Lista wszystkich lekarzy w przychodni [/getAllMedecins]

Lista terminów wizyt u lekarza [/getAllCreneaux/{idMedecin}]

Lista wizyt u lekarza [/getRvMedecinJour/{idMedecin}/{aaaa-mm-jj}

Kalendarz lekarza [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]

Aby dodać lub usunąć wizytę, korzystamy z rozszerzenia Chrome [Advanced Rest Client], ponieważ operacje te wykonuje się za pomocą POST.

Dodaj wizytę [/ajouterRv]

  • w [0] – URL z serwisu internetowego;
  • w [1] wykorzystywana jest metoda POST;
  • w [2] tekst JSON informacji przekazanych do serwisu internetowego w postaci {dzień, idClient, idCreneau};
  • w [3] klient informuje serwis internetowy, że przesyła mu informacje w formacie JSON;

Odpowiedź wygląda wówczas następująco:

  • w formacie [4]: klient wysyła nagłówek oznaczający, że przesyłane przez niego dane są w formacie JSON;
  • w formacie [5]: serwis internetowy odpowiada, że również wysyła dane w formacie JSON;
  • w formacie [6]: odpowiedź serwisu internetowego w formacie JSON. Pole [data] zawiera formę JSON dodanego spotkania;

Można sprawdzić obecność nowego terminu:

Usunięcie terminu spotkania [/supprimerRv]

  • w [1], URL usługi internetowej;
  • w [2] wykorzystywana jest metoda POST;
  • w [3] tekst JSON informacji przekazanych do serwisu internetowego w postaci {idRv};
  • w przypadku [4] klient informuje serwis internetowy, że przesyła mu informacje o treści JSON;

Odpowiedź wygląda wówczas następująco:

  • w [5]: pole [status] ma wartość 0, co oznacza, że operacja zakończyła się powodzeniem;

Można sprawdzić, czy spotkanie zostało usunięte:

Jak widać powyżej, wizyta pacjenta o identyfikatorze [Mme GERMAN] już nie występuje.

Usługa internetowa umożliwia również pobieranie rekordów na podstawie ich identyfikatora:

Wszystkie te obiekty URL są przetwarzane przez kontroler [RdvMedecinsController], który teraz przedstawiamy.

2.12.3. Struktura kontrolera [RdvMedecinsController]

  

Kontroler [RdvMedecinsController] wygląda następująco:


package rdvmedecins.web.controllers;

import java.text.ParseException;
...

@RestController
public class RdvMedecinsController {

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

    @PostConstruct
    public void init() {
        // komunikaty o błędach aplikacji
        messages = application.getMessages();
    }

    // lista lekarzy
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
...
    }

    // lista klientów
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients() {
...
    }

    // lista terminów wizyt u lekarza
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
    }

    // lista wizyt u lekarza
    @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) {
...
    }
}
  • wiersz 6: adnotacja [@RestController] sprawia, że klasa [RdvMedecinsController] staje się kontrolerem Spring. Ponadto powoduje to również, że metody obsługujące obiekty typu URL będą generować odpowiedź, która zostanie automatycznie przekształcona w obiekt typu JSON;
  • wiersze 9–10: obiekt typu [ApplicationModel] zostanie tutaj wstrzyknięty przez Spring;
  • wiersz 13: adnotacja [@PostConstruct] oznacza metodę, która ma zostać wykonana zaraz po instancjonowaniu klasy. Kiedy ta metoda zostanie wykonana, obiekty wstrzyknięte przez Spring będą dostępne;
  • wszystkie metody zwracają obiekt typu [Reponse] o następującej postaci:

package rdvmedecins.web.models;

public class Reponse {

    // ----------------- właściwości
    // status operacji
    private int status;
    // odpowiedź
    private Object data;
...
}

Obiekt ten jest serializowany do formatu JSON przed wysłaniem do przeglądarki klienta;

  • wiersz 20: adnotacja [@RequestMapping] określa warunki wywołania metody. W tym przypadku metoda przetwarza żądanie GET pochodzące od URL [/getAllMedecins]. Gdyby ten URL został wywołany przez POST, zostałby odrzucony, a Spring MVC wysłałby kod błędu HTTP do klienta internetowego;
  • wiersz 32: URL jest konfigurowany przez {idMedecin}. Parametr ten jest pobierany za pomocą adnotacji [@PathVariable] w wierszu 33;
  • wiersz 33: jedyny parametr [long idMedecin] otrzymuje swoją wartość z parametru {idMedecin} z URL [@PathVariable("idMedecin")]. Parametr w URL i parametr metody mogą mieć różne nazwy. Należy tutaj zauważyć, że [@PathVariable("idMedecin")] jest typu String (cała metoda URL jest typu String), podczas gdy parametr [long idMedecin] jest typu [long]. Zmiana typu odbywa się automatycznie. Jeśli zmiana typu nie powiedzie się, zwracany jest kod błędu HTTP;
  • wiersz 65: adnotacja [@RequestBody] określa treść zapytania. W żądaniu typu GET prawie nigdy nie ma treści (ale można ją dodać). W żądaniu typu POST najczęściej jest ona obecna (ale można jej nie podawać). W przypadku żądania URL [ajouterRv] klient internetowy wysyła w swoim żądaniu POST następujący ciąg znaków JSON:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

Składnia [@RequestBody PostAjouterRv post] (wiersz 65) w połączeniu z faktem, że metoda oczekuje JSON [consumes = "application/json; charset=UTF-8"] w wierszu 64, spowoduje, że ciąg JSON wysłany przez klienta internetowego zostanie deserializowany do obiektu typu [PostAjouter]. Wygląda on następująco:


package rdvmedecins.web.models;

public class PostAjouterRv {

    // dane wpisu
    private String jour;
    private long idClient;
    private long idCreneau;

    // metody pobierające i ustawiające
    ...
}

Również w tym przypadku niezbędne zmiany typu nastąpią automatycznie;

  • w wierszach 69–70 znajduje się podobny mechanizm dla URL i [/supprimerRv]. Wysłany ciąg znaków JSON wygląda następująco:
{"idRv":116}

a typ [PostSupprimerRv] wygląda następująco:


package rdvmedecins.web.models;

public class PostSupprimerRv {

    // dane postu
    private long idRv;

    // metody pobierające i ustawiające
    ...
}

2.12.4. Szablony serwisu internetowego

  

Przedstawiliśmy już modele [Reponse, PostAjouterRv, PostSupprimerRv]. Model [ApplicationModel] wygląda następująco:


package rdvmedecins.web.models;

import java.util.Date;
...

@Component
public class ApplicationModel implements IMetier {

    // warstwa [métier]
    @Autowired
    private IMetier métier;

    // dane pochodzące z warstwy [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    // komunikaty o błędach
   private List<String> messages;

    @PostConstruct
    public void init() {
        // pobieramy lekarzy i klientów
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }

    // metoda pobierająca
    public List<String> getMessages() {
        return messages;
    }

    // ------------------------- interfejs warstwy [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);
    }

}
  • wiersz 6: adnotacja [@Component] sprawia, że klasa [ApplicationModel] staje się komponentem Spring. Podobnie jak wszystkie dotychczas omówione komponenty Spring (z wyjątkiem @Controller), zostanie utworzona tylko jedna instancja tego typu (singleton);
  • wiersz 7: klasa [ApplicationModel] implementuje interfejs [IMetier];
  • wiersze 10–11: Spring wstrzykuje odwołanie do warstwy [métier];
  • wiersz 19: adnotacja [@PostConstruct] powoduje, że metoda [init] zostanie wykonana zaraz po utworzeniu instancji klasy [ApplicationModel];
  • wiersze 23–24: pobierane są listy lekarzy i klientów z warstwy [métier];
  • wiersz 26: jeśli wystąpi wyjątek, komunikaty ze stosu wyjątków są zapisywane w polu z wiersza 17;

Klasa [ApplicationModel] posłuży nam do dwóch celów:

  • jako pamięć podręczną do przechowywania list lekarzy i pacjentów (klientów);
  • jako jednolity interfejs dla kontrolerów;

Architektura warstwy internetowej wygląda następująco:

  • w klasie [2b] metody kontrolera lub kontrolerów komunikują się z singletonem [ApplicationModel];

Strategia ta zapewnia elastyczność w zarządzaniu pamięcią podręczną. Obecnie terminy wizyt lekarzy nie są zapisywane w pamięci podręcznej. Aby je tam umieścić, wystarczy zmodyfikować klasę [ApplicationModel]. Nie ma to żadnego wpływu na kontroler, który będzie nadal korzystał z metody [List<Creneau> getAllCreneaux(long idMedecin)] tak jak dotychczas. Zmianie ulegnie implementacja tej metody w klasie [ApplicationModel].

2.12.5. Klasa statyczna

Klasa [Static] zawiera zbiór statycznych metod pomocniczych, które nie mają charakteru „biznesowego” ani „internetowego”:

  

Jej kod wygląda następująco:


package rdvmedecins.web.helpers;

import java.text.SimpleDateFormat;
...

public class Static {

    public Static() {
    }

    // lista komunikatów o błędach związanych z wyjątkiem
    public static List<String> getErreursForException(Exception exception) {
        // pobieramy listę komunikatów o błędach związanych z wyjątkiem
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }

    // mapery Object --> Map
    // --------------------------------------------------------
....
}
  • wiersz 12: metoda [Static.getErreursForException], która została wykorzystana (wiersz 8 poniżej) w metodzie [init] klasy [ApplicationModel]:

    @PostConstruct
    public void init() {
        // pobieranie lekarzy i klientów
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

Metoda ta tworzy obiekt [List<String>] zawierający komunikaty o błędach [exception.getMessage()] wynikające z wyjątku [exception] oraz komunikaty zawarte w tym wyjątku [exception.getCause()].

Klasa [Static] zawiera inne metody pomocnicze, do których powrócimy, gdy się z nimi spotkamy.

Teraz omówimy szczegółowo przetwarzanie URL w usłudze internetowej. W przetwarzaniu tym biorą udział trzy główne klasy:

  • kontroler [RdvMedecinsController];
  • klasa metod pomocniczych [Static];
  • klasa pamięci podręcznej [ApplicationModel];
  

2.12.6. Metoda [init] kontrolera

Kontroler [RdvMedecinsController] (patrz punkt 2.12.3) posiada metodę [init], która jest wykonywana zaraz po jego instancjonowaniu:


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

    @PostConstruct
    public void init() {
        // komunikaty o błędach aplikacji
        messages = application.getMessages();
}
  • wiersz 8: komunikaty o błędach przechowywane w pamięci podręcznej aplikacji [ApplicationModel] są zapisywane lokalnie w polu w wierszu 3. Dzięki temu metody będą mogły sprawdzić, czy aplikacja zainicjowała się poprawnie.

2.12.7. URL [/getAllMedecins]

URL [/getAllMedecins] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


    // lista lekarzy
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lista lekarzy
        try {
            return new Reponse(0, application.getAllMedecins());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}
  • wiersz 5: sprawdzamy, czy aplikacja została poprawnie zainicjowana (messages==null). Jeśli tak nie jest, zwracamy odpowiedź z wartościami status=-1 i data=messages;
  • wiersz 10: w przeciwnym razie zwracamy listę lekarzy z wartością status równą 0. Metoda [application.getAllMedecins()] nie generuje wyjątku, ponieważ po prostu zwraca listę znajdującą się w pamięci podręcznej. Niemniej jednak zachowamy tę obsługę wyjątków na wypadek, gdyby lekarze nie byli już przechowywani w pamięci podręcznej;

Nie omówiliśmy jeszcze przypadku, w którym aplikacja nie zainicjowała się poprawnie. Zatrzymajmy SGBD i MySQL5, uruchommy usługę internetową, a następnie wywołajmy URL i [/getAllMedecins]:

Image

Rzeczywiście pojawia się błąd. W normalnych warunkach wyświetla się następujący widok:

2.12.8. URL [/getAllClients]

URL [/getAllClients] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


    // lista klientów
    @RequestMapping(value = "/getAllClients")
    public Reponse getAllClients() {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lista klientów
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}

Jest ona analogiczna do omówionej już metody [getAllMedecins]. Uzyskane wyniki są następujące:

2.12.9. URL [/getAllCreneaux/{idMedecin}]

URL [/getAllCreneaux/{idMedecin}] jest przetwarzana przez następującą metodę kontrolera [RdvMedecinsController]:


// lista terminów wizyt u lekarza
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // pobieranie danych lekarza
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // terminy wizyt u lekarza
        List<Creneau> créneaux = null;
        try {
            créneaux = application.getAllCreneaux(médecin.getId());
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // zwracanie odpowiedzi
        return new Reponse(0, Static.getListMapForCreneaux(créneaux));
    }
  • wiersz 9: lekarz zidentyfikowany przez parametr [id] jest wywoływany przez metodę lokalną:

    private Reponse getMedecin(long id) {
        // pobieranie danych lekarza
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // czy to istniejący lekarz?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Z tej metody zwracany jest parametr status do [0,1,2]. Wróćmy do kodu metody [getAllCreneaux]:

  • wiersze 10–12: jeśli status!=0, natychmiast zwracamy odpowiedź;
  • wiersz 13: pobieramy lekarza;
  • wiersz 17: pobieramy terminy tego lekarza;
  • wiersz 22: wysyłamy jako odpowiedź obiekt [Static.getListMapForCreneaux(créneaux)];

Przypomnijmy definicję klasy [Creneau]:


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

    private static final long serialVersionUID = 1L;
    // charakterystyka terminu RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // termin jest powiązany z lekarzem
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // klucz obcy
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • wiersz 13: lekarz jest wyszukiwany w trybie [FetchType.LAZY];

Przypomnijmy zapytanie JPQL, które implementuje metodę [getAllCreneaux] w warstwie [DAO]:


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

Notacja [c.medecin.id] wymusza połączenie tabel [CRENEAUX] i [MEDECINS]. W związku z tym zapytanie zwraca wszystkie terminy wizyt lekarza, a w każdym z nich pojawia się sam lekarz. Podczas serializacji tych terminów do formatu JSON w każdym z nich pojawia się ciąg znaków JSON odpowiadający lekarzowi. Jest to zbędne. Dlatego zamiast serializować obiekt [Creneau], zserializujemy obiekt [Map], w którym umieścimy tylko wybrane pola.

Wróćmy do kodu, który analizowaliśmy na początku:


// zwracamy odpowiedź
return new Reponse(0, Static.getListMapForCreneaux(créneaux));

Metoda [Static.getListMapForCreneaux] wygląda następująco:


    // List<Creneau> --> List<Map>
    public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
        // lista słowników <String, Object>
        List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
        for (Creneau créneau : créneaux) {
            liste.add(Static.getMapForCreneau(créneau));
        }
        // zwracamy listę
        return liste;
}

a metoda [Static.getMapForCreneau] wygląda następująco:


    // Creneau --> Map
    public static Map<String, Object> getMapForCreneau(Creneau créneau) {
        // coś do zrobienia?
        if (créneau == null) {
            return null;
        }
        // słownik <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());
        // zwracamy słownik
        return hash;
}
  • wiersz 8: tworzymy słownik;
  • wiersze 9–13: umieszczamy w nim pola, które chcemy zachować w ciągu JSON. Pola [medecin] tam nie ma;
  • wiersz 15: zwracamy ten słownik;

Otrzymane wyniki są następujące:

lub następujące, jeśli przedział nie istnieje:

lub takie, jeśli wystąpi błąd dostępu do bazy danych:

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

URL [/getRvMedecinJour/{idMedecin}/{jour}] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


// lista wizyt u lekarza
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // sprawdzamy datę
        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);
        }
        // pobieramy lekarza
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // lista jego wizyt
        List<Rv> rvs = null;
        try {
            rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // zwracanie odpowiedzi
        return new Reponse(0, Static.getListMapForRvs(rvs));
}
  • wiersz 31: zwracany jest obiekt typu List<Map<String,Object>> zamiast obiektu typu List<Rv>. Przypomnijmy definicję klasy [Rv]:

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

    // charakterystyka wizyty
    @Temporal(TemporalType.DATE)
    private Date jour;

    // wizyta jest powiązana z klientem
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // wizyta jest powiązana z przedziałem czasowym
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // klucze zewnętrzne
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

...

}
  • wiersz 11: klient jest wyszukiwany w trybie [FetchType.LAZY];
  • wiersz 18: wyszukiwany jest termin w trybie [FetchType.LAZY];

Przypomnijmy zapytanie JPQL, które wyszukuje terminy spotkań:


@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")

Wykonuje się jawne połączenia w celu pobrania pól [client] i [creneau]. Ponadto, z powodu połączenia [cr.medecin.id=?1], otrzymamy również dane lekarza. Lekarz pojawi się zatem w ciągu JSON każdej wizyty. Ta zduplikowana informacja jest jednak zbędna. Wróćmy do kodu metody:

  • wiersz 31: sami tworzymy słownik, który ma zostać zserializowany jako JSON;

Słownik utworzony dla jednej wizyty wygląda następująco:


    // Rv --> Mapa
    public static Map<String, Object> getMapForRv(Rv rv) {
        // coś do zrobienia?
        if (rv == null) {
            return null;
        }
        // słownik <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()));
        // zwracamy słownik
        return hash;
}
  • wiersz 11: pobieramy słownik obiektu [Creneau], który przedstawiliśmy wcześniej;

Otrzymane wyniki są następujące:

lub te z błędną datą:

lub te z błędnym lekarzem:

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

URL [/getAgendaMedecinJour/{idMedecin}/{jour}] są przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // sprawdzamy datę
        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) });
        }
        // pobieramy dane lekarza
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // pobieramy jego kalendarz
        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));
    }
}
  • w wierszu 30 zwracany jest obiekt typu List<Map<String,Object>.

Metoda [Static.getMapForAgendaMedecinJour] wygląda następująco:


    // AgendaMedecinJour --> Mapa
    public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
        // coś do zrobienia?
        if (agenda == null) {
            return null;
        }
        // słownik <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);
        // tworzymy słownik
        return hash;
}

Utworzony słownik ma trzy pola:

  • [medecin]: lekarz będący właścicielem terminarza. Zachowano tę informację, ponieważ występuje ona tylko raz, podczas gdy w poprzednich przypadkach była powtarzana w każdym ciągu JSON;
  • [jour]: dzień kalendarza;
  • [creneauxMedecin]: lista terminów lekarza wraz z ewentualną wizytą w danym terminie;

Metoda [getMapForCreneauMedecinJour] zastosowana w wierszu 13 wygląda następująco:


    // CreneauMedecinJour --> mapa
    public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
        // coś do zrobienia?
        if (créneau == null) {
            return null;
        }
        // słownik <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
        hash.put("rv", getMapForRv(créneau.getRv()));
        // zwracamy słownik
        return hash;
}
  • wiersze 9–10: wykorzystuje się słowniki omówione wcześniej dla typów [Creneau] i [Rv], które nie zawierają zatem obiektu [Medecin];

Uzyskane wyniki są następujące:

lub te, jeśli dzień jest nieprawidłowy:

lub takie, jeśli numer lekarza jest nieprawidłowy:

2.12.12. URL [/getMedecinById/{id}]

URL [/getMedecinById/{id}] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // pobieramy lekarza
        return getMedecin(id);
}

W wierszu 8 metoda [getMedecin] wygląda następująco:


    private Reponse getMedecin(long id) {
        // pobieramy lekarza
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // czy lekarz istnieje?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Otrzymane wyniki są następujące:

lub te, jeśli numer lekarza jest nieprawidłowy:

2.12.13. URL [/getClientById/{id}]

URL [/getClientById/{id}] jest przetwarzane przez kontroler [RdvMedecinsController] w następujący sposób:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // pobieramy dane klienta
        return getClient(id);
}

W wierszu 8 metoda [getClient] wygląda następująco:


    private Reponse getClient(long id) {
        // pobieramy klienta
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // czy klient istnieje?
        if (client == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, client);
}

Uzyskane wyniki są następujące:

lub te, jeśli numer klienta jest nieprawidłowy:

2.12.14. URL [/getCreneauById/{id}]

URL [/getCreneauById/{id}] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // pobieranie terminu
        Reponse réponse = getCreneau(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
        }
        // wynik
        return réponse;
}

W wierszu 8 metoda [getCreneau] wygląda następująco:


    private Reponse getCreneau(long id) {
        // pobieranie terminu
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // czy przedział czasowy istnieje?
        if (créneau == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, créneau);
}

Uzyskane wyniki są następujące:

lub te, jeśli numer przedziału czasowego jest nieprawidłowy:

2.12.15. URL [/getRvById/{id}]

URL [/getRvById/{id}] jest przetwarzane przez kontroler [RdvMedecinsController] w następujący sposób:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // pobieramy termin spotkania
        Reponse réponse = getRv(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
        }
        // wynik
        return réponse;
}

W wierszu 8 metoda [getRv] wygląda następująco:


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

W wierszu 10 metoda [Static.getMapForRv2] wygląda następująco:


// Rv --> Mapa
    public static Map<String, Object> getMapForRv2(Rv rv) {
        // czy trzeba coś zrobić?
        if (rv == null) {
            return null;
        }
        // słownik <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());
        // zwracamy słownik
        return hash;
    }

Otrzymane wyniki są następujące:

lub te, jeśli numer spotkania jest nieprawidłowy:

2.12.16. URL [/ajouterRv]

URL [/ajouterRv] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // pobieramy przesłane wartości
        String jour = post.getJour();
        long idCreneau = post.getIdCreneau();
        long idClient = post.getIdClient();
        // sprawdzamy datę
        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);
        }
        // pobieramy przedział czasowy
        Reponse réponse = getCreneau(idCreneau);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Creneau créneau = (Creneau) réponse.getData();
        // pobieramy klienta
        réponse = getClient(idClient);
        if (réponse.getStatus() != 0) {
            réponse.incrStatusBy(2);
            return réponse;
        }
        Client client = (Client) réponse.getData();
        // dodaje się termin spotkania
        Rv rv = null;
        try {
            rv = application.ajouterRv(jourAgenda, créneau, client);
        } catch (Exception e1) {
            return new Reponse(5, Static.getErreursForException(e1));
        }
        // zwracamy odpowiedź
        return new Reponse(0, Static.getMapForRv(rv));
    }

Nie ma tu nic, czego byśmy już nie widzieli. W wierszu 41 zwracamy termin spotkania, który został dodany w wierszu 36.

Wyniki uzyskane dla klienta [Advanced Rest Client] wyglądają następująco:

lub tak, jeśli na przykład podamy nieistniejący numer terminu:

2.12.17. URL [/supprimerRv]

URL [/supprimerRv] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
        // stan aplikacji
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // pobieranie przesłanych wartości
        long idRv = post.getIdRv();
        // pobieramy zmienną Rv
        Reponse réponse = getRv(idRv);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        // usuwamy Rv
        try {
            application.supprimerRv(idRv);
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // ok
        return new Reponse(0, null);
    }

Otrzymane wyniki są następujące:

lub następujące, jeśli numer spotkania nie istnieje:

To już wszystko, jeśli chodzi o kontroler. Teraz przyjrzyjmy się, jak skonfigurować projekt.

2.12.18. Konfiguracja serwisu internetowego

  

Klasa konfiguracyjna [AppConfig] wygląda następująco:


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 {

}
  • wiersz 9: przechodzimy do trybu [AutoConfiguration], aby Spring Boot mógł skonfigurować projekt na podstawie archiwów, które znajdzie w ścieżce Classpath projektu;
  • wiersz 10: nakazuje się, aby komponenty Spring były wyszukiwane w pakiecie [rdvmedecins.web] i jego podpakietach. W ten sposób zostaną wykryte komponenty:
    • [@RestController RdvMedecinsController] w pakiecie [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] w pakiecie [rdvmedecins.web.models];
  • wiersz 11: importujemy klasę [DomainAndPersistenceConfig], która konfiguruje projekt [rdvmedecins-metier-dao] w celu uzyskania dostępu do beanów tego projektu;

2.12.19. Klasa wykonalna serwisu internetowego

  

Klasa [Boot] ma następującą postać:


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

W wierszu 10 wykonywana jest metoda statyczna [SpringApplication.run], której pierwszym parametrem jest klasa konfiguracyjna projektu [AppConfig]. Metoda ta przeprowadzi automatyczną konfigurację projektu, uruchomi serwer Tomcat wbudowany w zależnościach i wdroży na nim kontroler [RdvMedecinsController].

Dzienniki generowane podczas wykonywania są następujące:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 [/**] do obsługi typu [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/**] do obsługi typu [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
  • wiersz 17: uruchamia się serwer Tomcat;
  • wiersze 23–31: inicjalizacja warstw [métier, DAO, JPA];
  • wiersz 34: wykryto metodę obsługującą URL [/getRvMedecinJour/{idMedecin}/{jour}]. Proces wykrywania metod kontrolera powtarza się aż do wiersza 44;
  • wiersz 52: serwlet Springa o nazwie MVC [DispatcherServlet] jest gotowy do obsługi żądań klientów internetowych;

Mamy już działającą usługę internetową, z której można korzystać za pomocą klienta internetowego. Teraz zajmiemy się zabezpieczeniem tej usługi: chcemy, aby tylko określone osoby mogły zarządzać wizytami u lekarzy. W tym celu wykorzystamy framework Spring Security, będący częścią ekosystemu Spring.

2.13. Wprowadzenie do Spring Security

Ponownie zaimportujemy bibliotekę Spring, wykonując kroki od 1 do 3 opisane poniżej:

  

Projekt składa się z następujących elementów:

  • w folderze [templates] znajdują się strony projektu HTML;
  • [Application]: jest klasą wykonywalną projektu;
  • [MvcConfig]: jest klasą konfiguracyjną Springa MVC;
  • [WebSecurityConfig]: jest klasą konfiguracyjną Spring Security;

2.13.1. Konfiguracja Maven

Projekt [3] jest projektem Maven. Przyjrzyjmy się jego plikowi [pom.xml], aby poznać jego zależności:


    <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>
  • wiersze 1–5: projekt jest projektem Spring Boot;
  • wiersze 8–11: zależność od frameworka [Thymeleaf], który umożliwia tworzenie dynamicznych stron HTML. Framework ten może zastąpić strony JSP (Java Server Pages), które do niedawna były domyślnym frameworkiem widoków Springa MVC;
  • wiersze 12–15: zależność od frameworka Spring Security;

2.13.2. Widoki Thymeleaf

  

Widok [home.html] wygląda następująco:

  

<!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>
  • Atrybuty [th:xx] są atrybutami Thymeleaf. Są one interpretowane przez Thymeleaf przed wysłaniem strony HTML do klienta. Klient ich nie widzi;
  • wiersz 12: atrybut [th:href="@{/hello}"] spowoduje wygenerowanie atrybutu [href] w tagu <a>. Wartość [@{/hello}] wygeneruje ścieżkę [<context>/hello], gdzie [context] jest kontekstem aplikacji internetowej;

Wygenerowany kod HTML wygląda następująco:

<!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>
  • wiersz 10: kontekstem aplikacji jest katalog główny /;

Widok [hello.html] wygląda następująco:

  

<!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>
  • wiersz 9: Atrybut [th:inline="text"] wygeneruje tekst tagu <h1>. Tekst ten zawiera wyrażenie $, które musi zostać obliczone. Element [[${#httpServletRequest.remoteUser}]] jest wartością atrybutu [RemoteUser] bieżącego zapytania HTTP. Jest to nazwa zalogowanego użytkownika;
  • wiersz 10: formularz HTML. Atrybut [th:action="@{/logout}"] wygeneruje atrybut [action] tagu [form]. Wartość [@{/logout}] wygeneruje ścieżkę [<context>/logout], gdzie [context] to kontekst aplikacji internetowej;

Wygenerowany kod HTML wygląda następująco:

<!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>
  • wiersz 8: tłumaczenie „Hello [[${#httpServletRequest.remoteUser}]]!”;
  • wiersz 9: tłumaczenie @{/logout};
  • wiersz 11: ukryte pole o nazwie (atrybut name) _csrf;

Ostatni widok [login.html] wygląda następująco:

  

<!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>
  • wiersz 9: atrybut [th:if="${param.error}"] powoduje, że tag <div> zostanie wygenerowany tylko wtedy, gdy URL, który wyświetla stronę logowania, zawiera parametr [error] (http://context/login?error);
  • wiersz 10: atrybut [th:if="${param.logout}"] powoduje, że tag <div> zostanie wygenerowany tylko wtedy, gdy URL, który wyświetla stronę logowania, zawiera parametr [logout] (http://context/login?logout);
  • wiersze 11–23: formularz HTML;
  • wiersz 11: formularz zostanie przesłany do URL [<context>/login], gdzie <context> to kontekst aplikacji internetowej;
  • wiersz 13: pole wprowadzania danych o nazwie [username];
  • wiersz 17: pole wprowadzania danych o nazwie [password];

Wygenerowany kod HTML wygląda następująco:

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

Warto zauważyć, że w wierszu 21 Thymeleaf dodał ukryte pole o nazwie [_csrf].

2.13.3. Konfiguracja Spring MVC

  

Klasa [MvcConfig] konfiguruje framework Spring 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");
    }

}
  • wiersz 7: adnotacja [@Configuration] sprawia, że klasa [MvcConfig] staje się klasą konfiguracyjną;
  • wiersz 8: klasa [MvcConfig] rozszerza klasę [WebMvcConfigurerAdapter] w celu przedefiniowania niektórych jej metod;
  • wiersz 10: redefinicja metody klasy nadrzędnej;
  • wiersze 11–16: metoda [addViewControllers] umożliwia powiązanie obiektów URL z widokami HTML. Dokonano następujących powiązań:
URL
widok
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

Sufiks [html] oraz folder [templates] to wartości domyślne używane przez Thymeleaf. Można je zmienić w konfiguracji. Folder [templates] musi znajdować się w katalogu głównym ścieżki Classpath projektu:

Powyżej [1], foldery [main] i [resources] są folderami źródłowymi (source folders). Oznacza to, że ich zawartość będzie znajdować się w katalogu głównym ścieżki klasy projektu. Zatem w folderze [2] foldery [hello] i [templates] będą znajdować się w katalogu głównym ścieżki Classpath.

2.13.4. Konfiguracja Spring Security

  

Klasa [WebSecurityConfig] konfiguruje framework Spring Security:


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");
    }
}
  • wiersz 9: adnotacja [@Configuration] sprawia, że klasa [WebSecurityConfig] staje się klasą konfiguracyjną;
  • wiersz 10: adnotacja [@EnableWebSecurity] sprawia, że klasa [WebSecurityConfig] staje się klasą konfiguracyjną Spring Security;
  • wiersz 11: klasa [WebSecurity] rozszerza klasę [WebSecurityConfigurerAdapter] w celu przedefiniowania niektórych jej metod;
  • wiersz 12: redefinicja metody klasy nadrzędnej;
  • wiersze 13–16: metoda [configure(HttpSecurity http)] została zdefiniowana na nowo w celu określenia uprawnień dostępu do różnych obiektów URL w aplikacji;
  • wiersz 14: metoda [http.authorizeRequests()] umożliwia powiązanie elementów URL z uprawnieniami dostępu. Dokonano w niej następujących powiązań:
URL
reguła
kod
/, /home
dostęp bez uwierzytelniania

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
dostęp wyłącznie po uwierzytelnieniu
http.anyRequest().authenticated();
  • wiersz 15: określa metodę uwierzytelniania. Uwierzytelnianie odbywa się za pośrednictwem formularza URL [/login], dostępnego dla wszystkich [http.formLogin().loginPage("/login").permitAll()]. Wylogowanie (logout) jest również dostępne dla wszystkich.
  • wiersze 19–21: redefiniują metodę [configure(AuthenticationManagerBuilder auth)], która zarządza użytkownikami;
  • wiersz 20: uwierzytelnianie odbywa się przy użyciu użytkowników zdefiniowanych na stałe ([auth.inMemoryAuthentication()]). Użytkownik jest tutaj zdefiniowany za pomocą loginu [user], hasła [password] oraz roli [USER]. Użytkownikom posiadającym tę samą rolę można przyznać te same uprawnienia;

2.13.5. Klasa wykonywalna

  

Klasa [Application] ma następującą postać:


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

}
  • wiersz 8: adnotacja [@EnableAutoConfiguration] nakazuje Spring Boot (wiersz 3) wykonanie konfiguracji, której programista nie przeprowadził w sposób jawny;
  • wiersz 9: sprawia, że klasa [Application] staje się klasą konfiguracyjną Spring;
  • wiersz 10: nakazuje przeszukanie katalogu klasy [Application] w celu znalezienia komponentów Spring. W ten sposób zostaną wykryte obie klasy [MvcConfig] i [WebSecurityConfig], ponieważ posiadają one adnotację [@Configuration];
  • wiersz 13: metoda [main] klasy wykonywalnej;
  • wiersz 14: statyczna metoda [SpringApplication.run] jest wykonywana z klasą konfiguracyjną [Application] jako parametrem. Spotkaliśmy się już z tym procesem i wiemy, że serwer Tomcat zawarty w zależnościach Maven projektu zostanie uruchomiony, a projekt zostanie na nim wdrożony. Widzieliśmy, że cztery instancje URL były zarządzane przez [/, /home, /login, /hello] oraz że niektóre z nich były chronione uprawnieniami dostępu.

2.13.6. Testowanie aplikacji

Zacznijmy od wywołania URL [/], który jest jednym z czterech zaakceptowanych URL. Jest on powiązany z widokiem [/templates/home.html]:

 

Żądany URL, czyli [/], jest dostępny dla wszystkich. Dlatego właśnie go uzyskaliśmy. Link [here] jest następujący:

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

Po kliknięciu linku pojawi się żądanie URL [/hello]. Jest on zabezpieczony:

URL
zasada
kod
/, /home
dostęp bez uwierzytelniania

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
dostęp wyłącznie po uwierzytelnieniu
http.anyRequest().authenticated();

Aby uzyskać dostęp, należy się uwierzytelnić. Spring Security przekieruje wówczas przeglądarkę klienta na stronę uwierzytelniania. Zgodnie z przedstawioną konfiguracją jest to strona o adresie URL [/login]. Strona ta jest dostępna dla wszystkich:


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

Otrzymujemy zatem [1]:

Kod źródłowy uzyskanej strony wygląda następująco:

<!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>
  • W wierszu 7 pojawia się ukryte pole, którego nie ma na oryginalnej stronie [login.html]. Zostało ono dodane przez Thymeleaf. Kod ten, o nazwie CSRF (Cross Site Request Forgery), ma na celu wyeliminowanie luki w zabezpieczeniach. Ten token musi zostać przesłany do Spring Security wraz z danymi uwierzytelniającymi, aby uwierzytelnienie zostało zaakceptowane;

Pamiętamy, że Spring Security rozpoznaje wyłącznie parę użytkownik/hasło. Jeśli wpiszemy coś innego w [2], otrzymamy tę samą stronę z komunikatem o błędzie w [3]. Spring Security przekierowało przeglądarkę na stronę o adresie URL [http://localhost:8080/login?error]. Obecność parametru [error] spowodowała wyświetlenie znacznika:


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

Teraz wprowadźmy oczekiwane wartości user/password [4]:

  • na [4], logujemy się;
  • w [5] Spring Security przekierowuje nas do URL [/hello], ponieważ to właśnie o URL prosiliśmy, gdy zostaliśmy przekierowani na stronę logowania. Tożsamość użytkownika została wyświetlona w następującym wierszu [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

Strona [5] wyświetla następujący formularz:


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

Po kliknięciu przycisku [Sign Out] zostanie wygenerowany plik POST na podstawie plików URL i [/logout]. Ten plik, podobnie jak URL i [/login], jest dostępny dla wszystkich:


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

W naszym skojarzeniu URL / widoki nie zdefiniowaliśmy niczego dla URL i [/logout]. Co się stanie? Spróbujmy:

  • w [6] klikamy przycisk [Sign Out];
  • z [7] widzimy, że zostaliśmy przekierowani do URL [http://localhost:8080/login?logout]. To Spring Security zażądało tego przekierowania. Obecność parametru [logout] w URL spowodowała wyświetlenie następującego wiersza w widoku:

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

2.13.7. Wniosek

W powyższym przykładzie mogliśmy najpierw napisać aplikację internetową, a dopiero potem ją zabezpieczyć. Spring Security nie jest inwazyjny. Można wdrożyć zabezpieczenia w już napisanej aplikacji internetowej. Ponadto odkryliśmy następujące kwestie:

  • możliwe jest zdefiniowanie strony uwierzytelniającej;
  • uwierzytelnianiu musi towarzyszyć token CSRF wydany przez Spring Security;
  • jeśli uwierzytelnianie się nie powiedzie, użytkownik zostanie przekierowany na stronę uwierzytelniania z dodatkowym parametrem „error” w tokenie URL;
  • jeśli uwierzytelnienie się powiedzie, użytkownik zostanie przekierowany na stronę, o którą prosił w momencie uwierzytelnienia. Jeśli użytkownik bezpośrednio wywoła stronę uwierzytelniania bez przechodzenia przez stronę pośrednią, wówczas Spring Security przekierowuje go do URL [/] (ten przypadek nie został przedstawiony);
  • wylogowujemy się, wysyłając żądanie do strony URL [/logout] z parametrem POST. Spring Security przekierowuje nas wówczas na stronę uwierzytelniania z parametrem logout w URL;

Wszystkie te wnioski opierają się na domyślnym zachowaniu Spring Security. Zachowanie to można zmienić poprzez konfigurację, redefiniując niektóre metody klasy [WebSecurityConfigurerAdapter].

Poprzedni samouczek nie będzie nam zbytnio pomocny w dalszej części. Będziemy bowiem korzystać z:

  • bazy danych do przechowywania użytkowników, ich haseł i ról;
  • uwierzytelnianie za pomocą nagłówka HTTP;

Istnieje stosunkowo niewiele samouczków dotyczących tego, co chcemy tutaj zrealizować. Proponowane rozwiązanie stanowi połączenie fragmentów kodu znalezionych w różnych źródłach.

2.14. Wdrażanie zabezpieczeń w serwisie internetowym do rezerwacji terminów

2.14.1. Baza danych

Baza danych [rdvmedecins] zostaje rozbudowana, aby uwzględnić użytkowników, ich hasła i role. Pojawiają się trzy nowe tabele:

Image

Tabela [USERS]: użytkownicy

  • ID: klucz główny;
  • VERSION: kolumna wersjonowania wiersza;
  • IDENTITY: identyfikator opisowy użytkownika;
  • LOGIN: nazwa użytkownika;
  • PASSWORD: hasło użytkownika;

W tabeli USERS hasła nie są przechowywane w postaci jawnej:

 

Algorytmem szyfrującym hasła jest algorytm BCRYPT.

Tabela [ROLES]: role

  • ID: klucz główny;
  • VERSION: kolumna wersji wiersza;
  • NAME: nazwa roli. Domyślnie Spring Security oczekuje nazw w postaci ROLE_XX, na przykład ROLE_ADMIN lub ROLE_GUEST;
 

Tabela [USERS_ROLES]: tabela łącząca USERS / ROLES

Użytkownik może pełnić wiele ról, a jedna rola może obejmować wielu użytkowników. Mamy tu do czynienia z relacją wiele-do-wielu, którą odzwierciedla tabela [USERS_ROLES].

  • ID: klucz główny;
  • VERSION: kolumna wersjonowania wiersza;
  • USER_ID: identyfikator użytkownika;
  • ROLE_ID: identyfikator roli;
 

Ponieważ wprowadzamy zmiany w bazie danych, należy zmodyfikować wszystkie warstwy projektu [métier, DAO, JPA]:

2.14.2. Nowy projekt Eclipse o nazwie [métier, DAO, JPA]

Duplikujemy pierwotny projekt [rdvmedecins-metier-dao], tworząc projekt [rdvmedecins-metier-dao-v2]:

  • do [1]: nowy projekt;
  • na [2]: zmiany wynikające z uwzględnienia kwestii bezpieczeństwa zostały zebrane w jednym pakiecie [rdvmedecins.security]. Te nowe elementy należą do warstw [JPA] i [DAO], ale dla uproszczenia zebrałem je w jednym pakiecie.

2.14.3. Nowe elementy [JPA]

Warstwa JPA definiuje trzy nowe elementy:

  

Klasa [User] jest odzwierciedleniem tabeli [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;

    // cechy
    private String identity;
    private String login;
    private String password;

    // producent
    public User() {
    }

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

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

    // metody pobierające i ustawiające
....
}
  • wiersz 9: klasa rozszerza klasę [AbstractEntity], która została już wykorzystana dla innych jednostek;
  • wiersze 13–15: nie podano nazw kolumn, ponieważ mają one takie same nazwy jak pola, z którymi są powiązane;

Klasa [Role] jest odzwierciedleniem tabeli [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;

    // właściwości
    private String name;

    // producenci
    public Role() {
    }

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

    // tożsamość
    @Override
    public String toString() {
        return String.format("Role[%s]", name);
    }

    // metody pobierające i ustawiające
...
}

Klasa [UserRole] jest odzwierciedleniem tabeli [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;

    // UserRole odwołuje się do obiektu typu User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // UserRole odwołuje się do roli
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // metody pobierające i ustawiające
...
}
  • wiersze 15–17: definiują klucz obcy z tabeli [USERS_ROLES] do tabeli [USERS];
  • wiersze 19–21: definiują klucz obcy z tabeli [USERS_ROLES] do tabeli [ROLES];

2.14.4. Zmiany w warstwie [DAO]

Warstwa [DAO] została wzbogacona o trzy nowe warstwy [Repository]:

  

Interfejs [UserRepository] zarządza dostępem do elementów [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> {

    // lista ról użytkownika zidentyfikowanego na podstawie jego identyfikatora
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista ról użytkownika zidentyfikowanego na podstawie loginu i hasła
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // wyszukiwanie użytkownika na podstawie loginu
    User findUserByLogin(String login);
}
  • wiersz 9: interfejs [UserRepository] rozszerza interfejs [CrudRepository] z biblioteki Spring Data (wiersz 4);
  • wiersze 12–13: metoda [getRoles(User user)] pozwala uzyskać wszystkie role użytkownika zidentyfikowanego za pomocą jego [id]
  • wiersze 16–17: to samo, ale dla użytkownika zidentyfikowanego za pomocą loginu i hasła;

Interfejs [RoleRepository] zarządza dostępem do encji [Role]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // wyszukiwanie roli na podstawie nazwy
    Role findRoleByName(String name);

}
  • wiersz 5: interfejs [RoleRepository] rozszerza interfejs [CrudRepository];
  • wiersz 8: można wyszukiwać rolę według nazwy;

Interfejs [userRoleRepository] zarządza dostępem do encji [UserRole]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • wiersz 5: interfejs [UserRoleRepository] jedynie rozszerza interfejs [CrudRepository], nie dodając do niego nowych metod;

2.14.5. Klasy zarządzania użytkownikami i rolami

  

Spring Security wymaga utworzenia klasy implementującej następujący interfejs [UsersDetail]:

 

Interfejs ten jest tutaj implementowany przez klasę [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;

    // właściwości
    private User user;
    private UserRepository userRepository;

    // konstruktory
    public AppUserDetails() {
    }

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

    // -------------------------interfejs
    @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;
    }

    // metody pobierające i ustawiające
    ...
}
  • wiersz 10: klasa [AppUserDetails] implementuje interfejs [UserDetails];
  • wiersze 15–16: klasa ta zawiera użytkownika (wiersz 15) oraz repozytorium, które umożliwia uzyskanie szczegółowych informacji o tym użytkowniku (wiersz 16);
  • wiersze 22–25: konstruktor, który tworzy instancję klasy z użytkownikiem i jego repozytorium;
  • wiersze 28–35: implementacja metody [getAuthorities] z interfejsu [UserDetails]. Musi ona utworzyć kolekcję elementów typu [GrantedAuthority] lub typu pochodnego. W tym przypadku używamy typu pochodnego [SimpleGrantedAuthority] (wiersz 32), który zawiera nazwę jednej z ról użytkownika z wiersza 15;
  • wiersze 31–33: przeglądamy listę ról użytkownika z wiersza 15 w celu utworzenia listy elementów typu [SimpleGrantedAuthority];
  • wiersze 38–40: implementują metodę [getPassword] interfejsu [UserDetails]. Zwracane jest hasło użytkownika z wiersza 15;
  • wiersze 38–40: implementują metodę [getUserName] interfejsu [UserDetails]. Zwracana jest nazwa użytkownika z wiersza 15;
  • wiersze 47–50: konto użytkownika nigdy nie wygasa;
  • wiersze 52–55: konto użytkownika nigdy nie zostaje zablokowane;
  • wiersze 57–60: dane logowania użytkownika nigdy nie tracą ważności;
  • wiersze 62–65: konto użytkownika jest zawsze aktywne;

Spring Security wymaga również istnienia klasy implementującej interfejs [AppUserDetailsService]:

 

Interfejs ten jest implementowany przez następującą klasę [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 {
        // wyszukiwanie użytkownika na podstawie loginu
        User user = userRepository.findUserByLogin(login);
        // znaleziono?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // zwracamy szczegóły użytkownika
        return new AppUserDetails(user, userRepository);
    }

}
  • wiersz 9: klasa będzie komponentem Springa, a więc dostępna w swoim kontekście;
  • wiersze 12–13: komponent [UserRepository] zostanie tutaj wstrzyknięty;
  • wiersze 16–25: implementacja metody [loadUserByUsername] z interfejsu [UserDetailsService] (wiersz 10). Parametrem jest login użytkownika;
  • wiersz 18: użytkownik jest wyszukiwany na podstawie jego loginu;
  • wiersze 20–22: jeśli użytkownika nie znaleziono, zgłaszany jest wyjątek;
  • wiersz 24: tworzony i renderowany jest obiekt [AppUserDetails]. Jest on rzeczywiście typu [UserDetails] (wiersz 16);

2.14.6. Testy warstwy [DAO]

  

Najpierw tworzymy klasę wykonywalną [CreateUser], która umożliwia utworzenie użytkownika z przypisaną rolą:


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) {
        // składnia: login hasło roleName

        // potrzebne są trzy parametry
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // pobieramy parametry
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // kontekst Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // czy rola już istnieje?
        Role role = roleRepository.findRoleByName(roleName);
        // jeśli nie istnieje, tworzymy ją
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // czy użytkownik już istnieje?
        User user = userRepository.findUserByLogin(login);
        // jeśli nie istnieje, to go tworzymy
        if (user == null) {
            // haszujemy hasło za pomocą bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // zapisujemy użytkownika
            user = userRepository.save(new User(login, login, crypt));
            // tworzymy powiązanie z rolą
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // użytkownik już istnieje – czy posiada żądaną rolę?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // jeśli nie znaleziono, tworzy się powiązanie z rolą
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // zamknięcie kontekstu Spring
        context.close();
    }

}
  • wiersz 17: klasa oczekuje trzech argumentów definiujących użytkownika: jego login, hasło oraz rolę;
  • wiersze 25–27: pobierane są te trzy parametry;
  • wiersz 29: kontekst Springa jest tworzony na podstawie klasy konfiguracyjnej [DomainAndPersistenceConfig]. Klasa ta istniała już w poprzednim projekcie. Należy ją zmodyfikować w następujący sposób:

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
  • wiersz 1: należy wskazać, że w pakiecie [rdvmedecins.security] znajdują się teraz komponenty [Repository];
  • wiersz 4: należy wskazać, że w pakiecie [rdvmedecins.security] znajdują się teraz jednostki JPA;

Wróćmy do kodu tworzenia użytkownika:

  • wiersze 30–32: pobieramy odniesienia do trzech elementów [Repository], które mogą nam się przydać do utworzenia użytkownika;
  • wiersz 34: sprawdzamy, czy rola już istnieje;
  • wiersze 36–38: jeśli nie, tworzymy ją w bazie danych. Będzie miała nazwę typu [ROLE_XX];
  • wiersz 40: sprawdzamy, czy login już istnieje;
  • wiersze 42–49: jeśli login nie istnieje, tworzymy go w bazie danych;
  • wiersz 44: szyfrujemy hasło. Wykorzystujemy tutaj klasę [BCrypt] z biblioteki Spring Security (wiersz 4). Potrzebujemy zatem archiwów tego frameworka. Plik [pom.xml] zawiera nową zależność:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • wiersz 46: użytkownik jest zapisywany w bazie danych;
  • wiersz 48: podobnie jak relacja łącząca go z jego rolą;
  • wiersze 51–57: sytuacja, w której login już istnieje – sprawdzamy wówczas, czy wśród jego ról znajduje się już ta, którą chcemy mu przypisać;
  • wiersze 59–61: jeśli nie znaleziono poszukiwanej roli, tworzy się wiersz w tabeli [USERS_ROLES], aby powiązać użytkownika z jego rolą;
  • nie zabezpieczono się przed ewentualnymi wyjątkami. Jest to klasa pomocnicza służąca do szybkiego tworzenia użytkownika z przypisaną rolą.

Po uruchomieniu klasy z argumentami [x x guest] w bazie danych uzyskuje się następujące wyniki:

Tabela [USERS]

 

Tabela [ROLES]

 

Tabela [USERS_ROLES]

 

Rozważmy teraz drugą klasę [UsersTest], która jest testem klasy 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() {
        // pobieramy użytkownika [admin]
        User user = userRepository.findUserByLogin("admin");
        // sprawdzamy, czy jego hasło to [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // sprawdzamy rolę admin / admin
        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() {
        // pobieramy użytkownika [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // sprawdzamy, czy jego hasło to [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // sprawdzana jest rola admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // metoda pomocnicza – wyświetla elementy kolekcji
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • wiersze 27–34: test wizualny. Wyświetlane są wszyscy użytkownicy wraz z ich rolami;
  • wiersze 36–46: sprawdzamy, czy użytkownik [admin] ma hasło [admin] i rolę [ROLE_ADMIN], korzystając z repozytorium [UserRepository];
  • wiersz 41: [admin] to hasło w postaci niezaszyfrowanej. W bazie jest ono zaszyfrowane zgodnie z algorytmem BCrypt. Metoda [ BCrypt.checkpw] pozwala sprawdzić, czy hasło w postaci jawnej po zaszyfrowaniu jest rzeczywiście równe temu z bazy;
  • wiersze 48–59: sprawdzamy, czy użytkownik [admin] ma hasło [admin] i rolę [ROLE_ADMIN], korzystając z usługi [appUserDetailsService];

Testy zakończyły się powodzeniem, generując następujące logi:

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. Wnioski pośrednie

Dodanie klas niezbędnych dla Spring Security udało się przy niewielkich zmianach w pierwotnym projekcie. Przypomnijmy je:

  • dodanie zależności od Spring Security w pliku [pom.xml];
  • utworzenie trzech dodatkowych tabel w bazie danych;
  • utworzenie encji JPA oraz komponentów Spring w pakiecie [rdvmedecins.security];

Ten bardzo korzystny przypadek wynika z faktu, że trzy tabele dodane do bazy danych są niezależne od istniejących tabel. Można by je nawet umieścić w oddzielnej bazie danych. Było to możliwe, ponieważ zdecydowano, że użytkownik istnieje niezależnie od lekarzy i klientów. Gdyby ci ostatni byli potencjalnymi użytkownikami, konieczne byłoby utworzenie powiązań między tabelą [USERS] a tabelami [MEDECINS] i [CLIENTS]. Miałoby to wówczas znaczący wpływ na istniejący projekt.

2.14.8. Projekt Eclipse warstwy [web]

Poprzedni projekt [rdvmedecins-webapi] został zduplikowany w projekcie [rdvmedecins-webapi-v2] [1]:

Jedyne zmiany należy wprowadzić w pakiecie [rdvmedecins.web.config], gdzie należy skonfigurować Spring Security. Spotkaliśmy się już z klasą konfiguracyjną Spring Security:


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

Postępujemy w ten sam sposób:

  • wiersz 11: zdefiniuj klasę rozszerzającą klasę [WebSecurityConfigurerAdapter];
  • wiersz 13: zdefiniuj metodę [configure(HttpSecurity http)], która określa uprawnienia dostępu do różnych klas URL serwisu internetowego;
  • wiersz 19: zdefiniowanie metody [configure(AuthenticationManagerBuilder auth)], która określa użytkowników i ich role;

Konfiguracja Spring Security jest realizowana przez klasę [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 {
        // uwierzytelnianie odbywa się za pomocą komponentu [appUserDetailsService]
        // hasło jest szyfrowane za pomocą algorytmu haszującego Bcrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // hasło jest przekazywane w nagłówku Authorization: Basic xxxx
        http.httpBasic();
        // tylko rola ADMIN może korzystać z aplikacji
        http.authorizeRequests() //
                .antMatchers("/", "/**") // wszystkie URL
                .hasRole("ADMIN");
    }
}
  • wiersze 14–15: wykorzystano adnotacje z przykładu;
  • wiersze 17–18: wstrzykiwana jest klasa [AppUserDetails], która zapewnia dostęp użytkownikom aplikacji;
  • wiersze 20–21: metoda [configure(HttpSecurity http)] definiuje użytkowników i ich role. Otrzymuje ona jako parametr typ [AuthenticationManagerBuilder]. Parametr ten jest wzbogacony o dwie informacje:
    • odwołanie do usługi [appUserDetailsService] z wiersza 18, która zapewnia dostęp zarejestrowanym użytkownikom. Należy tutaj zauważyć, że nie pojawia się informacja o tym, że są oni zarejestrowani w bazie danych. Mogą więc znajdować się w pamięci podręcznej, być dostarczani przez usługę internetową itp.;
    • rodzaj szyfrowania zastosowanego dla hasła. Przypominamy, że zastosowaliśmy algorytm BCrypt;
  • wiersze 27–40: metoda [configure(HttpSecurity http)] definiuje uprawnienia dostępu do URL serwisu internetowego;
  • wiersz 30: w projekcie wprowadzającym widzieliśmy, że domyślnie Spring Security zarządza tokenem CSRF (Cross Site Request Forgery), który użytkownik chcący się uwierzytelnić musiał odesłać do serwera. Tutaj mechanizm ten jest wyłączony;
  • wiersz 32: włączamy tryb uwierzytelniania za pomocą nagłówka HTTP. Klient będzie musiał wysłać następujący nagłówek HTTP:
Authorization:Basic code

gdzie „code” to kodowanie ciągu „login:password” za pomocą algorytmu Base64. Na przykład kodowanie Base64 ciągu admin:admin to YWRtaW46YWRtaW4=. Zatem użytkownik o nazwie użytkownika [admin] i haśle [admin] wyśle następujący nagłówek HTTP w celu uwierzytelnienia:

Authorization:Basic YWRtaW46YWRtaW4=
  • wiersze 34–36: wskazują, że wszystkie elementy URL serwisu internetowego są dostępne dla użytkowników posiadających rolę [ROLE_ADMIN]. Oznacza to, że użytkownik nieposiadający tej roli nie ma dostępu do serwisu internetowego;

Klasa [AppConfig], która konfiguruje całą aplikację, zmienia się w następujący sposób:

  

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 {

}
  • zmiana dotyczy wiersza 11: wskazano, że obecnie należy korzystać z dwóch plików konfiguracyjnych: [DomainAndPersistenceConfig] i [SecurityConfig].

2.14.9. Testy serwisu internetowego

Przetestujemy usługę internetową przy użyciu przeglądarki Chrome [Advanced Rest Client]. Będziemy musieli podać nagłówek uwierzytelniający HTTP:

Authorization:Basic code

gdzie [code] to kod Base64 ciągu znaków [login:password]. Aby wygenerować ten kod, można skorzystać z następującego programu:

  

package rdvmedecins.helpers;

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

public class Base64Encoder {

    public static void main(String[] args) {
        // oczekuje się dwóch argumentów: login i hasło
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // pobierane są oba argumenty
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // koduje się ciąg znaków
        byte[] data = Base64.encode(chaîne.getBytes());
        // wyświetla się jej kodowanie Base64
        System.out.println(new String(data));
    }

}

Jeśli uruchomimy ten program z dwoma argumentami [admin admin]:

  

otrzymujemy następujący wynik:

YWRtaW46YWRtaW4=

Teraz, gdy wiemy już, jak wygenerować nagłówek uwierzytelniający HTTP, uruchamiamy zabezpieczoną usługę internetową. Następnie za pomocą przeglądarki Chrome [Advanced Rest Client] żądamy listy wszystkich lekarzy:

  • w [1] żądamy listy lekarzy;
  • w [2], przy użyciu metody GET;
  • w [3] podajemy nagłówek uwierzytelniający HTTP. Kod [YWRtaW46YWRtaW4=] to kodowanie Base64 ciągu znaków [admin:admin];
  • w [4] wysyłamy polecenie HTTP;

Odpowiedź serwera jest następująca:

  • w [1] znajduje się nagłówek uwierzytelniający HTTP;
  • w [2] serwer zwraca odpowiedź JSON;
  • w [3] – lista lekarzy.

Spróbujmy teraz wysłać żądanie HTTP z nieprawidłowym nagłówkiem uwierzytelniającym. Odpowiedź będzie wówczas następująca:

  • w [1] i [3]: nagłówek uwierzytelniający HTTP;
  • na [2]: odpowiedź serwisu internetowego;

Teraz spróbujmy z użytkownikiem user / user. Użytkownik ten istnieje, ale nie ma dostępu do serwisu internetowego. Jeśli uruchomimy program do kodowania Base64 z dwoma argumentami [user user]:

  

otrzymujemy następujący wynik:

dXNlcjp1c2Vy
  • w postaci [1] i [3]: nagłówek uwierzytelniający HTTP;
  • na [2]: odpowiedź serwisu internetowego. Różni się ona od poprzedniej, która miała postać [401 Unauthorized]. Tym razem użytkownik poprawnie się uwierzytelnił, ale nie posiada wystarczających uprawnień, aby uzyskać dostęp do URL;

2.15. Conclusion

Przypomnijmy ogólną architekturę naszej aplikacji klient-serwer:

Zabezpieczona usługa internetowa jest już gotowa do działania. Zobaczymy, że będzie trzeba ją zmodyfikować w związku z problemami, które ujawnią się podczas tworzenia klienta Angularowego JS. Ale poczekamy, aż problem się pojawi, aby go rozwiązać. Teraz stworzymy klienta Angularowego, który zapewni interfejs internetowy do zarządzania wizytami u lekarzy.