痛点分析
我司的一个项目是集成项目,需要实时获取内部系统的数据。由于安全要求,我们无法直接访问对方的数据库或复用其 Java 服务,而只能采用基于 HTTP 的接口调用方案。具体实现上,使用 hutool 的 HttpUtil 来发送 HTTP 请求,后续再使用 Jackson 等 JSON 解析工具来处理响应结果。虽然 hutool 已经很方便了,但在实际使用中还是存在一些痛点。
来看一下原始的代码:
GET 请求示例
// 获取token
String token = tokenUtils.getZXJCToken();
// 构建请求
HttpRequest request = HttpUtil.createGet(getSmallWeatherListUrl)
.header("authorization", token)
.header("Content-Type", "application/json");
// 添加请求参数
request.form("device_id", device_id);
request.form("page", page);
request.form("pageSize", pageSize);
request.form("startTime", startTime);
request.form("endTime", endTime);
Log.get().info("发送请求【{}】,token:{},参数:device_id={}, page={}, pageSize={}, startTime={}, endTime={}",
getSmallWeatherListUrl, token, device_id, page, pageSize, startTime, endTime);
// 执行请求并获取响应
HttpResponse response = request.execute();
String responseBody = response.body();
Log.get().info("【{}】请求返回:{}", getSmallWeatherListUrl, responseBody);
POST 请求示例
// 获取token
String token = tokenUtils.getUAVToken();
// 构建请求体
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode request = objectMapper.createObjectNode();
request.put("startDate", startDate);
request.put("endDate", endDate);
request.put("planType", planType);
request.put("voltageLevel", voltageLevel);
String requestString = request.toString();
// 构建请求
HttpRequest request = HttpUtil.createPost(taskExecuteStatUrl)
.header("token", token)
.header("Content-Type", "application/json")
.body(requestString);
Log.get().info("发送请求【{}】,token:{},请求体:{}",
taskExecuteStatUrl, token, requestString);
// 执行请求并获取响应内容
HttpResponse response = request.execute();
String responseBody = response.body();
Log.get().info("【{}】请求返回:{}", taskExecuteStatUrl, responseBody);
如果上述类似的代码只是偶尔写一下其实也无所谓,但在集成项目中这种代码随处可见,每一次发起请求,都要配置请求参数、设置请求头、打印日志(包括请求之前和拿到请求结果后)... 搞得代码又臭又长。由于经常需要写类似的代码,所以是否可以把这些步骤封装得更优雅一点?
使用建造者模式重构
经过思考,我决定使用建造者模式来重构这段代码。为什么选择建造者模式呢?因为:
- HTTP 请求包含多个组成部分(URL、请求方法、请求头、请求参数、请求体等)
- 这些部分的设置顺序并不重要
- 最终需要一个统一的方式来执行请求
于是我设计了一个 HttpRequestBuilder 类:
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class HttpRequestBuilder {
private String url;
private String method = "GET";
private final Map<String, String> headers;
private final Map<String, Object> queryParams;
private String bodyJsonStr = "{}";
private String contentType = "application/json";
private HttpRequestBuilder() {
this.headers = new HashMap<>();
this.queryParams = new HashMap<>();
}
public static HttpRequestBuilder create() {
return new HttpRequestBuilder();
}
public HttpRequestBuilder url(String url) {
this.url = url;
return this;
}
public HttpRequestBuilder get() {
this.method = "GET";
return this;
}
public HttpRequestBuilder post() {
this.method = "POST";
return this;
}
public HttpRequestBuilder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public HttpRequestBuilder headers(Map<String, String> headers) {
this.headers.putAll(headers);
return this;
}
public HttpRequestBuilder queryParam(String key, Object value) {
this.queryParams.put(key, value);
return this;
}
public HttpRequestBuilder body(String bodyJsonStr) {
this.bodyJsonStr = bodyJsonStr;
return this;
}
public HttpRequestBuilder contentType(String contentType) {
this.contentType = contentType;
return this;
}
public String execute() {
if (url == null) {
throw new IllegalStateException("必须设置请求的url!");
}
HttpResponse response = sendRequest();
return response.body();
}
private HttpResponse sendRequest() {
HttpRequest request;
if ("POST".equals(method)) {
request = HttpRequest.post(url);
request.body(bodyJsonStr);
log.info("发送请求【{}】,headers:{},请求体:{}", url, headers, bodyJsonStr);
} else {
request = HttpRequest.get(url);
queryParams.forEach(request::form);
log.info("发送请求【{}】,headers:{},参数:{}", url, headers, queryParams);
}
headers.forEach(request::header);
if (!headers.containsKey("Content-Type")) {
request.header("Content-Type", contentType);
}
HttpResponse response = request.execute();
log.info("【{}】请求返回:{}", url, response.body());
return response;
}
}
使用这个工具类后,代码就变得清爽多了:
GET 请求示例
HttpResponse response = HttpRequestBuilder.create()
.url(getSmallWeatherListUrl)
.get()
.header("authorization", tokenUtils.getZXJCToken())
.queryParam("device_id", device_id)
.queryParam("page", page)
.queryParam("pageSize", pageSize)
.queryParam("startTime", startTime)
.queryParam("endTime", endTime)
.execute();
String responseBody = response.body();
POST 请求示例
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode request = objectMapper.createObjectNode();
request.put("startDate", startDate);
request.put("endDate", endDate);
request.put("planType", planType);
request.put("voltageLevel", voltageLevel);
String requestString = request.toString();
String responseBody = HttpRequestBuilder.create()
.url(taskExecuteStatUrl)
.post()
.body(requestString)
.header("token", tokenUtils.getUAVToken())
.execute();
重构后的代码有以下优点:
- 代码更简洁:链式调用使得代码结构更加紧凑
- 更少重复:日志打印、Content-Type 设置等通用逻辑都被封装起来了
- 使用更灵活:可以按需设置各种参数,不用的可以直接跳过
- 更好维护:将来如果要修改日志格式或添加新功能,只需要改一个地方