需求分析
近期在做的项目,依赖了挺多外部服务的接口。请求这些接口,由于一些原因,多方考虑,最后决定使用OkHttp去处理这些接口请求。考虑到这块项目后续的长期迭代,对于OkHttp进行了一些初步的封装。
目标确定
1,只封装POST和GET方式的请求;
2,请求和读取的超时时间可以自定义配置;
3,外部服务的接口稳定性比较一般,所以请求读取超时、请求返回Http错误、或者返回业务异常,要自动重试,并且可以自定义最大重试次数以及重试间隔;
代码实现
项目JDK版本1.8
Spring Boot版本2.6.14
项目使用Maven构建,先引入OkHttp的maven依赖
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.10.0</version>
</dependency>
先创建一个类OkHttpConfiguration用来构建OkHttpClient,超时的默认参数,重试的默认参数也维护在这个类里。提供按照默认参数创建OkHttpClient的方法,同时也支持自定义超时和重试请求接口的参数,也可以选择是否需要重试调用。具体代码如下:
public class OkHttpConfiguration {
/**
* 默认超时时间
*/
static final long DEFAULT_TIME_OUT = 3;
/**
* 默认超时时间单位
*/
static final TimeUnit DEFAULT_TIME_OUT_UNIT = TimeUnit.SECONDS;
/**
* 默认最大重试次数
*/
static final int DEFAULT_MAX_RETRIES = 2;
/**
* 默认重试间隔毫秒数
*/
static final int DEFAULT_RETRY_DELAY_MILLIS = 0;
/**
* 创建OkHttpClient
* <p>
* 超时时间:默认配置
* 重试:否
*
* @param
* @return okhttp3.OkHttpClient
*/
public static OkHttpClient createClient() {
return new OkHttpClient.Builder()
.connectTimeout(DEFAULT_TIME_OUT, DEFAULT_TIME_OUT_UNIT)
.readTimeout(DEFAULT_TIME_OUT, DEFAULT_TIME_OUT_UNIT)
.build();
}
/**
* 创建OkHttpClient
* <p>
* 超时时间:根据入参自定义
* 重试:否
*
* @param timeOut 超时时间
* @param timeOutUnit 超时时间单位
* @return okhttp3.OkHttpClient
*/
public static OkHttpClient createClient(final Long timeOut, final TimeUnit timeOutUnit) {
return new OkHttpClient.Builder()
.connectTimeout(null == timeOut ? DEFAULT_TIME_OUT : timeOut,
null == timeOutUnit ? DEFAULT_TIME_OUT_UNIT : timeOutUnit)
.readTimeout(null == timeOut ? DEFAULT_TIME_OUT : timeOut,
null == timeOutUnit ? DEFAULT_TIME_OUT_UNIT : timeOutUnit)
.build();
}
/**
* 创建OkHttpClient
* <p>
* 超时时间:默认配置
* 重试:是
* 重试配置:默认超时配置
*
* @param
* @return okhttp3.OkHttpClient
*/
public static OkHttpClient createClientWithRetry() {
return new OkHttpClient.Builder()
.connectTimeout(DEFAULT_TIME_OUT, DEFAULT_TIME_OUT_UNIT)
.readTimeout(DEFAULT_TIME_OUT, DEFAULT_TIME_OUT_UNIT)
.addInterceptor(new OkHttpRetryInterceptor(DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_MILLIS))
.build();
}
/**
* 创建OkHttpClient
* <p>
* 超时时间:根据入参自定义
* 重试:是
* 重试配置:默认超时配置
*
* @param timeOut 超时时间
* @param timeOutUnit 超时时间单位
* @return okhttp3.OkHttpClient
*/
public static OkHttpClient createClientWithRetry(final Long timeOut, final TimeUnit timeOutUnit) {
return new OkHttpClient.Builder()
.connectTimeout(null == timeOut ? DEFAULT_TIME_OUT : timeOut,
null == timeOutUnit ? DEFAULT_TIME_OUT_UNIT : timeOutUnit)
.readTimeout(null == timeOut ? DEFAULT_TIME_OUT : timeOut,
null == timeOutUnit ? DEFAULT_TIME_OUT_UNIT : timeOutUnit)
.addInterceptor(new OkHttpRetryInterceptor(DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_MILLIS))
.build();
}
/**
* 创建OkHttpClient
* <p>
* 超时时间:根据入参自定义
* 重试:是
* 重试配置:根据入参自定义
*
* @param timeOut 超时时间
* @param timeOutUnit 超时时间单位
* @param maxRetries 最大重试次数
* @param retryDelayMillis 重试间隔毫秒数
* @return okhttp3.OkHttpClient
*/
public static OkHttpClient createClientWithRetry(final Long timeOut, final TimeUnit timeOutUnit,
final Integer maxRetries, final Integer retryDelayMillis) {
return new OkHttpClient.Builder()
.connectTimeout(null == timeOut ? DEFAULT_TIME_OUT : timeOut,
null == timeOutUnit ? DEFAULT_TIME_OUT_UNIT : timeOutUnit)
.readTimeout(null == timeOut ? DEFAULT_TIME_OUT : timeOut,
null == timeOutUnit ? DEFAULT_TIME_OUT_UNIT : timeOutUnit)
.addInterceptor(new OkHttpRetryInterceptor(null == maxRetries ? DEFAULT_MAX_RETRIES : maxRetries,
null == retryDelayMillis ? DEFAULT_RETRY_DELAY_MILLIS : retryDelayMillis))
.build();
}
}
OkHttpUtil这个类用来处理HTTP请求,提供GET请求和POST请求的调用。具体代码如下:
@Slf4j
public class OkHttpUtil {
/**
* 按照默认策略GET请求
* <p>
* 默认策略 >>> 超时时间:3
* 默认策略 >>> 超时时间单位:TimeUnit.SECONDS
* 默认策略 >>> 重试最大次数:2
* 默认策略 >>> 重试间隔毫秒数:0
*
* @param url 请求地址
* @param headMap 请求头
* @param retry 是否重试
* @return java.lang.String
*/
public static String get(final String url, final Map<String, String> headMap, final boolean retry) throws IOException {
// OkHttp配置项初始化
OkHttpClient okHttpClient = OkHttpConfiguration.createClient();
if (retry) {
okHttpClient = OkHttpConfiguration.createClientWithRetry();
}
// 构建请求
Request request = buildGetRequest(url, headMap);
// 提交请求,返回结果
return execute(request, okHttpClient);
}
/**
* 按照默认策略POST请求
* <p>
* 默认策略 >>> 超时时间:3
* 默认策略 >>> 超时时间单位:TimeUnit.SECONDS
* 默认策略 >>> 重试最大次数:2
* 默认策略 >>> 重试间隔毫秒数:0
*
* @param url 请求地址
* @param obj 请求body
* @param headMap 请求header
* @param retry 是否重试
* @return java.lang.String
*/
public static String postJson(final String url, final Object obj, final Map<String, String> headMap,
final boolean retry) throws IOException {
// OkHttp配置项初始化
OkHttpClient okHttpClient = OkHttpConfiguration.createClient();
if (retry) {
okHttpClient = OkHttpConfiguration.createClientWithRetry();
}
// 构建请求
Request request = buildPostJsonRequest(url, obj, headMap);
// 提交请求,返回结果
return execute(request, okHttpClient);
}
/**
* 按照默认策略POST请求
* <p>
* 默认策略 >>> 超时时间:3
* 默认策略 >>> 超时时间单位:TimeUnit.SECONDS
* 默认策略 >>> 重试最大次数:2
* 默认策略 >>> 重试间隔毫秒数:0
*
* @param url 请求地址
* @param params 请求参数
* @param headMap 请求header
* @param retry 是否重试
* @return java.lang.String
*/
public static String postForm(final String url, final Map<String, Object> params,
final Map<String, String> headMap, final boolean retry) throws IOException {
// OkHttp配置项初始化
OkHttpClient okHttpClient = OkHttpConfiguration.createClient();
if (retry) {
okHttpClient = OkHttpConfiguration.createClientWithRetry();
}
// 构建请求
Request request = buildPostFormRequest(url, params, headMap);
// 提交请求,返回结果
return execute(request, okHttpClient);
}
/**
* 自定义策略GET请求
*
* @param url 请求地址
* @param headMap 请求头
* @param timeOut 超时时间
* @param timeOutUnit 超时时间单位
* @param retry 是否重试
* @param maxRetries 重试最大次数
* @param retryDelayMillis 重试间隔毫秒数
* @return java.lang.String
*/
public static String get(final String url, final Map<String, String> headMap, final Long timeOut, final TimeUnit timeOutUnit,
final boolean retry, final Integer maxRetries, final Integer retryDelayMillis) throws IOException {
// OkHttp配置项初始化
OkHttpClient okHttpClient = OkHttpConfiguration.createClient(timeOut, timeOutUnit);
if (retry) {
okHttpClient = OkHttpConfiguration.createClientWithRetry(timeOut, timeOutUnit, maxRetries, retryDelayMillis);
}
// 构建请求
Request request = buildGetRequest(url, headMap);
// 提交请求,返回结果
return execute(request, okHttpClient);
}
/**
* 自定义策略POST请求
*
* @param url 请求地址
* @param obj 请求body
* @param headMap 请求header
* @param timeOut 超时时间
* @param timeOutUnit 超时时间单位
* @param retry 是否重试
* @param maxRetries 重试最大次数
* @param retryDelayMillis 重试间隔毫秒数
* @return java.lang.String
*/
public static String postJson(final String url, final Object obj, final Map<String, String> headMap,
final Long timeOut, final TimeUnit timeOutUnit, final boolean retry,
final Integer maxRetries, final Integer retryDelayMillis) throws IOException {
// OkHttp配置项初始化
OkHttpClient okHttpClient = OkHttpConfiguration.createClient(timeOut, timeOutUnit);
if (retry) {
okHttpClient = OkHttpConfiguration.createClientWithRetry(timeOut, timeOutUnit, maxRetries, retryDelayMillis);
}
// 构建请求
Request request = buildPostJsonRequest(url, obj, headMap);
// 提交请求,返回结果
return execute(request, okHttpClient);
}
/**
* 自定义策略POST请求
*
* @param url 请求地址
* @param params 请求参数
* @param headMap 请求header
* @param timeOut 超时时间
* @param timeOutUnit 超时时间单位
* @param retry 是否重试
* @param maxRetries 重试最大次数
* @param retryDelayMillis 重试间隔毫秒数
* @return java.lang.String
*/
public static String postForm(final String url, final Map<String, Object> params, final Map<String, String> headMap,
final Long timeOut, final TimeUnit timeOutUnit, final boolean retry, final Integer maxRetries,
final Integer retryDelayMillis) throws IOException {
// OkHttp配置项初始化
OkHttpClient okHttpClient = OkHttpConfiguration.createClient(timeOut, timeOutUnit);
if (retry) {
okHttpClient = OkHttpConfiguration.createClientWithRetry(timeOut, timeOutUnit, maxRetries, retryDelayMillis);
}
// 构建请求
Request request = buildPostFormRequest(url, params, headMap);
// 提交请求,返回结果
return execute(request, okHttpClient);
}
/**
* 提交请求,返回结果
*
* @param request
* @param okHttpClient
* @return java.lang.String
*/
private static String execute(final Request request, final OkHttpClient okHttpClient) throws IOException {
Instant start = Instant.now();
Response response = okHttpClient.newCall(request).execute();
Instant end = Instant.now();
log.info(">>> OkHttp调用时间统计 >>> 接口:{},执行时间:{}ms", request.url(), end.toEpochMilli() - start.toEpochMilli());
return response.body().string();
}
/**
* Request Get组装
*
* @param url 请求地址
* @param headMap 请求头
* @return okhttp3.Request
*/
private static Request buildGetRequest(final String url, final Map<String, String> headMap) {
Request request = new Request.Builder().url(url).build();
if (null != headMap) {
request = new Request.Builder().url(url).headers(buildHeaders(headMap)).build();
}
return request;
}
/**
* Request PostJson组装
*
* @param url 请求地址
* @param obj 请求body
* @param headMap 请求header
* @return okhttp3.Request
*/
private static Request buildPostJsonRequest(final String url, final Object obj, final Map<String, String> headMap) {
return new Request.Builder()
.url(url)
.post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JSONObject.toJSONString(obj)))
.addHeader("Content-Type", "application/json")
.headers(buildHeaders(headMap))
.build();
}
/**
* Request PostForm组装
*
* @param url 请求地址
* @param params 请求参数
* @param headMap 请求header
* @return okhttp3.Request
*/
private static Request buildPostFormRequest(final String url, final Map<String, Object> params, final Map<String, String> headMap) {
FormBody.Builder formBuilder = new FormBody.Builder();
Iterator<String> it = params.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
formBuilder.add(key, (String) params.get(key));
}
FormBody body = formBuilder.build();
return new Request.Builder().url(url).headers(buildHeaders(headMap)).post(body).build();
}
/**
* Header组装
*
* @param headersParams
* @return okhttp3.Headers
*/
private static Headers buildHeaders(final Map<String, String> headersParams) {
Headers headers;
okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder();
if (MapUtils.isNotEmpty(headersParams)) {
Iterator<String> iterator = headersParams.keySet().iterator();
String key;
while (iterator.hasNext()) {
key = iterator.next();
headersBuilder.add(key, headersParams.get(key));
}
}
headers = headersBuilder.build();
return headers;
}
}
OkHttpRetryInterceptor类自动移拦截器,用来处理重试调用的逻辑。
这里有个地方需要注意一下。在处理业务系统返回异常重试的时候,需要通过response.body().string()方法去获取具体的返回结果。但是response.body().string()方法会关闭数据流,导致在OkHttp类里面再调用response.body().string()方法时提示资源已关闭。所以需要先通过response.peekBody(Long.MAX_VALUE)方法,生成一个ResponseBody的快照,通过这个快照获取具体的返回结果。
实现的具体代码如下:
@Slf4j
public class OkHttpRetryInterceptor implements Interceptor {
/**
* 请求返回需要重试的Code列表
*/
static final int[] RETRY_CODE_ARRAY = {400, 403, 404, 408, 500, 502};
/**
* 最大重试次数
*/
private final int maxRetries;
/**
* 重试间隔毫秒数
*/
private final int retryDelayMillis;
public OkHttpRetryInterceptor(final int maxRetries, final int retryDelayMillis) {
this.maxRetries = maxRetries;
this.retryDelayMillis = retryDelayMillis;
}
@Override
public Response intercept(Chain chain) {
return retry(chain, 0);
}
/**
* 重试
* <p>
* 递归处理
*
* @param chain
* @param retryCent
* @return okhttp3.Response
*/
private Response retry(Chain chain, int retryCent) {
Request request = chain.request();
Response response = null;
try {
log.info(">>> OkHttp第{}次调用接口{},详细请求:{}", (retryCent + 1), request.url(), request.toString());
response = chain.proceed(request);
// 请求HTTP失败
if (Arrays.asList(RETRY_CODE_ARRAY).contains(response.code()) && maxRetries > retryCent) {
TimeUnit.MILLISECONDS.sleep(retryDelayMillis);
return retry(chain, retryCent + 1);
}
// 因为response.body().string()会关闭资源,所以生成一个快照获取ResponseBody
ResponseBody responseBodyClone = response.peekBody(Long.MAX_VALUE);
String responseString = responseBodyClone.string();
responseBodyClone.close();
JSONObject jsonObject = JSONObject.parseObject(responseString);
// 请求业务系统返回异常
if (!jsonObject.getBoolean(Errors.SUCCESS.getMessage()) && maxRetries > retryCent) {
TimeUnit.MILLISECONDS.sleep(retryDelayMillis);
return retry(chain, retryCent + 1);
}
return response;
} catch (Exception e) {
if (maxRetries > retryCent) {
try {
TimeUnit.MILLISECONDS.sleep(retryDelayMillis);
} catch (InterruptedException ie) {
ie.printStackTrace();
}
return retry(chain, retryCent + 1);
} else {
return response;
}
}
}
}