@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,2346 @@
|
|
|
1
|
+
# Spring Boot Development Examples
|
|
2
|
+
|
|
3
|
+
Comprehensive code examples demonstrating Spring Boot patterns, best practices, and real-world use cases.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Basic Spring Boot Application](#1-basic-spring-boot-application)
|
|
8
|
+
2. [REST API with CRUD Operations](#2-rest-api-with-crud-operations)
|
|
9
|
+
3. [Database Integration with JPA](#3-database-integration-with-jpa)
|
|
10
|
+
4. [Custom Queries and Specifications](#4-custom-queries-and-specifications)
|
|
11
|
+
5. [Request Validation](#5-request-validation)
|
|
12
|
+
6. [Exception Handling](#6-exception-handling)
|
|
13
|
+
7. [Authentication with JWT](#7-authentication-with-jwt)
|
|
14
|
+
8. [Role-Based Authorization](#8-role-based-authorization)
|
|
15
|
+
9. [File Upload and Download](#9-file-upload-and-download)
|
|
16
|
+
10. [Caching with Redis](#10-caching-with-redis)
|
|
17
|
+
11. [Async Processing](#11-async-processing)
|
|
18
|
+
12. [Scheduled Tasks](#12-scheduled-tasks)
|
|
19
|
+
13. [Email Service](#13-email-service)
|
|
20
|
+
14. [Pagination and Sorting](#14-pagination-and-sorting)
|
|
21
|
+
15. [Database Transactions](#15-database-transactions)
|
|
22
|
+
16. [Actuator and Monitoring](#16-actuator-and-monitoring)
|
|
23
|
+
17. [Docker Deployment](#17-docker-deployment)
|
|
24
|
+
18. [API Versioning](#18-api-versioning)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Basic Spring Boot Application
|
|
29
|
+
|
|
30
|
+
**Application Class:**
|
|
31
|
+
|
|
32
|
+
```java
|
|
33
|
+
package com.example.demo;
|
|
34
|
+
|
|
35
|
+
import org.springframework.boot.SpringApplication;
|
|
36
|
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
37
|
+
import org.springframework.context.annotation.Bean;
|
|
38
|
+
import org.springframework.web.client.RestTemplate;
|
|
39
|
+
|
|
40
|
+
@SpringBootApplication
|
|
41
|
+
public class DemoApplication {
|
|
42
|
+
|
|
43
|
+
public static void main(String[] args) {
|
|
44
|
+
SpringApplication.run(DemoApplication.class, args);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Bean
|
|
48
|
+
public RestTemplate restTemplate() {
|
|
49
|
+
return new RestTemplate();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Simple Controller:**
|
|
55
|
+
|
|
56
|
+
```java
|
|
57
|
+
@RestController
|
|
58
|
+
@RequestMapping("/api")
|
|
59
|
+
public class WelcomeController {
|
|
60
|
+
|
|
61
|
+
@Value("${app.name}")
|
|
62
|
+
private String appName;
|
|
63
|
+
|
|
64
|
+
@GetMapping("/welcome")
|
|
65
|
+
public Map<String, Object> welcome() {
|
|
66
|
+
Map<String, Object> response = new HashMap<>();
|
|
67
|
+
response.put("message", "Welcome to " + appName);
|
|
68
|
+
response.put("timestamp", LocalDateTime.now());
|
|
69
|
+
response.put("status", "success");
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@GetMapping("/health")
|
|
74
|
+
public ResponseEntity<String> health() {
|
|
75
|
+
return ResponseEntity.ok("Application is running");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Configuration:**
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
# application.yml
|
|
84
|
+
app:
|
|
85
|
+
name: Spring Boot Demo Application
|
|
86
|
+
|
|
87
|
+
server:
|
|
88
|
+
port: 8080
|
|
89
|
+
|
|
90
|
+
logging:
|
|
91
|
+
level:
|
|
92
|
+
root: INFO
|
|
93
|
+
com.example.demo: DEBUG
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 2. REST API with CRUD Operations
|
|
99
|
+
|
|
100
|
+
**Entity:**
|
|
101
|
+
|
|
102
|
+
```java
|
|
103
|
+
@Entity
|
|
104
|
+
@Table(name = "products")
|
|
105
|
+
public class Product {
|
|
106
|
+
|
|
107
|
+
@Id
|
|
108
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
109
|
+
private Long id;
|
|
110
|
+
|
|
111
|
+
@Column(nullable = false)
|
|
112
|
+
private String name;
|
|
113
|
+
|
|
114
|
+
@Column(length = 1000)
|
|
115
|
+
private String description;
|
|
116
|
+
|
|
117
|
+
@Column(nullable = false)
|
|
118
|
+
private BigDecimal price;
|
|
119
|
+
|
|
120
|
+
@Column(nullable = false)
|
|
121
|
+
private Integer stock;
|
|
122
|
+
|
|
123
|
+
@Column(name = "created_at", nullable = false, updatable = false)
|
|
124
|
+
private LocalDateTime createdAt;
|
|
125
|
+
|
|
126
|
+
@Column(name = "updated_at")
|
|
127
|
+
private LocalDateTime updatedAt;
|
|
128
|
+
|
|
129
|
+
@PrePersist
|
|
130
|
+
protected void onCreate() {
|
|
131
|
+
createdAt = LocalDateTime.now();
|
|
132
|
+
updatedAt = LocalDateTime.now();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@PreUpdate
|
|
136
|
+
protected void onUpdate() {
|
|
137
|
+
updatedAt = LocalDateTime.now();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Constructors, getters, and setters
|
|
141
|
+
public Product() {}
|
|
142
|
+
|
|
143
|
+
public Product(String name, String description, BigDecimal price, Integer stock) {
|
|
144
|
+
this.name = name;
|
|
145
|
+
this.description = description;
|
|
146
|
+
this.price = price;
|
|
147
|
+
this.stock = stock;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Getters and setters omitted for brevity
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Repository:**
|
|
155
|
+
|
|
156
|
+
```java
|
|
157
|
+
@Repository
|
|
158
|
+
public interface ProductRepository extends JpaRepository<Product, Long> {
|
|
159
|
+
List<Product> findByNameContaining(String name);
|
|
160
|
+
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
|
|
161
|
+
List<Product> findByStockLessThan(Integer stock);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Service:**
|
|
166
|
+
|
|
167
|
+
```java
|
|
168
|
+
@Service
|
|
169
|
+
@Transactional
|
|
170
|
+
public class ProductService {
|
|
171
|
+
|
|
172
|
+
private static final Logger logger = LoggerFactory.getLogger(ProductService.class);
|
|
173
|
+
|
|
174
|
+
private final ProductRepository productRepository;
|
|
175
|
+
|
|
176
|
+
public ProductService(ProductRepository productRepository) {
|
|
177
|
+
this.productRepository = productRepository;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Transactional(readOnly = true)
|
|
181
|
+
public List<Product> findAll() {
|
|
182
|
+
logger.debug("Fetching all products");
|
|
183
|
+
return productRepository.findAll();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Transactional(readOnly = true)
|
|
187
|
+
public Optional<Product> findById(Long id) {
|
|
188
|
+
logger.debug("Fetching product with id: {}", id);
|
|
189
|
+
return productRepository.findById(id);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public Product create(Product product) {
|
|
193
|
+
logger.info("Creating new product: {}", product.getName());
|
|
194
|
+
return productRepository.save(product);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public Optional<Product> update(Long id, Product productDetails) {
|
|
198
|
+
logger.info("Updating product with id: {}", id);
|
|
199
|
+
return productRepository.findById(id)
|
|
200
|
+
.map(product -> {
|
|
201
|
+
product.setName(productDetails.getName());
|
|
202
|
+
product.setDescription(productDetails.getDescription());
|
|
203
|
+
product.setPrice(productDetails.getPrice());
|
|
204
|
+
product.setStock(productDetails.getStock());
|
|
205
|
+
return productRepository.save(product);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public boolean delete(Long id) {
|
|
210
|
+
logger.info("Deleting product with id: {}", id);
|
|
211
|
+
return productRepository.findById(id)
|
|
212
|
+
.map(product -> {
|
|
213
|
+
productRepository.delete(product);
|
|
214
|
+
return true;
|
|
215
|
+
})
|
|
216
|
+
.orElse(false);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@Transactional(readOnly = true)
|
|
220
|
+
public List<Product> searchByName(String name) {
|
|
221
|
+
return productRepository.findByNameContaining(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@Transactional(readOnly = true)
|
|
225
|
+
public List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
|
|
226
|
+
return productRepository.findByPriceBetween(minPrice, maxPrice);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Controller:**
|
|
232
|
+
|
|
233
|
+
```java
|
|
234
|
+
@RestController
|
|
235
|
+
@RequestMapping("/api/products")
|
|
236
|
+
public class ProductController {
|
|
237
|
+
|
|
238
|
+
private final ProductService productService;
|
|
239
|
+
|
|
240
|
+
public ProductController(ProductService productService) {
|
|
241
|
+
this.productService = productService;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@GetMapping
|
|
245
|
+
public ResponseEntity<List<Product>> getAllProducts() {
|
|
246
|
+
List<Product> products = productService.findAll();
|
|
247
|
+
return ResponseEntity.ok(products);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@GetMapping("/{id}")
|
|
251
|
+
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
|
|
252
|
+
return productService.findById(id)
|
|
253
|
+
.map(ResponseEntity::ok)
|
|
254
|
+
.orElse(ResponseEntity.notFound().build());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@PostMapping
|
|
258
|
+
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
|
|
259
|
+
Product created = productService.create(product);
|
|
260
|
+
URI location = ServletUriComponentsBuilder
|
|
261
|
+
.fromCurrentRequest()
|
|
262
|
+
.path("/{id}")
|
|
263
|
+
.buildAndExpand(created.getId())
|
|
264
|
+
.toUri();
|
|
265
|
+
return ResponseEntity.created(location).body(created);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@PutMapping("/{id}")
|
|
269
|
+
public ResponseEntity<Product> updateProduct(
|
|
270
|
+
@PathVariable Long id,
|
|
271
|
+
@RequestBody Product product) {
|
|
272
|
+
return productService.update(id, product)
|
|
273
|
+
.map(ResponseEntity::ok)
|
|
274
|
+
.orElse(ResponseEntity.notFound().build());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@DeleteMapping("/{id}")
|
|
278
|
+
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
|
|
279
|
+
if (productService.delete(id)) {
|
|
280
|
+
return ResponseEntity.noContent().build();
|
|
281
|
+
}
|
|
282
|
+
return ResponseEntity.notFound().build();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@GetMapping("/search")
|
|
286
|
+
public ResponseEntity<List<Product>> searchProducts(@RequestParam String name) {
|
|
287
|
+
List<Product> products = productService.searchByName(name);
|
|
288
|
+
return ResponseEntity.ok(products);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@GetMapping("/price-range")
|
|
292
|
+
public ResponseEntity<List<Product>> getProductsByPriceRange(
|
|
293
|
+
@RequestParam BigDecimal min,
|
|
294
|
+
@RequestParam BigDecimal max) {
|
|
295
|
+
List<Product> products = productService.findByPriceRange(min, max);
|
|
296
|
+
return ResponseEntity.ok(products);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 3. Database Integration with JPA
|
|
304
|
+
|
|
305
|
+
**Complex Entity with Relationships:**
|
|
306
|
+
|
|
307
|
+
```java
|
|
308
|
+
@Entity
|
|
309
|
+
@Table(name = "orders")
|
|
310
|
+
public class Order {
|
|
311
|
+
|
|
312
|
+
@Id
|
|
313
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
314
|
+
private Long id;
|
|
315
|
+
|
|
316
|
+
@Column(name = "order_number", nullable = false, unique = true)
|
|
317
|
+
private String orderNumber;
|
|
318
|
+
|
|
319
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
320
|
+
@JoinColumn(name = "customer_id", nullable = false)
|
|
321
|
+
private Customer customer;
|
|
322
|
+
|
|
323
|
+
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
324
|
+
private List<OrderItem> items = new ArrayList<>();
|
|
325
|
+
|
|
326
|
+
@Enumerated(EnumType.STRING)
|
|
327
|
+
@Column(nullable = false)
|
|
328
|
+
private OrderStatus status;
|
|
329
|
+
|
|
330
|
+
@Column(nullable = false)
|
|
331
|
+
private BigDecimal totalAmount;
|
|
332
|
+
|
|
333
|
+
@Column(name = "created_at", nullable = false, updatable = false)
|
|
334
|
+
private LocalDateTime createdAt;
|
|
335
|
+
|
|
336
|
+
@Column(name = "updated_at")
|
|
337
|
+
private LocalDateTime updatedAt;
|
|
338
|
+
|
|
339
|
+
@PrePersist
|
|
340
|
+
protected void onCreate() {
|
|
341
|
+
createdAt = LocalDateTime.now();
|
|
342
|
+
updatedAt = LocalDateTime.now();
|
|
343
|
+
if (orderNumber == null) {
|
|
344
|
+
orderNumber = generateOrderNumber();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@PreUpdate
|
|
349
|
+
protected void onUpdate() {
|
|
350
|
+
updatedAt = LocalDateTime.now();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Helper methods
|
|
354
|
+
public void addItem(OrderItem item) {
|
|
355
|
+
items.add(item);
|
|
356
|
+
item.setOrder(this);
|
|
357
|
+
calculateTotal();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public void removeItem(OrderItem item) {
|
|
361
|
+
items.remove(item);
|
|
362
|
+
item.setOrder(null);
|
|
363
|
+
calculateTotal();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private void calculateTotal() {
|
|
367
|
+
totalAmount = items.stream()
|
|
368
|
+
.map(OrderItem::getSubtotal)
|
|
369
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private String generateOrderNumber() {
|
|
373
|
+
return "ORD-" + System.currentTimeMillis();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Getters and setters
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@Entity
|
|
380
|
+
@Table(name = "order_items")
|
|
381
|
+
public class OrderItem {
|
|
382
|
+
|
|
383
|
+
@Id
|
|
384
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
385
|
+
private Long id;
|
|
386
|
+
|
|
387
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
388
|
+
@JoinColumn(name = "order_id")
|
|
389
|
+
private Order order;
|
|
390
|
+
|
|
391
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
392
|
+
@JoinColumn(name = "product_id", nullable = false)
|
|
393
|
+
private Product product;
|
|
394
|
+
|
|
395
|
+
@Column(nullable = false)
|
|
396
|
+
private Integer quantity;
|
|
397
|
+
|
|
398
|
+
@Column(nullable = false)
|
|
399
|
+
private BigDecimal price;
|
|
400
|
+
|
|
401
|
+
@Column(nullable = false)
|
|
402
|
+
private BigDecimal subtotal;
|
|
403
|
+
|
|
404
|
+
@PrePersist
|
|
405
|
+
@PreUpdate
|
|
406
|
+
private void calculateSubtotal() {
|
|
407
|
+
if (price != null && quantity != null) {
|
|
408
|
+
subtotal = price.multiply(BigDecimal.valueOf(quantity));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Getters and setters
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
@Entity
|
|
416
|
+
@Table(name = "customers")
|
|
417
|
+
public class Customer {
|
|
418
|
+
|
|
419
|
+
@Id
|
|
420
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
421
|
+
private Long id;
|
|
422
|
+
|
|
423
|
+
@Column(nullable = false)
|
|
424
|
+
private String name;
|
|
425
|
+
|
|
426
|
+
@Column(nullable = false, unique = true)
|
|
427
|
+
private String email;
|
|
428
|
+
|
|
429
|
+
@Column(nullable = false)
|
|
430
|
+
private String phone;
|
|
431
|
+
|
|
432
|
+
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
|
|
433
|
+
private List<Order> orders = new ArrayList<>();
|
|
434
|
+
|
|
435
|
+
// Getters and setters
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
public enum OrderStatus {
|
|
439
|
+
PENDING,
|
|
440
|
+
CONFIRMED,
|
|
441
|
+
PROCESSING,
|
|
442
|
+
SHIPPED,
|
|
443
|
+
DELIVERED,
|
|
444
|
+
CANCELLED
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Repository with Custom Queries:**
|
|
449
|
+
|
|
450
|
+
```java
|
|
451
|
+
@Repository
|
|
452
|
+
public interface OrderRepository extends JpaRepository<Order, Long> {
|
|
453
|
+
|
|
454
|
+
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId")
|
|
455
|
+
List<Order> findByCustomerId(@Param("customerId") Long customerId);
|
|
456
|
+
|
|
457
|
+
@Query("SELECT o FROM Order o WHERE o.status = :status")
|
|
458
|
+
List<Order> findByStatus(@Param("status") OrderStatus status);
|
|
459
|
+
|
|
460
|
+
@Query("SELECT o FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate")
|
|
461
|
+
List<Order> findByDateRange(
|
|
462
|
+
@Param("startDate") LocalDateTime startDate,
|
|
463
|
+
@Param("endDate") LocalDateTime endDate
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
|
|
467
|
+
Optional<Order> findByIdWithItems(@Param("id") Long id);
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## 4. Custom Queries and Specifications
|
|
474
|
+
|
|
475
|
+
**Specification Pattern:**
|
|
476
|
+
|
|
477
|
+
```java
|
|
478
|
+
public class ProductSpecification {
|
|
479
|
+
|
|
480
|
+
public static Specification<Product> hasName(String name) {
|
|
481
|
+
return (root, query, builder) ->
|
|
482
|
+
name == null ? null : builder.like(
|
|
483
|
+
builder.lower(root.get("name")),
|
|
484
|
+
"%" + name.toLowerCase() + "%"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public static Specification<Product> hasPriceGreaterThan(BigDecimal price) {
|
|
489
|
+
return (root, query, builder) ->
|
|
490
|
+
price == null ? null : builder.greaterThanOrEqualTo(root.get("price"), price);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
public static Specification<Product> hasPriceLessThan(BigDecimal price) {
|
|
494
|
+
return (root, query, builder) ->
|
|
495
|
+
price == null ? null : builder.lessThanOrEqualTo(root.get("price"), price);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
public static Specification<Product> hasStockGreaterThan(Integer stock) {
|
|
499
|
+
return (root, query, builder) ->
|
|
500
|
+
stock == null ? null : builder.greaterThan(root.get("stock"), stock);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@Repository
|
|
505
|
+
public interface ProductRepository extends JpaRepository<Product, Long>,
|
|
506
|
+
JpaSpecificationExecutor<Product> {
|
|
507
|
+
// Standard methods
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
@Service
|
|
511
|
+
public class ProductSearchService {
|
|
512
|
+
|
|
513
|
+
private final ProductRepository productRepository;
|
|
514
|
+
|
|
515
|
+
public ProductSearchService(ProductRepository productRepository) {
|
|
516
|
+
this.productRepository = productRepository;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
public List<Product> searchProducts(String name, BigDecimal minPrice,
|
|
520
|
+
BigDecimal maxPrice, Integer minStock) {
|
|
521
|
+
Specification<Product> spec = Specification.where(null);
|
|
522
|
+
|
|
523
|
+
if (name != null) {
|
|
524
|
+
spec = spec.and(ProductSpecification.hasName(name));
|
|
525
|
+
}
|
|
526
|
+
if (minPrice != null) {
|
|
527
|
+
spec = spec.and(ProductSpecification.hasPriceGreaterThan(minPrice));
|
|
528
|
+
}
|
|
529
|
+
if (maxPrice != null) {
|
|
530
|
+
spec = spec.and(ProductSpecification.hasPriceLessThan(maxPrice));
|
|
531
|
+
}
|
|
532
|
+
if (minStock != null) {
|
|
533
|
+
spec = spec.and(ProductSpecification.hasStockGreaterThan(minStock));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return productRepository.findAll(spec);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Native Query Example:**
|
|
542
|
+
|
|
543
|
+
```java
|
|
544
|
+
@Repository
|
|
545
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
546
|
+
|
|
547
|
+
@Query(value = """
|
|
548
|
+
SELECT u.* FROM users u
|
|
549
|
+
LEFT JOIN orders o ON u.id = o.customer_id
|
|
550
|
+
WHERE o.created_at >= :startDate
|
|
551
|
+
GROUP BY u.id
|
|
552
|
+
HAVING COUNT(o.id) >= :minOrders
|
|
553
|
+
""", nativeQuery = true)
|
|
554
|
+
List<User> findActiveCustomers(
|
|
555
|
+
@Param("startDate") LocalDateTime startDate,
|
|
556
|
+
@Param("minOrders") int minOrders
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
@Modifying
|
|
560
|
+
@Query(value = "UPDATE users SET last_login = :loginTime WHERE id = :userId",
|
|
561
|
+
nativeQuery = true)
|
|
562
|
+
void updateLastLogin(@Param("userId") Long userId,
|
|
563
|
+
@Param("loginTime") LocalDateTime loginTime);
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## 5. Request Validation
|
|
570
|
+
|
|
571
|
+
**Entity with Validation:**
|
|
572
|
+
|
|
573
|
+
```java
|
|
574
|
+
@Entity
|
|
575
|
+
@Table(name = "users")
|
|
576
|
+
public class User {
|
|
577
|
+
|
|
578
|
+
@Id
|
|
579
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
580
|
+
private Long id;
|
|
581
|
+
|
|
582
|
+
@NotBlank(message = "Name is required")
|
|
583
|
+
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
|
|
584
|
+
private String name;
|
|
585
|
+
|
|
586
|
+
@NotBlank(message = "Email is required")
|
|
587
|
+
@Email(message = "Email should be valid")
|
|
588
|
+
@Column(unique = true)
|
|
589
|
+
private String email;
|
|
590
|
+
|
|
591
|
+
@NotBlank(message = "Password is required")
|
|
592
|
+
@Size(min = 8, message = "Password must be at least 8 characters")
|
|
593
|
+
@Pattern(
|
|
594
|
+
regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$",
|
|
595
|
+
message = "Password must contain at least one digit, one lowercase, one uppercase, and one special character"
|
|
596
|
+
)
|
|
597
|
+
private String password;
|
|
598
|
+
|
|
599
|
+
@NotNull(message = "Age is required")
|
|
600
|
+
@Min(value = 18, message = "Age must be at least 18")
|
|
601
|
+
@Max(value = 120, message = "Age must be less than 120")
|
|
602
|
+
private Integer age;
|
|
603
|
+
|
|
604
|
+
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number")
|
|
605
|
+
private String phone;
|
|
606
|
+
|
|
607
|
+
// Getters and setters
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Custom Validator:**
|
|
612
|
+
|
|
613
|
+
```java
|
|
614
|
+
@Documented
|
|
615
|
+
@Constraint(validatedBy = UniqueEmailValidator.class)
|
|
616
|
+
@Target({ElementType.FIELD})
|
|
617
|
+
@Retention(RetentionPolicy.RUNTIME)
|
|
618
|
+
public @interface UniqueEmail {
|
|
619
|
+
String message() default "Email already exists";
|
|
620
|
+
Class<?>[] groups() default {};
|
|
621
|
+
Class<? extends Payload>[] payload() default {};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
@Component
|
|
625
|
+
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
|
|
626
|
+
|
|
627
|
+
private final UserRepository userRepository;
|
|
628
|
+
|
|
629
|
+
public UniqueEmailValidator(UserRepository userRepository) {
|
|
630
|
+
this.userRepository = userRepository;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
@Override
|
|
634
|
+
public boolean isValid(String email, ConstraintValidatorContext context) {
|
|
635
|
+
if (email == null) {
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
return !userRepository.existsByEmail(email);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Usage
|
|
643
|
+
public class UserDTO {
|
|
644
|
+
@UniqueEmail
|
|
645
|
+
@Email
|
|
646
|
+
private String email;
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**Validation Groups:**
|
|
651
|
+
|
|
652
|
+
```java
|
|
653
|
+
public interface CreateGroup {}
|
|
654
|
+
public interface UpdateGroup {}
|
|
655
|
+
|
|
656
|
+
@Entity
|
|
657
|
+
public class User {
|
|
658
|
+
|
|
659
|
+
@Null(groups = CreateGroup.class, message = "ID must be null when creating")
|
|
660
|
+
@NotNull(groups = UpdateGroup.class, message = "ID is required when updating")
|
|
661
|
+
private Long id;
|
|
662
|
+
|
|
663
|
+
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
|
|
664
|
+
private String name;
|
|
665
|
+
|
|
666
|
+
@NotBlank(groups = CreateGroup.class)
|
|
667
|
+
@Null(groups = UpdateGroup.class, message = "Password cannot be updated")
|
|
668
|
+
private String password;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
@RestController
|
|
672
|
+
@RequestMapping("/api/users")
|
|
673
|
+
public class UserController {
|
|
674
|
+
|
|
675
|
+
@PostMapping
|
|
676
|
+
public ResponseEntity<User> createUser(
|
|
677
|
+
@Validated(CreateGroup.class) @RequestBody User user) {
|
|
678
|
+
// Create logic
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
@PutMapping("/{id}")
|
|
682
|
+
public ResponseEntity<User> updateUser(
|
|
683
|
+
@PathVariable Long id,
|
|
684
|
+
@Validated(UpdateGroup.class) @RequestBody User user) {
|
|
685
|
+
// Update logic
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## 6. Exception Handling
|
|
693
|
+
|
|
694
|
+
**Custom Exceptions:**
|
|
695
|
+
|
|
696
|
+
```java
|
|
697
|
+
public class ResourceNotFoundException extends RuntimeException {
|
|
698
|
+
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
|
|
699
|
+
super(String.format("%s not found with %s: '%s'", resourceName, fieldName, fieldValue));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
public class BadRequestException extends RuntimeException {
|
|
704
|
+
public BadRequestException(String message) {
|
|
705
|
+
super(message);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
public class UnauthorizedException extends RuntimeException {
|
|
710
|
+
public UnauthorizedException(String message) {
|
|
711
|
+
super(message);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
**Error Response DTO:**
|
|
717
|
+
|
|
718
|
+
```java
|
|
719
|
+
public class ErrorResponse {
|
|
720
|
+
private LocalDateTime timestamp;
|
|
721
|
+
private int status;
|
|
722
|
+
private String error;
|
|
723
|
+
private String message;
|
|
724
|
+
private String path;
|
|
725
|
+
private Map<String, String> validationErrors;
|
|
726
|
+
|
|
727
|
+
public ErrorResponse(int status, String error, String message, String path) {
|
|
728
|
+
this.timestamp = LocalDateTime.now();
|
|
729
|
+
this.status = status;
|
|
730
|
+
this.error = error;
|
|
731
|
+
this.message = message;
|
|
732
|
+
this.path = path;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Getters and setters
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Global Exception Handler:**
|
|
740
|
+
|
|
741
|
+
```java
|
|
742
|
+
@RestControllerAdvice
|
|
743
|
+
public class GlobalExceptionHandler {
|
|
744
|
+
|
|
745
|
+
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
746
|
+
|
|
747
|
+
@ExceptionHandler(ResourceNotFoundException.class)
|
|
748
|
+
public ResponseEntity<ErrorResponse> handleResourceNotFound(
|
|
749
|
+
ResourceNotFoundException ex,
|
|
750
|
+
WebRequest request) {
|
|
751
|
+
logger.error("Resource not found: {}", ex.getMessage());
|
|
752
|
+
|
|
753
|
+
ErrorResponse error = new ErrorResponse(
|
|
754
|
+
HttpStatus.NOT_FOUND.value(),
|
|
755
|
+
"Not Found",
|
|
756
|
+
ex.getMessage(),
|
|
757
|
+
request.getDescription(false).replace("uri=", "")
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
@ExceptionHandler(BadRequestException.class)
|
|
764
|
+
public ResponseEntity<ErrorResponse> handleBadRequest(
|
|
765
|
+
BadRequestException ex,
|
|
766
|
+
WebRequest request) {
|
|
767
|
+
logger.error("Bad request: {}", ex.getMessage());
|
|
768
|
+
|
|
769
|
+
ErrorResponse error = new ErrorResponse(
|
|
770
|
+
HttpStatus.BAD_REQUEST.value(),
|
|
771
|
+
"Bad Request",
|
|
772
|
+
ex.getMessage(),
|
|
773
|
+
request.getDescription(false).replace("uri=", "")
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
780
|
+
public ResponseEntity<ErrorResponse> handleValidationErrors(
|
|
781
|
+
MethodArgumentNotValidException ex,
|
|
782
|
+
WebRequest request) {
|
|
783
|
+
logger.error("Validation error: {}", ex.getMessage());
|
|
784
|
+
|
|
785
|
+
Map<String, String> validationErrors = new HashMap<>();
|
|
786
|
+
ex.getBindingResult().getFieldErrors().forEach(error ->
|
|
787
|
+
validationErrors.put(error.getField(), error.getDefaultMessage())
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
ErrorResponse error = new ErrorResponse(
|
|
791
|
+
HttpStatus.BAD_REQUEST.value(),
|
|
792
|
+
"Validation Failed",
|
|
793
|
+
"Input validation failed",
|
|
794
|
+
request.getDescription(false).replace("uri=", "")
|
|
795
|
+
);
|
|
796
|
+
error.setValidationErrors(validationErrors);
|
|
797
|
+
|
|
798
|
+
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
@ExceptionHandler(DataIntegrityViolationException.class)
|
|
802
|
+
public ResponseEntity<ErrorResponse> handleDataIntegrityViolation(
|
|
803
|
+
DataIntegrityViolationException ex,
|
|
804
|
+
WebRequest request) {
|
|
805
|
+
logger.error("Data integrity violation: {}", ex.getMessage());
|
|
806
|
+
|
|
807
|
+
String message = "Database constraint violation";
|
|
808
|
+
if (ex.getCause() instanceof ConstraintViolationException) {
|
|
809
|
+
message = "Duplicate entry or constraint violation";
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
ErrorResponse error = new ErrorResponse(
|
|
813
|
+
HttpStatus.CONFLICT.value(),
|
|
814
|
+
"Conflict",
|
|
815
|
+
message,
|
|
816
|
+
request.getDescription(false).replace("uri=", "")
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
@ExceptionHandler(Exception.class)
|
|
823
|
+
public ResponseEntity<ErrorResponse> handleGlobalException(
|
|
824
|
+
Exception ex,
|
|
825
|
+
WebRequest request) {
|
|
826
|
+
logger.error("Unexpected error: ", ex);
|
|
827
|
+
|
|
828
|
+
ErrorResponse error = new ErrorResponse(
|
|
829
|
+
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
|
830
|
+
"Internal Server Error",
|
|
831
|
+
"An unexpected error occurred",
|
|
832
|
+
request.getDescription(false).replace("uri=", "")
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
842
|
+
## 7. Authentication with JWT
|
|
843
|
+
|
|
844
|
+
**JWT Utility Class:**
|
|
845
|
+
|
|
846
|
+
```java
|
|
847
|
+
@Component
|
|
848
|
+
public class JwtTokenProvider {
|
|
849
|
+
|
|
850
|
+
@Value("${app.jwt.secret}")
|
|
851
|
+
private String jwtSecret;
|
|
852
|
+
|
|
853
|
+
@Value("${app.jwt.expiration-ms}")
|
|
854
|
+
private long jwtExpirationMs;
|
|
855
|
+
|
|
856
|
+
public String generateToken(UserDetails userDetails) {
|
|
857
|
+
Date now = new Date();
|
|
858
|
+
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
|
|
859
|
+
|
|
860
|
+
return Jwts.builder()
|
|
861
|
+
.setSubject(userDetails.getUsername())
|
|
862
|
+
.setIssuedAt(now)
|
|
863
|
+
.setExpiration(expiryDate)
|
|
864
|
+
.signWith(SignatureAlgorithm.HS512, jwtSecret)
|
|
865
|
+
.compact();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
public String getUsernameFromToken(String token) {
|
|
869
|
+
Claims claims = Jwts.parser()
|
|
870
|
+
.setSigningKey(jwtSecret)
|
|
871
|
+
.parseClaimsJws(token)
|
|
872
|
+
.getBody();
|
|
873
|
+
|
|
874
|
+
return claims.getSubject();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
public boolean validateToken(String token) {
|
|
878
|
+
try {
|
|
879
|
+
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
|
|
880
|
+
return true;
|
|
881
|
+
} catch (SignatureException ex) {
|
|
882
|
+
logger.error("Invalid JWT signature");
|
|
883
|
+
} catch (MalformedJwtException ex) {
|
|
884
|
+
logger.error("Invalid JWT token");
|
|
885
|
+
} catch (ExpiredJwtException ex) {
|
|
886
|
+
logger.error("Expired JWT token");
|
|
887
|
+
} catch (UnsupportedJwtException ex) {
|
|
888
|
+
logger.error("Unsupported JWT token");
|
|
889
|
+
} catch (IllegalArgumentException ex) {
|
|
890
|
+
logger.error("JWT claims string is empty");
|
|
891
|
+
}
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**JWT Authentication Filter:**
|
|
898
|
+
|
|
899
|
+
```java
|
|
900
|
+
@Component
|
|
901
|
+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
902
|
+
|
|
903
|
+
private final JwtTokenProvider tokenProvider;
|
|
904
|
+
private final UserDetailsService userDetailsService;
|
|
905
|
+
|
|
906
|
+
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
|
|
907
|
+
UserDetailsService userDetailsService) {
|
|
908
|
+
this.tokenProvider = tokenProvider;
|
|
909
|
+
this.userDetailsService = userDetailsService;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
@Override
|
|
913
|
+
protected void doFilterInternal(HttpServletRequest request,
|
|
914
|
+
HttpServletResponse response,
|
|
915
|
+
FilterChain filterChain)
|
|
916
|
+
throws ServletException, IOException {
|
|
917
|
+
try {
|
|
918
|
+
String jwt = getJwtFromRequest(request);
|
|
919
|
+
|
|
920
|
+
if (jwt != null && tokenProvider.validateToken(jwt)) {
|
|
921
|
+
String username = tokenProvider.getUsernameFromToken(jwt);
|
|
922
|
+
|
|
923
|
+
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
|
924
|
+
UsernamePasswordAuthenticationToken authentication =
|
|
925
|
+
new UsernamePasswordAuthenticationToken(
|
|
926
|
+
userDetails,
|
|
927
|
+
null,
|
|
928
|
+
userDetails.getAuthorities()
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
authentication.setDetails(
|
|
932
|
+
new WebAuthenticationDetailsSource().buildDetails(request)
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
936
|
+
}
|
|
937
|
+
} catch (Exception ex) {
|
|
938
|
+
logger.error("Could not set user authentication in security context", ex);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
filterChain.doFilter(request, response);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
private String getJwtFromRequest(HttpServletRequest request) {
|
|
945
|
+
String bearerToken = request.getHeader("Authorization");
|
|
946
|
+
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
|
947
|
+
return bearerToken.substring(7);
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**Authentication Controller:**
|
|
955
|
+
|
|
956
|
+
```java
|
|
957
|
+
@RestController
|
|
958
|
+
@RequestMapping("/api/auth")
|
|
959
|
+
public class AuthController {
|
|
960
|
+
|
|
961
|
+
private final AuthenticationManager authenticationManager;
|
|
962
|
+
private final UserService userService;
|
|
963
|
+
private final JwtTokenProvider tokenProvider;
|
|
964
|
+
private final PasswordEncoder passwordEncoder;
|
|
965
|
+
|
|
966
|
+
@PostMapping("/register")
|
|
967
|
+
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
|
|
968
|
+
if (userService.existsByEmail(signUpRequest.getEmail())) {
|
|
969
|
+
return ResponseEntity.badRequest()
|
|
970
|
+
.body(new ApiResponse(false, "Email already in use"));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
User user = new User();
|
|
974
|
+
user.setName(signUpRequest.getName());
|
|
975
|
+
user.setEmail(signUpRequest.getEmail());
|
|
976
|
+
user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));
|
|
977
|
+
|
|
978
|
+
User result = userService.save(user);
|
|
979
|
+
|
|
980
|
+
return ResponseEntity.ok(new ApiResponse(true, "User registered successfully"));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
@PostMapping("/login")
|
|
984
|
+
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
|
|
985
|
+
Authentication authentication = authenticationManager.authenticate(
|
|
986
|
+
new UsernamePasswordAuthenticationToken(
|
|
987
|
+
loginRequest.getEmail(),
|
|
988
|
+
loginRequest.getPassword()
|
|
989
|
+
)
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
993
|
+
|
|
994
|
+
String jwt = tokenProvider.generateToken(
|
|
995
|
+
(UserDetails) authentication.getPrincipal()
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## 8. Role-Based Authorization
|
|
1006
|
+
|
|
1007
|
+
**User with Roles:**
|
|
1008
|
+
|
|
1009
|
+
```java
|
|
1010
|
+
@Entity
|
|
1011
|
+
@Table(name = "users")
|
|
1012
|
+
public class User {
|
|
1013
|
+
|
|
1014
|
+
@Id
|
|
1015
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
1016
|
+
private Long id;
|
|
1017
|
+
|
|
1018
|
+
private String email;
|
|
1019
|
+
private String password;
|
|
1020
|
+
|
|
1021
|
+
@ManyToMany(fetch = FetchType.EAGER)
|
|
1022
|
+
@JoinTable(
|
|
1023
|
+
name = "user_roles",
|
|
1024
|
+
joinColumns = @JoinColumn(name = "user_id"),
|
|
1025
|
+
inverseJoinColumns = @JoinColumn(name = "role_id")
|
|
1026
|
+
)
|
|
1027
|
+
private Set<Role> roles = new HashSet<>();
|
|
1028
|
+
|
|
1029
|
+
// Getters and setters
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
@Entity
|
|
1033
|
+
@Table(name = "roles")
|
|
1034
|
+
public class Role {
|
|
1035
|
+
|
|
1036
|
+
@Id
|
|
1037
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
1038
|
+
private Long id;
|
|
1039
|
+
|
|
1040
|
+
@Enumerated(EnumType.STRING)
|
|
1041
|
+
@Column(length = 20)
|
|
1042
|
+
private RoleName name;
|
|
1043
|
+
|
|
1044
|
+
// Getters and setters
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
public enum RoleName {
|
|
1048
|
+
ROLE_USER,
|
|
1049
|
+
ROLE_ADMIN,
|
|
1050
|
+
ROLE_MODERATOR
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
**UserDetails Implementation:**
|
|
1055
|
+
|
|
1056
|
+
```java
|
|
1057
|
+
public class UserPrincipal implements UserDetails {
|
|
1058
|
+
|
|
1059
|
+
private Long id;
|
|
1060
|
+
private String name;
|
|
1061
|
+
private String email;
|
|
1062
|
+
private String password;
|
|
1063
|
+
private Collection<? extends GrantedAuthority> authorities;
|
|
1064
|
+
|
|
1065
|
+
public UserPrincipal(Long id, String name, String email, String password,
|
|
1066
|
+
Collection<? extends GrantedAuthority> authorities) {
|
|
1067
|
+
this.id = id;
|
|
1068
|
+
this.name = name;
|
|
1069
|
+
this.email = email;
|
|
1070
|
+
this.password = password;
|
|
1071
|
+
this.authorities = authorities;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
public static UserPrincipal create(User user) {
|
|
1075
|
+
List<GrantedAuthority> authorities = user.getRoles().stream()
|
|
1076
|
+
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
|
|
1077
|
+
.collect(Collectors.toList());
|
|
1078
|
+
|
|
1079
|
+
return new UserPrincipal(
|
|
1080
|
+
user.getId(),
|
|
1081
|
+
user.getName(),
|
|
1082
|
+
user.getEmail(),
|
|
1083
|
+
user.getPassword(),
|
|
1084
|
+
authorities
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
@Override
|
|
1089
|
+
public Collection<? extends GrantedAuthority> getAuthorities() {
|
|
1090
|
+
return authorities;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
@Override
|
|
1094
|
+
public String getPassword() {
|
|
1095
|
+
return password;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
@Override
|
|
1099
|
+
public String getUsername() {
|
|
1100
|
+
return email;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
@Override
|
|
1104
|
+
public boolean isAccountNonExpired() {
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
@Override
|
|
1109
|
+
public boolean isAccountNonLocked() {
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
@Override
|
|
1114
|
+
public boolean isCredentialsNonExpired() {
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
@Override
|
|
1119
|
+
public boolean isEnabled() {
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Getters
|
|
1124
|
+
}
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
**Security Configuration:**
|
|
1128
|
+
|
|
1129
|
+
```java
|
|
1130
|
+
@Configuration
|
|
1131
|
+
@EnableWebSecurity
|
|
1132
|
+
@EnableMethodSecurity(prePostEnabled = true)
|
|
1133
|
+
public class SecurityConfig {
|
|
1134
|
+
|
|
1135
|
+
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
|
1136
|
+
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
|
1137
|
+
|
|
1138
|
+
@Bean
|
|
1139
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
1140
|
+
http
|
|
1141
|
+
.csrf().disable()
|
|
1142
|
+
.cors()
|
|
1143
|
+
.and()
|
|
1144
|
+
.exceptionHandling()
|
|
1145
|
+
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
|
|
1146
|
+
.and()
|
|
1147
|
+
.sessionManagement()
|
|
1148
|
+
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
1149
|
+
.and()
|
|
1150
|
+
.authorizeHttpRequests(auth -> auth
|
|
1151
|
+
.requestMatchers("/api/auth/**").permitAll()
|
|
1152
|
+
.requestMatchers("/api/public/**").permitAll()
|
|
1153
|
+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
1154
|
+
.requestMatchers("/api/moderator/**").hasAnyRole("ADMIN", "MODERATOR")
|
|
1155
|
+
.anyRequest().authenticated()
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
http.addFilterBefore(jwtAuthenticationFilter,
|
|
1159
|
+
UsernamePasswordAuthenticationFilter.class);
|
|
1160
|
+
|
|
1161
|
+
return http.build();
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
@Bean
|
|
1165
|
+
public PasswordEncoder passwordEncoder() {
|
|
1166
|
+
return new BCryptPasswordEncoder();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
@Bean
|
|
1170
|
+
public AuthenticationManager authenticationManager(
|
|
1171
|
+
AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
|
1172
|
+
return authenticationConfiguration.getAuthenticationManager();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
**Method-Level Security:**
|
|
1178
|
+
|
|
1179
|
+
```java
|
|
1180
|
+
@RestController
|
|
1181
|
+
@RequestMapping("/api/users")
|
|
1182
|
+
public class UserController {
|
|
1183
|
+
|
|
1184
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
1185
|
+
@GetMapping
|
|
1186
|
+
public List<User> getAllUsers() {
|
|
1187
|
+
return userService.findAll();
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
@PreAuthorize("hasRole('USER')")
|
|
1191
|
+
@GetMapping("/me")
|
|
1192
|
+
public User getCurrentUser(@CurrentUser UserPrincipal currentUser) {
|
|
1193
|
+
return userService.findById(currentUser.getId());
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
@PreAuthorize("hasRole('ADMIN') or #id == principal.id")
|
|
1197
|
+
@PutMapping("/{id}")
|
|
1198
|
+
public User updateUser(@PathVariable Long id, @RequestBody User user) {
|
|
1199
|
+
return userService.update(id, user);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
1203
|
+
@DeleteMapping("/{id}")
|
|
1204
|
+
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
|
|
1205
|
+
userService.delete(id);
|
|
1206
|
+
return ResponseEntity.ok().build();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
---
|
|
1212
|
+
|
|
1213
|
+
## 9. File Upload and Download
|
|
1214
|
+
|
|
1215
|
+
**File Storage Service:**
|
|
1216
|
+
|
|
1217
|
+
```java
|
|
1218
|
+
@Service
|
|
1219
|
+
public class FileStorageService {
|
|
1220
|
+
|
|
1221
|
+
private final Path fileStorageLocation;
|
|
1222
|
+
|
|
1223
|
+
@Autowired
|
|
1224
|
+
public FileStorageService(@Value("${file.upload-dir}") String uploadDir) {
|
|
1225
|
+
this.fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
|
|
1226
|
+
|
|
1227
|
+
try {
|
|
1228
|
+
Files.createDirectories(this.fileStorageLocation);
|
|
1229
|
+
} catch (Exception ex) {
|
|
1230
|
+
throw new RuntimeException("Could not create upload directory", ex);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
public String storeFile(MultipartFile file) {
|
|
1235
|
+
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
|
|
1236
|
+
|
|
1237
|
+
try {
|
|
1238
|
+
if (fileName.contains("..")) {
|
|
1239
|
+
throw new BadRequestException("Invalid file path: " + fileName);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
String uniqueFileName = System.currentTimeMillis() + "_" + fileName;
|
|
1243
|
+
Path targetLocation = this.fileStorageLocation.resolve(uniqueFileName);
|
|
1244
|
+
Files.copy(file.getInputStream(), targetLocation,
|
|
1245
|
+
StandardCopyOption.REPLACE_EXISTING);
|
|
1246
|
+
|
|
1247
|
+
return uniqueFileName;
|
|
1248
|
+
} catch (IOException ex) {
|
|
1249
|
+
throw new RuntimeException("Could not store file " + fileName, ex);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
public Resource loadFileAsResource(String fileName) {
|
|
1254
|
+
try {
|
|
1255
|
+
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
|
|
1256
|
+
Resource resource = new UrlResource(filePath.toUri());
|
|
1257
|
+
|
|
1258
|
+
if (resource.exists()) {
|
|
1259
|
+
return resource;
|
|
1260
|
+
} else {
|
|
1261
|
+
throw new ResourceNotFoundException("File", "name", fileName);
|
|
1262
|
+
}
|
|
1263
|
+
} catch (MalformedURLException ex) {
|
|
1264
|
+
throw new ResourceNotFoundException("File", "name", fileName);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
public void deleteFile(String fileName) {
|
|
1269
|
+
try {
|
|
1270
|
+
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
|
|
1271
|
+
Files.deleteIfExists(filePath);
|
|
1272
|
+
} catch (IOException ex) {
|
|
1273
|
+
throw new RuntimeException("Could not delete file " + fileName, ex);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
**File Controller:**
|
|
1280
|
+
|
|
1281
|
+
```java
|
|
1282
|
+
@RestController
|
|
1283
|
+
@RequestMapping("/api/files")
|
|
1284
|
+
public class FileController {
|
|
1285
|
+
|
|
1286
|
+
private final FileStorageService fileStorageService;
|
|
1287
|
+
|
|
1288
|
+
public FileController(FileStorageService fileStorageService) {
|
|
1289
|
+
this.fileStorageService = fileStorageService;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
@PostMapping("/upload")
|
|
1293
|
+
public ResponseEntity<UploadFileResponse> uploadFile(
|
|
1294
|
+
@RequestParam("file") MultipartFile file) {
|
|
1295
|
+
String fileName = fileStorageService.storeFile(file);
|
|
1296
|
+
|
|
1297
|
+
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
|
|
1298
|
+
.path("/api/files/download/")
|
|
1299
|
+
.path(fileName)
|
|
1300
|
+
.toUriString();
|
|
1301
|
+
|
|
1302
|
+
return ResponseEntity.ok(new UploadFileResponse(
|
|
1303
|
+
fileName,
|
|
1304
|
+
fileDownloadUri,
|
|
1305
|
+
file.getContentType(),
|
|
1306
|
+
file.getSize()
|
|
1307
|
+
));
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
@PostMapping("/upload-multiple")
|
|
1311
|
+
public ResponseEntity<List<UploadFileResponse>> uploadMultipleFiles(
|
|
1312
|
+
@RequestParam("files") MultipartFile[] files) {
|
|
1313
|
+
List<UploadFileResponse> responses = Arrays.stream(files)
|
|
1314
|
+
.map(file -> {
|
|
1315
|
+
String fileName = fileStorageService.storeFile(file);
|
|
1316
|
+
String fileDownloadUri = ServletUriComponentsBuilder
|
|
1317
|
+
.fromCurrentContextPath()
|
|
1318
|
+
.path("/api/files/download/")
|
|
1319
|
+
.path(fileName)
|
|
1320
|
+
.toUriString();
|
|
1321
|
+
|
|
1322
|
+
return new UploadFileResponse(
|
|
1323
|
+
fileName,
|
|
1324
|
+
fileDownloadUri,
|
|
1325
|
+
file.getContentType(),
|
|
1326
|
+
file.getSize()
|
|
1327
|
+
);
|
|
1328
|
+
})
|
|
1329
|
+
.collect(Collectors.toList());
|
|
1330
|
+
|
|
1331
|
+
return ResponseEntity.ok(responses);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
@GetMapping("/download/{fileName:.+}")
|
|
1335
|
+
public ResponseEntity<Resource> downloadFile(
|
|
1336
|
+
@PathVariable String fileName,
|
|
1337
|
+
HttpServletRequest request) {
|
|
1338
|
+
Resource resource = fileStorageService.loadFileAsResource(fileName);
|
|
1339
|
+
|
|
1340
|
+
String contentType = null;
|
|
1341
|
+
try {
|
|
1342
|
+
contentType = request.getServletContext()
|
|
1343
|
+
.getMimeType(resource.getFile().getAbsolutePath());
|
|
1344
|
+
} catch (IOException ex) {
|
|
1345
|
+
logger.info("Could not determine file type.");
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (contentType == null) {
|
|
1349
|
+
contentType = "application/octet-stream";
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return ResponseEntity.ok()
|
|
1353
|
+
.contentType(MediaType.parseMediaType(contentType))
|
|
1354
|
+
.header(HttpHeaders.CONTENT_DISPOSITION,
|
|
1355
|
+
"attachment; filename=\"" + resource.getFilename() + "\"")
|
|
1356
|
+
.body(resource);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
@DeleteMapping("/{fileName:.+}")
|
|
1360
|
+
public ResponseEntity<?> deleteFile(@PathVariable String fileName) {
|
|
1361
|
+
fileStorageService.deleteFile(fileName);
|
|
1362
|
+
return ResponseEntity.ok(new ApiResponse(true, "File deleted successfully"));
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
**Configuration:**
|
|
1368
|
+
|
|
1369
|
+
```yaml
|
|
1370
|
+
# application.yml
|
|
1371
|
+
file:
|
|
1372
|
+
upload-dir: ./uploads
|
|
1373
|
+
|
|
1374
|
+
spring:
|
|
1375
|
+
servlet:
|
|
1376
|
+
multipart:
|
|
1377
|
+
enabled: true
|
|
1378
|
+
max-file-size: 10MB
|
|
1379
|
+
max-request-size: 10MB
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
---
|
|
1383
|
+
|
|
1384
|
+
## 10. Caching with Redis
|
|
1385
|
+
|
|
1386
|
+
**Dependencies (pom.xml):**
|
|
1387
|
+
|
|
1388
|
+
```xml
|
|
1389
|
+
<dependency>
|
|
1390
|
+
<groupId>org.springframework.boot</groupId>
|
|
1391
|
+
<artifactId>spring-boot-starter-data-redis</artifactId>
|
|
1392
|
+
</dependency>
|
|
1393
|
+
<dependency>
|
|
1394
|
+
<groupId>org.springframework.boot</groupId>
|
|
1395
|
+
<artifactId>spring-boot-starter-cache</artifactId>
|
|
1396
|
+
</dependency>
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
**Redis Configuration:**
|
|
1400
|
+
|
|
1401
|
+
```java
|
|
1402
|
+
@Configuration
|
|
1403
|
+
@EnableCaching
|
|
1404
|
+
public class RedisConfig {
|
|
1405
|
+
|
|
1406
|
+
@Bean
|
|
1407
|
+
public RedisTemplate<String, Object> redisTemplate(
|
|
1408
|
+
RedisConnectionFactory connectionFactory) {
|
|
1409
|
+
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
|
1410
|
+
template.setConnectionFactory(connectionFactory);
|
|
1411
|
+
|
|
1412
|
+
Jackson2JsonRedisSerializer<Object> serializer =
|
|
1413
|
+
new Jackson2JsonRedisSerializer<>(Object.class);
|
|
1414
|
+
|
|
1415
|
+
ObjectMapper mapper = new ObjectMapper();
|
|
1416
|
+
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
|
1417
|
+
mapper.activateDefaultTyping(
|
|
1418
|
+
mapper.getPolymorphicTypeValidator(),
|
|
1419
|
+
ObjectMapper.DefaultTyping.NON_FINAL
|
|
1420
|
+
);
|
|
1421
|
+
serializer.setObjectMapper(mapper);
|
|
1422
|
+
|
|
1423
|
+
template.setKeySerializer(new StringRedisSerializer());
|
|
1424
|
+
template.setValueSerializer(serializer);
|
|
1425
|
+
template.setHashKeySerializer(new StringRedisSerializer());
|
|
1426
|
+
template.setHashValueSerializer(serializer);
|
|
1427
|
+
template.afterPropertiesSet();
|
|
1428
|
+
|
|
1429
|
+
return template;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
@Bean
|
|
1433
|
+
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
|
1434
|
+
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
|
1435
|
+
.entryTtl(Duration.ofMinutes(10))
|
|
1436
|
+
.serializeKeysWith(
|
|
1437
|
+
RedisSerializationContext.SerializationPair.fromSerializer(
|
|
1438
|
+
new StringRedisSerializer()
|
|
1439
|
+
)
|
|
1440
|
+
)
|
|
1441
|
+
.serializeValuesWith(
|
|
1442
|
+
RedisSerializationContext.SerializationPair.fromSerializer(
|
|
1443
|
+
new GenericJackson2JsonRedisSerializer()
|
|
1444
|
+
)
|
|
1445
|
+
)
|
|
1446
|
+
.disableCachingNullValues();
|
|
1447
|
+
|
|
1448
|
+
return RedisCacheManager.builder(connectionFactory)
|
|
1449
|
+
.cacheDefaults(config)
|
|
1450
|
+
.build();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
**Service with Caching:**
|
|
1456
|
+
|
|
1457
|
+
```java
|
|
1458
|
+
@Service
|
|
1459
|
+
public class ProductService {
|
|
1460
|
+
|
|
1461
|
+
private final ProductRepository productRepository;
|
|
1462
|
+
|
|
1463
|
+
@Cacheable(value = "products", key = "#id")
|
|
1464
|
+
public Optional<Product> findById(Long id) {
|
|
1465
|
+
logger.info("Fetching product from database: {}", id);
|
|
1466
|
+
return productRepository.findById(id);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
@Cacheable(value = "products", unless = "#result.isEmpty()")
|
|
1470
|
+
public List<Product> findAll() {
|
|
1471
|
+
logger.info("Fetching all products from database");
|
|
1472
|
+
return productRepository.findAll();
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
@CachePut(value = "products", key = "#product.id")
|
|
1476
|
+
public Product save(Product product) {
|
|
1477
|
+
logger.info("Saving product and updating cache: {}", product.getId());
|
|
1478
|
+
return productRepository.save(product);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
@CacheEvict(value = "products", key = "#id")
|
|
1482
|
+
public void delete(Long id) {
|
|
1483
|
+
logger.info("Deleting product and evicting from cache: {}", id);
|
|
1484
|
+
productRepository.deleteById(id);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
@CacheEvict(value = "products", allEntries = true)
|
|
1488
|
+
public void clearCache() {
|
|
1489
|
+
logger.info("Clearing all products from cache");
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
**Configuration:**
|
|
1495
|
+
|
|
1496
|
+
```yaml
|
|
1497
|
+
spring:
|
|
1498
|
+
redis:
|
|
1499
|
+
host: localhost
|
|
1500
|
+
port: 6379
|
|
1501
|
+
password:
|
|
1502
|
+
timeout: 2000ms
|
|
1503
|
+
jedis:
|
|
1504
|
+
pool:
|
|
1505
|
+
max-active: 8
|
|
1506
|
+
max-idle: 8
|
|
1507
|
+
min-idle: 0
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
---
|
|
1511
|
+
|
|
1512
|
+
## 11. Async Processing
|
|
1513
|
+
|
|
1514
|
+
**Enable Async:**
|
|
1515
|
+
|
|
1516
|
+
```java
|
|
1517
|
+
@Configuration
|
|
1518
|
+
@EnableAsync
|
|
1519
|
+
public class AsyncConfig {
|
|
1520
|
+
|
|
1521
|
+
@Bean(name = "taskExecutor")
|
|
1522
|
+
public Executor taskExecutor() {
|
|
1523
|
+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
|
1524
|
+
executor.setCorePoolSize(5);
|
|
1525
|
+
executor.setMaxPoolSize(10);
|
|
1526
|
+
executor.setQueueCapacity(100);
|
|
1527
|
+
executor.setThreadNamePrefix("async-");
|
|
1528
|
+
executor.initialize();
|
|
1529
|
+
return executor;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
**Async Service:**
|
|
1535
|
+
|
|
1536
|
+
```java
|
|
1537
|
+
@Service
|
|
1538
|
+
public class EmailService {
|
|
1539
|
+
|
|
1540
|
+
private static final Logger logger = LoggerFactory.getLogger(EmailService.class);
|
|
1541
|
+
|
|
1542
|
+
@Async("taskExecutor")
|
|
1543
|
+
public CompletableFuture<Void> sendEmail(String to, String subject, String body) {
|
|
1544
|
+
logger.info("Sending email to: {}", to);
|
|
1545
|
+
|
|
1546
|
+
try {
|
|
1547
|
+
// Simulate email sending
|
|
1548
|
+
Thread.sleep(3000);
|
|
1549
|
+
logger.info("Email sent successfully to: {}", to);
|
|
1550
|
+
} catch (InterruptedException e) {
|
|
1551
|
+
Thread.currentThread().interrupt();
|
|
1552
|
+
logger.error("Error sending email", e);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
return CompletableFuture.completedFuture(null);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
@Async
|
|
1559
|
+
public CompletableFuture<String> processLongRunningTask(String data) {
|
|
1560
|
+
logger.info("Starting long running task with data: {}", data);
|
|
1561
|
+
|
|
1562
|
+
try {
|
|
1563
|
+
Thread.sleep(5000);
|
|
1564
|
+
String result = "Processed: " + data;
|
|
1565
|
+
logger.info("Task completed: {}", result);
|
|
1566
|
+
return CompletableFuture.completedFuture(result);
|
|
1567
|
+
} catch (InterruptedException e) {
|
|
1568
|
+
Thread.currentThread().interrupt();
|
|
1569
|
+
logger.error("Task interrupted", e);
|
|
1570
|
+
return CompletableFuture.failedFuture(e);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
**Using Async Methods:**
|
|
1577
|
+
|
|
1578
|
+
```java
|
|
1579
|
+
@Service
|
|
1580
|
+
public class OrderService {
|
|
1581
|
+
|
|
1582
|
+
private final EmailService emailService;
|
|
1583
|
+
private final NotificationService notificationService;
|
|
1584
|
+
|
|
1585
|
+
@Transactional
|
|
1586
|
+
public Order createOrder(Order order) {
|
|
1587
|
+
Order saved = orderRepository.save(order);
|
|
1588
|
+
|
|
1589
|
+
// These run asynchronously
|
|
1590
|
+
emailService.sendEmail(
|
|
1591
|
+
order.getCustomer().getEmail(),
|
|
1592
|
+
"Order Confirmation",
|
|
1593
|
+
"Your order has been confirmed"
|
|
1594
|
+
);
|
|
1595
|
+
|
|
1596
|
+
notificationService.sendNotification(
|
|
1597
|
+
order.getCustomer().getId(),
|
|
1598
|
+
"Order placed successfully"
|
|
1599
|
+
);
|
|
1600
|
+
|
|
1601
|
+
return saved;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
public void processOrdersAsync(List<Long> orderIds) {
|
|
1605
|
+
List<CompletableFuture<Void>> futures = orderIds.stream()
|
|
1606
|
+
.map(id -> emailService.sendEmail("customer@example.com", "subject", "body"))
|
|
1607
|
+
.collect(Collectors.toList());
|
|
1608
|
+
|
|
1609
|
+
// Wait for all to complete
|
|
1610
|
+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
---
|
|
1616
|
+
|
|
1617
|
+
## 12. Scheduled Tasks
|
|
1618
|
+
|
|
1619
|
+
**Enable Scheduling:**
|
|
1620
|
+
|
|
1621
|
+
```java
|
|
1622
|
+
@Configuration
|
|
1623
|
+
@EnableScheduling
|
|
1624
|
+
public class SchedulingConfig {
|
|
1625
|
+
}
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
**Scheduled Service:**
|
|
1629
|
+
|
|
1630
|
+
```java
|
|
1631
|
+
@Service
|
|
1632
|
+
public class ScheduledTasks {
|
|
1633
|
+
|
|
1634
|
+
private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class);
|
|
1635
|
+
|
|
1636
|
+
private final OrderRepository orderRepository;
|
|
1637
|
+
private final EmailService emailService;
|
|
1638
|
+
|
|
1639
|
+
// Execute at fixed rate (every 5 seconds)
|
|
1640
|
+
@Scheduled(fixedRate = 5000)
|
|
1641
|
+
public void reportCurrentTime() {
|
|
1642
|
+
logger.info("The time is now {}", LocalDateTime.now());
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Execute at fixed delay (5 seconds after previous execution completes)
|
|
1646
|
+
@Scheduled(fixedDelay = 5000)
|
|
1647
|
+
public void scheduleFixedDelayTask() {
|
|
1648
|
+
logger.info("Fixed delay task - {}", LocalDateTime.now());
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Execute with initial delay
|
|
1652
|
+
@Scheduled(fixedRate = 5000, initialDelay = 10000)
|
|
1653
|
+
public void scheduleTaskWithInitialDelay() {
|
|
1654
|
+
logger.info("Task with initial delay - {}", LocalDateTime.now());
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Execute using cron expression (every day at 2 AM)
|
|
1658
|
+
@Scheduled(cron = "0 0 2 * * ?")
|
|
1659
|
+
public void scheduledDailyTask() {
|
|
1660
|
+
logger.info("Daily task executed at 2 AM");
|
|
1661
|
+
cleanupOldData();
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Every hour at minute 0
|
|
1665
|
+
@Scheduled(cron = "0 0 * * * ?")
|
|
1666
|
+
public void hourlyTask() {
|
|
1667
|
+
logger.info("Hourly task - {}", LocalDateTime.now());
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Every Monday at 9 AM
|
|
1671
|
+
@Scheduled(cron = "0 0 9 * * MON")
|
|
1672
|
+
public void weeklyTask() {
|
|
1673
|
+
logger.info("Weekly task - Monday 9 AM");
|
|
1674
|
+
generateWeeklyReports();
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Business day task (Mon-Fri at 10 AM)
|
|
1678
|
+
@Scheduled(cron = "0 0 10 * * MON-FRI")
|
|
1679
|
+
public void businessDayTask() {
|
|
1680
|
+
logger.info("Business day task");
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
private void cleanupOldData() {
|
|
1684
|
+
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);
|
|
1685
|
+
orderRepository.deleteByCreatedAtBefore(cutoffDate);
|
|
1686
|
+
logger.info("Deleted orders older than 30 days");
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
private void generateWeeklyReports() {
|
|
1690
|
+
// Generate and send reports
|
|
1691
|
+
logger.info("Generating weekly reports");
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Send daily summary email
|
|
1695
|
+
@Scheduled(cron = "0 0 18 * * ?")
|
|
1696
|
+
public void sendDailySummary() {
|
|
1697
|
+
LocalDate today = LocalDate.now();
|
|
1698
|
+
long orderCount = orderRepository.countByCreatedAtBetween(
|
|
1699
|
+
today.atStartOfDay(),
|
|
1700
|
+
today.plusDays(1).atStartOfDay()
|
|
1701
|
+
);
|
|
1702
|
+
|
|
1703
|
+
emailService.sendEmail(
|
|
1704
|
+
"admin@example.com",
|
|
1705
|
+
"Daily Summary",
|
|
1706
|
+
"Today's order count: " + orderCount
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
---
|
|
1713
|
+
|
|
1714
|
+
## 13. Email Service
|
|
1715
|
+
|
|
1716
|
+
**Dependencies:**
|
|
1717
|
+
|
|
1718
|
+
```xml
|
|
1719
|
+
<dependency>
|
|
1720
|
+
<groupId>org.springframework.boot</groupId>
|
|
1721
|
+
<artifactId>spring-boot-starter-mail</artifactId>
|
|
1722
|
+
</dependency>
|
|
1723
|
+
```
|
|
1724
|
+
|
|
1725
|
+
**Email Configuration:**
|
|
1726
|
+
|
|
1727
|
+
```yaml
|
|
1728
|
+
spring:
|
|
1729
|
+
mail:
|
|
1730
|
+
host: smtp.gmail.com
|
|
1731
|
+
port: 587
|
|
1732
|
+
username: your-email@gmail.com
|
|
1733
|
+
password: your-password
|
|
1734
|
+
properties:
|
|
1735
|
+
mail:
|
|
1736
|
+
smtp:
|
|
1737
|
+
auth: true
|
|
1738
|
+
starttls:
|
|
1739
|
+
enable: true
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
**Email Service:**
|
|
1743
|
+
|
|
1744
|
+
```java
|
|
1745
|
+
@Service
|
|
1746
|
+
public class EmailService {
|
|
1747
|
+
|
|
1748
|
+
private final JavaMailSender mailSender;
|
|
1749
|
+
private final TemplateEngine templateEngine;
|
|
1750
|
+
|
|
1751
|
+
@Value("${spring.mail.username}")
|
|
1752
|
+
private String fromEmail;
|
|
1753
|
+
|
|
1754
|
+
// Send simple email
|
|
1755
|
+
public void sendSimpleEmail(String to, String subject, String text) {
|
|
1756
|
+
SimpleMailMessage message = new SimpleMailMessage();
|
|
1757
|
+
message.setFrom(fromEmail);
|
|
1758
|
+
message.setTo(to);
|
|
1759
|
+
message.setSubject(subject);
|
|
1760
|
+
message.setText(text);
|
|
1761
|
+
|
|
1762
|
+
mailSender.send(message);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// Send HTML email
|
|
1766
|
+
public void sendHtmlEmail(String to, String subject, String htmlBody)
|
|
1767
|
+
throws MessagingException {
|
|
1768
|
+
MimeMessage message = mailSender.createMimeMessage();
|
|
1769
|
+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
1770
|
+
|
|
1771
|
+
helper.setFrom(fromEmail);
|
|
1772
|
+
helper.setTo(to);
|
|
1773
|
+
helper.setSubject(subject);
|
|
1774
|
+
helper.setText(htmlBody, true);
|
|
1775
|
+
|
|
1776
|
+
mailSender.send(message);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Send email with attachment
|
|
1780
|
+
public void sendEmailWithAttachment(String to, String subject, String text,
|
|
1781
|
+
String attachmentPath)
|
|
1782
|
+
throws MessagingException, IOException {
|
|
1783
|
+
MimeMessage message = mailSender.createMimeMessage();
|
|
1784
|
+
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
|
1785
|
+
|
|
1786
|
+
helper.setFrom(fromEmail);
|
|
1787
|
+
helper.setTo(to);
|
|
1788
|
+
helper.setSubject(subject);
|
|
1789
|
+
helper.setText(text);
|
|
1790
|
+
|
|
1791
|
+
FileSystemResource file = new FileSystemResource(new File(attachmentPath));
|
|
1792
|
+
helper.addAttachment(file.getFilename(), file);
|
|
1793
|
+
|
|
1794
|
+
mailSender.send(message);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// Send email using Thymeleaf template
|
|
1798
|
+
public void sendTemplatedEmail(String to, String subject,
|
|
1799
|
+
String templateName,
|
|
1800
|
+
Map<String, Object> variables)
|
|
1801
|
+
throws MessagingException {
|
|
1802
|
+
Context context = new Context();
|
|
1803
|
+
context.setVariables(variables);
|
|
1804
|
+
|
|
1805
|
+
String htmlBody = templateEngine.process(templateName, context);
|
|
1806
|
+
|
|
1807
|
+
sendHtmlEmail(to, subject, htmlBody);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Send order confirmation email
|
|
1811
|
+
public void sendOrderConfirmation(Order order) throws MessagingException {
|
|
1812
|
+
Map<String, Object> variables = new HashMap<>();
|
|
1813
|
+
variables.put("customerName", order.getCustomer().getName());
|
|
1814
|
+
variables.put("orderNumber", order.getOrderNumber());
|
|
1815
|
+
variables.put("items", order.getItems());
|
|
1816
|
+
variables.put("totalAmount", order.getTotalAmount());
|
|
1817
|
+
|
|
1818
|
+
sendTemplatedEmail(
|
|
1819
|
+
order.getCustomer().getEmail(),
|
|
1820
|
+
"Order Confirmation - " + order.getOrderNumber(),
|
|
1821
|
+
"order-confirmation",
|
|
1822
|
+
variables
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
```
|
|
1827
|
+
|
|
1828
|
+
**Thymeleaf Email Template (templates/order-confirmation.html):**
|
|
1829
|
+
|
|
1830
|
+
```html
|
|
1831
|
+
<!DOCTYPE html>
|
|
1832
|
+
<html xmlns:th="http://www.thymeleaf.org">
|
|
1833
|
+
<head>
|
|
1834
|
+
<title>Order Confirmation</title>
|
|
1835
|
+
</head>
|
|
1836
|
+
<body>
|
|
1837
|
+
<h1>Order Confirmation</h1>
|
|
1838
|
+
<p>Dear <span th:text="${customerName}">Customer</span>,</p>
|
|
1839
|
+
<p>Thank you for your order! Your order number is: <strong th:text="${orderNumber}">123</strong></p>
|
|
1840
|
+
|
|
1841
|
+
<h2>Order Items:</h2>
|
|
1842
|
+
<table>
|
|
1843
|
+
<tr>
|
|
1844
|
+
<th>Product</th>
|
|
1845
|
+
<th>Quantity</th>
|
|
1846
|
+
<th>Price</th>
|
|
1847
|
+
</tr>
|
|
1848
|
+
<tr th:each="item : ${items}">
|
|
1849
|
+
<td th:text="${item.product.name}">Product</td>
|
|
1850
|
+
<td th:text="${item.quantity}">1</td>
|
|
1851
|
+
<td th:text="${item.price}">$10.00</td>
|
|
1852
|
+
</tr>
|
|
1853
|
+
</table>
|
|
1854
|
+
|
|
1855
|
+
<p><strong>Total Amount: <span th:text="${totalAmount}">$100.00</span></strong></p>
|
|
1856
|
+
</body>
|
|
1857
|
+
</html>
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
---
|
|
1861
|
+
|
|
1862
|
+
## 14. Pagination and Sorting
|
|
1863
|
+
|
|
1864
|
+
**Service with Pagination:**
|
|
1865
|
+
|
|
1866
|
+
```java
|
|
1867
|
+
@Service
|
|
1868
|
+
public class ProductService {
|
|
1869
|
+
|
|
1870
|
+
private final ProductRepository productRepository;
|
|
1871
|
+
|
|
1872
|
+
@Transactional(readOnly = true)
|
|
1873
|
+
public Page<Product> findAll(int page, int size, String sortBy, String direction) {
|
|
1874
|
+
Sort.Direction sortDirection = direction.equalsIgnoreCase("desc")
|
|
1875
|
+
? Sort.Direction.DESC
|
|
1876
|
+
: Sort.Direction.ASC;
|
|
1877
|
+
|
|
1878
|
+
Pageable pageable = PageRequest.of(page, size, Sort.by(sortDirection, sortBy));
|
|
1879
|
+
return productRepository.findAll(pageable);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
@Transactional(readOnly = true)
|
|
1883
|
+
public Page<Product> searchProducts(String name, BigDecimal minPrice,
|
|
1884
|
+
BigDecimal maxPrice, int page, int size) {
|
|
1885
|
+
Specification<Product> spec = Specification.where(null);
|
|
1886
|
+
|
|
1887
|
+
if (name != null) {
|
|
1888
|
+
spec = spec.and(ProductSpecification.hasName(name));
|
|
1889
|
+
}
|
|
1890
|
+
if (minPrice != null) {
|
|
1891
|
+
spec = spec.and(ProductSpecification.hasPriceGreaterThan(minPrice));
|
|
1892
|
+
}
|
|
1893
|
+
if (maxPrice != null) {
|
|
1894
|
+
spec = spec.and(ProductSpecification.hasPriceLessThan(maxPrice));
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
Pageable pageable = PageRequest.of(page, size, Sort.by("name"));
|
|
1898
|
+
return productRepository.findAll(spec, pageable);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
```
|
|
1902
|
+
|
|
1903
|
+
**Controller with Pagination:**
|
|
1904
|
+
|
|
1905
|
+
```java
|
|
1906
|
+
@RestController
|
|
1907
|
+
@RequestMapping("/api/products")
|
|
1908
|
+
public class ProductController {
|
|
1909
|
+
|
|
1910
|
+
private final ProductService productService;
|
|
1911
|
+
|
|
1912
|
+
@GetMapping
|
|
1913
|
+
public ResponseEntity<Page<Product>> getProducts(
|
|
1914
|
+
@RequestParam(defaultValue = "0") int page,
|
|
1915
|
+
@RequestParam(defaultValue = "20") int size,
|
|
1916
|
+
@RequestParam(defaultValue = "id") String sortBy,
|
|
1917
|
+
@RequestParam(defaultValue = "asc") String direction) {
|
|
1918
|
+
Page<Product> products = productService.findAll(page, size, sortBy, direction);
|
|
1919
|
+
return ResponseEntity.ok(products);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
@GetMapping("/search")
|
|
1923
|
+
public ResponseEntity<Page<Product>> searchProducts(
|
|
1924
|
+
@RequestParam(required = false) String name,
|
|
1925
|
+
@RequestParam(required = false) BigDecimal minPrice,
|
|
1926
|
+
@RequestParam(required = false) BigDecimal maxPrice,
|
|
1927
|
+
@RequestParam(defaultValue = "0") int page,
|
|
1928
|
+
@RequestParam(defaultValue = "20") int size) {
|
|
1929
|
+
Page<Product> products = productService.searchProducts(
|
|
1930
|
+
name, minPrice, maxPrice, page, size
|
|
1931
|
+
);
|
|
1932
|
+
return ResponseEntity.ok(products);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
```
|
|
1936
|
+
|
|
1937
|
+
**Custom Page Response:**
|
|
1938
|
+
|
|
1939
|
+
```java
|
|
1940
|
+
public class PagedResponse<T> {
|
|
1941
|
+
private List<T> content;
|
|
1942
|
+
private int page;
|
|
1943
|
+
private int size;
|
|
1944
|
+
private long totalElements;
|
|
1945
|
+
private int totalPages;
|
|
1946
|
+
private boolean last;
|
|
1947
|
+
|
|
1948
|
+
public PagedResponse(Page<T> page) {
|
|
1949
|
+
this.content = page.getContent();
|
|
1950
|
+
this.page = page.getNumber();
|
|
1951
|
+
this.size = page.getSize();
|
|
1952
|
+
this.totalElements = page.getTotalElements();
|
|
1953
|
+
this.totalPages = page.getTotalPages();
|
|
1954
|
+
this.last = page.isLast();
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// Getters and setters
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
@GetMapping
|
|
1961
|
+
public ResponseEntity<PagedResponse<Product>> getProducts(
|
|
1962
|
+
@RequestParam(defaultValue = "0") int page,
|
|
1963
|
+
@RequestParam(defaultValue = "20") int size) {
|
|
1964
|
+
Page<Product> productPage = productService.findAll(page, size, "id", "asc");
|
|
1965
|
+
return ResponseEntity.ok(new PagedResponse<>(productPage));
|
|
1966
|
+
}
|
|
1967
|
+
```
|
|
1968
|
+
|
|
1969
|
+
---
|
|
1970
|
+
|
|
1971
|
+
## 15. Database Transactions
|
|
1972
|
+
|
|
1973
|
+
**Transaction Management:**
|
|
1974
|
+
|
|
1975
|
+
```java
|
|
1976
|
+
@Service
|
|
1977
|
+
@Transactional
|
|
1978
|
+
public class OrderService {
|
|
1979
|
+
|
|
1980
|
+
private final OrderRepository orderRepository;
|
|
1981
|
+
private final ProductRepository productRepository;
|
|
1982
|
+
private final EmailService emailService;
|
|
1983
|
+
|
|
1984
|
+
// Read-only transaction
|
|
1985
|
+
@Transactional(readOnly = true)
|
|
1986
|
+
public Optional<Order> findById(Long id) {
|
|
1987
|
+
return orderRepository.findById(id);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Default transaction (read-write)
|
|
1991
|
+
public Order createOrder(Order order) {
|
|
1992
|
+
// Validate stock
|
|
1993
|
+
for (OrderItem item : order.getItems()) {
|
|
1994
|
+
Product product = productRepository.findById(item.getProduct().getId())
|
|
1995
|
+
.orElseThrow(() -> new ResourceNotFoundException(
|
|
1996
|
+
"Product", "id", item.getProduct().getId()
|
|
1997
|
+
));
|
|
1998
|
+
|
|
1999
|
+
if (product.getStock() < item.getQuantity()) {
|
|
2000
|
+
throw new BadRequestException("Insufficient stock for product: " + product.getName());
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Decrease stock
|
|
2004
|
+
product.setStock(product.getStock() - item.getQuantity());
|
|
2005
|
+
productRepository.save(product);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// Save order
|
|
2009
|
+
Order saved = orderRepository.save(order);
|
|
2010
|
+
|
|
2011
|
+
// Send email (if this fails, transaction rolls back)
|
|
2012
|
+
try {
|
|
2013
|
+
emailService.sendOrderConfirmation(saved);
|
|
2014
|
+
} catch (Exception e) {
|
|
2015
|
+
throw new RuntimeException("Failed to send order confirmation", e);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
return saved;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Custom transaction settings
|
|
2022
|
+
@Transactional(
|
|
2023
|
+
propagation = Propagation.REQUIRES_NEW,
|
|
2024
|
+
isolation = Isolation.SERIALIZABLE,
|
|
2025
|
+
timeout = 30,
|
|
2026
|
+
rollbackFor = Exception.class
|
|
2027
|
+
)
|
|
2028
|
+
public void processPayment(Long orderId, PaymentDetails payment) {
|
|
2029
|
+
Order order = orderRepository.findById(orderId)
|
|
2030
|
+
.orElseThrow(() -> new ResourceNotFoundException("Order", "id", orderId));
|
|
2031
|
+
|
|
2032
|
+
// Process payment
|
|
2033
|
+
boolean paymentSuccess = paymentGateway.process(payment);
|
|
2034
|
+
|
|
2035
|
+
if (paymentSuccess) {
|
|
2036
|
+
order.setStatus(OrderStatus.PAID);
|
|
2037
|
+
orderRepository.save(order);
|
|
2038
|
+
} else {
|
|
2039
|
+
throw new RuntimeException("Payment failed");
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// Programmatic transaction management
|
|
2044
|
+
@Autowired
|
|
2045
|
+
private TransactionTemplate transactionTemplate;
|
|
2046
|
+
|
|
2047
|
+
public Order createOrderProgrammatic(Order order) {
|
|
2048
|
+
return transactionTemplate.execute(status -> {
|
|
2049
|
+
try {
|
|
2050
|
+
// Update stock
|
|
2051
|
+
for (OrderItem item : order.getItems()) {
|
|
2052
|
+
Product product = productRepository.findById(
|
|
2053
|
+
item.getProduct().getId()
|
|
2054
|
+
).orElseThrow();
|
|
2055
|
+
|
|
2056
|
+
product.setStock(product.getStock() - item.getQuantity());
|
|
2057
|
+
productRepository.save(product);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Save order
|
|
2061
|
+
Order saved = orderRepository.save(order);
|
|
2062
|
+
|
|
2063
|
+
// Send email
|
|
2064
|
+
emailService.sendOrderConfirmation(saved);
|
|
2065
|
+
|
|
2066
|
+
return saved;
|
|
2067
|
+
} catch (Exception e) {
|
|
2068
|
+
status.setRollbackOnly();
|
|
2069
|
+
throw new RuntimeException("Order creation failed", e);
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
```
|
|
2075
|
+
|
|
2076
|
+
---
|
|
2077
|
+
|
|
2078
|
+
## 16. Actuator and Monitoring
|
|
2079
|
+
|
|
2080
|
+
**Dependencies:**
|
|
2081
|
+
|
|
2082
|
+
```xml
|
|
2083
|
+
<dependency>
|
|
2084
|
+
<groupId>org.springframework.boot</groupId>
|
|
2085
|
+
<artifactId>spring-boot-starter-actuator</artifactId>
|
|
2086
|
+
</dependency>
|
|
2087
|
+
```
|
|
2088
|
+
|
|
2089
|
+
**Configuration:**
|
|
2090
|
+
|
|
2091
|
+
```yaml
|
|
2092
|
+
management:
|
|
2093
|
+
endpoints:
|
|
2094
|
+
web:
|
|
2095
|
+
exposure:
|
|
2096
|
+
include: health,info,metrics,prometheus,env,beans,mappings
|
|
2097
|
+
base-path: /actuator
|
|
2098
|
+
endpoint:
|
|
2099
|
+
health:
|
|
2100
|
+
show-details: always
|
|
2101
|
+
show-components: always
|
|
2102
|
+
metrics:
|
|
2103
|
+
export:
|
|
2104
|
+
prometheus:
|
|
2105
|
+
enabled: true
|
|
2106
|
+
info:
|
|
2107
|
+
env:
|
|
2108
|
+
enabled: true
|
|
2109
|
+
|
|
2110
|
+
info:
|
|
2111
|
+
app:
|
|
2112
|
+
name: Spring Boot Application
|
|
2113
|
+
description: My Spring Boot Application
|
|
2114
|
+
version: 1.0.0
|
|
2115
|
+
encoding: @project.build.sourceEncoding@
|
|
2116
|
+
java:
|
|
2117
|
+
version: @java.version@
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
**Custom Health Indicator:**
|
|
2121
|
+
|
|
2122
|
+
```java
|
|
2123
|
+
@Component
|
|
2124
|
+
public class CustomHealthIndicator implements HealthIndicator {
|
|
2125
|
+
|
|
2126
|
+
@Override
|
|
2127
|
+
public Health health() {
|
|
2128
|
+
// Check some custom health condition
|
|
2129
|
+
boolean isHealthy = checkCustomCondition();
|
|
2130
|
+
|
|
2131
|
+
if (isHealthy) {
|
|
2132
|
+
return Health.up()
|
|
2133
|
+
.withDetail("customService", "Available")
|
|
2134
|
+
.withDetail("timestamp", LocalDateTime.now())
|
|
2135
|
+
.build();
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
return Health.down()
|
|
2139
|
+
.withDetail("customService", "Unavailable")
|
|
2140
|
+
.withDetail("error", "Service is down")
|
|
2141
|
+
.build();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
private boolean checkCustomCondition() {
|
|
2145
|
+
// Implement your health check logic
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
```
|
|
2150
|
+
|
|
2151
|
+
**Custom Metrics:**
|
|
2152
|
+
|
|
2153
|
+
```java
|
|
2154
|
+
@Service
|
|
2155
|
+
public class OrderService {
|
|
2156
|
+
|
|
2157
|
+
private final MeterRegistry meterRegistry;
|
|
2158
|
+
private final Counter orderCounter;
|
|
2159
|
+
private final Timer orderTimer;
|
|
2160
|
+
|
|
2161
|
+
public OrderService(MeterRegistry meterRegistry) {
|
|
2162
|
+
this.meterRegistry = meterRegistry;
|
|
2163
|
+
this.orderCounter = Counter.builder("orders.created")
|
|
2164
|
+
.description("Total number of orders created")
|
|
2165
|
+
.register(meterRegistry);
|
|
2166
|
+
this.orderTimer = Timer.builder("orders.processing.time")
|
|
2167
|
+
.description("Time taken to process orders")
|
|
2168
|
+
.register(meterRegistry);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
public Order createOrder(Order order) {
|
|
2172
|
+
return orderTimer.record(() -> {
|
|
2173
|
+
Order saved = orderRepository.save(order);
|
|
2174
|
+
orderCounter.increment();
|
|
2175
|
+
return saved;
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
```
|
|
2180
|
+
|
|
2181
|
+
**Available Endpoints:**
|
|
2182
|
+
- `GET /actuator/health` - Application health
|
|
2183
|
+
- `GET /actuator/info` - Application info
|
|
2184
|
+
- `GET /actuator/metrics` - Application metrics
|
|
2185
|
+
- `GET /actuator/env` - Environment properties
|
|
2186
|
+
- `GET /actuator/beans` - Spring beans
|
|
2187
|
+
- `GET /actuator/mappings` - Request mappings
|
|
2188
|
+
|
|
2189
|
+
---
|
|
2190
|
+
|
|
2191
|
+
## 17. Docker Deployment
|
|
2192
|
+
|
|
2193
|
+
**Dockerfile:**
|
|
2194
|
+
|
|
2195
|
+
```dockerfile
|
|
2196
|
+
# Build stage
|
|
2197
|
+
FROM maven:3.8.5-openjdk-17 AS build
|
|
2198
|
+
WORKDIR /app
|
|
2199
|
+
COPY pom.xml .
|
|
2200
|
+
COPY src ./src
|
|
2201
|
+
RUN mvn clean package -DskipTests
|
|
2202
|
+
|
|
2203
|
+
# Run stage
|
|
2204
|
+
FROM openjdk:17-jdk-slim
|
|
2205
|
+
WORKDIR /app
|
|
2206
|
+
COPY --from=build /app/target/*.jar app.jar
|
|
2207
|
+
EXPOSE 8080
|
|
2208
|
+
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
2209
|
+
```
|
|
2210
|
+
|
|
2211
|
+
**docker-compose.yml:**
|
|
2212
|
+
|
|
2213
|
+
```yaml
|
|
2214
|
+
version: '3.8'
|
|
2215
|
+
|
|
2216
|
+
services:
|
|
2217
|
+
app:
|
|
2218
|
+
build: .
|
|
2219
|
+
ports:
|
|
2220
|
+
- "8080:8080"
|
|
2221
|
+
environment:
|
|
2222
|
+
- SPRING_PROFILES_ACTIVE=prod
|
|
2223
|
+
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb
|
|
2224
|
+
- SPRING_DATASOURCE_USERNAME=postgres
|
|
2225
|
+
- SPRING_DATASOURCE_PASSWORD=password
|
|
2226
|
+
depends_on:
|
|
2227
|
+
- db
|
|
2228
|
+
- redis
|
|
2229
|
+
networks:
|
|
2230
|
+
- app-network
|
|
2231
|
+
|
|
2232
|
+
db:
|
|
2233
|
+
image: postgres:15
|
|
2234
|
+
environment:
|
|
2235
|
+
- POSTGRES_DB=mydb
|
|
2236
|
+
- POSTGRES_USER=postgres
|
|
2237
|
+
- POSTGRES_PASSWORD=password
|
|
2238
|
+
ports:
|
|
2239
|
+
- "5432:5432"
|
|
2240
|
+
volumes:
|
|
2241
|
+
- postgres-data:/var/lib/postgresql/data
|
|
2242
|
+
networks:
|
|
2243
|
+
- app-network
|
|
2244
|
+
|
|
2245
|
+
redis:
|
|
2246
|
+
image: redis:7-alpine
|
|
2247
|
+
ports:
|
|
2248
|
+
- "6379:6379"
|
|
2249
|
+
networks:
|
|
2250
|
+
- app-network
|
|
2251
|
+
|
|
2252
|
+
volumes:
|
|
2253
|
+
postgres-data:
|
|
2254
|
+
|
|
2255
|
+
networks:
|
|
2256
|
+
app-network:
|
|
2257
|
+
driver: bridge
|
|
2258
|
+
```
|
|
2259
|
+
|
|
2260
|
+
**Build and Run:**
|
|
2261
|
+
|
|
2262
|
+
```bash
|
|
2263
|
+
# Build image
|
|
2264
|
+
docker build -t myapp:latest .
|
|
2265
|
+
|
|
2266
|
+
# Run with docker-compose
|
|
2267
|
+
docker-compose up -d
|
|
2268
|
+
|
|
2269
|
+
# View logs
|
|
2270
|
+
docker-compose logs -f app
|
|
2271
|
+
|
|
2272
|
+
# Stop
|
|
2273
|
+
docker-compose down
|
|
2274
|
+
```
|
|
2275
|
+
|
|
2276
|
+
---
|
|
2277
|
+
|
|
2278
|
+
## 18. API Versioning
|
|
2279
|
+
|
|
2280
|
+
**URL Versioning:**
|
|
2281
|
+
|
|
2282
|
+
```java
|
|
2283
|
+
@RestController
|
|
2284
|
+
@RequestMapping("/api/v1/users")
|
|
2285
|
+
public class UserControllerV1 {
|
|
2286
|
+
|
|
2287
|
+
@GetMapping("/{id}")
|
|
2288
|
+
public UserV1 getUser(@PathVariable Long id) {
|
|
2289
|
+
return userService.findByIdV1(id);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
@RestController
|
|
2294
|
+
@RequestMapping("/api/v2/users")
|
|
2295
|
+
public class UserControllerV2 {
|
|
2296
|
+
|
|
2297
|
+
@GetMapping("/{id}")
|
|
2298
|
+
public UserV2 getUser(@PathVariable Long id) {
|
|
2299
|
+
return userService.findByIdV2(id);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
```
|
|
2303
|
+
|
|
2304
|
+
**Header Versioning:**
|
|
2305
|
+
|
|
2306
|
+
```java
|
|
2307
|
+
@RestController
|
|
2308
|
+
@RequestMapping("/api/users")
|
|
2309
|
+
public class UserController {
|
|
2310
|
+
|
|
2311
|
+
@GetMapping(value = "/{id}", headers = "X-API-VERSION=1")
|
|
2312
|
+
public UserV1 getUserV1(@PathVariable Long id) {
|
|
2313
|
+
return userService.findByIdV1(id);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
@GetMapping(value = "/{id}", headers = "X-API-VERSION=2")
|
|
2317
|
+
public UserV2 getUserV2(@PathVariable Long id) {
|
|
2318
|
+
return userService.findByIdV2(id);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
```
|
|
2322
|
+
|
|
2323
|
+
**Accept Header Versioning:**
|
|
2324
|
+
|
|
2325
|
+
```java
|
|
2326
|
+
@RestController
|
|
2327
|
+
@RequestMapping("/api/users")
|
|
2328
|
+
public class UserController {
|
|
2329
|
+
|
|
2330
|
+
@GetMapping(value = "/{id}",
|
|
2331
|
+
produces = "application/vnd.myapp.v1+json")
|
|
2332
|
+
public UserV1 getUserV1(@PathVariable Long id) {
|
|
2333
|
+
return userService.findByIdV1(id);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
@GetMapping(value = "/{id}",
|
|
2337
|
+
produces = "application/vnd.myapp.v2+json")
|
|
2338
|
+
public UserV2 getUserV2(@PathVariable Long id) {
|
|
2339
|
+
return userService.findByIdV2(id);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
```
|
|
2343
|
+
|
|
2344
|
+
---
|
|
2345
|
+
|
|
2346
|
+
This examples file provides comprehensive, production-ready code examples for building Spring Boot applications. Each example demonstrates best practices and real-world patterns used in enterprise applications.
|