edbf78f236
- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试 - Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面 - 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等) - 设计文档和调查文档 - Halo 插件/主题开发 Agent Skills Co-Authored-By: Claude <noreply@anthropic.com>
4.5 KiB
4.5 KiB
Plugin Lifecycle
The plugin entry class extends run.halo.app.plugin.BasePlugin and must be annotated with @Component.
Source references (Halo main branch):
Entry Class Template
package com.example.myplugin;
import org.springframework.stereotype.Component;
import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;
@Component
public class MyPlugin extends BasePlugin {
public MyPlugin(PluginContext pluginContext) {
super(pluginContext);
}
@Override
public void start() {
// Called after classes are loaded and before the plugin is marked active
// Register schemes, initialize caches, start watchers, etc.
}
@Override
public void stop() {
// Called when the plugin is stopped (disabled)
// Unregister schemes, dispose watchers, clean up resources
}
@Override
public void delete() {
// Called when the plugin is uninstalled
// Final cleanup, delete external data if needed
}
}
Lifecycle Behavior
| Method | When Called | Typical Actions |
|---|---|---|
start() |
After plugin classes loaded, before activation | schemeManager.register(), init caches, register watchers |
stop() |
When plugin is disabled | schemeManager.unregister(), dispose watchers, clear rate limiters |
delete() |
When plugin is uninstalled | Delete external resources, cleanup |
Important Rules
- Only ONE class may extend
BasePluginand be annotated with@Component. Multiple candidates cause startup failure. - Must have
@Component(or other Spring stereotype). Without it, lifecycle methods are never invoked. - Use constructor injection for dependencies (preferred over
@Autowiredfields).
Scheme Registration / Cleanup
@Component
public class MyPlugin extends BasePlugin {
private final SchemeManager schemeManager;
public MyPlugin(PluginContext ctx, SchemeManager schemeManager) {
super(ctx);
this.schemeManager = schemeManager;
}
@Override
public void start() {
schemeManager.register(MyExtension.class, indexSpecs -> {
indexSpecs.add(IndexSpecs.<MyExtension, String>single("spec.slug", String.class)
.indexFunc(ext -> ext.getSpec().getSlug()));
});
}
@Override
public void stop() {
schemeManager.unregister(Scheme.buildFromType(MyExtension.class));
}
}
Watchers
Watch extension changes for cache invalidation or reactive workflows:
@Component
public class MyPlugin extends BasePlugin {
private final ReactiveExtensionClient client;
private Watcher watcher;
// ... constructor
@Override
public void start() {
watcher = new Watcher() {
private volatile boolean disposed = false;
@Override
public void onAdd(Extension extension) {
if (extension instanceof MyExtension) {
// handle add
}
}
@Override
public void onUpdate(Extension oldObj, Extension newObj) {
if (newObj instanceof MyExtension) {
// handle update
}
}
@Override
public void onDelete(Extension extension) {
if (extension instanceof MyExtension) {
// handle delete
}
}
@Override
public void dispose() { disposed = true; }
@Override
public boolean isDisposed() { return disposed; }
};
client.watch(watcher);
}
@Override
public void stop() {
if (watcher != null) watcher.dispose();
}
}
RateLimiter Cleanup
If creating rate limiters via RateLimiterRegistry, always clean up in stop():
private final Set<String> limiterNames = ConcurrentHashMap.newKeySet();
@Override
public void stop() {
limiterNames.forEach(rateLimiterRegistry::remove);
}