问题背景
跟上个问题一样,在公司业务缩减的大背景下,为了降本进行微服务项目合并,合并后测试一下喽,出问题本地单元测试排查一下,结果单元测试抛出了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
正常情况下我们的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对象中的注解数据中是可以找到该注解的
小结
问题定位解决方案也自然有了,那就是用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);
}
总结
- target.getClass().getAnnotation的方式可以获取到父类中可继承类型的注解
- spring通过cglib生成的动态代理,对于ConfigurationProperties注解的bean并没有什么特殊处理,不会自动动态的为你生成对应的字段属性。即使为你生成并绑定属性值,由于类定义没有属性,无法通过实例读取配置;那么既然是JSON是否可以转为json或者map然后通过配置的key获取配置value值?使用fastjson验证结论是不可以。强转map更行不通因为根本不是map的子类