首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring 靜態指定實現類策略:註冊表模式與 ApplicationContextAware 完整實作

     編輯:Java 相關知識

場景痛點

介面有多個實現類 (如 PaymentService 有 AlipayService, WechatPayService, UnionPayService),需**運行時根據業務參數動態選擇**具體實現,而非編譯期靜態注入。

一、核心方案:註冊表模式 + ApplicationContextAware

**核心思想**:啟動階段掃描所有目標介面的 Bean,按**自定義鍵 (業務代碼/枚舉/類名)** 建立 Map 註冊表,運行時 O(1) 查找。

二、完整實作代碼

1. 介面與多實現 (標註 @Service 指定 Bean 名稱)

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) { /* 銀聯邏輯 */ }
}

2. 註冊表元件 (核心:實現 ApplicationContextAware + InitializingBean)

@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());
    }
}

3. 業務層使用 (策略模式調用)

@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);
    }
}

4. 控制器暴露渠道列表

@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(),擴展無需修改註冊表

四、進階擴展:支援 SpEL 動態路由 / 條件裝配

1. 基於註解的聲明式 Key (避免硬編碼 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);
}

2. 條件裝配 (Profile/Environment 區分)

@Service
@Profile("prod") // 僅生產環境註冊
public class AlipayServiceImpl implements PaymentService { ... }

@Profile("test")
public class MockPaymentServiceImpl implements PaymentService { ... }

3. 鏈路追蹤 / 監控整合

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自動收集同類型 BeanKey 為 Bean 名稱 (不穩定)、順序不定簡單聚合場景

六、最佳實踐總結

  1. 介面定義業務 Key 方法:String getCode(),強制實現類提供穩定標識。
  2. 註冊表單例:實現 ApplicationContextAware + InitializingBean,啟動期構建 ConcurrentHashMap。
  3. 異常快速失敗:Key 衝突啟動拋異常,運行時 Key 不存在拋業務異常。
  4. 測試友好:單元測試可手動構建 Registry 注入 Mock 實現。
  5. 擴展零修改:新增實現類只需加 @Service 並實現 getCode(),無需動註冊表代碼。

結語

**「註冊表模式 + ApplicationContextAware」** 是 Spring 環境下解決「介面多實現動態路由」的**標準最佳實踐**。它將「如何選擇實現」從業務代碼剝離到基礎設施層,符合開閉原則、單一職責、依賴倒置,且效能極佳 (O(1) 查找)。掌握此模式,能優雅解決支付渠道、消息發送、文件解析、報表導出等大量多策略動態選擇場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved