spring获取注解的bean产生的NPE问题

问题背景

跟上个问题一样,在公司业务缩减的大背景下,为了降本进行微服务项目合并,合并后测试一下喽,出问题本地单元测试排查一下,结果单元测试抛出了NPE,堆栈如下:

...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'propertiesBeanDictionary': Invocation of init method failed; nested exception is java.lang.NullPointerException
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:137)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:407)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1623)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:481)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:312)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:308)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:121)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)
	... 29 more
Caused by: java.lang.NullPointerException
	at com.....wireless.switches.dictionary.PropertiesBeanDictionary.lambda$init$0(PropertiesBeanDictionary.java:47)
	at java.util.Iterator.forEachRemaining(Iterator.java:116)
	at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at com.....wireless.switches.dictionary.PropertiesBeanDictionary.init(PropertiesBeanDictionary.java:42)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:366)
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:311)
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:134)
	... 46 more

问题分析

堆栈有了源码看一哈

@PostConstruct
public void init() {
    Map<String, Object> configurationProperties = applicationContext
            .getBeansWithAnnotation(ConfigurationProperties.class);
    ...
    configurationProperties.entrySet().stream().forEach(entry -> {
        String beanName = entry.getKey();
        Object target = entry.getValue();
        ConfigurationProperties annotation = target.getClass()
                .getAnnotation(ConfigurationProperties.class);
        // 空指针是此处抛出的
        String prefix = annotation.prefix();
        ...
}

通过spring上下文获取存在ConfigurationProperties注解的bean列表,然后遍历bean获取bean的ConfigurationProperties注解信息,结果ConfigurationProperties注解是null?小朋友,你是否有很多问号?是不是源码不是最新的?断点看一哈,结果断点看到的注解annotation对象确实是null。。。为啥?断点定位到该bean是jsonPropertiesConfig,源码看一哈

@Configuration
@ConfigurationProperties(prefix = "json")
@PropertySource(value = { "classpath:json/json-order.properties",
		"classpath:json/json-order-detail.properties" }, ignoreResourceNotFound = true)
public class JsonPropertiesConfig {
}

多么朴实无华的源码,为啥会拿不到注解呢,断点时我们可以看下这个bean跟其他正常的bean有啥不一样,下图中第一个就是有问题的配置,其他都是正常的,可以明显的看出,该问题bean并不是一个普通的java bean,而是通过cglib生成的动态代理,问题渐渐浮出水面了
1

推断1

正常情况下我们的bean中是定义了与配置一一对应的字段属性的,这样spring结合ConfigurationProperties注解对bean的属性与properties配置进行数据绑定,得到一个普通的java bean,而出现问题的JsonPropertiesConfig配置是一个空类,没有任何属性,spring为了绑定properties属性为bean自动生成了动态代理,自动为其生成了对应的字段属性。并且自动生成的代理抛弃了原注解。
上面是我们的一种推单,看下源码来验证下该推断是对是错
了解spring ConfigurationProperties注解的同学知道,注入的前提是bean要存在ConfigurationProperties注解,那岂不是与当前的现象相背了,spring源码如下:

// org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String)
public Object postProcessBeforeInitialization(Object bean, String beanName)
		throws BeansException {
	ConfigurationProperties annotation = AnnotationUtils
			.findAnnotation(bean.getClass(), ConfigurationProperties.class);
	if (annotation != null) {
		postProcessBeforeInitialization(bean, beanName, annotation);
	}
	...
}

在此处断点发现此时的bean已经是cglib生成的代理类了,并非还是原始的bean实例,至此可以证明当前推单是错误的,因为动态代理的bean没有抛弃原注解,保留了原本类的所有注解属性

推断2

那么问题更加明显了,显然是两种获取注解bean的方式不同。有问题的获取方式是直接通过target.getClass().getAnnotation的方式获取,cglib是通过继承原始类实现动态代理的,那么注解是在父类上,如果该注解不是可继承类型则子类自然找不到该注解。查看ConfigurationProperties注解的定义确实不允许继承
那么允许继承的注解,通过getClass().getAnnotation的方式可以获取到吗?答案是:可以的。自定义一个可继承的注解,通过断点可以看到动态代理的class对象中的注解数据中是可以找到该注解的
2

小结

问题定位解决方案也自然有了,那就是用spring提供的工具类来获取类注解咯,因为工具类中是会递归获取子类的超类、所有接口的注解,从所有注解中查找目标注解

private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, Set<Annotation> visited) {
	...
	for (Class<?> ifc : clazz.getInterfaces()) {
		A annotation = findAnnotation(ifc, annotationType, visited);
		if (annotation != null) {
			return annotation;
		}
	}

	Class<?> superclass = clazz.getSuperclass();
	if (superclass == null || Object.class == superclass) {
		return null;
	}
	return findAnnotation(superclass, annotationType, visited);
}

3

总结

  1. target.getClass().getAnnotation的方式可以获取到父类中可继承类型的注解
  2. spring通过cglib生成的动态代理,对于ConfigurationProperties注解的bean并没有什么特殊处理,不会自动动态的为你生成对应的字段属性。即使为你生成并绑定属性值,由于类定义没有属性,无法通过实例读取配置;那么既然是JSON是否可以转为json或者map然后通过配置的key获取配置value值?使用fastjson验证结论是不可以。强转map更行不通因为根本不是map的子类
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值