SQL組み立て時に条件によってWHERE句が変化する場合のTips

SQLをプログラムで組み立てて実行するときで、条件によってWHERE句が変化する場合、ANDがつくかどうかでとても煩雑なコードになることがある。
以下の例のような感じだ。

StringBuilder sql = new StringBuilder();
sql.append("SELECT xxx, ... FROM xxx");
boolean where = false;
if (条件1) {
     sql.appned(" WHERE hoge = ?");
     where = true;
}
if (条件2) {
     if (where) {
          sql.append(" AND foo = ?");
     } else {
          sql.appned(" WHERE foo = ?");
          where = true;
     }
}
if (条件3) {
     if (where) {
          sql.append(" AND bar = ?");
     } else {
          sql.appned(" WHERE bar = ?");
          where = true;
     }
}

こんな場合は、以下のように「WHERE 1 = 1」を最初に書いておけば、常に AND をつけることができ、コードがスッキリする。

StringBuilder sql = new StringBuilder();
sql.append("SELECT xxx, ... FROM xxx WHERE 1 = 1");
if (条件1) {
     sql.appned(" AND hoge = ?");
}
if (条件2) {
     sql.append(" AND foo = ?");
}
if (条件3) {
     sql.append(" AND bar = ?");
}

log4j の appender がないと言われる警告を出さなくする

備忘録。
log4jを使用していて、以下のような警告が出ることがある。

log4j:WARN No appenders could be found for logger (xxx.xxx.xxx.Xxx).
log4j:WARN Please initialize the log4j system properly.

これはログ出力処理を行うクラスに対して、log4jの設定上、該当するAppenderの設定がされていない場合に出る。
よくあるのは、ライブラリを使用していてライブラリ内の処理でログ出力をしているのに、log4jの設定に該当するAppenderの設定がないパターン。
多数のライブラリを使用していると、いちいちパッケージを指定するのも面倒なので、ルートロガーを指定してやればよい。

log4j.properties の例

# logger
log4j.rootLogger=DEBUG, console
log4j.logger.xxx=DEBUG, console

# console appender
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d [%-5p-%c] %m%n

ただ、開発時には上記でよくても、運用時にはライブラリ内のログ出力は不要なこともあると思う。
そんな時に便利なのが、NullAppender。名前から想像できる通り、このAppenderは何も出力しない。

# logger
log4j.rootLogger=FATAL, null
log4j.logger.xxx=WARN, file

# null appender
log4j.appender.null=org.apache.log4j.varia.NullAppender

# file appender
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=/var/log/webapp/xxx_log
log4j.appender.file.DatePattern='.'yyyyMMdd
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d [%-5p-%c] %m%n
log4j.appender.file.encoding=UTF-8

続 SAStrutsをStruts形式のURLでも動くようにする

前回エントリの続き。
S2ModuleConfigを継承して、もう少しコードを整理してみる。

まずはFilter
2009/10/6 追記
前回エントリの修正に合わせて修正しました。

public class MyRoutingFilter extends RoutingFilter {
    public static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";
    public static final String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";

    protected static final MessageResources messages = MessageResources
            .getMessageResources("org.apache.struts.actions.LocalStrings");

    /** Struts URLパターンの正規表現 */
    protected String strutsUrlPattern = ".*Action\\.do$";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        String path = RequestUtil.getPath(req);

        if (!processDirectAccess(request, response, chain, path)) {
            return;
        }

        if (isStrutsPath(path)) {
            ActionConfig actionConfig = ((MyModuleConfig) S2ModuleConfigUtil
                    .getModuleConfig()).findStrutsActionConfig(processPath(req, res));
            if (actionConfig != null) {
                // struts-config.xmlに定義されているActionのフルクラス名を取得
                String actionType = actionConfig.getType();

                // ForwardAction の場合は、struts-config.xml の parameter
                // に定義されているパスへフォワードする
                if ("org.apache.struts.actions.ForwardAction"
                        .equals(actionType)) {
                    executeForwardAction(actionConfig, req, res);
                    return;
                }

                // struts-config.xml に forward 属性が定義されている場合は、定義されているパスへフォワードする
                if (actionConfig.getForward() != null) {
                    request.getRequestDispatcher(actionConfig.getForward())
                            .forward(request, response);
                    return;
                }

                // IncludeAction の場合は、struts-config.xml の parameter
                // に定義されているパスをインクルードする
                if ("org.apache.struts.actions.IncludeAction"
                        .equals(actionType)) {
                    executeIncludeAction(actionConfig, req, res);
                    return;
                }

                String actionPath = getActionPath(actionType);

                // Actionの実行メソッド名を取得
                String methodName = getActionMethodName(actionConfig, req);

                if (SingletonS2ContainerFactory.getContainer()
                    .hasComponentDef(ActionUtil.fromPathToActionName(actionPath))) {
                    if (StringUtil.isEmpty(methodName)) {
                        // RoutingFilter では、URLにメソッド指定がない場合は、
                        // S2ExecuteConfigUtil#findExecuteConfig(String,
                        // HttpServletRequest)で
                        // S2ExecuteConfigを取得しているが、その場合、SAStrutsの仕様により
                        // URLのパラメータ名にメソッド名と一致するものがあれば
                        // そのメソッドが実行されてしまうためStrutsと挙動が変わってしまう。
                        // Strutsと挙動を合わせるため、デフォルトメソッド名を補完する。
                        S2ExecuteConfig executeConfig = findExecuteConfig(actionPath, "index");
                        if (executeConfig != null) {
                            forward(req, res, actionPath, null, null);
                            return;
                        }
                    } else {
                        S2ExecuteConfig executeConfig = findExecuteConfig(actionPath, methodName);
                        if (executeConfig != null) {
                            forward(req, res, actionPath, methodName, executeConfig);
                            return;
                        }
                    }
                }
            }
        } else {
            super.doFilter(request, response, chain);
            return;
        }
        res.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
    }

    // 他のメソッドは前エントリと同じなので省略
}

Struts設定情報を保持するModuleConfig

public class MyModuleConfig extends S2ModuleConfig {
    private static final long serialVersionUID = 1L;

    /** Struts の Action 設定情報 */
    protected HashMap<String, ActionConfig> strutsActionConfigs = null;

    public MyModuleConfig(String prefix) {
        super(prefix);
        initialize();
    }

    @Override
    public void freeze() {
        super.freeze();
        // struts-config.xml の設定情報は、ActionServlet 初期化時には actionConfigs に格納されるが、
        // HotDeployではリクエストのたびに、S2ModuleConfig#disposeで actionConfigs がクリアされるため、
        // strutsActionConfigs に退避しておく。
        // struts-config.xml の設定情報は、HotDeploy 対象外となるため、strutsActionConfigs は
        // dispose でクリアしない。
        if (strutsActionConfigs == null) {
            strutsActionConfigs = new HashMap<String, ActionConfig>();
            for (Object key : actionConfigs.keySet()) {
                ActionConfig config = (ActionConfig) actionConfigs.get(key);
                if (!(config instanceof S2ActionMapping)) {
                    // SAStruts ではない、Struts の ActionMapping を保存する
                    strutsActionConfigs.put((String) key, config);
                }
            }
        }
    }

    public ActionConfig findStrutsActionConfig(String path) {
        return strutsActionConfigs.get(path);
    }
}

最後はMyModuleConfigを使用させるためのファクトリ

public class MyModuleConfigFactory extends ModuleConfigFactory {
    @Override
    public ModuleConfig createModuleConfig(String prefix) {
        return new MyModuleConfig(prefix);
    }
}

使い方

web.xmlに次のようにMyModuleConfigFactoryを設定する。
その他については、前回エントリと同じ。

<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    <init-param>
        <param-name>config</param-name>
        <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <init-param>
        <param-name>configFactory</param-name>
        <param-value>hoge.config.MyModuleConfigFactory</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

SAStrutsをStruts形式のURLでも動くようにする

SAStrutsではURLがきれいなURLになっている。これは今時必須の機能だと思うが、既存のシステムをStrutsからSAStrutsに置き換えるような場合は、既存のURLを変更しにくいことも多いと思う。
社内システム等閉じた範囲で使用するシステムであれば、URL変更の調整もしやすいと思うが、例えばB2CやB2Bサイトの場合は、コンシューマや取引先にまでURL変更の影響が及ぶので、きれいなURLだけではなく、既存のStrutsのXxxAction.doといったURLでも動いてほしいという要望がそれなりにあるんじゃないか?と思う。
この要望を実現するために、既存の形式のURLでリクエストされたらstruts-config.xmlを見て該当するActionを実行し、そうでなければSAStrutsの通常の処理を行うことを考えてみた。
既存のActionは新旧両方のURLで動くようにし、SAStrutsでシステムを再構築した後に追加したActionは新URLでのみ動かすという想定。再構築後も旧URLで動かすためにstruts-config.xmlを書くのは本末転倒だからね。

Strutsと一口に言っても素のActionやらDispatchActionやら色々なActionがあり、それに応じてURLや処理も異なるので完全互換とはいかないが、まずまずよく使うであろう形式のURL互換機能を実装してみた。
org.seasar.struts.filter.RoutingFilterを継承して実装する。

2009/10/3 追記
WARM deploy時に例外が発生して動作しなくなっていたので修正。
2009/10/6 追記
XXxActionのようにActionクラス名の2文字目が大文字の場合に404エラーになっていたので修正。

public class MyRoutingFilter extends RoutingFilter {
    public static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";

    public static final String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";

    protected static final MessageResources messages = MessageResources
            .getMessageResources("org.apache.struts.actions.LocalStrings");

    public static final String ACTION_CONFIGS_KEY = "tutorial.filter.ACTION_CONFIGS_KEY";

    /** Struts URLパターンの正規表現 */
    protected String strutsUrlPattern = ".*Action\\.do$";

    @Override
    public void init(FilterConfig config) throws ServletException {
        String configUrlPattern = config.getInitParameter("strutsUrlPattern");
        if (!StringUtil.isBlank(configUrlPattern)) {
            strutsUrlPattern = configUrlPattern;
        }
        super.init(config);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        HashMap<String, ActionConfig> actionConfigs = (HashMap<String, ActionConfig>) req
                .getSession().getServletContext().getAttribute(ACTION_CONFIGS_KEY);

        // 初回アクセス時の処理
        if (actionConfigs == null) {
            // HotDeployではリクエストのたびに、S2ModuleConfig#disposeでプロパティ(actionConfigsを含む)がクリアされるため、
            // ModuleConfigに保持されているstruts-config.xml設定を取得し、アプリケーションスコープに保存する。
            ModuleConfig moduleConfig = S2ModuleConfigUtil.getModuleConfig();
            Field f;
            try {
                f = moduleConfig.getClass().getSuperclass().getDeclaredField("actionConfigs");
                f.setAccessible(true);
                HashMap<String, ActionConfig> configs = (HashMap) f.get(moduleConfig);
                actionConfigs = new HashMap<String, ActionConfig>();
                for (Object key : configs.keySet()) {
                    ActionConfig config = (ActionConfig) configs.get(key);
                    if (!(config instanceof S2ActionMapping)) {
                        // SAStruts ではない、Struts の ActionMapping を保存する
                        actionConfigs.put((String) key, config);
                    }
                }
                req.getSession().getServletContext().setAttribute(ACTION_CONFIGS_KEY, actionConfigs);
            } catch (SecurityException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        String path = RequestUtil.getPath(req);

        if (!processDirectAccess(request, response, chain, path)) {
            return;
        }

        if (actionConfigs != null && isStrutsPath(path)) {
            ActionConfig actionConfig = (ActionConfig) actionConfigs.get(processPath(req, res));
            if (actionConfig != null) {
                // struts-config.xmlに定義されているActionのフルクラス名を取得
                String actionType = actionConfig.getType();

                // ForwardAction の場合は、struts-config.xml の parameter
                // に定義されているパスへフォワードする
                if ("org.apache.struts.actions.ForwardAction"
                        .equals(actionType)) {
                    executeForwardAction(actionConfig, req, res);
                    return;
                }

                // struts-config.xml に forward 属性が定義されている場合は、定義されているパスへフォワードする
                if (actionConfig.getForward() != null) {
                    request.getRequestDispatcher(actionConfig.getForward())
                            .forward(request, response);
                    return;
                }

                // IncludeAction の場合は、struts-config.xml の parameter
                // に定義されているパスをインクルードする
                if ("org.apache.struts.actions.IncludeAction".equals(actionType)) {
                    executeIncludeAction(actionConfig, req, res);
                    return;
                }

                String actionPath = getActionPath(actionType);

                // Actionの実行メソッド名を取得
                String methodName = getActionMethodName(actionConfig, req);

                if (SingletonS2ContainerFactory.getContainer()
                    .hasComponentDef(ActionUtil.fromPathToActionName(actionPath))) {
                    if (StringUtil.isEmpty(methodName)) {
                        // RoutingFilter では、URLにメソッド指定がない場合は、
                        // S2ExecuteConfigUtil#findExecuteConfig(String,
                        // HttpServletRequest)で
                        // S2ExecuteConfigを取得しているが、その場合、SAStrutsの仕様により
                        // URLのパラメータ名にメソッド名と一致するものがあれば
                        // そのメソッドが実行されてしまうためStrutsと挙動が変わってしまう。
                        // Strutsと挙動を合わせるため、デフォルトメソッド名を補完する。
                        S2ExecuteConfig executeConfig = findExecuteConfig(actionPath, "index");
                        if (executeConfig != null) {
                            forward(req, res, actionPath, null, null);
                            return;
                        }
                    } else {
                        S2ExecuteConfig executeConfig = findExecuteConfig(actionPath, methodName);
                        if (executeConfig != null) {
                            forward(req, res, actionPath, methodName, executeConfig);
                            return;
                        }
                    }
                }
            }
        } else {
            super.doFilter(request, response, chain);
            return;
        }
        res.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
    }

    protected String getActionPath(String actionClassName) {
        // ルートパッケージ名の取得
        NamingConvention namingConvention = SingletonS2Container
                .getComponent(NamingConvention.class);
        String[] rootPackageNames = namingConvention.getRootPackageNames();

        // Actionのフルクラス名からルートパッケージ名.action.を除く
        for (String rootPackageName : rootPackageNames) {
            if (actionClassName.startsWith(rootPackageName)) {
                actionClassName = actionClassName.replace(rootPackageName
                        + ".action.", "");
                break;
            }
        }

        // xxx.XxxAction -> xxx/xxx に変換
        int actionNameIndex = actionClassName.lastIndexOf(".");
        StringBuilder actionName = new StringBuilder();
        actionName.append(actionClassName.substring(0, actionNameIndex + 1));
        if (Character.isUpperCase(actionClassName.codePointAt(actionNameIndex + 2))) {
            // クラス名の2文字目が大文字の場合は、先頭は小文字にしない(xxx.XXxAction -> xxx/XXx)
            actionName.append(actionClassName.substring(actionNameIndex + 1,
                    actionNameIndex + 2));
        } else {
            actionName.append(actionClassName.substring(actionNameIndex + 1,
                    actionNameIndex + 2).toLowerCase());
        }
        actionName.append(actionClassName.substring(actionNameIndex + 2));
        StringBuilder actionPath = new StringBuilder();
        actionPath.append("/").append(
                actionName.toString().replace(".", "/").replaceFirst("Action$", ""));
        return actionPath.toString();
    }

    protected boolean isStrutsPath(String path) {
        if (path.matches(strutsUrlPattern)) {
            return true;
        }
        return false;
    }

    /**
     * Action の実行メソッド名を返します。<br>
     * Struts で、MappingDispatchAction を使用していた場合は、<br>
     * このメソッドをオーバーライドして、actionConfig.getParameter()を返すように実装してください。<br>
     */
    protected String getActionMethodName(ActionConfig actionConfig,
            HttpServletRequest request) {
        String methodName = "";

        String parameter = actionConfig.getParameter();
        if (parameter != null) {
            String paramValue = (String) request.getParameter(parameter);
            if (paramValue != null) {
                methodName = paramValue;
            }
        }

        // メソッド名が Struts のデフォルトメソッド名の場合は、index に変換。
        if (methodName.equals(getStrutsActionDefaultMethodName())) {
            methodName = "index";
        }
        return methodName;
    }

    /**
     * Struts のデフォルトメソッド名を返します。<br>
     * このメソッドがリクエストされた場合は、index メソッドを実行します。<br>
     * デフォルトでは null を返します。必要に応じて、このメソッドをオーバーライドしてください。<br>
     */
    protected String getStrutsActionDefaultMethodName() {
        return null;
    }

    protected String processPath(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String path = null;

        path = (String) request.getAttribute(INCLUDE_PATH_INFO);
        if (path == null) {
            path = request.getPathInfo();
        }
        if ((path != null) && (path.length() > 0)) {
            return (path);
        }

        path = (String) request.getAttribute(INCLUDE_SERVLET_PATH);
        if (path == null) {
            path = request.getServletPath();
        }
        String prefix = S2ModuleConfigUtil.getModuleConfig().getPrefix();
        if (!path.startsWith(prefix)) {
            ActionServlet servlet = (ActionServlet) request.getSession()
                    .getServletContext().getAttribute(
                            Globals.ACTION_SERVLET_KEY);
            String msg = servlet.getInternal().getMessage("processPath");
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
            return null;
        }

        path = path.substring(prefix.length());
        int slash = path.lastIndexOf("/");
        int period = path.lastIndexOf(".");
        if ((period >= 0) && (period > slash)) {
            path = path.substring(0, period);
        }
        return (path);
    }

    protected void executeForwardAction(ActionConfig config,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        String parameter = config.getParameter();
        if (parameter == null) {
            throw new ServletException(messages.getMessage("forward.path"));
        }
        request.getRequestDispatcher(parameter).forward(request, response);
    }

    protected void executeIncludeAction(ActionConfig config,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        String path = config.getParameter();
        if (path == null) {
            throw new ServletException(messages.getMessage("include.path"));
        }

        ActionServlet servlet = (ActionServlet) request.getSession()
                .getServletContext().getAttribute(Globals.ACTION_SERVLET_KEY);
        RequestDispatcher rd = servlet.getServletContext()
                .getRequestDispatcher(path);

        if (rd == null) {
            throw new ServletException(messages.getMessage("include.rd", path));
        }

        if (request instanceof MultipartRequestWrapper) {
            request = ((MultipartRequestWrapper) request).getRequest();
        }

        rd.include(request, response);
    }

    protected S2ExecuteConfig findExecuteConfig(String actionPath,
            String methodName) {
        S2ModuleConfig moduleConfig = S2ModuleConfigUtil.getModuleConfig();
        S2ActionMapping actionMapping = (S2ActionMapping) moduleConfig
                .findActionConfig(actionPath);
        // S2ExecuteConfigUtil#findExecuteConfig(String, String)ではメソッド名が一致していても
         // Action でurlPattern を指定していた場合は、URLパスがマッチしないため
        // executeConfig が取得できないので、メソッド名を指定して取得する。
        S2ExecuteConfig executeConfig = actionMapping
                .getExecuteConfig(methodName);
        return executeConfig;
    }
}

使い方

Struts形式のURLで動作させたいActionをstruts-config.xmlのactionタグに記述する。
標準で、Action、ForwardAction、IncludeAction、DispatchActionに対応している。
ForwardActionを使用する場合、action タグの parameter 属性には次のようにRoutingFilter通過後の形式のパスを設定する。

例.AddAction#hogeを実行する場合

<action path="/ForwardAction"
    type="org.apache.struts.actions.ForwardAction" parameter="/add.do?SAStruts.method=hoge">
</action>

MappingDispatchAction、LookupDispatchAction に対応させるには、getActionMethodName(ActionConfig, HttpServletRequest)をオーバーライドする。
struts-config.xml の action タグの属性については、path、type、parameter、forward のみ対応。他の属性については未対応。
また、Struts のモジュール機能にもおそらく未対応(使用したことがないので、よくわかりません。。。)
xxxAction.do 以外の形式の Struts URL に対応させる場合は web.xml の filter タグで、以下のように設定する。

<filter>
    <filter-name>routingfilter</filter-name>
    <filter-class>hoge.filter.MyRoutingFilter</filter-class>
    <init-param>
        <param-name>jspDirectAccess</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>strutsUrlPattern</param-name>
        <param-value>Struts URLパターンの正規表現(例 .*Action\.do$)</param-value>
    </init-param>
</filter>

ただし、/do/xxxAction などのようにStruts デフォルト形式から拡張子変更以上の変更を行っている場合は上記設定を行った上で、struts-config.xml の action タグの path 属性もURLに合うように変更必要。
例. /do/xxxAction の場合

<action-mappings>
    <action path="/do/AddAction" ...>


ModuleConfigのprotectedなactionConfigsにリフレクションで無理矢理アクセスしていたり等、あまりきれいなコードではないが、Filterひとつで実装してみた。
なお、初回アクセス時の処理の一部は、SAStrutsAOS(http://sourceforge.jp/projects/javasth/docs/sa-struts-aos.jar/ja/2/sa-struts-aos.jar)のS2AOSModuleConfigを参考にさせていただきました。
Filterひとつで無理矢理実装するのではなく、SAStrutsAOSのようにS2ModuleConfigを継承すれば、もう少しマシなコードにできますね。
それはまた今度にでも。

HOT deployでのClassCastException対策その2

現時点(Seasar2.4.34まで)のHOT deployでは、HttpSessionを直接使用して、リクエストをまたいでHttpSessionに格納されたオブジェクトを取得してキャストすると、ClassCastExceptionが発生する。

例.XxxAction#indexでセッションにXxxDtoを格納して、XxxAction#index2でセッションから取得する。

public class XxxAction {
    @Resource
    protected XxxDto xxxDto;

    @Execute(validator = false)
    public String index() {
        HttpSession session = RequestUtil.getRequest().getSession();
        session.setAttribute("xxxDto", xxxDto);
        System.out.println(xxxDto.getClass().getClassLoader());	// ※1
        return "xxx.jsp";
    }

    @Execute(validator = false)
    public String index2() {
        HttpSession session = RequestUtil.getRequest().getSession();
        System.out.println(XxxDto.class.getClassLoader());	// ※2
        System.out.println(session.getAttribute("xxxDto").getClass().getClassLoader());	// ※3
        XxxDto dto = (XxxDto) session.getAttribute("xxxDto");	// ここでClassCastExceptionが発生
        return "xxx.jsp";
    }
}

原因

原因は、やっぱりクラスローダーが異なるから。
上記を実行すると、次のように標準出力される。(上から順に※1、2、3に対応)

org.seasar.framework.container.hotdeploy.HotdeployClassLoader@10bc9e9
org.seasar.framework.container.hotdeploy.HotdeployClassLoader@b630d1
org.seasar.framework.container.hotdeploy.HotdeployClassLoader@10bc9e9

※2と3のクラスローダーが異なっていることが原因。
HotdeployClassLoaderは、org.seasar.framework.container.hotdeploy.HotdeployFilterにより、
リクエストの度に、生成と破棄が行われる。※1と2はリクエストが異なるので、当然HotdeployClassLoaderも別のものになるが、※3は1でセッションに格納したXxxDtoを取得しただけなので、クラスローダーも1と同じHotdeployClassLoaderになる。

対策

回避方法は以下の2通り。

1.HttpSessionを直接操作せず、Map sessionScopeを使用する。

Seasar2にはsessionScopeというHttpSessionの中身がマッピングされたMapが用意されているので、これを使用すればよい。sessionScopeを使用すると、org.seasar.framework.container.hotdeploy.HotdeployUtil#rebuildValueというメソッドが実行され、これにより、セッション中のオブジェクトのクラスが、最新のクラスローダーで再ロードされた上で、シリアライズ、デシリアライズにより、オブジェクトが再生成されるので、クラスローダーの不一致がなくなる。
sessionScopeの取得には、フィールドを用意してDIしてもらうか、次のようにS2Containerから直接取得すればよい。

Map<String, Object> sessionScope = SingletonS2Container.getComponent("sessionScope");

例.

public class XxxAction {
    @Resource
    protected XxxDto xxxDto;

    @Resource
    protected Map<String, Object> sessionScope;

    @Execute(validator = false)
    public String index() {
        sessionScope.put("xxxDto", xxxDto);
        return "xxx.jsp";
    }

    @Execute(validator = false)
    public String index2() {
        XxxDto dto = (XxxDto) sessionScope.get("xxxDto");
        return "xxx.jsp";
    }
}
2.セッションに格納するオブジェクトに@Component(instance = InstanceType.SESSION)アノテーションをつける。

@Component(instance = InstanceType.SESSION)アノテーションがつけられたオブジェクトは、Seasar2が自動的にセッションスコープで管理してくれる。

例.

@Component(instance = InstanceType.SESSION)
public class XxxDto implements Serializable {
    private static final long serialVersionUID = 1L;
	
    String xxx;
}

public class XxxAction {
    @Resource
    protected XxxDto xxxDto;

    @Execute(validator = false)
    public String index() {
        xxxDto.xxx = "hoge";
        return "xxx.jsp";
    }

    @Execute(validator = false)
    public String index2() {
        String xxx = xxxDto.xxx;
        return "xxx.jsp";
    }
}

番外だけど、対策1で出てきたHotdeployUtil#rebuildValueを直接実行することでも回避できる模様。ただし、正式にドキュメントに記載されている方法でもなく、あえてこの方法を選択する利点はないと思う。

例.

public class XxxAction {
    @Resource
    protected XxxDto xxxDto;

    @Execute(validator = false)
    public String index() {
        HttpSession session = RequestUtil.getRequest().getSession();
        session.setAttribute("xxxDto", xxxDto);
        return "xxx.jsp";
    }

    @Execute(validator = false)
    public String index2() {
        HttpSession session = RequestUtil.getRequest().getSession();
        XxxDto dto = (XxxDto) HotdeployUtil.rebuildValue(session.getAttribute("xxxDto"));
        return "xxx.jsp";
    }
}


なお、別にSeasar2に限った話ではなく、本来そうすべきことだが、セッションに格納するオブジェクトは、必ず、java.io.Serializableを実装する必要がある。Serializableを実装していないオブジェクトを、リクエストをまたいでセッションにputしてgetすると、対策1のようにsessionScopeを使用していてもClassCastExceptionが発生する。
おそらく、org.seasar.framework.container.hotdeploy.HotdeployUtil#rebuildValueから実行される、org.seasar.framework.container.hotdeploy.RebuilderImpl#rebuildで、ObjectOutputStream#writeObjectを実行する際に、NotSerializableExceptionが発生するためだと思うが、NotSerializableExceptionはcatchされて、再生成されていない元のオブジェクトがそのまま返されるため、クラスローダーの不一致が発生する。
NotSerializableExceptionが飛んでくるわけではないので、sessionScopeを使っているのにClassCastExceptionが発生する場合は、焦らずにSerializableなオブジェクトになっているか確認するといい。

補足

これを書くのに色々調べていたら、こんなのを見つけた。どうやら、次のバージョン(Seasar 2.4.35)からは、HttpSessionから直接getAttributeしてもClassCastExceptionは発生しなくなる模様。これでまた一つ嵌りどころがなくなる。

[S2Container] HOT deploy で Http セッションから取得したオブジェクトを新しいクラスローダでロードする処理を,sessionScope の Map ではなく HttpSession で行うようにしました.
https://www.seasar.org/issues/browse/CONTAINER-351

システム日付を変更する方法

システムテスト等で、システム日付を任意に設定したいことはよくあると思う。でも、テストサーバーのOSの日付を変更するのは、なかなか難しいことが多い。ファイルのタイムスタンプがぐちゃぐちゃになって管理上問題になるだけでなく、アプリケーションで問題が起こったり、ミドルウェアで問題が起こることもある。そんな時、RDBMSOracleを使用していて、かつアプリケーションの現在日付にOracle SYSDATEを使用している場合には、SYSDATEを任意に変更する手が使える。(Oracle 10g。他はやったことなし)

システム日付を任意に設定する
http://www.shift-the-oracle.com/debugging-tips/fixed_date.html

ここに記載されているように、

ALTER SYSTEM SET FIXED_DATE = 'YYYY-MM-DD-HH24:MI:SS' SCOPE=MEMORY;

でSYSDATEを変更できる。
戻すときは、*1

ALTER SYSTEM SET FIXED_DATE = none SCOPE=MEMORY;

なお、この方法では、時間がとまってしまうので、任意の日付に設定してかつ時間を進めていきたい場合は、

システム日付を連続的に変更する
http://www.shift-the-oracle.com/debugging-tips/flexible-sysdate.html

にあるようにプロシージャを作成して、無限ループさせながら、連続的に上記SYSDATEの変更SQLを発行すればよい。


ちなみに、Data PumpのようにOracleジョブスケジューラを使用しているものは、SYSDATE変更の影響を受けるみたいなので注意。自分は、SYSDATE固定に設定した上で、expdpしたのに、待てども待てどもエクスポートが始まらなくて焦りました。


*1:参照元サイトには、

ALTER SYSTEM SET FIXED_DATE = none SCOPE=MEMORY;

は危険なので、

ALTER SYSTEM RESET FIXED_DATE SCOPE=SPFILE SID='*';

を使えという記載があるが、Solaris 10でOracle Database 10g Enterprise Edition Release 10.2.0.3.0 - 64bit(非RAC)で試した限りでは、

ALTER SYSTEM RESET FIXED_DATE SCOPE=SPFILE SID='*';

はエラーになってしまった。SCOPE=MEMORYや、SCOPE=BOTH、SID=にORACLE_SIDを入れるのも試してみたが、どれもエラーになった。仕方なく、

ALTER SYSTEM SET FIXED_DATE = none SCOPE=MEMORY;

を使用しているが、特に問題が発生したことはない。

HOT deployでのClassCastException対策

Seasar2.4.xのHOT deployでは、filterやutil、entityといったのようなSMART deploy対象外のクラスから、actionやservice、dao、dtoといったSMART deploy対象のクラスを参照すると、ClassCastExceptionが発生する。この対策を考えてみた。
ちなみに、COOL deployでは発生しない。

2009/4/14 追記
utilやentityがSMART deploy対象外クラスと書いたが、S2Containerにコンポーネントの自動登録がされない(おそらくS2Containerがインスタンスを管理する必要がないから)だけで、コードの変更はHotに反映されるし、クラスローダーもちゃんとHotdeployClassLoaderになる。なので、SMART deploy対象外というのは誤りと思いますので、訂正しました。

原因

なぜ、HOT deployでは、SMART deploy対象外のクラスからSMART deploy対象のクラスを参照すると、ClassCastExceptionが発生するのか?例えば、SMART deploy対象外のクラスで次のような記述があると、ClassCastExceptionが発生する。

XxxDto xxxDto = (XxxDto) container.getComponent("xxxDto");

上記の場合、S2Containerから取得されるXxxDtoは、dtoがSMART deploy対象クラスであり、HOT deploy対象クラスとなるため、org.seasar.framework.container.hotdeploy.HotdeployClassLoaderでロードされたXxxDtoとなる。一方、キャスト先のXxxDtoは、この記述を実装しているクラスがSMART deploy対象外であるため、オリジナルのクラスローダー(Tomcatならorg.apache.catalina.loader.WebappClassLoader)にロードされたXxxDtoとなる。Javaでは、同一クラスであってもロードしたクラスローダーが異なれば、別クラスとして認識されるため、ClassCastExceptionが発生してしまう。
ちなみに、これがWebアプリであれば、一度ClassCastExceptionが発生すると、次回のリクエスト以降は発生しなくなる。これは、XxxDtoがClassCastException発生直前にオリジナルクラスローダーにロード済みのため、次回以降のリクエスト時には、例えSMART deploy対象クラスであってもHotDeployClassLoaderがXxxDtoをロードしないようになっており、S2Containerから取得されるXxxDtoもオリジナルクラスローダーにロードされたXxxDtoとなるため。

対策

原因を見れば分かる通り、同一のクラスローダーにロードされるようにすればClassCastExceptionは防げる。
その方法をいくつか考えてみる。

案1.SMART deploy対象外クラスを対象にする。

全部HotDeployClassLoaderにロードさせるようにできればClassCastExceptionは起きないという発想だけど、Creator、Customizerを作成して、S2Containerにコンポーネント登録させるところはできたとしても、HOT deployに対応させるのは、おそらく相当敷居が高いと思う。

案2.SMART deploy対象クラスとSMART deploy対象外クラスを明確に分離して、対象外クラスから対象クラスを参照しないようにする。

対象クラスと対象外クラスでパッケージを分けてしまう。
http://d.hatena.ne.jp/higayasuo/20090317/1237268663

http://ml.seasar.org/archives/seasar-user/2009-February/016916.html
で、示されている方法。これが公式な対策ということになるのかな?

ただ、個人的にはどうもこの方法はしっくりこない。
例えば、Filterでアクセスロギングを行っていて、ログインユーザー情報も併せてロギングするために、FilterでHttpSession上のログインユーザーDtoを取得しているようなアプリの場合、パッケージ分割の考え方にならうと、ログインユーザーDtoはSMART deploy対象外パッケージに置くことになる。開発体制として、Filter等の共通処理部分は共通基盤チームみたいなのが開発して、個々の業務画面等を個々の開発者が開発するような体制(SIerではよくある体制だと思う)を想定した場合、ログインユーザーDtoなんかは、色んな画面で参照するものなので、個々の開発者が皆、パッケージの違いを意識しないといけなくなってしまう。個々の開発者はSMART deploy対象クラスからログインユーザーDtoを参照するので元々ClassCastExceptionが発生しないにも関わらず、だ。個々の開発者も皆、HOT deployのクラスローダー云々とかClassCastException云々を知るべきでその上でパッケージ分割理由も皆で共有する、というような方針であれば、これでいいと思うけど、個々の開発者にそこまで意識させない方針であれば、パッケージ分割がどうしても非直観的になってしまわないだろうか?という気がする。

2009/3/19 追記
コメントで指摘いただいた。パッケージの違いはIDEが吸収してくれる(自力でパッケージを書くわけではない)から個々の開発者が意識する必要はないと思う。

案3.diconに書く。

SMART deploy対象外クラスから参照されるSMART deploy対象クラスをdiconに書くことで、SMART deploy対象外にする方法。Seasar2.3のコンポーネント自動登録を使うのも同様。
デメリット?は、SMART deploy対象外になるので、customizer.diconで定義したInterceptorとかが適用されなくなること。まあ、S2Containerの管理対象からはずれるわけではないので、Seasar2.3の記述方法で記述すれば、AOPも仕掛けられるけど。

案4.HotDeployClassLoaderより先にオリジナルクラスローダーにロードさせる。

上述の通り、HotDeployClassLoaderはオリジナルクラスローダーにロード済みのクラスはロードしないので、SMART deploy対象外クラスから参照されるSMART deploy対象クラスを、HotDeployClassLoaderより先にオリジナルクラスローダーにロードさせることで、オリジナルクラスローダーのみを使用することとなり、ClassCastExceptionを防ぐ。
HOT deploy対象外だけど、SMART deploy対象ということになる。なので、案3.のようにcustomizer.diconの定義内容が適用されないなんてこともない。
やり方は、Webアプリであれば、例えば次のようなFilterを作成し、web.xmlで、HotdeployFilterよりも前に適用されるように定義すればよい。HOT deployのためだけの処理が運用時に入っているのが気に入らなければ、COOL deploy時は、web.xmlコメントアウトすることで処理をはずすこともできる(案3.でもdiconから記述を削除すれば同じことになりそうだけど、それによってcustomizer.diconの適用がされることになったり等、影響が出るのでこちらの方が安心な気がする)

XxxDtoがSMART deploy対象外クラスから参照される場合の例

public class ExcludeHotdeployFilter implements Filter {

    private FilterConfig filterConfig;

    public void init(FilterConfig filterConfig) {
        this.filterConfig = filterConfig;
    }

    public void destroy() {
        this.filterConfig = null;
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        Class clazz = XxxDto.class;
        chain.doFilter(req, res);
    }
}

例では、XxxDtoを直書きしているけど、Filterなのでパラメータを使って、Class.forName()でやれば、web.xmlにクラス名を記述することも可能。


とりあえず、Webアプリであれば、案4.が簡単でデメリットも特になくてよいと思う。
ただし、あまりないとは思うけど、SMART deploy対象外クラスから参照するSMART deploy対象クラスが大量にある場合は、案3.とか案4.もしんどそうなので、案2ということになるかなあ。