pluginloader is a Spring Boot starter that lets a host application pull feature code from other Git projects at build time and register the feature beans at runtime.
- Build time (annotation processor): When the host runs
mvn compile,PluginLoaderProcessorreadspluginloader.featuresfromapplication.yml, clones each configured repository/branch, imports.javasources into generated sources so they compile with the host, scans for packages and bean classes, optionally builds the feature jar for reference, and emits a descriptor class undergenerated.<FeatureName>Descriptorthat lists packages and bean classes. - Runtime (auto-configuration): With
pluginloader.enabled=true,PluginloaderAutoConfigurationrunsFeatureManager, which loads the generated descriptors (or falls back to configured packages) and registers the discovered beans into the host context using the host classloader, no runtime cloning or building.
- Dependency:
<dependency> <groupId>com.aajumaharjan</groupId> <artifactId>pluginloader</artifactId> <version>0.0.28-SNAPSHOT</version> </dependency>
- Annotation processor on the compile plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>com.aajumaharjan</groupId> <artifactId>pluginloader</artifactId> <version>0.0.28-SNAPSHOT</version> </path> <!-- your other processors, e.g., Lombok --> </annotationProcessorPaths> </configuration> </plugin>
In the host src/main/resources/application.yml:
pluginloader:
enabled: true
features:
- repository: https://github.com/your-org/feature-repo.git # or file:///... for local
branch: main # optional, defaults to main
packages: # optional filters; if omitted, all packages are considered
- com.yourorg.feature.service
- com.yourorg.feature.auth- Run
mvn compilein the host app. The processor clones the repositories, imports sources, and generates descriptors undertarget/generated-sources/annotations. - Start the host app. Auto-configuration creates a child context for each feature and registers beans (by package scan or explicit bean class names from descriptors) into the host context so they can be autowired.
This example shows:
- the host enabling a feature via
application.yml, - the feature reading configuration provided by the host, and
- the host autowiring and using a bean contributed by the feature.
pluginloader:
enabled: true
features:
- repository: https://github.com/your-org/greeting-feature.git
branch: main
packages:
- com.yourorg.greeting
# Host-owned configuration that the feature will read
greeting:
message: "Hello from host config!"
enabled: truepackage com.yourorg.greeting;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "greeting")
public class GreetingProperties {
private String message = "Hello (default)";
private boolean enabled = true;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}package com.yourorg.greeting;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
private final GreetingProperties props;
public GreetingService(GreetingProperties props) {
this.props = props;
}
public String greet(String name) {
if (!props.isEnabled()) {
return "Greeting feature is disabled";
}
return props.getMessage() + " Name=" + name;
}
}This ensures the @ConfigurationProperties is registered when the feature is loaded.
package com.yourorg.greeting;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(GreetingProperties.class)
public class GreetingFeatureConfiguration { }If your feature already relies on component scanning and the configuration class is in a scanned package, this will be picked up when the feature is registered.
package com.yourorg.host;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HostApplication {
public static void main(String[] args) {
SpringApplication.run(HostApplication.class, args);
}
}package com.yourorg.host;
import com.yourorg.greeting.GreetingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private final GreetingService greetingService;
public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GetMapping("/greet")
public String greet(@RequestParam(defaultValue = "Aaju") String name) {
return greetingService.greet(name);
}
}-
In the host app:
mvn compile
This clones
greeting-feature, imports sources, and generates descriptors. -
Start the host:
mvn spring-boot:run
-
Call the endpoint:
-
GET /greet?name=Bob -
Response:
Hello from host config! Name=Bob
-
- Requires JDK 17+ and Maven available on PATH during compilation.
- Repositories must be reachable from the build machine; use
file:///...URLs to work offline. - The generated descriptor includes a JAR_PATH field for reference; current runtime registration uses host-visible classes and the packages/bean-class lists.
- Enable debug logging (
logging.level.com.aajumaharjan.pluginloader=DEBUG) to see processor and runtime integration details.
- Purpose: provide a configuration-driven way to compose modular features from separate repositories into a single Spring Boot host, without manual code wiring.
- Problem addressed: conventional component scanning works well within one codebase but does not help when features live in other repos; aligning dependencies, classpaths, and bean registration across repo boundaries is otherwise manual and error-prone.
- Convention-over-configuration limits: once features are split across repos, relying on package scanning alone is insufficient, discovery must know where code resides and which parts to include. pluginloader externalises that selection to configuration.
- Configuration-driven rationale: feature selection (which repos/branches/packages) is declared in
application.yml, making inclusion explicit, reviewable, and environment-specific without code changes. - Build-time source integration rationale: the annotation processor clones repos and imports sources during compilation so classes are compiled with the host, using the host classloader at runtime. This avoids runtime classloader complexity while keeping the dependency surface explicit.
- Clarity and control: descriptors list packages and bean classes; runtime uses only host-visible classes. This improves predictability, makes failures earlier (compile-time), and keeps operational behaviour transparent.
- Not a hot-reload system: pluginloader does not hot-swap code, is not an OSGi alternative, and is not a microservices framework; it is a build-time integration plus runtime auto-configuration mechanism for modular features.