[实践总结] 使用Apache HttpClient 4.x进行进行一次Http请求

使用Apache HttpClient 4.x进行进行一次Http请求

第1步:导入依赖

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.7</version>
</dependency>

第2步:构造请求并获取返回Json串

package com.zhangziwa.practisesvr.utils.http;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.ParseException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
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.util.EntityUtils;

import java.io.IOException;

@Slf4j
public class HttpPostUtils {
    /**
     * 该方法用于发送一个HTTP POST请求,并附带JSON格式的请求数据。它包含了重试机制来应对可能的网络问题。
     *
     * @param uri  请求地址,不能为空字符串
     * @param json 请求体,必须是格式正确的JSON字符串,同样不能为空字符串
     * @return 如果HTTP响应状态码为200 OK,则返回服务器返回的数据(已解码为UTF-8格式的字符串);否则抛出异常
     * @throws IOException    当无法成功打开或关闭HTTP连接时抛出此异常
     * @throws ParseException 如果服务器返回的数据无法被正确解析时抛出此异常
     */
    public String executeWithRetryHttpPostJson(String uri, String json) throws IOException, ParseException {
        if (StringUtils.isAnyBlank(uri, json)) {
            throw new IllegalArgumentException("URI or JSON data is blank.");
        }

        // 第1步:构建HttpClient客户端,设置重试处理器
        MyCustomRetryHandler myCustomRetryHandler = new MyCustomRetryHandler(3);
        CloseableHttpClient client = HttpClients.custom().setRetryHandler(myCustomRetryHandler).build();

        // 第2步:构建请求
        // 构建请求: 设置请求URI
        HttpPost httpPost = new HttpPost(uri);

        // 构建请求: 设置请求体
        StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
        httpPost.setEntity(entity);

        // 构建请求: 设置超时时间
        RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(5000) // 超5秒还没返回新的可用链接,抛ConnectionPoolTimeoutException。默认值为0,表示无限等待。
                .setConnectTimeout(5000) // 超5秒还没建立链接,抛ConnectTimeoutException。默认值为0,表示无限等待。
                .setSocketTimeout(5000) // 超5秒还没返回数据,抛SocketTimeoutException。默认值为0,表示无限等待。
                .build();
        httpPost.setConfig(config);

        // 第3步: 客户端HttpClient 执行 请求HttpPost
        try (CloseableHttpResponse response = client.execute(httpPost, HttpClientContext.create())) {
            // 第4步: 解析返回的结果
            // 检查响应状态
            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                // 处理非200状态码的情况
                throw new ParseException("Unexpected response status: " + response.getStatusLine().getStatusCode());
            }

            HttpEntity responseEntity = response.getEntity();
            return EntityUtils.toString(responseEntity, Consts.UTF_8.name());
        } catch (IOException | ParseException e) {
            log.error("Error occurred during HTTP request", e);
            throw e; // 重新抛出异常,提供更多上下文信息
        } finally {
            // 第5步: 关闭资源,处理异常
            client.close();
        }
    }

}

第3步:自定义失败重试机制

import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionPoolTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Arrays;

public class MyCustomRetryHandler implements HttpRequestRetryHandler {
    private final int maxRetries;

    public MyCustomRetryHandler(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    @Override
    public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
        if (executionCount >= maxRetries) {
            // 如果已超过最大重试次数,则不再重试
            return false;
        }

        // UnknownHostException extends IOException
        if (exception instanceof UnknownHostException) {
            // DNS解析错误(UnknownHostException)
            // 实际应用中,DNS解析错误一般不应该重试,因为短时间内再次尝试解析同一个无效域名很可能会得到相同的结果。
            return false;
        }

        // SSLHandshakeException extends SSLException
        if (exception instanceof SSLHandshakeException) {
            // SSL握手失败(SSLHandshakeException):
            // 如果发生SSL/TLS握手错误,通常不建议自动重试,
            // 因为这可能是由于证书验证失败、协议版本不匹配或其他安全相关问题导致的。
            return false;
        }
        // SSLException extends IOException
        if (exception instanceof SSLException) {
            // SSL异常(SSLException):
            // SSL异常一般不会是临时问题,因为SSL握手失败一般不会恢复,因此不建议重试。
            return false;
        }

        // NoHttpResponseException extends IOException
        if (exception instanceof NoHttpResponseException) {
            // 服务器过载或临时不可用
            // 这些情况下,可以根据实际情况决定是否进行重试。
            return true;
        }

        // ConnectionPoolTimeoutException extends ConnectTimeoutException
        if (exception instanceof ConnectionPoolTimeoutException) {
            // 连接池获取连接超时,可重试
            return true;
        }
        // ConnectTimeoutException extends InterruptedIOException
        if (exception instanceof ConnectTimeoutException) {
            // 服务器过载或临时不可用
            // 这些情况下,可以根据实际情况决定是否进行重试。
            return true;
        }
        // SocketTimeoutException extends InterruptedIOException
        if (exception instanceof SocketTimeoutException) {
            // 对于超时异常,也可以选择重试
            return true;
        }
        // InterruptedIOException extends IOException
        if (exception instanceof InterruptedIOException) {
            // 对于中断的IO异常(如SocketTimeoutException),不进行重试
            return false;
        }


        // HttpHostConnectException extends ConnectException
        if (exception instanceof HttpHostConnectException) {
            // 这类异常可能包括网络中断、连接关闭等,根据具体情况进行重试。
            return true;
        }
        // ConnectException extends SocketException
        if (exception instanceof ConnectException) {
            // Connection refused
            return false;
        }
        // SocketException extends IOException
        if (exception instanceof SocketException) {
            // 这类异常可能包括网络中断、连接关闭等,根据具体情况进行重试。
            return true;
        }

        // 检查请求是否幂等
        HttpCoreContext coreContext = HttpCoreContext.adapt(context);
        HttpRequest request = coreContext.getRequest();
        if (request instanceof HttpRequestWrapper wrapper) {
            HttpUriRequest originalRequest = (HttpUriRequest) wrapper.getOriginal();
            String method = originalRequest.getMethod();
            if ("GET".equals(method) || "HEAD".equals(method) || "PUT".equals(method) || "DELETE".equals(method)) {
                // 幂等请求,可根据实际情况决定是否重试
                return true;
            }
        }
        if (!(request instanceof HttpEntityEnclosingRequest)) {
            // 幂等请求,可根据实际情况决定是否重试
            return true;
        }

        int[] RETRYABLE_STATUS_CODES = {500, 503, 504};
        // 如果是HTTP响应错误并且在可重试的状态码列表中,则重试
        Object attribute = context.getAttribute(HttpClientContext.HTTP_RESPONSE);
        if (attribute instanceof CloseableHttpResponse response) {
            StatusLine statusLine = response.getStatusLine();
            return Arrays.stream(RETRYABLE_STATUS_CODES).anyMatch(status -> status == statusLine.getStatusCode());
        }

        return false;
    }
}

第4步:解析返回的Json串

方法1:个人已知最好方式

    private static List<User> getResJsonToUsers2(String resJson) {
        // 使用fastjson解析JSON字符串
        JSONObject jsonObject = JSON.parseObject(resJson, Feature.OrderedField);

        // 直接检查JSON对象是否为空,避免调用isEmpty()方法
        if (jsonObject == null || jsonObject.isEmpty()) {
            return new ArrayList<>();
        }

        // 热死JSON结构如下:{"num":["123","234","345"],"name":["name1","name2","name3"],"age":["11","23","23"]}
        // 获取JSONObject中数组字段的数量,这里假设所有数组长度一致
        List<String> fields = new ArrayList<>(jsonObject.keySet());
        int size = jsonObject.getJSONArray(fields.get(0)).size();

        List<User> res = new ArrayList<>(size);
        try {
            // 初始化PropertyDescriptor一次,避免多次调用Introspector.getBeanInfo()
            PropertyDescriptor[] properties = getPropertyDescriptors(User.class);

            for (int i = 0; i < size; i++) {
                User tempUser = new User();
                for (PropertyDescriptor property : properties) {
                    String propertyName = property.getName();
                    if ("serialVersionUID".equals(propertyName)) {
                        continue;
                    }
                    if (!jsonObject.containsKey(propertyName)) {
                        continue;
                    }
                    Method writeMethod = property.getWriteMethod();
                    Object value = jsonObject.getJSONArray(propertyName).get(i);
                    writeMethod.invoke(tempUser, value);
                }
                res.add(tempUser);
            }
        } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {
            throw new RuntimeException("Failed to parse JSON to User objects", e);
        }
        return res;
    }

    // 提取类的所有属性描述符并缓存,减少反射开销
    private static Map<Class<?>, PropertyDescriptor[]> cachedDescriptors = new HashMap<>();

    private static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws IntrospectionException {
        synchronized (cachedDescriptors) {
            if (!cachedDescriptors.containsKey(clazz)) {
                cachedDescriptors.put(clazz, Introspector.getBeanInfo(clazz, Object.class).getPropertyDescriptors());
            }
            return cachedDescriptors.get(clazz);
        }
    }

方法2:做了解,每次都反射,性能不行

    private static List<User> getResJsonToUsers(String resJson) {
        // 使用fastjson解析JSON字符串
        JSONObject jsonObject = JSON.parseObject(resJson, Feature.OrderedField);

        // 直接检查JSON对象是否为空,避免调用isEmpty()方法
        if (jsonObject == null || jsonObject.isEmpty()) {
            return new ArrayList<>();
        }

        // 热死JSON结构如下:{"num":["123","234","345"],"name":["name1","name2","name3"],"age":["11","23","23"]}
        // 获取JSONObject中数组字段的数量,这里假设所有数组长度一致
        List<String> fields = new ArrayList<>(jsonObject.keySet());
        int size = jsonObject.getJSONArray(fields.get(0)).size();

        List<User> users = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            User tempUser = new User();
            for (String field : fields) {
                Object value = jsonObject.getJSONArray(field).get(i);
                ReflectUtils.setFieldValue(tempUser, field, value);
            }
            users.add(tempUser);
        }
        return users;
    }

方法3:仅了解,fieldObj.setAccessible(true)一般禁用

    private static List<User> getResJsonToUsers3(String resJson) {
        // 使用fastjson解析JSON字符串
        JSONObject jsonObject = JSON.parseObject(resJson, Feature.OrderedField);

        // 直接检查JSON对象是否为空,避免调用isEmpty()方法
        if (jsonObject == null || jsonObject.isEmpty()) {
            return new ArrayList<>();
        }

        // 热死JSON结构如下:{"num":["123","234","345"],"name":["name1","name2","name3"],"age":["11","23","23"]}
        // 获取JSONObject中数组字段的数量,这里假设所有数组长度一致
        List<String> fields = new ArrayList<>(jsonObject.keySet());
        int size = jsonObject.getJSONArray(fields.get(0)).size();

        List<User> users = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            User tempUser = new User();
            for (String field : fields) {
                try {
                    Object value = jsonObject.getJSONArray(field).get(i);
                    // 使用反射设置字段值,如果存在更好的方法,可以替换掉这一部分,此实现不推荐使用,仅了解
                    Field fieldObj = tempUser.getClass().getDeclaredField(field);
                    fieldObj.setAccessible(true);
                    fieldObj.set(tempUser, value);
                } catch (IllegalAccessException | NoSuchFieldException e) {
                    throw new RuntimeException("Failed to parse JSON to User objects", e);
                }
            }
            users.add(tempUser);
        }
        return users;
    }

模拟测试

    public static void main(String[] args) {
        // 模拟返回的Json串
        Map<String, List<String>> map = new HashMap<>();
        map.put("name", Arrays.asList("name1", "name2", "name3"));
        map.put("age", Arrays.asList("11", "23", "23"));
        map.put("num", Arrays.asList("123", "234", "345"));
        String resJson = JsonUtils.toJson(map);
        System.out.println(resJson);
        // {"num":["123","234","345"],"name":["name1","name2","name3"],"age":["11","23","23"]}

        List<User> users = getResJsonToUsers(resJson);
        List<User> users2 = getResJsonToUsers2(resJson);
        List<User> users3 = getResJsonToUsers3(resJson);

        System.out.println(users);
        // [User(name=name1, age=11, num=123), User(name=name2, age=23, num=234), User(name=name3, age=23, num=345)]
        System.out.println(users2);
        // [User(name=name1, age=11, num=123), User(name=name2, age=23, num=234), User(name=name3, age=23, num=345)]
        System.out.println(users3);
        // [User(name=name1, age=11, num=123), User(name=name2, age=23, num=234), User(name=name3, age=23, num=345)]
    }

参考

使用Apache HttpClient 4.x进行异常重试

Need 学习消化

二十六、CloseableHttpClient的使用和优化

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值