一、基于微信开发页面的配置
1.微信公众平台主页
进入开发文档。
进入公众号系统申请测试账号。扫码登录后
会生成测试账号信息,测试账号可以配置在java代码中,也可以持久化到数据库(详细见代码)。
配置服务器域名,注意只配域名,不要带https://
点击修改,将授权回调页面服务器域名配置进去。
扫码关注获取测试账号权限。
二、开发的代码
Controller层代码:
package com.qdsg.controller.wechat;
import com.qdsg.service.wechat.WeChatService;
import com.qdsg.ylt.core.base.returnentity.ReturnEntity;
import com.qdsg.ylt.core.base.returnentity.SuccessEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 描述:
* 获取微信的信息
*
* @author ZhangFengda
* @create 2019-04-16 20:59
*/
@RestController
@RequestMapping("/weChatInfo")
public class WeChatInfoController {
@Autowired
WeChatService weChatService;
/**
* 功能:用户同意授权,获取code,并重定向
*
*/
@RequestMapping(value = "/authorization", produces = { "application/json;charset=utf-8" },method= RequestMethod.GET)
public void authorizationNew(HttpServletResponse response,String redirectUri){
// todo 方便测试 此处自定义重定向url 上线时url由用户传来
redirectUri="http://j6qq9i.natappfree.cc/wechat/a.html";
String redirectUrl=weChatService.authorization(redirectUri);
System.out.println("重定向redirectUrl "+redirectUrl);
try {
response.sendRedirect(redirectUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 网页 授权 access_token
*/
@RequestMapping(value = "/getAccessTokenByCode", produces = { "application/json;charset=utf-8" },method= RequestMethod.GET)
public ReturnEntity getAccessTokenByCode(String code){
return new SuccessEntity(weChatService.getAccessTokenByCode(code));
}
}
1.注意用户同意授权的接口必须通过微信web开发工具进行访问
访问后会跳转到重定向的页面同时生成Code
2.再将Code作为参数 去访问 网页授权页面 即可获取用户微信信息
Service层代码:
package com.qdsg.service.wechat;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.qdsg.common.util.StringUtil;
import com.qdsg.common.wechat.model.auth.AccessTokenByCode;
import com.qdsg.common.wechat.model.auth.WeChatUserInfo;
import com.qdsg.mapper.TbConfigMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
/**
* 描述:
* 获取微信信息
*
* @author ZhangFengda
* @create 2019-04-16 21:07
*/
@Service
public class WeChatService {
@Resource
TbConfigMapper configMapper;
Logger log = LoggerFactory.getLogger(WeChatAuthService.class);
/**
* <p class="detail">
* 功能:微信授权
* </p>
*
* @return
* @author zhaoyang
* @date 2017年7月12日
*/
public String authorization(String redirectUri) {
//从数据库中查寻appId
String appId = configMapper.getCvalueByCkey("wechat_public_platform_app_id");
return authorization(redirectUri,appId);
}
/**
* <p class="detail">
* 功能:微信授权获得code
* </p>
*
* @param appId 公众号的唯一标识
* @param redirectUri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
* @return
*/
private String authorization(String redirectUri,String appId){
Map<String,Object> m=new HashMap<>(2);
StringBuffer sb = new StringBuffer();
String state= StringUtil.number();
String redirect_uri=null;
log.info("redirectUri ***"+redirectUri);
try {
//回调地址
redirect_uri=URLEncoder.encode(redirectUri,"utf-8");
log.info("redirect_uri ***"+redirect_uri);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
sb.append("https://open.weixin.qq.com/connect/oauth2/authorize?appid=");
sb.append(appId);
sb.append("&redirect_uri=").append(redirect_uri);
sb.append("&response_type=code&scope=snsapi_userinfo");
sb.append("&state=").append(state);
sb.append("#wechat_redirect");
m.put("redirect", sb);
return m.get("redirect").toString();
}
/**
* 功能:按时间戳生成编号
* */
private static String number(){
String s = "";
while(s.length()<4)
s+=(int)(Math.random()*10);
String b= String.valueOf(System.currentTimeMillis());
return s+b;
}
/**
* <p class="detail">
* 功能:根据网页获取
* 网页授权接口调用凭证
* </p>
* @author zhaoyang
* @date 2017年6月19日
* @param code
* @return
*/
public Map<String,Object> getAccessTokenByCode(String code){
Map<String, Object> m = new HashMap<>();
// 去数据库中查寻appId 和 secret
String appId =configMapper.getCvalueByCkey("wechat_public_platform_app_id");
String secret=configMapper.getCvalueByCkey("wechat_public_platform_app_secret");
AccessTokenByCode atbc=getAccessTokenByCode(code, appId, secret);
String accessToken=atbc.getAccess_token();
String openId= atbc.getOpenid();
m.put("access_token", accessToken);
m.put("expires_in", atbc.getExpires_in());
m.put("refresh_token", atbc.getRefresh_token());
m.put("openid",openId);
m.put("scope", atbc.getScope());
// 刷新access_token
refreshAccessTokenByCode(atbc.getRefresh_token(),appId);
//获取微信用户的公开信息
WeChatUserInfo userInfo=getUserInfoByAccessToken(openId, accessToken);
System.out.println("hahha");
return m;
}
/**
* <p class="detail">
* 功能:通过网页授权 获取AccessToken
* </p>
* @param appId 公众号的唯一标识
* @param code code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
* @param secret 公众号的app secret
* @return
*/
private static AccessTokenByCode getAccessTokenByCode(String code, String appId, String secret) {
String url="https://api.weixin.qq.com/sns/oauth2/access_token";
Map<String,Object> formMap=new HashMap<>(4);
formMap.put("appid",appId);
formMap.put("secret",secret);
formMap.put("code",code);
formMap.put("grant_type","authorization_code");
//将封装好的formMap 作为参数 以get方式进行url请求 并获取响应结果
String body = HttpRequest.get(url).form(formMap).execute().body();
//将获取的body 转换成AccessTokenByCode
return JSONUtil.toBean(body, AccessTokenByCode.class);
}
/**
* <p class="detail">
* 功能:由于access_token拥有较短的有效期,
* 当access_token超时后,可以使用refresh_token进行刷新,
* refresh_token有效期为30天,当refresh_token失效之后,需要用户重新授权
* </p>
* @param refreshToken 填写通过access_token获取到的refresh_token参数
* @param appId 公众号的唯一标识
* @return
*/
private AccessTokenByCode refreshAccessTokenByCode(String refreshToken,String appId) {
String url="https://api.weixin.qq.com/sns/oauth2/refresh_token";
Map<String,Object> formMap=new HashMap<>(4);
formMap.put("appid",appId);
formMap.put("refresh_token",refreshToken);
formMap.put("grant_type","refresh_token");
String body = HttpRequest.get(url).form(formMap).execute().body();
return JSONUtil.toBean(body, AccessTokenByCode.class);
}
/***
* <p class="detail">
* 功能:获取微信用户的公开个人信息
* 根据 accessToken和openId
* 获取微信用户的公开个人信息
* </p>
* @param accessToken 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
* @param openId 用户的唯一标识
* @return
*/
private static WeChatUserInfo getUserInfoByAccessToken(String openId, String accessToken) {
WeChatUserInfo weChatUserInfo=null;
String url="https://api.weixin.qq.com/sns/userinfo";
Map<String,Object> formMap=new HashMap<>(4);
formMap.put("openid",openId);
formMap.put("access_token",accessToken);
// 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语
formMap.put("lang","zh_CN");
String body = HttpRequest.get(url).form(formMap).execute().body();
JSONObject jsonObject = JSONUtil.parseObj(body);
weChatUserInfo = JSONUtil.toBean(jsonObject, WeChatUserInfo.class);
return weChatUserInfo;
}
}
debug 示意图 获取到用户微信信息,断点在service中的178行。
实体类代码
package com.qdsg.common.wechat.model.auth;
import com.google.gson.JsonArray;
public class WeChatUserInfo {
// openid 普通用户的标识,对当前开发者帐号唯一
// nickname 普通用户昵称
// sex 普通用户性别,1为男性,2为女性
// province 普通用户个人资料填写的省份
// city 普通用户个人资料填写的城市
// country 国家,如中国为CN
// headimgurl
// 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
// privilege 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
// unionid 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
/** 用户的唯一标识 */
private String openid;
/** 用户昵称 */
private String nickname;
/** 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 */
private String sex;
/** 用户个人资料填写的省份 */
private String province;
/** 普通用户个人资料填写的城市 */
private String city;
/** 国家,如中国为CN */
private String country;
/** 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),
* 用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 */
private String headimgurl;
/** 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom) */
private JsonArray[] privilege;
/** 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 */
private String unionid;
private String jsonStr;
public JsonArray[] getPrivilege() {
return privilege;
}
public void setPrivilege(JsonArray[] privilege) {
this.privilege = privilege;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getHeadimgurl() {
return headimgurl;
}
public void setHeadimgurl(String headimgurl) {
this.headimgurl = headimgurl;
}
public String getUnionid() {
return unionid;
}
public void setUnionid(String unionid) {
this.unionid = unionid;
}
public String getJsonStr() {
return jsonStr;
}
public void setJsonStr(String jsonStr) {
this.jsonStr = jsonStr;
}
}
package com.qdsg.common.wechat.model.auth;
public class AccessTokenByCode {
/** 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 */
private String access_token;
/** access_token接口调用凭证超时时间,单位(秒) */
private int expires_in;
/** 用户刷新access_token */
private String refresh_token;
/** 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID */
private String openid;
/** 用户授权的作用域,使用逗号(,)分隔 */
private String scope;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
}
封装的请求类HttpRequest
package cn.hutool.http;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.BytesResource;
import cn.hutool.core.io.resource.FileResource;
import cn.hutool.core.io.resource.MultiFileResource;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.cookie.ThreadLocalCookieStore;
import cn.hutool.http.ssl.SSLSocketFactoryBuilder;
import cn.hutool.json.JSON;
import cn.hutool.log.StaticLog;
/**
* http请求类<br>
* Http请求类用于构建Http请求并同步获取结果,此类通过CookieManager持有域名对应的Cookie值,再次请求时会自动附带Cookie信息
*
* @author Looly
*/
public class HttpRequest extends HttpBase<HttpRequest> {
/** 默认超时时长,-1表示默认超时时长 */
public static final int TIMEOUT_DEFAULT = -1;
private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16);
private static final byte[] BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY).getBytes();
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
private static final String CONTENT_TYPE_X_WWW_FORM_URLENCODED_PREFIX = "application/x-www-form-urlencoded;charset=";
private static final String CONTENT_TYPE_MULTIPART_PREFIX = "multipart/form-data; boundary=";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";
/** Cookie管理 */
protected static CookieManager cookieManager;
static {
cookieManager = new CookieManager(new ThreadLocalCookieStore(), CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(cookieManager);
}
/**
* 获取Cookie管理器,用于自定义Cookie管理
*
* @return {@link CookieManager}
* @since 4.1.0
*/
public static CookieManager getCookieManager() {
return cookieManager;
}
/**
* 关闭Cookie
*
* @since 4.1.9
*/
public static void closeCookie() {
cookieManager = null;
CookieHandler.setDefault(null);
}
private String url;
private URLStreamHandler urlHandler;
private Method method = Method.GET;
/** 默认超时 */
private int timeout = TIMEOUT_DEFAULT;
/** 存储表单数据 */
private Map<String, Object> form;
/** 文件表单对象,用于文件上传 */
private Map<String, Resource> fileForm;
/** Cookie */
private String cookie;
/** 连接对象 */
private HttpConnection httpConnection;
/** 是否禁用缓存 */
private boolean isDisableCache;
/** 是否对url中的参数进行编码 */
private boolean encodeUrlParams;
/** 是否是REST请求模式 */
private boolean isRest;
/** 重定向次数计数器,内部使用 */
private int redirectCount;
/** 最大重定向次数 */
private int maxRedirectCount;
/** 代理 */
private Proxy proxy;
/** HostnameVerifier,用于HTTPS安全连接 */
private HostnameVerifier hostnameVerifier;
/** SSLSocketFactory,用于HTTPS安全连接 */
private SSLSocketFactory ssf;
/**
* 构造
*
* @param url URL
*/
public HttpRequest(String url) {
Assert.notBlank(url, "Param [url] can not be blank !");
this.url = URLUtil.normalize(url, true);
// 给定一个默认头信息
this.header(GlobalHeaders.INSTANCE.headers);
}
// ---------------------------------------------------------------- static Http Method start
/**
* POST请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest post(String url) {
return new HttpRequest(url).method(Method.POST);
}
/**
* GET请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest get(String url) {
return new HttpRequest(url).method(Method.GET);
}
/**
* HEAD请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest head(String url) {
return new HttpRequest(url).method(Method.HEAD);
}
/**
* OPTIONS请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest options(String url) {
return new HttpRequest(url).method(Method.OPTIONS);
}
/**
* PUT请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest put(String url) {
return new HttpRequest(url).method(Method.PUT);
}
/**
* PATCH请求
*
* @param url URL
* @return HttpRequest
* @since 3.0.9
*/
public static HttpRequest patch(String url) {
return new HttpRequest(url).method(Method.PATCH);
}
/**
* DELETE请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest delete(String url) {
return new HttpRequest(url).method(Method.DELETE);
}
/**
* TRACE请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest trace(String url) {
return new HttpRequest(url).method(Method.TRACE);
}
// ---------------------------------------------------------------- static Http Method end
/**
* 获取请求URL
*
* @return URL字符串
* @since 4.1.8
*/
public String getUrl() {
return url;
}
/**
* 设置URL
*
* @param url url字符串
* @since 4.1.8
*/
public HttpRequest setUrl(String url) {
this.url = url;
return this;
}
/**
* 设置{@link URLStreamHandler}
* <p>
* 部分环境下需要单独设置此项,例如当 WebLogic Server 实例充当 SSL 客户端角色(它会尝试通过 SSL 连接到其他服务器或应用程序)时,它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。<br>
* 如果主机名不匹配,则删除此连接。<br>
* 因此weblogic不支持https的sni协议的主机名验证,此时需要将此值设置为sun.net.www.protocol.https.Handler对象。
* <p>
* 相关issue见:https://gitee.com/loolly/hutool/issues/IMD1X
*
* @param urlHandler url字符串
* @since 4.1.9
*/
public HttpRequest setUrlHandler(URLStreamHandler urlHandler) {
this.urlHandler = urlHandler;
return this;
}
/**
* 获取Http请求方法
*
* @return {@link Method}
* @since 4.1.8
*/
public Method getMethod() {
return this.method;
}
/**
* 设置请求方法
*
* @param method HTTP方法
* @return HttpRequest
* @see #method(Method)
* @since 4.1.8
*/
public HttpRequest setMethod(Method method) {
return method(method);
}
/**
* 获取{@link HttpConnection}
*
* @return {@link HttpConnection}
* @since 4.2.2
*/
public HttpConnection getConnection() {
return this.httpConnection;
}
/**
* 设置请求方法
*
* @param method HTTP方法
* @return HttpRequest
*/
public HttpRequest method(Method method) {
if (Method.PATCH == method) {
this.method = Method.POST;
this.header("X-HTTP-Method-Override", "PATCH");
} else {
this.method = method;
}
return this;
}
// ---------------------------------------------------------------- Http Request Header start
/**
* 设置contentType
*
* @param contentType contentType
* @return HttpRequest
*/
public HttpRequest contentType(String contentType) {
header(Header.CONTENT_TYPE, contentType);
return this;
}
/**
* 设置是否为长连接
*
* @param isKeepAlive 是否长连接
* @return HttpRequest
*/
public HttpRequest keepAlive(boolean isKeepAlive) {
header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
return this;
}
/**
* @return 获取是否为长连接
*/
public boolean isKeepAlive() {
String connection = header(Header.CONNECTION);
if (connection == null) {
return !httpVersion.equalsIgnoreCase(HTTP_1_0);
}
return !connection.equalsIgnoreCase("close");
}
/**
* 获取内容长度
*
* @return String
*/
public String contentLength() {
return header(Header.CONTENT_LENGTH);
}
/**
* 设置内容长度
*
* @param value 长度
* @return HttpRequest
*/
public HttpRequest contentLength(int value) {
header(Header.CONTENT_LENGTH, String.valueOf(value));
return this;
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookies Cookie值数组,如果为{@code null}则设置无效,使用默认Cookie行为
* @return this
* @since 3.1.1
*/
public HttpRequest cookie(HttpCookie... cookies) {
if (ArrayUtil.isEmpty(cookies)) {
return disableCookie();
}
return cookie(ArrayUtil.join(cookies, ";"));
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookie Cookie值,如果为{@code null}则设置无效,使用默认Cookie行为
* @return this
* @since 3.0.7
*/
public HttpRequest cookie(String cookie) {
this.cookie = cookie;
return this;
}
/**
* 禁用默认Cookie行为,此方法调用后会将Cookie置为空。<br>
* 如果想重新启用Cookie,请调用:{@link #cookie(String)}方法自定义Cookie。<br>
* 如果想启动默认的Cookie行为(自动回填服务器传回的Cookie),则调用{@link #enableDefaultCookie()}
*
* @return this
* @since 3.0.7
*/
public HttpRequest disableCookie() {
return cookie(StrUtil.EMPTY);
}
/**
* 打开默认的Cookie行为(自动回填服务器传回的Cookie)
*
* @return this
*/
public HttpRequest enableDefaultCookie() {
return cookie((String) null);
}
// ---------------------------------------------------------------- Http Request Header end
// ---------------------------------------------------------------- Form start
/**
* 设置表单数据<br>
*
* @param name 名
* @param value 值
* @return this
*/
public HttpRequest form(String name, Object value) {
if (StrUtil.isBlank(name) || ObjectUtil.isNull(value)) {
return this; // 忽略非法的form表单项内容;
}
// 停用body
this.bodyBytes = null;
if (value instanceof File) {
// 文件上传
return this.form(name, (File) value);
} else if (value instanceof Resource) {
// 自定义流上传
return this.form(name, (Resource) value);
} else if (this.form == null) {
this.form = new LinkedHashMap<>();
}
String strValue;
if (value instanceof List) {
// 列表对象
strValue = CollectionUtil.join((List<?>) value, ",");
} else if (ArrayUtil.isArray(value)) {
if (File.class == ArrayUtil.getComponentType(value)) {
// 多文件
return this.form(name, (File[]) value);
}
// 数组对象
strValue = ArrayUtil.join((Object[]) value, ",");
} else {
// 其他对象一律转换为字符串
strValue = Convert.toStr(value, null);
}
form.put(name, strValue);
return this;
}
/**
* 设置表单数据
*
* @param name 名
* @param value 值
* @param parameters 参数对,奇数为名,偶数为值
* @return this
*
*/
public HttpRequest form(String name, Object value, Object... parameters) {
form(name, value);
for (int i = 0; i < parameters.length; i += 2) {
name = parameters[i].toString();
form(name, parameters[i + 1]);
}
return this;
}
/**
* 设置map类型表单数据
*
* @param formMap 表单内容
* @return this
*
*/
public HttpRequest form(Map<String, Object> formMap) {
if (MapUtil.isNotEmpty(formMap)) {
for (Map.Entry<String, Object> entry : formMap.entrySet()) {
form(entry.getKey(), entry.getValue());
}
}
return this;
}
/**
* 文件表单项<br>
* 一旦有文件加入,表单变为multipart/form-data
*
* @param name 名
* @param files 需要上传的文件
* @return this
*/
public HttpRequest form(String name, File... files) {
if (1 == files.length) {
final File file = files[0];
return form(name, file, file.getName());
}
return form(name, new MultiFileResource(files));
}
/**
* 文件表单项<br>
* 一旦有文件加入,表单变为multipart/form-data
*
* @param name 名
* @param file 需要上传的文件
* @return this
*/
public HttpRequest form(String name, File file) {
return form(name, file, file.getName());
}
/**
* 文件表单项<br>
* 一旦有文件加入,表单变为multipart/form-data
*
* @param name 名
* @param file 需要上传的文件
* @param fileName 文件名,为空使用文件默认的文件名
* @return this
*/
public HttpRequest form(String name, File file, String fileName) {
if (null != file) {
form(name, new FileResource(file, fileName));
}
return this;
}
/**
* 文件byte[]表单项<br>
* 一旦有文件加入,表单变为multipart/form-data
*
* @param name 名
* @param fileBytes 需要上传的文件
* @param fileName 文件名
* @return this
* @since 4.1.0
*/
public HttpRequest form(String name, byte[] fileBytes, String fileName) {
if (null != fileBytes) {
form(name, new BytesResource(fileBytes, fileName));
}
return this;
}
/**
* 文件表单项<br>
* 一旦有文件加入,表单变为multipart/form-data
*
* @param name 名
* @param resource 数据源,文件可以使用{@link FileResource}包装使用
* @return this
* @since 4.0.9
*/
public HttpRequest form(String name, Resource resource) {
if (null != resource) {
if (false == isKeepAlive()) {
keepAlive(true);
}
if (null == this.fileForm) {
fileForm = new HashMap<>();
}
// 文件对象
this.fileForm.put(name, resource);
}
return this;
}
/**
* 获取表单数据
*
* @return 表单Map
*/
public Map<String, Object> form() {
return this.form;
}
/**
* 获取文件表单数据
*
* @return 文件表单Map
* @since 3.3.0
*/
public Map<String, Resource> fileForm() {
return this.fileForm;
}
// ---------------------------------------------------------------- Form end
// ---------------------------------------------------------------- Body start
/**
* 设置内容主体
*
* @param body 请求体
* @return this
*/
public HttpRequest body(String body) {
return this.body(body, null);
}
/**
* 设置内容主体<br>
* 请求体body参数支持两种类型:
*
* <pre>
* 1. 标准参数,例如 a=1&b=2 这种格式
* 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
* </pre>
*
* @param body 请求体
* @param contentType 请求体类型
* @return this
*/
public HttpRequest body(String body, String contentType) {
body(StrUtil.bytes(body, this.charset));
this.form = null; // 当使用body时,停止form的使用
contentLength((null != body ? body.length() : 0));
if (null != contentType) {
// Content-Type自定义设置
this.contentType(contentType);
} else {
// 在用户未自定义的情况下自动根据内容判断
contentType = HttpUtil.getContentTypeByRequestBody(body);
if (null != contentType && ContentType.isDefault(this.header(Header.CONTENT_TYPE))) {
if (null != this.charset) {
// 附加编码信息
contentType = StrUtil.format("{};charset={}", contentType, this.charset.name());
}
this.contentType(contentType);
}
}
// 判断是否为rest请求
if (StrUtil.containsAnyIgnoreCase(contentType, "json", "xml")) {
this.isRest = true;
}
return this;
}
/**
* 设置JSON内容主体<br>
* 设置默认的Content-Type为 application/json 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8
*
* @param json JSON请求体
* @return this
*/
public HttpRequest body(JSON json) {
return this.body(json.toString());
}
/**
* 设置主体字节码<br>
* 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8
*
* @param bodyBytes 主体
* @return this
*/
public HttpRequest body(byte[] bodyBytes) {
this.bodyBytes = bodyBytes;
return this;
}
// ---------------------------------------------------------------- Body end
/**
* 设置超时,单位:毫秒
*
* @param milliseconds 超时毫秒数
* @return this
*/
public HttpRequest timeout(int milliseconds) {
this.timeout = milliseconds;
return this;
}
/**
* 禁用缓存
*
* @return this
*/
public HttpRequest disableCache() {
this.isDisableCache = true;
return this;
}
/**
* 是否对URL中的参数进行编码
*
* @param isEncodeUrlParams 是否对URL中的参数进行编码
* @return this
* @since 4.4.1
*/
public HttpRequest setEncodeUrlParams(boolean isEncodeUrlParams) {
this.encodeUrlParams = isEncodeUrlParams;
return this;
}
/**
* 设置是否打开重定向,如果打开默认重定向次数为2<br>
* 此方法效果与{@link #setMaxRedirectCount(int)} 一致
*
* @param isFollowRedirects 是否打开重定向
* @return this
*/
public HttpRequest setFollowRedirects(boolean isFollowRedirects) {
return setMaxRedirectCount(isFollowRedirects ? 2 : 0);
}
/**
* 设置最大重定向次数<br>
* 如果次数小于1则表示不重定向,大于等于1表示打开重定向
*
* @param maxRedirectCount 最大重定向次数
* @return this
* @since 3.3.0
*/
public HttpRequest setMaxRedirectCount(int maxRedirectCount) {
if (maxRedirectCount > 0) {
this.maxRedirectCount = maxRedirectCount;
} else {
this.maxRedirectCount = 0;
}
return this;
}
/**
* 设置域名验证器<br>
* 只针对HTTPS请求,如果不设置,不做验证,所有域名被信任
*
* @param hostnameVerifier HostnameVerifier
* @return this
*/
public HttpRequest setHostnameVerifier(HostnameVerifier hostnameVerifier) {
// 验证域
this.hostnameVerifier = hostnameVerifier;
return this;
}
/**
* 设置代理
*
* @param proxy 代理 {@link Proxy}
* @return this
*/
public HttpRequest setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
/**
* 设置SSLSocketFactory<br>
* 只针对HTTPS请求,如果不设置,使用默认的SSLSocketFactory<br>
* 默认SSLSocketFactory为:SSLSocketFactoryBuilder.create().build();
*
* @param ssf SSLScketFactory
* @return this
*/
public HttpRequest setSSLSocketFactory(SSLSocketFactory ssf) {
this.ssf = ssf;
return this;
}
/**
* 设置HTTPS安全连接协议,只针对HTTPS请求,可以使用的协议包括:<br>
*
* <pre>
* 1. TLSv1.2
* 2. TLSv1.1
* 3. SSLv3
* ...
* </pre>
*
* @see SSLSocketFactoryBuilder
* @param protocol 协议
* @return this
*/
public HttpRequest setSSLProtocol(String protocol) {
if (null == this.ssf) {
try {
this.ssf = SSLSocketFactoryBuilder.create().setProtocol(protocol).build();
} catch (Exception e) {
throw new HttpException(e);
}
}
return this;
}
/**
* 设置是否rest模式
*
* @param isRest 是否rest模式
* @return this
* @since 4.5.0
*/
public HttpRequest setRest(boolean isRest) {
this.isRest = isRest;
return this;
}
/**
* 执行Reuqest请求
*
* @return this
*/
public HttpResponse execute() {
return this.execute(false);
}
/**
* 异步请求<br>
* 异步请求后获取的{@link HttpResponse} 为异步模式,此时此对象持有Http链接(http链接并不会关闭),直调用获取内容方法为止
*
* @return 异步对象,使用get方法获取HttpResponse对象
*/
public HttpResponse executeAsync() {
return this.execute(true);
}
/**
* 执行Reuqest请求
*
* @param isAsync 是否异步
* @return this
*/
public HttpResponse execute(boolean isAsync) {
// 初始化URL
urlWithParamIfGet();
// 初始化 connection
initConnecton();
// 发送请求
send();
// 手动实现重定向
HttpResponse httpResponse = sendRedirectIfPosible();
// 获取响应
if (null == httpResponse) {
httpResponse = new HttpResponse(this.httpConnection, this.charset, isAsync, isIgnoreResponseBody());
}
return httpResponse;
}
/**
* 简单验证
*
* @param username 用户名
* @param password 密码
* @return HttpRequest
*/
public HttpRequest basicAuth(String username, String password) {
final String data = username.concat(":").concat(password);
final String base64 = Base64.encode(data, charset);
header("Authorization", "Basic " + base64, true);
return this;
}
// ---------------------------------------------------------------- Private method start
/**
* 初始化网络连接
*/
private void initConnecton() {
// 初始化 connection
this.httpConnection = HttpConnection.create(URLUtil.toUrlForHttp(this.url, this.urlHandler), this.method, this.hostnameVerifier, this.ssf, this.timeout, this.proxy)//
.header(this.headers, true); // 覆盖默认Header
// 自定义Cookie
if (null != this.cookie) {
this.httpConnection.setCookie(this.cookie);
}
// 是否禁用缓存
if (this.isDisableCache) {
this.httpConnection.disableCache();
}
// 定义转发
this.httpConnection.setInstanceFollowRedirects(maxRedirectCount > 0 ? true : false);
}
/**
* 对于GET请求将参数加到URL中
*/
private void urlWithParamIfGet() {
if (Method.GET.equals(method) && false == this.isRest) {
// 优先使用body形式的参数,不存在使用form
if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
this.url = HttpUtil.urlWithForm(this.url, StrUtil.str(this.bodyBytes, this.charset), this.charset, this.encodeUrlParams);
} else {
this.url = HttpUtil.urlWithForm(this.url, this.form, this.charset, this.encodeUrlParams);
}
}
}
/**
* 调用转发,如果需要转发返回转发结果,否则返回<code>null</code>
*
* @return {@link HttpResponse},无转发返回 <code>null</code>
*/
private HttpResponse sendRedirectIfPosible() {
if (this.maxRedirectCount < 1) {
// 不重定向
return null;
}
// 手动实现重定向
if (this.httpConnection.getHttpURLConnection().getInstanceFollowRedirects()) {
int responseCode;
try {
responseCode = httpConnection.responseCode();
} catch (IOException e) {
throw new HttpException(e);
}
if (responseCode != HttpURLConnection.HTTP_OK) {
if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
this.url = httpConnection.header(Header.LOCATION);
if (redirectCount < this.maxRedirectCount) {
redirectCount++;
return execute();
} else {
StaticLog.warn("URL [{}] redirect count more than two !", this.url);
}
}
}
}
return null;
}
/**
* 发送数据流
*
* @throws IOException
*/
private void send() throws HttpException {
try {
if (Method.POST.equals(this.method) || Method.PUT.equals(this.method) || Method.DELETE.equals(this.method) || this.isRest) {
if (CollectionUtil.isEmpty(this.fileForm)) {
sendFormUrlEncoded();// 普通表单
} else {
sendMultipart(); // 文件上传表单
}
} else {
this.httpConnection.connect();
}
} catch (IOException e) {
throw new HttpException(e.getMessage(), e);
}
}
/**
* 发送普通表单
*
* @throws IOException
*/
private void sendFormUrlEncoded() throws IOException {
if (StrUtil.isBlank(this.header(Header.CONTENT_TYPE))) {
// 如果未自定义Content-Type,使用默认的application/x-www-form-urlencoded
this.httpConnection.header(Header.CONTENT_TYPE, CONTENT_TYPE_X_WWW_FORM_URLENCODED_PREFIX + this.charset, true);
}
// Write的时候会优先使用body中的内容,write时自动关闭OutputStream
if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
IoUtil.write(this.httpConnection.getOutputStream(), true, this.bodyBytes);
} else {
final String content = HttpUtil.toParams(this.form, this.charset);
IoUtil.write(this.httpConnection.getOutputStream(), this.charset, true, content);
}
}
/**
* 发送多组件请求(例如包含文件的表单)
*
* @throws IOException
*/
private void sendMultipart() throws IOException {
setMultipart();// 设置表单类型为Multipart
final OutputStream out = this.httpConnection.getOutputStream();
try {
writeFileForm(out);
writeForm(out);
formEnd(out);
} catch (IOException e) {
throw e;
} finally {
IoUtil.close(out);
}
}
// 普通字符串数据
/**
* 发送普通表单内容
*
* @param out 输出流
* @throws IOException
*/
private void writeForm(OutputStream out) throws IOException {
if (CollectionUtil.isNotEmpty(this.form)) {
StringBuilder builder = StrUtil.builder();
for (Entry<String, Object> entry : this.form.entrySet()) {
builder.append("--").append(BOUNDARY).append(StrUtil.CRLF);
builder.append(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, entry.getKey()));
builder.append(entry.getValue()).append(StrUtil.CRLF);
}
IoUtil.write(out, this.charset, false, builder);
}
}
/**
* 发送文件对象表单
*
* @param out 输出流
* @throws IOException
*/
private void writeFileForm(OutputStream out) throws IOException {
for (Entry<String, Resource> entry : this.fileForm.entrySet()) {
appendPart(entry.getKey(), entry.getValue(), out);
}
}
/**
* 添加Multipart表单的数据项
*
* @param formFieldName 表单名
* @param resource 资源,可以是文件等
* @param out Http流
* @since 4.1.0
*/
private void appendPart(String formFieldName, Resource resource, OutputStream out) {
if (resource instanceof MultiResource) {
// 多资源
for (Resource subResource : (MultiResource) resource) {
appendPart(formFieldName, subResource, out);
}
} else {
// 普通资源
final StringBuilder builder = StrUtil.builder().append("--").append(BOUNDARY).append(StrUtil.CRLF);
builder.append(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, resource.getName()));
builder.append(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(resource.getName())));
IoUtil.write(out, this.charset, false, builder);
InputStream in = null;
try {
in = resource.getStream();
IoUtil.copy(in, out);
} finally {
IoUtil.close(in);
}
IoUtil.write(out, this.charset, false, StrUtil.CRLF);
}
}
// 添加结尾数据
/**
* 上传表单结束
*
* @param out 输出流
* @throws IOException
*/
private void formEnd(OutputStream out) throws IOException {
out.write(BOUNDARY_END);
out.flush();
}
/**
* 设置表单类型为Multipart(文件上传)
*
* @return HttpConnection
*/
private void setMultipart() {
this.httpConnection.header(Header.CONTENT_TYPE, CONTENT_TYPE_MULTIPART_PREFIX + BOUNDARY, true);
}
/**
* 是否忽略读取响应body部分<br>
* HEAD、CONNECT、OPTIONS、TRACE方法将不读取响应体
*
* @return 是否需要忽略响应body部分
* @since 3.1.2
*/
private boolean isIgnoreResponseBody() {
if (Method.HEAD == this.method || Method.CONNECT == this.method || Method.OPTIONS == this.method || Method.TRACE == this.method) {
return true;
}
return false;
}
// ---------------------------------------------------------------- Private method end
}
封装的响应类
package cn.hutool.http;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpCookie;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
/**
* Http响应类<br>
* 非线程安全对象
*
* @author Looly
*
*/
public class HttpResponse extends HttpBase<HttpResponse> implements Closeable{
/** 持有连接对象 */
private HttpConnection httpConnection;
/** Http请求原始流 */
private InputStream in;
/** 是否异步,异步下只持有流,否则将在初始化时直接读取body内容 */
private volatile boolean isAsync;
/** 响应状态码 */
private int status;
/** 是否忽略读取Http响应体 */
private boolean ignoreBody;
/** 从响应中获取的编码 */
private Charset charsetFromResponse;
/**
* 构造
*
* @param httpConnection {@link HttpConnection}
* @param charset 编码,从请求编码中获取默认编码
* @param isAsync 是否异步
* @param isIgnoreBody 是否忽略读取响应体
* @since 3.1.2
*/
protected HttpResponse(HttpConnection httpConnection, Charset charset, boolean isAsync, boolean isIgnoreBody) {
this.httpConnection = httpConnection;
this.charset = charset;
this.isAsync = isAsync;
this.ignoreBody = isIgnoreBody;
init();
}
/**
* 获取状态码
*
* @return 状态码
*/
public int getStatus() {
return this.status;
}
/**
* 请求是否成功,判断依据为:状态码范围在200~299内。
* @return 是否成功请求
* @since 4.1.9
*/
public boolean isOk() {
return this.status >= 200 && this.status < 300;
}
/**
* 同步<br>
* 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。<br>
* 当调用此方法时,异步状态转为同步状态,此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态,则不进行任何操作。
*
* @return this
* @throws HttpException IO异常
*/
public HttpResponse sync() throws HttpException{
return this.isAsync ? forceSync() : this;
}
// ---------------------------------------------------------------- Http Response Header start
/**
* 获取内容编码
* @return String
*/
public String contentEncoding() {
return header(Header.CONTENT_ENCODING);
}
/**
* @return 是否为gzip压缩过的内容
*/
public boolean isGzip(){
final String contentEncoding = contentEncoding();
return contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip");
}
/**
* 获取本次请求服务器返回的Cookie信息
* @return Cookie字符串
* @since 3.1.1
*/
public String getCookieStr() {
return header(Header.SET_COOKIE);
}
/**
* 获取Cookie
* @return Cookie列表
* @since 3.1.1
*/
public List<HttpCookie> getCookies(){
return HttpRequest.cookieManager.getCookieStore().getCookies();
}
/**
* 获取Cookie
*
* @param name Cookie名
* @return {@link HttpCookie}
* @since 4.1.4
*/
public HttpCookie getCookie(String name) {
List<HttpCookie> cookie = getCookies();
if(null != cookie) {
for (HttpCookie httpCookie : cookie) {
if(httpCookie.getName().equals(name)) {
return httpCookie;
}
}
}
return null;
}
/**
* 获取Cookie值
*
* @param name Cookie名
* @return Cookie值
* @since 4.1.4
*/
public String getCookieValue(String name) {
HttpCookie cookie = getCookie(name);
return (null == cookie) ? null : cookie.getValue();
}
// ---------------------------------------------------------------- Http Response Header end
// ---------------------------------------------------------------- Body start
/**
* 获得服务区响应流<br>
* 异步模式下获取Http原生流,同步模式下获取获取到的在内存中的副本<br>
* 如果想在同步模式下获取流,请先调用{@link #sync()}方法强制同步<br>
* 流获取后处理完毕需关闭此类
*
* @return 响应流
*/
public InputStream bodyStream(){
if(isAsync) {
return this.in;
}
return new ByteArrayInputStream(this.bodyBytes);
}
/**
* 获取响应流字节码<br>
* 此方法会转为同步模式
*
* @return byte[]
*/
public byte[] bodyBytes() {
sync();
return this.bodyBytes;
}
/**
* 获取响应主体
* @return String
* @throws HttpException 包装IO异常
*/
public String body() throws HttpException{
try {
return HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse);
} catch (IOException e) {
throw new HttpException(e);
}
}
/**
* 将响应内容写出到{@link OutputStream}<br>
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流(异步模式)
*
* @param out 写出的流
* @param isCloseOut 是否关闭输出流
* @param streamProgress 进度显示接口,通过实现此接口显示下载进度
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {
if (null == out) {
throw new NullPointerException("[out] is null!");
}
try {
return IoUtil.copyByNIO(bodyStream(), out, IoUtil.DEFAULT_BUFFER_SIZE, streamProgress);
} finally {
IoUtil.close(this);
if (isCloseOut) {
IoUtil.close(out);
}
}
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流(异步模式)
*
* @param destFile 写出到的文件
* @param streamProgress 进度显示接口,通过实现此接口显示下载进度
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(File destFile, StreamProgress streamProgress) {
if (null == destFile) {
throw new NullPointerException("[destFile] is null!");
}
if (destFile.isDirectory()) {
//从头信息中获取文件名
String fileName = getFileNameFromDisposition();
if(StrUtil.isBlank(fileName)) {
final String path = this.httpConnection.getUrl().getPath();
//从路径中获取文件名
fileName = StrUtil.subSuf(path, path.lastIndexOf(\'/\') + 1);
if (StrUtil.isBlank(fileName)) {
//编码后的路径做为文件名
fileName = URLUtil.encodeQuery(path, CharsetUtil.CHARSET_UTF_8);
}
}
destFile = FileUtil.file(destFile, fileName);
}
OutputStream out = null;
try {
out = FileUtil.getOutputStream(destFile);
return writeBody(out, false, streamProgress);
} catch (IORuntimeException e) {
throw new HttpException(e);
} finally {
IoUtil.close(out);
}
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流(异步模式)
*
* @param destFile 写出到的文件
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(File destFile) {
return writeBody(destFile, null);
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流(异步模式)
*
* @param destFilePath 写出到的文件的路径
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(String destFilePath) {
return writeBody(FileUtil.file(destFilePath));
}
// ---------------------------------------------------------------- Body end
@Override
public void close() {
IoUtil.close(this.in);
this.in = null;
//关闭连接
this.httpConnection.disconnect();
}
@Override
public String toString() {
StringBuilder sb = StrUtil.builder();
sb.append("Response Headers: ").append(StrUtil.CRLF);
for (Entry<String, List<String>> entry : this.headers.entrySet()) {
sb.append(" ").append(entry).append(StrUtil.CRLF);
}
sb.append("Response Body: ").append(StrUtil.CRLF);
sb.append(" ").append(this.body()).append(StrUtil.CRLF);
return sb.toString();
}
// ---------------------------------------------------------------- Private method start
/**
* 初始化Http响应<br>
* 初始化包括:
* <pre>
* 1、读取Http状态
* 2、读取头信息
* 3、持有Http流,并不关闭流
* </pre>
*
* @return this
* @throws HttpException IO异常
*/
private HttpResponse init() throws HttpException{
try {
this.status = httpConnection.responseCode();
this.headers = httpConnection.headers();
final Charset charset = httpConnection.getCharset();
this.charsetFromResponse = charset;
if(null != charset) {
this.charset = charset;
}
this.in = (this.status < HttpStatus.HTTP_BAD_REQUEST) ? httpConnection.getInputStream() : httpConnection.getErrorStream();
} catch (IOException e) {
if(e instanceof FileNotFoundException){
//服务器无返回内容,忽略之
}else{
throw new HttpException(e);
}
}
if(null == this.in) {
//在一些情况下,返回的流为null,此时提供状态码说明
this.in = new ByteArrayInputStream(StrUtil.format("Error request, response status: {}", this.status).getBytes());
} else if(isGzip() && false == (in instanceof GZIPInputStream)){
try {
in = new GZIPInputStream(in);
} catch (IOException e) {
//在类似于Head等方法中无body返回,此时GZIPInputStream构造会出现错误,在此忽略此错误读取普通数据
//ignore
}
}
//同步情况下强制同步
return this.isAsync ? this : forceSync();
}
/**
* 读取主体,忽略EOFException异常
* @param in 输入流
* @return 自身
* @throws IORuntimeException IO异常
*/
private void readBody(InputStream in) throws IORuntimeException{
if(ignoreBody) {
return;
}
int contentLength = Convert.toInt(header(Header.CONTENT_LENGTH), 0);
final FastByteArrayOutputStream out = contentLength > 0 ? new FastByteArrayOutputStream(contentLength) : new FastByteArrayOutputStream();
try {
IoUtil.copy(in, out);
} catch (IORuntimeException e) {
if(e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF")) {
//忽略读取HTTP流中的EOF错误
}else {
throw e;
}
}
this.bodyBytes = out.toByteArray();
}
/**
* 强制同步,用于初始化<br>
* 强制同步后变化如下:
* <pre>
* 1、读取body内容到内存
* 2、异步状态设为false(变为同步状态)
* 3、关闭Http流
* 4、断开与服务器连接
* </pre>
*
* @return this
*/
private HttpResponse forceSync() {
//非同步状态转为同步状态
try {
this.readBody(this.in);
} catch (IORuntimeException e) {
if(e.getCause() instanceof FileNotFoundException){
//服务器无返回内容,忽略之
}else{
throw new HttpException(e);
}
}finally {
if(this.isAsync) {
this.isAsync = false;
}
this.close();
}
return this;
}
/**
* 从Content-Disposition头中获取文件名
* @return 文件名,empty表示无
*/
private String getFileNameFromDisposition() {
String fileName = null;
final String desposition = header(Header.CONTENT_DISPOSITION);
if(StrUtil.isNotBlank(desposition)) {
fileName = ReUtil.get("filename=\"(.*?)\"", desposition, 1);
if(StrUtil.isBlank(fileName)) {
fileName = StrUtil.subAfter(desposition, "filename=", true);
}
}
return fileName;
}
// ---------------------------------------------------------------- Private method end
}