springboot动吧项目 日志模块
Posted wjcbk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot动吧项目 日志模块相关的知识,希望对你有一定的参考价值。
1 项目简介
1.1 概述
动吧旅游生态系统,应市场高端用户需求,公司决定开发这样的一套旅游系统,此系统包含旅游电商系统(广告子系统,推荐子系统,评价子系统,商品子系统,订单子系统,…),旅游分销系统(分销商的管理),旅游业务系统(产品研发,计调服务,系统管理,..),,。。。
1.2 原型分析
基于用户需求,进行原型设计(基于html+css+js进行静态页面实现)。例如系统登录页面:
系统登录成功页面(例如starter.html)
菜单展示页面
2 技术架构
2.1 项目分层架构
本项目应用层基于MVC设计思想,进行分层架构设计,其核心目的是将复杂问题简单化,实现各司其职,各尽所能.然后基于“高内聚,低耦合”的设计思想,再实现各对象之间协同,从而提高系统的可维护性,可扩展性。
其中:
1.开放接口层:可直接封装 Service 方法暴露成 RPC (远程过程调用)接口;也可通过 Web 封装成 http 接口;同时也可进行网关安全控制、流量控制等。
2.终端显示层:负责各个端的模板渲染并显示。当前主要是 thymeleaf 渲染,JS 渲染,移动端展示等。
3.Web请求处理层:主要是对访问控制进行转发,请求参数校验,响应结果处理等
4.Service 层:相对具体的业务逻辑服务层(核心业务,扩展业务)。
5.Manager 层:通用业务处理层,它有如下特征:
1) 对第三方平台封装的层,预处理返回结果及转化异常信息;
2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
3) 与 DAO 层交互,对多个 DAO 的组合复用。
6.DAO 层:数据访问层,与底层 mysql、Oracle、Hbase 等进行数据交互。
7.外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的 HTTP 接口
2.2 API应用架构
整体API应用架构:
3 技术整合
3.1 环境准备
3.1.1 数据库初始化
3.1.2 IDE配置初始化(STS)
3.2 创建项目
3.2.1 添加项目依赖
3.2.1 修改配置文件
在application.yml文件中添加如下配置(server,datasource,mybatis,mvc)
#spring datasource spring.datasource.url=jdbc:mysql:///dbsys?serverTimezone=GMT%2B8&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=root #mybatis mybatis.mapper-locations=classpath:/mapper/*/*.xml #spring web spring.thymeleaf.prefix=classpath:/templates/pages/ spring.thymeleaf.cache=false #Spring log logging.level.com.cy=debug
3.3 首页初始化
3.3.1 定义页面初始资源
-
将js、css、images相关资源拷贝到项目static目录
-
将pages页面拷贝到项目的templates目录
3.3.2 创建页面Controller
创建呈现首页页面的controller对象。
package com.cy.pj.sys.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("/") @Controller public class PageController { @RequestMapping("doIndexUI") public String doIndexUI(){ return "starter"; } }
3.3.3 启动项目进行测试
启动tomcat,在地址栏输入http://localhost/doIndexUI(地址中的端口号要参考自己tomcat启动端口)地址进行访问,假如没有问题会呈现如下页面
页面访问流程分析,如下图所示(了解):
-
业务设计说明
本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下:
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所示。
-
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); }); }
-
日志列表页面事件处理
-
业务描述与设计实现
当日志列表页面加载完成以后异步加载分页页面(page.html)。
-
关键代码设计与实现:
在log_list.html页面中异步加载page页面,这样可以实现分页页面重用,哪里需要分页页面,哪里就进行页面加载即可。关键代码如下:
$(function(){ $("#pageId").load("doPageUI"); });
-
日志管理列表数据呈现
-
数据架构分析
日志查询服务端数据基本架构,如图-4所示。
-
服务端API架构及业务时序图分析
服务端日志分页查询代码基本架构,如图-5所示:
服务端日志列表数据查询时序图,如图-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>
第五步:单元测试类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是一个自己定义的异常, 通过自定义异常可更好的实现对业务问题的描述,同时可以更好的提高用户体验。参考代码如下:
ackage 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所示:
-
客户端关键业务及代码实现
-
客户端页面事件分析
当用户点击首页日志管理时,其页面流转分析如图-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.发起异步请求 //argparse 代码片段只打印部分日志