SpringAOP+註解實現簡單的日誌管理

原文地址

  

  今天在再次深入學習SpringAOP之後想着基於註解的AOP實現日誌功能,在面試過程中我們也經常會被問到:假如項目已經上線,如何增加一套日誌功能?我們會說使用AOP,AOP也符合開閉原則:對代碼的修改禁止的,對代碼的擴展是允許的。今天經過自己的實踐簡單的實現了AOP日誌。

  在這裏我只是簡單的記錄下當前操作的人、做了什麼操作、操作結果是正常還是失敗、操作時間,實際項目中,如果我們需要記錄的更詳細,可以記錄當前操作人的詳細信息,比如說部門、身份證號等信息,這些信息可以直接從session中獲取,也可以從session中獲取用戶ID之後調用userService從數據庫獲取。我們還可以記錄用戶調用了哪個類的哪個方法,我們可以使用JoinPoint參數獲取或者利用環繞通知ProceedingJoinPoint去獲取。可以精確的定位到類、方法、參數,如果有必要我們就可以記錄在日誌中,看業務需求和我們的日誌表的設計。如果再細緻的記錄日誌,我們可以針對錯誤再建立一個錯誤日誌表,在發生錯誤的情況下(異常通知裏)記錄日誌的錯誤信息。

 

  實現的大致思路是:

    1.前期準備,設計日誌表和日誌類,編寫日誌Dao和Service以及實現

    2.自定義註解,註解中加入幾個屬性,屬性可以標識操作的類型(方法是做什麼的)

    3.編寫切面,切點表達式使用上面的註解直接定位到使用註解的方法,

    4.編寫通知,通過定位到方法,獲取上面的註解以及註解的屬性,然後從session中直接獲取或者從數據庫獲取當前登錄用戶的信息,最後根據業務處理一些日誌信息之後調用日誌Service存儲日誌。

  

  其實日誌記錄可以針對Controller層進行切入,也可以選擇Service層進行切入,我選擇的是基於Service層進行日誌記錄。網上的日誌記錄由的用前置通知,有的用環繞通知,我選擇在環繞通知中完成,環繞通知中可以完成前置、後置、最終、異常通知的所有功能,因此我選擇了環繞通知。(關於AOP的通知使用方法以及XML、註解AOP使用方法參考;http://www.cnblogs.com/qlqwjy/p/8729280.html)

    

 

下面是具體實現:

1.日誌數據庫:

CREATE TABLE `logtable` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `operateor` varchar(5) DEFAULT NULL,
  `operateType` varchar(20) DEFAULT NULL,
  `operateDate` datetime DEFAULT NULL,
  `operateResult` varchar(4) DEFAULT NULL,
  `remark` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8

 

 

  簡單的記錄操作了操作人,操作的類型,操作的日期,操作的結果。如果想詳細的記錄,可以將操作的類名與操作的方法名以及參數信息也新進日誌,在環繞通知中利用反射原理即可獲取這些參數(參考我的另一篇博客:http://www.cnblogs.com/qlqwjy/p/8729280.html)。

 

 

2.日誌實體類:

Logtable.java

package cn.xm.exam.bean.log;

import java.util.Date;

public class Logtable {
    private Integer id;

    private String operateor;

    private String operatetype;

    private Date operatedate;

    private String operateresult;

    private String remark;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getOperateor() {
        return operateor;
    }

    public void setOperateor(String operateor) {
        this.operateor = operateor == null ? null : operateor.trim();
    }

    public String getOperatetype() {
        return operatetype;
    }

    public void setOperatetype(String operatetype) {
        this.operatetype = operatetype == null ? null : operatetype.trim();
    }

    public Date getOperatedate() {
        return operatedate;
    }

    public void setOperatedate(Date operatedate) {
        this.operatedate = operatedate;
    }

    public String getOperateresult() {
        return operateresult;
    }

    public void setOperateresult(String operateresult) {
        this.operateresult = operateresult == null ? null : operateresult.trim();
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark == null ? null : remark.trim();
    }
}

 

 

3.日誌的Dao層使用的是Mybatis的逆向工程導出的mapper,在這裏就不貼出來了

4.日誌的Service層和實現類

  • LogtableService.java接口
package cn.xm.exam.service.log;

import java.sql.SQLException;

import cn.xm.exam.bean.log.Logtable;

/**
 * 日誌Service
 * 
 * @author liqiang
 *
 */
public interface LogtableService {
    /**
     * 增加日誌
     * @param log
     * @return
     * @throws SQLException
     */
    public boolean addLog(Logtable log) throws SQLException;
}

 

 

  • LogtableServiceImpl實現類
package cn.xm.exam.service.impl.log;

import java.sql.SQLException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import cn.xm.exam.bean.log.Logtable;
import cn.xm.exam.mapper.log.LogtableMapper;
import cn.xm.exam.service.log.LogtableService;

@Service
public class LogtableServiceImpl implements LogtableService {
    @Autowired
    private LogtableMapper logtableMapper;
    @Override
    public boolean addLog(Logtable log) throws SQLException {
        return logtableMapper.insert(log) > 0 ? true : false;
    }

}

 

 

5.自定義註解:

package cn.xm.exam.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 日誌註解
 * 
 * @author liqiang
 *
 */
@Target(ElementType.METHOD) // 方法註解
@Retention(RetentionPolicy.RUNTIME) // 運行時可見
public @interface LogAnno {
    String operateType();// 記錄日誌的操作類型
}

 

 

6.在需要日誌記錄的方法中使用註解:(此處將註解寫在DictionaryServiceImpl方法上)

package cn.xm.exam.service.impl.common;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;

import cn.xm.exam.annotation.LogAnno;
import cn.xm.exam.bean.common.Dictionary;
import cn.xm.exam.bean.common.DictionaryExample;
import cn.xm.exam.mapper.common.DictionaryMapper;
import cn.xm.exam.mapper.common.custom.DictionaryCustomMapper;
import cn.xm.exam.service.common.DictionaryService;

/**
 * 字典表的實現類
 * 
 * @author 
 *
 */
@Service
public class DictionaryServiceImpl implements DictionaryService {

    @Resource
    private DictionaryMapper dictionaryMapper;/**
     * 1、添加字典信息
     */
    @LogAnno(operateType = "添加了一個字典項")
    @Override
    public boolean addDictionary(Dictionary dictionary) throws SQLException {
        int result = dictionaryMapper.insert(dictionary);
        if (result > 0) {
            return true;
        } else {
            return false;
        }
    }
}

 

 

7.編寫通知,切入到切點形成切面(註解AOP實現,環繞通知記錄日誌。)

  注意:此處是註解AOP,因此在spring配置文件中開啓註解AOP

 

    <!-- 1.開啓註解AOP -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

 

 

 

 

LogAopAspect.java

package cn.xm.exam.aop;

import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.Date;

import org.apache.struts2.ServletActionContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import cn.xm.exam.annotation.LogAnno;
import cn.xm.exam.bean.log.Logtable;
import cn.xm.exam.bean.system.User;
import cn.xm.exam.service.log.LogtableService;

/**
 * AOP實現日誌
 * 
 * @author liqiang
 *
 */
@Component
@Aspect
public class LogAopAspect {

    @Autowired
    private LogtableService logtableService;// 日誌Service
    /**
     * 環繞通知記錄日誌通過註解匹配到需要增加日誌功能的方法
     * 
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("@annotation(cn.xm.exam.annotation.LogAnno)")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        // 1.方法執行前的處理,相當於前置通知
        // 獲取方法簽名
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 獲取方法
        Method method = methodSignature.getMethod();
        // 獲取方法上面的註解
        LogAnno logAnno = method.getAnnotation(LogAnno.class);
        // 獲取操作描述的屬性值
        String operateType = logAnno.operateType();
        // 創建一個日誌對象(準備記錄日誌)
        Logtable logtable = new Logtable();
        logtable.setOperatetype(operateType);// 操作說明

        // 整合了Struts,所有用這種方式獲取session中屬性(親測有效)
         User user = (User) ServletActionContext.getRequest().getSession().getAttribute("userinfo");//獲取session中的user對象進而獲取操作人名字
        logtable.setOperateor(user.getUsername());// 設置操作人

        Object result = null;
        try {
            //讓代理方法執行
            result = pjp.proceed();
            // 2.相當於後置通知(方法成功執行之後走這裏)
            logtable.setOperateresult("正常");// 設置操作結果
        } catch (SQLException e) {
            // 3.相當於異常通知部分
            logtable.setOperateresult("失敗");// 設置操作結果
        } finally {
            // 4.相當於最終通知
            logtable.setOperatedate(new Date());// 設置操作日期
            logtableService.addLog(logtable);// 添加日誌記錄
        }
        return result;
    }
}

 

  通過攔截帶有 cn.xm.exam.annotation.LogAnno 註解的方法,根據參數獲取到方法,然後獲取方法的LogAnno註解,獲取註解的屬性,在方法執行前後對其進行處理,實現AOP功能。

 

如果需要獲取IP地址可以用如下方法: 

    /**
     * 獲取IP地址的方法
     * @param request   傳一個request對象下來
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

 

 

 

8.測試:

  在頁面上添加一個字典之後打斷點進行查看:

 

  • 會話中當前登錄的用戶信息:

 

 

 

 

  • 當前日誌實體類的信息

 

 

 

 

 

  •  查看數據庫:
mysql> select * from logtable\G
*************************** 1. row ***************************
           id: 1
    operateor: 超級管理員
  operateType: 添加了一個字典項
  operateDate: 2018-04-08 20:46:19
operateResult: 正常
       remark: NULL

 

   到這裏基於註解AOP+註解實現日誌記錄基本實現了。

 

 

9.現在模擬在Service中拋出錯誤的測試:

1.修改ServiceIMpl模擬製造一個除零異常

package cn.xm.exam.service.impl.common;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;

import cn.xm.exam.annotation.LogAnno;
import cn.xm.exam.bean.common.Dictionary;
import cn.xm.exam.bean.common.DictionaryExample;
import cn.xm.exam.mapper.common.DictionaryMapper;
import cn.xm.exam.mapper.common.custom.DictionaryCustomMapper;
import cn.xm.exam.service.common.DictionaryService;

/**
 * 字典表的實現類
 * 
 *
 */
@Service
public class DictionaryServiceImpl implements DictionaryService {

    @Resource
    private DictionaryMapper dictionaryMapper;/**
     * 1、添加字典信息
     */
    @LogAnno(operateType = "添加了一個字典項")
    @Override
    public boolean addDictionary(Dictionary dictionary) throws SQLException {
        int i=1/0;
        int result = dictionaryMapper.insert(dictionary);
        if (result > 0) {
            return true;
        } else {
            return false;
        }
    }
}

 

 

2.修改切面(主要是修改捕捉異常,除零異常不是SQLException,所有修改,實際項目中視情況而定)

package cn.xm.exam.aop;

import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.Date;

import org.apache.struts2.ServletActionContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import cn.xm.exam.annotation.LogAnno;
import cn.xm.exam.bean.log.Logtable;
import cn.xm.exam.bean.system.User;
import cn.xm.exam.service.log.LogtableService;

/**
 * AOP實現日誌
 * 
 * @author liqiang
 *
 */
@Component
@Aspect
public class LogAopAspect {

    @Autowired
    private LogtableService logtableService;// 日誌Service
    /**
     * 環繞通知記錄日誌通過註解匹配到需要增加日誌功能的方法
     * 
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("@annotation(cn.xm.exam.annotation.LogAnno)")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        // 1.方法執行前的處理,相當於前置通知
        // 獲取方法簽名
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 獲取方法
        Method method = methodSignature.getMethod();
        // 獲取方法上面的註解
        LogAnno logAnno = method.getAnnotation(LogAnno.class);
        // 獲取操作描述的屬性值
        String operateType = logAnno.operateType();
        // 創建一個日誌對象(準備記錄日誌)
        Logtable logtable = new Logtable();
        logtable.setOperatetype(operateType);// 操作說明

        // 整合了Struts,所有用這種方式獲取session中屬性(親測有效)
         User user = (User) ServletActionContext.getRequest().getSession().getAttribute("userinfo");//獲取session中的user對象進而獲取操作人名字
        logtable.setOperateor(user.getUsername());// 設置操作人

        Object result = null;
        try {
            //讓代理方法執行
            result = pjp.proceed();
            // 2.相當於後置通知(方法成功執行之後走這裏)
            logtable.setOperateresult("正常");// 設置操作結果
        } catch (Exception e) {
            // 3.相當於異常通知部分
            logtable.setOperateresult("失敗");// 設置操作結果
        } finally {
            // 4.相當於最終通知
            logtable.setOperatedate(new Date());// 設置操作日期
            logtableService.addLog(logtable);// 添加日誌記錄
        }
        return result;
    }
}

 

 

3.結果:

mysql> select * from logtable\G
*************************** 1. row ***************************
           id: 3
    operateor: 超級管理員
  operateType: 添加了一個字典項
  operateDate: 2018-04-08 21:53:53
operateResult: 失敗
       remark: NULL
1 row in set (0.00 sec)

 

 

 

補充:在Spring+SpringMVC+Mybatis的框架中使用的時候,需要註解掃描包的配置以及spring代理方式的配置

    <!-- 6.開啓註解AOP (前提是引入aop命名空間和相關jar包) -->
    <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"></aop:aspectj-autoproxy>

    <!-- 7.開啓aop,對類代理強制使用cglib代理 -->
    <aop:config proxy-target-class="true"></aop:config>

    <!-- 8.掃描 @Service @Component 註解-->
    <context:component-scan base-package="cn.xm.jwxt" >
        <!-- 不掃描 @Controller的類 -->
        <context:exclude-filter type="annotation"
                                expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

 

解釋:  6配置是開啓註解aop,且暴露cglib代理對象,對cglib代理對象進行aop攔截

    7配置是強制spring使用cglib代理

    8是配置掃描的包。且不掃描@Controller 註解,如果需要配置掃描的註解可以:

<context:include-filter type="annotation"  expression="org.springframework.stereotype.Controller" />

 

注意:我在使用Spring+SpringMVc+Mybatis的過程中發現註解AOP沒反應,最後發現編譯只會找不到自己的Aspect類。。。。。。。。

 

 

最後:需要注意的是我在嘗試本實例方法調用本實例方法的時候發現被調用的方法上的註解無效。因此我在另一個類中寫了一個標記方法並打上註解才攔截到註解。

例如:我希望登錄成功之後記錄登錄信息,在登錄成功之後我調用service的一個標記方法即可以使註解生效。

    @MyLogAnnotation(operateDescription = "成功登錄系統")
    @Override
    public void logSuccess(){

    }

 

 

 

補充:關於在Service層和Controller層進行Aop攔截的配置  (如果不生效需要注意配置的配置以及掃描的位置)

  一般我們將掃描@Service寫在applicationContext.xml。因此在applicationContext.xml配置的AOP自動代理對@Service層的註解有效,如果我們需要在Controller層實現註解AOP,我們需要將AOP註解配置在SpringMVC.xml也寫一份,在SpringMVC.xml中只是掃描@Controller註解

  • Spring配置文件applicationContext.xml配置
    <!-- 6.開啓註解AOP (前提是引入aop命名空間和相關jar包) -->
    <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"></aop:aspectj-autoproxy>

    <!-- 7.開啓aop,對類代理強制使用cglib代理 -->
    <aop:config proxy-target-class="true"></aop:config>

    <!-- 8.掃描 @Service @Component 註解-->
    <context:component-scan base-package="cn.xm.jwxt" >
        <!-- 不掃描 @Controller的類 -->
        <context:exclude-filter type="annotation"
                                expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

 

 

  • SpringMVC的配置文件SpringMVC.xml
    <!--1.掃描controller-->
    <context:component-scan base-package="cn.xm.jwxt.controller" />
    <!-- 2.開啓aop,對類代理強制使用cglib代理 -->
    <aop:config proxy-target-class="true"/>
    <!-- 3開啓註解AOP (前提是引入aop命名空間和相關jar包) 暴露代理類-->
    <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"/>

 

 

 

 

 

最後給幾個鏈接,不明白上面的可以參考:

  註解的使用:http://www.cnblogs.com/qlqwjy/p/7139068.html

  Spring中獲取request和session對象:http://www.cnblogs.com/qlqwjy/p/8747136.html

  SpringAOP的使用方法:http://www.cnblogs.com/qlqwjy/p/8729280.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章