写个接口就能调HTTP?Spring的这个隐藏神器,你必须知道

0 阅读10分钟

1. 从繁琐到省心,HttpExchange登场

在Spring生态里做HTTP客户端开发,以前大家大多用的是编程式写法,手动建请求对象、请求参数、处理响应。如下以RestTemplate发送简单表单请求的例子:

// RestTemplate 发送表单请求示例
@Service
public class RestTemplateDemoService {
    @Autowired
    private RestTemplate restTemplate;

    public ResponseVO<String> submitSimpleForm(String name, int age) {
        // 1. 手动构建请求头,指定表单格式
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        
        // 2. 手动封装表单参数
        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
        params.add("name", name);
        params.add("age", age);
        
        // 3. 手动构建请求实体
        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers);
        
        // 4. 手动发送请求,指定URL、请求方式、响应类型
        return restTemplate.postForObject(
            "http://localhost:8081/httpexchange/api/form/simple",
            requestEntity,
            new ParameterizedTypeReference<ResponseVO<String>>() {}
        );
    }
}

为了解决手动处理这个痛点,Spring Framework就推出了HttpExchange注解,给我们提供了一种超省心的声明式HTTP客户端开发方式。

还是同样的表单请求,用HttpExchange就简单多了,示例如下:

// HttpExchange 发送表单请求示例(接口声明式)
@HttpExchange(url = "http://localhost:8081/httpexchange/api")
public interface HttpExchangeClient2 {
    // 仅需注解配置,无需手动构建请求
    @GetExchange(url = "form/simple")
    ResponseVO<String> submitSimpleForm(@RequestParam("name") String name, @RequestParam("age") int age);
}

和RestTemplate那种要手动写一堆代码的命令式编程比起来,HttpExchange就轻松多啦~ 是不是一下子就想到了Feign或者Dubbo?没错!都是只要写个接口声明,剩下的活儿,框架都会自动帮我们搞定。

本文从实际应用出发,给大家讲明白HttpExchange的各种请求场景、自定义扩展怎么玩、常用注解有啥用,还有返回值该怎么选,帮大家快速上手,把它用到自己的项目里。

2. 实战:常用场景+自定义扩展

咱们先定义一个HttpExchange客户端接口,里面包含了表单请求、JSON请求、文件请求这些平时常用的场景,后面就围绕这个接口,一步步讲解。

@HttpExchange(url = "http://localhost:8081/httpexchange/api")
public interface HttpExchangeClient2 {

    // 1. 表单请求
    @PostExchange(url = "form/submit", contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    ResponseVO<String> submitForm(FormRequestVO formRequest);

    @GetExchange(url = "form/simple")
    ResponseVO<String> submitSimpleForm(@RequestParam("name") String name, @RequestParam("age") int age);

    // 2. JSON请求
    @PostExchange(url = "json/submit")
    ResponseVO<JsonRequestVO> submitJson(@RequestBody JsonRequestVO jsonRequest);

    // 3. 文件请求
    @PostExchange(url = "file/upload", contentType = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseVO<String> uploadFile(@RequestPart("file") MultipartFile file);

    // 4. 包含文件的表单请求
    @PostExchange(url = "multipart/submit", contentType = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseVO<String> submitMultipart(@RequestPart("name") String name, @RequestPart("age") int age, @RequestPart("file") MultipartFile file);

    // 5. 自定义请求头
    @GetExchange(url = "header/custom")
    ResponseVO<String> getCustomHeader(@RequestHeader("X-User-Id") String userId, @RequestHeader("Authorization") String authorization);

    // 6. 超时测试
    @GetExchange(url = "timeout/test")
    ResponseVO<String> testTimeout();

    // 7.使用 URL uri, HttpMethod method
    @HttpExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    ResponseVO<String> submitForm(URI uri, HttpMethod method, FormRequestVO formRequest);

}

2.1 核心场景实战:覆盖日常HTTP请求需求

2.1.1 表单请求:简单参数与复杂对象都能搞定

表单请求分两种情况,简单的和复杂的(用对象封装参数):

  1. 简单表单请求:直接用@RequestParam注解指定参数就行,适合参数少的情况。比如上面接口里的submitSimpleForm方法,请求方式是GET,name和age这两个参数。

  2. 复杂表单请求:如果参数很多,咱们可以用对象(比如FormRequestVO)把参数包起来,但有个小注意点——默认情况下,HttpExchange不支持直接把对象当表单参数发送,得自己写个HttpServiceArgumentResolver,把对象转换成表单参数才行。

2.1.2 JSON请求:自动序列化,不用手动配置

JSON请求更简单啦!用@RequestBody注解指定请求体参数,HttpExchange会自动把对象(比如JsonRequestVO)转成JSON格式,还会自动设置请求头Content-Type为application/json,对应接口里的submitJson方法。

2.1.3 文件请求:单文件上传,几步就能实现

文件上传也不复杂,只要注意两点:一是请求头Content-Type要设为multipart/form-data,二是文件参数用@RequestPart注解,类型选MultipartFile就行,如接口里的uploadFile方法。

2.1.4 包含文件的表单请求:普通参数+文件协同传输

如果表单里既有普通参数(比如name、age),又有文件,和单文件上传一样,也要设Content-Type为multipart/form-data。普通参数用@RequestPart/@RequestParam注解,对应接口里的submitMultipart方法。

2.1.5 自定义请求头:按需配置,自动携带

有时候需要传自定义请求头,比如用户ID、授权信息,这时候用@RequestHeader注解就搞定了。比如接口里的getCustomHeader方法。

2.1.6 超时测试:提前规避异常,保障程序稳定

接口里的testTimeout方法,是用来测试请求超时的。后面我们会讲自定义异常处理,到时候就能捕获超时异常,统一处理。

2.1.7 动态URL与请求方式:灵活适配多变场景

有时候请求地址或者请求方式不固定,这时候就可以用URI和HttpMethod这两个参数,动态指定。比如接口里的submitForm方法,没有固定用@GetExchange或@PostExchange,而是通过参数动态设置。

2.2 自定义扩展:解锁更多玩法

2.2.1 扩展实现:表单请求支持对象参数传输

刚才说过,默认情况下HttpExchange不能直接把对象当表单参数发送,所以得自己写个扩展——HttpServiceArgumentResolver,实现对象到表单参数的转换

部分代码如下(文末源码):

public class FormArgumentResolver implements HttpServiceArgumentResolver {

    @Override
    public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {

        // 检查是否有 @RequestBody 注解,有则由 JSON 处理器处理,不进行表单转换
        if (parameter.hasParameterAnnotation(RequestBody.class)) {
            return false;
        }

        // 检查是否有 @RequestPart 注解(用于 multipart/form-data),有则由 multipart 处理器处理
        if (parameter.hasParameterAnnotation(RequestPart.class)) {
            return false;
        }

        // 对于没有特殊注解的复杂对象,进行表单转换
        if (shouldConvertToForm(parameter)) {
            convertToFormParams(argument, requestValues);
            return true;
        }

        return false;
    }
}

简单说下:这个resolver会跳过带@RequestBody和@RequestPart注解的参数,只处理没有特殊注解的复杂对象,通过反射拿到对象的字段,转成表单键值对,加到请求参数里。

2.2.2 自动扫描:无需手动注入,自动创建HttpExchange代理Bean

Spring 默认需要用@Bean 声明每个client bean,代码如下


@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        HttpClient httpClient = HttpClient.create().responseTimeout(Duration.ofMillis(500)) // 缩短响应超时到500毫秒
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500); // 缩短连接超时到500毫秒
        return WebClient.builder().defaultHeader("Accept-Charset", "UTF-8").clientConnector(new ReactorClientHttpConnector(httpClient)).build();
    }

    @Bean
    public HttpServiceProxyFactory httpServiceProxyFactory(WebClient webClient) {
        return HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient)).exchangeAdapterDecorator(ExceptionReactorHttpExchangeAdapterDecorator::new)
                .customArgumentResolver(new FormArgumentResolver()).build();
    }
    //定义HttpExchangeClient2 
    @Bean
    public HttpExchangeClient2 httpExchangeClient(HttpServiceProxyFactory httpServiceProxyFactory) {
        return httpServiceProxyFactory.createClient(HttpExchangeClient2.class);
    }
}

client对象多了这回很麻烦。所以写了个扩展,实现BeanDefinitionRegistryPostProcessor接口,让Spring自动扫描带@HttpExchange注解的接口,自动创建代理Bean,就像使用mybatis Mapper接口那样,部分代码如下(文末源码):

public class HttpExchangeClientRegistry implements BeanDefinitionRegistryPostProcessor {

    private static final Logger logger = LoggerFactory.getLogger(HttpExchangeClientRegistry.class);

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 获取 Spring 自动配置的包路径
        List<String> packages = AutoConfigurationPackages.get((BeanFactory) registry);
        // 创建扫描器,扫描带有 @HttpExchange 注解的接口
        HttpExchangeClientScanner scanner = new HttpExchangeClientScanner(registry);
        scanner.scan(packages.toArray(new String[0]));
    }

}

简单总结下:这个实现会自动扫描项目里带@HttpExchange注解的接口,通过FactoryBean自动创建client代理对象。

2.2.3 异常处理:统一捕获,避免程序崩溃

请求过程中难免会出现异常,比如超时、连接失败,要是不处理,程序可能会崩溃。可以继承ReactorHttpExchangeAdapterDecorator/HttpExchangeAdapterDecorator(异步/同步),做一个异常处理的装饰器,统一捕获和处理异常,

部分实现如下(文末源码):

public class ExceptionReactorHttpExchangeAdapterDecorator extends ReactorHttpExchangeAdapterDecorator {

    private Logger logger = LoggerFactory.getLogger(ExceptionReactorHttpExchangeAdapterDecorator.class);

    public ExceptionReactorHttpExchangeAdapterDecorator(HttpExchangeAdapter delegate) {
        super(delegate);
    }

    /**
     * 处理同步请求(无响应体)的异常
     */
    @Override
    public <T> T exchangeForBody(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
        try {
            return super.exchangeForBody(values, bodyType);
        } catch (WebClientRequestException ex) {
            logger.error("同步请求(无响应体)异常", ex);
            // 可根据实际需求返回默认值或抛出自定义异常
            return null;
        }
    }


3. 常用注解速查

常用的HttpExchange注解如下:

注解名称核心作用使用场景
@HttpExchange标记HTTP客户端接口/方法,定义基础URL和默认配置接口类上(设基础URL)或方法上(覆盖URL、设请求方式)
@GetExchange指定请求方式为GET,可配置URL、请求头等,是@HttpExchange的GET简化版发送GET请求(如查询数据),可配合@RequestParam、@PathVariable使用
@PostExchange指定请求方式为POST,可配置URL、请求头、 contentType等,是@HttpExchange的POST简化版发送POST请求(如提交数据、上传文件),可配合@RequestBody、@RequestPart使用
@RequestHeader指定请求头参数,把方法参数映射成请求头需要传自定义请求头(如Authorization、X-User-Id)
@PathVariable指定URL路径参数,把方法参数映射成URL占位符URL有动态路径(如/user/{id})的场景
@RequestAttribute获取请求属性(不是请求参数),从请求上下文拿值传上下文属性(如用户信息),不用请求头/参数传
@RequestBody指定请求体参数,把方法参数转成请求体(默认JSON)JSON请求等需要传请求体的场景
@RequestParam指定表单/URL查询参数,把方法参数映射成请求参数简单表单、GET请求的查询参数场景
@RequestPart指定multipart/form-data类型请求参数,可传普通/文件参数文件上传、带文件的表单请求场景
@CookieValue指定Cookie参数,把方法参数映射成请求Cookie需要传Cookie(如会话ID)的场景

4. 返回值详解:按客户端类型,选对返回方式

HttpExchange的返回值类型,和底层用的HTTP客户端(RestTemplate、RestClient、WebClient)有关,不同客户端支持的返回值不一样,规则如下:

底层客户端支持的返回值类型说明
RestTemplate、RestClient、WebClient(同步返回)HttpHeaders只返回响应头,没有响应体,适合只需要请求头的场景
(自定义实体类)直接返回响应体,自动转成指定类型T,适合只需要响应体的场景
ResponseEntity返回响应状态+响应头,没有响应体,适合只判断请求是否成功
ResponseEntity返回状态+头+体,能拿到完整响应信息,最常用
WebClient、RestClient(异步返回)Mono异步无响应体,适合异步请求且不用返回结果(如通知、日志)
Mono异步返回响应头,没有响应体,适合异步拿请求头
Mono异步返回单个响应体,自动转成T,适合异步拿单个结果
Flux异步返回多个响应体(流式),适合拿批量、流式数据
Mono<ResponseEntity>异步返回状态+头,无响应体,适合异步判断请求是否成功
Mono<ResponseEntity>异步返回完整响应(状态、头、体),适合异步拿完整信息
Mono<ResponseEntity<Flux>>异步返回流式响应完整信息,适合异步拿流式数据+状态

💡 补充说明:RestClient底层可以用RestTemplate或WebClient,所以既能同步请求,也能异步请求,两种返回值都支持;WebClient本身是异步客户端,但也能通过阻塞操作,同步返回结果,很灵活。

5. 总结:HttpExchange实战核心要点

最后总结几个关键点,帮大家快速记住:

  1. 简洁高效:用HttpExchange写HTTP客户端,不用手动建请求对象,注解一配就好,代码少了很多,开发效率直接拉满。
  2. 场景全面:表单、JSON、文件上传、自定义请求头这些常用场景,它都能搞定,基本能满足咱们项目里常见的HTTP请求需求。
  3. 可扩展性强:参数转换、自动扫描Bean、异常处理,都能自己自定义扩展,适配不同的业务场景,不用被框架束缚。

实际项目里:HttpExchange这种声明式客户端,最大优势就是简单便捷,注解一配就能用;而RestTemplate这类编程式客户端,虽然麻烦一点,但胜在灵活,能处理各种复杂场景。

源码连接:download.csdn.net/download/Th…