Qualité Algorithmique
Spring
SpringBoot
Spring
Spring Boot
class AService(val dbAccess: DbAccess)
interface DbAccess
class PostgresDb: DbAccess
class MySqlDb: DbAccess
val appPg = AService(PostgresDb())
val appMy = AService(MySqlDb())
class AService(val dbAccess: DbAccess)
interface DbAccess
class PostgresDb: DbAccess
class MySqlDb: DbAccess
val dbAccess = if (useMySql) MySqlDb() else PostgresDb()
val appPg = AService(dbAccess)
//
class MyConfig {
}
//
class MyConfig {
@Bean
fun myDb() = PostgresDb()
}
//
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService(dbAccess: DbAccess) = AService(dbAccess)
}
//
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService(dbAccess: DbAccess) = AService(dbAccess)
@Bean
fun restController(aService: AService) = RestController(aService)
}
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService(dbAccess: DbAccess) = AService(dbAccess)
@Bean
fun restController(aService: AService) = RestController(aService)
}
@Configuration
class MyConfig {
@Bean
fun myDb(aDriverFromALib: JdbcDriver) = GenericDb(aDriverFromALib)
@Bean
fun aService(dbAccess: DbAccess) = AService(dbAccess)
@Bean
fun restController(aService: AService) = RestController(aService)
}
//
class RestController {
@Autowired
lateinit var aService: AService
}
@Service
class RestController(val aService: AService)
@Service
class RestController {
@Autowired
lateinit var aService: AService
}
@Service
class RestController(val aService: AService)
@Service
class RestController {
@Autowired
lateinit var aService: AService
}
@Service // @Component @Repository ...
class RestController(val aService: AService)
curl -XGET localhost:8080/hello
Hello World
@RestController
class HelloController {
}
@RestController
class HelloController {
@GetMapping("/hello")
fun hello() = "Hello World"
}
start.spring.io
Créer HelloController.kt dans un sous-package controller.
@RestController
class HelloController {
@GetMapping("/hello")
fun hello() = "world"
}
Lancer **Application.kt qui est à la racine du projet (clic droit → run).
Appeler GET localhost:8080/hello et vérifier que la réponse est bien world.
GET /ponies/{name}/type
GET /ponies/{name}/?type=earth
GET /ponies?age=42
POST /ponies
GET /ponies/{name}
PUT /ponies/{name}
POST /ponies/{name}
@GetMapping("/hello")
fun queryParam(@RequestParam name: String) = "world of $name"
@GetMapping("/hello/{name}")
fun path(@PathVariable name: String) = "world of $name"
@PostMapping("/hello")
fun body(@RequestBody name: String) = "world of $name"
@GetMapping("/hello")
fun header(@RequestHeader name: String) = "world of $name"
@GetMapping("/hello/{name}")
fun path(@PathVariable name: String) = "world of $name"
@GetMapping("/hello/{name}")
fun helloPath(@PathVariable name: String) =
ResponseEntity.status(HttpStatus.OK).body("world of $name")
@GetMapping("/hello/{name}")
fun path(@PathVariable name: String) = "world of $name"
@GetMapping("/hello/{name}")
fun helloPath(@PathVariable name: String) =
ResponseEntity.ok("world of $name")
@GetMapping("/hello/{name}")
fun helloPath(@PathVariable name: String) = if (name.length <= 2) {
ResponseEntity.badRequest().body("Name size must be > 2")
} else {
ResponseEntity.ok("world of $name")
}
Desing Pattern - Data Transfert Object
Objet simple représentant la donnée
Dans le cas d’une API REST / Json il ne doit pas contenir de cycle pour être sérialisable en json.
data class PersonDTO(val name: String, val age: Int)
@GetMapping("/hello")
fun hello() = ResponseEntity.ok(PersonDTO("John", 42))
Le but de cet exercice est de créer un premier CRUD (Create, Read, Update, Delete).
Le CRUD doit manipuler des utilisateurs dont on a les informations: Nom, Prénom, Âge et Email.
Pour cette implémentation, une Map en mémoire permettra de faire office de base de données. La clé unique est l’adresse email.
L’implémentation se fera dans une classe UserController.
Le premier endpoint POST /api/users
qui prend le JSON d’un utilisateur, l’enregistre dans la Map et répond un HTTP 201 avec le contenu de l’utilisateur en body.
Exemple d’appel:
curl --location 'localhost:8080/api/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "firstName": "John", "lastName": "Doe", "age": 10, "email": "[email protected]" }'
Un endpoint de création doit normalement signaler si la ressource existe déjà.
Modifier le endpoint pour que si on envoie deux fois la même adresse email, la réponse soit un HTTP 409 (conflit).
Exemple d’appel:
curl --location 'localhost:8080/api/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "firstName": "John", "lastName": "Doe", "age": 10, "email": "[email protected]" }' \ && curl --location 'localhost:8080/api/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "firstName": "Another", "lastName": "Name", "age": 42, "email": "[email protected]" }'
Le premier endpoint de lecture est un endpoint de liste.
Un appel à GET /api/users
doit répondre 200 avec la liste des utilisateurs qui sont dans la Map.
Exemple d’appel:
curl --location 'localhost:8080/api/users'
Reponse:
[
{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"age": 10
}
]
Ajouter un endpoint GET /api/users/{email}
qui retourne :
un status 200 avec le contenu de l’utilisateur s’il existe dans la Map,
un status 404 sinon.
Exemple d’appel:
curl --location 'localhost:8080/api/users/[email protected]'
Reponse:
{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"age": 10
}
Ajouter un endpoint PUT /api/users/{email}
qui retourne :
un status 200 si l’utilisateur existe, met à jour l’utilisateur dans la Map et le retourne,
un status 400 sinon.
Exemple d’appel:
curl --location --request PUT 'localhost:8080/api/users/[email protected]' \ --header 'Content-Type: application/json' \ --data-raw '{ "firstName": "John", "lastName": "Doe", "age": 42, "email": "[email protected]" }'
Reponse:
{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"age": 42
}
Ajouter un endpoint DELETE /api/users/{email}
qui retourne :
un status 204 si l’utilisateur existe et le supprime de la Map,
un status 400 sinon.
Exemple d’appel:
curl --location --request DELETE 'localhost:8080/api/users/[email protected]'
Ajouter sur la liste des utilisateurs la possibilité de filtrer par âge.
Exemple d’appel:
curl --location 'localhost:8080/api/users?age=42'
Reponse:
[
{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"age": 42
}
]
Si ce n’est pas déjà fait, découper le code en 3:
UserController qui déclare le endpoint et fait une conversion UserDTO (monde externe) → User (domaine interne)
UserInMemoryRepository qui contient en propriété privée la Map
UserService qui contient le reste du code métier
/src /main /kotlin /monpackage /controller /dto UserDTO.kt UserController.kt /domain User.kt /service UserService.kt /repository UserInMemoryRepository.kt
Ces tests ne sont pas liés à l’utilisation de spring.
On peut utiliser un framework de test au choix (junit, spock…) et un système d’assertion au choix (junit, assertk…).
Junit est embarqué dans les dépendances Spring.
@Service
class DummyService {
fun doSomething(pony: String) = if (pony.length < 3) {
"bad"
} else {
"good"
}
}
class DummyServiceTest {
private val service = DummyService()
@Test
fun `length gt 3 is good`() {
// WHEN
val result = service.doSomething("pony")
// THEN
assertThat(result).isEqualTo("good") // assertK
}
}
@TestInstance(PER_CLASS)
class PonyTest {
@BeforeAll
fun beforeAll() = println("before all")
@BeforeEach
fun beforeEach() = println("before each")
@Test
fun test1() = println("test1")
@Test
fun test2() = println("test2")
@AfterEach
fun afterEach() = println("after each")
@AfterAll
fun afterAll() = println("after all")
}
before all
before each
test1
after each
before each
test2
after each
after all
@SpringBootTest
class DummyServiceTest {
@Autowired
private lateinit var service: DummyService
@Test
fun `length gt 3 is good`() {
// WHEN
val result = service.doSomething("pony")
// THEN
assertThat(result).isEqualTo("good") // assertK
}
}
@SpringBootTest
ne fonctionne que dans un sous package de l’application @SpringBootApplication
interface Dependency {
fun call(): Boolean
}
@Service
class DummyService(val dependency: Dependency) {
fun callDep(pony: String) = if (dependency.call()) {
"good"
} else {
"bad"
}
}
@SpringBootTest
class DummyServiceIntTest {
@Autowired
private lateinit var service: DummyService
@Test
fun `call good`() {
// WHEN
val result = service.callDep("pony")
// THEN
assertThat(result).isEqualTo("good")
}
}
@SpringBootTest
class DummyServiceIntTest {
@MockkBean
private lateinit var dependency: Dependency
@Autowired
private lateinit var service: DummyService
@Test
fun `call good`() {
// WHEN
val result = service.callDep("pony")
// THEN
assertThat(result).isEqualTo("good")
}
}
@SpringBootTest
class DummyServiceIntTest {
@MockkBean
private lateinit var dependency: Dependency
@Autowired
private lateinit var service: DummyService
@Test
fun `call good`() {
// GIVEN
every { dependency.call() } returns true
// WHEN
val result = service.callDep("pony")
// THEN
assertThat(result).isEqualTo("good")
}
}
every { dependency.call() } returns true
every { dependency.call(Pony("name") } returns "23"
every { dependency.call(any(), any()) } throws Exception("Nope")
testImplementation("com.willowtreeapps.assertk:assertk:0.27.0") testImplementation("io.mockk:mockk:1.13.8") testImplementation("com.ninja-squad:springmockk:4.0.2")
Sans utiliser @SpringBootTest, ajouter une couverture de tests unitaire à 100% sur UserInMemoryRepository.
En utilisant @SpringBootTest, ajouter une couverture de tests à 100% sur UserController en utilisant un mock de UserInMemoryRepository.
spring-boot-starter-validation
Permet d’ajouter de la validation sur les paramètres des méthodes.
@RestController
@Validated
class DemoController(val demoRepository: DemoRepository) {
@GetMapping
fun list(@RequestParam(required = false) @Size(min=2, max=20) name: String?)
= if (name == null) ...
@Valid @RequestBody demo: DemoDTO
data class DemoDTO(
val id: UUID = UUID.randomUUID(),
@field:Size(min=5, max=10)
val name: String,
)
Les erreurs de validation n’entrent pas dans le code de la fonction.
La gestion est externe et générique.
@ControllerAdvice
class HttpErrorHandler {
}
@ControllerAdvice
class HttpErrorHandler {
fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException) =
ResponseEntity.badRequest().body("You're arg is invalid")
}
@ControllerAdvice
class HttpErrorHandler {
@ExceptionHandler(MethodArgumentNotValidExceptSion::class)
fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException) =
ResponseEntity.badRequest().body("You're arg is invalid")
}
@ControllerAdvice
class HttpErrorHandler : ResponseEntityExceptionHandler() {
override fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest)
: ResponseEntity<Any>? {
return ResponseEntity.badRequest().body("You're arg is invalid")
}
}
Spring propose des tests de couche (layer).
Ces tests ne lancent qu’une partie de l’application.
Pour la partie web il faut remplacer @SpringBootTest par @WebMvcTest.
@WebMvcTest(DemoController::class)
class DemoControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
fun get() {
mockMvc.get("/api/demo")
.andExpect {
status { isOk() }
content { jsonPath("$.[0].name", `is`("name")) }
}
}
}
fun post() {
// GIVEN
every { demoRepository.save(any()) } returns DemoEntity(name = "name")
mockMvc.post("/api/demo") {
contentType = MediaType.APPLICATION_JSON
content = ObjectMapper().writeValueAsString(DemoEntity(name = "name"))
}
.andExpect {
status { isOk() }
content { jsonPath("$.name", `is`("name")) }
}
}
Ajouter de la validation :
le nom doit faire entre 2 et 30 caractères,
le prénom ne doit pas être vide,
l’âge doit être supérieur à 15,
l’email doit avoir un format valide.
Ajouter une class ControllerAdvice dans le package monpackage.config
Ce ControllerAdvice doit prendre les erreurs de validation et retourner une réponse avec dans le body:
le message d’erreur
le code http
le timestamp (date.now)
Dans le cas où le repository ne trouve pas l’utilisateur, lancer une exception UserNotFound.
Traiter l’exception dans le controllerAdvice
Faire une couverture des cas nominaux et des cas d’erreurs de UserController en utilisant WebMvcTest.
À la racine du dossier ressources.
application.properties
application.yml
application-dev.properties
application-dev.yml
En cas de conflit, la valeur prise est celle du dernier profil.
En yaml, on peut supprimer une valeur avec ~
.
VM option :
-Dspring.profiles.active=dev
Intellij ultimate fournit un champ pour les rajouter directement. |
@Value("\${custom.value:defaultValue}")
private lateinit var value: String
@ConfigurationProperties("custom-prop")
data class CustomProperties(val demo: String)
@Configuration
@EnableConfigurationProperties(CustomProperties::class)
class PropertiesConfig
Ajouter spring-boot-configuration-processor
pour avoir la génération de l’auto-complétion dans les fichiers de configuration. |
@Configuration
class DatabaseConfig {
@ConditionalOnProperty("db.external",
havingValue = "false",
matchIfMissing = true)
@Bean
fun inMemory() = UserInMemoryRepository()
}
En utilisant l’approche @Value, faire un endpoint /info
qui donne la liste des profils Spring actifs (la valeur passée en variable d’environnement).
La réponse de /info doit contenir "profiles": "dev"
en cas de profil dev "profiles": ""
sinon.
curl --location 'localhost:8080/api/info'
Reponse sans profile:
{
"profiles": []
}
Reponse avec le profile dev:
{
"profiles": ["dev"]
}
Ajouter au fichier de propriétés :
iut:
spring:
exo: 5
En utilisant l’approche @ConfigurationProperties, ajouter le contenu au endpoint qui doit donc répondre :
{
"profile": "dev",
"iut": {
"spring": {
"exo": 5
}
}
}
Spring Data JPA
jOOQ
MyBatis
Data Access Object
Design pattern pour séparer l’accès aux données persistantes.
Java Persistence API
Interface de programmation Java permettant d’organiser des données relationnelles.
JSR / Jakarta
spring-boot-starter-data-jpa
+
driver jdbc
=
Data Access Object (DAO) généré
Objet qui a une identité indépendante des changements de ses attributs.
Utilisé pour représenter un objet en base de données.
import jakarta.persistence.*
data class DemoEntity(
val id: Int?,
val name: String,
)
import jakarta.persistence.*
@Entity
data class DemoEntity(
val id: Int?,
val name: String,
)
import jakarta.persistence.*
@Entity
@Table(name = "Demo")
data class DemoEntity(
val id: Int?,
val name: String,
)
import jakarta.persistence.*
@Entity
@Table(name = "Demo")
data class DemoEntity(
@Id
val id: Int?,
val name: String,
)
import jakarta.persistence.*
@Entity
@Table(name = "Demo")
data class DemoEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int?,
val name: String,
)
import jakarta.persistence.*
@Entity
@Table(name = "Demo")
data class DemoEntity(
@Id
val id: UUID = UUID.randomUUID(),
val name: String,
)
interface JpaRepository<ENTITY, ID>
interface DemoRepository : JpaRepository<DemoEntity, UUID>
fun save(entity: T): T
fun findAll(): List<T>;
fun findById(id: ID): Optional<T>
fun deleteById(id: ID): Unit
fun deleteAll(): Unit
interface DemoRepository : JpaRepository<DemoEntity, UUID> {
fun findAllByName(name: String): List<DemoEntity>
}
@Query(value = "SELECT d from DemoEntity d where d.name = :name")
fun manual(name: String): List<DemoEntity>
@Query(value = """SELECT d from DemoEntity d
where (:name is null or d.name = :name)""")
fun manual(name: String?): List<DemoEntity>
interface DemoRepositoryCustom {
fun criteria(name: String?): List<DemoEntity>
}
interface DemoRepository :
JpaRepository<DemoEntity, UUID>,
DemoRepositoryCustom
class DemoRepositoryCustomImpl : DemoRepositoryCustom {
@PersistenceContext
private lateinit var entityManager: EntityManager
override fun criteria(name: String?): List<DemoEntity> {
val criteriaBuilder = entityManager.criteriaBuilder
val queryBuilder = criteriaBuilder.createQuery(DemoEntity::class.java)
val root: Root<DemoEntity> = queryBuilder.from(DemoEntity::class.java)
var query = queryBuilder.select(root)
if (name != null) {
val nameField: Path<DemoEntity> = root.get("name")
query = query.where(criteriaBuilder.equal(nameField, name))
}
return entityManager.createQuery(query).resultList
}
}
Spring propose des tests de "Layer".
Ces tests ne lancent qu’une partie de l’application.
Pour JPA, il faut remplacer @SpringBootTest par @DataJpaTest.
@DataJpaTest
class DemoRepositoryTest {
@Autowired
private lateinit var jpaRepository: DemoRepository
@Test
fun `find one existing`() {
// GIVEN
jpaRepository.save(DemoEntity(randomUUID(), "name"))
// WHEN
val result = jpaRepository.findAllByName("name")
// THEN
assertThat(result).hasSize(1)
}
}
Bien prendre tous les ajouts ! |
Extraire une interface UserRepository de UserInMemoryRepository.
Créer une classe UserDatabaseRepository qui implémente UserRepository et qui utilise un JpaRepository pour interroger la base de données.
Pour choisir entre l’implementation in-memory ou h2, remplacer l’annotation @Repository par un fichier de configuration avec des @ConditionalOnProperty
Utiliser le JPA par nom de méthode pour la liste des utilisateurs
Couvrir avec des tests UserDatabaseRepository.
UserDatabaseRepository peut être créer dans un BeforeEach. |
En SQL pour gérer des données hiérarchiques on utilise des jointures.
En JPA elles sont représenté par quatres annotations:
@OneToOne
@OneToMany
@ManyToOne
@ManyToMany
La cascade est la propagation d’une modification aux enfants de l’entité.
Si l’objet A contient l’objet B, lors d’un "update" de A en base, je peux vouloir modifier/ajouter/supprimer l’objet B ou ignorer toutes les modifications de B
Une relation peut être uni-directionnel ie je ne peux aller que de l’objet A vers l’objet B
ou bi-directionnel ie je peux aller de A à B et de B à A.
L’annotation @JoinColumn permet de fournir à hibernate des informations sur la manière de lier les entités.
name: nom de la foreign key
referencedColumnName : le nom de la colonne de l’autre entité utilisé pour la jointure.
@JoinColumn(referencedColumnName = "email")
@Entity
@Table(name = "users")
class UserEntity(
@Id val email: String,
@OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(referencedColumnName = "email")
val phone: PhoneEntity,
) {
@Entity
@Table(name = "phone")
class PhoneEntity(
@Id // Doit être unique, peut aussi être un @Column(unique = true)
val email: String,
val number: String,
)
class UserEntity(
@Id val email: String,
@OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "fk_email")
var phone: PhoneEntity?,
)
class PhoneEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Int?,
@OneToOne(mappedBy = "phone")
val user: UserEntity,
val number: String,
)
class UserEntity(
@Id val email: String,
@OneToOne(mappedBy = "user", cascade = [CascadeType.ALL])
var phone: PhoneEntity?,
)
class PhoneEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Int?,
@OneToOne
val user: UserEntity,
val number: String,
)
class UserEntity(
@Id val email: String,
@OneToMany(cascade = [CascadeType.ALL])
@JoinColumn(referencedColumnName = "email")
val phones: List<PhoneEntity> = emptyList(),
)
class PhoneEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Int?,
val email: String,
val number: String,
)
class UserEntity(
@Id val email: String,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "user")
var phones: List<PhoneEntity> = emptyList(),
)
class PhoneEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Int?,
@ManyToOne
@JoinColumn(name="fk_email")
val user: UserEntity?,
val number: String,
)
class UserEntity(
@Id val email: String,
@ManyToMany(cascade = [CascadeType.ALL])
@JoinTable(
name = "user_phone",
joinColumns = [JoinColumn(name = "email")],
inverseJoinColumns = [JoinColumn(name = "id")])
var phones: List<PhoneEntity> = emptyList(),
)
class PhoneEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Int?,
@ManyToMany
val user: List<UserEntity>,
val number: String,
)
Ajouter pour chaque utilisateur une liste de numéro de téléphones
Exemple d’appel:
curl --location 'localhost:8080/api/users'
Reponse:
[
{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"age": 10,
"phones": [
{
"type": "personal",
"number": "0611223344"
}
]
}
]
Ajouter pour chaque utilisateur une liste d’amis, chaque amis est un autre utilisateur
Exemple d’appel:
curl --location 'localhost:8080/api/users'
Reponse:
[
{
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"age": 30,
"phones": [
{
"type": "personal",
"number": "0611223344"
}
],
"friends": {
"email": "[email protected]",
"firstName": "Pinkie",
"lastName": "Pie"
}
},
{
"email": "[email protected]",
"firstName": "Pinkie",
"lastName": "Pie"
"age": 19,
"phones": [],
"friends": {
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe"
}
}
]
En utilisant @Query, faire une API qui retourne tous les utilisateurs avec au moins un numéro de téléphone
Pour être "production ready" une application doit être en mesure de fournir :
un health-check,
des metrics,
des logs.
Ajouter la dépendance Spring fournit directement plusieurs endpoints sous /actuator
.
endpoints | description |
---|---|
/actuator/health | santé de l’application |
/actuator/info | information général sur l’application |
/actuator/metrics | métriques de l’application |
/actuator/beans | liste des beans et de leur dépendance |
management:
endpoints:
web:
exposure:
include: info, health
'*' expose tous les enpoints. |
@Component
@EndpointWebExtension(endpoint = InfoEndpoint::class)
class CustomInfo(val delegate: InfoEndpoint) {
@ReadOperation
fun info(): WebEndpointResponse<Map<*, *>> {
val info = this.delegate.info()
info["custom.value"] = "pony"
return WebEndpointResponse(info, 200)
}
}
micrometer-registry-prometheus
@Component
class MetricsConfig(meterRegistry: MeterRegistry) {
private val myCount = meterRegistry.counter("name.my.count",
"aDimension", "theValue")
override fun theIncrement() {
myCount.increment()
}
De base dans Spring :
slf4j + logback
Optionel :
oshai:kotlin-logging.
ERROR
WARN
INFO
DEBUG
TRACE
logging:
level:
org.springframework.web: DEBUG
bzh.zomzog.prez: WARN
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<logger name="org.springframework.web" level="DEBUG"/>
</configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) -%kvp -%msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
private val logger = LoggerFactory.getLogger(javaClass)
logger.trace("trace of ${name}")
logger.warn("warning with exception", Exception())
Modifier les logs pour qu’ils soient sous la forme :
INFO - class - threadId - message
Ajouter des logs info au début des appels HTTP et de debug dans les repositories.
Ajouter la dépendance actuator à votre projet et vérifier qu’elle fonctionne.
Les endpoints sont sous localhost:8080/actuator.
Par exemple : localhost:8080/actuator/info.
Ajouter le contenu du endpoint précédent /api/info dans la réponse du /actuator/info
Certains endpoints de l’actuators exposent des données sensibles, voire sont dangereux.
En modifiant les propriétés
Sans profil, il ne doit y avoir que : health, info et metrics.
Avec le profil dev : shutdown est disponible.
Changer le port des actuators pour le port 9001.
Ajouter une métrique api.call
qui s’incrémente à chaque appel des endpoints.
Utiliser les dimensions pour faire la difference entre GET/PUT/POST/DELETE
Un IDL (Interface Definition Language) pour décrire des APIs.
Le but est de fournir un contrat entre producteur et consommateur de l’API.
format: YAML ou JSON.
openapi: 3.0.3
info:
title: OAS Petstore - OpenAPI 3.0
description: PetStore API
termsOfService: http://swagger.io/terms/
contact:
email: [email protected]
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
version: 1.0.11
servers:
- url: https://localhost:8080/api/v1
tags:
- name: pet
description: Everything about your Pets
externalDocs:
description: Find out more
url: http://swagger.io
- name: store
description: Access to Petstore orders
externalDocs:
description: Find out more about our store
url: http://swagger.io
- name: user
description: Operations about user
paths:
/pet/{petId}:
put:
tags:
- pet
summary: Update an existing pet
description: Update an existing pet by Id
operationId: updatePet
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
requestBody:
description: Update an existent pet in the store
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
required: true
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
security:
- petstore_auth:
- write:pets
- read:pets
paths:
/pet/{petId}:
put:
tags:
- pet
summary: Update an existing pet
description: Update an existing pet by Id
operationId: updatePet
security:
- petstore_auth:
- write:pets
- read:pets
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
requestBody:
description: Update an existent pet in the store
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
required: true
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Pet:
required:
- name
type: object
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: doggie
Swagger UI
springdoc-openapi-starter-webmvc-ui
Avec juste l’ajout de la dépendance on a :
swagger-ui: http://server:port/swagger-ui.html
l’OAS3: http://server:port/v3/api-docs
@Operation(summary = "List users")
@ApiResponses(value = [
ApiResponse(responseCode = "200", description = "List users",
content = [Content(mediaType = "application/json",
array = ArraySchema(
schema = Schema(implementation = UserDTO::class)))])])
@Parameter(description = "id of book to be searched")
@PathVariable id: UUID
Rajouter la dépendance springdoc-openapi-starter-webmvc-ui.
Vérifier que swagger-ui et l’OAS3 sont accessibles.
Ajouter la documentation des endpoints de UserController avec:
une description de chaque endpoints
une description de chaque parametre
une description des réponses http
des tags READ/WRITE suivant les endpoints
Spring fournis un client HTTP :
val restTemplate = RestTemplate()
val result: ResponseEntity<List<UserDTO>> =
restTemplate.getForEntity("http://localhost:9999/api/users")
if (result.statusCode.is2xxSuccessful) {
return result.body
} else {
throw Exception("Fail")
}
Simplification des appels :
val restTemplate = RestTemplateBuilder().rootUri("http://localhost:9999")
.build()
val result: ResponseEntity<List<UserDTO>> =
restTemplate.getForEntity("/api/users")
val body = UserDTO("email", "name", "last", 1)
val entity = HttpEntity(body)
val single = restTemplate
.postForEntity("/api/users", entity, UserDTO::class.java)
val multi = restTemplate
.postForEntity("/api/users", entity, Array<UserDTO>::class.java)
Le projet correction contient l’application dummy-app qui se lance sur le port 9999.
C’est un micro service de géstion de poneys.
Il expose un CRUD classique:
GET /api/ponies
POST /api/ponies { "name": "Le Nom", "type": "EARTH" }
GET /api/ponies/{id}
PUT /api/ponies/{id} { "name": "Le Nom", "type": "EARTH" }
DELETE /api/ponies/{id}
En base ajouter un PonyId à votre UserEntity.
En utilisant un client RestTemplate:
lors de la création d’un utilisateur, faire la création du poney et sauvegarder son ID en BDD
lors de la récupération d’un utilisateur, aller chercher le détail sur dummy-app
lors de la suppression d’un utilisateur, supprimer le poney
[
{
"email": "[email protected]",
(...)
"pony": {
"id": 1,
"name": "Pinkie Pie",
"type": "EARTH"
}
}
]
RestTemplate est un bon candidat pour un bean.
Déplacer sa création en tant que bean dans une classe RestTemplateConfig. La rootUri doit être configurable en properties.
Dummy-app ne peut pas avoir deux poneys avec le même nom.
Ajouter un contrôle pour ne créer le poney que s’il n’existe pas et le supprimer uniquement si aucun autre utilisateur n’y est lié.
org.springframework.boot:spring-boot-starter-webflux
Mono<Pony> : 1 item
Flux<Pony> : n items
Mono.error<String>(Exception())
.map(it -> it + "1") // non appelé
Mono.just("Data")
val wc = WebClient.builder().baseUrl("http://localhost:9997")
.defaultHeaders {
it.add("name", "value")
}
.build()
wc.get()
.uri { uriBuilder: UriBuilder ->
uriBuilder.path("/api/ponies")
.build()
}.retrieve()
.bodyToMono<String>()
.block()
wc.get()
.uri { ... }.retrieve()
.onStatus(
{ status: HttpStatusCode -> status.isError },
{ response -> Mono.error(
ResponseStatusException(response.statusCode(), "the error"))
}
)
.bodyToMono<String>()
.block()
wc.get()
...
.bodyToMono<String>()
.retryWhen(Retry.backoff(5, Duration.ofSeconds(1))
.filter {
when (it) {
is ResponseStatusException -> it.statusCode.is5xxServerError
else -> false
}
})
.block()
Reprendre l’exercice 10 en remplaçant le RestTemplate par un WebClient