最近做一个关于系统日志统计相关的功能,主要统计的是controller层的操作,比较简单,没有涉及统计service层和dao层的。
系统架构:SpringBoot、Shiro、Mybatis、thymelaf
问题分析:系统日志统计 其实已经很成熟了,网上随便一搜都能找到较为完善的解决思路及代码示例,我再写一遍,仅仅是为了对新学习的技术起到一个巩固、复盘的一个过程。
首先:统计系统访问日志,必然少不了日志表,还应该设计其主要统计的字段有哪些:
/** 主键ID */
private Integer logId;
/** 日志描述 */
private String description;
/** 日志类型常量 */
private String operationType;
/** 请求类型 */
private String httpMethod;
/** ip */
private String ip;
/** 记录的类方法 */
private String classMethod;
/** 参数列表 */
private String args;
/** 主要参数 */
private Integer primaryId;
/** 创建时间 */
private Date addTime;
/**
* 表名后缀
*/
private String tableSuffix;
之所以定义表名后缀,是因为系统中要统计各个模块下的操作记录,为了便于区分,那么每个模块的操作记录存放为一张表,都是以主表来衍生子表。
采用SpringAOP的方式,首先需要建立枚举类,用于自定义注解的
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemLog {
/**
* 日志表后缀,可以根据业务表自定义业务日志存储在哪张表中,前缀固定为z_system_log_
* 默认的Constant.BASE是存储在 z_system_log表中
* @return
*/
Constant suffix() default Constant.BASE;
/**
* 日志描述/说明
* @return
*/
String desc() default "";
/**
* 当前记录信息的主键ID名称,如:z_system_log映射的实体类主键名称为:logId,则填logId
* @return
*/
String primaryName() default "";
/**
* 操作类型,数据记录主表时需要传入
* @return
*/
SystemLogType operationType() d
然后定义系统日志的切面,将需要统计的controller定义切点并织入切点
@Aspect
@Component
@Slf4j
public class SystemLogAspect {
@Autowired
private IZSystemLogService systemLogService;
/**
* 定义切点
*/
@Pointcut("execution(public * com.ruoyi.project.system.*.controller..*.*(..))")
public void systemLog() {}
/**
* 前置通知:在连接点之前执行的通知
* @param joinPoint
* @throws Throwable
*/
@Before("systemLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 织入切点
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
SystemLog systemLog1 = method.getAnnotation(SystemLog.class);
//不需要记录日志
if(systemLog1 == null) {
return;
}
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
ZSystemLog systemLog = ZSystemLog.builder()
//日志描述
.description("'" + systemLog1.desc() + "'")
//日志类型常量
.operationType("'" + systemLog1.operationType().toString() + "'")
//请求类型
.httpMethod("'" + request.getMethod() + "'")
//ip
.ip("'" + request.getRemoteAddr() + "'")
//记录的类方法
.classMethod("'" + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName() + "'")
//参数列表
.args("'" + Arrays.toString(joinPoint.getArgs()) + "'")
//主要参数
.primaryId(getPrimaryId(systemLog1.primaryName(), request, signature, joinPoint.getArgs()))
.build();
systemLogService.saveSystemLog(systemLog, systemLog1.suffix().toString());
}
@AfterReturning(returning = "ret", pointcut = "systemLog()")
public void doAfterReturning(Object ret) throws Throwable {
}
/**
* 获取需要存储的主键信息
* @param primaryName
* @param request
* @return
*/
private int getPrimaryId(String primaryName, HttpServletRequest request, MethodSignature signature, Object[] args) {
int primaryId = -1;
if(StringUtils.isEmpty(primaryName)) {
return primaryId;
}
String pVal = request.getParameter(primaryName);
if(!StringUtils.isEmpty(pVal)) {
return Integer.valueOf(pVal);
}
Object aVal = request.getAttribute(primaryName);
if(!StringUtils.isEmpty(aVal)) {
return Integer.valueOf(aVal.toString());
}
String[] parameterNames = signature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
if(parameterNames[i].toLowerCase().equals(primaryName.toLowerCase())) {
primaryId = Integer.valueOf(args[i].toString());
break;
}
}
return primaryId;
}
}
然后定义要统计模块的系统常量,
/**
* @Auther: zh
* @Date: 2020-07-04
* @Description: 日志类型常量,主要记录模块下的日志
*/
public enum Constant {
// 日志用到的常量定义,用于判断日志类型
/**
* 基础日志类型,统一记录到z_system_log
*/
BASE,
/**
* 日志主表名称
*/
Z_SYSTEM_LOG,
/**
* 【XXX模块】
*/
XVUNIT_INFO,
}
=============
public enum SystemLogType {
/**
* 基础操作
*/
OPERATION,
/**
* 【XXX模块】
*/
XVUNIT_VISIT,
}
统计系统日志,无非就是两种情况:
- 有表,则直接将参数插入基础系统日志表中
- 无表,则先创建系统日志表,以基础表作为前缀,以模块自定义常量最为表后缀,创建表,然后再插入数据。
- 刚才创建的切面中有一个构建方法,该方法在所有Controller中使用了自定义注解@SystemLog的方法执行前都会进入该切面,执行方法
@Override
public void saveSystemLog(ZSystemLog systemLog, String tableSuffix) {
//1、判断表后缀是否和基础表相同
tableSuffix = tableSuffix.toUpperCase().equals(Constant.BASE.toString()) ? null : tableSuffix;
//2、如果为空,则以基础表为表名,否则以基础表加上表后缀组成新表
String tableName = StringUtils.isEmpty(tableSuffix) ? Constant.Z_SYSTEM_LOG.toString() : Constant.Z_SYSTEM_LOG.toString() + "_" + tableSuffix;
//3、判断表名是否存在,不存在创建表
if(!existTable(tableName)) {
createNewSystemLogTable(tableName);
}
zSystemLogMapper.saveSystemLog(systemLog, tableName);
}
@Override
public boolean existTable(String tableName) {
int tableNum = zSystemLogMapper.existTable(tableName);
return tableNum > 0 ? true : false;
}
@Override
public void createNewSystemLogTable(String tableName) {
zSystemLogMapper.createNewSystemLogTable(tableName);
}
- 建表语句以及查询表名语句
<select id="existTable" parameterType="String" resultType="Integer">
select COUNT(1) from INFORMATION_SCHEMA.TABLES
where TABLE_SCHEMA = '你的数据库名' and TABLE_NAME = #{tableName};
</select>
<update id="createNewSystemLogTable" parameterType="String">
CREATE TABLE ${tableName} (
`log_id` int(10) NOT NULL AUTO_INCREMENT,
`description` varchar(64) DEFAULT NULL,
`operation_type` VARCHAR(64) DEFAULT null,
`http_method` varchar(8) DEFAULT NULL,
`ip` varchar(32) DEFAULT NULL,
`class_method` varchar(255) DEFAULT NULL,
`args` varchar(128) DEFAULT NULL,
`primary_id` int(10) DEFAULT NULL,
`add_time` datetime DEFAULT NULL,
PRIMARY KEY (`log_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
</update>
<insert id="saveSystemLog" statementType="STATEMENT">
INSERT INTO ${tableName} (
description,
operation_type,
http_method,
ip,
class_method,
args,
primary_id,
add_time
)
VALUES (
${systemLog.description},
${systemLog.operationType},
${systemLog.httpMethod},
${systemLog.ip},
${systemLog.classMethod},
${systemLog.args},
${systemLog.primaryId},
NOW()
)
</insert>
- 在需要统计的方法上加入自定义注解@SystemLog即可
**
*suffix表后缀
*desc:日志描述
*operationType:操作类型
/
@GetMapping()
@SystemLog(suffix = Constant.XVUNIT_INFO,desc = "XXX模块日志统计",operationType = SystemLogType.XVUNIT_VISIT)
public String list() {
return prefix + "/list";
}
以上就是采用SpringAOP实现系统日志统计方法。
如果要讲各个模块统计的日志,数据可视化的呈现出来,可以参考echarts,可以数据实时渲染到前端页面,且提供了很多可用的主题。