Мне нужно было в рантайме вычислять истинность выражений типа

a>10 && b<c+5 && (a+b)<c*4

находящихся в строке Скалы.

Сам код у меня на Скале, но оценку разных библиотек для этого я делал на Котлине, просто чтобы поиграться с ним. Само выражение я получаю от клиента, но от внутреннего, поэтому мне не надо было заботиться о том, чтобы в выражении мне нe стерли файлы с диска.

Я оценивал разные библиотеки на то 1) могу ли они сделать то, что надо 2) скорость исполнения

Были проверены

  • интерполяция строк
  • Js Engine
  • javaluator
  • exp4j
  • evalEx
  • mxparser
  • MathEval
  • Groovy
  • Jexl

Результаты


Время пробега в мс для 1000 выражений (вернее одно и тоже выражение для 1000 разных набoров 3х переменных):

js 239 ms
mxParser 56713 ms
evalex 35 ms
groovy 118 ms
Jexl 62 ms

Остальные способы/библиотеки не сработали.

Под катом подробности:



1. Интерполяция строк.


Сначала я думал, нельзя ли приспособить интерполяцию строк Скалы для моих целей.
Я могу в скале написать
s"{a>10}"
но я не смог найти способ превратить обычную строку в строку для интерполяции.
В Скале есть StringContext, который чередует обычные строки с переменными:
s"You are ${age / 10} decades old, $name!"
это на самом деле
StringContext ("You are ", " decades old, ", "!").s (age / 10, name)
и превращается в
"You are " + (age / 10) + " decades old, " + (name) + "!"

но не хотелось возиться с парсингом строки и разделением ее на части

2. Использовать JavaScript Engine внутри Java.


работало без проблем

val e = ScriptEngineManager().getEngineByName("js")
fun jsEvaluate(a: Double, b: Double, c: Double): Boolean {
        e.context.setAttribute("a", a, ScriptContext.ENGINE_SCOPE)
        e.context.setAttribute("b", b, ScriptContext.ENGINE_SCOPE)
        e.context.setAttribute("c", c, ScriptContext.ENGINE_SCOPE)
        return e.eval(expr) as Boolean
}

3. Библиотека Javaluator


Без того, чтобы писать свои расширения, поддерживает только выражения с плавающей точкой.
С легкостью можно расширить для булевских выражений, и, наверное, можно расширить и для выражений типа "(a+b)>5"

4. Библиотека mxParser


Всё делает, но медленее чем JavaScript примерно в 1000 раз.
Результат всегда возвращает как Double.

val mxExpr = org.mariuszgromada.math.mxparser.Expression(expr)

fun mxParserEvaluate(a: Double, b: Double, c: Double): Boolean {
        val v1 = Argument("a = $a")
        val v2 = Argument("b = $b")
        val v3 = Argument("c = $c")
        mxExpr.addArguments(v1, v2, v3)
        return mxExpr.calculate() == 1.0
    }

5. Библиотека evalEx


Всё делает, самая быстрая, результат возвращает как BigDecimal

 val evalExpression = com.udojava.evalex.Expression(expr)
 
   fun evalexEvaluate(a: Double, b: Double, c: Double): Boolean {
        val eval = evalExpression.with("a", BigDecimal.valueOf(a)).and("b", BigDecimal.valueOf(b)).and("c", BigDecimal.valueOf(c)).eval()
        return eval == BigDecimal.ONE
    }

6. Библиотека exp4j


Нет поддержки для логических выражений, только выражения с плавающей точкой, но может быть расширена.

7. Библиотека MathEval


Нет поддержки для логических выражений, только выражения с плавающей точкой

8. Groovy




val template = groovy.text.GStringTemplateEngine().createTemplate(expr)

 fun groovyEvaluate(a: Double, b: Double, c: Double): Boolean {
        val binding = HashMap<String, Double>()
        binding.put("a", a)
        binding.put("b", b)
        binding.put("c", c)

        val template = template.make(binding)
        return template.toString().toBoolean()
    }


9. Jexl




   val jexl = JexlBuilder().create()
   val jexlExp = jexl.createExpression(expr)

 fun jexlEvaluate(a: Double, b: Double, c: Double): Boolean {

        val jc = MapContext()
        jc.set("a", a)
        jc.set("b", b)
        jc.set("c", c)

        return jexlExp.evaluate(jc) as Boolean
    }


Код, которым я проверял можно взять на гитхабе

Комментарии (17)


  1. sshikov
    30.04.2018 18:25
    +1

    >8. Groovy
    >Всё просто и всё работает, но медленно

    Насколько я помню, вы взяли не тот API. GroovyShell — это совсем не способ вычислить выражение. Выражение — это groovy.util.Eval, если мне опять же не изменяет память.

    Ну и… вы почему-то пропустили языки, которые поддерживают JSR 223, причем все кроме одного (а их было несколько десятков).


    1. javax Автор
      30.04.2018 21:29

      Внутри в Eval тот же самый GroovyShell

      Вы правы, можно было все имплементации JSR 223 пробывать, но мне показалось, что у evalEx такое преимущество, что уже и смысла нет


      1. sshikov
        01.05.2018 11:20

        >Внутри в Eval тот же самый GroovyShell

        Вообще-то, скорее наоборот. Шелл — это более высокоуровневая конструкция. Ну т.е. я верю, что разница может в итоге оказаться невелика, но все-таки я бы попробовал.


        1. javax Автор
          01.05.2018 11:21

          Вот код из класса Eval:

           public static Object xy(final Object x, final Object y, final String expression) throws CompilationFailedException {
                  Binding b = new Binding();
                  b.setVariable("x", x);
                  b.setVariable("y", y);
                  GroovyShell sh = new GroovyShell(b);
                  return sh.evaluate(expression);
              }


          1. sshikov
            01.05.2018 11:27

            Да, вы правы. Я видимо попутал с org.codehaus.groovy.tools.shell.Main, который тоже в некотором роде шелл, но другой.


  1. ggo
    30.04.2018 20:29
    +2

    GroovyShell для других целей.
    Вам к GStringTemplateEngine


    1. javax Автор
      30.04.2018 21:56
      +1

      Сделал, компиляцию выражения таким образом смог вынести и время сильно уменьшилось!
      Спасибо

      val template = groovy.text.GStringTemplateEngine().createTemplate(expr)
      
       fun groovyEvaluate(a: Double, b: Double, c: Double): Boolean {
              val binding = HashMap<String, Double>()
              binding.put("a", a)
              binding.put("b", b)
              binding.put("c", c)
      
              val template = template.make(binding)
              return template.toString().toBoolean()
          }
      


      1. SlavikF
        01.05.2018 06:09

        Вы забыли поменять суммарную таблицу в начале статьи


        1. javax Автор
          01.05.2018 11:22
          +1

          Сделал!


  1. ElegantBoomerang
    01.05.2018 11:18

    Хочу обратить внимание, что через ScriptEngineManager можно запускать и целый Котлин (смотреть про kotlin-scripts-utils). Работает шустро, но долго компилируется перед запуском — для случайных выражений долго, а для более-менее постоянных скриптов может быть и интересно.


  1. YuryB
    01.05.2018 11:18
    +1

    в 2018 результатам микробенчмарков без использования jmh никто не поверит :)


  1. dorincea
    01.05.2018 11:18

    А ещё есть jexl, который отлично справляется с задачей:

    JexlEngine jexl = new JexlBuilder().create();
    
    String jexlExp = "a>10 && b<c+5 && (a+b)<c*4";
    JexlExpression e = jexl.createExpression(jexlExp);
    
    JexlContext jc = new MapContext();
    jc.set("a", 1);
    jc.set("b", 2L);
    jc.set("c", 3D);
    
    Object o = e.evaluate(jc);
    System.out.println("result: " + o);
    


    1. sshikov
      01.05.2018 11:22

      Ну так и mvel тоже забыли. Который вообще говоря ровно для этого, и которому много лет, и он успешно применяется в camel, drools и много где еще. И ognl кстати.


      1. javax Автор
        01.05.2018 11:25

        Спасибо, попробую


    1. javax Автор
      01.05.2018 11:26

      Спасибо! Попробывал — очень хороший результат


  1. panchmp
    01.05.2018 21:08

    у меня на проекте используется старый добрый EL 2.0, но можно попробовать и EL 3.0
    пример для 3.0 illegalargumentexception.blogspot.ca/2008/04/java-using-el-outside-j2ee.html

    Тут есть список альтернатив stackoverflow.com/questions/17026863
    Все собираюсь написать для них benchmark


  1. igor_suhorukov
    02.05.2018 21:57

    Есть еще janino который можно как раз использовать как для выражений, так и для java компиляции с помощью ejc. В проекте реализовано много полезного для JSR 199 и т.п.