Decathlon Digital Nantes
~100 personnes
Lead tech Backend
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…) |
Historique des noms
J2EE | 1999 - 2006 |
Java EE | 2006 - 2019 |
Jakarta EE | 2019 - |
Ensemble de spécifications pour faire des applications pour les entreprises
Les versions sont retro-compatible
Par exemple la communication avec une base de données
Par exemple une transaction SQL
Par exemple la communication avec ActiveMq
Composant logiciel pouvant être appelé par le serveur
Classe Java de génération de contenu dynamique
Non limité au HTTP (JDBC…)
Génération de contenu statique (html…)
EL - Expressions Languages
Application "riche" ie communication avec le serveur (validation…)
JAX.WS - Java Api for XML Web Service
JAX.RS - Java API for RESTful Web Service
Communication http moderne
Implementation des spécifications JavaEE
Glassfish
WebLogic
WebSphere
JBoss
…
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
Serveurs léger qui ne font "que" les parties servlet et jsp
Exemple de serveurs développés en Java
Tomcat
Jetty
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 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
SpringBoot
Spring
Spring Boot
class AService() {
val db = PostgresDb()
fun findAll() = db.findAllInDb()
}
class AService() {
val pg = PostgresDb()
val my = MySqlDb()
fun findAll(pg: Boolean) = if(pg) {
db.findAllInDb()
} else {
my.findAllInDb()
}
}
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()
}
val appPg = AService(PostgresDb())
val appMy = AService(MySqlDb())
@Configuration
class MyConfig {
}
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
}
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService() = AService(myDb())
}
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService() = AService(myDb())
@Bean
fun another() = Other(myDb())
}
fun main() {
val context: ApplicationContext =
AnnotationConfigApplicationContext(MyConfig::class.java)
}
fun main() {
val context: ApplicationContext =
AnnotationConfigApplicationContext(MyConfig::class.java)
val service = context.getBean(AService::class.java)
}
fun main() {
val context: ApplicationContext =
AnnotationConfigApplicationContext(MyConfig::class.java)
val service = context.getBean(AService::class.java)
service.findAllInDb()
}
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService() = AService(myDb())
@Bean
fun another() = Other(myDb())
}
aService.dbAccess == another.dbAccess
@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
@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
Singleton → un unique bean
Prototype → un bean par instance d’objet
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
@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)
...
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService() = AService(myDb())
}
class AService (
val database: DBAccess
)
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService() = AService()
}
class AService {
lateinit var database: DbAccess
}
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
@Bean
fun aService() = AService()
}
class AService {
@Autowired
lateinit var database: DbAccess
}
@Configuration
@ComponentScan("bzh.zomzog.iut.poc")
class MyConfig {
@Bean
fun myDb() = PostgresDb()
//@Bean
//fun aService() = AService()
}
@Service
class AService {
@Autowired
lateinit var database: DBAccess
}
@Component → déclare que la classe doit devenir un bean lors du scan
3 alias:
@Controller
@Service
@Repository
@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
@Service
class AService {
@Autowired
lateinit var database: DBAccess
}
@Service
class AService(database: DBAccess) {
}
@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)
}
@Configuration
class MyConfig {
@Bean
fun myDb(aDriverFromALib: JdbcDriver) = GenericDb(aDriverFromALib)
@Bean
fun aService() = AService(myDb())
@Bean
fun another() = Other(myDb())
}
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
Par défaut, un bean a le nom de la méthode qui le crée
On peut le forcer @Bean("monNom")
@Primary sur un bean → en cas de conflit, c’est lui qui est choisi
@Autowired @Qualifier("monNom") spécifie le bean attendu
@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()
}
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
}
@SpringBootApplication
class PocApplication
fun main(args: Array<String>) {
runApplication<PocApplication>(*args)
@SpringBootApplication contient @ComponentScan
Il ne scan QUE son package et ses sous package |
Lancer le main:
lance un tomcat qui écoute sur le port 8080
lance les auto-configurations et scans de package
Requête
curl -XGET localhost:8080/hello
Réponse:
Hello World
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
RestController → Controller → Component
@RestController
class HelloController {
}
@RestController
class HelloController {
@GetMapping("/hello")
fun hello() = "Hello World"
}
GetMapping
PostMapping
PutMapping
DeleteMapping
alias →
RequestMapping
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))
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")
@Configuration
class MyConfig {
@Bean
fun myDb() = PostgresDb()
}
@RestController
class HelloController {
@GetMapping("/hello")
fun hello() = "Hello World"
}
spring-boot-starter-validation
Permet d’ajouter de la validation sur les paramètres des méthodes.
//
class DemoController(val demoRepository: DemoRepository) {
fun list(
i: Int // >= 10
) = ...
@Validated
class DemoController(val demoRepository: DemoRepository) {
fun list(
i: Int // >= 10
) = ...
@Validated
class DemoController(val demoRepository: DemoRepository) {
fun list(
@Min(10) i: Int
) = ...
@Validated
class DemoController(val demoRepository: DemoRepository) {
fun list(
@Max(5) @Min(10) i: Int
) = ...
@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();
}
@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>> = [],
)
@Validated
class DemoController(val demoRepository: DemoRepository) {
fun list(
@MinMax i: Int
) = ...
@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>> = [],
)
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
}
}
@Validated
class DemoController(val demoRepository: DemoRepository) {
fun list(
@MinMax(0, 10) i: Int
) = ...
@RestController
@Validated
class DemoController(val demoRepository: DemoRepository) {
@GetMapping
fun list(@RequestParam(required = false) @Size(min=2, max=20) name: String?)
= if (name == null) ...
@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, )
@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,
)
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
)
Les erreurs de validation n’entrent pas dans le code de la fonction.
La gestion est externe et générique.
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
class HttpErrorHandler {
}
@ControllerAdvice
class HttpErrorHandler {
fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException) =
ResponseEntity.badRequest().body("You're arg is invalid")
}
@ControllerAdvice
class HttpErrorHandler {
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException) =
ResponseEntity.badRequest().body("You're arg is invalid")
}
@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
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")
}
}
@AutoConfigureMockMvc
@SpringBootTest
class MovieControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
fun post() {
mockMvc.post("/api/demo") // mockMvc.perform(post("/api/movies"))
}
fun post() {
mockMvc.post("/api/demo") {
contentType = MediaType.APPLICATION_JSON
content = ObjectMapper()
.writeValueAsString(DemoEntity(name = "name"))
}
}
fun post() {
mockMvc.post("/api/demo") {
contentType = MediaType.APPLICATION_JSON
content = ObjectMapper()
.writeValueAsString(DemoEntity(name = "name"))
}
.andExpect {
status { isOk() }
}
}
fun post() {
mockMvc.post("/api/demo") {
contentType = MediaType.APPLICATION_JSON
content = ObjectMapper()
.writeValueAsString(DemoEntity(name = "name"))
}
.andExpect {
status { isOk() }
content { jsonPath("$.name", `is`("name")) }
}
}
fun get() {
mockMvc.get("/api/demo/{id}?param=value", "theId") {
headers {
contentLanguage = Locale.FRANCE
}
param("name", "value")
}
.andDo {
print()
}
.andExpect {
status { isOk() }
}
}
@SpringBootTest
@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(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() } }
}
}
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
Les profils son un moyen de séparer des parties de la configuration
Exemple:
un profile MySql
un profile MongoDb
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
-Dspring.profiles.active=prod,mongo
-Dspring.profiles.active=prod,mongo
class Demo {
@Value("\${custom.path.value}")
private lateinit var value: String
}
class Demo {
@Value("\${custom.path.value:defaultValue}")
private lateinit var value: String
}
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
EnableConfigurationProperties = Création de bean
@Service
class Demo(val properties: CustomProperties) {
}
@Configuration
class Demo {
@Bean
fun aBean(val properties: CustomProperties) =...
}
@ConfigurationProperties("path.custom-prop")
data class CustomProperties(
@NotBlank val demo: String,
@Min(10) val anotherDemo: Int,
)
@Configuration
class DatabaseConfig {
@ConditionalOnProperty("db.external",
havingValue = "true")
@Bean
fun mongo(): Database = UserMongoRepository()
}
@Configuration
class DatabaseConfig {
@ConditionalOnProperty("db.external",
havingValue = "true")
@Bean
fun mongo(): Database = UserMongoRepository()
@ConditionalOnProperty("db.external",
havingValue = "false")
@Bean
fun inMemory(): Database = UserInMemoryRepository()
}
@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()
}
@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
Spring Data JPA
jOOQ
MyBatis
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
Id | Name | Kind |
1 | Discord | Draconequus |
2 | Rainbow Dash | Pegasus |
3 | Pinkie Pie | Earth |
PonyId | Occupation |
1 | Honorary |
1 | Ruler of Equestria |
3 | Baker |
class Pony(
val id: Long?,
val name: String,
val kind: String,
val occupations: List<Occupation>,
)
class Occupation(
val name: String,
)
@Entity
class Pony(
@Id @GeneratedValue
val id: Long?,
val name: String,
val kind: String,
)
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()
}
}
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();
}
}
SELECT p
FROM Pony p
WHERE p.name = :ponyName
ORDER BY p.kind ASC
SELECT NEW bzh.zomzog.Partial(p.name, p.kind)
FROM Pony p
WHERE p.name = :ponyName
ORDER BY p.kind ASC
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();
}
}
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
}
@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
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>
fun findByAgeAndNameOrKindOrderByIdDesc(age: Int,
name: String,
kind: 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
}
}
@Entity
class Pony(
@Id @GeneratedValue
val id: Long?,
val name: String,
val kind: String,
)
@Entity
@Table("PonyTable")
class Pony(
@Id @GeneratedValue
val id: Long?,
val name: String,
val kind: String,
)
@Entity
@Table("PonyTable")
class Pony(
@Id @GeneratedValue
val id: Long?,
@Column(name = "n", nullable = false, unique = false)
val name: String,
val kind: String,
)
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,
)
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)
}
}
Filtre un point d’entrée de l’application
class FilterA : jakarta.servlet.Filter {
}
class FilterA : Filter {
override fun doFilter(request: ServletRequest,
response: ServletResponse,
chain: FilterChain) {
}
}
class FilterA : Filter {
override fun doFilter(request: ServletRequest,
response: ServletResponse,
chain: FilterChain) {
chain.doFilter(request, response)
}
}
class FilterA : Filter {
override fun doFilter(request: ServletRequest,
response: ServletResponse,
chain: FilterChain) {
// do before on request
chain.doFilter(request, response)
// do after on response
}
}
@Bean
fun filterA(filter: FilterA): FilterRegistrationBean<FilterA> {
val registrationBean = FilterRegistrationBean(filter)
registrationBean.addUrlPatterns("/api/*")
registrationBean.order = 1
return registrationBean
}
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}" }
}
}
Qui suis-je?
Que puis-je faire?
Toujours après l’authentication
Read ? Write ? Admin ? Publish?
Ensemble de droits sur l’application
Information de base sur l’utilisateur connécté
login, role…
API Spring pour faire la phase d’authentification
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")
Ajouter cette dépandance active directement la sécurité De base toute requete doit etre authentifié, donc tout répond un 401. |
@Configuration
@EnableWebSecurity
class MySecurityConfig {
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()
}
Bien ajouter cet import qui ne s’ajoute pas toujours automatiquement import org.springframework.security.config.annotation.web.invoke |
formLogin { }
/login
&& /logout
httpBasic { }
BASE=$(echo -ne "login:password" | base64 --wrap 0)
curl \
-H "Authorization: Basic $BASE" \
http://localhost:8080
Cross Site Request Forgery
csrf { disable() }
Cross-Origin Resource Sharing
cors { disable() }
http {
authorizeHttpRequests {
authorize("/ponies", permitAll)
authorize("/admin", hasRole("ADMIN"))
authorize(anyRequest, authenticated)
}
}
}
fun authorize(pattern: String,
access: AuthorizationManager<RequestAuthorizationContext>)
@Configuration
@EnableMethodSecurity
@PreAuthorize("hasRole('ADMIN')")
fun myMethod() ...
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
@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)
}
@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)
}
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);
Par "injection", on demande le Principal à Spring
@GetMapping
fun admin(principal: Principal): ResponseEntity<String> {
println("Login: ${principal.name}")
}
Pour du MVC, sur le Thread, par appel au SecurityContextHolder
SecurityContextHolder.getContext().authentication.principal.let {
println("Login: ${principal.name}")
}
@WebMvcTest
@Import(MySecurityFilterConfig::class)
class HelloControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `happy path`() {
mockMvc.get("/openEndpoint")
.andExpect {
status { isIAmATeapot() }
}
}
}
@WithAnonymousUser
@Test
fun `admin without auth`() {
mockMvc.get("/admin")
.andExpect {
status { isUnauthorized() }
}
}
@WithMockUser
@Test
fun `admin without admin`() {
mockMvc.get("/admin")
.andExpect {
status { isForbidden() }
}
}
@WithMockUser(roles =[ "ADMIN"])
@Test
fun `admin with admin`() {
mockMvc.get("/admin")
.andExpect {
status { isOk() }
}
}
ERROR
WARN
INFO
DEBUG
TRACE
Système de gestion des journaux d’évenements (logs)
Il gère la destination et le niveau de log.
<?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>
<?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>
<?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>
<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>
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.
private val logger = LoggerFactory.getLogger(javaClass)
logger.trace("trace of ${name}")
logger.warn("warning with exception", Exception())
Optionel :
oshai:kotlin-logging.
Lightweight Multiplatform logging framework for Kotlin
private val logger = KotlinLogging.logger {}
logger.debug(Exception("Demo")) { "Protocol: ${request.protocol}" }
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>
<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>
application.yml
logging:
level:
org.springframework.web: DEBUG
bzh.zomzog.prez: WARN
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, prometheus
'*' expose tous les enpoints. |
management:
endpoint:
health:
show-details: always
{
"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"
}
}
}
@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
http_server_requests_seconds_count
{method="GET",outcome="SUCCESS",status="200",uri="/actuator/health"}
117992
@Component
class MetricsConfig(meterRegistry: MeterRegistry) {
private val myCount = meterRegistry.counter("name.my.count",
"aDimension", "theValue")
override fun theIncrement() {
myCount.increment()
}
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