这是笔者的学习笔记 在这里与大家分享一下,不墨迹,不卖关子。
SpringBoot自动装配
SpringBoot自动装配用到了以下几个注解
- @Conditional
- @Enable
- @Import
- @EnableAutoConfigure
我们以注解为驱动 展开内容
Condition 条件判断
Condition是Spring在4.0引入的条件判断功能,Spring根据这个功能选择性的创建Bean。
下面我们使用SpringDataRedis 来演示一下
案例
- 创建SpringBoot工程,引入springboot基础设施依赖
<!-- springboot依赖版本控制-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--starter依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
- main方法
@SpringBootApplication
public class springbootApp {
public static void main(String[] args) {
// run方法返回的是IOC容器的上下文对象,后续我们会使用他来获取IOC容器内的Bean
ConfigurableApplicationContext context =
SpringApplication.run(springbootApp.class, args);
}
}
- 工程结构图
- 获取redisTemplate
@SpringBootApplication
public class springbootApp {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(springbootApp.class, args);
// 获取redisTemplate 并打印输出
Object redisTemplate = context.getBean("redisTemplate");
System.out.println(redisTemplate);
}
}
- 启动springboot 这里包括下文的启动步骤,我会直接贴出运行结果。
报错,没有名为“redisTemplate”的bean可用,这里大家都知道,因为我们没有引入相应的依赖。 - MAVEN中加入SpringdataRedis 依赖
<!--使用了<Parent> 标签锁定版本 这里无需在指定版本-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 再次启动
问题:为什么我们引入了相应坐标,就能在IOC容器中获取与之对应的Bean。 - 我们自定义一个实体类RedisTest,注入IOC容器,提出一个条件,当容器中存在jedis的时候加载该实体,否者不加载。
- 我们在POM中加入Jedis依赖
<!-- 这里贴出当前工程的所有坐标-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
- 定义实体RedisTest
public class RedisTest {
}
- 定义配置类 将实体对象注入IOC容器 (ps:直接注入很简单 这里是为了后续演示@Conditional注解)
@Configuration
public class RedisConfigurationTest {
@Bean
public RedisTest redisTest(){
return new RedisTest();
}
}
- main方法中获取RedisTest 并打印输出
@SpringBootApplication
public class SpringbootApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootApp.class, args);
// Object redisTemplate = context.getBean("redisTemplate");
Object redisTest = context.getBean("redisTest");
System.out.println(redisTest);
}
}
- 获取成功
依据条件当前容器中存在jedis时,我们才加载RedisTest。 - 修改配置类,增加@Conditional注解
@Configuration
public class RedisConfigurationTest {
@Bean
@Conditional()
public RedisTest redisTest(){
return new RedisTest();
}
}
这里@Conditional 是报错的 他需要传入一个参数。我们进入注解内部看一下他需要的参数是什么。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
需要一个类的class对象,这个类需要继承Condition,我们再次点击Condition,进入他的内部
import org.springframework.core.type.AnnotatedTypeMetadata;
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
Condition是一个接口 只有一个返回值是布尔类型的 mathces方法
- 那么我们就定义Condition配置类实现Condition接口,顺带说明一下两个参数的作用
public class ConditionalConfig implements Condition {
/**
*
* @param conditionContext 作用: 获取 Bean工厂 ClassLoad Class 环境变量等
* @param annotatedTypeMetadata 作用: 注解元对象
* @return
*/
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return false;
}
}
- 这个mathces 就是决定我们是否加载某个类的方法,返回false不加载,true则加载,那么依据刚才的条件我们可以对配置类做一些条件的判断。
public class ConditionalConfig implements Condition {
/**
*
* @param conditionContext 作用: 获取 Bean工厂 ClassLoad Class 环境变量等
* @param annotatedTypeMetadata 作用: 注解元对象
* @return
*/
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
boolean flag;
try {
Class<?> jedis = Class.forName("redis.clients.jedis.Jedis");
System.out.println("jedis加载成功: "+jedis);
flag = true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
flag = false;
}
return flag;
}
}
将配置类加载到@Conditional注解中
@Configuration
public class RedisConfigurationTest {
@Bean
@Conditional(ConditionalConfig.class)
public RedisTest redisTest(){
return new RedisTest();
}
}
- 工程结构
- 启动main方法
jedis、RedisTest均加载成功,接下来我们把jedis的坐标注释,再来观察一下运行结果。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>-->
<!--<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>-->
</dependencies>
- 启动 是报错的 加载不到RedisTest
2020-02-18 13:11:55.803 INFO 10936 --- [ main] com.ips.springbootapp.SpringbootApp : No active profile set, falling back to default profiles: default
java.lang.ClassNotFoundException: redis.clients.jedis.Jedis
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.ips.springbootapp.config.ConditionalConfig.matches(ConditionalConfig.java:19)
at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:184)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:144)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:120)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:331)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:236)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215)
at com.ips.springbootapp.SpringbootApp.main(SpringbootApp.java:10)
2020-02-18 13:11:56.212 INFO 10936 --- [ main] com.ips.springbootapp.SpringbootApp : Started SpringbootApp in 0.614 seconds (JVM running for 1.187)
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'redisTest' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:805)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1278)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:297)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1108)
at com.ips.springbootapp.SpringbootApp.main(SpringbootApp.java:12)
-
刚才说了matches方法中的两个参数的作用,其中一个是注解的元对象,我们可以根据这个将案例进行一个升级,自定义注解
-
定义RedisConditional注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ConditionalConfig.class)
public @interface RedisConditional {
// 在自定义注解上加入 @Conditional(ConditionalConfig.class) 让此类具有同样的功能
String[] value(); // 定义注解参数 这里我们使用数组。
}
- 修改实现的matches方法
public class ConditionalConfig implements Condition {
/**
*
* @param conditionContext 作用: 获取 Bean工厂 ClassLoad Class 环境变量等
* @param annotatedTypeMetadata 作用: 注解元对象
* @return
*/
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
boolean flag;
/**
* annotatedTypeMetadata.getAnnotationAttributes 方法可以获取注解的属性值
* 参数 为注解的全限定名 这里我们用类的Class属性直接获取
* 返回的map 结构为 {value=[]} vaule为固定key 我们直接通过value获取属性即可
*/
Map<String, Object> conditional =
annotatedTypeMetadata.getAnnotationAttributes(RedisConditional.class.getName());
//我们定义的属性值为数组 返回值为Object 强转为数组
String[] value = (String[]) conditional.get("value");
try {
for (String s : value) {
Class<?> aClass = Class.forName(s);
System.out.println(aClass + " 加载成功");
}
flag = true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
flag = false;
}
return flag;
}
}
- 修改RedisConfigurationTest的注解
@Configuration
public class RedisConfigurationTest {
@Bean
@RedisConditional({"redis.clients.jedis.Jedis","redis.clients.jedis.BinaryClient"})
public RedisTest redisTest(){
return new RedisTest();
}
}
这里传入了两个参数 ,将刚才的注释的jedis坐标在加上
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
- 启动
2020-02-18 13:44:44.379 INFO 19856 --- [ main] com.ips.springbootapp.SpringbootApp : Starting SpringbootApp on DESKTOP-KT61VF7 with PID 19856 (B:\designMode\springbootapp\target\classes started by 92862 in B:\designMode\Decorator)
2020-02-18 13:44:44.381 INFO 19856 --- [ main] com.ips.springbootapp.SpringbootApp : No active profile set, falling back to default profiles: default
class redis.clients.jedis.Jedis 加载成功
class redis.clients.jedis.BinaryClient 加载成功
2020-02-18 13:44:44.733 INFO 19856 --- [ main] com.ips.springbootapp.SpringbootApp : Started SpringbootApp in 0.555 seconds (JVM running for 1.158)
com.ips.springbootapp.doamin.RedisTest@1b45c0e
- 传入的两个类都加载出来了 这就是@Conditional 注解
- 这里我们使用了四个类分别是
RedisTest实体类
RedisConfigurationTest 配置类 将RedisTest注入IOC容器
ConditionConfig 注解配置类 实现Condition接口 自定义matches规则
RedisConditional 自定义注解类 定义注解参数为String 数组
我们要把RedisTest注入IOC容器,根据规则当容器中存在jedis时,我才进行注入操作,接着我自定义了注解RedisConditional,并引入了 @Condition注解让他拥有与 @Condition注解相同的行为,而引入的 @Condition注解使用了ConditionConfig注解配置类作为判断依据,ConditionConfig类则是实现了Condition接口 重写了matches方法 ,在matches方法中我们使用了AnnotatedTypeMetadata 注解元对象的getAnnotationAttributes方法,通过这个方法可以获取指定注解的属性值,我们选取了自己自定义的注解RedisConditional,并获取他的属性值,来作为matches返回参数的规则。
- 接下来我们去看一下SpringBoot是怎么使用@Conditional注解的
- 我们关注 这么一个类
这个与我们定义的RedisConfigurationTest配置类是不是很像 作用是将一些对象注入到IOC容器中,只不过他注入的是redisTemplate 指定了Bean的名字,而且它不仅在方法上加入了@ConditionalOnMissingBean 注解还在类上加入了ConditionalOnClass 做判断。
ps @ConditionalOnClass 是指在当前的classpath下如果有指定的某个类 就加载当前类
@ConditionalOnMissingBean(name = "redisTemplate") 是如果不存在redisTemplate这个bean 就注入
接着我们在去当前jar报下的 另一个包中condition包中
-
他定义了非常多的@Conditional 注解 我们就选择一个刚才看到的@ConditionalOnClass 看一下,其他的都与之类似
-
spring自定义的注解里面 也是使用了@Conditional注解,是不是跟我们的自定义注解类一样
-
他使用的注解配置类,大家感兴趣可以自己进入看一下,他 的调用链比较长,不过最后也是实现了Condition接口,(嘿嘿,spring跟我写的一样!!!)
-
所以在SpringApplication一启动的时候 我只要引入了坐标,他就去读取是否有这个类 读取到了 就加载相应的bean
-
我们在举个例子,大家都知道springboot内置了四个服务器 但是为什么springboot引入sping-boot-starter-web的时候就默认加载了tomcat呢
-
我们继续在当前包下找到web包
-
we包下有个embedded 看包名我们也能猜到,内置.
-
下面有五个类,其中四个看名字我们也就知道了他是干嘛的,我们关注第一个。
-
里面有四段注入bean的方法 篇幅原因,我将里面tomcat单独贴出来(
/**
* Nested configuration if Tomcat is being used.
* 如果有tomcat.class 就注入
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
- 其他的几个方法 分别对应 jetty netty undertow
- 我们引入spring-boot-starter-web依赖,进入内部查看一下,不出所料应该是加入了tomcat的依赖,所以在@Conditional 条件判断的时候选择加载了tomcat
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
果然如此
- 那么根据@Conditional 条件判断 我们只要剔除tomcat引入别的容器依赖 即可完成内置服务器的切换。 实践一下,剔除tomcat依赖,引入jetty
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>
- 启动
- 我们了解了Condition条件之后 知道了Springboot选择性的加载bean,那他到底是如何加载的呢,这里要看@Enable注解了
@Enable 注解
- 我们在使用SpringBoot技术栈进行开发的时候,肯定会遇到很多以Enable开头的注解。例如
@EnableAsync
@EnableDiscoveryClient
@EnableFeignClients
...
- Enable是动态的加载某些功能,其在内部使用的是@Import注解,是依据@Import的注解动态的导入配置类,实现bean的动态创建。
- 这个注解我们也用一个案例来演示。
案例
- 创建enable工程,enable-t工程
- enable-t工程中定义实体EnableTest,配置类EnableConfiguration
- EnableTest
public class EnableTest {
}
- EnableConfiguration
@Configuration
public class EnableConfiguration {
@Bean
public EnableTest enableTest() {
return new EnableTest();
}
}
- pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
- 工程enable 引入 工程enable-t 坐标
- pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.ips</groupId>
<artifactId>enable-t</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- 在main方法中获取 enable-t工程 注入的bean,直接拿肯定报错,不再演示了,一般我们需要加入包扫描
@SpringBootApplication
@ComponentScan("com.ips.enablet.config") // 扫描指定类当前包及其子包
public class EnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
Object enableTest = context.getBean("enableTest");
System.out.println(enableTest);
}
}
- 启动 enable工程,加载成功
- 除了包扫描 还能使用@Import注解
@SpringBootApplication
//@ComponentScan("com.ips.enablet.config")
@Import(EnableConfiguration.class)
public class EnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
Object enableTest = context.getBean("enableTest");
System.out.println(enableTest);
}
}
运行结果
- 这样做的前提是我们要知道目标类名或是包名才行,但是像redis这些框架他是不会把自己的这些信息给我们的。
- enable 工程 自定义Enable注解
- 注解类 EnableTConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfiguration.class)
public @interface EnableTConfiguration {
}
- main方法改用自定义Enable注解
@SpringBootApplication
//@ComponentScan("com.ips.enablet.config")
//@Import(EnableConfiguration.class)
@EnableTConfiguration
public class EnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
Object enableTest = context.getBean("enableTest");
System.out.println(enableTest);
}
}
启动
这是不是跟我们使用@Enable* 这些注解很像。
@Import注解
- 在@SpringBootApplication注解中,他也有一个@Enable注解,而我们刚才说了Enable注解内部其实是使用了@Import注解来动态导入配置。下面我们进入@SpringBootApplication的注解内部一探究竟,看看Spring的@Enable是如何使用@Import的
- 他在这里使用的AutoConfigurationImportSelector 与我们 自己定义的EnableConfiguration 作用都是引入目标配置类
-
只不过他的配置类内容会更丰富一些,我们进去看看
-
AutoConfigurationImportSelector 实现了一堆接口 ,重写了很多方法,其中selectImports()是SpringBoot动态加载配置最关键的方法,这个方法来自DeferredImportSelector接口,而DeferredImportSelector接口则继承了ImportSelector。
-
关于ImportSelector。接口我们就不进去看了。我们来看一下 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() 得到了AutoConfigurationMetadataLoader对象 最后将AutoConfigurationMetadataLoader的getConfigurations() 转换为数组返回
AutoConfigurationMetadataLoader 是AutoConfigurationImportSelector 的一个静态内部类
他内置了两个数据类型的变量分别为:
List<String> configurations;
Set<String> exclusions;
而selectImports()内则是将AutoConfigurationImportSelector 的configurations变量转换为数组返回,而AutoConfigurationImportSelector 来自于getAutoConfigurationEntry方法
- getAutoConfigurationEntry()
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
getAutoConfigurationEntry 接收的两个参数
@param1 AutoConfigurationMetadata 注解配置类元数据对象 获取注解配置属性
@param2 AnnotationMetadata 注解元数据对象 获取注解属性
返回参数 AutoConfigurationEntry对象 其内部configurations属性 包含了需要自动导入的配置类
而AutoConfigurationEntry的configurations属性 来源于 getCandidateConfigurations方法
- getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
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;
}
- getCandidateConfigurations获取的参数与getAutoConfigurationEntry一致,抛开这些,我们关注自己需要的部分
AutoConfigurationEntry的configurations属性 来自于 SpringFactoriesLoader.loadFactoryNames 方法
在这个方法中,他首先拿到了SpringFactoriesLoaderFactoryClass的class对象,在由class对象的Resource方法 读取 META-INF/spring.factories 下的属性配置类。
如果该对象为null,会直接使用ClassLoad.getSystemResources 方法读取 META-INF/spring.factories 下的属性配置类。 并返回一个list集合
Assert.notEmpty 如果判断的参数不为空则直接返回,否者返回错误信息,根据信息我们也能了解到他是去读取了 META-INF/spring.factories 下的内容
- 综上来看selectImports() 方法返回的配置类资源是读取自 META-INF/spring.factories。这中间经历了很多去重,筛选需要的配置类等操作,感兴趣的可以自己去研究一下。
- META-INF/spring.factories 中 这个META-INF是不是很熟悉,前面我们在翻看springboot的jar包时,是有见过的。我们再回去翻看一下jar包下的META-INF下是不是有个spring.factories ,他里面是些什么内容。
- 加载的原来是这些内容 ,找几个熟悉的看一下
- redisAutoConfiguration,Mongolian,jdbc是不是很熟悉,我们再看一下data包下的redisAutoConfiguration
他就是一开始我们看到的,用来动态加载redisTemplate的配置类,我们来过一下整体的步骤
- 在AutoConfigurationImportSelector使用getCandidateConfigurations方法 获取META-INF/spring.factories 中的配置信息然后返回给
getAutoConfigurationEntry方法 接着返回给selectImports并将其通过StringUtils.toStringArray 转化成数组返回。 - 通过@Import注解导入AutoConfigurationImportSelector.class得到返回的数组内容。
- 而@Import注解又作用在@EnableAutoConfiguration注解类
- @EnableAutoConfiguration注解又作用在@SpringBootApplication
- 所以在我们启动的时候通过@SpringBootApplication 将内容都加载了进来。
看了SpringBoot的@Import注解的玩法,我们可以把刚才的demo升级一下
- enable工程定义@Import配置类
- ImportConfigure (springboot是读取配置信息,这里我直接写死)
public class ImportConfigure implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{"com.ips.enablet.config.EnableConfiguration"};
}
}
- 修改EnableTConfiguration 注解类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ImportConfigure.class) //改为Import注解类
public @interface EnableTConfiguration {
}
- main 方法
@SpringBootApplication
//@ComponentScan("com.ips.enablet.config")
//@Import(EnableConfiguration.class)
@EnableTConfiguration
public class EnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
Object enableTest = context.getBean("enableTest");
System.out.println(enableTest);
}
}
启动
读取成功
自定义starter
springboot的自动装配仅限于与springboot整合的内容,通常我们单独引入某个框架的坐标时,需要自己手动注入bean,这里不在给大家演示。
-
我们不难发现,与springboot整合的框架,他们的坐标都均以spring-boot-starter开头,但也有例外,比如mybatis。这是因为以spring-boot-starter开头的坐标都是spring自己整合的,而mybatis是他自己自定义的starter。我们来看一下mybatis自己的starter的如何处理的。
-
引入mybatis的坐标 查看mybatis源码包
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
- 它定义了两个包
mybatis-spring-boot-starter
mybatis-spring-boot-autoconfigure
并在starter的pom中依赖了 autoconfigure
-
我们再来看一下mybatis-spring-boot-autoconfigure
-
MEAT-INF下有个spring.factories,@SpringBootApplication会从这里获取配置类,这两个AutoConfiguration我们选择一个进入
-
我将部分源码 贴在下面
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
private final MybatisProperties properties;
private final Interceptor[] interceptors;
...
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
...
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
- 这个配置类就是用来动态的创建bean的,与我们先前看到的RedisAutoConfiguration是一样的作用。
- 在这类上他也有一个Enable注解 @EnableConfigurationProperties({MybatisProperties.class})
- 我们进入MybatisProperties.class看一下
@ConfigurationProperties(
prefix = "mybatis"
)
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
private String configLocation;
private String[] mapperLocations;
private String typeAliasesPackage;
- 哦 他是用来加载以mybatis开头的属性的,我们在使用yml的时候,设置属性会以特定的格式开头,比如端口是 server,redis的是spring.redis。spring他也是同样在这里设置了prefix 前缀,我将server,redis配置源码贴在下面。
- server属性配置
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
/**
* Server HTTP port.
*/
private Integer port;
/**
* Network address to which the server should bind.
*/
private InetAddress address;
@NestedConfigurationProperty
private final ErrorProperties error = new ErrorProperties();
- spring.redis
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
/**
* Database index used by the connection factory.
*/
private int database = 0;
/**
* Connection URL. Overrides host, port, and password. User is ignored. Example:
* redis://user:[email protected]:6379
*/
private String url;
/**
* Redis server host.
*/
private String host = "localhost";
- 接下来我们就照着mybatis的starter, 自定义一个starter,为了演示方便,这里还是选择redis来操作。
- 创建工程
redisips-spring-boot-starter
redisips-spring-boot-autoconfigure
- redisips-spring-boot-autoconfigure的pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
- redisips-spring-boot-starter的pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.ips</groupId>
<artifactId>redisips-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- 按照mybatis的starter ,redisips-spring-boot-autoconfigure工程需要一个驱动配置类,和属性配置类,再加上spring的@Conditional注解
- redisips-spring-boot-autoconfigure
- 属性配置类RedisIpsProperties
// 我们选择加载 以redisips 开头的内容
@ConfigurationProperties(prefix = "redisips")
public class RedisIpsProperties {
// 设置默认地址和端口
private String host="localhost";
private int port=6379;
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;
}
}
- 驱动配置类 RedisIpsConfiguration 这里为了验证生效我们输出一段文字
@Configuration
@ConditionalOnClass(Jedis.class)
@EnableConfigurationProperties(RedisIpsProperties.class)
public class RedisIpsConfiguration {
@Bean(name="jedisIps")
@ConditionalOnMissingBean(name = "jedisIps")
public Jedis jedis(RedisIpsProperties redisIpsProperties){
System.out.println("jedis 正在注入 IOC 。。。");
return new Jedis(redisIpsProperties.getHost(),redisIpsProperties.getPort());
}
}
- 我们还需要META-INF/spring.factories 来加载我们的驱动配置类
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ips.redisipsspringbootautoconfigure.config.RedisIpsConfiguration
- redisips-spring-boot-starter工程
- 只需要依赖autoconfiguration
- 创建测试 工程 引用自定义的starter。这里不在打jar包演示,直接依赖
-
测试工程我也是直接用了以前创建的的springbootapp
-
pom 注意引入的依赖是我们自己的 redisips-spring-boot-starter
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.ips</groupId>
<artifactId>redisips-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- 工程结构 ,之前的测试类全部需要干掉
- 启动本地redis测试
- 测试工程的main方法
@SpringBootApplication
public class SpringbootApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootApp.class, args);
Jedis jedisIps = (Jedis) context.getBean("jedisIps");
jedisIps.set("key","jedisIps");
String value = jedisIps.get("key");
System.out.println("jedi获取成功 :"+value);
}
}
- 测试结果
成功,确实走了我们的RedisIpsConfiguration ,我们再RedisIpsConfiguration 上面加入了@ConditionalOnMissingBean ,接下来我们再测试类启动时注入bean,看@ConditionalOnMissingBean 注解是否生效
- main方法 注入bean
@SpringBootApplication
public class SpringbootApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootApp.class, args);
Jedis jedisIps = (Jedis) context.getBean("jedisIps");
jedisIps.set("key","jedisIps");
String value = jedisIps.get("key");
System.out.println("jedi获取成功 :"+value);
}
@Bean(name = "jedisIps")
public Jedis jedis(){
return new Jedis();
}
}
-
启动
验证成功 -
我们还设置了读取以redisips开头的配置,去除测试类中的bean注入,下面来测试一下配置类读取
- 启动测试
读取了配置没有问题。
以上的案例均是直接照着SpringBoot框架抄的,嘿嘿。感谢springboot的大力支持 ^ _ ^
一般我们再使用的时候都是直接引入依赖,却不知道他里面是如何操作的,用的时候都是心慌慌啊,现在知道了SpringBoot是如何通过依赖创建bean的,心里有底多了。