CDI - ApplicationScoped but configured

2019-04-17 00:16发布

问题:

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 @AplicationScopedannotated 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 ApplicationScopedinstance for each different value.

回答1:

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.