Here's a simplified version of my needs.
I have a program where every B object has its own C and D object, injected through Guice. In addition an A object is injected into every C and D objects.
What I want: that for each B object, its C and D objects will be injected with the same A object.
[Edit-Start]
(1) Guice supports "singleton" and "prototype" modes. However, what I need is something in between: I need A to be singleton WRT to a given B object (so that the C and D injected into a B object will share an A object). For another B object, I want another A. So it's a singleton but for a limited scope of the program (actually, a limited scope of a data structure).
(2) I don't mind a solution that uses method (setter)- or field- injection.
(3) I tried, several times, to achieve this and it always felt that I only need to implement some custom thingy of the DI container to make this work - but it never worked. Thus, I am looking for a detailed solution (not just "hand waving")
[Edit-End]
Specifically, I want the output of the program (below) to be:
Created C0 with [A0]
Created D0 with [A0]
Created B0 with [C0, D0]
Created C1 with [A1]
Created D1 with [A1]
Created B1 with [C1, D1]
Where it currently produces the following output:
Created C0 with [A0]
Created D0 with [A1] <-- Should be A0
Created B0 with [C0, D0]
Created C1 with [A2] <-- Should be A1
Created D1 with [A3] <-- Should be A1
Created B1 with [C1, D1]
I am expecting DI containers to allow this kind of customization but so far I had no luck in finding a solution. Below is my Guice-based code, but a Spring-based (or other DI containers-based) solution is welcome.
import java.util.Arrays;
import com.google.inject.*;
public class Main {
public static class Super {
private static Map<Class<?>,Integer> map = new HashMap<Class<?>,Integer>();
private Integer value;
public Super(Object... args) {
value = map.get(getClass());
value = value == null ? 0 : ++value;
map.put(getClass(), value);
if(args.length > 0)
System.out.println("Created " + this + " with " + Arrays.toString(args));
}
@Override
public final String toString() {
return "" + getClass().getSimpleName().charAt(0) + value;
}
}
public interface A { }
public static class AImpl extends Super implements A { }
public interface B { }
public static class BImpl extends Super implements B {
@Inject public BImpl(C c, D d) { super(c,d); }
}
public interface C { }
public static class CImpl extends Super implements C {
@Inject public CImpl(A a) { super(a); }
}
public interface D { }
public static class DImpl extends Super implements D {
@Inject public DImpl(A a) { super(a); }
}
public static class MyModule extends AbstractModule {
@Override
protected void configure() {
bind(A.class).to(AImpl.class);
bind(B.class).to(BImpl.class);
bind(C.class).to(CImpl.class);
bind(D.class).to(DImpl.class);
}
}
public static void main(String[] args) {
Injector inj = Guice.createInjector(new MyModule());
inj.getInstance(B.class);
inj.getInstance(B.class);
}
}
Here's one solution based on your original code - there are three changes:
This works because the singleton binding for A is now limited to each child injector.
[ Note: you could always cache the sub-module instance in a field of the
main-module if you don't want to keep creating it for each request of B ]
import java.util.*;
import com.google.inject.*;
public class Main {
public static class Super {
private static Map<Class<?>,Integer> map = new HashMap<Class<?>,Integer>();
private Integer value;
public Super(Object... args) {
value = map.get(getClass());
value = value == null ? 0 : ++value;
map.put(getClass(), value);
if(args.length > 0)
System.out.println("Created " + this + " with " + Arrays.toString(args));
}
@Override
public final String toString() {
return "" + getClass().getSimpleName().charAt(0) + value;
}
}
public interface A { }
public static class AImpl extends Super implements A { }
public interface B { }
public static class BImpl extends Super implements B {
@Inject public BImpl(C c, D d) { super(c,d); }
}
public interface C { }
public static class CImpl extends Super implements C {
@Inject public CImpl(A a) { super(a); }
}
public interface D { }
public static class DImpl extends Super implements D {
@Inject public DImpl(A a) { super(a); }
}
public static class MyModule extends AbstractModule {
@Override
protected void configure() {}
// >>>>>>>>
@Provides
B builder( Injector injector ) {
return injector.createChildInjector( new SubModule() ).getInstance( BImpl.class );
}
// <<<<<<<<
}
// >>>>>>>>
public static class SubModule extends AbstractModule {
@Override
protected void configure() {
bind(A.class).to(AImpl.class).in( Scopes.SINGLETON );
bind(C.class).to(CImpl.class);
bind(D.class).to(DImpl.class);
}
}
// <<<<<<<<
public static void main(String[] args) {
Injector inj = Guice.createInjector(new MyModule());
inj.getInstance(B.class);
inj.getInstance(B.class);
}
}
Now, I don't know if you absolutely need to have BImpl
, CImpl
and DImpl
created by Guice (to allow for AOP, for example), but if not this is simple:
public static class MyModule extends AbstractModule {
@Override
protected void configure() {
bind(A.class).to(AImpl.class);
}
@Provides
protected B provideB(A a) {
C c = new CImpl(a);
D d = new DImpl(a);
return new BImpl(c, d);
}
}
Alternatively (and I know you didn't specify this in your question), if you can bind each instance of B
that you use with a different binding annotation, you can use a private module like this, which you'd add once per binding annotation when creating your injector:
public static class MyOtherModule extends PrivateModule {
private final Annotation annotation;
public MyOtherModule(Annotation annotation) {
this.annotation = annotation;
}
@Override
protected void configure() {
bind(A.class).to(AImpl.class).in(Scopes.SINGLETON);
bind(C.class).to(CImpl.class);
bind(D.class).to(DImpl.class);
bind(B.class).annotatedWith(annotation).to(BImpl.class);
expose(B.class).annotatedWith(annotation);
}
}
main
for this looks like:
public static void main(String[] args) {
Injector inj = Guice.createInjector(new MyOtherModule(Names.named("first")),
new MyOtherModule(Names.named("second")));
inj.getInstance(Key.get(B.class, Names.named("first")));
inj.getInstance(Key.get(B.class, Names.named("second")));
}
I imagine there are other possibilities as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With