博客系统

This commit is contained in:
yinkanglong
2024-01-07 21:39:29 +08:00
parent 8185de976e
commit 37b228c3c2
13 changed files with 1360 additions and 254 deletions

View File

@@ -1,16 +1,15 @@
> https://blog.csdn.net/m0_62618110/article/details/123704869
# Java 正则表达式
## 0 概述
### 简介
正则表达式regex是一个字符串由字面值字符和特殊符号组成是用来描述匹配一个字符串集合的模式可以用来匹配、查找字符串。
正则表达式的两个主要作用:
* 查找:在字符串中查找符合固定模式的子串
* 匹配:整个字符串是否符合某个格式
@@ -47,7 +46,6 @@ public class RegexMatches
}
```
## 1 正则表达式语法
* 在其他语言中,`\\`表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。
@@ -55,37 +53,35 @@ public class RegexMatches
* 不要在重复词符中使用空白。如B{3,6} ,不能写成 B{3, 6}。空格也是有含义的。
* 可以使用括号来将模式分组。(ab){3}匹配ababab , 而ab{3} 匹配 abbb。
| 字符 | 匹配 | 示例 |
|---|---|---|
| . | 任意单个字符,除换行符外 | jav.匹配java |
| [ ] | [ ] 中的任意一个字符 | java匹配j[abc]va |
| - | [ ] 内表示字符范围 | java匹配[a-z]av[a-g] |
| ^ | 在[ ]内的开头,匹配除[ ]内的字符之外的任意一个字符 | java匹配j[^b-f]va |
| | | 或 | x|y匹配x或y |
| \ | 将下一字符标记为特殊字符、文本、反向引用或八进制转义符 | \(匹配( |
| $ | 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。 | ;$匹配位于一行及外围的;号 |
| * | 零次或多次匹配前面的字符 | zo*匹配zoo或z |
| + | 一次或多次匹配前面的字符 | zo+匹配zo或zoo |
| ? | 零次或一次匹配前面的字符 | zo?匹配z或zo |
| p{n} | n 是非负整数。正好匹配 n 次 | o{2}匹配food中的两个o |
| p{n,} | n 是非负整数。至少匹配 n 次 | o{2}匹配foood中的所有o |
| p{n,m} | M 和 n 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次 | o{1,3}匹配fooood中的三个o |
| \p{P} | 一个标点字符 !"#$%&'()*+,-./:;<=>?@[\]^_'{|}~ | J\p{P}a匹配J?a |
| \b | 匹配一个字边界 | va\b匹配java中的va但不匹配javar中的va |
| \B | 非字边界匹配 | va\B匹配javar中的va但不匹配java中的va |
| \d | 数字字符匹配 | 1[\\d]匹配13 |
| \D | 非数字字符匹配 | [\\D]java匹配Jjava |
| \w | 单词字符 | java匹配[\\w]ava |
| \W | 非单词字符 | $java匹配[\\W]java |
| \s | 空白字符 | Java 2匹配Java\\s2 |
| \S | 非空白字符 | java匹配 j[\\S]va |
| \f | 匹配换页符 | 等效于\x0c和\cL |
| \n | 匹配换行符 | 等效于\x0a和\cJ |
| 字符 | 匹配 | 示例 |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------- |
| . | 任意单个字符,除换行符外 | jav.匹配java |
| [ ] | [ ] 中的任意一个字符 | java匹配j[abc]va |
| - | [ ] 内表示字符范围 | java匹配[a-z]av[a-g] |
| ^ | 在[ ]内的开头,匹配除[ ]内的字符之外的任意一个字符 | java匹配j[^b-f]va |
| | | 或 |
| \ | 将下一字符标记为特殊字符、文本、反向引用或八进制转义符 | \(匹配( |
| $ | 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。 | ;$匹配位于一行及外围的;号 | |
| * | 零次或多次匹配前面的字符 | zo*匹配zoo或z |
| + | 一次或多次匹配前面的字符 | zo+匹配zo或zoo |
| ? | 零次或一次匹配前面的字符 | zo?匹配z或zo |
| p{n} | n 是非负整数。正好匹配 n 次 | o{2}匹配food中的两个o |
| p{n,} | n 是非负整数。至少匹配 n 次 | o{2}匹配foood中的所有o |
| p{n,m} | M 和 n 是非负整数,其中 n<= m。匹配至少 n 次,至多 m 次 | o{1,3}匹配fooood中的三个o |
| \p{P} | 一个标点字符 !"#$%&'()*+,-./:;<=>?@[\]^_'{ | }~ |
| \b | 匹配一个字边界 | va\b匹配java中的va但不匹配javar中的va |
| \B | 非字边界匹配 | va\B匹配javar中的va但不匹配java中的va |
| \d | 数字字符匹配 | 1[\\d]匹配13 |
| \D | 非数字字符匹配 | [\\D]java匹配Jjava |
| \w | 单词字符 | java匹配[\\w]ava |
| \W | 非单词字符 | $java匹配[\\W]java |
| \s | 空白字符 | Java 2匹配Java\\s2 |
| \S | 非空白字符 | java匹配 j[\\S]va |
| \f | 匹配换页符 | 等效于\x0c和\cL |
| \n | 匹配换行符 | 等效于\x0a和\cJ |
### 分组说明
```
正则表达式-字符类
@@ -130,19 +126,70 @@ public class RegexMatches
正则表达式-分组括号()
```
## 2 基本概念
### Patter类和Matcher类
Pattern 类:
pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。
Matcher 类:
Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。
#### 匹配模式
**1、Pattern.MULTILINE模式的用法**
正则表达式中出现了^或者$, 默认只会匹配第一行. 设置了Pattern.MULTILINE模式,会匹配所有行。例如,
```
Pattern p1 = Pattern.compile("^.*b.*$");
//输出false,因为正则表达式中出现了^或$默认只会匹配第一行第二行的b匹配不到。
System.out.println(p1.matcher("a\nb").find());
Pattern p2 = Pattern.compile("^.*b.*$",Pattern.MULTILINE);
//输出true,指定了Pattern.MULTILINE模式就可以匹配多行了。
System.out.println(p2.matcher("a\nb").find());
```
**2、Pattern.DOTALL模式的用法**
默认情况下, 正则表达式中点(.)不会匹配换行符, 设置了Pattern.DOTALL模式, 才会匹配所有字符包括换行符。例如,
```
Pattern p1 = Pattern.compile("a.*b");
//输出false默认点(.)没有匹配换行符
System.out.println(p1.matcher("a\nb").find());
Pattern p2 = Pattern.compile("a.*b", Pattern.DOTALL);
//输出true,指定Pattern.DOTALL模式可以匹配换行符。
System.out.println(p2.matcher("a\nb").find());
```
**3、同时指定Pattern.MULTILINE和Pattern.DOTALL模式**
实际情况中要是比较复杂的情况可能Pattern.MULTILINE模式和Pattern.DOTAL模式需要同时指定来匹配多行下面看一下
```
Pattern p1 = Pattern.compile("^a.*b$");
//输出false
System.out.println(p1.matcher("cc\na\nb").find());
Pattern p2 = Pattern.compile("^a.*b$", Pattern.DOTALL);
//输出false,因为有^或&没有匹配到下一行
System.out.println(p2.matcher("cc\na\nb").find());
Pattern p3 = Pattern.compile("^a.*b$", Pattern.MULTILINE);
//输出false匹配到下一行但.没有匹配换行符
System.out.println(p3.matcher("cc\na\nb").find());
//指定多个模式,中间用|隔开
Pattern p4 = Pattern.compile("^a.*b$", Pattern.DOTALL|Pattern.MULTILINE);
//输出true
System.out.println(p4.matcher("cc\na\nb").find());
```
### 捕获组
1. 捕获组是把多个字符当成一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。
```
捕获组通过从左到右计算其括号来编号。
@@ -153,27 +200,23 @@ Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pat
(B(C))
(C)
```
2. 捕获组可以通过调用matcher对象的groupCount方法来查看表达式有多少个分组。groupCount方法返回一个int值来表示matcher对象当前有多少个捕获组
3. 还有一个特殊的组零group(0)它代表整个表达式。该组不包括在groupCount的返回值中
4. 以 (?) 开头的组是纯的非捕获 组,它不捕获文本,也不针对组合计进行计数。
4. 以 (?) 开头的组是纯的非捕获 组,它不捕获文本,也不针对组合计进行计数。
## 3 Matcher用法
### 索引方法
1. public int start()
返回以前匹配的初始索引。
返回以前匹配的初始索引。
2. public int start(int group)
返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引
返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引
3. public int end()
返回最后匹配字符之后的偏移量。
返回最后匹配字符之后的偏移量。
4. public int end(int group)
返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。
返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。
```java
import java.util.regex.Matcher;
@@ -203,13 +246,13 @@ public class RegexMatches
### 匹配和查找方法
1. public boolean lookingAt()
尝试将从区域开头开始的输入序列与该模式匹配。开头匹配。
尝试将从区域开头开始的输入序列与该模式匹配。开头匹配。
2. public boolean find()
尝试查找与该模式匹配的输入序列的下一个子序列。
尝试查找与该模式匹配的输入序列的下一个子序列。
3. public boolean find(int start
重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
4. public boolean matches()
尝试将整个区域与模式匹配。全局匹配。
尝试将整个区域与模式匹配。全局匹配。
```java
import java.util.regex.Matcher;
@@ -240,19 +283,19 @@ public class RegexMatches
}
}
```
### 替换方法
1. public Matcher appendReplacement(StringBuffer sb, String replacement)
实现非终端添加和替换步骤。
实现非终端添加和替换步骤。
2. public StringBuffer appendTail(StringBuffer sb)
实现终端添加和替换步骤。
实现终端添加和替换步骤。
3. public String replaceAll(String replacement)
替换模式与给定替换字符串相匹配的输入序列的每个子序列。
替换模式与给定替换字符串相匹配的输入序列的每个子序列。
4. public String replaceFirst(String replacement)
替换模式与给定替换字符串匹配的输入序列的第一个子序列。
替换模式与给定替换字符串匹配的输入序列的第一个子序列。
5. public static String quoteReplacement(String s)
返回指定字符串的字面替换字符串。这个方法返回一个字符串就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。
返回指定字符串的字面替换字符串。这个方法返回一个字符串就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。
```java
import java.util.regex.Matcher;
@@ -298,7 +341,6 @@ public class RegexMatches
}
```
## 4 String自带的正则表达式功能
见String
见String

View File

@@ -2,11 +2,13 @@
> 数据库与数据源不是同一个东西。。。
> 三层关键概念需要理解
>
> 1. 数据库驱动mysql、hsqldb
> 2. 数据源datasource和数据库连接池Harica、Druid
> 3. 数据库操作工具JDBCTemplates、Mybatis
### 数据源配置
pom.xml
```
@@ -17,7 +19,9 @@ pom.xml
```
### 嵌入式数据库驱动
嵌入式数据库支持H2、HSQL、Derby。不需要任何配置被集成到springboot的jar包当中。
```
<dependency>
<groupId>org.hsqldb</groupId>
@@ -36,7 +40,9 @@ pom.xml
<artifactId>mysql-connector-java</artifactId>
</dependency>
```
* 配置数据源信息
```
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=dbuser
@@ -44,11 +50,20 @@ spring.datasource.password=dbpass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver//定义了数据引擎
```
### 自动配置
* DataSourceAutoConfiguration
* 底层自动配置了默认的数据源Hicari
* DataSourceTransactionAutoConfiguration
* JdbcTemplateAutoConfiguration
* JndiAutoConfiguration
### 连接JNDI数据源
JNDI避免了程序与数据库之间的紧耦合是指更容易配置和部署。
JNDI不需要用户使用java代码与数据库建立连接而是将连接交给应用服务器进行管理。java负责与应用服务器上的JNDI通信。
```
spring.datasource.jndi-name=java:jboss/datasources/customers
```
@@ -56,6 +71,7 @@ spring.datasource.jndi-name=java:jboss/datasources/customers
## 2 JdbcTemplate操作数据库
### 准备数据库
```
CREATE TABLE `User` (
`name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL,
@@ -64,6 +80,7 @@ CREATE TABLE `User` (
```
### 编写领域对象
并不是MVC的一部分。数据层实现数据访问
```
@@ -78,9 +95,11 @@ public class User {
```
### 编写数据访问对象
并非MVC的一部分。服务层实现业务逻辑
* 定义包含插入、删除、查询的抽象接口UserService
```java
public interface UserService {
@@ -167,7 +186,9 @@ public class UserServiceImpl implements UserService {
```
### 编写单元测试用例
创建对UserService的单元测试用例通过创建、删除和查询来验证数据库操作的正确性。
```java
@RunWith(SpringRunner.class)
@SpringBootTest
@@ -208,4 +229,4 @@ public class Chapter31ApplicationTests {
}
}
```
```

View File

@@ -1,9 +1,6 @@
> 对象关系映射模型Hibernate。用来实现非常轻量级的对象的封装。将对象与数据库建立映射关系。实现增删查改。
> MyBatis与Hibernate非常相似。对象关系映射模型ORG。java对象与关系数据库映射的模型。
## 1 配置MyBatis
### 最佳实践
@@ -16,8 +13,7 @@
● 复杂方法编写mapper.xml进行绑定映射
@MapperScan("com.atguigu.admin.mapper") 简化,其他的接口就可以不用标注@Mapper注解
### 在pom.xml中添加MyBatis依赖
### 添加MyBatis依赖
```
<dependency>
@@ -33,6 +29,7 @@
```
### 配置数据库连接
在application.properties中配置mysql的链接配置
```sh
@@ -42,7 +39,7 @@ spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
```
### 创建关系数据表
### 创建数据表
```sql
CREATE TABLE `User` (
@@ -53,7 +50,7 @@ CREATE TABLE `User` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
```
### 创建数据表的java对象
### 创建java对象
```java
@Data
@@ -72,17 +69,16 @@ public class User {
}
```
### MyBatis参数传递
## 2 MyBatis参数传递
### 使用@Param参数传递
使用@Param参数传递
```
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
```
### 使用map 传递参数
使用map 传递参数
```
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER})")
@@ -94,13 +90,14 @@ map.put("age", 40);
userMapper.insertByMap(map);
```
### 使用普通java对象
使用普通java对象
```
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insertByUser(User user);
```
## 3 注解模式
## 2注解模式
### 创建数据表的操作接口
@@ -116,7 +113,9 @@ public interface UserMapper {
}
```
### 增删改查操作
```java
public interface UserMapper {
@@ -133,7 +132,9 @@ public interface UserMapper {
void delete(Long id);
}
```
对增删查改的调用
```java
@Transactional
@RunWith(SpringRunner.class)
@@ -163,9 +164,12 @@ public class ApplicationTests {
}
```
## 4 XML方式
## 3 XML方式
### 创建Mapper文件
在应用主类中增加mapper的扫描包配置
### 在应用主类中增加mapper的扫描包配置
```
@MapperScan("com.didispace.chapter36.mapper")
@SpringBootApplication
@@ -178,7 +182,8 @@ public class Chapter36Application {
}
```
### Mapper包下创建User表
Mapper包下创建User表
```
public interface UserMapper {
@@ -189,12 +194,13 @@ public interface UserMapper {
}
```
### 在配置文件中通过mybatis.mapper-locations参数指定xml配置的位置
在配置文件中通过mybatis.mapper-locations参数指定xml配置的位置
```
mybatis.mapper-locations=classpath:mapper/*.xml
```
### xml配置目录下创建User表的mapper配置
xml配置目录下创建User表的mapper配置
```
<?xml version="1.0" encoding="UTF-8" ?>
@@ -213,6 +219,7 @@ mybatis.mapper-locations=classpath:mapper/*.xml
```
### 对xml方式进行调用
```
@Slf4j
@RunWith(SpringRunner.class)
@@ -234,11 +241,19 @@ public class Chapter36ApplicationTests {
}
```
## 5 整合 MyBatis-Plus 完成CRUD
## 4 MyBatis-Plus
### 什么是MyBatis-Plus
MyBatis-Plus简称 MP是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mybatis plus 官网
建议安装 MybatisX 插件
建议安装 MybatisX 插件
* 通用Mapper能力
* 增强单表查询能力
* 多种主键策略支持UUID和雪花算法
* 基础代码生成器
* 乐观锁
### 引入
@@ -249,7 +264,9 @@ mybatis plus 官网
<version>3.4.1</version>
</dependency>
```
### 自动配置
● MybatisPlusAutoConfiguration 配置类MybatisPlusProperties 配置项绑定。mybatis-plusxxx 就是对mybatis-plus的定制
● SqlSessionFactory 自动配置好。底层是容器中默认的数据源
● mapperLocations 自动配置好的。有默认值。classpath*:/mapper/**/*.xml任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件放在 mapper下
@@ -261,55 +278,168 @@ mybatis plus 官网
优点:
● 只需要我们的Mapper继承 BaseMapper 就可以拥有crud能力
### 进阶操作
1. 设置表明和自增主键
2. 使用条件构造器
1. `boolean condition`表示该条件**是否**加入最后生成的sql中
2. 支持多种条件表达式拼接SQL语句。每种表达式有多种类型的接口
```
AbstractWrapper
allEq
eq
ne
gt
ge
lt
le
between
notBetween
like
notLike
likeLeft
likeRight
notLikeLeft
notLikeRight
isNull
isNotNull
in
notIn
inSql
notInSql
groupBy
orderByAsc
orderByDesc
orderBy
having
func
or
and
nested
apply
last
exists
notExists
QueryWrapper
select
UpdateWrapper
set
setSql
lambda
```
### CRUD实例
### CRUD
DAO层
```java
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){
public interface ArticleMapper extends BaseMapper<Article> {
}
userService.removeById(id);
ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteById(T entity);
default int deleteByMap(Map<String, Object> columnMap) {
return this.delete((Wrapper)Wrappers.query().allEq(columnMap));
}
int delete(@Param("ew") Wrapper<T> queryWrapper);
@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历
// response.sendError
// List<User> users = Arrays.asList(new User("zhangsan", "123456"),
// new User("lisi", "123444"),
// new User("haha", "aaaaa"),
// new User("hehe ", "aaddd"));
// model.addAttribute("users",users);
//
// if(users.size()>3){
// throw new UserTooManyException();
// }
//从数据库中查出user表中的用户进行展示
int deleteBatchIds(@Param("coll") Collection<?> idList);
//构造分页参数
Page<User> page = new Page<>(pn, 2);
//调用page进行分页
Page<User> userPage = userService.page(page, null);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
// userPage.getRecords()
// userPage.getCurrent()
// userPage.getPages()
model.addAttribute("users",userPage);
return "table/dynamic_table";
default int update(@Param("ew") Wrapper<T> updateWrapper) {
return this.update((Object)null, updateWrapper);
}
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
void selectBatchIds(@Param("coll") Collection<? extends Serializable> idList, ResultHandler<T> resultHandler);
default List<T> selectByMap(Map<String, Object> columnMap) {
return this.selectList((Wrapper)Wrappers.query().allEq(columnMap));
}
default void selectByMap(Map<String, Object> columnMap, ResultHandler<T> resultHandler) {
this.selectList((Wrapper)Wrappers.query().allEq(columnMap), resultHandler);
}
default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {
return this.selectOne(queryWrapper, true);
}
default T selectOne(@Param("ew") Wrapper<T> queryWrapper, boolean throwEx) {
List<T> list = this.selectList(queryWrapper);
int size = list.size();
if (size == 1) {
return list.get(0);
} else if (size > 1) {
if (throwEx) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + size);
} else {
return list.get(0);
}
} else {
return null;
}
}
default boolean exists(Wrapper<T> queryWrapper) {
Long count = this.selectCount(queryWrapper);
return null != count && count > 0L;
}
Long selectCount(@Param("ew") Wrapper<T> queryWrapper);
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
void selectList(@Param("ew") Wrapper<T> queryWrapper, ResultHandler<T> resultHandler);
List<T> selectList(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);
void selectList(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper, ResultHandler<T> resultHandler);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
void selectMaps(@Param("ew") Wrapper<T> queryWrapper, ResultHandler<Map<String, Object>> resultHandler);
List<Map<String, Object>> selectMaps(IPage<? extends Map<String, Object>> page, @Param("ew") Wrapper<T> queryWrapper);
void selectMaps(IPage<? extends Map<String, Object>> page, @Param("ew") Wrapper<T> queryWrapper, ResultHandler<Map<String, Object>> resultHandler);
<E> List<E> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
<E> void selectObjs(@Param("ew") Wrapper<T> queryWrapper, ResultHandler<E> resultHandler);
default <P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper) {
page.setRecords(this.selectList(page, queryWrapper));
return page;
}
default <P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper) {
page.setRecords(this.selectMaps(page, queryWrapper));
return page;
}
}
```
### Service层
```java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
@@ -320,4 +450,240 @@ public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements Use
public interface UserService extends IService<User> {
}
```
```
#### 完整实例
```java
package com.baomidou.mybatisplus.samples.crud;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.samples.crud.entity.User;
import com.baomidou.mybatisplus.samples.crud.entity.User2;
import com.baomidou.mybatisplus.samples.crud.mapper.User2Mapper;
import com.baomidou.mybatisplus.samples.crud.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* <p>
* 内置 CRUD 演示
* </p>
*
* @author hubin
* @since 2018-08-11
*/
@SpringBootTest
public class CrudTest {
@Autowired
private UserMapper mapper;
@Autowired
private User2Mapper user2Mapper;
@Test
public void aInsert() {
User user = new User();
user.setName("小羊");
user.setAge(3);
user.setEmail("abc@mp.com");
assertThat(mapper.insert(user)).isGreaterThan(0);
// 成功直接拿回写的 ID
assertThat(user.getId()).isNotNull();
}
@Test
public void bDelete() {
assertThat(mapper.deleteById(3L)).isGreaterThan(0);
assertThat(mapper.delete(new QueryWrapper<User>()
.lambda().eq(User::getName, "Sandy"))).isGreaterThan(0);
}
@Test
public void cUpdate() {
assertThat(mapper.updateById(new User().setId(1L).setEmail("ab@c.c"))).isGreaterThan(0);
assertThat(
mapper.update(
new User().setName("mp"),
Wrappers.<User>lambdaUpdate()
.set(User::getAge, 3)
.eq(User::getId, 2)
)
).isGreaterThan(0);
User user = mapper.selectById(2);
assertThat(user.getAge()).isEqualTo(3);
assertThat(user.getName()).isEqualTo("mp");
mapper.update(
null,
Wrappers.<User>lambdaUpdate().set(User::getEmail, null).eq(User::getId, 2)
);
assertThat(mapper.selectById(1).getEmail()).isEqualTo("ab@c.c");
user = mapper.selectById(2);
assertThat(user.getEmail()).isNull();
assertThat(user.getName()).isEqualTo("mp");
mapper.update(
new User().setEmail("miemie@baomidou.com"),
new QueryWrapper<User>()
.lambda().eq(User::getId, 2)
);
user = mapper.selectById(2);
assertThat(user.getEmail()).isEqualTo("miemie@baomidou.com");
mapper.update(
new User().setEmail("miemie2@baomidou.com"),
Wrappers.<User>lambdaUpdate()
.set(User::getAge, null)
.eq(User::getId, 2)
);
user = mapper.selectById(2);
assertThat(user.getEmail()).isEqualTo("miemie2@baomidou.com");
assertThat(user.getAge()).isNull();
}
@Test
public void dSelect() {
mapper.insert(
new User().setId(10086L)
.setName("miemie")
.setEmail("miemie@baomidou.com")
.setAge(3));
assertThat(mapper.selectById(10086L).getEmail()).isEqualTo("miemie@baomidou.com");
User user = mapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getId, 10086));
assertThat(user.getName()).isEqualTo("miemie");
assertThat(user.getAge()).isEqualTo(3);
mapper.selectList(Wrappers.<User>lambdaQuery().select(User::getId))
.forEach(x -> {
assertThat(x.getId()).isNotNull();
assertThat(x.getEmail()).isNull();
assertThat(x.getName()).isNull();
assertThat(x.getAge()).isNull();
});
mapper.selectList(new QueryWrapper<User>().select("id", "name"))
.forEach(x -> {
assertThat(x.getId()).isNotNull();
assertThat(x.getEmail()).isNull();
assertThat(x.getName()).isNotNull();
assertThat(x.getAge()).isNull();
});
}
@Test
public void orderBy() {
List<User> users = mapper.selectList(Wrappers.<User>query().orderByAsc("age"));
assertThat(users).isNotEmpty();
//多字段排序
List<User> users2 = mapper.selectList(Wrappers.<User>query().orderByAsc("age", "name"));
assertThat(users2).isNotEmpty();
//先按age升序排列age相同再按name降序排列
List<User> users3 = mapper.selectList(Wrappers.<User>query().orderByAsc("age").orderByDesc("name"));
assertThat(users3).isNotEmpty();
}
@Test
public void selectMaps() {
List<Map<String, Object>> mapList = mapper.selectMaps(Wrappers.<User>query().orderByAsc("age"));
assertThat(mapList).isNotEmpty();
assertThat(mapList.get(0)).isNotEmpty();
System.out.println(mapList.get(0));
}
@Test
public void selectMapsPage() {
IPage<Map<String, Object>> page = mapper.selectMapsPage(new Page<>(1, 5), Wrappers.<User>query().orderByAsc("age"));
assertThat(page).isNotNull();
assertThat(page.getRecords()).isNotEmpty();
assertThat(page.getRecords().get(0)).isNotEmpty();
System.out.println(page.getRecords().get(0));
}
@Test
public void orderByLambda() {
List<User> users = mapper.selectList(Wrappers.<User>lambdaQuery().orderByAsc(User::getAge));
assertThat(users).isNotEmpty();
//多字段排序
List<User> users2 = mapper.selectList(Wrappers.<User>lambdaQuery().orderByAsc(User::getAge, User::getName));
assertThat(users2).isNotEmpty();
//先按age升序排列age相同再按name降序排列
List<User> users3 = mapper.selectList(Wrappers.<User>lambdaQuery().orderByAsc(User::getAge).orderByDesc(User::getName));
assertThat(users3).isNotEmpty();
}
@Test
public void testSelectMaxId() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("max(id) as id");
User user = mapper.selectOne(wrapper);
System.out.println("maxId=" + user.getId());
List<User> users = mapper.selectList(Wrappers.<User>lambdaQuery().orderByDesc(User::getId));
Assertions.assertEquals(user.getId().longValue(), users.get(0).getId().longValue());
}
@Test
public void testGroup() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("age, count(*)")
.groupBy("age");
List<Map<String, Object>> maplist = mapper.selectMaps(wrapper);
for (Map<String, Object> mp : maplist) {
System.out.println(mp);
}
/**
* lambdaQueryWrapper groupBy orderBy
*/
LambdaQueryWrapper<User> lambdaQueryWrapper = new QueryWrapper<User>().lambda()
.select(User::getAge)
.groupBy(User::getAge)
.orderByAsc(User::getAge);
for (User user : mapper.selectList(lambdaQueryWrapper)) {
System.out.println(user);
}
}
@Test
public void testTableFieldExistFalse() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("age, count(age) as count")
.groupBy("age");
List<User> list = mapper.selectList(wrapper);
list.forEach(System.out::println);
list.forEach(x -> {
Assertions.assertNull(x.getId());
Assertions.assertNotNull(x.getAge());
Assertions.assertNotNull(x.getCount());
});
mapper.insert(
new User().setId(10088L)
.setName("miemie")
.setEmail("miemie@baomidou.com")
.setAge(3));
User miemie = mapper.selectById(10088L);
Assertions.assertNotNull(miemie);
}
@Test
public void testSqlCondition() {
Assertions.assertEquals(user2Mapper.selectList(Wrappers.<User2>query()
.setEntity(new User2().setName("n"))).size(), 2);
Assertions.assertEquals(user2Mapper.selectList(Wrappers.<User2>query().like("name", "J")).size(), 2);
Assertions.assertEquals(user2Mapper.selectList(Wrappers.<User2>query().gt("age", 18)
.setEntity(new User2().setName("J"))).size(), 1);
}
}
```

View File

@@ -19,12 +19,12 @@ Redis 是一个开源BSD许可内存中的数据结构存储系统
### 配置原理
自动配置:
RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
自动注入了RedisTemplate<Object, Object> xxxTemplate
自动注入了StringRedisTemplatekv都是String
keyvalue
底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis
* RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
* 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
* 自动注入了RedisTemplate<Object, Object> xxxTemplate
* 自动注入了StringRedisTemplatekv都是String
* keyvalue
* 底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis
### 操作

View File

@@ -308,4 +308,47 @@ ConfigurableApplicationContext context = new SpringApplicationBuilder(Applicatio
.sources(ApplicationConfiguration.class) //可以多个Class
.run();
context.close(); //上下文关闭
```
```
## 5 spring.fatoriesSPI中的扩展点
在 Spring 框架中spring.factories 文件是一个 SPI (Service Provider Interface) 的形式它可以用于定义和加载多个框架扩展点。这些扩展点允许开发人员插入自定义逻辑或覆盖框架提供的默认行为。Spring Boot 使用 spring.factories 文件来支持各种类型的扩展和自定义。
spring.factories 文件中可以配置的一些主要扩展点包括:
自动配置 (EnableAutoConfiguration):
通过在 spring.factories 中列出自动配置类Spring Boot 可以在启动时自动应用这些配置。这些配置通常会基于类路径和其他环境条件来条件性地配置 Bean。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyAutoConfiguration
应用程序事件监听器 (ApplicationListener):
应用程序事件监听器可以监听和响应 Spring 应用程序上下文中的各种事件,如应用启动、停止和环境准备等。
org.springframework.context.ApplicationListener=com.example.MyApplicationListener
失败分析 (FailureAnalyzer):
当应用程序启动失败时,失败分析器可以提供特定的错误报告和解决建议。
org.springframework.boot.diagnostics.FailureAnalyzer=com.example.MyFailureAnalyzer
模板引擎 (TemplateAvailabilityProviders):
用于检测是否应该呈现模板以及哪种类型的模板可用。
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=com.example.MyTemplateAvailabilityProvider
环境后处理器 (EnvironmentPostProcessor):
环境后处理器允许开发人员在应用程序上下文刷新之前对环境(配置属性)进行编程更改。
org.springframework.boot.env.EnvironmentPostProcessor=com.example.MyEnvironmentPostProcessor
应用程序上下文初始化器 (ApplicationContextInitializer):
在 ApplicationContext 实例化之前初始化 ApplicationContext 的回调接口,可以用来设置上下文的状态或添加自定义的逻辑。
org.springframework.context.ApplicationContextInitializer=com.example.MyApplicationContextInitializer
命令行运行器 (CommandLineRunner) 和 应用程序运行器 (ApplicationRunner):
它们允许在 SpringApplication 运行之后,执行一些额外的代码。
org.springframework.boot.CommandLineRunner=com.example.MyCommandLineRunner
org.springframework.boot.ApplicationRunner=com.example.MyApplicationRunner
管理端点 (Endpoint):
自定义管理端点可以通过 Spring Boot Actuator 暴露。
org.springframework.boot.actuate.endpoint.Endpoint=com.example.MyCustomEndpoint
使用 spring.factories 文件为框架扩展点提供实现有几个好处:它支持条件化配置、松耦合、易于维护,并且可以在不修改主应用程序代码的情况下增强框架功能。
为了使用这些扩展点,开发人员需要在项目的 resources/META-INF/spring.factories 文件中指定他们的实现。然后当应用程序启动时Spring Boot 将自动发现并注册这些实现。

View File

@@ -1,4 +1,3 @@
## 1 架构设计
### 架构和技术说明
@@ -6,6 +5,7 @@
![](./draw/arch.drawio.svg)
本项目主要架构包括三部分
1. 数据层。通过vscode和markdown创建的笔记内容数据存储在github上。需要对笔记内容进行重构以适应多种形式的输出。
2. 业务层。提供博客系统的业务数据访问。
1. Sync模块从github同步数据存储到本地数据库中对数据进行分析和处理通过只能算法生成博客的元数据包括tag、description等对数据内容进行筛选根据配置和完整性校验可以通过定时任务和Github的流水线触发数据的增量处理任务。
@@ -13,16 +13,20 @@
3. Article模块提供文章的多级索引包括目录、tag、日期、关键字等方便用户快速找到自己感兴趣的文章也方便自己回顾阅读同时提供了一些基础的交互能力和数据统计能力包括点赞、评论、访问量。整个系统通过标准Rest接口对外提供访问。
3. 渲染层。通过vue前端框架和element组件库快速构建前端页面提供交互能力。访问后端的Rest接口进行渲染。
## 2 模型设计
### 2.1 对象设计
文章的对象模型主要包括以下五个:文章、分类、标签、评论、用户。
![](./draw/ER.drawio.svg)
### 2.2 逻辑设计
为了进一步简化设计和防止冗余数据计算没有必要将分类和Tag进行落库。其本身是对Article表的冗余数据每次更新article表全量刷新category和tag表冗余且没有必要直接将tag和Category的构建放到缓存中即可。访问即可刷新重新构建。
在逻辑设计的时候应该遵循以下原则
1. 不允许使用外键
2. 尽量减少连表操作。连表操作会大大增加数据管理的难度。
@@ -30,49 +34,49 @@
文章与目录文章与目录是一对多的关系。可以直接在文章中存储目录的id。不同目录下可能存在同名的子目录。所以目录名称的唯一键应该是 父目录+子目录。
文章与标签:文章与标签是多对多的关系。不需要建立关联关系表,直接将标签以逗号分割的形式添加到文章里即可。多选可以使用 Like or的方法直接进行查询。标签的名称是唯一键,主要用来显示标签云,不用每次都统计标签的数量
文章与标签:文章与标签是多对多的关系。不需要建立关联关系表,直接将标签以逗号分割的形式添加到文章里即可。多选可以使用 Like and的方法直接进行查询。标签的名称是唯一键,主要用来显示标签云,不用每次都统计标签的数量
文章与评论文章与评论是一对多关系。不需要建立关联关系。评论中存储文章id的外键。评论之间可以相互回复parent_id标识其上一级评论如果parent_id为零表示直接回复当篇文章。
> hash_value字段用来标识文章的完整性
>
> category添加多级目录标识表去掉使用路径标识category/category2/category3
![](./draw/logic.drawio.svg)
### 2.3 物理设计
```sql
--【1】创建 blog数据库
CREATE DATABASE blog SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
--【2】创建用户表 user
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(32) NOT NULL COMMENT '用户名',
`password` varchar(64) DEFAULT NULL COMMENT '用户密码',
`nickname` varchar(32) DEFAULT NULL COMMENT '用户',
`email` varchar(64) DEFAULT NULL COMMENT '用户邮箱',
`role` tinyint DEFAULT 0 COMMENT '注册时间0表示普通用户1表示管理员2表示匿名用户',
`photo` varchar(256) DEFAULT NULL COMMENT '用户头像',
`uuid` varchar(32) DEFAULT NULL COMMENT '唯一标识',
`ip` varchar(20) NOT NULL COMMENT '用户IP',
PRIMARY KEY (`id`),
KEY `user_name` (`user_name`),
KEY `email` (`email`)
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(32) NOT NULL COMMENT '用户名',
`password` varchar(64) DEFAULT NULL COMMENT '用户密码',
`nickname` varchar(32) DEFAULT NULL COMMENT '用户昵称',
`email` varchar(64) DEFAULT NULL COMMENT '用户邮箱',
`role` tinyint(4) DEFAULT 0 COMMENT '注册时间0表示普通用户1表示管理员2表示匿名用户',
`photo` varchar(256) DEFAULT NULL COMMENT '用户头像',
`uuid` varchar(36) DEFAULT NULL COMMENT '唯一标识',
`ip` varchar(15) NOT NULL COMMENT '用户IP',
PRIMARY KEY (`id`),
KEY `username` (`username`),
KEY `email` (`email`)
);
--【3】创建分类表 \category
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '目录ID',
`name` varchar(100) NOT NULL COMMENT '标签名称',
`parent` bigint NOT NULL DEFAULT '0' COMMENT '目录ID0表示顶层目录',
`name` varchar(128) NOT NULL COMMENT '目录名称',
`count` int NOT NULL DEFAULT '0' COMMENT '数量',
`description` text DEFAULT NULL COMMENT '目录下README.MD文件的内容',
PRIMARY KEY (`id`),
UNITUQ KEY `path`(`parent`,`name`)
UNIQUE KEY `name_key`(`name`)
)COMMENT='分类表';
@@ -95,29 +99,26 @@ CREATE TABLE `tag` (
CREATE TABLE `article` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '博文ID',
`user_id` bigint NOT NULL COMMENT '发表用户ID',
`category_id` bigint NOT NULL COMMENT '目录ID',
`title` varchar(128) NOT NULL COMMENT '博文标题',
`category` varchar(128) NOT NULL COMMENT '目录名称',
`tags` varchar(128) NOT NULL COMMENT '标签',
`content` longtext NOT NULL COMMENT '博文内容',
`description` text NOT NULL COMMENT '博文简介',
`love` int NOT NULL DEFAULT 0 COMMENT '喜欢量',
`view` int NOT NULL DEFAULT 0 COMMENT '浏览量',
`order` int NOT NULL DEFAULT 0 COMMENT '评论总数',
`order_number` int NOT NULL DEFAULT 0 COMMENT '评论总数',
`state` tinyint NOT NULL DEFAULT 0 COMMENT '状态0标识正常1表示草稿或隐藏',
`path` varchar(256) NOT NULL COMMENT '文章相对路径',
`cover` varchar(256) DEFAULT NULL COMMENT '文章封面图片路径',
PRIMARY KEY (`article_id`),
`hash_value` varchar(256) NOT NULL COMMENT '文章哈希',
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP(),
`modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `category_id`(`category_id`)
UNIQUE KEY `path`(`path`),
KEY `category`(`category`)
) COMMENT='文章表';
--【6】创建文章标签表 tag
CREATE TABLE `article_tag` (
`id` bigint NOT NULL AUTO_INCREMENT,
`article_id` bigint NOT NULL COMMENT '文章id',
`tag_id` bigint NOT NULL COMMENT '标签id',
PRIMARY KEY (`id`),
UNIQUE KEY `article_tag_id`(`article_id`,`tag_id`)
) COMMENT='文章标签表';
--【7】创建评论表 comment
@@ -126,7 +127,7 @@ CREATE TABLE `comment` (
`user_id` bigint NOT NULL COMMENT '创建者id',
`article_id` bigint NOT NULL COMMENT '文章id',
`parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父评论ID'
`content` varchar(256) DEFAULT '' COMMENT '评论内容',
`content` varchar(256) DEFAULT '' COMMENT '评论内容',
PRIMARY KEY (`id`)
)COMMENT='评论表';
@@ -139,4 +140,3 @@ CREATE TABLE `comment` (
-- ALTER TABLE blog_comment ADD CONSTRAINT FK_COMMENT_ARTICLEID FOREIGN KEY(article_id) REFERENCES blog_article(id);
```

View File

@@ -1,31 +1,61 @@
# 功能模块设计
# 后端功能设计
## 1 文章同步功能
## 1 数据处理功能
### 数据同步
将数据从github的仓库中同步到本地经过一系列数据处理操作将数据转化为结构化的本地数据方便进行展示。
### 构建目录
### 1.1 数据同步
### 构建文章
创建一个GitRepositoryManage类用来实现git相关的一系列操作。
### 构建标签
- [X] 打开Git仓库。Clone一个仓库到本地。如果仓库存在则Open一个仓库。
- [X] 获取文件时间。通过Git提交的历史记录得到每一个文件变更的历史数据。
- [X] 更新仓库内容。通过GitPull命令刷新仓库的内容。
- [X] 创建一个SyncDataService添加一个定时任务每隔1小时check一遍数据一致性。
- [X] 使用mysql创建数据表。
- [X] 使用mybatis-plus操作数据表实现增删查改。用到DAO层即可。
- [X] 重构了文章的设计文档。tag/category本来就是附属内容为了简化数据结构只在内存中生成一份数据而不是落库。
### 保存数据
### 1.2 构建文章
遍历文章获取一下内容
* [X] 构建文章分类:如果一个目录下有.md文件则该目录的相对路径可以作为分类。
* [X] 构建文章标题:使用无后缀名的文件名称作为标题
* [X] 构建文章内容:保存原始数据,原始数据意味着丢失更少的信息。
* [X] 填充文件路径:获取文章地址相对根目录的相对路径。
* [X] 生成文件标签:使用标签生成算法提取文章中的五个关键字。并增加标签计数。
* [X] 生成文章简介使用简介生成算法提取文章中100字简介。
* [X] 获取文章时间使用Git工具获取文章同步的时间
* [X] 生成文章哈希使用sh256生成文章哈希值校验文章是否发生变化。
* [X] 将文章数据落库。文章和分类、标签之间是软索引。通过路径和标签名称进行管理。
## 2 文章管理功能
通过各种方式索引文章。
### 2.1 文章索引功能
* [X] 分页查询文章列表
* [X] 按目录搜索。需要传递目录的全路径,在文章中直接检索该目录下的文章。
* [X] 按标签搜索。支持传递标签多个标签,在文章中列表中直接匹配标签列。支持多个标签交叉匹配
* [X] 按年月归档。支持根据不同的创建年月索引归档的文章。建立文章的时光轴,并且能够通过下拉不断更新时光轴周围的文章。
* [X] 文章刷新功能当笔记提交到git仓库后自动触发部署流水线回调系统接口触发数据同步和数据处理。
* [X] 支持幂等处理(同一时间只允许调用一次)
* [X] 支持缓存清空操作。及时更新缓存。
### 2.2 标签搜索功能
* [ ] 查询目录标签云,并支持缓存。
* [ ] 查询文章标签云。并支持缓存。
* [ ] 查询年月日归档信息。并支持缓存。
## 3 评论管理功能
## 4 用户管理功能
## 5 后台管理功能
## 6 自动部署功能

View File

@@ -1,4 +1,4 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="721px" height="761px" viewBox="-0.5 -0.5 721 761" content="&lt;mxfile&gt;&lt;diagram id=&quot;X03snOlxasRXa0Irbqj0&quot; name=&quot;Page-1&quot;&gt;1Z1vb5s4GMA/jV9ehTEY+2Vo0m3SnVRpOt3dS5bQBo2GitK1vU9/tjFJMA+XQMBONWkDBwj8/Pj5T4bI7dP7lzJ53v5RbNIc+d7mHZEl8n2CqfhbDnzUA4zzeuCxzDb1ED4MfM/+TfWgp0dfs0360jqwKoq8yp7bg+tit0vXVWssKcvirX3YQ5G3v/U5eUw7A9/XSd4d/SvbVFv9FKF3GP+aZo/b5puxpz95SpqD9cDLNtkUb0dDZIXIbVkUVb319H6b5pJdw6U+767n0/2NlemuOusEpu+j+mgeLt2IZ9W7u2In/om31VMu9rDYFNctP/4WO95N2Oz+I3ebneV7a+9D771USVktJPvDZbs3q+//pXgt1/p2/EhPcFI+pvowLS3yTo/O00/4JS2eUvHV4oAyzZMq+9WetURP/uP+uAMfsaERwbgCbywubAVWgLuw3NGKphIufIJXutsYtMTIXSZvTH1+nqB10TV6ygE6Po2gHTjaRkdcoWN4tNRdBzpnQsf8T04O+87Q0U+u6pi79RpNInWuwIXOuA01EeCNjpIVZukR+XAH4pxJngABjywh0N/9K8lf9UXRKkI8QAuGVhSJqYi766csXnebdKOhvG2zKv3+nKjHeRMBGSQWAIJfaVml7/8r1E1A5+lVoOM5n2k/+e0QHeEm5NkeRUbUu5wQ6RASn3TEJs9FcJiepvEghOW2yItSnUceHlK6XiuXvCx+pkefbCL+w/P0GY1EJq9VMRHSNlEcdImGAFAyAdAQEDmKxFqMQyl7iwjFQBh0LuAJ2AQNjEbciD04FIDDkfCcFisFZ4ViwP+0CSc6vRbngkOGut6jA10SdHUyxq5MMRnqOI9/bgo8t+fsuclg+zwuSh3Pi3V5OUwfBWN5DYwS2tZKX3c0xHByYPrU+yITX9tn8ygP21eoJ1GfZFDf38V5ExEOnYj3rKrngQW+3pcT8Zt344lVUA8cJkPufBzPzPBwRVziPi0z8VBpiSYMYTigQNwtiGliv1P51LnA+c4sTmArbz8bOmepmmB4anUmBM5SLqGdMsZs4AJn4Man+aznqiBwzjxFPjwpP0cmB2DCPVsIBvsc1hAQWwguyJJHfmsF3XjByUUk9sa5UFMw9W0xrS/RSkiEiC0RX6i0DUVAgt11gjAEMjazJQjp8IXn0FdqovlWRdCZwaNT1bXcsXPmaNLxwc21sHMW3zR3Y6o1hhiVedYYoziWVxMqbuEhdqeqIrHMv16brouoRV0XzeNnnZIdoO2IMleyE/lXw8Bd69XwzOxMDCJnMUdzN0YhS6qLSNVqlih2ry6o1y5mWXWNmoYZQ80u7rSalaxcl7PaeIjFQijulpYVnQDFgbJGRBoel3T2j9kU+5og0wadoM9ECzssNoR9jusQRJhrKqukcgOogFjkZVQYfJvC1FNWj5UwqUAt9q4bF+YWcUGFdrPx5du9YzwO+UBx/964cbTwEdQ9ZVNz+23dFFhcbH5P+CDkJlQqfIVioAxgkw416Fhs0/AhxyiSTSwCykr1+fCVamuJXBs4ExPFFjFBzhGVdGS4yWQjlNBC14EJG5iAmHM2TJCXJFykW6mA5KJTPoGM2RcSmVN3iRuYQouYetylUBk01cYp3G7pN60Qu8DsD29gnAKs0ZFHLXbk+awLtkPPUt6tF+WJduLmJbfZO2xo22PZR1PNJer77LTYnLxQaM7kdL06DRtz4dwhjpUaxnIFmTNebounH68vdvxAw08OvK70U0D6J0n4zVNVPCXCQMUrclakiIYXKWZiELpLen6qYgPIzlmyFKyhMpUFC9TGSvouHQ1jK1HoIjFIgGzFpWvsLMkABINMrlhgmxb4hk0zQZ5rHDspXfNC0xlHAnmVtcSq9xHiJVgSs2gczQQuJMZzGUcCuA7OpHjy13r7PLPghgQREwAZ5aHpp/HRDp93Q0MfR4Rj4gWkp8Y5g4RDicCrknDT/YOiytkkHDBdI/2Ai384YPTKmNxp6hFhswlprH7HxJ5+B4LbOmsQE7kMGJYpcZfSj9np1q65pD/wZ5J+a4F/84Mts8s+8W94GOz/TJQG6KyE+dIADanrTQNg7i4NEAJJWJPFyzZ5lpvr1zL/iMtk/VNK4Skw7TDnIc+ev+rtPPmR5vfFS1ZlxU6MlfUjxZJYtk7y343Pn7LNRt5LnOTZoxzI04fj4xd6eH/cPG9WY8M7x0G3Zg6FVftY66J5gqrAdRb4tlHsWGWBsarBUMQ8KGsunrVqzxP4yt9xMlgPNfDXgqBsi+7HDwnGQRrme/Udn1F2htbRJO++Q/a2kwq4rlKiUWe1mXtvpmH63PuoN1y7dnn0O6+cdI21tfdgTUeVBTdHtjswXos9O4Sz9nptCEQlK1UtlZ0tJzNrNhePWRG0uXh6PJp2j7MYEffIlhP89Iaj0mBb7JjFNpCm9x+Ww247+acCS4yaK4ssgu3t/WuJLpXued0sEd85/9WY0OBlUxDhMP5TL+zQpeqEu9+uSNpo5I5OBHhlsrF0KTuWdb/S0i0ds2JusV8pgnTXdXa/mZQsanjWTSj8+ec3t1LjG2tqH/nY4NETGEaIqw3GUQzEPsPodFAAwPrtm5HXIBalhUMaRyyfhXK0VBLj8vdILqJjuqGBxV5tDrmhobRQAops9F/Kdm2ndAxbblMfc0Afd1i4zCHUd3MNv7NwdrqgfYmzs/nYyBDMV9fiUNOw+vlEoSpk93CM2AUB2RRLwixt2FQYUMtj51df9wGE9bjhIrCmuJL5dI3YPfxXGrWUHv4/ErL6Dw==&lt;/diagram&gt;&lt;/mxfile&gt;">
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="721px" height="771px" viewBox="-0.5 -0.5 721 771" content="&lt;mxfile&gt;&lt;diagram id=&quot;X03snOlxasRXa0Irbqj0&quot; name=&quot;Page-1&quot;&gt;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&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs/>
<g>
<path d="M 365 320 L 365 383.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
@@ -99,6 +99,8 @@
<path d="M 313.18 390 L 287.99 375.42" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 305 444.55 L 275.66 456.55" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 347.99 390 L 326 351.2" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 305 404.4 L 238.84 387.21" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 233.76 385.89 L 241.42 384.26 L 238.84 387.21 L 239.65 391.04 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="305" y="390" width="120" height="60" rx="9" ry="9" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
@@ -117,8 +119,9 @@
</switch>
</g>
<path d="M 425 677.27 L 475 700" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 365 680 L 365 730" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 308 680 L 270 700" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 363.33 680 L 360.35 733.64" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 360.06 738.88 L 356.96 731.7 L 360.35 733.64 L 363.94 732.09 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="305" y="620" width="120" height="60" rx="9" ry="9" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
@@ -513,30 +516,13 @@
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 715px; margin-left: 246px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
分类名称
路径
</div>
</div>
</div>
</foreignObject>
<text x="270" y="719" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
分类名称
</text>
</switch>
</g>
<ellipse cx="365" cy="745" rx="25" ry="15" fill="#ffe6cc" stroke="#d79b00" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 745px; margin-left: 341px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
父分类
</div>
</div>
</div>
</foreignObject>
<text x="365" y="749" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
父分类
路径
</text>
</switch>
</g>
@@ -547,13 +533,13 @@
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 715px; margin-left: 451px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
分类描述
描述
</div>
</div>
</div>
</foreignObject>
<text x="475" y="719" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
分类描述
描述
</text>
</switch>
</g>
@@ -728,6 +714,40 @@
</text>
</switch>
</g>
<ellipse cx="215" cy="375" rx="25" ry="15" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 375px; margin-left: 191px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
哈希
</div>
</div>
</div>
</foreignObject>
<text x="215" y="379" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
哈希
</text>
</switch>
</g>
<ellipse cx="360" cy="755" rx="25" ry="15" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 755px; margin-left: 336px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
数量
</div>
</div>
</div>
</foreignObject>
<text x="360" y="759" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
数量
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,4 +1,4 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="771px" height="577px" viewBox="-0.5 -0.5 771 577" content="&lt;mxfile&gt;&lt;diagram id=&quot;URNENjsy1ZhaHGpxpS0s&quot; name=&quot;Page-1&quot;&gt;7Zxdb9s2FIZ/jYHtZpD17UvH6boBLVAsKNZeDYzEyEQoUaDo2O6vHymTkp2jdEolsxlAIAjMI1KmzkNKL18RXgSb8vCeo3r7keWYLnwvPyyC24XvR1Es/6vA8RRIV6tToOAkP4WWfeCOfMM66OnojuS4uagoGKOC1JfBjFUVzsRFDHHO9pfVHhi9/NYaFRgE7jJEYfRvkoutvorI6+N/YFJszTcvPX2kRKayDjRblLP9WSh4twg2nDFx+lQeNpiq3Jm8nNr9/sLRrmMcV2JUg+TU4gnRnb64XYO57pw4mitu9qSkqJKlmwdWiTt9xJPlbEto/gEd2U59YyNQ9mhKN1vGyTdZH1F5aCkD8jAXGqgfq7MRSjeMMi4DFWu/oG90p06mv4bjRjb7ZK5s+Sz0ER0uKn5AjTAdZJSiuiH3bZdVwxLxglQ3TAhW6kowcTqXT5gLfDgL6US+x6zEgh9lFXM01FD1qI4CPar3/Rjp6mzPxocf6yDS47Lozt2jkx80vRdIpoCkOrS+J/JSBQAqr0m0PDh7xM8ADDBBlBSVLFL8oJqppBA5H9Y6LFitTlajjFTFh7bObdhH/tLXqkJMtn2g7ZjfkjzHlULGBBLovhtSNZNdbnMR3cg/mZ2N91u0iGTHN7K87MvyT1XnYsMqeS2ItPSwhL/HagAMcU1Ggz0aPmM5TsfYDbvLCVmhUpbWT4hnW8R/CfxfHdEJRCPfHtHlChCtUdPsGVdPnQ5pHDqkU5AmqcVJ6gGkFcke3SSdlejSCy0iDQFSXCJC3Qydj2fgWeQZAZ6cqcysd1Ujs41ztWgg1dGJo4lUI5vqKIbP0i0T7GyW+lHspukkoKlFceTD9efnz3/envFc+qnjOWn14tlURgOr0NppovlgBjY1EVy5IJVsmRznD73eHwoirUnMItQbqYeCIJ0OM4BrFucPDXHtRv2b9IfSYcP2H8dyHpY2naEYrjkzJHDB+NHxnImnTVsogN6tIIJip2bn42nVFAp8OEGl8Gi7v6asKlpyDucEnDY9oSAAOHPcZJzUgrDKeQgzYrVpCgXwMUplziRP9/CciNGmFRRAx/aJ4L3DOH19YtMBCuAChfEcc8dxOkeb5k8AnbxGZkfdVt1rkzk8A4vCJ4TeT93uRHOKZzaeqUXFE8F7bKZy5oDOBzRYWtQ+4YBzgArAz7ns3+GmjybJs12YSTSOY7cUnQQSOgbOyRviGo4H+xNc9hA6BW5z16w0bfrs4YDPznatiecm5TSMNu31EBoEzr67ClWrJru5Ewy8BXMCaIZtBn5sUwBF8MnpBNAQ127Yv0kBFMFHphNAs9K0KYAi+OQ0nXcTczpKmyIogjugnQi6ClW7Igi67RkrSzwwM50G+m8NFK0uTaA4Hem2dy+uJ8GE+2adBhqclOlosD9BAyXQZdfbn89357ktXVOZWt1yCd+E9Tu63G6uiSRtCqEYvjJxmnY+lFbVTwwtICdqr4PV5n7LZAVY4bzARrEajSlKI0nxgYgvOnHq81f1WWbnVLo9nB26PZpCJXv1xZxAFc5aqWLfrC2Zdi+qzIbteKa7m2g3S0rlAutqJoPqUr6bco4pEuQJX5x9Uj7hLc8IEve2eJ6FQrga+Qjr7o+TgML7nlsoDHFNXv0Kw+pCAXreAwsFh3MCTptrhAR63/L26lDOhNLmIsF813gFclU1kdhQE7rpJ9YO1O5B510+6CLzY4XmFKdO6VbPMtx1Y1TSjdf2atlnPo+RfeMSHsKEmx1aMyd8zTk6nlXQk25iJqHg+8HUvhlFnQ7MAfPThf8LJD98R/GukM14YICbN/BXX5/IYv9DpadU9r/2Grz7Fw==&lt;/diagram&gt;&lt;/mxfile&gt;">
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="591px" height="603px" viewBox="-0.5 -0.5 591 603" content="&lt;mxfile&gt;&lt;diagram id=&quot;URNENjsy1ZhaHGpxpS0s&quot; name=&quot;Page-1&quot;&gt;7Zxhj6MoGMc/TZO7NxsVtfZlp93bu2Q22dxkc7uvNowySgbFIJ22++kPLFRbnI0THWYuR9LMlEdQeH6Afx5IF2BTHj4xWBefaYbIIvCywwJsF0HggygW/6TleLIkq9XJkDOcqUyd4Q7/RMroKesOZ6i5yMgpJRzXl8aUVhVK+YUNMkb3l9keKLl8ag1zZBjuUkhM6z8444VqReR19j8Rzgv9ZN9TV0qoMytDU8CM7nsm8HEBNoxSfvpWHjaISOdpv5zK/fHM1XPFGKr4qALLU4knSHaqcbsGMVU5ftQtbva4JLASqZsHWvE7dcUT6bTAJLuFR7qTT2w4TB916qagDP8U+SERl3xhEJcZV0CDWN4NE7KhhDJhqGj7gK7QnbyZegxDjSj2RbfMvzJ9hoeLjLew4bqClBBYN/i+rbIsWEKW4+qGck5Llcl0nPLlE2IcHXom5chPiJaIs6PIoq+GCqrq1RFQvXrf9ZFznqLXP4JYGaHql/n53h068UXRe4ZkYpCUl9b3WDSVG0BFm3jLg9FHdAVggAkkOK9EkqAHWUw6BYvxsFZmTmt5sxqmuMpv2zzbsLP8rdoqTVSUfSBtny9wlqFKIqMccnh/7lI1FVVufRHdiI/wzsb7EC0iUfGNSPtdWnxkdsY3tBJtgbilhwT8PZIdYIjrcjTYo+YzluN0jOdudzkgK1iK1PoJsrSA7DcQ/O6ITiAaBfaI+iuDaA2bZk+ZfOuckcahQzoF6TKxOEg9A2mF00c3SGcl6nuhRaShgRSVEBM3QufjCTyLPCODJ6PSM+td1Qhvo0wuGnB1dOJoItXIpjqKzXdpQTntjdIgit0wnQQ0sSiOAnP9+fXrX9seTz9IHM9JqxfPpjIaWIXWThPNBxPY1ETmygVKZwvnuPjQy+NDIFKaRC9CvZF6CMThdJjAXLO4+NAQ13Ovf5fxoWQ4YPvDsZyHpc3IEDBjfRxzgpz6mY+nzbDQyowhpJCjnIrbOKQ9pAa+IcrvIy4EApOp0B5tXdeEVnlL7v+Nc9oItRoWAsDAmaEmZbjmmFYujDAjVptxIWDOvET4TPB0emgiRpvRIGAGbZ8w2juM05coNoNAwFyjUJYh5jhO52gz/gPMYF4jvCOnVbdzMkfYwKLwCc3wT90eRnOKZzaeiUXFE5lzbCp95oDOBxT4FrXPyhygBWyKHyrtqM5GFViUQuFAiA/mBj+3ffILbupqpEeIjtT6I1+f5wDDJJBmHMiF3Ie4huPBvsH2SWjGf9ypvVlp2txACQcC7nTXhmbdoJyG0eq+ycigrAM6AajVXRM9CQzthDntM/3oSBBHFrVPZI5Pp32GuJ67/bvUPpH5trzSPm5tORGnTfGzMpeWTvzMg9Gm+InMI+1uR/pVqNpVQObeSUrLEg2MTCeAXh78iZOxwZ9oDpjmQWgngAYHZTIa7BsIoKW5Z6LOs/ePz7oDelOZ2lRBsblt0p3Pc2fzJpK0KYRiU8/qyruJdjpKq+onNuM/TtS+Dlabp2eXK4MVynKkFavWmLzUkhQdMP+mHCe/f5ffhXdOqe2hd2l71IlK1OqbvoFM9ErJZFesTelyz6pMIYtzpEzaW7Lav3QvQwRy/IQu7jTkO1X0C20nqGc3K2NweYuG7liKVKkrAudqjIOSvBTK3A4+teVSY72J070rp0dXwc9TpeZwemK+rP6LQ6NPLhkgp39UamZya8bgsZdBzakTkbx0HIzyip5K+17RgdTX7s9h4H9IgpVY7Ko/y8vurUf+5DlFJLufnDtl7364D3z8Fw==&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs>
<clipPath id="mx-clip-4-305-132-26-0">
<rect x="4" y="305" width="132" height="26"/>
@@ -63,17 +63,20 @@
<clipPath id="mx-clip-219-552-132-26-0">
<rect x="219" y="552" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-634-341-132-26-0">
<rect x="634" y="341" width="132" height="26"/>
<clipPath id="mx-clip-219-578-132-26-0">
<rect x="219" y="578" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-634-367-132-26-0">
<rect x="634" y="367" width="132" height="26"/>
<clipPath id="mx-clip-454-276-132-26-0">
<rect x="454" y="276" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-634-393-132-26-0">
<rect x="634" y="393" width="132" height="26"/>
<clipPath id="mx-clip-454-302-132-26-0">
<rect x="454" y="302" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-634-419-132-26-0">
<rect x="634" y="419" width="132" height="26"/>
<clipPath id="mx-clip-454-328-132-26-0">
<rect x="454" y="328" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-454-354-132-26-0">
<rect x="454" y="354" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-219-31-132-26-0">
<rect x="219" y="31" width="132" height="26"/>
@@ -102,15 +105,6 @@
<clipPath id="mx-clip-454-550-132-26-0">
<rect x="454" y="550" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-454-264-132-26-0">
<rect x="454" y="264" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-454-290-132-26-0">
<rect x="454" y="290" width="132" height="26"/>
</clipPath>
<clipPath id="mx-clip-454-316-132-26-0">
<rect x="454" y="316" width="132" height="26"/>
</clipPath>
</defs>
<g>
<path d="M 0 300 L 0 274 L 140 274 L 140 300" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
@@ -167,7 +161,7 @@
</text>
</g>
<path d="M 215 261 L 215 235 L 355 235 L 355 261" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 215 261 L 215 573 L 355 573 L 355 261" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 215 261 L 215 599 L 355 599 L 355 261" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 215 261 L 355 261" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" text-anchor="middle" font-size="12px">
<text x="284.5" y="252.5">
@@ -186,12 +180,12 @@
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-219-318-132-26-0)" font-size="12px">
<text x="220.5" y="330.5">
category_id:bigint
title:varchar(128)
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-219-344-132-26-0)" font-size="12px">
<text x="220.5" y="356.5">
title:varchar(128)
category:varchar(128)
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-219-370-132-26-0)" font-size="12px">
@@ -234,32 +228,37 @@
cover:varchar(256)
</text>
</g>
<path d="M 630 336 L 630 310 L 770 310 L 770 336" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 630 336 L 630 440 L 770 440 L 770 336" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 630 336 L 770 336" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-219-578-132-26-0)" font-size="12px">
<text x="220.5" y="590.5">
hash_value:varchar(256)
</text>
</g>
<path d="M 450 271 L 450 245 L 590 245 L 590 271" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 450 271 L 450 375 L 590 375 L 590 271" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 450 271 L 590 271" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" text-anchor="middle" font-size="12px">
<text x="699.5" y="327.5">
<text x="519.5" y="262.5">
tag
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-634-341-132-26-0)" font-size="12px">
<text x="635.5" y="353.5">
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-454-276-132-26-0)" font-size="12px">
<text x="455.5" y="288.5">
id:bigint
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-634-367-132-26-0)" font-size="12px">
<text x="635.5" y="379.5">
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-454-302-132-26-0)" font-size="12px">
<text x="455.5" y="314.5">
name:varchar(32)
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-634-393-132-26-0)" font-size="12px">
<text x="635.5" y="405.5">
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-454-328-132-26-0)" font-size="12px">
<text x="455.5" y="340.5">
count:int
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-634-419-132-26-0)" font-size="12px">
<text x="635.5" y="431.5">
description:varchar(256)
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-454-354-132-26-0)" font-size="12px">
<text x="455.5" y="366.5">
description:varchar
</text>
</g>
<path d="M 215 26 L 215 0 L 355 0 L 355 26" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
@@ -277,12 +276,12 @@
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-219-57-132-26-0)" font-size="12px">
<text x="220.5" y="69.5">
name:varchar(32)
name:varchar(256)
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-219-83-132-26-0)" font-size="12px">
<text x="220.5" y="95.5">
parent:bigint
count:int
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-219-109-132-26-0)" font-size="12px">
@@ -325,36 +324,11 @@
</g>
<path d="M 450 298 L 361.17 275.56" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 356.08 274.27 L 363.73 272.6 L 361.17 275.56 L 362.01 279.38 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 450 259 L 450 233 L 590 233 L 590 259" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 450 259 L 450 337 L 590 337 L 590 259" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 450 259 L 590 259" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" text-anchor="middle" font-size="12px">
<text x="519.5" y="250.5">
article_tag
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-454-264-132-26-0)" font-size="12px">
<text x="455.5" y="276.5">
id:bigint
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-454-290-132-26-0)" font-size="12px">
<text x="455.5" y="302.5">
article_id:bigint
</text>
</g>
<g fill="rgb(0, 0, 0)" font-family="Helvetica" pointer-events="none" clip-path="url(#mx-clip-454-316-132-26-0)" font-size="12px">
<text x="455.5" y="328.5">
tag_id:bigint
</text>
</g>
<path d="M 509.59 467 L 358.98 278.97" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 355.7 274.87 L 362.81 278.15 L 358.98 278.97 L 357.34 282.52 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 590 324 L 636.59 334.59" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 641.71 335.75 L 634.11 337.61 L 636.59 334.59 L 635.66 330.79 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 215 300 L 146.27 311.91" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 141.1 312.81 L 147.4 308.16 L 146.27 311.91 L 148.6 315.06 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 281.83 313 L 216.51 45.19" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 215.26 40.09 L 220.32 46.06 L 216.51 45.19 L 213.52 47.72 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 285 521 L 285 84.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 285 79.12 L 288.5 86.12 L 285 84.37 L 281.5 86.12 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1 +1,28 @@
在 Kubernetes 1.7 及之后的版本中,可以为 StatefulSet 设定 .spec.updateStrategy 字段,以便您可以在改变 StatefulSet 中 Pod 的某些字段时container/labels/resource request/resource limit/annotation等禁用滚动更新。
On Delete
OnDelete 策略实现了 StatefulSet 的遗留版本kuberentes 1.6及以前的版本)的行为。如果 StatefulSet 的 .spec.updateStrategy.type 字段被设置为 OnDelete当您修改 .spec.template 的内容时StatefulSet Controller 将不会自动更新其 Pod。您必须手工删除 Pod此时 StatefulSet Controller 在重新创建 Pod 时,使用修改过的 .spec.template 的内容创建新 Pod。
Rolling Updates
.spec.updateStrategy.type 字段的默认值是 RollingUpdate该策略为 StatefulSet 实现了 Pod 的自动滚动更新。在用户更新 StatefulSet 的 .spec.tempalte 字段时StatefulSet Controller 将自动地删除并重建 StatefulSet 中的每一个 Pod。处理顺序如下
从序号最大的 Pod 开始,逐个删除和更新每一个 Pod直到序号最小的 Pod 被更新
当正在更新的 Pod 达到了 Running 和 Ready 的状态之后,才继续更新其前序 Pod
Partitions
通过指定 .spec.updateStrategy.rollingUpdate.partition 字段可以分片partitioned执行RollingUpdate 更新策略。当更新 StatefulSet 的 .spec.template 时:
序号大于或等于 .spec.updateStrategy.rollingUpdate.partition 的 Pod 将被删除重建
序号小于 .spec.updateStrategy.rollingUpdate.partition 的 Pod 将不会更新,及时手工删除该 Podkubernetes 也会使用前一个版本的 .spec.template 重建该 Pod
如果 .spec.updateStrategy.rollingUpdate.partition 大于 .spec.replicas更新 .spec.tempalte 将不会影响到任何 Pod
TIP
大部分情况下,您不需要使用 .spec.updateStrategy.rollingUpdate.partition除非您碰到如下场景
执行预发布
执行金丝雀更新
执行按阶段的更新

View File

@@ -15,6 +15,7 @@
> * javaSPI。服务提供者接口
> * 在微服务中就是注册中心的发布订阅过程。发布者订阅者、提供者消费者。
> * 在消息中间件中就是发布订阅模式。
> https://juejin.cn/post/6993999863159455752
**意图**

View File

@@ -0,0 +1,582 @@
# 事件驱动和订阅发布模式
## 1 事件驱动/订阅发布模式
> 参考文献https://blog.csdn.net/weixin_46058921/article/details/126978976
### 简介
事件驱动一个常见的形式就是 发布-订阅 模式,在跨进程的通信间,我们常常使用 消息队列 来实现消息的发布订阅。目前主流的框架中,均采用消息的 发布-订阅 模式来进行大型分布式项目的解耦。使得数据生产方和发送方分离,同时 MQ 还能起到削峰的作用。同一进程内很多时候也需要这种事件驱动机制来进行解耦
### 原理
事件机制主要由三个部分组成:事件源、事件对象、监听器
* 事件源(订阅发布模式中的发布者):事件发生的起源
* 事件对象(观察者模式中的主题):事件实体,事件对象会持有一个事件源
* 监听器(观察者模式中的观察者、订阅发布模式中的订阅者):监听事件对象,对事件对象进行处理
* 事件分发器(复杂事件模型中负责将将事件源发出的时间触发订阅者的行为,决定了是同步行为还是异步行为)
## 2 Java事件驱动
Java 提供了关于事件相关的两个接口:
* EventObject事件对象自定义事件需要继承该类
* EventListener事件监听器接口
由于事件源 Source 不需要实现任何接口,所以 Java 中没有给出相应的定义
一个利用 Java 原生实现事件的例子:
* 事件。观察者模式中的主题。
```Java
import java.util.EventObject;
/**
* @Author: chenyang
* @DateTime: 2022/9/21 10:08
* @Description: 事件对象
*/
public class JavaEvent extends EventObject {
private String msg;
/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @throws IllegalArgumentException if source is null.
*/
public JavaEvent(Object source, String msg) {
super(source);
this.msg = msg;
}
public String getMsg() {
return msg;
}
}
```
* 事件监听器。观察者模式中的观察者、订阅发布模式中的订阅者
```
import com.yang.common.event.JavaEvent;
import java.util.EventListener;
/**
* @Author: chenyang
* @DateTime: 2022/9/21 10:09
* @Description: 事件监听者,按照 Java 规范应实现 EventListener 接口
*/
public class JavaListener implements EventListener {
public void handlerEvent(JavaEvent event){
System.out.println("Java Event msg : " + event.getMsg());
}
}
```
* 事件源发布事件
```java
import com.yang.common.event.JavaEvent;
import com.yang.common.listener.JavaListener;
import java.util.EventListener;
import java.util.HashSet;
/**
* @Author: chenyang
* @DateTime: 2022/9/21 10:12
* @Description: 事件源
*/
public class JavaSource {
private static HashSet<EventListener> set = new HashSet<>();
public void addListener(EventListener listener){
set.add(listener);
}
public void publishEvent(JavaEvent event){
for (EventListener listener : set) {
((JavaListener)listener).handlerEvent(event);
}
}
}
```
* 组装并触发事件
```java
public class Main {
public static void main(String[] args) {
JavaSource source = new JavaSource();
JavaListener listener = new JavaListener();
source.addListener(listener);
source.publishEvent(new JavaEvent(source, "SAY MY NAME !!!"));
}
}
```
## 3 Spring事件驱动
Spring 提供了事件相关的接口和类,在 Spring 中可以通过实现接口来实现事件的 发布-订阅。Spring 的事件机制是以 Java 的事件机制为基础按需进行了扩展。
Spring 中与事件相关的定义如下:
* ApplicationEvent继承 ObjectEvent 类,事件源应该继承该类。
* ApplicationListener事件监听者该类接受一个泛型供 ApplicationEventPublisher 在发布事件时选择 EventListener。
* ApplicationEventPublisher封装发布事件的方法通知所有在 Spring 中注册的该事件的监听者进行处理。
* ApplicationEventPublisherAwareSpring 提供的 Aware 接口之一,实现该接口的 Bean 可以获取 ApplicationEventPublisher 并进行发布事件。
### 通过继承ApplicationEventPublisherAware发布事件
* 实现事件对象Event。观察者模式中的主题
```java
import org.springframework.context.ApplicationEvent;
/**
* @Author: chenyang
* @DateTime: 2022/9/21 11:07
* @Description: 事件对象
*/
public class SpringEventAware extends ApplicationEvent {
private String msg;
public SpringEventAware(Object source, String msg) {
super(source);
this.msg = msg;
}
public SpringEventAware(Object source) {
super(source);
}
public String getMsg() {
return msg;
}
}
```
* 创建监听器。观察者模式中的观察者,订阅发布模式中的订阅者。
```java
import com.yang.common.event.SpringEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @Author: chenyang
* @DateTime: 2022/9/21 11:08
* @Description: 事件监听者事件监听者实现ApplicationListener<E extends ApplicationEvent>, 交由 Spring 进行管理,无需自己进行监听器的注册与通知过程
*/
@Component
public class SpringListenerAware implements ApplicationListener<SpringEventAware> {
@Override
public void onApplicationEvent(SpringEventAware event) {
System.out.println("publish event, msg is : " + event.getMsg());
}
}
```
* 事件源。观察者模式中的发布者。
```java
import com.yang.common.event.SpringEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
/**
* @Author: chenyang
* @DateTime: 2022/9/21 11:09
* @Description: 事件源
*/
@Component
public class SpringPublishAware implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
public void publishEvent(String msg){
applicationEventPublisher.publishEvent(new SpringEventAware(this, msg));
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}
@Autowired
private SpringPublishAware springPublishAware;
@Test
void contextLoads2() {
springPublishAware.publishEvent("通过 Spring 实现发布订阅");
}
```
### 通过注入ApplicationEventPublisher发布事件
* 事件。观察者模式中的主题
```java
@Data
public class Task {
private String name;
private String address;
}
public class SpringEvent extends ApplicationEvent {
private Task task;
public SpringEvent(Task task) {
super(task);
this.task = task;
}
public Task getTask() {
return task;
}
}
```
* 事件监听器。观察者、订阅者。
```java
@Component
public class SpringListener implements ApplicationListener<SpringEvent> {
@Override
public void onApplicationEvent(SpringEvent event) {
Task task = event.getTask();
System.err.println("事件接受任务");
System.err.println(task);
System.err.println("任务完成");
}
}
```
* 事件源。通过注入对象发布事件。
```java
@Autowired
private ApplicationEventPublisher publisher;
@Test
void contextLoads3() {
Task task = new Task();
task.setName("admin");
task.setAddress("unknown area");
SpringEvent event = new SpringEvent(task);
System.out.println("开始发布任务");
publisher.publishEvent(event);
System.out.println("发布任务完成");
}
```
以上代码中,可以看到。在 Spring 框架使用事件与在 Java 中使用时间机制其实并没有什么不同,均由 事件源、事件对象以及事件监听者组成。与 Java 原生提供的事件机制不同的是Spring 中提供了 ApplicationEvent 类作为基类,开发者可以以此为基础定义自己的自定义事件。
在 Spring 中,继承自 ApplicationEvent 的事件对象的监听者,可以由 Spring 容器进行管理,并在发布时通过 ApplicationEventPublisher 进行发布。这就避免了我们自己实现监听者的注册和通知过程,免去了很多繁杂的过程,使得更专心于业务本身。
## 4 Spring事件分发框架原理
### ApplicationEvent
* ApplicationEvent 继承了 JDK 中的事件对象 EventObject在 Spring 中所有事件对象均应继承自 ApplicationEvent。在Spring基础上其增加了事件发生时的时间戳属性以及序列化ID并提供了通过事件源进行构建的构造方法。 Spring 中的 ApplicationEvent 设置成抽象类,由于一个单独的 ApplicationEvent 是没有任何语义的,所以需要根据不同场景进行扩展,在其之上为事件赋予意义。此类的说明中,作者也很好的说明了这一点。
```java
/**
* Class to be extended by all application events. Abstract as it
* doesn't make sense for generic events to be published directly.
*
* @author Rod Johnson
* @author Juergen Hoeller
*/
public abstract class ApplicationEvent extends EventObject {
/** use serialVersionUID from Spring 1.2 for interoperability */
private static final long serialVersionUID = 7099057708183571937L;
/** System time when the event happened */
private final long timestamp;
/**
* Create a new ApplicationEvent.
* @param source the object on which the event initially occurred (never {@code null})
*/
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
/**
* Return the system time in milliseconds when the event happened.
*/
public final long getTimestamp() {
return this.timestamp;
}
}
```
### EventListener
* JDK 中提供了 EventListener 接口作为事件监听者标记。Spring 在 EventListener 接口的基础上,提供了 ApplicationListener 接口。该接口接收一个 ApplicationEvent 的子类完成事件的监听流程。具体源代码如下ApplicationEvent 继承了 JDK 中的事件对象 EventObject在 Spring 中所有事件对象均应继承自 ApplicationEvent。在Spring基础上其增加了事件发生时的时间戳属性以及序列化ID并提供了通过事件源进行构建的构造方法。 Spring 中的 ApplicationEvent 设置成抽象类,由于一个单独的 ApplicationEvent 是没有任何语义的,所以需要根据不同场景进行扩展,在其之上为事件赋予意义。此类的说明中,作者也很好的说明了这。该接口是一个函数型接口,提供了一个 `onApplicationEvent(E extends Application)` 方法定义,所有自行实现的监听者均需要实现该接口,并在该方法中进行事件的处理。
```java
/**
* Interface to be implemented by application event listeners.
* Based on the standard {@code java.util.EventListener} interface
* for the Observer design pattern.
*
* <p>As of Spring 3.0, an ApplicationListener can generically declare the event type
* that it is interested in. When registered with a Spring ApplicationContext, events
* will be filtered accordingly, with the listener getting invoked for matching event
* objects only.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @param <E> the specific ApplicationEvent subclass to listen to
* @see org.springframework.context.event.ApplicationEventMulticaster
*/
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}
```
### ApplicationEventMulticast
ApplicationEventMulticaster是Spring中事件核心通知中心负责将指定的事件类型发布给订阅者。主要包括以下内容。提供一个发布事件的接口multicastEvent
* 构建并维护事件类型和监听器关系。
* 解析事件类型resolveDefaultEventType
* 根据事件类型获取监听器列表getApplicationListeners。
* 实现了事件执行器。事件执行器可以有多种:同步执行器、异步执行器、消息队列异步执行器
```java
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
protected Collection<ApplicationListener<?>> getApplicationListeners(
ApplicationEvent event, ResolvableType eventType) {
Object source = event.getSource();
Class<?> sourceType = (source != null ? source.getClass() : null);
ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);
// Potential new retriever to populate
CachedListenerRetriever newRetriever = null;
// Quick check for existing entry on ConcurrentHashMap
CachedListenerRetriever existingRetriever = this.retrieverCache.get(cacheKey);
if (existingRetriever == null) {
// Caching a new ListenerRetriever if possible
if (this.beanClassLoader == null ||
(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
newRetriever = new CachedListenerRetriever();
existingRetriever = this.retrieverCache.putIfAbsent(cacheKey, newRetriever);
if (existingRetriever != null) {
newRetriever = null; // no need to populate it in retrieveApplicationListeners
}
}
}
if (existingRetriever != null) {
Collection<ApplicationListener<?>> result = existingRetriever.getApplicationListeners();
if (result != null) {
return result;
}
// If result is null, the existing retriever is not fully populated yet by another thread.
// Proceed like caching wasn't possible for this current local attempt.
}
return retrieveApplicationListeners(eventType, sourceType, newRetriever);
}
```
大致流程: 通过时间类型和事件中的数据源类型构建一个缓存key先去缓存中获取有无此key对应的事件处理器。 如果不存在则构建一个新的 `ListenerRetriever`,然后调用 `retrieveApplicationListeners`方法获得监听的listener
此处以 SimpleApplicationEventMulticaster 中的方法定义为例,作为默认注入的类型,通常我们在默认情况下的事件发布流程均遵循该实现。 从程序中可以看出multicastEvent的主要逻辑可以分为三部分
* 获取事件类型主要用来获得Spring Event的实际类型。resolveDefaultEventType(event))
* getApplicationListeners(event, type)根据事件和事件类型去获得此事件和事件类型的监听器
* 遍历监听者集合,通过 multicaster 内持有的 Executor 进行通知,此处最后调用了 ApplicationListener 中的 onApplicationEvent 方法,这一方法正是我们在自定义 ApplicationListener 时必须要覆写的方法。
### 自动注册事件监听关系
* Spring 中不需要我们手动进行监听器注册。ApplicationListener 对象一旦在 Spring 容器中被注册Spring 会进行监听器的注册,实现事件的监听。在介绍监听者注册流程之前,
* 首先需要介绍介绍一下org.springframework.context.event.ApplicationEventMulticaster其主要定义了管理事件监听者与发布事件到监听者的相关操作若没有定义Spring 容器将默认实例化 SimpleApplicationEventMulticaster 。
* 在 Spring 中,初始化容器时会调用 org.springframework.context.ConfigurableApplicationContext 接口中的 reFresh() 方法进行 Bean的加载该方法会进行事件的监听注册。具体代码如下
* 调用 `initApplicationEventMulticaster()` 方法初始化一个 ApplicationEventMulticaster默认情况下初始化为 SimpleApplicationEventMulticaster。
* 调用 `registerListeners()` 方法进行事件监听者的注册。
```java
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
contextRefresh.end();
}
}
}
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isTraceEnabled()) {
logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isTraceEnabled()) {
logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
"[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
}
}
}
protected void registerListeners() {
// Register statically specified listeners first.
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them!
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
// Publish early application events now that we finally have a multicaster...
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
```
由上文代码可见,注册监听者的过程主要可以分为以下三部分:
* 添加容器中指定的监听器,通常这部分添加的监听器由 Spring 控制;
* 从 BeanFactory 中获取全部实现了 ApplicationListener 接口的 BeanNames并把其推送给 ApplicationEventMulticaster
* 若有需要立即执行的事件,直接执行这些事件的发布
### 事件发布的流程如下
![](image/2023-12-29-16-01-39.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB