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 @Slf4jpublic class LogAspect { /** * 封裝工具類Logutil 后,日志切面類代碼修改 調用工具類實現環繞方法,標紅代碼,首先引入????????AsyncOperLogService*/@Autowiredprivate 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 {@Autowiredprivate SysOperLogMapper sysOperLogMapper;@Overridepublic 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='操作日志記錄';