Spring Boot(九):自定义 Starter 🍺

1,779 阅读5分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」。

starter 是 Spring Boot 中一种非常重要的机制,它可以将繁杂的配置统一集成到 starter 中,我们只需要通过 maven 将 starter 依赖导入到项目中,Spring Boot 就能自动扫描并加载相应的默认配置。

starter 的出现让开发人员从繁琐的框架配置中解放出来,将更多的精力专注于业务逻辑的开发,极大地提高了开发效率。

在一些特殊情况下,我们也可以将一些通用功能封装成自定义的 starter 进行使用,以下详细介绍如何自定义 starter。

1. 命名规范

Spring Boot 提供的 starter 以 spring-boot-starter-xxx 形式命名。为了与 Spring Boot 生态提供的 starter 区分,官方建议第三方开发者或技术(例如 Druid、Mybatis 等等)厂商自定义的 starter 使用 xxx-spring-boot-starter 的形式命名,例如 mybatis-spring-boot-starterdruid-spring-boot-starter 等等。

2. 模块规范

Spring Boot 官方建议我们在自定义 starter 时,创建两个 Module: autoConfigure Modulestarter Module其中 starter Module 依赖于 autoConfigure Module。当然,这只是 Spring Boot 官方的建议,并不是硬性规定,若不需要自动配置代码和依赖项目分离,我们也可以将它们组合到同一个 Module 里。

3 分析第三方 starter

若要自定义 starter,我们可以从 mybatis-spring-boot-startermybatis-spring-boot-autoconfigure 中获取一些启发:

mybatis-spring-boot-starter 仅仅将其下引入的依赖整合到一起并对外提供一个依赖坐标(同时依赖于 mybatis-spring-boot-autoconfigure),方便外部引用(解决版本冲突):

MybatisAutoConfigurationmybatis-spring-boot-starter 的自动配置类,其中又加载了 MybatisProperties 配置类信息:

该自动配置类必须被 Spring 容器所识别并加载,才能加载其中 @Bean 定义的 Bean 组件,这时就需要依靠 Spring Boot 启动时加载的 spring.factories 配置文件来标识 MybatisAutoConfiguration 自动配置类,从而加载其中的 Bean。

大致了解 mybatis-spring-boot-starter 的起步依赖后,我们就可以来自定义所需的 starter 了。

⭐如果对自动配置原理仍有疑惑,可参考下该篇博客:juejin.cn/post/700560…

4. 实现自定义 Starter

4.1 需求

自定义 scorpion-redis-spring-boot-starter,要求当导入 redis 坐标时,Spring Boot 自动创建 Jedis 的 Bean。

4.2 实现步骤

  1. 创建 scorpion-redis-spring-boot-autoconfigure 模块
  2. 创建 scorpion-redis-spring-boot-starter 模块,并依赖于 scorpion-redis-spring-boot-autoconfigure 模块
  3. scorpion-redis-spring-boot-autoconfigure 模块中初始化 Jedis 的 Bean,并定义 META-INF/spring.factories 文件
  4. 在测试模块中引入自定义的 scorpion-redis-spring-boot-starter 依赖,测试获取 Jedis 的 Bean,操作 redis。

4.3 创建&开发 autoconfigure 模块

4.3.1 创建 scorpion-redis-spring-boot-autoconfigure 项目

创建一个 maven 模块:

4.3.2 修改 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.wyk</groupId>
    <artifactId>scorpion-redis-spring-boot-autoconfigure</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>

    <!--如果是Maven项目需要手动指定父项目-->
    <!--如果是Spring Boot项目则会自动引入该父项目坐标-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 引入Jedis依赖 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
    </dependencies>

</project>

4.3.3 编写 RedisAutoConfiguration

  • @ConditionalOnClass(Jedis.class) : 当项目中存在 Jedis 类(pom.xml 引入该依赖),则可以加载 RedisAutoConfiguration 自动配置类,因为 return new Jedis() 需要使用到 Jedis.class
  • @ConditionalOnMissingBean(name = "jedis") : 当 Spring 容器中已存在 Jedis Bean,且 name = "jedis",那么则无需再重复定义一个 Jedis Bean 通过 @Bean 返回到 Spring 容器中了!

注:不要把 pom.xml 文件引入的类与 spring 容器中的类混淆了!

pom.xml 引入的是外部类,无法通过 @Component、``@Repository@Controller@Service等注解将其注入到 Spring 容器中,只能通过@Bean` 将其注入到 Spring 容器。

Spring 容器容器是Spring框架实现功能的核心,容器不只是帮我们创建了对象那么简单,它负责了对象整个的生命周期的管理——创建、装配、销毁。关于 Spring 的这个容器你最常听闻的一个术语就是 IOC 容器。总之一句话,我的应用程序里不用再过问对象的创建和管理对象之间的依赖关系了,都由 IOC 容器代劳,也就是说,我把对象创建、管理的控制权都交给 Spring 容器,这是一种控制权的反转,所以 Spring 容器才能称为 IOC 容器。不过这里要厘清一点:并不是说只有 Spring 的容器才叫 IOC 容器,基于 IOC 容器的框架还有很多,并不是 Spring 特有的。

package com.wyk.autoconfigure;

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;

/**
 * @Author Author Unknown
 * @Date 2021/11/11 7:41
 * @Description RedisAutoConfiguration: 自动配置类
 */
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@ConditionalOnClass(Jedis.class)
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "jedis")
    public Jedis jedis(RedisProperties redisProperties) {
        return new Jedis(redisProperties.getHost(), redisProperties.getPort());
    }
}

4.3.4 编写 RedisProperties

package com.wyk.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @Author Author Unknown
 * @Date 2021/11/11 7:42
 * @Description Redis: 配置信息类
 */
@ConfigurationProperties(prefix = RedisProperties.REDIS_PREFIX)
public class RedisProperties {

    public static final String REDIS_PREFIX = "redis";

    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;
    }
}

4.3.5 定义 META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wyk.autoconfigure.RedisAutoConfiguration

4.3.6 完整的模块目录

4.4 创建&开发 starter 模块

4.4.1 创建 scorpion-redis-spring-boot-starter 模块

与 autoconfigure 同理,不再赘述:

4.4.2 修改 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.wyk</groupId>
    <artifactId>scorpion-redis-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>scorpion-redis-spring-boot-starter</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--引入scorpion-redis-spring-boot-autoconfigure-->
        <dependency>
            <groupId>com.wyk</groupId>
            <artifactId>scorpion-redis-spring-boot-autoconfigure</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

4.4.3 完整的模块目录

4.5 测试模块引入并测试自定义 Starter

4.5.1 pom.xml

<!-- 测试自定义 Starter: scorpion-redis-spring-boot-starter: 所以引入该依赖 -->
<dependency>
    <groupId>com.wyk</groupId>
    <artifactId>scorpion-redis-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

4.5.2 Spring Boot 启动类中测试

EnableStarterApplication.java:

package com.enable.starter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
//import org.springframework.context.annotation.Bean;
import redis.clients.jedis.Jedis;

@SpringBootApplication
public class EnableStarterApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(EnableStarterApplication.class, args);

        Jedis jedis = context.getBean(Jedis.class);
        System.out.println(jedis);

        jedis.set("name", "kun");
        String name = jedis.get("name");
        System.out.println(name);
    }

//    如果在这里将 Jedis 加载到 Spring 容器中,则不会加载 RedisAutoConfiguration 中的 Jedis Bean! ==> 即不会输出 "RedisAutoConfiguration.."(定义在RedisAutoConfiguration中)
//    @Bean
//    public Jedis jedis() {
//        return new Jedis("localhost", 6379);
//    }

}

配置类生效:

自定义 Starter 测试成功!

希望本文对你有所帮助🧠
欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥