Figuring out what type you're injecting into

109 views
Skip to first unread message

Tim Boudreau

unread,
Jun 8, 2012, 12:59:01 PM6/8/12
to google-guice
I'm trying to figure out a way to determine the class name and package
of *what* is being injected for constructor injection, in time for a
Provider to give a different result based on that information.

Background: I'm helping out some folks who have a bunch of legacy
code which is littered with code that reads properties files from
disk. The goal is to migrate them to something where the
configuration could come from a central admin server or wherever; and
to get business logic out of the business of reading configuration
from anywhere.

I've done this sort of thing with @Named before, so it's no problem to
handle loading primitives from some layered stack of properties
objects which might or might not be the files on disk they're used to
using (and, for the first cut, probably will be).

Caveats are:
- In some cases a property file may still need to be available for
backward compatibility
- This is a small team, and I need to give them something
incrementally useful, not something where all the code must be
rewritten at once - they need to be able to evolve toward a better
approach

I've already taken care of how properties get *written* - you can
annotate classes with
@Defaults(value={"foo=bar"}, namespace=@Namespace("log-service"))
and an annotation processor will generate properties files in the
right spot on the classpath, and I can load and merge all such at
runtime (so we can start deleting things that have reasonable global
defaults and letting those settings live with the code that consumes
them). So figuring out what to inject for a given property name in a
given namespace is solved.

It's easy to solve the case of explicit requests for a namespaced
value - just use my own @Named clone with a namespace parameter:
Foo (@Value(name="foo-port", namespace=@Namespace("log-service"))
{ ... }

But, these guys are new to Guice and I want to give them a good, and
*non-verbose* experience. What seems optimal would be to be able to
add the
@Namespace("log-service")
on a class or package annotation, and my Guice plumbing will figure
out where to look for the values it injects.

So, that's where I'm stuck. Rightly, Guice can't hand me an instance
of an object that hasn't been created yet; and I know that sometimes
it will be a magic FastClassByGuice insta-subclass.

But it seems like there ought to be a way to get the metadata of the
type which will be injected, so that I could walk its hierarchy and
find an @Namespace annotation on its class or its package or its
package's parent packages.

Am I just missing something obvious?

-Tim

Tim Boudreau

unread,
Jun 8, 2012, 3:37:58 PM6/8/12
to google-guice
To make it clearer what I'm after, below is a test which, if it
passed, would mean the problem is solved.

The money shot is how getTheType() is implemented (it should be passed
whatever it actually needs). I've tried implementing
ProviderInstanceBinding and friends on PropsProvider, and introspected
what I get passed in a debugger - but nowhere have I found an object
with a reference to the type being injected into (InjectionPoint looks
promising, but I'm always passed an empty collection of them).

I'm guessing that the problem here is that Guice wants to be *told*
what to inject imperatively, and I want my code to be *asked* - which
Provider solves, but without enough metainfo to answer the question.

Since I am processing the annotation at compile time, I'm fine with
generating whatever's called for; and probably some sort horrible mix
of brute-force classpath scanning and custom scopes could be made to
do it. But it seems like there should be a clean, elegant way to do
this.

public class ValueBindingTest {
private static final class M extends AbstractModule {
private final Properties propsA = new Properties();
private final Properties propsB = new Properties();
private final Properties defaults = new Properties();

M() {
propsA.setProperty("a", "a");
propsB.setProperty("a", "b");
defaults.setProperty("a", "nothing");
}

public void configure() {
MyNamedImpl stringInjectionsOfA = new MyNamedImpl("a"); //
in real life we'd iterate all the keys

bind(String.class).annotatedWith(stringInjectionsOfA).toProvider(new
PropsProvider(stringInjectionsOfA.value()));
}

private Class<?> getTheType() {
//What to do here? I need the type of the object being
injected?
return InNamespaceTwo.class;
}

public Properties getProperties() {
Class<?> injectingInto = getTheType();
MyNamespace ns =
injectingInto.getAnnotation(MyNamespace.class);
Properties props = defaults; //fallback
if (ns != null) {
switch (ns.value()) {
case "one":
return propsA;
case "two":
return propsB;
}
}
return props;
}
class PropsProvider implements Provider<String> {
private final String key;

public PropsProvider(String key) {
this.key = key;
}

@Override
public String get() {
return getProperties().getProperty(key);
}
}
}
@Target(ElementType.PARAMETER)
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
@interface MyNamed {
String value();
}
private static class MyNamedImpl implements MyNamed {
private final String value;

public MyNamedImpl(String value) {
this.value = value;
}

@Override
public String value() {
return value;
}

@Override
public Class<? extends Annotation> annotationType() {
return MyNamed.class;
}

public boolean equals(Object o) {
System.out.println("Equals " + o);
return o instanceof MyNamed && value().equals(((MyNamed)
o).value());
}

public int hashCode() {
return (127 * "value".hashCode()) ^ value.hashCode();
}
}
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.FIELD,
ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
@interface MyNamespace {
String value();
}
@MyNamespace("one")
static class InNamespaceOne {
private final String value;

@Inject
public InNamespaceOne(@MyNamed("a") String value) {
this.value = value;
}
}
@MyNamespace("two")
static class InNamespaceTwo {
private final String value;

@Inject
public InNamespaceTwo(@MyNamed("a") String value) {
this.value = value;
}
}

static class NotNamespaced {
private final String value;

@Inject
public NotNamespaced(@MyNamed("a") String value) {
this.value = value;
}
}

@Test
public void test() {
Injector inj = Guice.createInjector(new M());
InNamespaceOne a = inj.getInstance(InNamespaceOne.class);
InNamespaceTwo b = inj.getInstance(InNamespaceTwo.class);
NotNamespaced c = inj.getInstance(NotNamespaced.class);
assertEquals("a", a.value);
assertEquals("b", b.value);
assertEquals("nothing", c.value);
}
}

Tim Boudreau

unread,
Jun 9, 2012, 12:58:20 AM6/9/12
to google...@googlegroups.com
Following a hunch, I did solve this - but I have no idea if the solution works by accident or if I should expect it to work in future versions of Guice.

What I did was simply:  Add a TypeListener which stores the TypeLiteral it is passed in a ThreadLocal;  read the ThreadLocal in Provider.get() and use getRawType to find the class and package to look for annotations on.

While less horrible than some things I imagined doing, it's less clear to me if it's actually correct or sane.  Would love it if someone could chime in with a few thoughts.

Thanks,

Tim

Tim Boudreau

unread,
Aug 14, 2012, 1:18:59 AM8/14/12
to google...@googlegroups.com
I ran into a problem with the above strategy today, which suggests it's not really the right way to do it.  So I'll renew my plea for any suggestions.

In a nutshell, what I'm after is a supported way to
 - In a Provider, get a Class object for the type being injected into (as in, if my provider being called to inject a parameter into an instance of Foo, I want to get Foo.class)
 - Look up an annotation on that *class* (not the parameter), or its Package (and similarly for interfaces and supertypes), and decide what the Provider should return based on that

The purpose is to allow legacy code which gets configuration from a variety of files to use injection with minimal code changes and minimal invasiveness - i.e. you just annotate the package to indicate where settings are loaded from, and the plumbing looks things up from the right place.

Any thoughts?

Thanks,

Tim

Wujek Srujek

unread,
Aug 14, 2012, 2:46:54 AM8/14/12
to google...@googlegroups.com
If I understood correctly, you could:
1. define a parameter of type InjectionPoint in your provider method
2. call injectionPoint.getMember().getDeclaringClass()

which should give you the class that declares the injection point. From the class, you can easily get the package.
Is that of any help?

wujek

--
You received this message because you are subscribed to the Google Groups "google-guice" group.
To view this discussion on the web visit https://groups.google.com/d/msg/google-guice/-/3OMFPpB9BXIJ.
To post to this group, send email to google...@googlegroups.com.
To unsubscribe from this group, send email to google-guice...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/google-guice?hl=en.

Tim Boudreau

unread,
Aug 14, 2012, 2:50:18 PM8/14/12
to google...@googlegroups.com
On Tuesday, August 14, 2012 2:46:54 AM UTC-4, wujek wrote:
If I understood correctly, you could:
1. define a parameter of type InjectionPoint in your provider method
2. call injectionPoint.getMember().getDeclaringClass()

which should give you the class that declares the injection point. From the class, you can easily get the package.
Is that of any help?

You mean, do something like this in the module class?

    @Provides
    public Settings getSettings(InjectionPoint ip) {
        TypeLiteral<?> type = ip.getDeclaringType();
        Namespace ns = findNamespace(type);
        String namespaceName = ns == null ? Namespace.DEFAULT : ns.value();
        return settingsForNamespace.get(namespaceName);
    }

 It sounds great - clean and elegant.  Alas, it doesn't work :
"Could not find a suitable constructor in com.google.inject.spi.InjectionPoint. Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private."

Maybe there is some other type that *could* be injected that would provide a path to the InjectionPoint?  InjectionPoint is definitely the thing I need to get hold of...

Thanks!

-Tim
Reply all
Reply to author
Forward
0 new messages