After introducing the request-payload data binding process, we must talk about validation.
The goal of this recipe is to show how to get Spring MVC to reject request body payloads that are not satisfying a bean validation (JSR-303) or not satisfying the constraints of a defined Spring validator implementation.
After the Maven and Spring configuration, we will see how to bind a validator to an incoming request, how to define the validator to perform custom rules, how to set up a JSR-303 validation, and how to handle the validation results.
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.3.1.Final</version> </dependency>
LocalValidatorFactoryBean
has been registered in our dispatcher-servlet.xml
(cloudstreetmarket-api
):<bean id="validator" class="org.sfw.validation.beanvalidation.LocalValidatorFactoryBean"/>
UsersController
and TransactionController
have seen their POST
and PUT
method signature altered with the addition of a @Valid
annotation on the @RequestBody
arguments:@RequestMapping(method=PUT) @ResponseStatus(HttpStatus.OK) public void update(@Valid @RequestBody User user, BindingResult result){ ValidatorUtil.raiseFirstError(result); user = communityService.updateUser(user); }
@InitBinder
annotated method:@InitBinder protected void initBinder(WebDataBinder binder) { binder.setValidator(new UserValidator()); }
UserValidator
which is Validator
implementation:package edu.zipcloud.cloudstreetmarket.core.validators; import java.util.Map; import javax.validation.groups.Default; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import edu.zc.csm.core.entities.User; import edu.zc.csm.core.util.ValidatorUtil; public class UserValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return User.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors err) { Map<String, String> fieldValidation = ValidatorUtil.validate((User)target, Default.class); fieldValidation.forEach( (k, v) -> err.rejectValue(k, v) ); } }
User
entity, a couple of special annotations have been added:@Entity @Table(name="users") public class User extends ProvidedId<String> implements UserDetails{ ... private String fullName; @NotNull @Size(min=4, max=30) private String email; @NotNull private String password; private boolean enabled = true; @NotNull @Enumerated(EnumType.STRING) private SupportedLanguage language; private String profileImg; @Column(name="not_expired") private boolean accountNonExpired; @Column(name="not_locked") private boolean accountNonLocked; @NotNull @Enumerated(EnumType.STRING) private SupportedCurrency currency; private BigDecimal balance; ... }
ValidatorUtil
class to make those validations easier and to reduce the amount of boilerplate code:package edu.zipcloud.cloudstreetmarket.core.util; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import javax.validation.groups.Default; import org.springframework.validation.BindingResult; public class ValidatorUtil { private static Validator validator; static { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); }
The following validate
method allows us to call for a JSR validation from whichever location that may require it:
public static <T> Map<String, String> validate(T object, Class<?>... groups) { Class<?>[] args = Arrays.copyOf(groups, groups.length + 1); args[groups.length] = Default.class; return extractViolations(validator.validate(object, args)); } private static <T> Map<String, String> extractViolations(Set<ConstraintViolation<T>> violations) { Map<String, String> errors = new HashMap<>(); for (ConstraintViolation<T> v: violations) { errors.put(v.getPropertyPath().toString(), "["+v.getPropertyPath().toString()+"] " + StringUtils.capitalize(v.getMessage())); } return errors; }
The following raiseFirstError
method is not of a specific standard, it is our way of rendering to the client the server side errors:
public static void raiseFirstError(BindingResult result) { if (result.hasErrors()) { throw new IllegalArgumentException(result.getAllErrors().get(0).getCode()); } else if (result.hasGlobalErrors()) { throw new IllegalArgumentException(result.getGlobalError().getDefaultMessage()); } } }
RestExceptionHandler
is still configured to handle IllegalArgumentExceptions
, rendering them with ErrorInfo
formatted responses:@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Autowired private ResourceBundleService bundle; @Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { ErrorInfo errorInfo = null; if(body!=null && bundle.containsKey(body.toString())){ String key = body.toString(); String localizedMessage = bundle.get(key); errorInfo = new ErrorInfo(ex, localizedMessage, key, status); } else{ errorInfo = new ErrorInfo(ex, (body!=null)? body.toString() : null, null, status); } return new ResponseEntity<Object>(errorInfo, headers, status); } @ExceptionHandler({ InvalidDataAccessApiUsageException.class, DataAccessException.class, IllegalArgumentException.class }) protected ResponseEntity<Object> handleConflict(final RuntimeException ex, final WebRequest request) { return handleExceptionInternal(ex, I18N_API_GENERIC_REQUEST_PARAMS_NOT_VALID, new HttpHeaders(), BAD_REQUEST, request); } }
ErrorInfo
object in the HTTP response:{"error":"[email] Size must be between 4 and 30", "message":"The request parameters were not valid!", "i18nKey":"error.api.generic.provided.request.parameters.not.valid", "status":400, "date":"2016-01-05 05:59:26.584"}
accountController
(in account_management.js
) is instantiated with a dependency to a custom errorHandler
factory. The code is as follows:cloudStreetMarketApp.controller('accountController', function ($scope, $translate, $location, errorHandler, accountManagementFactory, httpAuth, genericAPIFactory){ $scope.form = { id: "", email: "", fullName: "", password: "", language: "EN", currency: "", profileImg: "img/anon.png" }; ... }
accountController
has an update
method that invokes the errorHandler.renderOnForm
method:$scope.update = function () { $scope.formSubmitted = true; if(!$scope.updateAccount.$valid) { return; } httpAuth.put('/api/users', JSON.stringify($scope.form)).success( function(data, status, headers, config) { httpAuth.setCredentials($scope.form.id, $scope.form.password); $scope.updateSuccess = true; } ).error(function(data, status, headers, config) { $scope.updateFail = true; $scope.updateSuccess = false; $scope.serverErrorMessage = errorHandler.renderOnForms(data); } ); };
errorHandler
is defined as follows in main_menu.js
. It has the capability to pull translations messages from i18n
codes:cloudStreetMarketApp.factory("errorHandler", ['$translate', function ($translate) { return { render: function (data) { if(data.message && data.message.length > 0){ return data.message; } else if(!data.message && data.i18nKey && data.i18nKey.length > 0){ return $translate(data.i18nKey); } return $translate("error.api.generic.internal"); }, renderOnForms: function (data) { if(data.error && data.error.length > 0){ return data.error; } else if(data.message && data.message.length > 0){ return data.message; } else if(!data.message && data.i18nKey && data.i18nKey.length > 0){ return $translate(data.i18nKey); } return $translate("error.api.generic.internal"); } } }]);
ValidationUtils
:@Component public class TransactionValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Transaction.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "quote", " transaction.quote.empty"); ValidationUtils.rejectIfEmpty(errors, "user", " transaction.user.empty"); ValidationUtils.rejectIfEmpty(errors, "type", " transaction.type.empty"); } }
Spring offers a Validator
interface (org.sfw.validation.Validator
) for creating components to be injected or instantiated in the layer we want. Therefore, Spring validation components can be used in Spring MVC Controllers. The Validator
interface is the following:
public interface Validator { boolean supports(Class<?> clazz); void validate(Object target, Errors errors); }
The supports(Class<?> clazz)
method is used to assess the domain of a Validator
implementation, and also to restrict its use to a specific Type or super-Type.
The validate(Object target, Errors errors)
method imposes its standard so that the validation logic of the validator lives in this place. The passed target
object is assessed, and the result of the validation is stored in an instance of the org.springframework.validation.Errors
interface. A partial preview of the Errors
interface is shown here:
public interface Errors { ... void reject(String errorCode); void reject(String errorCode, String defaultMessage); void reject(String errorCode, Object[] errorArgs, String defaultMessage); void rejectValue(String field, String errorCode); void rejectValue(String field, String errorCode, String defaultMessage); void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage); void addAllErrors(Errors errors); boolean hasErrors(); int getErrorCount(); List<ObjectError> getAllErrors(); ... }
Using Spring MVC, we have the possibility to bind and trigger a Validator
to a specific method-handler. The framework looks for a validator instance bound to the incoming request. We have configured such a binding in our recipe at the fourth step:
@InitBinder protected void initBinder(WebDataBinder binder) { binder.setValidator(new UserValidator()); }
The Binders
(org.springframework.validation.DataBinder
) allow setting property values onto a target object. Binders also provide support for validation and binding-results analysis.
The DataBinder.validate()
method is called after each binding step and this method calls the validate
of the primary validator attached to the DataBinder
.
The binding-process populates a result object, which is an instance of the org.springframework.validation.BindingResult
interface. This result object can be retrieved using the DataBinder.getBindingResult()
method.
Actually, a BindingResult
implementation is also an Errors
implementation (as shown here). We have presented the Errors
interface earlier. Check out the following code:
public interface BindingResult extends Errors { Object getTarget(); Map<String, Object> getModel(); Object getRawFieldValue(String field); PropertyEditor findEditor(String field, Class<?> valueType); PropertyEditorRegistry getPropertyEditorRegistry(); void addError(ObjectError error); String[] resolveMessageCodes(String errorCode); String[] resolveMessageCodes(String errorCode, String field); void recordSuppressedField(String field); String[] getSuppressedFields(); }
The whole design can be summarized as follows:
We create a validator implementation. When an incoming request comes in for a specific Controller method handler, the request payload is converted into the class that is targeted by the @RequestBody
annotation (an Entity
in our case). An instance of our validator implementation is bound to the injected @RequestBody
object. If the injected @RequestBody
object is defined with a @Valid
annotation, the framework asks DataBinder
to validate the object on each binding step and to store errors in the BindingResultobject
of DataBinder
.
Finally, this BindingResult
object is injected as argument of the method handler, so we can decide what to do with its errors (if any). During the binding process, missing fields and property access exceptions are converted into FieldErrors
. These FieldErrors
are also stored into the Errors instance. The following error codes are used for FieldErrors
:
Missing field error: "required" Type mismatch error: "typeMismatch" Method invocation error: "methodInvocation"
When it is necessary to return nicer error messages for the user, a MessageSource
helps us to process a lookup and retrieve the right localized message from a MessageSourceResolvable
implementation with the following method:
MessageSource.getMessage(org.sfw.context.MessageSourceResolvable, java.util.Locale).
The ValodationUtils
utility class (org.sfw.validation.ValidationUtils
) provides a couple of convenient static methods for invoking validators and rejecting empty fields. These utility methods allow one-line assertions that also handle at the same time, the population of the Errors
objects. In this recipe, the 14th step details our TransactionValidator
that makes use of ValidationUtils
.
The next recipe will focus on internationalization of errors and content. However, let's see how we catch our errors from the controllers and how we display them. The update
method of UserController
has this custom method call on its first line:
ValidatorUtil.raiseFirstError(result);
We created the ValidatorUtil
support class for our needs; the idea was to throw an IllegalArgumentException
for any type of error that can be detected by our validator. The ValidatorUtil.raiseFirstError(result)
method call can also be found in the TransactionController.update(…)
method-handler. This method-handler relies on the TransactionValidator
presented in the 14th step.
If you remember this TransactionValidator
, it creates an error with a transaction.quote.empty
message code when a quote object is not present in the financial Transaction object. An IllegalArgumentException
is then thrown with the transaction.quote.empty
message detail.
In the next recipe, we will revisit how a proper internationalized JSON response is built and sent back to the client from an IllegalArgumentException
.
Spring Framework version 4 and above supports bean validation 1.0 (JSR-303) and bean validation 1.1 (JSR-349). It also adapts this bean validation to work with the Validator
interface, and it allows the creation of class-level validators using annotations.
The two specifications, JSR-303 and JSR-349, define a set of constraints applicable to beans, as annotations from the javax.validation.constraints
package.
Generally, a big advantage of using the code from specifications instead of the code from implementations is that we don't have to know which implementation is used. Also, the implementation can always potentially be replaced with another one.
Bean validation was originally designed for persistent beans. Even if the specification has a relatively low coupling to JPA, the reference implementation stays Hibernate validator. Having a persistence provider that supports those validation specifications is definitely an advantage. Now with JPA2, the persistent provider automatically calls for JSR-303 validation before persisting. Ensuring such validations from two different layers (controller and model) raises our confidence level.
We defined the @NotNull
and @Size
JSR-303 annotations on the presented User
entity. There are obviously more than two annotations to be found in the specification.
Here's a table summarizing the package of annotations (javax.validation.constraints
) in JEE7:
Bean validation implementations can also go beyond the specification and offer their set of extra validation annotations. Hibernate validator has a few interesting ones such as @NotBlank
, @SafeHtml
, @ScriptAssert
, @CreditCardNumber
, @Email
, and so on. These are all listed from the hibernate documentation accessible at the following URL
http://docs.jboss.org/hibernate/validator/4.3/reference/en-US/html_single/#table-custom-constraints
We have defined the following validator bean in our Spring context:
<bean id="validator" class="org.sfw.validation.beanvalidation.LocalValidatorFactoryBean"/>
This bean produces validator instances that implement JSR-303 and JSR-349. You can configure a specific provider class here. By default, Spring looks in the classpath for the Hibernate Validator JAR. Once this bean is defined, it can be injected wherever it is needed.
We have injected such validator instances in our UserValidator
and this makes it compliant with JSR-303 and JSR-349.
For internationalization, the validator produces its set of default message codes. These default message codes and values look like the following ones:
javax.validation.constraints.Max.message=must be less than or equal to {value} javax.validation.constraints.Min.message=must be greater than or equal to {value} javax.validation.constraints.Pattern.message=must match "{regexp}" javax.validation.constraints.Size.message=size must be between {min} and {max}
In this section we highlight a few validation concepts and components that we didn’t explain.
The ValidationUtils
Spring utility class provides convenient static methods for invoking a Validator
and rejecting empty fields populating the error object in one line:
We can couple constraints across more than one field to define a set of more advanced constraints:
http://beanvalidation.org/1.1/spec/#constraintdeclarationvalidationpr ocess-groupsequence
http://docs.jboss.org/hibernate/stable/validator/reference/en-US/ html_single/#chapter-groups
It can sometimes be useful to create a specific validator that has its own annotation. Check link, it should get us to:
http://howtodoinjava.com/2015/02/12/spring-mvc-custom-validator-example/
The best source of information remains the Spring reference on Validation
. Check link, it should get us to:
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html