Problem
Using CDI I want to produce @ApplicationScoped
beans.
Additionally I want to provide a configuration annotation to the injection points, e.g.:
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Configuration {
String value();
}
I do not want to write a separate producer for each different possibility of value
.
Approach
The usual way would be to make a producer and handle the injection point annotations:
@Produces
public Object create(InjectionPoint injectionPoint) {
Configuration annotation = injectionPoint.getAnnotated().getAnnotation(Configuration .class);
...
}
By consequence the bean cannot be application scoped anymore, because each injection point could be possibly different (parameter injectionpoint for producers does not work for @AplicationScoped
annotated producers).
So this solution does not work.
Question
I would need a possibility that the injection points with the same value get the same bean instance.
Is there an inbuilt CDI way? Or do I need to somehow "remember" the beans myself in a list, e.g. in the class containing the producer?
What I need is basically an ApplicationScoped
instance for each different value
.
What you try to achieve is not an out fo the box feature in CDI, but thanks to its SPI and a portable extension you can achieve what you need.
This extension will analyse all injection poins with a given type, get the @Configuration
annotations on each of them and will create a bean in applicationScoped for each different value of the member value()
in the annotation.
As you'll register multiple beans with the same type you'll have first to transform your annotation into a qualifier
@Qualifier
@Target({TYPE, METHOD, PARAMETER, FIELD})
@Retention(RUNTIME)
@Documented
public @interface Configuration {
String value();
}
Below the class to use to create your bean instances:
@Vetoed
public class ConfiguredService {
private String value;
protected ConfiguredService() {
}
public ConfiguredService(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
Note the @Vetoed
annotation to make sure that CDI won't pick up this class to create a bean as we'll do it ourself. This class has to have a default constructor with no parameter to be used as class of a passivating bean (in application scoped)
Then you need to declare the class of your custom bean. Sees it as a factory and metadata holder (scope, qualifiers, etc...) of your bean.
public class ConfiguredServiceBean implements Bean<ConfiguredService>, PassivationCapable {
static Set<Type> types;
private final Configuration configuration;
private final Set<Annotation> qualifiers = new HashSet<>();
public ConfiguredServiceBean(Configuration configuration) {
this.configuration = configuration;
qualifiers.add(configuration);
qualifiers.add(new AnnotationLiteral<Any>() {
});
}
@Override
public Class<?> getBeanClass() {
return ConfiguredService.class;
}
@Override
public Set<InjectionPoint> getInjectionPoints() {
return Collections.EMPTY_SET;
}
@Override
public boolean isNullable() {
return false;
}
@Override
public Set<Type> getTypes() {
return types;
}
@Override
public Set<Annotation> getQualifiers() {
return qualifiers;
}
@Override
public Class<? extends Annotation> getScope() {
return ApplicationScoped.class;
}
@Override
public String getName() {
return null;
}
@Override
public Set<Class<? extends Annotation>> getStereotypes() {
return Collections.EMPTY_SET;
}
@Override
public boolean isAlternative() {
return false;
}
@Override
public ConfiguredService create(CreationalContext<ConfiguredService> creationalContext) {
return new ConfiguredService(configuration.value());
}
@Override
public void destroy(ConfiguredService instance, CreationalContext<ConfiguredService> creationalContext) {
}
@Override
public String getId() {
return getClass().toString() + configuration.value();
}
}
Note that the qualifier is the only parameter, allowing us to link the content of the qualifier to the instance in the create()
method.
Finally, you'll create the extension that will register your beans from a collection of injection points.
public class ConfigurationExtension implements Extension {
private Set<Configuration> configurations = new HashSet<>();
public void retrieveTypes(@Observes ProcessInjectionPoint<?, ConfiguredService> pip, BeanManager bm) {
InjectionPoint ip = pip.getInjectionPoint();
if (ip.getAnnotated().isAnnotationPresent(Configuration.class))
configurations.add(ip.getAnnotated().getAnnotation(Configuration.class));
else
pip.addDefinitionError(new IllegalStateException("Service should be configured"));
}
public void createBeans(@Observes AfterBeanDiscovery abd, BeanManager bm) {
ConfiguredServiceBean.types = bm.createAnnotatedType(ConfiguredService.class).getTypeClosure();
for (Configuration configuration : configurations) {
abd.addBean(new ConfiguredServiceBean(configuration));
}
}
}
This extension is activated by adding its fully qualified classname to the META-INF/services/javax.enterprise.inject.spi.Extension
text file.
There are other way to create your feature with an extension, but I tried to give you a code working from CDI 1.0 (except for the @Vetoed
annotation).
You can find the source code of this extension in my CDI Sandbox on Github.
The code is quite straight forward, but don't hesitate if you have questions.