This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,453 @@
<audio id="audio" title="01Spring Bean 定义常见错误" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dd/02/dd84yyf22495ed8ed1de64ae94eaf402.mp3"></audio>
你好,我是傅健。
从导读中我们已知Spring 的核心是围绕 Bean 进行的。不管是 Spring Boot 还是 Spring Cloud只要名称中带有Spring关键字的技术都脱离不了 Bean而要使用一个 Bean 少不了要先定义出来,所以**定义一个Bean 就变得格外重要了**。
当然对于这么重要的工作Spring 自然给我们提供了很多简单易用的方式。然而,这种简单易用得益于 Spring 的“**约定大于配置**”,但我们往往不见得会对所有的约定都了然于胸,所以仍然会在 Bean 的定义上犯一些经典的错误。
接下来我们就来了解下那些经典错误以及它们背后的原理,你也可以对照着去看看自己是否也曾犯过,后来又是如何解决的。
## 案例 1隐式扫描不到 Bean 的定义
在构建 Web 服务时,我们常使用 Spring Boot 来快速构建。例如,使用下面的包结构和相关代码来完成一个简易的 Web 版 HelloWorld
<img src="https://static001.geekbang.org/resource/image/63/48/63f7d08fb89653e12b9946c4dca31c48.png" alt="">
其中,负责启动程序的 Application 类定义如下:
```
package com.spring.puzzle.class1.example1.application
//省略 import
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
提供接口的 HelloWorldController 代码如下:
```
package com.spring.puzzle.class1.example1.application
//省略 import
@RestController
public class HelloWorldController {
@RequestMapping(path = &quot;hi&quot;, method = RequestMethod.GET)
public String hi(){
return &quot;helloworld&quot;;
};
}
```
上述代码即可实现一个简单的功能:访问[http://localhost:8080/hi](http://localhost:8080/hi) 返回helloworld。两个关键类位于同一个包即 application中。其中 HelloWorldController 因为添加了@RestController,最终被识别成一个 Controller 的 Bean。
但是,假设有一天,当我们需要添加多个类似的 Controller同时又希望用更清晰的包层次和结构来管理时我们可能会去单独建立一个独立于 application 包之外的 Controller 包,并调整类的位置。调整后结构示意如下:
<img src="https://static001.geekbang.org/resource/image/f6/30/f6080f4e2b10e7f54e79040b8362c230.png" alt="">
实际上,我们没有改变任何代码,只是改变了包的结构,但是我们会发现这个 Web 应用失效了,即不能识别出 HelloWorldController 了。也就是说,我们找不到 HelloWorldController 这个 Bean 了。这是为何?
### 案例解析
要了解 HelloWorldController 为什么会失效,就需要先了解之前是如何生效的。对于 Spring Boot 而言,关键点在于 Application.java 中使用了 SpringBootApplication 注解。而这个注解继承了另外一些注解,具体定义如下:
```
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//省略非关键代码
}
```
从定义可以看出SpringBootApplication开启了很多功能其中一个关键功能就是 ComponentScan参考其配置如下
>
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class)
当 Spring Boot 启动时ComponentScan 的启用意味着会去扫描出所有定义的 Bean那么扫描什么位置呢这是由 ComponentScan 注解的 basePackages 属性指定的,具体可参考如下定义:
```
public @interface ComponentScan {
/**
* Base packages to scan for annotated components.
* &lt;p&gt;{@link #value} is an alias for (and mutually exclusive with) this
* attribute.
* &lt;p&gt;Use {@link #basePackageClasses} for a type-safe alternative to
* String-based package names.
*/
@AliasFor(&quot;value&quot;)
String[] basePackages() default {};
//省略其他非关键代码
}
```
而在我们的案例中,我们直接使用的是 SpringBootApplication 注解定义的 ComponentScan它的 basePackages 没有指定,所以默认为空(即{})。此时扫描的是什么包?这里不妨带着这个问题去调试下(调试位置参考 ComponentScanAnnotationParser#parse 方法),调试视图如下:
<img src="https://static001.geekbang.org/resource/image/75/c6/75a8abe6d5854f4f4a8d6c9a5655f3c6.png" alt="">
从上图可以看出,当 basePackages 为空时,扫描的包会是 declaringClass 所在的包在本案例中declaringClass 就是 Application.class所以扫描的包其实就是它所在的包即com.spring.puzzle.class1.example1.application。
对比我们重组包结构前后我们自然就找到了这个问题的根源在调整前HelloWorldController 在扫描范围内,而调整后,它已经远离了扫描范围(不和 Application.java 一个包了),虽然代码没有一丝丝改变,但是这个功能已经失效了。
所以,综合来看,这个问题是因为我们不够了解 Spring Boot 的默认扫描规则引起的。我们仅仅享受了它的便捷,但是并未了解它背后的故事,所以稍作变化,就可能玩不转了。
### 问题修正
针对这个案例,有了源码的剖析,我们可以快速找到解决方案了。当然了,我们所谓的解决方案肯定不是说把 HelloWorldController 移动回原来的位置,而是**真正去满足需求**。在这里,真正解决问题的方式是显式配置@ComponentScan。具体修改方式如下:
```
@SpringBootApplication
@ComponentScan(&quot;com.spring.puzzle.class1.example1.controller&quot;)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
通过上述修改我们显式指定了扫描的范围为com.spring.puzzle.class1.example1.controller。不过需要注意的是显式指定后默认的扫描范围即com.spring.puzzle.class1.example1.application就不会被添加进去了。另外我们也可以使用@ComponentScans 来修复问题,使用方式如下:
>
@ComponentScans(value = { @ComponentScan(value = "com.spring.puzzle.class1.example1.controller") })
顾名思义,可以看出 ComponentScans 相比较 ComponentScan 多了一个s支持多个包的扫描范围指定。
此时,细心的你可能会发现:如果对源码缺乏了解,很容易会顾此失彼。以 ComponentScan 为例,原有的代码扫描了默认包而忽略了其它包;而**一旦显式指定其它包,原来的默认扫描包就被忽略了。**
## 案例 2定义的 Bean 缺少隐式依赖
初学 Spring 时,我们往往不能快速转化思维。例如,在程序开发过程中,有时候,一方面我们把一个类定义成 Bean同时又觉得这个 Bean 的定义除了加了一些 Spring 注解外,并没有什么不同。所以在后续使用时,有时候我们会不假思索地去随意定义它,例如我们会写出下面这样的代码:
```
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
}
```
ServiceImpl 因为标记为@Service而成为一个 Bean。另外我们ServiceImpl 显式定义了一个构造器。但是,上面的代码不是永远都能正确运行的,有时候会报下面这种错误:
>
Parameter 0 of constructor in com.spring.puzzle.class1.example2.ServiceImpl required a bean of type 'java.lang.String' that could not be found.
那这种错误是怎么发生的呢?下面我们来分析一下。
### 案例解析
当创建一个 Bean 时,调用的方法是 AbstractAutowireCapableBeanFactory#createBeanInstance。它主要包含两大基本步骤:寻找构造器和通过反射调用构造器创建实例。对于这个案例,最核心的代码执行,你可以参考下面的代码片段:
```
// Candidate constructors for autowiring?
Constructor&lt;?&gt;[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}
```
Spring 会先执行 determineConstructorsFromBeanPostProcessors 方法来获取构造器,然后通过 autowireConstructor 方法带着构造器去创建实例。很明显,在本案例中只有一个构造器,所以非常容易跟踪这个问题。
autowireConstructor 方法要创建实例,不仅需要知道是哪个构造器,还需要知道构造器对应的参数,这点从最后创建实例的方法名也可以看出,参考如下(即 ConstructorResolver#instantiate
```
private Object instantiate(
String beanName, RootBeanDefinition mbd, Constructor&lt;?&gt; constructorToUse, Object[] argsToUse)
```
那么上述方法中存储构造参数的 argsToUse 如何获取呢换言之当我们已经知道构造器ServiceImpl(String serviceName),要创建出 ServiceImpl 实例,如何确定 serviceName 的值是多少?
很明显,这里是在使用 Spring我们**不能直接显式使用 new 关键字来创建实例**。Spring只能是去寻找依赖来作为构造器调用参数。
那么这个参数如何获取呢?可以参考下面的代码片段(即 ConstructorResolver#autowireConstructor
```
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
```
我们可以调用 createArgumentArray 方法来构建调用构造器的参数数组,而这个方法的最终实现是从 BeanFactory 中获取 Bean可以参考下述调用
```
return this.beanFactory.resolveDependency(
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
```
如果用调试视图,我们则可以看到更多的信息:
<img src="https://static001.geekbang.org/resource/image/51/a3/5113cfc71ec8dab37e254c5c5e9abba3.png" alt="">
如图所示,上述的调用即是根据参数来寻找对应的 Bean在本案例中如果找不到对应的 Bean 就会抛出异常,提示装配失败。
### 问题修正
从源码级别了解了错误的原因后,现在反思为什么会出现这个错误。追根溯源,正如开头所述,因为不了解很多隐式的规则:我们定义一个类为 Bean如果再显式定义了构造器那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean然后反射创建出这个 Bean。
了解了这个隐式规则后,解决这个问题就简单多了。我们可以直接定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 Bean例如定义如下
```
//这个bean装配给ServiceImpl的构造器参数“serviceName”
@Bean
public String serviceName(){
return &quot;MyServiceName&quot;;
}
```
再次运行程序,发现一切正常了。
所以,我们在使用 Spring 时,**不要总想着定义的Bean 也可以在非 Spring 场合直接用 new 关键字显式使用,这种思路是不可取的**。
另外,类似的,假设我们不了解 Spring 的隐式规则,在修正问题后,我们可能写出更多看似可以运行的程序,代码如下:
```
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
public ServiceImpl(String serviceName, String otherStringParameter){
this.serviceName = serviceName;
}
}
```
如果我们仍用非 Spring 的思维去审阅这段代码,可能不会觉得有什么问题,毕竟 String 类型可以自动装配了,无非就是增加了一个 String 类型的参数而已。
但是如果你了解 Spring 内部是用反射来构建 Bean 的话,就不难发现问题所在:存在两个构造器,都可以调用时,到底应该调用哪个呢?最终 Spring 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。
## 案例 3原型 Bean 被固定
接下来,我们再来看另外一个关于 Bean 定义不生效的案例。在定义 Bean 时,有时候我们会使用原型 Bean例如定义如下
```
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
```
然后我们按照下面的方式去使用它:
```
@RestController
public class HelloWorldController {
@Autowired
private ServiceImpl serviceImpl;
@RequestMapping(path = &quot;hi&quot;, method = RequestMethod.GET)
public String hi(){
return &quot;helloworld, service is : &quot; + serviceImpl;
};
}
```
结果,我们会发现,不管我们访问多少次[http://localhost:8080/hi](http://localhost:8080/hi),访问的结果都是不变的,如下:
>
helloworld, service is : com.spring.puzzle.class1.example3.error.ServiceImpl@4908af
很明显,这很可能和我们定义 ServiceImpl 为原型 Bean 的初衷背道而驰,如何理解这个现象呢?
### 案例解析
当一个属性成员 serviceImpl 声明为@Autowired 后,那么在创建 HelloWorldController 这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记为@Autowired 的属性成员(装配方法参考 AbstractAutowireCapableBeanFactory#populateBean)。
具体到执行过程,它会使用很多 BeanPostProcessor 来做完成工作,其中一种是 AutowiredAnnotationBeanPostProcessor它会通过 DefaultListableBeanFactory#findAutowireCandidates 寻找到 ServiceImpl 类型的 Bean然后设置给对应的属性即 serviceImpl成员
关键执行步骤可参考 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject
```
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Field field = (Field) this.member;
Object value;
//寻找“bean”
if (this.cached) {
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
}
else {
//省略其他非关键代码
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
}
if (value != null) {
//将bean设置给成员字段
ReflectionUtils.makeAccessible(field);
field.set(bean, value);
}
}
```
待我们寻找到要自动注入的 Bean 后即可通过反射设置给对应的field。这个field的执行只发生了一次所以后续就固定起来了它并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。
所以,**当一个单例的Bean使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。**
### 问题修正
通过上述源码分析,我们可以知道要修正这个问题,肯定是不能将 ServiceImpl 的 Bean 固定到属性上的,而应该是每次使用时都会重新获取一次。所以这里我提供了两种修正方式:
**1. 自动注入 Context**
即自动注入 ApplicationContext然后定义 getServiceImpl() 方法,在方法中获取一个新的 ServiceImpl 类型实例。修正代码如下:
```
@RestController
public class HelloWorldController {
@Autowired
private ApplicationContext applicationContext;
@RequestMapping(path = &quot;hi&quot;, method = RequestMethod.GET)
public String hi(){
return &quot;helloworld, service is : &quot; + getServiceImpl();
};
public ServiceImpl getServiceImpl(){
return applicationContext.getBean(ServiceImpl.class);
}
}
```
**2. 使用 Lookup 注解**
类似修正方法 1也添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。修正代码如下:
```
@RestController
public class HelloWorldController {
@RequestMapping(path = &quot;hi&quot;, method = RequestMethod.GET)
public String hi(){
return &quot;helloworld, service is : &quot; + getServiceImpl();
};
@Lookup
public ServiceImpl getServiceImpl(){
return null;
}
}
```
通过这两种修正方式,再次测试程序,我们会发现结果已经符合预期(每次访问这个接口,都会创建新的 Bean
这里我们不妨再拓展下,讨论下 Lookup 是如何生效的。毕竟在修正代码中我们看到getServiceImpl方法的实现返回值是 null这或许很难说服自己。
首先,我们可以通过调试方式看下方法的执行,参考下图:
<img src="https://static001.geekbang.org/resource/image/3d/91/3d0e125b9d9e0711489d3a6aeff88c91.png" alt="">
从上图我们可以看出,我们最终的执行因为标记了 Lookup 而走入了 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept
```
private final BeanFactory owner;
public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
Assert.state(lo != null, &quot;LookupOverride not found&quot;);
Object[] argsToUse = (args.length &gt; 0 ? args : null); // if no-arg, don't insist on args at all
if (StringUtils.hasText(lo.getBeanName())) {
return (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :
this.owner.getBean(lo.getBeanName()));
}
else {
return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) :
this.owner.getBean(method.getReturnType()));
}
}
```
我们的方法调用最终并没有走入案例代码实现的return null语句而是通过 BeanFactory 来获取 Bean。所以从这点也可以看出其实**在我们的 getServiceImpl 方法实现中,随便怎么写都行,这不太重要。**
例如,我们可以使用下面的实现来测试下这个结论:
```
@Lookup
public ServiceImpl getServiceImpl(){
//下面的日志会输出么?
log.info(&quot;executing this method&quot;);
return null;
}
```
以上代码,添加了一行代码输出日志。测试后,我们会发现并没有日志输出。这也验证了,当使用 Lookup 注解一个方法时,这个方法的具体实现已并不重要。
再回溯下前面的分析为什么我们走入了CGLIB 搞出的类,这是因为我们有方法标记了 Lookup。我们可以从下面的这段代码得到验证参考 SimpleInstantiationStrategy#instantiate
```
@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
// Don't override the class with CGLIB if no overrides.
if (!bd.hasMethodOverrides()) {
//
return BeanUtils.instantiateClass(constructorToUse);
}
else {
// Must generate CGLIB subclass.
return instantiateWithMethodInjection(bd, beanName, owner);
}
}
```
在上述代码中,当 hasMethodOverrides 为 true 时,则使用 CGLIB。而在本案例中这个条件的成立在于解析HelloWorldController 这个 Bean 时,我们会发现有方法标记了 Lookup此时就会添加相应方法到属性methodOverrides 里面去(此过程由 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成)。
添加后效果图如下:
<img src="https://static001.geekbang.org/resource/image/bc/f0/bc917a82f62e8686a3c4eca64f89yyf0.png" alt="">
以上即为 Lookup 的一些关键实现思路。还有很多细节例如CGLIB子类如何产生无法一一解释有兴趣的话可以进一步深入研究留言区等你。
## 重点回顾
这节课我们介绍了3个关于Bean定义的经典错误并分析了其背后原理。
不难发现要使用好Spring就**一定要了解它的一些潜规则**例如默认扫描Bean的范围、自动装配构造器等等。如果我们不了解这些规则大多情况下虽然也能工作但是稍微变化则可能完全失效例如在案例1中我们也只是把Controller从一个包移动到另外一个包接口就失效了。
另外,通过这三个案例的分析,我们也能感受到**Spring的很多实现是通过反射来完成的**了解了这点对于理解它的源码实现会大有帮助。例如在案例2中为什么定义了多个构造器就可能报错因为使用反射方式来创建实例必须要明确使用的是哪一个构造器。
最后我想说在Spring框架中解决问题的方式往往有多种不要拘泥于套路。就像案例3使用ApplicationContext和Lookup注解都能解决原型 Bean 被固定的问题一样。
## 思考题
在案例 2 中,显示定义构造器,这会发生根据构造器参数寻找对应 Bean 的行为。这里请你思考一个问题,假设寻找不到对应的 Bean一定会如案例 2 那样直接报错么?
尝试解决一下,我们留言区见!

View File

@@ -0,0 +1,486 @@
<audio id="audio" title="02Spring Bean 依赖注入常见错误(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d8/d7/d895b3b7fd9bb04343689dc1e3ff3fd7.mp3"></audio>
你好,我是傅健,这节课我们来聊聊 Spring @Autowired
提及Spring的优势或特性我们都会立马想起“**控制反转、依赖注入**”这八字真言。而@Autowired正是用来支持依赖注入的核心利器之一。表面上看,它仅仅是一个注解,在使用上不应该出错。但是,在实际使用中,我们仍然会出现各式各样的错误,而且都堪称经典。所以这节课我就带着你学习下这些经典错误及其背后的原因,以防患于未然。
## 案例1过多赠予无所适从
在使用@Autowired时不管你是菜鸟级还是专家级的Spring使用者都应该制造或者遭遇过类似的错误
>
required a single bean, but 2 were found
顾名思义我们仅需要一个Bean但实际却提供了2个这里的“2”在实际错误中可能是其它大于1的任何数字
为了重现这个错误我们可以先写一个案例来模拟下。假设我们在开发一个学籍管理系统案例需要提供一个API根据学生的学号ID来移除学生学生的信息维护肯定需要一个数据库来支撑所以大体上可以实现如下
```
@RestController
@Slf4j
@Validated
public class StudentController {
@Autowired
DataService dataService;
@RequestMapping(path = &quot;students/{id}&quot;, method = RequestMethod.DELETE)
public void deleteStudent(@PathVariable(&quot;id&quot;) @Range(min = 1,max = 100) int id){
dataService.deleteStudent(id);
};
}
```
其中DataService是一个接口其实现依托于Oracle代码示意如下
```
public interface DataService {
void deleteStudent(int id);
}
@Repository
@Slf4j
public class OracleDataService implements DataService{
@Override
public void deleteStudent(int id) {
log.info(&quot;delete student info maintained by oracle&quot;);
}
}
```
截止目前运行并测试程序是毫无问题的。但是需求往往是源源不断的某天我们可能接到节约成本的需求希望把一些部分非核心的业务从Oracle迁移到社区版Cassandra所以我们自然会先添加上一个新的DataService实现代码如下
```
@Repository
@Slf4j
public class CassandraDataService implements DataService{
@Override
public void deleteStudent(int id) {
log.info(&quot;delete student info maintained by cassandra&quot;);
}
}
```
实际上,当我们完成支持多个数据库的准备工作时,程序就已经无法启动了,报错如下:
<img src="https://static001.geekbang.org/resource/image/80/36/80b14cf13b383e48f64f7052d6747836.png" alt="">
很显然,上述报错信息正是我们这一小节讨论的错误,那么这个错误到底是怎么产生的呢?接下来我们具体分析下。
### 案例解析
要找到这个问题的根源,我们就需要对@Autowired实现的依赖注入的原理有一定的了解。首先,我们先来了解下 @Autowired 发生的位置和核心过程。
当一个Bean被构建时核心包括两个基本步骤
1. 执行AbstractAutowireCapableBeanFactory#createBeanInstance方法通过构造器反射构造出这个Bean在此案例中相当于构建出StudentController的实例
1. 执行AbstractAutowireCapableBeanFactory#populate方法填充即设置这个Bean在本案例中相当于设置StudentController实例中被@Autowired标记的dataService属性成员
在步骤2中“填充”过程的关键就是执行各种BeanPostProcessor处理器关键代码如下
```
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
//省略非关键代码
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
//省略非关键代码
}
}
}
}
```
在上述代码执行过程中因为StudentController含有标记为Autowired的成员属性dataService所以会使用到AutowiredAnnotationBeanPostProcessorBeanPostProcessor中的一种来完成“装配”过程找出合适的DataService的bean并设置给StudentController#dataService。如果深究这个装配过程,又可以细分为两个步骤:
1. 寻找出所有需要依赖注入的字段和方法参考AutowiredAnnotationBeanPostProcessor#postProcessProperties中的代码行
```
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
```
1. 根据依赖信息寻找出依赖并完成注入以字段注入为例参考AutowiredFieldElement#inject方法
```
@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Field field = (Field) this.member;
Object value;
//省略非关键代码
try {
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
//寻找“依赖”desc为&quot;dataService&quot;的DependencyDescriptor
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
}
}
//省略非关键代码
if (value != null) {
ReflectionUtils.makeAccessible(field);
//装配“依赖”
field.set(bean, value);
}
}
```
说到这里,我们基本了解了@Autowired过程发生的位置和过程。而且很明显我们案例中的错误就发生在上述“寻找依赖”的过程中上述代码的第9行那么到底是怎么发生的呢我们可以继续刨根问底。
为了更清晰地展示错误发生的位置我们可以采用调试的视角展示其位置即DefaultListableBeanFactory#doResolveDependency中代码片段),参考下图:
<img src="https://static001.geekbang.org/resource/image/4c/f9/4cb99e17967847995bfe1d7ec0fe75f9.png" alt="">
如上图所示当我们根据DataService这个类型来找出依赖时我们会找出2个依赖分别为CassandraDataService和OracleDataService。在这样的情况下如果同时满足以下两个条件则会抛出本案例的错误
1. 调用determineAutowireCandidate方法来选出优先级最高的依赖但是发现并没有优先级可依据。具体选择过程可参考DefaultListableBeanFactory#determineAutowireCandidate
```
protected String determineAutowireCandidate(Map&lt;String, Object&gt; candidates, DependencyDescriptor descriptor) {
Class&lt;?&gt; requiredType = descriptor.getDependencyType();
String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
if (primaryCandidate != null) {
return primaryCandidate;
}
String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);
if (priorityCandidate != null) {
return priorityCandidate;
}
// Fallback
for (Map.Entry&lt;String, Object&gt; entry : candidates.entrySet()) {
String candidateName = entry.getKey();
Object beanInstance = entry.getValue();
if ((beanInstance != null &amp;&amp; this.resolvableDependencies.containsValue(beanInstance)) ||
matchesBeanName(candidateName, descriptor.getDependencyName())) {
return candidateName;
}
}
return null;
}
```
如代码所示,优先级的决策是先根据@Primary来决策,其次是@Priority决策最后是根据Bean名字的严格匹配来决策。如果这些帮助决策优先级的注解都没有被使用名字也不精确匹配则返回null告知无法决策出哪种最合适。
1. @Autowired要求是必须注入的即required保持默认值为true或者注解的属性类型并不是可以接受多个Bean的类型例如数组、Map、集合。这点可以参考DefaultListableBeanFactory#indicatesMultipleBeans的实现
```
private boolean indicatesMultipleBeans(Class&lt;?&gt; type) {
return (type.isArray() || (type.isInterface() &amp;&amp;
(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));
}
```
对比上述两个条件和我们的案例,很明显,案例程序能满足这些条件,所以报错并不奇怪。而如果我们把这些条件想得简单点,或许更容易帮助我们去理解这个设计。就像我们遭遇多个无法比较优劣的选择,却必须选择其一时,与其偷偷地随便选择一种,还不如直接报错,起码可以避免更严重的问题发生。
### 问题修正
针对这个案例,有了源码的剖析,我们可以很快找到解决问题的方法:**打破上述两个条件中的任何一个即可,即让候选项具有优先级或压根可以不去选择。**不过需要你注意的是,不是每一种条件的打破都满足实际需求,例如我们可以通过使用标记@Primary的方式来让被标记的候选者有更高优先级,从而避免报错,但是它并不一定符合业务需求,这就好比我们本身需要两种数据库都能使用,而不是顾此失彼。
```
@Repository
@Primary
@Slf4j
public class OracleDataService implements DataService{
//省略非关键代码
}
```
现在请你仔细研读上述的两个条件要同时支持多种DataService且能在不同业务情景下精确匹配到要选择到的DataService我们可以使用下面的方式去修改
```
@Autowired
DataService oracleDataService;
```
如代码所示修改方式的精髓在于将属性名和Bean名字精确匹配这样就可以让注入选择不犯难需要Oracle时指定属性名为oracleDataService需要Cassandra时则指定属性名为cassandraDataService。
## 案例 2显式引用Bean时首字母忽略大小写
针对案例1的问题修正实际上还存在另外一种常用的解决办法即采用@Qualifier来显式指定引用的是那种服务,例如采用下面的方式:
```
@Autowired()
@Qualifier(&quot;cassandraDataService&quot;)
DataService dataService;
```
这种方式之所以能解决问题在于它能让寻找出的Bean只有一个即精确匹配所以压根不会出现后面的决策过程可以参考DefaultListableBeanFactory#doResolveDependency
```
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set&lt;String&gt; autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
//省略其他非关键代码
//寻找bean过程
Map&lt;String, Object&gt; matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
return null;
}
//省略其他非关键代码
if (matchingBeans.size() &gt; 1) {
//省略多个bean的决策过程即案例1重点介绍内容
}
//省略其他非关键代码
}
```
我们会使用@Qualifier指定的名称去匹配,最终只找到了唯一一个。
不过在使用@Qualifier时我们有时候会犯另一个经典的小错误就是我们可能会忽略Bean的名称首字母大小写。这里我们把校正后的案例稍稍变形如下
```
@Autowired
@Qualifier(&quot;CassandraDataService&quot;)
DataService dataService;
```
运行程序,我们会报错如下:
>
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'studentController': Unsatisfied dependency expressed through field 'dataService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.spring.puzzle.class2.example2.DataService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=CassandraDataService)}
这里我们很容易得出一个结论:**对于Bean的名字如果没有显式指明就应该是类名不过首字母应该小写。**但是这个轻松得出的结论成立么?
不妨再测试下假设我们需要支持SQLite这种数据库我们定义了一个命名为SQLiteDataService的实现然后借鉴之前的经验我们很容易使用下面的代码来引用这个实现
```
@Autowired
@Qualifier(&quot;sQLiteDataService&quot;)
DataService dataService;
```
满怀信心运行完上面的程序依然会出现之前的错误而如果改成SQLiteDataService则运行通过了。这和之前的结论又矛盾了。所以显式引用Bean时首字母到底是大写还是小写呢
### 案例解析
对于这种错误的报错位置其实我们正好在本案例的开头就贴出了即第二段代码清单的第9行
```
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
```
即当因为名称问题例如引用Bean首字母搞错了找不到Bean时会直接抛出NoSuchBeanDefinitionException。
在这里我们真正需要关心的问题是不显式设置名字的Bean其默认名称首字母到底是大写还是小写呢
看案例的话当我们启动基于Spring Boot的应用程序时会自动扫描我们的Package以找出直接或间接标记了@Component的Bean的定义即BeanDefinition。例如CassandraDataService、SQLiteDataService都被标记了@Repository而Repository本身被@Component标记,所以它们都是间接标记了@Component
一旦找出这些Bean的信息就可以生成这些Bean的名字然后组合成一个个BeanDefinitionHolder返回给上层。这个过程关键步骤可以查看下图的代码片段ClassPathBeanDefinitionScanner#doScan
<img src="https://static001.geekbang.org/resource/image/27/49/277f3b2421a0e173a0eee56b7d65f849.png" alt="">
基本匹配我们前面描述的过程其中方法调用BeanNameGenerator#generateBeanName即用来产生Bean的名字它有两种实现方式。因为DataService的实现都是使用注解标记的所以Bean名称的生成逻辑最终调用的其实是AnnotationBeanNameGenerator#generateBeanName这种实现方式,我们可以看下它的具体实现,代码如下:
```
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
if (definition instanceof AnnotatedBeanDefinition) {
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
if (StringUtils.hasText(beanName)) {
// Explicit bean name found.
return beanName;
}
}
// Fallback: generate a unique default bean name.
return buildDefaultBeanName(definition, registry);
}
```
大体流程只有两步看Bean有没有显式指明名称如果有则用显式名称如果没有则产生一个默认名称。很明显在我们的案例中是没有给Bean指定名字的所以产生的Bean的名称就是生成的默认名称查看默认名的产生方法buildDefaultBeanName其实现如下
```
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, &quot;No bean class name set&quot;);
String shortClassName = ClassUtils.getShortName(beanClassName);
return Introspector.decapitalize(shortClassName);
}
```
首先获取一个简短的ClassName然后调用Introspector#decapitalize方法,设置首字母大写或小写,具体参考下面的代码实现:
```
public static String decapitalize(String name) {
if (name == null || name.length() == 0) {
return name;
}
if (name.length() &gt; 1 &amp;&amp; Character.isUpperCase(name.charAt(1)) &amp;&amp;
Character.isUpperCase(name.charAt(0))){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}
```
到这,我们很轻松地明白了前面两个问题出现的原因:**如果一个类名是以两个大写字母开头的,则首字母不变,其它情况下默认首字母变成小写。**结合我们之前的案例SQLiteDataService的Bean其名称应该就是类名本身而CassandraDataService的Bean名称则变成了首字母小写cassandraDataService
### 问题修正
现在我们已经从源码级别了解了Bean名字产生的规则就可以很轻松地修正案例中的两个错误了。以引用CassandraDataService类型的Bean的错误修正为例可以采用下面这两种修改方式
1. 引用处纠正首字母大小写问题:
```
@Autowired
@Qualifier(&quot;cassandraDataService&quot;)
DataService dataService;
```
1. 定义处显式指定Bean名字我们可以保持引用代码不变而通过显式指明CassandraDataService 的Bean名称为CassandraDataService来纠正这个问题。
```
@Repository(&quot;CassandraDataService&quot;)
@Slf4j
public class CassandraDataService implements DataService {
//省略实现
}
```
现在我们的程序就可以精确匹配到要找的Bean了。比较一下这两种修改方法的话如果你不太了解源码不想纠结于首字母到底是大写还是小写建议你用第二种方法去避免困扰。
## 案例 3引用内部类的Bean遗忘类名
解决完案例2是不是就意味着我们能搞定所有Bean的显式引用不再犯错了呢天真了。我们可以沿用上面的案例稍微再添加点别的需求例如我们需要定义一个内部类来实现一种新的DataService代码如下
```
public class StudentController {
@Repository
public static class InnerClassDataService implements DataService{
@Override
public void deleteStudent(int id) {
//空实现
}
}
//省略其他非关键代码
}
```
遇到这种情况我们一般都会很自然地用下面的方式直接去显式引用这个Bean
```
@Autowired
@Qualifier(&quot;innerClassDataService&quot;)
DataService innerClassDataService;
```
很明显有了案例2的经验我们上来就直接采用了**首字母小写**以避免案例2中的错误但这样的代码是不是就没问题了呢实际上仍然会报错“找不到Bean”这是为什么
### 案例解析
实际上我们遭遇的情况是“如何引用内部类的Bean”。解析案例2的时候我曾经贴出了如何产生默认Bean名的方法即AnnotationBeanNameGenerator#buildDefaultBeanName当时我们只关注了首字母是否小写的代码片段而在最后变换首字母之前有一行语句是对class名字的处理代码如下
>
String shortClassName = ClassUtils.getShortName(beanClassName);
我们可以看下它的实现参考ClassUtils#getShortName方法
```
public static String getShortName(String className) {
Assert.hasLength(className, &quot;Class name must not be empty&quot;);
int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR);
if (nameEndIndex == -1) {
nameEndIndex = className.length();
}
String shortName = className.substring(lastDotIndex + 1, nameEndIndex);
shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR);
return shortName;
}
```
很明显,假设我们是一个内部类,例如下面的类名:
>
com.spring.puzzle.class2.example3.StudentController.InnerClassDataService
在经过这个方法的处理后,我们得到的其实是下面这个名称:
>
StudentController.InnerClassDataService
最后经过Introspector.decapitalize的首字母变换最终获取的Bean名称如下
>
studentController.InnerClassDataService
所以我们在案例程序中,直接使用 innerClassDataService 自然找不到想要的Bean。
### 问题修正
通过案例解析我们很快就找到了这个内部类Bean的引用问题顺手就修正了如下
```
@Autowired
@Qualifier(&quot;studentController.InnerClassDataService&quot;)
DataService innerClassDataService;
```
这个引用看起来有些许奇怪,但实际上是可以工作的,反而直接使用 innerClassDataService 来引用倒是真的不可行。
通过这个案例我们可以看出,**对源码的学习是否全面决定了我们以后犯错的可能性大小。**如果我们在学习案例2时就对class名称的变化部分的源码进行了学习那么这种错误是不容易犯的。不过有时候我们确实很难一上来就把学习开展的全面而深入总是需要时间和错误去锤炼的。
## 重点回顾
看完这三个案例我们会发现这些错误的直接结果都是找不到合适的Bean但是原因却不尽相同。例如案例1是因为提供的Bean过多又无法决策选择谁案例2和案例3是因为指定的名称不规范导致引用的Bean找不到。
实际上这些错误在一些“聪明的”IDE会被提示出来但是它们在其它一些不太智能的主流IDE中并不能被告警出来。不过悲剧的是即使聪明的IDE也存在误报的情况所以**完全依赖IDE是不靠谱的**,毕竟这些错误都能编译过去。
另外我们的案例都是一些简化的场景很容易看出和发现问题而真实的场景往往复杂得多。例如对于案例1我们的同种类型的实现可能不是同时出现在自己的项目代码中而是有部分实现出现在依赖的Jar库中。所以你一定要对案例背后的源码实现有一个扎实的了解这样才能在复杂场景中去规避这些问题。
## 思考题
我们知道了通过@Qualifier可以引用想匹配的Bean也可以直接命名属性的名称为Bean的名称来引用这两种方式如下
```
//方式1属性命名为要装配的bean名称
@Autowired
DataService oracleDataService;
//方式2使用@Qualifier直接引用
@Autowired
@Qualifier(&quot;oracleDataService&quot;)
DataService dataService;
```
那么对于案例3的内部类引用你觉得可以使用第1种方式做到么例如使用如下代码
>
<p>@Autowired<br>
DataService studentController.InnerClassDataService;</p>
期待在留言区看到你的答案,我们下节课见!

View File

@@ -0,0 +1,465 @@
<audio id="audio" title="03Spring Bean 依赖注入常见错误(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/15/8a/15363de02655604db4d3e0f4ccca978a.mp3"></audio>
你好我是傅健这节课我们接着聊Spring的自动注入。
上一讲我们介绍了3个Spring编程中关于依赖注入的错误案例这些错误都是比较常见的。如果你仔细分析的话你会发现它们大多都是围绕着@Autowired@Qualifier的使用而发生,而且自动注入的类型也都是普通对象类型。
那在实际应用中,我们也会使用@Value等不太常见的注解来完成自动注入,同时也存在注入到集合、数组等复杂类型的场景。这些情况下,我们也会遇到一些问题。所以这一讲我们不妨来梳理下。
## 案例1@Value没有注入预期的值
在装配对象成员属性时,我们常常会使用@Autowired来装配。但是,有时候我们也使用@Value进行装配。不过这两种注解使用风格不同,使用@Autowired一般都不会设置属性值,而@Value必须指定一个字符串值,因为其定义做了要求,定义代码如下:
```
public @interface Value {
/**
* The actual value expression &amp;mdash; for example, &lt;code&gt;#{systemProperties.myProp}&lt;/code&gt;.
*/
String value();
}
```
另外在比较这两者的区别时,**我们一般都会因为@Value常用于String类型的装配而误以为@Value不能用于非内置对象的装配,实际上这是一个常见的误区**。例如我们可以使用下面这种方式来Autowired一个属性成员
```
@Value(&quot;#{student}&quot;)
private Student student;
```
其中student这个Bean定义如下
```
@Bean
public Student student(){
Student student = createStudent(1, &quot;xie&quot;);
return student;
}
```
当然,正如前面提及,我们使用@Value更多是用来装配String,而且它支持多种强大的装配方式,典型的方式参考下面的示例:
```
//注册正常字符串
@Value(&quot;我是字符串&quot;)
private String text;
//注入系统参数、环境变量或者配置文件中的值
@Value(&quot;${ip}&quot;)
private String ip
//注入其他Bean属性其中student为bean的IDname为其属性
@Value(&quot;#{student.name}&quot;)
private String name;
```
上面我给你简单介绍了@Value的强大功能,以及它和@Autowired的区别。那么在使用@Value时可能会遇到那些错误呢?这里分享一个最为典型的错误,即使用@Value可能会注入一个不是预期的值
我们可以模拟一个场景我们在配置文件application.properties配置了这样一个属性
```
username=admin
password=pass
```
然后我们在一个Bean中分别定义两个属性来引用它们
```
@RestController
@Slf4j
public class ValueTestController {
@Value(&quot;${username}&quot;)
private String username;
@Value(&quot;${password}&quot;)
private String password;
@RequestMapping(path = &quot;user&quot;, method = RequestMethod.GET)
public String getUser(){
return username + &quot;:&quot; + password;
};
}
```
当我们去打印上述代码中的username和password时我们会发现password正确返回了但是username返回的并不是配置文件中指明的admin而是运行这段程序的计算机用户名。很明显使用@Value装配的值没有完全符合我们的预期
### 案例解析
通过分析运行结果,我们可以知道@Value的使用方式应该是没有错的毕竟password这个字段装配上了但是为什么username没有生效成正确的值接下来我们就来具体解析下。
我们首先了解下对于@ValueSpring是如何根据@Value来查询“值”的。我们可以先通过方法DefaultListableBeanFactory#doResolveDependency来了解@Value的核心工作流程,代码如下:
```
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set&lt;String&gt; autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
//省略其他非关键代码
Class&lt;?&gt; type = descriptor.getDependencyType();
//寻找@Value
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
if (value instanceof String) {
//解析Value值
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null &amp;&amp; containsBean(beanName) ?
getMergedBeanDefinition(beanName) : null);
value = evaluateBeanDefinitionString(strVal, bd);
}
//转化Value解析的结果到装配的类型
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
try {
return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
}
catch (UnsupportedOperationException ex) {
//异常处理
}
}
//省略其他非关键代码
}
```
​可以看到,@Value的工作大体分为以下三个核心步骤
**1. 寻找@Value**
在这步中,主要是判断这个属性字段是否标记为@Value依据的方法参考QualifierAnnotationAutowireCandidateResolver#findValue
```
@Nullable
protected Object findValue(Annotation[] annotationsToSearch) {
if (annotationsToSearch.length &gt; 0) {
AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
//valueAnnotationType即为@Value
if (attr != null) {
return extractValue(attr);
}
}
return null;
}
```
**2. 解析@Value的字符串值**
如果一个字段标记了@Value,则可以拿到对应的字符串值,然后就可以根据字符串值去做解析,最终解析的结果可能是一个字符串,也可能是一个对象,这取决于字符串怎么写。
**3. 将解析结果转化为要装配的对象的类型**
当拿到第二步生成的结果后我们会发现可能和我们要装配的类型不匹配。假设我们定义的是UUID而我们获取的结果是一个字符串那么这个时候就会根据目标类型来寻找转化器执行转化字符串到UUID的转化实际上发生在UUIDEditor中
```
public class UUIDEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text)) {
//转化操作
setValue(UUID.fromString(text.trim()));
}
else {
setValue(null);
}
}
//省略其他非关代码
}
```
通过对上面几个关键步骤的解析,我们大体了解了@Value的工作流程。结合我们的案例很明显问题应该发生在第二步即解析Value指定字符串过程执行过程参考下面的关键代码行
```
String strVal = resolveEmbeddedValue((String) value);
```
这里其实是在解析嵌入的值实际上就是“替换占位符”工作。具体而言它采用的是PropertySourcesPlaceholderConfigurer根据PropertySources来替换。不过当使用 ${username} 来获取替换值时其最终执行的查找并不是局限在application.property文件中的。通过调试我们可以看到下面的这些“源”都是替换依据
<img src="https://static001.geekbang.org/resource/image/25/40/25d4242bc0dae8fa730663b9122b7840.png" alt="">
```
[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'}]
```
而具体的查找执行我们可以通过下面的代码PropertySourcesPropertyResolver#getProperty)来获取它的执行方式:
```
@Nullable
protected &lt;T&gt; T getProperty(String key, Class&lt;T&gt; targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource&lt;?&gt; propertySource : this.propertySources) {
Object value = propertySource.getProperty(key);
if (value != null) {
//查到value即退出
return convertValueIfNecessary(value, targetValueType);
}
}
}
return null;
}
```
从这可以看出在解析Value字符串时其实是有顺序的查找的源是存在CopyOnWriteArrayList中在启动时就被有序固定下来一个一个“源”执行查找在其中一个源找到后就可以直接返回了。
如果我们查看systemEnvironment这个源会发现刚好有一个username和我们是重合的且值不是pass。
<img src="https://static001.geekbang.org/resource/image/eb/28/eb48b0d27dc7d0dyy32a548934edc728.png" alt="">
所以讲到这里你应该知道问题所在了吧这是一个误打误撞的例子刚好系统环境变量systemEnvironment中含有同名的配置。实际上对于系统参数systemProperties也是一样的这些参数或者变量都有很多如果我们没有意识到它的存在起了一个同名的字符串作为@Value的值,则很容易引发这类问题。
### 问题修正
针对这个案例,有了源码的剖析,我们就可以很快地找到解决方案了。例如我们可以避免使用同一个名称,具体修改如下:
```
user.name=admin
user.password=pass
```
但是如果我们这么改的话其实还是不行的。实际上通过之前的调试方法我们可以找到类似的原因在systemProperties这个PropertiesPropertySource源中刚好存在user.name真是无巧不成书。所以命名时我们一定要注意**不仅要避免和环境变量冲突,也要注意避免和系统变量等其他变量冲突**,这样才能从根本上解决这个问题。
通过这个案例我们可以知道Spring给我们提供了很多好用的功能但是这些功能交织到一起后就有可能让我们误入一些坑只有了解它的运行方式我们才能迅速定位问题、解决问题。
## 案例2错乱的注入集合
前面我们介绍了很多自动注入的错误案例,但是这些案例都局限在单个类型的注入,对于集合类型的注入并无提及。实际上,**集合类型的自动注入是Spring提供的另外一个强大功能。**
假设我们存在这样一个需求存在多个学生Bean我们需要找出来并存储到一个List里面去。多个学生Bean的定义如下
```
@Bean
public Student student1(){
return createStudent(1, &quot;xie&quot;);
}
@Bean
public Student student2(){
return createStudent(2, &quot;fang&quot;);
}
private Student createStudent(int id, String name) {
Student student = new Student();
student.setId(id);
student.setName(name);
return student;
}
```
有了集合类型的自动注入后我们就可以把零散的学生Bean收集起来了代码示例如下
```
@RestController
@Slf4j
public class StudentController {
private List&lt;Student&gt; students;
public StudentController(List&lt;Student&gt; students){
this.students = students;
}
@RequestMapping(path = &quot;students&quot;, method = RequestMethod.GET)
public String listStudents(){
return students.toString();
};
}
```
通过上述代码,我们就可以完成集合类型的注入工作,输出结果如下:
>
[Student(id=1, name=xie), Student(id=2, name=fang)]
然而业务总是复杂的需求也是一直变动的。当我们持续增加一些student时可能就不喜欢用这种方式来注入集合类型了而是倾向于用下面的方式去完成注入工作
```
@Bean
public List&lt;Student&gt; students(){
Student student3 = createStudent(3, &quot;liu&quot;);
Student student4 = createStudent(4, &quot;fu&quot;);
return Arrays.asList(student3, student4);
}
```
为了好记,这里我们不妨将上面这种方式命名为“直接装配方式”,而将之前的那种命名为“收集方式”。
实际上如果这两种方式是非此即彼的存在自然没有任何问题都能玩转。但是如果我们不小心让这2种方式同时存在了结果会怎样
这时候很多人都会觉得Spring很强大肯定会合并上面的结果或者认为肯定是以直接装配结果为准。然而当我们运行起程序就会发现后面的注入方式根本没有生效。即依然返回的是前面定义的2个学生。为什么会出现这样的错误呢
### 案例解析
要了解这个错误的根本原因你就得先清楚这两种注入风格在Spring中是如何实现的。对于收集装配风格Spring使用的是DefaultListableBeanFactory#resolveMultipleBeans来完成装配工作,针对本案例关键的核心代码如下:
```
private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set&lt;String&gt; autowiredBeanNames, @Nullable TypeConverter typeConverter) {
final Class&lt;?&gt; type = descriptor.getDependencyType();
if (descriptor instanceof StreamDependencyDescriptor) {
//装配stream
return stream;
}
else if (type.isArray()) {
//装配数组
return result;
}
else if (Collection.class.isAssignableFrom(type) &amp;&amp; type.isInterface()) {
//装配集合
//获取集合的元素类型
Class&lt;?&gt; elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
if (elementType == null) {
return null;
}
//根据元素类型查找所有的bean
Map&lt;String, Object&gt; matchingBeans = findAutowireCandidates(beanName, elementType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
//转化查到的所有bean放置到集合并返回
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
Object result = converter.convertIfNecessary(matchingBeans.values(), type);
//省略非关键代码
return result;
}
else if (Map.class == type) {
//解析map
return matchingBeans;
}
else {
return null;
}
}
```
到这,我们就不难概括出这种收集式集合装配方式的大体过程了。
**1. 获取集合类型的元素类型**
针对本案例目标类型定义为List&lt;Student&gt; students所以元素类型为Student获取的具体方法参考代码行
>
Class&lt;?&gt; elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
**2. 根据元素类型找出所有的Bean**
有了上面的元素类型即可根据元素类型来找出所有的Bean关键代码行如下
>
Map&lt;String, Object&gt; matchingBeans = findAutowireCandidates(beanName, elementType, new MultiElementDescriptor(descriptor));
**3. 将匹配的所有的Bean按目标类型进行转化**
经过步骤2我们获取的所有的Bean都是以java.util.LinkedHashMap.LinkedValues形式存储的和我们的目标类型大概率不同所以最后一步需要做的是**按需转化**。在本案例中我们就需要把它转化为List转化的关键代码如下
>
Object result = converter.convertIfNecessary(matchingBeans.values(), type);
如果我们继续深究执行细节就可以知道最终是转化器CollectionToCollectionConverter来完成这个转化过程。
学习完收集方式的装配原理我们再来看下直接装配方式的执行过程实际上这步在前面的课程中我们就提到过即DefaultListableBeanFactory#findAutowireCandidates方法执行),具体的执行过程这里就不多说了。
知道了执行过程接下来无非就是根据目标类型直接寻找匹配的Bean。在本案例中就是将Bean名称为students的List&lt;Student&gt;装配给StudentController#students属性
了解了这两种方式我们再来思考这两种方式的关系当同时满足这两种装配方式时Spring是如何处理的这里我们可以参考方法DefaultListableBeanFactory#doResolveDependency的几行关键代码,代码如下:
```
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}
Map&lt;String, Object&gt; matchingBeans = findAutowireCandidates(beanName, type, descriptor);
```
很明显,这两种装配集合的方式是**不能同存**的结合本案例当使用收集装配方式来装配时能找到任何一个对应的Bean则返回如果一个都没有找到才会采用直接装配的方式。说到这里你大概能理解为什么后期以List方式直接添加的Student Bean都不生效了吧。
### 问题修正
现在如何纠正这个问题就变得简单多了就是你一定要下意识地避免这2种方式共存去装配集合只用一个这个问题就迎刃而解了。例如在这里我们可以使用直接装配的方式去修正问题代码如下
```
@Bean
public List&lt;Student&gt; students(){
Student student1 = createStudent(1, &quot;xie&quot;);
Student student2 = createStudent(2, &quot;fang&quot;);
Student student3 = createStudent(3, &quot;liu&quot;);
Student student4 = createStudent(4, &quot;fu&quot;);
return Arrays.asList(student1student2student3, student4);
}
```
也可以使用收集方式来修正问题时,代码如下:
```
@Bean
public Student student1(){
return createStudent(1, &quot;xie&quot;);
}
@Bean
public Student student2(){
return createStudent(2, &quot;fang&quot;);
}
@Bean
public Student student3(){
return createStudent(3, &quot;liu&quot;);
}
@Bean
public Student student4(){
return createStudent(4, &quot;fu&quot;);
}
```
总之,都是可以的。还有一点要注意:**在对于同一个集合对象的注入上,混合多种注入方式是不可取的,这样除了错乱,别无所得。**
## 重点回顾
今天我们又学习了关于Spring自动注入的两个典型案例。
通过案例1的学习我们了解到@Value不仅可以用来注入String类型也可以注入自定义对象类型。同时在注入String时你一定要意识到它不仅仅可以用来引用配置文件里配置的值也可能引用到环境变量、系统参数等。
而通过案例2的学习我们了解到集合类型的注入支持两种常见的方式即上文中我们命名的收集装配式和直接装配式。这两种方式共同装配一个属性时后者就会失效。
综合上一讲的内容我们一共分析了5个问题以及背后的原理通过这些案例的分析我们不难看出Spring的自动注入非常强大围绕@Autowired@Qualifier@Value等内置注解,我们可以完成不同的注入目标和需求。不过这种强大,正如我在[开篇词](https://time.geekbang.org/column/article/364661)中提及的,它建立在很多隐性的规则之上。只有你把这些规则都烂熟于心了,才能很好地去规避问题。
## 思考题
在案例2中我们初次运行程序获取的结果如下
>
[Student(id=1, name=xie), Student(id=2, name=fang)]
那么如何做到让学生2优先输出呢
我们留言区见!

View File

@@ -0,0 +1,520 @@
<audio id="audio" title="04Spring Bean 生命周期常见错误" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a4/c9/a444280c3283aa2f5b6f19e919e674c9.mp3"></audio>
你好,我是傅健,这节课我们来聊一聊 Spring Bean 的初始化过程及销毁过程中的一些问题。
虽然说 Spring 容器上手简单,可以仅仅通过学习一些有限的注解,即可达到快速使用的目的。但在工程实践中,我们依然会从中发现一些常见的错误。尤其当你对 Spring 的生命周期还没有深入了解时,类初始化及销毁过程中潜在的约定就不会很清楚。
这会导致这样一些状况发生:有些错误,我们可以在 Spring 的异常提示下快速解决,但却不理解背后的原理;而另一些错误,并不容易在开发环境下被发现,从而在产线上造成较为严重的后果。
接下来我们就具体解析下这些常见案例及其背后的原理。
## 案例 1构造器内抛空指针异常
先看个例子。在构建宿舍管理系统时,有 LightMgrService 来管理 LightService从而控制宿舍灯的开启和关闭。我们希望在 LightMgrService 初始化时能够自动调用 LightService 的 check 方法来检查所有宿舍灯的电路是否正常,代码如下:
```
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 方法:
```
@Service
public class LightService {
public void start() {
System.out.println(&quot;turn on all lights&quot;);
}
public void shutdown() {
System.out.println(&quot;turn off all lights&quot;);
}
public void check() {
System.out.println(&quot;check all lights&quot;);
}
}
```
以上代码定义了 LightService 对象的原始类。
从整个案例代码实现来看,我们的期待是在 LightMgrService 初始化过程中LightService 因为标记为 @Autowired,所以能被自动装配好;然后在 LightMgrService 的构造器执行中LightService 的 shutdown() 方法能被自动调用;最终打印出 check all lights。
然而事与愿违,我们得到的只会是 NullPointerException错误示例如下
<img src="https://static001.geekbang.org/resource/image/4d/4e/4d4cecc9c82abaaa4f04cdb274c05e4e.png" alt="">
这是为什么呢?
### 案例解析
显然这是新手最常犯的错误,但是问题的根源,是我们**对Spring类初始化过程没有足够的了解**。下面这张时序图描述了 Spring 启动时的一些关键结点:
<img src="https://static001.geekbang.org/resource/image/6f/8a/6ff70ab627711065bc17c54c001ef08a.png" alt="">
这个图初看起来复杂,我们不妨将其分为三部分:
- 第一部分,将一些必要的系统类,比如 Bean 的后置处理器类,注册到 Spring 容器,其中就包括我们这节课关注的 CommonAnnotationBeanPostProcessor 类;
- 第二部分,将这些后置处理器实例化,并注册到 Spring 的容器中;
- 第三部分,实例化所有用户定制类,调用后置处理器进行辅助装配、类初始化等等。
第一部分和第二部分并非是我们今天要讨论的重点,这里仅仅是为了让你知道 CommonAnnotationBeanPostProcessor 这个后置处理类是何时被 Spring 加载和实例化的。
**这里我顺便给你拓展两个知识点:**
1. 很多必要的系统类,尤其是 Bean 后置处理器比如CommonAnnotationBeanPostProcessor、AutowiredAnnotationBeanPostProcessor 等),都是被 Spring 统一加载和管理的,并在 Spring 中扮演了非常重要的角色;
1. 通过 Bean 后置处理器Spring 能够非常灵活地在不同的场景调用不同的后置处理器,比如接下来我会讲到示例问题如何修正,修正方案中提到的 PostConstruct 注解,它的处理逻辑就需要用到 CommonAnnotationBeanPostProcessor继承自 InitDestroyAnnotationBeanPostProcessor这个后置处理器。
现在我们重点看下第三部分,即 Spring 初始化单例类的一般过程,基本都是 getBean()-&gt;doGetBean()-&gt;getSingleton(),如果发现 Bean 不存在,则调用 createBean()-&gt;doCreateBean() 进行实例化。
查看 doCreateBean() 的源代码如下:
```
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() &gt;SimpleInstantiationStrategy.instantiate(),最终执行到 BeanUtils.instantiateClass(),其代码如下:
```
public static &lt;T&gt; T instantiateClass(Constructor&lt;T&gt; ctor, Object... args) throws BeanInstantiationException {
Assert.notNull(ctor, &quot;Constructor must not be null&quot;);
try {
ReflectionUtils.makeAccessible(ctor);
return (KotlinDetector.isKotlinReflectPresent() &amp;&amp; KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ?
KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));
}
catch (InstantiationException ex) {
throw new BeanInstantiationException(ctor, &quot;Is it an abstract class?&quot;, ex);
}
//省略非关键代码
}
```
这里因为当前的语言并非 Kotlin所以最终将调用 ctor.newInstance() 方法实例化用户定制类 LightMgrService而默认构造器显然是在类实例化的时候被自动调用的Spring 也无法控制。而此时负责自动装配的 populateBean 方法还没有被执行LightMgrService 的属性 LightService 还是 null因而得到空指针异常也在情理之中。
### 问题修正
通过源码分析,现在我们知道了问题的根源,就是在于**使用 @Autowired 直接标记在成员属性上而引发的装配行为是发生在构造器执行之后的**。所以这里我们可以通过下面这种修订方法来纠正这个问题:
```
@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 方法,我们来看一下它的关键代码:
```
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 的父类):
```
private LifecycleMetadata buildLifecycleMetadata(final Class&lt;?&gt; clazz) {
//省略非关键代码
do {
//省略非关键代码
final List&lt;LifecycleElement&gt; currDestroyMethods = new ArrayList&lt;&gt;();
ReflectionUtils.doWithLocalMethods(targetClass, method -&gt; {
//此处的 this.initAnnotationType 值,即为 PostConstruct.class
if (this.initAnnotationType != null &amp;&amp; method.isAnnotationPresent(this.initAnnotationType)) {
LifecycleElement element = new LifecycleElement(method);
currInitMethods.add(element);
//非关键代码
}
```
在这个方法里Spring 将遍历查找被 PostConstruct.class 注解过的方法,返回到上层,并最终调用此方法。
**2. invokeInitMethods 与 InitializingBean 接口**
invokeInitMethods 方法会判断当前 Bean 是否实现了 InitializingBean 接口只有在实现了该接口的情况下Spring 才会调用该 Bean 的接口实现方法 afterPropertiesSet()。
```
protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
throws Throwable {
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean &amp;&amp; (mbd == null || !mbd.isExternallyManagedInitMethod(&quot;afterPropertiesSet&quot;))) {
// 省略非关键代码
else {
((InitializingBean) bean).afterPropertiesSet();
}
}
// 省略非关键代码
}
```
学到此处,答案也就呼之欲出了。我们还有两种方式可以解决此问题。
1. 添加 init 方法,并且使用 PostConstruct 注解进行修饰:
```
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() 方法中执行初始化代码:
```
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 方法,负责关闭所有的灯,关键代码如下:
```
import org.springframework.stereotype.Service;
@Service
public class LightService {
//省略其他非关键代码
public void shutdown(){
System.out.println(&quot;shutting down all lights&quot;);
}
//省略其他非关键代码
}
```
在之前的案例中,如果我们的宿舍管理系统在重启时,灯是不会被关闭的。但是随着业务的需求变化,我们可能会去掉 @Service 注解,而是使用另外一种产生 Bean 的方式:创建一个配置类 BeanConfiguration标记 @Configuration)来创建一堆 Bean其中就包含了创建 LightService 类型的 Bean并将其注册到 Spring 容器:
```
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 上下文。这样等同于模拟宿舍管理系统的启停:
```
@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 方法,而使用 @ComponentService 也是一种 Component将当前类自动注入到 Spring 容器时shutdown 方法则不会被自动执行。
我们可以尝试到 Bean 注解类的代码中去寻找一些线索,可以看到属性 destroyMethod 有非常大段的注释,基本上解答了我们对于这个问题的大部分疑惑。
使用 Bean 注解的方法所注册的 Bean 对象,如果用户不设置 destroyMethod 属性,则其属性值为 AbstractBeanDefinition.INFER_METHOD。此时 Spring 会检查当前 Bean 对象的原始类中是否有名为 shutdown 或者 close 的方法,如果有,此方法会被 Spring 记录下来,并在容器被销毁时自动执行;当然如若没有,那么自然什么都不会发生。
下面我们继续查看 Spring 的源代码来进一步分析此问题。
首先我们可以查找 INFER_METHOD 枚举值的引用,很容易就找到了使用该枚举值的方法 DisposableBeanAdapter#inferDestroyMethodIfNecessary
```
private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {
String destroyMethodName = beanDefinition.getDestroyMethodName();
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||(destroyMethodName == null &amp;&amp; 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-&gt;registerDisposableBeanIfNecessary-&gt;registerDisposableBean(new DisposableBeanAdapter)-&gt;inferDestroyMethodIfNecessary。
然后,我们追溯到了顶层的 doCreateBean 方法,代码如下:
```
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, &quot;Invalid destruction signature&quot;, ex);
}
return exposedObject;
}
```
到这,我们就可以对 doCreateBean 方法做一个小小的总结了。可以说 **doCreateBean 管理了Bean的整个生命周期中几乎所有的关键节点**,直接负责了 Bean 对象的生老病死,其主要功能包括:
- Bean 实例的创建;
- Bean 对象依赖的注入;
- 定制类初始化方法的回调;
- Disposable 方法的注册。
接着,继续查看 registerDisposableBean 方法:
```
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 方法:
```
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 属性设置为空的方式来解决这个问题。
第一种修改方式比较简单,所以这里只展示第二种修改方式,代码如下:
```
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
@Bean(destroyMethod=&quot;&quot;)
public LightService getTransmission(){
return new LightService();
}
}
```
另外,针对这个问题我想再多提示一点。如果我们能**养成良好的编码习惯**,在使用某个不熟悉的注解之前,认真研读一下该注解的注释,也可以大概率规避这个问题。
不过说到这里,你也可能还是会疑惑,为什么 @Service 注入的 LightService其 shutdown 方法不能被执行?这里我想补充说明下。
想要执行,则必须要添加 DisposableBeanAdapter而它的添加是有条件的
```
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) {
AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null);
if (!mbd.isPrototype() &amp;&amp; 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() &amp;&amp; requiresDestruction(bean, mbd)
很明显在案例代码修改前后我们都是单例所以区别仅在于是否满足requiresDestruction 条件。翻阅它的代码最终的关键调用参考DisposableBeanAdapter#hasDestroyMethod
```
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 的定义:
```
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 方法的注册等全部关键节点。
1. 后置处理器是 Spring 中最优雅的设计之一对于很多功能注解的处理都是借助于后置处理器来完成的。虽然这节课对其没有过多介绍但在第一个案例中Bean 对象“补充”初始化动作却是在 CommonAnnotationBeanPostProcessor继承自 InitDestroyAnnotationBeanPostProcessor这个后置处理器中完成的。
## 思考题
案例 2 中的类 LightService当我们不在 Configuration 注解类中使用 Bean 方法将其注入 Spring 容器,而是坚持使用 @Service 将其自动注入到容器,同时实现 Closeable 接口,代码如下:
```
import org.springframework.stereotype.Component;
import java.io.Closeable;
@Service
public class LightService implements Closeable {
public void close() {
System.out.println(&quot;turn off all lights);
}
//省略非关键代码
}
```
接口方法 close() 也会在 Spring 容器被销毁的时候自动执行么?
我在留言区期待你的答案!

View File

@@ -0,0 +1,549 @@
<audio id="audio" title="05Spring AOP 常见错误(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b5/e2/b5def29f36db744c283fff8d7066c2e2.mp3"></audio>
你好我是傅健。这节课开始我们聊聊Spring AOP使用中常遇到的一些问题。
Spring AOP是Spring中除了依赖注入外DI最为核心的功能顾名思义AOP即Aspect Oriented Programming翻译为面向切面编程。
而Spring AOP则利用CGlib和JDK动态代理等方式来实现运行期动态方法增强其目的是将与业务无关的代码单独抽离出来使其逻辑不再与业务代码耦合从而降低系统的耦合性提高程序的可重用性和开发效率。因而AOP便成为了日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用的技术。
追根溯源我们之所以能无感知地在容器对象方法前后任意添加代码片段那是由于Spring在运行期帮我们把切面中的代码逻辑动态“织入”到了容器对象方法内所以说**AOP本质上就是一个代理模式**。然而在使用这种代理模式时,我们常常会用不好,那么这节课我们就来解析下有哪些常见的问题,以及背后的原理是什么。
## 案例1this调用的当前类方法无法被拦截
假设我们正在开发一个宿舍管理系统这个模块包含一个负责电费充值的类ElectricService它含有一个充电方法charge()
```
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println(&quot;Electric charging ...&quot;);
this.pay();
}
public void pay() throws Exception {
System.out.println(&quot;Pay with alipay ...&quot;);
Thread.sleep(1000);
}
}
```
在这个电费充值方法charge()中我们会使用支付宝进行充值。因此在这个方法中我加入了pay()方法。为了模拟pay()方法调用耗时代码执行了休眠1秒并在charge()方法里使用 this.pay()的方式调用这种支付方法。
但是因为支付宝支付是第三方接口,我们需要记录下接口调用时间。这时候我们就引入了一个@Around的增强 分别记录在pay()方法执行前后的时间并计算出执行pay()方法的耗时。
```
@Aspect
@Service
@Slf4j
public class AopConfig {
@Around(&quot;execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) &quot;)
public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println(&quot;Pay method time costms: &quot; + (end - start));
}
}
```
最后我们再通过定义一个Controller来提供电费充值接口定义如下
```
@RestController
public class HelloWorldController {
@Autowired
ElectricService electricService;
@RequestMapping(path = &quot;charge&quot;, method = RequestMethod.GET)
public void charge() throws Exception{
electricService.charge();
};
}
```
完成代码后,我们访问上述接口,会发现这段计算时间的切面并没有执行到,输出日志如下:
>
<p>Electric charging ...<br>
Pay with alipay ...</p>
回溯之前的代码可知,在@Around的切面类中我们很清晰地定义了切面对应的方法但是却没有被执行到。这说明了在类的内部通过this方式调用的方法是没有被Spring AOP增强的。这是为什么呢我们来分析一下。
### 案例解析
我们可以从源码中找到真相。首先来设置个断点调试看看this对应的对象是什么样的
<img src="https://static001.geekbang.org/resource/image/e0/5f/e0f4b047228fac437d57f56dcd18185f.png" alt="">
可以看到this对应的就是一个普通的ElectricService对象并没有什么特别的地方。再看看在Controller层中自动装配的ElectricService对象是什么样
<img src="https://static001.geekbang.org/resource/image/b2/f9/b24f00b4b96c46983295da05180174f9.png" alt="">
可以看到这是一个被Spring增强过的Bean所以执行charge()方法时会执行记录接口调用时间的增强操作。而this对应的对象只是一个普通的对象并没有做任何额外的增强。
为什么this引用的对象只是一个普通对象呢这还要从Spring AOP增强对象的过程来看。但在此之前有些基础我需要在这里强调下。
**1. Spring AOP的实现**
Spring AOP的底层是动态代理。而创建代理的方式有两种**JDK的方式和CGLIB的方式**。JDK动态代理只能对实现了接口的类生成代理而不能针对普通类。而CGLIB是可以针对类实现代理主要是对指定的类生成一个子类覆盖其中的方法来实现代理对象。具体区别可参考下图
<img src="https://static001.geekbang.org/resource/image/99/a1/99c74d82d811ec567b28a24ccd6e85a1.png" alt="">
**2. 如何使用Spring AOP**
在Spring Boot中我们一般只要添加以下依赖就可以直接使用AOP功能
>
<p>&lt;dependency&gt;<br>
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>
&lt;artifactId&gt;spring-boot-starter-aop&lt;/artifactId&gt;<br>
&lt;/dependency&gt;</p>
而对于非Spring Boot程序除了添加相关AOP依赖项外我们还常常会使用@EnableAspectJAutoProxy来开启AOP功能。这个注解类引入ImportAspectJAutoProxyRegistrar它通过实现ImportBeanDefinitionRegistrar的接口方法来完成AOP相关Bean的准备工作。
补充完最基本的Spring底层知识和使用知识后我们具体看下创建代理对象的过程。先来看下调用栈
<img src="https://static001.geekbang.org/resource/image/1f/2a/1fb3735e51a8e06833f065a175517c2a.png" alt="">
创建代理对象的时机就是创建一个Bean的时候而创建的的关键工作其实是由AnnotationAwareAspectJAutoProxyCreator完成的。它本质上是一种BeanPostProcessor。所以它的执行是在完成原始Bean构建后的初始化BeaninitializeBean过程中。而它到底完成了什么工作呢我们可以看下它的postProcessAfterInitialization方法
```
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过程可参考下面的关键代码行
```
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、被代理对象等信息加入到代理工厂最后通过这个代理工厂来获取代理对象。一些关键过程参考下面的方法
```
protected Object createProxy(Class&lt;?&gt; 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的方式,在类的内部,自己引用自己:
```
@Service
public class ElectricService {
@Autowired
ElectricService electricService;
public void charge() throws Exception {
System.out.println(&quot;Electric charging ...&quot;);
//this.pay();
electricService.pay();
}
public void pay() throws Exception {
System.out.println(&quot;Pay with alipay ...&quot;);
Thread.sleep(1000);
}
}
```
另一种方法就是直接从AopContext获取当前的Proxy。那你可能会问了AopContext是什么简单说它的核心就是通过一个ThreadLocal来将Proxy和线程绑定起来这样就可以随时拿出当前线程绑定的Proxy。
不过使用这种方法有个小前提,就是需要在@EnableAspectJAutoProxy里加一个配置项exposeProxy = true表示将代理对象放入到ThreadLocal这样才可以直接通过 AopContext.currentProxy()的方式获取到,否则会报错如下:
<img src="https://static001.geekbang.org/resource/image/0e/98/0e42f3129e1c098b0f860f1f7f2e6298.png" alt="">
按这个思路,我们修改下相关代码:
```
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println(&quot;Electric charging ...&quot;);
ElectricService electric = ((ElectricService) AopContext.currentProxy());
electric.pay();
}
public void pay() throws Exception {
System.out.println(&quot;Pay with alipay ...&quot;);
Thread.sleep(1000);
}
}
```
同时不要忘记修改EnableAspectJAutoProxy注解的exposeProxy属性示例如下
```
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
// 省略非关键代码
}
```
这两种方法的效果其实是一样的,最终我们打印出了期待的日志,到这,问题顺利解决了。
```
Electric charging ...
Pay with alipay ...
Pay method time cost(ms): 1005
```
## 案例2直接访问被拦截类的属性抛空指针异常
接上一个案例在宿舍管理系统中我们使用了charge()方法进行支付。在统一结算的时候我们会用到一个管理员用户付款编号,这时候就用到了几个新的类。
User类包含用户的付款编号信息
```
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()方法,用来登录系统。
```
@Service
public class AdminUserService {
public final User adminUser = new User(&quot;202101166&quot;);
public void login() {
System.out.println(&quot;admin user login...&quot;);
}
}
```
我们需要修改ElectricService类实现这个需求在电费充值时需要管理员登录并使用其编号进行结算。完整代码如下
```
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(&quot;Electric charging ...&quot;);
this.pay();
}
public void pay() throws Exception {
adminUserService.login();
String payNum = adminUserService.adminUser.getPayNum();
System.out.println(&quot;User pay num : &quot; + payNum);
System.out.println(&quot;Pay with alipay ...&quot;);
Thread.sleep(1000);
}
}
```
代码完成后执行charge()操作,一切正常:
```
Electric charging ...
admin user login...
User pay num : 202101166
Pay with alipay ...
```
这时候由于安全需要就需要管理员在登录时记录一行日志以便于以后审计管理员操作。所以我们添加一个AOP相关配置类具体如下
```
@Aspect
@Service
@Slf4j
public class AopConfig {
@Before(&quot;execution(* com.spring.puzzle.class5.example2.AdminUserService.login(..)) &quot;)
public void logAdminLogin(JoinPoint pjp) throws Throwable {
System.out.println(&quot;! admin login ...&quot;);
}
}
```
添加这段代码后我们执行charge()操作发现不仅没有相关日志而且在执行下面这一行代码的时候直接抛出了NullPointerException
>
String payNum = dminUserService.user.getPayNum();
本来一切正常的代码因为引入了一个AOP切面抛出了NullPointerException。这会是什么原因呢我们先debug一下来看看加入AOP后调用的对象是什么样子。
<img src="https://static001.geekbang.org/resource/image/cd/a2/cd48479a45c2b06621c2e07a33f519a2.png" alt="">
可以看出加入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获取代理对象
```
public Object getProxy(ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}
```
在这里我们以CGLIB的Proxy的实现类CglibAopProxy为例来看看具体的流程
```
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()方法:
```
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
//创建代理类Class
Class&lt;?&gt; 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&lt;?&gt; 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方式实例化对象的流程。
<img src="https://static001.geekbang.org/resource/image/42/34/422160a6fd0c3ee1af8b05769a015834.png" alt="">
参照上述截图所示调用栈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()方法:
```
public User getUser() {
return user;
}
```
在ElectricService里通过getUser()获取User对象
>
<p>//原来出错的方式:<br>
//String payNum = = adminUserService.adminUser.getPayNum();<br>
//修改后的方式:<br>
String payNum = adminUserService.getAdminUser().getPayNum();</p>
运行下来,一切正常,可以看到管理员登录日志了:
```
Electric charging ...
! admin login ...
admin user login...
User pay num : 202101166
Pay with alipay ...
```
但你有没有产生另一个困惑呢既然代理类的类属性不会被初始化那为什么可以通过在AdminUserService里写个getUser()方法来获取代理类实例的属性呢?
我们再次回顾createProxyClassAndInstance的代码逻辑创建代理类后我们会调用setCallbacks来设置拦截后需要注入的代码
```
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
Class&lt;?&gt; 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它的接口是MethodInterceptorcallback的子接口实现了拦截方法intercept()。我们可以看下它是如何实现这个方法的:
```
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 省略非关键代码
TargetSource targetSource = this.advised.getTargetSource();
// 省略非关键代码
if (chain.isEmpty() &amp;&amp; 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如下
<img src="https://static001.geekbang.org/resource/image/83/7e/83e34cbd460ac74c5d623905dce0497e.png" alt="">
此时再调试程序你会发现adminUser已经不为null了
<img src="https://static001.geekbang.org/resource/image/3b/b1/3b2dd77392c3b439d0a182f5817045b1.png" alt="">
所以这也是解决这个问题的一种方法,相信聪明的你已经能从前文贴出的代码中找出它能够工作起来的原理了。
## 重点回顾
通过以上两个案例的介绍相信你对Spring AOP动态代理的初始化机制已经有了进一步的了解这里总结重点如下
<li>
使用AOP实际上就是让Spring自动为我们创建一个Proxy使得调用者能无感知地调用指定方法。而Spring有助于我们在运行期里动态织入其它逻辑因此AOP本质上就是一个动态代理。
</li>
<li>
我们只有访问这些代理对象的方法才能获得AOP实现的功能所以通过this引用是无法正确使用AOP功能的。在不能改变代码结果前提下我们可以通过@Autowired、AopContext.currentProxy()等方式获取相应的代理对象来实现所需的功能。
</li>
<li>
我们一般不能直接从代理类中去拿被代理类的属性这是因为除非我们显示设置spring.objenesis.ignore为true否则代理类的属性是不会被Spring初始化的我们可以通过在被代理类中增加一个方法来间接获取其属性。
</li>
## 思考题
第二个案例中,我们提到了通过反射来实例化类的三种方式:
- java.lang.Class.newInsance()
- java.lang.reflect.Constructor.newInstance()
- sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()
其中第三种方式不会初始化类属性,你能够写一个例子来证明这一点吗?
期待你的思考,我们留言区见!

View File

@@ -0,0 +1,506 @@
<audio id="audio" title="06Spring AOP 常见错误(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/38/61/38a05d0d652ec8ae40959310f4dc7261.mp3"></audio>
你好,我是傅健。
上一节课,我们介绍了 Spring AOP 常遇到的几个问题,通过具体的源码解析,相信你对 Spring AOP 的基本原理已经有所了解了。不过AOP 毕竟是 Spring 的核心功能之一,不可能规避那零散的两三个问题就一劳永逸了。所以这节课,我们继续聊聊 Spring AOP 中还会有哪些易错点。实际上,当一个系统采用的切面越来越多时,因为执行顺序而导致的问题便会逐步暴露出来,下面我们就重点看一下。
## 案例1错乱混合不同类型的增强
还是沿用上节课的宿舍管理系统开发场景。
这里我们先回顾下,你就不用去翻代码了。这个宿舍管理系统保护了一个电费充值模块,它包含了一个负责电费充值的类 ElectricService还有一个充电方法 charge()
```
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println(&quot;Electric charging ...&quot;);
}
}
```
为了在执行 charge() 之前,鉴定下调用者的权限,我们增加了针对于 Electric 的切面类 AopConfig其中包含一个 @Before 增强。这里的增强没有做任何事情,仅仅是打印了一行日志,然后模拟执行权限校验功能(占用 1 秒钟)。
```
//省略 imports
@Aspect
@Service
@Slf4j
public class AspectService {
@Before(&quot;execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) &quot;)
public void checkAuthority(JoinPoint pjp) throws Throwable {
System.out.println(&quot;validating user authority&quot;);
Thread.sleep(1000);
}
}
```
执行后,我们得到以下 log接着一切按照预期继续执行
```
validating user authority
Electric charging ...
```
一段时间后由于业务发展ElectricService 中的 charge() 逻辑变得更加复杂了,我们需要仅仅针对 ElectricService 的 charge() 做性能统计。为了不影响原有的业务逻辑,我们在 AopConfig 中添加了另一个增强,代码更改后如下:
```
//省略 imports
@Aspect
@Service
public class AopConfig {
@Before(&quot;execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) &quot;)
public void checkAuthority(JoinPoint pjp) throws Throwable {
System.out.println(&quot;validating user authority&quot;);
Thread.sleep(1000);
}
@Around(&quot;execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) &quot;)
public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
pjp.proceed();
long end = System.currentTimeMillis();
System.out.println(&quot;charge method time cost: &quot; + (end - start));
}
}
```
执行后得到日志如下:
>
<p>validating user authority<br>
Electric charging ...<br>
charge method time cost 1022 (ms)</p>
通过性能统计打印出的日志,我们可以得知 charge() 执行时间超过了 1 秒钟。然而,该方法仅打印了一行日志,它的执行不可能需要这么长时间。
因此我们很容易看出问题所在:当前 ElectricService 中 charge() 的执行时间,包含了权限验证的时间,即包含了通过 @Around 增强的 checkAuthority() 执行的所有时间。这并不符合我们的初衷,我们需要统计的仅仅是 ElectricService.charge() 的性能统计,它并不包含鉴权过程。
当然,这些都是从日志直接观察出的现象。实际上,这个问题出现的根本原因和 AOP 的执行顺序有关。针对这个案例而言当同一个切面Aspect中同时包含多个不同类型的增强时Around、Before、After、AfterReturning、AfterThrowing 等),它们的执行是有顺序的。那么顺序如何?我们不妨来解析下。
### 案例解析
其实一切都可以从源码中得到真相!在[第04课](https://time.geekbang.org/column/article/367876)我们曾经提到过Spring 初始化单例类的一般过程,基本都是 getBean()-&gt;doGetBean()-&gt;getSingleton(),如果发现 Bean 不存在,则调用 createBean()-&gt;doCreateBean() 进行实例化。
而如果我们的代码里使用了 Spring AOPdoCreateBean() 最终会返回一个代理对象。至于代理对象如何创建,大体流程我们在上一讲已经概述过了。如果你记忆力比较好的话,应该记得在代理对象的创建过程中,我们贴出过这样一段代码(参考 AbstractAutoProxyCreator#createProxy
```
protected Object createProxy(Class&lt;?&gt; beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
//省略非关键代码
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
//省略非关键代码
return proxyFactory.getProxy(getProxyClassLoader());
}
```
其中 advisors 就是增强方法对象,它的顺序决定了面临多个增强时,到底先执行谁。而这个集合对象本身是由 specificInterceptors 构建出来的,而 specificInterceptors 又是由 AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法构建:
```
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class&lt;?&gt; beanClass, String beanName, @Nullable TargetSource targetSource) {
List&lt;Advisor&gt; advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}
```
简单说,其实就是根据当前的 beanClass、beanName 等信息,结合所有候选的 advisors最终找出匹配Eligible的 Advisor为什么如此毕竟 AOP 拦截点可能会配置多个,而我们执行的方法不见得会被所有的拦截配置拦截。寻找匹配 Advisor 的逻辑参考 AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
```
protected List&lt;Advisor&gt; findEligibleAdvisors(Class&lt;?&gt; beanClass, String beanName) {
//寻找候选的 Advisor
List&lt;Advisor&gt; candidateAdvisors = findCandidateAdvisors();
//根据候选的 Advisor 和当前 bean 算出匹配的 Advisor
List&lt;Advisor&gt; eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
//排序
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}
```
通过研读代码,最终 Advisors 的顺序是由两点决定:
1. candidateAdvisors 的顺序;
1. sortAdvisors 进行的排序。
这里我们可以重点看下对本案例起关键作用的 candidateAdvisors 排序。实际上,它的顺序是在 @Aspect 标记的 AopConfig Bean 构建时就决定了。具体而言,就是在初始化过程中会排序自己配置的 Advisors并把排序结果存入了缓存BeanFactoryAspectJAdvisorsBuilder#advisorsCache)。
后续 Bean 创建代理时,直接拿出这个排序好的候选 Advisors。候选 Advisors 排序发生在 Bean 构建这个结论时,我们也可以通过 AopConfig Bean 构建中的堆栈信息验证:
<img src="https://static001.geekbang.org/resource/image/61/d1/611f386b14b05c2d151340d31f34e3d1.png" alt="">
可以看到,排序是在 Bean 的构建中进行的,而最后排序执行的关键代码位于下面的方法中(参考 ReflectiveAspectJAdvisorFactory#getAdvisorMethods
```
private List&lt;Method&gt; getAdvisorMethods(Class&lt;?&gt; aspectClass) {
final List&lt;Method&gt; methods = new ArrayList&lt;&gt;();
ReflectionUtils.doWithMethods(aspectClass, method -&gt; {
// Exclude pointcuts
if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
methods.add(method);
}
}, ReflectionUtils.USER_DECLARED_METHODS);
// 排序
methods.sort(METHOD_COMPARATOR);
return methods;
}
```
上述代码的重点是第九行 methods.sort(METHOD_COMPARATOR)方法。
我们来查看 METHOD_COMPARATOR 的代码,会发现它是定义在 ReflectiveAspectJAdvisorFactory 类中的静态方法块,代码如下:
```
static {
Comparator&lt;Method&gt; adviceKindComparator = new ConvertingComparator&lt;&gt;(
new InstanceComparator&lt;&gt;(
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
(Converter&lt;Method, Annotation&gt;) method -&gt; {
AspectJAnnotation&lt;?&gt; annotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (annotation != null ? annotation.getAnnotation() : null);
});
Comparator&lt;Method&gt; methodNameComparator = new ConvertingComparator&lt;&gt;(Method::getName);
//合并上面两者比较器
METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
}
```
METHOD_COMPARATOR 本质上是一个连续比较器,由 adviceKindComparator 和 methodNameComparator 这两个比较器通过 thenComparing()连接而成。
通过这个案例,我们重点了解 adviceKindComparator 这个比较器,此对象通过实例化 ConvertingComparator 类而来,而 ConvertingComparator 类是 Spring 中较为经典的一个实现。顾名思义,先转化再比较,它构造参数接受以下这两个参数:
- 第一个参数是基准比较器,即在 adviceKindComparator 中最终要调用的比较器,在构造函数中赋值于 this.comparator
- 第二个参数是一个 lambda 回调函数,用来将传递的参数转化为基准比较器需要的参数类型,在构造函数中赋值于 this.converter。
查看 ConvertingComparator 比较器核心方法 compare 如下:
```
public int compare(S o1, S o2) {
T c1 = this.converter.convert(o1);
T c2 = this.converter.convert(o2);
return this.comparator.compare(c1, c2);
}
```
可知,这里是先调用从构造函数中获取到的 lambda 回调函数 this.converter将需要比较的参数进行转化。我们可以从之前的代码中找出这个转化工作
```
(Converter&lt;Method, Annotation&gt;) method -&gt; {
AspectJAnnotation&lt;?&gt; annotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (annotation != null ? annotation.getAnnotation() : null);
});
```
转化功能的代码逻辑较为简单就是返回传入方法method上标记的增强注解Pointcut,Around,Before,After,AfterReturning 以及 AfterThrowing
```
private static final Class&lt;?&gt;[] ASPECTJ_ANNOTATION_CLASSES = new Class&lt;?&gt;[] {
Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
protected static AspectJAnnotation&lt;?&gt; findAspectJAnnotationOnMethod(Method method) {
for (Class&lt;?&gt; clazz : ASPECTJ_ANNOTATION_CLASSES) {
AspectJAnnotation&lt;?&gt; foundAnnotation = findAnnotation(method, (Class&lt;Annotation&gt;) clazz);
if (foundAnnotation != null) {
return foundAnnotation;
}
}
return null;
}
```
经过转化后,我们获取到的待比较的数据其实就是注解了。而它们的排序依赖于 ConvertingComparator 的第一个参数,即最终会调用的基准比较器,以下是它的关键实现代码:
```
new InstanceComparator&lt;&gt;(
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class)
```
最终我们要调用的基准比较器本质上就是一个 InstanceComparator 类,我们先重点注意下这几个增强注解的传递顺序。继续查看它的构造方法如下:
```
public InstanceComparator(Class&lt;?&gt;... instanceOrder) {
Assert.notNull(instanceOrder, &quot;'instanceOrder' array must not be null&quot;);
this.instanceOrder = instanceOrder;
}
```
构造方法也是较为简单的,只是将传递进来的 instanceOrder 赋予了类成员变量,继续查看 InstanceComparator 比较器核心方法 compare 如下,也就是最终要调用的比较方法:
```
public int compare(T o1, T o2) {
int i1 = getOrder(o1);
int i2 = getOrder(o2);
return (i1 &lt; i2 ? -1 : (i1 == i2 ? 0 : 1));
}
```
一个典型的 Comparator代码逻辑按照 i1、i2 的升序排列,即 getOrder() 返回的值越小,排序越靠前。
查看 getOrder() 的逻辑如下:
```
private int getOrder(@Nullable T object) {
if (object != null) {
for (int i = 0; i &lt; this.instanceOrder.length; i++) {
//instance 在 instanceOrder 中的“排号”
if (this.instanceOrder[i].isInstance(object)) {
return i;
}
}
}
return this.instanceOrder.length;
}
```
返回当前传递的增强注解在 this.instanceOrder 中的序列值,序列值越小,则越靠前。而结合之前构造参数传递的顺序,我们很快就能判断出:最终的排序结果依次是 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class。
到此为止答案也呼之欲出this.instanceOrder 的排序,即为不同类型增强的优先级,**排序越靠前,优先级越高**。
结合之前的讨论我们可以得出一个结论同一个切面中不同类型的增强方法被调用的顺序依次为Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class。
### 问题修正
从上述案例解析中,我们知道 Around 类型的增强被调用的优先级高于 Before 类型的增强,所以上述案例中性能统计所花费的时间,包含权限验证的时间,也在情理之中。
知道了原理,修正起来也就简单了。假设不允许我们去拆分类,我们可以按照下面的思路来修改:
1. 将 ElectricService.charge() 的业务逻辑全部移动到 doCharge(),在 charge() 中调用 doCharge()
1. 性能统计只需要拦截 doCharge()
1. 权限统计增强保持不变,依然拦截 charge()。
ElectricService 类代码更改如下:
```
@Service
public class ElectricService {
public void charge() {
doCharge();
}
public void doCharge() {
System.out.println(&quot;Electric charging ...&quot;);
}
}
```
切面代码更改如下:
```
//省略 imports
@Aspect
@Service
public class AopConfig {
@Before(&quot;execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) &quot;)
public void checkAuthority(JoinPoint pjp) throws Throwable {
System.out.println(&quot;validating user authority&quot;);
Thread.sleep(1000);
}
@Around(&quot;execution(* com.spring.puzzle.class6.example1.ElectricService.doCharge()) &quot;)
public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
pjp.proceed();
long end = System.currentTimeMillis();
System.out.println(&quot;charge method time cost: &quot; + (end - start));
}
}
```
## 案例 2错乱混合同类型增强
那学到这里,你可能还有疑问,如果同一个切面里的多个增强方法其增强都一样,那调用顺序又如何呢?我们继续看下一个案例。
这里业务逻辑类 ElectricService 没有任何变化,仅包含一个 charge()
```
import org.springframework.stereotype.Service;
@Service
public class ElectricService {
public void charge() {
System.out.println(&quot;Electric charging ...&quot;);
}
}
```
切面类 AspectService 包含两个方法,都是 Before 类型增强。
第一个方法 logBeforeMethod(),目的是在 run() 执行之前希望能输入日志,表示当前方法被调用一次,方便后期统计。另一个方法 validateAuthority(),目的是做权限验证,其作用是在调用此方法之前做权限验证,如果不符合权限限制要求,则直接抛出异常。这里为了方便演示,此方法将直接抛出异常:
```
//省略 imports
@Aspect
@Service
public class AopConfig {
@Before(&quot;execution(* com.spring.puzzle.class5.example2.ElectricService.charge())&quot;)
public void logBeforeMethod(JoinPoint pjp) throws Throwable {
System.out.println(&quot;step into -&gt;&quot;+pjp.getSignature());
}
@Before(&quot;execution(* com.spring.puzzle.class5.example2.ElectricService.charge()) &quot;)
public void validateAuthority(JoinPoint pjp) throws Throwable {
throw new RuntimeException(&quot;authority check failed&quot;);
}
}
```
我们对代码的执行预期为:当鉴权失败时,由于 ElectricService.charge() 没有被调用,那么 run() 的调用日志也不应该被输出,即 logBeforeMethod() 不应该被调用,但事实总是出乎意料,执行结果如下:
>
<p>step into -&gt;void com.spring.puzzle.class6.example2.Electric.charge()<br>
Exception in thread "main" java.lang.RuntimeException: authority check failed</p>
虽然鉴权失败,抛出了异常且 ElectricService.charge() 没有被调用,但是 logBeforeMethod() 的调用日志却被输出了,这将导致后期针对于 ElectricService.charge() 的调用数据统计严重失真。
这里我们就需要搞清楚一个问题:当同一个切面包含多个同一种类型的多个增强,且修饰的都是同一个方法时,这多个增强的执行顺序是怎样的?
### 案例解析
我们继续从源代码中寻找真相!你应该还记得上述代码中,定义 METHOD_COMPARATOR 的静态代码块吧。
METHOD_COMPARATOR 本质是一个连续比较器,而上个案例中我们仅仅只看了第一个比较器,细心的你肯定发现了这里还有第二个比较器 methodNameComparator任意两个比较器都可以通过其内置的 thenComparing() 连接形成一个连续比较器,从而可以让我们按照比较器的连接顺序依次比较:
```
static {
//第一个比较器,用来按照增强类型排序
Comparator&lt;Method&gt; adviceKindComparator = new ConvertingComparator&lt;&gt;(
new InstanceComparator&lt;&gt;(
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
(Converter&lt;Method, Annotation&gt;) method -&gt; {
AspectJAnnotation&lt;?&gt; annotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (annotation != null ? annotation.getAnnotation() : null);
})
//第二个比较器,用来按照方法名排序
Comparator&lt;Method&gt; methodNameComparator = new ConvertingComparator&lt;&gt;(Method::getName);
METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
}
```
我们可以看到,在第 12 行代码中,第 2 个比较器 methodNameComparator 依然使用的是 ConvertingComparator传递了方法名作为参数。我们基本可以猜测出该比较器是按照方法名进行排序的这里可以进一步查看构造器方法及构造器调用的内部 comparable()
```
public ConvertingComparator(Converter&lt;S, T&gt; converter) {
this(Comparators.comparable(), converter);
}
// 省略非关键代码
public static &lt;T&gt; Comparator&lt;T&gt; comparable() {
return ComparableComparator.INSTANCE;
}
```
上述代码中的 ComparableComparator 实例其实极其简单,代码如下:
```
public class ComparableComparator&lt;T extends Comparable&lt;T&gt;&gt; implements Comparator&lt;T&gt; {
public static final ComparableComparator INSTANCE = new ComparableComparator();
@Override
public int compare(T o1, T o2) {
return o1.compareTo(o2);
}
}
```
答案和我们的猜测完全一致methodNameComparator 最终调用了 String 类自身的 compareTo(),代码如下:
```
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k &lt; lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
```
到这,答案揭晓:如果两个方法名长度相同,则依次比较每一个字母的 ASCII 码ASCII 码越小,排序越靠前;若长度不同,且短的方法名字符串是长的子集时,短的排序靠前。
### 问题修正
从上述分析我们得知,在同一个切面配置类中,针对同一个方法存在多个同类型增强时,其执行顺序仅和当前增强方法的名称有关,而不是由谁代码在先、谁代码在后来决定。了解了这点,我们就可以直接通过调整方法名的方式来修正程序:
```
//省略 imports
@Aspect
@Service
public class AopConfig {
@Before(&quot;execution(* com.spring.puzzle.class6.example2.ElectricService.charge())&quot;)
public void logBeforeMethod(JoinPoint pjp) throws Throwable {
System.out.println(&quot;step into -&gt;&quot;+pjp.getSignature());
}
@Before(&quot;execution(* com.spring.puzzle.class6.example2.ElectricService.charge()) &quot;)
public void checkAuthority(JoinPoint pjp) throws Throwable {
throw new RuntimeException(&quot;authority check failed&quot;);
}
}
```
我们可以将原来的 validateAuthority() 改为 checkAuthority(),这种情况下,**对增强Advisor的排序其实最后就是在比较字符 l 和 字符 c**。显然易见checkAuthority()的排序会靠前,从而被优先执行,最终问题得以解决。
## 重点回顾
通过学习这两个案例,相信你对 Spring AOP 增强方法的执行顺序已经有了较为深入的理解。这里我来总结下关键点:
- 在同一个切面配置中,如果存在多个不同类型的增强,那么其执行优先级是按照增强类型的特定顺序排列,依次的增强类型为 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class
- 在同一个切面配置中,如果存在多个相同类型的增强,那么其执行优先级是按照该增强的方法名排序,排序方式依次为比较方法名的每一个字母,直到发现第一个不相同且 ASCII 码较小的字母。
同时,这节课我们也拓展了一些比较器相关的知识:
- 任意两个比较器Comparator可以通过 thenComparing() 连接合成一个新的连续比较器;
- 比较器的比较规则有一个简单的方法可以帮助你理解,就是最终一定需要对象两两比较,而比较的过程一定是比较这两个对象的同种属性。你只要抓住这两点:比较了什么属性以及比较的结果是什么就可以了,若比较结果为正数,则按照该属性的升序排列;若为负数,则按属性降序排列。
## 思考题
实际上,审阅上面两个案例的修正方案,你会发现它们虽然改动很小,但是都还不够优美。那么有没有稍微优美点的替代方案呢?如果有,你知道背后的原理及关键源码吗?顺便你也可以想想,我为什么没有用更优美的方案呢?
期待在留言区看到你的思考,我们下节课再见!

View File

@@ -0,0 +1,556 @@
<audio id="audio" title="07Spring事件常见错误" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/29/83/298ec04b83a2e5d9d0306a7a0c224483.mp3"></audio>
你好我是傅健这节课我们聊聊Spring事件上的常见错误。
前面的几讲中我们介绍了Spring依赖注入、AOP等核心功能点上的常见错误。而作为Spring 的关键功能支撑Spring事件是一个相对独立的点。或许你从没有在自己的项目中使用过Spring事件但是你一定见过它的相关日志。而且在未来的编程实践中你会发现一旦你用上了Spring事件往往完成的都是一些有趣的、强大的功能例如动态配置。那么接下来我就来讲讲Spring事件上都有哪些常见的错误。
## 案例1试图处理并不会抛出的事件
Spring事件的设计比较简单。说白了就是监听器设计模式在Spring中的一种实现参考下图
<img src="https://static001.geekbang.org/resource/image/34/c6/349f79e396276ab3744c04b0a29eccc6.jpg" alt="">
从图中我们可以看出Spring事件包含以下三大组件。
1. 事件Event用来区分和定义不同的事件在Spring中常见的如ApplicationEvent和AutoConfigurationImportEvent它们都继承于java.util.EventObject。
1. 事件广播器Multicaster负责发布上述定义的事件。例如负责发布ApplicationEvent 的ApplicationEventMulticaster就是Spring中一种常见的广播器。
1. 事件监听器Listener负责监听和处理广播器发出的事件例如ApplicationListener就是用来处理ApplicationEventMulticaster发布的ApplicationEvent它继承于JDK的 EventListener我们可以看下它的定义来验证这个结论
>
<p>public interface ApplicationListener&lt;E extends ApplicationEvent&gt; extends EventListener {<br>
void onApplicationEvent(E event);<br>
}</p>
当然虽然在上述组件中任何一个都是缺一不可的但是功能模块命名不见得完全贴合上述提及的关键字例如发布AutoConfigurationImportEvent的广播器就不含有Multicaster字样。它的发布是由AutoConfigurationImportSelector来完成的。
对这些基本概念和实现有了一定的了解后我们就可以开始解析那些常见的错误。闲话少说我们先来看下面这段基于Spring Boot技术栈的代码
```
@Slf4j
@Component
public class MyContextStartedEventListener implements ApplicationListener&lt;ContextStartedEvent&gt; {
public void onApplicationEvent(final ContextStartedEvent event) {
log.info(&quot;{} received: {}&quot;, this.toString(), event);
}
}
```
很明显这段代码定义了一个监听器MyContextStartedEventListener试图拦截ContextStartedEvent。因为在很多Spring初级开发者眼中Spring运转的核心就是一个Context的维护那么启动Spring自然会启动Context于是他们是很期待出现类似下面的日志的
>
2021-03-07 07:08:21.197 INFO 2624 --- [nio-8080-exec-1] c.s.p.l.e.MyContextStartedEventListener : com.spring.puzzle.class7.example1.MyContextStartedEventListener@d33d5a **received**: org.springframework.context.event.**ContextStartedEvent**[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@19b56c0, started on Sun Mar 07 07:07:57 CST 2021]
但是当我们启动Spring Boot后会发现并不会拦截到这个事件如何理解这个错误呢
### 案例解析
在Spring事件运用上这是一个常见的错误就是不假思索地认为一个框架只要定义了一个事件那么一定会抛出来。例如在本案例中ContextStartedEvent就是Spring内置定义的事件而Spring Boot本身会创建和运维Context表面看起来这个事件的抛出是必然的但是这个事件一定会在Spring Boot启动时抛出来么
答案明显是否定的我们首先看下要抛出这个事件需要调用的方法是什么在Spring Boot中这个事件的抛出只发生在一处即位于方法AbstractApplicationContext#start中
```
@Override
public void start() {
getLifecycleProcessor().start();
publishEvent(new ContextStartedEvent(this));
}
```
也就是说只有上述方法被调用才会抛出ContextStartedEvent但是这个方法在Spring Boot启动时会被调用么我们可以查看Spring启动方法中围绕Context的关键方法调用代码如下
```
public ConfigurableApplicationContext run(String... args) {
//省略非关键代码
context = createApplicationContext();
//省略非关键代码
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
//省略非关键代码
return context;
}
```
我们发现围绕Context、Spring Boot的启动只做了两个关键工作创建Context和Refresh Context。其中Refresh的关键代码如下
```
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}
```
很明显Spring启动最终调用的是AbstractApplicationContext#refresh,并不是 AbstractApplicationContext#start。在这样的残酷现实下ContextStartedEvent自然不会被抛出不抛出自然也不可能被捕获。所以这样的错误也就自然发生了。
### 问题修正
针对这个案例,有了源码的剖析,我们可以很快找到问题发生的原因,但是修正这个问题还要去追溯我们到底想要的是什么?我们可以分两种情况来考虑。
**1. 假设我们是误读了ContextStartedEvent。**
针对这种情况往往是因为我们确实想在Spring Boot启动时拦截一个启动事件但是我们粗略扫视相关事件后误以为ContextStartedEvent就是我们想要的。针对这种情况我们只需要把监听事件的类型修改成真正发生的事件即可例如在本案例中我们可以修正如下
```
@Component
public class MyContextRefreshedEventListener implements ApplicationListener&lt;ContextRefreshedEvent&gt; {
public void onApplicationEvent(final ContextRefreshedEvent event) {
log.info(&quot;{} received: {}&quot;, this.toString(), event);
}
}
```
我们监听ContextRefreshedEvent而非ContextStartedEvent。ContextRefreshedEvent的抛出可以参考方法AbstractApplicationContext#finishRefresh它本身正好是Refresh操作中的一步。
```
protected void finishRefresh() {
//省略非关键代码
initLifecycleProcessor();
// Propagate refresh to lifecycle processor first.
getLifecycleProcessor().onRefresh();
// Publish the final event.
publishEvent(new ContextRefreshedEvent(this));
//省略非关键代码
}
```
**2. 假设我们就是想要处理ContextStartedEvent。**
这种情况下我们真的需要去调用AbstractApplicationContext#start方法。例如,我们可以使用下面的代码来让这个事件抛出:
```
@RestController
public class HelloWorldController {
@Autowired
private AbstractApplicationContext applicationContext;
@RequestMapping(path = &quot;publishEvent&quot;, method = RequestMethod.GET)
public String notifyEvent(){
applicationContext.start();
return &quot;ok&quot;;
};
}
```
我们随便找一处来Autowired一个AbstractApplicationContext然后直接调用其start()就能让事件抛出来。
很明显这种抛出并不难但是作为题外话我们可以思考下为什么要去调用start()呢start()本身在Spring Boot中有何作用
如果我们去翻阅这个方法我们会发现start()是org.springframework.context.Lifecycle定义的方法而它在Spring Boot的默认实现中是去执行所有Lifecycle Bean的启动方法这点可以参考DefaultLifecycleProcessor#startBeans方法来验证
```
private void startBeans(boolean autoStartupOnly) {
Map&lt;String, Lifecycle&gt; lifecycleBeans = getLifecycleBeans();
Map&lt;Integer, LifecycleGroup&gt; phases = new HashMap&lt;&gt;();
lifecycleBeans.forEach((beanName, bean) -&gt; {
if (!autoStartupOnly || (bean instanceof SmartLifecycle &amp;&amp; ((SmartLifecycle) bean).isAutoStartup())) {
int phase = getPhase(bean);
LifecycleGroup group = phases.get(phase);
if (group == null) {
group = new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly);
phases.put(phase, group);
}
group.add(beanName, bean);
}
});
if (!phases.isEmpty()) {
List&lt;Integer&gt; keys = new ArrayList&lt;&gt;(phases.keySet());
Collections.sort(keys);
for (Integer key : keys) {
phases.get(key).start();
}
}
}
```
说起来比较抽象我们可以去写一个Lifecycle Bean代码如下
```
@Component
@Slf4j
public class MyLifeCycle implements Lifecycle {
private volatile boolean running = false;
@Override
public void start() {
log.info(&quot;lifecycle start&quot;);
running = true;
}
@Override
public void stop() {
log.info(&quot;lifecycle stop&quot;);
running = false;
}
@Override
public boolean isRunning() {
return running;
}
}
```
当我们再次运行Spring Boot时只要执行了AbstractApplicationContext的start()就会输出上述代码定义的行为输出LifeCycle start日志。
通过这个Lifecycle Bean的使用AbstractApplicationContext的start要做的事我们就清楚多了。它和Refresh()不同Refresh()是初始化和加载所有需要管理的Bean而start只有在有Lifecycle Bean时才有被调用的价值。那么我们自定义Lifecycle Bean一般是用来做什么呢例如可以用它来实现运行中的启停。这里不再拓展你可以自己做更深入的探索。
通过这个案例,我们搞定了第一类错误。而从这个错误中,我们也得出了一个启示:**当一个事件拦截不了时,我们第一个要查的是拦截的事件类型对不对,执行的代码能不能抛出它。**把握好这点,也就事半功倍了。
## 案例2监听事件的体系不对
通过案例1的学习我们可以保证事件的抛出但是抛出的事件就一定能被我们监听到么我们再来看这样一个案例首先上代码
```
@Slf4j
@Component
public class MyApplicationEnvironmentPreparedEventListener implements ApplicationListener&lt;ApplicationEnvironmentPreparedEvent &gt; {
public void onApplicationEvent(final ApplicationEnvironmentPreparedEvent event) {
log.info(&quot;{} received: {}&quot;, this.toString(), event);
}
}
```
这里我们试图处理ApplicationEnvironmentPreparedEvent。期待出现拦截事件的日志如下
>
2021-03-07 09:12:08.886 INFO 27064 --- [ restartedMain] licationEnvironmentPreparedEventListener : com.spring.puzzle.class7.example2.MyApplicationEnvironmentPreparedEventListener@2b093d received: org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent[source=org.springframework.boot.SpringApplication@122b9e6]
有了案例1的经验首先我们就可以查看下这个事件的抛出会不会存在问题。这个事件在Spring中是由EventPublishingRunListener#environmentPrepared方法抛出,代码如下:
```
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster
.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}
```
现在我们调试下代码你会发现这个方法在Spring启动时一定经由SpringApplication#prepareEnvironment方法调用,调试截图如下:
<img src="https://static001.geekbang.org/resource/image/f6/fe/f6e5b92bd2db8a3db93f53ff2a7944fe.png" alt="">
表面上看既然代码会被调用事件就会抛出那么我们在最开始定义的监听器就能处理但是我们真正去运行程序时会发现效果和案例1是一样的都是监听器的处理并不执行即拦截不了。这又是为何
### 案例解析
实际上这是在Spring事件处理上非常容易犯的一个错误即监听的体系不一致。通俗点说就是“驴头不对马嘴”。我们首先来看下关于ApplicationEnvironmentPreparedEvent的处理它相关的两大组件是什么
1. 广播器这个事件的广播器是EventPublishingRunListener的initialMulticaster代码参考如下
```
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
//省略非关键代码
private final SimpleApplicationEventMulticaster initialMulticaster;
public EventPublishingRunListener(SpringApplication application, String[] args) {
//省略非关键代码
this.initialMulticaster = new SimpleApplicationEventMulticaster();
for (ApplicationListener&lt;?&gt; listener : application.getListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
}
}
```
1. 监听器这个事件的监听器同样位于EventPublishingRunListener中获取方式参考关键代码行
>
this.initialMulticaster.addApplicationListener(listener);
如果继续查看代码我们会发现这个事件的监听器就存储在SpringApplication#Listeners中,调试下就可以找出所有的监听器,截图如下:
<img src="https://static001.geekbang.org/resource/image/14/6b/145f6d0a20a6f82fa8f6f08c7a08666b.png" alt="">
从中我们可以发现并不存在我们定义的MyApplicationEnvironmentPreparedEventListener这是为何
还是查看代码当Spring Boot被构建时会使用下面的方法去寻找上述监听器
>
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
而上述代码最终寻找Listeners的候选者参考代码 SpringFactoriesLoader#loadSpringFactories中的关键行
>
<p>//下面的FACTORIES_RESOURCE_LOCATION定义为 "META-INF/spring.factories"<br>
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :</p>
我们可以寻找下这样的文件spring.factories确实可以发现类似的定义
```
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
//省略其他监听器
```
说到这里相信你已经意识到本案例的问题所在。我们定义的监听器并没有被放置在META-INF/spring.factories中实际上我们的监听器监听的体系是另外一套其关键组件如下
1. 广播器即AbstractApplicationContext#applicationEventMulticaster
1. 监听器由上述提及的META-INF/spring.factories中加载的监听器以及扫描到的 ApplicationListener类型的Bean共同组成。
这样比较后,我们可以得出一个结论:**我们定义的监听器并不能监听到initialMulticaster广播出的ApplicationEnvironmentPreparedEvent。**
### 问题修正
现在就到了解决问题的时候了我们可以把自定义监听器注册到initialMulticaster广播体系中这里提供两种方法修正问题。
1. 在构建Spring Boot时添加MyApplicationEnvironmentPreparedEventListener
```
@SpringBootApplication
public class Application {
public static void main(String[] args) {
MyApplicationEnvironmentPreparedEventListener myApplicationEnvironmentPreparedEventListener = new MyApplicationEnvironmentPreparedEventListener();
SpringApplication springApplication = new SpringApplicationBuilder(Application.class).listeners(myApplicationEnvironmentPreparedEventListener).build();
springApplication.run(args);
}
}
```
1. 使用META-INF/spring.factories即在/src/main/resources下面新建目录META-INF然后新建一个对应的spring.factories文件
```
org.springframework.context.ApplicationListener=\
com.spring.puzzle.listener.example2.MyApplicationEnvironmentPreparedEventListener
```
通过上述两种修改方式即可完成事件的监听很明显第二种方式要优于第一种至少完全用原生的方式去解决而不是手工实例化一个MyApplicationEnvironmentPreparedEventListener。这点还是挺重要的。
反思这个案例的错误,结论就是**对于事件一定要注意“驴头”(监听器)对上“马嘴”(广播)**。
## 案例3部分事件监听器失效
通过前面案例的解析,我们可以确保事件在合适的时机被合适的监听器所捕获。但是理想总是与现实有差距,有些时候,我们可能还会发现部分事件监听器一直失效或偶尔失效。这里我们可以写一段代码来模拟偶尔失效的场景,首先我们完成一个自定义事件和两个监听器,代码如下:
```
public class MyEvent extends ApplicationEvent {
public MyEvent(Object source) {
super(source);
}
}
@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener&lt;MyEvent&gt; {
Random random = new Random();
@Override
public void onApplicationEvent(MyEvent event) {
log.info(&quot;{} received: {}&quot;, this.toString(), event);
//模拟部分失效
if(random.nextInt(10) % 2 == 1)
throw new RuntimeException(&quot;exception happen on first listener&quot;);
}
}
@Component
@Order(2)
public class MySecondEventListener implements ApplicationListener&lt;MyEvent&gt; {
@Override
public void onApplicationEvent(MyEvent event) {
log.info(&quot;{} received: {}&quot;, this.toString(), event);
}
}
```
这里监听器MyFirstEventListener的优先级稍高且执行过程中会有50%的概率抛出异常。然后我们再写一个Controller来触发事件的发送
```
@RestController
@Slf4j
public class HelloWorldController {
@Autowired
private AbstractApplicationContext applicationContext;
@RequestMapping(path = &quot;publishEvent&quot;, method = RequestMethod.GET)
public String notifyEvent(){
log.info(&quot;start to publish event&quot;);
applicationContext.publishEvent(new MyEvent(UUID.randomUUID()));
return &quot;ok&quot;;
};
}
```
完成这些代码后,我们就可以使用[http://localhost:8080/publishEvent](http://localhost:8080/publishEvent) 来测试监听器的接收和执行了。观察测试结果我们会发现监听器MySecondEventListener有一半的概率并没有接收到任何事件。可以说我们使用了最简化的代码模拟出了部分事件监听器偶尔失效的情况。当然在实际项目中抛出异常这个根本原因肯定不会如此明显但还是可以借机举一反三的。那么如何理解这个问题呢
### 案例解析
这个案例非常简易如果你稍微有些开发经验的话大概也能推断出原因处理器的执行是顺序执行的在执行过程中如果一个监听器执行抛出了异常则后续监听器就得不到被执行的机会了。这里我们可以通过Spring源码看下事件是如何被执行的
具体而言,当广播一个事件,执行的方法参考 SimpleApplicationEventMulticaster#multicastEvent(ApplicationEvent)
```
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener&lt;?&gt; listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -&gt; invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
```
上述方法通过Event类型等信息调用getApplicationListeners获取了具有执行资格的所有监听器在本案例中即为MyFirstEventListener和MySecondEventListener然后按顺序去执行。最终每个监听器的执行是通过invokeListener()来触发的,调用的是接口方法 ApplicationListener#onApplicationEvent。执行逻辑可参考如下代码:
```
protected void invokeListener(ApplicationListener&lt;?&gt; listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
}
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
//省略非关键代码
}
else {
throw ex;
}
}
}
```
这里我们并没有去设置什么org.springframework.util.ErrorHandler也没有绑定什么Executor 来执行任务,所以针对本案例的情况,我们可以看出:**最终事件的执行是由同一个线程按顺序来完成的,任何一个报错,都会导致后续的监听器执行不了。**
### 问题修正
怎么解决呢?好办,我提供两种方案给你。
**1. 确保监听器的执行不会抛出异常。**
既然我们使用多个监听器,我们肯定是希望它们都能执行的,所以我们一定要保证每个监听器的执行不会被其他监听器影响。基于这个思路,我们修改案例代码如下:
```
@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener&lt;MyEvent&gt; {
@Override
public void onApplicationEvent(MyEvent event) {
try {
// 省略事件处理相关代码
}catch(Throwable throwable){
//write error/metric to alert
}
}
}
```
**2. 使用org.springframework.util.ErrorHandler。**
通过上面的案例解析我们发现假设我们设置了一个ErrorHandler那么就可以用这个ErrorHandler去处理掉异常从而保证后续事件监听器处理不受影响。我们可以使用下面的代码来修正问题
```
SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = applicationContext.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, SimpleApplicationEventMulticaster.class);
simpleApplicationEventMulticaster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);
```
其中LOG_AND_SUPPRESS_ERROR_HANDLER的实现如下
```
public static final ErrorHandler LOG_AND_SUPPRESS_ERROR_HANDLER = new LoggingErrorHandler();
private static class LoggingErrorHandler implements ErrorHandler {
private final Log logger = LogFactory.getLog(LoggingErrorHandler.class);
@Override
public void handleError(Throwable t) {
logger.error(&quot;Unexpected error occurred in scheduled task&quot;, t);
}
}
```
对比下方案1使用ErrorHandler有一个很大的优势就是我们不需要在某个监听器中都重复类似下面的代码了
```
try {
//省略事件处理过程
}catch(Throwable throwable){
//write error/metric to alert
}
```
这么看的话其实Spring的设计还是很全面的它考虑了各种各样的情况。但是Spring使用者往往都不会去了解其内部实现这样就会遇到各种各样的问题。相反如果你对其实现有所了解的话也对常见错误有一个感知则大概率是可以快速避坑的项目也可以运行得更加平稳顺畅。
## 重点回顾
今天我们粗略地了解了Spring事件处理的基本流程。其实抛开Spring框架我们去设计一个通用的事件处理框架常常也会犯这三种错误
1. 误读事件本身含义;
1. 监听错了事件的传播系统;
1. 事件处理之间互相影响,导致部分事件处理无法完成。
这三种错误正好对应了我们这节课讲解的三个案例。
此外在Spring事件处理过程中我们也学习到了监听器加载的特殊方式即使用SPI的方式直接从配置文件META-INF/spring.factories中加载。这种方式或者说思想非常值得你去学习因为它在许多Java应用框架中都有所使用例如Dubbo就是使用增强版的SPI来配置编解码器的。
## 思考题
在案例3中我们提到默认的事件执行是在同一个线程中执行的即事件发布者使用的线程。参考如下日志佐证这个结论
>
<p>2021-03-09 09:10:33.052 INFO 18104 --- [nio-8080-exec-1] c.s.p.listener.HelloWorldController : start to publish event<br>
2021-03-09 09:10:33.055 INFO 18104 --- [nio-8080-exec-1] c.s.p.l.example3.MyFirstEventListener : com.spring.puzzle.class7.example3.MyFirstEventListener@18faf0 received: com.spring.puzzle.class7.example3.MyEvent[source=df42b08f-8ee2-44df-a957-d8464ff50c88]</p>
通过日志可以看出事件的发布和执行使用的都是nio-8080-exec-1线程但是在事件比较多时我们往往希望事件执行得更快些或者希望事件的执行可以异步化不影响主线程。此时应该怎么做呢
期待在留言区看到你的回复,我们下节课见!

View File

@@ -0,0 +1,455 @@
<audio id="audio" title="08答疑现场Spring Core 篇思考题合集" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f2/15/f285d5312613a58052cf4993bd7a7615.mp3"></audio>
你好,我是傅健。
如果你看到这篇文章,那么我真的非常开心,这说明第一章节的内容你都跟下来了,并且对于课后的思考题也有研究,在这我要手动给你点个赞。繁忙的工作中,还能为自己持续充电,保持终身学习的心态,我想我们一定是同路人。
那么到今天为止,我们已经学习了 17 个案例,解决的问题也不算少了,不知道你的感受如何?收获如何呢?
我还记得[开篇词](https://time.geekbang.org/column/article/364661)的留言区中有位很有趣的同学,他说:“作为一线 bug 制造者,希望能少写点 bug。” 感同身受,和 Spring 斗智斗勇的这些年,我也经常为一些问题而抓狂过,因不能及时解决而焦虑过,但最终还是觉得蛮有趣的,这个专栏也算是沉淀之作,希望能给你带来一些实际的帮助。
最初,我其实是想每节课都和你交流下上节课的思考题,但又担心大家的学习进度不一样,所以就有了这次的集中答疑,我把我的答案给到大家,你也可以对照着去看一看,也许有更好的方法,欢迎你来贡献“选项”,我们一起交流。希望大家都能在问题的解决中获得一些正向反馈,完成学习闭环。
## **[第1课](https://time.geekbang.org/column/article/364761)**
在案例 2 中,显示定义构造器,这会发生根据构造器参数寻找对应 Bean 的行为。这里请你思考一个问题,假设寻找不到对应的 Bean一定会如案例 2 那样直接报错么?
实际上,答案是否定的。这里我们不妨修改下案例 2 的代码,修改后如下:
```
@Service
public class ServiceImpl {
private List&lt;String&gt; serviceNames;
public ServiceImpl(List&lt;String&gt; serviceNames){
this.serviceNames = serviceNames;
System.out.println(this.serviceNames);
}
}
```
参考上述代码我们的构造器参数由普通的String改成了一个List<string>,最终运行程序会发现这并不会报错,而是输出 []。</string>
要了解这个现象,我们可以直接定位构建构造器调用参数的代码所在地(即 ConstructorResolver#resolveAutowiredArgument
```
@Nullable
protected Object resolveAutowiredArgument(MethodParameter param, String beanName,
@Nullable Set&lt;String&gt; autowiredBeanNames, TypeConverter typeConverter, boolean fallback) {
//省略非关键代码
try {
//根据构造器参数寻找 bean
return this.beanFactory.resolveDependency(
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
}
catch (NoUniqueBeanDefinitionException ex) {
throw ex;
}
catch (NoSuchBeanDefinitionException ex) {
//找不到 “bean” 进行fallback
if (fallback) {
// Single constructor or factory method -&gt; let's return an empty array/collection
// for e.g. a vararg or a non-null List/Set/Map parameter.
if (paramType.isArray()) {
return Array.newInstance(paramType.getComponentType(), 0);
}
else if (CollectionFactory.isApproximableCollectionType(paramType)) {
return CollectionFactory.createCollection(paramType, 0);
}
else if (CollectionFactory.isApproximableMapType(paramType)) {
return CollectionFactory.createMap(paramType, 0);
}
}
throw ex;
}
}
```
当构建集合类型的参数实例寻找不到合适的 Bean 时并不是不管不顾地直接报错而是会尝试进行fallback。对于本案例而言会使用下面的语句来创建一个空的集合作为构造器参数传递进去
>
CollectionFactory.createCollection(paramType, 0);
上述代码最终调用代码如下:
>
return new ArrayList&lt;&gt;(capacity);
所以很明显,最终修改后的案例并不会报错,而是把 serviceNames 设置为一个空的 List。从这一点也可知**自动装配远比想象的要复杂**。
## **[第2课](https://time.geekbang.org/column/article/366170)**
我们知道了通过@Qualifier可以引用想匹配的Bean也可以直接命名属性的名称为Bean的名称来引用这两种方式如下
```
//方式1属性命名为要装配的bean名称
@Autowired
DataService oracleDataService;
//方式2使用@Qualifier直接引用
@Autowired
@Qualifier(&quot;oracleDataService&quot;)
DataService dataService;
```
那么对于案例3的内部类引用你觉得可以使用第1种方式做到么例如使用如下代码
>
<p>@Autowired<br>
DataService studentController.InnerClassDataService;</p>
实际上,如果你动动手或者我们稍微敏锐点就会发现,代码本身就不能编译,因为中间含有“.”。那么还有办法能通过这种方式引用到内部类么?
查看决策谁优先的源码最终使用属性名来匹配的执行情况可参考DefaultListableBeanFactory#matchesBeanName方法的调试视图
<img src="https://static001.geekbang.org/resource/image/86/37/8658173a310332b1ca532997c4cd5337.png" alt="">
我们可以看到实现的关键其实是下面这行语句:
>
candidateName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), candidateName))
很明显我们的Bean没有被赋予别名而鉴于属性名不可能含有“.”,所以它不可能匹配上带“.”的Bean名即studentController.InnerClassDataService
综上如果一个内部类没有显式指定名称或者别名试图使用属性名和Bean名称一致来引用到对应的Bean是行不通的。
## **[第3课](https://time.geekbang.org/column/article/366930)**
在案例2中我们初次运行程序获取的结果如下
>
[Student(id=1, name=xie), Student(id=2, name=fang)]
那么如何做到让学生2优先输出呢
实际上在案例2中我们收集的目标类型是List而List是可排序的那么到底是如何排序的在案例2的解析中我们给出了DefaultListableBeanFactory#resolveMultipleBeans方法的代码,不过省略了一些非关键的代码,这其中就包括了排序工作,代码如下:
```
if (result instanceof List) {
Comparator&lt;Object&gt; comparator = adaptDependencyComparator(matchingBeans);
if (comparator != null) {
((List&lt;?&gt;) result).sort(comparator);
}
}
```
而针对本案例最终排序执行的是OrderComparator#doCompare方法,关键代码如下:
```
private int doCompare(@Nullable Object o1, @Nullable Object o2, @Nullable OrderSourceProvider sourceProvider) {
boolean p1 = (o1 instanceof PriorityOrdered);
boolean p2 = (o2 instanceof PriorityOrdered);
if (p1 &amp;&amp; !p2) {
return -1;
}
else if (p2 &amp;&amp; !p1) {
return 1;
}
int i1 = getOrder(o1, sourceProvider);
int i2 = getOrder(o2, sourceProvider);
return Integer.compare(i1, i2);
}
```
其中getOrder的执行获取到的order值相当于优先级是通过AnnotationAwareOrderComparator#findOrder来获取的
```
protected Integer findOrder(Object obj) {
Integer order = super.findOrder(obj);
if (order != null) {
return order;
}
return findOrderFromAnnotation(obj);
}
```
不难看出获取order值包含了2种方式
1.@Order获取值参考AnnotationAwareOrderComparator#findOrderFromAnnotation
```
@Nullable
private Integer findOrderFromAnnotation(Object obj) {
AnnotatedElement element = (obj instanceof AnnotatedElement ? (AnnotatedElement) obj : obj.getClass());
MergedAnnotations annotations = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY);
Integer order = OrderUtils.getOrderFromAnnotations(element, annotations);
if (order == null &amp;&amp; obj instanceof DecoratingProxy) {
return findOrderFromAnnotation(((DecoratingProxy) obj).getDecoratedClass());
}
return order;
}
```
1. 从Ordered 接口实现方法获取值参考OrderComparator#findOrder
```
protected Integer findOrder(Object obj) {
return (obj instanceof Ordered ? ((Ordered) obj).getOrder() : null);
}
```
通过上面的分析如果我们不能改变类继承关系例如让Student实现Ordered接口则可以通过使用@Order来调整顺序,具体修改代码如下:
```
@Bean
@Order(2)
public Student student1(){
return createStudent(1, &quot;xie&quot;);
}
@Bean
@Order(1)
public Student student2(){
return createStudent(2, &quot;fang&quot;);
}
```
现在我们就可以把原先的Bean输出顺序颠倒过来了示例如下
>
Student(id=2, name=fang)],[Student(id=1, name=xie)
## **[第4课](https://time.geekbang.org/column/article/367876)**
案例 2 中的类 LightService当我们不在 Configuration 注解类中使用 Bean 方法将其注入 Spring 容器,而是坚持使用 @Service 将其自动注入到容器,同时实现 Closeable 接口,代码如下:
```
import org.springframework.stereotype.Component;
import java.io.Closeable;
@Service
public class LightService implements Closeable {
public void close() {
System.out.println(&quot;turn off all lights);
}
//省略非关键代码
}
```
接口方法 close() 也会在 Spring 容器被销毁的时候自动执行么?
答案是肯定的,通过案例 2 的分析,你可以知道,当 LightService 是一个实现了 Closable 接口的单例 Bean 时,会有一个 DisposableBeanAdapter 被添加进去。
而具体到执行哪一种方法shutdown()close()? 在代码中你能够找到答案,在 DisposableBeanAdapter 类的 inferDestroyMethodIfNecessary 中,我们可以看到有两种情况会获取到当前 Bean 类中的 close()。
第一种情况,就是我们这节课提到的当使用@Bean且使用默认的 destroyMethod 属性INFER_METHOD第二种情况是判断当前类是否实现了 AutoCloseable 接口,如果实现了,那么一定会获取此类的 close()。
```
private String inferDestroyMethodIfNecessary(Object bean, RootBeanDefinition beanDefinition) {
String destroyMethodName = beanDefinition.getDestroyMethodName();
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||(destroyMethodName == null &amp;&amp; bean instanceof AutoCloseable)) {
if (!(bean instanceof DisposableBean)) {
try {
return bean.getClass().getMethod(CLOSE_METHOD_NAME).getName();
}
catch (NoSuchMethodException ex) {
try {
return bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName();
}
catch (NoSuchMethodException ex2) {
// no candidate destroy method found
}
}
}
return null;
}
return (StringUtils.hasLength(destroyMethodName) ? destroyMethodName : null);
}
```
到这,相信你应该可以结合 Closable 接口和@Service(或其他@Component)让关闭方法得到执行了。
## **[第5课](https://time.geekbang.org/column/article/369251)**
案例2中我们提到了通过反射来实例化类的三种方式
- java.lang.Class.newInsance()
- java.lang.reflect.Constructor.newInstance()
- sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()
其中第三种方式不会初始化类属性,你能够写一个例子来证明这一点吗?
能证明的例子,代码示例如下:
```
import sun.reflect.ReflectionFactory;
import java.lang.reflect.Constructor;
public class TestNewInstanceStyle {
public static class TestObject{
public String name = &quot;fujian&quot;;
}
public static void main(String[] args) throws Exception {
//ReflectionFactory.newConstructorForSerialization()方式
ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
Constructor constructor = reflectionFactory.newConstructorForSerialization(TestObject.class, Object.class.getDeclaredConstructor());
constructor.setAccessible(true);
TestObject testObject1 = (TestObject) constructor.newInstance();
System.out.println(testObject1.name);
//普通方式
TestObject testObject2 = new TestObject();
System.out.println(testObject2.name);
}
}
```
运行结果如下:
>
<p>null<br>
fujian</p>
## **[第6课](https://time.geekbang.org/column/article/369989)**
实际上,审阅这节课两个案例的修正方案,你会发现它们虽然改动很小,但是都还不够优美。那么有没有稍微优美点的替代方案呢?如果有,你知道背后的原理及关键源码吗?顺便你也可以想想,我为什么没有用更优美的方案呢?
我们可以将“未达到执行顺序预期”的增强方法移动到一个独立的切面类,而不同的切面类可以使用 @Order 进行修饰。@Order 的 value 值越低,则执行优先级越高。以案例 2 为例,可以修改如下:
```
@Aspect
@Service
@Order(1)
public class AopConfig1 {
@Before(&quot;execution(* com.spring.puzzle.class6.example2.ElectricService.charge()) &quot;)
public void validateAuthority(JoinPoint pjp) throws Throwable {
throw new RuntimeException(&quot;authority check failed&quot;);
}
}
@Aspect
@Service
@Order(2)
public class AopConfig2 {
@Before(&quot;execution(* com.spring.puzzle.class6.example2.ElectricService.charge())&quot;)
public void logBeforeMethod(JoinPoint pjp) throws Throwable {
System.out.println(&quot;step into -&gt;&quot;+pjp.getSignature());
}
}
```
上述修改的核心就是将原来的 AOP 配置,切成两个类进行,并分别使用@Order标记下优先级。这样修改后当授权失败了则不会打印“step into -&gt;”相关日志。
为什么这样是可行的呢这还得回溯到案例1当时我们提出这样一个结论AbstractAdvisorAutoProxyCreator 执行 findEligibleAdvisors代码如下寻找匹配的 Advisors 时,最终返回的 Advisors 顺序是由两点来决定的candidateAdvisors 的顺序和 sortAdvisors 执行的排序。
```
protected List&lt;Advisor&gt; findEligibleAdvisors(Class&lt;?&gt; beanClass, String beanName) {
List&lt;Advisor&gt; candidateAdvisors = findCandidateAdvisors();
List&lt;Advisor&gt; eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}
```
当时影响我们案例出错的关键点都是在 candidateAdvisors 的顺序上,所以我们重点介绍了它。而对于 sortAdvisors 执行的排序并没有多少涉及,这里我可以再重点介绍下。
在实现上sortAdvisors 的执行最终调用的是比较器 AnnotationAwareOrderComparator 类的 compare(),它调用了 getOrder() 的返回值作为排序依据:
```
public int compare(@Nullable Object o1, @Nullable Object o2) {
return doCompare(o1, o2, null);
}
private int doCompare(@Nullable Object o1, @Nullable Object o2, @Nullable OrderSourceProvider sourceProvider) {
boolean p1 = (o1 instanceof PriorityOrdered);
boolean p2 = (o2 instanceof PriorityOrdered);
if (p1 &amp;&amp; !p2) {
return -1;
}
else if (p2 &amp;&amp; !p1) {
return 1;
}
int i1 = getOrder(o1, sourceProvider);
int i2 = getOrder(o2, sourceProvider);
return Integer.compare(i1, i2);
}
```
继续跟踪 getOrder() 的执行细节,我们会发现对于我们的案例,这个方法会找出配置切面的 Bean 的 Order值。这里可以参考 BeanFactoryAspectInstanceFactory#getOrder 的调试视图验证这个结论:
<img src="https://static001.geekbang.org/resource/image/21/8e/211b5c15657881e5d0cc3cc86229a28e.png" alt="">
上述截图中aopConfig2 即是我们配置切面的 Bean 的名称。这里再顺带提供出调用栈的截图,以便你做进一步研究:
<img src="https://static001.geekbang.org/resource/image/60/a9/600ac1d34422c57276d83c8ee03a36a9.png" alt="">
现在我们就知道了,将不同的增强方法放置到不同的切面配置类中,使用不同的 Order 值来修饰是可以影响顺序的。相反,如果都是在一个配置类中,自然不会影响顺序,所以这也是当初我的方案中没有重点介绍 sortAdvisors 方法的原因,毕竟当时我们给出的案例都只有一个 AOP 配置类。
## **[第7课](https://time.geekbang.org/column/article/370741)**
在案例 3 中,我们提到默认的事件执行是在同一个线程中执行的,即事件发布者使用的线程。参考如下日志佐证这个结论:
>
<p>2021-03-09 09:10:33.052 INFO 18104 --- [nio-8080-exec-1] c.s.p.listener.HelloWorldController : start to publish event<br>
2021-03-09 09:10:33.055 INFO 18104 --- [nio-8080-exec-1] c.s.p.l.example3.MyFirstEventListener : com.spring.puzzle.class7.example3.MyFirstEventListener@18faf0 received: com.spring.puzzle.class7.example3.MyEvent[source=df42b08f-8ee2-44df-a957-d8464ff50c88]</p>
通过日志可以看出事件的发布和执行使用的都是nio-8080-exec-1线程但是在事件比较多时我们往往希望事件执行得更快些或者希望事件的执行可以异步化以不影响主线程。此时应该如何做呢
针对上述问题中的需求,我们只需要对于事件的执行引入线程池即可。我们先来看下 Spring 对这点的支持。实际上,在案例 3 的解析中,我们已贴出了以下代码片段(位于 SimpleApplicationEventMulticaster#multicastEvent 方法中):
```
//省略其他非关键代码
//获取 executor
Executor executor = getTaskExecutor();
for (ApplicationListener&lt;?&gt; listener : getApplicationListeners(event, type)) {
//如果存在 executor则提交到 executor 中去执行
if (executor != null) {
executor.execute(() -&gt; invokeListener(listener, event));
}
//省略其他非关键代码
```
对于事件的处理,可以绑定一个 Executor 去执行,那么如何绑定?其实与这节课讲过的绑定 ErrorHandler 的方法是类似的。绑定代码示例如下:
```
//注意下面的语句只能执行一次,以避免重复创建线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//省略非关键代码
SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = applicationContext.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, SimpleApplicationEventMulticaster.class);
simpleApplicationEventMulticaster.setTaskExecutor(newCachedThreadPool );
```
取出SimpleApplicationEventMulticaster然后直接调用相关 set() 设置线程池就可以了。按这种方式修改后的程序,事件处理的日志如下:
>
<p>2021-03-09 09:25:09.917 INFO 16548 --- [nio-8080-exec-1] c.s.p.c.HelloWorldController : start to publish event<br>
2021-03-09 09:25:09.920 INFO 16548 --- [pool-1-thread-3] c.s.p.l.example3.MyFirstEventListener : com.spring.puzzle.class7.example3.MyFirstEventListener@511056 received: com.spring.puzzle.class7.example3.MyEvent[source=cbb97bcc-b834-485c-980e-2e20de56c7e0]</p>
可以看出,事件的发布和处理分属不同的线程了,分别为 nio-8080-exec-1 和 pool-1-thread-3满足了我们的需求。
以上就是这次答疑的全部内容,我们下一章节再见!

View File

@@ -0,0 +1,181 @@
<audio id="audio" title="导读5分钟轻松了解Spring基础知识" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/24/a6/24f295653369a0d57b737574111dcea6.mp3"></audio>
你好,我是傅健。
在开始我们第一章的学习之前我想为你总结下有关Spring最基础的知识这可以帮助我们后面的学习进展更加顺利一些。
就第一章来说我们关注的是Spring核心功能使用中的各类错误案例。针对问题的讲解我们大多都是直奔主题这也是这个专栏的内容定位。所以对于**很多基础的知识和流程**,我们不会在解析过程中反复介绍,但它们依然是重要的,是我们解决问题的前提。借助这篇导读,我带你梳理下。
回顾Spring本身什么是Spring最基础的知识呢
其实就是那些**Spring最本质的实现和思想**。当你最开始学习的时候你可能困惑于为什么要用Spring而随着对Spring原理的深入探究和应用你慢慢会发现最大的收获其实还是对于这个困惑的理解。接下来我就给你讲讲。
在进行“传统的”Java编程时对象与对象之间的关系都是紧密耦合的例如服务类 Service 使用组件 ComponentA则可能写出这样的代码
```
public class Service {
private ComponentA component = new ComponentA(&quot;first component&quot;);
}
```
在没有Spring之前你应该会觉得这段代码并没有多大问题毕竟大家都这么写而且也没有什么更好的方式。就像只有一条大路可走时大家都朝一个方向走你大概率不会反思是不是有捷径。
而随着项目的开发推进,你会发现检验一个方式好不好的硬性标准之一,就是看它**有没有拥抱变化的能力**。假设有一天我们的ComponentA类的构造器需要更多的参数了你会发现上述代码到处充斥着这行需要改进的代码
>
private ComponentA component = new ComponentA("first component");
此时你可能会想了那我用下面这种方式来构造Service就可以了吧
```
public class Service {
private ComponentA component
public Service(ComponentA component){
this.component = component;
}
}
```
当然不行你忽略了一点你在构建Service对象的时候不还得使用new关键字来构建Component需要修改的调用处并不少
很明显这是一个噩梦。那么除了这点还有没有别的不好的地方呢上面说的是非单例的情况如果ComponentA本身是一个单例会不会好些毕竟我们可能找一个地方new一次ComponentA实例就足够了但是你可能会发现另外一些问题。
下面是一段用“双重检验锁”实现的CompoentA类
```
public class ComponentA{
private volatile static ComponentA INSTANCE;
private ComponentA() {}
public static ComponentA getInstance(){
if (INSTANCE== null) {
synchronized (ComponentA.class) {
if (INSTANCE== null) {
INSTANCE= new ComponentA();
}
}
}
return INSTANCE;
}
}
```
其实写了这么多代码最终我们只是要一个单例而已。而且假设我们有ComponentB、ComponentC、ComponentD等那上面的重复性代码不都得写一遍也是烦的不行不是么
除了上述两个典型问题还有不易于测试、不易扩展功能例如支持AOP等缺点。说白了所有问题的根源之一就是**对象与对象之间耦合性太强了**。
所以Spring的引入解决了上面这些零零种种的问题。那么它是怎么解决的呢
这里套用一个租房的场景。我们为什么喜欢通过中介来租房子呢?因为省事呀,只要花点小钱就不用与房东产生直接的“纠缠”了。
Spring就是这个思路它就像一个“中介”公司。当你需要一个依赖的对象房子你直接把你的需求告诉Spring中介就好了它会帮你搞定这些依赖对象按需创建它们而无需你的任何额外操作。
不过在Spring中房东和租房者都是对象实例只不过换了一个名字叫 Bean 而已。
可以说通过一套稳定的生产流程作为“中介”的Spring完成了生产和预装牵线搭桥这些Bean的任务。此时你可能想了解更多。例如如果一个Bean租房者需要用到另外一个Bean房子具体是怎么操作呢
本质上只能从Spring“中介”里去找有时候我们直接根据名称小区名去找有时候则根据类型户型各种方式不尽相同。你就把**Spring理解成一个Map型的公司**即可,实现如下:
```
public class BeanFactory {
private Map&lt;String, Bean&gt; beanMap = new HashMap&lt;&gt;();
public Bean getBean(String key){
return beanMap.get(key) ;
}
}
```
如上述代码所示Bean所属公司提供了对于Map的操作来完成查找找到Bean后装配给其它对象这就是依赖查找、自动注入的过程。
那么回过头看这些Bean又是怎么被创建的呢
对于一个项目而言不可避免会出现两种情况一些对象是需要Spring来管理的另外一些例如项目中其它的类和依赖的Jar中的类又不需要。所以我们得有一个办法去标识哪些是需要成为Spring Bean因此各式各样的注解才应运而生例如Component注解等。
那有了这些注解后,谁又来做“发现”它们的工作呢?直接配置指定自然不成问题,但是很明显“自动发现”更让人省心。此时,我们往往需要一个扫描器,可以模拟写下这样一个扫描器:
```
public class AnnotationScan {
//通过扫描包名来找到Bean
void scan(String packages) {
//
}
}
```
有了扫描器我们就知道哪些类是需要成为Bean。
那怎么实例化为Bean也就是一个对象实例而已很明显只能通过**反射**来做了。不过这里面的方式可能有多种:
- java.lang.Class.newInsance()
- java.lang.reflect.Constructor.newInstance()
- ReflectionFactory.newConstructorForSerialization()
**有了创建有了装配一个Bean才能成为自己想要的样子。**
而需求总是源源不断的我们有时候想记录一个方法调用的性能有时候我们又想在方法调用时输出统一的调用日志。诸如此类我们肯定不想频繁再来个散弹式的修改。所以我们有了AOP帮忙拦截方法调用进行功能扩展。拦截谁呢在Spring中自然就是Bean了。
其实AOP并不神奇结合刚才的Bean中介公司来讲假设我们判断出一个Bean需要“增强”了我们直接让它从公司返回的时候就使用一个代理对象作为返回不就可以了么示例如下
```
public class BeanFactory {
private Map&lt;String, Bean&gt; beanMap = new HashMap&lt;&gt;();
public Bean getBean(String key){
//查找是否创建过
Bean bean = beanMap.get(key);
if(bean != null){
return bean;
}
//创建一个Bean
Bean bean = createBean();
//判断要不要AOP
boolean needAop = judgeIfNeedAop(bean);
try{
if(needAop)
//创建代理对象
bean = createProxyObject(bean);
return bean;
else:
return bean
}finally{
beanMap.put(key, bean);
}
}
}
```
那么怎么知道一个对象要不要AOP既然一个对象要AOP它肯定被标记了一些“规则”例如拦截某个类的某某方法示例如下
```
@Aspect
@Service
public class AopConfig {
@Around(&quot;execution(* com.spring.puzzle.ComponentA.execute()) &quot;)
public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
//
}
}
```
这个时候很明显了假设你的Bean名字是ComponentA那么就应该返回ComponentA类型的代理对象了。至于这些规则是怎么建立起来的呢你看到它上面使用的各种注解大概就能明白其中的规则了无非就是**扫描注解,根据注解创建规则**。
以上即为Spring的一些核心思想包括**Bean的构建、自动注入和AOP**,这中间还会掺杂无数的细节,不过这不重要,抓住这个核心思想对你接下来理解各种类型的错误案例才是大有裨益的!
你好,我是傅健,这节课我们来聊一聊 Spring Bean 的初始化过程及销毁过程中的一些问题。
虽然说 Spring 容器上手简单,可以仅仅通过学习一些有限的注解,即可达到快速使用的目的。但在工程实践中,我们依然会从中发现一些常见的错误。尤其当你对 Spring 的生命周期还没有深入了解时,类初始化及销毁过程中潜在的约定就不会很清楚。