Servlet实战(1)
Servlet架构图
Servlet生命周期
Servlet生命周期是指其被创建到销毁得整个过程:
Servlet通过调用init()方法进行初始化。
Servlet调用service()方法来处理客户端的请求。
Servlet通过调用destory()方法终止。
最后,Servlet是由JVM的垃圾回收器进行垃圾回收的。
Servlet是单例多线程的,即在tomcat启动时创建一个线程池并init所有的Servlet,每次请求过来都是从线程池中取出一个正在等待被使用的线程去执行service方法,执行完毕放回线程池。等tomcat被关闭时去调用Servlet的destory方法,销毁Servlet。
Servlet的重要方法
service()方法由容器调用,service方法在适当的时候调用doGet,doPost,doPut,doDelete。所以,你不用对service()方法做任何动作,你只需要根据来自客户端的请求类型来重写doGet()或doPost()即可。
destory方法只会被调用一次,在Servlet生命周期结束时被调用。destory方法可以让你的Servlet关闭数据库连接、停止后台线程,把session写入磁盘,并执行其他类似清理的活动。在调用destory方法后,Servlet对象被标记为垃圾回收。
<web-app> <servlet> <servlet-name>hello_world</servlet-name> <servlet-class>servlet.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>hello_world</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>
就像上面说的,你没必要一定去写service方法,如果你知道请求的方式,可以直接写doXX,service方法是由容器调用的,service会在适当的时候调用doXX。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Insert title here</title> 6 </head> 7 <body> 8 <form action="hello"> 9 网站名:<input type="text" name="name"><br> 10 网址:<input type="text" name="name"> 11 <input type="submit"> 12 </form> 13 </body> 14 </html>
在action里直接写的是在web.xml中配置的servlet的url。这里的web.xml我们依旧用上一章节的web.xml。
1 public class HelloServlet extends HttpServlet{ 2 3 @Override 4 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 5 resp.setContentType("text/html;charset=UTF-8"); 6 // String name = new String(req.getParameter("name").getBytes("ISO8859-1"),"UTF-8"); 7 String name = req.getParameter("name"); 8 String url = req.getParameter("url"); 9 PrintWriter out = resp.getWriter(); 10 out.println(name + ":" + url); 11 } 12 13 }
启动tomcat,访问http://localhost:8080/Servlet/hello.html,填写表单即可出现响应结果。
注意:这里说的是Servlet3.0的web.xml配置即dtd,如果你使用的Servlet3.0之前的,则使用的不是dtd而是xsd的方式,它默认也是支持注解方式的(默认metadata-complete="true")!
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 3 "http://java.sun.com/dtd/web-app_2_3.dtd"> 4 <web-app> 5 6 <!-- 不在web.xml里配置mapping,使用注解方式配置 --> 7 <!-- <servlet> 8 <servlet-name>hello_world</servlet-name> 9 <servlet-class>servlet.HelloServlet</servlet-class> 10 <load-on-startup>1</load-on-startup> 11 </servlet> 12 13 <servlet-mapping> 14 <servlet-name>hello_world</servlet-name> 15 <url-pattern>/hello</url-pattern> 16 </servlet-mapping> --> 17 18 </web-app>
servlet3.0之后默认对注解支持,并推荐使用注解方式。
1 <?xml version="1.0" encoding="UTF-8"?> 2 3 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 5 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 6 version="2.5" metadata-complete="false"> 7 8 <!-- 不在web.xml里配置mapping,使用注解方式配置 --> 9 <!-- <servlet> 10 <servlet-name>hello_world</servlet-name> 11 <servlet-class>servlet.HelloServlet</servlet-class> 12 <load-on-startup>1</load-on-startup> 13 </servlet> 14 15 <servlet-mapping> 16 <servlet-name>hello_world</servlet-name> 17 <url-pattern>/hello</url-pattern> 18 </servlet-mapping> --> 19 20 </web-app>
Servlet3.0之前想使用注解方式,要么不配置metadata-complete项,要么配置为false。
在注解中可配置Servlet的name属性,如果没配置默认就是类名,也可以配置url路径,这是一个字符串数组,可以配置多个路径来对应一个Servlet处理,也可以配置loadOnStartup=1(大于0即可),表示启动时就创建Servlet实例,如果配置的loadOnStartup不大于0,则只有第一次访问时才创建Servlet实例。
@WebServlet("/HelloServlet") public class HelloServlet extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); // String name = new String(req.getParameter("name").getBytes("ISO8859-1"),"UTF-8"); String name = req.getParameter("name"); String url = req.getParameter("url"); PrintWriter out = resp.getWriter(); out.println(name + ":" + url); } }
<form action="HelloServlet"> 网站名:<input type="text" name="name"><br> 网址:<input type="text" name="url"> <input type="submit"> </form>
注意:要么你使用传统的方式在web.xml里配置mapping或者你使用注解的方式去配置mapping,两种方式只能取一种!
Servlet3.0之前的的部署描述文件web.xml的顶层标签<web-app>有一个metadata-complete属性,该属性指定当前的部署描述文件是否是完全的。如果设置为true,则容器在部署时将只依赖部署描述文件,忽略所有的注解(同时也会跳过web-fragment.xml扫描,亦禁用可插件支持)。如果不配置该属性或将其匹配为false,则表示启用注解支持(和可插件支持)。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5" metadata-complete="false"> <!-- 不在web.xml里配置mapping,使用注解方式配置 --> <!-- <servlet> <servlet-name>hello_world</servlet-name> <servlet-class>servlet.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>hello_world</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> --> </web-app>
首先你要把<!DOCTYPE这行注释掉,这是3.0的,只有注释了3.0的解析规则才能在web-app标签里使用其他属性【详见文件格式下:dtd和xsd的区别笔记】,在上面代码中我们模拟一下metadata-complete="false"的场景,即在Servlet3.0之前使用注解支持。
应用注解方便很多,不过现在都用集成spring等基本原始的servlet也不会多写了。
String name = new String(req.getParameter("name").getBytes("ISO8859-1"),"UTF-8");
但是我有个问题就是为什么会在Servlet中出现中文乱码?教程上写了,但我并没有写也没出现中文乱码问题。有问题就要去找答案。
传输方和接收方采用的编码不一致。传输方对参数采用的时UTF-8编码而接收方却使用ISO8859-1进行编码当然会出现乱码问题。
在tomcat7及之前的版本默认使用的编码为ISO8859-1,在tomcat8以后都开始使用UTF-8编码了,所以如果你用的是tomcat7可修改server.xml如下:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
但上面这样会使的你的应用程序过于依赖tomcat服务器,所以不想过于依赖服务器,你可以使用最上面那样写。对于GET请求上面两个代码段都可以解决问题,但是对于POST提交方式,在server.xml里设置编码并不起作用,只能通过:
request.setCharacterEncoding("UTF-8");
get请求简单点我们设置一下server.xml即可,但post请求,我们不可能每次都这样设置吧,毕竟很麻烦,所以tomcat也提供了解决方案,打开tomcat中全局的web.xml,你可查看到这么一段被注释的配置:
<!-- A filter that sets character encoding that is used to decode --> <!-- parameters in a POST request --> <!-- <filter> <filter-name>setCharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <async-supported>true</async-supported> </filter> -->
这是一段过滤器的配置,注释说明很清除,它是为解决post请求参数出现乱码的,通常我们不在全局web.xml里启用它,而是在自己的web应用里的web.xml启用上面一段配置【不推荐】。这样也会让你的应用过于依赖tomcat容器,所以最好的解决方式是自己在应用程序里写一个和上面这个SetCharacterEncodingFilter过滤器相同的过滤器【推荐】。
response.setContentType("text/html;charset=UTF-8");
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
在doGet或doPost中,我们可以做一些业务处理,通过request对象可以拿到前台传递进来的参数>最常用的比如:
request.setCharacterEncoding()
request.getRequestDispatcher() 注意它和用 ServletContext#getRequestDispatcher 的区别
嘿嘿,忍不住想要记录一下request.getRequestDispatcher(String path)和getServletContext().getRequestDispatcher(String path)的区别。这两者都是返回一个RequestDispatcher对象,都可用于将请求转发到资源或在响应中包含资源。
使用request直接获取的方式里面的参数path指定的路径名可能是相对的,比如说相对于当前Servlet注解中的路径,如果它是以"/"开头,则代表是相对于当前上下文根本目录。
使用getServletContext().getRequestDispatcher(String path)获取的方式,里面的参数path必须以"/"开头,表示相对于当前上下文根目录。
对比发现使用request获取RequestDispatcher更加灵活一些,它既可以相对当前路径也可以相对根路径,而后面一种方式只能相对于根路径。
记得之前自己写python的时候,并没有过多的考虑使用状态码,也没有使用过python提供的有关状态码返回的方法,自己常常都是直接使用response对象将一个statusCode封装进json对象传递到前台,这样虽然也能达到效果但是今天深入了了解了一下,总感觉我那种处理方式不妥,接口有接口的作用,自己不该乱用。
sendError(int code,String mesg)
1 @Override 2 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 3 resp.sendError(401, "需要认证访问"); 4 } 5 6 @Override 7 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 8 doGet(req, resp); 9 }
在最开始的时候,感觉简单,所以没看那么多就开始蒙头写了一个Servlet和一个处理编码问题的过滤器,一开始自己写的是extends Filter编译报错改成了implements后自己也没在意就只实现了doFilter()方法,到后来自己启动tomcat时总是会报错java.lang.AbstractMethodError,后来发现原因就是我的EncodingFilter里没有写init和destory方法,我挺纳闷的,因为我写Servlet时也没写init和destory方法,究其原因,感觉自己太蠢,这又要回到tomcat加载**Servlet和**Filter上了,我们常写的Servlet一般都是继承自HttpServlet和GenericServlet,这两个父类已经实现了init和destory方法,所以子类继承自它俩的子类不实现也没关系,在tomcat初始化这个Servlet会去找它的init,当然能找到并执行。但是**Filter呢?我直接实现的是一个接口,在tomcat初始化这个Filter时要去执行它的init方法,发现你没有这个方法,当然会出错了,在此记录。
过滤器通过Web部署描述符(web.xml)中的XML标签来声明,然后映射到你的应用程序的描述符中的Servlet名称或URL模式。在Web容器启动时,会读取web.xml创建你所配置的每一个过滤器。
Filter执行顺序和在web.xml配置文件中顺序一致,一般把Filter配置在所有Servlet之前。web.xml中的filter-mapping元素的顺序决定了Web容器应用过滤器到Servlet的顺序。若要反转过滤器的顺序,只需要反转filet-mapping元素即可。
我们所使用的Servlet还是上面用到的Servlet(不在使用注解的方式了),在Servlet同级目录下新建filter目录
1 package filter; 2 3 import java.io.IOException; 4 import java.util.logging.Level; 5 import java.util.logging.Logger; 6 import javax.servlet.Filter; 7 import javax.servlet.FilterChain; 8 import javax.servlet.FilterConfig; 9 import javax.servlet.ServletException; 10 import javax.servlet.ServletRequest; 11 import javax.servlet.ServletResponse; 12 import javax.servlet.http.HttpServletRequest; 13 14 public class LoggerFilter implements Filter { 15 Logger logger = Logger.getLogger("filter"); 16 17 @Override 18 public void init(FilterConfig filterConfig) throws ServletException { 19 // 父类Filter是一个接口 20 // Filter.super.init(filterConfig); 21 logger.setLevel(Level.INFO); 22 logger.info("初始化:" + logger); 23 24 // 可以获取web.xml里init-param里的数据 25 String value = filterConfig.getInitParameter("Function"); 26 System.out.println(value); 27 } 28 29 @Override 30 public void destroy() { 31 // 父类Filter是一个接口 32 // Filter.super.destroy(); 33 logger.info("销毁:" + logger); 34 } 35 36 @Override 37 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 38 throws IOException, ServletException { 39 HttpServletRequest req = (HttpServletRequest) request; 40 logger.info("拦截的地址:" + req.getRequestURL()); 41 chain.doFilter(request, response); 42 } 43 44 }
在web.xml中配置【过滤器也可以像Servlet那样使用@WebFilter的方式而不在web.xml中配置】:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 4 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 5 version="2.5" metadata-complete="true"> 6 <!-- 上面使用的不是dtd校验格式,因为马上我们要测试其他的 --> 7 8 <filter> 9 <filter-name>LoggerFilter</filter-name> 10 <filter-class>filter.LoggerFilter</filter-class> 11 <init-param> 12 <param-name>Function</param-name> 13 <param-value>打印日志</param-value> 14 </init-param> 15 </filter> 16 17 <filter-mapping> 18 <filter-name>LoggerFilter</filter-name> 19 <url-pattern>/*</url-pattern> 20 </filter-mapping> 21 22 <servlet> 23 <servlet-name>HelloServlet</servlet-name> 24 <servlet-class>servlet.HelloServlet</servlet-class> 25 <load-on-startup>1</load-on-startup> 26 </servlet> 27 28 29 <servlet-mapping> 30 <servlet-name>HelloServlet</servlet-name> 31 <url-pattern>/hello</url-pattern> 32 </servlet-mapping> 33 34 </web-app>
<form action="hello" method="get"> 网站名:<input type="text" name="name"><br> 网址:<input type="text" name="url"> <input type="submit"> </form> </div> <div> <!-- 这里的action就是web.xml里配置的url-pattern,不加/ --> <form action="hello" method="post"> 网站名:<input type="text" name="name"><br> 网址:<input type="text" name="url"> <input type="submit"> </form>
我们的servlet不在使用注解的方式了,所有在上面的web.xml中要添加servlet的配置。这样方便以后学习其他框架。看我们上面过滤器的配置,我们使用的是url-pattern的模式,我写的是/*表示拦截所有请求,当然你还可以配置特定的拦截路径,如果你这样配置了,那么这个拦截器仅仅作用于你拦截的范围。
不仅这些,在filter-mapping里还有其它方式的配置,了解一下:
<filter-name>用于为过滤器指定一个名字,该元素的内容不能为空。
<filter-class>元素用于指定过滤器的完整的限定类名。
<init-param>元素用于为过滤器指定初始化参数,它的子元素<param-name>指定参数的名字,<param-value>指定参数的值。
在过滤器中,可以使用FilterConfig接口对象来访问初始化参数。
2. <filter-mapping>元素用于设置一个 Filter 所负责拦截的资源。一个Filter拦截的资源可通过两种方式来指定:Servlet 名称和资源访问的请求路径
<filter-name>子元素用于设置filter的注册名称。该值必须是在<filter>元素中声明过的过滤器的名字
<url-pattern>设置 filter 所拦截的请求路径(过滤器关联的URL样式)
3. <servlet-name>指定过滤器所拦截的Servlet名称。
4. <dispatcher>指定过滤器所拦截的资源被 Servlet 容器调用的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST。用户可以设置多个<dispatcher>子元素用来指定 Filter 对资源的多种调用方式进行拦截。
5. <dispatcher>子元素可以设置的值及其意义【web.xml不能使用dtd校验】
REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。
INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。
现在来测试一下第3个<servlet-name>,看web.xml:
<filter-mapping> <filter-name>LoggerFilter</filter-name> <!-- <url-pattern>/*</url-pattern> --> <servlet-name>HelloServlet</servlet-name> </filter-mapping>
不在使用全部拦截的方式,而是只拦截HelloServlet的请求,在此运行还是会有相同的结果。
<filter-mapping> <filter-name>LoggerFilter</filter-name> <!-- <url-pattern>/*</url-pattern> --> <servlet-name>HelloServlet</servlet-name> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> </filter-mapping>
回顾一下tomcat加载Servlet的过程,我大致也能想到一些关于Filter的工作原理,Filter是在Servlet被加载之前加载的,Filter中含有过滤的条件,比如url-pattern或者servlet-name,不管是哪一种,我想最后都是落实在url-pattern上。下面这是一张从请求到Servlet处理步骤图:
我在画图的时候,一直在思考,Filter存在最开始的位置是否正确,它存在哪个位置比较合适,细想web.xml中filter-mapping有一个配置叫dispatcher,它里面可以配置直接请求或经过FORWARD转发的请求,对于转发的请求是怎么转发的?使用request.getRequestDispatcher(String path)或getServletContext().getRequestDispatcher(String path)先获取dispatcher然后在调用转发方法forward或include。有两种获取dispatcher的方式,那Filter存在的位置应该不是最开始的,有迷惑点,自己就去网上解决。
自己在【Tomcat工作流程笔记中tomcat对静态资源访问做了总结】,上面的那种画法确实是不准确的,自己感觉下面这种方式更好一些。
在看到菜鸟教程里关于Servlet异常处理这块,不由的想起了spring boot里好像也有这么一个东西,哦哦,spring boot里是全局异常处理器,我想这两个大概是类似的吧。
当一个Servlet抛出一个异常时,Web容器在使用了exception-type元素的web.xml中搜索异常类型相匹配的配置。你必须在web.xml中使用error-page元素来指定对特定异常或HTTP状态码作出相应的Servlet调用。
在第一次使用时,我想做一个ArithmeticExceptionServlet,也就是一个专门捕获除以0抛出的异常,然后我就开始风风火火的开始写。
先创建一个ArithmeticExceptionServlet
package servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ArithmeticExceptionServlet extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); Throwable throwable = (Throwable)req.getAttribute("javax.servlet.error.exception"); Integer statusCode = (Integer)req.getAttribute("javax.servlet.error.status_code"); String servletName = (String)req.getAttribute("javax.servlet.error.servlet_name"); System.out.println("异常类型:" + throwable.toString()); System.out.println("状态码:" + statusCode); System.out.println("谁抛出来的?" + servletName); out.println("发生了异常!"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
<?xml version="1.0" encoding="UTF-8"?> <!-- <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> --> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5" metadata-complete="true"> <filter> <filter-name>LoggerFilter</filter-name> <filter-class>filter.LoggerFilter</filter-class> <init-param> <param-name>Function</param-name> <param-value>打印日志</param-value> </init-param> </filter> <filter-mapping> <filter-name>LoggerFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>servlet.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <!-- 配置异常处理器 --> <servlet> <servlet-name>ArithmeticExceptionServlet</servlet-name> <servlet-class>servlet.ArithmeticExceptionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ArithmeticExceptionServlet</servlet-name> <url-pattern>/arithmeticException</url-pattern> </servlet-mapping> <!-- error-code 相关错误页面 --> <error-page> <error-code>401</error-code> <location>/arithmeticException</location> </error-page> <error-page> <error-code>402</error-code> <location>/arithmeticException</location> </error-page> <!-- exception-type 相关的错误页面 --> <error-page> <exception-type>java.lang.ArithmeticException</exception-type> <location>/arithmeticException</location> </error-page> </web-app>
现在万事俱备,只欠东风了,异常处理的Servlet我们也写好了,拦截条件我们也配置好了,以上不管是使用response发送错误码401/402,还是某一个Servlet直接抛出ArithmeticException异常更或者是直接在浏览器端访问http://localhost:8080/Servlet/arithmeticException是否都可以触发我们的异常处理器ArithmeticExceptionServlet?我们测试一下:
http://localhost:8080/Servlet/arithmeticException
页面报错,后台ArithmeticExceptionServlet第20行报错:java.lang.NullPointerException,由于我们是使用浏览器端直接访问的,所以其实并没有任何异常抛出,所以在第17行我们拿到的异常是null。你可以增加一些处理,来解决这个问题。
public class HelloServlet extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ resp.setContentType("text/html;charset=UTF-8"); resp.sendError(401); PrintWriter out = resp.getWriter(); out.println("Hello 世界!"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
我们在HelloServlet中添加第6行代码,访问一下hello.html提交,看看结果:
页面报错,后台java.lang.NullPointerException,也就是说也获取不到异常类型。
public class HelloServlet extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ resp.setContentType("text/html;charset=UTF-8"); int i = 100 / 0; // resp.sendError(401); PrintWriter out = resp.getWriter(); out.println("Hello 世界!"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
java.lang.ArithmeticException: / by zero... 异常类型:java.lang.ArithmeticException: / by zero 状态码:500 谁抛出来的?HelloServlet
对比这三种,它们都可以触发了我们异常处理的Servlet,但是前两种并不是通过抛出异常来触发的,所以获取不到异常信息,第三种是通过异常触发的,能获取异常信息。
对比spring boot的全局异常拦截器,如果上面的web.xml里仅配置<exception-type>,不在配置<error-code>,那么我想触发机制应该和spring boot是类似的了。
package wd.exception; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * * @author admin * @explain { * -@ExceptionHandler:表示拦截异常 * -@ControllerAdvice:是controller的一个辅助类,最常用的就是作为全局异常处理的切面类 * -@ControllerAdvice:可以指定扫描范围 * -@ControllerAdvice:约定了几种可行的返回值,如果是直接返回model类的话,使用@ResponseBody进行json转换 * } * */ @ControllerAdvice public class GlobelExceptionApp { @ExceptionHandler(ArithmeticException.class) @ResponseBody public String arithmeticException() { throw new ArithmeticException(); } }
我想让我写的ArithmeticExceptionServlet更接近于上面spring boot,于是我做了以下改造:
1 package filter; 2 3 import java.io.IOException; 4 import java.io.PrintWriter; 5 6 import javax.servlet.Filter; 7 import javax.servlet.FilterChain; 8 import javax.servlet.FilterConfig; 9 import javax.servlet.ServletException; 10 import javax.servlet.ServletRequest; 11 import javax.servlet.ServletResponse; 12 import javax.servlet.http.HttpServletResponse; 13 14 public class ExceptionFilter implements Filter{ 15 16 @Override 17 public void init(FilterConfig filterConfig) throws ServletException {} 18 19 @Override 20 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 21 throws IOException, ServletException { 22 System.out.println("REQUEST/FORWARD/INCLUDE请求的异常被拦截"); 23 HttpServletResponse resp = (HttpServletResponse)response; 24 PrintWriter out = resp.getWriter(); 25 resp.sendError(404); 26 // 截断请求! 27 // chain.doFilter(request, response); 28 } 29 30 @Override 31 public void destroy() {} 32 33 }
2.在web.xml里添加filter和filter-mapping,并修改之前ArithmeticExceptionServlet的url-pattern
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!-- <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 3 "http://java.sun.com/dtd/web-app_2_3.dtd"> --> 4 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 5 xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 6 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 7 version="2.5" metadata-complete="true"> 8 9 <filter> 10 <filter-name>LoggerFilter</filter-name> 11 <filter-class>filter.LoggerFilter</filter-class> 12 <init-param> 13 <param-name>Function</param-name> 14 <param-value>打印日志</param-value> 15 </init-param> 16 </filter> 17 18 <filter-mapping> 19 <filter-name>LoggerFilter</filter-name> 20 <url-pattern>/*</url-pattern> 21 </filter-mapping> 22 23 <filter> 24 <filter-name>ExceptionFilter</filter-name> 25 <filter-class>filter.ExceptionFilter</filter-class> 26 <init-param> 27 <param-name>Function</param-name> 28 <param-value>拦截以request方式的请求</param-value> 29 </init-param> 30 </filter> 31 32 <filter-mapping> 33 <filter-name>ExceptionFilter</filter-name> 34 <url-pattern>/exception/*</url-pattern> 35 <dispatcher>REQUEST</dispatcher> 36 <dispatcher>FORWARD</dispatcher> 37 <dispatcher>INCLUDE</dispatcher> 38 </filter-mapping> 39 40 <servlet> 41 <servlet-name>HelloServlet</servlet-name> 42 <servlet-class>servlet.HelloServlet</servlet-class> 43 <load-on-startup>1</load-on-startup> 44 </servlet> 45 46 47 <servlet-mapping> 48 <servlet-name>HelloServlet</servlet-name> 49 <url-pattern>/hello</url-pattern> 50 </servlet-mapping> 51 52 <servlet> 53 <servlet-name>ArithmeticExceptionServlet</servlet-name> 54 <servlet-class>servlet.ArithmeticExceptionServlet</servlet-class> 55 </servlet> 56 57 58 <servlet-mapping> 59 <servlet-name>ArithmeticExceptionServlet</servlet-name> 60 <url-pattern>/exception/arithmeticException</url-pattern> 61 </servlet-mapping> 62 63 <!-- exception-type 相关的错误页面 --> 64 <error-page> 65 <exception-type>java.lang.ArithmeticException</exception-type> 66 <location>/exception/arithmeticException</location> 67 </error-page> 68 69 </web-app>
从以上配置可以看出我的ExceptionFilter仅仅拦截REQUEST,FORWARD,INCLUDE三种方式的请求,只保留了ERROR,这样我只要把除了ERROR之外的请求截断即可。
现在我们在启动项目,如果直接访问http://localhost:8080/Servlet/exception/arithmeticException将会被我写的ExceptionFilter拦截,因为这是REQUEST方式的请求,拦截后返回一个404状态码,表示不存在,访问不到。但是对于我们访问http://localhost:8080/Servlet/hello.html提交表单,由HelloServlet产生的异常不会被ExceptionFilter拦截,因为这属于ERROR,没有ExceptionFilter的截断,它就会直接进入我们的ArithmeticExceptionServlet中。这样我们就实现了仅仅针对ERROR的拦截。这和spring boot的全局异常拦截器已经有些相像了,但是还差一些,那就是一个是注解,一个是配置文件。servlet 3之后我们可以在Servlet和Filter上使用注解,但是<error-page>呢?还有很多不同的是spring boot的全局异常处理是不在其他Servlet中抛出异常,如果出现异常就直接到全局异常里,而我们的是其他Servlet先抛出了异常然后才到ExceptionServlet中,这点就有很大差别了。不过使用传统的Servlet+Filter方式是可以实现类似spring boot那种全局异常捕获的效果的,这点才是要学习了解的。