1 序
- [Java SE/JDK/网络] 核心源码精讲:java.net.HttpURLConnection - 博客园/千千寰宇
- [身份认证/JWT] 身份认证方案与HTTP请求中Authorization Header - 博客园/千千寰宇 【推荐】
- [网络/HTTPS/Java] PKI公钥基础设施体系:数字证书(X.509)、CA机构 | 含:证书管理工具(jdk keytool / openssl) - 博客园/千千寰宇 【推荐】
- 类比: HttpClient / OkHttp 等 Java HTTP 网络请求工具
引入依赖
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.40</version>
</dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.6</version><scope>compile</scope>
</dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore</artifactId><version>4.5.6</version><scope>compile</scope>
</dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore-nio</artifactId><version>4.5.6</version><scope>compile</scope>
</dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpasyncclient</artifactId><version>4.5.6</version><scope>compile</scope>
</dependency>
HttpRequestUtils
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Map;public class HttpRequestUtils {private final static Logger logger = LoggerFactory.getLogger(HttpRequestUtils.class);private final static String APPLICATION_JSON = "application/json";private final static String APPLICATION_OCTET_STREAM = "application/octet-stream";/*** get unconnected HttpURLConnection* @reference-doc* [1] java Url请求带header - 51cto - https://blog.51cto.com/u_16213438/7165885* [2] HttpURLConnection 链接详解 - CSDN - https://blog.csdn.net/small_love/article/details/122410998* @param netUrl* @param requestProperties* @return HttpURLConnection* @throws IOException*/public static HttpURLConnection getHttpURLConnection(String netUrl, Map<String, String> requestProperties) throws IOException {return getHttpURLConnection(null, netUrl, requestProperties );}/*** get unconnected HttpURLConnection* @param sslContext* @param netUrl* @param requestProperties* @return HttpURLConnection* @throws IOException*/public static HttpURLConnection getHttpURLConnection(SSLContext sslContext, String netUrl, Map<String, String> requestProperties) throws IOException {checkUrlProtocol(netUrl);if(!ObjectUtils.isEmpty(sslContext)){setDefaultSSLSocketFactoryForHttpsURLConnection(sslContext);}URL url = new URL(netUrl);/** step1 打开连接 **/URLConnection connection = url.openConnection();/** step2 设置请求属性,如 请求头 **///connection.setRequestProperty("Content-Type", "application/json");//connection.setRequestProperty("Authorization", "Bearer your_token_here");if(!ObjectUtils.isEmpty(requestProperties)){requestProperties.entrySet().stream().forEach(requestPropertyEntry -> {connection.setRequestProperty(requestPropertyEntry.getKey(), requestPropertyEntry.getValue());});}/** step3 建立 TCP 连接 **///connection.connect();//建立TCP连接(但尚未发起HTTP请求) | 改由让调用方发起,并最终负责关闭连接HttpURLConnection httpURLConnection = (HttpURLConnection)connection;return httpURLConnection;}/*** get unconnected HttpURLConnection* @param netUrl* @return* @throws IOException*/public static HttpURLConnection getHttpURLConnection(String netUrl) throws IOException {return getHttpURLConnection(netUrl, new HashMap<>());}private static void checkUrlProtocol(String netUrl){if(netUrl == null){throw new RuntimeException("Fail to get HttpURLConnection cause by empty url!");}if(!netUrl.toLowerCase().startsWith("http")){//不支持非 http / https 协议的 url , 例如: "ftp://" , "file://" , "mail://" , ...throw new RuntimeException("Fail to get HttpURLConnection cause by not supported protocol(`http(s)`)!url:"+ netUrl);}}/*** 读取连接响应内容为 String* @description* 1. 一般该连接对象的响应类型(content-type)为 application/json* 2. 关闭连接 : 在调用方进行关闭,本方法不主动关闭流 【强制建议】* @param responseInputStream* @param connectionAlias* @throws IOException*/public static String readConnectionResponseAsString(InputStream responseInputStream, String connectionAlias) throws IOException {//step1 读取 HTTP 响应内容BufferedReader in = new BufferedReader(new InputStreamReader(responseInputStream));String inputLine;StringBuffer responseJsonStringBuffer = new StringBuffer();while ((inputLine = in.readLine()) != null) {responseJsonStringBuffer.append(inputLine);}//in.close();String responseJsonStr = responseJsonStringBuffer.toString();if(ObjectUtils.isEmpty(responseJsonStr)){logger.error(String.format("The connected http URL connection 's response content is empty!", connectionAlias));return "{}";}return responseJsonStr;}/*** 是否请求/响应成功* @param connectedHttpURLConnection 已连接的 HTTP-URL 连接 (有响应信息,也有请求信息)* @param connectionAlias 连接的别称,如: "xxxBackendXxxBusinessSceneApiRequest"* @note 注意事项* 1. 上层调用时,需捕获本方法抛出的异常,并及时关闭连接(`connectedHttpURLConnection.disconnect()`),以防止报如下错误:* java.io.IOException: stream is closed* at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.ensureOpen(HttpURLConnection.java:3429)* at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(HttpURLConnection.java:3454)* at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)* ...* @note* 因本方法调用了 getResponseCode() / getInputStream() ,故: 强制建议调用完本方法后,需主动调用 HttpURLConnection#disconect() 关闭连接* 因抛异常时不会主动关闭连接 ,故: 强制建议调用完本方法后,需主动捕获异常,并调用 HttpURLConnection#disconect() 关闭连接*/public static boolean isRequestSuccess(HttpURLConnection connectedHttpURLConnection,String connectionAlias) throws IOException {if(ObjectUtils.isEmpty(connectedHttpURLConnection)){throw new RuntimeException(String.format("The connected http URL connection (`%s`) is empty! request property: %s", connectionAlias, JSON.toJSONString( connectedHttpURLConnection.getRequestProperties() ) ));}/** 200 / ... **/int responseCode = connectedHttpURLConnection.getResponseCode();//getResponseCode : 实际上底层会调用 getInputStream() => 会彻底触发/发起 HTTP 请求if( !(responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) ){//200(成功) or 302(重定向)//connectedHttpURLConnection.disconnect();//关闭连接,不在此处操作,在外层捕获异常时操作throw new RuntimeException( String.format("The connected http URL connection (`%s`)'s response code is not success/200 (%d)!", connectionAlias, responseCode ) );}/** "application/json" / "application/octet-stream" / ... */String responseContentType = connectedHttpURLConnection.getContentType();if(ObjectUtils.isEmpty(responseContentType)){throw new RuntimeException(String.format("The connected http URL connection (`%s`)'s response's content type is empty!", connectionAlias ) );}switch (responseContentType){case APPLICATION_JSON:InputStream responseInputStream = connectedHttpURLConnection.getInputStream();String responseStr = readConnectionResponseAsString( responseInputStream, connectionAlias );responseInputStream.close();//关闭输入流/*** step1 将HTTP响应结果转为JSON格式* @sample "{"status":false,"data":null,"errorCode":"XXX_BACKEND_099","errorMsg":"获取外链失败","traceId":"52bbd4f1d8264a76bc161faee9d92509.120.16992379058863989"}"*/ObjectMapper objectMapper = new ObjectMapper();HashMap<String, Object> response = objectMapper.readValue( responseStr, HashMap.class);/*** step2 判断 status 是否为空或为真*/Boolean status = (Boolean) response.get("status");if(ObjectUtils.isEmpty(status) || status != true){throw new RuntimeException(String.format("The connected http URL connection (`%s`)'s response's content type is %s and it's status is empty or not true!The response content is : %s.", connectionAlias, APPLICATION_JSON, JSON.toJSONString(response)));}break;case APPLICATION_OCTET_STREAM://Do Nothing; //暂不检查此类型,默认均为成功break;default://Do Nothing;break;}return true;}/*** 获取 信任指定SSL证书的 SSLContext* @return*/public static SSLContext getAllowTargetSslSSLContext(String keyStoreFilePath, String keyStorePassword) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, KeyManagementException {KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());//keystore.type=jks[default] | configFile: $JAVA_HOME/jre/lib/security/java.securityFileInputStream keyStoreFileInputStream = new FileInputStream(keyStoreFilePath);keyStore.load(keyStoreFileInputStream, keyStorePassword.toCharArray());//FileInputStream keyStoreFileInputStream = new FileInputStream(keyStoreFilePath);//如 : "root.crt"//CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");//Certificate certificate = certificateFactory.generateCertificate(keyStoreFileInputStream);//keyStore.load(null, null);//keyStore.setCertificateEntry("ca", certificate);String trustManagerFactoryAlgorithm = TrustManagerFactory.getDefaultAlgorithm();TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerFactoryAlgorithm);trustManagerFactory.init(keyStore);SSLContext sslContext = SSLContext.getInstance("TLS");//sslContext.init(null, trustManagerFactory.getTrustManagers(), null);//或:sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());return sslContext;}/*** 获取 SSLSocketFactory* @note 外部直接使用本方法即可*/public static SSLSocketFactory getSSLSocketFactory(SSLContext sslContext){//LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(context);//或:SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();return sslSocketFactory;}/*** 为 HttpsURLConnection 设置默认的SSLSocketFactory* @useage* 信任所有SSL,为 HttpsURLConnection 设置信任所有SSL的策略* 在所有https开始进行请求之前,执行一次即可:* SSLContext sslContext = AllowAllSslHTTPSTrustManager.getAllowAllSslSSLContext();//信任所有证书* @note 外部直接使用本方法即可*/public static void setDefaultSSLSocketFactoryForHttpsURLConnection(SSLContext sslContext){//影响整个应用程序的 HTTPS 连接HttpsURLConnection.setDefaultSSLSocketFactory( getSSLSocketFactory(sslContext) );}/*** 为 HttpClient 设置默认的 SSLSocketFactory* @useage* HttpGet request = new HttpGet(url);* request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");* CloseableHttpResponse response = httpclient.execute(request);* String responseBody = readResponseBody(response);* System.out.println(responseBody);* @return*/public static CloseableHttpClient setDefaultSSLSocketFactoryForHttpClient(SSLContext sslContext){LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( sslContext );//或://SSLSocketFactory sslSocketFactory = getSSLSocketFactory();CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();return httpclient;}
}
AllowAllSslHTTPSTrustManager(可选)
import javax.net.ssl.*;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;/*** HTTPS信任证书管理器* @reference-doc* [1] 使用HttpsURLConnection的3种方法小结 - CSDN - https://blog.csdn.net/suyimin2010/article/details/81025083*/
public class AllowAllSslHTTPSTrustManager implements X509TrustManager {private static TrustManager[] trustManagers;private static final X509Certificate[] acceptedIssuers = new X509Certificate[] {};@Overridepublic void checkClientTrusted( X509Certificate[] x509Certificates, String s) throws java.security.cert.CertificateException {// To change body of implemented methods use File | Settings | File// Templates.// Do Nothing | don't check / allow all}@Overridepublic void checkServerTrusted(X509Certificate[] x509Certificates, String s)throws java.security.cert.CertificateException {// To change body of implemented methods use File | Settings | File// Templates.// Do Nothing | don't check / allow all}@Overridepublic X509Certificate[] getAcceptedIssuers() {return acceptedIssuers;}/*** 获取 信任全部 SSL/TLS CA证书的 SSLContext* @note 外部直接使用本方法即可*/public static SSLContext getAllowAllSslSSLContext() {HttpsURLConnection.setDefaultHostnameVerifier(DO_NOT_VERIFY);SSLContext context = null;if (trustManagers == null) {trustManagers = new TrustManager[] {new AllowAllSslHTTPSTrustManager()};}try {context = SSLContext.getInstance("TLS");//context.init(null, trustManagers, null);//或:context.init(null, trustManagers, new SecureRandom());} catch (NoSuchAlgorithmException e) {//Fail to getInstancee.printStackTrace();throw new RuntimeException( e );} catch (KeyManagementException e) {//Fail to inite.printStackTrace();throw new RuntimeException( e );}return context;}public static SSLSocketFactory getAllowAllSslSSLContextFactory() {SSLContext context = getAllowAllSslSSLContext();return context.getSocketFactory();}@SuppressWarnings("squid:S5527")final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {//根据 hostname 和 SSLSession ,做 SSL 验证public boolean verify(String hostname, SSLSession session) {//将所有验证的结果都直接设为true => 即 不验证return true;}};
}
Test
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Map;@Slf4j
public class HttpRequestUtilsTest {@Testpublic void test() throws IOException {/** step0 准备参数 **/String apiConnection = "fileTextDownloadApiRequest";String fileUri = "https://www.baidu.com";String agentCode = "xxx";String jobName = "xxxJob";Map<String, String> requestProperties = new HashMap<>();Boolean isEnableAgentCode = true;/** step1 设置租户代码 **/if(isEnableAgentCode){//requestProperties.put(Constants.AgentCode.AGENT_CODE_REQUEST_HEADER, agentCode);requestProperties.put("requester", "FLINK_JOB#" + jobName);//标识请求方,便于问题溯源}/** step2 发起API请求,获得连接 **/HttpURLConnection connectedHttpURLConnection = HttpRequestUtils.getHttpURLConnection(fileUri, requestProperties);/** step3 建立 tcp 连接 **/connectedHttpURLConnection.connect();/** step4 判定是否请求成功 **/boolean isRequestSuccess = true;try {isRequestSuccess = HttpRequestUtils.isRequestSuccess(connectedHttpURLConnection, apiConnection);log.debug(String.format("Success to build the api request for `%s`.fileUri : %s, agentCode : %s, isEnableAgentCode : %s", apiConnection, fileUri, agentCode, isEnableAgentCode));} catch (Exception exception){isRequestSuccess = false;log.error(exception.getMessage());log.error(String.format("Fail to build the api request for `%s`.fileUri : %s, agentCode : %s, isEnableAgentCode : %s", apiConnection, fileUri, agentCode, isEnableAgentCode));exception.printStackTrace();}InputStream responseInputStream = connectedHttpURLConnection.getInputStream();/** step5 解析 HTTP响应 + 断开连接 **///Map<String, List<XXXDTO>> xxxDtoMapList = null;//if(isRequestSuccess){// xxxDtoMapList = XXXFileUtils.parseXXXFileToSignalsDto(responseInputStream);//}/** step6 断开连接 **/connectedHttpURLConnection.disconnect();log.debug(String.format("disconnected the http url connection(`%s`)!", apiConnection));//return xxxDtoMapList;}@SneakyThrows@Testpublic void notVerifySslTest(){String url = "https://baidu.com";String urlAlias = "baiduIndexHttpRequest";Map<String, String> requestProperties = new HashMap<>();//SSLContext sslContext = HttpRequestUtils.getAllowTargetSslSSLContext(String keyStoreFilePath, String keyStorePassword);SSLContext sslContext = AllowAllSslHTTPSTrustManager.getAllowAllSslSSLContext();HttpURLConnection httpURLConnection = HttpRequestUtils.getHttpURLConnection(sslContext, url, requestProperties);httpURLConnection.connect();Boolean isRequestSuccess = null;try {isRequestSuccess = HttpRequestUtils.isRequestSuccess(httpURLConnection, urlAlias);log.debug(String.format("Success to build the api request for `%s`! url : %s", urlAlias , url));} catch (Exception exception) {log.error("Fail to request the target url!url:{},exception:\n{}", url, exception);exception.printStackTrace();httpURLConnection.disconnect();System.exit(0);}if(isRequestSuccess){InputStream responseInputStream = httpURLConnection.getInputStream();int available = responseInputStream.available();log.info("responseInputStream.available", available);}httpURLConnection.disconnect();//主动关闭连接log.info("end");}
}
Y 推荐文献
- [Java SE/JDK/网络] 核心源码精讲:java.net.HttpURLConnection - 博客园/千千寰宇
- [身份认证/JWT] 身份认证方案与HTTP请求中Authorization Header - 博客园/千千寰宇 【推荐】
- [网络/HTTPS/Java] PKI公钥基础设施体系:数字证书(X.509)、CA机构 | 含:证书管理工具(jdk keytool / openssl) - 博客园/千千寰宇 【推荐】