Blog:使用 Antlr 解析配置文件
在纠结了一阵子 yml,ini,xml甚至 lua 等等 配置文件的格式后,还是决定使用antlr实现了一种我自定义的格式的解析。 这个格式是这个样子的:
// Here is some comment
shared {
string _baseUrl = "http://localhost:8080";
string domain = "riguz.com";
bool ssl = false;
int version = 19;
int subVersion = 25;
float number = 19.25;
string urls = ["http://localhost:8080", "http://riguz.com:8080"];
string domains = ["riguz.com", "dr.riguz.com"];
bool sslArray = [true, false];
int versionArray = [19, 25];
float numberArray = [18.01, 19.25, 20.23];
};
scope dev_db {
string url = ${domain} .. ":3306/mysql";
string user = "lihaifeng";
int connections = 10;
string password = "iikjouioqueyjkajkqq==";
string domains = ${domains};
};
其实是一个k-v形式的文本文件,支持的基本类型有:字符串、布尔值、整数、小数、数组。定义的方法类似于Java或者C语言,
string _baseUrl = "http://localhost:8080"
前面会限定数据类型。如果要定义数组,则用
bool sslArray = [true, false];
这种形式。
然后使用scope区分不同的配置块。因为可能有些相同的配置会重名,这样我们利用不同的scope去区分就好了。考虑到有些配置中需要共同的变量的使用,所以定义了一个shared的scope,这个是写死的scope,其他scope中只能引用shared scope中的变量。
字符串连接使用```..```操作符。这样可以组装字符串。详细的实现可以在forks的子项目config中找到。
另外还实现了一个类似Play! Framework的路由定义文件的解析,长这个样子的:
controllers admin{
package com.riguz.forks.demo.controller
UserController
FileController
}
controllers {
package com.riguz.forks.demo.admin
UserController->AdminUserController
PostController
}
filters {
package com.riguz.forks.demo.filters
AuthorizationFilter
NocsrfFilter
}
routes admin {
+AuthorizationFilter
get /users UserController.getUsers()
get /users/:id UserController.getUser(id: Long)
post /users UserController.createUser()
get /users/:id/files/*name FileUserController.getFile(id: Long, name: String)
}
routes guest {
+NocsrfFilter
get /posts PostUserController.getPosts()
get /posts/:id PostUserController.getPost(id: String)
}
routes guest {
+NocsrfFilter
get /posts PostUserController.getPosts()
get /posts/:id PostUserController.getPost(id: String)
}
这个文件的解析也在上面的git中可以找到实现。通过Antlr可以很方便的把类似这样的文件解析出来,你甚至可以实现自己的领域语言。在实现过程中,遇到过一些问题,来说下问题吧。
首先是Antlr提供了Listener和Visitor两种方式,起初使用Listener来实现但是感觉比较麻烦,而使用Visitor则可以直接通过返回值来取得AST解析结果。我们解析一个文件的时候,是自顶向下的,一个个的去解析的,比如我们的配置文件的antlr语法定义如下:
script
: shared? scope*
EOF
;
其中shared又是这样的
shared
: SHARED LBRACE (property SEMI)* RBRACE SEMI
;
也就是说 ```shared { k=v...} ;```这样的形式,然后又开始到了property:
property
: type NAME ASSIGN expression #basicProperty
| type NAME ASSIGN LBRACK
expression? (COMMA expression)*
RBRACK #arrayProperty
;
这样层层往下来看。然后解析的时候也是一样,我们首先有一个顶层的解析器:
public class ScriptVisitor extends CfParserBaseVisitor<Map<String, ScriptVisitor.Scope>> {
private static final Logger logger = LoggerFactory.getLogger(ScriptVisitor.class);
@Override
public Map<String, Scope> visitScript(CfParser.ScriptContext ctx) {
...
}
这个Visitor负责解析语法文件中定义的script块,然后解析里面的scope:
ScopeVisitor scopeVisitor = new ScopeVisitor(context);
ctx.scope().forEach(scopeContext -> {
logger.debug("Visit scope:{}", scopeContext.getText());
Scope scope = scopeContext.accept(scopeVisitor);
scopes.put(scope.name, scope);
});
这样又实现一个ScopeVisitor去解析scope就好了。详细的实现就不多贴代码了。
另外一个问题是,对于错误的处理,我们在哪一步做?比如```bool s = "123";```这是错误的,我们其实可以在定义grammar的时候就避免这种错误来,但写起来会麻烦一些。目前的实现是在Visitor中去对逻辑进行判断的,前面只做语法检查就可以了。
参考: