場景
最近項目使用了Spring Boot 的STOMP 支持, 來完成服務器與瀏覽器之間的消息通知功能.
STOMP
首先, 簡單介紹一下STOMP 協議, Simple(or Streaming) Text Orientated Messaging Protocol 是一種簡單的消息文本協議, 其核心理念是簡單與可用性.
STOMP 與其它
在腳本語言(如Ruby, Python和Perl) 的編程環境中, 實現完整的消息協議(AMQP 或者JMS)是比較麻煩的, 同時也可能只需要使用部分的消息操作功能. STOMP 在這種環境中, 提供了簡單的消息協議實現的能力.
問題描述
在Java 后臺程序中, 使用了依賴注入的方式進行消息的發送.
@Autowire
private SimpMessageSendingOperations messageSendingOperations;
private void afterConnectionClosed(){
...
messageSendingOperations.convertAndSend("/topic/interrupt", interrupt);
}
程序是能夠正常運行的, 消息也能夠成功地發送給瀏覽器客戶端.
但是, 在該類的運行單元測試的時候, 出現以下的異常
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.messaging.simp.SimpMessageSendingOperations] found for dependency [org.springframework.messaging.simp.SimpMessageSendingOperations]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
問題跟蹤
根據異常的信息判斷, Spring 在運行測試的時候, 對于SimpMessageSendingOperations類型, 沒有找到合適的Bean 來注入.
但是程序是可以運行的, 所以我就debug 了程序的運行, 發現在運行時, 對于SimpMessageSendingOperations 類型, Spring 注入了SimpleMessaggingTemplate 類實例, 而這個類的構造器是需要傳入運行期間構造的MessageChannel 類型的.
而在單元測試中, 是無法構造出SimpleMessaggingTemplate 類實例的.
解決方案也比較單純, 就是在單元測試中, 給SimpMessageSendingOperations 類型注入合適的Bean 即可.
解決方案
方案1
Spring 提供了TestConfiguration 來進行額外Bean 注入. 所以, 第一次的方案是在測試類中, 添加一個內部類來進行Bean 的添加.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = {ApplicationConfig.class})
@TestPropertySource("classpath:application.yml")
public class WebSocketServerTest {
...
@TestConfiguration
static class Config {
@Bean
public static SimpMessageSendingOperations messageSendingOperations() {
return mock(SimpMessageSendingOperations.class);
}
}
...
}
該測試類能夠通過運行.
但是, 在運行項目的check 時, 發現其它測試類報出了同樣的注入失敗問題.
跟蹤下來, 對于使用SpringBootTest 注解的測試類, Spring 都會根據傳入的Config 來掃描所有的組件, 然后把組件所需的Bean 注入, 然后構造出一個ApplicationContext.
所以, 使用該方案的話, 需要在所有的SpringBootTest 注解類中加入內部類來完成Bean 的添加, 這是不太現實的.
方案2
既然內部類的作用范圍受限, 那么就講其提升為頂級類.
@TestConfiguration
public class SpringTestCommonConfig {
@Bean
public static SimpMessageSendingOperations messageSendingOperations() {
return mock(SimpMessageSendingOperations.class);
}
}
然后, 修改所有的SpringBootTest 注解的測試類的類頭注解.
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = {ApplicationConfig.class,SpringTestCommonConfig.class})
這種方式比方案1優雅一點, 但是仍然需要修改所有的SpringBootTest 注解的測試類.
方案3
利用Spring 的組件掃描機制, 將TestConfiguration 類放到掃描到的組件包中.
@ComponentScan(basePackages = {"com.sample"})
public class ApplicationConfig{...}
package com.sample // 將其置于ComponentScan 配置的包中.
@TestConfiguration
public class SpringTestCommonConfig {
@Bean
public static SimpMessageSendingOperations messageSendingOperations() {
return mock(SimpMessageSendingOperations.class);
}
}
使用這種方案, Spring 會在構造ApplicationContext 的時候, 掃描到該測試配置類, 然后將Bean 聲明加入到容器中. 從而達到了全局測試配置的效果.