Spring integration example
Spring is a popular DI container in the java world, this tutorial demonstrates how the construction logic of spring can be combined with the dispatching logic of Fluxtion to simplify building event driven applications. The goal is to allow the developer to concentrate on developing application logic while the container automatically builds the object graph and constructs event dispatch logic.
Fluxtion is a dependency injection container specialised for event driven application deployments. The container exposes event consumer end-points, routing events as methods calls to beans within the running container. A bean registers a method as an event-handler by using Fluxtion annotations. Any beans referencing an event-handler bean will be triggered by the container as the internal dispatcher propagates an event notification through the object graph.
All methods on an interface can be exported by annotating the interface in an implementing bean, the container exports the interface methods as a single service. A client can look up an exported service by interface type using the container apis. All method calls on the service proxy are routed through the container’s internal dispatcher.
This example builds a small banking application, that supports credit, debit, account query, credit checking, opening hours and persistence functions. The methods are grouped into service interfaces that are exposed by the container.
The steps to combine spring and fluxtion:
- Create service interfaces that define the api of the banking app
- Create implementing classes for the service interfaces
- Create a spring config file declaring instances the DI container will manage
- Use Fluxtion annotations to export services and define event notification methods
- Pass the spring config file to the Fluxtion compiler and generate the DI container AOT
- Create an instance of the DI container and locate the service interfaces
- Use the service interfaces in the sample application
Application structure
See the example on GitHUb, top level package is com.fluxtion.example.cookbook.spring.service
.
Package structure:
- top level: Banking app and a sample main
- service: interfaces the sample main and banking app invoke
- node: implementations of the service interfaces
- data: data types used by services
- generation: location of the Fluxtion ahead of time generated DI container
Spring beans
Fluxtion provides support for building the DI container using spring configuration. The example uses a spring configuration file to declare the beans that will be managed by the Fluxtion DI container:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountBean" class="com.fluxtion.example.cookbook.spring.node.AccountNode">
<property name="responsePublisher" ref="responsePublisher" />
</bean>
<bean id="creditCheck" class="com.fluxtion.example.cookbook.spring.node.CreditCheckNode">
<property name="transactionSource" ref="accountBean"/>
<property name="responsePublisher" ref="responsePublisher" />
</bean>
<bean id="transactionStore" class="com.fluxtion.example.cookbook.spring.node.CentralTransactionProcessor">
<property name="transactionSource" ref="creditCheck"/>
<property name="responsePublisher" ref="responsePublisher" />
</bean>
<bean id="responsePublisher" class="com.fluxtion.example.cookbook.spring.node.ResponsePublisher">
</bean>
</beans>
Once the file is created the file location can be passed to Fluxtion to build the container:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("com/fluxtion/example/cookbook/spring/spring-account.xml");
eventProcessor = FluxtionSpring.interpret(context);
Invoking a service
The BankingApp instance creates an instance of the AOT generated Fluxtion DI container and provides access to container exported services. The service reference the client code receives is a proxy the DI container creates, the proxy handler routes method calls to instances managed by the container.
Services the DI container exposes are event driven, they are designed to be invoked asynchronously and do not return
application values to client code. A service method can optionally return a boolean value that is used by the container
as an event propagation flag. If the flag is true then child references are notified the parent has changed due to an
external event. Child instances are notified of event propagation by the container calling a trigger method. A trigger
method is any zero argument method marked with an OnTrigger
annotation. OnTrigger
methods return an event
propagation flag to control event notification dispatch in the same was as exported service methods.
The fluxtion DI container manages all the proxy creation, event dispatch to services, monitoring dirty flags and propagating event notifications to child references. To access an exported service client code calls:
T exportedService = eventProcessor.getExportedService();
Main method execution
The main method creates an instance of the BankingApp, rerieves service interfaces and invokes application methods on the interfaces. It is expected the BankingApp would be instantiated and used within a larger application that marshalls client requests from the network and then invokes the BankingApp appropiately.
public class Main {
public static void main(String[] args) {
BankingApp bankingApp = new BankingApp(GenerationStrategy.USE_AOT);
//get services
Account accountService = bankingApp.getBankAccount();
BankingOperations bankControllerService = bankingApp.getBankingOperations();
CreditCheck creditCheckService = bankingApp.getCreditCheck();
//persistence
FileDataStore fileDataStore = new FileDataStore(Paths.get("data/spring/bank"));
bankControllerService.setDataStore(fileDataStore);
//replay state
fileDataStore.replay(bankingApp.getEventConsumer());
bankingApp.start();
//should reject unknown account
accountService.deposit(999, 250.12);
//get opening balance for acc 100
accountService.publishBalance(100);
//should reject bank closed
accountService.openAccount(100);
accountService.deposit(100, 250.12);
//open bank
bankControllerService.openForBusiness();
accountService.deposit(100, 250.12);
//blacklist an account
creditCheckService.blackListAccount(100);
accountService.deposit(100, 46.90);
//remove account from blacklist
creditCheckService.whiteListAccount(100);
accountService.deposit(100, 46.90);
//close bank
bankControllerService.closedForBusiness();
accountService.deposit(100, 13);
}
}
running the main method prints the following to the console:
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - deposit request:Transaction[accountNumber=999, amount=250.12, debit=false]
[INFO] AccountNode - reject unknown account:999
[INFO] ResponsePublisher - response reject:Transaction[accountNumber=999, amount=250.12, debit=false]
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - ------------------------------------------------------
[INFO] ResponsePublisher - account:100, balance:3267.22
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - opened account:100
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - deposit request:Transaction[accountNumber=100, amount=250.12, debit=false]
[INFO] CreditCheckNode - credit check passed
[WARN] CentralTransactionProcessor - reject bank closed
[INFO] ResponsePublisher - response reject:Transaction[accountNumber=100, amount=250.12, debit=false]
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
[INFO] CentralTransactionProcessor - open accepting transactions
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - deposit request:Transaction[accountNumber=100, amount=250.12, debit=false]
[INFO] CreditCheckNode - credit check passed
[INFO] CentralTransactionProcessor - accept bank open
[INFO] AccountNode - updated balance:3517.3399999999997 account:100
[INFO] ResponsePublisher - response accept:Transaction[accountNumber=100, amount=250.12, debit=false]
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
[INFO] CreditCheckNode - credit check blacklisted:100
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - deposit request:Transaction[accountNumber=100, amount=46.9, debit=false]
[WARN] CreditCheckNode - credit check failed
[INFO] ResponsePublisher - response reject:Transaction[accountNumber=100, amount=46.9, debit=false]
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
[INFO] CreditCheckNode - credit check whitelisted:100
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - deposit request:Transaction[accountNumber=100, amount=46.9, debit=false]
[INFO] CreditCheckNode - credit check passed
[INFO] CentralTransactionProcessor - accept bank open
[INFO] AccountNode - updated balance:3564.24 account:100
[INFO] ResponsePublisher - response accept:Transaction[accountNumber=100, amount=46.9, debit=false]
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
[WARN] CentralTransactionProcessor - closed rejecting all transactions
[INFO] AccountNode - ------------------------------------------------------
[INFO] AccountNode - deposit request:Transaction[accountNumber=100, amount=13.0, debit=false]
[INFO] CreditCheckNode - credit check passed
[WARN] CentralTransactionProcessor - reject bank closed
[INFO] ResponsePublisher - response reject:Transaction[accountNumber=100, amount=13.0, debit=false]
[INFO] AccountNode - request complete
[INFO] AccountNode - ------------------------------------------------------
Process finished with exit code 0
Exporting a service
To export a service the following steps are required:
- Create an interface and then implement the interface with a concrete class
- The implementation class must extend
ExportFunctionNode
- Mark the interface to export with
@ExportService
annotation
For example to export the CreditCheck service:
CreditCheck interface
public interface CreditCheck {
void blackListAccount(int accountNumber);
void whiteListAccount(int accountNumber);
}
CreditCheckNode concrete class
The CreditCheckNode implements two interfaces CreditCheck and TransactionProcessor. Only the CreditCheck interface
methods are exported as this is only interface marked with @ExportService
public class CreditCheckNode extends ExportFunctionNode implements @ExportService CreditCheck, TransactionProcessor {
private transient Set<Integer> blackListedAccounts = new HashSet<>();
private TransactionProcessor transactionSource;
private ResponsePublisher responsePublisher;
@Override
@NoPropagateFunction
public void blackListAccount(int accountNumber) {
log.[INFO]("credit check blacklisted:{}", accountNumber);
blackListedAccounts.add(accountNumber);
}
@Override
@NoPropagateFunction
public void whiteListAccount(int accountNumber) {
log.[INFO]("credit check whitelisted:{}", accountNumber);
blackListedAccounts.remove(accountNumber);
}
public boolean propagateParentNotification(){
Transaction transaction = transactionSource.currentTransactionRequest();
int accountNumber = transaction.accountNumber();
if(blackListedAccounts.contains(accountNumber)){
log.[WARN]("credit check failed");
transactionSource.rollbackTransaction();
responsePublisher.rejectTransaction(transaction);
return false;
}
log.[INFO]("credit check passed");
return true;
}
@Override
public Transaction currentTransactionRequest() {
return transactionSource.currentTransactionRequest();
}
@Override
public void rollbackTransaction() {
transactionSource.rollbackTransaction();
}
@Override
public void commitTransaction(){
transactionSource.commitTransaction();
}
}
Notice the two CreditCheck methods are annotated with @NoPropagateFunction
, telling Fluxtion that no event
propagation will occur when either of these methods is invoked. The credit black list is a map and these methods should
only change the state of the internal map and not cause further processing to occur in the object graph.
Locating a service
The steps required to locate a service and invoke methods on it are:
- Build the DI container using one of the Fluxtion build methods
- To correctly intitialise the container call
eventProcessor.init()
on the DI instance - To access the service call
T service = eventProcessor.getExportedService()
with the desired service type T
Accessing CreditCheck service
The code below uses an enum to allow the user to select the DI generation strategy, in this example we are using the AOT strategy. After eventprocessor generation the exported service are located and assigned to member variables in the BankingApp class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BankingApp {
private final EventProcessor<?> eventProcessor;
private final Account bankAccount;
private final CreditCheck creditCheck;
private final BankingOperations bankingOperations;
private final Consumer eventConsumer;
@SneakyThrows
public BankingApp(GenerationStrategy generationStrategy) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("com/fluxtion/example/cookbook/spring/spring-account.xml");
eventProcessor = switch (generationStrategy) {
case USE_AOT -> new SpringBankEventProcessor();
case INTERPRET -> FluxtionSpring.interpret(context);
case COMPILE -> FluxtionSpring.compile(context);
case GENERATE_AOT -> FluxtionSpring.compileAot(context, c -> {
c.setPackageName("com.fluxtion.example.cookbook.spring.generated");
c.setClassName("SpringBankEventProcessor");
});
};
eventProcessor.init();
bankAccount = eventProcessor.getExportedService();
creditCheck = eventProcessor.getExportedService();
bankingOperations = eventProcessor.getExportedService();
eventConsumer = eventProcessor::onEvent;
}
public CreditCheck getCreditCheck() {
return creditCheck;
}
}
line 22-24 acquire the exported services from the container by interface type