HttpClient工具类优化实践:提升性能与可维护性

概述

在Java应用开发中,HTTP客户端是常见的组件之一。本文将分享一个HTTP工具类的优化过程,通过代码重构提升性能、可维护性和稳定性。我们将分析问题,并逐步介绍优化方案。

原代码

贴上代码,看看问题🧐

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

public class HttpUtil {
    private static final Logger log = LoggerFactory.getLogger(HttpUtil.class);

    public static String get(HttpArgs httpArgs) {
        for (int i = 1; i <= 3; i++) {
            try {
                // 不报错就直接返回,报错就重试两次
                return sendGet(httpArgs);
            } catch (Exception o_O) {
                log.info("第{}次请求失败", i, o_O);
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (Exception ignored) {
                }
            }
        }
        return null;
    }

    private static String sendGet(HttpArgs httpArgs) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_2)       // 指定 HTTP 协议版本 (HTTP_1_1 或 HTTP_2)
                .connectTimeout(Duration.ofSeconds(30))   // 设置连接超时时间
                .followRedirects(HttpClient.Redirect.NORMAL) // 设置重定向策略 (ALWAYS, NEVER, NORMAL)
                .build();
        HttpRequest.Builder builder = HttpRequest.newBuilder()
                .uri(URI.create(httpArgs.getUrl())) // 设置目标 URI
                .timeout(Duration.ofSeconds(30));
        Map<String, String> headerMap = httpArgs.getHeaderMap();
        for (String key : headerMap.keySet()) {
            String value = headerMap.get(key);
            builder.header(key, value);
        }
        HttpRequest request = builder.GET().build();
        log.info("请求接口,{},{}", httpArgs, request);
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        String result = null;
        if (response.body() != null) {
            result = response.body();
        }
        log.info("请求结果 statusCode:{} uri:{} version:{} result:{}", response.statusCode(), response.uri(), response.version(), Optional.ofNullable(result).orElse("").substring(0, 300));
        return result;
    }

    // 测试
    public static void main(String[] args) {
        HttpArgs httpArgs = new HttpArgs();
        httpArgs.setUrl("https://m.cls.cn/nodeapi/telegraphs");
        String s = HttpUtil.get(httpArgs);
        System.out.println(s);
    }
}

代码分析

  1. HttpClient重复创建:每次请求都创建新的HttpClient实例,造成资源浪费
  2. 参数处理不足:没有对查询参数进行编码处理
  3. 异常处理不完善:线程中断处理不当
  4. 性能优化不足:缺乏连接池和线程池配置

优化方案

HttpClient单例化

java 复制代码
// 单例可重用的HttpClient
private volatile static HttpClient client;

private static HttpClient getClient() {
    if (client == null) {
        synchronized (HttpUtil.class) {
            if (client == null) {
                client = HttpClient.newBuilder()
                        .version(HttpClient.Version.HTTP_2)
                        .connectTimeout(Duration.ofSeconds(30))
                        .followRedirects(HttpClient.Redirect.NORMAL)
                        .executor(Executors.newFixedThreadPool(5)) // 使用有界线程池
                        .build();
            }
        }
    }
    return client;
}

优化点

  • 使用双重检查锁定实现线程安全的单例模式
  • 添加线程池配置提高并发处理能力
  • 避免频繁创建HttpClient带来的性能开销

查询参数编码处理

java 复制代码
String query = httpArgs.getParamMap().entrySet().stream()
        .map(entry -> entry.getKey() + "=" + URLEncoder.encode(
            String.valueOf(Optional.ofNullable(entry.getValue()).orElse("")), 
            StandardCharsets.UTF_8))
        .collect(Collectors.joining("&"));

优化点

  • 使用URLEncoder确保参数正确编码
  • 使用流式API简化代码
  • 处理null值避免空指针异常

改进异常处理

java 复制代码
try {
    TimeUnit.SECONDS.sleep(10);
} catch (Exception ignored) {
    // 多线程环境中断恢复后,通过调用interrupt()恢复中断状态
    Thread.currentThread().interrupt();
}

优化点

  • 正确处理线程中断状态
  • 符合多线程开发规范😀

简化Header处理

java 复制代码
// 使用Lambda表达式简化代码
httpArgs.getHeaderMap().forEach(builder::header);

优化点

  • 使用方法引用替代传统循环
  • 代码更简洁易读

完整优化后代码

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 网络请求工具
 * 注意:该工具会把请求异常会当作空值返回
 */
public class HttpUtil {
    private static final Logger log = LoggerFactory.getLogger(HttpUtil.class);

    // 单例可重用的HttpClient
    private volatile static HttpClient client;

    private static HttpClient getClient() {
        if (client == null) {
            synchronized (HttpUtil.class) {
                if (client == null) {
                    client = HttpClient.newBuilder()
                            .version(HttpClient.Version.HTTP_2)
                            .connectTimeout(Duration.ofSeconds(30))
                            .followRedirects(HttpClient.Redirect.NORMAL)
                            .executor(Executors.newFixedThreadPool(5))
                            .build();
                }
            }
        }
        return client;
    }

    public static String get(HttpArgs httpArgs) {
        for (int i = 1; i <= 3; i++) {
            try {
                return sendGet(httpArgs);
            } catch (Exception o_O) {
                log.info("第{}次请求失败", i, o_O);
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (Exception ignored) {
                    Thread.currentThread().interrupt();
                }
            }
        }
        return null;
    }

    private static String sendGet(HttpArgs httpArgs) throws Exception {
        String query = httpArgs.getParamMap().entrySet().stream()
                .map(entry -> entry.getKey() + "=" + URLEncoder.encode(
                    String.valueOf(Optional.ofNullable(entry.getValue()).orElse("")), 
                    StandardCharsets.UTF_8))
                .collect(Collectors.joining("&"));
        
        HttpRequest.Builder builder = HttpRequest.newBuilder()
                .uri(URI.create(httpArgs.getUrl() + "?" + query))
                .timeout(Duration.ofSeconds(30));
        
        httpArgs.getHeaderMap().forEach(builder::header);
        
        HttpRequest request = builder.GET().build();
        log.info("请求接口,{},{}", httpArgs, request);
        
        HttpResponse<String> response = getClient().send(request, HttpResponse.BodyHandlers.ofString());
        String result = response.body();
        
        log.info("请求结果 statusCode:{} uri:{} version:{} result:{}", 
            response.statusCode(), response.uri(), response.version(), 
            StringUtil.emptySubString(result, 0, 300));
        
        return result;
    }

    // 测试方法
    public static void main(String[] args) {
        HttpArgs httpArgs = new HttpArgs();
        httpArgs.setUrl("https://m.cls.cn/nodeapi/telegraphs");
        String s = HttpUtil.get(httpArgs);
        System.out.println(s);
    }
}

兄弟们,觉得不错就点波关注😘

相关推荐
ursazoo12 小时前
记一次线上API调用失败的排查过程:从405到时间同步
后端
Java中文社群12 小时前
面试官:如何提升项目并发性能?
java·后端·面试
阿杆12 小时前
OAuth 图解指南(阮老师推荐)
前端·后端·架构
Mintopia13 小时前
每个国家的核安全是怎么保证的,都不怕在自己的领土爆炸吗?
前端·后端·面试
tonydf13 小时前
浅聊一下AOP
后端
悟空码字13 小时前
腾讯开源啦,源码地址+部署脚本
后端·腾讯
Xxtaoaooo13 小时前
Spring Boot 启动卡死:循环依赖与Bean初始化的深度分析
java·后端·依赖注入·三级缓存机制·spring boot循环依赖
TechNomad13 小时前
设计模式:中介者模式(Mediator Pattern)
设计模式·中介者模式
洛小豆13 小时前
Ubuntu 网络配置演进:从 20.04 到 24.04 的静态 IP 设置指南
linux·后端·ubuntu