在软件业,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.创建日志记录model、dao、Mapper,代码及配置如下
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.日志记录表的内容如下
至此,整个过程完成!!!