通过自定义 Starter 理解 SpringBoot 自动配置

本文目标

  • 了解 SpringBoot 自动配置的功能,理解其基本实现原理(Level1)
  • 可以根据实际业务需求,去寻找合适的 Starter(Level1)
  • 当现成的 Starter 不满足需求时,可以自己封装自定义 Starter(Level2)
  • 理解自定义 Starter 源码背后的运行原理,即 @SpringBootApplication 到底做了哪些事情(Level3)

等级说明

  • Level1: 对 Java 和 SpringBoot 感兴趣的同学
  • Level2: 大量使用 Java 做开发的同学
  • Level3: 想要深入研究 Spring 和 SpringBoot 的同学

0 引言: SpringBoot Starter 与自动配置

SpringBoot Starter 是 SpringBoot 的重大特性之一。它分组整合了一些常用的依赖,使得用户在 maven 或 gradle 中只要依赖一个 starter,就有了一大堆功能集合。

比如,你只要依赖了 spring-boot-starter,就有了如下功能:

  • yaml 文件读取
  • 日志功能
  • Spring 核心功能
  • 自动配置

(Spring Boot 针对不同的应用场景提供了各种 starter,具体有哪些后面会提到。)

本文主要关注自动配置功能。

下面将通过一个自定义 Starter(RandomStarter) 的 demo 来讲解:

  1. 什么是自动配置
  2. 如何编写一个具备自动配置功能的自定义 Starter
  3. Spring Boot 底层实现自动配置的原理

1 RandomStarter 介绍与演示

(以下例子中将使用 gradle 管理依赖)

1.1 应用场景描述

我们有一个名为 Random 的功能模块,目前功能只有一个:从 m 个字符串中,随机选择 n 个返回(m > n,且 m 和 n 可配置)。

核心代码如下:

public class RandomChoiceMachine {
    private List<String> list;
    private int num;

    public RandomChoiceMachine(List<String> list, int num) {
        this.list = new ArrayList<>(list);
        this.num = num;
        assert this.num <= this.list.size();
    }

    public List<String> next() {
        Collections.shuffle(list);
        return new ArrayList<>(list.subList(0, num));
    }
}

现在我们要在项目中复用 Random 模块里的这个工具类。

1.2 Spring 中的做法

1.2.1 引入 Spring 依赖

dependencies {
    compile("org.springframework:spring-context:5.1.9.RELEASE")
    compile project(':random')
}

1.2.2 设置参数

// application.properties
demo.random.choice.items=a,b,c,d,e,f,g
demo.random.choice.num=2

1.2.3 写配置类

读取配置文件,并生成 Bean。

@Configuration
@PropertySource("classpath:application.properties")
@ComponentScan
public class AppConfig {
    @Value("${demo.random.choice.items}")
    private String itemsStr;
    @Value("${demo.random.choice.num}")
    private int num;

    @Bean
    public RandomChoiceMachine randomChoiceMachine() {
        return new RandomChoiceMachine(Arrays.asList(itemsStr.split(",")), num);
    }
}

1.2.4 运行测试

@Component
public class TestDriverSpringApplication {
    @Autowired
    private RandomChoiceMachine randomChoiceMachine;

    public void run() {
        for (int i = 0; i < 3; i++) {
            printList(randomChoiceMachine.next());
        }
    }

    private void printList(List<String> list) {
        System.out.println(String.join(",", list));
    }

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        TestDriverSpringApplication testDriver = context.getBean(TestDriverSpringApplication.class);
        testDriver.run();
    }
}

1.3 SpringBoot 中的做法(使用 RandomStarter)

1.3.1 引入 SpringBoot 和自定义 Starter 依赖

dependencies {
    implementation('org.springframework.boot:spring-boot-starter')
    compile project(':spring-boot-starter-random')
}

1.3.2 设置参数

// application.properties
demo.random.choice.items=a,b,c,d,e,f,g
demo.random.choice.num=2

1.3.3 运行测试

@SpringBootApplication
public class TestDriverSpringBootApplication implements CommandLineRunner {
    @Autowired
    private RandomChoiceMachine randomChoiceMachine;

    public static void main(String[] args) {
        SpringApplication.run(TestDriverSpringBootApplication.class);
    }

    @Override
    public void run(String... args) throws Exception {
        for (int i = 0; i < 3; i++) {
            printList(randomChoiceMachine.next());
        }
    }

    private void printList(List<String> list) {
        System.out.println(String.join(",", list));
    }
}

 1.4 对比总结

可以看到,我们在 SpringBoot 中,依赖了 spring-boot-starter-random 后,会自动读取 application.properties 文件,给我们一个配置好的 RandomChoiceMachine 实例。这就是“自动配置”的威力。

它是怎么实现的呢?我们可以写一遍 RandomStarter,来看看它里面到底有什么黑魔法。

2 构建项目

2.1 添加依赖

建立空项目,引入依赖:

dependencies {
    implementation('org.springframework.boot:spring-boot-autoconfigure')
    compile project(':random')
}

2.2 配置映射参数实体

@ConfigurationProperties(prefix="demo.random.choice")
public class RandomChoiceProperties {

    private List<String> items = new ArrayList<>();
    private int num = 1;

    public List<String> getItems() {
        return items;
    }

    public void setItems(String items) {
        this.items = Arrays.asList(items.split(","));
    }

    public int getNum() {
        return num;
    }

    public void setNum(String num) {
        this.num = Integer.parseInt(num);
    }
}

2.3 编写业务

业务已在 random 模块中写好。

2.4 实现自动化配置

@Configuration // 开启配置
@EnableConfigurationProperties(RandomChoiceProperties.class) // 开启使用映射实体对象
@ConditionalOnClass(RandomChoiceMachine.class) // 存在HelloService时初始化该配置类
@ConditionalOnProperty // 存在对应配置信息时初始化该配置类
        (
                prefix = "demo.random.choice", // 存在配置前缀
                value = "enabled", // 开启
                matchIfMissing = true // 缺失检查
        )
public class RandomAutoConfiguration {

    /**
     * application.properties 配置文件映射前缀实体对象
     */
    @Autowired
    private RandomChoiceProperties randomChoiceProperties;

    /**
     * 根据条件判断不存在 RandomChoiceMachine 时初始化新 bean 到 Spring 容器
     */
    @Bean
    @ConditionalOnMissingBean(RandomChoiceMachine.class)
    public RandomChoiceMachine randomChoiceMachine() {
        return new RandomChoiceMachine(randomChoiceProperties.getItems(), randomChoiceProperties.getNum());
    }
}

2.5 创建 spring.facotries

我们在 src/main/resource 目录下创建 META-INF 目录,并在目录内添加文件 spring.factories,具体内容如下:

# 配置自定义Starter的自动化配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.plough.random.RandomAutoConfiguration

到此就完成了整个 RandomStarter 的编写。

3 Starter 源码解释——SpringBoot 实现自动配置的底层原理

虽然已经讲完了 RandomStarter 是怎么写的,但还是有点抽象。RandomStarter 的源码是比较简单的,为啥这样写就能实现自动配置呢?

大致原理如下:

  1. @SpringBootApplication 注解的内部,有一个 @EnableAutoConfiguration,有了它,SpringBoot 就会去读取 spring.facotries 文件;
  2. spring.facotries 文件指定了配置类的位置,这样 SpringBoot 就加载到了 RandomAutoConfiguration 这个配置类;
  3. RandomAutoConfiguration 会根据设定的参数,去生成 RandomChoiceMachine 的 Bean。

忽略了较多细节,感兴趣的同学可以自行深入研究。

4 SpringBoot 提供的各类 Starter

可以到官方文档中去看。我们了解常用的 Starter 后,可以在项目中直接应用。

5 其他

5.1 项目源码

GitHub:https://github.com/plough/random-starter-demo

5.2 踩坑记录

  • 一开始,配置前缀用的是 random.choice,运行时会出错,无法正常读取。一通 debug 后,发现与 java.util.Random 的命名有冲突(具体有啥联系没有深究),于是把前缀改为 demo.random.choice,就正常了。

5.3 参考文章