前些时候,在群里面看到同事抛出来的一个比较神奇的空指针异常:
源码背景:
1.从代码可以确认,抛异常的点在于babyUserClientService为null
2.其中 weddingShopBriefInfoService 和 babyUserClientService 都是单例的,且初始化和注入方式都一样。
3.这些单例是被注入Struct2 的 Action 中
4.Spring 版本 3.1.2.RELEASE
告警现象:
1.偶发-不是所有的请求都会报
2.随机-每次报异常的点都不一样
异常分析:
1.这个偶发可能不是真正的偶发,而是特定场景下的必然。由于每次异常数量都比较少,而且需要过一段时间才会再次发生异常。故猜测:这个特定场景就是服务发布。查看发布记录与告警记录,结果与猜想的吻合。
2.再结合告警现象,摆明了一副“并发问题的嘴脸”。和1中的结果想结合,应该是服务器启动后遇到并发请求时会必然出现的结果。为了验证猜想,本地起了服务,手动访问,确实无法重现问题。于是开始准备压测,将qps提升到了100,然后启动服务。果然,问题重现了。
3.既然2中将问题重现了,说明朝着这个方向继续搞,准没错。但是究竟是什么原因呢?既然是 Spring 的 bean 注入失败,则就朝着这个方向去研究。由于 Struct2 的Action 作用域是 prototype,所以每一个请求过来Spring 都会调用 getBean 来创建Action对象的实例。实例创建成功后,Spring 会为其注入相应的依赖。目前看来是这个环节出了问题。
4.定位到代码环节了,那肯定就不能光空想了,经过一段时间的调试,定位到相关环节的代码:
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
InjectionMetadata metadata = findResourceMetadata(bean.getClass());
try {
metadata.inject(bean, beanName, pvs);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex);
}
return pvs;
}
不过这段代码并没什么问题,点开 findResourceMetadata 方法,这里返回了 InjectedElement 的实现 ResourceElement,再看 metadata.inject :
public void inject(Object target, String beanName, PropertyValues pvs) throws Throwable {
if (!this.injectedElements.isEmpty()) {
boolean debug = logger.isDebugEnabled();
for (InjectedElement element : this.injectedElements) {
if (debug) {
logger.debug("Processing injected method of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
}
又调用 ResourceElement.inject :
protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
if (this.isField) {
Field field = (Field) this.member;
ReflectionUtils.makeAccessible(field);
field.set(target, getResourceToInject(target, requestingBeanName));
}
else {
if (checkPropertySkipping(pvs)) {
return;
}
try {
Method method = (Method) this.member;
ReflectionUtils.makeAccessible(method);
method.invoke(target, getResourceToInject(target, requestingBeanName));
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
可以看到,如果注解在字段上的话,是通过 ResourceElement.getResourceToInject 来获取依赖对象的:
@Override
protected Object getResourceToInject(Object target, String requestingBeanName) {
Object value = null;
if (this.cached && this.shareable) {
value = this.cachedFieldValue;
}
synchronized (this) {
if (!this.cached) {
value = getResource(this, requestingBeanName);
if (value != null && this.shareable) {
this.cachedFieldValue = value;
this.cached = true;
}
}
}
return value;
}
重点来了,可以看出以上代码有个很明显的并发问题。可以想象,几个线程同时进入到 synchronized 这块,一个线程获得锁,进去了。执行完成后就将 this.cached = true,然后释放锁。而那个和他一起到 synchronized 的代码,在获取锁后,都会跳过 if(!this.cached ),而此时的 value 依旧等于null。这边还看了其他注解处理器:@Autowired、@Inject 等都有一样的问题
至此,以上所有的现象都能说通了。
结论
所有延迟创建的bean都可能存在并发问题(当然,这个问题只能是存在这个版本)
解决问题
问题既然已经定位到了,就要想怎么解决。由于是 Spring 的问题,可以分为2种方案:
1.spring 发现且在高版本中已经解决这个问题。那可以通过升级版本来解决。
2.spring 没有解决这个问题,则一边通过向spring 提 bug,一边先用服务预热的方式解决(启动过程中先调用getbean)。
结果也如想象的一样,spring 在 高版本中已经解决了这个问题,这里选择3.2.13.RELEASE版本查看代码,发现其已经不再使用锁机制获取对象了。具体逻辑这里不再累赘。有兴趣的小伙伴可以自行查看源码。
由于 3.2.13.RELEASE 和 3.1.2.RELEASE 都是 3.x版本下,差别不是太大。升级风险比较小。所以推荐可以直接升级至 3.2.13.RELEASE 。
我这里升级版本至 3.2.13.RELEASE 后放置 beta服务启动后再进行压测,发现已经没有 空指针异常了。
让相关同学升级后反馈问题解决。