基于 Spring + Mybatis 实现简单的 ElasticSearch 查询客户端

背景

在实际的 Java 项目开发中,比如 Spting Boot 应用,我们可能需要操作来自 ElasticSearch(后文简称 ES) 的数据,比如查询聚合等。同时,我们想要能够自定义DSL语句,满足复杂的查询需求。在目前的 ES Java 客户端 API 中 RestHighLevelClient 可以很好的实现,但是代码较为繁琐,而且不能满足 动 态 D S L \color{red}{动态 DSL} DSL 的需求。因此,考虑基于Spring + Mybatis 实现简单的 ElasticSearch 查询客户端。

原理

  • Mybatis mapper 文件解析生成 DSL 语句
  • Spring RestTemplate 访问 Restful 服务

实现思路
熟悉 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 语句。

DSL-mapper文件

<?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 为传入参数的数据。

核心类:

  • DSLConfiguration.java:完成 DSL 语句的解析
  • ESClient.java:向 ES 发送 Restful 请求获得查询结果
1. DSLConfiguration.java 核心代码
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 中。

2. ESClient.java 核心代码
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 对象列表。

3. 简单使用

在不考虑框架层级的情况下,来看看简单的使用方法:
这里采用客户端工厂返回单例的 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 查询客户端,读者可以进一步扩展,增加除去简单查询的功能,同时可以封装在框架层级,提供给应用开发者。

你可能感兴趣的:(Java,java,mybatis,elasticsearch)