Java 将 @Validated 和 @Valid 与 spring 验证器一起使用

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/36089676/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 17:26:10  来源:igfitidea点击:

Use @Validated and @Valid with spring validator

javaspringvalidationspring-validator

提问by sbzoom

I have a java bean being used to send JSON messages to a spring @RestControllerand I have bean validation setup and running just fine using @Valid. But I want to move to Protobuf/Thrift and move away from REST. It is an internal API and a lot of big companies have done away with REST internally. What this really means is that I no longer have control of the message objects - they are generated externally. I can't put annotations on them anymore.

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

So now my validation has to be programmatic. How do I do this? I have coded up a Validatorand it works just great. But it doesn't use the nice @Validannotation. I have to do the following:

所以现在我的验证必须是程序化的。我该怎么做呢?我已经编码了一个Validator,它工作得很好。但它没有使用漂亮的@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();
        }
    }
}

This stinks. I have to do this in every single method. Now, I can put a lot of that into one method that throws BindExceptionand that makes it one line of code to add to every method. But that's still not great.

这很臭。我必须在每一种方法中都这样做。现在,我可以把很多东西放到一个抛出的方法中BindException,这使它成为一行代码添加到每个方法中。但这仍然不是很好。

What I want is to see it look like this:

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

@Service
@Validated
public StuffEndpoint implements StuffThriftDef.Iface {

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

And still get the same result. Remember, my bean has no annotations. And yes, I know I can use the @InitBinderannotation on a method. But that only works for web requests.

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

I don't mind injecting the correct Validatorinto this class, but I would prefer if my ValidatorFactory could pull the correct one based on the supports()method.

我不介意将正确的Validator注入到这个类中,但我希望我的 ValidatorFactory 可以根据supports()方法提取正确的。

Is this possible? Is there a way to configure bean validation to actually use Spring validation instead? Do I have to hiHyman a Aspect somewhere? Hack into the LocalValidatorFactoryor the MethodValidationPostProcessor?

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

Thanks.

谢谢。

采纳答案by Ken Bekov

Its pretty complicated thing to combine Spring validation and JSR-303 constrains. And there is no 'ready to use' way. The main inconvenience is that Spring validation uses BindingResult, and JSR-303 uses ConstraintValidatorContextas result of validation.

将 Spring 验证和 JSR-303 约束结合起来非常复杂。并且没有“随时可用”的方式。主要的不便在于 Spring 验证使用BindingResult,而 JSR-303 使用ConstraintValidatorContext作为验证结果。

You can try to make your own validation engine, using Spring AOP. Let's consider, what we need to do for it. First of all, declare AOP dependencies (if you didn't yet):

您可以尝试使用 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>

I'm using Spring of version 4.2.4.RELEASE, but of cause you can use your own. AspectJ needed for use aspect annotation. Next step, we have to create simple validator registry:

我使用的是 Spring 版本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;
    }
}

As you see it is very simple class, which allow us to find validator for object. Now lets create annotation, that will be mark methods, that need to be validated:

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

package com.mydomain.validation;

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

Because of standard BindingExceptionclass is not RuntimeException, we can't use it in overriden methods. This means we need define our own exception:

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

public class CustomValidatorException extends RuntimeException {

    private BindingResult bindingResult;

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

    public BindingResult getBindingResult() {
        return bindingResult;
    }
}

Now we are ready to create an aspect that will do most of the work. Aspect will execute before methods, which marked with CustomValidationannotation:

现在我们已经准备好创建一个可以完成大部分工作的切面。Aspect 将在方法之前执行,其中标有CustomValidation注解:

@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)means, that this aspect will applied to any public methods of beans, which marked with @CustomValidationannotation. Also note, that to mark validated parameters we are using standard org.springframework.validation.annotation.Validatedannotation. But of cause we could make our custom. I think other code of aspect is very simple and does not need any comments. Further code of example validator:

execution(public * *(..)) && @annotation(com.springapp.mvc.validators.CustomValidation)意味着,此方面将应用于 bean 的任何公共方法,这些方法带有@CustomValidation注解。另请注意,为了标记经过验证的参数,我们使用了标准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");
        }
    }
}

Now we have make tune the configuration and all ready to use:

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

@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;
    }    
}

Note, proxyTargetClassis truebecause we will use cglibclass proxy.

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



Example of target method in service class:

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

@Service
public class PersonService{

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

}

Because of @CustomValidationannotation aspect will be applied, and because of @Validatedannotation personwill be validated. And example of usage of service in controller(or any other class):

因为@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";
    }

}

Keep in mind, that if you will invoke @CustomValidationfrom methods of PersonServiceclass, validation will not work. Because it will invoke methods of original class, but not proxy. This means, that you can invoke this methods only from outside of class (from other classes), if you want validation to be working (eg @Transactional works same way).

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

Sorry for long post. My answer is not about 'simple declarative way', and possible you will do not need it. But I was curious resolve this problem.

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

回答by sbzoom

I marked @Ken's answer as correct because it is. But I have taken it a little further and wanted to post what I have made. I hope anybody coming to this page will find it interesting. I might try to get it in front of the Spring folks to see if it might be something included in future releases.

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

The idea is to have a new annotation to replace @Valid. So I called it @SpringValid. Using this annotation would kick off the system put together above. Here are all the pieces:

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

SpringValid.java

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

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's examples show classes annotated with @Validatedso I wanted to keep that. The above aspect only targets classes with @Validatedat the class-level. And, just like when you use @Valid, it looks for the @SpringValidannotation stuck to a method parameter.

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

SpringValidationException.java

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

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;
  }
}

Just like the first answer, a place to register all classes that implement Spring's org.springframework.validation.Validatorinterface.

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

SpringValidator.java

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 {

}

This is just extra sauce to make it easier to register/find Validators. You could register all your Validatorsby hand, or you could find them via reflection. So this part is not required, I just thought it made things easier.

这只是额外的调味料,可以更容易地注册/查找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());
  }
}

See, scan your classpath and look for @SpringValidatorclasses and register them. Then register the Aspect and away you go.

查看,扫描您的类路径并查找@SpringValidator类并注册它们。然后注册 Aspect 就可以了。

Here is an example of such a Validator: MyMessageValidator.java

以下是此类验证器的示例: 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");
    }
  }
}

And here is the service class that uses the @SpringValidannotation:

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

MyService.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!";
  }
}

Hope this makes sense for someone at some point. I personally think it is quite useful. A lot of companies are starting to move their internal APIs away from REST and to something like Protobuf or Thrift. You can still use Bean Validation but you have to use XML, and it isn't all that nice. So I hope this will be helpful to people who want to still do programmatic validation.

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

回答by Joel

Hope it helps someone. I've got it working by adding the following configuration:

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

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();
     }

 }

The service is then annotated the same way (@Validated on the class and @Valid on the parameter) and can be injected into another bean where the method can be called directly and validation happens.

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