For AI agents: the complete documentation index is available at https://docs.halo.run/llms.txt, the full documentation bundle is available at https://docs.halo.run/llms-full.txt, and this page is available as Markdown at https://docs.halo.run/developer-guide/plugin/api-reference/server/extension-client.md.

与自定义模型交互

Halo 提供了两个类用于与自定义模型对象交互 ExtensionClientReactiveExtensionClient

它们提供了对自定义模型对象的增删改查操作。ExtensionClient 是阻塞式的,只能用于非响应式环境,例如已经切换到工作线程的后台任务;WebFlux 控制器、WebFilter 等响应式调用链应使用 ReactiveExtensionClient。后者返回 Reactor 提供的 MonoFlux

public interface ReactiveExtensionClient {

    <E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,
        Comparator<E> comparator);

    // 已经过时,建议使用 listBy 或 listAll 代替
    @Deprecated
    <E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,
        Comparator<E> comparator, int page, int size);

    <E extends Extension> Flux<E> listAll(Class<E> type, ListOptions options, Sort sort);

    <E extends Extension> Mono<ListResult<E>> listBy(Class<E> type, ListOptions options,
        PageRequest pageable);

    /**
     * Fetches Extension by its type and name.
     *
     * @param type is Extension type.
     * @param name is Extension name.
     * @param <E> is Extension type.
     * @return an optional Extension.
     */
    <E extends Extension> Mono<E> fetch(Class<E> type, String name);

    Mono<Unstructured> fetch(GroupVersionKind gvk, String name);

    <E extends Extension> Mono<E> get(Class<E> type, String name);

    /**
     * Creates an Extension.
     *
     * @param extension is fresh Extension to be created. Please make sure the Extension name does
     * not exist.
     * @param <E> is Extension type.
     */
    <E extends Extension> Mono<E> create(E extension);

    /**
     * Updates an Extension.
     *
     * @param extension is an Extension to be updated. Please make sure the resource version is
     * latest.
     * @param <E> is Extension type.
     */
    <E extends Extension> Mono<E> update(E extension);

    /**
     * Deletes an Extension.
     *
     * @param extension is an Extension to be deleted. Please make sure the resource version is
     * latest.
     * @param <E> is Extension type.
     */
    <E extends Extension> Mono<E> delete(E extension);

    /**
     * Watches the changes of all extensions.
     */
    void watch(Watcher watcher);
}
  • fetch(GroupVersionKind, name):以 Unstructured 形式按 GVK 和名称获取自定义模型,适用于不确定具体 Java 类型的场景。
  • watch:注册一个 Watcher 监听所有自定义模型的变更事件。两个 API 均自 Halo 2.4.0 起可用。
已废弃的方法

getJsonExtension(GroupVersionKind, name) 自 Halo 2.23.0 起被标记为 @Deprecated(forRemoval = true),请迁移到 fetch(GroupVersionKind, name)

示例

如果你想在插件中根据 name 参数查询获取到 Person 自定义模型的数据,则可以这样写:

@Service
@RequiredArgsConstructor
public PersonService {
    private final ReactiveExtensionClient client;

    Mono<Person> getPerson(String name) {
        return client.fetch(Person.class, name);
    }
}

或者使用阻塞式 Client

@Service
@RequiredArgsConstructor
public PersonService {
    private final ExtensionClient client;

    Optional<Person> getPerson(String name) {
        return client.fetch(Person.class, name);
    }
}

注意:非阻塞线程中不能调用阻塞式方法。

我们建议你更多的使用响应式的 ReactiveExtensionClient 去替代 ExtensionClient

监听模型变更

如果只需要在自定义模型发生变更时执行一些逻辑(而不是持续调谐到期望状态),可以通过 watch 方法注册一个 Watcher

import jakarta.annotation.PreDestroy;
import java.util.concurrent.atomic.AtomicBoolean;

@Component
public class PersonWatcher implements Watcher {

    private final AtomicBoolean disposed = new AtomicBoolean();
    private Runnable disposeHook = () -> {};

    public PersonWatcher(ReactiveExtensionClient client) {
        client.watch(this);
    }

    @Override
    public void onAdd(Extension extension) {
        if (extension instanceof Person person) {
            // do something
        }
    }

    @Override
    public void onUpdate(Extension oldExtension, Extension newExtension) {
        // do something
    }

    @Override
    public void onDelete(Extension extension) {
        // do something
    }

    @Override
    public void registerDisposeHook(Runnable dispose) {
        this.disposeHook = dispose;
    }

    @PreDestroy
    @Override
    public void dispose() {
        if (disposed.compareAndSet(false, true)) {
            disposeHook.run();
        }
    }

    @Override
    public boolean isDisposed() {
        return disposed.get();
    }
}

WatcheronAddonUpdateonDelete 分别对应模型的创建、更新和删除事件。需要注意的是 watch 会接收所有自定义模型的变更,需要在回调中自行过滤类型,并在插件上下文关闭时调用 dispose() 取消注册;如果需要声明式地将资源持续调谐到期望状态,应优先使用 Reconciler

查询

ReactiveExtensionClient 提供了以下方法用于查询数据:

  • listBy:分页查询数据。
  • listNamesBy:分页查询对象名称。
  • listAll:查询所有数据。
  • listAllNames:查询所有对象名称。
  • listTopNames:查询指定数量的对象名称。
  • countBy:统计符合条件的数据数量。

这些方法都需要一个 ListOptions 参数,用于传递查询条件:

public class ListOptions {
    private LabelSelector labelSelector;
    private FieldSelector fieldSelector;
}

其中 LabelSelector 用于传递标签查询条件,FieldSelector 用于传递字段查询条件。

FieldSelector 支持比自动生成的 APIs 中更多的查询条件,可以通过 run.halo.app.extension.index.query.Queries 来构建。

import static run.halo.app.extension.index.query.Queries.and;
import static run.halo.app.extension.index.query.Queries.equal;

ListOptions.builder()
    .fieldQuery(and(
        equal("name", "test"),
        equal("age", 18)
    ))
    .build();

支持的查询条件如下:

方法说明示例
equal等于equal("name", "test")
notEqual不等于notEqual("name", "test")
greaterThan大于,可通过第三个参数控制是否包含边界greaterThan("age", 18)greaterThan("age", 18, true)
lessThan小于,可通过第三个参数控制是否包含边界lessThan("age", 18)lessThan("age", 18, true)
between在范围内,可分别控制上下边界是否包含between("age", 18, true, 20, false)
in在给定值范围内in("age", 18, 19, 20)
isNull值为空isNull("deletedAt")
all指定字段的所有值all("age")
startsWith以指定字符串开头startsWith("name", "test")
endsWith以指定字符串结尾endsWith("name", "test")
contains包含指定字符串contains("name", "test")
andand(equal("name", "test"), equal("age", 18))
oror(equal("name", "test"), equal("age", 18))
not取反not(equal("name", "test"))
labelExists标签存在labelExists("halo.run/hidden")
labelEqual标签等于labelEqual("env", "production")
labelIn标签值在给定范围内labelIn("env", Set.of("production", "staging"))

FieldSelector 中使用的所有字段都必须添加为索引,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 自定义模型使用索引

使用 Queries 构建查询

从 2.22.0 开始,QueryFactory 已过时,请使用 Queries 创建查询条件。取反查询可以通过 Queries.not(condition)condition.not() 构建。

可以通过 andor 方法组合和嵌套查询条件:

import run.halo.app.extension.index.query.Condition;
import static run.halo.app.extension.index.query.Queries.and;
import static run.halo.app.extension.index.query.Queries.equal;
import static run.halo.app.extension.index.query.Queries.or;

Condition query = and(
    or(equal("dept", "A"), equal("dept", "B")),
    or(equal("age", 19), equal("age", 18))
);
ListOptions.builder()
    .fieldQuery(query)
    .build();

构建 ListOptions

ListOptions 提供了 builder 方法用于构建查询条件,fieldQuery 方法用于传递字段查询条件,labelSelector 方法用于传递标签查询条件。

import static run.halo.app.extension.index.query.Queries.equal;

ListOptions.builder()
    .labelSelector()
    .eq("key-1", "value-1")
    .end()
    .fieldQuery(equal("key-2", "value-2"))
    .build();
  • labelSelector 之后使用 end 方法结束标签查询条件的构建。
  • andQueryorQuery 用于组合多个 FieldSelector 查询条件。

排序

listBylistNamesBylistAlllistAllNameslistTopNames 方法都支持传递 Sort 参数,用于传递排序条件。

import org.springframework.data.domain.Sort;

Sort.by(Sort.Order.asc("metadata.name"))

通过 Sort.by 方法可以构建排序条件,Sort.Order 用于指定排序字段和排序方式,asc 表示升序,desc 表示降序。

排序中使用的字段必须是添加为索引的字段,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 自定义模型使用索引

分页

listBy 方法支持传递 PageRequest 参数,用于传递分页条件。

import run.halo.app.extension.PageRequestImpl;

PageRequestImpl.of(1, 10);

PageRequestImpl.of(1, 10, Sort.by(Sort.Order.asc("metadata.name"));

PageRequestImpl.ofSize(10);

通过 PageRequestImpl.of 方法可以构建分页条件,具有两个参数的方法用于指定页码和每页数量,具有三个参数的方法用于指定页码、每页数量和排序条件。

ofSize 方法用于指定每页数量,页码默认为 1。