基于 HttpClient 封装的 HTTP 请求工具类

基于 HttpClient 封装的 HTTP 请求工具类

一、人类写的

java 复制代码
package com.sky.utils;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Http工具类
 */
public class HttpClientUtil {

    static final  int TIMEOUT_MSEC = 5 * 1000;

    /**
     * 发送GET方式请求
     * @param url
     * @param paramMap
     * @return
     */
    public static String doGet(String url,Map<String,String> paramMap){
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        String result = "";
        CloseableHttpResponse response = null;

        try{
            URIBuilder builder = new URIBuilder(url);
            if(paramMap != null){
                for (String key : paramMap.keySet()) {
                    builder.addParameter(key,paramMap.get(key));
                }
            }
            URI uri = builder.build();

            //创建GET请求
            HttpGet httpGet = new HttpGet(uri);

            //发送请求
            response = httpClient.execute(httpGet);

            //判断响应状态
            if(response.getStatusLine().getStatusCode() == 200){
                result = EntityUtils.toString(response.getEntity(),"UTF-8");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                response.close();
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return result;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            // 创建参数列表
            if (paramMap != null) {
                List<NameValuePair> paramList = new ArrayList();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                // 模拟表单
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            if (paramMap != null) {
                //构造json格式数据
                JSONObject jsonObject = new JSONObject();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    jsonObject.put(param.getKey(),param.getValue());
                }
                StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
                //设置请求编码
                entity.setContentEncoding("utf-8");
                //设置数据类型
                entity.setContentType("application/json");
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }

}

二、AI 建议

这个工具类可以用 ,特别是对于学习阶段简单的脚本任务 或者并发量极低的内部工具来说,它是完全合格的。

但是,如果你打算将它应用在生产环境 (特别是高并发、微服务架构)中,这个版本存在几个严重的性能隐患

为了让你从"能用"进阶到"好用",我为你详细分析一下这段代码的优缺点,以及如何改进。

✅ 优点(做得好的地方)

  1. 功能完整:涵盖了 GET、POST(表单)、POST(JSON)三种最常用的场景。
  2. JSON 处理doPost4Json 方法利用 FastJson 手动构建 JSON 字符串,解决了原生 HttpClient 处理 JSON 不够直观的问题。
  3. 超时配置builderRequestConfig 方法设置了连接超时和读取超时,这是一个非常好的习惯,防止程序因为网络卡顿而无限挂起。
  4. 编码处理 :显式指定了 UTF-8,避免了中文乱码问题。

⚠️ 核心隐患(生产环境不可用原因)

这个工具类最大的问题在于 CloseableHttpClient 的创建方式

1. 每次请求都创建新客户端(性能杀手)

在你的代码中:

java 复制代码
// 每次调用 doGet 或 doPost 时,都会执行这一行
CloseableHttpClient httpClient = HttpClients.createDefault();
  • 后果HttpClients.createDefault() 会创建一个新的连接管理器。虽然你最后调用了 httpClient.close(),但频繁地创建和销毁 TCP 连接(包括 DNS 解析、三次握手、SSL 握手等)是非常消耗资源的。
  • 生产环境表现 :在高并发下,服务器会出现大量的 TIME_WAIT 状态连接,导致端口耗尽,响应速度极慢,甚至抛出 ConnectionPoolTimeoutException
2. 缺乏连接池(Connection Pool)
  • 后果:没有复用连接。HTTP 协议的一个巨大优势是"长连接"(Keep-Alive),即建立一次连接可以发送多次请求。你的代码每次请求完就彻底断开,浪费了这一特性。
3. 异常处理略显粗糙
  • 代码catch (Exception e) { e.printStackTrace(); }
  • 问题 :在生产环境中,e.printStackTrace() 只是打印到控制台,日志系统(如 Logback/Log4j)通常抓取不到这些错误。应该使用日志框架(如 log.error(...))来记录。

🚀 改进建议(如何升级为生产级)

要让这个工具类变得"专业",只需要做一个核心改动HttpClient 变为静态单例,并配置连接池。

以下是基于你原有代码的优化版本(保留了你的逻辑风格,只改了架构):

java 复制代码
package com.sky.utils;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; // 引入连接池
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class HttpClientUtil {

    // 1. 使用 Logger 替代 printStackTrace
    private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);

    // 2. 定义全局唯一的 HttpClient 实例 (单例)
    private static final CloseableHttpClient httpClient;

    // 3. 静态代码块初始化,配置连接池
    static {
        // 创建连接池管理器
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(50); // 最大连接数
        cm.setDefaultMaxPerRoute(10); // 每个路由的最大连接数

        // 构建 HttpClient,注入连接池
        httpClient = HttpClients.custom()
                .setConnectionManager(cm)
                .build();
    }

    static final int TIMEOUT_MSEC = 5 * 1000;

    /**
     * 发送GET方式请求
     */
    public static String doGet(String url, Map<String, String> paramMap) {
        CloseableHttpResponse response = null;
        String result = "";

        try {
            URIBuilder builder = new URIBuilder(url);
            if (paramMap != null) {
                for (String key : paramMap.keySet()) {
                    builder.addParameter(key, paramMap.get(key));
                }
            }
            URI uri = builder.build();
            HttpGet httpGet = new HttpGet(uri);
            httpGet.setConfig(builderRequestConfig()); // 记得把超时配置也加上

            // 4. 使用全局的 httpClient 执行请求,而不是新建的
            response = httpClient.execute(httpGet);

            if (response.getStatusLine().getStatusCode() == 200) {
                result = EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        } catch (Exception e) {
            logger.error("GET请求异常: {}", url, e); // 记录日志
        } finally {
            // 5. 只关闭 response,不要关闭 httpClient,否则连接池就废了
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    logger.error("关闭响应流异常", e);
                }
            }
        }
        return result;
    }

    // doPost 和 doPost4Json 方法同理:
    // 1. 删掉 CloseableHttpClient httpClient = HttpClients.createDefault();
    // 2. 直接使用类变量 httpClient.execute(httpPost);
    // 3. finally 块中只关闭 response
    
    // ... 其他方法保持不变,只需替换 httpClient 的获取方式 ...
    
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }
}

📌 总结

  • 学习用 :你现在的代码完全没问题,逻辑清晰,有助于理解 HTTP 请求的底层流程(构建 URL -> 构建请求 -> 执行 -> 处理响应 -> 关闭资源)。
  • 工作用 :请务必采用连接池 模式(即上面的改进版)。在面试或实际开发中,如果能说出"我知道 HttpClients.createDefault() 每次创建新连接会有性能问题,所以我使用了 PoolingHttpClientConnectionManager",这会是一个非常加分的亮点。

三、AI 写的

在 Java 后端开发中,直接使用原生的 HttpURLConnection 往往代码繁琐且难以维护。基于 Apache HttpClient 封装一个通用的工具类(HttpClientUtil)是业界的最佳实践。

我为你整理了一个生产级 的封装方案。这个方案重点解决了连接池管理 (避免频繁创建销毁连接)、超时控制 以及JSON 数据处理等核心问题。

🚀 核心设计思路

在编写代码之前,我们需要明确这个工具类的设计原则,以确保其在高并发场景下的稳定性:

  1. 单例模式与连接池HttpClient 实例应当是单例的,并配置 PoolingHttpClientConnectionManager。这能复用 TCP 连接,大幅提升性能。
  2. 自动资源释放 :利用 try-with-resources 语法糖,确保 CloseableHttpResponse 和流被正确关闭,防止内存泄漏。
  3. 灵活的数据支持 :同时支持 Map 参数(表单提交)和 Object/JSON(RESTful 接口)。
  4. 完善的异常处理:统一处理超时、状态码错误,并记录日志。

🛠️ 完整工具类代码实现

你可以直接参考以下代码结构。该类封装了 GET、POST、PUT、DELETE 方法,并内置了连接池配置。

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;

/**
 * 基于 Apache HttpClient 的 HTTP 请求工具类
 * 特点:支持连接池、自动序列化 JSON、超时控制
 */
public class HttpClientUtil {
    private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);
    
    // 全局单例 HttpClient(带连接池)
    private static final CloseableHttpClient httpClient;
    private static final ObjectMapper objectMapper = new ObjectMapper();

    static {
        // 1. 配置连接池
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(200); // 最大连接数
        cm.setDefaultMaxPerRoute(20); // 每个路由的最大连接数

        // 2. 配置请求参数(超时时间)
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000)      // 连接建立超时:5秒
                .setSocketTimeout(10000)      // 数据传输超时:10秒
                .setConnectionRequestTimeout(5000) // 从连接池获取连接超时:5秒
                .build();

        // 3. 创建 HttpClient 实例
        httpClient = HttpClients.custom()
                .setConnectionManager(cm)
                .setDefaultRequestConfig(requestConfig)
                .evictExpiredConnections() // 自动关闭过期连接
                .evictIdleConnections(60, java.util.concurrent.TimeUnit.SECONDS) // 自动关闭空闲连接
                .build();
    }

    // 私有构造,禁止实例化
    private HttpClientUtil() {}

    // ==================== GET 请求 ====================
    public static String doGet(String url) throws IOException {
        return doGet(url, null, null);
    }

    public static String doGet(String url, Map<String, String> params) throws IOException {
        return doGet(url, params, null);
    }

    public static String doGet(String url, Map<String, String> params, Map<String, String> headers) throws IOException {
        URIBuilder uriBuilder = new URIBuilder(url);
        if (params != null) {
            params.forEach(uriBuilder::setParameter);
        }
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        addHeaders(httpGet, headers);
        return executeRequest(httpGet);
    }

    // ==================== POST 请求 (JSON) ====================
    public static String doPostJson(String url, Object body) throws IOException {
        return doPostJson(url, body, null);
    }

    public static String doPostJson(String url, Object body, Map<String, String> headers) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        // 自动将对象序列化为 JSON 字符串
        String jsonStr = objectMapper.writeValueAsString(body);
        StringEntity entity = new StringEntity(jsonStr, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8));
        httpPost.setEntity(entity);
        
        // 默认添加 Content-Type,除非用户自定义覆盖
        if (headers != null && !headers.containsKey("Content-Type")) {
             httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
        }
        addHeaders(httpPost, headers);
        return executeRequest(httpPost);
    }

    // ==================== POST 请求 (表单) ====================
    public static String doPostForm(String url, Map<String, String> params) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        if (params != null) {
            List<NameValuePair> nvps = new ArrayList<>();
            params.forEach((k, v) -> nvps.add(new BasicNameValuePair(k, v)));
            httpPost.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
        }
        return executeRequest(httpPost);
    }

    // ==================== PUT / DELETE 请求 ====================
    public static String doPutJson(String url, Object body) throws IOException {
        HttpPut httpPut = new HttpPut(url);
        String jsonStr = objectMapper.writeValueAsString(body);
        httpPut.setEntity(new StringEntity(jsonStr, ContentType.APPLICATION_JSON));
        return executeRequest(httpPut);
    }

    public static String doDelete(String url) throws IOException {
        return executeRequest(new HttpDelete(url));
    }

    // ==================== 核心执行逻辑 ====================
    private static String executeRequest(HttpRequestBase request) throws IOException {
        try (CloseableHttpResponse response = httpClient.execute(request)) {
            int statusCode = response.getStatusLine().getStatusCode();
            HttpEntity entity = response.getEntity();
            String result = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : null;

            if (statusCode >= 200 && statusCode < 300) {
                logger.debug("请求成功: {}, 响应: {}", request.getURI(), result);
                return result;
            } else {
                logger.error("请求失败: {}, 状态码: {}, 响应: {}", request.getURI(), statusCode, result);
                throw new IOException("HTTP Error: " + statusCode);
            }
        }
    }

    private static void addHeaders(HttpRequestBase request, Map<String, String> headers) {
        if (headers != null) {
            headers.forEach(request::setHeader);
        }
    }
}

📊 关键功能点解析

为了让你更清楚如何使用和维护这个工具类,我总结了以下几个关键点:

1. 连接池配置 (Performance)

在静态代码块中,我们初始化了 PoolingHttpClientConnectionManager

  • 为什么要这样做? 如果不使用连接池,每次请求都会重新建立 TCP 三次握手,对于高并发系统来说,这是巨大的性能浪费。
  • 配置建议: setMaxTotal 通常设置为 CPU 核数 * 2 + 磁盘数,或者根据压测结果调整(示例中设为 200)。
2. 超时设置 (Stability)

代码中设置了三种超时时间:

  • Connect Timeout (5s):建立连接的超时。如果服务器宕机或网络不通,快速失败。
  • Socket Timeout (10s):数据传输超时。防止服务器处理时间过长导致线程阻塞。
  • Connection Request Timeout (5s):从连接池获取连接的超时。防止连接池耗尽时线程无限等待。
3. JSON 自动序列化 (Usability)

利用 Jackson 的 ObjectMapper,我们在 doPostJson 方法中直接接收 Object 类型参数。

  • 优势 :调用方不需要手动 JSON.toJSONString(obj),直接传实体类或 Map 即可,代码更清爽。

📝 使用示例

封装完成后,调用变得非常简单:

场景 1:发送 JSON 数据 (最常见)
java 复制代码
// 定义请求体对象
Map<String, Object> params = new HashMap<>();
params.put("username", "admin");
params.put("password", "123456");

try {
    // 一行代码完成 POST 请求
    String response = HttpClientUtil.doPostJson("https://api.example.com/login", params);
    System.out.println("登录结果: " + response);
} catch (IOException e) {
    e.printStackTrace();
}
场景 2:带请求头的 GET 请求
java 复制代码
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer token123456");

try {
    String userInfo = HttpClientUtil.doGet("https://api.example.com/user/info", null, headers);
    System.out.println(userInfo);
} catch (IOException e) {
    // 处理异常
}

⚠️ 注意事项

  1. 依赖引入 :确保你的 pom.xml 中包含了 httpclientjackson 依赖。
  2. 线程安全 :上述工具类中的 httpClient 是线程安全的,可以在多线程环境下共享。
  3. 字符集 :代码中显式指定了 StandardCharsets.UTF_8,避免出现中文乱码问题。

这个封装方案既保留了 Apache HttpClient 的强大功能,又屏蔽了底层的复杂性,非常适合在企业级项目中使用。

相关推荐
Predestination王瀞潞1 小时前
8.4.1 安全->SM商密(GM T 0002-2012 0003-2012):SM商密
网络·安全
小年糕是糕手2 小时前
【35天从0开始备战蓝桥杯 -- Day6】
开发语言·前端·网络·数据库·c++·蓝桥杯
佳木逢钺2 小时前
[特殊字符] 全栈机器人工程师的“三十维接入”主控台
网络·机器人·html
yy_xzz2 小时前
【Linux开发】 01 Linux TCP 网络编程——普通服务器
linux·网络·tcp/ip
阿kun要赚马内2 小时前
计算机网络:TCP三次握手
网络·tcp/ip·计算机网络
wanhengidc2 小时前
云手机与云真机分别是指什么
服务器·网络·安全·智能手机
Trouvaille ~2 小时前
【项目篇】从零手写高并发服务器(九):HTTP协议支持——从TCP到应用层
linux·服务器·c++·tcp/ip·http·高并发·应用层
.select.3 小时前
HTTPS RSA 握手解析
服务器·网络协议·https