seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

EL式(EL2.2)をアプリケーションへ組み込む方法

概要

JavaJSP/JSFでは「EL式」という簡易な式言語を用いてオブジェクトの連鎖を評価することができる。


たとえば、"${foo[bar].baz}"というEL式では、fooという配列またはリストから、添え字を示すbarの位置のあるオブジェクトのbazというプロパティを評価することを表す。


こうゆう仕組みはJSPの中にとどまらず、設定ファイルなどでも変数展開のように使えると便利だと思うときがたびたびある。

また、ちょっとした数式を"eval"したい、という場合もあるだろう。


そこで、EL式を自分のアプリケーションから利用する方法について調べてみた。

EL式のアプリケーションへの組み込み

ELのバージョン

EL式にはいくつかバージョンがあり、

  1. JSP2.0のEL式 (${}による即時評価)
  2. JSF1.0のEL式 (#{}による遅延評価)
  3. JSP2.1とJSF1.1の、EL式を統合した、Unified EL(EL2.1)。JavaEE5以降、Tomcat6等
  4. JSP2.2と、ビーンのメソッド呼び出しなどをサポートするようになったEL2.2(→jsr245)。JavaEE6以降、Tomcat7等 ← 現在のメインストリーム?
  5. ラムダや複数式、代入をサポートするように大幅に言語拡張されたEL3.0(→jsr341) (JavaEE7, 2013/6リリース)


最新のEL3.0では、JSP/JSFだけでなくスタンドアロンでの利用を想定した"ELProcessor(→Javadoc)"のようなクラスも追加されており、自アプリケーションに組み込むのであれば、かなり容易くなっている。


反面、ウェブアプリ内でEL式を使う場合には、環境がまだJavaEE6やTomcat7だったりすると、新しいライブラリを利用するのは難しいかもしれない。

また、EL3はJava8の登場を待たず先行してラムダやコレクションをストリームで扱う仕組みを取り入れているなどの尖った部分が目立ち、今後のJava8との整合性も気になるところである。


とりあえず、まずは現行で広く使われているEL2.2を利用できるようにすることを考えてみる。

ライブラリの準備 (EL2.2のインタフェースおよび実装クラス)

EL式はJavaEE5以降でjavax.el.*パッケージ(→Javadoc)として標準化されている。

EL2.2のインタフェースであれば、"el-api-2.2.jar(→mvn)"などをクラスパスを通すことで、EL2.2を利用したソースコードコンパイルできるようになる。


実際に動作させるにはELの実装クラスも必要だが、Tomcat7やGlassfish-v3(JavaEE6)系では、これらのEL2.2の実装クラスも組み込まれている。


ウェブアプリケーションではなく、スタンドアロンなアプリケーションでEL2.2を利用したい場合は、Glassfishが提供している"el-impl-2-2.jar(→mvn)"のような実装クラスを参照に加える。


Mavenのpomで示せば、以下のような感じとなる。

    <dependency>
      <groupId>javax.el</groupId>
      <artifactId>el-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>el-impl</artifactId>
      <version>2.2</version>
      <scope>runtime</scope>
    </dependency>
ライセンス

なお、"el-impl-2-2.jar"は、Glassfishの成果物なのでライセンスはCDDLまたはクラスパス例外のGPLv2であり、
よって自アプリからライブラリとして参照するだけならば、自アプリのライセンスに影響を与えない。

準備するクラス

EL2.2を利用するには以下のクラスの準備が必要である。

  • ELContext (必須)
  • FunctionMapper (△必須、手抜き可能)
  • VariableMapper (△必須、手抜き可能)
  • ELResolver (必要ならば)
ELContextの実装

ELContextは抽象クラスであり、
FunctionMapper, VariableMapper, ELResolverの3つの要素を返すメソッドを定義する必要がある。

FunctionMapperの実装

FunctionMapperは、EL式で式の呼び出しがあった場合に、それを実際のメソッドと結びつけるためのものである。
EL式の中で「式」を使わないのであれば、nullを返すだけの簡単な実装でも良い。


あるいは、el-impl.jarの中に含まれる「FunctionMapperImpl」というクラスを借用しても良い。

VariableMapperの実装


VariableMapperは、EL式中で参照される変数を事前に定義しておくもので、変数名から変数を結びつけるためのものである。

それも単純なHashMapにつなげるだけの実装で問題ない。


あるいは、さらに手抜きをしてel-impl.jarの中に含まれる「VariableMapperImpl」というクラスを借用しても良い。

ELResolverの実装


ELResolverは、EL式の"肝"となる部分である。


EL式は、"${foo[bar].baz}"の文法を解釈し、foo, bar, bazの3要素に分割する。


このあと、

  • もしfooがマップであればbarをキーとして、
  • もしfooがリストであればbarを添え字として、
  • もしfooがリソースバンドルであればbarをリソースキーとして、

というように、オブジェクトの種類に応じて値を取得する方法が異なる点を吸収する必要がある。

この「マップなら〜、リストなら〜、リソースバンドルなら〜」といった解釈の違いをサポートするのがELResolverである。


これらのELResolverをまとめるためのCompositeELResolverというELResolverもある。


必要最低限のELResolverとしては、

    CompositeELResolver resolver = new CompositeELResolver();
    resolver.add(new ResourceBundleELResolver()); // リソースバンドルの解決用
    resolver.add(new MapELResolver()); // Mapの解決用
    resolver.add(new ListELResolver()); // Listの解決用
    resolver.add(new ArrayELResolver()); // 配列の解決用
    resolver.add(new BeanELResolver()); // Beanのsetter/getterの解決用

とすれば、一般的なEL式の解釈はできるようになる。


上記のELResolverは、上から順に評価され解決した時点で終了するので、addする順序は重要である。


また、JSPJSFなどでは、"暗黙の変数"のような仮想的な変数のようなものをサポートするためにさらにいくつかのELResolverを追加している。

たとえばJSP上のEL式で使えるrequest変数等は、実際にはVariableMapperに格納されている変数ではなく、ELResolverでブリッジ(もしくは射影)されている"仮想の変数"といえる。


自アプリケーションでも独自の暗黙の変数のようなものをサポートするのであればELResolverの拡張を行うことができる。

最低限の利用例

最低限、上記の準備を実装したものが以下の例となる。

    /**
     * [テスト] 最低限のEL式評価方法のテスト
     */
    public void testMinimum() {
        final CompositeELResolver resolver = new CompositeELResolver();
        resolver.add(new ResourceBundleELResolver()); // リソースバンドルの解決用
        resolver.add(new MapELResolver()); // Map, Propertiesの解決用
        resolver.add(new ListELResolver()); // Listの解決用
        resolver.add(new ArrayELResolver()); // 配列の解決用
        resolver.add(new BeanELResolver()); // Beanのsetter/getterの解決用

        final VariableMapper varMapper = new VariableMapperImpl(); // 借用
        final FunctionMapper funcMapper = new FunctionMapperImpl(); // 借用

        ELContext elContext = new ELContext() {
            @Override
            public ELResolver getELResolver() {
                return resolver;
            }
            @Override
            public FunctionMapper getFunctionMapper() {
                return funcMapper;
            }
            @Override
            public VariableMapper getVariableMapper() {
                return varMapper;
            }
        };

        // EL式の評価ファクトリ
        ExpressionFactory ef = ExpressionFactory.newInstance();

        // EL式で評価するための変数を設定する.
        varMapper.setVariable("foo", ef.createValueExpression("FOO", String.class));
        
        MyBean myBean = new MyBean();
        myBean.setX(1);
        myBean.setY(2);
        varMapper.setVariable("bar", ef.createValueExpression(myBean, myBean.getClass()));

        // EL式を評価する.
        String expression = "hello, ${foo}! ${bar.x + 234}";
        ValueExpression ve = ef.createValueExpression(elContext, expression, String.class);
        String ret = (String) ve.getValue(elContext);

        System.out.println("result=" + ret);
        // result=hello, FOO! 235
    }

変数"foo"に使っているビーンは以下の定義とする。

    public static final class MyBean {
        
        private int x;
        
        private int y;
        
        public int getX() {
            return x;
        }
        
        public void setX(int x) {
            this.x = x;
        }
        
        public int getY() {
            return y;
        }
        
        public void setY(int y) {
            this.y = y;
        }
	
	public void mes(String mes) {
	    System.out.println("mes=" + mes);
	}
        
        @Override
        public String toString() {
            return "(x: " + x + ", y:" + y + ")";
        }
    }

これで一応、"hello, ${foo}! ${bar.x + 234}"というEL式を解析し、
結果として「hello, FOO! 235」という値を得ることができるようになっている。

変数の準備方法

EL式の中にある変数は、VariableMapperから取得される。

(もしなければ仮想変数のようなものとしてELResolverのチェーン内を検索する。)


EL式はcreateValueExpressionメソッドで、ValueExpressionを作成した時点のELContextの中にある変数の状態を記憶するため、事前に変数を設定しておく必要がある。
(つまり、式を作成した時点で見えている変数の状態が記憶されている。)


変数も"ValueExpression"という形式で作成しなければならないが、
これはExpressionFactory#createValueExpression(Object, Class)というヘルパメソッドによって、
オブジェクトをそのまま返すだけのラッパーとして容易に作成することができる。


したがって、

    varMapper.setVariable("foo", ef.createValueExpression("FOO", String.class));

のように"foo"という変数名で、"FOO"という文字列を格納することができる。


なお、第二引数は、このValueExpressionが返すオブジェクトの型を表す。

EL式のValueExpressionの評価結果は、戻り値となるべき型に自動的に"型変換"される。

EL式の解析(読み込み)

EL式には、${}による即時評価と、#{}による遅延評価の二種類があるが、
EL2.2ではAPI的には${}も#{}も区別なく、どちらも同じ扱いとなっている。

(唯一、区別しているのは、1つのEL式の中で${}と#{}を混在させられないことぐらいである。)


たとえば、"${bar.x}"というEL式を解析する場合、

    String expression = "${bar.x}";
    ValueExpression ve = ef.createValueExpression(elContext, expression, Object.class);

このように「ValueExpression」という形で取得することになる。


つまり、${}でも#{}でもAPI的には、遅延評価型の扱いになっているわけである。


実際に変数の値を取得するには、ValueExpression#getValue()メソッドを呼び出す必要がある。

EL式の解析(書き込み)

遅延評価型なので、変数を更新したい場合も同じように使うことができ、たとえば以下のように記述することができる。

    String expression2 = "${bar.x}";
    ValueExpression ve2 = ef.createValueExpression(elContext, expression2, Object.class);
    ve2.setValue(elContext, 123);
    System.out.println("myBean.x=" + myBean.getX());
    // myBean.x=123

変数として参照されているmyBeanインスタンスの値が変更されていることが確認できる。


もちろん更新できるのはEL式が指すものがオブジェクトの場合のみである。


以下のようにリテラルを混在したEL式は読み込み専用である。

    String expression = "Hello, ${foo}!";
    ValueExpression ve = ef.createValueExpression(elContext, expression, Object.class);
    ve.setValue(elContext, 123); // ここで例外が発生する

書き込もうとすると、結果は"javax.el.PropertyNotWritableException: Illegal Syntax for Set Operation"と例外が発生する。

EL式の解析(アクションの呼び出し)

EL2.2ではEL式中でオブジェクトのメソッドが呼び出せる。

    String expression = "${bar.mes('hello')}";
    MethodExpression me = ef.createMethodExpression(elContext, expression, Object.class, new Class[0]);
    me.invoke(elContext, new Object[0]);
    // コンソールに「mes=hello」と出る。

メソッドmesの引数をEL式の中で与えているので、MethodExpressionにせずとも、
ValueExpression#getValue()でも同じ結果となる。


MethodExpressionのメリットは、EL式ではメソッドまでを指定し、引数はプログラム側から与えられる点であろう。

    String expression = "#{bar.mes}";
    MethodExpression me = ef.createMethodExpression(
        elContext, expression, Object.class, new Class[] {String.class});
    me.invoke(elContext, new Object[] {"こんにちは、世界!!"});
    // mes=こんにちは、世界!!

イベントに対する「#{アクション}イベントハンドラ」など、コールバックを受けるメソッドをEL式から示すような用途が考えられる。

暗黙の変数をサポートする独自のELResolverの作成

これまでの方法で、任意の変数を設定してEL式で評価することは可能となっている。

これに加えて、jspのEL式などで使われているように「暗黙の変数」をサポートする場合はELResolverを拡張する必要がある。


「暗黙の変数」は、変数名から何らかのオブジェクトにブリッジさせるものであり、変数とは異なりValueExpressionの中には状態が保持されておらず、実際の評価時に「その時の値が使用される」という特徴がある。


ELResolverは、EL式の「${first.second}」の式を解析した結果、

  1. firstに相当するものを解釈する
  2. firstに対するオブジェクトがわかっている場合にsecondに相当するものを解釈する

という2つの動きを行う。

ELResolverは受け取ったものを自分が認識できない場合は何もせず、次のELResolverに渡す、という動きになっている。


「暗黙の変数」を処理する場合は、firstの部分に「暗黙の変数名」が来た場合に何らかのオブジェクトを返すようにすればいい。

この場合、secondについては考慮しなくてよい。

なぜならば暗黙の変数が何らかのbeanやMapを返したとすると、このオブジェクトは既存のMapELResolverやBeanELResolverによって解釈することができるためである。


また、暗黙の変数は一般的に代入不可なので、代入処理は考慮しなくてよい。


そうすると、以下のようなELResolverの実装を行えばよい。

    /**
     * 暗黙の"implicit"コンテキストの型
     */
    private static class ImplicitContext extends HashMap<String, Object> {}

    /**
     * 暗黙の変数"implicit"をELContextから取得するためのELResolverの実装例.
     */
    private static class MyImplicitELResolver extends ELResolver {

        @Override
        public Class<?> getCommonPropertyType(ELContext context, Object base) {
            if (base == null) {
                // baseがnull、つまり${first.xxxx}のfirstの場合は、
                // 文字列として変数名を受け取ることを示す.
                return String.class;
            }
            return null;
        }

        @Override
        public Iterator<FeatureDescriptor> getFeatureDescriptors(
                ELContext context, Object base) {
            if (base != null) {
                // とりあえず空を返しておく.(手抜き、なくてもEL式は動く)
                return Collections.<FeatureDescriptor>emptyList().iterator();
            }
            return null;
        }

        @Override
        public Class<?> getType(ELContext context, Object base, Object property) {
            if (base == null && property != null) {
                String name = property.toString();
                if ("implicit".equals(name)) {
                    // ${first.second}の、firstの場合、
                    // それが"implicit"という名前であれば、ImplicitContextクラスを返す.
                    context.setPropertyResolved(true); // 解釈済みであることを示す
                    return ImplicitContext.class;
                }
            }
            return null; // 不明な場合は次のELResolverへ
        }

        @Override
        public Object getValue(ELContext context, Object base, Object property) {
            if (base == null && property != null) {
                String name = property.toString();
                if ("implicit".equals(name)) {
                    // ${first.second}の、firstの場合、
                    // それが"implicit"という名前であれば、ELContextに設定されている
                    // ImplicitContextコンテキストを返す.
                    // ※ ELContext#setContext()で事前に設定しておくこと.
                    context.setPropertyResolved(true); // 解釈済みであることを示す
                    return context.getContext(ImplicitContext.class);
                }
            }
            return null; // 不明な場合は次のELResolverへ
        }

        @Override
        public boolean isReadOnly(ELContext context, Object base,
                Object property) {
            return true;
        }

        @Override
        public void setValue(ELContext context, Object base, Object property,
                Object value) {
            throw new PropertyNotWritableException("代入はサポートされていません/base="
                    + base + "/property=" + property);
        }
    }

このMyImplicitELResolverは、"implicit"という仮想の変数名を解釈し、ELContextに設定されている
ImplicitContextクラスのコンテキストを返すようにしている。


ImplicitContextは、実体はただのマップだが、ELContextのコンテキストとしてクラスをキーしてアクセスするため、
ImplicitContextという型にして利用しやすいようにしている。


これを利用するコードは以下のような使い方ができる。

    // 文字列リストの変数を作成する.
    List<String> lst = Arrays.asList("aaa", "bbb", "ccc", "ddd");
    varMapper.setVariable("list", ef.createValueExpression(lst, List.class));

    // 暗黙の変数をもつEL式
    String expression = "${list[implicit.idx]}";
    ValueExpression ve = ef.createValueExpression(elContext, expression, String.class);

    // "implicit"変数の値となるオブジェクト
    ImplicitContext implicitContext = new ImplicitContext();
    elContext.putContext(ImplicitContext.class, implicitContext);

    for (int idx = 0; idx < lst.size(); idx++) {
        implicitContext.put("idx", Integer.valueOf(idx));
        String ret = (String) ve.getValue(elContext);
        System.out.println("ret[" + idx + "]=" + ret);
    }

"${list[implicit.idx]}"というEL式を解析して、一旦、ValueExpressionを取得する。


この時点で変数listはValueExpressionに格納されている状態となるが、"implicit.idx"は実体がないので、変数としては格納されていない状態である。


このあと、forループで"implicit.idx"の値を変えながら、同じValueExpressionを繰り返し評価する。


すると、結果は以下のようになる。

ret[0]=aaa
ret[1]=bbb
ret[2]=ccc
ret[3]=ddd


このように、ELResolverによる暗黙の変数の拡張も比較的簡単にできることがわかる。

EL式の関数のサポート

ELでは独自の関数を定義することができる。


EL2.2からはオブジェクトのメソッドを呼び出すことが容易になっているため出番も少なくなっていると思われるが、
EL標準の演算子では足りない汎用的な演算のために、アプリケーション固有の関数を用意することもできる。


たとえば、java.lang.Mathクラスにあるユーテリティ関数のうち、第一引数がdoubleのものを利用可能とする場合、

    final FunctionMapper funcMapper = new FunctionMapper() {
        
        private HashMap<String, Method> methods = new HashMap<String, Method>();
        
        {
            for (Method method : Math.class.getMethods()) {
                if (Modifier.isStatic(method.getModifiers())) {
                    String name = method.getName();
                    Class<?>[] params = method.getParameterTypes();
                    boolean accept = true;
                    if (params.length > 0) {
                        if (!params[0].isAssignableFrom(double.class)) {
                            // 第一引数がある場合、double型でなければ不可とする.
			    // ※ 同名の型違いの関数を避けるため
                            accept = false;
                        }
                    }
                    if (accept) {
                        methods.put(name, method);
                    }
                }
            }
        }
        
        @Override
        public Method resolveFunction(String prefix, String localName) {
            if ("math".equals(prefix)) {
                return methods.get(localName);
            }
            return null; // nullの場合は関数が未登録であることを示す
        }
    };

このようにFunctionMapperで関数名が与えられたら、その関数のメソッドを返すだけである。

あとは、そのように実装したfuncMapperをELContextで返すようにすれば良い。


上記コードでは、プリフィックスとして「math」を用いているため、EL式としては、以下のように使うことになる。

    varMapper.setVariable("a", ef.createValueExpression(10, Integer.class));
    varMapper.setVariable("b", ef.createValueExpression(20, Integer.class));

    String expression = "max=${math:max(a,b)}, min=${math:min(a,b)}";
    ValueExpression ve = ef.createValueExpression(elContext,
            expression, String.class);

    String ret = (String) ve.getValue(elContext);
    System.out.println("ret=" + ret);
    // 結果は、"ret=max=20.0, min=10.0"

変数a, bはInteger型だが、EL式は自動的に型変換をサポートしているため、double型として計算されている。

(仮に変数が文字列型の"10", "20"であっても、正しくdouble型に変換されて計算される。)

雑多なユーテリティ

システムプロパティへのアクセス

EL式の中でシステムプロパティを参照できるようにするには、"System.getPriperties()"の結果をマップに詰めなおして変数に設定する。

MapELResolverが設定されていれば、あとは通常のMapと同様に処理される。

    Properties sysProps = System.getProperties();
    HashMap<String, Object> sysMap = new HashMap<String, Object>();
    for (String name : sysProps.stringPropertyNames()) {
        sysMap.put(name, sysProps.getProperty(name));
    }
    varMapper.setVariable("sys", ef.createValueExpression(sysMap, Map.class));

    String expression = "${sys['java.version']}";

    ValueExpression ve = ef.createValueExpression(elContext,
        expression, String.class);
    String ret = (String) ve.getValue(elContext);

※ PropertiesクラスはMapインタフェースをもっているので上記のように詰め替えなくてもマップとしてアクセスは可能であるが、Propertiesには親となるPropertiesをカスケードチェインにできる機能があり、その場合、Map#get()は、そのチェインをさかのぼることができない問題があるため、厳密にいえば、詰め替え処理が必要となる。

ResourceBundleへのアクセス

ResourceBundleELResolverが設定されていれば、変数としてリソースバンドルを設定するだけで、あとは名前でアクセスできる。

    // リソースバンドル
    ResourceBundle res = ResourceBundle.getBundle(getClass().getCanonicalName());
    varMapper.setVariable("res", ef.createValueExpression(res, ResourceBundle.class));

    String expression = "${res['foo.bar.baz']}";

    ValueExpression ve = ef.createValueExpression(elContext, expression, String.class);
    String ret = (String) ve.getValue(elContext);

結論

独自にEL式を評価することは思った以上に簡単だった。


EL式は、若干の定型的なコードを書くだけで、労せずにしてリッチな変数展開の仕組みを利用できる、大変有益なものであった。

いままで、この仕組みを調べていなかったことを後悔したくなるほどである。


EL3.0からは、いっそうスタンドアロン的にEL式を使うための仕組みも拡充されているようなので、
今後はもっと積極的に使っていっても良い手法だと思われる。

EL式(EL3.0)をアプリケーションから活用する

概要

先の記事でEL2.2でのアプリケーションからの利用方法について調べたが、
ついでにEL3.0についても調べてみることにした。


利用方法も更に簡単になっており、便利で興味深い機能も増えている。

EL3の特徴

EL3.0では、EL2.2から文法を大幅に拡張している。

互換性は維持されているので、引き続きEL2.2と同じ使い方は可能である。

  • 複数の式をセミコロンで区切って書ける。
  • 文字列結合演算子(+=)が使える。
  • ローカル変数への代入ができる
  • リスト、マップ、セットのリテラル表現が使える。
  • クラスのStaticメソッド/フィールドのアクセスができる。
    • その対象となるクラスをインポートする機能が追加されている。
    • java.lang.*のパッケージはインポート済みなので、Math.max()などが標準で使える
  • ラムダとストリームを使った繰り返し処理ができる。
  • ELProcessorというアプリ側からEL評価環境を簡単に作成できるコンビニエンスクラスが追加されている。
互換性

EL3では文法が大幅に拡張されているが、API的には2.2の延長にあり、仕組みは変わっていない。

先の記事で示したELResolverまわりが引き続き"肝"になる部分であることに変わりない。


ELProcessorという、あらたにELを独自に利用する場合に便利なクラスが用意されているが、
これは、EL2.2までは、独自にELContextを準備し、ELResolverを設定するなどの一連の定型的な作業が必要だったところを、
EL3.0では、これを一発で行ってくれる"便利クラス"として用意されたにすぎない。


※ ただし、Staticフィールドのリゾルバや、ローカル変数への代入などを可能とするための新しいリゾルバが用意されているので、
もし、EL3.0の機能を使う場合にはEL2.2で使用していたリゾルバに加えて、これらも設定する必要がある。


既存のシステムはEL3.0にライブラリを差し替えたしても、そのまま動くことになっている。

準備するライブラリ

mavenリポジトリから取得するのが手早いと思う。

mavenのpomで示す。

    <dependency>
      <groupId>javax.el</groupId>
      <artifactId>javax.el-api</artifactId>
      <version>3.0.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>javax.el</artifactId>
      <version>3.0.0</version>
      <scope>runtime</scope>
    </dependency>

なお、先のEL2.2の実験例は、上記のEL3.0のライブラリに差し替えても、
問題なくコンパイルも実行もできた。

ELProcessorを使ってみる。

EL3.0評価環境の一発作成

EL3.0で用意された、コンビニエンスクラス"ELProcessor"を使ってみる。

    ELProcessor elProc = new ELProcessor();

これだけでOK.

これだけでEL3.0に対応したELContextや各種ELResolverやMapperなどの設定を行ってくれる。

ローカル変数の定義

ローカル変数を設定するのも簡単になっている。

    elProc.defineBean("foo", new BigDecimal("123"));
    elProc.defineBean("bar", "brabrabra");

ただし、これはEL2.2までのVariableMapperへの"変数"の設定とは意味が異なり、
EL3.0のStandardELContext内部が保持している"ローカル変数"への設定となる。


EL3.0のローカル変数はBeanNameELResolverという新しいリゾルバを用いた仕組みで、
つまり、EL2.2まででは暗黙の変数やスコープ変数用に独自にELResolverで作成していたものを
自動的に用意してくれているものである。

VariableMapperの変数の定義

VariableMapperによる変数定義もサポートされている。

    // 変数の定義
    elProc.setVariable("v1", "foo * 2");

引数のEL式を評価する"ValueExpression型"としてVariableMapperに設定される.

Variableはローカル変数よりも優先され、setValueやEL式の代入では更新できずエラーとなる.

evalによる評価
    Number ret = (Number)elProc.eval("foo + 1");
    assertEquals(124, ret.intValue());

ELProcessor#eval()によるEL式の評価も簡単になっている。


ただし、EL式は${}で囲んではならない

ELProcessorのgetValue, setValue, evalの各メソッドは渡された式を強制的に「${}」で囲むため、
引数として「${}」を含めると内部的には「${${}}」と二重となり文法エラーになってしまう。

getValueによる評価
    String ret = (String)elProc.getValue("bar += '☆' += foo", String.class);
    assertEquals("brabrabra☆123", ret);

※ 式全体が${}で必ず囲まれるので、従来のように"${foo}${bar}"のようには書けない。
※ そのかわりEL3でサポートされた"文字列結合演算子 += "を使うと良い。

setValueによるローカル変数への代入
    elProc.setValue("foo", "1234");
    elProc.setValue("baz", "1234"); // ※ 存在しない変数への代入も可

"baz"というローカル変数は事前定義していないが、代入が可能である。

EL式からのローカル変数の代入

EL3.0からEL式でローカル変数を代入できるようになった。

    // EL式からのローカル変数への代入例
    elProc.eval("qux = [1,2,3,4]"); // リストも定義可能
    elProc.eval("map = {'a': 111, 'b': 222}"); // マップも定義可能

    System.out.println("qux=" + elProc.getValue("qux", Object.class));
    // qux=[1, 2, 3, 4]
    System.out.println("map=" + elProc.getValue("map", Object.class));
    // map={b=222, a=111}

EL3.0では、通常の数値や文字列といったリテラルの他、リスト(List)、マップ(Map)、セット(Set)を、宣言可能である。


また、存在しない変数への代入も可能である。

VariableMapperの変数の利用

VariableMapperの"変数"はEL式からは参照としてのみ利用できる。

また、同名のローカル変数がある場合はVariableMapperのほうが優先される。


※ これはローカル変数がELResolverによる仕組みであり、VariableMapperはELResolverでオブジェクトの解決を行う前に変数名が解決される為であろう。

    // ローカル変数とVariableMapper変数の混在
    Integer v = (Integer) elProc.getValue("v1 + foo", Integer.class);
    assertEquals(Integer.valueOf(123 + 123 * 2), v);

参照する分にはローカル変数もVariableMapperの変数も違いはない。


ただし、setValueメソッドや、EL式での代入によるVariableMapper変数の更新はできない。

    // VariableMapper変数の更新はできない.
    try {
        elProc.setValue("v1", 4567);
        assertTrue(false);

    } catch (PropertyNotWritableException ex) {
        System.out.println(ex.toString());
        // javax.el.PropertyNotWritableException: Illegal Syntax for Set Operation
    }

    // VariableMapper変数はEL式からも代入できない.
    try {
        elProc.eval("v1 = 4567");
        assertTrue(false);

    } catch (PropertyNotWritableException ex) {
        System.out.println(ex.toString());
        // javax.el.PropertyNotWritableException: Illegal Syntax for Set Operation
    }
複数式の実行

EL3.0では1つのEL式の中にセミコロンで区切って複数の式を書ける。

式の結果は最後の式の評価結果となる。

    Integer ret = (Integer) elProc.getValue("x=1;y=2;z=x+y", Integer.class);
    assertEquals(Integer.valueOf(3), ret);

※ なお、文法上「複文」ではなく、「セミコロン演算子」という扱いである。CやJavaScriptでいうところのカンマ演算子みたいなもの。

Staticフィールドへのアクセス

EL3.0ではクラスのスタティックフィールドやメソッドへのアクセスが容易になった。

    elProc.defineBean("Color", new ELClass(java.awt.Color.class));

    // スタティックフィールドのアクセス
    Color color = (Color) elProc.eval("Color.red");
    assertEquals(java.awt.Color.red, color);

    // スタティックメソッドの呼び出しも可
    Color color2 = (Color) elProc.eval("Color.decode('#ffffff')");
    assertEquals(java.awt.Color.white, color2);

ELClassにラップされたクラスに対してフィールドまたはメソッドを評価することができる。


※ EL3.0で新設されたStaticFieldELResolverによって解決される。


また、EL3.0ではデフォルトで「java.lang.*」のパッケージをインポートしているため、
java.lang.Mathや、java.lang.Integerなどのクラスのスタティックメソッドを自由に呼び出せる。

    Integer ret = (Integer) elProc.getValue("x=10;y=20;Math.max(x,y)", Integer.class);
    assertEquals(Integer.valueOf(20), ret);


任意のクラスをインポートすることで、EL式から利用することも可能となる。

    elProc.getELManager().importClass("java.sql.Types");
    Integer ret = (Integer) elProc.getValue("Types.BLOB", Integer.class);
    assertEquals(Integer.valueOf(java.sql.Types.BLOB), ret);
EL式用の関数の定義

EL式で使用される独自の関数の宣言も従来よりも簡単になっている。

    // EL3の独自関数の定義方法
    Method method;
    try {
        method = getClass().getMethod("strJoin",
                new Class[] {String.class, List.class});
        elProc.defineFunction("myFn", "join", method); // staticメソッドでないとダメ

    } catch (NoSuchMethodException ex) {
        throw new RuntimeException(ex);
    }

呼び出される関数は以下のように定義されているものとする。

    /**
     * ELから呼び出される関数
     * @param 区切り文字
     * @param args 文字列として連結する要素が格納されたリスト
     * @return 文字列とした連結された結果
     */
    public static String strJoin(String sep, List<Object> args) {
        StringBuilder buf = new StringBuilder();
        if (args != null) {
            for (Object arg : args) {
                if (buf.length() > 0) {
                    buf.append(sep);
                }
                buf.append(arg != null ? arg.toString() : "");
            }
        }
        return buf.toString();
    }

以下のように呼び出すことができる。

    Object ret = elProc.eval("myFn:join(',', ['aaa', 'bbb', 'ccc', 123])");
    assertEquals("aaa,bbb,ccc,123", ret);

EL3.0からはリストのリテラルを表現できるようになったので、
可変長引数のように複数個のデータを関数に渡すことも容易になった。

EL3のラムダとストリームの利用例

EL3.0ではラムダ式やコレクションのストリームが使えるようになっている。

    // EL3のラムダ式の利用例
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
    elProc.defineBean("list", list);
    
    // EL3の中間変数を使ったラムダ式とforEachによる繰り返し演算例
    String lambda1 = "sum=0;list.stream().forEach(x->(sum=sum+x));sum";
    Number ret1 = (Number) elProc.eval(lambda1); 
    assertEquals(1 + 2 + 3 + 4 + 5 + 6, ret1.intValue());

Listや配列に対してstream()して列挙させ、forEachにより全要素をチェックする。


これと同等なものはsumやaverage, max, min, countなどの集合演算があるので、もっとコンパクトにかける。

    // EL3のストリームと集合演算
    String lambda1b = "list.stream().sum()";
    Number ret1b = (Number) elProc.eval(lambda1b); 
    assertEquals(1 + 2 + 3 + 4 + 5 + 6, ret1b.intValue());


汎用的にはreduceを使う方法もある。

    // EL3のストリームと全体演算(reduce)、初期値0なのでストリームが空でも0となる。
    String lambda1c = "list.stream().reduce(0,(a,b)->a+b)";
    Number ret1c = (Number) elProc.eval(lambda1c);
    assertEquals(1 + 2 + 3 + 4 + 5 + 6, ret1c.intValue());


複数式やリストの構築が可能なので、以下のようなことも可能なようだ。

    // EL3のラムダ式を使ったリストのフィルタリングと型変換例
    String lambda2 ="lst=[];list.stream().forEach(x->((x % 2 == 0)?" +
            "lst.add(Integer.toString(x)):null));lst";
    @SuppressWarnings("unchecked")
    List<String> ret2 = (List<String>) elProc.eval(lambda2);
    assertEquals(Arrays.asList("2", "4", "6"), ret2);

※ 文字列に変換しているのはEL式内の型変換で数値がLongやBigDecimalに変換がされているかもしれないので。


もちろん、"filter"を使えば、もっとコンパクトにかける。

    // EL3のラムダ式を使ったリストのフィルタリングと型変換例(filter+map版)
    String lambda2b = "list.stream().filter(x->(x % 2 == 0)).map(" +
            "x->Integer.toString(x)).toList()";
    @SuppressWarnings("unchecked")
    List<String> ret2b = (List<String>) elProc.eval(lambda2b);
    assertEquals(Arrays.asList("2", "4", "6"), ret2b);


降順ソートして上位3件まで絞り込むようなこともラムダ式とストリームを使えば可能になる。

    // 降順ソートの上位3件の取得 (sortedとlimit)
    String lambda = "list=[9,3,6,4];" +
        "cmp=(a,b)->-(a-b);" + // 比較関数をラムダ変数で定義
        "cnv=(x)->Integer.toString(x);" + // タイプ変換関数をラムダ変数で定義
        "list.stream().sorted(cmp).limit(3).map(cnv).toList()";
    assertEquals(Arrays.asList("9", "6", "4"), elProc.eval(lambda));

ラムダ式は変数にできるので、コードを見やすくしたり、同じ関数を繰り返し使うようなことも可能である。


集合演算ではコレクションが空の場合は計算していない = 「値がない」という状態になるため、
そのような状態がありえる関数では、値がないかもしれないことを表すOption型として返される。

たとえば先頭1行を取得する場合、もし空の場合にどうするか決定するためorElse関数を使う。

    // 最初のアイテムを求める。コレクションが空ならば空文字にする。 (findFirstとorElse)
    assertEquals("", elProc.eval("list=[];list.stream().findFirst().orElse('')"));
ラムダの引数・変数のスコープと、高階関数性の確認 (2014/02/09追加)

ラムダ式は値として引数にもできるし戻り値にもできる = 高階関数性がある。

    // ラムダ式を返すラムダ式の変数格納と、変数からのラムダ式の呼び出し
    Number ret = (Number) elProc.eval("fn=a->(b->a+b);fn2=(a,f)->f(a);fn2(12,fn(21))");
    assertEquals(33, ret.intValue());

なお、ラムダは"javax.el.LambdaExpression型"として作成されるので、プログラム側からもinvokeすることは可能である。


ラムダの引数は、ラムダを生成した時点の値が保持される。

    elProc.eval("fn=a->(b->a+b)");
    elProc.eval("arg=10;fn1=fn(arg);arg=20;fn2=fn(arg)");
    Number ret = (Number) elProc.eval("arg=50;fn1(arg)+fn2(arg)");
    assertEquals(10 + 50 + 20 + 50, ret.intValue());

ラムダ引数はラムダが生成されたときの値を保持しており互いに干渉していないことが確認できる.

このあたりの仕組みは、EL3のELContext.html#enterLambdaScope(java.util.Map), ELContext#getLambdaArgument(String arg), ELContext.html#exitLambdaScope()などの仕組みによるものと思われる。


しかし、ラムダ式の内部から引数でない変数を参照した場合は、このように動きにはならない。

    elProc.eval("a=1234;fn=x->(x+a)");
    Number ret = (Number) elProc.eval("a=5678;fn(2345)");
    assertEquals(5678 + 2345, ret.intValue());

上記のように(試した限りでは)一般的なクロージャと異なりEL3のラムダ内からの引数でない変数へのアクセスはレキシカルスコープではない


この場合、Objective-CC#C++などのクロージャやラムダであればラムダ式を生成した時点の外部変数の参照はキャプチャされるのが一般的であるが、EL3のラムダの場合は、そのような動きにはなっていない。


あくまでも、ラムダ式を実際に評価した時点の"変数aの現在の値"を参照しており、ラムダ式に変数のスコープは影響していない。


ラムダは変数のスコープに影響を与えないため、ラムダ中で変更した変数はラムダの外から普通にアクセスできる。

    Number ret = (Number) elProc.eval("a=1234;fn=()->(a=2345;b=3456);fn();a+b");
    assertEquals(2345 + 3456, ret.intValue());

この動きについては、仕様書からは読み取れなかったが、この場合の変数a, bはELResolverでローカル変数として解決されるELの原理からすると、おそらくEL3の仕様だと思う。

結論

EL3.0は基本的にEL2.2の仕組みを、そのまま拡張しているだけであり、互換性は高そうである。


また、ELProcessorによりアプリケーション側からのELの利用が、より簡単になっている。

おそらく独自のELResolverを作る必要性は、ほとんどないだろう。


ラムダなどの鋭利な機能は、たしかにJSTLのforEachタグでitemsの指定時にフィルタできそうなので、そうゆう利用方法はあるかもしれない。


ユーテリティクラスなどのStaticメソッドの利用が可能になっているなどの点は非常に便利だと思う。


とりあえず、仕組み的にも文法的にも上位互換性があるわけで、
改善されたEL2.2として、従来どおりの使い方から徐々に移行するような形でも良いと思う。