pombreda / google-guice

Automatically exported from code.google.com/p/google-guice
Apache License 2.0
0 stars 1 forks source link

Increase support for optional injection. #758

Open GoogleCodeExporter opened 9 years ago

GoogleCodeExporter commented 9 years ago
Setter/field injection already has the ability to say @Inject(optional = true), 
but it is impossible to reproduce this 'optional' behavior when messing with a 
TypeListener.

I'm writing a tiny piece of code that uses spring-security, but wanted to do 
injection via Guice. Many of the types have @Autowired setters and a few of 
them have "required = false", but technically would have those objects injected 
into them (from Spring).

So, to get this to work, I wanted to introduce a concept of an OptionalProvider 
which is a wrapper around a Provider that simply returns 'null' if the provider 
fails to supply a value.

Here's the code (it's a singular commit: 10 files, +116, -6): 
https://code.google.com/r/natebauernfeind-google-guice/source/detail?r=472fc384b
b6ade560188fee2d76ae90be3d233cb

Thanks,
Nate

Original issue reported on code.google.com by nate.bau...@gmail.com on 5 Aug 2013 at 5:05

GoogleCodeExporter commented 9 years ago
I've been able to use the current TypeListener to provide optional injection 
without any core changes (from the context of simulating Plexus on top of 
Guice). Basically it involved a utility method that could wrap the provider 
from the binder and turn it into a no-op provider. If necessary you can also 
defer specific lookups even more by requesting a provider for the injector and 
then using that later on to do additional lookups (although this does mean that 
you're then sacrificing Guice's early error detection).

Are you able to share your spring-integration code, or maybe reduce it down to 
an example of how this feature could be used?

Original comment by mccu...@gmail.com on 6 Aug 2013 at 2:06

GoogleCodeExporter commented 9 years ago
Just to warn you, this is my first stab at trying to get this to work (and I 
apologize for the Scala).

This is how it works prior to my changes:

/**
 * This helper class helps recreate optional injection for not-required auto-wired annotations.
 */
class OptArg[T] {
  @Inject(optional = true)
  private val t: T = null.asInstanceOf[T]
  def arg(): Option[T] = Option(t)
}

class SecurityModule extends AbstractModule {
  def configure() {
    /* ... a bunch of bindings ... */

    bindListener(Matchers.any(), new TypeListener {
      def hear[I](literal: TypeLiteral[I], encounter: TypeEncounter[I]) {
        def hearClass(cls: Class[_]) {
          for (method <- cls.getDeclaredMethods.filter(_.isAnnotationPresent(classOf[Autowired]))) {
            val memberAnno = Option(method.getAnnotation(classOf[Qualifier])).map(q => Names.named(q.value()))

            val anno = method.getAnnotation(classOf[Autowired])
            val params = method.getParameterTypes
            if (params.length == 1) {
              val provider = if (anno.required()) memberAnno match {
                case Some(a) => encounter.getProvider(Key.get(params(0), a))
                case None => encounter.getProvider(params(0))
              } else memberAnno match {
                case Some(a) => encounter.getProvider(Key.get(Types.newParameterizedType(classOf[OptArg[_]], params(0)), a))
                case None => encounter.getProvider(Key.get(Types.newParameterizedType(classOf[OptArg[_]], params(0))))
              }

              encounter.register(new MembersInjector[I]() {
                def injectMembers(instance: I) {
                  val member = provider.get()
                  if (anno.required()) {
                    method.invoke(instance, member.asInstanceOf[Object])
                  } else {
                    val optMember = member.asInstanceOf[OptArg[Object]]
                    optMember.arg().foreach(o => method.invoke(instance, o))
                  }
                }
              })
            }
          }

          if (cls.getSuperclass != null) hearClass(cls.getSuperclass)
        }

        hearClass(literal.getRawType)
      }
    })
  }
}

This, previous, piece of code was working fine (even if ugly) until I tried to 
add Spring's @Qualifier annotation which is basically equivalent to Guice's 
@Named annotation. So the problems are thus:
1) This doesn't work at all if I 'binder.requireExplicitBindings()' in my 
Module (because it needs to try and create an OptArg on the fly to do the 
optional injection, but it cannot). (I prefer to require explicit bindings as 
often as possible; this is the first time I've found that it doesn't do what I 
really want it to do.)
2) You can't automagically generate bindings for OptArg if it is annotated with 
an @Named annotation. Which, technically, makes a lot of sense because I 
actually want the field 't' to have the specific Named annotation and not the 
wrapper class.

Now with my change, the code changes to something like this:
class SecurityModule extends AbstractModule {
  def configure() {
    binder.requireExplicitBindings()

    /* ... a bunch of bindings ... */

    bindListener(Matchers.any(), new TypeListener {
      def hear[I](literal: TypeLiteral[I], encounter: TypeEncounter[I]) {
        def hearClass(cls: Class[_]) {
          for (method <- cls.getDeclaredMethods.filter(_.isAnnotationPresent(classOf[Autowired]))) {
            val memberAnno = Option(method.getAnnotation(classOf[Qualifier])).map(q => Names.named(q.value()))

            val anno = method.getAnnotation(classOf[Autowired])
            val params = method.getParameterTypes.asInstanceOf[Array[Class[AnyRef]]]
            def getProvider(k: Key[AnyRef]): Provider[AnyRef] = if (anno.required()) encounter.getProvider(k) else encounter.getOptionalProvider(k)

            if (params.length == 1) {
              val key = memberAnno.map(a => Key.get(params(0), a)).getOrElse(Key.get(params(0)))
              val provider = getProvider(key)

              encounter.register(new MembersInjector[I]() {
                def injectMembers(instance: I) {
                  Option(provider.get()).foreach(o => method.invoke(instance, o))
                }
              })
            }
          }

          if (cls.getSuperclass != null) hearClass(cls.getSuperclass)
        }

        hearClass(literal.getRawType)
      }
    })
  }
}

Now, notice there are some major benefits in the second implementation:
1) I can require explicit bindings, because optional providers do not throw 
errors if the provider cannot be resolved.
2) I can optionally inject objects that have bound annotations.
3) The code is cleaner without the need of a helper class nor do I need to cast 
to the helper class before pulling out the real value.

I hadn't thought about looking up the injections inside of the MembersInjector 
-- I had, apparently incorrectly, assumed that the injector was not yet 
'ready'. Is that your recommendation/preference?

Original comment by nate.bau...@gmail.com on 6 Aug 2013 at 5:40

GoogleCodeExporter commented 9 years ago
Hmm, yeah that seems to work just fine. For anyone reading I did this:

val injectorProvider = getProvider(classOf[Injector])
val provider = if (anno.required()) encounter.getProvider(k) else new 
Provider[AnyRef]() {
  def get(): AnyRef = try { injectorProvider.get().getInstance(k) } catch { case e: Exception => null }
}

Any chance you happen to know which injector I get? For example, if I have a 
private module and I save off a provider for the injector, can I use that 
injector to get access to non-exposed objects?

Original comment by nate.bau...@gmail.com on 6 Aug 2013 at 6:41

GoogleCodeExporter commented 9 years ago
To answer my own question -- Interestingly enough it gives you the local 
injector. In the PrivateModule case, it gives you an injector that can pull 
objects out that were not exposed! That's actually very exciting and simplifies 
an idea that I've had for a long time! I probably shouldn't be so surprised, 
but that sure makes me happy.

Feel free to close this issue request.

Original comment by nate.bau...@gmail.com on 6 Aug 2013 at 10:16

GoogleCodeExporter commented 9 years ago
Glad to hear you found a solution - btw, you might also be interested in the 
new ProvisionListener API that's available in the latest code and the recent 
4.0-beta release:

http://code.google.com/p/google-guice/issues/detail?id=78#c16
http://code.google.com/p/google-guice/source/browse/core/src/com/google/inject/s
pi/ProvisionListener.java
http://code.google.com/p/google-guice/source/browse/core/src/com/google/inject/B
inder.java#379

this provides additional functionality (and flexibility) over the TypeListener 
approach.

Original comment by mccu...@gmail.com on 8 Aug 2013 at 12:52

GoogleCodeExporter commented 9 years ago
Closing issue as requested.

Original comment by mccu...@gmail.com on 8 Aug 2013 at 12:53