私は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
をハックしますか?
ありがとう。
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;
}
}
_
proxyTargetClass
はtrue
です。これは、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
_)は、このメソッドをクラスの外部(他のクラス)からのみ呼び出すことができます。
長い投稿でごめんなさい。私の答えは「簡単な宣言的な方法」についてではなく、あなたはそれを必要としない可能性があります。しかし、私はこの問題を解決することに興味がありました。
正しいので、@ 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を使用する必要がありますが、それだけではありません。したがって、これがプログラムによる検証を引き続き実行したい人々にとって役立つことを願っています。
それが誰かを助けることを願っています。次の設定を追加することで機能します。
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に注入できます。