【Spring MVC 系列】异常处理,为你的应用加层防护

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 概述异常处理是几乎所有编程语言都具有的特性,主要是处理程序运行时的非预期行为,保证程序的健壮性。JVM 运行时如果遇到未经处理的异常线程将意外退出,为了避免这种情况需要为线程设置默认的异常处理器。

概述


异常处理是几乎所有编程语言都具有的特性,主要是处理程序运行时的非预期行为,保证程序的健壮性。JVM 运行时如果遇到未经处理的异常线程将意外退出,为了避免这种情况需要为线程设置默认的异常处理器。


为了将异常处理与 Web 环境整合到一起,Servlet 规范也定义了一系列异常处理的内容。Spring MVC 在 Servlet 规范的基础上更上一层,结合自身特性又添加了自己全局异常处理的能力。这篇将对 Spring MVC 异常处理详细介绍。


Servlet 规范中的异常处理


Spring MVC 基于 Servlet 规范,因此,在介绍 Spring MVC 异常处理之前先对 Servlet 规范中的异常处理加以介绍。


错误页面配置


为了允许当 Servlet 发生异常时返回自定义的内容作为响应体,Servlet 规范允许在部署描述符文件 web.xml 中配置错误页面。


web.xml 错误页面配置格式如下。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
          http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <error-page>
        <error-code>500</error-code>
        <location>/error/500</location>
        <exception-type>java.lang.Exception</exception-type>
    </error-page>
    <error-page>
        <error-code>404</error-code>
        <location>/error/404</location>
    </error-page>
    <error-page>
        <location>/error</location>
    </error-page>
</web-app>


所有错误页面配置都放到 error-page 标签下,各子标签的含义如下:


location:表示发生异常时请求转发的地址,可以是静态资源 html 或 jsp,也可以是 Servlet 处理的 url,为必填项。

exception-type:异常类型,当 Servlet 抛出的异常匹配该项时才转发请求到location,非必填项。

error-code:HTTP 响应状态码,同样是非必填项。

请求转发到错误页面指定的地址有两种情况。


第一种情况是 Servlet 抛出异常,容器会根据异常类型查找错误页面,如果找不到将会使用仅配置了 location 的错误页面作为默认错误页面。

第二种情况是用户调用了方法 HttpServletResponse#sendError(int sc) ,容器根据这个方法指定的错误码查找错误页面,error-code 就是用来支持这项特性的。


请求属性设置


容器除了在 Servlet 发生异常时将请求转发到错误页,还会将异常的相关信息设置到请求的属性上,以便处理异常的 Servlet 获取,具体包括如下。


image.png


这些属性中,javax.servlet.error.exception 在 Servlet 2.3 引入后,javax.servlet.error.exception_type 和 javax.servlet.error.message仅用于保持向后兼容。Spring Boot 中默认的错误页面就使用这些属性。


Servlet 异常处理实战


创建一个仅抛出异常的 Servlet ,并配置到 web.xml,部署到 Tomcat ,项目启动访问后可以看到如下的报错。


15.png

image.png

这里返回了我们自定义的内容,相对来说更为友好。值得注意的是如果发生异常时转发的错误页面由 Servlet 处理,处理异常的 Servlet 发生了异常,容器将忽略错误页面转而返回默认的内容。


Spring MVC 异常解析器


异常解析器作用范围

Thread 默认的异常处理器作用范围为整个 Thread,Servlet 规范中的异常处理作用范围为 Servlet 处理请求过程,而 Spring MVC 中的异常处理作用范围则为 Spring MVC 处理器处理请求的过程,由异常解析器处理异常。


再把 DispatcherServlet 流程图祭出,添加异常处理部分,如下图所示。Spring MVC 的核心就是 DispatcherServlet 流程图,如果你不熟悉,可先移步《5 分钟彻底理解 Spring MVC》。


16.png


上图中右侧矩形内的流程部分就是异常处理的作用范围了,可以看出,Spring MVC 不仅可以处理 handler 产生的异常,还可以处理 interceptor 产生的异常,简化后的流程图如下。


17.png


Spring MVC 中的异常解析器捕获从拦截器预执行到拦截器后执行部分的异常,当发生异常时由异常解析器根据异常产生新的视图页面,然后再进行视图渲染。所以如果过滤器 Filter 产生了异常,这里的异常解析器是无法处理的。


默认异常解析器


异常解析器在 Spring MVC 中使用接口 HandlerExceptionResolver 表示,接口定义如下。


public interface HandlerExceptionResolver {
  @Nullable
  ModelAndView resolveException(
      HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}


默认情况下,Spring MVC 中有三种异常解析器,类图如下。


image.png


有多个异常解析器的情况下,Spring 将按照异常解析器的顺序获取视图,如果未获取到则使用下一个解析器获取。不管是基于 xml 配置的 Spring MVC,还是基于注解的 Spring MVC,默认的异常解析器顺序及作用都如下。


ExceptionHandlerExceptionResolver:这是用于支持 @ExceptionHandler 注解标注的异常处理器方法的异常解析器,Spring 根据异常类型查找异常处理器方法处理异常。

ResponseStatusExceptionResolver:从异常中解析出响应码,然后调用response.sendError 方法处理异常。

DefaultHandlerExceptionResolver:处理异常方式与 ResponseStatusExceptionResolver 类似,但是只能解析 Spring 内部定义的若干固定类型的异常。


自定义异常解析器


默认情况下,Spring MVC 的异常处理只是对 Servlet 规范中的异常处理进行增强,使用response.sendError 发送错误,如果没有对应的错误页面,响应仍将返回错误堆栈信息。


如果你想定义自己的异常解析器,可以直接实现 HandlerExceptionResolver,并将自定义的类注册为 Spring Bean。@EnanbleWebMvc 注解就使用了这种特性,并提供了 WebMvcConfigurer#extendHandlerExceptionResolvers 方法添加用户自定义的异常解析器,将多个异常解析器组合为一个然后注册为 bean。


Spring MVC 异常处理器


在注解大行其道的今天,通常情况下,我们不会直接配置异常解析器,而是使用默认的异常解析器 ExceptionHandlerExceptionResolver,然后通过 @ExceptionHandler 定义自己的异常处理器,这就是我们所熟悉的异常处理方式了。


异常处理器的配置有两种方式,包括局部异常处理和全局异常处理。


局部异常处理


处理器或拦截器发生异常时,Spring 优先在当前处理器中查找符合条件的异常处理器方法,这里的异常处理器是局部的,只支持当前处理器。


异常处理器方法上需要添加 @ExceptionHandler 注解,标注能处理的异常类型。示例代码如下。


@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        throw new RuntimeException("处理器发生异常");
    }
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView handleException(RuntimeException e) {
        ModelAndView modelAndView = new ModelAndView("error");
        modelAndView.addObject("exception", e);
        return modelAndView;
    }
}


全局异常处理


Spring 如果在当前处理器中查找不到符合条件的异常处理器方法,将在 @ControllerAdvice bean 中根据 @ExceptionHandler 查找异常处理器方法。


@ControllerAdvice bean 中的异常处理器方法是全局的,能处理所有的处理器或拦截器产生的异常。示例代码如下。


@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(RuntimeException.class)
    public Map<String, Object> handleException(RuntimeException e) {
        Map<String, Object> result = new HashMap<>();
        result.put("message", e.getMessage());
        return result;
    }
}


全局异常处理是我们使用最多的异常处理方式,利用全局异常处理,可以处理我们自定义的业务异常,还可以结合参数校验,优雅的返回校验错误信息。关于参数校验和全局异常的整合,可以参考《Spring 参数校验最佳实践及原理解析》。


Spring Boot 异常处理


Spring Boot 2.0 版本开始,并没有为异常处理添加新的异常解析器,而是使用了 Servlet 规范中的异常处理,默认将 /error 路径配置为错误页面,并提供了处理异常的 ErrorController,如果你想自定义错误页面逻辑,将自定义的 Controller 实现 ErrorController 即可。


Spring Boot 中 ErrorController 的默认实现是 BasicErrorController,这个 Controller 会将 Servlet 规范中定义的几个错误有关的 request 属性设置到 Model 中,然后以 html 的形式展示。


如果你想修改错误处理页面,可以配置 Spring 的环境变量 server.error.path,最简单的方式是在 application.properties 文件中指定,如 server.error.path=/error。


BasicErrorController 部分代码如下。


@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
  @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections
        .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  }
}


这里返回的视图实现为 ErrorMvcAutoConfiguration.StaticView,由ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration 中配置的 BeanNameViewResolver 解析,感兴趣可自行查阅源码。最后再看下默认异常处理的效果。


image.png

异常处理源码分析


Spring MVC 异常处理的部分位于DispatcherServlet#processDispatchResult,这个方法本用于处理 handler 产生的视图,在异常发生时会优先将异常解析为视图,简单看下代码。


public class DispatcherServlet extends FrameworkServlet {
  private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                     @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
                     @Nullable Exception exception) throws Exception {
    boolean errorView = false;
    if (exception != null) {
      // 异常处理
      if (exception instanceof ModelAndViewDefiningException) {
        logger.debug("ModelAndViewDefiningException encountered", exception);
        mv = ((ModelAndViewDefiningException) exception).getModelAndView();
      } else {
        Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
        mv = processHandlerException(request, response, handler, exception);
        errorView = (mv != null);
      }
    }
    // 视图渲染
    if (mv != null && !mv.wasCleared()) {
      render(mv, request, response);
      if (errorView) {
        WebUtils.clearErrorRequestAttributes(request);
      }
    } 
    ... 省略部分代码
  }
}


异常处理时调用了方法 #processHandlerException,实现如下。


public class DispatcherServlet extends FrameworkServlet {
  @Nullable
  protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
                           @Nullable Object handler, Exception ex) throws Exception {
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
      // 使用异常解析器进行异常处理
      for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
        exMv = resolver.resolveException(request, response, handler, ex);
        if (exMv != null) {
          break;
        }
      }
    }
    if (exMv != null) {
      if (exMv.isEmpty()) {
        request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
        return null;
      }
        ... 省略部分代码
      return exMv;
    }
    throw ex;
  }
}

这里又调用了异常解析器进行异常处理,和我们前面的描述是保持一致的,由于篇幅问题其他地方不再分析,感兴趣可自行查阅。


总结

异常处理的目的是为了增强程序的健壮性,Servlet 规范中定义了错误页面允许 Servlet 处理未捕获的异常,Spring Boot 使用了这个特性,提供了默认的错误页面,Spring MVC 还定义了处理 handler 异常的解析器,并允许用户使用 @ExceptionHandler 处理异常。


目录
相关文章
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
155 1
|
23天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
97 62
|
21天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
21天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
40 2
|
1月前
|
人工智能 开发框架 Java
总计 30 万奖金,Spring AI Alibaba 应用框架挑战赛开赛
Spring AI Alibaba 应用框架挑战赛邀请广大开发者参与开源项目的共建,助力项目快速发展,掌握 AI 应用开发模式。大赛分为《支持 Spring AI Alibaba 应用可视化调试与追踪本地工具》和《基于 Flow 的 AI 编排机制设计与实现》两个赛道,总计 30 万奖金。
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第8天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建并配置 Spring Boot 项目,实现后端 API 和安全配置。接着,使用 Ant Design Pro Vue 脚手架创建前端项目,配置动态路由和菜单,并创建相应的页面组件。最后,通过具体实践心得,分享了版本兼容性、安全性、性能调优等注意事项,帮助读者快速搭建高效且易维护的应用框架。
42 3
|
1月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第7天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建 Spring Boot 项目并配置 Spring Security。接着,实现后端 API 以提供菜单数据。在前端部分,使用 Ant Design Pro Vue 脚手架创建项目,并配置动态路由和菜单。最后,启动前后端服务,实现高效、美观且功能强大的应用框架。
44 2
|
1月前
|
人工智能 缓存 Java
深入解析Spring AI框架:在Java应用中实现智能化交互的关键
【10月更文挑战第12天】Spring AI 是 Spring 框架家族的新成员,旨在满足 Java 应用程序对人工智能集成的需求。它支持自然语言处理、图像识别等多种 AI 技术,并提供与云服务(如 OpenAI、Azure Cognitive Services)及本地模型的无缝集成。通过简单的配置和编码,开发者可轻松实现 AI 功能,同时应对模型切换、数据安全及性能优化等挑战。
112 3
|
29天前
|
存储 Java 数据管理
强大!用 @Audited 注解增强 Spring Boot 应用,打造健壮的数据审计功能
本文深入介绍了如何在Spring Boot应用中使用`@Audited`注解和`spring-data-envers`实现数据审计功能,涵盖从添加依赖、配置实体类到查询审计数据的具体步骤,助力开发人员构建更加透明、合规的应用系统。