上一篇博文介绍了ongl的基础语法,接下来进入实际的使用篇,我们将结合一些实际的case,来演示ognl究竟可以支撑到什么地步
在看本文之前,强烈建议先熟悉一下什么是ognl,以及其语法特点,减少阅读障碍,五分钟入门系列: 191129-Ognl 语法基础教程
I. 基本使用
1. 配置
我们选用的是java开发环境,使用maven来进行包管理,首先在pom文件中添加依赖
<!-- https://mvnrepository.com/artifact/ognl/ognl --> <dependency> <groupId>ognl</groupId> <artifactId>ognl</artifactId> <version>3.2.11</version> </dependency> 复制代码
2. 基础使用
对于Ognl的使用,关键的地方在于获取OgnlContext
, 在这个上下文中保存一些实例用来支撑ognl的语法
所以一般使用ognl的先前操作就是创建OgnlContext
,然后将我们的实例扔到上下文中,接收ognl表达式,最后执行并获取结果
伪代码如下
// 构建一个OgnlContext对象 OgnlContext context = (OgnlContext) Ognl.createDefaultContext(this, new DefaultMemberAccess(true), new DefaultClassResolver(), new DefaultTypeConverter()); // 设置根节点,以及初始化一些实例对象 context.setRoot(this); context.put("实例名", obj); ... // ognl表达式执行 Object expression = Ognl.parseExpression("#a.name") Object result = Ognl.getValue(expression, context, context.getRoot()); 复制代码
II. 实例演示
接下来进入实例演示,首先我们需要创建两个测试对象,用于填充OgnlContext
0. 准备
两个普通对象,一个静态类
@Data @NoArgsConstructor @AllArgsConstructor public class ADemo { private String name; private Integer age; } @Data public class PrintDemo { private String prefix; private ADemo aDemo; public void sayHello(String name, int age) { System.out.println("name: " + name + " age: " + age); } private void print(ADemo a) { System.out.println(prefix + " => " + a); } public <T> T print(String str, Class<T> clz) { T obj = JSON.parseObject(str, clz); System.out.println("class: " + obj); return obj; } public void print(String str, String clz) { System.out.println("str2a: " + str + " clz: " + clz); } public void print(String str, OgnlEnum ognlEnum) { System.out.println("enum: " + str + ":" + ognlEnum); } public void print(String str, ADemo a) { System.out.println("obj: " + str + ":" + a); } public void show(Class clz) { System.out.println(clz.getName()); } } public class StaticDemo { private static int num = (int) (Math.random() * 100); public static int showDemo(int a) { System.out.println("static show demo: " + a); return a; } } public enum OgnlEnum { CONSOLE, FILE; } 复制代码
上面在创建OgnlContext时,有一个DefaultMemberAccess
类,主要用于设置成员的访问权限,需要自己实现
@Setter public class DefaultMemberAccess implements MemberAccess { private boolean allowPrivateAccess = false; private boolean allowProtectedAccess = false; private boolean allowPackageProtectedAccess = false; public DefaultMemberAccess(boolean allowAllAccess) { this(allowAllAccess, allowAllAccess, allowAllAccess); } public DefaultMemberAccess(boolean allowPrivateAccess, boolean allowProtectedAccess, boolean allowPackageProtectedAccess) { super(); this.allowPrivateAccess = allowPrivateAccess; this.allowProtectedAccess = allowProtectedAccess; this.allowPackageProtectedAccess = allowPackageProtectedAccess; } @Override public Object setup(Map context, Object target, Member member, String propertyName) { Object result = null; if (isAccessible(context, target, member, propertyName)) { AccessibleObject accessible = (AccessibleObject) member; if (!accessible.isAccessible()) { result = Boolean.TRUE; accessible.setAccessible(true); } } return result; } @Override public void restore(Map context, Object target, Member member, String propertyName, Object state) { if (state != null) { ((AccessibleObject) member).setAccessible((Boolean) state); } } /** * Returns true if the given member is accessible or can be made accessible by this object. */ @Override public boolean isAccessible(Map context, Object target, Member member, String propertyName) { int modifiers = member.getModifiers(); if (Modifier.isPublic(modifiers)) { return true; } else if (Modifier.isPrivate(modifiers)) { return this.allowPrivateAccess; } else if (Modifier.isProtected(modifiers)) { return this.allowProtectedAccess; } else { return this.allowPackageProtectedAccess; } } } 复制代码
接下来创建我们的OgnlContext对象
ADemo a = new ADemo(); a.setName("yihui"); a.setAge(10); PrintDemo print = new PrintDemo(); print.setPrefix("ognl"); print.setADemo(a); // 构建一个OgnlContext对象 // 扩展,支持传入class类型的参数 OgnlContext context = (OgnlContext) Ognl.createDefaultContext(this, new DefaultMemberAccess(true), new DefaultClassResolver(), new DefaultTypeConverter()); context.setRoot(print); context.put("print", print); context.put("a", a); 复制代码
到此,我们的前置准备已经就绪,接下来进入实际case篇
1. 实例访问
我们的实例访问分为两类,分别为实例的方法调用;实例的属性访问
a. 实例方法调用
比如我们希望执行 print的sayHello
方法,可以如下使用
Object ans = Ognl.getValue(Ognl.parseExpression("#print.sayHello(\"一灰灰blog\", 18)"), context, context.getRoot()); System.out.println("实例方法执行: " + ans); 复制代码
关键点在ognl表达式: #print.sayHello("一灰灰blog", 18)
,其中print为实例
名,对应的构建OgnlContext对象之后执行的context.put("print", print);
这一行代码
输出结果:
name: 一灰灰blog age: 18 实例方法执行: null 复制代码
b. 实例成员属性访问
成员属性的访问可以划分为直径获取成员属性值和设置成员属性值,对此可以如下使用
ans = Ognl.getValue(Ognl.parseExpression("#a.name=\"一灰灰Blog\""), context, context.getRoot()); System.out.println("实例属性设置: " + ans); ans = Ognl.getValue(Ognl.parseExpression("#a.name"), context, context.getRoot()); System.out.println("实例属性访问: " + ans); 复制代码
输出结果
实例属性设置: 一灰灰Blog 实例属性访问: 一灰灰Blog 复制代码
看到上面这个,自然会想到一个问题,可不可以访问父类的私有成员呢?
为了验证这个问题,我们新建一个实例继承自ADemo,并注册到 OgnlContext
上下文
@Data public class BDemo extends ADemo { private String address; } // 注册到ognlContext BDemo b = new BDemo(); b.setName("b name"); b.setAge(20); b.setAddress("测试ing"); context.put("b", b); // 测试case ans = Ognl.getValue(Ognl.parseExpression("#b.name"), context, context.getRoot()); System.out.println("实例父类属性访问:" + ans); 复制代码
输出结果如下
实例父类属性访问:b name 复制代码
注意:
我们这里可以直接访问私有成员,访问私有方法,访问父类的私有成员,这些都得益于我们自定义的DefaultMemberAccess
,并制定了访问策略为true(即私有、保护、默认访问权限的都可以访问)
2. 静态类访问
实例成员,需要先注册到OgnlContext之后才能根据实例名来访问,但是静态类则不需要如此,默认支持当前的ClassLoader加载的所有静态类的访问姿势;下面我们进入实例演示
a. 静态类方法调用
静态类的访问需要注意的是需要传入全路径,用@
开头,类与方法之间也是用@
进行分割
ans = Ognl.getValue(Ognl.parseExpression("@git.hui.fix.test.ognl.bean.StaticDemo@showDemo(20)"), context, context.getRoot()); System.out.println("静态类方法执行:" + ans); 复制代码
输出结果
static show demo: 20 复制代码
a. 静态类成员访问
同样我们分为成员访问和修改
ans = Ognl.getValue(Ognl.parseExpression("@git.hui.fix.test.ognl.bean.StaticDemo@num"), context, context.getRoot()); System.out.println("静态类成员访问:" + ans); ans = Ognl.getValue(Ognl.parseExpression("@git.hui.fix.test.ognl.bean.StaticDemo@num=1314"), context, context.getRoot()); System.out.println("静态类成员设置:" + ans); 复制代码
输出结果如下
静态类方法执行:20 ognl.InappropriateExpressionException: Inappropriate OGNL expression: @git.hui.fix.test.ognl.bean.StaticDemo@num at ognl.SimpleNode.setValueBody(SimpleNode.java:312) at ognl.SimpleNode.evaluateSetValueBody(SimpleNode.java:220) at ognl.SimpleNode.setValue(SimpleNode.java:301) at ognl.ASTAssign.getValueBody(ASTAssign.java:53) at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) 复制代码
直接设置静态变量,抛出了移仓,提示InappropriateExpressionException
那么静态类的成员可以修改么?这里先留一个疑问
3. 特殊传参
一般的java操作,无外乎方法调用,属性访问两种,接下来我们聚焦在方法的调用上;如果一个方法接收的参数是一些基本类型的对象,使用起来还比较简单;但是其他的场景呢?
a. class类型参数
如我们前面的PrintDemo中,有一个方法如下
public <T> T print(String str, Class<T> clz) { T obj = JSON.parseObject(str, clz); System.out.println("class: " + obj); return obj; } 复制代码
如需调用上面的方法,clz参数可以怎么处理呢?
ans = Ognl.getValue(Ognl.parseExpression( "#print.print(\"{'name':'xx', 'age': 20}\", @git.hui.fix.test.ognl.bean.ADemo@class)"), context, context.getRoot()); System.out.println("class 参数方法执行:" + ans); // class传参 ans = Ognl.getValue(Ognl.parseExpression("#print.print(\"{'name':'haha', 'age': 10}\", #a.getClass())"), context, context.getRoot()); System.out.println("class 参数方法执行:" + ans); 复制代码
上面给出了两种方式,一个是根据已有的对象获取class,一个是直接根据静态类获取class,输出结果如下
class: ADemo(name=xx, age=20) class 参数方法执行:ADemo(name=xx, age=20) class: ADemo(name=haha, age=10) class 参数方法执行:ADemo(name=haha, age=10) 复制代码
b. 枚举参数
如PrintDemo中的方法, 其中第二个参数为枚举
public void print(String str, OgnlEnum ognlEnum) { System.out.println("enum: " + str + ":" + ognlEnum); } 复制代码
结合上面的使用姿势,这个也不太难
ans = Ognl.getValue( Ognl.parseExpression("#print.print(\"print enum\", @git.hui.fix.test.ognl.model.OgnlEnum@CONSOLE)"), context, context.getRoot()); System.out.println("枚举参数方法执行:" + ans); 复制代码
输出结果
enum: print enum:CONSOLE 枚举参数方法执行:null 复制代码
c. null传参
目标方法如下
private void print(ADemo a) { System.out.println(prefix + " => " + a); } 复制代码
因为我们需要传参为空对象,稍微有点特殊,ognl针对这个进行了支持,传参直接填null即可
ans = Ognl.getValue(Ognl.parseExpression("#print.print(null)"), context, context.getRoot()); System.out.println("null 传参:" + ans); 复制代码
输出如下
ognl => null null 传参:null 复制代码
然后一个问题来了,在PrintDemo中的print方法,有多个重载的case,那么两个参数都传null,具体是哪个方法会被执行呢?
public <T> T print(String str, Class<T> clz) { T obj = JSON.parseObject(str, clz); System.out.println("class: " + obj); return obj; } public void print(String str, String clz) { System.out.println("str2a: " + str + " clz: " + clz); } public void print(String str, OgnlEnum ognlEnum) { System.out.println("enum: " + str + ":" + ognlEnum); } public void print(String str, ADemo a) { System.out.println("obj: " + str + ":" + a); } 复制代码
通过实际的测试,第三个方法被调用了,这里面难道有啥潜规则么,然而我并没有找到
ans = Ognl.getValue(Ognl.parseExpression("#print.print(null, null)"), context, context.getRoot()); System.out.println("null 传参:" + ans); 复制代码
输出
enum: null:null null 传参:null 复制代码
d. 对象传递
传参是一个POJO对象,这个时候咋整?
public void print(String str, ADemo a) { System.out.println("obj: " + str + ":" + a); } 复制代码
现在的问题主要集中在如何构建一个Aemo对象,当做参数丢进去,通过前面的语法篇我们知道ognl是支持new来创建对象的, 如果ADemo恰好提供了全属性的构造方法,那么可以如下操作
ex = Ognl.parseExpression("#print.print(\"对象构建\", new git.hui.fix.test.ognl.bean.ADemo(\"test\", 20))"); Object ans = Ognl.getValue(ex, context, context.getRoot()); System.out.println("对象传参:" + ans); 复制代码
注意观察上面的ognl表达式,其中重点在new
git.hui.fix.test.ognl.bean.ADemo("test", 20))
,创建对象的时候,请指定全路径名
输出结果
obj: 对象构建:ADemo(name=test, age=20) 对象传参:null 复制代码
上面这个虽然实现了我们的case,但是有局限性,如果这个POJO没有全属性的构造方法,又可以怎么整?
这里就需要借助ognl语法中的链式语句了,通过new创建对象,然后设置属性,最后抛出对象
ex = Ognl.parseExpression("#print.print(\"对象构建\", (#demo=new git.hui.fix.test.ognl.bean.ADemo(), #demo.setName(\"一灰灰\"), #demo))"); ans = Ognl.getValue(ex, context, context.getRoot()); System.out.println("对象传参:" + ans); 复制代码
核心语句在(#demo=new git.hui.fix.test.ognl.bean.ADemo(), #demo.setName(\"一灰灰\"), #demo)
,创建对象,设置属性
输出结果
obj: 对象构建:ADemo(name=一灰灰, age=null) 对象传参:null 复制代码
虽说上面实现了我们的需求场景,但是这里有个坑,我们创建的这个属性会丢到OgnlContext上下文中,所以这种操作非常有可能导致我们自己创建的临时对象覆盖了原有的对象
那么有什么方法可以避免么?
这个问题先攒着,后面再叙说
e. 容器传参
在PrintDemo对象中添加方法
public void print(List<Integer> args) { System.out.println(args); } public void print(Map<String, Integer> args) { System.out.println(args); } 复制代码
然后我们的访问case如下
ex = Ognl.parseExpression("#print.print({1, 3, 5})"); ans = Ognl.getValue(ex, context, context.getRoot()); System.out.println("List传参:" + ans); ex = Ognl.parseExpression("#print.print(#{\"A\": 1, \"b\": 3, \"c\": 5})"); ans = Ognl.getValue(ex, context, context.getRoot()); System.out.println("Map传参:" + ans); 复制代码
输出结果
[1, 3, 5] List传参:null {A=1, b=3, c=5} Map传参:null 复制代码
4. 表达式执行
接下来属于另外一个范畴的case了,执行一些简单的算术操作or条件表达式
ans = Ognl.getValue(Ognl.parseExpression("1 + 3 + 4"), context, context.getRoot()); System.out.println("表达式执行: " + ans); // 阶乘 ans = Ognl.getValue(Ognl.parseExpression("#fact = :[#this<=1? 1 : #this*#fact(#this-1)], #fact(3)"), context, context.getRoot()); System.out.println("lambda执行: " + ans); 复制代码
输出
表达式执行: 8 lambda执行: 6 复制代码
III. 小结
鉴于篇幅过长,本篇博文将只限于使用基础的ognl能支持到什么地步,在java中使用ognl套路比较简单
1. 创建OgnlContext,并注册实例
// 构建一个OgnlContext对象 OgnlContext context = (OgnlContext) Ognl.createDefaultContext(this, new DefaultMemberAccess(true), new DefaultClassResolver(), new DefaultTypeConverter()); // 设置根节点,以及初始化一些实例对象 context.setRoot(this); context.put("实例名", obj); ... 复制代码
2. 编译ognl表达式,并获取执行结果
// ognl表达式执行 Object expression = Ognl.parseExpression("#a.name") Object result = Ognl.getValue(expression, context, context.getRoot()); 复制代码
3. 遗留
博文中遗留了两个问题尚未解答
- 静态成员默认场景下不能修改,那么有办法让它支持修改么
- 方法传参,传递对象时,通过链式创建临时对象时会缓存在OgnlContext上下文中,如何避免这种场景?