使用「Spring AMQP」開發「Shared-Libaray」踩到「自動裝載」的坑

更新 發佈閱讀 17 分鐘
翻車現場紀錄,在開發時,因忽略「自動裝載」的特性,導致所開發的「Shared-Library」產生臭蟲。

一、踩坑

說實在話,這次的坑,描述起來有些許複雜,且聽筆者娓娓道來。

最近筆者的工作項目是要開發一個基於「SpringBoot」的「Shared-Library」,說穿了就是開發一套僅限於公司內部使用的「第三方套件」;而其最主要的功能就是簡化「MQ」操作方式,而其作法其實就是對「MQ」的使用步驟進行封裝,讓導入該庫的開發者在開發時能夠透過簡單的「方法呼叫」就能將特定訊息放以「MQ」的方式進行傳遞,而不用去了解公司「MQ」的架構,或是那些繁瑣的連線配置、功能參數設定;附帶一提,筆者所使用的是「Rabbit MQ」。

那為什麼要基於「SpringBoot」呢?

事實上,如果只是簡單地與「Rabbit MQ」進行連線,並執行信息拋收的話,那麼的確使用官方所提供的「3th Library」為基底建置也行,而且還不需要與「SpringBoot」捆綁;不過由於公司目前的策略是以收斂技術棧為主,所以,除了許多現行的專案外,未來的專案也幾乎都會以「Spring Framework」作為開發框架。

但架不住「Spring」有提供一系列與基於「AMQP」協定通信相關的組件,其使用起來相當的容易且方便,詳細資訊請見「Messaging with RabbitMQ」,如果對於英文閱讀比較吃力的朋友,也可以參考其「中文文檔」,總之,就是一個字、兩個字的結論:香、真香。

所以在幾經思索後,便決定使用「SpringBoot」作為這個專案的基礎框架,當然,官方對於以「SpringBoot」構建「Shared-Library」的方式是支持的,詳細請見「Creating a Multi Module Project」。

不過人算不如天算,以為會是輕鬆簡單愉快寫意的開發,終究還是踩坑了。

關鍵就在於,敝專案實作的類型是「Shared-Library」;因爲「Shared-Library」的目的是提供給別的專案引入,這是什麼意思呢?

反過來說,若敝專案實作的類型是「Application」,筆者在開發「MQ」相關功能時,就能以「Spring AMQP」中比較建議的方式來配置「Rabbit MQ」的相關參數,包含伺服器網址、 連線信息、功能參數⋯等,也就是將該些參數的值依規定「Key」標示並配置在「application.yml」中。

但如果敝專案實作的類型是「Shared-Library」,我們就必須要考慮到引入專案,倘若筆者在「application.yml」配置了屬於我們「Library」所需要的,與「MQ」操作的必要參數,那麼,萬一引入專案也要操作「MQ」的話,該怎麼辦?

所以,我們得避開以「application.yml」的方式來配置「Rabbit MQ」相關連線參數,而這也就意味著,我們必須自行建立那些原本藉由「自動裝載」,與配置「Rabbit MQ」相關的「Bean」。

事實上,以「手動建立」取代「自動裝載」來產生「Bean」的這個行為並不困難,但必須要清楚的知道「Spring AMQP」利用「自動裝載」做了什麼,否則,一不小心就可能會翻車。

接著,我們就來復盤事故的經過吧。

範例程式碼如下:

@Configuration
@PropertySource("classpath:shared-lib.properties")
public class SharedLibConfig {

@Value("${lib.rabbitmq.hostname}")
private String hostname;
@Value("${lib.rabbitmq.port}")
private String port;
@Value("${lib.rabbitmq.vhost}")
private String vhost;
@Value("${lib.rabbitmq.username}")
private String username;
@Value("${lib.rabbitmq.password}")
public String password;

@Bean("lib-cf")
CachingConnectionFactory connectionFactory() {
CachingConnectionFactory cf = new CachingConnectionFactory();
cf.setHost(hostname);
cf.setPort(Integer.parseInt(port));
cf.setVirtualHost(vhost);
cf.setUsername(username);
connectionFactory.setPassword(password);
return connectionFactory;
}

@Bean("lib-rt")
RabbitTemplate rabbitTemplate(@Autowired @Qualifier("lib-cf") CachingConnectionFactory connectionFactory) {
return new RabbitTemplate(connectionFactory);
}

@Bean("lib-ra")
RabbitAdmin rabbitAdmin(@Autowired @Qualifier("lib-rt") RabbitTemplate rabbitTemplate) {
return new RabbitAdmin(rabbitTemplate);
}
}

在這個範例中,我們分為兩部份來說明,其一是「參數的餵入」,另一是「Bean」物件的建立。

在「參數的餵入」的部分;一如先前所述,我們應避免以「application.yml」的方式來配置參數;但這個說法其實不夠準確,更準確的說法是:我們應該避免以「Spring AMQP」中,其已定義的「Key」來配置參數;也就是說,即使我們用「application.yml」來配置參數,只要我們不是使用「Spring AMQP」中已經定義的「Key」,也是沒問題的;不過考慮到專案的獨立性,因此,筆者仍偏好使用自行建立的設定檔,也就是範例中的「shared-lib.properties」;也因為是自行建立的檔案,因此,在程式中,筆者使用「@PropertySource」來指向目標,而參數的餵入仍是以藉由「@Value」。

接著是在「Bean」物件的建立,在這部分,筆者會給予「指定名稱」,再搭配「@Qualifier」服用,以避免誤注入非目標「Bean」物件。

完成上述配置後,本以為萬無一失了,直到測試那時的無情翻車。

服務連啟動都無法,因為錯誤訊息包含敏感訊息,就不貼出來了,總之,其噴錯內容大意就是在連線到「Queue」時失敗,帳號密碼錯誤、權限不足等。

二、填坑

翻車總是來得這麼突然,雖然錯愕,但問題也不能不修,於是就追查了一下,然就就發現其原因是,由於引入筆者的「Library」的專案,其本身也有使用到「Rabbit MQ」,所以它就依照「Spring AMQP」的建議,在「application.yml」中配置參數,並產生相關的「Bean」物件;這件事本身沒有問題,但為什麼會噴錯呢?

其問題在於,因為筆者已經在「Library」中產生了「CachingConnectionFactory」,所以根據自動裝載的機制,它檢測到「CachingConnectionFactory」已經存在,它就不會再建立另一個,其源碼如下:

raw-image

關鍵在於「@ConditionalOnMissingBean(ConnectionFactory.class)」。

在查到問題後,我們第一個想到的方法是,在目標專案,也就是引入筆者的「Library」的專案中自行產生一個「CachingConnectionFactory」,為保險起見,我們還加上了「@Primary」,但其實這僅是一個「Workaround」的方式。

而且,僅是建立「CachingConnectionFactory」也不夠,因為在範例中,筆者一共建立了三個與「Rabbit MQ」相關的「Bean」,其分別為「CachingConnectionFactory」、「RabbitTemplate」,以及「RabbitAdmin」,但在它的程式中,它只建立了「CachingConnectionFactory」,此時,若程式要使用「RabbitTemplate」或是「RabbitAdmin」,也會如同先前一樣,程式會拿已經存在的「Bean」,而非是再建立一個,那麼就會依然拿到非目標的「Bean」;關於「Spring AMQP」中自動裝載的源碼請參考「RabbitAutoConfiguration」。

那怎麼辦呢?

其實方案很多,譬如剛才說的在「Library」中替專案建立該些「Bean」物件,在程式啟動時,判斷它是否有配置與「Rabbit MQ」有關的參數,有的話,則替它建立,但筆者私心討厭這個方式。

經過評估,筆者最後採用的方式是「簡單工廠模式」加上「單例模式」的方式來實作,建立一個「SharedLibCachingConnectionFactoryFactory」類別,在這個類別中,我們將「Library」所需的「CachingConnectionFactory」以「單例模式」封裝,其程式碼實作如下:

public class SharedLibCachingConnectionFactoryFactory {

private final SharedLibRabbitConnInfo.SharedLibRabbitConnInfo rabbitConnInfo;

private CachingConnectionFactory cachingConnectionFactory;

public SharedLibCachingConnectionFactoryFactory(SharedLibRabbitConnInfo.SharedLibRabbitConnInfo rabbitConnInfo) {
this.rabbitConnInfo = rabbitConnInfo;
}

public CachingConnectionFactory obtainCachingConnectionFactory() {
return Optional.ofNullable(cachingConnectionFactory).orElseGet(
() -> {
this.cachingConnectionFactory = new CachingConnectionFactory();

this.cachingConnectionFactory.setHost(rabbitConnInfo.hostname());
this.cachingConnectionFactory.setPort(Integer.parseInt(rabbitConnInfo.port()));
this.cachingConnectionFactory.setVirtualHost(rabbitConnInfo.vhost());
this.cachingConnectionFactory.setUsername(rabbitConnInfo.username());
this.cachingConnectionFactory.setPassword(rabbitConnInfo.password());

return this.cachingConnectionFactory;
}
);
}
}

接著,再將「SharedLibCachingConnectionFactoryFactory」交由「Spring」管理,如下:

@Configuration
@PropertySource("classpath:shared-lib.properties")
public class SharedLibConfig {

@Value("${lib.rabbitmq.hostname}")
private String hostname;
@Value("${lib.rabbitmq.port}")
private String port;
@Value("${lib.rabbitmq.vhost}")
private String vhost;
@Value("${lib.rabbitmq.username}")
private String username;
@Value("${lib.rabbitmq.password}")
public String password;

@Bean
SharedLibRabbitConnInfo sharedLibRabbitConnInfo() {
return new SharedLibRabbitConnInfo(this.hostname, this.port, this.vhost, this.username, this.password);
}

@Bean
SharedLibCachingConnectionFactoryFactory sharedLibCachingConnectionFactoryFactory(
SharedLibRabbitConnInfo sharedLibRabbitConnInfo) {
return new SharedLibCachingConnectionFactoryFactory(sharedLibRabbitConnInfo);
}

public record SharedLibRabbitConnInfo(String hostname, String port, String vhost, String username,
String password) {
}
}

如此一來,我們就可以完全避開與「Spring AMQP」的碰撞,也可以保留「Library」所需的「CachingConnectionFactory」,事實上,「Singleton」是必須的,我們不可能每次要連線時都去產生一個「CachingConnectionFactory」,這會拖垮服務的效能。

至於「RabbitTemplate」,以及「RabbitAdmin」,由於這兩個物件的建立並不會像建立「CachingConnectionFactory」一樣的吃資源,加上「RabbitAdmin」的使用其境並不多,因此,最後筆者選擇在使用當下時再以「CachingConnectionFactory」建立。

當然,「RabbitTemplate」與「RabbitAdmin」也可以如「CachingConnectionFactory」的方式來建立「Factory」,這部分就請依各自的使用情境評估了。

三、參考資料


留言
avatar-img
碼猿的亂寫手札
2會員
9內容數
總之,這是一隻程序猿的手札;所以其中內容通常與軟體開發相關,但並不限於。
你可能也想看
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
在網路速度有限的情況下,依序記錄不斷產生的資訊,能統計使用者在頁面上操作了哪些功能。
Thumbnail
在網路速度有限的情況下,依序記錄不斷產生的資訊,能統計使用者在頁面上操作了哪些功能。
Thumbnail
建立Maven專案 於pom.xml設定Spring Boot <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://
Thumbnail
建立Maven專案 於pom.xml設定Spring Boot <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://
Thumbnail
本書大多數的內容都以 OO 的概念出發,詳列了許多設計的臭味道,也有大量的例子。個人雖然不會這樣寫程式,但仍是覺得受益良多,至少在 code review 時能更清楚知道該怎麼描述問題。不過,即便不是用 OO 的概念,有些章節還是可以帶來一些想法,用 OO 概念寫程式的人更不該錯過這本好書。
Thumbnail
本書大多數的內容都以 OO 的概念出發,詳列了許多設計的臭味道,也有大量的例子。個人雖然不會這樣寫程式,但仍是覺得受益良多,至少在 code review 時能更清楚知道該怎麼描述問題。不過,即便不是用 OO 的概念,有些章節還是可以帶來一些想法,用 OO 概念寫程式的人更不該錯過這本好書。
Thumbnail
實際就業後,會發現收集與分析需求,通常都不是工程師在做,會有另一群人,以非工程的角度收集及分析需求,然後在開發過程中蹦出不同的火花,於是很好奇另一群人的想法是什麼?我不敢說這本書能完全代表另一群人的想法,但確實能夠得到很多有用的思維。推薦給所有的軟體工程師。
Thumbnail
實際就業後,會發現收集與分析需求,通常都不是工程師在做,會有另一群人,以非工程的角度收集及分析需求,然後在開發過程中蹦出不同的火花,於是很好奇另一群人的想法是什麼?我不敢說這本書能完全代表另一群人的想法,但確實能夠得到很多有用的思維。推薦給所有的軟體工程師。
Thumbnail
本書介紹了戰略設計、管理領域複雜度、實際應用領域驅動設計等主題。透過對核心子領域、支持子領域、限界上下文等概念的探討,提供了領域驅動設計的相關知識。這篇文章中還涉及了微服務、事件驅動架構和資料網格等相關主題,提供了設計系統和應用領域驅動設計的指導。
Thumbnail
本書介紹了戰略設計、管理領域複雜度、實際應用領域驅動設計等主題。透過對核心子領域、支持子領域、限界上下文等概念的探討,提供了領域驅動設計的相關知識。這篇文章中還涉及了微服務、事件驅動架構和資料網格等相關主題,提供了設計系統和應用領域驅動設計的指導。
Thumbnail
軟體系統的發展歷程大多相似,首重解決基本需求、提供操作介面,進而提升安全性、擴充功能、優化操作。
Thumbnail
軟體系統的發展歷程大多相似,首重解決基本需求、提供操作介面,進而提升安全性、擴充功能、優化操作。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News