近期,一个小伙伴遇到了此需求。要解决的问题就是:
记录用户在系统的操作,通过注解来灵活控制。 注解可以对方法进行修饰,描述。 后面会将注解上描述以及方法被调用时入参记录到数据库。 同时还需要对不同的操作进行分类(插入,修改,查看,下载/上传文件之类的),记录用户,时间以及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项目