问题描述
当我的 HttpServlet 抛出 ServletException 时,Tomcat 正在记录一条包含堆栈跟踪的 SEVERE 消息,尽管它已正确重定向到 web.xml 中的另一个 HttpServlet。
Tomcat 使用堆栈跟踪记录以下消息:
21-Mar-2015 15:24:57.521 SEVERE [http-nio-8080-exec-28] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [MyHttpServlet] in context with path [/HttpServletExceptionHandler] threw exception [CustomException] with root cause CustomException
at MyHttpServlet.doGet(MyHttpServlet.java:20)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:618)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:516)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1086)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:659)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:223)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1558)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1515)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
我做了什么?
首先,MyHttpServlet 抛出一个 ServletException,在它的 doGet() 方法中包装了一个 CustomException(Exception 的子类):
public class MyHttpServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
throw new ServletException(new CustomException());
}
}
然后,抛出的CustomException被重定向到MyServletExceptionHandler(映射到位置'/MyServletExceptionHandler'。这个重定向在web.xml中以如下方式定义:
<error-page>
<exception-type>CustomException</exception-type>
<location>/MyServletExceptionHandler</location>
</error-page>
最后,MyServletExceptionHandler 接收到抛出的异常并打印出来:
public class MyServletExceptionHandler extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
final Throwable throwable = (Throwable) req.getAttribute("javax.servlet.error.exception");
System.out.println("MyServletExceptionHandler caught Throwable: " + throwable.toString());
}
}
这会导致预期的“MyServletExceptionHandler 捕获 Throwable: CustomException”打印,所以这确实有效,但不知何故 Tomcat 也会记录上面提到的 SEVERE 消息,包括该堆栈跟踪。这弄乱了我的日志记录。
我为什么要这样?
根据 Java Beat 的OCEJWCD 6 Mock Exam – 4,上述方法是处理 Servlet 中的异常处理的正确方法。问题 29 状态(剧透警告:粗体为正确答案):
如果发生从 java.lang.Exception 扩展的业务异常,以下哪项是向客户端发送错误页面的明智方法?
- 捕获异常并使用 RequestDispatcher 将请求转发到错误页面
- 不要捕获异常并在 web.xml 中定义“异常到错误页面”映射
- 捕获异常,将其包装到 ServletException 中并在 web.xml 中定义“业务异常到错误页面”映射
- 捕获异常,将其包装到 ServletException 中,并在 web.xml 中定义 'ServletException to error-page' 映射
- 什么都不做,servlet容器会自动发送一个默认的错误页面
第三个答案(标记为正确)清楚地表明我重新引导异常的方式是一个明智的解决方案。
进一步讨论材料
我在此页面上找到了以下引用(来自 CodeRanch.com 的 Tom Holloway 的 10-2-2012)
实际上,ServletException 在 webapp 中无处可去,因此让它出现在主控制台上并没有那么不合理,因为它表明应用程序本身没有处理问题。
事实上,Javadocs 是这样描述 ServletException 构造函数的:
“用指定的消息构造一个新的 servlet 异常。该消息可以写入服务器日志和/或显示给用户。”
请注意,它明确表示服务器日志。
服务器可以在这里以多种方式参与。首先,您应该能够在 web.xml 中定义一个通用异常处理程序,以允许应用程序处理异常,该处理程序不仅可以记录到应用程序日志,还可以确定应该采取什么恢复操作(如果有)采取(更通用的服务器代码无法做到的事情)。其次,您可以定义一个自定义错误页面,在这种情况下,Tomcat 将捕获 ServletException 并调度该页面。但是请注意,操作词是 page。与登录屏幕一样,这些页面直接从 Tomcat 调用,因此无法通过 servlet 进行路由。换句话说,使用 HTML 或 JSP,而不是 Struts 或 JSF。
然而,底线是抛出 ServletExceptions 是糟糕的应用程序设计的标志。这意味着某人太懒或太匆忙而无法正确处理问题。与此相比,记录错误的位置是次要的。
这句话让我质疑 Java Beat 的 OCEJWCD 模拟考试(上面提到过)和我自己的解决方案作为良好实践。你认为业务异常应该由另一个 Servlet 处理吗?如果是这样,您认为 Servlet 容器 (Tomcat) 是否应该记录这些异常的堆栈跟踪?如果没有,那么最佳做法是什么?
最后的评论
- 抛出 RuntimeExceptions 而不是 ServletExceptions 会导致相同的 SEVERE 日志。
- 此 Bitbucket 存储库提供了该问题的一个工作示例。