基于oval注解支持JavaScript表达式约束条件

通常我们经常需要在接口中对DTO进行相关字段校验,我们可以采用传统的大量判断校验。目前我们采用使用基于oval(版本:1.90)注解约束,通过SpringAOP切面使用注解约束拦截。但是对于相关字段校验,如果字段跟其他字段有相关业务逻辑关系,,我们可以采用基于Oval javascript表达式约束,如果不依赖Mozilla Rhino,oval内部表达式引擎将会采用JSR223支持。

1、oval简介

oval是一个开源验证框架,基于注解约束,功能强大,使用简单。
具体使用API文档可以查看:http://oval.sourceforge.net/userguide.html

我们查看API文档可看到如下说明:

OVal requires Java 5 or later - mainly for annotation support but other new language features (generics, for each loop, etc.) are used across the OVal source code too. Java 5 is actually the only hard requirement, depending on the features you want to use additional libraries are required:

  • AspectJ is required if you want to use the above mentioned programming by contract features.
  • Apache Commons JEXL is required if you want to define constraints via JEXL expressions.
  • BeanShell is required if you want to define constraints via BeanShell expressions.
  • Groovy is required if you want to define constraints via Groovy expressions.
  • JRuby is required if you want to define constraints via Ruby expressions.
  • Mozilla Rhino is required if you want to define constraints via JavaScript expressions.
  • MVEL is required if you want to define constraints via MVEL expressions.
  • OGNL is required if you want to define constraints via OGNL expressions.
  • XStream is required if you want to configure OVal via XML configuration files.
  • GNU Trove is required if you want to have OVal to internally use the GNU Trove high performance collections.
  • Javolution is required if you want to have OVal to internally use Javolution’s high performance collections.
  • JXPath is required if you want to use JXPath expressions for the constraint target declarations.
  • JUnit and all other libraries are required if you want to run the test cases.

其中:Mozilla Rhino is required if you want to define constraints via JavaScript expressions.(翻译:如果你想要他通过JavaScript表达式定义约束,Mozilla Rhino是必须的)

2、场景再现

假设咱们满足这样一个业务需求,如果男性时(sex=1),年龄必须非空且大于0,否则提示“性别男性时,[年龄]不能为空且大于0”。

根据如上业务需求,我们可以采用@Check注解,我们查看API可以看到如下信息
The expr parameter holds the script to be evaluated. If the script returns true the constraint is satisfied. OVal provides two special variables:

  • _value - contains the value to validate (field value or getter return value)
  • _this - is a reference to the validated object

The lang parameter specifies the scripting language you want to use. In case the required libraries are loaded, OVal is aware of these languages:

  • bsh or beanshell for BeanShell,
  • groovy for Groovy,
  • jexl for JEXL,
  • js or javascript for JavaScript (via Mozilla Rhino),
  • mvel for MVEL,
  • ognl for OGNL,
  • ruby or jruby for Ruby (via JRuby)

Additional scripting languages can be registered via Validator.addExpressionLanguage(String, ExpressionLanguage).

这意味着我们可以采用javascript。

2.1、不引入Mozilla Rhino依赖

假设应用不引入Mozilla Rhino,代码示例如下:

import lombok.Data;
import net.sf.oval.Validator;
import net.sf.oval.constraint.Assert;

import java.util.Date;

/**
 * @description: 基于 oval 验证
 * @Date : 2018/10/8 下午6:07
 * @Author : 石冬冬-Seig Heil(dongdong.shi@mljr.com)
 */
@Data
public class Student {
    @Assert(expr="_value != null && _value > 0" ,lang="javascript",message="性别男性时,[年龄]不能为空且大于0"  ,when="javascript:_this.sex == 1")
    /**
     * 年龄
     */
    private Integer age;
    @Assert(expr="_value != null" ,lang="javascript",message="性别男性时,[入学日期]不能为空"  ,when="javascript:_this.sex == 1")
    /**
     * 入学日期
     */
    private Date enterDate;
    /**
     * 性别
     */
    private Integer sex;


    public static void main(String[] args) {
        Student student = new Student();
        student.setSex(1);
        student.setAge(0);
        //student.setEnterDate(new Date());
        Validator validator = new Validator();
        System.out.println(validator.validate(student).toString());
    }
}

然后运行结果

[net.sf.oval.ConstraintViolation: 性别男性时,[年龄]不能为空且大于0, net.sf.oval.ConstraintViolation: 性别男性时,[入学日期]不能为空]

这么来说,不引入Mozilla Rhino 依然是可以解析的,我们通过排查oval源码,查看@Assert注解的实现类,AssertCheck可以看到如下源码

 public boolean isSatisfied(final Object validatedObject, final Object valueToValidate, final OValContext context, final Validator validator)
            throws ExpressionEvaluationException, ExpressionLanguageNotAvailableException {
        final Map<String, Object> values = getCollectionFactory().createMap();
        values.put("_value", valueToValidate);
        values.put("_this", validatedObject);

        final ExpressionLanguage el = validator.getExpressionLanguageRegistry().getExpressionLanguage(lang);
        return el.evaluateAsBoolean(expr, values);
    }

这里有一个ExpressionLanguage,我们进而进入方法getExpressionLanguage
看到如下源码:

    /**
     *
     * @param languageId the id of the language, cannot be null
     *
     * @throws IllegalArgumentException if <code>languageName == null</code>
     * @throws ExpressionLanguageNotAvailableException
     */
    public ExpressionLanguage getExpressionLanguage(final String languageId) throws IllegalArgumentException, ExpressionLanguageNotAvailableException {
        Assert.argumentNotNull("languageId", languageId);

        ExpressionLanguage el = elcache.get(languageId);

        if (el == null)
            el = _initializeDefaultEL(languageId);

        if (el == null)
            throw new ExpressionLanguageNotAvailableException(languageId);

        return el;
    }

根据调试,进入_initializeDefaultEL方法,查看源码

private ExpressionLanguage _initializeDefaultEL(final String languageId) {
        // JavaScript support
        if (("javascript".equals(languageId) || "js".equals(languageId)) && ReflectionUtils.isClassPresent("org.mozilla.javascript.Context"))
            return registerExpressionLanguage("js", registerExpressionLanguage("javascript", new ExpressionLanguageJavaScriptImpl()));

        // Groovy support
        if ("groovy".equals(languageId) && ReflectionUtils.isClassPresent("groovy.lang.Binding"))
            return registerExpressionLanguage("groovy", new ExpressionLanguageGroovyImpl());

        // BeanShell support
        if (("beanshell".equals(languageId) || "bsh".equals(languageId)) && ReflectionUtils.isClassPresent("bsh.Interpreter"))
            return registerExpressionLanguage("beanshell", registerExpressionLanguage("bsh", new ExpressionLanguageBeanShellImpl()));

        // OGNL support
        if ("ognl".equals(languageId) && ReflectionUtils.isClassPresent("ognl.Ognl"))
            return registerExpressionLanguage("ognl", new ExpressionLanguageOGNLImpl());

        // MVEL2 support
        if ("mvel".equals(languageId) && ReflectionUtils.isClassPresent("org.mvel2.MVEL"))
            return registerExpressionLanguage("mvel", new ExpressionLanguageMVELImpl());

        // JRuby support
        else if (("jruby".equals(languageId) || "ruby".equals(languageId)) && ReflectionUtils.isClassPresent("org.jruby.Ruby"))
            return registerExpressionLanguage("jruby", registerExpressionLanguage("ruby", new ExpressionLanguageJRubyImpl()));

        // JEXL2 support
        if ("jexl".equals(languageId) && ReflectionUtils.isClassPresent("org.apache.commons.jexl2.JexlEngine"))
            return registerExpressionLanguage("jexl", new ExpressionLanguageJEXLImpl());

        // scripting support via JSR223
        if (ReflectionUtils.isClassPresent("javax.script.ScriptEngineManager")) {
            final ExpressionLanguage el = ExpressionLanguageScriptEngineImpl.get(languageId);
            if (el != null)
                return registerExpressionLanguage(languageId, el);
        }

        return null;
    }

这时,我们的languageId是javascript,我们可以看到第一个if条件判断,不成立,为啥呢,进一步看到一句

ReflectionUtils.isClassPresent("org.mozilla.javascript.Context")

内部源码:

 public static boolean isClassPresent(final String className) {
        try {
            Class.forName(className);
            return true;
        } catch (final ClassNotFoundException ex) {
            return false;
        }
    }

这意味着,通过反射查找相关表达式引擎类,如果没有引入Mozilla Rhino,则会走如下逻辑

// scripting support via JSR223
        if (ReflectionUtils.isClassPresent("javax.script.ScriptEngineManager")) {
            final ExpressionLanguage el = ExpressionLanguageScriptEngineImpl.get(languageId);
            if (el != null)
                return registerExpressionLanguage(languageId, el);
        }
2.2、JSR233

JSR233是什么呢?

JDK1.6开始,Java引入了JSR223 ,就是可以用一致的形式在JVM上执行一些脚本语言,如js脚本。
那么jsr223究竟是个什么东西?说通俗了,就是为各脚本引擎提供了统一的接口、统一的访问模式。在jsr223出现以前,如jdk1.4中,就已经可以直接调用js引擎(可从http://www.mozilla.org/rhino/下载)来运行js脚本了,只不过调用的时候是与js引擎的实现息息相关的,jsr223的引入,调用者与具体脚本引擎解耦了,调用类里,不再需要引入具体引擎相关的类。如果有两个脚本引擎所支持的语言语法相似,还可以直接更换scriptEngineManager.getEngineByName(“javascript”)中的引擎名称,以方便的切换引擎。

来个JSR代码示例

/**
 * @description: Script测试
 * @Date : 2018/10/9 下午2:21
 * @Author : 石冬冬-Seig Heil(dongdong.shi@mljr.com)
 */
public class ScriptTest {
    @Test
    public void script(){
        try {
            ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
            ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
            System.out.println("2*6-(6+5) 结果:"+scriptEngine.eval("2*6-(6+5)"));
            System.out.println("1>2?true:false 结果:"+scriptEngine.eval("1>2?true:false"));
            scriptEngine.put("a","1");
            scriptEngine.put("b","5");
            scriptEngine.put("c","2");
            System.out.println("(a >b || c < b) ?  (a + b) : (a - b) 结果:"+scriptEngine.eval("(a >b || c < b) ?  (a + b) : (a - b)"));
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
}

运行结果

2*6-(6+5) 结果:1
1>2?true:false 结果:false
(a >b || c < b) ? (a + b) : (a - b) 结果:15

2.3、引入Mozilla Rhino

Rhino 是一个纯 Java 的开源的 JavaScript 实现,rhino是使用java代码实现的javascript解释器,它实现了javascript的核心,符合Ecma-262标准,支持javascript标准的所有特性。

Rhino 提供了如下功能

  • 对 JavaScript 1.5 的完全支持
  • 直接在 Java 中使用 JavaScript 的功能
  • 一个 JavaScript shell 用于运行 JavaScript 脚本
  • 一个 JavaScript 的编译器,用于将 JavaScript 编译成 Java 二进制文件

官网:
http://www.mozilla.org/rhino/
http://www.mozilla.org/rhino/ScriptingJava.html

maven添加如下依赖

        <dependency>
            <groupId>org.mozilla</groupId>
            <artifactId>rhino</artifactId>
            <version>1.7R4</version>
        </dependency>

引入后,则会走如下逻辑

// JavaScript support
        if (("javascript".equals(languageId) || "js".equals(languageId)) && ReflectionUtils.isClassPresent("org.mozilla.javascript.Context"))
            return registerExpressionLanguage("js", registerExpressionLanguage("javascript", new ExpressionLanguageJavaScriptImpl()));

ExpressionLanguageJavaScriptImpl源码如下

/**
 * @author Sebastian Thomschke
 */
public class ExpressionLanguageJavaScriptImpl extends AbstractExpressionLanguage {
    private static final Log LOG = Log.getLog(ExpressionLanguageJavaScriptImpl.class);

    private final Scriptable parentScope;

    private final ObjectCache<String, Script> scriptCache = new ObjectCache<String, Script>();

    public ExpressionLanguageJavaScriptImpl() {
        final Context ctx = ContextFactory.getGlobal().enterContext();
        try {
            parentScope = ctx.initStandardObjects();
        } finally {
            Context.exit();
        }
    }

    public Object evaluate(final String expression, final Map<String, ?> values) throws ExpressionEvaluationException {
        LOG.debug("Evaluating JavaScript expression: {1}", expression);
        try {
            final Context ctx = ContextFactory.getGlobal().enterContext();
            Script script = scriptCache.get(expression);
            if (script == null) {
                ctx.setOptimizationLevel(9);
                script = ctx.compileString(expression, "<cmd>", 1, null);
                scriptCache.put(expression, script);
            }
            final Scriptable scope = ctx.newObject(parentScope);
            scope.setPrototype(parentScope);
            scope.setParentScope(null);
            for (final Entry<String, ?> entry : values.entrySet()) {
                scope.put(entry.getKey(), scope, Context.javaToJS(entry.getValue(), scope));
            }
            return script.exec(ctx, scope);
        } catch (final EvaluatorException ex) {
            throw new ExpressionEvaluationException("Evaluating JavaScript expression failed: " + expression, ex);
        } finally {
            Context.exit();
        }
    }
}

3、最佳实践

目前我们采用SpringAOP切面,拦截相关方法,获取DTO注解解析约束规则,并返回校验信息源码如下:

/**
 * @description: 基于oval的注解的DTO约束校验
 * <pre>
 *     如果基于oval,需要支持javascript表达式,需要引入
 *     <!-- https://mvnrepository.com/artifact/org.mozilla/rhino -->
 *     <dependency>
 *         <groupId>org.mozilla</groupId>
 *         <artifactId>rhino</artifactId>
 *         <version>1.7R4</version>
 *     </dependency>
 * </pre>
 * @Date : 2018/6/2 下午2:32
 * @Author : 石冬冬-Seig Heil(dongdong.shi@mljr.com)
 */
@Aspect
@Component
public class OvalValidatorAdvice {

    private Logger log = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private Validator validator;

    @Pointcut("@annotation(com.mljr.annotation.OvalValidator)")
    public void validator() {
    }

    @Around("validator()")
    public Object intercept(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        OvalValidator annotation = method.getAnnotation(OvalValidator.class);
        if (annotation == null) {
            return joinPoint.proceed();
        }
        String module = annotation.value();
        String action = "";
        action = annotation.action().value();
        if(StringTools.isNotEmpty(action)){
            action = MessageFormat.format(",action={0}",action);
        }
        Class<?> targetClass = joinPoint.getTarget().getClass();
        String className = targetClass.getName();
        String target = className + "." + method.getName();
        Object args[] = joinPoint.getArgs();
        log.info("[OvalValidator Request]{}{},target={},dto={}",module,action,target,JSON.toJSON(args));
        if (args == null || args.length == 0) {
            log.warn("该方法缺少校验参数 ");
            return joinPoint.proceed();
        }
        List<ConstraintViolation> violations = validator.validate(args[0]);
        if (CollectionsTools.isNotEmpty(violations)) {
            return Result.fail(RemoteEnum.ERROR_WITH_EMPTY_PARAM.getIndex(), violations.get(0).getMessage());
        }
        return joinPoint.proceed();
    }
}

其中,@OvalValidator是个自定义注解

/**
 * @Description 基于oval的注解的DTO约束校验注解
 * @Date : 2018/6/2 下午2:32
 * @Author : 石冬冬-Seig Heil(dongdong.shi@mljr.com)
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface OvalValidator {
    /**
     * 模块名称
     * 请注意,如果不定义请使用module值
     * @return
     */
    String value() default "未知模块";
    /**
     * 动作
     * @return
     */
    Action action() default @Action();
}

oval的Validator 该对象纳入Spring Bean容器管理

/**
 * @description: 基于Oval注解约束配置
 * @Date : 2018/10/8 下午7:30
 * @Author : 石冬冬-Seig Heil(dongdong.shi@mljr.com)
 */
@Configuration
@Slf4j
public class OvalValidatorConfig {

    @Bean
    public Validator ovalValidatorInit(){
        Validator validator = new Validator();
        try {
            validator.getExpressionLanguageRegistry().registerExpressionLanguage("javascript",new ExpressionLanguageJavaScriptImpl());
        } catch (Exception e) {
            log.warn("OvalValidatorConfig focus an warning={}",e.getMessage());
        }finally {
            log.info("OvalValidatorConfig initializing done.");
        }
        return validator;
    }
}

归纳:如果Validator注册ExpressionLanguageJavaScriptImpl该表达式实现类,意味着需要引入Mozilla Rhino。不引入该依赖时,采用JSR223内置解析表达式。对于大量DTO业务校验,我们采用validator纳入SpringBean容器管理初始化,无需每次实例创建,引擎表达式注册。

参考文章:
http://oval.sourceforge.net/userguide.html

下面的是我的公众号二维码图片,欢迎关注。
秋夜无霜

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值