Today’s CDI 2.0 topic is actually about features that have been in there for a long while, but that I don’t see much evidence of out in the wild.
A lot of people are used to injection as provided by Jersey. This is implemented under the covers by the excellent little underappreciated HK2 project, not by CDI.
HK2 can do really neat things with annotations. In today’s blog, I want to show you how I took an annotation that was being used by an HK2-based system to provide configuration injection and, using the sledgehammer of the CDI portable extension API, made it usable in a CDI project.
The annotation looks like this, more or less:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Config { | |
@Qualifier | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target({ElementType.FIELD, ElementType.PARAMETER}) | |
@interface Key { | |
String value(); | |
} | |
} |
As you can see, it is a simple Qualifier
annotation that can be applied to fields and parameters only. It has a value
element, which ends up being the name of a particular piece of configuration you want.
So you could see it being used like this:
@Inject @Config.Key("frobnicationInterval") private int frobnicationInterval;
Or like this:
@Inject public Frobnicator(@Config.Key("frobnicationInterval") final int frobnicationInterval) { super(); this.frobnicationInterval = frobnicationInterval; }
HK2 has a concept somewhat analogous to CDI’s producer. While a CDI producer can be a method or a field, it can’t be a class. HK2, on the other hand, defines only one kind of thing-that-can-make-other-things, and calls it a Factory
. Factory
‘s provide
method returns the thing it can make, and you annotate the provide
method in much the same way as you do a CDI producer method.
Unless, of course, you’re working with this particular annotation, as its Target
meta-annotation does not allow it to be placed on methods of any kind.
In the HK2 project I was looking at, then, the Factory
in question behind this Config.Key
annotation simply declared that it provides Object
. No qualifiers. Hmm.
Now, to get a Factory
to be treated as a Factory
by HK2, you have to mark it as such, or otherwise instruct HK2 to treat it as a Factory
. Otherwise it’s just another object that happens to implement the Factory
interface. None of those markings or instructions were immediately apparent.
The other thing you can do, though, is define something called an InjectionResolver
. If you do that, then inside that class you can do whatever you like to resolve the given injection point. As I looked at this project, I found one for Config.Key
. It delegated its work off to the Factory
I found, and other various internal configuration engines and whatnot, and the end result is that any class inside this project could do something like the examples I showed above.
I thought I’d cobble together some CDI constructs to do the same thing.
I knew that I couldn’t make any use of managed beans, because of course int
and String
are not managed beans. Short of really convoluted potential other solutions, obviously I’d need a producer method. I would just need to make a producer method that returned Object and that is qualified by Confi
—
Oops. The annotation doesn’t have ElementType.METHOD
as one of the places listed where it can be used. So my producer method code won’t compile, because I can’t put the Config.Key
annotation on it.
Off to the portable extension toolbox.
First, I wrote a private annotation (I named it Property
) that looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Qualifier | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) | |
private @interface Property { | |
@Nonbinding | |
String value() default ""; | |
} |
This private
annotation (an inner annotation) is solely for my bean-housing-the-producer-method’s use, and the use of the extension that installs it.
You’ll note it looks almost the same as Config.Key
, but this time it has ElementType.METHOD
in its Target
annotation. It also features the Nonbinding
annotation applied to its value
element, and the value
element now has a default value of ""
. We’ll talk about those things in a bit.
Then, for reasons to be clear in a bit, I wrote an AnnotationLiteral
implementation for it:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static final class PropertyLiteral extends AnnotationLiteral<Property> implements Property { | |
private static final long serialVersionUID = 1L; | |
private final String value; | |
private PropertyLiteral() { | |
this(""); | |
} | |
private PropertyLiteral(final String value) { | |
super(); | |
Objects.requireNonNull(value); | |
this.value = value; | |
} | |
@Nonbinding | |
@Override | |
public final String value() { | |
return this.value; | |
} | |
public static final PropertyLiteral of(final String value) { | |
return new PropertyLiteral(value); | |
} | |
} |
The new Property
annotation will let me write a producer method like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Produces | |
@Dependent // not really needed; I like to be explicit | |
@Property | |
private final Object produceProperty(final InjectionPoint ip, final BigCoConfigurationEngine config) { | |
// Get the right configuration value identified by the injection point | |
// using the BigCoConfigurationEngine | |
throw new UnsupportedOperationException("Not done yet"); | |
} |
…now that I can use @Property
on methods and not just on parameters and fields.
But who cares? No user code can use my new, private
inner Property
annotation! So who the heck is ever going to cause this method to be invoked?
Enter portable extensions. Now we need a portable extension that will search for injection points that feature @Config.Key
and make them behave as though they were written with @Property
instead. Once we do that, then we have a linkage: the user’s code says that it wants a particular kind of Object
to be injected—namely a @Config.Key
-ish one—but we know that for our purposes this should be translated into a desire for a @Property
-ish one, and that will be satisfied by the CDI typesafe resolution algorithm and this producer method will be invoked.
There is a container event for just this sort of thing. It’s called ProcessInjectionPoint
, and you can do things with it like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private final void processInjectionPoint(@Observes final ProcessInjectionPoint<?, ?> event) { | |
if (event != null) { | |
final InjectionPoint ip = event.getInjectionPoint(); | |
assert ip != null; | |
final Set<Annotation> existingQualifiers = ip.getQualifiers(); | |
if (existingQualifiers != null && !existingQualifiers.isEmpty()) { | |
final Set<Annotation> newQualifiers = new HashSet<>(); | |
existingQualifiers.stream().forEach((qualifier) -> { | |
assert qualifier != null; | |
if (qualifier instanceof Key) { | |
newQualifiers.add(new PropertyLiteral(((Key)qualifier).value())); | |
} else { | |
newQualifiers.add(qualifier); | |
} | |
}); | |
event.configureInjectionPoint().qualifiers(newQualifiers); | |
} | |
} | |
} |
This (private
!) method will be called on every injection point found by the container (or created by other portable extensions!). You can restrict the types of points you’re interested in by using something other than wildcards, but this will do for our purposes.
Here, you can see that we ask the InjectionPoint
directly for its qualifiers, and we effectively remove the Config.Key
qualifier and replace it with an equivalent Property
qualifier.
So under the covers it now looks like all client code is using Property
, not Config.Key
in its injection points. Cool!
Let’s circle back to the producer method, now that we know it will be invoked.
In CDI, a producer method can take parameters. If it does, then the parameters are supplied to it by the container as if the method had been marked with @Inject. So our producer method is handed an InjectionPoint
object and a BigCoConfigurationEngine
object. (Let’s pretend for the sake of this article that an instance of BigCoConfigurationEngine
has already been found by the container. We’ll just assume it’s there so will be successfully injected here.)
The InjectionPoint
unsurprisingly represents the site of the injection that the producer method will be called upon to implement as needed (as determined by the scope, in our case Dependent
). You can get lots of useful things from it: the parameter or field being injected “into”, the class housing the injection point, and so on.
So we should be able to get the actual Property
instance that caused our producer method to fire, and, using the value of its value
element, ask the BigCoConfigurationEngine
to get us the right configuration value.
There is one very important thing to note here.
There are a couple of “paths” to the information we would like. Only one of them is valid.
The first path looks like an easy one: we could just call injectionPoint.getAnnotated()
, and then call getAnnotation(Class)
on it and pass it our Property
class.
But we’ll get back no such annotation! How can that be? Didn’t our portable extension munge things at startup so that all Config.Key
qualifiers got effectively replaced by Property
qualifiers?
Yes, but on the injection point
itself, not on the objects reachable from the injection point.
That gives us path #2, which is the right one, though it is more cumbersome. We need to call injectionPoint.getQualifiers()
, and then find the Property
annotation in there. That is, our portable extension affected the contents of the return value of the InjectionPoint::getQualifiers
method, not the contents of the return value of injectionPoint.getAnnotated().getAnnotations()
. Sit and think about that for a moment. I’ll wait.
So our producer method ends up looking like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Produces | |
@Dependent | |
@Property | |
private static final Object produceProperty(final InjectionPoint injectionPoint, final BigCoConfigurationEngine config) { | |
Objects.requireNonNull(config); | |
Object returnValue = null; | |
if (injectionPoint == null) { | |
// This is a case that really shouldn't happen. | |
return null; // ok to do with Dependent scope | |
} else { | |
final Set<Annotation> qualifiers = injectionPoint.getQualifiers(); | |
assert qualifiers != null; | |
assert !qualifiers.isEmpty(); | |
final Optional<Annotation> propertyAnnotation = qualifiers.stream().filter((annotation) -> { | |
return annotation instanceof Property; | |
}).findFirst(); | |
assert propertyAnnotation.isPresent(); | |
final Property property = Property.class.cast(propertyAnnotation.get()); | |
assert property != null; | |
final String name = property.value(); | |
assert name != null; | |
returnValue = config.get(name); | |
} | |
return returnValue; | |
} |
The takeaway for me here was: in your producer method, if you want to be maximally flexible and a good citizen of the CDI multiverse, make sure you investigate the InjectionPoint
metadata itself as much as possible for information, not the Annotated
instances reachable from it.