自动装配
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。