拉勾教育学习-笔记分享の"斩杀"框架篇(Spring)

1,136 阅读12分钟

【文章内容输出来源:拉勾教育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一样,支持三种方式:

  1. 纯XML
  2. XML+注解
  3. 纯注解

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】- 核心配置
  1. 通知Bean交给Spring管理
  2. 使用aop:config开启aop的配置
  3. 使用aop:aspect配置切面
  4. 使用对应的标签配置 通知的类型
<!--
 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 源码剖析

待跟进