今天在再次深入学习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

SpringAOP+注解实现简单的日志管理的更多相关文章

  1. SpringAOP拦截Controller,Service实现日志管理(自定义注解的方式)

    转载:http://itindex.net/detail/50710-springaop-controller-service 从业近二,三年了,第一次写博客,平时做做脚手架或者架构一些基础框架然后给 ...

  2. c#: 简单的日志管理类(TextWriterTraceListener)

    以c#实现轻量级的日志管理,着实简单,置一静态类记之: /// <summary> /// 日志管理 /// </summary> public static class Lo ...

  3. SpringAop之日志管理

    导入的依赖均为JavaWeb界面在线配置代码生成器这篇文章,你只需将这篇文章的maven依赖导入即可. SpringAop利用注解的特性进行日志管理,只需在对应的方法上加上自己编写的注解,即可完美实现 ...

  4. SQL Server中的事务日志管理(4/9):简单恢复模式里的日志管理

    当一切正常时,没有必要特别留意什么是事务日志,它是如何工作的.你只要确保每个数据库都有正确的备份.当出现问题时,事务日志的理解对于采取修正操作是重要的,尤其在需要紧急恢复数据库到指定点时.这系列文章会 ...

  5. SpringAop进行日志管理。

    在java开发中日志的管理有非常多种.我通常会使用过滤器,或者是Spring的拦截器进行日志的处理.假设是用过滤器比較简单,仅仅要对全部的.do提交进行拦截,然后获取action的提交路径就能够获取对 ...

  6. LogCook 一个简单实用的Android日志管理工具

    众所周知,日志的管理是软件系统很重要的一部分,千万不可忽略其重要性.完整的日志将会在系统维护中起着异常重要的作用,就好像磨刀不误砍柴工一样,日志就像对系统进行分析的工具,工具便捷了,对系统分析起来就能 ...

  7. spring AOP自定义注解方式实现日志管理

    今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接开始!!! 关于配置我还是的再说一遍. 在appli ...

  8. spring AOP自定义注解 实现日志管理

    今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接开始!!! 关于配置我还是的再说一遍. 在appli ...

  9. Spring AOP 自定义注解实现统一日志管理

    一.AOP的基本概念: AOP,面向切面编程,常用于日志,事务,权限等业务处理.AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容(Spring核心之一),是函数式编程 ...

随机推荐

  1. BZOJ4025 二分图(线段树分治+并查集)

    之前学了一下线段树分治,这还是第一次写.思想其实挺好理解,即离线后把一个操作影响到的时间段拆成线段树上的区间,并标记永久化.之后一块处理,对于某个节点表示的时间段,影响到他的就是该节点一直到线段树根的 ...

  2. Web api dynamic

    参考: ajax调用实例     <script>         var user = { name_CN: "haha", pwd: "123" ...

  3. MongoDB安装的坑

    目前最新版本的MongoDB3.6在windows下安装会出现很难解决的问题,所以就换了3.4版本,由于我之前3.6的安装不成功,所以卸载了好几次,其中目录改变了一次,就导致了这次大坑,用了我三四个小 ...

  4. 【BZOJ2246】[SDOI2011]迷宫探险(搜索,动态规划)

    [BZOJ2246][SDOI2011]迷宫探险(搜索,动态规划) 题面 BZOJ 洛谷 题解 乍一看似乎是可以求出每个东西是陷阱的概率,然而会发现前面走过的陷阱是不是陷阱实际上是会对当前状态产生影响 ...

  5. [luogu3834]静态区间第k小【主席树】

    传送门:https://www.luogu.org/problemnew/show/P3834 题目描述 如题,给定N个整数构成的序列,将对于指定的闭区间查询其区间内的第K小值. 分析 很多人都说是用 ...

  6. 添加AD RMS role时,提示密码不能被验证The password could not be validated

    "The password could not be validated" when attempting to provision an AD RMS server. Sympt ...

  7. centos6.5下修改文件夹权限和用户名用户组

    0.说明 Linux系统下经常遇到文件或者文件夹的权限问题,或者是因为文件夹所属的用户问题而没有访问的权限.根据我自己遇到的情况,对这类问题做一个小结. 在命令行使用命令"ll"或 ...

  8. 【loj3043】【zjoi2019】线段树

    题目 描述 ​ 有\(m\)个操作一次发生,每个操作有\(\frac{1}{2}\)的概率被执行 ; ​ 一次操作为线段树([1,n])上的 \(modify(Node,l,r,ql,qr)\) ; ...

  9. 硬盘性能 & 文件碎片的一些思考

    昨天将一台机器上的数据转移(备份)到另一台机器上,花了差不多一个晚上,传输了100G左右的数据. 感觉数据源机器的硬盘越来越不行了,读写性能下降的很历害. 这些年来写软件的时候很少去考虑硬盘的读写性能 ...

  10. javascript高级程序设计第二章知识点提炼

    这是我整理的javascript高级程序设计第二章的脑图,内容也是非常浅显与简单.希望您看了我的博客能够给我一些意见或者建议.