1、介绍

spring retry是从spring batch独立出来的一个能功能,主要实现了重试和熔断,对于重试是有场景限制的,不是什么场景都适合重试, 比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。 比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。

2、框架介绍

3、概念类

1、BackOff:

补偿值,一般指失败后多久进行重试的延迟值。

2、Sleeper:

暂停应用的工具,通常用来应用补偿值。

3、BackOffPolicy:

退避策略,决定失败后如何确定补偿值。是立即重试还是等待一段时间后重试,比如是网络错误,立即重试将导致立即失败,最好等待一小段时间后重试,还要防止很多服务同时重试导致DDos

BackOffPolicy 提供了如下策略实现:

1、NoBackOffPolicy

​ 无退避算法策略,即当重试时是立即重试;

protected void doBackOff() throws BackOffInterruptedException {
}@Override
public String toString() {return "NoBackOffPolicy []";
}

2、FixedBackOffPolicy

​ 固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒;

/*** Default back off period - 1000ms.*/
private static final long DEFAULT_BACK_OFF_PERIOD = 1000L;/*** The back off period in milliseconds. Defaults to 1000ms.*/
private volatile long backOffPeriod = DEFAULT_BACK_OFF_PERIOD;private Sleeper sleeper = new ThreadWaitSleeper();public FixedBackOffPolicy withSleeper(Sleeper sleeper) {FixedBackOffPolicy res = new FixedBackOffPolicy();res.setBackOffPeriod(backOffPeriod);res.setSleeper(sleeper);return res;
}/*** Public setter for the {@link Sleeper} strategy.* @param sleeper the sleeper to set defaults to {@link ThreadWaitSleeper}.*/
public void setSleeper(Sleeper sleeper) {this.sleeper = sleeper;
}/*** Set the back off period in milliseconds. Cannot be < 1. Default value is 1000ms.* @param backOffPeriod the back off period*/
public void setBackOffPeriod(long backOffPeriod) {this.backOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1);
}/*** The backoff period in milliseconds.* @return the backoff period*/
public long getBackOffPeriod() {return backOffPeriod;
}/*** Pause for the {@link #setBackOffPeriod(long)}.* @throws BackOffInterruptedException if interrupted during sleep.*/
protected void doBackOff() throws BackOffInterruptedException {try {sleeper.sleep(backOffPeriod);}catch (InterruptedException e) {throw new BackOffInterruptedException("Thread interrupted while sleeping", e);}
}

3、UniformRandomBackOffPolicy

​ 随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒;

private static final long DEFAULT_BACK_OFF_MIN_PERIOD = 500L;/**
* Default max back off period - 1500ms.
*/
private static final long DEFAULT_BACK_OFF_MAX_PERIOD = 1500L;private volatile long minBackOffPeriod = DEFAULT_BACK_OFF_MIN_PERIOD;private volatile long maxBackOffPeriod = DEFAULT_BACK_OFF_MAX_PERIOD;private Random random = new Random(System.currentTimeMillis());private Sleeper sleeper = new ThreadWaitSleeper();public UniformRandomBackOffPolicy withSleeper(Sleeper sleeper) {UniformRandomBackOffPolicy res = new UniformRandomBackOffPolicy();res.setMinBackOffPeriod(minBackOffPeriod);res.setSleeper(sleeper);return res;
}protected void doBackOff() throws BackOffInterruptedException {try {long delta = maxBackOffPeriod==minBackOffPeriod ? 0 : random.nextInt((int) (maxBackOffPeriod - minBackOffPeriod));sleeper.sleep(minBackOffPeriod + delta );}catch (InterruptedException e) {throw new BackOffInterruptedException("Thread interrupted while sleeping", e);}
}

4、ExponentialBackOffPolicy

​ 指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier;

protected final Log logger = LogFactory.getLog(this.getClass());
/*** The default 'initialInterval' value - 100 millisecs. Coupled with the default* 'multiplier' value this gives a useful initial spread of pauses for 1-5 retries.*/
public static final long DEFAULT_INITIAL_INTERVAL = 100L;/*** The default maximum backoff time (30 seconds).*/
public static final long DEFAULT_MAX_INTERVAL = 30000L;/*** The default 'multiplier' value - value 2 (100% increase per backoff).*/
public static final double DEFAULT_MULTIPLIER = 2;/*** The initial sleep interval.*/
private volatile long initialInterval = DEFAULT_INITIAL_INTERVAL;/*** The maximum value of the backoff period in milliseconds.*/
private volatile long maxInterval = DEFAULT_MAX_INTERVAL;/*** The value to increment the exp seed with for each retry attempt.*/
private volatile double multiplier = DEFAULT_MULTIPLIER;private Sleeper sleeper = new ThreadWaitSleeper();/*** Public setter for the {@link Sleeper} strategy.* @param sleeper the sleeper to set defaults to {@link ThreadWaitSleeper}.*/
public void setSleeper(Sleeper sleeper) {this.sleeper = sleeper;
}public ExponentialBackOffPolicy withSleeper(Sleeper sleeper) {ExponentialBackOffPolicy res = newInstance();cloneValues(res);res.setSleeper(sleeper);return res;
}public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {ExponentialBackOffContext context = (ExponentialBackOffContext) backOffContext;
try {long sleepTime = context.getSleepAndIncrement();if (logger.isDebugEnabled()) {logger.debug("Sleeping for " + sleepTime);}sleeper.sleep(sleepTime);
}catch (InterruptedException e) {throw new BackOffInterruptedException("Thread interrupted while sleeping", e);}
}public synchronized long getSleepAndIncrement() {long sleep = this.interval;if (sleep > maxInterval) {sleep = maxInterval;}else {this.interval = getNextInterval();}return sleep;}protected long getNextInterval() {return (long) (this.interval * this.multiplier);
}

5、ExponentialRandomBackOffPolicy

​ 随机指数退避策略,引入随机乘数,之前说过固定乘数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。

public class ExponentialRandomBackOffPolicy extends ExponentialBackOffPolicy {/*** Returns a new instance of {@link org.springframework.retry.backoff.BackOffContext},* seeded with this policies settings.*/public BackOffContext start(RetryContext context) {return new ExponentialRandomBackOffContext(getInitialInterval(), getMultiplier(),getMaxInterval());}protected ExponentialBackOffPolicy newInstance() {return new ExponentialRandomBackOffPolicy();}static class ExponentialRandomBackOffContextextends ExponentialBackOffPolicy.ExponentialBackOffContext {private final Random r = new Random();public ExponentialRandomBackOffContext(long expSeed, double multiplier,long maxInterval) {super(expSeed, multiplier, maxInterval);}@Overridepublic synchronized long getSleepAndIncrement() {long next = super.getSleepAndIncrement();next = (long) (next * (1 + r.nextFloat() * (getMultiplier() - 1)));return next;}}
}

4、RetryContext:

​ 重试上下文,代表了能被重试动作使用的资源。

​ 重试上下文接口为RetryContext,其中继承了AttributeAccessor接口,此接口提供了对扩展属性的增删查改操作,用于对不同重试情况下特性的支持,RetryContext实现由AttributeAccessorSupport类实现(map集合实现)。RetryContextSupport类提供了一些重试基础数据的记录,包括重试次数,重试上文,最新异常,是否结束等。默认实现为SimpleRetryContext。

SimpleRetryContext:

​ 默认实现;

NeverRetryContext:

​ 新增是否结束字段;TimeoutRetryContext:

​ 扩展开始时间和过期时间字段,判断是否过期

5、RetryPolicy:

​ 重试策略,决定失败能否重试。

RetryPolicy提供了如下策略实现:

1、NeverRetryPolicy:

​ 只允许调用RetryCallback一次,不允许重试;

public class NeverRetryPolicy implements RetryPolicy {/*** Returns false after the first exception. So there is always one try, and* then the retry is prevented.* * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext)*/public boolean canRetry(RetryContext context) {return !((NeverRetryContext) context).isFinished();}private static class NeverRetryContext extends RetryContextSupport {private boolean finished = false;public NeverRetryContext(RetryContext parent) {super(parent);}public boolean isFinished() {return finished;}public void setFinished() {this.finished = true;}}

2、AlwaysRetryPolicy:

​ 允许无限重试,直到成功,此方式逻辑不当会导致死循环;

public class AlwaysRetryPolicy extends NeverRetryPolicy {/*** Always returns true.* * @see org.springframework.retry.RetryPolicy#canRetry(org.springframework.retry.RetryContext)*/public boolean canRetry(RetryContext context) {return true;}}

3、SimpleRetryPolicy:

​ 固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略;

/*** The default limit to the number of attempts for a new policy.*/
public final static int DEFAULT_MAX_ATTEMPTS = 3;private volatile int maxAttempts;private BinaryExceptionClassifier retryableClassifier = new BinaryExceptionClassifier(false);/*** Create a {@link SimpleRetryPolicy} with the default number of retry* attempts, retrying all exceptions.*/
public SimpleRetryPolicy() {this(DEFAULT_MAX_ATTEMPTS, Collections.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true));
}@Override
public boolean canRetry(RetryContext context) {Throwable t = context.getLastThrowable();return (t == null || retryForException(t)) && context.getRetryCount() < maxAttempts;
}

4、TimeoutRetryPolicy:

​ 超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试;

/*** Default value for timeout (milliseconds).*/
public static final long DEFAULT_TIMEOUT = 1000;private long timeout = DEFAULT_TIMEOUT;public boolean canRetry(RetryContext context) {return ((TimeoutRetryContext) context).isAlive();
}private static class TimeoutRetryContext extends RetryContextSupport {private long timeout;private long start;public TimeoutRetryContext(RetryContext parent, long timeout) {super(parent);this.start = System.currentTimeMillis();this.timeout = timeout;}public boolean isAlive() {return (System.currentTimeMillis() - start) <= timeout;}
}

5、CircuitBreakerRetryPolicy:

​ 有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate,具体擦看熔断源码

6、CompositeRetryPolicy:

​ 组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行。

public boolean canRetry(RetryContext context) {RetryContext[] contexts = ((CompositeRetryContext) context).contexts;RetryPolicy[] policies = ((CompositeRetryContext) context).policies;boolean retryable = true;if(this.optimistic) {retryable = false;for (int i = 0; i < contexts.length; i++) {if (policies[i].canRetry(contexts[i])) {retryable = true;}}}else {for (int i = 0; i < contexts.length; i++) {if (!policies[i].canRetry(contexts[i])) {retryable = false;}}}return retryable;
}

7、ExceptionClassifierRetryPolicy:

​ 设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试

6、RecoveryCallback:

​ 定义一个动作recover,在重试耗尽后的动作。

当重试耗尽时,RetryOperations可以将控制权传递给另一个回调RecoveryCallback,要使用此功能,客户端只需将回调函数一起传递给相同的方法,

final RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {public Object recover(RetryContext context) throws Exception {System.out.println("do recory operation");System.out.println(context.getAttribute("key1"));return null;}
};final Object execute = retryTemplate.execute(retryCallback, recoveryCallback);

7、RetryCallback:

​ 具体的重试动作。

8、RetryOperations:

​ 类主要实现了对重试的接口,RetryTemplate为其实现。主要包括execute方法,RetryCallback为重试需要执行的操作,RecoverCallback为重试结束后如何返回,比如提供默认值等等,即重试失败后需要进行的操作。RetryState retryState 重试状态,通常包含一个重试的键值,主要分为有状态和无状态重试,RetryTemplate默认的重试策略为SimpleRetryPlicy

<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E;<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState) throws E, ExhaustedRetryException;<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState)throws E;

9、RetryTemplate

​ RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy。具体执行doExecute

10、RetryState:

​ 有无重试状态,通常包含一个重试的键值。

1、无重试状态

​ 在最简单的情况下,重试只是一个while循环,RetryTemplate可以一直尝试,直到成功或失败。RetryContext包含一些状态来决定是重试还是中止,但是这个状态位于堆栈上,不需要将它存储在全局的任何位置,因此我们将此称为无状态重试。无状态重试和有状态重试之间的区别包含在RetryPolicy的实现中(RetryTemplate可以同时处理这两种情况),在无状态重试中,回调总是在重试失败时在同一个线程中执行。

2、有重试状态

如果失败导致事务性资源无效,则需要特别考虑,这并不适用于简单的远程调用,因为(通常)没有事务资源,但有时确实适用于数据库更新,尤其是在使用有事务的时候,在这种情况下,只有立即重新抛出调用失败的异常才有意义,以便事务可以回滚并启动一个新的有效的事务。

在这些情况下,无状态重试是不够的,因为重新抛出和回滚必然会离开RetryOperations.execute()方法,并可能丢失堆栈上的上下文。为了避免丢失它,我们必须引入一种存储策略,将它从堆栈中取出并(至少)放入堆存储中,为此,Spring Retry提供了一种存储策略RetryContextCache,可以将其注入RetryTemplate,RetryContextCache的默认实现在内存中,使用一个简单的Map,它有一个严格执行的最大容量,以避免内存泄漏,但它没有任何高级缓存功能,如生存时间。如果需要,应该考虑注入具有这些特性的

3、有状态重试情况

​ 主要分为事务回滚和熔断

比如:

​ 1、如数据库操作异常DataAccessException,则不能执行重试,而如果抛出其他异常可以重试。

​ 2、 熔断的意思不在当前循环中处理重试,而是全局重试模式(不是线程上下文)。熔断会跳出循环,那么必然会丢失线程上下文的堆栈信息。那么肯定需要一种“全局模式”保存这种信息,目前的实现放在一个cache(map实现的)中,下次从缓存中获取就能继续重试了。

3.1 事务代码:

RetryTemplate retryTemplate = new RetryTemplate();
//重试策略SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections.singletonMap(DataAccessException.class, true));
//退避策略FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(100);retryTemplate.setRetryPolicy(policy);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);//有状态 事务
BinaryExceptionClassifier classifier = new BinaryExceptionClassifier( Collections.singleton(DataAccessException.class));
RetryState state =new DefaultRetryState("mykey",false, classifier);final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, Exception>() {public Object doWithRetry(RetryContext context) throws DataAccessException {System.out.println("do some thing");//设置context一些属性,给RecoveryCallback传递一些属性context.setAttribute("key1", "value1");System.out.println(context.getRetryCount());throw new CannotAcquireLockException("exception");}
};// 如果RetryCallback执行出现指定异常, 并且超过最大重试次数依旧出现指定异常的话,就执行RecoveryCallback动作
final RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {public Object recover(RetryContext context) throws DataAccessException {System.out.println("do recory operation");System.out.println(context.getAttribute("key1"));return null;}
};try {final Object execute = retryTemplate.execute(retryCallback, recoveryCallback,state);
} catch (Exception e) {e.printStackTrace();
}
3.2 熔断器代码:

public static void main(String[] args) {RetryTemplate retryTemplate = new RetryTemplate();//重试策略  delegate是真正判断是否重试的策略,当重试失败时,则执行熔断策略CircuitBreakerRetryPolicy retryPolicy  = new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(3));//openWindow,配置熔断器电路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路)retryPolicy.setOpenTimeout(5000);//timeout,配置重置熔断器重新闭合的超时时间。retryPolicy.setResetTimeout(20000);retryTemplate.setRetryPolicy(retryPolicy);for (int i = 0; i <10 ; i++) {try {Object key = "circuit";boolean isForceRefresh = false;RetryState state = new DefaultRetryState(key, isForceRefresh);//退避策略FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();fixedBackOffPolicy.setBackOffPeriod(100);retryTemplate.setBackOffPolicy(fixedBackOffPolicy);RetryCallback<Object, RuntimeException> retryCallback = new RetryCallback<Object, RuntimeException>() {public Object doWithRetry(RetryContext context) throws RuntimeException {System.out.println("do some thing");//设置context一些属性,给RecoveryCallback传递一些属性context.setAttribute("key1", context.getRetryCount());System.out.println(context.getRetryCount());System.out.println(context.getAttribute("key1").toString()+"--");if(context.getAttribute("key1").toString().equals("2")){System.out.println(context.getAttribute("key1").toString()+"-----------------------");return null;}else{throw new RuntimeException("exception");}}};// 如果RetryCallback执行出现指定异常, 并且超过最大重试次数依旧出现指定异常的话,就执行RecoveryCallback动作RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {public Object recover(RetryContext context) throws RuntimeException {System.out.println("do recory operation");System.out.println(context.getAttribute("key1"));System.out.println(context.getAttribute("key1---------------------------------"));return null;}};Object execute = retryTemplate.execute(retryCallback, recoveryCallback,state);System.out.println(execute);}catch (Exception e){}}

后面的循环一直是处于失败的,不会重新进行执行,断路器被打开

这里由于设置了isForceRefresh = false,则key = "circuit"的值(也就是RetryContext)会从缓存中获取,所以当重试失败且满足this.time < this.openWindow发生熔断的时候,后面仍然可以继续已全局模式实现重试(拿到的RetryContext是同一个)。

11、RetryStatistics和RetryListener

​ 用来监控Retry的执行情况,并生成统计信息。

典型的“监听者”,在重试的不同阶段通知“监听者”(例如doSth,wait等阶段时通知)

4、注解基本使用

1、pom新增

<!--retry-->
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId><version>1.2.4.RELEASE</version>
</dependency><!--AOP-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId>
</dependency>
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>

2、在启动类上添加打开注解

@EnableRetry
/*** value:指定失敗重試的Exception類型* include:和value效果一樣,當exclude為空時,所有Exceoption都會重試,預設空* exclude:指定Exceoption不重試,當include為空時,所有Exceoption都會重試,預設空* maxAttempts:指定重試的次數,預設3* backoff:重試補償機制,默认使用FixedBackOffPolicy(指定等待时间),重试等待1000ms*         @Backoff*              delay:指定延遲後重試*              multiplier:指定延遲的倍数,例如:delay=3000l, multiplier=2,第一次重試為3秒,第二次為6秒,第三次為12秒**/
@Retryable(value = DemoException.class, maxAttempts = 4, backoff = @Backoff(delay = 3000l,multiplier = 2))
public void testSpringRetry() throws DemoException {System.out.println("输入------------------------------------------------------------------");throw new DemoException();
}
// 用于@Retryable重试失败后处理方法,此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理@Recoverpublic String recover(DemoException demoException, Integer id){// 全部重试失败处理}

@Retryable注解参数

参数 含义
interceptor 重试拦截器bean名称,用于可重试方法
value 可重试的异常类型。含义同include。默认为空(如果excludes也为空,则重试所有异常)
include 可重试的异常类型。默认为空(如果excludes也为空,则重试所有异常)
exclude 无需重试的异常类型。默认为空(如果includes也为空,则重试所有异常)
label 统计报告的唯一标签。如果未提供,
则调用者可以选择忽略它或提供一个默认值。
我的理解就是这个重试方法的唯一名称
stateful 若为true,标志重试是有状态的:即重新抛出异常,
但是重试策略与相同的策略应用于具有相同参数的后续调用。
若为false,则不会重新引发可重试的异常
maxAttempts 最大重试次数(包括第一次失败),默认为3次
maxAttemptsExpression 计算最大尝试次数(包括第一次失败)的表达式,默认为3 次
backoff 重试等待策略,下面会在@Backoff中介绍
exceptionExpression 指定在SimpleRetryPolicy.canRetry()返回true之后要求值的表达式-可用于有条件地禁止重试。

@Backoff注解

参数
value
delay 重试之间的等待时间(以毫秒为单位)
maxDelay 重试之间的最大等待时间(以毫秒为单位)
multiplier 指定延迟的倍数
delayExpression 重试之间的等待时间表达式
maxDelayExpression 重试之间的最大等待时间表达式
multiplierExpression 指定延迟的倍数表达式
random 随机指定延迟时间

5、基本使用

public class retryTest {public static void main(String[] args) {final RetryTemplate retryTemplate = new RetryTemplate();final SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean>singletonMap(Exception.class, true));FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();fixedBackOffPolicy.setBackOffPeriod(100);retryTemplate.setRetryPolicy(policy);retryTemplate.setBackOffPolicy(fixedBackOffPolicy);final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, Exception>() {public Object doWithRetry(RetryContext context) throws Exception {System.out.println("do some thing");//设置context一些属性,给RecoveryCallback传递一些属性context.setAttribute("key1", "value1");System.out.println(context.getRetryCount());throw new Exception("exception");//                return null;}};// 如果RetryCallback执行出现指定异常, 并且超过最大重试次数依旧出现指定异常的话,就执行RecoveryCallback动作final RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {public Object recover(RetryContext context) throws Exception {System.out.println("do recory operation");System.out.println(context.getAttribute("key1"));return null;}};// 设置listenerretryTemplate.setListeners(new RetryListener[]{new RetryListener() {@Overridepublic <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {System.out.println("RetryListener-open");return true;}@Overridepublic <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,Throwable throwable) {System.out.println("RetryListener-close");}@Overridepublic <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,Throwable throwable) {System.out.println("RetryListener-onError");}}});try {final Object execute = retryTemplate.execute(retryCallback, recoveryCallback);} catch (Exception e) {e.printStackTrace();}}
}

6、源码分包

1、classify

我们发现这个包里面还有一个和retry同级的classify包,显然它应该是retry需要用到,但是又不是包含的retry模型里面的东西

classify包作为retry的辅助类,主要应用于RetryPolicy的canRetry()方法中,通过比较捕获的异常与定义的异常直接关系,决定是否符合重试条件

1、Classifier<C, T> 接口:

​ 是整个包的核心接口,定义了 T classify(C classifiable);把一个C类型对象,转换为T类型对象,其中T类型通常是枚举类型或者布尔类型这种可以直接比较结果的。

2、ClassifierSupport<C, T> 类:

​ 一个基础实现,引入默认值机制,无论要传入任何C类型对象,都返回默认的T类型对象。

3、ClassifierAdapter<C, T> 类:

​ 定义了两种方式的转换,一种直接Classifier,在需要转换时候调用,一种传入通过识别一个目标类中@Classifier注解的方法,把它作为转换的实现类,在需要转换时候调用。

4、SubclassClassifier<T, C>类:

​ 首先要注意这里T和C对调写了,实现了能识别一个类和其子类都能被识别到,转换为目标类型对象的机制。这对于retry需要的异常类的转换十分重要,通常我们只需要定义某一类的异常重试策略,那么其子类异常也会同样应用到该策略,比如我们定义了数据库错误SQLException需要重试,实际运行可能抛出的是SQLTimeoutException,或者BatchUpdateException,它们就都会被捕获重试。

5、BinaryExceptionClassifier类:

​ 明确化了SubclassClassifier<T, C>的类型,其classify()方法把Throwable转换为Boolean。代码如下:

@Override
public Boolean classify(Throwable classifiable) {Boolean classified = super.classify(classifiable);if (!this.traverseCauses) {return classified;}/** If the result is the default, we need to find out if it was by default* or so configured; if default, try the cause(es).*/if (classified.equals(this.getDefault())) {Throwable cause = classifiable;do {if (this.getClassified().containsKey(cause.getClass())) {return classified; // non-default classification}cause = cause.getCause();classified = super.classify(cause);}while (cause != null && classified.equals(this.getDefault()));}return classified;
}

​ 如果traverseCauses为false,就简单调用父类进行转换即可,如果为真,就必须一直找Throwable的Cause链条,直到找到匹配的转换。

6、PatternMatcher和PatternMatchingClassifier :

能够把符合样式的字符串转换为T对象的转换器。

其核心方法为imatch(),对?和*进行了处理判断,判断输入的str是否符合某种样式pattern。

7、BackToBackPatternClassifier<C, T> 类:

背对背映射组合,先吧C对象转换为string,然后再把string转换为T对象。

2、retry顶级接口

1、RetryContext接口:

​ 从图上可以看到,重试上下文处于核心位置,作为核心数据接口,储存了重试所需要的各类信息。

​ RetryContext getParent();

​ 从获取父上下文方法可知,它是一个链式结构。

2、RetryPolicy接口:

 //判断一个上下文能否重试
boolean canRetry(RetryContext context);//开启一个重试上下文环境
RetryContext open(RetryContext parent);//关闭一个重试上下文环境
void close(RetryContext context);//出现异常时候,把异常登记到上下文中
void registerThrowable(RetryContext context, Throwable throwable);

从接口参数可以看出,策略都是根据上下文情况进行判断分析的。

3、RetryOperations接口:

​ 各种花式execute(),根据可配置的重试行为,进行方法的执行,其具体的实现就是核心类RetryTemplate

4、RetryListener接口:

​ 作为重试动作的监听器,给spring-retry加点料,用在统计机制上。监听3类动作:open()在开启操作之前,close()在关闭操作之后,onError()在出现错误时。

5、RetryStatistics接口:

​ 记录重试统计信息的接口。登记完成数、开始数、错误数、中止数、恢复数。

6、RetryException及ExhaustedRetryException,TerminatedRetryException异常

​ 定义了retry项目内部可能抛出的异常,RetryException是基类。

3、backoff包

1、BackOffPolicy接口:

​ 该包的核心接口,包含两个方法,一是生成一个当前补偿上下文环境,二是进行补偿动作

根据重试上下文生成一个补偿上下文
BackOffContext start(RetryContext context);//根据补偿上下文执行延迟操作,可能抛出中断异常
void backOff(BackOffContext backOffContext) throws BackOffInterruptedException;

2、Sleeper接口与ThreadWaitSleeper类

​ 真正的补偿动作具体执行器, ThreadWaitSleeper就是调用了Thread.sleep()方法进行延迟

3、StatelessBackOffPolicy抽象类

​ 其start方法返回null,也就是没有重试上下文,执行backOff时候调用的是无参数的doBackOff()。换句话说,代表具体补偿动作是固定的,并不依赖上下文环境。

4、NoBackOffPolicy类

​ 最简单的默认策略,具体延迟为空操作,也就是不补偿,不延迟。

5、SleepingBackOffPolicy接口

​ 有一个withSleeper()方法,传入一个Sleeper。

6、UniformRandomBackOffPolicy类

​ 标准的随机延迟策略,给定最小值,最大值(默认为500L,1500L),会在这个区间里面随机进行补偿延迟。

7、 FixedBackOffPolicy类

​ 标准的固定延迟策略,每次延迟固定时间(默认1000L)

​ ExponentialBackOffPolicy类和ExponentialRandomBackOffPolicy:

​ 这两个类都是SleepingBackOffPolicy的实现,内部用ThreadWaitSleeper延迟。实现的是延迟指数倍增的效果,区别是ExponentialBackOffPolicy是固定倍增,ExponentialRandomBackOffPolicy加入了随机性。

4、context包

该包只有一个类RetryContextSupport,重试上下文的具体实现。

  1. 扩展AttributeAccessorSupport:内部有个linkedHashMap存储标准属性外的其他属性
  2. 有parent属性,在构造时候传入其父上下文,这样就维护了一个链表信息,方便后续查找。
  3. count和lastException是记录型属性,登记这个上下文的状态。

5、stats包

重试的统计信息

1、MutableRetryStatistics接口:

​ 增加了incrementc××()方法,增加统计值

2、DefaultRetryStatistics类:

​ 统计的默认实现,定义了一堆的AtomicInterger存储统计值,同时扩展自AttributeAccessorSupport,还能存放其他信息。

3、ExponentialAverageRetryStatistics类:

​ 增加了指数平均指标的统计值,

4、RetryStatisticsFactory接口和DefaultRetryStatisticsFactory实现:

​ 就一个create()方法,构造MutableRetryStatistics。默认是生产ExponentialAverageRetryStatistics统计类。

5、StatisticsRepository接口:

​ 统计仓库,可以存放多个统计信息的接口

6、DefaultStatisticsRepository类:

​ 统计仓库的默认实现,内部有一个DefaultRetryStatisticsFactory,如果找不到对应名字的统计,就由工厂生产一个。

6、listener包

就一个类RetryListenerSupport,其具体实现子类StatisticsListener位于stats包中。主要监听close()和onError()事件,并调用repository进行记录统计信息。

7、support包

1、DefaultRetryState类

​ 代表了一个新的重试尝试的状态。包含3个属性:

 //该状态的键值
final private Object key;//是否需要强制刷新
final private boolean forceRefresh;//回滚的转换器,当转换为true值是,这个异常将会引起回滚操作
final private Classifier<? super Throwable, Boolean> rollbackClassifier;

有个rollbackFor()用来判断某个异常是否需要引起回滚,如果没有rollbackClassifier,默认返回true,否则按照rollbackClassifier转换值进行判断。

2、RetrySimulation类

​ 代表一个发生器,有个SleepSequence内部类,代表一组sleep值,有最长的,有总和的。而发生器根据序列维护内部的sleepHistogram直方图,在获得百分比是能返回对应值。

3、RetrySimulator类

​ 用来执行补偿+重试的操作的工具类。在构造函数中传入SleepingBackOffPolicy和RetryPolicy作为内部属性。在executeSingleSimulation()方法中,设置好补偿机制和重置策略,然后直接通过template执行失败的FailingRetryException,模拟失败的动作,进行补偿和重试的组合操作。

4、RetrySynchronizationManager类

​ 在ThreadLocal中存放RetryContext,用来保证一个线程只能维护一个重试上下文,进行一个重试操作。毕竟Sleep是用Tread.sleep,如果多个重试,这个补偿机制就无法生效了。

5、RetryTemplate类

​ 这个是这个包最重要的一个类了,之前看到重试策略,回退策略,缓存、监听器都是应用在这里。

​ a.它实现了RetryOperations接口。

​ b.很有风格的是,execute是给外部调用的,真正内部起作用的是doExecute()

8、interceptor包

​ 主要是为了利用AOP机制,把任意声明为@Retryable的方法都变成可以可重试的

​ interceptor包和annotation紧密相关

1、MethodArgumentsKeyGenerator接口

​ Object getKey(Object[] item);

​ 传入item通常为方法的参数,返回的Object为这些参数的唯一标识

2、FixedKeyGenerator类

​ 其简单实现,无论何种参数数组传入,都返回给定的Label值。

3、MethodInvocationRecoverer接口

​ 定义回复接口方法,具体声明如下:

​ T recover(Object[] args, Throwable cause);

4、NewMethodArgumentsIdentifier接口

​ 区别判断一组参数,之前是否执行过

5、RetryOperationsInterceptor类

由于Retry是利用AOP机制实现的,因而需要定义MethodInterceptor把我们声明的方法绑定到RetryTemplate的调用中去。

public Object invoke(final MethodInvocation invocation) throws Throwable { //先取到该方法调用的名字、String name;if (StringUtils.hasText(label)) {name = label;} else {name = invocation.getMethod().toGenericString();}final String label = name; //构造回调函数RetryCallback<Object, Throwable> retryCallback = new RetryCallback<Object, Throwable>() {public Object doWithRetry(RetryContext context) throws Exception { // 在上下文中登记Label context.setAttribute(RetryContext.NAME, label);/** If we don't copy the invocation carefully it won't keep a reference to* the other interceptors in the chain. We don't have a choice here but to* specialise to ReflectiveMethodInvocation (but how often would another* implementation come along?).*/if (invocation instanceof ProxyMethodInvocation) { // 多一重保险,判断是代理方法调用  try {return ((ProxyMethodInvocation) invocation).invocableClone().proceed(); //实际利用动态方法调用,执行方法本身}catch (Exception e) {  // 捕捉后重新抛出 throw e;}catch (Error e) { // 捕捉后重新抛出      throw e;}catch (Throwable e) { // 其他错误,就是非法错误了。throw new IllegalStateException(e);}}else {throw new IllegalStateException("MethodInvocation of the wrong type detected - this should not happen with Spring AOP, " +"so please raise an issue if you see this exception");}}};// 判断有无恢复方法,如果有,就构造一个恢复回调if (recoverer != null) {ItemRecovererCallback recoveryCallback = new ItemRecovererCallback(invocation.getArguments(), recoverer);// 实际还是传入RetryTemplate执行方法调用return this.retryOperations.execute(retryCallback, recoveryCallback);}//执行retryCallback具体操作return this.retryOperations.execute(retryCallback);}

6、StatefulRetryOperationsInterceptor类

​ RetryOperationsInterceptor类是公用一个RetryTemplate的。而又状态的RetryOperationsInterceptor就必须每个实例都有自己的RetryTemplate,再配合RetryState决定是否需要抛出RollbackException了。其核心invoke方法如下:

7、RetryInterceptorBuilder类

​ 流式构造的工厂RetryInterceptor类。具体使用的例子如下:

StatefulRetryOperationsInterceptor interceptor = RetryInterceptorBuilder.stateful() //构造有状态的Interceptor       .maxAttempts(5).backOffOptions(1, 2, 10) // initialInterval, multiplier,      .build();

​ 这个工厂能生产3种不同的Interceptor,StatefulRetryInterceptor(有状态的),StatelessRetryInterceptor(无状态),CircuitBreakerInterceptor(有状态加熔断)。

8、RecoverAnnotationRecoveryHandler类

​ MethodInvocationRecoverer的实现,根据一个方法,查找对应@Recover注解方法,封装到Recovery处理之中,也就是@Retryable和@Recover的自动匹配过程。从构造器可以看出,保存了目标类和目标方法,然后进行解析。

// target为目标类,method为需要Revover的目标方法
public RecoverAnnotationRecoveryHandler(Object target, Method method) {this.target = target;init(target, method);
}

其核心的init方法代码如下:

private void init(Object target, final Method method) {
//保存传入的方法作为备份方法final Map<Class<? extends Throwable>, Method> types = new HashMap();//调用ReflectionUtils反射工具,查找符合条件目标方法ReflectionUtils.doWithMethods(method.getDeclaringClass(), new MethodCallback() {// 声明回调函数,每个符合条件的目标方法,都会登记到types和methods中public void doWith(Method methodx) throws IllegalArgumentException, IllegalAccessException {//查找@Recover接口  Recover recover = (Recover)AnnotationUtils.findAnnotation(methodx, Recover.class);if (recover != null && methodx.getReturnType().isAssignableFrom(method.getReturnType())) {Class<?>[] parameterTypes = methodx.getParameterTypes();//判断找到的方法和目标方法参数,异常是否一致 if (parameterTypes.length > 0 && Throwable.class.isAssignableFrom(parameterTypes[0])) {Class<? extends Throwable> type = parameterTypes[0];//登记下这个revover方法的参数types.put(type, methodx);RecoverAnnotationRecoveryHandler.this.methods.put(methodx, new RecoverAnnotationRecoveryHandler.SimpleMetadata(parameterTypes.length, type));} else {//找不到,就给配置个默认值RecoverAnnotationRecoveryHandler.this.classifier.setDefaultValue(methodx);RecoverAnnotationRecoveryHandler.this.methods.put(methodx, new RecoverAnnotationRecoveryHandler.SimpleMetadata(parameterTypes.length, (Class)null));}}}});this.classifier.setTypeMap(types);this.optionallyFilterMethodsBy(method.getReturnType());
}

9、AnnotationAwareRetryOperationsInterceptor类

​ 注解解析器,查找工程中@Retryable方法,并生成RetryOperationsInterceptor的类。

10、RetryConfiguration类

​ @EnableRetry引入的配置类,内部封装AnnotationClassOrMethodPointcut,AnnotationClassOrMethodFilter,AnnotationMethodsResolver三个Aop工具类。通过反射查找到目标方法,并应用aop给方法加料(生成proxy对象),从而实现把普通方法变成可重试方法。

7、源码执行

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,RecoveryCallback<T> recoveryCallback, RetryState state)throws E, ExhaustedRetryException {// 重试策略RetryPolicy retryPolicy = this.retryPolicy;//退避策略BackOffPolicy backOffPolicy = this.backOffPolicy;// Allow the retry policy to initialise itself...//重试上下文,当前重试次数等记录全在里面RetryContext context = open(retryPolicy, state);if (this.logger.isTraceEnabled()) {this.logger.trace("RetryContext retrieved: " + context);}// Make sure the context is available globally for clients who need// it...//注册确保上下文需要context,注册到当前线程中 LocalThreadRetrySynchronizationManager.register(context);Throwable lastException = null;boolean exhausted = false;try {// Give clients a chance to enhance the context...拦截器模式,执行RetryListenerboolean running = doOpenInterceptors(retryCallback, context);if (!running) {throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt");}// Get or Start the backoff context...BackOffContext backOffContext = null;Object resource = context.getAttribute("backOffContext");if (resource instanceof BackOffContext) {backOffContext = (BackOffContext) resource;}if (backOffContext == null) {backOffContext = backOffPolicy.start(context);if (backOffContext != null) {context.setAttribute("backOffContext", backOffContext);}}/** We allow the whole loop to be skipped if the policy or context already* forbid the first try. This is used in the case of external retry to allow a* recovery in handleRetryExhausted without the callback processing (which* would throw an exception).*///判断是否可以重试执行while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {try {if (this.logger.isDebugEnabled()) {this.logger.debug("Retry: count=" + context.getRetryCount());}// Reset the last exception, so if we are successful// the close interceptors will not think we failed...lastException = null;//执行RetryCallback回调return retryCallback.doWithRetry(context);}//异常时,要进行下一次重试准备catch (Throwable e) {lastException = e;try {//遇到异常后,注册该异常的失败次数registerThrowable(retryPolicy, state, context, e);}catch (Exception ex) {throw new TerminatedRetryException("Could not register throwable",ex);}finally {//执行RetryListener#onErrordoOnErrorInterceptors(retryCallback, context, e);}//如果可以重试,执行退避算法,比如休眠一小段时间后再重试if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {try {backOffPolicy.backOff(backOffContext);}catch (BackOffInterruptedException ex) {lastException = e;// back off was prevented by another thread - fail the retryif (this.logger.isDebugEnabled()) {this.logger.debug("Abort retry because interrupted: count="+ context.getRetryCount());}throw ex;}}if (this.logger.isDebugEnabled()) {this.logger.debug("Checking for rethrow: count=" + context.getRetryCount());}//在有状态重试时,如果是需要执行回滚操作的异常,则立即抛出异常if (shouldRethrow(retryPolicy, context, state)) {if (this.logger.isDebugEnabled()) {this.logger.debug("Rethrow in retry for policy: count="+ context.getRetryCount());}throw RetryTemplate.<E>wrapIfNecessary(e);}}/** A stateful attempt that can retry may rethrow the exception before now,* but if we get this far in a stateful retry there's a reason for it,* like a circuit breaker or a rollback classifier.*///如果是有状态重试,且有GLOBAL_STATE属性,则立即跳出重试终止;当抛出的异常是非需要执行回滚操作的异常时,才会执行到此处,CircuitBreakerRetryPolicy会在此跳出循环;if (state != null && context.hasAttribute(GLOBAL_STATE)) {break;}}if (state == null && this.logger.isDebugEnabled()) {this.logger.debug("Retry failed last attempt: count=" + context.getRetryCount());}exhausted = true;//重试失败后,如果有RecoveryCallback,则执行此回调,否则抛出异常return handleRetryExhausted(recoveryCallback, context, state);}catch (Throwable e) {throw RetryTemplate.<E>wrapIfNecessary(e);}finally {//清理环境close(retryPolicy, context, state, lastException == null || exhausted);//执行RetryListener#close,比如统计重试信息doCloseInterceptors(retryCallback, context, lastException);//清空绑定RetrySynchronizationManager.clear();}}

8、熔断源码

熔断器策略配置代码,CircuitBreakerRetryPolic

  • delegate:是真正判断是否重试的策略,当重试失败时,则执行熔断策略;
  • openTimeout:openWindow,配置熔断器电路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路);
    resetTimeout:timeout,配置重置熔断器重新闭合的超时时间。
public boolean isOpen() {long time = System.currentTimeMillis() - this.start;boolean retryable = this.policy.canRetry(this.context);if (!retryable) {//重试失败//在重置熔断器超时后,熔断器器电路闭合,重置上下文if (time > this.timeout) {logger.trace("Closing");this.context = createDelegateContext(policy, getParent());this.start = System.currentTimeMillis();retryable = this.policy.canRetry(this.context);}//当在熔断器打开状态时,熔断器电路打开,立即熔断else if (time < this.openWindow) {if ((Boolean) getAttribute(CIRCUIT_OPEN) == false) {logger.trace("Opening circuit");setAttribute(CIRCUIT_OPEN, true);}this.start = System.currentTimeMillis();return true;}}else {//重试成功//在熔断器电路半打开状态时,断路器电路闭合,重置上下文if (time > this.openWindow) {logger.trace("Resetting context");this.start = System.currentTimeMillis();this.context = createDelegateContext(policy, getParent());}}if (logger.isTraceEnabled()) {logger.trace("Open: " + !retryable);}setAttribute(CIRCUIT_OPEN, !retryable);return !retryable;
}

从如上代码可看出spring-retry的熔断策略相对简单:

  • 当重试失败,且在熔断器打开时间窗口[0,openWindow) 内,立即熔断;
  • 当重试失败,且在指定超时时间后(>timeout),熔断器电路重新闭合;
  • 在熔断器半打开状态[openWindow, timeout] 时,只要重试成功则重置上下文,断路器闭合。

CircuitBreakerRetryPolicy的delegate应该配置基于次数的SimpleRetryPolicy或者基于超时的TimeoutRetryPolicy策略,且策略都是全局模式,而非局部模式,所以要注意次数或超时的配置合理性。

9、注意

1、使用了@Retryable的方法里面不能使用try…catch包裹,要在方法上抛出异常,不然不会触发。

2、在重试期间这个方法是同步的,如果使用类似Spring Cloud这种框架的熔断机制时,可以结合重试机制来重试后返回结果。

3、@Recover 用于@Retryable重试失败后处理方法,此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理

4、spring-retry通过AOP实现对目的方法的封装,执行在当前线程下,所以重试过程中当前线程会堵塞。如果BackOff时间设置比较长,最好起异步线程重试(也可以加@Async注解

5、如果使用注解用在幂等性接口上面一定需要设置stateful = true失败就不进行重试,事务回滚

10、结束

整理不易,记得点个赞,你的支持是我最大的动力

11、参考

链接: https://www.jianshu.com/p/58e753ca0151

spring-retry使用以及源码相关推荐

  1. spring boot 2.0 源码分析(二)

    在上一章学习了spring boot 2.0启动的大概流程以后,今天我们来深挖一下SpringApplication实例变量的run函数. 先把这段run函数的代码贴出来: /*** Run the ...

  2. spring 注解试事物源码解析

    spring 注解试事物源码解析 基于xml注解式事务入口 public class TxNamespaceHandler extends NamespaceHandlerSupport {stati ...

  3. Spring事件机制Event源码解析(未完待续)

    Spring事件机制Event源码解析(未完待续) 监听器: ApplicationEvent事件 ApplicationListener监听器(观察者) ApplicationEventMultic ...

  4. spring boot 2.0 源码分析(三)

    通过上一章的源码分析,我们知道了spring boot里面的listeners到底是什么(META-INF/spring.factories定义的资源的实例),以及它是创建和启动的,今天我们继续深入分 ...

  5. spring session spring:session:sessions:expires 源码跟踪

    2019独角兽企业重金招聘Python工程师标准>>> spring session spring:session:sessions:expires 源码跟踪 博客分类: sprin ...

  6. Spring AOP 超详细源码解析

    知识章节 基础知识 什么是 AOP AOP 的全称是 "Aspect Oriented Programming",即面向切面编程 在 AOP 的思想里面,周边功能(比如性能统计,日 ...

  7. java中jooq,在Spring中使用jOOQ源码案例

    Spring专题 在Spring中使用jOOQ源码案例 虽然ORM大部分性能问题是由开发者自己引起的,以只读方式使用ORM是不值得的,现在有一种其他可选方式,使用JOOQ,jOOQ从您的数据库生成Ja ...

  8. Spring之循环依赖源码解析

    目录 1.什么是循环依赖? 2.为什么会出现循环依赖? 3.面对循环依赖问题,我们该如何思考解决? 4.Spring是怎么解决循环依赖的? 5.总结 1.什么是循环依赖? 有两个类Order.Cust ...

  9. Spring IoC容器初始化源码(1)—容器初始化入口以及setConfigLocations设置容器配置信息【一万字】

      基于最新Spring 5.x,对于基于XML的Spring IoC容器初始化过程中的setConfigLocations设置容器配置信息方法的源码进行了详细分析,最后给出了比较详细的方法调用时序图 ...

  10. Spring Boot 2.0系列文章(四):Spring Boot 2.0 源码阅读环境搭建

    前提 前几天面试的时候,被问过 Spring Boot 的自动配置源码怎么实现的,没看过源码的我只能投降��了. 这不,赶紧来补补了,所以才有了这篇文章的出现,Spring Boot 2. 0 源码阅 ...

最新文章

  1. 一个妹子的美团面试经历,历经 4 轮 2 小时,成功拿到 Offer
  2. Jmeter工具的使用——功能测试
  3. STM32-I2C总线驱动程序分析
  4. iframe 自动登录_安阳联通利用python实现三集中未反馈工单自动提取
  5. 数字字符串转化为字母组合的种数
  6. CNCF 沙箱项目 OCM Placement 多集群调度指南
  7. 用etree解析xml_用python3教你任意Html主内容提取
  8. 如何估算代码量_没有量杯,没有称,如何估算碳水化合物?
  9. python mad函数_Python函数的基本定义
  10. django 部署_狂野的Django:部署生存的技巧
  11. 去哪儿-01-EnvironmentalPre
  12. 阿里云服务器报 Liunx异常文件下载处理办法
  13. springboot,hibernate,exception no session 问题解决
  14. setup_per_cpu_areas 函数
  15. iOS gif加载视图
  16. python背景怎么自定义铃声_Python 上课铃声的定时播放(具有较强的自我管理意识.jpg)...
  17. 河北光电机顶盒安装当贝桌面
  18. 《构建高可用Linux服务器 第3版》—— 1.5 Linux服务器的优化
  19. 英伟达 jetson xavier agx 开发(1)开发环境搭建
  20. 高防IP有什么优势?

热门文章

  1. mysql字段替换_mysql 替换字段部分内容及mysql 替换函数replace()
  2. 航空产业已成境外情报机构重点网络攻击目标
  3. 获取车辆VIN等OBD信息
  4. 直积与张量积的数学定义与物理定义异同
  5. 退出登录清空session
  6. 如何删除ZIP压缩包的密码?
  7. 两万字,清华刘云浩教授回答新生关于AI的90个问题
  8. 思岚A1与A2性能及建图测试比较
  9. it系统应急响应流程图_[应急处理程序] 应急响应程序6个过程
  10. ubuntu合并终端_如何在Ubuntu中安装多个终端以及更改默认终端