1. 概要

このチュートリアルでは、 Intercepting FilterPatternプレゼンテーション層のコアJ2EEパターンを紹介します。

これは、パターンシリーズの2番目のチュートリアルであり、フロントコントローラーパターンガイドのフォローアップです。このガイドはここにあります。

インターセプトフィルターは、着信要求がハンドラーによって処理される前または後にアクションをトリガーするフィルターです。

インターセプトフィルターは、すべてのリクエストに共通で、既存のハンドラーに影響を与えることなく拡張可能な、Webアプリケーションの集中化されたコンポーネントを表します。

2. ユースケース

前のガイドからexampleを拡張し、認証メカニズム、リクエストロギング、およびビジターカウンターを実装しましょう。 さらに、さまざまなエンコーディングでページを配信できるようにする必要があります。

これらはすべて、すべてのリクエストに共通であり、ハンドラーから独立している必要があるため、フィルターをインターセプトするためのユースケースです。

3. フィルター戦略

さまざまなフィルター戦略と典型的なユースケースを紹介しましょう。 Jettyサーブレットコンテナでコードを実行するには、次のコマンドを実行するだけです。

$> mvn install jetty:run

3.1. カスタムフィルター戦略

カスタムフィルター戦略は、リクエストの順序付けられた処理を必要とするすべてのユースケースで使用されます。つまり、 1つのフィルターは、実行チェーンの前のフィルターの結果に基づいています。

これらのチェーンは、 FilterChain インターフェイスを実装し、さまざまなFilterクラスを登録することで作成されます。

懸念の異なる複数のフィルターチェーンを使用する場合は、フィルターマネージャーでそれらを結合できます。

 

この例では、ビジターカウンターは、ログインしたユーザーからの一意のユーザー名をカウントすることで機能しています。つまり、認証フィルターの結果に基づいているため、両方のフィルターをチェーンする必要があります。

このフィルターチェーンを実装しましょう。

まず、設定された「username」属性のセッションが存在するかどうかを確認する認証フィルターを作成し、存在しない場合はログイン手順を発行します。

public class AuthenticationFilter implements Filter {
    ...
    @Override
    public void doFilter(
      ServletRequest request,
      ServletResponse response, 
      FilterChain chain) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        
        HttpSession session = httpServletRequest.getSession(false);
        if (session == null || session.getAttribute("username") == null) {
            FrontCommand command = new LoginCommand();
            command.init(httpServletRequest, httpServletResponse);
            command.process();
        } else {
            chain.doFilter(request, response);
        }
    }
    
    ...
}

それでは、ビジターカウンターを作成しましょう。 このフィルターは、一意のユーザー名の HashSet を維持し、リクエストに「counter」属性を追加します。

public class VisitorCounterFilter implements Filter {
    private static Set<String> users = new HashSet<>();

    ...
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) {
        HttpSession session = ((HttpServletRequest) request).getSession(false);
        Optional.ofNullable(session.getAttribute("username"))
          .map(Object::toString)
          .ifPresent(users::add);
        request.setAttribute("counter", users.size());
        chain.doFilter(request, response);
    }

    ...
}

次に、登録されたフィルターを繰り返し、doFilterメソッドを実行するFilterChainを実装します。

public class FilterChainImpl implements FilterChain {
    private Iterator<Filter> filters;

    public FilterChainImpl(Filter... filters) {
        this.filters = Arrays.asList(filters).iterator();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response) {
        if (filters.hasNext()) {
            Filter filter = filters.next();
            filter.doFilter(request, response, this);
        }
    }
}

コンポーネントを相互に接続するために、フィルターチェーンのインスタンス化、フィルターの登録、および開始を担当する単純な静的マネージャーを作成しましょう。

public class FilterManager {
    public static void process(HttpServletRequest request,
      HttpServletResponse response, OnIntercept callback) {
        FilterChain filterChain = new FilterChainImpl(
          new AuthenticationFilter(callback), new VisitorCounterFilter());
        filterChain.doFilter(request, response);
    }
}

最後のステップとして、FrontCommand内からリクエスト処理シーケンスの共通部分としてFilterManagerを呼び出す必要があります。

public abstract class FrontCommand {
    ...

    public void process() {
        FilterManager.process(request, response);
    }

    ...
}

3.2. 基本フィルター戦略

このセクションでは、基本フィルター戦略を紹介します。この戦略では、実装されているすべてのフィルターに共通のスーパークラスが使用されます。

このストラテジーは、前のセクションのカスタムストラテジー、または次のセクションで紹介する標準フィルターストラテジーとうまく連携します。

抽象基本クラスを使用して、フィルターチェーンに属するカスタム動作を適用できます。 この例では、これを使用して、フィルター構成とデバッグログに関連する定型コードを減らします。

public abstract class BaseFilter implements Filter {
    private Logger log = LoggerFactory.getLogger(BaseFilter.class);

    protected FilterConfig filterConfig;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("Initialize filter: {}", getClass().getSimpleName());
        this.filterConfig = filterConfig;
    }

    @Override
    public void destroy() {
        log.info("Destroy filter: {}", getClass().getSimpleName());
    }
}

この基本クラスを拡張して、次のセクションに統合される要求ロギングフィルターを作成しましょう。

public class LoggingFilter extends BaseFilter {
    private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);

    @Override
    public void doFilter(
      ServletRequest request, 
      ServletResponse response,
      FilterChain chain) {
        chain.doFilter(request, response);
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        
        String username = Optional
          .ofNullable(httpServletRequest.getAttribute("username"))
          .map(Object::toString)
          .orElse("guest");
        
        log.info(
          "Request from '{}@{}': {}?{}", 
          username, 
          request.getRemoteAddr(),
          httpServletRequest.getRequestURI(), 
          request.getParameterMap());
    }
}

3.3. 標準フィルター戦略

フィルタを適用するより柔軟な方法は、標準フィルタ戦略を実装することです。 これは、デプロイメント記述子でフィルターを宣言するか、サーブレット仕様3.0以降はアノテーションを使用して行うことができます。

標準のフィルター戦略を使用すると、明示的に定義されたフィルターマネージャーがなくても、新しいフィルターをデフォルトのチェーンにプラグインできます。

 

フィルタが適用される順序は、注釈を介して指定できないことに注意してください。 順序付けられた実行が必要な場合は、デプロイメント記述子を使用するか、カスタムフィルター戦略を実装する必要があります。

基本フィルター戦略も使用する注釈駆動型エンコーディングフィルターを実装しましょう。

@WebFilter(servletNames = {"intercepting-filter"}, 
  initParams = {@WebInitParam(name = "encoding", value = "UTF-8")})
public class EncodingFilter extends BaseFilter {
    private String encoding;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        super.init(filterConfig);
        this.encoding = filterConfig.getInitParameter("encoding");
    }

    @Override
    public void doFilter(ServletRequest request,
      ServletResponse response, FilterChain chain) {
        String encoding = Optional
          .ofNullable(request.getParameter("encoding"))
          .orElse(this.encoding);
        response.setCharacterEncoding(encoding); 
        
        chain.doFilter(request, response);
    }
}

デプロイメント記述子を持つサーブレットシナリオでは、web.xmlに次の追加の宣言が含まれます。

<filter>
    <filter-name>encoding-filter</filter-name>
    <filter-class>
      com.baeldung.patterns.intercepting.filter.filters.EncodingFilter
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>encoding-filter</filter-name>
    <servlet-name>intercepting-filter</servlet-name>
</filter-mapping>

サーブレットで使用できるように、ロギングフィルターを取得して注釈を付けましょう。

@WebFilter(servletNames = "intercepting-filter")
public class LoggingFilter extends BaseFilter {
    ...
}

3.4. テンプレートフィルター戦略

テンプレートフィルター戦略は、実装でオーバーライドする必要がある基本クラスで宣言されたテンプレートメソッドを使用することを除いて、基本フィルター戦略とほとんど同じです。

 

さらなる処理の前後に呼び出される2つの抽象フィルターメソッドを使用して、基本フィルタークラスを作成しましょう。

この戦略はあまり一般的ではなく、この例では使用しないため、具体的な実装とユースケースはあなたの想像力次第です。

public abstract class TemplateFilter extends BaseFilter {
    protected abstract void preFilter(HttpServletRequest request,
      HttpServletResponse response);

    protected abstract void postFilter(HttpServletRequest request,
      HttpServletResponse response);

    @Override
    public void doFilter(ServletRequest request,
      ServletResponse response, FilterChain chain) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        
        preFilter(httpServletRequest, httpServletResponse);
        chain.doFilter(request, response);
        postFilter(httpServletRequest, httpServletResponse);
    }
}

4. 結論

インターセプトフィルターパターンは、ビジネスロジックとは独立して進化する可能性のある横断的関心事をキャプチャします。 業務の観点から、フィルターは事前または事後アクションのチェーンとして実行されます。

これまで見てきたように、インターセプトフィルターパターンはさまざまな戦略を使用して実装できます。 「実世界」のアプリケーションでは、これらのさまざまなアプローチを組み合わせることができます。

いつものように、ソースはGitHubにあります。