SpringBoot 3.2引入了新的 HTTP interface 用于http接口调用,采用了类似 openfeign 的风格。
具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-http-interface
一、概述
HTTP Interface 是一个类似于 openfeign 的同步接口调用方法,采用 Java interfaces 声明远程接口调用的方法,理念上类似于SpringDataRepository,可以很大程度精简代码。
要使远程调用的接口可以执行,还需要通过 HttpServiceProxyFactory 指定底层的http接口调用库,支持 RestTemplate、WebClient、RestClient三种。
二、引入 HTTP interface
首先引入 spring-boot-starter-web 依赖。
在 build.gradle 中增加一行代码:
implementation 'org.springframework.boot:spring-boot-starter-web'
三、声明接口调用 Interface
通过声明 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);
}
在上述代码中,我们分别声明了包括 GET/POST/DELETE 操作的四个方法,其中第一个方法演示了如何在远程接口调用时指定header参数,只需要简单的使用 RequestHeader 注解即可。
四、使用声明的方法
类似于SpringDataRepository,使用 HTTP interface 也非常简单,只需要注入对应的 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;
}
}
便于演示方便,我们编写了自己的Controller。
在Controller中,我们注入声明好的 HTTP interface:
@Autowired
private MyService myService;
当我们自己的接口被调用时,接口内部会通过注入的 MyService 声明的方法调用其它系统的接口。
五、实现 HTTP interface
Spring framework 通过 HttpServiceProxyFactory 来实现 HTTP interface 方法,推荐使用 RestClient 作为底层实现:
@Configuration
public class MyClientConfig {
@Bean
public MyService myService() {
RestClient restClient = RestClient.builder().baseUrl("https://httpbin.org").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(MyService.class);
}
}
在上述配置中,我们可以看到 MyService 这个 HTTP interface 对应的 Bean 的初始化方法。
如果项目中仍在使用 RestTemplate,也可以通过 RestTemplateAdapter 基于 RestTemplate 创建客户端,作为兼容写法保留:
@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);
}
}
另外,在 Spring Boot 4 / Framework 7 中还提供了更简洁的方式:通过 @ImportHttpServices(group = "...") 注解配合 HttpServiceClientRegistry 注册 HTTP interface 客户端,无需再逐个手写 HttpServiceProxyFactory 的初始化代码。
六、单元测试
常用的单元测试方法对于 HTTP interface 仍然可用,对应的文章可以参照:springboot单元测试技术
示例基于 JUnit 5,注解均来自 org.junit.jupiter.api 包;@SpringBootTest 自带 SpringExtension,无需再添加 @RunWith(SpringRunner.class)。
@Slf4j
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DemoApplicationTest {
@Autowired
private TestRestTemplate testRestTemplate;
@Autowired
private RestTemplate restTemplate;
private MockRestServiceServer mockRestServiceServer;
@BeforeEach
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);
}
}
需要注意的是,MockRestServiceServer 绑定的对象要与客户端的底层实现保持一致:如果 HTTP interface 客户端是基于 RestClientAdapter 创建的,应使用 MockRestServiceServer.bindTo(restClientBuilder) 绑定(Spring 6.1+ 提供);绑定 RestTemplate 的写法只对基于 RestTemplateAdapter 创建的客户端生效。