1 日志管理设计说明
1.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= '系统
日志' ;
1.2 原型设计说明
基于用户需求,实现静态页面(html/css/js),通过静态页面为用户呈现基本需求实现,
如图-1所示。
说明:假如客户对此原型进行了确认,后续则可以基于此原型进行研发。
1.3 API设计说明
日志业务后台API分层架构及调用关系如图-2所示:
说明:分层目的主要讲复杂问题简单化,实现各司其职,各尽所能
二日志管理列表页面呈现
2.1业务时序分析
单点击首页左侧的“日志管理菜单时,其总体时序分析如图所示:
2.2服务器端实现
2.2.1 Controller实现
业务描述与环境实现
基于日志管理的请求业务,在PageControler中添加doLogUI方法,doPageUI方法分别返回日志列表页面,日志分页页面。
关键代码设计与实现
找到项目中start.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.1数据架构分析
日志查询服务端数据基本架构,如图所示:
3.2服务端API架构及业务时序图分析
服务端日志分页查询代码基本架构,如图所示:
3.3.1 pojo类实现
构建实体对象(pojo)封装从数据库查询到的记录,一行记录映射为内存中一个这样的对象。对象属性定义时尽量与表中字段有一定的映射关系,并添加对应的set/get/toString等方法,便于对数据进行更好的操作。
关键代码分析及实现
package com.cy.pj.sys.entity;
import java.io.Serializable;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
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;
}
说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据,实现层与层之间数据的传递。
思考:这个对象的set方法,get方法可能会在什么场景用到?
3.3.2Dao接口实现
业务描述
通过数据层对象,基于业务层参数数据查询日志记录总数以及当前页面呈现的用户行为日志信息。
关键代码实现与分析
第一步:定影数据层接口对象,通过此对象保证给业务层以提供日志数据访问,代码如下:
@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注解进行修饰并定义。
3.3.3Mapper文件实现
业务描述集设计实现
基于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>
思考:
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);
}
}
}
3.3.4 Service接口及实现类
业务描述与设计实现
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务对象首先要通过业务方法中的参数接收控制层数据(例如username,PageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,在基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。
关键代码及实现
业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考如下:
package com.cy.db.common.bo;
import java.io.Serializable;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 基于此对象封装数据层返回的数据以及计算的分页信息
* @author Administrator
* @param <T>
*/
@Data
@AllArgsConstructor
@Accessors(chain = true)
public class PageObject<T> implements Serializable {
private static final long serialVersionUID = 2116236024301796845L;
/**当前页的页码值*/
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;
}
}
定义日志业务接口的方法 ,暴露外界对日志业务数据的访问,其代码参考如下:
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);
}
}
3.3.5 Controler类实现
业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请求参数,然后通过业务层对象执行业务逻辑,再通过POJO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。
package com.cy.db.common.vo;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class JsonResult implements Serializable {
private static final long serialVersionUID = -3024401354172610541L;
/**状态码*/
private int state=1;//1表示SUCCESS,0表示ERROR
/**状态信息*/
private String message="ok";
/**正确数据*/
private Object data;
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;
@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);//封装
}
}