本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操做,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操做。其表设计语句以下:css
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),经过静态页面为用户呈现基本需求实现,如图所示。
说明:假如客户对此原型进行了确认,后续则能够基于此原型进行研发。html
日志业务后台API分层架构及调用关系如图所示:
说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。java
当点击首页左侧的"日志管理"菜单时,其整体时序分析如图所示:jquery
▪ 业务描述与设计实现
基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法分别用于返回日志列表页面,日志分页页面。web
▪ 关键代码设计与实现
第一步:在PageController中定义返回日志列表的方法。代码以下:面试
@RequestMapping("{module}/{moduleUI}") public String doModuleUI(@PathVariable String moduleUI){ return "sys/"+moduleUI; }
第二步:在PageController中定义用于返回分页页面的方法。代码以下:ajax
@RequestMapping("doPageUI") public String doPageUI() { return "common/page"; }
▪ 业务描述与设计
首先准备日志列表页面(/templates/pages/sys/log_list.html),而后在starter.html页面中点击日志管理菜单时异步加载日志列表页面。spring
▪ 关键代码设计与实现
找到项目中的starter.html 页面,页面加载完成之后,注册日志管理菜单项的点击事件,当点击日志管理时,执行事件处理函数。关键代码以下:sql
$(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.pojo; 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; } }
说明:经过此对象除了能够封装从数据库查询的数据,还能够封装客户端请求数据,实现层与层之间数据的传递。
▪ 业务描述及设计实现
经过数据层对象,基于业务层参数数据查询日志记录总数以及当前页要呈现的用户行为日志信息。
▪ 关键代码分析及实现:
第一步:定义数据层接口对象,经过将此对象保证给业务层以提供日志数据访问。代码以下:
package com.cy.pj.sys.dao; import org.apache.ibatis.annotations.Mapper; @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);
说明:
1) 当DAO中方法参数多余一个时尽可能使用@Param注解进行修饰并指定名字,而后在Mapper文件中即可以经过相似#{username}方式进行获取,不然只能经过#{arg0},#{arg1}或者#{param1},#{param2}等方式进行获取。
2) 当DAO方法中的参数应用在动态SQL中时不管多少个参数,尽可能使用@Param注解进行修饰并定义。
▪ 业务描述及设计实现
基于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>
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 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); } } }
▪ 业务描述与设计实现
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要经过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。而后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。
▪ 关键代码设计及实现
业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考以下:
package com.cy.pj.common.pojo.bo; import lombok.Data; import java.io.Serializable; import java.util.List; @Data 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.impl; import com.cy.pj.common.exception.ServiceException; import com.cy.pj.common.pojo.bo.PageObject; import com.cy.pj.sys.dao.SysLogDao; import com.cy.pj.sys.pojo.SysLog; import com.cy.pj.sys.service.SysLogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @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 = 5843835376260549700L; 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); } }
▪ 业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要经过控制层对象处理请求参数,而后经过业务层对象执行业务逻辑,再经过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。
▪ 关键代码设计与实现
定义控制层值对象(VO),目的是基于此对象封装控制层响应结果(在此对象中主要是为业务层执行结果添加状态信息)。Spring MVC框架在响应时能够调用相关API(例如jackson)将其对象转换为JSON格式字符串。
package com.cy.pj.common.pojo.vo; import lombok.Data; import java.io.Serializable; @Data public class JsonResult implements Serializable { private static final long serialVersionUID = -6205785765802397766L;//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();//返回错误信息 } }
定义Controller类,并将此类对象使用Spring框架中的@Controller注解进行标识,表示此类对象要交给Spring管理。而后基于@RequestMapping注解为此类定义根路径映射。代码参考以下:
package com.cy.pj.sys.controller; import com.cy.pj.sys.service.SysLogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @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 com.cy.pj.common.pojo.vo.JsonResult; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice public class GlobalExceptionHandler { //JDK中的自带的日志API @ExceptionHandler(RuntimeException.class) @ResponseBody public JsonResult doHandleRuntimeException( RuntimeException e){ e.printStackTrace();//也能够写日志异常信息 return new JsonResult(e);//封装 } }
控制层响应数据处理分析,如图所示:
当用户点击首页日志管理时,其页面流转分析如图-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 结果对象分析,如图所示:
第三步:定义回调函数,处理服务端的响应结果。代码以下:
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); } ); }
当用户执行日志删除操做时,客户端与服务端交互时的基本数据架构,如图所示。
客户端提交删除请求,服务端对象的工做时序分析,如图所示。
▪ 业务描述及设计实现
数据层基于业务层提交的日志记录id,进行日志删除操做。
▪ 关键代码设计及实现:
在SysLogDao中添加基于id执行日志删除的方法。代码参考以下:
int deleteObjects(@Param("ids")Integer... ids);
▪ 业务描述及设计实现
在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的语法。
▪ 业务描述与设计实现
在日志业务层定义用于执行删除业务的方法,首先经过方法参数接收控制层传递的多个记录的id,并对参数id进行校验。而后基于日志记录id执行删除业务实现。最后返回业务执行结果。
▪ 关键代码设计与实现
第一步:在SysLogService接口中,添加基于多个id进行日志删除的方法。关键代码以下:
int deleteObjects(@Param("ids")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; }
▪ 业务描述与设计实现
在日志控制层对象中,添加用于处理日志删除请求的方法。首先在此方法中经过形参接收客户端提交的数据,而后调用业务层对象执行删除操做,最后封装执行结果,并在运行时将响应对象转换为JSON格式的字符串,响应到客户端。
▪ 关键代码设计与实现
第一步:在SysLogController中添加用于执行删除业务的方法。代码以下:
@RequestMapping("doDeleteObjects") @ResponseBody public JsonResult doDeleteObjects(Integer... ids){ sysLogService.deleteObjects(ids); return new JsonResult("delete ok"); }
第二步:启动tomcat进行访问测试,打开浏览器输入以下网址:
http://localhost/log/doDeleteObjects?ids=1,2,3
▪ 业务描述及设计实现
用户在页面上首先选择要删除的元素,而后点击删除按钮,将用户选择的记录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之后再实现.
▪ 业务描述与设计实现
数据层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。
▪ 关键代码设计与实现
在SysLogDao接口中添加用于实现日志信息持久化的方法。关键代码以下:
int insertObject(SysLog sysLog);
▪ 业务描述与设计实现
基于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>
▪ 业务描述与设计实现
将日志切面中抓取到的用户行为日志信息,经过业务层对象方法持久化到数据库。
▪ 关键代码实现
第一步:在SysLogService接口中,添加保存日志信息的方法。关键代码以下:
void saveObject(SysLog sysLog);
第二步:在SysLogServiceImpl类中添加,保存日志的方法实现。关键代码以下:
@Override public void saveObject(SysLog sysLog) { sysLogDao.insertObject(sysLog); }
▪ 业务描述与设计实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象而后传递到业务,经过业务层对象对日志日志信息作进一步处理。此部份内容后续结合AOP进行实现(暂时先了解,不作具体实现)。
▪ 关键代码设计与实现
springboot工程中应用AOP时,首先要添加以下依赖(假若有则无需添加):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
定义切面使用的注解:
package com.cy.pj.common.annotion; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD)//注解做用域是方法 @Retention(RetentionPolicy.RUNTIME)//注解做用生命周期是运行时 public @interface RequiredLog { String value() default "" ; }
封装两个工具类:
package com.cy.pj.common.utils; 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 { 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; } private static Logger logger = LoggerFactory.getLogger(IPUtils.class); }
package com.cy.pj.common.utils; import com.cy.pj.sys.pojo.SysUser; import org.apache.shiro.SecurityUtils; public class ShiroUtils { public static String getUsername(){ return getUser().getUsername(); } public static SysUser getUser() { return (SysUser) SecurityUtils.getSubject().getPrincipal(); } }
定义日志切面类对象,经过环绕通知处理日志记录操做。关键代码以下:
package com.cy.pj.common.aspect; import com.cy.pj.common.annotion.RequiredLog; import com.cy.pj.common.utils.IPUtils; import com.cy.pj.common.utils.ShiroUtils; import com.cy.pj.sys.pojo.SysLog; import com.cy.pj.sys.service.SysLogService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; 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.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Date; @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); } }
原理分析,如图所示:
▪ 日志管理总体业务分析与实现。
1) 分层架构(应用层MVC:基于spring的mvc模块)。
2) API架构(SysLogDao,SysLogService,SysLogController)。
3) 业务架构(查询,删除,添加用户行为日志)。
4) 数据架构(SysLog,PageObject,JsonResult,..)。
▪ 日志管理持久层映射文件中SQL元素的定义及编写。
1) 定义在映射文件”mapper/sys/SysLogMapper.xml”(必须在加载范围内)。
2) 每一个SQL元素必须提供一个惟一ID,对于select必须指定结果映射(resultType)。
3) 系统底层运行时会将每一个SQL元素的对象封装一个值对象(MappedStatement)。
▪ 日志管理模块数据查询操做中的数据封装。
1) 数据层(数据逻辑)的SysLog对象应用(一行记录一个log对象)。
2) 业务层(业务逻辑)PageObject对象应用(封装每页记录以及对应的分页信息)。
3) 控制层(控制逻辑)的JsonResult对象应用(对业务数据添加状态信息)。
▪ 日志管理控制层请求数据映射,响应数据的封装及转换(转换为json 串)。
1) 请求路径映射,请求方式映射(GET,POST),请求参数映射(直接量,POJO)。
2) 响应数据两种(页面,JSON串)。
▪ 日志管理模块异常处理如何实现的。
1) 请求处理层(控制层)定义统一(全局)异常处理类。
2) 使用注解@RestControllerAdvice描述类,使用@ExceptionHandler描述方法.
3) 异常处理规则:能处理则处理,不能处理则抛出。
▪ 用户行为日志表中都有哪些字段?(面试时有时会问)
▪ 用户行为日志是如何实现分页查询的?(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),断点,搜索引擎)