开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

☕【JVM原理探索】让你完全攻克内存溢出(OOM)这一难题

发表于 2021-05-24

每日一句

只有经历地狱般的磨练,才能创造出天堂般的力量。

堆(Heap)内存不足

报错信息:

1
makefile复制代码java.lang.OutOfMemoryError: Java heap space

导致原因

  1. 代码中可能存在大对象分配
  2. 可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
  3. 业务场景会剧增对象数据,应该提升内存空间。

解决方法

  1. 检查是否存在大对象的分配,最有可能的是大数组分配
  2. 通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
  3. 如果没有找到明显的内存泄露,使用 -Xms/-Xmx 加大堆内存
  4. 还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性

方法区溢出

报错信息:

1
2
makefile复制代码java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Metaspace

导致原因

  • JDK8之前,永久代是HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
  • JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
+ 字符串常量由永久代转移到堆中
+ 和永久代相关的JVM参数已移除
  • 出现永久代或元空间的溢出的原因可能有如下几种:
1. 在Java7之前,频繁的错误使用String.intern方法。
2. 生成了大量的代理类,导致方法区被撑爆,无法卸载。
3. 应用长时间运行,没有重启。

解决方法

  • 永久代/元空间 溢出的原因比较简单,解决方法有如下几种:
1. 检查是否永久代空间或者元空间设置的过小。
2. 检查代码中是否存在大量的反射操作或者class加载操作以及生产class字节码。
3. dump之后通过mat检查是否存在大量由于反射生成的代理类
4. 放大招,重启JVM

GC overhead limit exceeded

报错信息

1
bash复制代码java.lang.OutOfMemoryError:GC overhead limit exceeded

导致原因

这个是JDK6新加的错误类型,一般都是堆太小导致的。

Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。

解决方法

  1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
  2. 添加参数-XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
  3. dump内存,检查是否存在内存泄露,如果没有,加大内存。

虚拟机栈和本地方法栈溢出

由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

这里把异常分成两种情况看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。

虚拟机的 StackOverflowError 异常

-Xss参数减小栈内存的容量,然后不断调用方法造成栈溢出,StackOverflowError 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码public class JVMStackSOF {
private int stacklength = 1; // 记录栈深度

// 调用这个递归方法以造成栈溢出
public void stackPush(){
stacklength++;
stackPush();
}
public static void main(String[] args) throws Throwable{
JVMStackSOF sof = new JVMStackSOF();
try{
sof.stackPush();
}catch(Throwable e){
System.out.println("stack length = " + sof.stacklength);
throw e;
}
}
}
1
2
3
4
5
6
arduino复制代码openjdk@ubuntu:~$ java -Xss256k -cp
/home/openjdk/NetBeansProjects/JavaApplication1/build/classes test_JVMStackSOF.JVMStackSOF
stack length = 1888
Exception in thread "main" java.lang.StackOverflowError
   at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:17)
   at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:18)

-Xss256K:设置参数栈内存容量为256K

  • 在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。

使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小。

定义了大量的本地变量,增加此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的栈深度相应缩小。


虚拟机栈隔离的,每个线程都有自己独立的虚拟机栈。

在 Java 虚拟机规范中,对虚拟机栈这个区域规定了两种异常状况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;
  2. 如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),在扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

虚拟机的 OutOfMemoryError 异常

通过-Xss2M参数增大栈内存的容量,然后不断开启新的线程,抛出OutOfMemoryError 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码public class JVMStackOOM {

private void dontStop() {
while (true) {
}
}

public static void main(String[] args) {
// 不断开启新的线程消耗虚拟机栈空间
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
}).start();
}
}
}

原理

  • 主要是因为-Xss参数设置的是一个线程的栈大小,前面已经说过虚拟机栈是线程私有的,即每个线程都有一个自己的栈。

操作系统分配给每个进程的内存是有限制的,譬如 32 位的 Windows 限制为 2GB。Java虚拟机提供了参数来控制 Java 堆和方法区的这两部分内存的最大值。

2GB(操作系统限制的内存大小)减去 Xmx(最大堆容量),再减去 MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。

所以每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。第一例中把栈空间占满而抛出 StackOverflowError 异常,第二例中把内存消耗完而抛出 OutOfMemoryError 异常。


方法栈溢出(从属于虚拟机栈的异常)

报错信息

1
sql复制代码java.lang.OutOfMemoryError : unable to create new native Thread

导致原因

出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。

解决方法

  1. 通过 -Xss 降低的每个线程栈大小的容量
  2. 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制
1
2
3
4
bash复制代码/proc/sys/kernel/pid_max
/proc/sys/kernel/thread-max
max_user_process(ulimit -u)
/proc/sys/vm/max_map_count

非常规溢出

下面这些OOM异常,可能大部分的同学都没有碰到过,但还是需要了解一下

分配超大数组

报错信息

1
arduino复制代码java.lang.OutOfMemoryError: Requested array size exceeds VM limit

这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。

解决方法就是检查你的代码中是否有创建超大数组的地方。

swap区溢出

报错信息 :

1
makefile复制代码java.lang.OutOfMemoryError: Out of swap space

这种情况一般是操作系统导致的,可能的原因有:

  1. swap 分区大小分配不足;
  2. 其他进程消耗了所有的内存。

解决方案

  1. 其它服务进程可以选择性的拆分出去
  2. 加大swap分区大小,或者加大机器内存大小

本地方法溢出

报错信息 :

1
makefile复制代码java.lang.OutOfMemoryError: stack_trace_with_native_method

本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在JNI代码或本地方法处。

本机直接内存溢出

  • 直接内存可以通过:-XX:MaxDirectMemorySize 来设置大小,如果不设置,默认和堆在最大值-Xmx一样大。
  • 设置本机直接内存的原则就是,各种内存大小+本机直接内存大小<机器物理内存。

下面程序利用 DirectByteBuffe 模拟直接内存溢出的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class DirectBufferOom {
public static void main(String[] args) {
final int _1M = 1024 * 1024;
List<ByteBuffer> buffers = new ArrayList<>();
int count = 1;
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
buffers.add(byteBuffer);
System.out.println(count++);
}
}
}

在命令行运行 java -XX:MaxDirectMemorySize=10M DirectBufferOom ,很快控制台就会出现异常

1
2
3
4
5
less复制代码Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at DirectBufferOom.main(DirectBufferOom.java:12)

其实它并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常。下
面的程序利用 Unsafe 类模拟直接内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeOom {
private static final int _1M = 1024 * 1024;

public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1M);
}
}
}

在命令行运行 java -XX:MaxDirectMemorySize=10M UnsafeOom ,结果如下

1
2
3
arduino复制代码Exception in thread"main"java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)

由 DirectMemory 导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果读者发现 OOM 之后 Dump 文件很小,而程序中又直接或间接使用了 NIO ,那就可以考虑检查一下是不是这方面的原因。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

谁说不能使用select *?!

发表于 2021-05-24

导读

我们先来回顾一下交友平台用户表的表结构:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码CREATE TABLE `user` (
`id` int(11) NOT NULL,
`user_id` int(8) DEFAULT NULL COMMENT '用户id',
`user_name` varchar(29) DEFAULT NULL COMMENT '用户名',
`user_introduction` varchar(498) DEFAULT NULL COMMENT '用户介绍',
`sex` tinyint(1) DEFAULT NULL COMMENT '性别',
`age` int(3) DEFAULT NULL COMMENT '年龄',
`birthday` date DEFAULT NULL COMMENT '生日',
PRIMARY KEY (`id`),
KEY `index_un_age_sex` (`user_name`,`age`,`sex`),
KEY `index_age_sex` (`age`,`sex`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

其中,user_introduction字段:用户介绍,里面允许用户填写非常长的内容,所以,我将这个字段的设为varchar(498),加上其他字段,单条记录的长度可能就会比较大了,这时,如果执行下面这条SQL:

1
sql复制代码select user_id, user_name, user_introduction from user where age > 20 and age < 50

假设用户表中已经存储300w条记录,执行上面的SQL,会发生什么情况呢?

对MySQL有初步了解的同学肯定知道Query Cache,它的作用就是缓存查询结果,通过首次查询时,建立SQL与结果的映射关系,相同SQL再次查询时,可以命中Query Cache,以此来提升后续相同查询的效率。

因此,对于上面的SQL查询,MySQL可以在首次执行这条SQL后,将查询结果写入Query Cache,下次相同SQL执行时,可以从Query Cache中取出结果返回。

但是,你有没有想过,如果满足查询条件的用户数超过10w,那么,这10w条记录能否完全写进Query Cache呢?

今天,我就从Query Cache的结构说起,逐步揭晓答案。

在《导读》中我提到MySQL通过建立SQL与查询结果的映射关系来实现再次查询的快速命中,那么,问题来了:为了实现这样的一个映射关系,总得有个结构承载这样的关系吧!那么,MySQL使用什么结构来承载这样的映射关系呢?

或许你已经想到了:HashMap!没错,MySQL的确使用了HashMap来表达SQL与结果集的映射关系。进而我们就很容易想到这个HashMap的Key和Value是什么了。

  • Key:MySQL使用query + database + flag组成一个key。这个key的结构还是比较直观的,它表示哪个库的哪条SQL使用了Query Cache。
  • Value:MySQL使用一个叫query_cache_block的结构作为Map的value,这个结构存放了一条SQL的查询结果。

Query Cache Block

那么,一条SQL的查询结果又是如何存放在query_cache_block中的呢?下面我们就结合《导读》中的SQL,来看看一个query_cache_block的结构:


如上图所示,一个query_cache_block主要包含3个核心字段:

  • used:存放结果集的大小。MySQL通过block在内存中的偏移量 + 这个大小来获取结果集。如上图,假设《导读》中SQL查询的结果为<10001, Jack, I'm Jack>,那么,used为这个查询结果的大小。
  • type:Block的类型。包含{FREE, QUERY, RESULT, RES_CONT, RES_BEG, RES_INCOMPLETE, TABLE, INCOMPLETE}这几种类型。这里我重点讲解QUERY和RESULT,其他类型你可以自行深入了解。
+ QUERY:表示这个block中存放的是查询语句。为什么要缓存查询语句呢?



> 在并发场景中,会存在多个会话执行同一条查询语句,因此,为了避免重复构造《导读》中所说的HashMap的Key,MySQL缓存了查询语句的Key,保证查询Query Cache的性能。
+ RESULT:表示这个block中存放的是查询结果。如上图,《导读》中SQL的查询结果`<10001, Jack, I'm Jack>`放入block,所以,block类型为RESULT。
  • n_tables:查询语句使用的表的数量。那么,block又为什么要存表的数量呢?

因为MySQL会缓存table结构,一张table对应一个table结构,多个table结构组成一条链表,MySQL需要维护这条链表增删改查,所以,需要n_tables字段。

现在我们知道了一个query_cache_block的结构了,下面我简称block。

现在有这么一个场景:

已知一个block的大小是1KB,而《导读》中的查询语句得到的结果记录数有10w,它的大小有1MB,那么,显然一个block放不下1MB的结果,此时,MySQL会怎么做呢?

为了能够缓存1MB的查询结果,MySQL设计了一个双向链表,将多个block串联起来,1MB的数据分别放在链表中多个block里。于是,就有了下面的结构:逻辑块链表。


图中,MySQL将多个block通过一个双向链表串联起来,每个block就是我上面讲到的block结构。通过双向链表我们就可以将一条查询语句对应的结果集串联起来。

比如针对《导读》中SQL的查询结果,图中,前两个block分别存放了两个满足查询条件的结果:<10001,Jack,I'm Jack>和<10009,Lisa,I'm Lisa>。同时,两个block通过双向指针串联起来。

还是《导读》中的SQL案例,已知一个block的大小是1K,假设SQL的查询结果为<10001,Jack,I'm Jack>这一条记录,该记录的大小只有100Byte,那么,此时查询结果小于block大小,如果把这个查询结果放到1K的block里,就会浪费1024-100=924 字节的block空间。所以,为了避免block空间的浪费,MySQL又引入了一个新结构:

image-20210523175639884.png

如上图,下面的物理块就是MySQL为了解决block空间浪费引入的新结构。该结构也是一个多block组成的双向链表。

以《导读》中的SQL为例,已知SQL查询的结果为<10001,Jack,I'm Jack>,那么,将逻辑块链表和物理块链表结合起来,这个结果在block中是如何表达的呢?

  • 如上图,逻辑块链表的第一个block存放了<10001,Jack,I'm Jack>这个查询结果。
  • 由于查询结果大小为100B,小于block的大小1K,所以,见上图,MySQL将逻辑块链表中的第一个block分裂,分裂出下面的两个物理块block,即红色箭头部分,将<10001,Jack,I'm Jack>这个结果放入第一个物理块中。其中,第一个物理块block大小为100B,第二个物理块block大小为924B。

讲完了query_cache_block,我想你应该对其有了较清晰的理解。但是,我在上面多次提到一个block的大小,那么,这个block的大小又是如何决定的呢?为什么block的大小是1K,而不是2K,或者3K呢?

要回答这个问题,就要涉及MySQL对block的内存管理了。MySQL为了管理好block,自己设计了一套内存管理机制,叫做query_cache_memory_bin。

下面我就详细讲讲这个query_cache_memory_bin。

Query Cache Memory Bin

MySQL将整个Query Cache划分多层大小不同的多个query_cache_memory_bin(简称bin),如下图:


说明:

  • steps:为层号,如上图中,从上到下分为0、1、2、3这4层。
  • bin:每一层由多个bin组成。其中,bin中包含以下几个属性:
+ size:bin的大小
+ free\_blocks:空闲的`query_cache_block`链表。每个bin包含一组`query_cache_block`链表,即逻辑块链表和物理块链表,也就是《Query Cache Block》中我讲到的两个链表组成一组`query_cache_block`。
+ 每层bin的个数通过下面的公式计算得到:



> bin个数 = 上一层bin数量总和 + QUERY\_CACHE\_MEM\_BIN\_PARTS\_INC) \* QUERY\_CACHE\_MEM\_BIN\_PARTS\_MUL


其中,`QUERY_CACHE_MEM_BIN_PARTS_INC = 1` ,`QUERY_CACHE_MEM_BIN_PARTS_MUL = 1.2`


因此,如上图,得到各层的bin个数如下:


    - 第0层:bin个数为1
    - 第1层:bin个数为2
    - 第2层:bin个数为3
    - 第3层:bin个数为4
+ 每层都有其固定大小。这个大小的计算公式如下:



> 第0层的大小 = query\_cache\_size >> QUERY\_CACHE\_MEM\_BIN\_FIRST\_STEP\_PWR2 >> QUERY\_CACHE\_MEM\_BIN\_STEP\_PWR2
> 
> 
> 其余层的大小 = 上一层的大小 >> QUERY\_CACHE\_MEM\_BIN\_STEP\_PWR2


其中,`QUERY_CACHE_MEM_BIN_FIRST_STEP_PWR2 = 4`,`QUERY_CACHE_MEM_BIN_STEP_PWR2 = 2`


因此,假设`query_cache_size = 25600K`,那么,得到计算各层的大小如下:


    - 第0层:400K
    - 第1层:100K
    - 第2层:25K
    - 第3层:6K
+ 每层中的bin也有固定大小,但最小不能小于`QUERY_CACHE_MIN_ALLOCATION_UNIT`。这个bin的大小的计算公式采用`对数逼近法`如下:



> bin的大小 = 层大小 / 每一层bin个数,无法整除向上取整


其中,`QUERY_CACHE_MIN_ALLOCATION_UNIT = 512B`


因此,如上图,得到各层bin的大小如下:


    - 第0层:400K / 1 = 400K
    - 第1层:100K / 2 = 50K
    - 第2层:25K / 3 = 9K,从最左边的bin开始分配大小:
        * 第1个bin:9K
        * 第2个bin:8K
        * 第3个bin:8K
    - 第3层:6K / 4 = 2K,从最左边的bin开始分配大小:
        * 第1个bin:2K
        * 第2个bin:2K
        * 第3个bin:1K
        * 第4个bin:1K

通过对MySQL管理Query Cache使用内存的讲解,我们应该猜到MySQL是如何给query_cache_block分配内存大小了。我以上图为例,简单说明一下:

由于每个bin中包含一组query_cache_block链表(逻辑块和物理块链表),如果一个block大小为1K,这时,通过遍历bin找到一个大于1K的bin,然后,把该block链接到bin中的free_blocks链表就行了。具体过程,我在下面会详细讲解。

在了解了query_cache_block、query_cache_memory_bin这两种结构之后,我想你对Query Cache在处理时用到的数据结构有了较清晰的理解。那么,结合这两种数据结构,我们再看看Query Cache的几种处理场景及实现原理。

Cache写入

我们结合《导读》中的SQL,先看一下Query Cache写入的过程:

image.png

  1. 结合上面HashMap的Key的结构,根据查询条件age > 20 and age < 50构造HashMap的Key:age > 20 and age < 50 + user + flag,其中flag包含了查询结果,将Key写入HashMap。如上图,Result就是这个Key。
  2. 根据Result对query_cache_mem_bin的层进行二分查找,找到层大小大于Result大小的层。如上图,假设第1层为找到的目标层。
  3. 根据Result从右向左遍历第1层的bin(因为每层bin大小从左向右降序排列,MySQL从小到大开始分配),计算bin中的剩余空间大小,如果剩余空间大小大于Result大小,那么,就选择这个bin存放Result,否则,继续向左遍历,直至找到合适的bin为止。如上图灰色bin,选择了第2层的第一个bin存放Result。
  4. 根据Result从左向右扫描上一步得到的bin中的free_blocks链表中的逻辑块链表,找到第一个block大小大于Result大小的block。如上图,找到第2个逻辑块block。
  5. 假设Result大小为100B,第2个逻辑块block大小为1k,由于block大于Result大小,所以,分裂该逻辑块block为2个物理块block,其中,分裂后第一个物理块block大小为100B,第二个物理块block大小为924B。
  6. 将Result结果写入第1个物理块block。如上图,将<10001, Jack, I'm Jack>这个Result写入灰色的物理块block。
  7. 根据Result所在的block,找到对应的block_table,更新table信息到block_table中。

Cache失效

当一个表发生改变时,所有与该表相关的cached queries将失效。一个表发生变化,包含多种语句,比如 INSERT, UPDATE, DELETE, TRUNCATE TABLE,ALTER TABLE, DROP TABLE, 或者 DROP DATABASE。

Query Cache Block Table

,
为了能够快速定位与一张表相关的Query Cache,将这张表相关的Query Cache失效,MySQL设计一个数据结构:Query_cache_block_table。如下图:

image.png

这是一个双向链表,对于一条SQL,如果包含多表联接,那么,就可以将这条SQL对应多张表链接起来,再插入这张链表,比如,我们把user和t_user_view(访客表)联接,查询用户访客信息,那么,在图中,假设逻辑块链表存放就是联表查询的结果,因此,我们就看到user表和t_user_view都指向了该逻辑块链表。

我们来看一下这个结构包含的核心属性:

  • block:与一张表相关的query_cache_block链表。如上图是user表的query_cache_block_table,该block中的block属性指向了逻辑块block链表,该链表中第1个block包含《导读》中SQL的查询结果<10001, Jack, I'm Jack>。
  • table:同样以user和t_user_view(访客表)联接,查询用户访客信息为例,这时,我对这个访客信息创建了视图,那么,MySQL如何表达表的关系呢?为了解决这个问题,MySQL引入了table,通过这个table记录视图信息,视图来源表都指向这个table来表达表的关系。如上图,user和t_user_view都指向了user_view,来表示user和t_user_view(访客表)对应的视图是user_view。

和Query Cache的HashMap结构一样,为了根据表名可以快速找到对应的query_cache_block,MySQL也设计了一个表名跟query_cache_block映射的HashMap,这样,MySQL就可以根据表名快速找到query_cache_block了。

通过上面这些内容的讲解,我想你应该猜到了一张表变更时,MySQL是如何失效Query Cache的?

image-20210524145423775.png

我们来看下上面这张图,关注红线部分:

  1. 根据user表找到其对应的query_cache_block_table。如上图,找到第2个table block。
  2. 根据query_cache_block_table中的block属性,找到table下的逻辑块链表。如上图,找到了右侧的逻辑块链表。
  3. 遍历逻辑块链表及每个逻辑块block下的物理块链表,释放所有block。

Cache淘汰

如果query_cache_mem_bin中没有足够空间的block存放Result,那么,将触发query_cache_mem_bin的内存淘汰机制。

这里我借用《Cache写入》的过程,一起来看看Query Cache的淘汰机制:

image.png

  1. 结合上面HashMap的Key的结构,根据查询条件age > 20 and age < 50构造HashMap的Key:age > 20 and age < 50 + user + flag,其中flag包含了查询结果,将Key写入HashMap。如上图,Result就是这个Key。
  2. 根据Result对query_cache_mem_bin的层进行二分查找,找到层大小大于Result大小的层。如上图,假设第1层为找到的目标层。
  3. 根据Result从右向左遍历第1层的bin(因为每层bin大小从左向右降序排列,MySQL从小到大开始分配),计算bin中的剩余空间大小,如果剩余空间大小大于Result大小,那么,就选择这个bin存放Result。如上图灰色bin,选择了第2层的第一个bin存放Result。
  4. 根据Result从左向右扫描上一步得到的bin中的block链表中的逻辑块链表,找到第一个block大小大于Result大小的block。如上图,找到第2个逻辑块block。
  5. 假设Result大小为100B,第2个逻辑块block大小为1k,由于block大于Result大小,所以,分裂该逻辑块block为2个物理块block,其中,分裂后第一个物理块block大小为100B,第二个物理块block大小为924B。
  6. 由于第1个物理块block已经被占用,所以,MySQL不得不淘汰该block,用以放入Result,淘汰过程如下:
    • 发现相邻的第2个物理块block最少使用,所以,将该物理块和第1个物理块block合并成一个新block。如上图右侧灰色block和虚线block合并成下面的一个灰色block。
  7. 将Result结果写入合并后的物理块block。如上图,将<10001, Jack, I'm Jack>这个Result写入合并后的灰色block。

在Cache淘汰这个场景中,我们重点关注一下第6步,我们看下这个场景:

  1. 从第1个物理块block开始扫描,合并相邻的第2个block跟第1个block为一个新block
  2. 如果合并后block大小仍然不足以存放Result,继续扫描下一个block,重复第1步
  3. 如果合并后block大小可以存放Result,结束扫描
  4. 将Result写入合并后block

通过上面的场景描述,我们发现如果Result很大,那么,MySQL将不断扫描物理块block,然后,不停地合并block,这是不小的开销,因此,我们要尽量避免这样的开销,保证Query Cache查询的性能。

有什么办法避免这样的开销呢?

我在最后小结的时候回答一下这个问题。

小结

好了,这篇内容我讲了很多东西,现在,我们来总结一下今天讲解的内容:

  1. 数据结构:讲解了Query Cache设计的数据结构:
数据结构 说明
Query_cache_block 存放了一条SQL的查询结果
Query_cache_mem_bin query_cache_block的内存管理结构
Query_cache_block_table 一张表对应一个block_table,方便快速失效query cache
2. Query Cache处理的场景:Cache写入、Cache失效和Cache淘汰。

最后,我们再回头看一下文章开头的那个问题:10w条用户记录是否可以写入Query Cache?我的回答是:

  1. 我们先对用户表的10w记录大小做个计算:

用户表包含user_id(8),user_name(29),user_introduction(498),age(3),sex(1)这几个字段,按字段顺序累加,一条记录的长度为8+30(varchar类型长度可以多存储1或2byte)+500+3+1=542byte,那么,10w条记录最大长度为542 * 10w = 54200000byte。

如果要将10w条记录写入Query Cache,则需要将近54200K大小的Query Cache来存储这10w条记录,而Query Cache大小默认为1M,所以,如果字段user_introduction在业务上非必须出现,请在select子句中排除该字段,减少查询结果集的大小,使结果集可以完全写入Query Cache,**这也是为什么DBA建议开发不要使用select 的原因,但是如果select 取出的字段都不大,查询结果可以完全写入Query Cache,那么,后续相同查询条件的查询性能也是会提升的,😁。
2. 调大query_cache_size这个MySQL配置参数,如果业务上一定要求select所有字段,而且内存足够用,那么,可以将query_cache_size调至可以容纳10w条用户记录,即54200K。
3. 调大query_cache_min_res_unit这个MySQL配置参数,使MySQL在第一次执行查询并写入Query Cache时,尽可能不要发生过多的bin合并,减少物理块block链表的合并开销。那么,query_cache_min_res_unit调成多少合适呢?

这需要结合具体业务场景综合衡量,比如,在用户中心系统中,一般会有一个会员中心的功能,而这个功能中,用户查询自己的信息是一个高频的查询操作,为了保证这类操作的查询性能,我们势必会将这个查询结果,即单个用户的基本信息写入Query Cache,在我的回答的第1条中,我说过一条用户记录最大长度为542byte,结合10w条用户记录需要54200K的Query Cache,那么,设置query_cache_min_res_unit = 542byte就比较合适了。

这样,有两点好处:

1. 保证查询单个用户信息,其直接可分配的bin大小大于542byte,写入单个用户信息时可以避免了bin的合并和空间浪费。
2. 10w条用户记录写入Query Cache,虽然第一次分配缓存时,仍然需要合并bin,但是,综合单用户查询的场景,这个合并过程是可以接受的,毕竟,只会在第一次写缓存时发生bin合并,后续缓存失效后,再次分配时,可以直接取到合并后的那个bin分配给10w条记录,不会再产生bin的合并,所以,这个合并过程是可以接受的。
  1. 调大query_cache_limit这个MySQL配置参数,我在本章节中没有提到这个参数,它是用来控制Query Cache最大缓存结果集大小的,默认是1M,所以,10w条记录,建议调大这个参数到54200K。

思考题

最后,对比前面《告诉面试官,我能优化groupBy,而且知道得很深!》这篇文章,发现MySQL特别喜欢自己实现内存的管理,而不用Linux内核的内存管理机制(比如:伙伴系统),为什么呢?

The End

如果你觉得写得不错,记得点赞哦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节跳动Java岗一二三面全经过分享 前言 一面:45分钟

发表于 2021-05-24

最近准备面试的朋友可以关注一下我的专栏——助力秋招,希望对你有所帮助

前言

金三银四才过去没多久,眼看着便又要秋招了,所以为大家写了这篇文章,来自一个刚参加完字节面试并高分通过的朋友亲口所述,除了字节的offer,他还分别通过了京东、百度以及腾讯阿里巴巴这些公司的面试,所以他的经验还是有一定价值的,准备参加秋招的朋友可以收藏一下,权当做个参考,如果真的对你的面试产生了一些帮助,我不胜荣幸。

他参加面试前所用的一些资料我也全都拿过来了,可以无偿分享给需要的朋友,直接点击领取就可以!

  • Java基础知识总结
  • 一线互联网公司Java面试核心知识点

那话不多说,坐稳扶好,发车喽!

一面二面连着一起,三面因为过了五一所以隔了很久,hr面在三面后一天

一面:45分钟

项目:介绍项目需求,设计思路,主要技术(因为问到的是ai相关的项目,因此没多问技术)

一、JAVA

1.垃圾回收算法

典型的垃圾回收算法

在JVM规范中并没有明确GC的运作方式,各个厂商可以采用不同的方式去实现垃圾回收器。这里讨论几种常见的GC算法。

标记-清除算法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图:

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

复制算法(Copying)

为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。

标记-整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

分代收集算法(Generational Collection)

分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。

而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。

另外,不要忘记在Java基础:Java虚拟机(JVM)中提到过的处于方法区的永生代(Permanet Generation)。它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。

如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。

典型的垃圾收集器

垃圾收集算法是垃圾收集器的理论基础,而垃圾收集器就是其具体实现。下面介绍HotSpot虚拟机提供的几种垃圾收集器。

3.1. Serial/Serial Old

最古老的收集器,是一个单线程收集器,用它进行垃圾回收时,必须暂停所有用户线程。Serial是针对新生代的收集器,采用Copying算法;而Serial Old是针对老生代的收集器,采用Mark-Compact算法。优点是简单高效,缺点是需要暂停用户线程。

ParNew

Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。

Parallel Scavenge

新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。

Parallel Old

Parallel Scavenge的老生代版本,采用Mark-Compact算法和多线程。

CMS

Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。

G1

G1(Garbage First)收集器技术的前沿成果,是面向服务端的收集器,能充分利用CPU和多核环境。是一款并行与并发收集器,它能够建立可预测的停顿时间模型。

2.cms和g1区别

CMS:以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现

过程:

1、初始标记:独占PUC,仅标记GCroots能直接关联的对象

2、并发标记:可以和用户线程并行执行,标记所有可达对象

3、重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正

4、并发清理:可以和用户线程并行执行,清理垃圾

优点:

并发,低停顿

缺点:

1、对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢

2、无法处理浮动垃圾:在最后一步并发清理过程中,用户县城执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾

3、CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了

G1:是一款面向服务端应用的垃圾收集器

特点:

1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。

3、空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。

4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

3.stop the world一般怎么处理

这个一两句话说不清楚,感兴趣的朋友可以自己上网搜一下教程

4.判断对象是否存活

判断对象是否存活一般有两种方式:

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java语言中,GC Roots包括:

虚拟机栈中引用的对象。

方法区中类静态属性实体引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI引用的对象。

二、计算机网络:

1.tcp三次握手四次挥手过程及各个状态

三次握手
第一次握手:主机A发送位码为syn=1,随机产生seq number=10001的数据包到服务器,主机B由SYN=1知道,A要求建立联机,此时状态为SYN_SENT;
第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=20001的包,此时状态由LISTEN变为SYN_RECV;
第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功,双方状态ESTABLISHED。

完成三次握手,主机A与主机B开始传送数据

  • 各个状态名称与含义

CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
SYN_RECV: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本 上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态 时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RECV遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状 态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。

  • 四次挥手

FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马 上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。

FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。

TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什 么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对 方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。

  • 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
    这 是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一 个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未 必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文 和FIN报文多数情况下都是分开发送的。
  • 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
    因为虽然双方都同意关闭连接了,而且握手的4个报文也都发送完毕,按理可以直接回到CLOSED 状态(就好比从SYN_SENT 状态到ESTABLISH 状态那样),但是我们必须假想网络是不可靠的,你无法保证你(客户端)最后发送的ACK报文一定会被对方收到,就是说对方处于LAST_ACK 状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT 状态的作用就是用来重发可能丢失的ACK报文。
  • 关闭TCP连接一定需要4次挥手吗?
    不一定,4次挥手关闭TCP连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT 状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的。

2.accept connect listen对应三次握手什么阶段

  1. Connect()函数:是一个阻塞函数 通过TCp三次握手父服务器建立连接

客户端主动连接服务器 建立连接方式通过TCP三次握手通知Linux内核自动完成TCP 三次握手连接 如果连接成功为0 失败返回值-1

一般的情况下 客户端的connect函数 默认是阻塞行为 直到三次握手阶段成功为止。

2.服务器端的listen() 函数:不是一个阻塞函数: 功能:将套接字 和 套接字对应队列的长度告诉Linux内核

他是被动连接的 一直监听来自不同客户端的请求 listen函数只要 作用将socketfd 变成被动的连接监听socket 其中参数backlog作用 设置内核中队列的长度 。

3.accept() 函数 阻塞:从处于established 状态的队列中取出完成的连接 当队列中没有完成连接时候 会形成阻塞,直到取出队列中已完成连接的用户连接为止。

问题一:服务器没有及时调用accept函数取走完成连接的队列怎么办?

服务器的连接队列满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的 connect 就会返回 ETIMEDOUT。但实际上Linux的并不是这样的 当TCP连接队列满了之后 Linux并不会书中所说的拒绝连接,只是会延时连接。

三、操作系统:

1.linux c程序布局

一个程序本质上都是由 BSS 段、data段、text段三个组成的。可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。

  • BSS段(未初始化数据区):在采用段式内存管理的架构中,BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
  • 数据段:在采用段式内存管理的架构中,数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
  • 代码段:在采用段式内存管理的架构中,代码段(text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

程序编译后生成的目标文件至少含有这三个段,这三个段的大致结构图如下所示:

text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。

bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。

data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。

数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。

可执行程序在运行时又多出两个区域:栈区和堆区。

(4)栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的 函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容 量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。

(5)堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的 malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。

下图将体现c的源文件对应存储空间:

image

此时程序还没有被放入内存,只是在硬盘存储的情况,此时bss并未占用空间。bss在链接的时候被获得内存空间。

下图表示程序运行,即程序在内存时的存储布局:

四、智力题:

一枚硬币不均匀,如何把他设计成公平硬币(拒绝采样)
这个题留给你们自己思考,发散一下思维,毕竟谁还没被智力题欺负过呢!

五、算法及数据结构:

堆排序建堆及排序过程

这里只简单的提一下吧
建堆
从第一个非叶子结点开始向下交换(即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。有了初始堆之后就可以进行排序了)
排序
排序过程为取出堆顶元素的操作,实现细节(取出堆顶元素与堆尾元素交换,将堆顶元素向下调整堆———在每一个调整过程中当不向下调整时即为调整结束)。取出的顺序为先第一个非叶子结点,第二个非叶子结点…一直到堆顶

这里还问了一下堆调整的时间复杂度,关于这个也不赘述了

手撕:

最长无重复子串

思路与解法
思路1: 暴力法,实际解题中不会使用暴力法,这并不代表我们可以忽略它。
索引从字符串的第一位开始,将后面的字符依次加入到 set 里面。如果 set 里面已经有了该字符,此次循环结束,内循环结束后记录 size。字符串的每一位都用这种方法去计算,得到的最大的 size 即是答案。

代码如下(不是Java的也看得懂,我进行了关键语法的注释,下同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public int lengthOfLongestSubstring(String s) {
int maxLen = 0;
for(int i = 0; i < s.length(); i++){
// 创建一个存放字符的集合
HashSet<Character> set = new HashSet<>();
for(int j = i; j < s.length(); j++) {
// 判断集合是否存在第 j 个字符
if(set.contains(s.charAt(j)))
break;
set.add(s.charAt(j));
}
maxLen = Math.max(maxLen,set.size());
}
return maxLen;
}


这里也只贴这一种解法吧,还有很多种有趣的解法,大伙也可以发散一下思维。

二面:1小时10分钟

项目:设计思路,遇到的最大问题,怎么保证分布式情况下主键全局唯一(项目聊了很久很久….)
场景题:大量主机向一台服务器发送消息,怎么保证性能(当时没太懂什么意思,就谈了谈io模型,被问用没用过epoll)

计算机网络:

1.同tcp三次四次老八股

这个前面说了,这里就跳过了

2.tcp keep alive实现原理

其实 keepalive 的原理就是 TCP 内嵌的一个心跳包
以服务器端为例,如果当前 server 端检测到超过一定时间(默认是 7,200,000 milliseconds ,也就是 2 个小时)没有数据传输,那么会 向client 端发送一个 keep-alive packet (该 keep-alive packet 就是 ACK 和当前 TCP 序列号减一的组合),此时 client 端应该为以下三种情况之一:

  1. client 端仍然存在,网络连接状况良好。此时 client 端会返回一个 ACK 。 server 端接收到 ACK 后重置计时器,在 2 小时后再发送探测。如果 2 小时内连接上有数据传输,那么在该时间基础上向后推延 2 个小时。
  2. 客户端异常关闭,或是网络断开。在这两种情况下, client 端都不会响应。服务器没有收到对其发出探测的响应,并且在一定时间(系统默认为 1000 ms )后重复发送 keep-alive packet ,并且重复发送一定次数( 2000 XP 2003 系统默认为 5 次 , Vista 后的系统默认为 10 次)。
  3. 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。

3.tcp粘包

www.cnblogs.com/sui77626523…

三面:1小时

项目:问了所有项目,设计思路,遇到最大的困难

这一块你如实说自己的经历就可以了,没什么好说的

操作系统:

1.复盘了一面没答好的malloc(从内存布局,空闲块链表分配算法以及系统调用全都说了一遍)

2.copyonwrite

CopyOnWrite 思想

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种通用优化策略。其核心思想是,如果有多个调用者(Callers)同时访问相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

通俗易懂的讲,写入时复制技术就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。

JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是采用了 COW 思想,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。

源码分析

我们先来看看 CopyOnWriteArrayList 的 add() 方法,其实也非常简单,就是在访问的时候加锁,拷贝出来一个副本,先操作这个副本,再把现有的数据替换为这个副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码    public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

CopyOnWriteArrayList 的 get(int index) 方法就是普通的无锁访问。

1
2
3
4
5
6
7
8
kotlin复制代码    public E get(int index) {
return get(getArray(), index);
}

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}

优点和缺点

1.优点

对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。

CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。

2.缺点

数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。

内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap

计算机网络:

1.如何在应用层保证udp可靠传输
2.https和http区别以及https的ssl握手机制
3.https为什么要采用对称和非对称加密结合的方式

智力题:

1.rand5到rand7,算是拒绝采样吧,和一面思路很像
2.有32个大小相同的石头,有一把称,请问最少称多少次可以找出质量最大的石头
第二大呢
第n大呢

手撕:

蛇形遍历数组

谈心:

平常看什么书,看哪些技术网站
能干多久
反问

hr面

这个没什么好说的,扯不到技术,就随便聊聊,别把天聊死了就行。

没被问到数据库有点诧异,毕竟准备最充分的就是数据库,明显感觉部门重视操作系统和计算机网络,算法无论是手撕还是口述都有,所以还是需要重视。


时间有点不够了,如果有空再把后面补上吧,先这样,瑞思拜!资料记得拿

  • Java基础知识总结
  • 一线互联网公司Java面试核心知识点

往期热文:

  • Java基础知识总结
  • 性能调优系列专题(JVM、MySQL、Nginx and Tomcat)
  • 从被踢出局到5个30K+的offer,一路坎坷走来,沉下心,何尝不是前程万里
  • 100个Java项目解析,带源代码和学习文档!

end

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深度剖析Spring AOP底层原理源码 前言 最后

发表于 2021-05-24

前言

关于Spring AOP的知识点总结了一个图谱,分享给大家:

Spring 知识总结.jpg

AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了将不同的关注点分离出来的效果。本文深入剖析Spring的AOP的原理。

AOP相关的概念

1)Aspect:切面,切入系统的一个切面。比如事务管理是一个切面,权限管理也是一个切面;

2)Join point:连接点,也就是可以进行横向切入的位置;

3)Advice:通知,切面在某个连接点执行的操作(分为:Before advice,After returning advice,After throwing advice,After (finally) advice,Around advice);

4)Pointcut:切点,符合切点表达式的连接点,也就是真正被切入的地方;

AOP 的实现原理

AOP分为静态AOP和动态AOP。静态AOP是指AspectJ实现的AOP,他是将切面代码直接编译到Java类文件中。动态AOP是指将切面代码进行动态织入实现的AOP。Spring的AOP为动态AOP,实现的技术为:JDK提供的动态代理技术 和 CGLIB(动态字节码增强技术)。尽管实现技术不一样,但都是基于代理模式,都是生成一个代理对象。

1) JDK动态代理

主要使用到 InvocationHandler 接口和 Proxy.newProxyInstance() 方法。JDK动态代理要求被代理实现一个接口,只有接口中的方法才能够被代理。其方法是将被代理对象注入到一个中间对象,而中间对象实现InvocationHandler接口,在实现该接口时,可以在 被代理对象调用它的方法时,在调用的前后插入一些代码。而 Proxy.newProxyInstance() 能够利用中间对象来生产代理对象。插入的代码就是切面代码。所以使用JDK动态代理可以实现AOP。我们看个例子:

被代理对象实现的接口,只有接口中的方法才能够被代理:

1
2
3
4
csharp复制代码public interface UserService {
public void addUser(User user);
public User getUser(int id);
}

被代理对象:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public class UserServiceImpl implements UserService {
public void addUser(User user) {
System.out.println("add user into database.");
}
public User getUser(int id) {
User user = new User();
user.setId(id);
System.out.println("getUser from database.");
return user;
}
}

代理中间类:

`import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ProxyUtil implements InvocationHandler {
private Object target; // 被代理的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("do sth before....");
Object result = method.invoke(target, args);
System.out.println("do sth after....");
return result;
}
ProxyUtil(Object target){
this.target = target;
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}

}`

测试:

`import java.lang.reflect.Proxy;
import net.aazj.pojo.User;

public class ProxyTest {
public static void main(String[] args){
Object proxyedObject = new UserServiceImpl(); // 被代理的对象
ProxyUtil proxyUtils = new ProxyUtil(proxyedObject);

1
2
3
4
5
6
scss复制代码    // 生成代理对象,对被代理对象的这些接口进行代理:UserServiceImpl.class.getInterfaces()
UserService proxyObject = (UserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
UserServiceImpl.class.getInterfaces(), proxyUtils);
proxyObject.getUser(1);
proxyObject.addUser(new User());
}

}`

执行结果:

do sth before.... getUser from database. do sth after.... do sth before.... add user into database. do sth after....

我们看到在 UserService接口中的方法addUser 和 getUser方法的前面插入了我们自己的代码。这就是JDK动态代理实现AOP的原理。

我们看到该方式有一个要求,被代理的对象必须实现接口,而且只有接口中的方法才能被代理。

2)CGLIB(code generate libary)

字节码生成技术实现AOP,其实就是继承被代理对象,然后Override需要被代理的方法,在覆盖该方法时,自然是可以插入我们自己的代码的。因为需要Override被代理对象的方法,所以自然CGLIB技术实现AOP时,就必须要求需要被代理的方法不能是final方法,因为final方法不能被子类覆盖。我们使用CGLIB实现上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码package net.aazj.aop;

import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CGProxy implements MethodInterceptor{
private Object target; // 被代理对象
public CGProxy(Object target){
this.target = target;
}
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy proxy) throws Throwable {
System.out.println("do sth before....");
Object result = proxy.invokeSuper(arg0, arg2);
System.out.println("do sth after....");
return result;
}
public Object getProxyObject() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass()); // 设置父类
// 设置回调
enhancer.setCallback(this); // 在调用父类方法时,回调 this.intercept()
// 创建代理对象
return enhancer.create();
}
}
1
2
3
4
5
6
7
8
9
java复制代码public class CGProxyTest {
public static void main(String[] args){
Object proxyedObject = new UserServiceImpl(); // 被代理的对象
CGProxy cgProxy = new CGProxy(proxyedObject);
UserService proxyObject = (UserService) cgProxy.getProxyObject();
proxyObject.getUser(1);
proxyObject.addUser(new User());
}
}

输出结果:

1
2
3
4
5
6
erlang复制代码do sth before....
getUser from database.
do sth after....
do sth before....
add user into database.
do sth after....

我们看到达到了同样的效果。它的原理是生成一个父类enhancer.setSuperclass(this.target.getClass())的子类enhancer.create(),然后对父类的方法进行拦截enhancer.setCallback(this). 对父类的方法进行覆盖,所以父类方法不能是final的。

3)接下来我们看下spring实现AOP的相关源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arduino复制代码@SuppressWarnings("serial")
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface()) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

从上面的源码我们可以看到:

1
2
3
4
arduino复制代码if (targetClass.isInterface()) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);

如果被代理对象实现了接口,那么就使用JDK的动态代理技术,反之则使用CGLIB来实现AOP,所以Spring默认是使用JDK的动态代理技术实现AOP的。

JdkDynamicAopProxy的实现其实很简单:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {    
@Override
public Object getProxy(ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

Spring AOP的配置

Spring中AOP的配置一般有两种方法,一种是使用 aop:config 标签在xml中进行配置,一种是使用注解以及@Aspect风格的配置。

1)基于aop:config的AOP配置

下面是一个典型的事务AOP的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码    <tx:advice id="transactionAdvice" transaction-manager="transactionManager"?>
<tx:attributes >
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="append*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />

<tx:method name="get*" propagation="SUPPORTS" />
<tx:method name="find*" propagation="SUPPORTS" />
<tx:method name="load*" propagation="SUPPORTS" />
<tx:method name="search*" propagation="SUPPORTS" />

<tx:method name="*" propagation="SUPPORTS" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* net.aazj.service..*Impl.*(..))" />
<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
</aop:config>

再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码    <bean id="aspectBean" class="net.aazj.aop.DataSourceInterceptor"/>
<aop:config>
<aop:aspect id="dataSourceAspect" ref="aspectBean">
<aop:pointcut id="dataSourcePoint" expression="execution(public * net.aazj.service..*.getUser(..))" />
<aop:pointcut expression="" id=""/>
<aop:before method="before" pointcut-ref="dataSourcePoint"/>
<aop:after method=""/>
<aop:around method=""/>
</aop:aspect>

<aop:aspect></aop:aspect>
</aop:config>
1
2
3
4
5
6
7
csharp复制代码<aop:aspect> 配置一个切面;<aop:pointcut>配置一个切点,基于切点表达式;<aop:before>,<aop:after>,<aop:around>是定义不同类型的advise. aspectBean 是切面的处理bean:

public class DataSourceInterceptor {
public void before(JoinPoint jp) {
DataSourceTypeManager.set(DataSources.SLAVE);
}
}

2) 基于注解和@Aspect风格的AOP配置

我们以事务配置为例:首先我们启用基于注解的事务配置

1
2
3
xml复制代码
<!-- 使用annotation定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />

然后扫描Service包:

1
ini复制代码    <context:component-scan base-package="net.aazj.service,net.aazj.aop" />

最后在service上进行注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;

@Transactional (readOnly=true)
public User getUser(int userId) {
System.out.println("in UserServiceImpl getUser");
System.out.println(DataSourceTypeManager.get());
return userMapper.getUser(userId);
}

public void addUser(String username){
userMapper.addUser(username);
// int i = 1/0; // 测试事物的回滚
}

public void deleteUser(int id){
userMapper.deleteByPrimaryKey(id);
// int i = 1/0; // 测试事物的回滚
}

@Transactional (rollbackFor = BaseBusinessException.class)
public void addAndDeleteUser(String username, int id) throws BaseBusinessException{
userMapper.addUser(username);
this.m1();
userMapper.deleteByPrimaryKey(id);
}

private void m1() throws BaseBusinessException {
throw new BaseBusinessException("xxx");
}

public int insertUser(User user) {
return this.userMapper.insert(user);
}
}

这种事务配置方式,不需要我们书写pointcut表达式,而是我们在需要事务的类上进行注解。但是如果我们自己来写切面的代码时,还是要写pointcut表达式。下面看一个例子(自己写切面逻辑):

首先去扫描@Aspect注解定义的切面:

1
ini复制代码<context:component-scan base-package="net.aazj.aop" />

启用@AspectJ风格的注解:

1
makefile复制代码<aop:aspectj-autoproxy />

这里有两个属性,<aop:aspectj-autoproxy proxy-target-class=”true” expose-proxy=”true”/>, proxy-target-class=”true” 这个最好不要随便使用,它是指定只能使用CGLIB代理,那么对于final方法时会抛出错误,所以还是让spring自己选择是使用JDK动态代理,还是CGLIB. expose-proxy=”true”的作用后面会讲到。

切面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect // for aop
@Component // for auto scan
@Order(0) // execute before @Transactional
public class DataSourceInterceptor {
@Pointcut("execution(public * net.aazj.service..*.get*(..))")
public void dataSourceSlave(){};

@Before("dataSourceSlave()")
public void before(JoinPoint jp) {
DataSourceTypeManager.set(DataSources.SLAVE);
}
}

我们使用到了 @Aspect 来定义一个切面;@Component是配合context:component-scan/,不然扫描不到;@Order定义了该切面切入的顺序,因为在同一个切点,可能同时存在多个切面,那么在这多个切面之间就存在一个执行顺序的问题。该例子是一个切换数据源的切面,那么他应该在 事务处理 切面之前执行,所以我们使用 @Order(0) 来确保先切换数据源,然后加入事务处理。@Order的参数越小,优先级越高,默认的优先级最低:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码/**
* Annotation that defines ordering. The value is optional, and represents order value
* as defined in the {@link Ordered} interface. Lower values have higher priority.
* The default value is {@code Ordered.LOWEST_PRECEDENCE}, indicating
* lowest priority (losing to any other specified order value).
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Order {
/**
* The order value. Default is {@link Ordered#LOWEST_PRECEDENCE}.
* @see Ordered#getOrder()
*/
int value() default Ordered.LOWEST_PRECEDENCE;
}

关于数据源的切换可以参加专门的博文:www.cnblogs.com/digdeep/p/4…

3)切点表达式(pointcut)

上面我们看到,无论是 aop:config 风格的配置,还是 @Aspect 风格的配置,切点表达式都是重点。都是我们必须掌握的。

1> pointcut语法形式(execution):

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
带有 ? 号的部分是可选的,所以可以简化成:ret-type-pattern name-pattern(param_pattern) 返回类型,方法名称,参数三部分来匹配。

配置起来其实也很简单: * 表示任意返回类型,任意方法名,任意一个参数类型; .. 连续两个点表示0个或多个包路径,还有0个或多个参数。就是这么简单。看下例子:

execution(* net.aazj.service...get(..)) :表示net.aazj.service包或者子包下的以get开头的方法,参数可以是0个或者多个(参数不限);

execution(* net.aazj.service.AccountService.*(..)): 表示AccountService接口下的任何方法,参数不限;

注意这里,将类名和包路径是一起来处理的,并没有进行区分,因为类名也是包路径的一部分。

参数param-pattern部分比较复杂: () 表示没有参数,(..)参数不限,(*,String) 第一个参数不限类型,第二参数为String.

2> within() 语法:

within()只能指定(限定)包路径(类名也可以看做是包路径),表示某个包下或者子报下的所有方法:

1
scss复制代码within(net.aazj.service.*), within(net.aazj.service..*),within(net.aazj.service.UserServiceImpl.*)

3> this() 与 target():

this是指代理对象,target是指被代理对象(目标对象)。所以 this() 和 target() 分别限定 代理对象的类型和被代理对象的类型:

1
2
3
scss复制代码this(net.aazj.service.UserService): 实现了UserService的代理对象(中的所有方法);

target(net.aazj.service.UserService): 被代理对象实现了UserService(中的所有方法);

4> args():

限定方法的参数的类型:

args(net.aazj.pojo.User): 参数为User类型的方法。

5> @target(), @within(), @annotation(), @args():

这些语法形式都是针对注解的,比如带有某个注解的类,带有某个注解的方法,参数的类型带有某个注解:

1
2
less复制代码@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)

两者都是指被代理对象类上有 @Transactional 注解的(类的所有方法),(两者似乎没有区别???)

@annotation(org.springframework.transaction.annotation.Transactional): 方法 带有@Transactional 注解的所有方法

@args(org.springframework.transaction.annotation.Transactional):参数的类型 带有@Transactional 注解的所有方法

6> bean(): 指定某个bean的名称

bean(userService): bean的id为 “userService” 的所有方法;

bean(*Service): bean的id为 “Service”字符串结尾的所有方法;

另外注意上面这些表达式是可以利用 ||, &&, ! 进行自由组合的。比如:execution(public * net.aazj.service..*.getUser(..)) && args(Integer,..)

向注解处理方法传递参数

有时我们在写注解处理方法时,需要访问被拦截的方法的参数。此时我们可以使用 args() 来传递参数,下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码@Aspect
@Component // for auto scan
//@Order(2)
public class LogInterceptor {
@Pointcut("execution(public * net.aazj.service..*.getUser(..))")
public void myMethod(){};

@Before("myMethod()")
public void before() {
System.out.println("method start");
}

@After("myMethod()")
public void after() {
System.out.println("method after");
}

@AfterReturning("execution(public * net.aazj.mapper..*.*(..))")
public void AfterReturning() {
System.out.println("method AfterReturning");
}

@AfterThrowing("execution(public * net.aazj.mapper..*.*(..))")
// @Around("execution(public * net.aazj.mapper..*.*(..))")
public void AfterThrowing() {
System.out.println("method AfterThrowing");
}

@Around("execution(public * net.aazj.mapper..*.*(..))")
public Object Around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("method Around");
SourceLocation sl = jp.getSourceLocation();
Object ret = jp.proceed();
System.out.println(jp.getTarget());
return ret;
}

@Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
public void before3(int userId) {
System.out.println("userId-----" + userId);
}

@Before("myMethod()")
public void before2(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println("userId11111: " + (Integer)args[0]);
System.out.println(jp.getTarget());
System.out.println(jp.getThis());
System.out.println(jp.getSignature());
System.out.println("method start");
}
}

方法:

1
2
3
4
java复制代码    @Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
public void before3(int userId) {
System.out.println("userId-----" + userId);
}

它会拦截 net.aazj.service 包下或者子包下的getUser方法,并且该方法的第一个参数必须是int型的,那么使用切点表达式args(userId,..)就可以使我们在切面中的处理方法before3中可以访问这个参数。

before2方法也让我们知道也可以通过 JoinPoint 参数来获得被拦截方法的参数数组。JoinPoint 是每一个切面处理方法都具有的参数,@Around类型的具有的参数类型为ProceedingJoinPoint。通过JoinPoint或者ProceedingJoinPoint参数可以访问到被拦截对象的一些信息(参见上面的before2方法)。

Spring AOP的缺陷

因为Spring AOP是基于动态代理对象的,那么如果target中的方法不是被代理对象调用的,那么就不会织入切面代码,看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;

@Transactional (readOnly=true)
public User getUser(int userId) {
return userMapper.getUser(userId);
}

public void addUser(String username){
getUser(2);
userMapper.addUser(username);
}

看到上面的addUser() 方法中,我们调用了 getUser() 方法,而getUser() 方法是谁调用的呢?是UserServiceImpl的实例,不是代理对象,那么getUser()方法就不会被织入切面代码。

切面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Aspect
@Component
public class AOPTest {
@Before("execution(public * net.aazj.service..*.getUser(..))")
public void m1(){
System.out.println("in m1...");
}
@Before("execution(public * net.aazj.service..*.addUser(..))")
public void m2(){
System.out.println("in m2...");
}
}

执行如下代码:

1
2
3
4
5
6
7
8
typescript复制代码public class Test {
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[]{"config/spring-mvc.xml","config/applicationContext2.xml"});

UserService us = context.getBean("userService", UserService.class);
if(us != null){
us.addUser("aaa");

输出结果如下:

1
erlang复制代码in m2...

虽然getUser()方法被调用了,但是因为不是代理对象调用的,所以AOPTest.m1()方法并没有执行。这就是Spring aop的缺陷。解决方法如下:

首先:将 <aop:aspectj-autoproxy /> 改为:

1
ini复制代码<aop:aspectj-autoproxy expose-proxy="true"/>

然后,修改UserServiceImpl中的 addUser() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;

@Transactional (readOnly=true)
public User getUser(int userId) {
return userMapper.getUser(userId);
}

public void addUser(String username){
((UserService)AopContext.currentProxy()).getUser(2);
userMapper.addUser(username);
}

((UserService)AopContext.currentProxy()).getUser(2); 先获得当前的代理对象,然后在调用 getUser() 方法,就行了。

expose-proxy=”true” 表示将当前代理对象暴露出去,不然 AopContext.currentProxy() 或得的是 null .

修改之后的运行结果:

1
2
erlang复制代码in m2...
in m1...

最后

我这边整理了一份Spring AOP资料文档、Spring系列全家桶、Java的系统化资料:(包括Java核心知识点、面试专题和20年最新的互联网真题、电子书等)有需要的朋友可以关注公众号【程序媛小琬】即可获取。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

无题

发表于 2021-05-24

昨天,有个读者私信我说,“老师正在教 Swing,这个知识点还需要学习吗?”

说句实在话,刚看到这个问题的时候,我是想骂娘的!不是骂读者啊,你懂得,骂学校,骂老师。但我硬是掐着自己的大腿忍住了,很客气地回复了一句“对,甚至可以不学”。

有点点到为止的味道。

我之所以这么委婉,是希望读者不要对学校的老师心存偏见,影响了后面的学习进度。但我内心其实是非常愤怒的,都什么时候了,Java 中的 Swing 早被淘汰了,哪个项目还会用这玩意编写客户端界面呢!

学 Swing、AWT 这些图形化组件纯属浪费时间!

可能有些老师也没真正在公司里实战过,拿本书就上来教,导致学生把大量的时间浪费在不需要学习的知识点上,以至于毕业找工作的时候,能力达不到招聘的要求。

有些学校很装逼的,老师都是拿自己的书来讲课的,反正我大学那会就是,觉得老师挺牛逼的呀,都自己出书了!后来等我自己出书了,才发现,原来出书也没什么难的呀(嗯,我在装逼)!

有些学校还不错,会选一些计算机专业的经典著作,比如说各种黑皮书作为教材。我只能说,能在这样的学校里待着的同学都是幸运的。

怕就怕,有些老师把教材直接拿过来不做过滤。不管是《Java 编程思想》还是《Java 核心技术卷 1》,里面都有大量的篇幅介绍 Swing、AWT 这些,但有工作经验的人都知道,这些在工作中是完全不用的东西。

我在推荐这两本书的时候,都会明确指出,不要去学这些东西,直接跳过,甚至可以直接撕掉。

希望出版社的编辑朋友们再版这些书的时候,能把这些不用的东西剔除掉,以免影响了初学 Java 的读者。当然了,我知道,这很难做到,删了等于书就变薄了,计算机专业领域的书都一本比一本厚,一本比一本贵!删了估计就卖不到这个价了,血亏。

那对于初学者来说,该怎么办呢?

当然是多关注一下真正有实力的编程大佬们,比如说我的好朋友 JavaGuide、江南一点雨等等,以及二哥我(加粗、加感叹号)!!!!!!!!!!

有问题,直接过来问,然后就知道学习的重点了!比跟着某些老师瞎学要强多了。

这不,刚好有编程新手过来问问题,二哥特别热情,还顺带做了一次心理按摩,帮助读者缓解压力,疏通脉络。

接下来,我来重点说说,怎么才能找到实习工作。

二哥的读者群体里有很大一部分都是大学生,所以如果你今天看了这篇文章,并且看到了这里,OK,恭喜你,直了,不不不,值了!

找 Java 后端的实习工作,首先要明白一点,校招不同社招,不会要求太多的项目经验,这一点,看似很浅显的道理,但往往很多学弟学妹容易在这一点上纠结,我缺项目经验啊,要狠狠地补,结果把计算机基础方面的学习给抛之脑后了,导致拣了芝麻丢了西瓜,得不偿失。

临到毕业季,很多公司就会到学校去校招,标准其实很简单,Java 方面能做一些简单的增删改查就行,但学习能力一定要强,基础一定要扎实,方便后面培养。因为刚入职的新人,靠谱的公司都会安排师父带,还会安排一定量的培训,公司是不怕你项目经验不足的,怕的是你学习进度跟不上。

我当年在外企实习的时候,就是,一上来,直接给两个月的时间,做个小项目,我当时被安排的是做个仿 Win7 的计算器(源码和成品还放在 CSDN 的资源库里),嗯,当时用的 Swing 做的界面,但那已经是 12 年前的事了!等过了辞退阶段,公司就安排了很多培训,培训编码规范了、开发流程了等等这些。

差不多工作了一年半的时候,我做了小组 Leader 的时候,还给新人培训过好多次,讲 Flex 是怎么用的。嗯,这玩意也被淘汰了!

(时不时就透露出我是一名 old 码农的浓烈气息)

好了,不废话了。来说下 Java 后端实习生的最基本的要求。

第一,经常用的工具一定要熟练。

比如说 Intellij IDEA 一定要能熟练地使用。如果你想写出质量杠杠的 Java 代码,又想追求开发效率,用 Intellij IDEA 准没错!可以去 GitHub 上看一下 Intellij IDEA 中文版的教程,快捷键设置了、代码模板了、常用插件了,这些都有详细地说明。

github.com/judasn/Inte…

我举个简单的例子,像 **CheckStyle、Alibaba Java 代码规范、SonarLint 这三个插件是一定要装的,可以极大程度上保证代码质量。**除了这 3 个,还有呢:

这些都能在很大程度上提高编写代码的效率。

比如说 Git 一定要能熟练的使用。大家都知道,版本控制系统非常重要!!!!!!

即便你只是一个人在编码,它也可以帮助你创建项目的快照、记录每个改动、创建不同的分支等等。如果你参与的是多人协作,它更是一个无价之宝,你不仅可以看到别人对代码的修改,还可以同时解决由于并行开发带来的冲突。版本控制系统有很多,其中最突出的代表就是 Git。

想要把 Git 学好的话,可以看一下 Pro Git 中文版 PDF,可以说是学习 Git 的最佳教程,因为作者就是 Git 的一个主要实现的贡献者。

第二,Java 基础一定要扎实。

像 Java 的数据类型、Java 的运算符、Java 的流程控制、Java 的面向对象、Java 的异常处理,这些都是最基础的东西,是初学 Java 的时候必须掌握的知识点。

然后是 Java 集合框架、Java IO、Java 网络编程、Java 多线程并发、Java 虚拟机,这些是 Java 中比较核心的知识点,也是必须要掌握的。

大家可以先看看我整理的这份 GitHub 上星标 115k+ 的 Java 教程,里面涵盖了 Java 所有的知识点,包括 Java 语法、Java 集合框架、Java IO、Java 并发编程和 Java 虚拟机,内容不多,只讲重点。

GitHub 星标 115k+的 Java 教程,超级硬核!

Java 集合框架中,像 ArrayList 与 LinkedList 之间的差别,HashMap 的数据结构、工作原理、哈希冲突、扩容过程、拉链法导致链表过深时为什么不用二叉查找树而选择红黑树、Java 8 时 HashMap 发生了什么变化、HashMap & ConcurrentHashMap 的区别等等,是面试的时候考察的重点。

多线程并发算是 Java 基础当中的难点之一,需要掌握的知识点我用思维导图列一下。

还有 Java 虚拟机,要学的知识点有:Java 虚拟机内存结构、垃圾收集策略与算法、内存分配与回收策略、Java 虚拟机性能调优、类文件结构、类加载机制等等。

怎么学呢?推荐大家一本书,就一本书,周志明老师的《深入理解 Java 虚拟机》,一开始啃起来可能会比较痛苦,但我必须得负责任的告诉你,多啃一啃,面试的时候很容易就把面试官给惊艳了。

第三,掌握 MySQL 数据库。

先来看一下 MySQL 的知识体系,还是用思维导图的形式。

对于基本的 SQL 语句,推荐《SQL 必知必会》这本书,很薄,很快就能学完了。然后是《MySQL 必知必会》,讲的很全,但很简练,非常适合零基础的学弟学妹。如果想更深入的学习 MySQL 的话,推荐两本书,《高性能 MySQL》和《MySQL 技术内幕:InnoDB 存储引擎》。

如果英语功底比较扎实的话,推荐看 MySQL Tutorial 这个英文网站教程,遇到阻碍的话,可以借助一下谷歌翻译。

www.mysqltutorial.org/

如果你有一台自己的服务器(学生购买的话也比较便宜)的话,可以到阿里云大学上学习,里面有 18 门免费课程,从 SQL 到 NoSQL,从自建到云上数据库,一站式学习+自测。

第四,掌握 Spring 系列框架。

想成为一名合格的 Java 后端开发工程师,Spring 的系列框架是必须得掌握的,yyds。从 SSM(Spring+Spring MVC+MyBatis)到 Spring Boot,再到 Spring Cloud,都得会用。

事实上,Spring 早已成为 Java 后端开发的行业标准,如何用好 Spring,是 Java 程序员的必修课之一。由于 Spring 的快速发展,它逐渐从一个轻量级的开发框架变成了一个“庞然大物”,越来越笨重,导致搭建一个应用程序的成本越来越高,于是 Spring Boot 就应运而生了,它能帮助开发者快速搭建出一个独立应用,只需要很少的配置就可以了。可以毫不夸张的说,Spring Boot,牛逼!

Spring Cloud 利用 Spring Boot 的开发便利性,巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,可以说,Spring Cloud 的诞生,又一次解放了Java 程序员的生产力。

关于 SSM 的学习,可以看下江南一点雨在 B 站上的视频,能为后面学习 Spring Boot 打下坚实的基础。

www.bilibili.com/video/BV1NX…

我来简单介绍一下 SSM 具体是什么。

1)Spring 是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。它可以装载 Bean,也就是 Java 中的类,包括 Service、Dao 里面的;利用控制反转这个机制,我们就不用在每次使用类的时候用 new 关键字声明并初始化。另外,Spring 事务管理也是开发中常用到的。

2)来看一下 SpringMVC 的工作原理:

  • 客户端发送请求到 DispacherServlet(分发器)
  • 由 DispacherServlet 控制器查询 HanderMapping,找到处理请求的 Controller
  • Controller 调用业务逻辑处理后,返回 ModelAndView
  • DispacherSerclet 查询视图解析器,找到 ModelAndView 指定的视图
  • 视图负责将结果显示到客户端

3)MyBatis 是一个支持普通 SQL 查询,存储过程和高级映射的持久层框架。它对 JDBC 做了封装,让数据库底层操作变的更透明了。 MyBatis 的操作都是围绕着一个叫 sqlSessionFactory 的实例展开的,通过配置文件关联到各个实体类的 Mapper 文件,再由 Mapper 文件映射每个类对数据库所需要执行的 SQL 语句。

关于 Spring Boot、Spring Cloud 的学习,可以看纯洁的微笑的博客,访问量在千万级别以上,影响了无数的初学者,我也是被影响者之一。

  • spring-boot - 纯洁的微笑博客
  • spring-cloud - 纯洁的微笑博客

至于说 Redis、Dubbo 等等等等,我就不再强调了,就上面的这 4 个最基本的要求,就够喝一壶了。还有像算法与数据结构、操作系统、编译原理、计算机网络、计算机组成原理等等这些通用层面的,我也不再一一强调了。大学阶段,有时间就搞这些,如果学校已经安排了这些课程,那就更好了,学,一定要学!

大学时光说短不短,说长不长,作为过来人,真的是感觉一眨眼就过去了;对于正在读大学的学弟学妹们来说,正是青春的好时光,有很多事情要做,忙着社交,忙着拓展,忙着锻炼,忙着课业,忙着恋爱,忙不得开交。怎么在这么多事情的夹缝中努力学习呢?

就是一定要学会做减法,不该学的内容就不要再花时间学习了。像前面提到的 Swing、AWT 这些东西早已经被淘汰了,还有像 Applet、JSP 这种的,如果大学还在教这些,真的是学生的不幸。

与其把时间花费到这些没用的知识点上,真不如:

1)痴迷于数据结构与算法

算法题就好像科举考试时代背的八股文,是知识改变命运的代表作。你不刷,就很过算法题这一关,因为不仅要考,还能提高你的编程功底。我一直给大家推荐的 Java 版的 LeetCode 刷题笔记,一定要下载下来刷一刷(可以点击下面的链接去下载)。

吃完 300 道 LeetCode 题后,我胖得快炸了!

2)热衷于 ACM

3)尝试 Unix、Linux 环境下编程

4)醉心于网络编程和多线程编程,对 TCP/IP、HTTP 等网络协议有很深的理解

好了,今天的分享就到这吧。

我是二哥呀,希望能给学弟学妹们一些帮助和启发,记得点赞哟~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

运维:你们 JAVA 服务内存占用太高,还只增不减!告警了,

发表于 2021-05-24

先点赞再看,养成好习惯

某天,运维老哥突然找我:“你们的某 JAVA 服务内存占用太高,告警了!GC 后也没释放,内存只增不减,是不是内存泄漏了!”

然后我赶紧看了下监控,一切正常,距离上次发版好几天了,FULL GC 一次没有,YoungGC,十分钟一次,堆空闲也很充足。

运维:“你们这个服务现在堆内存 used 才 800M,但这个 JAVA 进程已经占了 6G 内存了,是不是你们程序出啥内存泄露的 bug 了!”

我想都没想,直接回了一句:“不可能,我们服务非常稳定,不会有这种问题!”

b64da6adly1galljp2tdqj20hs0hswg8 (1).jpg

不过说完之后,内心还是自我质疑了一下:会不会真有什么bug?难道是堆外泄露?线程没销毁?导致内存泄露了???

然后我很“镇定”的补了一句:“我先上服务器看看啥情况”,被打脸可就不好了,还是不要装太满的好……

迅速上登上服务器又仔细的查看了各种指标,Heap/GC/Thread/Process 之类的,发现一切正常,并没有什么“泄漏”的迹象。

和运维的“沟通”

我们这个服务很正常啊,各个指标都ok,什么内存只增不减,在哪呢

9150e4e5ly1flbi3ko540g208c08cdg9.jpg

运维:你看你们这个 JAVA 服务,堆现在 used 才 400MB,但这个进程现在内存占用都 6G 了,还说没问题?肯定是内存泄露了,锅接好,赶紧回去查问题吧

然后我指着监控信息,让运维看:“大哥你看这监控历史,堆内存是达到过 6G 的,只是后面 GC 了,没问题啊!”

运维:“回收了你这内存也没释放啊,你看这个进程 Res 还是 6G,肯定有问题啊”

我心想这运维怕不是个der,JVM GC 回收和进程内存又不是一回事,不过还是和得他解释一下,不然一直baba个没完

“JVM 的垃圾回收,只是一个逻辑上的回收,回收的只是 JVM 申请的那一块逻辑堆区域,将数据标记为空闲之类的操作,不是调用 free 将内存归还给操作系统”

运维顿了两秒后,突然脸色一转,开始笑起来:“咳咳,我可能没注意这个。你再给我讲讲 JVM 的这个内存管理/回收和进程上内存的关系呗”

虽然我内心是拒绝的,但得罪谁也不能得罪运维啊,想想还是给大哥解释解释,“增进下感情”
e16fc503gy1flup1twc2bj206o05idg2 (1).jpg

操作系统 与 JVM的内存分配

JVM 的自动内存管理,其实只是先向操作系统申请了一大块内存,然后自己在这块已申请的内存区域中进行“自动内存管理”。JAVA 中的对象在创建前,会先从这块申请的一大块内存中划分出一部分来给这个对象使用,在 GC 时也只是这个对象所处的内存区域数据清空,标记为空闲而已

运维:“原来是这样,那按你的意思,JVM 就不会将 GC 回收后的空闲内存还给操作系统了吗?”

为什么不把内存归还给操作系统?

JVM 还是会归还内存给操作系统的,只是因为这个代价比较大,所以不会轻易进行。而且不同垃圾回收器 的内存分配算法不同,归还内存的代价也不同。

比如在清除算法(sweep)中,是通过空闲链表(free-list)算法来分配内存的。简单的说就是将已申请的大块内存区域分为 N 个小区域,将这些区域同链表的结构组织起来,就像这样:

每个 data 区域可以容纳 N 个对象,那么当一次 GC 后,某些对象会被回收,可是此时这个 data 区域中还有其他存活的对象,如果想将整个 data 区域释放那是肯定不行的。

所以这个归还内存给操作系统的操作并没有那么简单,执行起来代价过高,JVM 自然不会在每次 GC 后都进行内存的归还。

怎么归还?

虽然代价高,但 JVM 还是提供了这个归还内存的功能。JVM 提供了-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio 两个参数,用于配置这个归还策略。

  • MinHeapFreeRatio 代表当空闲区域大小下降到该值时,会进行扩容,扩容的上限为 Xmx
  • MaxHeapFreeRatio 代表当空闲区域超过该值时,会进行“缩容”,缩容的下限为Xms

不过虽然有这个归还的功能,不过因为这个代价比较昂贵,所以 JVM 在归还的时候,是线性递增归还的,并不是一次全部归还。

但是但是但是,经过实测,这个归还内存的机制,在不同的垃圾回收器,甚至不同的 JDK 版本中还不一样!

不同版本&垃圾回收器下的表现不同

下面是我之前跑过的测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public static void main(String[] args) throws IOException, InterruptedException {
List<Object> dataList = new ArrayList<>();
for (int i = 0; i < 25; i++) {
byte[] data = createData(1024 * 1024 * 40);// 40 MB
dataList.add(data);
}
Thread.sleep(10000);
dataList = null; // 待会 GC 直接回收
for (int i = 0; i < 100; i++) {
// 测试多次 GC
System.gc();
Thread.sleep(1000);
}
System.in.read();
}
public static byte[] createData(int size){
byte[] data = new byte[size];
for (int i = 0; i < size; i++) {
data[i] = Byte.MAX_VALUE;
}
return data;
}
JAVA 版本 垃圾回收器 VM Options 是否可以“归还”
JAVA 8 UseParallelGC(ParallerGC + ParallerOld) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 否
JAVA 8 CMS+ParNew -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 是
JAVA 8 UseG1GC(G1) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseG1GC 是
JAVA 11 UseG1GC(G1) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 是
JAVA 16 UseZGC(ZGC) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseZGC 否

测试结果刷新了我的认知。,MaxHeapFreeRatio 这个参数好像并没有什么用,无论我是配置40,还是配置90,回收的比例都有和实际的结果都有很大差距。

但是文档中,可不是这么说的……

而且 ZGC 的结果也是挺意外的,JEP 351 提到了 ZGC 会将未使用的内存释放,但测试结果里并没有。

除了以上测试结果,stackoverflow 上还有一些其他的说法,我就没有再一一测试了

  1. JAVA 9 后-XX:-ShrinkHeapInSteps参数,可以让 JVM 已非线性递增的方式归还内存
  2. JAVA 12 后的 G1,再应用空闲时,可以自动的归还内存

所以,官方文档的说法,也只能当作一个参考,JVM 并没有过多的透露这个实现细节。

不过这个是否归还的机制,除了这位“热情”的运维老哥,一般人也不太会去关心,巴不得 JVM 多用点内存,少 GC 几回……

而且别说空闲自动归还了,我们希望的是一启动就分配个最大内存,避免它运行中扩容影响服务;所以一般 JAVA 程序还会将 Xms和Xmx配置为相等的大小,避免这个扩容的操作。

听到这里,运维老哥若有所思的说到:“那是不是只要我把 Xms 和 Xmx 配置成一样的大小,这个 JAVA 进程一启动就会占用这个大小的内存呢?”

我接着答到:“不会的,哪怕你 Xms6G,启动也只会占用实际写入的内存,大概率达不到 6G,这里还涉及一个操作系统内存分配的小知识”

Xms6G,为什么启动之后 used 才 200M?

进程在申请内存时,并不是直接分配物理内存的,而是分配一块虚拟空间,到真正堆这块虚拟空间写入数据时才会通过缺页异常(Page Fault)处理机制分配物理内存,也就是我们看到的进程 Res 指标。

可以简单的认为操作系统的内存分配是“惰性”的,分配并不会发生实际的占用,有数据写入时才会发生内存占用,影响 Res。

所以,哪怕配置了Xms6G,启动后也不会直接占用 6G 内存,实际占用的内存取决于你有没有往这 6G 内存区域中写数据的。

运维:“卧槽,还有惰性分配这种东西!长知识了”

我:“这下明白了吧,这个内存情况是正常的,我们的服务一点问题都没有”

运维:“🐂🍺,是我理解错了,你们这个服务没啥问题”

我:“嗯呐,没事那我先去忙(摸鱼)了”

image.png

总结

对于大多数服务端场景来说,并不需要JVM 这个手动释放内存的操作。至于 JVM 是否归还内存给操作系统这个问题,我们也并不关心。而且基于上面那个测试结果,不同 JAVA 版本,不同垃圾回收器版本区别这么大,更是没必要去深究了。

综上,JVM 虽然可以释放空闲内存给操作系统,但是不一定会释放,在不同 JAVA 版本,不同垃圾回收器版本下表现不同,知道有这个机制就行。

参考

  • docs.oracle.com/javase/10/g…
  • stackoverflow.com/questions/3…
  • Does GC Release Back Memory to OS?
  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》 - 周志明 著

原创不易,禁止未授权的转载。如果我的文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

说说HashMap的实现原理

发表于 2021-05-23

本文关于HashMap的底层实现若无特殊说明都是基于JDK1.8。

HashMap的底层数据结构是数组+链表(红黑树),它是基于hash算法实现的,通过put(key, value) 和 get(key) 方法存储和获取对象。

我们一般是这么使用HashMap的

1
2
java复制代码Map<String, Object> map = new HashMap<>();
map.put("a", "first");

当调用HashMap的无参构造方法时,HashMap的底层数组是没有初始化的。

1
2
3
4
5
6
7
java复制代码/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

无参构造方法中只是对加载因子loadFactor初始化为默认值0.75,并没有对Node<K,V>[] table数组初始化。
(可以思考下HashMap中的加载因子为什么是0.75?)

put(key, value)方法存储元素

当我们第一次调用put(key, value) 方法时,key-value在HashMap内部的数组和链表中是如何存储的呢?
put方法内部首先根据key计算hash值,hash函数是如何计算的呢?是直接返回key的hashCode吗,当然不是啦!
HashMap中的hash算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

key的hash值已经知道了,那么计算 hash & (table.length - 1) 结果就是key的hash值在数组table中的位置bucket。
等等,数组table还没有初始化呢,那么在计算bucket之前应该先将table[]数组进行初始化,
既然要初始化一个数组,那么数组的长度应该是多少呢?

1
2
3
4
java复制代码/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认的初始容量16,数组创建方式如下

1
2
3
java复制代码newCap = DEFAULT_INITIAL_CAPACITY;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

Node[] 数组就这么被创建好了,是不是很简单(具体看源码中的resize()方法)。

数组创建好了,key在数组中对应的位置bucket也找到了,那么现在就该将key-value放入数组中了,该如何放入呢?
数组的类型为Node,那么需要将key-value封装成数组的元素类型Node。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 省略其他部分代码
}

Node类中包含4个属性,K key、V value、int hash、Node<K,V> next。
key、value就是我们要放入HashMap中的数据,hash就是我们上面计算出来的key的hash值,这个Node类型的next是干嘛的呢?
还记得HashMap底层的数据结构吗?数组 + 链表,next这个地方就是链表的实现。next指向与key的hash值相同的新Node。

根据key在数组中对应的位置bucket,获取bucket位置上的元素,如果该位置上没有元素,则直接将key-value封装成的Node放入数组中

1
2
java复制代码tab = table
tab[i] = newNode(hash, key, value, null);

如果该位置上有元素,则比较key的值是否相等,有两种情况:

1、如果key的值相等,则要更新key对应的vaule,将新的value覆盖旧的value;

2、如果key的值不相等,则说明发生了hash冲突。也就是说不同的key计算出的hash值相等,说明它们在table[]中在同一个位置,
这个时候就需要使用链表了,遍历链表,比较key值是否相等,直到链表的最后一个节点,若未找到则将新的元素插入到链表的尾部(尾插法)。

JDK1.8中put方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
java复制代码/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 初始化table
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 目标位置上没有元素,直接将key-value封装成的Node放入数组
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 目标位置上有元素,则比较key值是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// key值相等
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// key值不相等
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 遍历链表,找到链表的最后一个节点,将新的元素插入到链表的尾部(尾插法)
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 判断是否需要将链表转成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 链表继续遍历
p = e;
}
}
if (e != null) { // existing mapping for key
// 目标位置上有key值相等的元素,将新的value覆盖旧的value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
// 数组扩容
resize();
afterNodeInsertion(evict);
return null;
}

get(key) 方法获取对象

当获取对象时,首先跟put方法存储元素一样,也是先调用hash函数计算key的hash值,
然后计算hash & (table.length - 1) 的结果就是key的hash值在数组table中的位置bucket,
根据bucket获取该位置上的元素,判断key值是否相等,如果相等直接返回对象的value值。
如果不相等,则遍历链表,比较key值,直到找到key值相等的节点,如果没有找到则返回null。

JDK1.8中get方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 找到hash值在数组位置上的元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
// 比较key值是否相等
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 遍历链表
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

本文主要分析了HashMap存储对象和获取对象的过程,重点介绍了数组和链表在HashMap底层的使用。

更多精彩内容请关注公众号 geekymv,喜欢请分享给更多的朋友哦」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

接口异步调用,接口耗时减少的可不是一点点

发表于 2021-05-23

随着业务发展,底层数据量越来越大,业务逻辑也日趋复杂化,某些接口耗时也越来越长,这时候接口就需要进行性能优化了,当然性能优化主要跟业务相关涉及改造点可能各不相同,这里就来介绍异步调用多个接口减少响应时间

适用条件

  • 调用多个独立的接口,接口间无相互依赖关系
  • 非耗时最大的接口占总耗时比重较大

优化前调用方式

优化前的代码按照顺序调用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DemoTest {

public static void main(String[] args) throws Exception {
long beginTime = System.currentTimeMillis();
int processA = new InterfaceA().process();
int processB = new InterfaceB().process();
int result = processA + processB;
log.info("执行结果:{} 耗时:{}", result, System.currentTimeMillis() - beginTime);
}

@Slf4j
public final static class InterfaceA {
Integer result = 1;

public int process() {
long beginTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (Exception e) {
log.error("InterfaceA.process Exception");
}
log.info("执行接口InterfaceA.process 耗时:{}ms", System.currentTimeMillis() - beginTime);
return result;
}
}

@Slf4j
public final static class InterfaceB {
Integer result = 1;

public int process() {
long beginTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (Exception e) {
log.error("InterfaceB.process Exception");
}
log.info("执行接口InterfaceB.process 耗时:{}ms", System.currentTimeMillis() - beginTime);
return result;
}
}
}

执行结果:

1
2
3
ini复制代码21:40:17.603 [main] INFO DemoTest$InterfaceA - 执行接口InterfaceA.process 耗时:2002ms
21:40:19.612 [main] INFO DemoTest$InterfaceB - 执行接口InterfaceB.process 耗时:2001ms
21:40:19.613 [main] INFO DemoTest - 执行结果:2 耗时:4018

优化后调用方式

优化后的代码按照异步调用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码import cn.hutool.core.thread.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
public class DemoTest {
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(
5,
5,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1000),
ThreadFactoryBuilder.create().setNamePrefix("线程名称-").build()
);

public static void main(String[] args) throws Exception {
long beginTime = System.currentTimeMillis();

List<Future<Integer>> futures = new ArrayList<>(2);
List<Integer> results = new ArrayList<>(2);
futures.add(pool.submit(() -> new InterfaceA().process()));
futures.add(pool.submit(() -> new InterfaceB().process()));
for (Future<Integer> item : futures) {
results.add(item.get());
}

int result = results.get(0) + results.get(1);
log.info("执行结果:{} 耗时:{}", result, System.currentTimeMillis() - beginTime);
}

@Slf4j
public final static class InterfaceA {
Integer result = 1;

public int process() {
long beginTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (Exception e) {
log.error("InterfaceA.process Exception");
}
log.info("执行接口InterfaceA.process 耗时:{}ms", System.currentTimeMillis() - beginTime);
return result;
}
}

@Slf4j
public final static class InterfaceB {
Integer result = 1;

public int process() {
long beginTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (Exception e) {
log.error("InterfaceB.process Exception");
}
log.info("执行接口InterfaceB.process 耗时:{}ms", System.currentTimeMillis() - beginTime);
return result;
}
}
}

执行结果:

1
2
3
ini复制代码22:03:43.180 [线程名称-1] INFO DemoTest$InterfaceB - 执行接口InterfaceB.process 耗时:2004ms
22:03:43.180 [线程名称-0] INFO DemoTest$InterfaceA - 执行接口InterfaceA.process 耗时:2004ms
22:03:43.190 [main] INFO DemoTest - 执行结果:2 耗时:2020

此方式还可以结合CompletionService可实现异步任务和执行结果分离,大家可以自行搜索实践

强大的CompletableFuture JDK1.8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
java复制代码import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

@Slf4j
public class DemoTest {

public static void main(String[] args) throws Exception {
long beginTime = System.currentTimeMillis();

CompletableFuture<Integer> interfaceFuturesA = CompletableFuture.supplyAsync(() -> new InterfaceA().process());
CompletableFuture<Integer> interfaceFuturesB = CompletableFuture.supplyAsync(() -> new InterfaceB().process());
CompletableFuture<List<Integer>> future = CompletableFuture
.allOf(interfaceFuturesA, interfaceFuturesB)
.thenApply((none) -> {
List<Integer> dataList = new ArrayList<>(2);
try {
dataList.add(interfaceFuturesA.get());
dataList.add(interfaceFuturesB.get());
} catch (Exception e) {
log.error("执行异常");
}
return dataList;
}).exceptionally(e -> Lists.newArrayList());

int result = future.get().get(0) + future.get().get(1);
log.info("执行结果:{} 耗时:{}", result, System.currentTimeMillis() - beginTime);
}

@Slf4j
public final static class InterfaceA {
Integer result = 1;

public int process() {
long beginTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (Exception e) {
log.error("InterfaceA.process Exception");
}
log.info("执行接口InterfaceA.process 耗时:{}ms", System.currentTimeMillis() - beginTime);
return result;
}
}

@Slf4j
public final static class InterfaceB {
Integer result = 1;

public int process() {
long beginTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (Exception e) {
log.error("InterfaceB.process Exception");
}
log.info("执行接口InterfaceB.process 耗时:{}ms", System.currentTimeMillis() - beginTime);
return result;
}
}
}

执行结果:

1
2
3
ini复制代码22:31:44.822 [ForkJoinPool.commonPool-worker-5] INFO DemoTest$InterfaceB - 执行接口InterfaceB.process 耗时:2005ms
22:31:44.822 [ForkJoinPool.commonPool-worker-3] INFO DemoTest$InterfaceA - 执行接口InterfaceA.process 耗时:2002ms
22:31:44.831 [main] INFO DemoTest - 执行结果:2 耗时:2027

优化时注意点

  • 使用线程池防止内存溢出风险
  • 执行结果容器可自行根据需要设置
  • 接口粒度可根据实际业务情况组合和拆分

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 MyBatis 自定义插件的使用及 PageHel

发表于 2021-05-23

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

文章目的 :

  • 了解 MyBatis 插件的用法
  • 了解 主要的源码逻辑
  • 了解 PageHelper 的相关源码

这是一篇概括性文档 , 用于后续快速使用相关功能 , 整体难度较低.

二 . 流程

2.1 基础用法

基础拦截器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码@Intercepts(
{@org.apache.ibatis.plugin.Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class DefaultInterceptor implements Interceptor {

private Logger logger = LoggerFactory.getLogger(this.getClass());


@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.info("------> this is in intercept <-------");
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
logger.info("------> this is in plugin <-------");
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
logger.info("------> this is in setProperties <-------");
}
}

基础配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Configuration
public class PluginsConfig {

@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

@PostConstruct
public void addPageInterceptor() {
DefaultInterceptor interceptor = new DefaultInterceptor();
// 此处往 SqlSessionFactory 中添加
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}

这里可以看到 , 拦截到的参数如下 :

mybatis_plugin001.jpg

2.2 功能详解

整个拦截过程有几个主要的组成部分 :

Interceptor 拦截器接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
M- Interceptor#intercept(Invocation invocation) : 拦截方法
M- plugin : 调用 Plugin#wrap(Object target, Interceptor interceptor) 方法,执行代理对象的创建
M- setProperties : 从 properties 获取一些需要的属性值

// 这里可以看到 , 其中强制实现的方法只有 intercept


public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

default Object plugin(Object target) {
return Plugin.wrap(target, this);
}

default void setProperties(Properties properties) {
}

}

InterceptorChain 拦截器链

InterceptorChain 是一个拦截器链 , 用于执行相关的Interceptor 操作 , 其中有一个拦截器集合

1
2
3
4
5
6
7
8
java复制代码// 主要的大纲如此所示
F- List<Interceptor> ArrayList
M- addInterceptor : 添加拦截器
- 在 Configuration 的 #pluginElement(XNode parent) 方法中被调用
- 创建 Interceptor 对象,并调用 Interceptor#setProperties(properties) 方法
- 调用 Configuration#addInterceptor(interceptorInstance) 方法
- 添加到 Configuration.interceptorChain 中
M- pluginAll : 应用所有拦截器到指定目标对象

注解 @Intercepts 和 @Signature

这是过程中主要涉及的2个注解 :

  • @Intercepts : 定义拦截器 , Intercepts 中可以包含一个 Signature 数组
  • @Signature : 定义类型
    • type : 拦截器处理的类
    • method : 拦截的方法
    • args : 方法参数 (重载的原因)

2.3 源码跟踪

Step 1 : 资源的加载

资源的加载主要是对对应方法的代理逻辑 , 从前文我们可以看到 , 一个 plugin 操作 , 主要包含2个步骤 :

1
2
3
4
5
6
7
8
9
10
11
java复制代码
// 步骤一 : 声明拦截器对象
@Intercepts(
{@org.apache.ibatis.plugin.Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})


// 步骤二 : sqlSessionFactory 中添加拦截器对象
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);

来跟进一下相关的操作 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码C01- Configuration
F01_01- InterceptorChain interceptorChain
M01_01- addInterceptor
- interceptorChain.addInterceptor(interceptor)
?- 可以看到 , 这里往 InterceptorChain 添加了 interceptor

// M01_01 源代码
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}



public class InterceptorChain {

private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

// 可以看到 , 这里将拦截器加到了拦截器链中
// PS : 但是此处未完全完成 , 仅仅只是添加 , 具体的操作会在上面 pluginAll 中完成
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}

public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}

}

Step 2 : plugin 的构建

Step 1 中已经完成了相关 interceptors 的添加 , 这个环节需要通过 Interceptor 构建对应的 Plugin

先来看一下调用链 :

  • C- SqlSessionTemplate # selectList : 发起 Select 请求
  • C- SqlSessionInterceptor # invoke : 构建一个 Session 代理
  • C- SqlSessionUtils # getSqlSession : 获取 Session 对象
  • C- DefaultSqlSessionFactory # openSessionFromDataSource
  • C- Configuration # newExecutor

整体来说 ,从 getSqlSession 开始关注即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}

// 核心节点 , 开启 Session
session = sessionFactory.openSession(executorType);

registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;
}

DefaultSqlSessionFactory 构建一个 Session , 同时调用 Plugin 生成逻辑

此处构建 Session 的同时 , 最终调用 Plugin warp 构建 Plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码C- DefaultSqlSessionFactory
// 打开的起点 : 打开 session 的时候 ,
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

// 此处构建 Executor 并且放入 Session
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}


// 调用拦截链
C- Configuration
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 此处调用拦截链 , 构建 Executor 对象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

// 拦截链处理
C- InterceptorChain
public Object pluginAll(Object target) {
// 此处是在构建拦截器链 , 返回的是最后的拦截器处理类
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}


C- Plugin
// 调用 Plugin wrap , 生成了一个新的 plugin , 该 plugin 包含对应的 interceptor 的 拦截方法
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 核心 , 对方法做了代理 , 同时为代理类传入了 Plugin
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

Step 3 : invoke 执行

拦截器的实现是基于 Proxy 代理实现的 , 在上文看到了代理的生成 , 这里看一下代理的调用 :

以 Query 为例 :

当调用 Executor 中 Query 方法时 , 会默认调用代理类 , 上文说了 Execute 的构建 , 那么他在整个逻辑中是作为什么角色的?

Executor 作用 : Executor 是 Mybatis 中的顶层接口 , 定义了主要的数据库操作方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码public interface Executor {

ResultHandler NO_RESULT_HANDLER = null;

int update(MappedStatement ms, Object parameter) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

List<BatchResult> flushStatements() throws SQLException;

void commit(boolean required) throws SQLException;

void rollback(boolean required) throws SQLException;

CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

boolean isCached(MappedStatement ms, CacheKey key);

void clearLocalCache();

void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

Transaction getTransaction();

void close(boolean forceRollback);

boolean isClosed();

void setExecutorWrapper(Executor executor);

}

execute 的调用 :

前面构建 Session 的时候 , 已经为其定义了Plugin , 以下为 Plugin 的主要流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码// 1 . 选择合适的 plugin
C- DefaultSqlSession
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

// 2 . 中间通过 proxy 代理


// 3 . plugin 中调用拦截器
C05- Plugin
F05_01- private final Interceptor interceptor;
M05_01- invoke(Object proxy, Method method, Object[] args)
- 获取可以拦截的 method
- 判断当前的 method 是否在可拦截的


// M05_01 : invoke@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 此处调用了拦截器
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}

// 4 . 调用 interceptor
public Object intercept(Invocation invocation) throws Throwable {
// 此处调用代理方法的实际逻辑
return invocation.proceed();
}

三 . 扩展 PageHelper

以下为 PageHelper 中如何使用相关的拦截器方法的 :

PageHelper 中主要有 2个拦截器 :

  • QueryInterceptor : 查询操作插件
  • PageInterceptor : 分页操作插件

主要看一下 PageInterceptor 拦截器 :

拦截器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
java复制代码@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
private volatile Dialect dialect;
private String countSuffix = "_COUNT";
protected Cache<String, MappedStatement> msCountMap = null;
private String default_dialect_class = "com.github.pagehelper.PageHelper";

@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();

// 获取请求的四个参数
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
// 获取绑定 SQL : select id, type_code, type_class, type_policy, type_name, supplier_id, supplier_name from sync_type
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();

List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//处理查询总数,返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}

/**
* Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化
* <p>
* 因此这里会出现 null 的情况 fixed #26
*/
private void checkDialectExists() {
if (dialect == null) {
synchronized (default_dialect_class) {
if (dialect == null) {
setProperties(new Properties());
}
}
}
}

private Long count(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
String countMsId = ms.getId() + countSuffix;
Long count;
//先判断是否存在手写的 count 查询
MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
if (countMs != null) {
count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自动创建
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
return count;
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
//缓存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
dialect.setProperties(properties);

String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
}

}

补充 :ExecutorUtil.executeAutoCount 相关逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* 执行自动生成的 count 查询
**/
public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs,
Object parameter, BoundSql boundSql,
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
//创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
// 调用方言获取 count sql
// SELECT count(0) FROM sync_type
String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
//countKey.update(countSql);
BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
//当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行 count 查询
Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long) ((List) countResultList).get(0);
return count;
}

补充 : pageQuery 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql, CacheKey cacheKey) throws SQLException {
//判断是否需要进行分页查询
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分页的缓存 key
CacheKey pageKey = cacheKey;
//处理参数对象 , 此处生成分页的参数
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//调用方言获取分页 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行分页查询
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
}

因为源码的注释程度很高 , 所以基本上不需要做额外的标注了 , 整体的流程就是 :

  • Step 1 : 拦截器 intercept方法定义整体逻辑
  • Step 2 : count 方法决定他是否分页
  • Step 3 :pageQuery 调用方言进行事情SQL的拼接

整体中有几点值得关注 :

  • 只生成分页的 row 和 pageKey 等 , 在最终通过方言组合 , 以适应多种数据库结构
  • 核心还是调用 executor.query 原生方法

简述一下 PageHepler 的绑定流程

核心处理类为 AbstractHelperDialect , 先从创建开始 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码PageHelper.startPage(page, size);
List<SyncType> allOrderPresentList = syncTypeDAO.findAll();

// Step 1 : startPage 核心代码
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}

此处最核心的就是 setLocalPage , 会使用 ThreadLocal 保持线程参数
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

// Step 2 : 拦截器中的参数获取
// 可以看到第一句就是从 getLocalPage - ThreadLocal 中获取
C- AbstractHelperDialect # processParameterObject

public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
//处理参数
Page page = getLocalPage();
//如果只是 order by 就不必处理参数
if (page.isOrderByOnly()) {
return parameterObject;
}
Map<String, Object> paramMap = null;
if (parameterObject == null) {
paramMap = new HashMap<String, Object>();
} else if (parameterObject instanceof Map) {
//解决不可变Map的情况
paramMap = new HashMap<String, Object>();
paramMap.putAll((Map) parameterObject);
} else {
paramMap = new HashMap<String, Object>();
//动态sql时的判断条件不会出现在ParameterMapping中,但是必须有,所以这里需要收集所有的getter属性
//TypeHandlerRegistry可以直接处理的会作为一个直接使用的对象进行处理
boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
//需要针对注解形式的MyProviderSqlSource保存原值
if (!hasTypeHandler) {
for (String name : metaObject.getGetterNames()) {
paramMap.put(name, metaObject.getValue(name));
}
}
//下面这段方法,主要解决一个常见类型的参数时的问题
if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
String name = parameterMapping.getProperty();
if (!name.equals(PAGEPARAMETER_FIRST)
&& !name.equals(PAGEPARAMETER_SECOND)
&& paramMap.get(name) == null) {
if (hasTypeHandler
|| parameterMapping.getJavaType().equals(parameterObject.getClass())) {
paramMap.put(name, parameterObject);
break;
}
}
}
}
}
return processPageParameter(ms, paramMap, page, boundSql, pageKey);
}

剩下逻辑也比较清晰 , 也不涉及太多此处逻辑了 , 有需求再看更多

总结

这应该是写的最简单的一篇源码分析了 , 除了本身结构不复杂以外 , 相关源码的注释也很清晰 , 基本上没有什么分析的需求.

拦截器的使用 :

  • 准备拦截器类
  • sqlSessionFactory.getConfiguration().addInterceptor(interceptor) 添加拦截器

PageHelper 核心 :

  • parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey) : 获取分页的参数
  • dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey) : 参数解析为 SQL
  • ThreadLocal 保存参数

后续我们再关注一下Mybatis 其他要点和 PageHelper 的整体涉及优势

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Net之Docker部署详细流程 开篇语 创建项目 生成镜

发表于 2021-05-23

开篇语

自己从头开始走一遍docker部署.net的流程,作为一种学习总结,以及后续会写一些在该基础之上的文章。

本次示例环境:vs2019、net5、docker、postman

创建项目

本次事例代码是用过vs2019创建的ASP.NET Core Web API项目

image.png

目标框架是.Net5,无需身份验证,不配置HTTPS(根据个人需求勾选),启动Docker(我习惯于后期添加),启用OpenAPI支持(添加swagger文档)

image.png

默认配置

创建完成后,我们查看项目目录为下

image.png

我们直接F5启动项目,发现直接跳转一个API文档页面

image.png

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

修改配置

我基于个人习惯,我修改launchSettings.json文件,删除IIS配置,删除后如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Net5ByDocker": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": "true",
"applicationUrl": "http://localhost:5000"
}
}
}

删除默认控制器,添加新的控制器UserController,在里面添加默认一些方法操作,如下

基于个人习惯的操作,也可以不删除默认控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
csharp复制代码    [Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
public static List<string> userInfo = new();

[HttpGet]
public IEnumerable<string> Get()
{
return userInfo;
}

[HttpPost]
public List<string> Post([FromBody] string value)
{
if (!string.IsNullOrWhiteSpace(value))
userInfo.Add(value);
return userInfo.ToList();
}

[HttpDelete("{id}")]
public List<string> Delete(string id)
{
if (!string.IsNullOrWhiteSpace(id))
userInfo.Remove(id);
return userInfo.ToList();
}
}

其他配置保持默认,启动项目

image.png

生成镜像

添加dockerfile

选中项目右键添加docker支持,本次部署在windows平台

image.png

拉取基础镜像和sdk,还原nuget包,重新生成,发布

此时项目的目录结构为

image.png

运行命令

在文件资源管理器打开文件

image.png

在上层目录下运行cmd输入命令

1
erlang复制代码docker build -f .\Net5ByDocker\Dockerfile -t net5sample .

在不同的目录下命令有些许差异,这点非常感谢我的朋友王老师

image.png

注意:可能部分朋友在这一步会拉取官方镜像比较慢,可以配置docker加速器使用

通过docker客户端查看我们已经生成的镜像

image.png

生成容器

本文通过Terminal软件执行命令

1
css复制代码docker run --name net5sampleone -d -p 8060:80 net5sample

命令简述:

-d 后台运行

–name 容器名称

-p 端口映射

截至到这,我们已经把刚才的项目生成了容器,下面我们可以直接通过容器方法上面的项目

验证项目

通过浏览器访问地址:localhost:8060/swagger

image.png

懵逼!!!这个时候不是应该出来swagger文档的界面吗?难道我们部署的方式有问题?

让我们访问下项目的接口

image.png

说明我们的项目运行是正常的,仔细查看swagger配置后发现,因为为了安全默认不允许发布后出来swagger文档

image.png

如果是测试环境或者特殊情况可以通过调整swagger配置位置来显示文档

通过Postman访问

添加用户

image.png

查询用户

image.png

删除用户

image.png

再次运行查询接口数据已经为空了。

微信公众号【鹏祥】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…662663664…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%