Spring Boot RestClient:单例或请求级实例?测试最优用法探讨
根据Spring官方文档,RestClient的推荐用法是注入RestClient.Builder,在服务类构造方法中构建RestClient实例——这种方式在生产环境运行高效,但在多自定义Builder的测试场景下会遇到问题。
文档明确:当应用仅使用一个Builder时,@RestClientTest和@AutoConfigureMockRestServiceServer可以正常工作;但如果需要为不同外部服务配置多个自定义Builder(比如不同的baseUrl、拦截器),就需要为每个服务绑定独立的Builder,因此将这些Builder注册为Spring Bean是合理选择。
初始实现(存在测试问题)
配置类
@RequiredArgsConstructor @Configuration public class RestClientConfiguration { private final RestClientBuilderConfigurer restClientBuilderConfigurer; @Bean RestClient.Builder microservice1RestClientBuilder() { return createDefaultRestClientBuilder() .baseUrl("http://localhost:8081"); } /** * 逻辑复制自RestClientAutoConfiguration */ private RestClient.Builder createDefaultRestClientBuilder() { RestClient.Builder builder = RestClient .builder() .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS)); return restClientBuilderConfigurer.configure(builder); } }
控制器类
@RequiredArgsConstructor @RestController public class MainController { private final MainService mainService; @GetMapping public String hello() { return mainService.helloService(); } }
服务类(生产友好,但测试失败)
@Service public class MainService { private final RestClient restClient; public MainService(RestClient.Builder microservice1RestClientBuilder) { this.restClient = microservice1RestClientBuilder.build(); } public String helloService() { return restClient.get().uri("/").retrieve().body(String.class); } }
测试类
@SpringBootTest @AutoConfigureMockMvc class MainControllerTest { private MockRestServiceServer microservice1Server; @Autowired private RestClient.Builder microservice1RestClientBuilder; @Autowired private MockMvc mvc; @BeforeEach void init() { microservice1Server = MockRestServiceServer.bindTo(microservice1RestClientBuilder).build(); } @Test void test() throws Exception { microservice1Server .expect(requestTo("http://localhost:8081/")) .andRespond(withSuccess("Hello world", MediaType.TEXT_PLAIN)); mvc.perform(get("/")).andExpect(status().is2xxSuccessful()).andExpect(content().string("Hello world")); } }
问题分析
上述代码测试无法运行,核心原因是:RestClient实例在服务类初始化时就已创建,而MockRestServiceServer是在@BeforeEach阶段才绑定到Builder——此时RestClient已经使用了Builder原来的请求工厂,Mock无法拦截请求。
如果修改服务类每次请求都构建RestClient(如下),测试可以通过,但会导致每次请求创建新实例,造成不必要的资源消耗:
@RequiredArgsConstructor @Service public class MainService { private final RestClient.Builder microservice1RestClientBuilder; public String helloService() { return microservice1RestClientBuilder.build().get().uri("/").retrieve().body(String.class); } }
最优实现方案
核心思路是让RestClient使用的请求工厂可被Spring管理,测试时替换为Mock工厂,同时保持生产环境中RestClient的单例特性。
1. 调整配置类:绑定可管理的请求工厂
为每个自定义Builder单独配置请求工厂Bean,方便测试时替换:
@RequiredArgsConstructor @Configuration public class RestClientConfiguration { private final RestClientBuilderConfigurer restClientBuilderConfigurer; // 为microservice1定义独立的请求工厂Bean @Bean ClientHttpRequestFactory microservice1RequestFactory() { return ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS); } @Bean RestClient.Builder microservice1RestClientBuilder(ClientHttpRequestFactory microservice1RequestFactory) { return createDefaultRestClientBuilder() .baseUrl("http://localhost:8081") .requestFactory(microservice1RequestFactory); } private RestClient.Builder createDefaultRestClientBuilder() { RestClient.Builder builder = RestClient.builder(); return restClientBuilderConfigurer.configure(builder); } }
2. 服务类保持单例RestClient(生产友好)
服务类依然在构造方法中构建RestClient,保持单例特性,避免重复创建:
@Service public class MainService { private final RestClient restClient; public MainService(RestClient.Builder microservice1RestClientBuilder) { this.restClient = microservice1RestClientBuilder.build(); } public String helloService() { return restClient.get().uri("/").retrieve().body(String.class); } }
3. 测试类:替换请求工厂并绑定MockServer
通过@MockBean替换目标请求工厂,再绑定MockRestServiceServer,此时RestClient会使用Mock工厂拦截请求:
@SpringBootTest @AutoConfigureMockMvc class MainControllerTest { private MockRestServiceServer microservice1Server; @Autowired private RestClient.Builder microservice1RestClientBuilder; @Autowired private MockMvc mvc; // 替换microservice1的请求工厂为Mock实现 @MockBean private ClientHttpRequestFactory microservice1RequestFactory; @BeforeEach void init() { microservice1Server = MockRestServiceServer.bindTo(microservice1RestClientBuilder).build(); } @Test void test() throws Exception { microservice1Server .expect(requestTo("http://localhost:8081/")) .andRespond(withSuccess("Hello world", MediaType.TEXT_PLAIN)); mvc.perform(get("/")).andExpect(status().is2xxSuccessful()).andExpect(content().string("Hello world")); } }
4. 单独测试服务类的简化方案
如果不需要启动完整Spring上下文,仅测试服务类,可以使用@RestClientTest配合@Import导入配置类:
@RestClientTest(MainService.class) @Import(RestClientConfiguration.class) class MainServiceTest { @Autowired private MainService mainService; @Autowired private MockRestServiceServer mockServer; @Test void helloServiceTest() { mockServer.expect(requestTo("http://localhost:8081/")) .andRespond(withSuccess("Hello world", MediaType.TEXT_PLAIN)); String result = mainService.helloService(); assertEquals("Hello world", result); } }
方案优势
- 生产环境:RestClient是单例,请求工厂为默认实现,无性能损耗,符合官方推荐用法。
- 测试环境:通过替换请求工厂实现Mock拦截,无需修改服务类逻辑,测试用例简洁可靠。
- 多Builder场景:每个服务的Builder绑定独立的请求工厂Bean,测试时可分别替换,互不干扰。
内容的提问来源于stack exchange,提问作者Anthony Lofton

