Tomcat异常处理机制
声明
源码基于Spring Boot 2.3.12中依赖的Tomcat
异常例子
tomcat中返回错误页面目前主要是以下两种情况。
- 执行
servlet
发生异常 - 程序中主动调用
response.sendError()
方法。
下面先来看看tomcat默认的处理结果
编写以下例子触发第一种情况
@WebServlet("/exception")
public class ExceptionServlet extends HttpServlet {
private static final long serialVersionUID = -4621356333568059989L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
throw new IllegalArgumentException("name");
}
}
执行结果如下图所示
编写下面例子触发第二种情况
@WebServlet("/sendError")
public class SendErrorServlet extends HttpServlet {
private static final long serialVersionUID = -7823675542130292567L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 使用该属性可携带异常信息
req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, new NullPointerException("name must not be null"));
resp.sendError(500, "system error!");
}
}
执行结果如下图所示
如果把代码中req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, 'xxxx')
去掉则是以下结果
可以看到tomcat给我们返回的主要信息便是Message
、Exception
,其中Message
的取值逻辑如下,如果有异常信息,即存在Exception
,则为e.getMessage
,否则便是sendError
方法中指定的值。
注:
以上代码如果在Spring Boot环境中执行,需要在启动类排除掉错误处理的自动配置类,Spring Boot默认使用/error来返回错误页面。
即@SpringBootApplication(exclude = ErrorMvcAutoConfiguration.class)
源码说明
首先要明确一点,sendError
方法并不是在执行时就返回页面给客户端,该方法仅仅只是打了一个错误标记而已。打开源码可以看到以下片段
/**
* org.apache.catalina.connector.Response.java
*/
@Override
public void sendError(int status, String message) throws IOException {
if (isCommitted()) {
throw new IllegalStateException
(sm.getString("coyoteResponse.sendError.ise"));
}
// Ignore any call from an included servlet
if (included) {
return;
}
// 打一个错误标记,真正处理错误时会用到这个标记
setError();
// 设置返回状态码, 同时会清空message
getCoyoteResponse().setStatus(status);
// 设置错误信息
getCoyoteResponse().setMessage(message);
// Clear any data content that has been buffered
resetBuffer();
// Cause the response to be finished (from the application perspective)
setSuspended(true);
}
public boolean setError() {
// 将内部的reponse对象的errorState更新成1
return getCoyoteResponse().setError();
}
public boolean setError() {
return errorState.compareAndSet(0, 1);
}
真正执行错误页面逻辑的类是org.apache.catalina.core.StandardHostValve
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// ...略
// 从request对象中获取异常信息
Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
/*
* 判断是否有错误,errorState == 1
* sendError方法中会调用setError方法,更新errorState为1
*/
if (response.isErrorReportRequired()) {
AtomicBoolean result = new AtomicBoolean(false);
response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
if (result.get()) {
if (t != null) {
// 根据异常处理
throwable(request, response, t);
} else {
// 根据状态码处理
status(request, response);
}
}
}
}
protected void throwable(Request request, Response response,
Throwable throwable) {
Context context = request.getContext();
if (context == null) {
return;
}
// 真实的错误
Throwable realError = throwable;
if (realError instanceof ServletException) {
// 获取cause by
realError = ((ServletException) realError).getRootCause();
if (realError == null) {
realError = throwable;
}
}
// If this is an aborted request from a client just log it and return
if (realError instanceof ClientAbortException ) {
if (log.isDebugEnabled()) {
log.debug
(sm.getString("standardHost.clientAbort",
realError.getCause().getMessage()));
}
return;
}
// 获取该异常对应的错误页面
ErrorPage errorPage = context.findErrorPage(throwable);
if ((errorPage == null) && (realError != throwable)) {
// 没有找到,再根据真实的异常再找一遍
errorPage = context.findErrorPage(realError);
}
if (errorPage != null) {
// 错误页面不为空,设置一系列的属性,
if (response.setErrorReported()) {
response.setAppCommitted(false);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
errorPage.getLocation());
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
DispatcherType.ERROR);
// 错误码, 默认就是500
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
Integer.valueOf(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
// 错误原因
request.setAttribute(RequestDispatcher.ERROR_MESSAGE,
throwable.getMessage());
// 真实的异常信息
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION,
realError);
Wrapper wrapper = request.getWrapper();
if (wrapper != null) {
request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
wrapper.getName());
}
// 请求路径
request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
request.getRequestURI());
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE,
realError.getClass());
// 响应页面内容
if (custom(request, response, errorPage)) {
try {
response.finishResponse();
} catch (IOException e) {
container.getLogger().warn("Exception Processing " + errorPage, e);
}
}
}
} else {
// 根据异常没有找到错误页面,再根据错误码找,将状态码改成500
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
// The response is an error
response.setError();
// 根据状态码处理
status(request, response);
}
}
private void status(Request request, Response response) {
// 获取状态码
int statusCode = response.getStatus();
// Handle a custom error page for this status code
Context context = request.getContext();
if (context == null) {
return;
}
if (!response.isError()) {
return;
}
// 根据状态码获取错误页面
ErrorPage errorPage = context.findErrorPage(statusCode);
if (errorPage == null) {
// 找不到则使用0再找一次,这点很重要, SpringBoot就是注册了一个0的错误页面
errorPage = context.findErrorPage(0);
}
if (errorPage != null && response.isErrorReportRequired()) {
response.setAppCommitted(false);
// 设置状态码
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
Integer.valueOf(statusCode));
// 获取错误信息,sendError方法第二个参数的值(参加该方法说明)
String message = response.getMessage();
if (message == null) {
message = "";
}
// 设置错误信息
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
errorPage.getLocation());
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
DispatcherType.ERROR);
Wrapper wrapper = request.getWrapper();
if (wrapper != null) {
request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
wrapper.getName());
}
// 设置请求路径
request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
request.getRequestURI());
// 响应页面内容
if (custom(request, response, errorPage)) {
response.setErrorReported();
try {
response.finishResponse();
} catch (ClientAbortException e) {
// Ignore
} catch (IOException e) {
container.getLogger().warn("Exception Processing " + errorPage, e);
}
}
}
}
private boolean custom(Request request, Response response,
ErrorPage errorPage) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug("Processing " + errorPage);
}
try {
// Forward control to the specified location
ServletContext servletContext =
request.getContext().getServletContext();
// 根据错误页面的路径获取转发器
RequestDispatcher rd =
servletContext.getRequestDispatcher(errorPage.getLocation());
if (rd == null) {
container.getLogger().error(
sm.getString("standardHostValue.customStatusFailed", errorPage.getLocation()));
return false;
}
if (response.isCommitted()) {
// Response is committed - including the error page is the
// best we can do
rd.include(request.getRequest(), response.getResponse());
} else {
// Reset the response (keeping the real error code and message)
response.resetBuffer(true);
response.setContentLength(-1);
// 转发到指定页面,Spring Boot默认是/error
rd.forward(request.getRequest(), response.getResponse());
// If we forward, the response is suspended again
response.setSuspended(false);
}
// Indicate that we have successfully processed this custom page
return true;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// Report our failure to process this custom page
container.getLogger().error("Exception Processing " + errorPage, t);
return false;
}
}
如果根据异常或者状态码找到了对应的错误页面,则会根据错误页面的配置的路径进行返回。在Spring Boot中就是配置了一个状态码为0,路径为/error的一个错误页面,而/error则对应于BasicErrorController
这个处理器,用于处理所有的错误信息。当然了如果请求被全局异常处理器处理掉了,而没有走到tomcat本身的异常处理逻辑(即DispatcherServlet
没有向外抛出异常,也没有调用response.sendError
方法时)是不会转发到/error路径的。
默认情况下,tomcat中是没有配置任何错误页面的,因此根据异常或者状态码是找不到错误页面的,最终会执行一个默认处理,处理类为org.apache.catalina.valves.ErrorReportValve
,展示的内容如上面例子图片所示。
可以看到上面处理逻辑会先从request对象中获取异常信息,那么对于上文所说的第一种情况,即Servlet处理时往外抛出异常时,不难猜到tomcat内部在执行Servlet时会捕获异常,同时通过req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, e)
方法将异常信息放到request对象中,供后续异常处理时使用,具体代码见org.apache.catalina.core.StandardWrapperValve
类,该类会执行过滤器链以及Servlet本身逻辑。
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// ... 略
Servlet servlet = null;
try {
// 获取Servlet
servlet = wrapper.allocate();
}
// 省略了很多无关逻辑
try {
if ((servlet != null) && (filterChain != null)) {
// 执行过滤器链以及Servlet
filterChain.doFilter(request.getRequest(), response.getResponse());
}
} catch (Throwable e) {
exception(request, response, e);
}
// ...略
}
private void exception(Request request, Response response,
Throwable exception) {
// 设置异常信息
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
// 设置状态码
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
// 设置错误标记
response.setError();
}
到这里,tomcat内部的一个错误处理机制便基本介绍完了。
配置错误页面
前文说到,tomcat会根据异常信息或者状态码去寻找对应的错误页面,如果没有找到则会使用一个默认页面返回给客户端。那么该如何配置错误页面,便是本节要说明的内容。
- 通过web.xml方式配置
<!-- 根据状态码配置 -->
<error-page>
<error-code>404</error-code>
<location>/404.html</location>
</error-page>
<!-- 状态码为0时可以匹配所有的状态码 -->
<error-page>
<error-code>0</error-code>
<location>/error.html</location>
</error-page>
<!-- 根据异常类型配置 -->
<error-page>
<exception-type>java.lang.NullPointerException</exception-type>
<location>/error.html</location>
</error-page>
- 内嵌容器编码
Spring Boot运行时一般使用内嵌容器,在org.apache.catalina.Context
接口中定义了方法,可配置错误页面
public interface Context extends Container, ContextBind {
void addErrorPage(ErrorPage errorPage);
}
可以参考org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
protected void configureContext(Context context, ServletContextInitializer[] initializers) {
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(starter, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
context.addLifecycleListener(lifecycleListener);
}
for (Valve valve : this.contextValves) {
context.getPipeline().addValve(valve);
}
// 配置错误页面
for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
for (MimeMappings.Mapping mapping : getMimeMappings()) {
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
configureSession(context);
new DisableReferenceClearingContextCustomizer().customize(context);
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}
总结
response
中3个关键方法的作用
setStatus(404)
,设置状态码以及message,message将为空。sendError(404)
,设置错误标志,状态码,message为空。sendError(404, "System Error!")
,设置错误标志、状态码、message。
- tomcat处理错误时,优先以异常处理,然后再是状态码处理,如果有异常,状态码固定是500,即
setStatus
、sendError
方法指定的状态码无效,message取e.getMessage()。如果没有异常,则状态码为sendError
指定的状态码,message为sendError
指定的message。响应给客户端时,会优先根据异常信息寻找配置的错误页面,找不到则会再根据状态码寻找配置的错误页面,特别的,状态码为0的错误页面可以匹配到所有的状态码,找到错误页面后,会给request对象设置一系列的值,诸如状态码,message,异常,请求路径等重要信息,然后转发到错误页面指定的路径。若没有找到错误页面,则返回一个默认的返回给客户端,包含状态码,message,异常等重要信息。 request.getAttribute(RequestDispatcher.ERROR_EXCEPTION
)方法可以获取到Servlet执行时抛出的异常。
Tomcat异常处理机制的更多相关文章
- Java异常处理机制 try-catch-finally 剖析
Java拥有着强大的异常处理机制,最近初步学习了下,感觉内容还是挺多的,特此来将自己的理解写出来与大家分享. 一. 在Java代码code中,由于使用Myeclipse IDE,可以自动提醒用户哪里有 ...
- JAVA 异常处理机制
主要讲述几点: 一.异常的简介 二.异常处理流程 三.运行时异常和非运行时异常 四.throws和throw关键字 一.异常简介 异常处理是在程序运行之中出现的情况,例如除数为零.异常类(Except ...
- 深入理解java异常处理机制
异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 过API中Throwable类的众多子类描述各种不同的 ...
- C++学习笔记27:异常处理机制
一.异常处理机制基础 异常的定义 程序中可以检测的运行不正常的情况 异常处理的基本流程 某段程序代码在执行操作时发生特殊情况,引发一个特定的异常 另一段程序代码捕获该异常并处理它 二.异常的引发 th ...
- C++中的异常处理机制
C++中的捕获异常机制catch参数中实参的类型不同,采取的处理方式则不相同,且与普通的函数调用还不一样,具体表现为当抛出异常throw A()或throw obj时,对象会进行一次额外的对象复制操作 ...
- 16、java中的异常处理机制
异常:就是程序在运行时出现不正常情况.异常由来:问题也是现实生活中一个具体的事物,也可以通过java的类的形式进行描述.并封装成对象. 其实就是java对不正常情况进行描述后的对象体现. 对于问题的划 ...
- Struts——(四)异常处理机制
在通常的情况下,我们得到异常以后,需要将页面导航到一个错误提示的页面,提示错误信息.利用Stuts我们可以采用两种方式处理异常: 1.编程式异常处理 即我们在Action中调用业务逻辑层对象的方法时, ...
- Java面向对象编程之异常处理机制
一:Java的异常处理机制的优点: 1:把各种不同情况的异常情况分类,使用JAVA类来表示异常情况,这种类被称为异常类.把各种异常情况表示成异常类,可以充分的发挥类的可扩展性和可重用性. 2:异常流程 ...
- 图解Tomcat类加载机制
说到本篇的tomcat类加载机制,不得不说翻译学习tomcat的初衷. 之前实习的时候学习javaMelody的源码,但是它是一个Maven的项目,与我们自己的web项目整合后无法直接断点调试.后来同 ...
- Java之异常处理机制
来源:深入理解java异常处理机制 2.Java异常 异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 ...
随机推荐
- LG P3809 【模板】后缀排序
贴模板 注意:\(\text{id}\) 表示第二关键字排序后(其实无需排序,利用上轮的 \(\text{sa}\) 值即可)相应的第一关键字的位置 计数排序为了稳定性最后确定位置时要倒着开始 复制的 ...
- JZOJ 3528. 【NOIP2013模拟11.7A组】图书馆(library)
题目 解析 看到这题,没想到 \(dp\) 果断打了暴力 暴力理应只有 \(30\) 左右的样子 然而我加上了些奇技淫巧竟然有 \(80\) 分! 惊到我了! 我 \(80\) 分的暴力: 很容易想到 ...
- IP地址后面/24/26/27/28/29/30网关数量分别是多少?如何计算?
转载csdn: https://blog.csdn.net/jinfengyunIDC/article/details/112575286
- MySQL索引的基本理解
之前一致以为索引就是简单的在原表的数据上加了一些编号,让查询更加快捷.后来发现里面还有更深的知识. 索引用于快速查找具有特定列值的行.如果没有索引,MySQL 必须从第一行开始,然后通读整个表以找到相 ...
- Cobalt Strike 之:提权
郑重声明: 本笔记编写目的只用于安全知识提升,并与更多人共享安全知识,切勿使用笔记中的技术进行违法活动,利用笔记中的技术造成的后果与作者本人无关.倡导维护网络安全人人有责,共同维护网络文明和谐. Co ...
- NSAIDs以优化剂量治疗中轴型SpA:聚焦6周期间骶髂关节MRI变化
NSAIDs以优化剂量治疗中轴型SpA:聚焦6周期间骶髂关节MRI变化 PresentID: OP0170 TREATMENT OF AXIAL SPONDYLOARTHRITIS WITH AN O ...
- 聊一下kafka的消费组
介绍 消费组使kafka中很重的概念,只有弄清楚消费组的概念,才能在项目中把它运用好,在kafka中,每个消费者都对应一个消费组,消费者可以是一个线程,一个进程,一个服务实例,如果kafka想要消费消 ...
- mybatis动态标签——choose、when、otherwise
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "- ...
- MS-TCT: Multi-Scale Temporal ConvTransformer for Action Detection概述
1.针对的问题 为了在未修剪视频中建模时间关系,以前的多种方法使用一维时间卷积.然而,受核大小的限制,基于卷积的方法只能直接获取视频的局部信息,不能学习视频中时间距离较远的片段之间的直接关系.因此,这 ...
- gunicorn的功能及使用方法
一.gunicorn的简介Gunicorn是基于unix系统,被广泛应用的高性能的Python WSGI HTTP Server.用来解析HTTP请求的网关服务.它通常是在进行反向代理(如nginx) ...