preface
I suggest reading this article first [design mode] 7 principles of software design and 23 design modes (I)
I Interpreter mode
Interpreter pattern is used to construct a simple language interpreter to interpret and execute strings in a custom way. It is an uncommon design pattern
- Unless you are engaged in low-level development and need to define more complex expressions, it is basically different from this design pattern
- In the project, the js engine of Jruby, Groovy and java can be used to replace the role of interpreter to make up for the shortcomings of java language. You can also use open source parsing toolkits such as Expression4J, MESP (Math Expression String Parser), Jep, etc. they are powerful, easy to use, and efficient. There is no need to write them from scratch (using tools written by others is also a good choice).
. Consider this when preparing to use interpreter mode
II Interpreter mode applicable scenarios
-
Processing of EL expressions
-
Regular expression interpreter
-
Interpreter for SQL syntax
-
Mathematical expression parser
-
Off the shelf open source tool for expression parsing
II Interpreter mode role
In the interpreter mode, there are the concepts of terminator expression and non terminator expression.
-
Terminal expression: implements the interpretation operations related to the terminal in the syntax. Each terminator in syntax has a specific terminator expression corresponding to it.
For example, in our R=M+N operation, M and N are terminators, and the corresponding interpreter for parsing M and N is "terminator expression".
-
Non terminal expression: it implements the interpretation operations related to non terminals in the syntax. Each rule in the syntax corresponds to a non terminator expression. Nonterminal expressions are generally operators or keywords in grammar
As announced above, the "+" sign in R=M+N is a non terminator, and the interpreter that parses the "+" sign is a "non terminator expression".
The interpreter mode has the following four roles
-
Abstract expression: generally, an interpretation method will be defined, and the specific resolution will be implemented by subclasses (such as ieexpression in the example)
-
Terminal expression: implements the interpretation operations related to the terminal in the syntax (such as numberexpression in the following example).
For example, a and b in a + b, these operation elements do not need any processing except assignment, and their functions are basically the same. They are the smallest unit in syntax, equivalent to leaves in combination mode.
-
Non terminal expression: it implements the interpretation operations related to non terminals in the syntax (such as addressexpression and subexpression in the following example)
For example, for the + sign in a + b, these operation symbols will correspond to a specific business logic. For example, addition, subtraction, multiplication and division are four different non terminator expressions, which are equivalent to branches in the combination mode.
-
Context: contains global information outside the interpreter. Store the data or common functions that need to be used by each interpreter. (as the expressioncontext in the following example)
III Implementation of interpreter mode
Abstract expression
First, define a top-level expression interface
/** * Top level expression interface */ public interface IExpression { int interpret(); }
Non terminal expression
Define an abstract nonterminal expression (for example, the plus sign and minus sign are nonterminal expressions)
/** * Abstract nonterminal expression */ public abstract class AbstractNonTerminalExpression implements IExpression{ protected IExpression leftExpression; protected IExpression rightExpression; public AbstractNonTerminalExpression(IExpression leftExpression, IExpression rightExpression) { this.leftExpression = leftExpression; this.rightExpression = rightExpression; } }
In this example, only addition and subtraction are listed, so we also need to define an addition class and a subtraction class, that is, the concrete implementation of abstract non terminal expression:
/** * Concrete non terminal expression - addition expression */ public class AddExpression extends AbstractNonTerminalExpression { public AddExpression(IExpression leftExpression, IExpression rightExpression) { super(leftExpression, rightExpression); } @Override public int interpret() { return this.leftExpression.interpret() + this.rightExpression.interpret(); } }
/** * Concrete nonterminal expression - subtraction expression */ public class SubExpression extends AbstractNonTerminalExpression { public SubExpression(IExpression leftExpression, IExpression rightExpression) { super(leftExpression, rightExpression); } @Override public int interpret() { return this.leftExpression.interpret() - this.rightExpression.interpret(); } }
Terminal expression
Define an end expression (such as the value in addition and subtraction):
/** * Terminal expression - numeric expression */ public class NumberExpression implements IExpression{ private int value; public NumberExpression(String value) { this.value = Integer.valueOf(value); } @Override public int interpret() { return this.value; } }
Context
/** * Context information to store our operation results */ public class ExpressionContext { /** * Record the current operation result. If it is blank, it means no operation has been performed yet */ private Integer currValue; private Stack<IExpression> stack = new Stack<>(); public ExpressionContext(String expression) { this.parse(expression); } private void parse(String expression) { //Split according to spaces (it is not determined whether the extracted characters are pure numbers) String[] elementArr = expression.split(" "); for (int i = 0; i < elementArr.length; i++) { String element = elementArr[i]; if (element.equals("+")) { //Out of stack elements IExpression leftExpression = stack.pop(); //Take out the next element after the + sign IExpression rightExpression = new NumberExpression(elementArr[++i]); //Calculate and stack the results IExpression addExpression = new AddExpression(leftExpression, rightExpression); stack.push(new NumberExpression(addExpression.interpret() + "")); } else if (element.equals("-")) { //Out of stack elements IExpression leftExpression = stack.pop(); //Take out the next element after the - sign IExpression rightExpression = new NumberExpression(elementArr[++i]); //Calculate and stack the results IExpression subExpression = new SubExpression(leftExpression, rightExpression); stack.push(new NumberExpression(subExpression.interpret() + "")); } //If it is a number, it is directly put on the stack else { stack.push(new NumberExpression(element)); } } } public int calculate() { //After the previous analysis, there is only one number left in the stack, that is, the operation result return stack.pop().interpret(); } }
Customer class
public class Client { public static void main(String[] args) { //First add then subtract ExpressionContext context1 = new ExpressionContext("666 + 888 - 777"); System.out.println(context1.calculate()); //Decrease before increase ExpressionContext context2 = new ExpressionContext("123 - 456 + 11"); System.out.println(context2.calculate()); } }
results of enforcement
IV summary
1. Advantages and disadvantages of interpreter mode
advantage:
- Strong scalability. As can be seen from the above example, each expression is a class, so if you need to modify a rule, you only need to modify the corresponding expression class, and add a new class during extension.
Disadvantages:
- Cause the expansion of classes. Each syntax must produce a non terminator expression. When the syntax is complex, a large number of class files will be generated.
- When the syntax rules are complex, it is difficult to debug if an error occurs.
- The implementation efficiency is relatively low. Because when the expression is complex and the result depends on layers, it will be parsed recursively, and it is difficult to debug.
2. Application scenario of Interpreter pattern in Java
- Regular expression in JDK: Pattern class.
- ExpressionParse interface in SpringTherefore, we mentioned at the beginning that we generally use relatively few interpreter modes in business development. The commonly used expressions have been parsed for us and can be used directly, unless we are engaged in underlying development and need to define more complex expressions ourselves.
3. Performance comparison of common expression engine calculation schemes
Some common expression engine calculation schemes are selected, including java script engine (javax/script), groovy script engine, Expression4j and Fel expression engine.
- The java script engine uses two methods: interpretation execution and compilation execution, groovy script only uses compilation execution (interpretation execution is too slow), and Fel uses two methods: static parameters and dynamic parameters. The following is the test code:
public class ExpressionTest { private int count = 100000; //The compilation and execution efficiency of javax is slightly higher than that of interpretation? Why is it slightly higher?? @Test public void testCompiledJsScript() throws Throwable { javax.script.ScriptEngine se = new ScriptEngineManager().getEngineByName("js"); Compilable ce = (Compilable) se; CompiledScript cs = ce.compile("a*b*c"); Bindings bindings = se.createBindings(); bindings.put("a", 3600); bindings.put("b", 14); bindings.put("c", 4); long start = System.currentTimeMillis(); for (int i = 0; i < count; i++) { cs.eval(bindings); } System.out.println(System.currentTimeMillis() - start); } //JavaScript interpretation and execution @Test public void testJsScript() throws Throwable { javax.script.ScriptEngine se = new ScriptEngineManager().getEngineByName("js"); Bindings bindings = se.createBindings(); bindings.put("a", 3600); bindings.put("b", 14); bindings.put("c", 4); long start = System.currentTimeMillis(); for (int i = 0; i < count; i++) { se.eval("a*b*c", bindings); } System.out.println(System.currentTimeMillis() - start); } //Compilation and execution of groovy @Test public void testGroovy() { //The ScriptEngine and GroovyScriptEngine here are classes written by themselves, not native ScriptEngine se = this.getBean(GroovyScriptEngine.class); Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("param", 5); //ScriptEngine will cache the compiled script when it is executed for the first time. Here, it is deliberately executed once to facilitate caching se.eval("3600*34*param", paramMap); long start = System.currentTimeMillis(); for (int i = 0; i < count; i++) { se.eval("3600*34*param", paramMap); } System.out.println(System.currentTimeMillis() - start); } //Expression4J's expression engine, here through functions, is a little special @Test public void testExpression4j() throws Throwable { Expression expression = ExpressionFactory.createExpression("f(a,b,c)=a*b*c"); System.out.println("Expression name: " + expression.getName()); System.out.println("Expression parameters: " + expression.getParameters()); MathematicalElement element_a = NumberFactory.createReal(3600); MathematicalElement element_b = NumberFactory.createReal(34); MathematicalElement element_c = NumberFactory.createReal(5); Parameters parameters = ExpressionFactory.createParameters(); parameters.addParameter("a", element_a); parameters.addParameter("b", element_b); parameters.addParameter("c", element_c); long start = System.currentTimeMillis(); for (int i = 0; i < count; i++) { expression.evaluate(parameters); } System.out.println(System.currentTimeMillis() - start); } //Expression engine of fel (static parameters, same as above) @Test public void felTest() { FelEngine e = FelEngine.instance; final FelContext ctx = e.getContext(); ctx.set("a", 3600); ctx.set("b", 14); ctx.set("c", 5); com.greenpineyu.fel.Expression exp = e.compile("a*b*c", ctx); long start = System.currentTimeMillis(); Object eval = null; for (int i = 0; i < count; i++) { eval = exp.eval(ctx); } System.out.println(System.currentTimeMillis() - start); System.out.println(eval); } //fel expression engine (dynamic parameters, where the generation of dynamic parameters and the change of variables will consume time, so the test time is inaccurate, just to verify the support for dynamic parameters) @Test public void felDynaTest() { FelEngine e = FelEngine.instance; final FelContext ctx = e.getContext(); ctx.set("a", 3600); ctx.set("b", 14); ctx.set("c", 5); com.greenpineyu.fel.Expression exp = e.compile("a*b*c", ctx); long start = System.currentTimeMillis(); Object eval = null; Random r = new Random(); for (int i = 0; i < count; i++) { ctx.set("a", r.nextInt(10000)); ctx.set("b", r.nextInt(100)); ctx.set("c", r.nextInt(100)); eval = exp.eval(ctx); } System.out.println(System.currentTimeMillis() - start); System.out.println(eval); } public static void main(String[] args) throws Throwable { ExpressionTest et = new ExpressionTest(); //Perform 100W tests et.count = 1000000; et.testCompiledJsScript(); et.testJsScript(); et.testExpression4j(); et.testGroovy(); et.felTest(); } }
The test results are as follows:
From the above performance comparison (excluding the function of expression), fel obviously occupies a great advantage, and groovy and expression4j are acceptable. The execution of java script engine is slow. Therefore, when the expression is not very complex and performance requirements are high, it is recommended to use fel or groovy compilation and execution.