SpringAOP+注解实现简单的日志管理
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
SpringAOP+注解实现简单的⽇志管理
今天在再次深⼊学习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使⽤⽅法参考;)
下⾯是具体实现:
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=3DEFAULT CHARSET=utf8
简单的记录操作了操作⼈,操作的类型,操作的⽇期,操作的结果。
如果想详细的记录,可以将操作的类名与操作的⽅法名以及参数信息也新进⽇志,在环绕通知中利⽤反射原理即可获取这些参数(参考我的另⼀篇博客:)。
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 ng.annotation.ElementType;
import ng.annotation.Retention;
import ng.annotation.RetentionPolicy;
import ng.annotation.Target;
/**
* ⽇志注解
*
* @author liqiang
*
*/
@Target(ElementType.METHOD) // ⽅法注解
@Retention(RetentionPolicy.RUNTIME) // 运⾏时可见
public @interface LogAnno {
String operateType();// 记录⽇志的操作类型
}
6.在需要⽇志记录的⽅法中使⽤注解:(此处将注解写在DictionaryServiceImpl⽅法上)package mon;
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 mon.Dictionary;
import mon.DictionaryExample;
import mon.DictionaryMapper;
import mon.custom.DictionaryCustomMapper;
import mon.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 ng.reflect.Method;
import java.sql.SQLException;
import java.util.Date;
import org.apache.struts2.ServletActionContext;
import ng.ProceedingJoinPoint;
import ng.annotation.Around;
import ng.annotation.Aspect;
import ng.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import ponent;
import cn.xm.exam.annotation.LogAnno;
import cn.xm.exam.bean.log.Logtable;
import er;
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-0820:46:19
operateResult: 正常
remark: NULL
到这⾥基于注解AOP+注解实现⽇志记录基本实现了。
9.现在模拟在Service中抛出错误的测试:
1.修改ServiceIMpl模拟制造⼀个除零异常
package mon;
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 mon.Dictionary;
import mon.DictionaryExample;
import mon.DictionaryMapper;
import mon.custom.DictionaryCustomMapper;
import mon.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 ng.reflect.Method;
import java.sql.SQLException;
import java.util.Date;
import org.apache.struts2.ServletActionContext;
import ng.ProceedingJoinPoint;
import ng.annotation.Around;
import ng.annotation.Aspect;
import ng.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import ponent;
import cn.xm.exam.annotation.LogAnno;
import cn.xm.exam.bean.log.Logtable;
import er;
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-0821: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类。
最后:需要注意的是我在尝试本实例⽅法调⽤本实例⽅法的时候发现被调⽤的⽅法上的注解⽆效(因为本实例⽅法调⽤本实例⽅法不会⾛代理,所以不会⾛AOP)。
因此我在另⼀个类中写了⼀个标记⽅法并打上注解才拦截到注解。
例如:我希望登录成功之后记录登录信息,在登录成功之后我调⽤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"/>
最后给⼏个链接,不明⽩上⾯的可以参考:
注解的使⽤:
Spring中获取request和session对象:
SpringAOP的使⽤⽅法:
Spring AOP⽆法拦截内部⽅法调⽤原因:
补充:上⾯实际⽤的是AspectJ,可以看到引⽤包的好多地⽅也⽤到了AspectJ的相关东西
aop是⼀种思想⽽不是⼀种技术。
所以说,如果抛开spring,动态代理甚⾄静态代理都可以算是⼀种aop。
spring中的aop实现分为两种,基于动态代理的aop和基于AspectJ的aop。
AspectJ是完全独⽴于Spring存在的⼀个Eclipse发起的项⽬。
AspectJ甚⾄可以说是⼀门独⽴的语⾔,在java⽂件编译期间,织⼊字节码,改变原有的类。
我们常看到的在spring中⽤的@Aspect注解只不过是Spring2.0以后使⽤了AspectJ的风格⽽已,本质上还是Spring的原⽣实现。
补充:实际在AOP切⼊点JoinPoint 也可以获取到⽅法的参数名称和⽅法的值,如下:
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 1.⽅法执⾏前的处理,相当于前置通知
// 获取⽅法签名
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
// 获取⽅法
Method method = methodSignature.getMethod();
// 获取⽅法上⾯的注解
AopTag logAnno = method.getAnnotation(AopTag.class);
// 获取操作描述的属性值
String operateType = logAnno.desc();
// 获取参数名称
String[] parameterNames = methodSignature.getParameterNames();
// 获取参数值
Object[] args = pjp.getArgs();
// method获取参数信息
Parameter[] parameters = method.getParameters();
...
}。