Filter過濾器、ThreadLocal
尚硅谷JavaWeb筆記-10

Filter過濾器

用來修飾請求或響應

  • 在Web資源被訪問前,檢查request物件,修改請求頭和請求正文,或對請求進行預處理操作

  • 將請求傳遞到下一個過濾器或目標資源

  • 在Web資源被訪問後,檢查response物件,修改回應頭和回應正文

  • 常用來做權限控管、設定統一編碼等等

方法

Filter都是接口,用起來跟servlet一樣需要實現方法

前面都是偷雞造一個base去實現基本的方法然後繼承

  • init (FilterConfig filterConfig):初始化,由web容器調用

  • doFilter(ServletRequest request,SeivletResponse response, FilterChain chain):主要方法,在這邊對req或resp毛手毛腳,或是chain.doFilter再傳到下一個過濾器

  • destroy():釋放被這個filter物件占用的資源,由web容器調用

生命週期

  • 構造、init():跟隨web工程而啟動,被執行一次
  • doFilter():在每次攔截請求都調用
  • destrory():跟隨web工程結束而消亡,也只執行一次

FilterConfig

跟Servlet基本一樣

  • 每個Filter都有自己伴生的FilterConfig實例物件,當web容器啟動時一併被創造出來,其中包含了它Filter的訊息

  • filterconfig.getFilterName():

  • filterconfig.getInitParameter():取得初始參數(例如寫在web.xml中的<init-param>

  • filterconfig.getServletContext():也能獲得ServletContext

    • 記住取出的ServletContext context是整個web工程共用的唯一一個
    • 複習:可以用來從url網址取到對應的硬碟路徑, 例如context.getRealPath("/")
    • 也可以當map用存setAttribute,與取getAttribute

FilterChain

  • 串聯多個過濾器,直到沒有chain.doFilter(),就調用目標資源的service()方法
  • 它的執行順序,類似把doFilter()當作中隔的中序遍歷
    • F1上半部-F2上半部-資源-F2下半部-F1下半部
  • F1、F2的順序是由web.xml中標籤上下所決定
  • 如果是用註解的,無法決定順序
    • 實作上,每個過濾器都應該是獨立的,不應受順序影響功能

實作

引用 http://c.biancheng.net/servlet2/filter.html

web.xml

<filter>
    <filter-name>myFilter</filter-name>
    <filter-class>com.MyFilter</filter-class>
    <init-param>
        <param-name>name</param-name>
        <param-value>我的Filter</param-value>
    </init-param>
    <init-param>
        <param-name>URL</param-name>
        <param-value>www.MyFilter.net</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>/login</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

匹配規則

規則跟servlet一樣

規則 範例 可訪問的URL
精確 /開始,不能包含萬用字元*且必須完全匹配 /myServlet /myServlet
路徑 /開始,並以/*結尾 /user/ /user/之下都可以
後綴 *.開始,並以某種後綴結尾 *.jsp 工程目錄下所有.jsp
  • 但是要注意,servlet匹配是從上到下,有符合就交給它,剩下不管,一次必然只有一個servlet執行
  • 但filter是只要符合匹配規則就通通抓起來篩,所以可能符合多個且filter全都執行

註解

  • @WebFilter 注解也可以對篩檢程式進行配置,容器在部署應用時,會根據其具體屬性配置將相應的類部署為篩檢程式

  • @WebFilter 注解具有下表給出的一些常用屬性。以下所有屬性均為可選屬性,但 value、urlPatterns、servletNames 三者必需至少包含一個,且 value 和 urlPatterns 不能共存,如果同時指定,通常忽略 value 的取值

屬性名 類型 描述
filterName String 指定篩檢程式的 name 屬性,等價於
urlPatterns String[] 指定篩檢程式的 URL 匹配模式。等價於 標籤
value String[] 該屬性等價於 urlPatterns 屬性,但是兩者不能同時使用
servletNames String[] 指定篩檢程式將應用於哪些 Servlet。取值是 @WebServlet 中 filterName 屬性的取值,或者 web.xml 中 的取值
dispatcherTypes DispatcherType 指定篩檢程式攔截的資源被 Servlet 容器調用的方式。具體取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST
initParams WebInitParam[] 指定一組篩檢程式初始化參數,等價於 標籤
asyncSupported boolean 聲明篩檢程式是否支援非同步作業模式,等價於 標籤
description String 指定篩檢程式的描述資訊,等價於 標籤
displayName String 指定篩檢程式的顯示名,等價於 標籤
@WebFilter(filterName="FirstFilter",urlPatterns={"/first","*.do","*.jsp"})

應用至書城項目

不要導錯包,是javax的

@WebFilter(filterName = "ManagerFilter", urlPatterns = {"/pages/manager/*", "/manager/*", "/BookServlet"})
public class ManagerFilter implements Filter {
    public void init(FilterConfig config) throws ServletException {
    }

    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException
            , IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        User user = (User) httpServletRequest.getSession().getAttribute("user");
        if (user == null) {
            httpServletRequest.getRequestDispatcher("/pages/user/login.jsp").forward(request, response);
        } else if (user.getId() <= 3) {
            // 判斷為管理員ID
            System.out.println("有管理員來操作");
            chain.doFilter(request, response);
        } else {
            System.out.println("雖然登入但不是管理員");
            request.getRequestDispatcher("/pages/user/login.jsp").forward(request, response);
        }
    }
}

ThreadLocal

http://www.jasongj.com/java/threadlocal/

https://kucw.github.io/blog/2018/7/java-thread-local/

  • 用於在同一個線程中存取一個資料
  • 某方面來說可以解決線程資料安全問題,但是有區別
  • ThreadLocal適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變數在線程間隔離而在方法或類間共用的場景
  • Synchronize是應用在多線程間"共用變量"的加解鎖
  • ThreadLocal是一個線程的一個變量在"多個方法"間傳遞
    • 在方法參數都引用那個變量也可以達成,但是就需要每個方法都傳參,不夠優雅且耦合高
    • ThreadLocal底層有一個ThreadLocalMap,用set與get方法就能存取值(key固定是ThreadLocal的ID,所以只能存一個值)
    • 白話:理解為"線程域"
  • 應用場景:Session、線程池事務

使用

  • 前提是同一個線程
  • 一般都會設成static,方便跨方法資料的存取,例如
private static ThreadLocal<資料的類> 資料 = new ThreadLocal<>();

範例

用來確保一個線程使用同一個conn連接資料庫,來完成事務操作

  • JdbcUtils.java
public class JdbcUtils {
    private static DruidDataSource dataSource;
    // 造一個ThreadLocal,裡面存的是Connection
    // 我預期讓同一個線程都用同一個conn來完成資料庫的事務操作
    // 因為事務要全部提交或全部回滾所以需要是同一個conn
    private static ThreadLocal<Connection> threadLocalConn = new ThreadLocal<>();

    // 讀取設定
    static {
        Properties properties = new Properties();
        try {
            properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties"));
            dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 獲取連接,返回null說明連接失敗
    public static Connection getConnection() {
        // 從threadLocal拿連接
        Connection conn = threadLocalConn.get();
        if (conn == null) {
            // 裡面還沒存東西呢
            try {
                // 就從連接池拿一個新的連線
                conn = dataSource.getConnection();
                // 並且存到線程域中,以後都調用它
                threadLocalConn.set(conn);
                // 設置手動提交準備搞事務
                conn.setAutoCommit(false);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return conn;
    }

    /**
     * 提交事務並且關閉
     */
    public static void commitAndClose() {
        // 現在不需要把conn當作參數傳進來了,因為同一個線程我get就能取到它
        Connection conn = threadLocalConn.get();
        if (conn != null) {
            // 表示這個線程中有某dao取了連接並做了某些操作
            try {
                // 提交
                conn.commit();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            // 銷毀線程中的conn防止記憶體洩漏,
            // 雖然GC內建會做這事,但手動可以增加效率
            // 且Tomcat使用了線程池,不手動remove會直接報錯
            threadLocalConn.remove();
        }
    }

    /**
     * 回滾事務並且關閉
     */
    public static void rollbackAndClose() {
        Connection conn = threadLocalConn.get();
        if (conn != null) {
            try {
                // 回滾
                conn.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            threadLocalConn.remove();
        }
    }

    // // 關閉連接
    // public static void close(Connection conn) {
    //     if (conn != null) {
    //         try {
    //             conn.close();
    //         } catch (SQLException e) {
    //             e.printStackTrace();
    //         }
    //     }
    //
    // }
}
  • 修改baseDao中每個操作資料庫的方法,把原來的close()方法去掉,並且在出錯時手動拋出異常
public int update(String sql, Object... args) {
    Connection conn = JdbcUtils.getConnection();
    try {
        return queryRunner.update(conn, sql, args);
    } catch (SQLException e) {
        e.printStackTrace();
        // 遇到異常手動往外拋,否則事務不知道要回滾
        throw new RuntimeException(e);
    }
}
  • Service層只是簡單的調用dao,沒有拋出異常
  • 接著就到了baseServlet,這邊用反射判斷請求對應的方法轉給各個Service,也捕獲了異常,需要手動拋出
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
        IOException {
    request.setCharacterEncoding("UTF-8");
    // 判斷請求的隱藏標籤
    String action = request.getParameter("action");
    try {
        // 用反射找到方法
        Method declaredMethod = this.getClass().getDeclaredMethod(action, HttpServletRequest.class,
                HttpServletResponse.class);
        // 調用方法
        declaredMethod.invoke(this, request, response);
    } catch (Exception e) {
        e.printStackTrace();
        // 手動拋出異常給過濾器
        throw new RuntimeException(e);
    }
}
  • 最後來到過濾器,過濾條件設定"/*"讓所有人都要被過濾
  • 一個線程的所有請求、涉及的方法都要經過doFilter方法才能層層往下調用servlet > service > dao,並且我用ThreadLocal確保它都是同一個conn了,只要任何一個環節出錯,就回滾,以此完成事務操作
@WebFilter(filterName = "TransactionFilter", value = "/*")
public class TransactionFilter implements Filter {
    public void init(FilterConfig config) throws ServletException {
    }

    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException
            , IOException {
        // 因為篩選條件是/*所有人都要來這關,當然也包含了所有事務
        try {
            chain.doFilter(request, response);
            // 在此提交事務
            JdbcUtils.commitAndClose();
        } catch (Exception e) {
            // 出錯就回滾,剛剛讓baseServlet手動拋出就是為了在這邊能接到
            JdbcUtils.rollbackAndClose();
            e.printStackTrace();
        }
    }
}

Tomcat錯誤頁面

展示友好的錯誤頁面

  • 哈哈剛剛的/*過濾器還有戲,因為它是過濾所有,自然也會抓住所有異常,這裡把異常再往外拋
catch (Exception e) {
// 出錯就回滾,剛剛讓baseServlet手動拋出就是為了在這邊能接到
JdbcUtils.rollbackAndClose();
e.printStackTrace();
// 手動拋出異常給Tomcat
throw new RuntimeException(e);
}
  • web.xml
<error-page>
	<location>/pages/error/error.html</location>
</error-page>

也可以指定
<error-page>
    <error-code>
    <exception-type>
        把不同類型的error引導到對應的頁面

上次修改於 2022-01-10