Halo Plugin Loading

Since the release of version 2.0, Halo has transitioned into a highly modular ecosystem by introducing a robust plugin architecture. This system allows developers to extend the core functionality of the CMS through independent JAR files, ensuring that the core remains lightweight while offering nearly limitless customization.

The plugin loading process is a sophisticated orchestration managed by the PluginManager. This component is responsible for the entire lifecycle of a plugin—from the moment the system scans the file directory to the point where the plugin's features are integrated into the Spring application context. Understanding this mechanism is essential for developers looking to build stable, high-performance extensions.
The loading sequence is not a simple file read; it is a multi-stage process designed to ensure system stability and dependency integrity. The PluginManager follows these primary steps:

  1. Discovery: The system scans the designated plugins directory to identify all available JAR files.
  2. Parsing: For each discovered JAR, the manager parses the internal plugin.yaml file to extract essential metadata.
  3. Validation: The system performs a two-way check: verifying if the plugin is compatible with the current Halo version and ensuring all required dependencies are present and active.
  4. Isolation: A dedicated PluginClassLoader is instantiated for each plugin to prevent library conflicts.
  5. Instantiation: The plugin's entry point (the main class) is instantiated, and its lifecycle methods are triggered.
  6. Registration: The plugin's components (such as controllers or services) are scanned and registered into the Spring IoC container.

The Manifest: plugin.yaml

Every Halo plugin must contain a plugin.yaml file located at its root. This file acts as the "identity card" of the plugin, providing the system with the instructions necessary to manage it.

A standard manifest follows this structure:

apiVersion: plugin.halo.run/v1alpha1
kind: Plugin
metadata:
  name: my-plugin
spec:
  enabled: true
  requires: ">=2.20.0"
  version: 1.0.0
  displayName: "My Custom Plugin"
  description: "An example plugin for Halo"
  author:
    name: Demo Developer
    website: https://example.com
  logo: logo.png
  pluginClass: run.halo.plugin.demo.DemoPlugin

Key fields to note include:

  • pluginClass: This is the most critical field, as it points to the implementation of the BasePlugin class.
  • requires: This defines the version constraints for the Halo core, preventing crashes caused by API mismatches.
  • enabled: A boolean flag that determines whether the plugin should be activated automatically upon startup.

Class Loading and Dependency Isolation

One of the most significant challenges in plugin-based architectures is "dependency hell"—a situation where two different plugins require different versions of the same third-party library. Halo solves this through a sophisticated isolation strategy using a custom PluginClassLoader.

By extending URLClassLoader and overriding the loadClass logic, Halo implements a hybrid loading strategy:

  • Core Delegation: When a plugin requests a class from the run.halo.app.* package, the loader delegates this to the parent (system) class loader. This ensures all plugins have a unified view of the Halo API.
  • Plugin-First Loading: For classes contained within the plugin's own JAR, the PluginClassLoader prioritizes its own local classpath. This allows a plugin to use a specific version of a library (e.g., Jackson 2.15) even if another plugin or the core system uses a different version.
  • Strict Isolation: By default, one plugin cannot "see" the internal classes of another plugin.

Note for Developers: Because of this isolation, if you need to interact with another plugin, you should never attempt to reference its internal classes directly. Instead, always use the official Extension Points or the Event Bus provided by Halo.

Lifecycle Management and Spring Integration

Once a plugin is validated and isolated, Halo manages its operational lifecycle through the BasePlugin class. Developers implement two primary hooks:

  • start(): Triggered when the plugin is being initialized. This is the ideal place to set up event listeners, initialize local caches, or establish database connections.
  • stop(): Triggered during system shutdown or when the plugin is disabled. This method is crucial for resource cleanup to prevent memory leaks.

Beyond the lifecycle, Halo leverages Annotation-driven Extension. During the loading phase, the system scans the plugin for specific annotations and dynamically registers them into the Spring ApplicationContext. For example, defining a REST endpoint is as simple as:

@ApiController
public class DemoController {
    @GetMapping("/api/demo/hello")
    public String hello() {
        return "Hello from the plugin!";
    }
}

Other powerful extension points include @Finder for discovery mechanisms, @Theme for UI customizations, and @ReactiveExtension for non-blocking logic.

Dependency Resolution and Topological Sorting

Halo supports complex inter-plugin relationships. If "Plugin B" requires "Plugin A" to function, this relationship is declared in the pluginDependencies section of the plugin.yaml:

spec:
  pluginDependencies:
    - pluginName: other-plugin
      version: ">=1.0.0"

To handle these requirements, the PluginManager constructs a dependency graph. It then performs a topological sort to determine the exact order in which plugins must be loaded. This ensures that "Plugin A" is fully instantiated and its beans are registered before "Plugin B" attempts to access them. If a dependency is missing or the version requirement is not met, the system will abort the loading of the dependent plugin and log a specific error.

Practical Development Workflow

To build and deploy a plugin, follow this streamlined workflow:

  1. Project Setup: Initialize a Gradle project and include the halo-plugin-api dependency.
  2. Implementation: Create your main class extending BasePlugin and implement the start() and stop() methods.
  3. Manifest Creation: Generate the plugin.yaml in src/main/resources.
  4. Packaging: Run ./gradlew build to produce a distributable JAR file.
  5. Deployment: Drop the JAR into the plugins/ directory of your Halo installation.
  6. Verification: Restart Halo and monitor the logs to confirm the start() method was executed successfully.

Troubleshooting Common Issues

When a plugin fails to load, the first step should always be to set the Halo logging level to DEBUG. This provides visibility into the class-loading sequence and dependency resolution logic.

Common pitfalls include:

  • Version Mismatch: The requires field in plugin.yaml is too restrictive for the current Halo installation.
  • Missing Dependencies: A required plugin is either not installed or has been manually disabled.
  • Class Conflicts: While isolation protects the system, a plugin might fail if it incorrectly attempts to bundle core Halo classes instead of marking them as provided dependencies.
  • Disabled State: The plugin is present but the enabled flag in the manifest (or the admin console) is set to false.

By mastering these underlying mechanisms, developers can build highly integrated, scalable, and stable extensions that feel like a native part of the Halo ecosystem.