前言
这边文章是通过使用注解+AOP的形式,在业务代码前后做日志增强的功能。这样在不更改业务代码的情况下,利用注解作为 Pointcut 快速实现日志记录的功能。
准备工作
Maven版本
这里是基于springboot-2.3.5版本,需要引入 SpringAop 库。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
数据库结构
我这里采用的将接口访问日志,存储到 Mysql 数据库中,其它数据库原理一致,具体根据自己项目结构进行调整。
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL COMMENT 'ID',
`log_module` varchar(50) NOT NULL COMMENT '日志模块,sys,blog',
`log_title` varchar(50) NOT NULL COMMENT '日志标题',
`log_value` varchar(50) NOT NULL COMMENT '日志内容',
`log_type` tinyint(2) NOT NULL COMMENT '日志类型1:登录日志;2:操作日志;3:定时任务;4:异常日志;',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`operate_type` tinyint(2) NOT NULL COMMENT '操作类型',
`ip_address` varchar(100) DEFAULT NULL COMMENT 'IP地址',
`method` varchar(500) DEFAULT NULL COMMENT '请求方法',
`request_url` varchar(50) DEFAULT NULL COMMENT '请求url路径',
`request_type` varchar(50) DEFAULT NULL COMMENT '请求类型',
`request_params` text COMMENT '请求参数',
`cost_time` int(11) DEFAULT NULL COMMENT '耗费时间',
`err_msg` varchar(1000) DEFAULT NULL COMMENT '异常信息',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除,1是,0否',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志';
常量值
我将系统中使用的常量值单独分离了出来。
public interface CommonConstant {
/**************************SysLog 日志常量**************************/
//查询
public static final int SYS_LOG_OPERATE_QUERY = 1;
//添加
public static final int SYS_LOG_OPERATE_SAVE = 2;
//更新
public static final int SYS_LOG_OPERATE_UPDATE = 3;
//删除
public static final int SYS_LOG_OPERATE_REMOVE = 4;
//导入
public static final int SYS_LOG_OPERATE_IMPORT = 5;
//导出
public static final int SYS_LOG_OPERATE_EXPORT = 6;
//登录
public static final int SYS_LOG_TYPE_LOGIN = 1;
//操作
public static final int SYS_LOG_TYPE_OPERATE = 2;
//定时
public static final int SYS_LOG_TYPE_TIME = 3;
//异常
public static final int SYS_LOG_TYPE_ERROR = 4;
}
核心
这里的核心基于两个事物:
- @interface 注解类
- @Aspect 切面编程
@SysLog
这里根据我对系统的设计划分
- module:所属模块,可以划分大模块比如(Sys,系统模块),(Blog,博客模块)。
- title:子模块标题,可以简单划分大模块下的子模块。
- logType:日志类型,分为1:登录日志;2:操作日志;3:定时任务;4:异常日志。
- opreateType:操作类型,1查询,2添加,3修改,4删除,5导入,6导出。
- 日志内容:日志内容。
/**
* 系统日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
/**
* 所属模块
*
* @return ModuleType
*/
ModuleType module() default ModuleType.SYS;
/**
* 标题
* 例如:菜单管理
*
* @return
*/
String title() default "";
/**
* 日志类型
*
* @return 1:登录日志;2:操作日志;3:定时任务;4:异常日志;
*/
int logType() default CommonConstant.SYS_LOG_TYPE_OPERATE;
/**
* 操作类型
*
* @return (1查询,2添加,3修改,4删除,5导入,6导出)
*/
int operateType() default 0;
/**
* 日志内容
*
* @return
*/
String value() default "";
}
SysLogAspect
这里有几个需要注意的地方
- 需要在类上添加@Component 和 @Aspect 注解,前者是为了将SysLogAspect 作为一个Bean,注入到Spring容器中;后者是为了开启切面功能
- @Pointcut 注入点,注入点说明该切面在什么情况下生效,这里使用 "@annotation(top.zsmile.annotation.SysLog)" 表示只要是有加@SysLog注解的地方就能注入切面功能。
- @Around,括号内使用的就是对应的切点sysLogPointcut(),这时around方法就会执行。
- ProceedingJoinPoint joinPoint,指的是对应的执行点,当我们调用proceed()时,会调用对应的业务代码。
@Slf4j
@Component
@Aspect
public class SysLogAspect {
@Autowired
private SysLogService sysLogService;
@Autowired
private CommonAuthApi commonAuthApi;
@Pointcut("@annotation(top.zsmile.annotation.SysLog)")
public void sysLogPointcut() {
}
@Around("sysLogPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = System.currentTimeMillis();
try {
//执行方法
Object result = joinPoint.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
saveSysLog(joinPoint, time, result);
return result;
} catch (Throwable throwable) {
// 异常日志
long time = System.currentTimeMillis() - beginTime;
saveErrorLog(joinPoint, time, throwable.getMessage());
throw throwable;
}
}
/**
* 保存错误日志
*
* @param joinPoint
* @param costTime
* @param errMsg
*/
private void saveErrorLog(ProceedingJoinPoint joinPoint, long costTime, String errMsg) {
SysLogEntity sysLogEntity = commonLog(joinPoint, costTime, CommonConstant.SYS_LOG_TYPE_ERROR);
sysLogEntity.setErrMsg(errMsg);
sysLogService.save(sysLogEntity);
}
/**
* 保存正常日志
*
* @param joinPoint
* @param costTime
* @param result
*/
private void saveSysLog(ProceedingJoinPoint joinPoint, long costTime, Object result) {
SysLogEntity sysLogEntity = commonLog(joinPoint, costTime);
sysLogService.save(sysLogEntity);
}
private SysLogEntity commonLog(ProceedingJoinPoint joinPoint, long costTime) {
return commonLog(joinPoint, costTime, 0);
}
private SysLogEntity commonLog(ProceedingJoinPoint joinPoint, long costTime, int logType) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLogEntity sysLogEntity = new SysLogEntity();
SysLog sysLogAnno = method.getAnnotation(SysLog.class);
if (sysLogAnno != null) {
String title = sysLogAnno.title();
ModuleType module = sysLogAnno.module();
int operateType = sysLogAnno.operateType();
String value = sysLogAnno.value();
sysLogEntity.setLogTitle(title);
sysLogEntity.setLogValue(value);
sysLogEntity.setLogType(logType > 0 ? logType : sysLogAnno.logType());
sysLogEntity.setOperateType(getOperateType(method.getName(), operateType));
sysLogEntity.setLogModule(module.name());
}
//获取request
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
sysLogEntity.setIpAddress(IPUtils.getIpAddrByRequest(request));
//请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLogEntity.setMethod(className + "." + methodName);
// 请求类型
sysLogEntity.setRequestType(request.getMethod());
// 耗时
sysLogEntity.setCostTime(costTime);
// 请求参数
sysLogEntity.setRequestParams(getParams(joinPoint, request));
// 操作用户Id
Long userId = commonAuthApi.queryUserId();
sysLogEntity.setUserId(userId);
return sysLogEntity;
}
private int getOperateType(String methodName, int operateType) {
if (operateType > 0) {
return operateType;
}
if (methodName.startsWith("query") || methodName.startsWith("list")) {
return CommonConstant.SYS_LOG_OPERATE_QUERY;
} else if (methodName.startsWith("save")) {
return CommonConstant.SYS_LOG_OPERATE_SAVE;
} else if (methodName.startsWith("update")) {
return CommonConstant.SYS_LOG_OPERATE_UPDATE;
} else if (methodName.startsWith("remove")) {
return CommonConstant.SYS_LOG_OPERATE_REMOVE;
} else if (methodName.startsWith("import")) {
return CommonConstant.SYS_LOG_OPERATE_IMPORT;
} else if (methodName.startsWith("export")) {
return CommonConstant.SYS_LOG_OPERATE_EXPORT;
}
return CommonConstant.SYS_LOG_OPERATE_QUERY;
}
/**
* 获取Request参数
*
* @param httpServletRequest
* @return
*/
private String getParams(ProceedingJoinPoint joinPoint, HttpServletRequest httpServletRequest) {
String method = httpServletRequest.getMethod();
if (method.equalsIgnoreCase("POST") || method.equalsIgnoreCase("PUT") || method.equalsIgnoreCase("DELET")) {
PropertyFilter profilter = new PropertyFilter() {
@Override
public boolean apply(Object o, String name, Object value) {
if (value != null && value.toString().length() > 200) {
return false;
}
return true;
}
};
return JSON.toJSONString(joinPoint.getArgs(), profilter);
} else {
return JSON.toJSONString(httpServletRequest.getParameterMap());
}
}
}
使用方法
@SysLog(title = "角色管理", operateType = CommonConstant.SYS_LOG_OPERATE_QUERY, value = "分页查询")
\