面试题完成
This commit is contained in:
parent
aa124cc7c3
commit
f7568c8b9c
|
@ -0,0 +1 @@
|
|||
# 2024年1月1日
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: 1月
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 我的
|
||||
- 记录
|
||||
- 日记
|
||||
- "2024"
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: 2月
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 我的
|
||||
- 记录
|
||||
- 日记
|
||||
- "2024"
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: 2024年目录
|
||||
index: true
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 我的
|
||||
- 记录
|
||||
- 日记
|
||||
- "2024"
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: 2025年目录
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 我的
|
||||
- 记录
|
||||
- 日记
|
||||
- "2025"
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: 日记目录
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 我的
|
||||
- 记录
|
||||
- 日记
|
||||
- "2025"
|
||||
---
|
||||
|
||||
## 我的生活日记
|
|
@ -0,0 +1,93 @@
|
|||
# Java 并发集合有哪些
|
||||
|
||||
Java 提供了多种并发集合类,用于在多线程环境下安全地操作数据。这些集合类位于 `java.util.concurrent` 包中,相比于传统的集合类(如 `ArrayList`、`HashMap`),它们通过锁、CAS(Compare-And-Swap)等机制实现了线程安全。以下是常见的 Java 并发集合及其特点:
|
||||
|
||||
### **1. 并发 List**
|
||||
+ `CopyOnWriteArrayList`
|
||||
- 特点:写操作时复制底层数组,读操作无锁,适合读多写少的场景。
|
||||
- 优点:读操作性能高,线程安全。
|
||||
- 缺点:写操作性能较差,因为每次写操作都会复制整个数组。
|
||||
- 适用场景:读多写少,例如事件监听器列表。
|
||||
|
||||
### **2. 并发 Set**
|
||||
+ `CopyOnWriteArraySet`
|
||||
- 特点:基于 `CopyOnWriteArrayList` 实现,适合读多写少的场景。
|
||||
- 优点:线程安全,读操作性能高。
|
||||
- 缺点:写操作性能较差。
|
||||
- 适用场景:读多写少,例如缓存中的唯一值集合。
|
||||
+ `ConcurrentSkipListSet`
|
||||
- 特点:基于跳表(Skip List)实现,元素有序。
|
||||
- 优点:线程安全,支持高效的范围查询。
|
||||
- 缺点:内存占用较高。
|
||||
- 适用场景:需要有序且线程安全的集合。
|
||||
|
||||
### **3. 并发 Map**
|
||||
+ `ConcurrentHashMap`
|
||||
- 特点:基于分段锁(JDK 7)或 CAS + synchronized(JDK 8 及以后)实现,支持高并发。
|
||||
- 优点:线程安全,性能优于 `Hashtable` 和 `Collections.synchronizedMap`。
|
||||
- 缺点:不支持强一致性(如迭代器弱一致性)。
|
||||
- 适用场景:高并发环境下的键值对存储。
|
||||
+ `ConcurrentSkipListMap`
|
||||
- 特点:基于跳表实现,键有序。
|
||||
- 优点:线程安全,支持高效的范围查询。
|
||||
- 缺点:内存占用较高。
|
||||
- 适用场景:需要有序且线程安全的键值对存储。
|
||||
|
||||
### **4. 并发 Queue**
|
||||
+ `ConcurrentLinkedQueue`
|
||||
- 特点:基于链表实现的无界非阻塞队列。
|
||||
- 优点:线程安全,高性能。
|
||||
- 缺点:无界队列,可能导致内存溢出。
|
||||
- 适用场景:高并发环境下的任务队列。
|
||||
+ `LinkedBlockingQueue`
|
||||
- 特点:基于链表实现的可选有界阻塞队列。
|
||||
- 优点:线程安全,支持阻塞操作。
|
||||
- 缺点:有界队列可能限制吞吐量。
|
||||
- 适用场景:生产者-消费者模型。
|
||||
+ `ArrayBlockingQueue`
|
||||
- 特点:基于数组实现的有界阻塞队列。
|
||||
- 优点:线程安全,支持阻塞操作。
|
||||
- 缺点:固定容量,可能限制吞吐量。
|
||||
- 适用场景:固定大小的任务队列。
|
||||
+ `PriorityBlockingQueue`
|
||||
- 特点:基于堆实现的无界优先级阻塞队列。
|
||||
- 优点:线程安全,支持优先级排序。
|
||||
- 缺点:无界队列,可能导致内存溢出。
|
||||
- 适用场景:需要优先级排序的任务队列。
|
||||
+ `SynchronousQueue`
|
||||
- 特点:不存储元素的阻塞队列,每个插入操作必须等待一个移除操作。
|
||||
- 优点:线程安全,适合直接传递任务。
|
||||
- 缺点:容量为零,可能限制吞吐量。
|
||||
- 适用场景:直接传递任务的场景。
|
||||
|
||||
### **5. 并发 Deque**
|
||||
+ `ConcurrentLinkedDeque`
|
||||
- 特点:基于链表实现的无界非阻塞双端队列。
|
||||
- 优点:线程安全,高性能。
|
||||
- 缺点:无界队列,可能导致内存溢出。
|
||||
- 适用场景:高并发环境下的双端队列。
|
||||
+ `LinkedBlockingDeque`
|
||||
- 特点:基于链表实现的可选有界阻塞双端队列。
|
||||
- 优点:线程安全,支持阻塞操作。
|
||||
- 缺点:有界队列可能限制吞吐量。
|
||||
- 适用场景:需要双端操作的阻塞队列。
|
||||
|
||||
### **6. 其他并发工具**
|
||||
+ `BlockingQueue`** 的实现类**
|
||||
- 如 `LinkedBlockingQueue`、`ArrayBlockingQueue`、`PriorityBlockingQueue` 等,用于实现生产者-消费者模型。
|
||||
+ `ConcurrentSkipListSet`** 和 **`ConcurrentSkipListMap`
|
||||
- 基于跳表实现的有序集合和映射表。
|
||||
+ `DelayQueue`
|
||||
- 特点:基于优先级队列实现的无界阻塞队列,元素只有在其延迟到期后才能被取出。
|
||||
- 适用场景:定时任务调度。
|
||||
|
||||
### **总结**
|
||||
Java 并发集合类提供了多种线程安全的数据结构,适用于不同的并发场景。以下是一些选择建议:
|
||||
|
||||
+ 如果需要高并发且无序的键值对存储,选择 `ConcurrentHashMap`。
|
||||
+ 如果需要有序的集合或映射表,选择 `ConcurrentSkipListSet` 或 `ConcurrentSkipListMap`。
|
||||
+ 如果需要阻塞队列,选择 `LinkedBlockingQueue` 或 `ArrayBlockingQueue`。
|
||||
+ 如果需要无锁的高性能队列,选择 `ConcurrentLinkedQueue`。
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# List实现有哪些
|
||||
|
||||
1. **ArrayList**:
|
||||
2. **LinkedList**:
|
||||
3. **Vector**:
|
||||
4. **Stack**:
|
||||
5. **CopyOnWriteArrayList**:
|
||||
6. **Arrays.asList()**:
|
||||
|
||||
### ArrayList和LinkedList区别?
|
||||
1. **内部实现**:
|
||||
- ArrayList基于动态数组实现,通过数组存储元素,支持随机访问。
|
||||
- LinkedList基于双向链表实现,每个节点包含对前一个和后一个节点的引用。
|
||||
2. **随机访问**:
|
||||
- ArrayList支持高效的随机访问,因为可以根据索引直接访问数组中的元素。
|
||||
- LinkedList的随机访问效率较低,需要从头或尾部开始遍历链表,直到找到目标位置。
|
||||
3. **空间占用**:
|
||||
- ArrayList在添加元素时,可能需要重新分配内部数组的大小,会导致空间浪费。
|
||||
- LinkedList每个节点都需要额外的空间存储前后节点的引用,可能会占用更多的内存。
|
||||
4. **迭代性能**:
|
||||
- ArrayList通过迭代器(Iterator)进行迭代操作效率较高。
|
||||
- LinkedList在迭代操作时,由于需要遍历链表中的每个节点,性能相对较低。
|
||||
+ **查找**: ArrayList更快(O(1) vs O(n))。
|
||||
+ **插入**: 如果是在末尾插入,ArrayList更快;如果在中间或开头插入,LinkedList更快(O(1) vs O(n))。
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Java面试相关
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 笔记
|
||||
- 面试
|
||||
- 记录
|
||||
- Java
|
||||
dir:
|
||||
order: 3
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,29 @@
|
|||
# 2025项目中内容
|
||||
|
||||
## 我项目内容
|
||||
**AGV看板**
|
||||
|
||||
使用的时大屏展示,本来的实现方式是`vm+vh`但是后面说这个效果不太好,当时做了两个版本一个是`flexible.js`后面就使用了这个库进行开发的,不过刚开始因为样式问题会在不同的端上显示有问题,比如大小屏显示问题。但是后面只要是设计好了就没事了,因为没有设计稿的问题他给的是截图有很多误差的地方。
|
||||
|
||||
也涉及了`echarts`,包括表格设计循环滚动,其中也包含使用`konvajs`绘制的地图页面,不包含交管算法只是调用别人的接口进行渲染。之后就是常见的图表内容了。
|
||||
|
||||
**部门权限**
|
||||
|
||||
使用的是RBAC,主要包含用户、角色、权限,其中权限表不仅仅是一张表在不同的项目中可能是菜单权限、部门权限等 ,但是这个项目没有那么复杂只是一张权限表。
|
||||
|
||||
这个项目的目的是为了当时操作工会误操作不改操作的部门设备才做的,大部分的接口是调用斯坦德的接口少部分实现是自己的,当时要求是只要做到部门权限就行了,但是我怕后面还会继续修改所以当时就做到角色,指定相应的角色才能使用对应的页面或者接口,我的项目中时精确到接口的,每个用户需要带着自己的token去访问请求接口。
|
||||
|
||||
根据用户id去查询角色,之后再去查找对应的权限,如果当前的用户没有这个角色就返回401,如果用户可以访问这个页面但是这个页面。
|
||||
|
||||
刚加载的时候需要或许整个数据,若这个权限没有就是一个空白页并且也会告知用户没有权限,我在后端开发制作的时候喜欢使用抛出异常方式来解决返回状态码的问题,比如正常情况下是返回200这个没有问题,如果有异常全部抛出异常之后进行捕获,我认为这个是最简单也是最安全的做法,之后使用Spring自带的全局异常拦截器进行拦截既可。
|
||||
|
||||
关于前端页面我是用别人的模板进行开发的,返回路由是他们定义好的,我只要返回当前用户,也就是根据用户id查找对应角色之后对对应的角色查找对应的绑定的路由菜单,查询完成后返回给前端进行整合,每个路由返回中(因为这个是前端定义好的后端只要照着做既可)需要带上角色,前端会进行逻辑判断。
|
||||
|
||||
**WMS相关**
|
||||
|
||||
系统业务是这样的,员工负责将货箱放在指定地点,机器人负责搬运到货架上
|
||||
|
||||
入库相关操作,同步入库单MES需要同步订单给WMS,当WMS 接受都爱入库请求之后会判断ESS是否在线,如果不合格会取消入库,让人工搬运,反之机器人搬运。
|
||||
|
||||
出库操作:出库流程为自动出库和手动出库两种,自动模式依赖ESS在线状态,MES同步整个数据,ESS不在线需要人工搬运。
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
title: 我的项目中内容
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 笔记
|
||||
- 面试
|
||||
- 记录
|
||||
- Java
|
||||
- Web
|
||||
- 全栈
|
||||
- 我的
|
||||
dir:
|
||||
order: 1
|
||||
expanded: true
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: 面试问题目录
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 笔记
|
||||
- 面试
|
||||
- 记录
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,241 @@
|
|||
# 2025-02 面试题
|
||||
|
||||
## 宁波面试
|
||||
|
||||
### 什么是面向对象
|
||||
|
||||
> 面向对象(OOP)是一种程序设计范式,主要有:封装、继承、多态、抽象
|
||||
|
||||
### 什么是AOP
|
||||
|
||||
> AOP是一种 面向切面变成,将系统横切,比如日志内容、事务管理、安全控制,改进代码,通常配合Spring框架进行使用 。
|
||||
|
||||
### 什么是接口
|
||||
|
||||
> 方法声明、多重实现、常量、默认方法,接口提高了程序的灵活性和扩展性
|
||||
|
||||
### 说下ArrayList
|
||||
|
||||
> 动态数组,可以动态扩展,存储元素允许存储重复的元素,查询速度快,插入和删除性能较差,常见方法:`add`、`remove`、`clear`、`size`。
|
||||
|
||||
### List有哪些实现方法?
|
||||
|
||||
> 1. **ArrayList**:
|
||||
> 2. **LinkedList**:
|
||||
> 3. **Vector**:
|
||||
> 4. **Stack**:
|
||||
> 5. **CopyOnWriteArrayList**:
|
||||
> 6. **Arrays.asList()**:
|
||||
|
||||
### ArrayList和LinkedList区别?
|
||||
|
||||
> 1. **内部实现**:
|
||||
> - ArrayList基于动态数组实现,通过数组存储元素,支持随机访问。
|
||||
> - LinkedList基于双向链表实现,每个节点包含对前一个和后一个节点的引用。
|
||||
> 2. **随机访问**:
|
||||
> - ArrayList支持高效的随机访问,因为可以根据索引直接访问数组中的元素。
|
||||
> - LinkedList的随机访问效率较低,需要从头或尾部开始遍历链表,直到找到目标位置。
|
||||
> 3. **空间占用**:
|
||||
> - ArrayList在添加元素时,可能需要重新分配内部数组的大小,会导致空间浪费。
|
||||
> - LinkedList每个节点都需要额外的空间存储前后节点的引用,可能会占用更多的内存。
|
||||
> 4. **迭代性能**:
|
||||
> - ArrayList通过迭代器(Iterator)进行迭代操作效率较高。
|
||||
> - LinkedList在迭代操作时,由于需要遍历链表中的每个节点,性能相对较低。
|
||||
>
|
||||
> + **查找**: ArrayList更快(O(1) vs O(n))。
|
||||
> + **插入**: 如果是在末尾插入,ArrayList更快;如果在中间或开头插入,LinkedList更快(O(1) vs O(n))。
|
||||
|
||||
### 如果有一个数组要去重怎么做
|
||||
|
||||
> 使用`Set`和`Stream`是最简洁和高效的方式,但如果对空间复杂度有严格要求,手动去重或排序加双指针的方法可能更合适。
|
||||
|
||||
### 对数组去重要做统计呢?
|
||||
|
||||
> 使用Java流做成Map集合,Stream流中GroupingBy利用分组,手动去重
|
||||
|
||||
### 说下String相关的API
|
||||
|
||||
> + **字面量方式**:`String str = "Hello";`
|
||||
> + **使用**`new`**关键字**:`String str = new String("Hello");`
|
||||
> + `length()`:返回字符串的长度(字符数)。
|
||||
> + `equals(Object obj)`:比较两个字符串的内容是否相同。
|
||||
> + `equalsIgnoreCase(String anotherString)`:忽略大小写的比较。
|
||||
> + `compareTo(String anotherString)`:按字典顺序比较两个字符串,返回一个整数。
|
||||
> + `compareToIgnoreCase(String str)`:忽略大小写的字典顺序比较。
|
||||
> + `indexOf(String str)`:返回指定子字符串第一次出现的位置。
|
||||
> + `lastIndexOf(String str)`:返回指定子字符串最后一次出现的位置。
|
||||
> + `contains(CharSequence sequence)`:判断字符串是否包含指定的字符序列。
|
||||
> + `charAt(int index)`:返回指定位置的字符。
|
||||
> + `substring(int beginIndex)`:返回从指定索引到字符串末尾的子字符串。
|
||||
> + `substring(int beginIndex, int endIndex)`:返回从指定开始索引到结束索引之间的子字符串。
|
||||
> + `toUpperCase()`:将字符串转换为大写。
|
||||
> + `toLowerCase()`:将字符串转换为小写。
|
||||
> + `trim()`:去除字符串首尾的空格。
|
||||
> + `replace(char oldChar, char newChar)`:替换字符串中的字符。
|
||||
> + `replace(String target, String replacement)`:替换字符串中的子字符串。
|
||||
> + `replaceAll(String regex, String replacement)`:根据正则表达式替换字符串中的子字符串。
|
||||
> + `split(String regex)`:根据指定的正则表达式分割字符串,返回字符串数组。
|
||||
> + `split(String regex, int limit)`:根据指定的正则表达式分割字符串,返回字符串数组,指定数组的最大长度。
|
||||
> + `concat(String str)`:将指定字符串连接到当前字符串的末尾。
|
||||
> + `String.join(CharSequence delimiter, CharSequence... elements)`:使用指定的分隔符连接多个字符串。
|
||||
> + `toCharArray()`:将字符串转换为字符数组。
|
||||
> + `valueOf(...)`:将其他类型(如整数、布尔值等)转换为字符串。
|
||||
> + `isEmpty()`:判断字符串是否为空(长度为0)。
|
||||
> + `startsWith(String prefix)`:判断字符串是否以指定的前缀开始。
|
||||
> + `endsWith(String suffix)`:判断字符串是否以指定的后缀结束。
|
||||
> + `format(String format, Object... args)`:根据指定格式创建格式化字符串。
|
||||
|
||||
### String对性能有要求建议不使用字符串拼接,那么用
|
||||
|
||||
> + 使用`StringBuffer`时要考虑线程安全性,但会牺牲一些性能。
|
||||
> + 使用`StringBuilder`时要确保在单线程环境中使用,以获得更好的性能。
|
||||
> + 在大多数情况下,如果不需要线程安全,推荐使用`StringBuilder`。
|
||||
|
||||
### Java常见的异常类
|
||||
|
||||
> + `IOException`:输入输出异常,通常在文件操作或网络通信时发生。
|
||||
> + `SQLException`:与数据库操作相关的异常。
|
||||
> + `ClassNotFoundException`:当应用程序试图加载一个类但找不到时抛出。
|
||||
> + `FileNotFoundException`:当试图访问一个不存在的文件时抛出。
|
||||
> + `ParseException`:在解析字符串时发生错误,通常与日期和数字格式化相关。
|
||||
> + `InterruptedException`:当一个线程在等待、睡眠或其他阻塞状态被中断时抛出。
|
||||
> + `NullPointerException`:当应用程序试图使用`null`对象时抛出,常见于调用方法或访问属性。
|
||||
> + `ArrayIndexOutOfBoundsException`:当访问数组时索引超出范围时抛出。
|
||||
> + `ClassCastException`:当试图将对象强制转换为不兼容的类时抛出。
|
||||
> + `ArithmeticException`:在算术运算中发生错误,例如除以零。
|
||||
> + `IllegalArgumentException`:当方法接收到不合法或不适当的参数时抛出。
|
||||
> + `IllegalStateException`:当方法在不适当的状态下被调用时抛出。
|
||||
> + `NumberFormatException`:当试图将字符串转换为数字类型时格式不正确。
|
||||
> + `OutOfMemoryError`:当Java虚拟机无法分配足够的内存时抛出。
|
||||
> + `StackOverflowError`:当线程的栈深度超过了JVM的限制时抛出,通常是由于递归调用导致的。
|
||||
> + `NoClassDefFoundError`:当Java虚拟机或类加载器试图加载一个类,但找不到它的定义时抛出。
|
||||
|
||||
### 如果一个接口请求时间长你会怎么做?从数据库层面分析
|
||||
|
||||
> ###### **分析慢查询**
|
||||
>
|
||||
> + **使用慢查询日志**:启用数据库的慢查询日志,记录执行时间超过阈值的查询。分析这些查询,识别哪些查询耗时较长。
|
||||
> + **执行计划分析**:使用`EXPLAIN`语句(对于MySQL等数据库)分析查询的执行计划,查看查询的执行路径、使用的索引、连接方式等,识别潜在的性能瓶颈。
|
||||
>
|
||||
> ###### **优化查询**
|
||||
>
|
||||
> + **索引优化**:
|
||||
> - 确保常用的查询条件字段上有适当的索引。索引可以显著提高查询性能。
|
||||
> - 避免在索引字段上使用函数或计算,这会导致索引失效。
|
||||
> + **避免全表扫描**:尽量避免不必要的全表扫描,确保查询条件能够利用索引。
|
||||
> + **简化查询**:避免复杂的联接和子查询,尽量使用简单的查询语句。考虑将复杂查询拆分为多个简单查询。
|
||||
> + **选择合适的数据类型**:使用合适的数据类型可以减少存储和处理的开销。
|
||||
>
|
||||
> ###### **数据库设计优化**
|
||||
>
|
||||
> + **数据规范化与反规范化**:
|
||||
> - 确保数据库设计符合规范化原则,以减少数据冗余。
|
||||
> - 在某些情况下,适当的反规范化可以减少联接的复杂性,提高查询速度。
|
||||
> + **分区表**:对于大表,可以考虑使用分区表,将数据分散到多个物理分区,提高查询性能。
|
||||
>
|
||||
> ###### 4. **增加缓存**
|
||||
>
|
||||
> + **使用缓存**:在应用层使用缓存(如Redis、Memcached)来存储频繁访问的数据,减少对数据库的直接访问。
|
||||
> + **数据库查询缓存**:某些数据库(如MySQL)支持查询缓存,可以缓存查询结果。
|
||||
>
|
||||
> ###### 5. **连接池优化**
|
||||
>
|
||||
> + **使用连接池**:确保使用数据库连接池(如HikariCP、C3P0等),以减少连接的创建和关闭开销。
|
||||
> + **调整连接池配置**:根据负载情况调整连接池的大小和其他参数,确保数据库连接的高效利用。
|
||||
>
|
||||
> ###### 6. **监控和调优**
|
||||
>
|
||||
> + **监控数据库性能**:使用数据库监控工具(如Prometheus、Grafana)监控数据库的性能指标,如查询响应时间、连接数、CPU和内存使用率等。
|
||||
> + **定期维护**:定期进行数据库维护,如更新统计信息、重建索引、清理过时的数据等。
|
||||
>
|
||||
> ###### 7. **考虑异步处理**
|
||||
>
|
||||
> + **异步请求**:对于一些不需要立即返回结果的请求,可以考虑将其转为异步处理,用户可以在后台继续操作,而不是等待接口响应。
|
||||
> + **消息队列**:使用消息队列(如RabbitMQ、Kafka)将长时间运行的任务异步化,提升接口响应速度。
|
||||
|
||||
## 无锡面试
|
||||
|
||||
### mybatis中#和$区别
|
||||
|
||||
| 特性 | `#` | `$` |
|
||||
| ------------ | ----------------------------------------- | --------------------------------------------------- |
|
||||
| SQL 注入防范 | 会对参数值进行预编译和转义,防止 SQL 注入 | 不会进行任何转义,直接拼接参数值,容易发生 SQL 注入 |
|
||||
| 使用场景 | 用于所有需要传递参数的地方(推荐使用) | 用于动态拼接 SQL(如动态表名、列名等) |
|
||||
| 性能 | 使用预编译 SQL,性能较好 | 直接拼接,可能导致性能问题,且不安全 |
|
||||
|
||||
### SpringCloud和SpringBoot区别
|
||||
|
||||
> 在微服务架构中,**Spring Boot** 用来创建各个微服务,而 **Spring Cloud** 用来处理这些微服务之间的协作和通信。两者是可以互补的,Spring Boot提供基础构建,而Spring Cloud负责微服务架构中的复杂问题。
|
||||
>
|
||||
> **MySQL中递归**
|
||||
|
||||
```java
|
||||
WITH RECURSIVE org_chart AS (
|
||||
-- 基础查询:选取所有顶层管理者(没有上级的员工)
|
||||
SELECT id, name, manager_id, 1 AS level
|
||||
FROM employees
|
||||
WHERE manager_id IS NULL
|
||||
UNION ALL
|
||||
-- 递归查询:选取下一级的员工,并将层级+1
|
||||
SELECT e.id, e.name, e.manager_id, oc.level + 1
|
||||
FROM employees e
|
||||
JOIN org_chart oc ON e.manager_id = oc.id
|
||||
)
|
||||
-- 最终查询:输出所有员工及其层级
|
||||
SELECT id, name, manager_id, level
|
||||
FROM org_chart
|
||||
ORDER BY level, id;
|
||||
```
|
||||
|
||||
## 苏州面试
|
||||
|
||||
### 粘包和半包
|
||||
|
||||
- **粘包**:接收方一次性收到多个数据包(TCP 流式传输无边界导致)。
|
||||
- **半包**:一个数据包被拆分成多次接收。
|
||||
- **原因**:TCP 传输基于字节流,无消息边界概念;发送方写入速度与接收方读取速度不匹配。
|
||||
|
||||
---
|
||||
|
||||
### Netty 解决粘包/半包
|
||||
|
||||
- **解码器**:
|
||||
- **定长解码器**:`FixedLengthFrameDecoder`。
|
||||
- **分隔符解码器**:`DelimiterBasedFrameDecoder`。
|
||||
- **长度字段解码器**:`LengthFieldBasedFrameDecoder`(最灵活)。
|
||||
- **自定义协议**:在数据包头部添加长度字段,接收方根据长度解析。
|
||||
|
||||
---
|
||||
|
||||
### MyBatis 防止 SQL 注入
|
||||
|
||||
- **使用 `#{}` 占位符**:通过 `PreparedStatement` 预编译,参数化查询。
|
||||
- **避免 `${}` 拼接**:`${}` 直接替换为字符串,有注入风险。
|
||||
- **过滤输入**:对用户输入进行校验或转义(如 MyBatis 的 `like` 查询需手动处理)。
|
||||
|
||||
---
|
||||
|
||||
### MyBatis 底层处理 SQL 注入
|
||||
|
||||
- **预编译机制**:`#{}` 会被替换为 `?`,由数据库驱动进行参数填充,输入内容不会作为 SQL 语法解析。
|
||||
- **源码逻辑**:`SqlSourceBuilder` 解析 SQL,替换 `#{}` 为占位符,生成 `BoundSql`。
|
||||
|
||||
---
|
||||
|
||||
### Spring 依赖注入方式
|
||||
|
||||
- **构造器注入**:通过构造函数传入依赖(推荐,保证不可变性)。
|
||||
- **Setter 注入**:通过 Setter 方法注入。
|
||||
- **字段注入**:直接通过 `@Autowired` 注解字段(不推荐,难测试)。
|
||||
- **接口注入**:实现特定接口(已较少使用)。
|
||||
|
||||
---
|
||||
|
||||
### @Autowired 黄色波浪线问题
|
||||
|
||||
- **原因**:存在多个同类型 Bean,Spring 无法确定注入哪一个。
|
||||
- **解决**:
|
||||
- **@Qualifier**:指定 Bean 名称。
|
||||
- **@Primary**:标记首选 Bean。
|
||||
- **明确类型**:使用具体实现类而非接口。
|
||||
- **Lombok**:使用 `@RequiredArgsConstructor` 构造器注入。
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
title: Spring面试相关
|
||||
index: false
|
||||
date: 2025-2-19 12:46:50
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 笔记
|
||||
- 面试
|
||||
- 记录
|
||||
- Java
|
||||
- Web
|
||||
- 全栈
|
||||
- 我的
|
||||
dir:
|
||||
order: 4
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
date: 2025-2-19 12:49:58
|
||||
---
|
||||
|
||||
# Spring Bean 生命周期
|
||||
|
||||
Spring Bean 的生命周期是指从 Bean 的创建到销毁的整个过程。Spring 容器负责管理 Bean 的生命周期,开发者可以通过配置或实现特定接口来干预 Bean 的生命周期。以下是 Spring Bean 生命周期的详细过程:
|
||||
|
||||
### 1. **Bean 的实例化(Instantiation)**
|
||||
+ Spring 容器根据配置(如 XML、注解或 Java 配置)创建 Bean 的实例。
|
||||
+ 实例化方式:
|
||||
- 通过构造函数创建。
|
||||
- 通过工厂方法创建。
|
||||
|
||||
### 2. **属性赋值(Populate Properties)**
|
||||
+ Spring 容器根据配置为 Bean 的属性注入值(依赖注入)。
|
||||
+ 注入方式:
|
||||
- 通过 `@Autowired`、`@Resource` 或 `@Value` 注解。
|
||||
- 通过 XML 配置中的 `<property>` 标签。
|
||||
|
||||
### 3. **BeanNameAware 和 BeanFactoryAware 接口的回调**
|
||||
+ 如果 Bean 实现了 `BeanNameAware` 接口,Spring 会调用 `setBeanName()` 方法,传入 Bean 的名称。
|
||||
+ 如果 Bean 实现了 `BeanFactoryAware` 接口,Spring 会调用 `setBeanFactory()` 方法,传入 BeanFactory 实例。
|
||||
|
||||
### 4. **BeanPostProcessor 的前置处理**
|
||||
+ 如果容器中注册了 `BeanPostProcessor`,Spring 会调用其 `postProcessBeforeInitialization()` 方法,对 Bean 进行前置处理。
|
||||
|
||||
### 5. **InitializingBean 和 init-method 的调用**
|
||||
+ 如果 Bean 实现了 `InitializingBean` 接口,Spring 会调用其 `afterPropertiesSet()` 方法。
|
||||
+ 如果 Bean 配置了 `init-method`(通过 XML 或 `@Bean(initMethod = "init")`),Spring 会调用指定的初始化方法。
|
||||
|
||||
### 6. **BeanPostProcessor 的后置处理**
|
||||
+ 如果容器中注册了 `BeanPostProcessor`,Spring 会调用其 `postProcessAfterInitialization()` 方法,对 Bean 进行后置处理。
|
||||
|
||||
### 7. **Bean 的使用**
|
||||
+ 此时 Bean 已经初始化完成,可以被应用程序使用。
|
||||
|
||||
### 8. **DisposableBean 和 destroy-method 的调用**
|
||||
+ 当容器关闭时,如果 Bean 实现了 `DisposableBean` 接口,Spring 会调用其 `destroy()` 方法。
|
||||
+ 如果 Bean 配置了 `destroy-method`(通过 XML 或 `@Bean(destroyMethod = "destroy")`),Spring 会调用指定的销毁方法。
|
||||
|
||||
#### `BeanPostProcessor` 和 `BeanFactoryPostProcessor` 的区别是什么?
|
||||
|
||||
- `BeanPostProcessor` 作用于 Bean 的初始化前后,而 `BeanFactoryPostProcessor` 作用于 BeanFactory 的初始化阶段,用于修改 Bean 的定义。
|
||||
|
||||
#### `@PostConstruct` 和 `init-method` 的执行顺序是什么?
|
||||
|
||||
- `@PostConstruct` 注解的方法会先执行,然后是 `InitializingBean` 的 `afterPropertiesSet()` 方法,最后是 `init-method`。
|
||||
|
||||
#### 如何自定义 Bean 的生命周期?
|
||||
|
||||
- 可以通过实现 `InitializingBean`、`DisposableBean` 接口,或者配置 `init-method` 和 `destroy-method`,也可以使用 `BeanPostProcessor` 进行扩展。
|
||||
|
||||
### 总结:Bean 生命周期的关键步骤
|
||||
|
||||
1. 实例化 Bean。
|
||||
2. 属性赋值(依赖注入)。
|
||||
3. 调用 `BeanNameAware` 和 `BeanFactoryAware` 接口的方法。
|
||||
4. 调用 `BeanPostProcessor` 的 `postProcessBeforeInitialization()` 方法。
|
||||
5. 调用 `InitializingBean` 的 `afterPropertiesSet()` 方法和自定义的 `init-method`。
|
||||
6. 调用 `BeanPostProcessor` 的 `postProcessAfterInitialization()` 方法。
|
||||
7. Bean 准备就绪,可以被使用。
|
||||
8. 容器关闭时,调用 `DisposableBean` 的 `destroy()` 方法和自定义的 `destroy-method`。
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# 2025-02 面试题
|
||||
|
||||
## 宁波面试
|
||||
|
||||
### 常用的CSS单位
|
||||
|
||||
> em、rem、px、%、vh、vw、vmin、vmax
|
||||
|
||||
## 无锡面试
|
||||
|
||||
### vue2和vue3区别
|
||||
|
||||
> **Vue 2**:
|
||||
>
|
||||
> + Vue 2 主要使用 Options API(选项式 API),通过定义 `data`、`methods`、`computed`、`watch` 等来构建组件。
|
||||
> + vue2组件必须有一个根节点
|
||||
>
|
||||
> **Vue 3**:
|
||||
>
|
||||
> + Vue 3 引入了 Composition API,允许开发者通过 `setup()` 函数来组织和管理逻辑。Composition API 提供了更好的代码重用性和可维护性,尤其是在大型应用中。
|
||||
> + vue3支持Fragment
|
||||
> + Vue 3 在 Composition API 中引入了 `onMounted`、`onUpdated`、`onBeforeUnmount`
|
||||
> + **Vue 3**:Vue 3 引入了 `Suspense` 组件,提供更好的异步组件处理机制。此外,`Teleport` 组件可以让子组件的 DOM 渲染到另一个 DOM 节点,而不是父组件的 DOM 树中,这对于像模态框这样的场景非常有用。
|
||||
|
||||
### vue中生命周期
|
||||
|
||||
| 生命周期钩子 | Vue 2 | Vue 3 (Options API) | Vue 3 (Composition API) |
|
||||
| ------------ | --------------- | ------------------- | ----------------------- |
|
||||
| 实例创建前 | `beforeCreate` | `beforeCreate` | `beforeCreate` |
|
||||
| 实例创建后 | `created` | `created` | `created` |
|
||||
| 挂载前 | `beforeMount` | `beforeMount` | `onBeforeMount` |
|
||||
| 挂载后 | `mounted` | `mounted` | `onMounted` |
|
||||
| 更新前 | `beforeUpdate` | `beforeUpdate` | `onBeforeUpdate` |
|
||||
| 更新后 | `updated` | `updated` | `onUpdated` |
|
||||
| 销毁前 | `beforeDestroy` | `beforeUnmount` | `onBeforeUnmount` |
|
||||
| 销毁后 | `destroyed` | `unmounted` | `onUnmounted` |
|
||||
| 激活时 | — | — | `onActivated` |
|
||||
| 停用时 | — | — | `onDeactivated` |
|
||||
|
||||
### ref和reactive区别
|
||||
|
||||
> `ref` 用来创建一个单一的响应式数据,通常用于基本类型(如 `string`、`number`、`boolean`)或需要变动的引用类型(例如 `DOM` 元素或组件实例)。修改 `ref` 的值时需要使用 `.value`。
|
||||
>
|
||||
> `reactive` 用来创建一个深度响应式对象。它会将对象的所有属性变为响应式,适合用于处理复杂的数据结构(如对象、数组等)。不需要 `.value` 来访问和修改数据。
|
||||
|
||||
> `ref` 是用于处理基本数据类型或需要封装为引用类型的响应式数据。
|
||||
>
|
||||
> `reactive` 是用于处理复杂对象、数组等,并且它会递归地将对象中的每个属性变成响应式。
|
||||
|
||||
## 苏州面试
|
||||
|
||||
### Vue2 和 Vue3 区别
|
||||
|
||||
- **响应式原理**:Vue2 使用 `Object.defineProperty` 劫持对象属性,Vue3 改用 `Proxy` 代理对象,支持监听动态新增/删除属性及数组索引变化。
|
||||
- **组合式 API**:Vue3 引入 `setup` 函数和组合式 API(如 `ref`、`reactive`),替代 Vue2 的选项式 API。
|
||||
- **性能优化**:Vue3 支持 Tree-shaking,减少打包体积;虚拟 DOM 优化(静态节点提升、补丁标记)。
|
||||
- **生命周期**:Vue3 将 `beforeCreate` 和 `created` 合并到 `setup` 中,其他钩子名前加 `on`(如 `onMounted`)。
|
||||
- **TypeScript 支持**:Vue3 源码用 TypeScript 重写,提供更好的类型推断。
|
||||
|
||||
---
|
||||
|
||||
### Vue3 双向数据绑定的实现
|
||||
- **原理**:通过 `v-model` 语法糖实现,底层基于响应式系统(`Proxy`)和事件监听。
|
||||
- **示例**:`<input v-model="value">` 等价于:
|
||||
```html
|
||||
<input :value="value" @input="value = $event.target.value">
|
||||
```
|
||||
- **Proxy 的作用**:拦截对象的读写操作,在 `get` 中收集依赖,在 `set` 中触发更新。
|
||||
|
||||
---
|
||||
|
||||
### 响应式数据的实现
|
||||
- **Vue3**:使用 `Proxy` 代理对象,支持深层次监听,无需递归遍历初始化。
|
||||
```javascript
|
||||
const proxy = new Proxy(obj, {
|
||||
get(target, key) { /* 收集依赖 */ },
|
||||
set(target, key, value) { /* 触发更新 */ }
|
||||
});
|
||||
```
|
||||
- **Vue2**:通过 `Object.defineProperty` 劫持对象属性,需递归初始化,无法监听新增/删除属性。
|
||||
|
||||
### Vue Proxy 底层实现
|
||||
- **Proxy 代理**:对数据对象包装,拦截 `get`、`set`、`deleteProperty` 等操作。
|
||||
- **依赖收集**:在 `get` 中通过 `track` 收集当前依赖(副作用函数)。
|
||||
- **触发更新**:在 `set` 中通过 `trigger` 通知依赖更新,触发组件重新渲染。
|
||||
|
||||
---
|
||||
|
||||
### Vue 生命周期钩子
|
||||
- **Vue2**:`beforeCreate`, `created`, `beforeMount`, `mounted`, `beforeUpdate`, `updated`, `beforeDestroy`, `destroyed`。
|
||||
- **Vue3**:`setup` 替代 `beforeCreate` 和 `created`;其他钩子加 `on` 前缀(如 `onMounted`)。
|
||||
|
||||
---
|
||||
|
||||
### Pinia 响应式与持久化
|
||||
|
||||
- **响应式**:基于 Vue3 的 `reactive` 和 `ref` 管理状态,自动跟踪变化。
|
||||
- **刷新数据保留**:默认不持久化,需配合插件(如 `vuex-persistedstate`)或手动使用 `localStorage`。
|
||||
|
||||
---
|
||||
|
||||
### Vue Diff 算法
|
||||
- **双端对比**:新旧子节点数组的头尾指针同时比对,减少移动次数。
|
||||
- **Key 的作用**:通过唯一 `key` 识别节点,复用相同元素。
|
||||
- **优化策略**:
|
||||
- 同类型节点比较属性变化。
|
||||
- 静态节点跳过比对。
|
||||
- 最长递增子序列优化移动逻辑。
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
title: 前端面试相关
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 笔记
|
||||
- 面试
|
||||
- 记录
|
||||
- Java
|
||||
- Web
|
||||
- 全栈
|
||||
- 我的
|
||||
dir:
|
||||
order: 2
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,125 @@
|
|||
### 一、上位机/WPF/WinForm薪资现状分析(2025年数据)
|
||||
#### 1. **薪资分布情况**
|
||||
```plain
|
||||
| 技术方向 | 一线城市 | 新一线城市 | 二线城市 |
|
||||
|----------------|----------------|----------------|----------------|
|
||||
| 上位机开发 | 8-12K | 7-10K | 6-8K |
|
||||
| WPF开发 | 9-13K | 8-11K | 7-9K |
|
||||
| WinForm开发 | 7-10K | 6-9K | 5-7K |
|
||||
```
|
||||
|
||||
**关键发现:**
|
||||
|
||||
+ WPF薪资高于WinForm约2-3K
|
||||
+ 上位机开发薪资与Java后端持平
|
||||
+ 具备工业协议(如OPC UA)经验可溢价15-20%
|
||||
|
||||
#### 2. **薪资影响因素**
|
||||

|
||||
|
||||
### 二、技术栈选择建议
|
||||
#### 1. **技术栈对比分析**
|
||||
| 技术方向 | 优势 | 劣势 | 适用场景 |
|
||||
| --- | --- | --- | --- |
|
||||
| 上位机 | 工业领域刚需 | 技术更新慢 | 制造业/自动化 |
|
||||
| WPF | 界面表现力强 | 学习曲线陡峭 | 金融/医疗可视化 |
|
||||
| WinForm | 开发效率高 | 技术陈旧 | 传统行业内部系统 |
|
||||
|
||||
|
||||
#### 2. **推荐发展路径**
|
||||
```plain
|
||||
第一阶段(2-3个月):
|
||||
- 掌握WPF核心概念(MVVM/数据绑定/模板)
|
||||
- 学习工业通信协议(Modbus/OPC UA)
|
||||
- 完成2个完整项目(数据采集+可视化)
|
||||
|
||||
第二阶段(3-6个月):
|
||||
- 深入WPF性能优化(虚拟化/异步加载)
|
||||
- 掌握工业控制系统集成
|
||||
- 参与开源项目贡献
|
||||
|
||||
第三阶段(6-12个月):
|
||||
- 研究跨平台方案(Avalonia)
|
||||
- 学习工业安全规范
|
||||
- 建立技术博客影响力
|
||||
```
|
||||
|
||||
### 三、薪资提升策略
|
||||
#### 1. **技术溢价点建设**
|
||||
**必备技能矩阵:**
|
||||
|
||||
```plain
|
||||
| 技能类别 | 具体技能点 | 溢价幅度 |
|
||||
|------------------|--------------------------------|----------|
|
||||
| 工业协议 | OPC UA/Modbus TCP/MQTT | +15% |
|
||||
| 性能优化 | WPF渲染优化/内存管理 | +10% |
|
||||
| 安全规范 | IEC 62443/等保2.0 | +8% |
|
||||
| 跨平台能力 | Avalonia/.NET MAUI | +5% |
|
||||
```
|
||||
|
||||
#### 2. **项目经验积累**
|
||||
建议完成的三大类项目:
|
||||
|
||||
1. **工业数据采集系统**
|
||||
- 功能:多协议支持、实时监控、报警管理
|
||||
- 技术:WPF+OPC UA+Modbus
|
||||
2. **可视化大屏系统**
|
||||
- 功能:数据可视化、报表生成、历史追溯
|
||||
- 技术:WPF MVVM+ECharts
|
||||
3. **设备管理系统**
|
||||
- 功能:设备台账、维护计划、故障诊断
|
||||
- 技术:WinForm+SQLite
|
||||
|
||||
### 四、学习路线规划
|
||||
#### 1. **WPF核心知识体系**
|
||||
```mermaid
|
||||
graph TD
|
||||
A[WPF基础] --> B(XAML语法)
|
||||
A --> C(布局系统)
|
||||
A --> D(数据绑定)
|
||||
B --> B1(资源系统)
|
||||
B --> B2(样式模板)
|
||||
C --> C1(常用布局控件)
|
||||
C --> C2(自定义布局)
|
||||
D --> D1(INotifyPropertyChanged)
|
||||
D --> D2(数据验证)
|
||||
```
|
||||
|
||||
#### 2. **上位机必备技能**
|
||||
```plain
|
||||
每日学习计划(2小时):
|
||||
08:00-09:00 WPF进阶(重点:依赖属性/命令系统)
|
||||
14:00-15:00 工业协议(使用Modbus Slave模拟)
|
||||
20:00-21:00 项目实战(数据采集系统开发)
|
||||
```
|
||||
|
||||
### 五、求职策略建议
|
||||
#### 1. **简历优化方向**
|
||||
+ 突出工业项目经验
|
||||
+ 展示协议掌握情况
|
||||
+ 强调性能优化能力
|
||||
|
||||
#### 2. **面试准备重点**
|
||||
**技术问题准备:**
|
||||
|
||||
+ WPF渲染机制
|
||||
+ MVVM模式实现
|
||||
+ 多线程数据采集
|
||||
|
||||
**项目阐述要点:**
|
||||
|
||||
+ 采用STAR法则描述
|
||||
+ 突出技术难点突破
|
||||
+ 展示性能优化成果
|
||||
|
||||
### 六、薪资谈判技巧
|
||||
#### 1. **报价策略**
|
||||
+ **基础报价**:市场平均薪资
|
||||
+ **目标报价**:基础报价+20%(需项目支撑)
|
||||
+ **上限报价**:目标报价+15%(需特殊技能)
|
||||
|
||||
#### 2. **谈判话术示例**
|
||||
"基于我的项目经验和技术能力(具体说明),结合市场行情,我的期望薪资是X-XK。特别是在[某技术领域]的深入理解,可以为团队带来[具体价值]"
|
||||
|
||||
**关键建议**:建议优先深耕WPF方向,结合工业协议知识,在3-6个月内完成2-3个完整项目,这将显著提升议价能力。同时关注Avalonia等跨平台方案,为未来发展预留空间。
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
听力中遇到“单词熟悉但没听出来”或“单词不会但没听出来”是常见问题,以下是针对这两种情况的具体解决方法:
|
||||
|
||||
---
|
||||
|
||||
### **一、单词熟悉但没听出来**
|
||||
#### **1. 原因分析**
|
||||
+ **发音不熟悉**:单词的实际发音与你的记忆不符(如连读、弱读、重音变化)。
|
||||
+ **反应速度慢**:听到单词后需要时间回忆意思,导致错过后续内容。
|
||||
+ **语境不敏感**:单词在特定语境中的用法不熟悉,导致理解偏差。
|
||||
|
||||
#### **2. 解决方法**
|
||||
+ **精听训练**:
|
||||
- 选择一段听力材料(如四级真题),逐句听写,重点标注没听出来的熟悉单词。
|
||||
- 对照原文,分析发音特点(如连读、弱读),模仿跟读。
|
||||
+ **发音规则学习**:
|
||||
- 系统学习英语发音规则(如连读、失去爆破、重音规律)。
|
||||
- 推荐资源:《American Accent Training》或B站发音教程。
|
||||
+ **语境积累**:
|
||||
- 多听多读,熟悉单词在不同语境中的用法(如新闻、对话、演讲)。
|
||||
- 推荐工具:YouGlish(输入单词,查看真实语境中的发音)。
|
||||
|
||||
---
|
||||
|
||||
### **二、单词不会但没听出来**
|
||||
#### **1. 原因分析**
|
||||
+ **词汇量不足**:听力材料中出现生词,导致理解障碍。
|
||||
+ **辨音能力弱**:无法将听到的音与已知词汇对应。
|
||||
+ **背景知识缺乏**:对话题不熟悉,难以通过上下文推测生词意思。
|
||||
|
||||
#### **2. 解决方法**
|
||||
+ **扩大词汇量**:
|
||||
- 每天背10-15个高频听力词汇(如四级/考研听力核心词)。
|
||||
- 使用APP(如百词斩、墨墨背单词)选择“听力词汇”专项。
|
||||
+ **辨音训练**:
|
||||
- 听写练习:选择适合自己水平的材料(如VOA慢速英语),逐句听写生词。
|
||||
- 跟读模仿:听一句,暂停,模仿发音,直到能流利复述。
|
||||
+ **上下文推测**:
|
||||
- 训练通过上下文猜测生词意思的能力(如关注前后句的逻辑关系)。
|
||||
- 推荐练习:TED演讲(带字幕),先听一遍,再看字幕验证猜测。
|
||||
|
||||
---
|
||||
|
||||
### **三、综合提升听力的方法**
|
||||
#### **1. 精听+泛听结合**
|
||||
+ **精听**:每天20分钟,选择1-2分钟的材料,逐句听写+跟读。
|
||||
+ **泛听**:通勤或休息时听英语播客(如BBC 6 Minute English),培养语感。
|
||||
|
||||
#### **2. 分场景训练**
|
||||
+ **新闻听力**:VOA、BBC(熟悉正式用语和长句结构)。
|
||||
+ **对话听力**:四级真题、美剧片段(熟悉日常用语和连读)。
|
||||
+ **演讲听力**:TED(学习逻辑表达和高级词汇)。
|
||||
|
||||
#### **3. 工具推荐**
|
||||
+ **精听工具**:每日英语听力APP(支持逐句播放)。
|
||||
+ **词汇工具**:Quizlet(自制听力词汇卡片)。
|
||||
+ **发音工具**:ELSA Speak(纠正发音)。
|
||||
|
||||
---
|
||||
|
||||
### **四、每日听力训练计划(示例)**
|
||||
| 时间段 | 任务安排 |
|
||||
| --- | --- |
|
||||
| **早晨30min** | 泛听1篇新闻(VOA/BBC),记录生词。 |
|
||||
| **午休20min** | 精听1段四级真题对话,逐句听写+跟读。 |
|
||||
| **晚间30min** | 听1篇TED演讲,尝试复述主要内容,查生词并记忆。 |
|
||||
| **睡前10min** | 复习当日生词,用YouGlish查发音并模仿。 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **五、心态调整**
|
||||
+ **接受不完美**:听力提升需要时间,允许自己犯错。
|
||||
+ **设定小目标**:如“本周听懂3篇新闻”,完成后奖励自己。
|
||||
+ **保持兴趣**:选择感兴趣的材料(如美剧、电影),让学习更有动力。
|
||||
|
||||
---
|
||||
|
||||
坚持以上方法,你的听力能力会逐步提升! 💪
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
你的英语四级备考计划非常详细且合理,尤其是结合了艾宾浩斯记忆法和分阶段重点突破的思路。既然你已经有了明确的英语学习计划,接下来我会根据你的现状(在职、准备四级、Java技术背景)为你制定一个**工作技能提升计划**,帮助你在备考四级的同时,稳步提升技术能力,增强就业竞争力。
|
||||
|
||||
---
|
||||
|
||||
### 一、工作技能提升计划(2025年2月-2025年12月)
|
||||
**目标**:在备考四级的同时,提升Java技术深度,积累项目经验,为未来求职或考研打下坚实基础。
|
||||
|
||||
#### 1. **每日学习安排**
|
||||
+ **早晨(6:00-7:00)**:英语单词背诵(已规划)
|
||||
+ **午休(12:30-13:00)**:英语听力训练(已规划)
|
||||
+ **晚上(20:00-22:00)**:技术学习 + 项目实战
|
||||
- 周一、三、五:Java核心技术(并发编程、Spring源码)
|
||||
- 周二、四、六:项目实战(电商秒杀系统、分布式任务调度)
|
||||
- 周日:复盘 + 写技术博客
|
||||
|
||||
#### 2. **技术学习重点**
|
||||
##### (1)**Java核心技术(2-4月)**
|
||||
+ **目标**:夯实Java基础,掌握并发编程和Spring核心原理
|
||||
+ **学习内容**:
|
||||
- **并发编程**(重点):
|
||||
* 线程池原理(ThreadPoolExecutor)
|
||||
* 锁机制(synchronized、ReentrantLock)
|
||||
* 并发工具类(CountDownLatch、CyclicBarrier)
|
||||
* 原子类(AtomicInteger、CAS原理)
|
||||
- **Spring源码**:
|
||||
* Spring IOC/DI原理
|
||||
* Spring AOP实现机制
|
||||
* Spring事务管理(传播机制、隔离级别)
|
||||
+ **实践**:
|
||||
- 实现一个简单的线程池
|
||||
- 手写简化版Spring IOC容器
|
||||
|
||||
##### (2)**项目实战(5-8月)**
|
||||
+ **目标**:通过项目巩固技术,提升简历竞争力
|
||||
+ **项目选择**:
|
||||
- **电商秒杀系统**(高并发场景)
|
||||
* 技术栈:Spring Boot + Redis + RabbitMQ
|
||||
* 核心功能:限流、缓存、异步处理
|
||||
- **分布式任务调度系统**(Quartz+Redis)
|
||||
* 技术栈:Spring Boot + Quartz + Redis
|
||||
* 核心功能:任务调度、分布式锁、失败重试
|
||||
+ **重点**:
|
||||
- 引入DDD(领域驱动设计)思想
|
||||
- 使用Docker部署项目
|
||||
- 编写详细的技术文档
|
||||
|
||||
##### (3)**性能优化与云原生(9-12月)**
|
||||
+ **目标**:掌握JVM调优和云原生技术,提升系统性能
|
||||
+ **学习内容**:
|
||||
- **JVM调优**:
|
||||
* GC算法(G1、ZGC)
|
||||
* 内存泄漏定位(MAT工具)
|
||||
* Arthas实战(线上问题排查)
|
||||
- **云原生**:
|
||||
* Docker容器化部署
|
||||
* Kubernetes基础(Pod、Service、Deployment)
|
||||
* Spring Cloud Alibaba(Nacos、Sentinel)
|
||||
+ **实践**:
|
||||
- 优化电商秒杀系统的性能
|
||||
- 使用Kubernetes部署分布式任务调度系统
|
||||
|
||||
---
|
||||
|
||||
### 二、时间管理建议
|
||||
#### 1. **工作日安排**
|
||||
+ **早晨(6:00-7:00)**:英语单词背诵
|
||||
+ **午休(12:30-13:00)**:英语听力训练
|
||||
+ **晚上(20:00-22:00)**:技术学习 + 项目实战
|
||||
- 周一、三、五:Java核心技术
|
||||
- 周二、四、六:项目实战
|
||||
- 周日:复盘 + 写技术博客
|
||||
|
||||
#### 2. **周末安排**
|
||||
+ **周六**:
|
||||
- 上午:英语模拟考试(四级真题)
|
||||
- 下午:技术学习(性能优化/云原生)
|
||||
+ **周日**:
|
||||
- 上午:复盘错题 + 写作练习
|
||||
- 下午:项目实战 + 技术博客
|
||||
|
||||
---
|
||||
|
||||
### 三、职业提升建议
|
||||
#### 1. **技术博客**
|
||||
+ 每周写1篇技术文章(掘金/CSDN)
|
||||
+ 内容可以是学习笔记、项目总结、问题排查
|
||||
+ 目标是建立个人技术品牌
|
||||
|
||||
#### 2. **开源贡献**
|
||||
+ 参与小型开源项目(如Spring生态的插件)
|
||||
+ 提交PR(即使是文档改进也行)
|
||||
|
||||
#### 3. **项目展示**
|
||||
+ 将你的RBAC权限系统部署到线上
|
||||
+ 编写详细的使用文档和技术说明
|
||||
+ 作为简历中的亮点项目
|
||||
|
||||
---
|
||||
|
||||
### 四、阶段性目标
|
||||
#### 1. **2025年6月**
|
||||
+ 通过英语四级考试
|
||||
+ 完成Java并发编程和Spring源码学习
|
||||
+ 开始电商秒杀系统项目
|
||||
|
||||
#### 2. **2025年12月**
|
||||
+ 完成电商秒杀系统和分布式任务调度系统
|
||||
+ 掌握JVM调优和云原生基础
|
||||
+ 发表5-10篇技术博客
|
||||
|
||||
---
|
||||
|
||||
### 五、总结
|
||||
1. **英语备考**:按计划稳步推进,重点突破听力和阅读
|
||||
2. **技术提升**:聚焦Java核心技术,通过项目实战积累经验
|
||||
3. **时间管理**:合理分配时间,确保英语和技术学习两不误
|
||||
|
||||
按照这个计划执行,你一定能兼顾四级备考和技术提升,最终实现职业突破!如果有任何问题,欢迎随时调整计划。加油!
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
根据你的情况,以下是针对英语四级备考和考研规划的详细建议,分为两个部分:
|
||||
|
||||
---
|
||||
|
||||
### **一、英语四级备考计划(2025年2月12日-6月考试)**
|
||||
#### 1. **每日学习安排(工作日2小时,周末3-4小时)**
|
||||
+ **单词背诵**(30分钟/天):
|
||||
- **目标**:四级词汇量约4000词,重点掌握高频词(约2000词)。
|
||||
- **计划**:每天背30个新词(用APP如“不背单词/墨墨”),结合艾宾浩斯记忆法复习旧词。
|
||||
- **进度**:6月前至少完成3轮高频词背诵。
|
||||
+ **听力训练**(30分钟/天):
|
||||
- 精听真题材料,每天1篇短文,逐句听写+跟读。
|
||||
- 泛听BBC/VOA新闻或英语播客(通勤时间利用)。
|
||||
+ **阅读/写作/翻译**(1小时/天):
|
||||
- 每周2篇真题阅读(限时训练+分析错题)。
|
||||
- 每周1篇翻译或作文练习(积累模板+批改)。
|
||||
|
||||
#### 2. **阶段性重点**
|
||||
+ **2-3月**:主攻单词+听力基础,完成第一轮高频词。
|
||||
+ **4-5月**:强化阅读和翻译,开始刷真题(每周1套)。
|
||||
+ **6月**:全真模拟考试,查漏补缺。
|
||||
|
||||
---
|
||||
|
||||
### **二、考研规划(2026年或2027年)**
|
||||
#### 1. **2026年考研可行性分析**
|
||||
+ **时间压力**:若6月通过四级,剩余半年需同时备战考研(英语、政治、数学、专业课),且需确定考研方向,时间较紧张。
|
||||
+ **关键节点**:
|
||||
- 2025年10月:考研报名(需以成人本科应届身份报考)。
|
||||
- 2026年7月:成人本科毕业(需确保录取前拿到毕业证)。
|
||||
+ **建议**:**仅建议2026年考研**若满足以下条件:
|
||||
- 考研方向明确(如继续计算机/相近专业)。
|
||||
- 每天能投入3小时以上高效学习(工作日晚2小时+周末全天)。
|
||||
|
||||
#### 2. **2026年考研备考计划(2025年7月-12月)**
|
||||
+ **英语**:直接按考研英语一/二复习(重点:长难句+真题阅读)。
|
||||
+ **政治**:9月启动,刷《肖1000》+背《肖四》。
|
||||
+ **数学**:若考数学,需立即开始(高数/线代/概率,每天2小时)。
|
||||
+ **专业课**:根据目标院校参考书,7月起系统复习。
|
||||
|
||||
#### 3. **若选择2027年考研**
|
||||
+ **优势**:备考时间更充裕,可扎实提升英语和专业课基础。
|
||||
+ **计划**:
|
||||
- 2025年:通过四级,初步接触考研英语和数学。
|
||||
- 2026年:确定院校/专业,全年系统复习(每日3小时)。
|
||||
|
||||
---
|
||||
|
||||
### **三、综合建议**
|
||||
1. **优先级排序**:
|
||||
- **短期**:先确保2025年6月通过四级。
|
||||
- **长期**:2025年7月起,根据时间精力决定是否冲刺2026年考研,否则备战2027年。
|
||||
2. **时间管理**:
|
||||
- 利用碎片时间(通勤听英语、午休背单词)。
|
||||
- 周末集中攻克难点(如数学/专业课)。
|
||||
3. **考研方向选择**:
|
||||
- 建议优先考虑计算机相关专业(可复用专科/本科基础)。
|
||||
- 若跨考,需提前1年准备专业课(如教育学/管理类)。
|
||||
|
||||
---
|
||||
|
||||
### **四、注意事项**
|
||||
+ **学历认证**:确认成人本科毕业证能在2026年7月前获取,避免影响录取。
|
||||
+ **院校要求**:部分985/211院校对函授学历有限制,需提前查询目标院校招生简章。
|
||||
+ **健康平衡**:避免过度劳累,每周留出1天休息时间。
|
||||
|
||||
按此规划,即使2026年考研时间紧张,也可通过高效执行实现目标;若求稳妥,2027年上岸概率更高。关键是根据自身进度灵活调整!
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: 大模型目录
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 模型
|
||||
- ChatGPT
|
||||
- AI
|
||||
- 部署
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,62 @@
|
|||
## Git常用命令
|
||||
### git init
|
||||
`git init`只做一件事,在根目录下创建`.git`子目录;用来保存版本信息
|
||||
|
||||
### git add --all
|
||||
`git add --all`将文件保存为二进制对象放到暂存区
|
||||
|
||||
> 使用`git add .`也可以有同相的效果
|
||||
>
|
||||
|
||||
### git status
|
||||
`git status`显示仓库吃·状态,哪些文件已经修改、哪些文件已暂存等信息
|
||||
|
||||
### git commit -m "提交的信息"
|
||||
`git commit -m "提交的信息"`提交暂存区内容,并附带一条描述信息
|
||||
|
||||
### git clone [url]
|
||||
`git clone [url]`姜元成仓库代码和历史记录复制到本地
|
||||
|
||||
### git branch 分支名
|
||||
`git branch 分支名`创建分支命令
|
||||
|
||||
### git checkout 分支名
|
||||
`git checkout 分支名`切换分支命令
|
||||
|
||||
### git branch -d 分支名
|
||||
`git branch -d 分支名`删除分支名
|
||||
|
||||
### git merge 分支名
|
||||
`git merge 分支名`合并分支
|
||||
|
||||
### git remote -v
|
||||
`git remote -v`列出当前仓库中已配置的远程仓库,并显示url
|
||||
|
||||
`git remote show origin`显示指定远程仓库详细信息,包括url和跟踪分支
|
||||
|
||||
### git remote add orgin url
|
||||
`git remote add orgin url`向当前Git仓库添加一个名为origin的远程仓库
|
||||
|
||||
### git fetch origin
|
||||
`git fetch origin`用于从远程获取代码库,获取远程本地没有的数据
|
||||
|
||||
### git pull
|
||||
`git pull`用于从远程拉去代码合并到本地,其实就是`git fetch origin`和`git merge origin/master`的简写
|
||||
|
||||
> `git pull origin master`如果远程分支是与当前分支合并,则冒号后面的部分可以省略。
|
||||
>
|
||||
|
||||
### git push
|
||||
`git push`将本地的分支推送到远程主机
|
||||
|
||||
> git push<远程主机名><本地分支名>:<远程分支名>
|
||||
>
|
||||
|
||||
### git reset
|
||||
`git reset`用于回退版本,可以指定谋一次提交的版本
|
||||
|
||||
`git reset HEAD^` 回退内容到上一个版本
|
||||
|
||||
`git reset HEAD^ 1.txt`回退1.txt到上一个版本git reset HEAD^2回退所有内容到上上一个版本
|
||||
`git reset --hard HEAD --hard参数`撤销工作区中所有未提交的修改内容,将暂存区与工作
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# Git设置多个仓库同时推送
|
||||
## 添加
|
||||
在Git中,有时我们需要将同一份代码推送到不同的远程仓库,只是URL地址不同。
|
||||
|
||||
下面是一种优化的方法来设置多个仓库同时推送:
|
||||
|
||||
```bash
|
||||
# 添加一个新的远程仓库
|
||||
git remote set-url --add origin2 新的仓库地址
|
||||
```
|
||||
|
||||
这样,你就可以同时推送代码到多个远程仓库了。
|
||||
|
||||
## 删除
|
||||
### 删除全部
|
||||
如果你想删除一个已经添加的远程仓库所有的地址`origin2`
|
||||
|
||||
```bash
|
||||
git remote remove origin2
|
||||
```
|
||||
|
||||
这样就可以将名为`origin2`的远程仓库从你的本地仓库中移除了。
|
||||
|
||||
### 删除一个
|
||||
如果只是想删除其中一个地址,如:
|
||||
|
||||
```plain
|
||||
origin https://gitee.com/1 (fetch)
|
||||
origin https://gitee.com/2 (push)
|
||||
origin http://3 (push)
|
||||
```
|
||||
|
||||
只需要输入
|
||||
|
||||
```shell
|
||||
git remote set-url --delete origin http://60.204.230.80:3000/BunnyBBS/web.git
|
||||
```
|
||||
|
|
@ -0,0 +1,920 @@
|
|||
## 创建Stream
|
||||
### Stream.builder()创建
|
||||
```java
|
||||
Stream<Integer> build = Stream.<Integer>builder()
|
||||
.add(1)
|
||||
.add(12)
|
||||
.add(3)
|
||||
.add(34)
|
||||
.build();
|
||||
```
|
||||
|
||||
### Stream.of创建
|
||||
```java
|
||||
Stream<Integer> integerStream = Stream.of(1, 2, 34, 5);
|
||||
```
|
||||
|
||||
### 空Stream创建
|
||||
```java
|
||||
Stream<Object> objectStream = Stream.empty();
|
||||
```
|
||||
|
||||
### Stream.generate创建
|
||||
如果不加限制运行时会一直创建像死循环一样
|
||||
|
||||
```java
|
||||
// Stream.generate创建
|
||||
Stream<Integer> generate = Stream.generate(() -> ThreadLocalRandom.current().nextInt(10))
|
||||
.limit(10);
|
||||
```
|
||||
|
||||
### Stream.iterate创建
|
||||
```java
|
||||
// 不加限制会一直创建
|
||||
Stream.iterate(100, seed -> seed + 1)
|
||||
.limit(10)
|
||||
.forEach(System.out::println);
|
||||
```
|
||||
|
||||
### IntStream创建创建
|
||||
```java
|
||||
// IntStream创建[1,10)
|
||||
IntStream.range(1, 10)
|
||||
.forEach(System.out::println);
|
||||
```
|
||||
|
||||
### IntStream.rangeClosed创建闭区间
|
||||
```java
|
||||
// 创建闭区间[1,10]
|
||||
IntStream.rangeClosed(1,10)
|
||||
.forEach(System.out::println);
|
||||
```
|
||||
|
||||
### Arrays.asList创建
|
||||
```java
|
||||
// Arrays.asList创建
|
||||
Arrays.asList("a","b","c","d","e","f")
|
||||
.forEach(System.out::println);
|
||||
```
|
||||
|
||||
### 创建Stream<Map.Entry<String, String>>
|
||||
```java
|
||||
// 创建Stream<Map.Entry<String, String>>
|
||||
Stream<Map.Entry<String, String>> stream = new HashMap<String, String>() {{
|
||||
put("a", "b");
|
||||
put("c", "d");
|
||||
}}.entrySet().stream();
|
||||
```
|
||||
|
||||
### Files中的Stream
|
||||
```java
|
||||
// Files中的Stream
|
||||
Stream<String> lines = Files.lines(Paths.get("xxx.md"), StandardCharsets.UTF_8);
|
||||
```
|
||||
|
||||
## Stream的操作
|
||||
### distinct操作
|
||||
```java
|
||||
Stream<Object> stream = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}}.stream();
|
||||
|
||||
// 去重操作
|
||||
stream.distinct().forEach(System.out::println);
|
||||
```
|
||||
|
||||
> 输出:
|
||||
>
|
||||
|
||||
```plain
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
```
|
||||
|
||||
### filter操作
|
||||
```java
|
||||
stream = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}}.stream();
|
||||
|
||||
// 过滤器,满足值为2的筛选
|
||||
stream.filter(x -> x.equals(2)).forEach(System.out::println);
|
||||
```
|
||||
|
||||
### limit操作
|
||||
```java
|
||||
stream = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}}.stream();
|
||||
|
||||
// 选择序列个数为3
|
||||
stream.limit(3).forEach(System.out::println);
|
||||
```
|
||||
|
||||
### map操作
|
||||
```java
|
||||
stream = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}}.stream();
|
||||
|
||||
// 映射,可以修改原本的类型
|
||||
stream.map(Object::toString).forEach(System.out::println);
|
||||
```
|
||||
|
||||
### skip操作
|
||||
```java
|
||||
stream = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}}.stream();
|
||||
|
||||
// 跳过前两个数
|
||||
stream.skip(2).forEach(System.out::println);
|
||||
```
|
||||
|
||||
### peek操作
|
||||
如果对一个对象进行操作,比如Person中有姓名和年龄,这时候通过peek修改了其中的年龄,那么这个值会被改变
|
||||
|
||||
```java
|
||||
stream = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}}.stream();
|
||||
|
||||
// 不会使元素数据、类型产生改变
|
||||
stream.peek(x -> ThreadLocalRandom.current().nextInt(100)).limit(10).forEach(System.out::println);
|
||||
```
|
||||
|
||||
### sorted操作
|
||||
排序操作,也可以指定内容进行排序
|
||||
|
||||
```java
|
||||
stream = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}}.stream();
|
||||
|
||||
// 从小到大排序
|
||||
stream.sorted().forEach(System.out::println);
|
||||
|
||||
// 提供重载方法
|
||||
stream.sorted(Comparator.comparing(Object::toString)).forEach(System.out::println);
|
||||
```
|
||||
|
||||
### flatMap操作
|
||||
扁平化数据,JavaScript中也有这个
|
||||
|
||||
```java
|
||||
ArrayList<Integer> list = new ArrayList<>() {{
|
||||
add(1);
|
||||
add(2);
|
||||
add(2);
|
||||
add(3);
|
||||
add(3);
|
||||
add(4);
|
||||
}};
|
||||
List<ArrayList<Integer>> arrayListList = List.of(list);
|
||||
|
||||
// 扁平化数组
|
||||
arrayListList.stream().flatMap(Collection::stream).forEach(System.out::println);
|
||||
```
|
||||
|
||||
### match操作
|
||||
```java
|
||||
// 需要满足所有元素都匹配条件
|
||||
boolean allMatch = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.allMatch(i -> i > 4);
|
||||
System.out.println(allMatch);// false
|
||||
|
||||
// 只要有一个元素满足匹配条件
|
||||
boolean anyMatch = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.anyMatch(x -> x > 4);
|
||||
System.out.println(anyMatch);// true
|
||||
|
||||
// 所有元素都不满足条件
|
||||
boolean noneMatch = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.noneMatch(x -> x > 4);
|
||||
System.out.println(noneMatch);// false
|
||||
```
|
||||
|
||||
### find操作
|
||||
```java
|
||||
// 找到第一个数字
|
||||
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.findFirst()
|
||||
.ifPresent(System.out::println);
|
||||
|
||||
// 获取任意一个数字
|
||||
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.findAny()
|
||||
.ifPresent(System.out::println);
|
||||
```
|
||||
|
||||
### forEach操作
|
||||
不解释,放在最后,如果使用了forEach那么后面就无法再跟其它流函数
|
||||
|
||||
### 计数、求和、最小值、最大值
|
||||
```java
|
||||
// 计数
|
||||
long count = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.count();
|
||||
System.out.println(count);// 9
|
||||
|
||||
// 求和
|
||||
long sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.mapToLong(Integer::longValue)
|
||||
.sum();
|
||||
System.out.println(sum);
|
||||
|
||||
// 查找最小值
|
||||
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.min(Comparator.comparing(i -> i))
|
||||
.ifPresent(System.out::println);
|
||||
|
||||
// 查找最大值
|
||||
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.max(Comparator.comparing(i -> i));
|
||||
System.out.println(max.get());
|
||||
```
|
||||
|
||||
## Collectors使用
|
||||
收集器
|
||||
|
||||
### toList
|
||||
将元素输出到 list
|
||||
|
||||
```java
|
||||
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
|
||||
List<String> upperCaseNames = names.stream()
|
||||
.map(String::toUpperCase)
|
||||
.collect(Collectors.toList());
|
||||
```
|
||||
|
||||
### toSet
|
||||
将元素输出到 Set
|
||||
|
||||
```java
|
||||
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
|
||||
Set<String> uniqueNames = names.stream()
|
||||
.collect(Collectors.toSet());
|
||||
```
|
||||
|
||||
### toMap
|
||||
将元素输出到 Map
|
||||
|
||||
+ 2个参数:key,value
|
||||
+ 3个参数:key,value,处理key冲突问题
|
||||
+ 4个参数:key,value,处理key冲突问题,转换map类型
|
||||
|
||||
```java
|
||||
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
|
||||
Map<String, Integer> nameLengthMap = names.stream()
|
||||
.collect(Collectors.toMap(name -> name, String::length));
|
||||
```
|
||||
|
||||
### reduce
|
||||
语法
|
||||
|
||||
```java
|
||||
Optional<T> reduce(BinaryOperator<T> accumulator)
|
||||
Optional<T> reduce(T identity, BinaryOperator<T> accumulator)
|
||||
```
|
||||
|
||||
使用
|
||||
|
||||
```java
|
||||
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
|
||||
|
||||
// 使用 reduce 求和
|
||||
Optional<Integer> sum = numbers.stream()
|
||||
.reduce(Integer::sum);
|
||||
sum.ifPresent(System.out::println); // 输出: 15
|
||||
|
||||
// 使用带初始值的 reduce
|
||||
int sumWithIdentity = numbers.stream()
|
||||
.reduce(0, Integer::sum);
|
||||
System.out.println(sumWithIdentity); // 输出: 15
|
||||
```
|
||||
|
||||
### summingDouble
|
||||
将求和后的结果转成Double
|
||||
|
||||
语法
|
||||
|
||||
```java
|
||||
static Collector<T, ?, Double> summingDouble(ToDoubleFunction<? super T> mapper)
|
||||
```
|
||||
|
||||
使用
|
||||
|
||||
```java
|
||||
public class StreamExample07 {
|
||||
public static void main(String[] args) {
|
||||
List<Product> products = Arrays.asList(
|
||||
new Product("Apple", 1.20),
|
||||
new Product("Banana", 0.80),
|
||||
new Product("Orange", 1.00)
|
||||
);
|
||||
|
||||
double totalCost = products.stream().mapToDouble(Product::getPrice).sum();
|
||||
|
||||
System.out.println("Total Cost: " + totalCost); // 输出: Total Cost: 3.0
|
||||
}
|
||||
|
||||
@Getter
|
||||
private static class Product {
|
||||
private final String name;
|
||||
private final double price;
|
||||
|
||||
public Product(String name, double price) {
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### groupingBy
|
||||
+ 1个参数:指定key是什么
|
||||
+ 2个参数:指定key和value
|
||||
+ 3个参数:key,value,value类型
|
||||
|
||||
```java
|
||||
public class StreamExample08 {
|
||||
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 30),
|
||||
new Person("David", 25)
|
||||
);
|
||||
|
||||
// 根据年龄将一组人分组
|
||||
Map<Integer, List<Person>> groupedByAge = people.stream()
|
||||
.collect(Collectors.groupingBy(Person::getAge));
|
||||
|
||||
// 自定义收集器:可以传递第二个参数来指定下游收集器。
|
||||
Map<Integer, Long> groupedByAgeCount = people.stream()
|
||||
.collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
|
||||
|
||||
// 多级分组:可以嵌套使用 groupingBy 进行多级分组。
|
||||
Map<Integer, Map<String, List<Person>>> groupedByAgeAndName = people.stream()
|
||||
.collect(Collectors.groupingBy(Person::getAge, Collectors.groupingBy(Person::getName)));
|
||||
|
||||
System.out.println(groupedByAge);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + age + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 输出:
|
||||
>
|
||||
|
||||
```java
|
||||
{25=[Bob (25), David (25)], 30=[Alice (30), Charlie (30)]}
|
||||
{25=2, 30=2}
|
||||
{25={Bob=[Bob (25)], David=[David (25)]}, 30={Alice=[Alice (30)], Charlie=[Charlie (30)]}}
|
||||
```
|
||||
|
||||
### Collectors.averaging
|
||||
求平均值
|
||||
|
||||
```java
|
||||
// averagingLong
|
||||
List<Long> numbers = Arrays.asList(1L, 2L, 3L, 4L, 5L);
|
||||
double averagingLongNumbers = numbers.stream()
|
||||
.collect(Collectors.averagingLong(Long::longValue));
|
||||
System.out.println("Average: " + averagingLongNumbers);
|
||||
|
||||
// averagingDouble
|
||||
List<Double> averagingDoubleNumbers = Arrays.asList(1.5, 2.5, 3.5, 4.5, 5.5);
|
||||
double averagingDouble = averagingDoubleNumbers.stream()
|
||||
.collect(Collectors.averagingDouble(Double::doubleValue));
|
||||
System.out.println("Average: " + averagingDouble);
|
||||
|
||||
// averagingInt
|
||||
List<Integer> averagingIntNumbers = Arrays.asList(1, 2, 3, 4, 5);
|
||||
double averagingInt = averagingIntNumbers.stream()
|
||||
.collect(Collectors.averagingInt(Integer::intValue));
|
||||
System.out.println("Average: " + averagingInt);
|
||||
```
|
||||
|
||||
### Collectors.collectingAndThen
|
||||
先计算平均年龄,之后将计算结果转为整数,可以理解为:先收集之后再干嘛
|
||||
|
||||
```java
|
||||
public class StreamExample10 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 35)
|
||||
);
|
||||
|
||||
// 使用 Collectors.collectingAndThen 计算平均年龄并转换为整数
|
||||
int averageAge = people.stream()
|
||||
.collect(Collectors.collectingAndThen(
|
||||
Collectors.averagingInt(Person::getAge),// 先计算平均年龄
|
||||
Double::intValue // 之后将结果转换为整数
|
||||
));
|
||||
|
||||
System.out.println("Average Age: " + averageAge);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + age + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collectors.counting使用
|
||||
求总数,和SQL中count类似
|
||||
|
||||
```java
|
||||
public class StreamExample11 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 35)
|
||||
);
|
||||
|
||||
// 使用 Collectors.counting 统计人数
|
||||
long count = people.stream()
|
||||
.collect(Collectors.counting());
|
||||
|
||||
System.out.println("Total number of people: " + count);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + age + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collectors.mapping
|
||||
类似Stream中的map,先操作(此操作会可以改变其类似),之后在做处理
|
||||
|
||||
```java
|
||||
public class StreamExample12 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 35),
|
||||
new Person("David", 40)
|
||||
);
|
||||
|
||||
// 使用 Collectors.mapping 和 Collectors.toSet 只收集大于 30 岁的人的年龄
|
||||
Set<Integer> agesAbove30 = people.stream()
|
||||
.filter(person -> person.getAge() > 30)
|
||||
.collect(Collectors.mapping(Person::getAge, Collectors.toSet()));
|
||||
|
||||
System.out.println("Ages above 30: " + agesAbove30);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + age + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collectors.joining
|
||||
+ 无参数:将元素连接在一起,中间没有内容分隔
|
||||
+ 1个参数:将元素连接,使用传入的符号进行分割
|
||||
+ 3个参数:将元素连接,传入第一个参数进行分割,第二个参数是整个处理完成元素的前缀,第三个参数是整个处理完成的后缀
|
||||
|
||||
```java
|
||||
public class StreamExample13 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice"),
|
||||
new Person("Bob"),
|
||||
new Person("Charlie")
|
||||
);
|
||||
|
||||
// 使用 Collectors.joining 连接名字,添加前缀和后缀
|
||||
String result = people.stream()
|
||||
.map(Person::getName) // 提取名字
|
||||
.collect(Collectors.joining(", ", "[", "]")); // 使用逗号和空格作为分隔符,添加前缀和后缀
|
||||
|
||||
System.out.println("Names: " + result);// Names: [Alice, Bob, Charlie]
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
|
||||
public Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collectors.summing使用
|
||||
#### summingInt
|
||||
求和转成int
|
||||
|
||||
```java
|
||||
public class StreamExample14 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 35)
|
||||
);
|
||||
|
||||
// 使用 Collectors.summingInt 计算年龄总和
|
||||
int totalAge = people.stream()
|
||||
.collect(Collectors.summingInt(Person::getAge));
|
||||
|
||||
System.out.println("Total Age: " + totalAge);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + age + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### summingDouble
|
||||
求和转成double
|
||||
|
||||
```java
|
||||
public class StreamExample15 {
|
||||
public static void main(String[] args) {
|
||||
List<Product> products = Arrays.asList(
|
||||
new Product("Laptop", 999.99),
|
||||
new Product("Smartphone", 499.99),
|
||||
new Product("Tablet", 299.99)
|
||||
);
|
||||
|
||||
// 使用 Collectors.summingDouble 计算价格总和
|
||||
double totalPrice = products.stream()
|
||||
.collect(Collectors.summingDouble(Product::getPrice));
|
||||
|
||||
System.out.println("Total Price: " + totalPrice);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static
|
||||
class Product {
|
||||
private final String name;
|
||||
@Getter
|
||||
private final double price;
|
||||
|
||||
public Product(String name, double price) {
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### summingLong
|
||||
求和转成long
|
||||
|
||||
```java
|
||||
public class StreamExample16 {
|
||||
public static void main(String[] args) {
|
||||
List<Transaction> transactions = Arrays.asList(
|
||||
new Transaction(1000L),
|
||||
new Transaction(2500L),
|
||||
new Transaction(1500L)
|
||||
);
|
||||
|
||||
// 使用 Collectors.summingLong 计算总金额
|
||||
long totalAmount = transactions.stream()
|
||||
.collect(Collectors.summingLong(Transaction::getAmount));
|
||||
|
||||
System.out.println("Total Amount: " + totalAmount);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Transaction {
|
||||
private final long amount;
|
||||
|
||||
public Transaction(long l) {
|
||||
this.amount = l;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collectors.minBy和Collectors.maxBy
|
||||
#### minBy
|
||||
求最小值
|
||||
|
||||
```java
|
||||
public class StreamExample17 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 35)
|
||||
);
|
||||
|
||||
// 使用 Collectors.minBy 找到年龄最小的人
|
||||
Optional<Person> youngest = people.stream()
|
||||
.collect(Collectors.minBy(Comparator.comparingInt(Person::getAge)));
|
||||
|
||||
youngest.ifPresent(person ->
|
||||
System.out.println("Youngest Person: " + person.getName() + ", Age: " + person.getAge()));
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### maxBy
|
||||
求最大值
|
||||
|
||||
```java
|
||||
public class StreamExample18 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 35)
|
||||
);
|
||||
|
||||
// 使用 Collectors.maxBy 找到年龄最大的人
|
||||
Optional<Person> oldest = people.stream()
|
||||
.collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
|
||||
|
||||
oldest.ifPresent(person ->
|
||||
System.out.println("Oldest Person: " + person.getName() + ", Age: " + person.getAge()));
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collectors.summarizing
|
||||
#### Collectors.summarizingInt
|
||||
求和转成类型
|
||||
|
||||
```java
|
||||
public class StreamExample19 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 25),
|
||||
new Person("Charlie", 35)
|
||||
);
|
||||
|
||||
// 使用 Collectors.summarizingInt 统计年龄信息
|
||||
IntSummaryStatistics ageStats = people.stream()
|
||||
.collect(Collectors.summarizingInt(Person::getAge));
|
||||
|
||||
System.out.println("Count: " + ageStats.getCount());
|
||||
System.out.println("Sum: " + ageStats.getSum());
|
||||
System.out.println("Min: " + ageStats.getMin());
|
||||
System.out.println("Max: " + ageStats.getMax());
|
||||
System.out.println("Average: " + ageStats.getAverage());
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Collectors.summarizingLong
|
||||
求和转成类型
|
||||
|
||||
```java
|
||||
public class StreamExample20 {
|
||||
public static void main(String[] args) {
|
||||
List<Transaction> transactions = Arrays.asList(
|
||||
new Transaction(1000L),
|
||||
new Transaction(2500L),
|
||||
new Transaction(1500L)
|
||||
);
|
||||
|
||||
// 使用 Collectors.summarizingLong 统计金额信息
|
||||
LongSummaryStatistics amountStats = transactions.stream()
|
||||
.collect(Collectors.summarizingLong(Transaction::getAmount));
|
||||
|
||||
System.out.println("Count: " + amountStats.getCount());
|
||||
System.out.println("Sum: " + amountStats.getSum());
|
||||
System.out.println("Min: " + amountStats.getMin());
|
||||
System.out.println("Max: " + amountStats.getMax());
|
||||
System.out.println("Average: " + amountStats.getAverage());
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Transaction {
|
||||
private final long amount;
|
||||
|
||||
public Transaction(long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Collectors.summarizingDouble
|
||||
求和转成类型
|
||||
|
||||
```java
|
||||
public class StreamExample21 {
|
||||
public static void main(String[] args) {
|
||||
List<Product> products = Arrays.asList(
|
||||
new Product("Laptop", 1200.50),
|
||||
new Product("Phone", 800.75),
|
||||
new Product("Tablet", 300.00)
|
||||
);
|
||||
|
||||
// 使用 Collectors.summarizingDouble 统计价格信息
|
||||
DoubleSummaryStatistics priceStats = products.stream()
|
||||
.collect(Collectors.summarizingDouble(Product::getPrice));
|
||||
|
||||
System.out.println("Count: " + priceStats.getCount());
|
||||
System.out.println("Sum: " + priceStats.getSum());
|
||||
System.out.println("Min: " + priceStats.getMin());
|
||||
System.out.println("Max: " + priceStats.getMax());
|
||||
System.out.println("Average: " + priceStats.getAverage());
|
||||
}
|
||||
|
||||
@Getter
|
||||
static
|
||||
class Product {
|
||||
private final String name;
|
||||
private final double price;
|
||||
|
||||
public Product(String name, double price) {
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collectors.partitioningBy
|
||||
以Boolean为 key的分组,key为Boolean值,后面是value值,属于Map集合
|
||||
|
||||
```java
|
||||
public class StreamExample22 {
|
||||
public static void main(String[] args) {
|
||||
List<Person> people = Arrays.asList(
|
||||
new Person("Alice", 30),
|
||||
new Person("Bob", 17),
|
||||
new Person("Charlie", 25),
|
||||
new Person("David", 15)
|
||||
);
|
||||
|
||||
// 使用 Collectors.partitioningBy 按年龄分组
|
||||
Map<Boolean, List<Person>> partitionedByAge = people.stream()
|
||||
.collect(Collectors.partitioningBy(person -> person.getAge() >= 18));
|
||||
|
||||
// 输出结果
|
||||
System.out.println("成年人:");
|
||||
partitionedByAge.get(true).forEach(person -> System.out.println(person.getName()));
|
||||
|
||||
System.out.println("未成年人:");
|
||||
partitionedByAge.get(false).forEach(person -> System.out.println(person.getName()));
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Person {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
```java
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(name = "BaseVo", title = "基础返回对象内容", description = "基础返回对象内容")
|
||||
public class BaseVo implements Serializable {
|
||||
|
||||
@Schema(name = "id", title = "主键")
|
||||
@JsonProperty("id")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
@JSONField(serializeUsing = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(name = "updateTime", title = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(name = "createTime", title = "发布时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(name = "createUser", title = "创建用户")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
@JSONField(serializeUsing = ToStringSerializer.class)
|
||||
private Long createUser;
|
||||
|
||||
@Schema(name = "updateUser", title = "操作用户")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
@JSONField(serializeUsing = ToStringSerializer.class)
|
||||
private Long updateUser;
|
||||
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
Spring MVC 支持 **Ant 风格**(Ant-style path matching)主要体现在 URL 路径匹配模式上。这种模式是 Spring Web 框架用来进行 URL 模式匹配的一种方式,借用了 Apache Ant(一个流行的 Java 构建工具)中的路径匹配规则。Ant 风格的路径匹配使得 URL 路径映射更加灵活和方便。
|
||||
|
||||
+ `*` 匹配单一路径层级中的任意字符。
|
||||
+ `**` 匹配任意多个路径层级。
|
||||
+ `?` 匹配单个字符。
|
||||
|
||||
## 1. Ant 风格路径匹配规则
|
||||
Ant 风格的路径匹配规则中有几个特殊字符,分别是 `*`、`**` 和 `?`,它们具有不同的匹配意义:
|
||||
|
||||
### (1) `*`(匹配零个或多个字符)
|
||||
+ `*` 可以匹配路径中的任何部分,但只能匹配单一层级中的路径。
|
||||
+ 举个例子,`/foo/*` 可以匹配 `/foo/bar` 或 `/foo/abc`,但不能匹配 `/foo/bar/baz`。
|
||||
+ 示例:
|
||||
- `/foo/*` 匹配 `/foo/bar`。
|
||||
- `/foo/*/bar` 匹配 `/foo/abc/bar`。
|
||||
|
||||
### (2) `**`(匹配零个或多个目录)
|
||||
+ `**` 可以匹配多个目录层级,它比 `*` 更加强大,能够跨越多个层级。
|
||||
+ 示例:
|
||||
- `/foo/**/bar` 匹配 `/foo/bar`、`/foo/abc/bar`、`/foo/abc/def/bar` 等。
|
||||
- `/foo/**/bar/**/baz` 匹配 `/foo/abc/bar/xyz/baz`。
|
||||
|
||||
### (3) `?`(匹配单个字符)
|
||||
+ `?` 用于匹配单个字符,不是零个或多个字符。它通常用于精确匹配某些路径中的单个字符。
|
||||
+ 示例:
|
||||
- `/foo/a?c` 可以匹配 `/foo/abc`,但不能匹配 `/foo/abcc`。
|
||||
|
||||
## 2. Ant 风格的路径匹配应用
|
||||
Spring MVC 采用了这种路径匹配方式,使得映射 URL 路径时更加灵活。例如,使用 `@RequestMapping` 注解来定义控制器方法时,可以利用 Ant 风格的路径匹配规则。
|
||||
|
||||
### 示例 1:`@RequestMapping` 使用 Ant 风格
|
||||
```java
|
||||
@Controller
|
||||
public class MyController {
|
||||
|
||||
@RequestMapping("/foo/*") // 匹配路径 /foo/bar 或 /foo/abc
|
||||
public String handleFoo() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
@RequestMapping("/foo/**") // 匹配路径 /foo/bar 或 /foo/abc/xyz
|
||||
public String handleFooRecursive() {
|
||||
return "fooRecursive";
|
||||
}
|
||||
|
||||
@RequestMapping("/foo/a?c") // 匹配路径 /foo/abc,但不匹配 /foo/abcc
|
||||
public String handleSpecificPattern() {
|
||||
return "specificPattern";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这个例子中:
|
||||
|
||||
+ `/foo/*` 只会匹配 `/foo/bar` 或 `/foo/abc` 等简单路径。
|
||||
+ `/foo/**` 会匹配 `/foo/bar`、`/foo/abc/xyz` 等多层次路径。
|
||||
+ `/foo/a?c` 会匹配 `/foo/abc`,但不会匹配 `/foo/abcc`。
|
||||
|
||||
### 示例 2:`@RequestMapping` 配合请求方法
|
||||
Spring MVC 还支持在映射中结合请求方法(如 `GET`、`POST`)来实现更细粒度的路径匹配:
|
||||
|
||||
```java
|
||||
@Controller
|
||||
@RequestMapping("/foo")
|
||||
public class FooController {
|
||||
|
||||
@RequestMapping(value = "/bar/*", method = RequestMethod.GET) // GET 请求
|
||||
public String handleBar() {
|
||||
return "barGET";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/bar/**", method = RequestMethod.POST) // POST 请求
|
||||
public String handleBarPost() {
|
||||
return "barPOST";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这个例子中:
|
||||
|
||||
+ `/foo/bar/*` 仅在处理 `GET` 请求时匹配。
|
||||
+ `/foo/bar/**` 仅在处理 `POST` 请求时匹配。
|
||||
|
||||
## 3. Ant 风格路径与通配符的结合使用
|
||||
在实际开发中,Spring MVC 支持 Ant 风格路径的同时,还可以与路径变量、正则表达式等功能结合使用。
|
||||
|
||||
### 示例 1:路径变量 + Ant 风格
|
||||
```java
|
||||
@RequestMapping("/user/{id}/**") // 匹配多层路径,id 为路径变量
|
||||
public String handleUser(@PathVariable("id") String userId) {
|
||||
return "User ID: " + userId;
|
||||
}
|
||||
```
|
||||
|
||||
+ 这个路径匹配 `/user/123/abc/xyz`,其中 `id` 会捕获为 `123`。
|
||||
|
||||
### 示例 2:正则表达式 + Ant 风格
|
||||
```java
|
||||
@RequestMapping("/product/{id:\\d+}/**") // 正则匹配数字 id
|
||||
public String handleProduct(@PathVariable("id") String productId) {
|
||||
return "Product ID: " + productId;
|
||||
}
|
||||
```
|
||||
|
||||
+ 这里的路径 `/product/{id:\\d+}/**` 只会匹配数字形式的 `id`,比如 `/product/123/abc/xyz`。
|
||||
|
||||
## 4. 优先级和匹配规则
|
||||
在使用 Ant 风格路径匹配时,路径匹配的优先级有一定的规则。具体来说,`/**` 会匹配任何路径,所以它的优先级通常较低,避免与其他精确匹配的路径冲突。
|
||||
|
||||
### 示例:优先级比较
|
||||
```java
|
||||
@Controller
|
||||
@RequestMapping("/foo")
|
||||
public class FooController {
|
||||
|
||||
@RequestMapping("/foo/{id}") // 精确匹配路径 /foo/{id}
|
||||
public String handleFoo(@PathVariable String id) {
|
||||
return "foo:" + id;
|
||||
}
|
||||
|
||||
@RequestMapping("/foo/**") // 匹配所有以 /foo/ 开头的路径
|
||||
public String handleFooCatchAll() {
|
||||
return "catchAll";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这个例子中,如果访问 `/foo/bar`,它会首先匹配 `/foo/{id}`,因为它更精确。
|
||||
|
||||
##
|
|
@ -0,0 +1,4 @@
|
|||
+ 公共AI网站:[https://chatgptplus.cn/](https://chatgptplus.cn/)
|
||||
+ vue3官网:[https://cn.vuejs.org/](https://cn.vuejs.org/)
|
||||
+ SpringBoot官网:[https://docs.spring.io/spring-boot/index.html](https://docs.spring.io/spring-boot/index.html)
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
## <font style="color:rgb(0, 0, 0);">Spring 表单验证</font>在 Spring MVC 中,表单验证是通过一系列的注解来完成的。
|
||||
### `<font style="color:rgb(0, 0, 0);">@NotNull</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:确保字段值不为空。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:用于字段、方法参数或返回值上,表示该字段不能为空。如果字段为空,将验证失败并返回相应的错误信息。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@NotNull(message = "用户名不能为空")
|
||||
private String username;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@NotEmpty</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:确保字段不为空,并且不为一个空字符串。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:用于字符串、集合等类型,验证字段不仅不能为空,而且不能为空字符串。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@NotEmpty(message = "密码不能为空")
|
||||
private String password;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@NotBlank</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:确保字段不为空,并且不为一个空白字符串(即非空白字符)。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:类似于 </font>`<font style="color:rgb(0, 0, 0);">@NotEmpty</font>`<font style="color:rgb(0, 0, 0);">,但除了不为空,还要求去除空白字符后不能为零长度。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@NotBlank(message = "电子邮件不能为空")
|
||||
private String email;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Size(min, max)</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证字段的大小,适用于字符串、集合、数组等类型。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:可以设置最小值和最大值来限制字段的长度或集合的大小。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Size(min = 6, max = 20, message = "密码长度必须在6到20之间")
|
||||
private String password;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Email</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证字段是否符合有效的电子邮件格式。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:用于验证字符串字段是否为有效的电子邮件地址格式。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Email(message = "请输入有效的电子邮件地址")
|
||||
private String email;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Pattern(regexp)</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:根据正则表达式验证字段值。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:可以根据自定义的正则表达式来验证字段的内容。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Pattern(regexp = "^\\d{10}$", message = "请输入有效的手机号码")
|
||||
private String phoneNumber;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Min(value)</font>`** 和 **`<font style="color:rgb(0, 0, 0);">@Max(value)</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:确保数字类型字段的值在指定范围内。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:</font>`<font style="color:rgb(0, 0, 0);">@Min</font>`<font style="color:rgb(0, 0, 0);"> 用于验证值是否大于等于指定的最小值,</font>`<font style="color:rgb(0, 0, 0);">@Max</font>`<font style="color:rgb(0, 0, 0);"> 用于验证值是否小于等于指定的最大值。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Min(value = 18, message = "年龄不能小于18岁")
|
||||
@Max(value = 100, message = "年龄不能大于100岁")
|
||||
private int age;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@DecimalMin(value)</font>`** 和 **`<font style="color:rgb(0, 0, 0);">@DecimalMax(value)</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:用于验证浮动值是否在指定范围内,类似于 </font>`<font style="color:rgb(0, 0, 0);">@Min</font>`<font style="color:rgb(0, 0, 0);"> 和 </font>`<font style="color:rgb(0, 0, 0);">@Max</font>`<font style="color:rgb(0, 0, 0);">,但适用于 </font>`<font style="color:rgb(0, 0, 0);">BigDecimal</font>`<font style="color:rgb(0, 0, 0);"> 或 </font>`<font style="color:rgb(0, 0, 0);">Double</font>`<font style="color:rgb(0, 0, 0);"> 类型的数值。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:</font>`<font style="color:rgb(0, 0, 0);">@DecimalMin</font>`<font style="color:rgb(0, 0, 0);"> 验证值是否大于等于指定的最小值,</font>`<font style="color:rgb(0, 0, 0);">@DecimalMax</font>`<font style="color:rgb(0, 0, 0);"> 验证值是否小于等于指定的最大值。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@DecimalMin(value = "0.0", inclusive = true, message = "价格不能小于0")
|
||||
private BigDecimal price;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Future</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证日期字段的值是否为将来日期。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:用于 </font>`<font style="color:rgb(0, 0, 0);">java.util.Date</font>`<font style="color:rgb(0, 0, 0);">、</font>`<font style="color:rgb(0, 0, 0);">java.time.LocalDate</font>`<font style="color:rgb(0, 0, 0);"> 或 </font>`<font style="color:rgb(0, 0, 0);">java.time.LocalDateTime</font>`<font style="color:rgb(0, 0, 0);"> 等日期类型的字段。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Future(message = "日期必须是未来的时间")
|
||||
private LocalDate eventDate;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Past</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证日期字段的值是否为过去的日期。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:类似于 </font>`<font style="color:rgb(0, 0, 0);">@Future</font>`<font style="color:rgb(0, 0, 0);">,但是验证日期必须是过去的时间。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Past(message = "出生日期必须是过去的时间")
|
||||
private LocalDate birthDate;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@AssertTrue</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证字段值是否为 </font>`<font style="color:rgb(0, 0, 0);">true</font>`<font style="color:rgb(0, 0, 0);">。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:适用于布尔类型字段,如果值不是 </font>`<font style="color:rgb(0, 0, 0);">true</font>`<font style="color:rgb(0, 0, 0);">,则验证失败。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@AssertTrue(message = "必须接受条款和条件")
|
||||
private boolean acceptedTerms;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@AssertFalse</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证字段值是否为 </font>`<font style="color:rgb(0, 0, 0);">false</font>`<font style="color:rgb(0, 0, 0);">。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:适用于布尔类型字段,如果值不是 </font>`<font style="color:rgb(0, 0, 0);">false</font>`<font style="color:rgb(0, 0, 0);">,则验证失败。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@AssertFalse(message = "不能接受条款")
|
||||
private boolean declinedTerms;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Valid</font>`** 和 **`<font style="color:rgb(0, 0, 0);">@Validated</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:触发嵌套对象的验证。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:当你有嵌套对象(如表单中的对象属性是另一个对象),使用 </font>`<font style="color:rgb(0, 0, 0);">@Valid</font>`<font style="color:rgb(0, 0, 0);"> 或 </font>`<font style="color:rgb(0, 0, 0);">@Validated</font>`<font style="color:rgb(0, 0, 0);"> 注解来递归验证该对象。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Valid
|
||||
private Address address;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Digits(integer, fraction)</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证数字字段的有效性,确保字段值是一个有效的数字,并且整数部分和小数部分的位数符合指定要求。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:</font>`<font style="color:rgb(0, 0, 0);">integer</font>`<font style="color:rgb(0, 0, 0);"> 参数用于指定数字的整数部分的最大位数,</font>`<font style="color:rgb(0, 0, 0);">fraction</font>`<font style="color:rgb(0, 0, 0);"> 参数用于指定小数部分的最大位数。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Digits(integer = 5, fraction = 2, message = "金额应为最大5位整数和2位小数")
|
||||
private BigDecimal amount;
|
||||
```
|
||||
|
||||
- <font style="color:rgb(0, 0, 0);">这个例子验证金额字段的最大值为 </font>`<font style="color:rgb(0, 0, 0);">99999.99</font>`<font style="color:rgb(0, 0, 0);">(即最多5位整数和2位小数)。</font>
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@CreditCardNumber</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证信用卡号的有效性,确保其符合信用卡的常见格式,通常包括 Luhn 算法的验证。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:该注解用于验证信用卡号的格式是否有效。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@CreditCardNumber(message = "请输入有效的信用卡号")
|
||||
private String creditCardNumber;
|
||||
```
|
||||
|
||||
- <font style="color:rgb(0, 0, 0);">该注解会根据常见的信用卡规则(如 VISA、MasterCard 等)验证输入的信用卡号是否合法。</font>
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Range(min, max)</font>`<font style="color:rgb(0, 0, 0);">(不是 Spring 内置的,但通常来自 Hibernate Validator)</font>
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证字段值是否在指定的范围内。常用于 </font>`<font style="color:rgb(0, 0, 0);">Integer</font>`<font style="color:rgb(0, 0, 0);">、</font>`<font style="color:rgb(0, 0, 0);">Long</font>`<font style="color:rgb(0, 0, 0);">、</font>`<font style="color:rgb(0, 0, 0);">Double</font>`<font style="color:rgb(0, 0, 0);"> 等数值类型的字段。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:指定字段的有效范围,当值不在范围内时会验证失败。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Range(min = 1, max = 100, message = "数字必须在1到100之间")
|
||||
private int quantity;
|
||||
```
|
||||
|
||||
- <font style="color:rgb(0, 0, 0);">该注解会验证 </font>`<font style="color:rgb(0, 0, 0);">quantity</font>`<font style="color:rgb(0, 0, 0);"> 字段的值是否在 </font>`<font style="color:rgb(0, 0, 0);">1</font>`<font style="color:rgb(0, 0, 0);"> 到 </font>`<font style="color:rgb(0, 0, 0);">100</font>`<font style="color:rgb(0, 0, 0);"> 之间。</font>
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@URL</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证字段是否为有效的 URL 格式。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:用于字符串类型的字段,验证其是否符合有效的 URL 格式。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@URL(message = "请输入有效的网址")
|
||||
private String website;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Valid</font>`** 与 **`<font style="color:rgb(0, 0, 0);">@Validated</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:用于嵌套对象的验证,确保嵌套对象的字段也进行验证。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:这两个注解会触发嵌套对象的验证,通常用于嵌套的复杂表单数据结构。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Valid
|
||||
private Address address;
|
||||
```
|
||||
|
||||
- <font style="color:rgb(0, 0, 0);">如果 </font>`<font style="color:rgb(0, 0, 0);">Address</font>`<font style="color:rgb(0, 0, 0);"> 类中有字段使用了验证注解,</font>`<font style="color:rgb(0, 0, 0);">@Valid</font>`<font style="color:rgb(0, 0, 0);"> 会递归地验证 </font>`<font style="color:rgb(0, 0, 0);">Address</font>`<font style="color:rgb(0, 0, 0);"> 对象的所有字段。</font>
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@FutureOrPresent</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证日期或时间字段的值是否是当前日期(包括今天)或未来的日期。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:该注解用于日期和时间字段,确保其为今天或将来的日期。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font><font style="color:rgb(0, 0, 0);">:</font>
|
||||
|
||||
```java
|
||||
@FutureOrPresent(message = "事件日期必须是今天或将来")
|
||||
private LocalDate eventDate;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@PastOrPresent</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证日期或时间字段的值是否是当前日期(包括今天)或过去的日期。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:与 </font>`<font style="color:rgb(0, 0, 0);">@FutureOrPresent</font>`<font style="color:rgb(0, 0, 0);"> 相反,确保字段是过去或今天的日期。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示</font>
|
||||
|
||||
```java
|
||||
@PastOrPresent(message = "出生日期必须是过去的时间或今天")
|
||||
private LocalDate birthDate;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@Null</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:验证字段是否为 </font>`<font style="color:rgb(0, 0, 0);">null</font>`<font style="color:rgb(0, 0, 0);">。如果字段不为空,则验证失败。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:该注解可以用于字段或方法参数上,确保字段值必须为 </font>`<font style="color:rgb(0, 0, 0);">null</font>`<font style="color:rgb(0, 0, 0);">。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@Null(message = "该字段必须为null")
|
||||
private String nickname;
|
||||
```
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@ScriptAssert(lang, script)</font>`
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:通过自定义脚本验证字段值。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:允许使用自定义脚本(如 JavaScript)来执行复杂的验证逻辑。需要指定脚本语言和脚本内容。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@ScriptAssert(lang = "javascript", script = "_this.password == _this.confirmPassword", message = "密码和确认密码必须一致")
|
||||
private String password;
|
||||
private String confirmPassword;
|
||||
```
|
||||
|
||||
- <font style="color:rgb(0, 0, 0);">这个注解可以用于检查两个字段值是否一致。</font>
|
||||
|
||||
### `<font style="color:rgb(0, 0, 0);">@UniqueElements</font>`<font style="color:rgb(0, 0, 0);">(Hibernate Validator 扩展)</font>
|
||||
+ **作用**<font style="color:rgb(0, 0, 0);">:确保集合中的元素是唯一的,常用于 List 或 Set 类型字段。</font>
|
||||
+ **用法**<font style="color:rgb(0, 0, 0);">:适用于集合类型字段,确保集合中的元素不重复。</font>
|
||||
+ <font style="color:rgb(0, 0, 0);">示例</font>
|
||||
|
||||
```java
|
||||
@UniqueElements(message = "列表中的元素必须唯一")
|
||||
private List<String> tags;
|
||||
```
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
## <font style="color:rgb(0, 0, 0);">SpringSecurity</font>
|
||||
### 密码转换器(Password Encoder)
|
||||
Spring Security 提供了多种密码转换器(Password Encoder),这些转换器用于对用户密码进行加密和验证。常见的密码转换器包括:
|
||||
|
||||
1. **BCryptPasswordEncoder**:
|
||||
- 使用 **BCrypt** 算法对密码进行加密。
|
||||
- 是最常用的密码加密方案,具有强大的加密性,并且支持自动加盐(salt),防止暴力破解攻击。
|
||||
- 示例:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
```
|
||||
|
||||
2. **NoOpPasswordEncoder**:
|
||||
- 不对密码进行加密,直接返回明文密码。
|
||||
- 主要用于开发和测试环境,**不推荐在生产环境中使用**。
|
||||
- 示例:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
}
|
||||
```
|
||||
|
||||
3. **Pbkdf2PasswordEncoder**:
|
||||
- 使用 **PBKDF2** 算法进行密码加密。
|
||||
- 提供较强的安全性,并且支持对密码进行哈希。
|
||||
- 示例:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new Pbkdf2PasswordEncoder();
|
||||
}
|
||||
```
|
||||
|
||||
4. **Argon2PasswordEncoder**:
|
||||
- 使用 **Argon2** 算法对密码进行加密。
|
||||
- Argon2 是目前被认为最强的密码哈希算法,支持内存密集型计算,从而防止硬件加速破解。
|
||||
- 示例:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new Argon2PasswordEncoder();
|
||||
}
|
||||
```
|
||||
|
||||
5. **SCryptPasswordEncoder**:
|
||||
- 使用 **SCrypt** 算法进行密码加密。
|
||||
- SCrypt 是另一种内存密集型的密码加密算法,与 Argon2 类似,旨在防止硬件加速破解。
|
||||
- 示例:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new SCryptPasswordEncoder();
|
||||
}
|
||||
```
|
||||
|
||||
6. **MessageDigestPasswordEncoder** (已废弃):
|
||||
- 基于 **MessageDigest** 算法进行加密(如 SHA-1、SHA-256 等)。
|
||||
- 由于缺乏盐和密钥加密机制,已被其他更强的加密方式所替代。
|
||||
|
||||
> 选择密码转换器的建议:
|
||||
>
|
||||
> + 在现代应用中,推荐使用 **BCryptPasswordEncoder** 或 **Argon2PasswordEncoder**,这两种算法提供了强大的加密性。
|
||||
> + **Pbkdf2PasswordEncoder** 和 **SCryptPasswordEncoder** 也可以作为备选方案,尤其是当你希望加密算法能够承受更多资源密集型攻击时。
|
||||
> + **NoOpPasswordEncoder** 仅限于开发和测试环境。
|
||||
>
|
||||
|
||||
### 访问主页
|
||||
需要使用`http://localhost:8080/index`来访问主页,可以在配置中配置,访问根路径直接跳转
|
||||
|
||||
```java
|
||||
@RequestMapping
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@Operation(summary = "主页内容")
|
||||
@GetMapping("index")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Operation(summary = "订单内容")
|
||||
@GetMapping("order")
|
||||
public String order() {
|
||||
return "order";
|
||||
}
|
||||
|
||||
@Operation(summary = "login")
|
||||
@GetMapping("login")
|
||||
public String login() {
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在配置中
|
||||
|
||||
```java
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/")
|
||||
// .setViewName("forward:/index") //两种方式写法
|
||||
.setViewName("index");
|
||||
registry.addViewController("/login");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义登录
|
||||
```java
|
||||
package cn.bunny.springdemo.configuration;
|
||||
|
||||
import cn.bunny.springdemo.dao.entity.AdminUser;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new SCryptPasswordEncoder(4, 8, 1, 8, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用内存方式
|
||||
*
|
||||
* @param encoder 密码加密器
|
||||
* @return 基于内存的用户
|
||||
*/
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
|
||||
ArrayList<UserDetails> userDetails = new ArrayList<>();
|
||||
userDetails.add(new AdminUser("admin", encoder.encode("admin"), true));
|
||||
userDetails.add(new AdminUser("bunny", encoder.encode("password"), true));
|
||||
|
||||
return new InMemoryUserDetailsManager(userDetails);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 使用数据库方式
|
||||
// *
|
||||
// * @param userService 获取用户数据(如数据库)
|
||||
// * @return 基于数据库的用户
|
||||
// */
|
||||
// @Bean
|
||||
// public UserDetailsService userDetailsService(UserService userService) {
|
||||
// return username -> {
|
||||
// AdminUser adminUser = userService.getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getUsername, userService));
|
||||
// if (adminUser != null) {
|
||||
// return adminUser;
|
||||
// }
|
||||
// throw new UsernameNotFoundException("未找到 AdminUser");
|
||||
// };
|
||||
// }
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity security) throws Exception {
|
||||
return security
|
||||
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
|
||||
authorizationManagerRequestMatcherRegistry
|
||||
.requestMatchers("/order").hasRole("USER")// 请求需要含有USER角色
|
||||
.requestMatchers("/", "/index", "/login", "/images/**").permitAll()
|
||||
)
|
||||
/* 自定义登录页面 */
|
||||
// .formLogin(AbstractAuthenticationFilterConfigurer::permitAll)// 默认的登录页会自动启用,无需额外配置
|
||||
.formLogin(formLoginConfigurer -> formLoginConfigurer
|
||||
.loginPage("/login")
|
||||
// .loginProcessingUrl("/authenticate")
|
||||
.usernameParameter("username")// 自定义用户名名称
|
||||
.usernameParameter("password")// 自定义密码名称
|
||||
.defaultSuccessUrl("/index")// 登录成功后默认跳转页面
|
||||
// .defaultSuccessUrl("/index", true)// 登录成功后默认跳转页面,如果用户之前访问页面也需要强制跳转到 /index 可以传递第二个参数
|
||||
)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSRF 伪造
|
||||
通常在自定义登录页面中加入
|
||||
|
||||
```html
|
||||
<label>
|
||||
<input name="_csrf" placeholder="_csrf" th:value="${_csrf.token}" type="hidden"/>
|
||||
</label>
|
||||
|
||||
```
|
||||
|
||||
如果需要禁用
|
||||
|
||||
```java
|
||||
.csrf(AbstractHttpConfigurer::disable)// 前后端分离可以禁用
|
||||
```
|
||||
|
||||
### 开发授权服务器
|
||||
```xml
|
||||
<!--资源服务器-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-resource server</artifactId>
|
||||
</dependency>
|
||||
<!-- 客户应用 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<!-- 授权服务器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,158 @@
|
|||
# 权限管理设计基础数据字典
|
||||
https://www.cnblogs.com/myindex/p/9116177.html
|
||||
|
||||
## 1.用户信息表:
|
||||
| **用户信息表****(UserInfo)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| 用户ID | ID | Int | PK not null |
|
||||
| 用户名 | UserName | Varchar(20) | not null |
|
||||
|
||||
|
||||
## 2.角色表:
|
||||
| **角色表****(Role)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| 角色ID | ID | Int | PK not null |
|
||||
| 角色名 | RoleName | Varchar(30) | not null |
|
||||
|
||||
|
||||
## 3.用户与角色关联表
|
||||
| **用户与角色关联表****(User_Role)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 用户ID | UserId | Int | FK not null |
|
||||
| 角色ID | RoleId | Int | FK not null |
|
||||
|
||||
|
||||
## 4.用户组表
|
||||
| **用户组表****(UserGroup)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| 用户组ID | ID | Int | PK not null |
|
||||
| 用户组名 | UserGroupName | Varchar(30) | not null |
|
||||
|
||||
|
||||
## 5.用户组与用户信息关联表
|
||||
| **用户组与用户信息关联表****(UserGroup_UserInfo)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 用户组ID | UserGroupId | Int | FK not null |
|
||||
| 用户ID | UserId | Int | FK not null |
|
||||
|
||||
|
||||
## 6.用户组与角色关联表
|
||||
| **用户组与角色关联表****(UserGroup_Role)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 用户组ID | UserGroupId | Int | FK not null |
|
||||
| 角色ID | RoleId | Int | FK not null |
|
||||
|
||||
|
||||
## 7.菜单表
|
||||
| **菜单表****(Menu)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 菜单名称 | MenuName | Varchar(30) | not null |
|
||||
| 菜单URL | MenuUrl | Varchar(100) | |
|
||||
| 菜单父ID | ParentId | Int | |
|
||||
|
||||
|
||||
## 8.页面元素表
|
||||
| **页面元素表****(PageElement)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 页面元素名称 | PageElementName | Varchar(100) | not null |
|
||||
|
||||
|
||||
## 9.文件表
|
||||
| **文件表****(File)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 文件名称 | FileName | Varchar(50) | not null |
|
||||
| 文件路径 | FilePath | Varchar(100) | |
|
||||
|
||||
|
||||
## 10.权限表
|
||||
| **权限表****(Power)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 权限类型 | PowerType | Varchar(50) | not null |
|
||||
|
||||
|
||||
## 11.权限与菜单关联表
|
||||
| **权限与菜单关联表****(Power_Menu)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 权限ID | PowerId | Int | FK not null |
|
||||
| 菜单ID | MenuId | Int | FK not null |
|
||||
|
||||
|
||||
## 12.权限与页面元素关联表
|
||||
| **权限与页面元素关联表****(Power_Element)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 权限ID | PowerId | Int | FK not null |
|
||||
| 页面元素ID | ElementId | Int | FK not null |
|
||||
|
||||
|
||||
## 13.权限与文件关联表
|
||||
| **权限与文件关联表****(Power_File)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 权限ID | PowerId | Int | FK not null |
|
||||
| 文件ID | FileId | Int | FK not null |
|
||||
|
||||
|
||||
## 14.功能操作表
|
||||
| **功能操作表****(Operation)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 操作名称 | OperationName | Varchar(50) | not null |
|
||||
| 操作编码 | OperationCode | Varchar(50) | |
|
||||
| 拦截URL前缀 | Ljurlqz | Varchar(100) | |
|
||||
| 操作父ID | ParentId | Int | |
|
||||
|
||||
|
||||
## 15.权限与功能操作关联表
|
||||
| **权限与功能操作关联表****(Power_Operation)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 权限ID | PowerId | Int | FK not null |
|
||||
| 操作ID | OperationId | Int | FK not null |
|
||||
|
||||
|
||||
## 16.角色与权限关联表
|
||||
| **角色与权限关联表****(Role_Power)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 角色ID | RoleId | Int | FK not null |
|
||||
| 权限ID | PowerId | Int | FK not null |
|
||||
|
||||
|
||||
## 17.操作日志表
|
||||
| **操作日志表****(OperationLog)** | | | |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段名称 | 字段 | 类型 | 备注 |
|
||||
| ID | ID | Int | PK not null |
|
||||
| 操作类型Id | OperationTypeId | Int | FK not null |
|
||||
| 操作内容 | OperationContent | Varchar(500) | |
|
||||
| 操作用户ID | OperationUserId | Int | FK not null |
|
||||
| 操作时间 | OperationTime | Date | |
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
## <font style="color:rgb(0, 0, 0);">向application域共享数据</font>
|
||||
在 Spring MVC 中,`Application` 域(也称为 **ServletContext**)是一个全局范围,用于在整个应用程序中共享数据。不同于 **Session** 域和 **Request** 域,`Application` 域中的数据对整个 Web 应用的所有用户都是可见的,因此适合存储全局共享的配置信息、常量、初始化数据等。
|
||||
|
||||
在 Spring MVC 中,我们可以通过 `ServletContext` 来向 **Application** 域共享数据,通常使用 `setAttribute` 和 `getAttribute` 方法来进行操作。
|
||||
|
||||
### 1. **通过 **`ServletContext`** 共享数据**
|
||||
`ServletContext` 是与整个 Web 应用程序相关联的对象,它允许你在多个请求和多个会话之间共享数据。在 Spring MVC 中,可以通过以下两种方式来访问 `ServletContext`:
|
||||
|
||||
+ 直接使用 `HttpServletRequest.getSession().getServletContext()` 获取 `ServletContext`。
|
||||
+ 使用 `@Autowired` 注解注入 `ServletContext` 对象。
|
||||
|
||||
#### 示例 1:通过 `ServletContext` 共享数据
|
||||
##### 1.1 通过 `HttpServletRequest` 获取 `ServletContext`
|
||||
```java
|
||||
@Controller
|
||||
public class ApplicationController {
|
||||
|
||||
@RequestMapping("/setApplicationData")
|
||||
public String setApplicationData(HttpServletRequest request) {
|
||||
// 获取 ServletContext
|
||||
ServletContext servletContext = request.getServletContext();
|
||||
// 向 Application 域存储数据
|
||||
servletContext.setAttribute("appName", "SpringMVCApp");
|
||||
servletContext.setAttribute("version", "1.0.0");
|
||||
return "applicationDataSet";
|
||||
}
|
||||
|
||||
@RequestMapping("/getApplicationData")
|
||||
public String getApplicationData(HttpServletRequest request, Model model) {
|
||||
// 获取 ServletContext
|
||||
ServletContext servletContext = request.getServletContext();
|
||||
// 从 Application 域获取数据
|
||||
String appName = (String) servletContext.getAttribute("appName");
|
||||
String version = (String) servletContext.getAttribute("version");
|
||||
|
||||
model.addAttribute("appName", appName);
|
||||
model.addAttribute("version", version);
|
||||
return "applicationData";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**URL 请求:**
|
||||
|
||||
+ `GET /setApplicationData` 会向 `Application` 域中存储 `"appName": "SpringMVCApp"` 和 `"version": "1.0.0"`。
|
||||
+ `GET /getApplicationData` 会从 `Application` 域中读取数据,并返回给视图。
|
||||
|
||||
##### 1.2 通过 `@Autowired` 注入 `ServletContext`
|
||||
```java
|
||||
@Controller
|
||||
public class ApplicationController {
|
||||
|
||||
@Autowired
|
||||
private ServletContext servletContext;
|
||||
|
||||
@RequestMapping("/setApplicationData")
|
||||
public String setApplicationData() {
|
||||
// 向 Application 域存储数据
|
||||
servletContext.setAttribute("appName", "SpringMVCApp");
|
||||
servletContext.setAttribute("version", "1.0.0");
|
||||
return "applicationDataSet";
|
||||
}
|
||||
|
||||
@RequestMapping("/getApplicationData")
|
||||
public String getApplicationData(Model model) {
|
||||
// 从 Application 域获取数据
|
||||
String appName = (String) servletContext.getAttribute("appName");
|
||||
String version = (String) servletContext.getAttribute("version");
|
||||
|
||||
model.addAttribute("appName", appName);
|
||||
model.addAttribute("version", version);
|
||||
return "applicationData";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这个例子中,`@Autowired` 注解会自动将 `ServletContext` 注入到控制器中。
|
||||
|
||||
### 2. **应用场景**
|
||||
将数据放到 `Application` 域中通常用于存储以下类型的数据:
|
||||
|
||||
+ **应用级别的数据**:例如应用名称、版本号、初始化配置等,这些数据是全局共享的。
|
||||
+ **常量和初始化信息**:如果有一些需要在多个请求中共享的常量或初始化信息,可以将它们放到 `Application` 域中。
|
||||
+ **数据库连接池或常用资源**:对于一些全局共享的资源(如数据库连接池),可以在 `Application` 域中进行配置并在不同的请求中共享。
|
||||
|
||||
### 3. **注意事项**
|
||||
+ **全局共享**:与 `Session` 和 `Request` 域不同,`Application` 域中的数据对所有用户和请求都可见。因此,要特别小心在 `Application` 域中存储敏感数据,避免泄漏用户个人信息等。
|
||||
+ **生命周期**:`Application` 域中的数据在整个应用程序生命周期内有效,直到应用服务器重新启动。因此,放入 `Application` 域的数据一般是全局的、不会频繁变化的。
|
||||
+ **线程安全**:`Application` 域的数据是共享的,因此在并发访问时要考虑线程安全问题。如果有多个线程访问同一数据,可能需要进行同步。
|
||||
|
||||
### 4. **清理 Application 域中的数据**
|
||||
如果不再需要某个共享数据,可以使用 `removeAttribute` 方法从 `Application` 域中移除该数据。
|
||||
|
||||
#### 示例:删除 `Application` 域中的数据
|
||||
```java
|
||||
@Controller
|
||||
public class ApplicationController {
|
||||
|
||||
@RequestMapping("/removeApplicationData")
|
||||
public String removeApplicationData(HttpServletRequest request) {
|
||||
// 获取 ServletContext
|
||||
ServletContext servletContext = request.getServletContext();
|
||||
// 从 Application 域移除数据
|
||||
servletContext.removeAttribute("appName");
|
||||
servletContext.removeAttribute("version");
|
||||
return "applicationDataRemoved";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**URL 请求:**
|
||||
|
||||
+ `GET /removeApplicationData` 会从 `Application` 域中移除 `appName` 和 `version` 属性。
|
||||
|
||||
### 5. **使用 **`@ApplicationScope`**(Spring 方式)**
|
||||
如果使用 Spring 框架进行开发,也可以使用 Spring 提供的 `@ApplicationScope` 注解来定义在整个应用范围内共享的 Bean。这种方法通常用于 Spring 组件,而不是直接操作 `ServletContext`。
|
||||
|
||||
#### 示例:使用 `@ApplicationScope`
|
||||
```java
|
||||
@Component
|
||||
@Scope("application")
|
||||
public class AppConfig {
|
||||
private String appName = "SpringMVCApp";
|
||||
|
||||
public String getAppName() {
|
||||
return appName;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这种情况下,Spring 管理的 Bean 会在应用级别共享,类似于 `ServletContext` 中存储的数据。
|
||||
|
||||
> + `Application` 域(即 `ServletContext`)用于在整个应用程序范围内共享数据,适合存储全局共享的信息、配置和常量。
|
||||
> + 通过 `HttpServletRequest.getServletContext()` 或 `@Autowired` 注解可以访问 `ServletContext` 并向 `Application` 域中共享数据。
|
||||
> + 数据存储在 `Application` 域中可以在整个应用程序生命周期内有效,适用于共享全局性的、无需频繁更新的数据。
|
||||
> + 应谨慎存储敏感数据,并注意线程安全和数据的生命周期。
|
||||
>
|
||||
|
||||
##
|
|
@ -0,0 +1,194 @@
|
|||
## <font style="color:rgb(0, 0, 0);">向session共享数据</font>
|
||||
在 Spring MVC 中,**Session** 是用于存储用户会话期间的数据的一种机制。每个用户访问的应用程序都将拥有一个唯一的会话。通过 `HttpSession`,可以在用户的会话中存储一些数据,直到用户关闭浏览器或会话过期。
|
||||
|
||||
<font style="color:rgb(0, 0, 0);">Spring MVC 提供了多种方式来与 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);"> 进行交互,下面详细介绍如何通过 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);"> 向 Session 共享数据。</font>
|
||||
|
||||
### <font style="color:rgb(0, 0, 0);">1. </font>**通过 **`<font style="color:rgb(0, 0, 0);">HttpSession</font>`** 操作 Session 数据**
|
||||
<font style="color:rgb(0, 0, 0);">在 Spring MVC 控制器中,您可以通过 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);"> 对象来存储和读取会话数据。</font>
|
||||
|
||||
#### <font style="color:rgb(0, 0, 0);">示例:将数据添加到 Session</font>
|
||||
```java
|
||||
@Controller
|
||||
public class SessionController {
|
||||
|
||||
@RequestMapping("/setSessionData")
|
||||
public String setSessionData(HttpSession session) {
|
||||
// 将数据存储到 Session 中
|
||||
session.setAttribute("username", "JohnDoe");
|
||||
session.setAttribute("age", 30);
|
||||
return "sessionSet"; // 返回视图名
|
||||
}
|
||||
|
||||
@RequestMapping("/getSessionData")
|
||||
public String getSessionData(HttpSession session, Model model) {
|
||||
// 从 Session 中获取数据
|
||||
String username = (String) session.getAttribute("username");
|
||||
Integer age = (Integer) session.getAttribute("age");
|
||||
model.addAttribute("username", username);
|
||||
model.addAttribute("age", age);
|
||||
return "sessionData"; // 返回视图名
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**URL 请求:**
|
||||
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /setSessionData</font>`<font style="color:rgb(0, 0, 0);"> 会将数据 </font>`<font style="color:rgb(0, 0, 0);">"username": "JohnDoe"</font>`<font style="color:rgb(0, 0, 0);"> 和 </font>`<font style="color:rgb(0, 0, 0);">"age": 30</font>`<font style="color:rgb(0, 0, 0);"> 存储到 Session 中。</font>
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /getSessionData</font>`<font style="color:rgb(0, 0, 0);"> 会从 Session 中获取并显示存储的值。</font>
|
||||
|
||||
### <font style="color:rgb(0, 0, 0);">2. </font>**使用 **`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`** 注解**
|
||||
`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`<font style="color:rgb(0, 0, 0);"> 注解用于将控制器中的某些模型属性放入 Session 中。这种方式比直接操作 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);"> 更为方便和简洁,特别是当需要共享多个模型属性时。</font>
|
||||
|
||||
#### <font style="color:rgb(0, 0, 0);">示例:使用 </font>`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`
|
||||
```java
|
||||
@Controller
|
||||
@SessionAttributes("user")
|
||||
public class UserController {
|
||||
|
||||
// 在模型中添加用户对象
|
||||
@RequestMapping("/setUser")
|
||||
public String setUser(Model model) {
|
||||
User user = new User("John", 30);
|
||||
model.addAttribute("user", user);
|
||||
return "userSet";
|
||||
}
|
||||
|
||||
// 从 Session 中获取用户对象
|
||||
@RequestMapping("/getUser")
|
||||
public String getUser(@ModelAttribute("user") User user, Model model) {
|
||||
model.addAttribute("user", user);
|
||||
return "userDetails";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**URL 请求:**
|
||||
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /setUser</font>`<font style="color:rgb(0, 0, 0);"> 会将 </font>`<font style="color:rgb(0, 0, 0);">user</font>`<font style="color:rgb(0, 0, 0);"> 对象放入 Session 中。</font>
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /getUser</font>`<font style="color:rgb(0, 0, 0);"> 会从 Session 中获取 </font>`<font style="color:rgb(0, 0, 0);">user</font>`<font style="color:rgb(0, 0, 0);"> 对象。</font>
|
||||
|
||||
`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`<font style="color:rgb(0, 0, 0);"> 注解不仅可以放入 Session 中,还可以与 </font>`<font style="color:rgb(0, 0, 0);">@ModelAttribute</font>`<font style="color:rgb(0, 0, 0);"> 注解结合使用,确保模型数据保持在 Session 中。</font>
|
||||
|
||||
### <font style="color:rgb(0, 0, 0);">3. </font>**使用 **`<font style="color:rgb(0, 0, 0);">@ModelAttribute</font>`** 注解**
|
||||
`<font style="color:rgb(0, 0, 0);">@ModelAttribute</font>`<font style="color:rgb(0, 0, 0);"> 注解允许将数据放入模型中,并且在方法调用前通过 </font>`<font style="color:rgb(0, 0, 0);">Model</font>`<font style="color:rgb(0, 0, 0);"> 传递给视图。如果和 </font>`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`<font style="color:rgb(0, 0, 0);"> 一起使用,它可以将属性直接添加到 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);">。</font>
|
||||
|
||||
#### <font style="color:rgb(0, 0, 0);">示例:使用 </font>`<font style="color:rgb(0, 0, 0);">@ModelAttribute</font>`<font style="color:rgb(0, 0, 0);"> 和 </font>`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`
|
||||
```java
|
||||
@Controller
|
||||
@SessionAttributes("cart")
|
||||
public class CartController {
|
||||
|
||||
// 在模型中创建并存储购物车
|
||||
@ModelAttribute("cart")
|
||||
public Cart createCart() {
|
||||
return new Cart(); // 创建一个空的购物车对象
|
||||
}
|
||||
|
||||
// 添加商品到购物车
|
||||
@RequestMapping("/addToCart")
|
||||
public String addToCart(@ModelAttribute("cart") Cart cart, @RequestParam("item") String item) {
|
||||
cart.addItem(item); // 将商品添加到购物车
|
||||
return "cartUpdated";
|
||||
}
|
||||
|
||||
// 显示购物车内容
|
||||
@RequestMapping("/viewCart")
|
||||
public String viewCart(@ModelAttribute("cart") Cart cart, Model model) {
|
||||
model.addAttribute("cart", cart);
|
||||
return "viewCart";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**URL 请求:**
|
||||
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /addToCart?item=Apple</font>`<font style="color:rgb(0, 0, 0);"> 会将 </font>`<font style="color:rgb(0, 0, 0);">Apple</font>`<font style="color:rgb(0, 0, 0);"> 添加到 </font>`<font style="color:rgb(0, 0, 0);">cart</font>`<font style="color:rgb(0, 0, 0);"> 中。</font>
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /viewCart</font>`<font style="color:rgb(0, 0, 0);"> 会显示购物车中的内容。</font>
|
||||
|
||||
### <font style="color:rgb(0, 0, 0);">4. </font>**通过 **`<font style="color:rgb(0, 0, 0);">@RequestParam</font>`** 或 **`<font style="color:rgb(0, 0, 0);">@PathVariable</font>`** 获取 Session 数据**
|
||||
<font style="color:rgb(0, 0, 0);">如果在请求中需要通过路径变量或请求参数传递数据并存储到 Session 中,可以结合 </font>`<font style="color:rgb(0, 0, 0);">@RequestParam</font>`<font style="color:rgb(0, 0, 0);"> 或 </font>`<font style="color:rgb(0, 0, 0);">@PathVariable</font>`<font style="color:rgb(0, 0, 0);"> 来实现。</font>
|
||||
|
||||
#### <font style="color:rgb(0, 0, 0);">示例:使用 </font>`<font style="color:rgb(0, 0, 0);">@RequestParam</font>`<font style="color:rgb(0, 0, 0);"> 存储 Session 数据</font>
|
||||
```java
|
||||
@Controller
|
||||
public class SessionController {
|
||||
|
||||
@RequestMapping("/setSession/{username}")
|
||||
public String setSessionData(@PathVariable("username") String username, HttpSession session) {
|
||||
session.setAttribute("username", username);
|
||||
return "sessionSet";
|
||||
}
|
||||
|
||||
@RequestMapping("/getSession")
|
||||
public String getSessionData(HttpSession session, Model model) {
|
||||
String username = (String) session.getAttribute("username");
|
||||
model.addAttribute("username", username);
|
||||
return "sessionData";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**URL 请求:**
|
||||
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /setSession/JohnDoe</font>`<font style="color:rgb(0, 0, 0);"> 会将 </font>`<font style="color:rgb(0, 0, 0);">"username": "JohnDoe"</font>`<font style="color:rgb(0, 0, 0);"> 存储到 Session 中。</font>
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /getSession</font>`<font style="color:rgb(0, 0, 0);"> 会从 Session 中获取并显示 </font>`<font style="color:rgb(0, 0, 0);">username</font>`<font style="color:rgb(0, 0, 0);">。</font>
|
||||
|
||||
### <font style="color:rgb(0, 0, 0);">5. </font>**删除 Session 数据**
|
||||
<font style="color:rgb(0, 0, 0);">如果希望在某个操作后清除 Session 中的某些数据,可以使用 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);"> 提供的 </font>`<font style="color:rgb(0, 0, 0);">removeAttribute</font>`<font style="color:rgb(0, 0, 0);"> 方法。</font>
|
||||
|
||||
#### <font style="color:rgb(0, 0, 0);">示例:删除 Session 数据</font>
|
||||
```java
|
||||
@Controller
|
||||
public class SessionController {
|
||||
|
||||
@RequestMapping("/removeSessionData")
|
||||
public String removeSessionData(HttpSession session) {
|
||||
session.removeAttribute("username"); // 删除指定的属性
|
||||
return "sessionRemoved";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**URL 请求:**
|
||||
|
||||
+ `<font style="color:rgb(0, 0, 0);">GET /removeSessionData</font>`<font style="color:rgb(0, 0, 0);"> 会从 Session 中删除 </font>`<font style="color:rgb(0, 0, 0);">"username"</font>`<font style="color:rgb(0, 0, 0);"> 属性。</font>
|
||||
|
||||
### <font style="color:rgb(0, 0, 0);">6. </font>**Session 过期与清理**
|
||||
<font style="color:rgb(0, 0, 0);">默认情况下,Spring MVC 的 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);"> 会话会在用户关闭浏览器后过期,或者会话超时(默认30分钟)。可以在 </font>`<font style="color:rgb(0, 0, 0);">web.xml</font>`<font style="color:rgb(0, 0, 0);"> 或应用的配置类中设置会话超时:</font>
|
||||
|
||||
#### <font style="color:rgb(0, 0, 0);">示例:设置 Session 超时</font>
|
||||
```xml
|
||||
<session-config>
|
||||
<session-timeout>30</session-timeout> <!-- 设置会话超时为30分钟 -->
|
||||
</session-config>
|
||||
|
||||
```
|
||||
|
||||
### <font style="color:rgb(0, 0, 0);">7. </font>**通过 Spring 配置 Session**
|
||||
<font style="color:rgb(0, 0, 0);">通过 Spring 配置文件或 Java 配置类,还可以控制 Session 的相关行为(如会话过期时间、session 的持久化等)。</font>
|
||||
|
||||
#### <font style="color:rgb(0, 0, 0);">示例:Java 配置类</font>
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SessionInterceptor()).addPathPatterns("/**");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`<font style="color:rgb(0, 0, 0);">SessionInterceptor</font>`<font style="color:rgb(0, 0, 0);"> 可以用于监控和管理 Session 数据。</font>
|
||||
|
||||
> <font style="color:rgb(0, 0, 0);">在 Spring MVC 中,向 Session 共享数据主要有以下几种方式:</font>
|
||||
>
|
||||
> + `<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);">:通过 </font>`<font style="color:rgb(0, 0, 0);">HttpSession</font>`<font style="color:rgb(0, 0, 0);"> 对象存储和读取 Session 数据。</font>
|
||||
> + `<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`<font style="color:rgb(0, 0, 0);">:通过 </font>`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`<font style="color:rgb(0, 0, 0);"> 注解将模型属性添加到 Session 中。</font>
|
||||
> + `<font style="color:rgb(0, 0, 0);">@ModelAttribute</font>`<font style="color:rgb(0, 0, 0);">:结合 </font>`<font style="color:rgb(0, 0, 0);">@SessionAttributes</font>`<font style="color:rgb(0, 0, 0);"> 使用,将模型数据持久化到 Session 中。</font>
|
||||
> + `<font style="color:rgb(0, 0, 0);">@RequestParam</font>`** 和 **`<font style="color:rgb(0, 0, 0);">@PathVariable</font>`<font style="color:rgb(0, 0, 0);">:将请求参数或路径变量存储到 Session 中。</font>
|
||||
> + **Session 过期与清理**<font style="color:rgb(0, 0, 0);">:可以通过配置控制会话超时,或手动清除 Session 数据。</font>
|
||||
>
|
||||
|
||||
##
|
|
@ -0,0 +1,120 @@
|
|||
## `@SpringBootApplication` 注解
|
||||
1. `@EnableAutoConfiguration`:
|
||||
- 这个注解让 Spring Boot 根据项目中的依赖,自动配置 Spring 应用程序。Spring Boot 提供了大量的自动配置支持,帮助我们省去手动配置许多常见的功能。
|
||||
- 例如,如果你的项目中加入了 `spring-boot-starter-web` 依赖,Spring Boot 会自动配置一个嵌入式的 Tomcat 服务器和一些常见的 Web 功能。
|
||||
2. `@ComponentScan`:
|
||||
- 这个注解启用 Spring 的组件扫描机制。它会扫描当前类所在的包及其子包,自动发现并注册 `@Component`、`@Service`、`@Repository`、`@Controller` 等注解标注的类。
|
||||
- 通过 `@ComponentScan`,你不需要手动指定要扫描的包,Spring Boot 会自动扫描当前包及其子包下的所有组件。
|
||||
3. `@Configuration`:
|
||||
- 这个注解表示该类是一个 Spring 配置类,类似于 XML 配置文件,用于定义 Spring 应用的 Bean 配置。
|
||||
- 该类可以包含 `@Bean` 注解的方法,返回要管理的 Bean。
|
||||
|
||||
## `@SpringBootTest` 注解
|
||||
`@SpringBootTest` 注解是用于测试 Spring Boot 应用的一个重要注解,它提供了一种方便的方式来启动 Spring Boot 应用上下文,并对整个 Spring Boot 应用进行集成测试。这个注解本身也包含了多个注解,它使得我们能够在测试类中创建一个完整的 Spring 容器来进行集成测试。
|
||||
|
||||
具体来说,`@SpringBootTest` 是一个组合注解,它整合了以下几个主要的注解:
|
||||
|
||||
1. `@ContextConfiguration`:
|
||||
- `@ContextConfiguration` 注解用于加载 Spring 配置文件或者配置类,在测试时会初始化 Spring 容器。`@SpringBootTest` 默认会加载 Spring Boot 应用的主配置类(即包含 `@SpringBootApplication` 注解的类),作为 Spring 容器的上下文。
|
||||
- 它的作用是让测试类能够加载到 Spring 配置并创建一个完整的应用上下文。
|
||||
2. `@TestExecutionListeners`:
|
||||
- 该注解指定了测试执行时的监听器。在 Spring 测试框架中,`@TestExecutionListeners` 会提供某些扩展功能,如事务管理、环境配置等,但它的实际作用在大多数测试中不太明显,通常由 Spring Boot 自动配置。
|
||||
3. `@DirtiesContext`:
|
||||
- 这个注解会告诉 Spring 在测试执行之后清除(或重置)应用上下文,通常用于测试中的应用上下文需要被清理或重置,以避免测试间的相互影响。`@SpringBootTest` 会根据需要处理上下文的清理工作。
|
||||
4. `@BootstrapWith`:
|
||||
- 这个注解是用于引导测试的,它会指定 `SpringBootTestContextBootstrapper` 来启动 Spring Boot 测试上下文。这是一个 Spring Boot 测试框架中的内部机制,用于初始化应用上下文并准备测试。
|
||||
|
||||
## 测试页面
|
||||
编写控制界面,返回`index.html`
|
||||
|
||||
```java
|
||||
@RequestMapping
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@Operation(summary = "主页内容")
|
||||
@GetMapping("index")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
测试页面返回结果
|
||||
|
||||
```java
|
||||
@WebMvcTest(HomeController.class)
|
||||
class HomeControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void index() throws Exception {
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/"))// 访问路径
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())// 判断状态是否成功
|
||||
.andExpect(MockMvcResultMatchers.view().name("index"))// 判断视图名称是否是index
|
||||
// 是否包含字段
|
||||
.andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("欢迎。。。")));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
访问index的页面
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Index 测试页面</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>欢迎。。。</h1>
|
||||
<img alt="" th:src="@{/images/icon_3.png}">
|
||||
<img alt="通常MVC项目静态资源放在 static/images 下" src="images/index/diannao.png">
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
> 静态资源问题,如果用户是访问静态资源,如果直接写在`template`方式引入资源会找不到,因为默认资源都放在`static`目录下载的。
|
||||
>
|
||||
|
||||
### 什么时候该用这种测试
|
||||
#### 1. 验证控制器的业务逻辑
|
||||
+ `@WebMvcTest` 本质上是用来验证 **控制器层的行为**,而不是渲染出来的页面。它帮助你测试控制器是否能正确处理 HTTP 请求、是否返回正确的视图名称或正确的响应状态码等。
|
||||
+ 比如,你可以测试一个 `GET /home` 请求是否返回了预期的视图名称 `"index"`,或者返回的状态码是否是 `200 OK`,而这些正是你在控制器中希望验证的部分。**这与页面渲染无关**,所以它不会显示页面内容,也不会出现渲染时的误差。
|
||||
|
||||
#### 2. 关注点分离
|
||||
+ 使用 `@WebMvcTest` 进行单元测试能够将 **Web 层** 与 **业务逻辑层**(Service、Repository)进行解耦。通过这种方式,你可以专注于测试控制器层的 HTTP 请求和响应,而不需要担心 Service 层的逻辑是否正确,或者数据库的连接是否正常。
|
||||
+ 控制器层和页面渲染是两个不同的关注点。**控制器层的目标是处理请求、确定视图名称、返回模型数据**等,而页面渲染本身通常由前端技术或模板引擎处理,更多是浏览器中的前端渲染过程。
|
||||
|
||||
#### 3. 通过 Mock 数据模拟请求和响应
|
||||
+ 在 `@WebMvcTest` 中,`MockMvc` 会模拟请求并验证响应,它不会执行实际的页面渲染或显示,只会检查你定义的 **控制器输出** 是否符合预期。因此,它能有效避免浏览器渲染过程中的误差。
|
||||
+ 比如,`MockMvc` 可以验证你的控制器是否正确返回了某个 HTML 页面的视图名称,而不用关注 HTML 的具体内容。如果你的目标是确保控制器的逻辑没有错误,这种方式是非常高效的。
|
||||
|
||||
#### 4. 测试时更快速且更简洁
|
||||
+ 通过 `@WebMvcTest` 测试时,你只加载 Web 层的相关组件,不需要启动整个应用的上下文,因此测试速度通常更快。
|
||||
+ 它让你能够快速定位问题。例如,如果测试失败,你可以很清楚地知道是控制器中的视图名称、请求参数的处理,还是其他的 HTTP 相关操作出了问题。反而如果进行实际的浏览器渲染测试,可能涉及到浏览器兼容性、前端细节等问题,会增加复杂度。
|
||||
|
||||
#### 5. 避免不必要的页面渲染
|
||||
+ 事实上,控制器的核心功能并不依赖于页面渲染。控制器应该保证的是:
|
||||
- 正确处理请求
|
||||
- 返回适当的视图名称和模型数据
|
||||
- 设置正确的响应状态码和其他 HTTP 相关的参数
|
||||
+ 页面渲染通常是通过模板引擎(如 Thymeleaf、JSP)完成的,而 `@WebMvcTest` 仅仅验证的是 **控制器的行为**,例如它返回的是正确的视图名称(而不是真的生成 HTML 页面)。
|
||||
|
||||
#### 6. 不同类型的测试互为补充
|
||||
+ 你可以把
|
||||
|
||||
```java
|
||||
@WebMvcTest
|
||||
```
|
||||
|
||||
和 端到端测试(如集成测试或 UI 测试)结合起来使用:
|
||||
|
||||
- `@WebMvcTest` 用于测试控制器的行为、请求路径、视图名称等。
|
||||
- **端到端测试** 可以通过实际浏览器或工具(如 Selenium)来模拟用户操作,查看页面是否正确渲染,验证 HTML 是否按预期显示。
|
||||
|
||||
##
|
|
@ -0,0 +1,152 @@
|
|||
## 访问控制
|
||||
请求地址时返回对应的网页文件
|
||||
|
||||
+ `@RestController`用于返回对象格式的内容,在后面会使用`ModelAndView`可以返回网页文件
|
||||
+ `@Controller`用于返回网页文件
|
||||
|
||||
### 环境要求
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- thymeleaf -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<!-- devtools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.36</version>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
||||
### 错误页面设置
|
||||
在这个路径下可以配置错误码要访问的页面,也就是可以自定义页面内容
|
||||
|
||||

|
||||
|
||||
### 使用`@Controller`
|
||||
返回需要访问的HTML内容页面,最后返回的字符串就是页面,这个页面位于`templates`目录下
|
||||
|
||||
```java
|
||||
@RequestMapping("/use")
|
||||
@Controller
|
||||
public class UseController {
|
||||
|
||||
// 带参数访问
|
||||
@RequestMapping(value = "hello", method = RequestMethod.GET, params = {"name"})
|
||||
public String hello() {
|
||||
return "hello";
|
||||
}
|
||||
|
||||
@GetMapping("jumpPage")
|
||||
public String jumpPage() {
|
||||
return "jumpPage";
|
||||
}
|
||||
|
||||
@GetMapping("index")
|
||||
public String quick() {
|
||||
return "user";
|
||||
}
|
||||
|
||||
// 跳转的页面
|
||||
@GetMapping("toJump")
|
||||
public String toJump() {
|
||||
return "redirect:jumpPage";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果在使用`@Controller`需要返回JSON内容,需要在控制器方法上加上`@ResponseBody`
|
||||
|
||||
```java
|
||||
@GetMapping("getJson")
|
||||
@ResponseBody
|
||||
public List<String> getJson() {
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add("a");
|
||||
list.add("b");
|
||||
list.add("c");
|
||||
|
||||
return list;
|
||||
}
|
||||
```
|
||||
|
||||
#### 将视图和模型拆开
|
||||
```java
|
||||
// 将视图和模型拆开
|
||||
@GetMapping("page/test3")
|
||||
public String test3(Model model) {
|
||||
model.addAttribute("test3", "测试3");
|
||||
return "page/test3";
|
||||
}
|
||||
```
|
||||
|
||||
### 使用`@RestController`
|
||||
#### 使用方式1
|
||||
如果使用`@RestController`那么返回的就是JSON对象,但是这时候要想返回网页文件,需要使用`ModelAndView`
|
||||
|
||||
```java
|
||||
@RequestMapping("userRest")
|
||||
@RestController
|
||||
public class UseRestController {
|
||||
|
||||
@GetMapping("page/test")
|
||||
public ModelAndView test() {
|
||||
ModelAndView modelAndView = new ModelAndView();
|
||||
modelAndView.setViewName("page/test");
|
||||
modelAndView.addObject("message", "这是消息内容");
|
||||
return modelAndView;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们引入了`thymeleaf`所以有以下内容`<h4 th:text="'消息:'+ ${message}"></h4>`
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>使用RestController返回页面信息</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3>使用RestController返回页面信息</h3>
|
||||
<h4 th:text="'消息:'+ ${message}"></h4>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
> 其中`modelAndView.addObject("message", "这是消息内容");`是可选的
|
||||
>
|
||||
|
||||
#### 使用方式2
|
||||
在控制器方法上使用`ModelAndView`
|
||||
|
||||
```java
|
||||
@GetMapping("page/test2")
|
||||
public ModelAndView test2(ModelAndView modelAndView) {
|
||||
modelAndView.addObject("hello", "你好");
|
||||
modelAndView.setViewName("page/test2");
|
||||
return modelAndView;
|
||||
}
|
||||
```
|
||||
|
||||
##
|
|
@ -0,0 +1,171 @@
|
|||
在 Spring MVC 中,**重定向(Redirect)**和**转发(Forward)**是两种常见的请求处理方式,它们分别用于不同的场景。Spring 提供了灵活的 API 来实现这两种请求方式。
|
||||
|
||||
### 1. **转发(Forward)**
|
||||
**转发**是指服务器将请求转发到另一个资源(如 JSP 页面或另一个控制器方法),并且请求和响应都不会发生改变。即,URL 不会发生变化,客户端仍然看到原始的 URL。
|
||||
|
||||
#### 转发的实现
|
||||
在 Spring MVC 中,可以通过以下两种方式实现转发:
|
||||
|
||||
+ 使用 `RequestDispatcher.forward()` 方法。
|
||||
+ 使用 `ModelAndView` 中的 `"forward:"` 前缀来指定转发的路径。
|
||||
|
||||
##### 示例 1:使用 `RequestDispatcher` 转发
|
||||
```java
|
||||
@Controller
|
||||
public class ForwardController {
|
||||
|
||||
@RequestMapping("/forwardExample")
|
||||
public void forward(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
// 使用 Servlet 的 RequestDispatcher 进行转发
|
||||
request.getRequestDispatcher("/forwardedPage").forward(request, response);
|
||||
}
|
||||
|
||||
@RequestMapping("/forwardedPage")
|
||||
public String forwardedPage() {
|
||||
return "forwardedPage"; // 返回视图
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 示例 2:使用 `ModelAndView` 转发
|
||||
```java
|
||||
@Controller
|
||||
public class ForwardController {
|
||||
|
||||
@RequestMapping("/forwardExample")
|
||||
public ModelAndView forwardExample() {
|
||||
// 使用 ModelAndView 进行转发
|
||||
return new ModelAndView("forward:/forwardedPage");
|
||||
}
|
||||
|
||||
@RequestMapping("/forwardedPage")
|
||||
public String forwardedPage() {
|
||||
return "forwardedPage"; // 返回视图
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**解释:**
|
||||
|
||||
+ `forward:/forwardedPage` 表示将请求转发到 `/forwardedPage`,且客户端的 URL 不会发生变化。
|
||||
+ 在转发的过程中,控制器方法返回的视图会直接被渲染。
|
||||
|
||||
#### 转发的特点
|
||||
+ **URL 不改变**:用户的浏览器地址栏不会发生变化,仍然显示原始的请求路径。
|
||||
+ **共享同一个请求**:请求数据(如请求参数)会被转发到目标资源,可以通过 `request` 对象共享。
|
||||
+ **适用于内部请求**:转发是服务端内部的操作,适用于控制器之间的跳转或者从控制器到视图的跳转。
|
||||
|
||||
### 2. **重定向(Redirect)**
|
||||
**重定向**是指服务器告诉客户端(浏览器)重新发起一个新的请求。重定向会导致浏览器地址栏的 URL 更新为新的地址,因此会导致一次新的 HTTP 请求。
|
||||
|
||||
#### 重定向的实现
|
||||
在 Spring MVC 中,重定向可以通过以下几种方式实现:
|
||||
|
||||
+ 使用 `redirect:` 前缀返回视图。
|
||||
+ 使用 `HttpServletResponse.sendRedirect()` 方法。
|
||||
|
||||
##### 示例 1:使用 `redirect:` 前缀
|
||||
```java
|
||||
@Controller
|
||||
public class RedirectController {
|
||||
|
||||
@RequestMapping("/redirectExample")
|
||||
public String redirectExample() {
|
||||
// 使用 redirect: 前缀进行重定向
|
||||
return "redirect:/redirectedPage";
|
||||
}
|
||||
|
||||
@RequestMapping("/redirectedPage")
|
||||
public String redirectedPage() {
|
||||
return "redirectedPage"; // 返回视图
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 示例 2:使用 `HttpServletResponse.sendRedirect()`
|
||||
```java
|
||||
@Controller
|
||||
public class RedirectController {
|
||||
|
||||
@RequestMapping("/redirectExample")
|
||||
public void redirectExample(HttpServletResponse response) throws IOException {
|
||||
// 使用 HttpServletResponse.sendRedirect() 进行重定向
|
||||
response.sendRedirect("/redirectedPage");
|
||||
}
|
||||
|
||||
@RequestMapping("/redirectedPage")
|
||||
public String redirectedPage() {
|
||||
return "redirectedPage"; // 返回视图
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**解释:**
|
||||
|
||||
+ `redirect:/redirectedPage` 会发出一个 HTTP 302 重定向响应,浏览器会发起一次新的请求到 `/redirectedPage`,并更新地址栏的 URL。
|
||||
+ 使用 `HttpServletResponse.sendRedirect()` 方法也会发送重定向响应,同样会导致浏览器重新发起新的请求。
|
||||
|
||||
#### 重定向的特点
|
||||
+ **URL 改变**:浏览器的地址栏会更新为重定向的目标 URL。
|
||||
+ **不同的请求**:重定向会产生一次新的 HTTP 请求,原请求的参数不会自动携带到新请求中,除非在重定向 URL 中显式传递。
|
||||
+ **适用于跨请求跳转**:重定向适合用于不同请求之间的跳转,特别是当需要在多个请求中传递数据时。
|
||||
|
||||
### 3. **重定向与转发的区别**
|
||||
| 特性 | 重定向(Redirect) | 转发(Forward) |
|
||||
| --- | --- | --- |
|
||||
| **URL 变化** | 浏览器地址栏会更新为新的 URL | 浏览器地址栏不会改变,仍然是原始请求 URL |
|
||||
| **请求次数** | 会产生两次请求:第一次请求重定向,第二次是新的目标请求 | 只会产生一次请求(内部请求) |
|
||||
| **数据传递** | 重定向后,数据不能自动传递到新请求中,除非通过 URL 参数传递 | 可以直接传递请求数据,如请求参数和请求属性 |
|
||||
| **适用场景** | 当需要跨请求或跨控制器传递数据时,或者需要跳转到外部 URL | 当请求处理完成后,内部跳转到另一个视图或资源时 |
|
||||
| **性能** | 因为需要进行两次请求,所以相比转发略有性能开销 | 因为只需要一次请求,性能开销较小 |
|
||||
|
||||
|
||||
### 4. **常见使用场景**
|
||||
#### (1) **重定向的使用场景**
|
||||
+ **表单提交后的重定向**:表单数据提交后,为了防止表单重复提交(用户刷新页面时),常常会使用重定向到另一个页面,这种方法叫做 **Post/Redirect/Get (PRG)** 模式。例如,用户提交了一个订单数据后,页面显示 "订单已提交",并重定向到订单查看页面。
|
||||
|
||||
```java
|
||||
@RequestMapping("/submitOrder")
|
||||
public String submitOrder(Order order) {
|
||||
orderService.save(order);
|
||||
// 提交订单后重定向到订单详情页
|
||||
return "redirect:/orderDetails?orderId=" + order.getId();
|
||||
}
|
||||
```
|
||||
|
||||
+ **外部 URL 重定向**:在用户登录后重定向到外部系统或第三方网站。
|
||||
|
||||
```java
|
||||
@RequestMapping("/redirectToExternalSite")
|
||||
public String redirectToExternal() {
|
||||
// 重定向到外部 URL
|
||||
return "redirect:http://www.example.com";
|
||||
}
|
||||
```
|
||||
|
||||
#### (2) **转发的使用场景**
|
||||
+ **内部资源的转发**:当需要在同一应用程序内部跳转时(如从控制器到视图),或者从一个控制器跳转到另一个控制器时,可以使用转发。例如,在处理用户请求后,将其转发到 JSP 页面进行显示:
|
||||
|
||||
```java
|
||||
@RequestMapping("/showUser")
|
||||
public String showUserDetails(Model model) {
|
||||
User user = userService.getUser();
|
||||
model.addAttribute("user", user);
|
||||
return "forward:/userDetails.jsp"; // 内部转发到 userDetails.jsp 页面
|
||||
}
|
||||
```
|
||||
|
||||
+ **从一个控制器跳转到另一个控制器**:在处理完业务逻辑后,可能需要跳转到另一个控制器来处理某些逻辑。
|
||||
|
||||
```java
|
||||
@RequestMapping("/processOrder")
|
||||
public String processOrder(Order order) {
|
||||
orderService.process(order);
|
||||
return "forward:/orderConfirmation"; // 内部跳转到订单确认页面
|
||||
}
|
||||
```
|
||||
|
||||
+ **重定向(Redirect)**:客户端浏览器会发起一次新的请求,地址栏 URL 会发生变化,适用于需要跨请求跳转或外部跳转的场景。
|
||||
+ **转发(Forward)**:请求在服务器内部被转发,地址栏 URL 不变,适用于同一请求的内部跳转。
|
||||
|
||||
##
|
|
@ -0,0 +1,274 @@
|
|||
## Thymeleaf快速入门
|
||||
Thymeleaf 是一种现代化的 Java 模板引擎,广泛用于生成 HTML、XML、JavaScript 等内容。它有许多内置的指令和功能,用于渲染动态内容、条件渲染、循环、处理表达式等。以下是 Thymeleaf 中常见的指令和属性的详细介绍:
|
||||
|
||||
### 1. `th:text`
|
||||
用于替换元素的文本内容。
|
||||
|
||||
```html
|
||||
<span th:text="${message}"></span>
|
||||
|
||||
```
|
||||
|
||||
+ `${message}` 的值会替换 `span` 元素的文本。
|
||||
|
||||
> 如果需要格式化日期,需要注意,使用`temporals`进行操作
|
||||
>
|
||||
|
||||
```html
|
||||
<td class="text-success" th:text="${#temporals.format(bill.transactionDate,'yyyy-MM-dd HH:mm:ss')}"></td>
|
||||
```
|
||||
|
||||
```plain
|
||||
|
||||
```
|
||||
|
||||
### 2. `th:utext`
|
||||
用于替换元素的文本内容,并允许处理 HTML 标签(不会转义 HTML)。
|
||||
|
||||
```html
|
||||
<span th:utext="${htmlContent}"></span>
|
||||
|
||||
```
|
||||
|
||||
+ `${htmlContent}` 的内容直接插入,并解析其中的 HTML。
|
||||
|
||||
### 3. `th:value`
|
||||
设置表单元素的 `value` 属性,通常用于输入框或选择框。
|
||||
|
||||
```html
|
||||
<input type="text" th:value="${user.name}" />
|
||||
```
|
||||
|
||||
+ 将 `${user.name}` 的值赋给该输入框的 `value` 属性。
|
||||
|
||||
### 4. `th:each`
|
||||
用于循环遍历集合或数组。
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li th:each="person : ${people}">
|
||||
<span th:text="${person.name}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
```
|
||||
|
||||
+ 遍历 `people` 集合,输出每个 `person.name`。
|
||||
|
||||
### 5. `th:if`
|
||||
用于条件渲染,只有满足条件时才渲染元素。
|
||||
|
||||
```html
|
||||
<div th:if="${user.isAdmin}">
|
||||
<p>Welcome, admin!</p>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
+ 如果 `user.isAdmin` 为 `true`,渲染该 `div`。
|
||||
|
||||
### 6. `th:unless`
|
||||
与 `th:if` 相反,只有条件为 `false` 时才渲染元素。
|
||||
|
||||
```html
|
||||
<div th:unless="${user.isAdmin}">
|
||||
<p>You are not an admin!</p>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
+ 如果 `user.isAdmin` 为 `false`,渲染该 `div`。
|
||||
|
||||
### 7. `th:attr`
|
||||
用于设置元素的多个属性。
|
||||
|
||||
```html
|
||||
<img th:attr="src=${imageUrl}" th:attr="alt=${imageDescription}" />
|
||||
```
|
||||
|
||||
+ 设置 `img` 元素的 `src` 和 `alt` 属性。
|
||||
|
||||
### 8. `th:src`** / **`th:href`
|
||||
用于动态设置 `src` 或 `href` 属性。
|
||||
|
||||
```html
|
||||
<img th:src="@{${imageUrl}}" alt="Image">
|
||||
<a th:href="@{${linkUrl}}">Click Here</a>
|
||||
|
||||
```
|
||||
|
||||
+ `th:src` 用于设置图片的 `src` 属性,`th:href` 用于设置链接的 `href` 属性。
|
||||
|
||||
### 9. `th:class`
|
||||
动态设置 `class` 属性,支持条件表达式。
|
||||
|
||||
```html
|
||||
<div th:class="${isActive} ? 'active' : 'inactive'">...</div>
|
||||
|
||||
```
|
||||
|
||||
+ 如果 `isActive` 为 `true`,设置 `class="active"`,否则为 `inactive`。
|
||||
|
||||
### 10. `th:classappend`** / **`th:classprepend`
|
||||
分别在现有的 `class` 属性上追加或前置新类。
|
||||
|
||||
```html
|
||||
<div th:classappend="'newClass'">...</div>
|
||||
<div th:classprepend="'prefixClass'">...</div>
|
||||
|
||||
```
|
||||
|
||||
+ `th:classappend` 会将新的类追加到现有类的后面。
|
||||
+ `th:classprepend` 会将新的类添加到现有类的前面。
|
||||
|
||||
### 11. `th:id`
|
||||
设置元素的 `id` 属性。
|
||||
|
||||
```html
|
||||
<input type="text" th:id="${elementId}" />
|
||||
```
|
||||
|
||||
+ 设置 `input` 元素的 `id` 为 `${elementId}` 的值。
|
||||
|
||||
### 12. `th:action`
|
||||
设置表单的 `action` 属性。
|
||||
|
||||
```html
|
||||
<form th:action="@{/submitForm}" method="post">
|
||||
<!-- form fields -->
|
||||
</form>
|
||||
|
||||
```
|
||||
|
||||
+ 设置表单的 `action` 为 `/submitForm`。
|
||||
|
||||
### 13. `th:style`
|
||||
设置元素的 `style` 属性。
|
||||
|
||||
```html
|
||||
<div th:style="'color: ' + ${color}"></div>
|
||||
|
||||
```
|
||||
|
||||
+ 动态设置 `style` 属性,`${color}` 的值会成为 `color` 样式的值。
|
||||
|
||||
### 14. `th:fragment`
|
||||
定义一个可重用的片段,通常在模板中调用。
|
||||
|
||||
```html
|
||||
<div th:fragment="userFragment">
|
||||
<p>Welcome, <span th:text="${user.name}"></span></p>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
+ 定义一个 `userFragment` 片段,可以在其他模板中引用。
|
||||
|
||||
### 15. `th:replace`
|
||||
替换当前元素,并将一个片段或其他模板插入其中。
|
||||
|
||||
```html
|
||||
<div th:replace="~{userFragment}"></div>
|
||||
|
||||
```
|
||||
|
||||
+ `th:replace` 会将 `userFragment` 片段的内容插入到当前 `div` 中。
|
||||
|
||||
### 16. `th:include`
|
||||
将另一个模板的内容插入当前模板中,但不会替换当前元素。
|
||||
|
||||
```html
|
||||
<div th:include="~{userFragment}"></div>
|
||||
|
||||
```
|
||||
|
||||
+ 插入 `userFragment` 的内容,但保留当前 `div` 元素。
|
||||
|
||||
### 17. `th:with`
|
||||
局部变量声明,用于在模板中定义临时变量。
|
||||
|
||||
```html
|
||||
<div th:with="total=${cart.totalPrice}">
|
||||
<p th:text="'Total price: ' + ${total}"></p>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
+ `th:with` 用于在当前元素的上下文中定义变量,类似于局部变量。
|
||||
|
||||
### 18. `th:block`
|
||||
在模板中定义一个不会渲染任何 HTML 标签的块元素。用于组合多个元素。
|
||||
|
||||
```html
|
||||
<th:block th:each="person : ${people}">
|
||||
<p th:text="${person.name}"></p>
|
||||
</th:block>
|
||||
|
||||
```
|
||||
|
||||
+ `th:block` 不会渲染任何标签,但可以用来包装多个元素进行条件判断或循环。
|
||||
|
||||
### 19. `th:switch`** / **`th:case`
|
||||
类似于 Java 中的 `switch` 语句,用于条件选择。
|
||||
|
||||
```html
|
||||
<div th:switch="${status}">
|
||||
<span th:case="'active'">Active</span>
|
||||
<span th:case="'inactive'">Inactive</span>
|
||||
<span th:case="*">Unknown</span>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
+ 根据 `${status}` 的值,渲染对应的 `span` 元素。
|
||||
|
||||
### 20. `th:object`
|
||||
用来为表单元素绑定一个对象。
|
||||
|
||||
```html
|
||||
<form th:action="@{/submit}" th:object="${user}">
|
||||
<input type="text" th:field="*{name}" />
|
||||
<input type="text" th:field="*{email}" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
```
|
||||
|
||||
+ `th:object` 绑定整个表单到 `user` 对象。
|
||||
+ `th:field` 用于绑定每个表单字段到对象的属性。
|
||||
|
||||
### 21. `th:href`** / **`th:src`
|
||||
用于动态设置 URL 值。
|
||||
|
||||
```html
|
||||
<a th:href="@{/users/{id}(id=${user.id})}">Profile</a>
|
||||
<img th:src="@{/images/{imageName}(imageName=${image.name})}" />
|
||||
```
|
||||
|
||||
+ 动态生成 URL,支持路径变量的替换。
|
||||
|
||||
### 22. `th:placeholder`
|
||||
设置表单输入框的 `placeholder` 属性。
|
||||
|
||||
```html
|
||||
<input type="text" th:placeholder="${placeholderText}" />
|
||||
```
|
||||
|
||||
+ 设置 `input` 的 `placeholder` 为 `${placeholderText}` 的值。
|
||||
|
||||
### 23. `th:errors`
|
||||
显示与 `username` 属性相关的错误信息。如果 `username` 为空或者不符合验证规则,这里就会显示出相应的错误消息。
|
||||
|
||||
```html
|
||||
<div th:errors="*{email}"></div> <!-- 错误信息展示 -->
|
||||
```
|
||||
|
||||
你还可以通过 `th:errors` 对错误消息进行自定义格式化。例如,使用 `*{field}` 可以获取字段的错误信息。
|
||||
|
||||
```html
|
||||
<div th:errors="*{username}">Error</div>
|
||||
|
||||
```
|
||||
|
||||
如果验证失败,错误消息将显示在 `<div>` 中。如果没有错误,它会显示默认的 "Error" 文本。
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
```java
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.HttpDelete;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpPut;
|
||||
import org.apache.http.conn.ClientConnectionManager;
|
||||
import org.apache.http.conn.scheme.Scheme;
|
||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpUtil {
|
||||
public static HttpResponse doGet(String host, String path, String method, Map<String, String> headers, Map<String, String> querys) throws Exception {
|
||||
HttpClient httpClient = wrapClient(host);
|
||||
|
||||
HttpGet request = new HttpGet(buildUrl(host, path, querys));
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
request.addHeader(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
return httpClient.execute(request);
|
||||
}
|
||||
|
||||
public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys) throws Exception {
|
||||
HttpClient httpClient = wrapClient(host);
|
||||
|
||||
HttpPost request = new HttpPost(buildUrl(host, path, querys));
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
request.addHeader(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
if (bodys != null) {
|
||||
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
|
||||
|
||||
for (String key : bodys.keySet()) {
|
||||
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
|
||||
}
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
|
||||
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
|
||||
request.setEntity(formEntity);
|
||||
}
|
||||
|
||||
return httpClient.execute(request);
|
||||
}
|
||||
|
||||
|
||||
public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, String body) throws Exception {
|
||||
HttpClient httpClient = wrapClient(host);
|
||||
|
||||
HttpPost request = new HttpPost(buildUrl(host, path, querys));
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
request.addHeader(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(body)) {
|
||||
request.setEntity(new StringEntity(body, "utf-8"));
|
||||
}
|
||||
|
||||
return httpClient.execute(request);
|
||||
}
|
||||
|
||||
|
||||
public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, byte[] body) throws Exception {
|
||||
HttpClient httpClient = wrapClient(host);
|
||||
|
||||
HttpPost request = new HttpPost(buildUrl(host, path, querys));
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
request.addHeader(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
request.setEntity(new ByteArrayEntity(body));
|
||||
}
|
||||
|
||||
return httpClient.execute(request);
|
||||
}
|
||||
|
||||
|
||||
public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, String body) throws Exception {
|
||||
HttpClient httpClient = wrapClient(host);
|
||||
|
||||
HttpPut request = new HttpPut(buildUrl(host, path, querys));
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
request.addHeader(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(body)) {
|
||||
request.setEntity(new StringEntity(body, "utf-8"));
|
||||
}
|
||||
|
||||
return httpClient.execute(request);
|
||||
}
|
||||
|
||||
|
||||
public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, byte[] body) throws Exception {
|
||||
HttpClient httpClient = wrapClient(host);
|
||||
|
||||
HttpPut request = new HttpPut(buildUrl(host, path, querys));
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
request.addHeader(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
request.setEntity(new ByteArrayEntity(body));
|
||||
}
|
||||
|
||||
return httpClient.execute(request);
|
||||
}
|
||||
|
||||
|
||||
public static HttpResponse doDelete(String host, String path, String method, Map<String, String> headers, Map<String, String> querys) throws Exception {
|
||||
HttpClient httpClient = wrapClient(host);
|
||||
|
||||
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
request.addHeader(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
return httpClient.execute(request);
|
||||
}
|
||||
|
||||
private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
|
||||
StringBuilder sbUrl = new StringBuilder();
|
||||
sbUrl.append(host);
|
||||
if (!StringUtils.isBlank(path)) {
|
||||
sbUrl.append(path);
|
||||
}
|
||||
if (null != querys) {
|
||||
StringBuilder sbQuery = new StringBuilder();
|
||||
for (Map.Entry<String, String> query : querys.entrySet()) {
|
||||
if (!sbQuery.isEmpty()) {
|
||||
sbQuery.append("&");
|
||||
}
|
||||
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
|
||||
sbQuery.append(query.getValue());
|
||||
}
|
||||
if (!StringUtils.isBlank(query.getKey())) {
|
||||
sbQuery.append(query.getKey());
|
||||
if (!StringUtils.isBlank(query.getValue())) {
|
||||
sbQuery.append("=");
|
||||
sbQuery.append(URLEncoder.encode(query.getValue(), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sbQuery.isEmpty()) {
|
||||
sbUrl.append("?").append(sbQuery);
|
||||
}
|
||||
}
|
||||
|
||||
return sbUrl.toString();
|
||||
}
|
||||
|
||||
private static HttpClient wrapClient(String host) {
|
||||
HttpClient httpClient = new DefaultHttpClient();
|
||||
if (host.startsWith("https://")) {
|
||||
sslClient(httpClient);
|
||||
}
|
||||
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private static void sslClient(HttpClient httpClient) {
|
||||
try {
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
X509TrustManager tm = new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] xcs, String str) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] xcs, String str) {
|
||||
}
|
||||
};
|
||||
ctx.init(null, new TrustManager[]{tm}, null);
|
||||
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
|
||||
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
|
||||
ClientConnectionManager ccm = httpClient.getConnectionManager();
|
||||
SchemeRegistry registry = ccm.getSchemeRegistry();
|
||||
registry.register(new Scheme("https", 443, ssf));
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
```java
|
||||
/**
|
||||
* * 构建权限树形结构
|
||||
*
|
||||
* @param id 节点ID
|
||||
* @param powerVoList 节点列表
|
||||
* @return 树形列表
|
||||
*/
|
||||
public List<PowerVo> handlePowerVoChildren(Long id, List<PowerVo> powerVoList) {
|
||||
return powerVoList.stream()
|
||||
.filter(powerVo -> powerVo.getParentId().equals(id))
|
||||
.peek(powerVo -> powerVo.setChildren(handlePowerVoChildren(powerVo.getId(), powerVoList)))
|
||||
.toList();
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
```java
|
||||
import io.swagger.v3.oas.models.ExternalDocumentation;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class Knife4jConfig {
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
// 作者等信息
|
||||
Contact contact = new Contact().name("Bunny").email("1319900154@qq.com").url("http://z-bunny.cn");
|
||||
// 使用协议
|
||||
License license = new License().name("MIT").url("https://MUT.com");
|
||||
// 相关信息
|
||||
Info info = new Info().title("Bunny-Order").description("权限管理模板").version("v1.0.0").contact(contact).license(license).termsOfService("MIT");
|
||||
|
||||
return new OpenAPI().info(info).externalDocs(new ExternalDocumentation());
|
||||
}
|
||||
|
||||
// 管理员相关分类接口
|
||||
@Bean
|
||||
public GroupedOpenApi groupedOpenAdminApi() {
|
||||
return GroupedOpenApi.builder().group("默认请求接口").pathsToMatch("/**").build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖包
|
||||
```xml
|
||||
<!-- lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<!-- knife4j -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,35 @@
|
|||
```java
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 封装分页查询结果
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
@Schema(name = "PageResult 对象", title = "分页返回结果", description = "分页返回结果")
|
||||
public class PageResult<T> implements Serializable {
|
||||
|
||||
@Schema(name = "pageNo", title = "当前页")
|
||||
private Long pageNo;
|
||||
|
||||
@Schema(name = "pageSize", title = "每页记录数")
|
||||
private Long pageSize;
|
||||
|
||||
@Schema(name = "total", title = "总记录数")
|
||||
private Long total;
|
||||
|
||||
@Schema(name = "list", title = "当前页数据集合")
|
||||
private List<T> list;
|
||||
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
```java
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(name = "BaseVo", title = "基础返回对象内容", description = "基础返回对象内容")
|
||||
public class BaseVo implements Serializable {
|
||||
|
||||
@Schema(name = "id", title = "主键")
|
||||
@JsonProperty("id")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
@JSONField(serializeUsing = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(name = "updateTime", title = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(name = "createTime", title = "发布时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(name = "createUser", title = "创建用户")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
@JSONField(serializeUsing = ToStringSerializer.class)
|
||||
private Long createUser;
|
||||
|
||||
@Schema(name = "updateUser", title = "操作用户")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
@JSONField(serializeUsing = ToStringSerializer.class)
|
||||
private Long updateUser;
|
||||
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
```java
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 封装分页查询结果
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
@Schema(name = "PageResult 对象", title = "分页返回结果", description = "分页返回结果")
|
||||
public class PageResult<T> implements Serializable {
|
||||
|
||||
@Schema(name = "pageNo", title = "当前页")
|
||||
private Long pageNo;
|
||||
|
||||
@Schema(name = "pageSize", title = "每页记录数")
|
||||
private Long pageSize;
|
||||
|
||||
@Schema(name = "total", title = "总记录数")
|
||||
private Long total;
|
||||
|
||||
@Schema(name = "list", title = "当前页数据集合")
|
||||
private List<T> list;
|
||||
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
```java
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 统一返回结果状态信息类
|
||||
*/
|
||||
@Getter
|
||||
public enum ResultCodeEnum {
|
||||
// 成功操作 200
|
||||
SUCCESS(200, "操作成功"),
|
||||
ADD_SUCCESS(200, "添加成功"),
|
||||
UPDATE_SUCCESS(200, "修改成功"),
|
||||
DELETE_SUCCESS(200, "删除成功"),
|
||||
SORT_SUCCESS(200, "排序成功"),
|
||||
SUCCESS_UPLOAD(200, "上传成功"),
|
||||
SUCCESS_LOGOUT(200, "退出成功"),
|
||||
LOGOUT_SUCCESS(200, "退出成功"),
|
||||
EMAIL_CODE_REFRESH(200, "邮箱验证码已刷新"),
|
||||
EMAIL_CODE_SEND_SUCCESS(200, "邮箱验证码已发送"),
|
||||
|
||||
// 验证错误 201
|
||||
USERNAME_OR_PASSWORD_NOT_EMPTY(201, "用户名或密码不能为空"),
|
||||
EMAIL_CODE_NOT_EMPTY(201, "邮箱验证码不能为空"),
|
||||
SEND_EMAIL_CODE_NOT_EMPTY(201, "请先发送邮箱验证码"),
|
||||
EMAIL_CODE_NOT_MATCHING(201, "邮箱验证码不匹配"),
|
||||
LOGIN_ERROR(201, "账号或密码错误"),
|
||||
LOGIN_ERROR_USERNAME_PASSWORD_NOT_EMPTY(201, "登录信息不能为空"),
|
||||
GET_BUCKET_EXCEPTION(201, "获取文件信息失败"),
|
||||
SEND_MAIL_CODE_ERROR(201, "邮件发送失败"),
|
||||
EMAIL_CODE_EMPTY(201, "邮箱验证码过期或不存在"),
|
||||
EMAIL_EXIST(201, "邮箱已存在"),
|
||||
REQUEST_IS_EMPTY(201, "请求数据为空"),
|
||||
DATA_TOO_LARGE(201, "请求数据为空"),
|
||||
UPDATE_NEW_PASSWORD_SAME_AS_OLD_PASSWORD(201, "新密码与密码相同"),
|
||||
|
||||
// 数据相关 206
|
||||
ILLEGAL_REQUEST(206, "非法请求"),
|
||||
REPEAT_SUBMIT(206, "重复提交"),
|
||||
DATA_ERROR(206, "数据异常"),
|
||||
EMAIL_USER_TEMPLATE_IS_EMPTY(206, "邮件模板为空"),
|
||||
EMAIL_TEMPLATE_IS_EMPTY(206, "邮件模板为空"),
|
||||
EMAIL_USER_IS_EMPTY(206, "关联邮件用户配置为空"),
|
||||
DATA_EXIST(206, "数据已存在"),
|
||||
DATA_NOT_EXIST(206, "数据不存在"),
|
||||
ALREADY_USER_EXCEPTION(206, "用户已存在"),
|
||||
USER_IS_EMPTY(206, "用户不存在"),
|
||||
FILE_NOT_EXIST(206, "文件不存在"),
|
||||
NEW_PASSWORD_SAME_OLD_PASSWORD(206, "新密码不能和旧密码相同"),
|
||||
MISSING_TEMPLATE_FILES(206, "缺少模板文件"),
|
||||
|
||||
// 身份过期 208
|
||||
LOGIN_AUTH(208, "请先登陆"),
|
||||
AUTHENTICATION_EXPIRED(208, "身份验证过期"),
|
||||
SESSION_EXPIRATION(208, "会话过期"),
|
||||
|
||||
// 209
|
||||
THE_SAME_USER_HAS_LOGGED_IN(209, "相同用户已登录"),
|
||||
|
||||
// 提示错误
|
||||
UPDATE_ERROR(216, "修改失败"),
|
||||
URL_ENCODE_ERROR(216, "URL编码失败"),
|
||||
ILLEGAL_CALLBACK_REQUEST_ERROR(217, "非法回调请求"),
|
||||
FETCH_USERINFO_ERROR(219, "获取用户信息失败"),
|
||||
ILLEGAL_DATA_REQUEST(219, "非法数据请求"),
|
||||
CLASS_NOT_FOUND(219, "类名不存在"),
|
||||
ADMIN_ROLE_CAN_NOT_DELETED(219, "无法删除admin角色"),
|
||||
ROUTER_RANK_NEED_LARGER_THAN_THE_PARENT(219, "设置路由等级需要大于或等于父级的路由等级"),
|
||||
|
||||
// 无权访问 403
|
||||
FAIL_NO_ACCESS_DENIED(403, "无权访问"),
|
||||
FAIL_NO_ACCESS_DENIED_USER_OFFLINE(403, "用户强制下线"),
|
||||
TOKEN_PARSING_FAILED(403, "token解析失败"),
|
||||
FAIL_NO_ACCESS_DENIED_USER_LOCKED(403, "该账户已封禁"),
|
||||
|
||||
// 系统错误 500
|
||||
UNKNOWN_EXCEPTION(500, "服务异常"),
|
||||
SERVICE_ERROR(500, "服务异常"),
|
||||
UPLOAD_ERROR(500, "上传失败"),
|
||||
FAIL(500, "失败"),
|
||||
;
|
||||
|
||||
private final Integer code;
|
||||
private final String message;
|
||||
|
||||
ResultCodeEnum(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
```java
|
||||
import cn.bunny.dao.vo.result.Result;
|
||||
import cn.bunny.dao.vo.result.ResultCodeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.sql.SQLIntegrityConstraintViolationException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
// 运行时异常信息
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
@ResponseBody
|
||||
public Result<Object> exceptionHandler(RuntimeException exception) {
|
||||
String message = exception.getMessage();
|
||||
|
||||
// 解析异常
|
||||
String jsonParseError = "JSON parse error (.*)";
|
||||
Matcher jsonParseErrorMatcher = Pattern.compile(jsonParseError).matcher(message);
|
||||
if (jsonParseErrorMatcher.find()) {
|
||||
return Result.error(null, 500, "JSON解析异常 " + jsonParseErrorMatcher.group(1));
|
||||
}
|
||||
|
||||
// 数据过大
|
||||
String dataTooLongError = "Data too long for column (.*?) at row 1";
|
||||
Matcher dataTooLongErrorMatcher = Pattern.compile(dataTooLongError).matcher(message);
|
||||
if (dataTooLongErrorMatcher.find()) {
|
||||
return Result.error(null, 500, dataTooLongErrorMatcher.group(1) + " 字段数据过大");
|
||||
}
|
||||
|
||||
// 主键冲突
|
||||
String primaryKeyError = "Duplicate entry '(.*?)' for key .*";
|
||||
Matcher primaryKeyErrorMatcher = Pattern.compile(primaryKeyError).matcher(message);
|
||||
if (primaryKeyErrorMatcher.find()) {
|
||||
return Result.error(null, 500, "[" + primaryKeyErrorMatcher.group(1) + "]已存在");
|
||||
}
|
||||
|
||||
// corn表达式错误
|
||||
String cronExpression = "CronExpression '(.*?)' is invalid";
|
||||
Matcher cronExpressionMatcher = Pattern.compile(cronExpression).matcher(message);
|
||||
if (cronExpressionMatcher.find()) {
|
||||
return Result.error(null, 500, "表达式 " + cronExpressionMatcher.group(1) + " 不合法");
|
||||
}
|
||||
|
||||
log.error("GlobalExceptionHandler===>运行时异常信息:{}", message);
|
||||
exception.printStackTrace();
|
||||
return Result.error(null, 500, "服务器异常");
|
||||
}
|
||||
|
||||
// 捕获系统异常
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseBody
|
||||
public Result<Object> error(Exception exception) {
|
||||
log.error("捕获系统异常:{}", exception.getMessage());
|
||||
log.error("GlobalExceptionHandler===>系统异常信息:{}", (Object) exception.getStackTrace());
|
||||
|
||||
// 错误消息
|
||||
String message = exception.getMessage();
|
||||
|
||||
// 匹配到内容
|
||||
String patternString = "Request method '(\\w+)' is not supported";
|
||||
Matcher matcher = Pattern.compile(patternString).matcher(message);
|
||||
if (matcher.find()) return Result.error(null, 500, "请求方法错误,不是 " + matcher.group(1) + "类型请求");
|
||||
|
||||
// 请求API不存在
|
||||
String noStaticResource = "No static resource (.*)\\.";
|
||||
Matcher noStaticResourceMatcher = Pattern.compile(noStaticResource).matcher(message);
|
||||
if (noStaticResourceMatcher.find())
|
||||
return Result.error(null, 500, "请求API不存在 " + noStaticResourceMatcher.group(1));
|
||||
|
||||
// 返回错误内容
|
||||
return Result.error(null, 500, "系统异常");
|
||||
}
|
||||
|
||||
// 表单验证字段
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<String> handleValidationExceptions(MethodArgumentNotValidException ex) {
|
||||
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(DefaultMessageSourceResolvable::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
return Result.error(null, 201, errorMessage);
|
||||
}
|
||||
|
||||
// 特定异常处理
|
||||
@ExceptionHandler(ArithmeticException.class)
|
||||
@ResponseBody
|
||||
public Result<Object> error(ArithmeticException exception) {
|
||||
log.error("GlobalExceptionHandler===>特定异常信息:{}", exception.getMessage());
|
||||
|
||||
return Result.error(null, 500, exception.getMessage());
|
||||
}
|
||||
|
||||
// spring security异常
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseBody
|
||||
public Result<String> error(AccessDeniedException exception) throws AccessDeniedException {
|
||||
log.error("GlobalExceptionHandler===>spring security异常:{}", exception.getMessage());
|
||||
|
||||
return Result.error(ResultCodeEnum.SERVICE_ERROR);
|
||||
}
|
||||
|
||||
// 处理SQL异常
|
||||
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
|
||||
@ResponseBody
|
||||
public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException exception) {
|
||||
log.error("GlobalExceptionHandler===>处理SQL异常:{}", exception.getMessage());
|
||||
|
||||
String message = exception.getMessage();
|
||||
if (message.contains("Duplicate entry")) {
|
||||
// 错误信息
|
||||
return Result.error(ResultCodeEnum.USER_IS_EMPTY);
|
||||
} else {
|
||||
return Result.error(ResultCodeEnum.UNKNOWN_EXCEPTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
## ArrayBlockingQueue使用
|
||||
```java
|
||||
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(8);
|
||||
|
||||
// 阻塞添加
|
||||
queue.add("1");
|
||||
queue.add("2");
|
||||
queue.add("3");
|
||||
System.out.println(queue);
|
||||
|
||||
// 非阻塞添加
|
||||
queue.put("4");
|
||||
queue.put("5");
|
||||
System.out.println(queue);
|
||||
|
||||
// 阻塞添加,但队列满不会进入阻塞
|
||||
queue.offer("6");
|
||||
queue.offer("7");
|
||||
System.out.println(queue);
|
||||
|
||||
// 指定时间内进入阻塞
|
||||
queue.offer("8", 3, TimeUnit.SECONDS);
|
||||
queue.offer("9", 3, TimeUnit.SECONDS);// 8个队列满,不会进入阻塞
|
||||
System.out.println(queue);
|
||||
|
||||
// 从头部获取数据并移出,如果队列为空进入阻塞直到有数据添加
|
||||
String take = queue.take();
|
||||
System.out.println(take);
|
||||
|
||||
// 获取数据并移出队列第一个元素,如果队列为空将会阻塞指定的时间,直到在此期间有新数据写入,或者当前线程被其他线程中断,当线程超时会返回null
|
||||
String poll = queue.poll(3, TimeUnit.SECONDS);
|
||||
System.out.println(poll);
|
||||
|
||||
// 从头部获取数据并移出,如果队列为空不会阻塞
|
||||
String poll1 = queue.poll();
|
||||
System.out.println(poll1);
|
||||
|
||||
// 从头部获取数据不会移除,队列为空不会阻塞直接返回null
|
||||
String peek = queue.peek();
|
||||
System.out.println(peek);
|
||||
```
|
||||
|
||||
## PriorityBlockingQueue使用
|
||||
```java
|
||||
// 无边界队列,可以定义初始容量并不是最大容量
|
||||
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>(2);
|
||||
System.out.println(queue);
|
||||
|
||||
// 队列的添加方法都等于offer方法
|
||||
queue.offer(1);
|
||||
queue.offer(10);
|
||||
queue.offer(3);
|
||||
System.out.println(queue);
|
||||
```
|
||||
|
||||
## LinkedBlockingQueue使用
|
||||
```java
|
||||
// 属于可选边界
|
||||
LinkedBlockingQueue<Integer> integers = new LinkedBlockingQueue<>(10);
|
||||
boolean b = integers.remainingCapacity() == 10;
|
||||
System.out.println(b);
|
||||
|
||||
// 等于定义的
|
||||
LinkedBlockingQueue<Integer> integers1 = new LinkedBlockingQueue<>();
|
||||
boolean b1 = integers1.remainingCapacity() == Integer.MAX_VALUE;
|
||||
System.out.println(b1);
|
||||
```
|
||||
|
||||
## InterruptedException使用
|
||||
```java
|
||||
public class AtomicExample08 {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
DelayQueue<DelayedEntry> queue = new DelayQueue<>();
|
||||
queue.add(new DelayedEntry("A", 1000L));
|
||||
queue.add(new DelayedEntry("B", 1000L));
|
||||
queue.add(new DelayedEntry("C", 1000L));
|
||||
queue.add(new DelayedEntry("D", 1000L));
|
||||
queue.add(new DelayedEntry("E", 1000L));
|
||||
queue.add(new DelayedEntry("F", 1000L));
|
||||
queue.add(new DelayedEntry("G", 1000L));
|
||||
queue.add(new DelayedEntry("H", 1000L));
|
||||
queue.add(new DelayedEntry("I", 1000L));
|
||||
|
||||
// 非阻塞读取,立即返回但不移除头部元素,队列为空返回null
|
||||
assert queue.peek() != null;
|
||||
System.out.println(queue.peek().value);
|
||||
|
||||
// 非阻塞读取,当队列为空或者头部元素未达到过期时间返回值为null
|
||||
System.out.println(Objects.requireNonNull(queue.poll()).value);
|
||||
|
||||
// 最大阻塞单位时间,到达阻塞单位时间后,此刻为空或者头部元素未达到过期时间返回值为null,否则立即移出头部元素
|
||||
System.out.println(Objects.requireNonNull(queue.poll(3, TimeUnit.SECONDS)).value);
|
||||
|
||||
// 会一直阻塞到队列中有元素,并且队列头部元素达到过期时间,之后从队列中移除并返回
|
||||
System.out.println(queue.take().value);
|
||||
}
|
||||
|
||||
// 首先要实现Delayed接口
|
||||
@Getter
|
||||
static class DelayedEntry implements Delayed {
|
||||
private final String value;
|
||||
private final long time;
|
||||
|
||||
private DelayedEntry(String value, long time) {
|
||||
this.time = time;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit) {
|
||||
long delta = time - System.currentTimeMillis();
|
||||
return unit.convert(delta, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed o) {
|
||||
if (this.time < ((DelayedEntry) o).time) {
|
||||
return -1;
|
||||
} else if (this.time > ((DelayedEntry) o).time) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ConcurrentLinkedQueue(并发队列)性能
|
||||
ConcurrentLinkedQueue要优秀很多
|
||||
|
||||
```java
|
||||
import org.openjdk.jmh.annotations.*;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.Options;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Measurement(iterations = 10)
|
||||
@Warmup(iterations = 10)
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@State(Scope.Thread)
|
||||
@OutputTimeUnit(TimeUnit.MICROSECONDS)
|
||||
public class AtomicExample09 {
|
||||
private final static String DATA = "TEST";
|
||||
private final static Object LOCK = new Object();
|
||||
private SynchronizedLinkedList synchronizedLinkedList;
|
||||
private ConcurrentLinkedQueue<String> concurrentLinkedQueue;
|
||||
|
||||
// 如果使用不当反而会降低性能
|
||||
public static void main(String[] args) throws RunnerException {
|
||||
Options options = new OptionsBuilder()
|
||||
.include(AtomicExample09.class.getSimpleName())
|
||||
.forks(1)
|
||||
.build();
|
||||
new Runner(options).run();
|
||||
}
|
||||
|
||||
@Setup(Level.Iteration)
|
||||
public void setUp() {
|
||||
synchronizedLinkedList = new SynchronizedLinkedList();
|
||||
concurrentLinkedQueue = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
|
||||
// 测试 SynchronizedLinkedList
|
||||
@Group("sync")
|
||||
@Benchmark
|
||||
@GroupThreads(5)
|
||||
public void synchronizedListAdd() {
|
||||
synchronizedLinkedList.addLast(DATA);
|
||||
}
|
||||
|
||||
@Group("sync")
|
||||
@Benchmark
|
||||
@GroupThreads(5)
|
||||
public String synchronizedListGet() {
|
||||
return synchronizedLinkedList.removeFirst();
|
||||
}
|
||||
|
||||
// 测试 ConcurrentLinkedQueue
|
||||
@Group("concurrent")
|
||||
@Benchmark
|
||||
@GroupThreads(5)
|
||||
public void concurrentLinkedQueueAdd() {
|
||||
concurrentLinkedQueue.offer(DATA);
|
||||
}
|
||||
|
||||
@Group("concurrent")
|
||||
@Benchmark
|
||||
@GroupThreads(5)
|
||||
public String concurrentLinkedQueueGet() {
|
||||
return concurrentLinkedQueue.poll();
|
||||
}
|
||||
|
||||
private static class SynchronizedLinkedList {
|
||||
private final LinkedList<String> list = new LinkedList<>();
|
||||
|
||||
void addLast(String element) {
|
||||
synchronized (LOCK) {
|
||||
list.addLast(element);
|
||||
}
|
||||
}
|
||||
|
||||
String removeFirst() {
|
||||
synchronized (LOCK) {
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return list.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 测试结果
|
||||
// Benchmark Mode Cnt Score Error Units
|
||||
// AtomicExample09.concurrent avgt 10 0.221 ± 0.649 us/op
|
||||
// AtomicExample09.concurrent:concurrentLinkedQueueAdd avgt 10 0.407 ± 1.296 us/op
|
||||
// AtomicExample09.concurrent:concurrentLinkedQueueGet avgt 10 0.034 ± 0.048 us/op
|
||||
// AtomicExample09.sync avgt 10 0.240 ± 0.039 us/op
|
||||
// AtomicExample09.sync:synchronizedListAdd avgt 10 0.232 ± 0.034 us/op
|
||||
// AtomicExample09.sync:synchronizedListGet avgt 10 0.248 ± 0.044 us/op
|
||||
```
|
||||
|
||||
> 测试需要下载 pom 包
|
||||
>
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
```
|
||||
|
||||
```plain
|
||||
<artifactId>lombok</artifactId>
|
||||
```
|
||||
|
||||
```plain
|
||||
<version>1.18.36</version>
|
||||
```
|
||||
|
||||
|
||||
> org.slf4j
|
||||
>
|
||||
|
||||
```plain
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
```
|
||||
|
||||
```plain
|
||||
<version>2.0.16</version>
|
||||
```
|
||||
|
||||
|
||||
> org.slf4j
|
||||
>
|
||||
|
||||
```plain
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
```
|
||||
|
||||
```plain
|
||||
<version>2.0.16</version>
|
||||
```
|
||||
|
||||
|
||||
> org.openjdk.jmh
|
||||
>
|
||||
|
||||
```plain
|
||||
<artifactId>jmh-core</artifactId>
|
||||
```
|
||||
|
||||
```plain
|
||||
<version>1.19</version>
|
||||
```
|
||||
|
||||
|
||||
> org.openjdk.jmh
|
||||
>
|
||||
|
||||
```plain
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
```
|
||||
|
||||
```plain
|
||||
<version>1.19</version>
|
||||
```
|
||||
|
||||
|
||||
|
||||
```plain
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: 我的笔记目录
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category:
|
||||
- 笔记
|
||||
- 记录
|
||||
- 学习
|
||||
---
|
||||
|
||||
<Catalog />
|
|
@ -0,0 +1,26 @@
|
|||
# pnpm设置全局存储路径
|
||||
## 设置存储路径
|
||||
### 查看当前存储路径
|
||||
```bash
|
||||
pnpm c get
|
||||
```
|
||||
|
||||
### 设置存储路径
|
||||
```bash
|
||||
pnpm config set global-bin-dir "D:\software\Plugins\pnpm\pnpm-store"
|
||||
pnpm config set cache-dir "D:\software\Plugins\pnpm\pnpm-store\cache"
|
||||
pnpm config set state-dir "D:\software\Plugins\pnpm\pnpm-store\state"
|
||||
pnpm config set global-dir "D:\software\Plugins\pnpm\pnpm-store\global"
|
||||
```
|
||||
|
||||
## 设置国内镜像
|
||||
### 查看当前镜像
|
||||
```bash
|
||||
pnpm config get registry
|
||||
```
|
||||
|
||||
### 设置国内镜像
|
||||
```bash
|
||||
pnpm config set registry https://registry.npmmirror.com/
|
||||
```
|
||||
|
Loading…
Reference in New Issue