SpringBoot集成Apache HttpClient
一、SpringBoot的HttpClient简单实现
SpringBoot的HttpClient简单实现
二、覆盖绝大多数场景的封装
1.添加依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.18</version></dependency><!-- springboot中包含了httpclient --><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.14</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.44</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24</version><scope>provided</scope></dependency></dependencies>
2.代码实现
- 定义配置类
package com.zzc.component.http;import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;import java.util.List;
import java.util.Map;/*httpclient:charset: UTF-8conn-max-total: 300max-per-route: 100retry-num: 1connect-timeout: 30000 # 单位:毫秒read-timeout: 15000 # 单位:毫秒request-timeout: 200 # 单位:毫秒keep-alive-time: 60 # 单位:秒# 如果请求目标地址单独配置可长链接保持时间,使用该配置keep-alive-target-host:example.com: 120 # example.com 的保持活动时间为 120 秒api.example.org: 300 # api.example.org 的保持活动时间为 300 秒www.baidu.com: 60localhost: 60# http 请求 headerheaders:Content-Type: application/jsontarget-hosts:- domain: http://www.baidu.comkeep-alive-time: 120- domain: http://localhost:19999connect-timeout: 100read-timeout: 100request-timeout: 100*/
@Data
@ToString
@ConfigurationProperties(prefix = "httpclient")
public class HttpClientProperties {private String charset = "UTF-8";/*** 总链接数*/private Integer connMaxTotal = 3000;/*** 并发数量*/private Integer maxPerRoute = 1200;/*** 重试次数*/private Integer retryNum = 1;/*** 链接超时*/private Integer connectTimeout = 30000;/*** 读写超时*/private Integer readTimeout = 15000;/*** 链接不够用的等待时间,不宜过长,必须设置*/private Integer requestTimeout = 200;/*** 默认链接保持时间,单位 秒*/private Integer keepAliveTime = 60;/*** 如果请求目标地址单独配置可长链接保持时间,使用该配置*/private Map<String, Integer> keepAliveTargetHost;/*** http请求header*/private Map<String, String> headers;/*** 指定目标地址的配置一些基础配置*/private List<TargetHostConfig> targetHosts;@Data@ToStringpublic static class TargetHostConfig {/*** 指定域名*/private String domain;/*** 默认链接保持时间,单位 秒*/private Integer keepAliveTime;/*** 链接超时*/private Integer connectTimeout;/*** 最大并发数量*/private Integer maxPerRoute;/*** 读写超时*/private Integer readTimeout;/*** 链接不够用的等待时间,不宜过长*/private Integer requestTimeout;/*** http请求header*/private Map<String, String> headers;public String getHostname() {if (domain != null && !domain.trim().isEmpty()) {if (domain.startsWith("http://")) {return domain.replace("http://", "");} else if (domain.startsWith("https://")) {return domain.replace("https://", "");} else {return domain;}}return null;}}}
-
- 封装HttpClientStatus状态码枚举,避免框架替换的时候需要整改整个项目
package com.zzc.component.http;import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;public enum HttpClientStatus {// 1xx Informational/*** 客户端应当继续发送请求。这个代码只允许用在头部字段有 Expect 的情况下* {@code 100 Continue}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.2.1">HTTP/1.1: Semantics and Content, section 6.2.1</a>*/CONTINUE(100, Series.INFORMATIONAL, "Continue"),/*** 服务器已经理解了客户端的请求,并将通过升级协议来完成它(例如从 HTTP 切换到 WebSocket)。* {@code 101 Switching Protocols}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.2.2">HTTP/1.1: Semantics and Content, section 6.2.2</a>*/SWITCHING_PROTOCOLS(101, Series.INFORMATIONAL, "Switching Protocols"),/*** 服务器已经收到了请求,但尚未处理完毕,不过最终会处理完。* {@code 102 Processing}.** @see <a href="https://tools.ietf.org/html/rfc2518#section-10.1">WebDAV</a>*/PROCESSING(102, Series.INFORMATIONAL, "Processing"),/*** {@code 103 Checkpoint}.** @see <a href="https://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal">A proposal for supporting* resumable POST/PUT HTTP requests in HTTP/1.0</a>*/CHECKPOINT(103, Series.INFORMATIONAL, "Checkpoint"),// 2xx Success/*** 请求成功* {@code 200 OK}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.3.1">HTTP/1.1: Semantics and Content, section 6.3.1</a>*/OK(200, Series.SUCCESSFUL, "OK"),/*** 请求成功并且服务器创建了一个新的资源。通常用于 POST 请求后返回新创建资源的位置。* {@code 201 Created}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.3.2">HTTP/1.1: Semantics and Content, section 6.3.2</a>*/CREATED(201, Series.SUCCESSFUL, "Created"),/*** 请求已被接受,但还未处理完成。异步处理的结果可能稍后可用。* {@code 202 Accepted}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.3.3">HTTP/1.1: Semantics and Content, section 6.3.3</a>*/ACCEPTED(202, Series.SUCCESSFUL, "Accepted"),/*** 返回的信息不是由原始服务器提供的,而是来自缓存或第三方副本* {@code 203 Non-Authoritative Information}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.3.4">HTTP/1.1: Semantics and Content, section 6.3.4</a>*/NON_AUTHORITATIVE_INFORMATION(203, Series.SUCCESSFUL, "Non-Authoritative Information"),/*** 请求成功,但没有内容返回。通常用于 PUT 或 DELETE 请求。* {@code 204 No Content}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.3.5">HTTP/1.1: Semantics and Content, section 6.3.5</a>*/NO_CONTENT(204, Series.SUCCESSFUL, "No Content"),/*** 告诉客户端重置文档视图(例如清除表单)。不常用。* {@code 205 Reset Content}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.3.6">HTTP/1.1: Semantics and Content, section 6.3.6</a>*/RESET_CONTENT(205, Series.SUCCESSFUL, "Reset Content"),/*** 部分内容响应。通常与范围请求一起使用。* {@code 206 Partial Content}.** @see <a href="https://tools.ietf.org/html/rfc7233#section-4.1">HTTP/1.1: Range Requests, section 4.1</a>*/PARTIAL_CONTENT(206, Series.SUCCESSFUL, "Partial Content"),/*** WebDAV 请求可能包含多个操作状态* {@code 207 Multi-Status}.** @see <a href="https://tools.ietf.org/html/rfc4918#section-13">WebDAV</a>*/MULTI_STATUS(207, Series.SUCCESSFUL, "Multi-Status"),/*** 用于避免重复列举成员节点。* {@code 208 Already Reported}.** @see <a href="https://tools.ietf.org/html/rfc5842#section-7.1">WebDAV Binding Extensions</a>*/ALREADY_REPORTED(208, Series.SUCCESSFUL, "Already Reported"),/*** 服务器已经完成了对资源的部分 GET 请求,并且请求的部分已经在实体头中传输。* {@code 226 IM Used}.** @see <a href="https://tools.ietf.org/html/rfc3229#section-10.4.1">Delta encoding in HTTP</a>*/IM_USED(226, Series.SUCCESSFUL, "IM Used"),// 3xx Redirection/*** 请求的资源对应多个位置,客户端可以选择其中一个。* {@code 300 Multiple Choices}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.4.1">HTTP/1.1: Semantics and Content, section 6.4.1</a>*/MULTIPLE_CHOICES(300, Series.REDIRECTION, "Multiple Choices"),/*** 请求的资源已永久移动到新位置,客户端应该更新书签等。* {@code 301 Moved Permanently}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.4.2">HTTP/1.1: Semantics and Content, section 6.4.2</a>*/MOVED_PERMANENTLY(301, Series.REDIRECTION, "Moved Permanently"),/*** 请求的资源暂时从不同的 URI 响应请求。由于历史原因,很多浏览器会错误地将此视为临时重定向。* {@code 302 Found}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.4.3">HTTP/1.1: Semantics and Content, section 6.4.3</a>*/FOUND(302, Series.REDIRECTION, "Found"),/*** 请求的资源暂时从不同的 URI 响应请求。由于历史原因,很多浏览器会错误地将此视为临时重定向。* {@code 302 Moved Temporarily}.* @see <a href="https://tools.ietf.org/html/rfc1945#section-9.3">HTTP/1.0, section 9.3</a>* @deprecated in favor of {@link #FOUND} which will be returned from {@code HttpStatus.valueOf(302)}*///@Deprecated//MOVED_TEMPORARILY(302, Series.REDIRECTION, "Moved Temporarily"),/*** 建议客户端访问另一个 URL 来获取所请求的资源。* {@code 303 See Other}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.4.4">HTTP/1.1: Semantics and Content, section 6.4.4</a>*/SEE_OTHER(303, Series.REDIRECTION, "See Other"),/*** 资源未修改。通常用于条件 GET 请求,服务器告诉客户端资源自上次请求以来没有变化。* {@code 304 Not Modified}.** @see <a href="https://tools.ietf.org/html/rfc7232#section-4.1">HTTP/1.1: Conditional Requests, section 4.1</a>*/NOT_MODIFIED(304, Series.REDIRECTION, "Not Modified"),/*** 请求的资源必须通过代理访问。很少使用。* {@code 305 Use Proxy}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.4.5">HTTP/1.1: Semantics and Content, section 6.4.5</a>* @deprecated due to security concerns regarding in-band configuration of a proxy*/@DeprecatedUSE_PROXY(305, Series.REDIRECTION, "Use Proxy"),/*** 请求的资源临时从不同的 URI 响应请求。除了不允许改变请求方法外,其他行为类似于 302。* {@code 307 Temporary Redirect}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.4.7">HTTP/1.1: Semantics and Content, section 6.4.7</a>*/TEMPORARY_REDIRECT(307, Series.REDIRECTION, "Temporary Redirect"),/*** 求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。* {@code 308 Permanent Redirect}.** @see <a href="https://tools.ietf.org/html/rfc7238">RFC 7238</a>*/PERMANENT_REDIRECT(308, Series.REDIRECTION, "Permanent Redirect"),// --- 4xx Client Error ---/*** 请求无效或格式错误* {@code 400 Bad Request}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.1">HTTP/1.1: Semantics and Content, section 6.5.1</a>*/BAD_REQUEST(400, Series.CLIENT_ERROR, "Bad Request"),/*** 请求要求用户身份验证。如果身份验证失败或未提供,则返回此状态码。* {@code 401 Unauthorized}.** @see <a href="https://tools.ietf.org/html/rfc7235#section-3.1">HTTP/1.1: Authentication, section 3.1</a>*/UNAUTHORIZED(401, Series.CLIENT_ERROR, "Unauthorized"),/*** 保留用于未来使用。目前很少使用。* {@code 402 Payment Required}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.2">HTTP/1.1: Semantics and Content, section 6.5.2</a>*/PAYMENT_REQUIRED(402, Series.CLIENT_ERROR, "Payment Required"),/*** 服务器理解请求,但是拒绝执行它。权限不足时常见。* {@code 403 Forbidden}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.3">HTTP/1.1: Semantics and Content, section 6.5.3</a>*/FORBIDDEN(403, Series.CLIENT_ERROR, "Forbidden"),/*** 请求的资源不存在。* {@code 404 Not Found}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.4">HTTP/1.1: Semantics and Content, section 6.5.4</a>*/NOT_FOUND(404, Series.CLIENT_ERROR, "Not Found"),/*** 请求的方法对于目标资源是不允许的。* {@code 405 Method Not Allowed}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.5">HTTP/1.1: Semantics and Content, section 6.5.5</a>*/METHOD_NOT_ALLOWED(405, Series.CLIENT_ERROR, "Method Not Allowed"),/*** 服务器无法根据客户端请求的内容特性生成相应的内容。* {@code 406 Not Acceptable}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.6">HTTP/1.1: Semantics and Content, section 6.5.6</a>*/NOT_ACCEPTABLE(406, Series.CLIENT_ERROR, "Not Acceptable"),/*** 类似于 401,但是指代理的身份验证失败。* {@code 407 Proxy Authentication Required}.** @see <a href="https://tools.ietf.org/html/rfc7235#section-3.2">HTTP/1.1: Authentication, section 3.2</a>*/PROXY_AUTHENTICATION_REQUIRED(407, Series.CLIENT_ERROR, "Proxy Authentication Required"),/*** 请求超时。* {@code 408 Request Timeout}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.7">HTTP/1.1: Semantics and Content, section 6.5.7</a>*/REQUEST_TIMEOUT(408, Series.CLIENT_ERROR, "Request Timeout"),/*** 请求冲突,比如尝试创建已有资源。* {@code 409 Conflict}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.8">HTTP/1.1: Semantics and Content, section 6.5.8</a>*/CONFLICT(409, Series.CLIENT_ERROR, "Conflict"),/*** 请求的资源在服务器上不再可用,而且没有任何已知的转发地址。* {@code 410 Gone}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.9">* HTTP/1.1: Semantics and Content, section 6.5.9</a>*/GONE(410, Series.CLIENT_ERROR, "Gone"),/*** 服务器拒绝处理缺少 Content-Length 头部字段的请求。* {@code 411 Length Required}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.10">* HTTP/1.1: Semantics and Content, section 6.5.10</a>*/LENGTH_REQUIRED(411, Series.CLIENT_ERROR, "Length Required"),/*** 服务器在验证请求中给出的前提条件时,发现该前提条件为假。* {@code 412 Precondition failed}.** @see <a href="https://tools.ietf.org/html/rfc7232#section-4.2">* HTTP/1.1: Conditional Requests, section 4.2</a>*/PRECONDITION_FAILED(412, Series.CLIENT_ERROR, "Precondition Failed"),/*** 请求实体太大,服务器拒绝处理。* {@code 413 Payload Too Large}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.11">* HTTP/1.1: Semantics and Content, section 6.5.11</a>* @since 4.1*/PAYLOAD_TOO_LARGE(413, Series.CLIENT_ERROR, "Payload Too Large"),/**** {@code 413 Request Entity Too Large}.* @see <a href="https://tools.ietf.org/html/rfc2616#section-10.4.14">HTTP/1.1, section 10.4.14</a>* @deprecated in favor of {@link #PAYLOAD_TOO_LARGE} which will be* returned from {@code HttpStatus.valueOf(413)}*///@Deprecated//REQUEST_ENTITY_TOO_LARGE(413, Series.CLIENT_ERROR, "Request Entity Too Large"),/*** 请求 URI 太长,服务器拒绝处理。* {@code 414 URI Too Long}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.12">* HTTP/1.1: Semantics and Content, section 6.5.12</a>* @since 4.1*/URI_TOO_LONG(414, Series.CLIENT_ERROR, "URI Too Long"),/*** 请求 URI 太长,服务器拒绝处理。* {@code 414 Request-URI Too Long}.* @see <a href="https://tools.ietf.org/html/rfc2616#section-10.4.15">HTTP/1.1, section 10.4.15</a>* @deprecated in favor of {@link #URI_TOO_LONG} which will be returned from {@code HttpStatus.valueOf(414)}*///@Deprecated//REQUEST_URI_TOO_LONG(414, Series.CLIENT_ERROR, "Request-URI Too Long"),/*** 对于请求中指定的实体,服务器不支持该媒体类型。* {@code 415 Unsupported Media Type}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.13">* HTTP/1.1: Semantics and Content, section 6.5.13</a>*/UNSUPPORTED_MEDIA_TYPE(415, Series.CLIENT_ERROR, "Unsupported Media Type"),/*** 客户端请求的范围无效。* {@code 416 Requested Range Not Satisfiable}.** @see <a href="https://tools.ietf.org/html/rfc7233#section-4.4">HTTP/1.1: Range Requests, section 4.4</a>*/REQUESTED_RANGE_NOT_SATISFIABLE(416, Series.CLIENT_ERROR, "Requested range not satisfiable"),/*** 服务器不能满足 Expect 请求头字段的要求。* {@code 417 Expectation Failed}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.5.14">* HTTP/1.1: Semantics and Content, section 6.5.14</a>*/EXPECTATION_FAILED(417, Series.CLIENT_ERROR, "Expectation Failed"),/*** {@code 418 I'm a teapot}.** @see <a href="https://tools.ietf.org/html/rfc2324#section-2.3.2">HTCPCP/1.0</a>*/I_AM_A_TEAPOT(418, Series.CLIENT_ERROR, "I'm a teapot"),/*** @deprecated See* <a href="https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">* WebDAV Draft Changes</a>*/@DeprecatedINSUFFICIENT_SPACE_ON_RESOURCE(419, Series.CLIENT_ERROR, "Insufficient Space On Resource"),/*** @deprecated See* <a href="https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">* WebDAV Draft Changes</a>*/@DeprecatedMETHOD_FAILURE(420, Series.CLIENT_ERROR, "Method Failure"),/*** 请求被错误地发给了无法产生响应的服务器。** @deprecated See <a href="https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">* WebDAV Draft Changes</a>*/@DeprecatedDESTINATION_LOCKED(421, Series.CLIENT_ERROR, "Destination Locked"),/*** 请求格式正确,但由于语义错误而无法处理。* {@code 422 Unprocessable Entity}.** @see <a href="https://tools.ietf.org/html/rfc4918#section-11.2">WebDAV</a>*/UNPROCESSABLE_ENTITY(422, Series.CLIENT_ERROR, "Unprocessable Entity"),/*** 当前资源被锁定。* {@code 423 Locked}.** @see <a href="https://tools.ietf.org/html/rfc4918#section-11.3">WebDAV</a>*/LOCKED(423, Series.CLIENT_ERROR, "Locked"),/*** 请求失败依赖于另一个操作,而那个操作失败了。* {@code 424 Failed Dependency}.** @see <a href="https://tools.ietf.org/html/rfc4918#section-11.4">WebDAV</a>*/FAILED_DEPENDENCY(424, Series.CLIENT_ERROR, "Failed Dependency"),/*** 服务器不愿意风险处理可能会被重播的请求。* {@code 425 Too Early}.** @see <a href="https://tools.ietf.org/html/rfc8470">RFC 8470</a>* @since 5.2*/TOO_EARLY(425, Series.CLIENT_ERROR, "Too Early"),/*** 客户端应当切换协议。服务器关闭连接以强制客户端切换。* {@code 426 Upgrade Required}.** @see <a href="https://tools.ietf.org/html/rfc2817#section-6">Upgrading to TLS Within HTTP/1.1</a>*/UPGRADE_REQUIRED(426, Series.CLIENT_ERROR, "Upgrade Required"),/*** 服务器要求请求附带前提条件。* {@code 428 Precondition Required}.** @see <a href="https://tools.ietf.org/html/rfc6585#section-3">Additional HTTP Status Codes</a>*/PRECONDITION_REQUIRED(428, Series.CLIENT_ERROR, "Precondition Required"),/*** 用户在给定时间内发送了太多的请求(限流)。* {@code 429 Too Many Requests}.** @see <a href="https://tools.ietf.org/html/rfc6585#section-4">Additional HTTP Status Codes</a>*/TOO_MANY_REQUESTS(429, Series.CLIENT_ERROR, "Too Many Requests"),/*** 服务器拒绝处理,因为请求头字段太大。* {@code 431 Request Header Fields Too Large}.** @see <a href="https://tools.ietf.org/html/rfc6585#section-5">Additional HTTP Status Codes</a>*/REQUEST_HEADER_FIELDS_TOO_LARGE(431, Series.CLIENT_ERROR, "Request Header Fields Too Large"),/*** 请求非法,通常是由于法律原因。* {@code 451 Unavailable For Legal Reasons}.** @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-legally-restricted-status-04">* An HTTP Status Code to Report Legal Obstacles</a>* @since 4.3*/UNAVAILABLE_FOR_LEGAL_REASONS(451, Series.CLIENT_ERROR, "Unavailable For Legal Reasons"),// --- 5xx Server Error ---/*** 服务器遇到了意外情况,阻止了其完成请求。* {@code 500 Internal Server Error}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.6.1">HTTP/1.1: Semantics and Content, section 6.6.1</a>*/INTERNAL_SERVER_ERROR(500, Series.SERVER_ERROR, "Internal Server Error"),/*** 服务器不支持实现请求所需的功能。* {@code 501 Not Implemented}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.6.2">HTTP/1.1: Semantics and Content, section 6.6.2</a>*/NOT_IMPLEMENTED(501, Series.SERVER_ERROR, "Not Implemented"),/*** 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效响应。* {@code 502 Bad Gateway}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.6.3">HTTP/1.1: Semantics and Content, section 6.6.3</a>*/BAD_GATEWAY(502, Series.SERVER_ERROR, "Bad Gateway"),/*** 服务器当前无法处理请求。这可能是由于过载或维护。* {@code 503 Service Unavailable}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.6.4">HTTP/1.1: Semantics and Content, section 6.6.4</a>*/SERVICE_UNAVAILABLE(503, Series.SERVER_ERROR, "Service Unavailable"),/*** 作为网关或代理工作的服务器未能及时从上游服务器获得响应。* {@code 504 Gateway Timeout}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.6.5">HTTP/1.1: Semantics and Content, section 6.6.5</a>*/GATEWAY_TIMEOUT(504, Series.SERVER_ERROR, "Gateway Timeout"),/*** 服务器不支持请求中使用的 HTTP 版本。* {@code 505 HTTP Version Not Supported}.** @see <a href="https://tools.ietf.org/html/rfc7231#section-6.6.6">HTTP/1.1: Semantics and Content, section 6.6.6</a>*/HTTP_VERSION_NOT_SUPPORTED(505, Series.SERVER_ERROR, "HTTP Version not supported"),/*** 服务器有一个内部配置错误:所选的变体资源本身可以是协商过程的一部分。* {@code 506 Variant Also Negotiates}** @see <a href="https://tools.ietf.org/html/rfc2295#section-8.1">Transparent Content Negotiation</a>*/VARIANT_ALSO_NEGOTIATES(506, Series.SERVER_ERROR, "Variant Also Negotiates"),/*** 服务器无法存储完成请求所需要的内容。WebDAV 使用。* {@code 507 Insufficient Storage}** @see <a href="https://tools.ietf.org/html/rfc4918#section-11.5">WebDAV</a>*/INSUFFICIENT_STORAGE(507, Series.SERVER_ERROR, "Insufficient Storage"),/*** 服务器检测到了无限循环。* {@code 508 Loop Detected}** @see <a href="https://tools.ietf.org/html/rfc5842#section-7.2">WebDAV Binding Extensions</a>*/LOOP_DETECTED(508, Series.SERVER_ERROR, "Loop Detected"),/*** {@code 509 Bandwidth Limit Exceeded}*/BANDWIDTH_LIMIT_EXCEEDED(509, Series.SERVER_ERROR, "Bandwidth Limit Exceeded"),/*** 请求需要进一步扩展才能完成。* {@code 510 Not Extended}** @see <a href="https://tools.ietf.org/html/rfc2774#section-7">HTTP Extension Framework</a>*/NOT_EXTENDED(510, Series.SERVER_ERROR, "Not Extended"),/*** 客户端需要进行身份验证以获得网络访问权限* {@code 511 Network Authentication Required}.** @see <a href="https://tools.ietf.org/html/rfc6585#section-6">Additional HTTP Status Codes</a>*/NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required");private static final HttpClientStatus[] VALUES;static {VALUES = values();}private final int value;private final Series series;private final String reasonPhrase;HttpClientStatus(int value, Series series, String reasonPhrase) {this.value = value;this.series = series;this.reasonPhrase = reasonPhrase;}/*** Return the integer value of this status code.*/public int value() {return this.value;}/*** Return the HTTP status series of this status code.** @see Series*/public Series series() {return this.series;}/*** Return the reason phrase of this status code.*/public String getReasonPhrase() {return this.reasonPhrase;}/*** Whether this status code is in the HTTP series* <p>This is a shortcut for checking the value of {@link #series()}.** @see #series()* @since 4.0*/public boolean is1xxInformational() {return (series() == Series.INFORMATIONAL);}/*** Whether this status code is in the HTTP series* <p>This is a shortcut for checking the value of {@link #series()}.** @see #series()* @since 4.0*/public boolean is2xxSuccessful() {return (series() == Series.SUCCESSFUL);}/*** Whether this status code is in the HTTP series* <p>This is a shortcut for checking the value of {@link #series()}.** @see #series()* @since 4.0*/public boolean is3xxRedirection() {return (series() == Series.REDIRECTION);}/*** Whether this status code is in the HTTP series* <p>This is a shortcut for checking the value of {@link #series()}.** @see #series()* @since 4.0*/public boolean is4xxClientError() {return (series() == Series.CLIENT_ERROR);}/*** Whether this status code is in the HTTP series* <p>This is a shortcut for checking the value of {@link #series()}.** @see #series()* @since 4.0*/public boolean is5xxServerError() {return (series() == Series.SERVER_ERROR);}/*** Whether this status code is in the HTTP series* <p>This is a shortcut for checking the value of {@link #series()}.** @see #is4xxClientError()* @see #is5xxServerError()* @since 5.0*/public boolean isError() {return (is4xxClientError() || is5xxServerError());}/*** Return a string representation of this status code.*/@Overridepublic String toString() {return this.value + " " + name();}/*** Return the {@code HttpStatus} enum constant with the specified numeric value.** @param statusCode the numeric value of the enum to be returned* @return the enum constant with the specified numeric value* @throws IllegalArgumentException if this enum has no constant for the specified numeric value*/public static HttpClientStatus valueOf(int statusCode) {HttpClientStatus status = resolve(statusCode);if (status == null) {throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");}return status;}/*** Resolve the given status code to an {@code HttpStatus}, if possible.** @param statusCode the HTTP status code (potentially non-standard)* @return the corresponding {@code HttpStatus}, or {@code null} if not found* @since 5.0*/@Nullablepublic static HttpClientStatus resolve(int statusCode) {// Use cached VALUES instead of values() to prevent array allocation.for (HttpClientStatus status : VALUES) {if (status.value == statusCode) {return status;}}return null;}/*** Enumeration of HTTP status series.* <p>Retrievable via {@link HttpStatus#series()}.*/public enum Series {INFORMATIONAL(1),SUCCESSFUL(2),REDIRECTION(3),CLIENT_ERROR(4),SERVER_ERROR(5);private final int value;Series(int value) {this.value = value;}/*** Return the integer value of this status series. Ranges from 1 to 5.*/public int value() {return this.value;}/*** Return the {@code Series} enum constant for the supplied {@code HttpStatus}.** @param status a standard HTTP status enum constant* @return the {@code Series} enum constant for the supplied {@code HttpStatus}* @deprecated as of 5.3, in favor of invoking {@link HttpStatus#series()} directly*/@Deprecatedpublic static Series valueOf(HttpClientStatus status) {return status.series;}/*** Return the {@code Series} enum constant for the supplied status code.** @param statusCode the HTTP status code (potentially non-standard)* @return the {@code Series} enum constant for the supplied status code* @throws IllegalArgumentException if this enum has no corresponding constant*/public static Series valueOf(int statusCode) {Series series = resolve(statusCode);if (series == null) {throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");}return series;}/*** Resolve the given status code to an {@code Series}, if possible.** @param statusCode the HTTP status code (potentially non-standard)* @return the corresponding {@code Series}, or {@code null} if not found* @since 5.1.3*/@Nullablepublic static Series resolve(int statusCode) {int seriesCode = statusCode / 100;for (Series series : values()) {if (series.value == seriesCode) {return series;}}return null;}}}
- 封装HttpClientHeaders接口,避免框架替换的时候需要整改整个项目
package com.zzc.component.http;import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;public class HttpClientHeaders implements Map<String, String>, Serializable {/*** The HTTP {@code Accept} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">Section 5.3.2 of RFC 7231</a>*/public static final String ACCEPT = "Accept";/*** The HTTP {@code Accept-Charset} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.3">Section 5.3.3 of RFC 7231</a>*/public static final String ACCEPT_CHARSET = "Accept-Charset";/*** The HTTP {@code Accept-Encoding} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.4">Section 5.3.4 of RFC 7231</a>*/public static final String ACCEPT_ENCODING = "Accept-Encoding";/*** The HTTP {@code Accept-Language} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.5">Section 5.3.5 of RFC 7231</a>*/public static final String ACCEPT_LANGUAGE = "Accept-Language";/*** The HTTP {@code Accept-Patch} header field name.* @since 5.3.6* @see <a href="https://tools.ietf.org/html/rfc5789#section-3.1">Section 3.1 of RFC 5789</a>*/public static final String ACCEPT_PATCH = "Accept-Patch";/*** The HTTP {@code Accept-Ranges} header field name.* @see <a href="https://tools.ietf.org/html/rfc7233#section-2.3">Section 5.3.5 of RFC 7233</a>*/public static final String ACCEPT_RANGES = "Accept-Ranges";/*** The CORS {@code Access-Control-Allow-Credentials} response header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";/*** The CORS {@code Access-Control-Allow-Headers} response header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";/*** The CORS {@code Access-Control-Allow-Methods} response header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";/*** The CORS {@code Access-Control-Allow-Origin} response header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";/*** The CORS {@code Access-Control-Expose-Headers} response header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";/*** The CORS {@code Access-Control-Max-Age} response header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";/*** The CORS {@code Access-Control-Request-Headers} request header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";/*** The CORS {@code Access-Control-Request-Method} request header field name.* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>*/public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";/*** The HTTP {@code Age} header field name.* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.1">Section 5.1 of RFC 7234</a>*/public static final String AGE = "Age";/*** The HTTP {@code Allow} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.4.1">Section 7.4.1 of RFC 7231</a>*/public static final String ALLOW = "Allow";/*** The HTTP {@code Authorization} header field name.* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.2">Section 4.2 of RFC 7235</a>*/public static final String AUTHORIZATION = "Authorization";/*** The HTTP {@code Cache-Control} header field name.* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2">Section 5.2 of RFC 7234</a>*/public static final String CACHE_CONTROL = "Cache-Control";/*** The HTTP {@code Connection} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-6.1">Section 6.1 of RFC 7230</a>*/public static final String CONNECTION = "Connection";/*** The HTTP {@code Content-Encoding} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.2.2">Section 3.1.2.2 of RFC 7231</a>*/public static final String CONTENT_ENCODING = "Content-Encoding";/*** The HTTP {@code Content-Disposition} header field name.* @see <a href="https://tools.ietf.org/html/rfc6266">RFC 6266</a>*/public static final String CONTENT_DISPOSITION = "Content-Disposition";/*** The HTTP {@code Content-Language} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.3.2">Section 3.1.3.2 of RFC 7231</a>*/public static final String CONTENT_LANGUAGE = "Content-Language";/*** The HTTP {@code Content-Length} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3.2">Section 3.3.2 of RFC 7230</a>*/public static final String CONTENT_LENGTH = "Content-Length";/*** The HTTP {@code Content-Location} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.4.2">Section 3.1.4.2 of RFC 7231</a>*/public static final String CONTENT_LOCATION = "Content-Location";/*** The HTTP {@code Content-Range} header field name.* @see <a href="https://tools.ietf.org/html/rfc7233#section-4.2">Section 4.2 of RFC 7233</a>*/public static final String CONTENT_RANGE = "Content-Range";/*** The HTTP {@code Content-Type} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.5">Section 3.1.1.5 of RFC 7231</a>*/public static final String CONTENT_TYPE = "Content-Type";/*** The HTTP {@code Cookie} header field name.* @see <a href="https://tools.ietf.org/html/rfc2109#section-4.3.4">Section 4.3.4 of RFC 2109</a>*/public static final String COOKIE = "Cookie";/*** The HTTP {@code Date} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.2">Section 7.1.1.2 of RFC 7231</a>*/public static final String DATE = "Date";/*** The HTTP {@code ETag} header field name.* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>*/public static final String ETAG = "ETag";/*** The HTTP {@code Expect} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.1.1">Section 5.1.1 of RFC 7231</a>*/public static final String EXPECT = "Expect";/*** The HTTP {@code Expires} header field name.* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.3">Section 5.3 of RFC 7234</a>*/public static final String EXPIRES = "Expires";/*** The HTTP {@code From} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.5.1">Section 5.5.1 of RFC 7231</a>*/public static final String FROM = "From";/*** The HTTP {@code Host} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-5.4">Section 5.4 of RFC 7230</a>*/public static final String HOST = "Host";/*** The HTTP {@code If-Match} header field name.* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.1">Section 3.1 of RFC 7232</a>*/public static final String IF_MATCH = "If-Match";/*** The HTTP {@code If-Modified-Since} header field name.* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.3">Section 3.3 of RFC 7232</a>*/public static final String IF_MODIFIED_SINCE = "If-Modified-Since";/*** The HTTP {@code If-None-Match} header field name.* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.2">Section 3.2 of RFC 7232</a>*/public static final String IF_NONE_MATCH = "If-None-Match";/*** The HTTP {@code If-Range} header field name.* @see <a href="https://tools.ietf.org/html/rfc7233#section-3.2">Section 3.2 of RFC 7233</a>*/public static final String IF_RANGE = "If-Range";/*** The HTTP {@code If-Unmodified-Since} header field name.* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.4">Section 3.4 of RFC 7232</a>*/public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";/*** The HTTP {@code Last-Modified} header field name.* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.2">Section 2.2 of RFC 7232</a>*/public static final String LAST_MODIFIED = "Last-Modified";/*** The HTTP {@code Link} header field name.* @see <a href="https://tools.ietf.org/html/rfc5988">RFC 5988</a>*/public static final String LINK = "Link";/*** The HTTP {@code Location} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.2">Section 7.1.2 of RFC 7231</a>*/public static final String LOCATION = "Location";/*** The HTTP {@code Max-Forwards} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.1.2">Section 5.1.2 of RFC 7231</a>*/public static final String MAX_FORWARDS = "Max-Forwards";/*** The HTTP {@code Origin} header field name.* @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454</a>*/public static final String ORIGIN = "Origin";/*** The HTTP {@code Pragma} header field name.* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.4">Section 5.4 of RFC 7234</a>*/public static final String PRAGMA = "Pragma";/*** The HTTP {@code Proxy-Authenticate} header field name.* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.3">Section 4.3 of RFC 7235</a>*/public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";/*** The HTTP {@code Proxy-Authorization} header field name.* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.4">Section 4.4 of RFC 7235</a>*/public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";/*** The HTTP {@code Range} header field name.* @see <a href="https://tools.ietf.org/html/rfc7233#section-3.1">Section 3.1 of RFC 7233</a>*/public static final String RANGE = "Range";/*** The HTTP {@code Referer} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.5.2">Section 5.5.2 of RFC 7231</a>*/public static final String REFERER = "Referer";/*** The HTTP {@code Retry-After} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.3">Section 7.1.3 of RFC 7231</a>*/public static final String RETRY_AFTER = "Retry-After";/*** The HTTP {@code Server} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.4.2">Section 7.4.2 of RFC 7231</a>*/public static final String SERVER = "Server";/*** The HTTP {@code Set-Cookie} header field name.* @see <a href="https://tools.ietf.org/html/rfc2109#section-4.2.2">Section 4.2.2 of RFC 2109</a>*/public static final String SET_COOKIE = "Set-Cookie";/*** The HTTP {@code Set-Cookie2} header field name.* @see <a href="https://tools.ietf.org/html/rfc2965">RFC 2965</a>*/public static final String SET_COOKIE2 = "Set-Cookie2";/*** The HTTP {@code TE} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-4.3">Section 4.3 of RFC 7230</a>*/public static final String TE = "TE";/*** The HTTP {@code Trailer} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-4.4">Section 4.4 of RFC 7230</a>*/public static final String TRAILER = "Trailer";/*** The HTTP {@code Transfer-Encoding} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3.1">Section 3.3.1 of RFC 7230</a>*/public static final String TRANSFER_ENCODING = "Transfer-Encoding";/*** The HTTP {@code Upgrade} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-6.7">Section 6.7 of RFC 7230</a>*/public static final String UPGRADE = "Upgrade";/*** The HTTP {@code User-Agent} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.5.3">Section 5.5.3 of RFC 7231</a>*/public static final String USER_AGENT = "User-Agent";/*** The HTTP {@code Vary} header field name.* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.4">Section 7.1.4 of RFC 7231</a>*/public static final String VARY = "Vary";/*** The HTTP {@code Via} header field name.* @see <a href="https://tools.ietf.org/html/rfc7230#section-5.7.1">Section 5.7.1 of RFC 7230</a>*/public static final String VIA = "Via";/*** The HTTP {@code Warning} header field name.* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.5">Section 5.5 of RFC 7234</a>*/public static final String WARNING = "Warning";/*** The HTTP {@code WWW-Authenticate} header field name.* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.1">Section 4.1 of RFC 7235</a>*/public static final String WWW_AUTHENTICATE = "WWW-Authenticate";private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");final Map<String, String> headers;public HttpClientHeaders() {this(new HashMap<>());}public HttpClientHeaders(Map<String, String> headers) {this.headers = headers;}@Overridepublic int size() {return this.headers.size();}@Overridepublic boolean isEmpty() {return this.headers.isEmpty();}@Overridepublic boolean containsKey(Object key) {return this.headers.containsKey(key);}@Overridepublic boolean containsValue(Object value) {return this.headers.containsValue(value);}@Overridepublic String get(Object key) {return this.headers.get(key);}@Overridepublic String put(String key, String value) {return this.headers.put(key, value);}@Overridepublic String remove(Object key) {return this.headers.remove(key);}@Overridepublic void putAll(Map<? extends String, ? extends String> m) {this.headers.putAll(m);}@Overridepublic void clear() {this.headers.clear();}@Overridepublic Set<String> keySet() {return new HashSet<>(this.headers.keySet());}@Overridepublic Collection<String> values() {return new HashSet<>(this.headers.values());}@Overridepublic Set<Entry<String, String>> entrySet() {return new HashSet<>(this.headers.entrySet());}public static HttpClientHeadersBuilder builder() {return new HttpClientHeadersBuilder();}public static class HttpClientHeadersBuilder {private final Map<String, String> headers;public HttpClientHeadersBuilder() {this(new HashMap<>());}public HttpClientHeadersBuilder(Map<String, String> headers) {this.headers = headers;}public HttpClientHeadersBuilder addHeaders(Map<String, String> headers) {this.headers.putAll(headers);return this;}public HttpClientHeadersBuilder addHeader(String key, String value) {this.headers.put(key, value);return this;}public HttpClientHeaders build() {return new HttpClientHeaders(this.headers);}}}
- 封装响应的消息接口
package com.zzc.component.http;import java.io.IOException;
import java.io.InputStream;public interface HttpClientResponse extends AutoCloseable {InputStream getBody() throws IOException;HttpClientStatus getStatusCode() throws IOException;int getRawStatusCode() throws IOException;String getStatusText() throws IOException;@Overridevoid close();}
- 封装HttpClientResponseExtractor作为处理InputStream等操作的接口
package com.zzc.component.http;import java.io.IOException;public interface HttpClientResponseExtractor<T> {T extractData(HttpClientResponse response) throws IOException;}
- 封装HttpClientResponseEntity作为接收消息体
package com.zzc.component.http;public class HttpClientResponseEntity<T> {private final T body;private final Object status;private final HttpClientHeaders headers;public HttpClientResponseEntity(T body) {this(body, null, null);}public HttpClientResponseEntity(T body, HttpClientHeaders headers, Object status) {this.body = body;this.status = status;this.headers = headers;}public HttpClientHeaders getHeaders() {return this.headers;}public T getBody() {return this.body;}public Object getStatus() {return this.status;}public HttpClientStatus getStatusCode() {if (status instanceof HttpClientStatus) {return (HttpClientStatus) this.status;} else if (status instanceof Integer) {return HttpClientStatus.valueOf((Integer) this.status);} else {System.out.println(status.getClass());return HttpClientStatus.INTERNAL_SERVER_ERROR;}}public int getStatusCodeValue() {if (this.status instanceof HttpClientStatus) {return ((HttpClientStatus) this.status).value();} else if (this.status instanceof Integer) {return ((Integer) this.status);} else {return HttpClientStatus.INTERNAL_SERVER_ERROR.value();}}}
- 创建泛型保存抽象类,用于解决多重泛型对象序列问题(内部先序列化为String.class做法性能一般)
package com.zzc.component.http;import com.alibaba.fastjson2.TypeReference;
public abstract class Type<T> extends TypeReference<T> {
}
- 通过SpringBoot的方式进行配置
package com.zzc.component.http.httpclient;import com.zzc.component.http.HttpClientProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;@Slf4j
@Configuration
@ConditionalOnClass(value = {RestTemplate.class, CloseableHttpClient.class})
@EnableConfigurationProperties(HttpClientProperties.class)
public class HttpClientFactory {/*** 指定域名地址的请求配置*/private static Map<String, RequestConfig> TARGET_HOST_REQUEST_CONFIG = new HashMap<>();/*** 指定域名地址的请求头*/private static Map<String, Map<String, String>> TARGET_HOST_HEADERS = new HashMap<>();private final HttpClientProperties httpClientProperties;public HttpClientFactory(HttpClientProperties httpClientProperties) {this.httpClientProperties = httpClientProperties;}protected HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();factory.setConnectTimeout(httpClientProperties.getConnectTimeout());factory.setReadTimeout(httpClientProperties.getReadTimeout());factory.setConnectionRequestTimeout(httpClientProperties.getRequestTimeout());factory.setHttpClient(httpClient());return factory;}public HttpClient httpClient() {HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();try {Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", SSLConnectionSocketFactory.getSocketFactory()).build();PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);//设置全局最大连接数httpClientConnectionManager.setMaxTotal(httpClientProperties.getConnMaxTotal());//设置全局默认最大并发数httpClientConnectionManager.setDefaultMaxPerRoute(httpClientProperties.getMaxPerRoute());//设置指定域名的最大并发数setTargetHostMaxPerRoute(httpClientConnectionManager);//设置链接管理器httpClientBuilder.setConnectionManager(httpClientConnectionManager);//设置默认请求handlerhttpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(httpClientProperties.getRetryNum(), httpClientProperties.getRetryNum() != 0));//设置全局默认请求头List<Header> headers = genHeaders();if (!headers.isEmpty()) {httpClientBuilder.setDefaultHeaders(headers);}//设置指定地址的Keep-Alive策略httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy());//添加请求拦截器,默认请求头,请求超时设置等httpClientBuilder.addInterceptorFirst(httpRequestInterceptor());//设置定时关闭无效链接httpClientBuilder.evictIdleConnections(30L, TimeUnit.SECONDS);return httpClientBuilder.build();} catch (Exception e) {log.error("init http factory error", e);}return null;}private void setTargetHostMaxPerRoute(PoolingHttpClientConnectionManager httpClientConnectionManager) {List<HttpClientProperties.TargetHostConfig> targetHosts = httpClientProperties.getTargetHosts();if (targetHosts != null && !targetHosts.isEmpty()) {for (HttpClientProperties.TargetHostConfig config : targetHosts) {if (config.getHostname() == null || config.getMaxPerRoute() == null) {//地址没有配置的则跳过continue;}try {HttpHost httpHost = HttpHost.create(config.getHostname());httpClientConnectionManager.setMaxPerRoute(new HttpRoute(httpHost), config.getMaxPerRoute());} catch (Exception e) {log.error("setMaxPerRoute error. hostname:{}", config.getHostname(), e);}}}}private HttpRequestInterceptor httpRequestInterceptor() {initialize();//添加请求拦截器,默认请求头,请求超时设置等return (httpRequest, httpContext) -> {HttpHost httpHost = (HttpHost) httpContext.getAttribute(HttpClientContext.HTTP_TARGET_HOST);String host = httpHost.toHostString();String hostName = httpHost.getHostName();int port = httpHost.getPort();log.debug("Interceptors hostName:{}, port:{}, host:{}", hostName, port, host);if (TARGET_HOST_REQUEST_CONFIG.containsKey(host)) {RequestConfig requestConfig = TARGET_HOST_REQUEST_CONFIG.get(host);httpContext.setAttribute(HttpClientContext.REQUEST_CONFIG, requestConfig);}if (TARGET_HOST_HEADERS.containsKey(host)) {TARGET_HOST_HEADERS.get(host).forEach(httpRequest::addHeader);}};}/*** 初始化相关配置*/private void initialize() {List<HttpClientProperties.TargetHostConfig> targetHosts = httpClientProperties.getTargetHosts();if (targetHosts != null && !targetHosts.isEmpty()) {for (HttpClientProperties.TargetHostConfig config : targetHosts) {if (config.getHostname() == null) {//地址没有配置的则跳过continue;}log.info("addInterceptors config host:{}", config.getHostname());//超时时间等配置,如果没有配置则使用全局配置Integer connectTimeout = config.getConnectTimeout() == null ? httpClientProperties.getConnectTimeout() : config.getConnectTimeout();Integer readTimeout = config.getReadTimeout() == null ? httpClientProperties.getReadTimeout() : config.getReadTimeout();Integer requestTimeout = config.getRequestTimeout() == null ? httpClientProperties.getRequestTimeout() : config.getRequestTimeout();RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connectTimeout == null ? -1 : connectTimeout).setSocketTimeout(readTimeout == null ? -1 : readTimeout).setConnectionRequestTimeout(requestTimeout == null ? -1 : requestTimeout).build();TARGET_HOST_REQUEST_CONFIG.put(config.getHostname(), requestConfig);if (config.getHeaders() != null && !config.getHeaders().isEmpty()) {TARGET_HOST_HEADERS.computeIfAbsent(config.getHostname(), k -> new HashMap<>());config.getHeaders().forEach((key, value) -> TARGET_HOST_HEADERS.get(config.getHostname()).put(key, value));}if (config.getKeepAliveTime() != null) {TARGET_HOST_HEADERS.computeIfAbsent(config.getHostname(), k -> new HashMap<>());TARGET_HOST_HEADERS.get(config.getHostname()).put("Connection", "Keep-Alive");TARGET_HOST_HEADERS.get(config.getHostname()).put("Keep-Alive", "timeout=" + config.getKeepAliveTime());}}}}private ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {return ((httpResponse, httpContext) -> {HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.headerIterator(HTTP.CONN_KEEP_ALIVE));while (it.hasNext()) {HeaderElement he = it.nextElement();String name = he.getName();String value = he.getValue();if (value != null && "timeout".equalsIgnoreCase(name)) {try {return Long.parseLong(value) * 1000L;} catch (NumberFormatException ignore) {log.error("resolve Keep-Alive timeout", ignore);}}}HttpHost target = (HttpHost) httpContext.getAttribute(HttpClientContext.HTTP_TARGET_HOST);log.info("connectionKeepAliveStrategy target host:{}", target.getHostName());//如果请求的目标地址单独做了配置,使用以下的设置Optional<Map.Entry<String, Integer>> any = Optional.ofNullable(httpClientProperties.getKeepAliveTargetHost()).orElseGet(HashMap::new).entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(target.getHostName())).findAny();int keepAliveTime = httpClientProperties.getKeepAliveTime() == null ? 60 : httpClientProperties.getKeepAliveTime();return any.map(e -> e.getValue() * 1000L).orElse(keepAliveTime * 1000L);});}private List<Header> genHeaders() {List<Header> headers = new ArrayList<>();if (httpClientProperties.getHeaders() == null) {log.warn("init header is null");return headers;}for (Map.Entry<String, String> entry : httpClientProperties.getHeaders().entrySet()) {headers.add(new BasicHeader(entry.getKey(), entry.getValue()));}return headers;}private void modifyDefaultCharset(RestTemplate restTemplate) {List<HttpMessageConverter<?>> converterList = restTemplate.getMessageConverters();HttpMessageConverter<?> converterTarget = null;for (HttpMessageConverter<?> item : converterList) {if (StringHttpMessageConverter.class == item.getClass()) {log.info("HttpMessageConvert exist null");converterTarget = item;break;}}if (null != converterTarget) {converterList.remove(converterTarget);}Charset defaultCharset = Charset.forName(httpClientProperties.getCharset());converterList.add(1, new StringHttpMessageConverter(defaultCharset));}@Bean@ConditionalOnMissingBean(value = {RestTemplate.class})public RestTemplate httpClientRestTemplate() {log.info("init httpClientRestTemplate.");RestTemplate restTemplate = new RestTemplate(httpComponentsClientHttpRequestFactory());modifyDefaultCharset(restTemplate);restTemplate.setErrorHandler(new DefaultResponseErrorHandler());return restTemplate;}
}
- 定义接口HttpClientComponent
package com.zzc.component.http;import java.util.Map;public interface HttpClientComponent {/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求头,key-value格式,若传入的header为空,则使用默认的header 请求header默认"Content-Type", "application/json;charset=utf-8"* @param body 请求消息体* @param clazz 返回类型* @return* @param <T>*/<T> T post(String url, HttpClientHeaders headers, Object body, Class<T> clazz);/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param body 请求消息体* @param clazz 返回类型* @return* @param <T>*/<T> T post(String url, Object body, Class<T> clazz);/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param body 请求消息体* @param clazz HttpClientResponseEntity<返回类型>* @return*/<T> HttpClientResponseEntity<T> postEntity(String url, Object body, Class<T> clazz);/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求headers* @param body 请求消息体* @param clazz HttpClientResponseEntity<返回类型>* @return*/<T> HttpClientResponseEntity<T> postEntity(String url, HttpClientHeaders headers, Object body, Class<T> clazz);/*** get请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求头,key-value格式* @param uriVariables 不在url中的参数,将在该接口框架中进行处理* @param clazz 返回类型* @return* @param <T>*/<T> T get(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Class<T> clazz);/*** get请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求头,key-value格式* @param clazz 返回类型* @return* @param <T>*/<T> T get(String url, HttpClientHeaders headers, Class<T> clazz);/*** get请求* @param url 请求url,可以是域名,也可以是ip* @param clazz 返回类型* @return* @param <T>*/<T> T get(String url, Class<T> clazz);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param clazz 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, Class<T> clazz);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param headers 请求header* @param clazz 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Class<T> clazz);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param uriVariables url请求拼接的参数* @param clazz 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, Map<String, ?> uriVariables, Class<T> clazz);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param headers 请求header* @param uriVariables url参数* @param clazz 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Class<T> clazz);//-------------------------------------------------------------------------------------------------------------------/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求头,key-value格式,若传入的header为空,则使用默认的header 请求header默认"Content-Type", "application/json;charset=utf-8"* @param body 请求消息体* @param type 返回类型* @return* @param <T>*/<T> T post(String url, HttpClientHeaders headers, Object body, Type<T> type);/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param body 请求消息体* @param type 返回类型* @return* @param <T>*/<T> T post(String url, Object body, Type<T> type);/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param body 请求消息体* @param type HttpClientResponseEntity<返回类型>* @return*/<T> HttpClientResponseEntity<T> postEntity(String url, Object body, Type<T> type);/*** post请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求headers* @param body 请求消息体* @param type HttpClientResponseEntity<返回类型>* @return*/<T> HttpClientResponseEntity<T> postEntity(String url, HttpClientHeaders headers, Object body, Type<T> type);/*** get请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求头,key-value格式* @param uriVariables 不在url中的参数,将在该接口框架中进行处理* @param type 返回类型* @return* @param <T>*/<T> T get(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Type<T> type);/*** get请求* @param url 请求url,可以是域名,也可以是ip* @param headers 请求头,key-value格式* @param type 返回类型* @return* @param <T>*/<T> T get(String url, HttpClientHeaders headers, Type<T> type);/*** get请求* @param url 请求url,可以是域名,也可以是ip* @param type 返回类型* @return* @param <T>*/<T> T get(String url, Type<T> type);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param type 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, Type<T> type);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param headers 请求header* @param type 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Type<T> type);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param uriVariables url请求拼接的参数* @param type 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, Map<String, ?> uriVariables, Type<T> type);/*** get请求* @param url url 请求url,可以是域名,也可以是ip* @param headers 请求header* @param uriVariables url参数* @param type 返回类型* @return*/<T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Type<T> type);//-------------------------------------------------------------------------------------------------------------------/*** get请求* 当对响应的内容需要进行解析时,使用该方法;例:文件下载,对response进行解析等* @param url 请求url,可以是域名,也可以是ip* @param extractor 响应回调对象,实现 extractData 接口并从中处理响应的 response* @return* @param <T>*/<T> T get(String url, HttpClientResponseExtractor<T> extractor);/*** get请求* 当对响应的内容需要进行解析时,使用该方法;例:文件下载,对response进行解析等* @param url 请求url,可以是域名,也可以是ip* @param headers* @param extractor 响应回调对象,实现 extractData 接口并从中处理响应的 response* @return* @param <T>*/<T> T get(String url, HttpClientHeaders headers, HttpClientResponseExtractor<T> extractor);<T> T get(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, HttpClientResponseExtractor<T> extractor);
}
- 实现接口HttpClientComponentImpl
package com.zzc.component.http.httpclient;import com.alibaba.fastjson2.JSON;
import com.zzc.component.http.HttpClientComponent;
import com.zzc.component.http.HttpClientHeaders;
import com.zzc.component.http.HttpClientResponse;
import com.zzc.component.http.HttpClientResponseEntity;
import com.zzc.component.http.HttpClientResponseExtractor;
import com.zzc.component.http.HttpClientStatus;
import com.zzc.component.http.Type;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@Slf4j
@Lazy
@Component
@ConditionalOnClass(value = {RestTemplate.class, CloseableHttpClient.class})
@RequiredArgsConstructor
public class HttpClientComponentImpl implements HttpClientComponent {private final RestTemplate restTemplate;@Overridepublic <T> T post(String url, HttpClientHeaders headers, Object body, Class<T> clazz) {log.debug("rest post url:{}, headers:{}, body:{}", url, headers, body);HttpClientResponseEntity<?> response = postEntity(url, headers, body, clazz);if (response != null) {return (T) response.getBody();}return null;}@Overridepublic <T> T post(String url, Object body, Class<T> clazz) {return post(url, null, body, clazz);}@Overridepublic <T> HttpClientResponseEntity<T> postEntity(String url, Object body, Class<T> clazz) {return postEntity(url, null, body, clazz);}@Overridepublic <T> HttpClientResponseEntity<T> postEntity(String url, HttpClientHeaders headers, Object body, Class<T> clazz) {HttpEntity<Object> formEntity = new HttpEntity<>(body, generateHeader(headers));try {ResponseEntity<T> response = restTemplate.postForEntity(url, formEntity, clazz);if (response == null) {log.error("request error, url:{}", url);return null;}return new HttpClientResponseEntity<T>(response.getBody(), new HttpClientHeaders(response.getHeaders().toSingleValueMap()), HttpClientStatus.resolve(response.getStatusCodeValue()));} catch (Exception e) {log.error("post error. url:{}, headers:{}, body:{}", url, headers, body, e);throw e;}}@Overridepublic <T> T get(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Class<T> clazz) {HttpClientResponseEntity<?> response = getEntity(url, headers, uriVariables, clazz);if (response == null) {return null;}return (T) response.getBody();}@Overridepublic <T> T get(String url, HttpClientHeaders headers, Class<T> clazz) {return get(url, headers, null, clazz);}@Overridepublic <T> T get(String url, Class<T> clazz) {return get(url, null, null, clazz);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, Class<T> clazz) {return getEntity(url, null, null, clazz);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Class<T> clazz) {return getEntity(url, headers, null, clazz);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, Map<String, ?> uriVariables, Class<T> clazz) {return getEntity(url, null, uriVariables, clazz);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Class<T> clazz) {try {ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, generateHeader(headers)), clazz, uriVariables == null ? new HashMap<>() : uriVariables);if (response == null) {log.error("request error, url:{}", url);return null;}return new HttpClientResponseEntity<>(response.getBody(), new HttpClientHeaders(response.getHeaders().toSingleValueMap()), HttpClientStatus.resolve(response.getStatusCodeValue()));} catch (Exception e) {log.error("get error. url:{}, headers:{}, uriVariables:{}", url, headers, uriVariables, e);throw e;}}@Overridepublic <T> T post(String url, HttpClientHeaders headers, Object body, Type<T> type) {HttpClientResponseEntity<T> response = postEntity(url, headers, body, type);if (response == null) {return null;}return response.getBody();}@Overridepublic <T> T post(String url, Object body, Type<T> type) {return post(url, null, body, type);}@Overridepublic <T> HttpClientResponseEntity<T> postEntity(String url, Object body, Type<T> type) {return postEntity(url, null, body, type);}@Overridepublic <T> HttpClientResponseEntity<T> postEntity(String url, HttpClientHeaders headers, Object body, Type<T> type) {HttpEntity<Object> formEntity = new HttpEntity<>(body, generateHeader(headers));try {ResponseEntity<String> response = restTemplate.postForEntity(url, formEntity, String.class);if (response == null) {log.error("request error, url:{}", url);return null;}return new HttpClientResponseEntity<T>(parse(response.getBody(), type), new HttpClientHeaders(response.getHeaders().toSingleValueMap()), HttpClientStatus.resolve(response.getStatusCodeValue()));} catch (Exception e) {log.error("post error. url:{}, headers:{}, body:{}", url, headers, body, e);throw e;}}@Overridepublic <T> T get(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Type<T> type) {HttpClientResponseEntity<T> response = getEntity(url, headers, uriVariables, type);if (response == null) {return null;}return response.getBody();}@Overridepublic <T> T get(String url, HttpClientHeaders headers, Type<T> type) {return get(url, headers, null, type);}@Overridepublic <T> T get(String url, Type<T> type) {return get(url, null,null, type);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, Type<T> type) {return getEntity(url, null, null, type);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Type<T> type) {return getEntity(url, headers, null, type);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, Map<String, ?> uriVariables, Type<T> type) {return getEntity(url, null, uriVariables, type);}@Overridepublic <T> HttpClientResponseEntity<T> getEntity(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, Type<T> type) {try {ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, generateHeader(headers)), String.class, uriVariables == null ? new HashMap<>() : uriVariables);if (response == null) {log.error("request error, url:{}", url);return null;}return new HttpClientResponseEntity<T>(parse(response.getBody(), type), new HttpClientHeaders(response.getHeaders().toSingleValueMap()), HttpClientStatus.resolve(response.getStatusCodeValue()));} catch (Exception e) {log.error("get error. url:{}, headers:{}, uriVariables:{}", url, headers, uriVariables, e);throw e;}}@Overridepublic <T> T get(String url, HttpClientResponseExtractor<T> extractor) {return get(url, null, null, extractor);}@Overridepublic <T> T get(String url, HttpClientHeaders headers, HttpClientResponseExtractor<T> extractor) {return get(url, headers, null, extractor);}@Overridepublic <T> T get(String url, HttpClientHeaders headers, Map<String, ?> uriVariables, HttpClientResponseExtractor<T> extractor) {RequestCallback requestCallback = null;if (headers == null || headers.isEmpty()) {requestCallback = restTemplate.httpEntityCallback(HttpEntity.EMPTY);} else {HttpEntity<Map<String, Object>> formEntity = new HttpEntity<>(null, generateHeader(headers));requestCallback = restTemplate.httpEntityCallback(formEntity);}ResponseExtractor<T> responseExtractor = new ResponseExtractor<T>() {@Overridepublic T extractData(ClientHttpResponse response) throws IOException {HttpClientResponse httpClientResponse = new HttpClientResponse() {@Overridepublic InputStream getBody() throws IOException {return response.getBody();}@Overridepublic HttpClientStatus getStatusCode() throws IOException {return HttpClientStatus.resolve(response.getStatusCode().value());}@Overridepublic int getRawStatusCode() throws IOException {return response.getRawStatusCode();}@Overridepublic String getStatusText() throws IOException {return response.getStatusText();}@Overridepublic void close() {response.close();}};return extractor.extractData(httpClientResponse);}};if (uriVariables != null && !uriVariables.isEmpty()) {//URI expanded = getUriTemplateHandler().expand(url, uriVariables); 对uriVariables不做判空处理,等自己判断return restTemplate.execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);} else {return restTemplate.execute(url, HttpMethod.GET, requestCallback, responseExtractor);}}/*** 则需参数接收的载体需要使用 MultiValueMap* @return*/private static MultiValueMap<String, String> defaultHeaders() {return new HttpHeaders();}private static MultiValueMap<String, String> generateHeader(Map<String, String> headerMap) {if (headerMap == null || headerMap.isEmpty()) {return defaultHeaders();}MultiValueMap<String, String> headers = new HttpHeaders();for (Map.Entry<String, String> entry : headerMap.entrySet()) {List<String> objList = new ArrayList<>();objList.add(entry.getValue());headers.put(entry.getKey(), objList);}return headers;}private <T> T parse(String bodyStr, Type<T> type) {return JSON.parseObject(bodyStr, type);}
}