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

简介: 概述异常处理是几乎所有编程语言都具有的特性,主要是处理程序运行时的非预期行为,保证程序的健壮性。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 处理异常。


目录
相关文章
|
9月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1371 3
|
8月前
|
SQL Java 数据库连接
Spring Data JPA 技术深度解析与应用指南
本文档全面介绍 Spring Data JPA 的核心概念、技术原理和实际应用。作为 Spring 生态系统中数据访问层的关键组件,Spring Data JPA 极大简化了 Java 持久层开发。本文将深入探讨其架构设计、核心接口、查询派生机制、事务管理以及与 Spring 框架的集成方式,并通过实际示例展示如何高效地使用这一技术。本文档约1500字,适合有一定 Spring 和 JPA 基础的开发者阅读。
799 0
|
7月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
7月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
651 8
|
9月前
|
Java 应用服务中间件 开发者
Spring Boot 技术详解与应用实践
本文档旨在全面介绍 Spring Boot 这一广泛应用于现代企业级应用开发的框架。内容将涵盖 Spring Boot 的核心概念、核心特性、项目自动生成与结构解析、基础功能实现(如 RESTful API、数据访问)、配置管理以及最终的构建与部署。通过本文档,读者将能够理解 Spring Boot 如何简化 Spring 应用的初始搭建和开发过程,并掌握其基本使用方法。
675 2
|
9月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
9月前
|
安全 算法 Java
在Spring Boot中应用Jasypt以加密配置信息。
通过以上步骤,可以在Spring Boot应用中有效地利用Jasypt对配置信息进行加密,这样即使配置文件被泄露,其中的敏感信息也不会直接暴露给攻击者。这是一种在不牺牲操作复杂度的情况下提升应用安全性的简便方法。
1484 10
|
10月前
|
NoSQL Java Redis
Redis基本数据类型及Spring Data Redis应用
Redis 是开源高性能键值对数据库,支持 String、Hash、List、Set、Sorted Set 等数据结构,适用于缓存、消息队列、排行榜等场景。具备高性能、原子操作及丰富功能,是分布式系统核心组件。
771 2
|
10月前
|
安全 Java Nacos
0代码改动实现Spring应用数据库帐密自动轮转
Nacos作为国内被广泛使用的配置中心,已经成为应用侧的基础设施产品,近年来安全问题被更多关注,这是中国国内软件行业逐渐迈向成熟的标志,也是必经之路,Nacos提供配置加密存储-运行时轮转的核心安全能力,将在应用安全领域承担更多职责。
|
10月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
725 0