JAVA实现微信用户信息的获取

时间:2024-02-29 19:28:25

一、基于微信开发页面的配置

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
}