文章目录
- 参考资料
- 运行环境
- SpringBoot 自动配置原理
- 一、Condition
- 二、切换内置web服务器
- 三、Enable*注解
- 四、Import 注解
- 五、@EnableAutoConfiguration 注解
- 六、案例
- 七、收获与总结
参考资料
视频链接
运行环境
- win10
- IDEA专业版
- SpringBoot 2.6.2
SpringBoot 自动配置原理
一、Condition
Condition 是在 Spring 4.0 增加的条件判断功能,作用是实现选择性的创建Bean操作
思考:
SpringBoot是如何知道要创建哪个 Bean的?比如SpringBoot是如何知道要创建 RedisTemplate的?
需求:
在Spring的IOC容器中有一个User的Bean,现要求
0x001 导入 Jedis 坐标后,加载该Bean,没导入则不加载
测试的项目结构
User.java
package com.uni.springbootcondition.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String username;
private String password;
}
ClassCondition.java
package com.uni.springbootcondition.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class ClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean flag = true;
try {
Class<?> cls = Class.forName("redis.clients.jedis.Jedis");
} catch (ClassNotFoundException e) {
e.printStackTrace();
flag = false;
}
return flag;
}
}
UserConfig.java
package com.uni.springbootcondition.config;
import com.uni.springbootcondition.condition.ClassCondition;
import com.uni.springbootcondition.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
@Conditional(ClassCondition.class)
public User user(){
return new User();
}
}
SpringbootConditionApplication.java
package com.uni.springbootcondition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class SpringbootConditionApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args);
Object user = context.getBean("user");
System.out.println(user);
}
}
0x010 将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定。
项目结构:
ClassCondition.java
package com.uni.springbootcondition.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.Map;
public class ClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
String[] value = (String[])map.get("value");
boolean flag = true;
try {
for (String className : value) {
Class<?> cls = Class.forName(className);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
flag = false;
}
return flag;
}
}
ConditionOnClass.java
package com.uni.springbootcondition.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ClassCondition.class)
public @interface ConditionOnClass {
String[] value();
}
UserConfig.java
package com.uni.springbootcondition.config;
import com.uni.springbootcondition.condition.ClassCondition;
import com.uni.springbootcondition.condition.ConditionOnClass;
import com.uni.springbootcondition.domain.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
@ConditionOnClass("redis.clients.jedis.Jedis")
public User user(){
return new User();
}
}
其实在SpringBoot中,自动实现了之前的需求
源码:ConditionalOnBean.java
package org.springframework.boot.autoconfigure.condition;
import java.lang.annotation.Annotation;
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;
import org.springframework.context.annotation.Conditional;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnBeanCondition.class})
public @interface ConditionalOnBean {
Class<?>[] value() default {};
String[] type() default {};
Class<? extends Annotation>[] annotation() default {};
String[] name() default {};
SearchStrategy search() default SearchStrategy.ALL;
Class<?>[] parameterizedContainer() default {};
}
当 Spring IOC容器 存在某个Bean时才执行
比如 Redis的自动配置功能,源码org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguraion.java
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
@ConditionalOnMissingBean表示当某个Bean不存在时,源码中第3行,将name属性值指定为redisTemplate
在第13行也有用到,就是不存在的话就创建一个Redis实例,通过使用注解实现对Bean是否存在的判断,若不存在就进行创建对象的处理,从而实现了自动配置
0x011 小结
-
自定义条件
- 定义条件类:自定义实现 Condition 接口,重写 matches 方法, 在matches方法中进行逻辑判断,返回boolean值。 matches方法的两个参数:1) context:上下文对象,可以获取属性值,获取类加载器,获取BeanFactory等;2) metada:元数据对象,用于获取注解属性
- 判断条件:在初始化Bean时,使用
@Conditional(条件类.class)
注解
-
SpringBoot 提供的常用条件注解
注解名 |
描述 |
ConditionalOnProperty |
判断配置文件中是否有对应属性和值才初始化Bean |
ConditionalOnClass |
判断环境中是否有对应字节码文件才初始化Bean |
ConditionalOnMissingBean |
判断环境中没有对应Bean才初始化Bean |
二、切换内置web服务器
源码位置:org.springframework.boot.autoconfigure.web.embedded
EmbeddedWebServerFactoryCustomizerAutoConfiguration.java
源码中自动配置Tomcat部署的实现
@ConditionalOnClass({Tomcat.class, UpgradeProtocol.class})
public static class TomcatWebServerFactoryCustomizerConfiguration {
public TomcatWebServerFactoryCustomizerConfiguration() {
}
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
可以观察到它用到了 @ConditionalOnClass 注解,就是当存在Tomcat的类还有另外一个UpgradeProtocol的类才会进行配置,其他的几个部署模式也是这样的。
所以在SpringBoot中切换内置web服务器,只需要修改pom.xml
中的依赖,排除默认的Tomcat包,添加其他的包比如Jetty,就完成了。
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除 Tomcat 依赖 -->
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 Jetty的 依赖 -->
<dependency>
<artifactId>spring-boot-starter-jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
开启SpringBoot后,通过查看启动信息可以发现,内置的web服务器切换成功
三、Enable*注解
SpringBoot 提供了很多 Enable开头的注解,这些注解都是用于动态启动某些功能的。而其底层原理是使用@Import注解导入一些配置类,实现Bean的动态加载。
思考:SpringBoot工程是否可以直接获取Jar包中定义的Bean?
测试的项目结构
其中 springboot-enable-other用于模拟其他jar包,先在其放到enable项目内部,类似于给项目导入jar包,然后在到外面的SpringBoot项目里获取里面项目的Bean,测试看看能否获取到。
UserConfig.java
package com.uni.domain;
public class User {
}
EnableUser.java
模拟一个EnableUser注解,用于导入第三方包User的Bean
package com.uni.config;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)
public @interface EnableUser {
}
SpringBootEnableAction.java
package com.uni.springbootenable;
import com.uni.config.EnableUser;
import com.uni.config.UserConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@EnableUser
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
Object user = context.getBean("user");
System.out.println(user);
}
}
在@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}
)}
其中的 @EnableAutoConfiguration 则实现了自动配置
这体现了之前 能直接获取 redisTemplate Bean的原因,在pom.xml中导入 redis后,改包提供了类似于之前手写的@EnableUser注解,注解中用到了@Import注解,直接将类注入到了Spring的IOC容器,从而使得SpringBoot能识别到对应的类
四、Import 注解
@Enable*底层依赖于 @Import注解导入一些类 ,使用@Import导入的类会被Spring加载到IOC容器中。而@Import提供四种用法
测试的项目结构:
EnableUser.java
package com.uni.config;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)
public @interface EnableUser {
}
MyImportBeanDefinationRegister.java
package com.uni.config;
import com.uni.domain.User;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
registry.registerBeanDefinition("user", beanDefinition);
}
}
MyImportSelector.java
package com.uni.config;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.uni.domain.User", "com.uni.domain.Role"};
}
}
UserConfig.java
package com.uni.config;
import com.uni.domain.Role;
import com.uni.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
public User user(){
return new User();
}
@Bean
public Role role(){return new Role();}
}
SpringbootEnableApplication.java
package com.uni.springbootenable;
import com.uni.config.EnableUser;
import com.uni.config.MyImportBeanDefinitionRegistrar;
import com.uni.config.MyImportSelector;
import com.uni.config.UserConfig;
import com.uni.domain.Role;
import com.uni.domain.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import java.util.Map;
@SpringBootApplication
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
System.out.println(context.getBean(User.class));
}
}
@Import 注解的四种用法
- 导入Bean
- 导入配置类
- 导入ImportSelector的实现类
- 导入ImportBeanDefinitionRegistrar 类
SpringBoot启动类注解 @SpringBootApplication 是通过上面来实现导入其他类的,步骤依次为:
五、@EnableAutoConfiguration 注解
- @EnableAutoConfiguration 注解内部使用 @import(AutoConfigurationImportSelector.class) 来加载配置类
- 配置文件位置:
META-INF/spring.factories
, 改配置文件中定义了大量的配置类,当SpringBoot应用启动时,会自动加载这些配置类,初始化Bean
一层一层地查找源码:
通过源码的提示可以看到,自动加载类的配置文件在 META-INF/spring.factories
的位置,现在找到org.springframework.boot.autoconfigure
中的spring-boot-autoconfigure-2.5.6.jar
可以找到存在 META-INF文件夹,而里面就有一个 spring.factories
的文件,如下图
spring.factories
部分内容如下
可以发现它包含了Redis类,从而就加载到了容器中,这就是为什么SpringBoot可以直接获取到redisTemplate的原因
六、案例
需求:自定义 redis-starter。 要求当导入 redis坐标时,SpringBoot自动创建Jedis的Bean
导入 MyBatis起步依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
观察原生的jar包实现,里面会提供具体的自动配置类,以及供SpringBoot扫描的spring.factories
实现步骤:
- 创建
redis-spring-boot-autoconfigure
模块
- 创建
redis-spring-boot-starter
模块, 依赖 redis-spring-boot-autoconfigure
的模块
- 在
redis-spring-boot-autoconfigure
模块中初始化 Jedis 的 Bean。并定义 META-INF/spring.factories
文件
- 在测试模块中引入自定义的redis-starer依赖,测试获取Jedis的Bean,操作Redis
项目结构
在负责起步依赖的redis-spring-boot-starter模块中,只负责在pom.xml里添加confguration模块的依赖(注:该模块的test类需删除,否则会报错)
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 引入 configuation -->
<dependency>
<groupId>com.uni</groupId>
<artifactId>redis-spring-boot-configuration</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
在 redis-spring-boot-configuration模块中,负责配置 jedis 模拟 Redis的自动配置(注:该模块的test类需删除,否则会报错)
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 添加 Jedis 依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
RedisProperties.java
用于Redis连接的配置实体类,将作为Bean注入到Spring 容器中
package com.uni.redis.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix="redis")
public class RedisProperties {
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;
}
}
RedisAutoConfiguration.java
根据之前注入的redisProperties配置Bean来创建一个Jedis Bean 并注入到容器中
package com.uni.redis.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@ConditionalOnClass(Jedis.class)
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "jedis")
public Jedis jedis(RedisProperties redisProperties){
System.out.println("RedisAutoConfiguration...");
return new Jedis(redisProperties.getHost(), redisProperties.getPort());
}
}
根据之前的源码查找,在声明自动配置类后,需要在resources/META-INF下创建一个spring.factories
配置文件,用于声明实现了自动扫描的配置类
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.uni.redis.config.RedisAutoConfiguration
至此,已完成了模拟Redis自动配置的模块,现在到另一个SpringBoot模块中,引入实现的redis-spring-boot-starter
起步依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- 自定义的 Redis的starter -->
<dependency>
<groupId>com.uni</groupId>
<artifactId>redis-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
SpringbootTestApplication.java
package com.uni.springboottest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import redis.clients.jedis.Jedis;
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootTestApplication.class, args);
Jedis jedis = context.getBean(Jedis.class);
jedis.set("role", "uni");
String role = jedis.get("role");
System.out.println(role);
}
}
测试结果:
七、收获与总结
又一次温习了SpringBoot是提供了快速开发Spring项目的作用而不是增强功能。
本次写了好几个Demo,跟着视频教程,使用注解去实现Bean的注入,以及起步依赖之类的,最主要的还是第一次查看了SpringBoot启动类的注解,了解到里面自动配置的一些机制,第一次了解到可以将类作为参数传给一个注解。
本次内容比较多,目前笔者没有完全吸收,不过笔者觉得这一次的学习能有助于SpringBoot的使用,至少在将来写项目的时候,可以配置好依赖。