The descriptor
The descriptor is the generated object VersatiaDescriptor, placed in the package of the plugin's
main class so that the runtime can load it by name: for com.example.MyPlugin it is
com.example.VersatiaDescriptor. It implements VersatiaPluginDescriptor from
dev.versatia.engine.descriptor and builds its elements with the Direct and
Reflective implementations of Instantiator, Invoker and Setter. Those types are marked
@VersatiaInternalApi, so the file opts in with
@file:OptIn(VersatiaInternalApi::class) and compiles without warnings in any plugin.
The descriptor is a technical detail of the enhancement mechanism, not a feature. It records
facts about the annotated code (classes, supertypes, parameters, the annotations written on
them) and never what a feature makes of those facts: there is no "bean name" field, the DI
processor derives the name from the recorded @Qualifier attribute.
Shape
The descriptor is deliberately flat: a format version and a list of EnhancedElements.
Field of EnhancedElement | Meaning |
|---|---|
processorClass | the processor that interprets it, for example dev.versatia.di.LocalBeanEnhancementProcessor |
phase | the Phase at which it runs; see Phases and events |
annotation | the enhancement annotation that produced it, with all its arguments |
target | what was annotated: a ClassTarget, a FunctionTarget, a PropertyTarget or a PluginTarget |
origin | the class the annotation is physically written on: the target's own class, a base class of the main class for inherited enhancements, or the class carrying a subclass enhancement |
No iteration order, grouping or phase structure is baked into the file; the runtime decides how to walk it. Changing how VersatiaCore iterates therefore never requires regenerating plugins.
AnnotationOccurrence, Parameter and TypeReference
An AnnotationOccurrence is an annotation as written at one place: its class name and its
arguments, defaults included. Values are strings, booleans, numbers, chars, enum entry names,
class names and lists of those. A Parameter is a constructor or function parameter: its type,
the attributes on it, inherited ones included, optional
for a constructor parameter with a default value, and suppliedBy, the processor that supplies
its argument instead of injection (the @Value processor for a constructor property). A TypeReference is a type as
written on a property: a Kotlin qualified name, its type arguments and whether it is nullable,
kotlin.collections.List<kotlin.Int>?.
ClassTarget
A class whose instances the runtime may create.
| Field | Meaning |
|---|---|
className | Kotlin qualified name (com.example.Outer.Inner) |
assignableTypes | the class and every supertype except Any |
annotations | the attributes written on the class |
constructorParameters | one Parameter per constructor parameter, in order |
instantiator | how to create an instance from resolved arguments |
FunctionTarget
A function the runtime may call.
| Field | Meaning |
|---|---|
className | Kotlin qualified name of the declaring class |
binaryClassName | JVM name of the declaring class (com.example.Outer$Inner) |
functionName | the function |
owner | a MemberOwner: PLUGIN, OBJECT, INSTANCE or SUBCLASS_INSTANCE, who holds the instance to call it on; INSTANCE means the processor must obtain it (for @AutoInvoke, the bean of that class); SUBCLASS_INSTANCE, for @Value properties only, is the instance a subclass enhancement creates for a class that is no bean, such as a plain Listener |
annotations | the attributes written on the function |
parameters | one Parameter per parameter |
invoker | how to call it with a receiver and resolved arguments |
classAnnotations | the attributes written on the declaring class, for what applies to a whole class such as @ConditionalOnPlugin |
PropertyTarget
A mutable property the runtime may set.
| Field | Meaning |
|---|---|
className | Kotlin qualified name of the declaring class |
binaryClassName | JVM name of the declaring class |
propertyName | the property |
owner | a MemberOwner, as for a function |
type | the property's type as a TypeReference; what a processor reads for it |
annotations | the attributes written on the property |
setter | how to assign it on a receiver |
classAnnotations | the attributes written on the declaring class |
constructorParameter | for a property declared in the primary constructor of a bean or of a SUBCLASS_INSTANCE class, the index of that parameter: its value is passed to the constructor |
@Value produces these.
PluginTarget
The plugin's main class. It carries className, assignableTypes and annotations like a
ClassTarget, but no parameters and no instantiator: Bukkit creates the instance, the runtime
only hands it out. One element is produced per class enhancement written on the main class or on
one of its supertypes, all listed first; VersatiaPlugin contributes @PluginBean (the main
class as a bean), @ExtractResources (the plugin's resources) and two elements of
@RunBackgroundTasks (the plugin's coroutine scope, bound at start and stopped before the stop
functions). The same annotation, a subclass enhancement,
adds a ClassTarget per BackgroundTask class, or the PluginTarget when the main class is one.
Direct access versus reflection
Encapsulation is preserved: beans, constructors, functions and their parameter types may be
private. The generator picks per element:
-
Instantiator.Direct/Invoker.Directwhen the class, the member and every parameter type arepublicorinternal. The generated code contains the call itself:Instantiator.Direct { args -> ConsoleGreeter(args[0] as Clock) }Invoker.Direct { instance, args -> (instance as MyPlugin).start(args[0] as Clock) }Setter.Direct { instance, value -> (instance as Shop).tax = value as Percent }No reflection, no
setAccessible. -
Instantiator.Reflective/Invoker.Reflective/Setter.Reflectiveotherwise. They store JVM binary names and at runtime load the class without initialising it (Class.forName(name, false, loader)), find the matching declared constructor, method or property and make it accessible. A property with aprivate setis reflective too, however visible the property itself is. ASetter.Reflectivewith a third argument,"value", reads the property and stores into that member of what it holds, which is how avalholding aPreviousValueTrackingis filled; the direct form isSetter.Direct { _, value -> Limits.max.value = value as Int }. -
Instantiator.WithDefaultsfor a constructor with optional parameters, visible or not. It calls the constructor through Kotlin reflection (callBy), leaving out every argument given asDefaultArgument, so those parameters keep their defaults. -
A Kotlin
objectreached through a subclass enhancement is named directly (Instantiator.Direct { _ -> Console }) when visible and read from itsINSTANCEfield (Instantiator.ObjectInstance) otherwise.
A generated file
For
class AlphaPlugin : VersatiaPlugin()
interface Greeter
@ExposeSingleton
class Clock
@LocalBean
internal class ConsoleGreeter(val clock: Clock) : Greeter
@LocalBean
private class HiddenAudit(@Qualifier("consoleGreeter") val greeter: Greeter)
the processor emits (shortened):
@file:OptIn(VersatiaInternalApi::class)
object VersatiaDescriptor : VersatiaPluginDescriptor {
override val formatMajor: Int = 1
override val formatMinor: Int = 0
override val elements: List<EnhancedElement>
get() = listOf(
EnhancedElement(
processorClass = "dev.versatia.di.PluginBeanEnhancementProcessor",
phase = Phase.PLUGIN_RELOAD_OR_START,
annotation = AnnotationOccurrence("dev.versatia.di.PluginBean"),
target = PluginTarget(
className = "com.example.alpha.AlphaPlugin",
assignableTypes = listOf("com.example.alpha.AlphaPlugin", "dev.versatia.VersatiaPlugin", "org.bukkit.plugin.java.JavaPlugin", "org.bukkit.plugin.Plugin", /* ... */),
annotations = emptyList(),
),
),
EnhancedElement(
processorClass = "dev.versatia.resources.ExtractResourcesEnhancementProcessor",
phase = Phase.PLUGIN_RELOAD_OR_START,
annotation = AnnotationOccurrence("dev.versatia.resources.ExtractResources"),
target = PluginTarget(/* the same */),
),
EnhancedElement(
processorClass = "dev.versatia.task.PluginScopeEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.task.RunBackgroundTasks"),
target = PluginTarget(/* the same */),
),
EnhancedElement(
processorClass = "dev.versatia.task.PluginScopeEnhancementProcessor",
phase = Phase.PLUGIN_STOP,
annotation = AnnotationOccurrence("dev.versatia.task.RunBackgroundTasks"),
target = PluginTarget(/* the same */),
),
EnhancedElement(
processorClass = "dev.versatia.di.ExposeSingletonEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.di.ExposeSingleton"),
target = ClassTarget(
className = "com.example.alpha.Clock",
assignableTypes = listOf("com.example.alpha.Clock"),
annotations = emptyList(),
constructorParameters = emptyList(),
instantiator = Instantiator.Direct { _ -> Clock() },
),
),
EnhancedElement(
processorClass = "dev.versatia.di.LocalBeanEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.di.LocalBean"),
target = ClassTarget(
className = "com.example.alpha.ConsoleGreeter",
assignableTypes = listOf("com.example.alpha.ConsoleGreeter", "com.example.alpha.Greeter"),
annotations = emptyList(),
constructorParameters = listOf(Parameter("com.example.alpha.Clock")),
instantiator = Instantiator.Direct { args -> ConsoleGreeter(args[0] as Clock) },
),
),
EnhancedElement(
processorClass = "dev.versatia.di.LocalBeanEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.di.LocalBean"),
target = ClassTarget(
className = "com.example.alpha.HiddenAudit",
assignableTypes = listOf("com.example.alpha.HiddenAudit"),
annotations = emptyList(),
constructorParameters = listOf(Parameter("com.example.alpha.Greeter", listOf(AnnotationOccurrence("dev.versatia.di.Qualifier", mapOf("value" to "consoleGreeter"))))),
instantiator = Instantiator.Reflective("com.example.alpha.HiddenAudit", listOf("com.example.alpha.Greeter")),
),
),
)
}
Format version
The format has its own major.minor version, VersatiaPluginDescriptor.FORMAT_VERSION, currently
1.0. It is independent of the library version. A runtime accepts exactly the version it was
built with and rejects any other with a message naming the plugin, both versions and the fix; see
The runtime.
The generator writes the two numbers as literal Int properties (formatMajor, formatMinor), so
a runtime can read them from any descriptor, even one whose other members it does not understand,
before deciding what to do with it. For the same reason elements is a getter rather than an
initialised field: loading the descriptor class resolves nothing the elements refer to, so a
descriptor of another format still yields a clear version message instead of a
NoClassDefFoundError.