IO 流 一、什么是 IO 流
二、流的分类
按照流的方向
输入 (Input) 也就是 读 (Read)
输出 (Output) 也就是 写 (Write)
按照读取数据方式
字节:一次读取 1 个字节 byte,等同于一次读取 8 个二进制位,这种流什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件等…
字符:一次读取 1 个字符,这种流方便读取 普通文本文件 而存在的,这种流不能读取:图片、声音、视频等文件。只能读取 纯文本文件,连 word 文件都无法读取。
三、IO 流抽象类
字节流
java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
字符流
java.io.Reader 字符输入流
java.io.Writer 字符输出流
注意:
所有的流都实现了:java.io.Closeable 接口,都是可关闭 的,都有 close() 方法。用完一定要关闭
所有的输出流都实现了java.io.Flushable 接口,都是可刷新 的,都有 flush() 方法。输出流在最终输出之后,一定要记得 flush() 刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完 (清空管道!)刷新的作用就是清空管道 。
在 java 中只要“类名 ”以 Stream 结尾的都是字节流 。以“ Reader/Writer ”结尾的都是字符流 。
四、Java 要掌握的流 (16 个)
文件 专属:
转换流 :(将字节流转换成字符流)
缓冲流 专属:
数据流 专属:
java.io.DataInputStream
java.io.DataOutputStream
标准输出 流:
java.io.PrintWriter
java.io.PrintStream(掌握)
对象专属 流:
File 文件类
补充: Windows 各个文件的分隔符为:”\“,Linux 各个文件的分隔符为:”/“
五、类的方法 文件字节输入流,万能的,任何类型的文件都可以采用这个流来读
构造方法
构造方法名
备注
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 对象)
注意:
标准输出流不需要手动 close() 关闭
可以改变标准输出流的输出方向
序列化 参与序列化和反序列化的对象,必须实现**Serializable** 接口。
Serializable 接口起什么作用呢?
起到 标识 的作用,标志的作用,java 虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇。
Serializable 这个标志接口是给 java 虚拟机参考的,java 虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号。
序列化版本号有什么用 区分两个类是否相同
java 语言中是采用什么机制来区分类的
首先通过 类名 进行比对,如果类名不一样,肯定不是同一个类。
如果类名一样,再怎么进行类的区别?靠 序列化版本号 进行区分。
这种自动生成序列化版本号有什么缺陷 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:服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示:
虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO 可以一定程度解决这个问题。
NIO Java NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求 (连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。
一个线程中就可以调用多路复用接口(java 中是 select)阻塞同时监听来自多个客户端的 IO 请求,一旦有收到 IO 请求就调用对应函数处理,NIO 擅长 1 个线程管理多条连接,节约系统资源。
三、NIO 的核心实现 NIO 包含 3 个核心的组件:
Channel(通道)
Buffer(缓冲区)
Selector(选择器)
Channel(通道) Channel 是 NIO 的核心概念,它表示一个打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。
通道的主要实现类:
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); ByteBuffer buf = ByteBuffer.allocate(10 ); StringBuffer text = new StringBuffer (); while (fileChannel.read(buf) != -1 ){ buf.flip(); while (buf.position() < buf.limit()){ text.append((char )buf.get()); } buf.clear(); } 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(); } } channel.force(false ); fileChannel.close(); channel.close(); }
SocketChannel 类 网络套接字 IO 通道,TCP 协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。
TCP 客户端使用 SocketChannel 与服务端进行交互的流程为:
打开通道,连接到服务端
1 2 SocketChannel channel = SocketChannel.open(); channel.connect(new InetSocketAddress ("localhost" , 9090 ));
分配缓冲区
1 ByteBuffer buf = ByteBuffer.allocate(10 );
配置是否为阻塞方式。(默认为阻塞方式)
1 channel.configureBlocking(false );
与服务端进行数据交互
关闭连接
ServerSocketChannel 类 网络通信 IO 操作,TCP 协议,针对面向流的监听套接字的可选择通道(一般用于服务端),流程如下:
打开一个 ServerSocketChannel 通道, 绑定端口。
1 ServerSocketChannel server = ServerSocketChannel.open();
绑定端口
1 server.bind(new InetSocketAddress (9090 ));
阻塞等待连接到来,有新连接时会创建一个 SocketChannel 通道,服务端可以通过这个通道与连接过来的客户端进行通信。等待连接到来的代码一般放在一个循环结构中。
1 SocketChannel client = server.accept();
通过 SocketChannel 与客户端进行数据交互
关闭 SocketChannel
Buffer(缓冲区) 缓冲区 Buffer 是 Java NIO 中一个核心概念,在 NIO 库中,所有数据都是用缓冲区处理的。
在读取数据时,它是直接读到缓冲区中的,在写入数据时,它也是写入到缓冲区中的,任何时候访问 NIO 中的数据,都是将它放到缓冲区中。
而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。
Buffer 数据类型
从类图中可以看到,7 种数据类型对应着 7 种子类,这些名字是 Heap 开头子类,数据是存放在 JVM 堆中的。
MappedByteBuffer 而 MappedByteBuffer 则是存放在堆外的直接内存中,可以映射到文件。
通过 java.nio 包和 MappedByteBuffer 允许 Java 程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得 IO 操作非常快。
Mmap 内存映射和普通标准 IO 操作的本质区别在于它并不需要将文件中的数据先拷贝至 OS 的内核 IO 缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。
只有当缺页中断发生时,直接将文件从磁盘拷贝至用户态的进程空间内,只进行了一次数据拷贝,对于容量较大的文件来说(文件大小一般需要限制在 1.5~2G 以下),采用 Mmap 的方式其读/写的效率和性能都非常高,大家熟知的RocketMQ 就使用了该技术。
Buffer 数据流程 应用程序可以通过与 I/O 设备建立通道来实现对 I/O 设备的读写操作,操作的数据通过缓冲区 Buffer 来进行交互。
从 I/O 设备读取数据时:
应用程序调用通道 Channel 的 read() 方法
通道往缓冲区 Buffer 中填入 I/O 设备中的数据,填充完成之后返回
应用程序从缓冲区 Buffer 中获取数据
往 I/O 设置写数据时:
应用程序往缓冲区 Buffer 中填入要写到 I/O 设备中的数据
调用通道 Cannel 的 write() 方法,通道将数据传输至 I/O 设备
缓冲区核心方法 缓冲区存取数据的两个核心方法:
put():存入数据到缓冲区
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index,byte b):将指定字节写入缓冲区的索引位置 (不会移动 position)
get():获取缓冲区的数据
get():读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节 (不会移动 position)
Selector(选择器) Selector 类是 NIO 的核心类,Selector(选择器)选择器提供了选择已经就绪的任务的能力。
Selector 会不断的轮询注册在上面的所有 channel,如果某个 channel 为读写等事件做好准备,那么就处于就绪状态,通过 Selector 可以不断轮询发现出就绪的 channel,进行后续的 IO 操作。
一个 Selector 能够同时轮询多个 channel,这样,一个单独的线程就可以管理多个 channel,从而管理多个网络连接,这样就不用为每一个连接都创建一个线程,同时也避免了多线程之间上下文切换导致的开销。
选择器使用步骤
获取选择器
与通道和缓冲区的获取类似,选择器的获取也是通过静态工厂方法 open() 来得到的。
1 Selector selector = Selector.open();
获取可选择通道
能够被选择器监控的通道必须实现了 SelectableChannel 接口,并且需要将通道配置成非阻塞模式,否则后续的注册步骤会抛出 IllegalBlockingModeException。
1 2 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress ("localhost" , 9090 )); socketChannel.configureBlocking(false );
将通道注册到选择器
通道在被指定的选择器监控之前,应该先告诉选择器,并且告知监控的事件,即:将通道注册到选择器。
通道的注册通过 SelectableChannel.register(Selector selector, int ops) 来完成,ops 表示关注的事件,如果需要关注该通道的多个 I/O 事件,可以传入这些事件类型或运算之后的结果。这些事件必须是通道所支持的,否则抛出 IllegalArgumentException。
1 socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
轮询 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;如果有就绪事件,则将就绪事件放到一个集合,返回就绪事件的数量。
处理就绪事件
每次可以 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 对象可以得到:
就绪事件的对应的通道;
就绪的事件。通过这些信息,就可以很方便地进行 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.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(); } }
这三种创建线程的区别:
第二种创建线程的方式相比于第一种创建线程的优点
更适合多线程实行相同任务,可以减少代码量
避免了单继承的局限性
线程和任务分离,提高了程序健壮性
线程池接受 Runnable 类型任务,不接受 Thread 类型线程
第三种相比于前两种创建线程的区别:
使用 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 (() -> { synchronized (lockA) { System.out.println("线程 1 获得锁 A" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB) { System.out.println("线程 1 获得锁 B" ); } } }); t1.start(); Thread t2 = new Thread (() -> { synchronized (lockB) { System.out.println("线程 2 获得锁 B" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockA) { System.out.println("线程 2 获得了锁 A" ); } } }); t2.start(); } }
死锁产生的原因 形成死锁主要由以下 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(); } 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(); } System.out.println("线程 2 释放锁 B" ); } }, "线程 2" ); t2.start(); } }
Base64 一、何为 Base64 算法 Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2 的 6 次方等于 64,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 Base64 单元,即 3 个字节可由 4 个可打印字符来表示。它可用来作为电子邮件的传输编码。在 Base64 中的可打印字符包括字母A-Z、a-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 如何转换
把 3 个字节变成 4 个字节
每 76 哥字符加一个换行符
最后的结束符也要处理
例如: 转换前 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 () { Consumer<String> consumer = new Consumer <String>() { @Override public void accept (String s) { System.out.println(s); } }; consumer.accept("没有使用 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 () { 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("======================" ); 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 { public abstract void run () ; }
他主要有如下的特点:
含有@FunctionalInterface 注解
只有一个抽象方法
也就是说只有函数式接口的变量或者是函数式接口,才能够赋值为 Lambda 表达式。 当然了方法的类型可以任意。
2.函数式接口有什么用 函数式接口能够接受匿名内部类的实例化对象,换句话说,我们可以使用匿名内部类来实例化函数式接口的对象,而 Lambda 表达式能够代替内部类实现代码的进一步简化。并且 java 为我们提供了四个比较重要的函数式接口:
消费型接口:Consumer< T> void accept(T t) 有参数,无返回值的抽象方法;
供给型接口:Supplier < T> T get() 无参有返回值的抽象方法;
断定型接口: Predicate< T> boolean test(T t):有参,但是返回值类型是固定的 boolean
函数型接口: 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 的类型,它利用上下文被期待的类型 当做推导的目标类型,当满足下面条件时,就会被赋予目标类型:
被期待的目标类型是一个函数式接口
lambad 的入参类型和数量与该接口一致
返回类型一致
抛出异常类型一致
其实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 ()
根据 List 获取流
1 2 3 4 5 6 7 8 9 List<String> list = new ArrayList <>(); list.add("张老三" ); list.add("张小三" ); list.add("李四" ); list.add("赵五" ); list.add("张六" ); list.add("王八" ); Stream<String> stream1 = list.stream();
根据 Set 集合获取流
1 2 3 4 5 6 7 8 9 Set<String> set = new HashSet <>(); set.add("张老三" ); set.add("张小三" ); set.add("李四" ); set.add("赵五" ); set.add("张六" ); set.add("王八" ); Stream<String> stream2 = set.stream();
根据 Map 集合获取流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Map<Integer,String> map = new HashMap <>(); map.put(1 ,"张老三" ); map.put(2 ,"张小三" ); map.put(3 ,"李四" ); map.put(4 ,"赵五" ); map.put(5 ,"张六" ); map.put(6 ,"王八" ); Set<Integer> map1 = map.keySet(); Stream<Integer> stream3 = map1.stream(); Collection<String> map2 = map.values(); Stream<String> stream4 = map2.stream(); Set<Map.Entry<Integer, String>> map3 = map.entrySet(); Stream<Map.Entry<Integer, String>> stream5 = map3.stream();
根据数组获取流
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
收集到 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); }
收集到 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); }
收集到指定集合
例如:收集到 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); }
收集集合中的数据到数组中
转成 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 类型的数组,我们操作起来不是很方便
转成指定类型的数组——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()); } }
对流中的数据的操作
对流中的数据进行聚合计算 当我们使用 Stream 流处理数据后,可以像数据库聚合函数一样,对某个字段进行处理,比如,获取最大值,获取最小值,求总和,平均值,统计数量等。
获取最大值——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 ) ); 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); } }
获取最小值——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 ) ); 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); } }
求和——Collectors.summingDouble
1 2 Double aDouble = stream.collect(Collectors.summingDouble(Student::getScore));
求平均值——Collectors.averagingDouble
1 2 3 Double aDouble = stream.collect(Collectors.averagingDouble(Student::getScore));System.out.println(aDouble);
统计数量——Collection.counting
1 2 3 Long aLong = stream.collect(Collectors.counting());System.out.println(aLong);
对流中的数据进行分组 当我们使用 Stream 流处理数据后,可以根据某个属性将数据分组
简单的分组——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); }); } }
多级分组——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); }); }); } }
对流中的数据进行分区——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); }); } }
对流中的数据进行拼接——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 基本使用
Jedis 直连
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()); String token = jedis.get("token" );System.out.println("token = " + token); jedis.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 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 (); myRedisPool.execute(new CallJedis () { @Override public void call (Jedis jedis) { jedis.set("username" ,"huanji" ); jedis.hset("myhash" ,"f1" ,"v1" ); jedis.rpushx("mylist" ,"1" ,"2" ); jedis.sadd("myset" ,"a" ,"b" ,"a" ); 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: 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); template.setKeySerializer(new StringRedisSerializer ()); template.setValueSerializer(serializer); 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; public <T> void setCacheObject (final String key, final T value) { redisTemplate.opsForValue().set(key, value); } public <T> void setCacheObject (final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } public boolean expire (final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } public boolean expire (final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } public long getExpire (final String key) { return redisTemplate.getExpire(key); } public Boolean hasKey (String key) { return redisTemplate.hasKey(key); } public <T> T getCacheObject (final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } public boolean deleteObject (final String key) { return redisTemplate.delete(key); } public boolean deleteObject (final Collection collection) { return redisTemplate.delete(collection) > 0 ; } public <T> long setCacheList (final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } public <T> List<T> getCacheList (final String key) { return redisTemplate.opsForList().range(key, 0 , -1 ); } 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; } public <T> Set<T> getCacheSet (final String key) { return redisTemplate.opsForSet().members(key); } public <T> void setCacheMap (final String key, final Map<String, T> dataMap) { if (dataMap != null ) { redisTemplate.opsForHash().putAll(key, dataMap); } } public <T> Map<String, T> getCacheMap (final String key) { return redisTemplate.opsForHash().entries(key); } public <T> void setCacheMapValue (final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } public <T> T getCacheMapValue (final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } public <T> List<T> getMultiCacheMapValue (final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } public boolean deleteCacheMapValue (final String key, final String hKey) { return redisTemplate.opsForHash().delete(key, hKey) > 0 ; } 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 { }
控制层代码:
1 2 3 4 5 6 7 8 9 10 @Controller @Scope("prototype") public class AdminController { @Autowired private AdminService adminService; }
@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); } } } 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):切面的具体实现。就是要给目标对象织入的事情。 以目标方法为参照点,根据放置的地方不同,可分为:
切面(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 { private Map<String, Integer> count = new HashMap <>(); @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); System.out.println(signature.getDeclaringTypeName()); 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(); 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)
什么是 Bean?
在 Spring 中,Bean 是指由 Spring 容器管理的对象。这些对象通常是应用中的组件,例如服务、数据访问对象、实体等。
Spirng Bean 容器的作用:
Spring Bean 容器负责创建、管理和注入(或装配)应用中的 Bean。它提供了一个环境,使得开发者可以通过配置文件或者注解的方式定义和组织 Bean。
Bean 的生命周期
Bean 的生命周期包括实例化、初始化、使用和销毁四个阶段。
Spring 容器负责在适当的时机执行这些阶段的操作,例如通过构造函数实例化 Bean、调用初始化方法、提供 Bean 给其他组件使用,最后在应用关闭时销毁 Bean。
Bean 的作用域:
Spring 支持多种 Bean 的作用域,包括单例(Singleton)、原型(Prototype)、会话(Session)、请求(Request)等。
每种作用域定义了 Bean 实例的生命周期和访问范围。
Bean 的装配:
装配是指将不同的 Bean 组装在一起,形成应用的组件关系。
Spring 支持通过 XML 配置、注解和 JAVA 配置等方式进行 Bean 的装配
Bean 的依赖注入
依赖注入是 Spring 框架的一个关键特性,它通过将一个 Bean 的依赖关系通过构造函数、Setter 方法或者接口注入到另一个 Bean 中。这样可以降低组件之间的耦合度。
Bean 的自动装配:
Spring 支持自动装配,通过指定 @Autowired 注解或者使用 XML 配置,Spring 可以自动识别和满足 Bean 之间的依赖关系。
ApplicationContext 和 BeanFactory:
Spring 提供了两个核心的容器接口,即ApplicationContext和BeanFactory。ApplicationContext是BeanFactory的子接口,提供了更丰富的功能,例如事件传播、AOP 等。
配置元数据:
Bean 的配置信息通常使用配置元数据来定义,可以使用 XML 文件、Java 配置类或者注解。配置元数据包含了 Bean 的类型、依赖关系、作用域、初始化方法、销毁方法等信息。
总体而言,Spring Bean 容器为开发者提供了一种松散耦合的方式来组织和管理应用中的组件,使得应用更加灵活、可维护和可测试。
四、Spring 注解
声明 bean 的注解
注入 bean 的注解
@Autowired:由 Spring 提供
@Inject:由 JSR-330 提供
@Resource:由 JSR-250 提供
Java 配置类相关注解
@Configuration:声明当前类为配置类
@Bean:注解在方法上,声明当前方法的返回值为一个 bean,替代 xml 中的方式
@ComponentScan:用于对 Component 进行扫描
切面(AOP)相关注解
@Aspect:声明一个切面
@After:在方法执行之后执行(方法上)
@Before:在方法执行之前执行(方法上)
@Around:在方法执行之前与之后执行(方法上)
@PointCut:声明切点
@EnableAspectJAutoProxy:开启 Spring 对 AspectJ 代理的支持
@Bean 的属性支持
@Scope 设置类型包括:设置 Spring 容器如何新建 Bean 实例
Singleton:单例,一个 Spring 容器中只有一个 bean 实例,默认模式
Protetype:每次调用新建一个 bean
Request:web 项目中,给每个 http request 新建一个 bean
Session:web 项目中,给每个 http session 新建一个 bean
GlobalSession:给每一个 global http session 新建一个 Bean 实例
@Value 注解
注入普通字符、注入操作系统属性、注入表达式结果、注入其它 bean 属性、注入文件资源、注入网站资源、注入配置文件
环境切换
@Profile:指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件。
@Conditional:通过实现 Condition 接口,并重写 matches 方法,从而决定该 bean 是否被实例化。
异步相关
@EnableAsync:配置类中通过此注解开启对异步任务的支持
@Async:在实际执行的 bean 方法使用该注解来声明其是一个异步任务(方法上或类上所有的方法都将异步,需要@EnableAsync 开启异步任务)
定时任务相关
@EnableScheduling:在配置类上使用,开启计划任务的支持
@Scheduled:来申明这是一个任务,包括 cron,fixDelay,fixRate 等类型(方法上,需先开启计划任务的支持)
Enable***注解说明
@EnableAspectAutoProxy:开启对 AspectJ 自动代理的支持
@EnableAsync:开启异步方法的支持
@EnableScheduling:开启计划任务的支持
@EnableWebMvc:开启 web MVC 的配置支持
@EnableConfigurationProperties:开启对@ConfigurationProperties 注解配置 Bean 的支持
@EnableJpaRepositories:开启对 SpringData JPA Repository 的支持
@EnableTransactionManagement:开启注解式事务的支持
@EnableCaching:开启注解式的缓存支持
测试相关注解
@RunWith:运行器,Spring 中通常用于对 JUnit 的支持
@ContextConfiguration 用来加载配置文件,其中 classess 属性用来加载配置类
1 2 3 4 5 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath*:/*.xml"}) public class CDPlayerTest { }
五、SpringBoot 注解
@SpringBootApplication:SpringBoot的核心注解,主要目的是开启自动配置。它也是一个组合注解,主要组合了@Configuration,@EnableAutoConfiguration(核心)和@ComponentScan。可以通过@SpringBootApplication(exclude={想要关闭的自动配置的类名.class}) 来关闭特定的自动配置,其中@ComponentScan让spring Boot扫描到Configuration类并把它加入到程序上下文。
@EnableAutoConfiguration 此注释自动载入应用程序所需的所有Bean——这依赖于Spring Boot在类路径中的查找。该注解组合了@Import注解,@Import注解导入了EnableAutoCofigurationImportSelector类,它使用SpringFactoriesLoader.loaderFactoryNames方法来扫描具有META-INF/spring.factories文件的jar包。而spring.factories里声明了有哪些自动配置.
@Configuration: 等同于spring的XML配置文件;使用Java代码可以检查类型安全。
@ComponentScan
表示将该类自动发现扫描组件。个人理解相当于,如果扫描到有@Component、@Controller、@Service等这些注解的类,并注册为Bean,可以自动收集所有的Spring组件,包括@Configuration类。
SpringSecurity SpringBoot 项目部署 SpringSecurity 数据库表结构
user 表 用户
role 表 角色
user_role 表 用户角色关系
permission 表 系统权限
role_permission 表 角色权限关联
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 /> </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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <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 > <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 > <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: jdbc:mysql://127.0.0.1:3306/security_practice?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root 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)) { 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 { @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); } @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); } @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" ; public String generateToken (String username) { SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)); LocalDateTime tokenExpirationTime = LocalDateTime.now().plusMinutes(EXPIRE); return Jwts.builder() .signWith(signingKey, Jwts.SIG.HS512) .header().add("typ" , "JWT" ).and() .issuedAt(Timestamp.valueOf(LocalDateTime.now())) .subject(username) .expiration(Timestamp.valueOf(tokenExpirationTime)) .claims(Map.of("username" , username)) .compact(); } public Claims getClaimsByToken (String token) { SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)); return Jwts.parser() .verifyWith(signingKey) .build() .parseSignedClaims(token) .getPayload(); } public boolean isTokenExpired (Date expiration) { return expiration.before(new Date ()); } 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; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = request.getHeader(JwtUtil.HEADER); if (StrUtil.isBlankOrUndefined(token)) { chain.doFilter(request, response); return ; } Claims claims = jwtUtil.getClaimsByToken(token); if (claims == null ) { throw new BaseException (ResponseCodeEnum.BAD_REQUEST, "Token 异常" ); } if (jwtUtil.isTokenExpired(claims.getExpiration())) { throw new BaseException (ResponseCodeEnum.BAD_REQUEST, "Token 已过期" ); } String username = claims.getSubject(); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken (username, null , accountUserDetailsService.getUserAuthority(username)); authentication.setDetails(new WebAuthenticationDetailsSource ().buildDetails(request)); 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 { @Override public void handle (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8" ); httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); ResultData<String> resultDTO = ResultData.error(e.getMessage()); 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 { @Override public void commence (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8" ); httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); ResultData<String> resultDTO = ResultData.error("请先登录" ); 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; @Override public void onAuthenticationSuccess (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8" ); String token = jwtUtil.generateToken(authentication.getName()); httpServletResponse.setHeader(JwtUtil.HEADER, token); ResultData<String> resultDTO = ResultData.success("SuccessLogin" ); 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 { @Override public void onAuthenticationFailure (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8" ); ResultData<String> resultDTO = ResultData.error("用户名或密码错误" ); 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 { @Override public void onLogoutSuccess (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { if (authentication != null ) { new SecurityContextLogoutHandler ().logout(httpServletRequest, httpServletResponse, authentication); } httpServletResponse.setContentType("application/json;charset=UTF-8" ); httpServletResponse.setHeader(JwtUtil.HEADER, "" ); SecurityContextHolder.clearContext(); ResultData<String> resultDTO = ResultData.success("SuccessLogout" ); 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("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 (); } @Bean public AuthenticationProvider authenticationProvider () { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider (); authProvider.setUserDetailsService(accountUserDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean public AuthenticationManager authenticationManager (AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .formLogin(form -> form .loginPage("/login.html" ) .loginProcessingUrl("/login" ) .defaultSuccessUrl("/" ).permitAll() .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler)) .logout(logout -> logout.logoutSuccessHandler(jwtLogoutSuccessHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(URL_WHITELIST).permitAll() .anyRequest().authenticated()) .exceptionHandling(exception -> exception .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler)) .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 );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 => { const token = localStorage .getItem ('user' ) ? localStorage .getItem ('user' ): null ; if (token) { config.headers .Authorization = `${token} ` ; } return config; }); app.use (router); app.use (ElementPlus ).mount ('#app' ); 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' ;import { defineConfig } from 'vite' ;export default defineConfig ({ plugins : [Vue ()], server : { proxy : { '/sq' : { target : 'http://localhost:8080/sq' , changeOrigin : true , rewrite : (path ) => path.replace (/^\/sq/ , '' ), }, }, }, });
树形结构查询 数据库表结构
表数据
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); Map<String, CountryDTO> collect = countryDTOS.stream() .filter(item -> !id.equals(item.getId())) .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()); 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" )); } }
结果
网络图片上传/下载 前端 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 ( ) { var fileInput = $('#fileInput' )[0 ]; console .log (fileInput) var file = fileInput.files [0 ]; console .log (file) var formData = new FormData (); formData.append ('file' , file); $.ajax ({ url : '/upload' , type : 'POST' , data : 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); } @GetMapping("/download") public ResponseEntity<byte []> downloadFile(@RequestParam String fileUrl,@RequestParam String fileName) throws Exception { URL url = new URL (fileUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET" ); 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 (); byte [] buffer = new byte [6024 ]; int len; while ((len = inStream.read(buffer)) != -1 ) { outStream.write(buffer, 0 , len); } inStream.close(); return outStream.toByteArray(); } }