読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり、所属会社の見解ではありません。 GitHub: takahashikzn

コードレベルで変数のスコープを管理するカスタムタグ

Java Struts2

初めまして。ルート42株式会社の高橋と申します。


弊社では自社プロダクトの一つとしてフルスタックフレームワークの構築をしています。
その過程で得られたノウハウを、備忘録も兼ねてブログで公開してみることにしました。
不定期の更新になると思いますが、なるべく更新するように頑張るかもしれませんし、挫折するかもしれません(-_-)


ちなみにさりげなく奥ゆかしく宣伝ですが、弊社のHPは http://root42.jp/ です

今回のお題

弊社のフレームワーク(R42FWといいます)はStruts2をベースにしており、
各種の値を自動的にレンダリングするJSP部品をたくさん用意しています。
例えば、java.util.Dateの値を和暦にフォーマットして表示する、というようなものです。まあ、ありがちですね。


さて、R42FWの開発を始めてから今日まで、struts2のタグでローカル変数を扱っていたのですが、
最近いい加減に我慢できなくなってきた問題があります。それは、


などでインクルードしたJSPの中でが使われると、呼び出し元への副作用がある


というものです。

問題のあるコードの例

foo.jsp

<%@ page pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s"%>

<s:set var="hoge" value="1" />

hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される

<s:include value="./bar.jsp" />

hoge is <s:property value="#hoge" /> ← "hoge is 2"が表示される

bar.jsp

<%@ page pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s"%>

<s:set var="hoge" value="2" />

hoge is <s:property value="#hoge" /> ← "hoge is 2"が表示される

この通り、呼び出し先のJSPで値が勝手に書き変わってしまいます。
これは、で定義した変数が事実上グローバル変数扱いになるために発生する現象です。


フレームワークのように、高度に部品化したJSPを多数提供しているケースにおいては、3段階、4段階にネストしたを頻繁に使います。
なので、このまま強引に作業を進めていくと、その先に待っているのはグローバル変数だらけのおぞましい地獄…(笑)てな具合になります。


そこで、ローカル変数のスコープを宣言できるカスタムタグJavaでいうところの、ブロック構文のようなコーディングができる)を作ることにしました。
今回作成したタグは次の2つです。

作ったタグの使い方

foo.jsp

<%@ page pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s"%>
<%@ taglib uri="(略)" prefix="r"%>

<r:block>
  <r:set-local var="hoge" value="1" />

  hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される

  <s:include value="./bar.jsp" />

  hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される
</r:block>

hoge is <s:property value="(#hoge == null ? 'null' : 'not null')" /> ← "hoge is null"が表示される

bar.jsp

<%@ page pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s"%>
<%@ taglib uri="(略)" prefix="r"%>

<r:block>
  <r:set-local var="hoge" value="2" />

  hoge is <s:property value="#hoge" /> ← "hoge is 2"が表示される
</r:block>

hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される

ソースコード

コメントをちゃんと書いてなくて恐縮ですが、ソースは以下の通りです。

ALV2にしますので、商用利用もどうぞ。お約束ですが無保証ですのであしからず。


tldファイル

  <tag>
    <description><![CDATA[
      ローカル変数を宣言する。<r:block>と対で使用する。
    ]]></description>
    <name>set-local</name>
    <tag-class>jp.root42.r42fw.export.web.ui.jsptags.SetLocalTag</tag-class>
    <body-content>JSP</body-content>
    <attribute>
      <description></description>
      <name>value</name>
      <required>false</required>
      <rtexprvalue>false</rtexprvalue>
    </attribute>
    <attribute>
      <description></description>
      <name>var</name>
      <required>false</required>
      <rtexprvalue>false</rtexprvalue>
    </attribute>
    <dynamic-attributes>false</dynamic-attributes>
  </tag>

  <tag>
    <description><![CDATA[
      ローカル変数のスコープを宣言する。<r:set-local>と対で使用する。
    ]]></description>
    <name>block</name>
    <tag-class>jp.root42.r42fw.export.web.ui.jsptags.BlockTag</tag-class>
    <body-content>JSP</body-content>
    <dynamic-attributes>false</dynamic-attributes>
  </tag>


Block.java

package jp.root42.r42fw.export.web.ui.components;

import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

import com.opensymphony.xwork2.util.ValueStack;

import org.apache.commons.lang.StringUtils;
import org.apache.struts2.components.Component;


public class Block
    extends Component {

    private final Map<String, Object> overridedVars =
        new HashMap<String, Object>();

    private final Map<String, Object> overridedAttrVars =
        new HashMap<String, Object>();

    public Block(final ValueStack stack) {
        super(stack);
    }

    /** {@inheritDoc} */
    @Override
    public boolean end(final Writer writer, final String body) {
        final boolean result = super.end(writer, body);

        // ----
        // 値を復元する その1
        for (final String key : this.overridedAttrVars.keySet()) {
            final Object value = this.overridedAttrVars.get(key);
            this.setAttrValue(key, value);
        }

        // ----
        // 値を復元する その2
        for (final String key : this.overridedVars.keySet()) {
            final Object value = this.overridedVars.get(key);
            this.setValue(key, value);
        }

        return result;
    }


    public void setVar(final String key, final Object value) {
        if (StringUtils.isBlank(key)) {
            return;
        }

        // ----
        // 現在のブロックにおける値の状態を記録しておく その1
        if (!this.overridedAttrVars.containsKey(key)) {
            final Object attrVar = this.getAttrValue(key);
            this.overridedAttrVars.put(key, attrVar);
        }

        this.setAttrValue(key, value);

        // ----
        // 現在のブロックにおける値の状態を記録しておく その2
        if (!this.overridedVars.containsKey(key)) {
            final Object var = this.getValue(key);
            this.overridedVars.put(key, var);
        }

        this.setValue(key, value);
    }

    protected Object getAttrValue(final String key) {
        return this.getStack().findValue("#attr['" + key + "']");
    }

    protected void setAttrValue(final String key, final Object value) {
        this.getStack().setValue("#attr['" + key + "']", value, true);
    }

    protected Object getValue(final String key) {
        return this.getStack().findValue("#" + key);
    }

    protected void setValue(final String key, final Object value) {
        this.getStack().setValue("#" + key, value, true);
    }
}


BlockTag.java

package jp.root42.r42fw.export.web.ui.jsptags;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.opensymphony.xwork2.util.ValueStack;

import org.apache.struts2.views.jsp.ComponentTagSupport;

import jp.root42.r42fw.export.web.ui.components.Block;


public class BlockTag
    extends ComponentTagSupport {

    /** {@inheritDoc} */
    @Override
    public Block getBean(final ValueStack stack, final HttpServletRequest req,
        final HttpServletResponse res) {
        return new Block(stack);
    }
}


SetLocal.java

package jp.root42.r42fw.export.web.ui.components;

import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.opensymphony.xwork2.util.ValueStack;

import org.apache.commons.lang.StringUtils;
import org.apache.struts2.components.Component;
import org.apache.struts2.components.ContextBean;


public class SetLocal
    extends ContextBean {

    private String value;

    public SetLocal(final ValueStack stack) {
        super(stack);
    }

    /** {@inheritDoc} */
    @Override
    public boolean end(final Writer writer, final String body) {
        final Object o;
        if (this.value == null) {
            if (StringUtils.isEmpty(body)) {
                o = this.findValue("top");
            } else {
                o = body;
            }
        } else {
            o = this.findValue(this.value);
        }

        // ----
        final Block block = this.getEnclosingBlock();

        block.setVar(this.getVar(), o);

        return super.end(writer, "");
    }

    /**
     * 現在のset-localタグを囲っているblockタグを取得する。
     * 
     * @return Block
     */
    private Block getEnclosingBlock() {
        @SuppressWarnings("unchecked")
        final List<Component> componentStack =
            new ArrayList<Component>(this.getComponentStack());

        Collections.reverse(componentStack);

        // スタックを上から順に辿っていき、最初に見つかったBlockを使う
        Block block = null;
        for (final Component comp : componentStack) {
            if (comp instanceof Block) {
                block = (Block) comp;
                break;
            }
        }

        if (block == null) {
            throw new IllegalStateException("block not found: "
                + componentStack);
        } else {
            return block;
        }
    }

    public void setValue(final String value) {
        this.value = value;
    }

    /** {@inheritDoc} */
    @Override
    public boolean usesBody() {
        return true;
    }
}


SetLocalTag.java

package jp.root42.r42fw.export.web.ui.jsptags;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.opensymphony.xwork2.util.ValueStack;

import org.apache.struts2.views.jsp.ContextBeanTag;

import jp.root42.r42fw.export.web.ui.components.SetLocal;


public class SetLocalTag
    extends ContextBeanTag {

    /***/
    private String value;

    /** {@inheritDoc} */
    @Override
    public SetLocal getBean(final ValueStack stack,
        final HttpServletRequest req, final HttpServletResponse res) {
        return new SetLocal(stack);
    }

    /** {@inheritDoc} */
    @Override
    protected void populateParams() {
        super.populateParams();

        final SetLocal setLocal = (SetLocal) this.getComponent();
        setLocal.setValue(this.value);
    }

    public void setValue(final String value) {
        this.value = value;
    }
}



(追記)
こちらで示したタグを含めたライブラリをオープンソースとして公開しています。
こちらからどうぞ。