SpringBoot + AOP + MySQL監控系統數據變動實戰

記錄一下Spring AOP切入DAO層,進行數據監控。
在寫這個需求時,從網上找了不少的資料,大部分都是沒有解決相關問題的。固然也有少數部分解決,但用的仍是SSM架構的xml配置。一開始個人出發點是經過註解@annotation的方式來切入DAO須要監控的方法,可是並無用。接下來剖析下我的的實現和思路。java

相關依賴

  • spring-boot-starter-web
  • mysql-connector-java
  • lombok
  • spring-boot-starter-aop
  • mybatis-spring-boot-starter

Spring AOP兩種代理

  • jdk代理
    使用Java動態代理來建立AOP代理,在程序運行期間由JVM根據反射等機制動態的生成(固然此接口要有實現類)。
  • cglib代理
    代理類不是接口時,Spring會切換爲使用CGLIB代理,它的工做原理是:直接在class字節碼文件添加加強的代碼。

思路

在這裏只針對ADD、UPDATE、DELETE作相關數據處理。mysql

直接使用註解@annotation的方式是不能實現,因此我先經過execution的方式切到DAO層,再經過一個自定義註解區分數據操做的類型以及區分所操做的是哪一張表,具體詳情以下:git

  • 記錄ADD操做:使用@After註解,因爲新增一條數據中若是有自增的值,也須要把自增對應字段的值也要記錄,全部在執行完以後記錄
  • 記錄DELETE操做:使用@Before註解,在執行刪除以前,記錄原數據。若是在刪除以後再記錄的話,那條數據已經沒有了
  • 記錄UPDATE操做:使用@Around註解,記錄更新先後的數據,舊數據與新數據有父子關係,這樣數據才能一一對應。

主要核心代碼

  • 自定義註解類
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditAction {
    /**
     * 操做類型
     */
    Action action() default Action.GET;

    /**
     * 目標table
     */
    String targetTable() default "";
}
  • 操做類型枚舉類
public enum Action {
    ADD,
    DELETE,
    UPDATE,
    GET
}
  • 相關DAO層接口中,在須要監控的方法上加上註解
@Mapper
public interface CompanyDao {

    Company getCompanyByNum(int companyNum);

    @AuditAction(action = Action.ADD, targetTable = "company")
    int addCompany(Company company);

    @AuditAction(action = Action.UPDATE, targetTable = "company")
    int updateCompany(Company company);

    @AuditAction(action = Action.DELETE, targetTable = "company")
    int deleteCompany(int companyNum);
}
  • 定義切面類
@Slf4j
@Aspect
@Component
public class SystemAudioAspect {
    // 省略相關業務代碼,詳細代碼請看博客末尾
}
  • 切面類: 監控DELETE操做
    • 對應DELETE操做,這裏我採用在執行以前攔截@Before,@Before(value = "execution(public * com.jtcoding.auditlog.dao...delete(..))")** 攔截全部的delete*方法,經過@AuditAction註解來斷定是否須要監控,再經過註解中的targetTable來區分對應是對哪一個表的操做,具體代碼以下:
/**
 * 攔截DELETE操做,記錄被刪除的數據
 * @param joinPoint
 */
@Before(value = "execution(public * com.jtcoding.auditlog.dao..*.delete*(..))")
public void doBefore(JoinPoint joinPoint) {
    // 獲取方法中的參數
    Object[] args = joinPoint.getArgs();
    // 獲取該方法上的 @AuditAction註解
    AuditAction audioAction = this.getAudioActionByJoinPoint(joinPoint);
    if (audioAction != null && audioAction.action() == Action.DELETE) {
        Object obj = null;
        String targetTable = audioAction.targetTable();
        switch (targetTable) {
            case "company":
                int companyNum = (int) args[0];
                obj = companyService.getCompanyByNum(companyNum);
                break;
            case "plan":
                int planNum = (int) args[0];
                obj = planService.getPlanByNum(planNum);
                break;
        }
        if (obj != null) {
            this.addAudioLog(obj, AuditLogDao.DELETE, targetTable, null);
        }
    }
}
  • 切面類: 監控ADD操做
    • 原理與DELETE相似
/**
 * 攔截ADD操做,記錄新增的數據
 * @param joinPoint
 */
@After(value = "execution(public * com.jtcoding.auditlog.dao..*.add*(..))")
public void doAfter(JoinPoint joinPoint) {
    // 獲取該方法上的 @AuditAction註解
    AuditAction audioAction = this.getAudioActionByJoinPoint(joinPoint);
    if (audioAction != null && audioAction.action() == Action.ADD) {
        Object obj = joinPoint.getArgs()[0];
        this.addAudioLog(obj, AuditLogDao.ADD, audioAction.targetTable(), null);
    }
}
  • 切面類: 監控UPDATE操做
/**
 * 攔截UPDATE操做,記錄更新先後的數據
 * @param pjp
 * @return
 * @throws Throwable
 */
@Around(value = "execution(public * com.jtcoding.auditlog.dao..*.update*(..))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
    AuditAction audioAction = this.getAudioActionByJoinPoint(pjp);
    Object proceed = null;
    if (audioAction != null && audioAction.action() == Action.UPDATE) {
        String uuid = UUID.randomUUID().toString();
        Object originalObj = null;
        Object arg = pjp.getArgs()[0];
        String targetTable = audioAction.targetTable();
        switch (targetTable) {
            case "company":
                Company company = (Company) arg;
                originalObj = companyService.getCompanyByNum(company.getCompanyNum());
                break;
            case "plan":
                Plan plan = (Plan) arg;
                originalObj = planService.getPlanByNum(plan.getPlanNum());
                break;
        }
        AuditLog auditLog = null;
        if (originalObj != null) {
            // TODO 在執行原方法以前,記錄舊數據
            auditLog = this.addAudioLog(originalObj, AuditLogDao.UPDATE, targetTable, null);
        }
        // 執行原方法
        proceed = pjp.proceed();
        // TODO 在執行原方法以後,記錄新數據
        if (auditLog != null) {
            this.addAudioLog(arg, AuditLogDao.UPDATE, targetTable, auditLog.getLogNum());
        }
    }
    if (proceed == null) {
        return pjp.proceed();
    }
    return proceed;
}

至此,有關切面核心的邏輯已經代碼已經完成,相關Service和Controller代碼,請看這裏(源碼)web

測試

  • 經過Postman進行相關API測試
    • Add Company : /companies POST Request
    • Delete Company : /companies/{companyNum} DELETE Request
    • Update Company : /companies PUT Request
    • ............
  • 具體請求以下圖spring

     

    新增Companysql

     

    刪除Company數據庫

     

    獲取某一個Companymybatis

     

    修改Company架構

DB結果

數據庫數據app

  • 由上圖能夠看出:
    • 新增操做:記錄新增的數據
    • 刪除操做:記錄原來的數據
    • 更新操做:記錄原數據與新數據,兩條數據有一個父子關係,方便數據的關聯
    • log_type與log_table_name:能夠很是清晰的看出操做的是哪張表以及操做類型
    • src_num:能夠找到UPDATE操做,先後數據的關聯

遺留問題

  • 問題1:監控變動的數據,這裏使用的是MySQL數據庫,存儲類型是VARCHAR(255),因此很容易超出上限。
  • 問題2:若是項目中使用會更改IoC容器加載順序的Jar,致使切入點無效,能夠在使用到DAO接口的地方,加上@Lazy 懶加載註解便可。(該問題純屬本人猜想,還需深度研究)

最後

以上是我的的思路實現,有不對或者須要優化之處,請指出,謝謝。

相關文章
相關標籤/搜索