共计 25838 个字符,预计需要花费 65 分钟才能阅读完成。
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=1 DEFAULT CHARSET=utf8 COMMENT=’ 系统日志 ’;
-
原型设计说明
基于用户需要,实现动态页面(html/css/js),通过动态页面为用户出现根本需要实现,如图 - 1 所示。
图 -1
阐明: 如果客户对此原型进行了确认, 后续则能够基于此原型进行研发。
-
API 设计说明
日志业务后盾 API 分层架构及调用关系如图 - 2 所示:
图 -2
阐明:分层目标次要将简单问题简单化,实现各司其职,各尽所能。
-
日志治理列表页面出现
-
业务时序剖析
当点击首页左侧的 ” 日志治理 ” 菜单时, 其总体时序剖析如图 - 3 所示:
图 -3
-
服务端实现
-
Controller 实现
- 业务形容与设计实现
基于日志治理的申请业务,在 PageController 中增加 doLogUI 办法,doPageUI 办法别离用于返回日志列表页面,日志分页页面。
- 要害代码设计与实现
第一步:在 PageController 中定义返回日志列表的办法。代码如下:
@RequestMapping(“log/log_list”)
public String doLogUI() {
return “sys/log_list”;
}
第二步:在 PageController 中定义用于返回分页页面的办法。代码如下:
@RequestMapping(“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”);
});
阐明:数据加载通常是一个绝对比拟耗时操作,为了改善用户体验,能够先为用户出现一个页面,数据加载时,显示数据正在加载中, 数据加载实现当前再出现数据。这样也可满足现阶段不同类型客户端需要(例如手机端, 电脑端, 电视端, 手表端。)
-
日志治理列表数据出现
-
数据架构剖析
日志查问服务端数据根本架构,如图 - 4 所示。
图 -4
-
服务端 API 架构及业务时序图剖析
服务端日志分页查问代码根本架构,如图 - 5 所示:
图 -5
服务端日志列表数据查问时序图,如图 - 6 所示:
图 -6
-
服务端要害业务及代码实现
-
Entity 类实现
- 业务形容及设计实现
构建实体对象(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 办法可能会在什么场景用到?
-
Dao 接口实现
- 业务形容及设计实现
通过数据层对象,基于业务层参数数据查问日志记录总数以及当前页要出现的用户行为日志信息。
- 要害代码剖析及实现:
第一步:定义数据层接口对象,通过将此对象保障给业务层以提供日志数据拜访。代码如下:
@Mapper
public interface SysLogDao {
}
第二步:在 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);
阐明:
- 当 DAO 中办法参数多余一个时尽量应用 @Param 注解进行润饰并指定名字,而后在 Mapper 文件中便能够通过相似 #{username}形式进行获取,否则只能通过#{arg0},#{arg1}或者#{param1},#{param2}等形式进行获取。
- 当 DAO 办法中的参数利用在动静 SQL 中时无论多少个参数,尽量应用 @Param 注解进行润饰并定义。
-
Mapper 文件实现
- 业务形容及设计实现
基于 Dao 接口创立映射文件,在此文件中通过相干元素 (例如 select) 形容要执行的数据操作。
- 要害代码设计及实现
第一步:在映射文件的设计目录 (mapper/sys) 中增加 SysLogMapper.xml 映射文件,代码如下:
<?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”>
</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>
思考:
- 动静 sql: 基于用户需要动静拼接 SQL
- Sql 标签元素的作用是什么? 对 sql 语句中的共性进行提取, 以遍实现更好的复用.
- 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 SysLogDaoTests {
@Autowired
private SysLogDao sysLogDao;
@Test
public void testGetRowCount() {
int rows=sysLogDao.getRowCount(“admin”);
System.out.println(“rows=”+rows);
}
@Test
public void testFindPageObjects() {
List<SysLog> list=
sysLogDao.findPageObjects(“admin”, 0, 3);
for(SysLog log:list) {
System.out.println(log);
}
}
}
-
Service 接口及实现类
- 业务形容与设计实现
业务层次要是实现模块中业务逻辑的解决。在日志分页查问中,业务层对象首先要通过业务办法中的参数接管管制层数据 (例如 username,pageCurrent) 并校验。而后基于用户名进行总记录数的查问并校验,再基于起始地位及页面大小进行当前页记录的查问,最初对查问后果进行封装并返回。
- 要害代码设计及实现
业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考如下:
package com.cy.pj.common.bo;
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;
public interface SysLogService {
/**
- @param name 基于条件查问时的参数名
- @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 name, Integer pageCurrent) {
//1. 验证参数合法性
//1.1 验证 pageCurrent 的合法性,
// 不非法抛出 IllegalArgumentException 异样
if(pageCurrent==null||pageCurrent<1)
throw new IllegalArgumentException(“ 以后页码不正确 ”);
//2. 基于条件查问总记录数
//2.1) 执行查问
int rowCount=sysLogDao.getRowCount(name);
//2.2) 验证查问后果,如果后果为 0 不再执行如下操作
if(rowCount==0)
throw new ServiceException(“ 零碎没有查到对应记录 ”);
//3. 基于条件查问当前页记录(pageSize 定义为 2)
//3.1)定义 pageSize
int pageSize=2;
//3.2)计算 startIndex
int startIndex=(pageCurrent-1)*pageSize;
//3.3)执行以后数据的查问操作
List<SysLog> records=
sysLogDao.findPageObjects(name, 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;
}
}
在以后办法中须要的 ServiceException 是一个本人定义的异样, 通过自定义异样可更好的实现对业务问题的形容,同时能够更好的进步用户体验。参考代码如下:
package com.cy.pj.common.exception;
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 7793296502722655579L;
public ServiceException() {
super();
}
public ServiceException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public ServiceException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
阐明:简直在所有的框架中都提供了自定义异样,例如 MyBatis 中的 BindingException 等。
定义 Service 对象的单元测试类,代码如下:
package com.cy.pj.sys.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.cy.pj.common.vo.PageObject;
import com.cy.pj.sys.entity.SysLog;
@SpringBootTest
public class SysLogServiceTests {
@Autowired
private SysLogService sysLogService;
@Test
public void testFindPageObjects() {
PageObject<SysLog> pageObject=
sysLogService.findPageObjects(“admin”, 1);
System.out.println(pageObject);
}
}
-
Controller 类实现
- 业务形容与设计实现
管制层对象次要负责申请和响应数据的解决,例如,本模块首先要通过管制层对象解决申请参数,而后通过业务层对象执行业务逻辑,再通过 VO 对象封装响应后果(次要对业务层数据增加状态信息),最初将响应后果转换为 JSON 格局的字符串响应到客户端。
- 要害代码设计与实现
定义管制层值对象(VO),目标是基于此对象封装管制层响应后果(在此对象中次要是为业务层执行后果增加状态信息)。Spring MVC 框架在响应时能够调用相干 API(例如 jackson)将其对象转换为 JSON 格局字符串。
package com.cy.pj.common.vo;
public class JsonResult implements Serializable {
private static final long serialVersionUID = -856924038217431339L;//SysResult/Result/R
/* 状态码/
private int state=1;// 1 示意 SUCCESS,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 t){
this.state=0;
this.message=t.getMessage();
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
定义 Controller 类,并将此类对象应用 Spring 框架中的 @Controller 注解进行标识,示意此类对象要交给 Spring 治理。而后基于 @RequestMapping 注解为此类定义根门路映射。代码参考如下:
package com.cy.pj.sys.controller;
@Controller
@RequestMapping(“/log/”)
public class SysLogController {
@Autowired
private SysLogService sysLogService;
}
在 Controller 类中增加分页申请解决办法,代码参考如下:
@RequestMapping(“doFindPageObjects”)
@ResponseBody
public JsonResult doFindPageObjects(String username,Integer pageCurrent){
PageObject<SysLog> pageObject=
sysLogService.findPageObjects(username,pageCurrent);
return new JsonResult(pageObject);
}
定义全局异样解决类,对管制层可能呈现的异样,进行对立异样解决,代码如下:
package com.cy.pj.common.web;
import java.util.logging.Logger;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cy.pj.common.vo.JsonResult;
@ControllerAdvice
public class GlobalExceptionHandler {
//JDK 中的自带的日志 API
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public JsonResult doHandleRuntimeException(
RuntimeException e){
e.printStackTrace();// 也能够写日志
异样信息
return new JsonResult(e);// 封装
}
}
管制层响应数据处理剖析,如图 - 7 所示:
图 -7
-
客户端要害业务及代码实现
-
客户端页面事件剖析
当用户点击首页日志治理时,其页面流转剖析如图 - 8 所示:
图 -8
-
日志列表信息出现
- 业务形容与设计实现
日志分页页面加载实现当前,向服务端发动异步申请加载日志信息,当日志信息加载实现须要将日志信息、分页信息出现到列表页面上。
- 要害代码设计与实现
第一步:分页页面加载实现,向服务端发动异步申请,代码参考如下:
$(function(){
// 为什么要将 doGetObjects 函数写到 load 函数对应的回调外部。
$(“#pageId”).load(“doPageUI”,function(){
doGetObjects();
});
});
第二步:定义异步申请处理函数,代码参考如下:
function doGetObjects(){
//debugger;// 断点调试
//1. 定义 url 和参数
var url=”log/doFindPageObjects”
var params={“pageCurrent”:1};//pageCurrent=2
//2. 发动异步申请
// 请问如下 ajax 申请的回调函数参数名能够是任意吗?// 能够, 必须合乎标识符的标准
$.getJSON(url,params,function(result){
// 请问 result 是一个字符串还是 json 格局的 js 对象?对象
doHandleQueryResponseResult(result);
}
);// 非凡的 ajax 函数
}
result 后果对象剖析,如图 - 9 所示:
图 -9
第三步:定义回调函数,解决服务端的响应后果。代码如下:
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=$(“<tr></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=”<td><input type=’checkbox’ class=’cBox’ name=’cItem’ value='”+data.id+”‘></td>”+
“<td>”+data.username+”</td>”+
“<td>”+data.operation+”</td>”+
“<td>”+data.method+”</td>”+
“<td>”+data.params+”</td>”+
“<td>”+data.ip+”</td>”+
“<td>”+data.time+”</td>”;
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(){
//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};
// 为什么此地位要获取查问参数的值?
// 一种冗余的利用办法,目标时让此函数在查问时能够重用。
var username=$(“#searchNameId”).val();
// 如下语句的含意是什么?动静在 json 格局的 js 对象中增加 key/value,
if(username) params.username=username;// 查问时须要
//2. 发动异步申请
// 请问如下 ajax 申请的回调函数参数名能够是任意吗?能够, 必须合乎标识符的标准
$.getJSON(url,params,function(result){
// 请问 result 是一个字符串还是 json 格局的 js 对象?对象
doHandleQueryResponseResult(result);
}
);
}
-
日志治理删除操作实现
-
数据架构剖析
当用户执行日志删除操作时,客户端与服务端交互时的根本数据架构,如图 -10 所示。
图 -10
-
删除业务时序剖析
客户端提交删除申请,服务端对象的工作时序剖析,如图 -11 所示。
图 -11
-
服务端要害业务及代码实现
-
Dao 接口实现
- 业务形容及设计实现
数据层基于业务层提交的日志记录 id,进行日志删除操作。
- 要害代码设计及实现:
在 SysLogDao 中增加基于 id 执行日志删除的办法。代码参考如下:
int deleteObjects(@Param(“ids”)Integer… ids);
-
Mapper 文件实现
- 业务形容及设计实现
在 SysLogDao 接口对应的映射文件中增加用于执行删除业务的 delete 元素,此元素外部定义具体的 SQL 实现。
- 要害代码设计与实现
在 SysLogMapper.xml 文件增加 delete 元素,要害代码如下:
<delete id=”deleteObjects”>
delete from sys_Logs
where id in
<foreach collection=”ids”
open=”(“
close=”)”
separator=”,”
item=”id”>
#{id}
</foreach>
</delete>
FAQ 剖析: 如上 SQL 实现可能会存在什么问题?(可靠性问题, 性能问题)
从可靠性的角度剖析, 如果 ids 的值为 null 或长度为 0 时,SQL 构建可能会呈现语法问题,可参考如下代码进行改良(先对 ids 的值进行断定):
<delete id=”deleteObjects”>
delete from sys_logs
<if test=”ids!=null and ids.length>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 的语法。
-
Service 接口及实现类
- 业务形容与设计实现
在日志业务层定义用于执行删除业务的办法,首先通过办法参数接管管制层传递的多个记录的 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;
try{
rows=sysLogDao.deleteObjects(ids);
}catch(Throwable e){
e.printStackTrace();
// 发出报警信息(例如给运维人员发短信)
throw new ServiceException(“ 系统故障,正在复原中 …”);
}
//4. 对后果进行验证
if(rows==0)
throw new ServiceException(“ 记录可能曾经不存在 ”);
//5. 返回后果
return rows;
}
-
Controller 类实现
- 业务形容与设计实现
在日志管制层对象中,增加用于解决日志删除申请的办法。首先在此办法中通过形参接管客户端提交的数据,而后调用业务层对象执行删除操作,最初封装执行后果,并在运行时将响应对象转换为 JSON 格局的字符串,响应到客户端。
- 要害代码设计与实现
第一步:在 SysLogController 中增加用于执行删除业务的办法。代码如下:
@RequestMapping(“doDeleteObjects”)
@ResponseBody
public JsonResult doDeleteObjects(Integer… ids){
sysLogService.deleteObjects(ids);
return new JsonResult(“delete ok”);
}
第二步:启动 tomcat 进行拜访测试,关上浏览器输出如下网址:
http://localhost/log/doDelete…
-
客户端要害业务及代码实现
-
日志列表页面事件处理
- 业务形容及设计实现
用户在页面上首先抉择要删除的元素,而后点击删除按钮,将用户抉择的记录 id 异步提交到服务端,最初在服务端执行日志的删除动作。
- 要害代码设计与实现
第一步:页面加载实现当前,在删除按钮上进行点击事件注册。要害代码如下:
…
$(“.input-group-btn”)
.on(“click”,”.btn-delete”,doDeleteObjects)
…
第二步:定义删除操作对应的事件处理函数。要害代码如下:
function doDeleteObjects(){
//1. 获取选中的 id 值
var ids=doGetCheckedIds();
if(ids.length==0){
alert(“ 至多抉择一个 ”);
return;
}
//2. 发异步申请执行删除操作
var url=”log/doDeleteObjects”;
var params={“ids”:ids.toString()};
console.log(params);
$.post(url,params,function(result){
if(result.state==1){
alert(result.message);
doGetObjects();
}else{
alert(result.message);
}
});
}
第三步:定义获取用户选中的记录 id 的函数。要害代码如下:
function doGetCheckedIds(){
// 定义一个数组, 用于存储选中的 checkbox 的 id 值
var array=[];//new Array();
// 获取 tbody 中所有类型为 checkbox 的 input 元素
$(“#tbodyId input[type=checkbox]”).
// 迭代这些元素,每发现一个元素都会执行如下回调函数
each(function(){
// 假如此元素的 checked 属性的值为 true
if($(this).prop(“checked”)){
// 调用数组对象的 push 办法将选中对象的值存储到数组
array.push($(this).val());
}
});
return array;
}
第四步:Thead 中全选元素的状态影响 tbody 中 checkbox 对象状态。代码如下:
function doChangeTBodyCheckBoxState(){
//1. 获取以后点击对象的 checked 属性的值
var flag=$(this).prop(“checked”);//true or false
//2. 将 tbody 中所有 checkbox 元素的值都批改为 flag 对应的值。
// 第一种计划
/* $(“#tbodyId input[name=’cItem’]”)
.each(function(){
$(this).prop(“checked”,flag);
}); */
// 第二种计划
$(“#tbodyId input[type=’checkbox’]”)
.prop(“checked”,flag);
}
第五步:Tbody 中 checkbox 的状态影响 thead 中全选元素的状态。代码如下:
function doChangeTHeadCheckBoxState(){
//1. 设定默认状态值
var flag=true;
//2. 迭代所有 tbody 中的 checkbox 值并进行与操作
$(“#tbodyId input[type=’checkbox’]”)
.each(function(){
flag=flag&$(this).prop(“checked”)
});
//3. 批改全选元素 checkbox 的值为 flag
$(“#checkAll”).prop(“checked”,flag);
}
第六步:欠缺业务刷新办法,当在最初一页执行删除操作时,基于全选按钮状态及以后页码值,刷新页面。要害代码如下:
function doRefreshAfterDeleteOK(){
var pageCount=$(“#pageId”).data(“pageCount”);
var pageCurrent=$(“#pageId”).data(“pageCurrent”);
var checked=$(“#checkAll”).prop(“checked”);
if(pageCurrent==pageCount&&checked&&pageCurrent>1){
pageCurrent–;
$(“#pageId”).data(“pageCurrent”,pageCurrent);
}
doGetObjects();
}
阐明:最初将如上办法增加在删除操作胜利当前的代码块中。
-
日志治理数据增加实现
-
服务端要害业务及代码实现
这块业务学了 AOP 当前再实现.
-
Dao 接口实现
- 业务形容与设计实现
数据层基于业务层的长久化申请,将业务层提交的用户行为日志信息写入到数据库。
- 要害代码设计与实现
在 SysLogDao 接口中增加用于实现日志信息长久化的办法。要害代码如下:
int insertObject(SysLog entity);
-
Mapper 映射文件
- 业务形容与设计实现
基于 SysLogDao 中办法的定义,编写用于数据长久化的 SQL 元素。
- 要害代码设计与实现
在 SysLogMapper.xml 中增加 insertObject 元素,用于向日志表写入用户行为日志。要害代码如下:
<insert id=”insertObject”>
insert into sys_logs
(username,operation,method,params,time,ip,createdTime)
values
(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
</insert>
-
Service 接口及实现类
- 业务形容与设计实现
将日志切面中抓取到的用户行为日志信息,通过业务层对象办法长久化到数据库。
- 要害代码实现
第一步:在 SysLogService 接口中,增加保留日志信息的办法。要害代码如下:
void saveObject(SysLog entity)
第二步:在 SysLogServiceImpl 类中增加,保留日志的办法实现。要害代码如下:
@Override
public void saveObject(SysLog entity) {
sysLogDao.insertObject(entity);
}
-
日志切面 Aspect 实现
- 业务形容与设计实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象而后传递到业务,通过业务层对象对日志日志信息做进一步解决。此局部内容后续联合 AOP 进行实现(临时先理解,不做具体实现)。
- 要害代码设计与实现
定义日志切面类对象,通过盘绕告诉解决日志记录操作。要害代码如下:
@Aspect
@Component
public class SysLogAspect {
private Logger log=LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private SysLogService sysLogService;
@Pointcut(“@annotation(com.cy.pj.common.annotation.RequiredLog)”)
public void logPointCut(){}
@Around(“logPointCut()”)
public Object around(ProceedingJoinPoint // 连接点
jointPoint) throws Throwable{
long startTime=System.currentTimeMillis();
// 执行指标办法(result 为指标办法的执行后果)
Object result=jointPoint.proceed();
long endTime=System.currentTimeMillis();
long totalTime=endTime-startTime;
log.info(“ 办法执行的总时长为:”+totalTime);
saveSysLog(jointPoint,totalTime);
return result;
}
private void saveSysLog(ProceedingJoinPoint point,
long totleTime) throws NoSuchMethodException, SecurityException, JsonProcessingException{
//1. 获取日志信息
MethodSignature ms= (MethodSignature)point.getSignature();
Class<?> targetClass=point.getTarget().getClass();
String className=targetClass.getName();
// 获取接口申明的办法
String methodName=ms.getMethod().getName();
Class<?>[] parameterTypes=ms.getMethod().getParameterTypes();
// 获取指标对象办法(AOP 版本不同, 可能获取办法对象形式也不同)
Method targetMethod=targetClass.getDeclaredMethod(
methodName,parameterTypes);
// 获取用户名, 学完 shiro 再进行自定义实现, 没有就先给固定值
String username=ShiroUtils.getPrincipal().getUsername();
// 获取办法参数
Object[] paramsObj=point.getArgs();
System.out.println(“paramsObj=”+paramsObj);
// 将参数转换为字符串
String params=new ObjectMapper()
.writeValueAsString(paramsObj);
//2. 封装日志信息
SysLog log=new SysLog();
log.setUsername(username);// 登陆的用户
// 如果指标办法对象上有注解, 咱们获取注解定义的操作值
RequiredLog requestLog=
targetMethod.getDeclaredAnnotation(RequiredLog.class);
if(requestLog!=null){
log.setOperation(requestLog.value());
}
log.setMethod(className+”.”+methodName);//className.methodName()
log.setParams(params);//method params
log.setIp(IPUtils.getIpAddr());//ip 地址
log.setTime(totleTime);//
log.setCreateDate(new Date());
//3. 保留日志信息
sysLogService.saveObject(log);
}
}
办法中用到的 ip 地址获取须要提供一个如下的工具类:(不必本人实现,间接用)
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;
}
}
原理剖析,如图 -12 所示:
图 -12
-
总结
-
重难点剖析
- 日志治理整体业务剖析与实现。
- 分层架构(应用层 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 串)。
- 日志治理模块异样解决如何实现的。
- 申请解决层 (管制层) 定义对立 (全局) 异样解决类。
- 应用注解 @ControllerAdvice 形容类, 应用 @ExceptionHandler 形容办法.
- 异样解决规定: 能解决则解决, 不能解决则抛出。
-
FAQ 剖析
- 用户行为日志表中都有哪些字段?(面试时有时会问)
- 用户行为日志是如何实现分页查问的?(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 剖析及解决套路?(排除法,打桩,断点)
-
BUG 剖析
- 无奈找到对应的 Bean 对象(NoSuchBeanDefinitionException),如图 -13 所示:
图 -13
问题剖析:
- 检测 key 的名字写的是否正确。
- 检测 spring 对此 Bean 对象的扫描,对于 dao 而言。
- 应用有 @Mapper 注解形容或者在 @MapperScan 扫描范畴之内。
- 以上都正确,要检测是否编译了。
- 绑定异样(BindingException),如图 -14 所示。
图 -14
问题剖析:
- 接口的类全名与对应的映射文件命名空间不同。
- 接口的办法名与对应的映射文件元素名不存在。
- 检测映射文件的门路与 application.properties 或者 application.yml 中的配置是否统一。
- 以上都没有问题时,检测你的类和映射文件是否失常编译。
- 反射异样(ReflectionException),如图 -15 所示
图 -15
问题剖析:
- 映射文件中动静 sql 中应用的参数在接口办法中没有应用 @Param 注解润饰
- 如果应用了注解润饰还要检测名字是否统一。
阐明:当动静 sql 的参数在接口中没有应用 @Param 注解润饰,还能够借助_parameter 这个变量获取参数的值(mybatis 中的一种标准)。
- 后果映射异样,如图 -16 所示:
图 -16
问题剖析:getRowCount 元素可能没有写 resultType 或 resultMap。
- 绑定异样,如图 -17 所示:
图 -17
问题剖析: 绑定异样, 检测 findPageObjects 办法参数与映射文件参数名字是否匹配或者如果版本不是最新版本须要应用 @Param 注解形容。
- Bean 创立异样,如图 -18 所示
图 -18
问题剖析:应该是查问时的后果映射对的类全名写错了。
- 申请形式不匹配,如图 -19 示
图 -19
问题剖析:申请形式与管制层解决形式不匹配。
- 响应后果异样,如图 -20 所示:
图 -20
问题剖析:服务端响应数据不正确,例如服务端没有注册将对象转换为 JSON 串的 Bean
- 申请参数异样,如图 -21 示:
图 -21
问题剖析:客户端申请参数中不蕴含服务端管制层办法参数或格局不匹配。
- JS 编写谬误,如图 -22 所示:
图 -22
问题剖析:点击右侧 VM176:64 地位进行查看。
- JS 编写谬误,如图 -23 所示:
图 -23
问题剖析:找到对应地位,检测 data 的值以及数据起源。
- JS 编写谬误,如图 -24 所示:
图 -24
问题剖析:找到对应地位,如果无奈确定地位,可排除法或打桩,debug 剖析。
- JS 写谬误,如图 -25 所示:
图 -25
问题剖析:调用 length 办法的对象有问题,可先检测下对象的值。
- JS 编写谬误,如图 -26 所示:
图 -26
问题剖析:检测 record 定义或赋值的中央。
- 资源没找到,如图 -27 所示:
图 -27
问题剖析: 服务端资源没找到, 查看 url 和 controller 映射, 不要点击图中的 jquery。
- 视图解析异样,如图 -28 所示:
图 -28
问题剖析: 查看服务端要拜访的办法上是否有 @ResponseBody 注解.
**