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类型。于是抛出了异常
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值