SpringBoot 配置AOP记录用户操作日志

文章目录

      • 一、在pom.xml中启用starter-aop
      • 二、注解
            • 自定义注解
            • 常见注解:
      • 三、AOP实现
        • Http相关工具类:
        • 获取UUID主键的工具类:
        • AOP切面类,切点为controller方法
      • 四、最后再说一点
            • @AfterThrowing
            • @AfterThrowing和@Around并用:

一、在pom.xml中启用starter-aop


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0modelVersion>
	<parent>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-parentartifactId>
		<version>2.2.1.RELEASEversion>
		<relativePath/> 
	parent>
	<groupId>com.LirsgroupId>
	<artifactId>SpringartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>Springname>
	<description>SpringBoot学习框架description>

	<properties>
		<java.version>1.8java.version>
		<mysql.version>5.0.8mysql.version>
	properties>
	
	<dependencies>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>
		
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-testartifactId>
			<scope>testscope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintagegroupId>
					<artifactId>junit-vintage-engineartifactId>
				exclusion>
			exclusions>
		dependency>
		
		<dependency>
			<groupId>org.mybatis.spring.bootgroupId>
			<artifactId>mybatis-spring-boot-starterartifactId>
			<version>2.1.1version>
		dependency>
		
		<dependency>
			<groupId>com.alibabagroupId>
			<artifactId>druid-spring-boot-starterartifactId>
			<version>1.1.20version>
		dependency>
		
		<dependency>
			<groupId>mysqlgroupId>
			<artifactId>mysql-connector-javaartifactId>
		dependency>
		
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-jdbcartifactId>
		dependency>
		
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-aopartifactId>
		dependency>

	dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>
project>

二、注解

自定义注解

自定义注解的作用在于可以根据方法上是否包含该注解来判断该方法是否是切点,还可以设置属性,方便我们取值。

/*
*用于标注在方面上,可以作为一个标记,指定切点。
*/
@Target(ElementType.METHOD)//标注可以注在方法上
@Retention(RetentionPolicy.RUNTIME) //jvm在运行过程中仍然保留该注解
public @interface Log {
    String value() default ""; //value属性
}
常见注解:
  • @Aspect标注:将当前类标注为一个切面类,供Spring读取。

  • @Pointcut: Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。

@Pointcut("@annotation(com.Lirs.Spring.annotation.Log)")
public void pointcut() {}
  
@Pointcut("execution(public * com.Lirs.Spring.Controller.*.*(..))")
public void pointcut(){}

​ 解释:@Pointcut的作用就是指定切点。第一种方式就是根据annotation中指定注解进行切入。不一定非要public,只要可以调用pointcut()即可,即 pointcut与AOP方法在同一个类中时,就算是private也可以。第二种方式就是通过表达式指定某个路径下的指定某种或多种特征的类或方法为切点。上面表达式表示的范围是:com.Lirs.Spring.Controller包下的所有方法。

"execution(public * com.Lirs.Spring.Controller.*.*(..))"
 表示的意思 public 任意返回值 com.Lirs.Spring.Controller.任意类.任意方法(任意参数列表)

当然,也可以不写pointcut方法,直接在增强方法上写入表达式

@Pointcut("execution(public * com.Lirs.Spring.Controller.UserController.*(..))")
public void pointcut(){}

//@Before("execution(public * com.Lirs.Spring.Controller.UserController.*(..))")
@Before("pointcut()")
public void before(JoinPoint point){
	System.out.println("=========================Before======================================");
	long begin = System.currentTimeMillis();
	System.out.println("@Before:模拟权限检查。。。");
	System.out.println("@Before:目标方法为:" +
			point.getSignature().getName());
	System.out.println("@Before: 参数为:" + Arrays.toString(point.getArgs()));
	System.out.println("@Before:被织入的目标对象为:" + point.getTarget());
	System.out.println("=========================Before======================================");
}

上面两种方法写法效果是一样的。

  • @Around环绕增强:在切点方法执行前后都可以执行。使用这个注解之后的切点方法如果抛出异常,会影响@AfterThrowing。

  • @Before前置增强:在切点方法执行前执行。

  • @AfterReturning后置增强:切点方法正常结束之后执行。

  • @Ater: final增强,类似于try catch中的finally 在return前执行,无论切点方法是否正常结束,都会执行。

  • AfterThrowing异常抛出增强:切点方法抛出异常时执行。注意 必须是切点方法抛出了异常才会执行代码,如果切点方法自己try catch 捕获了异常,那么这个增强不会执行。

需要注意的是:一个切点是可以被多个切面切的。下面会给大家演示一下多个切面切一个切点的实例

三、AOP实现

Http相关工具类:

package com.Lirs.Spring.util;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * Http工具类,主要用于获取ip地址
 */
public class HttpUtil {

    /**
     * 获取客户端IP地址
     * 如果使用了Nginx等软件进行代理,则不能使用哪个reuqest.getRemoteAddr获取IP地址
     * 如果使用了多级反向代理的话,x-forwarded-for的值不止一个,真正的客户端IP地址就是第一个非unknown的有效IP字符串
     * @param request
     * @return
     */
    public static String getIp(HttpServletRequest request){
        String ip = request.getHeader("x-forwarded-for");
        if(ip == null || ip.length() == 0 || "unknown".equals(ip)){
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equals(ip)){
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equals(ip)){
            ip = request.getRemoteAddr();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

    /**
     * 获取用户request
     * @return
     */
    public static HttpServletRequest getHttpServletRequest(){
        return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
    }
}

获取UUID主键的工具类:

package com.Lirs.Spring.util;

import java.util.UUID;

public class UUIDUitl {
    public static String getUUID(){
        return UUID.randomUUID().toString().replace("-","");
    }
}

AOP切面类,切点为controller方法

package com.Lirs.Spring.aspect;

import com.Lirs.Spring.annotation.Log;
import com.Lirs.Spring.localMapper.SysLogMapper;
import com.Lirs.Spring.localMapper.SysThrowMapper;
import com.Lirs.Spring.model.SysLog;
import com.Lirs.Spring.model.SysThrow;
import com.Lirs.Spring.util.HttpUtil;
import com.Lirs.Spring.util.UUIDUitl;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;

@Aspect
@Component
public class SysLogAspect {
    @Autowired
    private SysLogMapper mapper;
    @Autowired
    private SysThrowMapper throwMapper;

//    @Pointcut("@annotation(com.Lirs.Spring.annotation.Log)")
//    public void pointcut() {}
    @Pointcut("execution(public * com.Lirs.Spring.Controller.LogController.*(..))")
    private void pointcut(){}

    /**
     * 环绕增强
     * @param point
     * @return
     */
     //   @Around(value = "execution(public * com.Lirs.Spring.Controller.*.*(..))")
    @Around(value = "pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable{
        System.out.println("=========================Around======================================");
        long begin = System.currentTimeMillis();
        System.out.println("@Around:模拟around环绕增强");
        System.out.println("@Around:原本输入的参数" + Arrays.toString(point.getArgs()));
        Object args[] = point.getArgs();
        Object result = new Object();
        if(args != null && args.length > 0){
            for(int i = 0; i < args.length ; i++){
                args[i] = "改变原方法的参数";
            }
        }
        System.out.println("参数修改完毕");
        result = point.proceed(args);
//        try{
//            result = point.proceed(args);
//        }catch(Throwable e){
//            e.printStackTrace();
//        }
        long time = System.currentTimeMillis() - begin;
        System.out.println("@Around:记录日志");
        saveLog(point,time);
        System.out.println("=========================Around======================================");
        return result;
    }

    /**
     * 前置增强 增强mapper下的SysLogMapper
     * @param point
     * @return
     */
    //@Before("execution(public * com.Lirs.Spring.Controller.UserController.*(..))")
    @Before("pointcut()")
    public void before(JoinPoint point){
        System.out.println("=========================Before======================================");
        long begin = System.currentTimeMillis();
        System.out.println("@Before:模拟权限检查。。。");
        System.out.println("@Before:目标方法为:" +
                point.getSignature().getName());
        System.out.println("@Before: 参数为:" + Arrays.toString(point.getArgs()));
        System.out.println("@Before:被织入的目标对象为:" + point.getTarget());
        System.out.println("=========================Before======================================");

    }

    @AfterReturning(value = "pointcut()",returning = "returnValue")
    public void afterReturing(JoinPoint point,Object returnValue){
        System.out.println("=========================AfterReturing===============================");
        System.out.println("@AferReturing:模拟方法执行完成后");
        System.out.println("@AferReturing:方法名为:" +
                point.getSignature().getName());
        System.out.println("@AferReturing:方法的参数为:" + Arrays.toString(point.getArgs()));
        System.out.println("@AferReturing:方法的返回值为:" + returnValue);
        System.out.println("=========================AfterReturing===============================");
    }

    @After("pointcut()")
    public void afterFinall(JoinPoint joinPoint){
        System.out.println("@After:模拟释放资源");
    }

    //@AfterThrowing(value = "execution(public * com.Lirs.Spring.Controller.UserController.*(..))",throwing = "ex")
    @AfterThrowing(value = "pointcut()",throwing = "ex")
    public void afterThrowing(JoinPoint point,Throwable ex){
        System.out.println("=========================AfterThrowing===============================");
        System.out.println("@AfterThrowing:模拟执行方法时发生了异常");
        System.out.println("@AfterThrowing:记录日志");
        saveThrow(point,ex);
        System.out.println("=========================AfterThrowing===============================");
    }

    /**
     * 将日志记录在数据库中
     * @param point
     * @param time
     */
    private void saveLog(ProceedingJoinPoint point,long time){
        MethodSignature signature =(MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        SysLog sysLog = new SysLog();
        Log logAnnotation = method.getAnnotation(Log.class);
        if(logAnnotation != null){
            //填充注解上的值
            sysLog.setOperation(logAnnotation.value());
        }
        //获取类名
        String className = point.getTarget().getClass().getName();
        //获取方法名
        String methodName = signature.getName();
        sysLog.setMethod(className + "." + methodName);
        //请求的方法的参数值
        Object args[] = point.getArgs();
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String params[] = u.getParameterNames(method);
        if(args != null && params != null){
            StringBuilder sb = new StringBuilder("");
            for(int i = 0;i < args.length;i++){
                sb.append(args[i]).append(":").append(params[i]).append("\t");
            }
            sysLog.setParams(sb.toString());
        }
        //获取request
        HttpServletRequest request =HttpUtil.getHttpServletRequest();
        /**
         * 获取ip
         */
        sysLog.setIp(HttpUtil.getIp(request));
        //模拟一个用户
        sysLog.setUsername("admin");
        sysLog.setTime((int) time);
        sysLog.setCreate_time(new Date());
        //获取UUid
        String uuid = UUIDUitl.getUUID();
        sysLog.setId(uuid);
        mapper.saveLog(sysLog);
    }
	//存储异常信息
    private void saveThrow(JoinPoint point,Throwable ex){
        SysThrow sysThrow = new SysThrow();
        //设置id
        sysThrow.setId(UUIDUitl.getUUID());
        //填充异常信息
        sysThrow.setException(ex.toString());
        //填入异常发生时间
        sysThrow.setTime(new Date().toString());
        //填入异常发生的方法
        sysThrow.setMethod(point.getSignature().getName());
        //填入异常方法接收的参数
        sysThrow.setParams(Arrays.toString(point.getArgs()));
        HttpServletRequest request = HttpUtil.getHttpServletRequest();
        //填入客户端IP地址
        sysThrow.setIp(HttpUtil.getIp(request));
        throwMapper.saveThrow(sysThrow);
    }
}

先来看看执行的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKr4rovt-1576224798222)(C:\Users\46275\AppData\Roaming\Typora\typora-user-images\image-20191211102834959.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MvbLt8j5-1576224798229)(C:\Users\46275\AppData\Roaming\Typora\typora-user-images\image-20191211105708943.png)]

从上图我们可以看出一般情况下几种增强的执行顺序:@Around前置 --> @Before --> 目标代码执行 --> @Around后置执行 --> @After执行 -->@AfterReturing执行

所以我们可以根据实际的业务需求去选择合适的增强;

四、最后再说一点

@AfterThrowing

这个注解目前来说在我眼里完全不知道有什么作用。。因为它需要切点抛出异常才会执行。可是一般我们在程序里都是尽量try catch捕获 避免直接将异常抛出到上一层。但是一旦异常被try catch块捕获之后,@AfterThrowing就不会执行了。

下面给大家举个栗子:

首先看看没有使用try catch块的情况

Controller层会抛出异常的代码:

@Log(value = "测试异常日志捕获")
@RequestMapping("/testThrow")
public String testThrow(String arg){
    throw new RuntimeException();
}
@AfterThrowing(value = "execution(public * com.Lirs.Spring.Controller.UserController.*(..))",throwing = "ex")
public void afterThrowing(JoinPoint point,Throwable ex){
    System.out.println("=========================AfterThrowing===============================");
    System.out.println("@AfterThrowing:模拟执行方法时发生了异常");
    System.out.println("@AfterThrowing:记录日志");
    saveThrow(point,ex);
    System.out.println("=========================AfterThrowing===============================");
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0S7q4dpr-1576224798231)(C:\Users\46275\AppData\Roaming\Typora\typora-user-images\image-20191211104808641.png)]

OK,虽然异常还是被抛出了,但是我们好歹记录了异常。

接下来看看加入try catch 之后是什么效果

@Log(value = "测试异常日志捕获")
@RequestMapping("/testThrow")
public String testThrow(String arg){
    try{
         throw new RuntimeException();
    }catch (RuntimeException ex){
        System.out.println("我自己捕获处理了异常:" + ex.toString());
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mwGCpPay-1576224798232)(C:\Users\46275\AppData\Roaming\Typora\typora-user-images\image-20191211105124379.png)]

这。。。。。。。。。。。。。。

说实话,我现在不太清楚这种类型的增强可以运用到什么样的业务场景了。这里也请教各位大佬们。。希望给我一丢丢宝贵的指点。。。提前感谢。

@AfterThrowing和@Around并用:

各位可以看看上面的代码,然后你们就会发现我在@Around的增强里面有一段注释掉了的代码:

看代码中的注释

/**
* 环绕增强
* @param point
* @return
*/
@Around(value = "pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable{
    System.out.println("=========================Around======================================");
    long begin = System.currentTimeMillis();
    System.out.println("@Around:模拟around环绕增强");
    System.out.println("@Around:原本输入的参数" + Arrays.toString(point.getArgs()));
    Object args[] = point.getArgs();
    Object result = new Object();
    if(args != null && args.length > 0){
        for(int i = 0; i < args.length ; i++){
            args[i] = "改变原方法的参数";
        }
    }
    System.out.println("参数修改完毕");
    result = point.proceed(args);
    //        try{
    //            result = point.proceed(args);
    //        }catch(Throwable e){
    //            e.printStackTrace();
    //        }
    long time = System.currentTimeMillis() - begin;
    System.out.println("@Around:记录日志");
    saveLog(point,time);
    System.out.println("=========================Around======================================");
    return result;
}

为什么要加这段注释呢?

是因为我在写的时候遇到的一个坑。切点方法的异常哪怕自己没有捕获,抛出异常之后,如果被@Around捕获了。。。那么@AfterThrowing一样不会执行。。。

至此,大工告辞。
文章中有不足或者错误的地方,欢迎大家指出。共同进步!感谢。

你可能感兴趣的:(SpringBoot,JAVA学习)