web-dev-qa-db-ja.com

Spring Validatorで@Validatedおよび@Validを使用する

私はJava Beanを使用してJSONメッセージを春に送信しています_@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をスローする1つのメソッドにその多くを入れることができ、すべてのメソッドに追加する1行のコードになります。しかし、それはまだ素晴らしいことではありません。

私が欲しいのはそれがこのように見えるのを見ることです:

_@Service
@Validated
public StuffEndpoint implements StuffThriftDef.Iface {

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

それでも同じ結果が得られます。私のBeanには注釈がありません。そして、はい、メソッドで_@InitBinder_アノテーションを使用できることを知っています。ただし、これはWebリクエストでのみ機能します。

このクラスに正しいValidatorを挿入してもかまいませんが、ValidatorFactoryがsupports()メソッドに基づいて正しいものをプルできるとよいでしょう。

これは可能ですか?代わりに実際にSpring検証を使用するようにBean検証を構成する方法はありますか?アスペクトをどこかにハイジャックする必要がありますか? LocalValidatorFactoryまたはMethodValidationPostProcessorをハックしますか?

ありがとう。

7
sbzoom

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>
_

私はバージョン_4.2.4.RELEASE_のSpringを使用していますが、独自のバージョンを使用できます。 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;
    }
}
_

これで、ほとんどの作業を行うアスペクトを作成する準備ができました。アスペクトは、メソッドの前に実行され、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)は、このアスペクトが_@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";
    }

}
_

PersonServiceクラスのメソッドから_@CustomValidation_を呼び出す場合、検証が機能しないことに注意してください。元のクラスのメソッドを呼び出しますが、プロキシは呼び出しません。つまり、検証を機能させたい場合(例:_@Transactional works same way_)は、このメソッドをクラスの外部(他のクラス)からのみ呼び出すことができます。

長い投稿でごめんなさい。私の答えは「簡単な宣言的な方法」についてではなく、あなたはそれを必要としない可能性があります。しかし、私はこの問題を解決することに興味がありました。

13
Ken Bekov

正しいので、@ Kenの答えを正しいとマークしました。しかし、私はそれをもう少し取り、私が作ったものを投稿したいと思いました。このページにアクセスしてくださった方に興味を持っていただければ幸いです。将来のリリースに含まれるものかどうかを確認するために、春の人たちの前でそれを試してみるかもしれません。

アイデアは、@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;
  }
}

最初の回答と同様に、Springのorg.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クラスを探して登録します。次に、アスペクトを登録して、先に進みます。

このようなバリデーターの例を以下に示します。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アノテーションを使用するサービスクラスです。

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

これが誰かにとってある時点で理にかなっているといいのですが。個人的にはかなり重宝すると思います。多くの企業が内部APIをRESTからProtobufやThriftのようなものに移行しています。引き続きBean Validationを使用できますが、XMLを使用する必要がありますが、それだけではありません。したがって、これがプログラムによる検証を引き続き実行したい人々にとって役立つことを願っています。

3
sbzoom

それが誰かを助けることを願っています。次の設定を追加することで機能します。

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に注入できます。

2
Joel