I have a system which is using Spring for dependency injection. I use annotation-based autowiring. The beans are discovered by component scanning, i.e. my context XML contains this:
<context:component-scan base-package="org.example"/>
I have created a noddy example below to illustrate my problem.
There is a Zoo
which is a container for Animal
objects. The developer of Zoo
does not know which Animal
objects will be contained whilst he is developing Zoo
; the set of concrete Animal
objects instantiated by Spring is known at compile-time, but there are various build profiles resulting in various sets of Animal
s, and the code for Zoo
must not change under these circumstances.
The purpose of Zoo
is to allow other parts of the system (illustrated here as ZooPatron
) to access the set of Animal
objects at runtime, without needing to depend explicitly on certain Animal
s.
Actually, the concrete Animal
classes will all be contributed by various Maven artifacts. I want to be able to assemble a distribution of my project by simply depending on the various artifacts containing these concrete Animal
s, and have everything autowire correctly at compile-time.
I have attempted to solve this problem (unsuccessfully) by having the individual Animal
s depend upon the Zoo
, in order that they can call a registration method on the Zoo
during @PostConstruct
. This avoids the Zoo
depending explicitly on an explicit list of Animal
s.
The problem with this approach is that the customers of Zoo
wish to interact with it only when all the Animal
s have registered. There is a finite set of Animal
s which is known at compile-time, and the registration all happens during the Spring wiring phase of my lifecycle, so a subscription model should be unneccesary (i.e. I don't wish to add Animal
s to the Zoo
at runtime).
Unfortunately, all the customers of Zoo
simply depend upon Zoo
. This is exactly the same relationship which the Animal
s have with Zoo
. Therefore, the @PostConstruct
methods of the Animal
s and ZooPatron
are called in an arbitrary sequence. This is illustrated with the example code below - at the time @PostConstruct
is invoked on ZooPatron
, no Animal
s have registered, it is some milliseconds later when they all register.
So there are two types of dependency here, which I am struggling to express in Spring. The customers of Zoo
only want to use it once all the Animal
s are in it. (perhaps "Ark" would have been a better example...)
My question is basically: what is the best way to solve this problem?
@Component
public class Zoo {
private Set<Animal> animals = new HashSet<Animal>();
public void register(Animal animal) {
animals.add(animal);
}
public Collection<Animal> getAnimals() {
return animals;
}
}
public abstract class Animal {
@Autowired
private Zoo zoo;
@SuppressWarnings("unused")
@PostConstruct
private void init() {
zoo.register(this);
}
@Component
public static class Giraffe extends Animal {
}
@Component
public static class Monkey extends Animal {
}
@Component
public static class Lion extends Animal {
}
@Component
public static class Tiger extends Animal {
}
}
public class ZooPatron {
public ZooPatron(Zoo zoo) {
System.out.println("There are " + zoo.getAnimals().size()
+ " different animals.");
}
}
@Component
public class Test {
@Autowired
private Zoo zoo;
@SuppressWarnings("unused")
@PostConstruct
private void init() {
new Thread(new Runnable() {
private static final int ITERATIONS = 10;
private static final int DELAY = 5;
@Override
public void run() {
for (int i = 0; i<ITERATIONS; i++) {
new ZooPatron(zoo);
try {
Thread.sleep(DELAY);
} catch (InterruptedException e) {
// nop
}
}
}
}).start();
}
}
public class Main {
public static void main(String... args) {
new ClassPathXmlApplicationContext("/context.xml");
}
}
Output:
There are 0 different animals.
There are 3 different animals.
There are 4 different animals.
There are 4 different animals.
... etc
Basically the answer is: no, you cannot guarantee the order of @PostConstruct
calls without either going "outside" Spring or modifying its behaviour.
The real problem here was not that I wanted to sequence the @PostConstruct
invocations, that was merely a symptom of the dependencies being expressed incorrectly.
If the consumers of Zoo
depend upon him, and Zoo
in turn depends upon Animal
s, everything works correctly. My mistake was that I didn't want Zoo
to depend upon an explicit list of Animal
subclasses, and therefore introduced this registration method. As pointed out in the answers, mixing a self-registration mechanism with dependency injection will never work without unnecessary complexity.
The answer is to declare that Zoo
is dependent upon a collection of Animal
s, then allow Spring to populate the collection through auto-wiring.
Thus, there is no hard list of collection members, they are discovered by Spring, but the dependencies are correctly expressed and therefore the @PostConstruct
methods happen in the sequence I want.
Thanks for the excellent answers.
You might instead have the Set of Animals @Inject
ed into the Zoo.
@Component
public class Zoo {
@Inject
private Set<Animal> animals = new HashSet<Animal>();
// ...
}
Then Zoo's @PostConstruct
should only be called once all the Animals are injected. The only gotcha is that there must be at least one Animal in the system, but it doesn't sound like that should be an issue.
I don't think there is a way to ensure @PostConstruct order without introducing dependencies.
I think you're looking for trouble trying to mix injection or self registration. To some extent, @PostConstruct call order should not matter - if it does, it might not be the right tool for the job.
A couple ideas for your example
I don't think there is a 'right' answer, it all depends on your use case.
Reframe your problem so that it doesn't rely on invocation order.
The best way, IMO, is to avoid doing too much work during the construction of the object graph (just as in Java, you avoid doing too much work in the constructor), and to avoid calling methods from dependencies when you're not sure they're fully initialized yet.
If you just remove the @PostConstruct annotation from the Test#init()
method, and simply invoke it from your main method, after the context has been created, you won't have this problem anymore.
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