@brookmind/ai-toolkit 1.1.4 → 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.