使用建造者模式优化 HTTP 请求工具类

在我司的一个集成项目中,我们需要频繁使用基于 HTTP 的接口调用来获取内部系统数据。虽然使用 hutool 的 HttpUtil 已经相对便捷,但在实际开发中发现代码仍然显得冗长,需要重复编写配置请求参数、设置请求头和打印日志等模板代码。为了解决这个问题,我使用建造者模式设计了一个 HttpRequestBuilder 工具类,通过链式调用的方式,不仅让代码更加简洁优雅,还把通用逻辑都封装起来,大大提升了代码的可维护性。

痛点分析

我司的一个项目是集成项目,需要实时获取内部系统的数据。由于安全要求,我们无法直接访问对方的数据库或复用其 Java 服务,而只能采用基于 HTTP 的接口调用方案。具体实现上,使用 hutoolHttpUtil 来发送 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 设置等通用逻辑都被封装起来了
  • 使用更灵活:可以按需设置各种参数,不用的可以直接跳过
  • 更好维护:将来如果要修改日志格式或添加新功能,只需要改一个地方
LICENSED UNDER CC BY-NC-SA 4.0
Comment