Http 框架
概述
在使用 Spring 开发后端项目的过程中,可能会用到两类技术,同步与异步,对应着 Servlet 与 WebFlux。这是两种截然不同的开发方式,这就导致在项目开发过程中,如果涉及到网络访问的话,就得针对不同的技术体系使用不同的网络访问框加。目前比较流行的是:
- 同步调用:Feign、RestTemplate
- 异步调用:WebClient
由于不同的网络框架在使用上存在不同的 API、调用逻辑等,为了统一调用方式(洁癖),因此在第三方网络框架的基础上,封装了这个 Http 框架。
这个 Http 框架的网络访问层通过 HttpExecutor 接口[链接]进行抽象,通过实现不同的 HttpExecutor 完成对第三方网络框架的封装。目前提供了以下第三方网络框架的封装:
- HttpClient(Java 11 内置):支持同步调用
- OKHttp:支持同步调用
- WebClient:支持响应式调用
本框架支持通过 Contract 接口[链接]实现了通过注解和动态代理实现了类似远程服务调用的功能。目前 Contract 接口实现了以下封装:
- SpringContract:复用 Spring Web 提供的注解,实现远程调用
这个功能参考了 Feign 的实现思路
同步调用
同步网络调用的主要特点,是在发起网络请求之后,需要阻塞当前线程,等待网络交互完毕之后再执行接下来的逻辑。这种调用方式常用于 Servlet 技术体系。
基础用法
基础用法可以通过 HttpClient、HttpRequest、HttpResposne 直接构造准确的 Http 网络请求对象。
java
private HttpClient client;
public void test() throws Throwable {
// 使用 OkHttp 作为通信框架
this.client = new HttpClient(OkHttpExecutor.Default());
// 添加切面处理器
this.client.addProcessor(new LoggerProcessor());
// 创建 Request,由于 Request 可能会有 Body,因此建议使用 try 方法来自动关闭请求体
try (var request = HttpRequest.get(HttpUrl.create("/api/accounts").setQuery("id", "accountId"))) {
// 添加请求头
request.addHeaders(headers -> {
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
})
// 添加 Cookie
.setCookie("Authorization", "xxx");
// 执行请求,使用 try 语法确保 response 在使用之后被关闭
try (var response = this.client.execute(request)) {
// 获取响应状态码
var status = response.getStatus();
// 获取响应头
var headers = response.getHeaders();
// 获取响应体
var body = response.getBody();
// 将响应体解析成字符串
var stringBody = body.extract(StringExtractor.of(StandardCharsets.UTF_8));
// 将响应体(JSON 格式)解析成对象
var accountBody = body.extract(JsonExtractor.of(TypeReference.of(Account.class)));
// 将响应体解析成文件(保存到 tmp 目录,并自动从响应头里解析文件名)
var fileBody = body.extract(FileExtractor.of(new File("./tmp")));
}
}
}
服务代理用法
在日常工作中,基础用法显得比较繁琐,因此更多情况下,我们都是使用服务代理的方式进行网络请求。
使用服务代理的方式调用网络时,需要先声明接口,下面的代码演示了如何使用 Spring Web 提供的注解声明代理接口。
java
// 全局添加请求路径
@RequestMapping("/api")
public interface ServerApi {
/**
* 使用 GET 方法请求 /api/accounts
*
* @param id 添加名为 id 的 URL 参数
* @return 将结果解析为 Account 对象
*/
@GetMapping("/accounts")
Account findById(@RequestParam String id);
/**
* 使用 POST 方法请求 /api/accounts
* 要求使用 application/json 格式提交请求体
*
* @param params 由于 consumes 指定使用 application/json,因此参数会被序列化为 JSON 并提交到服务器
*/
@PostMapping(value = "/accounts", consumes = MediaType.APPLICATION_JSON_VALUE)
Account create(@RequestBody AccountParams params);
/**
* 使用 DELETE 方法请求 /api/accounts
*
* @param ids 添加名为 ids 的 URL 参数
* @return 将结果解析为 long 类型
*/
@DeleteMapping("/accounts")
long deleteById(@RequestParam List<String> ids);
/**
* 使用 POST 方法请求 /api/accounts/avatars
* 要求使用 multipart/form-data 格式提交请求体
*
* @param avatar 名为 avatar 的文件参
* @param id 名为 id 的参数
* @return 将结果解析为 Upload 对象
*/
@PostMapping(value = "/accounts/avatars", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Upload upload(@RequestPart File avatar, @RequestPart String id);
/**
* 使用 GET 方法请求 /api/accounts/avatars
*
* @param id 添加名为 id 的 URL 参数
* @return 将结果解析为文件
*/
@GetMapping("/accounts/avatars")
File download(@RequestParam String id);
/**
* 使用 GET 方法请求 /api/accounts
*
* @param id 添加名为 id 的 URL 参数
* @return 直接返回 HttpResponse 对象,自行解析响应体(注意要关闭响应)
*/
@GetMapping("/accounts")
HttpResponse execute(@RequestParam String id);
/**
* 执行指定的请求
*
* @param request 自定义请求
* @return 将结果解析为 Account 对象
*/
Account execute(HttpRequest request);
}
java
private ServerApi api;
public void test() {
this.api = HttpProxyFactory.builder(JavaExecutor.Default())
// 指定服务器地址
.baseUrl("http://127.0.0.1:8080")
// 指定缓存目录
.tmp(new File("./cache"))
// 指定使用 Spring 注解
.contact(new SpringContract())
// 添加切面处理器
.processor(new AddHeaderProcessor("X-Forwarded-Proto", "https"))
.processor(new AddHeaderProcessor("X-Forwarded-Port", "443"))
// 生成服务代理
.target(ServerApi.class);
// 通过代理方法调用网络
var account = this.api.findById("accountId");
}
异步调用
待补充