如果启用加载时织入,并且我用工厂方法创建它,Spring会认为我的后置处理器没有实现正确的接口

后端开发 2026-07-07

我尝试创建一个尽量精简的问题示例。为了把问题理解到足以开始尝试,我花了好几个小时调试。希望我理解正确。我们需要三份文件:

  1. 带有后处理器和主类的Java源文件。为了减少“不同实体”的数量,在这个示例中我把它们放在同一个类里;据我所知,这其实并不重要。
  2. 声明我们Bean和织入器的Spring XML文件
  3. pom.xml 用来声明我们的Spring依赖

Spring上下文的XML很短,我们就从那里开始。前几行是用于声明XML命名空间和模式之类的样板代码,末尾再有两行“payload”:

<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd"
>
    <bean class="MyPostProc" factory-method="getInstance"/>
    <context:load-time-weaver/>
</beans>

pom.xml 文件也很简短,我们只需要spring-context,这会把其他依赖也引入进来;Maven对模式和其他东西并不在意,因此省略它:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>scratch-dbg</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.39</version>
        </dependency>
    </dependencies>
</project>

最后,如果采用一种有些非常规的格式风格,我们的Java文件也相当短:

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
class MyPostProc implements BeanFactoryPostProcessor {
    public static void main(String... args) {
        System.out.println("about to try to load spring context");
        new ClassPathXmlApplicationContext("context.xml");
        System.out.println("done");
    }

    static MyPostProc getInstance() {return new MyPostProc();}
    MyPostProc() {System.out.println("reached constructor");}

    @Override public void
    postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        System.out.println("We can post process!"); // we never get here
    }
}

有趣的是,如果我们从XML文件中移除 factory-method=getInstance,让它改用构造函数,那么事情实际上又能正常工作;我没法判断这是设计上的还是一个bug,亦或其他原因。据我所知,这仅仅是Spring内部机制里的一个小怪癖,在很深处它会选择一条略有不同的代码路径。

关于根本原因的模糊感想

在决定是否创建一个bean之前,Spring想要了解它的类型的一些信息。具体来说,它想知道它实现了哪些接口。理论上,这很简单:只加载类,然后进行一个 instanceof 的检查。或者,实际情况,因为 instanceof 需要一个实例,而我们还没有创建,因此我们改为进行 Class.isAssignableFrom()。在没有织入时,这基本是Spring的做法。但是当存在织入时,Spring不想做一次完整的“常规”类加载,因为那样织入器就没有机会干涉字节码。

因此,它改用一个一次性(可丢弃)的类加载器来加载类。但这是件颇为微妙的事,因为 instanceof 与Java中的类似操作都依赖于类加载器。在花了太多时间盯着调试器后,我的理解是,一次性类加载器具有一个机制(被排除的类),它会告诉自己:“不要试着自己加载这个列表中的类;改为交给普通的类加载器加载。”当Spring XML指定使用构造函数创建bean时,这一机制被正确使用,Spring也得出结论:“是的,MyPostProc 确实实现了 BeanFactoryPostProcessor”。使用工厂方法时,它走了稍微不同的代码路径,接口没有被放到排除名单中,最终类型检查失败。

现在怎么办?

以上理解对吗?有文档说明吗?有没有可行的工作方法?我在执行单元测试时遇到了这个问题,但在生产环境里似乎没问题,这到底取决于什么?为什么Spring看起来这么难以理解?

解决方案

好吧,原来我忽略了一个关键事实:这在Java 16及以上版本才会出现。

为何这点重要:Spring尝试在“正常”路径上使用 Class.findLoadedClass() 来判断是否应该使用一次性加载器来加载。这个方法是 protected,所以Spring无法访问它……因此它尝试使用反射。(参见ContextTypeMatchClassLoader中的静态初始化块。)

在Java 15中,这默认会成功,但JVM会记录一个警告。到了Java 16,这默认会失败,但Spring捕获异常,并以“debug”级别记录,好像这没什么大不了的。因为它无法确定一次性加载器应该加载哪些类,所以它加载的类比应该加载的更多。于是这一切就崩溃了。

至于生产环境为何没有出错?原来那里已经在使用 --add-opens java.base/java.lang=ALL-UNNAMED,因此工作正常。解决办法是把这个也加到测试中。

或者,如果你的模块路径中已经有Spring,请改用它,而不是ALL-UNNAMED。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章