spring自動注入類型不匹配問題

背景

微服務項目要整合合併,結果各種踩坑

問題描述

合併後的項目啓動應用報錯類型不一致,堆棧如下:

2020-07-02 11:32:04,008 ERROR  [main] org.springframework.boot.SpringApplication:: Application startup failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'CNGetPakgOverDistanceHandler': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'stringRedisTemplate' is expected to be of type 'org.springframework.data.redis.core.StringRedisTemplate' but was actually of type 'org.springframework.data.redis.core.RedisTemplate'
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:321) ~[spring-context-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1269) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:551) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:481) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:312) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:308) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867) ~[spring-context-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543) ~[spring-context-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) ~[spring-boot-1.5.17.RELEASE.jar:1.5.17.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693) ~[spring-boot-1.5.17.RELEASE.jar:1.5.17.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360) ~[spring-boot-1.5.17.RELEASE.jar:1.5.17.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:303) ~[spring-boot-1.5.17.RELEASE.jar:1.5.17.RELEASE]
	at com.dianwoba.wireless.fundamental.boot.WirelessSpringApplication.run(WirelessSpringApplication.java:16) ~[wireless-fundamental-1.0.0-20191205.115817-28.jar:1.0.0-SNAPSHOT]
	at com.dianwoba.order.foul.Application.main(Application.java:20) ~[classes/:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_131]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_131]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_131]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_131]
	at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:64) ~[?:?]
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'stringRedisTemplate' is expected to be of type 'org.springframework.data.redis.core.StringRedisTemplate' but was actually of type 'org.springframework.data.redis.core.RedisTemplate'
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:384) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.autowireResource(CommonAnnotationBeanPostProcessor.java:522) ~[spring-context-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.getResource(CommonAnnotationBeanPostProcessor.java:496) ~[spring-context-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor$ResourceElement.getResourceToInject(CommonAnnotationBeanPostProcessor.java:627) ~[spring-context-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.annotation.InjectionMetadata$InjectedElement.inject(InjectionMetadata.java:171) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87) ~[spring-beans-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:318) ~[spring-context-4.3.20.RELEASE.jar:4.3.20.RELEASE]
	... 21 more

查看發現項目中使用的自定義的redis模板定義,代碼如下CacheConfig:

@Bean
public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, String> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    template.setDefaultSerializer(template.getStringSerializer());
    StringRedisSerializer serializer = new StringRedisSerializer();
    template.setValueSerializer(serializer);
    template.setHashValueSerializer(serializer);
    return template;
}

這他丫的原服務怎麼啓動的?難道RedisTemplate<String, String>與StringRedisTemplate類型兼容?不會,不然我們的異常就不會報出來了。於是我切回原master分支。應用竟然啓動的賊流暢。小朋友,你是不是有很多問號?我有。

問題分析

spring注入類型兼容規則是什麼?

spring通過Resource註解注入類型匹配注入問題。兩個注入什麼樣類型的數據spring會自動兼容?沒錯跟java的父子類型轉換一樣,子類型可以注入到定義爲父類型的字段,但是父類型不能自動注入到定義爲子類型的字段。相關源碼如下:

  1. 構建注入元數據含元素ResourceElement,構造器中校驗類型,如果Resource註解指定了類型的情況時
  2. 獲取要注入到屬性的bean
// 內部類:org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.ResourceElement
// 該元素重寫了getResourceToInject方法,即獲取用來要注入到field字段的bean
protected Object getResourceToInject(Object target, String requestingBeanName) {
	return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) :
			getResource(this, requestingBeanName));
}
// 1. 首先從jndi工廠中按照名稱查找,名稱優先級(Resource註解配置):lookup->mappedName->name
// 2. 其次從resourceFactory(beanFactory)中獲取bean
protected Object getResource(LookupElement element, String requestingBeanName) throws BeansException {
	if (StringUtils.hasLength(element.mappedName)) {
		return this.jndiFactory.getBean(element.mappedName, element.lookupType);
	}
	if (this.alwaysUseJndiLookup) {
		return this.jndiFactory.getBean(element.name, element.lookupType);
	}
	if (this.resourceFactory == null) {
		throw new NoSuchBeanDefinitionException(element.lookupType,
				"No resource factory configured - specify the 'resourceFactory' property");
	}
	return autowireResource(this.resourceFactory, element, requestingBeanName);
}
  1. 從工廠中獲取要注入的bean(即CNGetPakgOverDistanceHandler父類DwdElemeOverDistanceCheckHandler所依賴的StringRedisTemplate)(doGetBean)。TypeConverterDelegate中對類型轉換並判斷是否匹配通過:ClassUtils.isAssignableValue(requiredType, convertedValue)方法判斷
// Check if required type matches the type of the actual bean instance.
if (requiredType != null && bean != null && !requiredType.isInstance(bean)) {
	try {
        // 如果需要的話進行類型轉換:TypeConverterSupport.convertIfNecessary
		return getTypeConverter().convertIfNecessary(bean, requiredType);
	}
	catch (TypeMismatchException ex) {
		if (logger.isDebugEnabled()) {
			logger.debug("Failed to convert bean '" + name + "' to required type '" +
					ClassUtils.getQualifiedName(requiredType) + "'", ex);
		}
		throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
	}
}

此場景下顯然RedisTemplate不是StringRedisTemplate的子。拋出類型不匹配異常

爲什麼合併前項目可以啓動成功?

打斷點發現注入的StringRedisTemplate實例根本不是我們自定義配置CacheConfig中定義的bean。而是springboot自動配置中定義的StringRedisTemplate bean。
RedisAutoConfiguration配置類

@Configuration
protected static class RedisConfiguration {

	...
	@Bean
	@ConditionalOnMissingBean(StringRedisTemplate.class)
	public StringRedisTemplate stringRedisTemplate(
			RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}
}

兩個Configuration註解的配置類優先級是什麼?

Application(springboot的source類)優先級如下(其他bean優先級相同):

成員內部類或接口->ComponentScans->Import->ImportResource->Application自身Bean註解的方法->Application的接口->ImportResource

那麼也就是說org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration(Application的Import階段,由SpringBootApplication註解繼承的父註解EnableAutoConfiguration導入)的優先級應該是低於我們自定義CacheConfig
簡單說一下EnableAutoConfigurationImportSelector的處理流程,它會從spring工廠中加載EnableAutoConfiguration的實現類進行import導入。自動配置文件根據spring-autoconfigure-metadata元數據配置進行排序、過濾,然後再執行導入。

  1. Configuration配置類採集至ConfigurationClassParser.configurationClasses
  2. ConfigurationClassBeanDefinitionReader遍歷讀取ConfigurationClassParser.configurationClasses緩存中配置類將bean定義註冊至工廠

ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass加載配置類中的bean定義,我們看下beanMethod方式的bean定義的處理邏輯:loadBeanDefinitionsForBeanMethod

  1. 判斷Conditional忽略bean定義
  2. 獲取bean定義的別名,讀取bean註解的name屬性,如果沒有則默認爲方法名稱
  3. 判斷是否被已存在的bean定義重寫。如果是ConfigurationClassBeanDefinition、ScannedGenericBeanDefinition、bean定義的角色大於ROLE_APPLICATION,則允許重寫
  4. 註冊bean定義至工廠registerBeanDefinition。獲取已存在的bean定義,如果不允許重寫bean定義則拋出異常(默認允許)。

小結

  1. 第一我們的自定義的bean優先級高於springboot自動配置的bean。
  2. 第二springboot自動配置的redis bean不會生效,條件判斷ConditionalOnMissingBean是註冊bean階段的條件(loadBeanDefinitionsForBeanMethod);會直接跳過,事實也證明是如此,見下圖斷點已經走到(即跳過了bean註冊):

1
所以不論如何都應該是自定義的cacheConfig中的redis bean生效纔對,也就是一定會報錯

爲什麼合併前工廠中是springboot自動配置的redis bean?

切回master分支運行斷點,依然是先走到自定義的redis bean,再走到springboot自動註冊redis bean,但是竟然沒有skip?那麼問題的原因漸漸浮現了,問題一定是在shouldSkip條件判斷這個階段

// OnBeanCondition.getMatchOutcome
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
	BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
			ConditionalOnMissingBean.class);
    // 如果找到bean則返回條件不通過noMatch。查找策略爲ALL,即所有
	List<String> matching = getMatchingBeans(context, spec);
	if (!matching.isEmpty()) {
		return ConditionOutcome.noMatch(ConditionMessage
				.forCondition(ConditionalOnMissingBean.class, spec)
				.found("bean", "beans").items(Style.QUOTE, matching));
	}
    // 如果未找到bean則返回條件通過
	matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec)
			.didNotFind("any beans").atAll();
}
return ConditionOutcome.match(matchMessage);
// 創建BeanSearchSpec
BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata,
		Class<?> annotationType) {
	this.annotationType = annotationType;
	MultiValueMap<String, Object> attributes = metadata
			.getAllAnnotationAttributes(annotationType.getName(), true);
	collect(attributes, "name", this.names);
	collect(attributes, "value", this.types);
	collect(attributes, "type", this.types);
	collect(attributes, "annotation", this.annotations);
	collect(attributes, "ignored", this.ignoredTypes);
	collect(attributes, "ignoredType", this.ignoredTypes);
	this.strategy = (SearchStrategy) metadata
			.getAnnotationAttributes(annotationType.getName()).get("search");
	BeanTypeDeductionException deductionException = null;
	try {
		if (this.types.isEmpty() && this.names.isEmpty()) {
			addDeducedBeanType(context, metadata, this.types);
		}
	}
    ...
}

getMatchingBeans查找bean
如果查找策略SearchStrategy是PARENTS或者ANCESTORS,則使用工廠的父類查找。
如果工廠爲null則返回空集合,即ConditionalOnMissingBean條件通過
從工廠中獲取所有註解type屬性指定類型的beanNames(包含父子層級)。此時cacheConfig中定義的類型是RedisTemplate所以不屬於指定類型。父類不能轉讓成子類。type=StringRedisTemplate,工廠中目前僅存在:RedisTemplate

// BeanTypeRegistry
Set<String> getNamesForType(Class<?> type) {
	updateTypesIfNecessary();
	Set<String> matches = new LinkedHashSet<String>();
	for (Map.Entry<String, Class<?>> entry : this.beanTypes.entrySet()) {
		if (entry.getValue() != null && type.isAssignableFrom(entry.getValue())) {
			matches.add(entry.getKey());
		}
	}
	return matches;
}

從beanNames中移除條件註解ignoredTypes屬性指定的類型對應的beanName
從工廠中獲取所有註解annotation屬性指定類型的beanNames(包含父子層級)
從工廠中獲取註解name屬性指定相同名稱的bean(包含父子層級),調用工廠containsBean方法。沒有指定name所以,最終判斷條件通過繼續註冊bean,會覆蓋自定義的cacheConfig中的redis bean
2

爲什麼合併後工廠中不是springboot自動配置的redis bean?

3
由於合併的分支增加了下面的配置,所以導致此處springboot的自動配置miss。

@Bean
@Primary
public StringRedisTemplate stringRedisTemplate2(RedisConnectionFactory redisConnectionFactory) {
	return new StringRedisTemplate(redisConnectionFactory);
}

問題總結

  1. 合併前,項目中沒有StringRedisTemplate類型的bean,所以自動配置的bean定義會覆蓋掉自定義bean定義。所以自定義的bean定義是錯的也沒有拋出異常
  2. 合併後,項目中存在StringRedisTemplate類型的bean配置,所以自動配置的bean定義不會覆蓋掉自定義bean定義。注入時根據名稱匹配注入,因爲自定義的配置類型是錯誤的RedisTemplate類型。於是拋出了異常
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章