手写Spring框架Day3

ylnxwlp 菲比啾比!

第三天就不写了,来细致的了解一些前两天没有写到的一些细节。

现在我们可以知道,一个Bean创建的生命周期如下:

生命周期

UserService.class –> 推断构造函数 –> 对象(实例化) –> 依赖注入 –> 初始化前 –> 初始化 –> 初始化后(AOP) –> 代理对象 –> 放入Map单例池 –> Bean对象

有一些部分我们在简单手写模拟的时候没有仔细的进行探究,比如:

推断构造函数

使用过Spring,我们都知道,依赖注入不仅可以通过@Autowired等注解声明,还有构造函数注入。实际上,Spring更为推荐构造函数注入方式,我们经常看见@Autowired注解上会有淡淡的黄线提示推荐使用构造函数注入。
那一个类里面的构造函数可能很多,Spring又是怎么知道该使用哪个构造函数呢?
首先,在只有一个构造函数的前提下,肯定就直接使用那个构造函数。
其次,如果构造函数超出一个,就要开始条件判断:

是否有@Autowired注解声明的构造函数

如果有,还要判断有几个构造函数被加上了@Autowired。
如果只有一个,就用那个。超出一个,报错,因为Spring不知道要用哪个。
如果没有,进入下一个判断。

是否有无参构造函数

如果有,直接调用无参构造。
如果没有,也报错,因为Spring仍然不知道要用哪个构造函数。
依据这样的规则,这样Spring就可以得到想要的唯一构造函数,然后调用。

找到构造函数bean

知道了要用哪个构造函数,那怎么去找到合适的bean注入到这个构造函数呢?

首先有个概念需要理解:单例bean中的单例不是对于这个类来说的,而是对于这个bean名字来说的。即,即使类型一样,也可以存在多个bean;但是一个bean名字只有一个bean对象。

我们可以通过一个代码来理解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class UserService{

}

//APPConfig
@bean
public UserService userService1(){
return new UserService;
}

@bean
public UserService userService2(){
return new UserService;
}

好了,这下类型居然有了三个bean,只是bean的名字不一样而已。

那是不是根据bean名字去找就行了?

看看这个例子:

1
2
3
public UserService(OrderService service){
this.OrderService = service;
}

这里的bean名字,不是固定的。程序员完全可以乱命名,这样就会拿到一个一个乱七八糟的bean,根本注入不进去。

所以,这里想要找到构造函数里面的bean,既不能只根据类型,也不能只根据bean名字。应该先根据类型找到所有bean,再根据bean名字确定具体bean实例。

注意

如果类型只有一个bean实例,那就直接用就好了。

如果这样的方法无法确定,就只能报错了,说明程序员根本不想让Spring找到这个bean……

那现在就有个疑问了,那这不是执行了DI依赖注入吗?那不是破坏了原本的生命周期顺序了吗?

其实不是的,这里只是因为要实例化不得已必须将需要的依赖注入,其他暂时没有使用的依赖,即使标注了@Autowired,也是不会注入的,会等到后续的DI注入环节完成注入。

现在我们可以分析一下为什么Spring强烈推荐构造函数注入了。

1、在实例化完成后,这个bean就已经是一个完整可用的bean了。

2、构造函数注入天然支持final字段。

3、避免NullPointerException,构造函数注入的依赖缺失会在应用启动时立即报错,而不是等到运行时才抛出NullPointerException。

一点小八股:你知道@Autowired和@Resource查找bean的顺序是怎么样的吗?

@PostConstruct

这个注解修饰的方法执行是在DI注入,Aware回调后,初始化的时候,也就是有一个bean的后置处理器进行执行。

Spring会查找这个类class对象的每一个方法上面是否有@PostConstruct这个注解,如果有,直接invoke即可。

AOP

讲到这里,我们应该都知道AOP是在什么时候创建出来的吧,即初始化之后通过后置处理器弄出一个代理对象,加入单例池里面。那我们可以考虑一个情况:

如果一个bean被代理了,里面的所有注入的bean还会有值吗?

我们第一直觉肯定是有的吧,不然怎么使用依赖的bean方法。但是实际上,在代理对象产生后,所有bean都变成了null。根据bean的生命周期我们可以看出来,在AOP动态代理创建了之后,不会再有一个DI环节,所有依赖的bean都是null。

那不是出问题了吗?那我们调用这个被代理的bean去打印一下里面依赖的bean,真的会是null吗?

实际上又是有值的。那不是矛盾了么?到底是有还是没有?

还真没有。那为什么打印的时候又有值?

我们以CGLIB代理为例。我们知道它可以通过继承的方式去为我们生成非接口的动态代理。假如父类UserService里面有一个test方法,打印了依赖的bean OrderService,,这个代理对象就会这样:

1
2
3
4
5
6
7
8
public class 某某Proxy extends UserService{

@Override
public void test(){
执行我们定义的切面逻辑……
调用原生的test方法……
}
}

看到调用原生的test方法,我们第一反应就是super.test()。但问题是,你依然没有对这个OrderService去进行依赖注入,调用父类的test方法仍然是null。所以肯定要调用有注入对象的对象方法。

哪里有注入好的对象?从始至终就只有一个吧,那就是Spring在AOP创建前的那个对象。那代理对象又是怎么调用的呢?

保存这个对象就好了。代理对象里面有一个字段target,表明被代理的对象是哪个,也就是 Private UserService target。

这下我们就知道了:

1
2
3
4
5
6
7
8
9
10
public class 某某Proxy extends UserService{

Private UserService target;

@Override
public void test(){
执行我们定义的切面逻辑……
target.test();
}
}

这样就可以正常工作了。

Spring事务

事务其实也是一个代理对象。Spring会为开启了事务的类生成代理,然后执行类似于AOP的逻辑,在执行的前后插入事务控制逻辑。

那怎么控制整体的事务呢?Spring让一个方法下的所有数据库操作都处于一个事务中,并用try-catch去判断是否要回滚。

为了让一个方法下的所有操作都处于一个事务,肯定要把事务自动提交关闭,但是如果是JDBC原生连接,这个是默认开启的。所以Spring统一管理了一个jdbcTemplate bean,并且连接在创建的时候把自动提交关闭了。

注意

如果你自己建立了一个jdbc连接,即使加上@Transactional,事务仍然失效。

事务执行流程:

1
开启事务 --> 事务管理器新建一个数据库连接connection,并关闭自动提交 --> 业务SQL(jdbc bean需要拿到数据库连接) --> 判断是否提交/回滚 

为什么内部调用导致事务传播失效?

这个时候我们就可以来讨论一下为什么内部调用会导致SPring事务失效了。

我们可以看一下这个:

1
2
3
4
5
6
7
8
9
10
@Transactional
public void a(){
业务SQL;
b();
}

@Transactional(propagation = Propagation.NEVER)
public void b(){
业务SQL;
}

我们配置了b这个方法当外部有事务存在时候,就抛异常。但是这个代码运行起来真的会抛异常吗?

当然不会。因为已经Spring事务在b已经失效了。@Transactional(propagation = Propagation.NEVER)相当于没有写。

为什么呢?我们想想上面的AOP原理:当执行业务方法的时候,我们调用的是target里面的原生方法。那么在原生方法里面,调用b方法可以看作:

1
this.b();

this是什么?是没有被代理的对象target吧?那对于必须要代理增强的事务来说,这个b没有被代理处理,自然b的事务就没有用了,也就相当于白写。

那怎么破局?自然就是要让代理去执行吧。我们不能让this去做,那我们就去得到一个proxy。从单例池里面直接注入当前target的代理对象,用这个注入的代理对象再去调用b就行了,实际上就是套娃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class userService{
@Autowired
private UserService userService;

@Transactional
public void a(){
业务SQL;
userService.b();
}

@Transactional(propagation = Propagation.NEVER)
public void b(){
业务SQL;
}
}

这样就可以在代理的target里面再套一个代理,就能使用了,这个时候运行就会抛出异常。

@Configuration是怎么影响Spring事务的?

在讲这个注解之前,回到最初的Spring事务准备流程。在正常使用事务功能前,是需要进行配置的。我们需要用bean声明连接,jdbc,数据源。其中,jdbc声明需要调用一次声明datasource bean的方法,连接声明也需要调用一次声明datasource bean的方法。

配置好了之后,如果在配置类APPconfig里面,不加上@Configuration,我们试着去执行一下事务的方法,发现事务直接就不能用了,而且不是像上面一样传播的时候才失效,而是从一开始就直接坏了。

为什么呢?在分析原理之前,我们要先知道一下Spring是通过什么去管理连接的。因为jdbc这个类对于每一个数据都是一样的,不一样的是不同数据的connection,jdbc只要拿到提前设置好的connection,就可以对每一个不同的数据库进行操作。

又由于在处理一个方法的时候肯定是单线程执行的,所以这里Spring存储管理用的是ThreadLocal,里面的类型为Map<DataSource, Connection>。这样jdbc在每一条线程执行的时候,根据需要拿到不同数据库的连接了。

那现在回到最初的bean声明环节。我们发现jdbc bean声明要调用一次DataSource bean声明方法,Connection bean声明也要调用一次DataSource bean声明方法。这样就会导致一个很大的问题:DataSource return了两个new的对象。换句话说,jdbc得到的DataSource对象和Connection得到的DataSource对象不一样,这个时候,当jdbc从ThreadLocal里面拿Connection,就会拿不到,那没办法了,Spring只得重新建立一个jdbc和connection去执行sql,但是此时,自动提交就是开启了的。事务当然也就失效了。

要解决这个方法,加上@Configuration就行了。根据这个原理分析,我们很容易推断出@Configuration的解决方案是什么,也就是把jdbc和connection里面的DataSource进行一个统一,那他是怎么做的呢?

同样基于动态代理。动态代理继承类,并重写声明连接,jdbc,数据源的bean方法。注意这里的重写方法里面调用的是super,而不是target

声明连接,jdbc没什么好做手脚的,问题主要出在数据源的bean声明。为了不让数据源bean声明两次,动态代理会在调用数据源的bean声明方法时候,先去单例池里面找有没有需要的DataSource的bean。如果有,就直接返回单例池里面的,只有没有才会进行创建

所以,@Configuration依照动态代理的方式,解决了这个问题。

循环依赖问题见单独文章:循环依赖 —— 为什么是三级缓存?

第三天的学习到这里也就结束了。

  • 标题: 手写Spring框架Day3
  • 作者: ylnxwlp
  • 创建于 : 2025-10-03 12:44:57
  • 更新于 : 2025-10-05 23:39:52
  • 链接: https://www.swissroll.today/2025/10/03/手写Spring框架day3/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论