kagamihogeの日記

kagamihogeの日記です。

Spring Framework Reference Documentation 4.1.xのIII. Core Technologies 8. Spring Expression Language (SpEL)をテキトーに訳した

http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/ をテキトーに訳した。Spring Framework Reference Documentation 4.1.xのIII. Core Technologies 7. Validation, Data Binding, and Type Conversionをテキトーに訳した - kagamihogeの日記の続き。

8. Spring Expression Language (SpEL)

8.1 Introduction

Spring Expression Language(SpEL)とは、強力な式言語で実行時にオブジェクトグラフの検索と操作を行います。シンタックスはUnified ELに似ていますが、追加機能があり、notably method invocationとbasic string templating functionalityがあります。

Javaで利用可能な式言語は他にも幾つかあり、例を挙げるとOGNL, MVEL, JBossELなどです。Spring Expression LanguageはSpringポートフォリオの全プロダクトで使用可能な式言語を一つSpringコミュニティに提供するべく作られました。言語機能に対する要求はSpringポートフォリオのプロダクトから上げられ、これにはeclipseベースのSpring Tool Suiteにおけるコード補完も含みます。しかし、SpELはAPIに依存しないため*1、他の式言語実装と統合する必要が生じた場合でも利用可能です。

SpELはSpringポートフォリオ内の式評価のための基盤を提供するものですが、Springに直接結合してはおらず、独立して使用可能です。自己完結にするため、このチャプターの多くの例は、SpELを独立した式言語であるかのように使います。これにはパーサーなど基盤となるクラスを起動する手間が必要です。Springユーザーの多くはこの基盤クラスを扱う必要は無く、評価用の式を書くだけです。一般的な使用例としてはSpELインテグレーションをXMLもしくはアノテーションベースのビーン定義生成に組み込みことで、Expression support for defining bean definitionsで説明します。

このチャプターは、式言語の機能・APIシンタックス、を解説します。式評価用のターゲットオブジェクトの例としてInventorおよびSocietyクラスを使います。これらのクラス宣言と処理に使うデータはチャプターの末尾に載せてあります。

8.2 Feature Overview

式言語は以下の機能をサポートします。

  • Literal expressions
  • Boolean and relational operators
  • Regular expressions
  • Class expressions
  • Accessing properties, arrays, lists, maps
  • Method invocation
  • Relational operators
  • Assignment
  • Calling constructors
  • Bean references
  • Array construction
  • Inline lists
  • Inline maps
  • Ternary operator
  • Variables
  • User defined functions
  • Collection projection
  • Collection selection
  • Templated expressions

8.3 Expression Evaluation using Spring’s Expression Interface

このセクションではSpELインタフェースと式言語の簡単な使用例を解説します。完全な言語リファレンスはLanguage Referenceにあります。

以下のコードはリテラル文字列の式Hello Worldを評価するSpEL APIの例です。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();

message変数の値は単にHello Worldとなります。

最も使用する可能性の高いSpELクラスとインタフェースはorg.springframework.expressionパッケージとそのサブパッケージとspel.supportにあります。

インタフェースExpressionParserは式の文字列をパースする役割を持ちます。この例では、式はシングルクオートで囲われた文字列リテラルです。インタフェースExpressionは事前に定義しておいた式を評価する役割を持ちます。parser.parseExpressionexp.getValueをそれぞれ呼び出す場合、ParseExceptionEvaluationException例外をスローする可能性があります。

SpELは、メソッド呼び出し・プロパティアクセス・コンストラクタ呼び出しなど、様々な機能を持ちます。

メソッド呼び出しの例としては、文字列リテラルconcatメソッドがあります。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();

この場合message変数の値はHello World!となります。

JavaBeanプロパティ呼び出しの例としては、文字列プロパティのBytesを以下のように呼び出せます。

ExpressionParser parser = new SpelExpressionParser();

// invokes getBytes()
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();

また、SpELはdot記法を使用してネストされたプロパティをサポートし、例えば、prop1.prop2.prop3でプロパティ値を設定します。

また、publicなフィールドアクセスも可能です。

ExpressionParser parser = new SpelExpressionParser();

// invokes getBytes().length
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
int length = (Integer) exp.getValue();

Stringのコンストラクタを文字列リテラルの代わりに使うことも可能です。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()");
String message = exp.getValue(String.class);

ジェネリックメソッドpublic <T> T getValue(Class<T> desiredResultType)を使っていることに注目してください。これを使用すると、式の値を型にキャストする必要が無くなります。もし値が型Tや型コンバーターでキャスト出来ない場合EvaluationExceptionをスローします。

SpELの良くある使い方は、(ルートオブジェクトと呼ばれる)オブジェクトインスタンスに対して評価を行う式を扱うものです。これには二種類のオプションがあり、式に対するオブジェクトが評価ごとに変化するかどうかで選択します。以下の例ではInventorインスタンスからnameプロパティを取得します。

// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");

EvaluationContext context = new StandardEvaluationContext(tesla);
String name = (String) exp.getValue(context);

最後の行では、文字列変数nameは"Nikola Tesla"になります。StandardEvaluationContextには"name"プロパティが評価されるオブジェクトを指定します。このメカニズムはルートオブジェクトが滅多に変更されない場合に使うもので、コンテキストにはただ一度だけ設定が可能です。ルートオブジェクトが何度も頻繁に変更される場合、次のサンプルで示すように、getValueを呼び出すごとにオブジェクトを渡します。

// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
String name = (String) exp.getValue(tesla);

この場合にはinventor teslaは直接getValueに渡しており、式の評価の裏側でデフォルトの評価コンテキストを内部的に作成して管理を行うので、コンテキストを渡す必要はありません。

StandardEvaluationContextは相対的に生成が高コストなので、繰り返し使うスコープ期間内はキャッシュすることで後続の評価処理が高速になります。こうした理由により、StandardEvaluationContextは評価処理ごとに新しく生成するよりも、キャッシュや再利用が可能な場所ではなるべくそうして下さい。

設定済みの評価コンテキストを使いつつgetValue呼び出し時に別のルートオブジェクトを渡すことも可能です。getValueはどちらのやり方でも呼び出し可能です。この場合、呼び出し時に渡すルートオブジェクトは評価コンテキストで指定した(もしくはnull)オブジェクトをオーバーライドすると見なせます。

上記の説明におけるSpELの標準的な使い方では、パーサーやパーサー式や評価コンテキストにルートコンテキストオブジェクトを生成する必要があります。ただし、より一般的な使い方では、例えばSpringビーンやSpring Web Flow定義では、設定の一部としてSpELの式のみ指定します。この場合では、パーサー・評価コンテキスト・ルートオブジェクト・定義済み変数などは暗黙的にすべてセットアップされるので、ユーザは式以外を指定する必要はありません。

前置きにおける最後の例として、前述の例のInventorオブジェクトでboolean演算子を使う例を示します。

Expression exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(context, Boolean.class); // evaluates to true

8.3.1 The EvaluationContext interface

EvaluationContextインタフェースは、プロパティ・メソッド・フィールド解決と型変換で式を評価する際に使われます。Spring標準実装のStandardEvaluationContextはオブジェクト操作にリフレクションを使用し、パフォーマンス向上のためにjava.lang.reflectMethodFieldConstructorをキャッシュします。

StandardEvaluationContextには、コンストラクタにルートオブジェクトを渡すか、setRootObject()メソッドで評価するルートオブジェクトを指定します。また、式で使う変数と関数をsetVariable()registerFunction()メソッドで指定します。変数と関数の使い方はlanguage reference sectionsのVariablesFunctionsに解説があります。StandardEvaluationContextには、SpEL式評価を拡張するConstructorResolvers, MethodResolvers, PropertyAccessorも登録可能です。これらのクラスの詳細についてはjavadocを参照してください。

Type Conversion

デフォルトではSpELは変換サービスにはSpringコア(org.springframework.core.convert.ConversionService)のものを使います。この変換サービスは、一般的な変換を行う多数のコンバーターで構成され、型間でカスタム変換を追加可能な拡張性があります。加えて、ジェネリクスも使用可能です。つまり、式でジェネリクス型を使う場合、SpELはオブジェクトの型の正しさを維持して変換を行います。

setValue()Listプロパティに設定する代入を例に取ります。プロパティの型は実際にはList<Boolean>です。SpELは、代入を行う前に、リストの要素をBooleanに変換する必要があると解釈します。以下が簡単な例です。

class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}

Simple simple = new Simple();

simple.booleanList.add(true);

StandardEvaluationContext simpleContext = new StandardEvaluationContext(simple);

// ここでは文字列としてfalseを渡している。SpELと変換サービスは文字列のfalseを
// 変換してBooleanにする必要があると正しく解釈する。
rser.parseExpression("booleanList[0]").setValue(simpleContext, "false");

// bはfalseとなる。
Boolean b = simple.booleanList.get(0);

8.3.2 Parser configuration

SpEL式パーサーはパーサー設定オブジェクト(org.springframework.expression.spel.SpelParserConfiguration)を使用して設定が可能です。設定オブジェクトは式コンポーネントの振る舞いのいくつかを制御します。たとえば、指定インデックスの要素や配列またはコレクションのインデックス参照先がnullの場合、自動的に要素を生成できます。これはプロパティ参照チェーンで構成される式を使う場合に有効です。また、配列やリストサイズを超えたインデックス指定をした場合、自動的にそのインデックスまで配列やリストのサイズを拡張することも可能です。

class Demo {
    public List<String> list;
}

// 引数指定:
// - null参照で自動初期化
// - コレクションの自動拡張
SpelParserConfiguration config = new SpelParserConfiguration(true,true);

ExpressionParser parser = new SpelExpressionParser(config);

Expression expression = parser.parseExpression("list[3]");

Demo demo = new Demo();

Object o = expression.getValue(demo);

// demo.listは4個のコレクションになる。
// 各エントリは空文字列。

また、SpEL式コンパイラの振る舞いを設定することも可能です。

8.3.3 SpEL compilation

Spring Framework 4.1にはベーシックな式コンパイラが含まれます。式は通常は動的な柔軟性を持つインタープリターで処理されますが、パフォーマンス最適化は行いません。たまにしか使わない式ならそれで良いですが、Springインテグレーションなど他コンポーネントで使われる場合、パフォーマンスは非常に重要であり、動的さはさほど必要にはなりません。

新しいSpELコンパイラーはそうした要求を視野に入れています。コンパイラは式の振る舞いを実体化する処理の裏側でJavaクラスを生成し、式評価を高速化するのに使います。式には型情報が欠落しているので、コンパイラコンパイル実行時に式のインタープリター評価中に集めた型情報を使います。たとえば、式からは全くプロパティ参照の型が分からなくとも、初回のインタープリタ評価中に型は分かります。当然ながら、この方法に基づくコンパイルでは式の型が後々に変更された場合にトラブルの原因になりえます。そのため、コンパイル方式は繰り返し評価する際に型情報が変化しない式に適しています。

一般的な式は以下のようになります。

someArray[0].someProperty.someOtherProperty < 0.1

上記は、配列にアクセスし、プロパティ参照と数値演算を行い、パフォーマンスは大変良好になると思われます。50000回のマイクロベンチマークでは、インタープリタを用いた評価では75msかかり、式のコンパイルバージョンを使用した場合は3msとなりました。

Compiler configuration

コンパイラはデフォルトでは作動せず、有効化するには二種類の方法があります。前述のパーサー設定プロセスを使用するか、別のコンポーネント内に組み込むSpELではシステムプロパティで設定します。このセクションでは両者について解説します。

重要なのはコンパイラモードが幾つかある点で、enumに定義されています(org.springframework.expression.spel.SpelCompilerMode)。モードは以下の通りです。

MIXEDモードが副作用を持つ式で問題を発生させる場合のためにIMMEDIATEモードは存在します。コンパイル済みの式が部分的に成功したあと失敗する場合、既にシステムに何らかの状態変更を行った後である可能性があります。これが発生した場合、呼び出し元はインタープリターモードで暗黙的な再実行を望まない筈で、なぜなら式の一部が二回実行されてしまうためです。

使うモードを選んだ後、パーサーを設定時にSpelParserConfigurationを使います。

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
    this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);

Expression expr = parser.parseExpression("payload");

MyMessage message = new MyMessage();

Object payload = expr.getValue(message);

コンパイラーモード指定時にクラスローダーも指定可能です(nullも指定可能)。コンパイル済の式は、引数で指定したクラスローダー下に作成される子クラスローダーに定義されます*2。クラスローダーを指定する場合、式評価処理が使うすべての型を参照可能にしてください。クラスローダー指定が無い場合はデフォルトのクラスローダー(通常は式評価を実行するスレッドのコンテキストクラスローダー)が使われます。

コンパイラを設定するもう一つの方法は、別のコンポーネント内に組み込むSpELで使うもので、この場合は設定オブジェクトでは設定出来ません。この場合にはシステムプロパティを使います。spring.expression.compiler.modeプロパティにSpelCompilerModeenumoff, immediate, mixed)のうち一つを設定します。

Compiler limitations

Spring Framework 4.1では基本的なコンパイルフレームワークが最初から組み込まれています。しかし、フレームワークは式のあらゆる種類のコンパイルをサポートしているわけではありません。パフォーマンスがクリティカルな文脈で使われうる一般的な式に当初はフォーカスしていました。式のうちいくつかは現時点ではコンパイル出来ません。

  • expressions involving assignment
  • expressions relying on the conversion service
  • expressions using custom resolvers or accessors
  • expressions using selection or projection

将来的にはそうした式もコンパイル可能になると思われます。

8.4 Expression support for defining bean definitions

SpEL式はXMLアノテーション設定メタデータBeanDefinitionを定義するのに使用可能です。両方式共にシンタックス#{ <expression string> }形式で定義します。

8.4.1 XML based configuration

プロパティやコンストラクタ引数の値は以下のような式で設定します。

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

組み込み変数systemPropertiesを以下のように式で使用可能です。注意点として、以下のようなコンテキストでは組み込み変数のプレフィクスに#は必要ありません。

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- other properties -->
</bean>

by nameで他ビーンのプロパティを参照することも可能です。

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="{ numberGuess.randomNumber }"/>

    <!-- other properties -->
</bean>

8.4.2 Annotation-based configuration

@Valueアノテーションは、フィールド・メソッドメソッド/コンストラクタ引数のデフォルト値指定、に設定可能です。

以下はフィールド変数にデフォルト値を設定する例です。

public static class FieldValueTestBean

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }

}

以下は上記と同等なことをプロパティのセッターメソッドで行う例です。

public static class PropertyValueTestBean

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }

}

オートワイヤメソッドコンストラクタにも@Valueアノテーションを使用可能です。

public class SimpleMovieLister {

    private MovieFinder movieFinder;
    private String defaultLocale;

    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }

    // ...
}
public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}

8.5 Language Reference

8.5.1 Literal expressions

リテラル式の型は、文字列・日付・数値(整数・実数・16進数)・真偽値・null、をサポートしています。文字列はシングルクオートで区切ります。文字列内にシングルクオートを含めるにはシングルクオートを二つ重ねます。以下にリテラルの簡単な使用例を示します。実際には以下のように独立して使わず、より複雑な式の一部として、例えば論理比較演算子のどちらかの辺のリテラル値などに使います。

ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

数値は、負号・指数表記・小数点、をサポートします。デフォルトでは実数はDouble.parseDouble()でパースします。

8.5.2 Properties, Arrays, Lists, Maps, Indexers

プロパティ参照は簡単で、ネストされたプロパティ値の指定にはピリオドを使います。InventorクラスのインスタンスのpupinとteslaはClasses used in the examples.セクションに示すデータで処理を行います。Teslaの誕生年とPupinの誕生地を得るには以下の式を使います。

// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

プロパティ名の最初の文字は大文字小文字を無視します。配列とリストの中身は角括弧で取得します。

ExpressionParser parser = new SpelExpressionParser();

// Inventions Array
StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla);

// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
        teslaContext, String.class);

// Members List
StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee);

// evaluates to "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
        societyContext, String.class);

// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
        societyContext, String.class);

mapの中身はブラケット内にキー値リテラルを指定することで取得します。この例の場合、Officersのmapのキーは文字列なので、文字列リテラルを指定可能です。

// Officer's Dictionary

Inventor pupin = parser.parseExpression("Officers['president']").getValue(
        societyContext, Inventor.class);

// evaluates to "Idvor"
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
        societyContext, String.class);

// setting values
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
        societyContext, "Croatia");

8.5.3 Inline lists

リストは{}を用いた式で表現できます。

// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

{}単独は空リストを意味します。パフォーマンス上の理由により、固定リテラルのみのリストの場合は、各評価ごとに新規リストを作らず、式を表現するのに定数リストを作ります。

8.5.4 Inline Maps

mapは{key:value}を使う式で表現できます。

// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

{:}単独は空mapを意味します。パフォーマンス上の理由により、固定リテラルもしくは他のネストされた定数構造(リストかmap)のmapの場合は、各評価ごとに新規mapを作らず、式を表現するのに定数mapを作ります。mapのキーはクオートで囲んでも囲まなくても良く、上記の例はクオートでキーを囲んでいません。

8.5.5 Array construction

配列はおなじみのJavaシンタックスで作成可能で、必要であればイニシャライザを指定できます。

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

今のところ多次元配列ではイニシャライズを指定できません。

8.5.6 Methods

メソッドは一般的なJavaプログラミングのシンタックスで呼び出します。メソッド呼び出しの引数にリテラルが使えます。また、Varargsもサポートしています。

8.5.7 Operators

Relational operators

比較演算子は標準演算子の記法、==, !=, <, <=, > >=、をサポートします。

// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

標準比較演算子に加えて、SpELはinstanceofmatchesベースの正規表現をサポートします。

// evaluates to false
boolean falseValue = parser.parseExpression(
        "'xyz' instanceof T(int)").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression(
        "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

//evaluates to false
boolean falseValue = parser.parseExpression(
        "'5.0067' matches '\^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

なお、演算子はアルファベットで同等な指定が可能です。ドキュメントタイプ(XMLなど)によっては式中の演算子が特別な意味を持つ場合を回避するためにこの指定を使います。lt (<), gt (>), le (<=), ge (>=), eq (==), ne (!=), div (/), mod (%), not (!) これらは大文字小文字を区別しません。

Logical operators

論理演算子はand, or, notをサポートします。使用例は以下の通りです。

// -- AND --

// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- OR --

// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- NOT --

// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
Mathematical operators

加算演算子は数値と文字列で使用可能です。減算・乗算・除算は数値でのみ使用可能です。他の算術演算子には剰余(%)と指数(^)があります。標準演算子の順序が適用されます。これら演算子の使用例は以下の通りです。

// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2

String testString = parser.parseExpression(
        "'test' + ' ' + 'string'").getValue(String.class); // test string

// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4

double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000

// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6

double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0

// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2

double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0

// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3

int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1

// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21

8.5.8 Assignment

プロパティの設定は代入演算子を使います。通常setValueで使いますが、getValueでも使用可能です。

Inventor inventor = new Inventor();
StandardEvaluationContext inventorContext = new StandardEvaluationContext(inventor);

parser.parseExpression("Name").setValue(inventorContext, "Alexander Seovic2");

// alternatively

String aleks = parser.parseExpression(
        "Name = 'Alexandar Seovic'").getValue(inventorContext, String.class);

8.5.9 Types

特殊なT演算子java.lang.Classインスタンス(つまり"型")を指定するのに使います。staticメソッドはこの演算子を使用して呼び出します。StandardEvaluationContextは型の参照にTypeLocatorを使用し、StandardTypeLocatorjava.langパッケージの指定は不要です(((which can be replaced) is built with an understanding of the java.lang package.が原文。すぐ次の文にあるようにjava.langの指定は不要という意味なので意訳した))。つまりT()でjava.lang下の型を参照する際は完全修飾名を必要とせず、他の型については完全修飾名が必要、という意味です。

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

boolean trueValue = parser.parseExpression(
        "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
        .getValue(Boolean.class);

8.5.10 Constructors

コンストラクタはnew演算子で呼び出します。常に完全修飾クラス名を使うべきですがプリミティブ型と文字列はその限りではありません。

Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);

//create new inventor instance within add method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);

8.5.11 Variables

式中で変数は#variableNameで参照します。変数はStandardEvaluationContextのsetVariableメソッドで設定します。

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context);

System.out.println(tesla.getName()) // "Mike Tesla"
The #this and #root variables

変数#thisは常に利用可能で現在の評価オブジェクトを指します(against which unqualified references are resolved)*3。#rootは常にルートコンテキストオブジェクトを指します。#thisは評価式のコンポーネントとして変化しますが、#rootは常にrootを指します。

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// create parser and set variable primes as the array of integers
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("primes",primes);

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

8.5.12 Functions

式内で呼び出せるユーザ定義関数を登録することでSpELを拡張できます。関数は以下のメソッドによりStandardEvaluationContextに登録されます。

public void registerFunction(String name, Method m)

上記のMethodが関数の実装です。たとえば、文字列を逆にするユーティリティーメソッドは以下の通りです。

public abstract class StringUtils {

    public static String reverseString(String input) {
        StringBuilder backwards = new StringBuilder();
        for (int i = 0; i < input.length(); i++)
            backwards.append(input.charAt(input.length() - 1 - i));
        }
        return backwards.toString();
    }
}

このメソッドを評価コンテキストに登録することで式から利用可能になります。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

context.registerFunction("reverseString",
    StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class }));

String helloWorldReversed = parser.parseExpression(
    "#reverseString('hello')").getValue(context, String.class);

8.5.13 Bean references

評価コンテキストがビーンリゾルバ―に登録されている場合、式から(@)でビーンをルックアップできます。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@foo").getValue(context);

8.5.14 Ternary Operator (If-Then-Else)

式内でif-then-else条件分岐を行うのに三項演算子が使えます。

String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

上記の例の場合、booleanのfalseは文字列falseExpを返します。より現実的な例は以下の通りです。

parser.parseExpression("Name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");

expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
        "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

また、三項演算子の短縮版としてElvis operatorがあります。

8.5.15 The Elvis Operator

Elvis operatorは三項演算子の短縮版でGroovy)で使われています。三項演算子では変数を二度繰り返す必要があります。例えば、

String name = "Elvis Presley";
String displayName = name != null ? name : "Unknown";

代わりにElvis operatorを使用可能で、この名前の由来はエルビス・プレスリーの髪型*4から来ています。

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("null?:'Unknown'").getValue(String.class);

System.out.println(name); // Unknown

以下はより複雑な例です。

ExpressionParser parser = new SpelExpressionParser();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
StandardEvaluationContext context = new StandardEvaluationContext(tesla);

String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);

System.out.println(name); // Nikola Tesla

tesla.setName(null);

name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);

System.out.println(name); // Elvis Presley

8.5.16 Safe Navigation operator

Safe Navigation演算子NullPointerExceptionを回避するためのものでGroovy)から来ています。通常、何らかのオブジェクト参照を使うときそのオブジェクトのメソッドやプロパティにアクセスする前にnullチェックをする必要があります。これを避けるため、Safe Navigation演算子は例外をスローする代わりにnullを返します。

ExpressionParser parser = new SpelExpressionParser();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

StandardEvaluationContext context = new StandardEvaluationContext(tesla);

String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);
System.out.println(city); // Smiljan

tesla.setPlaceOfBirth(null);

city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);

System.out.println(city); // null - does not throw NullPointerException!!!

Elvis operatorは@Valueなどの式におけるデフォルト値として使用可能です。

@Value("#{systemProperties['pop3.port'] ?: 25}")

システムプロパティpop3.portが定義されていればそれを使い、そうでなければ25をインジェクトします。

8.5.17 Collection Selection

Selectionは強力な式言語の機能で、あるコレクションを選択したエントリで別のコレクションに変換します。

Selectionのシンタックス?[selectionExpression]です。コレクションをフィルターして元のコレクションのサブセットで新規コレクションを返します。例えば、SelectionによりSerbian inventorsのリストを簡単に作れます。

List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "Members.?[Nationality == 'Serbian']").getValue(societyContext);

Selectionリストとmapの両方で使えます。リストでのSelectionは要素それぞれに対して評価が行われ、mapではmapのentry(Map.Entryオブジェクト)に対して評価が行われます。mapエントリはプロパティとしてアクセス可能なキーと値を持ち、Selectionではそれらを使います。

以下の式は元のmapからentryの値が27未満の要素から成る新しいmapを返します。

Map newMap = parser.parseExpression("map.?[value<27]").getValue();

全要素選択に加えて、最初や最後の値を取得することも可能です。selectionしたもののうち最初にマッチするエントリを取得するには^[...]を使い、最後にマッチする要素を取得するには$[...]です。

8.5.18 Collection Projection

Projectionはコレクションでサブ式を評価し、その結果を新しいコレクションとして返します。projectionのシンタックス![projectionExpression]です。簡単な例として、inventorsのリストから生まれた都市のリストが欲しい、とします。これをするにはinventorリストの各要素にplaceOfBirth.cityを評価する必要があります。この場合にはprojectionを使います。

// returns [Smiljan, Idvor ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");

mapでもprojectionは使用可能で、mapの各要素(Map.Entryオブジェクト)に対して評価が行われます。mapに対するprojectionの結果は、mapエントリの各要素に対してprojection式を評価した要素から成るリストが返されます。

8.5.19 Expression templating

Expression templatesにより、7一つ上以上の評価ブロックにリテラルテキストを入れられます。各評価ブロックは自前で定義するプレフィクスとサフィックスのデリミタで囲み、デリミタには慣例的に#{ }が使われます。例えば、

String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

リテラルテキスト'random number is 'に#{ }デリミタ内の式の評価結果を連結したものが評価されます。#{ }デリミタ内の式はrandom()を呼び出します。parseExpression()の第二引数はParserContext型です。ParserContextインタフェースはexpression templating機能をサポートする式のパース手順です。TemplateParserContextの定義は以下の通りです。

public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}

8.6 Classes used in the examples

Inventor.java

package org.spring.samples.spel.inventor;

import java.util.Date;
import java.util.GregorianCalendar;

public class Inventor {

    private String name;
    private String nationality;
    private String[] inventions;
    private Date birthdate;
    private PlaceOfBirth placeOfBirth;

    public Inventor(String name, String nationality) {
        GregorianCalendar c= new GregorianCalendar();
        this.name = name;
        this.nationality = nationality;
        this.birthdate = c.getTime();
    }

    public Inventor(String name, Date birthdate, String nationality) {
        this.name = name;
        this.nationality = nationality;
        this.birthdate = birthdate;
    }

    public Inventor() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNationality() {
        return nationality;
    }

    public void setNationality(String nationality) {
        this.nationality = nationality;
    }

    public Date getBirthdate() {
        return birthdate;
    }

    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
    }

    public PlaceOfBirth getPlaceOfBirth() {
        return placeOfBirth;
    }

    public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
        this.placeOfBirth = placeOfBirth;
    }

    public void setInventions(String[] inventions) {
        this.inventions = inventions;
    }

    public String[] getInventions() {
        return inventions;
    }
}

PlaceOfBirth.java

package org.spring.samples.spel.inventor;

public class PlaceOfBirth {

    private String city;
    private String country;

    public PlaceOfBirth(String city) {
        this.city=city;
    }

    public PlaceOfBirth(String city, String country) {
        this(city);
        this.country = country;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String s) {
        this.city = s;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

}

Society.java

package org.spring.samples.spel.inventor;

import java.util.*;

public class Society {

    private String name;

    public static String Advisors = "advisors";
    public static String President = "president";

    private List<Inventor> members = new ArrayList<Inventor>();
    private Map officers = new HashMap();

    public List getMembers() {
        return members;
    }

    public Map getOfficers() {
        return officers;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isMember(String name) {
        for (Inventor inventor : members) {
            if (inventor.getName().equals(name)) {
                return true;
            }
        }
        return false;
    }

}

*1:SpEL is based on a technology agnostic APIが原文

*2:Compiled expressions will be defined in a child classloader created under any that is supplied.が原文。誤訳してるかも…

*3:よくわからんので訳せず

*4:要イメージ検索 https://www.google.co.jp/search?q=Elvis+operator&tbm=isch