Using Spring's ObjectProvider
When Spring Framework 4.3 was released, it introduced ObjectProvider. As Spring’s blog [1] mentions, this provided an extension to the existing ObjectFactory interface with handy signatures such as getIfAvailable, getIfUnique etc to retrieve a bean only if it actually exists or if a single candidate can be determined (such as a primary candidate in case of multiple matching beans). This also improved the programmatic resolution of dependencies.
With 5.0 release, this has been improved to support newer APIs that allows you to pass a Supplier
for the getIfAvailable/getIfUnique APIs and a Consumer
for the ifAvailable/ifUnique APIs.
And with 5.1 release, this has been further extended to give it Iterable
and Stream
support [2].
Diving into ObjectProvider
Let’s take a look at the things you can do with ObjectProvider. We’ll dive into the simple use-cases first and then cover the newer APIs too.
The code discussed below is available here.
No dependencies
Let’s say I’ve this simple Interface for a logging service as given below:
I am going to use this LogService inside the Spring Bean as shown.
The above Spring bean wires in the LogService dependency using constructor-based injection.
You don’t really need to mention @Autowired
as part of the Constructor param as this is the only Constructor. This support came in with Spring 4.3 that allows Implicit constructor injection for single-constructor
scenarios. I also don’t need the @Component
since I am explicitly registering it with the ApplicationContext
. I am going to skip both in the later examples.
I’ll now wire this up in the following test case and we shall see what happens when there are no dependencies for LogService
.
Notice that no dependencies of LogService
were registered with the ApplicationContext. There is only one User defined bean.
As expected, we get the NoSuchBeanDefinitionException
for the field logService in ExampleOne
.
Throws Unsatisfied dependency expressed through field ‘logService’; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.example.beans.LogService’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
You may need to run your application in certain environments where it is normal for some dependencies to be missing (i.e. they are optional) and you don’t want your application to blow up like above.
How do you go above handling such a case then?
Making it Optional
Let’s see how the ObjectProvider
can come to our rescue here to handle optional dependencies.
I’ll update the type from LogService
to ObjectProvider<LogService>
and also modify the runApps method to use the ifAvailable
API.
Now when I run the same test as before, we don’t get any exception and things are all good. No output is printed as expected.
Let’s make sure this works even when a dependency actually exists. To do that, we register the PlainLogger
bean as well with the ApplicationContext as part of context.register(...)
.
This time I get some output as expected:
Data [some data] at 1545539958913
Note: You can also use the
java.util.Optional<T>
instead ofObjectProvider<T>
but it works well only when zero/one implementation(s) exists.
More than one dependency
Now what happens if there are more than one dependencies of LogService
, which one would be used with the above? Any guesses?
I am registering two implementations here for LogService
, namely PlainLogger
and JsonLogger
.
This would not work. Notice where this fails though. The wiring is fine though however when calling runApps
Spring doesn’t know which one to choose for you.
Since Spring cannot decide which specific implementation you want (and neither bean is marked as @Primary
too), this would throw an exception.
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.example.service.LogService’ available: expected single matching bean but found 2: plainLogger,jsonLogger
One fix for above would be to mark one of the implementations as @Primary
. But what if you needed both the implementations?
To do that, I’ll now use the API introduced in 5.1 for this (this is not supported pre-5.1 though).
We don’t need to make changes to the type. The type remains the same, it is still ObjectProvider<LogService>
and not ObjectProvider<List<LogService>>
.
However the API to access the beans is a bit different. We use the stream
API to access the dependencies. We could have used an enhanced for loop as well since the ObjectProvider
interface extends Iterable
. See the updated implementation of runApps
Let’s test this out.
When you run the above example, you get the output from both the LogService implementations.
Data [some app data with ExampleThree] at 1545540204711
{"log": { "message": "some app data with ExampleThree", "timestamp": 1545540204711 } }
Notice how the stream()
method allows us to access all the available dependencies (0 or more).
You can even use the orderedStream()
method to get the beans as defined by the order using @Order
annotation on the beans.
API Doc:
Try removing all the LogService implementations and the code still works with no output as expected (see testExampleThreeWithNoLoggers
).
So far, just using ObjectProvider
More Use-cases
Lets dive into examples covering the 5.0 API.
Adding a default implementation
Imagine if you have an application where you want to provide a default implementation programmatically when no dependencies are available for certain types. How do you add a fallback mechanism?.
getIfAvailable/getIfUnique
I’ll use the previous example where no dependencies of LogService
exist but it should fallback to use the PlainLogger
implementation.
API Doc:
In the getLogService method of the following example, I use the getIfAvailable
API that allows us to provide a Supplier
if no candidates are available.
Let’s run with no dependencies that invokes the getLogService()
on the ExampleFour bean.
We get the output from the PlainLogger
in this case.
Data [some app data with ExampleFour] at 1545540354820
An astute reader might notice the issue here with using the getIfAvailable
API.
What would happen when there are more than one implementations?
This would blow up (with NoUniqueBeanDefinitionException) since Spring cannot decide which one you need unless one of them is marked @Primary.
Rather we can use the getIfUnique
API. It works for all the cases where there are no dependencies, one or more than one.
So when there are none or more than one dependencies, the fallback PlainLogger
will be used. However if only one implementation of LogService
is found, that would be used instead.
ifAvailable/ifUnique
API Doc:
The following example uses the ifAvailable
API that allows you to hook in a Consumer
that accepts a bean instance if a dependency for the given type is found.
When the runApps
method is invoked, if a dependency is found, then the consumer is fired otherwise nothing happens. In the test above, you should see the output from the JsonLogger
.
You can similarly use the ifUnique
API that also takes in a Consumer
. This comes in handy to support all cases as explained in the earlier scenario with getIfUnique
.
Summary
That wraps up the basic use-cases for ObjectProvider. You can use it to retrieve a bean only if it actually exists or if a single candidate can be determined using programmatic resolution without blowing up at the wiring phase or while using the bean instances. With Iterable and Stream support added in 5.1, we can easily support cases (with the same code) when zero or more dependencies exist for the bean of a given type.
The above code samples are available here
References
[1] Spring Framework 4.3 Core Updates
blog comments powered by Disqus