HttpExchange声明式客户端

前一篇笔记我们介绍了RestClient的用法,它是一个命令式的HTTP客户端类,对于简单的接口调用还算方便,但如果用于大型微服务工程,服务间通信用RestClient是不合适的,命令式的写法仍会产生大量样板代码,使用不够方便。事实也证明开发者在这类工程中更倾向于声明式客户端,例如OpenFeign,SpringBoot 2.x时代OpenFeign便是Spring Cloud实现服务间通信给出的标准方案。不过SpringBoot 3.x时代情况又发生了变化,SpringWeb 6.x把声明式客户端给“吞”了,框架内置了声明式HTTP客户端方案HttpExchange,它的功能基本与OpenFeign相同,不过由于HttpExchange是内置的,因此也就没什么必要再引入一套OpenFeign了。

使用HttpExchange

定义HTTP服务接口

使用@HttpExchange系列注解,我们可以像写Feign接口一样定义HTTP客户端接口。常用的注解包括@HttpExchange@GetExchange@PostExchange@PutExchange@DeleteExchange等。

package com.gacfox.demo.client;

import com.gacfox.demo.model.QueryParam;
import com.gacfox.demo.model.User;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.*;

import java.util.List;

@HttpExchange("http://localhost:8080/api/v1")
public interface UserClient {
    @GetExchange("/getUserById")
    User getUserById(@RequestParam Long userId);

    @PostExchange("/postUser")
    User postUser(@RequestBody User user);

    @PostExchange("/getUserList")
    List<User> getUserList(@RequestBody QueryParam queryParam);

    @GetExchange("/download")
    byte[] downloadFile();

    @PostExchange(value = "/upload", contentType = "multipart/form-data")
    void upload(@RequestParam Long userId, @RequestPart("image") Resource image);
}

通过在方法上使用@GetExchange@PostExchange等注解,我们清晰地声明了请求方式、路径、参数绑定方式。相比命令式的RestClient,这种声明式写法更加直观。

配置HttpServiceProxyFactory

要让上面的接口生效,需要通过HttpServiceProxyFactory为其创建动态代理实现。HttpExchange本身并不实现具体的HTTP客户端,而是基于适配器模式设计,它需要RestClient或WebClient两者之一作为底层实现,我们这里使用阻塞式的RestClient,因此你需要完成上一章节中RestClient的配置,有关WebClient可以参考SpringWebFlux中的相关章节。

package com.gacfox.demo.config;

import com.gacfox.demo.client.UserClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
public class HttpClientConfig {
    @Bean
    public UserClient userClient(RestClient restClient) {
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

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

代码中我们通过方法参数注入了RestClient,然后通过RestClientAdapter类适配,最后使用HttpServiceProxyFactory生成接口的代理实例。生成后的UserClient可以直接注入到Service层使用。

在Service中使用

配置完成后,我们就可以像使用普通Spring Bean一样注入并调用我们配置的HTTP客户端接口了。

package com.gacfox.demo.service;

import com.gacfox.demo.model.QueryParam;
import com.gacfox.demo.model.User;
import com.gacfox.demo.client.UserClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class DemoService {
    private final UserClient userClient;

    @Autowired
    public DemoService(UserClient userClient) {
        this.userClient = userClient;
    }

    public User getUserById(Long userId) {
        return userClient.getUserById(userId);
    }

    public User createUser(User user) {
        return userClient.postUser(user);
    }

    public List<User> getUserList(QueryParam queryParam) {
        return userClient.getUserList(queryParam);
    }
}

返回ResponseEntity与泛型支持

如果需要获取完整的响应信息(状态码、响应头等),可以将方法的返回值声明为ResponseEntity<T>

@GetExchange("/getUserById")
ResponseEntity<User> getUserWithResponse(@RequestParam Long userId);

对于复杂泛型(如List<User>@HttpExchange也提供了良好的支持,无需额外使用ParameterizedTypeReference

@PostExchange("/getUserList")
List<User> getUserList(@RequestBody QueryParam queryParam);

文件上传与下载

文件上传可以使用@RequestPart结合ResourceMultipartFile实现。

@PostExchange(value = "/upload", contentType = "multipart/form-data")
void upload(@RequestParam Long userId, @RequestPart("image") Resource image);

文件下载同样简单,直接返回byte[]Resource即可。

@GetExchange("/download")
byte[] downloadFile();

@GetExchange("/download")
Resource downloadAsResource();

关于全局配置与拦截器

前面我们说过HttpExchange本身是基于RestClient或WebClient的,如果需要统一配置超时、请求头、拦截器等,在构建RestClient或WebClient时进行设置即可,HttpExchange中无需再配置什么了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。