代码提效-统一异常处理

引言

作为一名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();
    }
}


版权声明:本文为weixin_37935945原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。