SpringBoot系列(14): Spring AOP裝逼指南之實現(xiàn)操作日志記錄


作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。

摘要:AOP ,也稱為“面向切面編程”,其大名早已如雷貫耳,是 Spring 框架的核心特性之一,相信各位小伙伴也早已聽聞過,其最普遍的用法是“記錄應(yīng)用系統(tǒng)業(yè)務(wù)模塊的操作日志”,今天我們就來分享介紹一下如何利用Spring AOP實現(xiàn)業(yè)務(wù)模塊操作日志的記錄。

內(nèi)容:Spring  AOP,是“面向切面編程”的簡稱,可以起到“解耦業(yè)務(wù)模塊”的作用,深層次的作用可以利用網(wǎng)上一位博主所說的一句話進(jìn)行概括,即:

“AOP 可以實現(xiàn)在不修改源代碼的情況下給程序動態(tài)統(tǒng)一添加功能,而不需要破壞某個操作業(yè)務(wù)模塊代碼的完整性”

對于Spring AOP,在這里有必要再啰嗦一番其核心要素,“面向切面編程”,一聽就知道其核心要素是“切面”,而“切面”并非只是單純存在的玩意,它由兩大部分組成,“切點(diǎn)PointCut”+“通知Notice”,下面,讓我用“通俗易懂”的語句表達(dá)出來吧:

“切點(diǎn)”:在什么地方觸發(fā)我們自定義的操作(定義的是“何處”)

“通知”:在什么時候執(zhí)行我們自定義的操作,同時定義好“自定義的操作”具體是什么!

“切面”:切點(diǎn) + 通知;即在什么時候,在哪個地方觸發(fā)執(zhí)行什么樣的操作!

好了,若需要具體的、深層次的關(guān)于專業(yè)術(shù)語的介紹、剖析,可以上官網(wǎng)或者博客找找相應(yīng)的介紹吧!下面我們就進(jìn)入實際的代碼實戰(zhàn)過程!

(1)首先,我們當(dāng)然需要建立一個“記錄日志的數(shù)據(jù)庫表”,sys_log,其DDL定義如下所示:

CREATE TABLE `sys_log` (
`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 '請求參數(shù)',
`time` bigint(20) NOT NULL COMMENT '執(zhí)行時長(毫秒)',
`ip` varchar(64) DEFAULT NULL COMMENT 'IP地址',
`create_date` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系統(tǒng)日志';

然后,采用Mybatis逆向工程生成相應(yīng)的Entity、Mapper、Mapper.xml,之后便是等待著被調(diào)用!

(2)然后,我們定義一個注解LogAnnotation,這個注解將用于應(yīng)用系統(tǒng)中“所有需要觸發(fā)記錄日志”的操作模塊對應(yīng)的方法之上,在后面大家會發(fā)現(xiàn),正是這種方式,才實現(xiàn)了所謂的“服務(wù)模塊解耦”、“在不修改源代碼的情況下給程序動態(tài)統(tǒng)一添加功能”的作用。其完整源代碼如下所示:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
//value將用于存儲 “具體的操作模塊的操作描述”
String value() default "";
}

(3)緊接著,我們需要定義一個重量級的東西,即“切面”,這個切面是基于AspectJ注解實現(xiàn)的類LogAspect,其完整源代碼如下所示:  

/**
* 日志切面 = 切點(diǎn)+通知
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
* @Date: 2019/11/7 15:18
**/
@Aspect
@Component
public class LogAspect {

private static final Logger log= LoggerFactory.getLogger(LogAspect.class);

@Autowired
private SysLogMapper sysLogMapper;

@Autowired
private ObjectMapper objectMapper;

//定義切點(diǎn):發(fā)生的時機(jī) - 即一旦加了這個注解,將觸發(fā)某些事情
@Pointcut("@annotation(com.debug.springboot.server.annotation.LogAnnotation)")
public void logPointCut(){}

//定義通知:環(huán)繞通知 - 執(zhí)行核心任務(wù)之前 + 執(zhí)行完核心任務(wù)之后 該做的事情
@Around("logPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
long start=System.currentTimeMillis();
//TODO:執(zhí)行核心任務(wù)
Object object=joinPoint.proceed();
long time=System.currentTimeMillis()-start;

saveLog(joinPoint,time);
return object;
}

//TODO:保存操作日志
private void saveLog(ProceedingJoinPoint point,Long time) throws Exception{
log.info("開始觸發(fā)-保存操作日志");

MethodSignature signature= (MethodSignature) point.getSignature();
Method method=signature.getMethod();

SysLog entity=new SysLog();

//TODO:獲取請求操作的描述信息
LogAnnotation annotation=method.getAnnotation(LogAnnotation.class);
if (annotation!=null){
entity.setOperation(annotation.value());
}

//TODO:獲取操作方法名
String className=point.getTarget().getClass().getName();
String methodName=signature.getName();
entity.setMethod(className + "." + methodName + "()");

//TODO:獲取請求參數(shù)
Object[] args=point.getArgs();
entity.setParams(objectMapper.writeValueAsString(args[0]));

//TODO:獲取剩余參數(shù)
entity.setTime(time);
entity.setCreateDate(DateTime.now().toDate());
entity.setUsername("debug");
entity.setIp("localhost");
sysLogMapper.insertSelective(entity);
}
}

 (3)最后,我們在MultipartSourceController 中寫個請求方法,并在該請求方法上加入日志注解,用于記錄該請求方法在執(zhí)行前、后、執(zhí)行中的軌跡和相應(yīng)的參數(shù)。完整的源代碼如下所示:  

@LogAnnotation("多數(shù)據(jù)源時-跨事務(wù)-日志操作")
@RequestMapping(value = "update",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse update(@RequestBody LogUpdateDto dto) throws Exception{
BaseResponse response=new BaseResponse(StatusCode.Success);

User user=new User();
user.setName(dto.getName());
user.setCode(dto.getCode());
userMapper.insertSelective(user);

return response;
}

而LogUpdateDto只是一個簡單的實體類:  

/**
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
* @Date: 2019/11/7 16:14
**/
@Data
public class LogUpdateDto implements Serializable{

@NotBlank
private String name;

@NotBlank
private String code;
}

(4)最后,我們將整個系統(tǒng)跑起來,然后在Postman發(fā)起相應(yīng)的請求,請求完成后,可以看到數(shù)據(jù)庫表sys_log中記錄了相應(yīng)的請求軌跡以及相應(yīng)的請求參數(shù)等等,如下圖所示:


好了,本篇文章我們就介紹到這里了,其他相關(guān)的技術(shù),感興趣的小伙伴可以關(guān)注底部Debug的技術(shù)公眾號,或者加Debug的微信,拉你進(jìn)“微信版”的真正技術(shù)交流群!一起學(xué)習(xí)、共同成長!

補(bǔ)充

1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來進(jìn)行查看學(xué)習(xí):

https://gitee.com/steadyjack/SpringBootTechnology

2、目前Debug已將本文所涉及的內(nèi)容整理錄制成視頻教程,感興趣的小伙伴可以前往觀看學(xué)習(xí):https://www.fightjava.com/web/index/course/detail/5

3、關(guān)注一下Debug的技術(shù)微信公眾號,最新的技術(shù)文章、課程以及技術(shù)專欄將會第一時間在公眾號發(fā)布哦!