智慧云教育平台实战项目笔记

智慧云智慧教育平台实战项目笔记

一、简介

课程内容:智慧云教育平台管理后台、智慧云教育平台学生端、项目的正式部署

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提供了标准的软件生命周期模型和构建模型,通过配置就能对项目进行全面的管理。想了解更多点击这里

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-INFweb应用文件目录,web项目的信息,比如存放web.xml、本地图片、jsp视图页面
${basedir}/target打包输出目录
${basedir}/target/classes编译输出目录
${basedir}/target/test-classes测试编译输出目录
Test.javaMaven只会自动运行符合该命名规则的测试类
~/.m2/repositoryMaven默认的本地仓库目录位置

4、Maven下载

Maven 下载地址:http://maven.apache.org/download.cgi

在这里插入图片描述

不同平台下载对应的包:

系统包名
Windowsapache-maven-3.3.9-bin.zip
Linuxapache-maven-3.3.9-bin.tar.gz
Macapache-maven-3.3.9-bin.tar.gz

下载包后解压到对应目录:

系统存储位置 (可根据自己情况配置)
WindowsD:\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
系统变量MAVEN_HOME
编辑系统变量 Path,添加变量值:;%MAVEN_HOME%\bin
编辑系统变量Path

**注意:**注意多个值之间需要有分号隔开,然后点击确定。

6、修改Maven配置文件

打开E:\Maven\apache-maven-3.3.9\conf目录下的setting.xml文件

修改本地仓库路径:

<localRepository>D:\Apps\Maven-3.8.1\repository</localRepository>

修改镜像源为阿里云

    <mirror>
      <id>nexus-aliyun</id>
      <name>Nexus aliyun</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public</url>
      <mirrorOf>central</mirrorOf>
    </mirror>

修改编译器JDK版本为1.8

    <profile>
      <id>jdk-1.8</id>
      <activation>
        <activeByDefault>true</activeByDefault>
        <jdk>1.8</jdk>
      </activation>
      <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
      </properties>
    </profile>

7、pom.xml文件中常见的标签使用

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- 描述这个POM文件遵从哪个版本的项目描述符 -->
    <modelVersion>4.0.0</modelVersion>

    <!-- 项目组织的唯一标识 -->
    <groupId>cn.jasondom.springboot</groupId>
    <!-- 项目唯一标识 -->
    <artifactId>Template</artifactId>
    <!-- 项目版本号 -->
    <version>1.0.0</version>
    <!-- 项目打包类型jar、war、pom -->
    <packaging>jar</packaging>

    <!-- 项目名称 -->
    <name>SpringBoot</name>
    <!-- 项目介绍 -->
    <description>SpringBoot is a lightweight Java Web Framework</description>
    <!-- 项目地址 -->
    <url>http://maven.apache.org</url>

    <!-- 定义变量,通常用于定义依赖版本 -->
    <properties>
        <java.version>1.8</java.version>
        <velocity.version>2.2.3</velocity.version>
    </properties>

    <!--jar包依赖列表-->
    <dependencies>
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity</artifactId>
            <version>${velocity.version}</version>
            <!-- 
                当前依赖的作用范围:
                    compile:当前依赖参与所有
                    runtime:当前依赖仅参与项目的运行阶段
                   provided:与compile类似,区别在于不参与项目的最终打包
                     system:从本地磁盘中引用一个jar包(需要添加一个systemPath标签,用于指明jar包路径)
                       test:当前依赖仅参与项目的单元测试
             -->
            <scope></scope>

            <!-- 排除jar包依赖列表 -->
            <exclusions>
                <!-- 排除的jar包1 -->
                <exclusion>
                    <groupId>...</groupId>
                    <artifactId>...</artifactId>
                </exclusion>
                <!-- 排除的jar包2 -->
                <exclusion>
                    <groupId>...</groupId>
                    <artifactId>...</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <!-- 指定继承父项目的标签 -->
    <parent>
        <groupId>...</groupId>
        <artifactId>...</artifactId>
        <version>...</version>
        <!-- 指定父工程的pom.xml路径,此标签默认值为../pom.xml -->
        <relativePath/>
    </parent>

    <!-- 指定子模块 -->
    <modules>
        <module>子项目1目录路径</module>
        <module>子项目2目录路径</module>
        <module>...</module>
    </modules>

    <!-- 编译配置 -->
    <build>
        <!--插件列表-->
        <plugins>
            <plugin>
                <groupId></groupId>
                <artifactId></artifactId>
            </plugin>
        </plugins>
    </build>

</project>

8、多模块开发的好处

  • 降低项目复杂性,提升我们的开发效率
  • 有利于项目遵从高内聚,低耦合的设计模式,保证了代码的质量和健壮性
  • 避免重复造轮子,减少工作量

9、IDEA上配置Maven

File中打开Settings...,搜索Maven,根据安装目录修改配置
IDEA配置Maven

注意:建议使用Maven-3.5,如果版本过高可能与IDEA不兼容

为了避免所有新建项目都要修改Maven配置,可以打开File -> Other Settings -> Settings for New Project...重新设置一次Maven
修改新建项目的Maven配置

三、后端项目环境的搭建

1、创建Maven类型的父工程

在这里插入图片描述
在这里插入图片描述
接下来点击Next -> Finish完成创建,等项目构建完成后删除src加粗样式目录

2、创建SpringBoot类型的管理模块

(1)、在工程目录上右键单击,选择New -> Module...
在这里插入图片描述
(2)、设置模块名称
在这里插入图片描述
(3)、选择基本依赖
在这里插入图片描述

(4)、点击Next -> Finish完成创建,将管理模块的SpringBoot父依赖剪切至父工程中,并将版本改为2.1.9.RELEASE
在这里插入图片描述

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.9.RELEASE</version>
    <relativePath/>
  </parent>

(5)、将管理模块的parent标签修改成以下内容,使其依赖于父工程

    <parent>
        <groupId>com.education</groupId>
        <artifactId>education</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath/>
    </parent>

(6)、剪切管理模块的依赖列表至父工程

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

(7)、将JDK版本改为1.8

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

3、创建Maven类型的service模块

(1)、在工程目录上右键单击,选择New -> Module...
创建模块
(2)、设置模块名称(将GroupId设置为com.education.service
在这里插入图片描述
(3)、点击Next -> Finish完成创建,打开service模块下的pom.xml文件,修改groupId和父工程为如下内容

  <parent>
    <groupId>com.education</groupId>
    <artifactId>education</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath/>
  </parent>

  <groupId>com.education.service</groupId>
  <artifactId>education-service</artifactId>
  <version>1.0-SNAPSHOT</version>

4、创建common工具模块

(1)、在工程目录上右键单击,选择New -> Module...
创建模块
(2)、设置模块名称(将GroupId设置为com.education.common
在这里插入图片描述
(3)、点击Next -> Finish完成创建,打开common模块下的pom.xml文件,修改groupId和父工程为如下内容

  <parent>
    <groupId>com.education</groupId>
    <artifactId>education</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath/>
  </parent>

  <groupId>com.education.common</groupId>
  <artifactId>education-common</artifactId>
  <version>1.0-SNAPSHOT</version>

5、修改依赖关系

(1)、添加模块到父工程,在父工程的pom.xml文件中添加如下内容

  <modules>
    <module>education-admin-api</module>
    <module>education-service</module>
    <module>education-common</module>
  </modules>

(2)、在admin-api模块中添加依赖

<dependency>
    <groupId>com.education.common</groupId>
    <artifactId>education-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
    <groupId>com.education.service</groupId>
    <artifactId>education-service</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

(3)在service模块中添加依赖

<dependency>
    <groupId>com.education.common</groupId>
    <artifactId>education-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

6、测试环境

(1)、在管理模块src/main/resources/目录下创建如下三个文件(删除默认的application.properties文件)

配置文件名说明
application.yml默认环境配置文件
application-dev.yml开发环境配置文件
application-prod.yml生产环境配置文件

(2)、在application.yml中添加如下配置,激活开发环境配置文件

spring:
  profiles:
    active: dev

(3)、在application-dev.yml中添加如下配置,设置服务器访问端口号

server:
  port: 80

(4)、在com/education/admin/api/EducationAdminApiApplication.java入口类中编写测试方法test

@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

2.2 在IDEA上配置Git

File中打开Settings...,搜索Git,根据安装目录修改配置

在这里插入图片描述

3、将代码上传至码云仓库

3.2 项目码云步骤
3.1 IDEA中将代码上传至码云仓库

1)选择Create Git Repository...,在弹出的对话框中选择项目路径
在这里插入图片描述
2)将代码提交至本地仓库

提交代码到本地仓库
选中要提交的文件,输入提交信息,点击Commit即可提交到本地仓库(只提交源码和配置文件,.idea.mvn和编译等文件不用提交)
在这里插入图片描述
3)将项目提交至远程仓库
将项目提交至远程仓库
点击Define remote,输入远程仓库的地址点击OK,选择Push即可提交代码至远程仓库
在这里插入图片描述

4、代码的更新

(1)、可以直接在码云线上修改代码
在这里插入图片描述
(2)添加一个测试方法,并提交
提交测试
(3)按图示操作,即可更新代码到本地(有的IDEA有多个选项,可以选择MergeBranch 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 直接提交分支
image-20211115192412145 image-20211115191254754

(2)接下来就可以在码云平台看到新添加的dev分支了
image-20211115191658804

7.2 切换分支

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

7.3 分支代码的合并

(1)先切换到dev分支

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

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

(4)切换到master分支
image-20211115192221846

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

(7)按快捷键Ctrl + Shift + K 直接提交

7.4 在dev上检出一个bug分支

(1)先切换到dev分支

(2)新建一个bug分支
image-20211115192412145 image-20211115200719766

(3)在SpringBoot入口类中添加test1方法
image-20211115201211364

(4)提交(Ctrl + K
image-20211115195758404

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

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

(7)最后切换到master分支,按快捷键Ctrl + Shift + K 提交

7.5 使用码云创建分支

(1)登录码云,进入要创建分支的仓库,打开分支管理
image-20211115204158820

(2)创建feature分支
image-20211115204957733

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

8、码云平台相关功能

可以为项目添加合作伙伴,按不同的职责分配不同的角色权限
image-20211115205916668

五、项目的前期准备

1、Map和传统JavaBean技术选型

1.1 Map的优缺点

优点:

  • 灵活性强于JavaBean,易扩展,耦合度低。
  • 写起来简单,代码量少
  • MyBatis查询的返回结果本身就是Map

缺点:

  • 不能一眼看出Map中有哪些参数
1.2 JavaBean的优缺点

优点:

  • 符合Java语言面向对象设计的原则
  • 数据结构清晰,便于团队开发和后期维护

缺点:

  • 需要不断地去维护实体类
1.3 如何选型呢?
  • 团队人数少,追求开发效率,建议使用Map代替实体类
  • 项目庞大,需要持续维护,团队人数多,建议使用实体类

2、响应结果封装及全局异常处理

2.1 MVC概念

image-20211115215325947

2.2 MVC流程

image-20211115215427938

2.3 前后端分离

image-20211115215507386

2.4 前后端分离的优缺点

优点

  • 分工更明确,提升各自领域专注度
  • 前后端代码完全分离,有利于项目维护

缺点:

  • 需要不断地维护接口文档
  • 沟通成本更高
  • 部署相比较MVC架构要复杂点
2.5 后端接口统一json数据格式

接口返回成功示例:

{
	"code": 1,
	"message": "请求成功",
	"data": {
		"user_info": {
			"name": "张三",
			"address": "北京市xx区"
		}
	}
}

接口请求失败示例:

{
	"code": 0,
	"message": "系统异常"
}
2.6 响应结果封装

(1)给父工程添加以下两个依赖

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
</dependency>

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-core</artifactId>
</dependency>

(2)在common模块下创建utils包,用于放置系统中的工具类
image-20211115221535456

(3)创建ResultCode类和Result

package com.education.common.utils;

/**
 * http 响应状态码
 *  SUCCESS: 响应成功状态码
 *  FAIL: 响应失败状态码
 * @author Jason
 */
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;
    }
}
package com.education.common.utils;

/**
 * 对请求结果的封装
 * @author Jason
 */
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> 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
image-20211116000153974

(2)在exception包下创建系统业务异常类BusinessException和全局异常处理类SystemExceptionHandler

package com.education.common.exception;

import com.education.common.utils.ResultCode;

/**
 * 系统业务异常类
 * @author Jason
 */
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;
    }
}
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;

/**
 * 处理全局异常
 * @author Jason
 */
@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 线程池的执行原理

image-20211116003204352

3.4 线程池的销毁
//等待所有正在执行的任务全试图停止所有正在执行
.shutdoen();
// 部执行完毕之后才会销毁的线程
.shutdownNow();
3.5 线程池的创建方法

Executors创建线程池

方法名功能
newFixedThreadPool(int nThreads)创建固定大小的线程池
newSingleThreadExcutor()创建只有一个线程的线程池
newCachedThreadPool()创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

ThreadPoolExecutor

// Java 线程池的完整构造函数
public ThreadPoolExecutor (
    int corePoolSize,    // 线程池长期维持的线程数,即使线程处于idle状态,也不会回收
    int maximumPoolSize, // 线程数的上限
    long keepAliveTiem, TimeUnit unit,  // 超过corePoolSize的线程的idle时长
                                        // 超过这个时间,多余的线程会被回收
    BlookingQueue<Runnable> workQueue,  // 任务的排队队列
    ThreadFactory threadFactory,        // 新线程的产生方式
    RejectedExecutionHandler handler)   // 拒绝策略
3.6 Spring与线程池的整合
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 线程资源复用示例
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循环体代码:

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,当并发量超过线程数和队列容量的总和时,就会抛出异常
image-20211116134058883

(3)可以通过setRejectedExecutionHandler()方法设置拒绝策略,在for循环上方添加以下语句:

threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

(4)重新运行,会发现所有任务全部执行完成
image-20211116140235522

(5)修改拒绝策略

threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

(6)重新运行,会发现有部分任务没有执行,并且也没有抛出异常
image-20211116140507909

4.3 线程池在实际项目中的使用

(1) 在com.education.common.utils包下创建SpringBeanManager

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 实例工具类
 * @author Jason
 */
@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> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return (T) applicationContext.getBean(clazz);
    }
}

(2) 在service模块创建com.education.service.task包,并创建TaskListener接口和TaskManager

package com.education.service.task;

/**
 * 任务监听器
 * @author Jason
 */
public interface TaskListener {

    void onMessage(TaskParam taskParam);
}
package com.education.service.task;

import com.education.common.utils.SpringBeanManager;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 任务管理器
 * @author Jason
 */
public class TaskManager {

    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    private final Map<String, TaskListener> taskListenerMap = new ConcurrentHashMap<>();

    public TaskManager(ThreadPoolTaskExecutor threadPoolTaskExecutor) {
        this.threadPoolTaskExecutor = threadPoolTaskExecutor;
        this.registerTaskListener();
    }

    private void registerTaskListener() {
        ApplicationContext applicationContext = SpringBeanManager.getApplicationContext();
        String[] beanNames = applicationContext.getBeanNamesForType(TaskListener.class);
        for (String name: beanNames) {
            TaskListener taskListener = SpringBeanManager.getBean(name);
            taskListenerMap.put(name, taskListener);
        }
    }

    public void pushTask(TaskParam taskParam) {
        String beanName = taskParam.getTaskListenerBeanName();
        TaskListener taskListener = taskListenerMap.get(beanName);
        if (taskListener != null) {
            threadPoolTaskExecutor.execute(() -> {
                taskListener.onMessage(taskParam);
            });
        }

    }
}

(3)封装任务参数类TaskParam

package com.education.service.task;

/**
 * 封装任务参数类
 * @author Jason
 */
public class TaskParam {

    private String taskListenerBeanName;

    private final long timestamp;

    private Object data;

    public TaskParam() {
        this.timestamp = 0L;
    }

    public TaskParam(String taskListenerBeanName, long timestamp, Object data) {
        this.taskListenerBeanName = taskListenerBeanName;
        this.timestamp = timestamp;
        this.data = data;
    }

    public String getTaskListenerBeanName() {
        return taskListenerBeanName;
    }

    public void setTaskListenerBeanName(String taskListenerBeanName) {
        this.taskListenerBeanName = taskListenerBeanName;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public <T> T getData() {
        return (T) data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

(4)创建配置类BeanConfig

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;

/**
 * 配置线程池
 * @author Jason
 */
@Configuration
public class BeanConfig {

    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接口的实现类LogTaskListener

package com.education.service.task;

import org.springframework.stereotype.Component;

/**
 * @author Jason
 */
@Component
public class LogTaskListener implements TaskListener{

    @Override
    public void onMessage(TaskParam taskParam) {
        System.out.println("执行LogTaskListener:" + taskParam.getData());
    }
}

(6)在admin-api模块下com.education.admin.api.App中编写测试代码

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;

/**
 * @author Jason
 * 注解@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的实例,由七个子表达式组成,用来描述详细的时间信息

格式 [秒] [分] [时] [日] [月] [周] [年]
5.1.6 配置Cron表达式

Cron表达式的格式:秒 分 时 日 月 周 年(可选)

字段名允许的值允许的特殊字符
0—59, - * /
0—59, - * /
0—23, - * /
1—31, - * ?/LWC
1—12 或 JAN—DEC, - * /
1—7 或 SUN—SAT, - * ?/LC#
年(可选字段)empty1970—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起步依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

(1)创建任务测试类
image-20211116171704588

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

package com.education.service.job;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
 * @author Jason
 */
public abstract class BaseJob extends QuartzJobBean {}

(2)创建系统任务类SystemJob

package com.education.service.job;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author Jason
 */
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

package com.education.service.job;

import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Jason
 */
@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入口类

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;

/**
 * @author Jason
 * 注解@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函数,会发现报错
image-20211116180616389

(6)在JobBeanConfigsystemJob方法中添加storeDurably()方法
image-20211116185709622

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

六、RBAC权限管理

1、RBAC简介

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

image-20211116200612169

1.3 什么是权限?

权限是资源的集合,主要包括菜单、页面、字段、操作功能(增删改查)等等

  • 页面权限

  • 操作权限

  • 数据权限

1.4 为什么要使用权限?
  • 使用者的角度

    在限制范围内正确的使用权力

  • 设计者角度来说

    保证系统更加安全:控制不同的角色合理的访问不同的资源

1.5 RBAC中的功能模块

image-20211116201025255

2、RBAC权限系统数据库设计

2.1 系统(System)
2.1.1 系统管理员表(system_admin)
字段名类型KEY默认值 / 描述
idINTPRIMARY KEY主键
login_nameVARCHAR(100)NOT NULL登录名
nameVARCHAR(100)真实姓名
passwordVARCHAR(100)NOT NULL密码
encryptVARCHAR(100)NOT NULLMD5盐值
mobileVARCHAR(20)NOT NULL手机号
disabled_flagTINYINT(1)DEFAULT 01 是 0 否
mailVARCHAR(50)邮箱
last_login_timeDATETIME最后登录的时间
login_countINT(11)DEFAULT 0登录次数
last_login_ipVARCHAR(100)最后登录的IP
super_flagTINYINT(1)DEFAULT 0是否为超级管理员
create_dateDATETIME创建时间
update_dateDATETIME更新时间
2.1.2 系统角色表(system_role)
字段名类型KEY默认值 / 描述
idINTPRIMARY KEY主键
role_nameVARCHAR(100)NOT NULL角色名称
remarkVARCHAR(100)NOT NULL备注
create_dateDATETIME创建时间
update_dateDATETIME更新时间
2.1.3 系统菜单表(system_menu)
字段名类型KEY默认值 / 描述
idINTPRIMARY KEY主键
nameVARCHAR(100)NOT NULL菜单名称
urlVARCHAR(100)NOT NULL菜单地址
permissionVARCHAR(100)NOT NULL权限标识
iconVARCHAR(100)菜单图标
parent_idINTDEFAULT 0父ID
sortINTDEFAULT 0排序
typeTINYINT(2)菜单类型:0 目录 1 菜单 2 按钮
create_dateDATETIME创建时间
update_dateDATETIME更新时间
2.1.4 系统角色关联表(system_admin_role)
字段名类型KEY默认值 / 描述
idINTPRIMARY KEY主键
role_idINTNOT NULL角色ID
admin_idINTNOT NULL用户ID
2.1.5 角色权限关联表(system_role_menu)
字段名类型KEY默认值 / 描述
idINTPRIMARY KEY主键
role_idINTNOT NULL角色ID
menu_idINTNOT NULL菜单ID
2.1.6 地区表(system_region)
2.1.7 系统操作日志表(system_log)
字段名类型KEY默认值 / 描述
idINTPRIMARY KEY主键
operation_nameVARCHAR(255)操作详情
request_urlVARCHAR(255)NOT NULL请求接口
methodVARCHAR(100)NOT NULL请求方式
request_timeVARCHAR(100)接口请求时间
user_idINT(11)用户ID(前台或后台用户)
paramstext接口请求参数
exceptiontext接口请求异常信息
platform_typeTINYINT(2)DEFAULT 1类型(1 系统后台 2 web前端)
2.1.8 数据字典表(system_dict)
2.2 教学(education)
2.2.1 试题信息表(question_info)
2.2.1 试卷试题关联表(test_paper_question_info)
2.2.2 科目信息表(subject_info)
2.2.3 学校信息表(school_info)
2.2.4 试卷信息表(test_paper_info)
2.2.5 充值卡信息表(refillable_card)
2.2.6 用户答题记录表(user_question_answer)
2.2.7 课程信息表(course_info)
2.2.8 考试记录表(exam_info)
2.3 用户(user_info)
2.4 RBAC权限系统登录流程
2.4.1 前后端分离用户登录流程

image-20211116210638582

2.5 JWP介绍及其使用
2.5.1 JWT简介

JWT:Json Web Token,是基于json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

2.5.2 Json Web Token的组成

(1)Header

头信息通常包含两部分,type:代表token的类型,这里使用的是JWT类型。alg:使用的Hash算法,例如HMAC SHA256或RSA。

例如:{: "alg HS256", : "typ JWT" }

(2)Payload

荷载信息,它包含一些声明Claim(实体的描述,通常是一个User信息,还包括一些其他的元数据)

(3)signature

签证信息,需要使用编码后的headerpayload以及我们提供的一个 密钥,然后使用header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过。

2.5.3 项目实践

(1)在父工程中引入依赖

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.53</version>
</dependency>

(2)在common模块下创建包model,然后在包下创建JwtToken

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;

/**
 * @author Jason
 */
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> T parserToken(String token, Class<T> 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)编写单元测试方法

@Test
public void testToken() throws InterruptedException {
    JwtToken jwtToken = new JwtToken("education");
    Map<String, Object> params = new HashMap<>();
    params.put("id", 1);
    params.put("name", "我要自学网");
    String token = jwtToken.createToken(params, 2000);
    System.out.println(token);
    Thread.sleep(6000);

    Map<String, Object> map = jwtToken.parserToken(token, Map.class);
    System.out.println(map);
}

(4)测试运行,发现报错
image-20211116221621832

(5)检查修正
image-20211116221820917

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

(7)检查修正
image-20211116230853047

(8)测试运行,运行正常
image-20211116231037725

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

(10)测试运行
image-20211116231405866

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
SpringBoot项目实战笔记可以按照以下步骤进行: 1. 首先,你可以通过观看B站上的教程视频来学习SpringBoot项目实战。在视频中,你可以学习到如何使用SpringBoot、MyBatis和MySQL创建一个电脑商城项目。 2. 确保你的SpringBoot项目能够成功启动。找到被@SpringBootApplication注解修饰的入口启动类,并运行该类。如果你能够观察到图形化的界面,那么说明你的项目成功启动了。 3. 如果你还没有创建SpringBoot项目,你可以使用Spring Initializr来初始化一个新的项目。Spring Initializr是一个Web应用程序,可以为你生成Spring Boot项目的基本结构。你可以选择使用Maven或Gradle作为构建工具,并添加适合你的项目的依赖。然后,你只需要编写应用程序的代码即可。 希望以上信息对你有帮助!如果还有其他问题,请随时提问。123 #### 引用[.reference_title] - *1* *2* [SpringBoot项目实战笔记:电脑商城项目实战(SpringBoot+MyBatis+MySQL)](https://blog.csdn.net/weixin_44260350/article/details/127746667)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *3* [《SpringBoot实战》读书笔记](https://blog.csdn.net/sanhewuyang/article/details/104494202)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值