IO 流

一、什么是 IO 流

  • I : Input
  • O: Output

二、流的分类

  • 按照流的方向
    • 输入 (Input) 也就是 读 (Read)
    • 输出 (Output) 也就是 写 (Write)
  • 按照读取数据方式
    • 字节:一次读取 1 个字节 byte,等同于一次读取 8 个二进制位,这种流什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件等…
    • 字符:一次读取 1 个字符,这种流方便读取 普通文本文件 而存在的,这种流不能读取:图片、声音、视频等文件。只能读取 纯文本文件,连 word 文件都无法读取。

三、IO 流抽象类

  • 字节流

    1. java.io.InputStream 字节输入流
    2. java.io.OutputStream 字节输出流
  • 字符流

    1. java.io.Reader 字符输入流

    2. java.io.Writer 字符输出流

注意:

  1. 所有的流都实现了:java.io.Closeable接口,都是可关闭的,都有 close() 方法。用完一定要关闭
  2. 所有的输出流都实现了java.io.Flushable接口,都是可刷新的,都有 flush() 方法。输出流在最终输出之后,一定要记得 flush() 刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)刷新的作用就是清空管道
  3. 在 java 中只要“类名”以 Stream 结尾的都是字节流。以“ Reader/Writer ”结尾的都是字符流

四、Java 要掌握的流 (16 个)

  1. 文件专属:

    • java.io.FileInputStream(掌握)

    • java.io.FileOutputStream(掌握)

    • java.io.FileReader

    • java.io.FileWriter

  2. 转换流:(将字节流转换成字符流)

    • java.io.InputStreamReader

    • java.io.OutputStreamWriter

  3. 缓冲流专属:

    • java.io.BufferedReader

    • java.io.BufferedWriter

    • java.io.BufferedInputStream

    • java.io.BufferedOutputStream

  4. 数据流专属:

    • java.io.DataInputStream

    • java.io.DataOutputStream

  5. 标准输出流:

    • java.io.PrintWriter

    • java.io.PrintStream(掌握)

  6. 对象专属流:

    • java.io.ObjectInputStream(掌握)

    • java.io.ObjectOutputStream(掌握)

  7. File 文件类

    • java.io.File

补充:
Windows 各个文件的分隔符为:”\“,Linux 各个文件的分隔符为:”/“

五、类的方法

1. FileInputStream

文件字节输入流,万能的,任何类型的文件都可以采用这个流来读

构造方法

构造方法名 备注
FileInputStream(String name) name 为文件路径
FileInputStream(File file)

方法

方法名 作用
int read() 读取一个字节,返回值为该字节 ASCII 码;读到文件末尾返回-1
int read(byte[] b) 读 b 数组长度的字节到 b 数组中,返回值为读到的字节个数;读到文件末尾返回-1
int read(byte[] b, int off, int len) 从 b 数组 off 位置读 len 长度的字节到 b 数组中,返回值为读到的字节个数;读到文件末尾返回-1
int available() 返回文件有效的字节数
long skip(long n) 跳过 n 个字节
void close() 关闭文件输入流

2. FileOutputStream

构造方法

构造方法名 备注
FileOutputStream(String name) name 为文件路径
FileOutputStream(String name, boolean append) name 为文件路径,append 为 true 表示在文件末尾追加;为 false 表示清空文件内容,重新写入
FileOutputStream(File file)
FileOutputStream(File file, boolean append) append 为 true 表示在文件末尾追加;为 false 表示清空文件内容,重新写入

方法

方法名 作用
void write(int b) 将指定字节写入文件中
void write(byte[] b) 将 b.length 个字节写入文件中
void write(byte[] b, int off, int len) 将 b 数组 off 位置开始,len 长度的字节写入文件中
void flush() 刷新此输出流并强制写出所有缓冲的输出字节
void close() 关闭文件输出流

3. FileReader

FileReader 文件字符输入流,只能读取普通文本。读取文本内容时,比较方便,快捷。

构造方法

构造方法名 备注
FileReader(String fileName) name 为文件路径
FileReader(File file)

方法

方法名 作用
int read() 读取一个字符,返回值为该字符 ASCII 码;读到文件末尾返回-1
int read(char[] c) 读 c 数组长度的字节到 c 数组中,返回值为读到的字符个数;读到文件末尾返回-1
int read(char[] c, int off, int len) 从 c 素组 off 位置读 len 长度的字符到 c 数组中,返回值为读到的字符个数;读到文件末尾返回-1
long skip(long n) 跳过 n 个字符
void close() 关闭文件输入流

4. FileWriter

FileWriter 文件字符输出流。写。只能输出普通文本

构造方法

构造方法名 备注
FileWriter(String fileName) name 为文件路径
FileWriter(String fileName, boolean append) name 为文件路径,append 为 true 表示在文件末尾追加;为 false 表示清空文件内容,重新写入
FileWriter(File file)
FileWriter(File file, boolean append) append 为 true 表示在文件末尾追加;为 false 表示清空文件内容,重新写入

方法

方法名 作用
void write(int c) 将指定字符写入文件中
void write(char[] c) 将 c.length 个字符写入文件中
void write(char[] c, int off, int len) 将 c 数组 off 位置开始,len 长度的字符写入文件中
void write(String str) 将字符串写入文件中
void write(String str, int off, int len) 从字符串 off 位置开始截取 len 长度的字符串写入文件
void flush() 刷新此输出流并强制写出所有缓冲的输出字符
void close() 关闭文件输出流

5. PrintStream

标准的字节输出流。默认输出到控制台

构造方法

构造方法名 备注
PrintStream(File file)
PrintStream(OutputStream out)
PrintStream(String fileName) fileName 文件地址

方法

方法 作用
println(参数类型不定 x) 输出 x 带换行
print(参数类型不定 x) 输出 x 不带换行
void flush() 刷新此输出流并强制写出所有缓冲的输出字符
void close() 关闭流

改变流的输出方法

System.setOut(PrintStream 对象)

注意:

  1. 标准输出流不需要手动 close() 关闭
  2. 可以改变标准输出流的输出方向

序列化

参与序列化和反序列化的对象,必须实现**Serializable** 接口。

Serializable 接口起什么作用呢?

  • 起到 标识 的作用,标志的作用,java 虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇。
  • Serializable 这个标志接口是给 java 虚拟机参考的,java 虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号。

序列化版本号有什么用

区分两个类是否相同

java 语言中是采用什么机制来区分类的

  1. 首先通过 类名 进行比对,如果类名不一样,肯定不是同一个类。
  2. 如果类名一样,再怎么进行类的区别?靠 序列化版本号 进行区分。

这种自动生成序列化版本号有什么缺陷

Java 虚拟机看到 Serializable 接口之后,会自动生成一个序列化版本号。

这种自动生成的序列化版本号缺点是:一旦代码确定之后,不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候 java 虚拟机会认为这是一个全新的类。(这样就不好了!)

怎么使某个属性不序列化

使用 transient 关键字

transient 关键字表示游离的,不参与序列化

结论

凡是一个类实现了 Serializable 接口,建议给该类提供一个固定不变的序列化版本号
这样,以后这个类即使代码修改了,但是版本号不变,java 虚拟机会认为是同一个类。

NIO

一、NIO 简介

NIO 中的 N 可以理解为 Non-blocking,不单纯是 New,是解决高并发、I/O 高性能的有效方式。

Java NIO 是 Java1.4 之后推出来的一套 IO 接口,NIO 提供了一种完全不同的操作方式, NIO 支持面向缓冲区的、基于通道的 IO 操作。

新增了许多用于处理输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的功能。

二、NIO 和 BIO

BIO

BIO 全称是 Blocking IO,同步阻塞式 IO,是 JDK1.4 之前的传统 IO 模型。

Java BIO:服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示:

img

虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO 可以一定程度解决这个问题。

NIO

Java NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求 (连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。

img

一个线程中就可以调用多路复用接口(java 中是 select)阻塞同时监听来自多个客户端的 IO 请求,一旦有收到 IO 请求就调用对应函数处理,NIO 擅长 1 个线程管理多条连接,节约系统资源。

三、NIO 的核心实现

NIO 包含 3 个核心的组件:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

img

Channel(通道)

Channel 是 NIO 的核心概念,它表示一个打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。

img

通道的主要实现类:

FileChannel 类

本地文件 IO 通道,用于读取、写入、映射和操作文件的通道,使用文件通道操作文件的一半流程为:

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
public static void main(String[] args) throws IOException {
String file = new String("D:\\a.txt");
//获取文件通道 获取时需要指定文件路径和文件打开方式
FileChannel fileChannel = FileChannel.open(Paths.get(file), StandardOpenOption.READ);

//创建字节缓冲区 分配字节缓存为 10 个字节
ByteBuffer buf = ByteBuffer.allocate(10);

StringBuffer text = new StringBuffer();

while (fileChannel.read(buf) != -1){ //读取通道中的数据,并写入到 buf 中
buf.flip(); //缓存区切换到读模式
while (buf.position() < buf.limit()){ //读取 buf 中的数据
text.append((char)buf.get());
}
buf.clear(); // 清空 buffer,缓存区切换到写模式
}

text.append("\n");

FileChannel channel = FileChannel.open(Paths.get(file), StandardOpenOption.APPEND);

//写入数据
for(int i = 0; i < text.length(); i++){
buf.put((byte) text.charAt(i));
if(buf.position() == buf.limit() || i == text.length() - 1){
buf.flip(); // 将缓存区由写模式置为读模式
channel.write(buf); //将缓存区的数据写到通道
buf.clear();
}
}

//将数据刷出到物理磁盘,FileChannel 的 force(boolean metaData) 方法可以确保对文件的操作能够更新到磁盘
channel.force(false);

//关闭通道
fileChannel.close();
channel.close();
}

SocketChannel 类

网络套接字 IO 通道,TCP 协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。

TCP 客户端使用 SocketChannel 与服务端进行交互的流程为:

  1. 打开通道,连接到服务端
1
2
SocketChannel channel = SocketChannel.open(); // 打开通道,此时还没有打开 TCP 连接
channel.connect(new InetSocketAddress("localhost", 9090)); // 连接到服务端
  1. 分配缓冲区
1
ByteBuffer buf = ByteBuffer.allocate(10); // 分配一个 10 字节的缓冲区,不实用,容量太小
  1. 配置是否为阻塞方式。(默认为阻塞方式)
1
channel.configureBlocking(false); // 配置通道为非阻塞模式
  1. 与服务端进行数据交互
  2. 关闭连接
1
channel.close();	//关闭通道

ServerSocketChannel 类

网络通信 IO 操作,TCP 协议,针对面向流的监听套接字的可选择通道(一般用于服务端),流程如下:

  1. 打开一个 ServerSocketChannel 通道, 绑定端口。
1
ServerSocketChannel server = ServerSocketChannel.open(); // 打开通道
  1. 绑定端口
1
server.bind(new InetSocketAddress(9090)); // 绑定端口
  1. 阻塞等待连接到来,有新连接时会创建一个 SocketChannel 通道,服务端可以通过这个通道与连接过来的客户端进行通信。等待连接到来的代码一般放在一个循环结构中。
1
SocketChannel client = server.accept(); // 阻塞,直到有连接过来
  1. 通过 SocketChannel 与客户端进行数据交互
  2. 关闭 SocketChannel
1
client.close();

Buffer(缓冲区)

缓冲区 Buffer 是 Java NIO 中一个核心概念,在 NIO 库中,所有数据都是用缓冲区处理的。

在读取数据时,它是直接读到缓冲区中的,在写入数据时,它也是写入到缓冲区中的,任何时候访问 NIO 中的数据,都是将它放到缓冲区中。

而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

img

Buffer 数据类型

img

从类图中可以看到,7 种数据类型对应着 7 种子类,这些名字是 Heap 开头子类,数据是存放在 JVM 堆中的。

MappedByteBuffer

而 MappedByteBuffer 则是存放在堆外的直接内存中,可以映射到文件。

通过 java.nio 包和 MappedByteBuffer 允许 Java 程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得 IO 操作非常快。

Mmap 内存映射和普通标准 IO 操作的本质区别在于它并不需要将文件中的数据先拷贝至 OS 的内核 IO 缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。

img

只有当缺页中断发生时,直接将文件从磁盘拷贝至用户态的进程空间内,只进行了一次数据拷贝,对于容量较大的文件来说(文件大小一般需要限制在 1.5~2G 以下),采用 Mmap 的方式其读/写的效率和性能都非常高,大家熟知的RocketMQ 就使用了该技术。

Buffer 数据流程

应用程序可以通过与 I/O 设备建立通道来实现对 I/O 设备的读写操作,操作的数据通过缓冲区 Buffer 来进行交互。

img

从 I/O 设备读取数据时:

  1. 应用程序调用通道 Channel 的 read() 方法
  2. 通道往缓冲区 Buffer 中填入 I/O 设备中的数据,填充完成之后返回
  3. 应用程序从缓冲区 Buffer 中获取数据

往 I/O 设置写数据时:

  1. 应用程序往缓冲区 Buffer 中填入要写到 I/O 设备中的数据

  2. 调用通道 Cannel 的 write() 方法,通道将数据传输至 I/O 设备

缓冲区核心方法

缓冲区存取数据的两个核心方法:

  1. put():存入数据到缓冲区
    • put(byte b):将给定单个字节写入缓冲区的当前位置
    • put(byte[] src):将 src 中的字节写入缓冲区的当前位置
    • put(int index,byte b):将指定字节写入缓冲区的索引位置 (不会移动 position)
  2. get():获取缓冲区的数据
    • get():读取单个字节
    • get(byte[] dst):批量读取多个字节到 dst 中
    • get(int index):读取指定索引位置的字节 (不会移动 position)

Selector(选择器)

Selector 类是 NIO 的核心类,Selector(选择器)选择器提供了选择已经就绪的任务的能力。

Selector 会不断的轮询注册在上面的所有 channel,如果某个 channel 为读写等事件做好准备,那么就处于就绪状态,通过 Selector 可以不断轮询发现出就绪的 channel,进行后续的 IO 操作。

img

一个 Selector 能够同时轮询多个 channel,这样,一个单独的线程就可以管理多个 channel,从而管理多个网络连接,这样就不用为每一个连接都创建一个线程,同时也避免了多线程之间上下文切换导致的开销。

选择器使用步骤

  1. 获取选择器

    与通道和缓冲区的获取类似,选择器的获取也是通过静态工厂方法 open() 来得到的。

1
Selector selector = Selector.open(); // 获取一个选择器实例
  1. 获取可选择通道

    能够被选择器监控的通道必须实现了 SelectableChannel 接口,并且需要将通道配置成非阻塞模式,否则后续的注册步骤会抛出 IllegalBlockingModeException。

1
2
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 打开 SocketChannel 并连接到本机 9090 端口
socketChannel.configureBlocking(false); // 配置通道为非阻塞模式
  1. 将通道注册到选择器

    通道在被指定的选择器监控之前,应该先告诉选择器,并且告知监控的事件,即:将通道注册到选择器。

    通道的注册通过 SelectableChannel.register(Selector selector, int ops) 来完成,ops 表示关注的事件,如果需要关注该通道的多个 I/O 事件,可以传入这些事件类型或运算之后的结果。这些事件必须是通道所支持的,否则抛出 IllegalArgumentException。

1
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 将套接字通过到注册到选择器,关注 read 和 write 事件
  1. 轮询 select 就绪事件

    通过调用选择器的 Selector.select() 方法可以获取就绪事件,该方法会将就绪事件放到一个 SelectionKey 集合中,然后返回就绪的事件的个数。这个方法映射多路复用 I/O 模型中的 select 系统调用,它是一个阻塞方法。正常情况下,直到至少有一个就绪事件,或者其它线程调用了当前 Selector 对象的 wakeup() 方法,或者当前线程被中断时返回。

1
2
3
4
while (selector.select() > 0){ // 轮询,且返回时有就绪事件
Set<SelectionKey> keys = selector.selectedKeys(); // 获取就绪事件集合
.......
}
   有 3 种方式可以 select 就绪事件:

1. select() 阻塞方法,有一个就绪事件,或者其它线程调用了 wakeup() 或者当前线程被中断时返回。

2. select(long timeout) 阻塞方法,有一个就绪事件,或者其它线程调用了 wakeup(),或者当前线程被中断,或者阻塞时长达到了 timeout 时返回。不抛出超时异常。

3. selectNode() 不阻塞,如果无就绪事件,则返回 0;如果有就绪事件,则将就绪事件放到一个集合,返回就绪事件的数量。
  1. 处理就绪事件

    每次可以 select 出一批就绪的事件,所以需要对这些事件进行迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
for(SelectionKey key : keys){
if(key.isWritable()){ // 可写事件
if("Bye".equals( (line = scanner.nextLine()) )){
socketChannel.shutdownOutput();
socketChannel.close();
break;
}
buf.put(line.getBytes());
buf.flip();
socketChannel.write(buf);
buf.compact();
}
}

从一个 SelectionKey 对象可以得到:

  1. 就绪事件的对应的通道;
  2. 就绪的事件。通过这些信息,就可以很方便地进行 I/O 操作。

Java 实现文件上传,下载 (理论,实践)

服务端

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
public class Server {
public static void main(String... args) throws IOException {
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
executorService.execute(() -> {
InputStream inputStream = null;
BufferedInputStream bufferedInputStream = null;
OutputStream outputStream = null;
BufferedOutputStream bufferedOutputStream = null;
try {
inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int i = 0;
String name = "";
while ((i = inputStream.read(bytes)) != -1) {
name = new String(bytes, 0, i);
}
String filePath = "";
if ("1".equals(name)) {
filePath = "D:/暂存文件/信封.txt";
}else if ("2".equals(name)) {
filePath = "D:/暂存文件/信封 1.txt";
}
bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
byte[] run = Fuzhu.run(bufferedInputStream);
outputStream = socket.getOutputStream();
bufferedOutputStream = new BufferedOutputStream(outputStream);
bufferedOutputStream.write(run);
bufferedOutputStream.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (bufferedOutputStream != null) {
bufferedOutputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
if (bufferedInputStream != null) {
bufferedInputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
}

客户端

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
public class Client {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
boolean flag = true;
while(flag) {
System.out.println("请选择 上传 或 下载操作");
System.out.println("退出程序 x");
String x = scanner.next();
switch (x) {
case "下载":
new Client().downFile();
break;
case "上传":
new Client().upLoad();
break;
case "x":
flag = false;
break;
default:
System.out.println("请选择可用项");
}
}
System.out.println("程序已退出");
}

public void downFile() {
Scanner scanner = new Scanner(System.in);
System.out.println("请选择你要下载的文件");
System.out.println("1 -> 信封.txt");
System.out.println("2 -> 信封 1.txt");
InetAddress localHost = null;
Socket socket = null;
BufferedOutputStream bufferedOutputStream = null;
OutputStream outputStream = null;
BufferedInputStream bufferedInputStream = null;
try {
localHost = InetAddress.getLocalHost();
socket = new Socket(localHost.getHostAddress(), 8080);
outputStream = socket.getOutputStream();
String next = scanner.next();
outputStream.write(next.getBytes());
socket.shutdownOutput();
bufferedInputStream = new BufferedInputStream(socket.getInputStream());
byte[] run = Fuzhu.run(bufferedInputStream);
String filePath = "D:/下载/" + next + ".txt";
bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(filePath));
bufferedOutputStream.write(run);
bufferedOutputStream.flush();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (bufferedOutputStream != null) {
bufferedOutputStream.close();
}
if (bufferedInputStream != null) {
bufferedInputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

public void upLoad() {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入文件名称");
String name = scanner.next();
System.out.println("请输入信封内容");
String content = scanner.next();
File file = new File("D:/暂存文件" + name + ".txt");
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(content.getBytes());
fileOutputStream.flush();
System.out.println("上传完成");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}
}

传输工具类

1
2
3
4
5
6
7
8
9
10
11
12
public class Fuzhu {
public static byte[] run(InputStream a) throws IOException {
ByteArrayOutputStream x = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int i = 0;
while ((i = a.read(bytes)) != -1) {
x.write(bytes, 0, i);
}
byte[] byteArray = x.toByteArray();
return byteArray;
}
}

多线程

一、多线程三种调用方式

第一种方式:

编写一个类,直接继承**java.long.Thread,重写run方法**。

1
2
3
4
5
6
7
8
9
10
11
12
13
//定义线程类
public class MyThread extends Thread{
public void run(){

}
public static void main(String[] args){
//创建线程对象
MyThread t = new MyThread();
//启动线程
//t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
t.start();
}
}

第二种方式:

编写一个类,实现 java.lang.Runnable 接口,实现**run 方法**。

1
2
3
4
5
6
7
8
public class MyRunnble implements Runnble{
public void run(){
}
public static void main(String[] args){
Thread t = new Thread(new MyRunnble);
t.start();
}
}

采用匿名内部类创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadTest {
public static void main(String[] args) {
// 创建线程对象,采用匿名内部类方式。
Thread t = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("t 线程---> " + i);
}
}
});

// 启动线程
t.start();

for(int i = 0; i < 100; i++){
System.out.println("main 线程---> " + i);
}
}
}

第三种方式:

编写一个类,实现 java.util.concurrent.Callable 接口,实现**call 方法**。

call 方法具有返回值,Java5 使用 Future 接口来代表 call 方法的返回值,并且为 Future 接口提供了一个实现类 FutureTask
。FutureTask 既实现了 Future 接口又实现了 Runnable 接口。所以 FutureTask 可以作为 Thread 构造方法的参数传入来创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("你好");
Thread.sleep(1000);
return 1;
}
}
class Thread{
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Mycallable();
FutureTask<Integer> task = new FutureTask<>(callable);
Thread thread=new Thread(task);
//线程开始执行
thread.start();
}
}

这三种创建线程的区别:

  1. 第二种创建线程的方式相比于第一种创建线程的优点

    • 更适合多线程实行相同任务,可以减少代码量
    • 避免了单继承的局限性
    • 线程和任务分离,提高了程序健壮性
    • 线程池接受 Runnable 类型任务,不接受 Thread 类型线程
  2. 第三种相比于前两种创建线程的区别:

    使用 Callable 方式创建线程时,call 方法具有返回值。Future 封装了 call 方法的返回值,可以通过 FutureTask 的对象调用 Future 接口的一些方法来控制任务。如:V get():调用这个方法可以阻塞主线程直到子线程返回结果。等等。

二、多线程基本方法

方法名 作用
static Thread currentThread() 获取当前线程对象
String getName() 获取线程对象名字
void setName(String name) 修改线程对象名字
static void sleep(long millis) 让当前线程休眠 millis 秒
void interrupt() 终止线程的睡眠
void stop() 强行终止一个线程的执行 (不推荐使用)
建议使用布尔标记来结束进程的执行
int getPriority() 获得线程优先级
void setPriority(int newPriority) 设置线程优先级
static void yield() 让位方法,当前线程暂停,回到就绪状态,让给其它线程。
注意:在回到就绪之后,有可能还会再次抢到。
void join() 将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行直到结束
void setDaemon(boolean on) on 为 true 表示把线程设置为守护线程

setPriority(int newPriority) 参数使用的常量:

常量名 备注
static int MAX_PRIORITY 最高优先级(10)
static int MIN_PRIORITY 最低优先级(1)
static int NORM_PRIORITY 默认优先级(5)

关于 Object 类的 wait()、notify()、notifyAll() 方法

方法名 作用
void wait() 让活动在当前对象的线程无限等待(释放之前占有的锁)
void notify() 唤醒当前对象正在等待的线程(只提示唤醒,不会释放锁)
void notifyAll() 唤醒当前对象全部正在等待的线程(只提示唤醒,不会释放锁)

wait 和 notify 方法不是线程对象的方法,是 java 中任何一个 java 对象都有的方法,因为这两个方法是 Object 类中自带 的。

wait 方法和 notify 方法不是通过线程对象调用

作用:

对应生产消费者模式

什么是生产消费者模式

  • 生产线程负责生产,消费线程负责消费。
  • 生产线程和消费线程要达到均衡。
  • 这是一种特殊的业务需求,在这种特殊的情况下需要使用wait 方法和 notify 方法

三、死锁

什么是死锁

死锁 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。也就是两个线程拥有锁的情况下,又在尝试获取对方的锁,从而造成程序一直阻塞的情况。

死锁代码演示:

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
public class DeadLock {
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
// 1.占有一把锁
synchronized (lockA) {
System.out.println("线程 1 获得锁 A");
// 休眠 1s(让线程 2 有时间先占有锁 B)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取线程 2 的锁 B
synchronized (lockB) {
System.out.println("线程 1 获得锁 B");
}
}
});
t1.start();

Thread t2 = new Thread(() -> {
// 占 B 锁
synchronized (lockB) {
System.out.println("线程 2 获得锁 B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取线程 1 的锁 A
synchronized (lockA) {
System.out.println("线程 2 获得了锁 A");
}
}
});
t2.start();
}
}

死锁产生的原因

形成死锁主要由以下 4 个因素造成的:

  1. 互斥条件:一个资源只能只能被⼀个线程占有,当这个资源被占用之后其他线程就只能等待。
  2. 不可被剥夺条件:当⼀个线程不主动释放资源时,此资源一直被拥有线程占有。
  3. 请求并持有条件:线程已经拥有了⼀个资源之后,还不满足,又尝试请求新的资源。
  4. 环路等待条件:多个线程在请求资源的情况下,形成了环路链。

如何解决死锁问题

改变产生死锁原因中的任意⼀个或多个条件就可以解决死锁的问题,其中可以被修改的条件只有两个:请求并持有条件环路等待条件

改变环路等待条件

通过修改获取锁的有序性来改变环路等待条件,修改代码如下:

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
public class UnDeadLock2 {
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("线程 1 得到锁 A");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("线程 1 得到锁 B");
System.out.println("线程 1 释放锁 B");
}
System.out.println("线程 1 释放锁 A");
}
}, "线程 1");
t1.start();

Thread t2 = new Thread(() -> {
synchronized (lockA) {
System.out.println("线程 2 得到锁 A");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("线程 2 得到锁 B");
System.out.println("线程 2 释放锁 B");
}
System.out.println("线程 2 释放锁 A");
}
}, "线程 2");
t2.start();
}
}

破坏请求并持有条件

可以通过破坏请求并持有条件解决死锁,修改代码如下:

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
public class UnDeadLock {
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("线程 1 得到锁 A");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// synchronized (lockB) {
// System.out.println("线程 1 得到锁 B");
// System.out.println("线程 1 释放锁 B");
// }
System.out.println("线程 1 释放锁 A");
}
}, "线程 1");
t1.start();

Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("线程 2 得到锁 B");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// synchronized (lockA) {
// System.out.println("线程 2 得到锁 A");
// System.out.println("线程 2 释放锁 A");
// }
System.out.println("线程 2 释放锁 B");
}
}, "线程 2");
t2.start();
}
}

Base64

一、何为 Base64 算法

Base64是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2 的 6 次方等于 64,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 Base64 单元,即 3 个字节可由 4 个可打印字符来表示。它可用来作为电子邮件的传输编码。在 Base64 中的可打印字符包括字母A-Za-z、数字0-9,这样共有 62 个字符,此外两个可打印符号在不同的系统中而不同。

Base64 常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括 MIME 的电子邮件及 XML 的一些复杂数据。

二、Base64 算法是如何设计的

在不同的实现中,Base64 算法中由 64 个字符组成的字符集是不一样的。但是通常的实现方法是选择 64 个通用且能打印的字符来组成这样一个集合。且要保证这个集合中的每个字符组成的数据在数据传输系统中不会被修改。

早期的 Base64 算法是用来实现运行相同操作系统之间进行拨号操作而创建的。

让我们先来看一下最通常的 Base64 索引表:

索引 对应字符 索引 对应字符 索引 对应字符 索引 对应字符
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

三、Base64 如何转换

  1. 把 3 个字节变成 4 个字节
  2. 每 76 哥字符加一个换行符
  3. 最后的结束符也要处理

例如:
转换前 11111111, 11111111, 11111111 (二进制)

转换后 00111111, 00111111, 00111111, 00111111 (二进制)

上面的三个字节是原文,下面的四个字节是转换后的 Base64 编码,其前两位均为 0。

转换后,通过上面的码表来得到想要的字符串

Lambda 表达式

一、为什么要使用 Lambda 表达式

Lambda 表达式就是为了使得我们的代码更加的简洁。如何简洁呢?我们直接举个例子来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LambdaTest1 {
@Test
public void test1() {
//第一种
Runnable runnable = new Runnable() {
public void run() {
System.out.println("不使用 Lambda 表达式");
}
};
runnable.run();
System.out.println("=======================");
//第二种
Runnable runnable1 = () -> System.out.println("使用 Lambda 表达式");
runnable1.run();
}
}

之前我们新建一个线程使用 5 行代码,但是如果我们使用 lambda 表达式只需要 1 行代码即可。

二、Lambda 表达式的使用

1.基本语法

在上面的例子中我们使用了这样一行 () -> System.out.println(“使用 Lambda 表达式”);下面我们对 lambda 的格式进行一个介绍:

  • 左边括号:lambda 的形参列表,就好比是我们定义一个接口,里面有一个抽象方法,这个抽象方法的形参列表。
  • 箭头:lambda 的操作符,所以你看见这个箭头心中知道这是一个 lambda 表达式就可以了。
  • 右边 lambda 体:就好比是我们实现了接口中的抽象方法。

lambda 表达式的使用可以分为以下 5 种基本的情况。我们一个一个来介绍。

2. 无参无返回值

这个是最简单的一种情况,就是刚刚我们所举的例子。

1
2
3
4
5
//此时如果方法体比较复杂是多行代码,那么这个 {} 是不能省略的
Runnable runnable1 = () -> {
System.out.println("使用 Lambda 表达式");
System.out.println("使用 Lambda 表达式");
};

我们可以看到没有任何参数也没有任何返回值,因此可以直接写,不过 lambda 体如果不是一行代码,那么就需要使用 {} 将其括起来。

3.有参数无返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test2() {
//第一种:没有使用 lambda 表达式
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("没有使用 lambda:有参数,但是没有返回值");
//第二种:使用 lambda 表达式
Consumer<String> consumer1 = (String s)->{
//此时只有一行输出代码,因此可以省去外部的 {}
System.out.println(s);
};
consumer.accept("使用 lambda:有参数,但是没有返回值");
}

4.有参数无返回值,数据类型可省略,称为类型推断

这种情况只能称之为上面的一种特例,只不过我们可以不传入类型,由编译器帮我们推断出来即可。

1
2
3
4
5
Consumer<String> consumer1 = (s)->{
//此时只有一行输出代码,因此可以省去外部的 {}
System.out.println(s);
};
consumer.accept("使用 lambda:有参数,但是没有返回值");

在这个例子中我们可以看到,直接把 s 中的 String 类型给去掉了,此时运行依然是正确的。这就是编译器自动为我们推断出了 s 的类型就是 String 的。只有一个参数是可以将小括号去除

5.有多个参数,有返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test3() {
//第一种:没有使用 lambda 表达式
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println("o1:"+o1);
return o1.compareTo(o2);
}
};
System.out.println(comparator.compare(1,2));
System.out.println("======================");
//第二种:使用 lambda 表达式
Comparator<Integer> comparator2 = (o1,o2)->{
System.out.println("o1:"+o1);
return o1.compareTo(o2);
};
System.out.println(comparator2.compare(1,2));
}

我们使用了一个比较器,当然了如果只有一条 return 语句的话,那样式就更简单了。箭头直接指向我们要返回的结果。

1
2
Comparator<Integer> comparator2 = (o1,o2)-> o1.compareTo(o2);
System.out.println(comparator2.compare(1,2));

三、Lambda 表达式深入解析

想要对 lambda 表达式有一个深入的理解,我们需要去认识另外一个知识点,那就是函数式接口。在上面我们的举得例子中比如 Consumer 或者是 Comparator 为什么能够使用 lambda 呢?就是因为实函数式接口,下面我们来认识一下:

1.什么是函数式接口

比如我们的 Runnable 就是一个函数式接口,我们可以到源码中看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}

他主要有如下的特点:

  1. 含有@FunctionalInterface 注解
  2. 只有一个抽象方法

也就是说只有函数式接口的变量或者是函数式接口,才能够赋值为 Lambda 表达式。当然了方法的类型可以任意。

2.函数式接口有什么用

函数式接口能够接受匿名内部类的实例化对象,换句话说,我们可以使用匿名内部类来实例化函数式接口的对象,而 Lambda 表达式能够代替内部类实现代码的进一步简化。并且 java 为我们提供了四个比较重要的函数式接口:

  1. 消费型接口:Consumer< T> void accept(T t) 有参数,无返回值的抽象方法;
  2. 供给型接口:Supplier < T> T get() 无参有返回值的抽象方法;
  3. 断定型接口: Predicate< T> boolean test(T t):有参,但是返回值类型是固定的 boolean
  4. 函数型接口: Function< T,R> R apply(T t) 有参有返回值的抽象方法;

这里仅仅是给出了 4 个,其实 java 提供了很多。比如 java.util.function 包下还有很多函数式接口可供使用。

3.自定义一个函数式接口

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface MyInterface{
void test();
}

public class LambdaTest2 {
public static void main(String[] args) {
MyInterface myInterface = () -> System.out.println("test");
}
}

现在我们定义了一个 MyInterface 的函数式接口,里面定义了一个 test 方法,如果我们定义了两个就不能使用 lambda 表达式了,为什么呢?因为 lambda 是一个接口方法,如果有两个方法,应该指定哪一个呢?就搞混了。

4.类型推导

在第二部分介绍 lambda 语法的时候曾经说过,lambda 本身具有类型推导,那么这个类型推导可以做到什么程度呢?编译器负责推导 lambda 的类型,它利用上下文被期待的类型当做推导的目标类型,当满足下面条件时,就会被赋予目标类型:

  1. 被期待的目标类型是一个函数式接口
  2. lambad 的入参类型和数量与该接口一致
  3. 返回类型一致
  4. 抛出异常类型一致

其实lambda 最后会由编译器生成 static 方法在当前类中,利用了 invokedynamic 命令脱离了内部类实现的优化。

Stream 流

Stream 操作的三个步骤

  • 创建 stream
  • 中间操作 (过滤、map)
  • 终止操作

一、Stream 流的格式

1
2
3
4
5
Stream<T> filter(Predicate<? super T> predicate);
-----> 参数:public interface Predicate<T> (函数式接口)
----> 抽象方法:boolean test(T t);
-----> 参数:public interface Consumer<T> (函数式接口)
----> 抽象方法:boolean test(T t);

二、获取流

根据集合来获取:

根据 Collection 获取流:
1
default Stream<E> stream()
  1. 根据 List 获取流
1
2
3
4
5
6
7
8
9
// 创建 List 集合
List<String> list = new ArrayList<>();
list.add("张老三");
list.add("张小三");
list.add("李四");
list.add("赵五");
list.add("张六");
list.add("王八");
Stream<String> stream1 = list.stream();
  1. 根据 Set 集合获取流
1
2
3
4
5
6
7
8
9
// 创建 List 集合
Set<String> set = new HashSet<>();
set.add("张老三");
set.add("张小三");
set.add("李四");
set.add("赵五");
set.add("张六");
set.add("王八");
Stream<String> stream2 = set.stream();
  1. 根据 Map 集合获取流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建 Map 集合
Map<Integer,String> map = new HashMap<>();
map.put(1,"张老三");
map.put(2,"张小三");
map.put(3,"李四");
map.put(4,"赵五");
map.put(5,"张六");
map.put(6,"王八");

// 3.1 根据 Map 集合的键获取流
Set<Integer> map1 = map.keySet();
Stream<Integer> stream3 = map1.stream();
// 3.2 根据 Map 集合的值获取流
Collection<String> map2 = map.values();
Stream<String> stream4 = map2.stream();
// 3.3 根据 Map 集合的键值对对象获取瑞
Set<Map.Entry<Integer, String>> map3 = map.entrySet();
Stream<Map.Entry<Integer, String>> stream5 = map3.stream();
  1. 根据数组获取流
1
2
3
// 根据数组获取流
String[] arr = {"张颜宇","张三","李四","赵五","刘六","王七"};
Stream<String> stream6 = Stream.of(arr);

三、Stream 流的常用方法

Stream 流的常用方法:

终结方法:返回值类型不再是 Stream 接口本身类型的方法,例如:forEach() 方法和 count 方法

非终结方法/延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,除了终结方法都是延迟方法。例如:filter,limit,skip,map,conat
方法名称 方法作用 方法种类 是否支持链式调用
count 统计个数 终结方法
forEach 逐一处理 终结方法
filter 过滤 函数拼接
limit 取用前几个 函数拼接
skip 跳过前几个 函数拼接
map 映射 函数拼接
cocat 组合 合并两个流 函数拼接
distinct 去重 函数拼接
anyMatch 只要有一个元素匹配传入的条件,就返回 true。 终结方法
allMatch 只要有一个元素不匹配传入的条件,就返回 false;
如果全部匹配,则返回 true。
终结方法
noneMatch 只要有一个元素匹配传入的条件,就返回 false;
如果全部不匹配,则返回 true。
终结方法
collect 收集 终结方法

四、收集 Stream 流

收集 Stream 流中的结果到集合

Stream 流提供 collect 方法,其参数需要一个 java.util.stream.Collector<T,A,R> 接口对象来指定收集到哪种集合中。java.util.stream.Collectors 类提供了一些方法,可以作为 Collector 接口的实例,最常用的就是静态方法 toList 与 toSet

  1. 收集到 List 集合——toList
1
2
3
4
5
public static void testList(){
Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六");
List<String> list = stream.collect(Collectors.toList());
System.out.println(list);
}
  1. 收集到 Set 集合——toSet
1
2
3
4
5
public static void testSet(){
Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六");
Set<String> set = stream.collect(Collectors.toSet());
System.out.println(set);
}
  1. 收集到指定集合

    例如:收集到 ArrayList 集合中

1
2
3
4
5
public static void testArrayList(){
Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六");
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
System.out.println(arrayList);
}

收集集合中的数据到数组中

  1. 转成 Object 数组——toArray
1
2
3
4
5
6
7
public static void test2Array(){
Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六");
Object[] array = stream.toArray();
for(Object o : array){
System.out.println(o);
}
}

该方式转成 Object 类型的数组,我们操作起来不是很方便

  1. 转成指定类型的数组——toArray

    例如:将 String 流转成 String 数组

1
2
3
4
5
6
7
public static void test2Array1(){
Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六");
String[] array = stream.toArray(String[]::new);
for(String str : array){
System.out.println("元素是:" + str + " 元素长度是:" + str.length());
}
}

对流中的数据的操作

  1. 对流中的数据进行聚合计算
    当我们使用 Stream 流处理数据后,可以像数据库聚合函数一样,对某个字段进行处理,比如,获取最大值,获取最小值,求总和,平均值,统计数量等。

    1. 获取最大值——Collectors.maxBy
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class StreamCollectTest01 {
    public static void main(String[] args) {
    Stream<Student> stream = Stream.of(
    new Student("张三", 21, 97D),
    new Student("李四", 23, 88D),
    new Student("王五", 20, 62D),
    new Student("赵六", 18, 59D),
    new Student("钱七", 24, 100D)
    );

    // 查找分数最大的 student
    Optional<Student> optionalStudent = stream.collect(Collectors.maxBy((s1, s2) -> {
    return s1.getScore() - s2.getScore() > 0 ? 1 : -1;
    }));
    Student student = optionalStudent.get();
    System.out.println(student);
    }
    }
    1. 获取最小值——Collectors.minBy
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class StreamCollectTest01 {
    public static void main(String[] args) {
    Stream<Student> stream = Stream.of(
    new Student("张三", 21, 97D),
    new Student("李四", 23, 88D),
    new Student("王五", 20, 62D),
    new Student("赵六", 18, 59D),
    new Student("钱七", 24, 100D)
    );

    // 查找分数最小的 student
    Optional<Student> optionalStudent = stream.collect(Collectors.minBy((s1, s2) -> {
    return s1.getScore() - s2.getScore() > 0 ? 1 : -1;
    }));
    Student student = optionalStudent.get();
    System.out.println(student);
    }
    }
    1. 求和——Collectors.summingDouble
    1
    2
    // 求所有分数之和
    Double aDouble = stream.collect(Collectors.summingDouble(Student::getScore));
    1. 求平均值——Collectors.averagingDouble
    1
    2
    3
    // 求分数的平均值
    Double aDouble = stream.collect(Collectors.averagingDouble(Student::getScore));
    System.out.println(aDouble);
    1. 统计数量——Collection.counting
    1
    2
    3
    // 求流中一共有多少个数据
    Long aLong = stream.collect(Collectors.counting());
    System.out.println(aLong);
  2. 对流中的数据进行分组
    当我们使用 Stream 流处理数据后,可以根据某个属性将数据分组

    1. 简单的分组——Collectors.groupingBy
      方法定义:
    1
    2
    3
    public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) {
    return groupingBy(classifier, toList());
    }
     例如:按年龄分组
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class StreamCollectTest01 {
    public static void main(String[] args) {
    Stream<Student> stream = Stream.of(
    new Student("张三", 21, 97D),
    new Student("李四", 21, 88D),
    new Student("王五", 24, 62D),
    new Student("赵六", 18, 59D),
    new Student("钱七", 24, 100D)
    );

    // 根据年龄将数据分组
    Map<Integer, List<Student>> collect = stream.collect(Collectors.groupingBy(Student::getAge));
    collect.forEach((k,v)->{
    System.out.println("key:" + k + " value = " + v);
    });
    }
    }
    1. 多级分组——Collectors.groupingBy
      使用 Collectors.groupingBy 的重载函数,方法定义
    1
    2
    3
    public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream) {
    return groupingBy(classifier, HashMap::new, downstream);
    }
      例如:
    
    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
    public class StreamCollectTest01 {
    public static void main(String[] args) {
    Stream<Student> stream = Stream.of(
    new Student("张三", 21, 97D),
    new Student("李四", 21, 88D),
    new Student("王五", 24, 62D),
    new Student("赵六", 18, 59D),
    new Student("钱七", 24, 100D)
    );

    // 先根据年龄分组,在根据分数分组
    Map<Integer, Map<String, List<Student>>> collect = stream.collect(Collectors.groupingBy(Student::getAge, Collectors.groupingBy(s -> {
    if (s.getScore() > 80) {
    return "优秀";
    } else {
    return "一般";
    }
    })));
    collect.forEach((k,v)->{
    System.out.println("age:" + k);
    v.forEach((k1,v1)->{
    System.out.println("\t" + "k1:" + k1 + " v1:" + v1);
    });
    });
    }
    }
    1. 对流中的数据进行分区——Collectors.partitioningBy
      Collectors.partitioningBy 会根据值是否为 true,把集合分割为两个列表,一个 true 列表,一个 false 列表。

      例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class StreamCollectTest01 {
    public static void main(String[] args) {
    Stream<Student> stream = Stream.of(
    new Student("张三", 21, 97D),
    new Student("李四", 21, 88D),
    new Student("王五", 24, 62D),
    new Student("赵六", 18, 59D),
    new Student("钱七", 24, 100D)
    );

    Map<Boolean, List<Student>> map = stream.collect(Collectors.partitioningBy(s -> {
    return s.getScore() > 80;
    }));
    map.forEach((k,v) -> {
    System.out.println("k:" + k + " v:" + v);
    });
    }
    }
    1. 对流中的数据进行拼接——Collertors.joining
      Collertors.joining 会根据指定的连接符,将所有的元素连接成一个字符串。

      例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class StreamCollectTest01 {
    public static void main(String[] args) {
    Stream<Student> stream = Stream.of(
    new Student("张三", 21, 97D),
    new Student("李四", 21, 88D),
    new Student("王五", 24, 62D),
    new Student("赵六", 18, 59D),
    new Student("钱七", 24, 100D)
    );

    String collect = stream.map(Student::getName).collect(Collectors.joining(","));
    System.out.println(collect);
    }
    }

Jedis

一、获取 Jedis

Jedis 是基于 java 语言的 redis_cli

maven 依赖:

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.0</version>
</dependency>

二、Jedis 基本使用

  1. Jedis 直连

img

Jedis 直连相当于一个 TCP 连接,数据传输完成后关闭连接

1
2
3
4
5
6
JedisPool jedisPool = new JedisPool("127.0.0.1",6379); //创建一个连接池
Jedis jedis = jedisPool.getResource(); //从连接池获取
jedis.set("token", UUID.randomUUID().toString());//此处生成一个随机字符串并存入 Redis
String token = jedis.get("token");//从 Redis 获取 key 为 token 的字符串
System.out.println("token = " + token);//此处打印可以看到我们存入的字符串
jedis.close(); //归还连接
  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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public interface CallJedis {
void call(Jedis jedis);
}
public class MyRedisPool {
private JedisPool jedisPool;

public MyRedisPool() {
this.jedisPool = new JedisPool("127.0.0.1",6379,null,"123");
}

public void execute(CallJedis callJedis){
try(Jedis jedis = jedisPool.getResource()){
callJedis.call(jedis);
}
}
}
public class RedisPractice {
public static void main(String[] args) {
//实例化连接池
MyRedisPool myRedisPool = new MyRedisPool();

//获取 Redis 连接资源,并确保在使用后归还
myRedisPool.execute(new CallJedis() {
@Override
public void call(Jedis jedis) {
//String
jedis.set("username","huanji");
//Map
jedis.hset("myhash","f1","v1");
//List
jedis.rpushx("mylist","1","2");
//Set
jedis.sadd("myset","a","b","a");
//zset
jedis.zadd("myzset",22,"a");
//获取
String username = jedis.get("username");
Map<String, String> map = jedis.hgetAll("myhash");
List<String> mylist = jedis.lrange("mylist", 0, -1);
Set<String> myset = jedis.smembers("myset");
List<Tuple> myzset = jedis.zrangeWithScores("myzset", 0, -1);

System.out.println(username);
System.out.println(map.toString());
System.out.println(mylist.toString());
System.out.println(myset.toString());
System.out.println(myzset.toString());
}
});
}
}

SpringBoot 集成 Redis

首先导入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>

在 yaml 文件配置 redis 访问地址

1
2
3
4
5
6
spring:
redis:
host: 127.0.0.1
port: 6379
# 没有密码不用加 password
password: 123

配置 FastJson 序列化 Redis

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
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

private Class<T> clazz;

static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}

public FastJsonRedisSerializer(Class<T> clazz){
super();
this.clazz = clazz;
}

@Override
public byte[] serialize(T t) throws SerializationException {
if(t == null){
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}

@Override
public T deserialize(byte[] bytes) throws SerializationException {
if(bytes == null || bytes.length <= 0){
return null;
}
String str = new String(bytes,DEFAULT_CHARSET);
return JSON.parseObject(str,clazz);
}

protected JavaType getJavaType(Class<?> clazz){
return TypeFactory.defaultInstance().constructType(clazz);
}
}

配置 Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = {"unchecked","rawtypes"})
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
//使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
//Hash 的 key 也采用 StringRedisSerializer 的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);

template.afterPropertiesSet();
return template;
}
}

创建 Redis 工具类 RedisCache

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;

/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}

/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}

/**
* 设置有效时间
*
* @param key Redis 键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}

/**
* 设置有效时间
*
* @param key Redis 键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}

/**
* 获取有效时间
*
* @param key Redis 键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}

/**
* 判断 key 是否存在
*
* @param key 键
* @return true 存在 false 不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}

/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}

/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}

/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}

/**
* 缓存 List 数据
*
* @param key 缓存的键值
* @param dataList 待缓存的 List 数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}

/**
* 获得缓存的 list 对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}

/**
* 缓存 Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}

/**
* 获得缓存的 set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}

/**
* 缓存 Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}

/**
* 获得缓存的 Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}

/**
* 往 Hash 中存入数据
*
* @param key Redis 键
* @param hKey Hash 键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}

/**
* 获取 Hash 中的数据
*
* @param key Redis 键
* @param hKey Hash 键
* @return Hash 中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}

/**
* 获取多个 Hash 中的数据
*
* @param key Redis 键
* @param hKeys Hash 键集合
* @return Hash 对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}

/**
* 删除 Hash 中的某条数据
*
* @param key Redis 键
* @param hKey Hash 键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}

/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}

使用 Redis

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class RedisController {
@Autowired
private RedisCache redisCache;

@GetMapping("/redis")
public void redisGet(){
redisCache.setCacheObject("username","huanji");
Object username = redisCache.getCacheObject("username");
System.out.println(username);
}
}

Spring

一、IOC

什么是 IOC

 Inverse of Control ——**控制反转**,是一种思想,这种**控制反转的思想主要指的是将对象的创建、组装、管理都从代码中自己实现转移到了外部容器中来帮我们进行实现**。在传统的开发方式当中,我们直接手写代码去主动创建和组装对象(将对象所需要的属性注入);在 IOC 思想中,这个过程被反转了,即由外部容器负责创建和管理对象。

在 IOC 中,我们将应用程序设计成一个个的组件,每个组件提供一定的功能,并通过接口与其他组件进行交互。通过 IOC 容器,我们可以把这些组件注册并配置,**容器负责根据配置信息创建组件实例,并维护它们之间的依赖关系和生命周期。**

IOC 的实现方式

IoC 的主要实现方式有两种:依赖查找、依赖注入。

依赖注入是一种更可取的方式。

那么依赖查找和依赖注入有什么区别呢?

依赖查找,主要是容器为组件提供一个回调接口和上下文环境。这样一来,组件就必须自己使用容器提供的 API 来查找资源和协作对象,控制反转仅体现在那些回调方法上,容器调用这些回调方法,从而应用代码获取到资源。

依赖注入,组件不做定位查询,只提供标准的 Java 方法让容器去决定依赖关系。容器全权负责组件的装配,把符合依赖关系的对象通过 Java Bean 属性或构造方法传递给需要的对象。

IOC 容器

IoC 容器:具有依赖注入功能的容器,可以创建对象的容器。IoC 容器负责实例化、定位、配置应用程序中的对象并建立这些对象之间的依赖。

依赖注入

DI,英文名称,Dependency Injection,意为依赖注入。

依赖注入:由 IoC 容器动态地将某个对象所需要的外部资源(包括对象、资源、常量数据)注入到组件 (Controller, Service 等)之中。简单点说,就是 IoC 容器会把当前对象所需要的外部资源动态的注入给我们。

Spring 依赖注入的方式主要有四个,基于注解注入方式、set 注入方式、构造器注入方式、静态工厂注入方式。推荐使用基于注解注入方式,配置较少,比较方便。

基于注解注入方式:

服务层代码:

1
2
3
4
5
@Service
public class AdminService {

//code
}

控制层代码:

1
2
3
4
5
6
7
8
9
10
@Controller
//多实例 每次获取 Bean 的时候会有一个新的实例 看情况添加 大部分场景不用加
@Scope("prototype")
public class AdminController {
//自动装配
@Autowired
private AdminService adminService;

//code
}

@Autowired 与@Resource 都可以用来装配 Bean,都可以写在字段、setter 方法上。他们的区别是:

@Autowired 默认按类型进行自动装配(该注解属于 Spring),默认情况下要求依赖对象必须存在,如果要允许为 null,需设置 required 属性为 false,例:@Autowired(required=false)。如果要使用名称进行装配,可以与@Qualifier 注解一起使用。
1
2
3
@Autowired
@Qualifier("adminService")
private AdminService adminService;
@Resource 默认按照名称进行装配(该注解属于 J2EE),名称可以通过 name 属性来指定。如果没有指定 name 属性,当注解写在字段上时,默认取字段名进行装配;如果注解写在 setter 方法上,默认取属性名进行装配。当找不到与名称相匹配的 Bean 时,会按照类型进行装配。但是,name 属性一旦指定,就只会按照名称进行装配。
1
2
@Resource(name = "adminService")
private AdminService adminService;
除此之外,对于一些复杂的装载 Bean 的时机,比如我们需要根据配置装载不同的 Bean,以完成不同的操作,可以使用 getBean(“beanID”) 的方式来加载 Bean。

通过 BeanID 加载 Bean 方法如下:
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
@Component
public class BeanUtils implements ApplicationContextAware {

private static ApplicationContext applicationContext;

@Override

public void setApplicationContext(ApplicationContext applicationContext) {

if (BeanUtils.applicationContext == null) {
BeanUtils.applicationContext = applicationContext;
}
}

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

public static Object getBean(String id) throws Exception {

try {
return applicationContext.containsBean(id) ? applicationContext.getBean(id) : null;
} catch (BeansException e) {
e.printStackTrace();
throw new Exception("not found bean id: " + id);
}
}
}
//在需要加载 Bean 的地方调用该方法即可
public class BaseController {

protected IService loadService(String id) throws Exception {

IService iService = (IService) BeanUtils.getBean(id);

if (iService != null) {
return iService;
} else {
throw new Exception("加载 Bean 错误");
}
}
}

二、AOP

什么是 AOP

AOP 即面向切面编程,可以将那些与业务不想关但是很多业务都需要调用的代码提取出来,思想就是不侵入原有代码的同时对功能进行增强。

AOP 通过定义一个切面,切面可以横切到应用程序的多个模块中,并添加增强的行为。这样我们就可以将通用的功能逻辑从业务逻辑中解耦出来,提高代码的可维护性和重用性。

AOP 主要一般应用于签名验签、参数校验、日志记录、事务控制、权限控制、性能统计、异常处理等。

AOP 涉及名词

  • 目标对象(Target):需要对它进行操作的业务类

  • 连接点(JoinPoint):程序在运行过程中能够插入切面的地点。

    • 例如,方法调用、异常抛出等。Spring 只支持方法级的连接点。
    • 一个类的所有方法前、后、抛出异常时等都是连接点。
  • 切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

  • 通知(Advice):切面的具体实现。就是要给目标对象织入的事情。
    以目标方法为参照点,根据放置的地方不同,可分为:

    • 前置通知(Before)

    • 后置通知(AfterReturning)

    • 异常通知(AfterThrowing)

    • 最终通知(After)

    • 环绕通知(Around)

      5 种。在实际开发中通常是切面类中的一个方法,具体属于哪类通知,通过方法上的注解区分。

  • 切面(Aspect):共有功能的实现。如日志切面、权限切面、验签切面等。

    • 在实际开发中通常是一个存放共有功能实现的标准 Java 类。
    • 当 Java 类使用了@Aspect 注解修饰时,就能被 AOP 容器识别为切面。
    • 是通知和切点的结合,通知和切点共同定义了切面的全部内容
  • 织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译时、类加载时、运行时。Spring 是在运行时完成织入,运行时织入通过 Java 语言的反射机制与动态代理机制来动态实现。

  • 代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象本身业务逻辑加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。目标对象被织入共有功能后产生的对象。

切入点(Pointcut)用法:

Pointcut 格式为:

execution(modifier-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

1. 修饰符匹配 modifier-pattern? 例:public private
2. 返回值匹配 ret-type-pattern 可以用 * 表示任意返回值
3. 类路径匹配 declaring-type-pattern? 全路径的类名
4. 方法名匹配 name-pattern 可以指定方法名或者用 * 表示所有方法;set* 表示所有以 set 开头的方法
5. 参数匹配 (param-pattern) 可以指定具体的参数类型,多个参数用“,”分隔;可以用 * 表示匹配任意类型的参数;可以用 (..) 表示零个或多个任意参数
6. 异常类型匹配 throws-pattern? 例:throws Exception

其中后面跟着?表示可选项

例如:

1
2
3
4
@Pointcut("execution(public * cn.wbnull.springbootdemo.controller.*.*(..))")
private void sign() {

}

AOP 实现

SpringAOP 是基于动态代理实现的,动态代理有两种,一种是 JDK 动态代理,另一种是 Cglib 动态代理

jdk 动态代理是利用反射的原理来实现的,需要调用反射包下的 Proxy 类的 newProxyInstance 方法来返回代理对象,这个方法中有**三个参数**,分别是用于**加载代理类的类加载器**、**被代理类实现的接口的 class 数组**、**用于增强方法的 InvocatioHandler 实现类**

 cglib 动态代理原理是利用 asm 开源包来实现的,是把被代理类的 class 文件加载进来,通过修改它的字节码生成子类来处理

jdk 动态代理要求代理类必须有实现的接口,生成的动态代理类会和代理类实现同样的接口,cglib 则,生成的动态代理类会继承被代理类。**Spring 默认使用 jdk 动态代理,当要被代理的类没有实现任何接口的时候采用 cglib**。

例如:

这是一个实现统计 controller 路由访问次数的代码

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
@Aspect
@Component
public class MethodCount {

// 声明一个 Map 类型的对象,用于存储方法调用次数
private Map<String, Integer> count = new HashMap<>();

// 定义切点,表示拦截 com.qcby.springbootdemo.Controller 包下的所有方法
@Pointcut("execution(* com.qcby.springbootdemo.Controller.*.*(..))")
public void count() {
System.out.println("切点方法执行"); //声明切点,并不会实际调用
}

// 环绕通知,在目标方法执行前后进行拦截
@Around("count()")
public Object methodExec(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("方法执行前");

// 获取方法签名信息
Signature signature = pjp.getSignature();
String name = signature.getName();
System.out.println(name); //-----login
System.out.println(signature.getDeclaringTypeName()); //---com.qcby.springbootdemo.Controller.LoginController

// 获取方法参数
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("aop arg:" + arg);
}

Object result = null;
System.out.println(signature.toLongString());
System.out.println();

// 统计方法调用次数,使用方法的签名作为 key
String key = signature.toLongString();
count.put(key, count.getOrDefault(key, 0) + 1);

// 执行目标方法
result = pjp.proceed();
System.out.println("方法执行后");

// 输出访问路由次数
System.out.println(count);
return result;
}
}

三、Bean 容器基本理论

Spring Bean 容器是 Spring 框架的核心组件之一,负责管理和组织应用中的对象(也称为 Bean)

  1. 什么是 Bean?

    • 在 Spring 中,Bean 是指由 Spring 容器管理的对象。这些对象通常是应用中的组件,例如服务、数据访问对象、实体等。
  2. Spirng Bean 容器的作用:

    • Spring Bean 容器负责创建、管理和注入(或装配)应用中的 Bean。它提供了一个环境,使得开发者可以通过配置文件或者注解的方式定义和组织 Bean。
  3. Bean 的生命周期

    • Bean 的生命周期包括实例化、初始化、使用和销毁四个阶段。
    • Spring 容器负责在适当的时机执行这些阶段的操作,例如通过构造函数实例化 Bean、调用初始化方法、提供 Bean 给其他组件使用,最后在应用关闭时销毁 Bean。
  4. Bean 的作用域:

    • Spring 支持多种 Bean 的作用域,包括单例(Singleton)、原型(Prototype)、会话(Session)、请求(Request)等。
    • 每种作用域定义了 Bean 实例的生命周期和访问范围。
  5. Bean 的装配:

    • 装配是指将不同的 Bean 组装在一起,形成应用的组件关系。
    • Spring 支持通过 XML 配置、注解和 JAVA 配置等方式进行 Bean 的装配
  6. Bean 的依赖注入

    • 依赖注入是 Spring 框架的一个关键特性,它通过将一个 Bean 的依赖关系通过构造函数、Setter 方法或者接口注入到另一个 Bean 中。这样可以降低组件之间的耦合度。
  7. Bean 的自动装配:

    • Spring 支持自动装配,通过指定 @Autowired 注解或者使用 XML 配置,Spring 可以自动识别和满足 Bean 之间的依赖关系。
  8. ApplicationContext 和 BeanFactory:

    • Spring 提供了两个核心的容器接口,即ApplicationContextBeanFactoryApplicationContextBeanFactory的子接口,提供了更丰富的功能,例如事件传播、AOP 等。
  9. 配置元数据:

    • Bean 的配置信息通常使用配置元数据来定义,可以使用 XML 文件、Java 配置类或者注解。配置元数据包含了 Bean 的类型、依赖关系、作用域、初始化方法、销毁方法等信息。

总体而言,Spring Bean 容器为开发者提供了一种松散耦合的方式来组织和管理应用中的组件,使得应用更加灵活、可维护和可测试。

四、Spring 注解

  1. 声明 bean 的注解

    • @Component:泛指各种组件

    • @Controller、@Service、@Repository 都可以称为@Component

    • @Controller:控制层

    • @Service:业务层

    • @Repository:数据访问层

  2. 注入 bean 的注解

    • @Autowired:由 Spring 提供
    • @Inject:由 JSR-330 提供
    • @Resource:由 JSR-250 提供
  3. Java 配置类相关注解

    • @Configuration:声明当前类为配置类
    • @Bean:注解在方法上,声明当前方法的返回值为一个 bean,替代 xml 中的方式
    • @ComponentScan:用于对 Component 进行扫描
  4. 切面(AOP)相关注解

    • @Aspect:声明一个切面
    • @After:在方法执行之后执行(方法上)
    • @Before:在方法执行之前执行(方法上)
    • @Around:在方法执行之前与之后执行(方法上)
    • @PointCut:声明切点
    • @EnableAspectJAutoProxy:开启 Spring 对 AspectJ 代理的支持
  5. @Bean 的属性支持

    • @Scope 设置类型包括:设置 Spring 容器如何新建 Bean 实例
    • Singleton:单例,一个 Spring 容器中只有一个 bean 实例,默认模式
    • Protetype:每次调用新建一个 bean
    • Request:web 项目中,给每个 http request 新建一个 bean
    • Session:web 项目中,给每个 http session 新建一个 bean
    • GlobalSession:给每一个 global http session 新建一个 Bean 实例
  6. @Value 注解

    • 注入普通字符、注入操作系统属性、注入表达式结果、注入其它 bean 属性、注入文件资源、注入网站资源、注入配置文件
  7. 环境切换

    • @Profile:指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件。
    • @Conditional:通过实现 Condition 接口,并重写 matches 方法,从而决定该 bean 是否被实例化。
  8. 异步相关

    • @EnableAsync:配置类中通过此注解开启对异步任务的支持
    • @Async:在实际执行的 bean 方法使用该注解来声明其是一个异步任务(方法上或类上所有的方法都将异步,需要@EnableAsync 开启异步任务)
  9. 定时任务相关

    • @EnableScheduling:在配置类上使用,开启计划任务的支持
    • @Scheduled:来申明这是一个任务,包括 cron,fixDelay,fixRate 等类型(方法上,需先开启计划任务的支持)
  10. Enable***注解说明

    • @EnableAspectAutoProxy:开启对 AspectJ 自动代理的支持
    • @EnableAsync:开启异步方法的支持
    • @EnableScheduling:开启计划任务的支持
    • @EnableWebMvc:开启 web MVC 的配置支持
    • @EnableConfigurationProperties:开启对@ConfigurationProperties 注解配置 Bean 的支持
    • @EnableJpaRepositories:开启对 SpringData JPA Repository 的支持
    • @EnableTransactionManagement:开启注解式事务的支持
    • @EnableCaching:开启注解式的缓存支持
  11. 测试相关注解

    • @RunWith:运行器,Spring 中通常用于对 JUnit 的支持
    • @ContextConfiguration
      用来加载配置文件,其中 classess 属性用来加载配置类
    1
    2
    3
    4
    5
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {"classpath*:/*.xml"})
    public class CDPlayerTest {

    }

五、SpringBoot 注解

  1. @SpringBootApplication:
    SpringBoot的核心注解,主要目的是开启自动配置。它也是一个组合注解,主要组合了@Configuration,@EnableAutoConfiguration(核心)和@ComponentScan。可以通过@SpringBootApplication(exclude={想要关闭的自动配置的类名.class}) 来关闭特定的自动配置,其中@ComponentScanspring Boot扫描到Configuration类并把它加入到程序上下文。

  2. @EnableAutoConfiguration
    此注释自动载入应用程序所需的所有Bean——这依赖于Spring Boot在类路径中的查找。该注解组合了@Import注解,@Import注解导入了EnableAutoCofigurationImportSelector类,它使用SpringFactoriesLoader.loaderFactoryNames方法来扫描具有META-INF/spring.factories文件的jar包。而spring.factories里声明了有哪些自动配置.

  3. @Configuration:
    等同于springXML配置文件;使用Java代码可以检查类型安全。

  4. @ComponentScan

    表示将该类自动发现扫描组件。个人理解相当于,如果扫描到有@Component、@Controller、@Service等这些注解的类,并注册为Bean,可以自动收集所有的Spring组件,包括@Configuration类。

SpringSecurity

SpringBoot 项目部署 SpringSecurity

数据库表结构

  1. user 表 用户

image-20240302192031335

  1. role 表 角色

image-20240302192056423

  1. user_role 表 用户角色关系

image-20240302192331838

  1. permission 表 系统权限

image-20240302192457504

  1. role_permission 表 角色权限关联

image-20240302192548970

pom 文件

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.huanji</groupId>
<artifactId>spring_security_practice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_security_practice</name>
<description>spring_security_practice</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.40</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
<scope>compile</scope>
</dependency>
<!-- jjwt 依赖包 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.1</version>
</dependency>
<!-- hutool 工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
</project>

yaml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
application:
name: springboot3-security-jwt
datasource:
#数据库驱动完整类名
driver-class-name: com.mysql.cj.jdbc.Driver
#数据库连接 url
url: jdbc:mysql://127.0.0.1:3306/security_practice?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
#数据库用户名
username: root
#数据库密码
password: root
# Logger Config
logging:
level:
com.hexadecimal: debug
server:
port: 8080
servlet:
context-path: /sq

创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@TableName("t_user")
public class User implements Serializable {
private static final long serialVersionUID = -1L;

private Integer id;
private String name;
private String username;
private String password;
private String phone;
private Integer gender;
private Boolean enabled;
private LocalDateTime lastLoginTime;

}
1
2
3
4
5
6
7
8
9
10
@Data
@TableName("t_role")
public class Role implements Serializable {
private static final long serialVersionUID = -1L;

private Integer id;
private String name;
private String remark;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@TableName("t_permission")
public class Permission implements Serializable {
private static final long serialVersionUID = -1L;

private Integer id;
private String name;
private String url;
private Integer method;
private String service;
private Integer parentId;

}
1
2
3
4
5
6
7
8
9
10
@Data
@TableName("t_user_role")
public class UserRole implements Serializable {
private static final long serialVersionUID = -1L;

private Integer id;
private Integer roleId;
private Integer userId;

}
1
2
3
4
5
6
7
8
9
10
@Data
@TableName("t_role_permission")
public class RolePermission implements Serializable {
private static final long serialVersionUID = -1L;

private Integer id;
private Integer roleId;
private Integer permissionId;

}

Mapper 类

1
2
public interface UserMapper extends BaseMapper<User> {
}
1
2
public interface RoleMapper extends BaseMapper<Role> {
}
1
2
public interface PermissionMapper extends BaseMapper<Permission> {
}
1
2
public interface UserRoleMapper extends BaseMapper<UserRole> {
}
1
2
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
}

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
39
40
41
42
43
44
45
46
47
48
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
@Resource
private UserMapper mapper;
@Autowired
private UserRoleService userRoleService;
@Autowired
private RolePermissionService rolePermissionService;
@Autowired
private PermissionService permissionService;

//获取权限
public List<Permission> getPermissionByUsername(String username) {
User user = super.getOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username), true);
return this.getPermissionByUser(user);
}

public List<Permission> getPermissionByUserId(Integer userId) {
User user = super.getById(userId);
return this.getPermissionByUser(user);
}

public List<Permission> getPermissionByUser(User user) {
List<Permission> permissions = new ArrayList<>();
if (null != user) {
//获取用户对应的全部角色
List<UserRole> userRoles = userRoleService.list(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUserId, user.getId()));
//如果不为空
if (CollectionUtils.isNotEmpty(userRoles)) {
List<Integer> roleIds = new ArrayList<>();
userRoles.stream().forEach(userRole -> {
roleIds.add(userRole.getRoleId());
});
List<RolePermission> rolePermissions = rolePermissionService.list(Wrappers.<RolePermission>lambdaQuery().in(RolePermission::getRoleId, roleIds));
//如果不为空
if (CollectionUtils.isNotEmpty(rolePermissions)) {
//获取角色拥有权限的 id
List<Integer> permissionIds = new ArrayList<>();
rolePermissions.stream().forEach(rolePermission -> {
permissionIds.add(rolePermission.getPermissionId());
});
permissions = permissionService.list(Wrappers.<Permission>lambdaQuery().in(Permission::getId, permissionIds));
}
}
}
return permissions;
}
}
1
2
3
@Service
public class RoleService extends ServiceImpl<RoleMapper, Role> {
}
1
2
3
@Service
public class PermissionService extends ServiceImpl<PermissionMapper, Permission> {
}
1
2
3
@Service
public class RolePermissionService extends ServiceImpl<RolePermissionMapper, RolePermission> {
}
1
2
3
@Service
public class UserRoleService extends ServiceImpl<UserRoleMapper, UserRole> {
}

用户登录请求实体

1
2
3
4
5
6
7
@Data
public class UserLoginDTO implements Serializable {
private static final long serialVersionUID = -1L;

private String username;
private String password;
}

接口响应实体

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
@Data
public class ResultData<T> {
/**
* 响应状态码
*/
private Integer code;
/**
* 响应信息
*/
private String message;
/**
* 响应数据
*/
private T data;

public static ResultData<String> success() {
return success("ok");
}

public static <T> ResultData<T> success(String message) {
return success(message, null);
}

public static <T> ResultData<T> success(T data) {
return success("ok", data);
}

public static <T> ResultData<T> success(String message, T data) {
ResultData<T> resultDTO = new ResultData<T>();
resultDTO.setCode(ResponseCodeEnum.OK.getCode());
resultDTO.setMessage(message);
resultDTO.setData(data);
return resultDTO;
}

public static <T> ResultData<T> error(String message) {
return error(ResponseCodeEnum.ERROR, message);
}

public static <T> ResultData<T> error(ResponseCodeEnum responseCode, Throwable e) {
return error(responseCode, e.getMessage() != null ? e.getMessage() : "系统异常,请联系管理员!");
}

public static <T> ResultData<T> error(ResponseCodeEnum responseCode, String message) {
ResultData<T> resultDTO = new ResultData<T>();
resultDTO.setCode(responseCode.getCode());
resultDTO.setMessage(message);
return resultDTO;
}
}

响应码枚举

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
/**
* 响应状态码
*/
public enum ResponseCodeEnum {
OK(200, "请求成功"),

BAD_REQUEST(400, "失败的请求"),

UNAUTHORIZED(401, "未授权"),

FORBIDDEN(403, "禁止访问"),

NOT_FOUND(404, "请求找不到"),

NOT_ACCEPTABLE(406, "不可访问"),

CONFLICT(409, "冲突"),

ERROR(500, "服务器发生异常");

private final Integer code;

private final String message;

ResponseCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}

public Integer getCode() {
return code;
}

public String getMessage() {
return getMessage();
}

}

统一异常处理

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseException extends RuntimeException {
private ResponseCodeEnum responseCode;

public BaseException(ResponseCodeEnum responseCode, String message) {
super(message);

setResponseCode(responseCode);
}

}
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
/**
* 全局异常处理类
*/
@RestControllerAdvice
@Slf4j
public class BaseExceptionHandler {

/**
* 处理 BaseException
*
* @param response
* @param e
* @return
*/
@ExceptionHandler(BaseException.class)
public ResultData<String> handlerGlobalException(HttpServletResponse response, BaseException e) {
log.error("请求异常:", e);
response.setStatus(e.getResponseCode().getCode());

return ResultData.error(e.getResponseCode(), e);
}

/**
* 处理 BindException
*
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultData<String> handlerBindException(BindException e) {
log.error("请求异常:", e);
BindingResult bindingResult = e.getBindingResult();
FieldError fieldError = bindingResult.getFieldError();
assert fieldError != null;
String defaultMessage = fieldError.getDefaultMessage();

return ResultData.error(ResponseCodeEnum.BAD_REQUEST, defaultMessage);
}

/**
* 处理 Exception
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> handlerException(Exception e) {
log.error("请求异常:", e);

return ResultData.error(ResponseCodeEnum.ERROR, e);
}

}

MyBtaisPlus 配置

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@MapperScan("com.huanji.spring_security_practice.mapper")
public class MybatisPlusConfig {

/**
* 分页插件
*/
@Bean
public PaginationInnerInterceptor paginationInterceptor() {
return new PaginationInnerInterceptor();
}
}

JWT 工具类

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
@Component
@Slf4j
public class JwtUtil {
private static final String SECRET = "zxcvbnmfdasaererafafafafafafakjlkjalkfafadffdafadfafafaaafadfadfaf1234567890";
private static final long EXPIRE = 60 * 24 * 7;
public static final String HEADER = "Authorization";

/**
* 生成 jwt token
*/
public String generateToken(String username) {
SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
//过期时间
LocalDateTime tokenExpirationTime = LocalDateTime.now().plusMinutes(EXPIRE);
return Jwts.builder()
//使用 HMAC SHA-512 签名算法和指定的密钥对 Token 进行签名。
.signWith(signingKey, Jwts.SIG.HS512)
// 设置 Token 的头部信息
.header().add("typ", "JWT").and()
// 设置 Token 的签发时间
.issuedAt(Timestamp.valueOf(LocalDateTime.now()))
// 设置 Token 的主题,通常为用户名
.subject(username)
// 设置 Token 的过期时间
.expiration(Timestamp.valueOf(tokenExpirationTime))
// 向 Token 中添加自定义的声明信息
.claims(Map.of("username", username))
// 生成最终的 Token 字符串
.compact();
}

/**
* 通过令牌获取声明信息
*
* @param token JWT 令牌
* @return 包含声明信息的对象
*/
public Claims getClaimsByToken(String token) {
// 使用密钥创建签名密钥
SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));

// 解析令牌并验证签名
return Jwts.parser()
//使用指定的密钥进行验证,确保 Token 的签名有效。
.verifyWith(signingKey)
//构建 JWT Parser,准备解析 Token。
.build()
//解析已签名的 Token,验证签名并获取 Token 的声明(Claims)。
.parseSignedClaims(token)
//获取 Token 的负载(Payload),即包含在 Token 中的用户定义的信息。
.getPayload();
}

/**
* 检查 token 是否过期
*
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}

/**
* 获得 token 中的自定义信息,一般是获取 token 的 username,无需 secret 解密也能获得
* @param token
* @param filed
* @return
*/
public String getClaimFiled(String token, String filed){
try{
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(filed).asString();
} catch (JWTDecodeException e){
log.error("JwtUtil getClaimFiled error: ", e);
return null;
}
}
}

账户信息实体

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
@Slf4j
public class AccountUser implements UserDetails {
private static final long serialVersionUID = -1L;
private Integer userId;
private String password;
private String username;
private Collection<? extends GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;

public AccountUser(Integer userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}

public AccountUser(Integer userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

public Integer getUserId() {
return this.userId;
}

@Override
public String getPassword() {
return this.password;
}

@Override
public String getUsername() {
return this.username;
}

@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}

@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}

@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}

@Override
public boolean isEnabled() {
return this.enabled;
}

}

JWTAuthenticationFilter 过滤器

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
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Resource
private JwtUtil jwtUtil;

@Autowired
private AccountUserDetailsService accountUserDetailsService;

/**
* 进行 JWT Token 验证的过滤器。
*
* @param request HTTP 请求对象
* @param response HTTP 响应对象
* @param chain 过滤器链
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 从请求头中获取 JWT Token
String token = request.getHeader(JwtUtil.HEADER);

// 如果未获取到 Token,继续往后执行,因为后面可能有鉴权管理器等组件判断是否拥有身份凭证,可以放行
// 没有 Token 相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
if (StrUtil.isBlankOrUndefined(token)) {
chain.doFilter(request, response);
return;
}

// 解析并验证 JWT Token,获取 Token 的声明信息(Claims)
Claims claims = jwtUtil.getClaimsByToken(token);
if (claims == null) {
throw new BaseException(ResponseCodeEnum.BAD_REQUEST, "Token 异常");
}

// 检查 Token 是否过期
if (jwtUtil.isTokenExpired(claims.getExpiration())) {
throw new BaseException(ResponseCodeEnum.BAD_REQUEST, "Token 已过期");
}

// 从 Token 的声明信息中获取用户名
String username = claims.getSubject();

// 构建 UsernamePasswordAuthenticationToken
// 这里密码为 null,是因为提供了正确的 Token,实现自动登录
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, accountUserDetailsService.getUserAuthority(username));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

// 将认证信息设置到 Security 上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

// 继续执行后续的过滤器链
chain.doFilter(request, response);
}
}

认证/授权

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
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

/**
* 处理访问被拒绝(Access Denied)情况的方法。
*
* @param httpServletRequest HTTP 请求对象
* @param httpServletResponse HTTP 响应对象
* @param e 访问被拒绝的异常对象
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 异常
*/
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e)
throws IOException, ServletException {
// 设置响应的内容类型为 JSON 格式,并指定字符集为 UTF-8
httpServletResponse.setContentType("application/json;charset=UTF-8");
// 设置响应状态码为 403 Forbidden
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);

// 构建包含错误信息的 ResultData 对象
ResultData<String> resultDTO = ResultData.error(e.getMessage());

// 获取输出流,将 ResultData 对象转为 JSON 字符串并写入响应
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
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
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

/**
* 处理身份验证失败(未经授权)情况的方法。
*
* @param httpServletRequest HTTP 请求对象
* @param httpServletResponse HTTP 响应对象
* @param e 身份验证异常对象
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 异常
*/
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
throws IOException, ServletException {
// 设置响应的内容类型为 JSON 格式,并指定字符集为 UTF-8
httpServletResponse.setContentType("application/json;charset=UTF-8");
// 设置响应状态码为 401 Unauthorized
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

// 构建包含错误信息的 ResultData 对象
ResultData<String> resultDTO = ResultData.error("请先登录");

// 获取输出流,将 ResultData 对象转为 JSON 字符串并写入响应
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
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
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private JwtUtil jwtUtil;

/**
* 处理身份验证成功事件的方法。
*
* @param httpServletRequest HTTP 请求对象
* @param httpServletResponse HTTP 响应对象
* @param authentication 包含了有关已进行身份验证的主体的信息
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 异常
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)
throws IOException, ServletException {
// 设置响应的内容类型为 JSON 格式,并指定字符集为 UTF-8
httpServletResponse.setContentType("application/json;charset=UTF-8");

// 生成 JWT Token,并将其放置到响应头中
String token = jwtUtil.generateToken(authentication.getName());
httpServletResponse.setHeader(JwtUtil.HEADER, token);

// 构建登录成功的 ResultData 对象
ResultData<String> resultDTO = ResultData.success("SuccessLogin");

// 获取输出流,将 ResultData 对象转为 JSON 字符串并写入响应
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
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
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

/**
* 处理身份验证失败事件的方法。
*
* @param httpServletRequest HTTP 请求对象
* @param httpServletResponse HTTP 响应对象
* @param e 身份验证失败的异常对象
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 异常
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
throws IOException, ServletException {
// 设置响应的内容类型为 JSON 格式,并指定字符集为 UTF-8
httpServletResponse.setContentType("application/json;charset=UTF-8");

// 构建包含身份验证失败信息的 ResultData 对象
ResultData<String> resultDTO = ResultData.error("用户名或密码错误");

// 获取输出流,将 ResultData 对象转为 JSON 字符串并写入响应
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
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
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {

/**
* 处理登出成功事件的方法。
*
* @param httpServletRequest HTTP 请求对象
* @param httpServletResponse HTTP 响应对象
* @param authentication 包含了有关已进行身份验证的主体的信息
* @throws IOException 如果发生 I/O 错误
* @throws ServletException 如果发生 Servlet 异常
*/
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)
throws IOException, ServletException {
// 如果存在已进行身份验证的主体
if (authentication != null) {
// 使用 Spring Security 提供的 SecurityContextLogoutHandler 处理登出
new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
}

// 设置响应的内容类型为 JSON 格式,并指定字符集为 UTF-8
httpServletResponse.setContentType("application/json;charset=UTF-8");

// 清空响应头中的 JWT Token
httpServletResponse.setHeader(JwtUtil.HEADER, "");

// 清空 Spring Security 的上下文
SecurityContextHolder.clearContext();

// 构建登出成功的 ResultData 对象
ResultData<String> resultDTO = ResultData.success("SuccessLogout");

// 获取输出流,将 ResultData 对象转为 JSON 字符串并写入响应
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}

Controller

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
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {
@Resource
private JwtUtil jwtUtil;

@Autowired
private UserService userService;

@Autowired
private AuthenticationProvider authenticationProvider;

@PostMapping("/login")
public ResultData login(@RequestBody @Validated UserLoginDTO userLoginDTO) {
Authentication authenticate = authenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(userLoginDTO.getUsername(), userLoginDTO.getPassword()));
// 认证成功
String token = jwtUtil.generateToken(userLoginDTO.getUsername());
Map<String, String> map = new HashMap<>();
map.put("token", token);
return ResultData.success(map);
}

//@PreAuthorize 配合@EnableGlobalMethodSecurity(prePostEnabled = true) 使用
@PreAuthorize("hasAuthority('/user/logout')")
@GetMapping("/logout")
public ResultData logout(HttpServletRequest request, HttpServletResponse response) {
// 退出登录
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
//清除认证
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return ResultData.success();
}
}

Security 配置

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
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
private static final String[] URL_WHITELIST = {"/user/login","/login","/favicon.ico"};

@Autowired
private AccountUserDetailsService accountUserDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Autowired
private JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;


@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}


/**
* 配置身份认证提供者,用于对用户进行身份验证
*
* @return DaoAuthenticationProvider 实例
*/
@Bean
public AuthenticationProvider authenticationProvider() {
// 创建一个用户认证提供者
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 设置用户相关信息,可以从数据库中读取、或者缓存、或者配置文件
authProvider.setUserDetailsService(accountUserDetailsService);
// 设置加密机制,用于对用户进行身份验证
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

/**
* 配置身份验证管理器,用于处理身份验证请求
*
* @param config AuthenticationConfiguration 实例
* @return AuthenticationManager 实例
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

/**
* 配置 Spring Security 过滤器链,定义请求的安全配置
*
* @param http HttpSecurity 实例
* @return SecurityFilterChain 实例
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 csrf(防止跨站请求伪造攻击)
.csrf(csrf -> csrf.disable())
// 配置登录操作
.formLogin(form -> form
.loginPage("/login.html")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/").permitAll()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler))
// 配置登出操作
.logout(logout -> logout.logoutSuccessHandler(jwtLogoutSuccessHandler))
// 使用无状态 session,即不使用 session 缓存数据
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 设置白名单,允许特定路径的请求不进行身份验证
.authorizeHttpRequests(auth -> auth
.requestMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated())
// 配置异常处理器,处理身份验证和授权异常
.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler))
// 添加 JWT 身份验证过滤器
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}

前端

部署 vue3 项目

登录页

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
129
130
131
132
133
134
135
<template>
<div>
<el-form
:rules="rules"
ref="loginForm"
v-loading="loading"
element-loading-text="正在登录..."
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
:model="loginForm"
class="loginContainer">
<h3 class="loginTitle"> 系统登录 </h3>
<el-form-item prop="username">
<el-input size="normal" type="text" v-model="loginForm.username" auto-complete="off"
placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input size="normal" type="password" v-model="loginForm.password" auto-complete="off"
placeholder="请输入密码"></el-input>
</el-form-item>
<el-button size="normal" type="primary" style="width: 100%;" @click="submitLogin"> 登录 </el-button>
</el-form>
</div>
</template>

<script>

export default {
name: "Login",
data(){
return{
loading: false,
rules: {
username: [{required: true, message: ' 请输入用户名 ', trigger: 'blur'}],
password: [{required: true, message: ' 请输入密码 ', trigger: 'blur'}],
},
loginForm: {
username: '',
password: ''
}
}
},
mounted() {
// 在组件加载后,检查本地存储中是否存在用户信息
const user = window.localStorage.getItem('user');
if (user) {
// 如果用户信息存在,自动跳转到首页
this.$router.push({ name: 'index' });
}
},
methods: {
async postRequest(url, data) {
try {
const response = await this.$axios.post(url, data);
return response.data;
} catch (error) {
if (error.response) {
// 服务器返回错误
const status = error.response.status;
const errorMessage = error.response.data.message || "Server Error";

if (status === 500 && errorMessage === "Bad credentials") {
// 处理登录失败的逻辑,比如显示错误信息等
alert(' 用户名或密码错误 ');
} else {
// 处理其他服务器错误
console.error(`Server Error: ${status} - ${errorMessage}`);
// 显示通用错误提示
alert("服务器发生错误");
}
} else {
// 客户端请求错误,如网络问题等
console.error("Client Request Error:", error.message);
// 显示通用错误提示
alert("请求发生错误");
}

return null;
}
},

async submitLogin() {
this.$refs.loginForm.validate(async (valid) => {
if (valid) {
this.loading = true;
try {
const resp = await this.postRequest('sq/user/login', this.loginForm);
this.loading = false;

if (resp && resp.code === 200) {
// 处理登录成功的逻辑,保存用户信息等
localStorage.setItem('user', resp.data.token);
// 跳转到首页
this.$router.push({ name: 'index' });
}
} catch (error) {
alert("登录失败");
this.loading = false;
return false;
}
} else {
return false;
}
});
}
}
}
</script>
<style>
.loginContainer {
border-radius: 15px;
background-clip: padding-box;
margin: 180px auto;
width: 350px;
padding: 15px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}

.loginTitle {
margin: 15px auto 20px auto;
text-align: center;
color: #505458;
}

.loginRemember {
text-align: left;
margin: 0px 0px 15px 0px;
}
.el-form-item__content{
display: flex;
align-items: center;
}
</style>

首页

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
<template lang="">
<div>
<h1> 你好 </h1>
<br>
<button @click="logout()"> 退出登录 </button>
</div>
</template>
<script>
export default {
methods: {
async logout() {
try {
await this.$axios.get('sq/user/logout');
// 清除本地存储的用户信息
localStorage.removeItem('user');
this.$router.push('/login');
} catch (error) {
if (error.response) {
// 服务器返回错误
const status = error.response.status;
const errorMessage = error.response.data.message || "Server Error";

if (status === 500 && errorMessage === "不允许访问") {
// 处理不允许访问的逻辑,比如显示错误信息等
alert("您无权访问");
} else {
// 处理其他服务器错误
console.error(`Server Error: ${status} - ${errorMessage}`);
// 显示通用错误提示
this.$message.error("服务器发生错误");
}
} else {
// 客户端请求错误,如网络问题等
console.error("Client Request Error:", error.message);
// 显示通用错误提示
this.$message.error("请求发生错误");
}
}
}
}
}
</script>
<style lang="">

</style>

main.js

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
import './assets/main.css';
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import "element-plus/dist/index.css";
import axios from 'axios';
import App from './App.vue';
import { createRouter, createWebHistory } from 'vue-router';
import Index from './components/index.vue';
import Login from './components/login.vue';

const app = createApp(App);

// 使用 VueRouter
const router = createRouter({
history: createWebHistory(), // 指定使用历史模式
routes: [
{ path: '/', name: 'index', component: Index, meta: { requiresAuth: true }},
{ path: '/login', component: Login }
]
});

// 路由守卫
router.beforeEach((to, from, next) => {
// 检查是否需要登录权限
if (to.meta.requiresAuth) {
// 从本地存储中获取用户信息
const user = localStorage.getItem('user');
// 如果用户信息不存在,重定向到登录页
if (!user) {
next('/login');
} else {
// 如果存在用户信息,允许访问
next();
}
} else {
// 如果不需要登录权限,直接允许访问
next();
}
});
axios.interceptors.request.use(config => {
// 从本地存储或其他地方获取 Token
const token = localStorage.getItem('user') ? localStorage.getItem('user'): null;

// 添加 Authorization 头
if (token) {
config.headers.Authorization = `${token}`;
}

return config;
});

app.use(router);

// 使用 ElementPlus,将 axios 设置为全局属性,并挂载应用
app.use(ElementPlus).mount('#app');

// 将 axios 设置为全局属性
app.config.globalProperties.$axios = axios;

App

1
2
3
4
5
6
<script setup>
</script>

<template>
<router-view/>
</template>

config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vue from '@vitejs/plugin-vue';
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [Vue()],
server: {
proxy: {
'/sq': {
target: 'http://localhost:8080/sq', // 后端的根路径
changeOrigin: true,
rewrite: (path) => path.replace(/^\/sq/, ''),
},
},
},
});

树形结构查询

数据库表结构

image-20240301153524227

表数据

image-20240301153552904

DTO

1
2
3
4
5
@Data
public class CountryDTO extends Country implements Serializable {
//子节点
List<CountryDTO> countryDTOS;
}

Mapper

1
2
3
4
@Mapper
public interface CountryMapper {
List<CountryDTO> selectTreeNodes(String id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="review.mapper.CountryMapper">

<select id="selectTreeNodes" resultType="review.pojo.dto.CountryDTO">
with recursive t1 as (
select * from china p where id = #{id}
union all
select t.* from china t inner join t1 on t1.id = t.parentid
)
select * from t1 order by t1.id, t1.parentid
</select>
</mapper>

Service

1
2
3
4
5
@Service
public interface CountryService {
public List<CountryDTO> get(String id);
}

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
@Service
public class CountryServiceImpl implements CountryService {
@Autowired
private CountryMapper countryMapper;

@Override
public List<CountryDTO> get(String id) {
//取得数据库数据
List<CountryDTO> countryDTOS = countryMapper.selectTreeNodes(id);
//给每个数据都加上 key
Map<String, CountryDTO> collect = countryDTOS.stream()
.filter(item -> !id.equals(item.getId()))
//key 通过 id 获取,value 为对象,如果有重复的 key 取第二个 key 覆盖第一个
.collect(Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2));
//最终返回的集合
List<CountryDTO> countryDTOList = new ArrayList<>();
//将子节点放入对应的父节点里
countryDTOS.stream()
//排除根节点
.filter(item -> !id.equals(item.getId()))
//遍历每个元素
.forEach(item -> {
//如果它的上一个节点为根节点加入集合
if(item.getParentid().equals(id)){
countryDTOList.add(item);
}
//获取上一个节点
CountryDTO countryDTO = collect.get(item.getParentid());
//如果上一个节点不为 null
if(countryDTO != null){
if(countryDTO.getCountryDTOS() == null){
countryDTO.setCountryDTOS(new ArrayList<CountryDTO>());
}
//将此节点添加到上一个节点中
countryDTO.getCountryDTOS().add(item);
}
});
return countryDTOList;
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class CountryController {
@Autowired
private CountryService countryService;

@GetMapping("/get")
public ResultData<List<CountryDTO>> get(){
return ResultData.success(countryService.get("0"));
}
}

结果

image-20240301154046529

网络图片上传/下载

前端

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> 我的 </title>
<style>
.container{
width: 50%;
height: 300px;
overflow: hidden;
}
.container img{
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<h2> 文件上传 </h2>
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput"/>
<br>
<button type="button" onclick="uploadFile()"> 上传文件 </button>
</form>
<h2> 上传图片预览 </h2>
<div class="container">
<img id="img" src="">
</div>
<h2> 文件下载 </h2>
<button type="button" onclick="downloadFile()"> 下载文件 </button>

<script src="js/jquery-3.6.1.min.js"></script>
<script>
var fileName;
function getName(data) {
return data;
}
// 文件上传函数
function uploadFile() {
// 获取文件输入框的 DOM 元素
var fileInput = $('#fileInput')[0];
console.log(fileInput)

// 获取选择的文件
var file = fileInput.files[0];
console.log(file)

// 创建 FormData 对象,用于将文件数据发送到服务器
var formData = new FormData();
formData.append('file', file);

// 使用 jQuery 的 AJAX 方法发送文件到后端
$.ajax({
// 后端接收文件的 URL
url: '/upload',
// 请求类型为 POST
type: 'POST',
// 发送的数据为 FormData 对象
data: formData,
// 不对数据进行处理,由 FormData 处理
processData: false,
// 不设置内容类型,让浏览器自动识别
contentType: false,
// 成功时的回调函数
success: function (data) {
console.log(' 文件上传成功 ');
fileName = getName(data.data);
document.getElementById("img").setAttribute("src", "images/" + data.data);
},
// 失败时的回调函数
error: function (error) {
console.error(' 文件上传失败:', error);
}
});
}

// 文件下载
function downloadFile() {
var url = document.getElementById("img").getAttribute("src");
$.ajax({
url: '/download',
type: 'GET',
data: {
fileUrl: "http://localhost/" + url,
fileName: fileName
},
xhrFields:{
responseType: 'arraybuffer'
},
success: function (data) {
console.log(fileName);
var blob = new Blob([data], { type: "application/octet-stream" });
var link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
error: function (error) {
console.log(error.msg);
}
})
}
</script>
</body>
</html>

后端

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
@RestController
public class ImageController {
// 文件上传目录
private static final String UPLOAD_DIR = "D:/newImg";

@PostMapping("/upload")
public ResultData<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
//获取上传文件的字节数组
byte[] bytes = file.getBytes();
//生成随机的图片命名
String uuId = UUID.randomUUID().toString();
//获取原图片命名
String originalFilename = file.getOriginalFilename();
//截取后缀名
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
//拼接新名称
String fileName = uuId + ext;
//构建文件路径
Path path = Paths.get(UPLOAD_DIR + "/" + fileName);
//将字节数组写入文件
Files.write(path,bytes);
return ResultData.success(fileName);
}

//使用 url 下载
@GetMapping("/download")
public ResponseEntity<byte[]> downloadFile(@RequestParam String fileUrl,@RequestParam String fileName) throws Exception {
//定义一个 URL 对象,就是你想下载的图片的 URL 地址
URL url = new URL(fileUrl);
//打开连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置请求方式为"GET"
connection.setRequestMethod("GET");
//超过响应时间为 10 秒
connection.setConnectTimeout(10 * 1000);
//通过输入流获取图片数据
InputStream is = connection.getInputStream();
//得到图片的二进制数据,以二进制封装得到数据,具有通用性
byte[] data = readInputStream(is);
return getResponseEntity(fileName, data);
}

//用上传到本地的图片下载
@GetMapping("/downloadImg")
public ResponseEntity<byte[]> downloadImg(@RequestParam String fileName) throws IOException {
//构建文件路径
Path path = Paths.get(UPLOAD_DIR + "/" + fileName);

//读取文件的字节数组
byte[] data = Files.readAllBytes(path);

return getResponseEntity(fileName,data);
}

//构建响应文件的各种信息
private static ResponseEntity<byte[]> getResponseEntity(String fileName, byte[] data) {
HttpHeaders headers = new HttpHeaders();
String ext = fileName.substring(fileName.lastIndexOf("."));
if(".jpg".equals(ext)) {
headers.setContentType(MediaType.IMAGE_JPEG);
}else if(".png".equals(ext)){
headers.setContentType(MediaType.IMAGE_PNG);
}
headers.setContentDispositionFormData("attachment", fileName);
return ResponseEntity.ok()
.headers(headers)
.body(data);
}

public static byte[] readInputStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
//创建一个 Buffer 字符串
byte[] buffer = new byte[6024];
//每次读取的字符串长度,如果为-1,代表全部读取完毕
int len;
//使用一个输入流从 buffer 里把数据读取出来
while ((len = inStream.read(buffer)) != -1) {
//用输出流往 buffer 里写入数据,中间参数代表从哪个位置开始读,len 代表读取的长度
outStream.write(buffer, 0, len);
}
//关闭输入流
inStream.close();
//把 outStream 里的数据写入内存
return outStream.toByteArray();
}
}