跳至主要內容

SpringMVC-全局异常处理

holic-x...大约 8 分钟JAVA框架

SpringMVC-全局异常处理

学习核心

学习资料

全局异常处理

1.异常处理核心

​ 在使用SpringMVC开发的项目中,每个Controller层里边的方法都需要进行异常捕获以及处理,这种方式太繁琐且效率低,而且大部分异常是不能够直接向外抛出,需要一个统一的错误说法,因此如果能够全局捕获异常统一进行异常处理,将会是一个好的解决方案

image-20240611101323230

SpringMVC提供了两种全局异常捕获和处理的实现方式

  • 方式1:自定义类并实现 HandlerExceptionResolver 接口 并实现 resolveException 方法进行处理全局异常
  • 方式2:通过SpringMVC提供的特定注解@ControllerAdvice + @ExceptionHandler)方式来进行处理全局异常

2.异常处理案例(springboot项目)

全局异常处理方式1:自定义异常解析器

构建思路

  • 自定义类并实现HandlerExceptionResolver接口处理全局异常,通过**@Compontent**将其注册
  • 如果异常跳转返回的是页面,则需要配置相应的ViewResolver

案例构建

​ 此处构建springboot-exception-demo,用于测试异常处理案例,需要在pom.xml中引入web模块

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

​ controller层构建,此处设定多种异常处理的场景,验证不同方式下的异常处理策略

@RestController
@RequestMapping("/hello")
public class HelloController {

    // 1.方法内部出现异常
    @GetMapping("/1")
    public String one(){
        System.out.println("进入方法1测试....");
        // 模拟异常:抛出除数不能为0的异常
        int i = 10/0;
        return "success";
    }

    // 2.controller向上继续抛出异常
    @GetMapping("/2")
    public String two() throws Exception {
        System.out.println("进入方法2测试....");
        throw  new Exception("controller 手动抛出异常");
    }

    // 3.模拟没进入方法前就出现异常的场景(例如请求异常等情况)
    @GetMapping("/3")
    public String three(@RequestParam String name) {
        System.out.println("进入方法3测试....");
        return "success:" + name;
    }

}

自定义异常类实现:实现HandlerExceptionResolver

// 自定义异常
@Component
public class GlobalException implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生异常的处理器:" + handler + "具体异常信息:" + ex.getMessage());
        // 1.构建一个ModelAndView,用于最终返回
        ModelAndView modelAndView = new ModelAndView();

        // 2.构建一个错误页面(需加入一个视图解析器)
        modelAndView.setViewName("error/error");

        // 返回结果(必须返回一个ModelAndView对象,否则SpringMVC在处理的时候会报错)
        return modelAndView;
    }
}

​ 配置视图解析器和错误页面

# pom.xml 引入视图解析器thymeleaf
<!-- 引入视图解析器 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

​ 在resources/templates/error下创建error.html,用做异常返回的

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>错误页面</title>
    <h1>抱歉,系统出现异常了!!!!</h1>
</head>
<body>
</body>
</html>

异常处理分析

​ 在上述的案例中,构建了一个错误页面用作异常捕获后返回,相应要配置对应的视图解析器让其正常解析内容(类似Spring中配置errorPage的概念,此处使用thymeleaf来处理),否则访问的时候就会提示错误(可以通过设定断点来确认),下述为异常页面没有配置时访问的错误提示

image-20240611105252957

​ 下述为正常配置视图解析器并返回渲染的html页面

image-20240611110041929

接口测试

​ 分别访问接口测试不同异常发生的场景下这种配置方式的异常处理:

  • http://localhost:8080/hello/1
  • http://localhost:8080/hello/2
  • http://localhost:8080/hello/3

image-20240611110410220

​ 结合上述测试结果分析,当在进入方法前触发异常的时候,这种全局异常配置的方式无法捕获并处理这种异常情况,而针对进入方法后发生异常、controller手动向上抛出异常都能够被这种方式定义的全局异常处理捕获并处理

  • 接口1测试控制台输出:
进入方法1测试....
发生异常的处理器:com.noob.framework.controller.HelloController#one()具体异常信息:/ by zero
  • 接口2测试控制台输出:
进入方法2测试....
发生异常的处理器:com.noob.framework.controller.HelloController#two()具体异常信息:controller 手动抛出异常
  • 接口3测试控制台输出:
[nio-8080-exec-6] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'name' for method parameter type String is not present]

​ 也可以在自定义异常解析器时输出JSON格式的文本信息

@Component
public class GlobalException implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生异常的处理器:" + handler + "具体异常信息:" + ex.getMessage());
        // 1.构建一个ModelAndView,用于最终返回
        ModelAndView modelAndView = new ModelAndView();

        // 2.构建一个错误页面(需加入一个视图解析器)
        modelAndView.setViewName("error/error");

        // 3.返回一个错误的JSON格式文件信息
        response.setContentType("text/html;charset=UTF-8");
        try {
            response.getWriter().write("{\n" +
                    "    \"code\":\"-1\",\n" +
                    "    \"msg\":\"服务器出现异常,请联系管理员处理...\",\n" +
                    "    \"data\":null\n" +
                    "}");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 返回结果(必须返回一个ModelAndView对象,否则SpringMVC在处理的时候会报错)
        return modelAndView;
    }
}

image-20240611111752956

全局异常处理方式2:使用特定注解

构建思路

  • 创建全局异常处理类,使用特定注解@ControllerAdvice + @ExceptionHandler

构建参考

// 自定义异常
@ControllerAdvice
public class GlobalException2 {

    // 限定对何种异常进行处理,以及返回的格式
    @ExceptionHandler(value = ArithmeticException.class)
    public ModelAndView handler1(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生异常的处理器:" + handler + "具体异常信息:" + ex.getMessage());
        // 1.构建一个ModelAndView,用于最终返回
        ModelAndView modelAndView = new ModelAndView();

        // 2.构建一个错误页面(需加入一个视图解析器)
        modelAndView.setViewName("error/error");

        // 返回结果(必须返回一个ModelAndView对象,否则SpringMVC在处理的时候会报错)
        return modelAndView;
    }

    /**
     * @ExceptionHandler:限定对何种异常进行处理
     * @ResponseBody:处理返回的格式(SpringMVC会响应一个json格式信息)
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public String handler2(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生异常的处理器:" + handler + "具体异常信息:" + ex.getMessage());
        // 返回结果
        return "{\n" +
                "    \"code\":\"-1\",\n" +
                "    \"msg\":\"服务器出现异常,请联系管理员处理...\",\n" +
                "    \"data\":null\n" +
                "}";
    }
}

测试

  • http://localhost:8080/hello/1

  • http://localhost:8080/hello/2

  • http://localhost:8080/hello/3

image-20240611113009173

​ 从上述结果分析,所有异常被正常捕获且处理。结合代码分析:

  • 访问接口1:进入方法,抛出数字运算异常,此时匹配的是handler1,因此返回的是一个页面
  • 访问接口2:进入方法,手动抛出异常,被handler2匹配,返回的是一个json格式数据
  • 访问接口3:在进入方法前已经出现异常,被handler2匹配,返回的是一个json格式数据

注意事项

  • 如果设定了多个handler,需注意其捕获异常。针对同一类异常不可以设定多个handler处理(即不同的方法监听相同的异常类型会报错)
  • 如果匹配到了具体的异常,则会执行对应的handler,不会再往下执行(例如案例中的handler1限定处理算数异常,当访问接口1就只会调用handler1)

参数说明(方法签名和返回值)

  • 使用ExceptionHandler注解的异常处理方法可以使用很灵活的方法签名。可以使用以下类型的参数,参数可以以任意顺序传递。

    • 一个异常参数:声明一个一般性的异常或者更加具体的异常

    • Request 和/或 response 对象(Servlet API 或 Portlet API):可以选择一个特定 request/response的类型,比如ServletRequest / HttpServletRequest 或 PortletRequest / ActionRequest / RenderRequest

    • Session 对象 (Servlet API 或Portlet API)。可以是 HttpSession ,也可以是PortletSession。这种类型的参数强制要求存在一个会话。因此,这个参数不能为null。请注意,会话访问可能是非线程安全的,特别是在一个Servlet环境中。如果多个请求可以访问一个会话,请考虑将synchronizeOnSession标志修改为 "true";

    • WebRequest 或 NativeWebRequest

    • Locale

    • InputStream / Reader 访问请求内容

    • OutputStream / Writer 生成响应内容

    • Model

  • 异常处理方法支持的返回值类型:

    • ModelAndView 对象 (Servlet MVC or Portlet MVC)
    • Model 对象: 通过RequestToViewNameTranslator 隐式确定的一个视图名称
    • Map 对象: 通过RequestToViewNameTranslator 隐式确定的一个视图名称
    • View 对象
    • 被解析成一个视图名称的String
    • @ResponseBody 注解的方法 (仅限Servlet) 设置响应内容
    • HttpEntity<?> 或 ResponseEntity<?> (仅限Servlet) 设置响应头和响应内容
    • void。方法自己处理了响应(通过直接写响应内容,需要声明一个属于ServletResponse / HttpServletResponse / RenderResponse 类型的参数)或者通过RequestToViewNameTranslator 隐式确定的一个视图名称(没有在方法签名中声明任何响应参数,仅适用于Servlet环境)

    在Servlet环境中,可以将@ResponseStatus 与ExceptionHandler结合使用,来定义HTTP响应的响应状态

综合对比

结合上述两种全局异常的处理方式,可以看到方式2比方式1更加灵活

  • 方式2可以适配不同场景下的异常处理需求(返回不同类型的数据)
  • 方式2可以根据异常类型来匹配自定义不同的handler(对异常的处理精度更细)
  • 当方式1和方式2并存的时候,方式2的级别更高
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3