技术文档 v1.0
singleboot-技术文档
2022-03-19 23:03:27
1. 序言
1.1 简介
基于springboot搭建的单应用脚手架,实现快捷开发
1.2 SingleBoot教程
2. 使用手册
注意:
- 运行环境:JDK1.8
- maven 3.3.9或更高
- 作者当前使用开发工具为IDEA 2018.1.4
2.1 下载项目
登录码云平台,打开singleboot主页,下载项目。
2.2 导入项目
2.3 运行项目
运行前的准备:
- 安装mysql数据库,作者所用mysql版本为5.7
-
执行
single-main
模块下的sql/geekclo_single.sql
脚本,初始化数据库环境 -
打开
single-main/src/main/resources/application.yml
配置文件,修改数据连接
,账号
和密码
,改为您所连接数据库的配置,local为本地开发环境,dev为开发服务器的环境,test为测试服务器的环境,produce为正式上线的环境 -
启动系统 主类为
GeekcloApplication
-
打开浏览器,输入
localhost:8088
,即可访问到登录页面,默认登录账号密码:admin/111111
2.4 打包部署
目前singleboot支持两种打包方式,即jar包
和war包
-
打包之前修改
single-main.pom
中的packaging
节点,改为jar
或者war
-
在项目的
single-parent
目录执行maven 命令clean package -Dmaven.test.skip=true
,即可打包,如下 -
命令执行成功后,在
single-main/target
目录下即可看到打包好的文件
提示:若打的包为jar包,可通过java -jar single-main-1.0.0-SNAPSHOT.jar
来启动系统
3. 开发手册
3.1 了解 singleboot
3.1.1 模块结构
新版的5.1版本的singleboot结构,开发环境由多模块变成了单模块,化繁为简,返璞归真,
single-core
为抽象出的核心(通用)模块,以供其他模块调用,此模块主要封装了一些通用的工具类,公共枚举,常量,配置等等
single-generator
为代码生成模块,其中代码生成模块整合了mybatis-plus的代码生成器和独有的代码生成器,可以一键生成entity,dao,service,html,js等代码,可减少很多开发新模块的工作量
single-mian
为主模块
3.1.2 包结构说明
3.2 实战开发
开发三部曲 -> 1.建表 2.代码生成 3.添加菜单 4.适配业务代码
下面以一个商品业务
为例,实战演练如何编写简单的增删改查业务
3.2.1 建表
新建订单表如下:
CREATE TABLE `main_goods` (
`id` bigint(20) NOT NULL,
`title` varchar(255) NOT NULL COMMENT '标题',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
`brief` varchar(255) DEFAULT NULL COMMENT '简介',
`detail` varchar(255) DEFAULT NULL COMMENT '详情',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表'
3.2.2 代码生成
登录管理系统,打开代码生成页面,填写如下内容,注意看红线部分
内容
下面详细讲解代码生成使用:
1. 项目路径: 代码生成的路径,具体到single-main模块的绝对路径,一般不需要修改
,因为程序会自动计算出single-main的绝对路径
2. 项目的包: 为single-main的同GeekcloApplication
类同一目录的包,如下图,一般也不需要修改
3. 核心包: single-core的包,一般也不需要修改
4. 作者: 填写代码生成出的注释上的作者
5. 业务名称: 生成业务的中文名称,用于注释和菜单名用
6. 模块名称: 对应代码中modular包下的模块名称,默认为main,一般小的项目全部放main下就足够,若需增加新的模块可以重新命名。
7. 父级菜单: 此项的选择会影响生成sql添加菜单项的切入点,生成出的sql文件执行后可自动增加到sys_menu菜单项,省去手动添加菜单的繁琐
8. 表前缀: 填写此项会自动移除生成实体,mapper和service类的名称中包含的重复前缀,例如生成商品表业务代码时,填写main_
,则生成的实体中不会包含Main前缀名称,若不填写,则生成的实体类为MainGoods
9. 数据表: 选择即为生成该表所对应的实体,dao,service等类
10. 模板: 选择后生成相应的控制器,实体,service,dao代码等等
生成代码之后需要重启一下管理系统,生成的代码才可以生效!
3.3.3 添加菜单与分配权限
生成代码之后,需要为管理系统添加菜单,才可以让新增加的业务显示到页面上,添加菜单有两种方式:
第一种为手动添加菜单,依次点击系统管理
->菜单管理
->点击添加
,打开添加页面,如下
这里需要注意如下几点:
请求地址
需要和Controller中的RequestMapping的值一致排序
为同层级菜单中显示菜单的顺序父级编号
的选择可以更改菜单插入的位置图标
可以从H+的资源库中获取- 因为菜单管理不单单是对管理系统中的菜单管理,也包含权限的管理,所以需要选择是否是菜单这个选项
第二种添加菜单的方式为直接执行代码生成中的sql脚本,默认生成的sql文件在single-main/sql/menu
目录下,如下所示
INSERT INTO `geekclo_single`.`sys_menu` (`id`, `code`, `pcode`, `pcodes`, `name`, `icon`, `url`, `num`, `levels`, `ismenu`, `tips`, `status`, `isopen`) VALUES ('1072066764073857025', 'goods', '0', '[0],', '商品管理', '', '/goods', '99', '1', '1', NULL, '1', '0');
INSERT INTO `geekclo_single`.`sys_menu` (`id`, `code`, `pcode`, `pcodes`, `name`, `icon`, `url`, `num`, `levels`, `ismenu`, `tips`, `status`, `isopen`) VALUES ('1072066764073857026', 'goods_list', 'goods', '[0],[goods],', '商品管理列表', '', '/goods/list', '99', '2', '0', NULL, '1', '0');
INSERT INTO `geekclo_single`.`sys_menu` (`id`, `code`, `pcode`, `pcodes`, `name`, `icon`, `url`, `num`, `levels`, `ismenu`, `tips`, `status`, `isopen`) VALUES ('1072066764073857027', 'goods_add', 'goods', '[0],[goods],', '商品管理添加', '', '/goods/add', '99', '2', '0', NULL, '1', '0');
INSERT INTO `geekclo_single`.`sys_menu` (`id`, `code`, `pcode`, `pcodes`, `name`, `icon`, `url`, `num`, `levels`, `ismenu`, `tips`, `status`, `isopen`) VALUES ('1072066764073857028', 'goods_update', 'goods', '[0],[goods],', '商品管理更新', '', '/goods/update', '99', '2', '0', NULL, '1', '0');
INSERT INTO `geekclo_single`.`sys_menu` (`id`, `code`, `pcode`, `pcodes`, `name`, `icon`, `url`, `num`, `levels`, `ismenu`, `tips`, `status`, `isopen`) VALUES ('1072066764073857029', 'goods_delete', 'goods', '[0],[goods],', '商品管理删除', '', '/goods/delete', '99', '2', '0', NULL, '1', '0');
INSERT INTO `geekclo_single`.`sys_menu` (`id`, `code`, `pcode`, `pcodes`, `name`, `icon`, `url`, `num`, `levels`, `ismenu`, `tips`, `status`, `isopen`) VALUES ('1072066764073857030', 'goods_detail', 'goods', '[0],[goods],', '商品管理详情', '', '/goods/detail', '99', '2', '0', NULL, '1', '0');
执行完成后可以看到,菜单管理页面中已经有了新添加的订单相关的菜单和资源,如下
在添加完菜单之后,还需要给角色分配相关的菜单权限,才可以把新增的业务显示到菜单上
打开系统管理
->角色管理
,给当前的登录的超级管理员,增加刚才新增的权限,如下图
配置完成刷新页面即可看到,即可看到新增加的菜单,如下图,若看不到请重新登录
到这里,基本的增删改查功能就实现了
3.3.4 编写业务代码
由于代码生成器还不能实现100%的智能,所以生成之后还需要对生成的代码做一些完善,如果有除了增删改查以外的业务,还需要手动编写。
3.3 权限控制与校验
3.3.1 用户,角色和资源
用户、角色和资源(或者说权限),这三者的关系是用户对应角色
,角色对应资源
,菜单和所有的按钮都可以看做是资源
(或权限
),把某一个角色赋予相应的资源,那么该角色就会有访问该资源的权限,否则,该角色访问这些被管控的资源就会被服务器返回403 没有权限
,当角色绑定资源后还需要给用户赋予角色
才可以让登录的用户访问相关服务器接口。
一句话概括: 用户对应角色,角色对应资源
3.3.2 如何对资源进行权限控制
系统中,通过在Controller控制器上加@Permission
注解进行权限校验,如下所示,该接口在被访问的时候,就会进行权限校验
通过我们查找用户对应的角色
,并查找角色对应的资源
,可以找到,当前用户(admin)有该资源的权限
@Permission
注解中可以带一个String数组类型的参数,如下,加上该参数,则接口被限制为只有某个或某些角色才可访问
权限的检查是通过AOP
拦截@Permission
注解完成的,当访问受权限控制的资源时,AOP
对当前请求的servletPath
和数据库中sys_menu
表的url
字段进行匹配,如果当前用户所拥有的权限包含当前请求的servletPath
,则访问这个接口成功
3.3.3 前端页面对权限资源的显示
在前端页面中,如果增删改查等按钮受权限控制,则我们需要对资源进行一个权限检查,如果有该资源的权限,才能让该按钮显示,通过beetl
的shiro注册方法
即可完成该项的检查
@if(shiro.hasPermission("/member/add")){
<#button name="添加" icon="fa-plus" clickFun="Member.openAddMember()"/>
@}
@if(shiro.hasPermission("/member/update")){
<#button name="修改" icon="fa-edit" clickFun="Member.openMemberDetail()" space="true"/>
@}
@if(shiro.hasPermission("/member/delete")){
<#button name="删除" icon="fa-remove" clickFun="Member.delete()" space="true"/>
@}
其中shiro.hasPermission()
起到了权限检查的作用,如果有该资源对应的权限,则被检查的资源显示,若没有该资源的权限,则按钮不显示
3.4 多数据源的使用
1.打开application.yml中的多数据源开关
geekclo:
muti-datasource:
open: true
2.配置application.yml中的多数据源的连接信息
另外注意,如果想开启多数据源,需要关闭single-core中mybatis-plus中的自动配置!!重要!!如下!!
3.编写测试多数据源的代码,注意观察
@DataSource注解
.
@Override
@DataSource(name = DatasourceEnum.DATA_SOURCE_BIZ)
@Transactional
public void testBiz() {
Test test = new Test();
test.setBbb("bizTest");
testMapper.insert(test);
}
@Override
@DataSource(name = DatasourceEnum.DATA_SOURCE_GEEKCLO)
@Transactional
public void testGeekclo() {
Test test = new Test();
test.setBbb("GeekcloTest");
testMapper.insert(test);
}
4.执行,可以看出,两条数据同时插入了不同的数据库中的两张表中
多数据源的原理就是一个项目同时配置了两个DataSource
,并把这两个DataSource
放到DynamicDataSource
绑定,使用AOP进行动态切换当前操作的数据源。
若想深入了解多数据源的配置和原理可参考MybatisPlusConfig类
和MultiSourceExAop类
3.5 如何分页
singleboot的分页是通过mybatis-plus的分页插件实现的,大体分如下两种情况
3.5.1 简单查询的分页
如果查询结果为单表查询,例如查询用户列表,则可以调用mybatis plus的自动生成的mapper中的selectPage()
或者selectMapsPage()
方法,Page
类的构造函数中第一个参数为当前查询第几页,第二个参数为每页的记录数。
3.5.2 复杂查询的分页
若查询结果是关联多个表的操作,则需要用到自定义的mapper,此时的分页操作也很简单,只需要给mapper的第一个参数设置为Page
对象即可,例如LogController
中的查询操作日志列表
,用的就是复杂查询的分页,我们可以看到在mybatis接口的第一个参数中,传递了Page
对象,如下
List<OperationLog> getOperationLogs(@Param("page") Page<OperationLog> page, @Param("beginTime") String beginTime, @Param("endTime") String endTime)
当mybatis执行此方法的时候,会被mybatis-plus的分页插件自动拦截到,并且把分页查询的结果返回到这个Page
对象中!
3.6 数据范围
3.6.1 介绍
数据范围是指当前部门的用户可以看到当前部门和子部门的数据,子部门的数据不可以看到上级部门的数据,但超级管理员
例外。
3.6.2 如何使用
使用时,只需要new
一个DataScope
,并在构造方法中传递给当前用户用后的部门权限(一般我们用封装好的ShiroKit.getDeptDataScope()
方法即可获取到当前用户的部门权限集合),之后,传递给mybatis的dao方法的第一个参数即可,例子如下
DataScope dataScope = new DataScope(ShiroKit.getDeptDataScope());
List<Map<String, Object>> users = managerDao.selectUsers(dataScope, name, beginTime, endTime, deptid)
注意: 在使用过程中,原mybatis的dao方法的查询结果中必须包含deptid字段(默认情况)
,若部门id不叫deptid也可也初始化DateScope
对象的时候,修改该对象的scopeName
属性,改为自定义的部门id字段名即可
3.6.3 原理
数据范围的原理是利用了mybatis拦截器
,类似于mybatis-plus的分页插件,在原查询结果之上包装了一层select筛选查询
,如下
select (原语句字段) from (原语句) where deptid in (DataScope对象中包含的部门id列表)
若想深入了解数据范围的编写过程和原理可参考视频教程第15节 数据范围使用和原理
,内有详细的讲解
3.7 restApi模块的使用
rest功能的接口以 /restApi 为请求链接头,使用 token+jwt 做鉴权
3.7.1 关于jwt鉴权
在了解singleboot-rest模块的使用之前,需要了解一下jwt鉴权机制,下面给出一些参考资料
- 什么是JWT-JSON WEB TOKEN -> https://www.jianshu.com/p/576dbf44b2ae
说白了就是如果想请求服务器资源,需要先走服务器的auth/login接口,用账号和密码换取token,之后每个接口的请求都需要带着token去访问,否则就是鉴权失败.
3.7.2 关于传输数据的签名
签名机制是指客户端向服务端传输数据中,对传输数据进行md5加密,并且加密过程中利用Auth接口返回的随机字符串进行混淆加密,并把md5值同时附带给服务端,服务端通获取数据之后对数据再进行一次md5加密,若加密结果和客户端传来的数据一致,则认定客户端请求的数据是没有被篡改的,若不一致,则认为被加密的数据是被篡改的.
加密方式可查询HttpBodyController控制器
3.8 日志记录
在我们日常开发中,对于某些关键业务,我们通常需要记录该操作的内容,例如修改了什么数据,修改的内容是什么,删除了哪些数据等等,在singleboot中有一整套完善的解决方案来完成此项功能
3.8.1 业务日志
我们通过@BussinessLog注解
来记录日志,该注解源码如下,
@BussinessLog(value = "添加部门", key = "simplename", dict = DeptDict.class)
其中,value
为需要记录日志的业务名称,key
为修改或删除内容的唯一标识,通过这个唯一标识可以知道具体的修改的哪条记录,删除的哪条记录等等,dict
为对修改字段的中文翻译字典,因为程序记录的都是英文的字段名称,这里通过字典,把英文字段和中文名称对应起来,那么日志信息记录到数据库中就可以变为中文的记录
翻译字典类中包含两个方法init()
和initBeWrapped()
,其中init()
为存放英文字段和中文字段的匹配,initBeWrapped()
操作的是把某些字段的数字值翻译为中文直观名称的过程,例如当修改用户信息时,用户修改了一个人性别信息(数据库中1是男,2是女),由1变为了2,程序记录的是数据库中1变为2,但是这句话给业务人员看到他是不知道1和2是什么东西的,所以这里做了一个值的包装
,把1
包装为对应的中文名称男
,2
包装为对应的中文名称女
,这样,记录到数据库中,信息就变为了,xxx用户操作了修改用户
功能,值由男
变为了女
.
在initBeWrapped()方法中putFieldWrapperMethodName()
这个方法的第一参数是被包装的字段名,第二个参数是ConstantFactory
中的方法名,因为默认会调用ConstantFactory
来包装值属性
下面介绍业务日志记录的具体步骤:
- 1.在需要被记录日志的接口上添加@BussinessLog注解,并根据需要填写三个属性(value,key,dict)
- 2.若是添加或者修改业务,往往需要去编写Dict字典类
- 3.若是修改业务,例如修改用户信息,因为点击更新用户的时候不会提交修改之前的数据,所以在更新用户信息之前需要保存一下用户的旧的信息才可以记录用户修改的内容,这个缓存用户临时信息的地方一般添加在跳转到用户详情接口,用
LogObjectHolder.me().set(user);
这行代码来缓存用户的旧的信息,具体用法可以参考UserMgrController
类中的userEdit()
和edit()
3.8.2 异常日志
由于系统有统一的异常拦截器,一般程序的报错,不管是业务异常还是未知的RuntimeException都会拦截并记录到数据库,若是您有自己的异常日志需要记录到数据库或者日志文件,推荐如下做法
-
如果记录到数据库,调用系统的日志记录工具类,如下
-
LogManager.me().executeLog();
该方法为异步记录日志的方法,executeLog()方法中需要传递一个TimerTask
对象,TimerTask对象可以用LogTaskFactory
类创建,在LogTaskFactory
类中,有5个方法,可以分别记录不用的日志,有登录日志
,退出日志
,业务日志
,异常日志
等等,可以自行选择调用
2. 若需要记录日志到文件中,可以采用slf4j的org.slf4j.Logger
类记录,具体方法如下
-
//首先在类中初始化
-
private Logger log = LoggerFactory.getLogger(this.getClass());
-
//再在方法中调用
-
log.error("业务异常:", e);
3.9 如何使用缓存
在singleboot中使用缓存的地方不多,主要在ConstantFactory的查询中用了缓存,在ConstantFactory有高频调用的查询,所以在这些方法上加了缓存,搜索加上缓存后还要注意在修改了相关数据的时候要删除缓存,否则可能导致数据的不一致,在singleboot中默认用的是Ehcache缓存,并配合了spring cache使用,用spring cache的好处就是,spring cache是缓存的抽象,如果想换为redis缓存,则不用修改代码,改一下配置即可实现,下面介绍两种操作缓存的方法
3.9.1 用工具类操作
在singleboot-core中封装了一些常用的操作Ehcache缓存的工具类CacheUtil
,此类采用静态方法调用的方式,可以添加,获取,删除缓存,用法非常简单
-
//添加缓存,第一个参数为缓存的名称,是ehcache.xml中<cache>节点的NAME,key为添加缓存的键值,value为缓存的值
-
public static void put(String cacheName, Object key, Object value);
-
//获取某个缓存名称中的某个键值对应的缓存
-
public static <T> T get(String cacheName, Object key);
-
//获取某个缓存的所有key
-
public static List getKeys(String cacheName);
-
//删除某个key对应的缓存
-
public static void remove(String cacheName, Object key);
-
//删除某个缓存名称下的所有缓存
-
public static void removeAll(String cacheName);
3.9.2 用spring cache操作缓存
利用spring cache来操作缓存,可以很方便的在redis和ehcache之间切换缓存实现,利用spring cache 的缓存注解,加到方法之上可以很方便的缓存方法的结果,如果参数对应的键值存在了缓存,则下一次走这个方法则会直接返回缓存的结果,spring cache提供了4个注解来操作缓存.
- 1.@Cacheable表明在调用方法之前,首先应该在缓存中查找方法的返回值,如果这个值能够找到,则会返回缓存的值,否则执行该方法,并将返回值放到缓存中,一般在数据库查询(
select
)之后调用这个注解- 2.@CachePut表明在方法调用前不会检查缓存,方法始终都会被调用,调用之后把结果放到缓存中,一般在数据库操作插入数据(
save
)的时候调用- 3.@CacheEvict表明spring会清除一个或者多个缓存,一般在数据库更新或者删除数据的时候调用(
update
或者delete
)- 4.@Caching分组的注解,可以同时应用多个其他缓存注解,可以相同类型或者不同类型