如何全局管理异常ExceptionHandler、HandlerExceptionResolver、JoinPoint

首先讲一下使用全局异常的好处,不需要定义很多的返回值,当业务出错的时候直接通过异常的返回值方式来返回给前端或者API调用方错误信息。使用全局异常类定义一个业务异常类,所有的业务异常都需要抛出这一个异常,然后通过不同的状态码来区分具体是哪个异常,和对应的描述信息,状态码和描述信息在全局异常类里面通过枚举去定义。如果类型比较多,可以定义多个枚举来区分不同类型的业务异常。

其次罗列一下分别有几种方式去管理全局异常

1.使用@ExceptionHandler注解配合 @ControllerAdvice注解使用实现异常处理

2.实现HandlerExceptionResolver接口来管理异常

3.使用@Around注解抓取JoinPoint(切面)的proceed()方法来环绕管理方法抛出的异常

说一下第一点和第二点的区别,为啥很多文章都推荐第一种方式,是因为第一种方案可以使用@ResponseBody注解方法对特定异常进行处理),而使用HandlerExceptionResolver的话如果是ajax的请求,出现异常就会很尴尬,ajax并不认识ModelAndView,结论就是第二种方案只适合ModelAndView或者重定向,不支持返回json,这块在最后的列子中有说明,HandlerExceptionResolver接口中resolveException方法返回体是ModelAndView。

public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4);
}

重点再说下第三种方案,一个方法,只有满足指定某@annotation才进入该切面,只有符合execution指定返回体才进入切面

AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法:
1)JoinPoint
** java.lang.Object[] getArgs():获取连接点方法运行时的入参列表; **
** Signature getSignature() :获取连接点的方法签名对象; **
java.lang.Object getTarget() :获取连接点所在的目标对象;
java.lang.Object getThis() :获取代理对象本身;
2)ProceedingJoinPoint
ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法:
java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法;
java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。

@Around("@annotation(org.springframework.web.bind.annotation.ResponseBody) && (execution(public java.util.Map<String,Object> *(..)) || "+"execution(public com.*.RetResult *(..)) 
public Object around(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Class returnType = methodSignature.getReturnType();
        try {
            Object object = joinPoint.proceed();
            return object;
        } catch (ShopException e) {
            e.printStackTrace();
            if (returnType == Map.class) {
                Map<String, Object> map = MapUtils.getMap(RespEnum.CLIENT_ERROR, e.getMessage());
                return map;
            } else {
                return new RetResult(RespEnum.OK.getCode() + "", e.getMessage());
            }
        } catch (Throwable throwable) {
            logger.error("system error:{}", throwable);
            if (returnType == Map.class) {
                Map<String, Object> map = MapUtils.getMap(RespEnum.SERVER_FAIL, "系统异常!");
                return map;
            }  else {
                return new RetResult(RespEnum.SERVER_FAIL.getCode() + "", "系统异常!");
            }
        }
    }

ps:记得之前的一篇文章:https://www.jianshu.com/p/3f05fe61a54f
有提过实现Filter,通过Invoker来获取方法的方法名。参数信息等等,是不是和Joinpoint切点有异曲同工的感觉,
那么我们简单罗列下类似的名词,他们有啥共同点,又有啥区别:

Joinpoint:可以去执行目标方法
Invocation:可以获得参数,他说一种Joinpoint 可以被interceptor 进行intercepted
Advice:我们aop的时候需要真正执行的类
Interceptor:是advice的子接口
MethodInterceptor:是Interceptor子接口,一般我们们最后都是吧advice转换成他

来张美图休息一下,接下来撸代码咯~
  • 管理异常码全局工具类
/**
 * @Title:
 * @Auther: hangyu
 * @Date: 2019/3/28
 * @Description
 * @Version:1.0
 */
public class App {
    private static App INSTANCE = null;
    private final Logger logger = LoggerFactory.getLogger(App.class);
    /**
     * 错误码的properites
     */
    private PropertiesConfiguration errorCode;

    private App() {
        try {
            errorCode = new PropertiesConfiguration();
            errorCode.setEncoding("UTF-8");
            errorCode.setFileName("errorCode.properties");
            errorCode.load();
        } catch (ConfigurationException e) {
            logger.error(e.getMessage());
        }
    }

    public static synchronized App getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new App();
        }
        return INSTANCE;
    }

    /**
     * 获取错误码的文本信息
     *
     * @param code
     * @return
     */
    public String getErrorCode(int code) {
        String key = String.valueOf(code);
        if (errorCode.containsKey(key)) {
            return errorCode.getString(key);
        }
        return "";
    }
}
  • errorCode.properties

1=密码输入错误,您还可以输入{0}次
2=密码被锁住

  • 全局异常返回内容抽象
/**
 * @Title:
 * @Auther: hangyu
 * @Date: 2019/3/28
 * @Description
 * @Version:1.0
 */
public class ResultBean implements Serializable {
    private static final long serialVersionUID = -4365068809657107866L;
    private int code;
    private String desc;
    private Serializable content;
    private Integer totalNo;

    public ResultBean() {
    }

    public ResultBean(int code) {
        this.code = code;
        this.desc = App.getInstance().getErrorCode(code);
    }

    public ResultBean(int code, String param) {
        this.code = code;
        //App.getInstance().getErrorCode(code) 是过去错误码的unicode编码
        this.desc = MessageFormat.format(App.getInstance().getErrorCode(code), param);
    }

    public ResultBean(int code, Serializable content) {
        this(code);
        this.content = content;
    }

    @Override
    public String toString() {
        return "ResultBean{" +
            "code=" + code +
            ", desc='" + desc + '\'' +
            ", content=" + content +
            ", totalNo=" + totalNo +
            '}';
    }
}
  • 自定义异常
/**
 * @Title:
 * @Auther: hangyu
 * @Date: 2019/3/28
 * @Description
 * @Version:1.0
 */
public class ErrorCodeArgException extends Exception {

    private int error;
    private String request;
    private Serializable result;
    private String[] args;

    public ErrorCodeArgException(int error) {
        this.error = error;
    }

    public ErrorCodeArgException(int error, String... params) {
        this.error = error;
        this.args = params;
    }

    public ErrorCodeArgException(int error, String request, String... params) {
        this.error = error;
        this.request = request;
        this.args = params;
    }

    public ErrorCodeArgException(int error, Serializable result, String... params) {
        this.error = error;
        this.result = result;
        this.args = params;
    }

    public ErrorCodeArgException(int error, String request, Serializable result, String... params) {
        this.error = error;
        this.request = request;
        this.result = result;
        this.args = params;
    }

    public int getError() {
        return error;
    }

    public void setError(int error) {
        this.error = error;
    }

    public String getRequest() {
        return request;
    }

    public void setRequest(String request) {
        this.request = request;
    }

    public Serializable getResult() {
        return result;
    }

    public void setResult(Serializable result) {
        this.result = result;
    }

    public String[] getArgs() {
        return args;
    }

    public void setArgs(String[] args) {
        this.args = args;
    }
}
  • ErrorCode枚举
/**
 * @Title:
 * @Auther: hangyu
 * @Date: 2019/3/28
 * @Description
 * @Version:1.0
 */
public enum ErrorCode {

    PASSWORD_ERROR(1, "密码输入错误"),
    PASSWORD_ERROR_LOCKOUT(2, "密码被锁住");


    // 状态码
    private int code;

    // 描述
    private String desc;

    ErrorCode(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

  • 业务请求
/**
 * @Title:
 * @Auther: hangyu
 * @Date: 2019/3/28
 * @Description
 * @Version:1.0
 */
@Controller
@RequestMapping("/test")
public class TestController {

    private final Logger logger = LoggerFactory.getLogger(TestController.class);

    public int MAX_FAIL_PWD_COUNT = 2;

    @RequestMapping("/login")
    @ResponseBody
    public ResultBean login(int count) throws ErrorCodeArgException{
        ResultBean resultBean = new ResultBean();
        if (count > 2) {
            // 账户密码错误3次,锁定账户
                throw new ErrorCodeArgException(ErrorCode.PASSWORD_ERROR_LOCKOUT.getCode());
        }

        if (count <= 2 ){
            // 密码输入错误,您还可以输入{0}次
                throw new ErrorCodeArgException(ErrorCode.PASSWORD_ERROR.getCode(),
                        new String[] { String.valueOf(MAX_FAIL_PWD_COUNT - count) });
        }

        return  resultBean;

    }
}
  • 全局异常管理
/**
 * @Title:
 * @Auther: hangyu
 * @Date: 2019/3/28
 * @Description
 * @Version:1.0
 */
@ControllerAdvice
public class BaseControllerAdvice {

    private final Logger logger = LoggerFactory.getLogger(BaseControllerAdvice.class);

    @ExceptionHandler()
    @ResponseBody
    private ResultBean handleException(Exception e, HttpServletRequest request) {
        ResultBean bean = null;

        ErrorCodeArgException e1 = (ErrorCodeArgException)e;
        bean = new ResultBean(e1.getError(), e1.getArgs()[0]);

        logger.error("response is {}",bean.toString());
        return bean;
    }

}
上面是方案1实现,下面开始方案2列子
/**
 * @Title:
 * @Description:
 * @Author:hangyu
 * @Since:2019/3/29
 * @Version:1.0
 */
public class SimpleMappingExceptionResolver implements HandlerExceptionResolver {

    private final static Logger LOGGER =
        LoggerFactory.getLogger(SimpleMappingExceptionResolver.class);

    /**
     * <!-- 框架异常处理Handler -->
     * <bean id="exceptionResolver" class="cn.*.exception.SimpleMappingExceptionResolver"/>
     *
     * @param request
     * @param response
     * @param o
     * @param exception
     * @return
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception exception) {
        // 判断是否ajax请求
        if (!(request.getHeader("accept").contains("application/json")
            || (request.getHeader("X-Requested-With") != null
            && request.getHeader("X-Requested-With").contains("XMLHttpRequest")))) {
            // 如果不是ajax,JSP格式返回
            // 为安全起见,只有业务异常对前端可见,否则否则统一归为系统异常
            Map<String, Object> map = new HashMap<String, Object>();
            if (exception instanceof ErrorCodeArgException) {
                map.put("errorMsg", exception.getMessage());
            } else {
                map.put("errorMsg", "系统异常!");
            }
            LOGGER.error("系统运行异常:", exception);
            // 对于非ajax请求,统一跳转到error.jsp页面
            ModelAndView modelAndView = new ModelAndView("outException");
            modelAndView.addObject("errorInfo", map);
            return modelAndView;
        } else {
            // 如果是ajax请求,JSON格式返回
            try {
                response.setContentType("application/json;charset=UTF-8");
                PrintWriter writer = response.getWriter();
                // 为安全起见,只有业务异常对前端可见,否则统一归为系统异常
                String responseMsg = null;
                if (exception instanceof ErrorCodeArgException) {
                    responseMsg = exception.getMessage();
                }
                writer.write(responseMsg);
                writer.flush();
                writer.close();
            } catch (IOException e) {
                LOGGER.error("系统运行异常:", e);
            }
            return null;
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352