1. 程式人生 > >實現一個支援正則匹配的Filter以及Spring管理Filter遇到的問題

實現一個支援正則匹配的Filter以及Spring管理Filter遇到的問題

相信很多人都會對Http的Filter的url-pattern不支援正則而煩惱吧。就比如,在web專案中Struts,當我們想要對所有的或者某一個Action進行過濾,而不過濾其他的Action,尤其是不想過濾靜態資源,如果你的Struts配置了Action字尾可能會好一些,很可惜我的專案就沒有設定字尾,所有沒法使用url-pattern僅支援的幾種規則,所以就自己實現了一個抽象類,用來支援可配置需過濾以及不需過濾的url規則,且支援正則,具體原始碼可見下方。

import java.io.IOException;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 * 支援使用正則配置過濾指定URL
 * 
 * web.xml配置時加入init-params: include:配置需要過濾的url規則,支援正則,多個之間已','分割
 * exclude:配置不需要過濾的url規則,支援正則,多個之間已','分割
 * </p>
 * 
 * @author Vicky
 * @date 2015-5-13
 */
public abstract class PatternFilter implements Filter {
	protected Pattern[] includePattern = null;
	protected Pattern[] excludePattern = null;

	public final void init(FilterConfig filterconfig) throws ServletException {
		String include = filterconfig.getInitParameter("include");
		String exclude = filterconfig.getInitParameter("exclude");
		if (null != include && !"".equals(include)) {
			String[] arr = include.split(",");
			includePattern = new Pattern[arr.length];
			for (int i = 0; i < arr.length; i++) {
				includePattern[i] = Pattern.compile(arr[i]);
			}
		}
		if (null != exclude && !"".equals(exclude)) {
			String[] arr = exclude.split(",");
			excludePattern = new Pattern[arr.length];
			for (int i = 0; i < arr.length; i++) {
				excludePattern[i] = Pattern.compile(arr[i]);
			}
		}
		innerInit(filterconfig);
	}

	/**
	 * 子類進行初始化方法
	 * 
	 * @param filterconfig
	 */
	public abstract void innerInit(FilterConfig filterconfig) throws ServletException;

	public void destroy() {
		// TODO Auto-generated method stub
	}

	/**
	 * filter過濾方法,final子類不可覆蓋,實現正則匹配規則,子類覆蓋innerDoFilter
	 */
	public final void doFilter(ServletRequest servletrequest, ServletResponse servletresponse, FilterChain filterchain)
			throws IOException, ServletException {
		String url = ((HttpServletRequest) servletrequest).getServletPath();
		if (checkExclude(url) || !checkInclude(url)) {// 無需過濾該請求,則pass
			filterchain.doFilter(servletrequest, servletresponse);
			return;
		}
		// 呼叫innerDoFilter進行過濾
		innerDoFilter(servletrequest, servletresponse, filterchain);
		return;
	}

	/**
	 * 需子類覆蓋,實現過濾邏輯
	 * 
	 * @param servletrequest
	 * @param servletresponse
	 * @param filterchain
	 */
	public abstract void innerDoFilter(ServletRequest servletrequest, ServletResponse servletresponse,
			FilterChain filterchain) throws IOException, ServletException;

	/**
	 * 檢驗訪問請求是否在include列表中
	 * 
	 * @param requestUrl
	 * @return
	 */
	public final boolean checkInclude(String requestUrl) {
		boolean flag = true;
		if (null == includePattern || includePattern.length == 0) {
			return flag;
		}
		for (Pattern pat : includePattern) {
			if (flag = pat.matcher(requestUrl).matches())
				break;
		}
		return flag;
	}

	/**
	 * 檢驗訪問請求是否在exclude列表中
	 * 
	 * @param requestUrl
	 * @return
	 */
	public final boolean checkExclude(String requestUrl) {
		boolean flag = false;
		if (null == excludePattern || excludePattern.length == 0) {
			return flag;
		}
		for (Pattern pat : excludePattern) {
			if (flag = pat.matcher(requestUrl).matches())
				break;
		}
		return flag;
	}
}
原始碼其實很簡單,僅有基礎需要注意的地方:java的Matcher類是執行緒不安全的,所有在Filter中肯定是不能共享的,但是Pattern可以,所以我們將指定的規則初始化成Pattern快取起來,避免每次呼叫的時候都來初始化Pattern,提高效率;通過使用final修改方法以防止子類覆蓋doFilter的實現邏輯,並通過呼叫抽象方法innerDoFilter()來呼叫子類的過濾邏輯。

下面就來看看如何使用該過濾器。很簡單,首先寫一個Filter繼承自該類即可,實現innerDoFilter()以及innerInit()方法即可,然後在web.xml中新增該filter的配置,配置的時候就可以指定兩個init-param了,具體配置見下面。

filter>
		<filter-name>loginFilter</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
		<init-param>
			<description>指定Spring管理Filter生命週期</description>
			<param-name>targetFilterLifecycle</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<description>配置需要過濾的url規則,需使用正則,多個之間已','分割</description>
			<param-name>include</param-name>
			<param-value></param-value>
		</init-param>
		<init-param>
			<description>配置不需要過濾的url規則,需使用正則,多個之間已','分割</description>
			<param-name>exclude</param-name>
			<param-value>/css/.*,/js/.*,/images/.*,/fonts/.*,/plugin/.*</param-value>
		</init-param>
	</filter>

	<filter-mapping>
		<filter-name>loginFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
從上面的配置檔案可以看見,添加了兩個初始化引數,含義配置中有詳細描述。不過我的配置可能跟普通的filter配置不同,是因為我使用了Spring來管理Filter,所以這裡的fliter-class是Spring的一個類(org.springframework.web.filter.DelegatingFilterProxy),這個類預設會根據filter-name來查詢對應的bean來初始化成filter。下面說一下Spring的這個類DelegatingFilterProxy。

從PatternFilter的程式碼可以看出需要呼叫init()方法,但是如果使用Spring的DelegatingFilterProxy類來管理Filter,預設情況下是不會呼叫Filter的init()方法的,這是因為預設情況下Spring是不會管理Filter的生命週期的,所以不會呼叫init()以及destory()方法。下面來簡單分析下DelegatingFilterProxy這個類的原始碼,看看哪裡是解決這個問題的關鍵。

首先開啟DelegatingFilterProxy類的原始碼,可以看出繼承自GenericFilterBean,而GenericFilterBean實現了Filter,並實現了init()方法。

public final void init(FilterConfig filterConfig) throws ServletException {
		Assert.notNull(filterConfig, "FilterConfig must not be null");
		if (logger.isDebugEnabled()) {
			logger.debug("Initializing filter '" + filterConfig.getFilterName() + "'");
		}

		this.filterConfig = filterConfig;

		// Set bean properties from init parameters.
		try {
			PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
			ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
			bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
			initBeanWrapper(bw);
			bw.setPropertyValues(pvs, true);
		}
		catch (BeansException ex) {
			String msg = "Failed to set bean properties on filter '" +
				filterConfig.getFilterName() + "': " + ex.getMessage();
			logger.error(msg, ex);
			throw new NestedServletException(msg, ex);
		}

		// Let subclasses do whatever initialization they like.
		initFilterBean();

		if (logger.isDebugEnabled()) {
			logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");
		}
	}
從上面的程式碼可以看出GenericFilterBean的init()方法中通過呼叫initFilterBean()來呼叫子類的init()方法,下面我們來看下DelegatingFilterProxy的initFilterBean()方法。
protected void initFilterBean() throws ServletException {
		synchronized (this.delegateMonitor) {
			if (this.delegate == null) {
				// If no target bean name specified, use filter name.
				if (this.targetBeanName == null) {
					this.targetBeanName = getFilterName();
				}
				// Fetch Spring root application context and initialize the delegate early,
				// if possible. If the root application context will be started after this
				// filter proxy, we'll have to resort to lazy initialization.
				WebApplicationContext wac = findWebApplicationContext();
				if (wac != null) {
					this.delegate = initDelegate(wac);
				}
			}
		}
	}
該方法內部呼叫了initDelegate()方法。
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}
該方法內部呼叫了被代理的類(即我們自己的filter)init()方法。但是,注意下在呼叫的時候有一個if判斷,即判斷是否允許Spring管理filter的生命週期(isTargetFilterLifecycle()),看看這個方法內部時如果進行判斷的。
/**
	 * Return whether to invoke the {@code Filter.init} and
	 * {@code Filter.destroy} lifecycle methods on the target bean.
	 */
	protected boolean isTargetFilterLifecycle() {
		return this.targetFilterLifecycle;
	}
方法內部很簡單,只是判斷一個變數值,該變數值決定了是否允許Spring管理filter的生命週期,預設是false,所以當找到這裡的時候就已經知道是什麼原因引起的了。所以很自然的就是想看看是否存在一個設定該變數值的方法,當然,肯定是存在的(setTargetFilterLifecycle()),於是就找這個方法在哪裡被呼叫了,結果發現沒有任何地方顯示呼叫了該方法,注意是顯示,所以暫且沒轍了,於是很自然的想到了繼承該類以此來滿足自己的需求,但是總感覺不對,於是看了下這個類的註釋,結果很高興的發現了註釋中提到了這個變數值如果修改。
 * <p><b>NOTE:</b> The lifecycle methods defined by the Servlet Filter interface
 * will by default <i>not</i> be delegated to the target bean, relying on the
 * Spring application context to manage the lifecycle of that bean. Specifying
 * the "targetFilterLifecycle" filter init-param as "true" will enforce invocation
 * of the {@code Filter.init} and {@code Filter.destroy} lifecycle methods
 * on the target bean, letting the servlet container manage the filter lifecycle.
很簡單,就是在配置filter的時候在init-param中新增一個引數,名為targetFilterLifecycle,值為true即可,於是嘗試下,結果OK了。雖然問題解決了,但是還是想看一下到底是哪裡呼叫了setTargetFilterLifecycle()這個方法,畢竟沒有地方顯示呼叫,於是又回去翻原始碼。
我們回到GenericFilterBean這個類的init()方法,可以看到其中有一個地方很想設定變數值的地方:bw.setPropertyValues(pvs, true);。結果深入看了下,就是這個地方設定的setTargetFilterLifecycle值,具體做法是獲取filter中所有的init-param引數,然後根據引數名通過反射獲取對應的set方法,並呼叫賦值,其中具體程式碼有很多不解之處,不過不影響理解這個問題。根據以上的邏輯,DelegatingFilterProxy類中的所有變數只有存在對應的set方法應該都是可以通過init-param指定。

以上就是我自己實現的可配置規則的Filter類以及Spring管理Filter遇到的問題。