1. Présentation et objectifs
Le but est de reprendre à zéro le développement de notre architecture "à la microservice", en utilisant spring-boot
Pour rappel, dans cette architecture, chaque composant a son rôle précis :
-
la servlet reçoit les requêtes HTTP, et les envoie au bon controller (rôle de point d’entrée de l’application)
-
le controlleur implémente une méthode Java par route HTTP, récupère les paramètres, et appelle le service (rôle de routage)
-
le service implémente le métier de notre micro-service
-
le repository représente les accès aux données (avec potentiellement une base de données)
Et pour s’amuser un peu, nous allons réaliser un micro-service qui nous renvoie des données sur les Pokemons !
Nous allons développer :
-
un repository d’accès aux données de Pokemons (à partir d’un fichier JSON)
-
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
-
utiliser spring-boot pour instancier notre application !
Nous repartons de zéro pour ce TP ! |
2. Github
Identifiez-vous sur GitLab, et cliquez sur le lien suivant pour créer votre repository git: GitLab classroom
Clonez ensuite votre repository git sur votre poste !
3. La classe PokemonType
Pour commencer, nous allons créer notre objet métier.
3.1. La structure JSON
Pour implémenter notre objet, nous devons nous inspirer des champs que propose l’API https://pokeapi.co.
Par exemple, voici ce qu’on obtient en appelant l’API (un peu simplifiée) :
{
"base_experience": 261,
"height": 16,
"id": 145,
"moves": [],
"name": "zapdos",
"sprites": {
"back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/145.png",
"back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/145.png",
"front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/145.png",
"front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/145.png"
},
"stats": [
{
"base_stat": 100,
"effort": 0,
"stat": {
"name": "speed",
"url": "https://pokeapi.co/api/v2/stat/6/"
}
},
{
"base_stat": 90,
"effort": 0,
"stat": {
"name": "special-defense",
"url": "https://pokeapi.co/api/v2/stat/5/"
}
},
{
"base_stat": 125,
"effort": 3,
"stat": {
"name": "special-attack",
"url": "https://pokeapi.co/api/v2/stat/4/"
}
},
{
"base_stat": 85,
"effort": 0,
"stat": {
"name": "defense",
"url": "https://pokeapi.co/api/v2/stat/3/"
}
},
{
"base_stat": 90,
"effort": 0,
"stat": {
"name": "attack",
"url": "https://pokeapi.co/api/v2/stat/2/"
}
},
{
"base_stat": 90,
"effort": 0,
"stat": {
"name": "hp",
"url": "https://pokeapi.co/api/v2/stat/1/"
}
}
],
"types": [
{
"slot": 2,
"type": {
"name": "flying",
"url": "https://pokeapi.co/api/v2/type/3/"
}
},
{
"slot": 1,
"type": {
"name": "electric",
"url": "https://pokeapi.co/api/v2/type/13/"
}
}
],
"weight": 526
}
3.2. Les classes Java
Nous allons donc créer un record
Java qui reprend cette structure, mais en ne conservant que les champs
qui nous intéressent.
1
2
3
4
5
6
7
8
package fr.univ_lille.alom.pokemon_type_api;
record PokemonType(
int id,
String name,
Sprites sprites,
List<String> types
) {}
1
2
3
package fr.univ_lille.alom.pokemon_type_api;
record Sprites(String back_default, String front_default) {}
4. Le repository "stub"
4.1. Le fichier de données pokemon.
Récupérez le fichier pokemons.json et enregistrez le dans le répertoire src/main/resources
de votre projet.
4.2. 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
49
50
package fr.univ_lille.alom.pokemon_type_api;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PokemonTypeRepositoryImplTest {
private PokemonTypeRepositoryImpl repository = new PokemonTypeRepositoryImpl();
@Test
void findPokemonTypeById_with25_shouldReturnPikachu(){
var pikachu = repository.findPokemonTypeById(25);
assertNotNull(pikachu);
assertEquals("pikachu", pikachu.name());
assertEquals(25, pikachu.id());
}
@Test
void findPokemonTypeById_with145_shouldReturnZapdos(){
var zapdos = repository.findPokemonTypeById(145);
assertNotNull(zapdos);
assertEquals("zapdos", zapdos.name());
assertEquals(145, zapdos.id());
}
@Test
void findPokemonTypeByName_withEevee_shouldReturnEevee(){
var eevee = repository.findPokemonTypeByName("eevee");
assertNotNull(eevee);
assertEquals("eevee", eevee.name());
assertEquals(133, eevee.id());
}
@Test
void findPokemonTypeByName_withMewTwo_shouldReturnMewTwo(){
var mewtwo = repository.findPokemonTypeByName("mewtwo");
assertNotNull(mewtwo);
assertEquals("mewtwo", mewtwo.name());
assertEquals(150, mewtwo.id());
}
@Test
void findAllPokemonTypes_shouldReturn151Pokemons(){
var pokemons = repository.findAllPokemonTypes();
assertNotNull(pokemons);
assertEquals(151, pokemons.size());
}
}
4.3. L’implémentation
Ajouter l’interface du PokemonTypeRepository et son implémentation
1
2
3
4
5
interface PokemonTypeRepository {
PokemonType findPokemonTypeById(int id);
PokemonType findPokemonTypeByName(String name);
List<PokemonType> findAllPokemonTypes();
}
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
class PokemonTypeRepositoryImpl implements PokemonTypeRepository {
private List<PokemonType> pokemons;
PokemonTypeRepositoryImpl() {
try {
var pokemonsStream = this.getClass().getResourceAsStream("/pokemons.json"); (1)
var objectMapper = new ObjectMapper(); (2)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
var pokemonsArray = objectMapper.readValue(pokemonsStream, PokemonType[].class);
this.pokemons = Arrays.asList(pokemonsArray);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public PokemonType findPokemonTypeById(int id) {
System.out.println("Loading Pokemon information for Pokemon id " + id);
// TODO (3)
}
@Override
public PokemonType findPokemonTypeByName(String name) {
System.out.println("Loading Pokemon information for Pokemon name " + name);
// TODO (3)
}
@Override
public List<PokemonType> findAllPokemonTypes() {
// TODO (3)
}
}
1 | On charge le fichier json depuis le classpath (maven ajoute le répertoire src/main/resources au classpath java !) |
2 | On utilise l’ObjectMapper de jackson-databind pour transformer les objets JSON en objets JAVA |
3 | On a un peu de code à compléter ! |
4.4. Ajout de tests unitaires avec Spring
Modifiez le test unitaire de votre repository pour ajouter des éléments liés à Spring
1
2
3
4
5
6
7
8
9
10
11
@Test
void applicationContext_shouldLoadPokemonRepository(){
(1)
var context = new AnnotationConfigApplicationContext("fr.univ_lille.alom.pokemon_type_api");
var repoByName = context.getBean("pokemonTypeRepositoryImpl"); (2)
var repoByClass = context.getBean(PokemonTypeRepository.class); (3)
assertEquals(repoByName, repoByClass);
assertNotNull(repoByName);
assertNotNull(repoByClass);
}
1 | Ici, on instancie un ApplicationContext Spring, qui est capable d’analyser les annotations Java
on lui donne le nom du package Java que l’on souhaite analyser ! |
2 | Une fois le context instancié, on lui demande de récupérer le repository en utilisant le nom du bean (par défaut le nom de la classe en CamelCase) |
3 | ou en utilisant directement une classe assignable pour notre objet (ici l’interface !) |
Pour que Spring arrive à trouver notre classe de repository, il faut poser une annotation dessus !
1
2
3
4
@Repository
class PokemonTypeRepositoryImpl implements PokemonTypeRepository {
[...]
}
Cette phase doit bien être terminée avant de passer à la suite ! |
5. Le service
Maintenant que nous avons un repository fonctionnel, il est temps de développer un service qui consomme notre repository !
5.1. Le test unitaire
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
package fr.univ_lille.alom.pokemon_type_api;
import fr.univ_lille.alom.pokemon_type_api.PokemonTypeRepository;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class PokemonTypeServiceImplTest {
@Test
void pokemonTypeRepository_shouldBeCalled_whenFindById(){
var pokemonTypeRepository = mock(PokemonTypeRepository.class); (1)
var pokemonTypeService = new PokemonTypeServiceImpl(pokemonTypeRepository); (2)
pokemonTypeService.getPokemonType(25);
verify(pokemonTypeRepository).findPokemonTypeById(25);
}
@Test
void pokemonTypeRepository_shouldBeCalled_whenFindAll(){
var pokemonTypeRepository = mock(PokemonTypeRepository.class); (1)
var pokemonTypeService = new PokemonTypeServiceImpl(pokemonTypeRepository); (2)
pokemonTypeService.getAllPokemonTypes();
verify(pokemonTypeRepository).findAllPokemonTypes();
}
}
1 | On crée un mock du PokemonTypeRepository |
2 | et on l'injecte via le constructeur ! |
5.2. L’implémentation
L’interface Java
1
2
3
4
public interface PokemonTypeService {
PokemonType getPokemonType(int id);
List<PokemonType> getAllPokemonTypes();
}
et son implémentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package fr.univ_lille.alom.pokemon_type_api.service;
import fr.univ_lille.alom.pokemon_type_api.bo.PokemonType;
import java.util.List;
class PokemonTypeServiceImpl implements PokemonTypeService{
PokemonTypeServiceImpl(){ // TODO (1)
}
@Override
PokemonType getPokemonType(int id) {
// TODO (1)
}
@Override
List<PokemonType> getAllPokemonTypes(){
// TODO (1)
}
}
1 | à implémenter ! |
5.3. Implémentation avec Spring
Ajouter les tests suivants au PokemonTypeServiceImplTest
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
void applicationContext_shouldLoadPokemonTypeService(){
var context = new AnnotationConfigApplicationContext("fr.univ_lille.alom.pokemon_type_api");
var serviceByName = context.getBean("pokemonTypeServiceImpl");
var serviceByClass = context.getBean(PokemonTypeService.class);
assertEquals(serviceByName, serviceByClass);
assertNotNull(serviceByName);
assertNotNull(serviceByClass);
}
@Test
void pokemonTypeRepository_shouldBeAutowired_withSpring(){
var context = new AnnotationConfigApplicationContext("fr.univ_lille.alom.pokemon_type_api");
var service = context.getBean(PokemonTypeServiceImpl.class);
assertNotNull(service.pokemonTypeRepository);
}
Vous aurez également besoin d’importer les assertions de Junit en utilisant import static org.junit.jupiter.api.Assertions.*
|
N’oubliez pas que Spring utilise beaucoup les annotations Java, en voici quelques-unes :
N’oubliez pas que certaines de ces annotations peuvent être posées sur des classes, sur des méthodes, ou sur des constructeurs ! |
Imaginez un peu comment on aurait pu utiliser cette mécanique au sein de la DispatcherServlet que nous avons écrit la semaine dernière… |
6. Le Controlleur
Implémentons un Controlleur afin d’exposer nos Pokemons en HTTP/REST/JSON.
6.1. Le test unitaire
Le controlleur est simple et s’inspire de ce que nous avons fait au TP précédent.
Cependant, nous n’aurons plus à gérer les paramètres manuellement via une Map<String,String>
,
mais nous allons utiliser toute la puissance de Spring.
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
package fr.univ_lille.alom.pokemon_type_api;
import fr.univ_lille.alom.pokemon_type_api.PokemonType;
import fr.univ_lille.alom.pokemon_type_api.PokemonTypeService;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class PokemonTypeControllerTest {
@Test
void getPokemonType_shouldCallTheService(){
var service = mock(PokemonTypeService.class);
var controller = new PokemonTypeController(service);
var pikachu = new PokemonType(25, "pikachu", null, List.of());
when(service.getPokemonType(25)).thenReturn(pikachu);
var pokemon = controller.getPokemonTypeFromId(25);
assertEquals("pikachu", pokemon.name());
verify(service).getPokemonType(25);
}
@Test
void getAllPokemonTypes_shouldCallTheService(){
var service = mock(PokemonTypeService.class);
var controller = new PokemonTypeController(service);
controller.getAllPokemonTypes();
verify(service).getAllPokemonTypes();
}
}
6.2. L’implémentation
Compléter l’implémentation du controller :
1
2
3
4
5
6
7
8
9
10
11
12
13
class PokemonTypeController {
PokemonTypeController() { (1)
}
PokemonType getPokemonTypeFromId(int id){
// TODO (1)
}
List<PokemonType> getAllPokemonTypes() {
// TODO (1)
}
}
1 | Implémentez ! |
6.3. L’instrumentation pour spring-web !
Une fois les tests passés, nous pouvons implementer notre controlleur pour Spring web !
6.3.1. Les tests unitaires
Ajoutez les tests unitaires suivants à la classe PokemonTypeControllerTest
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
@Test
void pokemonTypeController_shouldBeAnnotated(){
var controllerAnnotation =
PokemonTypeController.class.getAnnotation(RestController.class);
assertNotNull(controllerAnnotation);
var requestMappingAnnotation =
PokemonTypeController.class.getAnnotation(RequestMapping.class);
assertArrayEquals(new String[]{"/pokemon-types"}, requestMappingAnnotation.value());
}
@Test
void getPokemonTypeFromId_shouldBeAnnotated() throws NoSuchMethodException {
var getPokemonTypeFromId =
PokemonTypeController.class.getDeclaredMethod("getPokemonTypeFromId", int.class);
var getMapping = getPokemonTypeFromId.getAnnotation(GetMapping.class);
assertNotNull(getMapping);
assertArrayEquals(new String[]{"/{id}"}, getMapping.value());
}
@Test
void getAllPokemonTypes_shouldBeAnnotated() throws NoSuchMethodException {
var getAllPokemonTypes =
PokemonTypeController.class.getDeclaredMethod("getAllPokemonTypes");
var getMapping = getAllPokemonTypes.getAnnotation(GetMapping.class);
assertNotNull(getMapping);
assertArrayEquals(new String[]{"/"}, getMapping.value());
}
6.3.2. L’implémentation
Posez les bonnes annotations spring pour instrumenter votre Controller et faire passer les tests unitaires.
Pour vous aider, voici des liens vers la documentation de spring-web : |
6.4. L’exécution de notre projet !
Pour exécuter notre projet, nous devons écrire un main java ! Implémentez la classe suivante :
1
2
3
4
5
6
7
@SpringBootApplication (1)
public class Application {
public static void main(String... args){
SpringApplication.run(Application.class, args); (2)
}
}
1 | On annote la classe comme étant le point d’entrée de notre application |
2 | On implémente un main pour démarrer notre application ! |
Démarrez le main, et observez les logs :
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) ) (1)
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.2.RELEASE)
[..] [main] c.m.a.pokemon_type_api.Application : Starting Application on jwittouck-N14xWU with PID 12414 (/home/jwittouck/workspaces/alom/alom-2020-2021/tp/pokemon-type-api/target/classes started by jwittouck in /home/jwittouck/workspaces/alom/alom-2021-2022)
[..] [main] c.m.a.pokemon_type_api.Application : No active profile set, falling back to default profiles: default
[..] INFO 12414 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
[..] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] (2)
[..] [main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.14]
[..] [main] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib]
[..] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
[..] [main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1617 ms
[..] [main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
[..] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
[..] [main] c.m.a.pokemon_type_api.Application : Started Application in 2.72 seconds (JVM running for 3.191)
1 | Wao! |
2 | On voit que un Tomcat est démarré sur le port 8080 |
On peut maintenant tester manuellement les URLs suivantes:
6.4.1. Plus de logs !
Nous voulons un peu plus de logs pour bien comprendre ce que fait spring-boot.
Pour ce faire, nous allons monter le niveau de logs au niveau TRACE
.
Créer un fichier application.properties
dans le répertoire src/main/resources
.
1
2
# on demande un niveau de logs TRACE a spring-web
logging.level.web=TRACE
Relancez l’application, vous devriez voir spring logguer ceci :
[main] s.w.s.m.m.a.RequestMappingHandlerMapping :
c.m.a.t.p.c.PokemonTypeController: (1)
{GET /pokemon-types/{id}}: getPokemonTypeFromId(int)
{GET /pokemon-types/}: getAllPokemonTypes()
[main] s.w.s.m.m.a.RequestMappingHandlerMapping :
o.s.b.a.w.s.e.BasicErrorController: (2)
{ /error, produces [text/html]}: errorHtml(HttpServletRequest,HttpServletResponse)
{ /error}: error(HttpServletRequest)
1 | On voit que spring a bien pris en compte notre controlleur |
2 | On voit également que spring a instancié un controlleur pour afficher des erreurs sous forme de page HTML |
6.4.2. Plus de test
Nous allons également rajouter un dernier test, qui a pour but de :
-
démarrer l’application spring en utilisant un port aléatoire
-
invoquer dynamiquement notre URL
Ajoutez la dépendance suivante à votre pom.xml
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
L’ajout de spring-boot-starter-test , depuis la version 2.2.0, ajoute également junit-jupiter et mockito . Vous pouvez donc supprimer ces dépendances de votre pom.
|
Ce genre de test, qui démarre une base de données ou un serveur par exemple, est appelé test d’intégration |
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
package fr.univ_lille.alom.pokemon_type_api;
import fr.univ_lille.alom.pokemon_type_api.PokemonType;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) (1)
class PokemonTypeControllerIntegrationTest {
@LocalServerPort (2)
private int port;
@Autowired
private TestRestTemplate restTemplate; (3)
@Autowired
private PokemonTypeController controller; (4)
@Test
void pokemonTypeController_shouldBeInstanciated(){ (4)
assertNotNull(controller);
}
@Test
void getPokemon_withId25_ShouldReturnPikachu() throws Exception {
var url = "http://localhost:" + port + "/pokemon-types/25"; (5)
var pikachu = this.restTemplate.getForObject(url, PokemonType.class); (6)
assertNotNull(pikachu); (7)
assertEquals(25, pikachu.id());
assertEquals("pikachu", pikachu.name());
}
}
1 | On utilise un SpringBootTest pour exécuter ce test. Ce test va donc instancier Spring. On précise également que l’environnement Spring doit utiliser un port aléatoire. |
2 | On demande à Spring de nous donner le port sur lequel le serveur aura été démarré |
3 | On demande à Spring de nous donner un TestRestTemplate , qui nous permettra de jouer une requête HTTP |
4 | On peut faire directement de l’injection de dépendance dans notre test, nous en profitons pour valider que notre controller est bien chargé. |
5 | On construit dynamiquement l’url à invoquer |
6 | On utilise le TestRestTemplate pour appeler notre API ! Le TestRestemplate va également se charger de convertir le JSON reçu, en objet Java en utilisant jackson-databind . |
7 | Enfin, on valide que Pikachu est arrivé en bon état ! |
7. Autres routes
Implémentez la route qui permet de récupérer un pokemon par son nom.
Elle doit être disponible via ces url de test :
8. Packager notre micro-service
Une fois notre service fonctionnel, nous pouvons le packager. Notre micro-service sera packagé dans un jar exécutable !
8.1. Le plugin spring-boot maven
Notre pom.xml
contient le bloc suivant :
1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Ce plugin nous met à disposition de nouvelles tâches maven !
Nous pouvons lancer notre application en exécutant la commande suivante :
mvn spring-boot:run
8.2. Packager notre micro-service
Avant de packager notre micro-service, nous devons modifier le chargement des ressources dans PokemonTypeRepositoryImpl
.
La mécanique d’exécution de spring-boot utilise 2 classpath Java, ce qui impose que les fichiers
de ressources (en particulier notre fichier JSON), doivent être chargés différemment.
Modifiez le constructeur du repository pour être le suivant :
1
2
3
4
5
6
7
8
9
10
11
12
PokemonTypeRepositoryImpl() {
try {
var pokemonsStream = new ClassPathResource("pokemons.json").getInputStream();
var objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
var pokemonsArray = objectMapper.readValue(pokemonsStream, PokemonType[].class);
this.pokemons = Arrays.asList(pokemonsArray);
} catch (IOException e) {
e.printStackTrace();
}
}
Pour créer un jar de notre service, il faut maintenant lancer la commande :
mvn package
Et pour l’exécuter, il suffit alors de lancer :
java -jar target/pokemon-type-api-0.1.0.jar
La construction de jar "autoporté" spring-boot, est aujourd’hui l’état de l’art des approches micro-service ! |
9. Continuez à développer
Les types de Pokemons sont des données "référentielles". Cela signifie qu’elles seront le plus souvent accédées en lecture seule. Cependant, nous pouvons développer des routes supportant des paramètres supplémentaires pour être capable de recherche plus finement un pokémon !
Par défaut, la liste des Pokemons doit également être triée par id
.
Développez les routes suivantes pour notre jeu :
-
http://localhost:8080/pokemon-types?types=electric (9 Pokemons ont le type électrique)
-
http://localhost:8080/pokemon-types?types=bug,poison (5 Pokemons ont les types insecte et poison)
Ajoutez les stats
, baseExperience
, weight
et height
.
Ajoutez des routes permettant de récupérer les PokemonType triés selon différents critères :