链表转化为红黑树的阈值是8,红黑树转化为链表的阈值是6,负载因子0.75
树形化最小hash表元素个数是64,即如果桶内元素已经达到转化红黑树阈值,但是表元素总数未达到阈值,则值进行扩容,不进行树形化
当hash表元素小于树形化最小hash表元素个数时,但是桶内元素个数大于链表转化为红黑树的阈值时,进行扩容。而当hash中元素个数大于MIN_TREEIFY_CAPACITY时,则进行树形化
遍历:结果一定有序,因为是在数组从前往后顺序遍历
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
如果key为null,就返回0;否则返回key的hashCode与它的无符号右移16位后的值做异或
从桶节点开始遍历,根据链表的顺序把树拆分为两条链表,判断如果链表的长度<=6,则把这个链表红黑树转化为链表;>6,如果链表拆分后仍然只有一根链表,那么结构不变,否则把链表转为红黑树。(红黑树拆分为两个链表之后,原来的树结构被打乱,所以需要再转红黑树或者链表)
5)node为链表,遍历链表,构造两个新链表,一个位置为老node数组的位置,一个为老node数组位置+老node数组长度;重新映射后,两条链表中的节点顺序并未发生变化,还是保持了扩容前的顺序
考察的点:
1)HashMap中的键值可以为Null吗?可以,当计算key的hashcode时,如果为null,直接返回0
2)hashmap的死锁时怎么回事?resize的时候,会transfer元素,一开始1->2,线程1移完之后,变为2->1,线程b移动时,也是移动成2->1,但是由于线程1使2.next=1,导致判断2.next不为空,要继续移1,这样就出现1->2->1的循环链表了
3)红黑树比较的是key的hashcode,哈希碰撞发生条件:两个key的hashcode不一定相同,只要他们的hashcode模上数组得到的数组中的位置相同即可
4)扩容机制要能复述:红黑树扩容后如果数量<=6还是需要转链表的,节点类型会转回node
底层;hashmap+双向链表保持插入顺序,内部保存了两个指针head和tail,可以保存插入顺序或者访问顺序
逻辑跟haspmap的插入逻辑大致相同,除了有几点:
1)新建node时,linkedhashmap覆盖了hashmap的newnode方法,增加了把新节点放入双向链表的逻辑:
置tail为当前节点,如果原链表为空,则置头节点为当前节点;否则设置原尾节点的next及该节点的before
2)插入如果是覆盖,会调用查询回调,如果是加了新节点,会调用插入回调
插入回调:即在新增节点时把entry放入双向链表的尾节点,访问方法不会改变顺序;
访问回调:在插入元素和访问元素时,都会把当前元素放入双向链表的末尾。所以其实最后访问或者放入的也是最后遍历
1) map.entry继承体系分析:map.entry->hashMap.node(key value hash next)->linkedHashMap.entry(key value hash next before after保持顺序的双向链表的两个指针)->hashMap.treeNode(key value hash next before after parent left right red颜色 pre这个指针主要是为了在树形变换的时候用的)
可以发现hashmap的treenode继承自linkedhashmap.entry,会浪费两个指针before,after的空间,为啥要浪费呢?源码注释有解释,如果我们的hash算法选用得当并且hashmap够大,则树形结构出现概率是很小的,因此这种浪费是可接受的
2)可以用作LRU缓存
底层就是一个红黑树,他的节点entry为 (key,value,left,right,parent,color),保存了一个根节点属性作为红黑树的入口
treemap的key需要实现comparator接口,也就是可比较
底层也是一个entry数组,entry为(hash,key,value,next),采用数组+链表
初始容量为11,负载因子为0.75
hashtable的value不允许为空,不然会报空指针。
重hash(扩容):新的容量为旧值的2倍加1
hashtable线程安全,对每个方法都加上了synchronized关键字
可重入锁
内部类Sync继承了AQS,NonfairSync和FairSync继承了Sync.
acquire方法:如果锁状态是0,cas加锁;否则判断锁的拥有者是否是当前线程,是的话就重入。剩余逻辑参照AQSDACQUIRE
aqs无所谓公平模式和非公平模式,它只是让两个等待的节点组成了双向队列。
公平模式,线程acquire资源时,会判断队列必须没有前驱阶段,也就是必须(aqs的双向链表队列的头节点)是head节点才会抢锁,fifo的顺序
非公平模式则不会有这个判断,只要acquire就会抢
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义
(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
(2)禁止进行指令重排序
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效
运算的结果并不依赖于变量的当前值,或者能够确保只有单一的线程修改变量的值
在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问;另外,每个类也会有一个锁,它可以用来控制对static数据成员的并发访问
(1)同步语句块:实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束
(2)方法:synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法,该标识指明了该方法是一个同步方法,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor
有两种锁,对象锁和类锁;每个对象有一个监视器锁(monitor)
(3)l静态方法:一个类只有一个class对象,Class对象其实也仅仅是1个java对象,因为每一个java对象都有1个互斥锁,而类的静态方法是须要Class对象。因此所谓的类锁,只不过是Class对象的锁而已
notify/notifyAll和wait方法必须处于synchronized代码块或者synchronized方法中,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被阻塞,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁
申请内存之后内存不能回收叫内存泄漏,申请内存时内存不够叫内存溢出
自旋锁
自适应自旋锁
轻量级锁
偏向锁
重量级锁
上述这三种机制的切换是根据竞争激烈程度进行的:
在几乎无竞争的条件下, 会使用偏向锁
在轻度竞争的条件下, 会由偏向锁升级为轻量级锁
在重度竞争的情况下, 会由轻量级锁升级为重量级锁
参考:1.7和1.8的区别总结
ConcurrentHashMap是HashMap的线程安全版本;
查询操作是不会加锁的,所以ConcurrentHashMap不是强一致性的;
写时复制,是ArrayList的线程安全版本,适用于读多写少
每次对数组的修改都完全拷贝一份新的数组来修改,修改完了再替换掉老数组,这样保证了只阻塞写操作,不阻塞读操作,实现读写分离。
底层包含一个volatile数组和一个reentrantlock,新增修改删除需要加锁,然后拷贝一份新的数组来修改,修改完再替换老数组,解锁;读的时候直接读数组不需要加锁;这样保证了只阻塞写操作,不阻塞读操作,实现读写分离。如果这时候有多个线程正在修改数据,则读取的是旧的数据。
缺点:内存占用,在写操作的时候,内存里会同时存在两个对象的内存;只能保证数据的最终一致性,不能保证实时一致性
底层是一个CopyOnWriteArrayList,只是不允许元素重复,调用的是CopyOnWriteArrayList的方法,如果元素已存在则不添加,元素不存在则添加
通过调用CopyOnWriteArrayList的addIfAbsent()方法来保证元素不重复
计数器 只能使用一次,利用它可以实现类似计数器的功能
内部类Sync继承了AQS
await():内部会调用方法tryAcquireShared(1),逻辑就是如果state=0,就返回成功,否则等待。
countDown();自旋并使state-1,如果state=0,那么通过循环唤醒AQS上面的等待队列
CountDownLatch初始化时会传入共享资源state的值,线程A调用await(),则会等待state=0这个条件;而线程B1、B2、B3使state自减,当state=0时,会唤醒等待的线程A
CountDownLatch与Thread.join()有何不同?Thread.join()是在主线程中调用的,它只能等待被调用的线程结束了才会通知主线程,而CountDownLatch则不同,它的countDown()方法可以在线程执行的任意时刻调用,灵活性更大
信号量 Semaphore可以控同时访问的线程个数即限流量,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可
内部类Sync继承了AQS,NonfairSync和FairSync分别提供非公平和公平策略。volatile state保存了当前资源状态
acquire方法:还是走AQS的框架,如果获取到锁,返回,获取不到,排队。获取锁的逻辑由具体的实现类来实现;即先获取剩余资源,如果资源数-需求数>0,成功返回,并且cas设置state,否则不停的循环
循环栅栏 可以重复使用,通过它可以实现让一组线程等待至某个状态之后再全部同时执行
内部有一个可重入锁reentrantlock,它保存了一个count,每次有线程await,都会自减,直到为0就通知signalall
CyclicBarrier基于ReentrantLock及其Condition实现整个同步逻辑
await():获取可重入锁,如果当前线程是最后一个线程,那么在这个线程中执行传入的runnable方法,并重置屏障使其可以循环使用,同时调用condition.singalall方法唤醒所有等待线程;若不是最后一个线程,则调用条件对象condition.await让它等待。最后解锁。
CyclicBarrier与CountDownLatch的对比?
前者是最后一个线程到达时自动唤醒,后者是通过显式地调用countDown()实现;
前者是通过重入锁及其条件锁实现的,后者是直接基于AQS实现的;
前者具有“代”的概念,可以重复使用,后者只能使用一次;
交换器
线程间交换数据,一般用于两个线程之间交换或者偶数个线程之间交换、互相消费。
v exchange(v):交换出去v类型数据,并返回v类型数据
在reentrantlock中使用condition时,存在两类同步队列,一个归属于reentrantlock(在aqs里,用来争用reentrantlock),一类归属于condition(用来等待条件)
线程的生命周期:
NEW 新建状态,线程还未开始
RUNNABLE 可运行状态,正在运行或者在等待系统资源
BLOCKED 阻塞状态,在等待一个监视器锁(也就是我们常说的synchronized)
WAITING 等待状态
TIMED_WAITING 超时等待状态,在调用了以下方法后会进入超时等待状态,如Thread.sleep() Object.wait(timeout)
TERMINATED 终止状态
RPC框架中异步调用是怎么实现的?
答:RPC框架常用的调用方式有同步调用、异步调用,其实它们本质上都是异步调用,它们就是用FutureTask的方式来实现的。
一般地,通过一个线程(我们叫作远程线程)去调用远程接口,如果是同步调用,则直接让调用者线程阻塞着等待远程线程调用的结果,待结果返回了再返回;如果是异步调用,则先返回一个未来可以获取到远程结果的东西FutureXxx,当然,如果这个FutureXxx在远程结果返回之前调用了get()方法一样会阻塞着调用者线程。
有兴趣的同学可以先去预习一下dubbo的异步调用(它是把Future扔到RpcContext中的)
springmvc
配置文件解析:首先根据工具类resources加载配置文件,得到输入流,根据输入流生成配置文件解析器,调用各个节点解析方法,把解析到的属性和值设置到configuration。其中,解析mapper节点会定位到mapper.xml文件,然后解析mapper文件,并存到configuration,这样映射文件就解析完了
springboot
在启动时会加载所有spring.facotrys中指定的类;扫描所有mapper,并得到bean定义的集合;重置了beanClass为代理类 MapperFactoryBean,个mapper都变成了MapperFactoryBean,为了区分他们,MapperFactoryBean的构造函数参数,传入了mapper接口的名字;
第一步判断configiration的mapper集合中必须包含目标目标Mapper,然后通过jdk的动态代理创建mapper对象,调用代理对象的sql方法就可以操作数据库了。动态代理具体添加了什么操作呢?如果方法是object对象方法,那么直接通过反射调用,如果是sql方法,先从缓存map里获取mappermethod对象,然后调用它的execute方法获取返回结果(底层也是调用statement执行sql)。execute方法:判断方法类型,根据不同的方法类型执行不同的方法
聚簇索引,索引存储的结构与数据存储的物理结构是一样的,因为物理顺序结构只有一种,那么一个表的聚簇索引也只有一个,通常是主键,设置主键系统默认就给你加上了聚簇索引。
非聚簇索引,记录的物理顺序与逻辑顺序没有必然的联系,与数据的存储物理结构没有关系;一个表对应的非聚簇索引可以有多条,根据不同列的约束可以建立不同要求的非聚簇索引
单列索引,包含主键索引,唯一索引
联合索引,包含多个字段,遵循最左前缀原则,只有在使用左边字段查询时才会被使用。
当创建(a,b,c)联合索引时,相当于创建了(a)单列索引,(a,b)联合索引以及(a,b,c)联合索引
想要索引生效的话,只能使用 a和a,b和a,b,c三种组合;当然,我们上面测试过,a,c组合也可以,但实际上只用到了a的索引,c并没有用到
数据库查询数据时,需要将需要查询的页数据加载到内存,每次加载需要一次磁盘io,磁盘io的速度很慢,如果使用平衡二叉树,最坏io的次数是树的高度,io频繁。因此将瘦高的树变矮胖,两点:每个节点存储更多元素,采用多叉树
b树和b+树区别
1)B+树叶子节点保存索引和数据,非叶子节点保存索引,查询时间复杂度固定为 log n;b树的内节点和叶子节点都保存索引和数据,查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)
2)B+树叶子节点是有序链表,大大增加区间访问性,可使用范围查询等,而B-树每个节点 key 和 data 在一起,则无法范围查找;
3)由于B+树的叶子节点的数据都是使用链表连接起来的,而且他们在磁盘里是顺序存储的,所以当读到某个值的时候,磁盘预读原理就会提前把这些数据都读进内存,使得范围查询和排序都很快
读未提交,读已提交(解决脏读),可重复读(解决不可重复读),可串行化(解决幻读)
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
共享锁:select xxx from xxx in share mode,共享锁在多个事务可以共享
排他锁:select xxx from xxx for update,排他锁获取之前必须等其他事务的共享锁释放
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
RR下查出的记录满足两个条件:(1)删除版本号未指定或者大于当前事务版本号 (2)创建版本号 小于或者等于 当前事务版本号
对于快照读,依赖mvcc机制;对于当前读,依赖于间隙锁
RR隔离级别已经解决了幻读,依靠(1)MVCC(2)间隙锁,(高性能mysql中已经提及了)
单一查询:
事务一:
select * from news where year=4 for update
要想锁定year为4的记录,那么不仅需要锁定当前year=4的记录,还需要锁定year=4前后的间隙。试想如果仅仅锁定(3,4),那么如果再插入一条(2,4)的纪录,就出现幻读了
事务二:
insert into news values(0,2);//成功
insert into news values(2,4);//阻塞
insert into news values(2,2);//阻塞
insert into news values(4,5);//阻塞
insert into news values(7,5);//成功
update news set id=4 where id=10;//阻塞
update news set id=12 where id=6;//成功!!!
间隙锁锁定的区域:
根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。where year=4的话,那么间隙锁的区间范围为(1,2)到(6,5),这两个节点还不包含的
间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
(1)防止间隙内有新数据被插入
(2)防止已存在的数据,更新成间隙内的数据(例如防止numer=3的记录通过update变成number=5)
范围查询:
事务一:
select * from news where year>4 for update
根据条件向左取4,向右为无穷大,因此间隙锁的范围为(3,4)到无穷大
事务二:
update news set id=4 where year=4;//阻塞
insert into news values(4,4);//阻塞
update news set id=3 where year=4;//成功!!说明是开区间
单一查询:
事务一:
select * from news where id=6 for update;
事务二:
insert into news values(5,5);//成功
insert into news values(7,5);//成功
select * from news where year=5 for update;//阻塞
select * from news where year=5 for update;//阻塞
事务一:
select * from news where id>5 for update
唯一索引的范围查询,锁定行和间隙。根据条件向左取3,向右取无穷大,因此区间为3到无穷大
事务二:
insert into news values(6,1);//阻塞
insert into news values(4,1);//阻塞!!!!
空查询:
select * from news where id>4 and id<7 for update;此时会对表中第一个不满足条件的项上加上gap锁,防止后续插入满足条件的记录。此时加锁的范围是((3,4),(8,5)]
对于如上的两个限定范围的查询,mysql只会将id>4这个正向扫描的条件下降到mysql的引擎层,因此innoDb看到的索引条件是id>4,读出的第一条记录是id=8,给(8,5)加上next key锁,然后返回给mysql server层进行id<7的判断
gap锁之间是不冲突的,但是两个事务同时执行上述sql会冲突,就是因为加了next key锁即行锁的缘故
1.选取最适用的字段属性
如邮政编码char(6)就不要写成vchar(255)
not null能加就加,这样可以省去是否等于null的判断
2.使用join代替子查询
因为使用子查询的话,mysql需要在内存中创建临时表来存这个子查询的结果
3.使用联合(UNION)来代替手动创建的临时表
4.使用外键
5.使用索引
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by涉及的列上建立索引
2.尽量避免在 where 子句中使用 !=或<> 操作符,否则将引擎放弃使用索引而进行全表扫描
3.尽量避免在 where 子句中对字段进行 null 值 判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
5.对于 like ‘…%’ (不以 % 开头)
6.尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
select id from t where num/2=100
7.在使用索引字段作为条件时,如果该索引是【复合索引】,那么必须使用到该索引中的【第一个字段】作为条件时才能保证系统使用该索引,否则该索引将不会被使用。并且应【尽可能】的让字段顺序与索引顺序相一致
mysql查询分为引擎层和server层,
ICP的使用限制
ICP的优化在引擎层就能够过滤掉大量的数据,这样无疑能够减少了对base table和mysql server的访问次数,提升了性能
ICP与锁
在RR隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。Index Key确定的范围,需要加上GAP锁;Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则不满足Index Filter的记录,不加X锁,否则需要X锁;Table Filter过滤条件,无论是否满足,都需要加X锁
参考:spring bean的生命周期
第4步,BeanPostProcessor前置处理,指的是调用Object postProcessBeforeInitialization(Object bean, String beanName)
第7步,BeanPostProcessor后置处理,指的是调用Object postProcessAfterInitialization(Object bean, String beanName)
第8步,Destruction指的是销毁时调用的回调方法
第2步中,设置对象的属性,如果是一个Bean对象依赖于另一个Bean对象,则Spring容器首先要先去实例化 bean 依赖的对象,实例化好后才能设置到当前 bean 中。如果是循环依赖,spring怎么初始化的呢?
如果依赖靠构造器的方式注入,则会报循环依赖异常;
如果是setter设置属性模式,并且是范围是单例,通过实例化后加缓存解决
如BeanA和BeanB循环依赖,A实例化后,将自己提前曝光加入到缓存中;此时开始设置属性,就开始创建B,实例化B后,设置B的属性,发现B依赖了A,就从提前曝光的缓存中拿到了A,顺利完成了属性设置,并顺利完成了后续的bean创建步骤,返回给A,接着A再继续自己的bean创建即可
入口是SpringApplication.run(Application.class, args)
1】new SpringApplication,并设置属性:从spring.factories配置文件中读取所有ApplicationContextInitializer,ApplicationListener的实现类并设置、设置mainApplicationClass等
2】开始执行run方法:
①加载spring.factories,发布application启动事件:读取并加载所有SpringApplicationRunListener(都是在spring.factories文件里定义的),并start它们,同时发布ApplicationStartingEvent事件,之前加载的ApplicationListener收到这个事件就会做出对应的操作,如设置log系统、配置environment、配置profile等。后续的发布的其他一些事件,也会执行监听器对应的方法,如环境准备好事件、application准备好事件、application启动失败事件等等
②环境准备:创建StandardEnvironment,发布环境准备事件。这里会加载一些系统环境变量、properties、yml文件配置等等
③创建容器ApplicationContext(简称Context,继承了beanFactory):通过反射的方式Class来newInstance容器
④准备容器Context:设置之前的Environment环境给Context、执行容器后置处理postProcessApplicationContext()、执行第一步加载的ApplicationContextInitializer中的初始化方法、发布Context准备事件等等,这里,springboot的容器就准备好了
⑤刷新容器【核心】:
a.初始化BeanFactory,并准备:如设置ClassLoader、设置Bean表达式解析器、Property解析器等准备工作
b.调用invokeBeanFactoryPostProcessors即BeanFactory后置处理方法
b1)从BeanFactory中获取实现了BeanFactoryPostProcessor接口的类,得到了ConfigurationClassPostProcessor类,然后触发它的方法postProcessBeanDefinitionRegistry(bean定义初始化后置处理方法):
b1a):遍历BeanFactory中的所有BeanDefinition,得到被@configuration注解的
BeanDefinition列表
b1b):对每个BeanDefinition解析,逻辑为:第一步解析成员,有的@configuration注解的类下面还有@Configuration子类,第二步,处理@PropertySources注解,将配置文件解析后存入(Stand)environment,如配置为redis.properties文件,第三步处理@ComponentScan注解,扫描路径下的所有class,判断这个类有@component注解【注意:@Configuration、@Service注解内部是包含@Component的】,得到Beandefition集合,对每个beandefinition,解析一下它的配置如scope、dependens on等等设置回beandefinition,并把beandefinition注册到BeanFactory(这里只是进行了注册,真正生成bean放在了d),第四步处理@Import注解,拿到注解上的引入类,设置到BeanDefinition中,第五步,处理@ImportResource注解,得到location信息设置到BeanDefinition中,第六步,处理@Bean注解,得到类中被@Bean修饰的所有方法集合,并设置到BeanDefinition中,第七步,处理接口的默认方法,有的话就设置到BeanDefinition中,第八步,如果就父类就把父类设置到BeanDefinition中,解析完成,所有设置好的BeanDefinition都保存到了解析类中
b2:[#aop1]调用invokeBeanDefinitionRegistryPostProcessors提供一个修改beanDefinition的机会
c.从BeanFactory中获取所有实现beanPostProcessors接口的类,并注册到BeanFactory中
d.调用finishBeanFactoryInitialization(),实例化所有已注册但还没创建bean的BeanDefinition,通常我们自己写的service、component都是在此注入到容器:
d1:循环处理BeanFactory(也就是容器Context)中的每一个BeanDefinition,检查是否实现了FactoryBean接口,如果是则将BeanName前面加上&,进行处理;否则先从缓存中获取对象,缓存中没有,把beanName放进alreadyCreated的set集合中,开始创建
d2:检查是否有DependsOn(控制bean加载顺序),有则对每一个依赖项,进行处理:先检查是否有循环依赖,然后注册依赖关系,然后创建依赖的bean
d3:处理lookup-method
d4:调用实例化前置方法resolveBeforeInstantiation,提供短路操作:遍历所有实现InstantiationAwareBeanPostProcessor接口的类,调用实例化前置方法postProcessBeforeInstantiation;如果得到了一个不为null的bean,则调用BeanPostProcessor.postProcessAfterInitialization初始化后置方法;最终得到的bean如果不为null,那么bean创建成功,直接返回d2
d5:创建一个BeanWrapper:此时实例化,通过反射创建一个实例,放进BeanWrapper
d6:判断bean是单例并且beanfactory允许循环依赖并且当前bean正在创建,那么就需要提早暴露这个单例bean,把它加入到注册单例set中
d7:填充属性:循环调用实例化后置方法InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation,接着循环调用InstantiationAwareBeanPostProcessor.postProcessProperties属性填充后置方法(通过反射对@Autowire、@Resource、@Value进行设置)和InstantiationAwareBeanPostProcessor.postProcessPropertyValues,接着从beanDefinition中获取所有属性值,注入到bean中
d8:开始初始化
d81:对实现Aware接口的类设置属性
d82:调用BeanPostProcessor.postProcessBeforeInitialization初始化前置方法。@PostConstruct是通过扩展这个方法来实现初始化动作
d83:如果实现了InitializingBean接口,那么执行它的afterPropertiesSet方法;
如果配置了自定义的InitMethod,就调用自定义的初始化方法
d84:调用BeanPostProcessor.postProcessAfterInitialization初始化后置方法。至此,bean创建结束
d9:把bean添加到beanFactory
⑥:创建一个TomcatServer并启动
@aspect的类都会加上@component注解注入spring,有这个注解就要创建bean.
1)[#aop1]这里会创建一个beanDefinition(InfrastructureAdvisorAutoProxyCreator)到beanfactory。它实现了BeanPostProcessor,在其他bean初始化后会调用它的postProcessAfterInitialization
2)会获取beanfactory中的所有的beanNames,根据beanname获取class,判断class上如果有aspect注解,那么解析这个class,获取所有没有被@pointcut注解的方法,循环解析这些方法,解析到aspectJAnnotation表达式,则包装成一个AspectJExpressionPointcut切点表达式切入点对象(即包含execution表达式的对象),最后再和当前class包装成Advisor对象.这样通过解析class的aspect方法得到了一个advisor的list集合.最后得到了所有的通知器advisors,并放入缓存。这样就完成了@aspect的类的解析和通知器的设置
上面并不会通过捷径创建代理类,对aop来说只是解析,还是要走正常的创建bean的流程。
3)在初始化bean的最后一步,会调用BeanPostProcessor.postProcessAfterInitialization初始化后置处理方法,这时会进入AbstractAutoProxyCreator.postProcessAfterInitialization方法内,对所有的bean判断,先获取所有advisor,(如果缓存有则拿缓存,没有则进入2)),如果bean中有方法能匹配到通知器advisors中的表达式[#springboot事物],那么开始创建代理类
4)先创建代理工厂AopProxy,此时会选择使用cglib还是jdk。
①是否启用优化optimize。由于 cglib的动态代理创建类比较慢,但是执行代码比较快,jdk动态代理相反,创建比较快,执行比较慢。如果配置了optimize=true,那么目标类实现了接口就使用jdk代理,否则就用cglib。默认是false。
②proxyTargetClass 是否强制使用cglib实现代理。默认是false
③目标接口是否是SpringProxy的子接口
上面三个条件如果都为false,则直接走jdk,否则需要判断,目标类是接口类或者目标类是SpringProxy的子接口,那么走jdk,否则走cglib。
5)创建完成开始获取代理类,然后注入到beanFactory中
jdk的getProxy:生成的代理类CglibAopProxy
cglib的getProxy:生成的代理类JdkDynamicAopProxy
springAop的调用逻辑:
当调用目标类的方法时,如果是cglib代理,代理类CglibAopProxy调用内部类DynamicAdvisedInterceptor的intercept代理方法,如果是jdk代理,代理类执行invoke代理方法
cglib的代理方法intercept():
①首先根据目标方法,获取匹配到的拦截器:
获取目标类相关的所有通知器,判断类和方法均能匹配上的话,就生成一个拦截器集合[#springboot事物调用][这里如果有事务也会获取到事务拦截器],这里spring事物的拦截器也是在这里获得
②如果拦截器数组是空并且方法是public,直接通过反射调用该方法;
否则,调用MethodInterceptor的proceed方法
③proceed方法:这里会取出拦截器数组的所有MethodInterceptor,然后执行他们的invoke方法[#springboot事物调用]
比如@AfterThrowing在调用结果异常时执行,逻辑就是try继续执行proceed方法向下传递,但是catch里回去异常然后反射调用业务方法.见AspectJAfterThrowingAdvice
@AfterReturning逻辑是先执行proceed方法,然后执行AfterReturn的业务逻辑,所以如果proceed抛异常,AfterReturn的业务逻辑不会执行
@After逻辑是try proceed方法,finnally里执行after逻辑。不管正常还是异常都会执行逻辑
@Around逻辑是生成一个JoinPoint对象,然后进行参数绑定JoinPoint,然后反射调用around逻辑,around逻辑里可以直接返回数据,也可以继续传递调用proceed方法
@before逻辑先绑定JoinPoint通过反射执行before逻辑,然后继续执行proceed
最后一次执行proceed方法时会发现拦截器链已执行完毕,就通过代理方法反射调用目标方法
所以业务逻辑先后顺序是@Around->@before->目标方法->@after->@afterreturn->@afterthrowing,其中@around提供了JoinPoint.proceed()方法让它自己选择是否继续proceed(),其他方法都只是单纯的业务方法,如果@around不执行proceed()自己返回一个对象,那么@Around->@after->@afterreturn->@afterthrowing,原因是拦截器链生成的时候是有序的,按照@afterthrowing->@afterreturn->@after->@Around->@before的调用顺序,如果@around不proceed,那么before和目标方法当然不执行
spring aop
[#springboot事物调用]
这里spring事务,则执行事务拦截器的TransactionInterceptor.invoke(),具体逻辑invokeWithinTransaction
1)首先获取事务的各种配置属性:解析方法上面的@transactional注解,因为注解上可以附带很多属性,就在这里解析,这样就得到了TransactionAttribute 对象
2)根据配置属性获取事务管理器,如果不配置会有一个default事务管理器
创建一个事务信息对象TransactionInfo,保存所有相关信息
3)开启事务,
4)通过反射调用目标方法(有人说这个地方和@around类似),
5)如果抛出异常,则catch 然后回滚事务,throw这个异常;
如何回滚completeTransactionAfterThrowing:
判断异常如果是 RuntimeException 类型异常或者 是 Error 类型的,就回滚。这就是默认的回滚策略。
为true,就执行事务管理器的rollback,首先关闭sqlsession对象,从数据库连接持有对象中获取数据库连接Connection,调用connection.rollback方法完成回滚。后面的sql底层。
6)finally释放事务相关资源
7)如果没有异常,提交事务,返回返回值
事务回滚提交,其实都是事务管理器里保存有jdbc的connection,通过这个connection来执行提交或回滚。
如何提交commitTransactionAfterReturning:从数据库连接持有对象中获取数据库连接Connection,调用connection.commit方法完成提交
springboot事务和aop基本一样
织入点:跟springaop一样,@transactional相当于@Around
调用:跟springaop一样
请求的入口是一个Thread的run(),最终会经过DispatcherServlet来处理。流程为
1)给request设置属性,最主要的是WebApplicationContext
2)获取HandlerExecutionChain拦截执行链:
a.循环处理所有的HandlerMapping,匹配上最佳的HandlerMapping。因为通过web请求,匹配上的是RequestMappingHandlerMapping,经过它的处理,返回了一个封装的handlerMethod对象,包含了调用的是哪个controller、哪个方法、参数等信息
b.对RequestMappingHandlerMapping里的Interceptor列表循环匹配得到的handlerMethod,匹配的上得到一个Interceptor链
3)匹配HandlerAdapter:DispatcherServlet中的HandlerAdapter循环匹配,最终匹配到RequestMappingHandlerAdapter
4)执行执行链的applyPreHandle():循环执行Interceptor链
的每个Interceptor的preHandle(),如果有一个返回false,就循环执行每一个Interceptor的afterCompletion(),退出;否则preHandle()执行完全
5)开始执行
a.解析参数
b.通过反射调用真实方法即controller的方法得到ModeleAndView
6)循环执行Interceptor链的PostHandle
7)执行Interceptor链的triggerAfterCompletion
结论:afterCompletion一定会执行,PreHandler不一定会执行完全
执行顺序:Filter->Interceptor.preHandle->Handler->Interceptor.postHandle->Interceptor.afterCompletion->Filter
开闭原则:对扩展开放,对修改关闭
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。抽象化
依赖倒转原则:针对接口编程,依赖于抽象而不依赖于具体
接口隔离原则:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度
迪米特法则,又称最少知道原则:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立
合成复用原则:尽量使用合成/聚合的方式,而不是使用继承