Java编程语言,编写一个基于java.net.HttpURLConnection封装http请求的工具类

一、背景

在spring boot编程中,有Okhttp、Apache Httpclient等http调用的工具类,也有RestTemplate和Hutool等这样的工具,还有Retrofit和openfeign这样的更上层次封装的工具。

而本文不借助于任何三方工具包,除了json转换工具外,仅使用java.net.HttpURLConnection来封装http api接口调用。

先介绍一种方式,拿它和xxl-job开源项目的实现作对比,希望可以让你对HttpURLConnection的使用有所帮助。(还是那句话,我们在平常写http调用,建议你去使用开头提及的成熟框架)

二、流程图

  • 信任https证书:ignoreCertificateErrors()
java 复制代码
private void ignoreCertificateErrors() {
        TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    @Override
                    public void checkClientTrusted(
                            java.security.cert.X509Certificate[] certs, String authType) {
                    }

                    @Override
                    public void checkServerTrusted(
                            java.security.cert.X509Certificate[] certs, String authType) {
                    }
                }
        };
        // Added per https://github.com/timols/java-gitlab-api/issues/44
        HostnameVerifier nullVerifier = (hostname, session) -> true;

        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
            // Added per https://github.com/timols/java-gitlab-api/issues/44
            HttpsURLConnection.setDefaultHostnameVerifier(nullVerifier);
        } catch (Exception ignore) {}
    }
  • 设置连接的属性,主要包括读写和连接超时时间、请求方法、http header(这里是User-Agent和Accept-Encoding)
java 复制代码
private HttpURLConnection setupConnection(URL url) throws IOException {
        if (root.isIgnoreCertificateErrors()) {
            ignoreCertificateErrors();
        }
        connection.setReadTimeout(root.getResponseReadTimeout());
        connection.setConnectTimeout(root.getConnectionTimeout());

        connection.setRequestMethod(method.name());
        
        connection.setRequestProperty("User-Agent", root.getUserAgent());
        connection.setRequestProperty("Accept-Encoding", "gzip");
        return connection;
    }
  • 核心代码,先是建立连接,然后设置连接的属性,再拼接请求报文,最后是解析响应报文。
java 复制代码
public <T> T to(String tailAPIUrl, Class<T> type, T instance) throws IOException {
        HttpURLConnection connection = null;
        try {
            connection = setupConnection(root.getAPIUrl(tailAPIUrl));
            // 是否有附件
            if (hasAttachments()) {
                submitAttachments(connection);
            } else if (hasOutput()) {
                 submitData(connection);
            } else if (PUT.equals(method)) {
                // PUT requires Content-Length: 0 even when there is no body (eg: API for protecting a branch)
                connection.setDoOutput(true);
                // 关闭上传缓存
                connection.setFixedLengthStreamingMode(0);
            }

            try {
                return parse(connection, type, instance);
            } catch (IOException e) {
                handleAPIError(e, connection);
            }

            return null;
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
  • hasOutput(),post请求方式,或者put请求方式且请求体不为空
java 复制代码
    private boolean hasOutput() {
        return method.equals(POST) || method.equals(PUT) && !data.isEmpty();
    }
  • submitData()
java 复制代码
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

   private Map<String, Object> data = new HashMap<>();
   
   // HttpRequestor是工具类的类名
   // HttpRequestor.with("userId", 10002).with("userName", "zhangsan");
   public HttpRequestor with(String key, Object value) {
        if (value != null && key != null) {
            data.put(key, value);
        }
        return this;
    }

    public static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

	private void submitData(HttpURLConnection connection) throws IOException {
        connection.setDoOutput(true);
        connection.setRequestProperty("Content-Type", "application/json");
		//  data是请求的入参,把它转换为json格式,传入请求体里
        MAPPER.writeValue(connection.getOutputStream(), data);
    }
  • parse(),如果是字节数组,则直接返回,否则需要进一步判断数据类型。

当是String类型或对象类型,需要进行数据类型的转换。

java 复制代码
private <T> T parse(HttpURLConnection connection, Class<T> type, T instance) throws IOException {
        InputStreamReader reader = null;
        try {
            // 如果是字节数组,直接返回
            if (byte[].class == type) {
                return type.cast(IOUtils.toByteArray(wrapStream(connection, connection.getInputStream())));
            }
    		// 读取响应报文内容,编码字符集是UTF-8
            reader = new InputStreamReader(wrapStream(connection, connection.getInputStream()), "UTF-8");
            
            String json = IOUtils.toString(reader);
            if (type != null && type == String.class) {
                return type.cast(json);
            }

			// MAPPER见上个方法
            if (type != null && type != Void.class) {
            // 把json字符串赋值给新建对象type
                return MAPPER.readValue(json, type);
            } else if (instance != null) {
            // 把json字符串赋值给对象type,仅更新,而非新建
                return MAPPER.readerForUpdating(instance).readValue(json);
            } else {
                return null;
            }
        } catch (SSLHandshakeException e) {
            throw new SSLException("You can disable certificate checking by setting ignoreCertificateErrors " +
                    "on GitlabHTTPRequestor.", e);
        } finally {
        // 关闭输入流
            IOUtils.closeQuietly(reader);
        }
    }
  • wrapStream(),需要对输入流进行判断,因为可能对数据进行了压缩。
java 复制代码
private InputStream wrapStream(HttpURLConnection connection, InputStream inputStream) throws IOException {
        String encoding = connection.getContentEncoding();

        if (encoding == null || inputStream == null) {
            return inputStream;
        } else if (encoding.equals("gzip")) {
            return new GZIPInputStream(inputStream);
        } else {
            throw new UnsupportedOperationException("Unexpected Content-Encoding: " + encoding);
        }
    }

三、xxl-job中的Http调用

顺便说一下,xxl-job中的http调用使用的就是HttpURLConnection,没有去依赖第三方的http请求框架。(详见其源码com.xxl.job.core.util.XxlJobRemotingUtil

java 复制代码
	public static ReturnT postBody(String url, String accessToken, int timeout, Object requestObj, Class returnTargClassOfT) {
        HttpURLConnection connection = null;
        BufferedReader bufferedReader = null;
        try {
            // connection
            URL realUrl = new URL(url);
            connection = (HttpURLConnection) realUrl.openConnection();

            // trust-https
            boolean useHttps = url.startsWith("https");
            if (useHttps) {
                HttpsURLConnection https = (HttpsURLConnection) connection;
                trustAllHosts(https);
            }

            // connection setting
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setReadTimeout(timeout * 1000);
            connection.setConnectTimeout(3 * 1000);
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
            connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");

            if(accessToken!=null && accessToken.trim().length()>0){
                connection.setRequestProperty(XXL_JOB_ACCESS_TOKEN, accessToken);
            }

            // do connection
            connection.connect();

            // write requestBody
            if (requestObj != null) {
                String requestBody = GsonTool.toJson(requestObj);

                DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
                dataOutputStream.write(requestBody.getBytes("UTF-8"));
                dataOutputStream.flush();
                dataOutputStream.close();
            }
            
            // valid StatusCode
            int statusCode = connection.getResponseCode();
            if (statusCode != 200) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "xxl-job remoting fail, StatusCode("+ statusCode +") invalid. for url : " + url);
            }

            // result
            bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
            StringBuilder result = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                result.append(line);
            }
            String resultJson = result.toString();

            // parse returnT
            try {
                ReturnT returnT = GsonTool.fromJson(resultJson, ReturnT.class, returnTargClassOfT);
                return returnT;
            } catch (Exception e) {
                logger.error("xxl-job remoting (url="+url+") response content invalid("+ resultJson +").", e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, "xxl-job remoting (url="+url+") response content invalid("+ resultJson +").");
            }

        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new ReturnT<String>(ReturnT.FAIL_CODE, "xxl-job remoting error("+ e.getMessage() +"), for url : " + url);
        } finally {
            try {
                if (bufferedReader != null) {
                    bufferedReader.close();
                }
                if (connection != null) {
                    connection.disconnect();
                }
            } catch (Exception e2) {
                logger.error(e2.getMessage(), e2);
            }
        }
    }
  • trustAllHosts()
java 复制代码
private static void trustAllHosts(HttpsURLConnection connection) {
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            SSLSocketFactory newFactory = sc.getSocketFactory();

            connection.setSSLSocketFactory(newFactory);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        connection.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
    }
    private static final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
        @Override
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return new java.security.cert.X509Certificate[]{};
        }
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
    }};
相关推荐
涡能增压发动积15 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
云烟成雨TD15 小时前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Wenweno0o15 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
行乾15 小时前
鸿蒙端 IMSDK 架构探索
架构·harmonyos
于慨15 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz15 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg32132115 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
tyung15 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald15 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
@yanyu66615 小时前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot