- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试 - Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面 - 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等) - 设计文档和调查文档 - Halo 插件/主题开发 Agent Skills Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
Shared Beans (Dependency Injection)
Halo exposes several core beans that any plugin can inject via constructor injection.
Source references (Halo main branch):
- ReactiveExtensionClient
- ExtensionClient
- SchemeManager
- ExtensionGetter
- UserService
- RoleService
- AttachmentService
- PostContentService
- ExternalLinkProcessor
- ExternalUrlSupplier
- NotificationReasonEmitter
- NotificationCenter
- CryptoService
- RateLimiterRegistry
- ReactiveSettingFetcher
- SettingFetcher
- PluginConfigUpdatedEvent
- JsonUtils
- ExtensionUtil
- MetadataUtil
- PageRequestImpl
- SortResolver
- AnonymousUserConst
Docs Routing
This file lists frequently used beans, not every injectable type in Halo. For method signatures and newer APIs, verify against the official docs or source before coding.
When working from a local docs checkout, use the same paths under
docs/developer-guide/...; versioned docs live under
versioned_docs/version-2.25/....
Quick Index
- Custom model CRUD: ReactiveExtensionClient
- Blocking model CRUD: ExtensionClient
- Extension point lookup: ExtensionGetter
- Plugin settings: SettingFetcher / ReactiveSettingFetcher
- Notifications: Notifications
- JSON helpers: JsonUtils
ReactiveExtensionClient
Reactive CRUD for custom extensions.
private final ReactiveExtensionClient client;
// List with options
client.listBy(Person.class, query.toListOptions(), query.toPageRequest());
// Get by name
client.fetch(Person.class, "person-name");
// Create
client.create(person);
// Update
client.update(person);
// Delete
client.delete(person);
ExtensionClient
Blocking version of ReactiveExtensionClient. Use only in non-NIO threads (e.g., background tasks).
SchemeManager
Register/unregister custom extension types.
schemeManager.register(Person.class);
schemeManager.register(Person.class, indexSpecs -> { /* ... */ });
schemeManager.unregister(Scheme.buildFromType(Person.class));
ExtensionGetter
Retrieve implementations of an extension point.
private final ExtensionGetter extensionGetter;
// Get all implementations
extensionGetter.getExtensions(AttachmentHandler.class);
UserService
User operations: get info, update password, create users.
ReactiveUserDetailsService
Mono<UserDetails> findByUsername(String username);
RoleService
Role operations: query roles, bindings, dependencies.
AttachmentService
Upload, delete, get access URLs for attachments.
PostContentService
Get post content with version management.
postContentService.getHeadContent(postName); // latest draft
postContentService.getReleaseContent(postName); // published version
postContentService.getSpecifiedContent(snapshotName);
postContentService.listSnapshots(postName);
ExternalLinkProcessor
Convert relative URLs to absolute using configured externalUrl.
externalLinkProcessor.processLink("/post/1");
// -> "https://example.com/post/1"
ExternalUrlSupplier
Get the configured external URL.
NotificationReasonEmitter / NotificationCenter
Send notifications and manage subscriptions.
ServerSecurityContextRepository
Access authentication context. Required if your filter runs before Spring Security's ReactorContextWebFilter.
CryptoService
Decrypt login passwords or reuse the public key.
cryptoService.readPublicKey();
cryptoService.decrypt(encryptedPassword);
RateLimiterRegistry
Create rate limiters (remember to clean up in stop()).
var rateLimiter = rateLimiterRegistry.rateLimiter(key,
new RateLimiterConfig.Builder()
.limitForPeriod(1)
.limitRefreshPeriod(Duration.ofSeconds(60))
.build());
SystemInfoGetter (Halo 2.20.11+)
Mono<SystemInfo> info = systemInfoGetter.get();
// Contains: title, subtitle, logo, favicon, url, version, seo, locale, timeZone, activatedThemeName
ReactiveSettingFetcher / SettingFetcher
Fetch plugin configuration defined in plugin.yaml (settingName / configMapName).
The fetcher caches values internally and auto-refreshes when configuration changes.
Reactive (WebFlux):
private final ReactiveSettingFetcher settingFetcher;
// Fetch a typed config object by group name
settingFetcher.fetch("seo", SeoSetting.class)
.doOnNext(seo -> { ... });
// Get raw JsonNode by group
settingFetcher.getSettingValue("seo");
// Get all groups as a map
settingFetcher.getSettingValues();
Blocking (background tasks / non-reactive code):
private final SettingFetcher settingFetcher;
Optional<SeoSetting> seo = settingFetcher.fetch("seo", SeoSetting.class);
JsonNode raw = settingFetcher.getSettingValue("seo");
Map<String, JsonNode> all = settingFetcher.getSettingValues();
Listen for config changes:
@EventListener
public void onConfigUpdated(PluginConfigUpdatedEvent event) {
if (event.getNewConfig().containsKey("seo")) {
// re-apply configuration
}
}
Prerequisite: In
plugin.yaml, declaresettingName: my-settingsandconfigMapName: my-configmap, and create a correspondingsettings.yamlextension resource defining the form schema and groups.
JsonUtils
JSON serialization/deserialization utilities.
Note:
@Deprecated(forRemoval = true, since = "2.23.0")— prefertools.jackson.databind.json.JsonMapperin newer Halo versions.
// Object to JSON string
String json = JsonUtils.objectToJson(myObject);
// JSON string to object
MyObject obj = JsonUtils.jsonToObject(json, MyObject.class);
// JSON string with generics
Set<String> tags = JsonUtils.jsonToObject(json, new TypeReference<>() {});
// Map to object
MyObject obj = JsonUtils.mapToObject(map, MyObject.class);
// Deep copy
MyObject copy = JsonUtils.deepCopy(original);
// Access the underlying ObjectMapper
ObjectMapper mapper = JsonUtils.mapper();
mapper.convertValue(data, new TypeReference<>() {});
ExtensionUtil / MetadataUtil
Utility methods for Extension lifecycle and metadata.
// Check deletion state
boolean deleted = ExtensionUtil.isDeleted(extension);
// Query for non-deleted resources (for ListOptions)
ListOptions.builder().fieldQuery(ExtensionUtil.notDeleting()).build();
// Finalizer management
ExtensionUtil.addFinalizers(metadata, Set.of("my-plugin/finalizer"));
ExtensionUtil.removeFinalizers(metadata, Set.of("my-plugin/finalizer"));
// Default sort: creationTimestamp desc, name asc
Sort sort = ExtensionUtil.defaultSort();
// Safe metadata access (auto-initializes null collections)
Map<String, String> labels = MetadataUtil.nullSafeLabels(extension);
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(extension);
PageRequestImpl / PageRequest
Pagination for list queries. Page numbers are 1-based.
// Basic pagination
PageRequestImpl.of(page, size); // page >= 1, size <= 1000
PageRequestImpl.of(page, size, Sort.by("name"));
PageRequestImpl.ofSize(size); // page 1, given size
// With sorting
PageRequestImpl.of(1, 20, Sort.by(
Sort.Order.desc("metadata.creationTimestamp"),
Sort.Order.asc("metadata.name")
));
SortResolver / SortableRequest
Resolve sorting from HTTP query parameters.
// In a CustomEndpoint handler
Sort sort = SortResolver.defaultInstance.resolve(exchange);
// Or extend SortableRequest for standardized list queries
public class MyQuery extends SortableRequest {
public MyQuery(ServerWebExchange exchange) { super(exchange); }
public ListOptions toListOptions() {
return labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
}
}
// Usage
MyQuery query = new MyQuery(request.exchange());
client.listBy(MyExtension.class, query.toListOptions(), query.toPageRequest());
Query parameter format: ?sort=field,asc or ?sort=field,desc. Multiple sorts: ?sort=field1,asc&sort=field2,desc.
AnonymousUserConst
Check for anonymous users:
if (AnonymousUserConst.isAnonymousUser(authentication.getName())) {
// Handle anonymous user
}
// Constants: AnonymousUserConst.PRINCIPAL ("anonymousUser"), AnonymousUserConst.Role ("anonymous")
BackupRootGetter / PluginsRootGetter
Supplier<Path> for backup directory and plugin directory.
LoginHandlerEnhancer
Hook into login success/failure for custom logic.