Spring AOP + 注解實(shí)現(xiàn)統(tǒng)一注解功能
1. 概述
在一般系統(tǒng)中,當(dāng)我們做了一些重要的操作時(shí),如登陸系統(tǒng),添加用戶,刪除用戶等操作時(shí),我們需要將這些行為持久化。本文我們通過(guò)Spring AOP和Java的自定義注解來(lái)實(shí)現(xiàn)日志的插入。此方案對(duì)原有業(yè)務(wù)入侵較低,實(shí)現(xiàn)較靈活
2. 日志的相關(guān)類(lèi)定義
我們將日志抽象為以下兩個(gè)類(lèi):功能模塊和操作類(lèi)型
使用枚舉類(lèi)定義功能模塊類(lèi)型ModuleType,如學(xué)生、用戶模塊
public enum ModuleType {
DEFAULT("1"), // 默認(rèn)值
STUDENT("2"),// 學(xué)生模塊
TEACHER("3"); // 用戶模塊
private ModuleType(String index){
this.module = index;
}
private String module;
public String getModule(){
return this.module;
}
}
使用枚舉類(lèi)定義操作的類(lèi)型:EventType。如登陸、添加、刪除、更新、刪除等
public enum EventType {
DEFAULT("1", "default"), ADD("2", "add"), UPDATE("3", "update"), DELETE_SINGLE("4", "delete-single"),
LOGIN("10","login"),LOGIN_OUT("11","login_out");
private EventType(String index, String name){
this.name = name;
this.event = index;
}
private String event;
private String name;
public String getEvent(){
return this.event;
}
public String getName() {
return name;
}
}
3. 定義日志相關(guān)的注解
3.1. @LogEnable
這里我們定義日志的開(kāi)關(guān)量,類(lèi)上只有這個(gè)值為true,這個(gè)類(lèi)中日志功能才開(kāi)啟
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogEnable {
/**
* 如果為true,則類(lèi)下面的LogEvent啟作用,否則忽略
* @return
*/
boolean logEnable() default true;
}
3.2. @LogEvent
這里定義日志的詳細(xì)內(nèi)容。如果此注解注解在類(lèi)上,則這個(gè)參數(shù)做為類(lèi)全部方法的默認(rèn)值。如果注解在方法上,則只對(duì)這個(gè)方法啟作用
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD, ElementType.TYPE})
public @interface LogEvent {
ModuleType module() default ModuleType.DEFAULT; // 日志所屬的模塊
EventType event() default EventType.DEFAULT; // 日志事件類(lèi)型
String desc() default ""; // 描述信息
}
3.3. @LogKey
此注解如果注解在方法上,則整個(gè)方法的參數(shù)以json的格式保存到日志中。如果此注解同時(shí)注解在方法和類(lèi)上,則方法上的注解會(huì)覆蓋類(lèi)上的值。
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogKey {
String keyName() default ""; // key的名稱(chēng)
boolean isUserId() default false; // 此字段是否是本次操作的userId,這里略
boolean isLog() default true; // 是否加入到日志中
}
4. 定義日志處理類(lèi)
4.1. LogAdmModel
定義保存日志信息的類(lèi)
public class LogAdmModel {
private Long id;
private String userId; // 操作用戶
private String userName;
private String admModel; // 模塊
private String admEvent; // 操作
private Date createDate; // 操作內(nèi)容
private String admOptContent; // 操作內(nèi)容
private String desc; // 備注
set/get略
}
4.2. ILogManager
定義日志處理的接口類(lèi)ILogManager
我們可以將日志存入數(shù)據(jù)庫(kù),也可以將日志發(fā)送到開(kāi)中間件,如果redis, mq等等。每一種日志處理類(lèi)都是此接口的實(shí)現(xiàn)類(lèi)
public interface ILogManager {
/**
* 日志處理模塊
* @param paramLogAdmBean
*/
void dealLog(LogAdmModel paramLogAdmBean);
}
4.3. DBLogManager
ILogManager實(shí)現(xiàn)類(lèi),將日志入庫(kù)。這里只模擬入庫(kù)
@Service
public class DBLogManager implements ILogManager {
@Override
public void dealLog(LogAdmModel paramLogAdmBean) {
System.out.println("將日志存入數(shù)據(jù)庫(kù),日志內(nèi)容如下: " + JSON.toJSONString(paramLogAdmBean));
}
}
5. AOP的配置
5.1. LogAspect定義AOP類(lèi)
使用@Aspect注解此類(lèi)
使用@Pointcut定義要攔截的包及類(lèi)方法
我們使用@Around定義方法
@Component
@Aspect
public class LogAspect {
@Autowired
private LogInfoGeneration logInfoGeneration;
@Autowired
private ILogManager logManager;
@Pointcut("execution(* com.hry.spring.mvc.aop.log.service..*.*(..))")
public void managerLogPoint() {
}
@Around("managerLogPoint()")
public Object aroundManagerLogPoint(ProceedingJoinPoint jp) throws Throwable {
….
}
}
aroundManagerLogPoint:主方法的主要業(yè)務(wù)流程
1. 檢查攔截方法的類(lèi)是否被@LogEnable注解,如果是,則走日志邏輯,否則執(zhí)行正常的邏輯
2. 檢查攔截方法是否被@LogEvent,如果是,則走日志邏輯,否則執(zhí)行正常的邏輯
3. 根據(jù)獲取方法上獲取@LogEvent 中值,生成日志的部分參數(shù)。其中定義在類(lèi)上@LogEvent 的值做為默認(rèn)值
4. 調(diào)用logInfoGeneration的processingManagerLogMessage填充日志中其它的參數(shù),做個(gè)方法我們后面再講
5. 執(zhí)行正常的業(yè)務(wù)調(diào)用
6. 如果執(zhí)行成功,則logManager執(zhí)行日志的處理(我們這里只記錄執(zhí)行成功的日志,你也可以定義記錄失敗的日志)
@Around("managerLogPoint()")
public Object aroundManagerLogPoint(ProceedingJoinPoint jp) throws Throwable {
Class target = jp.getTarget().getClass();
// 獲取LogEnable
LogEnable logEnable = (LogEnable) target.getAnnotation(LogEnable.class);
if(logEnable == null || !logEnable.logEnable()){
return jp.proceed();
}
// 獲取類(lèi)上的LogEvent做為默認(rèn)值
LogEvent logEventClass = (LogEvent) target.getAnnotation(LogEvent.class);
Method method = getInvokedMethod(jp);
if(method == null){
return jp.proceed();
}
// 獲取方法上的LogEvent
LogEvent logEventMethod = method.getAnnotation(LogEvent.class);
if(logEventMethod == null){
return jp.proceed();
}
String optEvent = logEventMethod.event().getEvent();
String optModel = logEventMethod.module().getModule();
String desc = logEventMethod.desc();
if(logEventClass != null){
// 如果方法上的值為默認(rèn)值,則使用全局的值進(jìn)行替換
optEvent = optEvent.equals(EventType.DEFAULT) ? logEventClass.event().getEvent() : optEvent;
optModel = optModel.equals(ModuleType.DEFAULT) ? logEventClass.module().getModule() : optModel;
}
LogAdmModel logBean = new LogAdmModel();
logBean.setAdmModel(optModel);
logBean.setAdmEvent(optEvent);
logBean.setDesc(desc);
logBean.setCreateDate(new Date());
logInfoGeneration.processingManagerLogMessage(jp,
logBean, method);
Object returnObj = jp.proceed();
if(optEvent.equals(EventType.LOGIN)){
//TODO 如果是登錄,還需要根據(jù)返回值進(jìn)行判斷是不是成功了,如果成功了,則執(zhí)行添加日志。這里判斷比較簡(jiǎn)單
if(returnObj != null) {
this.logManager.dealLog(logBean);
}
}else {
this.logManager.dealLog(logBean);
}
return returnObj;
}
/**
* 獲取請(qǐng)求方法
*
* @param jp
* @return
*/
public Method getInvokedMethod(JoinPoint jp) {
// 調(diào)用方法的參數(shù)
List classList = new ArrayList();
for (Object obj : jp.getArgs()) {
classList.add(obj.getClass());
}
Class[] argsCls = (Class[]) classList.toArray(new Class[0]);
// 被調(diào)用方法名稱(chēng)
String methodName = jp.getSignature().getName();
Method method = null;
try {
method = jp.getTarget().getClass().getMethod(methodName, argsCls);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return method;
}
}
6. 將以上的方案在實(shí)際中應(yīng)用的方案
這里我們模擬學(xué)生操作的業(yè)務(wù),并使用上文注解應(yīng)用到上面并攔截日志
6.1. IStudentService
業(yè)務(wù)接口類(lèi),執(zhí)行一般的CRUD
public interface IStudentService {
void deleteById(String id, String a);
int save(StudentModel studentModel);
void update(StudentModel studentModel);
void queryById(String id);
}
6.2. StudentServiceImpl:
@LogEnable : 啟動(dòng)日志攔截
類(lèi)上@LogEvent定義所有的模塊
方法上@LogEven定義日志的其它的信息
@Service
@LogEnable // 啟動(dòng)日志攔截
@LogEvent(module = ModuleType.STUDENT)
public class StudentServiceImpl implements IStudentService {
@Override
@LogEvent(event = EventType.DELETE_SINGLE, desc = "刪除記錄") // 添加日志標(biāo)識(shí)
public void deleteById(@LogKey(keyName = "id") String id, String a) {
System.out.printf(this.getClass() + "deleteById id = " + id);
}
@Override
@LogEvent(event = EventType.ADD, desc = "保存記錄") // 添加日志標(biāo)識(shí)
public int save(StudentModel studentModel) {
System.out.printf(this.getClass() + "save save = " + JSON.toJSONString(studentModel));
return 1;
}
@Override
@LogEvent(event = EventType.UPDATE, desc = "更新記錄") // 添加日志標(biāo)識(shí)
public void update(StudentModel studentModel) {
System.out.printf(this.getClass() + "save update = " + JSON.toJSONString(studentModel));
}
// 沒(méi)有日志標(biāo)識(shí)
@Override
public void queryById(String id) {
System.out.printf(this.getClass() + "queryById id = " + id);
}
}
執(zhí)行測(cè)試類(lèi),打印如下信息,說(shuō)明我們?nèi)罩咀⒔馀渲脝⒆饔昧耍?/p>
將日志存入數(shù)據(jù)庫(kù),日志內(nèi)容如下:
{"admEvent":"4","admModel":"1","admOptContent":"{\"id\":\"1\"}","createDate":1525779738111,"desc":"刪除記錄"}
7. 代碼
以上的詳細(xì)的代碼見(jiàn)下面
github代碼,請(qǐng)盡量使用tag v0.21,不要使用master,因?yàn)槲也荒鼙WCmaster代碼一直不變
相關(guān)文章
Mybatis?plus多租戶方案的實(shí)戰(zhàn)踩坑記錄
MybaitsPlus多租戶處理器是一個(gè)對(duì)于多租戶問(wèn)題的解決方案,下面這篇文章主要給大家介紹了關(guān)于Mybatis?plus多租戶方案踩坑的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-12-12
Java對(duì)象轉(zhuǎn)json的方法過(guò)程解析
這篇文章主要介紹了Java對(duì)象轉(zhuǎn)json的方法過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
Logback MDCAdapter日志跟蹤及自定義效果源碼解讀
這篇文章主要為大家介紹了Logback MDCAdapter日志跟蹤及自定義效果源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
java如何用遞歸生成樹(shù)形結(jié)構(gòu)
作者分享了自己在使用腳本之家資源進(jìn)行編程時(shí)的經(jīng)驗(yàn),包括準(zhǔn)備實(shí)體對(duì)象、測(cè)試數(shù)據(jù)、構(gòu)造樹(shù)形結(jié)構(gòu)遞歸函數(shù)、測(cè)試以及輸出結(jié)果等步驟,作者希望這些經(jīng)驗(yàn)?zāi)軐?duì)大家有所幫助,并鼓勵(lì)大家支持腳本之家2025-03-03
使用JPA中@Query 注解實(shí)現(xiàn)update 操作方法(必看)
下面小編就為大家?guī)?lái)一篇使用JPA中@Query 注解實(shí)現(xiàn)update 操作方法(必看)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
Java編寫(xiě)通用的導(dǎo)出任何對(duì)象列表數(shù)據(jù)到excel的工具類(lèi)
在工作中經(jīng)常會(huì)遇到列表數(shù)據(jù)的導(dǎo)出,每次需要的時(shí)候都要去開(kāi)發(fā)一次,且數(shù)據(jù)不斷在變化,所以本文將利用Java編寫(xiě)一個(gè)工具類(lèi)可以導(dǎo)出任何對(duì)象列表數(shù)據(jù)到excel,希望對(duì)大家有所幫助2024-12-12
Java線程池ThreadPoolExecutor的使用及其原理詳細(xì)解讀
這篇文章主要介紹了Java線程池ThreadPoolExecutor的使用及其原理詳細(xì)解讀,線程池是一種多線程處理形式,處理過(guò)程中將任務(wù)添加到隊(duì)列,然后在創(chuàng)建線程后自動(dòng)啟動(dòng)這些任務(wù),線程池線程都是后臺(tái)線程,需要的朋友可以參考下2023-12-12
java設(shè)計(jì)模式之橋接模式(Bridge)
這篇文章主要為大家詳細(xì)介紹了java設(shè)計(jì)模式之橋接模式Bridge,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
mybatis于xml方式和注解方式實(shí)現(xiàn)多表查詢的操作方法
在數(shù)據(jù)庫(kù)中,單表的操作是最簡(jiǎn)單的,但是在實(shí)際業(yè)務(wù)中最少也有十幾張表,并且表與表之間常常相互間聯(lián)系,本文給大家介紹mybatis于xml方式和注解方式實(shí)現(xiàn)多表查詢的操作方法,感興趣的朋友一起看看吧2023-12-12
springboot?vue前后端接口測(cè)試樹(shù)結(jié)點(diǎn)添加功能
這篇文章主要為大家介紹了springboot?vue前后端接口測(cè)試樹(shù)結(jié)點(diǎn)添加功能,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05

