no

Hands-on Coding: Spring Boot Common Exceptions Handling


Overview

What is an exception?

An exception is an unexpected event, behavior, or state during software execution. In Java, it is a subclass of java.lang.Throwable.


In this diagram, we can see two types of Throwable. 

  1. Exception - recoverable
  2. Error - unrecoverable
Exception VS RuntimeException

Exception - we need to declare the thrown exception in the method signature

public String readFromInputStream(InputStream inputStream) throws IOException {
  StringBuilder resultStringBuilder = new StringBuilder();
  try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
    String line;
    // BufferedReader.readLine throws IOException
    while ((line = br.readLine()) != null) {
      resultStringBuilder.append(line).append("\n");
    }
  }
  return resultStringBuilder.toString();
}
  

RuntimeException - there is no need to add the exception in the method signature

public void rtException() {
  int arr[] = null; // null array
  System.out.println("NullPointerException: " + arr.length);
}
  

In the calling block, we can catch the exception that was thrown.

try {
  readFromInputStream(is);
  rtException()
  
} catch (IOException ioe) {
  //
} catch (NullPointerException npe) {
  //  
}
  

Pre-Spring Boot 3

Before Spring Boot 3 there are several approaches on how we handle the exceptions. This article is opinionated so I will just list them for reference.

  • @ExceptionHandler annotation inside a controller
  • Implement HandlerExceptionResolver or extend AbstractHandlerExceptionResolver
  • Extending ResponseStatusException
  • Zalando exception handling library

Microservice

Microservice is a software architectures that build products as collections of small services. Often, these services are powered by Spring Boot, and they interact with each other via HTTP calls through REST endpoints. Before Spring Boot 3, developers often used Zalando to wrap and handle exceptions, but Spring patched this gap and now provides an elegant solution.

Commons Web Exceptions

Commons Web Exceptions is a project I created in Spring Boot 3, it is built around:

  • RestControllerAdvice - for centralizing the exceptions
  • ResponseEntityExceptionHandler - for providing the basic exception information structure
  • ErrorResponseException - for initializing an exception

In this image, all the classes and enums inside the blue block are part of the commons web exceptions library that we can use and extend in our service.

  • NativeWebExceptionEnumCodes - is an enum where native exceptions such as BAD_REQUEST, and INVALID_FORMAT are defined. In here, we can also add the class name of an exception in case we want to provide a specific code. Eg. HTTP_REQUEST_METHOD_NOT_SUPPORTED_EXCEPTION.
  • AbstractWebExceptions - is a container for all the native and service-defined exceptions that we can register by extending this class.
  • WebBaseException - a model class that extends ErrorResponseException. It provides the basic structure of the exception.
  • AbstractWebExceptionHandler - which extends the ResponseEntityExceptionHandler class. It gives us default handlers for the most common exceptions such as HttpRequestMethodNotSupportedException, HttpMediaTypeNotSupportedException, and HttpMediaTypeNotAcceptableException. In this class, we override some methods that will allow us to provide custom decorations to our exceptions. For example, when handling the invalid method argument exception we can list the errors (eg notNull) in the custom property "ERRORS".

Usage

In this section, I will provide some examples of how we can use this library.

Initializing the Library

Before we can define our custom exception handlers, we should extend the necessary base classes first.

1. Define the service's exception codes as enum. For example, in one of the services I have in Hivemaster, a custom Keycloak project that provides a multi-tenant feature.

@Getter
public enum AppExceptionCodes {

  USER_CREATION_FAILED("A1001", "Use creation failed"),
  USER_EID_NOT_FOUND("A1002", "User EID not found"),
  USER_EMAIL_NOT_FOUND("A1003", "User email not found"),
  USER_PHONE_NOT_FOUND("A1004", "User phone not found"),
  ORGANIZATION_NOT_FOUND("A1005", "Organization not found");

  private String code;
  private String message;

  AppExceptionCodes(String code, String message) {
    this.code = code;
    this.message = message;
  }

  public static Map<String, String> getMapValues() {

    Map<String, String> map = new LinkedHashMap<>();
    for (AppExceptionCodes errCode : values()) {
      map.put(errCode.getCode(), errCode.getMessage());
    }

    return map;
  }
}
  

2. Register the exception codes by extending the class AbstractWebExceptionCodes, so that they can be accessed by the commons-exception project.

@Component
public class WebExceptions extends AbstractWebExceptions {

  @Value("${spring.application.name}")
  private String serviceName;

  public WebExceptions() {

    super(HttpStatus.OK);

    registerExceptionMap(AppExceptionCodes.getMapValues());
  }

  @Override
  public String getServiceName() {
    return serviceName;
  }
}
  

3. Extend the WebBaseException class. So that we can handle business exceptions specific to the service. This class extends ErrorResponseException and RuntimeException which should be extended by the service exception classes. This will allow us to override the decoration that happens on the base WebBaseException class.

public class WebException extends WebBaseException {

  public WebException(HttpStatusCode status, String code) {
    this(status, code, null);
  }

  public WebException(HttpStatusCode status, String code, String message) {
    super(status, code, message);
  }
}
  

4. Extend the base exception handler AbstractWebExceptionHandler, which provides custom error handling and decoration for exceptions like method argument, runtime, invalid format, etc.

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class WebExceptionHandler extends AbstractWebExceptionHandler {

  private final WebExceptions webExceptions;

  @Override
  public String getServiceName() {
    return webExceptions.getServiceName();
  }
}
  

5. And finally, import the library into your project.

@EnableConfigurationProperties
@SpringBootApplication
@Import({WebExceptionHandlerConfig.class})
public class Application {}
  

Use Cases

Once all of these are done, we can now begin customizing our exceptions.

1. Handling native exceptions such as method invalid argument.

Here are the possible results that we may get:

Using custom assertion

{
    "type": "http://localhost:8080/errors/S404",
    "title": "Bad Request",
    "status": 400,
    "detail": "Validation failed for fields (object:userV1, field:emailOrPhoneOnly, message:AssertTrue)",
    "instance": "/api/native-exceptions/method-arguments",
    "code": "S404",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:41:21.660650900Z",
    "errors": [
        "object:userV1, field:emailOrPhoneOnly, message:AssertTrue"
    ]
}
  

Missing field

{
    "type": "http://localhost:8080/errors/S404",
    "title": "Bad Request",
    "status": 400,
    "detail": "Validation failed for fields (object:userV1, field:organization, message:NotNull)",
    "instance": "/api/native-exceptions/method-arguments",
    "code": "S404",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:43:25.418080500Z",
    "errors": [
        "object:userV1, field:organization, message:NotNull"
    ]
}
  

Invalid format

{
    "type": "http://localhost:8080/errors/S401",
    "title": "Bad Request",
    "status": 400,
    "detail": "Invalid format for (field: birthdate, value: xxx, type: Instant)",
    "instance": "/api/native-exceptions/method-arguments",
    "code": "S401",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:43:44.065740700Z",
    "errors": [
        "field:birthdate, value:xxx, type:Instant"
    ]
}
  

Invalid date

{
    "type": "http://localhost:8080/errors/S404",
    "title": "Bad Request",
    "status": 400,
    "detail": "Validation failed for fields (object:userV1, field:birthdate, message:Past)",
    "instance": "/api/native-exceptions/method-arguments",
    "code": "S404",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:44:28.829450600Z",
    "errors": [
        "object:userV1, field:birthdate, message:Past"
    ]
}
  

Missing resource

{
    "type": "http://localhost:8080/errors/S405",
    "title": "Not Found",
    "status": 404,
    "detail": "No static resource native-exceptions/resource-not-found.",
    "instance": "/api/native-exceptions/resource-not-found",
    "code": "S405",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:44:44.294238700Z",
    "errors": []
}
  

Unsupported method

{
    "type": "http://localhost:8080/errors/S402",
    "title": "Method Not Allowed",
    "status": 405,
    "detail": "Method 'PUT' is not supported.",
    "instance": "/api/native-exceptions/method-arguments",
    "code": "S402",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:45:00.240704900Z",
    "errors": []
}
  

Forbidden

{
    "type": "http://localhost:8080/errors/S501",
    "title": "Forbidden",
    "status": 403,
    "instance": "/api/native-exceptions/forbidden",
    "code": "S501",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:45:20.566494300Z",
    "errors": []
}
  

Exception defined in our service exception enum which could be business, application, or entity.

{
    "type": "http://localhost:8080/errors/A1001",
    "title": "Use creation failed",
    "status": 400,
    "instance": "/api/service-exceptions/users/exceptions",
    "code": "A1001",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:55:49.061434900Z",
    "errors": []
}

Custom exception

{
    "type": "http://localhost:8080/errors/B1001",
    "title": "Business exception",
    "status": 400,
    "detail": "Custom exception message",
    "instance": "/api/service-exceptions/users/custom-exceptions",
    "code": "B1001",
    "service": "commons-web-exception-client",
    "timestamp": "2024-04-20T09:56:30.612646200Z",
    "errors": []
}
  

Git Repository

Related

spring-rest 8271294245588918610

Post a Comment Default Comments

item