欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot踩坑记录:@Cacheable报错EL1007E: Property or field 'code' cannot be found on null

程序员文章站 2022-03-06 21:26:40
...

背景

最近在微服务项目开发中,两个服务的接口使用 @Cacheable 做缓存处理,一个正常运行,另一个接口一直报错,报错信息如下:

org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'code' cannot be found on null
 at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:213) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:104) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.expression.spel.ast.PropertyOrFieldReference.access$000(PropertyOrFieldReference.java:51) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.expression.spel.ast.PropertyOrFieldReference$AccessorLValue.getValue(PropertyOrFieldReference.java:406) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:92) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.expression.spel.ast.OpPlus.getValueInternal(OpPlus.java:83) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:112) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:267) ~[spring-expression-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.cache.interceptor.CacheOperationExpressionEvaluator.key(CacheOperationExpressionEvaluator.java:104) ~[spring-context-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContext.generateKey(CacheAspectSupport.java:778) ~[spring-context-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.cache.interceptor.CacheAspectSupport.generateKey(CacheAspectSupport.java:575) ~[spring-context-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:518) ~[spring-context-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:401) ~[spring-context-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:345) ~[spring-context-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61) ~[spring-context-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) ~[spring-aop-5.2.0.RELEASE.jar:5.2.0.RELEASE]
 at com.sun.proxy.$Proxy61.getValue(Unknown Source) ~[na:na]
 at com.qc.springlearn.controller.TestController.getValue(TestController.java:20) ~[classes/:na]
 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
 at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
 at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]

一般是因为cacheable中的key指定的参数找不到才会报这个错。

调试

为了分析这个问题的产生原因,自己写了一个简单的SpringBoot测试工程来研究。
先看看我测试样例关键代码:
参数实体类 SimpleDto.java:

@Data
@AllArgsConstructor
public class SimpleDto {
    private String code;
    private String name;
}

Service层实现类 BizServiceImpl.java :

@Service
@Slf4j
public class BizServiceImpl implements BizService {

    @Cacheable(value = "testCacheValue", key="#simpleDto.code+#simpleDto.name")
    @Override
    public String getValue(SimpleDto simpleDto){
        log.info("进入getValue()方法,模拟耗时操作,入参:{}, 当前时间: {}", JSON.toJSONString(simpleDto), System.currentTimeMillis());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String result = simpleDto.getCode() + "-" + simpleDto.getName();
        log.info("得到结果,当前时间: {}", System.currentTimeMillis());
        return result;
    }
}

Controller层 RestController.java :

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    BizService bizService;

    @GetMapping("/getValue")
    public String getValue(){
        SimpleDto simpleDto = new SimpleDto("20191229", "小明");
        return bizService.getValue(simpleDto);
    }
}

编写启动类并启动后,打开浏览器访问 http://localhost:8080/test/getValue
将触发报错。

分析结果

按照异常堆栈调试,会发现到底层通过配置的 key="#simpleDto.code+#simpleDto.name"BizServiceImpl.getValue(SimpleDto) 方法解析得到真正的键名时,取 getValue() 方法的参数param进行 param.isNamePresent() 为 false, param.getName() 结果为 arg0。
可知原因时通过反射取不到方法的形参名字。

查询资料,可知 Java8 中通过编译参数 javac -parameters 开启形参名称保留功能,生成的class文件中将保留形参名称,默认该选项不启用,可能是为了减少生成的class大小。

验证

先确认我们的IDEA中没有启用该参数:
SpringBoot踩坑记录:@Cacheable报错EL1007E: Property or field 'code' cannot be found on null
再写个简单的测试类验证一下,上测试代码:

public class MyTest {
    @Test
    public void test() throws NoSuchMethodException {
        Method method = BizServiceImpl.class.getMethod("getValue", SimpleDto.class);
        System.out.println(method.getParameters()[0].isNamePresent());
        System.out.println(method.getParameters()[0].getName());
    }
}

跑一下看看结果,可以看到取到的参数名称为arg0:
SpringBoot踩坑记录:@Cacheable报错EL1007E: Property or field 'code' cannot be found on null
在IDEA中添加 -parameters 编译参数:
打开设置 Setting - Java Compiler ,加上参数:
SpringBoot踩坑记录:@Cacheable报错EL1007E: Property or field 'code' cannot be found on null
然后重新编译代码产生新的class:
SpringBoot踩坑记录:@Cacheable报错EL1007E: Property or field 'code' cannot be found on null
再运行,可以看到已经可以取到参数名称了,如下图:
SpringBoot踩坑记录:@Cacheable报错EL1007E: Property or field 'code' cannot be found on null
回到我们的@Cacheable上面,我们可以通过开启 -parameters 参数来解决上面的问题,但是如果实际项目中,运维不归我们管,不确定到时候部署的环境上面是否有开启该参数,我们可以使用 p0 或者 a0 来代替这个参数名字,比如:

@Cacheable(value = "testCacheValue", key="#p0.code+#p0.name")
@Override
public String getValue(SimpleDto simpleDto){
    log.info("进入getValue()方法,模拟耗时操作,入参:{}, 当前时间: {}", JSON.toJSONString(simpleDto), System.currentTimeMillis());
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    String result = simpleDto.getCode() + "-" + simpleDto.getName();
    log.info("得到结果,当前时间: {}", System.currentTimeMillis());
    return result;
}

从而避开这个问题,看源码可以看到解析cacheable的key时,会遍历目标方法中的参数列表,将 p0,p1… 和 a0,a1…加到缓存key中。

其他说明

这个问题可能新的Spring版本中有做过优化,因为在我另一个开发环境中使用Spring 5.2.0 即使没有开这个参数也不会报错,待后续研究验证。

相关标签: SpringBoot踩坑记录