Files
halo-dark-mode-plugin/.agents/skills/halo-plugin-dev/references/server-lifecycle.md
T
Serendipity edbf78f236 feat: 初始化 Halo 暗色模式插件
- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试
- Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面
- 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等)
- 设计文档和调查文档
- Halo 插件/主题开发 Agent Skills

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 21:03:00 +08:00

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

  1. Only ONE class may extend BasePlugin and be annotated with @Component. Multiple candidates cause startup failure.
  2. Must have @Component (or other Spring stereotype). Without it, lifecycle methods are never invoked.
  3. Use constructor injection for dependencies (preferred over @Autowired fields).

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);
}