超详细的 Spring Boot Starter 开发入门,看这篇真够了!

超详细的 Spring Boot Starter 开发入门,看这篇真够了!

本文你将学习到以下知识点:

  • 如何开发一个 Spring Boot Starter
  • 写框架代码需要注意些什么?
  • 为什么要用自动配置?自动装配的机制
  • 如何通过 AutoConfiguration.imports 自动注入?
  • Spring的条件装配机制、如何配置默认Bean
  • Starter的接口抽象设计
  • 如何为我们的Starter做一个用户可控的开关
  • 等等

如果你只是想要一份可以直接使用的 Starter 示例代码,可以直接跳转到章节 4.5

0、引言

之前在重构一个项目的时候,经常涉及到一些表的数据合并、迁移等操作。小公司嘛,基建其实是不太成熟的,像我们常说的灰度发布、双写方案等等,这边都很难去做到,没法做到真正的不停机平滑数据迁移,停机的话又需要跟甲方那边申请,比较麻烦。

所以后来我选择了一种“平民方案” —— 在不改动现有核心逻辑的前提下,通过拦截器动态拦截特定接口防增量 再配合 Nacos 的配置热更新,做到了动态切换和控制,效果还不错,可以实现比较优雅的不停机动态配置拦截和响应的防增量。很多人就会想到了:这不就是 Sentinel 的流控嘛?确实类似。只是我们当时项目组没有接入 Sentinel,为了这个特地加个组件实在没必要。

回到项目,实现起来其实是很简单的,无非就是定位一下哪些接口有增量,再搭配Nacos配置中心的热更新,直接拦截并响应。

关于Nacos配置中心的使用,大家伙有兴趣可以看看这篇文章:

Spring项目将配置文件移至nacos上,实现统一配置和热更新_将此文件夹下所有配置文件内容复制到 nacos 对应的配置中-CSDN博客https://blog.csdn.***/weixin_46739493/article/details/134301141?ops_request_misc=%257B%2522request%255Fid%2522%253A%25226d8978fa82c676b18784cfd0107afb9b%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=6d8978fa82c676b18784cfd0107afb9b&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-3-134301141-null-null.nonecase&utm_term=nacos&spm=1018.2226.3001.4450

后来听其它同事说CV在其它项目中也有用,当然很是欣喜。所以打算实现一个Starter放到内网给大家引入即用。正好一直想着写个类似的博客,但是由于公司的保密原则和复杂性(有的i项目需要保密,有的需求又太复杂,很难在一篇文章中讲明白),这次这个需求很简单,正好借着这次实现,顺便记录下这个动作。

本文适用于:熟悉 Spring Boot ,且想要深入理解并构建自己的 starter 模块的开发者。

1、为什么要构建 Starter?

Spring Boot 的强大生态中,Starter 模块的设计是其核心之一。通过合理封装,我们可以将常用功能打包为 Starter,供别人一行依赖即可使用,极大提升开发效率。

2、确认需求

开发之前,特别是这种组件类的,一定要很明确自己的需求:

  • 可以动态注册一个拦截器;

  • 可以自动为接口返回结果做统一包装;

  • 用户只需引入依赖、加一点配置,功能开箱即用;

3、基础知识点

之前也有看过一些"教你如何快速开发Starter"的类似的文章,写的也挺好,只是发现基本上是不讲"为什么"的。我个人认为,开发Starter起码要理解这部分接下来说的最基本的几个点。不然后续在Strarter后续的拓展上会很受阻,这部分真的很重要。

我预留几个问题,可以后面思考一下,看看自己到底懂了没,比如:

  • 我们怎么让用户使用我们的Starter的时候去注册我们的Bean。
  • 自动配置是什么、为什么要自动配置?
  • 怎么样才能做到跟用户的Bean和接口不冲突
  • 如何去实现一个默认Bean等等。

3.1 自动配置

Starter 模块的灵魂就是“自动配置”,我们要让用户无需手动声明任何@***ponentScan 或 @Bean,功能即可生效

注意一下,是自动配置,不是自动装配。

  • 自动装配(Autowiring):是 Spring IoC 容器的功能,通过 @Autowired 等注解实现依赖注入。

  • 自动配置(Auto-Configuration):是 Spring Boot 的功能,通过 META-INF/spring.factories 或类似机制加载配置类,动态注册 Bean。Spring Boot 的 @EnableAutoConfiguration 会读取 META-INF中的配置加载这些配置类,然后通过 @Bean 方法注册 Bean。

要做到这点,我们依赖于 Java 的 SPI(Service Provider Interface)机制。

而在boot2和boot3中,有些不同:

Spring Boot 2 vs 3 自动配置机制对比

项目版本 自动配置入口声明文件 格式
Spring Boot 2.x META-INF/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=...
Spring Boot 3.x+ META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 直接写配置类的全限定类名,一行一个

boot2:

Spring Boot 3.x 之后,推荐使用这个文件:

Spring Boot 启动时会读取该文件,自动尝试注入配置类中的@Bean组件。

另外,boot3也兼容boot2的写法,但是已经标记为“非推荐方式”了

随便打开一个依赖下都能看到它们:

3.2 为什么要自动配置

一些小伙伴可能会有些疑惑:

为啥要通过配置 META-INF/spring.factories 或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 来自动加载并尝试注入?

@Bean、@***ponent 等注解不是也能把类注册成 Bean 吗?

首先得先知道一下 普通组件注册自动配置机制 的区别。

普通 Bean 注册的机制

大家都知道,@SpringBootApplication 是 Spring Boot 启动类的注解,里面包含了 @***ponentScan,它会扫描当前类所在包及其子包,自动将带有 @***ponent、@Service等注解的类注册为 Spring 容器中的 Bean。

这在我们自己开发应用时非常好用,因为我们可以控制代码结构和包路径。

但是,在开发 Starter 的时候就不行了!

当我们写一个 Spring Boot Starter 的时候,代码并不在用户应用的主包路径下。换句话说:

使用者写的应用并不会去扫描我们 Starter 的包路径

也就是说,就算我们在 Starter 中写了 @***ponent 或 @Configuration 等,这些类也不会被用户项目的 @***ponentScan 扫描到,更不会被 Spring 容器管理。

这时候就要自动配置了。所以 自动配置的目的是 让别人“什么都不写” 就能自动集成你的功能模块!

3.3 条件装配机制 (Conditional Auto Configuration)


核心知识点:

条件装配 是 Spring Boot 提供的一种机制:

只有在满足某个条件时,Spring 才会加载这个 @Configuration 类或注册这个 @Bean。

这些条件都是通过注解(如 @ConditionalXXX)来声明的。我们就可以通过这些注解,给用户提供一个按钮,让他们可以控制我们的组件的Bean注册、默认Bean创建等等。常用注解如下:

比如:

@ConditionalOnMissingBean: 

当 Spring 容器中 不存在 某个 Bean 时,当前的 Bean 或配置才会被加载。那我们就可以用它来做默认Bean,当用户的类重写了我们的接口并注册成Bean之后,就不用注册我们这个默认Bean了。

再比如:

@ConditionalOnProperty

基于配置文件中是否存在某个属性,以及其值是否匹配来控制 Bean 的注册。只有满足某个配置条件,才加载这个配置类或 Bean

那我们就可以用它,来给用户一个开关。他想要的时候就开启,不想要的时候关闭即可。

我也写过相关的文章:

Spring Boot 条件装配机制:用它写出更优雅的自动配置-CSDN博客https://blog.csdn.***/weixin_46739493/article/details/148170274?sharetype=blogdetail&sharerId=148170274&sharerefer=PC&sharesource=weixin_46739493&spm=1011.2480.3001.8118

3.4 总结一下 Spring Boot Starter 自动装配的流程

       用户项目
          ↓ 引入 starter 依赖
[Spring Boot 启动时自动加载配置类]
          ↓
     自动配置类 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
          ↓
Spring Boot 会扫描这些配置类,尝试注入为 @Configuration
          ↓
 条件装配(比如 @ConditionalOnProperty)满足才会生效
          ↓
最终,只有真正“符合条件”的 @Bean 会注册到 Spring 容器中

3.5 配置类绑定 @EnableConfigurationProperties

一句话总结:

@EnableConfigurationProperties 的作用就是把使用了 @ConfigurationProperties 注解的类注册为一个 Spring Bean,并读取配置文件自动赋值。

从规范上面讲,spring.factories 中 如果是一个简单的配置类(比如@ConfigurationProperties)的类,是不应该写在其中的。那么,这种配置类是要怎么加载成Bean呢。就使用这个注解: @EnableConfigurationProperties

以ShieldProperties配置类和ShieldAutoConfiguration自动配置类为例,

@Configuration
@EnableConfigurationProperties(ShieldProperties.class)
public class ShieldAutoConfiguration {

}

当 Spring Boot 加载 ShieldAutoConfiguration 时,自动顺带注册并初始化 ShieldProperties,你就可以像普通 Bean 一样使用它了。

在加载ShieldAutoConfiguration的时候,告诉Spring Boot:我要启用并注册ShieldProperties配置类,然后Spring就会创建ShieldProperties-Bean,并读取配置文件并赋值。这样清晰职责分离,是最佳实践。

4、代码开发

4.1 提炼需求

在知道了上面一些知识点,我们就可以开始实际的业务开发了。我们再确认一下需求,核心逻辑

命中配置的接口,就直接拦截并终止请求流程,返回一个统一的响应体。

典型的流程如下:

  1. 用户配置哪些路径要拦截(如 /test/**)

  2. 进入拦截器,判断路径是否命中

  3. 如果命中,构造一个 返回体 → 序列化为 JSON → 写入 response,终止后续流程

随便写个代码:

这就满足这个需求对吧。然后大家可以想到几个问题不:

1,每个用户、每个项目的通用响应体Response是不同的,用户怎么定制返回结构,如何「策略扩展」。

2,看起来像“硬编码的 JSON 响应”,少了点“框架味”和弹性

3,怎么注册这个拦截器

4.2 怎么让它更优雅?

简单点说 我们的设计重点其实是 怎么让用户"告诉你怎么构造这个拦截后返回的JSON"

我们现在从两个维度做抽象和升级,让我们的 starter 更具“可配置 + 可扩展 ”的特性:

4.2.1 抽象"响应内容"接口

public interface ShieldResponseBuilder {
    Object buildResponse(HttpServletRequest request, String matchedPath);
}

我们要默认提供一个实现(@ConditionalOnMissingBean):返回默认的响应体

    @Bean
    @ConditionalOnMissingBean(ShieldResponseBuilder.class)
    public ShieldResponseBuilder defaultShieldResponseBuilder(ShieldProperties properties) {
        return (request, path) -> {
            ShieldProperties.ShieldResponseConfig cfg = properties.getResponse();
            return Response.fail(cfg.getCode(), cfg.getMessage());
        };
    }

4.2.2 加上响应字段动态配置

前面我们有了开关和路径。那么此时我们要再对Resp进行动态配置。

shield:
  enabled: true
  paths:
    - /test/a
    - /test/b
  response:
    code: 403
    message: 服务升级中,请稍后再试

默认的 ShieldResponseBuilder 中读取这些字段生成响应。

4.2.3 怎么新增这个拦截器

 我们在项目中注册拦截器经常是这样写的,去重写这个添加拦截器接口:

感觉没问题对吧。

但是,我们在写框架代码的时候千万不能这么写,因为一旦存在用户也实现了WebMv***onfigurer,就会启动失败,因为Spring不知道要加载哪一个。所以我们要这么写:

然后可能会有小伙伴会有疑惑:

我自己 @Bean 提供了一个 WebMv***onfigurer,但别人项目中也有,Spring 会不会冲突?这里new的会不会覆盖? 等等

答案是 不会冲突,Spring Boot 会自动组合(***posite)所有的 WebMv***onfigurer Bean,它们的拦截器、格式化器、视图解析器等会被累加合并(组合模式)。

所以用户最终体验应该是:

什么都不用写,配置完 YAML,拦截器自动生效,

如果想定制响应结构,自己提供个 @Bean 实现接口就行。

这才是一个真正「优雅、可配置、可扩展」的 Spring Boot Starter。

4.3 接口拓展评估

不知道大家有没有接触过 Spring Security,它有一个非常核心的接口 —— UserDetails。

这个接口定义了用户认证所需的基本方法,比如:

  • getUsername() 获取用户名

  • getPassword():获取密码

  • 还有其他如账号是否锁定、是否过期等方法

这样的设计可以让开发者根据自身业务,灵活地实现接口,定义自己需要的字段和逻辑。

同样的还有比如:以 Spring Boot 的 HealthIndicator 

这个接口允许我们为系统的某些模块(如数据库、Redis、消息队列等)定义自定义的健康检查逻辑。例如:

@***ponent
public class MyRedisHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        // 检查 Redis 是否连接正常
        boolean redisStatus = checkRedis();
        if (redisStatus) {
            return Health.up().withDetail("Redis", "正常").build();
        } else {
            return Health.down().withDetail("Redis", "连接失败").build();
        }
    }
}

这种设计思路和 UserDetails 类似:提供一个接口,由用户在特定场景中自行扩展。
但如果你的项目追求快速集成和“开箱即用”,你可能会倾向使用 Spring Boot Actuator 自带的默认健康检查功能,而不去实现这些扩展。

因为我是希望可以引入一下依赖,加个yml配置(甚至公司内部使用公司底板的,连yml都不用加),直接可用的。而且功能上来讲也比较简单,这种小功能其实也没有什么太多需要拓展的地方,没什么必要性。所以就摒弃了这个想法。

不过我建议大家在学习和实践过程中,还是要了解并尝试这种设计思路。我认为没有任何一种设计方案是放之四海而皆准的“最优解” —— 灵活性和易用性常常是设计中的一对矛盾,如何取舍,需要看你当下最需要解决的问题是什么。设计的好坏,永远取决于具体的业务场景与使用需求。

4.4 条件装配评估

另外还有个很关键的点值得考虑:我们需不需要为 Bean 的注册提供开关,即是否只有在配置项 enable=true 的情况下才注册该 Bean。

问题

其实核心就是在于评估“没有注册开关会带来什么影响?”这背后主要涉及两个方面:

  1. Bean 会不会很重。如果我们默认自动装配(即不加开关),需要考虑这个 Bean 是否会对用户项目造成明显的性能开销或副作用。
  2. Bean 是否对功能是必要的?如果我们不进行注册,是否会导致某些核心功能无法生效,比如我们希望实现的“热更新”能力

结论

在本 Starter 的设计中,我们可以明确得出结论:这个 Bean 应当默认自动装配,且不需要额外的 enable 开关控制。

原因如下:

  • 这个 Bean 本身非常轻量,几乎不会对用户项目造成任何负担,不存在资源消耗大或强依赖等问题。而且,本身会引入这个依赖的用户,一定是为了这个功能而来的。
  • 自动装配是实现“热更新”功能的基础。我们希望用户在修改配置文件后,相关逻辑能立刻生效。如果不注册 Bean,将根本无法实现这一目标。

所以我们的拦截器可以这样写:

而我们的配置文件的默认值是:

也就是说,默认情况下(只引入了该依赖,但没做任何配置的),这个拦截器是注册了。但是并不会有任何的拦截逻辑。

  • 无论用户是否配置,只要引入了该依赖,拦截器就会注册;

  • 拦截器内部再根据配置属性 shield.enabled 和 shield.paths 来动态控制是否真正执行拦截

  • 配置文件中 enabled=false 且 paths=[] 是默认值,意味着“默认注册但默认不生效”。

这种设计方式的最大好处是:

这主要是为了 支持运行时热更新配置。因为我们的热更新是依赖于Bean的。那如果这时候都没注册上Bean,更何谈所谓的热更新。

所以我们反过来处理:Bean 永远注册,是否启用交给运行时配置判断,这样就天然支持热更新,非常适合业务场景下的动态开关控制。

好的。现在这样基本的设计就已经完成了。现在只需要实现代码即可。

代码部分比较简单:

4.5 完整代码:

https://gitee.***/zhuosihua1116/upgrade-shield-starterhttps://gitee.***/zhuosihua1116/upgrade-shield-starter

// Response 
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Response implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 成功标识true or false
     */
    private Boolean su***ess;
    /**
     * 响应code
     */
    private Integer code;
    /**
     * 信息
     */
    private String msg;

    public static Response fail(Integer code, String msg) {
        Response result = new Response();
        result.setSu***ess(Boolean.FALSE);
        result.setMsg(msg);
        result.setCode(code);
        return result;
    }

}

// ResultUtil 
public class ResultUtil {
    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 将resp对象响应到响应体中
     */
    public static void responseJson(HttpServletResponse response, Object resp) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);
        objectMapper.writeValue(response.getWriter(), resp);
    }
}

// ShieldAutoConfiguration 
package ***.za.upgradeshield;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMv***onfigurer;

@Configuration
@EnableConfigurationProperties(ShieldProperties.class)
public class ShieldAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(ShieldResponseBuilder.class)
    public ShieldResponseBuilder defaultShieldResponseBuilder(ShieldProperties properties) {
        return (request, path) -> {
            ShieldProperties.ShieldResponseConfig cfg = properties.getResponse();
            return Response.fail(cfg.getCode(), cfg.getMessage());
        };
    }

    @Bean
    public ShieldInterceptor shieldInterceptor(ShieldProperties properties, ShieldResponseBuilder responseBuilder) {
        return new ShieldInterceptor(properties, responseBuilder);
    }

    @Bean
    public WebMv***onfigurer shieldWebMv***onfigurer(ShieldInterceptor interceptor) {
        return new WebMv***onfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(interceptor);
            }
        };
    }
}

// ShieldInterceptor 
package ***.za.upgradeshield;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

public class ShieldInterceptor implements HandlerInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(ShieldInterceptor.class);

    private final ShieldProperties properties;
    private final ShieldResponseBuilder responseBuilder;

    public ShieldInterceptor(ShieldProperties properties, ShieldResponseBuilder builder) {
        this.properties = properties;
        this.responseBuilder = builder;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LOGGER.debug("enter shield interceptor ... ");
        if (!properties.isEnabled()) return true;

        String uri = request.getRequestURI();
        List<String> paths = properties.getPaths();
        if (paths != null && paths.stream().anyMatch(uri::startsWith)) {
            LOGGER.info("shield request:{} ", uri);
            ResultUtil.responseJson(response, responseBuilder.build(request, uri));
            return false;
        }

        return true;
    }
}

// ShieldProperties 
@ConfigurationProperties(prefix = "shield")
public class ShieldProperties {
    private boolean enabled;
    private List<String> paths;
    private ShieldResponseConfig response = new ShieldResponseConfig();

    public static class ShieldResponseConfig {
        private Integer code = 500;
        private String message = "系统升级中,部分功能暂时不可用。预计将于1小时内完成,请稍后再试。";

    // getter and setter
    }

    // getter and setter
}

// ShieldResponseBuilder 
@FunctionalInterface
public interface ShieldResponseBuilder {

    /**
     * 构建拦截响应体的方法。
     *
     * @param request 当前的 HTTP 请求对象,便于获取 Header、参数、IP 等上下文信息
     * @param path    当前被拦截的请求路径,便于用户判断来源路径或个性化提示
     * @return 任何可以被序列化为 JSON 的对象(如 Map、POJO、自定义响应体类等)
     */
    Object build(HttpServletRequest request, String path);
}

5、测试

5.1 starter打包

maven - clean - install

可以去maven的仓库里面看看:

这样就是有了。

5.2 外部项目

现在我们直接找一个别的项目,直接在maven中引入这个gav。

刷新一下maven,看看lib

然后啥都不用动,包括yml,直接启动!

在拦截器这里打个断点,发个请求。

可以看到顺利进来了。然后由于没配置启用。所以它直接放行了。

现在我们再去配置一下yml:

可以看到 得益于Nacos配置中心,我们的配置类热更新了。

不用重启项目,直接发请求:

可以看到接口被拦截下来了。且也按着我们默认的返回实体返回了。因此,如果是使用公司的项目底板,这就可以完美适配 无需任何改动,只要引入依赖就能直接使用!

接下来可以再试试用户动态配置的bean,我这里图省事,弄了个Map(这里需要重启,因为bean得重新载入)。再次发请求,可以看到响应体也按着用户的格式走了:

6、完善“用户友好体验”

6.1 Readme

一个好的项目,没有 Readme 是肯定不行的。

Readme 是用户接触你项目的第一印象,是项目的说明书、门面。

需要包括啥倒是没有硬性要求。无非包括以下几个关键内容:

  1. 项目简介  简要说明这个项目是做什么的,它解决了什么问题,适用于什么场景。可以用 2~3 行话让用户快速“理解价值”。

  2. 特性亮点  用简洁的列表方式列出核心功能点,比如:

    - ✅ 支持路径拦截和自定义响应 
    - ✅ 开箱即用,默认实现快速上手 
    - ✅ 支持通过配置文件一键启用/禁用 
  3. 快速开始使用说明、进阶配置常见问题、参与贡献、开源协议致谢/鸣谢 等等

比如这样:


 

6.2 配置提示文件

希望用户yaml写的更方便一点的话,可以加一下提示文件:

由于篇幅的原因,被我拆出去了,有兴趣的看看:

Spring Boot Starter 怎么加配置提示文件-CSDN博客https://blog.csdn.***/weixin_46739493/article/details/148172132?sharetype=blogdetail&sharerId=148172132&sharerefer=PC&sharesource=weixin_46739493&spm=1011.2480.3001.8118

todo 发布到 Maven 中央仓库

7、结束语

不知不觉写了1.3w字。本文主要还是Starter的构建过程,同时也记录了一些我当时实现的过程以及思考过程,也许这不是一篇特别标准的教程,但我希望它更像一次真实的开发记录,能给遇到类似需求的朋友提供一些参考或借鉴。

如果对大家有帮助的话可以点个关注收藏啥的,后续我会再写一篇实战篇。另外,文章中也可能存在一些不足之处,或者有些地方讲解不够清楚,欢迎在评论区留言指正或交流。

转载请说明出处内容投诉
CSS教程网 » 超详细的 Spring Boot Starter 开发入门,看这篇真够了!

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买