Guice inject based on annotation value

前端 未结 2 1458
孤独总比滥情好
孤独总比滥情好 2021-01-12 17:43

I would like to use goolge/guice inject a value based on a class i provide with the annotation.

AutoConfig annotation

@BindingAnnotation
@Retention(R         


        
相关标签:
2条回答
  • 2021-01-12 18:38

    If you know that @AutoConfig(provider = JsonConfigProvider) ConfigLoader<?> jsonConfig is going to return you exactly the results of jsonConfigProvider.get(), and JsonConfigProvider obviously has a public parameterless constructor for newInstance to work, why wouldn't you just ask for a JsonConfigProvider in the first place?

    Fundamentally Guice is just a Map<Key, Provider> with fancy wrapping. The bad news is that this makes variable bindings like "bind Foo<T> for all T" impossible to express concisely, and that includes your "bind @Annotation(T) Foo for all T". The good news is that you still have two options.

    Bind each provider separately

    Though you can't inspect annotations during provision (or tell Guice to do so for you), Guice will compare annotations using their equals methods if you bind an annotation instance rather than an annotation class (the way you would with Names.named("some-name")). This means that you can bind a ConfigLoader<?> with each expected annotation in a Module. Of course, this also means you'll have to have a list of possible ConfigLoader Providers available at configuration time, but they have to be compile-time constants anyway if you're using them as annotation parameters.

    This solution works with constructor injection as well, but for fields you'll need both @Inject and @AutoConfig(...), and AutoConfig will need to keep its @BindingAnnotation meta-annotation.

    To do this, you're going to have to write an implementation of your annotation, the way Guice does with NamedImpl. Note that the implementations of equals and hashCode must match the ones Java provides in java.lang.Annotation. Then it's just a matter of (redundantly) binding like this:

    for(Class<ConfigLoader<?>> clazz : loaders) {
      bind(ConfigLoader.class).annotatedWith(new AutoConfigImpl(clazz))
          .toProvider(clazz);
    }
    

    The definition of equals is up to you, which means you can (and should) bind @AutoConfig(ConfigEnum.JSON) and keep the Guice bindings in your modules rather than specifying your requested implementation all over your codebase.

    Use custom injections

    You can also use custom injections to search your injected types for custom annotations like @AutoConfig. At this point, you'd be using Guice as a platform to interpret @AutoConfig instead of @Inject, which means that constructor injection won't work but that you can control your injection based on the injected instance, field name, field annotation, annotation parameters, or any combination thereof. If you choose this style, you can drop @BindingAnnotation from AutoConfig.

    Use the example in the wiki article linked above as your template, but at minimum you'll need to:

    1. Use bindListener on Binder or AbstractModule to match types that need this custom injection.
    2. In the TypeListener you bind, search injected types for @AutoConfig-annotated fields, and if they have any matching methods then bind those matching methods to a MembersInjector or InjectionListener. You'll probably want to tease the class literal out of the annotation instance here, and pass in the Field and Class as constructor arguments to the MembersInjector/InjectionListener.
    3. In the MembersInjector or InjectionListener you write, instantiate the provider and set the field to the instance the provider provides.

    This is a very powerful feature, which would futher allow you to--for instance--automatically provide the configuration based on which instance you're injecting into or based on the name of the field. However, use it carefully and document it heavily, because it may be counter-intuitive to your coworkers that Guice is providing for an annotation other than @Inject. Also bear in mind that this won't work for constructor injection, so refactoring from field injection to constructor injection will cause Guice to complain that it's missing a required binding to instantiate the class.

    0 讨论(0)
  • 2021-01-12 18:48

    I had a similar problem. I wanted to use a custom annotation that receives a enum param to choose the implementation. After a lot of research, debug and testing, I came to the following solution:

    //enum to define authentication types
    public enum AuthType {
        Ldap, Saml
    }
    
    //custom annotation to be used in injection
    @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
    @BindingAnnotation
    public @interface Auth {
        AuthType value();
    }
    
    //defintion of authenticator
    public interface Authenticator {
        public void doSomehting();
    }
    
    
    //Authenticator implementations 
    public class LdapAuthenticator implements Authenticator {
    
        @Override
        public void doSomehting() {
            // doing ldap stuff
        }
    
    }
    
    public class SamlAuthenticator implements Authenticator {
    
        @Override
        public void doSomehting() {
            // doing saml stuff
        }
    
    }
    
    public class MyModule extends AbstractModule {
    
        // annotate fields to bind to implementations
        private @Auth(AuthType.Ldap) Authenticator ldap;
        private @Auth(AuthType.Saml) Authenticator saml;
    
        @Override
        protected void configure() {
            //bind the implementation to the annotation from field
            bindAnnotated("ldap", LdapAuthenticator.class);
            bindAnnotated("saml", SamlAuthenticator.class);
    
        }
    
        private void bindAnnotated(String fieldName, Class<? extends Authenticator> implementation) {
            try {
                //get the annotation from fields, then bind it to implementation                
                Annotation ann = MyModule.class.getDeclaredField(fieldName).getAnnotation(Auth.class);
                bind(Authenticator.class).annotatedWith(ann).to(implementation);
            } catch (NoSuchFieldException | SecurityException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    
    //usage:  add @Auth(<AuthType>) to the dependency
    
    public class ClientClass {
    
        private Authenticator authenticator;
    
        @Inject
        public ClientClass(@Auth(AuthType.Ldap) Authenticator authenticator) {
            this.authenticator = authenticator;
        }
    }
    

    Check the documentation of Binder

    I tested the Jeff Bowman solution, but it apparently works only binding to providers

    0 讨论(0)
提交回复
热议问题