引言
作为一名Java开发,每天就在和各位异常打交道,有时候代码中处理异常逻辑的部分甚至会超过正常逻辑的代码量,大量的try-catch代码散落在项目代码的各个地方。想象一下,下面哪种代码写起来更让人赏心悦目呢?
第一种:每个接口都try-catch异常
@RestController("exception")
public class ExceptionController {
@Autowired
private UserService userService;
@GetMapping("/get")
public BaseResult<User> getUser(Long id){
if (id == null){
return Result.fail("id不能为空");
}
try {
User user = userService.getUser(id);
return Result.success(user);
} catch (BusinessException e){
// do something
return Result.fail(e.getMessage());
} catch (Exception e){
// do something
return Result.fail(e.getMessage());
} catch (Throwable t){
// do something
return BaseResult.fail(t.getMessage());
}
}
@GetMapping("/getId")
public BaseResult<Long> getId(String userName){
if (StringUtils.isBlank(userName)){
return Result.fail("userName不能为空");
}
try {
Long id = userService.getId(name);
return Result.success(id);
} catch (BusinessException e){
// do something
return Result.fail(e.getMessage());
} catch (Exception e){
// do something
return Result.fail(e.getMessage());
} catch (Throwable t){
// do something
return BaseResult.fail(t.getMessage());
}
}
}
第二种:使用aop统一异常处理
@RestController("exception")
public class ExceptionController {
@Autowired
private UserService userService;
@GetMapping("/get")
@ApiExceptionHandler
public BaseResult<User> getUserName(Long id){
Preconditions.checkArgument(id != null,"id不能为空");
User user = userService.getUser(id);
return BaseResult.success(user);
}
@GetMapping("/getId")
@ApiExceptionHandler
public BaseResult<Long> getId(String userName) {
Preconditions.checkArgument(StringUtils.isNotBlank(name), "userName不能为空");
Long id = userService.getId(userName);
return BaseResult.success(id);
}
}
第一种方式:1. 重复代码多;2. 主逻辑淹没在异常逻辑中,易懂性差;3.异常类型不易管理,每个接口都要去catch全部异常,久而久之就会变成只catch一种异常即Exception
第二种方式:使用aop统一异常处理,主逻辑清晰,重复代码少,且异常只需要在一个地方维护
下面将介绍两种能够将两者优点结合起来的统一异常处理方案,第一种是spring自带的,另一种是借用aop自己实现。
统一异常处理方案:
1. Spring的统一异常处理
Spring提供@ControllerAdvice和@ExceptionHandler注解,**作用于Controller类,**当其发生异常时,会路由到对应的统一异常处理程序中。
1.1 实现代码
@Component
@ControllerAdvice
@Slf4j
public class UnitedExceptionHandler {
@ExceptionHandler(NoPermissionException.class)
@ResponseBody
public Result handleNoPermissionException(NoPermissionException e){
log.error("参数异常",e);
doNoPermission(e.getMessage(), e.getApplyLink());
return Result.fail(e.getMessage());
}
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result handleBusinessException(BusinessException e){
log.error("业务异常",e);
return Result.fail(e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handleException(Exception e){
log.error("未知异常",e);
return Result.fail(e.getMessage());
}
@ExceptionHandler(Throwable.class)
@ResponseBody
public Result handleThrowable(Throwable e){
log.error("未知异常",e);
return Result.fail(e.getMessage());
}
}
1.2 测试代码:
@Controller
public class MainController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping("/test2")
@ResponseBody
public Result<List<String>> test2() {
throw new BusinessException(1, "expectionHanlder");
}
}
1.3 运行结果:

1.4 路由原理
当异常发生时,怎么知道异常会路由到那个处理方法中呢?
ExceptionHandler使用精确匹配机制,即优先路由的最匹配的异常,比如当发生BusinessException时,会路由到handleBusinessException(BusinessException e)方法中,若匹配不到则向上匹配,直至Throwable结束,spring匹配源码如下:
public class ExceptionHandlerMethodResolver {
/**
* 返回异常对应的处理方法,不存在返回null
*/
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
if (!matches.isEmpty()) {
// 根据匹配程度进行排序,匹配度最高的排在前面,matches.get(0)取出
matches.sort(new ExceptionDepthComparator(exceptionType));
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}
}
匹配规则源码如下:
public class ExceptionDepthComparator implements Comparator<Class<? extends Throwable>> {
@Override
public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) {
// 递归匹配算法
int depth1 = getDepth(o1, this.targetException, 0);
int depth2 = getDepth(o2, this.targetException, 0);
return (depth1 - depth2);
}
/**
* 获取匹配深度
**/
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
// 终止条件1,精确匹配成功
if (exceptionToMatch.equals(declaredException)) {
// 返回递归深度
return depth;
}
// 终止条件2,未匹配到返回最大值
if (exceptionToMatch == Throwable.class) {
return Integer.MAX_VALUE;
}
// 递归推进条件,将目标异常类的父类设置为匹配类,且递归深度+1
return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
}
}
这样就匹配到对应异常处理方法,注意最多只会匹配到一个方法,执行后返回。
Spring自带的异常处理机制,只适用于controller层接口,但是在现在微服务盛行的今天,rpc接口应用也非常广泛,所以仅对controller层进行统一异常处理是远远不够的,下面我们将介绍适用aop自定义实现的统一异常处理
2. 自定义Aop实现统一异常处理
2.1 现状分析
好的代码是分层的,那么对应的异常处理也一定是分层的,底层如:DAO,Manager层,其实只需简单将异常翻译为可理解,有助于定位和解决问题的信息,封装成BizException等向上抛出即可,而对外暴露的层如Service(RPC接口)和Controller(HTTP接口),则需要统一处理异常,将返回结果封装成Result形式,其中的message字段用于异常信息提示。
请求的完整调用链路为:**请求-》api接口层(http or prc)-》内部处理-》返回结果,**所以只需要在api层对异常统一包装即可。
2.2 测试代码
首先定义一个注解,作用于方法或者类,用于标注需要统一异常处理的地方
@RestController
@Slf4j
@Validated
public class TestController {
@RequestMapping("/test/getCity")
@ApiExceptionHandler
public PageResult getCity(
@NotNull(message = "id不能为空") Long id, @NotBlank(message = "城市名称不能为空") String cityName){
log.error("id={},cityName={}",id,cityName);
CityDO cityDO = new CityDO();
cityDO.setCityName(cityName);
cityDO.setId(id);
return new PageResult();
}
2.3 运行结果

打印的日志:
2020-12-21 21:47:17.631 ERROR 29020 --- [nio-7001-exec-1] c.a.b.exception.ApiExceptionHandlerAop : com.alibaba.baiji.TestController|getCity|param|id=null|cityName=null|ConstraintViolationException
javax.validation.ConstraintViolationException: getCity.arg1: 城市名称不能为空, getCity.arg0: id不能为空
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
。。。
也可以结合规范的日志处理程序,来配置异常监控
2.4 切面程序处理代码:
@Component
@Slf4j
@Aspect
public class ApiExceptionHandlerAop {
@Pointcut("@annotation(com.alibaba.baiji.exception.ApiExceptionHandler)")
public void capture() {
}
@Around("capture()")
public Object doAround(ProceedingJoinPoint jp) {
Object result;
MethodSignature signature;
Class returnType = null;
try {
signature = (MethodSignature) jp.getSignature();
returnType = signature.getReturnType();
result = jp.proceed();
return result;
} catch (IllegalArgumentException e) {
log.error("{}|{}|IllegalArgumentException", jp.getTarget().getClass().getName(), param(jp), e);
return createErrorResult(returnType, GenericErrorCode.ILLEGAL_PARAM.getCode(), GenericErrorCode.ILLEGAL_PARAM.getDesc());
} catch (ConstraintViolationException e) {
StringBuilder sb = new StringBuilder(200);
e.getConstraintViolations().forEach((violation) -> {
sb.append(violation.getMessage()).append(",");
});
sb.deleteCharAt(sb.length() - 1);
log.error("{}|{}|ConstraintViolationException", jp.getTarget().getClass().getName(), param(jp), e);
return createErrorResult(returnType, GenericErrorCode.ILLEGAL_PARAM.getCode(), sb.toString());
} catch (Exception e) {
log.error("{}|{}|Exception", jp.getTarget().getClass().getName(), param(jp), e);
return createErrorResult(returnType, GenericErrorCode.SYSTEM_EXCEPTION.getCode(), GenericErrorCode.SYSTEM_EXCEPTION.getDesc());
} catch (Throwable t) {
log.error("{}|{}|Throwable", jp.getTarget().getClass().getName(), param(jp), t);
return createErrorResult(returnType, GenericErrorCode.SYSTEM_EXCEPTION.getCode(), GenericErrorCode.SYSTEM_EXCEPTION.getDesc());
}
}
/**
* 构造异常返回对象,当返回类型非BaseResult和PageResult时,抛出运行时异常
*
* @param returnType 真正返回值类型
* @param errorCode 错误代码
* @param message 错误描述
* @return
*/
private Object createErrorResult(Class returnType, String errorCode, String message) {
if (returnType.equals(BaseResult.class)) {
return BaseResult.failed(errorCode, message);
} else if (returnType.equals(PageResult.class)) {
PageResult pageResult = new PageResult();
pageResult.setErrorCode(errorCode, message);
return pageResult;
}
log.error("返回值类型不匹配,实际返回值类型:{}, 非BaseResult或PageResult", returnType);
// 基本上这种异常,开发阶段就可以发现,不放心可以配置监控兜底
throw new RuntimeException("返回值类型与异常自动捕获处理器返回类型不匹配,实际返回类型" + returnType + ",非BaseResult或PageResult");
}
/**
* 解析参数,解析后格式为: method|param|key=value,key1=value1
*
* @param jp
* @return
*/
public String param(ProceedingJoinPoint jp) {
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
Object[] args = jp.getArgs();
StringBuilder sb = new StringBuilder(1000);
sb.append(methodName).append("|")
.append("param").append("|")
.append(parsekeyValue(paramNames, args));
return sb.toString();
}
private String parsekeyValue(String[] parameters, Object[] args) {
if (ArrayUtils.isEmpty(args)) {
return "";
}
StringBuilder sb = new StringBuilder(300);
for (int i = 0; i < args.length; i++) {
sb.append(parameters[i]).append("=").append(args[i]).append("|");
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
}