【Spring】关于 Spring 中动态代理引发一些小问题的思考

73 阅读3分钟

【Spring】关于 Spring 中动态代理引发一些小问题的思考

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

在使用 Spring 异步任务 @Async 相关的内容时,遇到了一个小问题引发了一些思考,借此文做个总结

Async 场景

问题起源于如下一段测试代码:

@SpringBootApplication
public class DemoAopApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoAopApplication.class, args);
	}

	@Autowired
	ApplicationContext applicationContext;

	@Component
	public class TestRunner implements CommandLineRunner {

		@Override
		public void run(String... args) throws Exception {
			System.out.println(Thread.currentThread().getName());
			DemoAopApplication bean = applicationContext.getBean(DemoAopApplication.class);
			bean.test();
		}
	}

	@Async
	public void test() {
		System.out.println(Thread.currentThread().getName());
	}
}
  • 常规的 @EnabledAsync 配置类此处没有贴出来
  • 乍一看,这段代码没什么问题(至少我这么觉得。。),应该输出主线程名和异步线程名,但实际上抛出了 NPE,为空的正是主类的属性 ApplicationContext applicationContext
  • 事实上,在主类中注入属性 ApplicationContext applicationContext 的行为本身肯定是没问题的,执行 CommandLineRunner 的阶段也是在 refresh 阶段之后,所有 bean 包括主类 bean 的创建肯定都是已经完成的
  • Runner 类中 debug 发现对应的主类 DemoAopApplication 是一个代理类(因为它有个 @Async 方法嘛)
  • 于是注释 @Async 方法,NPE 解决
    NPE

Aspect 场景

同理,如果主类被 Aspect 切了,也是这样的现象

@Component
@Aspect
public class AspectTest {

    @Pointcut("execution(* com.example.demoaop.*.*())")
    public void pointCut() {

    }

    @Before("pointCut()")
    public void before() {
        System.out.println("proxy");
    }
}
  • 注释掉 @Async 的方法,加入切面配置类如上,扫描路径要覆盖主类(并记得给主类写个符合切面条件的方法)
  • 则同样复现上述场景,至此总结这个现象为:
  • 一个 bean 创建的大致流程是 Instantiation(实例化) -> (属性赋值) -> Initialization(初始化),而上述两种代理的场景均发生在 postProcessAfterInitialization 阶段,即 Initialization(初始化) 阶段之后,那代理对象必然是没有属性赋值的
  • 了解问题症结后,继续如下场景

Configuration 场景

于是在去掉 @Async@Aspect 场景下再做一次验证,可以发现成功获取属性,但主类仍然是个代理类:
代理类

  • 注释掉 @Async,去掉切面类,Runner 类成功访问到主类的 applicationContext 属性,但可见此处的主类仍旧是个代理类,这个现象是这样的:
  • 主类由注解 @SpringBootApplication 标记,其由元注解 @Configuration 标记,而在容器启动的 postProcessBeanFactory 阶段,会对所有 @Configuration(FULL 配置类) 进行 enhancer 增强:进行一次代理使这些配置类中的方法调用指向容器
  • 上述代理行为发生在 postProcessBeanFactory(bean 工厂的后置处理) 阶段,此时还未开始容器中的 bean 的创建,因此 enhancer 后的主类也在后续生命周期阶段被成功赋值了 applicationContext 属性
  • 可以使用 @ConfigurationproxyBeanMethods = false 属性来关闭 enhancer 行为,关闭后下图如预期:
    关闭 enhancer

总结

起初乍一下遇到一个本不该出现的问题,内心还蛮困惑,但经过排查梳理后清晰了一点,同时也总结回顾了一些知识点,写此文分享