Spring有一套Validation组件,并能与 Java EE 中的 Bean Validator组合使用。
我们在使用得比较多的是Spring MVC 中使用 @Valid 与 BingingResult方式组合使用。
在使用Dubbo或SpringCloud的作微服务架构流行的今日,对验证做了一些思考。
目标:一个服务中,验证在应用架构的service层次,service层次是对外的服务层次。
一个基于Spring Cloud的服务,将Spring MVC里Controller级别的验证方式转移到Service层次,同时将controller层次去掉,在Service层建立一个统一网关。这与Dubbo做PRC方式相似。
我们需要达到的目标代码样例:
@Validated
public class Service{
@Null public Result foo( @NotNull @Validated(Group1.class) Param p1, @NotNull @Validated(Group2.class) Param p2 ){
return ...;
}
}
Spring Validator的 @Validated 注解在 Service类的上标注,开启验证功能 。需要配置扩展:
1、默认不支持在方法参数上使用 @Validated的方式来验证参数类属性,即参数不支持BeanValidator的验证Bean。参考org.springframework.validation.beanvalidation.MethodValidationInterceptor
2、扩展BeanValidator相关注解的message相关i18n信息,配置ResourceBundleMessageSource
相关源码:https://gitee.com/jaffaXie/jaffa-framework
jaffa-framework-validation中
完成上面需求,项目与Spring boot整合。主要三个类:
ParamSupportMethodValidationInterceptor.java
修改invoke方法,扩展支持方法参数里使用@Validated验证Bean
/**
*
* @author jaffa
*
* 参考 org.springframework.validation.beanvalidation.MethodValidationInterceptor 实现
*
* 修改invoke方法,添加方法参数带 @Validated时的操作
*/
public class ParamSupportMethodValidationInterceptor implements MethodInterceptor
{
private final Validator validator;
public ParamSupportMethodValidationInterceptor()
{
this(Validation.buildDefaultValidatorFactory());
}
/**
* Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
* @param validatorFactory the JSR-303 ValidatorFactory to use
*/
public ParamSupportMethodValidationInterceptor(ValidatorFactory validatorFactory) {
this(validatorFactory.getValidator());
}
/**
* Create a new MethodValidationInterceptor using the given JSR-303 Validator.
* @param validator the JSR-303 Validator to use
*/
public ParamSupportMethodValidationInterceptor(Validator validator) {
this.validator = validator;
}
@Override
@SuppressWarnings("unchecked")
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
//-----------------------
// 此处扩展
//-----------------------
//校验同一个参数是否出现了 @Validated 跟 @Valid, 我们视这种情况为错误
checkParamValidAnnConflict(invocation);
// 处理了 方法 或 类 级别的验证
try {
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
//-----------------------
// 此处扩展
//-----------------------
//处理针对方法每个参数,有使用 @Validated({...class})时,进行校验里面的Bean,如果存在 @Valid则不再进行这个Bean的验证
Method handlerMethod = invocation.getMethod();
Class<?>[] paramTypes = handlerMethod.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++)
{
Object arg = invocation.getArguments()[i];
MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i);
Annotation[] paramAnns = methodParam.getParameterAnnotations();
/* 下面不进行处理 Valid,处理了 方法 或 类 级别的验证,已经进行Valid的引用验证 */
Validated validated = findAnn(paramAnns, Validated.class);
if(validated != null && arg != null){
Class<?>[] validatedGroups = validated.value();
Set<ConstraintViolation<Object>> violations = validator.validate(arg, validatedGroups);
if(!violations.isEmpty()){
throw new ConstraintViolationException(
handlerMethod.toString()+".arg"+i+",验证失败",
violations);
}
}
}
//------------------------
// 扩展结束
//------------------------
// 处理了 方法 或 类级别的验证
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
/**
* Determine the validation groups to validate against for the given method invocation.
* <p>Default are the validation groups as specified in the {@link Validated} annotation
* on the containing target class of the method.
* @param invocation the current MethodInvocation
* @return the applicable validation groups as a Class array
*/
protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
if (validatedAnn == null) {
validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
}
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
}
private void checkParamValidAnnConflict(MethodInvocation invocation)
{
Method handlerMethod = invocation.getMethod();
Class<?>[] paramTypes = handlerMethod.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++)
{
MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i);
Annotation[] paramAnns = methodParam.getParameterAnnotations();
if(isParamValidAnnConflict(paramAnns)){
throw new IllegalStateException(
"方法定义验证参数不能同时存在Validated跟Valid,推荐使用Validated,增加分组功能。"
+ "method="+handlerMethod+".arg"+i);
}
}
}
private boolean isParamValidAnnConflict(Annotation[] paramAnns)
{
return findAnn(paramAnns, Validated.class) != null
&& findAnn(paramAnns, Valid.class) != null;
}
private <T extends Annotation> T findAnn(Annotation[] paramAnns, Class<T> tagetType)
{
if(paramAnns == null || paramAnns.length==0){
return null;
}
for (Annotation paramAnn : paramAnns)
{
if(tagetType.isInstance(paramAnn)){
return (T) paramAnn;
}
}
return null;
}
}
ParamSupportMethodValidationPostProcessor.java
修改Advice返回,用于配置Bean到Spring容器
/**
* @author jaffa
* 参考 org.springframework.validation.beanvalidation.MethodValidationPostProcessor
*
* 修改
* 在 createMethodValidationAdvice 返回了 ParamSupportMethodValidationInterceptor
*/
public class ParamSupportMethodValidationPostProcessor extends MethodValidationPostProcessor
{
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
private Validator validator;
/**
* Set the 'validated' annotation type.
* The default validated annotation type is the {@link Validated} annotation.
* <p>This setter property exists so that developers can provide their own
* (non-Spring-specific) annotation type to indicate that a class is supposed
* to be validated in the sense of applying method validation.
* @param validatedAnnotationType the desired annotation type
*/
public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
this.validatedAnnotationType = validatedAnnotationType;
}
/**
* Set the JSR-303 Validator to delegate to for validating methods.
* <p>Default is the default ValidatorFactory's default Validator.
*/
public void setValidator(Validator validator) {
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else {
this.validator = validator;
}
}
/**
* Set the JSR-303 ValidatorFactory to delegate to for validating methods,
* using its default Validator.
* <p>Default is the default ValidatorFactory's default Validator.
* @see ValidatorFactory#getValidator()
*/
public void setValidatorFactory(ValidatorFactory validatorFactory) {
this.validator = validatorFactory.getValidator();
}
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
/**
* Create AOP advice for method validation purposes, to be applied
* with a pointcut for the specified 'validated' annotation.
* @param validator the JSR-303 Validator to delegate to
* @return the interceptor to use (typically, but not necessarily,
* a {@link MethodValidationInterceptor} or subclass thereof)
* @since 4.2
*/
protected Advice createMethodValidationAdvice(Validator validator) {
return (validator != null ? new ParamSupportMethodValidationInterceptor(validator) : new ParamSupportMethodValidationInterceptor());
}
}
JaffaHibernateBeanValidatorConfiguration.java
基于Spring Boot的Configuration类
/**
* @author jaffa
*/
@ConditionalOnProperty(
name = "jaffa.validation.spring.enabled",
havingValue = "true"
)
@Configuration
public class JaffaHibernateBeanValidatorConfiguration
{
@Value("${jaffa.validation.spring.basenames:ValidationMessages}")
private String[] basenames = new String[]{"ValidationMessages"};
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() throws Exception
{
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.setProviderClass(HibernateValidator.class);
localValidatorFactoryBean.setValidationMessageSource(validationResourceBundleMessageSource());
return localValidatorFactoryBean;
}
@Bean
public BeanValidationPostProcessor beanValidationPostProcessor(LocalValidatorFactoryBean localValidatorFactoryBean) throws Exception
{
BeanValidationPostProcessor beanValidationPostProcessor = new BeanValidationPostProcessor();
beanValidationPostProcessor.setValidator(localValidatorFactoryBean.getValidator());
return beanValidationPostProcessor;
}
@Bean
public ParamSupportMethodValidationPostProcessor methodValidationPostProcessor(LocalValidatorFactoryBean localValidatorFactoryBean) throws Exception
{
ParamSupportMethodValidationPostProcessor paramSupportMethodValidationPostProcessor = new ParamSupportMethodValidationPostProcessor();
paramSupportMethodValidationPostProcessor.setValidator(localValidatorFactoryBean.getValidator());
return paramSupportMethodValidationPostProcessor;
}
@Bean(name = "validationResourceBundleMessageSource")
@ConditionalOnMissingBean(name = "validationResourceBundleMessageSource")
public ResourceBundleMessageSource validationResourceBundleMessageSource() throws Exception
{
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames(this.basenames);
messageSource.setUseCodeAsDefaultMessage(false);
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(360);
return messageSource;
}
}
单元测试:
Account
package jaffa.framework.validation.spring.bean;
import jaffa.framework.validation.api.Create;
import jaffa.framework.validation.api.Update;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class Account {
@NotBlank(groups = Create.class, message = "{Account.username.NotBlank}")
private String username;
@NotBlank(groups = {Create.class, Update.class}, message = "{Account.password.NotBlank}")
@Size(min = 2, max = 10, message = "{Account.password.Size}")
private String password;
@NotBlank(groups = {Create.class, Update.class}, message = "{Account.nickname.NotBlank}")
private String nickname;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
}
AccountService
/**
* 作为微服务,Service层直接映射 Spring MVC相关
*/
@Service
@Validated
@RequestMapping("/account")
public class AccountService {
@PostMapping("/create")
public Account create(@NotNull(message = "{Account.NotNull}") @Validated(Create.class) Account account)
{
System.out.println("create account succeed.");
return account;
}
@PutMapping("/update")
public Account update(@NotNull(message = "{Account.NotNull}") @Validated(Update.class) Account account)
{
System.out.println("update account succeed");
return account;
}
}
测试类:
@SpringBootTest(classes = TestingApplication.class)
@RunWith(SpringRunner.class)
public class ValidationTest {
@Autowired
private AccountService accountService;
@Test
public void testCreateParamNull() throws Exception
{
try {
accountService.create(null);
Assert.fail();
}catch (ConstraintViolationException e){
printViolation(e.getConstraintViolations());
}
}
@Test
public void testCreateAccountBeanValidated() throws Exception
{
try{
Account account = new Account();
accountService.create(account);
Assert.fail();
}catch (ConstraintViolationException e){
// group Create
Assert.assertEquals(new Integer(3), new Integer(e.getConstraintViolations().size()));
printViolation(e.getConstraintViolations());
}
}
@Test
public void testUpdateAccountBeanValidated() throws Exception
{
try{
Account account = new Account();
accountService.update(account);
Assert.fail();
}catch (ConstraintViolationException e){
// group Update
Assert.assertEquals(new Integer(2), new Integer(e.getConstraintViolations().size()));
printViolation(e.getConstraintViolations());
}
}
private void printViolation(Set<ConstraintViolation<?>> violations){
violations.forEach(v->{
System.out.println(v.getPropertyPath()+":"+v.getMessage()+":"+v.getInvalidValue());
});
}
}