-
业务设计说明
本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下:
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所示:
说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。
-
日志管理列表页面呈现
-
业务时序分析
当点击首页左侧的"日志管理"菜单时,其总体时序分析如图-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所示。
-
服务端API架构及业务时序图分析
服务端日志分页查询代码基本架构,如图-5所示:
服务端日志列表数据查询时序图,如图-6所示:
-
服务端关键业务及代码实现
-
Entity类实现
- 业务描述及设计实现
构建实体对象(POJO)封装从数据库查询到的记录,一行记录映射为内存中一个的这样的对象。对象属性定义时尽量与表中字段有一定的映射关系,并添加对应的set/get/toString等方法,便于对数据进行更好的操作。
基于Dao接口创建映射文件,在此文件中通过相关元素(例如select)描述要执行的数据操作。
第一步:在映射文件的设计目录(mapper/sys)中添加SysLogMapper.xml映射文件,代码如下:
- 关键代码分析及实现
-
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文件实现
- 业务描述及设计实现
- 关键代码设计及实现
<?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.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;
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 {