# education
**Repository Path**: lixu0811/education
## Basic Information
- **Project Name**: education
- **Description**: 基于 SpringBoot + Mybatis Plus + Shiro + mysql + redis构建的智慧云智能教育平台。架构上使用完全前后端分离。 支持多种题型:选择题、多选题、判断题、填空题、综合题以及数学公式。支持在线考试,教师在线批改试卷。
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 2
- **Created**: 2023-03-10
- **Last Updated**: 2023-03-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 智慧云智慧教育平台实战项目笔记
## 一、项目简介
课程内容:智慧云教育平台管理后台、智慧云教育平台学生端、项目的正式部署
### 1、技术说明
- 后端技术:JDK1.8 + SpringBoot + MyBatis + Shiro
- 缓存框架:Redis
- 数据库:MySQL 5.7
- 前端技术:Element-UI + Vue
- 开发工具:IDEA 2019.3.3
- 项目管理工具:Maven、Git
使用最主流的框架 SpringBoot + Vue 实现完全前后端分离
### 2、核心功能介绍
- 管理后台核心功能:RBAC权限管理、试题管理、试卷批改
- 学生端核心功能:考试中心、我的错题本
### 3、学习前提
- 后端技术:掌握SpringBoot + MyBatis + Shiro + MySQL的基本使用
- 前端技术:掌握Vue、Element-UI、CS6语法的基本使用
- 热爱Java编程,喜欢研究新技术
### 4、课程收获
- 加强对Java程序员基础知识的掌握
- 掌握企业级项目编码规范,提升代码优化的能力
- 掌握企业级 SpringBoot + Vue + Element-UI 全栈开发技能,增加项目经验,提升职场竞争力
- 掌握项目从零搭建到项目正式部署的完整流程
## 二、Maven介绍及其配置
### 1、Maven是什么?
Maven是Apache下的一一个纯Java 开发的开源项目。它主要用来帮助实现项目的构建、测试、打包和部署。Maven提供了标准的软件生命周期模型和构建模型,通过配置就能对项目进行全面的管理。[想了解更多点击这里](https://www.runoob.com/maven/maven-tutorial.html)
### 2、Maven的优势
- Maven能够帮助我们快速构建和发布项目,提高工作效率
- Maven能够非常方便的帮助我们管理jar包和解决jar包冲突
- Maven对于目录结构有要求,约定优于配置,开发者在项目间切换就省去了学习成本
- Maven有助于项目的多模块开发
### 3、Maven项目结构
| 目录 | 目的 |
| :------------------------------------ | :------------------------------------------------------------ |
| `${basedir}` | 存放pom.xml和所有的子目录 |
| `${basedir}/src/main/java` | 项目的java源代码 |
| `${basedir}/src/main/resources` | 项目的资源,比如说property文件,springmvc.xml |
| `${basedir}/src/test/java` | 项目的测试类,比如说Junit代码 |
| `${basedir}/src/test/resources` | 测试用的资源 |
| `${basedir}/src/main/webapp/WEB-INF` | web应用文件目录,web项目的信息,比如存放web.xml、本地图片、jsp视图页面 |
| `${basedir}/target` | 打包输出目录 |
| `${basedir}/target/classes` | 编译输出目录 |
| `${basedir}/target/test-classes` | 测试编译输出目录 |
| `Test.java` | Maven只会自动运行符合该命名规则的测试类 |
| `~/.m2/repository` | Maven默认的本地仓库目录位置 |
### 4、Maven下载
Maven 下载地址:[http://maven.apache.org/download.cgi](http://maven.apache.org/download.cgi)

不同平台下载对应的包:
| 系统 | 包名 |
| :------- | :----------------------------- |
| Windows | apache-maven-3.3.9-bin.zip |
| Linux | apache-maven-3.3.9-bin.tar.gz |
| Mac | apache-maven-3.3.9-bin.tar.gz |
下载包后解压到对应目录:
| 系统 | 存储位置 (可根据自己情况配置) |
| :------- | :----------------------------- |
| Windows | `D:\Maven\apache-maven-3.3.9` |
| Linux | `/usr/local/apache-maven-3.3.9` |
| Mac | `/usr/local/apache-maven-3.3.9` |
### 5、设置Maven环境变量
右键 "计算机",选择 "属性",之后点击 "高级系统设置",点击"环境变量",来设置环境变量,有以下系统变量需要配置:
新建系统变量 `MAVEN_HOME`,变量值:`E:\Maven\apache-maven-3.3.9`

编辑系统变量 `Path`,添加变量值:`;%MAVEN_HOME%\bin`

**注意:**注意多个值之间需要有分号隔开,然后点击确定。
### 6、修改Maven配置文件
打开`E:\Maven\apache-maven-3.3.9\conf`目录下的`setting.xml`文件
修改本地仓库路径:
```xml
D:\Apps\Maven-3.8.1\repository
```
修改镜像源为阿里云
```xml
nexus-aliyun
Nexus aliyun
http://maven.aliyun.com/nexus/content/groups/public
central
```
修改编译器JDK版本为1.8
```xml
jdk-1.8
true
1.8
1.8
1.8
1.8
```
### 7、pom.xml文件中常见的标签使用
```xml
4.0.0
cn.jasondom.springboot
Template
1.0.0
jar
SpringBoot
SpringBoot is a lightweight Java Web Framework
http://maven.apache.org
1.8
2.2.3
org.apache.velocity
velocity
${velocity.version}
...
...
...
...
...
...
...
子项目1目录路径
子项目2目录路径
...
```
### 8、多模块开发的好处
- 降低项目复杂性,提升我们的开发效率
- 有利于项目遵从**高内聚,低耦合**的设计模式,保证了代码的质量和健壮性
- 避免重复造轮子,减少工作量
### 9、IDEA上配置Maven
在`File`中打开`Settings...`,搜索`Maven`,根据安装目录修改配置

**注意**:建议使用Maven-3.5,如果版本过高可能与IDEA不兼容
为了避免所有新建项目都要修改Maven配置,可以打开`File` -> `Other Settings` -> `Settings for New Project...`重新设置一次Maven

## 三、后端项目环境的搭建
### 1、创建Maven类型的父工程


接下来点击`Next` -> `Finish`完成创建,等项目构建完成后删除`src`加粗样式目录
### 2、创建SpringBoot类型的管理模块
(1)、在工程目录上右键单击,选择`New` -> `Module...`

(2)、设置模块名称

(3)、选择基本依赖

(4)、点击`Next` -> `Finish`完成创建,将管理模块的`SpringBoot`父依赖剪切至父工程中,并将版本改为`2.1.9.RELEASE`

```xml
org.springframework.boot
spring-boot-starter-parent
2.1.9.RELEASE
```
(5)、将管理模块的`parent`标签修改成以下内容,使其依赖于父工程
```xml
com.education
education
1.0-SNAPSHOT
```
**(6)、剪切管理模块的依赖列表至父工程**
```xml
junit
junit
4.11
test
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
```
(7)、将JDK版本改为1.8
```xml
UTF-8
1.8
1.8
```
### 3、创建Maven类型的service模块
(1)、在工程目录上右键单击,选择`New` -> `Module...`

(2)、设置模块名称(将`GroupId`设置为`com.education.service`)

(3)、点击`Next` -> `Finish`完成创建,打开`service`模块下的`pom.xml`文件,修改`groupId`和父工程为如下内容
```xml
com.education
education
1.0-SNAPSHOT
com.education.service
education-service
1.0-SNAPSHOT
```
### 4、创建common工具模块
(1)、在工程目录上右键单击,选择`New` -> `Module...`

(2)、设置模块名称(将`GroupId`设置为`com.education.common`)

(3)、点击`Next` -> `Finish`完成创建,打开`common`模块下的`pom.xml`文件,修改`groupId`和父工程为如下内容
```xml
com.education
education
1.0-SNAPSHOT
com.education.common
education-common
1.0-SNAPSHOT
```
### 5、修改依赖关系
(1)、添加模块到父工程,在父工程的`pom.xml`文件中添加如下内容
```xml
education-admin-api
education-service
education-common
```
(2)、在`admin-api`模块中添加依赖
```xml
com.education.common
education-common
1.0-SNAPSHOT
com.education.service
education-service
1.0-SNAPSHOT
```
(3)在`service`模块中添加依赖
```xml
com.education.common
education-common
1.0-SNAPSHOT
```
### 6、测试环境
(1)、在管理模块`src/main/resources/`目录下创建如下三个文件(删除默认的`application.properties`文件)
| 配置文件名 | 说明 |
|:--|:--|
| `application.yml` | 默认环境配置文件 |
| `application-dev.yml` | 开发环境配置文件 |
| `application-prod.yml` | 生产环境配置文件 |
(2)、在`application.yml`中添加如下配置,激活开发环境配置文件
```yaml
spring:
profiles:
active: dev
```
(3)、在`application-dev.yml`中添加如下配置,设置服务器访问端口号
```yaml
server:
port: 80
```
(4)、在`com/education/admin/api/EducationAdminApiApplication.java`入口类中编写测试方法test
```java
@SpringBootApplication
@RestController
public class EducationAdminApiApplication {
@GetMapping
public String test() {
return "success";
}
public static void main(String[] args) {
SpringApplication.run(EducationAdminApiApplication.class, args);
}
}
```
(5)、删除`education-admin-api/src/test/java/com/education/admin/api`包下的test类后,启动main函数
(6)、在浏览器中访问`localhost`,如果提示`success`表示环境搭建成功
### 7、Maven中常用的命令
| 指令 | 功能 |
| :--------- | :-------------------------------- |
| `clean` | 清除编译后的class文件 |
| `compile` | 编译项目的源代码 |
| `test` | 对项目进行单元测试 |
| `package` | 对项目进行打包 |
| `install` | 对项目进行打包,并安装到本地仓库 |
## 四、Git简介及基本使用
### 1、Git简介
#### 1.1 Git是什么?
Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。
#### 1.2 为什么要使用Git?
- 基于分布式的设计,有利于项目的多人合作开发,提高工作效率
- 方便开发者解决代码冲突
- 可以从当前版本回退到任意版本,防止误操作导致代码丢失
#### 1.3 Git的工作流程

#### 1.4 Git分支的概念
可以理解成一条条的**河流**,最终都要流入**大海**(master)
#### 1.5 Git分支结构

### 2、下载并配置Git
#### 2.1 下载Git
Git 各平台安装包下载地址为:[http://git-scm.com/downloads](http://git-scm.com/downloads)
#### 2.2 在IDEA上配置Git
在`File`中打开`Settings...`,搜索`Git`,根据安装目录修改配置

### 3、将代码上传至码云仓库
#### 3.2 项目码云步骤
- 注册码云账号 -> [https://gitee.com/](https://gitee.com/)
- 使用码云[创建一个远程仓库](https://gitee.com/help/articles/4120)
- 在IDEA中将代码上传至码云仓库
#### 3.1 IDEA中将代码上传至码云仓库
1)选择`Create Git Repository...`,在弹出的对话框中选择项目路径

2)将代码提交至本地仓库

选中要提交的文件,输入提交信息,点击`Commit`即可提交到本地仓库(只提交源码和配置文件,`.idea`、`.mvn`和编译等文件不用提交)

3)将项目提交至远程仓库

点击`Define remote`,输入远程仓库的地址点击`OK`,选择`Push`即可提交代码至远程仓库

### 4、代码的更新
(1)、可以直接在码云线上修改代码

(2)添加一个测试方法,并提交

(3)按图示操作,即可更新代码到本地(有的IDEA有多个选项,可以选择`Merge`或`Branch Default`,但**推荐选择`Merge`**,因为`Merge`不仅可以更新代码,在一般情况下,它还会自动帮忙合并代码)

(4)测试完成后将test测试方法删除,重新push到码云(图示中的`Commit and Push`可以同时将代码提交到本地和远程仓库,而`Commit`只能提交到本地)

### 5、使用Git解决代码冲突
当多个人修改了同一方法,就会产生**代码冲突**,下面通过线上线下同时修改代码模拟多人修改同一方法的场景
(1)、线上修改代码并提交

(2)、线下修改代码并提交

提交代码,选择`Commit and Push`,弹出对话框后继续选择`Commit and Push`,再选择`Push`

(3)、此时会弹出拒绝提交,需要合并代码的提示,选择`Merge`

(4)、弹出冲突提示,继续选择`Merge`

(5)、此时会弹出冲突代码的对比窗口,可以点击第15行代码处指向中间窗口的箭头(`>>`或`<<`)来控制合并代码,选择好后点击`Apply`,再按快捷键Ctrl + Shift + K 重新提交代码

### 6、将远程仓库代码导入到本地
(1)、从远程仓库新建项目

(2)、输入远程仓库地址,选择项目的存放路径,点击`Clone`,弹出提示后点击`Yes`

(3)、你会发现IDEA没有自动打开项目,可以通过`File` -> `Open` -> 选择项目的`pom.xml`文件打开,此时会多次弹出提示框,按提示操作即可,右下角弹出提示后选择`Add as Maven Project`,至此,项目就成功导入了
### 7、Git分支的使用
可通过右下角的`Git xxx`按钮查看当前分支

> `Local Branches`表示本地分支;`Remote Branches`表示远程仓库分支;按钮中的`xxx`表示当前分支名称;可以通过`New Branch`创建新分支;点击相应分支后会弹出二级菜单:`Checkout`表示切换到此分支;`Merge into Current` 表示合并此分支到当前分支
#### 7.1 创建`dev`分支
(1)点击 `New Branch` -> 在弹出的对话框中输入分支名`dev`,再按快捷键Ctrl + Shift + K 直接提交分支
 
(2)接下来就可以在码云平台看到新添加的`dev`分支了

#### 7.2 切换分支
点击右下角的 `Git dev` 按钮,点击要切换的分支,在弹出的二级菜单中点击`Checkout`即可切换到指定分支

#### 7.3 分支代码的合并
(1)先切换到`dev`分支

(2)在SpringBoot入口类中添加 `test` 方法

(3)按快捷键 Ctrl + K ,按照图示操作之后会弹出提交窗口,选择`Push`提交即可

(4)切换到`master`分支

(5)将`dev`分支合并到`master`分支,此时`dev`上修改的代码就合并到`master`上了

(7)按快捷键Ctrl + Shift + K 直接提交
#### 7.4 在`dev`上检出一个`bug`分支
(1)先切换到`dev`分支

(2)新建一个`bug`分支
 
(3)在`SpringBoot`入口类中添加`test1`方法

(4)提交(Ctrl + K )

(5)切换到`dev`分支,会发现没有`test1`方法

(6)将`bug`分支合并到`dev`分支,再按快捷键`Ctrl + Shift + K` 提交

(7)最后切换到master分支,按快捷键Ctrl + Shift + K 提交
#### 7.5 使用码云创建分支
(1)登录码云,进入要创建分支的仓库,打开分支管理

(2)创建`feature`分支

(3)在IDEA中点击Fetch可以更新分支到本地,更新完成后,在右下角点击`git master`即可看到线上新建的`feature`分支了

### 8、码云平台相关功能
可以为项目添加合作伙伴,按不同的职责分配不同的角色权限

## 五、项目的前期准备
### 1、Map和传统JavaBean技术选型
#### 1.1 Map的优缺点
> **优点:**
>
> - 灵活性强于JavaBean,易扩展,耦合度低。
> - 写起来简单,代码量少
> - MyBatis查询的返回结果本身就是Map
>
> **缺点:**
>
> - 不能一眼看出Map中有哪些参数
#### 1.2 JavaBean的优缺点
> **优点:**
>
> - 符合Java语言面向对象设计的原则
> - 数据结构清晰,便于团队开发和后期维护
>
> **缺点:**
>
> - 需要不断地去维护实体类
#### 1.3 如何选型呢?
- 团队人数少,追求开发效率,建议使用Map代替实体类
- 项目庞大,需要持续维护,团队人数多,建议使用实体类
### 2、响应结果封装及全局异常处理
#### 2.1 MVC概念

#### 2.2 MVC流程

#### 2.3 前后端分离

#### 2.4 前后端分离的优缺点
**优点**
- 分工更明确,提升各自领域专注度
- 前后端代码完全分离,有利于项目维护
**缺点:**
- 需要不断地维护接口文档
- 沟通成本更高
- 部署相比较MVC架构要复杂点
#### 2.5 后端接口统一json数据格式
接口返回成功示例:
```json
{
"code": 1,
"message": "请求成功",
"data": {
"user_info": {
"name": "张三",
"address": "北京市xx区"
}
}
}
```
接口请求失败示例:
```json
{
"code": 0,
"message": "系统异常"
}
```
#### 2.6 响应结果封装
(1)给父工程添加以下两个依赖
```xml
org.slf4j
slf4j-api
ch.qos.logback
logback-core
```
(2)在`common`模块下创建`utils`包,用于放置系统中的工具类

(3)创建`ResultCode`类和`Result`类
```java
package com.education.common.utils;
/**
* http 响应状态码
* SUCCESS: 响应成功状态码
* FAIL: 响应失败状态码
*/
public class ResultCode {
public static final int SUCCESS = 1;
public static final int FAIL = 0;
public static final String DEFAULT_SUCCESS_MESSAGE = "操作成功";
public static final String DEFAULT_FAIL_MESSAGE = "操作失败";
private int code = SUCCESS;
private String message;
public ResultCode() {}
public ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
```
```java
package com.education.common.utils;
/**
* 对请求结果的封装
*/
public class Result {
private Object data;
private ResultCode resultCode;
public Result() {}
public Result(ResultCode resultCode) {
this.resultCode = resultCode;
}
public Result(Object data) {
this.resultCode = new ResultCode(ResultCode.SUCCESS, ResultCode.DEFAULT_SUCCESS_MESSAGE);
this.data = data;
}
public Result(ResultCode resultCode,Object data) {
this(resultCode);
this.data = data;
}
/**
* 响应成功
* @param data 响应的数据
* @return 封装的请求
*/
public static Result success(Object data) {
return new Result(data);
}
/**
* 响应成功
* @param resultCode 响应的状态码
* @param data 响应的数据
* @return 封装的请求
*/
public static Result success(ResultCode resultCode, Object data) {
return new Result(resultCode, data);
}
/**
* 响应成功
* @param resultCode 响应的状态码
* @return 封装的请求
*/
public static Result success(ResultCode resultCode) {
return new Result(resultCode);
}
/**
* 响应失败
* @param resultCode 状态码
* @return 封装的请求
*/
public static Result fail(ResultCode resultCode) {
return new Result(resultCode);
}
/**
* 响应失败
* @return 封装的请求
*/
public static Result fail() {
return new Result(new ResultCode(ResultCode.FAIL,ResultCode.DEFAULT_FAIL_MESSAGE));
}
/**
* 判断请求是否成功
* @return 请求的状态:成功 true 失败 false
*/
public boolean isSuccess() {
return ResultCode.SUCCESS == this.resultCode.getCode();
}
public T getData() {
return (T) data;
}
public void setData(Object data) {
this.data = data;
}
public ResultCode getResultCode() {
return resultCode;
}
public void setResultCode(ResultCode resultCode) {
this.resultCode = resultCode;
}
}
```
#### 2.7 全局异常处理
(1)在`com.education.common`下创建`exception`包

(2)在`exception`包下创建系统业务异常类`BusinessException`和全局异常处理类`SystemExceptionHandler`
```java
package com.education.common.exception;
import com.education.common.utils.ResultCode;
/**
* 系统业务异常类
*/
public class BusinessException extends RuntimeException {
private ResultCode resultCode;
public BusinessException(ResultCode resultCode) {
this.resultCode = resultCode;
}
public BusinessException(String message) {
super(message);
}
public BusinessException(Throwable throwable) {
super(throwable);
}
public BusinessException(String message, Throwable throwable) {
super(message, throwable);
}
public ResultCode getResultCode() {
return resultCode;
}
public void setResultCode(ResultCode resultCode) {
this.resultCode = resultCode;
}
}
```
```java
package com.education.common.exception;
import com.education.common.utils.Result;
import com.education.common.utils.ResultCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 处理全局异常
*/
@ControllerAdvice
public class SystemExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(SystemExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public Result resolveException(Exception e) {
Result result = new Result(new ResultCode(ResultCode.FAIL,ResultCode.DEFAULT_FAIL_MESSAGE));
// 判断是否为业务异常
if(e instanceof BusinessException) {
BusinessException businessException = (BusinessException) e;
if (businessException.getResultCode() != null) {
result.setResultCode(businessException.getResultCode());
}
}
logger.error("系统异常",e);
return result;
}
}
```
### 3、Java线程池技术
#### 3.1 为什么要使用线程池?
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务,降低了系统资源的消耗
#### 3.2 Web系统中使用多线程的场景
主业务程序与子业务耦合度低:**发短信或发送邮件**、**请求第三方接口**
#### 3.3 线程池的执行原理

#### 3.4 线程池的销毁
```java
//等待所有正在执行的任务全试图停止所有正在执行
.shutdoen();
// 部执行完毕之后才会销毁的线程
.shutdownNow();
```
#### 3.5 线程池的创建方法
`Executors`创建线程池
| 方法名 | 功能 |
| :--------------------------------- | :--------------------------------------------------------- |
| `newFixedThreadPool(int nThreads)` | 创建固定大小的线程池 |
| `newSingleThreadExcutor()` | 创建只有一个线程的线程池 |
| `newCachedThreadPool()` | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
ThreadPoolExecutor
```java
// Java 线程池的完整构造函数
public ThreadPoolExecutor (
int corePoolSize, // 线程池长期维持的线程数,即使线程处于idle状态,也不会回收
int maximumPoolSize, // 线程数的上限
long keepAliveTiem, TimeUnit unit, // 超过corePoolSize的线程的idle时长
// 超过这个时间,多余的线程会被回收
BlookingQueue workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
```
#### 3.6 Spring与线程池的整合
```java
private static final int COUNT = Runtime.getRuntime().availableProcessors(); // cpu个数
private static final int CORE_SIZE = COUNT * 2;
private static final int MAX_SIZE = COUNT * 4;
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setMaxPoolSize(MAX_SIZE);
threadPoolTaskExecutor.setCorePoolSize(CORE_SIZE);
threadPoolTaskExecutor.setQueueCapacity(20);
threadPoolTaskExecutor.setKeepAliveSeconds(200);
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return threadPoolTaskExecutor;
}
```
### 4、线程池的使用
#### 4.1 线程资源复用示例
```java
package com.education.common;
import java.util.concurrent.*;
/**
* Unit test for simple App.
*/
public class AppTest
{
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); // 获取cpu数量
private static final int CORE_SIZE = CPU_COUNT * 2;
private static final int MAX_CORE_SIZE = CPU_COUNT * 4;
private static final int QUEUE_SIZE = 30;
static class MyThread implements Runnable {
private int number;
public MyThread(int number) {
this.number = number;
}
@Override
public void run() {
System.out.println("正在执行任务" + number + " " + Thread.currentThread());
}
}
public static void main(String [] args) throws ExecutionException, InterruptedException {
// SynchronousQueue; 是一个无界缓存等待队列,不能指定队列容量
// ArrayBlockingQueue; 是一个游街缓存等待队列,可以指定队列容量
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_SIZE, // 线程池长期维持的线程数,即使线程处于idle状态,也不会回收
MAX_CORE_SIZE, // 线程数的上限
60L, TimeUnit.SECONDS, // 超过corePoolSize线程的idle时长,超过这个时间,多余的线程会被回收
// 此处设置了队列容量,当设置了队列容量后,最大并发量不能超过队列容量和线程数的总和
new LinkedBlockingQueue<>(QUEUE_SIZE));
// System.out.println("并发上限:" + (MAX_CORE_SIZE + QUEUE_SIZE));
for (int i = 0; i < 200; i++) {
// 将任务添加到线程池
Future> future = threadPoolExecutor.submit(new MyThread(i));
if (future.get() == null) {
System.out.println(Thread.currentThread() + "任务已完成");
}
// threadPoolExecutor.execute(new MyThread(i));
}
}
}
```
> 通过运行结果会发现线程资源被多次复用。
#### 4.2 线程池的拒绝策略
(1)修改for循环体代码:
```java
for (int i = 0; i < 200; i++) {
// 将任务添加到线程池
/*
Future> future = threadPoolExecutor.submit(new MyThread(i));
if (future.get() == null) {
System.out.println(Thread.currentThread() + "任务已完成");
}
*/
threadPoolExecutor.execute(new MyThread(i));
}
```
(2)重新运行,会发现报错(没报错可以增大循环次数),这是因为设置了队列容量`QUEUE_SIZE`,当并发量超过线程数和队列容量的总和时,就会抛出异常

(3)可以通过`setRejectedExecutionHandler()`方法设置**拒绝策略**,在for循环上方添加以下语句:
```java
threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
```
(4)重新运行,会发现所有任务全部执行完成

(5)修改拒绝策略
```
threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
```
(6)重新运行,会发现有部分任务没有执行,并且也没有抛出异常

#### 4.3 线程池在实际项目中的使用
(1) 在`com.education.common.utils`包下创建`SpringBeanManager`类
```java
package com.education.common.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
/**
* bean 实例工具类
*/
@Component
@Lazy(false)
public class SpringBeanManager implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringBeanManager.applicationContext = applicationContext;
}
public static T getBean(String name) {
return (T) applicationContext.getBean(name);
}
public static T getBean(Class clazz) {
return (T) applicationContext.getBean(clazz);
}
}
```
(2)创建任务监听器
```java
package com.education.service.task;
/**
* 任务监听器
*/
public interface TaskListener {
void onMessage(TaskParam taskParam);
}
```
(3)创建任务管理器
```java
package com.education.service.task;
import com.education.common.utils.SpringBeanManager;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 任务管理器
* @author Jason
*/
public class TaskManager {
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
// private final Map taskListenerMap = new ConcurrentHashMap<>();
public TaskManager(ThreadPoolTaskExecutor threadPoolTaskExecutor) {
this.threadPoolTaskExecutor = threadPoolTaskExecutor;
}
public void pushTask(TaskParam taskParam) {
TaskListener taskListener = SpringBeanManager.getBean(taskParam.getTaskListenerClass());
if (taskListener != null) {
threadPoolTaskExecutor.execute(() -> {
taskListener.onMessage(taskParam);
});
}
}
}
```
(4)封装任务参数
```java
package com.education.service.task;
import java.util.HashMap;
/**
* 封装任务参数
* @author Jason
*/
public class TaskParam extends HashMap {
private Class extends TaskListener> taskListenerClass;
private final long timestamp;
private Object data;
public Class extends TaskListener> getTaskListenerClass() {
return taskListenerClass;
}
public void setTaskListenerClass(Class extends TaskListener> taskListenerClass) {
this.taskListenerClass = taskListenerClass;
}
public long getTimestamp() {
return timestamp;
}
public void setData(Object data) {
this.data = data;
}
public T getData() {
return (T) data;
}
public TaskParam() {
this.timestamp = 0L;
}
public TaskParam(Class extends TaskListener> taskListenerClass, long timestamp, Object data) {
this.taskListenerClass = taskListenerClass;
this.timestamp = timestamp;
this.data = data;
}
public TaskParam(Class extends TaskListener> taskListenerClass, Object data) {
this(taskListenerClass, System.currentTimeMillis(), data);
}
public TaskParam(Class extends TaskListener> taskListenerClass) {
this(taskListenerClass, System.currentTimeMillis(), null);
}
}
```
(4)配置线程池
```java
package com.education.service.task;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 配置线程池
*/
@Configuration
public class TaskBeanConfig {
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_SIZE = CPU_COUNT * 2;
private static final int MAX_CORE_SIZE = CPU_COUNT * 4;
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(CORE_SIZE);
threadPoolTaskExecutor.setMaxPoolSize(MAX_CORE_SIZE);
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return threadPoolTaskExecutor;
}
@Bean
public TaskManager taskManager (ThreadPoolTaskExecutor threadPoolTaskExecutor) {
return new TaskManager(threadPoolTaskExecutor);
}
}
```
(5)实现`TaskListener`接口
```java
package com.education.service.task;
import org.springframework.stereotype.Component;
/**
*/
@Component
public class LogTaskListener implements TaskListener{
@Override
public void onMessage(TaskParam taskParam) {
System.out.println("执行LogTaskListener:" + taskParam.getData());
}
}
```
(6)编写测试代码
```java
package com.education.admin.api;
import com.education.service.task.TaskManager;
import com.education.service.task.TaskParam;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
/**
* 注解@SpringBootApplication默认扫描当前包,这样会导致无法扫描
* 到com.education.service和com.education.common下的注解,
* 此时就需要设置扫描包路径为com.education
*/
@SpringBootApplication(scanBasePackages = "com.education")
public class App {
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(App.class, args);
// 获取TaskManager的实例
TaskManager taskManager = applicationContext.getBean(TaskManager.class);
// 定义任务参数
TaskParam taskParam = new TaskParam();
taskParam.setData("test");
// 设置监听器bean实例名称
taskParam.setTaskListenerBeanName("logTaskListener");
taskManager.pushTask(taskParam);
System.out.println(Thread.currentThread().getName());
}
}
```
### 5、整合Quartz
#### 5.1 Quartz介绍
##### 5.1.1 Quartz是什么?
Quartz是一个功能丰富的开源任务调度框架,它可以创建简单或者复杂的几十、几百、甚至成千上万的job。此外,quartz调度器还支持JTA事务和集群
##### 5.1.2 Quartz与Spring Task的区别
- Quartz默认多线程异步执行,而Spring Task默认单线程同步执行
- Spring Task属于轻量级任务调度框架,使用更简单
- Quartz在执行任务过程中如果抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务。而使用Spring Task一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务。
- Quartz每次执行都创建一个新的任务类对象,Spring Task则每次使用同一个任务类对象。
##### 5.1.3 Quartz三要素
- **Scheduler:**任务调度器,它是quartz的核心所在,所有的任务都是通过scheduler开始
- **Trigger:**任务触发器
- **JobDetail和Job:**定义任务具体执行的逻辑
##### 5.1.4 Trigger类中常用的方法
| 方法名 | 功能 |
| :-------------------------------------------- | :----------------------------- |
| `withIdentity(String name, String gtoup)` | 给触发器一些属性比如名字,组名 |
| `startNow()` | 立刻启动 |
| `withSchedule(ScheduleBuilder schedBuilder)` | 以某种触发器触发 |
| `usingJobData(String dataKey, Boolean value)` | 给具体job传递参数 |
##### 5.1.5 Cron表达式
Cron表达式用于配置`CronTrigger`的实例,由七个子表达式组成,用来描述详细的时间信息
```apl
格式 [秒] [分] [时] [日] [月] [周] [年]
```
##### 5.1.6 配置Cron表达式
Cron表达式的格式:秒 分 时 日 月 周 年(可选)
| 字段名 | 允许的值 | 允许的特殊字符 |
| :----------- | :-------------- | :----------------------------------------------------------- |
| 秒 | 0—59 | , - * / |
| 分 | 0—59 | , - * / |
| 时 | 0—23 | , - * / |
| 日 | 1—31 | , - * ?/LWC |
| 月 | 1—12 或 JAN—DEC | , - * / |
| 周 | 1—7 或 SUN—SAT | , - * ?/LC# |
| 年(可选字段) | empty | 1970—2099, - * / |
##### 5.1.7 Cron表达式示例
| 表达式 | 含义 |
| :------------------- | :----------------------------- |
| `0 * * * ?` | 每一分钟触发一次 |
| `0 0 10,14,16 * * ?` | 每天上午10点,下午2点,4点触发 |
| `0 0 5-15 * * ?` | 每天5-15点整点触发 |
| `0 0 10 * * ?` | 每天10点触发一次 |
| `*/5 * * * * ?` | 每隔5秒执行一次 |
#### 5.2 Quartz定时任务实例
(1)在父类工程中添加Quartz起步依赖
```xml
org.springframework.boot
spring-boot-starter-quartz
```
(1)创建任务测试类

```java
package com.education.admin.api;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestJob implements Job {
/**
* 执行具体的业务逻辑
* @param jobExecutionContext
* @throws JobExecutionException
*/
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(date));
System.out.println("执行任务:" + TestJob.class.getSimpleName());
}
private static final String DEFAULT_GROUP = "default_group";
public static void main(String[] args) throws SchedulerException {
// 创建一个JobDetail对象
JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity(TestJob.class.getSimpleName(), DEFAULT_GROUP).build();
// 创建任务触发器
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(TestJob.class.getSimpleName(), DEFAULT_GROUP)
.startNow().withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?")).build();
// 创建任务调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
}
```
> `DailyTimeIntervalScheduleBuiIder`:
>
> - `SimpIeScheduleBuilder`:简单循环执行,设定执行次数,开始结束时间等
> - `CronScheduleBuiIder`:Cron表达式实现的定时执行
运行调试会发现每5秒钟会运行一次`execute`方法
#### 5.3 SpringBoot整合Quartz
(1)在`service`模块下创建`job`包,再创建一个任务基类`BaseJob`
```java
package com.education.service.job;
import org.springframework.scheduling.quartz.QuartzJobBean;
/**
* @author Jason
* @version 1.0.0
* @date 2021-11-21 00:52
*/
public abstract class BaseJob extends QuartzJobBean {}
```
(2)创建系统任务类`SystemJob`
```java
package com.education.service.job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
*/
public class SystemJob extends BaseJob {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(date));
System.out.println("执行任务:" + SystemJob.class.getSimpleName());
}
}
```
(3)创建任务配置类`JobBeanConfig`
```java
package com.education.service.job;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*/
@Configuration
public class JobBeanConfig {
private static final String DEFAULT_GROUP = "default_group";
@Bean
public JobDetail systemJob() {
return JobBuilder.newJob(SystemJob.class)
.withIdentity(SystemJob.class.getSimpleName(), DEFAULT_GROUP).build();
}
@Bean
public Trigger jobTrigger() {
return TriggerBuilder.newTrigger().forJob(systemJob().getKey())
.withIdentity(SystemJob.class.getSimpleName(), DEFAULT_GROUP)
.startNow().withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?")).build();
}
}
```
(4)修改`admin-api`入口类
```java
package com.education.admin.api;
import com.education.service.task.TaskManager;
import com.education.service.task.TaskParam;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
/**
* 注解@SpringBootApplication默认扫描当前包,这样会
* 导致无法扫描到com.education.service和com.education.common下的注解,
* 所以需要设置扫描包路径为com.education
*/
@SpringBootApplication(scanBasePackages = "com.education")
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
/*
ApplicationContext applicationContext = SpringApplication.run(App.class, args);
TaskManager taskManager = applicationContext.getBean(TaskManager.class);
TaskParam taskParam = new TaskParam();
taskParam.setData("test");
taskParam.setTaskListenerBeanName("logTaskListener");
taskManager.pushTask(taskParam);
System.out.println(Thread.currentThread().getName());
*/
}
}
```
(5)启动main函数,会发现报错

(6)在`JobBeanConfig`类`systemJob`方法中添加`storeDurably()`方法

(7)重启启动main函数,会发现任务每个隔5秒钟执行一次

## 六、RBAC权限管理
### 1、RBAC简介
#### 1.1 RBAC权限管理
- 基于角色的权限访问控制(Role-Based Access Control)
- 用户和角色关联
- 角色关联权限
#### 1.2 RBAC流程

#### 1.3 什么是权限?
权限是资源的集合,主要包括菜单、页面、字段、操作功能(增删改查)等等
- 页面权限
- 操作权限
- 数据权限
#### 1.4 为什么要使用权限?
- 使用者的角度
在限制范围内正确的使用权力
- 设计者角度来说
保证系统更加安全:控制不同的角色合理的访问不同的资源
#### 1.5 RBAC中的功能模块

### 2、RBAC权限系统数据库设计
#### 2.1 系统(System)
##### 2.1.1 系统管理员表(system_admin)
| 字段名 | 类型 | KEY | 默认值 / 描述 |
| :-------------- | :----------- | :---------- | :--------------- |
| id | INT | PRIMARY KEY | 主键 |
| login_name | VARCHAR(100) | NOT NULL | 登录名 |
| name | VARCHAR(100) | | 真实姓名 |
| password | VARCHAR(100) | NOT NULL | 密码 |
| encrypt | VARCHAR(100) | NOT NULL | MD5盐值 |
| mobile | VARCHAR(20) | NOT NULL | 手机号 |
| disabled_flag | TINYINT(1) | DEFAULT 0 | 1 是 0 否 |
| mail | VARCHAR(50) | | 邮箱 |
| last_login_time | DATETIME | | 最后登录的时间 |
| login_count | INT(11) | DEFAULT 0 | 登录次数 |
| last_login_ip | VARCHAR(100) | | 最后登录的IP |
| super_flag | TINYINT(1) | DEFAULT 0 | 是否为超级管理员 |
| create_date | DATETIME | | 创建时间 |
| update_date | DATETIME | | 更新时间 |
##### 2.1.2 系统角色表(system_role)
| 字段名 | 类型 | KEY | 默认值 / 描述 |
| :---------- | :----------- | :---------- | :------------ |
| id | INT | PRIMARY KEY | 主键 |
| role_name | VARCHAR(100) | NOT NULL | 角色名称 |
| remark | VARCHAR(100) | NOT NULL | 备注 |
| create_date | DATETIME | | 创建时间 |
| update_date | DATETIME | | 更新时间 |
##### 2.1.3 系统菜单表(system_menu)
| 字段名 | 类型 | KEY | 默认值 / 描述 |
| :---------- | :----------- | :---------- | :----------------------------- |
| id | INT | PRIMARY KEY | 主键 |
| name | VARCHAR(100) | NOT NULL | 菜单名称 |
| url | VARCHAR(100) | NOT NULL | 菜单地址 |
| permission | VARCHAR(100) | NOT NULL | 权限标识 |
| icon | VARCHAR(100) | | 菜单图标 |
| parent_id | INT | DEFAULT 0 | 父ID |
| sort | INT | DEFAULT 0 | 排序 |
| type | TINYINT(2) | | 菜单类型:0 目录 1 菜单 2 按钮 |
| create_date | DATETIME | | 创建时间 |
| update_date | DATETIME | | 更新时间 |
##### 2.1.4 系统角色关联表(system_admin_role)
| 字段名 | 类型 | KEY | 默认值 / 描述 |
| :------- | :--- | :---------- | :------------ |
| id | INT | PRIMARY KEY | 主键 |
| role_id | INT | NOT NULL | 角色ID |
| admin_id | INT | NOT NULL | 用户ID |
##### 2.1.5 角色权限关联表(system_role_menu)
| 字段名 | 类型 | KEY | 默认值 / 描述 |
| :------ | :--- | :---------- | :------------ |
| id | INT | PRIMARY KEY | 主键 |
| role_id | INT | NOT NULL | 角色ID |
| menu_id | INT | NOT NULL | 菜单ID |
##### 2.1.6 系统操作日志表(system_log)
| 字段名 | 类型 | KEY | 默认值 / 描述 |
| :------------- | :----------- | :---------- | :--------------------------- |
| id | INT | PRIMARY KEY | 主键 |
| operation_name | VARCHAR(255) | | 操作详情 |
| request_url | VARCHAR(255) | NOT NULL | 请求接口 |
| method | VARCHAR(100) | NOT NULL | 请求方式 |
| request_time | VARCHAR(100) | | 接口请求时间 |
| user_id | INT(11) | | 用户ID(前台或后台用户) |
| params | text | | 接口请求参数 |
| exception | text | | 接口请求异常信息 |
| platform_type | TINYINT(2) | DEFAULT 1 | 类型(1 系统后台 2 web前端) |
#### 2.2 SQL语句
```sql
DROP DATABASE IF EXISTS `education`;
CREATE DATABASE `education` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `education`;
-- ----------------------------
-- 系统管理员表
-- ----------------------------
DROP TABLE IF EXISTS `system_admin`;
CREATE TABLE `system_admin` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`login_name` varchar(100) NOT NULL COMMENT '登录名',
`name` varchar(100) NULL COMMENT '真实姓名',
`password` varchar(100) NOT NULL COMMENT '密码',
`encrypt` varchar(100) NOT NULL COMMENT 'MD5盐值',
`mobile` varchar(20) NOT NULL COMMENT '手机号',
`disabled_flag` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 是 0 否',
`mail` varchar(50) NULL COMMENT '邮箱',
`last_login_time` datetime(0) NULL COMMENT '最后登录的时间',
`login_count` int NULL DEFAULT 0 COMMENT '登录次数',
`last_login_ip` varchar(100) NULL COMMENT '最后登录的IP',
`super_flag` tinyint(1) NULL DEFAULT 0 COMMENT '是否为超级管理员',
`create_date` datetime(0) NULL COMMENT '创建时间',
`update_date` datetime(0) NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) AUTO_INCREMENT = 1000 COMMENT = '系统管理员表';
-- ----------------------------
-- 系统角色关联表
-- ----------------------------
DROP TABLE IF EXISTS `system_admin_role`;
CREATE TABLE `system_admin_role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` int NOT NULL COMMENT '角色ID',
`admin_id` int NOT NULL COMMENT '用户ID',
PRIMARY KEY (`id`) USING BTREE
) AUTO_INCREMENT = 1000 COMMENT = '系统角色关联表';
-- ----------------------------
-- 系统操作日志表
-- ----------------------------
DROP TABLE IF EXISTS `system_log`;
CREATE TABLE `system_log` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`operation_name` varchar(255) NULL COMMENT '操作详情',
`request_url` varchar(255) NOT NULL COMMENT '请求接口',
`method` varchar(100) NOT NULL COMMENT '请求方式',
`request_time` datetime(0) NULL COMMENT '接口请求时间',
`user_id` int NULL COMMENT '用户ID(前台或后台用户)',
`params` text NULL COMMENT '接口请求参数',
`exception` text NULL COMMENT '接口请求异常信息',
`platform_type` tinyint NULL DEFAULT 1 COMMENT '类型(1 系统后台 2 web前端)',
PRIMARY KEY (`id`) USING BTREE
) AUTO_INCREMENT = 1000 COMMENT = '系统操作日志表';
-- ----------------------------
-- 系统菜单表
-- ----------------------------
DROP TABLE IF EXISTS `system_menu`;
CREATE TABLE `system_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(100) NOT NULL COMMENT '菜单名称',
`url` varchar(100) NOT NULL COMMENT '菜单地址',
`permission` varchar(100) NOT NULL COMMENT '权限标识',
`icon` varchar(100) NULL COMMENT '菜单图标',
`parent_id` int NULL DEFAULT 0 COMMENT '父ID',
`sort` int NULL DEFAULT 0 COMMENT '排序',
`type` tinyint NULL COMMENT '菜单类型:0 目录 1 菜单 2 按钮',
`create_date` datetime(0) NULL COMMENT '创建时间',
`update_date` datetime(0) NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) AUTO_INCREMENT = 1000 COMMENT = '系统菜单表';
-- ----------------------------
-- 系统角色表
-- ----------------------------
DROP TABLE IF EXISTS `system_role`;
CREATE TABLE `system_role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_name` varchar(100) NOT NULL COMMENT '角色名称',
`remark` varchar(100) NOT NULL COMMENT '备注',
`create_date` datetime(0) NULL COMMENT '创建时间',
`update_date` datetime(0) NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) AUTO_INCREMENT = 1000 COMMENT = '系统角色表';
-- ----------------------------
-- 角色权限关联表
-- ----------------------------
DROP TABLE IF EXISTS `system_role_menu`;
CREATE TABLE `system_role_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` int NOT NULL COMMENT '角色ID',
`menu_id` int NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`) USING BTREE
) AUTO_INCREMENT = 1000 COMMENT = '角色权限关联表';
```
#### 2.3 RBAC权限系统登录流程
##### 2.3.1 前后端分离用户登录流程

#### 2.4 JWT介绍及其使用
##### 2.4.1 JWT简介
JWT:Json Web Token,是基于json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
##### 2.4.2 Json Web Token的组成
**(1)Header**
头信息通常包含两部分,type:代表token的类型,这里使用的是JWT类型。alg:使用的Hash算法,例如HMAC SHA256或RSA。
例如:`{: "alg HS256", : "typ JWT" }`
**(2)Payload**
荷载信息,它包含一些声明Claim(实体的描述,通常是一个User信息,还包括一些其他的元数据)
**(3)signature**
签证信息,需要使用编码后的`header`和`payload`以及我们提供的一个 密钥,然后使用`header`中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过。
##### 2.4.3 项目实践
(1)在父工程中引入依赖
```xml
io.jsonwebtoken
jjwt
0.9.1
com.alibaba
fastjson
1.2.53
```
(2)在`common`模块下创建包`model`,然后在包下创建`JwtToken`类
```java
package com.education.common.model;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.util.JSONPObject;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.util.Date;
public class JwtToken {
private final String secret;
private static final Logger logger = LoggerFactory.getLogger(JwtToken.class);
public JwtToken(String secret) {
if (secret == null) {
throw new NullPointerException("secret value cant not ben null");
}
this.secret = secret;
}
/**
* 生成JWT token
* @param value
* @param expirationTime
* @return
*/
public String createToken(Object value, long expirationTime) {
// 生成SecretKey对象
SecretKey secretKey = this.createSecretKey();
String jsonString = JSONObject.toJSONString(value);
SignatureAlgorithm hs256 = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date();
JwtBuilder jwtBuilder = Jwts.builder().setIssuedAt(now).setSubject(jsonString).signWith(hs256, secretKey);
if (expirationTime > 0L) {
long expMills = nowMillis + expirationTime;
Date exp = new Date(expMills);
// 设置token过期时间
jwtBuilder.setExpiration(exp);
}
return jwtBuilder.compact();
}
private SecretKey createSecretKey() {
byte[] bytes = DatatypeConverter.parseBase64Binary(secret);
return new SecretKeySpec(bytes, 0, secret.length(), "AES");
}
public T parserToken(String token, Class clazz) {
SecretKey secretKey = this.createSecretKey();
try {
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJwt(token).getBody();
String subject = claims.getSubject();
return (T) JSONObject.parseObject(subject);
} catch (SignatureException | MalformedJwtException e) {
// token签名失败
logger.error("token 签名失败", e);
} catch (ExpiredJwtException e) {
// token已过期
logger.error("token 已过期", e);
} catch (Exception e) {
logger.error("token 验证异常", e);
}
return null;
}
}
```
(3)编写单元测试方法
```java
@Test
public void testToken() throws InterruptedException {
JwtToken jwtToken = new JwtToken("education");
Map params = new HashMap<>();
params.put("id", 1);
params.put("name", "我要自学网");
String token = jwtToken.createToken(params, 2000);
System.out.println(token);
Thread.sleep(6000);
Map map = jwtToken.parserToken(token, Map.class);
System.out.println(map);
}
```
(4)测试运行,发现报错

(5)检查修正

(6)测试运行,发现能正常生成token,但解析时抛出了一个异常

(7)检查修正

(8)测试运行,运行正常

(9)修改有效时间为3秒,模拟token过期

(10)测试运行

### 3、SpringBoot整合MyBatis
#### 3.1 引入依赖
在父工程中引入以下依赖
```xml
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.1
mysql
mysql-connector-java
8.0.13
```
#### 3.2 配置数据库连接池
在`admin-api`模块的配置文件中配置数据库连接池
```yaml
spring:
datasource:
url: jdbc:mysql://192.168.155.128:3306/education?serverTimezone=GMT%2B8&useSSL=true&useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
hikari:
# 数据库连接超时时间
connection-timeout: 30000
# 连接池最大连接数,默认是10
maximum-pool-size: 30
# 最小空闲连接数量
minimum-idle: 10
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期
max-lifetime: 6000
```
#### 3.3 MyBatis相关配置
##### 3.3.1 创建`mapper`模块
(1)创建`mapper`模块

(2)修改模块`pom.xml`

(3)在`service`模块的`pom`文件中加入`mapper`依赖
```xml
com.education.mapper
education-mapper
1.0-SNAPSHOT
```
##### 3.3.2 创建Mapper接口
(1)在`mapper`模块中创建基类接口`BaseMapper`
```java
package com.education.mapper;
import java.util.List;
import java.util.Map;
public interface BaseMapper {
/**
* 添加单条数据
* @param params
* @return
*/
int save(Map params);
/**
* 修改数据
* @param params
* @return
*/
int update(Map params);
/**
* 批量添加数据
* @param params
* @return
*/
int batchSave(Map params);
/**
* 批量添加
* @param params
* @return
*/
int batchUpdate(Map params);
/**
* 批量修改
* @param params
* @return
*/
List