𧲠Quick navigation
Callbacks is an annotation-based Java library for managing event flows. It works by generating callback classes (source files) at build time which can be intercepted with custom handlers and listeners.
- Handlers provide access to the callback's object so that it can be handled, hence the name.
- Listeners give access to the object's parameters, which are mirrored from the callback's constructor and passed to the listener when a callback is handled. Listeners cannot modify the object at all, they just listen to the result.
<repositories>
<repository>
<id>myth-mc-releases</id>
<name>myth-MC Repository</name>
<url>https://repo.mythmc.ovh/releases</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>ovh.mythmc</groupId>
<artifactId>callbacks-lib</artifactId>
<version>$latest$</version>
<scope>compile/<scope>
</dependency>
</dependencies>
repositories {
maven {
name = "mythmc"
url = uri("https://repo.mythmc.ovh/releases")
}
}
dependencies {
implementation("ovh.mythmc:callbacks-lib:$latest$")
}
repositories {
maven {
name = 'mythmc'
url = 'https://repo.mythmc.ovh/releases'
}
}
dependencies {
compileOnly 'ovh.mythmc:callbacks-lib:$latest$'
}
Generating a callback class can be as simple as putting a single @Callback
annotation above your class' definition. For example:
@Callback
public class Example {
public final String exampleField;
public Example(String exampleField) {
this.exampleField = exampleField;
}
}
This will generate a class named ExampleCallback
in the same package as the source class.
Classes with private fields and public getters require some additional configuration by using the @CallbackFieldGetter
annotation:
@Callback
@CallbackFieldGetter(field = "exampleField", getter = "getExampleField()")
public class Example {
private final String exampleField;
public Example(String exampleField) {
this.exampleField = exampleField;
}
public String getExampleField() {
return exampleField;
}
}
Tip
The @CallbackFieldGetter
annotation is marked as repeteable, which means that you can use it as many times as you need.
You can also group these annotations with @CallbackFieldGetters
:
@CallbackFieldGetters({
@CallbackFieldGetter(field = "exampleField", getter = "getExampleField()"),
@CallbackFieldGetter(field = "exampleField2", getter = "getExampleField2()")
})
When extending a @Callback
annotated class, all of its callback field getters will be inherited from the super class.
This means that we can easily extend our Example
class with even more fields:
@Callback
@CallbackFieldGetter(field = "newField", getter = "getNewField()")
public class ExtendExample extends Example {
private final String newField;
public ExtendExample(String exampleField, String newField) {
super(exampleField);
this.newField = newField;
}
public String getNewField() {
return newField;
}
}
Warning
The @Callback
annotation is not inherited.
You may also find cases where you might want to use a specific constructor from your class. This can be done too by using the constructor
property within the @Callback
annotation:
@Callback(constructor = 2)
public class ConstructorExample {
public String field;
ConstructorExample() { // 1
}
public ConstructorExample(String field) { // 2
this.field = field;
}
}
Record classes are also supported:
@Callback
public record ExampleRecord(String field) {
}
Handlers and listeners allow us to intercept callbacks and handle them accordingly.
Handlers provide access to the callback's object so that it can be handled, hence the name. Let's register one in our ConstructorExampleCallback
and modify the value of field
:
var callbackInstance = ConstructorExampleCallback.INSTANCE; // Singleton instance of our callback
var handlerIdentifier = IdentifierKey.of("group", "identifier"); // Unique identifier of our handler. You can also use a namespaced string ("group:identifier")
callbackInstance.registerHandler(handlerIdentifier, constructorExample -> {
constructorExample.field = "Hello world!";
});
Listeners give access to the object's parameters, which are mirrored from the callback's constructor and passed to the listener when a callback has been handled. Listeners cannot modify the object at all, they just listen to the result. Let's register one in our 'ConstructorExampleCallback' and listen to the result:
var callbackInstance = ConstructorExampleCallback.INSTANCE; // Singleton instance of our callback
var listenerIdentifier = IdentifierKey.of("group", "identifier"); // Unique identifier of our listener. You can also use a namespaced string ("group:identifier")
callbackInstance.registerListener(listenerIdentifier, field -> {
System.out.println(field);
});
If the callback object uses generic types, we'll need to specify the types we're expecting while registering handlers or listeners. For example:
callbackInstance.registerListener(listenerIdentifier, field -> {
System.out.println(field);
}, String.class);
Once we have our handlers and listeners registered, we can invoke the callback:
var callbackInstance = ConstructorExampleCallback.INSTANCE; // Singleton instance of our callback
callbackInstance.invoke(new ConstructorExample(""), result -> { // We can get the result of the callback (which will be a ConstructorExample as well)
System.out.println("Callback result: " + result.toString());
});
callbackInstance.invoke(new ConstructorExample("")); // Or we can ignore it
Most of the information on this README comes from our blog post "Callbacks: a modern approach to events".
These are some projects we've used as inspiration for the library:
- Using the Event API by Spigot
- Annotation Processing 101 by Hannes Dorfmann: great post on Java annotation processing with detailed explanations and lots of examples
- JavaPoet by palantir: this is actually the library we're usimg to generate callback classes as source files
- Lifecycle API by PaperMC
- Working With Events by PaperMC
- Events by SpongePowered
- Events by MinecraftForge