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 からロードするような場合)

エラー内容から CLASSPATHjavassist.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 した結果を使う事になると思います。 (他の方法もあるかもしれません)