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也不會多寫了。
細心的你肯定看到了我在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那種全域性異常捕獲的效果的,這點才是要學習瞭解的。