claudient 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +4 -2
- package/README.md +150 -103
- package/guides/agent-orchestration.md +1 -0
- package/guides/token-optimization.md +37 -0
- package/index.json +1801 -0
- package/package.json +5 -3
- package/scripts/build-index.js +139 -0
- package/scripts/cli.js +134 -1
- package/skills/backend/java/de/spring-boot.md +333 -0
- package/skills/backend/java/es/spring-boot.md +333 -0
- package/skills/backend/java/fr/spring-boot.md +333 -0
- package/skills/backend/java/nl/spring-boot.md +333 -0
- package/skills/backend/java/spring-boot.md +331 -0
- package/skills/productivity/caveman.md +70 -0
- package/skills/productivity/de/caveman.md +72 -0
- package/skills/productivity/es/caveman.md +72 -0
- package/skills/productivity/fr/caveman.md +72 -0
- package/skills/productivity/nl/caveman.md +72 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
> 🇪🇸 Versión en español. [Versión en inglés](../spring-boot.md).
|
|
2
|
+
|
|
3
|
+
# Skill Spring Boot
|
|
4
|
+
|
|
5
|
+
## Cuándo activar
|
|
6
|
+
- Construyendo una API REST o microservicio con Spring Boot
|
|
7
|
+
- Configurando Spring Data JPA con Hibernate y PostgreSQL/MySQL
|
|
8
|
+
- Configurando Spring Security (JWT, OAuth2, autenticación basada en sesión)
|
|
9
|
+
- Escribiendo pruebas para Spring Boot (unitarias con Mockito, de integración con TestRestTemplate o MockMvc)
|
|
10
|
+
- Configurando Spring Cloud (descubrimiento de servicios, servidor de configuración, API gateway)
|
|
11
|
+
- Implementando procesamiento asíncrono con `@Async` o Spring Events
|
|
12
|
+
- Configurando perfiles, propiedades y configuración externalizada
|
|
13
|
+
- Escribiendo starters personalizados de Spring Boot o auto-configuraciones
|
|
14
|
+
|
|
15
|
+
## Cuándo NO usar
|
|
16
|
+
- Proyectos con Quarkus o Micronaut — modelos de inyección de dependencias y configuración distintos
|
|
17
|
+
- Java simple sin Spring — la sobrecarga no está justificada para scripts simples
|
|
18
|
+
- Proyectos Android — ecosistema completamente diferente
|
|
19
|
+
- Jakarta EE/WildFly — modelo de servidor de aplicaciones distinto
|
|
20
|
+
|
|
21
|
+
## Instrucciones
|
|
22
|
+
|
|
23
|
+
### Estructura del proyecto
|
|
24
|
+
```
|
|
25
|
+
src/
|
|
26
|
+
├── main/
|
|
27
|
+
│ ├── java/com/example/app/
|
|
28
|
+
│ │ ├── AppApplication.java # @SpringBootApplication entry point
|
|
29
|
+
│ │ ├── config/ # @Configuration beans
|
|
30
|
+
│ │ ├── controller/ # @RestController — HTTP layer only
|
|
31
|
+
│ │ ├── service/ # Business logic — @Service
|
|
32
|
+
│ │ ├── repository/ # @Repository — data access
|
|
33
|
+
│ │ ├── domain/ # @Entity models
|
|
34
|
+
│ │ ├── dto/ # Request/response shapes (records)
|
|
35
|
+
│ │ ├── exception/ # @ControllerAdvice error handling
|
|
36
|
+
│ │ └── security/ # Security config and filters
|
|
37
|
+
│ └── resources/
|
|
38
|
+
│ ├── application.yml # Base config
|
|
39
|
+
│ ├── application-dev.yml # Dev overrides
|
|
40
|
+
│ └── application-prod.yml # Prod overrides
|
|
41
|
+
└── test/
|
|
42
|
+
└── java/com/example/app/
|
|
43
|
+
├── controller/ # MockMvc / @WebMvcTest
|
|
44
|
+
├── service/ # Unit tests with Mockito
|
|
45
|
+
└── integration/ # @SpringBootTest full context
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Punto de entrada de la aplicación
|
|
49
|
+
```java
|
|
50
|
+
@SpringBootApplication
|
|
51
|
+
public class AppApplication {
|
|
52
|
+
public static void main(String[] args) {
|
|
53
|
+
SpringApplication.run(AppApplication.class, args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Estructura de application.yml
|
|
59
|
+
```yaml
|
|
60
|
+
spring:
|
|
61
|
+
application:
|
|
62
|
+
name: my-service
|
|
63
|
+
datasource:
|
|
64
|
+
url: ${DATABASE_URL}
|
|
65
|
+
username: ${DB_USER}
|
|
66
|
+
password: ${DB_PASSWORD}
|
|
67
|
+
jpa:
|
|
68
|
+
hibernate:
|
|
69
|
+
ddl-auto: validate # Never 'create' or 'update' in production
|
|
70
|
+
show-sql: false
|
|
71
|
+
properties:
|
|
72
|
+
hibernate:
|
|
73
|
+
format_sql: true
|
|
74
|
+
|
|
75
|
+
server:
|
|
76
|
+
port: ${PORT:8080}
|
|
77
|
+
|
|
78
|
+
# Custom properties — always use @ConfigurationProperties, never @Value for groups
|
|
79
|
+
app:
|
|
80
|
+
jwt:
|
|
81
|
+
secret: ${JWT_SECRET}
|
|
82
|
+
expiration-ms: 86400000
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Entidad y Repository
|
|
86
|
+
```java
|
|
87
|
+
// domain/User.java
|
|
88
|
+
@Entity
|
|
89
|
+
@Table(name = "users")
|
|
90
|
+
public class User {
|
|
91
|
+
@Id
|
|
92
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
93
|
+
private Long id;
|
|
94
|
+
|
|
95
|
+
@Column(nullable = false, unique = true, length = 320)
|
|
96
|
+
private String email;
|
|
97
|
+
|
|
98
|
+
@Column(nullable = false)
|
|
99
|
+
private String passwordHash;
|
|
100
|
+
|
|
101
|
+
@CreationTimestamp
|
|
102
|
+
@Column(updatable = false)
|
|
103
|
+
private Instant createdAt;
|
|
104
|
+
|
|
105
|
+
// No-arg constructor required by JPA
|
|
106
|
+
protected User() {}
|
|
107
|
+
|
|
108
|
+
public User(String email, String passwordHash) {
|
|
109
|
+
this.email = email;
|
|
110
|
+
this.passwordHash = passwordHash;
|
|
111
|
+
}
|
|
112
|
+
// getters only — no setters on entities
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// repository/UserRepository.java
|
|
116
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
117
|
+
Optional<User> findByEmail(String email);
|
|
118
|
+
boolean existsByEmail(String email);
|
|
119
|
+
|
|
120
|
+
// JPQL for complex queries
|
|
121
|
+
@Query("SELECT u FROM User u WHERE u.createdAt > :since")
|
|
122
|
+
List<User> findRecentUsers(@Param("since") Instant since);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Capa de servicio
|
|
127
|
+
```java
|
|
128
|
+
@Service
|
|
129
|
+
@Transactional(readOnly = true) // Default read-only; override for writes
|
|
130
|
+
public class UserService {
|
|
131
|
+
private final UserRepository userRepo;
|
|
132
|
+
private final PasswordEncoder passwordEncoder;
|
|
133
|
+
|
|
134
|
+
public UserService(UserRepository userRepo, PasswordEncoder passwordEncoder) {
|
|
135
|
+
this.userRepo = userRepo;
|
|
136
|
+
this.passwordEncoder = passwordEncoder;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@Transactional // Override: this method writes
|
|
140
|
+
public UserDto createUser(CreateUserRequest request) {
|
|
141
|
+
if (userRepo.existsByEmail(request.email())) {
|
|
142
|
+
throw new ConflictException("Email already in use");
|
|
143
|
+
}
|
|
144
|
+
User user = new User(request.email(), passwordEncoder.encode(request.password()));
|
|
145
|
+
return UserDto.from(userRepo.save(user));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public UserDto getById(Long id) {
|
|
149
|
+
return userRepo.findById(id)
|
|
150
|
+
.map(UserDto::from)
|
|
151
|
+
.orElseThrow(() -> new NotFoundException("User " + id + " not found"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Capa de controlador
|
|
157
|
+
```java
|
|
158
|
+
@RestController
|
|
159
|
+
@RequestMapping("/api/v1/users")
|
|
160
|
+
@Validated
|
|
161
|
+
public class UserController {
|
|
162
|
+
private final UserService userService;
|
|
163
|
+
|
|
164
|
+
public UserController(UserService userService) {
|
|
165
|
+
this.userService = userService;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@PostMapping
|
|
169
|
+
@ResponseStatus(HttpStatus.CREATED)
|
|
170
|
+
public UserDto createUser(@RequestBody @Valid CreateUserRequest request) {
|
|
171
|
+
return userService.createUser(request);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@GetMapping("/{id}")
|
|
175
|
+
public UserDto getUser(@PathVariable Long id) {
|
|
176
|
+
return userService.getById(id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@GetMapping
|
|
180
|
+
public Page<UserDto> listUsers(Pageable pageable) {
|
|
181
|
+
return userService.findAll(pageable);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### DTOs con records
|
|
187
|
+
```java
|
|
188
|
+
// dto/CreateUserRequest.java
|
|
189
|
+
public record CreateUserRequest(
|
|
190
|
+
@NotBlank @Email String email,
|
|
191
|
+
@NotBlank @Size(min = 8) String password
|
|
192
|
+
) {}
|
|
193
|
+
|
|
194
|
+
// dto/UserDto.java
|
|
195
|
+
public record UserDto(Long id, String email, Instant createdAt) {
|
|
196
|
+
public static UserDto from(User user) {
|
|
197
|
+
return new UserDto(user.getId(), user.getEmail(), user.getCreatedAt());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Manejador global de excepciones
|
|
203
|
+
```java
|
|
204
|
+
@RestControllerAdvice
|
|
205
|
+
public class GlobalExceptionHandler {
|
|
206
|
+
|
|
207
|
+
@ExceptionHandler(NotFoundException.class)
|
|
208
|
+
@ResponseStatus(HttpStatus.NOT_FOUND)
|
|
209
|
+
public ProblemDetail handleNotFound(NotFoundException ex) {
|
|
210
|
+
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@ExceptionHandler(ConflictException.class)
|
|
214
|
+
@ResponseStatus(HttpStatus.CONFLICT)
|
|
215
|
+
public ProblemDetail handleConflict(ConflictException ex) {
|
|
216
|
+
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
220
|
+
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
221
|
+
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
|
|
222
|
+
var detail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
|
|
223
|
+
detail.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
|
|
224
|
+
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
225
|
+
.toList());
|
|
226
|
+
return detail;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Spring Security — JWT
|
|
232
|
+
```java
|
|
233
|
+
@Configuration
|
|
234
|
+
@EnableWebSecurity
|
|
235
|
+
public class SecurityConfig {
|
|
236
|
+
|
|
237
|
+
@Bean
|
|
238
|
+
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
|
|
239
|
+
return http
|
|
240
|
+
.csrf(AbstractHttpConfigurer::disable)
|
|
241
|
+
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
242
|
+
.authorizeHttpRequests(auth -> auth
|
|
243
|
+
.requestMatchers("/api/v1/auth/**", "/actuator/health").permitAll()
|
|
244
|
+
.anyRequest().authenticated()
|
|
245
|
+
)
|
|
246
|
+
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
|
|
247
|
+
.build();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Bean
|
|
251
|
+
public PasswordEncoder passwordEncoder() {
|
|
252
|
+
return new BCryptPasswordEncoder();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Pruebas
|
|
258
|
+
```java
|
|
259
|
+
// @WebMvcTest — prueba de slice del controlador (sin contexto completo)
|
|
260
|
+
@WebMvcTest(UserController.class)
|
|
261
|
+
class UserControllerTest {
|
|
262
|
+
@Autowired MockMvc mockMvc;
|
|
263
|
+
@MockitoBean UserService userService; // Spring Boot 3.4+: @MockitoBean
|
|
264
|
+
|
|
265
|
+
@Test
|
|
266
|
+
void createUser_returnsCreated() throws Exception {
|
|
267
|
+
given(userService.createUser(any())).willReturn(new UserDto(1L, "a@b.com", Instant.now()));
|
|
268
|
+
|
|
269
|
+
mockMvc.perform(post("/api/v1/users")
|
|
270
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
271
|
+
.content("""{"email":"a@b.com","password":"password123"}"""))
|
|
272
|
+
.andExpect(status().isCreated())
|
|
273
|
+
.andExpect(jsonPath("$.email").value("a@b.com"));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// @SpringBootTest — prueba de integración completa
|
|
278
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
279
|
+
@Transactional
|
|
280
|
+
class UserServiceIntegrationTest {
|
|
281
|
+
@Autowired UserService userService;
|
|
282
|
+
|
|
283
|
+
@Test
|
|
284
|
+
void createUser_persistsToDatabase() {
|
|
285
|
+
var user = userService.createUser(new CreateUserRequest("a@b.com", "password123"));
|
|
286
|
+
assertThat(user.email()).isEqualTo("a@b.com");
|
|
287
|
+
assertThat(user.id()).isNotNull();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Patrones de Spring Cloud
|
|
293
|
+
```yaml
|
|
294
|
+
# Service discovery with Eureka
|
|
295
|
+
spring:
|
|
296
|
+
application:
|
|
297
|
+
name: user-service
|
|
298
|
+
eureka:
|
|
299
|
+
client:
|
|
300
|
+
service-url:
|
|
301
|
+
defaultZone: http://eureka-server:8761/eureka/
|
|
302
|
+
|
|
303
|
+
# Config server client
|
|
304
|
+
spring:
|
|
305
|
+
config:
|
|
306
|
+
import: configserver:http://config-server:8888
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```java
|
|
310
|
+
// Feign client for inter-service calls
|
|
311
|
+
@FeignClient(name = "order-service")
|
|
312
|
+
public interface OrderClient {
|
|
313
|
+
@GetMapping("/api/v1/orders/user/{userId}")
|
|
314
|
+
List<OrderDto> getOrdersByUser(@PathVariable Long userId);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Ejemplo
|
|
319
|
+
|
|
320
|
+
**Usuario:** Construye una API REST con Spring Boot para un catálogo de productos con endpoints CRUD, PostgreSQL vía JPA, validación de beans, manejo global de errores y un `@WebMvcTest` para el endpoint GET.
|
|
321
|
+
|
|
322
|
+
**Resultado esperado:**
|
|
323
|
+
- Entidad `Product` — `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
|
|
324
|
+
- Record `ProductDto` + record `CreateProductRequest` con validaciones `@NotBlank` / `@Positive`
|
|
325
|
+
- `ProductRepository extends JpaRepository<Product, Long>`
|
|
326
|
+
- `ProductService` — clase con `@Transactional(readOnly = true)` a nivel de clase, `@Transactional` en escrituras
|
|
327
|
+
- `ProductController` — CRUD en `/api/v1/products`, `Pageable` en el listado GET
|
|
328
|
+
- `GlobalExceptionHandler` — `NotFoundException` → 404, `MethodArgumentNotValidException` → 400 con errores de campo
|
|
329
|
+
- `ProductControllerTest` usando `@WebMvcTest` + `@MockitoBean ProductService`
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
> **Trabaja con nosotros:** Claudient está respaldado por [Uitbreiden](https://uitbreiden.com/) — construimos productos de IA y soluciones B2B con comunidades de desarrolladores. [uitbreiden.com](https://uitbreiden.com/)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
> 🇫🇷 Version française. [English version](../spring-boot.md).
|
|
2
|
+
|
|
3
|
+
# Skill Spring Boot
|
|
4
|
+
|
|
5
|
+
## Quand activer
|
|
6
|
+
- Construire une API REST Spring Boot ou un microservice
|
|
7
|
+
- Configurer Spring Data JPA avec Hibernate et PostgreSQL/MySQL
|
|
8
|
+
- Configurer Spring Security (JWT, OAuth2, authentification par session)
|
|
9
|
+
- Écrire des tests Spring Boot (unitaires avec Mockito, d'intégration avec TestRestTemplate ou MockMvc)
|
|
10
|
+
- Mettre en place Spring Cloud (découverte de services, serveur de configuration, API gateway)
|
|
11
|
+
- Implémenter le traitement asynchrone avec `@Async` ou les événements Spring
|
|
12
|
+
- Configurer les profils, les propriétés et la configuration externalisée
|
|
13
|
+
- Écrire des starters Spring Boot personnalisés ou des auto-configurations
|
|
14
|
+
|
|
15
|
+
## Quand NE PAS utiliser
|
|
16
|
+
- Projets Quarkus ou Micronaut — modèles d'injection de dépendances et de configuration différents
|
|
17
|
+
- Java sans Spring — la surcharge n'est pas justifiée pour des scripts simples
|
|
18
|
+
- Projets Android — écosystème entièrement différent
|
|
19
|
+
- Jakarta EE/WildFly — modèle de serveur d'applications différent
|
|
20
|
+
|
|
21
|
+
## Instructions
|
|
22
|
+
|
|
23
|
+
### Structure du projet
|
|
24
|
+
```
|
|
25
|
+
src/
|
|
26
|
+
├── main/
|
|
27
|
+
│ ├── java/com/example/app/
|
|
28
|
+
│ │ ├── AppApplication.java # @SpringBootApplication entry point
|
|
29
|
+
│ │ ├── config/ # @Configuration beans
|
|
30
|
+
│ │ ├── controller/ # @RestController — HTTP layer only
|
|
31
|
+
│ │ ├── service/ # Business logic — @Service
|
|
32
|
+
│ │ ├── repository/ # @Repository — data access
|
|
33
|
+
│ │ ├── domain/ # @Entity models
|
|
34
|
+
│ │ ├── dto/ # Request/response shapes (records)
|
|
35
|
+
│ │ ├── exception/ # @ControllerAdvice error handling
|
|
36
|
+
│ │ └── security/ # Security config and filters
|
|
37
|
+
│ └── resources/
|
|
38
|
+
│ ├── application.yml # Base config
|
|
39
|
+
│ ├── application-dev.yml # Dev overrides
|
|
40
|
+
│ └── application-prod.yml # Prod overrides
|
|
41
|
+
└── test/
|
|
42
|
+
└── java/com/example/app/
|
|
43
|
+
├── controller/ # MockMvc / @WebMvcTest
|
|
44
|
+
├── service/ # Unit tests with Mockito
|
|
45
|
+
└── integration/ # @SpringBootTest full context
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Point d'entrée de l'application
|
|
49
|
+
```java
|
|
50
|
+
@SpringBootApplication
|
|
51
|
+
public class AppApplication {
|
|
52
|
+
public static void main(String[] args) {
|
|
53
|
+
SpringApplication.run(AppApplication.class, args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Structure de application.yml
|
|
59
|
+
```yaml
|
|
60
|
+
spring:
|
|
61
|
+
application:
|
|
62
|
+
name: my-service
|
|
63
|
+
datasource:
|
|
64
|
+
url: ${DATABASE_URL}
|
|
65
|
+
username: ${DB_USER}
|
|
66
|
+
password: ${DB_PASSWORD}
|
|
67
|
+
jpa:
|
|
68
|
+
hibernate:
|
|
69
|
+
ddl-auto: validate # Never 'create' or 'update' in production
|
|
70
|
+
show-sql: false
|
|
71
|
+
properties:
|
|
72
|
+
hibernate:
|
|
73
|
+
format_sql: true
|
|
74
|
+
|
|
75
|
+
server:
|
|
76
|
+
port: ${PORT:8080}
|
|
77
|
+
|
|
78
|
+
# Custom properties — always use @ConfigurationProperties, never @Value for groups
|
|
79
|
+
app:
|
|
80
|
+
jwt:
|
|
81
|
+
secret: ${JWT_SECRET}
|
|
82
|
+
expiration-ms: 86400000
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Entity et Repository
|
|
86
|
+
```java
|
|
87
|
+
// domain/User.java
|
|
88
|
+
@Entity
|
|
89
|
+
@Table(name = "users")
|
|
90
|
+
public class User {
|
|
91
|
+
@Id
|
|
92
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
93
|
+
private Long id;
|
|
94
|
+
|
|
95
|
+
@Column(nullable = false, unique = true, length = 320)
|
|
96
|
+
private String email;
|
|
97
|
+
|
|
98
|
+
@Column(nullable = false)
|
|
99
|
+
private String passwordHash;
|
|
100
|
+
|
|
101
|
+
@CreationTimestamp
|
|
102
|
+
@Column(updatable = false)
|
|
103
|
+
private Instant createdAt;
|
|
104
|
+
|
|
105
|
+
// No-arg constructor required by JPA
|
|
106
|
+
protected User() {}
|
|
107
|
+
|
|
108
|
+
public User(String email, String passwordHash) {
|
|
109
|
+
this.email = email;
|
|
110
|
+
this.passwordHash = passwordHash;
|
|
111
|
+
}
|
|
112
|
+
// getters only — no setters on entities
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// repository/UserRepository.java
|
|
116
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
117
|
+
Optional<User> findByEmail(String email);
|
|
118
|
+
boolean existsByEmail(String email);
|
|
119
|
+
|
|
120
|
+
// JPQL for complex queries
|
|
121
|
+
@Query("SELECT u FROM User u WHERE u.createdAt > :since")
|
|
122
|
+
List<User> findRecentUsers(@Param("since") Instant since);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Couche service
|
|
127
|
+
```java
|
|
128
|
+
@Service
|
|
129
|
+
@Transactional(readOnly = true) // Default read-only; override for writes
|
|
130
|
+
public class UserService {
|
|
131
|
+
private final UserRepository userRepo;
|
|
132
|
+
private final PasswordEncoder passwordEncoder;
|
|
133
|
+
|
|
134
|
+
public UserService(UserRepository userRepo, PasswordEncoder passwordEncoder) {
|
|
135
|
+
this.userRepo = userRepo;
|
|
136
|
+
this.passwordEncoder = passwordEncoder;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@Transactional // Override: this method writes
|
|
140
|
+
public UserDto createUser(CreateUserRequest request) {
|
|
141
|
+
if (userRepo.existsByEmail(request.email())) {
|
|
142
|
+
throw new ConflictException("Email already in use");
|
|
143
|
+
}
|
|
144
|
+
User user = new User(request.email(), passwordEncoder.encode(request.password()));
|
|
145
|
+
return UserDto.from(userRepo.save(user));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public UserDto getById(Long id) {
|
|
149
|
+
return userRepo.findById(id)
|
|
150
|
+
.map(UserDto::from)
|
|
151
|
+
.orElseThrow(() -> new NotFoundException("User " + id + " not found"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Couche contrôleur
|
|
157
|
+
```java
|
|
158
|
+
@RestController
|
|
159
|
+
@RequestMapping("/api/v1/users")
|
|
160
|
+
@Validated
|
|
161
|
+
public class UserController {
|
|
162
|
+
private final UserService userService;
|
|
163
|
+
|
|
164
|
+
public UserController(UserService userService) {
|
|
165
|
+
this.userService = userService;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@PostMapping
|
|
169
|
+
@ResponseStatus(HttpStatus.CREATED)
|
|
170
|
+
public UserDto createUser(@RequestBody @Valid CreateUserRequest request) {
|
|
171
|
+
return userService.createUser(request);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@GetMapping("/{id}")
|
|
175
|
+
public UserDto getUser(@PathVariable Long id) {
|
|
176
|
+
return userService.getById(id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@GetMapping
|
|
180
|
+
public Page<UserDto> listUsers(Pageable pageable) {
|
|
181
|
+
return userService.findAll(pageable);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### DTOs avec des records
|
|
187
|
+
```java
|
|
188
|
+
// dto/CreateUserRequest.java
|
|
189
|
+
public record CreateUserRequest(
|
|
190
|
+
@NotBlank @Email String email,
|
|
191
|
+
@NotBlank @Size(min = 8) String password
|
|
192
|
+
) {}
|
|
193
|
+
|
|
194
|
+
// dto/UserDto.java
|
|
195
|
+
public record UserDto(Long id, String email, Instant createdAt) {
|
|
196
|
+
public static UserDto from(User user) {
|
|
197
|
+
return new UserDto(user.getId(), user.getEmail(), user.getCreatedAt());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Gestionnaire d'exceptions global
|
|
203
|
+
```java
|
|
204
|
+
@RestControllerAdvice
|
|
205
|
+
public class GlobalExceptionHandler {
|
|
206
|
+
|
|
207
|
+
@ExceptionHandler(NotFoundException.class)
|
|
208
|
+
@ResponseStatus(HttpStatus.NOT_FOUND)
|
|
209
|
+
public ProblemDetail handleNotFound(NotFoundException ex) {
|
|
210
|
+
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@ExceptionHandler(ConflictException.class)
|
|
214
|
+
@ResponseStatus(HttpStatus.CONFLICT)
|
|
215
|
+
public ProblemDetail handleConflict(ConflictException ex) {
|
|
216
|
+
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
220
|
+
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
221
|
+
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
|
|
222
|
+
var detail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
|
|
223
|
+
detail.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
|
|
224
|
+
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
225
|
+
.toList());
|
|
226
|
+
return detail;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Spring Security — JWT
|
|
232
|
+
```java
|
|
233
|
+
@Configuration
|
|
234
|
+
@EnableWebSecurity
|
|
235
|
+
public class SecurityConfig {
|
|
236
|
+
|
|
237
|
+
@Bean
|
|
238
|
+
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
|
|
239
|
+
return http
|
|
240
|
+
.csrf(AbstractHttpConfigurer::disable)
|
|
241
|
+
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
242
|
+
.authorizeHttpRequests(auth -> auth
|
|
243
|
+
.requestMatchers("/api/v1/auth/**", "/actuator/health").permitAll()
|
|
244
|
+
.anyRequest().authenticated()
|
|
245
|
+
)
|
|
246
|
+
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
|
|
247
|
+
.build();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Bean
|
|
251
|
+
public PasswordEncoder passwordEncoder() {
|
|
252
|
+
return new BCryptPasswordEncoder();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Tests
|
|
258
|
+
```java
|
|
259
|
+
// @WebMvcTest — test de tranche contrôleur (sans contexte complet)
|
|
260
|
+
@WebMvcTest(UserController.class)
|
|
261
|
+
class UserControllerTest {
|
|
262
|
+
@Autowired MockMvc mockMvc;
|
|
263
|
+
@MockitoBean UserService userService; // Spring Boot 3.4+: @MockitoBean
|
|
264
|
+
|
|
265
|
+
@Test
|
|
266
|
+
void createUser_returnsCreated() throws Exception {
|
|
267
|
+
given(userService.createUser(any())).willReturn(new UserDto(1L, "a@b.com", Instant.now()));
|
|
268
|
+
|
|
269
|
+
mockMvc.perform(post("/api/v1/users")
|
|
270
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
271
|
+
.content("""{"email":"a@b.com","password":"password123"}"""))
|
|
272
|
+
.andExpect(status().isCreated())
|
|
273
|
+
.andExpect(jsonPath("$.email").value("a@b.com"));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// @SpringBootTest — test d'intégration complet
|
|
278
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
279
|
+
@Transactional
|
|
280
|
+
class UserServiceIntegrationTest {
|
|
281
|
+
@Autowired UserService userService;
|
|
282
|
+
|
|
283
|
+
@Test
|
|
284
|
+
void createUser_persistsToDatabase() {
|
|
285
|
+
var user = userService.createUser(new CreateUserRequest("a@b.com", "password123"));
|
|
286
|
+
assertThat(user.email()).isEqualTo("a@b.com");
|
|
287
|
+
assertThat(user.id()).isNotNull();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Patterns Spring Cloud
|
|
293
|
+
```yaml
|
|
294
|
+
# Service discovery with Eureka
|
|
295
|
+
spring:
|
|
296
|
+
application:
|
|
297
|
+
name: user-service
|
|
298
|
+
eureka:
|
|
299
|
+
client:
|
|
300
|
+
service-url:
|
|
301
|
+
defaultZone: http://eureka-server:8761/eureka/
|
|
302
|
+
|
|
303
|
+
# Config server client
|
|
304
|
+
spring:
|
|
305
|
+
config:
|
|
306
|
+
import: configserver:http://config-server:8888
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```java
|
|
310
|
+
// Feign client for inter-service calls
|
|
311
|
+
@FeignClient(name = "order-service")
|
|
312
|
+
public interface OrderClient {
|
|
313
|
+
@GetMapping("/api/v1/orders/user/{userId}")
|
|
314
|
+
List<OrderDto> getOrdersByUser(@PathVariable Long userId);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Exemple
|
|
319
|
+
|
|
320
|
+
**Utilisateur :** Construire une API REST Spring Boot pour un catalogue de produits avec des endpoints CRUD, PostgreSQL via JPA, validation des beans, gestion globale des erreurs, et un `@WebMvcTest` pour l'endpoint GET.
|
|
321
|
+
|
|
322
|
+
**Résultat attendu :**
|
|
323
|
+
- Entity `Product` — `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
|
|
324
|
+
- Record `ProductDto` + record `CreateProductRequest` avec validation `@NotBlank` / `@Positive`
|
|
325
|
+
- `ProductRepository extends JpaRepository<Product, Long>`
|
|
326
|
+
- `ProductService` — classe avec `@Transactional(readOnly = true)`, `@Transactional` sur les écritures
|
|
327
|
+
- `ProductController` — CRUD sur `/api/v1/products`, `Pageable` sur la liste GET
|
|
328
|
+
- `GlobalExceptionHandler` — `NotFoundException` → 404, `MethodArgumentNotValidException` → 400 avec les erreurs de champ
|
|
329
|
+
- `ProductControllerTest` utilisant `@WebMvcTest` + `@MockitoBean ProductService`
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
> **Travaillez avec nous :** Claudient est soutenu par [Uitbreiden](https://uitbreiden.com/) — nous construisons des produits IA et des solutions B2B avec des communautés de développeurs. [uitbreiden.com](https://uitbreiden.com/)
|