Java Plugin Implementation Approaches
ServiceLoader Mechanism
Java's ServiceLoader provides a standard SPI implementation. Define an interface with multiple implementations, then load them dynamically:
public interface NotificationService {
void sendAlert(String message);
}
public class EmailNotifier implements NotificationService {
@Override
public void sendAlert(String msg) {
System.out.println("Email notification: " + msg);
}
}
public class SmsNotifier implements NotificationService {
@Override
public void sendAlert(String msg) {
System.out.println("SMS notification: " + msg);
}
}
Create a service file at META-INF/services/com.example.NotificationService containning:
com.example.EmailNotifier
com.example.SmsNotifier
Load implementations:
ServiceLoader<NotificationService> services =
ServiceLoader.load(NotificationService.class);
services.forEach(service -> service.sendAlert("Test"));
Configuration-Based Plugin Loading
Define plugins in application properties:
plugins:
notification:
implementations:
- com.example.EmailNotifier
- com.example.SmsNotifier
Load dynamically:
@ConfigurationProperties("plugins.notification")
public class PluginConfig {
private List<String> implementations;
public List<NotificationService> loadServices() throws Exception {
List<NotificationService> services = new ArrayList<>();
for (String className : implementations) {
Class<?> clazz = Class.forName(className);
services.add((NotificationService) clazz.getDeclaredConstructor().newInstance());
}
return services;
}
}
Spring Boot Plugin Implemantation
Spring Factories Mechanism
Create META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.EmailAutoConfiguration,\
com.example.SmsAutoConfiguration
Auto-configuration classes:
@Configuration
public class EmailAutoConfiguration {
@Bean
public NotificationService emailNotifier() {
return new EmailNotifier();
}
}
Practical Implementation Example
- Define core interface:
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}
- Create implementations:
public class StripeProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
// Stripe implementation
}
}
- Load processors dynamically:
@Service
public class PaymentService {
private final Map<String, PaymentProcessor> processors;
public PaymentService(List<PaymentProcessor> processors) {
this.processors = processors.stream()
.collect(Collectors.toMap(
p -> p.getClass().getSimpleName(),
Function.identity()));
}
public PaymentResult process(String processorName, PaymentRequest request) {
PaymentProcessor processor = processors.get(processorName);
if (processor == null) {
throw new IllegalArgumentException("Unknown processor");
}
return processor.process(request);
}
}
- External JAR loading:
public class PluginLoader {
public void loadExternalPlugins(Path pluginDir) throws Exception {
Files.list(pluginDir)
.filter(path -> path.toString().endsWith(".jar"))
.forEach(this::loadJar);
}
private void loadJar(Path jarPath) {
try (URLClassLoader loader = new URLClassLoader(
new URL[]{jarPath.toUri().toURL()},
getClass().getClassLoader())) {
ServiceLoader<PaymentProcessor> services =
ServiceLoader.load(PaymentProcessor.class, loader);
services.stream().forEach(provider -> {
// Register discovered plugins
});
}
}
}