Back to blog

Tìm Hiểu Sâu: Best Practices Xử Lý Exception Trong Java

javaexceptionserror-handlingbest-practicesbackend
Tìm Hiểu Sâu: Best Practices Xử Lý Exception Trong Java

Xử lý exception là một trong những khía cạnh quan trọng nhất khi viết ứng dụng Java mạnh mẽ. Việc xử lý exception kém có thể dẫn đến rò rỉ tài nguyên, thông báo lỗi không rõ ràng, và ứng dụng fail âm thầm hoặc thảm khốc. Trong bài viết này, chúng ta sẽ khám phá mô hình exception của Java và các pattern production-ready giúp ứng dụng của bạn đáng tin cậy và dễ bảo trì hơn.

Hướng dẫn này xây dựng dựa trên các khái niệm từ Getting Started with Spring Boot và cung cấp nền tảng cho phát triển backend mạnh mẽ.

Hiểu Về Exception Hierarchy

Exception hierarchy của Java bắt nguồn từ Throwable, có hai nhánh chính:

Throwable
├── Error (JVM errors, don't catch these)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception
    ├── IOException (checked)
    ├── SQLException (checked)
    ├── RuntimeException (unchecked)
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   └── IllegalStateException
    └── ...

Nguyên tắc chính:

  • Error: Các vấn đề nghiêm trọng mà ứng dụng không nên catch (lỗi JVM)
  • Checked Exceptions: Phải được khai báo hoặc catch (bắt buộc compile-time)
  • Unchecked Exceptions: RuntimeException và các lớp con (xử lý tùy chọn)

Checked vs Unchecked Exceptions: Khi Nào Dùng Loại Nào

Checked Exceptions (Exception và các lớp con, trừ RuntimeException)

Sử dụng checked exceptions cho các điều kiện có thể khôi phục mà caller nên xử lý.

// Good: Caller can reasonably recover from file not found
public class ConfigurationLoader {
    public Configuration loadConfig(String path) throws IOException {
        Path configPath = Paths.get(path);
        if (!Files.exists(configPath)) {
            throw new IOException("Configuration file not found: " + path);
        }
        return parseConfiguration(Files.readString(configPath));
    }
}
 
// Caller must handle or propagate
public class Application {
    public void initialize() {
        try {
            Configuration config = configLoader.loadConfig("app.properties");
            // Use configuration
        } catch (IOException e) {
            // Fallback to defaults
            logger.warn("Using default configuration", e);
            Configuration config = Configuration.getDefaults();
        }
    }
}

Unchecked Exceptions (Các lớp con RuntimeException)

Sử dụng unchecked exceptions cho lỗi lập trình và điều kiện không thể khôi phục.

// Good: Programming error - caller should fix the code
public class UserService {
    public User findById(Long userId) {
        if (userId == null) {
            throw new IllegalArgumentException("User ID cannot be null");
        }
        return userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException(
                "User not found with id: " + userId));
    }
}
 
// Good: State validation
public class Order {
    private OrderStatus status;
    
    public void ship() {
        if (status != OrderStatus.PAID) {
            throw new IllegalStateException(
                "Cannot ship order in status: " + status);
        }
        status = OrderStatus.SHIPPED;
    }
}

Tranh Luận: Khi Nào Dùng Loại Nào?

Xu hướng Java hiện đại: Ưu tiên unchecked exceptions cho hầu hết trường hợp.

// ❌ Old style: Checked exceptions everywhere
public interface PaymentGateway {
    void processPayment(Payment payment) 
        throws InvalidPaymentException, 
               GatewayConnectionException, 
               InsufficientFundsException;
}
 
// ✅ Modern style: Unchecked for cleaner APIs
public interface PaymentGateway {
    void processPayment(Payment payment);
    // Throws PaymentException (unchecked) on any failure
}

Nguyên tắc chung:

  • Checked: Thao tác I/O, network calls khi retry có thể hoạt động
  • Unchecked: Lỗi validation, vi phạm business logic, lỗi lập trình

Try-Catch-Finally: Những Điều Cơ Bản Làm Đúng

Xử Lý Exception Cơ Bản

// ✅ Specific exceptions first, general last
public void processFile(String path) {
    try {
        String content = Files.readString(Paths.get(path));
        processContent(content);
    } catch (FileNotFoundException e) {
        logger.error("File not found: {}", path, e);
        // Specific handling
    } catch (IOException e) {
        logger.error("Error reading file: {}", path, e);
        // General I/O handling
    } catch (Exception e) {
        logger.error("Unexpected error processing file", e);
        // Last resort
    }
}

Finally Block: Dọn Dẹp Tài Nguyên

// ⚠️ Old style: Manual cleanup (error-prone)
public String readFile(String path) throws IOException {
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(path));
        return reader.readLine();
    } finally {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                logger.warn("Failed to close reader", e);
            }
        }
    }
}

Vấn đề: Dài dòng, dễ quên, exceptions trong finally có thể che lấp exception gốc.

Try-With-Resources: Cách Hiện Đại

Được giới thiệu trong Java 7, cải tiến trong Java 9.

Cách Sử Dụng Cơ Bản

// ✅ Automatic resource management
public String readFile(String path) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        return reader.readLine();
    }
    // Reader automatically closed, even if exception occurs
}

Nhiều Resources

// ✅ Multiple resources (closed in reverse order)
public void copyFile(String source, String destination) throws IOException {
    try (
        InputStream in = new FileInputStream(source);
        OutputStream out = new FileOutputStream(destination)
    ) {
        in.transferTo(out);
    }
}

Java 9+: Effectively Final Variables

// Java 9+: Can use existing variables if effectively final
public void processResource(BufferedReader reader) throws IOException {
    try (reader) {  // No need to redeclare
        String line = reader.readLine();
        // Process line
    }
}

Custom AutoCloseable

// Create custom resources
public class DatabaseConnection implements AutoCloseable {
    private Connection connection;
    
    public DatabaseConnection(String url) throws SQLException {
        this.connection = DriverManager.getConnection(url);
    }
    
    public void execute(String sql) throws SQLException {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute(sql);
        }
    }
    
    @Override
    public void close() throws SQLException {
        if (connection != null && !connection.isClosed()) {
            connection.close();
        }
    }
}
 
// Usage
try (DatabaseConnection db = new DatabaseConnection(dbUrl)) {
    db.execute("INSERT INTO users VALUES (...)");
} // Automatically closed

Tạo Custom Exceptions

Custom Exception Cơ Bản

// ✅ Unchecked exception for domain errors
public class OrderException extends RuntimeException {
    private final String orderNumber;
    
    public OrderException(String message, String orderNumber) {
        super(message);
        this.orderNumber = orderNumber;
    }
    
    public OrderException(String message, String orderNumber, Throwable cause) {
        super(message, cause);
        this.orderNumber = orderNumber;
    }
    
    public String getOrderNumber() {
        return orderNumber;
    }
}

Các Loại Exception Cụ Thể

// Create a hierarchy for better handling
public class PaymentException extends RuntimeException {
    public PaymentException(String message) {
        super(message);
    }
    
    public PaymentException(String message, Throwable cause) {
        super(message, cause);
    }
}
 
public class InsufficientFundsException extends PaymentException {
    private final BigDecimal available;
    private final BigDecimal required;
    
    public InsufficientFundsException(BigDecimal available, BigDecimal required) {
        super(String.format("Insufficient funds: have %s, need %s", 
                           available, required));
        this.available = available;
        this.required = required;
    }
    
    public BigDecimal getAvailable() { return available; }
    public BigDecimal getRequired() { return required; }
}
 
public class PaymentGatewayException extends PaymentException {
    private final String gatewayErrorCode;
    
    public PaymentGatewayException(String message, String errorCode, Throwable cause) {
        super(message, cause);
        this.gatewayErrorCode = errorCode;
    }
    
    public String getGatewayErrorCode() { return gatewayErrorCode; }
}

Cách Sử Dụng

public class PaymentService {
    public void processPayment(Order order) {
        try {
            gateway.charge(order.getTotal(), order.getPaymentMethod());
        } catch (IOException e) {
            throw new PaymentGatewayException(
                "Failed to connect to payment gateway", 
                "GATEWAY_UNAVAILABLE", 
                e
            );
        }
    }
    
    // Caller can handle specifically
    public void handlePayment(Order order) {
        try {
            processPayment(order);
        } catch (InsufficientFundsException e) {
            notifyCustomer(e.getAvailable(), e.getRequired());
        } catch (PaymentGatewayException e) {
            scheduleRetry(order);
            logger.error("Gateway error: {}", e.getGatewayErrorCode(), e);
        }
    }
}

Exception Chaining và Suppressed Exceptions

Exception Chaining

Luôn giữ lại exception gốc khi wrapping.

// ✅ Good: Preserve the cause
public User findUser(String email) {
    try {
        return userRepository.findByEmail(email);
    } catch (SQLException e) {
        throw new UserRepositoryException(
            "Failed to find user by email: " + email, 
            e  // Original exception preserved
        );
    }
}
 
// ❌ Bad: Lost stack trace
public User findUser(String email) {
    try {
        return userRepository.findByEmail(email);
    } catch (SQLException e) {
        throw new UserRepositoryException(
            "Failed to find user: " + e.getMessage()
            // Original exception lost!
        );
    }
}

Suppressed Exceptions

Try-with-resources tự động theo dõi suppressed exceptions.

public class ResourceDemo {
    static class FailingResource implements AutoCloseable {
        private final String name;
        
        public FailingResource(String name) {
            this.name = name;
        }
        
        public void doWork() {
            throw new RuntimeException("Error in " + name);
        }
        
        @Override
        public void close() {
            throw new RuntimeException("Error closing " + name);
        }
    }
    
    public static void main(String[] args) {
        try (FailingResource resource = new FailingResource("MyResource")) {
            resource.doWork();
        } catch (Exception e) {
            System.out.println("Main exception: " + e.getMessage());
            
            // Access suppressed exceptions
            for (Throwable suppressed : e.getSuppressed()) {
                System.out.println("Suppressed: " + suppressed.getMessage());
            }
        }
    }
}
 
// Output:
// Main exception: Error in MyResource
// Suppressed: Error closing MyResource

Suppression Thủ Công

public void processMultipleFiles(List<String> paths) {
    Exception firstException = null;
    
    for (String path : paths) {
        try {
            processFile(path);
        } catch (Exception e) {
            if (firstException == null) {
                firstException = e;
            } else {
                firstException.addSuppressed(e);
            }
        }
    }
    
    if (firstException != null) {
        throw new BatchProcessingException(
            "Failed to process some files", 
            firstException
        );
    }
}

Best Practices Cho Production Code

1. Fail Fast Với Validation

// ✅ Validate early
public class UserService {
    public User createUser(String email, String name, int age) {
        // Validate at entry point
        Objects.requireNonNull(email, "Email cannot be null");
        Objects.requireNonNull(name, "Name cannot be null");
        
        if (email.isBlank()) {
            throw new IllegalArgumentException("Email cannot be blank");
        }
        
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException(
                "Invalid age: " + age + " (must be 0-150)");
        }
        
        // Now proceed with confidence
        return userRepository.save(new User(email, name, age));
    }
}

2. Cung Cấp Error Messages Có Ý Nghĩa

// ❌ Bad: Vague error
throw new Exception("Error");
 
// ❌ Bad: No context
throw new IllegalArgumentException("Invalid value");
 
// ✅ Good: Clear, actionable message
throw new IllegalArgumentException(
    "Invalid email format: '" + email + "'. " +
    "Expected format: [email protected]"
);
 
// ✅ Excellent: Include debugging information
throw new OrderProcessingException(
    String.format(
        "Failed to process order %s for customer %s: " +
        "insufficient inventory for product %s (requested: %d, available: %d)",
        order.getId(), 
        order.getCustomerId(),
        product.getSku(),
        requestedQuantity,
        availableQuantity
    )
);

3. Log Đúng Cách

// ✅ Good logging practices
public class PaymentService {
    private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
    
    public void processPayment(Payment payment) {
        logger.info("Processing payment {} for order {}", 
                   payment.getId(), payment.getOrderId());
        
        try {
            gateway.charge(payment);
            logger.info("Payment {} completed successfully", payment.getId());
        } catch (PaymentGatewayException e) {
            // Log with context and exception
            logger.error(
                "Payment gateway failed for payment {}, order {}, amount {}",
                payment.getId(),
                payment.getOrderId(),
                payment.getAmount(),
                e  // Exception as last parameter
            );
            throw e;
        } catch (Exception e) {
            logger.error("Unexpected error processing payment {}", 
                        payment.getId(), e);
            throw new PaymentException("Payment processing failed", e);
        }
    }
}

4. Không Nuốt Exceptions

// ❌ NEVER do this
try {
    riskyOperation();
} catch (Exception e) {
    // Silent failure!
}
 
// ❌ Bad: Log and ignore
try {
    riskyOperation();
} catch (Exception e) {
    e.printStackTrace();  // Don't use this in production
}
 
// ✅ Good: Log and rethrow
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("Operation failed", e);
    throw e;
}
 
// ✅ Good: Log and handle
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("Operation failed, using fallback", e);
    return fallbackValue();
}

5. Sử Dụng Standard Exceptions Khi Thích Hợp

// ✅ Use Java's built-in exceptions
public class ProductService {
    public Product findById(Long id) {
        Objects.requireNonNull(id);  // NullPointerException
        
        return repository.findById(id)
            .orElseThrow(() -> new NoSuchElementException(
                "Product not found: " + id
            ));
    }
    
    public void updatePrice(Long id, BigDecimal newPrice) {
        if (newPrice.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException(
                "Price must be positive: " + newPrice
            );
        }
        
        Product product = findById(id);
        if (product.isDiscontinued()) {
            throw new IllegalStateException(
                "Cannot update price of discontinued product: " + id
            );
        }
        
        product.setPrice(newPrice);
        repository.save(product);
    }
}

Anti-Patterns Cần Tránh

❌ Anti-Pattern 1: Catching Generic Exception

// ❌ Bad: Too broad
try {
    processPayment();
    sendNotification();
    updateInventory();
} catch (Exception e) {
    // What went wrong? Can't tell!
    logger.error("Something failed", e);
}
 
// ✅ Good: Catch specific exceptions
try {
    processPayment();
    sendNotification();
    updateInventory();
} catch (PaymentException e) {
    // Handle payment failure
    refundCustomer();
} catch (NotificationException e) {
    // Notification failed, but payment succeeded
    scheduleRetry();
} catch (InventoryException e) {
    // Inventory update failed
    rollbackPayment();
}

❌ Anti-Pattern 2: Empty Catch Blocks

// ❌ Bad: Silent failure
try {
    updateCache();
} catch (CacheException e) {
}
 
// ✅ Good: At least log it
try {
    updateCache();
} catch (CacheException e) {
    logger.warn("Cache update failed, will retry on next request", e);
}

❌ Anti-Pattern 3: Sử Dụng Exceptions Cho Control Flow

// ❌ Bad: Exceptions for logic
public Integer parseIntOrNull(String value) {
    try {
        return Integer.parseInt(value);
    } catch (NumberFormatException e) {
        return null;  // Expensive way to check!
    }
}
 
// ✅ Good: Use proper validation
public Integer parseIntOrNull(String value) {
    if (value == null || value.isBlank()) {
        return null;
    }
    try {
        return Integer.parseInt(value);
    } catch (NumberFormatException e) {
        logger.debug("Invalid integer format: {}", value);
        return null;
    }
}

❌ Anti-Pattern 4: Throwing Từ Finally

// ❌ Bad: Exception from finally masks original
try {
    processData();
} finally {
    cleanup();  // If this throws, original exception is lost!
}
 
// ✅ Good: Guard finally block
try {
    processData();
} finally {
    try {
        cleanup();
    } catch (Exception e) {
        logger.error("Cleanup failed", e);
    }
}

❌ Anti-Pattern 5: Error Messages Quá Chung Chung

// ❌ Bad: Not helpful
throw new RuntimeException("An error occurred");
 
// ✅ Good: Specific and actionable
throw new OrderValidationException(
    String.format(
        "Order validation failed for order %s: " +
        "shipping address is required for physical products. " +
        "Found %d physical items without shipping address.",
        order.getId(),
        physicalItemCount
    )
);

Các Pattern Xử Lý Lỗi Hiện Đại

Pattern 1: Result Type (Optional + Either Pattern)

// Use Optional for nullable results
public Optional<User> findByEmail(String email) {
    return userRepository.findByEmail(email);
}
 
// Usage
Optional<User> user = userService.findByEmail(email);
user.ifPresent(u -> sendWelcomeEmail(u));
 
// Or with orElseThrow
User user = userService.findByEmail(email)
    .orElseThrow(() -> new UserNotFoundException(email));

Pattern 2: Functional Error Handling

// Chain operations safely
public Optional<String> getUserEmailDomain(Long userId) {
    return userRepository.findById(userId)
        .map(User::getEmail)
        .filter(email -> email.contains("@"))
        .map(email -> email.substring(email.indexOf("@") + 1));
}
 
// With Stream API
public List<Order> getCompletedOrders(Long customerId) {
    return orderRepository.findByCustomerId(customerId)
        .stream()
        .filter(order -> order.getStatus() == OrderStatus.COMPLETED)
        .toList();
}

Pattern 3: Retry Với Exponential Backoff

public class RetryableOperation {
    private static final int MAX_RETRIES = 3;
    private static final long INITIAL_DELAY_MS = 1000;
    
    public <T> T executeWithRetry(Supplier<T> operation) {
        int attempt = 0;
        long delay = INITIAL_DELAY_MS;
        
        while (true) {
            try {
                return operation.get();
            } catch (TransientException e) {
                attempt++;
                if (attempt >= MAX_RETRIES) {
                    throw new MaxRetriesExceededException(
                        "Operation failed after " + MAX_RETRIES + " attempts", 
                        e
                    );
                }
                
                logger.warn("Attempt {} failed, retrying in {}ms", 
                           attempt, delay, e);
                
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Retry interrupted", ie);
                }
                
                delay *= 2;  // Exponential backoff
            }
        }
    }
}
 
// Usage
RetryableOperation retry = new RetryableOperation();
Result result = retry.executeWithRetry(() -> callExternalApi());

Pattern 4: Circuit Breaker Pattern

public class CircuitBreaker {
    private final int threshold;
    private final long timeout;
    private int failureCount = 0;
    private long openedAt = 0;
    private State state = State.CLOSED;
    
    enum State { CLOSED, OPEN, HALF_OPEN }
    
    public <T> T execute(Supplier<T> operation) {
        if (state == State.OPEN) {
            if (System.currentTimeMillis() - openedAt >= timeout) {
                state = State.HALF_OPEN;
            } else {
                throw new CircuitBreakerOpenException(
                    "Circuit breaker is OPEN");
            }
        }
        
        try {
            T result = operation.get();
            reset();
            return result;
        } catch (Exception e) {
            recordFailure();
            throw e;
        }
    }
    
    private void recordFailure() {
        failureCount++;
        if (failureCount >= threshold) {
            state = State.OPEN;
            openedAt = System.currentTimeMillis();
        }
    }
    
    private void reset() {
        failureCount = 0;
        state = State.CLOSED;
    }
}

Xử Lý Exception Trong Spring Boot

Global Exception Handler

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            EntityNotFoundException e) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            "Resource Not Found",
            e.getMessage(),
            Instant.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadRequest(
            IllegalArgumentException e) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Invalid Request",
            e.getMessage(),
            Instant.now()
        );
        return ResponseEntity.badRequest().body(error);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericError(Exception e) {
        logger.error("Unhandled exception", e);
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal Server Error",
            "An unexpected error occurred",
            Instant.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(error);
    }
}
 
record ErrorResponse(
    int status,
    String error,
    String message,
    Instant timestamp
) {}

Controller-Level Exception Handling

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
        try {
            Order order = orderService.createOrder(request);
            return ResponseEntity.status(HttpStatus.CREATED).body(order);
        } catch (InvalidOrderException e) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST, 
                e.getMessage(), 
                e
            );
        } catch (InsufficientInventoryException e) {
            throw new ResponseStatusException(
                HttpStatus.CONFLICT,
                "Insufficient inventory for order",
                e
            );
        }
    }
    
    @ExceptionHandler(OrderException.class)
    public ResponseEntity<ErrorResponse> handleOrderException(
            OrderException e) {
        // Controller-specific handling
        return ResponseEntity.badRequest()
            .body(new ErrorResponse(400, "Order Error", e.getMessage(), 
                                   Instant.now()));
    }
}

Validation Errors

@RestControllerAdvice
public class ValidationExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidation(
            MethodArgumentNotValidException e) {
        
        List<FieldError> fieldErrors = e.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> new FieldError(
                error.getField(),
                error.getDefaultMessage()
            ))
            .toList();
        
        ValidationErrorResponse response = new ValidationErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation Failed",
            fieldErrors,
            Instant.now()
        );
        
        return ResponseEntity.badRequest().body(response);
    }
}
 
record ValidationErrorResponse(
    int status,
    String message,
    List<FieldError> errors,
    Instant timestamp
) {}
 
record FieldError(String field, String message) {}

Testing Exception Handling

JUnit 5 Exception Testing

class PaymentServiceTest {
    
    @Test
    void shouldThrowExceptionForNullPayment() {
        assertThrows(
            IllegalArgumentException.class,
            () -> paymentService.process(null)
        );
    }
    
    @Test
    void shouldThrowExceptionWithCorrectMessage() {
        Exception exception = assertThrows(
            InsufficientFundsException.class,
            () -> paymentService.process(insufficientPayment)
        );
        
        assertTrue(exception.getMessage().contains("Insufficient funds"));
    }
    
    @Test
    void shouldPreserveCauseInWrappedException() {
        PaymentGatewayException exception = assertThrows(
            PaymentGatewayException.class,
            () -> paymentService.processWithGateway(payment)
        );
        
        assertNotNull(exception.getCause());
        assertInstanceOf(IOException.class, exception.getCause());
    }
}

Testing Spring Boot Exception Handlers

@WebMvcTest(OrderController.class)
class OrderControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private OrderService orderService;
    
    @Test
    void shouldReturn404WhenOrderNotFound() throws Exception {
        when(orderService.findById(999L))
            .thenThrow(new EntityNotFoundException("Order not found"));
        
        mockMvc.perform(get("/api/orders/999"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.error").value("Resource Not Found"))
            .andExpect(jsonPath("$.message").value("Order not found"));
    }
    
    @Test
    void shouldReturn400ForInvalidRequest() throws Exception {
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{}"))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.errors").isArray());
    }
}

Tóm Tắt: Các Quy Tắc Vàng

  1. Chọn đúng loại exception: Checked cho có thể khôi phục, unchecked cho lỗi lập trình
  2. Sử dụng try-with-resources: Tự động dọn dẹp ngăn rò rỉ tài nguyên
  3. Fail fast: Validate input tại boundaries, fail ngay lập tức với invalid state
  4. Cung cấp context: Error messages phải rõ ràng, cụ thể, và có thể hành động
  5. Giữ exception chains: Luôn include cause khi wrapping exceptions
  6. Log thích hợp: Error level cho failures, warn cho recoverable issues
  7. Không bao giờ nuốt exceptions: Luôn log hoặc rethrow
  8. Không catch generic Exception: Catch các type cụ thể mà bạn có thể xử lý
  9. Tránh exceptions cho control flow: Sử dụng conditionals thích hợp thay vì
  10. Test exception scenarios: Xác minh xử lý lỗi hoạt động như mong đợi

Các Bước Tiếp Theo

Bây giờ bạn đã hiểu về xử lý exception trong Java, hãy khám phá các chủ đề liên quan:

  • Logging Best Practices: Structured logging, log levels, và monitoring
  • Testing Strategies: Unit testing, integration testing, và test coverage
  • Performance Monitoring: Theo dõi errors trong production với APM tools
  • Resilience Patterns: Circuit breakers, bulkheads, và rate limiting

Làm chủ xử lý exception là rất quan trọng để xây dựng ứng dụng production-ready. Kết hợp với nền tảng Spring Boot từ hướng dẫn getting started của chúng tôi, bạn đã được trang bị tốt để xây dựng ứng dụng Java mạnh mẽ và dễ bảo trì.


Thuộc series Lộ Trình Học Java

Quay lại: Giai Đoạn 3: Core Java APIs

Các Deep Dive Liên Quan:

Bước Tiếp Theo: Bắt Đầu Với Spring Boot

📬 Đă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.