Spring Boot系列十八 Spring AOP + 註解實現統一註解

1. 概述

在通常系統中,當咱們作了一些重要的操做時,如登錄系統,添加用戶,刪除用戶等操做時,咱們須要將這些行爲持久化。本文咱們經過Spring AOP和Java的自定義註解來實現日誌的插入。此方案對原有業務入侵較低,實現較靈活java

2. 日誌的相關類定義

咱們將日誌抽象爲如下兩個類:功能模塊和操做類型 使用枚舉類定義功能模塊類型ModuleType,如學生、用戶模塊git

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。如登錄、添加、刪除、更新、刪除等github

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,這個類中日誌功能纔開啓redis

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogEnable {
    /**
     * 若是爲true,則類下面的LogEvent啓做用,不然忽略
     * @return
     */
    boolean logEnable() default true;
}
複製代碼

3.2. @LogEvent

這裏定義日誌的詳細內容。若是此註解註解在類上,則這個參數作爲類所有方法的默認值。若是註解在方法上,則只對這個方法啓做用spring

@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)
@Documented
public @interface LogKey {
     String keyName() default ""; // key的名稱
     boolean isUserId() default false; // 此字段是不是本次操做的userId,這裏略
     boolean isLog() default true; // 是否加入到日誌中
}
複製代碼

4. 定義日誌處理類

4.1. LogAdmModel

定義保存日誌信息的類json

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等等。每一種日誌處理類都是此接口的實現類bash

public interface ILogManager {
    /**
     * 日誌處理模塊
     * @param paramLogAdmBean
     */
    void dealLog(LogAdmModel paramLogAdmBean);
}
複製代碼

4.3. DBLogManager

ILogManager實現類,將日誌入庫。這裏只模擬入庫mvc

@Service
public 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
@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:主方法的主要業務流程 1. 檢查攔截方法的類是否被@LogEnable註解,若是是,則走日誌邏輯,不然執行正常的邏輯 2. 檢查攔截方法是否被@LogEvent,若是是,則走日誌邏輯,不然執行正常的邏輯 3. 根據獲取方法上獲取@LogEvent 中值,生成日誌的部分參數。其中定義在類上@LogEvent 的值作爲默認值 4. 調用logInfoGeneration的processingManagerLogMessage填充日誌中其它的參數,作個方法咱們後面再講 5. 執行正常的業務調用 6. 若是執行成功,則logManager執行日誌的處理(咱們這裏只記錄執行成功的日誌,你也能夠定義記錄失敗的日誌)ide

@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代碼一直不變

相關文章
相關標籤/搜索