4.0.0-RC6 - Blizzard
Pre-releaseUpdate 4.0.0-RC6 - Blizzard
We are pleased to announce the sixth release candidate for CloudNet 4.0. This release mainly focuses on the migration from static instance usages to dependency injection - internally as well as for external api consumers. Please see the dedicated information section for dependency injection below for further info. We urge all users to install the update, as we will no longer provide support for RC5. Users who want to switch from 3.4.X to 4.0 can find instructions in the release information for RC1.
Cheers!
(Please remember, CloudNet is provided as-is - we are not responsible for data loss or corruption. You are encouraged to back up your files before any updates!)
Changelog
🐛 Fixes
- Fixed an issue that SyncProxy configurations which were applied to velocity proxy instances could not be applied to Bungeecord or Waterdog proxies as-is
- Fixed an issue that caused wildcard characters to not be escaped correctly when using the mysql database
- Fixed an issue that caused an exception when the profile which should be assigned to a npc cannot be resolved
- Fixed an issue with invalid npc info items which were causing exceptions
- Fixed an issue which caused a player to be connected to all matching services when calling any
PlayerExecutor#connect
method rather than the best matching service - Fixed an issue that caused a start loop when using the smart module with auto stop enabled due to an invalid check if enough services are running to satisfy the min service count
- Fixed an exception thrown when a channel message query got no response
- Fixed an issue that caused expired permissions to not be removed from a player
- Fixed an issue with SyncProxy permission tablist placeholders that were overridden by equivalently named bridge placeholders. The new placeholders are now prefixed with
perms_group_
✨ Improvements
- Add support for 1.19.3
- Migrated from the usage of static instances to dependency injection
- Improved accessibility to information in
CommandPreProcess
andCommandPostProcess
event - Renamed the
ColouredLogFormatter
toColoredLogFormatter
andConsoleColor.toColouredString
toConsoleColor.toColoredString
to keep the spelling of color consistent
🔗 Links
- Support Discord: https://discord.cloudnetservice.eu/
- Javadocs: https://cloudnetservice.eu/cloudnet/docs/4.0.0-RC6/
- Maven Dependencies: https://search.maven.org/search?q=eu.cloudnetservice.cloudnet
- If you found any issue, please report it on our issue tracker: https://github.com/CloudNetService/CloudNet-v3/issues
Dependency Injection
❓ Why DI
There are a few reasons why we deided to go for dependency injection. The top three include:
- Improved testability: DI allows for more easily replacing dependencies with mock objects in unit tests, which can help to improve the quality of the code and reduce bugs.
- Increased flexibility: DI makes it easier to change the implementation of a dependency without affecting the code that uses it. This allows for more easily making changes and adapting the codebase to new requirements.
- Loose coupling: DI allows for a more loosely-coupled architecture, which makes it easier to change or extend the codebase. This can lead to more maintainable code and also facilitates reusability and scalability of the code.
💡 What to note when using DI
What is DI?
Dependency injection is a design pattern that allows objects to be constructed without needing to know how their dependencies are created. A dependency is an object that another object relies on in order to function properly. In traditional software development, objects are responsible for creating and managing their own dependencies. However, with dependency injection, the responsibility of creating and managing dependencies is shifted to a separate component, known as an injector.
The injector is responsible for providing the necessary dependencies to objects at runtime. This is done by injecting the dependencies directly into the objects that need them, rather than having the objects create or manage the dependencies themselves. By doing so, the objects become less tightly coupled and more flexible, making it easier to change or replace dependencies without affecting the rest of the application.
Constructor injection is the most common form of dependency injection, where dependencies are passed in through the constructor of a class. The class is defined with a constructor that takes the dependencies as arguments, and the injector is responsible for providing these dependencies when the class is instantiated. The target constructor to inject is detected by using either of:
- The constructor which is annotated as
@Inject
(⚠️ Make sure to use the correct inject annotation. It should either bedev.derklaro.aerogel.Inject
orjakarta.inject.Inject
). - The constructor which takes no arguments.
- The constructor which takes all record components as its arguments (only applied for record classes).
When the injector constructed the type completely member injection is executed. Member injection will be done on
- all fields that are annoted as
@Inject
(Note that final fields might get ignored from injection) - all methods that are annotated as
@Inject
will be executed. These methods are allowed to take arguments. - all methods that are annotated as
@PostConstruct
will be executed. These methods are not allowed to take arguments.
The injection order of injectable methods and post construct listeners can be constrolled with the @Order
annotation.
A quickstart guide for dependency injection
- Identify dependencies: Identify the dependencies that need to be injected into an object and create interfaces or abstract classes for them.
- Use constructor injection: Constructor injection is the most common form of dependency injection, where dependencies are passed in through the constructor of a class.
- Avoid tight coupling: Dependency injection allows for loose coupling between objects, making it easier to change or replace dependencies.
- Use interfaces: Use interfaces or abstract classes to define dependencies, rather than concrete implementations, to make it easier to change or replace them.
- Understand the difference between singleton and prototype scope: The singleton scope will give a single instance of an object throughout the application, whereas the prototype scope will give a new instance of an object every time it is requested.
- Use annotations: Annotations are used to mark a class, constructor or a method, in order to let the framework know that it should be injected and how. For example the
@Inject
annotation markes constructors and classes that should be injected and the@Singleton
annotation lets aerogel know that only a single instance of the annotated type should be constructed. - Avoid circular dependencies: Be careful when defining dependencies to avoid circular dependencies, which can cause issues with object creation and injection. This only applies if the requested type is not an interface and therefore not proxyable.
⛅ How did we implement it in CloudNet
Because of the little flexibility and many outside influences we have due to the supported Minecraft server/proxy implementation, we had to implement DI to be as flexible as possible.
For this reason, some decisions had to be made that would not have been a problem in a normal standalone application:
- We use Aerogel as our DI library. This decision was made for the most part because:
- Bi-directional proxies are supported, unlike (for example) Google Guice. This means that if a circular injection request arises, and (e.g.) the parameter of a constructor cannot be proxied, then the type from which the circular reference arises is tried to be proxied. See the Aerogel documentation for more information on how the injection tree works.
- The library supports "specified" injectors. These are injectors that have certain types bound to them that do not get into the parent injector (e.g. useful for plugins: the description of a plugin should not be passed to the other plugin instances).
- The library supports automated binding of classes to interfaces and factory methods. More information about this can be found below.
- The complete creation of plugin information and main classes is now abstracted. This allows us to inject dependencies directly into the main classes of plugins without having to make an intermediate step (e.g. through another extra class).
- More annotations have been added to extend the previously mentioned Binding System for Platforms. Thus, multiple implementations for (e.g.) an interface can be created, which are bound (and later injected) to the interface based on the platform. More information about this is also available below.
🔌 Support for Plugins
Support for plugins can be achieved with the platform inject api. This generates classes during compile time based on annotations added to classes (a list of these annotations is below). It is important that the dependency for the annotation processor is only available at compile time and not at runtime:
// adds the api for platform inject, do not compile!
compileOnly 'eu.cloudnetservice.cloudnet:platform-inject-api:4.0.0-RC6'
// adds the annotation processor for the inject-api annotations
annotationProcessor 'eu.cloudnetservice.cloudnet:platform-inject-processor:4.0.0-RC6'
The supported annotations
@PlatformPlugin
: This annotates the main class of the plugin. Due to the abstraction of CloudNet extended/implemented, the annotated class no longer implements an implementation-specific class (such as JavaPlugin) but always implements the interface PlatformEntrypoint. The following settings can be made via the annotation:platform
: The name of the platform for which the annotated class is the entry point. Supported are currently:bukkit
,bungeecord
,fabric
,minestom
,nukkit
,sponge
,velocity
&waterdog
. Note that for the platform a plugin info file will be generated as well (for example theplugin.yml
for bukkit). A template file can be created in the resources directory with the name of the output file (see below for options) suffixed with.template
. All configurations made in the template file will be copied to the final file and will not get overridden.name
: The name of the plugin. CloudNet automatically takes care that the name complies with the platform's policies (and adjusts the name if necessary) and also generates a plugin ID based on the name (if necessary).version
: The version of the plugin.pluginFileNames
: Sets the output file name for the plugin. By default, the file names used by CloudNet are used, which can be customized during copying to the target service (e.g. plugin.bungeecord.yml). In the array you can specify the names of the files where the plugin info for the platform should be written. Folders are also supported, these are simply specified via slash (e.g.myFolder/plugin.yml
).api
: An optional api version that is required for the plugin to run. This setting can be interpreted differently or even ignored based on the target platform.description
: An optional description of the plugin.homepage
: An optional url to the homepage of the plugin.authors
: An optional array of the plugin authors.providesScan
: An optional set of patterns which should be considered when searching for@ProvidesFor
annotations. By default the same package in which the plugin main class is located in will be used. Each entry in the given array can be in the formts:<packageName>
: In this case the given name will be taken and all classes and sub-packages in the given package will be scanned<type>:<packageName>
: The name is used depending on the given type. The type can either be:r
,regexp
,pattern
: the given package name is parsed as a pattern.p
,plain
: the given package name is used in a literal way and all classes in the given and sub packages will be scanned.g
,glob
: the given package name is parsed as a simplified glob. The only supported glob chars are:*
,?
,.
and\
.
commands
: An optional array of commands that is provided by the plugin. Depending on the platform the information can be used in different ways or even be igored.dependencies
: An optional array of plugin-level dependencies. The given information may not be used based on the plugin.externalDependencies
: An optional array of external dependencies that are required for the plugin to run. Based on the platform the information might be ignored.
@ProvidesFor
: marks an implementation for the given type(s) which should only be present on the specified platform. The following settings can be made via the annotation:platform
: The platform on which the binding should be present.types
: The types to which the annotated class should get bound (Note that there will be no check if the type is actually assignable to the annotated type).bindGenericSupertypes
: If enabled the directly extended superclass and implemented interfaces will be scanned if they contain one of the types given via thetypes
setting which is generic. In that case the generic version will be bound based on the extending declaration (for example if in the type arrayMap.class
is specified and the class implementsMap<String, String>
then the map implementation with type parameters will be bound to the annotated type as well).bindWildcardTypeOfParameterizedTypes
: If enabled all generic, parameterized types will be bound fully wildcarded as well (for example if the class implementsMap<String, Map<String, String>>
the typeMap<?, ?>
will be bound to the type as well).
@ConstructionListener
: The annotation can be added to the main class of a plugin (the same class as annotated with@PlatformPlugin
) and specifies a single class will should get called when the plugin instance is created by the platform. Note that the target class must declare a constructor which takes the platform plugin data (which can vary based on the platform) as it's only argument. Only the constructor in the target class is called, all other steps are left to the constructor to do.
Annotations to use with @PlatformPlugin
The following annotations are used in the PlatformPlugin annotation and describe various configuration options for the info generation for each platform. Depending on the target platform, some of the information provided to the annotation might get dropped, or the complete annotation is ignored if unused:
@Command
: Allows to specify a command which is provided by the plugin.@Dependency
: Allows to specify a dependency on another plugin.@ExternalDependency
: Allows to specify an external dependency which is required for the plugin in order to run.@Repository
: Sets the repository of the external dependency. Some platform are only allowing dependencies to get downloaded from maven central, in that case the information provided to the annotation is ignored.
An example plugin class might look like this:
@Singleton
@PlatformPlugin(
platform = "velocity",
name = "TestingPlugin",
version = "1.0",
authors = "derklaro",
description = "Just a plugin to test some things",
dependencies = @Dependency(name = "CloudNet-Bridge")
)
public final class VelocityPluginEntryPoint implements PlatformEntrypoint {
@Inject
public VelocityPluginEntryPoint(
@NonNull @Named("plugin") Object pluginInstance, // the type of this varies based on the platform
@NonNull ModuleHelper moduleHelper,
@NonNull EventManager eventManager
) {
// assign fields
}
@Override
public void onLoad() {
System.out.println("Hello from my test plugin!");
}
Which types are bound for which platform by default?
The InjectionLayer
for the plugin can be injected by using the layer type and requesting an instance called plugin
.
Bukkit
Plugin Type
Plugin
,PluginBase
andJavaPlugin
Platform Types
Server
BukkitScheduler
PluginManager
ServicesManager
ScoreboardManager
Bungeecord
Plugin Type
Plugin
Platform Types
ProxyServer
ProxyConfig
TaskScheduler
PluginManager
Fabric
Fabric has no bindings by default as there is nothing to bind.
Minestom
Plugin Type
Extension
Platform Types
ComponentLogger
TagManager
TeamManager
BiomeManager
BlockManager
RecipeManager
BossBarManager
CommandManager
PacketProcessor
InstanceManager
ExceptionManager
ExtensionManager
BenchmarkManager
SchedulerManager
ConnectionManager
GlobalEventHandler
AdvancementManager
DimensionTypeManager
PacketListenerManager
Nukkit
Plugin Type
Plugin
andPluginBase
Platform Types
Server
CommandMap
ServerScheduler
PluginManager
ServiceManager
CraftingManager
ResourcePackManager
Sponge
Plugin Type
PluginContainer
Platform Types
Scheduler
(two variants: one namedsync
(for the sync scheduler) and one nameasync
(for the async scheduler))Game
Platform
SqlManager
DataManager
EventManager
ConfigManager
PluginManager
ChannelManager
BuilderProvider
FactoryProvider
MetricsConfigManager
ServiceProvider.GameScoped
MapStorage
UserManager
WorldManager
Server
RecipeManager
TeleportHelper
CommandManager
PackRepository
ResourceManager
CauseStackManager
GameProfileManager
GameProfileProvider
ServiceProvider
ServiceProvider.ServerScoped
Velocity
Plugin Type
PluginContainer
Object
namedplugin
which is the plugin main class instance
Platform Types
ProxyServer
Scheduler
EventManager
PluginManager
CommandManager
ChannelRegistrar
Waterdog
Plugin Type
Plugin
Platform Types
ProxyServer
MainLogger
CommandMap
PackManager
LangConfig
EventManager
PlayerManager
ServerInfoMap
PluginManager
WaterdogScheduler
ConfigurationManager
🔌 Support for Modules
The CloudNet module system (whether on the wrapper or the node) has full support for dependency injection. The module's main class can have dependencies injected into the constructor, and all module tasks have the ability to inject dependencies. No further configuration is required.
An example module task might be:
@ModuleTask(order = 127, event = ModuleLifeCycle.STARTED)
public void initModule(
@NonNull HttpServer httpServer,
@NonNull ServiceRegistry serviceRegistry,
@NonNull DataSyncRegistry dataSyncRegistry,
@NonNull @Named("module") InjectionLayer<?> injectionLayer // injects the injection layer of the module, this is always named "module"
) {
}
Commands
The node command system allows injection into command processing methods as well. This does not include suggestion processors or parameter parsers.
🔌 Support for Event Listeners
Event Listeners are allowed to take parameters via injection as well. The main catch here is, that the first parameter of an event listener method still needs to be the event which the method wants to get notified about. All other parameters are looked up when the listener should be invoked. An example listener that takes arguments:
@EventListener
public void handleChannelMessage(
@NonNull ChannelMessageReceiveEvent event,
@NonNull EventManager eventManager,
@NonNull PermissionManagement permissionManagement
) {
// do stuff
}