Commons OGNL でマッピング・フィルタリング・畳み込み
以前、Java でマッピング・フィルタリング・畳み込みを試しましたが、 今回は Commons OGNL を使って OGNL 式によるマッピング・フィルタリング・畳み込みを Groovy で試してみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20140118/
はじめに
Commons OGNL ではマッピングとフィルタリング用の式は用意されてますが、今のところ畳み込みは用意されていないようです。 (一応、畳み込みはラムダ式 :[ e ]
で実装できました)
処理 | 機能名 | OGNL式 |
---|---|---|
マッピング | Projection | e1.{ e2 } |
フィルタリング | Selection | e1.{? e2 } |
畳み込み | - | - |
Selection には、マッチした最初の要素を返す e1.{^ e2 }
や最後の要素を返す e1.{$ e2 }
のようなバリエーションも用意されています。
サンプルで使用するモデルクラス
今回のサンプルでは下記のようなモデルクラスを使いました。
Order.groovy
import groovy.transform.* @CompileStatic class Order { List<OrderLine> lines = [] } @CompileStatic @Immutable class OrderLine { String code BigDecimal price = 0 }
groovyc でコンパイルしておきます。
コンパイル
> groovyc Order.groovy
マッピング・フィルタリング
OGNL 式を使ったマッピング・フィルタリング処理を Groovy スクリプトで試してみます。
まず、@Grab
を使って普通に Ognl.getValue(<OGNL式>, <オブジェクト>)
を実行しようとすると下記のようなエラーが発生します。
エラー内容
java.lang.IllegalArgumentException: Javassist library is missing in classpath! Please add missed dependency! at org.apache.commons.ognl.OgnlRuntime.getCompiler(OgnlRuntime.java:210) ・・・ Caused by: java.lang.ClassNotFoundException: Unable to resolve class: javassist.ClassPool at org.apache.commons.ognl.OgnlRuntime.classForName(OgnlRuntime.java:665) ・・・
これは、デフォルトで使用される DefaultClassResolver
が SystemClassLoader から javassist.ClassPool
をロードしようとする事に起因するようで、Servlet 等の Java EE Web アプリケーションで実行する際にも同様のエラーが発生します。 (war ファイル内の JAR からロードするような場合)
エラー内容から CLASSPATH に javassist.jar が含まれていないように思ってしまいますが、そうではありません。
DefaultClassResolver に原因があるので、自前の ClassResolver を使って作成した Context を使うようにすればエラーを回避できます。
map_filter_sample.groovy
@GrabResolver('http://repository.apache.org/snapshots/') @Grab('org.apache.commons:commons-ognl:4.0-SNAPSHOT') import org.apache.commons.ognl.Ognl import org.apache.commons.ognl.ClassResolver def data = new Order() data.lines << new OrderLine('1', 100) data.lines << new OrderLine('2', 200) data.lines << new OrderLine('3', 300) try { // (1) DefaultClassResolver が原因でエラーが発生 println Ognl.getValue('lines.{? #this.code == "2" }', data) } catch (e) { println '(1) ' + e //e.printStackTrace() } println '-----------------------' // エラー回避のために自前の ClassResolver を使って Context 作成 def ctx = Ognl.createDefaultContext(null, { String className, Map<String, Object> context -> Class.forName(className) } as ClassResolver) // (2) マッピング println '(2) ' + Ognl.getValue('lines.{ #this.price > 100 }', ctx, data) // (3) フィルタリング println '(3) ' + Ognl.getValue('lines.{? #this.price > 100 }', ctx, data) // (4) マッチした最初の要素 println '(4) ' + Ognl.getValue('lines.{^ #this.price > 100 }', ctx, data) // (5) マッチした最後の要素 println '(5) ' + Ognl.getValue('lines.{$ #this.price > 100 }', ctx, data) // (6) OGNL式をコンパイルして使用 def exprNode = Ognl.compileExpression(ctx, null, 'lines.{? #this.code not in {"2", "4"} }') println '(6) ' + Ognl.getValue(exprNode, data)
実行結果は下記の通りです。
実行結果
> groovy map_filter_sample.groovy (1) java.lang.IllegalArgumentException: Javassist library is missing in classpath! Please add missed dependency! ----------------------- (2) [false, true, true] (3) [OrderLine(2, 200), OrderLine(3, 300)] (4) [OrderLine(2, 200)] (5) [OrderLine(3, 300)] (6) [OrderLine(1, 100), OrderLine(3, 300)]
畳み込み
OGNL のラムダ記法 :[ 処理 ]
を使って畳み込みを試しに実装してみました。
OGNL のラムダは引数を一つしか取れないようなので(ラムダ内の #this にバインドされます)、リストを使ってラムダ #fold
へ引数 {<処理>, <値>, <要素リスト>}
を渡すようにしています。
更に、OrderLine の price を合計するための処理もラムダで定義して (:[ #this[0] + #this[1].price ]
)、#fold
へ渡しています。
fold_sample.groovy
・・・ // 畳み込みの OGNL 式 def foldOgnl = ''' #fold = :[ #this[2].size() > 0 ? #fold({ #this[0], #this[0]({ #this[1], #this[2][0] }), #this[2].subList(1, #this[2].size()) }) : #this[1] ], #fold({ :[ #this[0] + #this[1].price ], 0, lines }) ''' // 0 + 100 + 200 + 300 println Ognl.getValue(foldOgnl, ctx, data)
実行結果
> groovy fold_sample.groovy 600
なお、合計を計算するだけなら以下のようにした方がシンプルです。
println Ognl.getValue('#v = 0, lines.{ #v = #v + #this.price }, #v', ctx, data)
並列実行時の注意点
最後に、並列処理で実行する際の注意点です。
問題確認のために、下記 3種類の処理を 50回並列でそれぞれ行ってみて OGNL の処理結果が正しかったかどうかを true・false で返すようにしてみます。
- (1) 文字列の OGNL 式で Context を再利用して getValue を実行
- (2) 文字列の OGNL 式で Context を都度作成して getValue を実行
- (3) compileExpression した結果で getValue を実行
parallel_sample.groovy
@GrabResolver('http://repository.apache.org/snapshots/') @Grab('org.apache.commons:commons-ognl:4.0-SNAPSHOT') import org.apache.commons.ognl.Ognl import org.apache.commons.ognl.ClassResolver import groovyx.gpars.* def createData = { i -> def data = new Order() data.lines << new OrderLine('1', i) data.lines << new OrderLine('2', i + 1) data.lines << new OrderLine('3', i + 2) data } def sum = { data -> data.lines.inject(0){ acc, val -> acc + val.price } } def printResult = { it.groupBy().each { k, v -> println "${k} ${v.size()}" } } def createContext = { Ognl.createDefaultContext(null, { String className, Map<String, Object> context -> Class.forName(className) } as ClassResolver) } def ctx = createContext() // OGNL式をコンパイル def exprNode = Ognl.compileExpression(ctx, null, '#v = 0, lines.{ #v = #v + #this.price }, #v') def count = 50 GParsPool.withPool(20) { // (1) 文字列の OGNL 式で並列処理(Context再利用) def res1 = (0..<count).collectParallel { def d = createData(it) try { // 稀に NoSuchPropertyException: Order.price が発生するため try-catch sum(d) == Ognl.getValue('#v = 0, lines.{ #v = #v + #this.price }, #v', ctx, d) } catch (e) { println e false } } // (2) 文字列の OGNL 式で並列処理(Context毎回作成) def res2 = (0..<count).collectParallel { def d = createData(it) sum(d) == Ognl.getValue('#v = 0, lines.{ #v = #v + #this.price }, #v', createContext(), d) } // (3) compileExpression 結果で並列処理 def res3 = (0..<count).collectParallel { def d = createData(it) sum(d) == Ognl.getValue(exprNode, d) } println '----- (1) 文字列の OGNL 式で並列処理(Context再利用)------' printResult res1 println '----- (2) 文字列の OGNL 式で並列処理(Context毎回作成) ---' printResult res2 println '----- (3) compileExpression 結果で並列処理 ----------------' printResult res3 }
処理結果は以下のようになり、(1) のケースで問題が発生しています。
実行結果
> groovy parallel_sample.groovy org.apache.commons.ognl.NoSuchPropertyException: Order.price ----- (1) 文字列の OGNL 式で並列処理(Context再利用)------ true 36 false 14 ----- (2) 文字列の OGNL 式で並列処理(Context毎回作成) --- true 50 ----- (3) compileExpression 結果で並列処理 ---------------- true 50
これは (1) のように Context を再利用すると #v
や #this
を並列処理間で共有してしまう事が原因と考えられます。
そのため、並列処理で実行する際は Context をその都度作成するか、compileExpression した結果を使う事になると思います。 (他の方法もあるかもしれません)