This feature (and this document) is aimed at platform developers who intend to provide additional platform features. It is not intended for people who simply want to contribute a technology extension, such as a database implementation or a storage backend!
In this document we will use the term "default provider" and "default provider method" synonymously to refer to a method
annotated with @Provider(isDefault=true)
. Similarly, "provider", "provider method" or "factory method"
refer to methods annotated with just @Provider
.
Default provider methods are intended to provide fallback implementations for services rather than to achieve extensibility - that is what extensions are for. There is a subtle but important semantic difference between fallback implementations and extensibility:
Fallbacks are meant as safety net, in case developers forget or don't want to add a specific implementation for a service. It is there so as not to end up without an implementation for a service interface. A good example for this are in-memory store implementations. It is expected that an actual persistence implementation is contributed by another extension. In-mem stores get you up and running quickly, but we wouldn't recommend using them in production environments. Typically, fallbacks should not have any dependencies onto other services.
Default-provided services, even though they are on the classpath, only get instantiated if there is no other implementation.
In contrast, extensibility refers to the possibility of swapping out one implementation of a service for another by
choosing the respective module at compile time. Each implementation must therefore be contained in its own java module,
and the choice between one or the other is made by referencing one or the other in the build file. The service
implementation is typically instantiated and provided by its own extension. In this case, the @Provider
-annotation **
must not** have the isDefault
attribute. This is also the case if there will likely only ever be one implementation
for a service.
One example for extensibility is the IdentityService
: there could be several implementations for it (OAuth,
DecentralizedIdentity, Keycloak etc.), but providing either one as default would make little sense, because all of them
require external services to work. Each implementation would be in its own module and get instantiated by its own
extension.
Provided services get instantiated only if they are on the classpath, but always get instantiated.
Generally speaking every extension goes through these lifecycle stages during loading:
inject
: all fields annotated with@Inject
are resolvedinitialize
: theinitialize()
method is invoked. All required collaborators are expected to be resolved after this.provide
: all@Provider
methods are invoked, the object they return is registered in the context.
Due to the fact that default provider methods act a safety net, they only get invoked if no other provider method offers
the same service type. However, what may be a bit misleading is the fact that they typically get invoked during the
inject
phase. The following section will demonstrate this.
Recall that @Provider
methods get invoked regardless, and after the initialze
phase. That means, assuming both
extensions are on the classpath, the extension that declares the provider method (= ExtensionA
) will get fully
instantiated before another extension (= ExtensionB
) can use the provided object:
public class ExtensionA { // gets loaded first
@Inject
private SomeStore store; // provided by some other extension
@Provider
public SomeService getSomeService() {
return new SomeServiceImpl(store);
}
}
public class ExtensionB { // gets loaded second
@Inject
private SomeService service;
}
After building the dependency graph, the loader mechanism would first fully construct ExtensionA
, i.e.
getSomeService()
is invoked, and the instance of SomeServiceImpl
is registered in the context. Note that this is
done regardless whether another extension actually injects a SomeService
. After that, ExtensionB
gets constructed,
and by the time it goes through its inject
phase, the injected SomeService
is already in the context, so the
SomeService
field gets resolved properly.
Methods annotated with @Provider(isDefault=true)
only get invoked if there is no other provider method for that
service, and at the time when the corresponding @Inject
is resolved. Modifying example 1 slightly we get:
public class ExtensionA {
@Inject
private SomeStore store;
@Provider(isDefault = true)
public SomeService getSomeService() {
return new SomeServiceImpl(store);
}
}
public class ExtensionB {
@Inject
private SomeService service;
}
The biggest difference here is the point in time at which getSomeService
is invoked. Default provider methods get
invoked when the @Inject
dependency is resolved, because that is the "latest" point in time that that decision can
be made. That means, they get invoked during ExtensionB
's inject phase, and not during ExtensionA
's provide phase.
There is no guarantee that ExtensionA
is already initialized by that time, because the extension loader does not know
whether it needs to invoke getSomeService
at all, until the very last moment, i.e. when resolving ExtensionB
's
service
field. By that time, the dependency graph is already built.
Consequently, default provider methods could (and likely would) get invoked before the defining extension's provide
phase has completed. They even could get invoked before the initialize
phase has completed: consider the following
situation the previous example:
- all implementors of
ServiceExtension
get constructed by the JavaServiceLoader
ExtensionB
gets loaded, runs through its inject phase- no provider for
SomeService
, thus the default provider kicks in ExtensionA.getSomeService()
is invoked, butExtensionA
is not yet loaded ->store
is null- -> potential NPE
Because there is no explicit ordering in how the @Inject
fields are resolved, the order may depend on several factors,
like the Java version or specific JVM used, the classloader and/or implementation of reflection used, etc.
From the previous sections and the examples demonstrated above we can derive a few important guidelines:
- do not use them to achieve extensibility. That is what extensions are for.
- use them only to provide a fallback implementation
- they should not depend on other injected fields (as those may still be null)
- they should be in their own dedicated extension (cf.
DefaultServicesExtension
) and Java module - do not provide and inject the same service in one extension
- rule of thumb: unless you know exactly what you're doing and why you need them - don't use them!