9

我有一个 java bean 用于将 JSON 消息发送到 spring @RestController,并且我有 bean 验证设置并且使用@Valid. 但我想转移到 Protobuf/Thrift 并远离 REST。它是一个内部 API,许多大公司已经在内部取消了 REST。这真正意味着我不再控制消息对象——它们是在外部生成的。我不能再给它们添加注释了。

所以现在我的验证必须是程序化的。我该怎么做呢?我已经编写了一个代码Validator,它工作得很好。但它不使用 nice@Valid注释。我必须执行以下操作:

@Service
public StuffEndpoint implements StuffThriftDef.Iface {

    @Autowired
    private MyValidator myValidator;

    public void things(MyMessage msg) throws BindException {
        BindingResult errors = new BeanPropertyBindingResult(msg, msg.getClass().getName());
        errors = myValidator.validate(msg);
        if (errors.hasErrors()) {
            throw new BindException(errors);
        } else {
            doRealWork();
        }
    }
}

这很臭。我必须在每一种方法中都这样做。现在,我可以将其中的很多内容放入一个抛出的方法中BindException,这样就可以将一行代码添加到每个方法中。但这仍然不是很好。

我想要的是看到它看起来像这样:

@Service
@Validated
public StuffEndpoint implements StuffThriftDef.Iface {

    public void things(@Valid MyMessage msg) {
        doRealWork();
    }
}

并且仍然得到相同的结果。请记住,我的 bean 没有注释。是的,我知道我可以@InitBinder在方法上使用注释。但这仅适用于 Web 请求。

我不介意将正确的Validator注入此类,但如果我的 ValidatorFactory 可以根据该supports()方法提取正确的,我更愿意。

这可能吗?有没有办法将 bean 验证配置为实际使用 Spring 验证?我必须在某个地方劫持一个方面吗?侵入LocalValidatorFactoryMethodValidationPostProcessor

谢谢。

4

3 回答 3

16

将 Spring 验证和 JSR-303 约束结合起来是相当复杂的事情。并且没有“准备使用”的方式。主要的不便之处在于 Spring 验证使用BindingResult,而 JSR-303 使用ConstraintValidatorContext作为验证的结果。

您可以尝试使用 Spring AOP 制作自己的验证引擎。让我们考虑一下,我们需要为此做些什么。首先,声明 AOP 依赖项(如果你还没有):

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.2.4.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjrt</artifactId>
   <version>1.8.8</version>
   <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.8.8</version>
</dependency>

我正在使用 Spring of version 4.2.4.RELEASE,但当然你可以使用自己的。使用方面注释需要 AspectJ。下一步,我们必须创建简单的验证器注册表:

public class CustomValidatorRegistry {

    private List<Validator> validatorList = new ArrayList<>();

    public void addValidator(Validator validator){
        validatorList.add(validator);
    }

    public List<Validator> getValidatorsForObject(Object o) {
        List<Validator> result = new ArrayList<>();
        for(Validator validator : validatorList){
            if(validator.supports(o.getClass())){
                result.add(validator);
            }
        }
        return result;
    }
}

如您所见,它是一个非常简单的类,它允许我们找到对象的验证器。现在让我们创建注释,这将是标记方法,需要验证:

package com.mydomain.validation;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomValidation {
}

由于标准BindingException类不是RuntimeException,我们不能在重写方法中使用它。这意味着我们需要定义自己的异常:

public class CustomValidatorException extends RuntimeException {

    private BindingResult bindingResult;

    public CustomValidatorException(BindingResult bindingResult){
        this.bindingResult = bindingResult;
    }

    public BindingResult getBindingResult() {
        return bindingResult;
    }
}

现在我们准备创建一个可以完成大部分工作的切面。CustomValidationAspect 将在标有注释的方法之前执行:

@Aspect
@Component
public class CustomValidatingAspect {

    @Autowired
    private CustomValidatorRegistry registry; //aspect will use our validator registry


    @Before(value = "execution(public * *(..)) && annotation(com.mydomain.validation.CustomValidation)")
    public void doBefore(JoinPoint point){
        Annotation[][] paramAnnotations  =
                ((MethodSignature)point.getSignature()).getMethod().getParameterAnnotations();
        for(int i=0; i<paramAnnotations.length; i++){
            for(Annotation annotation : paramAnnotations[i]){
                //checking for standard org.springframework.validation.annotation.Validated
                if(annotation.annotationType() == Validated.class){
                    Object arg = point.getArgs()[i];
                    if(arg==null) continue;
                    validate(arg);
                }
            }
        }
    }

    private void validate(Object arg) {
        List<Validator> validatorList = registry.getValidatorsForObject(arg);
        for(Validator validator : validatorList){
            BindingResult errors = new BeanPropertyBindingResult(arg, arg.getClass().getSimpleName());
            validator.validate(arg, errors);
            if(errors.hasErrors()){
                throw new CustomValidatorException(errors);
            }
        }
    }
}

execution(public * *(..)) && @annotation(com.springapp.mvc.validators.CustomValidation)意味着,这方面将应用于任何带有@CustomValidation注释的 bean 的公共方法。另请注意,为了标记经过验证的参数,我们使用标准org.springframework.validation.annotation.Validated注释。但我们当然可以定制。我认为方面的其他代码非常简单,不需要任何注释。示例验证器的更多代码:

public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class<?> aClass) {
        return aClass==Person.class;
    }

    @Override
    public void validate(Object o, Errors errors) {
        Person person = (Person)o;
        if(person.getAge()<=0){
            errors.rejectValue("age", "Age is too small");
        }
    }
}

现在我们已经调整配置并准备好使用:

@Configuration
@ComponentScan(basePackages = "com.mydomain")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig{

    .....

    @Bean
    public CustomValidatorRegistry validatorRegistry(){
        CustomValidatorRegistry registry = new CustomValidatorRegistry();
        registry.addValidator(new PersonValidator());
        return registry;
    }    
}

注意,proxyTargetClasstrue因为我们将使用cglib类代理。


服务类中的目标方法示例:

@Service
public class PersonService{

    @CustomValidation
    public void savePerson(@Validated Person person){        
       ....
    }

}

由于@CustomValidation注释方面将被应用,并且由于@Validated注释person将被验证。以及在控制器(或任何其他类)中使用服务的示例:

@Controller
public class PersonConroller{

    @Autowired
    private PersonService service;

    public String savePerson(@ModelAttribute Person person, ModelMap model){
        try{
            service.savePerson(person);
        }catch(CustomValidatorException e){
            model.addAttribute("errors", e.getBindingResult());
            return "viewname";
        }
        return "viewname";
    }

}

请记住,如果您@CustomValidation从类的方法中调用PersonService,验证将不起作用。因为它会调用原始类的方法,而不是代理。这意味着,如果您希望验证正常工作(例如),您只能从类外部(从其他类)调用此方法@Transactional works same way

对不起,长篇大论。我的回答不是关于“简单的声明方式”,您可能不需要它。但我很好奇解决这个问题。

于 2016-03-19T12:42:07.883 回答
4

我将@Ken 的答案标记为正确,因为它是正确的。但我更进一步,想发布我所做的。我希望任何来到这个页面的人都会觉得它很有趣。我可能会尝试将它放在 Spring 人员面前,看看它是否会包含在未来的版本中。

这个想法是有一个新的注释来替换@Valid. 所以我叫它@SpringValid。使用此注释将启动上面组合的系统。以下是所有部分:

SpringValid.java

package org.springframework.validation.annotation;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface SpringValid {

}

SpringValidationAspect.java

package org.springframework.validation;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

@Aspect
@Component
public class SpringValidationAspect {

  private SpringValidatorRegistry springValidatorRegistry;

  @Autowired
  public SpringValidationAspect(final SpringValidatorRegistry springValidatorRegistry) {
    this.springValidatorRegistry = springValidatorRegistry;
  }

  public SpringValidatorRegistry getSpringValidatorRegistry() {
    return springValidatorRegistry;
  }

  @Before("@target(org.springframework.validation.annotation.Validated) "
      + "&& execution(public * *(@org.springframework.validation.annotation.SpringValid (*), ..)) "
      + "&& args(validationTarget)")
  public void beforeMethodThatNeedsValidation(Object validationTarget) {
    validate(validationTarget);
  }

  private void validate(Object arg) {
    List<Validator> validatorList = springValidatorRegistry.getValidatorsForObject(arg);
    for (Validator validator : validatorList) {
      BindingResult errors = new BeanPropertyBindingResult(arg, arg.getClass().getSimpleName());
      validator.validate(arg, errors);
      if (errors.hasErrors()) {
        throw new SpringValidationException(errors);
      }
    }
  }
}

Spring 的示例显示了带有注释的类,@Validated因此我想保留它。上述方面仅针对具有@Validated类级别的类。而且,就像您使用 时一样@Valid,它会查找@SpringValid贴在方法参数上的注释。

SpringValidationException.java

package org.springframework.validation;

import org.springframework.validation.BindingResult;

public class SpringValidationException extends RuntimeException {

  private static final long serialVersionUID = 1L;

  private BindingResult bindingResult;

  public SpringValidationException(final BindingResult bindingResult) {
    this.bindingResult = bindingResult;
  }

  public BindingResult getBindingResult() {
    return bindingResult;
  }
}

SpringValidatorRegistry.java

package org.springframework.validation;

import org.springframework.validation.Validator;

import java.util.ArrayList;
import java.util.List;

public class SpringValidatorRegistry {

  private List<Validator> validatorList = new ArrayList<>();

  public void addValidator(Validator validator) {
    validatorList.add(validator);
  }

  public List<Validator> getValidatorsForObject(Object o) {
    List<Validator> result = new ArrayList<>();
    for (Validator validator : validatorList) {
      if (validator.supports(o.getClass())) {
        result.add(validator);
      }
    }
    return result;
  }
}

就像第一个答案一样,一个注册所有实现 Springorg.springframework.validation.Validator接口的类的地方。

SpringValidator.java

package org.springframework.validation.annotation;

import org.springframework.stereotype.Component;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface SpringValidator {

}

这只是额外的调味料,使注册/查找更容易Validators。您可以Validators手动注册所有内容,也可以通过反射找到它们。所以这部分不是必需的,我只是认为它让事情变得更容易。

MyConfig.java

package com.example.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.SpringValidationAspect;
import org.springframework.validation.SpringValidatorRegistry;
import org.springframework.validation.annotation.SpringValidator;

import java.util.Map;

import javax.validation.Validator;

@Configuration
public class MyConfig {

  @Autowired
  private ApplicationContext applicationContext;

  @Bean
  public SpringValidatorRegistry validatorRegistry() {
    SpringValidatorRegistry registry = new SpringValidatorRegistry();
    Map<String, Object> validators =
        applicationContext.getBeansWithAnnotation(SpringValidator.class);
    validators.values()
        .forEach(v -> registry.addValidator((org.springframework.validation.Validator) v));
    return registry;
  }

  @Bean
  public SpringValidationAspect springValidationAspect() {
    return new SpringValidationAspect(validatorRegistry());
  }
}

查看,扫描您的类路径并查找@SpringValidator类并注册它们。然后注册 Aspect 并离开。

以下是此类验证器的示例: MyMessageValidator.java

package com.example.validators;

import com.example.messages.MyMessage;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.SpringValidator;

@SpringValidator
public class MyMessageValidator implements Validator {

  @Override
  public boolean supports(Class<?> clazz) {
    return MyMessage.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "firstField", "{javax.validation.constraints.NotNull}",
    "firstField cannot be null");
    MyMessage obj = (MyMessage) target;
    if (obj.getSecondField != null && obj.getSecondField > 100) {
      errors.rejectField(errors, "secondField", "{javax.validation.constraints.Max}", "secondField is too big");
    }
  }
}

这是使用@SpringValid注解的服务类:

我的服务.java

package com.example.services;

import com.example.messages.MyMessage;

import org.springframework.validation.annotation.SpringValid;
import org.springframework.validation.annotation.Validated;

import javax.inject.Inject;

@Validated
public class MyService {

  public String doIt(@SpringValid final MyMessage msg) {
    return "we did it!";
  }
}

希望这在某些时候对某人有意义。个人觉得还是蛮好用的。许多公司开始将其内部 API 从 REST 转移到 Protobuf 或 Thrift 之类的东西上。您仍然可以使用 Bean Validation,但必须使用 XML,而且它并不是那么好。所以我希望这对仍然想进行程序验证的人有所帮助。

于 2016-03-23T02:41:38.170 回答
2

希望它可以帮助某人。我通过添加以下配置使其工作:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration
public class ValidatorConfiguration {

    @Bean
    public MethodValidationPostProcessor getMethodValidationPostProcessor(){
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
         processor.setValidator(this.validator());
         return processor;
     }

     @Bean
     public LocalValidatorFactoryBean validator(){
         return new LocalValidatorFactoryBean();
     }

 }

然后以相同的方式对服务进行注解(@Validated 在类上,@Valid 在参数上),并且可以注入到另一个 bean 中,在该 bean 中可以直接调用方法并进行验证。

于 2017-01-04T19:31:05.797 回答