1. Présentation et objectifs
Le but est de continuer le développement de notre architecture "à la microservice".
Nous allons aujourd’hui développer la WEB-UI de gestion des dresseurs de Pokemon.
Ce micro-service se connectera au micro service de pokemon management et trainer management !
On ressemble de plus en plus à une architecture micro-service, comme celle d’UBER ! |
Les pré-requis au développement de ce TP sont :
|
Nous allons développer :
-
deux repositories, qui accèdent aux données de trainer management et pokemon management
-
un service d’accès aux données
-
annoter ces composants avec les annotations de Spring et les tester
-
créer un controlleur spring pour gérer nos requêtes HTTP / REST
-
créer des templates pour afficher nos données
Nous repartons de zéro pour ce TP ! |
2. GitLab
Cliquez sur le lien suivant pour créer votre repository git: GitLab classroom
Clonez ensuite votre repository git sur votre poste !
N’oubliez pas ! Vous n’avez pas besoin de forker ce repostiory pour travailler, il vous appartient ! |
3. Architecture
Pour préparer les développements, on va également tout de suite créer quelques packages Java qui vont matérialiser notre architecture applicative.
Cette architecture est maintenant habituelle pour vous ! C’est l’architecture que l’on retrouve sur de nombreux projets |
Créer les packages suivants:
-
fr.univ_lille.alom.game_ui.views
: va contenir les controlleurs MVC de notre application -
fr.univ_lille.alom.game_ui.config
: va contenir la configuration de notre application -
fr.univ_lille.alom.game_ui.pokemonTypes
: va contenir les classes liées aux pokemons (bo et services) -
fr.univ_lille.alom.game_ui.trainers
: va contenir les classes liées aux dresseurs (bo et services)
Notre GUI va manipuler des concepts de plusieurs domaines métier (Trainer et Pokemon). Nous organisons notre application pour refléter ces domaines. |
Notre projet est prêt !
4. La première vue !
4.1. Le controlleur index
Nous allons développer un Controlleur
simple qui servira notre page d’index !
4.1.1. Le test unitaire
Implémentez le test unitaire suivant :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package fr.univ_lille.alom.game_ui.views;
import org.junit.jupiter.api.Test;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import static org.junit.jupiter.api.Assertions.*;
class IndexControllerTest {
@Test
void controllerShouldBeAnnotated(){
assertNotNull(IndexController.class.getAnnotation(Controller.class)); (1)
}
@Test
void index_shouldReturnTheNameOfTheIndexTemplate() {
var indexController = new IndexController();
var viewName = indexController.index();
assertEquals("index", viewName); (2)
}
@Test
void index_shouldBeAnnotated() throws NoSuchMethodException {
var indexMethod = IndexController.class.getMethod("index");
var getMapping = indexMethod.getAnnotation(GetMapping.class);
assertNotNull(getMapping);
assertArrayEquals(new String[]{"/"}, getMapping.value()); (3)
}
}
1 | notre controller doit être annoté @Controller (à ne pas confondre avec @RestController ) |
2 | si le retour de la méthode du controlleur est une chaîne de caractères, cette chaîne sera utilisée pour trouver la vue à afficher |
3 | on écoute les requêtes arrivant à / |
4.1.2. L’implémentation
Implémentez la classe IndexController !
1
2
3
4
5
6
7
8
9
// TODO
public class IndexController {
// TODO
public String index(){
return ""; // TODO
}
}
4.2. Ajout du moteur de template
Nous allons utiliser le moteur de template Mustache
.
Pour ce faire, ajoutez la dépendance suivante dans votre pom.xml
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
Par défaut, les templates Mustache
:
-
sont positionnés dans un répertoire du classpath
/templates
(donc danssrc/main/resources/templates
, puisque Maven ajoutesrc/main/resources
au classpath). -
sont des fichiers nommés
.mustache
Les propriétés disponibles sont détaillées dans la documentation Spring
Nous allons modifier le suffixe des fichiers de template, pour être .html
.
Créez le fichier src/main/resources/application.properties
et ajoutez-y les propriétés suivantes.
(1)
spring.mustache.prefix=classpath:/templates/
(2)
spring.mustache.suffix=.html
(3)
server.port=9000
1 | On garde ici la valeur par défaut. |
2 | On modifie la propriété pour prendre en compte les fichiers .html au lieu de .mustache |
3 | On en profite pour demander à Spring d’écouter sur le port 9000 ! |
4.3. Ajout du template !
Nous pouvons enfin ajouter notre template de page d’accueil !
La page d’accueil va contenir 2 liens, un lien pour se créer un compte, et un lien pour s’authentifier.
Créer le fichier src/main/resources/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!doctype html> (1)
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Pokemon Manager</title>
<!-- Bootstrap CSS --> (2)
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1 class="pt-md-5 pb-md-5">Pokemon Manager</h1> (3)
<div>
<a href="/login" class="btn btn-primary" role="button">
Login
</a>
<a href="/register" class="btn btn-secondary" role="button">
Register
</a>
</div>
</div>
</body>
</html>
1 | On crée une page HTML |
2 | En important les CSS de Bootstrap par exemple |
3 | On affiche un titre ! |
4.4. L’application
Démarrez votre application et allez consulter le résultat sur http://localhost:9000 !
5. Création de compte
Nous allons maintenant créer un formulaire de saisie permettant à un utilisateur de se créer un compte.
5.1. Servir des ressources statiques
Par défaut, Spring est capable de servir des ressources statiques.
Pour ce faire, il suffit de les placer au bon endroit !
Télécharger l’image chen.png et placez-la dans le répertoire src/main/resources/static/images
ou src/main/resources/public/images
Une image pokemon-logo.png est aussi disponible pour votre page d’accueil.
Le positionnement des ressources statiques est paramétrable à l’aide de l’application.properties :
# Path pattern used for static resources. (1)
spring.mvc.static-path-pattern=/**
# Locations of static resources. (2)
spring.web.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
1 | Ce paramétrage indique que l’ensemble des requêtes entrantes peut être une ressource statique ! |
2 | Et on indique à spring dans quels répertoires il doit chercher les ressources ! |
5.2. Ajouter un formulaire
Créez une page register.html
, contenant un formulaire :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="row">
<img src="/images/chen.png" class="col-md-2"/> (1)
<div class="row col">
<div style="white-space: pre-line"> (2)
Hello there!
Welcome to the world of Pokémon!
My name is Oak! People call me the Pokémon Prof!
This world is inhabited by creatures called Pokémon!
For some people, Pokémon are pets. Other use them for fights.
Myself… I study Pokémon as a profession. First, what is your name?
</div>
<form action="/register" method="post"> (3)
<div class="form-group">
<label for="trainerName">Trainer name</label>
<input type="text" class="form-control" id="trainerName" name="trainerName" aria-describedby="trainerHelp" placeholder="Enter your name">
<small id="trainerHelp" class="form-text text-muted">This will be your name in the game !</small>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
1 | Nous ajoutons notre ressource statique. |
2 | Le discours d’introduction original du Professeur Chen dans Pokémon Bleu et Rouge ! |
3 | Un formulaire de création de dresseur ! |
Notez comme la ressource statique est référencée par |
5.3. Les impacts sur la couche controlleur
Créez un nouveau controlleur pour servir notre page register
, et recevoir la requête POST /register
de création.
5.3.1. Les tests unitaires
Ajouter les tests unitaires suivants :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
void registerNewTrainer_shouldReturnAModelAndView(){
var registerController = new RegisterController();
var modelAndView = registerController.registerNewTrainer("Blue");
assertNotNull(modelAndView);
assertEquals("registered", modelAndView.getViewName());
assertEquals("Blue", modelAndView.getModel().get("name"));
}
@Test
void registerNewTrainer_shouldBeAnnotated() throws NoSuchMethodException {
var registerMethod = RegisterController.class.getDeclaredMethod("registerNewTrainer", String.class);
var getMapping = registerMethod.getAnnotation(PostMapping.class);
assertNotNull(getMapping);
assertArrayEquals(new String[]{"/register"}, getMapping.value());
}
5.3.2. L’implémentation
Implémenter le RegisterController.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class RegisterController {
@GetMapping("/register")
String register(){
return "register";
}
// TODO
ModelAndView registerNewTrainer(String trainerName){
// TODO
}
}
5.3.3. Le nouveau template
Nous allons devoir également créer un nouveau template pour afficher le résultat.
Créez le template registered.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!doctype html> (1)
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Pokemon Manager</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1 class="pt-md-5 pb-md-5">Pokemon Manager - Welcome {{name}}</h1> (1)
<div class="row">
<img src="/images/chen.png" class="col-md-2"/>
<div class="row col-md-10">
<p style="white-space: pre-line">
Right! So your name is {{name}}! (1)
{{name}}! (1)
Your very own Pokémon legend is about to unfold!
A world of dreams and adventures with Pokémon awaits!
Let's go!
</p>
<p>
<a href="/profile" class="btn btn-primary" role="button">View you profile !</a>
</p>
</div>
</div>
</div>
</body>
</html>
1 | On utilise le champ name du model pour alimenter notre titre et notre texte ! |
6. Ajouter du layouting
6.1. Le header
Nous allons utiliser l’inclusion de templates pour éviter de copier/coller notre header de page sur l’ensemble de notre application !
Créez un répertoire layout
dans src/main/resources/templates
. Ce répertoire va nous permettre de gérer les templates
liés à la mise en page de notre application.
Dans le répertoire layout
, créez un fichier que l’on appellera header.html
:
1
2
3
4
5
6
7
8
9
10
11
<!doctype html> (1)
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Pokemon Manager</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
6.2. L’inclusion
L’utilisation de notre header dans un template se fait alors avec une inclusion Mustache
.
Modifiez vos templates pour utiliser l’inclusion :
1
2
3
4
5
{{> /layout/header}}
<body>
[...]
</body>
7. Se connecter aux micro-services
Nous allons maintenant appeler le micro-service pokemon-type-api, que nous avons écrit lors du TP 3 !.
7.1. Le business object
La classe du business object va être la copie de la classe du micro-service que l’on va consommer.
Nous avons donc besoin ici de deux record (que vous pouvez copier/coller depuis votre TP 3 !) :
-
PokemonType: représentation d’un type de Pokemon
-
Sprites: représentation des images du Pokemon (avant et arrière)
1
public record PokemonType {}
1
public record Sprites {}
7.2. Le service
7.2.1. L’interface PokemonService
Écrire l’interface de service suivante :
1
2
3
4
5
public interface PokemonTypeService {
List<PokemonType> listPokemonsTypes();
}
7.2.2. La configuration du RestTemplate
Par défaut, Spring n’instancie pas de RestTemplate.
Il nous faut donc en instancier un, et l’ajouter à l' application context
afin de le rendre disponible en injection de dépendances.
Pour ce faire, nous allons développer une simple classe de configuration :
1
2
3
4
5
6
7
8
9
@Configuration (1)
public class RestConfiguration {
@Bean (2)
RestTemplate restTemplate(){
return new RestTemplate(); (3)
}
}
1 | L’annotation @Configuration enregistre notre classe RestConfiguration dans l’application context (comme @Component , ou @Service ) |
2 | L’annotation @Bean permet d’annoter une méthode, dont le résultat sera enregistré comme un bean dans l' application context de spring. |
7.2.3. Le test d’intégration
Implémentez le test d’intégration suivant :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package fr.univ_lille.alom.game_ui.pokemonTypes;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@RestClientTest(PokemonTypeServiceImpl.class)
@AutoConfigureWebClient(registerRestTemplate = true)
@TestPropertySource(properties = "pokemonType.service.url=http://localhost:8080")
class PokemonTypeServiceIntegrationTest {
@Autowired
PokemonTypeService pokemonTypeService;
@Autowired
MockRestServiceServer server;
@Autowired
PokemonTypeService service;
@Autowired
RestTemplate restTemplate;
@Test
void serviceAndTemplateShouldNotBeNull(){
assertNotNull(service);
assertNotNull(restTemplate);
}
@Test
void listPokemonsTypes_shouldCallTheRemoteService() {
// given
var response = """
[
{
"id": 151,
"name": "mew",
"types": ["psychic"]
}
]
""";
server.expect(requestTo("http://localhost:8080/pokemon-types/"))
.andRespond(withSuccess(response, MediaType.APPLICATION_JSON));
var pokemons = pokemonTypeService.listPokemonsTypes();
assertThat(pokemons).hasSize(1);
}
@Test
void pokemonServiceImpl_shouldBeAnnotatedWithService(){
assertNotNull(PokemonTypeServiceImpl.class.getAnnotation(Service.class));
}
@Test
void setRestTemplate_shouldBeAnnotatedWithAutowired() throws NoSuchMethodException {
var setRestTemplateMethod = PokemonTypeServiceImpl.class.getDeclaredMethod("setRestTemplate", RestTemplate.class);
assertNotNull(setRestTemplateMethod.getAnnotation(Autowired.class));
}
}
7.2.4. L’implémentation
Pour exécuter les appels au micro-service de gestion des pokemons, nous allons utiliser le |
Implémentez la classe suivante :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TODO
public class PokemonTypeServiceImpl implements PokemonTypeService {
public List<PokemonType> listPokemonsTypes() {
// TODO
}
void setRestTemplate(RestTemplate restTemplate) {
// TODO
}
void setPokemonTypeServiceUrl(String pokemonServiceUrl) {
// TODO
}
}
7.2.5. L’injection des properties
Nous allons également utiliser l’injection de dépendance pour l’URL d’accès au service !
Les paramètres de configuration d’une application sont souvent injectés selon la méthode que nous allons voir ! |
Modifiez le fichier application.properties
pour y ajouter une nouvelle propriété :
pokemonType.service.url=http://localhost:8080 (1)
1 | Nous utilisons un paramètre indiquant à quelle URL sera disponible notre micro-service de pokemons! Utilisez l’url à laquelle votre service est déployé, ou une URL en localhost. |
Ajoutez le test unitaire suivant au PokemonServiceIntegrationTest
1
2
3
4
5
6
7
@Test
void setPokemonServiceUrl_shouldBeAnnotatedWithValue() throws NoSuchMethodException {
var setter = PokemonTypeServiceImpl.class.getDeclaredMethod("setPokemonTypeServiceUrl", String.class);
var valueAnnotation = setter.getAnnotation(Value.class); (1)
assertNotNull(valueAnnotation);
assertEquals("${pokemonType.service.url}", valueAnnotation.value()); (2)
}
1 | On utilise une annotation @Value pour faire l’injection de dépendances de properties |
2 | Une expression ${} (spring-expression-language) est utilisée pour calculer la valeur à injecter |
Un guide intéressant sur l’injection de valeurs avec l’annotation @Value ici |
7.3. Le controlleur
Nous allons maintenant écrire le controlleur PokemonTypeController !
7.3.1. Le test unitaire
Implémentez le test unitaire suivant :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package fr.univ_lille.alom.game_ui.views;
import fr.univ_lille.alom.game_ui.pokemonTypes.PokemonType;
import fr.univ_lille.alom.game_ui.pokemonTypes.PokemonTypeService;
import org.junit.jupiter.api.Test;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class PokemonTypeControllerTest {
@Test
void controllerShouldBeAnnotated(){
assertNotNull(PokemonTypeController.class.getAnnotation(Controller.class));
}
@Test
void pokemons_shouldReturnAModelAndView() {
var pokemonTypeService = mock(PokemonTypeService.class);
var pikachu = new PokemonType("pikachu", 25);
when(pokemonTypeService.listPokemonsTypes()).thenReturn(List.of(pikachu));
var pokemonTypeController = new PokemonTypeController();
pokemonTypeController.setPokemonTypeService(pokemonTypeService);
var modelAndView = pokemonTypeController.pokedex();
assertEquals("pokedex", modelAndView.getViewName());
var pokemons = (List<PokemonType>)modelAndView.getModel().get("pokemonTypes");
assertEquals(1, pokemons.size());
verify(pokemonTypeService).listPokemonsTypes();
}
@Test
void pokemons_shouldBeAnnotated() throws NoSuchMethodException {
var pokemonsMethod = PokemonTypeController.class.getDeclaredMethod("pokedex");
var getMapping = pokemonsMethod.getAnnotation(GetMapping.class);
assertNotNull(getMapping);
assertArrayEquals(new String[]{"/pokedex"}, getMapping.value());
}
}
7.3.2. L’implémentation
Implémentez le controlleur :
1
2
3
4
5
6
7
8
9
// TODO
public class PokemonTypeController {
// TODO
public ModelAndView pokedex(){
// TODO
}
}
7.3.3. Le template
Nous allons créer une petite page qui va afficher pour chaque type de pokémon son nom, son image, ainsi que ses statistiques
Créer le template suivant :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{{> layout/header}}
<body>
<div class="container">
<h1 class="pt-md-5 pb-md-5">Pokedex</h1>
<div class="card-deck">
{{#pokemonTypes}} (1)
<div class="col-md-3">
<div class="card shadow-sm mb-3">
<div class="card-header">
(2)
<h4 class="my-0 font-weight-normal">{{name}} <span class="badge text-bg-secondary">Id {{}} </span></h4>(3)
</div>
<img class="card-img-top" src="{{}}" alt="Pokemon"/> (3)
<div class="card-body">
<span class="badge text-bg-primary">Type : {{}}</span> (3)
</div>
</div>
</div>
{{/pokemonTypes}}
</div>
</div>
</body>
</html>
1 | Voici comment on itère sur une liste ! |
2 | On affiche quelques valeurs |
3 | à compléter |
Attention, si le template n’est pas correct, la vue ne s’affichera pas quand elle est requêtée, et des exceptions peuvent apparaître dans la console, en particulier des NullPointerException ou StringIndexOutOfBoundsException .
|
8. Pour aller plus loin
-
Affichez sur le Pokedex les types de chaque Pokemon (plante, électrique…)
-
Affichez sur le Pokedex les images "vues de derrière"
-
Développez une page web qui affiche la liste des dresseurs de Pokemons (accessible à
/trainers
) -
Développez une page qui affiche le détail d’un dresseur de Pokemon (accessible à
/trainers/{name}
) :-
son nom
-
son équipe
-