8. Studium przypadku
8.1. Introduction
Zamierzamy stworzyć aplikację internetową do umawiania wizyt w gabinecie lekarskim. Zagadnienie to zostało omówione w dokumencie „Samouczek AngularJS / Spring 4” w sekcji URL [http://tahe.developpez.com/angularjs-spring4/]. Architektura tej aplikacji była następująca:
![]() |
- w [1] serwer WWW dostarcza statyczne strony do przeglądarki. Strony te zawierają aplikację AngularJS zbudowaną w oparciu o model MVC (Model – Widok – Kontroler). Model w tym przypadku obejmuje zarówno widoki, jak i domenę, reprezentowaną tutaj przez warstwę [Services];
- użytkownik będzie wchodził w interakcję z widokami wyświetlanymi w przeglądarce. Jego działania będą czasami wymagały wysłania zapytania do serwera Spring 4 o nazwie [2]. Serwer ten przetworzy żądanie i zwróci odpowiedź jSON (JavaScript w notacji obiektowej) [3]. Odpowiedź ta zostanie wykorzystana do aktualizacji widoku wyświetlanego użytkownikowi.
Proponujemy przejęcie tej aplikacji i wdrożenie jej od początku do końca przy użyciu Springa MVC. Architektura będzie wówczas wyglądać następująco:
![]() |
Przeglądarka połączy się z aplikacją [Web 1] zaimplementowaną przy użyciu Springa MVC, która pobierze dane z serwisu internetowego [Web 2], również zaimplementowanego przy użyciu Springa MVC.
8.2. Funkcje aplikacji
Zachęcamy czytelnika do zapoznania się z funkcjonalnością aplikacji poprzez jej przetestowanie. W STS ładujemy projekty Maven z folderu [etude-de-cas]:
![]() | ![]() |
Najpierw utworzymy bazę danych MySQL 5 [dbrdvmedecins] za pomocą narzędzia [Wamp Server] (patrz punkt 9.5):
![]() |
- w [1] wybieramy narzędzie [phpMyAdmin] z WampServer;
- w [2] wybiera się opcję [Importer];
![]() |
- w [3] wybieramy plik [database/dbrdvmedecins.sql];
- w przypadku pliku [4] należy go uruchomić;
- w pliku [5] tworzona jest baza danych.
Następnie należy uruchomić serwer połączony z bazą danych. Jest to projekt [rdvmedecins-webjson-server]
![]() |
Serwer będzie dostępny pod adresem URL [http://localhost:8080]. Można to zmienić w pliku [application.properties] projektu:
![]() |
server.port=8080
Parametry dostępu do bazy danych są zapisane w klasie [DomainAndPersistenceConfig] projektu [rdvmedecins-metier-dao]:
![]() |
// ź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;
}
Jeśli uzyskujesz dostęp do serwerów SGBD i MySQL przy użyciu innych identyfikatorów, to właśnie tam odbywa się cała operacja.
Następnie, podobnie jak w przypadku poprzedniego serwera, uruchamia się serwer [rdvmedecins-springthymeleaf-server]:
![]() | ![]() |
Serwer ten jest domyślnie dostępny pod adresem URL [http://localhost:8081]. Ponownie, można to skonfigurować w pliku [application.properties] projektu:
server.port=8081
Ponadto serwer ten musi znać identyfikator URL serwera połączonego z bazą danych. Konfiguracja ta znajduje się w powyższej klasie [AppConfig]:
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// katalog główny serwisu internetowego / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// limit czasu w milisekundach
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
Jeśli pierwszy serwer został uruchomiony na porcie innym niż 8080, należy zmodyfikować wiersz 5.
Następnie za pomocą przeglądarki należy wywołać plik URL [http://localhost:8081/boot.html]:
![]() |
- na [1], stronę startową aplikacji;
- na [2] oraz [3] – odpowiednio identyfikator i hasło osoby, która chce korzystać z aplikacji. Istnieją dwaj użytkownicy: admin/admin (login/hasło) z rolą (ADMIN) oraz user/user z rolą (USER). Tylko rola ADMIN ma uprawnienia do korzystania z aplikacji. Rola USER służy wyłącznie do pokazania, jaką odpowiedź zwraca serwer w tym przypadku użycia;
- w [4] znajduje się przycisk umożliwiający połączenie z serwerem;
- w [5] – język aplikacji. Dostępne są dwa: domyślnie francuski oraz angielski;
- w [6] – URL serwera [rdvmedecins-springthymeleaf-server];
![]() |
- w pliku [1] nawiązuje się połączenie;
![]() |
- po zalogowaniu można wybrać lekarza, u którego chce się umówić na wizytę [2], oraz dzień wizyty [3]. Po wprowadzeniu danych dotyczących lekarza i dnia automatycznie wyświetla się kalendarz:
![]() |
- po wyświetleniu kalendarza lekarza można zarezerwować termin [5];
![]() |
- w [6] wybiera się pacjenta na wizytę i potwierdza ten wybór w [7];
![]() |
Po zatwierdzeniu wizyty następuje automatyczny powrót do kalendarza, w którym nowa wizyta jest już zapisana. Wizytę tę można później usunąć za pomocą kodu [8].
Opisano już główne funkcje. Są one proste. Na koniec omówimy zarządzanie językiem:
1

- w [1] przechodzi się z języka francuskiego na angielski;
![]() |
- w [2] widok został przełączony na język angielski, łącznie z kalendarzem;
8.3. 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ć. Relacje 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 przedziałem czasowym powiązana jest 0 lub więcej wizyt (w różnych dniach).
8.3.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: tytuł (panna, pani, pan)
8.3.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)
8.3.3. Tabela [CRENEAUX]
Zawiera listę przedziałów czasowych, w których możliwe są 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).
8.3.4. Tabela [RV]
Zawiera listę RV przypisanych każdemu lekarzowi:
![]() |
- ID: numer jednoznacznie identyfikujący RV – klucz główny
- JOUR: dzień, w którym wystawiono RV
- ID_CRENEAU: przedział czasowy dla RV – klucz obcy w polu [ID] tabeli [CRENEAUX] – określa zarówno przedział czasowy, jak i danego lekarza.
- ID_CLIENT: numer klienta, dla którego dokonano rezerwacji – klucz obcy w polu [ID] tabeli [CLIENTS]
Ta tabela posiada ograniczenie unikalności dla wartości połączonych kolumn (JOUR, ID_CRENEAU):
Jeśli wiersz w tabeli [RV] ma wartości (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 id równy 3 (por. [1] powyżej) oznacza, że rezerwacja RV została dokonana na przedział czasowy nr 20 i 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.
8.3.5. Tworzenie bazy danych
Aby utworzyć bazę danych [dbrdvmedecins], wraz z przykładami zawartymi w niniejszym dokumencie [1-3] dostarczono skrypt [dbrdvmedecins.sql]:
![]() |
Korzystamy z narzędzia [PhpMyAdmin] z pakietu WampServer:
![]() |
- w [1] wybieramy narzędzie [phpMyAdmin] z WampServer;
- w [2] wybiera się opcję [Importer];
![]() |
- w [3] należy wybrać plik [database/dbrdvmedecins.sql];
- w [4] uruchamia się go;
- w pliku [5] tworzona jest baza danych.
8.4. Usługa internetowa / jSON
![]() |
W powyższej architekturze zajmiemy się teraz tworzeniem serwisu internetowego / jSON zbudowanego przy użyciu frameworka Spring MVC. 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 część dotyczącą uwierzytelniania przy użyciu Spring Security.
Poniżej znajduje się kopia dokumentu [http://tahe.developpez.com/angularjs-spring4/] z kilkoma zmianami.
8.4.1. 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ę ze 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 pliku [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.
8.4.1.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.1.10.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żywaj 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 [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.
8.4.1.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.
8.4.1.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], interfejs 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 odzyskać encję T identyfikowaną 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);
}
- w wierszu 9 można 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ą automatycznie implementowane. 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 z linii 9 jest automatycznie implementowana przez [Spring Data], ponieważ odwołuje się do pola [lastName] (linia 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):
Typ T musi zatem posiadać pole o nazwie [something]. W ten sposób metoda
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.
8.4.1.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 definiuje ź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 żaden z tych beanów nie jest zdefiniowany.
- 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 przy użyciu 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, którą ma zarządzać 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 16: 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.
Poniższe operacje wykorzystują wyłącznie metody 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:
- 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 15: 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 21–22: utworzono bazę danych. Utworzono tabelę [CUSTOMER]. Oznacza to, że Hibernate został skonfigurowany do generowania tabel na podstawie definicji JPA, w tym przypadku definicji JPA klasy [Customer];
- wiersze 27–31: wstawiono pięciu klientów;
- wiersze 33–635: wynik metody [findOne] z interfejsu;
- wiersze 37–40: wyniki metody [findByLastName];
- wiersze 41 i kolejne: logi zamknięcia kontekstu Spring.
8.4.1.5. Ręczna konfiguracja projektu Spring Data
Duplikujemy poprzedni projekt w projekcie [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.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- transakcje Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.1.10.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- Baza danych H2 -->
<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>
...
</project>
- wiersze 2–18: podstawowe biblioteki Springa;
- wiersze 19–29: biblioteki Spring do zarządzania transakcjami z bazą danych;
- wiersze 30–35: biblioteka Spring do pracy z ORM (Object Relational Mapper);
- wiersze 36–41: Spring Data służący do uzyskiwania dostępu do bazy danych;
- wiersze 42–47: Spring Boot do uruchamiania aplikacji;
- wiersze 54–59: SGBD H2;
- wiersze 60–70: 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 używany jest tryb [@EnableAutoConfiguration], a elementy JPA nie znajdują się w tym samym katalogu co klasa konfiguracyjna;
- wiersz 18: adnotacja [@ComponentScan] pozwala na wyliczenie katalogó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.
Wykonanie 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ż żadnych zależności od Spring Boot.
Wykonanie daje takie same wyniki jak poprzednio.
8.4.1.6. Tworzenie archiwum wykonywalnego
Aby utworzyć archiwum wykonywalne projektu, można postępować w następujący sposób:
![]() |
- w [1]: tworzymy konfigurację uruchomienia;
- w [2]: typu [Java Application]
- w [3]: określa projekt do uruchomienia (użyj przycisku Browse);
- w [4]: określa klasę do wykonania;
- w [5]: nazwa konfiguracji wykonania – może być dowolna;
![]() |
- w [6]: eksportuje się 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:
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:
8.4.1.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 [4]: podajemy nazwę artefaktu (w tym przypadku pliku jar), który zostanie utworzony podczas kompilacji projektu;
- w [5]: podaje się 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.2.0.RELEASE</version>
<relativePath/> <!-- wyszukiwanie elementu nadrzędnego 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.
8.4.2. Projekt serwera w środowisku 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 przetwarzane przez warstwę [métier];
- [rdvmdecins.config]: klasy konfiguracyjne warstwy trwałości;
- [rdvmedecins.boot]: prosta aplikacja konsolowa;
8.4.3. Konfiguracja Maven
![]() | ![]() | ![]() |
Plik [pom.xml] z tego 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.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Test Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- sterownik JDBC / MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- mapper jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- używaj UTF-8 do wszystkiego -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>rdvmedecins.boot.Boot</start-class>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<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 15–18: dla Spring Data;
- wiersze 20–24: dla testów JUnit;
- wiersze 26–29: dla biblioteki Spring Security, której warstwa [DAO] wykorzystuje jedną z klas szyfrowania haseł;
- wiersze 31–34: sterownik JDBC dla SGBD i MySQL5;
- wiersze 36–39: pula połączeń Tomcat o nazwie JDBC. Pula połączeń gromadzi otwarte połączenia z bazą danych. Gdy kod chce otworzyć połączenie, zwraca się z prośbą do puli. Gdy kod zamyka połączenie, nie jest ono zamykane, lecz zwracane do puli. Wszystko to odbywa się w sposób przezroczysty z punktu widzenia kodu. Zyskuje się na wydajności, ponieważ wielokrotne otwieranie i zamykanie połączenia wiąże się z kosztem czasowym. W tym przypadku pula połączeń nawiązuje określoną liczbę połączeń z bazą danych już w momencie jej instancjonowania. Następnie nie dochodzi ani do otwierania, ani do zamykania połączeń, chyba że liczba połączeń przechowywanych w puli okaże się niewystarczająca. W takim przypadku pula automatycznie tworzy nowe połączenia;
- wiersze 41–44: biblioteka Jackson do obsługi jSON;
- wiersze 46–50: biblioteka Google do zarządzania kolekcjami;
8.4.4. Entities JPA
![]() |
Entities JPA to obiekty, które będą enkapsulować 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.IDENTITY)
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) || entity==null) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id.longValue() == other.id.longValue();
}
// 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.IDENTITY)] wskazuje, że wartość tego klucza głównego jest generowana przez SGBD oraz że narzucony jest tryb generowania [IDENTITY]. W przypadku SGBD i MySQL oznacza to, że klucze główne będą generowane przez SGBD z atrybutem [AUTO_INCREMENT]
- wiersze 18–19: określają 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: otrzyma wyjątek, ponieważ posiada wersję (V1) inną niż ta w bazie (V1+1);
- wiersze 29–33: metoda [build] pozwala zainicjować dwa pola klasy [AbstractEntity]. Metoda ta zwraca odwołanie do tak zainicjowanej instancji klasy [AbstractEntity];
- wiersze 36–44: metoda [equals] klasy zostaje przedefiniowana: dwa obiekty uznaje się za równe, jeśli mają tę samą nazwę klasy i ten sam identyfikator id;
- wiersze 21–26: podczas redefiniowania metody [equals] klasy należy również zredefiniować jej metodę [hashCode] (wiersze 21–26). Zasada jest taka, że dwa obiekty uznane za równe przez metodę [equals] muszą wówczas mieć ten sam identyfikator [hashCode]. W tym przypadku identyfikator [hashCode] danego obiektu jest równy jego kluczowi podstawowemu [id]. [hashCode] klasy jest wykorzystywany w szczególności w zarządzaniu słownikami, których wartościami są instancje tej klasy;
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, do której odnosi się adnotacja, jest klasą nadrzędną dla 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: klasa jest encją 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:
Jeśli dodatkowo chcemy przypisać mu identyfikator i wersję, możemy zapisać:
gdzie metoda [build] jest tą zdefiniowaną 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 jednostką 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 setter i getter
...
}
- 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, co jest niezgodne ze standardem JPA. Dodajemy zatem atrybuty [insertable = false, updatable = false], co sprawia, że kolumna ta może być jedynie odczytywana;
Entytą [Rv] jest następująca:
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 miejscu 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];
8.4.5. 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 encji 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:
ponieważ pola klasy Rv o typach [Client] i [Creneau] są uzyskiwane w trybie [FetchType.LAZY], co oznacza, że należy o nie wyraźnie poprosić, 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;
8.4.6. Warstwa [métier]
![]() |
![]() |
- [IMetier] stanowi interfejs warstwy [métier], a [Metier] jest jej implementacją;
- [AgendaMedecinJour] i [CreneauMedecinJour] to dwie jednostki biznesowe;
8.4.6.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;
- linia 15: godziny przyjęć – z umówioną wizytą lub bez;
8.4.6.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 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);
// rodzaj działalności
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 w wierszu 88 jest specyficzna dla warstwy [métier]. Została umieszczona w tym miejscu, ponieważ realizuje przetwarzanie biznesowe, które 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);
// tworzy się 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 działa w następujący sposób:
- 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;
8.4.7. Konfiguracja projektu Spring
![]() |
Klasa [DomainAndPersistenceConfig] konfiguruje cały projekt:
package rdvmedecins.config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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;
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
// pakiety jednostek JPA
public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
// źródło danych MySQL
@Bean
public DataSource dataSource() {
// źródło danych TomcatJdbc
DataSource dataSource = new DataSource();
// konfiguracja JDBC
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
// początkowo otwarte połączenia
dataSource.setInitialSize(5);
// wynik
return dataSource;
}
// dostawcą jest Hibernate (JPA)
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
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 17: klasa ta jest klasą konfiguracyjną Springa;
- wiersz 18: pakiety, w których znajdują się interfejsy [CrudRepository] z biblioteki Spring Data. Zostaną one dodane do kontekstu Spring;
- wiersz 19: dodaje do kontekstu Springa wszystkie klasy z pakietu [rdvmedecins] oraz ich podklasy posiadające adnotację Springa. W pakiecie [rdvmdecins.metier] zostanie znaleziona klasa [Metier] wraz z jej adnotacją [@Service] i dodana do kontekstu Spring;
- wiersze 26–39: konfigurują pulę połączeń Tomcat JDBC (wiersz 5);
- wiersz 36: pula połączeń będzie miała domyślnie 5 otwartych połączeń. Ten wiersz podano jako przykład. W naszym przypadku wystarczyłoby jedno połączenie. Gdyby warstwa [DAO] była wykorzystywana przez wiele wątków, ten wiersz byłby niezbędny. Tak będzie miało miejsce w przyszłości, gdy warstwa [DAO] będzie służyć jako podłoże dla aplikacji internetowej, która z natury obsługuje wielu użytkowników jednocześnie;
- wiersze 42–49: zastosowana implementacja JPA jest implementacją Hibernate;
- wiersz 45: brak logów SQL;
- wiersz 46: brak regeneracji tabel;
- wiersz 47: używany SGBD to MySQL;
- wiersze 53–61: definiują obiekt EntityManagerFactory warstwy JPA. Na podstawie tego obiektu uzyskuje się obiekt [EntityManager], który umożliwia wykonywanie operacji JPA;
- wiersz 57: podaje się pakiet lub pakiety, w których znajdują się jednostki JPA;
- wiersz 58: wskazuje źródło danych, które ma zostać połączone z warstwą JPA;
- wiersze 64–69: menedżer transakcji powiązany z poprzednim elementem EntityManagerFactory. Domyślnie metody interfejsów [CrudRepository] biblioteki Spring Data są wykonywane w ramach transakcji. Transakcja jest uruchamiana przed wejściem do metody i kończona (poprzez commit lub rollback) po jej opuszczeniu;
8.4.8. 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));
// dodaj 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żywamy tego klucza głównego do wyszukania terminu w bazie danych;
- wiersz 54: sprawdzamy, czy szukane spotkanie i znalezione spotkanie są tym samym spotkaniem. Przypominamy, że metoda [equals] klasy [Rv] została przedefiniowana: dwa terminy są identyczne, jeśli mają ten sam identyfikator. 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:
![]() |
8.4.9. Program wyświetla w konsoli
![]() |
Program konsolowy jest prosty. Pokazuje, jak pobrać 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. Jednak zadbaliśmy o to, aby w encji [Creneau] przewidzieć pole [idMedecin] w celu pobrania klucza głównego lekarza;
- wiersz 35: pobieramy klucz pierwotny lekarza;
- wiersz 36: wyświetlana jest lista wizyt lekarza;
Wyniki wyświetlane w konsoli są następujące:
8.4.10. Zarządzanie logami
Logi konsoli są konfigurowane za pomocą dwóch plików: [application.properties] oraz [logback.xml] [1]:
![]() |
Plik [application.properties] jest wykorzystywany przez framework Spring Boot. Można w nim zdefiniować wiele parametrów, aby zmienić wartości domyślne stosowane przez Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Oto jego zawartość:
logging.level.org.hibernate=OFF
spring.main.show-banner=false
- wiersz 1: kontroluje poziom logowania Hibernate – w tym przypadku brak logów
- wiersz 2: kontroluje wyświetlanie banera Spring Boot – tutaj nie ma banera
Plik [logback.xml] jest plikiem konfiguracyjnym frameworka logowania [logback] [2]:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- kodery mają domyślnie przypisany typ ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- kontrola poziomu logów -->
<root level="info"> <!-- wyłączone, info, debug, ostrzeżenie -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- ogólny poziom logowania jest kontrolowany przez wiersz 9 – tutaj logi na poziomie [info];
Daje to następujący wynik:
Jeśli zmienimy poziom logowania Hibernate na [info] (nie wprowadzając żadnych innych zmian):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
otrzymujemy następujący wynik:
Jeśli zmienimy poziom logowania na [debug] (nie wprowadzając żadnych innych zmian):
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
daje to następujący wynik:
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...
8.4.11. Warstwa [web / jSON]
![]() |
![]() |
Warstwę [web / jSON] zbudujemy w kilku etapach:
- etap 1: stworzenie działającej warstwy internetowej bez uwierzytelniania;
- etap 2: wdrożenie uwierzytelniania za pomocą Spring Security;
- etap 3: wdrożenie CORS i [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 na to pozwoli. Zobaczymy, jak to zrobić;
8.4.11.1. 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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webjson-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- warstwa internetowa Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- warstwa testowa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- warstwa DAO -->
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
...
</project>
- wiersze 12–15: nadrzędny projekt Maven;
- wiersze 19–22: zależności dla projektu Spring MVC;
- wiersze 24–28: zależności dla testów JUnit / Spring;
- wiersze 30–34: zależności od projektu warstw [métier, DAO, JPA];
8.4.11.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 [Response] o następującej postaci:
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- właściwości
// status operacji
private int status;
// ewentualne komunikaty o błędach
private List<String> messages;
// treść odpowiedzi
private T body;
// konstruktory
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// metody pobierające i ustawiające
...
}
- wiersz 7: kod błędu odpowiedzi 0: OK, w pozostałych przypadkach: KO;
- wiersz 11: lista komunikatów o błędach, jeśli wystąpił błąd;
- wiersz 13: treść odpowiedzi;
Poniżej przedstawiamy zrzuty ekranu ilustrujące interfejs serwisu internetowego / jSON:
Lista wszystkich pacjentów gabinetu lekarskiego [/getAllClients]
![]() |
Lista wszystkich lekarzy w gabinecie medycznym [/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 [6]: odpowiedź jSON od serwisu internetowego. Pole [body] zawiera formę jSON dodanego spotkania;
Można sprawdzić obecność nowego terminu:
![]() |
Należy zwrócić uwagę na identyfikator spotkania [50]. Usuniemy to spotkanie.
Usunięcie spotkania [/supprimerRv]
![]() |
- w [1], URL z serwisu internetowego;
- 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 GERMAIN] 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 wkrótce przedstawimy.
8.4.11.3. Konfiguracja serwisu internetowego
![]() |
Klasa konfiguracyjna [AppConfig] wygląda następująco:
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- wiersz 12: klasa [AppConfig] konfiguruje całą aplikację;
- wiersz 9: klasa [AppConfig] jest klasą konfiguracyjną Spring;
- 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;
- wiersz 11: klasa [SecurityConfig] konfiguruje zabezpieczenia aplikacji internetowej. Na razie ją pominę;
- wiersz 11: klasa [WebConfig] konfiguruje warstwę [web / jSON];
Klasa [WebConfig] wygląda następująco:
package rdvmedecins.web.config;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@EnableWebMvc
public class WebConfig {
// konfiguracja serwletu dyspozytora dla nagłówków CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mapery jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- wiersze 20–25: definiują bean [dispatcherServlet]. Klasa [DispatcherServlet] jest serwletem frameworku Spring MVC. Pełni ona rolę [FrontController]: przechwytuje żądania kierowane do serwisu Spring MVC i przekazuje je do przetworzenia przez jeden z kontrolerów (Controller) serwisu;
- wiersz 22: instancjonowanie klasy;
- wiersz 23: ten wiersz można na razie pominąć;
- wiersze 27–30: serwlet [dispatcherServlet] przetwarza wszystkie żądania URL;
- wiersze 27–30: uruchamiają serwer Tomcat wbudowany w zależnościach projektu. Będzie on działał na porcie 8080;
- wiersze 38–67: cztery mapery jSON skonfigurowane z różnymi filtrami jSON;
- wiersze 38–41: mapper jSON bez filtrów;
- wiersze 43–49: mapper jSON [jsonMapperShortCreneau] serializuje / deserializuje obiekt [Creneau], pomijając pole [Creneau.medecin];
- wiersze 51–59: mapper jSON [jsonMapperLongRv] serializuje / deserializuje obiekt [Rv], pomijając pole [Rv.creneau.medecin];
- wiersze 61–67: mapper jSON [jsonMapperShortRv] serializuje / deserializuje obiekt [Rv], pomijając pola [Rv.creneau] i [Rv.client];
8.4.11.4. Klasa [ApplicationModel]
![]() |
Klasa [ApplicationModel] posłuży nam do dwóch celów:
- jako pamięć podręczna do przechowywania list lekarzy i pacjentów (klientów);
- jako jednolity interfejs dla kontrolerów;
package rdvmedecins.web.models;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
@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;
private List<String> messages;
// dane konfiguracyjne
private boolean CORSneeded = false;
private boolean secured = false;
@PostConstruct
public void init() {
// pobieramy dane 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(long idRv) {
métier.supprimerRv(idRv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
// metody pobierające i ustawiające
public boolean isCORSneeded() {
return CORSneeded;
}
public boolean isSecured() {
return secured;
}
}
- wiersz 19: adnotacja [@Component] sprawia, że klasa [ApplicationModel] staje się komponentem Spring. Podobnie jak wszystkie dotychczas omówione komponenty Spring (z wyjątkiem @Controller), zostanie utworzony tylko jeden obiekt tego typu (singleton);
- wiersz 20: klasa [ApplicationModel] implementuje interfejs [IMetier];
- wiersze 23–24: Spring wstrzykuje odwołanie do warstwy [métier];
- wiersz 34: adnotacja [@PostConstruct] powoduje, że metoda [init] zostanie wykonana zaraz po utworzeniu instancji klasy [ApplicationModel];
- wiersze 38–39: pobierane są listy lekarzy i klientów z warstwy [métier];
- wiersz 41: jeśli wystąpi wyjątek, komunikaty ze stosu wyjątków są zapisywane w polu z wiersza 17;
Architektura warstwy internetowej zmienia się w następujący sposób:
![]() |
- w warstwie [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 u 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].
8.4.11.5. Klasa Static
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.util.ArrayList;
import java.util.List;
public class Static {
public Static() {
}
// lista komunikatów o błędach wyjątku
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;
}
}
- 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()] z wyjątku [exception] oraz te, które on zawiera – [exception.getCause()].
8.4.11.6. Szkielet kontrolera [RdvMedecinsController]
![]() |
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];
![]() |
Kontroler [RdvMedecinsController] wygląda następująco:
package rdvmedecins.web.controllers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
@Controller
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
// lista komunikatów
private List<String> messages;
// mapery jSON
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
@PostConstruct
public void init() {
// komunikaty o błędach aplikacji
messages = application.getMessages();
}
// lista lekarzy
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {...}
// lista klientów
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {...}
// lista terminów wizyt u lekarza
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
// lista wizyt u lekarza
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {...}
}
- wiersz 35: adnotacja [@Controller] sprawia, że klasa [RdvMedecinsController] staje się kontrolerem Springa, czyli C z MVC;
- wiersze 38–39: obiekt typu [ApplicationModel] zostanie tutaj wstrzyknięty przez Spring. Przedstawiliśmy go już wcześniej;
- wiersze 41–42: Spring wstrzykuje tutaj obiekt typu [RdvMedecinsCorsController]. Obiekt ten przedstawimy dopiero później;
- wiersze 48–58: mapery jSON zdefiniowane w klasie konfiguracyjnej [WebConfig];
- wiersz 60: 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;
- wiersz 63: pobierane są ewentualne komunikaty o błędach z obiektu [ApplicationModel]. Obiekt ten został zainicjowany podczas uruchomienia aplikacji i próbował zapisać w pamięci podręcznej dane lekarzy i klientów. Jeśli operacja ta zakończyła się niepowodzeniem, pojawia się obiekt [messages!=null]. Dzięki temu metody kontrolera będą mogły sprawdzić, czy aplikacja zainicjowała się poprawnie;
- wiersze 67–118: obiekty URL udostępniane przez usługę [web / jSON]. Wszystkie metody zwracają ciąg jSON następującego obiektu typu [Response<T>]:
![]() |
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- właściwości
// status operacji
private int status;
// ewentualne komunikaty o błędach
private List<String> messages;
// treść odpowiedzi
private T body;
// konstruktory
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// metody pobierające i ustawiające
...
}
- wiersz 9: kod błędu: 0 oznacza brak błędu;
- wiersz 11: jeśli [status!=0], to [messages] jest listą komunikatów o błędach;
- wiersz 13: obiekt T zawarty w odpowiedzi. W przypadku błędu wartość T wynosi null;
Obiekt ten jest serializowany do postaci jSON przed wysłaniem do przeglądarki klienta;
- wiersz 67: ujawniony obiekt URL to [/getAllMedecins]. Klient musi użyć metody [GET], aby wysłać żądanie (method = RequestMethod.GET). Gdyby ten kod URL został zażądany przez kod POST, zostałby odrzucony, a Spring MVC wysłałby do klienta internetowego kod błędu HTTP. Metoda sama zwraca odpowiedź do klienta (wiersz 68). Będzie to ciąg znaków (wiersz 67). Nagłówek HTTP [Content-type : application/json; charset=UTF-8] zostanie wysłany do klienta, aby poinformować go, że otrzyma ciąg znaków jSON (wiersz 67);
- wiersz 77: parametr URL jest ustalany przez {idMedecin}. Parametr ten jest pobierany za pomocą adnotacji [@PathVariable] w wierszu 79;
- wiersz 79: 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 105: 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:
Składnia [@RequestBody PostAjouterRv post] (wiersz 105) w połączeniu z faktem, że metoda oczekuje jSON [consumes = "application/json; charset=UTF-8"] w wierszu 103, spowoduje, że ciąg jSON wysłany przez klienta internetowego zostanie deserializowany do obiektu typu [PostAjouterRv]. Wygląda on następująco:
package rdvmedecins.web.models;
public class PostAjouterRv {
// dane z postu
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 107–109 znajduje się podobny mechanizm dla URL i [/supprimerRv]. Wysłany ciąg znaków jSON wygląda następująco:
a typ [PostSupprimerRv] ma następującą postać:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// dane z postu
private long idRv;
// metody pobierające i ustawiające
...
}
8.4.11.7. URL [/getAllMedecins]
URL [/getAllMedecins] jest przetwarzany przez następującą metodę kontrolera [RdvMedecinsController]:
// lista lekarzy
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {
// odpowiedź
Response<List<Medecin>> response;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// lista lekarzy
try {
response = new Response<>(0, null, application.getAllMedecins());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
}
// odpowiedź
return jsonMapper.writeValueAsString(response);
}
- wiersze 9–10: sprawdzane jest, czy aplikacja została poprawnie zainicjowana (messages==null). Jeśli tak nie jest, zwracana jest odpowiedź z kodami status=-1 i body=messages;
- wiersz 13: w przeciwnym razie żądamy listy lekarzy od klasy [ApplicationModel];
- wiersz 19: wysyła się ciąg jSON z odpowiedzi wraz z mapowaniem jSON [jsonMapper], ponieważ klasa [Medecin]nie posiada filtra jSON. Odpowiedź może być bezbłędna (wiersz 14) lub zawierać błąd (wiersz 16). Metoda [application.getAllMedecins()] nie zgłasza wyjątku, ponieważ zwraca jedynie 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 zainicjowała się nieprawidłowo. Zatrzymajmy SGBD i MySQL5, uruchommy usługę internetową, a następnie wywołajmy URL i [/getAllMedecins]:

Rzeczywiście pojawia się błąd. W normalnych warunkach wyświetla się następujący widok:
![]() |
8.4.11.8. URL [/getAllClients]
URL [/getAllClients] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:
// lista klientów
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {
// odpowiedź
Response<List<Client>> response;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// lista klientów
try {
response = new Response<>(0, null, application.getAllClients());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
// odpowiedź
return jsonMapper.writeValueAsString(response);
}
Jest ona analogiczna do omówionej już metody [getAllMedecins]. Uzyskane wyniki są następujące:
![]() |
8.4.11.9. URL [/getAllCreneaux/{idMedecin}]
URL [/getAllCreneaux/{idMedecin}] jest przetwarzana przez następującą metodę kontrolera [RdvMedecinsController]:
// lista terminów u lekarza
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
// odpowiedź
Response<List<Creneau>> response;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// pobieranie danych lekarza
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
Medecin médecin = responseMedecin.getBody();
// terminy u lekarza
try {
response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// odpowiedź
return jsonMapperShortCreneau.writeValueAsString(response);
}
- wiersz 12: lekarz zidentyfikowany przez parametr [id] jest wywoływany przez metodę lokalną:
private Response<Medecin> getMedecin(long id) {
// pobieranie danych lekarza
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// czy lekarz istnieje?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
Z tej metody zwracany jest parametr status do [0,1,2]. Wróćmy do kodu metody [getAllCreneaux]:
- wiersze 13–14: jeśli status!=0, tworzy się odpowiedź z błędem;
- wiersz 16: pobieramy dane lekarza;
- wiersz 19: pobieramy terminy tego lekarza;
- wiersz 25: jako odpowiedź wysyłany jest obiekt [List<Creneau>]. Przypomnijmy definicję klasy [Creneau]:
@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;
// 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 danego lekarza, przy czym w każdym z nich pojawia się ten lekarz. Podczas serializacji tych terminów do tabeli jSON w każdym z nich pojawia się ciąg znaków jSON odpowiadający lekarzowi. Jest to zbędne. Aby sprawdzić proces serializacji, potrzebujemy dwóch rzeczy:
- dostęp do obiektu, który jest serializowany;
- skonfigurować obiekt, który ma zostać zserializowany;
Punkt 1 jest spełniony poprzez wstrzyknięcie do kontrolera konwertera jSON odpowiedniego dla danego obiektu:
@Autowired
private ObjectMapper jsonMapperShortCreneau;
Punkt 2 uzyskuje się poprzez dodanie adnotacji do klasy [Creneau] zdefiniowanej w projekcie [rdvmedecins-metier-dao]:
![]() |
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
- wiersz 3: adnotacja z biblioteki jSON Jackson. Tworzy ona filtr o nazwie [creneauFilter]. Za pomocą tego filtra będziemy mogli programowo zdefiniować pola, które mają zostać zserializowane, a które nie;
Serializacja obiektu [Creneau] odbywa się w następującym wierszu metody [getAllCreneaux]:
// odpowiedź
return jsonMapperShortCreneau.writeValueAsString(response);
Mapper jSON [jsonMapperShortCreneau] został zdefiniowany w klasie [WebConfig] w następujący sposób:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
- wiersz 5: filtr o nazwie [creneauFilter] jest powiązany z filtrem [creneauFilter] z wiersza 4. Filtr ten serializuje obiekt [Creneau] bez jego pola [medecin];
Wynikiem zwracanym przez metodę [getAllCreneaux] jest ciąg znaków jSON typu [Response<List<Creneau>].
Uzyskane wyniki są następujące:
![]() |
lub następujące, jeśli przedział nie istnieje:
![]() |
Z tego przykładu wywnioskujemy następującą zasadę:
- metody serwera WWW / jSON zwracają obiekt typu [Response<T>], który jest serializowany do postaci jSON;
- jeśli typ T posiada jeden lub więcej filtrów jSON, do jego serializacji zostanie użyty mapper z tymi samymi filtrami;
8.4.11.10. URL [/getRvMedecinJour/{idMedecin}/{jour}]
URL [/getRvMedecinJour/{idMedecin}/{jour}] jest przetwarzany przez następującą metodę kontrolera [RdvMedecinsController]:
// lista wizyt u lekarza
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// odpowiedź
Response<List<Rv>> response=null;
boolean erreur = false;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// sprawdzanie daty
Date jourAgenda = null;
if (!erreur) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<List<Rv>>(3, messages, null);
erreur = true;
}
}
Response<Medecin> responseMedecin = null;
if (!erreur) {
// pobieranie danych lekarza
responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
Medecin médecin = responseMedecin.getBody();
// lista jego wizyt
try {
response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// odpowiedź
return jsonMapperLongRv.writeValueAsString(response);
}
- należy przekształcić ciąg znaków jSON na typ [Response<List<Rv>>]. Klasa [Rv] posiada pole [Rv.creneau]. Jeśli pole to zostanie zserializowane, napotkamy filtr jSON [creneauFilter];
- wiersz 47: obiekt typu [Response<List<Rv>>] z wiersza 7 jest serializowany do jSON;
Przeanalizujmy przypadek, w którym lista spotkań została pobrana w wierszu 42. Klasa [Rv] w projekcie [rdvmedecins-metier-dao] jest zdefiniowana w następujący sposób:
@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 pacjentem
@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 przy użyciu trybu [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, aby pobrać pola [client] i [creneau]. Ponadto, dzięki połączeniu [cr.medecin.id=?1], otrzymamy również dane lekarza. Lekarz pojawi się zatem w łańcuchu jSON każdej wizyty. Ta zduplikowana informacja jest jednak zbędna. Widzieliśmy już, jak rozwiązać ten problem za pomocą filtra jSON zastosowanego do obiektu [Creneau]. Ze względu na tryby [FetchType.LAZY] pól [client] i [creneau] klasy [Rv], wkrótce okaże się konieczne zastosowanie filtra jSON w klasie [RV] projektu [rdvmedecins-metier-dao]:
@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...
Sprawdzimy serializację obiektu [Rv] przy użyciu filtra [rvFilter]. Wydaje się, że w tym przypadku nie ma potrzeby stosowania filtrowania, ponieważ potrzebujemy wszystkich pól obiektu typu [Rv]. Niemniej jednak, ponieważ wskazaliśmy, że klasa posiada filtr jSON, musimy zdefiniować go dla każdej serializacji obiektu typu [Rv], w przeciwnym razie wystąpi wyjątek. W tym celu wykorzystujemy następujący mapper jSON zdefiniowany w klasie [rdvMedecinsController]:
@Autowired
private ObjectMapper jsonMapperLongRv;
Ten mapper jest zdefiniowany w następujący sposób w klasie konfiguracyjnej [WebConfig]:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
- wiersz 4: wskazujemy, że wszystkie pola obiektu [Rv] mają zostać zserializowane;
- wiersz 5: określamy, że w obiekcie [Creneau] nie należy serializować pola [medecin];
- wiersz 6: dodajemy dwa filtry [rvFilter] i [creneauFilter] do filtrów jSON obiektu [jsonMapperLongRv];
Otrzymane wyniki są następujące:
![]() |
lub te z dniem bez umówionego terminu:
![]() |
lub te z nieprawidłową datą:
![]() |
lub te z nieprawidłowym lekarzem:
![]() |
8.4.11.11. URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
URL [/getAgendaMedecinJour/{idMedecin}/{jour}] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// odpowiedź
Response<AgendaMedecinJour> response = null;
boolean erreur = false;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// sprawdzana jest data
Date jourAgenda = null;
if (!erreur) {
// sprawdzana jest data
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
erreur = true;
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(3, messages, null);
}
}
// pobieranie danych lekarza
Medecin médecin = null;
if (!erreur) {
// pobieranie danych lekarza
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
médecin = responseMedecin.getBody();
}
}
// pobieranie kalendarza
if (!erreur) {
try {
response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// odpowiedź
return jsonMapperLongRv.writeValueAsString(response);
}
- wiersze 6, 49: zwracamy ciąg jSON typu [AgendaMedecinJour], zamknięty w obiekcie [Response];
Typ [AgendaMedecinJour] ma następującą postać:
public class AgendaMedecinJour implements Serializable {
// pola
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
Typ [CreneauMedecinJour] ma następującą postać:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// pola
private Creneau creneau;
private Rv rv;
Pola [creneau] i [rv] mają filtry jSON, które należy skonfigurować. Tak właśnie działa wiersz 49 metody [getAgendaMedecinJour], która wykorzystuje mapper jSON [jsonMapperLongRv], o którym już była mowa:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
Otrzymane wyniki są następujące:
![]() |
Powyżej widać, że 28 stycznia 2015 r. dr PELISSIER ma wizytę u pani Brigitte BISTROU o godz. 8:20;
lub te, jeśli data jest błędna:
![]() |
lub te, jeśli numer lekarza jest nieprawidłowy:
![]() |
8.4.11.12. URL [/getMedecinById/{id}]
URL [/getMedecinById/{id}] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
// odpowiedź
Response<Medecin> response;
// stan aplikacji
if (messages != null) {
response = new Response<Medecin>(-1, messages, null);
} else {
response = getMedecin(id);
}
// odpowiedź
return jsonMapper.writeValueAsString(response);
}
- wiersze 5, 13: metoda zwraca ciąg jSON typu [Medecin]. Ten typ nie posiada adnotacji filtru jSON. Dlatego w wierszu 14 używa się mapera jSON bez filtrów;
W wierszu 10 metoda [getMedecin] wygląda następująco:
private Response<Medecin> getMedecin(long id) {
// pobieramy dane lekarza
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// czy lekarz istnieje?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
Uzyskane wyniki są następujące:
![]() |
lub te, jeśli numer lekarza jest nieprawidłowy:
![]() |
8.4.11.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, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
// odpowiedź
Response<Client> response;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
response = getClient(id);
}
// odpowiedź
return jsonMapper.writeValueAsString(response);
}
- wiersze 5, 13: metoda zwraca ciąg jSON typu [Client]. Ten typ nie posiada adnotacji filtrów jSON. Dlatego w wierszu 13 używa się mapera jSON bez filtrów;
W wierszu 11 metoda [getClient] wygląda następująco:
private Response<Client> getClient(long id) {
// pobieramy dane klienta
Client client = null;
try {
client = application.getClientById(id);
} catch (RuntimeException e1) {
return new Response<Client>(1, Static.getErreursForException(e1), null);
}
// czy klient istnieje?
if (client == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le client d'id [%s] n'existe pas", id));
return new Response<Client>(2, messages, null);
}
// ok
return new Response<Client>(0, null, client);
}
Uzyskane wyniki są następujące:
![]() |
lub te, jeśli numer klienta jest nieprawidłowy:
![]() |
8.4.11.14. URL [/getCreneauById/{id}]
URL [/getCreneauById/{id}] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
// odpowiedź
Response<Creneau> response;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// zwracamy termin
response = getCreneau(id);
}
// odpowiedź
return jsonMapperShortCreneau.writeValueAsString(response);
}
- wiersze 5, 14: metoda zwraca ciąg znaków jSON typu [Response<Creneau>];
W wierszu 8 metoda [getCreneau] wygląda następująco:
private Response<Creneau> getCreneau(long id) {
// pobieramy termin
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
// czy przedział czasowy istnieje?
if (créneau == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
return new Response<Creneau>(2, messages, null);
}
// ok
return new Response<Creneau>(0, null, créneau);
}
Przypomnijmy kod jednostki [Creneau]:
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
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;
- wiersze 14–16: ponieważ pole [medecin] znajduje się w trybie [fetch = FetchType.LAZY], nie jest ono pobierane podczas wyszukiwania przedziału czasowego na podstawie jego [id]. Konieczne jest zatem wykluczenie go z serializacji. Bez tego wykluczenia pojawia się wyjątek. Wynika to z faktu, że obiekt serializacji [mapper] wywoła metodę [getMedecin] w celu uzyskania pola [medecin]. Jednak w przypadku implementacji JPA / Hibernate tryb [fetch = FetchType.LAZY] pola [medecin] zwrócił obiekt [Creneau], którego metoda [getMedecin] jest zaprogramowana tak, aby pobrać lekarza z kontekstu JPA. Nazywa się to obiektem [proxy]. Przypomnijmy sobie jednak architekturę aplikacji internetowej:
![]() |
Kontroler znajduje się w bloku [Contrôleurs / Actions]. Gdy znajdujemy się w tym bloku, pojęcie kontekstu JPA przestaje mieć znaczenie. Kontekst ten jest tworzony na czas operacji warstwy [DAO]. Nie istnieje on dłużej. Dlatego też, gdy kontroler próbuje uzyskać dostęp do kontekstu JPA, pojawia się wyjątek wskazujący, że kontekst ten został zamknięty. Aby uniknąć tego wyjątku, należy zapobiec serializacji pola [medecin] klasy [Rv]. Tak właśnie działa mapper jSON [jsonMapperShortCreneau]:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
Otrzymane wyniki są następujące:
![]() |
lub te, jeśli numer przedziału czasowego jest nieprawidłowy:
![]() |
8.4.11.15. URL [/getRvById/{id}]
URL [/getRvById/{id}] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
// odpowiedź
Response<Rv> response;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// pobieramy termin wizyty
response = getRv(id);
}
// odpowiedź
return jsonMapperShortRv.writeValueAsString(response);
}
- wiersze 5, 14: metoda zwraca ciąg znaków jSON typu [Response<Rv>];
W wierszu 11 metoda [getRv] wygląda następująco:
private Response<Rv> getRv(long id) {
// pobieranie Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
// Czy Rv istnieje?
if (rv == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
return new Response<Rv>(2, messages, null);
}
// ok
return new Response<Rv>(0, null, rv);
}
Klasa [Rv] posiada dwa pola z adnotacją [fetch = FetchType.LAZY]: pola [creneau] i [client]. Pola te nie są zatem pobierane podczas wyszukiwania klasy [Rv] na podstawie jej klucza głównego. Z tych samych powodów, co poprzednio, należy je zatem wykluczyć z serializacji. Tak właśnie działa poniższy mapper [jsonMapperShortRv] zdefiniowany w klasie [WebConfig]:
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
Otrzymane wyniki są następujące:
![]() |
lub te, jeśli numer spotkania jest nieprawidłowy:
![]() |
8.4.11.16. URL [/ajouterRv]
URL [/ajouterRv] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
// odpowiedź
Response<Rv> response = null;
boolean erreur = false;
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// pobieranie zapisanych wartości
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// pobieranie wartości zaksięgowanych
jour = post.getJour();
idCreneau = post.getIdCreneau();
idClient = post.getIdClient();
// sprawdzamy datę
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(6, messages, null);
erreur = true;
}
}
// pobieramy przedział czasowy
Response<Creneau> responseCréneau = null;
if (!erreur) {
// pobieramy termin
responseCréneau = getCreneau(idCreneau);
if (responseCréneau.getStatus() != 0) {
erreur = true;
response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
}
}
// pobieramy klienta
Response<Client> responseClient = null;
Creneau créneau = null;
if (!erreur) {
créneau = (Creneau) responseCréneau.getBody();
// pobieramy klienta
responseClient = getClient(idClient);
if (responseClient.getStatus() != 0) {
erreur = true;
response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
}
}
if (!erreur) {
Client client = responseClient.getBody();
// dodaje się termin spotkania
try {
response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(5, Static.getErreursForException(e1), null);
}
}
// odpowiedź
return jsonMapperLongRv.writeValueAsString(response);
}
- wiersze 5, 67: metoda musi zwrócić ciąg znaków jSON typu [Response<Rv>];
- wiersz 3: adnotacja [@RequestBody PostAjouterRv post] pobiera treść z POST i umieszcza ją w parametrze [PostAjouterRv post]. Treść ta pochodzi z jSON [consumes = "application/json; charset=UTF-8"] i zostanie automatycznie deserializowana do następującego typu [PostAjouterRv]:
public class PostAjouterRv {
// dane z postu
private String jour;
private long idClient;
private long idCreneau;
...
- następnie pojawia się kod, który już wcześniej występował w takiej czy innej formie;
- wiersz 67: konfiguracja filtrów jSON, [creneauFilter] i [rvFilter]. Metoda przekształca ciąg jSON w typ [Response<Rv>], gdzie Rv zostało uzyskane w wierszu 61. Obiekt [Rv] zawiera obiekt [Creneau] oraz obiekt [Client]. Obiekt [Creneau] ma zależność [FetchType.LAZY] od obiektu [Medecin] i został pobrany w wierszach 36–44. Został on wyszukany w kontekście JPA na podstawie klucza głównego i uzyskano go bez zależności [FetchType.LAZY]. Ostatecznie
- obiekt [Rv] posiada wszystkie swoje zależności. Można je zserializować;
- obiekt [Creneau] nie posiada swojej zależności [medecin]. Należy zatem upewnić się, że nie zostanie ona zserializowana;
Mapper jSON [jsonMapperLongRv] zdefiniowany w klasie [WebConfig] spełnia te ograniczenia:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
Wyniki uzyskane przy użyciu klienta [Advanced Rest Client] wyglądają następująco:
![]() |
- w [1], URL z POST;
- z [2] na POST;
- w [3] – wartość opublikowana;
- w [4a] ta zaksięgowana wartość pochodzi z jSON;
![]() |
- w [4b] klient wskazuje, że wysyła jSON;
- w [5] serwer wskazuje, że zwraca jSON;
![]() |
- w [6] znajduje się odpowiedź serwera jSON, która przedstawia dodane spotkanie. Widoczny jest tam identyfikator [id] dodanego spotkania;
W przypadku nieistniejącego numeru przedziału czasowego otrzymujemy następujący wynik:
![]() |
8.4.11.17. URL [/supprimerRv]
URL [/supprimerRv] jest przetwarzane przez następującą metodę kontrolera [RdvMedecinsController]:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
// odpowiedź
Response<Void> response = null;
boolean erreur = false;
// nagłówki CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stan aplikacji
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// pobieramy wartości przesłane
long idRv = post.getIdRv();
// pobieranie wartości rv
if (!erreur) {
Response<Rv> responseRv = getRv(idRv);
if (responseRv.getStatus() != 0) {
response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
// usuwanie rv
try {
application.supprimerRv(idRv);
response = new Response<Void>(0, null, null);
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// odpowiedź
return jsonMapper.writeValueAsString(response);
}
- wiersz 5: typ [Void] jest klasą odpowiadającą typowi pierwotnemu [void];
- wiersze 5, 34: metoda zwraca ciąg jSON typu [Response<Void>], który nie posiada filtrów jSON. Dlatego w wierszu 34 używa się mapera jSON bez filtrów;
- w wierszu 3: metoda przyjmuje jako parametr treść POST, tj. wartość wysłaną. Jest ona odbierana w postaci jSON [consumes = "application/json; charset=UTF-8"] i automatycznie deserializowana do następującego typu [PostSupprimerRv]:
public class PostSupprimerRv {
// dane z wysyłania
private long idRv;
- wiersz 28: gdy usunięcie zakończyło się powodzeniem, wysyłana jest odpowiedź o wartości [status=0];
Uzyskane wyniki są następujące:
![]() |
![]() |
- w [5] pole [status=0] wskazuje, że usunięcie zakończyło się powodzeniem;
W przypadku nieistniejącego numeru spotkania otrzymujemy następujący wynik:
![]() |
Zakończyliśmy pracę z kontrolerem. Teraz przyjrzyjmy się, jak uruchomić projekt.
8.4.11.18. Klasa wykonywalna serwisu internetowego
![]() |
Klasa [Boot] [1] wygląda następująco:
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 wywoływana jest metoda statyczna [SpringApplication.run], której pierwszym parametrem jest klasa konfiguracyjna projektu [AppConfig]. Metoda ta przeprowadzi autokonfigurację projektu, uruchomi serwer Tomcat wbudowany w zależnościach i wdroży na nim kontroler [RdvMedecinsController].
Logi są kontrolowane przez następujące pliki [2]:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- kodery mają domyślnie przypisany typ ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- kontrola poziomu logów -->
<root level="info"> <!-- wyłączone, informacyjne, debugowanie, ostrzeżenie -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- wiersz 9: ogólny poziom logowania jest ustawiony na [info];
[application.properties]
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false
Wiersze 1–2 umożliwiają ustawienie określonego poziomu logowania dla niektórych elementów aplikacji:
- wiersz 1: chcemy logi z warstwy [web];
- wiersz 2: nie chcemy logów z warstwy [JPA];
- wiersz 3: brak banera Spring Boot;
Podczas działania generowane są następujące logi:
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
11:06:04.732 [main] INFO rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
- wiersz 18: serwer Tomcat jest aktywny;
- wiersz 21: trwa inicjalizacja środowiska Spring;
- wiersze 27–38: wykryto zasoby URL udostępniane przez serwis internetowy;
- wiersz 44: serwer Tomcat jest gotowy i oczekuje na żądania na porcie 8080;
Jeśli zmodyfikujemy plik [application.properties] w następujący sposób:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
otrzymujemy następujące logi:
Jeśli dodatkowo zmodyfikujemy plik [logback.xml] w następujący sposób:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- enkodery mają domyślnie przypisany typ ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- kontrola poziomu logów -->
<root level="off"> <!-- wyłączone, informacyjne, debugowanie, ostrzeżenie -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
otrzymujemy następujące logi:
Widać więc, że mamy pewną kontrolę nad logami wyświetlanymi w konsoli. Poziom [info] jest często odpowiednim poziomem logów.
Mamy teraz 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.
8.4.12. Wprowadzenie do Spring Security
Ponownie zaimportujemy przewodnik Spring, wykonując kroki od 1 do 3 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;
8.4.12.1. Konfiguracja Maven
Projekt [3] jest projektem Maven. Przyjrzyjmy się jego plikowi [pom.xml], aby poznać jego zależności:
<?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-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- tag::security[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- end::security[] -->
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- wiersze 10–14: projekt jest projektem Spring Boot;
- wiersze 17–20: zależność od frameworka [Thymeleaf];
- wiersze 22–25: zależność od frameworka Spring Security;
8.4.12.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>
- wiersz 12: atrybut [th:href="@{/hello}"] wygeneruje atrybut [href] w tagu <a>. Wartość [@{/hello}] wygeneruje ścieżkę [<context>/hello], gdzie [context] stanowi 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>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
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="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
</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 wysł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>
<div>
You have been logged out.
</div>
<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="ef809b0a-88b4-4db9-bc53-342216b77632" />
</form>
</body>
</html>
Warto zwrócić uwagę, że w wierszu 28 Thymeleaf dodał ukryte pole o nazwie [_csrf].
8.4.12.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 |
/templates/home.html | |
/templates/hello.html | |
/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 [java] 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.
8.4.12.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 obiektów URL z uprawnieniami dostępu. Dokonano w niej następujących powiązań:
URL | reguła | kod |
dostęp bez uwierzytelniania | | |
dostęp wyłącznie po uwierzytelnieniu |
- 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 jako [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;
8.4.12.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.
8.4.12.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:
Po kliknięciu linku pojawi się żądanie URL [/hello]. Jest on chroniony:
URL | zasada | kod |
dostęp bez uwierzytelniania | | |
dostęp wyłącznie po uwierzytelnieniu |
Aby uzyskać dostęp, należy się uwierzytelnić. Spring Security przekieruje wówczas przeglądarkę klienta na stronę uwierzytelniającą. Zgodnie z widoczną 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:
- 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 wprowadzimy 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]:
![]() |
- w [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 kodu [hello.html]:
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 POST na stronie URL [/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];
- w [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>
8.4.12.7. Conclusion
W poprzednim 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.
8.4.13. Wdrażanie zabezpieczeń w serwisie internetowym do rezerwacji terminów
8.4.13.1. Baza danych
Baza danych [rdvmedecins] zostaje rozbudowana, aby uwzględnić użytkowników, ich hasła i role. Pojawiają się trzy nowe tabele:

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 formacie 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]:
![]() |
8.4.13.2. Nowy projekt STS powstał na bazie projektu [métier, DAO, JPA]
Projekt [rdvmedecins-metier-dao] zmienia się w następujący sposób:
![]() |
- do [1]: nowy projekt;
- do [2]: zmiany wynikające z uwzględnienia kwestii bezpieczeństwa zostały zebrane w jednym pakiecie o nazwie [rdvmedecins.security]. Te nowe elementy należą do warstw [JPA] i [DAO], ale dla uproszczenia zostały zebrane w jednym pakiecie.
8.4.13.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;
// konstruktor
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 ta rozszerza klasę [AbstractEntity], już wykorzystywaną dla innych encji;
- wiersze 13–15: nie podano nazw kolumn, ponieważ noszą one te same nazwy co 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;
// konstruktory
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];
8.4.13.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 na podstawie loginu i hasła;
- wiersz 20: wyszukiwanie użytkownika na podstawie loginu;
Interfejs [RoleRepository] zarządza dostępem do jednostek [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;
8.4.13.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 pozwala uzyskać szczegóły dotyczące tego użytkownika (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ę [AppUserDetailsService]:
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 nazwy logowania;
- wiersze 20–22: jeśli użytkownik nie zostanie znaleziony, zgłaszany jest wyjątek;
- wiersz 24: tworzony jest obiekt [AppUserDetails] i renderowany. Jest on rzeczywiście typu [UserDetails] (wiersz 16);
8.4.13.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 pierwotnym 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 nazwa użytkownika nie istnieje, tworzymy ją w bazie danych;
- wiersz 44: szyfrujemy hasło. Wykorzystujemy tutaj klasę [BCrypt] z biblioteki Spring Security (wiersz 4). Potrzebujemy więc 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] posiada hasło [admin] oraz rolę [ROLE_ADMIN], korzystając z usługi [appUserDetailsService];
Testy zakończyły się powodzeniem, generując następujące logi:
8.4.13.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.
8.4.13.8. Projekt STS warstwy [web]
![]() |
Projekt [rdvmedecins-webjson] rozwija się w następujący sposób z projektu [1]:
![]() |
Główne zmiany należy wprowadzić w pakiecie [rdvmedecins.web.config], gdzie należy skonfigurować Spring Security. Istnieją również inne, mniej istotne zmiany w klasach [AppConfig] i [ApplicationModel]. 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: zdefiniujemy klasę rozszerzającą klasę [WebSecurityConfigurerAdapter];
- wiersz 13: zdefiniuj metodę [configure(HttpSecurity http)], która określa uprawnienia dostępu do poszczególnych elementów 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.context.annotation.Configuration;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Autowired
private ApplicationModel application;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// uwierzytelnianie odbywa się za pomocą komponentu [appUserDetailsService]
// hasło jest szyfrowane za pomocą algorytmu skrótu BCrypt
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// czy aplikacja jest bezpieczna?
if (application.isSecured()) {
// hasło jest przesyłane w nagłówku Authorization: Basic xxxx
http.httpBasic();
// metoda HTTP OPTIONS musi być autoryzowana dla wszystkich
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// tylko rola ADMIN może korzystać z aplikacji
http.authorizeRequests() //
.antMatchers("/", "/**") // wszystkie URL
.hasRole("ADMIN");
// brak sesji
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- wiersz 15: klasa [SecurityConfig] jest klasą konfiguracyjną Spring;
- wiersz 16: w celu zapewnienia bezpieczeństwa projektu;
- wiersze 19–20: wstrzykiwana jest klasa [AppUserDetails], która zapewnia użytkownikom dostęp do aplikacji;
- wiersze 21–22: wstrzykiwana jest klasa [ApplicationModel], która służy jako pamięć podręczna aplikacji internetowej. Postanawiamy ją tutaj również wykorzystać, aby skonfigurować aplikację internetową w jednym miejscu. To właśnie ona definiuje wartość logiczną [isSecured] z wiersza 36. Ta wartość logiczna zabezpiecza (true) lub nie zabezpiecza (false) aplikację internetową;
- wiersze 25–29: metoda [configure(HttpSecurity http)] definiuje użytkowników i ich role. Otrzymuje ona jako parametr typ [AuthenticationManagerBuilder]. Parametr ten jest uzupełniony o dwie informacje (wiersz 28):
- odwołanie do usługi [appUserDetailsService] z wiersza 20, 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 38–47: metoda [configure(HttpSecurity http)] definiuje uprawnienia dostępu do URL serwisu internetowego;
- wiersz 34: 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. W tym przypadku mechanizm ten jest wyłączony. W połączeniu z wartością logiczną (isSecured=false) pozwala to na korzystanie z aplikacji internetowej bez zabezpieczeń;
- wiersz 38: włączamy tryb uwierzytelniania za pomocą nagłówka HTTP. Klient będzie musiał wysłać następujący nagłówek HTTP:
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 logowania [admin] i haśle [admin] wyśle następujący nagłówek HTTP w celu uwierzytelnienia:
- wiersze 40–42: 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;
- wiersz 47: hasło użytkownika może być zapisane w sesji lub nie. Jeśli jest zapisane, użytkownik musi uwierzytelnić się tylko za pierwszym razem. Przy kolejnych próbach nie będzie proszony o podanie danych uwierzytelniających. W tym przypadku wybrano tryb bez sesji. Każde żądanie musi być opatrzone danymi uwierzytelniającymi;
Klasa [AppConfig], która konfiguruje całą aplikację, zmienia się w następujący sposób:
![]() |
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- zmiana następuje w wierszu 11: dodano klasę konfiguracyjną [SecurityConfig];
Na koniec klasa [ApplicationModel] zostaje wzbogacona o zmienną logiczną:
@Component
public class ApplicationModel implements IMetier {
...
// dane konfiguracyjne
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- w wierszu 6: ustawia się wartość zmiennej logicznej [secured] na [true / false] w zależności od tego, czy chce się włączyć zabezpieczenie, czy nie.
8.4.13.9. Testy serwisu internetowego
Przetestujemy usługę internetową przy użyciu klienta Chrome o identyfikatorze [Advanced Rest Client]. Będziemy musieli określić nagłówek uwierzytelniający o identyfikatorze HTTP:
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
byte[] data = Base64.encode(chaîne.getBytes());
// wyświetla się jego kodowanie Base64
System.out.println(new String(data));
}
}
Jeśli uruchomimy ten program z dwoma argumentami [admin admin]:
![]() |
otrzymujemy następujący wynik:
Teraz, gdy wiemy już, jak wygenerować nagłówek uwierzytelniający HTTP, uruchamiamy zabezpieczoną usługę internetową:
@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;
Następnie za pomocą przeglądarki Chrome [Advanced Rest Client] wysyłamy żądanie o listę wszystkich lekarzy:
![]() |
- w [1] żądamy URL dotyczących 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] znajduje się lista nagłówków HTTP związanych z zabezpieczeniem aplikacji internetowej;
Otrzymujemy listę lekarzy:
![]() |
Spróbujmy teraz wysłać żądanie HTTP z nieprawidłowym nagłówkiem uwierzytelniającym. Odpowiedź jest wówczas następująca:
![]() |
- w [1] i [3]: nagłówek uwierzytelniający HTTP;
- w [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:
![]() |
- 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;
Zabezpieczona usługa internetowa jest już gotowa do działania. Uzupełnimy ją tak, aby zezwalała na żądania międzydomenowe. Potrzeba ta pojawiła się w dokumencie [Tutoriel AngularJS / Spring 4] i chociaż nie występuje ona w tym przypadku, to i tak ją uwzględnimy.
8.4.14. Wdrożenie żądań międzydomenowych
Przyjrzyjmy się problemowi żądań międzydomenowych. W dokumencie [Tutoriel AngularJS / Spring 4] opisano tworzenie aplikacji klient-serwer, w której klientem jest aplikacja AngularJS:
![]() |
- strony HTML / CSS / JS aplikacji Angular pochodzą z serwera [1];
- w [2] usługa [dao] wysyła żądanie do innego serwera, serwera [2]. Jest to jednak zabronione przez przeglądarkę, w której uruchomiona jest aplikacja Angular, ponieważ stanowi to lukę w zabezpieczeniach. Aplikacja może wysyłać zapytania wyłącznie do serwera, z którego pochodzi, czyli do serwera [1];
W rzeczywistości nieprawdziwe jest stwierdzenie, że przeglądarka zabrania aplikacji Angular wysyłania zapytania do serwera [2]. Aplikacja wysyła do niego zapytanie, aby dowiedzieć się, czy serwer zezwala na wysyłanie zapytań przez klienta spoza własnej domeny. Tę technikę udostępniania nazywa się CORS (Cross-Origin Resource Sharing). Serwer [2] wyraża zgodę, wysyłając odpowiednie nagłówki HTTP.
Aby zilustrować problemy, z jakimi można się spotkać, stworzymy aplikację klient-serwer, w której:
- serwerem będzie nasz serwer WWW / jSON;
- klientem będzie prosta strona HTML wyposażona w kod JavaScript, który będzie wysyłał żądania do serwera WWW / jSON;
8.4.14.1. Projekt klienta
![]() |
Projekt jest projektem Maven zawierającym następujący plik: [pom.xml]:
<?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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-cors</name>
<description>Client for webjson server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- wyszukiwanie elementu nadrzędnego w repozytorium -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.Client</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- wiersze 14–19: jest to projekt Spring Boot;
- wiersze 29–32: wykorzystywana jest zależność [spring-boot-starter-web], która zawiera serwer Tomcat oraz Spring MVC;
Strona HTML wygląda następująco:
![]() |
Jest ona generowana przez następujący kod:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
<h2>Client du service web / jSON</h2>
<form id="formulaire">
<!-- metoda HTTP -->
Méthode HTTP :
<!-- -->
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<!-- -->
<input type="radio" id="post" name="method" value="post" />POST
<!-- URL -->
<br /> <br />URL cible : <input type="text" id="url" size="30"><br />
<!-- wartość zaksięgowana -->
<br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
<!-- przycisk zatwierdzenia -->
<br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
</form>
<hr />
<h2>Réponse du serveur</h2>
<div id="response"></div>
</body>
</html>
- wiersz 6: importujemy bibliotekę jQuery;
- wiersz 7: importujemy kod, który zamierzamy napisać;
Kod [client.js] wygląda następująco:
// dane ogólne
var url;
var posted;
var response;
var method;
function requestServer() {
// pobieramy informacje z formularza
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// ręczne wywołanie Ajax
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// ręczne wywołanie Ajax
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + adres URL,
type : 'GET',
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// wynik tekstowy
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// błąd systemowy
response.text(jqXHR.responseText);
}
})
}
function doPost(url, posted) {
// ręczne wywołanie Ajax
$.ajax({
headers : {
'„Authorization”: „Basic YWRtaW46YWRtaW4=”
},
url : 'http://localhost:8080' + adres URL,
type : 'POST',
contentType : 'application/json',
data : posted,
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// wynik tekstowy
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// błąd systemowy
response.text(jqXHR.responseText);
}
})
}
// podczas ładowania dokumentu
$(document).ready(function() {
// pobierane są odniesienia do komponentów strony
url = $("#url");
posted = $("#posted");
response = $("#response");
});
Zostawiamy czytelnikowi zadanie zrozumienia tego kodu. Wszystkie elementy pojawiały się już wcześniej w tym czy innym kontekście. Niektóre wiersze wymagają jednak wyjaśnienia:
- wiersz 11:
- [document] oznacza dokument załadowany przez przeglądarkę, tzw. DOM (Document Object Model),
- [document.forms[0]] oznacza pierwszy formularz w dokumencie; dokument może zawierać kilka formularzy. W tym przypadku jest tylko jeden,
- [document.forms[0].elements['method']] oznacza element formularza, który posiada atrybut [name='method']. Są dwa takie elementy:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- wiersz 11:
- [document.forms[0].elements['method'].value] to wartość, która zostanie przesłana dla komponentu posiadającego atrybut [name='method']. Wiadomo, że wysyłana wartość to wartość atrybutu [value] zaznaczonego przycisku opcji. W tym przypadku będzie to więc jeden z ciągów znaków ['get', 'post'];
- wiersze 23–25: nawiązujemy połączenie z serwerem, który wymaga nagłówka HTTP [Authorization: Basic code]. Tworzymy ten nagłówek dla użytkownika [admin / admin], który jako jedyny może wysyłać zapytania do serwera;
- wiersz 26: użytkownik wprowadzi dane o typie URL, takie jak [/getAllMedecins, /supprimerRv, ...]. Należy zatem uzupełnić te dane o typie URL;
- wiersz 28: serwer zwraca jSON, który jest formą tekstową. Jako typ wyniku podaje się [text/plain], aby wyświetlić go w takiej postaci, w jakiej został odebrany;
- wiersz 33: wyświetlenie tekstowej odpowiedzi serwera;
- wiersz 39: wyświetlenie ewentualnego komunikatu o błędzie w formacie tekstowym;
- wiersz 52: w celu wskazania, że klient wysyła jSON;
W stworzonej aplikacji klient-serwer:
- klientem jest aplikacja internetowa dostępna pod adresem URL [http://localhost:8081]. Jest to aplikacja, którą właśnie tworzymy;
- serwer to aplikacja internetowa dostępna pod adresem URL [http://localhost:8080]. To nasz serwer internetowy / jSON;
Ponieważ klient nie łączy się z tego samego portu co serwer, pojawia się problem żądań międzydomenowych. [http://localhost:8080] i [http://localhost:8081] to dwie różne domeny.
Aplikacja Spring Boot to aplikacja konsolowa uruchamiana przez następującą klasę wykonywalną [Client]:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Client.class, args);
}
// strony statyczne
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
// konfiguracja dispatcherServlet
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
// wbudowany serwer Tomcat
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8081);
}
}
- wiersz 14: klasa [Client] jest klasą konfiguracyjną Spring;
- wiersz 15: konfiguruje się aplikację Spring MVC. Ta adnotacja powoduje wprowadzenie pewnych automatycznych ustawień;
- wiersz 16: aby zmienić niektóre wartości domyślne frameworka Spring MVC, należy rozszerzyć klasę [WebMvcConfigurerAdapter];
- wiersze 23–26: metoda [addResourceHandlers] pozwala określić katalogi, w których znajdują się zasoby statyczne (html, css, js itp.) aplikacji. W tym przypadku wskazano katalog [static] znajdujący się w ścieżce Classpath projektu:
![]() |
- wiersze 29–37: konfiguracja komponentu [dispatcherServlet], który wskazuje serwlet Springa MVC;
- wiersze 40–43: wbudowany serwer Tomcat będzie działał na porcie 8081;
8.4.14.2. URL [/getAllMedecins]
Uruchamiamy:
- serwer WWW / JSON na porcie 8080;
- klienta tego serwera na porcie 8081;
następnie wysyłamy żądanie do URL [http://localhost:8081/client.html] [1]:
![]() |
- w [2], wykonujemy GET na URL [http://localhost:8080/getAllMedecins];
Nie otrzymujemy odpowiedzi z serwera. Po sprawdzeniu konsoli programistycznej (Ctrl-Shift-I) widzimy błąd:
![]() |
- w [1], znajdujemy się w zakładce [Network];
- W przypadku [2] widać, że wysłane zapytanie HTTP to nie [GET], lecz [OPTIONS]. W przypadku żądania międzydomenowego przeglądarka sprawdza u serwera, czy spełnionych jest pewna liczba warunków, wysyłając do niego żądanie o numerze HTTP [OPTIONS]. W tym przypadku żądania to te wskazane przez kropki [5-6];
- w przypadku [5] przeglądarka pyta, czy adres docelowy URL jest dostępny za pośrednictwem adresu GET. Nagłówek żądania [Access-Control-Request-Method] wymaga odpowiedzi z nagłówkiem HTTP [Access-Control-Allow-Methods] wskazującym, że żądana metoda jest akceptowana;
- w [5] przeglądarka wysyła nagłówek HTTP [Origin: http://localhost:8081]. Nagłówek ten wymaga odpowiedzi w postaci nagłówka HTTP [Access-Control-Allow-Origin], wskazującego, że wskazane źródło jest akceptowane;
- w nagłówku [6] przeglądarka pyta, czy nagłówki HTTP, [accept] i [authorization] są akceptowane. Nagłówek żądania [Access-Control-Request-Headers] oczekuje odpowiedzi z nagłówkiem HTTP lub [Access-Control-Allow-Headers], wskazującym, że żądane nagłówki są akceptowane;
- w przypadku [3] występuje błąd. Po kliknięciu ikony pojawia się błąd [4];
- w [4] komunikat wskazuje, że serwer nie wysłał nagłówka HTTP [Access-Control-Allow-Origin], który określa, czy źródło żądania jest akceptowane;
- w [7] widać, że serwer rzeczywiście nie wysłał tego nagłówka. W rezultacie przeglądarka odmówiła wykonania żądania HTTP GET, o które pierwotnie proszono;
Musimy zmodyfikować serwer WWW / jSON. Wprowadzamy pierwszą zmianę w [ApplicationModel], który jest jednym z elementów konfiguracji serwisu internetowego:
![]() |
@Component
public class ApplicationModel implements IMetier {
...
// dane konfiguracyjne
private boolean corsAllowed = true;
private boolean secured = true;
...
public boolean isCorsAllowed() {
return corsAllowed;
}
- wiersz 6: tworzymy zmienną logiczną, która określa, czy akceptujemy klientów spoza domeny serwera;
- wiersze 10–12: metoda uzyskiwania dostępu do tej informacji;
Następnie tworzymy nowy kontroler Spring o nazwie MVC:
![]() |
Klasa [RdvMedecinsCorsController] wygląda następująco:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// wysyłanie opcji do klienta
public void sendOptions(String origin, HttpServletResponse response) {
// Czy CORS jest dozwolone?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// ustawiamy nagłówek CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// zezwalamy na niektóre nagłówki
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// zezwalamy na GET
response.addHeader("Access-Control-Allow-Methods", "GET");
}
// lista lekarzy
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
}
- wiersze 12–13: klasa [RdvMedecinsCorsController] jest kontrolerem Spring;
- wiersze 33–36: definiują akcję przetwarzającą URL [/getAllMedecins], gdy jest ona wywoływana za pomocą polecenia HTTP [OPTIONS];
- wiersz 34: metoda [getAllMedecins] przyjmuje jako parametry:
- obiekt [@RequestHeader(value = "Origin", required = false)], który pobierze nagłówek HTTP [Origin] z żądania. Nagłówek ten został wysłany przez nadawcę żądania:
Wskazuje się, że nagłówek HTTP [Origin] jest opcjonalny [required = false]. W takim przypadku, jeśli nagłówek nie występuje, parametr [String origin] przyjmie wartość null. Przy wartości domyślnej [required = true], w przypadku braku nagłówka generowany jest wyjątek. Chcieliśmy uniknąć tej sytuacji;
- wiersz 34:
- obiekt [HttpServletResponse response], który zostanie wysłany do klienta, który złożył żądanie;
Te dwa parametry są wstrzykiwane przez Spring;
- wiersz 35: przetwarzanie żądania przekazujemy metodzie z wierszy 19–30;
- wiersze 15–16: obiekt [ApplicationModel] jest wstrzykiwany;
- wiersze 21–23: jeśli aplikacja jest skonfigurowana tak, aby akceptować żądania międzydomenowe, a nadawca wysłał nagłówek HTTP [Origin] i jeśli źródło to zaczyna się od [http://localhost], wtedy żądanie międzydomenowe zostanie zaakceptowane, w przeciwnym razie zostanie odrzucone;
- wiersz 25: jeśli klient znajduje się w domenie [http://localhost:port], wysyłamy nagłówek HTTP:
co oznacza, że serwer akceptuje pochodzenie klienta;
- wiersz 25: w żądaniu HTTP [OPTIONS] zaznaczyliśmy dwa konkretne nagłówki HTTP:
Na nagłówek HTTP [Access-Control-Request-X] serwer odpowiada nagłówkiem HTTP [Access-Control-Allow-X], w którym wskazuje, co jest dozwolone. Wiersze 23–26 po prostu powtarzają żądanie klienta, aby wskazać, że zostało ono zaakceptowane;
Jesteśmy teraz gotowi do przeprowadzenia kolejnych testów. Uruchamiamy nową wersję serwisu internetowego i okazuje się, że problem nadal występuje. Nic się nie zmieniło. Jeśli w wierszu 35 powyżej umieścimy komunikat konsolowy, nigdy nie zostanie on wyświetlony, co wskazuje, że metoda [getAllMedecins] z wiersza 34 nigdy nie została wywołana.
Po kilku poszukiwaniach okazuje się, że Spring MVC samodzielnie przetwarza polecenia HTTP i [OPTIONS], stosując domyślną obsługę. W związku z tym to zawsze Spring odpowiada, a nigdy metoda [getAllMedecins] z linii 34. To domyślne zachowanie Springa MVC można zmienić. Modyfikujemy istniejącą klasę [WebConfig]:
![]() |
package rdvmedecins.web.config;
...
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
public class WebConfig {
// konfiguracja dispatcherservlet dla nagłówków CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
// mapowanie jSON
...
- wiersze 10–11: bean [dispatcherServlet] służy do zdefiniowania serwletu obsługującego żądania klientów. Jest to tutaj serwlet typu [DispatcherServlet], serwlet frameworka Spring MVC;
- wiersz 12: tworzona jest instancja typu [DispatcherServlet];
- wiersz 13: nakazuje się serwletowi przekazywanie do aplikacji poleceń HTTP oraz [OPTIONS];
- wiersz 14: serwlet zostaje skonfigurowany w ten sposób;
Ponownie przeprowadzamy testy z tą nową konfiguracją. Otrzymujemy następujący wynik:
![]() |
- w [1] widzimy, że istnieją dwa żądania HTTP skierowane do URL i [http://localhost:8080/getAllMedecins];
- w [2] widoczne jest żądanie [OPTIONS];
- w [3] znajdują się trzy nagłówki HTTP, które właśnie skonfigurowaliśmy w odpowiedzi serwera;
Przyjrzyjmy się teraz drugiemu żądaniu:
![]() |
- w [1] – analizowane żądanie;
- w [2], czyli żądanie GET. Dzięki pierwszemu żądaniu [OPTIONS] przeglądarka otrzymała żądane informacje. Teraz wysyła żądanie [GET], o które prosiła początkowo;
- w [3] – odpowiedź serwera;
- w [4] serwer wysyła jSON;
- w pliku [5] wystąpił błąd;
- w [6] – komunikat o błędzie;
Trudniej jest wyjaśnić, co się tutaj stało. Odpowiedź serwera [3] jest prawidłowa – [HTTP/1.1 200 OK]. Powinniśmy więc otrzymać żądany dokument. Możliwe, że serwer rzeczywiście wysłał dokument, ale to przeglądarka uniemożliwia jego wykorzystanie, ponieważ wymaga, aby również w przypadku żądania GET odpowiedź zawierała nagłówek HTTP [Access-Control-Allow-Origin:http://localhost:8081].
Modyfikujemy kontroler [RdvMedecinsController] w następujący sposób:
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// lista lekarzy
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins(HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// odpowiedź
Response<List<Medecin>> response;
// nagłówki CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stan aplikacji
...
- wiersze 1–2: dodano kontroler [RdvMedecinsCorsController];
- wiersze 7–8: do parametrów metody [getAllMedecins] wstrzykujemy obiekt HttpServletResponse, który zawiera odpowiedź, która zostanie wysłana do klienta, oraz nagłówek HTTP [Origin];
- wiersz 12: wywoływana jest metoda [sendOptions] kontrolera [RdvMedecinsCorsController], ta sama, która została wywołana w celu przetworzenia żądania HTTP [OPTIONS]. W związku z tym wyśle ona te same nagłówki HTTP, co w przypadku tego żądania;
Po tej modyfikacji wyniki są następujące:
![]() |
Udało nam się uzyskać listę lekarzy.
8.4.14.3. Pozostałe URL [GET]
Teraz przedstawiamy pozostałe URL, które zostały wywołane za pośrednictwem GET. W kontrolerach kod akcji, które je przetwarzają, jest zgodny ze wzorcem akcji, które wcześniej przetwarzały URL [/getAllMedecins]. Czytelnik może sprawdzić kod w przykładach dołączonych do niniejszego dokumentu. Oto przykład:
w [RdvMedecinsCorsController]
// lista wizyt u lekarza
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
w [RdvMedecinsController]
// lista wizyt u lekarza
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
throws JsonProcessingException {
// odpowiedź
Response<List<Rv>> response = null;
boolean erreur = false;
// nagłówki CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stan aplikacji
...
Poniżej znajdują się zrzuty ekranu z przebiegu działania:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. URL [POST]
Rozważmy następujący przypadek:
![]() |
- tworzymy POST [1] do URL [2];
- w [3] znajduje się wartość zaksięgowana. Jest to ciąg znaków jSON;
- w sumie chcemy usunąć spotkanie o wartości [id] 100;
Na razie nie modyfikujemy żadnego kodu. Uzyskany wynik jest następujący:
![]() |
- w [1], podobnie jak w przypadku żądań [GET], przeglądarka wysyła żądanie [OPTIONS];
- w przypadku [2] przeglądarka prosi o zezwolenie na dostęp dla żądania [POST]. Wcześniej było to [GET];
- w [3] przeglądarka prosi o zezwolenie na wysłanie nagłówków HTTP i [accept, authorization, content-type]. Wcześniej były tylko dwa pierwsze nagłówki;
Modyfikujemy metodę [RdvMedecinsCorsController.sendOptions] w następujący sposób:
public void sendOptions(String origin, HttpServletResponse response) {
// Czy dozwolone są dane Cors?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// ustawiamy nagłówek CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// zezwalamy na niektóre nagłówki
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// zezwala się na GET
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
- wiersz 9: dodano nagłówki HTTP i [Content-Type] (wielkość liter nie ma znaczenia);
- wiersz 11: dodano metodę HTTP [POST];
Dzięki temu metody [POST] są przetwarzane w taki sam sposób jak zapytania [GET]. Oto przykład dla URL [/supprimerRv]:
w [RdvMedecinsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// odpowiedź
Response<Void> response = null;
boolean erreur = false;
// nagłówki CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stan aplikacji
if (messages != null) {
...
w [RdvMedecinsCorsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
Otrzymano następujący wynik:
![]() |
W przypadku URL i [/ajouterRv] otrzymujemy następujący wynik:
![]() |
8.4.14.5. Conclusion
Nasza aplikacja obsługuje teraz żądania międzydomenowe. Można je zezwolić lub zablokować poprzez konfigurację w klasie [ApplicationModel]:
// dane konfiguracyjne
private boolean corsAllowed = false;
8.5. Klient serwisu internetowego / jSON
Wróćmy do ogólnej architektury aplikacji, którą chcemy stworzyć:
![]() |
Górna część schematu została już napisana. Jest to serwer internetowy / jSON. Teraz zajmiemy się dolną częścią, a najpierw jej warstwą [DAO]. Napiszemy tę warstwę, a następnie przetestujemy ją za pomocą klienta konsolowego. Architektura testowa będzie wyglądać następująco:
![]() |
8.5.1. Projekt klienta konsolowego
Projekt klienta konsolowego o nazwie STS będzie wyglądał następująco:
![]() |
8.5.2. Konfiguracja Maven
Plik [pom.xml] klienta konsoli wygląda następująco:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webjson-client-console</name>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- biblioteka jSON wykorzystywana przez Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- komponent używany przez Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
</project>
- wiersze 15–20: nadrzędny projekt Spring Boot;
- wiersze 24–27: klient konsoli serwera WWW / jSON opiera się na komponencie o nazwie [RestTemplate] dostarczanym przez zależność [spring-web];
- wiersze 29–36: serializacja/deserializacja obiektów jSON wymaga biblioteki jSON. Korzystamy z odmiany biblioteki Jackson używanej przez Spring Web;
- wiersze 38–41: na najniższym poziomie komponent [RestTemplate] komunikuje się z serwerem za pośrednictwem gniazd TCP/IP. Chcemy ustawić parametr [timeout], czyli maksymalny czas oczekiwania na odpowiedź serwera. Komponent [RestTemplate] nie pozwala nam tego ustawić. Aby to zrobić, przekażemy do konstruktora [RestTemplate] komponent niskiego poziomu dostarczony przez zależność [org.apache.httpcomponents.httpclient]. To właśnie ta zależność pozwoli nam ustawić [timeout] dla komunikacji;
8.5.3. Pakiet [rdvmedecins.client.entities]
![]() |
Pakiet [rdvmedecins.client.entities] zawiera wszystkie elementy, które usługa internetowa / jSON wysyła za pośrednictwem swoich różnych URL. Nie będziemy ich ponownie szczegółowo omawiać. Wystarczy powiedzieć, że elementy JPA i [Client, Creneau, Medecin, Rv, Personne] zostały pozbawione wszystkich swoich adnotacji JPA, a także adnotacji jSON. Oto na przykład klasa [Rv]:
package rdvmedecins.client.entities;
import java.util.Date;
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// dzień spotkania
private Date jour;
// spotkanie jest powiązane z klientem
private Client client;
// spotkanie jest powiązane z przedziałem czasowym
private Creneau creneau;
// klucze zewnętrzne
private long idClient;
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);
}
// metody pobierające i ustawiające
...
}
8.5.4. Pakiet [rdvmedecins.client.requests]
![]() |
Pakiet [rdvmedecins.client.requests] łączy dwie klasy, których wartość jSON jest przypisana do klas URL, [/ajouterRv] i [supprimerRv]. Są one identyczne z tymi po stronie serwera.
8.5.5. Pakiet [rdvmedecins.client.responses]
![]() |
[Response] to typ wszystkich odpowiedzi serwisu internetowego / jSON. Jest to typ ogólny:
package rdvmedecins.client.responses;
import java.util.List;
public class Response<T> {
// ----------------- właściwości
// status operacji
private int status;
// ewentualne komunikaty o błędach
private List<String> messages;
// treść odpowiedzi
private T body;
// konstruktory
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// metody pobierające i ustawiające
...
}
- wiersz 5: typ [T] różni się w zależności od URL serwisu internetowego / jSON;
8.5.6. Pakiet [rdvmedecins.client.dao]
![]() |
- [IDao] stanowi interfejs warstwy [DAO], a [Dao] jest jego implementacją. Powrócimy do tej implementacji;
8.5.7. Pakiet [rdvmedecins.client.config]
![]() |
Klasa [DaoConfig] konfiguruje aplikację. Jej kod wygląda następująco:
package rdvmedecins.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
@Bean
public RestTemplate restTemplate() {
// tworzenie komponentu RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// wynik
return restTemplate;
}
// mapery jSON
@Bean
public ObjectMapper jsonMapper(){
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- wiersz 13: klasa [DaoConfig] jest klasą konfiguracyjną Springa;
- wiersz 14: pakiet [rdvmedecins.client.dao] zostanie przeszukany w celu znalezienia komponentów Spring. Znajdziemy w nim komponent [Dao];
- wiersze 17–24: definiują singleton Spring o nazwie [restTemplate] (nazwa metody). Metoda ta zwraca instancję [RestTemplate], która jest podstawowym narzędziem dostarczanym przez Spring do komunikacji z serwisem internetowym / jSON;
- wiersz 21: można by wpisać [RestTemplate restTemplate = new RestTemplate() ;]. W większości przypadków to wystarczy. Jednak w tym przypadku chcemy ustalić parametry klienta [timeout]. W tym celu wstrzykujemy do komponentu [RestTemplate] komponent niskopoziomowy typu [HttpComponentsClientHttpRequestFactory] (wiersz 20), który pozwoli nam ustawić te [timeout]. Przedstawiono niezbędną zależność Maven;
- wiersze 28–57: definiują mapery typu jSON. Są to mapery jSON wykorzystywane po stronie serwera (patrz paragraf 8.4.11.3) do serializacji typu T w odpowiedzi [Response<T>]. Te same konwertery zostaną teraz wykorzystane po stronie klienta do deserializacji typu T;
8.5.8. Interfejs [IDao]
Wróćmy do architektury aplikacji:
![]() |
Warstwa [DAO] stanowi adapter pomiędzy warstwą [console] a warstwami URL udostępnianymi przez serwis internetowy / jSON. Jej interfejs [IDao] będzie wyglądał następująco:
package rdvmedecins.client.dao;
import java.util.List;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
public interface IDao {
// adres URL serwisu internetowego
public void setUrlServiceWebJson(String url);
// limit czasu
public void setTimeout(int timeout);
// uwierzytelnianie
public void authenticate(User user);
// lista klientów
public List<Client> getAllClients(User user);
// lista lekarzy
public List<Medecin> getAllMedecins(User user);
// lista terminów wizyt u lekarza
public List<Creneau> getAllCreneaux(User user, long idMedecin);
// znalezienie klienta na podstawie jego identyfikatora
public Client getClientById(User user, long id);
// znalezienie klienta na podstawie jego identyfikatora
public Medecin getMedecinById(User user, long id);
// znalezienie wizyty na podstawie identyfikatora
public Rv getRvById(User user, long id);
// znalezienie terminu na podstawie identyfikatora
public Creneau getCreneauById(User user, long id);
// dodaj RV
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
// usuń RV
public void supprimerRv(User user, long idRv);
// lista wizyt u lekarza w danym dniu
public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
// kalendarz
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
}
- wiersz 14: metoda umożliwiająca ustalenie katalogu głównego serwisu internetowego / jSON, na przykład [http://localhost:8080];
- wiersz 17: metoda umożliwiająca ustawienie parametrów po stronie klienta. Chcemy kontrolować ten parametr, ponieważ niektórzy klienci czasami bardzo długo czekają na odpowiedź, która nigdy nie nadejdzie;
- wiersz 20: metoda umożliwiająca identyfikację użytkownika [login, passwd]. Wywołuje wyjątek, jeśli użytkownik nie zostanie rozpoznany;
- wiersze 22–53: z każdą metodą URL udostępnianą przez serwis internetowy / jSON powiązana jest metoda interfejsu, której sygnatura wynika z sygnatury metody po stronie serwera przetwarzającej udostępnioną metodę URL. Weźmy na przykład następujący serwer URL:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
- wiersz 1: widać, że [idMedecin] i [jour] są parametrami metody URL. Będą to parametry wejściowe metody powiązanej z tą metodą po stronie klienta URL;
- wiersz 2: widać, że metoda serwerowa zwraca typ [Response<String>]. Ten typ [String] jest typem wartości jSON o typie [AgendaMedecinJour]. Typ wyniku metody powiązanej z tym URL po stronie klienta będzie wynosił [AgendaMedecinJour];
Po stronie klienta deklaruje się następującą metodę:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
Ta sygnatura jest odpowiednia, gdy serwer wysyła odpowiedź [int status, List<String> messages, String body] wraz z [status==0]. W tym przypadku mamy [messages==null && body!=null]. Nie jest ona odpowiednia, gdy mamy [status!=0]. W takim przypadku otrzymujemy [messages!=null && body==null]. Musimy w jakiś sposób zasygnalizować, że wystąpił błąd. W tym celu wygenerujemy wyjątek typu [RdvMedecinsException] w następujący sposób:
package rdvmedecins.client.dao;
import java.util.List;
public class RdvMedecinsException extends RuntimeException {
private static final long serialVersionUID = 1L;
// kod błędu
private int status;
// lista komunikatów o błędach
private List<String> messages;
public RdvMedecinsException() {
}
public RdvMedecinsException(int code, List<String> messages) {
super();
this.status = code;
this.messages = messages;
}
// metody pobierające i ustawiające
...
}
- wiersze 9 i 11: wyjątek przejmie wartości pól [status, messages] z obiektu [Response<T>] przesłanego przez serwer;
- wiersz 5: klasa [RdvMedecinsException] rozszerza klasę [RuntimeException]. Jest to zatem wyjątek niekontrolowany, tzn. nie ma obowiązku obsługiwania go za pomocą try/catch ani deklarowania go w sygnaturze metod interfejsu;
Ponadto wszystkie metody interfejsu [IDao], które wysyłają zapytania do serwisu internetowego / jSON, mają jako parametr następujący typ [User]:
package rdvmedecins.client.entities;
public class User {
// dane
private String login;
private String passwd;
// konstruktory
public User() {
}
public User(String login, String passwd) {
this.login = login;
this.passwd = passwd;
}
// metody pobierające i ustawiające
...
}
W rzeczywistości każda wymiana danych z serwisem internetowym / jSON musi być opatrzona nagłówkiem uwierzytelniającym HTTP.
8.5.9. Pakiet [rdvmedecins.clients.console]
Teraz, gdy znamy już interfejs warstwy [DAO], możemy przedstawić aplikację konsolową.
![]() |
Klasa [Main] ma następujący wygląd:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
// serializator jSON
static private ObjectMapper mapper = new ObjectMapper();
// limit czasu połączeń w milisekundach
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// pobieramy referencję na warstwie [DAO]
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// ustawiamy URL usługi internetowej / json
dao.setUrlServiceWebJson("http://localhost:8080");
// ustawianie limitów czasu w milisekundach
dao.setTimeout(TIMEOUT);
// Uwierzytelnianie
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,x]";
try {
dao.authenticate(new User("user", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [x,x]";
try {
dao.authenticate(new User("x", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lista klientów
message = "/getAllClients";
try {
showResponse(message, dao.getAllClients(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lista lekarzy
message = "/getAllMedecins";
try {
showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lista terminów wizyt lekarza nr 2
message = "/getAllCreneaux/2";
try {
showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// pacjent nr 1
message = "/getClientById/1";
try {
showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lekarz nr 2
message = "/getMedecinById/2";
try {
showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// termin nr 3
message = "/getCreneauById/3";
try {
showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// wizyta nr 4
message = "/getRvById/4";
try {
showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// dodanie wizyty
message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
long idRv = 0;
try {
Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
idRv = response.getId();
showResponse(message, response);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lista wizyt lekarza nr 1 z dnia 2015-01-08
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// kalendarz lekarza 1 z dnia 2015-01-08
message = "/getAgendaMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// usunięcie dodanej wizyty
message = String.format("/supprimerRv [idRv=%s]", idRv);
try {
dao.supprimerRv(new User("admin", "admin"), idRv);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lista wizyt lekarza 1 z dnia 2015-01-08
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// zamknięcie kontekstu
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
private static <T> void showResponse(String message, T response) throws JsonProcessingException {
System.out.println(String.format("URL [%s]", message));
System.out.println(mapper.writeValueAsString(response));
}
}
- wiersz 19: serializator jSON, który pozwoli nam wyświetlić odpowiedź serwera, wiersz 184;
- wiersz 25: komponent [AnnotationConfigApplicationContext] to komponent Spring, który potrafi wykorzystywać adnotacje konfiguracyjne aplikacji Spring. Przekazujemy do jego konstruktora klasę [AppConfig], która konfiguruje aplikację;
- wiersz 26: pobieramy odwołanie do warstwy [DAO];
- wiersze 27–30: konfigurujemy ją;
- wiersze 32–169: testujemy wszystkie metody interfejsu [IDao];
Uzyskane wyniki są następujące:
09:20:56.935 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
/authenticate [admin,admin] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [user,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [x,x]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/getAllClients]
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]
URL [/getAllMedecins]
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]
URL [/getAllCreneaux/2]
[{"id":25,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":2},{"id":26,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":2},{"id":27,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":2},{"id":28,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":2},{"id":29,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":2},{"id":30,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":2},{"id":31,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":2},{"id":32,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":2},{"id":33,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":2},{"id":34,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":2},{"id":35,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":2},{"id":36,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":2}]
URL [/getClientById/1]
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}
URL [/getMedecinById/2]
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"}
URL [/getCreneauById/3]
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1}
URL [/getRvById/4]
L'erreur n° [2] s'est produite :
Le rendez-vous d'id [4] n'existe pas
URL [/ajouterRv [idClient=4,idCreneau=8,jour=2015-01-08]]
{"id":144,"version":0,"jour":1420671600000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":0,"idCreneau":0}
URL [/getRvMedecinJour/1/2015-01-08]
[{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}]
URL [/getAgendaMedecinJour/1/2015-01-08]
{"medecin":{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},"jour":1420671600000,"creneauxMedecinJour":[{"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"rv":{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"medecin":null,"idMedecin":1},"rv":null}]}
URL [/getRvMedecinJour/1/2015-01-08]
[]
09:21:00.258 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
Pozostawiamy czytelnikowi zadanie powiązania wyników z kodem. Kod ten pokazuje, jak wywołać każdą metodę warstwy [DAO]. Zwróćmy jedynie uwagę na kilka kwestii:
- wiersze 2–14: pokazują, że w przypadku błędu uwierzytelniania serwer zwraca status HTTP, [403 Forbidden] lub [401 Unauthorized], w zależności od sytuacji;
- wiersze 30–31: dodano wizytę (Rv) dla lekarza nr 1;
- wiersze 32–33: widoczna jest ta wizyta. Jest to jedyna wizyta tego dnia;
- wiersze 34–35: widoczna jest ona również w kalendarzu lekarza;
- wiersze 36–37: wizyta zniknęła. Kod usunął ją w międzyczasie;
Logi konsoli są kontrolowane przez następujące pliki:
![]() |
[application.properties]
logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- kodery są domyślnie przypisane do typu ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- kontrola poziomu logów -->
<root level="info"> <!-- wyłączone, informacyjne, debugowanie, ostrzeżenie -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
8.5.10. Implementacja warstwy [DAO]
Pozostaje nam teraz przedstawić sedno warstwy [DAO], czyli implementację jej interfejsu [IDao]. Zrobimy to stopniowo.
![]() |
Interfejs [IDao] jest zaimplementowany przez klasę abstrakcyjną [AbstractDao] oraz jej klasę potomną [Dao].
Klasa nadrzędna [AbstractDao] ma następującą postać:
package rdvmedecins.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import rdvmedecins.client.entities.User;
public abstract class AbstractDao implements IDao {
// dane
@Autowired
protected RestTemplate restTemplate;
protected String urlServiceWebJson;
// URL usługa internetowa / jSON
public void setUrlServiceWebJson(String url) {
this.urlServiceWebJson = url;
}
public void setTimeout(int timeout) {
// ustawianie limitu czasu dla żądań klienta internetowego
HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
.getRequestFactory();
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
}
private String getBase64(User user) {
// kodowanie użytkownika i hasła w base64 – wymaga
// Java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
// ogólne żądanie
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- wiersz 20: klasa jest abstrakcyjna, co uniemożliwia nam wyznaczenie jej jako komponentu Spring. To jej klasa potomna zostanie wyznaczona jako komponent;
- wiersze 23–24: wstrzykujemy bean [restTemplate], który zdefiniowaliśmy w klasie konfiguracyjnej [AppConfig];
- wiersz 25: URL jest katalogiem głównym serwisu internetowego /jSON;
- wiersze 32–38: ustalają limit czasu dla klienta oczekującego na odpowiedź serwera;
- wiersz 34: pobieramy komponent [HttpComponentsClientHttpRequestFactory], który wstrzyknięto do fasoli [restTemplate] podczas jej tworzenia (patrz [AppConfig]);
- wiersz 36: ustalamy maksymalny czas oczekiwania klienta podczas nawiązywania połączenia z serwerem;
- wiersz 37: ustalamy maksymalny czas oczekiwania klienta na odpowiedź na jedno z jego żądań;
Implementacja metod komunikacji z serwerem zostanie wyodrębniona do następującej metody generycznej:
// ogólne żądanie
protected String getResponse(User user, String url, String jsonPost) {
...
}
- wiersz 2: parametry metody [getResponse] są następujące:
- [User user]: użytkownik nawiązujący połączenie;
- [String url]: obiekt URL, do którego ma zostać wysłane zapytanie. Jest to końcowa część obiektu URL, przy czym pierwsza część jest dostarczana przez pole [urlServiceWebJson] klasy,
- [String jsonPost]: ciąg jSON, który ma zostać wysłany. Jeśli ta wartość występuje, wówczas zostanie zażądany URL wraz z POST, w przeciwnym razie będzie to GET;
Kontynuujmy:
// ogólne żądanie
protected String getResponse(User user, String url, String jsonPost) {
// adres URL: URL – skontaktuj się
// jsonPost: wartość jSON do wysłania
try {
// wykonanie zapytania
RequestEntity<?> request;
if (jsonPost == null) {
HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
if (user != null) {
headersBuilder = headersBuilder.header("Authorization", getBase64(user));
}
request = headersBuilder.build();
} else {
BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
if (user != null) {
bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
}
request = bodyBuilder.body(jsonPost);
}
// zlecenie jest realizowane
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e) {
throw new RdvMedecinsException(20, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(21, getMessagesForException(e));
}
}
- wiersze 23–24: instrukcja, która wysyła zapytanie do serwera i odbiera jego odpowiedź. Komponent [RestTemplate] oferuje wiele metod komunikacji z serwerem. Można było wybrać inną metodę niż [exchange]. Drugi parametr wywołania określa typ oczekiwanej odpowiedzi, w tym przypadku jest to ciąg znaków jSON. Pierwszym parametrem jest żądanie typu [RequestEntity] (wiersz 7). Wynik metody [exchange] ma typ [ResponseEntity<String>]. Typ [ResponseEntity] zawiera pełną odpowiedź serwera, nagłówki HTTP oraz dokument wysłany przez serwer. Podobnie typ [RequestEntity] zawiera całe żądanie klienta, w tym nagłówki HTTP oraz ewentualną wartość przesłaną metodą POST;
- wiersz 23: to treść obiektu [ResponseEntity<String>] jest zwracana do wywołującej metody, tj. ciąg jSON wysłany przez serwer;
- wiersze 9–21: musimy skonstruować żądanie typu [RequestEntity]. Różni się ono w zależności od tego, czy do wysłania żądania używamy obiektu GET, czy POST;
- wiersz 9: zapytanie dla GET. Klasa [RequestEntity] udostępnia metody statyczne do tworzenia zapytań typu GET, POST, HEAD,... Metoda [RequestEntity.get] umożliwia utworzenie zapytania GET poprzez łańcuchowanie różnych metod, które je tworzą:
- metoda [RequestEntity.get] przyjmuje jako parametr docelowy obiekt URL w postaci instancji URI,
- metoda [accept] pozwala zdefiniować elementy nagłówka HTTP [Accept]. W tym miejscu wskazujemy, że akceptujemy typ [application/json], który wyśle serwer;
- wynikiem tego łańcucha metod jest typ [HeadersBuilder];
- wiersze 10–12: w przypadku, gdy parametr [User user] nie jest równy null, do żądania dołączamy nagłówek HTTP [Authorization];
- wiersz 13: metoda [HeadersBuilder.build] wykorzystuje te różne informacje do utworzenia typu zapytania [RequestEntity];
- wiersz 15: zapytanie dotyczące typu POST. Metoda [RequestEntity.post] pozwala utworzyć zapytanie typu POST poprzez połączenie różnych metod, które je tworzą:
- metoda [RequestEntity.post] przyjmuje jako parametr docelowy URL w postaci instancji URI,
- metoda [header] pozwala zdefiniować nagłówki HTTP, z których chcemy skorzystać, w tym przypadku nagłówek autoryzacji,
- następująca po niej metoda [header] dołącza do żądania nagłówek [Content-Type: application/json], aby wskazać, że przesłana wartość dotrze w postaci ciągu znaków jSON;
- metoda [accept] pozwala wskazać, że akceptujemy typ [application/json], który zostanie wysłany przez serwer;
- wiersze 17–19: w przypadku, gdy parametr [User user] nie jest równy null, do żądania dołączamy nagłówek HTTP [Authorization];
- wiersz 20: metoda [BodyBuilder.body] ustala wartość księgowaną. Jest to drugi parametr metody generycznej [getResponse] (wiersz 2);
- wiersze 25–28: w przypadku wystąpienia jakiegokolwiek błędu generowany jest wyjątek typu [RdvMedecinsException];
Metoda [getMessagesForException] z wierszy 26 i 28 ma następującą postać:
// lista komunikatów o błędach związanych z wyjątkiem
protected static List<String> getMessagesForException(Exception exception) {
// pobierana jest lista komunikatów o błędach związanych z wyjątkiem
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// pobieranie komunikatu tylko wtedy, gdy !=null i nie jest pusty
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// następna przyczyna
cause = cause.getCause();
}
return erreurs;
}
Prywatna metoda [getBase64] generuje kod Base64 ciągu znaków „login:passwd” dla nagłówka uwierzytelniającego HTTP:
private String getBase64(User user) {
// kodowanie użytkownika i hasła w systemie Base64 – wymaga Java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
Klasa [Dao] rozszerza klasę [AbstractDao] w następujący sposób:
package rdvmedecins.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
@Service
public class Dao extends AbstractDao implements IDao {
// mapery jSON
@Autowired
ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
public List<Client> getAllClients(User user) {
...
}
public List<Medecin> getAllMedecins(User user) {
...
}
...
}
- wiersz 22: klasa [Dao] jest komponentem Spring. Zastosowano tu adnotację [@Service]. Można było nadal korzystać z adnotacji [@Component], używanej do tej pory;
- wiersze 26–36: wstrzyknięcie czterech mapperów jSON zdefiniowanych w klasie konfiguracyjnej [DaoConfig];
Wszystkie metody klasy [Dao] działają według tego samego schematu. Omówimy szczegółowo operację GET oraz operację POST.
Najpierw zapytanie [GET]:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
// odpowiedź
Response<AgendaMedecinJour> response;
// kalendarz
String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
try {
// kalendarz AgendaMedecinJour
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
});
} catch (IOException e) {
throw new RdvMedecinsException(401, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(402, getMessagesForException(e));
}
// analiza odpowiedzi
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- wiersz 5: wywoływana jest metoda generyczna [getResponse]. Rzeczywiste parametry to:
- 1: użytkownik;
- 2: cel URL;
- 3: wartość do wysłania. W tym przypadku nie ma żadnej;
- wiersz 5: wywołanie nie zostało ujęte w blok try / catch. Metoda [getResponse] może wywołać wyjątek typu [RdvMedecinsException]. Jeśli wyjątek ten zostanie wywołany, zostanie on przekazany do metody, która wywołała powyższą metodę [getAgendaMedecinJour];
- wiersz 8:URL [/getAgendaMedecinJour] wysyła typ [Response<AgendaMedecinJour>], który został zserializowany po stronie serwera do postaci jSON przez mapper jSON [jsonMapperLongRv]. Ten sam mapper służy do deserializacji otrzymanego ciągu jSON;
- wiersze 10–13: jeśli w wierszu 9 wystąpi błąd, zgłaszany jest typ [RdvMedecinsException];
- wiersze 16–21: analizowana jest odpowiedź wysłana przez serwer;
- wiersze 17–18: jeśli serwer zgłosił błąd, wywoływany jest wyjątek zawierający informacje przesłane przez serwer;
- wiersze 19–21: w przeciwnym razie zwracany jest kalendarz lekarza;
Analizowane zapytanie o numerze POST będzie wyglądało następująco:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// odpowiedź
Response<Rv> response;
try {
// spotkanie
String jsonResponse = getResponse(user, "/ajouterRv",
jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
// Rv Rv
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
});
} catch (RdvMedecinsException e) {
throw e;
} catch (IOException e) {
throw new RdvMedecinsException(381, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(382, getMessagesForException(e));
}
// analiza odpowiedzi
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- wiersz 6: wywoływana jest metoda [getResponse] z następującymi parametrami:
- 1: użytkownik;
- 2: docelowy URL,
- 3: wartość przesłana: przekazywana jest wartość typu [PostAjouter], utworzona na podstawie informacji otrzymanych jako parametry przez metodę. Wykorzystywany jest mapper jSON bez filtrów;
- wiersz 9: po stronie serwera to mapper jSON [jsonMapperLongRv] zserializował odpowiedź serwera. Po stronie klienta używa się tego samego mappera do jej deserializacji;
- wiersz 6: mapper URL [/ajouterRv] zwraca wartość jSON typu [Response<Rv>];
- wiersze 4–11: w tym miejscu metoda [getResponse] została umieszczona w bloku try/catch, ponieważ serializacja wysyłanej wartości może spowodować wygenerowanie wyjątku. Metoda [getResponse] może wygenerować wyjątek [RdvMedecinsException]. W takim przypadku po prostu uruchamia się ją ponownie (wiersze 11–12);
Poniższy kod (wiersze 13–24) jest analogiczny do tego, który właśnie omówiliśmy. Jedyną różnicą w stosunku do operacji GET jest zatem drugi parametr metody [getResponse], który musi przyjmować wartość jSON odpowiadającą wartości, która ma zostać zapisana.
Pozostałe metody są zbudowane według tego samego schematu.
8.5.11. Anomalia
Podczas przeprowadzania różnych testów napotkano anomalię podsumowaną w następującej klasie [Anomalie]:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Anomalie {
// serializator jSON
static private ObjectMapper mapper = new ObjectMapper();
// limit czasu połączeń w milisekundach
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// pobieramy referencję na warstwie [DAO]
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// ustawiamy URL usługi web / json
dao.setUrlServiceWebJson("http://localhost:8080");
// ustawianie limitów czasu w milisekundach
dao.setTimeout(TIMEOUT);
// Uwierzytelnianie
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Uwierzytelnianie
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Uwierzytelnianie
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// zamknięcie kontekstu
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
}
- wiersze 31–38: uwierzytelniono użytkownika [admin, admin];
- wiersze 40–47: uwierzytelniono użytkownika [admin, x], który wprowadził nieprawidłowe hasło;
- wiersze 49–56: uwierzytelniono użytkownika [user, user], który jest istniejącym użytkownikiem, ale nie ma uprawnień;
Oto wyniki:
- wiersz 2: wbrew oczekiwaniom użytkownik [admin, x] został zaakceptowany;
Jeśli skomentujemy linie 33–38 kodu, otrzymamy następujący wynik:
co jest oczekiwanym wynikiem. Wygląda to tak, jakby po pierwszym pomyślnym zalogowaniu się użytkownika [admin, admin] jego hasło nie było już potrzebne przy kolejnych próbach. Tak właśnie jest. Spring Security domyślnie wykorzystuje sesję, dzięki czemu po uwierzytelnieniu się użytkownik nie musi tego powtarzać w kolejnych żądaniach. Można zmodyfikować konfigurację serwera WWW / [Spring Security] tak, aby tak nie było:
![]() |
Plik [SecurityConfig] należy zmodyfikować w następujący sposób:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// brak sesji
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
- w wierszu 5 należy ustawić brak sesji bezpieczeństwa;
To rozwiązało problem nieprawidłowości.
8.6. Kod serwera Spring / Thymeleaf
8.6.1. Wprowadzenie
Wróćmy do architektury aplikacji klient-serwer, którą mamy zbudować:
![]() |
- został zbudowany serwer [Web2] / jSON;
- została zbudowana warstwa kliencka [DAO] dla klienta [Web1];
Relacja między serwerem [Web1] a przeglądarkami klienckimi jest relacją klient–serwer, w której serwerem jest serwer internetowy / jSON. W rzeczywistości serwer [Web1] będzie dostarczał strumienie HTML zamknięte w łańcuchu jSON. Architektura klient–serwer wygląda następująco:
![]() |
- mamy architekturę klient-serwer o nazwach [2] / [1], w której klient i serwer komunikują się za pomocą jSON;
- w języku [1] warstwa internetowa Spring MVC / Thymeleaf dostarcza widoki, fragmenty widoków oraz dane w języku jSON. Serwer jest zatem serwerem internetowym / jSON, podobnie jak serwer [Web1]. On również jest bezstanowy;
- w [2]: kod JavaScript osadzony w widoku ładowanym podczas uruchamiania aplikacji jest podzielony na warstwy:
- warstwa [présentation] odpowiada za interakcje z użytkownikiem,
- warstwa [DAO] odpowiada za dostęp do danych za pośrednictwem serwera [Web2];
- klient [2] będzie buforował niektóre widoki, aby odciążyć serwer;
Zbudujemy serwer WWW / jSON [Web1] zaimplementowany przy użyciu Springa MVC / Thymeleaf w kilku etapach:
- zapoznanie się z frameworkiem CSS Bootstrap;
- tworzenie widoków;
- tworzenie kontrolera;
Następnie, w oddzielnym etapie, zbudujemy klienta JS dla serwera [Web1]. Aby wyraźnie pokazać, że klient ten jest w pewnym stopniu niezależny od serwera [Web1], zbudujemy go przy użyciu narzędzia [Webstorm], a nie STS.
W dalszej części pominięte zostaną pewne szczegóły, ponieważ mogłyby one odwrócić uwagę od tego, co najważniejsze, a mianowicie od organizacji kodu. Zainteresowany czytelnik może znaleźć pełny kod na stronie internetowej tego dokumentu.
8.6.2. Projekt STS
![]() |
- w [1] – kody Java;
- w pliku [2] – widoki;
Konfiguracja Maven w pliku [pom.xml] wygląda następująco:
<?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>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-springthymeleaf-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-springthymeleaf-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
...
</project>
- wiersze 16–19: projekt jest projektem Thymeleaf;
- wiersze 20–24: oparty na warstwie [DAO], którą właśnie stworzyliśmy;
Konfigurację Javy zapewniają dwa pliki:
![]() |
Warstwa [web] jest konfigurowana przez następujący plik [WebConfig]:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
// ----------------- konfiguracja warstwy [web]
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
// konfiguracja serwletu dispatcherservlet dla nagłówków CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
Wszyscy mieliśmy już do czynienia, w tym czy innym momencie, z wszystkimi elementami tej konfiguracji. Przypomnijmy tylko, że wiersze 42–47 są niezbędne, gdy chcemy wysyłać do serwera zapytania międzydomenowe (CORS). Tak właśnie będzie w tym przypadku.
Klasa [AppConfig] konfiguruje całą aplikację:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.client.config.DaoConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// katalog główny serwisu internetowego / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// limit czasu w milisekundach
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
}
- wiersz 11: [AppConfig] importuje konfigurację warstwy [DAO] oraz warstwy [web];
- wiersze 15–16: identyfikatory, które umożliwią aplikacji dostęp do fazy rozruchu w celu buforowania danych lekarzy i klientów;
- wiersz 18: identyfikator URL usługi internetowej / jSON [Web1];
- wiersz 20: identyfikator timeout dla wywołań HTTP z aplikacji;
- wiersz 22: wartość logiczna określająca, czy zezwala się na wywołania międzydomenowe;
Wreszcie w pliku [application.properties] serwer Tomcat jest skonfigurowany do pracy na porcie 8081:
![]() |
server.port=8081
8.6.3. Funkcje aplikacji
Zostały one opisane w punkcie 8.2. Przypomnimy je teraz. Za pomocą przeglądarki wysyłamy żądanie do URL [http://localhost:8081/boot.html]:
![]() |
- [1], czyli stronę startową aplikacji;
- w [2] i [3] – identyfikator i hasło osoby, która chce korzystać z aplikacji. Istnieją dwaj użytkownicy: admin/admin (login/hasło) z rolą (ADMIN) oraz user/user z rolą (USER). Tylko rola ADMIN ma uprawnienia do korzystania z aplikacji. Rola USER służy wyłącznie do pokazania, jaką odpowiedź zwraca serwer w tym przypadku użycia;
- w [4] znajduje się przycisk umożliwiający połączenie z serwerem;
- w [5] – język aplikacji. Dostępne są dwa: domyślnie francuski oraz angielski;
- w [6] – URL serwera [rdvmedecins-springthymeleaf-server];
![]() |
- w [1] logujemy się;
![]() |
- po zalogowaniu można wybrać lekarza, u którego chce się umówić na wizytę [2], oraz dzień wizyty [3]. Po wprowadzeniu danych dotyczących lekarza i dnia automatycznie wyświetla się kalendarz:
![]() |
- po wyświetleniu kalendarza lekarza można zarezerwować termin [5];
![]() |
- w [6] wybiera się pacjenta na wizytę i potwierdza ten wybór w [7];
![]() |
Po zatwierdzeniu wizyty następuje automatyczny powrót do kalendarza, w którym nowa wizyta jest już zapisana. Wizytę tę można później usunąć za pomocą kodu [8].
Opisano już główne funkcje. Są one proste. Na koniec omówimy zarządzanie językiem:
![]() |
- w [1] przechodzi się z języka francuskiego na angielski;
![]() |
- w [2] widok przełącza się na język angielski, łącznie z kalendarzem;
8.6.4. Krok 1: wprowadzenie do frameworka CSS Bootstrap
![]() |
W powyższej aplikacji internetowej strony HTML będą korzystać z frameworka CSS Bootstrap [http://getbootstrap.com/], który teraz przedstawiamy.
8.6.4.1. Projekt przykładowy
Przykładowy projekt będzie wyglądał następująco:
![]() |
- w [1]: cały projekt;
- w [2]: kody Java;
- w [3]: skrypty JavaScript;
![]() |
- w [4]: biblioteki JavaScript;
- w [5]: widoki Thymeleaf;
- w [6]: arkusze stylów;
8.6.4.1.1. Konfiguracja Maven
Plik [pom.xml] to plik projektu Maven Thymeleaf:
<?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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-bootstrap</name>
<description>Démos Bootstrap</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath /> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
8.6.4.1.2. Konfiguracja Java
![]() |
Klasa [BootstrapDemo] konfiguruje aplikację Spring / Thymeleaf:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(BootstrapDemo.class, args);
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
}
Z tym typem kodu mieliśmy już do czynienia.
8.6.4.1.3. Kontroler Spring
![]() |
Kontroler [BootstrapController] wygląda następująco:
package istia.st.rdvmedecins;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class BootstrapController {
@RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bso1() {
return "bs-01";
}
@RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs02() {
return "bs-02";
}
@RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs03() {
return "bs-03";
}
@RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs04() {
return "bs-04";
}
@RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs05() {
return "bs-05";
}
@RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs06() {
return "bs-06";
}
@RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs07() {
return "bs-07";
}
@RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs08() {
return "bs-08";
}
}
Akcje służą wyłącznie do wyświetlania widoków przetwarzanych przez Thymeleaf.
8.6.4.1.4. Plik [application.properties]
Plik [application.properties] konfiguruje wbudowany serwer Tomcat:
server.port=8082
8.6.4.2. Przykład nr 1: jumbotron
Akcja [/bs-01] wyświetla następujący widok [bs-01.xml]:
![]() |
Widok [bs-01.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- rdzeń Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
</head>
<body id="body">
<div class="container">
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- treść -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- błąd -->
<div id="erreur" class="alert alert-danger">
<span>Ici, un texte d'erreur</span>
</div>
</div>
</body>
</html>
- wiersz 7: plik CSS z frameworka Bootstrap;
- wiersz 8: lokalny plik CSS;
- wiersz 13: wyświetla plik [1];
- wiersze 19–21: wyświetlają plik [2];
- wiersz 11: klasa CSS [container] definiuje obszar wyświetlania wewnątrz przeglądarki;
- wiersz 19: klasa CSS [alert] wyświetla kolorowy obszar. Klasa [alert-danger] wykorzystuje predefiniowany kolor. Istnieje kilka takich klas, np. [alert-info, alert-warning,...];
Jumbotron [1] jest generowany przez następujący widok [jumbotron.xml]:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Jumbotron Bootstrap -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1>
Les Médecins
<br />
associés
</h1>
</div>
</div>
</div>
</section>
- wiersz 4: obszar ma klasę CSS [jumbotron];
- wiersz 5: klasa [row] definiuje wiersz o 12 kolumnach;
- wiersz 6: klasa [col-md-2] definiuje obszar o szerokości dwóch kolumn w tym wierszu;
- wiersz 7: w tych dwóch kolumnach umieszcza się obraz;
- wiersze 9–15: w pozostałych 10 kolumnach umieszcza się tekst;
8.6.4.3. Przykład nr 2: pasek nawigacyjny
Akcja [/bs-02] wyświetla następujący widok [bs-02.xml]:
![]() |
Nowością jest pasek nawigacyjny [1] wraz z formularzem wprowadzania danych i przyciskami:
Widok [bs-02.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- skrypty JS -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/js/bs-02.js"></script>
</head>
<body id="body">
<div class="container">
<!-- pasek nawigacyjny -->
<div th:include="navbar1"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- treść -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informacje -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- wiersz 10: importujemy jQuery;
- wiersz 11: lokalny skrypt JS;
- wiersz 16: pasek nawigacyjny;
Pasek nawigacyjny jest generowany przez następujący widok [navbar1.xml]:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- formularz identyfikacyjny -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
</div>
</div>
</div>
</div>
</section>
![]() |
- wiersz 3: klasa [navbar] określa styl paska nawigacyjnego. Klasa [navbar-inverse] nadaje jej czarne tło. Klasa [navbar-fixed-top] sprawi, że podczas przewijania strony wyświetlanej przez przeglądarkę pasek nawigacyjny pozostanie u góry ekranu;
- wiersze 5–13: definiują obszar [1]. Jest to typowy zestaw klas, którego nie rozumiem. Korzystam z tego komponentu w obecnej postaci;
- wiersze 14–26: definiują „responsywny” obszar paska sterowania. Na smartfonie obszar ten znika w obszarze menu;
- wiersz 15: obraz, który jest obecnie ukryty;
- wiersze 17–25: klasa [navbar-form] stylizuje formularz na pasku sterowania. Klasa [navbar-right] przesuwa go na prawą stronę tego formularza;
- wiersze 21–23: dwa pola wprowadzania danych w formularzu z wiersza 17 – [2]. Znajdują się one wewnątrz klasy [form-group], która określa wygląd elementów formularza, a każda z nich posiada klasę [form-control];
- wiersz 24: klasa [btn], która definiuje przycisk, wzbogacona o klasę [btn-success], nadającą mu zielony kolor;
- wiersz 24: po kliknięciu przycisku [Connexion] wykonywana jest następująca funkcja JS:
function connecter() {
showInfo("Connexion demandée...");
}
function showInfo(message) {
$("#info").text(message);
}
Oto przykład:

8.6.4.4. Przykład nr 3: przycisk z listą
Akcja [/bs-03] wyświetla następujący widok [bs-03.xml]:
![]() |
- nowością jest element listy rozwijanej o nazwie [1], zwany również „dropdown”;
Kod widoku [bs-03.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Rdzeń Bootstrap CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- rdzeń Bootstrap JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- skrypt lokalny -->
<script type="text/javascript" src="resources/js/bs-03.js"></script>
</head>
<body id="body">
<div class="container">
<!-- pasek nawigacyjny -->
<div th:include="navbar2"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- treść -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informacje -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- wiersz 11: przycisk z listą wymaga pliku JS z biblioteki Bootstrap;
- wiersz 18: nowy pasek nawigacyjny;
Widok [navbar2.xml] wygląda następująco:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- formularz logowania -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
<!-- języki -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- strona startowa -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initNavBar2();
/*]]>*/
</script>
</section>
- wiersze 25–40: definiują przycisk z listą;
- wiersz 27: klasa [btn-danger] nadaje mu kolor czerwony;
- wiersze 32–39: elementy listy. Są to linki, z których każdy jest powiązany z funkcją JS;
- wiersze 46–51: skrypt JS wykonywany po załadowaniu dokumentu;
Skrypt JS [bs-03.js] ma następującą postać:
function initNavBar2() {
// rozwijane menu języków
$('.dropdown-toggle').dropdown();
}
function connecter() {
showInfo("Connexion demandée...");
}
function setLang(lang) {
var msg;
switch (lang) {
case 'fr':
msg = "Vous avez choisi la langue française...";
break;
case 'en':
msg = "You have selected english language...";
break;
}
showInfo(msg);
}
function showInfo(message) {
$("#info").text(message);
}
- wiersze 1–4: funkcja inicjująca [dropdown]. [$('.dropdown-toggle')] lokalizuje element o klasie [dropdown-toggle]. Jest to przycisk z listą (wiersz 28 widoku). Zastosowano do niego funkcję JS [dropdown()], która jest zdefiniowana w pliku JS [bootstrap.js]. Dopiero po tej operacji przycisk zachowuje się jak przycisk z listą;
- wiersze 10–21: funkcja wykonywana po wybraniu języka;
Oto przykład:

8.6.4.5. Przykład nr 4: menu
Akcja [/bs-04] wyświetla następujący widok [bs-04.xml]:
![]() |
Dodano menu o nazwie [1].
Widok [bs-04.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- rdzeń Bootstrap CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- rdzeń Bootstrap JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- skrypt lokalny -->
<script type="text/javascript" src="resources/js/bs-04.js"></script>
</head>
<body id="body">
<div class="container">
<!-- pasek nawigacyjny -->
<div th:include="navbar3"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- treść -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informacje -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- wiersz 18: dodano nowy pasek nawigacyjny;
Widok [navbar3.xml] wygląda następująco:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<ul class="nav navbar-nav">
<li class="active" id="lnkAfficherAgenda">
<a href="javascript:afficherAgenda()">Agenda </a>
</li>
<li class="active" id="lnkAccueil">
<a href="javascript:retourAccueil()">Retour Accueil </a>
</li>
<li class="active" id="lnkRetourAgenda">
<a href="javascript:retourAgenda()">Retour Agenda </a>
</li>
<li class="active" id="lnkValiderRv">
<a href="javascript:validerRv()">Valider </a>
</li>
</ul>
<!-- przyciski po prawej stronie -->
<div class="navbar-form navbar-right" role="form">
<!-- Wylogowanie -->
<button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- języki -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- strona startowa -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initNavBar3();
/*]]>*/
</script>
</section>
- wiersze 16–29: tworzą menu z czterema opcjami, z których każda jest powiązana ze skryptem JS;
- wiersze 55–60: skrypt uruchamiany podczas ładowania strony;
Skrypt JS [bs-04.js] wygląda następująco:
...
function initNavBar3() {
// rozwijane menu języków
$('.dropdown-toggle').dropdown();
// animowany obraz
loading = $("#loading");
loading.hide();
}
function afficherAgenda() {
showInfo("option [Agenda] cliquée...");
}
function retourAccueil() {
showInfo("option [Retour accueil] cliquée...");
}
function retourAgenda() {
showInfo("option [Retour agenda] cliquée...");
}
function validerRv() {
showInfo("option [Valider] cliquée...");
}
function setMenu(show) {
// linki w menu
var lnkAfficherAgenda = $("#lnkAfficherAgenda");
var lnkAccueil = $("#lnkAccueil");
var lnkValiderRv = $("#lnkValiderRv");
var lnkRetourAgenda = $("#lnkRetourAgenda");
// umieszczamy je w słowniku
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// ukrywa się wszystkie linki
for ( var key in options) {
options[key].hide();
}
// wyświetla się te, o które poproszono
for (var i = 0; i < show.length; i++) {
var option = show[i];
options[option].show();
}
}
- wiersze 2–18: funkcja inicjalizacji strony;
- wiersz 4: aby wyświetlić przycisk z listą języków;
- wiersze 6–7: animowany obraz jest ukryty;
- wiersze 26–48: funkcja [setMenu], która pozwala określić, które opcje mają być widoczne;
Przejdźmy do konsoli programistycznej (Ctrl-Shift-I) i wprowadźmy następujący kod [1]:
![]() |
Następnie wróćmy do przeglądarki. Menu uległo zmianie: [2]:
8.6.4.6. Przykład nr 5: lista rozwijana
Akcja [/bs-05] wyświetla następujący widok [bs-05.xml]:
![]() |
Nowością jest element [1]. Wykorzystujemy tutaj komponent spoza biblioteki Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].
Kod widoku [bs-05.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Rdzeń Bootstrap CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Rdzeń Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<!-- skrypt lokalny -->
<script type="text/javascript" src="resources/js/bs-05.js"></script>
</head>
<body id="body">
<div class="container">
<!-- pasek nawigacyjny -->
<div th:include="navbar3"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- treść -->
<div id="content" th:include="choixmedecin">
</div>
<!-- informacje -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- wiersz 8: element CSS niezbędny do działania listy rozwijanej;
- wiersz 13: plik JS niezbędny do działania listy rozwijanej;
- wiersz 24: lista rozwijana;
Widok [choixmedecin.xml] wygląda następująco:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
</div>
<!-- skrypt lokalny -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initChoixMedecin();
/*]]>*/
</script>
</section>
- wiersze 7–12: mamy tu klasyczny tag [select], jednak z określoną klasą [combobox]. Atrybut [data-style="btn-primary"] nadaje komponentowi niebieski kolor;
- wiersze 16–21: skrypt wykonywany podczas ładowania strony;
Plik JS [bs-05.js] ma następującą treść:
...
function afficherAgenda() {
var idMedecin = $('#idMedecin option:selected').val();
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
function initChoixMedecin() {
// lista lekarzy
$('#idMedecin').selectpicker();
// menu
setMenu([ "lnkAfficherAgenda" ]);
}
- wiersze 7–12: funkcja wykonywana podczas ładowania strony;
- wiersz 9: instrukcja, która przekształca element [select] na stronie w listę rozwijaną Bootstrap. [$('#idMedecin')] odwołuje się do [select] (wiersz 7 widoku [choixmedecin]), a funkcja JS [selectpicker] pochodzi z pliku JS [bootstrap-select.js];
- wiersz 11: wyświetlana jest tylko jedna z opcji menu;
- wiersze 2–5: funkcja JS jest uruchamiana po kliknięciu opcji menu [Agenda];
- wiersz 3: pobierana jest wartość opcji wybranej z listy rozwijanej: [$('#idMedecin option:selected')] najpierw wyszukuje komponent [id=idMedecin], a następnie w tym komponencie wybraną opcję. Następnie operacja [..].val() pobiera wartość znalezionego elementu, tj. atrybut [value] wybranej opcji;
Oto przykład wyboru lekarza:
![]() |
8.6.4.7. Przykład nr 6: kalendarz
Akcja [/bs-06] wyświetla następujący widok [bs-06.xml]:

Wybór lekarza lub daty uruchamia funkcję JS, która wyświetla zarówno wybranego lekarza, jak i wybraną datę. Oto przykład:
![]() |
Za pomocą przycisku „Lista języków” można przełączyć kalendarz (i tylko kalendarz) na język angielski:

Jest to najbardziej złożony przykład z tej serii. Kalendarz jest komponentem o nazwie [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker].
Widok [bs-06.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Rdzeń Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<!-- skrypt lokalny -->
<script type="text/javascript" src="resources/js/bs-06.js"></script>
</head>
<body id="body">
<div class="container">
<!-- pasek nawigacyjny -->
<div th:include="navbar3"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- treść -->
<div id="content" th:include="choixmedecinjour">
</div>
<!-- informacje -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- wiersz 8: plik CSS komponentu [bootstrap-datepicker];
- wiersz 16: plik JS należący do komponentu [bootstrap-datepicker];
- wiersz 17: plik JS do obsługi francuskiego kalendarza. Domyślnie jest on w języku angielskim;
- wiersz 15: plik JS z biblioteki o nazwie [moment], która zapewnia dostęp do bardzo wielu funkcji obliczania czasu [http://momentjs.com/];
- wiersz 28: widok kalendarza;
Widok [choixmedecinjour.xml] wygląda następująco:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
<div class="col-md-3">
<h2>Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- skrypt lokalny -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initChoixMedecinJour();
/*]]>*/
</script>
</section>
- wiersze 17–23: kalendarz;
- wiersz 18: klasa [btn-primary] nadaje mu niebieski kolor;
- wiersz 18: atrybut [disabled="true"] uniemożliwia ręczne wpisanie daty. Konieczne jest skorzystanie z kalendarza;
- wiersz 16: kalendarz został umieszczony w sekcji [id="calendar_container"]. Aby zmienić język kalendarza, należy go usunąć, a następnie wygenerować na nowo. Należy zatem usunąć zawartość komponentu [id="calendar_container"], a następnie umieścić w nim nowy kalendarz z nowym językiem;
- wiersze 28–33: kod inicjalizujący stronę;
Plik JS [bs-06.js] ma następującą treść:
...
var calendar_infos = {};
function initChoixMedecinJour() {
// kalendarz
var calendar_container = $("#calendar_container");
calendar_infos = {
"container" : calendar_container,
"html" : calendar_container.html(),
"today" : moment().format('YYYY-MM-DD'),
"langue" : "fr"
}
// tworzenie kalendarza
updateCalendar();
// lista lekarzy
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// menu
setMenu([]);
}
- wiersz 2: kalendarz jest obsługiwany przez kilka funkcji JS. Zmienna [calendar_infos] będzie gromadzić informacje o kalendarzu. Jest to zmienna globalna, aby była widoczna dla różnych funkcji;
- wiersz 6: identyfikujemy kontener kalendarza;
- wiersze 7–12: informacje zapisane dla kalendarza;
- wiersz 8: odwołanie do jego kontenera,
- wiersz 9: kod kalendarza HTML. Dzięki tym dwóm informacjom można usunąć kalendarz i odtworzyć go,
- wiersz 10: dzisiejsza data w formacie [aaaa-mm-jj],
- wiersz 11: język kalendarza;
- wiersz 14: utworzenie kalendarza;
- wiersz 16: lista rozwijana lekarzy;
- wiersze 17–19: za każdym razem, gdy zmieni się wartość wybrana w tej liście rozwijanej, zostanie wykonana metoda [afficherAgenda];
- wiersz 21: brak menu na pasku nawigacyjnym;
Funkcja [updateCalendar] ma następujący kod:
function updateCalendar(renew) {
if (renew) {
// odświeżenie bieżącego kalendarza
calendar_infos.container.html(calendar_infos.html);
}
// inicjalizacja kalendarza
var calendar = $("#calendar");
var settings = {
format : "yyyy-mm-dd",
startDate : calendar_infos.today,
language : calendar_infos.langue,
};
calendar.datepicker(settings);
// wybór bieżącej daty
if (calendar_infos.date) {
calendar.datepicker('setDate', calendar_infos.date)
}
// wydarzenia
calendar.datepicker().on('hide', function(e) {
// wyświetlanie wybranego dnia
displayJour();
});
calendar.datepicker().on('changeDate', function(e) {
// zapisywanie nowej daty
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
// wyświetlanie informacji z kalendarza
afficherAgenda();
// wyświetlanie wybranego dnia
displayJour();
});
// wyświetlanie wybranego dnia
displayJour();
}
- wiersz 1: funkcja [updateCalendar] przyjmuje parametr, który może być obecny lub nie. Jeśli jest obecny, kalendarz jest odświeżany (wiersz 4) na podstawie informacji zawartych w [calendar_infos];
- wiersz 7: odwołujemy się do kalendarza;
- wiersze 8–12: parametry inicjalizacyjne;
- wiersz 9: format obsługiwanych dat [aaaa-mm-jj],
- wiersz 10: pierwsza data, którą można wybrać w kalendarzu. W tym przypadku jest to dzisiejsza data. Daty wcześniejsze nie będą dostępne do wyboru,
- wiersz 11: język kalendarza. Będą dwa: ['en'] i ['fr'];
- wiersz 13: kalendarz jest skonfigurowany;
- wiersze 15–17: jeśli data z pliku [calendar_infos] została zainicjowana, wówczas podaje się tę datę jako aktualną datę kalendarza;
- wiersze 19–22: za każdym razem, gdy kalendarz zostanie zamknięty, wyświetlana będzie wybrana data;
- wiersze 23–30: za każdym razem, gdy nastąpi zmiana daty w kalendarzu:
- wiersz 25: zapisujemy wybraną datę w [calendar_infos],
- wiersz 27: wyświetlane są informacje o terminach,
- wiersz 29: wyświetla się wybrany dzień;
- wiersz 32: wyświetlenie wybranego dnia, jeśli taki istnieje;
Metoda [displayJour], która wyświetla wybrany dzień, wygląda następująco:
// wyświetla wybrany dzień
function displayJour() {
if (calendar_infos.date) {
var displayjour = $("#displayjour");
moment.locale(calendar_infos.langue);
jour = moment(calendar_infos.date).format('LL');
displayjour.val(jour);
}
}
- wiersz 3: jeśli data została już wybrana (na początku w kalendarzu nie ma wybranej daty);
- wiersz 4: lokalizujemy komponent, w którym zapiszemy datę;
- wiersz 5: data ta może być zapisana w języku angielskim lub francuskim. Ustawiamy język biblioteki [moment];
- wiersz 6: wyświetla się wybraną datę w wybranym języku i w formacie długim;
- wiersz 7: data ta jest wyświetlana;
Oto dwa przykłady:
![]() | ![]() |
W przypadku zmiany lekarza lub daty wykonywana jest metoda [afficherAgenda]:
function afficherAgenda() {
// wyświetla lekarza i datę
var idMedecin = $('#idMedecin option:selected').val();
if (calendar_infos.date) {
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
}
}
8.6.4.8. Przykład nr 7: tabela „responsywna” o nazwie HTML
Uwaga: „responsive” to angielski termin oznaczający, że komponent jest w stanie dostosować się do rozmiaru ekranu, na którym jest wyświetlany. Pokażemy na tym przykładzie.
Akcja [/bs-07] wyświetla następujący widok [bs-07.xml] (na całym ekranie):
![]() |
Nowością jest tabela HTML [1]. Tabela ta jest obsługiwana przez bibliotekę JS [footable]: [https://github.com/fooplugins/FooTable].
Po zmniejszeniu okna przeglądarki otrzymujemy następujący wynik:
![]() |
- tabela HTML dostosowała się do rozmiaru ekranu;
- w [1], aby wyświetlić link [Réserver], należy kliknąć na symbol [+];
- w [2] to, co widać po kliknięciu znaku [+];
Widok [bs-07.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Rdzeń Bootstrap CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Rdzeń Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- skrypt lokalny -->
<script type="text/javascript" src="resources/js/bs-07.js"></script>
</head>
<body id="body">
<div class="container">
<!-- pasek nawigacyjny -->
<div th:include="navbar3" />
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron" />
<!-- treść -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda" />
<!-- informacje -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- wiersz 10: CSS z biblioteki [footable];
- wiersz 19: JS z biblioteki [footable];
- wiersz 31: tabela HTML z kalendarza;
Widok [agenda.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="row alert alert-danger">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="javascript:reserver(14)" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="javascript:supprimer(17)" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- strona startowa -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjujemy stronę
initAgenda();
/*]]>*/
</script>
</body>
</html>
- wiersz 4: umieszcza tabelę w jednym wierszu [row] oraz kolorową ramkę [alert alert-danger];
- wiersz 5: tabela zajmie 6 kolumn [col-md-6];
- wiersz 6: tabela HTML jest sformatowana za pomocą Bootstrap [class='table'];
- wiersz 9: atrybut [data-toggle] wskazuje kolumnę, w której znajduje się symbol [+/-] służący do rozwijania/zwijania wiersza;
- wiersz 15: atrybut [data-hide='phone'] wskazuje, że kolumna ma zostać ukryta, jeśli rozmiar ekranu odpowiada rozmiarowi ekranu telefonu. Można również użyć wartości „tablet”;
- wiersz 31: do linku [Réserver] przypisuje się funkcję JS;
- wiersz 46: przypisujemy funkcję JS do linku [Supprimer];
- wiersze 56–61: inicjalizacja strony;
Niektóre z wykorzystanych powyżej klas CSS pochodzą z pliku CSS [bootstrapDemo.css]:
@CHARSET "UTF-8";
#sloty th {
text-align: center;
}
#przedziały td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Style [status-*] pochodzą z przykładu wykorzystania tabeli [footable] znalezionego na stronie biblioteki.
W pliku JS [bs-07.js] strona jest inicjowana w następujący sposób:
function initAgenda() {
// tabela przedziałów czasowych
$("#creneaux").footable();
}
To wszystko. [$("#creneaux")] odwołuje się do tabeli HTML, którą chcemy dostosować do urządzeń mobilnych. Ponadto występują funkcje JS powiązane z dwoma linkami [Réserver] i [Supprimer]:
function reserver(idCreneau) {
showInfo("Réservation du créneau n° " + idCreneau);
}
function supprimer(idRv) {
showInfo("Suppression du rv n° " + idRv);
}
8.6.4.9. Przykład nr 8: okno modalne
Akcja [/bs-08] wyświetla następujący widok [bs-08.xml]:

Podczas gdy wcześniej kliknięcie linku [Réserver] powodowało wyświetlenie informacji w oknie informacyjnym, w tym przypadku wyświetli się okno modalne służące do wyboru klienta dla RV:

Wykorzystywany komponent to [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].
Widok [bs-08.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- rdzeń Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- skrypt lokalny -->
<script type="text/javascript" src="resources/js/bs-08.js"></script>
</head>
<body id="body">
<div class="container">
<!-- pasek nawigacyjny -->
<div th:include="navbar3" />
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron" />
<!-- treść -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda-modal" />
<div th:include="resa" />
<!-- informacje -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- wiersz 19: plik JS niezbędny do działania okienek modalnych;
- wiersz 32: widok [agenda-modal] jest identyczny z widokiem [agenda] z jednym wyjątkiem: funkcja JS, która obsługuje link [Réserver]:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
Funkcja [showDialogResa] odpowiada za wyświetlenie okna modalnego służącego do wyboru klienta;
- wiersz 33: widok [resa.xml] to okno modalne służące do wyboru klienta:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Tytuł okna modalnego</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span>Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2>Clients</h2>
<select id="idClient" class="combobox" data-style="btn-primary">
<option value="1">Mme Marguerite Planton</option>
<option value="2">Mr Maxime Franck</option>
<option value="3">Mlle Elisabeth Oron</option>
<option value="4">Mr Gaëtan Calot</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- strona początkowa -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjujemy stronę
initResa();
/*]]>*/
</script>
</section>
- wiersze 3–37: okno modalne;
- wiersze 13–30: zawartość tego okna (to, co zostanie wyświetlone);
- wiersze 31–34: przyciski w oknie dialogowym;
- wiersz 32: przycisk [Annuler] obsługiwany przez funkcję JS [cancelDialogResa];
- wiersz 33: przycisk [Valider] obsługiwany przez funkcję JS [validateResa];
- wiersze 39–44: skrypt inicjalizujący okno modalne;
Daje to następujący widok:
![]() |
Należy zauważyć, że okno modalne nie jest wyświetlane domyślnie. Dlatego nie widać go po uruchomieniu aplikacji, mimo że jego kod HTML znajduje się w dokumencie.
Plik JS [bs-08.js] ma następującą treść:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
// zapisywanie identyfikatora terminu
this.idCreneau = idCreneau;
// wyświetla się okno dialogowe rezerwacji
var resa = $("#resa");
resa.modal('show');
// log
showInfo("Réservation du créneau n° " + idCreneau);
}
function cancelDialogResa() {
// ukrywanie okna dialogowego
resa.modal('hide');
}
// potwierdzenie rezerwacji
function validateResa() {
// pobieramy informacje
var idClient = $('#idClient option:selected').val();
// ukrywanie okna dialogowego
resa.modal('hide');
// informacje
showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
function initResa() {
// lista klientów
$('#idClient').selectpicker();
// okno modalne
resa = $("#resa");
resa.modal({});
}
- wiersze 30–36: funkcja inicjalizacji okna modalnego;
- wiersz 32: okno modalne zawiera listę rozwijaną, którą należy zainicjować;
- wiersze 34–35: inicjalizacja samego okna modalnego;
- wiersze 5–13: funkcja JS powiązana z linkiem [Réserver];
- wiersz 7: zapisujemy parametr funkcji w zmiennej globalnej z wiersza 1;
- wiersze 9–10: okno modalne zostaje wyświetlone;
- wiersz 12: zapisujemy informację w oknie informacyjnym;
- wiersze 15–18: obsługa przycisku [Annuler]. Ograniczamy się do ukrycia okna modalnego (wiersz 17);
- wiersze 21–31: funkcja JS przypisana do przycisku [Valider];
- wiersz 23: pobieramy atrybut [value] wybranego klienta;
- wiersz 25: ukrywa się okno dialogowe;
- wiersz 27: rejestrujemy dwie informacje: numer zarezerwowanego terminu oraz nazwę klienta;
8.6.5. Krok 2: tworzenie widoków
Teraz opiszemy widoki generowane przez serwer [Web1] oraz ich szablony.
![]() |
8.6.5.1. Widok [navbar-start]
Wyświetla pasek nawigacyjny strony startowej:

Kod widoku [navbar-start.xml] wygląda następująco:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- formularz logowania -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
</div>
<div class="form-group">
<input type="text" th:placeholder="#{username}" class="form-control" id="login" />
</div>
<div class="form-group">
<input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
</div>
<button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
<!-- języki -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- strona początkowa -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initNavBarStart();
/*]]>*/
</script>
</section>
Ten widok nie ma szablonu. Posiada następujące procedury obsługi zdarzeń:
évt | obsługa |
kliknięcie przycisku logowania | |
kliknięcie linku [Français] | |
kliknij link [English] |
8.6.5.2. Widok [jumbotron]
Jest to widok wyświetlany pod paskiem nawigacyjnym [navbar-start] na stronie startowej:

Jej kod [jumbotron.xml] jest następujący:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Jumbotron Bootstrap -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1 th:utext="#{application.header}" />
</div>
</div>
</div>
</section>
Widok [jumbotron] nie ma ani szablonu, ani zdarzeń.
8.6.5.3. Widok [login]
Jest to widok wyświetlany pod jumbotronem na stronie startowej:

Jej kod [login.xml] wygląda następująco:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{identification}">Identification
</div>
</section>
Widok nie posiada ani szablonu, ani zdarzeń.
8.6.5.4. Widok [navbar-run]
Jest to pasek nawigacyjny wyświetlany po pomyślnym zalogowaniu:

Jej kod [navbar-run.xml] wygląda następująco:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- przyciski po prawej stronie -->
<form class="navbar-form navbar-right" role="form">
<!-- wylogowanie -->
<button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- języki -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</form>
</div>
</div>
</div>
<!-- inicjowanie strony -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initNavBarRun();
/*]]>*/
</script>
</section>
Ten widok nie ma szablonu. Posiada następujące procedury obsługi zdarzeń:
évt | obsługa |
kliknięcie przycisku wylogowania | |
kliknięcie linku [Français] | |
kliknij link [English] |
8.6.5.5. Widok [accueil]
Jest to widok wyświetlany bezpośrednio pod paskiem nawigacyjnym [navbar-run]:

Jej kod [accueil.html] jest następujący:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{rv.medecin}">Médecin</h2>
<select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
<option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
</select>
</div>
<div class="col-md-3">
<h2 th:text="#{rv.jour}">Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- kalendarz -->
<div id="agenda"></div>
<!-- skrypt lokalny -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initChoixMedecinJour();
/*]]>*/
</script>
</html>
Jego szablon wygląda następująco:
- [rdvmedecins.medecinItems] (wiersz 8): lista lekarzy;
W obecnej postaci widok ten wydaje się nie posiadać obsługi zdarzeń. W rzeczywistości są one zdefiniowane w funkcji [initChoixMedecinJour]. Funkcja ta została omówiona w paragrafie 8.6.4.7, na stronie 466, a w szczególności na stronie 469. Znajdują się tam następujące procedury obsługi zdarzeń:
évt | obsługa |
wyboru lekarza | |
wybór daty |
8.6.5.6. Widok [agenda]
Widok [agenda] przedstawia jeden dzień z kalendarza lekarza:

Jego kod [agenda.xml] jest następujący:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
<h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
de consultation</h4>
<th:block th:if="${agenda.creneaux.length}!=0">
<div class="row tab-content alert alert-warning">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
</th>
<th>
<span th:text="#{agenda.client}">Client</span>
</th>
<th data-hide="phone">
<span th:text="#{agenda.action}">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="creneau,iter : ${agenda.creneaux}">
<td>
<span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
<span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
</td>
<td>
<span th:text="${creneau.client}">Client</span>
</td>
<td>
<a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
class="status-metro status-active">Réserver
</a>
<a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
class="status-metro status-suspended">Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- rezerwacja -->
<section th:include="resa" />
</th:block>
<!-- inicjowanie strony -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initAgenda();
/*]]>*/
</script>
</body>
</html>
Szablon tego widoku zawiera tylko jeden element:
- [agenda] (wiersz 4): nieco złożony szablon stworzony specjalnie do wyświetlania kalendarza;
Posiada następujące menedżery zdarzeń:
évt | obsługujący |
kliknięcie przycisku [Supprimer] | |
kliknięcie linku [Réserver] |
Widok [resa] z wiersza 47 jest widokiem wyświetlanym po kliknięciu przez użytkownika linku [Réserver]:

Jego kod [resa.xml] jest następujący:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Tytuł okna modalnego</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span th:text="#{resa.titre}">Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{resa.client}">Client</h2>
<select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
<option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- inicjuje stronę -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicjowanie strony
initResa();
/*]]>*/
</script>
</body>
</html>
Jego szablon zawiera tylko jeden element:
- [clientItems] (wiersz 24): lista klientów;
Posiada następujące menedżery zdarzeń:
evt | obsługa |
kliknięcie przycisku [Annuler] | |
kliknięcie przycisku [Valider] |
8.6.5.7. Widok [erreurs]
Jest to widok wyświetlany, jeśli żądana przez użytkownika czynność nie powiodła się:

Kod [erreurs.xml] wygląda następująco:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-danger">
<h4>
<span th:text="#{erreurs.titre}">Les erreurs suivantes se sont produites :</span>
</h4>
<ul>
<li th:each="message : ${erreurs}" th:text="${message}" />
</ul>
</div>
</section>
Jego szablon zawiera tylko jeden element:
- [erreurs] (wiersz 8): lista błędów do wyświetlenia;
Widok nie posiada menedżera zdarzeń.
8.6.5.8. Résumé
Poniższa tabela przedstawia widoki i ich szablony:
widok | szablon | obsługa zdarzeń |
navbar-start | ||
jumbotron | ||
login | ||
pasek nawigacyjny – uruchom | ||
strona główna | ||
kalendarz | ||
rezerwacja | ||
błędy |
8.6.6. Krok 3: definiowanie akcji
Wróćmy do architektury serwisu internetowego [Web1]:
![]() |
Zobaczymy teraz, jakie akcje URL są udostępniane przez [Web1] oraz ich implementację:
8.6.6.1. Akcje URL udostępniane przez usługę [Web1]
Są to:
- URL dla każdego z powyższych widoków lub ich kombinacji;
- URL służąca do dodania RV;
- plik URL służący do usunięcia pliku RV;
Wszystkie one zwracają odpowiedź typu [Reponse] o następującej treści:
public class Reponse {
// ----------------- właściwości
// status operacji
private int status;
// pasek nawigacyjny
private String navbar;
// jumbotron
private String jumbotron;
// treść strony
private String content;
// kalendarz
private String agenda;
...
}
- wiersz 5: stan odpowiedzi: 1 (OK), 2 (błąd);
- wiersz 7: strumień HTML z widoków [navbar-start] lub [navbar-run], w zależności od przypadku;
- wiersz 9: strumień HTML z widoku [jumbotron];
- wiersz 13: strumień HTML z widoku [agenda];
- wiersz 9: strumień HTML z widoków [accueil], [erreurs], [login], w zależności od przypadku;
Przedstawione widoki URL są następujące
umieszcza widok [navbar-start] w [Reponse.navbar] | |
umieszcza widok [navbar-run] w widoku [Reponse.navbar] | |
umieszcza widok [accueil] w widoku [Reponse.content] | |
umieszcza widok [jumbotron] w widoku [Reponse.jumbotron] | |
umieszcza widok [agenda] w widoku [Reponse.agenda] | |
umieszcza widok [login] w widoku [Reponse.content] | |
| |
przenosi widok [navbar-run] do [Reponse.navbar], widok [jumbotron] do [Reponse.jumbotron], widok [accueil] do [Reponse.content], widok [agenda] do [Reponse.agenda] | |
dodaje wybrane spotkanie i umieszcza nowy kalendarz w [Reponse.agenda] | |
usuwa wybrane spotkanie i umieszcza nowy kalendarz w pliku [Reponse.agenda] |
8.6.6.2. Singleton [ApplicationModel]
![]() |
Klasa [ApplicationModel] jest instancjonowana jako pojedynczy egzemplarz i wstrzykiwana do kontrolera aplikacji. Jej kod wygląda następująco:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
....
}
- wiersz 6: [ApplicationModel] jest komponentem Spring;
- wiersz 7: który implementuje interfejs warstwy [DAO]. Robimy to po to, aby akcje nie musiały znać warstwy [DAO], a jedynie singleton [ApplicationModel]. Architektura [Web1] wygląda wówczas następująco:
![]() |
Wróćmy do kodu klasy [ApplicationModel]:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
// warstwa [DAO]
@Autowired
private IDao dao;
// konfiguracja
@Autowired
private AppConfig appConfig;
// dane pochodzące z warstwy [DAO]
private List<ClientItem> clientItems;
private List<MedecinItem> medecinItems;
// dane konfiguracyjne
private String userInit;
private String mdpUserInit;
private boolean corsAllowed;
// wyjątek
private RdvMedecinsException rdvMedecinsException;
// producent
public ApplicationModel() {
}
@PostConstruct
public void init() {
// konfiguracja
userInit = appConfig.getUSER_INIT();
mdpUserInit = appConfig.getMDP_USER_INIT();
dao.setTimeout(appConfig.getTIMEOUT());
dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
corsAllowed = appConfig.isCORS_ALLOWED();
// buforujemy listy rozwijane lekarzy i klientów
List<Medecin> medecins = null;
List<Client> clients = null;
try {
medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
clients = dao.getAllClients(new User(userInit, mdpUserInit));
} catch (RdvMedecinsException ex) {
rdvMedecinsException = ex;
}
if (rdvMedecinsException == null) {
// tworzy się elementy list rozwijanych
medecinItems = new ArrayList<MedecinItem>();
for (Medecin médecin : medecins) {
medecinItems.add(new MedecinItem(médecin));
}
clientItems = new ArrayList<ClientItem>();
for (Client client : clients) {
clientItems.add(new ClientItem(client));
}
}
}
// metody pobierające i ustawiające
...
// implementacja interfejsu [IDao]
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
return dao.ajouterRv(user, jour, idCreneau, idClient);
}
...
}
- wiersz 11: wstrzyknięcie odwołania do implementacji warstwy [DAO]. Następnie to właśnie to odwołanie jest wykorzystywane do implementacji interfejsu [IDao] (wiersze 64–80);
- wiersz 14: wstrzyknięcie konfiguracji aplikacji;
- wiersze 33–37: wykorzystanie tej konfiguracji do skonfigurowania różnych elementów architektury aplikacji;
- wiersze 38–46: buforujemy informacje, które będą zasilać listy rozwijane lekarzy i klientów. Zakładamy zatem, że w przypadku zmiany lekarza lub klienta aplikacja musi zostać ponownie uruchomiona. Chodzi tu o pokazanie, że singleton Spring może służyć jako pamięć podręczna dla aplikacji internetowej;
Klasy [MedecinItem] i [ClientItem] wywodzą się z następującej klasy [PersonneItem]:
package rdvmedecins.springthymeleaf.server.models;
import rdvmedecins.client.entities.Personne;
public class PersonneItem {
// element listy
private Long id;
private String texte;
// konstruktor
public PersonneItem() {
}
public PersonneItem(Personne personne) {
id = personne.getId();
texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
}
// metody pobierające i ustawiające
...
}
- wiersz 8: pole [id] będzie wartością atrybutu [value] jednej z opcji listy rozwijanej;
- wiersz 9: pole [texte] będzie tekstem wyświetlanym przez opcję z listy rozwijanej;
8.6.6.3. Klasa [BaseController]
![]() |
Klasa [BaseController] jest klasą nadrzędną dla kontrolerów [RdvMedecinsController] i [RdvMedecinsCorsController]. Utworzenie tej klasy nadrzędnej nie było konieczne. Zgromadzono w niej metody pomocnicze klasy [RdvMedecinsController], z których tylko jedna ma zasadnicze znaczenie. Można je podzielić na trzy grupy:
- metody pomocnicze;
- metody, które zwracają widoki po połączeniu z ich modelami;
- metoda inicjalizacji akcji
| dwie metody pomocnicze, które zwracają listę komunikatów o błędach. Spotkaliśmy się z nimi już wcześniej i korzystaliśmy z nich; |
| wyświetla widok [accueil] bez szablonu |
| zwraca widok [agenda] wraz z jego szablonem |
| zwraca widok [login] bez szablonu |
| zwraca odpowiedź dla klienta, gdy żądana operacja zakończyła się błędem |
| metoda inicjalizacji wszystkich akcji kontrolera [RdvMedecinsController] |
Przyjrzyjmy się dwóm z tych metod.
Metoda [getPartialViewAgenda] odpowiada za wygenerowanie najbardziej złożonego widoku, czyli kalendarza. Jej kod wygląda następująco:
// strumień [agenda]
protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
// konteksty
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
// tworzymy model strony [agenda]
ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
// kalendarz wraz z jego szablonem
thymeleafContext.setVariable("agenda", modelAgenda);
thymeleafContext.setVariable("clientItems", application.getClientItems());
return engine.process("agenda", thymeleafContext);
}
- wiersze 9–10: dwa elementy modelu kalendarza:
- wiersz 9: wyświetlany kalendarz.
- wiersz 10: lista klientów wyświetlana, gdy użytkownik umawia spotkanie;
Metoda [setModelforAgenda] z wiersza 7 wygląda następująco:
// szablon strony [Agenda]
private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
// tytuł strony
String dateFormat = springContext.getMessage("date.format", null, locale);
Medecin médecin = agenda.getMedecin();
String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
// terminy wizyt
ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
int i = 0;
for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
// termin wizyty u lekarza
Creneau créneau = creneauMedecinJour.getCreneau();
ViewModelCreneau modelCréneau = new ViewModelCreneau();
modelCréneaux[i] = modelCréneau;
// identyfikator
modelCréneau.setId(créneau.getId());
// przedział czasowy
modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
créneau.getHfin(), créneau.getMfin()));
Rv rv = creneauMedecinJour.getRv();
// klient i zamówienie
String commande;
if (rv == null) {
modelCréneau.setClient("");
commande = springContext.getMessage("agenda.reserver", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
} else {
Client client = rv.getClient();
modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
commande = springContext.getMessage("agenda.supprimer", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setIdRv(rv.getId());
modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
}
// następny termin
i++;
}
// udostępniamy szablon kalendarza
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- wiersz 6: kalendarz ma tytuł:

lub:

Widać, że format daty zależy od języka. Format ten pobieramy z plików komunikatów (wiersz 4).
- wiersze 11–40: dla każdego przedziału czasowego należy wyświetlić widok:
![]()
lub widok:
![]()
- wiersze 19–20: wyświetlają przedział czasowy;
- wiersze 25–28: sytuacja, w której przedział czasowy jest wolny. Należy wówczas wyświetlić przycisk [Réserver];
- wiersze 31–36: sytuacja, w której przedział czasowy jest zajęty. Należy wówczas wyświetlić zarówno klienta, jak i przycisk [Supprimer];
Inną metodą, którą omówimy bardziej szczegółowo, jest metoda [getActionContext]. Jest ona wywoływana na początku każdej akcji metody [RdvMedecinsController]. Jej sygnatura jest następująca:
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)
Zwraca ona następujący typ [ActionContext]:
public class ActionContext {
// data
private WebContext thymeleafContext;
private WebApplicationContext springContext;
private Locale locale;
private List<String> erreurs;
...
}
- wiersz 4: kontekst Thymeleaf akcji;
- wiersz 5: kontekst Spring akcji;
- wiersz 6: ustawienia regionalne akcji;
- wiersz 7: ewentualna lista komunikatów o błędach;
Jej parametry są następujące:
- [lang]: żądany język dla akcji „en” lub „fr”;
- [origin]: nagłówek HTTP [origin] w przypadku wywołania międzydomenowego;
- [request]: żądanie HTTP w trakcie przetwarzania, co od pewnego czasu nazywa się akcją;
- [response]: odpowiedź, która zostanie wysłana w odpowiedzi na to żądanie;
- [result]: każda akcja z [RdvMedecinsController] otrzymuje wartość przesłaną metodą POST, której poprawność jest sprawdzana. [result] to wynik tego testu;
- [rdvMedecinsController]: kontroler zawierający akcje;
Metoda [getActionContext] jest zaimplementowana w następujący sposób:
// kontekst działania
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
// język?
if (lang == null) {
lang = "fr";
}
// ustawienia regionalne
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// francuski
locale = new Locale("fr", "FR");
} else {
// wszystko inne w języku angielskim
locale = new Locale("en", "US");
}
// nagłówki CORS
rdvMedecinsCorsController.sendOptions(origin, response);
// ActionContext
ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
// błędy inicjalizacji
RdvMedecinsException e = application.getRdvMedecinsException();
if (e != null) {
actionContext.setErreurs(e.getMessages());
return actionContext;
}
// błędy w POST?
if (result != null && result.hasErrors()) {
actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
return actionContext;
}
// brak błędów
return actionContext;
}
- wiersze 3–15: na podstawie parametru [lang] ustala się lokalizację akcji;
- wiersz 17: wysyłane są nagłówki HTTP niezbędne do żądań międzydomenowych. Nie wchodzimy w szczegóły. Zastosowano technikę opisaną w paragrafie 8.4.14;
- wiersz 19: bezbłędne utworzenie obiektu [ActionContext];
- wiersz 21: w paragrafie 8.6.6.2 widzieliśmy, że singleton [ApplicationModel] uzyskuje dostęp do bazy danych w celu pobrania danych zarówno klientów, jak i lekarzy. Dostęp ten może się nie powieść. W takim przypadku rejestrujemy wystąpiony wyjątek. W wierszu 21 pobieramy ten wyjątek;
- wiersze 22–25: jeśli podczas uruchamiania aplikacji wystąpił wyjątek, wykonanie jakiejkolwiek czynności jest niemożliwe. W takim przypadku dla każdej czynności zwracamy obiekt [ActionContext] zawierający komunikaty o błędach z wyjątku;
- wiersze 27–20: analizujemy parametr [result], aby sprawdzić, czy przesłana wartość była prawidłowa, czy nie. Jeśli była nieprawidłowa, zwracamy obiekt [ActionContext] wraz z odpowiednimi komunikatami o błędach;
- wiersz 32: przypadek bez błędów;
Teraz przyjrzyjmy się działaniom kontrolera [RdvMedecinsController]
8.6.6.4. Akcja [/getNavBarStart]
Akcja [/getNavBarStart] zwraca widok [navbar-start]. Jej sygnatura jest następująca:
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
Zwraca następujący typ [Reponse]:
public class Reponse {
// ----------------- właściwości
// status operacji
private int status;
// pasek nawigacyjny
private String navbar;
// jumbotron
private String jumbotron;
// treść strony
private String content;
// kalendarz
private String agenda;
...
}
i posiada następujące parametry:
- [PostLang postlang]: następująca wartość wysłana:
public class PostLang {
// data
@NotNull
private String lang;
...
}
Klasa [PostLang] jest klasą nadrzędną wszystkich wartości przesłanych. Klient musi bowiem zawsze określić język, w którym ma zostać wykonana akcja.
Metoda [getNavbarStart] jest zaimplementowana w następujący sposób:
// navbar-start
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// zwracamy widok [navbar-start]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
return reponse;
}
- wiersz 7: inicjalizacja akcji;
- wiersze 10–13: jeśli metoda inicjalizacji akcji zgłosiła błędy, są one wysyłane w odpowiedzi do klienta (wiersz 12) ze statusem 2:
- wiersze 15–18: wysyłamy widok [navbar-start] ze statusem 1:
W dalszej części omówimy jedynie nowości.
8.6.6.5. Akcja [/getNavbarRun]
Akcja [/getNavBarRun] generuje widok [navbar-run]:
// pasek nawigacyjny – uruchom
@RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// zwracany jest widok [navbar-run]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
Akcja może zwrócić dwa rodzaje odpowiedzi:
- odpowiedź z błędem (wiersze 10–13):
- odpowiedź z widokiem [navbar-run]:
8.6.6.6. Akcja [/getJumbotron]
Akcja [/getJumbotron] zwraca widok [jumbotron]:
// jumbotron
@RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
@ResponseBody
public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// zwracany jest widok [jumbotron]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
return reponse;
}
Akcja może zwrócić dwa rodzaje odpowiedzi:
- odpowiedź z błędem (wiersze 10–13):
- odpowiedź z widokiem [jumbotron]:
8.6.6.7. Akcja [/getLogin]
Akcja [/getLogin] zwraca widok [login]:
@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
@ResponseBody
public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// zwracany jest widok [login]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
reponse.setContent(getPartialViewLogin(thymeleafContext));
return reponse;
}
Akcja może zwrócić dwa rodzaje odpowiedzi:
- odpowiedź z błędem (wiersze 9–11):
- odpowiedź z widokiem [login]:
8.6.6.8. Akcja [/getAccueil]
Akcja [/getAccueil] zwraca widok [accueil]. Jej sygnatura jest następująca:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- w wierszu 3 wartość wpisana ma typ [PostUser], jak poniżej:
public class PostUser extends PostLang {
// dane
@NotNull
private User user;
...
}
- wiersz 1: klasa [PostUser] rozszerza klasę [PostLang], a zatem zawiera język;
- wiersz 4: użytkownik, który próbuje uzyskać widok;
Kod implementacyjny wygląda następująco:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// widok [accueil] jest chroniony
try{
// użytkownik
User user = postUser.getUser();
// sprawdzane są dane uwierzytelniające [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
// zwracany jest błąd
return getViewErreurs(thymeleafContext, e.getMessages());
}
// zwracany jest widok [accueil]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- wiersze 15–22: należy zauważyć, że strona [accueil] jest chroniona, a zatem użytkownik musi zostać uwierzytelniony;
Akcja może zwrócić dwa rodzaje odpowiedzi:
- odpowiedź z błędem (wiersze 11 i 21):
- odpowiedź z widokiem [accueil] (wiersze 24–27):
8.6.6.9. Akcja [/getNavbarRunJumbotronAccueil]
Akcja [/getNavbarRunJumbotronAccueil] generuje widoki [navbar-run, jumbotron, accueil]. Ma ona następujący podpis:
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- wiersz 3: wartość wysyłana jest typu [PostUser];
Implementacja akcji wygląda następująco:
// pasek nawigacyjny + jumbotron + strona główna
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// widok [accueil] jest chroniony
try {
// użytkownik
User user = postUser.getUser();
// sprawdzane są dane uwierzytelniające [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
// zwracany jest błąd
return getViewErreurs(thymeleafContext, e.getMessages());
}
// wysyłana jest odpowiedź
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
Akcja może zwrócić dwa rodzaje odpowiedzi:
- odpowiedź z błędem (wiersze 13, 23):
- odpowiedź z widokami [navbar-run, jumbotron, accueil] (wiersze 26–31):
8.6.6.10. Akcja [/getAgenda]
Akcja [/getAgenda] zwraca widok [agenda]. Jej sygnatura jest następująca:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- wiersz 3: wartość wpisana jest typu [PostGetAgenda], zgodnie z poniższym:
public class PostGetAgenda extends PostUser {
// dane
@NotNull
private Long idMedecin;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- wiersz 1: klasa [PostGetAgenda] rozszerza klasę [PostUser], a zatem zawiera język i użytkownika;
- wiersz 5: numer lekarza, którego kalendarz ma zostać wyświetlony;
- wiersz 8: dzień, dla którego potrzebny jest kalendarz;
Implementacja wygląda następująco:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result, rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
Locale locale = actionContext.getLocale();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// sprawdzana jest poprawność przesłanych danych
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// zwracany jest widok [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- do wiersza 14 mamy kod, który stał się już standardowy;
- wiersze 16–21: przeprowadzamy dodatkową weryfikację przesłanej wartości. Data musi być późniejsza lub równa dzisiejszej. Aby to sprawdzić, używamy walidatora:
package rdvmedecins.web.validators;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
public class PostGetAgendaValidator implements Validator {
public PostGetAgendaValidator() {
}
@Override
public boolean supports(Class<?> classe) {
return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
}
@Override
public void validate(Object post, Errors errors) {
// wybrany dzień spotkania
Date jour = null;
if (post instanceof PostGetAgenda) {
jour = ((PostGetAgenda) post).getJour();
} else {
if (post instanceof PostValiderRv) {
jour = ((PostValiderRv) post).getJour();
}
}
// przekształca się daty do formatu yyyy-MM-dd
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String strJour = sdf.format(jour);
String strToday = sdf.format(new Date());
// wybrany dzień nie może poprzedzać dzisiejszej daty
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- wiersz 19: moduł walidacyjny obsługuje dwie klasy: [PostGetAgenda] i [PostValiderRv];
Wróćmy do kodu akcji [/getAgenda]:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
...
// działanie
try {
// kalendarz lekarza
AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
// odpowiedź
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException e1) {
// zwracamy widok [erreurs]
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// zwracamy widok [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- wiersze 9–10: na podstawie przesłanych parametrów pobieramy harmonogram lekarza;
- wiersze 12–13: zwracamy harmonogram:
- wiersze 17, 21: zwracamy odpowiedź z błędami:
8.6.6.11. Akcja [/getNavbarRunJumbotronAccueilAgenda]
Akcja [/getNavbarRunJumbotronAccueilAgenda] zwraca widoki [navbar-run, jumbotron, accueil, agenda]. Jej implementacja wygląda następująco:
@RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// kalendarz
Reponse agenda = getAgenda(post, result, request, response, null);
if (agenda.getStatus() != 1) {
return agenda;
}
// wysyłamy odpowiedź
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
reponse.setAgenda(agenda.getAgenda());
return reponse;
}
- wiersze 15–18: wykorzystujemy istnienie akcji [/getAgenda], aby ją wywołać. Następnie sprawdzamy status w odpowiedzi (wiersz 16). Jeśli wykryjemy błąd, nie kontynuujemy dalszych działań i zwracamy odpowiedź;
- wiersz 20: wysyłamy żądane widoki:
8.6.6.12. Akcja [/supprimerRv]
Akcja [/supprimerRv] umożliwia usunięcie spotkania. Jej sygnatura jest następująca:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- wiersz 3: wysyłana wartość ma typ [PostSupprimerRv], jak poniżej:
public class PostSupprimerRv extends PostUser {
// dane
@NotNull
private Long idRv;
..
}
- wiersz 1: klasa [PostSupprimerRv] rozszerza klasę [PostUser], a zatem zawiera język i użytkownika;
- wiersz 5: numer spotkania do usunięcia;
Implementacja tej akcji wygląda następująco:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// konteksty działania
ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// wartości przesłane
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// usuwamy termin
AgendaMedecinJour agenda = null;
try {
// przywracamy spotkanie
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// usuwamy powiązane spotkanie
application.supprimerRv(user, idRv);
// odświeżono kalendarz lekarza
agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
// zwracamy nowy kalendarz
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// zwracamy widok [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// zwracamy widok [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- wiersz 22: pobieramy wizytę, którą należy usunąć. Jeśli nie istnieje, zgłaszany jest wyjątek;
- wiersze 23–25: na podstawie tego terminu ustala się lekarza i odpowiedni dzień. Informacje te są niezbędne do odtworzenia kalendarza lekarza;
- wiersz 27: wizyta zostaje usunięta;
- wiersz 29: pobieramy nowy kalendarz lekarza. To ważne. Oprócz właśnie zwolnionego terminu inni użytkownicy aplikacji mogli wprowadzić zmiany w kalendarzu. Ważne jest, aby zwrócić użytkownikowi jego najnowszą wersję;
- wiersze 31–34: zwracamy kalendarz:
8.6.6.13. Akcja [/validerRv]
Akcja [/validerRv] dodaje wizytę do kalendarza lekarza. Jej sygnatura wygląda następująco:
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- wiersz 3: wysyłana wartość ma typ [PostValiderRv], który wygląda następująco:
public class PostValiderRv extends PostUser {
// dane
@NotNull
private Long idCreneau;
@NotNull
private Long idClient;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- wiersz 1: klasa [PostValiderRv] rozszerza klasę [PostUser], a zatem zawiera język i użytkownika;
- wiersz 5: numer przedziału czasowego;
- wiersz 7: numer klienta, dla którego dokonano rezerwacji;
- wiersz 10: dzień spotkania;
Implementacja akcji wygląda następująco:
// potwierdzenie spotkania
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// konteksty akcji
ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebApplicationContext springContext = actionContext.getSpringContext();
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// błędy?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// sprawdzamy poprawność daty spotkania
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// zwracany jest widok [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
// wartości zapisane
User user = postValiderRv.getUser();
long idClient = postValiderRv.getIdClient();
long idCreneau = postValiderRv.getIdCreneau();
Date jour = postValiderRv.getJour();
// działanie
try {
// pobierane są informacje o terminie
Creneau créneau = application.getCreneauById(user, idCreneau);
long idMedecin = créneau.getIdMedecin();
// dodajemy termin spotkania
application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
// odświeżamy kalendarz
AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
new SimpleDateFormat("yyyy-MM-dd").format(jour));
// zwracamy nowy kalendarz
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// zwracamy widok [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// zwracamy widok [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
Kod jest analogiczny do kodu akcji [/supprimerRv].
8.6.7. Krok 4: testy serwera Spring/Thymeleaf
Teraz przetestujemy różne powyższe akcje za pomocą wtyczki Chrome [Advanced Rest Client] (patrz punkt 9.6).
8.6.7.1. Konfiguracja testów
Wszystkie akcje oczekują wartości przesłanej metodą POST. Prześlemy różne warianty następującego ciągu znaków:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Ta wartość przesyłana zawiera informacje zbędne dla większości akcji. Są one jednak ignorowane przez akcje, które je otrzymują, i nie powodują błędu. Zaletą tej wartości przesyłanej jest to, że obejmuje ona różne wartości, które należy przesłać.
8.6.7.2. Akcja [/getNavbarStart]
![]() |
- do [1], akcja testowana;
- na [2], wartość przesłana;
- w [3] wartość zaksięgowana jest ciągiem znaków jSON;
- w [4], widok [navbar-start] jest żądany w języku angielskim;
Otrzymany wynik jest następujący:
![]() |
Otrzymaliśmy widok [navbar-start] w języku angielskim (podświetlone pola).
Teraz popełnijmy błąd. Zmienimy wartość atrybutu [lang] z wartości wysłanej na null. Otrzymujemy następujący wynik:
![]() |
Otrzymaliśmy komunikat o błędzie (status 2) wskazujący, że pole [lang] jest obowiązkowe.
8.6.7.3. Akcja [/getNavbarRun]
Wysyłamy akcję [getNavbarRun] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
8.6.7.4. Akcja [/getJumbotron]
Wysyłamy akcję [getJumbotron] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
8.6.7.5. Akcja [/getLogin]
Wysyłamy akcję [getLogin] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
8.6.7.6. Akcja [/getAccueil]
Wysyłamy akcję [getAccueil] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
Powtarzamy operację z nieznanym użytkownikiem:
{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymano następujący wynik:
![]() |
Zaczynamy ponownie z istniejącym użytkownikiem, który jednak nie ma uprawnień do korzystania z aplikacji:
{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
8.6.7.7. Akcja [/getAgenda]
Wysyłamy żądanie akcji [getAgenda] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
Powtarzamy operację, wybierając datę wcześniejszą niż dzisiejsza:
![]() |
Próbujemy ponownie z fikcyjnym lekarzem:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
8.6.7.8. Akcja [/getNavbarRunJumbotronAccueil]
Wysyłamy akcję [getNavbarRunJumbotronAccueil] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymany wynik jest następujący:
![]() |
To samo w przypadku nieznanego użytkownika:
![]() |
8.6.7.9. Akcja [/getNavbarRunJumbotronAccueilAgenda]
Wysyłamy żądanie akcji [getNavbarRunJumbotronAccueilAgenda] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymujemy następujący wynik:
![]() |
Wprowadzamy nieistniejącego lekarza:
![]() |
8.6.7.10. Akcja [/supprimerRv]
Wysyłamy żądanie akcji [supprimerRv] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Numer referencyjny 93 nie istnieje. Uzyskany wynik jest następujący:
![]() |
Z istniejącym terminem:
![]() |
W bazie danych można sprawdzić, czy spotkanie zostało rzeczywiście usunięte. Nowy kalendarz jest odsyłany.
8.6.7.11. Akcja [/validerRv]
Prosimy o wykonanie operacji [validerRv] z następującą wartością:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Otrzymano następujący wynik:
![]() |
W bazie danych można sprawdzić, czy spotkanie zostało rzeczywiście utworzone. Nowy kalendarz został odesłany.
Wykonujemy tę samą operację z nieistniejącym numerem przedziału czasowego:
![]() |
Wykonujemy tę samą czynność z nieistniejącym numerem klienta:
![]() |
8.6.8. Krok 5: Napisanie kodu klienckiego JavaScript
Wróćmy do architektury serwera [Web1]:
![]() |
Klient [2] serwera [Web1] jest klientem JavaScript typu APU (aplikacja jednostronicowa):
- klient żąda strony startowej od serwera WWW (niekoniecznie [Web1]);
- pobiera kolejne strony z serwera [Web1] za pomocą wywołań Ajax;
Aby stworzyć ten klient, wykorzystamy narzędzie [Webstorm] (patrz paragraf 9.8). Narzędzie to wydało mi się bardziej praktyczne niż STS. Jego główną zaletą jest to, że oferuje autouzupełnianie podczas wpisywania kodu, a także kilka opcji dostępnych w narzędziu refactoring. Pozwala to uniknąć wielu błędów.
8.6.8.1. Projekt JS
Projekt JS ma następującą strukturę katalogów:
![]() |
- w [1] znajduje się cały klient JS. [boot.html] to strona startowa. Będzie to jedyna strona ładowana przez przeglądarkę;
- w [2] – arkusze stylów komponentów Bootstrap;
- w pliku [3] znajdują się nieliczne obrazy wykorzystywane przez aplikację;
![]() |
- w pliku [4] znajdują się skrypty JS. To właśnie tutaj skupia się nasza praca;
- w pliku [5] – wykorzystywane biblioteki JS: głównie jQuery oraz biblioteki komponentów Bootstrap;
8.6.8.2. Architektura kodu
Kod został podzielony na trzy warstwy:
![]() |
- warstwa [présentation] zawiera funkcje inicjalizacji strony [boot.xml] oraz różne komponenty Bootstrap. Jest ona zaimplementowana w pliku [ui.js];
- warstwa [événements] zawiera wszystkie procedury obsługi zdarzeń z warstwy [présentation]. Jest ona zaimplementowana przez plik [evts.js];
- warstwa [DAO] wysyła żądania HTTP do serwera [Web1]. Jest ona zaimplementowana przez plik [dao.js];
8.6.8.3. Warstwa [présentation]
![]() |
Warstwa [présentation] jest zaimplementowana przez następujący plik [ui.js]:
//warstwa [présentation]
var ui = {
// zmienne globalne;
"agenda": "",
"resa": "",
"langue": "",
"urlService": "http://localhost:8081",
"page": "login",
"jourAgenda": "",
"idMedecin": "",
"user": {},
"login": {},
"exceptionTitle": {},
"calendar_infos": {},
"erreur": "",
"idCreneau": "",
"done": "",
// komponenty widoku
"body": "",
"navbar": "",
"jumbotron": "",
"content": "",
"exception": "",
"exception_text": "",
"exception_title": "",
"loading": ""
};
// warstwa zdarzeń
var evts = {};
// warstwa [dao]
var dao = {};
// ------------ dokument gotowy
$(document).ready(function () {
// inicjalizacja dokumentu
console.log("document.ready");
// elementy strony
ui.navbar = $("#navbar");
ui.jumbotron = $("#jumbotron");
ui.content = $("#content");
ui.erreur = $("#erreur");
ui.exception = $("#exception");
ui.exception_text = $("#exception-text");
ui.exception_title = $("#exception-title");
// zapisujemy stronę logowania, aby móc ją odtworzyć
ui.login.lang = ui.langue;
ui.login.navbar = ui.navbar.html();
ui.login.jumbotron = ui.jumbotron.html();
ui.login.content = ui.content.html();
// URL usługi
$("#urlService").val(ui.urlService);
});
// ------------------------ funkcje inicjalizacji komponentów Bootstrap
ui.initNavBarStart = function () {
...
};
ui.initNavBarRun = function () {
...
};
ui.initChoixMedecinJour = function () {
...
};
ui.updateCalendar = function (renew) {
...
};
// wyświetla wybrany dzień
ui.displayJour = function () {
...
};
ui.initAgenda = function () {
...
};
ui.initResa = function () {
...
};
- Aby oddzielić warstwy od siebie, postanowiono umieścić je w trzech obiektach:
- [ui] dla warstwy [présentation] (wiersze 2–27),
- [evts] dla warstwy zarządzania zdarzeniami (wiersz 29),
- [dao] dla warstwy [DAO] (wiersz 31);
Takie rozdzielenie warstw na trzy obiekty pozwala uniknąć pewnej liczby konfliktów nazw zmiennych i funkcji. Każda warstwa wykorzystuje zmienne i funkcje opatrzone prefiksem obiektu, który ją hermetyzuje.
- wiersze 38–44: zapisuje się pola, które będą zawsze obecne niezależnie od wyświetlanych widoków. Pozwala to uniknąć powtarzających się i niepotrzebnych wyszukiwań jQuery;
- wiersze 46–49: lokalnie zapisuje się stronę startową, aby móc ją przywrócić, gdy użytkownik wyloguje się i nie zmienił języka;
- wiersze 54–83: funkcje inicjalizacji komponentów Bootstrap. Wszystkie zostały omówione w rozdziale poświęconym tym komponentom w paragrafie 8.6.4;
8.6.8.4. Funkcje pomocnicze warstwy [événements]
![]() |
Obsługa zdarzeń została umieszczona w pliku [evts.js]. Obsługa zdarzeń regularnie korzysta z kilku funkcji. Przedstawiamy je poniżej:
// rozpoczęcie oczekiwania
evts.beginWaiting = function () {
// początek oczekiwania
ui.loading = $("#loading");
ui.loading.show();
ui.exception.hide();
ui.erreur.hide();
evts.travailEnCours = true;
};
// koniec oczekiwania
evts.stopWaiting = function () {
// koniec oczekiwania
evts.travailEnCours = false;
ui.loading = $("#loading");
ui.loading.hide();
};
// wyświetlenie wyniku
evts.showResult = function (result) {
// wyświetlanie otrzymanych danych
var data = result.data;
// analiza statusu
switch (result.status) {
case 1:
// błąd?
if (data.status == 2) {
ui.erreur.html(data.content);
ui.erreur.show();
} else {
if (data.navbar) {
ui.navbar.html(data.navbar);
}
if (data.jumbotron) {
ui.jumbotron.html(data.jumbotron);
}
if (data.content) {
ui.content.html(data.content)
}
if (data.agenda) {
ui.agenda = $("#agenda");
ui.resa = $("#resa");
}
}
break;
case 2:
// wyświetlanie błędu
evts.showException(data);
break;
}
};
// ------------ różne funkcje
evts.showException = function (data) {
// wyświetlanie błędu
ui.exception.show();
ui.exception_text.html(data);
ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
- wiersz 2: funkcja [evts.beginwaiting] jest wywoływana przed każdą asynchroniczną akcją [DAO];
- wiersze 4–5: wyświetlany jest animowany obrazek wskazujący na oczekiwanie;
- wiersze 6–7: ukrywa się obszar wyświetlania błędów i wyjątków (nie są to te same elementy);
- wiersz 8: odnotowuje się, że trwa operacja asynchroniczna;
- wiersz 12: funkcja [evts.stopwaiting] jest wywoływana po tym, jak asynchroniczna akcja [DAO] zwróciła wynik;
- wiersz 14: odnotowuje się, że operacja asynchroniczna została zakończona;
- wiersz 15: ukrywa się animowany obrazek oczekiwania;
- wiersz 20: funkcja [evts.showResult] wyświetla wynik [result] z asynchronicznej akcji [DAO]. Wynikiem jest obiekt JS o następującej postaci: {'status':status,'data':data,'sendMeBack':sendMeBack}.
- wiersze 47–50: wykorzystywane w przypadku [result.status==2]. Dzieje się tak, gdy serwer [Web1] wysyła odpowiedź z nagłówkiem błędu HTTP (na przykład 403 forbidden). W takim przypadku [data] to ciąg znaków jSON wysłany przez serwer w celu zgłoszenia błędu;
- wiersz 25: sytuacja, w której otrzymano prawidłową odpowiedź od serwera [Web1]. Pole [data] zawiera wówczas odpowiedź serwera: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
- wiersz 27: sytuacja, w której serwer [Web1] wysłał odpowiedź z błędem {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':błędy};
- wiersze 28–29: wyświetlany jest widok [erreurs];
- wiersze 31–33: ewentualne wyświetlenie paska nawigacyjnego;
- wiersze 34–36: ewentualne wyświetlenie jumbotronu;
- wiersze 37–39: ewentualne wyświetlenie pola [data.content]. W zależności od sytuacji odpowiada to jednemu z widoków [accueil, agenda];
- wiersze 40–43: jeśli kalendarz został zregenerowany, pobierane są pewne odniesienia do jego komponentów, aby nie trzeba było ich wyszukiwać za każdym razem, gdy będą potrzebne;
- wiersz 54: funkcja [evts.showException] służy do wyświetlenia tekstu wyjątku zawartego w jej parametrze [data];
- wiersze 57–58: wyświetlany jest tekst wyjątku;
- wiersz 58: tytuł wyjątku zależy od aktualnie ustawionego języka;
Plik [evts.js] zawiera ponad 300 wierszy kodu, których nie będę omawiać w całości. Przytoczę jedynie kilka przykładów, aby pokazać istotę tej warstwy.
8.6.8.5. Logowanie użytkownika

Logowanie użytkownika jest realizowane przez następującą funkcję:
// ------------------------ połączenie
evts.connecter = function () {
// pobieramy wartości do wysłania
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
// ustawianie parametru URL serwera
ui.urlService = $("#urlService").val().trim();
dao.setUrlService(ui.urlService);
// parametry żądania
var post = {
"user": {
"login": login,
"passwd": passwd
},
"lang": ui.langue
};
var sendMeBack = {
"user": {
"login": login,
"passwd": passwd
},
"caller": evts.connecterDone
};
// wysyłamy żądanie
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- wiersze 4–5: pobieramy login i hasło użytkownika;
- wiersze 7–8: pobierany jest identyfikator URL z usługi [Web1]. Jest on zapisywany zarówno w warstwie [ui], jak i w warstwie [dao];
- wiersze 10–16: wartość do przesłania: aktualny język oraz użytkownik próbujący się zalogować;
- wiersze 17–23: obiekt [sendMeBack] jest obiektem przekazywanym do funkcji [DAO], która zostanie wywołana i która musi zwrócić ten obiekt do funkcji z wiersza 22. W tym przypadku obiekt [sendMeBack] zawiera dane użytkownika próbującego się zalogować;
- wiersze 25–29: funkcja [evts.execute] może wykonać sekwencję akcji asynchronicznych. W tym przypadku przekazywana jest lista składająca się z jednej akcji. Jej pola są następujące:
- [name]: nazwa akcji asynchronicznej do wykonania,
- [post]: wartość, którą należy przesłać na serwer [Web1],
- [sendMeBack]: wartość, którą akcja asynchroniczna ma zwrócić wraz z wynikiem;
Zanim omówimy szczegółowo funkcję [evts.execute], przyjrzyjmy się funkcji [evts.connecterDone] z linii 22. Jest to funkcja, do której wywołana funkcja asynchroniczna [DAO] musi zwrócić swój wynik:
evts.connecterDone = function (result) {
// wyświetlanie wyniku
evts.showResult(result);
// Czy połączenie się powiodło?
if (result.status == 1 && result.data.status == 1) {
// strona
ui.page = "accueil-sans-agenda";
// rejestrujemy użytkownika
ui.user = result.sendMeBack.user;
}
};
- wiersz 3: wyświetlany jest wynik zwrócony przez serwer [Web1];
- wiersz 5: jeśli wynik ten nie zawiera błędów, zapisuje się typ nowej strony (wiersz 7) oraz uwierzytelnionego użytkownika (wiersz 9);
Funkcja [evts.execute] wykonuje sekwencję działań asynchronicznych:
// wykonanie sekwencji czynności
evts.execute = function (actions) {
// praca w toku?
if (evts.travailEnCours) {
// nic się nie dzieje
return;
}
// oczekiwanie
evts.beginWaiting();
// wykonywanie czynności
dao.doActions(actions, evts.stopWaiting);
};
- wiersz 2: parametr [actions] to lista działań asynchronicznych do wykonania;
- wiersze 4–7: wykonanie jest akceptowane tylko wtedy, gdy nie ma już innego w toku;
- wiersz 9: uruchamia się oczekiwanie;
- wiersz 11: wysyłane jest żądanie do warstwy [DAO] o wykonanie sekwencji akcji. Drugim parametrem jest nazwa funkcji, która ma zostać wykonana po zwróceniu wyników przez wszystkie akcje z sekwencji;
Nie będziemy teraz szczegółowo omawiać funkcji [dao.doActions]. Przyjrzyjmy się innemu zdarzeniu.
8.6.8.6. Zmiana języka

Zmianę języka zapewnia następująca funkcja:
// ------------------------ zmiana języka
evts.setLang = function (lang) {
// zmiana języka?
if (lang == ui.langue) {
// nie wykonuje się żadnych czynności
return;
}
// nowy język
ui.langue = lang;
// którą stronę należy przetłumaczyć?
switch (ui.page) {
case "login":
evts.getLogin();
break;
case "accueil-sans-agenda":
evts.getAccueilSansAgenda();
break;
case "accueil-avec-agenda":
evts.getAccueilAvecAgenda(ui);
break;
}
};
- wiersz 2: parametr [lang] to nowy język: „fr” lub „en”;
- wiersze 4–7: jeśli nowy język jest aktualnie używanym językiem, nie wykonuje się żadnych czynności;
- wiersz 9: zapisuje się nowy język;
- wiersze 12–20: w przypadku zmiany języka należy ponownie wygenerować stronę aktualnie wyświetlaną przez przeglądarkę. Istnieją trzy możliwe strony:
- strona o nazwie [login], na której wyświetlana jest strona uwierzytelniania,
- strona o nazwie [accueil-sans-agenda], która jest wyświetlana zaraz po pomyślnym uwierzytelnieniu,
- strona o nazwie [accueil-avec-agenda], która jest wyświetlana po wyświetleniu pierwszego kalendarza. Następnie pozostaje ona wyświetlana na stałe aż do wylogowania się użytkownika;
Zajmiemy się teraz stroną o nazwie [accueil-avec-agenda]. Istnieją trzy wersje tej funkcji:
![]() |
- wersja [ getAccueilAvecAgenda-one] wykonuje jedną akcję asynchroniczną;
- wersja [ getAccueilAvecAgenda-parallel] wykonuje cztery operacje asynchroniczne równolegle;
- wersja [ getAccueilAvecAgenda-sequence] powoduje wykonanie czterech akcji asynchronicznych jedna po drugiej;
8.6.8.7. Funkcja [ getAccueilAvecAgenda-one]
Jest to następująca funkcja:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// parametry zapytania
var post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
var sendMeBack = {
"caller": evts.getAccueilAvecAgendaDone
};
// zapytanie
evts.execute([{
"name": "accueil-avec-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- wiersze 4–9: wartość do przesłania zawiera dane zalogowanego użytkownika, wybrany język, numer lekarza, którego kalendarz ma zostać wyświetlony, oraz dzień, dla którego ma zostać wyświetlony kalendarz;
- wiersze 10–12: obiekt [sendMeBack] to obiekt, który zostanie zwrócony do funkcji z wiersza 11. W tym przypadku nie zawiera on żadnych informacji;
- wiersze 14–18: wykonanie sekwencji akcji asynchronicznej o nazwie [accueil-avec-agenda] (wiersz 15);
- wiersz 11: funkcja wykonywana po zwróceniu wyniku przez akcję asynchroniczną o nazwie [accueil-avec-agenda];
Funkcja [evts.getAccueilAvecAgendaDone] z wiersza 11 wyświetla wynik funkcji asynchronicznej o nazwie [accueil-avec-agenda]:
evts.getAccueilAvecAgendaDone = function (result) {
// wyświetlanie wyników
evts.showResult(result);
// nowa strona?
if (result.status == 1 && result.data.status == 1) {
ui.page = "accueil-avec-agenda";
}
};
- wiersz 1: [result] jest wynikiem funkcji asynchronicznej o nazwie [accueil-avec-agenda];
- wiersz 3: wynik ten jest wyświetlany;
- wiersz 5: jeśli wynik jest poprawny, zapisujemy nową stronę (wiersz 6);
8.6.8.8. Funkcja [ getAccueilAvecAgenda-parallel]
Jest to następująca funkcja:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// działania [navbar-run, jumbotron, accueil, agenda] w //
// pasek nawigacyjny-run
var navbarRun = {
"name": "navbar-run"
};
navbarRun.post = {
"lang": ui.langue
};
navbarRun.sendMeBack = {
"caller": evts.showResult
};
// jumbotron
var jumbotron = {
"name": "jumbotron"
};
jumbotron.post = {
"lang": ui.langue
};
jumbotron.sendMeBack = {
"caller": evts.showResult
};
// strona główna
var accueil = {
"name": "accueil"
};
accueil.post = {
"lang": ui.langue,
"user": ui.user
};
accueil.sendMeBack = {
"caller": evts.showResult
};
// kalendarz
var agenda = {
"name": "agenda"
};
agenda.post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'dzień: ui.jourAgenda,
"caller": evts.getAgendaDone
};
// wykonanie akcji w //
evts.execute([navbarRun, jumbotron, accueil, agenda])
};
- wiersz 51: tym razem wykonujemy cztery akcje asynchroniczne. Zostaną one wykonane równolegle;
- wiersze 5–13: definicja akcji [navbarRun], która pobiera pasek nawigacyjny [navbar-run];
- wiersz 12: funkcja, która ma zostać wykonana, gdy akcja asynchroniczna [navbarRun] zwróci wynik;
- wiersze 15–23: definicja akcji [jumbotron], która pobiera widok [jumbotron];
- wiersz 22: funkcja do wykonania po zwróceniu wyniku przez akcję asynchroniczną [jumbotron];
- wiersze 25–34: definicja akcji [accueil], która pobiera widok [accueil];
- wiersz 33: funkcja do wykonania po zwróceniu wyniku przez akcję asynchroniczną [accueil];
- wiersze 36–49: definicja akcji [agenda], która pobiera widok [jumbotron];
- wiersz 48: funkcja do wykonania po zwróceniu wyniku przez akcję asynchroniczną [agenda];
8.6.8.9. Funkcja [ getAccueilAvecAgenda-sequence]
Jest to następująca funkcja:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// działania [navbar-run, jumbotron, accueil, agenda] w kolejności
// kalendarz
var agenda = {
"name" : "agenda"
};
agenda.post = {
"user" : ui.user,
"lang" : ui.langue,
"idMedecin" : ui.idMedecin,
"jour" : ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin: ui.idMedecin,
'dzień: ui.jourAgenda,
"caller" : evts.getAgendaDone
};
// strona główna
var accueil = {
"name" : "accueil"
};
accueil.post = {
"lang" : ui.langue,
"user" : ui.user
};
accueil.sendMeBack = {
"caller" : evts.showResult,
"next" : agenda
};
// jumbotron
var jumbotron = {
"name" : "jumbotron"
};
jumbotron.post = {
"lang" : ui.langue
};
jumbotron.sendMeBack = {
"caller" : evts.showResult,
"next" : accueil
};
// pasek nawigacyjny-uruchom
var navbarRun = {
"name" : "navbar-run"
};
navbarRun.post = {
"lang" : ui.langue
};
navbarRun.sendMeBack = {
"caller" : evts.showResult,
"next" : jumbotron
};
// sekwencyjne wykonywanie akcji
evts.execute([ navbarRun ])
};
- wiersz 54: wykonywana jest akcja [navbarRun]. Po jej zakończeniu przechodzi się do następnej: [jumbotron], wiersz 51. Ta akcja jest następnie wykonywana. Po jej zakończeniu przechodzi się do następnej: [accueil], wiersz 40. Ta akcja jest następnie wykonywana. Po jej zakończeniu przechodzi się do następnej: [agenda], wiersz 29. Ta z kolei jest wykonywana. Po jej zakończeniu proces się zatrzymuje, ponieważ akcja [agenda] nie ma następnej akcji.
8.6.8.10. Warstwa [DAO]
![]() |
Plik [dao.js] zawiera wszystkie funkcje warstwy [DAO]. Przedstawimy je po kolei:
// URL udostępniane przez serwer
dao.urls = {
"login": "/getLogin",
"accueil": "/getAccueil",
"jumbotron": "/getJumbotron",
"agenda": "/getAgenda",
"supprimerRv": "/supprimerRv",
"validerRv": "/validerRv",
"navbar-start": "/getNavbarStart",
"navbar-run": "/getNavbarRun",
"accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
"accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interfejs
// adres URL serwera
dao.setUrlService = function (urlService) {
dao.urlService = urlService;
};
- wiersze 16–18: funkcja umożliwiająca ustawienie URL usługi [Web1];
- wiersze 2–13: słownik łączący nazwę akcji asynchronicznej z URL serwera [Web1], do którego ma zostać wysłane zapytanie;
// ------------------ ogólne zarządzanie akcjami
// wykonanie sekwencji działań asynchronicznych
dao.doActions = function (actions, done) {
// przetwarzanie akcji
dao.actionsCount = actions.length;
dao.actionIndex = 0;
for (var i = 0; i < dao.actionsCount; i++) {
// asynchroniczne żądanie DAO
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, actions[i], done);
}
};
- wiersz 3: funkcja [dao.doActions] wykonuje sekwencję akcji asynchronicznych [actions]. Parametr [done] to funkcja, która ma zostać wykonana po zwróceniu wyników przez wszystkie akcje;
- wiersze 7–12: działania asynchroniczne są wykonywane równolegle. Jednak w przypadku, gdy jedno z nich ma następne działanie, jest ono wykonywane po zakończeniu działania, które je poprzedza;
- wiersz 9: obiekt [Deferred] znajduje się w stanie [pending];
- wiersz 10: gdy obiekt ten przejdzie do stanu [resolved], zostanie wykonana funkcja [dao.actionDone];
- wiersz 11: akcja nr i z listy jest wykonywana asynchronicznie. Jako parametr przekazywany jest parametr [done] z wiersza 3;
Funkcja [dao.actionDone], która jest wykonywana na końcu każdej akcji asynchronicznej, ma następującą postać:
// otrzymano wynik
dao.actionDone = function (result) {
// numer dzwoniącego?
var sendMeBack = result.sendMeBack;
if (sendMeBack && sendMeBack.caller) {
sendMeBack.caller(result);
}
// następny?
if (sendMeBack && sendMeBack.next) {
// asynchroniczne żądanie DAO
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
}
// zakończone?
dao.actionIndex++;
if (dao.actionIndex == dao.actionsCount) {
// zakończone?
if (sendMeBack && sendMeBack.done) {
sendMeBack.done(result);
}
}
};
- wiersz 2: funkcja [dao.actionDone] otrzymuje wynik [result] z jednej z akcji asynchronicznych z listy akcji do wykonania;
- wiersze 4–7: jeśli zakończona akcja asynchroniczna określała funkcję, do której należy zwrócić wynik, funkcja ta jest wywoływana;
- wiersze 9–14: jeśli zakończona akcja asynchroniczna ma następną, wówczas ta akcja jest z kolei wykonywana;
- wiersz 16: akcja została zakończona. Zwiększa się licznik zakończonych akcji. Akcja, która ma nieokreśloną liczbę kolejnych akcji, liczy się jako jedna akcja;
- wiersze 19–21: jeśli początkowo określono, że funkcja [done] ma zostać wykonana po zwróceniu wyników przez wszystkie kolejne akcje, to funkcja ta jest teraz wykonywana;
Metoda [dao.doAction] wykonuje akcję asynchroniczną:
// wykonanie akcji
dao.doAction = function (deferred, action, done) {
// funkcja „done” do wbudowania w akcję
if (action.sendMeBack) {
action.sendMeBack.done = done;
} else {
action.sendMeBack = {
"done": done
};
}
// wykonanie akcji
dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
- wiersze 4–10: jak właśnie widzieliśmy, funkcja, która będzie przetwarzać wynik akcji asynchronicznej, która ma zostać wykonana, musi mieć dostęp do funkcji [done]. W tym celu umieszczamy tę ostatnią w obiekcie [sendMeBack], który będzie częścią wyniku operacji asynchronicznej;
- wiersz 12: uruchamiamy funkcję [dao.executePost], która wykonuje wywołanie funkcji HTTP na serwerze [Web1]. Celem funkcji URL jest funkcja URL powiązana z nazwą akcji do wykonania;
Funkcja [dao.executePost] wykonuje wywołanie HTTP:
// żądanie HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
// ręczne wywołanie Ajax
$.ajax({
headers: {
'„Accept”: „application/json”,
'„Content-Type”: „application/json”
},
url: dao.urlService + url,
type: 'POST',
data: JSON3.stringify(post),
dataType: 'json',
success: function (data) {
// zwracamy wynik
deferred.resolve({
"status": 1,
"data": data,
"sendMeBack": sendMeBack
});
},
error: function (jqXHR, textStatus, errorThrown) {
var data;
if (jqXHR.responseText) {
data = jqXHR.responseText;
} else {
data = textStatus;
}
// zwracamy błąd
deferred.resolve({
"status": 2,
"data": data,
"sendMeBack": sendMeBack
});
}
});
};
Funkcję tę omówiliśmy już wcześniej. Warto jedynie zauważyć w wierszu 9, że funkcją docelową URL jest połączenie funkcji URL serwera [Web1] z funkcją URL powiązaną z nazwą akcji.
8.6.8.11. Strona startowa
![]() |

Strona startowa [boot.html] wyświetla powyższy widok. Jest to jedyna strona ładowana bezpośrednio przez przeglądarkę. Pozostałe są pobierane za pomocą wywołań Ajax. Jej kod wygląda następująco:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>RdvMedecins</title>
<!-- Rdzeń Bootstrap CSS -->
<link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
<link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
<link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
<!-- Niestandardowe style dla tego szablonu -->
<link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
<!-- Rdzeń Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap.js"></script>
<script type="text/javascript" src="vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="vendor/footable.js"></script>
<!-- skrypty użytkowników -->
<script type="text/javascript" src="js/json3.js"></script>
<script type="text/javascript" src="js/ui.js"></script>
<script type="text/javascript" src="js/evts.js"></script>
<script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
<script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
<!-- formularz logowania -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
</div>
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
</div>
<button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
<!-- języki -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:evts.setLang('fr')">Français</a></li>
<li><a href="javascript:evts.setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Jumbotron Bootstrap -->
<div id="jumbotron">
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>
Cabinet médical<br/>Les Médecins associés
</h1>
</div>
</div>
</div>
</div>
<!-- komunikaty o błędach -->
<div id="erreur"></div>
<div id="exception" class="alert alert-danger" style="display: none">
<h3 id="exception-title"></h3>
<span id="exception-text"></span>
</div>
<!-- treść -->
<div id="content">
<div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
</div>
</div>
<!-- strona początkowa -->
<script>
// inicjowanie strony
ui.langue = 'fr';
ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
ui.exceptionTitle['en'] = "The following server error was met:";
ui.initNavBarStart();
</script>
</body>
</html>
- ten typ strony omówiliśmy już w rozdziale poświęconym Bootstrapowi (punkt 8.6.4);
- wiersze 99–105: inicjalizacja niektórych elementów warstwy [présentation];
- w wierszu 27 wykorzystywany jest skrypt [getAccueilAvecAgenda-sequence.js]. Zmieniając skrypt w tym wierszu, uzyskujemy trzy różne sposoby wyświetlenia strony [accueil-avec-agenda]:
- [getAccueilAvecAgenda-one.js] generuje stronę za pomocą pojedynczego wywołania HTTP,
- [getAccueilAvecAgenda-parallel.js] pobiera stronę za pomocą czterech równoczesnych wywołań funkcji HTTP,
- [getAccueilAvecAgenda-sequence.js] pobiera stronę poprzez cztery kolejne wywołania HTTP;
8.6.8.12. Tests
Istnieją różne sposoby przeprowadzania testów. W tym przypadku wykorzystamy narzędzie [Webstorm]:
![]() |
- w [1] otwieramy projekt. Wystarczy wskazać folder [2] zawierający statyczną strukturę (HTML, CSS, JS) testowanej witryny;
![]() |
- do pliku [3] – statyczną witrynę;
- w [4-5] ładowana jest strona [boot.html];
![]() |
- w [5] widać, że serwer wbudowany w [Webstorm] dostarczył stronę [boot.html] z portu [63342]. Jest to ważna kwestia, którą należy zrozumieć, ponieważ oznacza to, że skrypty na stronie [boot.html] będą wykonywać wywołania międzydomenowe do serwera [Web1], który z kolei obsługuje stronę [localhost:8081]. Przeglądarka, która załadowała stronę [boot.html], wie, że załadowała ją ze strony [localhost:63342]. Nie zaakceptuje więc, aby ta strona nawiązywała połączenia z witryną [localhost:8081], ponieważ nie jest to ten sam port. W związku z tym przeglądarka zrealizuje wywołania międzydomenowe opisane w paragrafie 8.4.14. Z tego powodu aplikacja [Web1] musi być skonfigurowana tak, aby akceptowała te wywołania międzydomenowe. Decyzja ta jest podejmowana w pliku [AppConfig] na serwerze Spring / Thymeleaf:
![]() |
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// katalog główny serwisu internetowego / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// limit czasu w milisekundach
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
Pozostawiamy czytelnikowi przeprowadzenie testów klienta JS. Powinien on być w stanie odtworzyć funkcjonalności opisane w paragrafie 8.6.3.
Gdy klient JS zostanie uznany za poprawny, można go wdrożyć w folderze serwera [Web1], aby uniknąć konieczności zezwalania na żądania międzydomenowe:
![]() |
Powyżej skopiowaliśmy testowaną witrynę do folderu [src / main / resources / static]. Następnie można wywołać URL i [http://localhost:8081/boot.html]:

Teraz nie potrzebujemy już żądań międzydomenowych i możemy zapisać w pliku konfiguracyjnym [AppConfig] serwera [Web1]:
// CORS
private final boolean CORS_ALLOWED=false;
Powyższa aplikacja będzie nadal działać. Jeśli jednak wrócimy do aplikacji [Webstorm], okaże się, że już nie działa:


Po przejściu do konsoli programistycznej (Ctrl-Shift-I) widoczna jest przyczyna błędu:

Jest to błąd dotyczący nieautoryzowanego żądania międzydomenowego.
8.6.8.13. Conclusion
Stworzyliśmy następującą architekturę: JS:
![]() |
- warstwy są dość wyraźnie rozdzielone;
- mamy aplikację typu APU (aplikacja jednostronicowa). To właśnie ta cecha pozwoli nam teraz wygenerować aplikację natywną dla różnych urządzeń mobilnych (Android, IoS, Windows Phone);
- stworzyliśmy model zdolny do wykonywania akcji asynchronicznych równolegle, sekwencyjnie lub w połączeniu obu tych trybów;
8.6.9. Krok 6: generowanie aplikacji natywnej dla systemu Android
Narzędzie [Phonegap] [http://phonegap.com/] umożliwia wygenerowanie pliku wykonywalnego dla urządzeń mobilnych (Android, IoS, Windows 8, ...) na podstawie aplikacji HTML / JS / CSS. Istnieją różne sposoby osiągnięcia tego celu. Korzystamy z najprostszego: narzędzia dostępnego online na stronie Phonegap [http://build.phonegap.com/apps]. Narzędzie to „prześle” plik zip ze statyczną stroną, którą chcemy przekonwertować. Strona startowa musi nosić nazwę [index.html]. Zmieniamy więc nazwę strony [boot.html] na [index.html]:
![]() |
następnie kompresujemy folder, w tym przypadku [rdvmedecins-client-js-03]. Potem przechodzimy na stronę Phonegap [http://build.phonegap.com/apps]:
![]() |
- przed [1] może być konieczne utworzenie konta;
- w [1] rozpoczynamy;
- w [2] wybieramy bezpłatny plan, który pozwala na stworzenie tylko jednej aplikacji Phonegap;
![]() |
- w [3] pobieramy spakowaną aplikację [4];
![]() |
- w [5] nadajemy nazwę aplikacji;
- w [6] należy ją skompilować. Operacja ta może potrwać 1 minutę. Należy poczekać, aż ikony poszczególnych platform mobilnych wskażą, że kompilacja została zakończona;
![]() |
- wygenerowano jedynie pliki binarne dla systemu Android ([7]) i systemu Windows ([8]);
- należy kliknąć [7], aby pobrać plik binarny dla systemu Android;
![]() |
- w [9] pobrany plik binarny [apk];
Uruchom emulator [GenyMotion] dla tabletu z systemem Android (patrz punkt 9.9):
![]() |
Powyżej uruchamiamy emulator tabletu z systemem Android o numerze API 19. Po uruchomieniu emulatora
- odblokuj go, przesuwając suwak (jeśli jest) w bok, a następnie zwalniając go;
- za pomocą myszki przeciągnij pobrany plik [PGBuildApp-debug.apk] i upuść go na emulatorze. Zostanie on wówczas zainstalowany i uruchomiony;
![]() |
Należy zmienić nazwę pliku z URL na [1]. W tym celu w oknie poleceń wpisz polecenie [ipconfig] (wiersz 1 poniżej), które wyświetli różne adresy IP na Twoim komputerze:
C:\Users\Serge Tahé>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 15 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet Connexion au réseau local :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
...
Zapisz albo adres Wi-Fi IP (wiersze 6–9), albo adres w sieci lokalnej IP (wiersze 11–17). Następnie użyj tego adresu IP w polu URL serwera WWW:
![]() |
Po wykonaniu tej czynności należy połączyć się z serwisem internetowym:
![]() |
Przetestuj aplikację na emulatorze. Powinna działać. Po stronie serwera można zezwolić lub nie na nagłówki CORS w klasie [ApplicationModel]:
// CORS
private final boolean CORS_ALLOWED=false;
Nie ma to znaczenia dla aplikacji na Androida. Nie jest ona uruchamiana w przeglądarce. Wymóg dotyczący nagłówków CORS wynika jednak z przeglądarki, a nie z serwera.
8.6.10. Wnioski z analizy przypadku
Opracowaliśmy następującą architekturę:
![]() |
Jest to złożona architektura trójwarstwowa. Jej celem było ponowne wykorzystanie warstwy [Web2], która stanowiła warstwę serwerową aplikacji [AngularJS-Spring MVC] z dokumentu [Tutoriel AngularJS / Spring 4] doURL i [http://tahe.developpez.com/angularjs-spring4/]. Tylko z tego powodu mamy do czynienia z architekturą trójwarstwową. Natomiast w aplikacji [AngularJS-Spring MVC] klientem aplikacji [Web2] był klient [AngularJS], tutaj klientem aplikacji [Web2] jest architektura dwuwarstwowa [jQuery] / [Spring MVC / Thymeleaf]. Zwiększyliśmy liczbę warstw, więc stracimy na wydajności.
Analizowana tutaj aplikacja była z biegiem czasu rozwijana w trzech różnych dokumentach:
- [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile], URL oraz [http://tahe.developpez.com/java/primefaces/]. Studium przypadku zostało wówczas opracowane przy użyciu frameworków JSF2 / Primefaces. Primefaces to biblioteka komponentów obsługujących AJAX, która pozwala uniknąć pisania kodu JavaScript. Opracowana wówczas aplikacja była mniej złożona niż ta analizowana w niniejszym opracowaniu. Posiadała klasyczną wersję internetową przeznaczoną dla komputerów oraz wersję mobilną dla telefonów;
- [Tutoriel AngularJS / Spring 4] do URL [http://tahe.developpez.com/angularjs-spring4/]. Opracowana wówczas aplikacja miała te same cechy, co ta omawiana w niniejszym dokumencie. Aplikacja została również przeniesiona na system Android;
- niniejszy dokument;
Z tej pracy wynikają dla mnie następujące wnioski:
- aplikacja [Primefaces] była zdecydowanie najprostsza do napisania, a jej mobilna wersja internetowa okazała się wydajna. Nie wymaga ona znajomości języka JavaScript. Nie da się jej natywnie przenieść na platformy OS różnych urządzeń mobilnych, ale czy jest to konieczne? Wydaje się, że trudno jest zmienić styl aplikacji. Pracujemy bowiem z arkuszami stylów Primefaces. Może to stanowić wadę;
- aplikacja [AngularJS-Spring MVC] była skomplikowana w tworzeniu. Framework [AngularJS] wydał mi się dość trudny do opanowania, gdy chciałem go w pełni wykorzystać. Architektura [client Angular] / [service web / jSON implémenté par Spring MVC] jest wyjątkowo przejrzysta i wydajna. Architekturę tę można zastosować w każdej aplikacji internetowej. Wydaje mi się, że jest to najbardziej obiecująca architektura, ponieważ wymaga różnych umiejętności zarówno po stronie klienta, jak i po stronie serwera (JS+HTML+CSS po stronie klienta, Java lub inne technologie po stronie serwera), co pozwala na równoległe tworzenie klienta i serwera;
- w przypadku aplikacji opracowanej w niniejszym dokumencie z wykorzystaniem architektury trójwarstwowej [client jQuery] / [serveur Web1 / Spring MVC / Thymeleaf] / [serveur Web2 / Spring MVC], niektórzy mogą uznać technologię [jQuery+Spring MVC+Thymelaf] za łatwiejszą do opanowania niż technologię [AngularJS]. Warstwa [DAO] klienta JavaScript, którą stworzyliśmy, może być ponownie wykorzystana w innych aplikacjach;

























































































































































































































































































