AOP记录日志
AOP记录日志的主要优点包括:
1、低侵入性:AOP记录日志不需要修改原有的业务逻辑代码,只需要新增一个切面即可。
2、统一管理:通过AOP记录日志可以将各个模块中需要记录日志的部分进行统一管理,降低了代码重复度,提高了代码可维护性和可扩展性。
3、提升效率:通过引入AOP记录日志,可以避免手动编写日志记录代码,减少了开发人员的工作量,提升了开发效率。
4、安全性:通过AOP记录日志,可以收集系统的操作日志,帮助管理员及时发现问题并进行调整,从而提高系统的安全性。
AOP记录日志的整体思想:
1、基于自定义注解来确定切入点【优势:可以通过自定义注解携带一些变化的参数,比如模块名称】
2、基于环绕通知来完成日志记录
搭建之前,要明白AOP主要核心术语
下面:
切面类环境搭建
1.在common模块下创建一个独立的记录日志的模块【common-log】
在该模块下加入如下的依赖
2.自定义Log注解,如下所示:
代码如下:
import com.atguigu.spzx.common.log.enums.OperatorType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* // 自定义操作日志记录注解
*/
@SuppressWarnings("all")
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
// 模块名称
public String title() ;
// 操作人类别
public OperatorType operatorType() default OperatorType.MANAGE;
// 业务类型(0其它 1新增 2修改 3删除)
public int businessType() ;
//是否保存请求的参数
public boolean isSaveRequestData() default true;
// 是否保存响应的参数
public boolean isSaveResponseData() default true;
}
//操作人类别
OperatorType定义
public enum OperatorType { // 操作人类别
OTHER, // 其他
MANAGE, // 后台用户
MOBILE // 手机端用户
}
3.定义一个切面类,并且在该切面类中提供一个环绕通知方法
LogAspect
/** * 这个方法作为环绕通知,环绕通知方法中两个参数,ProceedingJoinPoint和Log * 1.ProceedingJoinPoint 表示能调用我们业务方法并且可以得到相关的参数等信息 * 2.Log 方法上加了public @interface Log这个注解, * 加上@Around(value = "@annotation(sysLog)"),方法执行时就会执行环绕通知,就可以完成机制 * 实现当方法上加上注解之后,就会实行环绕通知 */
@Aspect
@Component
@Slf4j// 环绕通知切面类定义
public class LogAspect {
@Around(value = "@annotation(sysLog)")
public Object doAroundAdvice(ProceedingJoinPoint joinPoint , Log sysLog) {
Object proceed = null;
try {
// 执行业务方法
proceed = joinPoint.proceed();
} catch (Throwable e) {
// 代码执行进入到catch中,业务方法执行产生异常
throw new RuntimeException(e);
}
// 返回执行结果
return proceed ;
}
}
4.想让LogAspect这个切面类在其他的业务服务中进行使用,那么就需要该切面类纳入到Spring容器中。Spring Boot默认会扫描和启动类所在包相同包中的bean以及子包中的bean。而LogAspect切面类不满足扫描条件,因此无法直接在业务服务中进行使用。那么此时可以通过自定义注解进行实现
代码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(value = LogAspect.class)
// 通过Import注解导入日志切面类到Spring容器中
public @interface EnableLogAspect {
}
5.在启动类pom.xml 引入依赖
<dependency> <groupId>org.liuliu</groupId> <artifactId>common-log</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
6.在ManagerApplication服务的启动类上添加@EnableLogAspect注解
7.创建Log工具类,代码如下
/** * 自定义注解搞完,切面环绕方法类方法创建完成,在manager引入common包依赖,在启动类添加注解@EnableLogAspect * 然后封装一个logutil工具类备用(切面环绕方法类执行时调用工具类) */
import com.alibaba.fastjson.JSON; import com.atguigu.spzx.common.log.annotation.Log; import com.atguigu.spzx.model.entity.system.SysOperLog; import com.atguigu.spzx.utils.AuthContextUtil; import jakarta.servlet.http.HttpServletRequest; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.http.HttpMethod; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.lang.reflect.Method; import java.util.Arrays; public class LogUtil { //操作执行之后调用 public static void afterHandlLog(Log sysLog, Object proceed, SysOperLog sysOperLog, int status , String errorMsg) { if(sysLog.isSaveResponseData()) { sysOperLog.setJsonResult(JSON.toJSONString(proceed)); } sysOperLog.setStatus(status); sysOperLog.setErrorMsg(errorMsg); } //操作执行之前调用 public static void beforeHandleLog(Log sysLog, ProceedingJoinPoint joinPoint, SysOperLog sysOperLog) { // 设置操作模块名称 sysOperLog.setTitle(sysLog.title()); sysOperLog.setOperatorType(sysLog.operatorType().name()); // 获取目标方法信息 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature() ; Method method = methodSignature.getMethod(); sysOperLog.setMethod(method.getDeclaringClass().getName()); // 获取请求相关参数 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); sysOperLog.setRequestMethod(request.getMethod()); sysOperLog.setOperUrl(request.getRequestURI()); sysOperLog.setOperIp(request.getRemoteAddr()); // 设置请求参数 if(sysLog.isSaveRequestData()) { String requestMethod = sysOperLog.getRequestMethod(); if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) { String params = Arrays.toString(joinPoint.getArgs()); sysOperLog.setOperParam(params); } } sysOperLog.setOperName(AuthContextUtil.get().getUserName()); } }
8.在common-log模块中定义保存日志数据的service接口,然后在具体的业务服务中给出实现。分析图如下:
1)common-log 模块下创建service包
上代码:
import com.atguigu.spzx.model.entity.system.SysOperLog; /** * // 异步操作日志记录服务接口 */ public interface AsyncOperLogService { }
2)启动类项目中的serviceImpl包中实现日志记录服务接口,创建Mapper和mapper.xml
(创建过程自动脑补,继续实现项目),都创建完成,接下来去保存日志操作:
2.1)修改切面类,调用封装工具
import com.atguigu.spzx.common.log.annotation.Log; import com.atguigu.spzx.common.log.service.AsyncOperLogService; import com.atguigu.spzx.common.log.utils.LogUtil; import com.atguigu.spzx.model.entity.system.SysOperLog; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 定义一个切面类,并且在该切面类中提供一个环绕通知方法 * // 环绕通知切面类定义 */ @Aspect @Component @Slf4j public class LogAspect { /** * 封装工具类Logutil 后,日志切面类代码修改 调用工具类实现环绕方法,标红代码,首先引入 AsyncOperLogService */ @Autowired private AsyncOperLogService asyncOperLogService ; @Around(value = "@annotation(sysLog)") public Object doAroundAdvice(ProceedingJoinPoint joinPoint, Log sysLog){ // 构建前置参数 SysOperLog sysOperLog = new SysOperLog() ; LogUtil.beforeHandleLog(sysLog , joinPoint , sysOperLog) ; Object proceed=null; try { // 执行业务方法 proceed = joinPoint.proceed(); // 构建响应结果参数 LogUtil.afterHandlLog(sysLog , proceed, sysOperLog,0,null); } catch (Throwable e) { e.printStackTrace(); LogUtil.afterHandlLog(sysLog , proceed, sysOperLog,1, e.getMessage()); // 代码执行进入到catch中,业务方法执行产生异常 throw new RuntimeException(); } // 保存日志数据 asyncOperLogService.saveSysOperLog(sysOperLog); // 返回执行结果 return proceed; } }
2.2)执行上述切面类保存日志方法:
// 保存日志数据 asyncOperLogService.saveSysOperLog(sysOperLog);
//service接口
import com.atguigu.spzx.model.entity.system.SysOperLog;
/**
* // 异步操作日志记录服务接口
*/
public interface AsyncOperLogService {
保存日志数据
public abstract void saveSysOperLog(SysOperLog sysOperLog) ;
}
//service实现类
import com.atguigu.spzx.common.log.service.AsyncOperLogService; import com.atguigu.spzx.manager.mapper.SysOperLogMapper; import com.atguigu.spzx.model.entity.system.SysOperLog; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AsyncOperLogServiceImpl implements AsyncOperLogService { @Autowired private SysOperLogMapper sysOperLogMapper; @Override public void saveSysOperLog(SysOperLog sysOperLog) { // 异步执行保存日志操作 sysOperLogMapper.insert(sysOperLog); } }
//mapper接口
import com.atguigu.spzx.model.entity.system.SysOperLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysOperLogMapper {
//保存日志操作
void insert(SysOperLog sysOperLog);
}
//mapper.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.atguigu.spzx.manager.mapper.SysOperLogMapper">
<sql id="columns">
id,title,method,request_method,operator_type,oper_name,oper_url,oper_ip,oper_param,json_result
,status,error_msg,create_time,update_time,is_deleted
</sql>
<insert id="insert">
insert into sys_oper_log (
id,
title,
method,
request_method,
operator_type,
oper_name,
oper_url,
oper_ip,
oper_param,
json_result,
status,
error_msg
) values (
#{id},
#{title},
#{method},
#{requestMethod},
#{operatorType},
#{operName},
#{operUrl},
#{operIp},
#{operParam},
#{jsonResult},
#{status},
#{errorMsg}
)
</insert>
</mapper>
2.3)一切准备就绪,在需要添加操作日志的接口方法上添加@Log注解进行测试。(添加红色表示框内容),启动器启动
//查询列表
@Log(title = "品牌管理",businessType = 0,operatorType = OperatorType.OTHER)
@GetMapping("/{page}/{limit}")
public Result<PageInfo<Brand>> list(@PathVariable(value = "page") Integer page, @PathVariable(value = "limit") Integer limit){
PageInfo<Brand> brandList=brandService.findByPage(page, limit);
return Result.build(brandList, ResultCodeEnum.SUCCESS);
}
结果:
创作不易,谢谢大家
补充:附上日志数据表结构,方便理解
CREATE TABLE `sys_oper_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志主键',
`title` varchar(50) DEFAULT '' COMMENT '模块标题',
`business_type` varchar(20) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',
`method` varchar(100) DEFAULT '' COMMENT '方法名称',
`request_method` varchar(10) DEFAULT '' COMMENT '请求方式',
`operator_type` varchar(20) DEFAULT '0' COMMENT '操作类别(0其它 1后台用户 2手机端用户)',
`oper_name` varchar(50) DEFAULT '' COMMENT '操作人员',
`dept_name` varchar(50) DEFAULT '' COMMENT '部门名称',
`oper_url` varchar(255) DEFAULT '' COMMENT '请求URL',
`oper_ip` varchar(128) DEFAULT '' COMMENT '主机地址',
`oper_param` varchar(2000) DEFAULT '' COMMENT '请求参数',
`json_result` varchar(2000) DEFAULT '' COMMENT '返回参数',
`status` int DEFAULT '0' COMMENT '操作状态(0正常 1异常)',
`error_msg` varchar(2000) DEFAULT '' COMMENT '错误消息',
`oper_time` datetime DEFAULT NULL COMMENT '操作时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:不可用 1:可用)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=67 DEFAULT CHARSET=utf8mb3 COMMENT='操作日志记录';