How to bind with provider which uses annotation va

2019-02-19 09:17发布

问题:

Is there any way to bind with provider which interprets target's annotation value in Google Guice?

Example:

bind(Resource.class)
    .annotatedWith(MyAnnotation.class)
    .toProvider(new MyProvider<MyAnnotation, Resource>{
        public Resource get(MyAnnotation anno){
            return resolveResourceByAnnoValue(anno.value());
        }
    });

I want to initialize field of an Android Activity class by annotated binding. It should have to take multiple resources by it's unique Id.

Original Way:

public class TestActivity extends Activity{
    private TextView textView;
    private Button testButton;

    public void onAfterCreate(...){
        // set UI declaration resource.
        setContentView(R.layout.activity_test);

        // initialize fields, it must be done after setting ui definition.
        textView = (TextView) findViewById(R.id.textView);

        .... initialize other fields, hook them...

    ...
}

I want to bind UI and it's field in declarative way, not pragmatically likes above:

@ResourceID(R.layout.activity_test)
public class TestActivity extends InjectiveActivity{
    @ResourceID(R.id.textView) // Auto generated static resource id constant
    private TextView textView;

    @ResourceID(R.id.testButton)
    private Button testButton;

    ...
}

回答1:

This isn't possible as such.

If @MyAnnotation is a binding annotation, it will be compared using its equals method. @MyAnnotation(5) Resource will be bound to @MyAnnotation(5) Resource, and that will not match at all compared to @MyAnnotation(6) Resource. Check out this SO answer for more. As in that answer, you could loop through your possible annotation values and bind each one individually, if you feel like it.

If @MyAnnotation isn't a binding annotation, you won't be able to access it at all from your provider. As mentioned in this SO answer, it is a rejected feature to add injection-site information to the provider or dependency itself.

Your best bet is to create an @Assisted injection (or manual factory) to accept the parameter:

class MyConsumer {
  final Resource resource;
  @Inject MyConsumer(Resource.Factory resourceFactory) {
    int previouslyAnnotatedValue = 5;
    this.resource = resourceFactory.createWithValue(previouslyAnnotatedValue);
  }
}

You may also consider using Custom Injections, which will let you use an arbitrary annotation other than @Inject, which may use runtime annotation values however you'd like.



回答2:

Here is an example in Scala (I like using Scala for prototyping, it's Java in a different dress after all) which I came up with after wondering about it myself in Dynamic Google Juice injection depending on value of an annotation

import java.lang.reflect.{Constructor, Parameter}
import java.util.concurrent.atomic.AtomicReference
import javax.inject.{Inject, Named, Provider}

import com.google.inject.matcher.Matchers
import com.google.inject.spi.ProvisionListener.ProvisionInvocation
import com.google.inject.{AbstractModule, Binder, Guice}
import com.google.inject.spi.{DependencyAndSource, ProviderInstanceBinding, ProvisionListener}
import com.typesafe.config.ConfigFactory
import net.codingwell.scalaguice.InjectorExtensions._
import net.codingwell.scalaguice.ScalaModule

import scala.collection.JavaConverters._

object GuiceExperiments extends App {

  val injector = Guice.createInjector(new MyModule())

  val some = injector.instance[Some]

  println(some)

  some.go()
}

trait Some {
  def go(): Unit
}

class Impl @Inject()(
                    @Named("a.a.a") hello: String,
                    @Named("a.a.b") bello: String,
                    @Named("a.b.a") kello: String

                    ) extends Some {
  override def go() = {
    println(hello)
    println(bello)
    println(kello)
  }
}

abstract class DynamicProvider[T >: Null](binder: Binder) extends Provider[T] {

  private[this] val nextValue = new AtomicReference[T]

  binder.bindListener(Matchers.any(), new ProvisionListener {

    private[this] def tryProvide(target: DependencyAndSource): Unit = {
      val dependency = target.getDependency
      val injectionPoint = dependency.getInjectionPoint
      val parameterIndex = dependency.getParameterIndex

      injectionPoint.getMember match {
        case constructor: Constructor[_] =>
          val parameter = constructor.getParameters()(parameterIndex)
          nextValue.set(getFor(parameter))
      }
    }

    override def onProvision[V](provision: ProvisionInvocation[V]): Unit = {

      provision.getBinding match {
        case binding: ProviderInstanceBinding[_] if binding.getUserSuppliedProvider eq DynamicProvider.this =>
          provision.getDependencyChain.asScala.lastOption.foreach(tryProvide)
        case _ => ()
      }
    }
  })

  final override def get(): T = nextValue.getAndSet(null)

  def getFor(parameter: Parameter): T
}

class MyModule extends AbstractModule with ScalaModule {

  override def configure(): Unit = {

    bind[Some].to[Impl]

    bind[String].annotatedWith[Named].toProvider(new DynamicProvider[String](binder) {
      override def getFor(parameter: Parameter): String = {
        if (parameter.isAnnotationPresent(classOf[Named])) {
          parameter.getAnnotation(classOf[Named]).value()
        } else {
          null
        }
      }
    })

  }

}

this only inserts the value of the @Named, but looks like it pretty damn works. so much for not possible.