当我们第一次创建一个springboot工程时,我们会对启动类(xxxApplication)有许多困惑,为什么只要运行启动类我们在项目中自定义的bean无需配置类配置,扫描就能自动注入到IOC容器中?为什么我们在pom文件中引入starter就可以自动的将第三方组件注册到IOC容器中?这些困惑的答案就是本文的答案
谈及启动类,肯定绕不开@SpringBootApplication注解,这个注解是干什么的?有什么用?只有知晓其组成成分我们才能解答这些问题。
(1)@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 {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class>[] exclude() default {};
@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class>[] scanBasePackageClasses() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "nameGenerator"
)
Class extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
@ComponentScan注解相信大家都很熟悉了,它的作用就是扫描指定的package或者类,将所有加了@Component、@Service、@Repository、@Controller、@Configuration注解的类对象配置成bean,加载到IOC容器中统一管理。
@ComponentScan注解使用:
@Configuration
@ComponentScan(basePackages = {"com.hammajang.annotations.service",
"com.hammajang.annotations.controller"},
basePackageClasses = User.class)
public class Application{
public static void main(String[] args){
Application.run(Application.class,args);
}
}
在上述例子中,Spring会扫描com.hammajang.annotations下的service包、controller包,将这些包中配置成bean的类加载到IOC容器中,同时还会扫描User类,如果它也配置成bean,那么也会加载到容器中。
补充:
@SpringBootConfiguration组成:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
可以看到里面嵌套了一个@Configuration注解,这表明启动类也是一个配置类,也会被注册成bean加载到容器中。
在基于Springboot搭建的项目中,如果我们想引入redis作为缓存中间件只需要三步
(1)在pom文件引入redis的starter依赖:
org.springframework.boot
spring-boot-starter-data-redis
(2)在yaml文件配置redis服务主机地址、端口等信息:
spring:
redis:
host: 127.0.0.1
port: 6379
(3)在业务层注入redis客户端对象:
@Resource
StringRedisTemplate redisTemplate;
在整个过程中,我们只引入了redis相关jar包、配置了redis信息,但没有将redis客户端对象配置成bean,那Spring是如何将这些第三方组件基于配置文件配置成bean并加载到IOC容器中的?
先来看@EnableAutoConfiguration注解实现:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class>[] exclude() default {};
String[] excludeName() default {};
}
@AutoConfigurationPackage作用:指定Spring扫描的包范围,默认是标注了该注解的类(启动类)所在的包路径,将启动类包路径下的所有bean加载到IOC容器中。
@Import(AutoConfigurationImportSelector.class)作用:将第三方的自动配置类加载到IOC容器中。
那么第一个疑问我们已经有答案了:@SpringBootApplication是一个复合注解,其中包含了@ComponentScan注解以及@AutoConfigurationPackage,这两个注解共同作用,默认扫描当前包(启动类所在包)及其子包,将配置的bean加载到IOC容器中。
接下来我们再聚焦于AutoConfigurationImportSelector这个类,这个类含有一个很重要的方法:selectImports():
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
//返回值:需要注册到IOC容器中的类的全路径名数组
//如:["com.hammajang.entity.User","com.hammajang.service.UserServiceImpl"]
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
再来看获取字符串数组的getAutoConfigurationEntry()方法:
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
//检查是否启用了自动配置
if (!this.isEnabled(annotationMetadata)) {
//没有启用则返回空
return EMPTY_ENTRY;
} else {
//启用了自动配置,读取注解所有属性信息
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//获取候选配置
//加载spring.factories文件中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值
List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//去除重复的自动配置类
configurations = this.removeDuplicates(configurations);
//获取需要排除的自动配置类
Set exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
在getCandidateConfigurations()方法处打上断点,启动Springboot工程:
可以看到List数组存储了很多自动配置类的全路径名;再来看看getCandidateConfigurations()方法实现:
protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 使用 SpringFactoriesLoader 从 META-INF/spring.factories 文件中获取自动配置类的全路径名
List configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
// 使用断言确保自动配置类列表非空,如果为空则抛出异常
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
// 返回获取到的自动配置类列表
return configurations;
}
进入autoconfigure的spring.factories文件中我们可以看到如下配置信息:
最后我们再来看一下SpringFactoriesLoader是如何读取每个starter依赖中spring.factories的配置信息的:
private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
//通过类加载器获取所有starter中spring.factories文件的URL资源列表
Enumeration urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
// 创建一个多值映射,用于保存工厂类型和实现类的映射关系
LinkedMultiValueMap result = new LinkedMultiValueMap();
//迭代资源列表
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
//遍历工厂实现类数组,将映射关系添加到结果中
for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
//将加载的映射关系放入缓存中
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
那么第三方组件如何自动注入到IOC容器的疑问我们也有答案了:
(1)Springboot通过@Import注解将AutoConfigurationImportSelector类注入到IOC容器中。
(2)AutoConfigurationImportSelector实现了ImportSelector接口,其中有一个selectImports()方法用于导入自动配置类。
(3)Springboot通过SpringFactoriesLoader加载每一个starter中的spring.factories文件,获取key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的value值,再通过反射将自动装配类(xxxAutoConfiguraion)加载到IOC容器中。
(4)最后再通过自动装配类的配置信息,将第三方组件配置成bean加载到IOC容器中。