今天在再次深入学习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 SQLException { 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("失败");// 设置操作结果 } finally { // 4.相当于最终通知 logtable.setOperatedate(new Date());// 设置操作日期 logtableService.addLog(logtable);// 添加日志记录 } return result; } }
通过拦截带有 cn.xm.exam.annotation.LogAnno 注解的方法,根据参数获取到方法,然后获取方法的LogAnno注解,获取注解的属性,在方法执行前后对其进行处理,实现AOP功能。
如果需要获取IP地址可以用如下方法:
/** * 获取IP地址的方法 * @param request 传一个request对象下来 * @return */ public static String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; }
8.测试:
在页面上添加一个字典之后打断点进行查看:
- 会话中当前登录的用户信息:
- 当前日志实体类的信息
- 查看数据库:
mysql> select * from logtable\G *************************** 1. row *************************** id: 1 operateor: 超级管理员 operateType: 添加了一个字典项 operateDate: 2018-04-08 20:46:19 operateResult: 正常 remark: NULL
到这里基于注解AOP+注解实现日志记录基本实现了。
9.现在模拟在Service中抛出错误的测试:
1.修改ServiceIMpl模拟制造一个除零异常
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; /** * 字典表的实现类 * * */ @Service public class DictionaryServiceImpl implements DictionaryService { @Resource private DictionaryMapper dictionaryMapper;/** * 1、添加字典信息 */ @LogAnno(operateType = "添加了一个字典项") @Override public boolean addDictionary(Dictionary dictionary) throws SQLException { int i=1/0; int result = dictionaryMapper.insert(dictionary); if (result > 0) { return true; } else { return false; } } }
2.修改切面(主要是修改捕捉异常,除零异常不是SQLException,所有修改,实际项目中视情况而定)
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 (Exception e) { // 3.相当于异常通知部分 logtable.setOperateresult("失败");// 设置操作结果 } finally { // 4.相当于最终通知 logtable.setOperatedate(new Date());// 设置操作日期 logtableService.addLog(logtable);// 添加日志记录 } return result; } }
3.结果:
mysql> select * from logtable\G *************************** 1. row *************************** id: 3 operateor: 超级管理员 operateType: 添加了一个字典项 operateDate: 2018-04-08 21:53:53 operateResult: 失败 remark: NULL 1 row in set (0.00 sec)
补充:在Spring+SpringMVC+Mybatis的框架中使用的时候,需要注解扫描包的配置以及spring代理方式的配置
<!-- 6.开启注解AOP (前提是引入aop命名空间和相关jar包) --> <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"></aop:aspectj-autoproxy> <!-- 7.开启aop,对类代理强制使用cglib代理 --> <aop:config proxy-target-class="true"></aop:config> <!-- 8.扫描 @Service @Component 注解--> <context:component-scan base-package="cn.xm.jwxt" > <!-- 不扫描 @Controller的类 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan>
解释: 6配置是开启注解aop,且暴露cglib代理对象,对cglib代理对象进行aop拦截
7配置是强制spring使用cglib代理
8是配置扫描的包。且不扫描@Controller 注解,如果需要配置扫描的注解可以:
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
注意:我在使用Spring+SpringMVc+Mybatis的过程中发现注解AOP没反应,最后发现编译只会找不到自己的Aspect类。。。。。。。。
最后:需要注意的是我在尝试本实例方法调用本实例方法的时候发现被调用的方法上的注解无效。因此我在另一个类中写了一个标记方法并打上注解才拦截到注解。
例如:我希望登录成功之后记录登录信息,在登录成功之后我调用service的一个标记方法即可以使注解生效。
@MyLogAnnotation(operateDescription = "成功登录系统") @Override public void logSuccess(){ }
补充:关于在Service层和Controller层进行Aop拦截的配置 (如果不生效需要注意配置的配置以及扫描的位置)
一般我们将扫描@Service写在applicationContext.xml。因此在applicationContext.xml配置的AOP自动代理对@Service层的注解有效,如果我们需要在Controller层实现注解AOP,我们需要将AOP注解配置在SpringMVC.xml也写一份,在SpringMVC.xml中只是扫描@Controller注解
- Spring配置文件applicationContext.xml配置
<!-- 6.开启注解AOP (前提是引入aop命名空间和相关jar包) --> <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"></aop:aspectj-autoproxy> <!-- 7.开启aop,对类代理强制使用cglib代理 --> <aop:config proxy-target-class="true"></aop:config> <!-- 8.扫描 @Service @Component 注解--> <context:component-scan base-package="cn.xm.jwxt" > <!-- 不扫描 @Controller的类 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan>
- SpringMVC的配置文件SpringMVC.xml
<!--1.扫描controller--> <context:component-scan base-package="cn.xm.jwxt.controller" /> <!-- 2.开启aop,对类代理强制使用cglib代理 --> <aop:config proxy-target-class="true"/> <!-- 3开启注解AOP (前提是引入aop命名空间和相关jar包) 暴露代理类--> <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"/>
最后给几个链接,不明白上面的可以参考:
注解的使用: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