本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方 法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下:
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=1 DEFAULT CHARSET=utf8 COMMENT='系统 日志';
1.2 原型设计说明
基于用户需求,实现静态页面(html/css/js),通过静态页面为用户呈现基本需求实现, 如图-1所示。
说明:假如客户对此原型进行了确认,后续则可以基于此原型进行研发。
业务描述与设计实现 基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法 分别用于返回日志列表页面,日志分页页面。
关键代码设计与实现 第一步:在PageController中定义返回日志列表的方法。代码如下:
@RequestMapping("log/log_list")
public String doLogUI(){
return "sys/log_list";
}
第二步:在PageController中定义用于返回分页页面的方法。代码如下:
@RequestMaping("doPageUI")
public String doPageUI(){
return "common/page";
}
业务描述与设计
首 先 准 备 日 志 列 表 页 面 (/templates/pages/sys/log_list.html) ,然后在 starter.html 页面中点击日志管理菜单时异步加载日志列表页面。
关键代码设计与实现
找到项目中的 starter.html 页面,页面加载完成以后,注册日志管理菜单项的点击 事件,当点击日志管理时,执行事件处理函数。关键代码如下:
$(function(){
doLoadUI("load-log-id","log/log_list")
})
function doLoadUI(id,url){
$("#"+id).click(function(){
$("#mainContentId").load(url);
});
}
其中,load 函数为 jquery 中的 ajax 异步请求函数。
业务描述与设计实现
当日志列表页面加载完成以后异步加载分页页面(page.html)。
关键代码设计与实现:
在 log_list.html 页面中异步加载 page 页面,这样可以实现分页页面重用,哪里需 要分页页面,哪里就进行页面加载即可。关键代码如下:
$(function(){
$("#pageId").load("doPageUI");
});
说明:数据加载通常是一个相对比较耗时操作,为了改善用户体验,可以先为用户呈 现一个页面,数据加载时,显示数据正在加载中,数据加载完成以后再呈现数据。这样也可 满足现阶段不同类型客户端需求(例如手机端,电脑端,电视端,手表端。)
- 业务描述及设计实现
构建实体对象(POJO)封装从数据库查询到的记录,一行记录映射为内存中一个的这 样的对象。对象属性定义时尽量与表中字段有一定的映射关系,并添加对应的 set/get/toString 等方法,便于对数据进行更好的操作。- 关键代码分析及实现
package com.cy.pj.sys.entity;
import java.io.Serializable;
import java.util.Date;
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
//用户名
private String username;
//用户操作
private String operation;
//请求方法
private String method;
//请求参数
private String params;
//执行时长(毫秒)
private Long time;
//IP 地址
private String ip;
//创建时间
private Date createdTime;
/**设置:*/
public void setId(Integer id) {
this.id = id;
}
/**获取:*/
public Integer getId() {
return id;
}
/**设置:用户名*/
public void setUsername(String username) {
this.username = username;
}
/** 获取:用户名*/
public String getUsername() {
return username;
}
/**设置:用户操作*/
public void setOperation(String operation) {
this.operation = operation;
}
/**获取:用户操作*/
public String getOperation() {
return operation;
}
/**设置:请求方法*/
public void setMethod(String method) {
this.method = method;
}
/**获取:请求方法*/
public String getMethod() {
return method;
}
/** 设置:请求参数*/
public void setParams(String params) {
this.params = params;
}
/** 获取:请求参数 */
public String getParams() {
return params;
}
/**设置:IP 地址 */
public void setIp(String ip) {
this.ip = ip;
}
/** 获取:IP 地址*/
public String getIp() {
return ip;
}
/** 设置:创建时间*/
public void setCreateDate(Date createdTime) {
this.createdTime = createdTime;
}
/** 获取:创建时间*/
public Date getCreatedTime() { return createdTime; }
public Long getTime() { return time; }
public void setTime(Long time) { this.time = time; } }
说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据, 实现层与层之间数据的传递。 思考:这个对象的 set 方法,get 方法可能会在什么场景用到?
- 业务描述及设计实现
通过数据层对象,基于业务层参数数据查询日志记录总数以及当前页要呈现的用户行 为日志信息。- 关键代码分析及实现: 第一步:定义数据层接口对象,通过将此对象保证给业务层以提供日志数据访问。代 码如下:
@Mapper
public interface SystemLogDao{
}
第二步:在 SysLogDao 接口中添加 getRowCount 方法用于按条件统计记录总数。代 码如下:
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @return 总记录数(基于这个结果可以计算总页数) */
int getRowCount(@Param("username") String username);
第三步:在 SysLogDao 接口中添加 findPageObjects 方法,基于此方法实现当前页 记录的数据查询操作。
代码如下:
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @param startIndex 当前页的起始位置
* @param pageSize 当前页的页面大小
* @return 当前页的日志记录信息
* 数据库中每条日志信息封装到一个 SysLog 对象中*/
List<SysLog> findPageObjects(
@Param("username") String username,
@Param("startIndex") Integer startIndex,
@Param("pagesize") Integer pagesize);
说明:
1) 当DAO 中方法参数多余一个时尽量使用@Param 注解进行修饰并指定名字,然后在 Mapper 文件中便可以通过类似#{username}方式进行获取,否则只能通过 #{arg0},#{arg1}或者#{param1},#{param2}等方式进行获取。
2) 当 DAO 方法中的参数应用在动态 SQL 中时无论多少个参数,尽量使用@Param 注 解进行修饰并定义。
业务描述及设计实现 基于 Dao 接口创建映射文件,在此文件中通过相关元素(例如 select)描述要执行的 数据操作。
▪关键代码设计及实现 第一步:在映射文件的设计目录(mapper/sys)中添加 SysLogMapper.xml 映射文件, 代码如下:
<mapper namespace="com.cy.pj.sys.dao.SysLogDao">
mapper>
第二步:在映射文件中添加 sql 元素实现,SQL 中的共性操作,代码如下:
<sql id="queryWhereId">
from sys_logs
<where>
<if test="username!=null and username!=''">
username like concat("%",#{username},"%")
if>
where>
sql>
第三步:在映射文件中添加 id 为 getRowCount 元素,按条件统计记录总数, 代码如下:
<select id="getRowCount" resultType="int">
select count(*)
<include refid="queryWhereId"/>
select>
第四步:在映射文件中添加 id 为 findPageObjects 元素,实现分页查询。
代码如下:
<select id="findPageObjects" resultType="com.cy.pj.sys.entity.SysLog">
select *
<include refid="queryWhereId"/>
order by createdTime desc
limit #{startIndex},#{pageSize}
select>
思考:
1) 动态 sql:基于用户需求动态拼接 SQL
2) Sql 标签元素的作用是什么?对sql 语句中的共性进行提取,以遍实现更好的复用.
3) Include 标签的作用是什么?引入使用 sql 标签定义的元素
第五步:单元测试类 SysLogDaoTests,对数据层方法进行测试。
package com.cy.pj.sys.dao;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.cy.pj.sys.entity.SysLog;
@SpringBootTest
public class SysLogDaoTest(){
@Autowired
private SysLogDao sysLogDao;
@Test
public void testGetRowCount(){
int rows = sysLogDao.getRowCount("admin");
System.out.println("rows="+rows);
}
@Test
public void testFindPageObject(){
List<SysLog> list = sysLogDao.findPageObject("admin",0,3);
for(SysLog log : list){
System.out.println(log);
}
}
}
业务描述与设计实现 :
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通 过业务方法中的参数接收控制层数据(例如 username,pageCurrent)并校验。然后基于用 户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最 后对查询结果进行封装并返回。
关键代码设计及实现 :
业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码 参考如下:
package com.cy.pj.common.vo;
public class PageObject<T> implements Serializable {
private static final long serialVersionUID = 6780580291247550747L;//类泛型
/**当前页的页码值*/
private Integer pageCurrent=1;
/**页面大小*/
private Integer pageSize=3;
/**总行数(通过查询获得)*/
private Integer rowCount=0;
/**总页数(通过计算获得)*/
private Integer pageCount=0;
/**当前页记录*/
private List<T> records;
public PageObject(){}
public PageObject(Integer pageCurrent, Integer pageSize, Integer rowCount, List<T> records)
{ super();
this.pageCurrent = pageCurrent;
this.pageSize = pageSize;
this.rowCount = rowCount;
this.records = records;
// this.pageCount=rowCount/pageSize;
// if(rowCount%pageSize!=0) {
// pageCount++;
// }
this.pageCount=(rowCount-1)/pageSize+1; }
public Integer getPageCurrent() { return pageCurrent; }
public void setPageCurrent(Integer pageCurrent) { this.pageCurrent = pageCurrent; }
public Integer getPageSize() { return pageSize; }
public void setPageSize(Integer pageSize) { this.pageSize = pageSize; }
public Integer getRowCount() { return rowCount; }
public void setRowCount(Integer rowCount) { this.rowCount = rowCount; }
public Integer getPageCount() { return pageCount; }
public void setPageCount(Integer pageCount) { this.pageCount = pageCount; }
public List<T> getRecords() { return records; }
public void setRecords(List<T> records) { this.records = records; } }
定义日志业务接口及方法,暴露外界对日志业务数据的访问,其代码参考如下:
package com.cy.pj.sys.service;
import com.cy.pj.common.pojo.PageObject;
import com.cy.pj.sys.pojo.SysLog;
public interface SysLogService {
/*
* @Param username基于条件查询时的参数名
* @Param pageCurrent当前页码值
* @return 当前分页记录和分页信息
* */
PageObject<SysLog> findPageObjects(String username,Integer pageCurrent);
}
日志业务接口及实现类,用于具体执行日志业务数据的分页查询操作,其代码如下:
package com.cy.pj.sys.service.impl;
@Service
public class SysLogServiceImpl implements SysLogService {
@Autowired
private SysLogDao sysLogDao;
@Override
public PageObject<SysLog> findPageObjects(String username, Integer pageCurrent) {
//1、验证参数合法性
//1.1验证pageCurrent合法性,不合法抛出 IllegalArgumentException 异常
if (pageCurrent == null || pageCurrent < 0) throw new IllegalArgumentException("当前页码不正确");
//2.基于条件查询总记录数
// 2.1 执行查询
int rowCount = sysLogDao.getRowcount(username);
//2.2) 验证查询结果,假如结果为 0 不再执行如下操作
if (rowCount < 0) throw new ServiceException("系统没有查到对应记录");
//3.基于条件查询当前页记录(pageSize 定义为 2)
// 3.1)定义 pageSize
int pageSize = 5;
//3.2)计算 startIndex
int startIndex = (pageCurrent-1)*pageSize;
//3.3)执行当前数据的查询操作
List<SysLog> records = sysLogDao.findPageObjects(username,startIndex,pageSize);
//4.对分页信息以及当前页记录进行封装
// 4.1)构建 PageObject 对象
PageObject<SysLog> pageObject = new PageObject<>();
//4.2)封装数据
pageObject.setPageCurrent(pageCurrent);
pageObject.setPageSize(pageSize);
pageObject.setRowCount(rowCount);
pageObject.setRecords(records);
pageObject.setPageCount((rowCount-1)/pageSize+1);
//5.返回封装结果。
return pageObject;
}
}
当然,这样的代码看起来有些复杂,其实我们有更简单的方式实现,在父module中添加pageHelper依赖,使用pageHelper插件实现分页查询,具体代码如下:
@Autowired
private SysLogDao sysLogDao;
@Override
public PageObject<SysLog> findPageObject(String username,Integer pageCurrent){
//1.参数校验
//2.查询当前页记录
int pageSize = 5;
Page<SysLog> page = PageHelper.startPage(pageCurrent,pageSize);
List<SysLog> records = PageHlper.findPageObjects(String username);
//3、封装查询结果
return new PageObject<>((int)page.getTotal(),records,page.getPages(),pageSize,pageCurrent);
}
说明:几乎在所有的框架中都提供了自定义异常,例如 MyBatis 中的 BindingException 等。
定义 Service 对象的单元测试类,代码如下:
业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请 求参数,然后通过业务层对象执行业务逻辑,再通过 VO 对象封装响应结果(主要对业务层 数据添加状态信息),最后将响应结果转换为 JSON 格式的字符串响应到客户端。
关键代码设计与实现
定义控制层值对象(VO),目的是基于此对象封装控制层响应结果(在此对象中主要是为 业务层执行结果添加状态信息)。Spring MVC 框架在响应时可以调用相关 API(例如 jackson)将其对象转换为 JSON 格式字符串。
package com.cy.pj.common.pojo;
import lombok.Setter;
import java.io.Serializable;
/**借助此对象封装控制层响应到客户端的数据*/
@Setter
public class JsonResult implements Serializable {
private static final long serialVersionUID = -8722122492343039602L;
/**响应状态码*/
private Integer state=1;//1 表示ok,0表示error
/**响应状态码对应的具体信息*/
private String message="ok";
/**响应数据(一般是查询时获取到的正确数据)*/
private Object data;
public JsonResult(){}
public JsonResult(String message){
this.message=message;
}
public JsonResult(Object data){
this.data=data;
}
public JsonResult(Throwable e){
this.state=0;
this.message=e.getMessage();
}
public Integer getState() {
return state;
}
public String getMessage() {
return message;
}
public Object getData() {
return data;
}
}
定义 Controller 类,并将此类对象使用 Spring 框架中的@Controller 注解进行标 识,表示此类对象要交给 Spring 管理。
然后基于@RequestMapping 注解为此类定义根路 径映射。代码参考如下:
package com.cy.pj.sys.controller;
import com.cy.pj.common.pojo.JsonResult;
import com.cy.pj.sys.service.SysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/log/")
public class SysLogController {
@Autowired
private SysLogService sysLogService;
@RequestMapping("doFindPageObjects")
public JsonResult doFindPageObjects(String username,Integer pageCurrent){
return new JsonResult(sysLogService.findPageObjects(username,pageCurrent));
}
}
定义全局异常处理类,对控制层可能出现的异常,进行统一异常处理,代码如下:
package com.cy.pj.common.web;
import com.cy.pj.common.pojo.JsonResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public JsonResult doHandleRuntimeException(RuntimeException e){
e.printStackTrace();//输出到控制台,也可以写日志信息
log.error("e.message {}",e.getMessage());
return new JsonResult(e);
}
}
业务描述与设计实现
日志分页页面加载完成以后,向服务端发起异步请求加载日志信息,当日志信息加载 完成需要将日志信息、分页信息呈现到列表页面上。
关键代码设计与实现
第一步:分页页面加载完成,向服务端发起异步请求,代码参考如下:
$(function(){
$("pageId").load("pageId",function(){
doGetObjects();
})
})
第二步:定义异步请求处理函数,代码参考如下:
function doGetObjects(){
//1、定义url和参数
var url="log/doFindPageObjects";
var param={"pageCurrent":1};
//2.发起异步请求
//请问如下 ajax 请求的回调函数参数名可以是任意吗?
//可以,必须符合标识 符的规范
$.getJSON(url,params,function(result){
//请问 result 是一个字符串还是 json 格式的 js 对象?对象
doHandleQueryResponseResult(result);
}
);//特殊的 ajax 函数
}
function doHandleQueryResponseResult (result){ //JsonResult
if(result.state==1){//ok
//更新 table 中 tbody 内部的数据
doSetTableBodyRows(result.data.records);//将数据呈现在页面上
//更新页面 page.html 分页数据
doSetPagination(result.data); //此方法写到 page.html 中
}else{
alert(result.message);
}
}
第四步:将异步响应结果呈现在 table 的 tbody 位置。代码参考如下:
function doSetTableBodyRows(records){
//1.获取 tbody 对象,并清空对象
var tBody=$("#tbodyId");
tBody.empty();
//2.迭代 records 记录,并将其内容追加到 tbody
for(var i in records){
//2.1 构建 tr 对象
var tr=$(" ");
//2.2 构建 tds 对象
var tds=doCreateTds(records[i]);
//2.3 将 tds 追加到 tr 中
tr.append(tds);
//2.4 将 tr 追加到 tbody 中
tBody.append(tr);
}
}
第五步:创建每行中的 td 元素,并填充具体业务数据。代码参考如下:
function doCreateTds(data){
var tds=" "+
""+data.username+" "+
""+data.operation+" "+
""+data.method+" "+
""+data.params+" "+
""+data.ip+" "+
""+data.time+" ";
return tds;
}
业务描述与设计实现
日志信息列表初始化完成以后初始化分页数据(调用setPagination 函数),然后再 点击上一页,下一页等操作时,更新页码值,执行基于当前页码值的查询。
关键代码设计与实现:
第一步:在 page.html 页面中定义 doSetPagination 方法(实现分页数据初始化), 代码如下:
function doSetPagination(page){
//1.始化数据
$(".rowCount").html("总记录数("+page.rowCount+")");
$(".pageCount").html("总页数("+page.pageCount+")");
$(".pageCurrent").html("当前页("+page.pageCurrent+")");
//2.绑定数据(为后续对此数据的使用提供服务)
$("#pageId").data("pageCurrent",page.pageCurrent);
$("#pageId").data("pageCount",page.pageCount);
}
第二步:分页页面 page.html 中注册点击事件。代码如下:
$(function(){
//事件注册
$("#pageId").on("click",".first,.pre,.next,.last",doJumpToPage); })
第三步:定义 doJumpToPage 方法(通过此方法实现当前数据查询)
function doJumpToPage(){
//1.获取点击对象的 class 值
var cls=$(this).prop("class");//Property
//2.基于点击的对象执行 pageCurrent 值的修改
//2.1 获取 pageCurrent,pageCount 的当前值
var pageCurrent=$("#pageId").data("pageCurrent");
var pageCount=$("#pageId").data("pageCount");
//2.2 修改 pageCurrent 的值
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; }
//3.对 pageCurrent 值进行重新绑定
$("#pageId").data("pageCurrent",pageCurrent);
//4.基于新的 pageCurrent 的值进行当前页数据查询
doGetObjects(); }
修改分页查询方法:
function doGetObjects(){
//debugger;//断点调试
//1.定义 url 和参数
var url="log/doFindPageObjects"
//? 请问 data 函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
var pageCurrent=$("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化 pageCurrent 的值为 1
//pageCurrent 参数在没有赋值的情况下,默认初始值应该为 1.
if(!pageCurrent) pageCurrent=1;
var params={"pageCurrent":pageCurrent};//pageCurrent=2
//2.发起异步请求
//请问如下 ajax 请求的回调函数参数名可以是任意吗?可以,必须符合标识符 的规范
$.getJSON(url,params,function(result){
//请问 result 是一个字符串还是 json 格式的 js 对象?对象
doHandleQueryResponseResult(result);
}
);//特殊的 ajax 函数 }
业务描述及设计
当用户点击日志列表的查询按钮时,基于用户输入的用户名进行有条件的分页查询, 并将查询结果呈现在页面。
关键代码设计与实现:
第一步:日志列表页面加载完成,在查询按钮上进行事件注册。代码如下:
$(".input-group-btn").on("click",".btn-search",doQueryObjects)
第二步:定义查询按钮对应的点击事件处理函数。代码如下:
function doQueryObjects(){
//为什么要在此位置初始化 pageCurrent 的值为 1?
//数据查询时页码的初始位置也应该是第一页
$("#pageId").data("pageCurrent",1);
//为什么要调用 doGetObjects 函数?
//重用 js 代码,简化 jS 代码编写。
doGetObjects(); }
第三步:在分页查询函数中追加 name 参数定义,代码如下:
function doGetObjects(){
$("#checkAll").prop("checked",false);
//1.定义请求参数
//这个值最初是在page.html中进行了绑定
var pageCurrent=$("#pageId").data("pageCurrent");//data(key)表示获取值
if(!pageCurrent)pageCurrent=1;
//var params="pageCurrent="+pageCurrent;//{"pageCurrent":1}
var params={"pageCurrent":pageCurrent}
//获取查询时参数username的值(可能有值,也可能没有值)
var uname=$("#searchNameId").val();
//假如uname有值,则将key:value添加到params对象中,这里的key需要与控制器方法参数相同
if(uname)params.username=uname;
//2.定义请求url
var url="log/doFindPageObjects";
//3.发送异步请求加载数据,并处理响应结果
$.getJSON(url,params,function(result){//result-->JsonResult
console.log(result);//json 格式的javascript对象
// debugger
doHandleResponseResult(result);//处理响应结果
})
}
业务描述及设计实现
数据层基于业务层提交的日志记录 id,进行日志删除操作。
关键代码设计及实现:
在 SysLogDao 中添加基于 id 执行日志删除的方法。代码参考如下:
FAQ 分析:如上 SQL 实现可能会存在什么问题?(可靠性问题,性能问题) 从可靠性的角度分析,假如 ids 的值为 null 或长度为 0 时,SQL 构建可能会出现语法 问题,可参考如下代码进行改进(先对 ids 的值进行判定):
<delete id="deleteObjects">
delete from sys_logs
<if test="ids!=null and ids.leng>0">
where id in
<foreach collection="ids"
open="("
close=")"
separator=","
item="id">
#{id}
foreach>
if>
<if test="ids==null or ids.length=0">
where 1=2
if>
delete>
从 SQL 执行性能角度分析,一般在 SQL 语句中不建议使用 in 表达式,可以参考如下代
码进行实现(重点是 forearch 中 or 运算符的应用):
<delete id="deleteObjects">
delete from sys_logs
<choose>
<when test="ids!=null and ids.length>0">
<where>
<foreach collection="ids"
item="id"
separator="or">
id=#{id}
foreach>
where>
when>
<otherwise>
where 1=2
otherwise>
choose>
delete>
说明:这里的 choose 元素也为一种选择结构,when 元素相当于 if,otherwise 相当 于 else 的语法。
业务描述与设计实现
在日志业务层定义用于执行删除业务的方法,首先通过方法参数接收控制层传递的多 个记录的 id,并对参数 id 进行校验。然后基于日志记录 id 执行删除业务实现。最后返回 业务执行结果。
关键代码设计与实现
第一步:在 SysLogService 接口中,添加基于多个 id 进行日志删除的方法。关键代 码如下:
int deleteObjects(Integer… ids) {}
第二步:在 SysLogServiceImpl 实现类中添加删除业务的具体实现。关键代码如下:
@Override
public int deleteObjects(Integer... ids) {
//1、判断参数合法性
if (ids == null || ids.length == 0) throw new IllegalArgumentException("请选择一个");
//2、执行删除操作
int rows = 0;
try{
rows = sysLogDao.deleteObjects(ids);
}catch(Throwable e){
e.printStackTrace();
//发出报警信息(例如给运维人员发短信)
System.out.println("系统故障,恢复中。。。");
}
//3、对结果进行验证
if (rows == 0) throw new ServiceException("记录可能已经不存在");
//5、返回结果
return rows;
}
业务描述与设计实现
在日志控制层对象中,添加用于处理日志删除请求的方法。首先在此方法中通过形参 接收客户端提交的数据,然后调用业务层对象执行删除操作,最后封装执行结果,并在运 行时将响应对象转换为 JSON 格式的字符串,响应到客户端。
关键代码设计与实现
第一步:在 SysLogController 中添加用于执行删除业务的方法。代码如下:
@RequestMapping("doDeleteObjects")
public JsonResult doDeleteOnjects(Integer... ids){
return new JsonResult(sysLogService.deleteObjects(ids));
}
第二步:启动 tomcat 进行访问测试,打开浏览器输入如下网址:
http://localhost/log/doDeleteObjects?ids=1,2,3
业务描述及设计实现
用户在页面上首先选择要删除的元素,然后点击删除按钮,将用户选择的记录 id 异步 提交到服务端,最后在服务端执行日志的删除动作。
关键代码设计与实现
第一步:页面加载完成以后,在删除按钮上进行点击事件注册。关键代码如下:
$(".input-group-btn")
.on("click",".btn-delete",doDeleteObjects)
第二步:定义删除操作对应的事件处理函数。关键代码如下:
function doDeleteObjects(){
//1.获取选中的checkbox对象value属性的值,并存储到数组。
var idArray=doGetCheckedIds();
debugger
//2.校验数组内容
if(idArray.length==0){
alert("请先选择");
return;
}
//3.给出提示,确定删除吗?
if(!confirm("确定删除吗"))return;
//4.执行异步删除操作
//4.1定义删除的url
var url="log/doDeleteObjects"
//4.2定义删除参数
var params={"ids":idArray.toString()};//[1,2,3]--toString--->1,2,3
//4.3发送异步请求执行删除
$.post(url,params,function(result){
if(result.state==1){
alert(result.message);
//刷新当前页面
doRefreshAfterDeleteOK();
}else{
doSetErrorMsg(result.message);
}
});
}
第三步:定义获取用户选中的记录 id 的函数。关键代码如下:
function doGetCheckedIds(){
//定义js数组
var array=[];
//获取所有tbody对象内部选中的checkbox对象的值,并将其放入数组
$("#tbodyId input[type='checkbox']")
//each函数表示迭代,这里用于迭代checkbox对象
.each(function(){
if($(this).prop("checked")){//判定checkbox是否已选中
//push用于向数组中放数据
array.push($(this).val())//val()获取value属性的值
}
});
return array;
}
第四步:Thead 中全选元素的状态影响 tbody 中 checkbox 对象状态。代码如下:
function doChangeTBodyCheckBoxState(){
//1.获取thead中checkbox对象状态
var flag=$(this).prop("checked");
//2.修改tbody中checkbox对象状态
$("#tbodyId input[type='checkbox']").prop("checked",flag);
}
第五步:Tbody 中 checkbox 的状态影响 thead 中全选元素的状态。代码如下:
function doChangeTHeadCheckBoxState(){
//1.获得tbody中所有checkbox对象进行逻辑与结果
var flag=true;
$("#tbodyId input[type='checkbox']")
.each(function(){
flag=flag&&$(this).prop("checked");
});
//2.修改thead中checkbox对象的状态值。
$("#checkAll").prop("checked",flag)
}
第六步:完善业务刷新方法,当在最后一页执行删除操作时,基于全选按钮状态及当 前页码值,刷新页面。关键代码如下:
function doRefreshAfterDeleteOK(){
//1.假如当前页码值不是最后一页,页码值不变。
//2.修改当前页码值为上一页的页码值,但必须满足如下几个条件
//1)当前页码值已经是最后一页
//2)当前页码值大于>1
//3)还有Thead中checkbox对象状态为全选状态
var pageCurrent=$("#pageId").data("pageCurrent");
var pageCount=$("#pageId").data("pageCount");
var checkAllState=$("#checkAll").prop("checked");
if(checkAllState&&pageCurrent==pageCount&&pageCurrent>1){
pageCurrent--;
$("#pageId").data("pageCurrent",pageCurrent);
}
//3.查询日志信息
doGetObjects();
}
说明:最后将如上方法添加在删除操作成功以后的代码块中。
这块业务学了 AOP 以后再实现.
业务描述与设计实现
数据层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。
关键代码设计与实现
在 SysLogDao 接口中添加用于实现日志信息持久化的方法。关键代码如下:
int insertObject(SysLog entity);
业务描述与设计实现
基于 SysLogDao 中方法的定义,编写用于数据持久化的 SQL 元素。
关键代码设计与实现
在 SysLogMapper.xml 中添加 insertObject 元素,用于向日志表写入用户行为日志。 关键代码如下:
<insert id="insertObject">
insert into sys_logs
(username,ip,operation,method,params,time,createdTime)
values
(#{username},#{ip},#{operation},#{method},#{params},#{time},#{createdTime})
insert>
业务描述与设计实现
将日志切面中抓取到的用户行为日志信息,通过业务层对象方法持久化到数据库。
关键代码实现
第一步:在 SysLogService 接口中,添加保存日志信息的方法。关键代码如下:
void saveObjects(SysLog entity);
第二步:在 SysLogServiceImpl 类中添加,保存日志的方法实现。关键代码如下:
业务描述与设计实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业 务层对象对日志日志信息做进一步处理。此部分内容后续结合 AOP 进行实现(暂时先了 解,不做具体实现)。
关键代码设计与实现
springboot工程中应用 AOP时,首先要添加如下依赖(假如有则无需添加):
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
定义日志切面类对象,通过环绕通知处理日志记录操作。关键代码如下:
package com.cy.pj.common.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**@Aspect 注解用于定义切面对象*/
@Aspect
@Component
public class SysLogFileAspect {
/**切入点的定义
* 1)使用@Pointcut注解进行切入点的描述
* 2)使用bean表达式定义切入点,语法:bean(spring容器中管理的某个bean的名字)
* bean表达式是一种粗粒度的切入点表达式(不能具体到bean中哪个方法)
* */
//@Pointcut("bean(categoryServiceImpl)")
@Pointcut("bean(*ServiceImpl)")
public void doLog(){}//这里的doLog()方法,方法体内不需要写任何内容,作用是承载切入点
/**在切入点对应的目标方法执行时,要进行的动作可以以如下方式进行定义
* @param joinPoint 封装了切入点集合方法中的某个正在执行的目标方法,
* ProceedingJoinPoint 类型的连接点只能应用在@Around注解描述的方法参数中
* @return 为目标方法的返回结果
* */
@Around("doLog()") //@Around注解描述在方法可以在目标方法执行之前,之后做一些业务拓展
public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
long t1=System.currentTimeMillis();
Object result=joinPoint.proceed();//去调用目标方法
long t2=System.currentTimeMillis();
System.out.println("execute time "+(t2-t1));
//..........
return result;//目标方法的执行结果
}
}
方法中用到的 ip 地址获取需要提供一个如下的工具类:(不用自己实现,直接用)
package com.cy.pj.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
public static String getIpAddr() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
return ip;
}
}
日志管理整体业务分析与实现。
- 分层架构(应用层 MVC:基于 spring 的 mvc 模块)。
- API架构(SysLogDao,SysLogService,SysLogController)。
- 业务架构(查询,删除,添加用户行为日志)。
- 数据架构(SysLog,PageObject,JsonResult,…)。
日志管理持久层映射文件中 SQL元素的定义及编写。
- 定义在映射文件”mapper/sys/SysLogMapper.xml”(必须在加载范围内)。
- 每个 SQL 元素必须提供一个唯一ID,对于 select 必须指定结果映射 (resultType)。
- 系统底层运行时会将每个 SQL 元素的对象封装一个值对象(MappedStatement)。
日志管理模块数据查询操作中的数据封装。
- 数据层(数据逻辑)的 SysLog对象应用(一行记录一个 log 对象)。
- 业务层(业务逻辑)PageObject 对象应用(封装每页记录以及对应的分页信息)。
- 控制层(控制逻辑)的 JsonResult 对象应用(对业务数据添加状态信息)。
日志管理控制层请求数据映射,响应数据的封装及转换(转换为 json 串)。
- 请求路径映射,请求方式映射(GET,POST),请求参数映射(直接量,POJO)。
- 响应数据两种(页面,JSON 串)。
日志管理模块异常处理如何实现的。
- 请求处理层(控制层)定义统一(全局)异常处理类。
- 使用注解@RestControllerAdvice 描述类,使用@ExceptionHandler 描述方法.
- 异常处理规则:能处理则处理,不能处理则抛出。
用户行为日志表中都有哪些字段?(面试时有时会问)
▪ 用户行为日志是如何实现分页查询的?(limit)
▪ 用户行为数据的封装过程?(数据层,业务层,控制层)
▪ 项目中的异常是如何处理的?
▪ 页面中数据乱码,如何解决?(数据来源,请求数据,响应数据)
▪ 说说的日志删除业务是如何实现?
▪ Spring MVC 响应数据处理?(view,json)
▪ 项目你常用的 JS函数说几个?(data,prop,ajax,each,…)
▪ MyBatis中的@Params注解的作用?(为参数变量指定其其别名)
▪ Jquery 中 data 函数用于做什么?可以借助 data 函数将数据绑定到指定对象,语法为 data(key[,value]),key和 value为自己业务中的任意数据,假如只有 key表示取值。
▪ Jquery 中的 prop 函数用于获取 html 标签对象中”标准属性”的值或为属性赋值,其语法 为 prop(propertyName[,propertyValue]),假如只有属性名则为获取属性值。
▪ Jquery 中 attr 函数为用户获取 html 标签中任意属性值或为属性赋值的一个方法,其语法为:attr(propertyName[,propertyValue]),假如只有属性名则为获取属性值。
▪ 日志写操作事务的传播特性如何配置?(每次开启新事务,没学就暂时搁置)?
▪ 日志写操作为什么应该是异步的?(用户体验会更好,不会阻塞用户正常业务)
▪ Spring 中的异步操作如何实现?,(自己直接创建线程或者借助池中线程)
▪ Spring 中的@Async如何应用?(没学就暂时搁置)
▪ 项目中的 BUG分析及解决套路?(排除法,打桩(log),断点,搜索引擎)