Global Exception Handling in Spring Boot with @ControllerAdvice and Multilingual Support
Here's a generic response class in Java that can be used across all API responses in a Spring Boot application. This class provides a consistent structure for API responses, including a status code, message, and data.
public class ApiResponse<T> {
??? private int status;
??? private String message;
??? private T data;
??? public ApiResponse() {
??? }
??? public ApiResponse(int status, String message, T data) {
??????? this.status = status;
??????? this.message = message;
??????? this.data= data;
??? }
??? public int getStatus() {
??????? return status;
??? }
??? public void setStatus(int status) {
??????? this.status = status;
??? }
??? public String getMessage() {
??????? return message;
??? }
??? public void setMessage(String message) {
??????? this.message = message;
??? }
??? public T getData() {
??????? return data;
??? }
??? public void setData(T data) {
??????? this.data = data;
??? }
??? public static <T> ApiResponse<T> success(T data) {
??????? return new ApiResponse<>(HttpStatus.OK.value(), getLocalizedMessage("success"), data);
??? }
??? public static <T> ApiResponse<T> error(int status, String messageKey, Locale locale) {
??????? return new ApiResponse<>(status, getLocalizedMessage(messageKey, locale), null);
??? }
??? private static String getLocalizedMessage(String key, Locale locale) {
??????? ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
??????? return bundle.getString(key);
??? }
}
This ApiResponse<T> class provides a generic way to wrap API responses in a structured format.
You can use ApiResponse.success(data) for successful responses and ApiResponse.error(status, message) for errors.
In a Spring Boot application, the best practice is for the service layer to return only the business data (domain objects or DTOs), while the controller is responsible for wrapping the response in an ApiResponse<T> object.
Why?
?
Alternative Approach (Less Recommended)
If the service layer directly returns ApiResponse<T>, then it becomes tightly coupled with HTTP response handling, which reduces flexibility and maintainability.
?
Example Implementation
Service Layer (Returning Business Data)
@Service
public class UserService {
??? private final UserRepository userRepository;
??? public UserService(UserRepository userRepository) {
??????? this.userRepository = userRepository;
??? }
??? public List<User> getAll () {
??????? return userRepository.findAll();
??? }
??? public Page<User> getAll(Pageable pageable) {
??????? return userRepository.findAll(pageable);
??? }
??? public User getById(Long id, Locale locale) {
??????? return userRepository.findById(id)
??????????????? .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "user.not.found"));
??? }
??? public User insert (User user) {
??????? return userRepository.save(user);
??? }
??? public User update (Long id, User updatedUser, Locale locale) {
??????? return userRepository.findById(id).map(user -> {
??????????? user.setTitle(updatedUser.getTitle());
??????????? user.setContent(updatedUser.getContent());
??????????? return userRepository.save(user);
??????? }).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "user.not.found"));
??? }
??? public void delete(Long id, Locale locale) {
??????? if (!userRepository.existsById(id)) {
??????????? throw new ResponseStatusException(HttpStatus.NOT_FOUND, "user.not.found");
??????? }
??????? userRepository.deleteById(id);
??? }
}
Controller (Wrapping the Response)
@RestController
@RequestMapping("/users")
public class UserController {
??? private final UserService userService;
??? public UserController(UserService userService) {
??????? this.userService = userService;
??? }
??? @GetMapping
??? public ResponseEntity<ApiResponse<List<User>>> getAll() {
??????? return ResponseEntity.ok(ApiResponse.success(userService.getAll()));
??? }
??? @GetMapping("/paged")
??? public ResponseEntity<ApiResponse<Page<User>>> getAllUsersPageable(Pageable pageable) {
??????? return ResponseEntity.ok(ApiResponse.success(userService.getAll (pageable)));
??? }
??? @GetMapping("/{id}")
??? public ResponseEntity<ApiResponse<User>> getById(@PathVariable Long id, Locale locale) {
??????? return ResponseEntity.ok(ApiResponse.success(userService.getById(id, locale)));
??? }
??? @PostMapping
??? public ResponseEntity<ApiResponse<User>> insert (@RequestBody User user) {
??????? return ResponseEntity.ok(ApiResponse.success(userService.insert (user)));
??? }
??? @PutMapping("/{id}")
??? public ResponseEntity<ApiResponse<User>> update(@PathVariable Long id, @RequestBody User updatedUser, Locale locale) {
??????? return ResponseEntity.ok(ApiResponse.success(userService.update (id, updatedUser, locale)));
??? }
??? @DeleteMapping("/{id}")
??? public ResponseEntity<ApiResponse<Void>> delete (@PathVariable Long id, Locale locale) {
??????? userService.delete (id, locale);
??????? return ResponseEntity.ok(ApiResponse.success(null));
??? }
}
Centralize Exception Handling
@RestControllerAdvice is an annotation in Spring Boot that allows you to handle exceptions globally across multiple controllers. It centralizes exception handling, making the code cleaner and more maintainable by avoiding repetitive try-catch blocks in controllers.
Key Features of @RestControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
??? private String getLocalizedMessage(String key, Locale locale) {
??????? ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
??????? return bundle.getString(key);
??? }
??? @ExceptionHandler(ResponseStatusException.class)
??? public ResponseEntity<ApiResponse<Void>> handleResponseStatusException(ResponseStatusException ex, Locale locale) {
??????? String message = getLocalizedMessage(ex.getReason(), locale);
??????? return ResponseEntity.status(ex.getStatusCode())
??????????????? .body(ApiResponse.error(ex.getStatusCode().value(), message));
??? }
??? @ExceptionHandler(Exception.class)
??? public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex, Locale locale) {
??????? String message = getLocalizedMessage("internal.server.error", locale);
??????? return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
??????????????? .body(ApiResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), message));
??? }
}