一、背景
在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 {
}
}};