作者 :打码养家
日期 :2025年12月19日
场景:第三方登录系统重构(钉钉、企业微信等)
一、背景:为什么需要重构?
在开发一个 SaaS 平台时,我们最初采用最朴素的方式实现第三方登录:
java
// ❌ 初始版本:硬编码 + if-else 地狱
@PostMapping("/login")
public LoginInfoVo thirdPartyLogin(@RequestParam String provider, @RequestParam String code) {
if ("dingtalk".equals(provider)) {
// 钉钉登录逻辑(50行)
return dingTalkService.login(code);
} else if ("wecom".equals(provider)) {
// 企业微信登录逻辑(60行)
return weComService.login(code);
} else if ("feishu".equals(provider)) {
// 飞书登录逻辑(55行)
return feishuService.login(code);
}
throw new IllegalArgumentException("不支持的登录方式: " + provider);
}
🚨 问题暴露:
- 违反开闭原则:每新增一个登录方式,都要修改 Controller。
- 职责混乱:Controller 承担了"选择逻辑"和"业务调用"双重职责。
- 难以测试:无法单独测试某一种登录策略。
- 耦合严重:Controller 直接依赖所有 Service。
二、第一次尝试:仅使用策略模式
我首先想到用策略模式解耦:
java
// ✅ 策略接口
public interface ThirdPartyLoginStrategy {
LoginInfoVo login(String code);
String getProvider();
}
// ✅ 具体策略
@Component
public class DingTalkLoginStrategy implements ThirdPartyLoginStrategy {
private final DingTalkService dingTalkService; // Spring Bean
public DingTalkLoginStrategy(DingTalkService service) {
this.dingTalkService = service;
}
@Override
public LoginInfoVo login(String code) {
return dingTalkService.login(code);
}
@Override
public String getProvider() {
return "dingtalk";
}
}
❓ 我的第一个疑问:客户端怎么用?
如果不用工厂,客户端只能这样写:
java
// ❌ 客户端仍需硬编码
if ("dingtalk".equals(provider)) {
strategy = new DingTalkLoginStrategy(dingTalkService); // ← 但 dingTalkService 是 Spring Bean!
}
💥 问题来了 :
new DingTalkLoginStrategy(...)会导致dingTalkService为 null!因为 Spring 无法管理手动
new出来的对象。
结论 :纯策略模式在 Spring 环境中无法直接使用,除非策略不依赖任何 Bean。
例子:
如果只用策略模式(不用工厂),也是完全可行的,而且在某些简单场景下非常合适。但它的适用范围和优缺点与"策略+工厂"组合有明显区别。
✅ (一)、什么是"纯策略模式"?
策略模式的核心思想是:
将一组算法/行为封装成独立的类,它们实现同一个接口,客户端可以在运行时选择使用哪一个。
关键点:客户端自己决定用哪个策略,而不是通过工厂动态获取。
🌰 (二)、纯策略模式示例
1. 策略接口
java
public interface DiscountStrategy {
BigDecimal applyDiscount(BigDecimal amount);
}
2. 具体策略
java
public class VIPDiscountStrategy implements DiscountStrategy {
public BigDecimal applyDiscount(BigDecimal amount) {
return amount.multiply(BigDecimal.valueOf(0.8)); // 打8折
}
}
public class SeasonalDiscountStrategy implements DiscountStrategy {
public BigDecimal applyDiscount(BigDecimal amount) {
return amount.multiply(BigDecimal.valueOf(0.9)); // 打9折
}
}
3. 客户端直接使用(无工厂)
java
public class OrderService {
public BigDecimal calculatePrice(String userType, BigDecimal originalPrice) {
DiscountStrategy strategy;
if ("VIP".equals(userType)) {
strategy = new VIPDiscountStrategy(); // ← 客户端自己 new
} else {
strategy = new SeasonalDiscountStrategy(); // ← 客户端自己判断
}
return strategy.applyDiscount(originalPrice);
}
}
✅ 这就是纯策略模式:没有工厂,客户端直接创建并使用策略。
✅ (三)、什么时候适合只用策略模式?
| 场景 | 说明 |
|---|---|
| 策略数量少(1~3个) | 比如只有"普通用户"和"VIP"两种折扣 |
| 选择逻辑简单固定 | if-else 或 switch 足够清晰 |
| 策略不需要 Spring Bean | 不依赖数据库、Redis、其他 Service |
| 性能敏感(避免 Map 查找) | 直接 new,无额外开销 |
| 一次性或脚本式代码 | 不需要长期维护和扩展 |
💡 例如:工具类中的格式化策略、简单的配置开关等。
三、引入工厂模式:真正的解耦
于是,我决定引入工厂模式,让 Spring 来管理策略的创建和注入。
✅ 最终方案:策略 + 工厂 + Spring 自动注册
1. 策略接口(不变)
java
public interface ThirdPartyLoginStrategy {
LoginInfoVo login(String code);
String getProvider(); // 返回唯一标识,如 "dingtalk"
}
2. 具体策略(由 Spring 管理)
java
@Component
public class DingTalkLoginStrategy implements ThirdPartyLoginStrategy {
private final DingTalkService dingTalkService;
public DingTalkLoginStrategy(DingTalkService service) {
this.dingTalkService = service;
}
@Override
public LoginInfoVo login(String code) {
return dingTalkService.login(code);
}
@Override
public String getProvider() {
return "dingtalk";
}
}
✅ 所有策略类都加
@Component,成为 Spring Bean。
3. 工厂类(核心)
java
@Component
public class LoginStrategyFactory {
private final Map<String, ThirdPartyLoginStrategy> strategyMap = new ConcurrentHashMap<>();
// Spring 自动注入所有实现了 ThirdPartyLoginStrategy 的 Bean
public LoginStrategyFactory(ThirdPartyLoginStrategy[] strategies) {
for (ThirdPartyLoginStrategy strategy : strategies) {
strategyMap.put(strategy.getProvider(), strategy);
}
}
public ThirdPartyLoginStrategy getStrategy(String provider) {
ThirdPartyLoginStrategy strategy = strategyMap.get(provider);
if (strategy == null) {
throw new IllegalArgumentException("不支持的登录方式: " + provider);
}
return strategy;
}
}
4. 客户端(Controller)
java
@RestController
@RequiredArgsConstructor
public class ThirdPartyLoginController {
private final LoginStrategyFactory strategyFactory;
@PostMapping("/login")
public LoginInfoVo login(@RequestParam String provider, @RequestParam String code) {
ThirdPartyLoginStrategy strategy = strategyFactory.getStrategy(provider);
return strategy.login(code);
}
}
四、关键设计细节解析
🔍 1. 为什么用构造器注入 ThirdPartyLoginStrategy[]?
Spring 有一个强大特性:
当注入一个接口数组时,会自动收集容器中所有该接口的实现类 Bean。
java
private final ThirdPartyLoginStrategy[] strategies;
→ 启动时,Spring 自动把 DingTalkLoginStrategy、WeComLoginStrategy 等全部注入进来。
✅ 无需手动注册,无需修改工厂代码!
🔍 2. 为什么用 ConcurrentHashMap?
- Controller 可能被高并发调用
strategyMap在初始化后只读,但初始化过程需线程安全ConcurrentHashMap保证init()方法在多线程下安全
💡 实际上,由于
@PostConstruct只在 Bean 初始化时调用一次,普通HashMap也够用。但用ConcurrentHashMap更严谨。
🔍 3. 为什么策略类必须是 @Component?
- 只有被 Spring 管理,才能自动注入
DingTalkService等依赖 - 只有是 Spring Bean,才能被
ThirdPartyLoginStrategy[]自动收集
⚠️ 如果忘记加
@Component,启动时strategies数组为空!
五、我的深度疑问与解答
❓ 疑问 1:这算"简单工厂"还是"工厂方法"?
- 这是"简单工厂"(Simple Factory) ,因为:
- 一个工厂类(
LoginStrategyFactory) - 通过参数(
provider)返回不同产品
- 一个工厂类(
- 不是"工厂方法"(Factory Method),后者需要子类重写创建方法。
✅ 在大多数业务场景中,"简单工厂 + 策略"已足够。
❓ 疑问 2:能否不用工厂,直接用 ApplicationContext 获取?
可以,但不推荐:
java
// 不推荐!
ThirdPartyLoginStrategy strategy = applicationContext.getBean(provider + "LoginStrategy", ThirdPartyLoginStrategy.class);
问题:
- 需要约定 Bean 名称(脆弱)
- 无法统一校验"是否支持"
- 引入全局状态,难以测试
✅ 工厂提供了抽象层,隐藏了获取细节。
❓ 疑问 3:如果策略需要动态配置(如 API 密钥),怎么办?
可以在策略中注入配置:
java
@Component
public class DingTalkLoginStrategy implements ThirdPartyLoginStrategy {
private final String appId;
private final String appSecret;
public DingTalkLoginStrategy(@Value("${dingtalk.app-id}") String appId,
@Value("${dingtalk.app-secret}") String appSecret) {
this.appId = appId;
this.appSecret = appSecret;
}
}
或者通过配置中心动态加载------不影响工厂+策略结构。
❓ 疑问 4:如何支持"默认策略"或"组合策略"?
-
默认策略 :在
getStrategy中加 fallbackjavaif (strategy == null) return defaultStrategy; -
组合策略 (如先钉钉再微信):新增一个
CompositeLoginStrategy实现
✅ 策略模式天然支持扩展。
六、优势总结:为什么这个组合如此强大?
| 维度 | 优化前 | 优化后 |
|---|---|---|
| 扩展性 | 新增登录方式需改 Controller | 只需新增一个 @Component 策略类 |
| 可测试性 | 无法单独测试钉钉逻辑 | 可直接 new DingTalkLoginStrategy(mock) |
| 可维护性 | 登录逻辑散落在 Controller | 每个策略独立、职责单一 |
| 健壮性 | 魔法字符串 "dingtalk" |
通过 getProvider() 统一管理 |
| Spring 集成 | 手动调用 Service | 完全依赖注入,符合 Spring 哲学 |
七、适用场景扩展
这套模式不仅适用于登录,还可用于:
| 场景 | 策略标识 | 策略行为 |
|---|---|---|
| 支付网关 | "alipay", "wechat" |
pay(order) |
| 消息推送 | "sms", "email", "wechat" |
send(message) |
| 文件解析 | "excel", "csv", "json" |
parse(file) |
| 权限校验 | "role", "acl", "rbac" |
check(user, resource) |
💡 凡是"根据类型执行不同算法"的地方,都适用此模式。
八、避坑指南:常见错误
❌ 错误 1:策略类忘记加 @Component
→ 启动时不报错,但运行时找不到策略。
✅ 解决:确保所有策略类被 Spring 扫描到。
❌ 错误 2:getProvider() 返回值重复
→ 后注册的策略会覆盖先注册的。
✅ 解决:使用枚举或常量,避免手写字符串。
❌ 错误 3:在策略中做太多事
→ 策略应只负责"协调",具体逻辑下沉到 Service。
✅ 解决:策略类保持轻量,只调用其他 Service。
九、未来演进方向
-
策略元数据化
用注解定义策略标识:
java@StrategyProvider("dingtalk") public class DingTalkLoginStrategy { ... } -
动态注册/卸载策略
通过管理后台启用/禁用某些登录方式。
-
策略性能监控
在工厂中加入 Metrics 统计各策略调用次数、耗时。
十、结语
从最初的 if-else 到现在的 策略 + 工厂 + Spring 自动装配,我深刻体会到:
好的设计不是一蹴而就的,而是在解决实际问题中逐步演进的。
这套模式不仅解决了当前需求,更为未来扩展铺平了道路。它体现了面向对象的核心思想:封装变化、依赖抽象、开闭原则。
如果你也在处理类似的多实现场景,不妨试试这个组合------它可能比你想象的更强大。
附:完整代码结构
src/
└── main/
└── java/
└── com/rihuayun/auth/
├── strategy/
│ ├── ThirdPartyLoginStrategy.java // 策略接口
│ ├── DingTalkLoginStrategy.java // 具体策略
│ └── WeComLoginStrategy.java
├── factory/
│ └── LoginStrategyFactory.java // 工厂类
└── controller/
└── ThirdPartyLoginController.java // 客户端