Spring Boot: Global Exception Handling for Controllers

Spring Boot: Global Exception Handling for Controllers

 

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