Người dịch: Nguyễn Như Huy Hoàng.
Link bài viết gốc: RestControllerAdvice Handling

Xử lý Exception, trong bối cảnh rộng hơn của phát triển phần mềm, đề cập đến cách tiếp cận có hệ thống để quản lý và phản hồi các sự kiện hoặc lỗi bất ngờ có thể xảy ra trong quá trình thực thi của một chương trình máy tính. Những Exception này có thể bao gồm các lỗi runtime, network failures , cho đến các vấn đề về xác thực đầu vào. Mục tiêu chính của việc xử lý Exception là ngăn chặn sự cố ứng dụng và cung cấp phản hồi hợp lý cho người dùng khi xảy ra lỗi, từ đó nâng cao độ tin cậy và khả năng chịu lỗi của phần mềm.

Trong hướng dẫn này, chúng ta sẽ đi sâu vào các kỹ thuật và thực hành cụ thể để triển khai xử lý Exception hiệu quả trong Spring, và cách sử dụng annotation @RestControllerAdvice.


Giới thiệu về @RestControllerAdvice

Annotation @RestControllerAdvice là một dạng chuyên biệt của @Component, cho phép tự động phát hiện thông qua classpath scanning. Nó hoạt động như một bộ chặn bao quanh các hoạt động trong các Controller, cho phép ta áp dụng logic chung cho chúng.

Các phương thức trong @RestControllerAdvice (được đánh dấu với @ExceptionHandler) có thể truy cập toàn cầu cho nhiều thành phần @Controller, và mục đích của chúng là bắt các Exception và chuyển chúng thành các phản hồi HTTP. @ExceptionHandler chỉ định loại annotation nào nên được quản lý. Phương thức sẽ nhận instance của Exception và yêu cầu làm đối số phương thức.

Bằng cách kết hợp hai annotation này, ta đạt được những điều sau:

  • Kiểm soát nội dung và mã trạng thái của phản hồi.
  • Khả năng xử lý nhiều Exception trong một phương thức duy nhất.

@RestControllerAdvice hay @ControllerAdvice?

Trong bối cảnh xử lý Exception của Spring Boot, hai annotation chính, @RestControllerAdvice@ControllerAdvice, đóng vai trò khác nhau. Hiểu được sự khác biệt giữa chúng là điều quan trọng để phát triển ứng dụng hiệu quả.

  • @ControllerAdvice: annotation này phù hợp cho các ứng dụng chủ yếu tạo ra các view HTML truyền thống. Khi ứng dụng của bạn tập trung vào việc render các trang web, @ControllerAdvice là lựa chọn phù hợp. Nó xuất sắc trong việc xử lý Exception trong ngữ cảnh của các view web và cung cấp phương tiện để tùy chỉnh phản hồi lỗi ở định dạng HTML.
  • @RestControllerAdvice: Ngược lại, @RestControllerAdvice tìm thấy vị trí của mình trong các ứng dụng chủ yếu cung cấp dữ liệu dưới dạng JSON, XML, hoặc các định dạng có cấu trúc khác. Điều này đặc biệt phổ biến trong các dịch vụ web RESTful, nơi nhiệm vụ chính là cung cấp dữ liệu cho các client. @RestControllerAdvice được thiết kế để quản lý Exception và tùy chỉnh các phản hồi lỗi đặc biệt cho các tình huống tập trung vào dữ liệu này.

Tuy nhiên, @ControllerAdvice cũng có thể được sử dụng cho các dịch vụ REST, nhưng cần lưu ý những điều sau:
@RestControllerAdvice là một annotation được hợp thành với cả annotation @ControllerAdvice@ResponseBody, điều này có nghĩa là các phương thức @ExceptionHandler sẽ được render thành phần thân phản hồi thông qua chuyển đổi thông điệp (thay vì giải quyết view hoặc render template). Vì vậy, @ControllerAdvice cũng có thể được sử dụng cho các dịch vụ web REST, nhưng bạn cần sử dụng thêm @ResponseBody.

Ưu điểm của việc sử dụng @RestControllerAdvice

Khám phá những lợi ích của @RestControllerAdvice và vai trò của nó trong việc xử lý Exception trong Spring cho các ứng dụng tập trung vào dữ liệu:

  1. Phản hồi lỗi nhất quán: @RestControllerAdvice cho phép các nhà phát triển tạo ra các phản hồi lỗi nhất quán và có cấu trúc trong các định dạng như JSON hoặc XML. Sự chuẩn hóa này tăng cường giao tiếp giữa ứng dụng và các client, giúp dễ dàng diễn giải và xử lý lỗi một cách có lập trình.

  2. Chính xác trong môi trường RESTful: Dịch vụ RESTful thường yêu cầu độ chính xác cao trong “xử lý Exception trong Spring”. Với @RestControllerAdvice, bạn có thể tùy chỉnh thông báo lỗi để đáp ứng nhu cầu cụ thể của các client API RESTful của bạn, đảm bảo rằng họ nhận được các phản hồi thông tin và phù hợp với ngữ cảnh.

  3. Cải thiện xác thực dữ liệu: Xác thực dữ liệu là rất quan trọng trong các dịch vụ RESTful. @RestControllerAdvice giúp đơn giản hóa việc xác thực dữ liệu đầu vào và phản hồi nhanh chóng với các thông báo lỗi xác thực rõ ràng, giúp duy trì tính toàn vẹn dữ liệu và giảm nguy cơ nhập dữ liệu sai.

  4. Đơn giản hóa việc xử lý Exception: Xử lý Exception trở nên đơn giản hơn với @RestControllerAdvice. Bằng cách hợp nhất logic xử lý lỗi vào một nơi, nó làm giảm sự phức tạp của mã và giảm sự dư thừa, dẫn đến cơ sở mã sạch hơn và dễ bảo trì hơn.

  5. Tiếp cận tập trung: @RestControllerAdvice phù hợp với các ứng dụng chủ yếu tập trung vào việc cung cấp dữ liệu. Sự chuyên biệt này cho phép các nhà phát triển giải quyết các Exception và lỗi theo cách phù hợp với bản chất hướng dữ liệu của các ứng dụng của họ.

Những ưu điểm này nhấn mạnh tầm quan trọng của @RestControllerAdvice trong bối cảnh các dịch vụ RESTful và ứng dụng tập trung vào dữ liệu. Bằng cách tận dụng khả năng của nó, các nhà phát triển có thể đảm bảo rằng các ứng dụng của họ không chỉ xử lý lỗi một cách hợp lý mà còn cung cấp trải nghiệm mượt mà và đáng tin cậy cho người dùng và khách hàng của họ.

Trong các phần tiếp theo, chúng ta sẽ khám phá các ví dụ thực tế và đi sâu vào các thực hành tốt nhất để khai thác tối đa tiềm năng của @RestControllerAdvice trong các dự án dựa trên Spring của bạn.

Ví dụ thực tế:

Để kiểm tra chức năng của @RestControllerAdvice và các trường hợp sử dụng khác nhau, chúng ta sẽ định nghĩa hai trường hợp nhỏ, nơi chúng ta sẽ tạo một khách hàng mới và tìm kiếm nó bằng cách sử dụng định danh của nó. Để làm điều này, tôi mang đến cho bạn một phương pháp xử lý lỗi tùy chỉnh mà tôi thấy khá sạch sẽ và mạnh mẽ:

Bắt đầu với:

@RestController
@RequiredArgsConstructor
public class ClientController {

    private final ClientService clientService;

    @PostMapping("api/v1/clients")
    public ClientDTO create(@RequestBody ClientDTO clientDTO) {
        try {
            return this.clientService.create(clientDTO);
        } catch (ClientAlreadyExistsException ex) {
            System.out.println("Client already exists");
            throw new CustomException1(ex.getMessage());

        } catch (EmailBadFormatException ex) {
            System.out.println("Email has bad format");
            throw new CustomException2(ex.getMessage());
        }
    }

    @GetMapping("api/v1/clients/{id}")
    public ClientDTO find(@PathVariable Long id) {
        try {
            return this.clientService.find(id);
        } catch (ClientNotFoundException ex) {
            System.out.println("Client not found");
            throw new CustomException3(ex.getMessage());

        }
    }
}

Như bạn có thể thấy, đây là một ví dụ rất cơ bản về xử lý Exception, nhưng bạn có thể thấy cách độ phức tạp của controller tăng lên khi số lượng điều kiện trong ứng dụng của chúng ta tăng lên, do đó ảnh hưởng đến tính dễ đọc của mã.

Để cải thiện điều này, chúng ta sẽ tạo @RestControllerAdvice của mình, sẽ là điểm trung tâm xử lý Exception, từ đó làm sạch phần còn lại của ứng dụng khỏi nhiệm vụ khó khăn này.

Ban đầu, chúng ta sẽ định nghĩa Exception “trừu tượng” của mình phải kế thừa từ RuntimeException và nơi chúng ta sẽ định nghĩa các trường mà chúng ta muốn thu thập trong Exception của mình. Lớp này sẽ là cha của phần còn lại của các Exception mà chúng ta định nghĩa trong ứng dụng của mình:

@Getter
@Setter
@AllArgsConstructor
public class AbstractException extends RuntimeException {

    private String message;

    private Map<String, String> details;

}

Trong trường hợp của chúng ta, chúng ta chỉ muốn thu thập một thông điệp tùy chỉnh và một bản đồ chi tiết nơi chúng ta có thể cung cấp thông tin chi tiết hơn về Exception đã xảy ra (định nghĩa của các trường là theo nhu cầu của ứng dụng hoặc trí tưởng tượng của từng nhà phát triển).

Tiếp theo, chúng ta sẽ tạo 2 Exception tùy chỉnh: một cho các cuộc gọi tới API Rest của chúng ta không hợp lệ và một cho khi một thực thể cụ thể được tìm thấy trong ứng dụng.

public class EntityNotFoundException extends AbstractException {
    
    public EntityNotFoundException(String message, Map<String, String> details) {
        super(message, details);
    }
}
public class InvalidCallException extends AbstractException {

    public InvalidCallException(String message, Map<String, String> details) {
        super(message, details);
    }
}

Như bạn có thể thấy, chúng chỉ đơn giản gọi constructor của AbstractException mà chúng ta đã tạo trước đó. Tại thời điểm này, điều này có thể tùy chỉnh, ví dụ chúng ta có thể định nghĩa một thông điệp tĩnh cho mỗi loại Exception và trong details chúng ta có thông tin cụ thể hơn về Exception của chúng ta.

Cuối cùng, chúng ta định nghĩa bộ xử lý Exception toàn cầu của mình cùng với DTO lỗi mà chúng ta muốn gửi tới client (frontend) để được hiển thị trên màn hình hoặc xử lý theo cách mà nó cho là phù hợp nhất:

@Data
@Builder
public class ErrorMessageDTO implements Serializable {

    private String message;

    private Instant date;

    private Map<String, String> details;

}
@RestControllerAdvice
public class GlobalExceptionResponseHandler {
    
    @ExceptionHandler(value = {InvalidCallException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorMessageDTO mapInvalidCallException(InvalidCallException ex, WebRequest request) {

        return ErrorMessageDTO.builder()
                .message(ex.getMessage())
                .date(Instant.now())
                .details(ex.getDetails())
                .build();
    }

    @ExceptionHandler(value = {EntityNotFoundException.class})
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorMessageDTO mapNotFoundException(EntityNotFoundException ex, WebRequest request) {

        return ErrorMessageDTO.builder()
                .message(ex.getMessage())
                .date(Instant.now())
                .details(ex.getDetails())
                .build();
    }
}

Hoạt động của bộ xử lý Global Exception của chúng ta khá đơn giản, chúng ta chỉ cần chú thích lớp với @RestControllerAdvice và định nghĩa một phương thức cho mỗi loại Exception tùy thuộc vào HTTP Status mà chúng ta muốn gửi tới client. Trong mỗi phương thức, chúng ta cần định nghĩa trong @ExceptionHandler Exception sẽ được xử lý trong phương thức đó + @ResponseStatus chỉ ra trạng thái HTTP sẽ được gửi tới client.

Tại thời điểm này, mỗi phương thức xử lý Exception được chỉ định và được xây dựng trong DTO phản hồi dựa trên các trường của AbstractException (nó là Exception cha định nghĩa các trường được sử dụng bởi InvalidCallException và EntityNotFoundException).

Tất cả kiến trúc này có thể tùy chỉnh, ví dụ chúng ta cũng có thể định nghĩa một ExceptionMessagesEnum nơi chúng ta định nghĩa các thông điệp mà chúng ta muốn gửi tới client và sau đó tái sử dụng enum này khi gửi Exception của loại mà chúng ta cho là phù hợp. Ví dụ:

@Getter
@AllArgsConstructor
public enum ExceptionMessagesEnum {
    
    CLIENT_NOT_FOUND("Client not found"),
    
    EMAIL_BAD_FORMAT("Email has bad format"),
    
    CLIENT_ALREADY_EXISTS("Client already exists");
    
    private final String description;
}

Trong service:

public ClientDTO find(Long id) {
    return this.clientRepository.findById(id)
            .map(this.mapper::asDto)
            .orElseThrow(() -> new EntityNotFoundException(CLIENT_NOT_FOUND.getDescription(), Map.of("ID", String.valueOf(id))));
}

Kết quả:

result

Chúng ta thậm chí có thể thêm một cải tiến: nếu ứng dụng của chúng ta có Quốc tế hóa (Internationalization), chúng ta có thể định nghĩa lại trường description trong enum exception để chỉ ra khóa của các file messages.properties. Vì vậy, trong bộ xử lý global exception của chúng ta, chúng ta sẽ tiêm bean MessageSource để lấy thông điệp được quốc tế hóa dựa trên trường mới này.

Tham khảo: Spring Boot Internationalization

Ví dụ:

@Getter
@AllArgsConstructor
public enum ExceptionMessagesEnum {
    
    CLIENT_NOT_FOUND("error.client.not.found"),
    
    EMAIL_BAD_FORMAT("error.email.bad.format"),
    
    CLIENT_ALREADY_EXISTS("error.client.already.exists");
    
    private final String key;
}

Các phương pháp tốt nhất

Trong việc xử lý Exception trong Spring, tuân theo các phương pháp sau là rất quan trọng để đảm bảo tính kiên cường và độ tin cậy của các ứng dụng của bạn. Dưới đây là một số khuyến nghị chính:

  1. Lựa Chọn Exception Chính Xác: Xử lý Exception bắt đầu bằng việc chọn đúng loại Exception . Chọn những Exception phản ánh chính xác kịch bản lỗi để cung cấp phản hồi có ý nghĩa.

  2. Thông Điệp Lỗi Tùy Chỉnh: Tùy chỉnh thông điệp lỗi để truyền tải thông tin liên quan đến người dùng và nhà phát triển. Bao gồm mã lỗi, mô tả và hướng dẫn giải quyết.

  3. Ghi Log và Giám Sát: Triển khai ghi log mạnh mẽ để nắm bắt chi tiết lỗi. Ngoài ra, thiết lập giám sát để nhận diện và giải quyết các vấn đề tái diễn một cách chủ động.

  4. Xử Lý Exception Toàn Cục: Xem xét việc sử dụng bộ xử lý Exception toàn cục để tập trung hóa logic xử lý Exception chung. Điều này cải thiện khả năng bảo trì mã và giảm sự trùng lặp.

  5. Kiểm Tra và Xác Thực: Kiểm tra kỹ lưỡng logic xử lý Exception của bạn để đảm bảo nó hoạt động như dự định. Xác thực dữ liệu đầu vào để bắt lỗi trước khi chúng xảy ra.

  6. Tài Liệu Hóa: Tài liệu hóa các chiến lược xử lý Exception và các phản hồi được cung cấp. Tài liệu này là nguồn tài nguyên quý giá cho nhóm phát triển của bạn và người dùng API.

  7. Cập Nhật Thường Xuyên: Xem xét và cập nhật cơ chế xử lý Exception khi ứng dụng của bạn phát triển. Đảm bảo chúng luôn phù hợp với các yêu cầu đang phát triển của ứng dụng.

  8. Xem Xét Bảo Mật: Khi xử lý Exception , hãy lưu ý đến các lỗ hổng bảo mật tiềm ẩn. Tránh lộ thông tin nhạy cảm trong thông điệp lỗi.

Bằng cách tuân theo các phương pháp tốt nhất này, bạn có thể tạo ra một cơ chế Xử lý Exception trong Spring mạnh mẽ và tin cậy không chỉ nâng cao trải nghiệm người dùng mà còn đơn giản hóa nỗ lực phát triển và khắc phục sự cố.

Kết Luận và Bổ Sung tài liệu

Kết luận, hướng dẫn này đã cung cấp cái nhìn toàn diện về Xử lý Exception trong Spring với trọng tâm đặc biệt vào annotation @RestControllerAdvice. Chúng ta đã khám phá các nguyên tắc cơ bản của Xử lý Exception trong Spring, nhấn mạnh vai trò quan trọng của nó trong việc xây dựng các ứng dụng mạnh mẽ và tin cậy.

Khi bạn bắt đầu hành trình nâng cao tính kiên cường cho ứng dụng của mình, hãy nhớ tuân theo các phương pháp hay nhất, tùy chỉnh thông điệp lỗi và duy trì độ chính xác trong các phản hồi lỗi. Liên tục cập nhật và kiểm tra cơ chế xử lý Exception của bạn để phù hợp với các yêu cầu đang phát triển của ứng dụng.

Để mở rộng thêm kiến thức và kỹ năng về Spring Framework và Xử lý Exception, hãy xem xét các tài liệu tham khảo sau:

  1. Tài liệu Spring Framework: Tài liệu chính thức của Spring Framework cung cấp các thông tin chi tiết, hướng dẫn và tài liệu tham khảo giúp bạn nắm vững các thành phần và tính năng khác nhau của Spring.

  2. Cộng Đồng Trực Tuyến: Tham gia vào các cộng đồng trực tuyến, diễn đàn và bảng thảo luận liên quan đến Spring Framework. Các nền tảng như Stack Overflow và Spring Community Forum cung cấp không gian cho các nhà phát triển tìm kiếm sự giúp đỡ và chia sẻ kiến thức.

  3. Khóa Học Spring Nâng Cao: Xem xét việc đăng ký các khóa học Spring nâng cao hoặc chứng chỉ để nâng cao chuyên môn của bạn về Spring Framework và các tính năng nâng cao của nó.

  4. Sách: Khám phá các cuốn sách dành riêng cho Spring Framework và Xử lý Exception để có được kiến thức sâu sắc và các hiểu biết thực tiễn từ các tác giả có kinh nghiệm.

Bằng cách tiếp tục học hỏi và áp dụng các phương pháp hay nhất, bạn sẽ được trang bị tốt để xử lý exception một cách hiệu quả và xây dựng các ứng dụng kiên cường trong thế giới động của Spring Framework. Xử lý Exception trong Spring là một khía cạnh quan trọng của phát triển ứng dụng, và chuyên môn của bạn trong lĩnh vực này sẽ đặt nền tảng cho phần mềm tin cậy và thân thiện với người dùng.

HAPPY CODING! 🎯