Freemarker编写ES语句 JSON解析
Freemarker编写ES语句 JSON解析
业务上需要使用ES json语句做检索。又想类似Mybatis Mapper SQL写法,比起ES的API,SQL语句的可读性较高。遂采用Freemarker模板引擎做JSON解析工具。在此做个记录
模板引擎
freemarker、velocity或者thymeleaf都能实现。这里只说明Freemarker相关。
语句
例如下面的这条简单SQL:
<@select>
findByAggs:
{
"query": {
<#if shouldArray??>
"bool": {
"should": ${shouldArray}
}
<#else >
"match_all":{}
</#if>
},
"aggs": {
"body": {
"terms": {
"field": "${interestType}"
}
}
}
}
</@select>
<@select></@select>
这个是自定义指令,Freemarker官网有关于自定义指令的描述
Freemarker中文手册-自定义指令,这里就不多做赘述了。- findByAggs: 是自定义的方法名称,调用模板引擎时,需要通过方法名称参数,来找到对应的语句。
- 其余的就是Freemarker的语法和ES JSON的格式要求,这里也不多做说明。
工具类
Configuration的配置
首先是Configuration的配置
private Configuration getCfg() {
if (cfg == null) {
cfg = new Configuration(Configuration.VERSION_2_3_28);
cfg.setClassLoaderForTemplateLoading(this.getClass().getClassLoader(), this.esNoSqlTemplateConfig.getPathResources());
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
cfg.setLogTemplateExceptions(false);
}
return cfg;
}
@Configuration
public class EsNoSqlTemplateConfig {
@Value("${es.template.path:/noSql/}")
private String pathResources;
public EsNoSqlTemplateConfig() {
}
public String getPathResources() {
return this.pathResources;
}
这些都是基本配置
- 这里我是配置了一个压制异常,setTemplateExceptionHandler。Freemarker如果说你在模板中引入了插值,而没有传递对应的参数,会报错误,但不影响它的执行。由于业务中,同一个.ftl文件中,不可能只会有一个方法,基本上都是多个,为了不显示这些不必要的错误日志,我这里选择把Freemarker错误日志给压制了。
- setClassLoaderForTemplateLoading 这个是用于配置模板引擎加载路径。作为通用工具类,可能被各个微服务所引用,每个微服务对应的模板引擎的存放位置也可能不尽相同。所以此处留了一个配置项
读取模板引擎
读取模板引擎的主要方法
private Template getTemplate(String name) throws Exception {
Configuration cfg = this.getCfg();
return cfg.getTemplate(name);
}
public String getContent(String name, String methodName, Map<String, Object> rootMap) {
try {
String templateName = name;
if (rootMap == null) {
rootMap = new HashMap(10);
}
if (!name.contains("ftl")) {
templateName = name + ".ftl";
}
((Map)rootMap).put("select", new SelectDirective(methodName));
StringWriter writer = new StringWriter();
Template template = (Template)templateMap.get(templateName);
if (template == null) {
template = this.getTemplate(templateName);
templateMap.put(templateName, template);
}
template.process(rootMap, writer);
String jsonStr = writer.toString().replaceAll("[\u0000]", "");
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
return jsonObject.toJSONString();
} catch (Exception var9) {
var9.printStackTrace();
throw new RuntimeException("JSON transfer failed,please checked SQL");
}
}
}
自定义指令的类的实例
public class SelectDirective implements TemplateDirectiveModel {
private String methodName;
private boolean isNew;
private boolean finish;
private static Logger logger = LoggerFactory.getLogger(SelectDirective.class);
public SelectDirective() {
}
public SelectDirective(String methodName) {
this.methodName = methodName;
}
public void execute(Environment environment, Map params, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
this.isNew = false;
if (!this.finish) {
if (templateDirectiveBody == null) {
throw new RuntimeException("method body cannot empty!");
}
templateDirectiveBody.render(new SelectDirective.SelectWriter(environment.getOut()));
}
}
private class SelectWriter extends Writer {
private final Writer out;
SelectWriter(Writer out) {
this.out = out;
}
public void write(char[] childrenBuffer, int off, int len) throws IOException {
String value = String.valueOf(childrenBuffer, off, len);
if (!StringUtils.isEmpty(value) && (value.contains(SelectDirective.this.methodName) || SelectDirective.this.isNew)) {
value = value.replaceAll(SelectDirective.this.methodName + ":", "");
SelectDirective.this.isNew = true;
this.out.write(value);
SelectDirective.this.finish = true;
}
}
public void flush() throws IOException {
}
public void close() throws IOException {
}
}
}
- 由于使用的是自定义指令,所以在传参的时候,我这里多传入了一个参数
select,new SelectDirective(methodName)
。官网也有这个自定义类的描述,我也是参照官网的做了一些改动 methodName
就是外部传进来的方法名,我传入方法名去寻找模板引擎里对应的方法语句- 官网推荐自定义指令放入共享变量中,但由于在业务上,每次传进来的方法名其实都不一样,所以没有选择放入共享变量中,每次调用都会生成一个新的
SelectDirective
对象存入到Map中 - 这里我加了两个布尔值,isNew是为了过滤其他方法的语句。可能有更好的解决方案,只是目前没找到,采用了这种方式。在模板中匹配方法名,定位到之后,我就把这个方法相关的语句截取到。这是在Debug的时候发现,如果是不同的语句,也就是每个
<@select><@/select>
,都会重新调用一次execute()方法,基于这个机制,所以采用了布尔值和方法名来截取出真正的语句。
finish 只是用于找到语句后,直接结束,返回结果 - 这里要提一个坑点,之前也是被困扰了很久。
public void write(char[] childrenBuffer, int off, int len)
,
重新write方法的时候,转成String
时切记,一定要带上off
和len
。例如代码里的String value = String.valueOf(childrenBuffer, off, len);
之前写的时候没有带上起始和结束,导致freemarker插值出现了错误,调试时发现freemarker最后写入数据时,又会把一个参数给带进来。这里我懒得贴错误场景,可以自己尝试一下便知,不带off``len
,有惊喜。
到这里差不多就完成了,根据传入的MAP参数已经Freemarker语法,生成动态的JSON语句。这个仅针对与ES查询的场景。如果有其他业务场景,需要重新做调整