A demonstration project showcasing how to implement a plugin-based system using PF4J-Spring framework with dynamic plugin loading capabilities. This project implements a simple payment initiation API resource to illustrate plugin concepts.
This demo shows how to:
- Create a plugin-based architecture with Spring
- Dynamically load plugins at runtime
- Map plugins to business identifiers (bank IDs in this case)
pf4j-spring-payment-plugins/
├── bank-spi/ # Shared interfaces
├── first-bank-plugin/ # Implementation for Bank 1
├── second-bank-plugin/ # Implementation for Bank 2
└── api/ # Host application
import org.pf4j.ExtensionFactory;
import org.pf4j.spring.SingletonSpringExtensionFactory;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public SpringPluginManager springPluginManager(AppProperties appProperties) {
return new SpringPluginManager(Path.of(appProperties.getPluginsPath())) {
@Override
protected ExtensionFactory createExtensionFactory() {
return new SingletonSpringExtensionFactory(this);
}
};
}
}
@RestController
@RequestMapping("/api/plugins")
@RequiredArgsConstructor
public class PluginsController {
private final BankPluginManager bankPluginManager;
@PostMapping(value = "/{bankId}/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> addBankPlugin(@PathVariable String bankId,
@RequestParam("file") MultipartFile file) {
bankPluginManager.addBankPlugin(bankId, file);
return new ResponseEntity<>("Plugin added successfully", HttpStatus.CREATED);
}
}
package com.aziz.pf4jspringpaymentplugins.api.controller;
import com.aziz.pf4jspringpaymentplugins.api.plugin.BankPluginManager;
import com.aziz.pf4jspringpaymentplugins.bankspi.model.InitiatePaymentRequest;
import com.aziz.pf4jspringpaymentplugins.bankspi.model.InitiatePaymentResponse;
import com.aziz.pf4jspringpaymentplugins.bankspi.spi.PaymentInitiator;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/payments")
@RequiredArgsConstructor
public class PaymentController {
private final BankPluginManager bankPluginManager;
@PostMapping("/initiate/{bankId}")
public ResponseEntity<InitiatePaymentResponse> initiatePayment(@PathVariable String bankId,
@RequestBody InitiatePaymentRequest request) {
PaymentInitiator bankPaymentInitiator =
bankPluginManager.getBankPluginImplementation(bankId, PaymentInitiator.class);
InitiatePaymentResponse initiatePaymentResponse = bankPaymentInitiator.initiatePayment(request);
return new ResponseEntity<>(initiatePaymentResponse, HttpStatus.CREATED);
}
}
@Extension
@Component
@RequiredArgsConstructor
@Slf4j
public class PaymentInitiatorImpl implements PaymentInitiator {
private final PaymentInitiatorValidator paymentInitiatorValidator;
@Override
public InitiatePaymentResponse initiatePayment(InitiatePaymentRequest request) {
log.info("Validating payment request...");
paymentInitiatorValidator.validatePaymentInitiationRequest(request);
return InitiatePaymentResponse
.builder()
.paymentStatus(PaymentStatus.PENDING)
.txId(UUID.randomUUID().toString())
.build();
}
}
Any extension marked with @Extension
should be part of the target/classes/META-INF/extensions.idx
generated file by PF4J.
If annotation processing isn't working properly in your IDE, you can manually add it.
# Generated by PF4J
com.aziz.pf4jspringpaymentplugins.firstbankplugin.service.PaymentInitiatorImpl
mvn clean package
java -jar api/target/api-0.0.1-SNAPSHOT.jar
- go to http://localhost:8080/swagger-ui/index.html
- Upload either bank's plugin JAR from
[first|second]-bank-plugin/target/*.jar
and associate it with the<bank-id>
- Initiate a payment against
<bank-id>
.<bank-id>
plugin implementation will then be used.
A plugin module can share the application context with the host module. This can be done by setting the parent application context of the plugin module to that of the host.
if(wrapper.getPluginManager() instanceof SpringPluginManager springPluginManager) {
applicationContext.setParent(springPluginManager.getApplicationContext());
}