Back to blog

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

javaspring-bootrest-apibackendapi-design
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/1

Chiế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/1

5. 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: 3600000

8. 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: 1800000

Async 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: true

Logging 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: 30

CORS 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.yml

Tổ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

  1. Triển khai các mẫu này vào projects Spring Boot hiện tại của bạn
  2. Thêm monitoring với Prometheus và Grafana
  3. Deploy lên production với Docker và Kubernetes
  4. Khám phá kiến trúc microservices với Spring Cloud

Bài viết liên quan


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.