- 集合:提供一种存储空间可变的存储模式,存储的数据容量可以随时发生改变
- 数组:存储的是同种数据类型,长度在定义之后不能改变
- Collection是单列集合的顶层接口,Map是双列集合的顶级接口
- 迭代器是集合专用的遍历方式,通过Iterator iterator() 方法,hasNext()判断是否有下一个元素,如果有,则用next返回下一个元素
- 迭代器并发修改异常:迭代遍历过程中,通过集合对象修改集合中元素,比如增删改查等操作,造成了迭代器获取元素中判断预期修改值和实际值不一致
- 解决方案:用for循环替代迭代器进行遍历,可以见简单理解为,迭代器在执行循环之前获得一个数据值,在执行到此
-
List和Set的区别
- list集合元素有序,并且允许重复
- set集合无序,并且不允许重复
-
ArrayList和linkedList的区别
- ArrayList底层数据结构为数组,所以有索引,查询速递比较快,增删比较慢
- linkedList底层数据结构是双向链表,所以查询比较慢,但是增删比较快
- 介绍ArrayList底层实现
- 介绍linkedList底层实现
- 两者各适用于那些场合
-
ArrayList:底层是Object数组实现的,由于数组的地址是连续的,数组支持随机访问,数组在初始化的时候,需要指定容量,数组不支持动态扩容。像ArrayList,Vector和Stack使用的时候,看似不用考虑容量的问题,,但是他们的底层实际都做了扩容,数组的扩容代价比较大,需要重新开辟一个新数组,将数据拷贝进去,数组扩容效率低。
ArrayList:适合读数据较多的场合
-
linkedList:底层使用一个Node数据结构,有前后两个指针,双向链表实现的,相对数组,链表插入效率较高,只需要更改前后两个指针即可。另外,链表不存在扩容问题,因为链表不要求存储空间连续,每次插入数据都只是改变last指针,另外,链表所需要的内存比数组要多,因为他要维护前后两个指针。
linkedList:适合删除,插入较多的场景,另外linkedList还实现了Deque接口
-
数组和链表的特性差异:本质是,连续空间存储和非连续空间存储的差异。
Set集合父接口是Collection,Set集合底层实现的是Map集合,其中Set实现的是Map中的Key值,所以Set集合中的元素是不允许重复的,同时也是只能有一个null值,Set集合常用到的集合有HashSet、TreeSet
-
HashSet
- 底层实现的就是HashMap,所以是根据HashCode来判断是否是重复元素
- 因为TreeSet是额外使用红黑树来保证元素的有序性,所以相对来说是HashSet的性能是要比TreeSet要好
- 初始化容量是16,这是因为底层实现的是HashMap,加载因子是0.75
- HashSet是无序的,也就是说插入的顺序和取出的顺序是不一样的
- 因为HashSet不能根据索引去取数据,所以不能用普通的for循环来取数据,应该用增强for循环,这也进一步说明了HashSet的查询性能没有ArrayList高。
-
TreeSet
- 底层实现的是TreeMap
- 元素不能重复,可以有一个是null值,并且这个null值一直在第一个位置上
- 默认容量是:16,加载因子是0.75
- TreeMap是有序的,这个有序不是存入和取出的顺序是一样的,而是根据自然规律排序( 这个有序是通过Comparator的compare方法排序 )
-
Set集合如何去重的:
这是因为用到了HashCode()和equals(),这两个方法去决定的
步骤:
- 首席按获取该元素的Hash值,然后子啊Hash表中找到辞职下面有没有元素,如果没有则表示不是重复元素,可以添加。反之不可以添加
- 如果重复则还要用equals()比下是否一样,如果在和Hash值下面的所有元素都比较后,发现没有一样的,则可以添加,反之不可以添加
- 也就是说Hash值一样的元素,不一定相等,但是equals相同的元素Hash值一定相等
- AbstractMap ( 抽象父类 )
- SortedMap ( 有序
Map常见实现类有哪些
- HashTable:java早期提供的一个哈希表实现,他是线程安全的,不支持null键和值,性能不如ConCurrentHashMap
- HashMap:最常见的哈希表实现,如果程序中没有多线程的需求,HashMap是一个很好的选择,支持null值和键
- TreeMap:基于红黑树的一种提供顺序访问的Map,自身实现了Key的自然排序,也可以指定Comparator来自定义排序
- linkedHashMap:HashMap的一个子类。保存了记录插入顺序,可在遍历时保持与插入一样的顺序
HashMap–继承了AbstractMap接口
-
数据结构
- HashMap底层的数组,被称为哈希桶,每个桶中存放的是链表。链表中的每个节点就是HashMap中的每个元素。
- JDK1.7之前:数组+链表(链表前插)
- JDK1.8之后:数组+链表+红黑树(链表尾插)红黑树用以提升查询和插入的效率
- HashMap数据结构,是由数组(table) + 链表(entry)组成,每个entry对应一个数组的索引,也对应着一个唯一的hash地址值
- HashMap默认初始容量为16,默认加载因子为0.75
-
HashMap在put数据的时候,在底层代码会先判断put的值是否为null。如果为null,会固定存放在table[0]下面。如果不为null,会通过hash()方法计算对应的index地址,通过hash地址去寻找数据对应存放的table指定索引下。找到之后,会判断put的key在链表中是否存在,如果存在则为替换,如果不存在,则为新增
-
执行流程:
- 对key进行hash操作,计算储存的index
- 判断是否有hash碰撞,如果没有碰撞,直接放入哈希桶中,如果有碰撞,则以链表的形式存储,
- 判断已有的元素类型,决定是追加树还是追加链表,当链表大于等于8时,把链表转换成红黑树
- 如果节点已经存在,就替换旧值
- 判断是否超过阀值,如果超过,就要扩容
-
使用HashMap可能会遇到什么问题,如何避免
-
JDK1.7中,HashMap在并发场景中,可能出现死循环的问题,这是因为HashMap在扩容的时候,会对链表进行一次倒叙处理,假设两个线程同时执行扩容操作,第一个线程执行B->A,第二个线程又执行了A->B,这个时候就会出现B->A->B的问题,造成死循环
-
升级JDK版本。JDK8之后扩容不会进行倒序,因此死循环问题得到了极大的改善,但是如果是多线程,高并发的环境下,可以使用ConCurrentHashMap替代HashMap
-
TreeMap–继承了AbstractMap,实现了SortedMap
有那些方法可以解决hash冲突- 开放定址法
- 在哈希法
- 链地址法
- 建立公共溢出区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55ENsylx-1638325927282)(C:UsersMustangAppDataRoamingTyporatypora-user-imagesimage-20211125165332386.png)]
- new:初始状态
- runnable( running / ready ):运行状态(就绪状态)
- blocked:阻塞状态
- waiting:等待状态
- timed_waiting
- terminated:终止状态
线程调度优先选择优先级最高的运行,但是如果出现以下情况,就会终止运行
-
线程调用了yield方法,让出了CPU的使用权,进入线程就绪状态
-
线程调用了sleep()方法,使其进入计时状态
-
线程由于IO受阻
-
另一个更高优先级线程出现
-
在支持的时间片系统中,该线程的时间片用完
- 线程调度:是一个操作系统服务,它负责为存储在Runnable状态的线程分配时间片,一旦我们创建一个线程,并启动它,他的执行便依赖线程调度器的实现
- 时间分片:是指CPU可用时间分配给Runnable的过程,分配的时间可以根据线程优先级或者线程等待时间
- wait():进入等待状态,并释放锁
- sleep():使线程进入休眠状态,带有锁,是一个静态方法,需要处理异常
- notify():唤醒一个线程处于等待的线程( wait / sleep ),并不能确定会唤醒哪个线程,由JVM以及优先级有关
- notifyAll():唤醒所有处于等待状态的所有线程,但是不是把锁给所有线程。
两个方法都可以使线程进入等待状态
- 类型不同: sleep是静态方法,wait是Object类下的方法
- 是否释放锁: sleep不释放锁,wait释放锁
- 用处不同: wait通常用于线程间的通信,sleep长用户暂停执行
- 用法不同:
- wait用完之后,线程不会自动执行,只能由notify或者notifyAll 方法才能执行,sleep方法调用后,线程经过一段时间后会自动苏醒。
- wait(参数)也可以传参使其苏醒,因为wait方法会释放锁,所以在苏醒之后,会进入阻塞状态,和其他线程去竞争锁。而sleep在苏醒之后,进入就绪状态,等待CPU分配时间片,得到时间片之后即可运行。
- 乐观锁:每个去拿数据的时候,都认为别人不会修改,所以都不会上锁,但是在更新的时候,会判断一下在此期间有没有去更新这个数据,所以乐观锁使用了多读的场合,这样可以提高吞吐量,像数据库提供类似的write_condition机制,都是用的乐观锁
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候,都会认为有人修改,所以每次在拿数据的时候都会上锁,这样别的对象像拿到数据,就必须阻塞,直到拿到锁。传统的数据库用到了很多这种锁机制,比如读锁,写锁,在操作之前都会先上锁,synchronized同步代码块,用到的也是悲观锁
每个线程都具有优先级,一般来说,高优先级的在线程调度的时候,会具有优先被调用权,我们可以自定义线程的优先级,但是这并不能保证高优先级的在低优先级之前被调用,只是概率有点大。
线程优先级是1-10,1代表最低,10代表最高
java的线程优先级调度会委托操作系统去完成,所以具体的操作系统优先级也有关,如非特别需要,一般不去修改优先级
线程池 线程池的好处- 减低资源消耗,通过重复利用一创建的线程,降低线程创建和销毁造成的消耗
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
- 增加线程的可管理性,线程是稀缺资源,使用线程池可以进行统一分配,调优和监控
- threadFactory:线程工厂,用于创建线程的工厂
- corePoolSize:核心线程数,当线程池运行的线程少于corePoolSize时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态、
- workQueue:用于保留任务,并移交给工作线程的阻塞队列
- maximumPoolSize:最大线程数,线程池允许开启的最大线程数
- handler:拒绝策略,往线程池添加任务时,将在下面两种情况触发拒绝策略:
- 线程池的运行状态不是Running
- 线程池已经达到最大线程数,并且阻塞队列已满时
- keepAliveTime:保持存活时间,如果线程池当前线程数超过corePoolSize,则多余的线程空闲时间超过keepAliveTime时会被终止
- unit:空闲线程存活时间单位,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LqD59xVH-1638325927286)(C:UsersMustangAppDataRoamingTyporatypora-user-imagesimage-20211125202447429.png)]
线程池的五个状态- running:接收新任务并处理排队的任务
- shutdown:不接受新任务,但处理排队的任务
- stop:不接受新任务,不处理排队的任务,并终端正在进行的任务
- tidying:所有的任务都已终止,workerCount为零,线程转换到tidying状态将运行terminated钩子方法
- terminated:terminated已完成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wK0av4I0-1638325927288)(C:UsersMustangAppDataRoamingTyporatypora-user-imagesimage-20211125205257086.png)]
常见的阻塞队列4种- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出,对元素进行排序
- linkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于ArrayBlockingQueue
- SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制,要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝,使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务,只有当线程池时无界的或者可以拒绝任务时,该队列才有实际价值
- PriorityBlockingQueue:具有优先级的无界队列,先优先级对元素进行排序,元素的优先级是通过自然顺序或者Comparator来定义的
- 使用有界队列时,需要注意线程池满了以后,被拒绝的任务如何处理
- 使用无界队列时,需要注意,如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。
- AbortPolicy:终止策略,默认的拒绝策略,直接抛出RejectedExecutionException异常,调用者可以捕获这个异常,然后根据自己的需要求,编写处理代码
- DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务
- DiscardOldestPolicy:抛弃最老策略,抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务,如果阻塞队列是一个优先队列,那么抛弃最老策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用
- CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务,该策略实现了一种调节机制。该策略不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定的时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动,但是我们可以使用prestartCoreThread启动一个核心线程,或者prestartAllCoreThreads启动全部核心线程方法来提前启动核心线程
核心线程怎么实现一直存活阻塞队列方法有四种形式,它们以不同的方式处理操作
| 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 | |
|---|---|---|---|---|
| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除 | remove() | poll() | take() | poll(time,unit) |
| 检查 | element() | peek() | 不可用 | 不可用 |
核心线程在获取任务时,通过阻塞队列的take()方法,实现的一直阻塞(存活)
非核心线程如何实现在keepAliveTime后死亡通过利用阻塞队列的方法,在获取任务时,通过阻塞队列的poll(time,unit)方法实现的延迟死亡
非核心线程能成为核心线程吗虽然一直讲着核心线程和非核心线程,但是其实线程池内部是不区分核心线程和非核心线程的,只是跟根据当前线程池的工作线程数来进行调整,因此看起来像是有核心线程与非核心线程
如何终止线程池- shutdown:关闭线程池,不接受新的任务,但是会在关闭之前,将提交的任务处理完毕
- shutdownNow:直接关闭线程池,Thread.interrupt()方法终止所有线程,不会等待之前提交的任务执行完毕,但是会返回队列中未处理的任务
- newFixedThreadPool:固定线程数的线程池,corePoolSize=maximumPoolSize、keepAliveTime为0,工作队列使用无界的
- linkedBlockingQueue:只有一个线程的线程池,corePoolSize = maximumPoolSize = 1,keepAliveTime为0,工作队列使用无界的linkedBlockingQueue,适用于需要保证顺序的各个任务场景
- newCachedThreadPool:按需要创建新线程的线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交SynchronousQueue,该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时,会自动回收空闲线程,适用于执行很多的短期异步任务,或者时负载较轻的服务器
- newScheduledThreadPool:创建一个以延迟或定时的方式来执行任务的线程池,工作队列为DelayedWorkQueue,适用于需要多个后台执行周期任务。
- newWorkStealingPool:JDK1.8新增,用于创建一个可以窃取的线程池。底层使用ForkJoinPool实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MOJGCc0v-1638325927290)(C:UsersMustangAppDataRoamingTyporatypora-user-imagesimage-20211125225742270.png)]
Throwable- Throwable是java语言中所有错误与异常的超类
- Throwable包含两个子类:Error和Exception,他们通常用于指示发生了异常情况
- Throwable包含了其线程创建时线程的快照,它提供了printStackTrace()等接口,用户获取对战跟踪数据等信息
-
运行时异常
编译器不会检查他,就是说当程序中可能出现这种异常时,即使没有throws,或者try-catch也会编译通过。
-
编译时异常
java编译器会检查他,如果程序中出现此类异常,要么throws,要么try-catch,否则不能通过编译
-
CheckedException:编译器要求必须被处理的异常
-
UncheckedException:编译器不会进行检查,并且不要求必须处理的异常
习惯上,自定义一个异常类应该包含两个构造函数,一个无参构造函数,和一个带有详细描述信息的构造函数(Throwable的toString方法会打印这些详细信息)
MySQL一般来说,索引本身也很大,不能全部存储到内存中,因此索引网网以索引文件的形式储存在磁盘上
索引的优势- 查找:类似图书馆建书目索引,提高数据检索的效率。降低IO成本
- 排序:通过索引列对数据进行排序,降低数据排序的成本,降低CPU消耗
- 实际上,索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录。所以索引列也是要占用空间的
- 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对比表进行增删改,因为更新表的同时,不仅要保存数据,还要保存以下索引文件每次更新添加了索引列的字段,都会调整,因为更新所带来的键值变化后的索引信息
- 索引只是提高效率的一个因素,如果你的mysql中有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询
- 单值索引:一个索引只包含单个列,一个表可以有多个单列索引
- 唯一索引:索引列的值必须唯一,但允许有空
- 符合索引:一个索引包含多个列
- 主键索引:特殊的唯一索引
- B+树索引
- Hash索引
- Full-text索引
- R-Tree索引
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应创建索引,比如订单号,银行系统的手机号,
- 查询中与其他表关联的字段,外键关系建立索引
- 频繁更新的字段不适合创建索引,因为每次更新不单单只是更新了数据,还会更新索引
- WHERe条件里用不到的字段不创建索引
- 单键/组合索引的选择:在高并发下倾向创建组合索引
- 查询中排序的字段,排序字段如果通过索引去访问将大大提高排序速度
- 查询中,统计或者分组的字段
- 表记录太少
- 经常增删改的表:提高了查询速度,同时会降低增删改表的速度
- 数据重复,且平均分布的表字段,因此应该只为最经常查询和最经常排序的数据建立索引
结构化的查询语言,是一种数据库查询语言。
数据库的三大范式是- 每个列都不可拆分。
- 每个非主键列都与主键列相关
- 每个非主键列都与主键列直接相关,而不能间接相关
- InnoDB:提供了对数据库ACID事务的支持,并且还提供了行级锁和外键约束,他的设计目标就是处理大数据容量的数据库系统
- MyIASM:不支持事务,也不支持行级锁和外键
- MEMORY:所有的数据都存储在内存中,数据的而处理速度快,但是安全性不高
- InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引
- InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效
- 第一个自己编,处理办法是加锁(了解JUC的话可以说读写锁,否则就不要说),或者使用线程安全的集合(例如CopyOnWriteArrayList,集合里存的啥自己编)。
- 有定时任务可以说ScheduledExecutorService,需要处理大量短任务可以说newCachedThreadPool,需要处理少量长任务可以说newFixedThreadPool。
- 第一个问题看张宏建的笔记
- 线程池的工作机制(流程):
- 1.线程池刚创建的时候是空的,里面没有线程
- 2.有任务过来时先创建核心线程来处理请求,如果存在空闲的核心线程,依旧会创建新的核心线程来处理新来的请求,直到核心线程数达到最大核心线程数
- 3.如果所有的核心线程都被占用了,又来了新的请求,会将请求暂时存放在该线程池的工作队列中
- 4.如果工作队列也满了,又来了新的请求,会创建临时线程来处理新的请求,如果存在空闲的临时线程,会交给空闲的临时线程来处理
- 5.如果临时线程也满了,而且没有空闲的,又来了新的请求,会执行拒绝处理。
- 1.在经常作为查询条件的字段上添加索引
- 2.不要使用*,使用全字段名称
- 3.多表联查时尽量使用join关联查询,不要使用笛卡尔积或嵌套select。并且用小表join大表
- 4.把一条大sql分成多条小sql
- 5.where子句的执行顺序是从右到左,所以将能过滤掉大量数据的条件放在后面
1.范围where条件,比如where value < 100
2.like模糊查询时以%开头
3.查询条件中使用or会使索引失效,要想是索引生效,需要将or中的每个字段都加上索引。
4.对索引字段进行函数运算
5.最左原则
- 哪个字段上加了索引:这个自己心里一定要有数,没有加过的就编一下
- **加索引的原因:**经常作为查询条件
- 在哪用的,怎么查的:但凡问这个问题就是想让你说多表联查,所以需要你非常清除自己项目的表结构,不要人家连问两次都是一个表就出结果了
- 复合索引可以编一下,不过你说了就会被问为什么在这几个字段上加复合索引
- **数据库的分库和分表:**原则上能不分就不分,因为会增加系统资源的消耗,所以如果简历上的项目规模很小,就说没有分过就好(狗头保命)
- 在controller层使用try-catch,但是大量使用try-catch会使代码显得冗余,复杂度变高不便于维护2.定义全局异常处理类
//1.标识该类是全局异常处理机制.返回值都是JSON串
// 通知: AOP中的技术,解决特定问题的
// 特点: 该异常处理机制,只拦截Controller层抛出的异常
@RestControllerAdvice
public class SystemExe {
@ExceptionHandler({RuntimeException.class})
public SysResult fail(Exception e){
e.printStackTrace();
return SysResult.fail();
}
}
单个服务事务控制(声明式事务和编程式事务):
- 声明式事务使用@Transactional注解:
- 1.在业务层的需要操作数据库的方法上添加@Transactional注解 2.@Transactional注解是Spring提供的事务管理注解,基于Spring的AOP来实现事务控制,本质就是在目标方法执行前后进行拦截,在被@Transactional注解修饰的方法上添加环绕通知,在整个方法执行之前开启事务,在整个方法执行完成后提交或回滚事务,事务粒度是整个方法。
(1)当需要事务控制的方法被同类方法调用时:Spring采用动态代理(AOP)实现对bean的管理和切片,它为我们的每个class生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。而在同一个class中,方法B调用方法A,调用的是原对象的方法,而不通过代理对象。所以Spring无法切到这次调用,也就无法通过注解保证事务性了。也就是说,在同一个类中的方法调用,则不会被方法拦截器拦截到,因此事务不会起作用。
解决方案:可以将方法放入另一个类,并且该类通过spring注入,即符合了在对象之间调用的条件。
(2)长事务导致数据库连接池中的连接被长时间占用,当并发度稍微高一些的时候有可能导致数据库连接池耗尽。会报CannotGetJdbcConnectionException的异常。
解决方法:使用编程式事务控制,编程式事务由开发人员手动控制事务的开启和提交以及回滚
1.基于底层的API,开发者在代码中手动的管理事务的开启、提交、回滚等操作。在spring项目中可以使用TransactionTemplate类的对象,手动控制事务
2.启动类添加@EnableAspectJAutoProxy(exposeProxy = true),方法内使用AopContext.currentProxy()获得代理类,使用事务。
用到的设计模式:代理模式(cglib动态代理和jdk自带的动态代理) 通知如何实现的:拦截器(具体的可以自己看看)spring在SSM中的作用:
首先,spring是一个bean容器,用来管理bean的生命周期,其次,用于框架的集成
spring中使用的设计模式:工厂模式:通过BeanFactory来创建Bean对象 单例模式:每个交给spring管理的Bean默认都是单例的 代理模式:Aop基于cglib动态代理来实现 观察者模式:SpringBoot启动过程中通过广播器发布事件和监听器监听事件来完成整个启动流程.Bean的生命周期:
Bean的创建: (1)执行一系列Aware接口,向当前Bean中设置一些属性值,方便后续通过Bean直接获取,例如BeanName,BeanFactory,BeanClassLoader等。 (2)通过反射执行Bean的实例化 (3)执行Bean初始化前置处理,PostProcessBeforeInitialization方法 (4)执行Bean的初始化 (5)调用自定义Bean初始化方法 (6)执行Bean初始化后置处理,PostProcessAfterInitialization方法 Bean的销毁: (1)执行Bean工厂关闭前置处理,PostProcessBeforeDestruction方法 (2)关闭Bean工厂 (3)执行Bean的失效方法mybatis的好处:
(1) 把sql从java代码中独立出来 (2) 封装了jdbc,基于orm方式简化了java对数据库的操作,并且将操作数据库的结果集自动转换成javabean对象FactoryBean:“高级定制”,
用来生产某一个唯一复杂对象,其中提供了3个方法getObject,getObjectType,getSingleton在整个Spring框架中,如果想在某些阶段做一些独特的事,使用的是观察者模式(监听器) 什么是context上下文对象
context对象中保存的是某个作用域中包含的属性的值分布式事务(看涛哥帖子) springboot启动流程
整体大致流程:
(1)通过BeanDefinationReader接口的某一具体实现类Reader读取并解析Bean的定义信息
(2)将解析后的Bean的定义信息交给IoC容器
(3)BeanFactory从IoC容器中获取Bean的定义信息,通过反射来创建Bean对象,完成Bean的实例化。在获取Bean定义信息的过程中可通过BeanFactoryPostProcessor对Bean的定义信息进行修改或增强。
(4)执行Bean的初始化。在初始化前可以执行一系列BeanPostProcessor方法。
具体流程:
1.将当前启动类的字节码文件放入primarySource的linkedHashSet中
2.将primarySource集合作为参数,new一个SpringApplication对象
3.给这个SpringApplication对象设置一些属性值:
(1)当前web应用的类型(有三种,reactive响应式,none,servlet,常见的是servlet)
(2)初始化器(不带自定义的有7个)
(3)监听器(不带自定义的有11个)
(这些初始化器和监听器都是从spring的jar包中的meta-INF目录下的spring.factories文件中取出来的,取出来的仅仅是类的全路径,也就是包名加类名,方便后续通过Class.forName(类的全路径)利用反射来实例化这些对象)
4.执行run方法
(1)开启计时(用于记录启动时间)
(2)定义异常报告器
(3)从SpringApplication对象中读取监听器,并发布SpringBoot启动的事件
(4)监听器监听到启动事件,触发接下来的操作
(5)读取系统环境信息,并忽略掉 .ignore文件中的信息
(6)获取当前web应用的context上下文对象(后续会向其中设置属性值)
(7)设置当前web应用的异常报告器(刚才定义的)
(8)初始化上下文对象
i.向其中设置属性值(系统环境信息,类加载器信息,类型转换服务等)
ii.通过context对象获取BeanFactory,并注册单例对象(最常用的BeanFactory:DefaultListableBeanFactory)
iii.执行load方法,其中通过BeanDefinationReader的一个实现类对象AnnotatedBeanDefinationReader通过扫描@Component注解读取到当前启动类
(@SpringBootApplication注解一直往里点会出现@Component注解)
iiii.广播器发布事件:上下文对象准备完毕
5.refresh方法(整个启动流程的核心)
(1)开启SpringBoot容器(close属性设置为false,active属性设置为true)
(2)获取BeanFactory
(3)执行BeanFactory的准备工作,设置一些属性值
(4)配置Bean工厂的后置处理器,BeanFactoryPostProcessor(空方法,SpringBoot做了一点扩展)
(5)执行BeanFactory的实例化,并执行BeanFactoryPostProcessor
(6)向Bean工厂中注册BeanPostProcessor
(7)国际化配置 il8n
(8)初始化事件广播器
(9)onfresh方法获取Tomcat Server
(10)注册监听器
(11)完成BeanFactory初始化,并实例化所有非懒加载的Bean对象
(12)finishRefresh
1.防止用户重复提交:前端控制,用户点击注册按钮后将按钮变为一个等待图标,直到收到后端返回的数据。 2.用户点击注册按钮后,先到数据库中查找是否存在相同的用户名(邮箱),若不存在,则提示用户需要进行邮箱验证,用户点击确定后向用户注册的邮箱发送验证邮件,其中是一个激活用的url。不直接将用户信息入库以及不直接向用户填写的邮箱发送邮件是为了防止恶意脚本攻击。 3.邮箱激活(依赖,不可重入锁,失效机制):目的是防止用户输入的邮箱格式正确但实际不存在 注册的表单中有一个隐藏域,每次注册页面加载的时候会将一个基于UUID生成的字符串方到隐藏域中,提示用户需要进行邮箱验证,用户点击确定后,会将该字符串作为key,用户填写的注册表单信息作为value保存到redis中,并设置有效期,同时会将这个字符串传到后端,作为激活链接的一部分,激活链接就是基础url拼接上这个字符串,用户点击激活链接就是发起一个到redis中查询数据的请求,查的key就是那个UUID,若已过期,则提示用户重新注册,若存在,则读取将用户填写的注册信息并将redis中的这个key删除,然后将用户的注册信息添加到数据库中。 这里会出现几个问题: (1)如果用户收到验证邮件后并没有点击激活,而是又重新注册了一遍,就会再向原来的邮箱发送一封验证邮件,如果紧接着用户点击了两封验证邮件中的激活链接,就会向数据库中插入两条用户名(邮箱)相同的记录。 (2)如果多个用户同时使用同一个邮箱进行注册,注册进度也几乎一致,都该向邮箱发送验证邮件了,此时这些用户都点击确定之后,在redis中就会产生多个记录,因为这些用户在注册时的UUID是不同的,但他们不会同时点击激活链接,因为同一时间只有一个人能登录那个邮箱,但邮箱中却会有多封邮件,就又回到了上面的问题。 问题的解决: (1)首先在建表的时候会给用户邮箱的这个字段添加唯一约束,这样的话插入重复数据会产生异常,我们会将异常捕获处理后提示用户该邮箱已激活。 (2)其次,不再使用UUID,提示用户需要进行邮箱验证并且用户点击确定后,将用户邮箱使用MD5加密算法生成一个密文,加密时添加设定好的盐值,用这个密文取代之前的UUID,作为key保存到redis中,同样要设置有效时长,在保存之前,先到redis中查询是否存在这个key,若已存在,则提示用户已向该邮箱发送过验证邮件,若不存在,再发送验证邮件,之前激活链接中的UUID改成这个密文。 (3)最后,将发送邮件的请求放入RabbitMQ消息队列中。 其他问题: (1)发不了邮件:主机邮箱需要开启SMTP,POP3,IMAP4服务,可以在邮箱设置中开启 (2)主机邮箱认证错误:主机用户名必须和邮箱名的@之前的内容一致,主机密码是开启SMTP服务后邮箱官方提供的授权码 (3)找不到MailLogger类:依赖导错了,将javax.mail的依赖包换成com.sun.mail的依赖包 4.分布式id(雪花算法) 64位Long类型二进制数字 第一位是符号位 之后41位是毫秒级的时间戳,可以表示69年的时间 之后10位自定义,比如定义为当前数据库所在的服务器号(假设为4位)和磁盘号(假设为6位) 雪花算法的实现主要是依赖于数据中心和数据节点id两个参数二、登录
token的存储,过期,刷新 认证三、网关
请求过滤和转发



