Apache Commons JEXL是简洁的同样表达式语言,可以实现动态脚本。本文应用其实现一种需求————动态pojo验证。
对象验证是实际应用中非常普遍,且有很多种方式实现。但是通过JEXL,我们可以把验证规则存在在配置文件中或数据库表中,JEXL加载并实现运行时验证pojo。下面通过示例详细说明。
假设Person类有四个属性:SSN, firstName, lastName and birthYear,不同环境有两个不同验证规则:
(1)规则1:person对象在计算奖金福利时,需要 SSN != null and birthYear < 1990 。
(2)规则2:person对象在图书预定时,需要 firstName != null and lastName != null 。
Person 类定义如下:
@Setter
@Getter
@AllArgsConstructor
public class Person {
private String ssn;
private String firstName;
private String lastName;
private Integer birthYear;
}
Person类实例p接受上述两个验证规则,我们用4个表达式表示:
“p.ssn != null”, “p.birthYear < 1990”, “p.firstName != null”, and “lastName != null”
首先定义抽象类BaseValidator:
package com.dataz.validate;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author : jack
*/
@NoArgsConstructor
public abstract class BaseValidator {
protected List<String> rules;
public BaseValidator(List<String> rules) {
this.rules = rules;
}
/**
* JEXL expression based general object validation.
*
* @param obj to validate object
* @return Return null if no error found.
* Otherwise first error of the JEXL expression validation rules.
*/
protected abstract ValidationError validate(Object obj) ;
}
该类中定义了 List rules 属性,用于保存一组基于字符串的验证规则。ValidationError validate(Object obj) 方法基于特定jexlExprRules验证给定对象。ValidationError类简单保存错误消息,定义如下:
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
@AllArgsConstructor
public class ValidationError {
private String errCode;
private String errMsgs;
}
下面定义Person的验证类PersonObjectValidator,继承抽象类。
import com.dataz.pojp.Person;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.jexl2.Expression;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.MapContext;
import java.util.List;
/**
* @author : tommy
*/
@Log4j2
public class PersonObjectValidator extends BaseValidator {
public PersonObjectValidator(List<String> jexlExprRules) {
super(jexlExprRules);
}
@Override
public ValidationError validate(Object obj) {
Person person = (Person) obj;
JexlEngine jexl = new JexlEngine();
JexlContext context = new MapContext();
context.set("p", person);
for (String expr : rules) {
Expression e = jexl.createExpression(expr);
Boolean isValid = (Boolean) e.evaluate(context);
if (!isValid) {
log.error("{} validate fail on {}.", e.getExpression(), person);
return new ValidationError("personErr", e.getExpression());
}
}
return null;
}
}
validate(Object obj)验证方法中,我们首先转换参数为Person类型,然后初始化JexlContext 上下文。然后把person对象赋值给上下文中命名为p的key。
然后我们迭代验证规则列表,针对每个表达式规则评估p对象。如果有任何规则失败,停止并返回保存错误信息的ValidationError对象,否则返回null。
上述过程,Jexl验证需三个步骤:
1)基于JexlEngine对象创建基于Jexl语法字符串的Jexl表达式。
2)使用JexlContext对象保存验证对象(实际应用中可能有多个)。
3)使用步骤1中的表达式验证存储在上下文中的对象。
你可能对上面代码中这行有疑问:
Boolean isValid = (Boolean) e.evaluate(context);
因为我们定义的表达式总是返回boolean值。JEXL表达式可以返回任何对象————可以是Integer或特定领域对象等。
下面定义单元测试,验证上述需求。
import com.dataz.pojp.Person;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class PersonObjectValidatorTest {
private List<String> rule1;
private List<String> rule2;
private Person p;
@Before
public void init() {
// Rule 1
rule1 = new ArrayList<String>();
rule1.add("p.ssn != null");
rule1.add("p.birthYear < 1990");
// Rule 2
rule2 = new ArrayList<String>();
rule2.add("p.firstName != null");
rule2.add("p.lastName != null");
// Person object to be validated
p = new Person("123-45-1234","John", null, 1980);
}
// The person object is valid in one run-time application case
@Test
public void testValidPersonObjectInBonusPrizeDrawComponent() {
PersonObjectValidator validator = new PersonObjectValidator(rule1);
ValidationError err = validator.validate(p);
assertNull(err);
}
// The same person object is invalid in another run-time application case
@Test
public void testInValidPersonObjectInGuestbookSignupComponent() {
PersonObjectValidator validator = new PersonObjectValidator(rule2);
ValidationError err = validator.validate(p);
// The lastName is null to cause the person object being invalid
assertNotNull(err);
}
}
需要理解的是,JEXL使用java反射API,“p.firstName != null” 规则实际执行的语句为“p.getFirstName() != null”。实际应用中表达式可以包含任何对象方法调用。
为了让代码更简洁,项目使用了以下gradle依赖:
compile group: 'org.apache.commons', name: 'commons-jexl', version: '2.1.1'
compile group: 'org.projectlombok', name: 'lombok', version: '1.18.4'
testCompile group: 'junit', name: 'junit', version: '4.12'
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.11.1'
上述示例比较简单,仅演示了动态pojo验证。我们仅把验证规则定义在List中,实际中可以从配置文件或数据库中加载,从而实现动态验证pojo。我们也可以使用Map<String,String>保存验证规则,同时保存验证规则和错误消息,当验证失败时以更直接的方式显示错误消息,读者也可以扩展更丰富的应用。