【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
!! 整体重心放在Ioc和AOP的源码剖析中,同时结合手写实现充分"得之以渔"
一、 Spring概述
1.1 简介
它是——分层的full-stack(全栈)轻量级开源框架,已Ioc和AOP为内核。
整合了开源世界众多著名的第三方库和类库
Spring官网-->重要的资料查阅渠道
大家常说的Spring指得就是Spring Framework
官网使用方式:
1.2 Spring's History
- 1997~2006:EJB1.0 -> EJB3.0
- 2017.09:Rod Johnson(Spring之父)团队发布Spring5.0通用版(GA)
1.3 长存优势
「解耦、简化开发」
对象间的依赖关系被交给Spring容器控制,避免了硬编码
造成的程序耦合;
用户不再需要为底层需求改动代码,可以专注于上层的业务实现.
「AOP编程支持」
有了面向切面编程的功能,很多OOP实现起来困难的问题,AOP能够轻松解决.
「声明式事务支持」
Transactional
事务的管理往往是枯燥繁琐的,有了声明方式可以让事务的管理更加高效.
「支持简易测试」
可以⽤⾮容器依赖的编程⽅式进⾏⼏乎所有的测试⼯作,测试不再是昂贵的操作,⽽是随⼿可做的 事情
「方便继承各类优秀框架」
Spring可以降低各种框架的使⽤难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、 Quartz等)的直接⽀持
「降低JavaEE API的使用难度」
Spring对JavaEE API(如JDBC、JavaMail、远程调⽤等)进⾏了薄薄的封装层,使这些API的使⽤ 难度⼤为降低
「源码设计非常优秀,可以算是经典」
Spring的源代码设计精妙、结构清晰、匠⼼独⽤,处处体现着⼤师对Java设计模式灵活运⽤以及对
Java技术的⾼深造诣。
它的源代码无疑是Java技术的最佳实践的范例
1.4 核心结构
1.5 实时查询版本以及支持
查看官网推荐版本
获取官方推荐jdk支持
使用Spring 5.x+ 时一定要注意jdk保持8+以上
二、 核心思想
在Spring出现之前,Ico和AOP就已经提出了,只不过那时候是理论化的。
Spring是将它们进行了理论实现
2.1 啥是Ioc?Ico解决了啥子问题?
控制反转(Inversion of Control)
是一种技术思想,而非技术实现
-
传统开发:A中依赖B,则在A中 new 一个B对象
-
Ioc思想下开发:'new对象'的这个动作,交给Ioc容器(Spring框架)去实例化和管理;程序员需要用哪个问容器索取即可。
分析[控制+反转]
- 控制 —— 对象创建(实例化、管理)的权利
- 反转 —— 控制权交给外部环境(Spring框架、Ioc容器)
解决问题点:解耦
2.2 Ioc和DI的区别
二者描述的是同样的概念,但描述的角度不同
- Ioc 站在
对象的角度
对象的实例化、管理权利 交给(反转给) 容器; - DI 站在
容器的角度
在A依赖B时,容器将B的实例创建好 然后注入 进A中.
2.3 啥是AOP?AOP解决了啥子问题?
面向切面编程(Aspect oriented Programming)
是OOP的延续
- 三大特征 —— 封装、继承、多态
2.4 为什么叫它 '面向切面'?
- 「切」从垂直的业务流中 将功能切入;
- 「面」通常要切入的功能,影响的不止一个方法,每个被影响的方法被切入一个点,多个点组成了面.
三、 手写实现Ioc和AOP
模拟「银行转账」项目
拉勾开源试验代码 (每一步旧记录均注释)
前端页面
数据库
「基础内容加餐」
A 简单工厂模式再回顾
public interface INoodles {
/**
* 描述面条
*/
void desc();
}
public class LaMian implements INoodles {
@Override
public void desc() {
System.out.println("一碗来自兰州的拉面");
}
}
public class PaoMian implements INoodles {
@Override
public void desc() {
System.out.println("一碗来自超市的泡面");
}
}
public class DanDanMian implements INoodles {
@Override
public void desc() {
System.out.println("一碗来自四川的担担面");
}
}
public class SimpleNoodlesFactory {
public static final int LM = 1;
public static final int PM = 2;
public static final int DDM = 1;
public static INoodles createNoodles(int noodleType) {
switch (noodleType) {
case 1:
return new LaMian();
case 2:
return new PaoMian();
case 3:
return new DanDanMian();
default:
return null;
}
}
}
测试
public class Test {
public static void main(String[] args) {
SimpleNoodlesFactory.createNoodles(1).desc();
SimpleNoodlesFactory.createNoodles(2).desc();
SimpleNoodlesFactory.createNoodles(3).desc();
}
}
B dom4j + xpath 使用
匹配模式 | 说明 |
---|---|
nodename | 选取此节点的所有子节点 |
/ | 从根节点选取 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
依赖
<!--DMO4J-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
<scope>test</scope>
</dependency>
<!--xpath表达式-->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
测试文件
<?xml version="1.0" encoding="UTF-8"?>
<books>
<book num="1001" name="《Thinking in JAVA》">
<version id="1.0" time="2017.09"/>
</book>
<book num="1002" name="《Leaning Python》"/>
<book num="1003" name="《C Primer Plus》"/>
<book num="1004" name="《Sharp Jquery》"/>
</books>
测试类
@Test
public void test() throws DocumentException {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("books.xml");
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(resourceAsStream);
Element root = document.getRootElement();
// 直接nodeName —— 选取节点的 所有子节点
System.out.println("------------------");
List<Element> bookList = root.selectNodes("book");
for (Element element : bookList) {
Attribute attrib = (Attribute) element.selectNodes("@num").get(0);
System.out.println( attrib.getValue() + " - " + element.attributeValue("name"));
}
// / —— 从根节点选取
System.out.println("------------------");
List<Element> firstNode = root.selectNodes("/bookStore");
for (Element element : firstNode) {
System.out.println("根节点:" + element.attributeValue("Sname"));
}
// // —— 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置
System.out.println("------------------");
List<Element> innerBooks = root.selectNodes("//book");
for (Element element : innerBooks) {
System.out.print(element.attributeValue("name") + "\t");
// .. —— 选取当前节点父节点
List<Element> fatherNodes = element.selectNodes("..");
for (Element fatherNode : fatherNodes) {
System.out.println("--> 父节点是:" + fatherNode.attributeValue("Sname"));
}
}
// . —— 选取当前节点
System.out.println("------------------");
List<Element> currentNodes = root.selectNodes(".");
for (Element element : currentNodes) {
System.out.println("当前节点: " + element.attributeValue("Sname"));
}
}
C 单例模式
- 饿汉模式
public class HungrySingleton {
// 构造方法私有化(单例模式必须的一步)
private HungrySingleton() {}
// 将自身实例化对象设置为一个属性,并用static、final修饰
private static final HungrySingleton instance = new HungrySingleton();
// 静态方法返回该实例
public static HungrySingleton getInstance() {
return instance;
}
}
- 懒汉模式
public class LazySingleton {
// 构造方法私有化
private LazySingleton() {}
// 将自身实例化对象设置为一个属性,并用static修饰
private static LazySingleton instance;
// 静态方法返回该实例,加synchronized关键字实现同步(不然两个线程都进来会有安全问题)
public static synchronized LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
D 注解基础
@Target 注解
说明了Annotation所修饰的对象范围:
- FIELD 被用于描述域
- PACKAGE 被用于 packages
- TYPE 被用于 types(类、接口、枚举、Annotation类型)
- METHOD/CONSTRUCTOR 被用于 类型成员(方法、构造方法、成员变量、枚举值)
- PARAMETER 被用于 方法参数和本地变量(如循环变量、catch参数)
@Retention
定义了该Annotation被保留的时间长短
- SOURCE 源文件中有效
- CLASS 在class文件中有效
- RUNTIME 运行时有效
Retention meta-annotation类型有唯一的value作为成员,它的取值来自Java.lang.annotation.RetentionPolicy的枚举类型值
@Documented
表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理(无成员的标记注解)
四、 Spring IOC 应用
Tips. 无论是xml还是注解,其实都有一一对应关系,深入一个即可
4.1 Spring框架的三种IOC实现
-A- 纯XML
开启方式
- JavaSE应用
// 1
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 2
new FileSystemXmlApplicationContext("c:/beans.xml");
- JavaWeb应用
使用 ContextLoaderListener 监听器
-B- XML + 注解 工作最常用
- JavaSE应用
和 A 一样
- JavaWeb应用
和 A 一样
-C- 纯注解
- JavaSE应用
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
- JavaWeb应用
使用 ContextLoaderListener 监听器
4.2 BeanFactory与ApplicationContext区别
待跟进
4.3 Spring IOC⾼级特性
待跟进
-A- 延迟加载 lazy-init
待跟进
-B- 后置处理器
待跟进
五、 Spring IOC 源码深度剖析
「开战前准备」
- 当前工具版本:
IDEA2019.3
+Spring 5.0x
+gradle 5.6.3
+jdk8
- 从gitHub上拉源码
- spring是用gradle管理的,请自行安装并配置 大神级参考I 大神级参考II
过程中会遇到各式各样的问题,网上都有解决方式。
P.S.(各类工具都不要用 自认为最新的)
5.1 Spring IoC容器初始化主体流程
从简单的一行代码开始:
// 内含洞天
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
- ApplicationContext是容器的高级接口,BeanFacotry(顶级容器/根容器,规范了/定义了容器的基础行为)
- ApplicationContext是 Spring应用上线文——官方:
IoC容器(一套组件和过程的集合)
- ApplicationContext 有一个重要组件——Map型的
单例池 singletonObjects
从BeanFactory开始
spring.beans项目中
5.2 Bean生命周期关键时机点
P.S. 以下两张图片from拉勾
-【A】- Bean的无参构造断点
观察调用栈,从测试方法testIoC()开始 向上依次是往后执行的调用
-【B】- 在初始化方法中断点
-【C】- BeanFactory后置处理器 初始化中/调用方法时 断点
-【D】- Bean后置处理器 初始化 断点
-【E】- Bean后置处理器beafore/after方法 断点
-【总结】-
-
构造器执行、初始化方法执行、Bean后置处理器的before/after方法、:
AbstractApplicationContext
-->refresh
-->finishBeanFactoryInitialization
-
Bean工厂后置处理器初始化、方法执行
AbstractApplicationContext
-->refresh
-->invokeBeanFactoryPostProcessors
-
Bean后置处理器初始化:
AbstractApplicationContext
-->refresh
-->registerBeanPostProcessors
—————————— 可见,refresh() 是个非常重要的方法——————————
5.3 容器初始化具体子流程
-【正向进入】- refresh()
进入!
5.4 BeanFactory创建流程
开始进入-->ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
- refreshBeanFactory
- 时序图
- loadBeanDefinitions(加载BeanDefinition)
5.5 Bean创建流程
待跟进
5.6 lazy-init 延迟加载机制原理
待跟进
5.7 「对标P7难点」Spring IoC循环依赖问题
内核思想
时序图
六、 Spring AOP 应用
AOP本质: 在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、⽇志代码、事务控制代码、性能监控代码
我们将红框代码抽离出来:
6.1 相关术语
6.1.1 Joinpoint(连接点)
-指的是那些可以⽤于把增强代码加⼊到业务主线中的点。在上图中,这些点指的就是⽅法。
在⽅法执⾏的前后通过动态代理技术加⼊增强的
代码。
-在Spring框架AOP思想的技术实现中,也只⽀持⽅法类型的连接点
6.1.2 Pointcut(切入点)
-指的是那些已经把增强代码加⼊到业务主线进来之后的连接点。
6.1.3 Advice(通知/增强)
-指的是切⾯类中⽤于提供增强功能的⽅法。
-不同的⽅法增强的时机是不⼀样的:
- 开启事务肯定要在业务⽅法执⾏之前执⾏;
- 提交事务要在业务⽅法正常执⾏之后执⾏;
- 回滚事务要在业务⽅法执⾏产⽣异常之后执⾏;
- ...
6.1.4 Target(目标对象)
-指的是代理的⽬标对象。即被代理对象
6.1.5 Proxy(代理)
-指的是⼀个类被AOP织⼊增强后,产⽣的代理类。即代理对象
6.1.5 Weaving(织入)
-指的是把增强应⽤到⽬标对象来创建新的代理对象的过程。
- spring采⽤动态代理织⼊;
- AspectJ采⽤编译期织⼊和类装载期织⼊.
6.1.5 Aspect(切面)= 切⼊点+增强
-指的是增强的代码所关注的⽅⾯,把这些相关的增强代码定义到⼀个类中,这个类就是切⾯类。
6.2 Spring如何选择 代理类型?
默认情况下,Spring会根据被代理对象是否实现接⼝来选择使⽤JDK还是CGLIB。
- 当被代理对象没有实现任何接⼝时,Spring会选择CGLIB
- 当被代理对象实现了接⼝,Spring会选择JDK官⽅的代理技术
我们可以通过配置的⽅式,让Spring强制使⽤CGLIB。
6.3 Spring中AOP的配置方式
和IoC一样,支持三种方式:
- 纯XML
- XML+注解
- 纯注解
6.4 Spring中AOP的实现(XML模式)
-【A】- 引入jar包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.12.RELEASE</version>
</dependency> <dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
-【B】- 核心配置
- 把
通知Bean
交给Spring管理 - 使用
aop:config
开启aop的配置 - 使用
aop:aspect
配置切面 - 使用对应的标签配置 通知的类型
<!--
Spring基于XML的AOP配置前期准备:
在spring的配置⽂件中加⼊aop的约束
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd-->
<!--把通知bean交给spring来管理-->
<bean id="logUtil" class="com.lagou.utils.LogUtil"></bean>
<!--开始aop的配置-->
<aop:config>
<!--配置切⾯-->
<aop:aspect id="logAdvice" ref="logUtil">
<!--配置前置通知-->
<aop:before method="printLog" pointcut="execution(public *com.lagou.service.impl.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account))"></aop:before>
</aop:aspect>
</aop:config>
-【补充】- 切入点表达式&AspectJ
指的是遵循特定语法结构的字符串,其作⽤是⽤于对符合语法格式的连接点进⾏增强
。
(它是AspectJ表达式的⼀部分。)
AspectJ是⼀个基于Java语⾔的AOP框架,Spring框架从2.0版本之后集成了AspectJ框架 中切⼊点表达式的部分,开始⽀持AspectJ切⼊点表达式。
-【补充】- 五种通知类型
- 前置通知
aop:before
只能出现在aop:aspect标签内部
method:⽤于指定前置通知的⽅法名称
pointcut:⽤于指定切⼊点表达式
pointcut-ref:⽤于指定切⼊点表达式的引⽤
前置通知可以获取切⼊点⽅法的参数,并对其进⾏增强
- 正常执行时通知
aop:after-returning
只能出现在aop:aspect标签内部
method:⽤于指定后置通知的⽅法名称
pointcut:⽤于指定切⼊点表达式
pointcut-ref:⽤于指定切⼊点表达式的引⽤
- 异常通知
aop:after-throwing
只能出现在aop:aspect标签内部
method:⽤于指定异常通知的⽅法名称
pointcut:⽤于指定切⼊点表达式
pointcut-ref:⽤于指定切⼊点表达式的引⽤
异常通知不仅可以获取切⼊点⽅法执⾏的参数,也可以获取切⼊点⽅法执⾏产⽣的异常信息
- 最终通知
aop:after
只能出现在aop:aspect标签内部
method:⽤于指定最终通知的⽅法名称
pointcut:⽤于指定切⼊点表达式
pointcut-ref:⽤于指定切⼊点表达式的引⽤
最终通知执⾏时,可以获取到通知⽅法的参数。同时它可以做⼀些清理操作
- 环绕通知
aop:around
只能出现在aop:aspect标签的内部
method:⽤于指定环绕通知的⽅法名称
pointcut:⽤于指定切⼊点表达式
pointcut-ref:⽤于指定切⼊点表达式的引⽤
【特别说明】
它是有别于前⾯四种通知类型外的特殊通知,前四种(前置,后置,异常和最终)它们都是指定何时增强的通知类型。
⽽环绕通知,它是Spring框架为我们提供的⼀种可以通过编码的 ⽅式,控制增强代码何时执⾏的通知类型
。它⾥⾯借助的ProceedingJoinPoint接⼝及其实现类,
实现⼿动触发切⼊点⽅法的调⽤
6.5 声明式事务的支持
带跟进
七、 Spring AOP 源码剖析
待跟进