JavaWeb高階程式設計(九)—— 使用過濾器改進應用程式
一、瞭解過濾器
過濾器是可以攔截訪問資源的請求、資源的響應或者同時攔截兩者的應用元件,它們將以某種方式作用於這些請求或響應。過濾器可以檢測和修改請求或響應,它們甚至可以拒絕、重定向或轉發請求。如同Servlet一樣,過濾器可以在部署描述符中以程式設計或者宣告的方式進行宣告,它們可以有初始化引數,並且可以訪問ServletContext。
二、建立、宣告、對映過濾器
過濾器在初始化時將呼叫init方法,它可以訪問過濾器的配置、初始化引數和ServletContext。當請求進入到過濾器中時,doFilter方法將會被呼叫,在doFilter之中,可以拒絕請求或者呼叫FilterChain物件的doFilter方法;可以修改請求或響應;可以封裝請求或響應物件。應用程式在關閉時,將呼叫過濾器的destroy方法。
1、瞭解過濾器鏈
儘管只有一個Servlet可以處理請求,但可以使用許多過濾器攔截請求。呼叫FilterChain.doFilter()方法將觸發過濾器鏈的持續執行,如果當前處理器是過濾器鏈的最後一個過濾器,那麼呼叫FilterChain.doFilter()方法將把控制權返回到Servlet容器中,它將把請求傳遞給Servlet。如果當前的過濾器沒有呼叫FilterChain.doFilter()方法,那麼過濾器鏈將會被中斷,Servlet和所有剩餘的過濾器都無法再處理該請求。
過濾器鏈的工作原理:
過濾器鏈的這種工作方式非常像棧(確實,一系列方法的執行都執行在Java棧上)。當請求來臨時,它將首先進入第一個過濾器,該過濾器將被新增到棧中。當過濾器鏈繼續執行時,下一個過濾器將被新增到棧中。一直到請求進入到Servlet中,它是被新增到棧中的最後一個元素。當請求完成並且Servlet的service方法也返回時,Servlet將從棧中移除,然後控制權被返回到最後一個過濾器,當它的doFilter方法返回時,該過濾器將從棧中移除,控制也將返回到之前的過濾器中。一直到控制權返回到第一個過濾器中,當它的doFilter方法也返回時,它將被從棧中移除,此時棧是空的,請求處理也就完成了。因此,過濾器可以在目標Servlet處理請求的前後執行某些操作。
2、對映到URL模式和Servle名稱
同Servlet一樣,過濾器可以對映到URL模式,這會決定哪個或哪些過濾器攔截某個請求。任何匹配某個過濾器的URL模式的請求在被匹配的Servlet處理之前將首先進入該過濾器。通過使用URL模式,我們不止可以攔截Servlet的請求,還可以攔截其他資源,例如圖片、CSS檔案、JavaScript檔案等。
與對映到URL上相反,我們可以把它對映到一個或多個Servlet名稱,如果請求匹配於某個Servlet,容器將尋找所有匹配該Servlet名稱的過濾器,並將它們應用到請求上。
3、對映到不同的請求派發器型別
① 普通請求
這些請求來自於客戶端,幷包含了容器中特定Web應用程式的目標URL;
② 轉發請求
當代碼呼叫RequestDispatcher的forward方法或者使用<jsp:forward>標籤時將觸發這些請求;
③ 包含請求
使用<jsp:include>標籤或者呼叫RequestDispatcher的include方法時,將會產生一個不同的、與原始請求相關的內部請求;
④ 錯誤資源請求
這些是訪問處理HTTP錯誤的錯誤頁面的請求。
④ 非同步請求
這些請求是在處理任何其他請求的過程中,由AsyncContext派發的請求。
注意:在Servlet3.0中,Servlet的service方法將在請求的響應傳送之前返回,所以過濾器鏈的能力受到了損害。為了作出補償,Servlet3.0為AsyncContext派發的過濾器攔截請求添加了新的非同步派發型別。實現非同步過濾器要小心,因為它們可以被單個非同步請求呼叫多次(多個不同的執行緒)。
4、使用部署描述符配置過濾器
下面看一個簡單的過濾器在部署描述符中的配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.mengfei.hellofilter.filter.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/do/*</url-pattern>
<url-pattern>/bur</url-pattern>
<servlet-name>myServlet</servlet-name>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>com.mengfei.hellofilter.servlet.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/index</url-pattern>
</servlet-mapping>
</web-app>
此時,過濾器會處理所有相對於應用程式的URL——/do/*和/bur的請求,以及任何最終由myServlet處理的請求。有效的<dispatcher>型別有REQUEST、FORWARD、INCLUDE、ERROR、ASYNC,過濾器可以對映0個或多個<dispatcher>元素,如果沒有指定,那麼預設的就是REQUEST。
與Servlet不同的是,過濾器不可以在第一個請求到達時載入,過濾器的init()方法總是在應用程式啟動時呼叫,在ServletContextListener初始化之後,Servlet初始化之前,它們將按照部署描述符中出現的順序依次載入。
5、使用註解配置過濾器
示例如下:
@WebFilter(
filterName = "myFilter",
urlPatterns = {"/do/*","/bur"},
servletNames = {"myServlet"},
dispatcherTypes = {DispatcherType.REQUEST,DispatcherType.ASYNC}
)
public class MyFilter implements Filter
使用註解宣告和對映過濾器的主要缺點是:不能對過濾器鏈上的過濾器進行排序。為了使用過濾器正確執行,特定的執行順序是很重要的,如果希望在不使用部署描述符的情況下,控制過濾器執行的順序,那麼需要使用程式設計式配置。
6、使用程式設計式配置過濾器
示例如下:
@WebListener
public class Configurator implements ServletContextListener{
@Override
public void contextInitialized(ServletContextEvent event) {
//獲取正在啟動的Servlet容器
ServletContext context = event.getServletContext();
//在容器中新增過濾器,動態返回該過濾器的登記表
FilterRegistration.Dynamic registration = context.addFilter("myFilter", new MyFilter());
//在登記表中新增URL對映資訊
registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST,DispatcherType.ASYNC),
false,"/do/*","/bur");
//在登記表中新增過濾的Servlet
registration.addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST,DispatcherType.ASYNC),
false,"myServlet");
}
}
如上,在新增URL或Servlet對映時,第一個引數是DispatcherType的物件集合,如果為空則預設為REQUEST;第二個引數表示相對於於部署描述符中過濾器的順序,如果使用的引數為false,那程式設計式的過濾器將在部署描述描述符中的過濾器之前載入,如果為true,那麼部署描述符中的過濾器將會優先載入;最後一個引數指定了對映的URL或Servlet名稱。
三、過濾器排序
過濾器順序決定了過濾器在過濾鏈中出現的位置,這反過來也決定了過濾器什麼時候處理請求。由於使用註解時無法對過濾器進行排序,所以企業級應用中一般採用部署描述符或者程式設計式配置,而不會使用註解配置過濾器。
1、URL模式對映和Servlet名稱對映對排序的影響
匹配請求的過濾器將按照它們出現在部署描述符或者程式設計式配置中的順序新增到過濾器鏈中,如果同時在部署描述符或程式設計式配置中設定了一些過濾器,那麼需要在程式設計式配置中使用addMapping*方法的第2個引數,決定程式設計式對映是否應該出現在XML對映之前。另外,URL對映的過濾器優先順序比Servlet名稱對映的過濾器優先順序更高,它與在部署描述符中出現的順序無關。
2、演示過濾器順序
在專案中建立一個Servlet和三個Filter,然後其中一個Filter使用Servlet的URL對映進行配置,其他兩個單獨配置URL,分別在Filter和Servlet中輸出一句話,最後啟動專案,訪問:http://localhost:8082/order/do/test,示例如下:
<servlet>
<servlet-name>servletOne</servlet-name>
<servlet-class>com.mengfei.hellofilter.servlet.ServletOne</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletOne</servlet-name>
<url-pattern>/order/do/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>filterThree</filter-name>
<filter-class>com.mengfei.hellofilter.filter.FilterThree</filter-class>
</filter>
<filter-mapping>
<filter-name>filterThree</filter-name>
<servlet-name>servletOne</servlet-name>
</filter-mapping>
<filter>
<filter-name>filterTwo</filter-name>
<filter-class>com.mengfei.hellofilter.filter.FilterTwo</filter-class>
</filter>
<filter-mapping>
<filter-name>filterTwo</filter-name>
<url-pattern>/order/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>filterOne</filter-name>
<filter-class>com.mengfei.hellofilter.filter.FilterOne</filter-class>
</filter>
<filter-mapping>
<filter-name>filterOne</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
列印結果如下:
請求到達過濾二
請求到達過濾一
請求到達過濾三
請求已到達ServletOne
3、使用過濾器處理非同步請求
可以參考:http://www.wrox.com/WileyCDA/WroxTitle/Professional-Java-for-Web-Applications.productCd-1118656466,descCd-DOWNLOAD.html 連結中的chapter9 Click to Download 下載包中的Filter-Async專案
四、調查過濾器中的實際用例
1、一個簡單的日誌過濾器
web.xml配置如下:
<!--配置一個簡單的日誌過濾器-->
<filter>
<filter-name>logFilter</filter-name>
<filter-class>com.mengfei.hellofilter.filter.LogFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>logFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
LogFilter內容如下:
package com.mengfei.hellofilter.filter;
import org.apache.commons.lang3.time.StopWatch;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
/**
* author Alex
* date 2018/11/11
* description 一個簡單的日誌過濾器
*/
public class LogFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
//Instant是JDK8中的一個代表時間刻的類,參考:https://www.cnblogs.com/sbj-dawn/p/7439953.html
//Instant now = Instant.now();
LocalDateTime now = LocalDateTime.now();
//StopWatch是lang3中一個任務執行時間監視器,參考:https://blog.csdn.net/u012191627/article/details/59484658
StopWatch timer = new StopWatch();
try {
timer.start();
filterChain.doFilter(servletRequest,servletResponse);
}finally {
//將列印日誌放到finally塊中,這樣即便出現異常也可以照樣列印日誌
timer.stop();
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
String length = response.getHeader("Content-Length");
if(null == length || length.length() == 0){
length = "-";
}
System.out.println(
request.getRemoteAddr() + " - - [" + now + "]" + " \""
+ request.getMethod() + " " + request.getRequestURI() + " "
+ request.getProtocol() + "\" " + response.getStatus() + " "
+ length + " " + timer
);
}
}
@Override
public void destroy() {
}
}
2、使用過濾器壓縮響應內容
由於響應資料可以在Servlet完成請求處理之前返回到客戶端,在非同步請求處理的情況下,它還可以在Servlet完成請求處理之後返回到客戶端。因此,如果希望修改響應內容,必須在傳遞響應物件到過濾器鏈之前對它進行封裝。
web.xml配置內容如下:
<filter>
<filter-name>gzipFilter</filter-name>
<filter-class>com.mengfei.hellofilter.filter.GzipFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>gzipFilter</filter-name>
<url-pattern>/gzip</url-pattern>
</filter-mapping>
GzipServlet的內容如下:
package com.mengfei.hellofilter.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* author Alex
* date 2018/11/12
* description 一個轉發壓縮請求的Servlet
*/
@WebServlet(
name = "gzipServlet",
urlPatterns = {"/gzip"}
)
public class GzipServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
response.getOutputStream()
.println("This is GzipServlet");
}
}
GzipFilter的內容如下:
package com.mengfei.hellofilter.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;
/**
* author Alex
* date 2018/11/11
* description 一個壓縮響應內容的過濾器
*/
public class GzipFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
if(((HttpServletRequest)request).getHeader("Accept-Encoding")
.contains("gzip"))
{
System.out.println("Encoding requested.");
((HttpServletResponse)response).setHeader("Content-Encoding", "gzip");
ResponseWrapper wrapper =
new ResponseWrapper((HttpServletResponse)response);
try
{
chain.doFilter(request, wrapper);
}
finally
{
try {
wrapper.finish();
} catch(Exception e) {
e.printStackTrace();
}
}
}
else
{
System.out.println("Encoding not requested.");
chain.doFilter(request, response);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void destroy() { }
private static class ResponseWrapper extends HttpServletResponseWrapper
{
private GZIPServletOutputStream outputStream;
private PrintWriter writer;
public ResponseWrapper(HttpServletResponse request)
{
super(request);
}
@Override
public synchronized ServletOutputStream getOutputStream()
throws IOException
{
if(this.writer != null)
throw new IllegalStateException("getWriter() already called.");
if(this.outputStream == null)
this.outputStream =
new GZIPServletOutputStream(super.getOutputStream());
return this.outputStream;
}
@Override
public synchronized PrintWriter getWriter() throws IOException
{
if(this.writer == null && this.outputStream != null)
throw new IllegalStateException(
"getOutputStream() already called.");
if(this.writer == null)
{
this.outputStream =
new GZIPServletOutputStream(super.getOutputStream());
this.writer = new PrintWriter(new OutputStreamWriter(
this.outputStream, this.getCharacterEncoding()
));
}
return this.writer;
}
@Override
public void flushBuffer() throws IOException
{
if(this.writer != null)
this.writer.flush();
else if(this.outputStream != null)
this.outputStream.flush();
super.flushBuffer();
}
@Override
public void setContentLength(int length) { }
@Override
public void setContentLengthLong(long length) { }
@Override
public void setHeader(String name, String value)
{
if(!"content-length".equalsIgnoreCase(name))
super.setHeader(name, value);
}
@Override
public void addHeader(String name, String value)
{
if(!"content-length".equalsIgnoreCase(name))
super.setHeader(name, value);
}
@Override
public void setIntHeader(String name, int value)
{
if(!"content-length".equalsIgnoreCase(name))
super.setIntHeader(name, value);
}
@Override
public void addIntHeader(String name, int value)
{
if(!"content-length".equalsIgnoreCase(name))
super.setIntHeader(name, value);
}
public void finish() throws IOException
{
if(this.writer != null)
this.writer.close();
else if(this.outputStream != null)
this.outputStream.finish();
}
}
private static class GZIPServletOutputStream extends ServletOutputStream
{
private final ServletOutputStream servletOutputStream;
private final GZIPOutputStream gzipStream;
public GZIPServletOutputStream(ServletOutputStream servletOutputStream)
throws IOException
{
this.servletOutputStream = servletOutputStream;
this.gzipStream = new GZIPOutputStream(servletOutputStream);
}
@Override
public boolean isReady()
{
return this.servletOutputStream.isReady();
}
@Override
public void setWriteListener(WriteListener writeListener)
{
this.servletOutputStream.setWriteListener(writeListener);
}
@Override
public void write(int b) throws IOException
{
this.gzipStream.write(b);
}
@Override
public void close() throws IOException
{
this.gzipStream.close();
}
@Override
public void flush() throws IOException
{
this.gzipStream.flush();
}
public void finish() throws IOException
{
this.gzipStream.finish();
}
}
}
請求地址和請求結果展示如下:
檢視Chrome的請求和響應資訊,如下圖:
五、使用過濾器簡化登入認證
web.xml配置如下:
<!--配置登入驗證的過濾器-->
<filter>
<filter-name>loginFilter</filter-name>
<filter-class>com.mengfei.hellofilter.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loginFilter</filter-name>
<url-pattern>/login</url-pattern>
</filter-mapping>
LoginFilter的內容如下:
package com.mengfei.hellofilter.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* author Alex
* date 2018/11/12
* description 一個簡化登入驗證的過濾器
*/
public class LoginFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpSession session = ((HttpServletRequest) servletRequest).getSession();
if(null == session || null == session.getAttribute("username")){
((HttpServletResponse)servletResponse).sendRedirect("login.jsp");
}else {
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void destroy() {
}
}