思来想去 决定下决心 写一篇博客 特别长的那种 一边DEBUG 一边总结 不能在犹豫了 观众老爷觉得哪里不对 赶紧提出,博主要贯穿spring 整个流程 说白了 就是你的ssm 工程 开始启动 到你的启动完毕的过程中发生了什么 不废话 开搞 争取每日更新
本篇基于SSM工程 进行调试 spring 版本 5.1.4.RELEASE 不算太老
第一步 你要找到监听类 这个监听类 就是在web.xml 中配置的
这个类就是我们要找的监听类 如下图 第一步先初始化 应用到web环境的上下文
进入方法 (只截取有用的部分)具体方法请观众老爷自行查看
我们来看一下 这个context的类型 是什么
他是继承于 ApplicationContext 这个接口的 WebApplicationContext接口的作用 至于ApplicationContext 暂且放一放
Interface to provide configuration for a web application.
下一步该创建上下文了
首先 要决定一件事情 创建web环境上下文必然需要子类 这个子类是有默认 和 可配置的两种方法
这个方法会到你的web.xml 里去寻找关于contextClass 配置
笔者这边简单配置了一个
以上是我自定义配置的一个类 用它作为web上下文的实例
当然 你可以用默认的实现类 spring 帮你配置好了
但是在查看ContextLoader.properties 之后 你会发现 它的默认实现类就是
org.springframework.web.context.WebApplicationContext =
org.springframework.web.context.support.XmlWebApplicationContext
没错 我自定义的类就是这个默认类的子类 这里有个默认规则 往下看
熟悉的父子关系判断问题 即 自定义的这个类 需要是 ConfigurableWebApplicationContext 这个接口的实现
笔者偷懒 哈哈哈 继承了默认类
下一步初始化 实例
这个时候就到了我自己的定义类 上了 中间的过程 就是进行简单的判断一下 这个自定义的类不应该是interface 然后通过构造方法初始化 我自己的就不拿出来了
直接看默认类就行
看到没 默认的上下文配置位置 换句话说 如果你不配置这个在web.xml(看下图)
它会默认寻找这个文件 只要你工程里这个文件放到了指定位置 你不配置也可以
我为什么这么肯定呢 看一下方法就知道了 都在这了 上面的自定义路径 下面的是默认的 所以 你不配置也没关系
该类还有其他方法 等到用的时候再解析
下面来看 AbstractRefreshableWebApplicationContext 这个抽象类
这个方法设置的就是个展示的名字 继续往父类走
这个抽象类 有点意思 看 这个 getDefaultConfigLocations() 方法是不是在哪见过 啊哈哈 对就是 默认实现类中的那个方法
但是我现在有个问题 这个configLocations 在哪来 观众老爷就会说你不在web.xml配置的吗???是 我是配了 但是如何赋值的呢
接下来我先把 赋值的这个方法 先拿出来
看到了啥 这是个数组 蛤蛤蛤 说明你可以配置多个 CONFIG_LOCATION_DELIMITERS就是分割条件 具体笔者也没试过
没那么多花式操作。。。这时候你会发现 configLocations 数组为空 不着急 继续找父类
AbstractRefreshableApplicationContext类
这有两个属性 第一个 是 允许BeanDefintion 被复写 第二个 是允许循环引用 这两个属性比较重要
这里先提一句 待我整理好了 补上说明
当然 这个类还有一个重磅的方法 refresh 方法 当然别的方法也很重要 不止一个(蛤蛤蛤蛤) 现在先不讲 先梳理父子关系
这个抽象类中有几个属性
这三个是分别是 应用于国际化的beanName名称(这是个固定的配置 就是说你如果要声明 一个国际化的bean 时 只能用这个name)
让我们看一下第二个
这里转载了大佬的博客 https://www.jianshu.com/p/43b65ed2e166 针对lifecycle进行讲解 大家可以仔细看看
第三个 是 事件发布器
另一个大佬 https://blog.csdn.net/u013905744/article/details/93643487 这个大佬总结的有点乱 不过是贯穿整个流程的
下面继续寻找父类 DefaultResourceLoader
DefaultResourceLoader 继承的接口 是 ResourceLoader
看到没 这个地方有个变量是前缀 就是常写的classpath:
那 观众老爷可能会问 classpath*: 呢 在 ResourceLoader 的子接口 ResourcePatternResolver
到目前为止 实现类->父类 整理完了 可能有一些漏的 大家积极补充
现在 从父类->实现类啦~~~~~
注意 AbstractApplicationContext 这个类
它的构造方法 中创建了 ResourcePatternResolver的变量 这个接口是干嘛的呢?
这个接口主要用于 通过模式匹配去获取你的配置文件(当然这个配置文件已经被封装成了Resource了)
然后狂摁F7回到ContextLoader类中
判断状态 active 是 默认值是false 下一步设置 父上下文 说白了就是有了层级 只可惜 没有parent 默认返回null
然后进入下一环节
本文只截取重要部分 进入该方法之后还设置了 这个cwac 上下文的id 大家自行查看 简单掠过 呼
看 下图 先为web应用程序上下文设置ServletContext 而后 读取 web.xml 配置 真相了 观众老爷 这个就是值的来历
拖了将近一周的时间 没办法 博主被疾病困扰 放心哈 不是 新冠肺炎 不扯了 开搞
现在我要初始化 configLocation 这个数组了 之前代码已贴
现在看 resolvePath 这个方法
首先需要环境 在类AbstractApplicationContext中
这个环境就是上下文的环境 如下图 初始默认为null 所以需要创建环境
类 AbstractRefreshableWebApplicationContext
这时候关注的点 就到了 StandardServletEnvironment
这个类有三个属性 需要关注
SERVLET_CONTEXT_PROPERTY_SOURCE_NAME SERVLET_CONFIG_PROPERTY_SOURCE_NAME JNDI_PROPERTY_SOURCE_NAME
这三个 是代表Property Source的名字 所谓PropertySource 是干什么的呢?
Abstract base class representing a source of name/value property pairs
一个表示(属性名=> 属性值)对的源的抽象基类 什么叫 对的源 博主的理解 是这样的 首先 属性 需要有名字 需要有值
这就表示 这本身就是一对 (你要是再不懂 说白了 就是 key/value) 源是 数据的来源 因为它是一个存储键值对的一个抽象基类
它本身就是封装了 每一个属性 注意 从英语语法的角度 这是个单数 PropertySource 那么 我那么多属性 我怎么办呢?
PropertySources 出现了 真相了 如下图 持有保存 一个或多个PropertySource 对象 但是问题是 我没有在 该类 找到 什么添加操作啊 这是怎么回事 当然 这个问题交给子类了嘛
作为 PropertySources唯一的子类 它有两个构造方法 一个是无参构造 另一个就是 有参构造 见下图
这里小小说明下 PropertySources 继承了 Iterable
这时候你就可以对他进行遍历了嘛 就像这样 list.forEach((s)->System.out.println(s)); 为了排版 只能用lambda 表达式。。。
注意 addLast 方法 它操作的起始是一个 list 这个list 就是这个类唯一一个属性
为什么要用 CopyOnWriteArrayList 呢 ??? 首先了解 这个实现类干什么的 用于什么场景 是不是线程安全 (这个很重要)
博主需要细致的讲 也是给自己总结 大家如果觉得那块不对 抓紧指出 博主18年毕业 工作不到两年 有些地方需要大家指正 (谢谢大家)
我们来看一下 这个 CopyOnWriteArrayList 类 先不去管 它的继承关系 单看 属性 就够你喝一壶的
首先 ReentrantLock是什么 能干什么 为什么要用
ReentrantLock 是实现Lock 接口的类 Lock 顾名思义 锁 重点来了 这个东西是不是在多线程中对临界区进行读/写时可以用到的
首先锁 有公平锁 非公平锁 我们熟知的 synchronized 关键字 就是非公平锁 那为什么叫非公平锁 它的特点是什么?
这里先安利 Java多线程编程实战指南 这本书 干货满满
首先 ReentrantLock 中 有个 自定义类的属性 Sync 这个类 继承了 AbstractQueuedSynchronizer 那么重点来了 这个类干嘛的
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.
提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。
但是博主阅读注释的时候 看到了这么一句话 就上网搜了一下 这一搜 不得了
The wait queue is a variant of a "CLH" 就是这句话 翻译过来就是 等待队列 是CLH 锁的变体 ???? 变体 。。。
什么是CLH 锁 在看这个之前呢 需要先了解 自旋锁 所以 博主有点抗不住了 这块我得学习几天 。。。这块先不讲 容易误人子弟
耻辱线 唉
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
那我们先跳回来 ReentrantLock 中 Sync 这个类有两个实现类 一个是 NonfairSync 另一个是 FairSync
啥意思呢 ? 一个是非公平锁 一个是公平锁 默认 ReentrantLock 是非公平锁 取决于你的构造方法啦
那非公平锁特点是啥
白话讲 就是 你出去买饭排队 等了好长时间 这时候你看见 窗口有个人没有排队 直接把饭买走了 而你呢 只能等
这就是不公平的 你就是在等待队列里等待唤醒的线程 而没有排队的那个呢 是个Ready 状态下的活跃线程 当你被notify的时候
你发现活跃线程把你的买早饭的权利抢了 ******** 这时候你不得不再次进入等待队列 这就产生了上下文切换 这是非公平锁的缺点啦 但是非公平锁单位时间并发程度会比公平锁高 只不过可能产生的代价也会更大 也可能 等待队列中的线程 出现了线程饥饿的情况 永远不能被调用 。好了特点说了大概。我们来看一下获得锁的具体代码吧
先分析 非公平锁 的lock 方法
首先调用了一个CAS操作 首先说一下 0 代表这个锁当前时刻没有被任何线程持有 注意 当前时刻 不能说它是一直没有被持有
这样说比较严谨 1 就是这个线程成功的获取到了锁 下一步 如下图 在抽象类 AbstractOwnableSynchronizer 中
这是干什么的?设置当前拥有独占访问权限的线程 你设置null 就是没有啦 此方法不强制任何同步或Volatile字段访问。
没办法 百度翻译 我真的是,,, 最主要的是 设置独占访问权限的线程 就是这个锁 只能由 该线程访问
这在 AbstractQueuedSynchronizer 类中 注释上已经说明了 exclusive 是 独立的 shared 是共享的
当在exclusive 模式获取锁成功时 其他企图获取锁的线程不会成功 然而 shared mode 可能会成功 两种模式下的等待线程共享同一队列
就是请求锁的等待队列啦
这时候回来看 else 部分 acquire 方法 看到 Node.EXCLUSIVE了吗? 没错 该方法仍然是已这个模式获取 忽略中断
通过至少调用一次tryAcquire 方法 如果成功 就返回 否则进入队列 那我们先看下 tryAcquire 干了啥!!!
我们来关注一下 nonfairTryAcquire 这个方法 先看上半部分
获取 当前线程 获取当前锁的状态 如果 此刻没有线程持有 就 调用CAS操作 还是那一套 设置当前的线程为唯一一个有访问权限的线程
这部分 说明了 当前锁已经有线程持有 而且访问的线程 恰好就是那个唯一一个有权限的线程 注意 下面一步很重要
为什么 有+的操作 ??? 哈哈哈 这是因为 ReentrantLock 是个可重入锁 就是 可以重复获取锁 当你获取锁的时候 再次获取
先给可重入锁留个位置 后期 仔细讲讲 -------------------------------
只需要改变state 就完成了获取锁的操作 最后 如果上述都失败了 返回false 直接进入下个方法 进入队列
这个时候 需要先分析 addWaiter 方法 上图已贴 自己找
注意这句话 为当前线程所给定模式创建并入队节点
首先先创建 队列中的一个结点 因为队列是要从队尾进 首先要有个辅助指针指向队尾 如果队尾不为空 (这里插一句)
|--------如果队列的tail 是空 其实这个队列是不存在的 因为刚开始 队列的head和队列的tail指向同一节点-------|
如果队列tail 不为空 新插入节点的前继节点指向队尾 下一步 进行CAS 操作 交换队尾 使原来的队尾 变成 新插入节点
如果成功 原来的tail 的后继节点指向 新插入节点 其实就是个双端链表 就跟LinkedList 差不多
如果失败 现在只有新插入节点的前继节点指向原来的队尾 所以它进行了入队操作 原理是一样的 就是我上述讲的这些
但是为什么要先要用CAS 操作呢 因为在你队列不为空的情况下 恰好这个时候tail 没有变化 你就可以直接入队 这样会
很快速的完成这件事 当然了 这种碰运气的事 看命被!!!
下面我们来看一下 入队这个操作 首先 入队的代码调用看上图 分两种情况 一个是 tail为空(队列不存在) 另一个是cas操作失败
但是这块不一样的是 它是被无限循环包围的一段代码 为什么这么写呢 这时候需要搬出自旋锁的概念了
我们首先要知道 他跟普通的锁有什么共同点 有什么不同点
共同点 每个线程申请锁资源的时候 最多有一个线程持有 不同的是 在并发的情况下 A 线程 获得了 互斥锁 (代号 C)
B线程 想要获取C 结果发现 A线程已经抢先了 B线程就进入等待队列 但是自旋锁 不一样 我有耐心 你不是抢走了C吗?
我不进入等待队列 我还是等着C 每次我都调用CAS 操作 看你到底释放锁没有 如果释放了 哈哈哈 我不就得到了C
但是自旋锁有弊端啊 作为一个线程 要懂得能屈能伸 不能一棵树上吊死 检查一段时间后 发现资源还没被释放 不要了
哈哈哈 待会再来看看 不是永远不来了 自旋锁 适合那种锁资源持有时间短的 太长了 CPU 抗不住 那不就无限循环了吗
我还干不干活。。。吐槽ubuntu的自带输入法 太不智能了。。。。
好 回到主题 我们看一下 首先队列不存在 必须初始化啊 人家都说了 Must initialize 这样队头 队尾 不就在一起了吗? 嘿嘿
然后如果队列不为空 那就入队呗 从队尾插入 结束
下面我们来分析一下 刚开始我以为讲完了 发现还有,,,
这个方法是返回前继节点 如果为null 抛出异常
接下来看代码
我的node 入队之后 node前继节点是head(头节点) 这时候程序 会重新尝试获取lock 如果成功了 node 充当head 节点
因为head节点是不存值的 他就是一个New node() 属性都是空
下面看 shouldParkAfterFailedAcquire方法
先不写了 jdk 源码看得脑袋晕晕的 可能是我口罩带的太闷了 哈哈 晚上继续更
waitStatus 分为 SIGNAL 、CANCELLED 、CONDITION 、PROPAGATE 、0
先讲一下 通过 节点的状态 首先 ws == Node.SIGNAL SIGNAL 代表 当前节点(pred) 的后续节点(即node) 被阻塞 当前节点(pred)被释放或被取消时 node节点代表的线程将被唤醒
注释也说了 这个node 节点 设置状态,要求释放以发出信号,以便可以安全地挂起。
这几个状态中 只有 CANCELLED 的值大于0 注意代码
node.prev = pred = pred.prev; 这段代码直接 把pred的节点 删除了 并继续找 是否还有 被取消的线程 如果有 继续删除
剩下的状态就剩 0 和 PROPAGATE 表示我们需要信号,但不要挂起 调用方需要重试以确保在挂起前无法获取锁资源。
下面分析 parkAndCheckInterrupt 这个方法就是挂起线程 然后确认线程是否被中断
下一步 如果条件成立 线程被成功 中断
最后 failed == true 调用 cancelAcquire 这个方法的作用是 取消正在进行的获取锁的尝试
这个是 节点不存在 的情况 直接忽略
因为线程被取消所以 thread = null 删除 线程被取消的节点都会被删除
这块先做个标记 缓缓在讲 (现在这块我有点晕)---------------------
这块就是 线程中断 就是线程没有获取到锁 进入队列 改变状态
到现在非公平锁 的独占模式 的lock方法 大概有了了解 (MMP 这东西真搞脑子)
谈到 lock 当然还有释放锁 呜呜呜 又开始恶魔之旅了
那么先关注一下 tryRelease 方法 因为 ReentrantLock 是可重入锁 所以你很难保证释放操作执行完成后 就真的被释放 可能需要好几次才能释放成功 起始就是更新state 如果state = 0 就是 成功被释放了
冤家路窄 来到了 unparkSuccessor方法
不行了 锁先停一下 有点不太懂
锁这块写的的确是虎头蛇尾 。。。。 先画个耻辱线-------------------------------------------------
上面讲到 StandardServletEnvironment 接着解析 父类 StandardEnvironment
这两个属性 由注释可以看出 这是关于系统的PropertySource
继续看父类 AbstractEnvironment
第一个属性 表示spring 忽略系统变量 的系统属性 不会尝试通过 System.getenv() 获取值 这个配置默认为false
返回到系统环境变量检查Spring环境属性(例如,配置字符串中的占位符)是否不可解析
第二个属性 用来设置当前生效的配置环境 dev/test/product
第三个属性 用来设置默认的配置环境
第四个属性 这个是如果上述的没有配置默认的配置环境 也没有配置当前生效的配置环境 则该配置生效
下面看一下这个属性 propertyResolver
那我们就来关注一下 ConfigurablePropertyResolver这个接口是干什么的
Provides facilities for accessing and customizing the ConversionService used when converting property values from one type to another.
即提供访问和自定义将属性值从一种类型转换为另一种类型时使用的转换服务的工具。
所以我们来看一下这个接口的方法
返回对属性执行类型转换时使用的可配置转换服务 返回的转换服务的可配置性允许方便的添加或删除单个转换器实例
ConfigurableConversionService getConversionService();
设置转换服务
void setConversionService(ConfigurableConversionService conversionService);
自定义设置占位符的前缀 默认是${
void setPlaceholderPrefix(String placeholderPrefix);
自定义设置占位符的后缀 默认是}
void setPlaceholderSuffix(String placeholderSuffix);
那我们看一下 ConfigurableConversionService 这个接口
它实现了俩个接口 一个是 ConversionService 另一个 ConverterRegistry
先看ConversionService 接口中的方法
如果sourceType 能转换成targetType 的话 return true 如果为true 那就意味着 convert 方法可以成功的把 source 转换为targetType类型
同理 如上所述
从起名字的语义就能分辨出来
下面看一下 ConverterRegistry接口
For registering converters with a type conversion system
即这个类是提供转换器注册的类 先知道这是干嘛的
添加转换器 转换器的sourceType targetType自定义 靠你自己实现
添加转换器 转换器的sourceType targetType 作为参数指定 指定转换器 S->T
添加转换器
添加转换器工厂
移除所有的转换器(指由S->T的)
回到 AbstractEnvironment 类中
一共添加了 5种属性源 上图的三种 通过调用 super.customizePropertySources 又添加了两个属性源 类 StandardEnvironment
先写到这 我先看会代码
请各位观众老爷持续关注!!! 连载中~~~~~