今天在再次深入学习SpringAOP之后想着基于注解的AOP实现日志功能,在面试过程中我们也经常会被问到:假如项目已经上线,如何增加一套日志功能?我们会说使用AOP,AOP也符合开闭原则:对代码的修改禁止的,对代码的扩展是允许的。今天经过自己的实践简单的实现了AOP日志。
在这里我只是简单的记录下当前操作的人、做了什么操作、操作结果是正常还是失败、操作时间,实际项目中,如果我们需要记录的更详细,可以记录当前操作人的详细信息,比如说部门、身份证号等信息,这些信息可以直接从session中获取,也可以从session中获取用户ID之后调用userService从数据库获取。我们还可以记录用户调用了哪个类的哪个方法,我们可以使用JoinPoint参数获取或者利用环绕通知ProceedingJoinPoint去获取。可以精确的定位到类、方法、参数,如果有必要我们就可以记录在日志中,看业务需求和我们的日志表的设计。
实现的大致思路是:
1.前期准备,设计日志表和日志类,编写日志Dao和Service以及实现
2.自定义注解,注解中加入几个属性,属性可以标识操作的类型(方法是做什么的)
3.编写切面,切点表达式使用上面的注解直接定位到使用注解的方法,
4.编写通知,通过定位到方法,获取上面的注解以及注解的属性,然后从session中直接获取或者从数据库获取当前登录用户的信息,最后根据业务处理一些日志信息之后调用日志Service存储日志。
其实日志记录可以针对Controller层进行切入,也可以选择Service层进行切入,我选择的是基于Service层进行日志记录。网上的日志记录由的用前置通知,有的用环绕通知,我选择在环绕通知中完成,环绕通知中可以完成前置、后置、最终、异常通知的所有功能,因此我选择了环绕通知。(关于AOP的通知使用方法以及XML、注解AOP使用方法参考;http://www.cnblogs.com/qlqwjy/p/8729280.html)
下面是具体实现:
1.日志数据库:
CREATE TABLE `logtable` ( `id` int(11) NOT NULL AUTO_INCREMENT, `operateor` varchar(5) DEFAULT NULL, `operateType` varchar(20) DEFAULT NULL, `operateDate` datetime DEFAULT NULL, `operateResult` varchar(4) DEFAULT NULL, `remark` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
简单的记录操作了操作人,操作的类型,操作的日期,操作的结果。如果想详细的记录,可以将操作的类名与操作的方法名以及参数信息也新进日志,在环绕通知中利用反射原理即可获取这些参数(参考我的另一篇博客:http://www.cnblogs.com/qlqwjy/p/8729280.html)。
2.日志实体类:
Logtable.java
package cn.xm.exam.bean.log; import java.util.Date; public class Logtable { private Integer id; private String operateor; private String operatetype; private Date operatedate; private String operateresult; private String remark; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getOperateor() { return operateor; } public void setOperateor(String operateor) { this.operateor = operateor == null ? null : operateor.trim(); } public String getOperatetype() { return operatetype; } public void setOperatetype(String operatetype) { this.operatetype = operatetype == null ? null : operatetype.trim(); } public Date getOperatedate() { return operatedate; } public void setOperatedate(Date operatedate) { this.operatedate = operatedate; } public String getOperateresult() { return operateresult; } public void setOperateresult(String operateresult) { this.operateresult = operateresult == null ? null : operateresult.trim(); } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark == null ? null : remark.trim(); } }
3.日志的Dao层使用的是Mybatis的逆向工程导出的mapper,在这里就不贴出来了
4.日志的Service层和实现类
- LogtableService.java接口
package cn.xm.exam.service.log; import java.sql.SQLException; import cn.xm.exam.bean.log.Logtable; /** * 日志Service * * @author liqiang * */ public interface LogtableService { /** * 增加日志 * @param log * @return * @throws SQLException */ public boolean addLog(Logtable log) throws SQLException; }
- LogtableServiceImpl实现类
package cn.xm.exam.service.impl.log; import java.sql.SQLException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import cn.xm.exam.bean.log.Logtable; import cn.xm.exam.mapper.log.LogtableMapper; import cn.xm.exam.service.log.LogtableService; @Service public class LogtableServiceImpl implements LogtableService { @Autowired private LogtableMapper logtableMapper; @Override public boolean addLog(Logtable log) throws SQLException { return logtableMapper.insert(log) > 0 ? true : false; } }
5.自定义注解:
package cn.xm.exam.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 日志注解 * * @author liqiang * */ @Target(ElementType.METHOD) // 方法注解 @Retention(RetentionPolicy.RUNTIME) // 运行时可见 public @interface LogAnno { String operateType();// 记录日志的操作类型 }
6.在需要日志记录的方法中使用注解:(此处将注解写在DictionaryServiceImpl方法上)
package cn.xm.exam.service.impl.common; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.annotation.Resource; import org.springframework.stereotype.Service; import cn.xm.exam.annotation.LogAnno; import cn.xm.exam.bean.common.Dictionary; import cn.xm.exam.bean.common.DictionaryExample; import cn.xm.exam.mapper.common.DictionaryMapper; import cn.xm.exam.mapper.common.custom.DictionaryCustomMapper; import cn.xm.exam.service.common.DictionaryService; /** * 字典表的实现类 * * @author * */ @Service public class DictionaryServiceImpl implements DictionaryService { @Resource private DictionaryMapper dictionaryMapper;/** * 1、添加字典信息 */ @LogAnno(operateType = "添加了一个字典项") @Override public boolean addDictionary(Dictionary dictionary) throws Exception { int result = dictionaryMapper.insert(dictionary); if (result > 0) { return true; } else { return false; } } }
7.编写通知,切入到切点形成切面(注解AOP实现,环绕通知记录日志。)
注意:此处是注解AOP,因此在spring配置文件中开启注解AOP
<!-- 1.开启注解AOP --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
LogAopAspect.java
package cn.xm.exam.aop; import java.lang.reflect.Method; import java.sql.SQLException; import java.util.Date; import org.apache.struts2.ServletActionContext; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import cn.xm.exam.annotation.LogAnno; import cn.xm.exam.bean.log.Logtable; import cn.xm.exam.bean.system.User; import cn.xm.exam.service.log.LogtableService; /** * AOP实现日志 * * @author liqiang * */ @Component @Aspect public class LogAopAspect { @Autowired private LogtableService logtableService;// 日志Service /** * 环绕通知记录日志通过注解匹配到需要增加日志功能的方法 * * @param pjp * @return * @throws Throwable */ @Around("@annotation(cn.xm.exam.annotation.LogAnno)") public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable { // 1.方法执行前的处理,相当于前置通知 // 获取方法签名 MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); // 获取方法 Method method = methodSignature.getMethod(); // 获取方法上面的注解 LogAnno logAnno = method.getAnnotation(LogAnno.class); // 获取操作描述的属性值 String operateType = logAnno.operateType(); // 创建一个日志对象(准备记录日志) Logtable logtable = new Logtable(); logtable.setOperatetype(operateType);// 操作说明 // 整合了Struts,所有用这种方式获取session中属性(亲测有效) User user = (User) ServletActionContext.getRequest().getSession().getAttribute("userinfo");//获取session中的user对象进而获取操作人名字 logtable.setOperateor(user.getUsername());// 设置操作人 Object result = null; try { //让代理方法执行 result = pjp.proceed(); // 2.相当于后置通知(方法成功执行之后走这里) logtable.setOperateresult("正常");// 设置操作结果 } catch (SQLException e) { // 3.相当于异常通知部分 logtable.setOperateresult("失败");// 设置操作结果 throw e; } finally { // 4.相当于最终通知 logtable.setOperatedate(new Date());// 设置操作日期 logtableService.addLog(logtable);// 添加日志记录 } return result; } }
通过拦截带有 cn.xm.exam.annotation.LogAnno 注解的方法,根据参数获取到方法,然后获取方法的LogAnno注解,获取注解的属性,在方法执行前后对其进行处理,实现AOP功能。
8.测试:
在页面上添加一个字典之后打断点进行查看:
- 会话中当前登录的用户信息:
- 当前日志实体类的信息
- 查看数据库:
mysql> select * from logtable\G *************************** 1. row *************************** id: 1 operateor: 超级管理员 operateType: 添加了一个字典项 operateDate: 2018-04-08 20:46:19 operateResult: 正常 remark: NULL
到这里基于注解AOP+注解实现日志记录基本实现了。
最后给几个链接,不明白上面的可以参考:
注解的使用:http://www.cnblogs.com/qlqwjy/p/7139068.html
Spring中获取request和session对象:http://www.cnblogs.com/qlqwjy/p/8747136.html
SpringAOP的使用方法:http://www.cnblogs.com/qlqwjy/p/8729280.html
原文地址:https://www.cnblogs.com/qlqwjy/p/8747476.html