JAVA實現微信用戶信息的獲取


一、基於微信開發頁面的配置

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&amp;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
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM