基础设施建设——全局异常请求处理

基础设施建设——全局异常请求处理

1.引言

在大型微服务架构中,伴随着错综复杂的调用链,统一的、全局的异常请求兜底处理就显得非常重要,如果没有全局统一的请求/响应规范,上下游之间的接口调用、协同配合将会变得异常困难,但是单纯的在业务逻辑中声明可能抛出的异常或者可能返回的错误类并不能完全覆盖所有异常情况,总会有一些“漏网之鱼”成为服务潜在的威胁,因此就需要兜底措施,即全局的统一异常请求处理。

一般来说,有着全局影响的代码会作为基础架构的一部分放到类似 base/basic/infrastructure包下,所有域导入基础包依赖。作为全局的、异常请求的统一处理类,我们可以从中提取出一些要素:首先,”全局“要求方法所处的位置要在比较高的层面;其次,“统一处理”提示我们应该采用aop或者使用回调函数加入FilterChain的方式;最后,“异常请求”说明我们的切入点应该是服务间的接口调用层面。通过以上分析结合我司技术架构,可以得到结论:在web mvc接口、dubbo接口以及部分open feign接口做统一增强/异常封装。

2.dubbo接口的统一异常处理

对于dubbo接口的统一异常处理,可以直接扩展其provider侧的过滤器,如有异常则打印日志返回异常响应,参考代码如下:

@Activate(group = Constants.PROVIDER,  order = -1000)
public class DubboExceptionFilter implements Filter {

    private static final Logger log = LoggerFactory.getLogger(DubboExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        try {
            Result result = invoker.invoke(invocation);
            if (result.hasException() && GenericService.class != invoker.getInterface()) {
                Throwable throwable = result.getException();
                if (throwable instanceof Exception || throwable instanceof Error) {
                    AsyncRpcResult asyncRpcResult = AsyncRpcResult.newDefaultAsyncResult(invocation);
                    // 业务Error类
                    RpcInvokeThrowExceptionError rpcInvokeThrowExceptionError = new RpcInvokeThrowExceptionError(null);
                    rpcInvokeThrowExceptionError.setErrorMessage(throwable.getMessage());
                    asyncRpcResult.setValue(fail(rpcInvokeThrowExceptionError));
                    // 打印错误日志
                    if (throwable instanceof Exception) {
                        error((Exception) throwable);
                    } else {
                        error((Error) throwable);
                    }
                    ThreadUtil.execAsync(() -> sendDingTalk(invocation, throwable));
                    return asyncRpcResult;
                }
            }
            return result;
        } catch (Exception e) {
            throw e;
        }
    }
    
    public CommonResponse fail(BizError bizError) {
        CommonResponse response = new CommonResponse();
        response.setSuccess(false);
        response.setCode(bizError.getErrorCode());
        response.setMessage(bizError.getErrorMessage());
        response.setData(null);
        response.setRequestId(getRequestId());
        response.setCommonBizError(new CommonBizError(bizError));
        return response;
    }

    public static void error(Error e) {
        if (log.isErrorEnabled()) {
            String className = null;
            String methodName = null;
            Integer lineNumber = null;
            StackTraceElement invoker = Thread.currentThread().getStackTrace()[2];
            if (Objects.nonNull(invoker)) {
                className = invoker.getClassName();
                methodName = invoker.getMethodName();
                lineNumber = invoker.getLineNumber();
            }
            log.error("[Class:{}][Method:{}][Line:{}]-[{}]", className, methodName, lineNumber, e, e.getStackTrace());
        }
    }

    public static void error(Exception e) {
        if (log.isErrorEnabled()) {
            String className = null;
            String methodName = null;
            Integer lineNumber = null;
            StackTraceElement invoker = Thread.currentThread().getStackTrace()[2];
            if (Objects.nonNull(invoker)) {
                className = invoker.getClassName();
                methodName = invoker.getMethodName();
                lineNumber = invoker.getLineNumber();
            }
            log.error("[Class:{}][Method:{}][Line:{}]-[{}]", className, methodName, lineNumber, null);
        }
    }
}

3.http接口的统一异常处理

基于http协议的请求在JEE中都是要遵循servlet规范,servlet规范中定义了Filter拦截器用于处理容器级别的请求过滤,而在SpringMVC中也存在拦截器(Interceptor)通过AOP的方式处理请求,所以在Spring Web中是要先后经过容器的Filter和SpringMVC的拦截器,但是并不是所有的应用都会用SpringMVC来处理http请求,所以要在Filter层做好兜底措施。

而对于SpringMVC处理的请求,则使用@RestControllerAdvice+@ExceptionHandler注解的方式拦截捕获请求,前者一个复合注解,包含@ControllerAdvice @ResponseBody

  • @ControllerAdvice:该注解标志着一个类可以为所有的 @RequestMapping 处理方法提供通用的异常处理和数据绑定等增强功能。当应用到一个类上时,该类中定义的方法将在所有控制器类的请求处理链中生效。

  • @ResponseBody:表示方法的返回值将被直接写入 HTTP 响应体中,通常配合 Jackson 或 Gson 等 JSON 库将对象转换为 JSON 格式的响应。

而后者通过在控制器中或标记@ControllerAdvice的类中标记@ExceptionHandler,可以为特定类型的异常提供自定义的处理逻辑。SpringMVC会在启动时扫描容器中标注@ControllerAdvice的bean,ExceptionHandlerMethodResolver初始化时解析会当前的@ControllerAdvice的bean异常处理器。

@RestControllerAdvice
@AllArgsConstructor
@Slf4j
public class GlobalExceptionHandler {

    private final String applicationName;

    /**
     * 不是所有的应用都会用SpringMVC来处理http请求,所以要在Filter层做好兜底措施
     */
    public ConsoleResponse<?> filterExceptionHandler(HttpServletRequest request, Throwable ex) {
        if (ex instanceof MissingServletRequestParameterException) {
            return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
        }
        if (ex instanceof MethodArgumentTypeMismatchException) {
            return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
        }
        if (ex instanceof MethodArgumentNotValidException) {
            return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
        }
        if (ex instanceof BindException) {
            return bindExceptionHandler((BindException) ex);
        }
        if (ex instanceof ConstraintViolationException) {
            return constraintViolationExceptionHandler((ConstraintViolationException) ex);
        }
        if (ex instanceof ValidationException) {
            return validationException((ValidationException) ex);
        }
        if (ex instanceof NoHandlerFoundException) {
            return noHandlerFoundExceptionHandler(request, (NoHandlerFoundException) ex);
        }
        if (ex instanceof HttpRequestMethodNotSupportedException) {
            return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
        }
        if (ex instanceof ServiceException) {
            return serviceExceptionHandler((ServiceException) ex);
        }
        if (ex instanceof AccessDeniedException) {
            return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
        }
        return defaultExceptionHandler(request, ex);
    }

    /**
     * 处理 SpringMVC 请求参数缺失
     */
    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public ConsoleResponse<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
        return ConsoleResponse.fail(null, BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
    }

    /**
     * 处理 SpringMVC 请求参数类型错误
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ConsoleResponse<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
        return ConsoleResponse.fail(null, BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
    }

    /**
     * 处理 SpringMVC 参数校验不正确
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ConsoleResponse<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
        log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
        FieldError fieldError = ex.getBindingResult().getFieldError();
        assert fieldError != null;
        return ConsoleResponse.fail(null, BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
    }

    /**
     * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
     */
    @ExceptionHandler(BindException.class)
    public ConsoleResponse<?> bindExceptionHandler(BindException ex) {
        log.warn("[handleBindException]", ex);
        FieldError fieldError = ex.getFieldError();
        assert fieldError != null;
        return ConsoleResponse.fail(null, BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
    }

    /**
     * 处理 Validator 校验不通过产生的异常
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    public ConsoleResponse<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
        log.warn("[constraintViolationExceptionHandler]", ex);
        ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
        return ConsoleResponse.fail(null, BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
    }

    /**
     * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
     */
    @ExceptionHandler(value = ValidationException.class)
    public ConsoleResponse<?> validationException(ValidationException ex) {
        log.warn("[constraintViolationExceptionHandler]", ex);
        return ConsoleResponse.fail(null, BAD_REQUEST);
    }
    
    /**
     * 处理 Spring Security 权限不足的异常,来源: @PreAuthorize
     */
    @ExceptionHandler(value = AccessDeniedException.class)
    public ConsoleResponse<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
        log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), req.getRequestURL(), ex);
        return ConsoleResponse.fail(null, FORBIDDEN);
    }

    /**
     * 处理业务异常 ServiceException
     */
    @ExceptionHandler(value = ServiceException.class)
    public ConsoleResponse<?> serviceExceptionHandler(ServiceException ex) {
        log.info("[serviceExceptionHandler]", ex);
        ConsoleResponse<Integer> fail = ConsoleResponse.fail(null, ex.getCode(), ex.getMessage());
        if (ex.getRequestId() != null) {
            fail.setRequestId(ex.getRequestId());
        }
        return fail;
    }

    /**
     * 兜底处理
     */
    @ExceptionHandler(value = Exception.class)
    public ConsoleResponse<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {

        // ...
        log.error("[defaultExceptionHandler]", ex);
        return ConsoleResponse.fail(null, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
    }
}

再创建Filter拦截器,直接调用filterExceptionHandler方法即可。

@RequiredArgsConstructor
public class ExceptionFilter extends OncePerRequestFilter {

    private final GlobalExceptionHandler globalExceptionHandler;
    
    @Override
    @SuppressWarnings("NullableProblems")
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
       try {
        // 认证授权...
        } catch (Throwable ex) {
            ConsoleResponse<?> result = globalExceptionHandler.filterExceptionHandler(request, ex);
            ServletUtils.writeJSON(response, result);
            return;
        }
        chain.doFilter(request, response);
    }
}

本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。

posted @ 2024-06-12 14:44  爱吃麦辣鸡翅  阅读(32)  评论(0编辑  收藏  举报