第三阶段实战(二)——日志管理功能设计与实现

1 日志管理设计说明

1.1 业务设计说明

本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方 法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下:

CREATE​ ​TABLE`sys_logs` (   
	`id` bigint(20)NOT​ ​NULLAUTO_INCREMENT,
    `username`varchar(50)DEFAULT​ ​ ​NULLCOMMENT'登陆用户名',
    `operation`varchar(50)DEFAULT​ ​NULLCOMMENT'用户操作',
    `method`varchar(200)DEFAULT​ ​NULLCOMMENT'请求方法',
    `params`varchar(5000)DEFAULT​ ​NULLCOMMENT'请求参数',
    `time` bigint(20)NOT​ ​NULLCOMMENT'执行时长(毫秒)',
    `ip`varchar(64)DEFAULT​ ​NULLCOMMENT'IP地址',   
    `createdTime` datetimeDEFAULT​ ​NULLCOMMENT'日志记录时间',PRIMARY​ ​KEY(`id`) ) 
    ENGINE=InnoDB AUTO_INCREMENT=1DEFAULTCHARSET=utf8 COMMENT='系统 日志';

1.2 原型设计说明
基于用户需求,实现静态页面(html/css/js),通过静态页面为用户呈现基本需求实现, 如图-1所示。
第三阶段实战(二)——日志管理功能设计与实现_第1张图片
说明:假如客户对此原型进行了确认,后续则可以基于此原型进行研发。

1.3 API设计说明

日志业务后台API分层架构及调用关系如图-2所示:
第三阶段实战(二)——日志管理功能设计与实现_第2张图片
说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。

2 日志管理列表页面呈现

2.1 业务时序分析

当点击首页左侧的"日志管理"菜单时,其总体时序分析如图-3所示:
第三阶段实战(二)——日志管理功能设计与实现_第3张图片

2.2 服务端实现

2.2.1 Controller实现

业务描述与设计实现 基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法 分别用于返回日志列表页面,日志分页页面。
关键代码设计与实现 第一步:在PageController中定义返回日志列表的方法。代码如下:

@RequestMapping("log/log_list")
public String doLogUI(){
	return "sys/log_list";
}

第二步:在PageController中定义用于返回分页页面的方法。代码如下:

@RequestMaping("doPageUI")
public String doPageUI(){
	return "common/page";
}

2.3 客户端实现

2.3.1 日志菜单事件处理

业务描述与设计
首 先 准 备 日 志 列 表 页 面 (/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 异步请求函数。

2.3.2 日志列表页面事件处理

业务描述与设计实现
当日志列表页面加载完成以后异步加载分页页面(page.html)。
关键代码设计与实现:
在 log_list.html 页面中异步加载 page 页面,这样可以实现分页页面重用,哪里需 要分页页面,哪里就进行页面加载即可。关键代码如下:

$(function(){
	$("#pageId").load("doPageUI");
});

说明:数据加载通常是一个相对比较耗时操作,为了改善用户体验,可以先为用户呈 现一个页面,数据加载时,显示数据正在加载中,数据加载完成以后再呈现数据。这样也可 满足现阶段不同类型客户端需求(例如手机端,电脑端,电视端,手表端。)

3 日志管理列表数据呈现

3.1 数据架构分析

日志查询服务端数据基本架构,如图-4 所示。
第三阶段实战(二)——日志管理功能设计与实现_第4张图片

3.2 服务端 API 架构及业务时序图分析

服务端日志分页查询代码基本架构,如图-5 所示:
第三阶段实战(二)——日志管理功能设计与实现_第5张图片
服务端日志列表数据查询时序图,如图-6 所示:
第三阶段实战(二)——日志管理功能设计与实现_第6张图片

3.3 服务端关键业务及代码实现

3.3.1 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 方法可能会在什么场景用到?

3.3.2 Dao 接口实现

  • 业务描述及设计实现
    通过数据层对象,基于业务层参数数据查询日志记录总数以及当前页要呈现的用户行 为日志信息。
  • 关键代码分析及实现: 第一步:定义数据层接口对象,通过将此对象保证给业务层以提供日志数据访问。代 码如下:
@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 注 解进行修饰并定义。

3.3.3 Mapper 文件实现

业务描述及设计实现 基于 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); 
		}
	}
}

3.3.4 Service 接口及实现类

业务描述与设计实现 :
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通 过业务方法中的参数接收控制层数据(例如 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 对象的单元测试类,代码如下:
第三阶段实战(二)——日志管理功能设计与实现_第7张图片

3.3.5 Controller 类实现

业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请 求参数,然后通过业务层对象执行业务逻辑,再通过 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);
    }
}

控制层响应数据处理分析,如图
第三阶段实战(二)——日志管理功能设计与实现_第8张图片

3.4 客户端关键业务及代码实现

3.4.1 客户端页面事件分析

当用户点击首页日志管理时,其页面流转分析如图
第三阶段实战(二)——日志管理功能设计与实现_第9张图片

3.4.2 日志列表信息呈现

业务描述与设计实现
日志分页页面加载完成以后,向服务端发起异步请求加载日志信息,当日志信息加载 完成需要将日志信息、分页信息呈现到列表页面上。
关键代码设计与实现
第一步:分页页面加载完成,向服务端发起异步请求,代码参考如下:

$(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 函数    
} 

result 结果对象分析,如图
第三阶段实战(二)——日志管理功能设计与实现_第10张图片
第三步:定义回调函数,处理服务端的响应结果。代码如下:

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;    
 }

3.4.3 分页数据信息呈现

业务描述与设计实现
日志信息列表初始化完成以后初始化分页数据(调用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 函数 }

3.4.4 列表页面信息查询实现

业务描述及设计
当用户点击日志列表的查询按钮时,基于用户输入的用户名进行有条件的分页查询, 并将查询结果呈现在页面。
关键代码设计与实现:
第一步:日志列表页面加载完成,在查询按钮上进行事件注册。代码如下:

$(".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);//处理响应结果
	   })
   }

4 日志管理删除操作实现

4.1 数据架构分析

当用户执行日志删除操作时,客户端与服务端交互时的基本数据架构,如图:
第三阶段实战(二)——日志管理功能设计与实现_第11张图片

4.2 删除业务时序分析

客户端提交删除请求,服务端对象的工作时序分析,如图
第三阶段实战(二)——日志管理功能设计与实现_第12张图片

4.3 服务端关键业务及代码实现

4.3.1 Dao 接口实现

业务描述及设计实现
数据层基于业务层提交的日志记录 id,进行日志删除操作。
关键代码设计及实现:
在 SysLogDao 中添加基于 id 执行日志删除的方法。代码参考如下:
第三阶段实战(二)——日志管理功能设计与实现_第13张图片
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 的语法。

4.3.3 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 = 0;
        try{
            rows = sysLogDao.deleteObjects(ids);
        }catch(Throwable e){
            e.printStackTrace();
            //发出报警信息(例如给运维人员发短信)
            System.out.println("系统故障,恢复中。。。");
        }
        //3、对结果进行验证
        if (rows == 0) throw new ServiceException("记录可能已经不存在");
        //5、返回结果
        return rows;
    }

4.3.4 Controller 类实现

业务描述与设计实现
在日志控制层对象中,添加用于处理日志删除请求的方法。首先在此方法中通过形参 接收客户端提交的数据,然后调用业务层对象执行删除操作,最后封装执行结果,并在运 行时将响应对象转换为 JSON 格式的字符串,响应到客户端。
关键代码设计与实现
第一步:在 SysLogController 中添加用于执行删除业务的方法。代码如下:

@RequestMapping("doDeleteObjects")
public JsonResult doDeleteOnjects(Integer... ids){
    return new JsonResult(sysLogService.deleteObjects(ids));
}

第二步:启动 tomcat 进行访问测试,打开浏览器输入如下网址:
http://localhost/log/doDeleteObjects?ids=1,2,3

4.4 客户端关键业务及代码实现

4.4.1 日志列表页面事件处理

业务描述及设计实现
用户在页面上首先选择要删除的元素,然后点击删除按钮,将用户选择的记录 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();   
   }

说明:最后将如上方法添加在删除操作成功以后的代码块中。

5 日志管理数据添加实现

5.1 服务端关键业务及代码实现

这块业务学了 AOP 以后再实现.

5.1.1 Dao 接口实现

业务描述与设计实现
数据层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。
关键代码设计与实现
在 SysLogDao 接口中添加用于实现日志信息持久化的方法。关键代码如下:

int insertObject(SysLog entity); 

5.1.2 Mapper 映射文件

业务描述与设计实现
基于 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>

5.1.3 Service 接口及实现类

业务描述与设计实现
将日志切面中抓取到的用户行为日志信息,通过业务层对象方法持久化到数据库。
关键代码实现
第一步:在 SysLogService 接口中,添加保存日志信息的方法。关键代码如下:

void saveObjects(SysLog entity);

第二步:在 SysLogServiceImpl 类中添加,保存日志的方法实现。关键代码如下:

5.1.4 日志切面 Aspect 实现

业务描述与设计实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业 务层对象对日志日志信息做进一步处理。此部分内容后续结合 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;
		}

}

第三阶段实战(二)——日志管理功能设计与实现_第14张图片

6 总结

6.1 重难点分析

日志管理整体业务分析与实现。

  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. 异常处理规则:能处理则处理,不能处理则抛出。

6.2 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分析及解决套路?(排除法,打桩(log),断点,搜索引擎)

你可能感兴趣的:(Java进阶第三阶段实战案例,spring,java)