如何统一处理controller层的异常并返回

时间:2024-05-20 07:02:35

这几天在跟同事调接口过程中,遇到经常报道查看对方异常堆栈的错误信息。很明显是异常没有进行处理。一般我们会用标准的try-catch来捕获异常。
但是这样地处理只适合那种主动提示的检查时异常,因为你不用的try-catch就过不了编译检查,所以你能主动地抓获异常并进行处理。但是,如果存在运行时异常,你没有来得及想到去处理它的时候会发生什么,我们可以来先看看下面的这个没有处理运行时异常的例子

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

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

HTTP://本地主机:8080 / zxtest / getNullPointerException

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

如何统一处理controller层的异常并返回

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

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

PS:上面的项目如果使用SpringBoot的话可能在前端得不到报错信息,因为SpringBoot自动对返回的报错内容做了处理,我们需要使用的Maven的网站模板创建一个只包含用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;
    }
}

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

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

1,使用HandlerExceptionResolver

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

基本使用

  • 方式一:使用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的来统一返回想返回的信息如下是请求相同的链接时返回给浏览器的内容:

如何统一处理controller层的异常并返回

 

  • 方式二:使用的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,使用@ExceptionHandler

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

基本使用 

新建一个控制器如下:

@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注解,表示它是一个增强版的控制器,然后在里面创建了一个返回的ModelAndView对象的exceptionHandler的方法,上其加上@ExceptionHandler注解,表示这是一个异常处理方法,然后在方法里面写上具体的异常处理及返回参数逻辑即可,如此就完成了所有的工作,真的是太方便了。

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

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

具体异常的处理

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

我们改变下控制器层的代码如下:

@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异常");
    }
}

已知NullPointerException状语从句:ClassCastException都继承RuntimeException,而RuntimeException状语从句:IOException都继承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;
    }
}

那么

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

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

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

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