📕开源风云系列
- 🍊本系列将从开源名将若依出发,探究优质开源项目脚手架汲取编程之道。
- 🍉从不分离版本开写到前后端分离版,再到微服务版本,乃至其中好玩的一系列增强Plus操作。
- 🍈希望你具备如下技术栈 :
- 🍎Spring
- 🍍SpringMVC
- 🍐Mybatis/Mybatis-plus
- 🍅Thymeleaf
- 🥝SpringBoot
- 🍓Shiro
- 🍏SpringSecurity
- 🍌SpringCloud
- 🍒云服务器相关知识
- 本篇是分离版第三篇
- 文章同时同步到我的个人站点🍊欢迎来访! :
目录
1、分页
[!NOTE]
分页主要是后端做,前端只需要传给后端,我要第几页,每页多少条数据。
1.1、前端封装分页
以操作日志页面为例子,前端每次给后端发请求的时候,都是带了分页参数的,默认都是请求第一页,每页10条数据。
我们请求后端接口的时候会带上queryParams
对象,我们进入日志页面,就会执行getList
方法,进而执行list
方法发送请求,发送请求的时候会带上queryParams、dateRange 参数,这个dateRange参数是日期范围,也就是前端搜索框筛选日期的参数,为什么这个日期范围参数不放在 queryParams 对象里面一起传参进去呢?
其实 dateRange 日期范围是在 data 里面,和 element ui 的日期选择器进行了 v-model 双向绑定
若依对 element ui 的分页进行了封装,包括分页的页码数等等,可以结合 element ui 的 Pagination 分页的参数进行更改。
[!NOTE]
前端只需要维护好数据,我需要当前是第几页,一共有几条数据,每页多少条。
前端只需要维护好这三个数据,其他的交给后端就行了。
1.2、后端分页方式
后端是通过pagehelper插件实现的。ry通过pagehelper间接引入了mybatis:
[!NOTE]
- 若依在
BaseController
中封装了分页方法。也就是说我们自己写的controller只要继承了BaseController,直接调用startPage方法即可完成分页。- 分页的源码调试讲解:源码解析 很牛,值得一看
我们的Controller继承BaseController,只需要在我们的Service的查询方法之前加上startPage()
就会默认表示sql需要分页。
2、MyBatisPlus整合
关于若依集成MyBatisPlus,我们可以按照步骤如下:
- 在根pom.xml中引入mybatis-plus,在common模块中继承
- 在common模块下删除mybatis冲突依赖
- 填写 yml 配置
yaml
mybatis-plus:
# 配置mapper的扫描,找到所有的mapper.xml映射文件
configLocation: classpath:mybatis/mybatis-config.xml
# 搜索指定包别名
typeAliasesPackage: com.kuang.**.domain
- 删除MyBatisConfig,也就是删除ry自己的配置文件,全部注释掉也可以
然后直接重启项目,成功!
- 为什么要排除pagehelper中的mybatis依赖?
答:因为mybatis-plus的启动器和pagehelper都引入了mybatis,mybatis-plus的启动器的mybatis版本比较高。pagehelper引入的mybatis的版本比较低,mybatis-plus用到了高版本mybatis的方法。所以需要排除pagehelper中的mybatis。否则报错没找到方法。但是我自己的版本如下:
Pagehelper V1.4.7:
- mybatis:3.5.13
- mybatis-spring:2.1.1
mybatis-plus V3.5.3:
- mybatis:3.5.10
- mybatis-spring:2.0.7
也就是其实mybatis-plus用的版本都是低的版本,那么maven会自动将高的版本排除,保留低版本的依赖。所以不需要手动排除,Maven会自动排除。
那么我把低版本的依赖排除,使用高版本的依赖,发现也是OK的。
2.1、MP测试
- 我们测试一下,改造一下登录日志的查询,首先给 mapper 继承
BaseMapper
- 在impl实现类下执行查询
- 访问日志管理会发现报错
控制台打印的SQL语句是:
sql
SELECT info_id,user_name,status,ipaddr,login_location,browser,os,msg,login_time,
search_value,create_by,create_time,update_by,update_time,remark,params
FROM sys_logininfor LIMIT ?
可以发现我们的登录日志数据库表中就没有 search_value
后面的字段了,那么为什么这个sql语句会带呢?
是因为继承关系导致的,SysLogininfor
继承了 BaseEntity
,BaseEntity的属性也被写入了SQL中
所以我们只需要给BaseEntity
的不需要的属性上加上注解@TableField(exist = false)
完成上述操作后,再进行登录日志菜单的点击,就可以正常查询显示了。
3、多数据源
多数据源不是必须的,需求是让一个项目同时连接操作多个数据库。
-
对于一个请求(假如此请求需要查询数据库),我们首先是前端点击,然后前端给后端发请求,后端controller层拿到请求,controller把请求发给service层,service层把数据再递交给mapper层,mapper层在若依中是mybatis接管。
-
当mybatis看到数据库查询请求后,这时候代理模式就开始了,我们的mapper层的接口被代理,框架直接通过反射拿到当前的代理对象、方法、相关参数、DefaultSqlSession去执行相关查询。
-
重点来了,怎么查询?首先要干嘛?当然是得到当前数据库的连接Connection,然后通过此数据库连接进行执行相关查询。
-
我们自己写一个类DynamicDataSource(动态数据源的英文),实现DataSource接口,然后重写getConnection方法,我们可以在DynamicDataSource类中准备好几个连接,然后getConnection方法中根据情况获得不同的连接。
[!NOTE]
总结:如果需要切换多个数据源,只需要把
@DataSource
注解加到方法上或者类上,切面会把注解中的value读出来并设置到线程私有上下文中,然后在getConnection的时候读取线程私有的value来确定用哪个数据源。
我们来玩一下三个MySQL数据源
- 首先在
DruidConfig
中增加一个数据源
- 修改一下数据库的枚举
- 注入三个数据库到动态数据库
java
@Bean
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource,
DataSource slaveDataSource,
DataSource thirdDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
targetDataSources.put(DataSourceType.SLAVE.name(),slaveDataSource);
targetDataSources.put(DataSourceType.THIRD.name(),thirdDataSource);
return new DynamicDataSource(masterDataSource, targetDataSources);
}
- 我们在某个方法上面加上注解
@DataSource(DataSourceType.THIRD)
,就会使用第三个数据库,就完成了切库的操作。
4、事务
若依-Vue中的事务和SpringBoot中完全一致。
在方法或者类上可以假如@Transactional
注解,只要该注解加上,方法中无论有多少个执行SQL的地方,只要执行遇到一个异常,所有的操作都将会回滚。
@Transactional注解默认是捕捉运行时异常,如果需要抓更大的异常,可以设置参数:
java
@Transactional(rollbackFor = Exception.class)
5、操作日志
操作日志是记录用户的某些操作。
为什么要记录呢?
-
可能后续系统出现问题,需要回来找证据的时候会用到。
-
供管理员查看用户的行为。
-
记录正确或者错误记录,也方便程序员修改程序。
总而言之,操作日志有它存在的必要。但是操作日志多少也会消耗一些性能。我们所记录的操作日志,一般都是用户对数据库进行增删改的记录。对于查询,用户并没有更改数据库,所以往往不会去记录。
老生常谈AOP。操作日志是通过下面的类实现的:
切面类会切Log
注解,意思是要某个方法有操作日志,只需要手动加上@Log
注解。IDEA可以帮我们看到我们切了哪个方法。
以新增部门方法为例,处理完请求后执行会拿到切入点、拿到@Log()
注解、拿到请求返回的结果:
以用户修改菜单为例,我们只需要加上@Log(title = "用户管理", businessType = BusinessType.UPDATE)
当我们对用户信息进行修改的时候,就可以在操作日志菜单中看到
6、数据权限
所谓数据权限,就是不同的人对数据的权限不同。举个例子,一个学校的校长能看到学生管理系统的全部学生的信息,进行管理维护;而一个老师只能维护管理系统中自己班的学生,别的人根本看不到。
一般来说,但凡是一个管理类的系统,都有着数据方面的权限。ry中的数据权限是AOP切入,修改SQL实现的,让SQL中拼接一些关于权限的SQL来实现。
可以看到DataScopeAspect
切面类,切的是注解@DataScope
,处理请求前执行会拿到切入点、拿到@DataScope()
注解。
我们来实操一下:
首先随便建立一个表,注意,表字段必须有user_id
,不然怎么和用户关联,不和用户表关联谈什么数据权限?
比如说,有这样一个需求:
- 作为一个高校的研究生,有可能要每周收青年大学习的截图。那么我现在需要学生自己去我的网站上面上传截图,并且用户自己只能看到自己上传的截图,但是管理员可以看到所有的截图。这样一个需求,我们马上安排。
SQL语句如下:
sql
-- 创建kuang_picture表
CREATE TABLE `kuangstudy_vue`.`kuang_picture` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`uri` varchar(500) NULL COMMENT '图片uri',
`user_id` bigint(20) NULL COMMENT '关联用户user_id',
`create_by` varchar(50) NULL COMMENT '创建人',
`create_time` datetime NULL COMMENT '创建时间',
`update_by` varchar(50) NULL COMMENT '更新人',
`update_time` datetime NULL COMMENT '更新时间',
`remark` varchar(500) NULL COMMENT '备注',
PRIMARY KEY (`id`)
);
这里我就直接使用若依的代码生成器来生成页面,只需要注意图片的URI显示类型是图片上传就好。
然后我们使用两个普通用户分别上传图片,发现这俩人竟然可以看到别人的图片。
这就不合理了,我们要自己开始编码,首先要填充 user_id,不然谈什么数据权限?填充之前表里面这么显示的:
我们开始编码填充:
然后重启项目,我们再次使用普通角色上传图片,就会连带着 user_id 显示
那么我们开始进行数据权限的编码,需要在查询的SQL上进行改动,我们对查询图片列表的sql进行改动:
我们给 kuang_picture 、 sys_user 、 sys_dept 三张表分别起别名为 k
、u
、d
,由于 sys_user 中也有 create_by
等字段,所以我们要指定查询的是哪张表的user_id
、create_by
等字段。
所以我们加上查询指定的表名.id、表名.uri等等
最重要的一步:记得在sql最后加上
${params.dataScope}
最后在方法上加上注解@DataScope
,d
表示部门表的别名,u
表示用户表的别名。
这样就实现了我们使用 Gitee 登录的用户上传图片只有 Gitee 登录的用户可以看到, ry 登录的用户上传图片只有 ry 用户可以看到,管理员可以看到所有人的图片。
7、服务监控
从服务监控的前端页面展示看到,有CPU信息、内存信息、服务器信息、Java虚拟机信息和磁盘信息。
后端代码如上,就是调用API封装成Server对象,将信息返回给前端。
7.1、服务监控前端
当打开服务监控的页面时,页面会有一个加载的 loading
可能有小伙伴问,为啥先getList,后loading的,为啥loading还能走到?
因为getList里面有Promise的异步请求操作。异步不会阻塞。此外,getList方法的接口还睡了一秒钟,所以正常来说getList后执行完毕。
7.2、栅格布局
我们自己来仿照若依的服务监控来写一个栅格布局,首先新建菜单:
- 之后去
views/kuang
下新建kuang_server.vue
vue
<template>
<div class="kuang-contain">
<el-row :gutter="30">
<el-col :span="12">
<el-card>
<div slot="header">
<span><i class="el-icon-cpu"></i> CPU</span>
<el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
</div>
<div>
<el-descriptions :column="2">
<el-descriptions-item label="核心数" v-if="server.cpu"><el-tag size="medium">{{ server.cpu.cpuNum }}核</el-tag></el-descriptions-item>
<el-descriptions-item label="系统使用率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.sys }}%</el-tag></el-descriptions-item>
<el-descriptions-item label="用户使用率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.used }}%</el-tag></el-descriptions-item>
<el-descriptions-item label="当前等待率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.wait }}%</el-tag></el-descriptions-item>
<el-descriptions-item label="当前空闲率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.free }}%</el-tag></el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div slot="header">
<span><i class="el-icon-tickets"></i> 服务器内存</span>
<el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
</div>
<div>
<el-descriptions :column="2">
<el-descriptions-item label="总内存" v-if="server.mem"><el-tag size="medium">{{ server.mem.total }}G</el-tag></el-descriptions-item>
<el-descriptions-item label="已用内存"v-if="server.mem"><el-tag size="small">{{ server.mem.used }}G</el-tag></el-descriptions-item>
<el-descriptions-item label="剩余内存"v-if="server.mem"><el-tag size="small">{{ server.mem.free }}G</el-tag></el-descriptions-item>
<el-descriptions-item label="使用率"v-if="server.mem"><el-tag size="small">{{ server.mem.usage }}%</el-tag></el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div slot="header">
<span><i class="el-icon-tickets"></i> JVM内存</span>
<el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
</div>
<div>
<el-descriptions :column="2">
<el-descriptions-item label="总内存" v-if="server.mem"><el-tag size="medium">{{ server.jvm.total }}M</el-tag></el-descriptions-item>
<el-descriptions-item label="已用内存"v-if="server.mem"><el-tag size="small">{{ server.jvm.used }}M</el-tag></el-descriptions-item>
<el-descriptions-item label="剩余内存"v-if="server.mem"><el-tag size="small">{{ server.jvm.free }}M</el-tag></el-descriptions-item>
<el-descriptions-item label="使用率"v-if="server.mem"><el-tag size="small">{{ server.jvm.usage }}%</el-tag></el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</el-col>
</el-row>
<el-row class="kuang-row">
<el-col>
<el-card>
<div slot="header">
<span><i class="el-icon-monitor"></i> 服务器信息</span>
<el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
</div>
<div>
<el-descriptions :column="2">
<el-descriptions-item label="服务器IP" v-if="server.sys"><el-tag size="medium">{{ server.sys.computerIp }}</el-tag></el-descriptions-item>
<el-descriptions-item label="服务器名称"v-if="server.sys"><el-tag size="small">{{ server.sys.computerName }}</el-tag></el-descriptions-item>
<el-descriptions-item label="系统架构"v-if="server.sys"><el-tag size="small">{{ server.sys.osArch }}</el-tag></el-descriptions-item>
<el-descriptions-item label="操作系统"v-if="server.sys"><el-tag size="small">{{ server.sys.osName }}</el-tag></el-descriptions-item>
<el-descriptions-item label="项目路径"v-if="server.sys"><el-tag size="medium">{{ server.sys.userDir }}</el-tag></el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</el-col>
</el-row>
<el-row class="kuang-row">
<el-col>
<el-card>
<div slot="header">
<span>Java虚拟机</span>
<el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
</div>
<div>
<el-descriptions :column="3">
<el-descriptions-item label="Java名称" v-if="server.jvm"><el-tag size="medium">{{ server.jvm.name }}</el-tag></el-descriptions-item>
<el-descriptions-item label="服务启动时间" v-if="server.jvm"><el-tag size="small">{{ server.jvm.startTime }}</el-tag></el-descriptions-item>
<el-descriptions-item label="jdk安装路径" v-if="server.jvm"><el-tag size="small">{{ server.jvm.home }}</el-tag></el-descriptions-item>
<el-descriptions-item label="jdk版本" v-if="server.jvm"><el-tag size="small">{{ server.jvm.version }}</el-tag></el-descriptions-item>
<el-descriptions-item label="最大可用内存数" v-if="server.jvm"><el-tag size="small">{{ server.jvm.max }}M</el-tag></el-descriptions-item>
<el-descriptions-item label="运行时长" v-if="server.jvm"><el-tag size="medium">{{ server.jvm.runTime }}</el-tag></el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import {getServer} from "@/api/monitor/server";
export default {
name: "kuang_server",
created() {
this.getList();
this.openLoading();
},
data(){
return {
server: [],
}
},
methods: {
/** 查询服务器信息 */
getList() {
getServer().then(response => {
this.server = response.data;
console.log(this.server);
this.$modal.closeLoading();
});
},
// 打开加载层
openLoading() {
this.$modal.loading("正在加载服务监控数据,请稍候!");
}
}
}
</script>
<style scoped>
.kuang-contain{
margin: 30px;
}
.kuang-row{
margin-top: 30px;
}
</style>
仿造若依写一个监控页面就完成了。
8、Swagger
原生swagger不但很丑,而且参数配置的请求list还有必填项很难绕过!所以我们来换个皮肤:
- 在跟pom.xml中引入
xml
<!--Swagger增强-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
<version>${knife4j-spring-ui.version}</version>
</dependency>
- 在admin的pom.xml中继承
xml
<!--Swagger增强-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
</dependency>
- 编写前端vue文件
vue
<template>
<i-frame :src="url" />
</template>
<script>
import iFrame from "@/components/iFrame/index";
export default {
name: "kuang_swagger",
components: { iFrame },
data() {
return {
url: process.env.VUE_APP_BASE_API + "/doc.html"
};
},
};
</script>
- 添加菜单
如此便大功告成
我们来看一下SwaggerConfig
,启用Swagger,哪些接口暴露给Swagger展示呢?我们可以使用注解,也可以指定扫描哪个包中的注解,也可以扫描所有的api。最后还设置了一个请求前缀pathMapping,在 yml 中配置为 /dev-api
,意思是说请求项目的/abc接口实际上需要请求/dev-api/abc,这一点需要特别注意!
同时在类中可以看到Swagger文档的标题、描述设置等。
那么我们如何使用Swagger呢?我们想要自己的API展示到Swagger里面该怎么做呢?很简单,我们在自己包也引入关于Swagger的依赖
xml
<!-- swagger3-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
<!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.6.2</version>
</dependency>
<!--Swagger增强-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
</dependency>
然后在自己的方法上加上注解@ApiOperation("xxxx")
然后就可以在Swagger中看到啦!
9、XSS脚本攻击
以若依系统为例,我们在修改用户的时候,企图在备注中植入 JavaScript 脚本,然后将会被ry的XSS脚本攻击拦截:
若依如何抵抗XSS攻击呢?
当xss.enabled为true的时候,会开启XSS攻击防护,
首先new一个过滤注册器FilterRegistrationBean,当请求来的时候会触发这个过滤器,注册器里面设置拦截路径,拦截路径也就是我们在yml中配置的/system/*,/monitor/*,/tool/*
,给过滤器设置一个名字叫xssFilter
,设置过滤器的执行顺序,优先级为最高,最终将yml
中排除的链接初始化进过滤器,也就是排除的链接/system/notice
不需要开启XSS防护。
- 过滤器可以根据DispatcherType的类型选择是否执行
- FilterRegistrationBean,这个类用于把我们的过滤器注册到servlet容器中
- registration.addUrlPatterns用于设置拦截的URL
- registration.setOrder用于设置优先级,数字越小优先级越高
- registration.setInitParameters用于设置初始化参数,例如可以设置excludes(排除的URL,然而逻辑需要自己实现,默认不支持),接受参数类型为Map<String, String>
接着我们看看XSS过滤器到底干了什么
我们这里直接说结论:
- init方法主要是为了处理排除的URL
- handleExcludeURL方法用于看看URL是不是排除的URL,如果是GET和DELETE则直接不过滤;另外,如果所请求的URL在排除URL也直接不过滤
- XSS过滤原理是重新包装了Request,重写了getParameterValues方法和getInputStream方法。
- getParameterValues方法用于获取参数值的数组,如果存在值,那么就进行HTML过滤,并且排除前后空格。
- getInputStream方法用于处理JSON类型,如果发现是JSON,那么会先HTML过滤,然后重新包装成流进行返回。
10、防止重复提交过滤
我们讲述过前端的防止重复提交,如果请求数据 和请求URL 和最近一次请求一致,并且请求间隔小于1000ms,就进行请求拦截,直接拒绝当前请求。
从我上面的描述,发现了一个bug,总有手快的人,喜欢点A按钮,然后立刻点B按钮,然后又立刻点A按钮。那么对于A按钮是重复提交了,但是又不满足前端判断重复请求的条件,于是重复请求进入了后端,这时候就需要后端再次校验,是不是重复请求。
后端过滤重复请求也是通过过滤器,并且和XSS过滤惊人的相似。
首先new一个注册器,然后注册一个重复过滤器,拦截所有路径,优先级为最低,注入到Spring容器中。