Spel字段解析功能源码浅析
起因
最近在写日志功能,aop + 注解 + spel,使用 @RestControllerAdvice 全局捕获到的Exception的异常信息肯定是需要打印的,于是我想当然获取自定义信息的spel表达式是 e.getDetailMessage,detailMessage异常信息字段是异常继承于Throwable的,然后运行代码spel解析出现异常,网上搜一圈无果,只好自己debug看源码,找找原因了。
Debuging
题外话,学会debug技巧应该是我们初级后端进阶的第一步,学会debug才好看源码,Evaluate Expression 执行表达式,断点设置条件以及是否线程,这两个小技巧掌握好就可以看源码了。
readProperty
debug步进直到spel判断核心
org.springframework.expression.spel.ast.Indexer#readProperty 关键代码节选
1 | for (PropertyAccessor accessor : accessorsToTry) { |
canRead这个方法是校验的关键,这里如果校验不通过就不会返回结果,再往后走都是抛异常了,我们进canRead看看
canRead
PropertyAccessor.canRead的具体实现如下
org.springframework.expression.spel.support.ReflectivePropertyAccessor#canRead
1 | public boolean canRead(EvaluationContext context, Object target, String name)throws AccessException { |
canRead的主要逻辑
- 判断是否为数组,表达式是length则可执行
- 缓存中有,可执行
- findGetterForProperty 找方法
- 找不到方法 findField 找字段
我们先看Method,通过 findGetterForProperty 是怎么找的
findGetterForProperty 在canRead这个类里面
1
2
3
4
5
6
7
8
9protected Method findGetterForProperty(String propertyName, Class<?> clazz, boolean mustBeStatic) {
Method method = findMethodForProperty(getPropertyMethodSuffixes(propertyName),
"get", clazz, mustBeStatic, 0, ANY_TYPES);
if (method == null) {
method = findMethodForProperty(getPropertyMethodSuffixes(propertyName),
"is", clazz, mustBeStatic, 0, BOOLEAN_TYPES);
}
return method;
}不用看到最底下,里面的实现是一堆校验找方法,最主要的判断逻辑是将 get 或 is 作为前缀,表达式字段作为后缀进行拼接,然后在目标类里面查找是否有相关的方法。
找完方法后继续canRead里的逻辑,如果方法不为空,说明有可拼接的 get、is方法,可以继续走 readProperty 的流程。
如果方法为空,则没有对应的get方法,去 findField 找字段。
findField 在canRead这个类里面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21protected Field findField(String name, Class<?> clazz, boolean mustBeStatic) {
Field[] fields = clazz.getFields();
for (Field field : fields) {
if (field.getName().equals(name) && (!mustBeStatic || Modifier.isStatic(field.getModifiers()))) {
return field;
}
}
if (clazz.getSuperclass() != null) {
Field field = findField(name, clazz.getSuperclass(), mustBeStatic);
if (field != null) {
return field;
}
}
for (Class<?> implementedInterface : clazz.getInterfaces()) {
Field field = findField(name, implementedInterface, mustBeStatic);
if (field != null) {
return field;
}
}
return null;
}看第一行,发现这里只获取了公共字段,使用 getDeclaredFields() 才可以获取私有字段。
获取公共字段后与表达式进行匹配,然后进行静态校验,相同则返回。
公共字段找不到则获取父类,递归查找。
递归完仍然查不到,则去实现的接口,也会递归。
这里我有个点没想明白,getFields获取全部公共字段包括父类、接口,所以为什么还要继续递归?大胆质疑源码!!!
createOptimalAccessor
1 | ((ReflectivePropertyAccessor) accessor).createOptimalAccessor(); |
readProperty 在通过 canRead 校验后,如果继承 ReflectivePropertyAccessor,则会用做一些缓存操作,且调用OptimalPropertyAccessor构造器,缓存 member 的值
read
如果通过了canRead的校验,readProperty方法会先进行缓存,然后调用read返回值。
ReflectivePropertyAccessor.OptimalPropertyAccessor#read
我debug是走了内部类的read方法
1 | public TypedValue read(EvaluationContext context, Object target, String name)throws AccessException { |
这里 member 会使用 createOptimalAccessor 缓存好的值,通过代理 invoke 方法走对应类的方法获取值,或者直接去获取字段的值。
解惑
源码分析后,在回来看问题的起因,我想获取异常信息 Throwable:detailMessage,我表达式写的是e.getDetailMessage,按照刚刚的分析走到 findGetterForProperty 会去查找字段对应的get、is方法,但是异常里面获取信息的方法是
1 | public String getMessage() {return detailMessage;} |
并不是 getDetailMessage,所以方法查找不到,而detailMessage又是私有字段,所以 findField 也找不到字段,最后就拿不到我们想要的值,出现解析异常。
解决办法就是将表达式写成 e.message,走getMessage方法获取 detailMessage 字段的值。
我上面分析的都是针对字段表达式,如果写的是方法表达式,getMessage(),带括号的这种,spel调用的时候就需要换一种写法,给表达式指定rootObject,而且表达式的解析路径也不一样了,那么源码就是另外一种解析路径了,下次这里有坑我再去看吧😂
彩蛋
在看 findFields 对字段校验时,有个地方吸引到我了
1 | Modifier.isStatic(field.getModifiers()) |
字面意思可以看出来,是在判断字段是否静态变量,点进源码看看。
1 | public static final int STATIC = 0x00000008; |
发现是在做位运算,用 field.getModifiers() 的值和 STATIC,STATIC表示十六进制数:8。
看到这想起之前刷算法遇到位运算的题,就想看看jdk是怎么联动的,通过位运算判断静态变量。
网上搜了下 field.getModifiers() 这个方法是获取字段前面修饰符的值,jdk底层把每个修饰符按十六进制数进行定义了,通过这个方法可以得到修饰符之和。以下列举部分:
1 | public static final int PUBLIC = 0x00000001; //1 |
那么就很清晰了,获取字段修饰符之和,然后在和 STATIC:8 进行按位与计算,按照位运算,如果修饰符之和没有STATIC:8,得到的结果一定是0,不为0则说明组合的修饰符包含 static。同理其他修饰符都可以使用 Modifier 类中的方法进行位运算判断。
ps:如果不明白就去复习一下位运算吧,按位与&,每位计算的结果如下:
1 | 0 & 0 | 0 & 1 | 1 & 0 = 0 |
只有同位都为1,按位与才等于1,所以当前位不为1说明修饰符之和不包含要判断的修饰符。
追加内容
最近看到1v5大佬分享的内容讲到了Spel,刚好自己写日志的时候有过了解,看完视频后对Spel这一块内容认知更全面了,主要是从三个层面进行理解,解析器、表达式、上下文。
Spel关联链路:使用解析器转换输入字符串内容,得到需要的表达式类型,然后设置上下文内容,最后根据表达式在上下文中获取参数对应的值。这是Spel技术实现的三个顶层抽象,从这三个层面进行展开,我们再去看实现类之间的关系,确实更加清晰了。非常推荐看到这里的朋友去看看上方链接的视频。此次讲解Spel也让我对源码阅读有了新的思路,看顶层抽象,入手设计层面上的基础关联关系。