Poison

OGNL

最近业务有需求根据配置的表达式获取对应 Java Bean 中的字段值,开始的想法是自己写一个基于反射获取字段的工具类,然后再加上缓存,而后又想起 MyBatis 中的表达式解析,底层使用的 OGNL 库,于是查询了相关文档,决定直接使用 OGNL 库实现该需求,不再重复造轮子。

如果查看 MyBatis 的 pom.xml,可以发现含有如下声明:

1
2
3
4
5
6
7
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.2.20</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>

同时在 pom.xml 中还使用了 maven-shade-plugin 对 OGNL 的相关类进行重定位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<artifactSet>
<includes>
<include>org.mybatis:mybatis</include>
<include>ognl:ognl</include>
<include>org.javassist:javassist</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>ognl</pattern>
<shadedPattern>org.apache.ibatis.ognl</shadedPattern>
</relocation>
<relocation>
<pattern>javassist</pattern>
<shadedPattern>org.apache.ibatis.javassist</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>

pom.xml 的源码可以参见:pom.xml,关于重定位类,我之前编写 Spark 应用也用到过,参见:Relocating Classes

关于 OGNL 在 MyBatis 中的应用,主要是使用表达式获取相应对象中的值,代码位于 OgnlCache.java,比较简单,主要就是调用 OGNL 库实现,然后缓存了解析的表达式,这里就不贴了。我们主要看看 OGNL 底层的代码,此处记录最常见的获取对象字段值的实现,从最核心的 getValue 方法开始,源码位于 Ognl.java at master:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Evaluates the given OGNL expression tree to extract a value from the given root object. The default context is
* set for the given context and root via <code>addDefaultContext()</code>.
*
* @param tree the OGNL expression tree to evaluate, as returned by parseExpression()
* @param context the naming context for the evaluation
* @param root the root object for the OGNL expression
* @return the result of evaluating the expression
* @throws MethodFailedException if the expression called a method which failed
* @throws NoSuchPropertyException if the expression referred to a nonexistent property
* @throws InappropriateExpressionException if the expression can't be used in this context
* @throws OgnlException if there is a pathological environmental problem
*/
public static <T> T getValue( Object tree, Map<String, Object> context, Object root )
throws OgnlException
{
return Ognl.<T> getValue( tree, context, root, null );
}

如果是获取对象字段值,跟随源码,会调用至 ObjectPropertyAccessor 类的 getPossibleProperty 方法,源码位于 ObjectPropertyAccessor.java at master:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Returns OgnlRuntime.NotFound if the property does not exist.
*/
public Object getPossibleProperty( Map<String, Object> context, Object target, String name )
throws OgnlException
{
Object result;
OgnlContext ognlContext = (OgnlContext) context;

try
{
result = OgnlRuntime.getMethodValue( ognlContext, target, name, true );
if ( result == OgnlRuntime.NotFound )
{
result = OgnlRuntime.getFieldValue( ognlContext, target, name, true );
}
} catch ( OgnlException ex )
{
throw ex;
} catch ( Exception ex )
{
throw new OgnlException( name, ex );
}

return result;
}

可以看出,先尝试获取方法值,获取不到再获取字段值,其中获取方法是获取以 get 开头与属性名匹配的且参数个数为 0 的方法,获取字段是直接获取与属性名匹配的字段,中途解析了类的方法信息或字段信息后会将这些信息进行缓存,避免后续重复解析,最后调用反射对象的获取值方法获取的字段的值,该实现方式和之前预想的相同,附上相关源码 OgnlRuntime.java at master:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Method getGetMethod( OgnlContext unused, Class<?> targetClass, String propertyName )
throws IntrospectionException, OgnlException
{
Method result = null;

List<Method> methods = getDeclaredMethods( targetClass, propertyName, false /* find 'get' methods */ );

if ( methods != null )
{
for ( Method method : methods )
{
Class<?>[] mParameterTypes = findParameterTypes( targetClass, method ); // getParameterTypes(method);

if ( mParameterTypes.length == 0 )
{
result = method;
break;
}
}
}

return result;
}

OgnlRuntime.java at master:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static Object getFieldValue( OgnlContext context, Object target, String propertyName,
boolean checkAccessAndExistence )
throws NoSuchFieldException
{
Object result = null;
Class<?> targetClass = target == null ? null : target.getClass();
Field field = getField( targetClass, propertyName );

if ( checkAccessAndExistence && (( field == null ) || !context.getMemberAccess().isAccessible( context, target, field, propertyName )) )
{
result = NotFound;
}
if ( result == null )
{
if ( field == null )
{
throw new NoSuchFieldException( propertyName );
}
try
{
Object state;

if ( Modifier.isStatic( field.getModifiers() ) ) {
throw new NoSuchFieldException( propertyName );
}
state = context.getMemberAccess().setup( context, target, field, propertyName );
result = field.get( target );
context.getMemberAccess().restore( context, target, field, propertyName, state );

}
catch ( IllegalAccessException ex )
{
throw new NoSuchFieldException( propertyName );
}
}
return result;
}

中途通过反射解析方法和字段时会用到 OgnlCache.java at master,以避免重复解析类。

Reference

OGNL - Apache Commons OGNL - Object Graph Navigation Library