This commit is contained in:
Looly 2022-10-27 03:21:40 +08:00
parent 11f47e5b0f
commit 35a8f5e281
30 changed files with 1039 additions and 925 deletions

View File

@ -133,6 +133,23 @@ public class FileNameUtil {
return extName(fileName); return extName(fileName);
} }
/**
* 增加临时扩展名
*
* @param fileName 文件名
* @param suffix 临时扩展名如果为空使用`.temp`
* @return 临时文件名
*/
public static String addTempSuffix(final String fileName, String suffix){
if (StrUtil.isBlank(suffix)) {
suffix = ".temp";
} else {
suffix = StrUtil.addPrefixIfNot(suffix, StrUtil.DOT);
}
return fileName + suffix;
}
/** /**
* 返回主文件名 * 返回主文件名
* *

View File

@ -1,98 +0,0 @@
package cn.hutool.http;
import cn.hutool.http.client.engine.jdk.HttpInterceptor;
import cn.hutool.http.client.engine.jdk.HttpRequest;
import cn.hutool.http.client.engine.jdk.HttpResponse;
/**
* 全局的拦截器<br>
* 包括请求拦截器和响应拦截器
*
* @author looly
* @since 5.8.0
*/
public enum GlobalInterceptor {
INSTANCE;
private final HttpInterceptor.Chain<HttpRequest> requestInterceptors = new HttpInterceptor.Chain<>();
private final HttpInterceptor.Chain<HttpResponse> responseInterceptors = new HttpInterceptor.Chain<>();
/**
* 设置拦截器用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
*/
synchronized public GlobalInterceptor addRequestInterceptor(final HttpInterceptor<HttpRequest> interceptor) {
this.requestInterceptors.addChain(interceptor);
return this;
}
/**
* 设置拦截器用于在响应读取后完成编辑或读取
*
* @param interceptor 拦截器实现
* @return this
*/
synchronized public GlobalInterceptor addResponseInterceptor(final HttpInterceptor<HttpResponse> interceptor) {
this.responseInterceptors.addChain(interceptor);
return this;
}
/**
* 清空请求和响应拦截器
*
* @return this
*/
public GlobalInterceptor clear() {
clearRequest();
clearResponse();
return this;
}
/**
* 清空请求拦截器
*
* @return this
*/
synchronized public GlobalInterceptor clearRequest() {
requestInterceptors.clear();
return this;
}
/**
* 清空响应拦截器
*
* @return this
*/
synchronized public GlobalInterceptor clearResponse() {
responseInterceptors.clear();
return this;
}
/**
* 复制请求过滤器列表
*
* @return {@link HttpInterceptor.Chain}
*/
HttpInterceptor.Chain<HttpRequest> getCopiedRequestInterceptor() {
final HttpInterceptor.Chain<HttpRequest> copied = new HttpInterceptor.Chain<>();
for (final HttpInterceptor<HttpRequest> interceptor : this.requestInterceptors) {
copied.addChain(interceptor);
}
return copied;
}
/**
* 复制响应过滤器列表
*
* @return {@link HttpInterceptor.Chain}
*/
HttpInterceptor.Chain<HttpResponse> getCopiedResponseInterceptor() {
final HttpInterceptor.Chain<HttpResponse> copied = new HttpInterceptor.Chain<>();
for (final HttpInterceptor<HttpResponse> interceptor : this.responseInterceptors) {
copied.addChain(interceptor);
}
return copied;
}
}

View File

@ -2,9 +2,6 @@ package cn.hutool.http;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.net.ssl.SSLUtil; import cn.hutool.core.net.ssl.SSLUtil;
import cn.hutool.http.client.engine.jdk.HttpInterceptor;
import cn.hutool.http.client.engine.jdk.HttpRequest;
import cn.hutool.http.client.engine.jdk.HttpResponse;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
@ -79,15 +76,6 @@ public class HttpConfig {
*/ */
boolean decodeUrl = HttpGlobalConfig.isDecodeUrl(); boolean decodeUrl = HttpGlobalConfig.isDecodeUrl();
/**
* 请求前的拦截器用于在请求前重新编辑请求
*/
public final HttpInterceptor.Chain<HttpRequest> requestInterceptors = GlobalInterceptor.INSTANCE.getCopiedRequestInterceptor();
/**
* 响应后的拦截器用于在响应后处理逻辑
*/
public final HttpInterceptor.Chain<HttpResponse> responseInterceptors = GlobalInterceptor.INSTANCE.getCopiedResponseInterceptor();
/** /**
* 重定向时是否使用拦截器 * 重定向时是否使用拦截器
*/ */
@ -125,7 +113,7 @@ public class HttpConfig {
} }
/** /**
* 设置连接超时单位毫秒 * 设置读取超时单位毫秒
* *
* @param milliseconds 超时毫秒数 * @param milliseconds 超时毫秒数
* @return this * @return this
@ -268,28 +256,6 @@ public class HttpConfig {
return this; return this;
} }
/**
* 设置拦截器用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
*/
public HttpConfig addRequestInterceptor(final HttpInterceptor<HttpRequest> interceptor) {
this.requestInterceptors.addChain(interceptor);
return this;
}
/**
* 设置拦截器用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
*/
public HttpConfig addResponseInterceptor(final HttpInterceptor<HttpResponse> interceptor) {
this.responseInterceptors.addChain(interceptor);
return this;
}
/** /**
* 重定向时是否使用拦截器 * 重定向时是否使用拦截器
* *

View File

@ -124,7 +124,7 @@ public class HttpUtil {
*/ */
@SuppressWarnings("resource") @SuppressWarnings("resource")
public static String get(final String urlString, final Charset customCharset) { public static String get(final String urlString, final Charset customCharset) {
return HttpRequest.get(urlString).charset(customCharset).execute().body(); return HttpRequest.get(urlString).charset(customCharset).execute().bodyStr();
} }
/** /**
@ -147,7 +147,7 @@ public class HttpUtil {
*/ */
@SuppressWarnings("resource") @SuppressWarnings("resource")
public static String get(final String urlString, final int timeout) { public static String get(final String urlString, final int timeout) {
return HttpRequest.get(urlString).timeout(timeout).execute().body(); return HttpRequest.get(urlString).timeout(timeout).execute().bodyStr();
} }
/** /**
@ -159,7 +159,7 @@ public class HttpUtil {
*/ */
@SuppressWarnings("resource") @SuppressWarnings("resource")
public static String get(final String urlString, final Map<String, Object> paramMap) { public static String get(final String urlString, final Map<String, Object> paramMap) {
return HttpRequest.get(urlString).form(paramMap).execute().body(); return HttpRequest.get(urlString).form(paramMap).execute().bodyStr();
} }
/** /**
@ -173,7 +173,7 @@ public class HttpUtil {
*/ */
@SuppressWarnings("resource") @SuppressWarnings("resource")
public static String get(final String urlString, final Map<String, Object> paramMap, final int timeout) { public static String get(final String urlString, final Map<String, Object> paramMap, final int timeout) {
return HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().body(); return HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().bodyStr();
} }
/** /**
@ -198,7 +198,7 @@ public class HttpUtil {
*/ */
@SuppressWarnings("resource") @SuppressWarnings("resource")
public static String post(final String urlString, final Map<String, Object> paramMap, final int timeout) { public static String post(final String urlString, final Map<String, Object> paramMap, final int timeout) {
return HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().body(); return HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().bodyStr();
} }
/** /**
@ -235,7 +235,7 @@ public class HttpUtil {
*/ */
@SuppressWarnings("resource") @SuppressWarnings("resource")
public static String post(final String urlString, final String body, final int timeout) { public static String post(final String urlString, final String body, final int timeout) {
return HttpRequest.post(urlString).timeout(timeout).body(body).execute().body(); return HttpRequest.post(urlString).timeout(timeout).body(body).execute().bodyStr();
} }
// ---------------------------------------------------------------------------------------- download // ---------------------------------------------------------------------------------------- download

View File

@ -0,0 +1,241 @@
package cn.hutool.http.client;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.net.ssl.SSLUtil;
import cn.hutool.http.HttpGlobalConfig;
import cn.hutool.http.ssl.DefaultSSLInfo;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetSocketAddress;
import java.net.Proxy;
/**
* Http客户端配置
*
* @author looly
*/
public class ClientConfig {
/**
* 创建新的 ClientConfig
*
* @return ClientConfig
*/
public static ClientConfig of() {
return new ClientConfig();
}
/**
* 默认连接超时
*/
private int connectionTimeout;
/**
* 默认读取超时
*/
private int readTimeout;
/**
* HostnameVerifier用于HTTPS安全连接
*/
private HostnameVerifier hostnameVerifier;
/**
* SSLSocketFactory用于HTTPS安全连接
*/
private SSLSocketFactory socketFactory;
/**
* 是否禁用缓存
*/
public boolean disableCache;
/**
* 代理
*/
public Proxy proxy;
/**
* 构造
*/
public ClientConfig() {
connectionTimeout = HttpGlobalConfig.getTimeout();
readTimeout = HttpGlobalConfig.getTimeout();
hostnameVerifier = DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER;
socketFactory = DefaultSSLInfo.DEFAULT_SSF;
}
/**
* 设置超时单位毫秒<br>
* 超时包括
*
* <pre>
* 1. 连接超时
* 2. 读取响应超时
* </pre>
*
* @param milliseconds 超时毫秒数
* @return this
* @see #setConnectionTimeout(int)
* @see #setReadTimeout(int)
*/
public ClientConfig setTimeout(final int milliseconds) {
setConnectionTimeout(milliseconds);
setReadTimeout(milliseconds);
return this;
}
/**
* 获取连接超时单位毫秒
*
* @return 连接超时单位毫秒
*/
public int getConnectionTimeout() {
return connectionTimeout;
}
/**
* 设置连接超时单位毫秒
*
* @param connectionTimeout 超时毫秒数
* @return this
*/
public ClientConfig setConnectionTimeout(final int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
return this;
}
/**
* 获取读取超时单位毫秒
*
* @return 读取超时单位毫秒
*/
public int getReadTimeout() {
return readTimeout;
}
/**
* 设置读取超时单位毫秒
*
* @param readTimeout 读取超时单位毫秒
* @return this
*/
public ClientConfig setReadTimeout(final int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
/**
* 获取域名验证器
*
* @return 域名验证器
*/
public HostnameVerifier getHostnameVerifier() {
return hostnameVerifier;
}
/**
* 设置域名验证器<br>
* 只针对HTTPS请求如果不设置不做验证所有域名被信任
*
* @param hostnameVerifier HostnameVerifier
* @return this
*/
public ClientConfig setHostnameVerifier(final HostnameVerifier hostnameVerifier) {
// 验证域
this.hostnameVerifier = hostnameVerifier;
return this;
}
/**
* 获取SSLSocketFactory
*
* @return SSLSocketFactory
*/
public SSLSocketFactory getSocketFactory() {
return socketFactory;
}
/**
* 设置SSLSocketFactory<br>
* 只针对HTTPS请求如果不设置使用默认的SSLSocketFactory<br>
* 默认SSLSocketFactory为SSLSocketFactoryBuilder.create().build();
*
* @param ssf SSLScketFactory
* @return this
*/
public ClientConfig setSocketFactory(final SSLSocketFactory ssf) {
this.socketFactory = ssf;
return this;
}
/**
* 设置HTTPS安全连接协议只针对HTTPS请求可以使用的协议包括<br>
* 此方法调用后{@link #setSocketFactory(SSLSocketFactory)} 将被覆盖
*
* <pre>
* 1. TLSv1.2
* 2. TLSv1.1
* 3. SSLv3
* ...
* </pre>
*
* @param protocol 协议
* @return this
* @see SSLUtil#createSSLContext(String)
* @see #setSocketFactory(SSLSocketFactory)
*/
public ClientConfig setSSLProtocol(final String protocol) {
Assert.notBlank(protocol, "protocol must be not blank!");
setSocketFactory(SSLUtil.createSSLContext(protocol).getSocketFactory());
return this;
}
/**
* 是否禁用缓存
*
* @return 是否禁用缓存
*/
public boolean isDisableCache() {
return disableCache;
}
/**
* 设置是否禁用缓存
*
* @param disableCache 是否禁用缓存
*/
public void setDisableCache(final boolean disableCache) {
this.disableCache = disableCache;
}
/**
* 获取代理
*
* @return 代理
*/
public Proxy getProxy() {
return proxy;
}
/**
* 设置Http代理
*
* @param host 代理 主机
* @param port 代理 端口
* @return this
*/
public ClientConfig setHttpProxy(final String host, final int port) {
final Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress(host, port));
return setProxy(proxy);
}
/**
* 设置代理
*
* @param proxy 代理 {@link Proxy}
* @return this
*/
public ClientConfig setProxy(final Proxy proxy) {
this.proxy = proxy;
return this;
}
}

View File

@ -1,6 +1,7 @@
package cn.hutool.http.client; package cn.hutool.http.client;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrUtil; import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.http.meta.Header; import cn.hutool.http.meta.Header;
@ -11,12 +12,12 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* HTTP请求头的存储和相关方法 * HTTP请求头的存储和读取相关方法
* *
* @param <T> 返回对象类型方便链式编程 * @param <T> 返回对象类型方便链式编程
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public interface Headers<T extends Headers<T>> { public interface HeaderOperation<T extends HeaderOperation<T>> {
// region ----------------------------------------------------------- headers // region ----------------------------------------------------------- headers
@ -100,6 +101,27 @@ public interface Headers<T extends Headers<T>> {
return header(name, value, true); return header(name, value, true);
} }
/**
* 设置请求头<br>
* 不覆盖原有请求头
*
* @param headerMap 请求头
* @param isOverride 是否覆盖
* @return this
*/
default T header(final Map<String, List<String>> headerMap, final boolean isOverride) {
if (MapUtil.isNotEmpty(headerMap)) {
String name;
for (final Map.Entry<String, List<String>> entry : headerMap.entrySet()) {
name = entry.getKey();
for (final String value : entry.getValue()) {
this.header(name, StrUtil.emptyIfNull(value), isOverride);
}
}
}
return (T) this;
}
/** /**
* 设置contentType * 设置contentType
* *

View File

@ -4,10 +4,11 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.HttpGlobalConfig; import cn.hutool.http.HttpGlobalConfig;
import cn.hutool.http.client.body.RequestBody; import cn.hutool.http.client.body.HttpBody;
import cn.hutool.http.meta.Header; import cn.hutool.http.meta.Header;
import cn.hutool.http.meta.Method; import cn.hutool.http.meta.Method;
@ -23,7 +24,7 @@ import java.util.Map;
* @author looly * @author looly
* @since 6.0.0 * @since 6.0.0
*/ */
public class Request implements Headers<Request> { public class Request implements HeaderOperation<Request> {
/** /**
* 构建一个HTTP请求<br> * 构建一个HTTP请求<br>
@ -70,7 +71,7 @@ public class Request implements Headers<Request> {
/** /**
* 请求方法 * 请求方法
*/ */
private Method method = Method.GET; private Method method;
/** /**
* 请求的URL * 请求的URL
*/ */
@ -78,8 +79,24 @@ public class Request implements Headers<Request> {
/** /**
* 存储头信息 * 存储头信息
*/ */
private final Map<String, List<String>> headers = new HashMap<>(); private final Map<String, List<String>> headers;
private RequestBody body; /**
* 请求体
*/
private HttpBody body;
/**
* 最大重定向次数
*/
private int maxRedirectCount;
/**
* 默认构造
*/
public Request() {
method = Method.GET;
headers = new HashMap<>();
maxRedirectCount = HttpGlobalConfig.getMaxRedirectCount();
}
/** /**
* 获取Http请求方法 * 获取Http请求方法
@ -195,7 +212,7 @@ public class Request implements Headers<Request> {
* *
* @return this * @return this
*/ */
public RequestBody body() { public HttpBody body() {
return this.body; return this.body;
} }
@ -205,8 +222,35 @@ public class Request implements Headers<Request> {
* @param body 请求体可以是文本表单byte[] Multipart * @param body 请求体可以是文本表单byte[] Multipart
* @return this * @return this
*/ */
public Request body(final RequestBody body) { public Request body(final HttpBody body) {
this.body = body; this.body = body;
// 根据内容赋值默认Content-Type
if (StrUtil.isBlank(header(Header.CONTENT_TYPE))) {
header(Header.CONTENT_TYPE, body.getContentType(), true);
}
return this;
}
/**
* 获取最大重定向请求次数
*
* @return 最大重定向请求次数
*/
public int maxRedirectCount() {
return maxRedirectCount;
}
/**
* 设置最大重定向次数<br>
* 如果次数小于1则表示不重定向大于等于1表示打开重定向
*
* @param maxRedirectCount 最大重定向次数
* @return this
*/
public Request setMaxRedirectCount(final int maxRedirectCount) {
this.maxRedirectCount = Math.max(maxRedirectCount, 0);
return this; return this;
} }
} }

View File

@ -1,10 +1,12 @@
package cn.hutool.http.client; package cn.hutool.http.client;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.StrUtil; import cn.hutool.core.text.StrUtil;
import cn.hutool.http.meta.Header;
import cn.hutool.http.HttpException; import cn.hutool.http.HttpException;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.http.client.body.ResponseBody;
import cn.hutool.http.meta.Header;
import java.io.Closeable; import java.io.Closeable;
import java.io.InputStream; import java.io.InputStream;
@ -49,14 +51,32 @@ public interface Response extends Closeable {
*/ */
InputStream bodyStream(); InputStream bodyStream();
/**
* 获取响应体包含服务端返回的内容和Content-Type信息
* @return {@link ResponseBody}
*/
default ResponseBody body(){
return new ResponseBody(this, true);
}
/** /**
* 获取响应主体 * 获取响应主体
* *
* @return String * @return String
* @throws HttpException 包装IO异常 * @throws HttpException 包装IO异常
*/ */
default String body() throws HttpException { default String bodyStr() throws HttpException {
return HttpUtil.getString(bodyStream(), charset(), true); return HttpUtil.getString(bodyBytes(), charset(), true);
}
/**
* 获取响应流字节码<br>
* 此方法会转为同步模式
*
* @return byte[]
*/
default byte[] bodyBytes() {
return IoUtil.readBytes(bodyStream());
} }
/** /**

View File

@ -22,7 +22,7 @@ import java.util.Map;
* @author looly * @author looly
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public abstract class FormBody<T extends FormBody<T>> implements RequestBody { public abstract class FormBody<T extends FormBody<T>> implements HttpBody {
/** /**
* 存储表单数据 * 存储表单数据
*/ */

View File

@ -9,7 +9,7 @@ import java.io.OutputStream;
/** /**
* 定义请求体接口 * 定义请求体接口
*/ */
public interface RequestBody { public interface HttpBody {
/** /**
* 写出数据不关闭流 * 写出数据不关闭流
@ -18,6 +18,13 @@ public interface RequestBody {
*/ */
void write(OutputStream out); void write(OutputStream out);
/**
* 获取Content-Type
*
* @return Content-Type值
*/
String getContentType();
/** /**
* 写出并关闭{@link OutputStream} * 写出并关闭{@link OutputStream}
* *

View File

@ -59,6 +59,7 @@ public class MultipartBody extends FormBody<MultipartBody> {
public MultipartBody(final Map<String, Object> form, final Charset charset, final String boundary) { public MultipartBody(final Map<String, Object> form, final Charset charset, final String boundary) {
super(form, charset); super(form, charset);
this.boundary = boundary; this.boundary = boundary;
} }
/** /**
@ -66,6 +67,7 @@ public class MultipartBody extends FormBody<MultipartBody> {
* *
* @return Multipart的Content-Type类型 * @return Multipart的Content-Type类型
*/ */
@Override
public String getContentType() { public String getContentType() {
return CONTENT_TYPE_MULTIPART_PREFIX + boundary; return CONTENT_TYPE_MULTIPART_PREFIX + boundary;
} }

View File

@ -1,5 +1,6 @@
package cn.hutool.http.client.body; package cn.hutool.http.client.body;
import cn.hutool.core.io.resource.HttpResource;
import cn.hutool.core.io.resource.Resource; import cn.hutool.core.io.resource.Resource;
import java.io.InputStream; import java.io.InputStream;
@ -11,9 +12,9 @@ import java.io.OutputStream;
* @author looly * @author looly
* @since 6.0.0 * @since 6.0.0
*/ */
public class ResourceBody implements RequestBody { public class ResourceBody implements HttpBody {
private final Resource resource; private final HttpResource resource;
/** /**
* 创建 Http request body * 创建 Http request body
@ -21,7 +22,7 @@ public class ResourceBody implements RequestBody {
* @param resource body内容 * @param resource body内容
* @return BytesBody * @return BytesBody
*/ */
public static ResourceBody of(final Resource resource) { public static ResourceBody of(final HttpResource resource) {
return new ResourceBody(resource); return new ResourceBody(resource);
} }
@ -30,7 +31,7 @@ public class ResourceBody implements RequestBody {
* *
* @param resource Body内容 * @param resource Body内容
*/ */
public ResourceBody(final Resource resource) { public ResourceBody(final HttpResource resource) {
this.resource = resource; this.resource = resource;
} }
@ -52,4 +53,9 @@ public class ResourceBody implements RequestBody {
public InputStream getStream() { public InputStream getStream() {
return resource.getStream(); return resource.getStream();
} }
@Override
public String getContentType() {
return this.resource.getContentType();
}
} }

View File

@ -0,0 +1,243 @@
package cn.hutool.http.client.body;
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.io.file.FileNameUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.regex.ReUtil;
import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.HttpException;
import cn.hutool.http.client.Response;
import cn.hutool.http.meta.Header;
import java.io.EOFException;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 响应体部分封装
*
* @author looly
*/
public class ResponseBody implements HttpBody {
private final Response response;
/**
* 是否忽略响应读取时可能的EOF异常<br>
* 在Http协议中对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束<br>
* 如果服务端未遵循这个规范或响应没有正常结束会报EOF异常此选项用于是否忽略这个异常
*/
private final boolean isIgnoreEOFError;
/**
* 构造
*
* @param response 响应体
* @param isIgnoreEOFError 是否忽略EOF错误
*/
public ResponseBody(final Response response, final boolean isIgnoreEOFError) {
this.response = response;
this.isIgnoreEOFError = isIgnoreEOFError;
}
@Override
public String getContentType() {
return response.header(Header.CONTENT_TYPE);
}
@Override
public InputStream getStream() {
return response.bodyStream();
}
@Override
public void write(final OutputStream out) {
write(out, false, null);
}
/**
* 将响应内容写出到{@link OutputStream}<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param out 写出的流
* @param isCloseOut 是否关闭输出流
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出bytes数
* @since 3.3.2
*/
public long write(final OutputStream out, final boolean isCloseOut, final StreamProgress streamProgress) {
Assert.notNull(out, "[out] must be not null!");
final long contentLength = response.contentLength();
try {
return copyBody(getStream(), out, contentLength, streamProgress, isIgnoreEOFError);
} finally {
if (isCloseOut) {
IoUtil.close(out);
}
}
}
/**
* 将响应内容写出到文件
*
* @param targetFileOrDir 写出到的文件或目录的路径
* @return 写出的文件
*/
public File write(final String targetFileOrDir) {
return write(FileUtil.file(targetFileOrDir));
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param targetFileOrDir 写出到的文件或目录
* @return 写出的文件
*/
public File write(final File targetFileOrDir) {
return write(targetFileOrDir, null);
}
/**
* 将响应内容写出到文件-避免未完成的文件
* 来自<a href="https://gitee.com/dromara/hutool/pulls/407">https://gitee.com/dromara/hutool/pulls/407</a><br>
* 此方法原理是先在目标文件同级目录下创建临时文件下载之等下载完毕后重命名避免因下载错误导致的文件不完整
*
* @param targetFileOrDir 写出到的文件或目录
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出的文件对象
*/
public File write(final File targetFileOrDir, final StreamProgress streamProgress) {
return write(targetFileOrDir, null, streamProgress);
}
/**
* 将响应内容写出到文件-避免未完成的文件
* 来自<a href="https://gitee.com/dromara/hutool/pulls/407">https://gitee.com/dromara/hutool/pulls/407</a><br>
* 此方法原理是先在目标文件同级目录下创建临时文件下载之等下载完毕后重命名避免因下载错误导致的文件不完整
*
* @param targetFileOrDir 写出到的文件或目录
* @param tempFileSuffix 临时文件后缀默认".temp"
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出的文件对象
* @since 5.7.12
*/
public File write(final File targetFileOrDir, final String tempFileSuffix, final StreamProgress streamProgress) {
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
File outFile = getTargetFile(targetFileOrDir, null);
// 目标文件真实名称
final String fileName = outFile.getName();
// 临时文件
outFile = new File(outFile.getParentFile(), FileNameUtil.addTempSuffix(fileName, tempFileSuffix));
try {
outFile = writeDirect(outFile, null, streamProgress);
// 重命名下载好的临时文件
return FileUtil.rename(outFile, fileName, true);
} catch (final Throwable e) {
// 异常则删除临时文件
FileUtil.del(outFile);
throw new HttpException(e);
}
}
/**
* 将响应内容直接写出到文件目标为目录则从Content-Disposition中获取文件名
*
* @param targetFileOrDir 写出到的文件
* @param customParamName 自定义的Content-Disposition中文件名的参数名
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出的文件
*/
public File writeDirect(final File targetFileOrDir, final String customParamName, final StreamProgress streamProgress) {
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
final File outFile = getTargetFile(targetFileOrDir, customParamName);
write(FileUtil.getOutputStream(outFile), true, streamProgress);
return outFile;
}
// region ---------------------------------------------------------------------------- Private Methods
/**
* 从响应头补全下载文件名返回补全名称后的文件
*
* @param targetFileOrDir 目标文件夹或者目标文件
* @param customParamName 自定义的参数名称如果传入{@code null}默认使用"filename"
* @return File 保存的文件
* @since 5.4.1
*/
private File getTargetFile(final File targetFileOrDir, final String customParamName) {
if (false == targetFileOrDir.isDirectory()) {
// 非目录直接返回
return targetFileOrDir;
}
// 从头信息中获取文件名
final String fileName = getFileNameFromDisposition(ObjUtil.defaultIfNull(customParamName, "filename"));
if (StrUtil.isBlank(fileName)) {
throw new HttpException("Can`t get file name from [Content-Disposition]!");
}
return FileUtil.file(targetFileOrDir, fileName);
}
/**
* 从Content-Disposition头中获取文件名
*
* @param paramName 文件名的参数名
* @return 文件名empty表示无
* @since 5.8.10
*/
private String getFileNameFromDisposition(final String paramName) {
String fileName = null;
final String disposition = response.header(Header.CONTENT_DISPOSITION);
if (StrUtil.isNotBlank(disposition)) {
fileName = ReUtil.get(paramName + "=\"(.*?)\"", disposition, 1);
if (StrUtil.isBlank(fileName)) {
fileName = StrUtil.subAfter(disposition, paramName + "=", true);
}
}
return fileName;
}
/**
* 将响应内容写出到{@link OutputStream}<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param in 输入流
* @param out 写出的流
* @param contentLength 总长度-1表示未知
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @param isIgnoreEOFError 是否忽略响应读取时可能的EOF异常
* @return 拷贝长度
*/
private static long copyBody(final InputStream in, final OutputStream out, final long contentLength, final StreamProgress streamProgress, final boolean isIgnoreEOFError) {
if (null == out) {
throw new NullPointerException("[out] is null!");
}
long copyLength = -1;
try {
copyLength = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE, contentLength, streamProgress);
} catch (final IORuntimeException e) {
//noinspection StatementWithEmptyBody
if (isIgnoreEOFError
&& (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF"))) {
// 忽略读取HTTP流中的EOF错误
} else {
throw e;
}
}
return copyLength;
}
// endregion ---------------------------------------------------------------------------- Private Methods
}

View File

@ -1,5 +1,6 @@
package cn.hutool.http.client.body; package cn.hutool.http.client.body;
import cn.hutool.core.io.resource.HttpResource;
import cn.hutool.core.io.resource.StringResource; import cn.hutool.core.io.resource.StringResource;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
@ -30,6 +31,6 @@ public class StringBody extends ResourceBody {
* @param charset 自定义编码 * @param charset 自定义编码
*/ */
public StringBody(final String body, final String contentType, final Charset charset) { public StringBody(final String body, final String contentType, final Charset charset) {
super(new StringResource(body, contentType, charset)); super(new HttpResource(new StringResource(body, contentType, charset), contentType));
} }
} }

View File

@ -3,6 +3,7 @@ package cn.hutool.http.client.body;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.net.url.UrlQuery; import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.text.StrUtil; import cn.hutool.core.text.StrUtil;
import cn.hutool.http.meta.ContentType;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -42,4 +43,9 @@ public class UrlEncodedFormBody extends FormBody<UrlEncodedFormBody> {
final byte[] bytes = StrUtil.bytes(UrlQuery.of(form, true).build(charset), charset); final byte[] bytes = StrUtil.bytes(UrlQuery.of(form, true).build(charset), charset);
IoUtil.write(out, false, bytes); IoUtil.write(out, false, bytes);
} }
@Override
public String getContentType() {
return ContentType.FORM_URLENCODED.toString(charset);
}
} }

View File

@ -1,7 +1,7 @@
package cn.hutool.http.client.engine.httpclient4; package cn.hutool.http.client.engine.httpclient4;
import cn.hutool.http.client.body.BytesBody; import cn.hutool.http.client.body.BytesBody;
import cn.hutool.http.client.body.RequestBody; import cn.hutool.http.client.body.HttpBody;
import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.entity.AbstractHttpEntity;
import java.io.InputStream; import java.io.InputStream;
@ -9,14 +9,14 @@ import java.io.OutputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
/** /**
* {@link RequestBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象 * {@link HttpBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象
* *
* @author looly * @author looly
* @since 6.0.0 * @since 6.0.0
*/ */
public class HttpClient4BodyEntity extends AbstractHttpEntity { public class HttpClient4BodyEntity extends AbstractHttpEntity {
private final RequestBody body; private final HttpBody body;
/** /**
* 构造 * 构造
@ -24,9 +24,9 @@ public class HttpClient4BodyEntity extends AbstractHttpEntity {
* @param contentType Content-Type类型 * @param contentType Content-Type类型
* @param charset 自定义请求编码 * @param charset 自定义请求编码
* @param chunked 是否块模式传输 * @param chunked 是否块模式传输
* @param body {@link RequestBody} * @param body {@link HttpBody}
*/ */
public HttpClient4BodyEntity(final String contentType, final Charset charset, final boolean chunked, final RequestBody body) { public HttpClient4BodyEntity(final String contentType, final Charset charset, final boolean chunked, final HttpBody body) {
super(); super();
setContentType(contentType); setContentType(contentType);
setContentEncoding(null == charset ? null : charset.name()); setContentEncoding(null == charset ? null : charset.name());

View File

@ -7,7 +7,7 @@ import cn.hutool.http.HttpException;
import cn.hutool.http.client.ClientEngine; import cn.hutool.http.client.ClientEngine;
import cn.hutool.http.client.Request; import cn.hutool.http.client.Request;
import cn.hutool.http.client.Response; import cn.hutool.http.client.Response;
import cn.hutool.http.client.body.RequestBody; import cn.hutool.http.client.body.HttpBody;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
@ -87,7 +87,7 @@ public class HttpClient4Engine implements ClientEngine {
request.setHeaders(toHeaderList(message.headers()).toArray(new Header[0])); request.setHeaders(toHeaderList(message.headers()).toArray(new Header[0]));
// 填充自定义消息体 // 填充自定义消息体
final RequestBody body = message.body(); final HttpBody body = message.body();
request.setEntity(new HttpClient4BodyEntity( request.setEntity(new HttpClient4BodyEntity(
// 用户自定义的内容类型 // 用户自定义的内容类型
message.header(cn.hutool.http.meta.Header.CONTENT_TYPE), message.header(cn.hutool.http.meta.Header.CONTENT_TYPE),

View File

@ -81,7 +81,7 @@ public class HttpClient4Response implements Response {
} }
@Override @Override
public String body() throws HttpException { public String bodyStr() throws HttpException {
try { try {
return EntityUtils.toString(rawRes.getEntity(), charset()); return EntityUtils.toString(rawRes.getEntity(), charset());
} catch (final IOException e) { } catch (final IOException e) {

View File

@ -1,7 +1,7 @@
package cn.hutool.http.client.engine.httpclient5; package cn.hutool.http.client.engine.httpclient5;
import cn.hutool.http.client.body.BytesBody; import cn.hutool.http.client.body.BytesBody;
import cn.hutool.http.client.body.RequestBody; import cn.hutool.http.client.body.HttpBody;
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
import java.io.IOException; import java.io.IOException;
@ -10,14 +10,14 @@ import java.io.OutputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
/** /**
* {@link RequestBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象 * {@link HttpBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象
* *
* @author looly * @author looly
* @since 6.0.0 * @since 6.0.0
*/ */
public class HttpClient5BodyEntity extends AbstractHttpEntity { public class HttpClient5BodyEntity extends AbstractHttpEntity {
private final RequestBody body; private final HttpBody body;
/** /**
* 构造 * 构造
@ -25,9 +25,9 @@ public class HttpClient5BodyEntity extends AbstractHttpEntity {
* @param contentType Content-Type类型 * @param contentType Content-Type类型
* @param charset 自定义请求编码 * @param charset 自定义请求编码
* @param chunked 是否块模式传输 * @param chunked 是否块模式传输
* @param body {@link RequestBody} * @param body {@link HttpBody}
*/ */
public HttpClient5BodyEntity(final String contentType, final Charset charset, final boolean chunked, final RequestBody body) { public HttpClient5BodyEntity(final String contentType, final Charset charset, final boolean chunked, final HttpBody body) {
super(contentType, null == charset ? null : charset.name(), chunked); super(contentType, null == charset ? null : charset.name(), chunked);
this.body = body; this.body = body;
} }

View File

@ -7,7 +7,7 @@ import cn.hutool.http.HttpException;
import cn.hutool.http.client.ClientEngine; import cn.hutool.http.client.ClientEngine;
import cn.hutool.http.client.Request; import cn.hutool.http.client.Request;
import cn.hutool.http.client.Response; import cn.hutool.http.client.Response;
import cn.hutool.http.client.body.RequestBody; import cn.hutool.http.client.body.HttpBody;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
@ -83,7 +83,7 @@ public class HttpClient5Engine implements ClientEngine {
request.setHeaders(toHeaderList(message.headers()).toArray(new Header[0])); request.setHeaders(toHeaderList(message.headers()).toArray(new Header[0]));
// 填充自定义消息体 // 填充自定义消息体
final RequestBody body = message.body(); final HttpBody body = message.body();
request.setEntity(new HttpClient5BodyEntity( request.setEntity(new HttpClient5BodyEntity(
// 用户自定义的内容类型 // 用户自定义的内容类型
message.header(cn.hutool.http.meta.Header.CONTENT_TYPE), message.header(cn.hutool.http.meta.Header.CONTENT_TYPE),

View File

@ -81,7 +81,7 @@ public class HttpClient5Response implements Response {
} }
@Override @Override
public String body() throws HttpException { public String bodyStr() throws HttpException {
try { try {
return EntityUtils.toString(rawRes.getEntity(), charset()); return EntityUtils.toString(rawRes.getEntity(), charset());
} catch (final IOException e) { } catch (final IOException e) {

View File

@ -6,7 +6,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrUtil; import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.meta.Header; import cn.hutool.http.meta.Header;
import cn.hutool.http.client.Headers; import cn.hutool.http.client.HeaderOperation;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
@ -23,7 +23,7 @@ import java.util.Map.Entry;
* @author Looly * @author Looly
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public abstract class HttpBase<T extends HttpBase<T>> implements Headers<T> { public abstract class HttpBase<T extends HttpBase<T>> implements HeaderOperation<T> {
/** /**
* 默认的请求编码URL的encodedecode编码 * 默认的请求编码URL的encodedecode编码
@ -253,10 +253,19 @@ public abstract class HttpBase<T extends HttpBase<T>> implements Headers<T> {
* *
* @return 字符集 * @return 字符集
*/ */
public String charset() { public String charsetName() {
return charset.name(); return charset.name();
} }
/**
* 返回字符集
*
* @return 字符集
*/
public Charset charset() {
return this.charset;
}
/** /**
* 设置字符集 * 设置字符集
* *

View File

@ -1,13 +1,12 @@
package cn.hutool.http.client.engine.jdk; package cn.hutool.http.client.engine.jdk;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.URLUtil; import cn.hutool.core.net.url.URLUtil;
import cn.hutool.core.reflect.FieldUtil; import cn.hutool.core.reflect.FieldUtil;
import cn.hutool.core.text.StrUtil; import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.HttpException; import cn.hutool.http.HttpException;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.http.meta.Header; import cn.hutool.http.client.HeaderOperation;
import cn.hutool.http.meta.Method; import cn.hutool.http.meta.Method;
import cn.hutool.http.ssl.DefaultSSLInfo; import cn.hutool.http.ssl.DefaultSSLInfo;
@ -21,23 +20,21 @@ import java.net.HttpURLConnection;
import java.net.ProtocolException; import java.net.ProtocolException;
import java.net.Proxy; import java.net.Proxy;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException; import java.nio.charset.UnsupportedCharsetException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
/** /**
* http连接对象对HttpURLConnection的包装 * http连接对象对HttpURLConnection的包装
* *
* @author Looly * @author Looly
*/ */
public class HttpConnection { public class HttpConnection implements HeaderOperation<HttpConnection> {
private final URL url; private final URL url;
private final Proxy proxy; private final Proxy proxy;
private HttpURLConnection conn; private final HttpURLConnection conn;
/** /**
* 创建HttpConnection * 创建HttpConnection
@ -61,7 +58,7 @@ public class HttpConnection {
return new HttpConnection(url, proxy); return new HttpConnection(url, proxy);
} }
// --------------------------------------------------------------- Constructor start // region --------------------------------------------------------------- Constructor
/** /**
* 构造HttpConnection * 构造HttpConnection
@ -74,31 +71,14 @@ public class HttpConnection {
this.proxy = proxy; this.proxy = proxy;
// 初始化Http连接 // 初始化Http连接
initConn(); this.conn = HttpUrlConnectionUtil.openHttp(url, proxy);
}
// --------------------------------------------------------------- Constructor end
/**
* 初始化连接相关信息
*
* @return HttpConnection
* @since 4.4.1
*/
public HttpConnection initConn() {
try {
this.conn = openHttp();
} catch (final IOException e) {
throw new HttpException(e);
}
// 默认读取响应内容 // 默认读取响应内容
this.conn.setDoInput(true); this.conn.setDoInput(true);
return this;
} }
// --------------------------------------------------------------- Getters And Setters start // endregion --------------------------------------------------------------- Constructor
// region --------------------------------------------------------------- Getters And Setters
/** /**
* 获取请求方法,GET/POST * 获取请求方法,GET/POST
@ -165,127 +145,15 @@ public class HttpConnection {
return conn; return conn;
} }
// --------------------------------------------------------------- Getters And Setters end
// ---------------------------------------------------------------- Headers start
/** /**
* 设置请求头<br> * 是否禁用缓存
* 当请求头存在时覆盖之
*
* @param header 头名
* @param value 头值
* @param isOverride 是否覆盖旧值
* @return HttpConnection
*/
public HttpConnection header(final String header, final String value, final boolean isOverride) {
if (null != this.conn) {
if (isOverride) {
this.conn.setRequestProperty(header, value);
} else {
this.conn.addRequestProperty(header, value);
}
}
return this;
}
/**
* 设置请求头<br>
* 当请求头存在时覆盖之
*
* @param header 头名
* @param value 头值
* @param isOverride 是否覆盖旧值
* @return HttpConnection
*/
public HttpConnection header(final Header header, final String value, final boolean isOverride) {
return header(header.toString(), value, isOverride);
}
/**
* 设置请求头<br>
* 不覆盖原有请求头
*
* @param headerMap 请求头
* @param isOverride 是否覆盖
* @return this
*/
public HttpConnection header(final Map<String, List<String>> headerMap, final boolean isOverride) {
if (MapUtil.isNotEmpty(headerMap)) {
String name;
for (final Entry<String, List<String>> entry : headerMap.entrySet()) {
name = entry.getKey();
for (final String value : entry.getValue()) {
this.header(name, StrUtil.emptyIfNull(value), isOverride);
}
}
}
return this;
}
/**
* 获取Http请求头
*
* @param name Header名
* @return Http请求头值
*/
public String header(final String name) {
return this.conn.getHeaderField(name);
}
/**
* 获取Http请求头
*
* @param name Header名
* @return Http请求头值
*/
public String header(final Header name) {
return header(name.toString());
}
/**
* 获取所有Http请求头
*
* @return Http请求头Map
*/
public Map<String, List<String>> headers() {
return this.conn.getHeaderFields();
}
// ---------------------------------------------------------------- Headers end
/**
* 设置https请求参数<br>
* 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现此为sun内部api按照普通http请求处理
*
* @param hostnameVerifier 域名验证器非https传入null
* @param ssf SSLSocketFactory非https传入null
* @return this
* @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装
*/
public HttpConnection setHttpsInfo(final HostnameVerifier hostnameVerifier, final SSLSocketFactory ssf) throws HttpException {
final HttpURLConnection conn = this.conn;
if (conn instanceof HttpsURLConnection) {
// Https请求
final HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
// 验证域
httpsConn.setHostnameVerifier(ObjUtil.defaultIfNull(hostnameVerifier, DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER));
httpsConn.setSSLSocketFactory(ObjUtil.defaultIfNull(ssf, DefaultSSLInfo.DEFAULT_SSF));
}
return this;
}
/**
* 关闭缓存
* *
* @param isDisableCache 是否禁用缓存
* @return this * @return this
* @see HttpURLConnection#setUseCaches(boolean) * @see HttpURLConnection#setUseCaches(boolean)
*/ */
public HttpConnection disableCache() { public HttpConnection setDisableCache(final boolean isDisableCache) {
this.conn.setUseCaches(false); this.conn.setUseCaches(!isDisableCache);
return this; return this;
} }
@ -331,15 +199,25 @@ public class HttpConnection {
} }
/** /**
* 设置Cookie * 设置https请求参数<br>
* 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现此为sun内部api按照普通http请求处理
* *
* @param cookie Cookie * @param hostnameVerifier 域名验证器非https传入null
* @param ssf SSLSocketFactory非https传入null
* @return this * @return this
* @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装
*/ */
public HttpConnection setCookie(final String cookie) { public HttpConnection setHttpsInfo(final HostnameVerifier hostnameVerifier, final SSLSocketFactory ssf) throws HttpException {
if (cookie != null) { final HttpURLConnection conn = this.conn;
header(Header.COOKIE, cookie, true);
if (conn instanceof HttpsURLConnection) {
// Https请求
final HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
// 验证域
httpsConn.setHostnameVerifier(ObjUtil.defaultIfNull(hostnameVerifier, DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER));
httpsConn.setSSLSocketFactory(ObjUtil.defaultIfNull(ssf, DefaultSSLInfo.DEFAULT_SSF));
} }
return this; return this;
} }
@ -368,6 +246,55 @@ public class HttpConnection {
return this; return this;
} }
// endregion --------------------------------------------------------------- Getters And Setters
// region ---------------------------------------------------------------- Headers
/**
* 设置请求头<br>
* 当请求头存在时覆盖之
*
* @param header 头名
* @param value 头值
* @param isOverride 是否覆盖旧值
* @return HttpConnection
*/
@Override
public HttpConnection header(final String header, final String value, final boolean isOverride) {
if (null != this.conn) {
if (isOverride) {
this.conn.setRequestProperty(header, value);
} else {
this.conn.addRequestProperty(header, value);
}
}
return this;
}
/**
* 获取Http请求头
*
* @param name Header名
* @return Http请求头值
*/
@Override
public String header(final String name) {
return this.conn.getHeaderField(name);
}
/**
* 获取所有Http请求头
*
* @return Http请求头Map
*/
@Override
public Map<String, List<String>> headers() {
return this.conn.getHeaderFields();
}
// endregion---------------------------------------------------------------- Headers
/** /**
* 连接 * 连接
* *
@ -456,7 +383,7 @@ public class HttpConnection {
// 在sun.net.www.protocol.http.HttpURLConnection.getOutputStream0方法中会把GET方法 // 在sun.net.www.protocol.http.HttpURLConnection.getOutputStream0方法中会把GET方法
// 修改为POST而且无法调用setRequestMethod方法修改因此此处使用反射强制修改字段属性值 // 修改为POST而且无法调用setRequestMethod方法修改因此此处使用反射强制修改字段属性值
// https://stackoverflow.com/questions/978061/http-get-with-request-body/983458 // https://stackoverflow.com/questions/978061/http-get-with-request-body/983458
if(method == Method.GET && method != getMethod()){ if (method == Method.GET && method != getMethod()) {
FieldUtil.setFieldValue(this.conn, "method", Method.GET.name()); FieldUtil.setFieldValue(this.conn, "method", Method.GET.name());
} }
@ -469,7 +396,7 @@ public class HttpConnection {
* @return 响应码 * @return 响应码
* @throws IOException IO异常 * @throws IOException IO异常
*/ */
public int responseCode() throws IOException { public int getCode() throws IOException {
if (null != this.conn) { if (null != this.conn) {
return this.conn.getResponseCode(); return this.conn.getResponseCode();
} }
@ -521,32 +448,4 @@ public class HttpConnection {
return sb.toString(); return sb.toString();
} }
// --------------------------------------------------------------- Private Method start
/**
* 初始化http或https请求参数<br>
* 有些时候https请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现此为sun内部api按照普通http请求处理
*
* @return {@link HttpURLConnection}https返回{@link HttpsURLConnection}
*/
private HttpURLConnection openHttp() throws IOException {
final URLConnection conn = openConnection();
if (false == conn instanceof HttpURLConnection) {
// 防止其它协议造成的转换异常
throw new HttpException("'{}' of URL [{}] is not a http connection, make sure URL is format for http.", conn.getClass().getName(), this.url);
}
return (HttpURLConnection) conn;
}
/**
* 建立连接
*
* @return {@link URLConnection}
* @throws IOException IO异常
*/
private URLConnection openConnection() throws IOException {
return (null == this.proxy) ? url.openConnection() : url.openConnection(this.proxy);
}
// --------------------------------------------------------------- Private Method end
} }

View File

@ -1,56 +0,0 @@
package cn.hutool.http.client.engine.jdk;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* Http拦截器接口通过实现此接口完成请求发起前或结束后对请求的编辑工作
*
* @param <T> 过滤参数类型HttpRequest或者HttpResponse
* @author looly
* @since 5.7.16
*/
@FunctionalInterface
public interface HttpInterceptor<T extends HttpBase<T>> {
/**
* 处理请求
*
* @param httpObj 请求或响应对象
*/
void process(T httpObj);
/**
* 拦截器链
*
* @param <T> 过滤参数类型HttpRequest或者HttpResponse
* @author looly
* @since 5.7.16
*/
class Chain<T extends HttpBase<T>> implements cn.hutool.core.lang.Chain<HttpInterceptor<T>, Chain<T>> {
private final List<HttpInterceptor<T>> interceptors = new LinkedList<>();
@Override
public Chain<T> addChain(final HttpInterceptor<T> element) {
interceptors.add(element);
return this;
}
@Override
public Iterator<HttpInterceptor<T>> iterator() {
return interceptors.iterator();
}
/**
* 清空
*
* @return this
* @since 5.8.0
*/
public Chain<T> clear() {
interceptors.clear();
return this;
}
}
}

View File

@ -16,32 +16,30 @@ import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.text.StrUtil; import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.meta.ContentType;
import cn.hutool.http.GlobalHeaders; import cn.hutool.http.GlobalHeaders;
import cn.hutool.http.meta.Header;
import cn.hutool.http.HttpConfig; import cn.hutool.http.HttpConfig;
import cn.hutool.http.HttpException; import cn.hutool.http.HttpException;
import cn.hutool.http.HttpGlobalConfig; import cn.hutool.http.HttpGlobalConfig;
import cn.hutool.http.meta.HttpStatus;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.http.meta.Method;
import cn.hutool.http.client.body.BytesBody; import cn.hutool.http.client.body.BytesBody;
import cn.hutool.http.client.body.UrlEncodedFormBody;
import cn.hutool.http.client.body.MultipartBody; import cn.hutool.http.client.body.MultipartBody;
import cn.hutool.http.client.body.RequestBody; import cn.hutool.http.client.body.HttpBody;
import cn.hutool.http.client.body.UrlEncodedFormBody;
import cn.hutool.http.client.cookie.GlobalCookieManager; import cn.hutool.http.client.cookie.GlobalCookieManager;
import cn.hutool.http.meta.ContentType;
import cn.hutool.http.meta.Header;
import cn.hutool.http.meta.HttpStatus;
import cn.hutool.http.meta.Method;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.Proxy; import java.net.Proxy;
import java.net.URLStreamHandler; import java.net.URLStreamHandler;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
@ -352,28 +350,6 @@ public class HttpRequest extends HttpBase<HttpRequest> {
// ---------------------------------------------------------------- Http Request Header start // ---------------------------------------------------------------- Http Request Header start
/**
* 设置contentType
*
* @param contentType contentType
* @return HttpRequest
*/
public HttpRequest contentType(final String contentType) {
header(Header.CONTENT_TYPE, contentType);
return this;
}
/**
* 设置是否为长连接
*
* @param isKeepAlive 是否长连接
* @return HttpRequest
*/
public HttpRequest keepAlive(final boolean isKeepAlive) {
header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
return this;
}
/** /**
* @return 获取是否为长连接 * @return 获取是否为长连接
*/ */
@ -406,35 +382,6 @@ public class HttpRequest extends HttpBase<HttpRequest> {
return this; return this;
} }
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookies Cookie值数组如果为{@code null}则设置无效使用默认Cookie行为
* @return this
* @since 5.4.1
*/
public HttpRequest cookie(final Collection<HttpCookie> cookies) {
return cookie(CollUtil.isEmpty(cookies) ? null : cookies.toArray(new HttpCookie[0]));
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookies Cookie值数组如果为{@code null}则设置无效使用默认Cookie行为
* @return this
* @since 3.1.1
*/
public HttpRequest cookie(final HttpCookie... cookies) {
if (ArrayUtil.isEmpty(cookies)) {
return disableCookie();
}
// 名称/值对之间用分号和空格 ('; ')
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cookie
return cookie(ArrayUtil.join(cookies, "; "));
}
/** /**
* 设置Cookie<br> * 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为 * 自定义Cookie后会覆盖Hutool的默认Cookie行为
@ -447,27 +394,6 @@ public class HttpRequest extends HttpBase<HttpRequest> {
this.cookie = cookie; this.cookie = cookie;
return this; 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 // ---------------------------------------------------------------- Http Request Header end
// ---------------------------------------------------------------- Form start // ---------------------------------------------------------------- Form start
@ -947,42 +873,6 @@ public class HttpRequest extends HttpBase<HttpRequest> {
return this; return this;
} }
/**
* 设置拦截器用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
* @see #addRequestInterceptor(HttpInterceptor)
* @since 5.7.16
*/
public HttpRequest addInterceptor(final HttpInterceptor<HttpRequest> interceptor) {
return addRequestInterceptor(interceptor);
}
/**
* 设置拦截器用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
* @since 5.8.0
*/
public HttpRequest addRequestInterceptor(final HttpInterceptor<HttpRequest> interceptor) {
config.addRequestInterceptor(interceptor);
return this;
}
/**
* 设置拦截器用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
* @since 5.8.0
*/
public HttpRequest addResponseInterceptor(final HttpInterceptor<HttpResponse> interceptor) {
config.addResponseInterceptor(interceptor);
return this;
}
/** /**
* 执行Reuqest请求 * 执行Reuqest请求
* *
@ -1013,7 +903,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
* @return this * @return this
*/ */
public HttpResponse execute(final boolean isAsync) { public HttpResponse execute(final boolean isAsync) {
return doExecute(isAsync, config.requestInterceptors, config.responseInterceptors); return doExecute(isAsync);
} }
/** /**
@ -1074,41 +964,6 @@ public class HttpRequest extends HttpBase<HttpRequest> {
return proxyAuth(HttpUtil.buildBasicAuth(username, password, charset)); return proxyAuth(HttpUtil.buildBasicAuth(username, password, charset));
} }
/**
* 令牌验证生成的头类似于"Authorization: Bearer XXXXX"一般用于JWT
*
* @param token 令牌内容
* @return HttpRequest
* @since 5.5.3
*/
public HttpRequest bearerAuth(final String token) {
return auth("Bearer " + token);
}
/**
* 验证简单插入Authorization头
*
* @param content 验证内容
* @return HttpRequest
* @since 5.2.4
*/
public HttpRequest auth(final String content) {
header(Header.AUTHORIZATION, content, true);
return this;
}
/**
* 验证简单插入Authorization头
*
* @param content 验证内容
* @return HttpRequest
* @since 5.4.6
*/
public HttpRequest proxyAuth(final String content) {
header(Header.PROXY_AUTHORIZATION, content, true);
return this;
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder sb = StrUtil.builder(); final StringBuilder sb = StrUtil.builder();
@ -1123,18 +978,9 @@ public class HttpRequest extends HttpBase<HttpRequest> {
* 执行Reuqest请求 * 执行Reuqest请求
* *
* @param isAsync 是否异步 * @param isAsync 是否异步
* @param requestInterceptors 请求拦截器列表
* @param responseInterceptors 响应拦截器列表
* @return this * @return this
*/ */
private HttpResponse doExecute(final boolean isAsync, final HttpInterceptor.Chain<HttpRequest> requestInterceptors, private HttpResponse doExecute(final boolean isAsync) {
final HttpInterceptor.Chain<HttpResponse> responseInterceptors) {
if (null != requestInterceptors) {
for (final HttpInterceptor<HttpRequest> interceptor : requestInterceptors) {
interceptor.process(this);
}
}
// 初始化URL // 初始化URL
urlWithParamIfGet(); urlWithParamIfGet();
// 初始化 connection // 初始化 connection
@ -1147,14 +993,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
// 获取响应 // 获取响应
if (null == httpResponse) { if (null == httpResponse) {
httpResponse = new HttpResponse(this.httpConnection, this.config, this.charset, isAsync, isIgnoreResponseBody()); httpResponse = new HttpResponse(this.httpConnection, this.config.isIgnoreEOFError(), this.charset, isAsync, isIgnoreResponseBody());
}
// 拦截响应
if (null != responseInterceptors) {
for (final HttpInterceptor<HttpResponse> interceptor : responseInterceptors) {
interceptor.process(httpResponse);
}
} }
return httpResponse; return httpResponse;
@ -1186,16 +1025,14 @@ public class HttpRequest extends HttpBase<HttpRequest> {
if (null != this.cookie) { if (null != this.cookie) {
// 当用户自定义Cookie时全局Cookie自动失效 // 当用户自定义Cookie时全局Cookie自动失效
this.httpConnection.setCookie(this.cookie); this.httpConnection.cookie(this.cookie);
} else { } else {
// 读取全局Cookie信息并附带到请求中 // 读取全局Cookie信息并附带到请求中
GlobalCookieManager.add(this.httpConnection); GlobalCookieManager.add(this.httpConnection);
} }
// 是否禁用缓存 // 是否禁用缓存
if (config.isDisableCache) { this.httpConnection.setDisableCache(config.isDisableCache);
this.httpConnection.disableCache();
}
} }
/** /**
@ -1229,17 +1066,17 @@ public class HttpRequest extends HttpBase<HttpRequest> {
private HttpResponse sendRedirectIfPossible(final boolean isAsync) { private HttpResponse sendRedirectIfPossible(final boolean isAsync) {
// 手动实现重定向 // 手动实现重定向
if (config.maxRedirectCount > 0) { if (config.maxRedirectCount > 0) {
final int responseCode; final int code;
try { try {
responseCode = httpConnection.responseCode(); code = httpConnection.getCode();
} catch (final IOException e) { } catch (final IOException e) {
// 错误时静默关闭连接 // 错误时静默关闭连接
this.httpConnection.disconnectQuietly(); this.httpConnection.disconnectQuietly();
throw new HttpException(e); throw new HttpException(e);
} }
if (responseCode != HttpURLConnection.HTTP_OK) { if (code != HttpURLConnection.HTTP_OK) {
if (HttpStatus.isRedirected(responseCode)) { if (HttpStatus.isRedirected(code)) {
final UrlBuilder redirectUrl; final UrlBuilder redirectUrl;
String location = httpConnection.header(Header.LOCATION); String location = httpConnection.header(Header.LOCATION);
@ -1258,9 +1095,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
setUrl(redirectUrl); setUrl(redirectUrl);
if (redirectCount < config.maxRedirectCount) { if (redirectCount < config.maxRedirectCount) {
redirectCount++; redirectCount++;
// 重定向不再走过滤器 return doExecute(isAsync);
return doExecute(isAsync, config.interceptorOnRedirect ? config.requestInterceptors : null,
config.interceptorOnRedirect ? config.responseInterceptors : null);
} }
} }
} }
@ -1307,7 +1142,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
} }
// Write的时候会优先使用body中的内容write时自动关闭OutputStream // Write的时候会优先使用body中的内容write时自动关闭OutputStream
final RequestBody body; final HttpBody body;
if (ArrayUtil.isNotEmpty(this.bodyBytes)) { if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
body = BytesBody.of(this.bodyBytes); body = BytesBody.of(this.bodyBytes);
} else { } else {

View File

@ -1,30 +1,19 @@
package cn.hutool.http.client.engine.jdk; package cn.hutool.http.client.engine.jdk;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.stream.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.StreamProgress; import cn.hutool.core.io.stream.FastByteArrayOutputStream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.net.url.URLEncoder;
import cn.hutool.core.regex.ReUtil;
import cn.hutool.core.text.StrUtil; import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.meta.Header;
import cn.hutool.http.HttpConfig;
import cn.hutool.http.HttpException; import cn.hutool.http.HttpException;
import cn.hutool.http.HttpUtil; import cn.hutool.http.client.Response;
import cn.hutool.http.client.body.ResponseBody;
import cn.hutool.http.client.cookie.GlobalCookieManager; import cn.hutool.http.client.cookie.GlobalCookieManager;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.Closeable; import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpCookie; import java.net.HttpCookie;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
@ -36,12 +25,14 @@ import java.util.Map.Entry;
* *
* @author Looly * @author Looly
*/ */
public class HttpResponse extends HttpBase<HttpResponse> implements Closeable { public class HttpResponse extends HttpBase<HttpResponse> implements Response, Closeable {
/** /**
* Http配置 * 是否忽略响应读取时可能的EOF异常<br>
* 在Http协议中对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束<br>
* 如果服务端未遵循这个规范或响应没有正常结束会报EOF异常此选项用于是否忽略这个异常
*/ */
protected HttpConfig config; protected boolean ignoreEOFError;
/** /**
* 持有连接对象 * 持有连接对象
*/ */
@ -71,15 +62,14 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* 构造 * 构造
* *
* @param httpConnection {@link HttpConnection} * @param httpConnection {@link HttpConnection}
* @param config Http配置 * @param ignoreEOFError 是否忽略响应读取时可能的EOF异常
* @param charset 编码从请求编码中获取默认编码 * @param charset 编码从请求编码中获取默认编码
* @param isAsync 是否异步 * @param isAsync 是否异步
* @param isIgnoreBody 是否忽略读取响应体 * @param isIgnoreBody 是否忽略读取响应体
* @since 3.1.2
*/ */
protected HttpResponse(final HttpConnection httpConnection, final HttpConfig config, final Charset charset, final boolean isAsync, final boolean isIgnoreBody) { protected HttpResponse(final HttpConnection httpConnection, final boolean ignoreEOFError, final Charset charset, final boolean isAsync, final boolean isIgnoreBody) {
this.httpConnection = httpConnection; this.httpConnection = httpConnection;
this.config = config; this.ignoreEOFError = ignoreEOFError;
this.charset = charset; this.charset = charset;
this.isAsync = isAsync; this.isAsync = isAsync;
this.ignoreBody = isIgnoreBody; this.ignoreBody = isIgnoreBody;
@ -91,20 +81,11 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* *
* @return 状态码 * @return 状态码
*/ */
@Override
public int getStatus() { public int getStatus() {
return this.status; return this.status;
} }
/**
* 请求是否成功判断依据为状态码范围在200~299内
*
* @return 是否成功请求
* @since 4.1.9
*/
public boolean isOk() {
return this.status >= 200 && this.status < 300;
}
/** /**
* 同步<br> * 同步<br>
* 如果为异步状态则暂时不读取服务器中响应的内容而是持有Http链接的{@link InputStream}<br> * 如果为异步状态则暂时不读取服务器中响应的内容而是持有Http链接的{@link InputStream}<br>
@ -118,56 +99,6 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
// ---------------------------------------------------------------- Http Response Header start // ---------------------------------------------------------------- Http Response Header start
/**
* 获取内容编码
*
* @return String
*/
public String contentEncoding() {
return header(Header.CONTENT_ENCODING);
}
/**
* 获取内容长度以下情况长度无效
* <ul>
* <li>Transfer-Encoding: Chunked</li>
* <li>Content-Encoding: XXX</li>
* </ul>
* 参考<a href="https://blog.csdn.net/jiang7701037/article/details/86304302">https://blog.csdn.net/jiang7701037/article/details/86304302</a>
*
* @return 长度-1表示服务端未返回或长度无效
* @since 5.7.9
*/
public long contentLength() {
long contentLength = Convert.toLong(header(Header.CONTENT_LENGTH), -1L);
if (contentLength > 0 && (isChunked() || StrUtil.isNotBlank(contentEncoding()))) {
//按照HTTP协议规范 Transfer-Encoding和Content-Encoding设置后 Content-Length 无效
contentLength = -1;
}
return contentLength;
}
/**
* 是否为Transfer-Encoding:Chunked的内容
*
* @return 是否为Transfer-Encoding:Chunked的内容
* @since 4.6.2
*/
public boolean isChunked() {
final String transferEncoding = header(Header.TRANSFER_ENCODING);
return "Chunked".equalsIgnoreCase(transferEncoding);
}
/**
* 获取本次请求服务器返回的Cookie信息
*
* @return Cookie字符串
* @since 3.1.1
*/
public String getCookieStr() {
return header(Header.SET_COOKIE);
}
/** /**
* 获取Cookie * 获取Cookie
* *
@ -220,6 +151,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* *
* @return 响应流 * @return 响应流
*/ */
@Override
public InputStream bodyStream() { public InputStream bodyStream() {
if (isAsync) { if (isAsync) {
return this.in; return this.in;
@ -227,6 +159,11 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
return new ByteArrayInputStream(this.bodyBytes); return new ByteArrayInputStream(this.bodyBytes);
} }
@Override
public ResponseBody body() {
return new ResponseBody(this, this.ignoreEOFError);
}
/** /**
* 获取响应流字节码<br> * 获取响应流字节码<br>
* 此方法会转为同步模式 * 此方法会转为同步模式
@ -241,148 +178,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
} }
/** /**
* 获取响应主体 * 设置主体字节码一般用于拦截器修改响应内容<br>
*
* @return String
* @throws HttpException 包装IO异常
*/
public String body() throws HttpException {
return HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse);
}
/**
* 将响应内容写出到{@link OutputStream}<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param out 写出的流
* @param isCloseOut 是否关闭输出流
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(final OutputStream out, final boolean isCloseOut, final StreamProgress streamProgress) {
Assert.notNull(out, "[out] must be not null!");
final long contentLength = contentLength();
try {
return copyBody(bodyStream(), out, contentLength, streamProgress, this.config.isIgnoreEOFError());
} finally {
IoUtil.close(this);
if (isCloseOut) {
IoUtil.close(out);
}
}
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param targetFileOrDir 写出到的文件或目录
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(final File targetFileOrDir, final StreamProgress streamProgress) {
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
final File outFile = completeFileNameFromHeader(targetFileOrDir, null);
return writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
}
/**
* 将响应内容写出到文件-避免未完成的文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式<br>
* 来自<a href="https://gitee.com/dromara/hutool/pulls/407">https://gitee.com/dromara/hutool/pulls/407</a><br>
* 此方法原理是先在目标文件同级目录下创建临时文件下载之等下载完毕后重命名避免因下载错误导致的文件不完整
*
* @param targetFileOrDir 写出到的文件或目录
* @param tempFileSuffix 临时文件后缀默认".temp"
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出bytes数
* @since 5.7.12
*/
public long writeBody(final File targetFileOrDir, String tempFileSuffix, final StreamProgress streamProgress) {
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
File outFile = completeFileNameFromHeader(targetFileOrDir, null);
if (StrUtil.isBlank(tempFileSuffix)) {
tempFileSuffix = ".temp";
} else {
tempFileSuffix = StrUtil.addPrefixIfNot(tempFileSuffix, StrUtil.DOT);
}
// 目标文件真实名称
final String fileName = outFile.getName();
// 临时文件名称
final String tempFileName = fileName + tempFileSuffix;
// 临时文件
outFile = new File(outFile.getParentFile(), tempFileName);
final long length;
try {
length = writeBody(outFile, streamProgress);
// 重命名下载好的临时文件
FileUtil.rename(outFile, fileName, true);
} catch (final Throwable e) {
// 异常则删除临时文件
FileUtil.del(outFile);
throw new HttpException(e);
}
return length;
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param targetFileOrDir 写出到的文件
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出的文件
* @since 5.6.4
*/
public File writeBodyForFile(final File targetFileOrDir, final StreamProgress streamProgress) {
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
final File outFile = completeFileNameFromHeader(targetFileOrDir, null);
writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
return outFile;
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param targetFileOrDir 写出到的文件或目录
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(final File targetFileOrDir) {
return writeBody(targetFileOrDir, null);
}
/**
* 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param targetFileOrDir 写出到的文件或目录的路径
* @return 写出bytes数
* @since 3.3.2
*/
public long writeBody(final String targetFileOrDir) {
return writeBody(FileUtil.file(targetFileOrDir));
}
/**
* 设置主体字节码一版用于拦截器修改响应内容<br>
* 需在此方法调用前使用charset方法设置编码否则使用默认编码UTF-8 * 需在此方法调用前使用charset方法设置编码否则使用默认编码UTF-8
* *
* @param bodyBytes 主体 * @param bodyBytes 主体
@ -420,54 +216,9 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
return sb.toString(); return sb.toString();
} }
/** @Override
* 从响应头补全下载文件名 public String header(final String name) {
* return super.header(name);
* @param targetFileOrDir 目标文件夹或者目标文件
* @param customParamName 自定义的参数名称如果传入{@code null}默认使用"filename"
* @return File 保存的文件
* @since 5.4.1
*/
public File completeFileNameFromHeader(final File targetFileOrDir, final String customParamName) {
if (false == targetFileOrDir.isDirectory()) {
// 非目录直接返回
return targetFileOrDir;
}
// 从头信息中获取文件名
String fileName = getFileNameFromDisposition(ObjUtil.defaultIfNull(customParamName, "filename"));
if (StrUtil.isBlank(fileName)) {
final String path = httpConnection.getUrl().getPath();
// 从路径中获取文件名
fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
if (StrUtil.isBlank(fileName)) {
// 编码后的路径做为文件名
fileName = URLEncoder.encodeQuery(path, charset);
} else {
// issue#I4K0FS@Gitee
fileName = URLEncoder.encodeQuery(fileName, charset);
}
}
return FileUtil.file(targetFileOrDir, fileName);
}
/**
* 从Content-Disposition头中获取文件名
*
* @param paramName 文件名的参数名
* @return 文件名empty表示无
* @since 5.8.10
*/
public String getFileNameFromDisposition(final String paramName) {
String fileName = null;
final String disposition = header(Header.CONTENT_DISPOSITION);
if (StrUtil.isNotBlank(disposition)) {
fileName = ReUtil.get(paramName + "=\"(.*?)\"", disposition, 1);
if (StrUtil.isBlank(fileName)) {
fileName = StrUtil.subAfter(disposition, paramName + "=", true);
}
}
return fileName;
} }
// ---------------------------------------------------------------- Private method start // ---------------------------------------------------------------- Private method start
@ -509,7 +260,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
private void init() throws HttpException { private void init() throws HttpException {
// 获取响应状态码 // 获取响应状态码
try { try {
this.status = httpConnection.responseCode(); this.status = httpConnection.getCode();
} catch (final IOException e) { } catch (final IOException e) {
if (false == (e instanceof FileNotFoundException)) { if (false == (e instanceof FileNotFoundException)) {
throw new HttpException(e); throw new HttpException(e);
@ -529,11 +280,10 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
// 存储服务端设置的Cookie信息 // 存储服务端设置的Cookie信息
GlobalCookieManager.store(httpConnection, this.headers); GlobalCookieManager.store(httpConnection, this.headers);
// 获取响应编码 // 获取响应编码如果非空替换用户定义的编码
final Charset charset = httpConnection.getCharset(); final Charset charsetFromResponse = httpConnection.getCharset();
this.charsetFromResponse = charset; if (null != charsetFromResponse) {
if (null != charset) { this.charset = charsetFromResponse;
this.charset = charset;
} }
// 获取响应内容流 // 获取响应内容流
@ -591,40 +341,8 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
final long contentLength = contentLength(); final long contentLength = contentLength();
final FastByteArrayOutputStream out = new FastByteArrayOutputStream((int) contentLength); final FastByteArrayOutputStream out = new FastByteArrayOutputStream((int) contentLength);
copyBody(in, out, contentLength, null, this.config.isIgnoreEOFError()); body().writeClose(out);
this.bodyBytes = out.toByteArray(); this.bodyBytes = out.toByteArray();
} }
/**
* 将响应内容写出到{@link OutputStream}<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式
*
* @param in 输入流
* @param out 写出的流
* @param contentLength 总长度-1表示未知
* @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @param isIgnoreEOFError 是否忽略响应读取时可能的EOF异常
* @return 拷贝长度
*/
private static long copyBody(final InputStream in, final OutputStream out, final long contentLength, final StreamProgress streamProgress, final boolean isIgnoreEOFError) {
if (null == out) {
throw new NullPointerException("[out] is null!");
}
long copyLength = -1;
try {
copyLength = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE, contentLength, streamProgress);
} catch (final IORuntimeException e) {
//noinspection StatementWithEmptyBody
if (isIgnoreEOFError
&& (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF"))) {
// 忽略读取HTTP流中的EOF错误
} else {
throw e;
}
}
return copyLength;
}
// ---------------------------------------------------------------- Private method end // ---------------------------------------------------------------- Private method end
} }

View File

@ -1,13 +1,19 @@
package cn.hutool.http.client.engine.jdk; package cn.hutool.http.client.engine.jdk;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.reflect.FieldUtil; import cn.hutool.core.reflect.FieldUtil;
import cn.hutool.core.reflect.ModifierUtil; import cn.hutool.core.reflect.ModifierUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.SystemUtil; import cn.hutool.core.util.SystemUtil;
import cn.hutool.http.HttpException; import cn.hutool.http.HttpException;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController; import java.security.AccessController;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
@ -55,4 +61,38 @@ public class HttpUrlConnectionUtil {
FieldUtil.setStaticFieldValue(methodsField, METHODS); FieldUtil.setStaticFieldValue(methodsField, METHODS);
} }
} }
/**
* 初始化http或https请求参数<br>
* 有些时候https请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现此为sun内部api按照普通http请求处理
*
* @param url 请求的URL必须为http
* @param proxy 代理无代理传{@code null}
* @return {@link HttpURLConnection}https返回{@link HttpsURLConnection}
* @throws IORuntimeException IO异常
*/
public static HttpURLConnection openHttp(final URL url, final Proxy proxy) throws IORuntimeException {
final URLConnection conn = openConnection(url, proxy);
if (false == conn instanceof HttpURLConnection) {
// 防止其它协议造成的转换异常
throw new HttpException("'{}' of URL [{}] is not a http connection, make sure URL is format for http.",
conn.getClass().getName(), url);
}
return (HttpURLConnection) conn;
}
/**
* 建立连接
*
* @return {@link URLConnection}
* @throws IORuntimeException IO异常
*/
private static URLConnection openConnection(final URL url, final Proxy proxy) throws IORuntimeException {
try {
return (null == proxy) ? url.openConnection() : url.openConnection(proxy);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
} }

View File

@ -0,0 +1,208 @@
package cn.hutool.http.client.engine.jdk;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.text.StrUtil;
import cn.hutool.http.HttpException;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.client.ClientConfig;
import cn.hutool.http.client.ClientEngine;
import cn.hutool.http.client.Request;
import cn.hutool.http.client.Response;
import cn.hutool.http.client.body.HttpBody;
import cn.hutool.http.client.cookie.GlobalCookieManager;
import cn.hutool.http.meta.Header;
import cn.hutool.http.meta.HttpStatus;
import cn.hutool.http.meta.Method;
import java.io.IOException;
import java.net.HttpURLConnection;
/**
* 基于JDK的UrlConnection的Http客户端引擎实现
*
* @author looly
*/
public class JdkClientEngine implements ClientEngine {
private final ClientConfig config;
private HttpConnection conn;
/**
* 重定向次数计数器内部使用
*/
private int redirectCount;
/**
* 构造
*/
public JdkClientEngine() {
this.config = ClientConfig.of();
}
@Override
public Response send(final Request message) {
return send(message, false);
}
/**
* 发送请求
*
* @param message 请求消息
* @param isAsync 是否异步异步不会立即读取响应内容
* @return {@link Response}
*/
public HttpResponse send(final Request message, final boolean isAsync) {
initConn(message);
try {
doSend(message);
} catch (final IOException e) {
// 出错后关闭连接
IoUtil.close(this);
throw new RuntimeException(e);
}
return sendRedirectIfPossible(message, isAsync);
}
@Override
public Object getRawEngine() {
return this;
}
@Override
public void close() throws IOException {
if (null != conn) {
conn.disconnectQuietly();
}
}
/**
* 执行发送
*
* @param message 请求消息
* @throws IOException IO异常
*/
private void doSend(final Request message) throws IOException {
final HttpBody body = message.body();
if (null != body) {
// 带有消息体一律按照Rest方式发送
body.writeClose(this.conn.getOutputStream());
return;
}
// 非Rest简单GET请求
this.conn.connect();
}
/**
* 初始化连接对象
*
* @param message 请求消息
*/
private void initConn(final Request message) {
// 执行下次请求时自动关闭上次请求常用于转发
IoUtil.close(this);
this.conn = buildConn(message);
}
/**
* 构建{@link HttpConnection}
*
* @param message {@link Request}消息
* @return {@link HttpConnection}
*/
private HttpConnection buildConn(final Request message) {
final HttpConnection conn = HttpConnection
.of(message.url().toURL(), config.proxy)
.setConnectTimeout(config.getConnectionTimeout())
.setReadTimeout(config.getReadTimeout())
.setMethod(message.method())//
.setHttpsInfo(config.getHostnameVerifier(), config.getSocketFactory())
// 关闭JDK自动转发采用手动转发方式
.setInstanceFollowRedirects(false)
.setChunkedStreamingMode(message.isChunked() ? 4096 : -1)
.setDisableCache(config.isDisableCache())
// 覆盖默认Header
.header(message.headers(), true);
if (null == message.header(Header.COOKIE)) {
// 用户没有自定义Cookie则读取全局Cookie信息并附带到请求中
GlobalCookieManager.add(conn);
}
return conn;
}
/**
* 调用转发如果需要转发返回转发结果否则返回{@code null}
*
* @param isAsync 最终请求是否异步
* @return {@link HttpResponse}无转发返回 {@code null}
*/
private HttpResponse sendRedirectIfPossible(final Request message, final boolean isAsync) {
final HttpConnection conn = this.conn;
// 手动实现重定向
if (message.maxRedirectCount() > 0) {
final int code;
try {
code = conn.getCode();
} catch (final IOException e) {
// 错误时静默关闭连接
conn.disconnectQuietly();
throw new HttpException(e);
}
if (code != HttpURLConnection.HTTP_OK) {
if (HttpStatus.isRedirected(code)) {
message.url(getLocationUrl(message.url(), conn.header(Header.LOCATION)));
if (redirectCount < message.maxRedirectCount()) {
redirectCount++;
return send(message, isAsync);
}
}
}
}
// 最终页面
return new HttpResponse(this.conn, true, message.charset(), isAsync,
isIgnoreResponseBody(message.method()));
}
/**
* 获取转发的新的URL
*
* @param parentUrl 上级请求的URL
* @param location 获取的Location
* @return 新的URL
*/
private static UrlBuilder getLocationUrl(final UrlBuilder parentUrl, String location) {
final UrlBuilder redirectUrl;
if (false == HttpUtil.isHttp(location) && false == HttpUtil.isHttps(location)) {
// issue#I5TPSY
// location可能为相对路径
if (false == location.startsWith("/")) {
location = StrUtil.addSuffixIfNot(parentUrl.getPathStr(), "/") + location;
}
redirectUrl = UrlBuilder.of(parentUrl.getScheme(), parentUrl.getHost(), parentUrl.getPort(),
location, null, null, parentUrl.getCharset());
} else {
redirectUrl = UrlBuilder.ofHttpWithoutEncode(location);
}
return redirectUrl;
}
/**
* 是否忽略读取响应body部分<br>
* HEADCONNECTOPTIONSTRACE方法将不读取响应体
*
* @return 是否需要忽略响应body部分
*/
private static boolean isIgnoreResponseBody(final Method method) {
return Method.HEAD == method //
|| Method.CONNECT == method //
|| Method.OPTIONS == method //
|| Method.TRACE == method;
}
}

View File

@ -1,24 +1,24 @@
package cn.hutool.http.client.engine.okhttp; package cn.hutool.http.client.engine.okhttp;
import cn.hutool.http.client.body.RequestBody; import cn.hutool.http.client.body.HttpBody;
import okhttp3.MediaType; import okhttp3.MediaType;
import okio.BufferedSink; import okio.BufferedSink;
/** /**
* OkHttp的请求体实现通过{@link RequestBody}转换实现 * OkHttp的请求体实现通过{@link HttpBody}转换实现
* *
* @author looly * @author looly
*/ */
public class OkHttpRequestBody extends okhttp3.RequestBody { public class OkHttpRequestBody extends okhttp3.RequestBody {
private final RequestBody body; private final HttpBody body;
/** /**
* 构造 * 构造
* *
* @param body 请求体{@link RequestBody} * @param body 请求体{@link HttpBody}
*/ */
public OkHttpRequestBody(final RequestBody body) { public OkHttpRequestBody(final HttpBody body) {
this.body = body; this.body = body;
} }

View File

@ -184,22 +184,6 @@ public class HttpRequestTest {
Console.log(execute.getStatus(), execute.header(Header.LOCATION)); Console.log(execute.getStatus(), execute.header(Header.LOCATION));
} }
@Test
@Ignore
public void addInterceptorTest() {
HttpUtil.createGet("https://hutool.cn")
.addInterceptor(Console::log)
.addResponseInterceptor((res)-> Console.log(res.getStatus()))
.execute();
}
@Test
@Ignore
public void addGlobalInterceptorTest() {
GlobalInterceptor.INSTANCE.addRequestInterceptor(Console::log);
HttpUtil.createGet("https://hutool.cn").execute();
}
@Test @Test
@Ignore @Ignore
public void getWithFormTest(){ public void getWithFormTest(){