定期总结-2023
2023-10-23 11:19:04 0 举报
AI智能生成
java
作者其他创作
大纲/内容
分析
基础
记忆
ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:
原子性(atomicity,或称不可分割性)
一致性(consistency)
隔离性(isolation,又称独立性)
持久性(durability)
BASE柔性事务
BASE柔性事务是指 Basic Available(基本可用)、Soft-state(软状态/柔性事务)、Eventual Consistency(最终一致性)。从广义上来看,像XA事务其实也是属于一种柔性事务。但是一般情况下,BASE柔性事务特指Seata框架提供的柔性事务,因为BASE实际上是集成了阿里对于分布式事务的所有研究,而阿里的这些研究成果,最终都沉淀到了Seata框架中。ShardingSphere中对于柔性事务的支持,其实也是更多的基于Seata的AT模式,来实现的两阶段提交。这里要注意的是,虽然XA和AT都是基于两阶段协议提供的实现,但是AT模式相比XA模式,简化了对于资源锁的要求,所以可以认为在大部分的业务场景下,AT模式比XA模式性能稍高。
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
造成死锁的四个必要条件
及其各自的避免死锁方案
及其各自的避免死锁方案
互斥条件
使用锁就是为了互斥临界资源,没有办法破坏
请求与保持条件
一次性申请所有需要的资源
不剥夺条件
占用部分资源的线程,如果在获取资源时获取不到,就主动释放它占有的资源
循环等待
按照循序申请资源,按照反序释放资源,破坏循环等待条件
将系统中的所有资源统一编号,进程提出资源申请时必须按照资源的编号顺序(升序)提出。
这样可以确保系统不会出现死锁,因为资源按照编号顺序分配,不会出现循环等待
这样可以确保系统不会出现死锁,因为资源按照编号顺序分配,不会出现循环等待
@Trasactional
JavaSE
java集合
Collection
List
ArrayList
底层数据结构是数组,连续内存,所以查询快(指根据下标访问),增删慢
线程不安全
效率高
Linkedlist
底层数据结构是链表,无需连续内存,所以查询慢(要沿着链表遍历),增删快
线程不安全
效率高
Vector
底层数据结构是数组,所以查询快增删慢
线程安全
效率低
Set
HashSet
底层数据结构是哈希表
LinkedHashSet
底层数据结构由链表和哈希表组成
由链表保证元素有序
由哈希表保证元素唯一
TreeSet
底层数据结构是红黑树。(是一种自平衡的二叉树)
根据比较的返回值是否是0来决定保证元素唯一性
两种排序方式
自然排序(元素具备比较性)
让元素所属的类实现Comparable接口
比较器排序(集合具备比较性)
让集合接收一个Comparator的实现类对象
Map
(Map集合的数据结构仅仅针对键有效,与值无关。
存储的是键值对形式的元素,键唯一,值可重复。)
(Map集合的数据结构仅仅针对键有效,与值无关。
存储的是键值对形式的元素,键唯一,值可重复。)
HashMap
底层数据结构是哈希表。线程不安全,效率高
哈希表依赖两个方法:hashCode()和equals()
jdk8之前数据结构
数组+链表
头插法
多线程死锁问题
jdk8及其之后
数组+链表+红黑树
尾插法
1.树化 :hash冲突的值放链表,当数组长度大于64,且链表长度超过阈值8,则转成红黑树,否则先进行扩容
2.树退化: a.扩容时,原来红黑树上一部分数据可能会转移到别的槽中,当红黑树中的元素小于等于6时,退化成链表
b.调用remove方法删除数据时,删除之前校验红黑树根节点 左儿子 左孙子 右儿子是否存在,如果有一个不存在,退化成链表
2.树退化: a.扩容时,原来红黑树上一部分数据可能会转移到别的槽中,当红黑树中的元素小于等于6时,退化成链表
b.调用remove方法删除数据时,删除之前校验红黑树根节点 左儿子 左孙子 右儿子是否存在,如果有一个不存在,退化成链表
重要成员变量
DEFAULT_INITIAL_CAPACITY = 1 << 4; Hash表默认初始容量 16,有参构造函数:可以指定容量。会根据指定的正整数找到不小于指定容量的2的幂数
MAXIMUM_CAPACITY = 1 << 30; 最大Hash表容量,如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE( 2^31-1 ,即永远不超出阈值了)
DEFAULT_LOAD_FACTOR = 0.75f;默认加载因子,加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量,默认情况下是16x0.75=12时,会触发扩容操作
TREEIFY_THRESHOLD = 8;链表转红黑树阈值
UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值
MIN_TREEIFY_CAPACITY = 64;链表转红黑树时hash表最小容量阈值,达不到优先扩容。
MAXIMUM_CAPACITY = 1 << 30; 最大Hash表容量,如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE( 2^31-1 ,即永远不超出阈值了)
DEFAULT_LOAD_FACTOR = 0.75f;默认加载因子,加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量,默认情况下是16x0.75=12时,会触发扩容操作
TREEIFY_THRESHOLD = 8;链表转红黑树阈值
UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值
MIN_TREEIFY_CAPACITY = 64;链表转红黑树时hash表最小容量阈值,达不到优先扩容。
HashMap的put方法过程
不得不聊
多线程使用(不在此体系下)
java.util.concurrent.ConcurrentHashMap
属于 JUC 包下的一个集合类,可以实现线程安全
hashtable区别
● Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
● ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
● ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
子主题
concurrentHashMap(☆☆☆)
1.7
Segment 段(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
1.8
Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突
ConcurrentHashMap的数据结构与HashMap基本类似,
区别在于:
1、内部在数据写入时加了同步机制(分段锁)保证线程安全,读操作是无锁操作;
2、扩容时老数据的转移是并发执行的,这样扩容的效率更高。
区别在于:
1、内部在数据写入时加了同步机制(分段锁)保证线程安全,读操作是无锁操作;
2、扩容时老数据的转移是并发执行的,这样扩容的效率更高。
Segment
Java7 ConcurrentHashMap基于ReentrantLock实现分段锁
Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized关键字实现
重要的成员
LOAD_FACTOR: 负载因子, 默认75%, 当table使用率达到75%时, 为减少table的hash碰撞, tabel长度将扩容一倍。负载因子计算: 元素总个数%table.lengh
TREEIFY_THRESHOLD: 默认8, 当链表长度达到8时, 将结构转变为红黑树。
UNTREEIFY_THRESHOLD: 默认6, 红黑树转变为链表的阈值。
MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位个数。
MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容
TREEBIN, 置为-2, 代表此元素后接红黑树。
nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable上。
sizeCtl: 用来标志table初始化和扩容的,不同的取值代表着不同的含义:
0: table还没有被初始化
-1: table正在初始化
小于-1: 实际值为resizeStamp(n)<<RESIZE_STAMP_SHIFT+2, 表明table正在扩容
大于0: 初始化完成后, 代表table最大存放元素的个数, 默认为0.75*n
transferIndex: table容量从n扩到2n时, 是从索引n->1的元素开始迁移, transferIndex代表当前已经迁移的元素下标
ForwardingNode: 一个特殊的Node节点, 其hashcode=MOVED, 代表着此时table正在做扩容操作。扩容期间, 若table某个元素为null, 那么该元素设置为ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就会帮着扩容。
ConcurrentHashMap由三部分构成, table+链表+红黑树, 其中table是一个数组, 既然是数组, 必须要在使用时确定数组的大小, 当table存放的元素过多时, 就需要扩容, 以减少碰撞发生次数, 本文就讲解扩容的过程。扩容检查主要发生在插入元素(putVal())的过程:
一个线程插完元素后, 检查table使用率, 若超过阈值, 调用transfer进行扩容
一个线程插入数据时, 发现table对应元素的hash=MOVED, 那么调用helpTransfer()协助扩容
LOAD_FACTOR: 负载因子, 默认75%, 当table使用率达到75%时, 为减少table的hash碰撞, tabel长度将扩容一倍。负载因子计算: 元素总个数%table.lengh
TREEIFY_THRESHOLD: 默认8, 当链表长度达到8时, 将结构转变为红黑树。
UNTREEIFY_THRESHOLD: 默认6, 红黑树转变为链表的阈值。
MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位个数。
MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容
TREEBIN, 置为-2, 代表此元素后接红黑树。
nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable上。
sizeCtl: 用来标志table初始化和扩容的,不同的取值代表着不同的含义:
0: table还没有被初始化
-1: table正在初始化
小于-1: 实际值为resizeStamp(n)<<RESIZE_STAMP_SHIFT+2, 表明table正在扩容
大于0: 初始化完成后, 代表table最大存放元素的个数, 默认为0.75*n
transferIndex: table容量从n扩到2n时, 是从索引n->1的元素开始迁移, transferIndex代表当前已经迁移的元素下标
ForwardingNode: 一个特殊的Node节点, 其hashcode=MOVED, 代表着此时table正在做扩容操作。扩容期间, 若table某个元素为null, 那么该元素设置为ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就会帮着扩容。
ConcurrentHashMap由三部分构成, table+链表+红黑树, 其中table是一个数组, 既然是数组, 必须要在使用时确定数组的大小, 当table存放的元素过多时, 就需要扩容, 以减少碰撞发生次数, 本文就讲解扩容的过程。扩容检查主要发生在插入元素(putVal())的过程:
一个线程插完元素后, 检查table使用率, 若超过阈值, 调用transfer进行扩容
一个线程插入数据时, 发现table对应元素的hash=MOVED, 那么调用helpTransfer()协助扩容
table扩容过程就是将table元素迁移到新的table上, 在元素迁移时, 可以并发完成, 加快了迁移速度, 同时不至于阻塞线程。所有元素迁移完成后, 旧的table直接丢失, 直接使用新的table
CopyOnWrite机制(写时复制)
核心思想:读写分离,空间换时间,避免为保证并发安全导致的激烈的锁竞争。
在CopyOnWrite机制中,当一个线程要修改共享数据时,会先将共享数据的副本复制出来,进行修改。
修改完成后,再将修改后的数据复制回原来的位置,用新的数据替换旧的数据。
在此过程中,其他线程可以继续读取旧的数据,不会受到影响。
修改完成后,再将修改后的数据复制回原来的位置,用新的数据替换旧的数据。
在此过程中,其他线程可以继续读取旧的数据,不会受到影响。
关键点
1、CopyOnWrite适用于读多写少的情况,最大程度的提高读的效率;
2、CopyOnWrite是最终一致性,在写的过程中,原有的读的数据是不会发生更新的,只有新的读才能读到最新数据;
3、如何使其他线程能够及时读到新的数据,需要使用volatile变量;
4、写的时候不能并发写,需要对写操作进行加锁;
2、CopyOnWrite是最终一致性,在写的过程中,原有的读的数据是不会发生更新的,只有新的读才能读到最新数据;
3、如何使其他线程能够及时读到新的数据,需要使用volatile变量;
4、写的时候不能并发写,需要对写操作进行加锁;
LinkedHashMap
Hashtable
TreeMap
多线程
实现方式
1) 继承Thread
demo
2)实现Runnable接口
demo
3) 使用 Callable接口 (可以使用CompletableFuture )
demo
Callable可以返回任务的执行结果。
异步结果
CompletableFuture
之前都在使用 Future,要么只能用 get 方法阻塞,要么就用 isDone 来判断,JDK1.8 之后新增了 CompletableFuture 用于异步编程,它针对 Future 的功能增加了回调能力,可以简化异步编程。
CompletableFuture 主要包含四个静态方法去创建对象,主要区别在于 supplyAsync 返回计算结果,runAsync 不返回,另外两个方法则是可以指定线程池,如果不指定线程池则默认使用 ForkJoinPool,默认线程数为CPU核数。
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);
public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
方法
串行
AND 聚合
Or 聚合
匿名内部类
线程状态(生命周期)
五态
图
新生状态(New)
用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
就绪状态(Runnable)
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态:
1. 新建线程:调用start()方法,进入就绪状态;
2. 阻塞线程:阻塞解除,进入就绪状态;
3. 运行线程:调用yield()方法,直接进入就绪状态;
4. 运行线程:JVM将CPU资源从本线程切换到其他线程。
1. 新建线程:调用start()方法,进入就绪状态;
2. 阻塞线程:阻塞解除,进入就绪状态;
3. 运行线程:调用yield()方法,直接进入就绪状态;
4. 运行线程:JVM将CPU资源从本线程切换到其他线程。
运行状态(Running)
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
阻塞状态(Blocked)
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:
1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
死亡状态(Terminated)
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。
线程池Executor
常见线程池的特点和用法
FixedThreadPool
固定数量线程池
CachedThreadPool
可缓存线程池
特点:具有自动回收多余线程的功能
特点:具有自动回收多余线程的功能
ScheduledThreadPool
支持定时及周期性任务执行的线程池
SingleThreadExecutor
单线程的线程池:它只会用唯一的工作线程来执行任务
它的原理和FixedThreadPool是一样的,但是此时的线程数量被设置为了1
它的原理和FixedThreadPool是一样的,但是此时的线程数量被设置为了1
以上4种线程池的构造函数的参数
以上4种线程池对应的阻塞队列分析
FixedThreadPool和SingleThreadExecutor的Queue是LinkedBlockingQueue?
CachedThreadPool使用的Queue是SynchronousQueue?
ScheduledThreadPool来说,它使用的是延迟队列DelayedWorkQueue
workStealingPool是JDK1.8加入的
这个线程池和之前的都有很大不同
子任务
一般是不加锁的
窃取
执行顺序不能保障
适用于递归
自定义线程池
ThreadPoolExecutor
demo
建议
ThreadPoolExecutor的重要参数包括:
corePoolSize:核心线程数,即使没有任务需要执行,线程池也会保持这些线程。对于长时间等待的任务,线程池不会创建超过这个数的线程。
maximumPoolSize:当线程数大于或等于核心线程数,且任务队列已满时,线程池会创建新的线程,直到线程数量达到这个数。
keepAliveTime:当线程空闲时间达到这个参数所设置的时间时,线程会被销毁,直到线程数量等于核心线程数。
unit:keepAliveTime参数的时间单位。
workQueue:执行前用于保存任务的队列。如果这个队列已满,新提交的任务将被拒绝。
threadFactory:用于创建新线程的工厂。
handler:当拒绝处理任务时的处理策略。
对于ThreadPoolExecutor的参数设置建议与优化,以下是一些建议:
根据业务需求设定corePoolSize和maximumPoolSize。corePoolSize建议设置为通常执行任务的线程数,maximumPoolSize建议设置为最高可能执行的线程数。可以通过监控线程池的状态和使用情况,根据实际需求进行调整。
根据任务类型和需求设定keepAliveTime。如果任务需要立即执行,keepAliveTime可以设置为0;如果任务可能需要等待一段时间才能被执行,可以将keepAliveTime适当设置,以避免创建过多的线程。
根据任务类型和需求选择适当的workQueue。不同的任务队列适用于不同的场景,需要根据实际需求选择适当的队列,例如LinkedBlockingQueue、ArrayBlockingQueue等。
使用ThreadPoolExecutor的allowCoreThreadTimeOut方法。这个方法可以在指定时间内让核心线程空闲并自动关闭,以避免资源浪费。
根据实际需求设定handler。如果任务不能被接受,可以根据实际情况设定对应的拒绝策略,例如直接抛出异常、存储到磁盘等。
总之,ThreadPoolExecutor的参数设置需要根据实际业务需求和系统资源情况进行调整和优化,以达到最佳的性能和资源利用率。
corePoolSize:核心线程数,即使没有任务需要执行,线程池也会保持这些线程。对于长时间等待的任务,线程池不会创建超过这个数的线程。
maximumPoolSize:当线程数大于或等于核心线程数,且任务队列已满时,线程池会创建新的线程,直到线程数量达到这个数。
keepAliveTime:当线程空闲时间达到这个参数所设置的时间时,线程会被销毁,直到线程数量等于核心线程数。
unit:keepAliveTime参数的时间单位。
workQueue:执行前用于保存任务的队列。如果这个队列已满,新提交的任务将被拒绝。
threadFactory:用于创建新线程的工厂。
handler:当拒绝处理任务时的处理策略。
对于ThreadPoolExecutor的参数设置建议与优化,以下是一些建议:
根据业务需求设定corePoolSize和maximumPoolSize。corePoolSize建议设置为通常执行任务的线程数,maximumPoolSize建议设置为最高可能执行的线程数。可以通过监控线程池的状态和使用情况,根据实际需求进行调整。
根据任务类型和需求设定keepAliveTime。如果任务需要立即执行,keepAliveTime可以设置为0;如果任务可能需要等待一段时间才能被执行,可以将keepAliveTime适当设置,以避免创建过多的线程。
根据任务类型和需求选择适当的workQueue。不同的任务队列适用于不同的场景,需要根据实际需求选择适当的队列,例如LinkedBlockingQueue、ArrayBlockingQueue等。
使用ThreadPoolExecutor的allowCoreThreadTimeOut方法。这个方法可以在指定时间内让核心线程空闲并自动关闭,以避免资源浪费。
根据实际需求设定handler。如果任务不能被接受,可以根据实际情况设定对应的拒绝策略,例如直接抛出异常、存储到磁盘等。
总之,ThreadPoolExecutor的参数设置需要根据实际业务需求和系统资源情况进行调整和优化,以达到最佳的性能和资源利用率。
线程池的核心参数
1最大线程数maximumPoolSize
2核心线程数corePoolSize
3活跃时间keepAliveTime
4阻塞队列workQueue
5拒绝策略RejectedExecutionHandler
2核心线程数corePoolSize
3活跃时间keepAliveTime
4阻塞队列workQueue
5拒绝策略RejectedExecutionHandler
当提交一个新任务到线程池时,具体的执行流程
图
1当提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
2当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
3当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,
如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
4如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
2当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
3当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,
如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
4如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
拒绝策略主要有四种: 1 AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2 CallerRunsPolicy:使用调用者所在的线程来处理任务
3 DiscardOldestPolicy:丢弃等待队列中最老的任务,并执行当前任务
4 DiscardPolicy:直接丢弃任务,也不抛出异常
JVM
组成(JVM包含两个系统和两个组件)
系统
类加载器子系统
JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。
类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。
在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,
除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,
而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销
类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。
在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,
除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,
而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销
类加载器
启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用
是虚拟机自身的一部分,用来加载支撑JVM运行的位于JRE的lib目录下的核心类库,
比如rt.jar、charsets.jar等,
或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
比如rt.jar、charsets.jar等,
或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。
Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
负责加载\lib\ext目录或Java.ext.dirs系统变量指定的路径中的JAR类包
应用类加载器:AppClassLoader
也叫做系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。主要就是加载你自己写的那些类
一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
也叫做系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。主要就是加载你自己写的那些类
一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现
类装载的执行过程
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
双亲委派模型
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的
类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一
个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到
JVM 内存,然后再转化为 class 对象
类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一
个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到
JVM 内存,然后再转化为 class 对象
图
解释
先找父亲加载,不行再由儿子自己加载
原因
为什么要设计双亲委派机制?
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心
API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一
次,保证被加载类的唯一性
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心
API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一
次,保证被加载类的唯一性
打破双亲委派机制
重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
loadClass
字节码执行引擎
组件
运行时数据区(内存结构)
需要找时间去官网校验
需要找时间去官网校验
Java7
图
Java堆
堆中主要存储的是实际创建的对象,也就是会存储通过new关键字创建的对象,堆中的对象能够被多个线程共享。堆中的数据不需要事先明确生存期,可以动态的分配内存,不再使用的数据和对象由JVM中的GC机制自动回收。对JVM的性能调优一般就是对堆内存的调优。
Java中基本类型的包装类:Byte、Short、Integer、Long、Float、Double、Boolean、Character类型的数据是存储在堆中的。
堆一般会被分成年轻代和老年代。而年轻代又会被进一步分为1个Eden区和2个Survivor区。在内存分配上,如果保持默认配置的话,年轻代和老年代的内存大小比例为1 : 2,年轻代中的1个Eden区和2个Survivor区的内存大小比例为:8 : 1 : 1。
子主题
垃圾回收
GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停
(1)对新生代的对象的收集称为minor GC;
(2)对旧生代的对象的收集称为Full GC;
(3)程序中主动调用System.gc()强制执行的GC为Full GC。
(2)对旧生代的对象的收集称为Full GC;
(3)程序中主动调用System.gc()强制执行的GC为Full GC。
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
(3)弱引用:在GC时一定会被GC回收
(4)虚引用:由于虚引用只是用来得知对象是否被GC
(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
(3)弱引用:在GC时一定会被GC回收
(4)虚引用:由于虚引用只是用来得知对象是否被GC
垃圾收集算法
标记-清除算法
最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有标记的对象。
它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
复制算法
为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。
标记-整理算法
复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。
这样就可以根据各个年代的特点采用不同的收集算法。
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
这样就可以根据各个年代的特点采用不同的收集算法。
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
虚拟机栈
栈一般又叫作线程栈或虚拟机栈,一般存储的是局部变量。在Java中,每个线程都会有一个单独的栈区,每个栈中的元素都是私有的,不会被其他的栈所访问。栈中的数据大小和生存期都是确定的,存取速度比较快。
在Java中,所有的基本数据类型(byte、short、int、long、float、double、boolean、char)和引用变量(对象引用)都是在栈中的。一般情况下,线程退出或者方法退出时,栈中的数据会被自动清除。
程序在执行过程中,会在栈中为不同的方法创建不同的栈帧,在栈帧中又包含了:局部变量表、操作数栈、动态链接和方法出口。
栈中一般会存储对象的引用,这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。
注意:这里说的是这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。
因为在JVM中,如果开启了逃逸分析和标量替换,则可能不会再在堆上创建对象,可能会将对象直接分配到栈上,也可能不再创建对象,而是进一步分解对象中的成员变量,将其直接在栈上分配空间并赋值。
注意:这里说的是这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。
因为在JVM中,如果开启了逃逸分析和标量替换,则可能不会再在堆上创建对象,可能会将对象直接分配到栈上,也可能不再创建对象,而是进一步分解对象中的成员变量,将其直接在栈上分配空间并赋值。
方法区(官方叫做永久代)
程序计数器
程序计数器也叫作PC计数器,只要存储的是下一条将要执行的命令的地址。
本地方法栈
本地方法栈相对来说比较简单,就是保存native方法进入区域的地址。
例如,在Java中创建线程,调用Thread对象的start()方法时,会通过本地方法start0()调用操作系统创建线程的方法。此时,本地方法栈就会保存start0()方法进入区域的内存地址。
例如,在Java中创建线程,调用Thread对象的start()方法时,会通过本地方法start0()调用操作系统创建线程的方法。此时,本地方法栈就会保存start0()方法进入区域的内存地址。
Java8
图
把方法区永久删除了,取而代之的是元空间,使用的是直接内存
方法区(元空间)
区域是JDK1.8中划分出来的。主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应的Class实例的引用等信息。
方法区中的信息能够被多个线程共享。
方法区中的信息能够被多个线程共享。
例如,在程序中声明的常量、静态变量和有关于类的信息等的引用,都会存放在方法区,而这些引用所指向的具体对象 一般都会在堆中开辟单独的空间进行存储,也可能会在直接内存中进行存储。
对比
各版本常量池的位置
Java6和6之前,常量池是存放在方法区(永久代)中的。
Java7,将常量池是存放到了堆中。
Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间(方法区)中,而字符串常量池依然存放在堆中。
Java7,将常量池是存放到了堆中。
Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间(方法区)中,而字符串常量池依然存放在堆中。
移除永久代前后调整方法区大小所设置的参数
jdk1.8之前
-XX:PermSize=N // 方法区 (永久代) 初始大小
-XX:MaxPermSize=N // 方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
-XX:MaxPermSize=N // 方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
jdk1.8之后
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
总结
看似好像没多大区别,其实元空间和永久代一个很大不同是如果不指定大小,随着更多类的创建,虚拟机会耗光所有可用的系统内存。
整个永久代有一个JVM本身设置的固定大小上下限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,然后元空间仍可能存在溢出,但几率比之前小了很多
本地接口
调优
参数
-Xss:每个线程的栈大小
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
优化
日均百万级订单交易系统如何设置JVM参数
思想
就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。
让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc
调优工具
Jmap
查看内存信息,实例个数以及占用内存大小
jmap -histo 14660 #查看历史生成的实例
jmap -histo:live 14660 #查看当前存活的实例,执行过程中可能会触发一次full gc
jmap -histo:live 14660 #查看当前存活的实例,执行过程中可能会触发一次full gc
num:序号
instances:实例数量
bytes:占用空间大小
class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
instances:实例数量
bytes:占用空间大小
class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
堆内存dump
jmap -dump:format=b,file=eureka.hprof 14660
用jvisualvm命令工具导入该dump文件分析
Jstack
用jstack加进程id查找死锁
子主题
找出占用cpu最高的线程堆栈信息
使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
1,使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
2,按H,获取每个线程的内存情况
3,找到内存和cpu占用最高的线程tid,比如19664
4,转为十六进制得到 0x4cd0,此为线程id的十六进制表示
5,执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
2,按H,获取每个线程的内存情况
3,找到内存和cpu占用最高的线程tid,比如19664
4,转为十六进制得到 0x4cd0,此为线程id的十六进制表示
5,执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
Jinfo
查看正在运行的Java应用程序的扩展参数
Jstat
查看堆内存各部分的使用量,以及加载类的数量
Arthas
内存泄露到底是怎么回事
一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。
对象创建的主要流程:
图
简单说loadClass的类加载过程有如下几步
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的
main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口
main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证:校验字节码文件的正确性
准备:给类的静态变量分配内存,并赋予默认值
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如
main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过
程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过
程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
初始化:对类的静态变量初始化为指定的值,执行静态代码块
类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的
引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的
对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。
jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的
引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的
对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。
jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
图
Java中创建的对象是存储在JVM中的哪个区域的?
不考虑JVM的逃逸分析情况,新创建的对象是存放在JVM堆空间中年轻代的Eden区
对象“已死”的判定算法
由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。
而Java堆和方法区则不同,线程共享,是GC的所关注的部分
而Java堆和方法区则不同,线程共享,是GC的所关注的部分
在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收
有两种算法可以判定对象是否存活
引用计数算法(计数器)
给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。
可达性分析算法(有向图)
通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
异常
图
Error(错误)
系统中的错误,是在程序编译时出现的错误,只能通过修改程序才能修正。
一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。
一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。
Exception(异常)
表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
子类
RuntimeException运行时异常
派生于RuntimeException的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。 因此由系统自动检测并将它们交给缺省的异常处理程序(用户可不必对其处理)。
这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常,经常需要通过增加“逻辑处理来避免这些异常”。
CheckedException已检查异常
所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,
如IOException、SQLException等以及用户自定义的Exception异常。
如IOException、SQLException等以及用户自定义的Exception异常。
这类异常在编译时就必须做出处理,否则无法通过编译。
异常的处理方式有两种
“try/catch”捕获异常
“throws”声明异常
当CheckedException产生时,不一定立刻处理它,可以再把异常throws出去。
方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。
自定义异常
在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
自定义异常类只需从Exception类或者它的子类派生一个子类即可。
自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。
习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
demo
自定义异常类
自定义异常类的使用
使用异常机制的建议
1.要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
2.处理异常不可以代替简单测试---只在异常情况下使用异常机制。
3.不要进行小粒度的异常处理---应该将整个任务包装在一个try语句块中。
4.异常往往在高层处理
2.处理异常不可以代替简单测试---只在异常情况下使用异常机制。
3.不要进行小粒度的异常处理---应该将整个任务包装在一个try语句块中。
4.异常往往在高层处理
Spring
Spring 框架
依赖注入DI(Dependency Injecttion)
控制反转IOC(Inversion Of Control)
Bean的生命周期
SpringMVC
简化开发
SpringBoot
常用注解
HTTP协议的四种传参方式
图
@RequestBody
修饰请求参数,注解用于接收HTTP的body,默认是使用JSON的格式
@ResponseBody
修饰返回值,注解用于在HTTP的body中携带响应数据,默认是使用JSON的格式。如果不加该注解,spring响应字符串类型,是跳转到模板页面或jsp页面的开发模式。说白了:加上这个注解你开发的是一个数据接口,不加这个注解你开发的是一个页面跳转控制器。
@RequestMapping
注解是所有常用注解中,最有看点的一个注解,用于标注HTTP服务端点。
value: 应用请求端点,最核心的属性,用于标志请求处理方法的唯一性;
method: HTTP协议的method类型, 如:GET、POST、PUT、DELETE等;
consumes: HTTP协议请求内容的数据类型(Content-Type),例如application/json, text/html;
produces: HTTP协议响应内容的数据类型。
params: HTTP请求中必须包含某些参数值的时候,才允许被注解标注的方法处理请求。
headers: HTTP请求中必须包含某些指定的header值,才允许被注解标注的方法处理请求。
method: HTTP协议的method类型, 如:GET、POST、PUT、DELETE等;
consumes: HTTP协议请求内容的数据类型(Content-Type),例如application/json, text/html;
produces: HTTP协议响应内容的数据类型。
params: HTTP请求中必须包含某些参数值的时候,才允许被注解标注的方法处理请求。
headers: HTTP请求中必须包含某些指定的header值,才允许被注解标注的方法处理请求。
PostMapping等同于@RequestMapping的method等于POST。同理:@GetMapping、@PutMapping、@DeleteMapping也都是简写的方式
@RestController与@Controller
@Controller
开发中最常使用的注解,它的作用有两层含义
告诉Spring,被该注解标注的类是一个Spring的Bean,需要被注入到Spring的上下文环境中
该类里面所有被RequestMapping标注的注解都是HTTP服务端点
@RestController
相当于 @Controller和@ResponseBody结合。它有两层含义
作为Controller的作用,将控制器类注入到Spring上下文环境,该类RequestMapping标注方法为HTTP服务端点。
作为ResponseBody的作用,请求响应默认使用的序列化方式是JSON,而不是跳转到jsp或模板页面。
@PathVariable 与@RequestParam
@PathVariable
用于URI上的{参数},请求URL为“/article/1”,那么将匹配DeleteMapping并且PathVariable接收参数id=1。
DeleteMapping("/article/{id}")
public @ResponseBody AjaxResponse deleteArticle(@PathVariable Long id) {
public @ResponseBody AjaxResponse deleteArticle(@PathVariable Long id) {
@RequestParam
用于接收普通表单方式或者ajax模拟表单提交的参数数据。
@PostMapping("/article")
public @ResponseBody AjaxResponse deleteArticle(@RequestParam Long id) {
public @ResponseBody AjaxResponse deleteArticle(@RequestParam Long id) {
@RequestBody
RequestParam只能接收平面的、一对一的参数
接收复杂嵌套对象
@Requestpart
用于接收HTTP请求中的文件参数,通常用于文件上传功能。
使用该注解时,需要在方法参数中声明MultipartFile类型的参数,Spring MVC框架会自动将上传的文件转换为MultipartFile类型的对象
使用该注解时,需要在方法参数中声明MultipartFile类型的参数,Spring MVC框架会自动将上传的文件转换为MultipartFile类型的对象
lombok的@Slf4j注解
JSON数据处理
Jackson
SpringBoot默认
常用注解
这些注解通常用于标注java实体类或实体类的属性
这些注解通常用于标注java实体类或实体类的属性
@JsonPropertyOrder(value={"pname1","pname2"})
改变子属性在JSON序列化中的默认定义的顺序。如:param1在先,param2在后。
@JsonIgnore
排除某个属性不做序列化与反序列化
@JsonProperty(anotherName)
为某个属性换一个名称,体现在JSON数据里面
@JsonInclude(JsonInclude.Include.NON_NULL)
排除为空的元素不做序列化反序列化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
指定日期类型的属性格式
通常会对日期类型转换,进行全局配置,而不是在每一个java bean里面配置
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
手动数据转换
/jackson的ObjectMapper 转换对象
ObjectMapper mapper = new ObjectMapper();
//将某个java对象转换为JSON字符串
String jsonStr = mapper.writeValueAsString(javaObj);
//将jsonStr转换为Ademo类的对象
Ademo ademo = mapper.readValue(jsonStr, Ademo.class);
ObjectMapper mapper = new ObjectMapper();
//将某个java对象转换为JSON字符串
String jsonStr = mapper.writeValueAsString(javaObj);
//将jsonStr转换为Ademo类的对象
Ademo ademo = mapper.readValue(jsonStr, Ademo.class);
当JSON字符串代表的对象的字段多于类定义的字段时,使用readValue会抛出UnrecognizedPropertyException异常,在类的定义处加上@JsonIgnoreProperties(ignoreUnknown = true)可以解决这个问题。
全局配置
配置文件的方式
通过代码的方式
FastJSON
阿里巴巴
Gson
junit测试框架
junit4和junit5中,注解的写法有些许变化
图
swagger3
图
配置管理
装载顺序
@ConditionOnXXXXXXX
全局配置文件
application.properties
application.yml
SpringBoot入口启动类使用了SpringBootApplication,实际上就是开启了自动配置功能@EnableAutoConfiguration
获取配置值
@ConfigurationProperties
@Value
比较
注意:
1.对象需要成为Bean对象,使用注解@Component
2.需要@Autowired,new对象是没有值的
1.对象需要成为Bean对象,使用注解@Component
2.需要@Autowired,new对象是没有值的
SpEL结合 @Value注解读取系统环境变量
获取JAVA_HOME目录
@Value ("#{systemProperties['java.home']}")
private String javaHome;
private String javaHome;
获取系统用户工作目录
@Value ("#{systemProperties['user.dir']}")
private String userDir;
private String userDir;
https://docs.spring.io/spring-framework/docs/4.3.10.RELEASE/spring-framework-reference/html/expressions.html
profile不同环境使用不同配置
全局配置文件:application.yml
开发环境配置文件:application-dev.yml
测试环境配置文件:application-test.yml
生产环境配置文件:application-prod.yml
开发环境配置文件:application-dev.yml
测试环境配置文件:application-test.yml
生产环境配置文件:application-prod.yml
切换环境的方式
通过配置application.yml
spring.profiles.active
加载优先级
命令行参数
来自java:comp/env的JNDI属性
Java系统属性(System.getProperties())
操作系统环境变量
RandomValuePropertySource配置的random.*属性值
jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件
jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件
jar包外部的application.properties或application.yml(不带spring.profile)配置文件
jar包内部的application.properties或application.yml(不带spring.profile)配置文件
@Configuration注解类上的@PropertySource
通过SpringApplication.setDefaultProperties指定的默认属性
来自java:comp/env的JNDI属性
Java系统属性(System.getProperties())
操作系统环境变量
RandomValuePropertySource配置的random.*属性值
jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件
jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件
jar包外部的application.properties或application.yml(不带spring.profile)配置文件
jar包内部的application.properties或application.yml(不带spring.profile)配置文件
@Configuration注解类上的@PropertySource
通过SpringApplication.setDefaultProperties指定的默认属性
file:./config/ (当前项目路径config目录下);
file:./ (当前项目路径下);
classpath:/config/ (类路径config目录下);
classpath:/ (类路径下).
file:./ (当前项目路径下);
classpath:/config/ (类路径config目录下);
classpath:/ (类路径下).
图
也可以通过配置spring.config.location来改变默认配置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置
https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-external-config
配置文件敏感字段加密
Jasypt
防君子不防小人
整合数据库
使用jdbc操作数据库
加载数据库驱动
建立数据库连接
创建数据库操作对象
定义操作的 SQL 语句
执行数据库操作
获取并操作结果集
关闭对象,回收资源
建立数据库连接
创建数据库操作对象
定义操作的 SQL 语句
执行数据库操作
获取并操作结果集
关闭对象,回收资源
JdbcTemplate
主流ORM持久层框架选型
JPA(不推荐)
实体Model类
@Entity 必选注解,表示这个类是一个实体类,接受JPA控制管理,对应数据库中的一个表
@Table 可选注解,指定这个类对应数据库中的表名。如果这个类名和数据库表名符合驼峰及下划线规则,可以省略这个注解。如FlowType类名对应表名flow_type。
@Id 指定这个字段为表的主键
@GeneratedValue(strategy=GenerationType.IDENTITY) 指定主键的生成方式,一般主键为自增的话,就采用GenerationType.IDENTITY的生成方式
@Column 注解针对一个字段,对应表中的一列。nullable = false表示数据库字段不能为空, unique = true表示数据库字段不能有重复值,length = 32表示数据库字段最大程度为32.
@Table 可选注解,指定这个类对应数据库中的表名。如果这个类名和数据库表名符合驼峰及下划线规则,可以省略这个注解。如FlowType类名对应表名flow_type。
@Id 指定这个字段为表的主键
@GeneratedValue(strategy=GenerationType.IDENTITY) 指定主键的生成方式,一般主键为自增的话,就采用GenerationType.IDENTITY的生成方式
@Column 注解针对一个字段,对应表中的一列。nullable = false表示数据库字段不能为空, unique = true表示数据库字段不能有重复值,length = 32表示数据库字段最大程度为32.
https://docs.jboss.org/hibernate/annotations/3.4/reference/zh_cn/html_single/#entity
数据操作接口
public interface ArticleRepository extends JpaRepository<Article,Long> {
}
}
service层接口实现
//注意这个方法的名称,jPA会根据方法名自动生成SQL执行
Article findByAuthor(String author);
Article findByAuthor(String author);
使用方法和生产成 SQL 如下表所示
图
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference
等等
Mybatis
Mybatis Genenrator
Mybatis-plus
PageHelper分页插件
查询结果属性映射的最佳实践
mybatis:
configuration:
mapUnderscoreToCamelCase: true
configuration:
mapUnderscoreToCamelCase: true
实体类属性userName对应SQL的字段user_name;
实体类属性userId对应SQL的字段user_id;
实体类属性userId对应SQL的字段user_id;
其他的实现方式都很不友好,都需要写一个查询SQL,做一套映射配置
第一种:xml的属性映射举例resultMap
<mapper namespace="data.UserMapper">
<resultMap type="data.User" id="userResultMap">
<!-- 用id属性来映射主键字段 -->
<id property="id" column="user_id"/>
<!-- 用result属性来映射非主键字段 -->
<result property="userName" column="user_name"/>
</resultMap>
</mapper>
<resultMap type="data.User" id="userResultMap">
<!-- 用id属性来映射主键字段 -->
<id property="id" column="user_id"/>
<!-- 用result属性来映射非主键字段 -->
<result property="userName" column="user_name"/>
</resultMap>
</mapper>
第二种:通过注解 @Results 和 @Result
@Select("select * from t_user where user_name = #{userName}")
@Results(
@Result(property = "userId", column = "user_id"),
@Result(property = "userName", column = "user_name")
)
User getUserByName(@Param("userName") String userName);
@Results(
@Result(property = "userId", column = "user_id"),
@Result(property = "userName", column = "user_name")
)
User getUserByName(@Param("userName") String userName);
第三种:通过SQL字段别名来完成映射
@Select("select user_name as userName, user_id as userId from t_user where user_name = #{userName}")
User getUserByName(@Param("userName") String userName);
User getUserByName(@Param("userName") String userName);
使用@MapperScan
这样就会自动扫描com.zimug.**.mapper目录下面的所有XXXXMapper文件,并且完成自动注入。
不需要在每一个Mapper上面都加@Mapper注解
不需要在每一个Mapper上面都加@Mapper注解
对比选型
图
事务管理
Spring中七种事务传播行为
@Trasactional
多个数据源
分布式事务
跨库
java bean的赋值转换
图
如果你的业务,可以用一个实体类对象,就可以贯穿持久层到展现层,就没有必要做映射赋值转换,也没有必要去分VO、BO、PO。比如:单表表格数据展现、修改、新增
转换工具
专指Spring包下面的BeanUtils
ArticleVO articleVO = new ArticleVO();
BeanUtils.copyProperties(article,articleVO);
BeanUtils.copyProperties(article,articleVO);
Dozer
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
// article(PO) -> articleVO
ArticleVO articleVO = mapper .map(article, ArticleVO.class);
// article(PO) -> articleVO
ArticleVO articleVO = mapper .map(article, ArticleVO.class);
实现rest接口的最快方式
Spring Data REST
在JPA(mogodb等)Repository接口上面加上RepositoryRestResource注解,path是Rest接口资源的基础访问路径
接口文档
OpenAPI
一键生成数据库文档
screw(螺丝钉)
静态资源
默认配置
/static: classpath:/static/
/public: classpath:/public/
/resources: classpath:/resources/
/META-INF/resources:classpath:/META-INF/resources/
/public: classpath:/public/
/resources: classpath:/resources/
/META-INF/resources:classpath:/META-INF/resources/
可以通过spring.resources.static-locations配置指定静态文件的位置。
但是要特别注意,一旦自己指定了静态资源目录,系统默认的静态资源目录就会失效。
所以系统默认的就已经足够使用了,尽量不要自定义
但是要特别注意,一旦自己指定了静态资源目录,系统默认的静态资源目录就会失效。
所以系统默认的就已经足够使用了,尽量不要自定义
favicon.ico图标
如果在配置的静态资源目录中有favicon.ico文件,SpringBoot会自动将其设置为应用图标。
欢迎页面
SpringBoot支持静态和模板欢迎页,它首先在静态资源目录查看index.html文件做为首页,若未找到则查找index模板。
WebJars管理css&js
依赖
测试
引入 webjars-locator 值后可以省略版本号
模板引擎选型
jsp
(糅合三种元素:Java代码、动态的数据、HTML代码结构)
(糅合三种元素:Java代码、动态的数据、HTML代码结构)
java模板引擎
(数据模型与业务代码分离)
(数据模型与业务代码分离)
freemarker
Thymeleaf
velocity
前端工程化
前后端分离技术
VUE、angularjs、reactjs
Servlet域对象与属性变化监听
Servlet 监听器
观察者模式
关注特定事物的创建、销毁以及变化并做出回调动作
具有异步的特性
监听三大域对象的创建和销毁事件
ServletContext Listener:application 级别,整个应用只存在一个,所有用户使用一个ServletContext
HttpSession Listener:session 级别,同一个用户的浏览器开启与关闭生命周期内使用的是同一个session
ServletRequest Listener:request 级别,每一个HTTP请求为一个request
HttpSession Listener:session 级别,同一个用户的浏览器开启与关闭生命周期内使用的是同一个session
ServletRequest Listener:request 级别,每一个HTTP请求为一个request
图
还可以监听域对象中属性发生修改的事件
HttpSessionAttributeListener
ServletContextAttributeListener
ServletRequestAttributeListener
ServletContextAttributeListener
ServletRequestAttributeListener
实现
以用来统计在线人数和在线用户、统计网站访问量、系统启动时初始化信息等
全局Servlet组件扫描注解
在启动类中加入@ServletComponentScan进行自动注册
Servlet 过滤器
目的
在客户端的请求访问后端资源之前,拦截这些请求。
在服务器的响应发送回客户端之前,处理这些响应。
在服务器的响应发送回客户端之前,处理这些响应。
场景
基于一定的授权逻辑,对HTTP请求进行过滤,从而保证数据访问的安全。比如:判断请求的来源IP是否在系统黑名单中
对于一些经过加密的HTTP请求数据,进行统一解密,方便后端资源进行业务处理
或者我们社交应用经常需要的敏感词过滤,也可以使用过滤器,将触发敏感词的非法请求过滤掉
对于一些经过加密的HTTP请求数据,进行统一解密,方便后端资源进行业务处理
或者我们社交应用经常需要的敏感词过滤,也可以使用过滤器,将触发敏感词的非法请求过滤掉
特点
可以过滤所有请求
能够改变请求的数据内容
实现
图
方式一:利用WebFilter注解配置
@WebFilter时Servlet3.0新增的注解,原先实现过滤器,需要在web.xml中进行配置,而现在通过此注解,启动启动时会自动扫描自动注册
编写Filter类
在启动类加入@ServletComponentScan注解
注意
无法指定过滤器的先后执行顺序
通过过滤器的java类名称,进行顺序的约定,比如LogFilter和AuthFilter,此时AuthFilter就会比LogFilter先执行,因为首字母A比L前面
FilterRegistrationBean可以
方式二:FilterRegistrationBean
Filter的注册代码
原始servlet进行开发
@WebServlet
拦截器Interceptor
图
三个方法
preHandle 表示被拦截的URL对应的控制层方法,执行前的自定义处理逻辑
postHandle 表示被拦截的URL对应的控制层方法,执行后的自定义处理逻辑,此时还未将modelAndView进行页面渲染。
afterCompletion 表示此时modelAndView已做页面渲染,执行拦截器的自定义处理。
postHandle 表示被拦截的URL对应的控制层方法,执行后的自定义处理逻辑,此时还未将modelAndView进行页面渲染。
afterCompletion 表示此时modelAndView已做页面渲染,执行拦截器的自定义处理。
拦截器与过滤器的核心区别
作用是类似的
场景有一些区别
规范不同:Filter是在Servlet规范中定义的组件,在servlet容器内生效。而拦截器是Spring框架支持的,在Spring 上下文中生效。
拦截器可以获取并使用Spring IOC容器中的bean,但过滤器就不行。因为过滤器是Servlet的组件,而IOC容器的bean是Spring框架内使用,拦截器恰恰是Spring框架内衍生出来的。
拦截器可以访问Spring上下文值对象,如ModelAndView,过滤器不行。基于与上一点同样的原因。
过滤器在进入servlet容器之前处理请求,拦截器在servlet容器之内处理请求。过滤器比拦截器的粒度更大,比较适合系统级别的所有API的处理动作。比如:权限认证,Spring Security就大量的使用了过滤器。
拦截器相比于过滤器粒度更小,更适合分模块、分范围的统一业务逻辑处理。比如:分模块的、分业务的记录审计日志。(使用拦截器实现统一访问日志的记录)
比如说:我们在Filter中使用注解,注入一个测试service,结果为null。因为过滤器无法使用Spring IOC容器bean
图
实现
实现
HandlerInterceptor
继承WebMvcConfigurerAdapter注册拦截器
(WebMvcConfigurerAdapter类已经被废弃,请实现WebMvcConfigurer接口完成拦截器的注册)
(WebMvcConfigurerAdapter类已经被废弃,请实现WebMvcConfigurer接口完成拦截器的注册)
子主题
请求链路
通过输出结果分析一下拦截器、过滤器中各接口函数的执行顺序
图
CustomFilter : customFilter 请求处理之前----doFilter方法之前过滤请求
CustomHandlerInterceptor : preHandle:请求前调用
CustomHandlerInterceptor : postHandle:请求后调用
CustomHandlerInterceptor : afterCompletion:请求调用完成后回调方法,即在视图渲染完成后回调
CustomFilter : customFilter 请求处理之后----doFilter方法之后处理响应
CustomHandlerInterceptor : preHandle:请求前调用
CustomHandlerInterceptor : postHandle:请求后调用
CustomHandlerInterceptor : afterCompletion:请求调用完成后回调方法,即在视图渲染完成后回调
CustomFilter : customFilter 请求处理之后----doFilter方法之后处理响应
自定义事件的发布与监听
事件监听
实现事件监听机制有很多方法
使用消息队列中间件的发布订阅模式
JDK自带的java.util.EventListener
Spring环境下的实现事件发布监听的方法
1.写代码向ApplicationContext中添加监听器
2.使用Component注解将监听器装载入spring容器
3.在application.properties中配置监听器
4.通过@EventListener注解实现事件监听(推荐)
2.使用Component注解将监听器装载入spring容器
3.在application.properties中配置监听器
4.通过@EventListener注解实现事件监听(推荐)
...
应用启动的监听
实现CommandLineRunner、ApplicationRunner接口
CommandLineRunner、ApplicationRunner的核心用法是一致的,就是用于应用启动前的特殊代码执行。
ApplicationRunner的执行顺序先于CommandLineRunner;
ApplicationRunner将参数封装成了对象,提供了获取参数名、参数值等方法,操作上会方便一些
ApplicationRunner的执行顺序先于CommandLineRunner;
ApplicationRunner将参数封装成了对象,提供了获取参数名、参数值等方法,操作上会方便一些
嵌入式容器的配置与应用
统一全局异常处理
@ControllerAdvice
@ControllerAdvice
开发规范
Controller、Service、DAO层拦截异常转换为自定义异常,不允许将异常私自截留。必须对外抛出
统一数据响应代码,使用http状态码,不要自定义。自定义不方便记忆,HTTP状态码程序员都知道。
但是太多了程序员也记不住,在项目组规定范围内使用几个就可以。
比如:200请求成功,400用户输入错误导致的异常,500系统内部异常,999未知异常
但是太多了程序员也记不住,在项目组规定范围内使用几个就可以。
比如:200请求成功,400用户输入错误导致的异常,500系统内部异常,999未知异常
自定义异常里面有message属性,用对用户友好的语言描述异常的发生情况,并赋值给message.
不允许对父类Excetion统一catch,要分小类catch,这样能够清楚地将异常转换为自定义异常传递给前端
数据结构
CustomException 自定义异常。核心要素包含异常错误编码(400,500)、异常错误信息message。
ExceptionTypeEnum 枚举异常分类,将异常分类固化下来,防止开发人员思维发散。
AjaxResponse 用于响应HTTP 请求的统一数据结构。
当请求成功的情况下,可以使用AjaxResponse.success()构建返回结果给前端。
当查询请求等需要返回业务数据,请求成功的情况下,可以使用AjaxResponse.success(data)构建返回结果给前端。携带结果数据。
当请求处理过程中发生异常,需要将异常转换为CustomException ,然后在控制层使用AjaxResponse error(CustomException)构建返回结果给前端。
在某些情况下,没有任何异常产生,我们判断某些条件也认为请求失败。这种使用AjaxResponse error(customExceptionType,errorMessage)构建响应结果。
当查询请求等需要返回业务数据,请求成功的情况下,可以使用AjaxResponse.success(data)构建返回结果给前端。携带结果数据。
当请求处理过程中发生异常,需要将异常转换为CustomException ,然后在控制层使用AjaxResponse error(CustomException)构建返回结果给前端。
在某些情况下,没有任何异常产生,我们判断某些条件也认为请求失败。这种使用AjaxResponse error(customExceptionType,errorMessage)构建响应结果。
通用异常处理逻辑
捕获异常,并将异常转换为自定义异常。使用用户友好的信息去填充CustomException的message,并将CustomException抛出去
全局异常处理器
@ControllerAdvice
作用
监听所有的Controller,一旦Controller抛出CustomException,
就会在@ExceptionHandler(CustomException.class)注解的方法里面对该异常进行处理。
处理方法很简单就是使用AjaxResponse.error(e)包装为通用的接口数据结构返回给前端
就会在@ExceptionHandler(CustomException.class)注解的方法里面对该异常进行处理。
处理方法很简单就是使用AjaxResponse.error(e)包装为通用的接口数据结构返回给前端
业务状态与HTTP协议状态一致
(让业务状态与HTTP协议Response状态码一致)
(让业务状态与HTTP协议Response状态码一致)
实现ResponseBodyAdvice 接口的作用是:在将数据返回给用户之前,做最后一步的处理。
也就是说,ResponseBodyAdvice 的处理过程在全局异常处理的后面
也就是说,ResponseBodyAdvice 的处理过程在全局异常处理的后面
最终代码
服务端数据校验异常处理逻辑
参数合法性校验
java的JSR 303: Bean Validation规范
JSR 303只是个规范,并没有具体的实现,目前通常都是才有hibernate-validator进行统一参数校验
JSR303定义的校验类型
图
Hibernate Validator 附加的 constraint
图
当用户输入参数不符合注解给出的校验规则的时候,
会抛出BindException或MethodArgumentNotValidException
会抛出BindException或MethodArgumentNotValidException
Assert断言与IllegalArgumentException
友好的数据校验异常处理(用户输入异常的全局处理)
对这两种异常(BindException或MethodArgumentNotValidException)
做全局处理,防止重复编码
做全局处理,防止重复编码
使用org.springframework.util.Assert断言,
如果不满足条件就抛出IllegalArgumentException
如果不满足条件就抛出IllegalArgumentException
AOP完美处理页面跳转异常
非前后端分离的应用
用面向切面的方式,将Exception转换为ModelAndViewException。
全局异常处理器拦截ModelAndViewException,返回ModelAndView,即error.html页面
切入点是带@ModelView注解的Controller层方法
全局异常处理器拦截ModelAndViewException,返回ModelAndView,即error.html页面
切入点是带@ModelView注解的Controller层方法
日志
框架
JDK java.util.logging
log4j
过时的函数库,已经停止更新,不推荐使用,相比之下,性能和功能也是最差的
Logback
Spring Boot 默认的日志记录框架使用的是 Logback
性能上还是不及 Log4j2,因此,在现阶段,日志记录首选 Log4j2
配置
一、application配置文件实现日志配置
logging.level.root=info指定整个系统的默认日志级别是info,日志级别统一化
logging.level.com.zimug.boot.launch.controller=debug,指定某个特定的package的日志级别是debug,日志级别个性化。优先级角度,个性配置大于统一配置。
logging.file.path将日志输出到指定目录,如果不指定logging.file.name,日志文件的默认名称是spring.log。配置了logging.file.name之后,logging.file.path配置失效。
无论何种设置,Spring Boot都会自动按天分割日志文件,也就是说每天都会自动生成一个新的log文件,而之前的会自动打成GZ压缩包。# 日志文件大小
可以设置logging.file.max-size=10MB分割的每个日志的文件最大容量,超过这个size之后日志继续分隔。
可以设置保留的日志时间logging.file.max-history=10,以天为单位
logging.pattern.file输出到文件中的日志的格式
logging.pattern.console控制台输出日志的格式,为了在控制台调试时候显示效果更清晰,为日志增加了颜色。red、green等等
logging.level.com.zimug.boot.launch.controller=debug,指定某个特定的package的日志级别是debug,日志级别个性化。优先级角度,个性配置大于统一配置。
logging.file.path将日志输出到指定目录,如果不指定logging.file.name,日志文件的默认名称是spring.log。配置了logging.file.name之后,logging.file.path配置失效。
无论何种设置,Spring Boot都会自动按天分割日志文件,也就是说每天都会自动生成一个新的log文件,而之前的会自动打成GZ压缩包。# 日志文件大小
可以设置logging.file.max-size=10MB分割的每个日志的文件最大容量,超过这个size之后日志继续分隔。
可以设置保留的日志时间logging.file.max-history=10,以天为单位
logging.pattern.file输出到文件中的日志的格式
logging.pattern.console控制台输出日志的格式,为了在控制台调试时候显示效果更清晰,为日志增加了颜色。red、green等等
日志格式占位符
%d{HH:mm:ss.SSS}:日志输出时间(red)
%thread:输出日志的进程名字,这在Web应用以及异步任务处理中很有用 (green)
%-5level:日志级别,并且使用5个字符靠左对齐 (highlight高亮蓝色)
%logger:日志输出类的名字 (boldMagenta粗体洋红色)
%msg:日志消息 (cyan蓝绿色)
%n:平台的换行符
%thread:输出日志的进程名字,这在Web应用以及异步任务处理中很有用 (green)
%-5level:日志级别,并且使用5个字符靠左对齐 (highlight高亮蓝色)
%logger:日志输出类的名字 (boldMagenta粗体洋红色)
%msg:日志消息 (cyan蓝绿色)
%n:平台的换行符
二、使用logback-spring.xml实现日志配置
需求复杂的话
生产环境输出到控制台和文件,一天一个文件,保留30天.
开发环境输出到控制台和打印sql(mybatis)输出,生产环境不打印这个信息
测试环境只输出到控制台。不输出到文件
打印Mybatis SQL,只需要把使用到Mybatis的package的日志级别调整为DEBUG,就可以将SQL打印出来。
前提:项目已经支持application.yml的profile多环境配置
开发环境输出到控制台和打印sql(mybatis)输出,生产环境不打印这个信息
测试环境只输出到控制台。不输出到文件
打印Mybatis SQL,只需要把使用到Mybatis的package的日志级别调整为DEBUG,就可以将SQL打印出来。
前提:项目已经支持application.yml的profile多环境配置
实现
因为logback是spring boot的默认日志框架,所以不需要引入maven依赖,直接上logback-spring.xml放在resources下面
异步日志配置
异步日志queueSize 默认值256,异步日志队列的容量。
discardingThreshold:当异步日志队列的剩余容量小于这个阈值,会丢弃TRACE, DEBUG or INFO级别的日志。如果不希望丢弃日志(即全量保存),那可以设置为0。但是当队列占满后,非阻塞的异步日志会变成阻塞的同步日志。所以在高并发低延迟要求的系统里面针对不重要的日志可以设置discardingThreshold丢弃策略,值大于0
discardingThreshold:当异步日志队列的剩余容量小于这个阈值,会丢弃TRACE, DEBUG or INFO级别的日志。如果不希望丢弃日志(即全量保存),那可以设置为0。但是当队列占满后,非阻塞的异步日志会变成阻塞的同步日志。所以在高并发低延迟要求的系统里面针对不重要的日志可以设置discardingThreshold丢弃策略,值大于0
log4j2
一、引入maven依赖
log4j是之前使用比较广泛的软件,容易与log4j2发生冲突,如果冲突将log4j从相应的软件里面排除掉,比如:dozer
二、添加配置文件log4j2-spring.xml
在resources目录下新建一个log4j2-spring.xml文件,放在src/main/resources目录下即可被Spring Boot应用识别
上文两个Appender,一个叫做CONSOLE用于输出日志到控制台,一个叫做FILE-APPENDER输出日志到文件
PatternLayout用于指定输出日志的格式,[%d][%p][%t][%C] %m%n 这些占位符将结合下文测试结果为大家介绍
Policies用于指定文件切分参数
TimeBasedTriggeringPolicy默认的size是1,结合filePattern定义%d{yyyy-MM-dd},则每天生成一个文件(最小的时间切分粒度是小时)
<SizeBasedTriggeringPolicy size="100 MB"/> 当文件大小到100MB的时候,切分一个新的日志文件
<DefaultRolloverStrategy max="20"/>表示文件最大的存档数量,多余的将被删除
上文中的日志格式占位符号
PatternLayout用于指定输出日志的格式,[%d][%p][%t][%C] %m%n 这些占位符将结合下文测试结果为大家介绍
Policies用于指定文件切分参数
TimeBasedTriggeringPolicy默认的size是1,结合filePattern定义%d{yyyy-MM-dd},则每天生成一个文件(最小的时间切分粒度是小时)
<SizeBasedTriggeringPolicy size="100 MB"/> 当文件大小到100MB的时候,切分一个新的日志文件
<DefaultRolloverStrategy max="20"/>表示文件最大的存档数量,多余的将被删除
上文中的日志格式占位符号
占位符
<PatternLayout pattern="[%style{%d}{bright,green}][%highlight{%p}][%style{%t}{bright,blue}][%style{%C}{bright,yellow}]: %msg%n%style{%throwable}{red}"
disableAnsi="false" noConsoleNoAnsi="false"/>
disableAnsi="false" noConsoleNoAnsi="false"/>
%d : date时间
%p : 日志级别
%t : thread线程名称
%C: class类文件名称
%msg:日志信息
%n换行
%style{%throwable}{red} 加样式,异常信息红色显示
%p : 日志级别
%t : thread线程名称
%C: class类文件名称
%msg:日志信息
%n换行
%style{%throwable}{red} 加样式,异常信息红色显示
三、自定义配置文件
不同的环境使用不同的配置
比如:我们需要三个log4j2 xml文件:
log4j2-dev.xml 开发环境日志配置
log4j2-prod.xml 生产环境日志配置
log4j2-test.xml 测试环境日志配置
log4j2-dev.xml 开发环境日志配置
log4j2-prod.xml 生产环境日志配置
log4j2-test.xml 测试环境日志配置
在application-dev.yml里面使用log4j2-dev.xml配置文件
异步日志配置
一、引入disruptor
二、 全局异步模式
两种方式
在应用启动类里面使用System.setProperty
通过启动参数来设置全局异步日志
三、异步/同步混合模式
采用异步/同步混合模式不需要配置Log4jContextSelector
实现
在log4j2 xml里面对Loggers配置进行改造,加入AsyncLogger也就是异步日志,
只针对com.zimug.boot.launch包(假如已知这个包对处理性能要求比较高)
下的代码产生的日志采用异步模式,其他的日志仍然使用同步模式
只针对com.zimug.boot.launch包(假如已知这个包对处理性能要求比较高)
下的代码产生的日志采用异步模式,其他的日志仍然使用同步模式
日志门面
每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,
这就大大的增加应用程序代码对于日志框架的耦合性要求。
有了SLF4J这个门面之后,程序员永远都是面向SLF4J编程,
可以实现简单快速地替换底层的日志框架而不会导致业务代码需要做相应的修改
这就大大的增加应用程序代码对于日志框架的耦合性要求。
有了SLF4J这个门面之后,程序员永远都是面向SLF4J编程,
可以实现简单快速地替换底层的日志框架而不会导致业务代码需要做相应的修改
commons-logging
slf4j-api
一般的Java项目而言,日志框架会选择slf4j-api作为门面
引入Lombok
@Slf4j 注解来自动生成上面那个变量,默认的变量名是 log,
如果我们想采用惯用的 LOGGER 变量名,那么可以在工程的 main/java 目录中增加 lombok.config 文件,
并在文件中增加 lombok.log.fieldName=LOGGER 的配置项即可
如果我们想采用惯用的 LOGGER 变量名,那么可以在工程的 main/java 目录中增加 lombok.config 文件,
并在文件中增加 lombok.log.fieldName=LOGGER 的配置项即可
推荐的日志记录选型
SLF4J + Log4j2
日志级别
(trace<debug<info<warn<error<fatal)
(trace<debug<info<warn<error<fatal)
TRACE:追踪。一般上对核心系统进行性能调试或者跟踪问题时有用,此级别很低,一般上是不开启的,开启后日志会很快就打满磁盘的。
DEBUG:调试。这个大家应该不陌生了。开发过程中主要是打印记录一些运行信息之类的。
INFO:信息。这个是最常见的了,大部分默认就是这个级别的日志。一般上记录了一些交互信息,一些请求参数等等。可方便定位问题,或者还原现场环境的时候使用。此日志相对来说是比较重要的。
WARN:警告。这个一般上是记录潜在的可能会引发错误的信息。比如启动时,某某配置文件不存在或者某个参数未设置之类的。
ERROR:错误。这个也是比较常见的,一般上是在捕获异常时输出,虽然发生了错误,但不影响系统的正常运行。但可能会导致系统出错或是宕机等。
日志级别从小到大为trace<debug<info<warn<error<fatal,由于通常日志框架默认日志级别设置为INFO,因此1.3.小节中样例trace和debug级别的日志都看不到
DEBUG:调试。这个大家应该不陌生了。开发过程中主要是打印记录一些运行信息之类的。
INFO:信息。这个是最常见的了,大部分默认就是这个级别的日志。一般上记录了一些交互信息,一些请求参数等等。可方便定位问题,或者还原现场环境的时候使用。此日志相对来说是比较重要的。
WARN:警告。这个一般上是记录潜在的可能会引发错误的信息。比如启动时,某某配置文件不存在或者某个参数未设置之类的。
ERROR:错误。这个也是比较常见的,一般上是在捕获异常时输出,虽然发生了错误,但不影响系统的正常运行。但可能会导致系统出错或是宕机等。
日志级别从小到大为trace<debug<info<warn<error<fatal,由于通常日志框架默认日志级别设置为INFO,因此1.3.小节中样例trace和debug级别的日志都看不到
常见术语概念
appender:主要控制日志输出到哪里,比如:文件、数据库、控制台打印等
logger: 用来设置某一个包或者具体某一个类的日志打印级别、以及指定appender
root:也是一个logger,是一个特殊的父logger。所有的子logger最终都会将输出流交给root,除非在子logger中配置了additivity="false"。
rollingPolicy:所有日志都放在一个文件是不好的,所以可以指定滚动策略,按照一定周期或文件大小切割存放日志文件。
RolloverStrategy:日志清理策略。通常是指日志保留的时间。
异步日志:单独开一个线程做日志的写操作,达到不阻塞主线程的目的。
同步日志,主线程要等到日志写磁盘完成之后,才能继续向下执行
异步日志,主线程写日志只是将日志消息放入一个队列,之后就继续向下执行,这个过程是在内存层面完成的。之后由专门的线程从队列中获取日志数据写入磁盘,所以不阻塞主线程。主线程(核心业务代码)执行效率很高。
logger: 用来设置某一个包或者具体某一个类的日志打印级别、以及指定appender
root:也是一个logger,是一个特殊的父logger。所有的子logger最终都会将输出流交给root,除非在子logger中配置了additivity="false"。
rollingPolicy:所有日志都放在一个文件是不好的,所以可以指定滚动策略,按照一定周期或文件大小切割存放日志文件。
RolloverStrategy:日志清理策略。通常是指日志保留的时间。
异步日志:单独开一个线程做日志的写操作,达到不阻塞主线程的目的。
同步日志,主线程要等到日志写磁盘完成之后,才能继续向下执行
异步日志,主线程写日志只是将日志消息放入一个队列,之后就继续向下执行,这个过程是在内存层面完成的。之后由专门的线程从队列中获取日志数据写入磁盘,所以不阻塞主线程。主线程(核心业务代码)执行效率很高。
拦截器实现统一访问日志
需求
针对当前系统的每一次接口访问,要记录是什么人访问的(用户名)、什么时间访问的、访问耗时多长时间、
使用什么HTTP method方法访问的、访问结果如何等。可以称为审计日志。
将访问记录审计日志,输出到一个单独的日志文件access.log
使用什么HTTP method方法访问的、访问结果如何等。可以称为审计日志。
将访问记录审计日志,输出到一个单独的日志文件access.log
定义访问日志内容记录实体类
自定义日志拦截器
获取ip访问地址的工具类
拦截器注册
ACCESS-LOG"的日志Logger定义
图
LoggerFactory.getLogger("ACCESS-LOG") 代码去配置文件里面找一个name为ACCESS-LOG的Logger配置。
该Logger是一个AsyncLogger,指向的输出目标是ACCESS-APPENDER
ACCESS-APPENDER是一个日志文件输出配置,日志文件是access-log.log
该Logger是一个AsyncLogger,指向的输出目标是ACCESS-APPENDER
ACCESS-APPENDER是一个日志文件输出配置,日志文件是access-log.log
任务
异步任务
环境准备
在 Spring Boot 入口类上配置 @EnableAsync 注解开启异步处理
创建任务抽象类 AbstractTask,并分别配置三个任务方法 doTaskOne(),doTaskTwo(),doTaskThree()
同步调用
简单示例
定义 Task 类,继承 AbstractTask,三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10 秒内)
单元测试
任务一、任务二、任务三顺序的执行
异步调用
通过 异步调用 的方式来 并发执行
在方法上配置 @Async 注解,将原来的 同步方法 变为 异步方法
单元测试
注意:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效
异步回调
假设我们需要统计一下三个任务 并发执行 共耗时多少
使用 Future<T> 来返回 异步调用 的 结果
创建 AsyncCallBackTask 类,
声明 doTaskOneCallback(),
doTaskTwoCallback(),
doTaskThreeCallback() 三个方法,
对原有的三个方法进行包装
声明 doTaskOneCallback(),
doTaskTwoCallback(),
doTaskThreeCallback() 三个方法,
对原有的三个方法进行包装
单元测试
循环调用 Future 的 isDone() 方法等待三个 并发任务 执行完成,记录最终执行时间
为异步任务规划线程池
定义线程池
创建一个 线程池配置类TaskConfiguration ,并配置一个 任务线程池对象taskExecutor
参数
图
拒绝策略
AbortPolicy,用于被拒绝任务的处理程序,它将抛出RejectedExecutionException。
CallerRunsPolicy,用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
DiscardOldestPolicy,用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
DiscardPolicy,用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
CallerRunsPolicy,用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
DiscardOldestPolicy,用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
DiscardPolicy,用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
AsyncExecutorTask类,三个任务的配置和 AsyncTask 一样,不同的是 @Async 注解需要指定前面配置的 线程池的名称taskExecutor
单元测试
优雅地关闭线程池
由于在应用关闭的时候异步任务还在执行,导致类似 数据库连接池 这样的对象一并被 销毁了,当 异步任务 中对 数据库 进行操作就会出错
解决方案
重新设置线程池配置对象,新增线程池 setWaitForTasksToCompleteOnShutdown() 和 setAwaitTerminationSeconds() 配置
setWaitForTasksToCompleteOnShutdown(true): 该方法用来设置 线程池关闭 的时候 等待 所有任务都完成后,再继续 销毁 其他的 Bean,这样这些 异步任务 的 销毁 就会先于 数据库连接池对象 的销毁。
setAwaitTerminationSeconds(60): 该方法用来设置线程池中 任务的等待时间,如果超过这个时间还没有销毁就 强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
setAwaitTerminationSeconds(60): 该方法用来设置线程池中 任务的等待时间,如果超过这个时间还没有销毁就 强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
定时任务
通过@Scheduled实现定时任务
开启定时任务方法
入口main方法上加注解
@EnableScheduling //开启定时任务
不同定时方式的解析
fixedDelay和fixedRate,单位是毫秒
fixedRate
每隔多长时间执行一次。(开始------->X时间------>再开始)。
如果间隔时间小于任务执行时间,上一次任务执行完成下一次任务就立即执行。
如果间隔时间大于任务执行时间,就按照每隔X时间运行一次
如果间隔时间小于任务执行时间,上一次任务执行完成下一次任务就立即执行。
如果间隔时间大于任务执行时间,就按照每隔X时间运行一次
fixedDelay
当任务执行完毕后一段时间再次执行。(开始--->结束(隔一分钟)开始----->结束)。
上一次执行任务未完成,下一次任务不会开始
上一次执行任务未完成,下一次任务不会开始
cron表达式
灵活
图
含义
第一位,表示秒,取值0-59
第二位,表示分,取值0-59
第三位,表示小时,取值0-23
第四位,日期天/日,取值1-31
第五位,日期月份,取值1-12
第六位,星期,取值1-7,星期一,星期二...,注:不是第1周,第二周的意思,另外:1表示星期天,2表示星期一。
第七位,年份,可以留空,取值1970-2099
第二位,表示分,取值0-59
第三位,表示小时,取值0-23
第四位,日期天/日,取值1-31
第五位,日期月份,取值1-12
第六位,星期,取值1-7,星期一,星期二...,注:不是第1周,第二周的意思,另外:1表示星期天,2表示星期一。
第七位,年份,可以留空,取值1970-2099
特殊的符号
(*)星号:可以理解为每的意思,每秒,每分,每天,每月,每年...
(?)问号:问号只能出现在日期和星期这两个位置。
(-)减号:表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12
(,)逗号:表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一,星期二,星期四
(/)斜杠:如:x/y,x是开始值,y是步长,比如在第一位(秒) 0/15就是,从0秒开始,每15秒,最后就是0,15,30,45,60 另:/y,等同于0/y
(?)问号:问号只能出现在日期和星期这两个位置。
(-)减号:表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12
(,)逗号:表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一,星期二,星期四
(/)斜杠:如:x/y,x是开始值,y是步长,比如在第一位(秒) 0/15就是,从0秒开始,每15秒,最后就是0,15,30,45,60 另:/y,等同于0/y
在线工具
https://cron.qqe2.com/
实现定时任务
所有的定时任务使用的都是一个线程,所以彼此互相影响
解决定时任务单线程运行的问题
quartz简单定时任务(内存持久化)
引入对应的 maven依赖
创建一个任务类Job
创建 Quartz 定时配置类
将之前创建的定时任务添加到定时调度里面
深入解析
核心概念
Job:一个仅包含一个void execute(JobExecutionContext context)Abstract方法的简单接口。
在实际开发中,要执行的任务是通过实现接口自定义实现的。JobExecutionContext提供调度上下文信息。
在实际开发中,要执行的任务是通过实现接口自定义实现的。JobExecutionContext提供调度上下文信息。
JobDetail:包含多个构造函数,最常用的是JobDetail(String name, String group, Class jobClass)Jobclass是实现作业接口的类,name是调度程序中任务的名称,group是调度程序中任务的组名。默认组名称为Scheduler.DEFAULT_GROUP。
Trigger:描述触发作业执行的时间规则的类。包含:
SimpleTrigger:一次或固定间隔时间段的触发规则。
CronTrigger:通过cron表达式描述更复杂的触发规则。
Calendar:Quartz 提供的Calendar类。触发器可以与多个Calendar关联以排除特殊日期。
Scheduler:代表独立于Quartz 的运行容器。在Scheduler 中注册了Trigger和JobDetail。它们在调度程序中具有自己的名称(名称)和组名称(Group)。触发器和JobDetail名称和组名称的组合必须唯一,但是触发器名称和组名称的组合可以与JobDetail相同。一个Job可以绑定到多个触发器,也可以不绑定。
--------------------------------------------------------------------------------
Job还具有一个子接口:statefuljob,这是一个没有方法的标签接口,表示有状态任务。
无状态任务:它具有jobdatamap复制,因此可以并发运行;
有状态任务statefuljob:共享一个jobdatamap,并且将保存对jobdatamap的每次修改。因此,前一个有statefuljob将阻止下一个statefuljob。
4.2.SimpleTrigger and CronTrigger
SimpleTrigger可以在指定的时间段内执行一个Job任务,也可以在一个时间段内多次执行。
CronTrigger功能非常强大,它基于Calendar进行作业调度,并且可以比simpletrigger更精确地指定间隔,因此crotrigger比simpletrigger更常用。Crotrigger基于cron表达式。
Trigger:描述触发作业执行的时间规则的类。包含:
SimpleTrigger:一次或固定间隔时间段的触发规则。
CronTrigger:通过cron表达式描述更复杂的触发规则。
Calendar:Quartz 提供的Calendar类。触发器可以与多个Calendar关联以排除特殊日期。
Scheduler:代表独立于Quartz 的运行容器。在Scheduler 中注册了Trigger和JobDetail。它们在调度程序中具有自己的名称(名称)和组名称(Group)。触发器和JobDetail名称和组名称的组合必须唯一,但是触发器名称和组名称的组合可以与JobDetail相同。一个Job可以绑定到多个触发器,也可以不绑定。
--------------------------------------------------------------------------------
Job还具有一个子接口:statefuljob,这是一个没有方法的标签接口,表示有状态任务。
无状态任务:它具有jobdatamap复制,因此可以并发运行;
有状态任务statefuljob:共享一个jobdatamap,并且将保存对jobdatamap的每次修改。因此,前一个有statefuljob将阻止下一个statefuljob。
4.2.SimpleTrigger and CronTrigger
SimpleTrigger可以在指定的时间段内执行一个Job任务,也可以在一个时间段内多次执行。
CronTrigger功能非常强大,它基于Calendar进行作业调度,并且可以比simpletrigger更精确地指定间隔,因此crotrigger比simpletrigger更常用。Crotrigger基于cron表达式。
quartz动态定时任务(数据库持久化)
原理
使用quartz提供的API完成配置任务的增删改查
将任务的配置保存在数据库中
将任务的配置保存在数据库中
配置
application.yml
可能是版本bug,有的时候自动建表不会生效,自己去quartz-scheduler-x.x.x.jar里面找一下建表sql脚本:
classpath:org/quartz/impl/jdbcjobstore/tables_@@platform@@.sql,然后执行
classpath:org/quartz/impl/jdbcjobstore/tables_@@platform@@.sql,然后执行
动态配置代码实现
创建一个定时任务相关实体类用于保存定时任务相关信息到数据库当中
创建定时任务暂停,修改,启动,单次启动工具类
demo
缓存
redis缓存
整合
引入依赖包
引入 commons-pool 2 是因为 Lettuce 需要使用 commons-pool 2 创建 Redis 连接池
单例模式连接配置
哨兵模式连接配置
注意,当我们使用spring boot连接哨兵模式的redis集群,连接的是sentinel节点,而不是redis服务实例节点
集群模式连接配置
使用
使用redisTemplate操作数据
使用Redis Repository操作数据
redis 缓存配置
自定义缓存到期时间
redis分布式锁
分布式锁实现过程中的问题
异常导致锁没有释放
为redis的key设置过期时间
获取锁与设置过期时间操作不是原子性的
获取锁的同时设置过期时间
锁过期之后被别的线程重新获取与释放
在释放锁之前判断一下,这把锁是不是自己的那一把,如果是别人的锁你就不要动
锁的释放不是原子性的
使用redis lua脚本(lua脚本是在一个事务里面执行的,可以保证原子性)
其他的问题?
目前我们的程序获取不到锁,就无限的重试,是不是应该在重试一定的次数之后就抛出异常?在有限的时间内通过异常给用户一个友好的响应。比如:程序太忙,请您稍后再试!
程序A没有执行完成,锁定的key就过期了。虽然过期之后会自动释放锁,但是我的程序A的确没有执行完成啊,也没有异常抛出,就是执行的时间比较长,这个时候是不是应该对锁定的key进行续期?
笔者对于分布式锁自动续期的这个功能也不是特别感冒,我觉得程序超过了我们设置的过期时间(比如说60s)一定是出现了问题,如果不是离线大数据批处理,一个程序执行60秒还没完成那一定是出问题了,你给我抛出异常就可以了。对于一个出问题的程序一直续期和死锁没什么区别。
所以实现一个分布式锁,不是我们想的那么简单,在高并发的环境下需要考虑的问题会复杂得多。怎么办?实际上分布式锁的细节时间有很多的现成的解决方案
程序A没有执行完成,锁定的key就过期了。虽然过期之后会自动释放锁,但是我的程序A的确没有执行完成啊,也没有异常抛出,就是执行的时间比较长,这个时候是不是应该对锁定的key进行续期?
笔者对于分布式锁自动续期的这个功能也不是特别感冒,我觉得程序超过了我们设置的过期时间(比如说60s)一定是出现了问题,如果不是离线大数据批处理,一个程序执行60秒还没完成那一定是出问题了,你给我抛出异常就可以了。对于一个出问题的程序一直续期和死锁没什么区别。
所以实现一个分布式锁,不是我们想的那么简单,在高并发的环境下需要考虑的问题会复杂得多。怎么办?实际上分布式锁的细节时间有很多的现成的解决方案
比较完整优秀的分布式锁实现
RedisLockRegistry是spring-integration-redis中提供redis分布式锁实现类
基于Redisson实现分布式锁原理(Redission是一个独立的redis客户端,是与Jedis、Lettuce同级别的存在)
对比
RedisLockRegistry通过本地锁(ReentrantLock)和redis锁,双重锁实现;Redission通过Netty Future机制、Semaphore (jdk信号量)、redis锁实现。
RedisLockRegistry和Redssion都是实现的可重入锁。
RedisLockRegistry对锁的刷新没有处理(续期),Redisson通过Netty的TimerTask、Timeout 工具完成锁的定期刷新任务。
RedisLockRegistry和Redssion都是实现的可重入锁。
RedisLockRegistry对锁的刷新没有处理(续期),Redisson通过Netty的TimerTask、Timeout 工具完成锁的定期刷新任务。
spring cache缓存
EhCache缓存
适用于单体应用的缓存
集群多节点应用session共享
原理
单个应用的session应用
图
集群应用的Session共享
图
同一IP(域名),不同端口,在同一个浏览器cookies是共享的。不同IP(域名)的Cookies,在同一个浏览器Cookies肯定不共享的。对于这种情况需要在集群应用的前面加上负载均衡器逆向代理,如:nginx,haproxy。让客户端看上去访问的是同一个IP(代理IP),从而浏览器认为基于这个IP的Cookies是共享的。
SESSION正常是由Servlet容器来维护的(内存里面,每个服务器内存是不共享的),这样SESSION就无法共享。如果希望Session共享,就需要把sessionID的存储放到一个统一的地方,如:redis。SessionID的维护交给Spring session则更加方便。
除了Cookies可以维持Sessionid,Spring Session还提供了了另外一种方式,就是使用header传递SESSIONID。目的是为了方便类似于手机这种没有cookies的客户端进行session共享
SESSION正常是由Servlet容器来维护的(内存里面,每个服务器内存是不共享的),这样SESSION就无法共享。如果希望Session共享,就需要把sessionID的存储放到一个统一的地方,如:redis。SessionID的维护交给Spring session则更加方便。
除了Cookies可以维持Sessionid,Spring Session还提供了了另外一种方式,就是使用header传递SESSIONID。目的是为了方便类似于手机这种没有cookies的客户端进行session共享
不要把跨域请求和cookies跨域名的概念搞混了。同源策略是要求IP、端口、协议全一致,不一致的请求就是跨域请求。但cookies是可以跨域共享的,但是不能跨域名(IP)共享
集成Spring session
依赖
.配置启用Redis的httpSession
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 30 * 60 * 1000)
启动类上方加上注解,启动SpringSession管理应用的session,并设置session数据的有效期30分钟
配置redis链接信息(application.yml)
测试
在同一台机器上启动多个实例,ip相同所以session是共享的。
如果你在不同的服务器上启动多个实例(IP)不同,需要在应用前方加上负载均衡逆向代理才可以实现session共享
如果你在不同的服务器上启动多个实例(IP)不同,需要在应用前方加上负载均衡逆向代理才可以实现session共享
文件系统
文件上传目录自定义配置
文件上传的Controller实现
测试
分布式
MinIO
FastDFS
服务器推送
为了避免轮询
全双工(双向通信)通信:WebSocket
整合
依赖
开启websocket功能
兼容HTTPS协议
WebSocket的ws协议是基于HTTP协议实现的
WebSocket的wss协议是基于HTTPS协议实现的
一旦你的项目里面使用了https协议,你的websocket就要使用wss协议才可以
WebSocket的wss协议是基于HTTPS协议实现的
一旦你的项目里面使用了https协议,你的websocket就要使用wss协议才可以
服务端主动推送:SSE (Server Send Event)(应用较少)
邮件发送
应用程序监控管理
消息队列的整合与使用
ActiveMQ
RocketMQ
架构
图
NameServer:消息队列的大脑,同时监控消息队列,存储关于队列的基本信息,如各个队列的分布,ip、服务地址、目前的健康状况、队列的处理进度等。多个Nmaeserver之间没有通信。
broker:队列服务,负责接受请求并分发请求。数据存储、消息分发的负载均衡。可以是master-slave的主从结构。
console-ng:消息队列的监控控制台
broker:队列服务,负责接受请求并分发请求。数据存储、消息分发的负载均衡。可以是master-slave的主从结构。
console-ng:消息队列的监控控制台
安装nameserver和broker
RocketMQ控制台
防火墙开放端口
RocketMQ
依赖
配置
接口
定义一个常量配置类
对外暴露一个接口用于发送消息
消费监听
消费者需要实现RocketMQListener<T>接口
消费者组
新建一个RocketConsumer2,如果和RocketConsumer 是同一个组consumerGroup ,一条消息二者选其一消费一次,而且呈现负载均衡分布。即:同一个消费者组的消费者订阅同一话题,组内只能消费一次消息
新建一个RocketConsumer2,如果和RocketConsumer 不是同一个组consumerGroup ,一条消息被消费两次。即:不同消费者组订阅同一话题,组内只消费一次,但多组消费多次
实现2种消费模式
Pull消费模式
消费者主动从Broker拉取消息
Push消费模式
Broker主动将消息推送给消费者
3.熟悉SQL语言编写、调优,对事务、索引、MVCC机制等有深入理解,拥有线上慢SQL优化、使ShardingSphere
进行分库分表经验
进行分库分表经验
4.熟悉Redis、RabbitMQ,Elasticsearch,了解Redis性能优化,数据一致性方案
5.熟悉版本控制工具 Git、SVN
6.熟悉Netty、Nginx、Tomcat、docker
7.熟悉windows、linux部署等相关操作
8.熟悉前端知识,前端三剑客、BootStrap及其插件、Echerts、VUE、element等
9.了解SpringCloud alibaba技术体系,对Nacos、Sentinel有研究、服务注册与发现、服务限流、降级、熔断等
10.了解分布式事务解决方案,2PC、TCC、本地消息表、可靠消息最终一致性、最大努力通知等实现方案
2PC(Two-Phase Commit):两阶段提交协议是一种分布式事务协议,用于保证分布式系统中的事务的原子性和一致性。它分为准备阶段和提交阶段,准备阶段中协调者询问所有参与者是否准备提交事务,提交阶段中协调者下发事务提交或回滚的指令。该协议可以避免单点故障,但也存在阻塞和同步等待的问题。
2PC的实现需要引入一个协调者角色,通常由一个单独的节点担任。协调者负责发起事务的开始,并在准备阶段询问所有参与者是否准备提交事务,在提交阶段下发提交或回滚的指令。参与者必须回复事务的执行结果,包括成功或失败。协调者根据所有参与者的回复做出决策,如果所有参与者都回复成功,则提交事务;如果有任何参与者回复失败,则回滚事务。
TCC(Try, Confirm, Cancel):Try、Confirm和Cancel三阶段提交是一种改进的分布式事务协议,也称为T2C2。它分为Try、Confirm和Cancel三个阶段。在Try阶段,事务执行准备工作;在Confirm阶段,执行尝试提交事务并生成确认消息,同时将操作转化为已确认的消息并保存;在Cancel阶段,发送取消消息,取消已确认的消息操作。TCC协议可以解决2PC协议的阻塞问题,但实现复杂且开销较大。
TCC的实现分为Try、Confirm和Cancel三个阶段。在Try阶段,执行事务的准备工作,例如预留资源、创建临时数据等。在Confirm阶段,尝试提交事务并生成确认消息,同时将操作转化为已确认的消息并保存。在Cancel阶段,发送取消消息,取消已确认的消息操作。TCC的实现需要针对每个业务进行定制化开发,实现较为复杂。
本地消息表:本地消息表是一种消息传递机制,用于保证分布式系统中的消息可靠性。它使用本地数据库表来存储待发送的消息,通过数据库的事务和锁定机制来保证消息的可靠性和一致性。当发送方发送消息时,将消息写入本地数据库表中,接收方从表中读取消息并处理。这种方式可以避免消息丢失和重复处理的问题。
本地消息表的实现需要引入一个本地数据库表来存储待发送的消息。发送方将消息写入本地数据库表中,接收方从表中读取消息并处理。通过数据库的事务和锁定机制来保证消息的可靠性和一致性。
可靠消息最终一致性:可靠消息最终一致性是一种消息传递机制,用于实现分布式系统中的最终一致性。它使用消息队列和重试机制来保证消息的可靠性和一致性。发送方将消息发送到消息队列中,接收方从队列中读取消息并处理。如果处理失败,可以通过重试机制重新发送消息,直到达到一定的重试次数或者成功处理为止。这种方式可以实现最终一致性,但需要合理设置重试次数和控制重试的频率。
可靠消息最终一致性的实现需要借助消息队列和重试机制。发送方将消息发送到消息队列中,接收方从队列中读取消息并处理。如果处理失败,可以通过重试机制重新发送消息,直到达到一定的重试次数或者成功处理为止。在实现中需要设置合理的重试次数和控制重试的频率。
最大努力通知:最大努力通知是一种消息传递机制,用于实现分布式系统中的通知可靠性。它通过尽可能多次地发送通知来保证接收方能够收到通知。发送方可以多次发送通知,接收方也需要尽可能多次地接收通知并处理。这种方式无法保证通知的可靠性,但可以最大程度地提高通知的成功率。
最大努力通知的实现不需要特殊的机制,只需要发送方尽可能多次地发送通知,接收方也尽可能多次地接收通知并处理即可。这种方式无法保证通知的可靠性,但可以最大程度地提高通知的成功率。
基础算法
排序
选择排序
从左到右遍历,进行比较,找出最小值,赋值给最左边元素,将最左边元素值,赋值给此元素,依次遍历
将所有元素从左到右遍历,分为两个循环
第一个循环
遍历左到右除去最后一个元素
第二个循环
为了找出最值下标
遍历左到右除去第一个元素
去除元素算是个优化,因为不需要和本身进行比较,比较不同位置的元素就行
进行判断
找出最小值的下标
根据下标将最小值元素和第一层遍历元素进行互换
代码
public class Sort {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 7, 2, 4, 10, 9};
int[] newArr1 = new Sort().selectionSort(arr1);
for (int num : newArr1) {
System.out.println(num);
}
}
/**
* 选择排序:每次选择一个最小值
*
* @param arr
* @return
*/
int[] selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
// 注意使用minIndex
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
return arr;
}
}
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 7, 2, 4, 10, 9};
int[] newArr1 = new Sort().selectionSort(arr1);
for (int num : newArr1) {
System.out.println(num);
}
}
/**
* 选择排序:每次选择一个最小值
*
* @param arr
* @return
*/
int[] selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
// 注意使用minIndex
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
return arr;
}
}
当然 找出最大进行选择排序也可
复杂度O(N²)
冒泡排序
第一趟排序能得出最后一个元素一定是最大元素
第二趟排序能得出倒数第二个元素为第二大元素
所以需要元素个数-1趟,最后一个元素不需要冒泡了
代码
时间复杂度
最好
O(n)
平均
O(N²)
插入排序
0 条评论
下一页