SpringBoot自动配置
一、Condition
Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建Bean操作。
思考:比如 SpringBoot 是如何知道要创建哪个 Bean 的?比如 SpringBoot 是如何知道要创建 RedisTemplate 的?
背景(结合上面的思考)
- 创建一个 SpringBoot项目,不整合框架
@SpringBootApplication
public class SpringbootConditionApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args);
//获取 bean
Object redisTemplate = context.getBean("redisTemplate");
System.out.println(redisTemplate);
}
}
此时会报错
No bean named 'redisTemplate' available
- 导入 redis 的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
再次启动发现没有报错,说明我们找到了 bean
实例一
在 Spring 的 IOC 容器中有一个 User 的 Bean,现要求:
1.导入 Jedis 坐标厚,加载该 Bean,没导入,则不加载
1. 创建一个 User 实体类
public class User {
}
2. 创建一个配置类 UserConfig
@Configuration
public class UserConfig {
@Bean
public User user() {
return new User();
}
}
3. 在启动类中获取user 的 bean
@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);
}
}
此时可以获取到容器中定义的 bean>user
4. 在创建 bean 的时候使用注解@Conditional
首先我们要知道@Conditional 这个注解的作用是什么
@Conditional
注解是 Spring Framework 提供的一个条件注解,用于根据指定条件决定是否创建某个 Bean 或者应用某个配置。具体来说,@Conditional
注解的作用是根据指定的条件判断是否满足,若条件满足则才会创建相应的 Bean 或者应用相应的配置。 使用@Conditional
注解可以实现一些条件化的 Bean 创建或者配置,例如基于环境、系统属性、配置值等进行条件判断。这样可以根据不同的条件,灵活地控制 Spring 应用中的 Bean 创建和配置。
1️⃣ 创建条件类实现条件接口
package cn.hxy.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) {
return false;
}
}
2️⃣ 使用条件类
@Configuration
public class UserConfig {
@Bean
@Conditional(ClassCondition.class)
public User user() {
return new User();
}
}
@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);
}
}
此时当我们启动时,会发现找不到 user 的 bean,因为此时的 condition 的条件对象返回的是 false
5. 根据是否导入Jedis 判断是否让条件类返回 true
在条件类中判断 Jedis 是否导入坐标
package cn.hxy.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 {
//根据路径去查询文件类型,如果不存在就会报错,然后异常被捕获,执行 catch 中的代码
Class<?> aClass = Class.forName("redis.clients.jedis.Jedis");
} catch (ClassNotFoundException e) {
flag = false;
}
return flag;
}
}
如果导入了,此时条件类返回为 true,那么@conditional 注解满足条件,执行对应的 bean 创建注入,在启动时打印user成功
如果没有导入对应的 Jedis 坐标,那么 ClassCondition 中的判断结果为 false,则 bean 不会被创建注入,在启动时打印 user 找不到 bean
实例二
将类的判断定义为动态的.判断哪个字节码文件存在可以动态指定.
1. 定义一个注解 ConditionOnClass
将我们之前的 conditional 注解放在自定义的注解上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ClassCondition.class)
public @interface ClassOnCondition {
String[] value();
}
2. 在要创建 bean 上使用我们自定义的注解判断
package cn.hxy.springbootcondition.config;
import cn.hxy.springbootcondition.condition.ClassOnCondition;
import cn.hxy.springbootcondition.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
@ClassOnCondition("redis.clients.jedis.Jedis")
public User user() {
return new User();
}
}
3. 修改我们的ClassCondition 条件类
package cn.hxy.springbootcondition.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.Map;
/**
* context 上下文对象,用于获取环境、ioc 容器、classLoader 对象
* metadata 注解元对象,可以用于获取注解定义的属性值
*/
public class ClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取注解属性值 value
Map<String, Object> map = metadata.getAnnotationAttributes(ClassOnCondition.class.getName());
System.out.println(map);
//可以指定多个属性值进行判断
String[] value = (String[]) map.get("value");
boolean flag = true;
try {
for (String s : value) {
//根据路径去查询文件类型,如果不存在就会报错,然后异常被捕获,执行 catch 中的代码
Class<?> aClass = Class.forName(s);
}
} catch (ClassNotFoundException e) {
flag = false;
}
return flag;
}
}
4. 修改我们的 pom 导入新的坐标测试
导入 fastjson 坐标进行测试
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.9.graal</version>
</dependency>
在创建 user 的 bean时使用自定义注解传入坐标
@Configuration
public class UserConfig {
@Bean
@ClassOnCondition("com.alibaba.fastjson.JSON")
public User user() {
return new User();
}
}
此时如果 fastjson 的坐标导入了,那么我们自定义注解中使用的自定义条件类判断的结果为 true,那么创建 user 的 bean。 否则不创建 user 的 bean,打印 user 失败,找不到 bean
那么回到我们的思考:SpringBoot 是如何知道要创建哪个 Bean 的?其实就是通过我们的 Condition来判断,判断当前环境中有没有导入我们的redis,如果导入了,就帮我们创建 RedisTemplate
其实在我们的 Springboot 中已经帮我们定义好了很多 condition 的注解,我们只需要去按照不同功能进行使用即可
@Bean
@ConditionalOnBean(DataSource.class)
public User user1() {
return new User();
}
例如,当我们要创建一个 user1
的 bean 时,只有当容器中存在 DataSource
类型的 Bean 时,user1
的 Bean 才会被创建注册到容器中。
二、切换内置web服务器
SpringBoot的web环境中默认使用tomcat作为内置服务器,其实SpringBoot提供了4中内置服务器供我们选择,我们可以很方便的进行切换。
当我们没有导入 web依赖时可以发现启动项目并没有一直运行,而是在导入了 web 依赖之后,有了 web 提供的 tomcat 服务器,程序才会一直运行
其实在这里依然使用了我们的 condition条件来判断加载什么类型的内置服务器,当我们导入 web 时默认加载的是 tomcat 服务器,也可以替换为jetty、netty 的服务器
如下面的代码,是我们springboot 包中的,用来判断加载什么类型服务器的底层代码,当我们导入的是 web 默认依赖时,加载 tomacat 服务器,当我们在导入依赖时进行了切换,则可以使用其他的内置服务器
该文件路径:
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.boot.autoconfigure.web.embedded;
import io.undertow.Undertow;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.UpgradeProtocol;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Loader;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWarDeployment;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.xnio.SslClientAuthMode;
import reactor.netty.http.server.HttpServer;
@AutoConfiguration
@ConditionalOnNotWarDeployment
@ConditionalOnWebApplication
@EnableConfigurationProperties({ServerProperties.class})
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
public EmbeddedWebServerFactoryCustomizerAutoConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({HttpServer.class})
public static class NettyWebServerFactoryCustomizerConfiguration {
public NettyWebServerFactoryCustomizerConfiguration() {
}
@Bean
public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new NettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Undertow.class, SslClientAuthMode.class})
public static class UndertowWebServerFactoryCustomizerConfiguration {
public UndertowWebServerFactoryCustomizerConfiguration() {
}
@Bean
public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new UndertowWebServerFactoryCustomizer(environment, serverProperties);
}
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Server.class, Loader.class, WebAppContext.class})
public static class JettyWebServerFactoryCustomizerConfiguration {
public JettyWebServerFactoryCustomizerConfiguration() {
}
@Bean
public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new JettyWebServerFactoryCustomizer(environment, serverProperties);
}
@Bean
@ConditionalOnThreading(Threading.VIRTUAL)
JettyVirtualThreadsWebServerFactoryCustomizer jettyVirtualThreadsWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new JettyVirtualThreadsWebServerFactoryCustomizer(serverProperties);
}
}
@Configuration(
proxyBeanMethods = false
)
//判断是否存在 tomcat 的类,如果存在,则使用 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);
}
@Bean
@ConditionalOnThreading(Threading.VIRTUAL)
TomcatVirtualThreadsWebServerFactoryCustomizer tomcatVirtualThreadsProtocolHandlerCustomizer() {
return new TomcatVirtualThreadsWebServerFactoryCustomizer();
}
}
}
其实在这里我们也可以通过 idea 查看当前项目的依赖关系,如图,web 依赖默认使用的是 tomcat 作为内置服务器
我们可以在 pom.xml 文件中进行切换
<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>
此时,我们排除了 tomcat 的依赖,引入了 jetty 的依赖,conditionalOnClass 就会去判断对应的类型是否存在以此来加载对应的内置服务器。
三、@Enable*注解
其实我们的自动配置就是依赖于
@EnableAutoConfiguration
注解 SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用@Import 解导入一些配置类,实现Bean的动态加载。
springboot工程是否可以直接获取 jar 包中定义的 bean 呢?不能
因为在@SpringBootApplication这个注解中有一个@ComponentScan
注解,它的扫描范围:当前引导类所在包及其子包
针对上面包的扫描问题,我们可以手动的使用@ComponentScan 进行扫描,但是不方便,这时候就可以使用@Import 注解加载类,这些类都会被 SpringBoot 创建并且放入 IOC 容器中,如下面实例
@ComponentScan
1.创建2个空的 SpringBoot 项目
第一个项目相当于我们自己的项目,第二个项目相当于第三方的 jar 包
在第二个项目中创建一个 User 的配置类获取 Bean
@Configuration
public class UserConfig {
@Bean
public User user() {
return new User();
}
}
在第一个项目pom.xml中导入第二个项目的依赖
<dependency>
<groupId>cn.hxy</groupId>
<artifactId>springboot-enable-other</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
在第一个项目的引导类中获取 bean 并打印
@SpringBootApplication
@ComponentScan("cn.hxy.springbootenableother")//扫描 user bean 的包路径
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
Object user = context.getBean("user");
System.out.println(user);
}
}
此时如果没有使用 @ComponentScan 扫描第二个项目中的 User 配置类的包路径的话,会报错找不到 user 的 Bean,如果添加了则可以找到。但是难道每一个项目我们都要去写一次@ComponentScan 吗,很麻烦,所以我们可以使用@Import 注解
@Import
在第一个项目的引导类上一样的使用注解
@SpringBootApplication
@Import(UserConfig.class)//通过 bean 的类型自动引入
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
Object user = context.getBean("user");
System.out.println(user);
}
}
但是在这里还是不够方便,我们需要去记住引入的类的UserConfig配置类
对@Import 注解进行封装
在第二个项目中定义一个注解类
package cn.hxy.springbootenableother;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)
public @interface EnableUser {
}
在第一个项目中使用定义的注解直接引入
@SpringBootApplication
@EnableUser
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
Object user = context.getBean("user");
System.out.println(user);
}
}
四、@Import注解
@Enable*底层依赖于@Import注解导入一些类,使用@lmport导入的类会被Spring加载到IOC容器中。而@Import提供4种用法:
- 导入Bean
- 导入配置类
- 导入ImportSelector实现类。一般用于加载配置文件中的类
- 导入ImportBeanDefinitionRegistrar 实现类。
1. 直接导入 Bean
@SpringBootApplication
@Import(User.class)
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
Object user = context.getBean(User.class);
System.out.println(user);
}
}
2. 导入配置类
@SpringBootApplication
@Import(UserConfig.class)
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
Object user = context.getBean(User.class);
System.out.println(user);
}
}
3. 导入ImportSelector 的实现类
创建一个 java 类MySelector
实现 ImportSelector,修改实现方法的返回值
public class MySelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//获取的是字符串,这里的字符串可以在配置文件中动态的植入
return new String[]{"cn.hxy.springbootenableother.domain.User"};
}
}
在引导类中使用
@SpringBootApplication
//@Import(UserConfig.class)
@Import(MySelector.class)
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
Object user = context.getBean(User.class);
System.out.println(user);
}
}
4. 导入ImportBeanDefinitionRegistrar实现类
创建一个 java 类实现ImportBeanDefinitionRegistrar
public class MyImportBean implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
//在 ioc 工厂注册一个 bean
registry.registerBeanDefinition("user",beanDefinition);
}
}
在引导类使用
@SpringBootApplication
//@Import(User.class)
//@Import(UserConfig.class)
//@Import(MySelector.class)
@Import(MyImportBean.class)
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
Object user = context.getBean(User.class);
System.out.println(user);
}
}
五、@EnableAutoConfiguration注解
- @EnableAutoConfiguration 注解内部使用 @Import(AutoConfigurationImportSelector.class)来加载配置类。
- 配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当SpringBoot 应用启动时,会自动加载这些配置类,初始化Bean。注意高版本的 springboot 的配置文件位置放在了
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
中- 并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean
上面就是 springboot 自动配置的原理
实例:自定义starter步骤分析
自定义redis-starter。要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean。
创建 redis-spring-boot-autoconfigure模块
创建 redis-spring-boot-starter模块,依赖redis-spring.boot-autoconfigure的模块
在 redis-spring-boot-autoconfigure模块中初始化Jedis 的Bean。并定义META-INF/spring.factories文件
在测试模块中引入自定义的 redis-starter依赖,测试获取Jedis 的Bean,操作 redis.
创建一个RedisAutoConfiguration配置类
@Configuration
@EnableConfigurationProperties(RedisProperties.class) //动态获取配置的 redis 的配置参数
public class RedisAutoConfiguration {
@Bean
public Jedis jedis(RedisProperties redisProperties) {
return new Jedis(redisProperties.getHost(),redisProperties.getPort());
}
}
创建动态获取 yml 文件的实体类
/**
* 获取 yml 配置文件中配置的 redis 信息
*/
@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;
}
}
在 resource 中定义META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
,用于自定义 Spring Boot 自动配置,通过指定需要导入的自动配置类,来扩展或定制 Spring Boot 应用程序的配置和功能。
cn.hxy.redis.config.RedisAutoConfiguration//指定需要导入的自动配置类
在其他模块中引入 redis-spring-boot-starter 坐标 通过 context 上下文容器获取 jedis 的 bean 进行使用
@SpringBootApplication
//@Import(User.class)
//@Import(UserConfig.class)
//@Import(MySelector.class)
@Import(MyImportBean.class)
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
//从上下文中获取另一个项目依赖中的 Bean 对象
// Object user = context.getBean(User.class);
// System.out.println(user);
Jedis jedis = context.getBean(Jedis.class);
jedis.set("name", "hxy");
jedis.set("password", "123");
String name = jedis.get("name");
System.out.println(name);
}
}
注意在使用的模块中可以通过 yml 配置redis的 port 或者 host 动态修改 redis 的启动信息.如果是本地,要确保本地的 redis 服务已经启动
完善starter
修改自定义配置类,加上 condition
@Configuration
@EnableConfigurationProperties(RedisProperties.class) //动态获取配置的 redis 的配置参数
@ConditionalOnClass(Jedis.class)//当存在Jedis时才加载该 Bean
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "jedis")//当用户自己没有定义自己的 jedis 时,我们才提供 jedis
public Jedis jedis(RedisProperties redisProperties) {
System.out.println("当前是引入的依赖提供的 jedis")
return new Jedis(redisProperties.getHost(),redisProperties.getPort());
}
}
可以简单的测试一下当前 jedis 是导入依赖提供的还是自定义的 在引导类上加上一个自定义 jedis的 Bean
/**
* 自定义 jedis
* @return Jedis
*/
@Bean
public Jedis jedis() {
return new Jedis();
}