Execute dynamic expression statements in Java

In some rule set or workflow projects, it is common to encounter the ability to dynamically parse an expression and perform a result.

A rules engine is a component embedded in an application that separates business rules from business code and implements them using predefined semantic specifications. The rules engine takes input data, evaluates business rules, and makes business decisions.

Workflow is an abstract and general description of a Workflow and the service rules between each operation step. Workflow modeling, the logic and rules of how the work in a workflow is organized back and forth, is expressed and computed in an appropriate model in a computer. The main problem to be solved by workflow is the automatic transfer of documents, information, or tasks between multiple participants by a predetermined rule to achieve a business goal.


Table of Contents

  • Prefix, infix, postfix expressions (inverse Polish expressions)
    • Infix expression
    • Postfix expression
    • Prefix expression
  • OGNL
  • SpEL
  • Jexl/Jexl3
  • Execute a simple expression
  • Groovy
    • Execute expression
  • extension
  • reference

Prefix, infix, postfix expressions (inverse Polish expressions)

The earliest expression analysis I got to know was when I was teaching data structure. At that time, the assignment was “to parse a simple four-way mixed operation statement and calculate the result”, which was simply a calculator.

Infix expression

An expression that writes an operator between operands is called an infix expression.

Infix expressions are the most familiar and easiest to read

For example: 12 + 34 + 5 * 6-30/5

That is, the mathematical formulas that we often use are expressed by infix expressions

Postfix expression

Expressions that write an operator after two operands are called postfix expressions.

12, 34 plus 5, 6 times plus 30, 5 over minus

Prefix expressions are read from left to right. When encountering an operation, two operands are taken from the left

Read from left to right can be divided into

(12, 34 +) (5, 6 times) +) (30/5) minus

The parentheses are just auxiliary, not really

Prefix expression

A prefix expression is an expression that writes an operator before two operands.

Prefix expressions need to be read from right to left. When encountering an operation, two operands are taken from the right

12 plus 34 plus 5 times 6 minus 30/5

Minus plus plus 12, 34 times 5, 6/30, 5

  • Infix:12 plus 34 plus 5 times 6 minus 30/5
  • The suffix:12, 34 plus 5, 6 times plus 30, 5 over minus
  • The prefix:Minus plus plus 12, 34 times 5, 6/30, 5

OGNL

OGNL (Object-Graph Navigation Language for short) is an expression Language. In addition to setting and obtaining properties of Java objects, OGNL also provides projection and filtering of collections and lambda expressions.

Introduction of depend on

<! -- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
    <groupId>ognl</groupId>
    <artifactId>ognl</artifactId>
    <version>3.2.18</version>
</dependency>
Copy the code
MemberAccess memberAccess = new AbstractMemberAccess() {
    @Override
    public boolean isAccessible(Map context, Object target, Member member, String propertyName) {
        int modifiers = member.getModifiers();
        returnModifier.isPublic(modifiers); }}; OgnlContext context = (OgnlContext) Ognl.createDefaultContext(this,
    memberAccess,
    new DefaultClassResolver(),
    new DefaultTypeConverter());

context.put("verifyStatus".1);
Object expression = Ognl.parseExpression("#verifyStatus == 1");
boolean result =(boolean) Ognl.getValue(expression, context, context.getRoot());
Assert.assertTrue(result);
Copy the code

SpEL

Spring Expression Language (SpEL) is the Spring Expression Language. It is an EL expression language similar to JSP but more powerful and useful than the latter.

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("#verifyStatus == 1");

EvaluationContext context = new StandardEvaluationContext();
context.setVariable("verifyStatus".1);
boolean result = (boolean) expression.getValue(context);
Assert.assertTrue(result);
Copy the code

Jexl/Jexl3

Introduction of depend on

<! -- https://mvnrepository.com/artifact/org.apache.commons/commons-jexl3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-jexl3</artifactId>
    <version>3.1</version>
</dependency>
Copy the code

Execute a simple expression

JexlEngine jexl = new JexlBuilder().create();
JexlContext jc = new MapContext();
jc.set("verifyStatus".1);
JexlExpression expression = jexl.createExpression("verifyStatus == 1");
boolean result = (boolean) expression.evaluate(jc);
Assert.assertTrue(result);
Copy the code

Groovy

Groovy is a good choice, with full parsing execution capabilities for Groovy and Java syntax.

Import dependencies, which can import the latest version as needed

<! -- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy -->
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>2.5.6</version>
</dependency>
Copy the code

Execute expression

Binding binding = new Binding();
binding.setVariable("verifyStatus".1);
GroovyShell shell = new GroovyShell(binding);
boolean result = (boolean) shell.evaluate("verifyStatus == 1");
Assert.assertTrue(result);
Copy the code

extension

Those of you who use MyBatis frequently must have used dynamic statements

<select id="getList" 
    resultMap="UserBaseMap"
    parameterType="com.xx.Param">
    select
        id, invite_code, phone, name
    from user
    where status = 1
    <if test="_parameter ! = null">
        <if test="inviteCode ! =null and inviteCode ! = "">
            and invite_code = #{inviteCode}
        </if>
    </if>
</select>
Copy the code

So let’s simplify this a little bit

This example is illustrative and not necessarily useful, where @if is equivalent to

above

select id, invite_code, phone, name 
from user 
where status = 1 
@if(:inviteCode ! = null) { and invite_code = :inviteCode }
Copy the code

In this SQL, we can use the re to separate @if from the normal statement

List<String> results = StringUtil.matches(sql, "@if([\\s\\S]*?) }");
Copy the code

In this way matches @if(:inviteCode! = null) { and invite_code = :inviteCode }

Then separate the expression that needs to be evaluated from the SQL to be concatenated


String text = "@if(:inviteCode ! = null) { and invite_code = :inviteCode }";

List<String> sqlFragment = StringUtil.matches(text, "\\(([\\s\\S]*?) \\)|\\{([\\s\\S]*?) \ \}");
Copy the code

isolated

  • :inviteCode ! = null
  • and invite_code = :inviteCode

Among them: inviteCode! = null is a statement that needs to be processed dynamically, for :inviteCode! = null we need to identify those are the names of the variables that we need to copy

List<String> sqlFragmentParam = StringUtil.matches(":inviteCode ! = null"."\ \? \\d+(\\.[A-Za-z]+)? |:[A-Za-z0-9]+(\\.[A-Za-z]+)?");
Copy the code

Get inviteCode and somehow find the corresponding value,

JexlEngine jexl = new JexlBuilder().create();
JexlContext jc = new MapContext();
jc.set(":inviteCode"."ddddsdfa");
JexlExpression expression = jexl.createExpression(sqlExp);
boolean needAppendSQL = (boolean) expression.evaluate(jc);
Copy the code

With needAppendSQL deciding whether to concatenate or not, a simple dynamic SQL is implemented, written in Jexl above, you can change to either of the above schemes, just for demonstration

Specific code, for reference only

@Test
public void testSQL(a) {
  String sql = "select id, invite_code, phone, name \n"
  + "from user \n"
  + "where status = 1 \n"
  + "@if(:inviteCode ! = null) { and invite_code = :inviteCode }";

  Map<String, Object> params = new HashMap<String, Object>();
params.put("inviteCode"."dd");

  System.out.println(parseJexl(sql, params));
}

public String parseJexl(String jexlSql, Map<String, Object> params) {

  // Check whether @if is included
  List<String> results = StringUtil.matches(jexlSql, "@if([\\s\\S]*?) }");
  if (results.isEmpty()) {
      return jexlSql;
  }

  JexlEngine jexl = new JexlBuilder().create();
  JexlContext jc = new MapContext();

  for (String e : results) {
    List<String> sqlFragment = StringUtil.matches(e, "\\(([\\s\\S]*?) \\)|\\{([\\s\\S]*?) \ \}");
    String sqlExp = sqlFragment.get(0).trim().substring(1, sqlFragment.get(0).length() - 1);
    List<String> sqlFragmentParam = StringUtil.matches(sqlExp, "\ \? \\d+(\\.[A-Za-z]+)? |:[A-Za-z0-9]+(\\.[A-Za-z]+)?");
    for (String param : sqlFragmentParam) {
      String newSQLExp = "_" + param.substring(1);
      sqlExp = sqlExp.replace(param, newSQLExp);
      jc.set(newSQLExp, params.get(param.substring(1)));
    }
    JexlExpression expression = jexl.createExpression(sqlExp);
    Boolean needAppendSQL = (Boolean) expression.evaluate(jc);
    if (needAppendSQL) {
      jexlSql = jexlSql.replace(e, sqlFragment.get(1).trim().substring(1, sqlFragment.get(1).length() - 1));
    } else {
      jexlSql = jexlSql.replace(e, ""); }}return jexlSql;
}
Copy the code

reference

Refer to the documentation for specific uses of OGNL, SpEL, Jexl3, and Groovy

  • Commons.apache.org/proper/comm…
  • Docs. Spring. IO/spring – fram…
  • groovy-lang.org/syntax.html
  • Commons.apache.org/proper/comm…