一、基於微信開發頁面的配置
1.微信公眾平台主頁
進入開發文檔。
進入公眾號系統申請測試賬號。掃碼登錄后
會生成測試賬號信息,測試賬號可以配置在java代碼中,也可以持久化到數據庫(詳細見代碼)。
配置服務器域名,注意只配域名,不要帶https://
點擊修改,將授權回調頁面服務器域名配置進去。
掃碼關注獲取測試賬號權限。
二、開發的代碼
Controller層代碼:
package com.qdsg.controller.wechat;
import com.qdsg.service.wechat.WeChatService;
import com.qdsg.ylt.core.base.returnentity.ReturnEntity;
import com.qdsg.ylt.core.base.returnentity.SuccessEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 描述:
* 獲取微信的信息
*
* @author ZhangFengda
* @create 2019-04-16 20:59
*/
@RestController
@RequestMapping("/weChatInfo")
public class WeChatInfoController {
@Autowired
WeChatService weChatService;
/**
* 功能:用戶同意授權,獲取code,並重定向
*
*/
@RequestMapping(value = "/authorization", produces = { "application/json;charset=utf-8" },method= RequestMethod.GET)
public void authorizationNew(HttpServletResponse response,String redirectUri){
// todo 方便測試 此處自定義重定向url 上線時url由用戶傳來
redirectUri="http://j6qq9i.natappfree.cc/wechat/a.html";
String redirectUrl=weChatService.authorization(redirectUri);
System.out.println("重定向redirectUrl "+redirectUrl);
try {
response.sendRedirect(redirectUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 網頁 授權 access_token
*/
@RequestMapping(value = "/getAccessTokenByCode", produces = { "application/json;charset=utf-8" },method= RequestMethod.GET)
public ReturnEntity getAccessTokenByCode(String code){
return new SuccessEntity(weChatService.getAccessTokenByCode(code));
}
}
1.注意用戶同意授權的接口必須通過微信web開發工具進行訪問
訪問后會跳轉到重定向的頁面同時生成Code
2.再將Code作為參數 去訪問 網頁授權頁面 即可獲取用戶微信信息
Service層代碼:
package com.qdsg.service.wechat;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.qdsg.common.util.StringUtil;
import com.qdsg.common.wechat.model.auth.AccessTokenByCode;
import com.qdsg.common.wechat.model.auth.WeChatUserInfo;
import com.qdsg.mapper.TbConfigMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
/**
* 描述:
* 獲取微信信息
*
* @author ZhangFengda
* @create 2019-04-16 21:07
*/
@Service
public class WeChatService {
@Resource
TbConfigMapper configMapper;
Logger log = LoggerFactory.getLogger(WeChatAuthService.class);
/**
* <p class="detail">
* 功能:微信授權
* </p>
*
* @return
* @author zhaoyang
* @date 2017年7月12日
*/
public String authorization(String redirectUri) {
//從數據庫中查尋appId
String appId = configMapper.getCvalueByCkey("wechat_public_platform_app_id");
return authorization(redirectUri,appId);
}
/**
* <p class="detail">
* 功能:微信授權獲得code
* </p>
*
* @param appId 公眾號的唯一標識
* @param redirectUri 授權后重定向的回調鏈接地址, 請使用 urlEncode 對鏈接進行處理
* @return
*/
private String authorization(String redirectUri,String appId){
Map<String,Object> m=new HashMap<>(2);
StringBuffer sb = new StringBuffer();
String state= StringUtil.number();
String redirect_uri=null;
log.info("redirectUri ***"+redirectUri);
try {
//回調地址
redirect_uri=URLEncoder.encode(redirectUri,"utf-8");
log.info("redirect_uri ***"+redirect_uri);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
sb.append("https://open.weixin.qq.com/connect/oauth2/authorize?appid=");
sb.append(appId);
sb.append("&redirect_uri=").append(redirect_uri);
sb.append("&response_type=code&scope=snsapi_userinfo");
sb.append("&state=").append(state);
sb.append("#wechat_redirect");
m.put("redirect", sb);
return m.get("redirect").toString();
}
/**
* 功能:按時間戳生成編號
* */
private static String number(){
String s = "";
while(s.length()<4)
s+=(int)(Math.random()*10);
String b= String.valueOf(System.currentTimeMillis());
return s+b;
}
/**
* <p class="detail">
* 功能:根據網頁獲取
* 網頁授權接口調用憑證
* </p>
* @author zhaoyang
* @date 2017年6月19日
* @param code
* @return
*/
public Map<String,Object> getAccessTokenByCode(String code){
Map<String, Object> m = new HashMap<>();
// 去數據庫中查尋appId 和 secret
String appId =configMapper.getCvalueByCkey("wechat_public_platform_app_id");
String secret=configMapper.getCvalueByCkey("wechat_public_platform_app_secret");
AccessTokenByCode atbc=getAccessTokenByCode(code, appId, secret);
String accessToken=atbc.getAccess_token();
String openId= atbc.getOpenid();
m.put("access_token", accessToken);
m.put("expires_in", atbc.getExpires_in());
m.put("refresh_token", atbc.getRefresh_token());
m.put("openid",openId);
m.put("scope", atbc.getScope());
// 刷新access_token
refreshAccessTokenByCode(atbc.getRefresh_token(),appId);
//獲取微信用戶的公開信息
WeChatUserInfo userInfo=getUserInfoByAccessToken(openId, accessToken);
System.out.println("hahha");
return m;
}
/**
* <p class="detail">
* 功能:通過網頁授權 獲取AccessToken
* </p>
* @param appId 公眾號的唯一標識
* @param code code作為換取access_token的票據,每次用戶授權帶上的code將不一樣,code只能使用一次,5分鍾未被使用自動過期。
* @param secret 公眾號的app secret
* @return
*/
private static AccessTokenByCode getAccessTokenByCode(String code, String appId, String secret) {
String url="https://api.weixin.qq.com/sns/oauth2/access_token";
Map<String,Object> formMap=new HashMap<>(4);
formMap.put("appid",appId);
formMap.put("secret",secret);
formMap.put("code",code);
formMap.put("grant_type","authorization_code");
//將封裝好的formMap 作為參數 以get方式進行url請求 並獲取響應結果
String body = HttpRequest.get(url).form(formMap).execute().body();
//將獲取的body 轉換成AccessTokenByCode
return JSONUtil.toBean(body, AccessTokenByCode.class);
}
/**
* <p class="detail">
* 功能:由於access_token擁有較短的有效期,
* 當access_token超時后,可以使用refresh_token進行刷新,
* refresh_token有效期為30天,當refresh_token失效之后,需要用戶重新授權
* </p>
* @param refreshToken 填寫通過access_token獲取到的refresh_token參數
* @param appId 公眾號的唯一標識
* @return
*/
private AccessTokenByCode refreshAccessTokenByCode(String refreshToken,String appId) {
String url="https://api.weixin.qq.com/sns/oauth2/refresh_token";
Map<String,Object> formMap=new HashMap<>(4);
formMap.put("appid",appId);
formMap.put("refresh_token",refreshToken);
formMap.put("grant_type","refresh_token");
String body = HttpRequest.get(url).form(formMap).execute().body();
return JSONUtil.toBean(body, AccessTokenByCode.class);
}
/***
* <p class="detail">
* 功能:獲取微信用戶的公開個人信息
* 根據 accessToken和openId
* 獲取微信用戶的公開個人信息
* </p>
* @param accessToken 網頁授權接口調用憑證,注意:此access_token與基礎支持的access_token不同
* @param openId 用戶的唯一標識
* @return
*/
private static WeChatUserInfo getUserInfoByAccessToken(String openId, String accessToken) {
WeChatUserInfo weChatUserInfo=null;
String url="https://api.weixin.qq.com/sns/userinfo";
Map<String,Object> formMap=new HashMap<>(4);
formMap.put("openid",openId);
formMap.put("access_token",accessToken);
// 返回國家地區語言版本,zh_CN 簡體,zh_TW 繁體,en 英語
formMap.put("lang","zh_CN");
String body = HttpRequest.get(url).form(formMap).execute().body();
JSONObject jsonObject = JSONUtil.parseObj(body);
weChatUserInfo = JSONUtil.toBean(jsonObject, WeChatUserInfo.class);
return weChatUserInfo;
}
}
debug 示意圖 獲取到用戶微信信息,斷點在service中的178行。
實體類代碼
package com.qdsg.common.wechat.model.auth;
import com.google.gson.JsonArray;
public class WeChatUserInfo {
// openid 普通用戶的標識,對當前開發者帳號唯一
// nickname 普通用戶昵稱
// sex 普通用戶性別,1為男性,2為女性
// province 普通用戶個人資料填寫的省份
// city 普通用戶個人資料填寫的城市
// country 國家,如中國為CN
// headimgurl
// 用戶頭像,最后一個數值代表正方形頭像大小(有0、46、64、96、132數值可選,0代表640*640正方形頭像),用戶沒有頭像時該項為空
// privilege 用戶特權信息,json數組,如微信沃卡用戶為(chinaunicom)
// unionid 用戶統一標識。針對一個微信開放平台帳號下的應用,同一用戶的unionid是唯一的。
/** 用戶的唯一標識 */
private String openid;
/** 用戶昵稱 */
private String nickname;
/** 用戶的性別,值為1時是男性,值為2時是女性,值為0時是未知 */
private String sex;
/** 用戶個人資料填寫的省份 */
private String province;
/** 普通用戶個人資料填寫的城市 */
private String city;
/** 國家,如中國為CN */
private String country;
/** 用戶頭像,最后一個數值代表正方形頭像大小(有0、46、64、96、132數值可選,0代表640*640正方形頭像),
* 用戶沒有頭像時該項為空。若用戶更換頭像,原有頭像URL將失效。 */
private String headimgurl;
/** 用戶特權信息,json 數組,如微信沃卡用戶為(chinaunicom) */
private JsonArray[] privilege;
/** 只有在用戶將公眾號綁定到微信開放平台帳號后,才會出現該字段。 */
private String unionid;
private String jsonStr;
public JsonArray[] getPrivilege() {
return privilege;
}
public void setPrivilege(JsonArray[] privilege) {
this.privilege = privilege;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getHeadimgurl() {
return headimgurl;
}
public void setHeadimgurl(String headimgurl) {
this.headimgurl = headimgurl;
}
public String getUnionid() {
return unionid;
}
public void setUnionid(String unionid) {
this.unionid = unionid;
}
public String getJsonStr() {
return jsonStr;
}
public void setJsonStr(String jsonStr) {
this.jsonStr = jsonStr;
}
}
package com.qdsg.common.wechat.model.auth;
public class AccessTokenByCode {
/** 網頁授權接口調用憑證,注意:此access_token與基礎支持的access_token不同 */
private String access_token;
/** access_token接口調用憑證超時時間,單位(秒) */
private int expires_in;
/** 用戶刷新access_token */
private String refresh_token;
/** 用戶唯一標識,請注意,在未關注公眾號時,用戶訪問公眾號的網頁,也會產生一個用戶和公眾號唯一的OpenID */
private String openid;
/** 用戶授權的作用域,使用逗號(,)分隔 */
private String scope;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
}
封裝的請求類HttpRequest
package cn.hutool.http;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.BytesResource;
import cn.hutool.core.io.resource.FileResource;
import cn.hutool.core.io.resource.MultiFileResource;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.cookie.ThreadLocalCookieStore;
import cn.hutool.http.ssl.SSLSocketFactoryBuilder;
import cn.hutool.json.JSON;
import cn.hutool.log.StaticLog;
/**
* http請求類<br>
* Http請求類用於構建Http請求並同步獲取結果,此類通過CookieManager持有域名對應的Cookie值,再次請求時會自動附帶Cookie信息
*
* @author Looly
*/
public class HttpRequest extends HttpBase<HttpRequest> {
/** 默認超時時長,-1表示默認超時時長 */
public static final int TIMEOUT_DEFAULT = -1;
private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16);
private static final byte[] BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY).getBytes();
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
private static final String CONTENT_TYPE_X_WWW_FORM_URLENCODED_PREFIX = "application/x-www-form-urlencoded;charset=";
private static final String CONTENT_TYPE_MULTIPART_PREFIX = "multipart/form-data; boundary=";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";
/** Cookie管理 */
protected static CookieManager cookieManager;
static {
cookieManager = new CookieManager(new ThreadLocalCookieStore(), CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(cookieManager);
}
/**
* 獲取Cookie管理器,用於自定義Cookie管理
*
* @return {@link CookieManager}
* @since 4.1.0
*/
public static CookieManager getCookieManager() {
return cookieManager;
}
/**
* 關閉Cookie
*
* @since 4.1.9
*/
public static void closeCookie() {
cookieManager = null;
CookieHandler.setDefault(null);
}
private String url;
private URLStreamHandler urlHandler;
private Method method = Method.GET;
/** 默認超時 */
private int timeout = TIMEOUT_DEFAULT;
/** 存儲表單數據 */
private Map<String, Object> form;
/** 文件表單對象,用於文件上傳 */
private Map<String, Resource> fileForm;
/** Cookie */
private String cookie;
/** 連接對象 */
private HttpConnection httpConnection;
/** 是否禁用緩存 */
private boolean isDisableCache;
/** 是否對url中的參數進行編碼 */
private boolean encodeUrlParams;
/** 是否是REST請求模式 */
private boolean isRest;
/** 重定向次數計數器,內部使用 */
private int redirectCount;
/** 最大重定向次數 */
private int maxRedirectCount;
/** 代理 */
private Proxy proxy;
/** HostnameVerifier,用於HTTPS安全連接 */
private HostnameVerifier hostnameVerifier;
/** SSLSocketFactory,用於HTTPS安全連接 */
private SSLSocketFactory ssf;
/**
* 構造
*
* @param url URL
*/
public HttpRequest(String url) {
Assert.notBlank(url, "Param [url] can not be blank !");
this.url = URLUtil.normalize(url, true);
// 給定一個默認頭信息
this.header(GlobalHeaders.INSTANCE.headers);
}
// ---------------------------------------------------------------- static Http Method start
/**
* POST請求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest post(String url) {
return new HttpRequest(url).method(Method.POST);
}
/**
* GET請求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest get(String url) {
return new HttpRequest(url).method(Method.GET);
}
/**
* HEAD請求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest head(String url) {
return new HttpRequest(url).method(Method.HEAD);
}
/**
* OPTIONS請求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest options(String url) {
return new HttpRequest(url).method(Method.OPTIONS);
}
/**
* PUT請求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest put(String url) {
return new HttpRequest(url).method(Method.PUT);
}
/**
* PATCH請求
*
* @param url URL
* @return HttpRequest
* @since 3.0.9
*/
public static HttpRequest patch(String url) {
return new HttpRequest(url).method(Method.PATCH);
}
/**
* DELETE請求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest delete(String url) {
return new HttpRequest(url).method(Method.DELETE);
}
/**
* TRACE請求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest trace(String url) {
return new HttpRequest(url).method(Method.TRACE);
}
// ---------------------------------------------------------------- static Http Method end
/**
* 獲取請求URL
*
* @return URL字符串
* @since 4.1.8
*/
public String getUrl() {
return url;
}
/**
* 設置URL
*
* @param url url字符串
* @since 4.1.8
*/
public HttpRequest setUrl(String url) {
this.url = url;
return this;
}
/**
* 設置{@link URLStreamHandler}
* <p>
* 部分環境下需要單獨設置此項,例如當 WebLogic Server 實例充當 SSL 客戶端角色(它會嘗試通過 SSL 連接到其他服務器或應用程序)時,它會驗證 SSL 服務器在數字證書中返回的主機名是否與用於連接 SSL 服務器的 URL 主機名相匹配。<br>
* 如果主機名不匹配,則刪除此連接。<br>
* 因此weblogic不支持https的sni協議的主機名驗證,此時需要將此值設置為sun.net.www.protocol.https.Handler對象。
* <p>
* 相關issue見:https://gitee.com/loolly/hutool/issues/IMD1X
*
* @param urlHandler url字符串
* @since 4.1.9
*/
public HttpRequest setUrlHandler(URLStreamHandler urlHandler) {
this.urlHandler = urlHandler;
return this;
}
/**
* 獲取Http請求方法
*
* @return {@link Method}
* @since 4.1.8
*/
public Method getMethod() {
return this.method;
}
/**
* 設置請求方法
*
* @param method HTTP方法
* @return HttpRequest
* @see #method(Method)
* @since 4.1.8
*/
public HttpRequest setMethod(Method method) {
return method(method);
}
/**
* 獲取{@link HttpConnection}
*
* @return {@link HttpConnection}
* @since 4.2.2
*/
public HttpConnection getConnection() {
return this.httpConnection;
}
/**
* 設置請求方法
*
* @param method HTTP方法
* @return HttpRequest
*/
public HttpRequest method(Method method) {
if (Method.PATCH == method) {
this.method = Method.POST;
this.header("X-HTTP-Method-Override", "PATCH");
} else {
this.method = method;
}
return this;
}
// ---------------------------------------------------------------- Http Request Header start
/**
* 設置contentType
*
* @param contentType contentType
* @return HttpRequest
*/
public HttpRequest contentType(String contentType) {
header(Header.CONTENT_TYPE, contentType);
return this;
}
/**
* 設置是否為長連接
*
* @param isKeepAlive 是否長連接
* @return HttpRequest
*/
public HttpRequest keepAlive(boolean isKeepAlive) {
header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
return this;
}
/**
* @return 獲取是否為長連接
*/
public boolean isKeepAlive() {
String connection = header(Header.CONNECTION);
if (connection == null) {
return !httpVersion.equalsIgnoreCase(HTTP_1_0);
}
return !connection.equalsIgnoreCase("close");
}
/**
* 獲取內容長度
*
* @return String
*/
public String contentLength() {
return header(Header.CONTENT_LENGTH);
}
/**
* 設置內容長度
*
* @param value 長度
* @return HttpRequest
*/
public HttpRequest contentLength(int value) {
header(Header.CONTENT_LENGTH, String.valueOf(value));
return this;
}
/**
* 設置Cookie<br>
* 自定義Cookie后會覆蓋Hutool的默認Cookie行為
*
* @param cookies Cookie值數組,如果為{@code null}則設置無效,使用默認Cookie行為
* @return this
* @since 3.1.1
*/
public HttpRequest cookie(HttpCookie... cookies) {
if (ArrayUtil.isEmpty(cookies)) {
return disableCookie();
}
return cookie(ArrayUtil.join(cookies, ";"));
}
/**
* 設置Cookie<br>
* 自定義Cookie后會覆蓋Hutool的默認Cookie行為
*
* @param cookie Cookie值,如果為{@code null}則設置無效,使用默認Cookie行為
* @return this
* @since 3.0.7
*/
public HttpRequest cookie(String cookie) {
this.cookie = cookie;
return this;
}
/**
* 禁用默認Cookie行為,此方法調用后會將Cookie置為空。<br>
* 如果想重新啟用Cookie,請調用:{@link #cookie(String)}方法自定義Cookie。<br>
* 如果想啟動默認的Cookie行為(自動回填服務器傳回的Cookie),則調用{@link #enableDefaultCookie()}
*
* @return this
* @since 3.0.7
*/
public HttpRequest disableCookie() {
return cookie(StrUtil.EMPTY);
}
/**
* 打開默認的Cookie行為(自動回填服務器傳回的Cookie)
*
* @return this
*/
public HttpRequest enableDefaultCookie() {
return cookie((String) null);
}
// ---------------------------------------------------------------- Http Request Header end
// ---------------------------------------------------------------- Form start
/**
* 設置表單數據<br>
*
* @param name 名
* @param value 值
* @return this
*/
public HttpRequest form(String name, Object value) {
if (StrUtil.isBlank(name) || ObjectUtil.isNull(value)) {
return this; // 忽略非法的form表單項內容;
}
// 停用body
this.bodyBytes = null;
if (value instanceof File) {
// 文件上傳
return this.form(name, (File) value);
} else if (value instanceof Resource) {
// 自定義流上傳
return this.form(name, (Resource) value);
} else if (this.form == null) {
this.form = new LinkedHashMap<>();
}
String strValue;
if (value instanceof List) {
// 列表對象
strValue = CollectionUtil.join((List<?>) value, ",");
} else if (ArrayUtil.isArray(value)) {
if (File.class == ArrayUtil.getComponentType(value)) {
// 多文件
return this.form(name, (File[]) value);
}
// 數組對象
strValue = ArrayUtil.join((Object[]) value, ",");
} else {
// 其他對象一律轉換為字符串
strValue = Convert.toStr(value, null);
}
form.put(name, strValue);
return this;
}
/**
* 設置表單數據
*
* @param name 名
* @param value 值
* @param parameters 參數對,奇數為名,偶數為值
* @return this
*
*/
public HttpRequest form(String name, Object value, Object... parameters) {
form(name, value);
for (int i = 0; i < parameters.length; i += 2) {
name = parameters[i].toString();
form(name, parameters[i + 1]);
}
return this;
}
/**
* 設置map類型表單數據
*
* @param formMap 表單內容
* @return this
*
*/
public HttpRequest form(Map<String, Object> formMap) {
if (MapUtil.isNotEmpty(formMap)) {
for (Map.Entry<String, Object> entry : formMap.entrySet()) {
form(entry.getKey(), entry.getValue());
}
}
return this;
}
/**
* 文件表單項<br>
* 一旦有文件加入,表單變為multipart/form-data
*
* @param name 名
* @param files 需要上傳的文件
* @return this
*/
public HttpRequest form(String name, File... files) {
if (1 == files.length) {
final File file = files[0];
return form(name, file, file.getName());
}
return form(name, new MultiFileResource(files));
}
/**
* 文件表單項<br>
* 一旦有文件加入,表單變為multipart/form-data
*
* @param name 名
* @param file 需要上傳的文件
* @return this
*/
public HttpRequest form(String name, File file) {
return form(name, file, file.getName());
}
/**
* 文件表單項<br>
* 一旦有文件加入,表單變為multipart/form-data
*
* @param name 名
* @param file 需要上傳的文件
* @param fileName 文件名,為空使用文件默認的文件名
* @return this
*/
public HttpRequest form(String name, File file, String fileName) {
if (null != file) {
form(name, new FileResource(file, fileName));
}
return this;
}
/**
* 文件byte[]表單項<br>
* 一旦有文件加入,表單變為multipart/form-data
*
* @param name 名
* @param fileBytes 需要上傳的文件
* @param fileName 文件名
* @return this
* @since 4.1.0
*/
public HttpRequest form(String name, byte[] fileBytes, String fileName) {
if (null != fileBytes) {
form(name, new BytesResource(fileBytes, fileName));
}
return this;
}
/**
* 文件表單項<br>
* 一旦有文件加入,表單變為multipart/form-data
*
* @param name 名
* @param resource 數據源,文件可以使用{@link FileResource}包裝使用
* @return this
* @since 4.0.9
*/
public HttpRequest form(String name, Resource resource) {
if (null != resource) {
if (false == isKeepAlive()) {
keepAlive(true);
}
if (null == this.fileForm) {
fileForm = new HashMap<>();
}
// 文件對象
this.fileForm.put(name, resource);
}
return this;
}
/**
* 獲取表單數據
*
* @return 表單Map
*/
public Map<String, Object> form() {
return this.form;
}
/**
* 獲取文件表單數據
*
* @return 文件表單Map
* @since 3.3.0
*/
public Map<String, Resource> fileForm() {
return this.fileForm;
}
// ---------------------------------------------------------------- Form end
// ---------------------------------------------------------------- Body start
/**
* 設置內容主體
*
* @param body 請求體
* @return this
*/
public HttpRequest body(String body) {
return this.body(body, null);
}
/**
* 設置內容主體<br>
* 請求體body參數支持兩種類型:
*
* <pre>
* 1. 標准參數,例如 a=1&b=2 這種格式
* 2. Rest模式,此時body需要傳入一個JSON或者XML字符串,Hutool會自動綁定其對應的Content-Type
* </pre>
*
* @param body 請求體
* @param contentType 請求體類型
* @return this
*/
public HttpRequest body(String body, String contentType) {
body(StrUtil.bytes(body, this.charset));
this.form = null; // 當使用body時,停止form的使用
contentLength((null != body ? body.length() : 0));
if (null != contentType) {
// Content-Type自定義設置
this.contentType(contentType);
} else {
// 在用戶未自定義的情況下自動根據內容判斷
contentType = HttpUtil.getContentTypeByRequestBody(body);
if (null != contentType && ContentType.isDefault(this.header(Header.CONTENT_TYPE))) {
if (null != this.charset) {
// 附加編碼信息
contentType = StrUtil.format("{};charset={}", contentType, this.charset.name());
}
this.contentType(contentType);
}
}
// 判斷是否為rest請求
if (StrUtil.containsAnyIgnoreCase(contentType, "json", "xml")) {
this.isRest = true;
}
return this;
}
/**
* 設置JSON內容主體<br>
* 設置默認的Content-Type為 application/json 需在此方法調用前使用charset方法設置編碼,否則使用默認編碼UTF-8
*
* @param json JSON請求體
* @return this
*/
public HttpRequest body(JSON json) {
return this.body(json.toString());
}
/**
* 設置主體字節碼<br>
* 需在此方法調用前使用charset方法設置編碼,否則使用默認編碼UTF-8
*
* @param bodyBytes 主體
* @return this
*/
public HttpRequest body(byte[] bodyBytes) {
this.bodyBytes = bodyBytes;
return this;
}
// ---------------------------------------------------------------- Body end
/**
* 設置超時,單位:毫秒
*
* @param milliseconds 超時毫秒數
* @return this
*/
public HttpRequest timeout(int milliseconds) {
this.timeout = milliseconds;
return this;
}
/**
* 禁用緩存
*
* @return this
*/
public HttpRequest disableCache() {
this.isDisableCache = true;
return this;
}
/**
* 是否對URL中的參數進行編碼
*
* @param isEncodeUrlParams 是否對URL中的參數進行編碼
* @return this
* @since 4.4.1
*/
public HttpRequest setEncodeUrlParams(boolean isEncodeUrlParams) {
this.encodeUrlParams = isEncodeUrlParams;
return this;
}
/**
* 設置是否打開重定向,如果打開默認重定向次數為2<br>
* 此方法效果與{@link #setMaxRedirectCount(int)} 一致
*
* @param isFollowRedirects 是否打開重定向
* @return this
*/
public HttpRequest setFollowRedirects(boolean isFollowRedirects) {
return setMaxRedirectCount(isFollowRedirects ? 2 : 0);
}
/**
* 設置最大重定向次數<br>
* 如果次數小於1則表示不重定向,大於等於1表示打開重定向
*
* @param maxRedirectCount 最大重定向次數
* @return this
* @since 3.3.0
*/
public HttpRequest setMaxRedirectCount(int maxRedirectCount) {
if (maxRedirectCount > 0) {
this.maxRedirectCount = maxRedirectCount;
} else {
this.maxRedirectCount = 0;
}
return this;
}
/**
* 設置域名驗證器<br>
* 只針對HTTPS請求,如果不設置,不做驗證,所有域名被信任
*
* @param hostnameVerifier HostnameVerifier
* @return this
*/
public HttpRequest setHostnameVerifier(HostnameVerifier hostnameVerifier) {
// 驗證域
this.hostnameVerifier = hostnameVerifier;
return this;
}
/**
* 設置代理
*
* @param proxy 代理 {@link Proxy}
* @return this
*/
public HttpRequest setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
/**
* 設置SSLSocketFactory<br>
* 只針對HTTPS請求,如果不設置,使用默認的SSLSocketFactory<br>
* 默認SSLSocketFactory為:SSLSocketFactoryBuilder.create().build();
*
* @param ssf SSLScketFactory
* @return this
*/
public HttpRequest setSSLSocketFactory(SSLSocketFactory ssf) {
this.ssf = ssf;
return this;
}
/**
* 設置HTTPS安全連接協議,只針對HTTPS請求,可以使用的協議包括:<br>
*
* <pre>
* 1. TLSv1.2
* 2. TLSv1.1
* 3. SSLv3
* ...
* </pre>
*
* @see SSLSocketFactoryBuilder
* @param protocol 協議
* @return this
*/
public HttpRequest setSSLProtocol(String protocol) {
if (null == this.ssf) {
try {
this.ssf = SSLSocketFactoryBuilder.create().setProtocol(protocol).build();
} catch (Exception e) {
throw new HttpException(e);
}
}
return this;
}
/**
* 設置是否rest模式
*
* @param isRest 是否rest模式
* @return this
* @since 4.5.0
*/
public HttpRequest setRest(boolean isRest) {
this.isRest = isRest;
return this;
}
/**
* 執行Reuqest請求
*
* @return this
*/
public HttpResponse execute() {
return this.execute(false);
}
/**
* 異步請求<br>
* 異步請求后獲取的{@link HttpResponse} 為異步模式,此時此對象持有Http鏈接(http鏈接並不會關閉),直調用獲取內容方法為止
*
* @return 異步對象,使用get方法獲取HttpResponse對象
*/
public HttpResponse executeAsync() {
return this.execute(true);
}
/**
* 執行Reuqest請求
*
* @param isAsync 是否異步
* @return this
*/
public HttpResponse execute(boolean isAsync) {
// 初始化URL
urlWithParamIfGet();
// 初始化 connection
initConnecton();
// 發送請求
send();
// 手動實現重定向
HttpResponse httpResponse = sendRedirectIfPosible();
// 獲取響應
if (null == httpResponse) {
httpResponse = new HttpResponse(this.httpConnection, this.charset, isAsync, isIgnoreResponseBody());
}
return httpResponse;
}
/**
* 簡單驗證
*
* @param username 用戶名
* @param password 密碼
* @return HttpRequest
*/
public HttpRequest basicAuth(String username, String password) {
final String data = username.concat(":").concat(password);
final String base64 = Base64.encode(data, charset);
header("Authorization", "Basic " + base64, true);
return this;
}
// ---------------------------------------------------------------- Private method start
/**
* 初始化網絡連接
*/
private void initConnecton() {
// 初始化 connection
this.httpConnection = HttpConnection.create(URLUtil.toUrlForHttp(this.url, this.urlHandler), this.method, this.hostnameVerifier, this.ssf, this.timeout, this.proxy)//
.header(this.headers, true); // 覆蓋默認Header
// 自定義Cookie
if (null != this.cookie) {
this.httpConnection.setCookie(this.cookie);
}
// 是否禁用緩存
if (this.isDisableCache) {
this.httpConnection.disableCache();
}
// 定義轉發
this.httpConnection.setInstanceFollowRedirects(maxRedirectCount > 0 ? true : false);
}
/**
* 對於GET請求將參數加到URL中
*/
private void urlWithParamIfGet() {
if (Method.GET.equals(method) && false == this.isRest) {
// 優先使用body形式的參數,不存在使用form
if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
this.url = HttpUtil.urlWithForm(this.url, StrUtil.str(this.bodyBytes, this.charset), this.charset, this.encodeUrlParams);
} else {
this.url = HttpUtil.urlWithForm(this.url, this.form, this.charset, this.encodeUrlParams);
}
}
}
/**
* 調用轉發,如果需要轉發返回轉發結果,否則返回<code>null</code>
*
* @return {@link HttpResponse},無轉發返回 <code>null</code>
*/
private HttpResponse sendRedirectIfPosible() {
if (this.maxRedirectCount < 1) {
// 不重定向
return null;
}
// 手動實現重定向
if (this.httpConnection.getHttpURLConnection().getInstanceFollowRedirects()) {
int responseCode;
try {
responseCode = httpConnection.responseCode();
} catch (IOException e) {
throw new HttpException(e);
}
if (responseCode != HttpURLConnection.HTTP_OK) {
if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
this.url = httpConnection.header(Header.LOCATION);
if (redirectCount < this.maxRedirectCount) {
redirectCount++;
return execute();
} else {
StaticLog.warn("URL [{}] redirect count more than two !", this.url);
}
}
}
}
return null;
}
/**
* 發送數據流
*
* @throws IOException
*/
private void send() throws HttpException {
try {
if (Method.POST.equals(this.method) || Method.PUT.equals(this.method) || Method.DELETE.equals(this.method) || this.isRest) {
if (CollectionUtil.isEmpty(this.fileForm)) {
sendFormUrlEncoded();// 普通表單
} else {
sendMultipart(); // 文件上傳表單
}
} else {
this.httpConnection.connect();
}
} catch (IOException e) {
throw new HttpException(e.getMessage(), e);
}
}
/**
* 發送普通表單
*
* @throws IOException
*/
private void sendFormUrlEncoded() throws IOException {
if (StrUtil.isBlank(this.header(Header.CONTENT_TYPE))) {
// 如果未自定義Content-Type,使用默認的application/x-www-form-urlencoded
this.httpConnection.header(Header.CONTENT_TYPE, CONTENT_TYPE_X_WWW_FORM_URLENCODED_PREFIX + this.charset, true);
}
// Write的時候會優先使用body中的內容,write時自動關閉OutputStream
if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
IoUtil.write(this.httpConnection.getOutputStream(), true, this.bodyBytes);
} else {
final String content = HttpUtil.toParams(this.form, this.charset);
IoUtil.write(this.httpConnection.getOutputStream(), this.charset, true, content);
}
}
/**
* 發送多組件請求(例如包含文件的表單)
*
* @throws IOException
*/
private void sendMultipart() throws IOException {
setMultipart();// 設置表單類型為Multipart
final OutputStream out = this.httpConnection.getOutputStream();
try {
writeFileForm(out);
writeForm(out);
formEnd(out);
} catch (IOException e) {
throw e;
} finally {
IoUtil.close(out);
}
}
// 普通字符串數據
/**
* 發送普通表單內容
*
* @param out 輸出流
* @throws IOException
*/
private void writeForm(OutputStream out) throws IOException {
if (CollectionUtil.isNotEmpty(this.form)) {
StringBuilder builder = StrUtil.builder();
for (Entry<String, Object> entry : this.form.entrySet()) {
builder.append("--").append(BOUNDARY).append(StrUtil.CRLF);
builder.append(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, entry.getKey()));
builder.append(entry.getValue()).append(StrUtil.CRLF);
}
IoUtil.write(out, this.charset, false, builder);
}
}
/**
* 發送文件對象表單
*
* @param out 輸出流
* @throws IOException
*/
private void writeFileForm(OutputStream out) throws IOException {
for (Entry<String, Resource> entry : this.fileForm.entrySet()) {
appendPart(entry.getKey(), entry.getValue(), out);
}
}
/**
* 添加Multipart表單的數據項
*
* @param formFieldName 表單名
* @param resource 資源,可以是文件等
* @param out Http流
* @since 4.1.0
*/
private void appendPart(String formFieldName, Resource resource, OutputStream out) {
if (resource instanceof MultiResource) {
// 多資源
for (Resource subResource : (MultiResource) resource) {
appendPart(formFieldName, subResource, out);
}
} else {
// 普通資源
final StringBuilder builder = StrUtil.builder().append("--").append(BOUNDARY).append(StrUtil.CRLF);
builder.append(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, resource.getName()));
builder.append(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(resource.getName())));
IoUtil.write(out, this.charset, false, builder);
InputStream in = null;
try {
in = resource.getStream();
IoUtil.copy(in, out);
} finally {
IoUtil.close(in);
}
IoUtil.write(out, this.charset, false, StrUtil.CRLF);
}
}
// 添加結尾數據
/**
* 上傳表單結束
*
* @param out 輸出流
* @throws IOException
*/
private void formEnd(OutputStream out) throws IOException {
out.write(BOUNDARY_END);
out.flush();
}
/**
* 設置表單類型為Multipart(文件上傳)
*
* @return HttpConnection
*/
private void setMultipart() {
this.httpConnection.header(Header.CONTENT_TYPE, CONTENT_TYPE_MULTIPART_PREFIX + BOUNDARY, true);
}
/**
* 是否忽略讀取響應body部分<br>
* HEAD、CONNECT、OPTIONS、TRACE方法將不讀取響應體
*
* @return 是否需要忽略響應body部分
* @since 3.1.2
*/
private boolean isIgnoreResponseBody() {
if (Method.HEAD == this.method || Method.CONNECT == this.method || Method.OPTIONS == this.method || Method.TRACE == this.method) {
return true;
}
return false;
}
// ---------------------------------------------------------------- Private method end
}
封裝的響應類
package cn.hutool.http;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpCookie;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
/**
* Http響應類<br>
* 非線程安全對象
*
* @author Looly
*
*/
public class HttpResponse extends HttpBase<HttpResponse> implements Closeable{
/** 持有連接對象 */
private HttpConnection httpConnection;
/** Http請求原始流 */
private InputStream in;
/** 是否異步,異步下只持有流,否則將在初始化時直接讀取body內容 */
private volatile boolean isAsync;
/** 響應狀態碼 */
private int status;
/** 是否忽略讀取Http響應體 */
private boolean ignoreBody;
/** 從響應中獲取的編碼 */
private Charset charsetFromResponse;
/**
* 構造
*
* @param httpConnection {@link HttpConnection}
* @param charset 編碼,從請求編碼中獲取默認編碼
* @param isAsync 是否異步
* @param isIgnoreBody 是否忽略讀取響應體
* @since 3.1.2
*/
protected HttpResponse(HttpConnection httpConnection, Charset charset, boolean isAsync, boolean isIgnoreBody) {
this.httpConnection = httpConnection;
this.charset = charset;
this.isAsync = isAsync;
this.ignoreBody = isIgnoreBody;
init();
}
/**
* 獲取狀態碼
*
* @return 狀態碼
*/
public int getStatus() {
return this.status;
}
/**
* 請求是否成功,判斷依據為:狀態碼范圍在200~299內。
* @return 是否成功請求
* @since 4.1.9
*/
public boolean isOk() {
return this.status >= 200 && this.status < 300;
}
/**
* 同步<br>
* 如果為異步狀態,則暫時不讀取服務器中響應的內容,而是持有Http鏈接的{@link InputStream}。<br>
* 當調用此方法時,異步狀態轉為同步狀態,此時從Http鏈接流中讀取body內容並暫存在內容中。如果已經是同步狀態,則不進行任何操作。
*
* @return this
* @throws HttpException IO異常
*/
public HttpResponse sync() throws HttpException{
return this.isAsync ? forceSync() : this;
}
// ---------------------------------------------------------------- Http Response Header start
/**
* 獲取內容編碼
* @return String
*/
public String contentEncoding() {
return header(Header.CONTENT_ENCODING);
}
/**
* @return 是否為gzip壓縮過的內容
*/
public boolean isGzip(){
final String contentEncoding = contentEncoding();
return contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip");
}
/**
* 獲取本次請求服務器返回的Cookie信息
* @return Cookie字符串
* @since 3.1.1
*/
public String getCookieStr() {
return header(Header.SET_COOKIE);
}
/**
* 獲取Cookie
* @return Cookie列表
* @since 3.1.1
*/
public List<HttpCookie> getCookies(){
return HttpRequest.cookieManager.getCookieStore().getCookies();
}
/**
* 獲取Cookie
*
* @param name Cookie名
* @return {@link HttpCookie}
* @since 4.1.4
*/
public HttpCookie getCookie(String name) {
List<HttpCookie> cookie = getCookies();
if(null != cookie) {
for (HttpCookie httpCookie : cookie) {
if(httpCookie.getName().equals(name)) {
return httpCookie;
}
}
}
return null;
}
/**
* 獲取Cookie值
*
* @param name Cookie名
* @return Cookie值
* @since 4.1.4
*/
public String getCookieValue(String name) {
HttpCookie cookie = getCookie(name);
return (null == cookie) ? null : cookie.getValue();
}
// ---------------------------------------------------------------- Http Response Header end
// ---------------------------------------------------------------- Body start
/**
* 獲得服務區響應流<br>
* 異步模式下獲取Http原生流,同步模式下獲取獲取到的在內存中的副本<br>
* 如果想在同步模式下獲取流,請先調用{@link #sync()}方法強制同步<br>
* 流獲取后處理完畢需關閉此類
*
* @return 響應流
*/
public InputStream bodyStream(){
if(isAsync) {
return this.in;
}
return new ByteArrayInputStream(this.bodyBytes);
}
/**
* 獲取響應流字節碼<br>
* 此方法會轉為同步模式
*
* @return byte[]
*/
public byte[] bodyBytes() {
sync();
return this.bodyBytes;
}
/**
* 獲取響應主體
* @return String
* @throws HttpException 包裝IO異常
*/
public String body() throws HttpException{
try {
return HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse);
} catch (IOException e) {
throw new HttpException(e);
}
}
/**
* 將響應內容寫出到{@link OutputStream}<br>
* 異步模式下直接讀取Http流寫出,同步模式下將存儲在內存中的響應內容寫出<br>
* 寫出后會關閉Http流(異步模式)
*
* @param out 寫出的流
* @param isCloseOut 是否關閉輸出流
* @param streamProgress 進度顯示接口,通過實現此接口顯示下載進度
* @return 寫出bytes數
* @since 3.3.2
*/
public long writeBody(OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {
if (null == out) {
throw new NullPointerException("[out] is null!");
}
try {
return IoUtil.copyByNIO(bodyStream(), out, IoUtil.DEFAULT_BUFFER_SIZE, streamProgress);
} finally {
IoUtil.close(this);
if (isCloseOut) {
IoUtil.close(out);
}
}
}
/**
* 將響應內容寫出到文件<br>
* 異步模式下直接讀取Http流寫出,同步模式下將存儲在內存中的響應內容寫出<br>
* 寫出后會關閉Http流(異步模式)
*
* @param destFile 寫出到的文件
* @param streamProgress 進度顯示接口,通過實現此接口顯示下載進度
* @return 寫出bytes數
* @since 3.3.2
*/
public long writeBody(File destFile, StreamProgress streamProgress) {
if (null == destFile) {
throw new NullPointerException("[destFile] is null!");
}
if (destFile.isDirectory()) {
//從頭信息中獲取文件名
String fileName = getFileNameFromDisposition();
if(StrUtil.isBlank(fileName)) {
final String path = this.httpConnection.getUrl().getPath();
//從路徑中獲取文件名
fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
if (StrUtil.isBlank(fileName)) {
//編碼后的路徑做為文件名
fileName = URLUtil.encodeQuery(path, CharsetUtil.CHARSET_UTF_8);
}
}
destFile = FileUtil.file(destFile, fileName);
}
OutputStream out = null;
try {
out = FileUtil.getOutputStream(destFile);
return writeBody(out, false, streamProgress);
} catch (IORuntimeException e) {
throw new HttpException(e);
} finally {
IoUtil.close(out);
}
}
/**
* 將響應內容寫出到文件<br>
* 異步模式下直接讀取Http流寫出,同步模式下將存儲在內存中的響應內容寫出<br>
* 寫出后會關閉Http流(異步模式)
*
* @param destFile 寫出到的文件
* @return 寫出bytes數
* @since 3.3.2
*/
public long writeBody(File destFile) {
return writeBody(destFile, null);
}
/**
* 將響應內容寫出到文件<br>
* 異步模式下直接讀取Http流寫出,同步模式下將存儲在內存中的響應內容寫出<br>
* 寫出后會關閉Http流(異步模式)
*
* @param destFilePath 寫出到的文件的路徑
* @return 寫出bytes數
* @since 3.3.2
*/
public long writeBody(String destFilePath) {
return writeBody(FileUtil.file(destFilePath));
}
// ---------------------------------------------------------------- Body end
@Override
public void close() {
IoUtil.close(this.in);
this.in = null;
//關閉連接
this.httpConnection.disconnect();
}
@Override
public String toString() {
StringBuilder sb = StrUtil.builder();
sb.append("Response Headers: ").append(StrUtil.CRLF);
for (Entry<String, List<String>> entry : this.headers.entrySet()) {
sb.append(" ").append(entry).append(StrUtil.CRLF);
}
sb.append("Response Body: ").append(StrUtil.CRLF);
sb.append(" ").append(this.body()).append(StrUtil.CRLF);
return sb.toString();
}
// ---------------------------------------------------------------- Private method start
/**
* 初始化Http響應<br>
* 初始化包括:
* <pre>
* 1、讀取Http狀態
* 2、讀取頭信息
* 3、持有Http流,並不關閉流
* </pre>
*
* @return this
* @throws HttpException IO異常
*/
private HttpResponse init() throws HttpException{
try {
this.status = httpConnection.responseCode();
this.headers = httpConnection.headers();
final Charset charset = httpConnection.getCharset();
this.charsetFromResponse = charset;
if(null != charset) {
this.charset = charset;
}
this.in = (this.status < HttpStatus.HTTP_BAD_REQUEST) ? httpConnection.getInputStream() : httpConnection.getErrorStream();
} catch (IOException e) {
if(e instanceof FileNotFoundException){
//服務器無返回內容,忽略之
}else{
throw new HttpException(e);
}
}
if(null == this.in) {
//在一些情況下,返回的流為null,此時提供狀態碼說明
this.in = new ByteArrayInputStream(StrUtil.format("Error request, response status: {}", this.status).getBytes());
} else if(isGzip() && false == (in instanceof GZIPInputStream)){
try {
in = new GZIPInputStream(in);
} catch (IOException e) {
//在類似於Head等方法中無body返回,此時GZIPInputStream構造會出現錯誤,在此忽略此錯誤讀取普通數據
//ignore
}
}
//同步情況下強制同步
return this.isAsync ? this : forceSync();
}
/**
* 讀取主體,忽略EOFException異常
* @param in 輸入流
* @return 自身
* @throws IORuntimeException IO異常
*/
private void readBody(InputStream in) throws IORuntimeException{
if(ignoreBody) {
return;
}
int contentLength = Convert.toInt(header(Header.CONTENT_LENGTH), 0);
final FastByteArrayOutputStream out = contentLength > 0 ? new FastByteArrayOutputStream(contentLength) : new FastByteArrayOutputStream();
try {
IoUtil.copy(in, out);
} catch (IORuntimeException e) {
if(e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF")) {
//忽略讀取HTTP流中的EOF錯誤
}else {
throw e;
}
}
this.bodyBytes = out.toByteArray();
}
/**
* 強制同步,用於初始化<br>
* 強制同步后變化如下:
* <pre>
* 1、讀取body內容到內存
* 2、異步狀態設為false(變為同步狀態)
* 3、關閉Http流
* 4、斷開與服務器連接
* </pre>
*
* @return this
*/
private HttpResponse forceSync() {
//非同步狀態轉為同步狀態
try {
this.readBody(this.in);
} catch (IORuntimeException e) {
if(e.getCause() instanceof FileNotFoundException){
//服務器無返回內容,忽略之
}else{
throw new HttpException(e);
}
}finally {
if(this.isAsync) {
this.isAsync = false;
}
this.close();
}
return this;
}
/**
* 從Content-Disposition頭中獲取文件名
* @return 文件名,empty表示無
*/
private String getFileNameFromDisposition() {
String fileName = null;
final String desposition = header(Header.CONTENT_DISPOSITION);
if(StrUtil.isNotBlank(desposition)) {
fileName = ReUtil.get("filename=\"(.*?)\"", desposition, 1);
if(StrUtil.isBlank(fileName)) {
fileName = StrUtil.subAfter(desposition, "filename=", true);
}
}
return fileName;
}
// ---------------------------------------------------------------- Private method end
}