Springboot自定義重試注解@Retryable
一、概述
微服務(wù)之間相互調(diào)用,難免會出現(xiàn)形形色色的異常,出現(xiàn)異常時(shí)有些情況可能需要先落重試任務(wù)表,然后通過任務(wù)調(diào)度等進(jìn)行定時(shí)重試;通過自定義重試注解@Retryable,減少對核心業(yè)務(wù)代碼入侵,增強(qiáng)代碼可讀性、可維護(hù)性。下面通過實(shí)戰(zhàn),開發(fā)自定義重試注解@Retryable。諸位可根據(jù)業(yè)務(wù)需要,稍作改造直接使用;如果有疑問、或者好的想法,歡迎留言,經(jīng)驗(yàn)共享。
二、實(shí)戰(zhàn)
重試任務(wù)表定義(retry_task):
CREATE TABLE `retry_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵值',
`business_type_code` varchar(32) COLLATE NOT NULL DEFAULT '' COMMENT '業(yè)務(wù)類型編碼',
`business_type_desc` varchar(100) COLLATE NOT NULL DEFAULT '' COMMENT '業(yè)務(wù)類型描述',
`retry_service_name` varchar(100) COLLATE NOT NULL DEFAULT '' COMMENT '重試的service名稱',
`business_param` text COLLATE NOT NULL DEFAULT '' COMMENT '業(yè)務(wù)參數(shù)',
`wait_retry_times` int(11) NOT NULL DEFAULT 3 COMMENT '待重試次數(shù)',
`already_retry_times` int(11) NOT NULL DEFAULT 0 COMMENT '已重試次數(shù)',
`retry_result_code` varchar(36) COLLATE NOT NULL DEFAULT '' COMMENT '重試結(jié)果碼',
`retry_result_msg` varchar(255) COLLATE NOT NULL DEFAULT '' COMMENT '重試結(jié)果描述',
`create_user` varchar(36) COLLATE NOT NULL DEFAULT '' COMMENT '創(chuàng)建人',
`create_time` datetime NOT NULL COMMENT '創(chuàng)建時(shí)間',
`update_user` varchar(36) COLLATE NOT NULL DEFAULT '' COMMENT '更新人',
`update_time` datetime NOT NULL COMMENT '更新時(shí)間',
PRIMARY KEY (`id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_business_type_code` (`business_type_code`)
) COMMENT='重試任務(wù)表';
重試任務(wù)表實(shí)體類(RetryTaskEntity):
@Data
public class RetryTaskEntity implements Serializable {
private static final long serialVersionUID = -1950778520234119369L;
/**
* 主鍵值
*/
private BigInteger id;
/**
* 業(yè)務(wù)類型編碼
*/
private String businessTypeCode;
/**
* 業(yè)務(wù)類型描述
*/
private String businessTypeDesc;
/**
* 重試的service名稱
*/
private String retryServiceName;
/**
* 業(yè)務(wù)參數(shù)
*/
private String businessParam;
/**
* 待重試的次數(shù)
*/
private Integer waitRetryTimes;
/**
* 已重試的次數(shù)
*/
private Integer alreadyRetryTimes;
/**
* 重試結(jié)果碼
*/
private String retryResultCode;
/**
* 重試結(jié)果描述
*/
private String retryResultMsg;
/**
* 創(chuàng)建人
*/
private String createUser;
/**
* 創(chuàng)建時(shí)間
*/
private Date createTime;
/**
* 更新人
*/
private String updateUser;
/**
* 更新時(shí)間
*/
private Date updateTime;
}
重試任務(wù)表mapper和對應(yīng)的xml文件:
public interface RetryTaskMapper {
int addRetryTask(RetryTaskEntity retryTaskEntity);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.boot.demo.mapper.RetryTaskMapper">
<insert id="addRetryTask" parameterType="com.boot.demo.pojo.RetryTaskEntity">
INSERT INTO retry_task(business_type_code,
business_type_desc,
retry_service_name,
business_param,
wait_retry_times,
already_retry_times,
retry_result_code,
retry_result_msg,
create_user,
create_time,
update_user,
update_time)
VALUES (#{businessTypeCode},
#{businessTypeDesc},
#{retryServiceName},
#{businessParam},
#{waitRetryTimes},
#{alreadyRetryTimes},
#{retryResultCode},
#{retryResultMsg},
#{createUser},
#{createTime},
#{updateUser},
#{updateTime})
</insert>
</mapper>
重試任務(wù)表service和對應(yīng)的serviceImpl:
public interface RetryTaskService {
void addRetryTask(RetryTaskEntity retryTaskEntity);
}
@Service
public class RetryTaskServiceImpl implements RetryTaskService {
@Autowired
private RetryTaskMapper retryTaskMapper;
@Override
public void addRetryTask(RetryTaskEntity retryTaskEntity) {
retryTaskMapper.addRetryTask(retryTaskEntity);
}
}
業(yè)務(wù)類型枚舉類(RetryTaskDefinitionEnum):
/**
* 重試任務(wù)枚舉
*/
public enum RetryTaskDefinitionEnum {
ADD_STOCK("101", "采購入庫成功后新增庫存異常重試", "purchaseService", 3);
/**
* 業(yè)務(wù)類型編碼
*/
private final String businessTypeCode;
/**
* 業(yè)務(wù)類型描述
*/
private final String businessTypeDesc;
/**
* 重試的service名稱
*/
private final String retryServiceName;
/**
* 重試次數(shù)
*/
private final Integer retryTimes;
RetryTaskDefinitionEnum(String businessTypeCode, String businessTypeDesc, String retryServiceName, Integer retryTimes) {
this.businessTypeCode = businessTypeCode;
this.businessTypeDesc = businessTypeDesc;
this.retryServiceName = retryServiceName;
this.retryTimes = retryTimes;
}
public static RetryTaskDefinitionEnum getTaskDefinitionByBusinessTypeCode(String businessTypeCode) {
if (StringUtils.isBlank(businessTypeCode)) {
return null;
}
for (RetryTaskDefinitionEnum taskDefinition : values()) {
if (taskDefinition.getBusinessTypeCode().equals(businessTypeCode)) {
return taskDefinition;
}
}
return null;
}
public String getBusinessTypeCode() {
return businessTypeCode;
}
public String getBusinessTypeDesc() {
return businessTypeDesc;
}
public String getRetryServiceName() {
return retryServiceName;
}
public Integer getRetryTimes() {
return retryTimes;
}
}
自定義注解(@MyRetryable):
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface MyRetryable {
RetryTaskDefinitionEnum businessType();
}
自定義注解切面(MyRetryableAspect):
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import com.boot.demo.result.Result;
import com.boot.demo.result.ResultCode;
import com.boot.demo.pojo.RetryTaskEntity;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import com.boot.demo.annotation.MyRetryable;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.ProceedingJoinPoint;
import com.boot.demo.service.RetryTaskService;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.boot.demo.annotation.RetryTaskDefinitionEnum;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
@Slf4j
@Aspect
@Component
public class MyRetryableAspect {
@Autowired
private RetryTaskService retryTaskService;
@Pointcut("@annotation(com.boot.demo.annotation.MyRetryable)")
public void pointCut() {
}
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
Result result = null;
try {
// 執(zhí)行目標(biāo)方法
result = (Result) joinPoint.proceed();
// 目標(biāo)方法返回:成功結(jié)果碼(200),則無需重試
if (ResultCode.SUCCESS.getCode() == result.getCode()) {
return result;
}
// 目標(biāo)方法返回:非成功結(jié)果碼(非200)則需重試(此次可根據(jù)需要判斷什么樣的返回碼需要重試)
dealAddRetryTask(joinPoint);,
return result;
} catch (Throwable e) {
log.error("myRetryableAspectLog error param: {} result: {} e: ", joinPoint.getArgs(), result, e);
// 此處捕獲異常之后,也可以根據(jù)需要重試,這里就僅輸出異常日志
return result;
}
}
private void dealAddRetryTask(ProceedingJoinPoint joinPoint) {
// 獲取重試注解信息
MyRetryable myRetryableAnnotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.class);
if (null == myRetryableAnnotation) {
return;
}
// 根據(jù)業(yè)務(wù)類型編碼,獲取枚舉中定義的業(yè)務(wù)類型描述、重試的service、重試次數(shù)等信息
String businessTypeCode = myRetryableAnnotation.businessType().getBusinessTypeCode();
RetryTaskDefinitionEnum retryTaskDefinition = RetryTaskDefinitionEnum.getTaskDefinitionByBusinessTypeCode(businessTypeCode);
if (null == retryTaskDefinition) {
return;
}
RetryTaskEntity retryTaskEntity = new RetryTaskEntity();
retryTaskEntity.setBusinessTypeCode(businessTypeCode);
retryTaskEntity.setBusinessTypeDesc(retryTaskDefinition.getBusinessTypeDesc());
retryTaskEntity.setRetryServiceName(retryTaskDefinition.getRetryServiceName());
retryTaskEntity.setBusinessParam(JSON.toJSONString(joinPoint.getArgs()[0]));
retryTaskEntity.setWaitRetryTimes(retryTaskDefinition.getRetryTimes());
retryTaskEntity.setAlreadyRetryTimes(0);
retryTaskEntity.setRetryResultCode("");
retryTaskEntity.setRetryResultMsg("");
retryTaskEntity.setCreateUser("SYS");
retryTaskEntity.setCreateTime(new Date());
retryTaskEntity.setUpdateUser("SYS");
retryTaskEntity.setUpdateTime(new Date());
retryTaskService.addRetryTask(retryTaskEntity);
}
}
基礎(chǔ)類(Result、ResultCode、ResultGenerator)。
Result類:
public class Result {
private int code;
private String message;
private Object data;
public Result setCode(ResultCode resultCode) {
this.code = resultCode.getCode();
return this;
}
public int getCode() {
return code;
}
public Result setCode(int code) {
this.code = code;
return this;
}
public String getMessage() {
return message;
}
public Result setMessage(String message) {
this.message = message;
return this;
}
public Object getData() {
return data;
}
public Result setData(Object data) {
this.data = data;
return this;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Result{");
sb.append("code=").append(code);
sb.append(", message='").append(message).append('\'');
sb.append(", data=").append(data);
sb.append('}');
return sb.toString();
}
}
ResultCode類:
public enum ResultCode {
SUCCESS(200),
FAIL(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
NOT_FOUND(404),
INTERNAL_SERVER_ERROR(500);
private final int code;
ResultCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
ResultGenerator類:
public class ResultGenerator {
private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS";
private ResultGenerator() {
}
public static Result genSuccessResult() {
return new Result().setCode(ResultCode.SUCCESS).setMessage(DEFAULT_SUCCESS_MESSAGE);
}
public static Result genSuccessResult(Object data) {
return new Result().setCode(ResultCode.SUCCESS).setMessage(DEFAULT_SUCCESS_MESSAGE).setData(data);
}
public static Result genFailResult(String message) {
return new Result().setCode(ResultCode.FAIL).setMessage(message);
}
public static Result genFailResult(ResultCode code, String message) {
return new Result().setCode(code).setMessage(message);
}
public static Result genFailResult(String message, Object data) {
return new Result().setCode(ResultCode.FAIL).setMessage(message).setData(data);
}
}
測試controller(PurchaseController):
@RestController
@RequestMapping("/purchase")
public class PurchaseController {
@Autowired
private PurchaseService purchaseService;
@GetMapping("/test")
public String test(String param) {
purchaseService.addStock(param);
return "success";
}
}
測試PurchaseService、和PurchaseServiceImpl
public interface PurchaseService {
Result addStock(String param);
}
@Service("purchaseService")
public class PurchaseServiceImpl implements PurchaseService {
@Override
// 在需要重試的業(yè)務(wù)方法上新增重試注解即可
@MyRetryable(businessType = RetryTaskDefinitionEnum.ADD_STOCK)
public Result addStock(String param) {
// return ResultGenerator.genSuccessResult();
return ResultGenerator.genFailResult("系統(tǒng)異常...");
}
}
三、總結(jié)
新增重試任務(wù)成功之后,我們可通過調(diào)度平臺(比如:xxlJob),定時(shí)查詢重試任務(wù)表,然后調(diào)用RetryTaskDefinitionEnum中定義的重試的service(retryServiceName),這里可以定義一個模板方法,根據(jù)retryServiceName,從spring中獲取到對應(yīng)的bean,執(zhí)行具體的業(yè)務(wù)方法,然后更新任務(wù)狀態(tài)和重試次數(shù)即可。