spring boot,

中文版

Spring Boot 3.2 New Feature: HTTP Interface

qihaiyan qihaiyan Follow Dec 24, 2023 · 6 mins read

Spring Boot 3.2 introduces the new HTTP interface for calling HTTP APIs, in a style similar to openfeign.

The complete code is available in the example project https://github.com/qihaiyan/springcamp/tree/main/spring-http-interface.

1. Overview

The HTTP interface is a synchronous way of calling remote APIs, similar to openfeign. Remote API calls are declared as Java interfaces, conceptually like SpringDataRepository, which can greatly reduce boilerplate code.

To make the declared interfaces executable, you also need to specify the underlying HTTP client library via HttpServiceProxyFactory; RestTemplate, WebClient, and RestClient are the three options supported.

2. Adding the HTTP interface

First add the spring-boot-starter-web dependency.

Add one line to build.gradle:

implementation 'org.springframework.boot:spring-boot-starter-web'

3. Declaring the Interface

Remote API call methods are implemented by declaring an interface:

public interface MyService {
    @GetExchange("/anything")
    String getData(@RequestHeader("MY-HEADER") String headerName);

    @GetExchange("/anything/{id}")
    String getData(@PathVariable long id);

    @PostExchange("/anything")
    String saveData(@RequestBody MyData data);

    @DeleteExchange("/anything/{id}")
    ResponseEntity<Void> deleteData(@PathVariable long id);
}

In the code above we declare four methods covering GET/POST/DELETE operations. The first method shows how to pass a header parameter in a remote API call — simply use the RequestHeader annotation.

4. Using the Declared Methods

Like SpringDataRepository, the HTTP interface is very easy to use — just inject the corresponding bean:

public class MyController {
    @Autowired
    private MyService myService;

    @GetMapping("/foo")
    public String getData() {
        return myService.getData("myHeader");
    }

    @GetMapping("/foo/{id}")
    public String getDataById(@PathVariable Long id) {
        return myService.getData(id);
    }

    @PostMapping("/foo")
    public String saveData() {
        return myService.saveData(new MyData(1L, "demo"));
    }

    @DeleteMapping("/foo")
    public ResponseEntity<Void> deleteData() {
        ResponseEntity<Void> resp = myService.deleteData(1L);
        log.info("delete {}", resp);
        return resp;
    }
}

For demonstration purposes we wrote our own controller.

In the controller, we inject the declared HTTP interface:

    @Autowired
    private MyService myService;

When our own endpoint is called, it internally uses the methods declared by the injected MyService to call other systems’ APIs.

RestClient restClient = RestClient.builder(restTemplate).baseUrl("https://httpbin.org").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

5. Implementing the HTTP interface

Spring framework implements the HTTP interface methods through HttpServiceProxyFactory:

@Configuration
public class MyClientConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }

    @Bean
    public MyService myService(RestTemplate restTemplate) {
        restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://httpbin.org"));
        RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

        return factory.createClient(MyService.class);
    }
}

In the configuration above we can see how the bean for the MyService HTTP interface is initialized.

If you want to use the new RestClient from Spring Boot 3.2, the initialization code can be changed to

RestClient restClient = RestClient.builder(restTemplate).baseUrl("https://httpbin.org").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

6. Unit Testing

The usual unit testing techniques still apply to the HTTP interface; see the article: Spring Boot unit testing

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DemoApplicationTest {

    @Autowired
    private TestRestTemplate testRestTemplate;
    @Autowired
    private RestTemplate restTemplate;

    private MockRestServiceServer mockRestServiceServer;

    @Before
    public void before() {
        mockRestServiceServer = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
        this.mockRestServiceServer.expect(ExpectedCount.manyTimes(), MockRestRequestMatchers.requestTo(Matchers.startsWithIgnoringCase("https://httpbin.org")))
                .andExpect(method(HttpMethod.GET))
                .andRespond(MockRestResponseCreators.withSuccess("{\"get\": 200}", MediaType.APPLICATION_JSON));
        this.mockRestServiceServer.expect(ExpectedCount.manyTimes(), MockRestRequestMatchers.requestTo(Matchers.startsWithIgnoringCase("https://httpbin.org")))
                .andExpect(method(HttpMethod.POST))
                .andRespond(MockRestResponseCreators.withSuccess("{\"post\": 200}", MediaType.APPLICATION_JSON));
        this.mockRestServiceServer.expect(ExpectedCount.manyTimes(), MockRestRequestMatchers.requestTo(Matchers.startsWithIgnoringCase("https://httpbin.org")))
                .andExpect(method(HttpMethod.DELETE))
                .andRespond(MockRestResponseCreators.withSuccess("{\"delete\": 200}", MediaType.APPLICATION_JSON));
    }

    @Test
    public void testRemoteCallRest() {
        log.info("testRemoteCallRest get {}", testRestTemplate.getForObject("/foo", String.class));
        log.info("testRemoteCallRest getById {}", testRestTemplate.getForObject("/foo/1", String.class));
        log.info("testRemoteCallRest post {}", testRestTemplate.postForObject("/foo", new MyData(1L, "demo"), String.class));
        testRestTemplate.exchange("/foo", HttpMethod.DELETE, HttpEntity.EMPTY, String.class);
    }
}
qihaiyan
Written by qihaiyan
业精于勤而荒于嬉,行成于思而毁于随