效果
标题可能会有些抽象,让我们直接先来看一下最后的结果,来了解我们所谓的"动态逻辑判断能力"指的是什么,以下是我们的成品调用代码
kotlin
fun main() {
val context = mapOf(
"age" to 20,
"name" to "Bob",
"isActive" to true
)
val statement1 = "age >= 18 && (name == "John" OR name == "Jane") && isActive == true"
val userPredicate = KPredicate(statement1, context)
println("result: ${userPredicate.evaluate()}") // 应该输出 false
val statement2 = "age >= 18 && (name == "Bob" OR name == "Bob") && isActive == true"
val userPredicate2 = KPredicate(statement2, context)
println("result: ${userPredicate2.evaluate()}") // 应该输出 true
}
可以看到,我们使用 map 来表示应用执行中的一些上下文信息,同时还有两条不同的语句,然后创建KPredicate对象,把语句和 map 传入,然后调用evaluate方法,就可以得到对应的结果。
背景
看完上面的 demo,你可能会有一个疑问,就是我们为什么需要这样的一个能力?对于这样的逻辑语句,我们直接写代码判断不就好了吗?
但是在实际的复杂项目中,这些语句通常都不会硬编码在实际的代码中,而是由后端服务或者配置服务下发,然后再在具体的业务场景中执行,来给用户做一些具体的逻辑,比如针对不同国家、地区的用户展示不同的入口,针对实时低性能的用户开启一些优化能力等
而在实现这样能力的过程中,iOS 的同事直接使用 iOS 官方提供的NSPredicate 就可以实现,而 Android 平台没有类似的能力,需要自己手动实现,因此为了对齐 iOS 的能力,才有了以下的探索
实现思路
对于这样一个能力,我能够想到的最快的思路就是,站在巨人的肩膀上,抄一个业内已经被广泛使用的有类似能力的库。
在 Android 平台上,我能想到的第一反应就是 Room,熟悉 Android 的朋友都知道这是一个数据库框架,其中包含 SQL 语句解析的能力,因此我们可以看看它是怎么做到的。
在顺着 Room 的注解处理逻辑分析了 Room 相关的源码后,我在 Room 的 compiler 模块找到了 SQLiteLexer.g4 和 SQLiteParser.g4 这两个文件,其中SQLiteLexer.g4
这个文件中包含了很多我们熟悉的SQL 关键字,篇幅原因我就只列其中一小部分
antlr
lexer grammar SQLiteLexer;
options {
caseInsensitive = true;
}
// TODO: Add JSON operators -> and ->> added in 3.38.0
SCOL : ';';
DOT : '.';
OPEN_PAR : '(';
CLOSE_PAR : ')';
COMMA : ',';
ASSIGN : '=';
STAR : '*';
EQ : '==';
NOT_EQ1 : '!=';
...省略
// http://www.sqlite.org/lang_keywords.html
SELECT_ : 'SELECT';
DELETE_ : 'DELETE';
UPDATE_ : 'UPDATE';
INSERT_ : 'INSERT';
...省略
通过搜索学习,我们可以了解到 g4 文件是ANTLR的文件格式,关于 ANTLR,在它的官网上是这么介绍的
ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It's widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees.
ANTLR(ANother Tool for Language Recognition)是一种 强大的解析器生成器,用于读取、处理、执行或 翻译结构化文本或二进制文件。它被广泛用于构建 语言、工具和框架。 ANTLR 从语法中生成一个 可以构建和遍历解析树的解析器。
根据简介,我们可以知道,ANTLR 支持自定义关键字和语句,然后帮我们生成语句对应的解析器,因此,我们的思路基本就可以确定为,基于 ANTLR 定义我们支持的关键字和逻辑语句类型,然后基于它生成的解析器,解析语句后完成我们的逻辑。
代码实现
下面我们就一步步来看怎么样去实现
项目配置
首先,我们要在我们的项目里配置antlr相关的依赖
kotlin
plugins {
kotlin( "jvm" ) version "2.1.20"
// 添加antlr的插件依赖
antlr
}
dependencies {
// 依赖antlr,分为两部分
// 1.添加编译时gradle任务,把g4文件生成对应的java解析文件
// 2.添加运行时依赖
antlr( "org.antlr:antlr4:4.13.1" )
}
// 配置g4文件扫描生成任务
tasks.generateGrammarSource {
arguments = arguments + listOf( "-visitor" , "-long-messages" , "-package" , "org.example.antlr" )
outputDirectory = project.layout.buildDirectory.dir( "generated-src/antlr/main/org/example/antlr" ).get().asFile
}
// 注册在编译kotlin时运行g4文件的解析生成任务
tasks.compileKotlin {
dependsOn(tasks.generateGrammarSource)
}
词法定义
之后我们就可以开始定义我们要使用的所有关键字
antlr
lexer grammar PredicateLexer;
// 定义所有支持的关键字
BOOLEAN: 'true' | 'false' | 'TRUE' | 'FALSE' ;
AND: 'AND' | 'and' | '&&' ;
OR: 'OR' | 'or' | '||' ;
EQUALS: '==' ;
NOT_EQUALS: '!=' ;
GREATER: '>' ;
LESS: '<' ;
GREATER_EQUALS: '>=' ;
LESS_EQUALS: '<=' ;
LPAREN: '(' ;
RPAREN: ')' ;
// 定义字面量
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*;
STRING: '"' (~["\\r\n] | EscapeSequence)* '"' ;
NUMBER: [0-9]+ ( '.' [0-9]+)?;
// 支持转义的 \b \t \n \f \r 和八进制 十六进制
fragment EscapeSequence
: '\' [btnfr"'\]
| '\' ([0-3]? [0-7])? [0-7]
| '\' 'u' + HexDigit HexDigit HexDigit HexDigit
;
fragment HexDigit: [0-9a-fA-F];
// 遇到空白字符就跳过,空白字符没有意义
WS: [ \t\r\n]+ -> skip;
语法定义
之后基于定义好的词法,定义语法
antlr
parser grammar PredicateParser;
// 引入上方定义的词法
options { tokenVocab=PredicateLexer; }
// 定义语法规则
// 接受输入一个predicate,predicate 由一个expr和一个 EOF 组成
predicate: expr EOF;
// expr 由一个comparison和 0 到多个被logicOp连接的comparison组成
expr: comparison (logicOp comparison)*;
// logicOp 只有两种可能,AND 或者 OR
logicOp: AND | OR;
// 每一个 comparison 包括 IDENTIFIER op value 三部分,可能会被括号包裹
comparison: IDENTIFIER op value | LPAREN expr RPAREN;
// op就是操作符,支持我们上面在词法定义中定义的这些
op: EQUALS | NOT_EQUALS | GREATER | LESS | GREATER_EQUALS | LESS_EQUALS;
// value 就是值,可以接受 STRING,NUMBER,BOOLEAN 三种类型
value: STRING | NUMBER | BOOLEAN;
通过插件查看解析效果
定义好之后,我们首先可以安装一个ANTLR的插件,然后我们就可以先在插件提供的窗口中,查看整体解析的正确与否以及解析完的语法树
以下图为例,输入一个表达式之后,就可以在右侧看到解析完的树形结构

如果有错误的话,会直接显示出来

查看产物
在我们之前gradle任务定义好的路径下,可以看到生成的产物,其中PredicateLexer
和PredicateParser
就是对应的词法和语法解析器,然后比较重要的就是PredicateParserBaseVisitor
以及它的接口PredicateParserVisitor
,从命名来看,它是一个访问者设计模式的实现,下面我们就会基于它,来看一下具体它怎么用

kotlin代码实现
kotlin
import org.example.antlr.PredicateParser
import org.example.antlr.PredicateParserBaseVisitor
internal class PredicateEvaluator(private val context: Map<String, Any>) : PredicateParserBaseVisitor<Boolean>() {
companion object {
private const val TAG = "PredicateEvaluator"
}
override fun visitPredicate(ctx: PredicateParser.PredicateContext): Boolean {
Log.d(TAG, "Visiting predicate: ${ ctx.text } " )
return ctx.expr()?.let { visit(it) } ?: false
}
override fun visitExpr(ctx: PredicateParser.ExprContext): Boolean {
Log.d(TAG, "Visiting expr : ${ ctx.text } " )
val comparisonSize = ctx.comparison().size
if (comparisonSize == 0) {
return false
}
val firstComparison = ctx.comparison(0)
var result = visit(firstComparison)
// 如果只有一个表达式,直接返回结果
if (comparisonSize == 1) {
return result
}
// 多个的话,依次往后计算结果
for (i in 1 until ctx.comparison().size) {
val nextComparison = ctx.comparison(i)
val nextValue = visit(nextComparison)
val logicOp = ctx.logicOp(i - 1)
result = when {
// AND的情况下走 &&
logicOp.AND() != null -> result && nextValue
// OR的情况下走 ||
logicOp.OR() != null -> result || nextValue
else -> false
}
}
return result
}
override fun visitComparison(ctx: PredicateParser.ComparisonContext): Boolean {
Log.d(TAG, "Visiting comparison: ${ ctx.text } " )
val inParen = ctx.LPAREN() != null
if (inParen) {
return ctx.expr()?.let { visit(it) } ?: false
}
val identifier = ctx.IDENTIFIER()?.text ?: return false
val operator = ctx.op() ?: return false
val comparisonValue = ctx.value() ?: return false
val value = when {
comparisonValue.STRING() != null -> {
val text = comparisonValue.STRING().text
// 值是string的话,需要把单引号和双引号去掉
text.trim( ' ' ' , '"' )
}
comparisonValue.NUMBER() != null -> {
// 数字类型的话 统一转成double进行计算
comparisonValue.NUMBER().text.toDouble()
}
comparisonValue.BOOLEAN() != null -> {
val boolText = ctx.value().BOOLEAN().text
// boolean类型的话,直接进行文本对比
boolText.equals( "true" , ignoreCase = true)
}
else -> return false
}
val contextValue = context[identifier]
Log.d(TAG, "Comparing: $ identifier ${ operator.text } $ value with context value: $ contextValue " )
when {
// 先看是不是判断相等/不等的语句,是的话直接判断即可
operator.EQUALS() != null -> return value == contextValue
operator.NOT_EQUALS() != null -> return value != contextValue
}
// 否则的话,把map里传的值和字面量里的值都进行解析,然后比较
val contextValueNum = (contextValue as ? Number)?.toDouble() ?: 0.0
val valueNum = (value as ? Number)?.toDouble() ?: 0.0
return when {
operator.GREATER() != null -> contextValueNum > valueNum
operator.LESS() != null -> contextValueNum < valueNum
operator.GREATER_EQUALS() != null -> contextValueNum >= valueNum
operator.LESS_EQUALS() != null -> contextValueNum <= valueNum
else -> false
}
}
}
再之后,只需要简单的进行外部封装一下就可以使用了
kotlin
class KPredicate(
predicateString: String,
private val context: Map<String, Any>
) {
private val parser: PredicateParser
init {
val input = CharStreams.fromString(predicateString)
val lexer = PredicateLexer(input)
val tokens = CommonTokenStream(lexer)
parser = PredicateParser(tokens)
}
fun evaluate(): Boolean {
val tree = parser.predicate()
return PredicateEvaluator(context).visit(tree)
}
}
总结
通过以上内容,我们简单介绍了ANTLR这个强大的工具以及如何基于它实现了一套简单的动态逻辑执行能力,整体功能还比较简单以及没有考虑很多的边界case,但是整体的思路是明确的,如果你的项目里也有类似的需求,都可以基于这种方案来做定制。