Java Servlet 實戰入門教程-10-servlet 過濾器 Filter
過濾器
概念
Filter(過濾器)是 Java 元件,允許執行過程中改變進入資源的請求和資源返回的響應中的有效負載和頭資訊。
Java Servlet API 類和方法提供了一種輕量級的框架用於過濾動態和靜態內容。
還描述瞭如何在 Web 應用配置 filter,以及它們實現的約定和語義。
什麼是過濾器
過濾器是一種程式碼重用的技術,它可以轉換 HTTP 請求的內容,響應,及頭資訊。
過濾器通常不產生響應或像 servlet 那樣對請求作出響應,而是修改或調整到資源的請求,修改或調整來自資源的響應。
過濾器可以作用於動態或靜態內容。
這章說的動態和靜態內容指的是 Web 資源。
供開發人員使用的過濾器功能有如下幾種型別:
-
在執行請求之前訪問資源。
-
在執行請求之前處理資源的請求。
-
用請求物件的自定義版本包裝請求對請求的header和資料進行修改。
-
用響應物件的自定義版本包裝響應對響應的header和資料進行修改。
-
攔截資源呼叫之後的呼叫。
-
作用在一個Servlet,一組Servlet,或靜態內容上的零個,一個或多個攔截器按指定的順序執行
常見過濾器元件
Authentication filters //使用者身份驗證過濾器
Logging and auditing filters //日誌記錄與審計過濾器
Image conversion filters //圖片轉換過濾器
Data compression filters //資料壓縮過濾器
Encryption filters //加密過濾器
Tokenizing filters //分詞過濾
Filters that trigger resource access events //觸發資源訪問事件過濾
XSL/T filters that transform XML content
MIME-type chain filters //MIME-TYPE 鏈過濾器
Caching filters //快取過濾器
實戰例子
問題需求
想對所有的 servlet 進行攔截。
比如列印響應的日誌資訊,編碼的過濾處理。
程式碼
- FilterCharsetServlet.java
輸出一句中文在頁面
@WebServlet ("/filter/charset")
public class FilterCharsetServlet extends HttpServlet {
private static final long serialVersionUID = -670379553788197281L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("中文描述");
}
}
- 日誌過濾器
public class LogFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("LogFilter start");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("列印一些日誌資訊...");
chain.doFilter(request, response);
}
public void destroy() {
}
}
ps: chain.doFilter(request, response);
這句話其實應該交給框架本身去呼叫,而不是開發者去手動呼叫。
開發者應該更加專注於過濾器的編寫。
配置
- web.xml
<filter>
<filter-name>LogFilter</filter-name>
<filter-class>com.github.houbb.servlet.learn.base.filter.LogFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LogFilter</filter-name>
<url-pattern>/filter/*</url-pattern>
</filter-mapping>
執行訪問
- 頁面顯示
????
- 命令列日誌
LogFilter start
列印一些日誌資訊...
可見我們的日誌攔截器是執行了,但是編碼的問題還沒有解決。
在每一個 servlet 中都處理編碼問題太笨了,還是交給過濾器處理吧。
多個過濾器
編碼過濾器
- CharsetFilter.java
這一次我們演示基於註解 @WebFilter
的實現方式。
@WebFilter(filterName = "CharsetFilter",
urlPatterns = {"/filter/*"},
initParams = {@WebInitParam(name = TEST_INIT_PARAM, value = "CharsetFilterParam")})
public class CharsetFilter implements Filter {
static final String TEST_INIT_PARAM = "test";
/**
* web 應用程式啟動時,web 伺服器將建立Filter 的例項物件,並呼叫其init方法,讀取web.xml配置,完成物件的初始化功能,
* 從而為後續的使用者請求作好攔截的準備工作(filter物件只會建立一次,init方法也只會執行一次)。
* 開發人員通過init方法的引數,可獲得代表當前filter配置資訊的FilterConfig物件。
*/
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化引數:" + filterConfig.getInitParameter(TEST_INIT_PARAM));
}
/**
* 該方法完成實際的過濾操作,當客戶端請求方法與過濾器設定匹配的URL時,Servlet容器將先呼叫過濾器的doFilter方法。
* FilterChain使用者訪問後續過濾器。
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 編碼的設定一定要放在 resp.getXXX(), resp.setXXX() 之前
response.setContentType("text/plain;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 把請求傳回過濾鏈
chain.doFilter(request, response);
}
/**
* Servlet容器在銷燬過濾器例項前呼叫該方法,在該方法中釋放Servlet過濾器佔用的資源。
*/
public void destroy() {
}
}
訪問
- 頁面顯示
????
- 命令列日誌
LogFilter start
列印一些日誌資訊...
Filter 主要概念
- 頁面顯示
中文描述
- 命令列日誌
初始化引數:CharsetFilterParam
LogFilter start
列印一些日誌資訊...
過濾器核心概念
應用開發人員通過實現 javax.servlet.Filter 介面並提供一個公共的空參構造器來建立過濾器。
該類及構建Web應用的靜態資源和 servlet 打包在 Web 應用歸檔檔案中。
Filter 在部署描述符中通過 <filter>
元素宣告。
一個過濾器或一組過濾器可以通過在部署描述符中定義 <filter-mapping>
來為呼叫配置。
可以使用 servlet 的邏輯檢視名把過濾器對映到一個特定的 servlet,或者使用 URL 模式把一組 servlet 和靜態內容資源對映到過濾器。
過濾器生命週期
在 Web 應用部署之後,在請求導致容器訪問 Web 資源之前,容器必須找到過濾器列表並按照如上所述的應用到 Web 資源。
容器必須確保它為過濾器列表中的每一個都例項化了一個適當類的過濾器, 並呼叫其 init(FilterConfig config) 方法。
過濾器可能會丟擲一個異常,以表明它不能正常運轉。如果異常的型別是 UnavailableException,容器可以檢查異常的 isPermanent 屬性並可以選擇稍候重試過濾器。
在部署描述符中宣告的每個 <filter>
在每個 JVM 的容器中僅例項化一個例項。
容器提供了宣告在過濾器的部署描述符的過濾器config(譯者注:FilterConfig),對 Web 應用的 ServletContext 的引用,和一組初始化引數。
當容器接收到傳入的請求時,它將獲取列表中的第一個過濾器並呼叫doFilter 方法,傳入 ServletRequest 和 ServletResponse,和一個它將使用的 FilterChain 物件的引用。
- 實現形式
過濾器的 doFilter 方法通常會被實現為如下或如下形式的子集:
-
該方法檢查請求的頭。
-
該方法可以用自定義的 ServletRequest 或 HttpServletRequest 實現包裝請求物件為了修改請求的頭或資料。
-
該方法可以用自定義的 ServletResponse 或 HttpServletResponse實現包裝傳入 doFilter 方法的響應物件用於修改響應的頭或資料。
-
該過濾器可以呼叫過濾器鏈中的下一個實體。下一個實體可能是另一個過濾器,或者如果當前呼叫的過濾器是該過濾器鏈配置在部署描述符中的最後一個過濾器,下一個實體是目標Web資源。呼叫FilterChain物件的doFilter方法將影響下一個實體的呼叫,且傳入的它被呼叫時請求和響應,或傳入它可能已經建立的包裝版本。 由容器提供的過濾器鏈的doFilter方法的實現,必須找出過濾器鏈中的下一個實體並呼叫它的doFilter方法,傳入適當的請求和響應物件。另外,過濾器鏈可以通過不呼叫下一個實體來阻止請求,離開過濾器負責填充響應物件。 service 方法必須和應用到 servlet 的所有過濾器執行在同一個執行緒中。
-
過濾器鏈中的下一個過濾器呼叫之後,過濾器可能檢查響應的頭。
-
另外,過濾器可能丟擲一個異常以表示處理過程中出錯了。如果過濾器在 doFilter 處理過程中丟擲 UnavailableException,容器必須停止處理剩下的過濾器鏈。 如果異常沒有標識為永久的,它或許選擇稍候重試整個鏈。
-
當鏈中的最後的過濾器被呼叫,下一個實體訪問的是鏈最後的目標 servlet 或資源。
-
在容器能把服務中的過濾器例項移除之前,容器必須先呼叫過濾器的 destroy 方法以便過濾器釋放資源並執行其他的清理工作。
包裝請求和響應
過濾器的核心概念是包裝請求或響應,以便它可以覆蓋行為執行過濾任務。
在這個模型中,開發人員不僅可以覆蓋請求和響應物件上已有的方法,也能提供新的 API 以適用於對過濾器鏈中剩下的過濾器或目標 web 資源做特殊的過濾任務。
例如,開發人員可能希望用更高級別的輸出物件如 output stream 或 writer 來擴充套件響應物件,如允許 DOM 物件寫回客戶端的API。
為了支援這種風格的過濾器,容器必須支援如下要求。當過濾器呼叫容器的過濾器鏈實現的 doFilter 方法時,容器必須確保請求和響應物件傳到過濾器鏈中的下一個實體,或如果過濾器是鏈中最後一個,將傳入目標 web 資源,且與呼叫過濾器傳入 doFilter 方法的物件是一樣的。
當呼叫者包裝請求或響應物件時,對包裝物件的要求同樣適用於從 servlet 或過濾器到 RequestDispatcher.forward
或 RequestDispatcher.include
的呼叫。
在這種情況下,呼叫 servlet 看到的請求和響應物件與呼叫 servlet 或過濾器傳入的包裝物件必須是一樣的。
過濾器環境
可以使用部署描述符中的 <init-params>
元素把一組初始化引數關聯到過濾器。
這些引數的名字和值在過濾器執行期間可以使用過濾器的FilterConfig 物件的 getInitParameter 和 getInitParameterNames 方法得到。
另外,FilterConfig 提供訪問 Web 應用的 ServletContext用於載入資源,記錄日誌,在 ServletContext 的屬性列表儲存狀態。
鏈中最後的過濾器和目標 servlet 或資源必須執行在同一個呼叫執行緒。
在 Web 應用中配置過濾器
過濾器可以通過 @WebFilter
註解定義或者在部署描述符中使用 <filter>
元素定義。
filter
在這個元素中,程式設計師可以宣告如下內容:
-
filter-name: 用於對映過濾器到 servlet 或 URL
-
filter-class: 由容器用於表示過濾器型別
-
init-params: 過濾器的初始化引數
程式設計師可以選擇性的指定 icon 圖示,文字說明,和工具操作顯示的名字。
容器必須為部署描述符中定義的每個過濾器宣告例項化一個 Java 類例項。
因此,如果開發人員對同一個過濾器類聲明瞭兩次,則容器將例項化兩個相同的過濾器類的例項。
下面是一個過濾器宣告的例子:
<filter>
<filter-name>Image Filter</filter-name>
<filter-class>com.acme.ImageServlet</filter-class>
</filter>
filter-mapping
filter-mapping 元素用於設定一個 Filter 所負責攔截的資源。
一個Filter攔截的資源可通過兩種方式來指定:Servlet 名稱和資源訪問的請求路徑
filter-name 子元素用於設定 filter 的註冊名稱。該值必須是在 <filter>
元素中宣告過的過濾器的名字
url-pattern 設定 filter 所攔截的請求路徑(過濾器關聯的URL樣式)
servlet-name
指定過濾器所攔截的 Servlet 名稱
過濾器和 RequestDispatcher
dispatcher
<dispatcher>
指定過濾器所攔截的資源被 Servlet 容器呼叫的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,預設REQUEST。
使用者可以設定多個 <dispatcher>
子元素用來指定 Filter 對資源的多種呼叫方式進行攔截。
子元素可以設定的值及其意義
-
REQUEST:當用戶直接訪問頁面時,Web容器將會呼叫過濾器。如果目標資源是通過RequestDispatcher的include()或forward()方法訪問時,那麼該過濾器就不會被呼叫。
-
INCLUDE:如果目標資源是通過RequestDispatcher的include()方法訪問時,那麼該過濾器將被呼叫。除此之外,該過濾器不會被呼叫。
-
FORWARD:如果目標資源是通過RequestDispatcher的forward()方法訪問時,那麼該過濾器將被呼叫,除此之外,該過濾器不會被呼叫。
-
ERROR:如果目標資源是通過宣告式異常處理機制呼叫時,那麼該過濾器將被呼叫。除此之外,過濾器不會被呼叫。
思考
Filter 執行順序
- web.xml
按照寫的順序執行。如果想調整,直接調整順序即可。
- 基於註解
沒有看到可以指定 order 順序。猜測是程式碼直接掃描註解。
- 混合
同上。
責任鏈模式
這裡是責任鏈模式一種非常經典的引用。
ps: mybatis 攔截器也是類似的道理。
我們在寫自己的框架時候,也可以參考這種方式。
參考資料
《Head First Servlet & JSP》