Spring Retry 是Spring框架中的一个组件,
它提供了自动重新调用失败操作的能力。这在错误可能是暂时发生的(如网络故障)的情况下很有帮助。
在本文中,我们将看到使用Spring Retry的各种方式:注解、RetryTemplate以及回调。
让我们首先将spring-retry
依赖项添加到我们的pom.xml
文件中:
org.springframework.retry
spring-retry
2.0.4
我们还需要将Spring AOP添加到我们的项目中:
org.springframework
spring-aspects
5.2.8.RELEASE
可以查看Maven来获取最新版本的spring-retry和spring-aspects 依赖。
要在应用程序中启用Spring Retry,我们需要将@EnableRetry
注释添加到我们的@Configuration
类:
@Configuration
@EnableRetry
public class AppConfig { ... }
@Retryable
而不用恢复我们可以使用@Retryable
注解为方法添加重试功能:
@Service
public interface MyService {
@Retryable(value = RuntimeException.class)
void retryService(String sql);
}
在这里,当抛出RuntimeException时尝试重试。
根据@Retryable的默认行为,重试最多可能发生3次,重试之间有1秒的延迟。
@Retryable
和@Recover
现在让我们使用@Recover
注解添加一个恢复方法:
@Service
public interface MyService {
@Retryable(value = SQLException.class)
void retryServiceWithRecovery(String sql) throws SQLException;
@Recover
void recover(SQLException e, String sql);
}
这里,当抛出SQLException
时重试会尝试运行。 当@Retryable
方法因指定异常而失败时,@Recover
注解定义了一个单独的恢复方法。
因此,如果retryServiceWithRecovery
方法在三次尝试之后还是抛出了SQLException
,那么recover()
方法将被调用。
恢复处理程序的第一个参数应该是Throwable
类型(可选)和相同的返回类型。其余的参数按相同顺序从失败方法的参数列表中填充。
@Retryable
的行为为了自定义重试的行为,我们可以使用参数maxAttempts
和backoff
:
@Service
public interface MyService {
@Retryable( value = SQLException.class,
maxAttempts = 2, backoff = @Backoff(delay = 300))
void retryServiceWithCustomization(String sql) throws SQLException;
}
这样最多将有两次尝试和100毫秒的延迟。
我们还可以在@Retryable
注解中使用properties。
为了演示这一点,我们将看到如何将delay
和maxAttempts
的值外部化到一个properties文件中。
首先,让我们在名为retryConfig.properties
的文件中定义属性:
retry.maxAttempts=2
retry.maxDelay=100
然后我们指示@Configuration
类加载这个文件:
@PropertySource("classpath:retryConfig.properties")
public class AppConfig { ... }
// ...
最后,我们可以在@Retryable
的定义中注入retry.maxAttempts
和retry.maxDelay
的值:
@Service
public interface MyService {
@Retryable( value = SQLException.class, maxAttemptsExpression = "${retry.maxAttempts}",
backoff = @Backoff(delayExpression = "${retry.maxDelay}"))
void retryServiceWithExternalizedConfiguration(String sql) throws SQLException;
}
请注意,我们现在使用的是maxAttemptsExpression
和delayExpression
而不是maxAttempts
和delay
。
@Retryable
注解可以在接口上的方法上也能单独在实体的方法上。
以下是测试方法:(可以看到在查询后面加了一个by zero
的异常)
@Retryable(value = RuntimeException.class)
public void clearVeryLongDrafts() {
// 清除超过7天以上的附件
log.info("start clear attachments older than 7 days");
DateTime dateTime = DateUtil.offsetDay(new Date(), -7);
String format = DateUtil.format(dateTime, DatePattern.NORM_DATE_PATTERN);
List drafeBoxList = new LambdaQueryChainWrapper<>(drafeBoxMapper)
.le(DrafeBox::getSubmitTime, format)
.list();
int i = 1/0;
}
2023-11-20 15:21:50.311 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate : doExecute:324: Retry: count=0
2023-11-20 15:21:50.317 INFO [srv_crm_base,,,] 892 --- [1-1700464910262] c.y.c.b.j.DraftBoxJob : clearVeryLongDrafts:48: start clear attachments older than 7 days
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58b6c600] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@40904969] will not be managed by Spring
==> Preparing: SELECT id,company_code,business_type,business_name,json_drafe,submit_time,create_by,create_time,update_by,update_time,is_delete FROM t_drafe_box WHERE is_delete=0 AND (submit_time <= ?)
==> Parameters: 2023-11-13(String)
<== Columns: id, company_code, business_type, business_name, json_drafe, submit_time, create_by, create_time, update_by, update_time, is_delete
<== Row: 1722184859663953922, 010000, TRANSFER_APPLICATION, 客户转移申请, <>, 2023-11-08 17:30:42, BWhuhang, 2023-11-08 17:30:42, BWhuhang, 2023-11-10 09:51:29, 0
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58b6c600]
2023-11-20 15:21:51.398 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate : doExecute:360: Checking for rethrow: count=1
2023-11-20 15:21:51.399 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate : doExecute:324: Retry: count=1
2023-11-20 15:21:51.399 INFO [srv_crm_base,,,] 892 --- [1-1700464910262] c.y.c.b.j.DraftBoxJob : clearVeryLongDrafts:48: start clear attachments older than 7 days
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@186a72c6] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@40904969] will not be managed by Spring
==> Preparing: SELECT id,company_code,business_type,business_name,json_drafe,submit_time,create_by,create_time,update_by,update_time,is_delete FROM t_drafe_box WHERE is_delete=0 AND (submit_time <= ?)
==> Parameters: 2023-11-13(String)
<== Columns: id, company_code, business_type, business_name, json_drafe, submit_time, create_by, create_time, update_by, update_time, is_delete
<== Row: 1722184859663953922, 010000, TRANSFER_APPLICATION, 客户转移申请, <>, 2023-11-08 17:30:42, BWhuhang, 2023-11-08 17:30:42, BWhuhang, 2023-11-10 09:51:29, 0
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@186a72c6]
2023-11-20 15:21:52.413 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate : doExecute:360: Checking for rethrow: count=2
2023-11-20 15:21:52.413 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate : doExecute:324: Retry: count=2
2023-11-20 15:21:52.414 INFO [srv_crm_base,,,] 892 --- [1-1700464910262] c.y.c.b.j.DraftBoxJob : clearVeryLongDrafts:48: start clear attachments older than 7 days
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1e349061] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@40904969] will not be managed by Spring
==> Preparing: SELECT id,company_code,business_type,business_name,json_drafe,submit_time,create_by,create_time,update_by,update_time,is_delete FROM t_drafe_box WHERE is_delete=0 AND (submit_time <= ?)
==> Parameters: 2023-11-13(String)
<== Columns: id, company_code, business_type, business_name, json_drafe, submit_time, create_by, create_time, update_by, update_time, is_delete
<== Row: 1722184859663953922, 010000, TRANSFER_APPLICATION, 客户转移申请, <>, 2023-11-08 17:30:42, BWhuhang, 2023-11-08 17:30:42, BWhuhang, 2023-11-10 09:51:29, 0
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1e349061]
2023-11-20 15:21:52.424 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate : doExecute:360: Checking for rethrow: count=3
2023-11-20 15:21:52.424 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate : doExecute:383: Retry failed last attempt: count=3
从上面的全部日志可以看到,再每一次查询之后就出现了以下日志,这就表示重试了count
标识重试次数。
o.s.r.s.RetryTemplate : doExecute:360: Checking for rethrow: count=1
o.s.r.s.RetryTemplate : doExecute:324: Retry: count=1
当然我们还可以自定义很多参数,这些就不一一测试了。
RetryTemplate
RetryOperations
Spring Retry提供了RetryOperations
接口,它提供了一组execute()
方法:
public interface RetryOperations {
T execute(RetryCallback retryCallback) throws Exception;
...
}
execute()
方法的参数RetryCallback
,是一个接口,可以插入需要在失败时重试的业务逻辑:
public interface RetryCallback {
T doWithRetry(RetryContext context) throws Throwable;
}
RetryTemplate
配置RetryTemplate
是RetryOperations
的一个实现。
让我们在@Configuration
类中配置一个RetryTemplate
的bean:
@Configuration
public class AppConfig {
//...
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(2000l);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(2);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
这个RetryPolicy
确定了何时应该重试操作。
其中SimpleRetryPolicy
定义了重试的固定次数,另一方面,BackOffPolicy
用于控制重试尝试之间的回退。
最后,FixedBackOffPolicy
会使重试在继续之前暂停一段固定的时间。
RetryTemplate
要使用重试处理来运行代码,我们可以调用retryTemplate.execute()
方法:
retryTemplate.execute(new RetryCallback() {
@Override
public Void doWithRetry(RetryContext arg0) {
myService.templateRetryService();
...
}
});
我们可以使用lambda表达式代替匿名类:
retryTemplate.execute(arg0 -> {
myService.templateRetryService();
return null;
});
监听器在重试时提供另外的回调。我们可以用这些来关注跨不同重试的各个横切点。
回调在RetryListener
接口中提供:
public class DefaultListenerSupport extends RetryListenerSupport {
@Override
public void close(RetryContext context,
RetryCallback callback, Throwable throwable) {
logger.info("onClose");
...
super.close(context, callback, throwable);
}
@Override
public void onError(RetryContext context,
RetryCallback callback, Throwable throwable) {
logger.info("onError");
...
super.onError(context, callback, throwable);
}
@Override
public boolean open(RetryContext context,
RetryCallback callback) {
logger.info("onOpen");
...
return super.open(context, callback);
}
}
open
和close
的回调在整个重试之前和之后执行,而onError
应用于单个RetryCallback
调用。
接下来,我们将我们的监听器(DefaultListenerSupport)
注册到我们的RetryTemplate
bean:
@Configuration
public class AppConfig {
...
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
...
retryTemplate.registerListener(new DefaultListenerSupport());
return retryTemplate;
}
}
为了完成我们的示例,让我们验证一下结果:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
classes = AppConfig.class,
loader = AnnotationConfigContextLoader.class)
public class SpringRetryIntegrationTest {
@Autowired
private MyService myService;
@Autowired
private RetryTemplate retryTemplate;
@Test(expected = RuntimeException.class)
public void givenTemplateRetryService_whenCallWithException_thenRetry() {
retryTemplate.execute(arg0 -> {
myService.templateRetryService();
return null;
});
}
}
从测试日志中可以看出,我们已经正确配置了RetryTemplate
和RetryListener
:
2020-01-09 20:04:10 [main] INFO c.p.s.DefaultListenerSupport - onOpen
2020-01-09 20:04:10 [main] INFO c.pin.springretry.MyServiceImpl - throw RuntimeException in method templateRetryService()
2020-01-09 20:04:10 [main] INFO c.p.s.DefaultListenerSupport - onError
2020-01-09 20:04:12 [main] INFO c.pin.springretry.MyServiceImpl - throw RuntimeException in method templateRetryService()
2020-01-09 20:04:12 [main] INFO c.p.s.DefaultListenerSupport - onError
2020-01-09 20:04:12 [main] INFO c.p.s.DefaultListenerSupport - onClose
在本文中,我们看到了如何使用注解、RetryTemplate
和回调监听器来使用Spring Retry。了解了如何使用 Spring Retry 来减少样板代码并使代码更具可读性和可维护性。通过 Spring Retry,相信你也能够消除冗余代码。