SpringMVC原始碼閱讀:過濾器
1.前言
SpringMVC是目前J2EE平臺的主流Web框架,不熟悉的園友可以看 SpringMVC原始碼閱讀入門 ,它交代了SpringMVC的基礎知識和原始碼閱讀的技巧
本文將通過原始碼(基於Spring4.3.7)分析,弄清楚SpringMVC過濾器是如何執行的
2.原始碼分析
web.xml配置
<filter> <!--過濾器名稱--> <filter-name>Set Character Encoding</filter-name> <!--過濾器處理類--> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <!--初始化引數--> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>Set Character Encoding</filter-name> <!--攔截路徑--> <url-pattern>/*</url-pattern> </filter-mapping>
開啟CharacterEncoding類,該類為request設定字元編碼,開啟類繼承圖
CharacterEncodingFilter重寫父類OncePerRequestFilter的doFilterInterval方法
forceRequestEncoding為True
188行獲取編碼
191行為request設定編碼
194行為response設定編碼
197行呼叫FilterChain
開啟CharcterEncodingFilter的父類OncePerRequestFilter,OncePerRequestFilter保證請求分發執行一次Filter
CharcterEncodingFilter實現Filter介面的doFilter方法,開啟之
96行獲取屬性,該屬性表明這個方法是否被過濾
98行 hasAlreadyFilteredAttribute表明這個方法是否被過濾; skipDispatch 方法判斷是否跳過分發; shouldNotFilter 方法預設返回False,被子類重寫,然而, CharacterEncodingFilter 類並未重寫,ForwardedHeaderFilter重寫該方法,暫時不看
101行直接呼叫FilterChain的doFilter方法,FilterChain部分
98行的上述條件都不滿足,則進入105行,為request設定“已過濾”標識
107行呼叫子類CharacterEncodingFilter的doFilterInterval方法
繼續深入,開啟OncePerRequestFilter類的父類GenericFilterBean類
重點看init方法,該方法啟動服務才會進入,主要負責獲取web.xml裡配置的引數
180行獲取FilterConfig
filterClass和filterName是我們在web.xml配置的屬性
184行獲取<init-param>裡屬性
185行宣告BeanWrapper,一個通用的JavaBean介面,封裝了setget方法,在 SpringMVC原始碼閱讀:屬性編輯器、資料繫結 我已經講過
186行宣告資源載入器,載入classpath和檔案系統
189行給屬性設定True
199行 initFilterBean 方法被子類重寫,用於補充初始化方法,OncePerRequestFilter未重寫,我們暫時不看
3.自定義過濾器
現在Web專案基本都是前後端分離的,不可避免地要解決跨域問題。看Filter介面繼承圖,發現CorsFilter可以處理跨域,但是並不方便
什麼是跨域?
前端地址和後臺地址的
IP一致,埠不一致
IP不一致,埠不一致
IP不一致,埠一致
都屬於跨域範疇
前端程式碼如下
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <!-- <link rel="stylesheet" href=""> --> <style> </style> </head> <body> <script></script> <script type="text/javascript" src="jquery-3.1.1.min.js"></script> <script type="text/javascript"> $.ajax({ url: "http://localhost:8080/springmvcdemo/employee/detail/8", data: {}, success: function (data) { console.log(html) }, dataType: "json" }); </script> </body> </html>
將前端程式碼地址設定為http://localhost:1234,VSCode可以做,不贅述。後臺程式碼地址http://localhost:8080,因為埠不一致,形成了跨域,不做跨域處理,直接訪問有如下效果
現在我們來自定義過濾器解決跨域問題
3.1 自定義過濾器繼承 OncePerRequestFilter
web.xml
<filter> <!--過濾器名稱--> <filter-name>CorsFilter</filter-name> <!--過濾器處理類--> <filter-class>org.format.demo.custom.ExtendedCorsFilter</filter-class> </filter> <filter-mapping> <filter-name>CorsFilter</filter-name> <!--攔截路徑--> <url-pattern>/*</url-pattern> </filter-mapping>
ExtendedCorsFilter .java,重寫doFilterInterval方法
public class ExtendedCorsFilter extends OncePerRequestFilter{ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { response.addHeader("Access-Control-Allow-Origin", "*"); response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); response.addHeader("Access-Control-Allow-Headers", "Content-Type"); filterChain.doFilter(request,response); } }
此時再執行前端程式碼,已經成功請求到結果
Headers設定成功
3.2 自定義過濾器實現Filter介面
web.xml配置如下
<filter> <!--過濾器名稱--> <filter-name>CorsFilter</filter-name> <!--過濾器處理類--> <filter-class>org.format.demo.custom.ImplementedCorsFilter</filter-class> <!--初始化引數--> <init-param> <param-name>allowedOrigins</param-name> <param-value>*</param-value> </init-param> <init-param> <param-name>allowedMethods</param-name> <param-value>*</param-value> </init-param> <init-param> <param-name>allowedHeaders</param-name> <param-value>*</param-value> </init-param> </filter>
ImplementedCorsFilter.java
public class ImplementedCorsFilter implements Filter{ //允許的請求域 private String allowedOrigins; //允許的請求方法 private String allowedMethods; //允許的請求頭 private String allowedHeaders; @Override public void init(FilterConfig filterConfig) throws ServletException { this.allowedOrigins = filterConfig.getInitParameter("allowedOrigins"); this.allowedMethods = filterConfig.getInitParameter("allowedMethods"); this.allowedHeaders = filterConfig.getInitParameter("allowedHeaders"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse rsp = (HttpServletResponse) response; rsp.setHeader("Access-Control-Allow-Origin", allowedOrigins); rsp.setHeader("Access-Control-Allow-Methods", allowedMethods); rsp.setHeader("Access-Control-Allow-Headers", allowedHeaders); chain.doFilter(request, response); } @Override public void destroy() { } }
相比繼承OncePerRequestFilter方法,實現Filter介面可以通過重寫init方法獲取web.xml屬性值,效果一致
4.過濾器(Filter)和攔截器(Interceptor)的區別
我在 SpringMVC原始碼閱讀:攔截器 詳細講解了SpringMVC攔截器的工作原理
過濾器先於攔截器執行,後於攔截器執行結束
4.1 過濾器:
依賴於servlet容器。在實現上基於函式回撥,可以對幾乎所有請求進行過濾,但是缺點是一個過濾器例項只能在容器初始化時呼叫一次。使用過濾器的目的是用來做一些過濾操作,獲取我們想要獲取的資料.
比如:在過濾器中修改字元編碼;在過濾器中修改HttpServletRequest的一些引數,包括:過濾低俗文字、危險字元等
4.2 攔截器:
依賴於web框架,在SpringMVC中就是依賴於SpringMVC框架。在實現上基於Java的反射機制,屬於面向切面程式設計(AOP)的一種運用。由於攔截器是基於Web框架的呼叫。
因此可以使用spring的依賴注入(DI)進行一些業務操作,同時一個攔截器例項在一個controller生命週期之內可以多次呼叫。但是缺點是隻能對controller請求進行攔截,對其他的一些比如直接訪問靜態資源的請求則沒辦法進行攔截處理。
總結:業務中儘量使用基於方法的攔截器,在進行一些需要統一處理的業務可以使用基於Servlet的過濾器
5.總結:
Filter介面用來執行過濾任務
CompositeFilter實現filter,用到了組合設計模式
抽象類GenericFilterBean實現Filter介面,負責解析web.xml的Filter的init-param中引數,是所有過濾器的父類。init方法解析web.xml的引數
抽象類OncePerRequestFilter繼承GenericFilterBean,doFilter方法根據hasAlreadyFilteredAttribute判斷是否執行過濾
CharacterEncodingFilter重寫父類OncePerRequestFilter的doFilterInterval方法,呼叫FilterChain的doDilter方法執行過濾邏輯
其他過濾器園友可以自行檢視,不再贅述, demo原始碼
6.參考
https://docs.spring.io/spring/docs/current/javadoc-api/
https://github.com/spring-projects/spring-framework
文中難免有不足,歡迎指正