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)類定義
我們將日志抽象為以下兩個(gè)類:功能模塊和操作類型
使用枚舉類定義功能模塊類型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; } }
使用枚舉類定義操作的類型: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)量,類上只有這個(gè)值為true,這個(gè)類中日志功能才開(kāi)啟
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface LogEnable { /** * 如果為true,則類下面的LogEvent啟作用,否則忽略 * @return */ boolean logEnable() default true; }
3.2. @LogEvent
這里定義日志的詳細(xì)內(nèi)容。如果此注解注解在類上,則這個(gè)參數(shù)做為類全部方法的默認(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; // 日志事件類型 String desc() default ""; // 描述信息 }
3.3. @LogKey
此注解如果注解在方法上,則整個(gè)方法的參數(shù)以json的格式保存到日志中。如果此注解同時(shí)注解在方法和類上,則方法上的注解會(huì)覆蓋類上的值。
@Target({ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogKey { String keyName() default ""; // key的名稱 boolean isUserId() default false; // 此字段是否是本次操作的userId,這里略 boolean isLog() default true; // 是否加入到日志中 }
4. 定義日志處理類
4.1. LogAdmModel
定義保存日志信息的類
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
定義日志處理的接口類ILogManager
我們可以將日志存入數(shù)據(jù)庫(kù),也可以將日志發(fā)送到開(kāi)中間件,如果redis, mq等等。每一種日志處理類都是此接口的實(shí)現(xiàn)類
public interface ILogManager { /** * 日志處理模塊 * @param paramLogAdmBean */ void dealLog(LogAdmModel paramLogAdmBean); }
4.3. DBLogManager
ILogManager實(shí)現(xiàn)類,將日志入庫(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類
使用@Aspect注解此類
使用@Pointcut定義要攔截的包及類方法
我們使用@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. 檢查攔截方法的類是否被@LogEnable注解,如果是,則走日志邏輯,否則執(zhí)行正常的邏輯
2. 檢查攔截方法是否被@LogEvent,如果是,則走日志邏輯,否則執(zhí)行正常的邏輯
3. 根據(jù)獲取方法上獲取@LogEvent 中值,生成日志的部分參數(shù)。其中定義在類上@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(); } // 獲取類上的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)用方法名稱 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ù)接口類,執(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)日志攔截 類上@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è)試類,打印如下信息,說(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批量更新對(duì)象數(shù)據(jù)的兩種實(shí)現(xiàn)方式
這篇文章主要介紹了Mybatis批量更新對(duì)象數(shù)據(jù)的兩種實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08java數(shù)據(jù)結(jié)構(gòu)循環(huán)隊(duì)列的空滿判斷及長(zhǎng)度計(jì)算
這篇文章主要為大家介紹了java數(shù)據(jù)結(jié)構(gòu)循環(huán)隊(duì)列的空滿判斷及長(zhǎng)度計(jì)算,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06springboot集成redis并使用redis生成全局唯一索引ID
本文主要介紹了springboot集成redis并使用redis生成全局唯一索引ID,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Java跳出當(dāng)前的多重嵌套循環(huán)的五種方法
在Java編程中,跳出多重嵌套循環(huán)可以使用break語(yǔ)句、標(biāo)號(hào)與break組合、return語(yǔ)句、標(biāo)志變量和異常處理五種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-10-10SpringBoot常用讀取配置文件的3種方法小結(jié)
本文主要介紹了SpringBoot常用讀取配置文件的3種方法小結(jié),主要包括@Value讀取配置文件,@ConfigurationProperties 讀取配置文件和讀取配置文件中的List,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10