1. 程式人生 > >用Spring AOP完成資料庫日誌記錄

用Spring AOP完成資料庫日誌記錄

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

以我現在做的專案為例,講解用Spring AOP完成資料庫日誌記錄的實現過程。

1.建立日誌記錄表(MySQL

Sql程式碼:

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for `room_log`
-- ----------------------------
DROP TABLE IF EXISTS `room_log`;
CREATE TABLE `room_log` (
  `id` int(12) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `roomno` varchar(10) NOT NULL COMMENT '房間號碼',
  `optTime` datetime NOT NULL COMMENT '操作時間',
  `sysTime` datetime NOT NULL COMMENT '賬務日期',
  `memo` varchar(100) DEFAULT NULL COMMENT '備註',
  `optMan` varchar(12) NOT NULL COMMENT '操作人',
  `substoreid` varchar(10) NOT NULL COMMENT '分店號',
  `content` varchar(100) NOT NULL COMMENT '日誌內容',
  `logType` varchar(8) NOT NULL COMMENT '日誌型別(增刪改查)',
  `optType` varchar(20) NOT NULL COMMENT '操作型別',
  `source` varchar(10) DEFAULT NULL COMMENT '來源,客戶端,app 等等',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;

2.自定義日誌註解

Java程式碼:
package com.leike.roomStatus.core.annotation;

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

/**
 * @func 自定義日誌註解
 * @author 皮鋒
 * @date 2016/11/17
 */
// 在使用Retention時必須要提供一個RetentionPolicy的列舉型別引數
// RetentionPolicy有三個列舉內容:CLASS RUNTIME SOURCE
// SOURCE, //編譯程式處理完Annotation資訊後就完成任務
// CLASS, //編譯程式將Annotation儲存於class檔中,預設
// RUNTIME //編譯程式將Annotation儲存於class當中,可由JVM讀入(通過反射機制)。這個功能搭配反射是非常強大的
@Retention(RetentionPolicy.RUNTIME)
// @Target裡面的ElementType是用來指定Annotation型別可以用在哪一些元素上的.說明一下:TYPE(型別), FIELD(屬性),
// METHOD(方法), PARAMETER(引數), CONSTRUCTOR(建構函式),LOCAL_VARIABLE(區域性變數),
// ANNOTATION_TYPE,PACKAGE(包),其中的TYPE(型別)是指可以用在Class,Interface,Enum和Annotation型別上.
@Target(ElementType.METHOD)
public @interface Loggable {

	/**
	 * @func 操作型別:四種(INSERT, UPDATE, SELECT, DELETE)
	 */
	public String optType();

	/**
	 * @func 描述
	 */
	public String describe();

	/**
	 * @func 日誌模組,不同模組的日誌儲存到不同的日誌表中
	 */
	public String module();
}

3.定義資料庫日誌操作型別常量

Java程式碼:

package com.leike.roomStatus.common.consts;

/**
 * @func 資料庫日誌操作型別
 * @author 皮鋒
 * @date 2016/11/18
 */
public class LogOptType {

	public static final String INSERT = "INSERT";
	public static final String UPDATE = "UPDATE";
	public static final String SELECT = "SELECT";
	public static final String DELETE = "DELETE";

}

4.定義資料庫中不同的日誌模組常量

不同的模組對應不同的資料庫日誌表。

Java程式碼:

package com.leike.roomStatus.common.consts;

/**
 * @func 資料庫中不同的日誌模組
 * @author 皮鋒
 * @date 2016/11/17
 */
public class LogModule {

	public static final String ROOMLOG = "room_log"; // 房間日誌表
}

5.配置spring,啟用aop註解支援

<!--啟用aop註解支援-->
<aop:aspectj-autoproxy/>

6.建立日誌記錄modeldaoMapper程式碼及配置如下

1. 日誌記錄model類:

package com.leike.roomStatus.common.model;

import java.util.Date;

import com.alibaba.fastjson.annotation.JSONField;
import com.leike.common.page.base.BaseEntity;

public class RoomLog extends BaseEntity {

	private static final long serialVersionUID = 8651053349591997316L;
	private long id;// 主鍵
	private String roomno;// 房號
	@JSONField(format = "yyyy-MM-dd HH:mm:ss")
	private Date optTime;// 操作時間
	@JSONField(format = "yyyy-MM-dd HH:mm:ss")
	private Date sysTime;// 賬務時間
	private String memo;// 備註
	private String optMan;// 操作人
	private String substoreid;// 分店號
	private String content;// 日誌內容
	private String logType;// 日誌型別
	private String optType;// 做什麼事情
	private String source;// 日誌來源

	public RoomLog(long id, String roomno, Date optTime, Date sysTime,
			String memo, String optMan, String substoreid, String content,
			String logType, String optType, String source) {
		this.id = id;
		this.roomno = roomno;
		this.optTime = optTime;
		this.sysTime = sysTime;
		this.memo = memo;
		this.optMan = optMan;
		this.substoreid = substoreid;
		this.content = content;
		this.logType = logType;
		this.optType = optType;
		this.source = source;
	}

	public RoomLog() {
	}

	public long getId() {
		return id;
	}

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

	public String getRoomno() {
		return roomno;
	}

	public void setRoomno(String roomno) {
		this.roomno = roomno;
	}

	public Date getOptTime() {
		return optTime;
	}

	public void setOptTime(Date optTime) {
		this.optTime = optTime;
	}

	public Date getSysTime() {
		return sysTime;
	}

	public void setSysTime(Date sysTime) {
		this.sysTime = sysTime;
	}

	public String getMemo() {
		return memo;
	}

	public void setMemo(String memo) {
		this.memo = memo;
	}

	public String getOptMan() {
		return optMan;
	}

	public void setOptMan(String optMan) {
		this.optMan = optMan;
	}

	public String getSubstoreid() {
		return substoreid;
	}

	public void setSubstoreid(String substoreid) {
		this.substoreid = substoreid;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public String getLogType() {
		return logType;
	}

	public void setLogType(String logType) {
		this.logType = logType;
	}

	public String getOptType() {
		return optType;
	}

	public void setOptType(String optType) {
		this.optType = optType;
	}

	public String getSource() {
		return source;
	}

	public void setSource(String source) {
		this.source = source;
	}

}

2. 日誌記錄dao以及其實現

package com.leike.roomStatus.data.dao;

import java.io.Serializable;

import com.leike.common.exception.BizException;
import com.leike.common.page.base.EntityDao;
import com.leike.roomStatus.common.model.RoomLog;
/**
 * @func 操作日誌DAO介面
 * @author 皮鋒
 * @date 2016/11/17
 * @param <E>
 * @param <PK>
 */
public interface LogDao<E, PK extends Serializable> extends EntityDao<E, PK> {

	/**
	 * @func 插入房間日誌記錄
	 * @date 2016/11/18
	 * @param roomLog
	 * @return int
	 * @throws BizException
	 */
	public int insertRoomLog(RoomLog roomLog) throws BizException;

	
}
package com.leike.roomStatus.data.dao.impl;

import com.leike.common.exception.BizException;
import com.leike.common.page.base.BaseMyIbatisDao;
import com.leike.roomStatus.common.model.RoomLog;
import com.leike.roomStatus.data.dao.LogDao;

/**
 * @func 日誌相關的Dao介面實現
 * @author 皮鋒
 * @date 2016/11/17
 */
public class LogDaoImpl extends BaseMyIbatisDao<Object, String> implements
		LogDao<Object, String> {

	@Override
	public int saveOrUpdate(Object arg0) {
		return 0;
	}

	@Override
	public Class getEntityClass() {
		return Object.class;
	}

	/**
	 * @func 插入房間日誌記錄
	 * @date 2016/11/18
	 * @param roomLog
	 * @return int
	 * @throws BizException
	 */
	@Override
	public int insertRoomLog(RoomLog roomLog) throws BizException {
		return db().insert("Log.insertRoomLog", roomLog);
	}

}

3. Mapper.xml程式碼

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">

<mapper namespace="Log">
	
	<insert id="insertRoomLog" parameterType="com.leike.roomStatus.common.model.RoomLog">
		insert into room_log(roomno,optTime,sysTime,memo,optMan,substoreid,content,logType,optType,source) 
		value (#{roomno},#{optTime},#{sysTime},#{memo},#{optMan},#{substoreid},#{content},#{logType},#{optType},#{source})
	</insert>
	
</mapper>

7.建立aop包,在aop包下建立切面類OperationLogger

/*
 * Pointcut可以有下列方式來定義或者通過&& || 和!的方式進行組合. 
 * args()
 * @args()
 * execution()
 * this()
 * target()
 * @target()
 * within()
 * @within()
 * @annotation
 * 其中execution 是用的最多的,其格式為:
 * execution 表示式,其格式為:
 * execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
 * returning type pattern,name pattern, and parameters pattern是必須的.
 * ret-type-pattern:可以為*表示任何返回值,全路徑的類名等.
 * name-pattern:指定方法名,*代表所以,set*,代表以set開頭的所有方法.
 * parameters pattern:指定方法引數(宣告的型別),(..)代表所有引數,(*)代表一個引數,(*,String)代表第一個引數為任何值,第二個為String型別.
 * 舉例說明:
 * 任意公共方法的執行:
 * execution(public * *(..))
 * 任何一個以“set”開始的方法的執行:
 * execution(* set*(..))
 * AccountService 介面的任意方法的執行:
 * execution(* com.xyz.service.AccountService.*(..))
 * 定義在service包裡的任意方法的執行:
 * execution(* com.xyz.service.*.*(..))
 * 定義在service包和所有子包裡的任意類的任意方法的執行:
 * execution(* com.xyz.service..*.*(..))
 * 定義在pointcutexp包和所有子包裡的JoinPointObjP2類的任意方法的執行:
 * execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")
 * 最靠近(..)的為方法名,靠近.*(..))的為類名或者介面名,如上例的JoinPointObjP2.*(..))
 * pointcutexp包裡的任意類.
 * within(com.test.spring.aop.pointcutexp.*)
 * pointcutexp包和所有子包裡的任意類.
 * within(com.test.spring.aop.pointcutexp..*)
 * 實現了Intf介面的所有類,如果Intf不是介面,限定Intf單個類.
 * this(com.test.spring.aop.pointcutexp.Intf)
 * 
 * 當一個實現了介面的類被AOP的時候,用getBean方法必須cast為介面型別,不能為該類的型別.
 * 
 * 帶有@Transactional標註的所有類的任意方法.
 * @within(org.springframework.transaction.annotation.Transactional)
 * @target(org.springframework.transaction.annotation.Transactional)
 * 帶有@Transactional標註的任意方法.
 * @annotation(org.springframework.transaction.annotation.Transactional)
 * @within和@target針對類的註解,@annotation是針對方法的註解
 * 
 * 引數帶有@Transactional標註的方法.
 * @args(org.springframework.transaction.annotation.Transactional)
 * 引數為String型別(執行是決定)的方法.
 * args(String)
 * Pointcut 可以通過Java註解和XML兩種方式配置
 */
package com.leike.roomStatus.service.aop;

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

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import com.leike.common.exception.BizException;
import com.leike.roomStatus.common.model.RoomKeep;
import com.leike.roomStatus.common.model.RoomLog;
import com.leike.roomStatus.core.annotation.Loggable;
import com.leike.roomStatus.data.dao.HotelInfoDao;
import com.leike.roomStatus.data.dao.LogDao;

/**
 * @func 操作日誌
 * @author 皮鋒
 * @date 2016/11/17
 */
// @Aspect的意思是面向切面程式設計,一個類前面加上@Aspect說明這個類使用了這個技術
@Aspect
// 這裡就是說把這個類交給Spring管理,重新起個名字叫operationLogger,由於不好說這個類屬於哪個層面,就用@Component
@Component(value = "operationLogger")
public class OperationLogger {

	private LogDao logDao;

	private HotelInfoDao hotelInfoDao;

	public HotelInfoDao getHotelInfoDao() {
		return hotelInfoDao;
	}

	public void setHotelInfoDao(HotelInfoDao hotelInfoDao) {
		this.hotelInfoDao = hotelInfoDao;
	}
	
	public LogDao getLogDao() {
		return logDao;
	}

	public void setLogDao(LogDao logDao) {
		this.logDao = logDao;
	}

	// @Pointcut是指那些方法需要被執行"AOP",是由"Pointcut Expression"來描述的
	@Pointcut("execution(* *(..)) && @annotation(com.leike.roomStatus.core.annotation.Loggable)")
	public void log() {
	}

	// @AfterReturning(value="切入點表示式或命名切入點",pointcut="切入點表示式或命名切入點",argNames="引數列表引數名",returning="返回值對應引數名")
	@AfterReturning(value = "log()", returning = "retVal")
	public void log(JoinPoint point, Object retVal) throws BizException {
		Object[] params = point.getArgs();// 獲取引數
		String methodName = point.getSignature().getName();// 獲取方法名
		Class<?> targetClass = point.getTarget().getClass();// 獲取目標物件的類名
		Method method = null;
		for (Method mt : targetClass.getMethods()) {
			if (methodName.equals(mt.getName())) {
				method = mt;
				break;
			}
		}
		Loggable loggable = method.getAnnotation(Loggable.class);// 自定義註解
		if (loggable == null) {
			return;
		}
		String desc = loggable.describe();// 描述
		String optType = loggable.optType();// 方法名
		String module = loggable.module();// 日誌模組
		if ("SELECT".equals(optType)) {// 選擇
			selectLog(params, desc, module);
		} else if ("UPDATE".equals(optType)) {// 更新
			updateLog(params, desc, module);
		} else if ("INSERT".equals(optType)) {// 插入
			insert(params, desc, module);
		} else if ("DELETE".equals(optType)) {// 刪除
			delete(params, desc, module);
		}
	}

	/**
	 * @func 刪除操作的日誌
	 * @date 2016/11/17
	 * @param params
	 * @param desc
	 * @param module
	 */
	private void delete(Object[] params, String desc, String module) {

	}

	/**
	 * @func 插入操作的日誌
	 * @date 2016/11/17
	 * @param params
	 * @param desc
	 * @param module
	 */
	private void insert(Object[] params, String desc, String module) {

	}

	/**
	 * @func 更新操作的日誌
	 * @date 2016/11/17
	 * @param params
	 * @param desc
	 * @param module
	 * @throws BizException 
	 */
	private void updateLog(Object[] params, String desc, String module) throws BizException {
		if("room_log".equals(module)){  //房間日誌表
			RoomKeep room=(RoomKeep) params[0];
			RoomLog roomLog=new RoomLog();
			roomLog.setRoomno(room.getRoomno());
			roomLog.setOptTime(new Date());
			roomLog.setSysTime(this.hotelInfoDao.getSysTime("00001"));
			roomLog.setMemo(room.getMemo());
			roomLog.setOptMan(room.getTheMan());
			roomLog.setSubstoreid(room.getSubstoreid());
			roomLog.setContent(desc);
			roomLog.setLogType("UPDATE");
			roomLog.setOptType(room.getMemo());
			roomLog.setSource("客戶端");
			this.logDao.insertRoomLog(roomLog);
		}
	}

	/**
	 * @func 查詢操作的日誌
	 * @date 2016/11/17
	 * @param params
	 * @param desc
	 * @param module
	 */
	private void selectLog(Object[] params, String desc, String module) {

	}

}

8.使用自定義日誌註解記錄日誌

Java程式碼:
/**
	 * @func 把指定房間置成保留房
	 * @date 2016/11/16
	 * @param roomKeep
	 * @return boolean
	 * @throws BizException
	 */
	@Loggable(describe = "把指定房間置成保留房", optType = LogOptType.UPDATE, module = LogModule.ROOMLOG)
	@Transactional(readOnly = false, rollbackFor = BizException.class)
	@Override
	public boolean updateToKeepRoom(RoomKeep r) throws BizException {
		// 1.更新房態
		int num1 = this.roomStatusDao.updateRoomStatus(r.getSubstoreid(),
				r.getRoomno(), r.getOldRoomStatus(), r.getNewRoomStatus());
		if (num1 > 0) {
			r.setFinishFlag("0");// 保留未完成
			int num2 = this.roomKeepDao.saveToKeepRoom(r);
			if (num2 > 0) {
				return true;
			}
		}
		return false;
	}

9.日誌記錄表的內容如下




至此,整個過程完成!!!