claudient 0.1.0 β 0.3.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 +1745 -0
- package/package.json +5 -3
- package/scripts/build-index.js +139 -0
- package/scripts/cli.js +378 -66
- 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
|
+
> π©πͺ Deutsche Version. [Englische Version](../spring-boot.md).
|
|
2
|
+
|
|
3
|
+
# Spring Boot Skill
|
|
4
|
+
|
|
5
|
+
## Wann aktivieren
|
|
6
|
+
- Aufbau einer Spring Boot REST API oder eines Microservices
|
|
7
|
+
- Einrichtung von Spring Data JPA mit Hibernate und PostgreSQL/MySQL
|
|
8
|
+
- Konfiguration von Spring Security (JWT, OAuth2, sitzungsbasierte Authentifizierung)
|
|
9
|
+
- Schreiben von Spring Boot Tests (Unit-Tests mit Mockito, Integrationstests mit TestRestTemplate oder MockMvc)
|
|
10
|
+
- Einrichtung von Spring Cloud (Service Discovery, Config Server, API Gateway)
|
|
11
|
+
- Implementierung asynchroner Verarbeitung mit `@Async` oder Spring Events
|
|
12
|
+
- Konfiguration von Profilen, Properties und externalisierter Konfiguration
|
|
13
|
+
- Schreiben benutzerdefinierter Spring Boot Starters oder Auto-Konfigurationen
|
|
14
|
+
|
|
15
|
+
## Wann NICHT verwenden
|
|
16
|
+
- Quarkus- oder Micronaut-Projekte β unterschiedliche Dependency-Injection- und Konfigurationsmodelle
|
|
17
|
+
- Reines Java ohne Spring β der Overhead ist fΓΌr einfache Skripte nicht gerechtfertigt
|
|
18
|
+
- Android-Projekte β vollstΓ€ndig anderes Γkosystem
|
|
19
|
+
- Jakarta EE/WildFly β anderes Application-Server-Modell
|
|
20
|
+
|
|
21
|
+
## Anweisungen
|
|
22
|
+
|
|
23
|
+
### Projektstruktur
|
|
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
|
+
### Anwendungs-Einstiegspunkt
|
|
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
|
+
### application.yml Struktur
|
|
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 und 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
|
+
### Service-Schicht
|
|
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
|
+
### Controller-Schicht
|
|
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 mit 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
|
+
### Globaler Exception-Handler
|
|
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 β Controller-Slice-Test (kein vollstΓ€ndiger Kontext)
|
|
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 β vollstΓ€ndiger Integrationstest
|
|
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
|
+
### Spring Cloud Muster
|
|
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
|
+
## Beispiel
|
|
319
|
+
|
|
320
|
+
**Nutzer:** Erstelle eine Spring Boot REST API fΓΌr einen Produktkatalog mit CRUD-Endpunkten, PostgreSQL ΓΌber JPA, Bean-Validierung, globalem Fehler-Handling und einem `@WebMvcTest` fΓΌr den GET-Endpunkt.
|
|
321
|
+
|
|
322
|
+
**Erwartete Ausgabe:**
|
|
323
|
+
- `Product` Entity β `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
|
|
324
|
+
- `ProductDto` Record + `CreateProductRequest` Record mit `@NotBlank` / `@Positive` Validierung
|
|
325
|
+
- `ProductRepository extends JpaRepository<Product, Long>`
|
|
326
|
+
- `ProductService` β `@Transactional(readOnly = true)` auf Klassenebene, `@Transactional` fΓΌr Schreiboperationen
|
|
327
|
+
- `ProductController` β CRUD unter `/api/v1/products`, `Pageable` auf GET-Liste
|
|
328
|
+
- `GlobalExceptionHandler` β `NotFoundException` β 404, `MethodArgumentNotValidException` β 400 mit Feldfehlern
|
|
329
|
+
- `ProductControllerTest` mit `@WebMvcTest` + `@MockitoBean ProductService`
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
> **Arbeite mit uns:** Claudient wird von [Uitbreiden](https://uitbreiden.com/) unterstΓΌtzt β wir entwickeln KI-Produkte und B2B-LΓΆsungen mit Entwickler-Communities. [uitbreiden.com](https://uitbreiden.com/)
|
|
@@ -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/)
|