hatenob

プログラムって分からないことだらけ

ValveでServletの前後に処理を追加する

JBossのWeb層を司るJBossWebには、Tomcatのそれと同じく、Valveという機構がありまして、Servletの処理を呼ぶ前後に処理を追加できます。

前後にっていうと分かりにくいですが、テンプレートメソッドのような、AOPのような形です。
平たく言うとServlet Filterと同じようなもんです。
挟み込む場所によっては、アプリケーションのコンテキストクラスローダをバインドする前に呼ばれるので、ちょっと早めに処理を挟むことができます。
一方で、ValveはTomcat独自の機構であり移植性はなくなります。

Tomcatでは何にValveを使っているかというと、アクセスログ出力であったり、認証であったり。
Apache Tomcat 7 Configuration Reference (7.0.47) - The Valve Component

実はこれ以外にも、Tomcatのソースやバイナリを見てみると、いくつか用意されています。

ValveはTomcatの場合、HostやContextといった単位で挟にこむことができます。
JBossの場合は、EAP6.1からグローバルな定義とアプリケーション固有の定義ができます。(6.0ではグローバルはできない模様)

自分で作る

org.apache.catalina.valves.ValveBaseというクラスを継承して作成するとよいようです。
org.apache.catalina.Lifecycleインタフェースを実装すると、よりようです。
例えば、起動時とか停止時とかに処理を追加したいようなことがあればLifecycleインタフェースを使って実現できそうです。
このように書くと任意のようにも思えますが、既存のValveクラスを見るとどれもLifecycleインタフェースを実装しているので、お作法としてはそれが正しいのだと思います。
今回はちょっと試すだけなので、ValveBaseの継承だけにしています。

リクエストごとの処理

invokeメソッドをオーバーライドします。
下の処理はこんな感じのことをやってますが、ここを用途に合わせて実装します。

  1. リクエストURIが「(コンテキストルート)/goto503」だと503エラーを返し、
  2. そうでなければ受け付けたリクエスト数をカウントアップ
private final AtomicInteger requestCount = new AtomicInteger(0);

@Override
public void invoke(Request request, Response response)
    throws IOException, ServletException {

  if (request.getRequestURI().startsWith(
      request.getContextPath() + "/goto503")) {
    response.sendError(Response.SC_SERVICE_UNAVAILABLE);

  } else {
    this.requestCount.incrementAndGet();

    next.invoke(request, response);
  }
}

キモになるのは、「next.invoke(request, response);」のところで、これをすることで後段の処理につなげています。
これを呼ばなければ後段は呼ばれません。

エラーステータスを返した時の振る舞いには注意が必要です。

グローバルの場合、エラー画面は返りません。
これは、JBossのデフォルトエラー画面を返す役割であるErrorReportValve(これもValve)が、追加するグローバルValveよりも後段に設定されているためです。
なので、エラー画面を返したい場合は自分で何とかするしかありません。(ErrorReportValveを参考に実装すれば多分大丈夫)

アプリケーションの場合、通常のアプリケーションと同じくweb.xmlのerror-page設定か、デフォルトの画面になります。

バックグラウンドの処理

バックグラウンド処理を行いたい場合は、backgroundProcessメソッドをオーバーライドします。
バックグラウンド処理は10秒に1回呼び出されます。

@Override
public void backgroundProcess() {
  LOG.info("execute background process");
}

MBean

JBossWebではシステムプロパティorg.apache.tomcat.util.ENABLE_MODELERをtrueに設定することで、JMX経由でMBeanにアクセスできるようになります。
Valveの場合、何もしなくてもgetter/setterの有無によって自動でMBeanとして登録されます。
勝手に登録はorg.apache.tomcat.util.modeler.modules.MbeansDescriptorsIntrospectionSourceがやってくれます。
ただし、型に制限があり、自動で登録されるのは下記の型だけのようです。
厳密には、これ以外にも自動登録できる型があるようですので気になったら同クラスのisBeanCompatibleメソッドを見てください。

package org.apache.tomcat.util.modeler.modules;
...
public class MbeansDescriptorsIntrospectionSource
   extends ModelerSource
{
...
    private static Class[] supportedTypes  = new Class[] {
        Boolean.class,
        Boolean.TYPE,
        Byte.class,
        Byte.TYPE,
        Character.class,
        Character.TYPE,
        Short.class,
        Short.TYPE,
        Integer.class,
        Integer.TYPE,
        Long.class,
        Long.TYPE,
        Float.class, 
        Float.TYPE,
        Double.class,
        Double.TYPE,
        String.class,
        strArray.getClass(),
        BigDecimal.class,
        BigInteger.class,
        ObjectName.class,
        objNameArray.getClass(),
        java.io.File.class,
    };

自動登録ではなく、mbeans-descriptors.xmlファイルを用意することでもMBeanの登録ができます。
Apache Tomcat 7 (7.0.47) - MBean Descriptor How To
同ファイルが存在する場合は自動登録されず、こちらの設定が優先されます。
このあたりは、org.apache.tomcat.util.modeler.RegistryクラスのloadDescriptorsメソッドでやっています。
ファイルを使う場合はListやMapといった型も使うことができるので、どうしてもそういった型が必要であったり、余計なものまで自動登録されるのが困るような場合はファイルを使うのがよいのではないでしょうか。

使い方

グローバルの場合はstandalone.xmlに、アプリケーション固有の場合はWEB-INF/jboss-web.xmlに書きます。

ソースはコチラ。
chanko/chanko-valve at master · nobrooklyn/chanko · GitHub