系列文章第3篇 | 共5篇
难度:⭐⭐⭐ | 适合人群:想深入理解的开发者
📝 上期回顾
上一篇我们成功写出了第一个Starter:
- ✅ 创建了HelloService业务类
- ✅ 编写了自动配置类
- ✅ 配置了spring.factories
- ✅ 测试项目引入依赖后直接就能用
上期思考题解答:
Q: 如果想让HelloService只在hello.enabled=true
时才生效,怎么做?
A: 用@ConditionalOnProperty
注解!(本篇详细讲解)
💥 开场:一个真实的尴尬场景
时间: 周三下午
地点: 公司工位
小王: "诶,我引入了咱们组的common-log-starter
,但是我不想用这个功能啊..."
我: "那你别引入依赖不就行了?"
小王: "不行啊,这个Starter里还有其他工具类我要用,但我不想要日志功能..."
我: "emmm..." 😅(陷入沉思)
小王: "你看人家Zuul,引入了依赖,但可以通过@EnableZuulProxy
注解控制要不要启用,这才叫专业!"
我: "有道理!" 💡(醍醐灌顶)
于是,这周我的任务变成了:
"把现有的Starter改造成可插拔的,让用户自己决定要不要启用!"
🤔 第一问:什么是"可插拔"?
Q1:字面意思理解
日常生活类比:
想象你的电脑:
-
USB接口 = Starter依赖
-
U盘 = Starter功能
-
插拔 = 启用/禁用
插上U盘(启用)→ 电脑识别并使用
拔掉U盘(禁用)→ 电脑不受影响
放到Starter上:
引入依赖(插入)→ 可以选择启用或不启用
不启用功能(拔出)→ 不影响项目运行
Q2:技术上的定义
可插拔Starter = 引入依赖 ≠ 自动启用
对比:
类型 | 引入依赖后 | 控制方式 | 灵活性 |
---|---|---|---|
普通Starter | 自动启用,无法关闭 | 无 | ❌ 低 |
可插拔Starter | 默认不启用,需要手动开启 | 注解/配置 | ✅ 高 |
Q3:为什么需要可插拔?
场景1:功能冲突
java
// 引入了Starter,但它的功能和项目现有功能冲突
// 普通Starter:没办法,只能删依赖
// 可插拔Starter:关闭这个功能就行
场景2:按需启用
java
// 开发环境需要Mock功能,生产环境不需要
// 普通Starter:得维护两个依赖版本
// 可插拔Starter:配置文件一行搞定
场景3:渐进式迁移
java
// 老项目想试用新Starter,但不能影响现有功能
// 普通Starter:风险太大,不敢试
// 可插拔Starter:先引入不启用,测试通过再启用
结论: 可插拔 = 用户有选择权 = 更专业! ✨
🎮 第二问:实现可插拔的三种方式
方式一:配置文件开关(最简单)
原理: 通过配置文件中的开关来控制是否启用
实现步骤:
1. 修改Properties类
java
package com.example.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
/**
* 是否启用Hello功能
* 默认值:true(向后兼容)
*/
private boolean enabled = true; // ← 新增开关
private String prefix = "Hello";
private String suffix = "!";
// Getter 和 Setter
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
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;
}
}
2. 修改自动配置类
java
package com.example.starter;
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;
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "hello", // 配置前缀
name = "enabled", // 配置项名称
havingValue = "true", // 期望值
matchIfMissing = true // 配置缺失时是否匹配(默认启用)
) // ← 关键注解
public HelloService helloService(HelloProperties properties) {
return new HelloService(properties.getPrefix(), properties.getSuffix());
}
}
关键注解深度解析:
java
@ConditionalOnProperty(
prefix = "hello", // 配置前缀:hello.enabled
name = "enabled", // 完整配置路径:hello.enabled
havingValue = "true", // 值为true时才生效
matchIfMissing = true // 如果用户不配置,默认认为是true
)
各参数含义:
参数 | 含义 | 示例 |
---|---|---|
prefix |
配置前缀 | hello |
name |
配置名称 | enabled |
havingValue |
期望值 | "true" |
matchIfMissing |
配置缺失时的默认行为 | true (默认启用) |
3. 使用方式
场景A:启用功能(默认)
yaml
# 不配置,默认启用
# 或者明确配置
hello:
enabled: true
prefix: Hello
suffix: !
结果: HelloService注入成功 ✅
场景B:禁用功能
yaml
hello:
enabled: false # ← 关闭功能
结果: HelloService不会注入,使用时会报错 ❌
测试代码:
java
@SpringBootTest
public class HelloServiceTest {
@Autowired(required = false) // ← 注意:required = false
private HelloService helloService;
@Test
public void testHelloService() {
if (helloService != null) {
System.out.println("功能已启用:" + helloService.sayHello("World"));
} else {
System.out.println("功能已禁用");
}
}
}
方式二:自定义注解开关(最优雅)
原理: 通过自定义@EnableXxx
注解来控制是否启用
实现步骤:
1. 创建自定义注解
java
package com.example.starter;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* 启用Hello功能
* 使用方式:在主类或配置类上添加此注解
*/
@Target(ElementType.TYPE) // 只能用在类上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
@Import(HelloAutoConfiguration.class) // ← 关键:导入自动配置类
public @interface EnableHello {
}
关键点:
@Import(HelloAutoConfiguration.class)
导入配置类- 只有加了
@EnableHello
注解,配置类才会被加载
2. 修改自动配置类
java
package com.example.starter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
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;
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
/**
* 创建一个标记Bean
* 用于标识是否启用了Hello功能
*/
@Bean
public HelloMarker helloMarker() {
return new HelloMarker();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(HelloMarker.class) // ← 只有存在HelloMarker才注册
public HelloService helloService(HelloProperties properties) {
return new HelloService(properties.getPrefix(), properties.getSuffix());
}
}
3. 创建标记类
java
package com.example.starter;
/**
* Hello功能标记类
* 这个类本身没有任何功能,只是用来标记是否启用了Hello功能
*/
public class HelloMarker {
// 空类,仅作标记用
}
Q:为什么需要标记类?
A: 因为@ConditionalOnBean
需要一个具体的类来判断!
流程:
less
用户加了@EnableHello
↓
导入HelloAutoConfiguration
↓
创建HelloMarker Bean
↓
@ConditionalOnBean(HelloMarker.class)生效
↓
注册HelloService
4. 删除spring.factories(重要!)
properties
# 注释掉或删除这一行
# org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
# com.example.starter.HelloAutoConfiguration
为什么要删?
- 因为现在通过
@EnableHello
手动导入配置类 - 不再需要自动扫描加载
5. 使用方式
启用功能:
java
@SpringBootApplication
@EnableHello // ← 加上这个注解
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
禁用功能:
java
@SpringBootApplication
// 不加@EnableHello注解
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
就这么简单! ✨
方式三:混合模式(最灵活)
原理: 结合配置文件和注解,提供多种控制方式
实现步骤:
1. 自定义注解(同方式二)
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(HelloAutoConfiguration.class)
public @interface EnableHello {
}
2. 自动配置类(组合条件)
java
package com.example.starter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
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;
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
@Bean
public HelloMarker helloMarker() {
return new HelloMarker();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(HelloMarker.class) // 条件1:必须有@EnableHello
@ConditionalOnProperty(
prefix = "hello",
name = "enabled",
havingValue = "true",
matchIfMissing = true
) // 条件2:配置文件开关
public HelloService helloService(HelloProperties properties) {
return new HelloService(properties.getPrefix(), properties.getSuffix());
}
}
多条件逻辑:
ini
启用条件 = 加了@EnableHello注解 && hello.enabled=true
3. 使用方式
完全启用:
java
@SpringBootApplication
@EnableHello // 加注解
public class Application {
}
yaml
hello:
enabled: true # 配置开启(或不配置,默认true)
结果: 功能启用 ✅
注解启用,配置禁用:
java
@SpringBootApplication
@EnableHello // 加注解
public class Application {
}
yaml
hello:
enabled: false # 配置关闭
结果: 功能禁用 ❌(配置优先)
无注解:
java
@SpringBootApplication
// 不加@EnableHello
public class Application {
}
结果: 功能禁用 ❌(缺少注解)
📊 三种方式对比
对比表格
维度 | 配置文件开关 | 自定义注解 | 混合模式 |
---|---|---|---|
实现难度 | ⭐ 简单 | ⭐⭐ 中等 | ⭐⭐⭐ 较复杂 |
控制方式 | 配置文件 | 注解 | 配置+注解 |
灵活性 | ⭐⭐ 一般 | ⭐⭐⭐ 较高 | ⭐⭐⭐⭐ 很高 |
用户体验 | 简单直观 | 专业优雅 | 可选择 |
修改成本 | 低 | 中 | 中 |
适用场景 | 简单Starter | 框架级Starter | 企业级Starter |
场景选择建议
选择配置文件开关:
✅ Starter功能简单
✅ 用户主要是初级开发者
✅ 追求简单易用
选择自定义注解:
✅ Starter功能复杂
✅ 想体现专业性
✅ 参考官方框架(如Zuul、Feign)
选择混合模式:
✅ 企业级Starter
✅ 需要多层控制
✅ 用户群体多样
🔍 第三问:大厂是怎么做的?
案例1:Zuul的可插拔实现
Zuul的启用方式:
java
@SpringBootApplication
@EnableZuulProxy // ← Zuul的开关注解
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
源码分析:
@EnableZuulProxy注解
java
package org.springframework.cloud.netflix.zuul;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@EnableCircuitBreaker // 启用断路器
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class) // ← 导入标记配置
public @interface EnableZuulProxy {
}
ZuulProxyMarkerConfiguration
java
package org.springframework.cloud.netflix.zuul;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ZuulProxyMarkerConfiguration {
@Bean
public Marker zuulProxyMarkerBean() {
return new Marker(); // ← 创建标记Bean
}
/**
* 标记类
*/
class Marker {
// 空类,仅作标记
}
}
ZuulProxyAutoConfiguration
java
package org.springframework.cloud.netflix.zuul;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class) // ← 条件判断
public class ZuulProxyAutoConfiguration {
// 注册Zuul相关Bean
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
@Bean
public ZuulHandlerMapping zuulHandlerMapping() {
return new ZuulHandlerMapping();
}
// ...更多Bean
}
Zuul的设计思路:
css
@EnableZuulProxy
↓
导入 ZuulProxyMarkerConfiguration
↓
创建 Marker Bean
↓
ZuulProxyAutoConfiguration 检测到 Marker
↓
注册 Zuul 相关 Bean
↓
Zuul功能启用
和我们的实现对比:
维度 | 我们的实现 | Zuul的实现 | 差异 |
---|---|---|---|
注解名 | @EnableHello | @EnableZuulProxy | 命名规范一致 |
标记类 | HelloMarker | Marker内部类 | Zuul用内部类更优雅 |
条件注解 | @ConditionalOnBean | @ConditionalOnBean | 完全一致 |
设计思想 | 标记Bean判断 | 标记Bean判断 | ✅ 相同 |
结论: 我们的实现和Zuul是一样的思路! 🎉
案例2:MyBatis的可插拔实现
MyBatis的启用方式:
java
@SpringBootApplication
@MapperScan("com.example.mapper") // ← MyBatis的开关注解
public class Application {
}
或者:
java
@Mapper // 在每个Mapper接口上加注解
public interface UserMapper {
}
源码简析:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MapperScannerRegistrar.class) // ← 导入注册器
public @interface MapperScan {
String[] value() default {};
String[] basePackages() default {};
}
MyBatis的思路:
- 不用标记Bean
- 直接通过
@Import
导入扫描注册器 - 扫描指定包下的Mapper接口
和Zuul对比:
框架 | 实现方式 | 适用场景 |
---|---|---|
Zuul | 标记Bean | 功能开关型 |
MyBatis | 扫描注册器 | 批量注册型 |
案例3:Feign的可插拔实现
Feign的启用方式:
java
@SpringBootApplication
@EnableFeignClients // ← Feign的开关注解
public class Application {
}
源码:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
}
和MyBatis几乎一样的思路!
🎓 第四问:我该选哪种方式?
决策树
css
你的Starter是什么类型?
│
├─ 简单工具类(如:字符串处理、日期工具)
│ └─ 选择:配置文件开关 ⭐
│
├─ 框架集成类(如:Redis、MQ、OSS)
│ └─ 选择:自定义注解 ⭐⭐
│
├─ 业务组件类(如:日志、监控、权限)
│ └─ 选择:混合模式 ⭐⭐⭐
│
└─ 批量注册类(如:Mapper扫描、Bean扫描)
└─ 选择:@Import + Registrar ⭐⭐⭐
实际案例推荐
如果你要封装:
Starter类型 | 推荐方式 | 理由 |
---|---|---|
Redis Starter | 配置文件开关 | 简单够用,用户习惯配置文件 |
OSS Starter | 自定义注解 | 专业,可以带参数(如指定region) |
监控 Starter | 混合模式 | 需要细粒度控制 |
权限 Starter | 混合模式 | 既要全局开关,又要局部控制 |
Mapper Starter | @Import + Registrar | 需要批量扫描注册 |
💻 完整实战:改造我们的Starter
选择方案
我们选择自定义注解方式(参考Zuul)
理由:
- 专业优雅
- 符合SpringBoot生态习惯
- 可扩展性强
最终代码
1. EnableHello.java
java
package com.example.starter;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* 启用Hello功能
*
* 使用示例:
* <pre>
* @SpringBootApplication
* @EnableHello
* public class Application {
* public static void main(String[] args) {
* SpringApplication.run(Application.class, args);
* }
* }
* </pre>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(HelloAutoConfiguration.class)
public @interface EnableHello {
}
2. HelloMarker.java
java
package com.example.starter;
/**
* Hello功能标记类
* 用于标识是否启用了Hello功能
*/
public class HelloMarker {
// 标记类,无需任何实现
}
3. HelloAutoConfiguration.java
java
package com.example.starter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
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自动配置类
* 只有在存在HelloMarker时才会注册HelloService
*/
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
/**
* 注册标记Bean
* 表示Hello功能已启用
*/
@Bean
public HelloMarker helloMarker() {
return new HelloMarker();
}
/**
* 注册HelloService
* 条件:必须存在HelloMarker(即加了@EnableHello注解)
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(HelloMarker.class)
public HelloService helloService(HelloProperties properties) {
System.out.println("HelloService自动配置成功!"); // 日志
return new HelloService(properties.getPrefix(), properties.getSuffix());
}
}
4. 删除spring.factories
properties
# 方式一:完全删除文件
# 方式二:注释掉自动配置
# org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
# com.example.starter.HelloAutoConfiguration
5. 打包测试
bash
mvn clean install
测试验证
测试1:不加注解(功能禁用)
java
@SpringBootApplication
// 不加@EnableHello
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
java
@RestController
public class TestController {
@Autowired(required = false) // ← 注意
private HelloService helloService;
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
if (helloService == null) {
return "HelloService未启用!";
}
return helloService.sayHello(name);
}
}
访问: http://localhost:8080/hello/World
输出: HelloService未启用!
结论: 禁用成功 ✅
测试2:加注解(功能启用)
java
@SpringBootApplication
@EnableHello // ← 加上注解
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
启动日志:
HelloService自动配置成功!
访问: http://localhost:8080/hello/World
输出: Hello World !
结论: 启用成功 ✅
🎯 知识点总结
本篇你学到了什么?
✅ 可插拔的概念
- 引入依赖 ≠ 自动启用
- 用户有选择权
✅ 三种实现方式
- 配置文件开关:
@ConditionalOnProperty
- 自定义注解:
@EnableXxx
+ 标记Bean - 混合模式:两者结合
✅ 大厂实现分析
- Zuul:标记Bean方式
- MyBatis:扫描注册方式
- Feign:扫描注册方式
✅ 条件注解家族
@ConditionalOnProperty
:根据配置@ConditionalOnBean
:根据Bean存在@ConditionalOnMissingBean
:根据Bean不存在
🤔 思考题
问题: 如果我想实现更复杂的条件,比如:
- 只在生产环境启用
- 只在特定版本的Spring下启用
- 同时满足多个条件才启用
怎么办?
提示: SpringBoot还有很多@ConditionalOnXxx
注解哦!
答案: 下下篇源码分析时会讲到!
📢 下期预告
《手写SpringBoot Starter(四):配置元数据让你的Starter更专业!》
下一篇我们将:
- 理解配置元数据是什么
- 学习如何生成智能提示
- 实现配置项的描述、默认值、类型提示
- 让你的Starter像官方一样专业
让IDE智能提示你的配置项! ✨
💬 互动时间
你觉得哪种可插拔方式最优雅?
你见过哪些框架的可插拔设计?
有什么想法或疑问?
欢迎评论区讨论!💭
觉得有帮助?三连支持: 👍 点赞 | ⭐ 收藏 | 🔄 转发
看完这篇,你的Starter已经很专业了! 🚀
下一篇见! 👋