介面有多個實現類 (如 PaymentService 有 AlipayService, WechatPayService, UnionPayService),需**運行時根據業務參數動態選擇**具體實現,而非編譯期靜態注入。
**核心思想**:啟動階段掃描所有目標介面的 Bean,按**自定義鍵 (業務代碼/枚舉/類名)** 建立 Map 註冊表,運行時 O(1) 查找。
public interface PaymentService {
void pay(PaymentContext ctx);
String getChannelCode(); // 業務唯一標識
}
@Service("alipayService")
public class AlipayServiceImpl implements PaymentService {
@Override public String getChannelCode() { return "ALIPAY"; }
@Override public void pay(PaymentContext ctx) { /* 支付寶邏輯 */ }
}
@Service("wechatPayService")
public class WechatPayServiceImpl implements PaymentService {
@Override public String getChannelCode() { return "WECHAT"; }
@Override public void pay(PaymentContext ctx) { /* 微信邏輯 */ }
}
@Service("unionPayService")
public class UnionPayServiceImpl implements PaymentService {
@Override public String getChannelCode() { return "UNIONPAY"; }
@Override public void pay(PaymentContext ctx) { /* 銀聯邏輯 */ }
}
@Service
public class PaymentServiceRegistry implements ApplicationContextAware, InitializingBean {
private final Map registry = new ConcurrentHashMap<>();
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.applicationContext = ctx;
}
@Override
public void afterPropertiesSet() {
// 1. 從容器獲取所有 PaymentService 類型的 Bean
Map beans = applicationContext.getBeansOfType(PaymentService.class);
// 2. 註冊到 Map (Key: getChannelCode(), Value: Bean 實例)
for (PaymentService service : beans.values()) {
String key = service.getChannelCode(); // 業務代碼作為 Key
if (registry.putIfAbsent(key, service) != null) {
throw new IllegalStateException("重複的支付渠道代碼: " + key);
}
}
System.out.println("✅ 支付服務註冊表初始化完成: " + registry.keySet());
}
/** 運行時動態獲取 */
public PaymentService getService(String channelCode) {
PaymentService service = registry.get(channelCode);
if (service == null) {
throw new IllegalArgumentException("不支援的支付渠道: " + channelCode);
}
return service;
}
/** 獲取所有可用渠道 */
public Collection getAvailableChannels() {
return Collections.unmodifiableCollection(registry.keySet());
}
}
@Service
public class OrderPaymentService {
@Autowired
private PaymentServiceRegistry registry;
public void processPayment(Order order, String channelCode) {
// 1. 根據前端傳入渠道代碼獲取具體實現
PaymentService paymentService = registry.getService(channelCode);
// 2. 構建上下文
PaymentContext ctx = new PaymentContext(order.getId(), order.getAmount(), order.getUserId());
// 3. 委派執行
paymentService.pay(ctx);
}
}
@RestController
@RequestMapping("/api/payment")
public class PaymentController {
@Autowired
private PaymentServiceRegistry registry;
@GetMapping("/channels")
public Result> channels() {
return Result.success(new ArrayList<>(registry.getAvailableChannels()));
}
}
| 技術點 | 作用 | 注意事項 |
|---|---|---|
| ApplicationContextAware | 讓 Bean 感知 Spring 容器,可在啟動期主動查找其他 Bean | 僅在單例 Bean 中安全,原型 Bean 需謹慎使用 |
| InitializingBean.afterPropertiesSet() | 屬性注入完成後回調,適合初始化註冊表 | 或用 @PostConstruct 註解方法 |
| getBeansOfType() | 類型安全獲取所有實現 Bean (含繼承/介面) | 不會觸發懶加載 Bean 初始化 |
| ConcurrentHashMap | 執行緒安全註冊表,支援高併發讀取 | 啟動期單執行緒寫入,運行期唯讀 |
| 業務代碼作為 Key | 穩定、語義清晰、前後端約定、不依賴 Bean 名稱 | 由實現類自己定義 getChannelCode(),擴展無需修改註冊表 |
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PaymentChannel {
String code();
}
@PaymentChannel(code = "ALIPAY")
@Service
public class AlipayServiceImpl implements PaymentService { ... }
// 註冊表掃描註解
for (PaymentService service : beans.values()) {
PaymentChannel anno = service.getClass().getAnnotation(PaymentChannel.class);
String key = (anno != null) ? anno.code() : service.getClass().getSimpleName();
registry.put(key, service);
}
@Service
@Profile("prod") // 僅生產環境註冊
public class AlipayServiceImpl implements PaymentService { ... }
@Profile("test")
public class MockPaymentServiceImpl implements PaymentService { ... }
public PaymentService getService(String channelCode) {
return Metrics.timer("payment.service.lookup")
.record(() -> {
PaymentService s = registry.get(channelCode);
if (s == null) throw new IllegalArgumentException(...);
return s;
});
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 註冊表模式 (本文) | O(1) 查找、啟動期驗證、擴展開閉原則、業務代碼解耦 | 需實現類配合定義 Key | **多實現動態路由首選** |
| @Qualifier + @Autowired | 編譯期類型安全、IDE 導航 | **無法動態決定**、需修改注入點 | 固定依賴、少數實現 |
| ApplicationContext.getBean(name) | 簡單直接 | **運行時字串風險**、耦合容器、難測試 | 臨時/遷移期方案 |
| 策略模式 + Map 手動注入 | 完全控制、無 Spring 依賴 | 樣板代碼多、易遺漏註冊 | 非 Spring 環境、單元測試 |
| Spring 4.3+ @Autowired List/Map | 自動收集同類型 Bean | Key 為 Bean 名稱 (不穩定)、順序不定 | 簡單聚合場景 |
String getCode(),強制實現類提供穩定標識。ApplicationContextAware + InitializingBean,啟動期構建 ConcurrentHashMap。@Service 並實現 getCode(),無需動註冊表代碼。**「註冊表模式 + ApplicationContextAware」** 是 Spring 環境下解決「介面多實現動態路由」的**標準最佳實踐**。它將「如何選擇實現」從業務代碼剝離到基礎設施層,符合開閉原則、單一職責、依賴倒置,且效能極佳 (O(1) 查找)。掌握此模式,能優雅解決支付渠道、消息發送、文件解析、報表導出等大量多策略動態選擇場景。