Spring boot實(shí)現(xiàn)AOP記錄操作日志

在實(shí)際的項(xiàng)目中,特別是管理系統(tǒng)中,對(duì)于那些重要的操作我們通常都會(huì)記錄操作日志。比如對(duì)數(shù)據(jù)庫(kù)的CRUD操作,我們都會(huì)對(duì)每一次重要的操作進(jìn)行記錄,通常的做法是向數(shù)據(jù)庫(kù)指定的日志表中插入一條記錄。這里就產(chǎn)生了一個(gè)問(wèn)題,難道要我們每次在CRUD的時(shí)候都手動(dòng)的插入日志記錄嗎?這肯定是不合適的,這樣的操作無(wú)疑是加大了開(kāi)發(fā)量,而且不易維護(hù),所以實(shí)際項(xiàng)目中總是利用AOP(Aspect Oriented Programming)即面向切面編程這一技術(shù)來(lái)記錄系統(tǒng)中的操作日志。

日志分類(lèi)

這里我把日志按照面向的對(duì)象不同分為兩類(lèi):

面向用戶的日志:用戶是指使用系統(tǒng)的人,這一類(lèi)日志通常記錄在數(shù)據(jù)庫(kù)里邊,并且通常是記錄對(duì)數(shù)據(jù)庫(kù)的一些CRUD操作。

面向開(kāi)發(fā)者的日志:查看這一類(lèi)日志的一般都是開(kāi)發(fā)人員,這類(lèi)日志通常保存在文件或者在控制臺(tái)打印(開(kāi)發(fā)的時(shí)候在控制臺(tái),項(xiàng)目上線之后之后保存在文件中),這一類(lèi)日志主要用于開(kāi)發(fā)者開(kāi)發(fā)時(shí)期和后期維護(hù)時(shí)期定位錯(cuò)誤。

面向不同對(duì)象的日志,我們采用不同的策略去記錄。很容易看出,對(duì)于面向用戶的日志具有很強(qiáng)的靈活性,需要開(kāi)發(fā)者控制用戶的哪些操作需要向數(shù)據(jù)庫(kù)記錄日志,所以這一類(lèi)保存在數(shù)據(jù)庫(kù)的日志我們?cè)谑褂肁OP記錄時(shí)用自定義注解的方式去匹配;而面向開(kāi)發(fā)者的日志我們則使用表達(dá)式去匹配就可以了(這里有可能敘述的有點(diǎn)模糊,看了下面去案例將會(huì)很清晰),下面分別介紹兩種日志的實(shí)現(xiàn)。

實(shí)現(xiàn)AOP記錄面向用戶的日志

接下來(lái)分步驟介紹Spring boot中怎樣實(shí)現(xiàn)通過(guò)AOP記錄操作日志。

添加依賴

在pom.xml文件中添加如下依賴:

<!-- aop依賴 -->org.springframework.bootspring-boot-starter-aop

修改配置文件

在項(xiàng)目的application.properties文件中添加下面一句配置:

spring.aop.auto=true

這里特別說(shuō)明下,這句話不加其實(shí)也可以,因?yàn)槟J(rèn)就是true,只要我們?cè)趐om.xml中添加了依賴就可以了,這里提出來(lái)是讓大家知道有這個(gè)有這個(gè)配置。

自定義注解

上邊介紹過(guò)了了,因?yàn)檫@類(lèi)日志比較靈活,所以我們需要自定義一個(gè)注解,使用的時(shí)候在需要記錄日志的方法上添加這個(gè)注解就可以了,首先在啟動(dòng)類(lèi)的同級(jí)包下邊新建一個(gè)config包,在這個(gè)報(bào)下邊新建new一個(gè)名為L(zhǎng)og的Annotation文件,文件內(nèi)容如下:

packagecom.web.springbootaoplog.config;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/***@authorPromise*@createTime2018年12月18日 下午9:26:25*@description定義一個(gè)方法級(jí)別的@log注解*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceLog {Stringvalue()default"";}

準(zhǔn)備數(shù)據(jù)庫(kù)日志表以及實(shí)體類(lèi),sql接口,xml文件

既然是向數(shù)據(jù)庫(kù)中插入記錄,那么前提是需要?jiǎng)?chuàng)建一張記錄日志的表,下面給出我的表sql,由于是寫(xiě)樣例,我這里這張表設(shè)計(jì)的很簡(jiǎn)單,大家可以自行設(shè)計(jì)。

CREATE TABLE `sys_log` (? `id` int(11) NOT NULL AUTO_INCREMENT COMMENT'主鍵',? `user_id` int(11) NOT NULL COMMENT'操作員id',? `user_action` varchar(255) NOT NULL COMMENT'用戶操作',? `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT'創(chuàng)建時(shí)間',? PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COMMENT='日志記錄表';

通過(guò)上篇博客介紹的MBG生成相應(yīng)的實(shí)體類(lèi),sql接口文件,以及xml文件,這里不再概述,

當(dāng)然還需要?jiǎng)?chuàng)建service接口文件以及接口實(shí)現(xiàn)類(lèi),這里直接給出代碼:

ISysLogServcie.java

packagecom.web.springbootaoplog.service;importcom.web.springbootaoplog.entity.SysLog;/***@authorPromise*@createTime2018年12月18日 下午9:29:48*@description日志接口*/publicinterfaceISysLogService{/** * 插入日志 *@paramentity *@return*/intinsertLog(SysLog entity);}

SysLogServiceImpl.java

packagecom.web.springbootaoplog.service.impl;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importcom.web.springbootaoplog.config.Log;importcom.web.springbootaoplog.dao.SysLogMapper;importcom.web.springbootaoplog.entity.SysLog;importcom.web.springbootaoplog.service.ISysLogService;/***@authorPromise*@createTime2018年12月18日 下午9:30:57*@description*/@Service("sysLogService")publicclassSysLogServiceImplimplementsISysLogService{@AutowiredprivateSysLogMapper sysLogMapper;@OverridepublicintinsertLog(SysLog entity){// TODO Auto-generated method stubreturnsysLogMapper.insert(entity);}}

AOP的切面和切點(diǎn)

準(zhǔn)備上邊的相關(guān)文件后,下面來(lái)介紹重點(diǎn)--創(chuàng)建AOP切面實(shí)現(xiàn)類(lèi),同樣我們這里將該類(lèi)放在config包下,命名為L(zhǎng)ogAsPect.java,內(nèi)容如下:

packagecom.web.springbootaoplog.config;importjava.lang.reflect.Method;importjava.util.Arrays;importjava.util.Date;importorg.aspectj.lang.JoinPoint;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Before;importorg.aspectj.lang.annotation.Pointcut;importorg.aspectj.lang.reflect.MethodSignature;importorg.hibernate.validator.internal.util.logging.LoggerFactory;importorg.slf4j.Logger;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.core.LocalVariableTableParameterNameDiscoverer;importorg.springframework.stereotype.Component;importcom.web.springbootaoplog.entity.SysLog;importcom.web.springbootaoplog.service.ISysLogService;/***@authorPromise*@createTime2018年12月18日 下午9:33:28*@description切面日志配置*/@Aspect@ComponentpublicclassLogAsPect{privatefinalstaticLogger log = org.slf4j.LoggerFactory.getLogger(LogAsPect.class);@AutowiredprivateISysLogService sysLogService;//表示匹配帶有自定義注解的方法@Pointcut("@annotation(com.web.springbootaoplog.config.Log)")publicvoidpointcut(){}@Around("pointcut()")publicObjectaround(ProceedingJoinPoint point){Object result =null;longbeginTime = System.currentTimeMillis();try{? ? log.info("我在目標(biāo)方法之前執(zhí)行!");result = point.proceed();longendTime = System.currentTimeMillis();insertLog(point,endTime-beginTime);}catch(Throwable e) {// TODO Auto-generated catch block}returnresult;}privatevoidinsertLog(ProceedingJoinPoint point ,longtime){MethodSignature signature = (MethodSignature)point.getSignature();Method method = signature.getMethod();SysLog sys_log =newSysLog();Log userAction = method.getAnnotation(Log.class);if(userAction !=null) {// 注解上的描述sys_log.setUserAction(userAction.value());}// 請(qǐng)求的類(lèi)名String className = point.getTarget().getClass().getName();// 請(qǐng)求的方法名String methodName = signature.getName();// 請(qǐng)求的方法參數(shù)值String args = Arrays.toString(point.getArgs());//從session中獲取當(dāng)前登陸人id// Long useride = (Long)SecurityUtils.getSubject().getSession().getAttribute("userid");Long userid =1L;//應(yīng)該從session中獲取當(dāng)前登錄人的id,這里簡(jiǎn)單模擬下sys_log.setUserId(userid);sys_log.setCreateTime(newjava.sql.Timestamp(newDate().getTime()));log.info("當(dāng)前登陸人:{},類(lèi)名:{},方法名:{},參數(shù):{},執(zhí)行時(shí)間:{}",userid, className, methodName, args, time);sysLogService.insertLog(sys_log);}}

這里簡(jiǎn)單介紹下關(guān)于AOP的幾個(gè)重要注解:

@Aspect:這個(gè)注解表示將當(dāng)前類(lèi)視為一個(gè)切面類(lèi)

@Component:表示將當(dāng)前類(lèi)交由Spring管理。

@Pointcut:切點(diǎn)表達(dá)式,定義我們的匹配規(guī)則,上邊我們使用@Pointcut("@annotation(com.web.springbootaoplog.config.Log)")表示匹配帶有我們自定義注解的方法。

@Around:環(huán)繞通知,可以在目標(biāo)方法執(zhí)行前后執(zhí)行一些操作,以及目標(biāo)方法拋出異常時(shí)執(zhí)行的操作。

下面看一段關(guān)鍵的代碼:

log.info("我在目標(biāo)方法之前執(zhí)行!");result = point.proceed();longendTime = System.currentTimeMillis();insertLog(point,endTime-beginTime);

其中result = point.proceed();這句話表示執(zhí)行目標(biāo)方法,可以看出我們?cè)谶@段代碼執(zhí)行之前打印了一句日志,并在執(zhí)行之后調(diào)用了insertLog()插入日志的方法,并且在方法中我們可以拿到目標(biāo)方法所在的類(lèi)名,方法名,參數(shù)等重要的信息。

測(cè)試控制器

在controller包下新建一個(gè)HomeCOntroller.java(名字大家隨意),內(nèi)容如下:

packagecom.web.springbootaoplog.controller;importjava.util.HashMap;importjava.util.Map;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.bind.annotation.ResponseBody;importcom.web.springbootaoplog.config.Log;importcom.web.springbootaoplog.entity.SysLog;importcom.web.springbootaoplog.service.ISysLogService;/***@authorPromise*@createTime2019年1月2日 下午10:35:30*@description測(cè)試controller*/@ControllerpublicclassHomeController{privatefinalstaticLogger log = org.slf4j.LoggerFactory.getLogger(HomeController.class);@AutowiredprivateISysLogService logService;@RequestMapping("/aop")@ResponseBody@Log("測(cè)試aoplog")publicObjectaop(String name, String nick){Map map =newHashMap<>();log.info("我被執(zhí)行了!");map.put("res","ok");returnmap;}}

定義一個(gè)測(cè)試方法,帶有兩個(gè)參數(shù),并且為該方法添加了我們自定義的@Log注解,啟動(dòng)項(xiàng)目,瀏覽器訪問(wèn)localhost:8080/aop?name=xfr&nick=eran,這時(shí)候查看eclipse控制臺(tái)的部分輸出信息如下:

2019-01-24 22:02:17.682? INFO 3832 --- [nio-8080-exec-1] c.web.springbootaoplog.config.LogAsPect? : 我在目標(biāo)方法之前執(zhí)行!2019-01-24 22:02:17.688? INFO 3832 --- [nio-8080-exec-1] c.w.s.controller.HomeController? ? ? ? ? : 我被執(zhí)行了!2019-01-24 22:02:17.689? INFO 3832 --- [nio-8080-exec-1] c.web.springbootaoplog.config.LogAsPect? : 當(dāng)前登陸人:1,類(lèi)名:com.web.springbootaoplog.controller.HomeController,方法名:aop,參數(shù):[xfr, eran],執(zhí)行時(shí)間:6

可以看到我們成功在目標(biāo)方法執(zhí)行前后插入了一些邏輯代碼,現(xiàn)在再看數(shù)據(jù)庫(kù)里邊的數(shù)據(jù)

成功記錄了一條數(shù)據(jù)。

實(shí)現(xiàn)AOP記錄面向開(kāi)發(fā)者的日志

首先這里我列舉一個(gè)使用該方式的應(yīng)用場(chǎng)景,在項(xiàng)目中出現(xiàn)了bug,我們想要知道前臺(tái)的請(qǐng)求是否進(jìn)入了我們控制器中,以及參數(shù)的獲取情況,下面開(kāi)始介紹實(shí)現(xiàn)步驟。

其實(shí)原理跟上邊是一樣的,只是切點(diǎn)的匹配規(guī)則變了而已,而且不用將日志記錄到數(shù)據(jù)庫(kù),打印出來(lái)即可。

首先在LogAsPect.java中定義一個(gè)新的切點(diǎn)表達(dá)式,如下:

@Pointcut("execution(public * com.web.springbootaoplog.controller..*.*(..))")publicvoidpointcutController(){}

@Pointcut("execution(public * com.web.springbootaoplog.controller..*.*(..))")表示匹配com.web.springbootaoplog.controller包及其子包下的所有公有方法。

再添加匹配到方法時(shí)我們要做的操作:

@Before("pointcutController()")publicvoidaround2(JoinPoint point){//獲取目標(biāo)方法String methodNam = point.getSignature().getDeclaringTypeName() +"."+ point.getSignature().getName();//獲取方法參數(shù)String params = Arrays.toString(point.getArgs());log.info("get in {} params :{}",methodNam,params);}

@Before:表示目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容。

再在控制器中添加一個(gè)測(cè)試方法:

@RequestMapping("/testaop3")@ResponseBodypublicObjecttestAop3(String name, String nick){Map map =newHashMap<>();map.put("res","ok");returnmap;}

可以看到這個(gè)方法我們并沒(méi)有加上@Log注解,重啟項(xiàng)目,瀏覽器訪問(wèn)localhost:8080/testaop3?name=xfr&nick=eran,這時(shí)候查看eclipse控制臺(tái)的部分輸出信息如下:

2019-01-24 23:19:49.108? INFO 884 --- [nio-8080-exec-1] c.web.springbootaoplog.config.LogAsPect? : getincom.web.springbootaoplog.controller.HomeController.testAop3 params :[xfr, eran]

打印出了關(guān)鍵日志,這樣我們就能知道是不是進(jìn)入了該方法,參數(shù)獲取是否正確等關(guān)鍵信息。

這里有的朋友或許會(huì)有疑問(wèn)這樣會(huì)不會(huì)與添加了@Log的方法重復(fù)了呢,的確會(huì),所以在項(xiàng)目中我通常都將@Log注解用在了Service層的方法上,這樣也更加合理。

結(jié)語(yǔ)

好了,關(guān)于Aop記錄日志的內(nèi)容就介紹這么多了在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群。交流學(xué)習(xí)群號(hào):938837867 暗號(hào):555 里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容