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 表单请求:简单参数与复杂对象都能搞定
表单请求分两种情况,简单的和复杂的(用对象封装参数):
-
简单表单请求:直接用@RequestParam注解指定参数就行,适合参数少的情况。比如上面接口里的submitSimpleForm方法,请求方式是GET,name和age这两个参数。
-
复杂表单请求:如果参数很多,咱们可以用对象(比如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实战核心要点
最后总结几个关键点,帮大家快速记住:
- 简洁高效:用HttpExchange写HTTP客户端,不用手动建请求对象,注解一配就好,代码少了很多,开发效率直接拉满。
- 场景全面:表单、JSON、文件上传、自定义请求头这些常用场景,它都能搞定,基本能满足咱们项目里常见的HTTP请求需求。
- 可扩展性强:参数转换、自动扫描Bean、异常处理,都能自己自定义扩展,适配不同的业务场景,不用被框架束缚。
实际项目里:HttpExchange这种声明式客户端,最大优势就是简单便捷,注解一配就能用;而RestTemplate这类编程式客户端,虽然麻烦一点,但胜在灵活,能处理各种复杂场景。