前言
在實(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)中的操作日志。
文章首發(fā)于個(gè)人博客:【http://www.xiongfrblog.cn】
日志分類(lèi)
這里我把日志按照面向的對(duì)象不同分為兩類(lèi):
- 面向用戶(hù)的日志:用戶(hù)是指使用系統(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ì)于面向用戶(hù)的日志具有很強(qiáng)的靈活性,需要開(kāi)發(fā)者控制用戶(hù)的哪些操作需要向數(shù)據(jù)庫(kù)記錄日志,所以這一類(lèi)保存在數(shù)據(jù)庫(kù)的日志我們?cè)谑褂?AOP記錄時(shí)用自定義注解的方式去匹配;而面向開(kāi)發(fā)者的日志我們則使用表達(dá)式去匹配就可以了(這里有可能敘述的有點(diǎn)模糊,看了下面去案例將會(huì)很清晰),下面分別介紹兩種日志的實(shí)現(xiàn)。
實(shí)現(xiàn)AOP記錄面向用戶(hù)的日志
接下來(lái)分步驟介紹Spring boot中怎樣實(shí)現(xiàn)通過(guò)AOP記錄操作日志。
添加依賴(lài)
在pom.xml文件中添加如下依賴(lài):
<!-- aop依賴(lài) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
修改配置文件
在項(xiàng)目的application.properties文件中添加下面一句配置:
spring.aop.auto=true
這里特別說(shuō)明下,這句話不加其實(shí)也可以,因?yàn)槟J(rèn)就是
true,只要我們?cè)?code>pom.xml中添加了依賴(lài)就可以了,這里提出來(lái)是讓大家知道有這個(gè)有這個(gè)配置。
自定義注解
上邊介紹過(guò)了了,因?yàn)檫@類(lèi)日志比較靈活,所以我們需要自定義一個(gè)注解,使用的時(shí)候在需要記錄日志的方法上添加這個(gè)注解就可以了,首先在啟動(dòng)類(lèi)的同級(jí)包下邊新建一個(gè)config包,在這個(gè)報(bào)下邊新建new一個(gè)名為Log的Annotation文件,文件內(nèi)容如下:
package com.web.springbootaoplog.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Promise
* @createTime 2018年12月18日 下午9:26:25
* @description 定義一個(gè)方法級(jí)別的@log注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
這里用到的是Java元注解的相關(guān)知識(shí),不清楚相關(guān)概念的朋友可以去這篇博客get一下【傳送門(mén)】。
準(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 '用戶(hù)操作',
`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文件,這里不再概述,不清楚的朋友請(qǐng)移步【傳送門(mén)】
當(dāng)然還需要?jiǎng)?chuàng)建service接口文件以及接口實(shí)現(xiàn)類(lèi),這里直接給出代碼:
ISysLogServcie.java
package com.web.springbootaoplog.service;
import com.web.springbootaoplog.entity.SysLog;
/**
* @author Promise
* @createTime 2018年12月18日 下午9:29:48
* @description 日志接口
*/
public interface ISysLogService {
/**
* 插入日志
* @param entity
* @return
*/
int insertLog(SysLog entity);
}
SysLogServiceImpl.java
package com.web.springbootaoplog.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.web.springbootaoplog.config.Log;
import com.web.springbootaoplog.dao.SysLogMapper;
import com.web.springbootaoplog.entity.SysLog;
import com.web.springbootaoplog.service.ISysLogService;
/**
* @author Promise
* @createTime 2018年12月18日 下午9:30:57
* @description
*/
@Service("sysLogService")
public class SysLogServiceImpl implements ISysLogService{
@Autowired
private SysLogMapper sysLogMapper;
@Override
public int insertLog(SysLog entity) {
// TODO Auto-generated method stub
return sysLogMapper.insert(entity);
}
}
AOP的切面和切點(diǎn)
準(zhǔn)備上邊的相關(guān)文件后,下面來(lái)介紹重點(diǎn)--創(chuàng)建AOP切面實(shí)現(xiàn)類(lèi),同樣我們這里將該類(lèi)放在config包下,命名為LogAsPect.java,內(nèi)容如下:
package com.web.springbootaoplog.config;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import com.web.springbootaoplog.entity.SysLog;
import com.web.springbootaoplog.service.ISysLogService;
/**
* @author Promise
* @createTime 2018年12月18日 下午9:33:28
* @description 切面日志配置
*/
@Aspect
@Component
public class LogAsPect {
private final static Logger log = org.slf4j.LoggerFactory.getLogger(LogAsPect.class);
@Autowired
private ISysLogService sysLogService;
//表示匹配帶有自定義注解的方法
@Pointcut("@annotation(com.web.springbootaoplog.config.Log)")
public void pointcut() {}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) {
Object result =null;
long beginTime = System.currentTimeMillis();
try {
log.info("我在目標(biāo)方法之前執(zhí)行!");
result = point.proceed();
long endTime = System.currentTimeMillis();
insertLog(point,endTime-beginTime);
} catch (Throwable e) {
// TODO Auto-generated catch block
}
return result;
}
private void insertLog(ProceedingJoinPoint point ,long time) {
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
SysLog sys_log = new SysLog();
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(new java.sql.Timestamp(new Date().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í)行的操作。
我們用到的注解就這幾個(gè),當(dāng)然還有其他的注解,這里我就不一一介紹了,想要深入了解AOP相關(guān)知識(shí)的朋友可以移步官方文檔【傳送門(mén)】
下面看一段關(guān)鍵的代碼:
log.info("我在目標(biāo)方法之前執(zhí)行!");
result = point.proceed();
long endTime = 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)容如下:
package com.web.springbootaoplog.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.web.springbootaoplog.config.Log;
import com.web.springbootaoplog.entity.SysLog;
import com.web.springbootaoplog.service.ISysLogService;
/**
* @author Promise
* @createTime 2019年1月2日 下午10:35:30
* @description 測(cè)試controller
*/
@Controller
public class HomeController {
private final static Logger log = org.slf4j.LoggerFactory.getLogger(HomeController.class);
@Autowired
private ISysLogService logService;
@RequestMapping("/aop")
@ResponseBody
@Log("測(cè)試aoplog")
public Object aop(String name, String nick) {
Map<String, Object> map =new HashMap<>();
log.info("我被執(zhí)行了!");
map.put("res", "ok");
return map;
}
}
定義一個(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..*.*(..))")
public void pointcutController() {}
@Pointcut("execution(public * com.web.springbootaoplog.controller..*.*(..))")表示匹配com.web.springbootaoplog.controller包及其子包下的所有公有方法。
關(guān)于這個(gè)表達(dá)式詳細(xì)的使用方法可以移步這里,【傳送門(mén)】
再添加匹配到方法時(shí)我們要做的操作:
@Before("pointcutController()")
public void around2(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")
@ResponseBody
public Object testAop3(String name, String nick) {
Map<String, Object> map = new HashMap<>();
map.put("res", "ok");
return map;
}
可以看到這個(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 : get in com.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)容就介紹這么多了,下一篇博客再見(jiàn)。bye~