S-JIS[2009-10-12/2009-10-21] 変更履歴

Javaサーブレット フィルター

フィルター(javax.servlet.Filter)は、各サーブレット共通で前処理・後処理を行いたい場合に使用する。

参考

コーディング例

SampleFilter.java:

package jp.hishidama.sample.filter;
import java.io.IOException;
import java.util.Enumeration;

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;
public class SampleFilter implements Filter {

	@Override
	public void init(FilterConfig config) throws ServletException {
		System.out.println("SampleFilter#init()");
	}

	@Override
	public void destroy() {
		System.out.println("SampleFilter#destroy()");
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
	 throws IOException, ServletException {

		System.out.println("SampleFilter#doFilter() start");

		//●前処理
		HttpServletRequest req = (HttpServletRequest) request;
		System.out.println("uri: " + req.getRequestURI());

		try {
			chain.doFilter(request, response);
		} finally {
			//●後処理
			System.out.println("SampleFilter#doFilter() end");
		}
	}
}

フィルターは、Filterインターフェースを実装する。

実装すべきメソッド 呼ばれるタイミング 実装すべき内容
init() JavaEEサーバー(Tomcat等)の起動時 一度だけ呼ばれるので、初期処理を記述する。
FilterConfigが引数になっているのはこのメソッドのみなので
初期化パラメーターの取得はここで行う。
destroy() JavaEEサーバー(Tomcat等)の終了時 終了時に一度だけ呼ばれる。
何か保持しているデータがあったらここで解放すべきだが
基本的にフィルターで保持するデータで解放が必要なものは無いはず。
doFilter() サーブレットが呼ばれる前 リクエストにつき1回呼ばれる。
(内部でサーブレットが何回フォワードしても、フィルターは1回のみ)
フィルターは複数定義できるので、次のフィルターを実行する為に
chain.doFilter()を呼び出す必要がある。
(一番末端のフィルターでサーブレットが呼び出される。
 逆に、処理を中断したければchain.doFilter()を呼び出さない→中断方法
chain.doFilter()の前に記述するのが前処理、後に記述すれば後処理になる。

web.xml:

<web-app …>

	<filter>
		<filter-name>sampleFilter</filter-name>
		<filter-class>jp.hishidama.sample.filter.SampleFilter</filter-class>
	</filter>

	<filter-mapping>
		<filter-name>sampleFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<servlet>
		〜
	</servlet>

	<servlet-mapping>
		〜
	</servlet-mapping>
〜
</web-app>

web.xmlのfilter-mappingのurl-patternに、フィルターをマッチさせたいURLを書く。
「/*」は、全URLにマッチすることを意味する。


ServletContextの使用方法

ServletContextは、doFilter()では取得できないが、init()で取得できる。[2009-10-21]
したがって、init()で取得しておいてフィールドに保持しておけば良さそう。

public class SampleFilter implements Filter {

	private ServletContext context;

	@Override
	public void init(FilterConfig config) throws ServletException {
		this.context = config.getServletContext();
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
	 throws IOException, ServletException {

		dumpActions(context);
	}

参考: TECHSCOREのフィルタサーブレット


コーディングの注意点

サーブレットと同様に、フィルターもマルチスレッドで呼ばれる
つまりMTセーフ(スレッドセーフ)な作りにしなければならない

通常、フィルターでフィールドに保持していいのは、読み取り専用の値、つまりinit()で取得したコンフィグのパラメーターくらいだろう。


初期化パラメーター

web.xmlでフィルターを定義する際に、初期化パラメーターを指定することが出来る。
指定したパラメーターは、init()メソッドで取得することが出来るので、ここでフィールドに入れ直すのだろう。

web.xml:

	<filter>
		<filter-name>sampleFilter</filter-name>
		<filter-class>jp.hishidama.sample.filter.SampleFilter</filter-class>
		<init-param>
			<param-name>param1</param-name>
			<param-value>param1value</param-value>
		</init-param>
		<init-param>
			<param-name>param2</param-name>
			<param-value>paramValue2</param-value>
		</init-param>
	</filter>

SampleFilter.java:

public class SampleFilter implements Filter {

	protected String param1;
	protected String param2;
	protected String param3;

	@Override
	public void init(FilterConfig config) throws ServletException {

		@SuppressWarnings("unchecked")
		Enumeration<String> names = config.getInitParameterNames();
		while (names.hasMoreElements()) {
			String name = names.nextElement();
			String value = config.getInitParameter(name);
			System.out.printf("%s = %s%n", name, value);
		}

		this.param1 = config.getInitParameter("param1");
		this.param2 = config.getInitParameter("param2");
		this.param3 = config.getInitParameter("param3");
	}

	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {

		System.out.printf("\t%s %s %s%n", param1, param2, param3);

		chain.doFilter(request, response);
	}
}

init()メソッドの引数にFilterConfigがあり、そのgetInitParameter()でweb.xmlに書かれたパラメーターの値を取得できる。
パラメーターが記述されていなかった場合はnullになる。

パラメーターは固定(不変)なので、フィールドに保持しておいても大丈夫。

また、丁寧な人ならdestroy()で「param1 = null;」のようにクリーンナップ処理を入れるだろう。
(フィルターのインスタンスが使い回されるなら必須だが…)


複数フィルターの実行順序

web.xmlには複数のフィルター定義を書くことが出来る。

filter-mappingで使用されないフィルターであっても、filter要素が書かれれば、インスタンス自体は作られるようだ。
その際、filter要素に同じクラス名が使われていても、別インスタンスになる。(パラメーターで使い分ける為らしい)
ただし、インスタンス化される順序は不定。(web.xmlに書かれた順序とは関係なさそう)

一方、filter-mappingで複数のフィルターが指定された場合、
web.xmlに記述された順番でフィルターチェインが実行されていくようだ。


フィルター処理の中断

フィルター内で何らかの判断(例えばログインチェックによる不正とか)によって処理を中断したい(エラー画面を表示したい)場合は、後続のフィルター処理(chain.doFilter())を呼ばず、自分で別画面へ遷移させる。

別JSPへ遷移させる例

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {

		if (判定NG) {
			RequestDispatcher rd = request.getRequestDispatcher("/error.jsp");
			rd.forward(request, response);
			return;
		}

		chain.doFilter(request, response);
	}

遷移先はJSPだけでなく、web.xmlに記述された他のURIへ遷移することも出来る。


例外を発生させて遷移させる例

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {

		if (判定NG) {
			throw new MyException();
		}

		chain.doFilter(request, response);
	}

web.xml:

〜
	</welcome-file-list>

	<error-page>
		<exception-type>jp.hishidama.sample.MyException</exception-type>
		<location>/error.jsp</location>
	</error-page>

	<jsp-config>
〜

web.xmlでerror-pageを指定すれば、例外をキャッチして、locationで指定したURIへ遷移できる。

しかし、error-pageは最終的に処理しきれなかった例外を遷移させる為にjava.lang.Exceptionを捕捉するような使い方をするべきであり、業務ロジック的な遷移には使わない方がいいと思う。


フィルターとStruts

フィルター処理とStrutsのActionの初期化(厳密にはActionServlet#init()が呼ばれる事)は、JavaEEサーバー起動直後の初回のURIの内容による。

初回のURIがStrutsを使用するものである場合、まずActionServletがインスタンス化されてinit()が呼ばれ、
その後にフィルター処理→ActionServlet#process()処理となる。

一方、初回のURIがStrutsとは無関係(web.xmlの定義のみでの遷移)の場合は、フィルター処理が最初に呼ばれるので、その時点ではStruts関連の初期化は行われていない。
この状態でStrutsのタグ(例えばhtml:errors)を使用したJSPへ遷移しようとすると、ModuleConfigとかが初期化されていないので、ErrorsTagでNullPointerExceptionが発生したりする。

そこで、フィルターでStrutsのメッセージを設定してhtml:errorsを使ったエラー画面へ遷移したい場合は、struts-config.xmlのactionを経由してやればよい。

SampleFilter.java:

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {

		if (判定NG) {
			// html:errorsで表示できるようにエラーメッセージを生成
			ActionMessage msg = new ActionMessage("msg.key.filter");
			ActionErrors errors = new ActionErrors();
			errors.add(ActionErrors.GLOBAL_MESSAGE, msg);
			req.setAttribute(Globals.ERROR_KEY, errors);

			RequestDispatcher rd = request.getRequestDispatcher("/strutsError.do");
			rd.forward(request, response);
			return;
		}

		chain.doFilter(request, response);
	}

struts-config.xml:

		<action path="/strutsError" forward="/error.jsp"/>

上記の方式では、リクエストに既にエラーメッセージが入っている場合は、上書きしてしまう。
普通のAction#addErrors()では、既に存在していたら追加したり別の情報も書き換えたりと、いくつかの事を行っている。
したがって、Actionクラスを作って、そこでaddErrors()を呼ぶ方が良さそう。
他にも必要に応じていろいろな処理を行うことも出来るし。

SampleFilter.java:

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {

		if (判定NG) {
			// html:errorsで表示できるようにエラーメッセージを生成
			ModuleException ex = new ModuleException("msg.key.filter");
			req.setAttribute(SampleFilterAction.EXCEPTION_KEY, ex);

			RequestDispatcher rd = request.getRequestDispatcher("/strutsError.do");
			rd.forward(request, response);
			return;
		}

		chain.doFilter(request, response);
	}

struts-config.xml:

		<action path="/strutsError"
		 type="jp.hishidama.sample.filter.SampleFilterAction"
		 forward="/error.jsp"/>

SampleFilterAction.java:

public class SampleFilterAction extends Action {

	public static final String EXCEPTION_KEY = "jp.hishidama.sample.filter.SampleFilterAction.EXCEPTION";

	@Override
	public ActionForward execute(ActionMapping mapping, ActionForm form,
		HttpServletRequest request, HttpServletResponse response) throws Exception {

		ModuleException ex = (ModuleException)request.getAttribute(EXCEPTION_KEY);
		ActionMessage msg = ex.getActionMessage();

		ActionMessages errors = new ActionErrors();
		errors.add(ActionErrors.GLOBAL_MESSAGE, msg);
		addErrors(request, errors);

		return new ActionForward(mapping.getForward());
	}
}

フィルターからアクションへのデータの受け渡しには、requestのattributeを使う。
データの型は何でもいいので、上記の例ではModuleExceptionを使っている。普通はもっと自分の目的に特化したクラスを作るだろう。

attributeにセットする際のキーは、「javax.servlet.error.」で始まるキーワードは避けなければならない。
javax.servlet.error.exception」はフィルターで発生した例外が保持されるキーなので、ここに例外インスタンスが入っていると 、フィルター処理終了後にエラー処理に行ってしまうっぽい。
他にも「javax」で始まるキーワードが色々使われているようなので、当然避けるべきだろう。
(Strutsでも「org.apache.struts.〜」で色々使っている)


サーブレットへ戻る / JavaEEへ戻る / Java目次へ戻る / Strutsへ行く
メールの送信先:ひしだま