diff --git a/README.md b/README.md
index e2c0b91..e98b843 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,12 @@
+👍推荐[2021最新实战项目源码下载](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=100018862&idx=1&sn=858e00b60c6097e3ba061e79be472280&chksm=4ea1856579d60c73224e4d852af6b0188c3ab905069fc28f4b293963fd1ee55d2069fb229848#rd)
+
+👍[《JavaGuide 面试突击版》PDF 版本](#公众号) 。[图解计算机基础 PDF 版](#优质原创PDF资源)
+
+书单已经被移动到[awesome-cs](https://github.com/CodingDocs/awesome-cs) 这个仓库。
+
+
+
+
@@ -10,55 +19,58 @@
+**在线阅读** : https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问)
-**在线阅读:** https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问)
+**开源的目的是为了大家能一起完善,如果你觉得内容有任何需要完善/补充的地方,欢迎提交 issue/pr。**
-## 重要知识点
-
-### 开始
+- Github地址:https://github.com/CodingDocs/springboot-guide
+- 码云地址:https://gitee.com/SnailClimb/springboot-guide(Github无法访问或者访问速度比较慢的小伙伴可以看码云上的对应内容)
-1. **[Spring Boot 介绍](./docs/start/springboot-introduction.md)**
-2. [SpringBoot 开发环境要求](./docs/start/springboot-system-requirements.md)
-3. **[Spring Boot 版 Hello World & Spring Boot 项目结构分析](./docs/start/springboot-hello-world.md)**
+## 重要知识点
### 基础
-1. **[开发 RestFul Web 服务](./docs/basis/sringboot-restful-web-service.md)**
-2. **[RestController VS Controller](./docs/basis/RestControllerVSController.md)**
-3. **[Spring Boot 异常处理](./docs/advanced/springboot-handle-exception.md)**
-4. **[实际项目中我们是这样做异常处理的](./docs/advanced/springboot-handle-exception-plus.md)**
-5. [使用 spring-boot-devtools 进行热部署](./docs/basis/spring-boot-devtools.md)
-6. **[ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md)**
-7. **[JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md)**
-8. [SpringBoot 实现过滤器](./docs/basis/springboot-filter.md)
-9. [SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md)
-10. [整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md)
+1. [Spring Boot 介绍](./docs/start/springboot-introduction.md)
+2. [第一个 Hello World](./docs/start/springboot-hello-world.md)
+3. [第一个 RestFul Web 服务](./docs/basis/sringboot-restful-web-service.md)
+4. [Spring 如何优雅读取配置文件?](./docs/basis/read-config-properties.md)
+5. **异常处理** :[Spring Boot 异常处理的几种方式](./docs/advanced/springboot-handle-exception.md)、[Spring Boot 异常处理在实际项目中的应用](./docs/advanced/springboot-handle-exception-plus.md)
+6. **JPA** : [ Spring Boot JPA 基础:常见操作解析](./docs/basis/springboot-jpa.md) 、 [JPA 中非常重要的连表查询就是这么简单](./docs/basis/springboot-jpa-lianbiao.md)
+7. **拦截器和过滤器** :[SpringBoot 实现过滤器](./docs/basis/springboot-filter.md) 、[SpringBoot 实现拦截器](./docs/basis/springboot-interceptor.md)
+8. **MyBatis** :[整合 SpringBoot+Mybatis](./docs/basis/springboot-mybatis.md) 、[SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置](./docs/basis/springboot-mybatis-mutipledatasource.md) (TODO:早期文章,不建议阅读,待重构~)
+9. [MyBatis-Plus 从入门到上手干事!](./docs/MyBatisPlus.md)
+10. [SpringBoot 2.0+ 集成 Swagger 官方 Starter + knife4j 增强方案](./docs/basis/swagger.md)
### 进阶
-1. **[Bean映射工具之Apache BeanUtils VS Spring BeanUtils](./docs/advanced/Apache-BeanUtils-VS-SpringBean-Utils.md)**
-2. [5种常见Bean映射工具的性能比对](./docs/advanced/Performance-of-Java-Mapping-Frameworks.md)
-3. **[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](./docs/advanced/spring-bean-validation.md)**
+1. Bean映射工具 :[Bean映射工具之Apache BeanUtils VS Spring BeanUtils](./docs/advanced/Apache-BeanUtils-VS-SpringBean-Utils.md) 、[5种常见Bean映射工具的性能比对](./docs/advanced/Performance-of-Java-Mapping-Frameworks.md)
+3. [如何在 Spring/Spring Boot 中优雅地做参数校验?](./docs/spring-bean-validation.md)
+3. [使用 PowerMockRunner 和 Mockito 编写单元测试用例](./docs/PowerMockRunnerAndMockito.md)
4. [5分钟搞懂如何在Spring Boot中Schedule Tasks](./docs/advanced/SpringBoot-ScheduleTasks.md)
-5. **[新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md)**
-6. [Spring Boot 整合 阿里云OSS 存储服务,快来免费搭建一个自己的图床](./docs/advanced/springboot-oss.md)
+5. [新手也能看懂的 Spring Boot 异步编程指南](./docs/advanced/springboot-async.md)
+6. [Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka)
7. [超详细,新手都能看懂 !使用Spring Boot+Dubbo 搭建一个分布式服务](./docs/advanced/springboot-dubbo.md)
8. [从零入门 !Spring Security With JWT(含权限验证)](https://github.com/Snailclimb/spring-security-jwt-guide)
-### 面试题
+### 补充
-- [几道简单的 SpringBoot面试题](./docs/interview/springboot-questions.md)
+1. [`@PostConstruct`和`@PreDestroy` 简单使用以及Java9+中的替代方案](./docs/basis/@PostConstruct与@PreDestroy.md)
+
+## 实战项目
+
+1. [使用 Spring Boot搭建一个在线文件预览系统!支持ppt、doc等多种类型文件预览](./docs/projects/kkFileView-SpringBoot在线文件预览系统.md)
+2. [ SpringBoot 前后端分离后台管理系统分析!分模块开发、RBAC权限控制...](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247495011&idx=1&sn=f574f5d75c3720d8b2a665d1d5234d28&chksm=cea1a2a8f9d62bbe9f13f5a030893fe3da6956c4be41471513e6247f74cba5a8df9941798b6e&token=212861022&lang=zh_CN#rd)
+3. [一个基于Spring Cloud 的面试刷题系统。](./docs/projects/SpringCloud刷题系统.md)
+4. [一个基于 Spring Boot 的在线考试系统](./docs/projects/一个基于SpringBoot的在线考试系统.md)
## 说明
1. 项目 logo 由 [logoly](https://logoly.pro/#/) 生成。
-2. 利用 docsify 生成文档部署在 Github pages: [docsify 官网介绍](https://docsify.js.org/#/)
-
-### 联系我
+2. 利用 docsify 生成文档部署在 Github Pages 和 Gitee Pages: [docsify 官网介绍](https://docsify.js.org/#/)
-添加我的微信备注“Github”,回复关键字 **“加群”** 即可入群。
+### 优质原创PDF资源
-
+
### 公众号
@@ -66,6 +78,6 @@
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
-**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
+**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。

diff --git a/docs/MyBatisPlus.md b/docs/MyBatisPlus.md
new file mode 100644
index 0000000..afd4043
--- /dev/null
+++ b/docs/MyBatisPlus.md
@@ -0,0 +1,975 @@
+`MyBatis` 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射,而实际开发中,我们都会选择使用 `MyBatisPlus`,它是对 `MyBatis` 框架的进一步增强,能够极大地简化我们的持久层代码,下面就一起来看看 `MyBatisPlus` 中的一些奇淫巧技吧。
+
+> 说明:本篇文章需要一定的 `MyBatis` 与 `MyBatisPlus` 基础
+
+MyBatis-Plus 官网地址 : https://baomidou.com/ 。
+
+## CRUD
+
+使用 `MyBatisPlus` 实现业务的增删改查非常地简单,一起来看看吧。
+
+**1.首先新建一个 SpringBoot 工程,然后引入依赖:**
+
+```xml
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ 3.4.2
+
+
+ mysql
+ mysql-connector-java
+ runtime
+
+
+ org.projectlombok
+ lombok
+
+```
+
+**2.配置一下数据源:**
+
+```yaml
+spring:
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ username: root
+ url: jdbc:mysql:///mybatisplus?serverTimezone=UTC
+ password: 123456
+```
+
+**3.创建一下数据表:**
+
+```sql
+CREATE DATABASE `mybatisplus`;
+
+USE `mybatisplus`;
+
+DROP TABLE IF EXISTS `tbl_employee`;
+
+CREATE TABLE `tbl_employee` (
+ `id` bigint(20) NOT NULL,
+ `last_name` varchar(255) DEFAULT NULL,
+ `email` varchar(255) DEFAULT NULL,
+ `gender` char(1) DEFAULT NULL,
+ `age` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=gbk;
+
+insert into `tbl_employee`(`id`,`last_name`,`email`,`gender`,`age`) values (1,'jack','jack@qq.com','1',35),(2,'tom','tom@qq.com','1',30),(3,'jerry','jerry@qq.com','1',40);
+```
+
+**4.创建对应的实体类:**
+
+```java
+@Data
+public class Employee {
+
+ private Long id;
+ private String lastName;
+ private String email;
+ private Integer age;
+}
+```
+
+**4.编写 `Mapper` 接口:**
+
+```java
+public interface EmployeeMapper extends BaseMapper {
+}
+```
+
+我们只需继承 `MyBatisPlus` 提供的 `BaseMapper` 接口即可,现在我们就拥有了对 `Employee` 进行增删改查的 API,比如:
+
+```java
+@SpringBootTest
+@MapperScan("com.wwj.mybatisplusdemo.mapper")
+class MybatisplusDemoApplicationTests {
+
+ @Autowired
+ private EmployeeMapper employeeMapper;
+
+ @Test
+ void contextLoads() {
+ List employees = employeeMapper.selectList(null);
+ employees.forEach(System.out::println);
+ }
+}
+```
+
+运行结果:
+
+```java
+org.springframework.jdbc.BadSqlGrammarException:
+### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Table 'mybatisplus.employee' doesn't exist
+```
+
+程序报错了,原因是不存在 `employee` 表,这是因为我们的实体类名为 `Employee`,`MyBatisPlus` 默认是以类名作为表名进行操作的,可如果类名和表名不相同(实际开发中也确实可能不同),就需要在实体类中使用 `@TableName` 注解来声明表的名称:
+
+```java
+@Data
+@TableName("tbl_employee") // 声明表名称
+public class Employee {
+
+ private Long id;
+ private String lastName;
+ private String email;
+ private Integer age;
+}
+```
+
+重新执行测试代码,结果如下:
+
+```java
+Employee(id=1, lastName=jack, email=jack@qq.com, age=35)
+Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
+Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40)
+```
+
+`BaseMapper` 提供了常用的一些增删改查方法:
+
+
+
+具体细节可以查阅其源码自行体会,注释都是中文的,非常容易理解。
+
+在开发过程中,我们通常会使用 `Service` 层来调用 `Mapper` 层的方法,而 `MyBatisPlus` 也为我们提供了通用的 `Service`:
+
+```java
+public interface EmployeeService extends IService {
+}
+
+@Service
+public class EmployeeServiceImpl extends ServiceImpl implements EmployeeService {
+}
+```
+
+事实上,我们只需让 `EmployeeServiceImpl` 继承 `ServiceImpl` 即可获得 `Service` 层的方法,**那么为什么还需要实现 `EmployeeService` 接口呢?**
+
+这是因为实现 `EmployeeService` 接口能够更方便地对业务进行扩展,一些复杂场景下的数据处理,`MyBatisPlus` 提供的 `Service` 方法可能无法处理,此时我们就需要自己编写代码,这时候只需在 `EmployeeService` 中定义自己的方法,并在 `EmployeeServiceImpl` 中实现即可。
+
+先来测试一下 `MyBatisPlus` 提供的 `Service` 方法:
+
+```java
+@SpringBootTest
+@MapperScan("com.wwj.mybatisplusdemo.mapper")
+class MybatisplusDemoApplicationTests {
+
+ @Autowired
+ private EmployeeService employeeService;
+
+ @Test
+ void contextLoads() {
+ List list = employeeService.list();
+ list.forEach(System.out::println);
+ }
+}
+```
+
+运行结果:
+
+```java
+Employee(id=1, lastName=jack, email=jack@qq.com, age=35)
+Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
+Employee(id=3, lastName=jerry, email=jerry@qq.com, age=40)
+```
+
+接下来模拟一个自定义的场景,我们来编写自定义的操作方法,首先在 `EmployeeMapper` 中进行声明:
+
+```java
+public interface EmployeeMapper extends BaseMapper {
+
+ List selectAllByLastName(@Param("lastName") String lastName);
+}
+```
+
+此时我们需要自己编写配置文件实现该方法,在 `resource` 目录下新建一个 `mapper` 文件夹,然后在该文件夹下创建 `EmployeeMapper.xml` 文件:
+
+```xml
+
+
+
+
+
+ id, last_name, email, gender, age
+
+
+
+
+```
+
+`MyBatisPlus` 默认扫描的是类路径下的 `mapper` 目录,这可以从源码中得到体现:
+
+
+
+所以我们直接将 `Mapper` 配置文件放在该目录下就没有任何问题,可如果不是这个目录,我们就需要进行配置,比如:
+
+```yaml
+mybatis-plus:
+ mapper-locations: classpath:xml/*.xml
+```
+
+编写好 `Mapper` 接口后,我们就需要定义 `Service` 方法了:
+
+```java
+public interface EmployeeService extends IService {
+
+ List listAllByLastName(String lastName);
+}
+
+@Service
+public class EmployeeServiceImpl extends ServiceImpl implements EmployeeService {
+
+ @Override
+ public List listAllByLastName(String lastName) {
+ return baseMapper.selectAllByLastName(lastName);
+ }
+}
+```
+
+在 `EmployeeServiceImpl` 中我们无需将 `EmployeeMapper` 注入进来,而是使用 `BaseMapper`,查看 `ServiceImpl` 的源码:
+
+
+
+可以看到它为我们注入了一个 `BaseMapper` 对象,而它是第一个泛型类型,也就是 `EmployeeMapper` 类型,所以我们可以直接使用这个 `baseMapper` 来调用 `Mapper` 中的方法,此时编写测试代码:
+
+```java
+@SpringBootTest
+@MapperScan("com.wwj.mybatisplusdemo.mapper")
+class MybatisplusDemoApplicationTests {
+
+ @Autowired
+ private EmployeeService employeeService;
+
+ @Test
+ void contextLoads() {
+ List list = employeeService.listAllByLastName("tom");
+ list.forEach(System.out::println);
+ }
+}
+```
+
+运行结果:
+
+```java
+Employee(id=2, lastName=tom, email=tom@qq.com, age=30)
+```
+
+## ID 策略
+
+在创建表的时候我故意没有设置主键的增长策略,现在我们来插入一条数据,看看主键是如何增长的:
+
+```java
+@Test
+void contextLoads() {
+ Employee employee = new Employee();
+ employee.setLastName("lisa");
+ employee.setEmail("lisa@qq.com");
+ employee.setAge(20);
+ employeeService.save(employee);
+}
+```
+
+插入成功后查询一下数据表:
+
+```sql
+mysql> select * from tbl_employee;
++---------------------+-----------+--------------+--------+------+
+| id | last_name | email | gender | age |
++---------------------+-----------+--------------+--------+------+
+| 1 | jack | jack@qq.com | 1 | 35 |
+| 2 | tom | tom@qq.com | 1 | 30 |
+| 3 | jerry | jerry@qq.com | 1 | 40 |
+| 1385934720849584129 | lisa | lisa@qq.com | NULL | 20 |
++---------------------+-----------+--------------+--------+------+
+4 rows in set (0.00 sec)
+```
+
+可以看到 id 是一串相当长的数字,这是什么意思呢?提前剧透一下,这其实是分布式 id,那又何为分布式 id 呢?
+
+我们知道,对于一个大型应用,其访问量是非常巨大的,就比如说一个网站每天都有人进行注册,注册的用户信息就需要存入数据表,随着日子一天天过去,数据表中的用户越来越多,此时数据库的查询速度就会受到影响,所以一般情况下,当数据量足够庞大时,数据都会做分库分表的处理。
+
+然而,一旦分表,问题就产生了,很显然这些分表的数据都是属于同一张表的数据,只是因为数据量过大而分成若干张表,那么这几张表的主键 id 该怎么管理呢?每张表维护自己的 id?那数据将会有很多的 id 重复,这当然是不被允许的,其实,我们可以使用算法来生成一个绝对不会重复的 id,这样问题就迎刃而解了,事实上,分布式 id 的解决方案有很多:
+
+1. UUID
+1. SnowFlake
+1. TinyID
+1. Uidgenerator
+1. Leaf
+6. Tinyid
+7. ......
+
+以 UUID 为例,它生成的是一串由数字和字母组成的字符串,显然并不适合作为数据表的 id,而且 id 保持递增有序会加快表的查询效率,基于此,`MyBatisPlus` 使用的就是 `SnowFlake`(雪花算法)。
+
+`Snowflake` 是 Twitter 开源的分布式 ID 生成算法。`Snowflake` 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义:
+
+- **第 0 位**: 符号位(标识正负),始终为 0,没有用,不用管。
+- **第 1~41 位** :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)
+- **第 42~52 位** :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。
+- **第 53~64 位** :一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。
+
+
+
+这也就是为什么插入数据后新的数据 id 是一长串数字的原因了,我们可以在实体类中使用 `@TableId` 来设置主键的策略:
+
+```java
+@Data
+@TableName("tbl_employee")
+public class Employee {
+
+ @TableId(type = IdType.AUTO) // 设置主键策略
+ private Long id;
+ private String lastName;
+ private String email;
+ private Integer age;
+}
+```
+
+`MyBatisPlus` 提供了几种主键的策略:
+
+其中 `AUTO` 表示数据库自增策略,该策略下需要数据库实现主键的自增(auto_increment),`ASSIGN_ID` 是雪花算法,默认使用的是该策略,`ASSIGN_UUID` 是 UUID 策略,一般不会使用该策略。
+
+这里多说一点, 当实体类的主键名为 id,并且数据表的主键名也为 id 时,此时 `MyBatisPlus` 会自动判定该属性为主键 id,倘若名字不是 id 时,就需要标注 `@TableId` 注解,若是实体类中主键名与数据表的主键名不一致,则可以进行声明:
+
+```java
+@TableId(value = "uid",type = IdType.AUTO) // 设置主键策略
+private Long id;
+```
+
+还可以在配置文件中配置全局的主键策略:
+
+```yaml
+mybatis-plus:
+ global-config:
+ db-config:
+ id-type: auto
+```
+
+这样能够避免在每个实体类中重复设置主键策略。
+
+## 属性自动填充
+
+翻阅《阿里巴巴Java开发手册》,在第 5 章 MySQL 数据库可以看到这样一条规范:
+
+对于一张数据表,它必须具备三个字段:
+
+- `id` : 唯一ID
+- `gmt_create` : 保存的是当前数据创建的时间
+- `gmt_modified` : 保存的是更新时间
+
+我们改造一下数据表:
+
+```sql
+alter table tbl_employee add column gmt_create datetime not null;
+alter table tbl_employee add column gmt_modified datetime not null;
+```
+
+然后改造一下实体类:
+
+```java
+@Data
+@TableName("tbl_employee")
+public class Employee {
+
+ @TableId(type = IdType.AUTO) // 设置主键策略
+ private Long id;
+ private String lastName;
+ private String email;
+ private Integer age;
+ private LocalDateTime gmtCreate;
+ private LocalDateTime gmtModified;
+}
+```
+
+此时我们在插入数据和更新数据的时候就需要手动去维护这两个属性:
+
+```java
+@Test
+void contextLoads() {
+ Employee employee = new Employee();
+ employee.setLastName("lisa");
+ employee.setEmail("lisa@qq.com");
+ employee.setAge(20);
+ // 设置创建时间
+ employee.setGmtCreate(LocalDateTime.now());
+ employee.setGmtModified(LocalDateTime.now());
+ employeeService.save(employee);
+}
+
+@Test
+void contextLoads() {
+ Employee employee = new Employee();
+ employee.setId(1385934720849584130L);
+ employee.setAge(50);
+ // 设置创建时间
+ employee.setGmtModified(LocalDateTime.now());
+ employeeService.updateById(employee);
+}
+```
+
+每次都需要维护这两个属性未免过于麻烦,好在 `MyBatisPlus` 提供了字段自动填充功能来帮助我们进行管理,需要使用到的是 `@TableField` 注解:
+
+```java
+@Data
+@TableName("tbl_employee")
+public class Employee {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String lastName;
+ private String email;
+ private Integer age;
+ @TableField(fill = FieldFill.INSERT) // 插入的时候自动填充
+ private LocalDateTime gmtCreate;
+ @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新的时候自动填充
+ private LocalDateTime gmtModified;
+}
+```
+
+然后编写一个类实现 MetaObjectHandler 接口:
+
+```java
+@Component
+@Slf4j
+public class MyMetaObjectHandler implements MetaObjectHandler {
+
+ /**
+ * 实现插入时的自动填充
+ * @param metaObject
+ */
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ log.info("insert开始属性填充");
+ this.strictInsertFill(metaObject,"gmtCreate", LocalDateTime.class,LocalDateTime.now());
+ this.strictInsertFill(metaObject,"gmtModified", LocalDateTime.class,LocalDateTime.now());
+ }
+
+ /**
+ * 实现更新时的自动填充
+ * @param metaObject
+ */
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ log.info("update开始属性填充");
+ this.strictInsertFill(metaObject,"gmtModified", LocalDateTime.class,LocalDateTime.now());
+ }
+}
+```
+
+该接口中有两个未实现的方法,分别为插入和更新时的填充方法,在方法中调用 `strictInsertFill()` 方法 即可实现属性的填充,它需要四个参数:
+
+1. `metaObject`:元对象,就是方法的入参
+1. `fieldName`:为哪个属性进行自动填充
+1. `fieldType`:属性的类型
+1. `fieldVal`:需要填充的属性值
+
+此时在插入和更新数据之前,这两个方法会先被执行,以实现属性的自动填充,通过日志我们可以进行验证:
+
+```java
+@Test
+void contextLoads() {
+ Employee employee = new Employee();
+ employee.setId(1385934720849584130L);
+ employee.setAge(15);
+ employeeService.updateById(employee);
+}
+```
+
+运行结果:
+
+```java
+INFO 15584 --- [ main] c.w.m.handler.MyMetaObjectHandler : update开始属性填充
+2021-04-24 21:32:19.788 INFO 15584 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
+2021-04-24 21:32:21.244 INFO 15584 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
+```
+
+属性填充其实可以进行一些优化,考虑一些特殊情况,对于一些不存在的属性,就不需要进行属性填充,对于一些设置了值的属性,也不需要进行属性填充,这样可以提高程序的整体运行效率:
+
+```java
+@Component
+@Slf4j
+public class MyMetaObjectHandler implements MetaObjectHandler {
+
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ boolean hasGmtCreate = metaObject.hasSetter("gmtCreate");
+ boolean hasGmtModified = metaObject.hasSetter("gmtModified");
+ if (hasGmtCreate) {
+ Object gmtCreate = this.getFieldValByName("gmtCreate", metaObject);
+ if (gmtCreate == null) {
+ this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime.class, LocalDateTime.now());
+ }
+ }
+ if (hasGmtModified) {
+ Object gmtModified = this.getFieldValByName("gmtModified", metaObject);
+ if (gmtModified == null) {
+ this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now());
+ }
+ }
+ }
+
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ boolean hasGmtModified = metaObject.hasSetter("gmtModified");
+ if (hasGmtModified) {
+ Object gmtModified = this.getFieldValByName("gmtModified", metaObject);
+ if (gmtModified == null) {
+ this.strictInsertFill(metaObject, "gmtModified", LocalDateTime.class, LocalDateTime.now());
+ }
+ }
+ }
+}
+```
+
+## 逻辑删除
+
+逻辑删除对应的是物理删除,分别介绍一下这两个概念:
+
+1. **物理删除** :指的是真正的删除,即:当执行删除操作时,将数据表中的数据进行删除,之后将无法再查询到该数据
+1. **逻辑删除** :并不是真正意义上的删除,只是对于用户不可见了,它仍然存在与数据表中
+
+在这个数据为王的时代,数据就是财富,所以一般并不会有哪个系统在删除某些重要数据时真正删掉了数据,通常都是在数据库中建立一个状态列,让其默认为 0,当为 0 时,用户可见;当执行了删除操作,就将状态列改为 1,此时用户不可见,但数据还是在表中的。
+
+
+
+按照《阿里巴巴Java开发手册》第 5 章 MySQL 数据库相关的建议,我们来为数据表新增一个`is_deleted` 字段:
+
+```sql
+alter table tbl_employee add column is_deleted tinyint not null;
+```
+
+在实体类中也要添加这一属性:
+
+```java
+@Data
+@TableName("tbl_employee")
+public class Employee {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String lastName;
+ private String email;
+ private Integer age;
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime gmtCreate;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime gmtModified;
+ /**
+ * 逻辑删除属性
+ */
+ @TableLogic
+ @TableField("is_deleted")
+ private Boolean deleted;
+}
+```
+
+
+
+还是参照《阿里巴巴Java开发手册》第 5 章 MySQL 数据库相关的建议,对于布尔类型变量,不能加 is 前缀,所以我们的属性被命名为 `deleted`,但此时就无法与数据表的字段进行对应了,所以我们需要使用 `@TableField` 注解来声明一下数据表的字段名,而 `@TableLogin` 注解用于设置逻辑删除属性;此时我们执行删除操作:
+
+```java
+@Test
+void contextLoads() {
+ employeeService.removeById(3);
+}
+```
+
+查询数据表:
+
+```sql
+mysql> select * from tbl_employee;
++---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+
+| id | last_name | email | gender | age | gmt_create | gmt_modified | is_deleted |
++---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+
+| 1 | jack | jack@qq.com | 1 | 35 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 |
+| 2 | tom | tom@qq.com | 1 | 30 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 |
+| 3 | jerry | jerry@qq.com | 1 | 40 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 1 |
+| 1385934720849584129 | lisa | lisa@qq.com | NULL | 20 | 0000-00-00 00:00:00 | 0000-00-00 00:00:00 | 0 |
+| 1385934720849584130 | lisa | lisa@qq.com | NULL | 15 | 2021-04-24 21:14:18 | 2021-04-24 21:32:19 | 0 |
++---------------------+-----------+--------------+--------+------+---------------------+---------------------+------------+
+5 rows in set (0.00 sec)
+```
+
+可以看到数据并没有被删除,只是 `is_deleted` 字段的属性值被更新成了 1,此时我们再来执行查询操作:
+
+```java
+@Test
+void contextLoads() {
+ List list = employeeService.list();
+ list.forEach(System.out::println);
+}
+```
+
+执行结果:
+
+```java
+Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
+Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
+Employee(id=1385934720849584129, lastName=lisa, email=lisa@qq.com, age=20, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=false)
+Employee(id=1385934720849584130, lastName=lisa, email=lisa@qq.com, age=15, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:32:19, deleted=false)
+```
+
+会发现第三条数据并没有被查询出来,它是如何实现的呢?我们可以输出 `MyBatisPlus` 生成的 SQL 来分析一下,在配置文件中进行配置:
+
+```yaml
+mybatis-plus:
+ configuration:
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出SQL日志
+```
+
+运行结果:
+
+```java
+==> Preparing: SELECT id,last_name,email,age,gmt_create,gmt_modified,is_deleted AS deleted FROM tbl_employee WHERE is_deleted=0
+==> Parameters:
+<== Columns: id, last_name, email, age, gmt_create, gmt_modified, deleted
+<== Row: 1, jack, jack@qq.com, 35, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0
+<== Row: 2, tom, tom@qq.com, 30, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0
+<== Row: 1385934720849584129, lisa, lisa@qq.com, 20, 2021-04-24 21:14:18, 2021-04-24 21:14:18, 0
+<== Row: 1385934720849584130, lisa, lisa@qq.com, 15, 2021-04-24 21:14:18, 2021-04-24 21:32:19, 0
+<== Total: 4
+```
+
+原来它在查询时携带了一个条件: `is_deleted=0` ,这也说明了 `MyBatisPlus` 默认 0 为不删除,1 为删除。
+若是你想修改这个规定,比如设置-1 为删除,1 为不删除,也可以进行配置:
+
+```yaml
+mybatis-plus:
+ global-config:
+ db-config:
+ id-type: auto
+ logic-delete-field: deleted # 逻辑删除属性名
+ logic-delete-value: -1 # 删除值
+ logic-not-delete-value: 1 # 不删除值
+```
+
+但建议使用默认的配置,阿里巴巴开发手册也规定 1 表示删除,0 表示未删除。
+
+## 分页插件
+
+对于分页功能,`MyBatisPlus` 提供了分页插件,只需要进行简单的配置即可实现:
+
+```java
+@Configuration
+public class MyBatisConfig {
+
+ /**
+ * 注册分页插件
+ * @return
+ */
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+ return interceptor;
+ }
+}
+```
+
+接下来我们就可以使用分页插件提供的功能了:
+
+```java
+@Test
+void contextLoads() {
+ Page page = new Page<>(1,2);
+ employeeService.page(page, null);
+ List employeeList = page.getRecords();
+ employeeList.forEach(System.out::println);
+ System.out.println("获取总条数:" + page.getTotal());
+ System.out.println("获取当前页码:" + page.getCurrent());
+ System.out.println("获取总页码:" + page.getPages());
+ System.out.println("获取每页显示的数据条数:" + page.getSize());
+ System.out.println("是否有上一页:" + page.hasPrevious());
+ System.out.println("是否有下一页:" + page.hasNext());
+}
+```
+
+其中的 `Page` 对象用于指定分页查询的规则,这里表示按每页两条数据进行分页,并查询第一页的内容,运行结果:
+
+```java
+Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)
+Employee(id=2, lastName=tom, email=tom@qq.com, age=30, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)
+获取总条数:4
+获取当前页码:1
+获取总页码:2
+获取每页显示的数据条数:2
+是否有上一页:false
+是否有下一页:true
+```
+
+倘若在分页过程中需要限定一些条件,我们就需要构建 QueryWrapper 来实现:
+
+```java
+@Test
+void contextLoads() {
+ Page page = new Page<>(1, 2);
+ employeeService.page(page, new QueryWrapper()
+ .between("age", 20, 50)
+ .eq("gender", 1));
+ List employeeList = page.getRecords();
+ employeeList.forEach(System.out::println);
+}
+```
+
+此时分页的数据就应该是年龄在 20~50 岁之间,且 gender 值为 1 的员工信息,然后再对这些数据进行分页。
+
+## 乐观锁
+
+当程序中出现并发访问时,就需要保证数据的一致性。以商品系统为例,现在有两个管理员均想对同一件售价为 100 元的商品进行修改,A 管理员正准备将商品售价改为 150 元,但此时出现了网络问题,导致 A 管理员的操作陷入了等待状态;此时 B 管理员也进行修改,将商品售价改为了 200 元,修改完成后 B 管理员退出了系统,此时 A 管理员的操作也生效了,这样便使得 A 管理员的操作直接覆盖了 B 管理员的操作,B 管理员后续再进行查询时会发现商品售价变为了 150 元,这样的情况是绝对不允许发生的。
+
+要想解决这一问题,可以给数据表加锁,常见的方式有两种:
+
+1. 乐观锁
+1. 悲观锁
+
+悲观锁认为并发情况一定会发生,所以在某条数据被修改时,为了避免其它人修改,会直接对数据表进行加锁,它依靠的是数据库本身提供的锁机制(表锁、行锁、读锁、写锁)。
+
+而乐观锁则相反,它认为数据产生冲突的情况一般不会发生,所以在修改数据的时候并不会对数据表进行加锁的操作,而是在提交数据时进行校验,判断提交上来的数据是否会发生冲突,如果发生冲突,则提示用户重新进行操作,一般的实现方式为 `设置版本号字段` 。
+
+就以商品售价为例,在该表中设置一个版本号字段,让其初始为 1,此时 A 管理员和 B 管理员同时需要修改售价,它们会先读取到数据表中的内容,此时两个管理员读取到的版本号都为 1,此时 B 管理员的操作先生效了,它就会将当前数据表中对应数据的版本号与最开始读取到的版本号作一个比对,发现没有变化,于是修改就生效了,此时版本号加 1。
+
+而 A 管理员马上也提交了修改操作,但是此时的版本号为 2,与最开始读取到的版本号并不对应,这就说明数据发生了冲突,此时应该提示 A 管理员操作失败,并让 A 管理员重新查询一次数据。
+
+
+
+乐观锁的优势在于采取了更加宽松的加锁机制,能够提高程序的吞吐量,适用于读操作多的场景。
+
+那么接下来我们就来模拟这一过程。
+
+**1.创建一张新的数据表:**
+
+```sql
+create table shop(
+ id bigint(20) not null auto_increment,
+ name varchar(30) not null,
+ price int(11) default 0,
+ version int(11) default 1,
+ primary key(id)
+);
+
+insert into shop(id,name,price) values(1,'笔记本电脑',8000);
+```
+
+**2.创建实体类:**
+
+```java
+@Data
+public class Shop {
+
+ private Long id;
+ private String name;
+ private Integer price;
+ private Integer version;
+}
+```
+
+**3.创建对应的 `Mapper` 接口:**
+
+```java
+public interface ShopMapper extends BaseMapper {
+}
+```
+
+**4.编写测试代码:**
+
+```java
+@SpringBootTest
+@MapperScan("com.wwj.mybatisplusdemo.mapper")
+class MybatisplusDemoApplicationTests {
+
+ @Autowired
+ private ShopMapper shopMapper;
+
+ /**
+ * 模拟并发场景
+ */
+ @Test
+ void contextLoads() {
+ // A、B管理员读取数据
+ Shop A = shopMapper.selectById(1L);
+ Shop B = shopMapper.selectById(1L);
+ // B管理员先修改
+ B.setPrice(9000);
+ int result = shopMapper.updateById(B);
+ if (result == 1) {
+ System.out.println("B管理员修改成功!");
+ } else {
+ System.out.println("B管理员修改失败!");
+ }
+ // A管理员后修改
+ A.setPrice(8500);
+ int result2 = shopMapper.updateById(A);
+ if (result2 == 1) {
+ System.out.println("A管理员修改成功!");
+ } else {
+ System.out.println("A管理员修改失败!");
+ }
+ // 最后查询
+ System.out.println(shopMapper.selectById(1L));
+ }
+}
+```
+
+执行结果:
+
+```java
+B管理员修改成功!
+A管理员修改成功!
+Shop(id=1, name=笔记本电脑, price=8500, version=1)
+```
+
+**问题出现了,B 管理员的操作被 A 管理员覆盖,那么该如何解决这一问题呢?**
+
+其实 `MyBatisPlus` 已经提供了乐观锁机制,只需要在实体类中使用 `@Version` 声明版本号属性:
+
+```java
+@Data
+public class Shop {
+
+ private Long id;
+ private String name;
+ private Integer price;
+ @Version // 声明版本号属性
+ private Integer version;
+}
+```
+
+然后注册乐观锁插件:
+
+```java
+@Configuration
+public class MyBatisConfig {
+
+ /**
+ * 注册插件
+ * @return
+ */
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ // 分页插件
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+ // 乐观锁插件
+ interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+ return interceptor;
+ }
+}
+```
+
+重新执行测试代码,结果如下:
+
+```java
+B管理员修改成功!
+A管理员修改失败!
+Shop(id=1, name=笔记本电脑, price=9000, version=2)
+```
+
+此时 A 管理员的修改就失败了,它需要重新读取最新的数据才能再次进行修改。
+
+## 条件构造器
+
+在分页插件中我们简单地使用了一下条件构造器(`Wrapper`),下面我们来详细了解一下。
+先来看看 `Wrapper` 的继承体系:
+
+分别介绍一下它们的作用:
+
+- `Wrapper`:条件构造器抽象类,最顶端的父类
+ - `AbstractWrapper`:查询条件封装抽象类,生成 SQL 的 where 条件
+ - `QueryWrapper`:用于对象封装
+ - `UpdateWrapper`:用于条件封装
+ - `AbstractLambdaWrapper`:Lambda 语法使用 Wrapper
+ - `LambdaQueryWrapper`:用于对象封装,使用 Lambda 语法
+ - `LambdaUpdateWrapper`:用于条件封装,使用 Lambda 语法
+
+通常我们使用的都是 `QueryWrapper` 和 `UpdateWrapper`,若是想使用 Lambda 语法来编写,也可以使用 `LambdaQueryWrapper` 和 `LambdaUpdateWrapper`,通过这些条件构造器,我们能够很方便地来实现一些复杂的筛选操作,比如:
+
+```java
+@SpringBootTest
+@MapperScan("com.wwj.mybatisplusdemo.mapper")
+class MybatisplusDemoApplicationTests {
+
+ @Autowired
+ private EmployeeMapper employeeMapper;
+
+ @Test
+ void contextLoads() {
+ // 查询名字中包含'j',年龄大于20岁,邮箱不为空的员工信息
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.like("last_name", "j");
+ wrapper.gt("age", 20);
+ wrapper.isNotNull("email");
+ List list = employeeMapper.selectList(wrapper);
+ list.forEach(System.out::println);
+ }
+}
+```
+
+运行结果:
+
+```java
+Employee(id=1, lastName=jack, email=jack@qq.com, age=35, gmtCreate=2021-04-24T21:14:18, gmtModified=2021-04-24T21:14:18, deleted=0)
+```
+
+条件构造器提供了丰富的条件方法帮助我们进行条件的构造,比如 `like` 方法会为我们建立模糊查询,查看一下控制台输出的 SQL:
+
+```sql
+==> Preparing: SELECT id,last_name,email,age,gmt_create,gmt_modified,is_deleted AS deleted FROM tbl_employee WHERE is_deleted=0 AND (last_name LIKE ? AND age > ? AND email IS NOT NULL)
+==> Parameters: %j%(String), 20(Integer)
+```
+
+可以看到它是对 `j` 的前后都加上了 `%` ,若是只想查询以 `j` 开头的名字,则可以使用 `likeRight` 方法,若是想查询以 `j` 结尾的名字,,则使用 `likeLeft` 方法。
+
+年龄的比较也是如此, `gt` 是大于指定值,若是小于则调用 `lt` ,大于等于调用 `ge` ,小于等于调用 `le` ,不等于调用 `ne` ,还可以使用 `between` 方法实现这一过程,相关的其它方法都可以查阅源码进行学习。
+
+因为这些方法返回的其实都是自身实例,所以可使用链式编程:
+
+```java
+@Test
+void contextLoads() {
+ // 查询名字中包含'j',年龄大于20岁,邮箱不为空的员工信息
+ QueryWrapper wrapper = new QueryWrapper()
+ .likeLeft("last_name", "j")
+ .gt("age", 20)
+ .isNotNull("email");
+ List list = employeeMapper.selectList(wrapper);
+ list.forEach(System.out::println);
+}
+```
+
+也可以使用 `LambdaQueryWrapper` 实现:
+
+```java
+@Test
+void contextLoads() {
+ // 查询名字中包含'j',年龄大于20岁,邮箱不为空的员工信息
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
+ .like(Employee::getLastName,"j")
+ .gt(Employee::getAge,20)
+ .isNotNull(Employee::getEmail);
+ List list = employeeMapper.selectList(wrapper);
+ list.forEach(System.out::println);
+}
+```
+
+这种方式的好处在于对字段的设置不是硬编码,而是采用方法引用的形式,效果与 `QueryWrapper` 是一样的。
+
+`UpdateWrapper` 与 `QueryWrapper` 不同,它的作用是封装更新内容的,比如:
+
+```java
+@Test
+void contextLoads() {
+ UpdateWrapper wrapper = new UpdateWrapper()
+ .set("age", 50)
+ .set("email", "emp@163.com")
+ .like("last_name", "j")
+ .gt("age", 20);
+ employeeMapper.update(null, wrapper);
+}
+```
+
+将名字中包含 `j` 且年龄大于 20 岁的员工年龄改为 50,邮箱改为 emp@163.com,`UpdateWrapper` 不仅能够封装更新内容,也能作为查询条件,所以在更新数据时可以直接构造一个 `UpdateWrapper` 来设置更新内容和条件。
\ No newline at end of file
diff --git a/docs/PowerMockRunnerAndMockito.md b/docs/PowerMockRunnerAndMockito.md
new file mode 100644
index 0000000..469e075
--- /dev/null
+++ b/docs/PowerMockRunnerAndMockito.md
@@ -0,0 +1,161 @@
+单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在 Spring 框架中常用的两种测试框架:`PowerMockRunner` 和 `SpringRunner` 两个单元测试,鉴于 `SpringRunner` 启动的一系列依赖和数据连接的问题,推荐使用 `PowerMockRunner`,这样能有效的提高测试的效率,并且其提供的 API 能覆盖的场景广泛,使用方便,可谓是 Java 单元测试之模拟利器。
+
+## 1. PowerMock 是什么?
+
+`PowerMock` 是一个 Java 模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用 `PowerMock`,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。`PowerMock` 通过在执行测试时在运行时修改字节码来完成这些技巧。`PowerMock` 还包含一些实用程序,可让您更轻松地访问对象的内部状态。
+
+举个例子,你在使用 `Junit` 进行单元测试时,并不想让测试数据进入数据库,怎么办?这个时候就可以使用 `PowerMock`,拦截数据库操作,并模拟返回参数。
+
+## 2. PowerMock 包引入
+
+```xml
+
+
+ org.powermock
+ powermock-core
+ 2.0.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 2.23.0
+
+
+ org.powermock
+ powermock-module-junit4
+ 2.0.4
+ test
+
+
+ org.powermock
+ powermock-api-mockito2
+ 2.0.2
+ test
+
+
+ com.github.jsonzou
+ jmockdata
+ 4.3.0
+
+```
+
+## 3. 重要注解说明
+
+```java
+@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试
+@PrepareForTest({RandomUtil.class}) // 所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类
+@PowerMockIgnore("javax.management.*") //为了解决使用powermock后,提示classloader错误
+```
+
+## 4. 使用示例
+
+### 4.1 模拟接口返回
+
+首先对接口进行 mock,然后录制相关行为
+
+```java
+InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class)
+Powermockito.when(mock.method(Params…)).thenReturn(value)
+Powermockito.when(mock.method(Params..)).thenThrow(Exception)
+```
+
+### 4.2 设置对象的 private 属性
+
+需要使用 `Whitebox` 向 class 或者对象中赋值。
+
+如我们已经对接口尽心了 mock,现在需要将此 mock 加入到对象中,可以采用如下方法:
+
+```java
+Whitebox.setInternalState(Object object, String fieldname, Object… value);
+```
+
+其中 object 为需要设置属性的静态类或对象。
+
+### 4.3 模拟构造函数
+
+对于模拟构造函数,也即当出现 `new InstanceClass()` 时可以将此构造函数拦截并替换结果为我们需要的 mock 对象。
+
+注意:使用时需要加入标记:
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ InstanceClass.class })
+@PowerMockIgnore("javax.management.\*")
+
+Powermockito.whenNew(InstanceClass.class).thenReturn(Object value)
+```
+
+##### 4.4 模拟静态方法
+
+模拟静态方法类似于模拟构造函数,也需要加入注释标记。
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ StaticClassToMock.class })
+@PowerMockIgnore("javax.management.\*")
+
+Powermockito.mockStatic(StaticClassToMock.class);
+Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
+```
+
+##### 4.5 模拟 final 方法
+
+Final 方法的模拟类似于模拟静态方法。
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ FinalClassToMock.class })
+@PowerMockIgnore("javax.management.\*")
+
+Powermockito.mockStatic(FinalClassToMock.class);
+Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
+```
+
+### 4.6 模拟静态类
+
+模拟静态类类似于模拟静态方法。
+
+### 4.7 使用 spy 方法避免执行被测类中的成员函数
+
+如被测试类为:TargetClass,想要屏蔽的方法为 targetMethod.
+
+```java
+1) PowerMockito.spy(TargetClass.class);
+
+2) Powemockito.when(TargetClass.targetMethod()).doReturn()
+
+3) 注意加入
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(DisplayMoRelationBuilder.class)
+@PowerMockIgnore("javax.management.*")
+```
+
+### 4.8 参数匹配器
+
+有时我们在处理 `doMethod(Param param)` 时,不想进行精确匹配,这时可以使用 `Mockito` 提供的模糊匹配方式。
+
+如:`Mockito.anyInt()`,`Mockito.anyString()`
+
+### 4.9 处理 public void 型的静态方法
+
+```java
+Powermockito.doNothing.when(T class2mock, String method, … params>
+```
+
+## 5. 单元测试用例可选清单
+
+输入数据验证:这些检查通常可以对输入到应用程序系统中的数据采用。
+
+- 必传项测试
+- 唯一字段值测试
+- 空值测试
+- 字段只接受允许的字符
+- 负值测试
+- 字段限于字段长度规范
+- 不可能的值
+- 垃圾值测试
+- 检查字段之间的依赖性
+- 等效类划分和边界条件测试
+- 错误和异常处理测试单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在 Spring 框架中常用的两种测试框架:PowerMockRunner 和 SpringRunner 两个单元测试,鉴于 SpringRunner 启动的一系列依赖和数据连接的问题,推荐使用 PowerMockRunner,这样能有效的提高测试的效率,并且其提供的 API 能覆盖的场景广泛,使用方便,可谓是 Java 单元测试之模拟利器。
\ No newline at end of file
diff --git a/docs/advanced/SpringBoot-ScheduleTasks.md b/docs/advanced/SpringBoot-ScheduleTasks.md
index 135eb2d..a443545 100644
--- a/docs/advanced/SpringBoot-ScheduleTasks.md
+++ b/docs/advanced/SpringBoot-ScheduleTasks.md
@@ -12,7 +12,7 @@
> Cron 表达式: 主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。
>
-> 推荐一个在线Cron表达式生成器:[http://cron.qqe2.com/](http://cron.qqe2.com/)
+> 推荐一个在线Cron表达式生成器: [https://crontab.guru/](https://crontab.guru/)
```java
import org.slf4j.Logger;
diff --git a/docs/advanced/spring-bean-validation.md b/docs/advanced/spring-bean-validation.md
deleted file mode 100644
index 902220a..0000000
--- a/docs/advanced/spring-bean-validation.md
+++ /dev/null
@@ -1,540 +0,0 @@
-**数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。**
-
-本文结合自己在项目中的实际使用经验,可以说**文章介绍的内容很实用,不了解的朋友可以学习一下,后面可以立马实践到项目上去。**
-
-下面我会通过实例程序演示如何在 Java 程序中尤其是 Spring 程序中优雅地的进行参数验证。
-
-## 基础设施搭建
-
-### 相关依赖
-
-如果开发普通 Java 程序的的话,你需要可能需要像下面这样依赖:
-
-```xml
-
- org.hibernate.validator
- hibernate-validator
- 6.0.9.Final
-
-
- javax.el
- javax.el-api
- 3.0.0
-
-
- org.glassfish.web
- javax.el
- 2.2.6
-
-```
-
-使用 Spring Boot 程序的话只需要`spring-boot-starter-web` 就够了,它的子依赖包含了我们所需要的东西。除了这个依赖,下面的演示还用到了 lombok ,所以不要忘记添加上相关依赖。
-
-```xml
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.projectlombok
- lombok
- true
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
-```
-
-### 实体类
-
-下面这个是示例用到的实体类。
-
-```java
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-public class Person {
-
- @NotNull(message = "classId 不能为空")
- private String classId;
-
- @Size(max = 33)
- @NotNull(message = "name 不能为空")
- private String name;
-
- @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
- @NotNull(message = "sex 不能为空")
- private String sex;
-
- @Email(message = "email 格式不正确")
- @NotNull(message = "email 不能为空")
- private String email;
-
-}
-```
-
-> 正则表达式说明:
->
-> ```
-> - ^string : 匹配以 string 开头的字符串
-> - string$ :匹配以 string 结尾的字符串
-> - ^string$ :精确匹配 string 字符串
-> - ((^Man$|^Woman$|^UGM$)) : 值只能在 Man,Woman,UGM 这三个值中选择
-> ```
-
-下面这部分校验注解说明内容参考自:https://www.cnkirito.moe/spring-validation/ ,感谢@[徐靖峰](https://github.com/lexburner)。
-
-**JSR提供的校验注解**:
-
-
-- `@Null` 被注释的元素必须为 null
-- `@NotNull` 被注释的元素必须不为 null
-- `@AssertTrue` 被注释的元素必须为 true
-- `@AssertFalse` 被注释的元素必须为 false
-- `@Min(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
-- `@Max(value) ` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
-- `@DecimalMin(value) ` 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
-- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
-- `@Size(max=, min=) ` 被注释的元素的大小必须在指定的范围内
-- `@Digits (integer, fraction) ` 被注释的元素必须是一个数字,其值必须在可接受的范围内
-- `@Past ` 被注释的元素必须是一个过去的日期
-- `@Future` 被注释的元素必须是一个将来的日期
-- `@Pattern(regex=,flag=) ` 被注释的元素必须符合指定的正则表达式
-
-
-**Hibernate Validator提供的校验注解**:
-
-
-- `@NotBlank(message =) ` 验证字符串非null,且长度必须大于0
-- `@Email` 被注释的元素必须是电子邮箱地址
-- `@Length(min=,max=) ` 被注释的字符串的大小必须在指定的范围内
-- `@NotEmpty ` 被注释的字符串的必须非空
-- `@Range(min=,max=,message=)` 被注释的元素必须在合适的范围内
-
-## 验证Controller的输入
-
-### 验证请求体(RequestBody)
-
-**Controller:**
-
-我们在需要验证的参数上加上了`@Valid`注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。默认情况下,Spring会将此异常转换为HTTP Status 400(错误请求)。
-
-```java
-
-@RestController
-@RequestMapping("/api")
-public class PersonController {
-
- @PostMapping("/person")
- public ResponseEntity getPerson(@RequestBody @Valid Person person) {
- return ResponseEntity.ok().body(person);
- }
-}
-```
-
-**ExceptionHandler:**
-
-自定义异常处理器可以帮助我们捕获异常,并进行一些简单的处理。如果对于下面的处理异常的代码不太理解的话,可以查看这篇文章 [《SpringBoot 处理异常的几种常见姿势》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485568&idx=2&sn=c5ba880fd0c5d82e39531fa42cb036ac&chksm=cea2474bf9d5ce5dcbc6a5f6580198fdce4bc92ef577579183a729cb5d1430e4994720d59b34&token=1924773784&lang=zh_CN#rd)。
-
-```java
-@ControllerAdvice(assignableTypes = {PersonController.class})
-public class GlobalExceptionHandler {
- @ExceptionHandler(MethodArgumentNotValidException.class)
- public ResponseEntity