Spring AOP 完成日誌記錄
1、技術目標
- 掌握Spring AOP基本用法
- 使用Spring AOP完成日誌記錄功能
提示:本文所用項目爲"影片管理",參看
http://hotstrong.iteye.com/blog/1160153
本文基於"影片管理"項目進行了日誌記錄功能擴充
注意:本文所實現的項目(MyEclipse工程)已提供下載,數據庫
腳本可參看《MyBatis 1章 入門(使用MyBatis完成CRUD)》
2、什麼是AOP
AOP是Aspect Oriented Programming的縮寫,意思是面向方面編程,AOP實際是GoF設計模式的延續
注意:關於AOP的詳細介紹不是本文重點
3、關於Spring AOP的一些術語
- 切面(Aspect):在Spring AOP中,切面可以使用通用類或者在普通類中以@Aspect 註解(@AspectJ風格)來實現
- 連接點(Joinpoint):在Spring AOP中一個連接點代表一個方法的執行
- 通知(Advice):在切面的某個特定的連接點(Joinpoint)上執行的動作。通知有各種類型,其中包括"around"、"before”和"after"等通知。許多AOP框架,包括Spring,都是以攔截器做通知模型, 並維護一個以連接點爲中心的攔截器鏈
- 切入點(Pointcut):定義出一個或一組方法,當執行這些方法時可產生通知,Spring缺省使用AspectJ切入點語法。
4、通知類型
- 前置通知(@Before):在某連接點(join point)之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常)
- 返回後通知(@AfterReturning):在某連接點(join point)正常完成後執行的通知:例如,一個方法沒有拋出任何異常,正常返回
- 拋出異常後通知(@AfterThrowing):方法拋出異常退出時執行的通知
- 後通知(@After):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)
- 環繞通知(@Around):包圍一個連接點(join point)的通知,如方法調用。這是最強大的一種通知類型,環繞通知可以在方法調用前後完成自定義的行爲,它也會選擇是否繼續執行連接點或直接返回它們自己的返回值或拋出異常來結束執行
5、@AspectJ風格的AOP配置
Spring AOP配置有兩種風格:
- XML風格 = 採用聲明形式實現Spring AOP
- AspectJ風格 = 採用註解形式實現Spring AOP
注意:本文采用AspectJ風格
6、使用準備
閒話少說,下面開始日誌記錄的準備工作
6.1)創建日誌記錄表(MySQL),
CREATE TABLE `t_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`userid` bigint(20) unsigned NOT NULL,
`createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建日期',
`content` varchar(8000) NOT NULL DEFAULT '' COMMENT '日誌內容',
`operation` varchar(250) NOT NULL DEFAULT '' COMMENT '用戶所做的操作',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
6.2)在經過了Spring Security的權限驗證後,可以從Security中獲取到
登錄管理員的帳號,而日誌記錄表t_log中存儲的是管理員id,所以需要通
過管理員的帳號查詢出管理員id,創建管理員POJO、Mapper、Service,
代碼及配置如下:
管理員POJO類:
package com.xxx.pojo;
public class Admin extends BaseDomain {
private String nickname;//管理員帳號
private String passwd;//管理員密碼
private String phoneno;//聯繫電話
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getPhoneno() {
return phoneno;
}
public void setPhoneno(String phoneno) {
this.phoneno = phoneno;
}
}
管理員Mapper接口與XML配置文件:
package com.xxx.dao;
import com.xxx.pojo.Admin;
/**
* 管理員Mapper接口
*/
public interface AdminMapper {
/**
* 獲取指定帳號名的管理員
*/
public Admin findAdminByNickname(String userName);
}
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xxx.dao.AdminMapper"> <!-- 通過賬號名稱查詢管理員 --> <select id="findAdminByNickname" parameterType="string" resultType="Admin"> select * from t_admin where nickname=#{userName} </select> </mapper>
管理員Service接口與實現類:
package com.xxx.service;
import com.xxx.pojo.Admin;
/**
* 管理員信息業務邏輯接口
*/
public interface AdminService {
/**
* 獲取指定帳號名的管理員
*/
public Admin findAdminByNickname(String userName);
}
package com.xxx.service;
import org.springframework.beans.factory.annotation.Autowired;
import com.xxx.dao.AdminMapper;
import com.xxx.pojo.Admin;
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;//Mapper接口
public Admin findAdminByNickname(String userName) {
return adminMapper.findAdminByNickname(userName);
}
}
6.3)創建日誌記錄POJO、Mapper、Service,代碼及配置如下:
日誌記錄POJO類:
package com.xxx.pojo;
import java.io.Serializable;
import java.util.Date;
/**
* 日誌記錄POJO
*/
public class Log extends BaseDomain implements Serializable{
private static final long serialVersionUID = 1024792477652984770L;
private Long userid;//管理員id
private Date createdate;//日期
private String content;//日誌內容
private String operation;//操作(主要是"添加"、"修改"、"刪除")
//getter、setter,此處省略N字(你懂的)
}
日誌記錄Mapper接口與XML配置文件:
package com.xxx.dao;
import com.xxx.pojo.Log;
/**
* 日誌記錄Mapper
*/
public interface LogMapper {
public void insert(Log log);//添加日誌記錄
}
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xxx.dao.LogMapper"> <!-- 添加日誌記錄 --> <insert id="insert" parameterType="Log"> INSERT INTO t_log(userid,createdate,operation,content) VALUES(#{userid},NOW(),#{operation},#{content}); </insert> </mapper>
日誌記錄Service接口與實現類:
package com.xxx.service;
import org.springframework.transaction.annotation.Transactional;
import com.xxx.pojo.Log;
/**
* 日誌記錄業務邏輯接口
*/
public interface LogService {
/**
* 日誌記錄
* @param log
*/
@Transactional
public void log(Log log);
/**
* 獲取登錄管理員ID
*/
public Long loginUserId();
}
package com.xxx.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import com.xxx.dao.LogMapper;
import com.xxx.pojo.Admin;
import com.xxx.pojo.Log;
/**
* 日誌記錄業務邏輯接口實現類
* @author HotStrong
*/
public class LogServiceImpl implements LogService{
@Autowired
private AdminService adminService;
@Autowired
private LogMapper logMapper;
public void log(Log log) {
logMapper.insert(log);
}
/**
* 獲取登錄管理員ID
*
* @return
*/
public Long loginUserId() {
if(SecurityContextHolder.getContext() == null){
return null;
}
if(SecurityContextHolder.getContext().getAuthentication() == null){
return null;
}
UserDetails userDetails = (UserDetails) SecurityContextHolder
.getContext().getAuthentication().getPrincipal();
if(userDetails == null){
return null;
}
//獲取登錄管理員帳號名
String userName = userDetails.getUsername();
if(userName == null || userName.equals("")){
return null;
}
// 根據管理員帳號名獲取帳號ID
Admin admin = this.adminService.findAdminByNickname(userName);
if(admin == null){
return null;
}
return admin.getId();
}
}
7、在MyBatis配置文件mybatis-config.xml中配置POJO,如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- changes from the defaults --> <setting name="lazyLoadingEnabled" value="false" /> </settings> <typeAliases> <typeAlias alias="Film" type="com.xxx.pojo.Film"/> <typeAlias alias="Admin" type="com.xxx.pojo.Admin"/> <typeAlias alias="Log" type="com.xxx.pojo.Log"/> </typeAliases> </configuration>
8、創建aop包,在aop包下創建切面類LogAspect
package com.xxx.aop;
import java.lang.reflect.Method;
import java.util.Date;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import com.xxx.pojo.Film;
import com.xxx.pojo.Log;
import com.xxx.service.FilmService;
import com.xxx.service.LogService;
/**
* 日誌記錄,添加、刪除、修改方法AOP
* @author HotStrong
*
*/
@Aspect
public class LogAspect {
@Autowired
private LogService logService;//日誌記錄Service
@Autowired
private FilmService filmService;//影片Service
/**
* 添加業務邏輯方法切入點
*/
@Pointcut("execution(* com.xxx.service.*.insert*(..))")
public void insertServiceCall() { }
/**
* 修改業務邏輯方法切入點
*/
@Pointcut("execution(* com.xxx.service.*.update*(..))")
public void updateServiceCall() { }
/**
* 刪除影片業務邏輯方法切入點
*/
@Pointcut("execution(* com.xxx.service.FilmService.deleteFilm(..))")
public void deleteFilmCall() { }
/**
* 管理員添加操作日誌(後置通知)
* @param joinPoint
* @param rtv
* @throws Throwable
*/
@AfterReturning(value="insertServiceCall()", argNames="rtv", returning="rtv")
public void insertServiceCallCalls(JoinPoint joinPoint, Object rtv) throws Throwable{
//獲取登錄管理員id
Long adminUserId = logService.loginUserId();
if(adminUserId == null){//沒有管理員登錄
return;
}
//判斷參數
if(joinPoint.getArgs() == null){//沒有參數
return;
}
//獲取方法名
String methodName = joinPoint.getSignature().getName();
//獲取操作內容
String opContent = adminOptionContent(joinPoint.getArgs(), methodName);
//創建日誌對象
Log log = new Log();
log.setUserid(logService.loginUserId());//設置管理員id
log.setCreatedate(new Date());//操作時間
log.setContent(opContent);//操作內容
log.setOperation("添加");//操作
logService.log(log);//添加日誌
}
/**
* 管理員修改操作日誌(後置通知)
* @param joinPoint
* @param rtv
* @throws Throwable
*/
@AfterReturning(value="updateServiceCall()", argNames="rtv", returning="rtv")
public void updateServiceCallCalls(JoinPoint joinPoint, Object rtv) throws Throwable{
//獲取登錄管理員id
Long adminUserId = logService.loginUserId();
if(adminUserId == null){//沒有管理員登錄
return;
}
//判斷參數
if(joinPoint.getArgs() == null){//沒有參數
return;
}
//獲取方法名
String methodName = joinPoint.getSignature().getName();
//獲取操作內容
String opContent = adminOptionContent(joinPoint.getArgs(), methodName);
//創建日誌對象
Log log = new Log();
log.setUserid(logService.loginUserId());//設置管理員id
log.setCreatedate(new Date());//操作時間
log.setContent(opContent);//操作內容
log.setOperation("修改");//操作
logService.log(log);//添加日誌
}
/**
* 管理員刪除影片操作(環繞通知),使用環繞通知的目的是
* 在影片被刪除前可以先查詢出影片信息用於日誌記錄
* @param joinPoint
* @param rtv
* @throws Throwable
*/
@Around(value="deleteFilmCall()", argNames="rtv")
public Object deleteFilmCallCalls(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
//環繞通知處理方法
try {
//獲取方法參數(被刪除的影片id)
Integer id = (Integer)pjp.getArgs()[0];
Film obj = null;//影片對象
if(id != null){
//刪除前先查詢出影片對象
obj = filmService.getFilmById(id);
}
//執行刪除影片操作
result = pjp.proceed();
if(obj != null){
//創建日誌對象
Log log = new Log();
log.setUserid(logService.loginUserId());//用戶編號
log.setCreatedate(new Date());//操作時間
StringBuffer msg = new StringBuffer("影片名 : ");
msg.append(obj.getFname());
log.setContent(msg.toString());//操作內容
log.setOperation("刪除");//操作
logService.log(log);//添加日誌
}
}
catch(Exception ex) {
ex.printStackTrace();
}
return result;
}
/**
* 使用Java反射來獲取被攔截方法(insert、update)的參數值,
* 將參數值拼接爲操作內容
*/
public String adminOptionContent(Object[] args, String mName) throws Exception{
if (args == null) {
return null;
}
StringBuffer rs = new StringBuffer();
rs.append(mName);
String className = null;
int index = 1;
// 遍歷參數對象
for (Object info : args) {
//獲取對象類型
className = info.getClass().getName();
className = className.substring(className.lastIndexOf(".") + 1);
rs.append("[參數" + index + ",類型:" + className + ",值:");
// 獲取對象的所有方法
Method[] methods = info.getClass().getDeclaredMethods();
// 遍歷方法,判斷get方法
for (Method method : methods) {
String methodName = method.getName();
// 判斷是不是get方法
if (methodName.indexOf("get") == -1) {// 不是get方法
continue;// 不處理
}
Object rsValue = null;
try {
// 調用get方法,獲取返回值
rsValue = method.invoke(info);
if (rsValue == null) {//沒有返回值
continue;
}
} catch (Exception e) {
continue;
}
//將值加入內容中
rs.append("(" + methodName + " : " + rsValue + ")");
}
rs.append("]");
index++;
}
return rs.toString();
}
}
9、對管理員登錄操作進行日誌記錄
還記得《使用Spring Security實現權限管理》一文中第7步提到的兩個類嗎?其中LoginSuccessHandler類中可以記錄管理員的登錄操作,代碼如下:
package com.xxx.security;
import java.io.IOException;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import com.xxx.pojo.Log;
import com.xxx.service.LogService;
/**
* 處理管理登錄日誌
*
*/
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
@Autowired
private LogService logService;//日誌記錄Service
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException,
ServletException {
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
//創建日誌對象
Log log = new Log();
log.setUserid(logService.loginUserId());//設置管理員id
log.setCreatedate(new Date());//操作時間
log.setContent("管理員 " + userDetails.getUsername());//操作內容
log.setOperation("登錄");//操作
logService.log(log);//添加日誌
super.onAuthenticationSuccess(request, response, authentication);
}
}
10、在applicationContext-services.xml中加入新的配置
applicationContext-services.xml中加入了Aspectj配置以及新增的管理員Service、日誌記錄Service配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> <!-- 加入Aspectj配置 --> <aop:aspectj-autoproxy /> <bean id="logAspect" class="com.xxx.aop.LogAspect" /> <!-- 電影業務邏輯對象 --> <bean id="filmService" class="com.xxx.service.FilmServiceImpl"></bean> <!-- 管理員業務邏輯對象 --> <bean id="adminService" class="com.xxx.service.AdminServiceImpl"></bean> <!-- 日誌記錄業務邏輯對象 --> <bean id="logService" class="com.xxx.service.LogServiceImpl"></bean> </beans>
11、配置成功後分別進行登錄、添加、修改、刪除影片操作,日誌記錄表的內容如下:
參考文章:
MyBatis 1章 入門(使用MyBatis完成CRUD)