Tiny, lightweight java command bus.
The main idea is to route command to corresponding handler, but there is something more. The interesting part lies in three components, that allow simple yet powerful and flexible configuration.
First and most important component is CommandHandler. CommandHandler is responsible for handling specific Command and there is one-to-one mapping. Which means, that there should be exactly oneCommandHandler, otherwise exception is thrown.
Simplest CommandHandler would be:
publicclassMyCommand {
}
publicclassCommandHandler {
@HandlerpublicIntegerhandle(MyCommandcmd) {
// Do something...return5;
}
}First and most important thing is @Handler annotation, which marks method as a command handler. First parameter is always Command to be handled and any @Handler method should have at least 1 parameter.
That means, that when MyCommand will be executed, this exact method will handle it.
MyCommand is just a POJO, which means it could be anything, there is no special requirements for Command to be command. Typically it just contains some data.
CommandHandler may or may not return some result. In this case it returns Integer, but it also could be void.
It is also possible to have more than one @Handler method in one class. In that case, two CommandHandlers would be registered:
publicclassCommandHandler {
@HandlerpublicIntegerhandle(MyCommandcmd) {
return5;
}
@Handlerpublicvoidhandle2(MyCommand2cmd) {
// Do something...
}
}What makes it interesting, is that CommandHandler can have additional parameters:
publicclassCommandHandler {
@Handlerpublicvoidhandle(MyCommandcmd, ClientcurrentClient) {
// Do something...
}
}In that case current client will be provided by Value provider.
Value provider is very similar to CommandHandler in terms of definition.
publicclassValueProvider {
@ProviderpublicIntegera() {
return5;
}
}@Provider annotation marks method as Value provider. But there are few important differences.
@Provider method should have return type different from void.
@Provider method name is also important, because it is possible to have multiple @Provider that returns the same type, in that case name must be different, otherwise exception is thrown. i.e.:
publicclassCommandHandler {
@Handlerpublicvoidhandle(MyCommandcmd, Integera, Integerb) {
// Do something...
}
publicstaticclassValueProvider {
@ProviderpublicIntegera() {
return5;
}
@ProviderpublicIntegerb() {
return6;
}
}
}In that case, CommandHandlerMyCommand and two Integers. In order to resolve it correctly, @Provider method name is used.
Middleware is used to provide pre/post processing, command/result transformation or to simply ignore command.
publicclassTimerMiddlewareimplementsMiddleware {
@Overridepublic <R> Rexecute(Objectcommand, Function<Object, R> next) {
Timer.start();
Rresult = next.apply(command);
Timer.stop();
returnresult;
}
}Measuring execution time would be one of common tasks for middleware. It could also be:
- starting transactional context;
- logging;
- etc
To make it all happen Bus should be built. In order to do that BusBuilder is used.
It is important in what order Middleware is registered
publicclassM1implementsMiddleware {
@Overridepublic <R> Rexecute(Objectcommand, Function<Object, R> next) {
System.out.println("1");
Rres = next.apply(command);
System.out.println("2");
returnres;
}
}
publicclassM2implementsMiddleware {
public <R> Rexecute(Objectcommand, Function<Object, R> next) {
System.out.println("3");
Rres = next.apply(command);
System.out.println("4");
returnres;
}
}
Busbus = newBusBuilder()
.registerCommandHandler(newCommandHandler())
.registerCommandHandler(newCommandHandler2())
.registerValueProvider(newValueProvider())
.registerMiddleware(newM1())
.registerMiddleware(newM2())
.build();
Objectres = bus.execute(newMyCommand());
System.out.println("Wohoo! Result of the command is " + res);In that case it the output would be following: 1->3->4->2
Bus is the actual worker here. It has only one method - execute(Object), which accepts Command. It will then synchronously execute this Command which means it will first execute Middleware chain, then find appropriate CommandHandler, if CommandHandler has additional params it will resolve them with the help of ValueProvider and finally result will be returned.
Since Spring Framework is so popular, to make CommandHandler and ValueProvider registration easier - first add io.github.fdside.commandbus.spring dependency and then create such configuration.
@ConfigurationpublicclassBusConfig {
@BeanBuscommandBus(ApplicationContextcontext) {
returnnewSpringBusBuilder(context)
.registerCommandHandlers("your.base.package.with.command.handlers")
.registerCommandHandlers("your.base.package2.with.command.handlers")
.registerValueProviders("your.base.package.with.value.providers")
.registerValueProviders("your.base.package2.with.value.providers")
.registerMiddleware(newTMiddleware())
.build();In that case registerCommandHandlers() and registerValueProviders() accepts String which is base package. It is scanned and all @Handler and @Provider are automatically registered.