Extensions
When you start your application, Botrino automatically loads all the classes present in bot modules. According to the type of classes that are discovered, an action will be performed on them such as registering a service or adding a configuration entry. Extensions allow you to hook into this module scanning process to add your own logic when classes are loaded.
Declaring an extension
Unlike other components of the framework, extensions do not need to reside in a module annotated with @BotModule
.
Think of extensions like plugins for the framework itself and not for your bot application directly. Your module does
not need to be open
either, extensions are loaded
via java.util.ServiceLoader
.
The first step is to create a class implementing the BotrinoExtension
interface:
package com.example.myproject;
import botrino.api.extension.BotrinoExtension;
import com.github.alex1304.rdi.config.ServiceDescriptor;
import reactor.core.publisher.Mono;
import java.util.Set;
public final class MyExtension implements BotrinoExtension {
@Override
public void onClassDiscovered(Class<?> clazz) {
// ...
}
@Override
public void onServiceCreated(Object o) {
// ...
}
@Override
public Set<ServiceDescriptor> provideExtraServices() {
// ...
}
@Override
public Set<Class<?>> provideExtraDiscoverableClasses() {
// ...
}
@Override
public Mono<Void> finishAndJoin() {
// ...
}
}
Before going into the details of the methods to implement, let's register this class as a provider
for BotrinoExtension
. This is done via the module-info.java
:
import botrino.api.extension.BotrinoExtension;
import com.example.extension.MyExtension;
module com.example.extension {
requires botrino.api;
provides BotrinoExtension with MyExtension;
}
You don't have to create a separate module just for your extension. It is totally fine to add the provides
directive
directly in your @BotModule
, this example just shows that you are not required to.
Implementing an extension
Let's review each of the methods of BotrinoExtension
to implement.
void onClassDiscovered(Class<?> clazz)
This is a callback method invoked each time a class is discovered in a bot module. In most cases, you will check if this class implements a specific interface or is annotated with a specific annotation, and do some processing when it is relevant to do so.
If you intend to create an instance of the class, it is highly recommended to skip classes annotated with @RdiService
from this method, as they are supposed to be instantiated by the RDI container. That's why
the onServiceCreated(Object)
method exists.
void onServiceCreated(Object o)
This is a callback method invoked each time a service is created. It allows to execute some action on the service object right after it's created.
As this method returns void
, it is not suited for performing reactive tasks. Instead, store the service object in a
field and perform this task in finishAndJoin()
.
Set<ServiceDescriptor> provideExtraServices()
Even though the extension may not be inside a bot module, it is still possible to register services that will be exposed
to the bot application. You can do so via this method, allowing you to provide a set
of RDI service descriptors. This method is only useful if you
want to provide complex services that require writing raw descriptors (for example registering a class from a
third-party library as a service with a custom name). For simple services maintained by yourself, you can use RDI
annotations and make the annotated class discoverable via provideExtraDiscoverableClasses()
instead of doing it via
this method.
Set<Class<?>> provideExtraDiscoverableClasses()
With this method you can explicitly specify a set of classes that Botrino will pick up just like if they were inside a
bot module. It is guaranteed that each class contained in the set will eventually be passed to
the onClassDiscovered(Class)
method (unless they have the @Exclude
annotation). As said earlier, it can be used as
an alternative way to provide extra services, if the class contained in the set is annotated with RDI annotations. It
can also be used to register new configuration entries, or new things you're defining yourself in your own extension!
Mono<Void> finishAndJoin()
This is the last method that is invoked during the startup sequence. It allows you to perform a task, possibly reactive,
based on the classes and objects you were able to collect via previous invocations of onClassDiscovered(Class)
and onServiceCreated(Object)
. The "join" part of this method's name indicates the fact that the returned reactive
sequence does not need to be a finite source: you can use it to start processes living during the entire lifetime of the
application, for example installing event listeners or starting a web server. The subscription to the returned Mono
is
automatically cancelled once the bot disconnects, allowing the application to shut down properly.
If an exception is thrown or an error is emitted via the Mono
from this method, the exception will propagate to the
main thread, which will result in the bot to forcefully disconnect and the application to be terminated.
A concrete example: the interaction library
The interaction library of Botrino provides an implementation
of BotrinoExtension
, which is in charge of collecting the classes
implementing XxxInteractionListener
, InteractionErrorHandler
, InteractionEventProcessor
and so on, in order to
register them in the InteractionService
. It also exposes a new entry in config.json
that allows to construct
the configuration object.
You can check the source code of the extension class of the interaction library on GitHub here. A few things to note to understand the code:
- Classes with the
@RdiService
annotation are ignored, since we want to use the instance created by RDI in caseXxxInteractionListener
,InteractionErrorHandler
andInteractionEventProcessor
are declared as services. - An
InstanceCache
is used so that the same instance can be reused in case a class implements more than one interface. InteractionService
utilizes RDI annotations, so we provide it viaprovideExtraDiscoverableClasses()
and notprovideExtraServices()
.- All implementations that were found are finally registered in the
finishAndJoin()
method, which runs the interaction service at the end.