Geek injection based on annotation value

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

AutoConfig Annotations

@BindingAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.PARAMETER, ElementType.FIELD }) public @interface AutoConfig { // default null not possible Class<? extends Provider<? extends ConfigLoader<?>>> provider() default XMLAutoConfigProvider.class; } 

This is my annotation, which allows you to configure the type of config that should be used for annotated fields.

USECASE:

 @AutoConfig() ConfigLoader<?> defaultConfig; @AutoConfig(provider = JsonConfigProvider) ConfigLoader<?> jsonConfig; 

I want to have two configurations: one by default / xml and json. They will probably never meet in the same class at the same time. But I do not know when this or that is used. I used the class approach because they are provided by some / libs dependencies, and this annotation will be used for some (plug-in) submodules.

Myguicemodule

 public class MyGuiceModule extends AbstractModule { @Override protected void configure() { bind(new TypeLiteral<ConfigLoader<?>>() {}) .annotatedWith(AutoConfig.class) .toProvider(autoConfig.provider()); } } 

This is a critical part, I just canโ€™t imagine how to implement it.

So basically I just want to use the provider class specified in the annotation. It is not necessary to use the provider class here. Because autoConfig.provider (). NewInstance () is basically all I need. (I need to use the setter in a new instance, but that's all I want to do in this place)

To summarize, all I really want to do is click on the annotation (or its values โ€‹โ€‹to the provider) either using get (AutoConfig autoConfig) or in the constructor. Currently, I use the constructor only to enter the configFile value that I want to set for the newly created configuration instance.

+5
source share
2 answers

If you know that @AutoConfig(provider = JsonConfigProvider) ConfigLoader<?> jsonConfig will return the exact results jsonConfigProvider.get() , and JsonConfigProvider obviously has an open constructor without parameters for newInstance to work, why don't you just ask JsonConfigProvider first of all?

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

Bind each vendor separately

Although you cannot check annotations at the time of submission (or tell Guice to do this for you), Guice will compare annotations using their equals methods if you are linking an instance of the annotation and not an annotation class (like you would with Names.named("some-name") ). This means that you can bind ConfigLoader<?> To each expected annotation in the module. Of course, this also means that you will need to have a list of possible ConfigLoader providers available at setup time, but they must be compile-time constants if you use them as annotation parameters.

This solution also works with the constructor injector, but for fields you will need both @Inject and @AutoConfig(...) , and AutoConfig will need to save the @BindingAnnotation meta annotation.

To do this, you will have to write an implementation of your annotation, as Guice does with NamedImpl . Note that the equals and hashCode must match those Java provides in java.lang.Annotation . Then this is just a (redundant) question related as follows:

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

The definition of equals up to you, which means you can (and should) bind @AutoConfig(ConfigEnum.JSON) and store Guice bindings in your modules, rather than specifying the requested implementation throughout your code base.

Use custom injections

You can also use custom injections to search for typed types for custom annotations like @AutoConfig . At this point, you will use Guice as the platform for interpreting @AutoConfig instead of @Inject , which means the constructor installation will not work, but you can control your injection based on the entered instance, field, annotation field, annotation parameters, or any combination of them . If you choose this style, you can remove @BindingAnnotation from AutoConfig.

Use the example in the wiki article above as a template, but at a minimum you need to:

  • Use a bindListener in a Binder or AbstractModule to match the types that this custom injection needs.
  • In the TypeListener that you are binding, search for the entered types for @AutoConfig occupied fields, and if they have any matching methods, bind these matching methods to MemberInjector or InjectionListener. You will probably want to tease the class literal from the annotation instance here and pass in the field and class as constructor arguments to MemberInjector / InjectionListener.
  • In MemberInjector or InjectionListener, you write, create an instance of the provider, and set the field to the instance provided by the provider.

This is a very powerful feature that allows you, for example, to automatically provide a configuration based on which instance you enter or based on the field name. However, use it carefully and document it, because it may not be reasonable for your colleagues that Guice provides an annotation other than @Inject . Also keep in mind that this will not work for constructor injection, so refactoring from field injection to constructor injection will cause Guis to complain that he does not have a binding to create an instance of the class.

+3
source

I had a similar problem. I wanted to use a custom annotation that gets an enum parameter to select an implementation. After much research, debugging 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 Binder Documentation

I tested Jeff Bowman's solution, but apparently it only works with suppliers

+1
source

Source: https://habr.com/ru/post/1213493/


All Articles