@PostConstruct方法被调用的顺序是否可以保证?

18

我有一个系统,使用Spring进行依赖注入。我使用基于注解的自动装配。组件扫描发现bean,即我的上下文XML包含以下内容:

<context:component-scan base-package="org.example"/>
我创建了一个简单的示例来说明我的问题。
有一个叫做“Zoo”的容器,可以容纳一些“Animal”对象。在开发“Zoo”时,开发人员并不知道会在其中包含哪些具体的“Animal”对象;由Spring实例化的具体“Animal”对象集合在编译时已知,但是有各种构建配置文件会导致不同的“Animal”集合,而在这些情况下,“Zoo”的代码必须不变。
“Zoo”的作用是允许系统中的其他部分(此处称为“ZooPatron”)在运行时访问“Animal”对象集,而不需要显式地依赖于某些“Animal”。
实际上,所有具体的“Animal”类都将由各种Maven工件贡献。我希望能够通过依赖于包含这些具体“Animal”的各种工件的不同工程,组装我的项目的发行版,并且在编译时自动连接所有东西。
我尝试通过使各个“Animal”依赖于“Zoo”来解决这个问题(但没有成功),以便它们可以在@PostConstruct期间调用“Zoo”上的注册方法。这避免了“Zoo”显式依赖于“Animal”的显式列表。
这种方法的问题在于,“Zoo”的客户端希望仅在所有“Animal”都注册时才与之交互。已知有限的“Animal”集合在编译时已知,并且所有注册都发生在我的生命周期的Spring连接阶段,因此订阅模型应该是不必要的(即我不希望在运行时将“Animal”添加到“Zoo”中)。
不幸的是,“Zoo”的所有客户端只依赖于“Zoo”。这与“Animal”对“Zoo”的关系完全相同。因此,在任意顺序调用“Animal”和“ZooPatron”的@PostConstruct方法。下面的示例代码说明了这一点-当在@PostConstruct上调用“ZooPatron”时,没有任何“Animal”已注册,但它们都会在几毫秒后注册。
因此,在这里有两种依赖关系,我很难在Spring中表达。 “Zoo”的客户端只想在其中包含所有“Animal”时使用它。(也许“Ark”会是一个更好的示例......)
我的问题基本上是:解决这个问题的最佳方法是什么?
@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");
    }

}

输出:

There are 0 different animals.
There are 3 different animals.
There are 4 different animals.
There are 4 different animals.
... etc

被接受解决方案的解释

基本上,答案是:不,你不能保证@PostConstruct调用的顺序,除非要么“越过”Spring或修改其行为。

真正的问题在于,我想要按顺序执行@PostConstruct方法并不是根本原因,这只是表明依赖关系被错误地表示了。

如果Zoo的消费者依赖于他,并且Zoo依次依赖于Animal,则一切都能正常工作。我的错误在于我不想让Zoo依赖于一个明确的Animal子类列表,因此引入了这种注册方法。正如答案中指出的那样,将自注册机制与依赖注入混合在一起将会带来不必要的复杂性。

答案是声明Zoo依赖于一个集合Animal,然后允许Spring通过自动装配填充集合。

因此,没有硬编码的集合成员列表,它们由Spring发现,但是依赖关系得到了正确的表示,因此@PostConstruct方法会按照我想要的顺序发生。

感谢各位出色的答案。


好的,我只是快速检查了一下...但我认为长颈鹿、猴子、狮子和老虎也应该有@PostConstruct。还有为什么要用静态? - Cygnusx1
这些具体的动物不需要PostConstruct,它们抽象父类的PostConstruct方法会被Spring正确调用。 - wool.in.silver
5个回答

4
你可以将动物的集合通过 @Inject 注入到动物园中。
@Component
public class Zoo {

    @Inject
    private Set<Animal> animals = new HashSet<Animal>();

    // ...
}

那么动物园的 @PostConstruct 只有在所有的动物都被注入后才会被调用。唯一需要注意的是系统中必须至少有一个动物,但听起来这应该不是问题。


是的,这是最好的想法,正如@ptyx所建议的那样。 - wool.in.silver

2

我认为在不引入依赖的情况下,无法确保@PostConstruct的顺序。

如果你尝试混合注入或自注册,那么你可能会自找麻烦。在某种程度上,@PostConstruct调用顺序并不重要——如果顺序很重要,那么它可能不是最合适的工具。

以下是您示例的几个想法:

  • 尝试在Zoo#animals上使用@Autowired:不需要动物进行自注册,同时动物园知道动物但反过来不行,这样更加清晰明了
  • 保持注册,但让外部参与者进行注册(有人将动物放在动物园里对吧?他们不会自己出现在入口处)
  • 如果您需要随时插入新的动物,但不想手动插入,请在zoo上进行更动态的访问器:不要存储动物列表,而是使用spring上下文获取所有现有接口的实例。

我认为没有“正确”的答案,一切都取决于您的用例。


我认为你说得很对 - 我找不到一种方法来使这个工作正常,而不会扭曲PostConstruct的目的。问题的根源在于我不希望任何人意识到特定的动物(这可能更多地与我的构建架构有关),如果我改变了动物集合,我不能有代码更改。只要在类路径上有一个动物就足以将其连接到系统中。然而,其他人需要在运行时知道这些动物的总集合。 - wool.in.silver
1
如果你的动物是豆子,那么使用一个集合自动装配。Spring会自动将所有声明的豆子放入一个自动装配的Map<String,Animal> animals中,你不需要在Zoo bean中逐个列出它们。 - ptyx
是的,自动装配集合是最好的解决方案。一旦正确表达了依赖关系,那么PostConstruct调用的顺序就不重要了。 - wool.in.silver

2

重新构思你的问题,使其不依赖于调用顺序。


是的,我现在明白了,代码中表达的依赖关系是反向的;修复这个问题意味着我不必依赖于调用顺序。 - wool.in.silver

1
在我看来,最好的方法是在对象图构建期间避免做太多的工作(就像在Java中,你避免在构造函数中做太多的工作),并且在你不确定依赖项是否完全初始化时避免调用依赖项的方法。
如果你只是从Test#init()方法中删除@PostConstruct注释,并在上下文创建后从你的主方法中简单地调用它,那么你将不再有这个问题。

也许我的示例代码过于简化了;实际上我的应用程序是一个Web应用程序,因此我有一个XmlWebApplicationContext,这要归功于DispatcherServlet。所以,我不确定我是否有这个机会。 - wool.in.silver
你仍然可以以懒加载的方式初始化你的Test类(或者它的真实名称),即在第一次调用其方法,或通过工厂访问它的时候。或者你也可以使用ServletContextListener来进行初始化。 - JB Nizet

0

在我看来,你的情况中动物园对象与所有动物类型之间存在依赖关系。如果你设计你的动物园对象来反映这种依赖关系,问题就解决了。 例如,你可以这样做:

<bean id="zoo" class="Zoo">
<property name="animals">
<list>
<ref bean="Monkey" />
<ref bean="Tiger" />
<ref bean="Lion" />
</list>
</property>
</bean>

可以尝试使用非 register 方法。


啊,不行 - 这正是我想要避免的 :-) ZooPatron和Zoo都不想有一个“硬性”的动物列表。实际上,现实世界的情况是我有一堆从各种Maven构件中贡献出来的游戏,以及系统'core'中的游戏注册表。只有游戏注册表对系统的其余部分可见,开发人员无法知道消费游戏注册表的开发人员,但在组装“分发”时,它们在编译时已知于Spring与其他组件。我将编辑q以澄清。 - wool.in.silver
你是对的:在问题领域中,动物园依赖于动物。以任何其他方式表达都会导致问题。我的愚蠢在于我假设我必须制作一个显式的动物列表,而Spring可以为我注入一个集合。 - wool.in.silver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接