SpringBoot踩坑记录:@Cacheable报错EL1007E: Property or field 'code' cannot be found on null
背景
最近在微服务项目开发中,两个服务的接口使用 @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中没有启用该参数:
再写个简单的测试类验证一下,上测试代码:
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:
在IDEA中添加 -parameters 编译参数:
打开设置 Setting - Java Compiler ,加上参数:
然后重新编译代码产生新的class:
再运行,可以看到已经可以取到参数名称了,如下图:
回到我们的@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 即使没有开这个参数也不会报错,待后续研究验证。
上一篇: Matlab读取单波段tif影像,并显示所有数据和不重复数据
下一篇: YD:Jmeter下载安装