起因

最近在写日志功能,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
2
3
4
5
6
7
8
9
10
for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canRead(evalContext, contextObject.getValue(), name)) {
if (accessor instanceof ReflectivePropertyAccessor) {
accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor(
evalContext, contextObject.getValue(), name);
}
this.cachedReadAccessor = accessor;
return accessor.read(evalContext, contextObject.getValue(), name);
}
}

canRead这个方法是校验的关键,这里如果校验不通过就不会返回结果,再往后走都是抛异常了,我们进canRead看看

canRead

PropertyAccessor.canRead的具体实现如下

org.springframework.expression.spel.support.ReflectivePropertyAccessor#canRead

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
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
if (target == null) {
return false;
}

Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass());
if (type.isArray() && name.equals("length")) {
return true;
}

PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class);
if (this.readerCache.containsKey(cacheKey)) {
return true;
}

Method method = findGetterForProperty(name, type, target);
if (method != null) {
Property property = new Property(type, method, null);
TypeDescriptor typeDescriptor = new TypeDescriptor(property);
method = ClassUtils.getInterfaceMethodIfPossible(method);
this.readerCache.put(cacheKey, new InvokerPair(method, typeDescriptor));
this.typeDescriptorCache.put(cacheKey, typeDescriptor);
return true;
}
else {
Field field = findField(name, type, target);
if (field != null) {
TypeDescriptor typeDescriptor = new TypeDescriptor(field);
this.readerCache.put(cacheKey, new InvokerPair(field, typeDescriptor));
this.typeDescriptorCache.put(cacheKey, typeDescriptor);
return true;
}
}

return false;
}
  • canRead的主要逻辑

    • 判断是否为数组,表达式是length则可执行
    • 缓存中有,可执行
    • findGetterForProperty 找方法
    • 找不到方法 findField 找字段

    我们先看Method,通过 findGetterForProperty 是怎么找的

  • findGetterForProperty 在canRead这个类里面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    protected 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
    21
    protected 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
if (this.member instanceof Method) {
Method method = (Method) this.member;
try {
ReflectionUtils.makeAccessible(method);
Object value = method.invoke(target);
return new TypedValue(value, this.typeDescriptor.narrow(value));
}
catch (Exception ex) {
throw new AccessException("Unable to access property '" + name + "' through getter method", ex);
}
}
else {
Field field = (Field) this.member;
try {
ReflectionUtils.makeAccessible(field);
Object value = field.get(target);
return new TypedValue(value, this.typeDescriptor.narrow(value));
}
catch (Exception ex) {
throw new AccessException("Unable to access field '" + name + "'", ex);
}
}
}

这里 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
2
3
4
public static final int STATIC           = 0x00000008;
public static boolean isStatic(int mod) {
return (mod & STATIC) != 0;
}

发现是在做位运算,用 field.getModifiers() 的值和 STATIC,STATIC表示十六进制数:8。

看到这想起之前刷算法遇到位运算的题,就想看看jdk是怎么联动的,通过位运算判断静态变量。

网上搜了下 field.getModifiers() 这个方法是获取字段前面修饰符的值,jdk底层把每个修饰符按十六进制数进行定义了,通过这个方法可以得到修饰符之和。以下列举部分:

1
2
3
4
5
6
7
8
9
10
11
public static final int PUBLIC           = 0x00000001; 	//1
public static final int PRIVATE = 0x00000002; //2
public static final int PROTECTED = 0x00000004; //4
public static final int STATIC = 0x00000008; //8
public static final int FINAL = 0x00000010; //16
public static final int SYNCHRONIZED = 0x00000020; //32
public static final int VOLATILE = 0x00000040; //64
public static final int TRANSIENT = 0x00000080; //8*16
public static final int NATIVE = 0x00000100; //16*16
public static final int INTERFACE = 0x00000200; //32*16
public static final int ABSTRACT = 0x00000400; //64*16

那么就很清晰了,获取字段修饰符之和,然后在和 STATIC:8 进行按位与计算,按照位运算,如果修饰符之和没有STATIC:8,得到的结果一定是0,不为0则说明组合的修饰符包含 static。同理其他修饰符都可以使用 Modifier 类中的方法进行位运算判断。

ps:如果不明白就去复习一下位运算吧,按位与&,每位计算的结果如下:

1
2
0 & 0 | 0 & 1 | 1 & 0 = 0
1 & 1 = 1

只有同位都为1,按位与才等于1,所以当前位不为1说明修饰符之和不包含要判断的修饰符。

追加内容

https://www.bilibili.com/video/BV1R24y1W7DY/?spm_id_from=333.999.0.0&vd_source=77c5fa1cb2cbdaccec0dbbcfa038a8e7

最近看到1v5大佬分享的内容讲到了Spel,刚好自己写日志的时候有过了解,看完视频后对Spel这一块内容认知更全面了,主要是从三个层面进行理解,解析器、表达式、上下文。

Spel关联链路:使用解析器转换输入字符串内容,得到需要的表达式类型,然后设置上下文内容,最后根据表达式在上下文中获取参数对应的值。这是Spel技术实现的三个顶层抽象,从这三个层面进行展开,我们再去看实现类之间的关系,确实更加清晰了。非常推荐看到这里的朋友去看看上方链接的视频。此次讲解Spel也让我对源码阅读有了新的思路,看顶层抽象,入手设计层面上的基础关联关系。

46.png