微盛|团队技术博客

异常传递流转规范

千里(许硕)

2023-04-14

一、背景

团队内部开发同学对异常的中断、传递、处理、日志用法各种各样,甚至有少部分错用,给线上问题排查和系统间对接带来不必要的麻烦,经基础服务开发同学讨论对齐,共同制定了以下异常使用规范

二、异常抛出触发点

1. 可预期的业务异常

抛出自定义异常

1
2
3
4
5
TenantInfoEditionFullDTO tenantInfo = tenantAdapter.getTenantInfo(tenantId);
if(ObjectUtils.isEmpty(tenantInfo)){
log.warn("获取租户信息失败,{}",tenantId);
throw new BaseUserCenterException(UserErrorEnum.U012005);
}

2. 可预期的程序异常

抛出自定义异常

1
2
3
4
5
6
7
8
9
RLock lock = redisson.getLock("LOCK_KEY");
try {
if(lock.tryLock(1, 5, TimeUnit.SECONDS)){
// ...
}
} catch (InterruptedException e) {
log.error("获取XXX锁失败,tenantId:{}",tenantId,e);
throw new BaseBizException(BaseBizExceptionEnum.Z067004);
}

3. 不可预期的catch异常

向外传递,交由全局切面处理

1
2
3
4
5
6
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.error("XXXX", e);
throw e;
}

4. 不可预期的异常

无感知,交由全局切面处理

1
2
3
4
5
6
7
8
eg.
int base = 1;
int sum = 0;
double rate = base/sum;

eg.
List<ActiveAccountDTO> activeAccountDTOS = new ArrayList<>();
activeAccountDTOS.get(1);

5. 不处理的异常【不建议使用】

1
2
3
4
5
try {
// ...
} catch (Exception e) {
log.error("XXXX", e);
}

异常流转原则:可预期异常封装后抛出,不可预期异常交由切面处理
日志原则:可预期异常,打印warn日志,不打印堆栈信息、不可预期异常,打印error日志,打印堆栈信息

三、全局切面

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
package com.wshoto.basebiz.service.common.exception;

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.wshoto.framework.common.base.Result;
import com.wshoto.framework.common.base.ResultUtils;
import com.wshoto.framework.common.handller.BaseExceptionHandler;

import lombok.extern.slf4j.Slf4j;

/**
*
* @author : qianli
* @date : 2022/11/21
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends BaseExceptionHandler {

/**
* 自定义异常处理
*
* @param e
* @return
*/
@ExceptionHandler(BaseBizException.class)
public Result error(BaseBizException e) {
log.warn("基础服务聚合层可预期异常", e);
return ResultUtils.error(e.getCode(), e.getOriginalMessage());
}

/**
* @RequestParam 缺少参数效验异常
*
* @param e
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public Result error(MissingServletRequestParameterException e) {
log.warn("基础服务聚合层RequestParam参数校验异常", e.getMessage());
return ResultUtils.error(BaseBizExceptionEnum.PARAMETER_REQUIRED_EXCEPTION.getCode(), String.format(BaseBizExceptionEnum.PARAMETER_REQUIRED_EXCEPTION.getMessage(),e.getParameterName()));
}

/**
* validation表单校验
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result error(MethodArgumentNotValidException e) {
log.warn("基础服务聚合层表单参数校验异常", e.getMessage());
BindingResult result = e.getBindingResult();
if (result.hasErrors()) {
return ResultUtils.error(BaseBizExceptionEnum.DATA_PARAM_NONE.getCode(), result.getAllErrors().get(0).getDefaultMessage());
} else {
return ResultUtils.error(BaseBizExceptionEnum.DATA_PARAM_NONE.getCode(), BaseBizExceptionEnum.DATA_PARAM_NONE.getMessage());
}
}
/**
* 全局异常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Result error(Exception e) {
log.error("基础服务聚合层系统异常", e);
return ResultUtils.error(BaseBizExceptionEnum.DEFAULT.getCode(), BaseBizExceptionEnum.DEFAULT.getMessage());
}
/**
* 全局错误
* @param e
* @return
*/
@ExceptionHandler(Throwable.class)
public Result error(Throwable e) {
log.error("基础服务聚合层系统错误", e);
return ResultUtils.error(BaseBizExceptionEnum.DEFAULT.getCode(), BaseBizExceptionEnum.DEFAULT.getMessage());
}

}

四、调用二方 feign 接口异常处理

1. 中断程序

对于大部分场景来说,当调用feign接口失败时,程序需要做中断处理,这种情况如果需要避免中断,可在调用处做try catch处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 获取租户应用授权信息
*
* @return
*/
public TenantAgentInfoDTO getAgentInfoBySuiteId(String tenantId, String suiteId, Boolean withCorp) {
log.info("获取租户应用授权信息,入参:{},{},{}", tenantId, suiteId, withCorp);
Result<TenantAgentInfoDTO> result = qwTenantQueryFeign.getAgentInfoBySuiteId(tenantId, suiteId, withCorp);
log.info("获取租户应用授权信息,查询结果:{}", JSONObject.toJSONString(result));
if (!ResultUtils.isSuccess(result)) {
log.error("获取租户应用授权信息异常,{}",JSONObject.toJSONString(result));
throw new BaseUserCenterException(result.getCode(),result.getMsg());
}
return result.getData();
}

2. 赋默认值【慎用】

有一些特殊业务,比如说,发送消息,或大事务中的非关键流程,为了避免类似逻辑异常影响主流程可采用这种方式,调用方需要特别注意,返回值有业务失真的风险

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 调用消息中心发送应用消息
* @param type
* @param corpId
* @param sendSuiteMessageReq
* @return
*/
public List<SendSuiteMessageDTO> messageSendWithDefaul(String type, String corpId, SendSuiteMessageReq sendSuiteMessageReq) {
log.info("调用消息中心发送应用消息入参:{}, {}, {},", type, corpId, sendSuiteMessageReq.getToUser(),sendSuiteMessageReq.getDescription());
Result<List<SendSuiteMessageDTO>> result = messageCenterServiceFeign.messageSend(type, corpId, sendSuiteMessageReq);
if (ResultUtils.isSuccess(result)) {
log.info("调用消息中心发送应用消息返回值:{}", result);
return result.getData();
}
log.warn("调用消息中心发送应用消息返回值:{}", result);
return new ArrayList<>();
}

3. 异常隔离【慎用】

仅适用于聚合层编排过程中使用,可以保留每一块儿业务数据的异常快照及提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 获取租户列表
*/
public Result<List<TenantAgentInfoDTO>> queryTenantAgentByListOriginal(TenantAgentFilterRequest request) {
try{
log.info("调用租户中心查询租户信息,入参:{}", JSONUtil.toJsonStr(request));
Result<List<TenantAgentInfoDTO>> result = qwTenantQueryFeign.queryTenantAgentByList(request);
log.info("调用租户中心查询租户信息,返回值:{}", JSONUtil.toJsonStr(result));
return result;
} catch (InterruptedException e) {
log.error("XXXX", e);
}
return ResultUtil.error();
}

五、异常传递时序图

1. 正常流程

image.png

2. 异常流程

image.png

六、结论

  1. feign 接口是否需要 try catch

不需要,有查询日志需要,使用@WebLog 注解
2. 聚合层的编排
聚合层的feign接口用adapter封装
3. 包结构

image.png

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
package com.wshoto.basebiz.service.adapter.tenant;

import cn.hutool.json.JSONUtil;
import com.wshoto.basebiz.service.common.exception.BaseBizException;
import com.wshoto.basebiz.service.common.exception.BaseBizExceptionEnum;
import com.wshoto.framework.common.base.Result;
import com.wshoto.framework.common.base.ResultUtils;
import com.wshoto.tenantservice.feign.news.client.QwTenantQueryFeign;
import com.wshoto.tenantservice.feign.news.dto.TenantAgentInfoDTO;
import com.wshoto.tenantservice.feign.news.request.TenantAgentFilterRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;

/**
* @author qianli
*/
@Slf4j
public class TenantAdapter {
@Autowired
private QwTenantQueryFeign qwTenantQueryFeign;

/**
* 获取租户应用授权信息
*
* @return
*/
public TenantAgentInfoDTO getAgentInfoBySuiteId(String tenantId, String suiteId, Boolean withCorp) {
Result<TenantAgentInfoDTO> result = getAgentInfoBySuiteIdOriginal(tenantId, suiteId, withCorp);
if (!ResultUtils.isSuccess(result)) {
log.error("获取租户应用授权信息异常,{}",JSONUtil.toJsonStr(result));
throw new BaseBizException(result.getCode(),result.getMsg());
}
return result.getData();
}
/**
* 获取租户应用授权信息
* 当返回单个对象时,返回的默认值为null,当返回值为集合时,返回空集合
*
* @return
*/
public TenantAgentInfoDTO getAgentInfoBySuiteIdWithDefault(String tenantId, String suiteId, Boolean withCorp) {
Result<TenantAgentInfoDTO> result = getAgentInfoBySuiteIdOriginal(tenantId, suiteId, withCorp);
if (ResultUtils.isSuccess(result)) {
return result.getData();
}
return null;
// return new ArrayList<>();
// return new HashSet<>();
}
/**
* 获取租户应用授权信息
*
* @return
*/
public Result<TenantAgentInfoDTO> getAgentInfoBySuiteIdOriginal(String tenantId, String suiteId, Boolean withCorp) {

try{
log.info("获取租户应用授权信息,入参:{},{},{}", tenantId, suiteId, withCorp);
Result<TenantAgentInfoDTO> result = qwTenantQueryFeign.getAgentInfoBySuiteId(tenantId, suiteId, withCorp);
log.info("获取租户应用授权信息,查询结果:{}", JSONUtil.toJsonStr(result));
return result;
} catch (Exception e) {
log.error("XXXX", e);
return ResultUtils.error(BaseBizExceptionEnum.DEFAULT.getCode(),BaseBizExceptionEnum.DEFAULT.getMessage() );
}
}
}

Tags: 后端

作者: 千里(许硕)