Spring Boot REST APIs: Mẫu Thiết Kế Nâng Cao

Giới thiệu
Trong bài viết trước về Bắt đầu với Spring Boot, chúng ta đã xây dựng một REST API đơn giản. Bây giờ, hãy nâng cấp lên một tầm cao mới bằng cách khám phá các mẫu thiết kế nâng cao và best practices được sử dụng trong môi trường production.
Những gì bạn sẽ học được
✅ Triển khai xử lý ngoại lệ mạnh mẽ và error responses
✅ Thêm validation cho request với Bean Validation
✅ Xây dựng phân trang và filtering cho datasets lớn
✅ Triển khai các chiến lược phiên bản API
✅ Bảo mật API với Spring Security và JWT
✅ Tạo tài liệu OpenAPI/Swagger
✅ Viết tests toàn diện (Unit, Integration, E2E)
✅ Tối ưu hiệu suất API
Yêu cầu
- Java 17 trở lên đã cài đặt
- Hiểu biết cơ bản về Spring Boot (xem hướng dẫn bắt đầu)
- Quen thuộc với nguyên tắc REST API
- Kinh nghiệm với Maven hoặc Gradle
1. Xử lý ngoại lệ nâng cao
Global Exception Handler
@ControllerAdvice của Spring Boot cho phép xử lý ngoại lệ tập trung trên tất cả controllers.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex,
WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now(),
request.getDescription(false)
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
errors,
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex,
WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Đã xảy ra lỗi không mong đợi",
LocalDateTime.now(),
request.getDescription(false)
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}Custom Error Response
@Data
@AllArgsConstructor
public class ErrorResponse {
private int status;
private String message;
private LocalDateTime timestamp;
private String path;
private Map<String, String> errors; // Cho validation errors
}Custom Exceptions
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, String field, Object value) {
super(String.format("%s không tìm thấy với %s: '%s'", resource, field, value));
}
}
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}Sử dụng trong Controller:
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
return ResponseEntity.ok(user);
}2. Request Validation với Bean Validation
Thêm Dependencies
<!-- Đã bao gồm trong spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>Entity Validation
@Entity
@Table(name = "users")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Username là bắt buộc")
@Size(min = 3, max = 50, message = "Username phải từ 3 đến 50 ký tự")
private String username;
@NotBlank(message = "Email là bắt buộc")
@Email(message = "Email phải hợp lệ")
private String email;
@NotBlank(message = "Password là bắt buộc")
@Size(min = 8, message = "Password phải có ít nhất 8 ký tự")
@Pattern(
regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$",
message = "Password phải chứa chữ hoa, chữ thường, số và ký tự đặc biệt"
)
private String password;
@Min(value = 18, message = "Tuổi phải ít nhất 18")
@Max(value = 120, message = "Tuổi phải nhỏ hơn 120")
private Integer age;
@Past(message = "Ngày sinh phải ở quá khứ")
private LocalDate birthDate;
}DTO Validation
@Data
public class CreateUserRequest {
@NotBlank(message = "Username là bắt buộc")
@Size(min = 3, max = 50)
private String username;
@NotBlank(message = "Email là bắt buộc")
@Email(message = "Định dạng email không hợp lệ")
private String email;
@NotBlank(message = "Password là bắt buộc")
@Size(min = 8, max = 100)
private String password;
@NotNull(message = "Tuổi là bắt buộc")
@Min(18)
@Max(120)
private Integer age;
}Controller Validation
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(
@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(
@PathVariable @Min(1) Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}Custom Validators
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email đã tồn tại";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired
private UserRepository userRepository;
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
return email != null && !userRepository.existsByEmail(email);
}
}3. Phân trang và Filtering
Phân trang với Spring Data
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir) {
Sort.Direction direction = sortDir.equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
Page<UserDTO> users = userService.getUsers(pageable);
return ResponseEntity.ok(users);
}
}Custom Pagination Response
@Data
@AllArgsConstructor
public class PageResponse<T> {
private List<T> content;
private int pageNumber;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean last;
private boolean first;
}
// Trong service layer
public PageResponse<UserDTO> getUsers(Pageable pageable) {
Page<User> page = userRepository.findAll(pageable);
List<UserDTO> content = page.getContent().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return new PageResponse<>(
content,
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isLast(),
page.isFirst()
);
}Advanced Filtering với Specifications
public class UserSpecification {
public static Specification<User> hasUsername(String username) {
return (root, query, cb) ->
username == null ? null : cb.like(root.get("username"), "%" + username + "%");
}
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.like(root.get("email"), "%" + email + "%");
}
public static Specification<User> ageGreaterThan(Integer age) {
return (root, query, cb) ->
age == null ? null : cb.greaterThanOrEqualTo(root.get("age"), age);
}
public static Specification<User> ageLessThan(Integer age) {
return (root, query, cb) ->
age == null ? null : cb.lessThanOrEqualTo(root.get("age"), age);
}
}
// Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
// Controller
@GetMapping("/search")
public ResponseEntity<Page<UserDTO>> searchUsers(
@RequestParam(required = false) String username,
@RequestParam(required = false) String email,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
Pageable pageable) {
Specification<User> spec = Specification
.where(UserSpecification.hasUsername(username))
.and(UserSpecification.hasEmail(email))
.and(UserSpecification.ageGreaterThan(minAge))
.and(UserSpecification.ageLessThan(maxAge));
Page<User> users = userRepository.findAll(spec, pageable);
// Convert sang DTO và return
}4. Chiến lược phiên bản API
Chiến lược 1: URI Versioning (Khuyến nghị)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
// Triển khai phiên bản 1
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
// Triển khai phiên bản 2 với breaking changes
}Ưu điểm: Rõ ràng, dễ hiểu, được sử dụng rộng rãi Nhược điểm: URL dài, code trùng lặp
Chiến lược 2: Header Versioning
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(headers = "API-Version=1")
public ResponseEntity<UserDTOV1> getUserV1(@PathVariable Long id) {
// Logic phiên bản 1
}
@GetMapping(headers = "API-Version=2")
public ResponseEntity<UserDTOV2> getUserV2(@PathVariable Long id) {
// Logic phiên bản 2
}
}Request:
curl -H "API-Version: 2" http://localhost:8080/api/users/1Chiến lược 3: Content Negotiation
@GetMapping(produces = "application/vnd.company.v1+json")
public ResponseEntity<UserDTOV1> getUserV1(@PathVariable Long id) {
// Logic phiên bản 1
}
@GetMapping(produces = "application/vnd.company.v2+json")
public ResponseEntity<UserDTOV2> getUserV2(@PathVariable Long id) {
// Logic phiên bản 2
}Request:
curl -H "Accept: application/vnd.company.v2+json" http://localhost:8080/api/users/15. Bảo mật với Spring Security và JWT
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>JWT Utility Class
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}JWT Authentication Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}Authentication Controller
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(token));
}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
// Logic đăng ký user
// Hash password với passwordEncoder.encode()
// Lưu user vào database
// Tạo token
// Return token
}
}6. Tài liệu OpenAPI/Swagger
Dependencies
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>Configuration
@Configuration
public class OpenAPIConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("User Management API")
.version("1.0")
.description("REST API quản lý user với Spring Boot")
.contact(new Contact()
.name("Chanh Le")
.email("[email protected]")
.url("https://chanhle.dev")
)
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")
)
)
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}
}Controller Documentation
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "Quản lý User", description = "APIs quản lý users")
public class UserController {
@Operation(
summary = "Lấy user theo ID",
description = "Trả về một user dựa trên ID của họ"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Tìm thấy user",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = UserDTO.class)
)
),
@ApiResponse(
responseCode = "404",
description = "Không tìm thấy user",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)
)
),
@ApiResponse(responseCode = "401", description = "Chưa xác thực")
})
@SecurityRequirement(name = "bearerAuth")
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(
@Parameter(description = "User ID", required = true)
@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@Operation(
summary = "Tạo user mới",
description = "Tạo user mới và trả về user đã tạo"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "Tạo user thành công"
),
@ApiResponse(
responseCode = "400",
description = "Dữ liệu đầu vào không hợp lệ"
)
})
@PostMapping
public ResponseEntity<UserDTO> createUser(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User object cần tạo",
required = true,
content = @Content(
schema = @Schema(implementation = CreateUserRequest.class)
)
)
@Valid @RequestBody CreateUserRequest request) {
UserDTO user = userService.createUser(request);
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
}Truy cập Swagger UI: http://localhost:8080/swagger-ui.html
7. Chiến lược Testing
Unit Tests
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
public void testGetUserById_Success() {
// Arrange
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setUsername("testuser");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act
UserDTO result = userService.getUserById(userId);
// Assert
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("testuser", result.getUsername());
verify(userRepository, times(1)).findById(userId);
}
@Test
public void testGetUserById_NotFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(ResourceNotFoundException.class, () -> {
userService.getUserById(userId);
});
}
}Integration Tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@Order(1)
public void testCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setUsername("testuser");
request.setEmail("[email protected]");
request.setPassword("Test@1234");
request.setAge(25);
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
@Order(2)
public void testGetUserById() throws Exception {
mockMvc.perform(get("/api/v1/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").isNotEmpty());
}
@Test
public void testGetUserById_NotFound() throws Exception {
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("User không tìm thấy với id: '999'"));
}
}Repository Tests
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void testFindByEmail_Success() {
// Arrange
User user = new User();
user.setUsername("testuser");
user.setEmail("[email protected]");
user.setPassword("password");
user.setAge(25);
userRepository.save(user);
// Act
Optional<User> found = userRepository.findByEmail("[email protected]");
// Assert
assertTrue(found.isPresent());
assertEquals("testuser", found.get().getUsername());
}
@Test
public void testExistsByEmail() {
// Arrange
User user = new User();
user.setEmail("[email protected]");
userRepository.save(user);
// Act
boolean exists = userRepository.existsByEmail("[email protected]");
// Assert
assertTrue(exists);
}
}Test Configuration
# src/test/resources/application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
h2:
console:
enabled: true
jwt:
secret: testsecretkeytestsecretkeytestsecretkey
expiration: 36000008. Tối ưu hiệu suất
Caching với Spring Cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "posts");
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public UserDTO getUserById(Long id) {
// Database call - được cache sau lần gọi đầu tiên
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
// Xóa cache khi delete
}
@CachePut(value = "users", key = "#result.id")
public UserDTO updateUser(Long id, UpdateUserRequest request) {
// Cập nhật cache với giá trị mới
}
}Tối ưu Database Query
// N+1 Query Problem - TỆ
@OneToMany(mappedBy = "user")
private List<Post> posts;
// Giải pháp 1: JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.posts WHERE u.id = :id")
User findByIdWithPosts(@Param("id") Long id);
// Giải pháp 2: Entity Graph
@EntityGraph(attributePaths = {"posts", "comments"})
Optional<User> findById(Long id);
// Giải pháp 3: Batch Fetching
@BatchSize(size = 10)
@OneToMany(mappedBy = "user")
private List<Post> posts;Connection Pool Tuning
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000Async Processing
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
public class EmailService {
@Async
public CompletableFuture<Void> sendWelcomeEmail(String email) {
// Gửi email bất đồng bộ
return CompletableFuture.completedFuture(null);
}
}9. Best Practices cho Production
Health Checks và Actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: trueLogging Configuration
logging:
level:
root: INFO
com.example.api: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file:
name: /var/log/app.log
max-size: 10MB
max-history: 30CORS Configuration
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}Rate Limiting
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>7.6.0</version>
</dependency>@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String key = request.getRemoteAddr();
Bucket bucket = resolveBucket(key);
if (bucket.tryConsume(1)) {
return true;
}
response.setStatus(429); // Too Many Requests
response.getWriter().write("Vượt quá giới hạn request");
return false;
}
private Bucket resolveBucket(String key) {
return cache.computeIfAbsent(key, k -> {
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)));
return Bucket.builder()
.addLimit(limit)
.build();
});
}
}10. Cấu trúc Project hoàn chỉnh
src/
├── main/
│ ├── java/
│ │ └── com/example/api/
│ │ ├── config/
│ │ │ ├── SecurityConfig.java
│ │ │ ├── OpenAPIConfig.java
│ │ │ ├── CacheConfig.java
│ │ │ └── AsyncConfig.java
│ │ ├── controller/
│ │ │ ├── UserController.java
│ │ │ └── AuthController.java
│ │ ├── dto/
│ │ │ ├── UserDTO.java
│ │ │ ├── CreateUserRequest.java
│ │ │ ├── UpdateUserRequest.java
│ │ │ └── AuthResponse.java
│ │ ├── entity/
│ │ │ └── User.java
│ │ ├── exception/
│ │ │ ├── GlobalExceptionHandler.java
│ │ │ ├── ResourceNotFoundException.java
│ │ │ └── ErrorResponse.java
│ │ ├── filter/
│ │ │ └── JwtAuthenticationFilter.java
│ │ ├── repository/
│ │ │ └── UserRepository.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ └── UserServiceImpl.java
│ │ ├── specification/
│ │ │ └── UserSpecification.java
│ │ ├── util/
│ │ │ └── JwtUtil.java
│ │ └── ApiApplication.java
│ └── resources/
│ ├── application.yml
│ └── application-prod.yml
└── test/
├── java/
│ └── com/example/api/
│ ├── controller/
│ │ └── UserControllerTest.java
│ ├── service/
│ │ └── UserServiceTest.java
│ └── repository/
│ └── UserRepositoryTest.java
└── resources/
└── application-test.ymlTổng kết và Key Takeaways
Trong hướng dẫn toàn diện này, chúng ta đã đề cập các mẫu thiết kế nâng cao để xây dựng REST APIs sẵn sàng cho production với Spring Boot:
✅ Exception Handling: Xử lý lỗi tập trung với @ControllerAdvice
✅ Validation: Bean Validation với custom validators
✅ Pagination: Phân trang và filtering với Spring Data Specifications
✅ API Versioning: Chiến lược URI, header và content negotiation
✅ Security: JWT authentication với Spring Security
✅ Documentation: Tích hợp OpenAPI/Swagger
✅ Testing: Unit, integration và repository tests
✅ Performance: Caching, tối ưu query, async processing
✅ Production: Health checks, logging, CORS, rate limiting
Các bước tiếp theo
- Triển khai các mẫu này vào projects Spring Boot hiện tại của bạn
- Thêm monitoring với Prometheus và Grafana
- Deploy lên production với Docker và Kubernetes
- Khám phá kiến trúc microservices với Spring Cloud
Bài viết liên quan
- Bắt đầu với Spring Boot: REST API đầu tiên của bạn
- Learning FastAPI: Các mẫu thiết kế nâng cao
- Docker cho Backend Developers: Hướng dẫn thực tế
Có câu hỏi hoặc feedback? Đừng ngại liên hệ tại [email protected] hoặc kết nối với tôi trên X.
Chúc bạn code vui vẻ! 🚀
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.