spring依赖注入隐藏的NPE炸弹

spring在处理循环依赖的问题上引入了“提前曝光对象”,但是这避免不了在出现循环依赖时所带来的NPE可能性,这种场景博主在另一篇文章中有描述https://blog.csdn.net/u010597819/article/details/86646162
那么没有循环依赖是不是我们就安全了?哎,果然还是与我们NPE的再次邂逅

问题描述

测试已经测完了大半的功能,突然在一天下午出现怎么都起不来服务,测试问我们有没有代码改动,而我们并没有提交任何代码,代码没有改动,之前测试好好的,突然怎么就起不来了呢?查看日志里面正是我们的老朋友NPE,然后根据日志查看具体代码

Caused by: java.lang.NullPointerException
	at .....spring.boot.autoconfigure.api.AbstractClientCache.queryFromClientCache(AbstractClientCache.java:79) ~[client-cache-support-1.0.0.jar:1.0.0]
	at .......DispatchFilterTypeConfigFullyCache.lambda$buildCache$1(DispatchFilterTypeConfigFullyCache.java:47) ~[classes/:?]
	at java.util.ArrayList.forEach(ArrayList.java:1249) ~[?:1.8.0_131]
	at .......DispatchFilterTypeConfigFullyCache.buildCache(DispatchFilterTypeConfigFullyCache.java:45) ~[classes/:?]
	at .....spring.boot.autoconfigure.api.AbstractClientCache.initCache(AbstractClientCache.java:34) ~[client-cache-support-1.0.0.jar:1.0.0]
	at .....spring.boot.autoconfigure.ClientCacheHandler.afterPropertiesSet(ClientCacheHandler.java:36) ~[client-cache-support-1.0.0.jar:1.0.0]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
	... 14 more

问题原因分析

  1. 是不是我们之前碰到的循环依赖问题呢?于是赶紧去看了下依赖的bean里面是否存在循环依赖的问题,发现并木有。。。
@Component
public class DispatchFilterInfoFullyCache extends
        AbstractFullyClientCache<String, List<DispatchFilterInfoDTO>, DispatchFilterInfo> {

    @Resource
    private DispatchFilterInfoManager dispatchFilterInfoManager;
  1. 了解spring加载bean流程的同学应该知道,bean在加载时,对于依赖的引用会默认使用提前曝光对象注入,所以不应该再该处出现空指针问题;那么问题出在了哪呢?
  2. 我们再来跟着日志一步步看下异常原因,很明显是dispatchFilterInfoFullyCache.cache为空导致的
  3. 也就是说依赖已经注入,但是注入的缓存还没有创建导致

1.type缓存调用另一个缓存,DispatchFilterTypeConfigFullyCache.lambda$buildCache$1(DispatchFilterTypeConfigFullyCache.java:47)
List<DispatchFilterInfoDTO> dispatchFilterInfoDTOS = dispatchFilterInfoFullyCache.queryFromClientCache(dispatchFilterTypeConfigResultDTO.getId()+"");
2.DispatchFilterInfoFullyCache缓存查询key对应的值
public V queryFromClientCache(K key) {
    return this.cache.getIfPresent(key);
}
  1. 问题基本确认了,如果说ClientCacheHandler Bean在其他本地缓存前进行初始化的话,list属性中注入本地缓存的“提前曝光对象”,如果此时list中的缓存顺序是DispatchFilterTypeConfigFullyCache、DispatchFilterInfoFullyCache;那么在调用DispatchFilterTypeConfigFullyCache的buildCache方法时,由于该缓存使用了DispatchFilterInfoFullyCache缓存,如果DispatchFilterInfoFullyCache缓存还没有创建本地缓存,那么必然会出现空指针问题
@Bean
public ClientCacheHandler clientCacheHandler() {
    return new ClientCacheHandler();
}
public class ClientCacheHandler implements InitializingBean {
    @Autowired
    private List<ClientCache> list;

    public void afterPropertiesSet() {
            Iterator var1 = this.list.iterator();

            while(var1.hasNext()) {
                ClientCache clientCache = (ClientCache)var1.next();
								...
                //init缓存中调用了本地缓存的buildCache方法
                Cache cache = clientCache.initCache();
                ...

问题复现

首先按照分析结果尝试复现问题。我们使用@Order注解DispatchFilterInfoFullyCache,也就是强制指定顺序为最后。并且让DispatchFilterTypeConfigFullyCache实现PriorityOrdered接口提供更高优先级的加载或者也使用@Order注解指明最高优先级,果然问题本地复现成功。证明了我们的分析

问题解决

确认了问题之后,解决方案也就很明朗了,没错,只要保证DispatchFilterTypeConfigFullyCache依赖的DispatchFilterInfoFullyCache缓存先一步建立并刷新缓存即可,使用@Order(HIGHEST_PRECEDENCE)注解指定注入的list表中DispatchFilterInfoFullyCache排在最前面,因为缓存是按照顺序初始化加载,则问题自然就解决了
于是马上修改代码通知测试小伙伴重新打包发布,问题修复

期间的思考

疑惑:第一次复现时仅指定了DispatchFilterInfoFullyCache为@Order(默认为最低_LOWEST_PRECEDENCE_),已经可以百分百复现了,但是写本文时竟然不能复现了,需要同时配置DispatchFilterTypeConfigFullyCache为最高优先级才可以复现。这个是为什么???

那我们来看看在注入列表属性时的排序规则吧

  1. populateBean注入属性
  2. 根据类型注入autowireByType
  3. resolveDependency解析依赖对象
  4. 放入属性列表中待注入

解析依赖对象

  1. 工厂类解析依赖DefaultListableBeanFactory.resolveDependency
  2. 如果是Optional、ObjectFactory、ObjectProvider、javax.inject.Provider,则按照相应的提供者解析
  3. 如果均不是则判断是否懒惰方式处理,不是则执行解析doResolveDependency
  4. 如果是多bean则执行多bean解析resolveMultipleBeans并返回
  5. 我们直接看与我们需求对应的列表类型,如果是Collection集合类型,并且是接口类型,获取队列的bean列表
  6. 如果存在依赖比较器并且要注入的属性是List类型,则排序后返回
if (getDependencyComparator() != null && result instanceof List) {
	Collections.sort((List<?>) result, adaptDependencyComparator(matchingBeans));
}
return result;

依赖比较器实现

  1. 依赖比较器在创建AnnotatedBeanDefinitionReader对象时注册,AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
  2. 实现类即:AnnotationAwareOrderComparator
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
		BeanDefinitionRegistry registry, Object source) {
	DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
	if (beanFactory != null) {
		if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
			beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
		}
		if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
			beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
		}
	}
  1. AnnotationAwareOrderComparator执行比较
private int doCompare(Object o1, Object o2, OrderSourceProvider sourceProvider) {
	boolean p1 = (o1 instanceof PriorityOrdered);
	boolean p2 = (o2 instanceof PriorityOrdered);
	if (p1 && !p2) {
		return -1;
	}
	else if (p2 && !p1) {
		return 1;
	}

	// Direct evaluation instead of Integer.compareTo to avoid unnecessary object creation.
	int i1 = getOrder(o1, sourceProvider);
	int i2 = getOrder(o2, sourceProvider);
	return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
}
  1. 如果p1是PriorityOrdered类型p2不是,则p1优先级高,相反,p2优先级高
  2. 如果存在sourceProvider(默认为空)则通过sourceProvider获取order,如果读取的order为空,则getOrder
  3. getOrder从对象的注解中获取order,如果不存在则使用_LOWEST_PRECEDENCE_
protected int getOrder(Object obj) {
	Integer order = findOrder(obj);
	return (order != null ? order : Ordered.LOWEST_PRECEDENCE);
}

解答:这就解答了我们的疑惑,如果注解@Order为_LOWEST_PRECEDENCE其实与其他没有Order注解的bean是同等级的,所以我第一次仅用一个Order注解就复现问题是偶发情况,第二次便没有这么好运了_
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值