How to inject String constants easily with Weld?
Asked Answered
Y

5

16

We have a situation where we provide an external configuration in form of a Map to our running programs. I have found that JSR-330 Dependency Injection gives a much cleaner way to use that configuration map in the code instead of passing the map around or to use JNDI to get it.

@Inject @Named("server.username") String username;

lets the JSR-330 implementation fill in this field automatically.

With Guice I can set the value with

bindConstant().annotatedWith(Names.named(key)).to(value);

I would like to be able to do the same in Weld (bind "server.username" to e.g. "foobar") and I understand that the mechanism most likely is beans.xml, but I would prefer a simple "feed this map to Weld, please" code alternative. What would be a good way to do this?


EDIT 2013-10-16: After looking into Dagger which works at compile time and not runtime, I found that with us usually having 10-20 per program we could live with having a @Provider method for each configuration string which then looks up in the configuration map. This allows for method specific behavior (including default values), ability to provide javadoc, and ability to put all these methods in the same class. Also it works well with Weld out of the box. I am considering writing a fuller explanation in a blog entry.

Yoghurt answered 3/11, 2010 at 13:59 Comment(0)
B
12

I'd like that bounty now please. Figuring this out taught me quite a bit about the innards of WELD, and here's the most interesting lesson: @Named is a qualifier, and must be treated as such if you are going to be able to match against it.

I do have a warning for you: If you are missing any values in your app, it will fail at deploy or load time. This may be desirable for you, but it does specifically mean that "default" values are not possible.

The injection point is specified exactly as you have above, and here's the extension code needed to make it work:

@ApplicationScoped
public class PerformSetup implements Extension {

    Map<String, String> configMap;

    public PerformSetup() {
        configMap = new HashMap<String, String>();
        // This is a dummy initialization, do something constructive here
        configMap.put("string.value", "This is a test value");
    }

    // Add the ConfigMap values to the global bean scope
    void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) {
        // Loop through each entry registering the strings.
        for (Entry<String, String> configEntry : configMap.entrySet()) {
            final String configKey = configEntry.getKey();
            final String configValue = configEntry.getValue();

            AnnotatedType<String> at = bm.createAnnotatedType(String.class);
            final InjectionTarget<String> it = bm.createInjectionTarget(at);

            /**
             * All of this is necessary so WELD knows where to find the string,
             * what it's named, and what scope (singleton) it is.
             */ 
            Bean<String> si = new Bean<String>() {

                public Set<Type> getTypes() {
                    Set<Type> types = new HashSet<Type>();
                    types.add(String.class);
                    types.add(Object.class);
                    return types;
                }

                public Set<Annotation> getQualifiers() {
                    Set<Annotation> qualifiers = new HashSet<Annotation>();
                    qualifiers.add(new NamedAnnotationImpl(configKey));
                    return qualifiers;

                }

                public Class<? extends Annotation> getScope() {
                    return Singleton.class;
                }

                public String getName() {
                    return configKey;
                }

                public Set<Class<? extends Annotation>> getStereotypes() {
                    return Collections.EMPTY_SET;
                }

                public Class<?> getBeanClass() {
                    return String.class;
                }

                public boolean isAlternative() {
                    return false;
                }

                public boolean isNullable() {
                    return false;
                }

                public Set<InjectionPoint> getInjectionPoints() {
                    return it.getInjectionPoints();
                }

                @Override
                public String create(CreationalContext<String> ctx) {
                    return configValue;

                }

                @Override
                public void destroy(String instance,
                        CreationalContext<String> ctx) {
                    // Strings can't be destroyed, so don't do anything
                }
            };
            abd.addBean(si);
        }
    }

    /**
     * This is just so we can create a @Named annotation at runtime.
     */
    class NamedAnnotationImpl extends AnnotationLiteral<Named> implements Named {
        final String nameValue;

        NamedAnnotationImpl(String nameValue) {
            this.nameValue = nameValue;
        }

        public String value() {
            return nameValue;
        }

    }
}

I tested that this worked by making a WELD-SE app:

@ApplicationScoped
public class App {

    @Inject
    @Parameters
    List<String> parameters;

    @Inject
    @Named("string.value")
    String stringValue;

    public void printHello(@Observes ContainerInitialized event) {
        System.out.println("String Value is " + stringValue);
    }

}

Lastly, don't forget /META-INF/services/javax.enterprise.inject.spi.Extension, replacing weldtest with the classpath you use:

weldtest.PerformSetup

That should make all of this work. Let me know if you run into any difficulties and I'll send you my test project.

Barksdale answered 25/1, 2011 at 4:40 Comment(2)
Regarding the "fail at deploy time if value is missing". In our situation - injecting configuration values - that is a feature!Narcho
@Thorbjørn Ravn Andersen Thanks for the bounty. That was quite the dramatic spike in reputation.Barksdale
B
11

Not all that interested in the bounty, but I'll take it if it's still on the table. This is VERY similar to some code I'm using at $DAYJOB, and so this isn't theory, it's what I use in production code, but modified to protect the guilty. I haven't tried compiling the modified code, so be warned that I may have made some errors in changing names and such, but the principles involved here have all been tested and work.

First, you need a Value Holder Qualifier. Use @Nonbinding to keep WELD from matching ONLY to qualifiers with identical values, since we want all values of this particular qualifier to match a single injection point. By keeping the qualifier and value in the same annotation, you can't just "forget" one of them by accident. (KISS principle)

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface ConfigValue {
    // Excludes this value from being considered for injection point matching
    @Nonbinding 
    // Avoid specifying a default value, since it can encourage programmer error.
    // We WANT a value every time.
    String value();
}

Next, you need a producer method which knows how to get the Map. You should probably have a Named bean which holds the producer method, so you can either explicitly initialize the value by using getters/setters, or else have the bean initialize it for you.

We must specify a blank value for the qualifier on the producer method to avoid compile time errors, but it's never used in practice.

@Named
public class ConfigProducer {
    //@Inject // Initialize this parameter somehow
    Map<String,String> configurationMap;

    @PostConstructor
    public void doInit() {
         // TODO: Get the configuration map here if it needs explicit initialization
    }

    // In general, I would discourage using this method, since it can be difficult to control exactly the order in which beans initialize at runtime.
    public void setConfigurationMap(Map<String,String> configurationMap) {
        this.configurationMap = configurationMap;
    }

    @Produces
    @ConfigValue("")
    @Dependent
    public String configValueProducer(InjectionPoint ip) {
        // We know this annotation WILL be present as WELD won't call us otherwise, so no null checking is required.
        ConfigValue configValue = ip.getAnnotated().getAnnotation(ConfigValue.class);
        // This could potentially return a null, so the function is annotated @Dependent to avoid a WELD error.
        return configurationMap.get(configValue.value());
    }
}

Usage is simple:

@Inject
@ConfigValue("some.map.key.here")
String someConfigValue;
Barksdale answered 20/1, 2011 at 9:37 Comment(4)
Very, very close. Can this work with @Named or must I have a new annotation?Narcho
I've never tried putting strings into the global WELD namespace, but that doesn't mean it's not possible. What I will say is that it will take more research and effort than the solution I just presented. What I would look at for the @Named solution is the BeanManager interface. I'll give you a theoretical one that I've done with other types of beans, but haven't tried with Strings so far, but see no reason it shouldn't work.Barksdale
@Thorbjørn Ravn Andersen I believe this is the correct solution. In spring, for example, you do have a special annotation - @Value, and it makes sense.Patronymic
@Bozho, so you would recommend a @ConfigValue attribute, instead of polluting the global name space for @Named?Narcho
B
0

what about

@Resource(name = "server.username", type = java.lang.String.class)
private String injectTo;

Javadoc: http://download.oracle.com/javase/6/docs/api/javax/annotation/Resource.html

Brittabrittain answered 3/11, 2010 at 14:25 Comment(1)
The @Inject should hopefully be unchanged. I am asking how to configure this in Weld in the first place (as I have found out how to do it in GUice)Narcho
S
0

Would implementing a custom Weld InjectionServices not be an option here ?

Socage answered 22/11, 2010 at 11:3 Comment(2)
If you write a functional example which, given a Map<String, String>, can do what I need, I'll open a new bounty and give you.Narcho
And it is open to everyone :) implement this for a 500 point bounty.Narcho
W
0

It may be possible to implement this as an @Dependent Producer method that itself injects an @InjectionPoint which would allow you to reflect upon the field you're being injected into -- this would let you peek into a custom annotation (not a qualifier) member on the field to figure out the val you want to return

@Inject @ConfigMapQualifier @Val("user.name") String user;

...

@Produces @ConfigMapQualifier configProducr(...) { 
...
@Inject InjectionPoint ip;

// use e.g. ip/getJavaMember() then reflection to figure out the @Val value membr.
Workwoman answered 4/1, 2011 at 19:35 Comment(1)
There's no reason not to combine the qualifier and @Val annotation. The piece you're missing is that the value on the @ConfigMapQualifier needs to be annotated as @Nonbinding so that WELD doesn't consider the value for injectionpoint/producer matching. It's a good piece of knowledge to have, as separating the value from the qualifier like this is error prone.Barksdale

© 2022 - 2024 — McMap. All rights reserved.