You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot RestClient:单例或请求级实例?测试最优用法探讨

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 14:06:01