springboot controllerAdvice使用

[text]在spring中使用全局异常捕获来处理全局异常是一种很常见的操作

1 创建 GlobalExceptionHandler.java

package com.wycms.framework.web.exception;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;

import com.wycms.common.enums.ExceptionEnum;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import com.wycms.common.core.domain.AjaxResult;
import com.wycms.common.exception.BusinessException;
import com.wycms.common.exception.DemoModeException;
import com.wycms.common.utils.ServletUtils;
import com.wycms.common.utils.security.PermissionUtils;

/**
 * 全局异常处理器
 *
 * @author wycms
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 权限校验失败 如果请求为ajax返回json,普通请求跳转页面
     */
    @ExceptionHandler(AuthorizationException.class)
    public Object handleAuthorizationException(HttpServletRequest request, AuthorizationException e) {
        log.error(e.getMessage(), e);
        if (ServletUtils.isAjaxRequest(request)) {
            return AjaxResult.error(PermissionUtils.getMsg(e.getMessage()));
        } else {
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.setViewName("error/unauth");
            return modelAndView;
        }
    }

    /**
     * 请求方式不支持
     */
    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public AjaxResult handleException(HttpRequestMethodNotSupportedException e) {
        log.error(e.getMessage(), e);
        return AjaxResult.error("不支持' " + e.getMethod() + "'请求");
    }

    /**
     * 拦截未知的运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    public AjaxResult runtimeErrorHandler(RuntimeException e) {
        log.error("运行时异常:", e);
        String detailMsg = e.getCause() != null ? e.getMessage() : "";
        String additionInfo = e.getCause() != null ? e.getCause().getMessage() : "";
        detailMsg += "  " + additionInfo;
        return AjaxResult.error("运行时异常:" + e.getMessage() + " " + detailMsg);
    }



    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Object businessException(HttpServletRequest request, BusinessException e) {
        log.error(e.getMessage(), e);
        if (ServletUtils.isAjaxRequest(request)) {
            return AjaxResult.error(e.getMessage());
        } else {
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.addObject("errorMessage", e.getMessage());
            modelAndView.setViewName("error/business");
            return modelAndView;
        }
    }

    /**
     * 自定义验证异常
     */
    @ExceptionHandler(BindException.class)
    public AjaxResult validatedBindException(BindException e) {
        log.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();
        return AjaxResult.error(message);
    }

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public AjaxResult handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return AjaxResult.failure(ExceptionEnum.PARAM_NOT_VALID.getShowMsg());
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public AjaxResult handleConstraintViolationException(ConstraintViolationException ex) {
        return AjaxResult.failure(ExceptionEnum.PARAM_NOT_VALID.getShowMsg());
    }

    /**
     * 演示模式异常
     */
    @ExceptionHandler(DemoModeException.class)
    public AjaxResult demoModeException(DemoModeException e) {
        return AjaxResult.error("演示模式,不允许操作");
    }

    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e) {
        log.error(e.getMessage(), e);
        return AjaxResult.error("服务器错误,请联系管理员");
    }
}

自定义的异常BusinessException

public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    protected final String message;

    public BusinessException(String message) {
        this.message = message;
    }

    public BusinessException(String message, Throwable e) {
        super(message, e);
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

2 使用

public String importUser(List<UserOperateModel> userList, Boolean isUpdateSupport) {
        if (StringUtils.isNull(userList) || userList.size() == 0) {
            throw new BusinessException("导入用户数据不能为空!");
        }
        int successNum = 0;
        int failureNum = 0;
        

        return "";
    }

3. 常见问题,无法进入到我们的自定义Exception,而被全局捕获

1. 全局Aop中有try catch,但是catch中没有做相应的throw的类型转换

@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    // 配置织入点
    @Pointcut("@annotation(com.wycms.common.annotation.Log)")
    public void logPointCut() {
    }

    /**
     * 处理时执行,记录入参
     * @param joinPoint
     * @return
     * @throws Exception
     */
    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Exception {
        //R.error(1000,"接口调用失败!") ;
        Object result = AjaxResult.success();
        try {
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();

            Map<String, String[]> parameterMap = request.getParameterMap();
            StringBuffer sb = new StringBuffer();
            //String requestId = IdUtil.simpleUUID();
            //sb.append("\n【request_id】:").append(requestId);
            sb.append("\n【请求 URL】:").append(request.getRequestURL());
            sb.append("\n【请求 IP】:").append(getIp(request));
            sb.append("\n【请求类名】:").append(joinPoint.getSignature().getDeclaringTypeName());
            sb.append("【请求方法名】:").append(joinPoint.getSignature().getName());
            sb.append("\n【body】:").append(tryToGetArgsString(1, joinPoint, null));
            sb.append("\n【请求参数】:").append(tryToGetArgsString(2, null, parameterMap));

            String requestLog = sb.toString();

            logger.info(" \n 请求接口时间:" + DateUtils.getNowDate() + ",信息为:{} ", requestLog);
            // result的值就是被拦截方法的返回值
            long startTime = System.currentTimeMillis();

            handleLog(joinPoint,null,sb.toString());

            //执行成功后
            result = joinPoint.proceed();
            long endTime = System.currentTimeMillis();
            String resultStr = result == null ? "" : result.toString();
            logger.info("接口返回--[{}]", resultStr);
            logger.info("接口耗时--[{}]", endTime - startTime + "ms");

 
            handleLog(joinPoint,null,resultStr);

            return result;

        } 
        catch (Throwable e) {
            logger.error("接口调用错误,错误原因:[{}]", ExceptionUtils.getMessage(e));
            //这里处理是具体是抛出了什么类型的异常
            if (e instanceof CommonException) {
                throw new CommonException(((CommonException) e).getCode(), e.getMessage(), ((CommonException) e).getErrorMsg());
            }

            if (e instanceof BusinessException) {
                throw new BusinessException(e.getMessage());
            }

            throw new Exception(e);
        }
    }

    <code>
    throw new Exception(e);  更正一下这里,其实这里只需要改成

    throw e  即可解决类型判断的问题
    </code>

    ```java
    catch (Throwable e) {
            logger.error("接口调用错误,错误原因:[{}]", ExceptionUtils.getMessage(e));
            //这里处理是具体是抛出了什么类型的异常
            // 由于最终 throw e,所以这里不再需要判断了
            // if (e instanceof CommonException) {
            //     throw new CommonException(((CommonException) e).getCode(), e.getMessage(), ((CommonException) e).getErrorMsg());
            // }

            // if (e instanceof BusinessException) {
            //     throw new BusinessException(e.getMessage());
            // }

            //这里不要包装
            // throw new Exception(e);

            // 直接throw原来的出去就可以
            throw e
        }
    ```


    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // 获得注解
            Log controllerLog = getAnnotationLog(joinPoint);
            if (controllerLog == null) {
                return;
            }

            // 获取当前的用户
            SysUser currentUser = ShiroUtils.getSysUser();

            // *========数据库日志=========*//
            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = ShiroUtils.getIp();
            operLog.setOperIp(ip);
            // 返回参数
            operLog.setJsonResult(JSON.marshal(jsonResult));

            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
            if (currentUser != null) {
                operLog.setOperName(currentUser.getLoginName());
                if (StringUtils.isNotNull(currentUser.getDept())
                        && StringUtils.isNotEmpty(currentUser.getDept().getDeptName())) {
                    operLog.setDeptName(currentUser.getDept().getDeptName());
                }
            }

            if (e != null) {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(controllerLog, operLog);
            // 保存数据库
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        } catch (Exception exp) {
            // 记录本地异常日志
            logger.error("==前置通知异常==");
            logger.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(Log log, SysOperLog operLog) throws Exception {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(operLog);
        }
    }

    /**
     * 获取请求的参数,放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(SysOperLog operLog) throws Exception {
        Map<String, String[]> map = ServletUtils.getRequest().getParameterMap();
        String params = JSON.marshal(map);
        operLog.setOperParam(StringUtils.substring(params, 0, 2000));
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(Log.class);
        }
        return null;
    }

    private String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 这里要注意的是,对于有一些回调的逻辑,会出错,所以要catch
     *
     * @param paramType args为1  parameter为2
     * @param point     注入点
     * @param map       其它信息
     * @return
     */
    private String tryToGetArgsString(int paramType, ProceedingJoinPoint point, Map map) {
        try {
            if (paramType == 1) {
                if (point.getArgs() != null && point.getArgs().length > 0) {
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < point.getArgs().length; i++) {
                        sb.append(point.getArgs()[i]).append("  \n");
                    }

                    return sb.toString();
                }
            }
            if (paramType == 2) {
                return com.alibaba.fastjson.JSON.toJSONString(map);
            }

            return "";

        } catch (Exception e) {
            return e.toString();
        }
    }
}

注意看上面的:doAround方法中,如果在抛出异常的地方,被这个AOP拦截了,在catch中又没有对Throwable e的类型进行分别处理,那么其实最终抛出的异常就会变成throw new Exception(e); 这时候就变成了默认的Exception了,全局的异常就自然没办法捕获到自定义抛出的异常类型了

2. 抛出异常的顺序不对

在实际的业务中,首先应该是先抛出符合业务的自定义的异常,最后再抛出通用异常