Java後端自頂向下方法——過濾器與監聽器

Java後端自頂向下方法——過濾器與監聽器

(一)過濾器有什麼用

當客戶端發出Web資源的請求時,Web服務器根據應用程序配置文件設置的過濾規則進行檢查,若客戶請求滿足過濾規則,則對客戶請求/響應進行攔截,對請求頭和請求數據進行檢查或改動,並依次通過過濾器鏈,最後把請求/響應交給請求的Web資源處理。請求信息在過濾器鏈中可以被修改,也可以根據條件讓請求不發往資源處理器,並直接向客戶機發回一個響應。當資源處理器完成了對資源的處理後,響應信息將逐級逆向返回。同樣在這個過程中,用戶可以修改響應信息,從而完成一定的任務。

在這裏插入圖片描述

很顯然,過濾器就像一個擋在客戶端和Servlet之間的一個篩子,這個篩子很特別,他不僅可以篩選請求和響應(攔下不符合我們定義的規則的請求和響應),還可以對請求和響應的內容進行修改。總之,只要記住過濾器就像一個篩子就行,然後記住這個篩子的位置:客戶端和Servlet之間。一個過濾器可以附加到一個或者多個Servlet上,一個Servlet可以附加一個或者多個過濾器。

當我們的業務邏輯越來越複雜時,一種篩子可能就不太夠用了,我們可能就需要多種篩子,幫助我們依據不同的規則進行篩選。那麼我們的多種過濾器之間的順序改如何確定呢?(PS:初學者可能會感覺過濾器順序的不同不會造成不同的篩選結果,其實這種想法不完全對,只能說存在不造成影響結果的情況,但是也有可能會造成影響,所以過濾器之間的順序是要好好考慮來確定的)我們就要用到過濾器鏈。

(二)過濾器與過濾器鏈

在這裏插入圖片描述
過濾器鏈十分好理解,服務器會按照過濾器定義的先後順序組裝成一條鏈。執行過程如下:執行第一個過濾器的chain.doFilter()之前的代碼,執行第二個過濾器的chain.doFilter()之前的代碼,執行Servlet中的service方法,執行第二個過濾器的chain.doFilter()之後的代碼,執行第一個過濾器的chain.doFilter()之後的代碼,最後返回響應給客戶端。

這邊的執行過程非常關鍵,核心就是這個chain.doFilter()方法,簡單來說,這個方法的作用就是跳轉到下一個過濾器,這個很重要。因此我們就很容易理解,我們的請求只經歷了chain.doFilter方法前面的代碼的篩選,而chain.doFilter()方法後面的代碼是來篩選響應的,就像圖上畫的那樣。

另外還有一點要注意,就是篩選請求和篩選響應的順序是相反的,有點像對棧操作的先進後出(FILO)原則,這一點在開發時一定要格外注意。相信瞭解Koa的朋友一看就發現(沒聽說過Koa的就當我沒說。。。),這不是Koa中的中間件嗎?確實,這兩個東西基本上是一樣的功能,而Koa中間件的最著名的一張圖,就是洋蔥模型圖:

在這裏插入圖片描述
有了這張圖我們就能更好的理解過濾器的執行過程了,衆所周知,洋蔥是一層一層的,每一層就相當於一個過濾器,請求穿過一層層(一旦遇到chain.doFilter()方法就會穿到下一層)的過濾器到達洋蔥中心(Servlet),然後響應再穿過一層層的過濾器到達外面(客戶端),很明顯,處理請求的時的第一層在處理響應時已經是最後一層了,是不是很直觀?放個gif動圖給用過Koa的朋友們複習一下:

在這裏插入圖片描述
這裏有四個中間件,順序爲從上到下,裏面的yield next就相當於我們過濾器中的chain.doFilter()方法,我們可以很清晰的看出跳轉的順序,這與我們的過濾器鏈的順序完全一致。

(三)過濾器的註解配置

一個過濾器必須實現javax.servlet.Filter接口並實現其中的三個方法:

  1. init()方法:這個方法在實例化過濾器時被調用,容器爲這個方法傳遞一個FilterConfig對象,其中包含配置信息。
  2. doFilter()方法:這個方法用於處理請求和相應,是過濾器的核心。它接受3個輸入參數:ServletRequest、ServletReponse、FilterChain對象。
  3. destroy()方法:該方法由容器在銷燬過濾器實例之前調用。

下面我們來看一個例子,我們用到了一個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;

@WebServlet(name = "TestServlet", value = "/test")
public class TestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("Servlet調用了");
    }
}
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(filterName = "Filter1", servletNames = "TestServlet")
public class Filter1 implements Filter {
    public void destroy() {
        System.out.println("Filter1銷燬了");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        System.out.println("Filter1過濾請求...");
        chain.doFilter(req, resp);
        System.out.println("Filter1過濾響應...");
    }

    public void init(FilterConfig config) throws ServletException {
        System.out.println("Filter1初始化了");
    }
}
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(filterName = "Filter2", servletNames = "TestServlet")
public class Filter2 implements Filter {
    public void destroy() {
        System.out.println("Filter2銷燬了");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        System.out.println("Filter2過濾請求...");
        chain.doFilter(req, resp);
        System.out.println("Filter2過濾響應...");
    }

    public void init(FilterConfig config) throws ServletException {
        System.out.println("Filter2初始化了");
    }
}

我們這裏的配置都是基於註解的,我們會發現我們的過濾器的註解裏面寫了兩個參數,一個是過濾器名稱,一個是掛載的Servlet的名稱。過濾器名稱沒啥好講的,這個掛載的Servlet名稱是指我們的過濾器要掛載在哪個Servlet上,也就是說發送到這個Servlet的請求會被我們指定的過濾器過濾。那麼問題來了,如果我們的Servlet很多,比如有三百個,是不是要把這三百個Servlet的名字都寫上去呢?當然不是!

我們可以將註解中的servletName屬性換成urlPattern屬性,在裏面我們可以用正則表達式來指定我們需要過濾的API,比如我們可以讓他過濾全部的API,相當於掛載到所有的Servlet上,我們就可以指定urlPattern = “/*”,這樣就簡單多了。

至於過濾器在過濾器鏈中的順序,@WebFilter註解沒有提供對應的屬性可以設置(網傳的通過字母序來排序那個是假的。。。大家千萬不要相信,如果遇到成功的案例僅爲巧合),也就是說我們只有通過傳統的XML配置文件方式才能指定過濾器順序。

(四)過濾器的XML配置

雖然我個人不是特別喜歡XML配置文件的方式,但是爲了指定過濾器的順序,我們只能這樣做。(希望早日能給@WebFilter加上指定順序的屬性)

我們把上面那個例子的過濾器的註解去掉,然後編輯web.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
	http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <filter>
    <filter-name>Filter1</filter-name>
    <filter-class>Filter1</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>Filter1</filter-name>
    <servlet-name>TestServlet</servlet-name>
  </filter-mapping>

  <filter>
    <filter-name>Filter2</filter-name>
    <filter-class>Filter2</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>Filter2</filter-name>
    <servlet-name>TestServlet</servlet-name>
  </filter-mapping>
</web-app>

配置Filter很簡單,和註解類似,我們先要有過濾器,然後再指定他要過濾的請求對應的Servlet

在配置文件中指定順序非常簡單,配置在前面的就會排在前面,就像這裏,Filter1就排在Filter前面。我們調用這個Servlet,我們就會得到我們想要的結果:

Filter1初始化了
Filter2初始化了
Filter1過濾請求...
Filter2過濾請求...
Servlet調用了
Filter2過濾響應...
Filter1過濾響應...

注意兩個過濾器調用的順序,是不是和前面講的一樣?還有一點要強調的是,許多人會把過濾器和攔截器傻傻分不清,其實這兩個東西區別真的很大的,雖然實現的功能好像看起來差不多,但是過濾器基於Servlet容器,底層是回調函數實現的,而攔截器基於Spring容器,底層是反射實現的。因此他們兩個有本質的區別,不能混爲一談,相比較而言,攔截器的功能更強,他可以做所有過濾器能做的事。

(五)監聽器有什麼用

監聽器就是一個實現特定接口的普通java程序,這個程序專門用於監聽另一個java對象的方法調用或屬性改變,當被監聽對象發生上述事件後,監聽器某個方法將立即被執行。

聽起來好像很厲害,我們先來看看Servlet給我們提供的常用的六個監聽器,分別是HttpSessionListener、ServletContextListener、ServletRequestListener、ServletContextAttributeListener、HttpSessionAttributeListener和ServletRequestAttributeListener。其實我們從他們的名字上就能看出一點規律,我們下面先分一下類:

監聽器 監聽內容
ServletRequestListener、ServletRequestAttributeListener request(請求)
HttpSessionListener、HttpSessionAttributeListener session(會話)
ServletContextListener、ServletContextAttributeListener context(上下文)

首先我們來理解一下這三個名詞——請求、會話和上下文。首先,請求是這三個裏面最小的單位,就是指我們發送HTTP請求然後收到響應的過程。其次,稍微大一點的是會話,比如我們登陸了一個網頁,服務器會保存你的一些信息在一個session對象中,然後退出登錄之後,這個會話就會被服務器清除。很顯然,在一個會話中我們可以多次發出請求,而每一個用戶都會獲得一個單獨的會話。最後,上下文是最大的單位,代表了整個應用程序,裏面包含了多個會話,就像一個網頁可以被多個用戶同時訪問一樣。

這三個對象對應了三個作用域,也就是說他們都是用來保存一些我們需要用到的屬性的存儲空間而已,存儲方式是常見的key-value對。請求對象保存的屬性僅在本請求中可見,會話對象保存的屬性在會話內所有請求均可見,上下文對象保存的屬性在本應用程序所有的會話和請求中均可見。

然後我們再來看這些監聽器,一共三類,每一類裏面有兩個,其中一個只是比另一個多了“Attribute”(屬性)。其實不帶“Attribute”的監聽的東西比較宏觀,帶“Attribute”的監聽的東西比較微觀,甚至還可以修改屬性。下面我們來一一介紹:

(六)監聽器分類

1. Request監聽器

ServletRequestListener接口:用於對Request請求進行監聽(創建、銷燬)。

public void requestInitialized(ServletRequestEvent sre);//request初始化
public void requestDestroyed(ServletRequestEvent sre);//request銷燬
public ServletRequest getServletRequest();//取得一個ServletRequest對象
public ServletContext getServletContext();//取得一個ServletContext(application)對象

ServletRequestAttributeListener接口:對Request屬性的監聽(增刪改屬性)。

public void attributeAdded(ServletRequestAttributeEvent srae);//增加屬性
public void attributeRemoved(ServletRequestAttributeEvent srae);//屬性刪除
public void attributeReplaced(ServletRequestAttributeEvent srae);//屬性替換(第二次設置同一屬性)
public String getName();//得到屬性名稱
public Object getValue();//取得屬性的值

2. Session監聽器

HttpSessionListener接口:對Session的整體狀態的監聽。

public void sessionCreated(HttpSessionEvent se);//session創建
public void sessionDestroyed(HttpSessionEvent se);//session銷燬
public HttpSession getSession();//取得當前操作的session

HttpSessionAttributeListener接口:對session的屬性監聽。

public void attributeAdded(HttpSessionBindingEvent se);//增加屬性
public void attributeRemoved(HttpSessionBindingEvent se);//刪除屬性
public void attributeReplaced(HttpSessionBindingEvent se);//替換屬性
public String getName();//取得屬性的名稱
public Object getValue();//取得屬性的值
public HttpSession getSession();//取得當前的session

3. Context監聽器

ServletContextListener:用於對Servlet整個上下文進行監聽(創建、銷燬)。

public void contextInitialized(ServletContextEvent sce);//上下文初始化
public void contextDestroyed(ServletContextEvent sce);//上下文銷燬
public ServletContext getServletContext();//取得一個ServletContext(application)對象

ServletContextAttributeListener:對Servlet上下文屬性的監聽(增刪改屬性)。

public void attributeAdded(ServletContextAttributeEvent scab);//增加屬性
public void attributeRemoved(ServletContextAttributeEvent scab);//屬性刪除
public void attributeRepalced(ServletContextAttributeEvent scab);//屬性替換(第二次設置同一屬性)
public String getName();//得到屬性名稱
public Object getValue();//取得屬性的值

監聽器的使用和Servlet和Filter的使用非常類似,都是實現接口然後重寫對應的方法,就可以實現相應的功能了,上面列出了六個接口中的方法。

(七)監聽器實際案例

下面我們來做一個小案例,將監聽器和三個作用域(request、session、context)的內容來總結一下。首先我們先來看看我們要做什麼,我們的需求是統計網頁當前在線的人數,並且記錄用戶的sessionID,IP地址和登錄時間這三個信息。因此,我們需要一個類來保存上述的信息,我們就先寫這個實體類。

package entity;

public class User {
    private String sessionID;
    private String ip;
    private String firstTime;

    public String getSessionID() {
        return sessionID;
    }

    public void setSessionID(String sessionID) {
        this.sessionID = sessionID;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String getFirstTime() {
        return firstTime;
    }

    public void setFirstTime(String firstTime) {
        this.firstTime = firstTime;
    }

    @Override
    public String toString(){
        return sessionID + "\t" + ip + "\t" + firstTime;
    }
}

然後我們要用一個鏈表在存儲這些保存了用戶數據的對象,但是有一點要注意,我們如何保證鏈表內不會有重複的對象?我們判斷多次請求是否來自同一個用戶的依據是什麼?前面講過,每個用戶的一次會話中的所有請求用的是同一個session對象,並且有相同的sessionID。用戶的請求中正好帶了這個sessionID,也就是說我們可以以這個sessionID爲依據,所有sessionID相同的請求都被認爲來自於同一個用戶。我們可以寫一個幫助類,幫我們找到鏈表內指定sessionID的User對象。

package util;

import java.util.ArrayList;
import entity.User;

public class SessionUtil {
    public static User getUserBySessionID(ArrayList<User> userList, String sessionID) {
        for (User user : userList) {
            if (user.getSessionID().equals(sessionID)) {
                return user;
            }
        }
        return null;
    }
}

根據上面講到的思路,我們可以寫下監聽請求的監聽器:

package listenner;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import entity.User;
import util.SessionUtil;

@WebListener
public class MyServletRequestListener implements ServletRequestListener {
    @SuppressWarnings("unchecked")
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        ArrayList<User> userList = null;
        //獲取上下文對象中的鏈表,若不存在,則創建一個鏈表
        userList= (ArrayList<User>) servletRequestEvent.getServletContext().getAttribute("userList");
        if (userList == null) {
            userList = new ArrayList<User>();
        }
        //獲取請求對象
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletRequest();
        //通過獲取會話對象拿到sessionID
        String sessionID = httpServletRequest.getSession().getId();
        //通過sessionID在鏈表中查找對應的User對象,若不存在,則創建並加入到鏈表中
        if (SessionUtil.getUserBySessionID(userList, sessionID) == null) {
            User user = new User();
            user.setSessionID(sessionID);
            user.setFirstTime(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
            user.setIp(httpServletRequest.getRemoteAddr());
            userList.add(user);
        }
        //將鏈表保存到上下文對象中,便於多地使用
        servletRequestEvent.getServletContext().setAttribute("userList", userList);
    }
}

另外,光是添加User對象肯定是不行的,因爲用戶可能還會登出下線,因此我們就需要把這些人從在線人數中扣除,相應的用戶信息也要一併刪除。因此,我們還需要監聽session的銷燬(我們這個案例中沒有提供手動銷燬session的方法,只能依賴系統根據session過期時間自動銷燬,Tomcat中默認的session過期時間是三十分鐘)。

package listenner;

import java.util.ArrayList;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import entity.User;
import util.SessionUtil;

@WebListener
public class MyHttpSessionListener implements HttpSessionListener {
    private int num = 0; //統計人數

    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
    	//創建一個session,人數加一
        num++;
        httpSessionEvent.getSession().getServletContext().setAttribute("number", num);
    }

    @SuppressWarnings("unchecked")
    public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
    	//銷燬一個session,人數減一
        num--;
        httpSessionEvent.getSession().getServletContext().setAttribute("number", num);
        //在此用戶被銷燬的時候,將鏈表中對應的用戶對象刪除
        ArrayList<User> userList = (ArrayList<User>) httpSessionEvent.getSession().getServletContext().getAttribute("userList");
        if (SessionUtil.getUserBySessionID(userList, httpSessionEvent.getSession().getId()) != null) {
            userList.remove(SessionUtil.getUserBySessionID(userList, httpSessionEvent.getSession().getId()));
        }
    }
}

其實你會發現,如果我們只要當前在線人數,只監聽session就可以了。因爲這裏的session創建和銷燬已經可以滿足我們的需要,但我們還需要用戶IP地址等信息,所以光監聽session是不夠的,我們還需要監聽請求。

最後就是我們的Servlet,Servlet乾的事很簡單,接收請求後打印當前在線人數和用戶信息:

package controller;

import entity.User;
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;
import java.util.ArrayList;

@WebServlet(name = "MyServlet", value = "/test")
public class MyServlet extends HttpServlet {
    @SuppressWarnings("unchecked")
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ArrayList<User> userList = (ArrayList<User>) request.getSession().getServletContext().getAttribute("userList");
        int number = (int) request.getSession().getServletContext().getAttribute("number");
        System.out.println("當前在線人數:" + number);
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

我們來測試一下,首先我用我的手機請求,控制檯上打印瞭如下內容:

當前在線人數:4
CA2B5BF8BABBA786717411F6731E7B4F	127.0.0.1	2020-05-25 09:48:37
E40ACB667614F7C249C41F205960576A	127.0.0.1	2020-05-25 09:48:37
222DAF40FB4AC0FF83FB1CA872EC7C03	0:0:0:0:0:0:0:1	2020-05-25 09:48:37
773BE8633271CA60A03B1F7F92B43A64	192.168.1.104	2020-05-25 09:48:55

然後我用另一臺電腦再次請求,控制檯上打印瞭如下內容:

當前在線人數:5
CA2B5BF8BABBA786717411F6731E7B4F	127.0.0.1	2020-05-25 09:48:37
E40ACB667614F7C249C41F205960576A	127.0.0.1	2020-05-25 09:48:37
222DAF40FB4AC0FF83FB1CA872EC7C03	0:0:0:0:0:0:0:1	2020-05-25 09:48:37
773BE8633271CA60A03B1F7F92B43A64	192.168.1.104	2020-05-25 09:48:55
88EB3378C10EBB84BC3C9131B9BAC6A9	192.168.1.106	2020-05-25 09:49:14

前三條信息我們暫時先不管。我們可以看到,這樣我們就實現了我們需求的基本的功能,IP地址和時間也是正確的,我的手機的IP地址是192.168.1.104,我的另一臺電腦的IP地址是192.168.1.106。然後可以再試試,用我的手機和電腦重複發出請求,上述內容無變化,證明我們成功的識別了來自同一個用戶的多次請求。

以上就是過濾器和監聽器的全部內容了。這一章內容好像講的有點多了(早知道我把過濾器和監聽器分開講了),不過代碼部分還是要仔細摸索的,尤其是監聽器部分和三個作用域,內容可能會有一點抽象,仔細的思考一下也不算是太難。

2020年5月25日

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章