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 Node
s 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.