1. 概述
在一般系統中,當我們做了一些重要的操作時,如登陸系統,添加用戶,刪除用戶等操作時,我們需要將這些行為持久化。本文我們通過Spring AOP和Java的自定義註解來實現日誌的插入。此方案對原有業務入侵較低,實現較靈活
2. 日誌的相關類定義
我們將日誌抽象為以下兩個類:功能模塊和操作類型
使用枚舉類定義功能模塊類型ModuleType,如學生、用戶模塊
public enum ModuleType { DEFAULT("1"), // 默認值STUDENT("2"),// 學生模塊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. 定義日誌相關的註解
3.1. @LogEnable
這裡我們定義日誌的開關量,類上只有這個值為true,這個類中日誌功能才開啟
@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface LogEnable { /** * 如果為true,則類下面的LogEvent啟作用,否則忽略* @return */ boolean logEnable() default true;}3.2. @LogEvent
這裡定義日誌的詳細內容。如果此註解註解在類上,則這個參數做為類全部方法的默認值。如果註解在方法上,則只對這個方法啟作用
@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
此註解如果註解在方法上,則整個方法的參數以json的格式保存到日誌中。如果此註解同時註解在方法和類上,則方法上的註解會覆蓋類上的值。
@Target({ElementType.FIELD,ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @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; // 操作內容private String admOptContent; // 操作內容private String desc; // 備註set/get略}4.2. ILogManager
定義日誌處理的接口類ILogManager
我們可以將日誌存入數據庫,也可以將日誌發送到開中間件,如果redis, mq等等。每一種日誌處理類都是此接口的實現類
public interface ILogManager { /** * 日誌處理模塊* @param paramLogAdmBean */ void dealLog(LogAdmModel paramLogAdmBean);}4.3. DBLogManager
ILogManager實現類,將日誌入庫。這裡只模擬入庫
@Servicepublic class DBLogManager implements ILogManager { @Override public void dealLog(LogAdmModel paramLogAdmBean) { System.out.println("將日誌存入數據庫,日誌內容如下: " + JSON.toJSONString(paramLogAdmBean)); }}5. AOP的配置
5.1. LogAspect定義AOP類
使用@Aspect註解此類
使用@Pointcut定義要攔截的包及類方法
我們使用@Around定義方法
@Component@Aspectpublic 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:主方法的主要業務流程
1. 檢查攔截方法的類是否被@LogEnable註解,如果是,則走日誌邏輯,否則執行正常的邏輯
2. 檢查攔截方法是否被@LogEvent,如果是,則走日誌邏輯,否則執行正常的邏輯
3. 根據獲取方法上獲取@LogEvent 中值,生成日誌的部分參數。其中定義在類上@LogEvent 的值做為默認值
4. 調用logInfoGeneration的processingManagerLogMessage填充日誌中其它的參數,做個方法我們後面再講
5. 執行正常的業務調用
6. 如果執行成功,則logManager執行日誌的處理(我們這裡只記錄執行成功的日誌,你也可以定義記錄失敗的日誌)
@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做為默認值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){ // 如果方法上的值為默認值,則使用全局的值進行替換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 如果是登錄,還需要根據返回值進行判斷是不是成功了,如果成功了,則執行添加日誌。這裡判斷比較簡單if(returnObj != null) { this.logManager.dealLog(logBean); } }else { this.logManager.dealLog(logBean); } return returnObj; } /** * 獲取請求方法* * @param jp * @return */ public Method getInvokedMethod(JoinPoint jp) { // 調用方法的參數List classList = new ArrayList(); for (Object obj : jp.getArgs()) { classList.add(obj.getClass()); } Class[] argsCls = (Class[]) classList.toArray(new Class[0]); // 被調用方法名稱String methodName = jp.getSignature().getName(); Method method = null; try { method = jp.getTarget().getClass().getMethod(methodName, argsCls); } catch (NoSuchMethodException e) { e.printStackTrace(); } return method; } }6. 將以上的方案在實際中應用的方案
這裡我們模擬學生操作的業務,並使用上文註解應用到上面並攔截日誌
6.1. IStudentService
業務接口類,執行一般的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 : 啟動日誌攔截類上@LogEvent定義所有的模塊方法上@LogEven定義日誌的其它的信息@Service@LogEnable // 啟動日誌攔截@LogEvent(module = ModuleType.STUDENT)public class StudentServiceImpl implements IStudentService { @Override @LogEvent(event = EventType.DELETE_SINGLE, desc = "刪除記錄") // 添加日誌標識public void deleteById(@LogKey(keyName = "id") String id, String a) { System.out.printf(this.getClass() + "deleteById id = " + id); } @Override @LogEvent(event = EventType.ADD, desc = "保存記錄") // 添加日誌標識public int save(StudentModel studentModel) { System.out.printf(this.getClass() + "save save = " + JSON.toJSONString(studentModel)); return 1; } @Override @LogEvent(event = EventType.UPDATE, desc = "更新記錄") // 添加日誌標識public void update(StudentModel studentModel) { System.out.printf(this.getClass() + "save update = " + JSON.toJSONString(studentModel)); } // 沒有日誌標識@Override public void queryById(String id) { System.out.printf(this.getClass() + "queryById id = " + id); }}執行測試類,打印如下信息,說明我們日誌註解配置啟作用了:
將日誌存入數據庫,日誌內容如下:
{"admEvent":"4","admModel":"1","admOptContent":"{/"id/":/"1/"}","createDate":1525779738111,"desc":"刪除記錄"}7. 代碼
以上的詳細的代碼見下面
github代碼,請盡量使用tag v0.21,不要使用master,因為我不能保證master代碼一直不變