四、SpringBoot 原理分析

176 阅读12分钟

SpringBoot自动配置

一、Condition

Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建Bean操作。
思考:比如 SpringBoot 是如何知道要创建哪个 Bean 的?比如 SpringBoot 是如何知道要创建 RedisTemplate 的?

背景(结合上面的思考)

  1. 创建一个 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

  1. 导入 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 作为内置服务器

image.png

我们可以在 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 就会去判断对应的类型是否存在以此来加载对应的内置服务器。

image.png


三、@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种用法:

  1. 导入Bean
  2. 导入配置类
  3. 导入ImportSelector实现类。一般用于加载配置文件中的类
  4. 导入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。

  1. 创建 redis-spring-boot-autoconfigure模块

  2. 创建 redis-spring-boot-starter模块,依赖redis-spring.boot-autoconfigure的模块

  3. 在 redis-spring-boot-autoconfigure模块中初始化Jedis 的Bean。并定义META-INF/spring.factories文件

  4. 在测试模块中引入自定义的 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//指定需要导入的自动配置类

截屏2024-04-22 02.27.01.png

在其他模块中引入 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();
}