跟上个问题一样,在公司业务缩减的大背景下,为了降本进行微服务项目合并,合并后测试一下喽,出问题本地单元测试排查一下,结果单元测试抛出了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生成的动态代理,问题渐渐浮出水面了
正常情况下我们的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没有抛弃原注解,保留了原本类的所有注解属性
那么问题更加明显了,显然是两种获取注解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); }