引言:那些年我們學過的自動裝配
記得畢業那會剛開始學習Spring Boot的時候,自動裝配機制讓我眼前一亮——"約定大於配置"的理念真是太巧妙了!相信很多小夥伴都和我一樣,懷着好奇心去研究@EnableAutoConfiguration和spring.factories的奧秘,甚至動手嘗試編寫過自己的Starter。
但説實話,在實際項目開發中,真正需要自己實現自動裝配的場景並不多。大多數時候,我們都是在使用Spring Boot官方或者第三方提供的Starter。直到最近,我遇到了一個實實在在的需求,才讓我有機會深入實踐這個機制。
背景:Dubbo調用成了性能瓶頸
我在公司參與的這個大型項目採用了典型的微服務架構,各個服務之間通過Dubbo進行調用。項目規模較大,因此分成多個開發小組,每個小組負責不同的微服務模塊。
隨着業務量增長,我們發現了一個棘手的問題:某些高頻的數據查詢操作通過Dubbo調用時,性能開銷變得不可忽視。雖然單次調用的延遲不大,但在高併發場景下,這些開銷累積起來就相當可觀了。同時提供duboo的服務,因為高頻調用已經存在併發瓶頸,頻繁告警,如果繼續增加調用量隨時可能崩潰。(因為數據庫規格較高,瓶頸不在於數據庫,而只在於dubbo服務提供方,且因為各種原因無法進行橫向擴容機器)
經過我們小組討論,決定開發一個多數據源SDK,由我負責實現。讓各個小組能夠通過SDK直連需要的數據庫,減少不必要的Dubbo調用。這個SDK不僅要給其他小組使用,我們自己也打算針對一些高頻調用duboo接口替換為本地調用。
設計思路:條件化自動裝配的多數據源SDK
我的設計目標是開發一個"智能"的SDK,能夠根據配置自動裝配所需的數據源、Dao和Service。業務方只需要引入依賴和添加配置,就可以直接使用相關的服務。
由於SDK中有些還需要包含一些業務邏輯,我們不能只提供DAO層,還需要提供Service層。為了避免與業務項目中可能已經存在的Bean出現名稱衝突,所有Bean都加上了"Sdk"前綴。
SDK項目結構設計
先來看看整個SDK的項目結構:
sdk-multi-datasource/
├── src/main/java/com/example/sdk/
│ ├── config/
│ │ ├── condition/
│ │ │ └── AnySdkDataSourceCondition.java
│ │ ├── datasource/
│ │ │ ├── SdkPrimaryDataConfig.java
│ │ │ └── SdkSecondaryDataConfig.java
│ │ └── SdkAutoConfiguration.java
│ ├── dao/
│ │ ├── primary/
│ │ │ └── SdkAppInfoDao.java
│ │ └── secondary/
│ │ └── SdkOtherDataDao.java
│ ├── service/
│ │ ├── SdkAppInfoService.java
│ │ └── SdkOtherDataService.java
│ ├── entity/
│ └── util/
├── src/main/resources/
│ ├── META-INF/
│ │ └── spring.factories
│ └── mapper/
│ ├── primary/
│ └── secondary/
└── pom.xml
核心代碼實現
1. 條件判斷類:智能感知數據源配置
首先,我創建了一個條件類,用於判斷是否需要啓用自動配置:
public class AnySdkDataSourceCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
// 檢查是否配置了任意一個SDK數據源
// 條件註解的優勢:只有業務方真正配置了數據源,SDK才會生效,避免不必要的Bean加載
return env.containsProperty("spring.datasource.sdk-primary.jdbc-url") ||
env.containsProperty("spring.datasource.sdk-secondary.jdbc-url");
}
}
條件註解的優勢在於它允許我們根據環境動態決定是否啓用某些配置,這樣可以避免加載不必要的Bean,提高應用啓動速度,並且避免與業務項目中可能存在的Bean衝突。
2. 數據源配置:完整的SDK主數據源配置
下面是完整的主數據源配置代碼,我添加了詳細的註釋説明:
@Configuration
// 條件註解:只有配置了sdk-primary數據源時才啓用此配置
@ConditionalOnProperty(prefix = "spring.datasource.sdk-primary", name = "jdbc-url")
// 指定Mapper接口的掃描路徑,並指定SqlSessionFactory的Bean名稱
@MapperScan(
basePackages = "com.example.sdk.dao.primary",
sqlSessionFactoryRef = "sdkPrimarySqlSessionFactory"
)
public class SdkPrimaryDataConfig {
// 主數據源Bean,使用@ConfigurationProperties讀取配置
@Bean(name = "sdkPrimaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.sdk-primary")
public DataSource sdkPrimaryDataSource() {
return DataSourceBuilder.create().build();
}
// 主數據源SqlSessionFactory
@Bean(name = "sdkPrimarySqlSessionFactory")
public SqlSessionFactory sdkPrimarySqlSessionFactory(
@Qualifier("sdkPrimaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 設置Mapper XML文件的位置
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/primary/*.xml"));
return bean.getObject();
}
// 主數據源SqlSessionTemplate
@Bean(name = "sdkPrimarySqlSessionTemplate")
public SqlSessionTemplate sdkPrimarySqlSessionTemplate(
@Qualifier("sdkPrimarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
// 主數據源事務管理器
@Bean(name = "sdkPrimaryTransactionManager")
public DataSourceTransactionManager sdkPrimaryTransactionManager(
@Qualifier("sdkPrimaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
次數據源配置
SdkSecondaryDataConfig的結構與主數據源配置基本相同,區別在於:
- Bean名稱中的"primary"替換為"secondary"
- 掃描的包路徑不同(
com.example.sdk.dao.secondary)- 配置前綴不同(
spring.datasource.sdk-secondary)
3. DAO層接口
為了避免與業務項目中的Bean衝突,所有DAO接口都加上了"Sdk"前綴:
@Mapper
public interface SdkAppInfoDao {
AppInfo getByBusinessId(String businessId);
}
4. Service層實現
Service類也遵循相同的命名規則,為了保持SDK的簡單性和靈活性,我選擇了傳統的setter注入方式:
public class SdkAppInfoService {
private SdkAppInfoDao sdkAppInfoDao;
public void setSdkAppInfoDao(SdkAppInfoDao sdkAppInfoDao) {
this.sdkAppInfoDao = sdkAppInfoDao;
}
public AppInfo getByBusinessId(String businessId) {
// 這裏可以添加具體業務邏輯,如本地緩存、日誌等
return sdkAppInfoDao.getByBusinessId(businessId);
}
}
5. 自動配置類:解決依賴注入問題
這是整個SDK的核心,我通過條件判斷確保只有配置了對應數據源的情況下才創建相應的Service Bean:
@Configuration
@Conditional(AnySdkDataSourceCondition.class)
@Import({SdkPrimaryDataConfig.class, SdkSecondaryDataConfig.class})
public class SdkAutoConfiguration {
// 只有配置了sdk-primary數據源時才創建此Bean
@Bean
@Lazy // 延遲加載,確保DAO先初始化
@ConditionalOnProperty(prefix = "spring.datasource.sdk-primary", name = "jdbc-url")
public SdkAppInfoService sdkAppInfoService(SdkAppInfoDao sdkAppInfoDao) {
SdkAppInfoService service = new SdkAppInfoService();
service.setSdkAppInfoDao(sdkAppInfoDao);
return service;
}
// 只有配置了sdk-secondary數據源時才創建此Bean
@Bean
@Lazy
@ConditionalOnProperty(prefix = "spring.datasource.sdk-secondary", name = "jdbc-url")
public SdkOtherDataService sdkOtherDataService(SdkOtherDataDao sdkOtherDataDao) {
SdkOtherDataService service = new SdkOtherDataService();
service.setSdkOtherDataDao(sdkOtherDataDao);
return service;
}
}
這裏使用了@Conditional(AnySdkDataSourceCondition.class)和@ConditionalOnProperty註解,它的優勢是能夠根據配置文件中的屬性值決定是否創建Bean。這樣設計的好處是:
- 業務方未配置任何sdk數據源時,不會進行自動裝配
- 只有在業務方真正配置了對應數據源時,才會創建相關的Service Bean
- 避免了不必要的Bean創建,減少內存佔用
- 防止因缺少配置而導致的運行時錯誤
@Lazy 的核心作用是延遲 Bean 的初始化時機。在未使用該註解時,由於 Spring Bean 的創建順序不確定,特別是在條件化配置中,Service 可能會在依賴的 Dao 之前被創建,導致注入的 Dao 實例為 null,進而引發異常。這本質上是由於 Bean 的依賴注入時機與初始化順序不匹配所導致的。
通過添加 @Lazy,可以確保 Service 只有在首次被使用時才初始化,此時其依賴的 Dao 必然已經準備就緒,從而從根本上避免了順序問題。
6. 註冊自動配置
最後,在spring.factories中註冊自動配置類:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sdk.config.SdkAutoConfiguration
業務方使用方式
業務方使用我們這個SDK非常簡單:
- 添加依賴:
<dependency>
<groupId>com.example</groupId>
<artifactId>sdk-multi-datasource</artifactId>
<version>1.0.0</version>
</dependency>
- 配置數據源(按照Spring Boot的配置習慣):
spring:
datasource:
sdk-primary:
jdbc-url: jdbc:mysql://primary-db-host:3306/primary_db
username: db_user
password: db_password
driver-class-name: com.mysql.jdbc.Driver
sdk-secondary:
jdbc-url: jdbc:mysql://secondary-db-host:3306/secondary_db
username: db_user
password: db_password
driver-class-name: com.mysql.jdbc.Driver
- 直接使用Service:
@RestController
public class BusinessController {
@Autowired
private SdkAppInfoService sdkAppInfoService;
@GetMapping("/app-info/{businessId}")
public AppInfo getAppInfo(@PathVariable String businessId) {
return sdkAppInfoService.getByBusinessId(businessId);
}
}
效果與反思
通過這個SDK,我們成功將部分高頻的Dubbo調用改為了本地數據庫直連,顯著降低了延遲和系統負載。各個小組的反響也很好,他們喜歡這種"開箱即用"的體驗。
條件註解的使用讓我們的SDK更加智能和靈活:
- 按需加載:只有配置了數據源時才會加載相關Bean
- 避免衝突:通過條件判斷和Bean命名約定,避免了與業務項目的Bean衝突
- 靈活配置:業務方可以根據需要選擇啓用哪些數據源
架構思考:微服務與單體的平衡
這個優化過程讓我思考微服務架構與單體架構之間的平衡。微服務架構帶來了清晰的服務邊界和獨立的擴展性,但也**引入了網絡調用開銷和分佈式系統的複雜性。
通過這個多數據源SDK,我們找到了一種折中方案:既保持了微服務的架構優勢,又在特定場景下獲得了接近單體架構的性能。
最重要的是根據實際場景選擇最合適的方案。 在這個微服務大行其道的時代,偶爾迴歸"單體"思維,反而能讓我們找到更好的平衡點。
從微服務到"部分單體",這不是倒退,而是架構思維的成熟。作為開發者,我們應該保持開放的心態,根據實際需求選擇最合適的技術方案,而不是盲目追隨技術潮流。