From 93ec0a52ec9d43659a538588f442d8844bcad4ae Mon Sep 17 00:00:00 2001 From: qinzy Date: Fri, 24 May 2024 18:59:52 +0800 Subject: [PATCH] =?UTF-8?q?05=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...70\350\247\201\351\224\231\350\257\257.md" | 256 ++++++++++++++++ ...57\357\274\210\344\270\212\357\274\211.md" | 282 ++++++++++++++++++ zh-cn/geek courses/spring/README.md | 6 +- 3 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 "zh-cn/geek courses/spring/04 Spring Bean \347\224\237\345\221\275\345\221\250\346\234\237\345\270\270\350\247\201\351\224\231\350\257\257.md" create mode 100644 "zh-cn/geek courses/spring/05 Spring AOP \345\270\270\350\247\201\351\224\231\350\257\257\357\274\210\344\270\212\357\274\211.md" diff --git "a/zh-cn/geek courses/spring/04 Spring Bean \347\224\237\345\221\275\345\221\250\346\234\237\345\270\270\350\247\201\351\224\231\350\257\257.md" "b/zh-cn/geek courses/spring/04 Spring Bean \347\224\237\345\221\275\345\221\250\346\234\237\345\270\270\350\247\201\351\224\231\350\257\257.md" new file mode 100644 index 00000000..b27dcd6e --- /dev/null +++ "b/zh-cn/geek courses/spring/04 Spring Bean \347\224\237\345\221\275\345\221\250\346\234\237\345\270\270\350\247\201\351\224\231\350\257\257.md" @@ -0,0 +1,256 @@ +你好,我是傅健,这节课我们来聊一聊 Spring Bean 的初始化过程及销毁过程中的一些问题。 + +虽然说 Spring 容器上手简单,可以仅仅通过学习一些有限的注解,即可达到快速使用的目的。但在工程实践中,我们依然会从中发现一些常见的错误。尤其当你对 Spring 的生命周期还没有深入了解时,类初始化及销毁过程中潜在的约定就不会很清楚。 + +这会导致这样一些状况发生:有些错误,我们可以在 Spring 的异常提示下快速解决,但却不理解背后的原理;而另一些错误,并不容易在开发环境下被发现,从而在产线上造成较为严重的后果。 + +接下来我们就具体解析下这些常见案例及其背后的原理。 + +## 案例 1:构造器内抛空指针异常 + +先看个例子。在构建宿舍管理系统时,有 LightMgrService 来管理 LightService,从而控制宿舍灯的开启和关闭。我们希望在 LightMgrService 初始化时能够自动调用 LightService 的 check 方法来检查所有宿舍灯的电路是否正常,代码如下: + +```java +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class LightMgrService { @Autowired private LightService lightService; public LightMgrService() { lightService.check(); } } +``` + +我们在 LightMgrService 的默认构造器中调用了通过 @Autoware 注入的成员变量 LightService 的 check 方法: + +```csharp +@Service public class LightService { public void start() { System.out.println("turn on all lights"); } public void shutdown() { System.out.println("turn off all lights"); } public void check() { System.out.println("check all lights"); } } +``` + +以上代码定义了 LightService 对象的原始类。 + +从整个案例代码实现来看,我们的期待是在 LightMgrService 初始化过程中,LightService 因为标记为 @Autowired,所以能被自动装配好;然后在 LightMgrService 的构造器执行中,LightService 的 shutdown() 方法能被自动调用;最终打印出 check all lights。 + +然而事与愿违,我们得到的只会是 NullPointerException,错误示例如下: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/496f362c7239432d8f35c2b01e263ba9.jpg) + +这是为什么呢? + +### 案例解析 + +显然这是新手最常犯的错误,但是问题的根源,是我们**对Spring类初始化过程没有足够的了解**。下面这张时序图描述了 Spring 启动时的一些关键结点: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/69de7d4cd2dc4b3084d219acfd3b594d.jpg) + +这个图初看起来复杂,我们不妨将其分为三部分: + +- 第一部分,将一些必要的系统类,比如 Bean 的后置处理器类,注册到 Spring 容器,其中就包括我们这节课关注的 CommonAnnotationBeanPostProcessor 类; +- 第二部分,将这些后置处理器实例化,并注册到 Spring 的容器中; +- 第三部分,实例化所有用户定制类,调用后置处理器进行辅助装配、类初始化等等。 + +第一部分和第二部分并非是我们今天要讨论的重点,这里仅仅是为了让你知道 CommonAnnotationBeanPostProcessor 这个后置处理类是何时被 Spring 加载和实例化的。 + +**这里我顺便给你拓展两个知识点:** + +1. 很多必要的系统类,尤其是 Bean 后置处理器(比如CommonAnnotationBeanPostProcessor、AutowiredAnnotationBeanPostProcessor 等),都是被 Spring 统一加载和管理的,并在 Spring 中扮演了非常重要的角色; +2. 通过 Bean 后置处理器,Spring 能够非常灵活地在不同的场景调用不同的后置处理器,比如接下来我会讲到示例问题如何修正,修正方案中提到的 PostConstruct 注解,它的处理逻辑就需要用到 CommonAnnotationBeanPostProcessor(继承自 InitDestroyAnnotationBeanPostProcessor)这个后置处理器。 + +现在我们重点看下第三部分,即 Spring 初始化单例类的一般过程,基本都是 getBean()->doGetBean()->getSingleton(),如果发现 Bean 不存在,则调用 createBean()->doCreateBean() 进行实例化。 + +查看 doCreateBean() 的源代码如下: + +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { //省略非关键代码 if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } final Object bean = instanceWrapper.getWrappedInstance(); //省略非关键代码 Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } catch (Throwable ex) { //省略非关键代码 } +``` + +上述代码完整地展示了 Bean 初始化的三个关键步骤,按执行顺序分别是第 5 行的 createBeanInstance,第 12 行的 populateBean,以及第 13 行的 initializeBean,分别对应实例化 Bean,注入 Bean 依赖,以及初始化 Bean (例如执行 @PostConstruct 标记的方法 )这三个功能,这也和上述时序图的流程相符。 + +而用来实例化 Bean 的 createBeanInstance 方法通过依次调用DefaultListableBeanFactory.instantiateBean() >SimpleInstantiationStrategy.instantiate(),最终执行到 BeanUtils.instantiateClass(),其代码如下: + +```scss +public static T instantiateClass(Constructor ctor, Object... args) throws BeanInstantiationException { Assert.notNull(ctor, "Constructor must not be null"); try { ReflectionUtils.makeAccessible(ctor); return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args)); } catch (InstantiationException ex) { throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex); } //省略非关键代码 } +``` + +这里因为当前的语言并非 Kotlin,所以最终将调用 ctor.newInstance() 方法实例化用户定制类 LightMgrService,而默认构造器显然是在类实例化的时候被自动调用的,Spring 也无法控制。而此时负责自动装配的 populateBean 方法还没有被执行,LightMgrService 的属性 LightService 还是 null,因而得到空指针异常也在情理之中。 + +### 问题修正 + +通过源码分析,现在我们知道了问题的根源,就是在于**使用 @Autowired 直接标记在成员属性上而引发的装配行为是发生在构造器执行之后的**。所以这里我们可以通过下面这种修订方法来纠正这个问题: + +```java +@Component public class LightMgrService { private LightService lightService; public LightMgrService(LightService lightService) { this.lightService = lightService; lightService.check(); } } +``` + +在[第02课](https://time.geekbang.org/column/article/366170)的案例 2 中,我们就提到了构造器参数的隐式注入。当使用上面的代码时,构造器参数 LightService 会被自动注入LightService 的 Bean,从而在构造器执行时,不会出现空指针。可以说,**使用构造器参数来隐式注入是一种 Spring 最佳实践**,因为它成功地规避了案例1中的问题。 + +另外,除了这种纠正方式,有没有别的方式? + +实际上,Spring 在类属性完成注入之后,会回调用户定制的初始化方法。即在 populateBean 方法之后,会调用 initializeBean 方法,我们来看一下它的关键代码: + +```typescript +protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { //省略非关键代码 if (mbd == null || !mbd.isSynthetic()) { wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); } try { invokeInitMethods(beanName, wrappedBean, mbd); } //省略非关键代码 } +``` + +这里你可以看到 applyBeanPostProcessorsBeforeInitialization 和 invokeInitMethods 这两个关键方法的执行,它们分别处理了 @PostConstruct 注解和 InitializingBean 接口这两种不同的初始化方案的逻辑。这里我再详细地给你讲讲。 + +**1\. applyBeanPostProcessorsBeforeInitialization 与@PostConstruct** + +applyBeanPostProcessorsBeforeInitialization 方法最终执行到后置处理器 InitDestroyAnnotationBeanPostProcessor 的 buildLifecycleMetadata 方法(CommonAnnotationBeanPostProcessor 的父类): + +```java +private LifecycleMetadata buildLifecycleMetadata(final Class clazz) { //省略非关键代码 do { //省略非关键代码 final List currDestroyMethods = new ArrayList<>(); ReflectionUtils.doWithLocalMethods(targetClass, method -> { //此处的 this.initAnnotationType 值,即为 PostConstruct.class if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) { LifecycleElement element = new LifecycleElement(method); currInitMethods.add(element); //非关键代码 } +``` + +在这个方法里,Spring 将遍历查找被 PostConstruct.class 注解过的方法,返回到上层,并最终调用此方法。 + +**2\. invokeInitMethods 与 InitializingBean 接口** + +invokeInitMethods 方法会判断当前 Bean 是否实现了 InitializingBean 接口,只有在实现了该接口的情况下,Spring 才会调用该 Bean 的接口实现方法 afterPropertiesSet()。 + +```java +protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd) throws Throwable { boolean isInitializingBean = (bean instanceof InitializingBean); if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { // 省略非关键代码 else { ((InitializingBean) bean).afterPropertiesSet(); } } // 省略非关键代码 } +``` + +学到此处,答案也就呼之欲出了。我们还有两种方式可以解决此问题。 + +1. 添加 init 方法,并且使用 PostConstruct 注解进行修饰: + +```typescript +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class LightMgrService { @Autowired private LightService lightService; @PostConstruct public void init() { lightService.check(); } } +``` + +1. 实现 InitializingBean 接口,在其 afterPropertiesSet() 方法中执行初始化代码: + +```java +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class LightMgrService implements InitializingBean { @Autowired private LightService lightService; @Override public void afterPropertiesSet() throws Exception { lightService.check(); } } +``` + +对比最开始提出的解决方案,很明显,针对本案例而言,后续的两种方案并不是最优的。但是在一些场景下,这两种方案各有所长,不然 Spring 为什么要提供这个功能呢?对吧! + +## 案例 2:意外触发 shutdown 方法 + +上述实例我给你讲解了类初始化时最容易遇到的问题,同样,在类销毁时,也会有一些相对隐蔽的约定,导致一些难以察觉的错误。 + +接下来,我们再来看一个案例,还是沿用之前的场景。这里我们可以简单复习一下LightService 的实现,它包含了 shutdown 方法,负责关闭所有的灯,关键代码如下: + +```typescript +import org.springframework.stereotype.Service; @Service public class LightService { //省略其他非关键代码 public void shutdown(){ System.out.println("shutting down all lights"); } //省略其他非关键代码 } +``` + +在之前的案例中,如果我们的宿舍管理系统在重启时,灯是不会被关闭的。但是随着业务的需求变化,我们可能会去掉 @Service 注解,而是使用另外一种产生 Bean 的方式:创建一个配置类 BeanConfiguration(标记 @Configuration)来创建一堆 Bean,其中就包含了创建 LightService 类型的 Bean,并将其注册到 Spring 容器: + +```kotlin +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BeanConfiguration { @Bean public LightService getTransmission(){ return new LightService(); } } +``` + +复用案例 1 的启动程序,稍作修改,让 Spring 启动完成后立马关闭当前 Spring 上下文。这样等同于模拟宿舍管理系统的启停: + +```typescript +@SpringBootApplication public class Application { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); context.close(); } } +``` + +以上代码没有其他任何方法的调用,仅仅是将所有符合约定的类初始化并加载到 Spring 容器,完成后再关闭当前的 Spring 容器。按照预期,这段代码运行后不会有任何的 log 输出,毕竟我们只是改变了 Bean 的产生方式。 + +但实际运行这段代码后,我们可以看到控制台上打印了 shutting down all lights。显然 shutdown 方法未按照预期被执行了,这导致一个很有意思的 bug:在使用新的 Bean 生成方式之前,每一次宿舍管理服务被重启时,宿舍里所有的灯都不会被关闭。但是修改后,只有服务重启,灯都被意外关闭了。如何理解这个 bug? + +### 案例解析 + +通过调试,我们发现只有通过使用 Bean 注解注册到 Spring 容器的对象,才会在 Spring 容器被关闭的时候自动调用 shutdown 方法,而使用 @Component(Service 也是一种 Component)将当前类自动注入到 Spring 容器时,shutdown 方法则不会被自动执行。 + +我们可以尝试到 Bean 注解类的代码中去寻找一些线索,可以看到属性 destroyMethod 有非常大段的注释,基本上解答了我们对于这个问题的大部分疑惑。 + +使用 Bean 注解的方法所注册的 Bean 对象,如果用户不设置 destroyMethod 属性,则其属性值为 AbstractBeanDefinition.INFER\_METHOD。此时 Spring 会检查当前 Bean 对象的原始类中是否有名为 shutdown 或者 close 的方法,如果有,此方法会被 Spring 记录下来,并在容器被销毁时自动执行;当然如若没有,那么自然什么都不会发生。 + +下面我们继续查看 Spring 的源代码来进一步分析此问题。 + +首先我们可以查找 INFER\_METHOD 枚举值的引用,很容易就找到了使用该枚举值的方法 DisposableBeanAdapter#inferDestroyMethodIfNecessary: + +```typescript +private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) { String destroyMethodName = beanDefinition.getDestroyMethodName(); if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||(destroyMethodName == null && bean instanceof AutoCloseable)) { if (!(bean instanceof DisposableBean)) { try { //尝试查找 close 方法 return bean.getClass().getMethod(CLOSE_METHOD_NAME).getName(); } catch (NoSuchMethodException ex) { try { //尝试查找 shutdown 方法 return bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName(); } catch (NoSuchMethodException ex2) { // no candidate destroy method found } } } return null; } return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null); } +``` + +我们可以看到,代码逻辑和 Bean 注解类中对于 destroyMethod 属性的注释完全一致destroyMethodName 如果等于 INFER\_METHOD,且当前类没有实现 DisposableBean 接口,那么首先查找类的 close 方法,如果找不到,就在抛出异常后继续查找 shutdown 方法;如果找到了,则返回其方法名(close 或者 shutdown)。 + +接着,继续逐级查找引用,最终得到的调用链从上到下为 doCreateBean->registerDisposableBeanIfNecessary->registerDisposableBean(new DisposableBeanAdapter)->inferDestroyMethodIfNecessary。 + +然后,我们追溯到了顶层的 doCreateBean 方法,代码如下: + +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { //省略非关键代码 if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } //省略非关键代码 // Initialize the bean instance. Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } //省略非关键代码 // Register bean as disposable. try { registerDisposableBeanIfNecessary(beanName, bean, mbd); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); } return exposedObject; } +``` + +到这,我们就可以对 doCreateBean 方法做一个小小的总结了。可以说 **doCreateBean 管理了Bean的整个生命周期中几乎所有的关键节点**,直接负责了 Bean 对象的生老病死,其主要功能包括: + +- Bean 实例的创建; +- Bean 对象依赖的注入; +- 定制类初始化方法的回调; +- Disposable 方法的注册。 + +接着,继续查看 registerDisposableBean 方法: + +```typescript +public void registerDisposableBean(String beanName, DisposableBean bean) { //省略其他非关键代码 synchronized (this.disposableBeans) { this.disposableBeans.put(beanName, bean); } //省略其他非关键代码 } +``` + +在 registerDisposableBean 方法内,DisposableBeanAdapter 类(其属性destroyMethodName 记录了使用哪种 destory 方法)被实例化并添加到 DefaultSingletonBeanRegistry#disposableBeans 属性内,disposableBeans 将暂存这些 DisposableBeanAdapter 实例,直到 AnnotationConfigApplicationContext 的 close 方法被调用。 + +而当 AnnotationConfigApplicationContext 的 close 方法被调用时,即当 Spring 容器被销毁时,最终会调用到 DefaultSingletonBeanRegistry#destroySingleton。此方法将遍历 disposableBeans 属性逐一获取 DisposableBean,依次调用其中的 close 或者 shutdown 方法: + +```typescript +public void destroySingleton(String beanName) { // Remove a registered singleton of the given name, if any. removeSingleton(beanName); // Destroy the corresponding DisposableBean instance. DisposableBean disposableBean; synchronized (this.disposableBeans) { disposableBean = (DisposableBean) this.disposableBeans.remove(beanName); } destroyBean(beanName, disposableBean); } +``` + +很明显,最终我们的案例调用了 LightService#shutdown 方法,将所有的灯关闭了。 + +### 问题修正 + +现在,我们已经知道了问题的根源,解决起来就非常简单了。 + +我们可以通过**避免在Java类中定义一些带有特殊意义动词的方法来解决**,当然如果一定要定义名为 close 或者 shutdown 方法,也可以通过将 Bean 注解内 destroyMethod 属性设置为空的方式来解决这个问题。 + +第一种修改方式比较简单,所以这里只展示第二种修改方式,代码如下: + +```kotlin +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BeanConfiguration { @Bean(destroyMethod="") public LightService getTransmission(){ return new LightService(); } } +``` + +另外,针对这个问题我想再多提示一点。如果我们能**养成良好的编码习惯**,在使用某个不熟悉的注解之前,认真研读一下该注解的注释,也可以大概率规避这个问题。 + +不过说到这里,你也可能还是会疑惑,为什么 @Service 注入的 LightService,其 shutdown 方法不能被执行?这里我想补充说明下。 + +想要执行,则必须要添加 DisposableBeanAdapter,而它的添加是有条件的: + +```typescript +protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) { AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null); if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) { if (mbd.isSingleton()) { // Register a DisposableBean implementation that performs all destruction // work for the given bean: DestructionAwareBeanPostProcessors, // DisposableBean interface, custom destroy method. registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc)); } else { //省略非关键代码 } } } +``` + +参考上述代码,关键的语句在于: + +> !mbd.isPrototype() && requiresDestruction(bean, mbd) + +很明显,在案例代码修改前后,我们都是单例,所以区别仅在于是否满足requiresDestruction 条件。翻阅它的代码,最终的关键调用参考DisposableBeanAdapter#hasDestroyMethod: + +```typescript +public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) { if (bean instanceof DisposableBean || bean instanceof AutoCloseable) { return true; } String destroyMethodName = beanDefinition.getDestroyMethodName(); if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)) { return (ClassUtils.hasMethod(bean.getClass(), CLOSE_METHOD_NAME) || ClassUtils.hasMethod(bean.getClass(), SHUTDOWN_METHOD_NAME)); } return StringUtils.hasLength(destroyMethodName); } +``` + +如果我们是使用 @Service 来产生 Bean 的,那么在上述代码中我们获取的destroyMethodName 其实是 null;而使用 @Bean 的方式,默认值为AbstractBeanDefinition.INFER\_METHOD,参考 Bean 的定义: + +```scss +public @interface Bean { //省略其他非关键代码 String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; } +``` + +继续对照代码,你就会发现 @Service 标记的 LightService 也没有实现 AutoCloseable、DisposableBean,最终没有添加一个 DisposableBeanAdapter。所以最终我们定义的 shutdown 方法没有被调用。 + +## 重点回顾 + +通过以上两个案例,相信你对 Spring 生命周期,尤其是对于 Bean 的初始化和销毁流程已经有了一定的了解。这里带你再次回顾下重点: + +1. DefaultListableBeanFactory 类是 Spring Bean 的灵魂,而核心就是其中的 doCreateBean 方法,它掌控了 Bean 实例的创建、Bean 对象依赖的注入、定制类初始化方法的回调以及 Disposable 方法的注册等全部关键节点。 +2. 后置处理器是 Spring 中最优雅的设计之一,对于很多功能注解的处理都是借助于后置处理器来完成的。虽然这节课对其没有过多介绍,但在第一个案例中,Bean 对象“补充”初始化动作却是在 CommonAnnotationBeanPostProcessor(继承自 InitDestroyAnnotationBeanPostProcessor)这个后置处理器中完成的。 + +## 思考题 + +案例 2 中的类 LightService,当我们不在 Configuration 注解类中使用 Bean 方法将其注入 Spring 容器,而是坚持使用 @Service 将其自动注入到容器,同时实现 Closeable 接口,代码如下: + +```scss +import org.springframework.stereotype.Component; import java.io.Closeable; @Service public class LightService implements Closeable { public void close() { System.out.println("turn off all lights); } //省略非关键代码 } +``` + +接口方法 close() 也会在 Spring 容器被销毁的时候自动执行么? + +我在留言区期待你的答案! \ No newline at end of file diff --git "a/zh-cn/geek courses/spring/05 Spring AOP \345\270\270\350\247\201\351\224\231\350\257\257\357\274\210\344\270\212\357\274\211.md" "b/zh-cn/geek courses/spring/05 Spring AOP \345\270\270\350\247\201\351\224\231\350\257\257\357\274\210\344\270\212\357\274\211.md" new file mode 100644 index 00000000..d84a0259 --- /dev/null +++ "b/zh-cn/geek courses/spring/05 Spring AOP \345\270\270\350\247\201\351\224\231\350\257\257\357\274\210\344\270\212\357\274\211.md" @@ -0,0 +1,282 @@ +你好,我是傅健。这节课开始,我们聊聊Spring AOP使用中常遇到的一些问题。 + +Spring AOP是Spring中除了依赖注入外(DI)最为核心的功能,顾名思义,AOP即Aspect Oriented Programming,翻译为面向切面编程。 + +而Spring AOP则利用CGlib和JDK动态代理等方式来实现运行期动态方法增强,其目的是将与业务无关的代码单独抽离出来,使其逻辑不再与业务代码耦合,从而降低系统的耦合性,提高程序的可重用性和开发效率。因而AOP便成为了日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用的技术。 + +追根溯源,我们之所以能无感知地在容器对象方法前后任意添加代码片段,那是由于Spring在运行期帮我们把切面中的代码逻辑动态“织入”到了容器对象方法内,所以说**AOP本质上就是一个代理模式**。然而在使用这种代理模式时,我们常常会用不好,那么这节课我们就来解析下有哪些常见的问题,以及背后的原理是什么。 + +## 案例1:this调用的当前类方法无法被拦截 + +假设我们正在开发一个宿舍管理系统,这个模块包含一个负责电费充值的类ElectricService,它含有一个充电方法charge(): + +```csharp +@Service public class ElectricService { public void charge() throws Exception { System.out.println("Electric charging ..."); this.pay(); } public void pay() throws Exception { System.out.println("Pay with alipay ..."); Thread.sleep(1000); } } +``` + +在这个电费充值方法charge()中,我们会使用支付宝进行充值。因此在这个方法中,我加入了pay()方法。为了模拟pay()方法调用耗时,代码执行了休眠1秒,并在charge()方法里使用 this.pay()的方式调用这种支付方法。 + +但是因为支付宝支付是第三方接口,我们需要记录下接口调用时间。这时候我们就引入了一个@Around的增强 ,分别记录在pay()方法执行前后的时间,并计算出执行pay()方法的耗时。 + +```java +@Aspect @Service @Slf4j public class AopConfig { @Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) ") public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); joinPoint.proceed(); long end = System.currentTimeMillis(); System.out.println("Pay method time cost(ms): " + (end - start)); } } +``` + +最后我们再通过定义一个Controller来提供电费充值接口,定义如下: + +```java +@RestController public class HelloWorldController { @Autowired ElectricService electricService; @RequestMapping(path = "charge", method = RequestMethod.GET) public void charge() throws Exception{ electricService.charge(); }; } +``` + +完成代码后,我们访问上述接口,会发现这段计算时间的切面并没有执行到,输出日志如下: + +> Electric charging …- Pay with alipay … + +回溯之前的代码可知,在@Around的切面类中,我们很清晰地定义了切面对应的方法,但是却没有被执行到。这说明了在类的内部,通过this方式调用的方法,是没有被Spring AOP增强的。这是为什么呢?我们来分析一下。 + +### 案例解析 + +我们可以从源码中找到真相。首先来设置个断点,调试看看this对应的对象是什么样的: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/5498c998bac045dc8ad374e868fcf8d1.jpg) + +可以看到,this对应的就是一个普通的ElectricService对象,并没有什么特别的地方。再看看在Controller层中自动装配的ElectricService对象是什么样: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/518ef134060c4de883bd2df066db9c40.jpg) + +可以看到,这是一个被Spring增强过的Bean,所以执行charge()方法时,会执行记录接口调用时间的增强操作。而this对应的对象只是一个普通的对象,并没有做任何额外的增强。 + +为什么this引用的对象只是一个普通对象呢?这还要从Spring AOP增强对象的过程来看。但在此之前,有些基础我需要在这里强调下。 + +**1\. Spring AOP的实现** + +Spring AOP的底层是动态代理。而创建代理的方式有两种,**JDK的方式和CGLIB的方式**。JDK动态代理只能对实现了接口的类生成代理,而不能针对普通类。而CGLIB是可以针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,来实现代理对象。具体区别可参考下图: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/6dabec74d4d34deab987b094e480244c.jpg) + +**2\. 如何使用Spring AOP** + +在Spring Boot中,我们一般只要添加以下依赖就可以直接使用AOP功能: + +> \- org.springframework.boot\- spring-boot-starter-aop\- + +而对于非Spring Boot程序,除了添加相关AOP依赖项外,我们还常常会使用@EnableAspectJAutoProxy来开启AOP功能。这个注解类引入(Import)AspectJAutoProxyRegistrar,它通过实现ImportBeanDefinitionRegistrar的接口方法来完成AOP相关Bean的准备工作。 + +补充完最基本的Spring底层知识和使用知识后,我们具体看下创建代理对象的过程。先来看下调用栈: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/4a8c347cef6f47d5870cc590340d1378.jpg) + +创建代理对象的时机就是创建一个Bean的时候,而创建的的关键工作其实是由AnnotationAwareAspectJAutoProxyCreator完成的。它本质上是一种BeanPostProcessor。所以它的执行是在完成原始Bean构建后的初始化Bean(initializeBean)过程中。而它到底完成了什么工作呢?我们可以看下它的postProcessAfterInitialization方法: + +```typescript +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; } +``` + +上述代码中的关键方法是wrapIfNecessary,顾名思义,**在需要使用AOP时,它会把创建的原始的Bean对象wrap成代理对象作为Bean返回**。具体到这个wrap过程,可参考下面的关键代码行: + +```typescript +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // 省略非关键代码 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } // 省略非关键代码 } +``` + +上述代码中,第6行的createProxy调用是创建代理对象的关键。具体到执行过程,它首先会创建一个代理工厂,然后将通知器(advisors)、被代理对象等信息加入到代理工厂,最后通过这个代理工厂来获取代理对象。一些关键过程参考下面的方法: + +```typescript +protected Object createProxy(Class beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { // 省略非关键代码 ProxyFactory proxyFactory = new ProxyFactory(); if (!proxyFactory.isProxyTargetClass()) { if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { evaluateProxyInterfaces(beanClass, proxyFactory); } } Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); // 省略非关键代码 return proxyFactory.getProxy(getProxyClassLoader()); } +``` + +经过这样一个过程,一个代理对象就被创建出来了。我们从Spring中获取到的对象都是这个代理对象,所以具有AOP功能。而之前直接使用this引用到的只是一个普通对象,自然也就没办法实现AOP的功能了。 + +### 问题修正 + +从上述案例解析中,我们知道,**只有引用的是被动态代理创建出来的对象,才会被Spring增强,具备AOP该有的功能**。那什么样的对象具备这样的条件呢? + +有两种。一种是被@Autowired注解的,于是我们的代码可以改成这样,即通过@Autowired的方式,在类的内部,自己引用自己: + +```csharp +@Service public class ElectricService { @Autowired ElectricService electricService; public void charge() throws Exception { System.out.println("Electric charging ..."); //this.pay(); electricService.pay(); } public void pay() throws Exception { System.out.println("Pay with alipay ..."); Thread.sleep(1000); } } +``` + +另一种方法就是直接从AopContext获取当前的Proxy。那你可能会问了,AopContext是什么?简单说,它的核心就是通过一个ThreadLocal来将Proxy和线程绑定起来,这样就可以随时拿出当前线程绑定的Proxy。 + +不过使用这种方法有个小前提,就是需要在@EnableAspectJAutoProxy里加一个配置项exposeProxy = true,表示将代理对象放入到ThreadLocal,这样才可以直接通过 AopContext.currentProxy()的方式获取到,否则会报错如下: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/8c4f7f4dc86741c283289ba6de698700.jpg) + +按这个思路,我们修改下相关代码: + +```java +import org.springframework.aop.framework.AopContext; import org.springframework.stereotype.Service; @Service public class ElectricService { public void charge() throws Exception { System.out.println("Electric charging ..."); ElectricService electric = ((ElectricService) AopContext.currentProxy()); electric.pay(); } public void pay() throws Exception { System.out.println("Pay with alipay ..."); Thread.sleep(1000); } } +``` + +同时,不要忘记修改EnableAspectJAutoProxy注解的exposeProxy属性,示例如下: + +```less +@SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class Application { // 省略非关键代码 } +``` + +这两种方法的效果其实是一样的,最终我们打印出了期待的日志,到这,问题顺利解决了。 + +```sql +Electric charging ... Pay with alipay ... Pay method time cost(ms): 1005 +``` + +## 案例2:直接访问被拦截类的属性抛空指针异常 + +接上一个案例,在宿舍管理系统中,我们使用了charge()方法进行支付。在统一结算的时候我们会用到一个管理员用户付款编号,这时候就用到了几个新的类。 + +User类,包含用户的付款编号信息: + +```typescript +public class User { private String payNum; public User(String payNum) { this.payNum = payNum; } public String getPayNum() { return payNum; } public void setPayNum(String payNum) { this.payNum = payNum; } } +``` + +AdminUserService类,包含一个管理员用户(User),其付款编号为202101166;另外,这个服务类有一个login()方法,用来登录系统。 + +```java +@Service public class AdminUserService { public final User adminUser = new User("202101166"); public void login() { System.out.println("admin user login..."); } } +``` + +我们需要修改ElectricService类实现这个需求:在电费充值时,需要管理员登录并使用其编号进行结算。完整代码如下: + +```java +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class ElectricService { @Autowired private AdminUserService adminUserService; public void charge() throws Exception { System.out.println("Electric charging ..."); this.pay(); } public void pay() throws Exception { adminUserService.login(); String payNum = adminUserService.adminUser.getPayNum(); System.out.println("User pay num : " + payNum); System.out.println("Pay with alipay ..."); Thread.sleep(1000); } } +``` + +代码完成后,执行charge()操作,一切正常: + +```sql +Electric charging ... admin user login... User pay num : 202101166 Pay with alipay ... +``` + +这时候,由于安全需要,就需要管理员在登录时,记录一行日志以便于以后审计管理员操作。所以我们添加一个AOP相关配置类,具体如下: + +```less +@Aspect @Service @Slf4j public class AopConfig { @Before("execution(* com.spring.puzzle.class5.example2.AdminUserService.login(..)) ") public void logAdminLogin(JoinPoint pjp) throws Throwable { System.out.println("! admin login ..."); } } +``` + +添加这段代码后,我们执行charge()操作,发现不仅没有相关日志,而且在执行下面这一行代码的时候直接抛出了NullPointerException: + +> String payNum = dminUserService.user.getPayNum(); + +本来一切正常的代码,因为引入了一个AOP切面,抛出了NullPointerException。这会是什么原因呢?我们先debug一下,来看看加入AOP后调用的对象是什么样子。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/1238fcfd96a747688ef9be88a2f7f7d4.jpg) + +可以看出,加入AOP后,我们的对象已经是一个代理对象了,如果你眼尖的话,就会发现在上图中,属性adminUser确实为null。为什么会这样?为了解答这个诡异的问题,我们需要进一步理解Spring使用CGLIB生成Proxy的原理。 + +### 案例解析 + +我们在上一个案例中解析了创建Spring Proxy的大体过程,在这里,我们需要进一步研究一下通过Proxy创建出来的是一个什么样的对象。正常情况下,AdminUserService只是一个普通的对象,而AOP增强过的则是一个AdminUserService $\\(EnhancerBySpringCGLIB\\)$xxxx。 + +这个类实际上是AdminUserService的一个子类。它会overwrite所有public和protected方法,并在内部将调用委托给原始的AdminUserService实例。 + +从具体实现角度看,CGLIB中AOP的实现是基于org.springframework.cglib.proxy包中 Enhancer和MethodInterceptor两个接口来实现的。 + +**整个过程,我们可以概括为三个步骤:** + +- 定义自定义的MethodInterceptor负责委托方法执行; +- 创建Enhance并设置Callback为上述MethodInterceptor; +- enhancer.create()创建代理。 + +接下来,我们来具体分析一下Spring的相关实现源码。 + +在上个案例分析里,我们简要提及了Spring的动态代理对象的初始化机制。在得到Advisors之后,会通过ProxyFactory.getProxy获取代理对象: + +```scss +public Object getProxy(ClassLoader classLoader) { return createAopProxy().getProxy(classLoader); } +``` + +在这里,我们以CGLIB的Proxy的实现类CglibAopProxy为例,来看看具体的流程: + +```typescript +public Object getProxy(@Nullable ClassLoader classLoader) { // 省略非关键代码 // 创建及配置 Enhancer Enhancer enhancer = createEnhancer(); // 省略非关键代码 // 获取Callback:包含DynamicAdvisedInterceptor,亦是MethodInterceptor Callback[] callbacks = getCallbacks(rootClass); // 省略非关键代码 // 生成代理对象并创建代理(设置 enhancer 的 callback 值) return createProxyClassAndInstance(enhancer, callbacks); // 省略非关键代码 } +``` + +上述代码中的几个关键步骤大体符合之前提及的三个步骤,其中最后一步一般都会执行到CglibAopProxy子类ObjenesisCglibAopProxy的createProxyClassAndInstance()方法: + +```kotlin +protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { //创建代理类Class Class proxyClass = enhancer.createClass(); Object proxyInstance = null; //spring.objenesis.ignore默认为false //所以objenesis.isWorthTrying()一般为true if (objenesis.isWorthTrying()) { try { // 创建实例 proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache()); } catch (Throwable ex) { // 省略非关键代码 } } if (proxyInstance == null) { // 尝试普通反射方式创建实例 try { Constructor ctor = (this.constructorArgs != null ? proxyClass.getDeclaredConstructor(this.constructorArgTypes) : proxyClass.getDeclaredConstructor()); ReflectionUtils.makeAccessible(ctor); proxyInstance = (this.constructorArgs != null ? ctor.newInstance(this.constructorArgs) : ctor.newInstance()); //省略非关键代码 } } // 省略非关键代码 ((Factory) proxyInstance).setCallbacks(callbacks); return proxyInstance; } +``` + +这里我们可以了解到,Spring会默认尝试使用objenesis方式实例化对象,如果失败则再次尝试使用常规方式实例化对象。现在,我们可以进一步查看objenesis方式实例化对象的流程。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/83462c65b9ae4cd1b866eee554137674.jpg) + +参照上述截图所示调用栈,objenesis方式最后使用了JDK的ReflectionFactory.newConstructorForSerialization()完成了代理对象的实例化。而如果你稍微研究下这个方法,你会惊讶地发现,这种方式创建出来的对象是不会初始化类成员变量的。 + +所以说到这里,聪明的你可能已经觉察到真相已经暴露了,我们这个案例的核心是代理类实例的默认构建方式很特别。在这里,我们可以总结和对比下通过反射来实例化对象的方式,包括: + +- java.lang.Class.newInsance() +- java.lang.reflect.Constructor.newInstance() +- sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance() + +前两种初始化方式都会同时初始化类成员变量,但是最后一种通过ReflectionFactory.newConstructorForSerialization().newInstance()实例化类则不会初始化类成员变量,这就是当前问题的最终答案了。 + +### 问题修正 + +了解了问题的根本原因后,修正起来也就不困难了。既然是无法直接访问被拦截类的成员变量,那我们就换个方式,在UserService里写个getUser()方法,从内部访问获取变量。 + +我们在AdminUserService里加了个getUser()方法: + +```csharp +public User getUser() { return user; } +``` + +在ElectricService里通过getUser()获取User对象: + +> //原来出错的方式:- //String payNum = = adminUserService.adminUser.getPayNum();- //修改后的方式:- String payNum = adminUserService.getAdminUser().getPayNum(); + +运行下来,一切正常,可以看到管理员登录日志了: + +```sql +Electric charging ... ! admin login ... admin user login... User pay num : 202101166 Pay with alipay ... +``` + +但你有没有产生另一个困惑呢?既然代理类的类属性不会被初始化,那为什么可以通过在AdminUserService里写个getUser()方法来获取代理类实例的属性呢? + +我们再次回顾createProxyClassAndInstance的代码逻辑,创建代理类后,我们会调用setCallbacks来设置拦截后需要注入的代码: + +```typescript +protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { Class proxyClass = enhancer.createClass(); Object proxyInstance = null; if (objenesis.isWorthTrying()) { try { proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache()); } // 省略非关键代码 ((Factory) proxyInstance).setCallbacks(callbacks); return proxyInstance; } +``` + +通过代码调试和分析,我们可以得知上述的callbacks中会存在一种服务于AOP的DynamicAdvisedInterceptor,它的接口是MethodInterceptor(callback的子接口),实现了拦截方法intercept()。我们可以看下它是如何实现这个方法的: + +```typescript +public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 省略非关键代码 TargetSource targetSource = this.advised.getTargetSource(); // 省略非关键代码 if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = methodProxy.invoke(target, argsToUse); } else { // We need to create a method invocation... retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); } retVal = processReturnType(proxy, target, method, retVal); return retVal; } //省略非关键代码 } +``` + +当代理类方法被调用,会被Spring拦截,从而进入此intercept(),并在此方法中获取被代理的原始对象。而在原始对象中,类属性是被实例化过且存在的。因此代理类是可以通过方法拦截获取被代理对象实例的属性。 + +说到这里,我们已经解决了问题。但如果你看得仔细,就会发现,其实你改变一个属性,也可以让产生的代理对象的属性值不为null。例如修改启动参数spring.objenesis.ignore如下: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/b254a5540742452b8eeb60577f38095c.jpg) + +此时再调试程序,你会发现adminUser已经不为null了: + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/assets/eda0228b7bc34cfeb2297e82ccc60e4c.jpg) + +所以这也是解决这个问题的一种方法,相信聪明的你已经能从前文贴出的代码中找出它能够工作起来的原理了。 + +## 重点回顾 + +通过以上两个案例的介绍,相信你对Spring AOP动态代理的初始化机制已经有了进一步的了解,这里总结重点如下: + +1. 使用AOP,实际上就是让Spring自动为我们创建一个Proxy,使得调用者能无感知地调用指定方法。而Spring有助于我们在运行期里动态织入其它逻辑,因此,AOP本质上就是一个动态代理。 + +2. 我们只有访问这些代理对象的方法,才能获得AOP实现的功能,所以通过this引用是无法正确使用AOP功能的。在不能改变代码结果前提下,我们可以通过@Autowired、AopContext.currentProxy()等方式获取相应的代理对象来实现所需的功能。 + +3. 我们一般不能直接从代理类中去拿被代理类的属性,这是因为除非我们显示设置spring.objenesis.ignore为true,否则代理类的属性是不会被Spring初始化的,我们可以通过在被代理类中增加一个方法来间接获取其属性。 + + +## 思考题 + +第二个案例中,我们提到了通过反射来实例化类的三种方式: + +- java.lang.Class.newInsance() +- java.lang.reflect.Constructor.newInstance() +- sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance() + +其中第三种方式不会初始化类属性,你能够写一个例子来证明这一点吗? + +期待你的思考,我们留言区见! \ No newline at end of file diff --git a/zh-cn/geek courses/spring/README.md b/zh-cn/geek courses/spring/README.md index 1ba89f01..154a1ded 100644 --- a/zh-cn/geek courses/spring/README.md +++ b/zh-cn/geek courses/spring/README.md @@ -4,4 +4,8 @@ 2. [02 Spring Bean 依赖注入常见错误(上)]() -3. [03 Spring Bean 依赖注入常见错误(下)]() \ No newline at end of file +3. [03 Spring Bean 依赖注入常见错误(下)]() + +4. [04 Spring Bean 生命周期常见错误]() + +5. [05 Spring AOP 常见错误(上)]() \ No newline at end of file