Skip to content

Creating services

To register a class as a service, add @BService to your class declaration.

@BService is the base annotation to register a service, other annotations exist such as @Command and @Resolver, but the appropriate documentation will specify if such alternatives are required.

Info

All classes available for dependency injection must be in the framework's classpath, by adding packages to BConfigBuilder#packages, or by using BConfigBuilder#addSearchPath, all classes are searched recursively.

Service factories

Service factories are methods that create initialized services themselves, they accept other services as parameters and define a service with the method's return type.

In addition to the package requirement, they must be annotated with @BService, be in a service, or in an object, or be a static method.

Terminology

Classes registered as services, and service factories, are service providers.

Example
public class Config {
    private static Config INSTANCE = null;

    /* */

    // Service factory, registers as "Config" (as it is the return type), with the name "config"
    // You can use any method name, but the method name is what the service is registered as
    @BService
    public static Config config() {
        if (INSTANCE == null) {
            // Of course here you would load the config from a file
            INSTANCE = new Config();
        }

        return INSTANCE;
    }
}
class Config {
    /* */

    companion object {
        // Service factory, registers as Config (as it is the return type), with the name "config"
        // You can use any method name, but the method name is what the service is registered as
        @BService
        fun config(): Config {
            // Of course here you would load the config from a file
            Config()
        }
    }
}
class Config {
    /* */

    companion object {
        // Service factory, registers as Config (as it is the return type), with the name "config"
        @get:BService
        val config: Config by lazy {
            // Of course here you would load the config from a file
            Config()
        }
    }
}

Tip

To suppress unused warnings on the declaring class, you can use @BConfiguration, unless the declaring class itself must be a service.

Conditional services

Warning

This section only applies to the built-in dependency injection

Some services may not always be instantiable, some may require soft dependencies (prevents instantiation if a service is unavailable, without failing), while some run a set of conditions to determine if a service can be instantiated.

Services that are not instantiable will not be created at startup, will be unavailable for injection and do not figure in the list of interfaced services.

Info

All the following annotations must be used alongside a service-declaring annotation, such as @BService or @Command.

Tip

There is a few built-in conditional annotations, such as:

  • @RequiredIntents: Enables a service if the JDAService has the specified intents enabled.
  • @RequiresDatabase: Enables a service if a (Blocking)Database instance is available.
  • @RequiresComponents: Enables a service if components are enabled.

Dependencies

The @Dependencies annotation lets you define soft dependencies, that is, if any of these classes in the annotation are unavailable, your service will not be instantiated.

Without the annotation, any unavailable dependency would throw an exception.

Interfaced conditions

@ConditionalService defines a list of classes implementing ConditionalServiceChecker, the service is only created if none of these classes return an error message.

ConditionalServiceChecker can be implemented on any class that has a no-arg constructor, or is an object.

Example
@Command
@ConditionalService(TagCommand.FeatureCheck.class) // Only create the command if this passes
public class TagCommand {
    /* */

    public static class FeatureCheck implements ConditionalServiceChecker {
        @Nullable
        @Override
        public String checkServiceAvailability(@NotNull ServiceContainer serviceContainer, @NotNull Class<?> checkedClass) {
            final var config = serviceContainer.getService(Config.class); // Suppose this is your configuration
            if (!config.areTagsEnabled()) {
                return "Tags are disabled in the configuration"; // Do not allow the tag command!
            }
            return null; // No error message, allow the tag command!
        }
    }
}
@Command
@ConditionalService(TagCommand.FeatureCheck::class) // Only create the command if this passes
class TagCommand {
    /* */

    object FeatureCheck : ConditionalServiceChecker {
        override fun checkServiceAvailability(serviceContainer: ServiceContainer, checkedClass: Class<*>): String? {
            val config = serviceContainer.getService<Config>() // Suppose this is your configuration
            if (!config.enableTags) {
                return "Tags are disabled in the configuration" // Do not allow the tag command!
            }
            return null // No error message, allow the tag command!
        }
    }
}

Annotation conditions

@Condition is a meta-annotation (an annotation for annotations) which marks your own annotation as being a condition.

Similar to interfaced conditions, they must refer to an implementation of CustomConditionChecker, to determine if the annotated service can be created, you can also indicate if the service creation must throw an exception in case it fails.

The implementation must have a no-arg constructor, or be an object

Note

The annotation must also be in the framework's classpath.

Example
DevCommand.java
// Same targets as service annotations
@Target({ElementType.TYPE, ElementType.METHOD})
// The implementation of our CustomConditionChecker
@Condition(type = DevCommandChecker.class)
public @interface DevCommand { }
DevCommandChecker.java
// Checks services annotated with @DevCommand
public class DevCommandChecker implements CustomConditionChecker<DevCommand> {
    @NotNull
    @Override
    public Class<DevCommand> getAnnotationType() {
        return DevCommand.class;
    }

    @Nullable
    @Override
    public String checkServiceAvailability(@NotNull ServiceContainer serviceContainer, @NotNull Class<?> checkedClass, @NotNull DevCommand annotation) {
        final var config = serviceContainer.getService(Config.class); // Suppose this is your configuration
        if (!config.isDevModeEnabled()) {
            return "Dev mode is disable in the configuration"; // Do not allow the dev commands!
        }
        return null; // No error message, allow the tag command!
    }
}
SlashShutdown.java
@Command
@DevCommand // Our custom condition, this command will only exist if it passes.
public class SlashShutdown {
    /* */
}
DevCommand.kt
// Same targets as service annotations
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
// The implementation of our CustomConditionChecker
@Condition(type = DevCommandChecker::class)
annotation class DevCommand

// Checks services annotated with @DevCommand
object DevCommandChecker : CustomConditionChecker<DevCommand> {
    override val annotationType: Class<DevCommand> = DevCommand::class.java

    override fun checkServiceAvailability(serviceContainer: ServiceContainer, checkedClass: Class<*>, annotation: DevCommand): String? {
        val config = serviceContainer.getService<Config>() // Suppose this is your configuration
        if (!config.enableDevMode) {
            return "Dev mode is disable in the configuration" // Do not allow the dev commands!
        }
        return null // No error message, allow the tag command!
    }
}
SlashShutdown.kt
@Command
@DevCommand // Our custom condition, this command will only exist if it passes.
class SlashShutdown {
    /* */
}

Interfaced services

Warning

This section only applies to the built-in dependency injection

Interfaced services are interfaces, or abstract class, marked by @InterfacedService, they must be implemented by a service.

In addition to the service's type, implementations of these annotated interfaces have the interface's type automatically added.

Some interfaced services may only be implemented once, some may allow multiple implementations, if an interfaced service only accepts one implementation, multiple implementations can exist, but only one must be instantiable.

Creating multiple interfaced services in one

You can implement multiple interfaced services at once, which may be useful for text, application and component filters.

Creating interfaced services without registering the interface type

You can also implement an interfaced service, without it being accessible as such, by using @IgnoreServiceTypes.

2.X Migration

Most methods in CommandsBuilder accepting interfaces, implementations or lambdas, were moved to interfaced services:

Global:

  • CommandsBuilder#setComponentManager: Removed, using components must be enabled in BComponentsConfigBuilder#enable, and a ConnectionSupplier service be present
  • CommandsBuilder#setSettingsProvider: Needs to implement SettingsProvider
  • CommandsBuilder#setUncaughtExceptionHandler: Needs to implement GlobalExceptionHandler
  • CommandsBuilder#setDefaultEmbedFunction: Needs to implement DefaultEmbedSupplier and DefaultEmbedFooterIconSupplier

Text commands:

  • TextCommandBuilder#addTextFilter: Needs to implement TextCommandFilter, and TextCommandRejectionHandler
  • TextCommandBuilder#setHelpBuilderConsumer: Needs to implement HelpBuilderConsumer

Application commands:

  • ApplicationCommandBuilder#addApplicationFilter: Needs to implement ApplicationCommandFilter, and ApplicationCommandRejectionHandler
  • ApplicationCommandBuilder#addComponentFilter: Needs to implement ComponentCommandFilter, and ComponentCommandRejectionHandler

Extensions:

  • ExtensionsBuilder#registerAutocompletionTransformer: Needs to implement AutocompleteTransformer
  • ExtensionsBuilder#registerCommandDependency: Replaced with standard dependency injection
  • ExtensionsBuilder#registerConstructorParameter: Replaced with standard dependency injection
  • ExtensionsBuilder#registerCustomResolver: Needs to implement ClassParameterResolver and ICustomResolver
  • ExtensionsBuilder#registerDynamicInstanceSupplier: Needs to implement DynamicSupplier
  • ExtensionsBuilder#registerInstanceSupplier: Replaced by service factories
  • ExtensionsBuilder#registerParameterResolver: Needs to implement ClassParameterResolver and the resolver interface of your choices

Service properties

Warning

This section only applies to the built-in dependency injection

Service providers can have names, additional registered types, and an instantiation priority.

Service names

Named services may be useful if you have multiple services of the same type, but need to get a specific one.

The name is either defined by using @ServiceName, or with BService#name on the service provider.

Example

You can have a caching HttpClient named cachingHttpClient, while the usual client uses the default name.

Service types

In addition to the type of the service provider, @ServiceType enables you to register a service as a supertype.

Service priority

Service priorities control how service providers are sorted.

A higher priority means that the service will be loaded first, or that an interfaced service will appear first when requesting interfaced services.

The priority is either defined by using @ServicePriority, or with BService#priority on the service provider, see their documentation to learn what how service providers are sorted.