Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

Bean ValidationでSpring Expression Languageを使って相関バリデーションする

このエントリで実装を見ていたとき、思いついた。 jyukutyo.hatenablog.com

Spring Expression Language(SpEL)でもバリデーションできそうだな〜と。Spring、Hibernate Validatorの利用が前提になってしまうけど。

@SpringELAssertアノテーションというのを作り、expressionの値にバリデーション処理を記述する感じ。

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

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ CONSTRUCTOR, METHOD })
@Retention(RUNTIME)
@Constraint(validatedBy = SpringELAssertValidator.class)
public @interface SpringELAssert {

    String message() default "{}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    String expression();

    /**
     * Defines several {@link SpringELAssert} annotations on the same executable.
     */
    @Target({ CONSTRUCTOR, METHOD })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        SpringELAssert[] value();
    }

}

このアノテーションは@ParameterScriptAssertと内容は同じです。で、バリデーション処理はSpringELAssertValidatorクラスに記述する。

import java.util.List;
import java.util.Map;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;

import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class SpringELAssertValidator implements ConstraintValidator<SpringELAssert, Object[]> {

    private Expression expression;

    @Override
    public void initialize(SpringELAssert constraintAnnotation) {
        ExpressionParser parser = new SpelExpressionParser();
        this.expression = parser.parseExpression(constraintAnnotation.expression());
    }

    @Override
    public boolean isValid(Object[] arguments, ConstraintValidatorContext context) {

        List<String> parameterNames = ((ConstraintValidatorContextImpl)context).getMethodParameterNames();

        Map<String, Object> bindings = getBindings(arguments, parameterNames );

        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
        evaluationContext.setVariables(bindings);

        Boolean result = this.expression.getValue(evaluationContext, Boolean.class);
        return result == null ? true : result.booleanValue();
    }

    private Map<String, Object> getBindings(Object[] arguments, List<String> parameterNames) {
        Map<String, Object> bindings = new HashMap()<>;

        for ( int i = 0; i < arguments.length; i++ ) {
            bindings.put( parameterNames.get( i ), arguments[i] );
        }

        return bindings;
    }
}

これもParameterScriptAssertValidatorクラスの内容を少し書き換えた。引数名とバリデーションする値をMapにできるので、このMapをSpELのコンテキストにvariableとしてセットする。expressionではvariableは"#name"で参照できる。

Variables can be referenced in the expression using the syntax #variableName. Variables are set using the method setVariable on the StandardEvaluationContext.

これで完成。コントローラはこうなる。

import java.util.Date;

import jp.furyu.voyager.newsp.spring.validation.SpringELAssert;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@Validated
public class HogeController {

    @RequestMapping(path = "hoge")
    @ResponseBody
    @SpringELAssert(expression = "#start.before(#end)")
    public String index(@RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date start, @RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date end) {
        return "hoge";
    }

}

@ParameterScriptAssertとほぼ変わらないけど、SpELで書けるのでここでいきなりJavaScriptを持ち出してくるよりはいいかな?