今天在再次深入学习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. Js更改样式导致hover效果消失

    [问题来源] 今天做单次倒计时,利用JS更改了button样式之后,再次点击时,发现hover效果消失. 原因: CSS的优先级问题导致 [解决方法] 利用!important提高hover的优先级 ...

  2. vimrc 的配置

    windows syntax on set nocompatible set guifont=Consolas:h17 set linespace=0 color molokai set clipbo ...

  3. 自学Linux Shell1.3-Linux文件系统

    点击返回 自学Linux命令行与Shell脚本之路 1.3-Linux文件系统 文件系统是文件存放在磁盘等存储设备上的组织方法.Linux系统能支持多种目前流行的文件系统,如EXT2. EXT3. F ...

  4. 小trick总结

    一个圆上的整点数量不会很多.(Cf AIM TR 5 F) 二分图完美匹配求字典序最小的方案:先一遍匈牙利求出任意一组完美匹配.再跑一遍逐位确定,要求不能修改编号比它小的匹配.(LG 4100) 如果 ...

  5. numpy 从入门到遗忘

    不常用的函数总是遗忘,很是困扰啊.于是痛下时间,做一个系统的总结,纯原创,都是些实际项目中常用的函数和方法,当然还有一些这边也是没有记录的,因为我在实际数据处理过程中也没有遇到过(如字符串处理等等). ...

  6. Python文件和异常

    程序和运行时数据是在内存中驻留的,涉及到数据交换的地方,通常是磁盘.网络等,因此需要IO接口. IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单 ...

  7. Logstash解析Json array

    logstash解析json数组是一种常见的需求,我以网上一组数据为例来描述 我们的数据test.json内容如下:(此处我linux上的json文本需要是compact的) {"type& ...

  8. 洛谷P2680 运输计划

    大概就是二分+树上差分... 题意:给你树上m条路径,你要把一条边权变为0,使最长的路径最短. 最大的最小,看出二分(事实上我并没有看出来...) 然后二分k,对于所有大于k的边,树上差分求出最长公共 ...

  9. JAVA:当数据库重启后连接池没有自动识别的解决办法

    今天发现服务器上的一个服务程序出现问题,软件抛出:Connection reset by peer: socket write error 无法正常提供服务,找了一下原因,原来是因为数据库服务器重启, ...

  10. mfc editline 变为大框框

    属性: