本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下:
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='系统日志';
完整的数据库信息在我的码云里,可以去码云里自行下载。
码云地址:https://gitee.com/Robert8640/bootstrap-3.3.7-dist.git
日志业务后台API分层架构及调用关系如图:
说明:分层的目的主要将复杂问题简单化,实现各司其职,各尽所能。
程序编写一般分为两种:由上至下和由下至上。
由上至下:通过前端页面分析,写后端代码
由下至上:通过后端提供的URL,参数等信息写前端页面
我们这次通过由上至下的方式去写代码。
首先我们启动服务器,打开谷歌浏览器,访问http://localhost/doIndexUI,按F12打开控制台,然后点开系统管理的日志管理,这时候会发现“log_list”报404错误。我们将其打开。
可以得知日志查询的URL为:http://localhost/log/log_list
请求方式为:GET
业务时序图为:
基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法分别用于返回日志列表页面,日志分页页面。
由时序图可以得知,客户端向服务端发送一个“log/log_list”请求,服务端向客户端传一个“log_list.html”页面;
客户端向服务端发送一个“doPageUI”请求,服务端向客户端传一个“page.html”页面。
第一步:在PageController中定义返回日志列表的方法。代码如下:
@GetMapping("log/log_list")
public String doLogUI() {
return "sys/log_list";
}
第二步:在PageController中定义用于返回分页页面的方法。代码如下:
@RequestMapping("doPageUI")
public String doPageUI() {
return "common/page";
}
再次访问
日志查询服务端数据基本架构如图:
服务端日志分页查询代码基本架构
服务端日志列表数据查询时序图:
构建实体对象(POJO)封装从数据库查询到的记录,一行记录映射为内存中一个的这样的对象。对象属性定义时尽量与表中字段有一定的映射关系,并添加对应的set/get/toString等方法,便于对数据进行更好的操作。
package com.sy.pj.sys.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* @author Robert
*/
@Data
public class SysLog implements Serializable {
private static final long serialVersionUID = 5413565156381864339L;
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;
}
说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据,实现层与层之间数据的传递。
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请求参数,然后通过业务层对象执行业务逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。
定义控制层值对象(VO),目的是基于此对象封装控制层响应结果(在此对象中主要是为业务层执行结果添加状态信息)。Spring MVC框架在响应时可以调用相关API(例如jackson)将其对象转换为JSON格式字符串。
package com.sy.pj.common.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class JsonResult {
/*状态码*/
private int state=1;
/*状态信息*/
private String message="ok";
/*正确数据*/
private Object data;
public JsonResult(int state) {
this.state = state;
}
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控制层发送一个doFindPageObjects请求
package com.sy.pj.sys.controller;
import com.sy.pj.common.pojo.JsonResult;
import com.sy.pj.common.pojo.PageObject;
import com.sy.pj.sys.pojo.SysLog;
import com.sy.pj.sys.service.SysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Robert
*/
@RequestMapping("/log/")
@RestController
public class SysLogController {
@Autowired
private SysLogService sysLogService;
@GetMapping("doFindPageObjects")
public JsonResult doFindPageObjects(String username, Integer pageCurrent){
PageObject pageObject= sysLogService.findPageObjects(username, pageCurrent);
return new JsonResult(pageObject);//封装的是正确的响应结果
}
}
Controller层向业务层发送一个findPageObjects的请求
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。
定义日志业务接口及方法,暴露外界对日志业务数据的访问
package com.sy.pj.sys.service;
import com.sy.pj.common.pojo.PageObject;
import com.sy.pj.sys.pojo.SysLog;
public interface SysLogService {
/**
* @param name 基于条件查询时的参数名
* @param pageCurrent 当前的页码值
* @return 当前页记录+分页信息
*/
PageObject findPageObjects(
String username,
Integer pageCurrent);
}
业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考如下:
package com.sy.pj.common.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* @author Robert
*/
@Data
@NoArgsConstructor //无参构造
public class PageObject implements Serializable {
private static final long serialVersionUID = 8387806564138656480L;
/**总记录数*/
private Integer rowCount;
/**当前页记录*/
private List records;
/**总页数(计算出来)*/
private Integer pageCount;
/**当前页码值*/
private Integer pageCurrent;
/**页面大小(每页最多显示多少条记录)*/
private Integer pageSize;
public PageObject(Integer rowCount, List records, Integer pageCurrent, Integer pageSize) {
this.rowCount = rowCount;
this.records = records;
this.pageCurrent = pageCurrent;
this.pageSize = pageSize;
this.pageCount=rowCount/pageSize;
if(this.rowCount%this.pageSize!=0)this.pageCount++;
}
}
日志业务实现类,用于具体执行日志业务数据的分页查询操作的具体方法
package com.sy.pj.sys.service.serviceimpl;
import com.sy.pj.common.ServiceEcxeption.ServiceException;
import com.sy.pj.common.pojo.PageObject;
import com.sy.pj.sys.dao.SysLogDao;
import com.sy.pj.sys.pojo.SysLog;
import com.sy.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;
/**
* @param username 基于条件查询时的参数名
* @param pageCurrent 当前的页码值
* @return 当前页记录+分页信息
*/
@Override
public PageObject findPageObjects(String username, Integer pageCurrent) {
// 1.验证参数的合法性
// 1.1 验证pageCurrent的合法性
// 不合法抛出IllegalArgumentException异常
if (pageCurrent == null||pageCurrent<1) {
throw new IllegalArgumentException("当前页码值不正确");
}
//2.基于条件查询总记录数
//2.1) 执行查询
int rowCount =sysLogDao.getRowCount(username);
if (rowCount == 0) {
throw new ServiceException("系统没有查到对应记录");
}
//3.基于条件查询当前页记录(pageSize定义为5)
//3.1)定义pageSize
int pageSize=5;
//3.2)计算startIndex
int startIndex=(pageCurrent-1)*pageSize;
//3.3)执行当前数据的查询操作
List records=
sysLogDao.findPageObjects(username,startIndex,pageSize);
//4.对分页信息以及当前页记录进行封装
//4.1)构建PageObject对象
PageObject pageObject=new PageObject<>();
//4.2)封装数据
pageObject.setPageCount((rowCount-1)/pageSize+1);//总页数(通过计算获得)
pageObject.setPageCurrent(pageCurrent);//当前的页码值
pageObject.setPageSize(pageSize);//当前页面显示的记录大小
pageObject.setRecords(records);//当前页记录
pageObject.setRowCount(rowCount);//总行数(通过查询获得)
//5 返回封装结果
return pageObject;
}
}
在该类中 ServiceException是一个自己定义的异常, 通过自定义异常可更好的实现对业务问题的描述,同时可以更好的提高用户体验。
package com.sy.pj.common.ServiceEcxeption;
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 1661395446600784985L;
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
}
}
通过数据层对象,基于业务层参数数据查由时序图可知,syslog实现类向Dao数据层发送一个getRowCount请求,Dao数据层给syslog实现类返回一个rowcount结果
询日志记录总数以及当前页要呈现的用户行为日志信息。
定义数据层接口对象,通过将此对象保证给业务层以提供日志数据访问。
package com.sy.pj.sys.dao;
import com.sy.pj.sys.pojo.SysLog;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @author Robert
*/
@Mapper
public interface SysLogDao {
/**添加getRowCount方法用于按条件统计记录总数。*/
int getRowCount(@Param("username") String username);
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @param startIndex 当前页的起始位置
* @param pageSize 当前页的页面大小
* @return 当前页的日志记录信息
* 数据库中每条日志信息封装到一个SysLog对象中
*/
List findPageObjects(
@Param("username")String username,
@Param("startIndex")Integer startIndex,
@Param("pageSize")Integer pageSize
);
}
基于Dao接口创建映射文件,在此文件中通过相关元素(例如select)描述要执行的数据操作。
在映射文件的设计目录(mapper/sys)中添加SysLogMapper.xml映射文件
from sys_logs
username like concat("%",#{username},"%")
启动服务器,谷歌浏览器输入http://localhost/doIndexUI,点击系统管理的日志查询可以的得到以下页面
日志删除操作相对而言是很简单的操作,只需要根据id就可以删除对应的数据。这边值得注意的就是多条数据的删除操作,传值的方式和sql语句的优化。
首先我们按F12打开控制台,选择Network,然后选择一条数据,点击删除操作按钮,可以得到删除操作的URL,请求方式和参数(页面放不下,往下可以找到)。
当用户执行日志删除操作时,客户端与服务端交互时的基本数据架构,如图所示。
删除业务时序图
客户端提交删除请求,服务端对象的工作时序分析,如图所示。
在日志控制层对象中,添加用于处理日志删除请求的方法。首先在此方法中通过形参接收客户端提交的数据,然后调用业务层对象执行删除操作,最后封装执行结果,并在运行时将响应对象转换为JSON格式的字符串,响应到客户端。
根据前端的URL,传递方式和传值可以写出Controller类
@PostMapping("doDeleteObjects")
public JsonResult doDeleteObjects(Integer... ids){
sysLogService.deleteObjects(ids);
return new JsonResult("删除成功!");
}
这边值得注意的就是,因为我们要实现多条数据的删除,所以我们需要在“integer”后面加“...”,这样可以接受多条id值,并提交到业务层处理。
在日志业务层定义用于执行删除业务的方法,首先通过方法参数接收控制层传递的多个记录的id,并对参数id进行校验。然后基于日志记录id执行删除业务实现。最后返回业务执行结果。
首先在service接口类中添加基于多个id的删除方法
int deleteObjects(Integer... ids);
在ServiceImpl实现类中写具体的方法
@Override
public int deleteObjects(Integer... ids) {
//1 判断参数合法性
if (ids == null || ids.length == 0) {//保证有数据传入,且这个数据不等于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;
}
需要在dao层添加具体的代码对数据库进行操作
数据层基于业务层提交的日志记录id,进行删除操作
int deleteObjects(@Param("ids")Integer… ids);
接下来要写mapper层,也就是动态sql语句对数据库进行删除操作
在SysLogDao接口对应的映射文件中添加用于执行删除业务的delete元素,此元素内部定义具体的SQL实现。
delete from sys_logs
where id in
#{id}
where 1=2
从SQL执行性能角度分析,一般在SQL语句中不建议使用in表达式,可以参考如下代码进行实现(重点是forearch中or运算符的应用):
delete from sys_logs
id=#{id}
where 1=2
说明:这里的choose元素也为一种选择结构,when元素相当于if,otherwise相当于else的语法。
写代码就是不断优化和改进的过程,不能只满足实现其功能上。
日志数据的添加实现是该模块的最核心也是最值得研究学习的一点(对于初学者而言)。
日志的目的是为了记录哪位用户在什么地方登录,进行了什么操作的,形成一个文档,如果有人搞破坏可以找到“犯人”。
记录日志的话,首先我们会想到通过在类里面添加方法存入到数据库中。如果这样做的话,每个实现类都要添加这样的方法做记录,代码显得臃肿不易读。如果以后每添加一个对象或方法都要修改代码,不易于维护。那么有什么方法可以解决这个问题呢?这时候我就要说下AOP了。
AOP是一种设计思想,是面向切面的编程,他是面向对象的编程(oop)的一种补充和完善。在预编译和运行期动态代理方式,实现给程序动态统一添加额外功能的一种技术。通常将面向对象理解为一个静态过程,面向切面的运行期代理方式,可以理解为一个动态过程,可以在对象运行时动态织入一些扩展功能... ...
具体内容可以看看我另一篇博客:https://blog.csdn.net/weixin_49792497/article/details/109374014
数据层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。
在SysLogDao接口中添加用于实现日志信息持久化的方法。
int insertObject(SysLog entity);
基于SysLogDao中方法的定义,编写用于数据持久化的SQL元素。
在SysLogMapper.xml中添加insertObject元素,用于向日志表写入用户行为日志。
insert into sys_logs
(username,operation,method,params,time,ip,createdTime)
values
(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
将日志切面中抓取到的用户行为日志信息,通过业务层对象方法持久化到数据库。
先在SysLogService接口中,添加保存日志信息的方法。
void saveObject(SysLog entity)
然后在在SysLogServiceImpl类中添加具体的保存日志信息的方法。
@Override
public void saveObject(SysLog entity) {
sysLogDao.insertObject(entity);
}
在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业务层对象对日志日志信息做进一步处理,将其存入数据库中。
在springboot工程中要想使用aop时,应在pom文件中添加依赖:
org.springframework.boot
spring-boot-starter-aop
我们首先在common包下新建一个包,包名为:aspect,我们将切面对象放在这里面
在创建一个包叫注解,报名为:annotation,
我们先在aspect包下,新建一个切面SysLogAspect,在切面上加一个注解@component,表示这是一个普通的组件对象:再加一个@aspect表示这还是一个aop中切面对象。在切面对象中定义(1)切入点(Pointcut):织入扩展功能的一些连接点的集合 (2)通知方法(Advice):封装了扩展逻辑的方法。
我们先在切面对象有了,那么我们怎么定义切入点和通知方法呢?
切入点定义有很多方式,我们先用注解的方式来实现,我们在日志的实现类上加一个注解@RequiredLog。使用这个注解描述的方法,就是一个切入点方法。由于这个注解没有,所以需要我们自己定义这个注解。
我们在刚刚新建的annotation的包下新建一个Annotation类,类名为RequiredLog。
这个类是我们自定义的一个注解,现在就考到了我们java基础部分的知识点了。那就是当我们自定义一个注解时一定要注意哪几个方面?
首先,我们要定义这个注解可以描述什么,其次就是这个注解什么时候起作用。
RequiredLog注解代码如下
@Retention(RetentionPolicy.RUNTIME)//定义我们的注解何时有效
@Target(ElementType.METHOD) //定义我们的注解可以修饰谁
public @interface RequiredLog {
}
这时我们在日志的实现类上加上这个注解就表示,在这个方法运行时要加入一个日志功能。这时我们要在切面对象中定义@RequiredLog这个注解是一个切入点
这样才可以真正实现在日志实现类的方法运行时加入日志功能。
所以我们现在要在切面对象中定义一个切入点
package com.sy.pj.common.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SysLogAspect {
//通过Pointcut定义一个切入点,@annotation方式为定义切入点的一种方式,
//在这里表示业务对象中由com.cy.pj.common.annotation.RequiredLog注解描述的方法为一些切入点方法
@Pointcut("@annotation(com.sy.pj.common.annotation.RequiredLog)")
public void doLog(){}//doLog方法仅仅是@Pointcut注解的一个载体,方法体内不需要写任何内容
/**
* @Around 注解描述的方法可以在目标方法执行之前和之后做功能扩展
* @param joinPoint 封装了目标方法信息的一个对象(连接点对象)
* @return 目标方法的执行结果
* @throws Throwable
*/
}
然后在我们的实现类上面加上我们刚刚自定义的注解,为了提高日志的可读性,我们应该知道所切入点的方法具体叫什么,可以再注解后面加上“日志查询”
@RequiredLog("查询日志")
@Override
public PageObject findPageObjects(String username, Integer pageCurrent) {
当然我们也可以在实现类中的删除方法上面加这个注解,然后在后面加上“删除日志”,提高日志记录的可读性。我们先以查询日志做示范。
如果我们要在注解后面加字符串的话,就要稍微修改下我们的注解:
package com.sy.pj.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)//定义我们的注解何时有效
@Target(ElementType.METHOD) //定义我们的注解可以修饰谁
public @interface RequiredLog {
String value() default "";
}
在我们写用作日志记录的切面类之前,我们应该看下日志的实体类,需要哪些值:
由于我们还没有做登录模块,所以username和ip暂时用一个固定值代替
这里面比较难获取到的就是用户操作,也就是我们注解后面的“日志查询”。
如果想获取注解里面的东西,我们首先要获取注解
如果要获取注解的话,前提是要获取到目标方法(加注解的方法)。
一个类里面可能有很多相同名字的方法,所以只获取方法的名字是远远不够的,方法的唯一标识是:方法名和参数列表
所以我们还要获得参数列表。
如果要想获得一个类里面的一个方法,那我们就要先获取到这个类。
到这里就变得简单了。
具体代码如下:
package com.sy.pj.common.aspect;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.sy.pj.common.annotation.RequiredLog;
import com.sy.pj.sys.pojo.SysLog;
import com.sy.pj.sys.service.SysLogService;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
@Aspect
@Component
public class SysLogAspect {
//通过Pointcut定义一个切入点,@annotation方式为定义切入点的一种方式,
//在这里表示业务对象中由com.cy.pj.common.annotation.RequiredLog注解描述的方法为一些切入点方法
@Pointcut("@annotation(com.sy.pj.common.annotation.RequiredLog)")
public void doLog() {
}//doLog方法仅仅是@Pointcut注解的一个载体,方法体内不需要写任何内容
/**
* @param joinPoint 封装了目标方法信息的一个对象(连接点对象)
* @return 目标方法的执行结果
* @throws Throwable
* @Around 注解描述的方法可以在目标方法执行之前和之后做功能扩展
*/
@Around("doLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long t1 = System.currentTimeMillis();
Object result = joinPoint.proceed();//去调用目标方法,其返回值为目标方法返回值
long t2 = System.currentTimeMillis();
System.out.println("time:" + (t2 - t1));
//将正常的用户行为日志写入到数据库
saveSysLog(joinPoint, (t2 - t1));
return result;
}
@Autowired
private SysLogService sysLogService;
private void saveSysLog(ProceedingJoinPoint joinPoint,long time) throws NoSuchMethodException, JsonProcessingException {
//1 获取用户行为日志
//获取目标对象类型
Class> targetClass = joinPoint.getTarget().getClass();
//获取目标方法的签名信息
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
//获取目标方法(方法的唯一标识是:方法名+参数列表)
Method targetMethod =
targetClass.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
//获得RequiredLog
RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
//获取操作名
String operation = requiredLog.value();
//封装日志信息
SysLog entity = new SysLog();
entity.setUsername("user");//将来这个位置是登录的用户名,我们先随便写一个代替
entity.setIp("192.168.126.129");//先写个假的IP代替
entity.setOperation(operation);//操作名
entity.setMethod(targetClass.getName() + "." + targetMethod.getName());//调用方法时传递实践参数
entity.setParams(Arrays.toString(joinPoint.getArgs()));
entity.setTime(time);//操作的时间
entity.setCreatedTime(new Date());//操作的日期
//3保存用户行为
sysLogService.saveObject(entity);
}
}
日志模块到这里基本就结束了。这里面的用户名和IP地址,等到做完用户登录之后再做更改。
我们打开服务器,进入http://localhost/doIndexUI进行检验。
我们也可以在实现类中删除方法上加
@RequiredLog("删除日志")
功能基本实现(日志模块的作用就是记录用户真实的行为,所以不用加修改操作)。
日志模块到这里就结束了。我们先做个总结。