接口補償機制需求分析&方案設計
文章目錄
接口補償機制需求分析&方案設計
需求分析
背景
解決方案
業務示例
注意事項
示例
業務Controller
實現
重試信息類&數據處理入庫
接口重試的主要方法
需求分析
背景
業務系統逐漸開始與多個第三方系統進行對接,在對接時,需要調用外部系統接口進行數據的交換,如果在接口請求的過程中發生了網絡抖動或其他問題,會導致接口調用失敗;
對於此類問題,需要一個長效的接口重新調用機制,在發生網絡抖動時可以進行自動地補償調用,或者記錄下來通知人工處理。
解決方案
建立 “補償接口信息表” ,主要字段:
全類名(即包名+類名):class_name
方法名:method_name
參數類型數組:method_param_types 按照方法簽名的順序插入數組
參數值數組:method_param_valuesTips:對象-->JsonString、null-->'null',按照方法簽名的順序插入數組 ,組成字符串數組
錯誤信息: error_msg
重試次數:retry_count
最大次數:max_retry_count
重試有效期: retry_expiry_date
數據防重code:unique_hash_code Unique_key Hash(class_name+method_name+method_param_values)
狀態:status 10:未解決;20:已解決
編寫InterfaceRetryInfo類用以記錄類名、方法名、參數值數組、最大次數、重試有效期等信息,開發人員在需要重試的業務方法中調用第三方系統接口失敗時,給這些信息賦值並調用InterfaceRetryInfoService.asyncRetry(retryInfo)方法異步存入數據庫,之后拋出RetryFlagException異常,以方便補償方法可以判斷重試調用成功與否;
編寫接口補償方法public boolean processRetryInfo(InterfaceRetryInfo retryInfo),通過反射獲取方法和參數並調用;
編寫遍歷方法doRetry(),遍歷數據庫中的所有未解決的接口補償數據,並提供Restful接口;
接入公司定時任務系統DING,定時調用doRetry()進行接口補償,如果補償成功,則修改數據狀態為**20:已解決**。
業務示例
注意事項
需要補償的第三方接口需滿足冪等性
調用第三方接口的邏輯需為單獨的處理方法,和業務邏輯分離;
第三方接口調用失敗或異常的情況下,需保證處理方法一定要拋出RetryFlagException異常。
處理方法的參數,類型可以為T、List<String>、List<T>、Map<String,String>,T代表Java基礎類型或者POJO,且POJO的屬性中如果有Map<K,V>,則K、V必須是String或其他Java基礎類型;否則處理會出錯。
如果處理方法使用了@Async注解實現異步處理,則返回值必須為Future且異常處理最后一定要return false。
示例
業務Controller
@RestController
@Slf4j
@RequestMapping(value = "/retry/demo")
public class RetryDemoController {
@Autowired
private InterfaceRetryInfoService interfaceRetryInfoService;
@RequestMapping(method = RequestMethod.GET)
public BaseResult<String> retryDemo(){
List<SampleBoxDO> sampleBoxDOList = new ArrayList<>();
List<InventoryOwnCardDO> demoList = new LinkedList<>();
Map<String, String> demoMap = new HashMap<>();
demoMap.put("test11", "test11");
demoMap.put("test22", "test22");
//省略初始化&賦值代碼
sampleBoxDO.setDemoList(demoList);
sampleBoxDO.setDemoMap(demoMap);
sampleBoxDOList.add(sampleBoxDO);
sampleBoxDOList.add(sampleBoxDO2);
//調用第三方接口的邏輯需為單獨的處理方法,和業務邏輯分離
String msg = notifySomeone("Hello World", null, sampleBoxDOList, demoMap);
return BaseResultUtils.ok(msg);
}
//調用第三方接口的處理方法
@Async
private Boolean notifySomeone(String param1, List<String> param2, List<SampleBoxDO> param3, Map<String, String> param4) {
try {
Random random = new Random();
int a = random.nextInt(10);
if (a < 5){ //模擬調用第三方失敗
//如果調用第三方接口異常,異步保持處理方法的信息到數據庫,等待定時任務進行補償重試
log.error("notifySomeone error--->");
String className = RetryDemoController.class.getName();
String methodName = "notifySomeone";
//方法參數值是不定長參數,param1、param2...paramn
InterfaceRetryInfo retryInfo = new InterfaceRetryInfo(className, methodName, e.getMessage(), param1, param2, param3, param4);
interfaceRetryInfoService.asyncRetry(retryInfo);
throw new RetryFlagException("調用第三方失敗,需要重試");
}else {//模擬調用第三方成功
System.out.println("param1->"+param1);
if (CollectionUtils.isNotEmpty(param2)){
param2.forEach(p -> System.out.println(p));
}
if (CollectionUtils.isNotEmpty(param3)){
param3.forEach(p -> System.out.println(p.toString()));
}
}
} catch (Exception e) {
//如果調用第三方接口異常,異步保持處理方法的信息到數據庫,等待定時任務進行補償重試
log.error("notifySomeone error--->",e);
String className = RetryDemoController.class.getName();
String methodName = "notifySomeone";
//方法參數值是不定長參數,param1、param2...paramn
InterfaceRetryInfo retryInfo = new InterfaceRetryInfo(className, methodName, e.getMessage(), param1, param2, param3, param4);
interfaceRetryInfoService.asyncRetry(retryInfo);
//處理方法最后一定要return false
return new AsyncResult<>(false);
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
實現
重試信息類&數據處理入庫
@Data
public class InterfaceRetryInfo implements Serializable {
//。。。省略屬性定義
/*構造方法
* 通過反射獲取方法的參數類型數組
* 使用JsonObject將參數值序列化為字符串
* 存入數據庫
*/
public InterfaceRetryInfo(String className, String methodName, String errorMsg, Object... methodParamValues) {
Assert.notNull(className);
Assert.notNull(methodName);
Assert.notNull(errorMsg);
this.className = className;
this.methodName = methodName;
if (methodParamValues != null && methodParamValues.length > 0) {
try {
//反射獲取類的Class,並獲取所有的聲明方法,遍歷之,獲取需要進行重試的方法(該方法不可重載,否則無法獲取准確的方法)
Class<?> clazz = Class.forName(className);
Method[] methods = clazz.getDeclaredMethods();
Method calledMethod=null;
for(Method method:methods){
if(method.getName().equals(methodName)){
calledMethod=method;
break;
}
}
//獲取方法的參數類型,遍歷參數值數組
Class<?>[] paramTypes = calledMethod.getParameterTypes();
List<String> paramValueStrList = new LinkedList<>();
for (int i = 0; i < methodParamValues.length; i++) {
Object paramObj = methodParamValues[i];
//如果值為空,則存入 "null"
if (null == paramObj){
paramValueStrList.add("null");
}else {
//如果參數是String類型,則直接存入
if (paramTypes[i] == String.class){
paramValueStrList.add((String) paramObj);
}else {
//如果參數是POJO或集合類型,則序列化為字符串
paramValueStrList.add(JSONObject.toJSONString(paramObj));
}
}
}
this.methodParamValues = JSONObject.toJSONString(paramValueStrList);
this.methodParamTypes = JSONObject.toJSONString(paramTypes);
} catch (ClassNotFoundException e ) {}
}
this.errorMsg = errorMsg;
this.uniqueHashCode = MD5.getInstance().getMD5String((className + methodName + this.methodParamValues).getBytes());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
接口重試的主要方法
@Override
public boolean processRetryInfo(InterfaceRetryInfo retryInfo) {
String className = retryInfo.getClassName();
String methodName = retryInfo.getMethodName();
String paramValuesStr = retryInfo.getMethodParamValues();
//反射獲取類的Class,並定位到要重試的方法
try {
Class<?> clazz = Class.forName(className);
Object object = context.getBean(clazz);
Method[] methods = clazz.getDeclaredMethods();
Method calledMethod=null;
for(Method method:methods){
if(method.getName().equals(methodName)){
calledMethod=method;
break;
}
}
Object[] paramValueList = null;
//如果方法有參數,則進行參數解析
if (StringUtils.isNotEmpty(paramValuesStr)){
List<String> paramValueStrList = JSONObject.parseArray(paramValuesStr, String.class);
//獲取方法所有參數的Type
Type[] paramTypes = calledMethod.getGenericParameterTypes();
paramValueList = new Object[paramTypes.length];
for (int i = 0; i < paramValueStrList.size(); i++) {
String paramStr = paramValueStrList.get(i);
//如果參數值為空,則置為null
if ("null".equalsIgnoreCase(paramStr)){
paramValueList[i] = null;
}else {
//如果參數是String類型,則直接賦值
if (paramTypes[i] == String.class){
paramValueList[i] = paramStr;
// 如果參數是帶泛型的集合類或者不帶泛型的List,則需要特殊處理
}else if(paramTypes[i] instanceof ParameterizedType || paramTypes[i] == List.class){
Type genericType = paramTypes[i];
//如果是不帶泛型的List 直接解析數組
if (genericType == List.class){
paramValueList[i] = JSON.parseObject(paramStr, List.class);
}else if (((ParameterizedTypeImpl) genericType).getRawType() == List.class){
// 如果是帶泛型的List,則獲取其泛型參數類型
ParameterizedType pt = (ParameterizedType) genericType;
//得到泛型類型對象
Class<?> genericClazz = (Class<?>)pt.getActualTypeArguments()[0];
//反序列化
paramValueList[i] = JSON.parseArray(paramStr, genericClazz);
}else {
//如果是帶泛型的其他集合類型,直接反序列化
paramValueList[i] = JSON.parseObject(paramStr, paramTypes[i], Feature.OrderedField);
}
}else {
//如果是POJO類型,則直接解析對象
paramValueList[i] = JSON.parseObject(paramStr, paramTypes[i], Feature.OrderedField);
}
}
}
}
//設置訪問權限,否則會調用失敗,throw IllegalAccessException
calledMethod.setAccessible(true);
//反射調用方法
boolean asyncFlag = false;
Annotation[] annotations = calledMethod.getDeclaredAnnotations();
if (annotations != null && annotations.length > 0){
for (Annotation annotation : annotations) {
if (annotation.annotationType().getTypeName().equalsIgnoreCase("org.springframework.scheduling.annotation.Async")){
asyncFlag = true;
}
}
}
if (asyncFlag){
Future<Boolean> future = (Future) calledMethod.invoke(object, paramValueList);
Boolean flag = future.get();
if(!flag){
throw new RetryFlagException();
}
}else {
calledMethod.invoke(object, paramValueList);
}
retryInfo.setStatus(InterfaceRetryInfoStatusEnum.HAS_DONE.getCode());
} catch (ClassNotFoundException | IllegalAccessException | InterruptedException | ExecutionException e ) {
log.error("反射異常-->",e);
return false;
}catch (InvocationTargetException | RetryFlagException e){
log.error("重試調用失敗,更新次數-->",e);
}
retryInfo.setRetryTimes((retryInfo.getRetryTimes()) + 1);
interfaceRetryInfoDAO.updateByPrimaryKeySelective(retryInfo);
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
————————————————
版權聲明:本文為CSDN博主「忙里偷閑得幾回」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/panyongcsd/article/details/81485298