SpringBoot的自动装配

自动装配

SpringBoot帮我们解决了Spring的配置繁杂的问题,这个还是很爽的,不需要任何配置,采用约定大于配置原则,创建一个项目就可以跑起来了,但是这种高度封装也会有一些问题,就是太多的东西对我们是不可见的,导致深度使用和排查问题的难度增加了,所以了解下SpringBoot自动装配的原理还是很有必要的。

开始的地方

我们知道SpringBoot是从DemoApplication中的main方法开始的:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

但实际上自动装配和main方法里实际运行的代码无关,和@SpringBootApplication注解关系很大。

SpringBootApplication注解

SpringBootApplication注解是一个复合注解,它包含了不少注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

其中:

  • SpringBootConfiguration注解表示这个是SpringBoot配置类;
  • EnableAutoConfiguration注解表示启动自动装配,会帮我们自动去加载自动装配类,在自动装配里,这个注解最重要了;
  • ComponentScan注解表示包扫描的范围。

EnableAutoConfiguration注解

EnableAutoConfiguration注解也是一个复合注解,它会去调用自动装配所使用到的类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

这里使用Import注解来引入AutoConfigurationImportSelector类的bean,这个类是实现了DeferredImportSelector接口,实现了selectImports方法。

  • selectImports方法:
	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
				annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

selectImports方法调用了getAutoConfigurationEntry方法,然后往下跟会经过getCandidateConfigurations-》SpringFactoriesLoader.loadFactoryNames-》loadSpringFactories方法,然后看下loadSpringFactories方法。

  • loadSpringFactories方法:
	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
			// 看这里
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryClassName = ((String) entry.getKey()).trim();
					for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryClassName, factoryName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}

在这个方法中,会加载FACTORIES_RESOURCE_LOCATION文件,然后遍历里面的元素,得到factoryClassName,显然自动装配需要装配哪些东西是在FACTORIES_RESOURCE_LOCATION文件中配置的,那看下这个文件吧,它的值是META-INF/spring.factories,打开这个文件看下发现,里面全是key=value…格式的键值对,值还是多个的,找到正在跟的EnableAutoConfiguration发现他的值好多。

  • spring.factories:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
...
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

显然这么多配置我们不可能全部都自动装配进来吧,因为很多我们的项目中根本没用到,装配进来不仅消耗时间资源,而且还可能因为缺少依赖而报错,所以它应该是按照条件来装配的,我们需要就装配进来,以HttpEncodingAutoConfiguration为例子说明一下吧,先看下HttpEncodingAutoConfiguration类上的注解。

  • HttpEncodingAutoConfiguration:
@Configuration
@EnableConfigurationProperties(HttpProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
  • @Configuration注解表示这是一个配置类;
  • @EnableConfigurationProperties注解表示开启配置属性,这个注解就是我们在application.properties配置文件中配置的具体应用,HttpProperties.class表示接受配置的类,这个类里面的属性就是我们在配置文件里面的配置的key;
  • @ConditionalOnXXX注解表示条件装配,只有当条件成立的时候才会装配这个类,这类注解可以配置多个,它们之间的逻辑关系是与运算,也就是说只有当它们都成立时,才会装配这个类,常见的有:
@ConditionalOnBean:容器中存在指定的bean;
@ConditionalOnMissBean:容器中不存在指定的bean;
@ConditionalOnClass:系统中存在指定的类;
@ConditionalOnMissBean:系统中不存在指定的类;
@ConditionalOnProperty:系统中指定的属性有指定的值;
@ConditionalOnResource:路径下是否有指定的文件;
@ConditionalOnWebApplication:web环境;
@ConditionalOnNotWebApplication:非web环境。

在HttpEncodingAutoConfiguration配置类中,就有三个条件:
1、要是Servlet web环境;
2、要有CharacterEncodingFilter类;
3、要有参数spring.http.encoding配置值为true,满足这三个条件就会装配这个类了;
到这里,整个自动装配的流程就过完了,但是其实还是有一些问题。

问题:

@Import注解的使用

@Import注解的作用就是以快速导入的方式将对象添加到spring的ioc容器中,一般有三种使用方式:

  • 直接使用类的完整路径和名称:
import com.example.autodemo.bean.importTest.Test1;
import org.springframework.context.annotation.Import;

@Import(Test1.class)
public class Test {
}

像这样,这个时候会在ioc容器中创建两个bean,一个是Test的bean,名称是test(类名的小写),一个是Test1的bean,名称是Test1的全路径和名称。
在这里插入图片描述

  • 使用实现了ImportSelector接口的类
    ImportSelector接口中有一个selectImports方法,这个方法会返回一个String数组,数组中存放的是类的全名称,@Import注解会将这个数组中所有类的bean都注入到Ioc容器中:
public class Test2 implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.example.autodemo.bean.importTest.Test3"};
    }
}

这个代码不会注入Test2类的bean,但是会注入Test3类的bean,beanName为Test3的全路径和名称,执行结果是:
在这里插入图片描述
这种使用方式很重要,在SpringBoot里面很多地方都用到了。

  • 使用实现了ImportBeanDefinitionRegistrar接口
    这种方式其实和ImportSelector接口的方式差不多,只不过这种方式可以指定bean的name:
public class Test4 implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition(Test5.class);
        registry.registerBeanDefinition("test5Name", beanDefinition);
    }
}

这里会注入Test5的bean,beanName为我们指定的test5Name,执行结果是:
在这里插入图片描述

AutoConfigurationImportSelector#selectImports方法的调用

其实这里SpringBoot的用法就是上面@Import注解的第二种使用方法:实现selectImports方法,返回从spring.factories文件中解析出来的类路径,然后spring就会将这些类注入到ioc容器中,那spring是怎么处理这一块的呢?还是要看下,看网上说在SpringBoot启动的过程中是不会调用这个selectImports方法的,但是我调试了一下,发现还是会调用的,我用的版本是2.0.3.RELEASE,调用栈是:
在这里插入图片描述
过程是从refresh方法开始的,这个方法已经很熟悉了,是ioc容器初始化的关键方法,沿着调用栈往上可以看到一个parse方法,这个是重点,看下源码:

	public void parse(Set<BeanDefinitionHolder> configCandidates) {
		this.deferredImportSelectors = new LinkedList<>();

		for (BeanDefinitionHolder holder : configCandidates) {
			BeanDefinition bd = holder.getBeanDefinition();
			try {
				if (bd instanceof AnnotatedBeanDefinition) {
					parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
				}
				else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
					parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
				}
				else {
					parse(bd.getBeanClassName(), holder.getBeanName());
				}
			}
			catch (BeanDefinitionStoreException ex) {
				throw ex;
			}
			catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
			}
		}
		// 看这里
		processDeferredImportSelectors();
	}

重点是最后一句,DeferredImportSelector的延时性也体现在这里,它会把其他所有的bean处理完成了再来处理DeferredImportSelect,再进入到processDeferredImportSelectors方法中看看:

	private void processDeferredImportSelectors() {
		List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
		this.deferredImportSelectors = null;
		if (deferredImports == null) {
			return;
		}

		deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
		Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
		Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
		for (DeferredImportSelectorHolder deferredImport : deferredImports) {
			Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
			DeferredImportSelectorGrouping grouping = groupings.computeIfAbsent(
					(group == null ? deferredImport : group),
					(key) -> new DeferredImportSelectorGrouping(createGroup(group)));
			grouping.add(deferredImport);
			configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
					deferredImport.getConfigurationClass());
		}
		for (DeferredImportSelectorGrouping grouping : groupings.values()) {
			// 看这里
			grouping.getImports().forEach((entry) -> {
				ConfigurationClass configurationClass = configurationClasses.get(
						entry.getMetadata());
				try {
					processImports(configurationClass, asSourceClass(configurationClass),
							asSourceClasses(entry.getImportClassName()), false);
				}
				catch (BeanDefinitionStoreException ex) {
					throw ex;
				}
				catch (Throwable ex) {
					throw new BeanDefinitionStoreException(
							"Failed to process import candidates for configuration class [" +
									configurationClass.getMetadata().getClassName() + "]", ex);
				}
			});
		}
	}

在grouping.getImports()的时候会调用selectImports方法,获取要装配的类的路径,然后在后续进行实例化,添加到ioc容器中。

如何定义自己的starter

其实定义自己的starter只需要两步就行了:

  • 我们知道,SpringBoot会读取META-INF/spring.factories文件,然后装配文件中配置的类的bean,所以第一步我们需要创建自己的spring.factories文件,指定自动配置类:
    在这里插入图片描述
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.autodemo.starter.HelloAutoConfiguration
  • 第二步就是编写自动配置类HelloAutoConfiguration:
@Configuration
@ConditionalOnProperty(value = "hello.name")
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {

    @Autowired
    private HelloProperties helloProperties;

    @Bean
    public HelloService getHelloService(){
        return new HelloService(helloProperties);
    }
}

这样我们自定义的starter就写完了,至于HelloProperties类、HelloService类都是业务细节,无关紧要,还有就是SpringBoot官方建议非官方的starter命名格式为xxx-spring-boot-starter,所以我们这个自定义的starter最好取名为hello-spring-boot-starter。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值