如果启用加载时织入,并且我用工厂方法创建它,Spring会认为我的后置处理器没有实现正确的接口
我尝试创建一个尽量精简的问题示例。为了把问题理解到足以开始尝试,我花了好几个小时调试。希望我理解正确。我们需要三份文件:
- 带有后处理器和主类的Java源文件。为了减少“不同实体”的数量,在这个示例中我把它们放在同一个类里;据我所知,这其实并不重要。
- 声明我们Bean和织入器的Spring XML文件
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。