工作中经常会遇到某些接口超时、返回的数据不是我们想要的,在这些情况下,可能会要求我们对该接口进行重试,但是有的接口需要重试三次,有的需要重试两次,有的不需要重试;有的返回连接超时才重试,有的读取超时才重试,有的404才重试;有的返回-1才重试,有的返回null才重试;有的超时时间3秒,有的30秒。各种各样的场景需要我们在调用完成后自己判断是否进行重试以及进行几次重试。
现在springcloud提供的ribbon以及fegin都提供了重试机制,但也只能设置统一的或者针对某一系统的重试次数、超时时间。不符合上面的要求,所以本文来实现一个定制化的重试template(借鉴fegin,fegin的重试通过代理)。
1.首先,先抽象出我们对外提供服务的接口,如下
package com.wxind.httpexecutor.executor; /** * Created by wangxindong on 18/5/17 */ public interface HttpExecutors { /** * 自己设置重试机制、重试次数和重试间隔时间 * @param task 需执行的restTemplate任务 * @param retryer 重试机制 * @param retryMeta 重试机制的元数据,包括重试次数,以及重试的间隔时间 * @param restTemplateType 需要使用的restTemplate * @param <T> 返回值类型,与restTemplate需要的返回值类型相同 * @return * @throws Exception */ <T> T executor(Task<T> task, Retryer retryer, Retryer.RetryMeta retryMeta, RestTemplateType restTemplateType) throws Exception; /** * 根据needRetry判断是否需要重试,如果needRetry为true,则使用默认的重试机制以及默认的次数和间隔时间 * 默认重试次数为2次 默认间隔时间为2秒 * @param task 需执行的restTemplate任务 * @param needRetry 是否需要重试 * @param restTemplateType 需要使用的restTemplate * @param <T> 返回值类型,与restTemplate需要的返回值类型相同 * @return * @throws Exception */ <T> T executor(Task<T> task, boolean needRetry, RestTemplateType restTemplateType) throws Exception; }抽象出了两个接口,一个需要自己设置重试机制、重试次数以及重试间隔。另一个只需要告知是否需要重试,内部采用默认的机制、次数和间隔。还有一个参数是restTemplateType是用来选择restTemplate的,因为我们需要不同的超时时间,一个restTemplate只能在初始化的时候设置一个超时时间,所以我只能通过设置多个具有不同超时时间的template来实现针对不同超时时间的重试,如果有更好的方法,麻烦大家指导一下。
其中执行任务的接口Task代码如下
package com.wxind.httpexecutor.executor; import org.springframework.web.client.RestTemplate; /** * Created by wangxindong on 18/5/17 */ public interface Task<T> { T excutor(RestTemplate restTemplate); }重试机制Retryer以及元数据RetryMeta代码如下
package com.wxind.httpexecutor.executor; /** * 重试机制 * Created by wangxindong on 18/5/16 */ @FunctionalInterface public interface Retryer { /** * 是否重试函数 * 用来设置判断重试的条件 * @param e * @return */ boolean retry(RetryException e); /** * * @param retryer 重试机制 * @param retryException 用来承载抛出的异常和正常的返回值 * @param retryMeta 重试的元数据 * @return */ default boolean continueOrEnd(Retryer retryer, RetryException retryException, RetryMeta retryMeta){ //判断是否超出设置的重试次数 if (null == retryMeta || retryMeta.attempt++ > retryMeta.maxAttempts){ return false; } //判断是否符合重试机制的要求 boolean retry = retryer.retry(retryException); //符合重试要求,进行重试 if (retry){ try { //重试间隔通过sleep实现 Thread.sleep(retryMeta.interval); } catch (InterruptedException e) { } } return retry; } /** * 重试机制所需要的元数据 */ class RetryMeta{ public static final RetryMeta NO_RETRY = new RetryMeta(0,0); /** * 最大重试次数 */ private int maxAttempts; /** * 重试间隔 * 单位为毫秒 */ private int interval; /** * 尝试次数 */ private int attempt; /** * 默认构造 * 尝试二次 * 每次间隔2秒 */ public RetryMeta() { this.maxAttempts = 2; this.interval = 2000; this.attempt = 1; } public RetryMeta(int maxAttempts, int interval) { this.maxAttempts = maxAttempts; this.interval = interval; this.attempt = 1; } public int getMaxAttempts() { return maxAttempts; } public void setMaxAttempts(int maxAttempts) { this.maxAttempts = maxAttempts; } public int getInterval() { return interval; } public void setInterval(int interval) { this.interval = interval; } public int getAttempt() { return attempt; } public void setAttempt(int attempt) { this.attempt = attempt; } } }可以看到,我们提供了一个函数式的接口Retryer(所谓函数式接口就是只有一个待实现方法的接口)。这个接口包含了一个需要实现的retry方法,该方法就是用来设置具体的重试条件的。还包含了一个默认的方法continueOrEnd,用来判断是否需要进行重试。还包含一个重试的元数据,元数据中有三个属性:最大重试次数,重试间隔,以及已经重试的次数,默认的构造方法构造了一个重试两次,每次间隔两秒的元数据。continueOrEnd的入参有一个ReTryException用来承载任务抛出的异常和正常的返回值
package com.wxind.httpexecutor.executor; /** * Created by wangxindong on 18/5/17 */ public class RetryException extends RuntimeException { private Object result; private Exception exception; public Object getResult() { return result; } public void setResult(Object result) { this.result = result; } public Exception getException() { return exception; } public void setException(Exception exception) { this.exception = exception; } public void clear(){ this.result = null; this.exception = null; } }RestTemplateType是一个枚举,定义了RestTemplate的类型,代码如下
package com.wxind.httpexecutor.executor; /** * Created by wangxindong on 18/5/17 */ public enum RestTemplateType { SHORT,LONG,COMMON }定义了三种超时时间的template此外,还需要一个用来选择restTemplate的路由器Router
package com.wxind.httpexecutor.executor; /** * Created by wangxindong on 18/4/20 */ public interface Router<T,K> { T choose(K key); }因为考虑到这个路由器以后不止用来路由template,所以建一个抽象类实现他,抽象的实现如下:package com.wxind.httpexecutor.executor; import org.springframework.web.client.RestTemplate; /** * Created by wangxindong on 18/5/22 */ public abstract class RestTemplateRouter<T extends RestTemplate> implements Router<RestTemplate,String> { @Override public abstract RestTemplate choose(String key); }具体的实现如下:package com.wxind.httpexecutor.executor; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; /** * Created by wangxindong on 18/5/17 */ @Component("restTemplateRouter") public class DynamicsRestTemplateRouter extends RestTemplateRouter { @Resource(name = "shortRestTemplate") private RestTemplate bigDataRestTemplate; @Resource(name = "longRestTemplate") private RestTemplate longRestTemplate; @Resource(name = "commonRestTemplate") private RestTemplate commonRestTemplate; @Override public RestTemplate choose(String key) { switch (key){ case "SHORT": return bigDataRestTemplate; case "LONG": return longRestTemplate; default: return commonRestTemplate; } } }根据定义的枚举选出具体的template。准备阶段到此为止,下面进行具体的接口实现
2.具体实现
先来实现一个默认的重试机制Retryer,该机制在连接其他接口异常(包括连接读取超时)时允许重试
package com.wxind.httpexecutor.executor; import org.springframework.stereotype.Component; import org.springframework.web.client.ResourceAccessException; /** * Created by wangxindong on 18/5/17 */ @Component public class CommonRetryer implements Retryer{ @Override public boolean retry(RetryException e) { if (e.getException() instanceof ResourceAccessException){ return true; } return false; } }到重头戏了,HttpExecutors的实现
package com.wxind.httpexecutor.executor.impl; import com.alibaba.fastjson.JSON; import com.wxind.httpexecutor.executor.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; /** * Created by wangxindong on 18/5/17 */ @Component public class CommonExecutors implements HttpExecutors { private Logger logger = LoggerFactory.getLogger(getClass()); @Resource(name = "restTemplateRouter") private Router restTemplateRouter; //默认的重试机制 private static final Retryer DEFAULT_RETRYER = new CommonRetryer(); @Override public <T> T executor(Task<T> task, Retryer retryer, Retryer.RetryMeta retryMeta, RestTemplateType restTemplateType) throws Exception { //先根据restTemplateType选出需要用到的restTemplate RestTemplate restTemplate = (RestTemplate) restTemplateRouter.choose(restTemplateType.name()); T result = null; //new一个承载异常和正常返回值的容器RetryException RetryException exception = new RetryException(); long overallStartTime=System.currentTimeMillis(); logger.info("[HttpExecutors--进入HttpExecutors-{},本次任务最大需要重试{}次]", overallStartTime,null == retryMeta ? 0 : retryMeta.getMaxAttempts()); while (true) { long startTime=System.currentTimeMillis(); try { //每次循环时先清空RetryException容器 exception.clear(); //执行我们具体的任务 result = task.excutor(restTemplate); //如果返回的结果正常,将结果放入RetryException exception.setResult(result); } catch (Exception e) { //如果返回异常,将异常放入RetryException exception.setException(e); }finally { logger.info("[HttpExecutors--本次HTTP请求结束]" +"\r\n结果:[{}]" + "\r\n异常信息:[{}]" + "\r\n耗时:[{}]", JSON.toJSONString(result),exception.getException() ,(System.currentTimeMillis()-startTime)); logger.info("[HttpExecutors--判断本次HTTP请求是否触发重试]"); //判断是否需要重试 if (null != retryer && retryer.continueOrEnd(retryer,exception,retryMeta)){ logger.info("[HttpExecutors--重试机制生效,已等待:{},开始第{}次重试]",retryMeta.getInterval(), retryMeta.getAttempt() - 1); //进行重试,此时已经睡了间隔时间 continue; }else { //跳出重试 logger.info("[HttpExecutors--HTTP请求{},准备退出]",null == retryer ? "无重试机制" : null == retryMeta || retryMeta.getMaxAttempts() == 0 ? "无重试次数" : retryMeta.getAttempt() > retryMeta.getMaxAttempts() ? "到达重试临界值" : "不满足重试要求"); if (null != exception.getResult()){ //跳出重试时任务执行没有异常 logger.info("[HttpExecutors--退出--HTTP请求状态正常,返回结果可能未能达到预期]-[总耗时:{}]",System.currentTimeMillis() - overallStartTime); return result; }else { //跳出重试时任务执行发生异常 logger.error("[HttpExecutors--退出--HTTP请求状态异常]-[总耗时:{}]", System.currentTimeMillis() - overallStartTime); throw exception.getException(); } } } } } @Override public <T> T executor(Task<T> task, boolean needRetry, RestTemplateType restTemplateType) throws Exception { //如果needRetry为true,使用默认的重试机制,以及默认的重试元数据(重试2次,间隔2秒) return executor(task, needRetry ? DEFAULT_RETRYER : null, needRetry ? new Retryer.RetryMeta() : Retryer.RetryMeta.NO_RETRY, restTemplateType); } }此外,还要记得初始化三个restTemplate
package com.wxind.httpexecutor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.web.client.RestTemplate; import javax.net.ssl.*; import java.io.IOException; import java.net.HttpURLConnection; import java.nio.charset.Charset; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; @SpringBootApplication public class HttpexecutorApplication { private static final HostnameVerifier PROMISCUOUS_VERIFIER = (s, sslSession ) -> true; public static void main(String[] args) { SpringApplication.run(HttpexecutorApplication.class, args); } @Bean public RestTemplate commonRestTemplate(){ RestTemplate restTemplate = new RestTemplate(); //验证主机名和服务器验证方案的匹配是可接受的 restTemplate.setRequestFactory(getRequestFactory(20000,20000)); List<ClientHttpRequestInterceptor> interceptorsTimeout = new ArrayList<ClientHttpRequestInterceptor>(); interceptorsTimeout.add(new HeaderRequestInterceptor()); restTemplate.setInterceptors(interceptorsTimeout); StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); restTemplate.getMessageConverters().set(1,stringConverter); return restTemplate; } @Bean public RestTemplate longRestTemplate(){ RestTemplate restTemplate = new RestTemplate(); //验证主机名和服务器验证方案的匹配是可接受的 restTemplate.setRequestFactory(getRequestFactory(30000,30000)); List<ClientHttpRequestInterceptor> interceptorsTimeout = new ArrayList<ClientHttpRequestInterceptor>(); interceptorsTimeout.add(new HeaderRequestInterceptor()); restTemplate.setInterceptors(interceptorsTimeout); StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); restTemplate.getMessageConverters().set(1,stringConverter); return restTemplate; } @Bean public RestTemplate shortRestTemplate(){ RestTemplate bigDataRestTemplate = new RestTemplate(); //验证主机名和服务器验证方案的匹配是可接受的 bigDataRestTemplate.setRequestFactory(getRequestFactory(10000,10000)); List<ClientHttpRequestInterceptor> interceptorsTimeout = new ArrayList<ClientHttpRequestInterceptor>(); interceptorsTimeout.add(new HeaderRequestInterceptor()); bigDataRestTemplate.setInterceptors(interceptorsTimeout); StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); bigDataRestTemplate.getMessageConverters().set(1,stringConverter); return bigDataRestTemplate; } private SimpleClientHttpRequestFactory getRequestFactory(int readTimeout , int connectTimeout) { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() { @Override protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { if (connection instanceof HttpsURLConnection) { ((HttpsURLConnection) connection).setHostnameVerifier(PROMISCUOUS_VERIFIER); ((HttpsURLConnection) connection).setSSLSocketFactory(trustSelfSignedSSL()); } super.prepareConnection(connection, httpMethod); } }; requestFactory.setReadTimeout(readTimeout); requestFactory.setConnectTimeout(connectTimeout); return requestFactory; } private static class HeaderRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpRequest wrapper = new HttpRequestWrapper(request); wrapper.getHeaders().set("Accept-charset", "utf-8"); return execution.execute(wrapper, body); } } public SSLSocketFactory trustSelfSignedSSL() { try { SSLContext ctx = SSLContext.getInstance("TLS"); X509TrustManager tm = new X509TrustManager() { public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException { } public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException { } public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } }; ctx.init(null, new TrustManager[]{tm}, null); return ctx.getSocketFactory(); } catch (Exception ex) { throw new RuntimeException("Exception occurred ", ex); } } }三种template只有超时时间和连接时间不同。3.执行示例
因为Retryer和Task都是函数式接口,所以可以直接用lambda表达式或者具体的实现类,xxxxxxxxx为具体的url。 Task接口的入参可以改成泛型,从而实现任意任务的重试(不仅限于http任务)。//重试机制为全部重试,重试1次,间隔2秒 try { RuntimeNode.Request excutor = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", RuntimeNode.Request.class, requestObject), (RetryException e) -> true, new Retryer.RetryMeta(1, 2000), RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //重试机制抛出异常,并且异常为NullPointerException时重试,重试3次,间隔1秒 try { HashMap excutor1 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), (RetryException e) -> e.getException() instanceof NullPointerException, new Retryer.RetryMeta(3, 1000), RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //重试机制为返回值为HashMap,并且HashMap包含111的key时进行重试,重试3次,间隔1秒 try { HashMap excutor1 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), (RetryException e) -> { HashMap result = (HashMap) e.getResult(); return result.containsKey("111"); }, new Retryer.RetryMeta(3, 1000), RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //重试机制为全部重试,重试0次,间隔2秒 try { HashMap excutor2 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), (RetryException e) -> true, new Retryer.RetryMeta(0, 2000), RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //重试机制为全部重试,无重试元数据 不进行重试 try { HashMap excutor3 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), (RetryException e) -> true, null, RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //无重试机制,重试3次,间隔1秒 不进行重试 try { HashMap excutor4 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), null, new Retryer.RetryMeta(3, 1000), RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //无重试机制,无重试元数据 不进行重试 try { HashMap excutor5 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), null, null, RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //needRetry为false,不进行重试 try { HashMap excutor6 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), false, RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //needRetry为true,按默认机制和默认元数据重试 try { HashMap excutor7 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), true, RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //重试机制为全部不重试,重试次数为0,间隔为2秒 不进行重试 try { HashMap excutor8 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), (RetryException e) -> false, new Retryer.RetryMeta(0, 2000), RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //重试机制为全部不重试,重试次数为3,间隔为2秒 不进行重试 try { HashMap excutor9 = httpClient.executor((RestTemplate r) -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), (RetryException e) -> false, new Retryer.RetryMeta(3, 2000), RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); } //重试机制为全部不重试,无重试元数据 不进行重试 try { HashMap excutor10 = httpClient.executor(r -> r.getForObject("xxxxxxxxx", HashMap.class, requestObject), (RetryException e) -> false, null, RestTemplateType.LONG); } catch (Exception e) { e.printStackTrace(); }