Programmation avancée

Thibault Duperron

bigM

Decathlon Digital Nantes

~100 personnes

Lead tech Backend

Cours 1

Java

snow white blow

Java SE - Java Standard Edition

JSR

Java Specification Requests, constituant les spécifications

JRE

un Java Runtime Environment, contenant le seul environnement d’exécution

JDK

Java Development Kit (JDK), contenant les bibliothèques logicielles (compilateur, debug…​)

Jakarta EE

Historique des noms

J2EE

1999 - 2006

Java EE

2006 - 2019

Jakarta EE

2019 -

Jakarta EE

Ensemble de spécifications pour faire des applications pour les entreprises

Les versions sont retro-compatible

archi jee

JPA - Java Persistence API

Par exemple la communication avec une base de données

JTA - Java Transaction API

Par exemple une transaction SQL

JMS - Java Message Service

Par exemple la communication avec ActiveMq

EJB - Entreprise Java Bean

Composant logiciel pouvant être appelé par le serveur

CDI - Contexts and Dependency Injection

Servlet - Point entrée application

Classe Java de génération de contenu dynamique

Non limité au HTTP (JDBC…​)

JSP - Java Server page

Génération de contenu statique (html…​)

JSF - Java server faces

EL - Expressions Languages

Application "riche" ie communication avec le serveur (validation…​)

JAX

JAX.WS - Java Api for XML Web Service

JAX.RS - Java API for RESTful Web Service

Communication http moderne

Serveur Application

Implementation des spécifications JavaEE

  • Glassfish

  • WebLogic

  • WebSphere

  • JBoss

  • …​

Fonctionnement

Création de l’appication java et packaging en .WAR

Installation et lancement du serveur

Ajout du WAR dans le serveur

Déploiement du code et mapping des servlets

archi jee

Conteneur Web / Servlet

Serveurs léger qui ne font "que" les parties servlet et jsp

Exemple de serveurs développés en Java

  • Tomcat

  • Jetty

Spring != Jakarta

Spring a été créé en 2003 comme une alternative légère pour répondre à la complexité des premières versions des spécifications J2EE

Pas d’EJB

Mais des servlets

Spring - Histoire

Diagram

Spring

Spring est en premier lieu un système d’injection de dépendances

Spring fournit des librairies d’abstraction d’autres frameworks

  • spring-security

  • spring-data

  • spring-kafka

  • …​

Spring

Diagram

Spring Boot

Diagram

Spring vs Spring Boot

Spring ingredients

SpringBoot gateau

Spring vs Spring Boot

Spring ingredients

Spring Boot alsa

Injection de dépendances

di everywhere

Injection de dépendances

Diagram
class AService() {
    val db = PostgresDb()

    fun findAll() = db.findAllInDb()
}

Injection de dépendances

Diagram
class AService() {
  val pg = PostgresDb()
  val my = MySqlDb()

  fun findAll(pg: Boolean) = if(pg) {
    db.findAllInDb()
  } else {
    my.findAllInDb()
  }
}

Injection de dépendances

Diagram
class AService(val db: DbAccess) {}
  fun findAll() = db.findAllInDb()
}

interface DbAccess {
  fun findAll() = TODO()
}

class PostgresDb(): DbAccess {
  override fun findAll() = TODO()
}

class MySqlDb(): DbAccess{
  override fun findAll() = TODO()
}

Injection de dépendances

val appPg = AService(PostgresDb())
val appMy = AService(MySqlDb())

Beans

@Configuration
class MyConfig {








}

Beans

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()






}

Beans

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService(myDb())



}

Beans

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService(myDb())

    @Bean
    fun another() = Other(myDb())
}

Application Context

fun main() {
  val context: ApplicationContext =
     AnnotationConfigApplicationContext(MyConfig::class.java)


}

Application Context

fun main() {
  val context: ApplicationContext =
     AnnotationConfigApplicationContext(MyConfig::class.java)
  val service = context.getBean(AService::class.java)

}

Application Context

fun main() {
  val context: ApplicationContext =
     AnnotationConfigApplicationContext(MyConfig::class.java)
  val service = context.getBean(AService::class.java)
  service.findAllInDb()
}

Scope

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService(myDb())

    @Bean
    fun another() = Other(myDb())
}

aService.dbAccess == another.dbAccess

Scope

@Configuration
class MyConfig {
    @Bean @Scope(BeanDefinition.SCOPE_SINGLETON)
    fun myDb() = PostgresDb()

    @Bean @Scope(BeanDefinition.SCOPE_SINGLETON)
    fun aService() = AService(myDb())

    @Bean @Scope(BeanDefinition.SCOPE_SINGLETON)
    fun another() = Other(myDb())
}

aService.dbAccess == another.dbAccess

Scope

@Configuration
class MyConfig {
    @Bean @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService(myDb())

    @Bean
    fun another() = Other(myDb())
}

aService.dbAccess != another.dbAccess

Scope

Singleton → un unique bean

Prototype → un bean par instance d’objet

Web-aware Scope

Request → un bean pour la durée de vie de la requête HTTP

Session → un bean pour la durée de la session HTTP

Application → un bean pour la durée de vie de la servlet

WebSocket → un bean pour la durée de vie de la WebSocket

Proxy proxy proxy

@Configuration
class MyConfig {
    @Bean
●   fun myDb() = PostgresDb()
}

Stack du breakpoint

myDb:4, MyConfig (bzh.zomzog)
CGLIB$myDb$2:-1, MyConfig$$SpringCGLIB$$0 (bzh.zomzog)
Invoke-1, MyConfig$$SpringCGLIB$$FastClass$$1 (bzh.zomzog)
...

Autowired

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService(myDb())
}
class AService (

    val database: DBAccess
)

Autowired

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService()
}
class AService {

    lateinit var database: DbAccess
}

Autowired

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService()
}
class AService {
    @Autowired
    lateinit var database: DbAccess
}

Stereotype

@Configuration
@ComponentScan("bzh.zomzog.iut.poc")
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    //@Bean
    //fun aService() = AService()
}
@Service
class AService {
    @Autowired
    lateinit var database: DBAccess
}

Stereotype

@Component → déclare que la classe doit devenir un bean lors du scan

3 alias:

@Controller

@Service

@Repository

Stereotype - @Configuration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
}

@Configuration est une extension de component, mais a son propre cycle de vie

@Configuration crée quand même un bean

Depenency Injection

@Service
class AService {
    @Autowired
    lateinit var database: DBAccess
}
@Service
class AService(database: DBAccess) {
}

Depenency Injection

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService() = AService(myDb())
}
@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()

    @Bean
    fun aService(dbAccess: DBAccess) = AService(dbAccess)
}

External Beans

@Configuration
class MyConfig {
    @Bean
    fun myDb(aDriverFromALib: JdbcDriver) = GenericDb(aDriverFromALib)

    @Bean
    fun aService() = AService(myDb())

    @Bean
    fun another() = Other(myDb())
}

Beans limits

Le nom de chaque bean doit être unique

Si plusieurs beans correspondent à un autowired, la résolution doit être explicitée

Il ne faut pas de cycle pour leur création

Noms

Par défaut, un bean a le nom de la méthode qui le crée

On peut le forcer @Bean("monNom")

Conflit

@Primary sur un bean → en cas de conflit, c’est lui qui est choisi

@Autowired @Qualifier("monNom") spécifie le bean attendu

SpringBoot

@Configuration
@ComponentScan("bzh.zomzog.iut.poc")
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()
}
fun main() {
  val context: ApplicationContext =
     AnnotationConfigApplicationContext(MyConfig::class.java)
  val service = context.getBean(AService::class.java)
  service.findAllInDb()
}

SpringBoot

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()
}
@SpringBootApplication
class PocApplication

fun main(args: Array<String>) {
    runApplication<PocApplication>(*args)

Spring Boot Application

@SpringBootApplication contient @ComponentScan

Il ne scan QUE son package et ses sous package

Spring Boot Application

Lancer le main:

  • lance un tomcat qui écoute sur le port 8080

  • lance les auto-configurations et scans de package

Hello World Rest

Requête

curl -XGET localhost:8080/hello

Réponse:

Hello World

spring-boot-starter-web

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

RestController

RestController → Controller → Component

RestController

@RestController
class HelloController {


}

RestController

@RestController
class HelloController {

    @GetMapping("/hello")
    fun hello() = "Hello World"
}

Mappings

GetMapping

PostMapping

PutMapping

DeleteMapping

alias →

RequestMapping

API Rest

GET /ponies/{name}/type

API Rest

GET /ponies/{name}/?type=earth

Lister

GET /ponies?age=42

Créer

POST /ponies

Obtenir un élément

GET /ponies/{name}

Mettre à jour un élément

PUT /ponies/{name}

Supprimer un élément

POST /ponies/{name}

Paramètres

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

Code retour

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

Code retour

    @GetMapping("/hello/{name}")
    fun path(@PathVariable name: String) = "world of $name"

    @GetMapping("/hello/{name}")
    fun helloPath(@PathVariable name: String) =
        ResponseEntity.ok("world of $name")

Code retour

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

DTO

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.

DTO

data class PersonDTO(val name: String, val age: Int)
@GetMapping("/hello")
fun hello() = ResponseEntity.ok(PersonDTO("John", 42))

Tests

code coverage

Pyramide des Tests

pyramide test

Tests unitaires

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.

Tests unitaires

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

Tests unitaires

@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

Tests d’intégration

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

warning

@SpringBootTest ne fonctionne que dans un sous package de l’application @SpringBootApplication

warning

Bouchons (mock) pour test d’integration

interface Dependency {
    fun call(): Boolean
}
@Service
class DummyService(val dependency: Dependency) {

    fun callDep(pony: String) = if (dependency.call()) {
        "good"
    } else {
        "bad"
    }
}

Mockk (version Kotlin de Mockito)

@SpringBootTest
class DummyServiceIntTest {
    @Autowired
    private lateinit var service: DummyService

    @Test
    fun `call good`() {
        // WHEN
        val result = service.callDep("pony")
        // THEN
        assertThat(result).isEqualTo("good")
    }
}

Mockk (version Kotlin de Mockito)

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

Mockk (version Kotlin de Mockito)

@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

every { dependency.call() } returns true
every { dependency.call(Pony("name") } returns "23"
every { dependency.call(any(), any()) } throws Exception("Nope")

Cours 2

Rapel

SpringBoot

@Configuration
class MyConfig {
    @Bean
    fun myDb() = PostgresDb()
}

RestController

@RestController
class HelloController {

    @GetMapping("/hello")
    fun hello() = "Hello World"
}

Spring validation

Spring validation

spring-boot-starter-validation

Permet d’ajouter de la validation sur les paramètres des méthodes.

Activation validation

//
class DemoController(val demoRepository: DemoRepository) {
  fun list(
     i: Int // >= 10
  ) = ...

Activation validation

@Validated
class DemoController(val demoRepository: DemoRepository) {
  fun list(
     i: Int // >= 10
  ) = ...

Activation validation

@Validated
class DemoController(val demoRepository: DemoRepository) {
  fun list(
     @Min(10) i: Int
  ) = ...

Activation validation

@Validated
class DemoController(val demoRepository: DemoRepository) {
  fun list(
     @Max(5) @Min(10) i: Int
  ) = ...

Constraint Annotation

@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Min {
 String message() default "{jakarta.validation.constraints.Min.message}";

 Class<?>[] groups() default { };

 Class<? extends Payload>[] payload() default { };

 long value();
}

Meta annotation

@Target(allowedTargets = [AnnotationTarget.VALUE_PARAMETER])
@Retention(RUNTIME)
@Constraint(validatedBy = [])
@Min(value = 0)
@Max(value = 10)
annotation class MinMax(
    val message: String = "Value must be between min and max",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = [],
)

Meta annotation

@Validated
class DemoController(val demoRepository: DemoRepository) {
  fun list(
     @MinMax i: Int
  ) = ...

Custom annotation

@Target(allowedTargets = [AnnotationTarget.VALUE_PARAMETER])
@Retention(RUNTIME)
@Constraint(validatedBy = [MinMaxValidator::class])
annotation class MinMax(
  val min: Int,
  val max: Int,
  val message: String = "Value must be between min and max",
  val groups: Array<KClass<*>> = [],
  val payload: Array<KClass<out Payload>> = [],
)

Custom annotation

class MinMaxValidator: ConstraintValidator<MinMax, Int> {
  private var min: Int = 0
  private var max: Int = 0

  override fun initialize(annotation: MinMax) {
    min = annotation.min
    max = annotation.max
  }

  override fun isValid(value: Int?,
                       context: ConstraintValidatorContext?): Boolean {
    return value != null &&
           value in min..max
  }
}

Custom annotation

@Validated
class DemoController(val demoRepository: DemoRepository) {
  fun list(
     @MinMax(0, 10) i: Int
  ) = ...

API Validation

@RestController
@Validated
class DemoController(val demoRepository: DemoRepository) {
  @GetMapping
  fun list(@RequestParam(required = false) @Size(min=2, max=20) name: String?)
      = if (name == null) ...

Validation du body

@RestController
@Validated
class DemoController(val demoRepository: DemoRepository) {
  @PostMapping
  fun save(@Demo  @RequestBody demo: DemoDTO) = ...
data class DemoDTO(
        val id: UUID = UUID.randomUUID(),
        @field:Size(min=5, max=10)
        val name: String,
)

Validation du body

@RestController
@Validated
class DemoController(val demoRepository: DemoRepository) {
  @PostMapping
  fun save(@Valid @RequestBody demo: DemoDTO) = ...
data class DemoDTO(
        val id: UUID = UUID.randomUUID(),
        @field:Size(min=5, max=10)
        val name: String,
)

Annotation target

class Pony {
  @OnName
  private String name;

  @OnGet
  public String getName() {
    return name;
  }

  @OnSet
  public String setName(String n) {
    name = n;
  }
}
class Pony(
  @field:OnName
  @get:OnGet
  @set:OnGet
  var name: String
)

Gestion des erreurs

Gestion des erreurs

Les erreurs de validation n’entrent pas dans le code de la fonction.

La gestion est externe et générique.

Current schema

Diagram

DispatcherServlet

Diagram

DispatcherServlet

Diagram

DispatcherServlet - PSEUDO-code

fun dispatch(request: HttpServletRequest) {
    try {
        val parameters = deserialize(request)
        val handler = getHandler(request)
        val response = handler.handle(parameters)
        return serialize(response)
    } catch (e: Exception) {
        val response = errorHandler.handle(e)
        return serialize(response)
    }
}

ControllerAdvice

@ControllerAdvice
class HttpErrorHandler {




}

ControllerAdvice

@ControllerAdvice
class HttpErrorHandler {


    fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException) =
        ResponseEntity.badRequest().body("You're arg is invalid")
}

ControllerAdvice

@ControllerAdvice
class HttpErrorHandler {

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException) =
        ResponseEntity.badRequest().body("You're arg is invalid")
}

ControllerAdvice

@ControllerAdvice
class HttpErrorHandler {

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException) =
        ResponseEntity.badRequest().body("You're arg is invalid")

    @ExceptionHandler(Exception::class)
    fun fallback(e: Exception) =
        ResponseEntity.internalServerError().body("Unhandled error")
}

ControllerAdvice - Spring

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

Tests

@AutoConfigureMockMvc
@SpringBootTest
class MovieControllerTest {

    @Autowired
    lateinit var mockMvc: MockMvc

MockMvc

fun post() {
    mockMvc.post("/api/demo") // mockMvc.perform(post("/api/movies"))








}

MockMvc

fun post() {
    mockMvc.post("/api/demo") {
        contentType = MediaType.APPLICATION_JSON
        content = ObjectMapper()
            .writeValueAsString(DemoEntity(name = "name"))
    }




}

MockMvc

fun post() {
    mockMvc.post("/api/demo") {
        contentType = MediaType.APPLICATION_JSON
        content = ObjectMapper()
            .writeValueAsString(DemoEntity(name = "name"))
    }
    .andExpect {
        status { isOk() }

    }
}

MockMvc

fun post() {
    mockMvc.post("/api/demo") {
        contentType = MediaType.APPLICATION_JSON
        content = ObjectMapper()
            .writeValueAsString(DemoEntity(name = "name"))
    }
    .andExpect {
        status { isOk() }
        content { jsonPath("$.name", `is`("name")) }
    }
}

MockMvc

fun get() {
    mockMvc.get("/api/demo/{id}?param=value", "theId") {
        headers {
            contentLanguage = Locale.FRANCE
        }
        param("name", "value")
    }
    .andDo {
        print()
    }
    .andExpect {
        status { isOk() }
    }
  }

Layers

@SpringBootTest

Diagram

@WebMvcTest

Diagram

WebMvcTest

@WebMvcTest
class DemoControllerTest {

    @MockkBean
    private lateinit var demoRepository: Repository
    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun get() {
        every { demoRepository.save(any()) } returns Unit
        mockMvc.get("/api/demo")
                .andExpect { status { isOk() } }
    }
}

WebMvcTest

@WebMvcTest(DemoController::class)
class DemoControllerTest {

    @MockkBean
    private lateinit var demoRepository: Repository
    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun get() {
        every { demoRepository.save(any()) } returns Unit
        mockMvc.get("/api/demo")
                .andExpect { status { isOk() } }
    }
}

Application Configuration

Application Configuration

src/resources/application.properites

spring.application.name=demo
spring.main.lazy-initialization=true

src/resources/application.y(a?)ml

spring:
    application.name: demo
    main.lazyInitialization: true

lazyInitialization équivalent à lazy-initialization

properties > yml > yaml

Diagram

Profiles

Les profils son un moyen de séparer des parties de la configuration

Exemple:

un profile MySql

un profile MongoDb

Activer un profile

Dans les fichiers application.*

spring.profiles.active=dev,mysql

En variable d’environnement

export spring_profiles_active=dev

JVM parameter

java -jar app.jar -Dspring.profiles.active=prod,mongo

Ordre

-Dspring.profiles.active=prod,mongo
Diagram

Ordre

-Dspring.profiles.active=prod,mongo
Diagram

Accès aux propriétés - Injection

class Demo {
  @Value("\${custom.path.value}")
  private lateinit var value: String
}

Accès aux propriétés - Injection

class Demo {
  @Value("\${custom.path.value:defaultValue}")
  private lateinit var value: String
}

Accès aux propriétés

path:
  custom-prop:
    demo: val1
    anotherDemo: val2
@ConfigurationProperties("path.custom-prop")
data class CustomProperties(
    val demo: String,
    val anotherDemo: String,
)
@Configuration
@EnableConfigurationProperties(
    CustomProperties::class)
class PropertiesConfig

Utilisation

EnableConfigurationProperties = Création de bean

@Service
class Demo(val properties: CustomProperties) {
}
@Configuration
class Demo {
    @Bean
    fun aBean(val properties: CustomProperties) =...
}

Validation

@ConfigurationProperties("path.custom-prop")
data class CustomProperties(
    @NotBlank val demo: String,
    @Min(10) val anotherDemo: Int,
)

Conditional Bean

@Configuration
class DatabaseConfig {
    @ConditionalOnProperty("db.external",
                           havingValue = "true")
    @Bean
    fun mongo(): Database = UserMongoRepository()






}

Conditional Bean

@Configuration
class DatabaseConfig {
    @ConditionalOnProperty("db.external",
                           havingValue = "true")
    @Bean
    fun mongo(): Database = UserMongoRepository()

    @ConditionalOnProperty("db.external",
                           havingValue = "false")

    @Bean
    fun inMemory(): Database = UserInMemoryRepository()
}

Conditional Bean

@Configuration
class DatabaseConfig {
    @ConditionalOnProperty("db.external",
                           havingValue = "true")
    @Bean
    fun mongo(): Database = UserMongoRepository()

    @ConditionalOnProperty("db.external",
                           havingValue = "false",
                           matchIfMissing = true)
    @Bean
    fun inMemory(): Database = UserInMemoryRepository()
}

Conditional Bean on Profile

@Configuration
class DatabaseConfig {

    @Bean
    @Profile("mongo")
    fun mongo(): Database = UserMongoRepository()

    @Bean
    @Profile("!mongo")
    fun inMemory(): Database = UserInMemoryRepository()
}

ConditionalOnProperty

ConditionalOnBean

ConditionalOnMissingBean

ConditionalOnClass

ConditionalOnJava

ConditionalOnResource

ConditionalOnExpression

ConditionalOnJndi

ConditionalOnThreading

ConditionalOnNotWarDeployment

ConditionalOnNotWebApplication

ConditionalOnCheckpointRestore

ConditionalOnCloudPlatform

ConditionalOnSingleCandidate

ConditionalOnWarDeployment

ConditionalOnWebApplication

ConditionalOnMissingClass

Accès à la base de données SQL

Spring Data JPA

jOOQ

MyBatis

JPA - Pourquoi?

Spring est basé sur des POJO

Pour les contrôleurs on représente les JSON sous forme de classes

C’est pareil pour les bases de données

Base relationnel

Id

Name

Kind

1

Discord

Draconequus

2

Rainbow Dash

Pegasus

3

Pinkie Pie

Earth

PonyId

Occupation

1

Honorary

1

Ruler of Equestria

3

Baker

Kotlin Version

class Pony(
    val id: Long?,
    val name: String,
    val kind: String,
    val occupations: List<Occupation>,
)

class Occupation(
    val name: String,
)

Entity

@Entity
class Pony(
    @Id @GeneratedValue
    val id: Long?,
    val name: String,
    val kind: String,
)

EntityManager

class Repository {
  @PersistenceUnit
  private lateinit var factory: EntityManagerFactory

  fun save(pony: Pony) = factory.createEntityManager().use { em ->
      em.transaction.begin()
      em.persist(pony)
      em.transaction.commit()
  }





}

EntityManager

class Repository {
  @PersistenceUnit
  private lateinit var factory: EntityManagerFactory

  fun save(pony: Pony) = factory.createEntityManager().use { em ->
    em.transaction.begin()
    em.persist(pony)
    em.transaction.commit()
  }
  fun findAll(): List<Pony> = factory.createEntityManager().use { em ->
    em.createQuery("SELECT pony from Pony pony WHERE p.name = :ponyName", Pony::class.java)
      .setParameter("ponyName", "Pinkie")
      .getResultList();
  }
}

JPQL

SELECT p
FROM Pony p
WHERE p.name = :ponyName
ORDER BY p.kind ASC

JPQL

SELECT NEW bzh.zomzog.Partial(p.name, p.kind)
FROM Pony p
WHERE p.name = :ponyName
ORDER BY p.kind ASC

EntiyManager with Spring

class Repository {
  @PersistenceUnit
  private lateinit var factory: EntityManagerFactory

  fun save(pony: Pony) = factory.createEntityManager().use { em ->
      em.transaction.begin()
      em.persist(pony)
      em.transaction.commit()
  }

  fun findAll(): List<Pony> = factory.createEntityManager().use { em ->
      em.createQuery("SELECT pony from Pony pony", Pony::class.java)
          .getResultList();
  }
}

EntiyManager with Spring

class Repository {

  @PersistenceContext
  private lateinit var entityManager: EntityManager

  @Transaction
  fun save(pony: Pony) = entityManager.persist(pony)

  fun findAll() = entityManager.createQuery("SELECT pony from Pony pony",
                                            Pony::class.java)
      .resultList
}

Named Query

@Entity
@NamedQuery(name = "Pony.findAll", query = "SELECT p FROM Pony p")
class Pony(
    @Id @GeneratedValue
    val id: Long?,
    val name: String,
    val kind: String,
)
fun findAll() = entityManager.createNamedQuery("Pony.findAll",
                                                Pony::class.java)
    .resultList

Spring JPA

interface JpaRepository<ENTITY, ID>
interface DemoRepository : JpaRepository<DemoEntity, UUID>

Jpa Repository

fun save(entity: T): T

fun findAll(): List<T>;

fun findById(id: ID): Optional<T>

fun deleteById(id: ID): Unit

fun deleteAll(): Unit

Jpa Repository Custom Query

interface DemoRepository : JpaRepository<DemoEntity, UUID> {

    fun findAllByName(name: String): List<DemoEntity>

    fun findByAgeAndNameOrKindOrderByIdDesc(age: Int,
                                            name: String,
                                            kind: String): List<DemoEntity>
}

Jpa Custom Query

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

Jpa Criteria

interface DemoRepositoryCustom {
    fun criteria(name: String?): List<DemoEntity>
}

interface DemoRepository :
        JpaRepository<DemoEntity, UUID>,
        DemoRepositoryCustom

Jpa Criteria

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

Entity

@Entity

class Pony(
    @Id @GeneratedValue
    val id: Long?,

    val name: String,
    val kind: String,
)

Entity

@Entity
@Table("PonyTable")
class Pony(
    @Id @GeneratedValue
    val id: Long?,

    val name: String,
    val kind: String,
)

Entity

@Entity
@Table("PonyTable")
class Pony(
    @Id @GeneratedValue
    val id: Long?,
    @Column(name = "n", nullable = false, unique = false)
    val name: String,
    val kind: String,
)

Jointures

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

Cascades

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

Direction

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.

Join-Column

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

One-To-One uni-directionnel

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

One-To-One

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

One-To-One

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

One-To-Many uni-directionnel

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

Many-To-One

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

Many-To-Many

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

Test Jpa

Spring propose des tests de "Layer".

Ces tests ne lancent qu’une partie de l’application.

Pour JPA, il faut remplacer @SpringBootTest par @DataJpaTest.

Test Jpa

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

Cours 3

Filter

Filtre un point d’entrée de l’application

Filter HTTP

Schema simplifié d’une requète (cours 1)
Schema simplifié d’une requète (cours 1)

Filter HTTP

Ajout d’étapes avec la pattern Intercepting filter
Ajout d’étapes avec la pattern Intercepting filter

Filter

class FilterA : jakarta.servlet.Filter {








}

Filter

class FilterA : Filter {

  override fun doFilter(request: ServletRequest,
                        response: ServletResponse,
                        chain: FilterChain) {



  }
}

Filter

class FilterA : Filter {

  override fun doFilter(request: ServletRequest,
                        response: ServletResponse,
                        chain: FilterChain) {

      chain.doFilter(request, response)

  }
}

Filter

class FilterA : Filter {

  override fun doFilter(request: ServletRequest,
                        response: ServletResponse,
                        chain: FilterChain) {
      // do before on request
      chain.doFilter(request, response)
      // do after on response
  }
}

Ajouter le filtre

@Bean
fun filterA(filter: FilterA): FilterRegistrationBean<FilterA> {
  val registrationBean = FilterRegistrationBean(filter)
  registrationBean.addUrlPatterns("/api/*")
  registrationBean.order = 1
  return registrationBean
}

HttpFilter

class LoggerHttpFilter : HttpFilter() {
  private val logger = KotlinLogging.logger {}
  override fun doFilter(request: HttpServletRequest,
                        response: HttpServletResponse,
                        chain: FilterChain) {
    logger.debug { "Request: ${request.method} ${request.requestURI}" }
    chain.doFilter(request, response)
    logger.debug { "Response: ${response.status}" }
  }
}

Spring Security

Vocabulaire

Authentication

Qui suis-je?

Diagram

Authorization

Que puis-je faire?

Toujours après l’authentication

Read ? Write ? Admin ? Publish?

Role

Ensemble de droits sur l’application

User

Information de base sur l’utilisateur connécté

login, role…​

UserDetail

API Spring pour faire la phase d’authentification

Dépendances

implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")

Attention

Ajouter cette dépandance active directement la sécurité

De base toute requete doit etre authentifié, donc tout répond un 401.

Enable Web Security

@Configuration
@EnableWebSecurity
class MySecurityConfig {

Security Filter

import org.springframework.security.config.annotation.web.invoke

@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  http {
    csrf { disable() }
    authorizeHttpRequests {
      authorize("/ponies", permitAll)
      authorize(anyRequest, authenticated)
    }
    httpBasic { }
    formLogin { }
  }
  return http.build()
}

Attention

Bien ajouter cet import qui ne s’ajoute pas toujours automatiquement

import org.springframework.security.config.annotation.web.invoke

Form Login

formLogin { }

login

/login && /logout

Http Basic

httpBasic { }

BASE=$(echo -ne "login:password" | base64 --wrap 0)
curl \
 -H "Authorization: Basic $BASE" \
 http://localhost:8080

Autres

Cross Site Request Forgery

csrf { disable() }

Cross-Origin Resource Sharing

cors { disable() }

authorizeHttpRequests

  http {
    authorizeHttpRequests {
      authorize("/ponies", permitAll)
      authorize("/admin", hasRole("ADMIN"))
      authorize(anyRequest, authenticated)
    }
  }
}
fun authorize(pattern: String,
              access: AuthorizationManager<RequestAuthorizationContext>)

Alternative pour les droits

@Configuration
@EnableMethodSecurity
@PreAuthorize("hasRole('ADMIN')")
fun myMethod() ...

authentification

@Bean
fun passwordEncoder(): PasswordEncoder {
    return BCryptPasswordEncoder()
}

In Memory User Detail Manager

@Bean
fun userDetailService(passwordEncoder: PasswordEncoder): UserDetailsManager {
    val admin = User.withUsername("admin")
        .password(passwordEncoder.encode("1234"))
        .roles("ADMIN")
        .build()
    val demo = User.withUsername("login")
        .password(passwordEncoder.encode("password"))
        .roles("ADMIN")
        .build()
    return InMemoryUserDetailsManager(admin, demo)
}

Jdbc User Detail

@Bean
fun userDetailService(dataSource: DataSource,
                      passwordEncoder: PasswordEncoder): UserDetailsManager {
  val user1 = User.withUsername("u1")
      .password(passwordEncoder.encode("pw"))
      .roles("USER")
      .build()
  return JdbcUserDetailsManager(dataSource).apply {
      createUser(user1)
  }

Jdbc User Detail

CREATE TABLE USERS (
  username VARCHAR(50) NOT NULL PRIMARY KEY,
  password VARCHAR(500) NOT NULL,
  enabled BOOLEAN NOT NULL
);

CREATE TABLE AUTHORITIES (
  username VARCHAR(50) NOT NULL,
  authority VARCHAR(50) NOT NULL,
  CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users (username)
);

CREATE UNIQUE INDEX ix_auth_username ON AUTHORITIES (username, authority);

Récupération du User

Par "injection", on demande le Principal à Spring

@GetMapping
fun admin(principal: Principal): ResponseEntity<String> {
  println("Login: ${principal.name}")
}

Récupération du User

Pour du MVC, sur le Thread, par appel au SecurityContextHolder

SecurityContextHolder.getContext().authentication.principal.let {
  println("Login: ${principal.name}")
}

TEST !

@WebMvcTest
@Import(MySecurityFilterConfig::class)
class HelloControllerTest {

    @Autowired
    lateinit var mockMvc: MockMvc

    @Test
    fun `happy path`() {
        mockMvc.get("/openEndpoint")
            .andExpect {
                status { isIAmATeapot() }
            }
    }
}

WithAnonymousUser

@WithAnonymousUser
@Test
fun `admin without auth`() {
    mockMvc.get("/admin")
        .andExpect {
            status { isUnauthorized() }
        }
}

WithMockUser

@WithMockUser
@Test
fun `admin without admin`() {
    mockMvc.get("/admin")
        .andExpect {
            status { isForbidden() }
        }
}

WithMockUser

@WithMockUser(roles =[ "ADMIN"])
@Test
fun `admin with admin`() {
    mockMvc.get("/admin")
        .andExpect {
            status { isOk() }
        }
}

Logs

Niveaux de log

  • ERROR

  • WARN

  • INFO

  • DEBUG

  • TRACE

logback.

Système de gestion des journaux d’évenements (logs)

Il gère la destination et le niveau de log.

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <Pattern>
        %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
      </Pattern>
    </layout>
  </appender>
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>
  <logger name="org.springframework.web" level="DEBUG"/>
</configuration>

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="CONSOLE"...</appender>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>/tmp/tests.log</file>
    <append>true</append>
    <encoder>
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
    </encoder>
  </appender>
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
  </root>
</configuration>

logback.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" />
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">...</appender>
  <logger name="iut.nantes" level="debug" additivity="false">
      <appender-ref ref="FILE"/>
      <appender-ref ref="CONSOLE"/>
  </logger>
  <root level="INFO">
      <appender-ref ref="CONSOLE"/>
  </root>
</configuration>

Jansi

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

Logs dans Spring

De base dans Spring :

slf4j → logback

Simple Logging Facade for Java

SLF4J sert d’abstraction pour divers frameworks de journalisation (java.util.logging, logback, log4j…​) permettant à l’utilisateur final de brancher le framework de journalisation souhaité au moment du déploiement.

logger

private val logger = LoggerFactory.getLogger(javaClass)
logger.trace("trace of ${name}")
logger.warn("warning with exception", Exception())

Kotlin

Optionel :

oshai:kotlin-logging.

Lightweight Multiplatform logging framework for Kotlin

logger kotlin

private val logger = KotlinLogging.logger {}
logger.debug(Exception("Demo")) { "Protocol: ${request.protocol}" }

logback spring

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

logback-spring.xml

<configuration>
  <springProfile name="dev">
    <appender name="MY_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
        ....
    </appender>
  </springProfile>
  <springProfile name="default">
    <appender name="MY_APPENDER" class="ch.qos.logback.core.FileAppender">
        ....
    </appender>
  </springProfile>
  <root level="INFO">
    <appender-ref ref="MY_APPENDER"/>
  </root>
</configuration>

Alternative au logback.xml

application.yml

logging:
  level:
    org.springframework.web: DEBUG
    bzh.zomzog.prez: WARN

Actuators

Pour être "production ready" une application doit être en mesure de fournir :

  • un health-check,

  • des metrics,

  • des logs.

Actionneurs Spring

Ajouter la dépendance Spring fournit directement plusieurs endpoints sous /actuator.

endpointsdescription

/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

Configuration des actuators

management:
  endpoints:
    web:
      exposure:
        include: info, health, prometheus
'*' expose tous les enpoints.

Configuration de health

management:
  endpoint:
    health:
      show-details: always

Configuration de health

{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "H2",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 252841029632,
        "free": 17691353088,
        "threshold": 10485760,
        "path": "C:\\git\\zomzog\\iut\\.",
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    }
  }
}

Customisation des actuators

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

Metrics

micrometer-registry-prometheus

/actuator/prometheus

http_server_requests_seconds_count

{method="GET",outcome="SUCCESS",status="200",uri="/actuator/health"}

117992

Custom metrics

@Component
class MetricsConfig(meterRegistry: MeterRegistry) {
    private val myCount = meterRegistry.counter("name.my.count",
                                                "aDimension", "theValue")

    override fun theIncrement() {
        myCount.increment()
    }

OpenAPI Specification 3

oas3

OAS3 ?

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.

Info

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

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

Path

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

Path

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

Path

      parameters:
        - name: petId
          in: path
          description: ID of pet to return
          required: true
          schema:
            type: integer
            format: int64

Path

      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

Path

      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

components:
  schemas:
    Pet:
      required:
        - name
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 10
        name:
          type: string
          example: doggie

Visualisation

Swagger UI

swagger ui

Génération VS Génération

codecontract

Génération depuis le code

springdoc-openapi-starter-webmvc-ui

Avec juste l’ajout de la dépendance on a :

Personnalisation

@Operation(summary = "List users")
@ApiResponses(value = [
    ApiResponse(responseCode = "200", description = "List users",
            content = [Content(mediaType = "application/json",
                    array = ArraySchema(
                            schema = Schema(implementation = UserDTO::class)))])])

Personnalisation

@Parameter(description = "id of book to be searched")
@PathVariable id: UUID