Java 手写 Spring框架 IOC 和 AOP----手写Spring IoC和AOP

源码(码云):https://gitee.com/yin_zhipeng/implement-_spring_of_myself.git

本文源码基于5.1.12版本讲解:Spring的源代码设计精妙、结构清晰、匠心独用、处处体现大师风范,对Java设计模式的灵活运用,对Java技术的高深造诣,无疑是Java技术的最佳实践范例
  1. 另外,IoC和AOP两种思想,不是Spring提出,在Spring之前就提出了,只不过更偏向于理论化,而Spring在技术层次,把两个思想用Java语言做了非常好的实现

一、Spring 概念复习

这里快速过一下Spring基本概念 ,免得手写源码和看源码的时候,有些东西忘了(会觉得不明所以),而且也是面试官经常会问的一些问题

1. 基本概念

什么是Spring 框架(Spring Framework)
  1. 分层(Controller,Service,Mapper)的full-stack(全栈)轻量级开源框架(发展越来越好,已经可以提供全套的功能支持)
  2. 以IoC和AOP为内核,提供展现层Spring MVC和业务层事务管理等众多企业级应用技术(Spring可以用在各层提供服务)
  3. 可以整合开源世界众多著名的第三方框架和类库,已经是使用最多的Java EE企业应用开源框架(站在巨人的肩膀上,提供整合接口,可以整合其它很多优秀框架)
Spring 框架的优势
  1. 方便解耦合,简化开发
  1. 通过IoC容器,可以将对象间依赖关系交由Spring进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为单例设计模式类、属性文件解析等这些很底层的需求编写代码,可以更注重与上层的应用实现。
  1. AOP编程的支持
  1. 方便进行面向切面的编程,许多用传统OOP不容易实现的功能,可以通过AOP轻松应付
  1. 声明式事务的支持(事务和业务代码没有耦合到一块)
  1. 一般我们只需要方法上加个@Transactional注解就可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式的方式灵活的进行事务的控制,提高开发效率和质量
  1. 方便程序的测试
  1. 可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,随手就可以做
  1. 方便继承各种优秀框架
  1. Spring 可以降低各种框架使用难度,提供了对各种优秀框架的直接支持(Struts、Hibernate、Hessian、Quartz等等)
  1. 降低JavaEE API的使用难度
  1. Spring对JavaEE API(例如JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些API的使用难度大为降低。
Spring核心结构
  1. Spring是一个分层非常清晰并且依赖关系、职责定位非常明确的轻量级框架,主要包括数据处理、Web、AOP/Aspects、Core Container、Test模块
  2. Spring依靠这些基本模块,实现了一个令人愉悦的融合了现有解决方案的零入侵的人轻量级框架,并且模块化的思想,我们需要什么模块,引用什么就可以了,一个模块包含多个jar包,如下图:
    在这里插入图片描述

2. IoC编程思想和DI

什么是IoC(Inversion of Control/控制反转/反转控制)
  1. IoC是一个思想,不是技术实现,IoC思想下,是将对象,统一交给IoC容器管理
  2. 我们不需要考虑对象的创建、销毁等管理问题,这些全部交给IoC容器去做
  3. IoC容器会帮我们实例化对象并管理它们,我们要用,就去和IoC容器要
控制反转
  1. 控制:指对象的创建(实例化,管理)的权利
  2. 反转:控制权交给外部环境(Spring框架、IoC容器)
为什么可以帮助我们解决类与类之间的耦合
  1. 一般我们Service(逻辑业务层)会使用dao(持久层)的功能,如果我们使用传统new对象的话,我们就需要和具体实现类耦合(一般我们都使用接口,但是new对象,需要用具体实现类)
//假设我们有一开始的实现类是UserDaoImpl
UserDao userDao = new UserDaoImpl();
//当我们想要将实现类换为UserDaoInplNew,那么所有Service层的代码都需要修改
UserDao userDao = new UserDaoInplNew();
  1. 但使用Spring,我们只需要接口,不需要在Service层new实现类,此时无论我们怎么变换接口的实现类,都不需要修改Service层代码
@Autowired
private UserDao userDao;
DI(Dependancy Injection依赖注入)
  1. 和IoC描述的是同一件事情,但是角度不一样
  2. IoC站在对象的角度,对象实例化及管理权利交(反转)给了容器
  3. DI站在容器的角度,会把对象依赖的其它对象注入,比如IoC容器中有A类和B类。A类依赖B类,那么我们实例化A类的时候,需要将B类对象注入给A对象
什么是依赖
  1. 假设有A类和B类,A类中有这样一段代码B b = new B();
  2. 那么我们说A类依赖B类

3. AOP编程思想

传统OOP(面向对象)利用封装继承多态,可以解决大部分代码重复问题,一种垂直纵向的继承体系
  1. 下图中,每个动物都有吃和跑方法,那么就可以让他们继承公共父类Animal,而不用每个动物都写重复代码
    在这里插入图片描述
AOP(Aspect oriented Programming/面向切面编程/面向方面编程):是OOP的延续,解决OOP解决不了的问题
  1. 假设我们要在每个方法执行前后做一些额外操作,比如记录每个动物,什么时候开始吃,什么时候吃完,什么时候开始跑,什么时候跑完
  2. 那么我们就需要在每个方法(eat()和run())的逻辑代码前后添加额外的代码,并且这些代码是重复的,当然我们要避免这种情况,这时就可以使用切面编程,下图中,可以看出,不使用切面编程,有很多重复代码
    在这里插入图片描述
  3. 当然我们可以使用动态代理设计模式,而AOP就是动态代理的实现
    在这里插入图片描述
  4. 上图中,可以发现,我们将每个方法(假设有100个方法)前后执行的额外动作,抽离出去,然后向一把刀一样,对着每个方法的两个点(一堆点,就组成了一个面),切了上去,就像一个切面,所以我们叫面向切面编程
  5. 这样呢,我们将横切代码,和业务逻辑代码分开了(解耦合),而且,我们不用每个方法中都写横切代码,只需要写一份
  6. 可以理解为,原来我们将横切代码做成点,在每一个方法进行横切,而AOP,是直接做成一把刀,一刀下去所有点都一起切了
AOP解决的问题
  1. 不改变原有业务逻辑的情况下,增强功能(横切逻辑代码),根本上解耦合,避免横切逻辑代码重复

二、手写IoC容器

实例化对象问题,使用反射解决
  1. 为了避免使用接口,new实现类这种耦合问题,我们需要换一种实例化对象的方式
  2. 除了new关键字实例化对象以外,我们还可以通过反射来获取对象实例
  3. 只需要提供这个类的全限定类名(完整的类名例如:java.lang.String)然后通过Class.forName(“全限定类名”);就可以拿到这个类的对象
  4. 但是这又有新的耦合问题,全限定类名如果写到类中,就会耦合,日后我们想要改,又得去改所有用到的类
  5. 所以我们可以让它和类解耦合,将全限定类名配置到xml文件中
思路有了,用什么方法实例化对象呢?我们可以参考设计模式,例如工厂模式
  1. 使用工厂设计模式,通过反射技术生产对象
  2. 工厂模式是解耦合的一种非常好用的方式

1. 通过xml文件配置bean实例

假设我们有如下类结构

在这里插入图片描述

为了解耦合,我们将实例对象都配置到xml文件中
  1. 我们实例化的是特定的实现类 的全限定类名 class
  2. 我们要给每一个实例配置一个唯一标识 id
    在这里插入图片描述

2. 通过工厂生产实例

思路
  1. 读取xml文件,根据class全限定类名,反射实例化对象,然后以id为key,以实例化对象为value,存储到map中
  2. 对外提供获取实例对象的接口(根据id)
为了方便起见,我们使用dom4j工具类,解析xml文件,例如Jaxen提供的XPath表达式,方便我们操作解析好的xml文档对象(可以快速检索我们需要的东西),引入相关maven依赖

在这里插入图片描述

XPath表达式,通过路径表达式来选取XML文档中的节点或节点集
    /**
     * 常用表达式
     * nodename 选取此节点的所有子节点
     * /        从根结点选取
     * //       从当前匹配的节点选取,会选取当前节点下的所有匹配的子节点(不考虑层级,包括孙子节点)
     * *        选取所有
     * **       选取当前结点的父节点
     * @        选取属性
     */
工厂类

在这里插入图片描述
在这里插入图片描述

此时我们Service层想用dao层对象,就不用new实现类了

在这里插入图片描述

问题
  1. 虽然不用new实现类了,但是我们把工厂写死到代码里面了,也有一定程度的耦合
  2. 理想的状态应该是
private AccountDao accountDao;
  1. 那实例对象从哪来呢,我们可以通过set方法来设置
    在这里插入图片描述
那么我们该如何做呢?
  1. xml中给bean实例配置属性,我们根据属性值AccountDao,调用setAccountDao方法,传入的参数用ref来指定(反射好的AccountDaoImpl映射,就是上面配置的内个)
    在这里插入图片描述
  2. 工厂实例化对象完成后,维护对象依赖关系,根据属性将其注入
    在这里插入图片描述
测试一下效果
  1. 给两个接口提供test()方法,两个实现类进行实现
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 写个测试类测试
    在这里插入图片描述
    在这里插入图片描述

三、手写AOP

一般我们会用AOP来处理事务问题,假设我们有A用户和B用户,两人分别有10元
  1. 当用户a给用户b转账5元后,程序正常情况下,最终A有5元,B有15元
  2. 但是如果用户a转账5元后,程序出错,将前转给B的代码没有运行,最终就会发生用户A失去了5元,但是用户B没有收到钱的情况
    在这里插入图片描述
  3. 如果换一下,B先+5元,然后A再-5元,那么程序出错后,B有15元,但是A没有-5元,依然有10元,这样银行就亏钱了
  4. 那么我们希望什么结果呢?
  1. 当程序出错后,及时回滚,
  2. 假设a-5后,程序报错,我们要立即回滚,让a的余额重新变为10,然后通知转账失败
如果将上面的操作,都换成JDBC操作数据库(两次查询,两次修改,一共4次数据库(JDBC)操作),那么会有如下问题
  1. 首先数据库事务,其实是Connection(连接)的事务,默认是自动提交事务
connection.setAutoCommit(false);//设置为不自动提交
connection.commit();//提交事务
connection.rollback();//回滚事务
  1. 那么两次update使用两个数据库连接Connection,这样的话,肯定不属于同一个事务控制
  2. 而且事务控制我们要统一放到service层控制(一个service可能涉及到很多dao层操作)
解决方案
  1. 两次update使用同一个connection连接
  1. 首先,一个请求过来肯定在一个线程中执行,无论多少次update,都在这个线程中,所以将connection和当前线程绑定,当前线程有关系的数据库操作都使用这个Connection
  1. 把事务控制添加在service层

1. 使用JDBC操作数据库模拟银行转账,解决事务问题

1. 创建数据库和实体类,引入相关依赖

数据库

在这里插入图片描述

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (
  `id` char(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `money` int(20) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES ('1', 'A', 10);
INSERT INTO `account` VALUES ('2', 'B', 10);

SET FOREIGN_KEY_CHECKS = 1;
对应实体类

在这里插入图片描述

引入相关依赖

在这里插入图片描述

2. 将连接与当前线程绑定,多个JDBC操作使用同一个连接

Druid数据库连接池工具类,懒汉式单例

在这里插入图片描述

将连接绑定到当前线程的工具类,单例模式,否则每次都new一个对象,依然不是同一个connection连接

在这里插入图片描述

编写dao层,提供对数据库的操作

在这里插入图片描述
在这里插入图片描述

此时,一个线程,多个JDBC操作都使用同一个连接,我们编写service层。进行测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. service层添加事务控制

上面已经实现了一个线程绑定一个连接,用一个连接执行多个JDBC操作,现在就可以编写事务了
  1. 如果JDBC操作使用不同连接,这些操作不在一个事务中,是没办法控制的
  2. 想要事务控制,就必须实现共用一个连接
接下来,我们将一步步的升级,从普通事务处理,到动态代理,到AOP
为transfer转账方法添加事务代码

在这里插入图片描述

2. 面向切面,事务管理

上面解决了事务问题,但是现在每个方法,都需要写相同的事务代码,接下来将其解耦

1. TransactionManager事务管理器

我们将事务代码单独写到事务管理器

在这里插入图片描述

service层使用

在这里插入图片描述

这种办法很笨,如果有100个方法,就需要这样写100次,而且也依旧存在业务代码和事务代码耦合的问题。虽然改事务,直接去事务管理器改就行,但是如果我们要换个事务管理器,又得每个方法都改

2. 动态代理改造

代理设计模式可以参考这篇文章,详细介绍了静态代理和动态代理:https://blog.csdn.net/grd_java/article/details/109690730
为什么选用动态代理
  1. 静态代理,是针对特定的一件事代理,比如你是租房中介代理,那么你需要单独的一个代理类处理租房这件事,租车代理,又需要单独写租车代理
  2. 显然,使用spring的千千万,没办法为每一件事都写一个静态代理类,有办法,也太多了
  3. 动态代理,由反射实现,不需要为每一个事件,都写一个代理类,它可以动态的为很多事进行代理,也就是根据委托对象的不同,生成不同的代理人
动态代理,Java有两种主流实现方式,一种是JDK原生动态代理,一种是利用第三方jar包CGLIB实现动态代理
  1. 我们做一个代理工厂,根据我们的需要,选择不同的代理
  2. 通过JDK(需要提供接口)或CGLIB(不需要接口)生产代理对象
  3. CGLIB需要引入额外jar包
    在这里插入图片描述
代理工厂:生成不同种类的代理,例如JDK原生,或者Cglib

在这里插入图片描述
在这里插入图片描述

接下来,service层就不需要任何事务横切代码了

在这里插入图片描述

使用动态代理对象,处理事务

在这里插入图片描述
在这里插入图片描述

当然,你可以更粗暴一点,service层异常代码统统可以不要

在这里插入图片描述

3. 交给IoC管理

我们的AOP实现,全部使用new对象的方式,耦合严重,我们需要将其解耦合
  1. 配置beans.xml
    在这里插入图片描述
  2. ConnectionUtils类,有IoC管理,我们不用自己实现单例设计模式了
    在这里插入图片描述
  3. TransactionManager,不用自己实现单例,并且依赖ConnectionUtils,解耦合,让IoC依赖注入,不new对象
    在这里插入图片描述
  4. ProxyFactory,不自己实现单例,不new对象,让IoC帮忙注入
    在这里插入图片描述
  5. 使用代理工厂时(AOP),和IoC要
    在这里插入图片描述

四、定义@Service、@AutoWired、@Transaction注解类

来回写代码,不如一个注解直接解决来的香,接下来完成基于注解的IOC容器(Bean对象创建及依赖注入维护)和声明式事务控制
  1. 我们要考虑注解有无value属性值(例如@Service(value=""))
  2. 添加事务时,service层是否有接口,选择使用JDK还是CGLIB版本的代理对象
@TODO 日后更新

五、高级使用和源码

由于篇幅限制,我将其放在这篇文章:https://blog.csdn.net/grd_java/article/details/122647693
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值