声明

源码基于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给我们返回的主要信息便是MessageException,其中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);
}
}

总结

  1. response中3个关键方法的作用
  • setStatus(404),设置状态码以及message,message将为空。
  • sendError(404),设置错误标志,状态码,message为空。
  • sendError(404, "System Error!"),设置错误标志、状态码、message。
  1. tomcat处理错误时,优先以异常处理,然后再是状态码处理,如果有异常,状态码固定是500,即setStatussendError方法指定的状态码无效,message取e.getMessage()。如果没有异常,则状态码为sendError指定的状态码,message为sendError指定的message。响应给客户端时,会优先根据异常信息寻找配置的错误页面,找不到则会再根据状态码寻找配置的错误页面,特别的,状态码为0的错误页面可以匹配到所有的状态码,找到错误页面后,会给request对象设置一系列的值,诸如状态码,message,异常,请求路径等重要信息,然后转发到错误页面指定的路径。若没有找到错误页面,则返回一个默认的返回给客户端,包含状态码,message,异常等重要信息。
  2. request.getAttribute(RequestDispatcher.ERROR_EXCEPTION)方法可以获取到Servlet执行时抛出的异常。

Tomcat异常处理机制的更多相关文章

  1. Java异常处理机制 try-catch-finally 剖析

    Java拥有着强大的异常处理机制,最近初步学习了下,感觉内容还是挺多的,特此来将自己的理解写出来与大家分享. 一. 在Java代码code中,由于使用Myeclipse IDE,可以自动提醒用户哪里有 ...

  2. JAVA 异常处理机制

    主要讲述几点: 一.异常的简介 二.异常处理流程 三.运行时异常和非运行时异常 四.throws和throw关键字 一.异常简介 异常处理是在程序运行之中出现的情况,例如除数为零.异常类(Except ...

  3. 深入理解java异常处理机制

       异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 过API中Throwable类的众多子类描述各种不同的 ...

  4. C++学习笔记27:异常处理机制

    一.异常处理机制基础 异常的定义 程序中可以检测的运行不正常的情况 异常处理的基本流程 某段程序代码在执行操作时发生特殊情况,引发一个特定的异常 另一段程序代码捕获该异常并处理它 二.异常的引发 th ...

  5. C++中的异常处理机制

    C++中的捕获异常机制catch参数中实参的类型不同,采取的处理方式则不相同,且与普通的函数调用还不一样,具体表现为当抛出异常throw A()或throw obj时,对象会进行一次额外的对象复制操作 ...

  6. 16、java中的异常处理机制

    异常:就是程序在运行时出现不正常情况.异常由来:问题也是现实生活中一个具体的事物,也可以通过java的类的形式进行描述.并封装成对象. 其实就是java对不正常情况进行描述后的对象体现. 对于问题的划 ...

  7. Struts——(四)异常处理机制

    在通常的情况下,我们得到异常以后,需要将页面导航到一个错误提示的页面,提示错误信息.利用Stuts我们可以采用两种方式处理异常: 1.编程式异常处理 即我们在Action中调用业务逻辑层对象的方法时, ...

  8. Java面向对象编程之异常处理机制

    一:Java的异常处理机制的优点: 1:把各种不同情况的异常情况分类,使用JAVA类来表示异常情况,这种类被称为异常类.把各种异常情况表示成异常类,可以充分的发挥类的可扩展性和可重用性. 2:异常流程 ...

  9. 图解Tomcat类加载机制

    说到本篇的tomcat类加载机制,不得不说翻译学习tomcat的初衷. 之前实习的时候学习javaMelody的源码,但是它是一个Maven的项目,与我们自己的web项目整合后无法直接断点调试.后来同 ...

  10. Java之异常处理机制

    来源:深入理解java异常处理机制 2.Java异常    异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 ...

随机推荐

  1. LG P3809 【模板】后缀排序

    贴模板 注意:\(\text{id}\) 表示第二关键字排序后(其实无需排序,利用上轮的 \(\text{sa}\) 值即可)相应的第一关键字的位置 计数排序为了稳定性最后确定位置时要倒着开始 复制的 ...

  2. JZOJ 3528. 【NOIP2013模拟11.7A组】图书馆(library)

    题目 解析 看到这题,没想到 \(dp\) 果断打了暴力 暴力理应只有 \(30\) 左右的样子 然而我加上了些奇技淫巧竟然有 \(80\) 分! 惊到我了! 我 \(80\) 分的暴力: 很容易想到 ...

  3. IP地址后面/24/26/27/28/29/30网关数量分别是多少?如何计算?

    转载csdn: https://blog.csdn.net/jinfengyunIDC/article/details/112575286

  4. MySQL索引的基本理解

    之前一致以为索引就是简单的在原表的数据上加了一些编号,让查询更加快捷.后来发现里面还有更深的知识. 索引用于快速查找具有特定列值的行.如果没有索引,MySQL 必须从第一行开始,然后通读整个表以找到相 ...

  5. Cobalt Strike 之:提权

    郑重声明: 本笔记编写目的只用于安全知识提升,并与更多人共享安全知识,切勿使用笔记中的技术进行违法活动,利用笔记中的技术造成的后果与作者本人无关.倡导维护网络安全人人有责,共同维护网络文明和谐. Co ...

  6. NSAIDs以优化剂量治疗中轴型SpA:聚焦6周期间骶髂关节MRI变化

    NSAIDs以优化剂量治疗中轴型SpA:聚焦6周期间骶髂关节MRI变化 PresentID: OP0170 TREATMENT OF AXIAL SPONDYLOARTHRITIS WITH AN O ...

  7. 聊一下kafka的消费组

    介绍 消费组使kafka中很重的概念,只有弄清楚消费组的概念,才能在项目中把它运用好,在kafka中,每个消费者都对应一个消费组,消费者可以是一个线程,一个进程,一个服务实例,如果kafka想要消费消 ...

  8. mybatis动态标签——choose、when、otherwise

    <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "- ...

  9. MS-TCT: Multi-Scale Temporal ConvTransformer for Action Detection概述

    1.针对的问题 为了在未修剪视频中建模时间关系,以前的多种方法使用一维时间卷积.然而,受核大小的限制,基于卷积的方法只能直接获取视频的局部信息,不能学习视频中时间距离较远的片段之间的直接关系.因此,这 ...

  10. gunicorn的功能及使用方法

    一.gunicorn的简介Gunicorn是基于unix系统,被广泛应用的高性能的Python WSGI HTTP Server.用来解析HTTP请求的网关服务.它通常是在进行反向代理(如nginx) ...