自动补偿实现
- 要求: 方法调用的过程中。失败的时候,系统有办法进行自动重试,重试达到一定次数后,钉钉通知开发
- 实现设计: 注解,反射,定时任务
表的设计
– 补偿表
CREATE TABLE `a_compensation` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '数据库主键',
`busKey` VARCHAR(255) NOT NULL COMMENT '业务主键',
`type` VARCHAR(20) NOT NULL DEFAULT 'auto' COMMENT '处理方式:自动(auto),manual(人工介入)',
`dataStatus` TINYTEXT NOT NULL COMMENT '数据状态:0待处理,1:成功,2:失败',
`className` VARCHAR(200) NOT NULL COMMENT '完整类名',
`beanName` VARCHAR(100) NOT NULL COMMENT '类名',
`methodName` VARCHAR(100) NOT NULL COMMENT '方法名',
`reqArgsType` VARCHAR(500) NOT NULL COMMENT '方法入参类型',
`reqArgs` TEXT NOT NULL COMMENT '方法入参参数',
`reqArgsTypeReal` VARCHAR(500) DEFAULT NULL COMMENT '实际参数类型',
`retryCount` BIGINT(10) NOT NULL COMMENT '重试次数',
`resultMsg` VARCHAR(2000) DEFAULT NULL COMMENT '返回信息',
`gmtCreate` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
`gmtModified` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '更新时间',
`isValid` TINYINT(4) NOT NULL DEFAULT '1' COMMENT '是否有效',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
代码
- 注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Compensate {
/**
* 异常类型,用于补偿指定异常的数据,默认值Exception
*/
Class<?>[] exceptionType() default Exception.class;
}
- 切面类
@Aspect
@Component
public class AnnotationCompensateEntry {
private static final Logger logger = LoggerFactory.getLogger(AnnotationCompensateEntry.class);
@Autowired
private CompensationService compensationService;
/**
* 切点,通过注解的方式
*
* @param joinPoint 连接点
* @param e 异常
*/
@AfterThrowing(pointcut = "@annotation(com.base.annotation.Compensate)", throwing = "e")
public void aopCompensate(JoinPoint joinPoint, Exception e) {
try {
// 1:获取需要的数据,并且进行验证
Method targetMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
Compensate compensate = targetMethod.getAnnotation(Compensate.class);
Object[] args = joinPoint.getArgs();
if (args.length == 0) {
logger.info("无参方法无法补偿!");
return;
}
//异常类型
Class<?>[] exceptionTypes = compensate.exceptionType();
for (Class<?> exceptionType : exceptionTypes) {
// exceptionType 是 e.getClass() 的父类或者一样的。
if (exceptionType.isAssignableFrom(e.getClass())) {
saveCompensationDO(e, joinPoint);
}
}
} catch (Throwable throwable) {
// 异常需要抛出。要不然没办法,业务代码依赖这个报错的信息会被干扰。
logger.error(throwable.getMessage(), throwable);
Throwables.propagateIfPossible(throwable);
}
}
/**
* 2: 保存异常数据
* 保存的数据,应该先查询下,看数据库中是否存在这样的数据,如果不存在就进行插入。存在啥也不做。
*
* @param e 异常类
* @param joinPoint 切面类
*/
public void saveCompensationDO(Exception e, JoinPoint joinPoint) throws JsonProcessingException {
// 1:获取切面对象的参数
MethodInfo methodInfo = new MethodInfo(joinPoint);
methodInfo.init();
// 2:构建需要补偿的对象
CompensationEntity compensationDO = buildCompensationDO(methodInfo, e);
// 3: 上面根据的数据,得保证是满足数据的完整性。然后进行数据的储存入库。然后结束。
List<CompensationEntity> compensationEntities = compensationService.findByProperty(CompensationEntity.class, "busKey", compensationDO.getBusKey());
if (CollectionUtils.isEmpty(compensationEntities)) {
compensationService.save(compensationDO);
}
}
/**
* 2-1: 数据转换
*
* @param methodInfo 包装过后的数据对象
* @return 转换的补偿类
*/
private CompensationEntity buildCompensationDO(MethodInfo methodInfo, Exception e) {
// 1:存入通用的参数
CompensationEntity compensationDO = new CompensationEntity();
Annotation[] annotations= methodInfo.getTargetClass().getAnnotations();
String springBeanName="";
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(Service.class)){
springBeanName=methodInfo.getTargetClass().getAnnotation(Service.class).value();
}
if (annotation.annotationType().equals(Component.class)){
springBeanName=methodInfo.getTargetClass().getAnnotation(Component.class).value();
}
}
if (StringUtil.isEmpty(springBeanName)){
springBeanName=methodInfo.getTargetClass().getSimpleName();
}
compensationDO.setBeanName(springBeanName);
compensationDO.setClassName(methodInfo.getClassName());
compensationDO.setMethodName(methodInfo.getMethodName());
compensationDO.setDataStatus(StatusEnum.StatusEnum_INIT.getCode());
compensationDO.setRetryCount(0);
compensationDO.setResultMsg(e.getMessage());
compensationDO.setType(HandleMethodEnum.AUTO.getDes());
// 2:处理请求参数类型
// 一:消息无转换,说明入参是一样的。直接存。 1: 单个->类型 2:多个->数组 3. 无参不处理
Object[] args = methodInfo.getArgs();
ValidateUtils.isFalse(args.length == 0, "无参方法不允许使用补偿注解!");
if (args.length == 1) {
//获取方法参数类型数组
compensationDO.setReqArgs(methodInfo.getJsonArgs());
compensationDO.setReqArgsType(args[0].getClass().getName());
}
if (args.length > 1) {
Class<?>[] classesReq = methodInfo.getMethod().getParameterTypes();
StringBuilder stringBuffer = new StringBuilder();
for (Class<?> aClass : classesReq) {
stringBuffer.append(aClass.getName()).append(CommonConstants.Public.COMMA);
}
compensationDO.setReqArgs(methodInfo.getJsonArgs());
compensationDO.setReqArgsType(Object[].class.getName());
compensationDO.setReqArgsTypeReal(stringBuffer.substring(0, stringBuffer.length() - 1));
}
compensationDO.setBusKey(compensationDO.createBusKey());
return compensationDO;
}
}
- 辅助类
@Data
public class MethodInfo {
/*** 切入点信息*/
private JoinPoint joinPoint;
/*** 方法签名*/
private MethodSignature signature;
/*** 方法信息*/
private Method method;
/*** 类信息*/
private Class<?> targetClass;
/*** 参数信息*/
private Object[] args;
/*** 参数信息String*/
private String jsonArgs;
/*** 类名*/
private String className;
/*** 方法名*/
private String methodName;
public MethodInfo(JoinPoint joinPoint) {
this.joinPoint = joinPoint;
}
public void init() {
this.signature = (MethodSignature) joinPoint.getSignature();
this.method = signature.getMethod();
this.methodName = method.getName();
this.targetClass = method.getDeclaringClass();
this.className = targetClass.getName();
this.args = joinPoint.getArgs();
if (args.length == 1) {
this.jsonArgs = JsonUtil.toJsonString(args[0]);
} else {
this.jsonArgs = JsonUtil.toJsonString(args);
}
}
}
public class CompensationEntity {
/***数据库自增id*/
private Integer id;
/***业务主键 根据类名,方法名,入参生成一个摘要值*/
private String busKey;
/***人工处理,还是自动补偿*/
private String type;
/***数据状态 刚入库的时候,状态为:待重试:1,重试限定次数还失败给0,重试成功后给2*/
private String dataStatus;
/*** 完整类名*/
private String className;
/*** bean名*/
private String beanName;
/*** 方法名*/
private String methodName;
/*** 方法入参类型*/
private String reqArgsType;
/*** 方法入参参数*/
private String reqArgs;
/***实际参数类型*/
private String reqArgsTypeReal;
/*** 重试次数*/
private Integer retryCount;
/*** 错误描述*/
private String resultMsg;
/*** 创建时间*/
private Date gmtCreate = new Date();
/*** 修改时间*/
private Date gmtModified = new Date();
/*** 是否有效*/
private Boolean isValid = Boolean.TRUE;
@Id
@GeneratedValue(generator = "generator")
@GenericGenerator(name = "generator", strategy = "native")
@Column(name = "Id")
public Integer getId() {
return id;
}
/**
* 业务主键 根据类名,方法名,入参生成一个摘要值
*
* @return 业务主键
*/
public String createBusKey() {
return MD5Util.string2MD5(this.getClassName() + this.getMethodName() + this.getReqArgs());
}
}
/**
* Json工具
*/
public class JsonUtil {
/** 默认使用jackson */
public static ObjectMapper mapper;
static {
mapper = new ObjectMapper();
//设置Jackson序列化时只包含不为空的字段
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//设置在反序列化时忽略在JSON字符串中存在,而在Java中不存在的属性
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
public static String toPrettyJsonString(Object value) {
//打印原始格式不做格式化
return toJsonString(value);
/*String jsonString;
try {
jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new BizException("Json序列化失败");
}
return jsonString;*/
}
public static String toJsonString(Object value) {
String jsonString;
try {
jsonString = mapper.writeValueAsString(value);
}catch (JsonProcessingException e){
throw new BusinessException("Json序列化失败",false);
}
return jsonString;
}
public static <T> T fromJsonString(String content,Class<T> valueType) {
T t;
try {
t = mapper.readValue(content,valueType);
} catch (IOException e) {
throw new BusinessException("Json反序列化失败\n" + GatewayUtil.getStackTraceAsString(e),false);
}
return t;
}
}
- 补偿那块的逻辑实现
@Service
@Transactional
public class CompensationServiceImpl extends CommonServiceImpl implements CompensationService {
private static final Logger logger = LoggerFactory.getLogger(CompensationServiceImpl.class);
public final static Integer RETRY_LIMIT_COUNT = 3;
@Resource
CompensationMiniDao compensationMiniDao;
/**
* 1:定时任务调度的方式
*
* @return 处理说明
*/
@Override
public String autoRetry() {
// 1: 查询所有需要重试的记录,数据入库的时候重试次数为0
StringBuilder stringBuffer = new StringBuilder();
List<CompensationEntity> list = compensationMiniDao.getRetryList();
stringBuffer.append("待补偿数据:").append(list.size()).append("条!");
int successCount = 0;
int failCount = 0;
for (CompensationEntity compensationEntity : list) {
int retryCount = compensationEntity.getRetryCount() + 1;
compensationEntity.setRetryCount(retryCount);
try {
// 调用需要重试的方法
boolean callStatus = callTargetMethod(compensationEntity);
// 补偿成功
if (callStatus) {
compensationEntity.setResultMsg(CommonConstants.Public.SUCCESS);
compensationEntity.setDataStatus(StatusEnum.StatusEnum_SUCCESS.getCode());
successCount++;
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
compensationEntity.setResultMsg(e.getMessage());
if (RETRY_LIMIT_COUNT == retryCount) {
compensationEntity.setDataStatus(StatusEnum.StatusEnum_FAIL.getCode());
}
failCount++;
}
this.updateBySqlString(buildUpdateSql(compensationEntity));
}
stringBuffer.append("处理成功:").append(successCount).append("条!");
stringBuffer.append("处理失败:").append(failCount).append("条!");
return stringBuffer.toString();
}
/**
* 2:人工重试的方式
*
* @param id id
* @return 处理信息
*/
@Override
public String manualRetry(Integer id) {
// 1: 定损需要处理的对象和返回的参数
CompensationEntity dealObj = null;
String result = CommonConstants.Public.SUCCESS;
try {
// 2: 根据id查询数据库
List<CompensationEntity> dealObjList = this.findByProperty(CompensationEntity.class, "id", id);
ValidateUtils.notEmpty(dealObjList, String.format("根据id:%s,查询返回的对象为空", id));
dealObj = dealObjList.get(0);
int retryCount = dealObj.getRetryCount() + 1;
dealObj.setRetryCount(retryCount);
dealObj.setType(HandleMethodEnum.MANUAL.getMsg());
boolean handleResult = callTargetMethod(dealObj);
if (handleResult) {
dealObj.setDataStatus(StatusEnum.StatusEnum_SUCCESS.getCode());
}
} catch (Exception e) {
logger.error(e.getCause().getMessage());
result = e.getCause().getMessage();
}
// 3: 操作保存数据,如果依然是失败只改重试的次数,不修改数据的状态
if (Objects.nonNull(dealObj)) {
dealObj.setResultMsg(result);
this.updateBySqlString(buildUpdateSql(dealObj));
}
return result;
}
/**
* 构建修改的sql预计
*
* @param compensationEntity 实体类对象
* @return 构建的update sql
*/
private String buildUpdateSql(CompensationEntity compensationEntity) {
StringBuilder stringBufferUpdate = new StringBuilder("update a_compensation set dataStatus='");
stringBufferUpdate.append(compensationEntity.getDataStatus()).append("'").append(CommonConstants.Public.COMMA);
stringBufferUpdate.append(" retryCount=").append(compensationEntity.getRetryCount()).append(CommonConstants.Public.COMMA);
stringBufferUpdate.append(" type=").append("\"").append(compensationEntity.getType()).append("\"");
if (StringUtil.isNotEmpty(compensationEntity.getResultMsg())) {
stringBufferUpdate.append(CommonConstants.Public.COMMA);
stringBufferUpdate.append("resultMsg=").append("\"").append(compensationEntity.getResultMsg()).append("\"");
}
stringBufferUpdate.append(" where id=").append(compensationEntity.getId());
return stringBufferUpdate.toString();
}
/**
* 分析数据
*
* @return 分析数据的结果
*/
@Override
public String analysisData() {
List<CompensationEntity> allCompensationEntityData = this.getList(CompensationEntity.class);
StringBuilder dataStr = new StringBuilder(String.format("补偿表数据总量为:%s条!", allCompensationEntityData.size()));
if (CollectionUtils.isNotEmpty(allCompensationEntityData)) {
Map<String, List<CompensationEntity>> resultList = allCompensationEntityData.stream().collect(Collectors.groupingBy(CompensationEntity::getDataStatus));
resultList.forEach((k, v) -> {
StatusEnum statusEnum = StatusEnum.getStatusEnumByCode(k);
ValidateUtils.notNull(statusEnum, String.format("补偿表钉钉提醒的时候,查询的数据状态有问题,状态为:%s", k));
dataStr.append(statusEnum.getMsg()).append(":").append(v.size()).append("条!");
});
}
return dataStr.toString();
}
/**
* 3: 调用的核心代码
*
* @param compensationEntity 补偿对象。
*/
private boolean callTargetMethod(CompensationEntity compensationEntity) throws Exception {
// 1: 通过类找到对应的方法
Method targetMethod = null;
try {
Class<?> targetClass = Class.forName(compensationEntity.getClassName());
ValidateUtils.notNull(targetClass, "补偿时未获取到对应的class bean");
Method[] methods = targetClass.getMethods();
for (Method method : methods) {
// 匹配方法
if (matchMethod(method, compensationEntity)) {
targetMethod = method;
break;
}
}
} catch (ClassNotFoundException e) {
logger.error(e.getMessage(), e);
throw e;
}
try {
//补偿的目标方法中的参数类型
ValidateUtils.notNull(targetMethod, "补偿时未获取到对应的bean需要调用的目标方法");
// 2: 回去方法的参数类型
Class<?>[] paramTypes = targetMethod.getParameterTypes();
// 3: 从spring 取的对象
Object targetObject = SpringUtils.getBean(StringUtil.firstLowerCase(compensationEntity.getBeanName()));
if (paramTypes.length == 1) {
// 转成实际类型调用目标方法
Object obj = JsonUtil.mapper.readValue(compensationEntity.getReqArgs(), paramTypes[0]);
targetMethod.invoke(targetObject, obj);
} else if (paramTypes.length > 1) {
List<Object> objectList = JsonUtil.mapper.readValue(compensationEntity.getReqArgs(), new TypeReference<List<Object>>() {
});
Type[] paramTypesReq = targetMethod.getGenericParameterTypes();
Object[] realParam = new Object[objectList.size()];
for (int i = 0; i < objectList.size(); i++) {
Type paramType = paramTypesReq[i];
String param = JsonUtil.mapper.writeValueAsString(objectList.get(i));
realParam[i] = JsonUtil.mapper.readValue(param, JsonUtil.mapper.getTypeFactory().constructType(paramType));
}
targetMethod.invoke(targetObject, realParam);
} else {
logger.error("不支持的参数类型调用");
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw e;
}
return true;
}
/**
* 3-1: 寻找类中对应的方法。考虑重载
*
* @param method 方法method
* @param compensationEntity 补偿的类
* @return 是否匹配上
*/
private Boolean matchMethod(Method method, CompensationEntity compensationEntity) {
// 1:先判断方法名,如果类名不匹配,直接返回false
if (!method.getName().equals(compensationEntity.getMethodName())) {
return false;
}
// 2: 方法名匹配成功后,匹配参数
Class<?>[] args = method.getParameterTypes();
if (args.length == 1) {
return args[0].getName().equals(compensationEntity.getReqArgsType()) && StringUtils.isBlank(compensationEntity.getReqArgsTypeReal());
} else {
Class<?>[] classesReq = method.getParameterTypes();
StringBuilder stringBuffer = new StringBuilder();
for (Class<?> aClass : classesReq) {
stringBuffer.append(aClass.getName()).append(CommonConstants.Public.COMMA);
}
String multiParamStr = stringBuffer.substring(0, stringBuffer.length() - 1);
return compensationEntity.getReqArgsTypeReal().equals(multiParamStr);
}
}
}
- 定时任务类
@Component
public class CompensateDataSchedulerTask {
private static final Logger logger = LoggerFactory.getLogger(CompensateDataSchedulerTask.class);
@Resource
CompensationService compensationService;
@Resource
MessageReminderHandler messageReminderHandler;
@Autowired
private UtilDictKVService utilDictKVService;
public final static String INTERFACE_NAME = "通用补偿接口";
/**
* 1: 任务只重试status=0的
* 每隔5分钟一次,每次每条记录重试1次
* tip:
* 因为考虑到 定时任务是5分钟跑一次,所以数据量不会很大,因此,
* 我们不用异步和线程池处理了。就同步处理。数据也同步更新到数据库。
* 后期有改造,再进行优化
* <p>
* 考虑到线上有多台机器。我们默认指定一台机器进行运行。
*/
@Scheduled(initialDelay = 10000, fixedDelay = 300000)
public void retry() {
if (exeScheduledIP()) {
logger.info("开始进行补偿!");
String dealResult = compensationService.autoRetry();
logger.info(dealResult);
}
}
/**
* 钉钉提醒:失败待补偿的钉钉提醒。
* 每天上午10:15触发
*/
@Scheduled(cron = "0 15 10 ? * *")
public void dingDingCompensationRemind() {
if (exeScheduledIP()) {
// 获取需要发送的 内容
String env = messageReminderHandler.getEnv();
String remindStr = compensationService.analysisData();
DingDingMsgVO dingDingMsgVO = messageReminderHandler.buildDingDingMsgVO(INTERFACE_NAME, "环境:" + env + "!" + remindStr, DictCodeKeyEnum.DING_DING_COMPENSATE_MOBILE);
String sendResult = messageReminderHandler.dingDingSend(DictCodeKeyEnum.DING_DING_COMPENSATE_URL, dingDingMsgVO);
logger.info("CompensateDataSchedulerTask->dingDingRemind->sendResult:" + sendResult);
}
}
/**
* 是否可以执行定时任务
*
* @return true/false
*/
private Boolean exeScheduledIP() {
UtilDictKVEntity utilDictKVEntity = utilDictKVService.getKvsByDictCodeKey(DictCodeKeyEnum.CURRENT_COMPENSATE_IP.getCode(), DictCodeKeyEnum.CURRENT_COMPENSATE_IP.getKey(), 3600L);
ValidateUtils.notNull(utilDictKVEntity, "未配置执行定时任务的");
String localIp = null;
try {
InetAddress ia = InetAddress.getLocalHost();
localIp = ia.getHostAddress();
} catch (UnknownHostException e) {
logger.error(e.getMessage(), e);
return false;
}
return localIp.equals(utilDictKVEntity.getDictValue());
}
}