在实际的 Java 项目开发中,比如 Spting Boot 应用,我们可能需要操作来自 ElasticSearch(后文简称 ES) 的数据,比如查询聚合等。同时,我们想要能够自定义DSL语句,满足复杂的查询需求。在目前的 ES Java 客户端 API 中 RestHighLevelClient 可以很好的实现,但是代码较为繁琐,而且不能满足 动 态 D S L \color{red}{动态 DSL} 动态DSL 的需求。因此,考虑基于Spring + Mybatis 实现简单的 ElasticSearch 查询客户端。
实现思路
熟悉 Mybatis 加载流程的都知道,Mybatis 会将所有的配置以及 SQL 语句初始化到 Configuration中。而且Spring 与 Mybatis 集成后会对 Mybatis 进行加载初始化。因此,可以将 DSL 语句以 SQL 的方式写到 mapper 文件中(以 xml 的的方式),利用 Mybatis 的 mapper 解析器生成相应的 DSL 语句字符串,还能利用 Mybatis 的标签完成动态 DSL 语句。
有了 DSL 语句后,我们就可以利用 Java 代码向 ES 服务器的索引发送请求,如 post 请求进行查询。这里直接采用的是 Spring 的 RestTemplate。
这里以 post 请求查询为例。
1.创建一个简单的 Spring Boot 应用,引入 Mybatis 和 Spring Web 的依赖(这里不一定是 Spring Boot 应用,根据自己实际 Java 应用封装即可)。
2.在 src/main/resources/mapper/es/ 路径下新建 DslMapper.dsl.xml,用于编写 DSL 语句。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gler.springboot.mapper.es.DslMapper">
<select id="qryFromES" >
{
"query":{
"term":{
"ID":"${ID}"
}
}
}
</select>
</mapper>
这里写一个简单的 DSL 查询语句, sql ID = qryFromES,配置参数采用$,查询 ES 服务器索引中 ID 为传入参数的数据。
核心类:
public class DSLConfiguration extends Configuration {
private static final String ES_PATH = "classpath:**/*.dsl.xml"; // 可配置在文件中进行读取
public DSLConfiguration() {
super();
try {
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resource = resolver.getResources(ES_PATH);
for (Resource rs : resource) {
new XMLMapperBuilder(rs.getInputStream(), this, resource.toString(), this.getSqlFragments()).parse();
}
} catch (Exception e) {
}
}
}
DSLConfiguration 继承自 Mybatis 的 Configuration,在构造方法中,增加 DSL mapper 文件的 XMLMapperBuilder 解析。首先利用 Spring 的 ResourcePatternResolver 对指定路径下的所有 DSL mapper 文件进行加载,再遍历所有的加载资源,用 XMLMapperBuilder 进行解析,将 DSL 语句加到 DSLConfiguration 中。
public class ESClient {
private final Configuration configuration;
public ESClient(Configuration configuration) {
this.configuration = configuration;
}
/**
* 生成DSL语句
*
* @param mapperId
* @param params
* @return
*/
private String generateDsl(String mapperId, Object param) {
MappedStatement statement = configuration.getMappedStatement(mapperId);
return statement.getBoundSql(param).getSql();
}
/**
* 发送http请求进行查询
*
* @param index
* @param dslStr
* @return
*/
private String sendQuery(String index, String dslStr) {
try {
String authorization = "Basic "
+ Base64.encodeBase64String(("ES集群用户名" + ":" + "ES集群密码").getBytes("UTF-8"));
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", authorization);
headers.set("Content-Type", "application/json; charset=UTF-8");
String url = "http://ES集群IP:端口/" + index + "/_search";
HttpEntity<String> requestEntity = new HttpEntity<String>(dslStr, headers);
RestTemplate restTemplate = new RestTemplate();
return restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class).getBody();
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 解析查询结果【简单查询解析,聚合查询需要类似的解析】
*
* @param responeJsonStr
*/
private List<JSONObject> parse(String responeJsonStr) {
JSONObject resultJson = JSONObject.parseObject(responeJsonStr);
List<JSONObject> results = new ArrayList<JSONObject>();
if (resultJson.containsKey("hits")) {
JSONArray innerHits = resultJson.getJSONObject("hits").getJSONArray("hits");
/* 剔除外层元数据,组装最后结果集 */
for (Object hit : innerHits) {
JSONObject result = ((JSONObject) hit).getJSONObject("_source");
if (result != null) {
results.add(result);
}
}
}
return results;
}
}
/**
* @param mapperId mapper 的 Sql Id
* @param param DSL 语句中的参数
* @param index 索引名
*/
public List<JSONObject> query(String mapperId, Object param, String index) {
String dslStr = this.generateDsl(mapperId, param);
String responeStr = this.sendQuery(index, dslStr);
return this.parse(responeStr);
}
ESClient 这里有简单的几个方法:
generateDsl 方法
从 DSLConfiguration 中获取相应 Sql ID 的 DSL 语句。
sendQuery 方法
向 ES 集群的具体索引发送 post 请求(带着 DSL)查询数据,并返回响应报文。
parse 方法
将查询结果字符串解析成 JSONObject 对象列表。
在不考虑框架层级的情况下,来看看简单的使用方法:
这里采用客户端工厂返回单例的 ESClient,只会 new 一个ESClient。比如从索引 gler_test_index 查询 ID 为 gler001 的数据。
ESClient client = ESClientFactory.getClient();
Map<String, Object> param = new HashMap<String, Object>();
param.put("ID", "gler001");
List<JSONObject> resultList = client.query("com.gler.springboot.mapper.es.DslMapper.qryFromES", param, "gler_test_index");
我们来看看发送的请求:
{
"query": {
"term": {
"ID": "gler001"
}
}
}
收到的响应报文:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 9.114636,
"hits": [{
"_index": "gler_test_index-20200101144724",
"_type": "main",
"_id": "gler001",
"_score": 9.114636,
"_source": {
"USER_ID": "gler001",
"USER_NAME": "gler",
"ID": "gler001"
}
}]
}
}
这样,就根据自定义 DSL 语句查询到了想要的数据。同时,还可以利用 Mybatis 的标签定义动态 DSL,使得查询变得灵活。
这里采用 Mybatis 和 Spring 构建了简单的 ES 查询客户端,读者可以进一步扩展,增加除去简单查询的功能,同时可以封装在框架层级,提供给应用开发者。