I have an existing object hierarchy where some objects have fields that need to be injected. Also there are some other objects that are constructed using Google Guice and need to be injected with references to some objects from previously described object hierarchy. How do I do such kind of injection with Guice?
The problem is that objects from existing hierarchy were not constructed using Guice, and therefore are not subject to inject process by default. There is, of course injector.injectMembers()
method that is able to inject into existing object instance, but it does not work on object hierarchies.
For those wondering why I can't build mentioned object hierarchy using Guice. This hierarchy represents GUI objects and is built by a GUI framework (Apache Pivot) from a declarative GUI description (in fact this process can be described as object deserialization). That way interface construction is rather simple, and I only want to inject certain service references into interface objects and vice versa (for callbacks).
Approach I am currently about to take is described below.
For injecting into preexisting object hierarchy just let all objects that are interested in injection imple开发者_StackOverflowment certain interface, like:
public interface Injectable {
void injectAll(Injector injector);
}
Those objects would then implement this interface like so:
public void injectAll(Injector injector) {
injector.injectMembers(this);
for (Injectable child : children)
child.injectAll(injector);
}
Then I'd just call mainWindow.injectAll(injector)
for root object in hierarchy and all objects of interest are injected.
Not very nice solution, but gets the work done on one side. On the other side, I need to inject objects from this hierarchy. I guess it can be done via implementing custom provider for such objects.
Is there a better solution to my problem? Maybe there is also something wrong with my approach?
This solution will work, but I'd like to propose a slightly different one to you.
Specifically, since you're going to traverse a deep object structure, this really looks like a job for the Visitor pattern. Also, what you're describing seems to call out for a two-stage injector: a "bootstrap" stage that can inject stuff needed by the pivot-created hierarchy (but can't inject any pivot-created elements) and a second stage that is the real injector used by your app (that can inject anything).
What I would suggest is this basic pattern: make a visitor that traverses the hierarchy and, as it goes, it does injection on those things that need it and records those things that need to be injected elsewhere. Then, when it is done visitng everything, it uses Injector.createChildInjector
to make a new Injector
that can inject stuff from the original Injector
and stuff from the pivot-created hierarchy.
First define a visitor that can hit everything in this hierarchy:
public interface InjectionVisitor {
void needsInjection(Object obj);
<T> void makeInjectable(Key<T> key, T instance);
}
Then define an interface for all your pivot-created elements:
public interface InjectionVisitable {
void acceptInjectionVisitor(InjectionVisitor visitor);
}
You'd implement this interface in your pivot-created classes as (assuming this code in the FooContainer
class):
public void acceptInjectionVisitor(InjectionVisitor visitor) {
visitor.needsInjection(this);
visitor.makeInjectable(Key.get(FooContainer.class), this);
for (InjectionVisitable child : children) {
child.acceptInjectionVisitor(visitor);
}
}
Note that the first two statements are optional - it may be that some objects in the pivot hierarchy don't need injection and it could also be that some of them you wouldn't want to have injectable later. Also, notice the use of Key
- this means that if you want some class to be injectable with a particular annotation you can do something like:
visitor.makeInjectable(Key.get(Foo.class, Names.named(this.getName())), this);
Now, how do you implement InjectionVisitor
? Here's how:
public class InjectionVisitorImpl implements InjectionVisitor {
private static class BindRecord<T> {
Key<T> key;
T value;
}
private final List<BindRecord<?>> bindings = new ArrayList<BindRecord<?>>();
private final Injector injector;
public InjectionVisitorImpl(Injector injector) {
this.injector = injector;
}
public void needsInjection(Object obj) {
injector.injectMemebers(obj);
}
public <T> void makeInjectable(Key<T> key, T instance) {
BindRecord<T> record = new BindRecord<T>();
record.key = key;
record.value = instance;
bindings.add(record);
}
public Injector createFullInjector(final Module otherModules...) {
return injector.createChildInjector(new AbstractModule() {
protected void configure() {
for (Module m : otherModules) { install(m); }
for (BindRecord<?> record : bindings) { handleBinding(record); }
}
private <T> handleBinding(BindRecord<T> record) {
bind(record.key).toInstance(record.value);
}
});
}
}
You then use this in your main
method as:
PivotHierarchyTopElement top = ...; // whatever you need to do to make that
Injector firstStageInjector = Guice.createInjector(
// here put all the modules needed to define bindings for stuff injected into the
// pivot hierarchy. However, don't put anything for stuff that needs pivot
// created things injected into it.
);
InjectionVisitorImpl visitor = new InjectionVisitorImpl(firstStageInjector);
top.acceptInjectionVisitor(visitor);
Injector fullInjector = visitor.createFullInjector(
// here put all your other modules, including stuff that needs pivot-created things
// injected into it.
);
RealMainClass realMain = fullInjector.getInstance(RealMainClass.class);
realMain.doWhatever();
Note that the way createChildInjector
works ensures that if you have any @Singleton
things bound in the stuff injected into the pivot hierarchy, you'll get the same instances injected by your real injector - the fullInjector
will delegate injectoion to the firstStageInjector
so long as the firstStageInjector
is able to handle the injection.
Edited to add: An interesting extension of this (if you want to delve into deep Guice magic) is to modify InjectionImpl
so that it records the place in your source code that called makeInjectable
. This then lets you get better error messages out of Guice when your code accidentally tells the visitor about two different things bound to the same key. To do this, you'd want to add a StackTraceElement
to BindRecord
, record the result of new RuntimeException().getStackTrace()[1]
inside the method makeInjectable
, and then change handleBinding
to:
private <T> handleBinding(BindRecord<T> record) {
binder().withSource(record.stackTraceElem).bind(record.key).toInstance(record.value);
}
You could inject MembersInjectors to inject nested fields. For example, this will deeply inject an existing Car instance:
public class Car {
Radio radio;
List<Seat> seats;
Engine engine;
public Car(...) {...}
@Inject void inject(RadioStation radioStation,
MembersInjector<Seat> seatInjector,
MembersInjector<Engine> engineInjector) {
this.radio.setStation(radioStation);
for (Seat seat : seats) {
seatInjector.injectMembers(seat);
}
engineInjector.injectMembers(engine);
}
}
public class Engine {
SparkPlug sparkPlug;
Turbo turbo
public Engine(...) {...}
@Inject void inject(SparkPlug sparkplug,
MembersInjector<Turbo> turboInjector) {
this.sparkPlug = sparkPlug;
turboInjector.injectMembers(turbo);
}
}
精彩评论