一、SpringBoot自动装配原理
从Spring Boot的核心注解@SpringBootApplication开始入手。观察首启动类
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
@SpringBootApplication 这个注解是标注一个Spring Boot应用,标注这个类是主启动类,SpringApplication.run()
方法会启动Spring应用上下文,进行自动配置和组件扫描。
进入这个注解的内部
@SpringBootConfiguration : 标注在某个类上,表示这是一个Spring Boot的配置类;
@EnableAutoConfiguration : 开启自动装配功能@ComponentScan : 配置扫描路径,用于加载使用注解格式定义的bean
1、进入@SpringBootConfiguration注解内部
可以发现除了几个元注解外,该注解被@Configuration标注,那么可以理解为@SpringBootConfiguration标注的类为一个配置类,配置类主要负责定义应用程序的配置信息,包括创建和配置 Bean 对象、设置数据源、事务管理器等。
1.1、再观察@Configuration的内部
@Component说明该注解被注入到Spring IOC容器中,所以在SpringBoot项目启动时可以自动找到配置。
2.@EnableAutoConfiguration开启自动配置
@AutoConfigurationPackage 指定了默认的包规则将主程序类所在包及所有子包下的组件扫描到Spring容器中;
@Import(AutoConfigurationImportSelector.class) : 通过 @Import 注解导入 AutoConfigurationImportSelector类,然后通过该类的selectimport()方法去读取MATE-INF/spring.factories文件中配置的组件的全类名,并按照一定的规则过滤掉不符合要求的组件的全类名,将剩余读取到的各个组件的全类名集合返回给IOC容器并将这些组件注册为bean
源代码解读如下:
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
2.1、@AutoConfigurationPackage 自动配置包
进入这个注解内部,可看出被@AutoConfigurationPackage标注,该注解的作用是将 添加该注解的类所在的package 作为 自动配置package 进行管理。进入这个注解内部,可以看出该注解导入了Registrar.class类,进入这个类中
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
2.1.1、registerBeanDefinitions()方法
在类中的registerBeanDefinitions()方法中,传入两个参数,AnnotationMetadata metadata为注解元数据,用于检索注解的属性、判断是否存在特定的注解等。第二个参数BeanDefinitionRegistry registry为Bean的信息对象,分析register()方法中的第二个参数
new PackageImports(metadata).getPackageNames().toArray(new String[0])),进入 PackageImports () 方法
PackageImports(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
List<String> packageNames = new ArrayList(Arrays.asList(attributes.getStringArray("basePackages")));
Class[] var4 = attributes.getClassArray("basePackageClasses");
int var5 = var4.length;
for(int var6 = 0; var6 < var5; ++var6) {
Class<?> basePackageClass = var4[var6];
packageNames.add(basePackageClass.getPackage().getName());
}
if (packageNames.isEmpty()) {
packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
}
this.packageNames = Collections.unmodifiableList(packageNames);
}
2.1.2、PackageImports()方法
在这个构造方法中将元数据即启动类AnnotationMetadata metadata经过处理获取标签注解信息,
basePackages、 basePackageClasses为注解@AutoConfigurationPackage中的属性。
根据注解信息里面的basePackages和basePackageClasses是否有数据判断,如果packageNames为空,则获取注解所在的类的名字目录,放在List集合中
如此获得packageNames属性也就是启动类所在的包。回到Registrar中的registerBeanDefinitions()方法中register()方法的第二个参数即为启动类所在的包的名称,并且使用数组来进行表示。
2.1.3、再进入到register()方法内部,
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
AutoConfigurationPackages.BasePackagesBeanDefinition beanDefinition = (AutoConfigurationPackages.BasePackagesBeanDefinition)registry.getBeanDefinition(BEAN);
beanDefinition.addBasePackages(packageNames);
} else {
registry.registerBeanDefinition(BEAN, new AutoConfigurationPackages.BasePackagesBeanDefinition(packageNames));
}
}
这个方法的if语句为判断registry这个参数中是否已经注册了AutoConfigurationPackages的类路径所对应的bean(AutoConfigurationPackages)。如若已经被注册,则把上面分析的第二个参数所获取的包(启动类所在的包的名称)添加到这个bean的定义中。如若没有,则注册这个bean并且把包名设置到该bean的定义中。
2.2、回到EnableAutoConfiguration中再看该注解使用@Import({AutoConfigurationImportSelector.class})导入一个类,进入该类的内部
2.2.1、找到selectImports()方法
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
在这个用法中,所返回的字符串数组为所有的将要被导入的类的全类名。分析return可知该方法的返回值在getAutoConfigurationEntry()方法中获得,所以分析这个方法。
2.2.2、getAutoConfigurationEntry()方法
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> 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);
}
}
可以从return大体看出,返回的是一个使用属性configurations所生成的自动配置实体,而这个实体是通过getCandidateConfigurations方法获得,进入这个方法
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
return configurations,那我们又去找该返回值的获取方法,loadFactoryNames()方法
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
classLoader翻译过了就是类加载器,就是负责把磁盘上的.class
文件加载到JVM内存中
getClassLoader()
方法,得到是谁把我从.class
文件加载到内存中变成Class对象的
该方法中通过判断若是classLoader为null,则直接获取SpringFactoriesLoader的classLoader,若不为null则调用loadSpringFactories()方法,进入这个方法:
2.2.3、loadSpringFactories()方法
//这个方法返回的为Map<String, List<String>> 变量result。
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
//从全局变量cache中以classLoader为key获取所对应的value
Map<String, List<String>> result = cache.get(classLoader);
//判断在cache中是否存在相对应的value,如果已经存在则直接返回对应的result
if (result != null) {
return result;
}
//至此,已经判断得出result为空,所以实例化一个新的HashMap
result = new HashMap<>();
try {
//从传入的类加载器中获取资源,路径为"META-INF/spring.factories"
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
//将获取到的urls进行遍历
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
//获取资源的url在当前项目中的位置等
UrlResource resource = new UrlResource(url);
//通过配置类加载工具类加载配置类,获取所有配置
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
//遍历所有配置实体
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
//遍历当前配置实体中的所有value,如果不存在当前key,则将当前key以及下面遍历所得到的value一起添加到result中,如果存在则将该value添加到所对应的key下面。
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements(译文:用包含唯一元素的不可修改列表替换所有列表)
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
//将该classLoader以及对应的result添加到cache中
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
//至此,返回了META-INF/spring.factories中的所有的配置
return result;
}
小结
1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件**,按照条件装配,@Conditional最终会按需配置
3、回到@ComponentScan注解
这个注解在Spring中很重要 ,它对应XML配置中的元素。
作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOz
总结:
@EnableAutoConfiguration 注解内部使用 @Import(AutoConfigurationImportSelector.class) 来加载配置类。
配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot应用启动时,会自动加载这些配置类,初始化Bean。但并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean
二、Condition
Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建 Bean 操 作。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
@Conditional 注解可以放在类或者方法上。当你放置在配置类上时,只有当所有指定的条件都满足时,配置类中的Bean才会被创建。放置在Bean声明的方法上时,只有当条件满足时,对应的Bean才会被注册
内置条件注解
Spring Boot提供了一系列内置的条件注解,可直接使用:
- @ConditionalOnBean: 当指定的Bean存在时。
- @ConditionalOnMissingBean: 当指定的Bean不存在时。
- @ConditionalOnClass: 当指定的字节码文件存在时。
- @ConditionalOnMissingClass: 当指定的字节码文件不存在时。
- @ConditionalOnProperty: 当指定的属性有指定的值时。
- @ConditionalOnResource: 当指定的资源存在时。
- @ConditionalOnWebApplication: 当项目是一个Web应用程序时。
- @ConditionalOnNotWebApplication: 当项目不是一个Web应用程序时。
Spring Boot中条件匹配具体实现:
4.1、若在 Spring 的 IOC 容器中有一个 User 的 Bean, 导入Jedis坐标后,加载该Bean,没导入,则不加载。
1、创建实体类
public class User {
//
}
2、创建配置类,使用@Bean将User注入容器,标注@Conditional注解,value值为ClassCondition类
@Configuration
public class UserConfig {
//@Conditional中的ClassCondition.class的matches方法,返回true执行以下代码,否则反之
@Bean
@Conditional(value= ClassCondition.class)
public User user(){
return new User();
}
@param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
@param metadata 注解元对象。 可以用于获取注解定义的属性值
3、创建条件类,判断坐标是否导入
public class ClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//1.需求: 导入Jedis坐标后创建Bean
//思路:判断redis.clients.jedis.Jedis.class文件是否存在
boolean flag=true;
try {
Class<?> aClass = Class.forName("redis.clients.jedis.Jedis");
return true;
} catch (Exception e) {
flag=false;
}
return flag;
}
}
4.2、在 Spring 的 IOC 容器中有一个 User 的 Bean,现要求: 将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定
1、创建实体类
public class User {
//
}
2、创建注解
@Target({ElementType.TYPE, ElementType.METHOD})//可以修饰在类与方法上
@Retention(RetentionPolicy.RUNTIME)//注解生效节点runtime
@Documented//生成文档
@Conditional(value = ClassCondition.class)
public @interface conditiononmy {
String[] value();//设置此注解的属性redis.clients.jedis.Jedis
}
3、创建一个条件类继承Condition重写matches方法
public class ClassCondition implements Condition {
//2.需求: 导入通过注解属性值value指定坐标后创建Bean
//获取注解属性值 value
Map<String, Object> map = metadata.getAnnotationAttributes(conditiononmy.class.getName());
System.out.println(map);
String[] value = (String[]) map.get("value");
boolean flag = true;
try {
for (String s : value) {
Class<?> cls = Class.forName(s);
}
flag = true;
} catch (Exception e) {
flag = false;
}
return flag;
}
}
3.创建配置类加载User对象
@Configuration
public class Userconfig {
//@Conditional中的ClassCondition.class的matches方法,返回true执行以下代码,否则反之
@Bean
@conditiononmy(value = {"com.alibaba.fastjson.JSON","redis.clients.jedis.Jedis"})
public User user(){
return new User();
}
}
最后在启动类测试
@SpringBootApplication
public class SpringbootCondition01Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootCondition01Application.class, args);
//固定坐标在condition输入,使用原有注解实现
Object user = context.getBean("user");
System.out.println(user);
//不存在坐标:NoSuchBeanDefinitionException: No bean named 'user' available
//存在坐标:com.example.springboot_condition_01.domain.User@726a6b94
//动态输入坐标,在config配置类使用自定义注解@ConditiononClass(坐标数组)输入,使用自定义注解实现
Object user1 = context.getBean("user1");
System.out.println(user1);
Object user2 = context.getBean("user2");
System.out.println(user2);
}
}
三. @Enable注解
SpringBoot中提供了很多Enable开头的注解,都用于启动某些功能。其底层原理是使用@Import注解导入一些配置类,实现Bean对象的加载。使用@Import导入的类会被Spring加载到IOC容器中。
例如:
@EnableAutoConfiguration开启自动配置,具体详解件上
@Import提供4中用法:
1. 导入Bean
方法一:
- 创建enable模块
创建一个User的Bean对象
public class User {
//
}
创建一个配置类,加载User对象
@Configuration
public class Userconfig {
@Bean
public User user(){
return new User();
}
}
测试模块
- 依赖enable的模块
先将enable模块坐标导入依赖enable的模块
<dependency>
<groupId>com.apesource</groupId>
<artifactId>enable</artifactId>
<version>0.0.1SNAPSHOT</version>
</dependency>
在启动类测试获取User
@SpringBootApplication
@Import(User.class)
public class SpringbootEnable01Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable01Application.class, args);
User user = run.getBean(User.class);
System.out.println(user);
}
}
方法二:在enable模块写一个@Enable注解,用@Import注解导入注入User的配置类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(Userconfig.class)
public @interface EnableUser {
}
在测试模块的启动类上方通过@EnableUser注解导入Bean对象
@EnableUser
@Import(myimportselector.class)
@SpringBootApplication
public class SpringbootEnable03Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable03Application.class, args);
Object user = run.getBean("user");
System.out.println(user);
}
}
2. 导入配置类
也可以在启动类上扫描enable模块的config配置类
@Import(Userconfig.class)
@SpringBootApplication
public class SpringbootEnable03Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable03Application.class, args);
Object user = run.getBean("user");
System.out.println(user);
}
}
3. 导入 ImportSelector 实现类
- enble模块
创建Student对象
public class Student{
//
}
创建MyImportSelector类,实现了ImportSelector接口,重写接口中的selectImports方法,往String[]中传入两个Bean对象
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.example.springboot_enable_02_other.domain.User","com.example.springboot_enable_02_other.domain.Student"};
}
}
测试
@Import(myimportselector.class)
@SpringBootApplication
public class SpringbootEnable03Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable03Application.class, args);
Object user = run.getBean("user");
System.out.println(user);
}
}
4. 导入 ImportBeanDefinitionRegistrar 实现类
- enable模块
MyImportBeanDefinitionRegister类,实现了ImportBeanDefinitionRegistrar接口,重写接口中的registerBeanDefinitions方法,往String[]中传入两个Bean对象
public class MyImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//AnnotationMetadata注解
//BeanDefinitionRegistry向spring容器中注入
//1.获取user的definition对象
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
//2.通过beanDefinition属性信息,向spring容器中注册id为user的对象
registry.registerBeanDefinition("user",beanDefinition);
}
}
测试模块
@SpringBootApplication
//ImportBeanDefinitionRegister实现类
@Import(MyImportBeanDefinitionRegister.class)
public class SpringbootEnable01Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable01Application.class, args);
//ImportBeanDefinitionRegister实现类
User user = run.getBean(User.class);
System.out.println(user);
}
}
四、自定义启动器
自定义redisstarter,要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean
直接设置port、host
- 创建redisspringbootautoconfigure模块
1.1、导入Jedis坐标
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2、创建配置类,注入jedisBean对象
@Configuration
public class redisautoconfigure {
@Bean
public Jedis jedis(){
return new Jedis("localhost",6379);
}
}
- 在resources中定义METAINF/spring.factories文件,
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.apesource.redisspringbootautoconfigure.RedisAutoconfiguration
1.2 创建redisspringbootstarter模块
- 依赖redisspringbootautoconfigure的模块
导入坐标
<dependency>
<groupId>com.apesource</groupId>
<artifactId>redisspringbootautoconfigure</artifactId>
<version>0.0.1SNAPSHOT</version>
</dependency>
1.3 测试模块
引入自定义的redisstarter依赖
<dependency>
<groupId>com.apesource</groupId>
<artifactId>redisspringbootstarter</artifactId>
<version>0.0.1SNAPSHOT</version>
</dependency>
测试获取Jedis的Bean,操作redis。
@SpringBootApplication
public class SpringbootStarter01Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootStarter01Application.class, args);
Jedis bean = run.getBean(Jedis.class);
System.out.println(bean);
}
}
将host,port隐藏实现方法、
在redisspringbootautoconfigure模块创建properties文件
设置host,port
spring.redis.host=121.0.0 spring.redis.port=6379
创建一个properties配置类,将host,port设置为私有属性
@ConfigurationProperties
public class redisproperties {
private int port=6379;
private String host="localhost";
public redisproperties(int port, String host) {
this.port = port;
this.host = host;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public redisproperties() {
}
}
在配置类中使用@EnableConfigurationProperties扫描properties配置文件启动
@Configuration
@EnableConfigurationProperties(redisproperties.class)
public class redisautoconfigure {
@Bean
public Jedis jedis(redisproperties redisproperties){
return new Jedis(redisproperties.getHost(),redisproperties.getPort());
}
}