Controller层的异常统一处理及返回

Controller层的异常统一处理及返回

一、为什么要做这件事?

不知道你平时在写Controller层接口的时候,有没有注意过抛出异常该怎么处理,是否第一反应是想着用个try-catch来捕获异常?

但是这样地处理只适合那种编译器主动提示的检查时异常,因为你不用try-catch就过不了编译检查,所以你能主动地抓获异常并进行处理。但是,如果存在运行时异常且你没有来得及想到去处理它的时候会发生什么呢?我们可以来先看看下面的这个没有处理运行时异常的例子:

@RestController
public class ExceptionRest {
    @GetMapping("getNullPointerException")
    public Map<String,Object> getNullPointerException(){
        throw new NullPointerException("出现了空指针异常");
    }
}

以上代码在基于maven的SpringMVC项目中,使用tomcat启动后,浏览器端发起如下请求:

http://localhost:8080/zxtest/getNullPointerException

访问后得到的结果是这样的:

浏览器收到的报错信息

可以看到,我们在Controller接口层抛出了一个空指针异常,然后没有捕获,结果异常堆栈就会返回给前端浏览器,给用户造成了非常不好的体验。

除此之外,前端从报错信息中能看到后台系统使用的服务器及中间件类型、所采用的框架信息及类信息,甚至如果后端抛出的是SQL异常,那么还可以看到SQL异常的具体查询的参数信息,这是一个中危安全漏洞,是必须要修复的。

PS:上面的项目如果使用SpringBoot的话可能在前端得不到报错信息,因为SpringBoot自动对返回的报错内容做了处理,我们需要使用Maven的web模板创建一个只包含SpringMVC的项目来复现以上场景。

二、如何做到统一处理?

当出现这种运行时异常的时候,我们想到的最简单的方法也许就是给可能会抛出异常的代码加上异常处理,如下所示:

@RestController
public class ExceptionRest {
    private Logger log = LoggerFactory.getLogger(ExceptionRest.class);
    @GetMapping("getNullPointerException")
    public Map<String,Object> getNullPointerException(){
        Map<String,Object> returnMap = new HashMap<String,Object>();
        try{
            throw new NullPointerException("出现了空指针异常");
        }catch(NullPointerException e){
            log.error("出现了空指针异常",e);
            returnMap.put("success",false);
            returnMap.put("mesg","请求发生异常,请稍后再试");
        }
        return returnMap;
    }
}

因为我们手动地在抛出异常的地方加上了处理,并妥善地返回发生异常时该返回给前端的内容,因此,当我们再次在浏览器发起相同的请求时得到就是以下内容:

{
success: false,
mesg: "请求发生异常,请稍后再试"
}

貌似问题得到了解决,但是你能确保你可以在所有可能会发生异常的地方都正好捕获了异常并处理吗?你能确保团队的其他人也这么做?

很明显,你需要一个统一的异常捕获与处理方案。

2.1 使用HandlerExceptionResolver

HandlerExceptionResolver是一个异常处理接口,实现它的类在spring配置文件中注册后就能捕获Controller层抛出的所有异常,我们就是基于此来实现统一Web异常的处理和返回结果的配置。

2.1.1 基本使用

  • 方式一:使用HttpServletResponse返回JSON信息
@Controller
public class WebExceptionResolver implements HandlerExceptionResolver {
    private Logger log = LoggerFactory.getLogger(WebExceptionResolver.class);
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        log.error("请求{}发生异常!",httpServletRequest.getRequestURI());
        ModelAndView mv = new ModelAndView();
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setCharacterEncoding("UTF-8");
        String retStr = "{\"success\":false,\"msg\":\"请求异常,请稍后再试\"}";
        try{
            httpServletResponse.getWriter().write(retStr);
        }catch(Exception ex){
            log.error("WebExceptionResolver处理异常",ex);
        }
        // 需要返回空的ModelAndView以阻止异常继续被其它处理器捕获
        return mv;
    }
}

通过以上的处理,所有Web层的异常都能被WebExceptionResolver捕获并在resolveException中进行处理,然后可以使用HttpServletResponse来统一返回想返回的信息。如下是请求相同的链接时返回给浏览器的内容:

拦截Web异常后的返回信息
  • 方式二:使用ModelAndView返回JSON信息
@Controller
public class WebExceptionResolver implements HandlerExceptionResolver {
    private Logger log = LoggerFactory.getLogger(WebExceptionResolver.class);
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        log.error("请求{}发生异常!",httpServletRequest.getRequestURI());
        ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
        mv.addObject("success","false");
        mv.addObject("mesg","请求异常,请稍后再试");
        return mv;
    }
}

这两种方式效果等同,唯一的区别是,使用第二种方式需要多引入jackson-databind包。

<dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
</dependency>

2.1.2 具体异常的处理

有时候我们需要在发生特定异常的时候做一些处理,那么只需要判断捕获的异常类型进行分别处理即可:

@Controller
public class WebExceptionResolver implements HandlerExceptionResolver {
    private Logger log = LoggerFactory.getLogger(WebExceptionResolver.class);
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        log.error("请求{}发生异常!",httpServletRequest.getRequestURI());
        ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
        mv.addObject("success","false");
        if(e instanceof NullPointerException){
            mv.addObject("mesg","请求发生了空指针异常,请稍后再试");
        }else if(e instanceof ClassCastException){
            mv.addObject("mesg","请求发生了类型转换异常,请稍后再试");
        }else{
            mv.addObject("mesg","请求发生异常,请稍后再试");
        }
        return mv;
    }
}

2.1.3 异常处理链

如果存在多个实现HandlerExceptionResolver的异常处理类,那么它们就会形成一个处理链,此时需要在spring配置文件中声明哪个处理在前,哪个处理在后,越是在前面声明的处理类就越是可以先对异常处理,甚至可以拦截异常,不再被后续的处理器处理。

PS:如果打算在当前的异常处理器中拦截异常,防止继续往外抛出被别的处理器处理,那么直接在最后返回一个空的ModelAndView对象即可。如果打算不拦截这个异常,继续让别的处理器处理的话,就返回null即可。

// 需要返回空的ModelAndView以阻止异常继续被其它处理器捕获
return mv;
// 返回null将不会拦截异常,其它处理器可以继续处理该异常
return null;

2.2 使用@ExceptionHandler

Spring3.2以后,SpringMVC引入了ExceptionHandler的处理方法,使得对异常的处理变得更加简单和精确,你唯一需要做的就是新建一个Controller,然后再里面加上两个注解即可完成Controller层所有异常的捕获与处理。

2.2.1基本使用

新建一个Controller如下:

@ControllerAdvice
public class ExceptionConfigController {
    @ExceptionHandler
    public ModelAndView exceptionHandler(Exception e){
        ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
        mv.addObject("success",false);
        mv.addObject("mesg","请求发生了异常,请稍后再试");
        return mv;
    }
}

我们在如上的代码中,类上加了@ControllerAdvice注解,表示它是一个增强版的controller,然后在里面创建了一个返回ModelAndView对象的exceptionHandler方法,其上加上@ExceptionHandler注解,表示这是一个异常处理方法,然后在方法里面写上具体的异常处理及返回参数逻辑即可,如此就完成了所有的工作,真的是太方便了。

我们在浏览器发起调用后就返回了如下的结果:

{
success: false,
mesg: "请求发生了异常,请稍后再试"
}

2.2 具体异常的处理

相比与HandlerExceptionResolver而言,使用@ExceptionHandler更能灵活地对不同的异常进行分别的处理。并且,当抛出的异常是指定异常的子类,那么照样能够被捕获和处理。

我们改变下controller层的代码如下:

@RestController
public class ExceptionController {

    @GetMapping("getNullPointerException")
    public Map<String, Object> getNullPointerException() {
        throw new NullPointerException("出现了空指针异常");
    }

    @GetMapping("getClassCastException")
    public Map<String, Object> getClassCastException() {
        throw new ClassCastException("出现了类型转换异常");
    }

    @GetMapping("getIOException")
    public Map<String, Object> getIOException() throws IOException {
        throw new IOException("出现了IO异常");
    }
}

已知NullPointerExceptionClassCastException都继承RuntimeException,而RuntimeExceptionIOException都继承Exception

我们在ExceptionConfigController做这样的处理:

@ControllerAdvice
public class ExceptionConfigController {
    // 专门用来捕获和处理Controller层的空指针异常
    @ExceptionHandler(NullPointerException.class)
    public ModelAndView nullPointerExceptionHandler(NullPointerException e){
        ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
        mv.addObject("success",false);
        mv.addObject("mesg","请求发生了空指针异常,请稍后再试");
        return mv;
    }

    // 专门用来捕获和处理Controller层的运行时异常
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView runtimeExceptionHandler(RuntimeException e){
        ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
        mv.addObject("success",false);
        mv.addObject("mesg","请求发生了运行时异常,请稍后再试");
        return mv;
    }

    // 专门用来捕获和处理Controller层的异常
    @ExceptionHandler(Exception.class)
    public ModelAndView exceptionHandler(Exception e){
        ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
        mv.addObject("success",false);
        mv.addObject("mesg","请求发生了异常,请稍后再试");
        return mv;
    }
}

那么

  • 当我们在Controller层抛出NullPointerException时,就会被nullPointerExceptionHandler进行处理,然后拦截。

    {
    success: false,
    mesg: "请求发生了空指针异常,请稍后再试"
    }
    
  • 当我们在Controller层抛出ClassCastException时,就会被runtimeExceptionHandler进行处理,然后拦截。

    {
    success: false,
    mesg: "请求发生了运行时异常,请稍后再试"
    }
    
  • 当我们在Controller层抛出IOException时,就会被exceptionHandler进行处理,然后拦截。

    {
    success: false,
    mesg: "请求发生了异常,请稍后再试"
    }
    

三、总结

SpringMVC为我们提供的Controller层异常处理真的是太方便了,尤其是@ExceptionHandler,推荐大家使用。

本文完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 21,783评论 1 92
  • 最近突然发现,青春里面有部雷剧,五雷轰顶劈晕你,但是还是会手贱,去点开来看,还是会好奇,去追更新,然而每次看完都会...
    大鑫_zou阅读 528评论 2 2
  • 2016.9.3 2016年9月3号,今天一早听美乐家简单介绍,然后到南京生活馆给会员办理入会,直接下订单,特别的...
    施以诺阅读 357评论 0 0
  • 秋风,秋雨,秋天凉。 秋季,秋叶,秋草黄。 秋云,秋月,秋气爽。 秋菊,秋雁,秋收忙。 秋情,秋意,秋缠绵。 秋思...
    宏红阅读 103评论 0 0
  • 【文章摘要】看资料时会分析原作者是如何思考的。想表达什么?为什么选择这种手法来表达?为何有这种想法?有时间会尝试用...
    用户运营笔记阅读 729评论 0 51