1.用户行为日志管理
日志业务分析及设计
背景分析
在实际项目中,用户操作软件的过程,通常会以日志记录。例如记录用户在什么时间点,执行了什么操作,访问了什么方法,传递了什么参数,执行时长是多少等这些信息要存储到数据库。
业务表的分析及设计
对于用户行为日志表的设计如下:
CREATE TABLE `sys_logs` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL COMMENT '登录用户',
`operation` varchar(50) DEFAULT NULL COMMENT '用户操作',
`method` varchar(200) DEFAULT NULL COMMENT '请求方法',
`params` varchar(5000) DEFAULT NULL COMMENT '请求参数',
`time` bigint(20) NOT NULL COMMENT '执行时长(毫秒)',
`ip` varchar(64) DEFAULT NULL COMMENT 'IP地址',
`createdTime` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=175 DEFAULT CHARSET=utf8 COMMENT='系统日志';
日志业务原型设计
日志业务核心API设计
日志模块业务核心API设计,如图所示:
其中:
- SysLog (封装用户行为日志)
- SysLogDao(执行日志数据逻辑)
- SysLogService&SysLogServiceImpl (执行日志业务逻辑操作)
- SysLogController(执行日志的请求、响应控制逻辑操作)
用户行为日志查询并呈现
业务分析与设计
业务分析
将用户行为日志从数据库查询出来以后,以统一的JSON格式,将数据响应给客户端
业务数据架构设计
用户日志行为数据查询时,其数据封装及传递架构如下:
业务操作访问时序设计
基于业务描述,进行API访问时序设计,如图所示:
其中:
- 页面加载时序设计
第一步:用户点击首页日志管理菜单时向服务端发送异步加载请求
第二步:服务端通过PageController中的方法处理日志页面加载请求
第三步:在日志列表页面加载完成以后,向服务端发起异步数据加载请求
- 数据访问时序设计
第一步:用户向服务端发送数据查询请求(默认先查询第一页数据)
http://localhost/log/doFindPageObjects?pageCurrent=1
第二步:服务端调用SysLogController的doFindPageObjects方法处理查询请求并将响应结果封装到JsonResult,然后响应到客户端。
第三步:在SysLogController的doFindPageObjects方法内部调用SysLogService对象的findPageObjects方法执行分页查询逻辑,并将查询结果封装到PageObject对象,然后返回给控制层。
第四步:在SysLogServiceImpl的findPageObjects方法内部调用SysLogDao的getRowCount方法,findPageObjects方法获取用户日志总记录数以及当前页要呈现的记录。
服务端代码设计及实现
POJO类设计及实现
设计SysLog对象,通过此对象封装查询到用户日志行为信息,关键代码如下
package com.cy.pj.sys.pojo;
@Data
public class SysLog implements Serializable{
private static final long serialVersionUID = 4805229781871486416L;
private Integer id;
private String ip;
private String username;
private String operation;
private String method;
private String params;
private Integer time;
private Date createdTime;
}
DAO 接口设计及实现
设计SysLogDao,并通过此类型的对象实现分页数据查询逻辑。
第一步:创建SysLogDao接口,关键代码如下:
package com.cy.pj.sys.dao;
@Mapper
public interface SysLogDao{
}
第二步:在SysLogDao接口中添加查询日志日志总记录数的方法,关键代码如下:
/**
* 基于条件查询用户日志总记录数
* @param username 查询条件
* @return 查询到的总记录数
*/
int getRowCout(String username);
第三步:在SysLogDao接口中添加当前页日志记录的方法,关键代码如下:
/**
* 基于条件查询当前页的日志记录
* @param username 查询条件
* @param startIndex 分页查询条件的起始位置
* @param pageSize 每页最多查询多少条记录
* @return 当前页用户行为日志
*/
List<SysLog> findPageObjects(String username,
Integer startIndex,Integer pageSize);
第四步:定义SysLogDao中方法对应的SQL映射
在mappers/sys目录中创建SysLogMapper.xml文件,并在文件中定义SQL映射,关键代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cy.pj.sys.dao.SysLogDao">
<!--通过sql元素提取共性-->
<sql id="queryWhereId">
from sys_logs
<where>
<if test="username!=null and username!=''">
username like concat("%",#{username},"%")
</if>
</where>
</sql>
<!--基于条件查询总记录数-->
<select id="getRowCount" resultType="int">
select count (*)
<include refid="queryWhereId"/>
</select>
<!--基于条件查询当前页记录-->
<select id="findPageObjects" resultType="com.cy.pj.sys.pojo.SysLog">
select *
<include refid="queryWhereId"/>
order by createdTime desc
limit #{startIndex},#{pageSize}
</select>
</mapper>
Service对象设计和实现
基于查询条件获取用户日志行为信息并进行计算和封装
第一步:定义SysLogService接口并添加日志查询方法,关键代码如下:
package com.cy.pj.sys.service;
public interface SysLogService{
/**
* 基于条件查询用户行为日志并进行封装
* @param username 查询条件
* @param pageCurrent 当前页面值
* @return 封装分页查询结果
*/
PageObject<SysLog> findPageObjects(String username,
Integer pageCurrent);
}
第二步:定义SysLogService接口实现类并重写分页查询方法
package com.cy.pj.sys.service.impl;
@Service
public class SysLogServiceImpl implements SysLogService{
@Autowired
private SysLogDao sysLogDao;
public PageObject<SysLog> findPageObjects(String username,
Integer pageCurrent){
//1.对参数进行校验(可以自己校验,也可以借助框架:spring validation)
if(pageCurrent==null||pageCurrent<1)
throw new IllegalArgumentException("页码值不合法");//无效参数异常
//2.基于查询条件查询总记录数并校验
int rowCount=sysLogDao.getRowCount(username);
if(rowCount==0)
throw new ServiceException("没有找到对应记录");
//3.查询当前页记录
int pageSize=5;//页面大小,每页最多显示多少条记录
int startIndex=(pageCurrent-1)*pageSize;//当前页起始位置
List<SysLog> records=
sysLogDao.findPageObjects(username,startIndex,pageSize);
//4.封装查询结果
return new PageObject<>(rowCount, records, pageSize, pageCurrent);
}
}
Controller 类设计及实现
创建SysLogController类型,通过此类型的对象处理客户端日志请求。
第一步:创建SysLogController类型类型,关键代码如下
package com.cy.pj.sys.controller;
@RestController
@RequestMapping("/log/")
public class SysLogController{
@Autowired
private SysLogService sysLogService;
}
第二步:在SysLogController类中添加处理日志查询请求的方法,关键代码如下:
@GetMapping("doFindPageObjects")
public JsonResult doFindPageObjects(String username,Integer pageCurrent){
PageObject<SysLog> pageObject=
sysLogService.findPageObjects(username,pageCurrent);
return new JsonResult(pageObject);
}
第三步:启动服务进行访问测试
打开浏览器,在地址栏中输入http://localhost/log/doFindPageObjects?pageCurrent=1进行资源方法,响应结果如图所示:
客户端代码设计及实现
Starter页面事件处理
第一步: 定义响应请求页面的方法
在PageController类中,添加响应模块页面请求的方法,关键代码如下
@GetMapping("/{module}/{moduleUI}")
public String doModuleUI(@PathVariable String moduleUI){
return "sys/"+moduleUI;
}
第二步:首页页面日志管理事件注册
starter.html页面加载完成以后,进行click事件注册,并基于请求的url加载页面资源,然后将响应页面呈现在浏览器中。
$(function(){
doLoadUI("load-log-id","log/log_list");
})
function doLoadUI(id,url){
$("#"+id).click(function(){
//通过jquery中的load函数异步加载url对应的页面
$("#mainContentId").load(url);
})
}
日志列表页面事件处理
第一步:在PageController中定义响应分页页面方法,关键代码如下:
@GetMapping("doPageUI")
public String doPageUI(){
return "common/page";
}
第二步:日志列表页面加载完成定义执行分页页面加载,关键代码如下
$(function(){
$("#pageId").load("doPageUI");
})
第三步:日志分页页面加载完成以后异步加载服务端数据并进行呈现
修改分页页面加载的JS代码逻辑,关键代码如下:
$(function(){
$("#pageId").load("doPageUI",doGetObjects);
})
定义异步加载日志信息的doGetObjects方法,关键代码如下:
function doGetObjects(){
//1.params
let params={pageCurrent:1};//暂时默认指定页码值为1
//2.url
let url='log/doFindPageObjects';
//3.ajax request
$.ajax({
url:url,
data:params,
success(result){//JsonResult
//处理查询到的响应结果
doHandleQueryResponseResult(result);
}
})
}
将当前页记录呈现在页面上,关键代码如下:
function doHandleQueryResponseResult(result){
if(result.state==1){//1表示ok
doSetTableBodyRows(result.data.records);
}else{
doSetTableBodyErrors(result.message);
}
}
定义处理错误信息的doSetTableBodyErrors方法,关键代码如下:
function doSetTableBodyErrors(msg){
let thColumns=$("#log-table>thead>tr>th").length;
let tBody=$("#log-table>tbody");
let msgRow=`<tr><td colspan=${thColumns}>${msg}</td></tr>`
tBody.html(msgRow);
}
迭代记录并将其追加到tbody中,关键代码如下:
function doSetTableBodyRows(records){
let tBody=$("#log-table>tbody");
tBody.empty();
records.forEach((item,i)=>tBody.append(doCreateRow(item,i)))
}
基于每条记录进行"行渲染"
function doCreateRow(item,i){
return `<tr>
<td>${i+1}</td>
<td>${item.username}</td>
<td>${item.operation}</td>
<td>${item.method}</td>
<td>${item.params}</td>
<td>${item.ip}</td>
<td>${item.time}</td>
</tr>`
}
Page 页面事件处理
第一步:定义初始化分页数据的方法,关键代码如下:
function doSetPagination(pageObject){
$(".rowCount").html(`总记录数(${pageObject.rowCount})`);
$(".pageCount").html(`总页数(${pageObject.pageCount})`);
$(".pageCurrent").html(`当前页(${pageObject.pageCurrent})`);
//存储当前页码值和总页数(后续要用)
//data(key[,value])为jquery中的一个数据存取函数,没有value时就表示取
$("#pageId").data("pageCurrent",pageObject.pageCurrent);
$("#pageId").data("pageCount",pageObject.pageCount);
}
第二步:在日志列表页面中的处理查询结果的方法内部,调用doSetPagination方法
function doHandleQueryResponseResult(result){
if(result.state==1){//1表示ok
doSetTableBodyRows(result.data.records);
doSetPagination(result.data);//pageObject
}else{
doSetTableBodyErrors(result.message);
}
}
第三步:注册并处理分页点击事件,关键代码如下:
$(function(){
//在pageId元素上注册click事件,当点击元素内部指定的子元素时,指定doJumToPage函数
$("#pageId").on("click",".first,.pre,.next,.last",doJumpToPage)
})
function doJumToPage(){//基于新的页码值重新加载数据
//1.基于点击事件修改当前页面值
//1.1 获取当前页码值以及总页数
let pageCurrent=$("#pageId").data("pageCurrent");
let pageCount=$("#pageId").data("pageCount");
//1.2 修改当前页码值
//获取被点击对象的class属性值($(this)可以获取被点击对象)
let cls=$(this).prop("class");
if(cls=="first"){
pageCurrent=1;
}else if(cls=="pre"&&pageCurrent>1){
pageCurrent--;
}else if(cls=="next"&&pageCurrent<pageCount){
pageCurrent++;
}else if(cls=="last"){
pageCurrent=pageCount;
}else{
return;
}
//2.基于新的页码值重新执行查询
//2.1重新存储新的页码值
$("#pageId").data("pageCurrent",pageCurrent);
//2.2调用列表页面中的doGetObjects方法,并在方法内部基于新的页码值重新执行查询
doGetObjects();
}
第四步:修改列表页面的doGetObjects方法内部参数的定义
let pageCurrent=$("#pageId").data("pageCurrent");
if(!pageCurrent)pageCurrent=1;
let params={pageCurrent:pageCurrent};//暂时默认指定页码值为1
列表页面查询按钮事件处理
第一步:查询按钮事件注册
日志列表"页面加载完成"以后,添加查询按钮事件注册操作,关键代码如下:
$(".input-group-btn").on("click",".btn-search",doQueryObjects);
定义查询按钮事件处理函数
function doQueryObjects(){
//初始化页码值
$("#pageId").data("pageCurrent",1);
doGetObjects();
}
第二步:修改列表页面doGetObjects方法中的参数定义
let pageCurrent=$("#pageId").data("pageCurrent");
if(!pageCurrent)pageCurrent=1;
let params={pageCurrent:pageCurrent};//暂时默认指定页码值为1
let queryName=$("#searchNameId").val();//获取输入的用户名
if(queryName)params.username=queryName;//username不能随意写
章节总结(Summary)
重难点分析
- 日志表的设计(记住有哪些字段)
- 核心API的设计(SysLog,SysLogDao,SysLogService,SysLogController)
- 日志数据查询结果的封装和传递(SysLog,PageObject,JsonResult)
- 项目中统一异常处理类的设计(GlobalExceptionHandler)
- 客户端向服务端传递的参数设计(username,pageCurrent)
2.菜单管理
菜单业务分析设计
背景分析
几乎所有软件都需要一个操作界面,通过界面中的一些选项或按钮操作具体的业务,这些选项和按钮我们通常称之为菜单.菜单是资源外在的一种表现形式,通过菜单操作我们系统中的资源.
业务设计分析
在数据库对应的表中设计并存储所有菜单信息,每个菜单可能都有一个对应的url,基于这个url可以找到对应的资源,进而可以访问和操作这些资源.其具体表的设计如下:
CREATE TABLE `sys_menus` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT '资源名称',
`url` varchar(200) DEFAULT NULL COMMENT '资源URL',
`type` int(11) DEFAULT NULL COMMENT '类型 1:菜单 2:按钮',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`note` varchar(100) DEFAULT NULL COMMENT '备注',
`parentId` int(11) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`permission` varchar(500) DEFAULT NULL COMMENT '授权(如:user:create)',
`createdTime` datetime DEFAULT NULL COMMENT '创建时间',
`modifiedTime` datetime DEFAULT NULL COMMENT '修改时间',
`createdUser` varchar(20) DEFAULT NULL COMMENT '创建用户',
`modifiedUser` varchar(20) DEFAULT NULL COMMENT '修改用户',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=149 DEFAULT CHARSET=utf8 COMMENT='资源管理';
业务原型设计分析
基于菜单需求设计菜单的列表页面,如图所示:
基于菜单需求设计菜单编辑页面,如图所示:
业务核心API设计分析
- Pojo (SysMenu)
- Dao (SysMenuDao)
- Service (SysMenuService,SysMenuServiceImpl)
- Controller (SysMenuController)
菜单数据查询设计及实现
业务分析
将数据库中菜单(SysMenu)表中的信息查询出来,然后在客户端以树结构(TreeGrid)方式进行呈现.
服务端设计及实现
Pojo对象设计
通过此对象封装菜单相关信息
package com.cy.pj.sys.pojo;
@Data
public class SysMenu implements Serializable {
private static final long serialVersionUID = -4948259309231173588L;
private Integer id;
private String name;
private String url;
private Integer type;
private Integer sort;
private String note;
private Integer parentId;
private String parentName;
private String permission;
private Date createdTime;
private Date modifiedTime;
private String createdUser;
private String modifiedUser;
}
Dao 接口及方法设计
定义菜单数据接口及数据查询方法
package com.cy.pj.sys.dao;
@Mapper
public interface SysMenuDao{
/**查询所有菜单信息*/
List<SysMenu> findObjects();
}
创建SysMenuMapper.xml文件并基于菜单查询方法定义SQL映射
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cy.pj.sys.dao.SysMenuDao">
<select id="findObjects" resultType="com.cy.pj.sys.pojo.SysMenu">
select c.*,p.name parentName
from sys_menus c left join sys_menus p
on c.parentId=p.id
</select>
</mapper>
Service接口及方法设计
第一步:定义菜单业务接口,关键代码如下:
package com.cy.pj.sys.service;
public interface SysMenuService{
List<SysMenu> findObjects();
}
第二步:定义菜单业务接口的实现类,关键代码如下:
package com.cy.pj.sys.service.impl;
@Service
public class SysMenuServiceImpl implements SysMenuService{
@Autowired
private SysMenuDao sysMenuDao;
public List<SysMenu> findObjects(){
return sysMenuDao.findObjects();
}
}
Controller 类及方法设计
创建SysMenuController类型,通过此类型对象处理客户端的菜单请求.
package com.cy.pj.sys.controller;
@RestController
@RequestMapping("/menu/")
public class SysMenuController{
@Autowired
private SysMenuService sysMenuService;
@GetMapping("doFindObjects")
public JsonResult doFindObjects(){
List<SysMenu> list= sysMenuService.findObjects();
return new JsonResult(list);
}
}
启动服务,对controller进行访问测试,测试结果如下:
客户端设计及实现
(省略)
菜单添加页面菜单树的加载
服务端设计及实现
Dao接口方法定义
在SysMenuDao中定义查询菜单信息的方法及sql映射.
@Select("select id,name,parentId from sys_menus")
public List<Node> findZtreeMenuNodes();
Service接口方法定义及实现
第一步:在SysMenuService接口中添加菜单节点查询方法,关键代码如下
public List<Node> findZtreeMenuNodes();
第二步:在SysMenuServiceImpl类中添加菜单节点查询方法的具体实现,关键代码如下
public List<Node> findZtreeMenuNodes(){
return sysMenuDao.findZtreeMenuNodes()
}
Controller类中方法设计及实现
第一步:在SysMenuController类中添加处理查询菜单节点信息的方法,关键代码如下:
@GetMapping("doFindZtreeMenuNodes")
public JsonResult doFindZtreeMenuNodes(){
return new JsonResult(sysMenuService.findZtreeMenuNodes());
}
第二步:启动服务进行访问测试,如图所示:
客户端设计及实现
(省略)
菜单数据添加设计及实现
服务端设计及实现
Dao 接口方法定义
第一步:在SysMenuDao中添加向数据库新增菜单信息的方法
int insertObject(SysMenu entity);
第二步:在映射文件SysMenuMapper.xml中添加insert元素
<insert id="insertObject" parameterType="com.cy.pj.sys.pojo.SysMenu">
insert into sys_menus
(name,url,type,sort,note,parentId,permission,
createdTime,modifiedTime,createdUser,modifiedUser)
values
(#{name},#{url},#{type},#{sort},#{note},#{parentId},#{permission},
now(),now(),#{createdUser},#{modifiedUser})
</insert>
Service 接口方法设计及实现
第一步:在SysMenuService接口中添加新增菜单的方法,关键代码如下:
int saveObject(SysMenu entity);
第二步:在SysMenuServiceImpl实现类中重写接口方法,关键代码如下:
public int saveObject(SysMenu entity){
//对参数校验(省略)
int rows= sysMenuDao.insertObject(entity);
return rows;
}
Controller类中方法设计及实现
第一步:在SysMenuController中添加处理新增菜单请求的方法,关键代码如下:
@PostMapping("doSaveObject")
public JsonResult doSaveObject(SysMenu entity){
sysMenuService.saveObject(entity);
return new JsonResult("save ok");
}
第二步:打开Postman工具通过post请求向服务端提交菜单信息
执行send操作,假如出现如下结果形式则表示数据保存成功:
说明:假如控制层方法参数使用了@RequestBody描述,关键代码如下:
@PostMapping("doSaveObject")
public JsonResult doSaveObject(@RequestBody SysMenu entity){
sysMenuService.saveObject(entity);
return new JsonResult("save ok");
}
我们使用postman提交数据时,需要向服务端提交json格式数据,如图所示:
客户端设计及实现
(省略)
菜单修改页面数据的呈现
列表页面数据的绑定
菜单列表页面数据呈现实现,底层的数据绑定,绑定的目的是为后续获取提供便利。
其中,具体的JS文件如图所示,
修改按钮事件处理
具体代码(省略),可参考menu_list.html,menu_edit.html页面
菜单页面数据更新设计及实现
服务端设计和实现
Dao 方法设计和实现
第一步:在SysMenuDao接口中添加菜单数据更新方法,关键代码如下:
int updateObject(SysMenu entity);
第二步:在SysMenuMapper.xml文件中添加SQL更新映射,关键代码如下:
<update id="updateObject" parameterType="com.cy.pj.sys.pojo.SysMenu">
update sys_menus
set name=#{name},
type=#{type},
url=#{url},
sort=#{sort},
parentId=#{parentId},
permission=#{permission},
modifiedTime=now(),
modifiedUser=#{modifiedUser}
where id=#{id}
</update>
Service 方法设计和实现
第一步:在SysMenuService接口中添加菜单更新方法,关键代码如下:
int updateObject(SysMenu entity);
第二步:在SysMenuServiceImpl类中添加菜单更新方法的具体实现,关键代码如下:
public int updateObject(SysMenu entity){
return sysMenuDao.updateObject(entity);
}
Controller方法设计及实现
在SysMenuController中添加更新方法,关键代码如下:
@RequestMapping("doUpdateObject")
public JsonResult doUpdateObject(SysMenu entity){
sysMenuService.updateObject(entity);
return new JsonResult("update ok")
}
假如客户端向服务端传递的数据为json格式的数据,服务端方法参数需要使用@RequestBody进行描述,关键代码如下:
@PutMapping("doUpdateObject")
public JsonResult doUpdateObject(@RequestBody SysMenu entity){
sysMenuService.updateObject(entity);
return new JsonResult("update ok")
}
启动服务,进行菜单数据更新测试.
客户端设计和实现
省略
总结(Summary)
重难点分析
- 数据表的设计(表中有哪些字段)
- 菜单列表数据查询(左外关联)
- 菜单数据封装的设计(SysMenu,Node,JsonResult)
- 菜单数据的呈现(TreeGrid,Ztree)-知道有这样的插件即可(用时参考官网demo).
FAQ分析
- 如何理解项目中的菜单?
(菜单是资源的外在表现形式,通过菜单操作系统资源) - 菜单表中都有哪些字段?
- 菜单数据的呈现是如何实现的?
- 菜单数据写入到数据库是乱码?
(首先检测服务端收到的是什么?url中配置编码了吗)
3.角色管理
角色业务分析及设计
背景分析
任何一个权限管理子系统,它的设计中都会有一个角色管理的模块,角色通常会与系统资源有一个对应关系,不同角色可能有不同或相同资源的访问权限.
核心业务分析
在权限管理子系统中,本质上控制的是用户对资源的访问权限,但是在授权时,一般会先将资源(菜单)的访问权限授予角色(Role),然后再将角色授予用户,此时用户就相当于拥有了某些资源的访问权限.具体角色表设计如下:
CREATE TABLE `sys_roles` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL COMMENT '角色名称',
`note` varchar(500) DEFAULT NULL COMMENT '备注',
`createdTime` datetime DEFAULT NULL COMMENT '创建时间',
`modifiedTime` datetime DEFAULT NULL COMMENT '修改时间',
`createdUser` varchar(20) DEFAULT NULL COMMENT '创建用户',
`modifiedUser` varchar(20) DEFAULT NULL COMMENT '修改用户',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8 COMMENT='角色';
角色和菜单是一种多对多关系(Many2Many),对于多对多的关系,其关系的维护方通常会在关系表中进行实现,其关系表的设计如下:
CREATE TABLE `sys_role_menus` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`menu_id` int(11) DEFAULT NULL COMMENT 'ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1400 DEFAULT CHARSET=utf8 COMMENT='角色与菜单对应关系';
业务原型设计分析
角色列表页面,如图所示:
角色添加页面,如图所示:
业务核心API设计分析
- POJO: SysRole
- Dao: SysRoleDao,SysRoleMenuDao
- Service: SysRoleService,SysRoleServiceImpl
- Controller: SysRoleController
角色列表数据查询设计及实现
业务分析
将数据库角色表中的数据,基于分页查询的需求查询数据,并在页面上进行呈现
服务端设计及实现
Pojo对象设计及实现
定义SysRole类型,基于此类型封装从数据库查询到的角色信息.关键代码如下:
package com.cy.pj.sys.pojo;
@Data
public class SysRole implements Serializable{
private static final long serialVersionUID = 1937011637090526629L;
private Integer id;
private String name;
private String note;
private Date createdTime;
private Date modifiedTime;
private String createdUser;
private String modifiedUser;
}
Dao对象设计及实现
第一步:创建SysRoleDao,并定义角色分页查询方法,关键代码如下\
package com.cy.pj.sys.dao;
@Mapper
public interface SysRoleDao{
int getRowCount(String name);
List<SysRole> findPageObjects(String name,
Integer startIndex,Integer pageSize);
}
第二步:创建SysRoleMapper.xml文件,并定义SQL分页查询映射,关键代码如下:
<mapper namespace="com.cy.pj.sys.dao.SysRoleDao">
<!--SQL共性定义-->
<sql i="queryWhereId">
from sys_roles
<where>
<if test="name!=null and name!=''">
name like concat("%",#{name},"%")
</if>
</where>
</sql>
<!--统计总记录数-->
<select id="getRowCount" resultType="int">
select count(*)
<include refid="queryWhereId"/>
</select>
<!--分页查询当前页记录-->
<select id="findPageObjects" resultType="com.cy.pj.sys.pojo.SysRole">
select *
<include refid="queryWhereId"/>
order by createdTime desc
limit #{startIndex},#{pageSize}
</select>
</mapper>
Service对象设计及实现
第一步:定义SysRoleService接口以及分页查询方法,关键代码如下
package com.cy.pj.sys.service;
public interface SysRoleService{
PageObject<SysRole> findPageObjects(String name,Integer pageCurrent);
}
第二步:定义SysRoleService接口实现并重写分页查询方法,关键代码如下
package com.cy.pj.sys.service.impl;
@Service
public class SysRoleServiceImpl implements SysRoleService{
@Autowired
private SysRoleDao sysRoleDao;
public PageObject<SysRole> findPageObjects(String name,
Integer pageCurrent){
if(pageCurrent==null||pageCurrent<1)
throw new IllegalArgumentException("页码值不正确");
int rowCount=sysRoleDao.getRowCount(name);
if(rowCount==0)
throw new ServiceException("记录可能已经不存在");
int pageSize=3;
int startIndex=(pageCurrent-1)*pageSize;
List<SysRole> records=
sysRoleDao.findPageObjects(name,startIndex,pageSize);
return new PageObject<>(rowCount,records,pageSize,pageCurrent);
}
}
Controller对象设计及实现
第一步:定义SysRoleController及处理分页查询请求的方法,关键代码如下:
package com.cy.pj.sys.controller;
@RestController
@RequestMapping("/role/")
public class SysRoleController{
@Autowired
private SysRoleService sysRoleService;
@GetMapping("doFindPageObjects")
public JsonResult doFindPageObjects(String name,Integer pageCurrent){
PageObject<SysRole> pageObject=
sysRoleService.findPageObjects(name,pageCurrent);
return new JsonResult(pageObject);
}
}
第二步:启动服务,进行访问测试(在浏览器或postman输入访问url,检测输出结果),如图所示:
客户端设计及实现
(省略)
角色添加页面数据的保存实现
业务分析
在角色列表页面,点击添加按钮时进入角色添加页面,在添加页面中,输入要新增的角色信息以及要为角色赋予的权限信息(角色可访问的菜单),表单填写完成以后,点击save按钮,将数据异步提交到服务端.
服务端设计及实现
Dao 方法设计及定义
第一步:在SysRoleDao中添加用于新增角色信息的方法,关键代码如下:
int insertObject(SysRole entity);
第二步:在SysRoleMapper.xml文件中添加用于新增角色的SQL映射,关键代码如下
<insert id="insertObject" parameterType="com.cy.pj.sys.pojo.SysRole"
keyProperty="id"
useGeneratedKeys=true>
insert into sys_roles
(name,note,createdTime,modifiedTime,createdUser,modifiedUser)
values
(#{name},#{note},now(),now(),#{createdUser},#{modifiedUser})
</insert>
其中:
- useGeneratedKeys 表示要使用自增的主键值.
- keyProperty 表示要将自增主键值赋值给参数对象的哪个属性
第三步:创建角色菜单关系表对应的Dao接口及新增方法,关键代码如下
package com.cy.pj.sys.dao;
@Mapper
public interface SysRoleMenuDao{
int insertObjects(Integer roleId,Integer[] menuIds);
}
第四步:创建角色菜单关系表对应的映射文件以及SQL映射,关键代码如下:
在指定目录添加SysRoleMenuMapper.xml文件以及SQL映射
<mapper namespace="com.cy.pj.sys.dao.SysRoleMenuDao">
<insert id="insertObjects">
insert into sys_role_menus
(role_id,menu_id)
values
<foreach collection="menuIds" separator="," item="menuId">
(#{roleId},#{menuId})
</foreach>
</insert>
</mapper>
Service 方法设计及实现
第一步:在SysRoleService接口中添加新增角色信息的方法,关键代码如下:
int saveObject(SysRole entity,Integer[] menuIds);
第二步:在SysRoleServiceImpl中添加SysRoleMenuDao属性,关键代码如下:
@Autowired
private SysRoleMenuDao sysRoleMenuDao;
第三步:在SysRoleServiceImpl中重写新增角色的方法,关键代码如下:
public int saveObjects(SysRole entity,Integer[] menuIds){
//1.参数校验(自己校验)
//2.保存角色自身信息
int rows=sysRoleDao.insertObject(entity);
//3.保存角色和菜单关系数据
sysRoleMenuDao.insertObjects(entity.getId(),menuIds);
return rows;
}
Controller方法设计及实现
第一步:在SysRoleController中定义,处理角色新增请求的方法,关键代码如下:
@PostMapping("doSaveObject")
public JsonResult doSaveObject(SysRole entity,Integer []menuIds){
sysRoleService.saveObject(entity,menuIds);
return new JsonResult("save ok");
}
假如客户端提交的数据为json格式,可以将方法中的参数封装到一个pojo对象中,然后使用@RequestBody注解描述(一个方法中只能使用一个@RequestBody注解)即可。
第二步:打开Postman进行post请求测试
客户端设计及实现
省略
角色修改页面数据加载及呈现
业务分析
当点击角色列表页面的修改按钮时,我们要做哪些业务呢?如图所示:
基于页面上的需求分析,我们在点击修改按钮时,需要基于id查询角色自身信息以及角色对应的关系数据,对于这样的需求,首先确定我们的数据来源(两张表),基于数据来源,我们给出解决方案如下:
方案1:业务层向数据层发起两次单表查询请求,将两次查询结果在业务层进行封装。(最简单)
方案2:业务层发起一次查询请求,数据层进行表关联查询?(两个表需要有对应的关系字段)
方案3:业务层发起一次查询请求,数据层进行表嵌套查询? (数据层多次查询)
服务端设计及实现
Pojo 对象设计及实现
定义SysRoleMenu对象,用于封装基于id查询到的角色和菜单id信息,关键代码如下:
package com.cy.pj.sys.pojo;
@Data
public class SysRoleMenu implements Serializable{
private static final long serialVersionUID = -2671028987524519218L;
private Integer id;
private String name;
private String note;
private List<Integer> menuIds;
}
Dao 方法设计及实现
方案1:数据层两次单表查询(依次查询角色表和角色菜单关系表)
第一步:在SysRoleDao中定义基于id查询角色信息的方法,关键代码如下:
SysRoleMenu findById(Integer id);
第二步:在SysRoleMapper中添加基于id查询角色自身信息的sql映射,关键代码如下:
<select id="findById" resultType="com.cy.pj.sys.pojo.SysRoleMenu">
select id,name,note
from sys_roles
where id=#{id}
</select>
第三步:在SysRoleMenuDao中添加基于角色id查询菜单id的方法,关键代码如下
List<Integer> findMenuIdsByRoleId(Integer roleId);
第四步:在SysRoleMenuMapper中添加基于角色id查询菜单id的sql映射,关键代码如下:
<select id="findMenuIdsByRoleId" resultType="int">
select menu_id
from sys_role_menus
where role_id=#{roleId}
</select>
方案2:数据层进行一次多表关联查询
第一步:在SysRoleDao中定义基于id查询角色信息的方法,关键代码如下(假如已有则无需再定义):
SysRoleMenu findById(Integer id);
第二步:在SysRoleMapper中添加基于角色id查询角色和菜单id的SQL映射,关键代码如下:
<select id="findById" resultMap="sysRoleMenu">
select r.id,r.name,r.note,rm.menu_id
from sys_roles r left join sys_role_menus rm
on r.id=rm.role_id
where r.id=#{id}
</select>
<resultMap id="sysRoleMenu" type="com.cy.pj.sys.pojo.SysRoleMenu">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="note" column="note"/>
<!--one2many映射时会使用collection元素-->
<collection property="menuIds" ofType="integer">
<result column="menu_id"/>
</collection>
</resultMap>
映射分析,如图所示:
说明:resultMap元素是mybatis中实现高级查询映射的一个元素,当表中字段名与pojo类中的属性不同或者是多表查询,嵌套查询时,一般都会使用resultMap元素进行自定义映射。
Service 方法设计及实现
方案1:业务层发起多次单表查询(对应数据层方案1:两次单表查询)
第一步:在SysRoleService接口中添加基于角色id查询角色以及菜单id的方法,关键代码如下:
SysRoleMenu findById(Integer id);
第二步:在SysRoleServiceImpl类中添加基于角色id查询角色以及菜单id的方法实现,关键代码如下:
public SysRoleMenu findById(Integer id){
SysRoleMenu roleMenu=sysRoleDao.findById(id);//id,name,note
if(roleMenu==null)
throw new ServiceException("记录可能已经不存在");
List<Integer> menuIds=sysRoleMenuDao.findMenuIdsByRoleId(id);
roleMenu.setMenuIds(menuIds);
return roleMenu;
}
方案2,业务层发起一次查询,数据层表关联或表嵌套查询 (对应数据层的方案2和方案3)
public SysRoleMenu findById(Integer id){
SysRoleMenu roleMenu=sysRoleDao.findById(id);//id,name,note
if(roleMenu==null)
throw new ServiceException("记录可能已经不存在");
return roleMenu;
}
Controller 方法设计及实现
第一步:在SysRoleController中添加基于id查询角色和菜单id的方法,关键代码如下:
@GetMapping("doFindById/{id}")
public JsonResult doFindById(@PathVariable Integer id){
return new JsonResult(sysRoleService.findById(id));
}
第二步:启动服务基于浏览器或postman进行访问测试,如图所示
客户端设计及实现
原型设计分析
当点击角色列表页面上的更新按钮时,基于id查询角色以及角色对应的菜单id,然后更新到角色编辑页面,如图所示:
将服务端响应到客户端的数据,更新到页面上,其核心js代码如下:
角色修改页面数据更新实现
业务分析
获取修改页面中的表单数据,然后将数据异步提交到服务端,执行更新操作。
服务端设计及实现
Dao 方法设计及实现
第一步:在SysRoleDao接口中定义更新角色自身信息的方法,关键代码如下:
int updateObject(SysRole entity);
第二步:在SysRoleMapping中定义更新角色信息的SQL映射,关键代码如下:
<update id="updateObject">
update sys_roles
set name=#{name},
note=#{note},
modifiedTime=now(),
modifiedUser=#{modifiedUser}
where id=#{id}
</update>
第三步:在SysRoleMenuDao中定义更新关系数据方法,关键代码如下:
对于one2many关系数据的更新,一般是先删除原有关系数据,再添加新的关系数据,所以dao
中要定义基于角色id删除角色菜单关系数据的方法
@Delete("delete from sys_role_menus where role_id=#{roleId}")
int deleteObjectsByRoleId(Integer roleId);
对于添加新的关系数据的方法及映射,在角色添加模块已经实现,这里不再重复实现。
Service 方法设计及实现
第一步:在SysRoleService中添加更新角色信息的方法,关键代码如下:
int updateObject(SysRole entity,Integer[] menuIds);
第二步:在SysRoleServiceImpl类中添加更新角色信息的方法实现,关键代码如下:
public int updateObject(SysRole entity,Integer[] menuIds){
int rows=sysRoleDao.updateObject(entity);
if(rows==0)
throw new ServiceException("记录可能已经不存在了");
sysRoleMenuDao.deleteObjectsByRoleId(entity.getId());
sysRoleMenuDao.insertObjects(entity.getId(),menuIds);
return rows;
}
Controller 方法设计及实现
第一步:在SysRoleController中添加更新角色信息的方法,关键代码如下:
@RequestMapping("doUpdateObject")
public JsonResult doUpdateObject(SysRole entity,Integer[]menuIds){
sysRoleService.updateObject(entity,menuIds);
return new JsonResult("update ok");
}
第二步:启动服务进行,借助post进行更新测试,如图所示:
客户端设计及实现
(省略)
总结(Summary)
重难点分析
- 角色模块表设计
- 角色模块与菜单模块的关系
- 多对多关系数据的维护
- 角色和角色对应的菜单数据的保存
- 基于id查询角色以及角色对应的菜单id(三种方案)
FAQ 分析
- Insert操作执行时,如何获取表中自动生成的主键值?
- resultMap 元素要解决什么问题?(完成查询的高级映射-自定义映射,one2many等关系映射)
- 业务层需要的数据来自多张表时,具体的查询方案是怎样的?
- 修改角色信息时,角色和菜单的关系数据如何修改?
- 执行查询时,客户端没有呈现服务端响应的数据?(先检查控制层数据,标准,…)
- 执行save操作时,数据库中没有看到保存的数据?(先检查控制层是否收到了数据)
BUG 分析
- 方法参数中使用多个@ResponseBody注解描述参数,无法获取客户端提交的数据?
- 405异常,客户端请求方式与服务端处理请求的方式不匹配
4.AOP 技术简介
背景分析
对于一个业务而言,我们如何在不修改源代码的基础上对对象功能进行拓展,例如现有一个公告(通知)业务:
interface NoticeService{
boolean send(String notice);
}
public class NoticeServiceImpl implements NoticeService{
public boolean send(String notice){
System.out.println(notice);
return true;
}
}
需求:
基于OCP(开闭原则-对扩展开放对修改关系)设计方式对NoticeServiceImpl类的功能进行扩展,例如在send业务方法执行之前和之后输出一下系统时间.
方案1:基于继承方式实现其功能扩展,关键设计如下:
public class CglibLogNoticeService extends NoticeServiceImpl{
public boolean send(String notice){
System.out.println("Start:"+System.currentTimeMillis());
super.send(notice);
System.out.println("After:"+System.currentTimeMillis());
return true;
}
}
测试类如下:
public class NoticeServiceTests{
public static void main(String[] args){
NoticeService ns=new CglibLogNoticeService();
ns.send("hello");
}
}
基于继承方式实现功能扩展的优势,劣势分析:
- 优势: 简单,容易理解.
- 劣势:不够灵活(只能直接一个接口下的子类)和稳定(父类一旦修改了其方法,所有子类都要改.)
方案2:基于组合方式实现其功能扩展,关键代码设计如下:
public class JdkLogNoticeService implements NoticeService{
private NoticeService noticeService;//has a
public JdkLogNoticeService(NoticeService noticeService){
this.noticeService=noticeService;
}
public boolean send(String notice){
System.out.println("Start:"+System.currentTimeMillis());
this.noticeService.send(notice);
System.out.println("After:"+System.currentTimeMillis());
return true;
}
}
测试类
public class NoticeServiceTests{
public static void main(String[] args){
NoticeService ns=
new JdkLogNoticeService(new NoticeServiceImpl());
ns.send("hello");
}
}
基于组合方式实现功能扩展的优势,劣势分析:
- 优势: 灵活(可以为指定接口下的所有实现类做功能扩展),稳定(组合的具体对象发生变化,不会影响当前类)
- 劣势:相对继承而言不容易理解.
总结:
无论是继承,还是组合都是基于OCP方式实现了对象功能扩展,都有相应的优缺点,并且我们都要自己去写这些子类或兄弟类,在这些类中调用目标对象(父类或兄弟类对象)的方法以及扩展业务逻辑.对于这样的模板代码我们能否进行简化呢?例如.由框架实现其共性(创建目录类型的子类类型或兄弟类型),特性交给用户自己实现.
AOP 是什么?
AOP(Aspect Oriented Programming)是面向切面编程,是一种设计思想,它要在不改变原有目标对象的基础上,为目标对象基于动态织入的特定方式(可以是编译是的动态,也可以运行时的动态)进行功能扩展.我们可以将设计思想理解为OOP(面向对象编程)思想的补充和完善,OOP强调的一种静态过程(一个项目由哪些子系统构成,一个子系统有哪些模块,一个模块又由哪些对象构成,一个对象又有哪些属性和方法).AOP是一个动态过程,它要为设计好的对象在动态编译或运行时做服务增益,例如,记录日志,事务增强,权限控制等,如图所示:
AOP 应用原理初步分析
AOP可以在系统启动时为目标类型创建子类或兄弟类型对象,这样的对象我们通常会称之为动态代理对象.如图所示:
其中:
为目标类型(XxxServiceImpl)创建其代理对象方式有两种(先了解):
第一种方式:借助JDK官方API为目标对象类型创建其兄弟类型对象,但是目标对象类型需要实现相应接口.
第二种方式:借助CGLIB库为目标对象类型创建其子类类型对象,但是目标对象类型不能使用final修饰.
AOP 相关术语概要分析
- 切面对象(Aspect):封装了扩展业务逻辑的对象,在spring中可以使用@AspectJ描述.
- 切入点(Pointcut): 定义了切入扩展业务逻辑的一些方法的集合(哪些方法运行时切入扩展业务),一般会通过表达式进行相关定义,一个切面中可以定义多个切入点的定义.
- 连接点(JoinPoint):切入点方法集合中封装了某个正在执行的目标方法信息的对象,可以通过此对象获取具体的目标方法信息,甚至去调用目标方法.
- 通知(Advice):切面(Aspect)内部封装扩展业务逻辑的具体方法对象,一个切面中可以有多个通知(例如@Around).
其中,切入点与连接点的分析,如图所示:
说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全检查过程看成是通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。
Spring中AOP快速入门
业务描述
在项目中定义一个日志切面,通过切面中的通知方法为目标业务对象做日志功能增强.
添加AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring 切面对象定义
在springboot工程中,切面对象需要使用@Aspect,关键代码如下:
package com.cy.pj.sys.service.aspect;
/**
* @Aspect 注解描述的类型为切面对象类型,此切面中可以定义多个切入点和通知方法.
*/
@Slf4j
@Aspect
@Component
public class SysLogAspect {
/**
* @Pointcut注解用于定义切入点
* bean("spring容器中bean的名字")这个表达式为切入点表达式定义的一种语法,
* 它描述的是某个bean或多个bean中所有方法的集合为切入点,这个形式的切入点
* 表达式的缺陷是不能精确到具体方法的.
*/
@Pointcut("bean(sysUserServiceImpl)")
public void doLog(){}//此方法只负责承载切入点的定义
/**
* @Around注解描述的方法,可以在切入点执行之前和之后进行业务拓展,
* 在当前业务中,此方法为日志通知方法
* @param joinPoint 连接点对象,此对象封装了要执行的切入点方法信息.
* 可以通过连接点对象调用目标方法,这里的ProceedingJoinPoint类型只能应用于@Around描述的方法参数中
* @return 目标方法的执行结果
* @throws Throwable
*/
@Around("doLog()")
public Object doAround(ProceedingJoinPoint jp)throws Throwable{
log.info("Start:{}",System.currentTimeMillis());
try {
Object result = jp.proceed();//执行目标方法(切点方法中的某个方法)
log.info("After:{}",System.currentTimeMillis());
return result;//目标业务方法的执行结果
}catch(Throwable e){
e.printStackTrace();
log.error("Exception:{}",System.currentTimeMillis());
throw e;
}
}
}
启动项目进行访问测试,例如访问sysUserServiceImpl对象中的方法,检测其日志输出.
Spring 切面工作原理分析
当我们切面内部,切入点对应的目标业务方法执行时,底层会通过代理对象访问切面中的通知方法,进而通过通知方法为目标业务做功能增强.
项目中JDK动态代理对象应用分析,如图所示:
其中:springboot工程默认的AOP代理为CGLIB代理,假如希望是JDK代理,则需要在springboot的配置文件中进行如下配置:
spring:
aop:
proxy-target-class: false
项目中CGLIB动态代理对象应用分析,如图所示:
Spring中AOP技术应用进阶
AOP 通知类型
Spring框架AOP模块定义通知类型,有如下几种:
- @Around (所有通知中优先级最高的通知,可以在目标方法执行之前,之后灵活进行业务拓展.)
- @Before (目标方法执行之前调用)
- @AfterReturning (目标方法正常结束时执行)
- @AfterThrowing (目标方法异常结束时执行)
- @After (目标方法结束时执行,正常结束和异常结束它都会执行)
基于对通知类型的理解,通过案例分析通知的具体执行时间点,关键代码分析
package com.cy.pj.sys.service.aspect;
@Aspect
@Component
public class SysTimeAspect {
@Pointcut("bean(sysRoleServiceImpl)")
public void doTime(){}
@Before("doTime()")
public void doBefore(JoinPoint jp){
System.out.println("@Before");
}
@After("doTime()")
public void doAfter(){
System.out.println("@After");
}
@AfterReturning("doTime()")
public void doAfterReturning(){
System.out.println("@AfterReturning");
}
@AfterThrowing("doTime()")
public void doAfterThrowing(){
System.out.println("@AfterThrowing");
}
//最重要,优先级也是最高
@Around("doTime()")
public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
try {
System.out.println("@Around.before");
Object result = joinPoint.proceed();
System.out.println("@Around.AfterReturning");
return result;
}catch(Exception e){
System.out.println("@Around.AfterThrowing");
e.printStackTrace();
throw e;
}finally {
System.out.println("@Around.after");
}
}
}
其中,实际项目中并不是要将所有通知方法都定义一遍,会结合具体业务添加通知方法.
AOP切入点表达式
在Spring工程中对于切入点表达式,可以分成两大类型:
- 粗粒度切入点表达式定义(不能精确到具体方法),例如bean,within表达式
- 细粒度切入点表达式定义(可以精确到具体方法),例如execution,@annotation表达式
粗粒切入点表达式
- bean(“bean的名字”) 表达式案例分析
1) bean(sysUserServiceImpl),sysUserServiceImpl类中所有方法集合为切入点
2) bean(*ServiceImpl),以ServiceImpl结尾的所有方法集合为切入点
- within (“包名.类型”) 表达式案例分析
1) within(com.pj.service.SysUserServiceImpl),SysUserServiceImpl类中所有方法集合为切入点
2) within(com.pj.service.*),com.pj.service包下所有类中的方法集合为切入点
3) within(com.pj.service..*),com.pj.service包以及子包中所有类中方法的集合为切入点
细粒度切入点表达式
- execution(“返回值 类全名.方法名(参数列表)”) 表达式案例分析
1) execution(int com.cy.pj.service.SysUserService.validById(Integer,Integer))
2) execution(* com.cy.pj.service..*.*(..))
- @annotation(“注解的类全名”)表达式案例分析
1) @annotation(com.annotation.RequiredCache),由RequiredCache注解描述的方法为缓存切入点方法
2) @annotation(com.annotation.RequiredLog),由RequiredLog注解描述的方法为日志切入点方法
AOP切面优先级设置分析
当项目中有多个切面时,并且对应着相同的切入点,假如切面中的通知方法的执行需要有严格顺序要求,此时我们需要设置切面的优先级。这个优先级可以通过@Order(数字优先级)注解进行描述,数字越小优先级越高。例如:
设置日志切面优先级,关键代码如下:
@Order(2)
@Aspect
@Component
public class SysLogAspect{}
设置缓存切面优先级,关键代码如下:
@Order(1)
@Aspect
@Component
public class SysCacheAspect{}
假如默认没有指定优先级,默认是最低的优先级。相同切入点下的多个切面会形成一个切面链。
Cache切面案例设计及实现
我们项目中有很多查询业务,为了提高其查询性能,减少对数据库访问,可以将业务层查询到数据存储到缓存中,下次查询从缓存中去查询,这样的切面及缓存设计如何实现?
第一步:在common项目中定义描述切入点方法的注解,关键代码如下:
package com.cy.pj.common.annotation;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredCache{
}
package com.cy.pj.common.annotation;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClearCache{
}
第二步:在common项目中定义缓存切面SysCacheAspect,关键代码如下:
package com.cy.pj.common.aspect;
@Order(1)
@Aspect
@Component
public class SysCacheAspect{
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)")
public void doCache(){}
@Pointcut("@annotation(com.cy.pj.common.annotation.ClearCache)")
public void doClearCache(){}
@AfterReturning("doClearCache()")
public void doAfterReturning(){
//清除缓存中数据
}
@Around("doCache()")
public Object doCacheAround(ProceedingJoinPoint jp)throws Throwable{
//1.从缓存去取数据,假如缓存中有则直接return缓存数据
//2.缓存中没有则执行目标方法查询数据
//3.将查询结果存储到缓存对象
return null;//查询到的结果
}
}
第三步:通过注解在业务类中定义切入点方法,关键代码如下:
以部门业务为例进行分析。
@RequiredCache
List<Map<String,Object> findObjects(){}//查询数据
@ClearCache
int updateObject(SysDept entity){}//更新数据的方法
用户日志切面案例设计及实现
获取登录用户行为日志,并将日志信息写入到数据库。
第一步:在SysLogDao中添加新增日志的方法,关键代码如下:
int insertObject(SysLog entity);
第二步:在SysLogMapper映射文件中添加新增日志的SQL映射,关键代码如下:
<insert id="insertObject">
insert into sys_logs
(username,ip,operation,method,params,time,createdTime)
values
(#{username},#{ip},#{operation},#{method},#{param},#{time},now())
</insert>
第三步:在SysLogService接口中添加新增日志的方法,关键代码如下:
void saveObject(SysLog entity);
第四步:在SysLogServiceImpl类中添加新增日志的方法,关键代码如下:
public void saveObject(SysLog entity){
sysLogDao.insertObject(entity);
}
第五步:定义RequiredLog注解,通过此注解定义切入点方法以及操作名称,关键代码如下:
package com.cy.pj.common.annotation;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
String value() default "operation";
}
第六步:修改SysLogAspect切面对象,添加获取并新增日志的业务逻辑,关键操作及代码如下:
修改切入点表达式,基于注解方式进行定义(更加灵活)
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
public void doLog(){}
添加用来进行保存日志信息的方法
@Autowired
private SysLogService sysLogService;
//记录用户行为日志
private void saveUserLog(ProceedingJoinPoint jp,long time)
throws Exception {
//1.获取用户行为日志
//1.1获取登录用户名(没做登录时,可以先给个固定值)
String username="cgb";
//1.2获取ip地址
String ip= IPUtils.getIpAddr();
//1.3获取操作名(operation)-@RequiredLog注解中value属性的值
//1.3.1获取目标对象类型
Class<?> targetCls=jp.getTarget().getClass();
//1.3.2获取目标方法
MethodSignature ms= (MethodSignature) jp.getSignature();//方法签名
Method targetMethod=
targetCls.getMethod(ms.getName(),ms.getParameterTypes());
//1.3.3 获取方法上RequiredLog注解
RequiredLog annotation =
targetMethod.getAnnotation(RequiredLog.class);
//1.3.4 获取注解中定义操作名
String operation=annotation.value();
//1.4获取方法声明(类全名+方法名)
String classMethodName=targetCls.getName()+"."+targetMethod.getName();
//1.5获取方法实际参数信息
Object[]args=jp.getArgs();
String params=new ObjectMapper().writeValueAsString(args);
//2.封装用户行为日志
SysLog sysLog=new SysLog();
sysLog.setUsername(username);
sysLog.setIp(ip);
sysLog.setOperation(operation);
sysLog.setMethod(classMethodName);
sysLog.setParams(params);
sysLog.setTime(time);
//3.存储用户信息日志到数据库
sysLogService.saveObject(sysLog);
}
修改日志切面通知方法,在通知方法中调用save 日志的方法,关键代码如下:
@Around("doLog()")
public Object doAround(ProceedingJoinPoint jp)throws Throwable{
long t1=System.currentTimeMillis();
log.info("Start:{}",t1);
try {
Object result = jp.proceed();//执行目标方法(切点方法中的某个方法)
long t2=System.currentTimeMillis();
log.info("After:{}",t2);
saveUserLog(jp,t2-t1);
return result;//目标业务方法的执行结果
}catch(Throwable e){
e.printStackTrace();
log.error("Exception:{}",System.currentTimeMillis());
throw e;
}
}
Spring中AOP技术高级应用
Spring 中的异步操作
异步业务描述
当我们项目中的一些非核心业务运行时,影响到用户核心业务的响应时间,导致用户体验下降,可以将这些非业务放到新的线程中异步执行。
启动Spring中异步操作
在SpringBoot工程,可以在启动类的上面,添加启动异步操作的注解(@EnableAsync)描述,代码如下:
@EnableAsync
@SpringBootApplication
public class DbpmsApplication {
public static void main(String[] args) {
SpringApplication.run(DbpmsApplication.class, args);
}
}
说明,当启动类上添加了@EnableAsync注解描述时,再次运行启动类时底层会帮我们配置一个线程池
业务方法上执行异步操作
假如此时某个业务方法需要执行异步操作,可以使用@Async注解对方法进行描述,例如写日志的业务。
@Async
public void saveObject(SysLog entity){
sysLogDao.insertObject(entity);
}
其中,@Async注解描述的方法,在spring中会认为这是一个异步切入点方法, 在这个切入点方法执行时,底层会通过通知方法获取线程池中的线程,通过池中的线程调用切入点方法(底层默认池类型为ThreadPoolExecutor类型)。
Spring中异步线程池的配置
当springboot中默认的线程池配置,不满足我们实际项目需求时,我们可以对线程池进行自定义的配置,关键配置如下:
spring:
task:
execution:
pool:
core-size: 8
max-size: 256
keep-alive: 60000
queue-capacity: 256
thread-name-prefix: db-service-task-
其中:
1)core-size :核心线程数,当池中线程数没达到core-size的值时,每接收一个新的任务都会创建一个新线程,然后存储到池。假如池中线程数已经达到core-size设置的值,再接收新的任务时,要检测是否有空闲的核心线程,假如有,则使用空闲的核心线程执行新的任务。
2)queue-capacity:队列容量,假如核心线程数已达到core-size设置的值,并且所有的核心线程都在忙,再来新的任务,会将任务存储到任务队列。
3)max-size: 最大线程数,当任务队列已满,核心线程也都在忙,再来新的任务则会创建新的线程,但所有线程数不能超过max-size设置的值,否则可能会出现异常(拒绝执行)
4)keep-alive:线程空闲时间,假如池中的线程数多余core-size设置的值,此时又没有新的任务,则一旦空闲线程空闲时间超过keep-alive设置的时间值,则会被释放。
5)thread-name-prefix:线程名的前缀,项目中设置线程名的目的主要是为了对线程进行识别,一旦出现线程问题,可以更好的定位问题。
Spring 中的事务处理
事务业务描述
事务是一个不可分割逻辑工作单元,是一个业务,事务的处理通常要结合业务进行落地的实现。进而更好保证业务的完整性(要么都成功,要么都失败)。
Spring 中的事务控制
Spring中的事务控制,推荐在业务层基于AOP方式进行实现,这样可以将事务逻辑与业务逻辑进行更好的解耦,同时可以重用事务逻辑代码.进而简化大量事务模板代码的编写.
SpringBoot工程中的事务控制,可以直接在需要进行事务控制的类或业务方法上通过@Transaction注解描述即可,由此注解描述的方法为事务切入点方法,底层在切入点方法执行时会通过“通知方法”进行事务逻辑增强,示例代码如下:
@Transactional
public int updateObject(...){
....
}
当一个类中有个方法都需要事务控制,我们可以使用@Transactional注解对类进行描述,示例代码如下:
@Transactional
public class XxxServiceImpl implements XxxService{}
Spring 中的事务属性分析
在使用@Transactional描述类或方法时候,还可以指定一些事务属性,例如:
- readOnly 用于描述此事务是否为只读事务,默认值是false(表示不是只读事务),对于查询而言建议设置值为true.
- timeout 事务的超时时间,超过设置的时间会抛出异常,默认为-1(不超时,实际项目中建议设置超时时间)。
- rollbackFor 设置出现什么异常时要回滚事务(默认为RuntimeException)。
- isolation 设置事务并发执行时的隔离级别(隔离级别越高,数据正确性越好,但并发越差)。
5)propagation 设置事务的传播特性(默认值为Propagation.REQUIRED),不同业务对象之间的方法出现相互调用时,事务的执行策略。REQUIRED表示参与到调用者的事务中去,其它选项自行查阅。
案例分析:
@Transactional(readOnly = false,
rollbackFor = Throwable.class,
isolation = Isolation.READ_COMMITTED,
timeout = 5,
propagation= Propagation.REQUIRED)
public class XxxServiceImpl implements XxxService{}
说明,假如类和方法上都定义了事务特性,那方法上定义的事务特性优先级比较高。
总结(Summary)
重难点分析
- AOP 是什么,解决了什么问题,应用场景?
- AOP 编程基本步骤及实现过程(以基于 AspectJ 框架实现为例)。
- AOP 编程中的核心对象及应用关系。(代理对象,切面对象,通知,切入点)
- AOP 思想在 Spring 中的实现原理分析。(基于代理方式进行扩展业务的织入)
- AOP 应用中切入点表达式的定义方式(bean,@annotation)
- AOP 应用中通知的应用类型以及执行时间点(@Around,@Before,@AfterReturning,@AfterThrowing,@After)
- AOP 中切面的优先级设置?(@Order)
- AOP中异步操作应用场景及配置过程,基本原理.
- AOP中事务的控制方式,配置过程,基本原理.
FAQ分析
- AOP中切面的定义需要注意什么?(@Aspect,@Component)
- AOP 切面中都需要定义什么?(切入点,通知方法)
- AOP中切面可以有多个吗?(可以)
- AOP中多个切面可以作用于同一个切入点方法吗?(可以)
- AOP中定义切入点的方式有哪些?
- AOP中通知方法的应用类型哪些,分别是什么时间点执行?
- SpringBoot中默认AOP代理对象创建方式?(CGLIB-更加灵活)
- AOP方式进行异步操作时的一个基本步骤是什么?
- AOP方式的事务控制注解(@Transaction)都有什么属性?
BUG分析
- 切入点引入错误?
- 事务超时
5.Shiro框架在项目中的应用
Shiro 框架简介
Shiro 概述
Shiro 是Apache公司推出一个权限管理框架,其内部封装了项目中认证,授权,加密,会话等逻辑操作,通过Shiro框架可以简化我们项目权限控制逻辑的代码的编写。其认证和授权业务分析,如图所示:
Shiro 框架概要架构
Shiro 框架中主要通过Subject,SecurityManager,Realm对象完整认证和授权业务,其简要架构如下:
其中:
- Subject 此对象负责提交用户身份、权限等信息
- SecurityManager 负责完成认证、授权等核心业务
- Realm 负责通过数据逻辑对象获取数据库或文件中的数据。
Shiro 框架详细架构分析
Shiro 框架进行权限管理时,要涉及到的一些核心对象,主要包括:认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象以及 Realm 管理对象(领域对象:负责处理认证和授权领域的数据访问题)等,其具体架构如图- 所示:
其中:
- Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
- SecurityManager(安全管理器) :Shiro 的核心,用来协调管理组件工作。
- Authenticator(认证管理器):负责执行认证操作。
- Authorizer(授权管理器):负责授权检测。
- SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一
个强有力的 Session 体验。
- SessionDAO:代表 SessionManager 执行 Session 持久(CRUD)动作,它允
许任何存储的数据挂接到 session 管理基础上。
- CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
- Cryptography(加密管理器):提供了加密方式的设计及管理。
- Realms(领域对象):是 shiro 和你的应用程序安全数据之间的桥梁。
Shiro 框架基础配置
Shiro 依赖
在项目中添加Shiro相关依赖(参考官网http://shiro.apache.org/spring-boot.html),假如项目中添加过shiro-spring依赖,将shiro-spring依赖替换掉即可。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>
说明,添加完此依赖,直接启动项目会启动失败,还需要额外的配置。
Shiro 基本配置
第一步:创建一个Realm类型的实现类(基于此类通过DAO访问数据库),关键代码如下:
package com.cy.pj.sys.service.realm;
public class ShiroRealm extends AuthorizingRealm {
/**此方法负责获取并封装授权信息*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
return null;
}
/**此方法负责获取并封装认证信息*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
第二步:在项目启动类中添加Realm对象配置,关键代码如下:
@Bean
public Realm realm(){//org.apache.shiro.realm.Realm
return new ShiroRealm();
}
第三步:在启动类中定义过滤规则(哪些访问路径要进行认证才可以访问),关键代码如下:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition =
new DefaultShiroFilterChainDefinition();
LinkedHashMap<String,String> map=new LinkedHashMap<>();
//设置允许匿名访问的资源路径(不需要登录即可访问)
map.put("/bower_components/**","anon");//anon对应shiro中的一个匿名过滤器
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
//设置需认证以后才可以访问的资源(注意这里的顺序,匿名访问资源放在上面)
map.put("/**","authc");//authc 对应一个认证过滤器,表示认证以后才可以访问
chainDefinition.addPathDefinitions(map);
return chainDefinition;
}
第四步:配置认证页面(登录页面)
在spring的配置文件(application.yml)中,添加登录页面的配置,关键代码如下:
shiro:
loginUrl: /login.html
其中,login.html页面为项目中static目录定义好的一个页面。
第五步:启动服务进行访问测试
打开浏览器,输入http://localhost/doIndexUI检测是否会出现登录窗口,如图所示:
Shiro认证业务分析及实现
认证流程分析
当我们在登录页面,输入用户信息,提交到服务端进行认证,其中shiro框架的认证时序如图所示:
其中:
- token :封装用户提交的认证信息(例如用户名和密码)的一个对象。
- Subject: 负责将认证信息提交给SecurityManager对象的一个主体对象。
- SecurityManager是shiro框架的核心,负责完成其认证、授权等业务。
- Authenticator 认证管理器对象,SecurityManager继承了此接口。
- Realm 负责从数据库获取认证信息并交给认证管理器。
Shiro框架认证业务实现
第一步:在SysUserDao中定义基于用户名查询用户信息的方法,关键代码如下:
@Select("select * from sys_users where username=#{username}")
SysUser findUserByUsername(String username);
第二步:修改ShiroRealm中获取认证信息的方法,关键代码如下:
@Autowired
private SysUserDao sysUserDao;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取用户提交的认证用户信息
UsernamePasswordToken upToken=(UsernamePasswordToken) authenticationToken;
String username=upToken.getUsername();
//2.基于用户名查询从数据库用户信息
SysUser sysUser = sysUserDao.findUserByUsername(username);
//3.判断用户是否存在
if(sysUser==null) throw new UnknownAccountException();//账户不存在
//4.判断用户是否被禁用
if(sysUser.getValid()==0)throw new LockedAccountException();
//5.封装认证信息并返回
ByteSource credentialsSalt=
ByteSource.Util.bytes(sysUser.getSalt());
SimpleAuthenticationInfo info=
new SimpleAuthenticationInfo(
sysUser, //principal 传入的用户身份
sysUser.getPassword(),//hashedCredentials
credentialsSalt,//credentialsSalt
getName());
return info;//返回给认证管理器
}
第三步:在ShiroRealm中重谢获取凭证加密算法的方法,关键代码如下:
@Override
public CredentialsMatcher getCredentialsMatcher() {
HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");//加密算法
matcher.setHashIterations(1);//加密次数
return matcher;
}
第四步:在SysUserController中添加处理登录请求的方法,关键代码如下:
@RequestMapping("doLogin")
public JsonResult doLogin(String username,String password){
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
return new JsonResult("login ok");
}
第五步:统一异常处理类中添加shiro异常处理代码,关键如下:
@ExceptionHandler(ShiroException.class)
public JsonResult doShiroException(ShiroException e){
JsonResult r=new JsonResult();
r.setState(0);
if(e instanceof UnknownAccountException){
r.setMessage("用户名不存在");
}else if(e instanceof IncorrectCredentialsException){
r.setMessage("密码不正确");
}else if(e instanceof LockedAccountException){
r.setMessage("账户被锁定");
}else if(e instanceof AuthorizationException){
r.setMessage("没有权限");
}else{
r.setMessage("认证或授权失败");
}
return r;
}
第五步:在过滤配置中允许登录时的url匿名访问,关键代码如下:
...
map.put("/user/doLogin","anon");
...
第六步:再过滤配置中配置登出url操作,关键代码如下:
..
map.put("/doLogout","logout");//logout是shiro框架给出一个登出过滤器
...
第六步:启动服务器,进行登录访问测试
第七步:Shiro框架认证流程总结分析
- Step01:登录客户端(login.html)中的用户输入的登录信息提交SysUserController对象
- Step02:SysUserController对象基于doLogin方法处理登录请求.
- Step03:SysUserController中的doLogin方法将用户信息封装token中, 然后基于subject对象将token提交给SecurityManager对象。
- Step04:SecurityManager对象调用认证方法(authenticate)去完成认证,在此方法内部会调用ShiroRealm中的doGetAuthenticationInfo获取数据库中的用户信息,然后再与客户端提交的token中的信息进行比对,比对时会调用getCredentialsMatcher方法获取凭证加密对象,通过此对象对用户提交的token中的密码进行加密。
Shiro授权业务分析及实现
授权流程分析
已认证用户,在进行系统资源的访问时,我们还要检查用户是否有这个资源的访问权限。并不是所有认证用户都可以访问系统内所有资源,也应该是受限访问的,具体授权流程如图所示:
Shiro框架中的授权实现
第一步: 在SysMenuDao中定义查询用户权限标识的方法,关键代码分析:
Set<String> findUserPermissions(Integer userId);
第二步:在SysMenuMapper中添加查询用户权限标识的SQL映射,关键代码如下:
<select id="findUserPermissions" resultType="string">
select distinct permission
from sys_user_roles ur join sys_role_menus rm join sys_menus m
on ur.role_id=rm.role_id and rm.menu_id=m.id
where ur.user_id=#{userId} and m.permission is not null and trim(m.permission)!=''
</select>
第三步:修改ShiroRealm中获取权限并封装权限信息的方法,关键代码如下
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
//1.获取登录用户(登录时传入的用户身份是谁)
SysUser user= (SysUser) principalCollection.getPrimaryPrincipal();
//2.基于登录用户id获取用户权限标识
Set<String> stringPermissions=
sysMenuDao.findUserPermissions(user.getId());
//3.封装数据并返回
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.setStringPermissions(stringPermissions);
return info;//返回给授权管理器
}
第四步:定义授权切入点方法,示例代码如下:
在shiro框架中,授权切入点方法需要通过@RequiresPermissions注解进行描述,例如:
@RequiresPermissions("sys:user:update")
public int validById(Integer id,Integer valid){
int rows=sysUserDao.validById(id,valid);
if(rows==0)throw new ServiceException("记录可能已经不存在");
return rows;
}
其中, @RequiresPermissions注解中定义的内容为,访问此方法需要的权限.
第五步:启动服务进行访问测试和原理分析
在访问时首先要检测一下用户有什么权限,检测过程,先查询用户有什么角色,再查看角色有什么菜单的访问权限.
授权原理分析:(底层基于AOP实现)
- Step01 页面上用户通过菜单触发对服务端资源的访问.
- Step02 服务端Controller处理客户端的资源访问请求
- Step03 假如客户端请求访问的资源业务放上有@RequiresPermissions注解描述则底层Controller对象会调用Service的代理对象,代理对象会调用AOP中通知方法,在通知方法中获取@RequiresPermissions上的定义的权限标识.
- Step04 通过Subject 对象提交@RequiresPermissions注解中的授权标识给SecurityManager对象,此对象会调用ShiroRealm中的获取用户权限的方法,最终会将从数据权限信息与@RequiresPermissions中的定义的权限信息做一个比对.
Shiro认证授权业务进阶实现
呈现登录用户信息
Controller 方法定义及实现
修改PageController中的doIndex方法,关键代码如下:
@GetMapping("doIndexUI")
public String doIndexUI(Model model){
//获取登录用户信息(shiro框架给出的固定写法)
SysUser user=(SysUser)
SecurityUtils.getSubject().getPrincipal();
//存储登录用户信息
model.addAttribute("username", user.getUsername());
return "starter";
}
页面Thymeleaf 表达式应用
打开starter.html页面,找到用户名对应的位置,然后通过[[${}]]表达式获取服务端model中数据,呈现在页面上,关键代码如下:
<span class="hidden-xs" id="loginUserId">[[${username}]]</span>
用户菜单的动态化呈现
业务分析
我们希望不同登录用户,登录系统以后,看到的用户菜单是不一样的。登录用户只能看到自己可以访问的一些菜单选项。
服务端设计及实现
第一步:定义pojo对象存储用户菜单信息,关键代码如下:
package com.cy.pj.sys.pojo;
@Data
public class SysUserMenu implements Serializable{
private static final long serialVersionUID = -410105494012229800L;
private Integer id;
private String name;
private String url;
private List<SysUserMenu> childMenus;
}
第二步:在SysMenuDao中定义查询用户一级和二级菜单信息的方法,关键代码如下:
List<SysUserMenu> findUserMenus(Integer userId);
第三步:在SysMenuMapper中定义查询用户一级和二级菜单信息时对应的sql映射,关键代码如下:
<select id="findUserMenus" resultMap="sysUserMenu">
select p.id,p.name,p.url,c.id cid,c.name cname,c.url curl
from sys_menus p left join sys_menus c
on p.id=c.parentId
where p.parentId is null and c.id in (
select rm.menu_id
from sys_user_roles ur join sys_role_menus rm
on ur.role_id=rm.role_id
where ur.user_id=#{userId}
)
</select>
<resultMap id="sysUserMenu" type="com.cy.pj.sys.pojo.SysUserMenu">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="url" column="url"/>
<collection property="childMenus" ofType="com.cy.pj.sys.pojo.SysUserMenu">
<id property="id" column="cid"/>
<result property="name" column="cname"/>
<result property="url" column="curl"/>
</collection>
</resultMap>
第四步:在SysMenuService接口中添加查询用户菜单信息的方法,关键代码如下:
List<SysUserMenu> findUserMenus(Integer userId);
第五步:在SysMenuServiceImpl类中添加查询用户菜单信息的方法,关键代码如下:
public List<SysUserMenu> findUserMenus(Integer userId){
return sysMenuDao.findUserMenus(userId);
}
第六步:修改PageController中的doIndexUI方法,关键代码如下:
@Autowired
private SysMenuService sysMenuService;
@GetMapping("doIndexUI")
public String doIndexUI(Model model){
//获取登录用户信息(shiro框架给出的固定写法)
SysUser user=(SysUser)
SecurityUtils.getSubject().getPrincipal();
//存储登录用户信息
model.addAttribute("username", user.getUsername());
//查询用户菜单
List<SysUserMenu> userMenus=
sysMenuService.findUserMenus(user.getId());
model.addAttribute("userMenus", userMenus);
return "starter";
}
客户端设计及实现
第一步:修改starter页面菜单呈现部分的内容,关键代码如下:
<li class="treeview" th:each="p:${userMenus}">
<a href="#"><i class="fa fa-link"></i>
<span>[[${p.name}]]</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li th:each="c:${p.childMenus}">
<a th:onclick="doLoadRS([[${c.url}]])">[[${c.name}]]</a></li>
</ul>
</li>
第二步:添加菜单事件处理函数,关键代码如下:
function doLoadRS(url){
$("#mainContentId").load(url);
}
第三步:启动服务进行访问测试:
。。。。。
密码修改业务的设计及实现
修改登录用户密码(参考课前资料用户模块)-作业
总结(Summary)
重难点分析
- Shiro的基本配置(参考官网)
- Shiro的认证流程及实现过程
- Shiro的授权流程及实现过程
FAQ 分析
- Shiro框架的核心组件?(Subject,SecurityManager,Realm)
- Shiro框架中的认证流程是怎样的?(底层基于什么机制去实现的)
- Shiro框架中的授权实现原理是怎样的?(AOP)