自定义 API
自动生成的自定义模型 CRUD API 不能满足聚合查询或业务动作时,可以定义响应式自定义 API。简单资源读写仍应优先使用自定义模型 API。
选择 API Group
自定义 API 的 group 决定主要使用范围和 OpenAPI 分组:
自定义 API 仍需配置最小权限。使用公开 group 不会自动允许匿名访问,权限规则见角色模板。
使用 CustomEndpoint
CustomEndpoint 从 Halo 2.0.0 开始提供,为 RouterFunction 自动添加 group 和 version 前缀:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
@Component
public class PersonEndpoint implements CustomEndpoint {
@Override
public RouterFunction<ServerResponse> endpoint() {
return RouterFunctions.route()
.GET("/persons/{name}", this::getPerson)
.build();
}
private Mono<ServerResponse> getPerson(ServerRequest request) {
return ServerResponse.ok()
.contentType(APPLICATION_JSON)
.bodyValue(Map.of("name", request.pathVariable("name")));
}
@Override
public GroupVersion groupVersion() {
return new GroupVersion("console.api.my-plugin.halo.run", "v1alpha1");
}
}
最终路径为 /apis/console.api.my-plugin.halo.run/v1alpha1/persons/{name}。保持资源型 API 路径不超过角色模板支持的层级;复杂筛选使用查询参数。
使用带注解的控制器
ApiVersion 从 Halo 2.0.0 开始提供。需要注解风格时,可以使用 @RestController,并通过 @ApiVersion 添加 Halo API group 和 version 前缀。缺少该注解时,控制器仍可能按原始 @RequestMapping 路径注册,因此不能用省略注解的方式禁用 API:
@ApiVersion("console.api.my-plugin.halo.run/v1alpha1")
@RequestMapping("/persons")
@RestController
public class PersonController {
@GetMapping("/{name}")
public Mono<Map<String, String>> get(@PathVariable String name) {
return Mono.just(Map.of("name", name));
}
}
Halo 基于 WebFlux;控制器不能返回阻塞调用。无法替换的阻塞工作应按响应式服务端开发隔离。
处理列表参数
需要支持分页、排序和选择器时,可以在 Halo 2.5.0 及以上版本继承 SortableRequest,复用 Halo 的查询参数解析:
public class PersonQuery extends SortableRequest {
public PersonQuery(ServerWebExchange exchange) {
super(exchange);
}
public String getKeyword() {
return queryParams.getFirst("keyword");
}
@Override
public ListOptions toListOptions() {
var builder = ListOptions.builder(super.toListOptions());
if (StringUtils.isNotBlank(getKeyword())) {
builder.andQuery(contains("spec.name", getKeyword()));
}
return builder.build();
}
}
spec.name 必须已经注册索引。查询与分页调用见 ReactiveExtensionClient。
校验请求体
自定义 API 的输入属于信任边界。使用 Jakarta Bean Validation 或等价的显式校验,不要只依赖 OpenAPI 描述:
public class PersonParam {
@NotBlank
@Size(max = 100)
private String name;
@Min(0)
@Max(150)
private int age;
}
WebFlux Functional Endpoint 不会仅凭字段上的约束注解自动执行校验。先在插件应用上下文中提供 Validator:
@Configuration
public class PluginConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
然后在读取请求体后显式调用:
private final Validator validator;
private Mono<ServerResponse> createPerson(ServerRequest request) {
return request.bodyToMono(PersonParam.class)
.doOnNext(this::validate)
.flatMap(param -> ServerResponse.ok().bodyValue(param));
}
private void validate(PersonParam param) {
var errors = new BeanPropertyBindingResult(param, "person");
validator.validate(param, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
这里的 Validator 为 org.springframework.validation.Validator。校验失败时返回可定位的 4xx 错误,不能把无效输入写入自定义模型。涉及当前用户语言时,在请求范围内设置 Locale,并在结束后恢复。
生成 OpenAPI 文档
需要让 Functional Endpoint 出现在 Swagger 和生成的 API Client 中时,使用 SpringdocRouteBuilder 描述 operationId、tag、参数和响应。operationId 应稳定且唯一,tag 建议包含资源 Kind、版本和作用域。
OpenAPI 分组与 API group 对应。生成客户端的步骤见 DevTools。不要手动修改生成的 api-client 或 api-docs。
源码参考