近期,一個小夥伴遇到了此需求。要解決的問題就是:
記錄用戶在系統的操作,通過註解來靈活控制。 註解可以對方法進行修飾,描述。 後面會將註解上描述以及方法被調用時入參記錄到數據庫。 同時還需要對不同的操作進行分類(插入,修改,查看,下載/上傳文件之類的),記錄用戶,時間以及IP,客戶端User-agent . 我在這裏將部分實現寫了出來,實際在項目中可以直接參照進行修改就可以滿足以上功能。
開發環境:W7 + Tomcat7 + jdk1.7 + Mysql5
框架:spring,springmvc,hibernate
於是乎,下班後動手寫了個小demo,主要使用註解實現,思路如下:
1.打算在service 層切入,所以在springmvc配置文件中排除對service層的掃描
2.在spring配置文件中掃描沒有被springmvc掃描的service層,aop對其增強
3.實現註解,註解要能滿足記錄【方法描述,參數描述,操作類型】等等
4.對攔截到的方法進行統一處理,持久化日誌
代碼結構圖:
1.springmvc-servlet.xml配置
<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd"
default-autowire="byName" >
<!-- 掃描的時候過濾掉Service層,aop要在service進行切入! -->
<strong><span style="color:#006600;"><context:component-scan base-package="com.billstudy.springaop">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
<context:component-scan base-package="com.buyantech.log">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan></span></strong>
<bean class="cn.org.rapid_framework.spring.web.servlet.mvc.support.ControllerClassNameHandlerMapping" >
<!-- <property name="caseSensitive" value="true"/> -->
<!-- 前綴可選 -->
<property name="pathPrefix" value="/"></property>
<!-- 攔截器註冊 -->
<property name="interceptors">
<bean class="javacommon.springmvc.interceptor.SharedRenderVariableInterceptor"/>
</property>
</bean>
<!-- Default ViewResolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/pages"/>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
2.spring配置文件中掃描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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd" default-autowire="byName" default-lazy-init="false">
<strong><span style="color:#009900;"><context:component-scan base-package="com.**.service" /></span></strong>
</beans>
3.Aop攔截處理,以及註解實現部分
/**
* 文件名:Operation.java
* 版權:Copyright 2014-2015 BuyanTech.All Rights Reserved.
* 描述:
* 修改人:Bill
* 修改時間:2014/11/03
* 修改內容: 無
*/
package com.billstudy.springaop.log.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.billstudy.springaop.log.enums.OperationType;
/**
* @Descrption該註解描述方法的操作類型和方法的參數意義
*/
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface Operation {
/**
* @Description描述操作類型,參見{@linkOperationType ,爲必填項
*/
OperationType type();
/**
* @Description描述操作意義,比如申報通過或者不通過等
*/
String desc() default "";
/**
* @Description描述操作方法的參數意義,數組長度需與參數長度一致,否則無效
*/
String[] arguDesc() default {};
}
package com.billstudy.springaop.log.aspect;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import com.billstudy.springaop.log.annotation.Operation;
import com.billstudy.springaop.log.operationlog.model.Operationlog;
import com.billstudy.springaop.log.operationlog.service.OperationlogManager;
/**
* 使用註解,aop 實現日誌的打印以及保存至數據庫。
* @author Bill
* @since V.10 2015年4月16日 - 下午8:40:20
*/
@Aspect
public class OperationLogAspect {
@Autowired
private OperationlogManager operationlogManager;
@Autowired
private HttpServletRequest request;
private static final Logger logger = Logger.getLogger(OperationLogAspect.class);
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Pointcut("@annotation(com.billstudy.springaop.log.annotation.Operation)")
public void anyMethod() {}
@Around("anyMethod()")
public Object doBasicProfiling(ProceedingJoinPoint jp)throws Throwable{
System.err.println("doBasicProfiling...");
// 獲取簽名
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
// 記錄日誌
Operation annotation = method.getAnnotation(Operation.class);
// 解析參數
Object[] objParam = jp.getArgs();
String[] arguDesc = annotation.arguDesc();
Object result = null;
if(objParam.length == arguDesc.length){
// 抽取出方法描述:
String paramDesc = extractParam(objParam,arguDesc);
System.out.println(paramDesc);
// 記錄時間
Operationlog log = new Operationlog();
Date sDate = Calendar.getInstance().getTime();
String requestStartDesc = "執行開始時間爲:"+SIMPLE_DATE_FORMAT.format(sDate)+"";
logger.info(requestStartDesc);
System.out.println("進入方法前");
result = jp.proceed();
System.out.println("進入方法後");
Date eDate = Calendar.getInstance().getTime();
long time = eDate.getTime()-sDate.getTime();
String requestEndDesc = "執行完成時間爲:"+SIMPLE_DATE_FORMAT.format(eDate)+",本次用時:"+time+"毫秒!";
logger.info("執行完成時間爲:"+SIMPLE_DATE_FORMAT.format(eDate)+",本次用時:"+time+"毫秒!");
log.setLogCreateTime(sDate);
log.setLogDesc(annotation.desc()+" 用時/"+requestStartDesc + "," + requestEndDesc);
log.setLogResult(result+"");
log.setLogType(annotation.type()+"");
log.setLogParam(paramDesc);
operationlogManager.save(log);
logger.info(log.toJsonString());
}else{
result = jp.proceed();
String methodName = signature.getName();
String className = jp.getThis().getClass().getName();
className = className.substring(0, className.indexOf("$$")); // 截取掉cglib代理類標誌
String errorMsg = "警告:"+methodName+" 方法記錄日誌失敗,註解[arguDesc]參數長度與方法實際長度不一致,需要參數"+objParam.length+"個,實際爲"+arguDesc.length+"個,請檢查"+className+":"+methodName+"註解!";
logger.warn(errorMsg);
System.err.println(errorMsg);
}
return result;
}
/**
* 根據註解參數以及方法實參拼接出方法描述
* @param objParam
* @param arguDesc
* @return
*/
private String extractParam(Object[] objParam, String[] arguDesc) {
StringBuilder paramSb = new StringBuilder();
int size = objParam.length-1;
for (int i = 0; i < arguDesc.length; i++) {
paramSb.append(arguDesc[i]+":"+objParam[i]+(i==size?"":","));
}
return paramSb.toString();
}
}
/**
* 文件名:OperationType.java
* 版權:Copyright 2014-2015 BuyanTech.All Rights Reserved.
* 描述:
* 修改人:Bill
* 修改時間:2014/11/03
* 修改內容: 無
*/
package com.billstudy.springaop.log.enums;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public enum OperationType {
/**
* 新增,添加
*/
ADD("新增"),
/**
* 修改,更新
*/
UPDATE("修改"),
/**
* 刪除
*/
DELETE("刪除"),
/**
* 下載
*/
DOWNLOAD("下載"),
/**
* 查詢
*/
QUERY("查詢"),
/**
* 登入
*/
LOGIN("登入"),
/**
* 登出
*/
LOGOUT("登出");
private String name;
private OperationType() {
}
public String getName() {
return name;
}
private OperationType(String name) {
this.name = name;
}
/**
* 獲取所有的枚舉集合
* @return
*/
public static List<OperationType> getOperationTypes() {
return new ArrayList<OperationType>(Arrays.asList(OperationType
.values()));
}
public static void main(String[] args) {
System.out.println(Arrays.toString(OperationType.values()));
}
}
Person/OperationLog類以及數據庫腳本:
/**
* 文件名:Operationlog.java
* 版權:Copyright 2014-2015 BuyanTech.All Rights Reserved.
* 描述:
* 修改人:Bill
* 修改時間:2014/11/03
* 修改內容: 無
*/package com.billstudy.springaop.log.operationlog.model;
import javacommon.base.BaseEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.validator.constraints.Length;
import cn.org.rapid_framework.util.DateConvertUtils;
/**
* @author Bill
* @version 1.0
* @date 2014
*/
@Entity
@Table(name = "operationlog")
public class Operationlog extends BaseEntity implements java.io.Serializable {
private static final long serialVersionUID = 5454155825314635342L;
// alias
public static final String TABLE_ALIAS = "系統日誌";
public static final String ALIAS_LOG_ID = "日誌主鍵";
public static final String ALIAS_LOG_USER_ID = "用戶編號";
public static final String ALIAS_LOG_USER_NAME = "用戶名";
public static final String ALIAS_LOG_IP = "用戶IP";
public static final String ALIAS_LOG_PARAM = "操作參數";
public static final String ALIAS_LOG_DESC = "操作描述";
public static final String ALIAS_LOG_CREATE_TIME = "操作日期";
public static final String ALIAS_LOG_LOGTYPE = "操作類型";
public static final String ALIAS_LOG_RESULT = "執行結果";
// date formats
public static final String FORMAT_LOG_CREATE_TIME = DATE_TIME_FORMAT;
// 可以直接使用: @Length(max=50,message="用戶名長度不能大於50")顯示錯誤消息
// columns START
/**
* 日誌主鍵 db_column: logId
*/
// private String _id;
private java.lang.Integer logId;
/**
* 用戶編號 db_column: logUserId
*/
@Length(max = 255)
private java.lang.String logUserId;
/**
* 用戶名 db_column: logUserName
*/
@Length(max = 255)
private java.lang.String logUserName;
/**
* 用戶IP db_column: logIp
*/
@Length(max = 255)
private java.lang.String logIp;
/**
* 操作參數 db_column: logParam
*/
@Length(max = 255)
private java.lang.String logParam;
/**
* 操作描述 db_column: logDesc
*/
@Length(max = 255)
private java.lang.String logDesc;
/**
* 操作日期 db_column: logCreateTime
*/
private String logType;
private java.util.Date logCreateTime;
private String logResult;
// private
// columns END
public Operationlog() {
}
/*
* public Operationlog( java.lang.Integer logId ){ this.logId = logId; }
*
*
*
* public void setLogId(java.lang.Integer value) { this.logId = value; }
*/
/*
* @Id
*
* @GeneratedValue(generator = "uuid-id")
*
* @GenericGenerator(name = "uuid-id", strategy = "uuid")
*
* @Column(name = "_id", unique = true, nullable = false, insertable = true,
* updatable = true, length = 128) public String get_id() { return _id; }
*
* public void set_id(String _id) { this._id = _id; }
*/
@Id
@GeneratedValue(generator = "paymentableGenerator")
@GenericGenerator(name = "paymentableGenerator", strategy = "increment")
@Column(name = "logId", unique = true, nullable = false, insertable = true, updatable = true, length = 10)
public java.lang.Integer getLogId() {
return this.logId;
}
public String getLogType() {
return logType;
}
public void setLogId(java.lang.Integer logId) {
this.logId = logId;
}
public void setLogType(String logType) {
this.logType = logType;
}
@Column(name = "logUserId", unique = false, nullable = true, insertable = true, updatable = true, length = 255)
public java.lang.String getLogUserId() {
return this.logUserId;
}
public void setLogUserId(java.lang.String value) {
this.logUserId = value;
}
@Column(name = "logUserName", unique = false, nullable = true, insertable = true, updatable = true, length = 255)
public java.lang.String getLogUserName() {
return this.logUserName;
}
public void setLogUserName(java.lang.String value) {
this.logUserName = value;
}
@Column(name = "logIp", unique = false, nullable = true, insertable = true, updatable = true, length = 255)
public java.lang.String getLogIp() {
return this.logIp;
}
public void setLogIp(java.lang.String value) {
this.logIp = value;
}
@Column(name = "logParam", unique = false, nullable = true, insertable = true, updatable = true, length = 65535)
public java.lang.String getLogParam() {
return this.logParam;
}
public void setLogParam(java.lang.String value) {
this.logParam = value;
}
@Column(name = "logDesc", unique = false, nullable = true, insertable = true, updatable = true, length = 65535)
public java.lang.String getLogDesc() {
return this.logDesc;
}
public void setLogDesc(java.lang.String value) {
this.logDesc = value;
}
@Transient
public String getLogCreateTimeString() {
return DateConvertUtils.format(getLogCreateTime(),
FORMAT_LOG_CREATE_TIME);
}
public void setLogCreateTimeString(String value) {
setLogCreateTime(DateConvertUtils.parse(value, FORMAT_LOG_CREATE_TIME,
java.util.Date.class));
}
@Column(name = "logCreateTime", unique = false, nullable = true, insertable = true, updatable = true, length = 0)
public java.util.Date getLogCreateTime() {
return this.logCreateTime;
}
public void setLogCreateTime(java.util.Date value) {
this.logCreateTime = value;
}
@Column(name = "logResult", unique = false, nullable = true, insertable = true, updatable = true, length = 65535)
public String getLogResult() {
return logResult;
}
public void setLogResult(String logResult) {
this.logResult = logResult;
}
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("LogId",getLogId())
.append("LogUserId", getLogUserId())
.append("LogUserName", getLogUserName())
.append("LogIp", getLogIp()).append("LogParam", getLogParam())
.append("LogDesc", getLogDesc())
.append("LogDesc", getLogResult())
.append("LogCreateTime", getLogCreateTime()).toString();
}
public String toJsonString() {
return new StringBuilder("{")
.append("\"logId\":\"").append(getLogId()+"\",")
.append("\"logUserId\":\"").append(getLogUserId() + "\",")
.append("\"logUserName\":\"").append(getLogUserName() + "\",")
.append("\"logIp\":\"").append(getLogIp() + "\",")
.append("\"logParam\":\"").append(getLogParam() + "\",")
.append("\"logDesc\":\"").append(getLogDesc() + "\",")
.append("\"logCreateTime\":\"")
.append(getLogCreateTime() + "\",").append("}").toString();
}
public int hashCode() {
return new HashCodeBuilder()
// .append(getLogId())
.append(getLogId()).toHashCode();
}
public boolean equals(Object obj) {
if (obj instanceof Operationlog == false)
return false;
if (this == obj)
return true;
Operationlog other = (Operationlog) obj;
return new EqualsBuilder()
// .append(getLogId(),other.getLogId())
.append(getLogId(), other.getLogId()).isEquals();
}
}
package com.billstudy.springaop.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javacommon.base.BaseEntity;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.validator.constraints.Length;
/**
* Person model
*
* @author Bill
* @since V.10 2015年4月16日 - 下午7:57:22
*/
@Entity
@Table(name = "person")
public class Person extends BaseEntity implements Serializable {
private static final long serialVersionUID = 7340067893523769892L;
@Length(max = 255)
private int id;
@Length(max = 255)
private String name;
@Length(max = 255)
private Integer age;
@Length(max = 255)
private String address;
@Id
@GeneratedValue(generator = "paymentableGenerator")
@GenericGenerator(name = "paymentableGenerator", strategy = "increment")
@Column(name = "id", unique = true, nullable = false, insertable = true, updatable = true, length = 10)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Column(name = "name", unique = false, nullable = true, insertable = true, updatable = true, length = 255)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Column(name = "age", unique = false, nullable = true, insertable = true, updatable = true, length = 255)
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Column(name = "address", unique = false, nullable = true, insertable = true, updatable = true, length = 255)
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", age=" + age
+ ", address=" + address + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((address == null) ? 0 : address.hashCode());
result = prime * result + ((age == null) ? 0 : age.hashCode());
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (address == null) {
if (other.address != null)
return false;
} else if (!address.equals(other.address))
return false;
if (age == null) {
if (other.age != null)
return false;
} else if (!age.equals(other.age))
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public Person(String name, Integer age, String address) {
super();
this.name = name;
this.age = age;
this.address = address;
}
public Person() {
// TODO Auto-generated constructor stub
}
}
CREATE TABLE `person` (
`id` int(255) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
`age` int(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
CREATE TABLE `operationlog` (
`logId` int(11) NOT NULL AUTO_INCREMENT COMMENT '日誌主鍵',
`logUserId` varchar(255) DEFAULT NULL COMMENT '用戶編號',
`logUserName` varchar(255) DEFAULT NULL COMMENT '用戶名',
`logIp` varchar(255) DEFAULT NULL COMMENT '用戶IP',
`logParam` text COMMENT '操作參數',
`logDesc` text COMMENT '操作描述',
`logResult` text COMMENT '操作結果',
`logType` varchar(255) DEFAULT NULL COMMENT '操作類型',
`logCreateTime` datetime DEFAULT NULL COMMENT '操作日期',
PRIMARY KEY (`logId`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
最後再配置下切面處理類,因爲該類部分屬性需要從Spring中獲取。
<?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:p="http://www.springframework.org/schema/p"
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">
<!-- 配置日誌文件類 -->
<bean id="operationLogAspect" class="com.billstudy.springaop.log.aspect.OperationLogAspect"></bean>
</beans>
我在PersonController裏面寫了2個方法用做測試,insert / find
package com.billstudy.springaop.controller;
import java.io.IOException;
import javacommon.base.BaseSpringController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.billstudy.springaop.log.enums.OperationType;
import com.billstudy.springaop.model.Person;
import com.billstudy.springaop.service.PersonManager;
/**
* AOP Controller
* @author Bill
* @since V.10 2015年4月16日 - 下午7:52:11
*/
@Controller
public class PersonController extends BaseSpringController{
@Autowired
private PersonManager personManager;
/**
* Test insert
* @since V.10 2015年4月16日 - 下午7:53:18
* @param request
* @param response
* @throws IOException
*/
public void insert(HttpServletRequest request,HttpServletResponse response) throws IOException{
/**
* 執行描述:
* 方法執行到這裏時,不會立即進入到save方法
* 而是會進入到 com.billstudy.springaop.log.aspect.OperationLogAspect.doBasicProfiling(ProceedingJoinPoint)
* 進行預處理,然後調用proceed方法時纔會進入
**/
// 對應的註解:arguDesc={"person","姓名","年齡"},type=OperationType.ADD,desc="保存"
personManager.save(new Person("飛機",10,"上海"),"念念",200);
System.out.println("insert...");
response.getWriter().print("success");
}
/**
* Test find
* @since V.10 2015年4月16日 - 下午7:53:18
* @param request
* @param response
* @throws IOException
*/
public void find(HttpServletRequest request,HttpServletResponse response) throws IOException{
// 對應的註解 :arguDesc={"用戶編號"},type=OperationType.QUERY,desc="查詢"
Person person = personManager.findById(Integer.parseInt(request.getParameter("id")));
System.out.println("find...");
response.setContentType("text/html;charset=UTF-8");
response.getWriter().print(person.toString());
}
}
開啓測試模式:
1.直接請求insert方法:
瀏覽器request to :http://bill/SpringAopLogDemo/Person/insert.do
好了,下面放開斷點了。 看看控制檯輸出,以及數據庫日誌記錄.
控制檯:
數據庫:
好了,問題到這裏就差不多了。 關於AOP 相關理論,請自行查閱文檔學習噢。 這裏不描述了,忙着搞別的去了。 哈哈。
本文所有代碼:點擊下載SpringAopDemo項目