java:接口请求重试
1 前言
接口请求重试的思路,采用循环 + try \ catch的方式,若接口请求失败或超时,则根据设置的重试次数再次发起请求重试。
2 使用
具体实现如下,若执行中抛出超时异常,则接口请求发起重试,最多执行次数为5次:
public String buildGetRequestWithRetry(String urlPath,
Map<String,Object> paramsMap,
Map<String,String> headerMap){
/* 请求重试次数(包含原本do执行的1次) */
int retryTimes = 5;
final int finalRetryTimes = retryTimes;
String result = null;
label:{
do{
try{
result = this.buildGetRequest(urlPath, paramsMap, headerMap);
break label;
}catch (CrawlerForJException crawlerForJException){
if(isConnectTimeOut(crawlerForJException) ||
isReadTimeOut(crawlerForJException)){
retryTimes --;
if(retryTimes > 0){
LogUtils.warn(requestLogger,
"接口请求第{}次请求重试...",
(finalRetryTimes - retryTimes));
}
ThreadUtils.sleepByMilli(1000);
}else{
throw crawlerForJException;
}
}
}while(retryTimes > 0);
ExcpUtils.throwExp("总执行" + finalRetryTimes + "次请求,请求依然失败.");
}
return result;
}
RequestHelper:
@Component
public class RequestHelper {
@Autowired
RequestUtils requestUtils;
public String doRequestDispatch(String method,
String urlPath,
Map<String, Object> params,
Map<String, String> headers){
/* reqE不会为null,找不到只会抛出异常,避免switch(null)空指针异常 */
RequestMethodEnum reqE = this.getRequestMEnumByMethod(method);
switch (reqE){
case GET:
return requestUtils.buildGetRequestWithRetry(urlPath, params, headers);
case POST:
ExcpUtils.throwExp("post方法暂时不支持");
default:
ExcpUtils.throwExp("其他方法暂时不支持");
}
return null;
}
/* 暂时只支持使用RequestMethodEnum枚举中配置的 */
public RequestMethodEnum getRequestMEnumByMethod(String method){
RequestMethodEnum[] var1 = RequestMethodEnum.values();
int var2 = var1.length;
int var3 = 0;
RequestMethodEnum reqM = null;
label : {
if(var2 > 0){
do{
RequestMethodEnum requestM = var1[var3];
if(requestM.getName().equalsIgnoreCase(method)){
reqM = requestM;
break label;
}
var3++;
}while(var3 < var2);
}
ExcpUtils.throwExp("没有找到匹配request请求方法:"+method);
}
return reqM;
}
}
RequestMethodEnum:
public enum RequestMethodEnum {
/*避免java.net.ProtocolException: Invalid HTTP method: get 以及
java.net.ProtocolException: Invalid HTTP method: post异常,应该是GET和POST*/
GET("GET","get请求","01"),
POST("POST","post请求","02");
private String name;
private String description;
private String code;
RequestMethodEnum(String name,String description,String code){
this.name = name;
this.description = description;
this.code = code;
}
/*根据code获取请求方式*/
public static RequestMethodEnum getMethodByCode(String code){
for (RequestMethodEnum value : RequestMethodEnum.values()) {
if(value.getCode().equals(code)){
return value;
}
}
return null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
RequestUtils:
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import com.xiaoxu.crawler.enums.request.RequestMethodEnum;
import com.xiaoxu.crawler.excp.CrawlerForJException;
import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.*;
/**
* @author xiaoxu
* @date 2022-11-06 15:29
* crawlerJ:com.xiaoxu.crawler.utils.RequestUtils
*/
@Component
public class RequestUtils {
private static final String UTF8 = "utf-8";
private static final String AndSet = "&";
private static final String CommaSet = ",";
private static final String EqualSet = "=";
private static final String QuestionMark = "?";
private static final String ConnectTimeOutMsg = "connect timed out";
private static final String ReadTimeOutMsg = "read timed out";
private static final Logger requestLogger =
LoggerFactory.getLogger(RequestUtils.class);
/**
* @param urlPath GET请求接口路径
* @param paramsMap GET请求传参,可为空
* @param headerMap 请求头,可为空
* @return GET请求接口返回值(InputStream)
*/
/* get接口请求 */
public InputStream buildGetRequestInputStream(String urlPath,
Map<String,Object> paramsMap,
Map<String,String> headerMap){
/* query String 的请求方式 */
return buildAndGetResultInputStream(RequestMethodEnum.GET.getName(),
urlPath,
buildParamsStr(paramsMap),
headerMap);
}
public String buildGetRequestWithRetry(String urlPath,
Map<String,Object> paramsMap,
Map<String,String> headerMap){
/* 请求重试次数(包含原本do执行的1次) */
int retryTimes = 5;
final int finalRetryTimes = retryTimes;
String result = null;
label:{
do{
try{
result = this.buildGetRequest(urlPath, paramsMap, headerMap);
break label;
}catch (CrawlerForJException crawlerForJException){
if(isConnectTimeOut(crawlerForJException) ||
isReadTimeOut(crawlerForJException)){
retryTimes --;
if(retryTimes > 0){
LogUtils.warn(requestLogger,
"接口请求第{}次请求重试...",
(finalRetryTimes - retryTimes));
}
ThreadUtils.sleepByMilli(1000);
}else{
throw crawlerForJException;
}
}
}while(retryTimes > 0);
ExcpUtils.throwExp("总执行" + finalRetryTimes + "次请求,请求依然失败.");
}
return result;
}
private boolean isConnectTimeOut(Throwable th) {
return null != th
&& th.getMessage().toLowerCase(Locale.ROOT).contains(ConnectTimeOutMsg);
}
private boolean isReadTimeOut(Throwable th) {
return null != th
&& th.getMessage().toLowerCase(Locale.ROOT).contains(ReadTimeOutMsg);
}
/**
* @param urlPath GET接口请求路径
* @param paramsMap GET接口请求参数
* @param headerMap 请求头
* @return 响应结果(String)
*/
public String buildGetRequest(String urlPath,
Map<String,Object> paramsMap,
Map<String,String> headerMap){
return buildAndGetResult(RequestMethodEnum.GET.getName(),
urlPath,
buildParamsStr(paramsMap),
headerMap);
}
/**
* @param method 请求方式:GET、POST
* @param urlPath 请求路径
* @param data 请求数据
* @param headerMap 请求头(根据接口情况传入)
* @return
*/
@SuppressWarnings(value = "all")
protected static InputStream buildAndGetResultInputStream(@NonNull String method,
@NonNull String urlPath,
String data,
Map<String,String> headerMap){
StringBuilder str = new StringBuilder();
InputStream in = null;
FileOutputStream out = null;
HttpURLConnection connection = null;
try {
if(!StringUtils.hasLength(method)){
ExcpUtils.throwExp(
MessageFormat.format("buildRequest方法的method不能为空:{0}",method));
}
if(!StringUtils.hasLength(urlPath)){
ExcpUtils.throwExp(
MessageFormat.format("buildRequest方法的urlPath不能为空:{0}",urlPath));
}
if(!method.equals(RequestMethodEnum.GET.getName())&&!method.equals(RequestMethodEnum.POST.getName())){
ExcpUtils.throwExp(
MessageFormat.format("buildRequest方法的method只能为GET或POST:{0}",method));
}
/*处理data,如果data为null,转换成"",否则data.getBytes会抛出空指针异常*/
data = Optional.ofNullable(data).orElse("");
data = data.replaceAll(" ","");
if("get".equals(method.toLowerCase(Locale.ROOT))){
/*get请求需要拼接路径(无参数,data应该是"",有参数则拼接)*/
urlPath += data;
}
LogUtils.info(requestLogger, method+"请求获取stream的url:{}", urlPath);
URL url;
url = new URL(urlPath);
connection = (HttpURLConnection) url.openConnection();
if(!MapUtils.isEmpty(headerMap)){
/*设置请求头的属性,比如:Cookie,Token等等
System.out.println("key:"+k+";"+"value:"+v);*/
headerMap.forEach(connection::setRequestProperty);
}
/*设置请求方式*/
connection.setRequestMethod(method);
connection.setRequestProperty("User-Agent", "Mozilla/4.76");
/*设置超时时间*/
connection.setConnectTimeout(5000);
connection.setReadTimeout(2000);
/*post不同于get,get只需要将请求参数拼接到url中,而post请求的是正文*/
if("post".equals(method.toLowerCase(Locale.ROOT))){
/*post请求不能使用缓存*/
connection.setUseCaches(false);
/*post参数不是放在URL字符串里,而是http请求的正文
post:http正文内,因此需要设为true*/
connection.setDoOutput(true);
/*connection获取输出的流,write写入post请求的参数:格式:key=value&key1=value1
(当post请求是Content-Type:x-www-form-urlencoded(表单提交时使用))
post请求参数为JSON格式:当请求头设置了"Content-Type","application/json;charset=UTF-8"时使用*/
OutputStream outputStream = connection.getOutputStream();
/*String的getBytes,字符串转字节*/
byte[] postBytes = data.getBytes(StandardCharsets.UTF_8);
outputStream.write(postBytes);
}
/*获取URLConnection对象对应的输入流*/
in = connection.getInputStream();
} catch (IOException e) {
throw new CrawlerForJException("buildRequest获取stream发生IO异常:"+e.getMessage(),e.getCause());
} catch (Throwable th){
throw new CrawlerForJException("buildRequest获取stream发生未知异常:"+th.getClass().getName()+th.getMessage(),
th.getCause());
} finally {
/*关闭连接*/
Optional.ofNullable(connection).ifPresent(HttpURLConnection::disconnect);
}
return in;
}
protected static String buildAndGetResult(@NonNull String method,
@NonNull String urlPath,
String data,
Map<String,String> headerMap){
StringBuilder str = new StringBuilder();
InputStream in = null;
HttpURLConnection connection = null;
try {
if(!StringUtils.hasLength(method)){
ExcpUtils.throwExp(
MessageFormat.format("buildRequest方法的method不能为空:{0}",method));
}
if(!StringUtils.hasLength(urlPath)){
ExcpUtils.throwExp(
MessageFormat.format("buildRequest方法的urlPath不能为空:{0}",urlPath));
}
if(!method.equals(RequestMethodEnum.GET.getName())&&!method.equals(RequestMethodEnum.POST.getName())){
ExcpUtils.throwExp(
MessageFormat.format("buildRequest方法的method只能为GET或POST:{0}",method));
}
/*处理data,如果data为null,转换成"",否则data.getBytes会抛出空指针异常*/
data = Optional.ofNullable(data).orElse("");
data = data.replaceAll(" ","");
if("get".equals(method.toLowerCase(Locale.ROOT))){
/* get请求需要拼接路径(无参数,data应该是"",有参数则拼接) */
urlPath += data;
}
LogUtils.info(requestLogger, method+"请求的url:{}", urlPath);
URL url;
url = new URL(urlPath);
connection = (HttpURLConnection) url.openConnection();
if(!MapUtils.isEmpty(headerMap)){
/*
* 设置请求头的属性,比如:Cookie,Token等等;
* System.out.println("key:"+k+";"+"value:"+v);
* */
headerMap.forEach(connection::setRequestProperty);
}
/* 设置请求方式 */
connection.setRequestMethod(method);
connection.setRequestProperty("User-Agent", "Mozilla/4.76");
/* 设置超时时间 */
connection.setConnectTimeout(5000);
connection.setReadTimeout(2000);
/* post不同于get,get只需要将请求参数拼接到url中,而post请求的是正文 */
if("post".equals(method.toLowerCase(Locale.ROOT))){
/* post请求不能使用缓存 */
connection.setUseCaches(false);
/*
* post参数不是放在URL字符串里,而是http请求的正文
* post:http正文内,因此需要设为true
* */
connection.setDoOutput(true);
/*
* connection获取输出的流,write写入post请求的参数:格式:key=value&key1=value1
* (当post请求是Content-Type:x-www-form-urlencoded(表单提交时使用))
* post请求参数为JSON格式:当请求头设置了"Content-Type","application/json;charset=UTF-8"时使用
* */
OutputStream outputStream = connection.getOutputStream();
/* String的getBytes,字符串转字节 */
byte[] postBytes = data.getBytes(StandardCharsets.UTF_8);
outputStream.write(postBytes);
}
/* 获取URLConnection对象对应的输入流 */
in = connection.getInputStream();
/* 将接口请求的接口打印出来 */
BufferedReader b = new BufferedReader(new InputStreamReader(in));
String s;
while((s = b.readLine()) != null){
str.append(s);
}
} catch (IOException e) {
throw new CrawlerForJException("buildRequest发生IO异常:"+e.getMessage(), e.getCause());
} catch (Throwable th){
throw new CrawlerForJException("buildRequest发生未知异常:"+th.getClass().getName() + ":[" + th.getMessage() + "]",
th.getCause());
} finally {
/*关闭连接*/
Optional.ofNullable(connection).ifPresent(HttpURLConnection::disconnect);
/*关闭流*/
closeIn(in, "请求接口, 关闭输入流失败:{}");
}
return str.toString();
}
private static void closeIn(InputStream stream, String message) {
if(stream != null){
try {
stream.close();
} catch (IOException e) {
LogUtils.info(requestLogger, message, e.getMessage());
}
}
}
private static void closeOut(OutputStream stream, String message) {
if(stream != null){
try {
stream.close();
} catch (IOException e) {
LogUtils.info(requestLogger, message, e.getMessage());
}
}
}
/* 构建get请求的参数url,类似:?key=value&key1=value1 (query String)*/
@SuppressWarnings(value = "all")
public static String buildParamsStr(Map<String,Object> paramsMap){
paramsMap = Optional.ofNullable(paramsMap).orElse(new HashMap<>());
StringBuilder s = new StringBuilder();
StringBuilder temp = new StringBuilder();
try{
for(Map.Entry<String,Object> m: paramsMap.entrySet()) {
StringBuilder rep = new StringBuilder();
if (m.getValue() instanceof List) {
Iterator<?> iter =((List<?>) m.getValue()).iterator();
while(iter.hasNext()){
String s1 = String.valueOf(iter.next());
if(rep.length() == 0){
rep.append(getEncode(m.getKey())).append(EqualSet).append(getEncode(s1));
}else{
rep.append(getEncode(CommaSet)).append(getEncode(s1));
}
}
}else if(m.getValue() instanceof String ||
m.getValue() instanceof Long ||
m.getValue() instanceof Integer){
rep.append(getEncode(m.getKey())).append(EqualSet).append(getEncode(String.valueOf(m.getValue())));
}else{
throw new CrawlerForJException("get请求参数存在value为非String,或非List,非Integer,非Long的数据");
}
if(temp.length() == 0){
temp.append(rep);
}
temp.append(AndSet).append(rep);
}
}catch (CrawlerForJException | UnsupportedEncodingException c){
throw new CrawlerForJException(c);
}catch (Exception e){
throw new CrawlerForJException("未知异常:" + e.getMessage());
}
if (temp.length() != 0) {
s.append(QuestionMark).append(temp);
}
return s.toString();
}
/* urlEncoded,get请求 query String使用 */
private static String getEncode(String val) throws UnsupportedEncodingException {
return URLEncoder.encode(Optional.ofNullable(val).orElse(""), UTF8);
}
/*
* 构建post请求参数,类似:key = value & key1 = value1
* 当post请求是Content-Type:x-www-form-urlencoded(表单提交时使用)
* */
public static String buildPayloadStr(Map<String,Object> payloadMap){
StringBuilder s = new StringBuilder();
payloadMap = Optional.ofNullable(payloadMap).orElse(Maps.newHashMap());
for(Map.Entry<String,Object> m: payloadMap.entrySet()){
if(s.length() == 0){
s.append(m.getKey()).append(EqualSet).append(m.getValue());
}else{
s.append(AndSet).append(m.getKey()).append(EqualSet).append(m.getValue());
}
}
return s.toString();
}
/*
* 构建post请求2:json字符串格式,
* 当请求头设置了"Content-Type","application/json;charset=UTF-8"时使用
* */
public static String buildJsonPayloadStr(Map<String,Object> payloadMap){
String jsonData;
/*允许为空*/
payloadMap = Optional.ofNullable(payloadMap).orElse(Maps.newHashMap());
try {
jsonData = JSON.toJSONString(payloadMap);
} catch (Exception e) {
throw new CrawlerForJException("application-jsonData转换异常: "+e.getClass().getName()+"; "+e.getMessage());
}
return jsonData;
}
}
ThreadUtils:
public class ThreadUtils {
public static void sleepByMilli(long milliSeconds){
if(milliSeconds <= 0)
return;
try {
Thread.sleep(milliSeconds);
} catch (InterruptedException ignored) {
/* 忽略人为中止异常 */
}
}
}