其实 web 服务器是会处理错误的
在 Web.xml 还是随处可见的年代时(确实有点老黄历了), 下面的这些配置应该都不陌生.
根据错误代码处理错误, 如下图 01:
根据异常类型处理错误, 如下图 02:
不过我们更加熟悉的应该是 SpringMVC 的统一异常处理. 如下图 03:
看到 @ControllerAdvice 注解和 @ExceptionHandler 注解都应该很熟悉吧.
处理原理就是在捕获到业务 Controller 有异常抛出时, 根据异常类型来这里找到对应的方法并执行.
可见, 整个异常的处理过程都是在 SpringMVC 内部给搞定了, 根本就没有涉及到 Web 服务器, 如 tomcat.
那么问题来了, 明明 Web 服务器可以处理异常, 为啥 SpringMVC 还要自己处理呢?
是不想呢, 还是另有苦衷
读过本系列上一篇文章的小伙伴应该能猜出来一些. 因为 SpringMVC 开发的工程最终打成 war 包, 然后扔到 tomcat 下面即可.
而且 SpringMVC 和 tomcat 之间是通过 Java Web 的规范联系起来的, 它们之间根本没有办法自由交互的.
然而 tomcat 的错误处理需要在 Web.xml 里面配置. 严格来说, Web.xml 其实和 SpringMVC 关系不大.
特别是 Spring 全面进入 Java 和注解配置时代以后, Web.xml 逐渐被弱化, 继而变得可有可无, 直到最终完全消失.
所以(我猜测)SpringMVC 可能不希望自己的用户到一个和自己关系不大的 Web.xml 里面配置一些和业务相关的异常处理映射.
所以只好自己把异常处理消化掉. 不劳 tomcat 大驾. 因此才有了 SpringMVC 统一处理异常.
重新让 Web 服务器来处理错误
当历史来到了 SpringBoot 的时代, SpringBoot 翻身做主成了入口, Web 服务器竟然成了一个组件.
SpringBoot 可以操作 Web 服务器的 API, 通过编程的方式, 对 Web 服务器进行深度配置.
所以很多事情都变得容易起来, 比如错误处理.
因为 Web.xml 里的错误处理映射最终是注册到 tomcat 里面了, 所以 SpringBoot 只要操作 tomcat 的 API, 使用编程的方式也来注册一些错误处理映射不就可以了嘛.
因为用户直接是和 SpringBoot 打交道的, 所以 SpringBoot 需要抽象出一套错误处理注册机制, 让用户来注册.
这样 SpringBoot 拿到用户注册的错误处理映射信息后, 在生成 Web 服务器 (如 tomcat) 时, 把这些映射信息添加到 Web 服务器中即可.
SpringBoot 注册错误处理映射的方案
先来看看错误处理映射是如何描述的, 如下图 04:
三个字段的含义是:
path 是一个路径, 它表示的是错误处理的 url.
status 是一个状态码, 如 404,500 等.
exception 是一个异常类型, 如 LoginFailedException.
一共有三种使用方式:
1)status + path, 如 404 + /404, 表示如果遇到了 404, 就去执行 / 404 这个 url.
2)exception + path, 如 LoginFailedException + /loginfailed, 表示如果遇到登陆失败异常, 就去执行后面这个 url
3)没有 status 和 exception, 只有 path, 这相当于通配符, 匹配所有异常情况.
接下来就该考虑如何注册了, 照例给个接口就行了, 如下图 0506:
Spring 是以 bean 打天下的, 所以 SpringBoot 给的方法当然也是和 bean 相关.
只要向容器中注册一个该接口类型的 bean 即可, 如下图 07:
这些信息会被收集到并存好, 如下图 08:
然后在创建 Web 服务器时添加进去就行了, 以 tomcat 为例, 如下图 09:
可以看到最终转换为 tomcat 的错误映射, 如下图 10:
这里的 setLocation,setErrorCode,setExceptionType 三个 setter 方法, 就对应于 Web.xml 里的 < location>,<error-code>,<exception-type > 这三个标签.
至此, 错误处理已经被注册好了.
SpringBoot 仍需协助处理错误
有一点需要明白, SpringMVC 交给 tomcat 的只是错误处理映射的匹配工作, 但有些真正的错误处理还是要自己做的.
所以整个过程是这样的, SpringMVC 随意抛出异常, 这个异常会被抛到 tomcat 里面, tomcat 获取异常类型并根据注册的错误处理映射关系找到一个 url, 然后调用这个 url.
那么请问这个 url 指向哪里呢? 很大概率又回到了 SpringMVC 里面了, 是不是很有意思, 哈哈.
tomcat 就像一面镜子, SpringMVC 向它发射了一束光线, 经过反射后又回来了. 只不过发射的是一个异常, 回来的是一个 url.
这个 url 对应的是能够处理这个错误的一个 Controller 的方法. 这样执行这个 Controller 方法就等于处理了异常.
这个 Controller 是一个能够处理错误的 Controller, 所以就叫 ErrorController. 如下图 11:
其实它主要是一个 Marker 接口, 也就起一个标志作用.
下面是真正用于处理错误的 Controller, 实现了刚刚的标志接口, 如下图 12:
@RequestMapping 方法就是处理错误的, 既可以返回 JSON, 也可以返回视图页面.
我们可以自己写一个错误处理类, 然后继承这个类, 添加自己的错误处理方法, 最后使用 @Controller 注解重新注册即可.
可以把一个异常映射成和它名称一样的 url 路径, 如把 Exception 映射为 / Exception, 如下图 13:
这两个方法, 一个返回 html 页面, 一个返回一个 JSON.
备注: 异常类型和它映射的 url 之间的关系, 可以按自己的需求去规划, 统一规则即可.
处理错误时, 自然要获得错误相关信息才行, 这个接口可以满足, 如下图 14:
可以获得错误属性和抛出的异常对象.
可以看到接口的两个方法都有 WebRequest 这个参数, 说明错误信息是从 request 中获取出来的.
同时也说明有些错误信息是有人专门放入 request 中的, 是 SpringBoot 放的, 还是 Web 服务器放的?
其实都有, 比如异常对象是 SpringBoot 放的, 响应状态码是 Web 服务器 (按 Java Web 规范) 放的.
这也说明 Web 服务器执行错误处理的 url 时用的是转发 (forward) 而非重定向(redirect), 因为要保留 request 中的信息.
还剩最后一个, 错误视图解析器, 如下图 15:
SpringBoot 自己提供了默认的视图解析器实现, 默认去 classpath 下面的 error 目录下寻找. HTML 视图页面.
如下图 16:
支持状态码到页面的映射, 如 404 默认映射为 / error/404.HTML,500 默认映射为 / error/500.HTML.
如果没有这么具体的页面, 还支持系列映射, 如 404 映射为 / error/4xx.HTML,500 映射为 5xx.HTML.
当然, 默认的一般都无法满足需求, 我们可以继承这个默认的类, 然后重写视图解析方法.
最后把这个类注册为 bean 即可, 这样 SpringBoot 就会使用我们的类了. 如下图 17:
本文总结, 重要
1)用户使用 SpringBoot 提供的错误处理映射机制注册状态码和异常的映射 url 信息.
2)这些映射信息最终会被注册进 Web 服务器中, 如 tomcat.
3)SpringBoot 把异常抛给 Web 服务器, Web 服务器根据异常找到对应的 url, 并执行它.
4)流程再次回到 SpringBoot 中, 进入错误处理 Controller 的方法中, 执行错误处理方法.
5)如果需要解析视图的, 使用错误视图解析器进行视图解析. 否则就是直接返回 JSON.
其中用户能参与的就三步, 注册异常映射, 扩展错误处理 Controller, 扩展错误视图解析器.
具体参与方式文章中都有, 无非就是实现接口或继承某个类, 然后注册为 bean 即可.
>>> 玩转 SpringBoot 系列文章 <<<[玩转 SpringBoot] 配置文件 YAML 的正确打开姿势
[玩转 SpringBoot] 用好条件相关注解, 开启自动配置之门
[玩转 SpringBoot] 给自动配置来个整体大揭秘
[玩转 SpringBoot] 看似复杂的 Environment 其实很简单
[玩转 SpringBoot] 翻身做主人, 一统 Web 服务器
>>> 品 Spring 系列文章 <<<品 Spring: 帝国的基石
品 Spring:bean 定义上梁山
品 Spring: 实现 bean 定义时采用的 "先进生产力"
品 Spring: 注解终于 "成功上位"
品 Spring: 能工巧匠们对注解的 "加持"
品 Spring:SpringBoot 和 Spring 到底有没有本质的不同?
品 Spring: 负责 bean 定义注册的两个 "排头兵"
品 Spring:SpringBoot 轻松取胜 bean 定义注册的 "第一阶段"
品 Spring:SpringBoot 发起 bean 定义注册的 "二次攻坚战"
品 Spring: 注解之王 @Configuration 和它的一众 "小弟们"
品 Spring:bean 工厂后处理器的调用规则
品 Spring: 详细解说 bean 后处理器
品 Spring: 对 @PostConstruct 和 @PreDestroy 注解的处理方法
品 Spring: 对 @Resource 注解的处理方法
品 Spring: 对 @Autowired 和 @Value 注解的处理方法
品 Spring: 真没想到, 三十步才能完成一个 bean 实例的创建
品 Spring: 关于 @Scheduled 定时任务的思考与探索, 结果尴尬了
>>> 热门文章集锦 <<<
毕业 10 年, 我有话说
[面试] 我是如何面试别人 List 相关知识的, 深度有点长文
我是如何在毕业不久只用 1 年就升为开发组长的
爸爸又给 Spring MVC 生了个弟弟叫 Spring WebFlux
[面试] 我是如何在面试别人 Spring 事务时 "套路" 对方的
[面试] Spring 事务面试考点吐血整理(建议珍藏)
[面试] 我是如何在面试别人 Redis 相关知识时 "软怼" 他的
[面试] 吃透了这些 Redis 知识点, 面试官一定觉得你很 NB(干货 | 建议珍藏)
[面试] 如果你这样回答 "什么是线程安全", 面试官都会对你刮目相看(建议珍藏)
[面试] 迄今为止把同步 / 异步 / 阻塞 / 非阻塞 / BIO/NIO/AIO 讲的这么清楚的好文章(快快珍藏)
[面试] 一篇文章帮你彻底搞清楚 "I/O 多路复用" 和 "异步 I/O" 的前世今生(深度好文, 建议珍藏)
[面试] 如果把线程当作一个人来对待, 所有问题都瞬间明白了
Java 多线程通关 --- 基础知识挑战
品 Spring: 帝国的基石
作者是工作超过 10 年的码农, 现在任架构师. 喜欢研究技术, 崇尚简单快乐. 追求以通俗易懂的语言解说技术, 希望所有的读者都能看懂并记住. 下面是公众号的二维码, 欢迎关注!
来源: https://www.cnblogs.com/lixinjie/p/playing-springboot-006.html