通常我们经常需要在接口中对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
下面的是我的公众号二维码图片,欢迎关注。