1 序
- 轻量级HTTP网络请求工具,接续:
- 类比: HttpClient / OkHttp 等 Java HTTP 网络请求工具
引入依赖
- jdk 1.8.0_261
- maven 依赖
<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.security
FileInputStream 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[] {};
@Override
public 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
}
@Override
public 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
}
@Override
public 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 getInstance
e.printStackTrace();
throw new RuntimeException( e );
} catch (KeyManagementException e) {//Fail to init
e.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 {
@Test
public 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
@Test
public 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) - 博客园/千千寰宇 【推荐】