You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 2 Next »

Background

Plugins in Log4j have long supported a primitive form of dependency injection through the use of annotations that indicate where in the configuration to obtain the injected data. This initially began with the static plugin factory method whose arguments could be attributes (keys with scalar values; in XML, this is an element attribute; in JSON and YAML, this is a field with a scalar value), elements (further-defined plugin objects to inject; in XML, this is an element; in JSON and YAML, this is a field with a non-scalar value), and values (a special kind of attribute; in XML, this is the text contents of an element; in JSON and YAML, this is another attribute typically named "value" or similar), along with bindings for the Configuration being processed (usually used for obtaining the configuration's StrSubstitutor for variable interpolation) and the currently processed Node.

Configurations are parsed as a tree of nodes where each node contains a name, list of attributes, child nodes, an optional value, which are processed recursively unless a plugin sets its deferChildren option to true which defers loading and binding of the children nodes of that plugin's node. This lazy loading option is primarily useful for advanced plugins like RoutingAppender which selectively load plugin configurations based on processed LogEvent data.

Later on, support for creating and configuring plugins via builder classes was added for simplifying default value strategies, adding new configuration options over time without maintaining several deprecated static factory methods, and making tests easier to write when directly referencing plugin instances. This added injection through fields of the builder class along with a separate static factory method on the plugin class to construct fresh builder instances.

While this strategy has worked well enough for configurations where all necessary plugin objects are configured through a config file, this has left much duplicate functionality around log4j-core for loading other types of plugins, and it has made it difficult to inject broader-scoped objects into narrower-scoped ones (e.g., injecting a singleton-scoped instance into a configuration-scoped instance). In order to simplify plugin handling and configuration loading, these related plugin loading systems should be unified into a consistent dependency injection API.

Proposal

Plugin annotations should be updated to support a dependency injection API similar to javax.inject which is sufficiently simple to model both existing injection strategies as well as new ones introduced by this system (such as method-based and constructor-based injection). To avoid third party dependencies, Log4j should add its own equivalent annotations to those in javax.inject for better integration with how Log4j already works. For example, javax.inject does not specify anything about plugin aliases, so our system needs to account for this. Some immediate differences in our API include replacing the javax.inject.Provider<T> interface with java.util.function.Supplier<T> as well as changing the @Named annotation to use an array of strings rather than a single string so that aliases can also be specified. The @FactoryType annotation is introduced for annotating factory annotations such as @PluginFactory and the more generic @Factory annotation. Other meta-annotations used in this system include @QualifierType and @ScopeType for marking annotations as qualifiers and scopes respectively.

The Key<T> class provides a way to identify plugins by type, optional qualifier, and a name. Existing @PluginAttribute and @PluginValue annotations are natural qualifier types, though given that these annotations are duplicated in log4j-plugins and log4j-core, a more generic mechanism is needed to treat these as equivalent. This is where the name aspect of a Key comes in; all named-style qualifier annotations are treated as @Named qualifiers. The ConfigurationInjector API in log4j-plugins should be replaced with a simpler strategy for supplying a (possibly converted) value for a particular Node instance.

When processing a Configuration with a Node tree, the general logic for combining plugin instances is handled specially by different Configuration implementations. AbstractConfiguration should be updated to rely on dependency injection to form the full graph of plugin instances so that different configuration formats would only be responsible for transforming input configuration data into a tree of Nodes which get processed by dependency injection. Plugins that inject a Configuration can be refactored to inject whichever relevant instances are needed from the configuration instead of looking them up manually.

No annotation processing updates are required here as the existing plugin annotations provide enough useful metadata on plugins to avoid loading them all at runtime. Plugin classes relying on @Inject are supported as soon as that class is referenced in the injection system as the injection constructor is discovered on demand.

An additional API should be added for users to provide custom injection modules to allow programmatic configuration of other injectable instances. This may be useful for unifying some ServiceLoader-related APIs in Log4j. These modules can set up bindings for configurable classes outside the normal configuration system. For example, if injection begins from Log4jLoggerContext, then these modules can be used for programmatically configuring a ContextSelector and ShutdownCallbackRegistry instead of relying on optional system properties to specify a class name. Programmatic configuration from a ServiceLoader-created class simplifies class loading as direct references to classes can be compiled in directly. When injection begins from a LoggerContext, modules can customize classes that are global to a configuration or logger context. This also provides a natural place to define or override ConfigurationFactory instances. Other programmatic instances needed for various plugins can be more easily specified through this API.

  • No labels