整合 Elasticsearch
TioAdminElasticsearchConfig 读取连接配置,创建 RestHighLevelClient,注册到 java-db 的 Elastic 工具类,并在应用关闭时释放客户端。业务代码直接调用 Elastic,不必重复创建客户端或连接池。
1. 添加客户端依赖
在已有 tio-boot-admin 工程的 pom.xml 中加入客户端依赖;已经声明时不重复添加:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.10.0</version>
</dependency>
这里使用框架配置类依赖的 High Level REST Client,不是新的 Java API Client。客户端需要与服务端兼容;不要直接替换成另一套客户端依赖后继续使用原有类型。上游 provided 依赖不会自动进入应用运行时,因此应检查最终应用的依赖和打包结果。
2. 配置连接地址与认证
在当前环境的配置文件,例如 app-dev.properties 中添加:
elasticsearch.rest.urls=http://127.0.0.1:9200
开启用户名和密码认证时,通过本地秘密配置补充:
elasticsearch.rest.username=replace-with-username
elasticsearch.rest.password=replace-with-password
没有启用认证时不配置用户名、密码,不在源码和文档中保存真实凭据。
| 配置项 | 内置配置的处理方式 |
|---|---|
elasticsearch.rest.urls | 地址为空或未设置时跳过初始化;填写包含协议、主机和端口的完整地址 |
elasticsearch.rest.username | 非空时设置 Basic Authentication |
elasticsearch.rest.password | 与用户名一起使用的密码 |
虽然属性名为 urls,当前配置调用 HttpHost.create(urls) 并只创建一个 HttpHost,不能填写逗号分隔的多个地址。只配置密码、不配置用户名时不会启用认证。
当前内置配置没有读取多节点列表、API Key、超时或自定义证书参数。HTTPS 使用客户端默认的证书校验;如果需要自签名 CA、多节点或其他认证方式,应单独扩展客户端配置,不能用不存在的属性假定这些能力已经开启。
3. 在启动配置中初始化
在已有 AdminAppConfig 顶部导入:
import nexus.io.tio.boot.admin.config.TioAdminElasticsearchConfig;
在其 config() 方法中调用一次,并放在业务使用 Elasticsearch 之前:
new TioAdminElasticsearchConfig().config();
如果已有该调用,不要再重复创建配置或客户端。内置配置依次完成:
- 读取地址和认证参数;没有有效地址时返回。
- 创建 RestHighLevelClient,并通过
Elastic.setClient(client)注册。 - 注册关闭钩子,应用退出时关闭客户端。
- 使用
client.ping(RequestOptions.DEFAULT)尝试检查连接。
启动过程会记录 ping 结果,IOException 会被捕获。因此应用启动成功不代表 Elasticsearch 一定可用,更不代表当前账号拥有索引读写权限。应继续验证实际文档操作。
如果应用编译提示找不到 TioAdminElasticsearchConfig,确认应用依赖的框架构建产物已经包含该类;本地修改框架源码后,应先安装框架构建产物,再编译应用。
4. 最小 Handler:写入、读取、删除
下面的示例直接使用 Elastic.index、Elastic.get、Elastic.delete,不单独创建 Service。固定使用示例索引和文档 ID,避免开放任意索引操作。
import com.jfinal.kit.Kv;
import nexus.io.es.client.Elastic;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.exception.BusinessException;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.validator.ParameterValidator;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RequestOptions;
/**
* Minimal document operations using the client initialized by the admin
* framework.
*/
public class ElasticsearchHandler extends BaseHandler {
private static final String INDEX = "example_admin_demo";
public HttpResponse set(HttpRequest request) {
Kv parameters = Kv.create().set(request.getRequestMap());
String message = ParameterValidator.text(parameters.get("message"), "message", 4096);
String documentId = String.valueOf(currentUserId());
Kv document = Kv.create().set("message", message);
IndexRequest indexRequest = new IndexRequest(INDEX).id(documentId).source(document);
IndexResponse response = Elastic.index(indexRequest, RequestOptions.DEFAULT);
Kv result = Kv.create().set("id", response.getId()).set("result", response.getResult().getLowercase());
return TioRequestContext.getResponse().respond(RespBodyVo.ok(result));
}
public HttpResponse get(HttpRequest request) {
String documentId = String.valueOf(currentUserId());
GetRequest getRequest = new GetRequest(INDEX, documentId);
GetResponse response = Elastic.get(getRequest, RequestOptions.DEFAULT);
Kv result = Kv.create().set("id", documentId).set("exists", response.isExists());
if (response.isExists()) {
Kv document = Kv.create().set(response.getSourceAsMap());
result.set("document", document);
}
return TioRequestContext.getResponse().respond(RespBodyVo.ok(result));
}
public HttpResponse delete(HttpRequest request) {
String documentId = String.valueOf(currentUserId());
DeleteRequest deleteRequest = new DeleteRequest(INDEX, documentId);
DeleteResponse response = Elastic.delete(deleteRequest, RequestOptions.DEFAULT);
Kv result = Kv.create().set("id", response.getId()).set("result", response.getResult().getLowercase());
return TioRequestContext.getResponse().respond(RespBodyVo.ok(result));
}
}
同一个 ID 再次写入会覆盖整份文档,不是局部字段更新。GET 默认实时读取文档,不需要为了读回刚写入的数据强制刷新索引;全文检索属于另一条接口链路,受到索引刷新时机影响。
首次写入需要具备索引权限。若集群禁止自动创建索引,由管理员提前手动执行以下示例,不在应用启动时自动建索引:
PUT /example_admin_demo
Content-Type: application/json
{
"mappings": {
"properties": {
"message": { "type": "text" }
}
}
}
索引存在但文档不存在时,读取返回 exists=false,重复删除返回 not_found。索引本身不存在、认证失败或权限不足时,客户端可能抛出异常,由应用统一异常处理器处理;示例不把这些异常伪装成操作成功。
5. 注册方法路由
在已有路由配置方法中直接创建 Handler:
HttpRequestRouter router = TioBootServer.me().getRequestRouter();
ElasticsearchDemoHandler handler = new ElasticsearchDemoHandler();
router.add(HttpMethod.POST, "/api/elasticsearch-demo/set", handler::set);
router.add(HttpMethod.GET, "/api/elasticsearch-demo/get", handler::get);
router.add(HttpMethod.POST, "/api/elasticsearch-demo/delete", handler::delete);
对应类型是 nexus.io.tio.http.server.router.HttpRequestRouter、nexus.io.tio.boot.server.TioBootServer、nexus.io.tio.http.common.HttpMethod 和上面的示例 Handler。Handler 交给路由器持有,无需 Aop。
保留后台 Token 拦截器保护,不把示例写接口加入匿名放行列表。自定义业务鉴权时,仍需按项目的路由权限策略注册。真实业务还需要文档归属校验与合适的业务分层。
6. HTTP 验证步骤
假设服务端口为 8100,没有额外 context-path,使用后台登录返回的 token:
POST /api/elasticsearch-demo/set HTTP/1.1
Host: 127.0.0.1:8100
Authorization: Bearer <token>
依次执行:
| 请求 | 预期结果 |
|---|---|
| POST /api/elasticsearch-demo/set | 首次写入 result=created,重复写入 result=updated |
| GET /api/elasticsearch-demo/get | exists=true,document.message 为 Hello Elasticsearch |
| POST /api/elasticsearch-demo/delete | result=deleted |
| 再次 GET | exists=false |
| 再次 POST delete | result=not_found |
如果配置了 context-path,需要在上述路径前补上前缀。删除接口只删除示例文档,不删除整个索引。
7. 排查连接问题
- 客户端类缺失:检查运行时依赖及最终 JAR。
- 连接被拒绝或超时:检查地址、端口、网络及 Elasticsearch 是否启动。
- 认证或授权失败:检查用户名、密码和索引读写权限。
- TLS 握手失败:确认协议、证书信任及主机名匹配,不关闭证书校验来规避问题。
- ping 成功但写入失败:检查索引权限、自动创建索引策略以及 mapping。
