Skip to content

Commit

Permalink
解决README链接和文章img显示问题
Browse files Browse the repository at this point in the history
  • Loading branch information
qinzy committed Jun 20, 2024
1 parent 12ca703 commit f9b7cb8
Show file tree
Hide file tree
Showing 145 changed files with 158 additions and 162 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

- [一个月学Docker](zh-cn/docker/docker-4-weeks/)

#### 5.云原生

#### 5.专题课程

#### 6.专题课程

- [极客时间](<zh-cn/geek courses/README.md>)
- [极客时间](<zh-cn/course/README.md>)
12 changes: 6 additions & 6 deletions zh-cn/course/spring/01 Spring Bean 定义常见错误.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

在构建 Web 服务时,我们常使用 Spring Boot 来快速构建。例如,使用下面的包结构和相关代码来完成一个简易的 Web 版 HelloWorld:

![](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/06dff389d2fe498f83b115052e2c37cc.jpg)
![](assets/01_01.jpg)

其中,负责启动程序的 Application 类定义如下:

Expand All @@ -28,7 +28,7 @@ package com.spring.puzzle.class1.example1.application //省略 import @RestContr

但是,假设有一天,当我们需要添加多个类似的 Controller,同时又希望用更清晰的包层次和结构来管理时,我们可能会去单独建立一个独立于 application 包之外的 Controller 包,并调整类的位置。调整后结构示意如下:

![](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/eec656c5109d470a854c41a74a725494.jpg)
![](assets/01_02.jpg)

实际上,我们没有改变任何代码,只是改变了包的结构,但是我们会发现这个 Web 应用失效了,即不能识别出 HelloWorldController 了。也就是说,我们找不到 HelloWorldController 这个 Bean 了。这是为何?

Expand All @@ -52,7 +52,7 @@ public @interface ComponentScan { /** * Base packages to scan for annotated comp

而在我们的案例中,我们直接使用的是 SpringBootApplication 注解定义的 ComponentScan,它的 basePackages 没有指定,所以默认为空(即{})。此时扫描的是什么包?这里不妨带着这个问题去调试下(调试位置参考 ComponentScanAnnotationParser#parse 方法),调试视图如下:

![](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/13447ea3fe70423cb7e44d66a33689b8.jpg)
![](assets/01_03.jpg)

从上图可以看出,当 basePackages 为空时,扫描的包会是 declaringClass 所在的包,在本案例中,declaringClass 就是 Application.class,所以扫描的包其实就是它所在的包,即com.spring.puzzle.class1.example1.application

Expand Down Expand Up @@ -124,7 +124,7 @@ return this.beanFactory.resolveDependency( new DependencyDescriptor(param, true)

如果用调试视图,我们则可以看到更多的信息:

![](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/66f90d9085eb4658a12e4a526752d328.jpg)
![](assets/01_04.jpg)

如图所示,上述的调用即是根据参数来寻找对应的 Bean,在本案例中,如果找不到对应的 Bean 就会抛出异常,提示装配失败。

Expand Down Expand Up @@ -214,7 +214,7 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property

首先,我们可以通过调试方式看下方法的执行,参考下图:

![](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/f4ff060783ff4b228c1a65563975eead.jpg)
![](assets/01_05.jpg)

从上图我们可以看出,我们最终的执行因为标记了 Lookup 而走入了 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept

Expand Down Expand Up @@ -242,7 +242,7 @@ private final BeanFactory owner; public Object intercept(Object obj, Method meth

添加后效果图如下:

![](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/4285e0e5eff4486ca6e718d0cdc9e291.jpg)
![](assets/01_06.jpg)

以上即为 Lookup 的一些关键实现思路。还有很多细节,例如CGLIB子类如何产生,无法一一解释,有兴趣的话,可以进一步深入研究,留言区等你。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public interface DataService { void deleteStudent(int id); } @Repository @Slf4j

实际上,当我们完成支持多个数据库的准备工作时,程序就已经无法启动了,报错如下:

![](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/b731c4a063eb453f880cb8eed3687567.jpg)
![](assets/02_01.jpg)

很显然,上述报错信息正是我们这一小节讨论的错误,那么这个错误到底是怎么产生的呢?接下来我们具体分析下。

Expand Down Expand Up @@ -67,7 +67,7 @@ InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), p

为了更清晰地展示错误发生的位置,我们可以采用调试的视角展示其位置(即DefaultListableBeanFactory#doResolveDependency中代码片段),参考下图:

![](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/96842c40dffc44cc818fc225c563c909.jpg)
![](assets/02_02.jpg)

如上图所示,当我们根据DataService这个类型来找出依赖时,我们会找出2个依赖,分别为CassandraDataServiceOracleDataService。在这样的情况下,如果同时满足以下两个条件则会抛出本案例的错误:

Expand Down Expand Up @@ -155,7 +155,7 @@ raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);

一旦找出这些Bean的信息,就可以生成这些Bean的名字,然后组合成一个个BeanDefinitionHolder返回给上层。这个过程关键步骤可以查看下图的代码片段(ClassPathBeanDefinitionScanner#doScan):

![](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/96ad15af93994cdab1a43b3a03f9f286.jpg)
![](assets/02_03.jpg)

基本匹配我们前面描述的过程,其中方法调用BeanNameGenerator#generateBeanName即用来产生Bean的名字,它有两种实现方式。因为DataService的实现都是使用注解标记的,所以Bean名称的生成逻辑最终调用的其实是AnnotationBeanNameGenerator#generateBeanName这种实现方式,我们可以看下它的具体实现,代码如下:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ String strVal = resolveEmbeddedValue((String) value);
这里其实是在解析嵌入的值,实际上就是“替换占位符”工作。具体而言,它采用的是PropertySourcesPlaceholderConfigurer根据PropertySources来替换。不过当使用 ${username} 来获取替换值时,其最终执行的查找并不是局限在application.property文件中的。通过调试,我们可以看到下面的这些“源”都是替换依据:
![](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/579184faf70a4425b7fbf42cd24aa3d5.jpg)
![](assets/03_01.jpg)
```bash
[ConfigurationPropertySourcesPropertySource {name='configurationProperties'}, StubPropertySource {name='servletConfigInitParams'}, ServletContextPropertySource {name='servletContextInitParams'}, PropertiesPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'}, OriginTrackedMapPropertySource {name='applicationConfig: classpath:/application.properties]'}, MapPropertySource {name='devtools'}]
Expand All @@ -102,7 +102,7 @@ String strVal = resolveEmbeddedValue((String) value);
如果我们查看systemEnvironment这个源,会发现刚好有一个username和我们是重合的,且值不是pass。
![](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/ab560187bc08439b980ea314e04e9aca.jpg)
![](assets/03_02.jpg)
所以,讲到这里,你应该知道问题所在了吧?这是一个误打误撞的例子,刚好系统环境变量(systemEnvironment)中含有同名的配置。实际上,对于系统参数(systemProperties)也是一样的,这些参数或者变量都有很多,如果我们没有意识到它的存在,起了一个同名的字符串作为@Value的值,则很容易引发这类问题。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.spring

然而事与愿违,我们得到的只会是 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)
![](assets/04_01.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)
![](assets/04_02.jpg)

这个图初看起来复杂,我们不妨将其分为三部分:

Expand Down
18 changes: 9 additions & 9 deletions zh-cn/course/spring/05 Spring AOP 常见错误(上).md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ Spring AOP是Spring中除了依赖注入外(DI)最为核心的功能,顾

我们可以从源码中找到真相。首先来设置个断点,调试看看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)
![](assets/05_01.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)
![](assets/05_02.jpg)

可以看到,这是一个被Spring增强过的Bean,所以执行charge()方法时,会执行记录接口调用时间的增强操作。而this对应的对象只是一个普通的对象,并没有做任何额外的增强。

Expand All @@ -52,7 +52,7 @@ Spring AOP是Spring中除了依赖注入外(DI)最为核心的功能,顾

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)
![](assets/05_03.jpg)

**2\. 如何使用Spring AOP**

Expand All @@ -64,7 +64,7 @@ Spring AOP的底层是动态代理。而创建代理的方式有两种,**JDK

补充完最基本的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)
![](assets/05_04.jpg)

创建代理对象的时机就是创建一个Bean的时候,而创建的的关键工作其实是由AnnotationAwareAspectJAutoProxyCreator完成的。它本质上是一种BeanPostProcessor。所以它的执行是在完成原始Bean构建后的初始化Bean(initializeBean)过程中。而它到底完成了什么工作呢?我们可以看下它的postProcessAfterInitialization方法:

Expand Down Expand Up @@ -100,7 +100,7 @@ protected Object createProxy(Class<?> beanClass, @Nullable String beanName, @Nul
不过使用这种方法有个小前提,就是需要在@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)
![](assets/05_05.jpg)
按这个思路,我们修改下相关代码:
Expand Down Expand Up @@ -160,7 +160,7 @@ Electric charging ... admin user login... User pay num : 202101166 Pay with alip
本来一切正常的代码,因为引入了一个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)
![](assets/05_06.jpg)
可以看出,加入AOP后,我们的对象已经是一个代理对象了,如果你眼尖的话,就会发现在上图中,属性adminUser确实为null。为什么会这样?为了解答这个诡异的问题,我们需要进一步理解Spring使用CGLIB生成Proxy的原理。
Expand Down Expand Up @@ -200,7 +200,7 @@ protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callb
这里我们可以了解到,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)
![](assets/05_07.jpg)
参照上述截图所示调用栈,objenesis方式最后使用了JDK的ReflectionFactory.newConstructorForSerialization()完成了代理对象的实例化。而如果你稍微研究下这个方法,你会惊讶地发现,这种方式创建出来的对象是不会初始化类成员变量的。
Expand Down Expand Up @@ -250,11 +250,11 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy
说到这里,我们已经解决了问题。但如果你看得仔细,就会发现,其实你改变一个属性,也可以让产生的代理对象的属性值不为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)
![](assets/05_07.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)
![](assets/05_08.jpg)
所以这也是解决这个问题的一种方法,相信聪明的你已经能从前文贴出的代码中找出它能够工作起来的原理了。
Expand Down
2 changes: 1 addition & 1 deletion zh-cn/course/spring/06 Spring AOP 常见错误(下).md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName
后续 Bean 创建代理时,直接拿出这个排序好的候选 Advisors。候选 Advisors 排序发生在 Bean 构建这个结论时,我们也可以通过 AopConfig Bean 构建中的堆栈信息验证:
![](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/7e622f4a23b640b4842497602ef22200.jpg)
![](assets/06_01.jpg)
可以看到,排序是在 Bean 的构建中进行的,而最后排序执行的关键代码位于下面的方法中(参考 ReflectiveAspectJAdvisorFactory#getAdvisorMethods):
Expand Down
6 changes: 3 additions & 3 deletions zh-cn/course/spring/07 Spring事件常见错误.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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/998134895f5e421d8e1acba0ec46be96.jpg)
![](assets/07_01.jpg)

从图中我们可以看出,Spring事件包含以下三大组件。

Expand Down Expand Up @@ -122,7 +122,7 @@ private void startBeans(boolean autoStartupOnly) { Map<String, Lifecycle> lifecy
现在我们调试下代码,你会发现这个方法在Spring启动时一定经由SpringApplication#prepareEnvironment方法调用,调试截图如下:
![](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/16a124d3fef34f5dbcf738be5fef393c.jpg)
![](assets/07_02.jpg)
表面上看,既然代码会被调用,事件就会抛出,那么我们在最开始定义的监听器就能处理,但是我们真正去运行程序时会发现,效果和案例1是一样的,都是监听器的处理并不执行,即拦截不了。这又是为何?
Expand All @@ -142,7 +142,7 @@ public class EventPublishingRunListener implements SpringApplicationRunListener,
如果继续查看代码,我们会发现这个事件的监听器就存储在SpringApplication#Listeners中,调试下就可以找出所有的监听器,截图如下:
![](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/9a29c0f2db8c4d47a538947896b97bde.jpg)
![](assets/07_03.jpg)
从中我们可以发现并不存在我们定义的MyApplicationEnvironmentPreparedEventListener,这是为何?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
查看决策谁优先的源码,最终使用属性名来匹配的执行情况可参考DefaultListableBeanFactory#matchesBeanName方法的调试视图:
![](08%20%E7%AD%94%E7%96%91%E7%8E%B0%E5%9C%BA%EF%BC%9ASpring%20Core%20%E7%AF%87%E6%80%9D%E8%80%83%E9%A2%98%E5%90%88%E9%9B%86/5ebdca5725f745d59e7b18fd160d3c19.jpg)
![](assets/08_01.jpg)
我们可以看到实现的关键其实是下面这行语句:
Expand Down Expand Up @@ -182,11 +182,11 @@ public int compare(@Nullable Object o1, @Nullable Object o2) { return doCompare(
继续跟踪 getOrder() 的执行细节,我们会发现对于我们的案例,这个方法会找出配置切面的 Bean 的 Order值。这里可以参考 BeanFactoryAspectInstanceFactory#getOrder 的调试视图验证这个结论:
![](08%20%E7%AD%94%E7%96%91%E7%8E%B0%E5%9C%BA%EF%BC%9ASpring%20Core%20%E7%AF%87%E6%80%9D%E8%80%83%E9%A2%98%E5%90%88%E9%9B%86/57691fdf7c94454a9da898113608d70d.jpg)
![](assets/08_02.jpg)
上述截图中,aopConfig2 即是我们配置切面的 Bean 的名称。这里再顺带提供出调用栈的截图,以便你做进一步研究:
![](08%20%E7%AD%94%E7%96%91%E7%8E%B0%E5%9C%BA%EF%BC%9ASpring%20Core%20%E7%AF%87%E6%80%9D%E8%80%83%E9%A2%98%E5%90%88%E9%9B%86/5da30f9691264c2eb3e71f15b7d04da6.jpg)
![](assets/08_03.jpg)
现在我们就知道了,将不同的增强方法放置到不同的切面配置类中,使用不同的 Order 值来修饰是可以影响顺序的。相反,如果都是在一个配置类中,自然不会影响顺序,所以这也是当初我的方案中没有重点介绍 sortAdvisors 方法的原因,毕竟当时我们给出的案例都只有一个 AOP 配置类。
Expand Down
Loading

0 comments on commit f9b7cb8

Please sign in to comment.