Skip to content

Spring Lemon Exceptions Guide

Sanjay Patel edited this page Apr 8, 2021 · 5 revisions

The spring-lemon-exceptions library translates exceptions that occur when processing requests into standard error responses. It also provides you utilities for doing REST API validation elegantly.

For example, when a ConstraintViolationException is thrown in your application due to a user inputting some invalid data, it would respond with the following 422 response:

{
    "exceptionId": "ConstraintViolationException",
    "error": "Unprocessable Entity",
    "message": "Validation Error",
    "status": 422,
    "errors": [
        {
            "field": "user.captchaResponse",
            "code": "{com.naturalprogrammer.spring.wrong.captcha}",
            "message": "Looks like you are a robot! Please try again."
        },
        {
            "field": "user.password",
            "code": "{com.naturalprogrammer.spring.blank.password}",
            "message": "Password needed"
        }
    ]
}

As we'll see in this guide, spring-lemon-exceptions is quite extensible and customizable to easily take care of sophisticated exception handling and validation needs.

Adding to your Spring Boot project

Add the following to your pom.xml (or the equivalent to build.gradle):

<dependencies>

    <dependency>
        <groupId>com.naturalprogrammer.spring-lemon</groupId>
        <artifactId>spring-lemon-exceptions</artifactId>
        <version>${spring-lemon.version}</version><!-- See https://github.com/naturalprogrammer/spring-lemon/releases for the latest release -->
    </dependency>   
    ...
</dependencies>

<repositories>

    <repository>
        <id>naturalprogrammer</id>
        <url>https://naturalprogrammer.github.io/mvn-repository</url>
    </repository>
    ...
</repositories>

Basic Building Blocks

ErrorResponse

ErrorResponse is the DTO that represents the error response that you see at the top of this page. It looks as below:

public class ErrorResponse {
	
	private String exceptionId;
	private String error;
	private String message;
	private Integer status;
	private Collection<LemonFieldError> errors;

        ...
}

LemonFieldError

Noticed Collection<LemonFieldError> in the ErrorResponse above? It contains a collection of all the individual errors. It looks as below:

public class LemonFieldError {
	
	// Name of the field having error. Null in case of a global form level error. 
	private String field;
	
	// Error code. Typically the I18n message-code.
	private String code;
	
	// Error message
	private String message;

        ...
}

Exception handlers

To translate exceptions to ErrorResponses, spring-lemon-exceptions would use exception handlers. Exception handlers are classes inheriting AbstractExceptionHandler. One exception handler translates one particular exception type. So, you will need one exception handler per exception type that your application throws. Spring Lemon already comes with a collection of built-in exception handlers, and you can easily add your own handlers by extending AbstractExceptionHandler or one of its subclasses.

AbstractExceptionHandler

AbstractExceptionHandler looks as below:

public abstract class AbstractExceptionHandler<T extends Throwable> {
    
    private Class<?> exceptionClass;
    
    public AbstractExceptionHandler(Class<?> exceptionClass) {
        this.exceptionClass = exceptionClass;
    }

    public Class<?> getExceptionClass() {
        return exceptionClass;
    }
    
    protected String getExceptionId(T ex) {
        return LexUtils.getExceptionId(ex);
    }

    protected String getMessage(T ex) {
        return ex.getMessage();
    }
    
    protected HttpStatus getStatus(T ex) {
        return null;
    }
    
    protected Collection<LemonFieldError> getErrors(T ex) {
        return null;
    }

    public ErrorResponse getErrorResponse(T ex) {
        
        ErrorResponse errorResponse = new ErrorResponse();
        
        errorResponse.setExceptionId(getExceptionId(ex));
        errorResponse.setMessage(getMessage(ex));
        
        HttpStatus status = getStatus(ex);
        if (status != null) {
            errorResponse.setStatus(status.value());
            errorResponse.setError(status.getReasonPhrase());
        }
        
        errorResponse.setErrors(getErrors(ex));
        return errorResponse;
    }
}

The getErrorResponse method above is called for translating an exception to an ErrorRespone. As you see above, it in turn calls the getExceptionId, getMessage, getStatus and getErrors methods. So, when coding a handler, you can override any of these methods.

Exception handlers are configured as Spring beans so that Spring Lemon finds them. So, don't forget to annotate your custom handlers with @Component.

Mapping exception types to handlers

How does Spring Lemon know which handler to use for an exception?

Did you notice the getExceptionClass method above? That tells Spring Lemon the exception type a particular handler would handle.

When the application starts, Spring Lemon has an ErrorResponseComposer that injects all the exception handler beans into a map, with the key as the exception types. Technically it's a Map<Class, AbstractExceptionHandler>.

Then, when an exception is caught, ErrorResponseComposer searches for its type in that map. That's how a handler is chosen.

Overriding built-in handlers

When ErrorResponseComposer finds multiple handlers for the same exception type, it picks the one with highest precedence. So, if you want to override a built-in handler, define yours with a higher precedence, as below:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MyMultiErrorExceptionHandler extends AbstractExceptionHandler<MultiErrorException> {

   ...

}

All built-in handlers are defined as LOWEST_PRECEDENCE, and so yours will get priority.

ErrorResponseComposer

ErrorResponseComposer is the class that picks the right handler from the map of handlers it would have built on startup, and then uses it to make the ErrorResponse.

Catching the exceptions

So, when an exception occurs in our application, we should

  1. Catch it
  2. Translate it to an ErrorResponse by calling the compose method of ErrorResponseComposer
  3. Send the response to the client

Catching the exception and sending the composed response is not a part of this spring-lemon-exceptions module, because it would be different for different environments. E.g., WebFlux applications would do it differently than WebMvc applications. So, Spring Lemon has this in more specific modules that we'll discuss separetely.

Customizing exceptionId

Did you notice the "exceptionId": "ConstraintViolationException" in the response example at the top of this page? The value, "ConstraintViolationException", was made by an ExceptionIdMaker, which takes a Throwable and translates it to an ID.

ExceptionMaker interface looks as below:

@FunctionalInterface
public interface ExceptionIdMaker {

	String make(Throwable t);
}

A default implementation bean is provided in LemonExceptionsAutoConfiguration as below:

@Bean
@ConditionalOnMissingBean(ExceptionIdMaker.class)
public ExceptionIdMaker exceptionIdMaker() {
    
    log.info("Configuring ExceptionIdMaker");
    return ex -> {
        
        if (ex == null)
            return null;
        
        return ex.getClass().getSimpleName();
    };
}

As you see, it'd return the name of the class. To override it, just provide your own ExceptionIdMaker bean.

Exception classes

spring-lemon-exception comes with a couple of exception classes that you can use in your code:

  1. MultiErrorException
  2. VersionException

In fact, these are well used in other modules of Spring Lemon.

MultiErrorException

MultiErrorException facilitates accumulating multiple errors and throwing them at once. It also has many utility methods for doing manual validation, as well as JSR-303 bean validation. Here is an example usage:

new MultiErrorException()
   .exceptionId("ConstraintViolationException") // optional; default is MultiErrorException
   .httpStatus(HttpStatus.UNPROCESSABLE_ENTITY) // optional; default is UNPROCESSABLE_ENTITY
   .validateField("fieldA", fieldA != 0, "msg1") // accumulates a field level error
   .validateField("fieldA", fieldA < 10000, "msg2", 10000) // accumulates another field level error
   .validateField("fieldB", fieldB.indexOf("foo") != -1, "msg3") // accumulates another field level error
   .validate(form.isOk(), "msg4") // accumulates a form level error
   .validateBean("userForm", form) // accummulates JSR-303 bean validation constraint violations
   .validationGroups(groups...) // sets validation groups for forthcoming bean validations
   .validateBean("profileForm", profileForm) // accummulates JSR-303 bean validation constraint violations, applying above groups
   .go(); // throws the exception if there are any errors accummulated      

To make it look a bit nicer, LexUtils has three validate methods:

  1. LexUtils.validateField: same as new MultiErrorException().validateField.
  2. LexUtils.validate: same as new MultiErrorException().validate.
  3. LexUtils.validateBean(beanName, bean, validationGroups): same as below
    new MultiErrorException()
        .exceptionId("ConstraintViolationException")
        .validationGroups(validationGroups)
        .validateBean(beanName, bean)

In summary, MultiErrorException is a single powerful tool to validate multiple beans and arbitrary conditions at one go. If you want to use a single and simple validation pattern, just use this.

VersionException

Throw the VersionException in cases like optimistic locking failure. Find its usage in Spring Lemon source to know more about it.

Coding Exception Handlers

Spring Lemon already comes with a rich set of exception handlers:

Spring Lemon exception handler hierarchy

Depending on the modules you are using, some or all of these would already be available to you. However, for the exception types not covered above, or for overriding any of the above handlers, you may need to code your own handlers.

To know how exactly to code your own handlers, let's take an example from Spring Lemon itself:

@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class UsernameNotFoundExceptionHandler
    extends AbstractExceptionHandler<UsernameNotFoundException> {
    
    public UsernameNotFoundExceptionHandler() {
        
        super(UsernameNotFoundException.class);
        log.info("Created");
    }
    
    @Override
    public HttpStatus getStatus(UsernameNotFoundException ex) {
        return HttpStatus.UNAUTHORIZED;
    }
}

Points to note:

  1. We have annotated it with @Order(Ordered.LOWEST_PRECEDENCE), so that you can override it with your implementation, if needed. When coding your handler, use @Order(Ordered.HIGHEST_PRECEDENCE) instead.
  2. The handler inherits AbstractExceptionHandler. You can as well inherit one of its subclass. For examples, see JsonParseExceptionHandler and ConstraintViolationExceptionHandler.
  3. Call super in the constructor, passing the exception class to be handled.
  4. Override zero or more base methods, as desired. In the above case, only the getStatus method is overridden, leaving others to default.

For more examples, have a look at the other handlers in Spring Lemon.

LexUtils

LexUtils class provides you some vital static methods. Do have a look at its methods, how they are used in Spring Lemon code, and then use those in your code!

Our Recommendations On Validation

Our general recommendations on validating request data would be:

  1. Use service layer validation. See here for a detailed discussion.
  2. When service layer validation isn't supported by the framework, e.g. in WebFlux as of this writing, use this validate utility method in LexUtils:
    LexUtils.validate("formName", formObj, ... validation groups if any);`
  3. For common custom validations, define field-level and form-level custom annotations. See @UniqueEmail and @RetypePassword in Spring Lemon for examples.
  4. For manual validations after the bean validation cycle, or for doing bean validations and manual validations together, use MultiErrorException.

The last one is the most powerful pattern. If you want to use a single pattern, use just that.