今天在再次深刻學習SpringAOP以後想着基於註解的AOP實現日誌功能,在面試過程當中咱們也常常會被問到:假如項目已經上線,如何增長一套日誌功能?咱們會說使用AOP,AOP也符合開閉原則:對代碼的修改禁止的,對代碼的擴展是容許的。今天通過本身的實踐簡單的實現了AOP日誌。html
在這裏我只是簡單的記錄下當前操做的人、作了什麼操做、操做結果是正常仍是失敗、操做時間,實際項目中,若是咱們須要記錄的更詳細,能夠記錄當前操做人的詳細信息,好比說部門、身份證號等信息,這些信息能夠直接從session中獲取,也能夠從session中獲取用戶ID以後調用userService從數據庫獲取。咱們還能夠記錄用戶調用了哪一個類的哪一個方法,咱們可使用JoinPoint參數獲取或者利用環繞通知ProceedingJoinPoint去獲取。能夠精確的定位到類、方法、參數,若是有必要咱們就能夠記錄在日誌中,看業務需求和咱們的日誌表的設計。若是再細緻的記錄日誌,咱們能夠針對錯誤再創建一個錯誤日誌表,在發生錯誤的狀況下(異常通知裏)記錄日誌的錯誤信息。java
實現的大體思路是:mysql
1.前期準備,設計日誌表和日誌類,編寫日誌Dao和Service以及實現面試
2.自定義註解,註解中加入幾個屬性,屬性能夠標識操做的類型(方法是作什麼的)spring
3.編寫切面,切點表達式使用上面的註解直接定位到使用註解的方法,sql
4.編寫通知,經過定位到方法,獲取上面的註解以及註解的屬性,而後從session中直接獲取或者從數據庫獲取當前登陸用戶的信息,最後根據業務處理一些日誌信息以後調用日誌Service存儲日誌。數據庫
其實日誌記錄能夠針對Controller層進行切入,也能夠選擇Service層進行切入,我選擇的是基於Service層進行日誌記錄。網上的日誌記錄由的用前置通知,有的用環繞通知,我選擇在環繞通知中完成,環繞通知中能夠完成前置、後置、最終、異常通知的全部功能,所以我選擇了環繞通知。(關於AOP的通知使用方法以及XML、註解AOP使用方法參考;http://www.cnblogs.com/qlqwjy/p/8729280.html)express
下面是具體實現:apache
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)。session
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;/** * 一、添加字典信息 */ @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;/** * 一、添加字典信息 */ @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