@brookmind/ai-toolkit 1.1.3 → 1.1.5
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.
|
@@ -0,0 +1,1519 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spring-boot-development
|
|
3
|
+
description: Comprehensive Spring Boot development skill covering auto-configuration, dependency injection, REST APIs, Spring Data, security, and enterprise Java applications
|
|
4
|
+
category: backend
|
|
5
|
+
tags: [spring-boot, java, rest-api, spring-data, jpa, security, microservices, enterprise]
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
context7_library: /spring-projects/spring-boot
|
|
8
|
+
context7_trust_score: 7.5
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Spring Boot Development Skill
|
|
12
|
+
|
|
13
|
+
This skill provides comprehensive guidance for building modern Spring Boot applications using auto-configuration, dependency injection, REST APIs, Spring Data, Spring Security, and enterprise Java patterns based on official Spring Boot documentation.
|
|
14
|
+
|
|
15
|
+
## When to Use This Skill
|
|
16
|
+
|
|
17
|
+
Use this skill when:
|
|
18
|
+
- Building enterprise REST APIs and microservices
|
|
19
|
+
- Creating web applications with Spring MVC
|
|
20
|
+
- Developing data-driven applications with JPA and databases
|
|
21
|
+
- Implementing authentication and authorization with Spring Security
|
|
22
|
+
- Building production-ready applications with actuator and monitoring
|
|
23
|
+
- Creating scalable backend services with Spring Boot
|
|
24
|
+
- Migrating from traditional Spring to Spring Boot
|
|
25
|
+
- Developing cloud-native applications
|
|
26
|
+
- Building event-driven systems with messaging
|
|
27
|
+
- Creating batch processing applications
|
|
28
|
+
|
|
29
|
+
## Core Concepts
|
|
30
|
+
|
|
31
|
+
### Auto-Configuration
|
|
32
|
+
|
|
33
|
+
Spring Boot automatically configures your application based on the dependencies you have added to the project. This reduces boilerplate configuration significantly.
|
|
34
|
+
|
|
35
|
+
**How Auto-Configuration Works:**
|
|
36
|
+
```java
|
|
37
|
+
@SpringBootApplication
|
|
38
|
+
public class MyApplication {
|
|
39
|
+
public static void main(String[] args) {
|
|
40
|
+
SpringApplication.run(MyApplication.class, args);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `@SpringBootApplication` annotation is a combination of:
|
|
46
|
+
- `@Configuration`: Tags the class as a source of bean definitions
|
|
47
|
+
- `@EnableAutoConfiguration`: Enables Spring Boot's auto-configuration mechanism
|
|
48
|
+
- `@ComponentScan`: Enables component scanning in the current package and sub-packages
|
|
49
|
+
|
|
50
|
+
**Conditional Auto-Configuration:**
|
|
51
|
+
```java
|
|
52
|
+
@Configuration
|
|
53
|
+
@ConditionalOnClass(DataSource.class)
|
|
54
|
+
@ConditionalOnProperty(name = "spring.datasource.url")
|
|
55
|
+
public class DataSourceAutoConfiguration {
|
|
56
|
+
|
|
57
|
+
@Bean
|
|
58
|
+
@ConditionalOnMissingBean
|
|
59
|
+
public DataSource dataSource() {
|
|
60
|
+
return DataSourceBuilder.create().build();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Customizing Auto-Configuration:**
|
|
66
|
+
```java
|
|
67
|
+
// Exclude specific auto-configurations
|
|
68
|
+
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
|
|
69
|
+
public class MyApplication {
|
|
70
|
+
// ...
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Or in application.properties
|
|
74
|
+
// spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Dependency Injection
|
|
78
|
+
|
|
79
|
+
Spring's IoC (Inversion of Control) container manages object creation and dependency injection.
|
|
80
|
+
|
|
81
|
+
**Constructor Injection (Recommended):**
|
|
82
|
+
```java
|
|
83
|
+
@Service
|
|
84
|
+
public class UserService {
|
|
85
|
+
|
|
86
|
+
private final UserRepository userRepository;
|
|
87
|
+
private final EmailService emailService;
|
|
88
|
+
|
|
89
|
+
// Constructor injection - recommended approach
|
|
90
|
+
public UserService(UserRepository userRepository, EmailService emailService) {
|
|
91
|
+
this.userRepository = userRepository;
|
|
92
|
+
this.emailService = emailService;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public User createUser(User user) {
|
|
96
|
+
User saved = userRepository.save(user);
|
|
97
|
+
emailService.sendWelcomeEmail(saved);
|
|
98
|
+
return saved;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Field Injection (Not Recommended):**
|
|
104
|
+
```java
|
|
105
|
+
@Service
|
|
106
|
+
public class UserService {
|
|
107
|
+
|
|
108
|
+
@Autowired // Avoid field injection
|
|
109
|
+
private UserRepository userRepository;
|
|
110
|
+
|
|
111
|
+
// Difficult to test and creates tight coupling
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Setter Injection (Optional Dependencies):**
|
|
116
|
+
```java
|
|
117
|
+
@Service
|
|
118
|
+
public class UserService {
|
|
119
|
+
|
|
120
|
+
private UserRepository userRepository;
|
|
121
|
+
private EmailService emailService;
|
|
122
|
+
|
|
123
|
+
@Autowired
|
|
124
|
+
public void setUserRepository(UserRepository userRepository) {
|
|
125
|
+
this.userRepository = userRepository;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Autowired(required = false)
|
|
129
|
+
public void setEmailService(EmailService emailService) {
|
|
130
|
+
this.emailService = emailService;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Component Stereotypes:**
|
|
136
|
+
```java
|
|
137
|
+
@Component // Generic component
|
|
138
|
+
public class MyComponent { }
|
|
139
|
+
|
|
140
|
+
@Service // Business logic layer
|
|
141
|
+
public class MyService { }
|
|
142
|
+
|
|
143
|
+
@Repository // Data access layer
|
|
144
|
+
public class MyRepository { }
|
|
145
|
+
|
|
146
|
+
@Controller // Presentation layer (web)
|
|
147
|
+
public class MyController { }
|
|
148
|
+
|
|
149
|
+
@RestController // REST API controller
|
|
150
|
+
public class MyRestController { }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Spring Web (REST APIs)
|
|
154
|
+
|
|
155
|
+
Build RESTful web services with Spring MVC annotations.
|
|
156
|
+
|
|
157
|
+
**Basic REST Controller:**
|
|
158
|
+
```java
|
|
159
|
+
@RestController
|
|
160
|
+
@RequestMapping("/api/users")
|
|
161
|
+
public class UserController {
|
|
162
|
+
|
|
163
|
+
private final UserService userService;
|
|
164
|
+
|
|
165
|
+
public UserController(UserService userService) {
|
|
166
|
+
this.userService = userService;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@GetMapping
|
|
170
|
+
public List<User> getAllUsers() {
|
|
171
|
+
return userService.findAll();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@GetMapping("/{id}")
|
|
175
|
+
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
|
176
|
+
return userService.findById(id)
|
|
177
|
+
.map(ResponseEntity::ok)
|
|
178
|
+
.orElse(ResponseEntity.notFound().build());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@PostMapping
|
|
182
|
+
public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
|
|
183
|
+
User created = userService.save(user);
|
|
184
|
+
URI location = ServletUriComponentsBuilder
|
|
185
|
+
.fromCurrentRequest()
|
|
186
|
+
.path("/{id}")
|
|
187
|
+
.buildAndExpand(created.getId())
|
|
188
|
+
.toUri();
|
|
189
|
+
return ResponseEntity.created(location).body(created);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@PutMapping("/{id}")
|
|
193
|
+
public ResponseEntity<User> updateUser(@PathVariable Long id,
|
|
194
|
+
@RequestBody @Valid User user) {
|
|
195
|
+
return userService.update(id, user)
|
|
196
|
+
.map(ResponseEntity::ok)
|
|
197
|
+
.orElse(ResponseEntity.notFound().build());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@DeleteMapping("/{id}")
|
|
201
|
+
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
|
202
|
+
if (userService.delete(id)) {
|
|
203
|
+
return ResponseEntity.noContent().build();
|
|
204
|
+
}
|
|
205
|
+
return ResponseEntity.notFound().build();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Request Mapping Variations:**
|
|
211
|
+
```java
|
|
212
|
+
@RestController
|
|
213
|
+
@RequestMapping("/api/products")
|
|
214
|
+
public class ProductController {
|
|
215
|
+
|
|
216
|
+
// Query parameters
|
|
217
|
+
@GetMapping("/search")
|
|
218
|
+
public List<Product> search(@RequestParam String name,
|
|
219
|
+
@RequestParam(required = false) String category) {
|
|
220
|
+
return productService.search(name, category);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Multiple path variables
|
|
224
|
+
@GetMapping("/categories/{categoryId}/products/{productId}")
|
|
225
|
+
public Product getProductInCategory(@PathVariable Long categoryId,
|
|
226
|
+
@PathVariable Long productId) {
|
|
227
|
+
return productService.findInCategory(categoryId, productId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Request headers
|
|
231
|
+
@GetMapping("/{id}")
|
|
232
|
+
public Product getProduct(@PathVariable Long id,
|
|
233
|
+
@RequestHeader("Accept-Language") String language) {
|
|
234
|
+
return productService.find(id, language);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Matrix variables
|
|
238
|
+
@GetMapping("/{id}")
|
|
239
|
+
public Product getProductWithMatrix(@PathVariable Long id,
|
|
240
|
+
@MatrixVariable Map<String, String> filters) {
|
|
241
|
+
return productService.findWithFilters(id, filters);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Response Handling:**
|
|
247
|
+
```java
|
|
248
|
+
@RestController
|
|
249
|
+
@RequestMapping("/api/orders")
|
|
250
|
+
public class OrderController {
|
|
251
|
+
|
|
252
|
+
// Return different status codes
|
|
253
|
+
@PostMapping
|
|
254
|
+
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
|
|
255
|
+
Order created = orderService.create(order);
|
|
256
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Custom headers
|
|
260
|
+
@GetMapping("/{id}")
|
|
261
|
+
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
|
|
262
|
+
Order order = orderService.findById(id);
|
|
263
|
+
return ResponseEntity.ok()
|
|
264
|
+
.header("X-Order-Version", order.getVersion().toString())
|
|
265
|
+
.body(order);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// No content response
|
|
269
|
+
@DeleteMapping("/{id}")
|
|
270
|
+
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
|
|
271
|
+
orderService.delete(id);
|
|
272
|
+
return ResponseEntity.noContent().build();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Spring Data JPA
|
|
278
|
+
|
|
279
|
+
Spring Data JPA provides repository abstractions for database access.
|
|
280
|
+
|
|
281
|
+
**Entity Definition:**
|
|
282
|
+
```java
|
|
283
|
+
@Entity
|
|
284
|
+
@Table(name = "users")
|
|
285
|
+
public class User {
|
|
286
|
+
|
|
287
|
+
@Id
|
|
288
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
289
|
+
private Long id;
|
|
290
|
+
|
|
291
|
+
@Column(nullable = false, unique = true)
|
|
292
|
+
private String email;
|
|
293
|
+
|
|
294
|
+
@Column(nullable = false)
|
|
295
|
+
private String name;
|
|
296
|
+
|
|
297
|
+
@Column(name = "created_at", nullable = false, updatable = false)
|
|
298
|
+
private LocalDateTime createdAt;
|
|
299
|
+
|
|
300
|
+
@Column(name = "updated_at")
|
|
301
|
+
private LocalDateTime updatedAt;
|
|
302
|
+
|
|
303
|
+
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
304
|
+
private List<Order> orders = new ArrayList<>();
|
|
305
|
+
|
|
306
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
307
|
+
@JoinColumn(name = "department_id")
|
|
308
|
+
private Department department;
|
|
309
|
+
|
|
310
|
+
@PrePersist
|
|
311
|
+
protected void onCreate() {
|
|
312
|
+
createdAt = LocalDateTime.now();
|
|
313
|
+
updatedAt = LocalDateTime.now();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@PreUpdate
|
|
317
|
+
protected void onUpdate() {
|
|
318
|
+
updatedAt = LocalDateTime.now();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Getters and setters
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Repository Interface:**
|
|
326
|
+
```java
|
|
327
|
+
@Repository
|
|
328
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
329
|
+
|
|
330
|
+
// Query method - Spring Data generates implementation
|
|
331
|
+
Optional<User> findByEmail(String email);
|
|
332
|
+
|
|
333
|
+
List<User> findByNameContaining(String name);
|
|
334
|
+
|
|
335
|
+
List<User> findByDepartmentId(Long departmentId);
|
|
336
|
+
|
|
337
|
+
// Custom JPQL query
|
|
338
|
+
@Query("SELECT u FROM User u WHERE u.email = ?1")
|
|
339
|
+
Optional<User> findByEmailQuery(String email);
|
|
340
|
+
|
|
341
|
+
// Named parameters
|
|
342
|
+
@Query("SELECT u FROM User u WHERE u.name LIKE %:name% AND u.department.id = :deptId")
|
|
343
|
+
List<User> searchByNameAndDepartment(@Param("name") String name,
|
|
344
|
+
@Param("deptId") Long deptId);
|
|
345
|
+
|
|
346
|
+
// Native SQL query
|
|
347
|
+
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
|
|
348
|
+
Optional<User> findByEmailNative(String email);
|
|
349
|
+
|
|
350
|
+
// Modifying query
|
|
351
|
+
@Modifying
|
|
352
|
+
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
|
|
353
|
+
int updateUserName(@Param("id") Long id, @Param("name") String name);
|
|
354
|
+
|
|
355
|
+
// Pagination and sorting
|
|
356
|
+
Page<User> findByDepartmentId(Long departmentId, Pageable pageable);
|
|
357
|
+
|
|
358
|
+
List<User> findByNameContaining(String name, Sort sort);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Repository Usage:**
|
|
363
|
+
```java
|
|
364
|
+
@Service
|
|
365
|
+
public class UserService {
|
|
366
|
+
|
|
367
|
+
private final UserRepository userRepository;
|
|
368
|
+
|
|
369
|
+
public UserService(UserRepository userRepository) {
|
|
370
|
+
this.userRepository = userRepository;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
public Optional<User> findById(Long id) {
|
|
374
|
+
return userRepository.findById(id);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public User save(User user) {
|
|
378
|
+
return userRepository.save(user);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
public List<User> findAll() {
|
|
382
|
+
return userRepository.findAll();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
public Page<User> findAll(int page, int size) {
|
|
386
|
+
Pageable pageable = PageRequest.of(page, size, Sort.by("name"));
|
|
387
|
+
return userRepository.findAll(pageable);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public boolean delete(Long id) {
|
|
391
|
+
if (userRepository.existsById(id)) {
|
|
392
|
+
userRepository.deleteById(id);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Relationships:**
|
|
401
|
+
```java
|
|
402
|
+
// One-to-Many
|
|
403
|
+
@Entity
|
|
404
|
+
public class Order {
|
|
405
|
+
@Id
|
|
406
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
407
|
+
private Long id;
|
|
408
|
+
|
|
409
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
410
|
+
@JoinColumn(name = "user_id")
|
|
411
|
+
private User user;
|
|
412
|
+
|
|
413
|
+
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
414
|
+
private List<OrderItem> items = new ArrayList<>();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Many-to-Many
|
|
418
|
+
@Entity
|
|
419
|
+
public class Student {
|
|
420
|
+
@Id
|
|
421
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
422
|
+
private Long id;
|
|
423
|
+
|
|
424
|
+
@ManyToMany
|
|
425
|
+
@JoinTable(
|
|
426
|
+
name = "student_course",
|
|
427
|
+
joinColumns = @JoinColumn(name = "student_id"),
|
|
428
|
+
inverseJoinColumns = @JoinColumn(name = "course_id")
|
|
429
|
+
)
|
|
430
|
+
private Set<Course> courses = new HashSet<>();
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Configuration
|
|
435
|
+
|
|
436
|
+
Spring Boot uses `application.properties` or `application.yml` for configuration.
|
|
437
|
+
|
|
438
|
+
**Application Properties:**
|
|
439
|
+
```properties
|
|
440
|
+
# Server configuration
|
|
441
|
+
server.port=8080
|
|
442
|
+
server.servlet.context-path=/api
|
|
443
|
+
|
|
444
|
+
# Database configuration
|
|
445
|
+
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
|
|
446
|
+
spring.datasource.username=user
|
|
447
|
+
spring.datasource.password=password
|
|
448
|
+
spring.datasource.driver-class-name=org.postgresql.Driver
|
|
449
|
+
|
|
450
|
+
# JPA configuration
|
|
451
|
+
spring.jpa.hibernate.ddl-auto=validate
|
|
452
|
+
spring.jpa.show-sql=true
|
|
453
|
+
spring.jpa.properties.hibernate.format_sql=true
|
|
454
|
+
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
|
455
|
+
|
|
456
|
+
# Logging
|
|
457
|
+
logging.level.root=INFO
|
|
458
|
+
logging.level.com.example=DEBUG
|
|
459
|
+
logging.level.org.springframework.web=DEBUG
|
|
460
|
+
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
|
|
461
|
+
|
|
462
|
+
# Custom properties
|
|
463
|
+
app.name=My Application
|
|
464
|
+
app.version=1.0.0
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Application YAML:**
|
|
468
|
+
```yaml
|
|
469
|
+
server:
|
|
470
|
+
port: 8080
|
|
471
|
+
servlet:
|
|
472
|
+
context-path: /api
|
|
473
|
+
|
|
474
|
+
spring:
|
|
475
|
+
datasource:
|
|
476
|
+
url: jdbc:postgresql://localhost:5432/mydb
|
|
477
|
+
username: user
|
|
478
|
+
password: password
|
|
479
|
+
driver-class-name: org.postgresql.Driver
|
|
480
|
+
|
|
481
|
+
jpa:
|
|
482
|
+
hibernate:
|
|
483
|
+
ddl-auto: validate
|
|
484
|
+
show-sql: true
|
|
485
|
+
properties:
|
|
486
|
+
hibernate:
|
|
487
|
+
format_sql: true
|
|
488
|
+
dialect: org.hibernate.dialect.PostgreSQLDialect
|
|
489
|
+
|
|
490
|
+
logging:
|
|
491
|
+
level:
|
|
492
|
+
root: INFO
|
|
493
|
+
com.example: DEBUG
|
|
494
|
+
org.springframework.web: DEBUG
|
|
495
|
+
|
|
496
|
+
app:
|
|
497
|
+
name: My Application
|
|
498
|
+
version: 1.0.0
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Configuration Properties Class:**
|
|
502
|
+
```java
|
|
503
|
+
@Configuration
|
|
504
|
+
@ConfigurationProperties(prefix = "app")
|
|
505
|
+
public class AppConfig {
|
|
506
|
+
|
|
507
|
+
private String name;
|
|
508
|
+
private String version;
|
|
509
|
+
private Security security = new Security();
|
|
510
|
+
|
|
511
|
+
public static class Security {
|
|
512
|
+
private int tokenExpiration = 3600;
|
|
513
|
+
private String secretKey;
|
|
514
|
+
|
|
515
|
+
// Getters and setters
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Getters and setters
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Usage
|
|
522
|
+
@Service
|
|
523
|
+
public class MyService {
|
|
524
|
+
|
|
525
|
+
private final AppConfig appConfig;
|
|
526
|
+
|
|
527
|
+
public MyService(AppConfig appConfig) {
|
|
528
|
+
this.appConfig = appConfig;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
public void printConfig() {
|
|
532
|
+
System.out.println("App: " + appConfig.getName());
|
|
533
|
+
System.out.println("Version: " + appConfig.getVersion());
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Environment-Specific Configuration:**
|
|
539
|
+
```properties
|
|
540
|
+
# application.properties (default)
|
|
541
|
+
spring.profiles.active=dev
|
|
542
|
+
|
|
543
|
+
# application-dev.properties
|
|
544
|
+
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb_dev
|
|
545
|
+
logging.level.root=DEBUG
|
|
546
|
+
|
|
547
|
+
# application-prod.properties
|
|
548
|
+
spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb_prod
|
|
549
|
+
logging.level.root=WARN
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
**Profile-Specific Beans:**
|
|
553
|
+
```java
|
|
554
|
+
@Configuration
|
|
555
|
+
public class DatabaseConfig {
|
|
556
|
+
|
|
557
|
+
@Bean
|
|
558
|
+
@Profile("dev")
|
|
559
|
+
public DataSource devDataSource() {
|
|
560
|
+
return new EmbeddedDatabaseBuilder()
|
|
561
|
+
.setType(EmbeddedDatabaseType.H2)
|
|
562
|
+
.build();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@Bean
|
|
566
|
+
@Profile("prod")
|
|
567
|
+
public DataSource prodDataSource() {
|
|
568
|
+
return DataSourceBuilder.create().build();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Spring Security
|
|
574
|
+
|
|
575
|
+
Implement authentication and authorization in your application.
|
|
576
|
+
|
|
577
|
+
**Basic Security Configuration:**
|
|
578
|
+
```java
|
|
579
|
+
@Configuration
|
|
580
|
+
@EnableWebSecurity
|
|
581
|
+
public class SecurityConfig {
|
|
582
|
+
|
|
583
|
+
@Bean
|
|
584
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
585
|
+
http
|
|
586
|
+
.csrf().disable()
|
|
587
|
+
.authorizeHttpRequests(auth -> auth
|
|
588
|
+
.requestMatchers("/api/public/**").permitAll()
|
|
589
|
+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
590
|
+
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
|
|
591
|
+
.anyRequest().authenticated()
|
|
592
|
+
)
|
|
593
|
+
.httpBasic();
|
|
594
|
+
|
|
595
|
+
return http.build();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
@Bean
|
|
599
|
+
public PasswordEncoder passwordEncoder() {
|
|
600
|
+
return new BCryptPasswordEncoder();
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**In-Memory Authentication:**
|
|
606
|
+
```java
|
|
607
|
+
@Configuration
|
|
608
|
+
@EnableWebSecurity
|
|
609
|
+
public class SecurityConfig {
|
|
610
|
+
|
|
611
|
+
@Bean
|
|
612
|
+
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
|
|
613
|
+
UserDetails user = User.builder()
|
|
614
|
+
.username("user")
|
|
615
|
+
.password(passwordEncoder.encode("password"))
|
|
616
|
+
.roles("USER")
|
|
617
|
+
.build();
|
|
618
|
+
|
|
619
|
+
UserDetails admin = User.builder()
|
|
620
|
+
.username("admin")
|
|
621
|
+
.password(passwordEncoder.encode("admin"))
|
|
622
|
+
.roles("ADMIN", "USER")
|
|
623
|
+
.build();
|
|
624
|
+
|
|
625
|
+
return new InMemoryUserDetailsManager(user, admin);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Database Authentication:**
|
|
631
|
+
```java
|
|
632
|
+
@Service
|
|
633
|
+
public class CustomUserDetailsService implements UserDetailsService {
|
|
634
|
+
|
|
635
|
+
private final UserRepository userRepository;
|
|
636
|
+
|
|
637
|
+
public CustomUserDetailsService(UserRepository userRepository) {
|
|
638
|
+
this.userRepository = userRepository;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
@Override
|
|
642
|
+
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
|
643
|
+
User user = userRepository.findByEmail(username)
|
|
644
|
+
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
|
645
|
+
|
|
646
|
+
return org.springframework.security.core.userdetails.User.builder()
|
|
647
|
+
.username(user.getEmail())
|
|
648
|
+
.password(user.getPassword())
|
|
649
|
+
.roles(user.getRoles().toArray(new String[0]))
|
|
650
|
+
.build();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
@Configuration
|
|
655
|
+
@EnableWebSecurity
|
|
656
|
+
public class SecurityConfig {
|
|
657
|
+
|
|
658
|
+
private final CustomUserDetailsService userDetailsService;
|
|
659
|
+
|
|
660
|
+
public SecurityConfig(CustomUserDetailsService userDetailsService) {
|
|
661
|
+
this.userDetailsService = userDetailsService;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
@Bean
|
|
665
|
+
public DaoAuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder) {
|
|
666
|
+
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
|
667
|
+
provider.setUserDetailsService(userDetailsService);
|
|
668
|
+
provider.setPasswordEncoder(passwordEncoder);
|
|
669
|
+
return provider;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**JWT Authentication:**
|
|
675
|
+
```java
|
|
676
|
+
@Component
|
|
677
|
+
public class JwtTokenProvider {
|
|
678
|
+
|
|
679
|
+
@Value("${app.security.jwt.secret}")
|
|
680
|
+
private String jwtSecret;
|
|
681
|
+
|
|
682
|
+
@Value("${app.security.jwt.expiration}")
|
|
683
|
+
private int jwtExpiration;
|
|
684
|
+
|
|
685
|
+
public String generateToken(Authentication authentication) {
|
|
686
|
+
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
|
687
|
+
|
|
688
|
+
Date now = new Date();
|
|
689
|
+
Date expiryDate = new Date(now.getTime() + jwtExpiration);
|
|
690
|
+
|
|
691
|
+
return Jwts.builder()
|
|
692
|
+
.setSubject(Long.toString(userPrincipal.getId()))
|
|
693
|
+
.setIssuedAt(now)
|
|
694
|
+
.setExpiration(expiryDate)
|
|
695
|
+
.signWith(SignatureAlgorithm.HS512, jwtSecret)
|
|
696
|
+
.compact();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
public Long getUserIdFromJWT(String token) {
|
|
700
|
+
Claims claims = Jwts.parser()
|
|
701
|
+
.setSigningKey(jwtSecret)
|
|
702
|
+
.parseClaimsJws(token)
|
|
703
|
+
.getBody();
|
|
704
|
+
|
|
705
|
+
return Long.parseLong(claims.getSubject());
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
public boolean validateToken(String authToken) {
|
|
709
|
+
try {
|
|
710
|
+
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
|
|
711
|
+
return true;
|
|
712
|
+
} catch (SignatureException | MalformedJwtException | ExpiredJwtException |
|
|
713
|
+
UnsupportedJwtException | IllegalArgumentException ex) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
@Component
|
|
720
|
+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
721
|
+
|
|
722
|
+
private final JwtTokenProvider tokenProvider;
|
|
723
|
+
private final CustomUserDetailsService customUserDetailsService;
|
|
724
|
+
|
|
725
|
+
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
|
|
726
|
+
CustomUserDetailsService customUserDetailsService) {
|
|
727
|
+
this.tokenProvider = tokenProvider;
|
|
728
|
+
this.customUserDetailsService = customUserDetailsService;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
@Override
|
|
732
|
+
protected void doFilterInternal(HttpServletRequest request,
|
|
733
|
+
HttpServletResponse response,
|
|
734
|
+
FilterChain filterChain) throws ServletException, IOException {
|
|
735
|
+
try {
|
|
736
|
+
String jwt = getJwtFromRequest(request);
|
|
737
|
+
|
|
738
|
+
if (jwt != null && tokenProvider.validateToken(jwt)) {
|
|
739
|
+
Long userId = tokenProvider.getUserIdFromJWT(jwt);
|
|
740
|
+
|
|
741
|
+
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
|
|
742
|
+
UsernamePasswordAuthenticationToken authentication =
|
|
743
|
+
new UsernamePasswordAuthenticationToken(
|
|
744
|
+
userDetails, null, userDetails.getAuthorities()
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
748
|
+
}
|
|
749
|
+
} catch (Exception ex) {
|
|
750
|
+
logger.error("Could not set user authentication", ex);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
filterChain.doFilter(request, response);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private String getJwtFromRequest(HttpServletRequest request) {
|
|
757
|
+
String bearerToken = request.getHeader("Authorization");
|
|
758
|
+
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
|
759
|
+
return bearerToken.substring(7);
|
|
760
|
+
}
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
## API Reference
|
|
767
|
+
|
|
768
|
+
### Common Annotations
|
|
769
|
+
|
|
770
|
+
**Core Spring Annotations:**
|
|
771
|
+
- `@SpringBootApplication`: Main application class
|
|
772
|
+
- `@Component`: Generic component
|
|
773
|
+
- `@Service`: Service layer component
|
|
774
|
+
- `@Repository`: Data access layer component
|
|
775
|
+
- `@Configuration`: Configuration class
|
|
776
|
+
- `@Bean`: Bean definition method
|
|
777
|
+
- `@Autowired`: Dependency injection
|
|
778
|
+
- `@Value`: Inject property values
|
|
779
|
+
- `@Profile`: Conditional beans based on profiles
|
|
780
|
+
|
|
781
|
+
**Web Annotations:**
|
|
782
|
+
- `@RestController`: REST API controller
|
|
783
|
+
- `@Controller`: MVC controller
|
|
784
|
+
- `@RequestMapping`: Map HTTP requests
|
|
785
|
+
- `@GetMapping`: Map GET requests
|
|
786
|
+
- `@PostMapping`: Map POST requests
|
|
787
|
+
- `@PutMapping`: Map PUT requests
|
|
788
|
+
- `@DeleteMapping`: Map DELETE requests
|
|
789
|
+
- `@PatchMapping`: Map PATCH requests
|
|
790
|
+
- `@PathVariable`: Extract path variables
|
|
791
|
+
- `@RequestParam`: Extract query parameters
|
|
792
|
+
- `@RequestBody`: Extract request body
|
|
793
|
+
- `@RequestHeader`: Extract request headers
|
|
794
|
+
- `@ResponseStatus`: Set response status
|
|
795
|
+
|
|
796
|
+
**Data Annotations:**
|
|
797
|
+
- `@Entity`: JPA entity
|
|
798
|
+
- `@Table`: Table mapping
|
|
799
|
+
- `@Id`: Primary key
|
|
800
|
+
- `@GeneratedValue`: Auto-generated values
|
|
801
|
+
- `@Column`: Column mapping
|
|
802
|
+
- `@OneToOne`: One-to-one relationship
|
|
803
|
+
- `@OneToMany`: One-to-many relationship
|
|
804
|
+
- `@ManyToOne`: Many-to-one relationship
|
|
805
|
+
- `@ManyToMany`: Many-to-many relationship
|
|
806
|
+
- `@JoinColumn`: Join column
|
|
807
|
+
- `@JoinTable`: Join table
|
|
808
|
+
|
|
809
|
+
**Validation Annotations:**
|
|
810
|
+
- `@Valid`: Enable validation
|
|
811
|
+
- `@NotNull`: Field cannot be null
|
|
812
|
+
- `@NotEmpty`: Field cannot be empty
|
|
813
|
+
- `@NotBlank`: Field cannot be blank
|
|
814
|
+
- `@Size`: String or collection size
|
|
815
|
+
- `@Min`: Minimum value
|
|
816
|
+
- `@Max`: Maximum value
|
|
817
|
+
- `@Email`: Email format
|
|
818
|
+
- `@Pattern`: Regex pattern
|
|
819
|
+
|
|
820
|
+
**Transaction Annotations:**
|
|
821
|
+
- `@Transactional`: Enable transaction management
|
|
822
|
+
- `@Transactional(readOnly = true)`: Read-only transaction
|
|
823
|
+
|
|
824
|
+
**Security Annotations:**
|
|
825
|
+
- `@EnableWebSecurity`: Enable security
|
|
826
|
+
- `@PreAuthorize`: Method-level authorization
|
|
827
|
+
- `@PostAuthorize`: Post-method authorization
|
|
828
|
+
- `@Secured`: Role-based access
|
|
829
|
+
|
|
830
|
+
**Async and Scheduling:**
|
|
831
|
+
- `@EnableAsync`: Enable async processing
|
|
832
|
+
- `@Async`: Async method
|
|
833
|
+
- `@EnableScheduling`: Enable scheduling
|
|
834
|
+
- `@Scheduled`: Scheduled method
|
|
835
|
+
|
|
836
|
+
## Workflow Patterns
|
|
837
|
+
|
|
838
|
+
### REST API Design Pattern
|
|
839
|
+
|
|
840
|
+
**Complete CRUD REST API:**
|
|
841
|
+
```java
|
|
842
|
+
// Entity
|
|
843
|
+
@Entity
|
|
844
|
+
@Table(name = "products")
|
|
845
|
+
public class Product {
|
|
846
|
+
@Id
|
|
847
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
848
|
+
private Long id;
|
|
849
|
+
|
|
850
|
+
@NotBlank(message = "Name is required")
|
|
851
|
+
private String name;
|
|
852
|
+
|
|
853
|
+
@NotBlank(message = "Description is required")
|
|
854
|
+
private String description;
|
|
855
|
+
|
|
856
|
+
@NotNull(message = "Price is required")
|
|
857
|
+
@Min(value = 0, message = "Price must be positive")
|
|
858
|
+
private BigDecimal price;
|
|
859
|
+
|
|
860
|
+
@NotNull(message = "Stock is required")
|
|
861
|
+
@Min(value = 0, message = "Stock must be positive")
|
|
862
|
+
private Integer stock;
|
|
863
|
+
|
|
864
|
+
private LocalDateTime createdAt;
|
|
865
|
+
private LocalDateTime updatedAt;
|
|
866
|
+
|
|
867
|
+
@PrePersist
|
|
868
|
+
protected void onCreate() {
|
|
869
|
+
createdAt = LocalDateTime.now();
|
|
870
|
+
updatedAt = LocalDateTime.now();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
@PreUpdate
|
|
874
|
+
protected void onUpdate() {
|
|
875
|
+
updatedAt = LocalDateTime.now();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Getters and setters
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Repository
|
|
882
|
+
@Repository
|
|
883
|
+
public interface ProductRepository extends JpaRepository<Product, Long> {
|
|
884
|
+
List<Product> findByNameContaining(String name);
|
|
885
|
+
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Service
|
|
889
|
+
@Service
|
|
890
|
+
@Transactional
|
|
891
|
+
public class ProductService {
|
|
892
|
+
|
|
893
|
+
private final ProductRepository productRepository;
|
|
894
|
+
|
|
895
|
+
public ProductService(ProductRepository productRepository) {
|
|
896
|
+
this.productRepository = productRepository;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
@Transactional(readOnly = true)
|
|
900
|
+
public Page<Product> findAll(Pageable pageable) {
|
|
901
|
+
return productRepository.findAll(pageable);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
@Transactional(readOnly = true)
|
|
905
|
+
public Optional<Product> findById(Long id) {
|
|
906
|
+
return productRepository.findById(id);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
public Product create(Product product) {
|
|
910
|
+
return productRepository.save(product);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
public Optional<Product> update(Long id, Product productDetails) {
|
|
914
|
+
return productRepository.findById(id)
|
|
915
|
+
.map(product -> {
|
|
916
|
+
product.setName(productDetails.getName());
|
|
917
|
+
product.setDescription(productDetails.getDescription());
|
|
918
|
+
product.setPrice(productDetails.getPrice());
|
|
919
|
+
product.setStock(productDetails.getStock());
|
|
920
|
+
return productRepository.save(product);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
public boolean delete(Long id) {
|
|
925
|
+
return productRepository.findById(id)
|
|
926
|
+
.map(product -> {
|
|
927
|
+
productRepository.delete(product);
|
|
928
|
+
return true;
|
|
929
|
+
})
|
|
930
|
+
.orElse(false);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Controller
|
|
935
|
+
@RestController
|
|
936
|
+
@RequestMapping("/api/products")
|
|
937
|
+
public class ProductController {
|
|
938
|
+
|
|
939
|
+
private final ProductService productService;
|
|
940
|
+
|
|
941
|
+
public ProductController(ProductService productService) {
|
|
942
|
+
this.productService = productService;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
@GetMapping
|
|
946
|
+
public Page<Product> getAllProducts(
|
|
947
|
+
@RequestParam(defaultValue = "0") int page,
|
|
948
|
+
@RequestParam(defaultValue = "10") int size,
|
|
949
|
+
@RequestParam(defaultValue = "id") String sortBy) {
|
|
950
|
+
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
|
|
951
|
+
return productService.findAll(pageable);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
@GetMapping("/{id}")
|
|
955
|
+
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
|
|
956
|
+
return productService.findById(id)
|
|
957
|
+
.map(ResponseEntity::ok)
|
|
958
|
+
.orElse(ResponseEntity.notFound().build());
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
@PostMapping
|
|
962
|
+
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
|
|
963
|
+
Product created = productService.create(product);
|
|
964
|
+
URI location = ServletUriComponentsBuilder
|
|
965
|
+
.fromCurrentRequest()
|
|
966
|
+
.path("/{id}")
|
|
967
|
+
.buildAndExpand(created.getId())
|
|
968
|
+
.toUri();
|
|
969
|
+
return ResponseEntity.created(location).body(created);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@PutMapping("/{id}")
|
|
973
|
+
public ResponseEntity<Product> updateProduct(
|
|
974
|
+
@PathVariable Long id,
|
|
975
|
+
@Valid @RequestBody Product product) {
|
|
976
|
+
return productService.update(id, product)
|
|
977
|
+
.map(ResponseEntity::ok)
|
|
978
|
+
.orElse(ResponseEntity.notFound().build());
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
@DeleteMapping("/{id}")
|
|
982
|
+
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
|
|
983
|
+
if (productService.delete(id)) {
|
|
984
|
+
return ResponseEntity.noContent().build();
|
|
985
|
+
}
|
|
986
|
+
return ResponseEntity.notFound().build();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
### Exception Handling Pattern
|
|
992
|
+
|
|
993
|
+
**Global Exception Handler:**
|
|
994
|
+
```java
|
|
995
|
+
// Custom exceptions
|
|
996
|
+
public class ResourceNotFoundException extends RuntimeException {
|
|
997
|
+
public ResourceNotFoundException(String message) {
|
|
998
|
+
super(message);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
public class BadRequestException extends RuntimeException {
|
|
1003
|
+
public BadRequestException(String message) {
|
|
1004
|
+
super(message);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Error response
|
|
1009
|
+
public class ErrorResponse {
|
|
1010
|
+
private LocalDateTime timestamp;
|
|
1011
|
+
private int status;
|
|
1012
|
+
private String error;
|
|
1013
|
+
private String message;
|
|
1014
|
+
private String path;
|
|
1015
|
+
|
|
1016
|
+
// Constructors, getters, setters
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Global exception handler
|
|
1020
|
+
@RestControllerAdvice
|
|
1021
|
+
public class GlobalExceptionHandler {
|
|
1022
|
+
|
|
1023
|
+
@ExceptionHandler(ResourceNotFoundException.class)
|
|
1024
|
+
public ResponseEntity<ErrorResponse> handleResourceNotFound(
|
|
1025
|
+
ResourceNotFoundException ex,
|
|
1026
|
+
WebRequest request) {
|
|
1027
|
+
ErrorResponse error = new ErrorResponse(
|
|
1028
|
+
LocalDateTime.now(),
|
|
1029
|
+
HttpStatus.NOT_FOUND.value(),
|
|
1030
|
+
"Not Found",
|
|
1031
|
+
ex.getMessage(),
|
|
1032
|
+
request.getDescription(false).replace("uri=", "")
|
|
1033
|
+
);
|
|
1034
|
+
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
@ExceptionHandler(BadRequestException.class)
|
|
1038
|
+
public ResponseEntity<ErrorResponse> handleBadRequest(
|
|
1039
|
+
BadRequestException ex,
|
|
1040
|
+
WebRequest request) {
|
|
1041
|
+
ErrorResponse error = new ErrorResponse(
|
|
1042
|
+
LocalDateTime.now(),
|
|
1043
|
+
HttpStatus.BAD_REQUEST.value(),
|
|
1044
|
+
"Bad Request",
|
|
1045
|
+
ex.getMessage(),
|
|
1046
|
+
request.getDescription(false).replace("uri=", "")
|
|
1047
|
+
);
|
|
1048
|
+
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
1052
|
+
public ResponseEntity<Map<String, Object>> handleValidationErrors(
|
|
1053
|
+
MethodArgumentNotValidException ex) {
|
|
1054
|
+
Map<String, Object> errors = new HashMap<>();
|
|
1055
|
+
errors.put("timestamp", LocalDateTime.now());
|
|
1056
|
+
errors.put("status", HttpStatus.BAD_REQUEST.value());
|
|
1057
|
+
|
|
1058
|
+
Map<String, String> fieldErrors = new HashMap<>();
|
|
1059
|
+
ex.getBindingResult().getFieldErrors().forEach(error ->
|
|
1060
|
+
fieldErrors.put(error.getField(), error.getDefaultMessage())
|
|
1061
|
+
);
|
|
1062
|
+
errors.put("errors", fieldErrors);
|
|
1063
|
+
|
|
1064
|
+
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
@ExceptionHandler(Exception.class)
|
|
1068
|
+
public ResponseEntity<ErrorResponse> handleGlobalException(
|
|
1069
|
+
Exception ex,
|
|
1070
|
+
WebRequest request) {
|
|
1071
|
+
ErrorResponse error = new ErrorResponse(
|
|
1072
|
+
LocalDateTime.now(),
|
|
1073
|
+
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
|
1074
|
+
"Internal Server Error",
|
|
1075
|
+
ex.getMessage(),
|
|
1076
|
+
request.getDescription(false).replace("uri=", "")
|
|
1077
|
+
);
|
|
1078
|
+
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Database Integration Pattern
|
|
1084
|
+
|
|
1085
|
+
**Complete Database Setup:**
|
|
1086
|
+
```java
|
|
1087
|
+
// application.yml
|
|
1088
|
+
/*
|
|
1089
|
+
spring:
|
|
1090
|
+
datasource:
|
|
1091
|
+
url: jdbc:postgresql://localhost:5432/mydb
|
|
1092
|
+
username: user
|
|
1093
|
+
password: password
|
|
1094
|
+
jpa:
|
|
1095
|
+
hibernate:
|
|
1096
|
+
ddl-auto: validate
|
|
1097
|
+
show-sql: true
|
|
1098
|
+
properties:
|
|
1099
|
+
hibernate:
|
|
1100
|
+
dialect: org.hibernate.dialect.PostgreSQLDialect
|
|
1101
|
+
*/
|
|
1102
|
+
|
|
1103
|
+
// Flyway migrations (db/migration/V1__Create_users_table.sql)
|
|
1104
|
+
/*
|
|
1105
|
+
CREATE TABLE users (
|
|
1106
|
+
id BIGSERIAL PRIMARY KEY,
|
|
1107
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
1108
|
+
name VARCHAR(255) NOT NULL,
|
|
1109
|
+
password VARCHAR(255) NOT NULL,
|
|
1110
|
+
created_at TIMESTAMP NOT NULL,
|
|
1111
|
+
updated_at TIMESTAMP NOT NULL
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
1115
|
+
*/
|
|
1116
|
+
|
|
1117
|
+
// Entity with auditing
|
|
1118
|
+
@Entity
|
|
1119
|
+
@Table(name = "users")
|
|
1120
|
+
@EntityListeners(AuditingEntityListener.class)
|
|
1121
|
+
public class User {
|
|
1122
|
+
@Id
|
|
1123
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
1124
|
+
private Long id;
|
|
1125
|
+
|
|
1126
|
+
@Column(nullable = false, unique = true)
|
|
1127
|
+
private String email;
|
|
1128
|
+
|
|
1129
|
+
@Column(nullable = false)
|
|
1130
|
+
private String name;
|
|
1131
|
+
|
|
1132
|
+
@Column(nullable = false)
|
|
1133
|
+
private String password;
|
|
1134
|
+
|
|
1135
|
+
@CreatedDate
|
|
1136
|
+
@Column(name = "created_at", nullable = false, updatable = false)
|
|
1137
|
+
private LocalDateTime createdAt;
|
|
1138
|
+
|
|
1139
|
+
@LastModifiedDate
|
|
1140
|
+
@Column(name = "updated_at")
|
|
1141
|
+
private LocalDateTime updatedAt;
|
|
1142
|
+
|
|
1143
|
+
// Getters and setters
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Enable JPA auditing
|
|
1147
|
+
@Configuration
|
|
1148
|
+
@EnableJpaAuditing
|
|
1149
|
+
public class JpaConfig {
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
### Testing Pattern
|
|
1154
|
+
|
|
1155
|
+
**Unit Tests:**
|
|
1156
|
+
```java
|
|
1157
|
+
@SpringBootTest
|
|
1158
|
+
class UserServiceTest {
|
|
1159
|
+
|
|
1160
|
+
@Mock
|
|
1161
|
+
private UserRepository userRepository;
|
|
1162
|
+
|
|
1163
|
+
@InjectMocks
|
|
1164
|
+
private UserService userService;
|
|
1165
|
+
|
|
1166
|
+
@BeforeEach
|
|
1167
|
+
void setUp() {
|
|
1168
|
+
MockitoAnnotations.openMocks(this);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
@Test
|
|
1172
|
+
void testFindById_Success() {
|
|
1173
|
+
User user = new User();
|
|
1174
|
+
user.setId(1L);
|
|
1175
|
+
user.setEmail("test@example.com");
|
|
1176
|
+
|
|
1177
|
+
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
|
|
1178
|
+
|
|
1179
|
+
Optional<User> result = userService.findById(1L);
|
|
1180
|
+
|
|
1181
|
+
assertTrue(result.isPresent());
|
|
1182
|
+
assertEquals("test@example.com", result.get().getEmail());
|
|
1183
|
+
verify(userRepository, times(1)).findById(1L);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
@Test
|
|
1187
|
+
void testFindById_NotFound() {
|
|
1188
|
+
when(userRepository.findById(1L)).thenReturn(Optional.empty());
|
|
1189
|
+
|
|
1190
|
+
Optional<User> result = userService.findById(1L);
|
|
1191
|
+
|
|
1192
|
+
assertFalse(result.isPresent());
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
**Integration Tests:**
|
|
1198
|
+
```java
|
|
1199
|
+
@SpringBootTest
|
|
1200
|
+
@AutoConfigureMockMvc
|
|
1201
|
+
@Transactional
|
|
1202
|
+
class UserControllerIntegrationTest {
|
|
1203
|
+
|
|
1204
|
+
@Autowired
|
|
1205
|
+
private MockMvc mockMvc;
|
|
1206
|
+
|
|
1207
|
+
@Autowired
|
|
1208
|
+
private ObjectMapper objectMapper;
|
|
1209
|
+
|
|
1210
|
+
@Autowired
|
|
1211
|
+
private UserRepository userRepository;
|
|
1212
|
+
|
|
1213
|
+
@Test
|
|
1214
|
+
void testCreateUser_Success() throws Exception {
|
|
1215
|
+
User user = new User();
|
|
1216
|
+
user.setEmail("test@example.com");
|
|
1217
|
+
user.setName("Test User");
|
|
1218
|
+
|
|
1219
|
+
mockMvc.perform(post("/api/users")
|
|
1220
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
1221
|
+
.content(objectMapper.writeValueAsString(user)))
|
|
1222
|
+
.andExpect(status().isCreated())
|
|
1223
|
+
.andExpect(jsonPath("$.email").value("test@example.com"))
|
|
1224
|
+
.andExpect(jsonPath("$.name").value("Test User"));
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
@Test
|
|
1228
|
+
void testGetUser_Success() throws Exception {
|
|
1229
|
+
User user = new User();
|
|
1230
|
+
user.setEmail("test@example.com");
|
|
1231
|
+
user.setName("Test User");
|
|
1232
|
+
User saved = userRepository.save(user);
|
|
1233
|
+
|
|
1234
|
+
mockMvc.perform(get("/api/users/" + saved.getId()))
|
|
1235
|
+
.andExpect(status().isOk())
|
|
1236
|
+
.andExpect(jsonPath("$.id").value(saved.getId()))
|
|
1237
|
+
.andExpect(jsonPath("$.email").value("test@example.com"));
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
@Test
|
|
1241
|
+
void testGetUser_NotFound() throws Exception {
|
|
1242
|
+
mockMvc.perform(get("/api/users/999"))
|
|
1243
|
+
.andExpect(status().isNotFound());
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
## Best Practices
|
|
1249
|
+
|
|
1250
|
+
### 1. Use Constructor Injection
|
|
1251
|
+
|
|
1252
|
+
Constructor injection is the recommended approach for dependency injection.
|
|
1253
|
+
|
|
1254
|
+
```java
|
|
1255
|
+
// Good - Constructor injection
|
|
1256
|
+
@Service
|
|
1257
|
+
public class UserService {
|
|
1258
|
+
private final UserRepository userRepository;
|
|
1259
|
+
private final EmailService emailService;
|
|
1260
|
+
|
|
1261
|
+
public UserService(UserRepository userRepository, EmailService emailService) {
|
|
1262
|
+
this.userRepository = userRepository;
|
|
1263
|
+
this.emailService = emailService;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Bad - Field injection
|
|
1268
|
+
@Service
|
|
1269
|
+
public class UserService {
|
|
1270
|
+
@Autowired
|
|
1271
|
+
private UserRepository userRepository;
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
### 2. Use DTOs for API Requests/Responses
|
|
1276
|
+
|
|
1277
|
+
Don't expose entities directly through REST APIs.
|
|
1278
|
+
|
|
1279
|
+
```java
|
|
1280
|
+
// DTO
|
|
1281
|
+
public class UserDTO {
|
|
1282
|
+
private Long id;
|
|
1283
|
+
private String email;
|
|
1284
|
+
private String name;
|
|
1285
|
+
|
|
1286
|
+
// No password field exposed
|
|
1287
|
+
// Getters and setters
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Mapper
|
|
1291
|
+
@Component
|
|
1292
|
+
public class UserMapper {
|
|
1293
|
+
public UserDTO toDTO(User user) {
|
|
1294
|
+
UserDTO dto = new UserDTO();
|
|
1295
|
+
dto.setId(user.getId());
|
|
1296
|
+
dto.setEmail(user.getEmail());
|
|
1297
|
+
dto.setName(user.getName());
|
|
1298
|
+
return dto;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
public User toEntity(UserDTO dto) {
|
|
1302
|
+
User user = new User();
|
|
1303
|
+
user.setEmail(dto.getEmail());
|
|
1304
|
+
user.setName(dto.getName());
|
|
1305
|
+
return user;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Controller
|
|
1310
|
+
@RestController
|
|
1311
|
+
@RequestMapping("/api/users")
|
|
1312
|
+
public class UserController {
|
|
1313
|
+
private final UserService userService;
|
|
1314
|
+
private final UserMapper userMapper;
|
|
1315
|
+
|
|
1316
|
+
@GetMapping("/{id}")
|
|
1317
|
+
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
|
|
1318
|
+
return userService.findById(id)
|
|
1319
|
+
.map(userMapper::toDTO)
|
|
1320
|
+
.map(ResponseEntity::ok)
|
|
1321
|
+
.orElse(ResponseEntity.notFound().build());
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
### 3. Use Validation
|
|
1327
|
+
|
|
1328
|
+
Always validate input data.
|
|
1329
|
+
|
|
1330
|
+
```java
|
|
1331
|
+
// Entity with validation
|
|
1332
|
+
@Entity
|
|
1333
|
+
public class User {
|
|
1334
|
+
@NotBlank(message = "Email is required")
|
|
1335
|
+
@Email(message = "Email should be valid")
|
|
1336
|
+
private String email;
|
|
1337
|
+
|
|
1338
|
+
@NotBlank(message = "Name is required")
|
|
1339
|
+
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
|
|
1340
|
+
private String name;
|
|
1341
|
+
|
|
1342
|
+
@NotBlank(message = "Password is required")
|
|
1343
|
+
@Size(min = 8, message = "Password must be at least 8 characters")
|
|
1344
|
+
private String password;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Controller
|
|
1348
|
+
@PostMapping
|
|
1349
|
+
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
|
|
1350
|
+
// Validation happens automatically
|
|
1351
|
+
return ResponseEntity.ok(userService.save(user));
|
|
1352
|
+
}
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
### 4. Use Transactions Properly
|
|
1356
|
+
|
|
1357
|
+
Mark service methods with appropriate transaction settings.
|
|
1358
|
+
|
|
1359
|
+
```java
|
|
1360
|
+
@Service
|
|
1361
|
+
@Transactional
|
|
1362
|
+
public class OrderService {
|
|
1363
|
+
|
|
1364
|
+
@Transactional(readOnly = true)
|
|
1365
|
+
public List<Order> findAll() {
|
|
1366
|
+
return orderRepository.findAll();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
@Transactional
|
|
1370
|
+
public Order createOrder(Order order) {
|
|
1371
|
+
// Multiple database operations in one transaction
|
|
1372
|
+
Order saved = orderRepository.save(order);
|
|
1373
|
+
inventoryService.decreaseStock(order.getItems());
|
|
1374
|
+
emailService.sendOrderConfirmation(saved);
|
|
1375
|
+
return saved;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
### 5. Use Pagination
|
|
1381
|
+
|
|
1382
|
+
Always paginate large datasets.
|
|
1383
|
+
|
|
1384
|
+
```java
|
|
1385
|
+
@GetMapping
|
|
1386
|
+
public Page<Product> getProducts(
|
|
1387
|
+
@RequestParam(defaultValue = "0") int page,
|
|
1388
|
+
@RequestParam(defaultValue = "20") int size,
|
|
1389
|
+
@RequestParam(defaultValue = "id") String sortBy) {
|
|
1390
|
+
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
|
|
1391
|
+
return productService.findAll(pageable);
|
|
1392
|
+
}
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
### 6. Handle Exceptions Globally
|
|
1396
|
+
|
|
1397
|
+
Use `@RestControllerAdvice` for centralized exception handling.
|
|
1398
|
+
|
|
1399
|
+
```java
|
|
1400
|
+
@RestControllerAdvice
|
|
1401
|
+
public class GlobalExceptionHandler {
|
|
1402
|
+
|
|
1403
|
+
@ExceptionHandler(ResourceNotFoundException.class)
|
|
1404
|
+
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
|
|
1405
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
1406
|
+
.body(new ErrorResponse(ex.getMessage()));
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
### 7. Use Logging
|
|
1412
|
+
|
|
1413
|
+
Implement proper logging throughout your application.
|
|
1414
|
+
|
|
1415
|
+
```java
|
|
1416
|
+
@Service
|
|
1417
|
+
public class UserService {
|
|
1418
|
+
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
|
|
1419
|
+
|
|
1420
|
+
public User createUser(User user) {
|
|
1421
|
+
logger.info("Creating user with email: {}", user.getEmail());
|
|
1422
|
+
try {
|
|
1423
|
+
User saved = userRepository.save(user);
|
|
1424
|
+
logger.info("User created successfully with id: {}", saved.getId());
|
|
1425
|
+
return saved;
|
|
1426
|
+
} catch (Exception e) {
|
|
1427
|
+
logger.error("Error creating user: {}", e.getMessage(), e);
|
|
1428
|
+
throw e;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
### 8. Secure Your Endpoints
|
|
1435
|
+
|
|
1436
|
+
Implement proper authentication and authorization.
|
|
1437
|
+
|
|
1438
|
+
```java
|
|
1439
|
+
@Configuration
|
|
1440
|
+
@EnableWebSecurity
|
|
1441
|
+
public class SecurityConfig {
|
|
1442
|
+
|
|
1443
|
+
@Bean
|
|
1444
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
1445
|
+
http
|
|
1446
|
+
.authorizeHttpRequests(auth -> auth
|
|
1447
|
+
.requestMatchers("/api/public/**").permitAll()
|
|
1448
|
+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
1449
|
+
.anyRequest().authenticated()
|
|
1450
|
+
)
|
|
1451
|
+
.oauth2ResourceServer().jwt();
|
|
1452
|
+
|
|
1453
|
+
return http.build();
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
### 9. Use Database Migrations
|
|
1459
|
+
|
|
1460
|
+
Use Flyway or Liquibase for database version control.
|
|
1461
|
+
|
|
1462
|
+
```sql
|
|
1463
|
+
-- V1__Create_users_table.sql
|
|
1464
|
+
CREATE TABLE users (
|
|
1465
|
+
id BIGSERIAL PRIMARY KEY,
|
|
1466
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
1467
|
+
name VARCHAR(255) NOT NULL,
|
|
1468
|
+
created_at TIMESTAMP NOT NULL
|
|
1469
|
+
);
|
|
1470
|
+
|
|
1471
|
+
-- V2__Add_password_column.sql
|
|
1472
|
+
ALTER TABLE users ADD COLUMN password VARCHAR(255);
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
### 10. Monitor Your Application
|
|
1476
|
+
|
|
1477
|
+
Use Spring Boot Actuator for monitoring.
|
|
1478
|
+
|
|
1479
|
+
```properties
|
|
1480
|
+
# application.properties
|
|
1481
|
+
management.endpoints.web.exposure.include=health,info,metrics
|
|
1482
|
+
management.endpoint.health.show-details=always
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
## Examples
|
|
1486
|
+
|
|
1487
|
+
See EXAMPLES.md for detailed code examples including:
|
|
1488
|
+
- Basic Spring Boot Application
|
|
1489
|
+
- REST API with CRUD Operations
|
|
1490
|
+
- Database Integration with JPA
|
|
1491
|
+
- Custom Queries and Specifications
|
|
1492
|
+
- Request Validation
|
|
1493
|
+
- Exception Handling
|
|
1494
|
+
- Authentication with JWT
|
|
1495
|
+
- Role-Based Authorization
|
|
1496
|
+
- File Upload/Download
|
|
1497
|
+
- Caching with Redis
|
|
1498
|
+
- Async Processing
|
|
1499
|
+
- Scheduled Tasks
|
|
1500
|
+
- Multiple Database Configuration
|
|
1501
|
+
- Actuator and Monitoring
|
|
1502
|
+
- Docker Deployment
|
|
1503
|
+
|
|
1504
|
+
## Summary
|
|
1505
|
+
|
|
1506
|
+
This Spring Boot development skill covers:
|
|
1507
|
+
|
|
1508
|
+
1. **Auto-Configuration**: Automatic configuration based on dependencies
|
|
1509
|
+
2. **Dependency Injection**: IoC container, constructor injection, component stereotypes
|
|
1510
|
+
3. **REST APIs**: Controllers, request mapping, response handling
|
|
1511
|
+
4. **Spring Data JPA**: Entities, repositories, relationships, queries
|
|
1512
|
+
5. **Configuration**: Properties, YAML, profiles, custom properties
|
|
1513
|
+
6. **Security**: Authentication, authorization, JWT, role-based access
|
|
1514
|
+
7. **Exception Handling**: Global exception handling, custom exceptions
|
|
1515
|
+
8. **Testing**: Unit tests, integration tests, MockMvc
|
|
1516
|
+
9. **Best Practices**: DTOs, validation, transactions, pagination, logging
|
|
1517
|
+
10. **Production Ready**: Actuator, monitoring, database migrations, deployment
|
|
1518
|
+
|
|
1519
|
+
The patterns and examples are based on official Spring Boot documentation (Trust Score: 7.5) and represent modern enterprise Java development practices.
|