SpringAOP+注解实现简单的日志管理

  

  今天在再次深刻学习SpringAOP以后想着基于注解的AOP实现日志功能,在面试过程当中咱们也常常会被问到:假如项目已经上线,如何增长一套日志功能?咱们会说使用AOP,AOP也符合开闭原则:对代码的修改禁止的,对代码的扩展是容许的。今天通过本身的实践简单的实现了AOP日志。html

  在这里我只是简单的记录下当前操做的人、作了什么操做、操做结果是正常仍是失败、操做时间,实际项目中,若是咱们须要记录的更详细,能够记录当前操做人的详细信息,好比说部门、身份证号等信息,这些信息能够直接从session中获取,也能够从session中获取用户ID以后调用userService从数据库获取。咱们还能够记录用户调用了哪一个类的哪一个方法,咱们可使用JoinPoint参数获取或者利用环绕通知ProceedingJoinPoint去获取。能够精确的定位到类、方法、参数,若是有必要咱们就能够记录在日志中,看业务需求和咱们的日志表的设计。若是再细致的记录日志,咱们能够针对错误再创建一个错误日志表,在发生错误的状况下(异常通知里)记录日志的错误信息。java

 

  实现的大体思路是:mysql

    1.前期准备,设计日志表和日志类,编写日志Dao和Service以及实现面试

    2.自定义注解,注解中加入几个属性,属性能够标识操做的类型(方法是作什么的)spring

    3.编写切面,切点表达式使用上面的注解直接定位到使用注解的方法,sql

    4.编写通知,经过定位到方法,获取上面的注解以及注解的属性,而后从session中直接获取或者从数据库获取当前登陆用户的信息,最后根据业务处理一些日志信息以后调用日志Service存储日志。数据库

  

  其实日志记录能够针对Controller层进行切入,也能够选择Service层进行切入,我选择的是基于Service层进行日志记录。网上的日志记录由的用前置通知,有的用环绕通知,我选择在环绕通知中完成,环绕通知中能够完成前置、后置、最终、异常通知的全部功能,所以我选择了环绕通知。(关于AOP的通知使用方法以及XML、注解AOP使用方法参考;http://www.cnblogs.com/qlqwjy/p/8729280.html)express

    

 

下面是具体实现:apache

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)。session

 

 

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;/** * 一、添加字典信息 */ @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;/** * 一、添加字典信息 */ @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

相关文章
相关标签/搜索