Spring Boot: Global Exception Handling for Controllers
When developing a Spring Boot application, handling exceptions in a structured and centralized manner is crucial. This ensures that clients interacting with your APIs receive meaningful error responses rather than unhelpful generic messages. In this article, we will explore how to implement a robust global exception handling mechanism using @ControllerAdvice
and @ExceptionHandler
. We’ll also dive into integrating Spring Security’s exception handling to provide consistent error messages for authentication and authorization issues.
1. Understanding @ControllerAdvice
@ControllerAdvice
is a powerful annotation in Spring Boot that allows you to define global exception handling logic for all controllers in your application. By using this annotation, you can centralize your error-handling logic and eliminate repetitive code in individual controllers.
Key Features of @ControllerAdvice
:
- Intercepts exceptions thrown by any controller.
- Provides flexibility to target specific controllers or packages.
- Simplifies response customization.
2. Implementing Global Exception Handling
Let’s create a global exception handler to manage different types of exceptions, including specific ones like ResponseStatusException
and general ones like Exception
.
Step 1: Define the Exception Handler Class
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ResponseStatusException;
@ControllerAdvice
public class GlobalExceptionHandler {
// Handle ResponseStatusException
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Object> handleResponseStatusException(ResponseStatusException ex) {
return ResponseEntity.status(ex.getStatusCode())
.body(Map.of(
"errorCode", ex.getStatusCode().value(),
"message", ex.getReason()
));
}
// Handle NullPointerException (example of specific exception handling)
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<Object> handleNullPointerException(NullPointerException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"errorCode", 500,
"message", "Null Pointer Exception occurred!"
));
}
// Handle all other exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleGeneralException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"errorCode", 500,
"message", ex.getMessage() != null ? ex.getMessage() : "An unexpected error occurred"
));
}
}
How It Works
- The
@ExceptionHandler
annotation specifies the type of exception to handle. - The
ResponseEntity
object allows customizing the HTTP status code and response body. - The
Exception.class
handler acts as a catch-all for unhandled exceptions, ensuring no exception goes unreported.
3. Customizing the Scope of @ControllerAdvice
By default, @ControllerAdvice
applies to all controllers. However, you can limit its scope using the following attributes:
Target Specific Controllers
@ControllerAdvice(assignableTypes = {MyController.class, AnotherController.class})
public class CustomExceptionHandler {
// Exception handling logic here
}
Target Specific Packages
@ControllerAdvice(basePackages = "com.example.myapp.controllers")
public class CustomExceptionHandler {
// Exception handling logic here
}
This flexibility allows you to create specialized exception handlers for different parts of your application.
4. Spring Security Exception Handling
When using Spring Security, exceptions like AccessDeniedException
or AuthenticationException
are handled by default handlers, which may not produce the desired response format. To customize this behavior, you can implement AccessDeniedHandler
and AuthenticationEntryPoint
.
Step 1: Create a Custom AccessDeniedHandler
This handles cases where a user is authenticated but does not have the required permissions (403 Forbidden).
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getWriter().write("{"errorCode": 403, "message": \"Access Denied!\"}");
}
}
Step 2: Create a Custom AuthenticationEntryPoint
This handles cases where authentication fails (401 Unauthorized).
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{"errorCode": 401, "message": \"Unauthorized!\"}");
}
}
Step 3: Register the Custom Handlers
Integrate the custom handlers into the Spring Security configuration.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.accessDeniedHandler(customAccessDeniedHandler()) // 403 Handler
.authenticationEntryPoint(customAuthenticationEntryPoint()) // 401 Handler
);
return http.build();
}
@Bean
public AccessDeniedHandler customAccessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
@Bean
public AuthenticationEntryPoint customAuthenticationEntryPoint() {
return new CustomAuthenticationEntryPoint();
}
}
5. Example Responses
403 Forbidden Response
{
"errorCode": 403,
"message": "Access Denied!"
}
401 Unauthorized Response
{
"errorCode": 401,
"message": "Unauthorized!"
}
Custom Exception Response (e.g., NullPointerException
)
{
"errorCode": 500,
"message": "Null Pointer Exception occurred!"
}
6. Conclusion
Centralized exception handling in Spring Boot using @ControllerAdvice
simplifies error management and ensures consistency across your APIs. By extending this with Spring Security’s exception handling, you can deliver a seamless experience to your API clients with detailed, structured error responses.
Whether you're building a small application or a large-scale system, implementing these strategies will significantly improve the maintainability and usability of your APIs.
Comments
Post a Comment