手写SpringBoot Starter(二):手把手教你写第一个Starter,原来这么简单!

系列文章第2篇 | 共5篇
难度:⭐⭐ | 适合人群:想动手实践的开发者


📝 上期回顾

上一篇我们学到了:

  • ✅ Starter是SpringBoot的"套餐"机制
  • ✅ Starter能告别配置地狱,统一依赖管理
  • ✅ 官方和第三方Starter的命名规范

上期思考题解答:

Q1: Starter是怎么做到"自动配置"的?
A: 通过spring.factories文件 + @Configuration配置类 + SpringBoot的自动装配机制。(本篇详细讲解)

Q2: 我能不能自己写一个Starter?
A: 当然可以!今天就手把手教你写!🎉

Q3: Starter的原理是什么?
A: 看完今天的实战,原理自然就懂了!


💭 开场:一个真实的需求

时间: 周一早会
地点: 会议室

技术经理: "咱们公司的10个微服务都需要统一的日志格式,每次都要复制一堆配置代码,太麻烦了。小李啊,能不能封装一个Starter,大家直接引用就能用?"

小李(我): "Starter?我只会用,不会写啊..." 😰

技术经理: "这周学一下,下周给我看成果。"

我: "...好吧。" (内心:完了,要现学现卖了)


一周后...

我: "经理,搞定了!我写了一个common-log-spring-boot-starter,大家只需要引入依赖,日志格式就自动统一了!"

技术经理: "不错啊!怎么实现的?"

我: "其实很简单,就四步..." 😎


🎯 本篇目标

今天我们要实现一个超简单但完整的Starter:

功能: 提供一个HelloService,自动注入到Spring容器
效果: 其他项目引入依赖后,直接使用HelloService

虽然简单,但麻雀虽小五脏俱全,包含了Starter的所有核心要素!


🛠️ 第一步:创建SpringBoot项目

Q:为什么Starter本身也是SpringBoot项目?

A: 因为Starter需要用到SpringBoot的自动配置机制啊!

类比: 就像你要做面包,得先有面粉(SpringBoot)一样。


创建项目

方式一:使用IDEA创建(推荐)

  1. File → New → Project
  2. 选择 Spring Initializr
  3. 填写项目信息:
    Group: com.example
    Artifact: hello-spring-boot-starter
    Name: hello-spring-boot-starter
    Package name: com.example.starter
    
  4. Dependencies: 暂时不选,后面手动加
  5. 点击 Create

方式二:使用 Spring Initializr 网站

访问:start.spring.io/

Project: Maven Project
Language: Java
Spring Boot: 2.7.x(选择稳定版本)
Group: com.example
Artifact: hello-spring-boot-starter
Packaging: Jar
Java: 8  11

点击 GENERATE,下载后导入IDEA


清理项目

创建完成后,项目结构是这样的:

hello-spring-boot-starter/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/starter/
│   │   │       └── HelloSpringBootStarterApplication.java  ← 删掉
│   │   └── resources/
│   │       └── application.properties  ← 删掉
│   └── test/  ← 整个删掉
├── pom.xml
└── ...

为啥要删?

  • HelloSpringBootStarterApplication:Starter不需要启动类
  • application.properties:Starter不需要自己的配置文件
  • test目录:为了简洁,测试放在使用方项目

删除后的目录结构:

hello-spring-boot-starter/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/starter/  ← 后面在这里写代码
│       └── resources/
│           └── META-INF/  ← 后面在这里创建
└── pom.xml

看起来清爽多了!


📦 第二步:配置pom.xml

完整的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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 项目基本信息 -->
    <groupId>com.example</groupId>
    <artifactId>hello-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>hello-spring-boot-starter</name>
    <description>我的第一个自定义Starter</description>

    <!-- 继承SpringBoot父项目 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 核心依赖:SpringBoot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 可选:配置元数据(后面文章会讲) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 可选:Lombok,简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

</project>

Q:为啥只需要这几个依赖?

A: 因为Starter的核心就是"自动配置"!

依赖分析:

依赖作用必需?
spring-boot-starter提供SpringBoot核心功能✅ 必需
spring-boot-configuration-processor生成配置元数据(智能提示)⭕ 可选
lombok简化代码(@Data等)⭕ 可选

记住: Starter的pom.xml要保持简洁,不要引入业务相关的依赖!


Q:为什么有些依赖标记了<optional>true</optional>

A: 这是个好问题!

场景演示:

<!-- Starter项目 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>  ← 注意这里
</dependency>

效果:

  • Starter项目自己: 可以用Lombok
  • 引用Starter的项目: 不会自动引入Lombok

为什么这样设计?

  • lombokconfiguration-processor开发时工具
  • 使用方不需要这些依赖
  • 避免依赖污染

记忆技巧: 工具类依赖加optional=true,业务类依赖不加!


💻 第三步:编写核心代码

3.1 创建Service类

src/main/java/com/example/starter/下创建HelloService.java

package com.example.starter;

/**
 * Hello服务类
 * 提供简单的问候功能
 */
public class HelloService {

    private String prefix;  // 前缀
    private String suffix;  // 后缀

    public HelloService(String prefix, String suffix) {
        this.prefix = prefix;
        this.suffix = suffix;
    }

    /**
     * 说Hello
     * @param name 名字
     * @return 问候语
     */
    public String sayHello(String name) {
        return prefix + " " + name + " " + suffix;
    }
}

代码解读:

  • 非常简单的一个Service类
  • 接收前缀和后缀参数
  • 提供sayHello方法拼接字符串

Q:为啥不加@Service注解?
A: 因为这个类要在自动配置类中手动注册成Bean,不需要@Service


3.2 创建配置属性类(可选但推荐)

src/main/java/com/example/starter/下创建HelloProperties.java

package com.example.starter;

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

/**
 * Hello配置属性类
 * 对应配置文件中的 hello.* 配置
 */
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {

    /**
     * 前缀,默认值:Hello
     */
    private String prefix = "Hello";

    /**
     * 后缀,默认值:!
     */
    private String suffix = "!";

    // Getter 和 Setter
    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}

代码解读:

@ConfigurationProperties(prefix = "hello")
    ↓
意思是:读取配置文件中以 hello. 开头的配置

配置文件中这样写:
hello.prefix=你好
hello.suffix=!

就会自动映射到这个类的属性上!

Q:不写这个类行不行?
A: 行,但是不推荐!

对比:

方式优点缺点
有Properties类配置集中管理
类型安全
支持默认值
IDE有提示
多写一个类
没有Properties类少写代码配置散乱
容易出错
没有提示

结论: 多写几行代码,换来更好的体验,值!✅


3.3 创建自动配置类(核心!)

src/main/java/com/example/starter/下创建HelloAutoConfiguration.java

package com.example.starter;

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;

/**
 * Hello自动配置类
 * SpringBoot启动时会自动加载这个配置类
 */
@Configuration  // ← 标记为配置类
@EnableConfigurationProperties(HelloProperties.class)  // ← 启用配置属性
public class HelloAutoConfiguration {

    /**
     * 注册HelloService到Spring容器
     * @param properties 配置属性
     * @return HelloService实例
     */
    @Bean  // ← 注册为Bean
    @ConditionalOnMissingBean  // ← 如果容器中没有HelloService才注册
    public HelloService helloService(HelloProperties properties) {
        return new HelloService(properties.getPrefix(), properties.getSuffix());
    }
}

代码深度解读:

注解1:@Configuration

@Configuration
public class HelloAutoConfiguration {

作用: 告诉Spring这是一个配置类,类似于XML配置文件
效果: Spring会扫描这个类中的@Bean方法


注解2:@EnableConfigurationProperties

@EnableConfigurationProperties(HelloProperties.class)

作用: 启用HelloProperties配置类
效果:

  1. HelloProperties注册为Bean
  2. 自动读取配置文件中的hello.*配置
  3. 注入到HelloProperties对象中

Q:不加这个注解会怎样?
A: HelloProperties不会生效,配置文件的值读取不到!


注解3:@Bean

@Bean
public HelloService helloService(HelloProperties properties) {
    return new HelloService(properties.getPrefix(), properties.getSuffix());
}

作用: 将方法返回值注册为Spring Bean
效果: 其他地方可以通过@Autowired注入HelloService


注解4:@ConditionalOnMissingBean

@ConditionalOnMissingBean
public HelloService helloService(HelloProperties properties) {

作用: 条件注册——只有容器中没有HelloService时才注册
效果: 允许使用方自定义HelloService覆盖默认实现

场景演示:

// 使用方项目中
@Configuration
public class CustomConfig {
    @Bean
    public HelloService helloService() {
        return new HelloService("自定义前缀", "自定义后缀");
    }
}

// 因为有了@ConditionalOnMissingBean
// Starter就不会再注册HelloService
// 使用的是用户自定义的版本!

这就是"约定大于配置,但允许覆盖"的体现! 💡


代码结构总结

到这里,我们的代码结构是这样的:

src/main/java/com/example/starter/
├── HelloService.java           ← 业务类
├── HelloProperties.java        ← 配置属性类
└── HelloAutoConfiguration.java ← 自动配置类(核心)

三个类的关系:

HelloProperties(配置)
        ↓ 读取配置文件
HelloAutoConfiguration(自动配置)
        ↓ 创建并注册Bean
HelloService(业务类)
        ↓ 被注入到Spring容器
使用方可以 @Autowired 使用

📄 第四步:创建spring.factories(关键!)

Q:什么是spring.factories?

A: 它是SpringBoot的"插件注册表"!

类比:

  • 你的Starter是一个"插件"
  • spring.factories是"插件商店的登记表"
  • SpringBoot启动时会查这个表,找到你的插件并加载

创建文件

src/main/resources/下创建目录和文件:

src/main/resources/
└── META-INF/
    └── spring.factories  ← 创建这个文件

注意:

  • 目录名必须是META-INF(全大写)
  • 文件名必须是spring.factories
  • 位置不能错,否则SpringBoot扫描不到!

文件内容

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.HelloAutoConfiguration

格式解读:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  ↓
这是固定的key,SpringBoot会读取这个key

com.example.starter.HelloAutoConfiguration
  ↓
你的自动配置类的全限定名

注意事项:

  1. 等号后面的\是什么?

    • 换行符,如果配置很多可以换行写
    • 如果只有一个配置类,可以不要\
  2. 可以配置多个吗?

    • 可以!用逗号分隔:
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.example.starter.HelloAutoConfiguration,\
    com.example.starter.OtherAutoConfiguration
    
  3. 文件编码是什么?

    • UTF-8或ISO-8859-1都可以
    • IDEA默认会用正确的编码

Q:为什么要指定META-INF目录?

A: 这是SpringBoot的约定!

原理简述:

// SpringBoot源码(简化版)
public class SpringFactoriesLoader {
    public static final String FACTORIES_RESOURCE_LOCATION = 
        "META-INF/spring.factories";  // ← 看!写死的路径
    
    public static List<String> loadFactoryNames() {
        // 扫描所有jar包的 META-INF/spring.factories
        // 读取配置
        // 加载自动配置类
    }
}

你不按这个路径写?SpringBoot: "对不起,扫不到!" 🙅


📦 第五步:打包到本地仓库

Q:为什么要打包?

A: 因为要给其他项目用啊!

类比:

  • 你写的Starter是一个"商品"
  • 打包就是"装箱"
  • 本地仓库就是"自己的仓库"
  • 其他项目就是"客户"

Maven三大打包命令

命令对比:

命令全称作用结果
package打包编译+测试+打jar包jar包在target目录
install安装package + 部署到本地仓库jar包在本地Maven仓库
deploy部署install + 部署到远程仓库jar包在本地+远程仓库

我们用哪个? install

为什么?

  • package:只打包,不部署,其他项目引用不到
  • install:部署到本地仓库,本机其他项目能用,完美!
  • deploy:需要配置远程仓库,现在用不上

打包方式一:IDEA Maven插件

步骤:

  1. 打开右侧 Maven 面板
  2. 展开项目 → Lifecycle
  3. 双击 clean(清理旧文件)
  4. 双击 install(打包安装)

看到这个就成功了:

[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.141 s
[INFO] Finished at: 2024-01-15T10:30:00+08:00
[INFO] ------------------------------------------------------------------------

打包方式二:命令行(推荐)

步骤:

  1. 打开项目根目录(pom.xml所在目录)
  2. 打开终端/命令行
  3. 执行命令:
mvn clean install

参数说明:

  • clean:清理target目录
  • install:编译、测试、打包、安装到本地仓库

跳过测试(可选):

mvn clean install -DskipTests

验证是否打包成功

查看本地仓库:

Windows: C:\Users\你的用户名\.m2\repository\com\example\hello-spring-boot-starter\1.0.0\

Mac/Linux: ~/.m2/repository/com/example/hello-spring-boot-starter/1.0.0/

应该看到这些文件:

hello-spring-boot-starter-1.0.0.jar      ← Starter的jar包
hello-spring-boot-starter-1.0.0.pom      ← pom文件
hello-spring-boot-starter-1.0.0.jar.sha1 ← 校验文件

看到了?恭喜,打包成功! 🎉


🧪 第六步:创建测试项目验证

创建测试项目

方式: 创建一个普通的SpringBoot项目

Group: com.example
Artifact: starter-test
Dependencies: Spring Web(方便测试)

引入自定义Starter

在测试项目的pom.xml中添加:

<dependencies>
    <!-- 引入我们自己写的Starter -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>hello-spring-boot-starter</artifactId>
        <version>1.0.0</version>
    </dependency>

    <!-- Spring Web(用于测试) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

配置文件(可选)

application.yml中配置:

# 使用默认配置(不写这个也行)
hello:
  prefix: Hello
  suffix: !

# 或者自定义配置
hello:
  prefix: 你好
  suffix: ,欢迎你!

编写测试Controller

package com.example.test.controller;

import com.example.starter.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired  // ← 直接注入,不需要任何配置!
    private HelloService helloService;

    @GetMapping("/hello/{name}")
    public String hello(@PathVariable String name) {
        return helloService.sayHello(name);
    }
}

代码亮点:

  • ✨ 没有写任何配置类
  • ✨ 直接@Autowired就能用
  • ✨ 这就是Starter的魅力!

运行测试

启动项目:

@SpringBootApplication
public class StarterTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(StarterTestApplication.class, args);
    }
}

访问接口:

http://localhost:8080/hello/World

预期结果(默认配置):

Hello World !

预期结果(自定义配置):

hello:
  prefix: 你好
  suffix: ,欢迎你!
你好 World ,欢迎你!

看到这个结果?恭喜你,Starter写成功了! 🎉🎉🎉


🎯 测试场景汇总

场景1:默认配置

配置文件: 不写任何配置

结果:

访问:/hello/张三
输出:Hello 张三 !

结论: 默认配置生效 ✅


场景2:自定义配置

配置文件:

hello:
  prefix: 早上好
  suffix: ,祝你开心!

结果:

访问:/hello/李四
输出:早上好 李四 ,祝你开心!

结论: 自定义配置生效 ✅


场景3:覆盖默认Bean

测试项目中自定义Bean:

@Configuration
public class CustomConfig {
    @Bean
    public HelloService helloService() {
        return new HelloService("自定义", "覆盖成功");
    }
}

结果:

访问:/hello/王五
输出:自定义 王五 覆盖成功

结论: @ConditionalOnMissingBean生效,用户可以覆盖默认实现 ✅


🐛 常见问题排查

问题1:注入失败

现象:

@Autowired
private HelloService helloService;  // ← 报错:Could not autowire

原因排查:

  1. 检查spring.factories是否创建

    • 位置:src/main/resources/META-INF/spring.factories
    • 路径对不对?
  2. 检查spring.factories内容

    # 类的全限定名对不对?
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.example.starter.HelloAutoConfiguration
    
  3. 检查是否重新打包

    • 修改代码后必须重新mvn clean install
  4. 检查依赖是否引入

    <dependency>
        <groupId>com.example</groupId>
        <artifactId>hello-spring-boot-starter</artifactId>
        <version>1.0.0</version>
    </dependency>
    

问题2:配置不生效

现象:

hello:
  prefix: 你好

但输出还是: Hello 张三 !

原因排查:

  1. 检查@EnableConfigurationProperties

    @Configuration
    @EnableConfigurationProperties(HelloProperties.class)  // ← 有这个吗?
    public class HelloAutoConfiguration {
    
  2. 检查配置文件格式

    # ❌ 错误:多了空格
    hello :
      prefix: 你好
    
    # ✅ 正确
    hello:
      prefix: 你好
    
  3. 检查配置属性前缀

    @ConfigurationProperties(prefix = "hello")  // ← 前缀对吗?
    public class HelloProperties {
    

问题3:打包后找不到jar

现象: 执行mvn install后,其他项目引用报错

原因排查:

  1. 检查打包是否成功

    • 看控制台有没有BUILD SUCCESS
  2. 检查本地仓库

    • ~/.m2/repository/看jar包在不在
  3. 检查坐标是否一致

    <!-- Starter的pom.xml -->
    <groupId>com.example</groupId>
    <artifactId>hello-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    
    <!-- 测试项目的pom.xml -->
    <dependency>
        <groupId>com.example</groupId>  ← 一致吗?
        <artifactId>hello-spring-boot-starter</artifactId>  ← 一致吗?
        <version>1.0.0</version>  ← 一致吗?
    </dependency>
    

💡 知识点总结

本篇你学到了什么?

Starter的四大核心组件

  1. HelloService:业务类
  2. HelloProperties:配置属性类
  3. HelloAutoConfiguration:自动配置类
  4. spring.factories:注册文件

关键注解的作用

  • @Configuration:标记配置类
  • @EnableConfigurationProperties:启用配置属性
  • @Bean:注册Bean
  • @ConditionalOnMissingBean:条件注册

spring.factories的作用

  • SpringBoot的"插件注册表"
  • 必须放在META-INF目录
  • 告诉SpringBoot加载哪些自动配置类

打包部署流程

  • mvn clean install打包到本地仓库
  • 其他项目通过坐标引用

🎓 进阶思考

思考1:如果业务类很多怎么办?

场景: 不只一个HelloService,还有GoodbyeServiceWelcomeService...

解决方案:

@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public HelloService helloService(HelloProperties properties) {
        return new HelloService(properties.getPrefix(), properties.getSuffix());
    }

    @Bean
    @ConditionalOnMissingBean
    public GoodbyeService goodbyeService() {
        return new GoodbyeService();
    }

    @Bean
    @ConditionalOnMissingBean
    public WelcomeService welcomeService() {
        return new WelcomeService();
    }
}

或者分多个配置类:

# spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.HelloAutoConfiguration,\
com.example.starter.GoodbyeAutoConfiguration,\
com.example.starter.WelcomeAutoConfiguration

思考2:如果想让Starter可插拔怎么办?

什么是可插拔?

  • 引入了依赖,但可以选择不启用

解决方案: 下一篇文章详细讲!敬请期待! 😎


🤔 留个思考题

问题: 如果我想让HelloService只在特定条件下才注册(比如配置文件中hello.enabled=true时才生效),应该怎么做?

提示: 关键词@ConditionalOnProperty

答案: 下一篇揭晓!


📢 下期预告

《手写SpringBoot Starter(三):实现可插拔Starter,像Zuul一样优雅!》

下一篇我们将:

  • 理解什么是"可插拔"
  • 学习@Conditional系列注解
  • 实现三种可插拔方式
  • 分析Zuul、MyBatis的可插拔实现
  • 对比不同方案的优劣

让你的Starter更加灵活和专业! 🚀


💬 互动时间

你在写Starter时遇到过什么问题?
想实现什么样的Starter?
对哪部分内容还有疑问?

欢迎在评论区分享!我会一一回复!💭


觉得有帮助?别忘了三连支持: 👍 点赞 | ⭐ 收藏 | 🔄 转发

跟着这篇文章,你已经成功写出了第一个Starter! 🎉


下一篇见! 👋