Binding error spring

declaration: package: org.springframework.validation, interface: BindingResult

All Superinterfaces:
Errors
All Known Implementing Classes:
AbstractBindingResult, AbstractPropertyBindingResult, BeanPropertyBindingResult, BindException, DirectFieldBindingResult, MapBindingResult, MethodArgumentNotValidException, WebExchangeBindException

public interface BindingResult
extends Errors

General interface that represents binding results. Extends the
Errors interface for error registration capabilities,
allowing for a Validator to be applied, and adds
binding-specific analysis and model building.

Serves as result holder for a DataBinder, obtained via
the DataBinder.getBindingResult() method. BindingResult
implementations can also be used directly, for example to invoke
a Validator on it (e.g. as part of a unit test).

Since:
2.0
Author:
Juergen Hoeller
See Also:
  • DataBinder
  • Errors
  • Validator
  • BeanPropertyBindingResult
  • DirectFieldBindingResult
  • MapBindingResult
  • Field Summary

    Fields

    Prefix for the name of the BindingResult instance in a model,
    followed by the object name.

  • Method Summary

    void

    Find a custom property editor for the given type and property.

    getModel()

    Return a model Map for the obtained state, exposing a BindingResult
    instance as ‘MODEL_KEY_PREFIX + objectName’
    and the object itself as ‘objectName’.

    Return the underlying PropertyEditorRegistry.

    Extract the raw field value for the given field.

    Return the list of fields that were suppressed during the bind process.

    getTarget()

    Return the wrapped target object, which may be a bean, an object with
    public fields, a Map — depending on the concrete binding strategy.

    default void

    Record the given value for the specified field.

    default void

    Mark the specified disallowed field as suppressed.

    Resolve the given error code into message codes.

    Resolve the given error code into message codes for the given field.

    Methods inherited from interface org.springframework.validation.Errors

    addAllErrors, getAllErrors, getErrorCount, getFieldError, getFieldError, getFieldErrorCount, getFieldErrorCount, getFieldErrors, getFieldErrors, getFieldType, getFieldValue, getGlobalError, getGlobalErrorCount, getGlobalErrors, getNestedPath, getObjectName, hasErrors, hasFieldErrors, hasFieldErrors, hasGlobalErrors, popNestedPath, pushNestedPath, reject, reject, reject, rejectValue, rejectValue, rejectValue, setNestedPath

  • Field Details

    • MODEL_KEY_PREFIX

      static final String MODEL_KEY_PREFIX

      Prefix for the name of the BindingResult instance in a model,
      followed by the object name.

  • Method Details

    • getTarget

      Return the wrapped target object, which may be a bean, an object with
      public fields, a Map — depending on the concrete binding strategy.

    • getModel

      Return a model Map for the obtained state, exposing a BindingResult
      instance as ‘MODEL_KEY_PREFIX + objectName’
      and the object itself as ‘objectName’.

      Note that the Map is constructed every time you’re calling this method.
      Adding things to the map and then re-calling this method will not work.

      The attributes in the model Map returned by this method are usually
      included in the ModelAndView
      for a form view that uses Spring’s bind tag in a JSP,
      which needs access to the BindingResult instance. Spring’s pre-built
      form controllers will do this for you when rendering a form view.
      When building the ModelAndView instance yourself, you need to include
      the attributes from the model Map returned by this method.

      See Also:
      • Errors.getObjectName()
      • MODEL_KEY_PREFIX
      • ModelAndView
      • BindTag
    • getRawFieldValue

      Extract the raw field value for the given field.
      Typically used for comparison purposes.

      Parameters:
      field — the field to check
      Returns:
      the current value of the field in its raw form, or null if not known
    • findEditor

      Find a custom property editor for the given type and property.

      Parameters:
      field — the path of the property (name or nested path), or
      null if looking for an editor for all properties of the given type
      valueType — the type of the property (can be null if a property
      is given but should be specified in any case for consistency checking)
      Returns:
      the registered editor, or null if none
    • getPropertyEditorRegistry

      Return the underlying PropertyEditorRegistry.

      Returns:
      the PropertyEditorRegistry, or null if none
      available for this BindingResult
    • resolveMessageCodes

      Resolve the given error code into message codes.

      Calls the configured MessageCodesResolver with appropriate parameters.

      Parameters:
      errorCode — the error code to resolve into message codes
      Returns:
      the resolved message codes
    • resolveMessageCodes

      Resolve the given error code into message codes for the given field.

      Calls the configured MessageCodesResolver with appropriate parameters.

      Parameters:
      errorCode — the error code to resolve into message codes
      field — the field to resolve message codes for
      Returns:
      the resolved message codes
    • addError

      See Also:
      • ObjectError
      • FieldError
      • BindingErrorProcessor
    • recordFieldValue

      Record the given value for the specified field.

      To be used when a target object cannot be constructed, making
      the original field values available through Errors.getFieldValue(java.lang.String).
      In case of a registered error, the rejected value will be exposed
      for each affected field.

      Parameters:
      field — the field to record the value for
      type — the type of the field
      value — the original value
      Since:
      5.0.4
    • recordSuppressedField

      default void recordSuppressedField(String field)

      Mark the specified disallowed field as suppressed.

      The data binder invokes this for each field value that was
      detected to target a disallowed field.

      See Also:
      • DataBinder.setAllowedFields(java.lang.String...)
    • getSuppressedFields

      default String[] getSuppressedFields()

      Return the list of fields that were suppressed during the bind process.

      Can be used to determine whether any field values were targeting
      disallowed fields.

      See Also:
      • DataBinder.setAllowedFields(java.lang.String...)

One of the main blocker for solving this problem is the default eagerly-failing nature of the jackson data binder; one would have to somehow convince it to continue parsing instead of just stumble at first error. One would also have to collect these parsing errors in order to ultimately convert them to BindingResult entries. Basically one would have to catch, suppress and collect parsing exceptions, convert them to BindingResult entries then add these entries to the right @Controller method BindingResult argument.

The catch & suppress part could be done by:

  • custom jackson deserializers which would simply delegate to the default related ones but would also catch, suppress and collect their parsing exceptions
  • using AOP (aspectj version) one could simply intercept the default deserializers parsing exceptions, suppress and collect them
  • using other means, e.g. appropriate BeanDeserializerModifier, one could also catch, suppress and collect the parsing exceptions; this might be the easiest approach but requires some knowledge about this jackson specific customization support

The collecting part could use a ThreadLocal variable to store all necessary exceptions related details. The conversion to BindingResult entries and the addition to the right BindingResult argument could be pretty easily accomplished by an AOP interceptor on @Controller methods (any type of AOP, Spring variant including).

What’s the gain

By this approach one gets the data binding errors (in addition to the validation ones) into the BindingResult argument the same way as would expect for getting them when using an e.g. @ModelAttribute. It will also work with multiple levels of embedded objects — the solution presented in the question won’t play nice with that.

Solution Details (custom jackson deserializers approach)

I created a small project proving the solution (run the test class) while here I’ll just highlight the main parts:

/**
* The logic for copying the gathered binding errors 
* into the @Controller method BindingResult argument.
* 
* This is the most "complicated" part of the project.
*/
@Aspect
@Component
public class BindingErrorsHandler {
    @Before("@within(org.springframework.web.bind.annotation.RestController)")
    public void logBefore(JoinPoint joinPoint) {
        // copy the binding errors gathered by the custom
        // jackson deserializers or by other means
        Arrays.stream(joinPoint.getArgs())
                .filter(o -> o instanceof BindingResult)
                .map(o -> (BindingResult) o)
                .forEach(errors -> {
                    JsonParsingFeedBack.ERRORS.get().forEach((k, v) -> {
                        errors.addError(new FieldError(errors.getObjectName(), k, v, true, null, null, null));
                    });
                });
        // errors copied, clean the ThreadLocal
        JsonParsingFeedBack.ERRORS.remove();
    }
}

/**
 * The deserialization logic is in fact the one provided by jackson,
 * I only added the logic for gathering the binding errors.
 */
public class CustomIntegerDeserializer extends StdDeserializer<Integer> {
    /**
    * Jackson based deserialization logic. 
    */
    @Override
    public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        try {
            return wrapperInstance.deserialize(p, ctxt);
        } catch (InvalidFormatException ex) {
            gatherBindingErrors(p, ctxt);
        }
        return null;
    }

    // ... gatherBindingErrors(p, ctxt), mandatory constructors ...
}

/**
* A simple classic @Controller used for testing the solution.
*/
@RestController
@RequestMapping("/errormixtest")
@Slf4j
public class MixBindingAndValidationErrorsController {
    @PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Level1 post(@Valid @RequestBody Level1 level1, BindingResult errors) {
    // at the end I show some BindingResult logging for a @RequestBody e.g.:
    // {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}}
    // ... your whatever logic here ...

With these you’ll get in BindingResult something like this:

Field error in object 'level1' on field 'nr12': rejected value [1]; codes [Min.level1.nr12,Min.nr12,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.nr12,nr12]; arguments []; default message [nr12],5]; default message [must be greater than or equal to 5]
Field error in object 'level1' on field 'nr11': rejected value [x]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.level3.nr31': rejected value [xxx]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.nr22': rejected value [1]; codes [Min.level1.level2.nr22,Min.level2.nr22,Min.nr22,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.level2.nr22,level2.nr22]; arguments []; default message [level2.nr22],5]; default message [must be greater than or equal to 5]

where the 1th line is determined by a validation error (setting 1 as the value for a @Min(5) private Integer nr12;) while the 2nd is determined by a binding one (setting "x" as value for a @JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;). 3rd line tests binding errors with embedded objects: level1 contains a level2 which contains a level3 object property.

Note how other approaches could simply replace the usage of custom jackson deserializers while keeping the rest of the solution (AOP, JsonParsingFeedBack).

Errors Spring Boot Starter

Build Status
codecov
Maven Central
Javadocs
Sonatype
Sonar Quality Gate
License

A Bootiful, Consistent and Opinionated Approach to Handle all sorts of Exceptions.

Table of Contents

  • Make Error Handling Great Again!
  • Getting Started
    • Download
    • Prerequisites
    • Overview
    • Error Codes
    • Error Message
    • Exposing Arguments
      • Exposing Named Arguments
      • Named Arguments Interpolation
    • Validation and Binding Errors
    • Custom Exceptions
    • Spring MVC
    • Spring Security
      • Reactive
      • Servlet
    • Error Representation
      • Fingerprinting
      • Customizing the Error Representation
    • Default Error Handler
    • Refining Exceptions
    • Logging Exceptions
    • Post Processing Handled Exceptions
    • Registering Custom Handlers
    • Test Support
  • Appendix
    • Configuration
  • License

Make Error Handling Great Again!

Built on top of Spring Boot’s great exception handling mechanism, the errors-spring-boot-starter offers:

  • A consistent approach to handle all exceptions. Doesn’t matter if it’s a validation/binding error or a
    custom domain-specific error or even a Spring related error, All of them would be handled by a WebErrorHandler
    implementation (No more ErrorController vs @ExceptionHandler vs WebExceptionHandler)
  • Built-in support for application specific error codes, again, for all possible errors.
  • Simple error message interpolation using plain old MessageSources.
  • Customizable HTTP error representation.
  • Exposing arguments from exceptions to error messages.
  • Supporting both traditional and reactive stacks.
  • Customizable exception logging.
  • Supporting error fingerprinting.

Getting Started

Download

Download the latest JAR or grab via Maven:

<dependency>
    <groupId>me.alidg</groupId>
    <artifactId>errors-spring-boot-starter</artifactId>
    <version>1.4.0</version>
</dependency>

or Gradle:

compile "me.alidg:errors-spring-boot-starter:1.4.0"

If you like to stay at the cutting edge, use our 1.5.0-SNAPSHOT version. Of course you should define the following
snapshot repository:

<repositories>
    <repository>
        <id>Sonatype</id>
        <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
    </repository>
</repositories>

or:

repositories {
    maven {
      url 'https://oss.sonatype.org/content/repositories/snapshots/'
    }
}

Prerequisites

The main dependency is JDK 8+. Tested with:

  • JDK 8, JDK 9, JDK 10 and JDK 11 on Linux.
  • Spring Boot 2.2.0.RELEASE (Also, should work with any 2.0.0+)

Overview

The WebErrorHandler implementations are responsible for handling different kinds of exceptions. When an exception
happens, the WebErrorHandlers (A factory over all WebErrorHandler implementations) catches the exception and would
find an appropriate implementation to handle the exception. By default, WebErrorHandlers consults with the
following implementations to handle a particular exception:

  • An implementation to handle all validation/binding exceptions.
  • An implementation to handle custom exceptions annotated with the @ExceptionMapping.
  • An implementation to handle Spring MVC specific exceptions.
  • And if the Spring Security is on the classpath, An implementation to handle Spring Security specific exceptions.

After delegating to the appropriate handler, the WebErrorHandlers turns the handled exception result into a HttpError,
which encapsulates the HTTP status code and all error code/message combinations.

Error Codes

Although using appropriate HTTP status codes is a recommended approach in RESTful APIs, sometimes, we need more information
to find out what exactly went wrong. This is where Error Codes comes in. You can think of an error code as a Machine Readable
description of the error. Each exception can be mapped to at least one error code.

In errors-spring-boot-starter, one can map exceptions to error codes in different ways:

  • Validation error codes can be extracted from the Bean Validation‘s constraints:

    public class User {  
    
        @NotBlank(message = "username.required")
        private final String username;
     
        @NotBlank(message = "password.required")
        @Size(min = 6, message = "password.min_length")
        private final String password;
     
        // constructor and getter and setters
    }

    To report a violation in password length, the password.min_length would be reported as the error code. As you may guess,
    one validation exception can contain multiple error codes to report all validation violations at once.

  • Specifying the error code for custom exceptions using the @ExceptionMapping annotation:

    @ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
    public class UserAlreadyExistsException extends RuntimeException {}

    The UserAlreadyExistsException exception would be mapped to user.already_exists error code.

  • Specifying the error code in a WebErrorHandler implementation:

    public class ExistedUserHandler implements WebErrorHandler {
    
        @Override
        public boolean canHandle(Throwable exception) {
            return exception instanceof UserAlreadyExistsException;
        }
     
        @Override
        public HandledException handle(Throwable exception) {   
            return new HandledException("user.already_exists", BAD_REQUEST, null);
        }
    }

Error Message

Once the exception mapped to error code(s), we can add a companion and Human Readable error message. This can be done
by registering a Spring MessageSource to perform the code-to-message translation. For example, if we add the following
key-value pair in our message resource file:

user.already_exists=Another user with the same username already exists

Then if an exception of type UserAlreadyExistsException was thrown, you would see a 400 Bad Request HTTP response
with a body like:

{
  "errors": [
    {
      "code": "user.already_exists",
      "message": "Another user with the same username already exists"
    }
  ]
}

Since MessageSource supports Internationalization (i18n), our error messages can possibly have different values based
on each Locale.

Exposing Arguments

With Bean Validation you can pass parameters from the constraint validation, e.g. @Size, to its corresponding
interpolated message. For example, if we have:

password.min_length=The password must be at least {0} characters

And a configuration like:

@Size(min = 6, message = "password.min_length")
private final String password;

The min attribute from the @Size constraint would be passed to the message interpolation mechanism, so:

{
  "errors": [
    {
      "code": "password.min_length",
      "message": "The password must be at least 6 characters"
    }
  ]
}

In addition to support this feature for validation errors, we extend it for custom exceptions using the @ExposeAsArg
annotation. For example, if we’re going to specify the already taken username in the message:

user.already_exists=Another user with the '{0}' username already exists

We could write:

@ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
public class UserAlreadyExistsException extends RuntimeException {
    @ExposeAsArg(0) private final String username;
    
    // constructor
}

Then the username property from the UserAlreadyExistsException would be available to the message under the
user.already_exists key as the first argument. @ExposeAsArg can be used on fields and no-arg methods with a
return type. The HandledException class also accepts the to-be-exposed arguments in its constructor.

Exposing Named Arguments

By default error arguments will be used in message interpolation only. It is also possible to additionally get those
arguments in error response by defining the configuration property errors.expose-arguments.
When enabled, you might get the following response payload:

{
  "errors": [
    {
      "code": "password.min_length",
      "message": "The password must be at least 6 characters",
      "arguments": {
        "min": 6
      }
    }
  ]
}

The errors.expose-arguments property takes 3 possible values:

  • NEVER — named arguments will never be exposed. This is the default setting.
  • NON_EMPTY — named arguments will be exposed only in case there are any. If error has no arguments,
    result payload will not have "arguments" element.
  • ALWAYS — the "arguments" element is always present in payload, even when the error has no arguments.
    In that case empty map will be provided: "arguments": {}.

Checkout here for more detail on how we expose arguments for different exception categories.

Named Arguments Interpolation

You can use either positional or named argument placeholders in message templates. Given:

@Size(min = 6, max = 20, message = "password.length")
private final String password;

You can create message template in messages.properties with positional arguments:

password.length=Password must have length between {1} and {0}

Arguments are sorted by name. Since lexicographically max < min, placeholder {0} will be substituted
with argument max, and {1} will have value of argument min.

You can also use argument names as placeholders:

password.length=Password must have length between {min} and {max}

Named arguments interpolation works out of the box, regardless of the errors.expose-arguments value.
You can mix both approaches, but it is not recommended.

If there is a value in the message that should not be interpolated, escape the first { character with a backslash:

password.length=Password \{min} is {min} and \{max} is {max}

After interpolation, this message would read: Password {min} is 6 and {max} is 20.

Arguments annotated with @ExposeAsArg will be named by annotated field or method name:

@ExposeAsArg(0)
private final String argName; // will be exposed as "argName"

This can be changed by the name parameter:

@ExposeAsArg(value = 0, name = "customName")
private final String argName; // will be exposed as "customName"

Validation and Binding Errors

Validation errors can be processed as you might expect. For example, if a client passed an empty JSON to a controller method
like:

@PostMapping
public void createUser(@RequestBody @Valid User user) {
    // omitted
}

Then the following error would be returned:

{
  "errors": [
    {
      "code": "password.min_length",
      "message": "corresponding message!"
    },
    {
       "code": "password.required",
       "message": "corresponding message!"
    },
    {
      "code": "username.required",
      "message": "corresponding message!"
    }
  ]
}

Bean Validation’s ConstraintViolationExceptions will be handled in the same way, too.

Custom Exceptions

Custom exceptions can be mapped to status code and error code combination using the @ExceptionMapping annotation:

@ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
public class UserAlreadyExistsException extends RuntimeException {}

Here, every time we catch an instance of UserAlreadyExistsException, a Bad Request HTTP response with user.already_exists
error would be returned.

Also, it’s possible to expose some arguments from custom exceptions to error messages using the ExposeAsArg:

@ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
public class UserAlreadyExistsException extends RuntimeException {
    @ExposeAsArg(0) private final String username;
    
    // constructor
    
    @ExposeAsArg(1)
    public String exposeThisToo() {
        return "42";
    }
}

Then the error message template can be something like:

user.already_exists=Another user exists with the '{0}' username: {1}

During message interpolation, the {0} and {1} placeholders would be replaced with annotated field’s value and
method’s return value. The ExposeAsArg annotation is applicable to:

  • Fields
  • No-arg methods with a return type

Spring MVC

By default, a custom WebErrorHandler is registered to handle common exceptions thrown by Spring MVC:

Exception Status Code Error Code Exposed Args
HttpMessageNotReadableException 400 web.invalid_or_missing_body
HttpMediaTypeNotAcceptableException 406 web.not_acceptable List of acceptable MIME types
HttpMediaTypeNotSupportedException 415 web.unsupported_media_type The unsupported content type
HttpRequestMethodNotSupportedException 405 web.method_not_allowed The invalid HTTP method
MissingServletRequestParameterException 400 web.missing_parameter Name and type of the missing query Param
MissingServletRequestPartException 400 web.missing_part Missing request part name
NoHandlerFoundException 404 web.no_handler The request path
MissingRequestHeaderException 400 web.missing_header The missing header name
MissingRequestCookieException 400 web.missing_cookie The missing cookie name
MissingMatrixVariableException 400 web.missing_matrix_variable The missing matrix variable name
others 500 unknown_error

Also, almost all exceptions from the ResponseStatusException hierarchy, added in Spring Framework 5+ , are handled compatible
with the Spring MVC traditional exceptions.

Spring Security

When Spring Security is present on the classpath, a WebErrorHandler implementation would be responsible to handle
common Spring Security exceptions:

Exception Status Code Error Code
AccessDeniedException 403 security.access_denied
AccountExpiredException 400 security.account_expired
AuthenticationCredentialsNotFoundException 401 security.auth_required
AuthenticationServiceException 500 security.internal_error
BadCredentialsException 400 security.bad_credentials
UsernameNotFoundException 400 security.bad_credentials
InsufficientAuthenticationException 401 security.auth_required
LockedException 400 security.user_locked
DisabledException 400 security.user_disabled
others 500 unknown_error

Reactive Security

When the Spring Security is detected along with the Reactive stack, the starter registers two extra handlers to handle
all security related exceptions. In contrast with other handlers which register themselves automatically, in order to use these
two handlers, you should register them in your security configuration manually as follows:

@EnableWebFluxSecurity
public class WebFluxSecurityConfig {

    // other configurations

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                            ServerAccessDeniedHandler accessDeniedHandler,
                                                            ServerAuthenticationEntryPoint authenticationEntryPoint) {
        http
                .csrf().accessDeniedHandler(accessDeniedHandler)
                .and()
                .exceptionHandling()
                    .accessDeniedHandler(accessDeniedHandler)
                    .authenticationEntryPoint(authenticationEntryPoint)
                // other configurations

        return http.build();
    }
}

The registered ServerAccessDeniedHandler and ServerAuthenticationEntryPoint are responsible for handling AccessDeniedException
and AuthenticationException exceptions, respectively.

Servlet Security

When the Spring Security is detected along with the traditional servlet stack, the starter registers two extra handlers to handle
all security related exceptions. In contrast with other handlers which register themselves automatically, in order to use these
two handlers, you should register them in your security configuration manually as follows:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final AccessDeniedHandler accessDeniedHandler;
    private final AuthenticationEntryPoint authenticationEntryPoint;

    public SecurityConfig(AccessDeniedHandler accessDeniedHandler, AuthenticationEntryPoint authenticationEntryPoint) {
        this.accessDeniedHandler = accessDeniedHandler;
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling()
                    .accessDeniedHandler(accessDeniedHandler)
                    .authenticationEntryPoint(authenticationEntryPoint);
    }
}

The registered AccessDeniedHandler and AuthenticationEntryPoint are responsible for handling AccessDeniedException
and AuthenticationException exceptions, respectively.

Error Representation

By default, errors would manifest themselves in the HTTP response bodies with the following JSON schema:

{
  "errors": [
    {
      "code": "the_error_code",
      "message": "the_error_message"
    }
  ]
}

Fingerprinting

There is also an option to generate error fingerprint. Fingerprint is a unique hash of error
event which might be used as a correlation ID of error presented to user, and reported in
application backend (e.g. in detailed log message). To generate error fingerprints, add
the configuration property errors.add-fingerprint=true.

We provide two fingerprint providers implementations:

  • UuidFingerprintProvider which generates a random UUID regardless of the handled exception.
    This is the default provider and will be used out of the box if
    errors.add-fingerprint=true property is configured.
  • Md5FingerprintProvider which generates MD5 checksum of full class name of original exception
    and current time.

Customizing the Error Representation

In order to change the default error representation, just implement the HttpErrorAttributesAdapter
interface and register it as Spring Bean:

@Component
public class OopsDrivenHttpErrorAttributesAdapter implements HttpErrorAttributesAdapter {
    
    @Override
    public Map<String, Object> adapt(HttpError httpError) {
        return Collections.singletonMap("Oops!", httpError.getErrors());
    }
}

Default Error Handler

By default, when all registered WebErrorHandlers refuse to handle a particular exception, the LastResortWebErrorHandler
would catch the exception and return a 500 Internal Server Error with unknown_error as the error code.

If you don’t like this behavior, you can change it by registering a Bean of type WebErrorHandler with
the defaultWebErrorHandler as the Bean Name:

@Component("defaultWebErrorHandler")
public class CustomDefaultWebErrorHandler implements WebErrorHandler {
    // Omitted
}

Refining Exceptions

Sometimes the given exception is not the actual problem and we need to dig deeper to handle the error, say the actual
exception is hidden as a cause inside the top-level exception. In order to transform some exceptions before handling
them, we can register an ExceptionRefiner implementation as a Spring Bean:

@Component
public class CustomExceptionRefiner implements ExceptionRefiner {
    
    @Override
    Throwable refine(Throwable exception) {
        return exception instanceof ConversionFailedException ? exception.getCause() : exception;
    }
}

Logging Exceptions

By default, the starter issues a few debug logs under the me.alidg.errors.WebErrorHandlers logger name.
In order to customize the way we log exceptions, we just need to implement the ExceptionLogger interface and register it
as a Spring Bean:

@Component
public class StdErrExceptionLogger implements ExceptionLogger {
    
    @Override
    public void log(Throwable exception) {
        if (exception != null)
            System.err.println("Failed to process the request: " + exception);
    }
}

Post Processing Handled Exceptions

As a more powerful alternative to ExceptionLogger mechanism, there is also WebErrorHandlerPostProcessor
interface. You may declare multiple post processors which implement this interface and are exposed
as Spring Bean. Below is an example of more advanced logging post processors:

@Component
public class LoggingErrorWebErrorHandlerPostProcessor implements WebErrorHandlerPostProcessor {
    private static final Logger log = LoggerFactory.getLogger(LoggingErrorWebErrorHandlerPostProcessor.class);
    
    @Override 
    public void process(@NonNull HttpError error) {
        if (error.getHttpStatus().is4xxClientError()) {
            log.warn("[{}] {}", error.getFingerprint(), prepareMessage(error));
        } else if (error.getHttpStatus().is5xxServerError()) {
            log.error("[{}] {}", error.getFingerprint(), prepareMessage(error), error.getOriginalException());
        }
    }
    
    private String prepareMessage(HttpError error) {
        return error.getErrors().stream()
                    .map(HttpError.CodedMessage::getMessage)
                    .collect(Collectors.joining("; "));
    }
}

Registering Custom Handlers

In order to provide a custom handler for a specific exception, just implement the WebErrorHandler interface for that
exception and register it as a Spring Bean:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomWebErrorHandler implements WebErrorHandler {
    
    @Override
    public boolean canHandle(Throwable exception) {
        return exception instanceof ConversionFailedException;
    }

    @Override
    public HandledException handle(Throwable exception) {
        return new HandledException("custom_error_code", HttpStatus.BAD_REQUEST, null);
    }
}

If you’re going to register multiple handlers, you can change their priority using @Order. Please note that all your custom
handlers would be registered after built-in exception handlers (Validation, ExceptionMapping, etc.). If you don’t like
this idea, provide a custom Bean of type WebErrorHandlers and the default one would be discarded.

Test Support

In order to enable our test support for WebMvcTests, just add the @AutoConfigureErrors annotation to your test
class. That’s how a WebMvcTest would look like with errors support enabled:

@AutoConfigureErrors
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerIT {
    
    @Autowired private MockMvc mvc;
    
    @Test
    public void createUser_ShouldReturnBadRequestForInvalidBodies() throws Exception {
        mvc.perform(post("/users").content("{}"))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.errors[0].code").value("username.required"));    
    }
}

For WebFluxTests, the test support is almost the same as the Servlet stack:

@AutoConfigureErrors
@RunWith(SpringRunner.class)
@WebFluxTest(UserController.class)
@ImportAutoConfiguration(ErrorWebFluxAutoConfiguration.class) // Drop this if you're using Spring Boot 2.1.4+
public class UserControllerIT {

    @Autowired private WebTestClient client;

    @Test
    public void createUser_ShouldReturnBadRequestForInvalidBodies() {
        client.post()
                .uri("/users")
                .syncBody("{}").header(CONTENT_TYPE, APPLICATION_JSON_UTF8_VALUE)
                .exchange()
                .expectStatus().isBadRequest()
                .expectBody().jsonPath("$.errors[0].code").isEqualTo("username.required");
    }
}

Appendix

Configuration

Additional configuration of this starter can be provided by configuration properties — the Spring Boot way.
All configuration properties start with errors. Below is a list of supported properties:

Property Values Default value
errors.expose-arguments NEVER, NON_EMPTY, ALWAYS NEVER
errors.add-fingerprint true, false false

Check ErrorsProperties implementation for more details.

License

Copyright 2018 alimate

Licensed under the Apache License, Version 2.0 (the «License»);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an «AS IS» BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Create global or application-level Exception handlers and return Custom Error Messages in Spring REST APIs.

Overview

Effective communication is the key to healthy and efficient relationships. Interestingly, the same applies to any Client and Server relationships. The client’s request may succeed or fail on the server. However, the server should provide the most appropriate status code in either of the outcomes.

Although sending a correct status code is enough for a client to take real action based on the outcome of a request, in case of failures, the client may need more details about what went wrong. For example, failure details like the exception type and an error message can help clients log the error or provide appropriate failure messages to their clients.

This article will teach How to handle different failures and return Custom Error Messages from a Spring REST API. If you don’t know how to handle exceptions in Spring REST API, please read Spring Rest Service Exception Handling.

Return a Generic Error Message using @ResponseStatus

The most basic way of returning an error message from a REST API is to use the @ResponseStatus annotation. We can add the error message in the annotation’s reason field. Although we can only return a generic error message, we can specify exception-specific error messages.

Next is an example of a @ControllerAdvice using @ResponseStatus annotations to return exception-specific error messages.

@ControllerAdvice public class ApplicationExceptionHandler { @ResponseStatus( value = HttpStatus.NOT_FOUND, reason = "Requested Student Not Found") @ExceptionHandler(StudentNotFoundException.class) public void handleException(StudentNotFoundException e) { } @ResponseStatus( value = HttpStatus.BAD_REQUEST, reason = "Received Invalid Input Parameters") @ExceptionHandler(InputValidationException.class) public void handleException(InputValidationException e) { } @ResponseStatus( value = HttpStatus.GATEWAY_TIMEOUT, reason = "Upstream Service Not Responding, Try Again") @ExceptionHandler(ServiceUnavailableException.class) public void handleException(ServiceUnavailableException e) { } }

Code language: Java (java)

The Exception handler class has three exception handlers, each of which returns a specific HTTP Response Status. Each response status specifies a reason field with a particular error message.

To view the error message in the response, ensure you have turned on include-messages in the server configuration. To learn more about Spring Boot server configurations, please visit Spring Boot Embedded Tomcat Configuration.

server: error: include-message: always

Code language: YAML (yaml)

Next is an example of a response object the REST API returns. Note that the response object has the specified error message.

{ "timestamp": "", "status": 404, "error": "Not Found", "message": "Requested Student Not Found", "path": "/students/Jack" }

Code language: JSON / JSON with Comments (json)

Although we can specify exception-specific error messages, it is still not informative. Therefore in the following sections, we will learn how to return a more specific error message from Spring REST API.

Return Error Message Using Custom Error Object

Let’s create a class representing the error message and the status code. We will return an instance of that in case of errors.

Next is the Error class representing the status code and a String message. We use a few Lombok annotations that introduce regular getter and setter methods and a constructor using the final fields.

Custom Response Error Class

@Data @RequiredArgsConstructor public class Error { private final HttpStatus httpStatus; private final String message; }

Code language: Java (java)

Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.

@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity handleException( StudentNotFoundException e) { Error error = new Error(HttpStatus.NOT_FOUND, e.getLocalizedMessage()); return new ResponseEntity<>(error, error.getHttpStatus()); }

Code language: Java (java)

The exception handler returns an instance of the Error class populated with the exception message and HTTP Status Code.

Now, we can throw our Not Found Exception with a custom error message.

throw new StudentNotFoundException ("Student service failed, studentId : " + studentId);

Code language: Java (java)

When the REST API cannot find the requested resource, we get a detailed error as a response.

{ "httpStatus": "NOT_FOUND", "message": "Student service failed, studentId : Jack" }

Code language: JSON / JSON with Comments (json)

Return Error Message Using HashMap

Also, instead of creating a dedicated error class, we can return a detailed error message using a simple HashMap. Next is an example of producing and returning a Custom Error Message using Java HashMap.

@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity<Map<String, String>> handleException(StudentNotFoundException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.NOT_FOUND.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); }

Code language: Java (java)

Handle Bad Request Exceptions

The Bad Request errors are the Client errors where the client’s request doesn’t meet the requirements of the target server. This section will see how to handle Bad Request exceptions and provide a custom or detailed error response.

Type Mismatch Exceptions

The Type Mismatch Exceptions occur when Spring Controller cannot map the request parameters, path variables, or header values into controller method arguments. This section covers the handling of MethodArgumentTypeMismatchException and TypeMismatchException.

Spring throws MethodArgumentTypeMismatchException when the controller argument doesn’t have a required type. On the other hand, Spring throws TypeMismatchException when there is a type mismatch while setting Bean properties. Also, both exceptions provide a detailed error message that we can use to prepare the Error object.

To demonstrate that, next is an example of Handling MethodArgumentTypeMismatchException and TypeMismatchException and returning a detailed error message in Controller Advice.

@ExceptionHandler({ MethodArgumentTypeMismatchException.class, TypeMismatchException.class }) public ResponseEntity<Map<String, String>> handleException(TypeMismatchException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }

Code language: Java (java)

Note that the controller advice catches both exceptions; however, the method arguments accept an exception of type TypeMismatchException because it is the parent of the other exception.

Next, the snippet shows a detailed error message when we call a rest endpoint with an incompatible path variable leading to MethodArgumentTypeMismatchException.

{ "httpStatus": "BAD_REQUEST", "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; nested exception is java.lang.NumberFormatException: For input String: "Jack"" }

Code language: JSON / JSON with Comments (json)

Bean Validation Exceptions

The Bean Validation exceptions occur when the request contents do not pass the provided validations.

The BindException occurs when the binding errors are fatal. While the MethodArgumentNotValidException occurs when validations specified by @Valid fail. Note that the MethodArgumentNotValidException is a subclass of BindException. Thus, we can handle them using the same Spring REST API’s exception handler.

@ExceptionHandler({ BindException.class, MethodArgumentNotValidException.class }) public ResponseEntity<Map<String, Object>> handleException(BindException e) { List<String> errors = new ArrayList<>(); e.getFieldErrors() .forEach(err -> errors.add(err.getField() + ": " + err.getDefaultMessage())); e.getGlobalErrors() .forEach(err -> errors.add(err.getObjectName() + ": " + err.getDefaultMessage())); Map<String, Object> errorResponse = Map.of( "error", errors, "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }

Code language: Java (java)

Here we have created a List<String> to represent individual binding errors and add that to the response Map. Instead, we can add a List<String> field to the Error class we created in the previous section and populate the list with individual errors.

Spring throws HttpMediaTypeNotSupportedException, when a POST, PUT, or PATCH endpoint on the server cannot handle the content type sent by the client. The REST Controllers on the server specify the content type they can support. When the media type that a client sends doesn’t match, the client gets this exception back.

To demonstrate, next is an example of handling HttpMediaTypeNotSupportedException and returning a custom error response.

@ExceptionHandler(HttpMediaTypeNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpMediaTypeNotSupportedException e) { String provided = e.getContentType().toString(); List<String> supported = e.getSupportedMediaTypes().stream() .map(MimeType::toString) .collect(Collectors.toList()); String error = provided + " is not one of the supported media types (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.UNSUPPORTED_MEDIA_TYPE.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.UNSUPPORTED_MEDIA_TYPE); }

Code language: Java (java)

As seen in the exception handler above, the instance of HttpMediaTypeNotSupportedException provides detailed information about the incorrect media type we provided and a list of actually supported media types. Thus, we create a custom error message based on the available information.

{ "error":"text/plain;charset=UTF-8 is not one of the supported media types ( application/octet-stream, text/plain, application/xml, text/xml, application/x-www-form-urlencoded, application/*+xml, multipart/form-data, multipart/mixed, application/json, application/*+json, */*)", "message":"Content type 'text/plain;charset=UTF-8' not supported", "status":"415 UNSUPPORTED_MEDIA_TYPE" }

Code language: JSON / JSON with Comments (json)

The above snippet shows a client’s sample error response when it sends a request with an invalid media type.

Handle Request Body Not Readable Exception

Now we will see an example of handling HttpMessageNotReadableException and returning a custom error response. The HttpMessageNotReadableException occurs when the request body is missing or unreadable.

@ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<Map<String, String>> handleException( HttpMessageNotReadableException e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }

Code language: Java (java)

Handle HTTP Request Method Not Supported Exception

The HttpMethodNotSupportedException occurs when the HTTP endpoint on the REST API does not support the HTTP request method. Let’s write an exception handler for HttpMethodNotSupportedException and return a detailed error message.

@ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpRequestMethodNotSupportedException e) throws IOException { String provided = e.getMethod(); List<String> supported = List.of(e.getSupportedMethods()); String error = provided + " is not one of the supported Http Methods (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.METHOD_NOT_ALLOWED.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.METHOD_NOT_ALLOWED); }

Code language: Java (java)

As seen in the exception handler above, the exception instance provides detailed information about the provided HTTP Method and an array of Supported HTTP Methods. We use it to form a clear error message.

{ "error": "GET is not one of the supported Http Methods (POST)", "message": "Request method 'GET' not supported", "status": "405 METHOD_NOT_ALLOWED" }

Code language: JSON / JSON with Comments (json)

The snippet showed an example response when the client attempted to execute a GET endpoint, while the REST API supports only POST.

Default Exception Handler

Similarly, we can create a default exception handler advice that handles all Exception types. Spring attempts to find the most specific handler when we have multiple exception handlers and falls back to the default handler if there is no suitable handler.

@ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleException( Exception e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.INTERNAL_SERVER_ERROR.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); }

Code language: Java (java)

Above is an example of writing a default exception handler that returns an error message by the exception instance and an HTTP Status of 500.

Summary

This detailed tutorial taught us how to Return Custom Error Messages in Spring REST API. Firstly, we understood that Spring returns a generic error message and the most suitable HTTP Status Code by default. However, we can write our exception handlers for specific exceptions using @ControllerAdvice and produce a custom and detailed error response.

For more on Spring and Spring Boot Tutorials, please visit Spring Tutorials.


This
is continuation to my previous posts. In my previous post, I explained how
spring binds the request parameters to a java model object using
@ModelAttribute annotation.

But
in some cases, user may enter wrong inputs. For example, you developed a
registration form, where you are expecting below information.

Information

Type

userName

String

age

int

hobbies

Collection

If
user enter age as ‘Twenty Five’, where you are expecting an integer. Spring
throws an exception.

As
you see above registration form, I given ‘Twenty Five’ as the value for the
field ‘Age’. When I click on submit button, I got below exception.

As
you see above error page, I do not get any meaningful error message. To solve
this problem, spring provides ‘BindingResult’ interface.

BindingResult
interface

General
interface that represents binding results. If any error occurs, at the time of
binding the request parameters to java model object, we can use this interface
to retrieve the number of errors, error messages.

@RequestMapping(«/registerMe»)

public
ModelAndView getHelloMessage(@ModelAttribute(«studentInfo») Student
student, BindingResult bindingResult) {

         if (bindingResult.hasErrors()) {

                  ModelAndView modelAndView =
new ModelAndView(«registration»);

                  return modelAndView;

         }

         …..

         …..

         …..

}

As
you see above snippet, I am using bindingResult.hasErrors() method to check
whether there is any errors.

you
can use the  <form:errors /> tag to
render those field error messages in an HTML page.

Example

<form:errors
path=»studentInfo.*» />

Above
tag displays the error message, if the binding of the request parameters to the
studentInfo object is not successful.

Find
the below working application.

HelloWorldController.java

package com.sample.myApp.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.sample.myApp.model.Student;

@Controller
public class HelloWorldController {
 @RequestMapping("/registration")
 public String getRegistrationpage() {
  return "registration";
 }

 @RequestMapping("/registerMe")
 public ModelAndView getHelloMessage(@ModelAttribute("studentInfo") Student student, BindingResult bindingResult) {
  if (bindingResult.hasErrors()) {
   ModelAndView modelAndView = new ModelAndView("registration");
   return modelAndView;
  }

  ModelAndView modelAndView = new ModelAndView("welcome");

  modelAndView.addObject("message", "Dear User, your details are registered");

  return modelAndView;
 }

}

Student.java

package com.sample.myApp.model;

import java.util.List;

public class Student {
 private String userName;
 private int age;
 private List<String> hobbies;

 public String getUserName() {
  return userName;
 }

 public void setUserName(String userName) {
  this.userName = userName;
 }

 public int getAge() {
  return age;
 }

 public void setAge(int age) {
  this.age = age;
 }

 public List<String> getHobbies() {
  return hobbies;
 }

 public void setHobbies(List<String> hobbies) {
  this.hobbies = hobbies;
 }

}

Create
registration.jsp, welcome.jsp files under WEB-INF/jsp folder.

registration.jsp

<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>
<head>
<title>User Information Page</title>
</head>

<body>
 <h2 style="color:red;"><form:errors path="studentInfo.*" /></h2>

 <form method="post" action="/springdemo/registerMe" id="f1">
  <table>
   <tr>
    <td>User Name :</td>
    <td><input type="text" name="userName" value="" /></td>
   </tr>
   <tr>
    <td>Age :</td>
    <td><input type="text" name="age" value="" /></td>
   </tr>
   <tr>
    <td>Hobbies :</td>
    <td><select multiple name="hobbies">
      <option value="cricket">Cricket</option>
      <option value="chess">Chess</option>
      <option value="football">Football</option>
      <option value="tennis">Tennis</option>
    </select></td>
   </tr>
   <tr>
    <td><input type="submit" name="submit" value="submit"
     style="font-size: 18px;" /></td>
   </tr>
  </table>
 </form>

</body>
</html>

welcome.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
 pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Hello World Spring Web MVC</title>
</head>
<body>
 <h1>${message}</h1>

 <h3>
  User Name : ${studentInfo.userName} <br /> 
  Age : ${studentInfo.age} <br />
  Hobbies: ${studentInfo.hobbies}
 </h3>
</body>
</html>

Create
web.xml, HelloWorld-servlet.xml files under WEB-INF folder.

web.xml

<web-app id="WebApp_ID" version="2.4"
 xmlns="http://java.sun.com/xml/ns/j2ee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
   http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

 <display-name>Spring MVC Hello WorldApplication</display-name>

 <servlet>
  <servlet-name>HelloWorld</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>

 <servlet-mapping>
  <servlet-name>HelloWorld</servlet-name>
  <url-pattern>/</url-pattern>
 </servlet-mapping>

</web-app>

HelloWorld-servlet.xml

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="
   http://www.springframework.org/schema/beans     
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/context 
   http://www.springframework.org/schema/context/spring-context-3.0.xsd">

 <context:component-scan
  base-package="com.sample.myApp" />

 <bean
  class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="prefix" value="/WEB-INF/jsp/" />
  <property name="suffix" value=".jsp" />
 </bean>

</beans>

Create
index.jsp file under webapp folder.

index.jsp

<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>
<head>
<title>Welcome Page</title>
</head>

<body>
 <h1>Hello World</h1>
</body>
</html>

Project
structure looks like below.

Run
the application on server.

I
entered ‘Twenty Five’ in the Age field, When I click on submit button, it shows
the error information.


Java


Рекомендация: подборка платных и бесплатных курсов Java — https://katalog-kursov.ru/

Нередко пользователи пытаются передать в приложение некорректные данные. Это происходит либо из злого умысла, либо по ошибке. Поэтому стоит проверять данные на соответствие бизнес-требованиям.

Эту задачу решает Bean Validation. Он интегрирован со Spring и Spring Boot. Hibernate Validator считается эталонной реализацией Bean Validation.

Основы валидации Bean

Для проверки данных используются аннотации над полями класса. Это декларативный подход, который не загрязняет код.

При передаче размеченного таким образом объекта класса в валидатор, происходит проверка на ограничения.

Настройка

Добавьте следующие зависимости в проект:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.0.Final</version>
</dependency>

dependencies {
    compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
    compile group: 'org.hibernate', name: 'hibernate-validator', version: '7.0.0.Final'
}

Валидация в Spring MVC Controller

Сначала данные попадают в контроллер. У входящего HTTP-запроса возможно проверить следующие параметры:

  • тело запроса
  • переменные пути (например, id в /foos/{id})
  • параметры запроса

Рассмотрим каждый из них подробнее.

Валидация тела запроса

Тело запроса POST и PUT обычно содержит данные в формате JSON. Spring автоматически сопоставляет входящий JSON с объектом Java.

Проверяем соответствует ли входящий Java объект нашим требованиям.

class Input {

     @Min(1)
     @Max(10)
     private int numberBetweenOneAndTen;

     @Pattern(regexp = "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$")
     private String ipAddress;

     // ...
}

  • Поле numberBetweenOneAndTen должно быть от 1 до 10, включительно.
  • Поле ipAddress должно содержать строку в формате IP-адреса.

Контроллер REST принимает объект Input и выполняет проверку:

@RestController
class ValidateRequestBodyController {

  @PostMapping("/validateBody")
  public ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {
    return ResponseEntity.ok("valid");
  }

}

Достаточно добавить в параметр input аннотацию @Valid, чтобы сообщить спрингу передать объект Валидатору, прежде чем делать с ним что-либо еще.

Если класс содержит поле с другим классом, который тоже необходимо проверить — это поле необходимо пометить аннотацией Valid.

Исключение MethodArgumentNotValidException выбрасывается, когда объект не проходит проверку. По умолчанию, Spring переведет это исключение в HTTP статус 400.

Проверка переменных пути и параметров запроса

Проверка переменных пути и параметров запроса работает по-другому.

Не проверяются сложные Java-объекты, так как path-переменные и параметры запроса являются примитивными типами, такими как int, или их аналогами: Integer или String.

Вместо аннотации поля класса, как описано выше, добавляют аннотацию ограничения (в данном случае @Min) непосредственно к параметру метода в контроллере Spring:

@Validated
@RestController
class ValidateParametersController {

  @GetMapping("/validatePathVariable/{id}")
  ResponseEntity<String> validatePathVariable(
      @PathVariable("id") @Min(5) int id
  ) {
    return ResponseEntity.ok("valid");
  }

  @GetMapping("/validateRequestParameter")
  ResponseEntity<String> validateRequestParameter(
      @RequestParam("param") @Min(5) int param
  ) { 
    return ResponseEntity.ok("valid");
  }
}

Обратите внимание, что необходимо добавить @Validated Spring в контроллер на уровне класса, чтобы сказать Spring проверять ограничения на параметрах метода.

В этом случае аннотация @Validated устанавливается на уровне класса, даже если она присутствует на методах.

В отличии валидации тела запроса, при неудачной проверки параметра вместо метода MethodArgumentNotValidException будет выброшен ConstraintViolationException. По умолчанию последует ответ со статусом HTTP 500 (Internal Server Error), так как Spring не регистрирует обработчик для этого исключения.

Вернем HTTP статус 400, так как клиент предоставил недействительный параметр. Для этого добавляем пользовательский обработчик исключений в контоллер:

@RestController
@Validated
class ValidateParametersController {

  // request mapping method omitted

  @ExceptionHandler(ConstraintViolationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
    return new ResponseEntity<>("not valid due to validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
  }

}

Позже рассмотрим, как вернуть структурированный ответ об ошибке, содержащий подробности обо всех неудачных подтверждениях для проверки клиентом.

Валидация в сервисном слое

Можно проверять данные на любых компонентах Spring. Для этого используется комбинация аннотаций @Validated и @Valid.

@Service
@Validated
class ValidatingService{

    void validateInput(@Valid Input input){
      // do something
    }

}

Аннотация @Validated устанавливается только на уровне класса, так что не ставьте ее на метод в данном случае.

Валидация сущностей JPA

Persistence Layer это последняя линия проверки данных. По умолчанию Spring Data использует Hibernate, который поддерживает Bean Validation из коробки.

Обычно мы не хотим делать проверку так поздно, поскольку это означает, что бизнес-код работал с потенциально невалидными объектами, что может привести к непредвиденным ошибкам.

Допустим, необходимо хранить объекты нашего класса Input в базе данных. Сначала добавляем нужную JPA аннотацию @Entity, а так же поле id:

@Entity
public class Input {

  @Id
  @GeneratedValue
  private Long id;

  @Min(1)
  @Max(10)
  private int numberBetweenOneAndTen;

  @Pattern(regexp = "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$")
  private String ipAddress;

  // ...

}

Когда репозиторий пытается сохранить невалидный Input, чьи аннотации ограничений нарушаются, выбрасывается ConstraintViolationException.

Bean Validation запускается Hibernate только после того как EntityManager вызовет flush.

Чтобы отключить Bean Validation в репозиториях Spring, достаточно установить свойство Spring Boot spring.jpa.properties.javax.persistence.validation.mode равным null.

Валидация конфигурации приложения

Spring Boot аннотация @ConfigurationProperties используется для связывания свойств из application.properties с Java объектом.

Данные из application необходимы для стабильной работы приложения. Bean Validation поможет обнаружить ошибку в этих данных при старте приложения.

Допустим имеется следующий конфигурационный класс:

@Validated
@ConfigurationProperties(prefix="app.properties")
class AppProperties {

  @NotEmpty
  private String name;

  @Min(value = 7)
  @Max(value = 30)
  private Integer reportIntervalInDays;

  @Email
  private String reportEmailAddress;

  // getters and setters
}

При попытке запуска с недействительным адресом электронной почты получаем ошибку:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException:
  Failed to bind properties under 'app.properties' to
  io.reflectoring.validation.AppProperties failed:

    Property: app.properties.reportEmailAddress
    Value: manager.analysisapp.com
    Reason: must be a well-formed email address

Action:

Update your application's configuration

Стандартные ограничения

Библиотека javax.validation имеет множество аннотаций для валидации.

Каждая аннотация имеет следующие поля:

  • message — указывает на ключ свойства в ValidationMessages.properties, который используется для отправки сообщения в случае нарушения ограничения.
  • groups — позволяет определить, при каких обстоятельствах будет срабатывать эта проверка (о группах проверки поговорим позже).
  • payload — позволяет определить полезную нагрузку, которая будет передаваться сс проверкой.
  • @Constraint — указывает на реализацию интерфейса ConstraintValidator.

Рассмотрим популярные ограничения.

@NotNull и @Null

@NotNull — аннотированный элемент не должен быть null. Принимает любой тип.
@Null — аннотированный элемент должен быть null. Принимает любой тип.

@NotBlank и @NotEmpty

@NotBlank — аннотированный элемент не должен быть null и должен содержать хотя бы один непробельный символ. Принимает CharSequence.
@NotEmpty — аннотированный элемент не должен быть null или пустым. Поддерживаемые типы:

  • CharSequence
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

@NotBlank применяется только к строкам и проверяет, что строка не пуста и не состоит только из пробелов.

@NotNull применяется к CharSequence, Collection, Map или Array и проверяет, что объект не равен null. Но при этом он может быть пуст.

@NotEmpty применяется к CharSequence, Collection, Map или Array и проверяет, что он не null имеет размер больше 0.

Аннотация @Size(min=6) пропустит строку состоящую из 6 пробелов и/или символов переноса строки, а @NotBlank не пропустит.

@Size

Размер аннотированного элемента должен быть между указанными границами, включая сами границы. null элементы считаются валидными.

Поддерживаемые типы:

  • CharSequence. Оценивается длина последовательности символов
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

Добавление пользовательского валидатора

Если имеющихся аннотаций ограничений недостаточно, то создайте новые.

В классе Input использовалось регулярное выражение для проверки того, что строка является IP адресом. Регулярное выражение не является полным: оно позволяет сокеты со значениями больше 255, таким образом «111.111.111.333» будет считаться действительным.

Давайте напишем валидатор, который реализует эту проверку на Java. Потому что как говорится, до решения проблемы регулярным выражением у вас была одна проблема, а теперь стало двe :smile:

Сначала создаем пользовательскую аннотацию @IpAddress:

@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = IpAddressValidator.class)
@Documented
public @interface IpAddress {

  String message() default "{IpAddress.invalid}";

  Class<?>[] groups() default { };

  Class<? extends Payload>[] payload() default { };

}

Реализация валидатора выглядит следующим образом:

class IpAddressValidator implements ConstraintValidator<IpAddress, String> {

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    Pattern pattern = 
      Pattern.compile("^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$");
    Matcher matcher = pattern.matcher(value);
    try {
      if (!matcher.matches()) {
        return false;
      } else {
        for (int i = 1; i <= 4; i++) {
          int octet = Integer.valueOf(matcher.group(i));
          if (octet > 255) {
            return false;
          }
        }
        return true;
      }
    } catch (Exception e) {
      return false;
    }
  }
}

Теперь можно использовать аннотацию @IpAddress, как и любую другую аннотацию ограничения.

class InputWithCustomValidator {

  @IpAddress
  private String ipAddress;

  // ...

}

Принудительный вызов валидации

Для принудительного вызова проверки, без использования Spring Boot, создайте валидатор вручную.

class ProgrammaticallyValidatingService {

  void validateInput(Input input) {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    Set<ConstraintViolation<Input>> violations = validator.validate(input);
    if (!violations.isEmpty()) {
      throw new ConstraintViolationException(violations);
    }
  }

}

Тем не менее, Spring Boot предоставляет предварительно сконфигурированный экземпляр валидатора. Внедрив этот экземпляр в сервис не придется создавать его вручную.

@Service
class ProgrammaticallyValidatingService {

  private Validator validator;

  public ProgrammaticallyValidatingService(Validator validator) {
    this.validator = validator;
  }

  public void validateInputWithInjectedValidator(Input input) {
    Set<ConstraintViolation<Input>> violations = validator.validate(input);
    if (!violations.isEmpty()) {
      throw new ConstraintViolationException(violations);
    }
  }
}

Когда этот сервис внедряется Spring, в конструктор автоматически вставляется экземпляр валидатора.

Группы валидаций

Некоторые объекты участвуют в разных вариантах использования.

Возьмем типичные операции CRUD: при обновлении и создании, скорее всего, будет использоваться один и тот же класс. Тем не менее, некоторые валидации должны срабатывать при различных обстоятельствах:

  • только перед созданием
  • только перед обновлением
  • или в обоих случаях

Функция Bean Validation, которая позволяет нам внедрять такие правила проверки, называется «Validation Groups».

Все аннотации ограничений должны иметь поле groups. Это поле быть использовано для передачи любых классов, каждый из которых определяет группу проверки.

Для нашего примера CRUD определим два маркерных интерфейса OnCreate и OnUpdate:

interface OnCreate {}

interface OnUpdate {}

Затем используем эти интерфейсы с любой аннотацией ограничения:

class InputWithGroups {

  @Null(groups = OnCreate.class)
  @NotNull(groups = OnUpdate.class)
  private Long id;

  // ...

}

Это позволит убедиться, что id пуст при создании и заполнен при обновлении.

Spring поддерживает группы проверки только с аннотацией @Validated

@Service
@Validated
class ValidatingServiceWithGroups {

    @Validated(OnCreate.class)
    void validateForCreate(@Valid InputWithGroups input){
      // do something
    }

    @Validated(OnUpdate.class)
    void validateForUpdate(@Valid InputWithGroups input){
      // do something
    }

}

Обратите внимание, что аннотация @Validated применяется ко всему классу. Чтобы определить, какая группа проверки активна, она также применяется на уровне метода.

Использование групп проверки может легко стать анти-паттерном. При использовании групп валидации сущность должна знать правила валидации для всех случаев использования (групп), в которых она используется.

Возвращение структурных ответов на ошибки

Когда проверка не удается, лучше вернуть клиенту понятное сообщение об ошибке. Для этого необходимо вернуть структуру данных с сообщением об ошибке для каждой проверки, которая не прошла валидацию.

Сначала нужно определить эту структуру данных. Назовем ее ValidationErrorResponse и она содержит список объектов Violation:

public class ValidationErrorResponse {

  private List<Violation> violations = new ArrayList<>();

  // ...
}

public class Violation {

  private final String fieldName;

  private final String message;

  // ...
}

Затем создадим глобальный ControllerAdvice, который обрабатывает все ConstraintViolationExventions, которые пробрасываются до уровня контроллера. Чтобы отлавливать ошибки валидации и для тел запросов, мы также будем работать с MethodArgumentNotValidExceptions:

@ControllerAdvice
class ErrorHandlingControllerAdvice {

  @ExceptionHandler(ConstraintViolationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onConstraintValidationException(
      ConstraintViolationException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (ConstraintViolation violation : e.getConstraintViolations()) {
      error.getViolations().add(
        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
    }
    return error;
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onMethodArgumentNotValidException(
      MethodArgumentNotValidException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
      error.getViolations().add(
        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
    }
    return error;
  }

}

Здесь информацию о нарушениях из исключений переводится в нашу структуру данных ValidationErrorResponse.

Обратите внимание на аннотацию @ControllerAdvice, которая делает методы обработки исключений глобально доступными для всех контроллеров в контексте приложения.

Spring integrates Bean Validation and gives you more

01. Validations in Java world

Validating data is a common task that occurs throughout an application, from the presentation layer to the persistence layer. Often the same validation logic is implemented in each layer, proving to be time-consuming and error-prone.

JSR (Java Specification Requests) has developed a Java Bean validation specification. Javax and Hibernate have implemented the specification.

Spring supports Bean validation and integrates Javax and Hibernate-validation and also adds more helper class to allow better validation.

I’ll use the following DTO as an example:

@Data
public class ToDoDto implements Serializable {

    private static final long serialVersionUID = 1L;

    private UUID uuid;

    @Email(message = "Please provide a valid Email")
    private String email;

    @Range(min = 13, max = 13, message = "Please provide a valid phone number. E.g., 0064-01-234-5678")
    private Integer phoneNumber;

    private String postAddress;

    @Digits(integer = 4, fraction =0,message = "Please provide a valid post code" )
    private String postCode;

    @NotBlank(message = "Content cannot be empty")
    private String content;

    @Future()
    private LocalDateTime due;
}

Bean Validation API

Bean validation annotation constraints can be applied on types, fields, methods, constructors, parameters, container elements, and other container annotations. Validation is applied not only to the object level, but it can also be inherited from super classes. Entire object graphs can be validated, meaning that if a class declares a field that has the type of a separate class containing validation, cascading validation can occur.

It is specifically not tied to either the web tier or the persistence tier, and is available for both server-side application programming. It evolves as to 3 versions now:

  1. JSR-303 : Bean Validation
    • Hibernate Validator 4.3.1.Final
  2. JSR 349 : Bean Validation 1.1
    • Hibernate Validator 5.1.1.Final
  3. JSR 380 : Bean Validation 2.0 (Jakarta Bean Validation)
    • Hibernate Validator 6.0.17.Final

Official package

javax.validation is JavaBean Validation official package

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

Hibernate implementation

You have noticed, hibernate has implemented it and uses it in different versions.

Hibernate-Validator extends javax.validation, e.g., it adds @Range and @Length.

You can mix the use of javax.validation.constraints and org.hibernate.validator.constraints.

for example, for our case, we imported both of them

import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Email;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotEmpty;

Hibernate-Validator Unit tests

You can get a validator by a ValidatorFactory and you can also just get it by @Autowire because Spring already provides a default validator.

class ToDoDtoValidationTest {

    //@Autowired Validator validate

    private Validator validator;

    @BeforeEach
    void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void tesAllGood() {
        ToDoDto dto = new ToDoDto();
        dto.setEmail("example@example.com");
        dto.setPostCode(1234);
        dto.setContent("example");
        Set<ConstraintViolation<ToDoDto>> violations = validator.validate(dto);
        Assertions.assertTrue(violations.isEmpty(), "should be empty, however we got violations, size: " + violations.size());
    }

    @Test
    public void testContentIsEmpty() {

        ToDoDto dto = new ToDoDto();
        dto.setEmail("example@example.com");
        dto.setPostCode(1234);
        Set<ConstraintViolation<ToDoDto>> violations = validator.validate(dto);

        Assertions.assertEquals(1, violations.size(), "should be 1 exception");
    }
}

Spring Validation

Spring supports Java’s Bean Validation API, This makes it easy to declare validation rules as opposed to explicitly writing declaration logic in your application code.

Spring provides a built-in validation API by way of the Validator interface. This interface allowing you to encapsulate your validation logic into a class responsible for validating the target object. In addition to the target object, the validate method takes an Errors object, which is used to collect any validation errors that may occur.

Spring also provides a handy utility class, ValidationUtils, which provides convenience methods for invoking other validators, checking for common problems such as empty strings, and reporting errors back to the provided Errors object.

LocalValidatorFactoryBean is a Spring-managed bean since Spring 3.0.

How to import validation package

  1. If your spring-boot version is less than 2.3.x,spring-boot-starter-web has included hibernate-validator so you don’t need to import other packages.
  2. otherwise, you need to manually import either of the following packages:
    1. hibernate-validator
    2. spring-boot-starter-validation

Where should the validation happen

Generally speaking, there are three layers we would like to implement validation:

  • presentation layer, we must validate the DTO
  • Service layer, something we need to do validation
  • persistence layer, defitely you don’t want to save some bad data

02. DTO Bean Validation in Spring, using @Valid

For now, we would like to know how Java Bean validation works in Spring.

Later, in the next chapter, we’ll introduce the parameter validation in Spring. Parameters are not java beans, so you cannot use bean validation against them.

The above two validations would throw different kinds of exceptions, we’ll cover it in a separate chapter.

Note that Spring MVC validates ViewModels (VM is not DTO) and put results to a BindResult, I don’t have a plan to mention it in the following parts.

Request body Validation with Bean Validation and Spring Validation

  1. use Bean Validation, add @Valid in front of @RequestBody works. It validates all the fields before throwing exceptions. Invalid Request body throws MethodArgumentNotValidException.

     @PostMapping
     ResponseEntity<ToDoDto> createTodo(@Valid @RequestBody ToDoDto toDoDto) {
         return ResponseEntity.ok(toDoDto);
     }
    
     @ExceptionHandler({MethodArgumentNotValidException.class})
     public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    
         HttpHeaders headers = new HttpHeaders();
         String ResponseBody = "Response Body, should be a JSON";
         return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST);
     }
    
  2. use Spring Validation to validate Request Body

    1. class level @Validated doesn’t help with Request Body validation
    2. Add @Valdaited in front of @RequestBody works, we’ll explain why both of them work
    3. Without Spring data-binding and validation errors, it throws MethodArgumentNotValidException and goes to 400 error
     @PostMapping
     ResponseEntity<ToDoDto> createTodo(@Validated @RequestBody ToDoDto toDoDto) {
         return ResponseEntity.ok(toDoDto);
     }
    
     @ExceptionHandler({MethodArgumentNotValidException.class})
     public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    
         HttpHeaders headers = new HttpHeaders();
         String ResponseBody = "Response Body, should be a JSON";
         return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST);
     }
    
  3. Use Spring validation error binding and validation errors. You can take advantage of the errors object, it cotnains the filed and error message, so that you can return the error message to the API caller. It works for both Bean validation and Spring validation. It’s from package org.springframework.validation

@PostMapping
ResponseEntity<ToDoDto> createTodo(@Valid @RequestBody ToDoDto toDoDto, Errors errors) {

    if (errors.hasErrors()) {
        throw new RuntimeException("Please handle these validation exceptions");
    }

    return ResponseEntity.ok(toDoDto);
}
@PostMapping
ResponseEntity<ToDoDto> createTodo(@Validated @RequestBody ToDoDto toDoDto, Errors errors) {

    if (errors.hasErrors()) {
        throw new RuntimeException("Please handle these validation exceptions");
    }

    return ResponseEntity.ok(toDoDto);
}

Testing with DTO Bean Validation

Note that you should customize the error message. We will do this in another chapter.

@PostMapping
ResponseEntity<ToDoDto> createTodo(@Valid @RequestBody ToDoDto toDoDto) {
    return ResponseEntity.ok(toDoDto);
}

@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {

    HttpHeaders headers = new HttpHeaders();
    String ResponseBody = "Response Body, should be a JSON";
    return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST);
}
@Test
void createToDoWithInvalidEmail() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@@");
    dto.setPostCode(1234);
    dto.setContent("exdample");

    String body = objectMapper.writeValueAsString(dto);

    mockMvc.perform(post("/")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                    .andExpect(status().isBadRequest())
                    .andExpect(content().string("Response Body, should be a JSON"));
}

@Test
void createToDoWithValidEmail() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@example.com");
    dto.setPostCode(1234);
    dto.setContent("example");

    String body = objectMapper.writeValueAsString(dto);

    mockMvc.perform(post("/")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isOk());
}

03. Spring Parameter validation, introducing @Validated

Parameters are not JavaBeans, so Bean Validation doesn’t help.
Spring implements parameter validation by itself.

The keys are:

  1. add @Validated to the class you want to validate parameters. Without it, parameter validation is not enabled
  2. you don’t need to add @Valid to the parameter
  3. it throws ConstraintViolationException if the parameter is invalid
  4. it works for both RequestParam and PathVariable
  5. you cannot use Errors Binding here, otherwise, you’ll get an IllegalStateException

     java.lang.IllegalStateException:
         An Errors/BindingResult argument is expected to be declared immediately after the model attribute,
         the @RequestBody or the @RequestPart arguments to which they apply:
             org.springframework.http.ResponseEntity
             com.mg.todo.ToDoController.fetchByEmail(
                     java.lang.String,org.springframework.validation.Errors)
    

Example and test case

import org.springframework.validation.annotation.Validated;

@Validated
@RestController
public class ToDoController {

    private final ToDoService toDoService;

    public ToDoController(ToDoService toDoService) {
        this.toDoService = toDoService;
    }

    @GetMapping
    ResponseEntity<List<ToDoDto>> fetchByEmail(@RequestParam("email") @Email(message = "should be a valid email") String email) {
        return ResponseEntity.ok(new ArrayList<ToDoDto>());
    }

    @ExceptionHandler({ConstraintViolationException.class})
    public ResponseEntity handleConstrainViolationException() {
        HttpHeaders headers = new HttpHeaders();
        String ResponseBody = "should be a valid email";
        return new ResponseEntity(ResponseBody,headers, HttpStatus.BAD_REQUEST );
    }
}
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ToDoController.class)
class ToDoControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    ToDoService toDoService;

    @Test
    void fetchByValidEmail() throws Exception {
        mockMvc.perform(get("/")
            .param("email","valid@example.com")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(status().isOk());
    }

    @Test
    void fetchByInvalidEmail() throws Exception {
        mockMvc.perform(get("/")
            .param("email","invalid@@")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(status().isBadRequest())
            .andExpect(content().string("should be a valid email"));
    }
}

You can also use @Validated for Request Body DTO validation

In the background, Spring wraps Hibernate-Validator and does the validation work.
Note that resolveArgument() method calls validateIfApplicable(binder, parameter); and this is the key.

It picks up @Validate parameters first and it triggers validation.
If it’s absent, it looks for any parameters’ annotation that start from Valid.

That’s why @Validated works for both DTO and parameters and DTO here.

package org.springframework.web.servlet.mvc.method.annotation;

/**
 * Resolves method arguments annotated with {@code @RequestBody} and handles return
 * values from methods annotated with {@code @ResponseBody} by reading and writing
 * to the body of the request or response with an {@link HttpMessageConverter}.
 */
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {

    /**
    * Throws MethodArgumentNotValidException if validation fails.
    * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
    * is {@code true} and there is no body content or if there is no suitable
    * converter to read the content with.
    */
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = parameter.nestedIfOptional();
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }

        return adaptArgumentIfNecessary(arg, parameter);
    }
}
package org.springframework.web.servlet.mvc.method.annotation;
/**
 * A base class for resolving method argument values by reading from the body of
 * a request with {@link HttpMessageConverter HttpMessageConverters}.
 */
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
    /**
        * Validate the binding target if applicable.
        * <p>The default implementation checks for {@code @javax.validation.Valid},
        * Spring's {@link org.springframework.validation.annotation.Validated},
        * and custom annotations whose name starts with "Valid".
        */
    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation ann : annotations) {
            Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
                binder.validate(validationHints);
                break;
            }
        }
    }
}

04. Handle Validation Errors

In the above examples, we would know:

  1. Bean validation throws MethodArgumentNotValidException
    1. If we use parameter Error Binding, we can make use of the errors object.
    2. You can get each error by ex.getBindingResult().getFieldErrors()
  2. Spring parameter validation throws ConstraintViolationException,
    1. it cannot use parameter Error Binding.
    2. You can get each error by e.getConstraintViolations()
  3. There actually is another exception org.springframework.validation.BindException, thrown in MVC form submit Content-Type: multipart/form-data, we didn’t mention this but you should know it exists.

A new issue comes out. How can we format/unify our error response body, so that the API caller would have a meaningful error message.

  1. We should always return a unified Response entity for exceptions. Use ResponseStatusException
  2. We could make use of @RestControllerAdvice to handle exception globally

Returns Unified error response

this content is from All You Need To Know About Bean Validation With Spring Boot

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ValidationErrorResponse {
    private List<Violation> violations = new ArrayList<>();
}

@Data
@AllArgsConstructor
 class Violation {
    private String fieldName;
    private String message;
}
@ControllerAdvice
public class GlobalRestExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    ValidationErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        ValidationErrorResponse error = new ValidationErrorResponse();
        for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
            error.getViolations().add(
                new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
        }
        return error;
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    ValidationErrorResponse onConstraintValidationException(
        ConstraintViolationException e) {
        ValidationErrorResponse error = new ValidationErrorResponse();
        for (ConstraintViolation violation : e.getConstraintViolations()) {
            error.getViolations().add(
                new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
        }
        return error;
    }
}
@Test
void createToDoWithInvalidEmail() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@@");
    dto.setPostCode("0060");

    String body = objectMapper.writeValueAsString(dto);

    final MvcResult mvcResult = mockMvc.perform(post("/")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isBadRequest())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andReturn();

    final String content = mvcResult.getResponse().getContentAsString();

    Assertions.assertNotNull(content);
    Assertions.assertTrue(content.contains(""fieldName":"content","message":"Content cannot be empty""),"should contain this text");
    Assertions.assertTrue(content.contains(""fieldName":"email","message":"Please provide a valid Email""),"should contain this text");
}

05. @Valid vs @Validated, validation groups using @Validated

Oftentimes, you need to validate a filed differently based on the scenario. A common case, we don’t have an Id for an object on creating, however, we do need a valid id while updating. The javax.validation @Valid doesn’t support Groups. The Spring @Validated does.

Steps:

  1. Add groups to the Model
  2. Use @Validated and Groups in Controller
  3. MockMVC testing

Use Validation Groups

@Data
public class ToDoDto implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotNull(message = "Id should not be null on updating", groups = Update.class)
    private UUID uuid;

    @Email(message = "Please provide a valid Email", groups = {Create.class, Update.class})
    private String email;

    @Range(min = 13, max = 13, message = "Please provide a valid phone number. E.g., 0064-01-234-5678", groups = {Create.class, Update.class})
    private Integer phoneNumber;

    @Digits(integer = 4, fraction = 0, message = "Please provide a valid post code", groups = {Create.class, Update.class})
    private String postCode;

    @NotBlank(message = "Content cannot be empty", groups = {Create.class, Update.class})
    private String content;

    @Future(groups = {Create.class, Update.class})
    private LocalDateTime due;

    //on creating
    public interface Create { }

    // on updating
    public interface Update { }
}
@PostMapping
ResponseEntity<ToDoDto> createTodo(@Validated(ToDoDto.Create.class) @RequestBody ToDoDto toDoDto) {
    return ResponseEntity.ok(toDoDto);
}
@Test
void updateToDoWithUUID() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@example.com");
    dto.setPostCode("0060");
    dto.setContent("example");
    dto.setUuid(UUID.randomUUID());

    String body = objectMapper.writeValueAsString(dto);

    mockMvc.perform(put("/")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON));
}

@Test
void updateToDoWithoutUUID() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@example.com");
    dto.setPostCode("0060");
    dto.setContent("example");

    String body = objectMapper.writeValueAsString(dto);

    final MvcResult mvcResult = mockMvc.perform(put("/")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isBadRequest())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andReturn();

    String content = mvcResult.getResponse().getContentAsString();

    Assertions.assertTrue(content.contains( "violations"));
    Assertions.assertTrue(content.contains("Id should not be null on updating"));
}

06. @Valid vs @Validated, Collection validation using @Valid

In the Demo, all fields we validate are Java provided types, e.g., String, Long or LocalDateTime. However, it’s not always the case.

  1. we may need to validate another object nested in your DTO
  2. We may need to validate a list of your DTOs

Validate Nested object

@Data
public class ToDoDto implements Serializable {

    // .... other fields

    @NotNull(groups = {Create.class, Update.class})
    @Valid
    private MetaData metaData;

    @Data
    public static class MetaData {

        @Min(value = 1, groups = Update.class)
        private Long Id;

        @NotNull(groups = {Create.class, Update.class})
        @Length(min = 2, max = 10, groups = {Create.class, Update.class})
        private String Name;

        @NotNull(groups = {Create.class, Update.class})
        @Length(min = 2, max = 10, groups = {Create.class, Update.class})
        private String position;
    }

    public interface Create { }
    public interface Update { }

In our case, if you have an empty Meta data object, you’ll get the following error mesasge:

{"violations":[
    {"fieldName":"metaData.Name","message":"must not be null"},
    {"fieldName":"metaData.position","message":"must not be null"}
    ]}

Validate a list of DTOs

  1. In java.util.Collection, List and Set don’t work for validation
  2. We need to implement our own collection, e.g., myValidationList to accept the data and do validation

Bean Validation doesn’t work in List of DTOs

@PostMapping(value = "/todos")
ResponseEntity<List<ToDoDto>> createTodos(@Validated(ToDoDto.Create.class) @RequestBody List<ToDoDto> toDoDtos) {
    return ResponseEntity.ok(toDoDtos);
}
@Test
void createToDosWithInvalidEmailShouldBe400ButIs200() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@@");
    dto.setPostCode("0060");

    ToDoDto dto2 = new ToDoDto();
    dto2.setEmail("example@@");
    dto2.setPostCode("0060");

    String body = objectMapper.writeValueAsString(List.of(dto, dto2));

    mockMvc.perform(post("/todos")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isOk());
}

Implement MyValidationList for List of DTOs

It throws org.springframework.beans.NotReadablePropertyException if there is any violation

public class MyValidationList<E> implements List<E> {
    @Delegate // lombok annotation
    @Valid
    public List<E> list = new ArrayList<>();
}
@PostMapping(value = "/todos2")
ResponseEntity<List<ToDoDto>> createTodos2(@Validated(ToDoDto.Create.class) @RequestBody MyValidationList<ToDoDto> toDoDtos) {
    return ResponseEntity.ok(toDoDtos);
}

@ExceptionHandler({NotReadablePropertyException.class})
public ResponseEntity handleMethodArgumentNotValidException(NotReadablePropertyException e) {

    HttpHeaders headers = new HttpHeaders();
    String ResponseBody = e.getMessage();
    return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST);
}
@Test
void createToDosWithMyValidationListOfDtosWithInvalidEmail() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@@");
    dto.setPostCode("0060");

    ToDoDto dto2 = new ToDoDto();
    dto2.setEmail("example@@");
    dto2.setPostCode("0060");

    String body = objectMapper.writeValueAsString(List.of(dto, dto2));

    mockMvc.perform(post("/todos2")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isBadRequest());
}

@Test
void createToDosWithMyValidationListOfDtos() throws Exception {

    ToDoDto dto = new ToDoDto();
    dto.setEmail("example@example.com");
    dto.setPostCode("0060");
    dto.setContent("example");
    dto.setUuid(UUID.randomUUID());
    final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
    metaData.setName("name");
    metaData.setPosition("position");
    dto.setMetaData(metaData);

    ToDoDto dto2 = new ToDoDto();
    dto2.setEmail("example@example.com");
    dto2.setPostCode("0060");
    dto2.setContent("example");
    dto2.setUuid(UUID.randomUUID());
    final ToDoDto.MetaData metaData2 = new ToDoDto.MetaData();
    metaData2.setName("name");
    metaData2.setPosition("position");
    dto2.setMetaData(metaData2);

    String body = objectMapper.writeValueAsString(List.of(dto));

    mockMvc.perform(post("/todos2")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isOk());
}

07. Declarative customized annotation validation

Again, in a real scenario, we may need more complicated logic than what Spring provides us.

Steps:

  1. Add the new annotation interface
  2. Add a customized validator and implement ConstraintValidator
  3. Add this new annotation to the DTO
  4. No need to do anything to the Controller or Service layer
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface NotGmail {

    String message() default "We don't support Gmail";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
public class NotGmailValidator implements ConstraintValidator<NotGmail, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value != null) {
            return !value.toLowerCase().contains("@gmail");
        }
        return true;
    }
}
@Data
public class ToDoDto implements Serializable {
    @Email(message = "Please provide a valid Email", groups = {Create.class, Update.class})
    @com.mg.todo.dto.NotGmail(message = "Sorry, no Google", groups = {Create.class, Update.class})
    private String email;
}
@Test
void createToDoWithGoogleEmailShouldBeInvalid() throws Exception {
    ToDoDto dto = new ToDoDto();
    dto.setEmail("xiguadawang@gmail.com");
    dto.setPostCode("0060");
    dto.setContent("example");
    final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
    metaData.setName("name");
    metaData.setPosition("position");
    dto.setMetaData(metaData);
    String body = objectMapper.writeValueAsString(dto);

    final MvcResult mvcResult = mockMvc.perform(post("/")
        .contentType(MediaType.APPLICATION_JSON)
        .content(body))
        .andExpect(status().isBadRequest())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andReturn();

    final String content = mvcResult.getResponse().getContentAsString();

    Assertions.assertTrue(content.contains("{"violations":[{"fieldName":"email","message":"Sorry, no Google"}]}"),"should contain this text");
}

08. Programmatic validator validation

Spring Boot provides us with a pre-configured Validator instance, and you can always explicitly reference the javax.validation.Validator.

It doesn’t need the @Validated key world on the class name.

A scenario like when a new user registers. When all fields are good, we still need to validate the username is occupied or not.
We use declarative validation for the correctness of the email and we need to issue a database call to check the username which should be a programmatic validation.

Spring Validator and Javax Validator works the same way

@RestController
public class ToDoController {

    @Qualifier("defaultValidator")
    @Autowired
    private org.springframework.validation.Validator springValidator;

    @Autowired
    private javax.validation.Validator globalValidator;

    @PostMapping(value = "/javaxValidator")
    ResponseEntity<ToDoDto> createTodos3(@RequestBody ToDoDto toDoDto) {
        Set<ConstraintViolation<ToDoDto>> violations = globalValidator.validate(toDoDto, ToDoDto.Create.class);
        if (violations.isEmpty()) {
            // passed the validation, free to go
        } else {
            for (ConstraintViolation<ToDoDto> userDTOConstraintViolation : violations) {
                // Failed the validation, do your business here. E.g., send the error to a Queue
            }
        }
        return new ResponseEntity("Failed for javaxValidator", new HttpHeaders(), HttpStatus.BAD_REQUEST);
    }

    @PostMapping(value = "/springValidator")
    ResponseEntity<ToDoDto> createTodos4(@RequestBody ToDoDto toDoDto) {
        Set<ConstraintViolation<ToDoDto>> violations = globalValidator.validate(toDoDto, ToDoDto.Create.class);
        if (violations.isEmpty()) {
            // passed the validation, free to go
        } else {
            for (ConstraintViolation<ToDoDto> userDTOConstraintViolation : violations) {
                // Failed the validation, do your business here. E.g., send the error to a Queue
            }
        }
        return new ResponseEntity("Failed for spring Validator", new HttpHeaders(), HttpStatus.BAD_REQUEST);
    }
}
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ToDoController.class)
class ToDoControllerTest {

    @Autowired
    private javax.validation.Validator globalValidator;

    @Qualifier("defaultValidator")
    @Autowired
    private org.springframework.validation.Validator springValidator;

    @Test
    void createToDosWithJavaxValidatorAndGoogleEmailShouldReturnBadRequest() throws Exception {

        ToDoDto dto = new ToDoDto();
        dto.setEmail("xiguadawang@gmail.com");
        dto.setPostCode("0060");
        dto.setContent("example");
        dto.setUuid(UUID.randomUUID());
        final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
        metaData.setName("name");
        metaData.setPosition("position");
        dto.setMetaData(metaData);

        String body = objectMapper.writeValueAsString(dto);

        final MvcResult mvcResult = mockMvc.perform(post("/javaxValidator")
            .contentType(MediaType.APPLICATION_JSON)
            .content(body))
            .andExpect(status().isBadRequest())
            .andReturn();

        final String content = mvcResult.getResponse().getContentAsString();
        Assertions.assertEquals("Failed for javaxValidator", content);
    }

    @Test
    void createToDosWithSpringValidatorGmailShouldReturnBadRequest() throws Exception {

        ToDoDto dto = new ToDoDto();
        dto.setEmail("xiguadawang@gmail.com");
        dto.setPostCode("0060");
        dto.setContent("example");
        dto.setUuid(UUID.randomUUID());
        final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
        metaData.setName("name");
        metaData.setPosition("position");
        dto.setMetaData(metaData);

        String body = objectMapper.writeValueAsString(dto);

        final MvcResult mvcResult = mockMvc.perform(post("/springValidator")
            .contentType(MediaType.APPLICATION_JSON)
            .content(body))
            .andExpect(status().isBadRequest())
            .andReturn();

        final String content = mvcResult.getResponse().getContentAsString();
        Assertions.assertEquals("Failed for spring Validator", content);
    }
}

Configure Validator Bean in Spring

You can configure a Validator bean by yourself. By default, it uses HibernateValidator.class, there are other options like ApacheValidationProvider.class.

A benefit of configuring it is to make it fail fast.

In failFast mode, it throws an exception on the first violation.

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class)
        .configure()
        .failFast(true)
        .buildValidatorFactory();
    return validatorFactory.getValidator();
}
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .addProperty( "hibernate.validator.fail_fast", "true" )
        .buildValidatorFactory();
    return validatorFactory.getValidator();

09. Service layer validation needs both @Validate and @Valid

@Service
@Validated
class ValidatingService{

    void validateInput(@Valid DTO dto){
      // do something
    }
}

10. Persistence layer validation

  1. By default, Spring uses Hibernate-Validator by default so that it supports Bean Validation out of the box
  2. The validation happends on the repository save or update method
  3. No need to add any annotation to the Repository
  4. It throws ConstraintViolationException on violations

References

  • Spring Validation最佳实践及其实现原理
  • Complete Guide to Validation With Spring Boot
  • Jakarta Bean Validation — Constrain once, validate everywhere

Во время работы вашего приложения часто будут возникать исключительные ситуации. Когда у вас простое консольное приложение, то все просто – ошибка выводится в консоль. Но как быть с веб-приложением?

Допустим у пользователя отсутсвует доступ, или он передал некорректные данные. Лучшим вариантом будет в ответ на такие ситуации, отправлять пользователю сообщения с описанием ошибки. Это позволит клиенту вашего API скорректировать свой запрос.

В данной статье разберём основные возможности, которые предоставляет SpringBoot для решения этой задачи и на простых примерах посмотрим как всё работает.

@ExceptionHandler

@ExceptionHandler позволяет обрабатывать исключения на уровне отдельного контроллера. Для этого достаточно объявить метод в контроллере, в котором будет содержаться вся логика обработки нужного исключения, и пометить его аннотацией.

Для примера у нас будет сущность Person, бизнес сервис к ней и контроллер. Контроллер имеет один эндпойнт, который возвращает пользователя по логину. Рассмотрим классы нашего приложения:

Сущность Person:

package dev.struchkov.general.sort; import java.text.MessageFormat;

public class Person { private String lastName; private String firstName; private Integer age; //getters and setters }

Контроллер PersonController:

package dev.struchkov.example.controlleradvice.controller; import dev.struchkov.example.controlleradvice.domain.Person; import dev.struchkov.example.controlleradvice.service.PersonService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.UUID;

@Slf4j @RestController @RequestMapping("api/person") @RequiredArgsConstructor public class PersonController { private final PersonService personService; @GetMapping public ResponseEntity<Person> getByLogin(@RequestParam("login") String login) { return ResponseEntity.ok(personService.getByLoginOrThrown(login)); } @GetMapping("{id}") public ResponseEntity<Person> getById(@PathVariable("id") UUID id) { return ResponseEntity.ok(personService.getById(id).orElseThrow()); } }

И наконец PersonService, который будет возвращать исключение NotFoundException, если пользователя не будет в мапе persons.

package dev.struchkov.example.controlleradvice.service; import dev.struchkov.example.controlleradvice.domain.Person; import dev.struchkov.example.controlleradvice.exception.NotFoundException; import lombok.NonNull; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID;

@Service public class PersonService { private final Map<UUID, Person> people = new HashMap<>(); public PersonService() { final UUID komarId = UUID.randomUUID(); people.put(komarId, new Person(komarId, "komar", "Алексей", "ertyuiop")); } public Person getByLoginOrThrown(@NonNull String login) { return people.values().stream() .filter(person -> person.getLogin().equals(login)) .findFirst() .orElseThrow(() -> new NotFoundException("Пользователь не найден")); } public Optional<Person> getById(@NonNull UUID id) { return Optional.ofNullable(people.get(id)); } }

Перед тем, как проверить работу исключения, давайте посмотрим на успешную работу эндпойнта.

Все отлично. Нам в ответ пришел код 200, а в теле ответа пришел JSON нашей сущности. А теперь мы отправим запрос с логином пользователя, которого у нас нет. Посмотрим, что сделает Spring по умолчанию.

Обратите внимание, ошибка 500 – это стандартный ответ Spring на возникновение любого неизвестного исключения. Также исключение было выведено в консоль.

Как я уже говорил, отличным решением будет сообщить пользователю, что он делает не так. Для этого добавляем метод с аннотацией @ExceptionHandler, который будет перехватывать исключение и отправлять понятный ответ пользователю.

@RequestMapping("api/person")
@RequiredArgsConstructor
public class PersonController {

    private final PersonService personService;

    @GetMapping
    public ResponseEntity<Person> getByLogin(@RequestParam("login") String login) {
        return ResponseEntity.ok(personService.getByLoginOrThrown(login));
    }

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorMessage> handleException(NotFoundException exception) {
        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ErrorMessage(exception.getMessage()));
    }

}

Вызываем повторно наш метод и видим, что мы стали получать понятное описание ошибки.

Но теперь вернулся 200 http код, куда корректнее вернуть 404 код.

Однако некоторые разработчики предпочитают возвращать объект, вместо ResponseEntity<T>. Тогда вам необходимо воспользоваться аннотацией @ResponseStatus.

    import org.springframework.web.bind.annotation.ResponseStatus;

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NotFoundException.class)
    public ErrorMessage handleException(NotFoundException exception) {
        return new ErrorMessage(exception.getMessage());
    }

Если попробовать совместить ResponseEntity<T> и @ResponseStatus, http-код будет взят из ResponseEntity<T>.

Главный недостаток @ExceptionHandler в том, что он определяется для каждого контроллера отдельно. Обычно намного проще обрабатывать все исключения в одном месте.

Хотя это ограничение можно обойти если @ExceptionHandler будет определен в базовом классе, от которого будут наследоваться все контроллеры в приложении, но такой подход не всегда возможен, особенно если перед нами старое приложение с большим количеством легаси.

HandlerExceptionResolver

Как мы знаем в программировании магии нет, какой механизм задействуется, чтобы перехватывать исключения?

Интерфейс HandlerExceptionResolver является общим для обработчиков исключений в Spring. Все исключений выброшенные в приложении будут обработаны одним из подклассов HandlerExceptionResolver. Можно сделать как свою собственную реализацию данного интерфейса, так и использовать существующие реализации, которые предоставляет нам Spring из коробки.

Давайте разберем стандартные для начала:

ExceptionHandlerExceptionResolver — этот резолвер является частью механизма обработки исключений помеченных аннотацией @ExceptionHandler, которую мы рассмотрели выше.

DefaultHandlerExceptionResolver — используется для обработки стандартных исключений Spring и устанавливает соответствующий код ответа, в зависимости от типа исключения:

Exception HTTP Status Code
BindException 400 (Bad Request)
ConversionNotSupportedException 500 (Internal Server Error)
HttpMediaTypeNotAcceptableException 406 (Not Acceptable)
HttpMediaTypeNotSupportedException 415 (Unsupported Media Type)
HttpMessageNotReadableException 400 (Bad Request)
HttpMessageNotWritableException 500 (Internal Server Error)
HttpRequestMethodNotSupportedException 405 (Method Not Allowed)
MethodArgumentNotValidException 400 (Bad Request)
MissingServletRequestParameterException 400 (Bad Request)
MissingServletRequestPartException 400 (Bad Request)
NoSuchRequestHandlingMethodException 404 (Not Found)
TypeMismatchException 400 (Bad Request)

Мы можем создать собственный HandlerExceptionResolver. Назовем его CustomExceptionResolver и вот как он будет выглядеть:

package dev.struchkov.example.controlleradvice.service;

import dev.struchkov.example.controlleradvice.exception.NotFoundException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;

@Component public class CustomExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) { final ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView()); if (e instanceof NotFoundException) { modelAndView.setStatus(HttpStatus.NOT_FOUND); modelAndView.addObject("message", "Пользователь не найден"); return modelAndView; } modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); modelAndView.addObject("message", "При выполнении запроса произошла ошибка"); return modelAndView; } }

Мы создаем объект представления – ModelAndView, который будет отправлен пользователю, и заполняем его. Для этого проверяем тип исключения, после чего добавляем в представление сообщение о конкретной ошибке и возвращаем представление из метода. Если ошибка имеет какой-то другой тип, который мы не предусмотрели в этом обработчике, то мы отправляем сообщение об ошибке при выполнении запроса.

Так как мы пометили этот класс аннотацией @Component, Spring сам найдет и внедрит наш резолвер куда нужно. Посмотрим, как Spring хранит эти резолверы в классе DispatcherServlet.

Все резолверы хранятся в обычном ArrayList и в случае исключнеия вызываются по порядку, при этом наш резолвер оказался последним. Таким образом, если непосредственно в контроллере окажется @ExceptionHandler обработчик, то наш кастомный резолвер не будет вызван, так как обработка будет выполнена в ExceptionHandlerExceptionResolver.

Важное замечание. У меня не получилось перехватить здесь ни одно Spring исключение, например MethodArgumentTypeMismatchException, которое возникает если передавать неверный тип для аргументов @RequestParam.

Этот способ был показан больше для образовательных целей, чтобы показать в общих чертах, как работает этот механизм. Не стоит использовать этот способ, так как есть вариант намного удобнее.

@RestControllerAdvice

Исключения возникают в разных сервисах приложения, но удобнее всего обрабатывать все исключения в каком-то одном месте. Именно для этого в SpringBoot предназначены аннотации @ControllerAdvice и @RestControllerAdvice. В статье мы рассмотрим @RestControllerAdvice, так как у нас REST API.

На самом деле все довольно просто. Мы берем методы помеченные аннотацией @ExceptionHandler, которые у нас были в контроллерах и переносим в отдельный класс аннотированный @RestControllerAdvice.

package dev.struchkov.example.controlleradvice.controller; import dev.struchkov.example.controlleradvice.domain.ErrorMessage; import dev.struchkov.example.controlleradvice.exception.NotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

@RestControllerAdvice public class ExceptionApiHandler { @ExceptionHandler(NotFoundException.class) public ResponseEntity<ErrorMessage> notFoundException(NotFoundException exception) { return ResponseEntity .status(HttpStatus.NOT_FOUND) .body(new ErrorMessage(exception.getMessage())); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<ErrorMessage> mismatchException(MethodArgumentTypeMismatchException exception) { return ResponseEntity .status(HttpStatus.NOT_FOUND) .body(new ErrorMessage(exception.getMessage())); } }

За обработку этих методов класса точно также отвечает класс ExceptionHandlerExceptionResolver. При этом мы можем здесь перехватывать даже стандартные исключения Spring, такие как MethodArgumentTypeMismatchException.

На мой взгляд, это самый удобный и простой способ обработки возвращаемых пользователю исключений.

Еще про обработку

Все написанное дальше относится к любому способу обработки исключений.

Запись в лог

Важно отметить, что исключения больше не записываются в лог. Если помимо ответа пользователю, вам все же необходимо записать это событие в лог, то необходимо добавить строчку записи в методе обработчике, например вот так:

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> handleException(NotFoundException exception) {
    log.error(exception.getMessage(), exception);
    return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(new ErrorMessage(exception.getMessage()));
}

Перекрытие исключений

Вы можете использовать иерархию исключений с наследованием и обработчики исключений для всей своей иерархии. В таком случае обработка исключения будет попадать в самый специализированный обработчик.

Допустим мы бросаем NotFoundException, как в примере выше, который наследуется от RuntimeException. И у вас будет два обработчика исключений для NotFoundException и RuntimeException. Исключение попадет в обработчик для NotFoundException. Если этот обработчик убрать, то попадет в обработчик для RuntimeException.

Резюмирую

Обработка исключений это важная часть REST API. Она позволяет возвращать клиентам информационные сообщения, которые помогут им скорректировать свой запрос.

Мы можем по разному реализовать обработку в зависимости от нашей архитектуры. Предпочитаемым способом считаю вариант с @RestControllerAdvice. Этот вариант самый чистый и понятный.

Понравилась статья? Поделить с друзьями:
  • Bfsvc error failed to set element application device status c00000bb
  • Bfsvc error failed to initialize global state status c0000001
  • Bfsvc error could not open the bcd template store status c000015c
  • Bfsvc error could not open the bcd template store status c0000022
  • Bfsvc error could not open the bcd template store status c000000f windows 10