前言

平时做项目的时候,习惯于记录系统日志,出错去查看系统日志而对于用户具体做了什么操作无法知道,所以在这里我打算使用Spring时期所学AOP,整合到springboot中实现对用户操作信息的详细记录.



一、目录结构

SpringBoot 2.4.X 整合 AOP --- 监听用户操作记录并保存数据库_bc

二、 代码编写:

1. Pom.xml 添加必要依赖

<!--提供JdbcTemplate-->
 		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        
		<!--数据库连接驱动-->
		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--引入druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.13</version>
        </dependency>

        <!-- aop依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2. yml 相关配置

server:
  port: 8888
  servlet:
    context-path: /SpringAop

spring:
  datasource:
    druid:
      # 数据库访问配置, 使用druid数据源
      url: jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf8&useSSL=false
      username: 你的账号
      password: 你的密码
      driver-class-name: com.mysql.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource

3. 自定义注解

定义一个方法级别的@Log注解,用于标注需要监听的方法

@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解类型保留时间的长短
public @interface Log {
    String value() default ""; // value是注解描述的意思,应用: @Log("第一个方法执行")
}

4. 创建库表和实体

数据库名称为 springbootdb, 在该数据库中创建一张sys_log表,用于保存用户的操作日志,数据库服务采用MySQL 5.7:

create table sys_log (
    ID  int auto_increment primary key,
    USERNAME   varchar(20)  null,
    OPERATION  varchar(20)  null,
    TIMES      int(20)      null,
    METHOD     varchar(200) null,
    PARAMS     varchar(500) null,
    IP         varchar(64)  null,
    CREATETIME date         null
);

SpringBoot 2.4.X 整合 AOP --- 监听用户操作记录并保存数据库_spring_02

创建数据库时注意要设置编码格式 character set utf8

表对应的实体类:

@Data
public class SysLog implements Serializable {
    private static final long serialVersionUID = -6309732882044872298L; // 序列化
    private Integer id;
    private String username;
    private String operation;
    private Integer times;
    private String method;
    private String params;
    private String ip;
    private Date createTime;

}

小贴士:引入 lombok 依赖即可使用@Data注解,不需要写getter,setter 方法,自动生成


5. 保存日志的方法

接口:

public interface SysLogDao {
    void saveSysLog(SysLog sysLog);
}

实现类:

@Repository
public class SysLogDaoImpl implements SysLogDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Override
    public void saveSysLog(SysLog sysLog) {
        String sql = "insert into sys_log(username,operation,times,method,params,ip,createTime) values (?,?,?,?,?,?,?)";

        jdbcTemplate.update(sql,sysLog.getUsername(), sysLog.getOperation(),
                sysLog.getTimes(),sysLog.getMethod(), sysLog.getParams(),sysLog.getIp(),
                sysLog.getCreateTime());
    }
}

由于没有整合mybatis,所以这里直接使用 spring 的 JdbcTemplate 完成数据库操作工作


6. 封装两个小工具 => HttpContextUtil 、IpUtil

  • HttpContextUtil : 方便随时能取到当前请求的request对象
  • IpUtil: 获取用户当前的 IP 地址
  • HttpContextUtil
public class HttpContextUtil {

	/*
	* RequestContextHolder : 持有上下文的Request容器
	* */

	// 获取Request对象
	public static HttpServletRequest getHttpServletRequest() {
		return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
	}

	// 获取Response对象
	public static HttpServletResponse getHttpServletResponse() {
		return ((ServletWebRequest)RequestContextHolder.getRequestAttributes()).getResponse();
	}
}
  • IpUtil
public class IpUtil {
    /**
     * 获取IP地址
     *
     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
     */

    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.getRemoteAddr();
        }
        return  "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}

7. 切面和切点的代码编写(重点,注解详细)

@Aspect
@Component
public class logAspect {
    @Autowired
    private SysLogDao sysLogDao;

    // 切点
    @Pointcut("@annotation(com.xiao.springaop.annotation.Log)") // 所有带有 Log 注解的方法作为 织入点
    public void pointcut(){}

    @Around("pointcut()") // 增强处理方法 , 参数必须是 Proceedding 类型
    public void around(ProceedingJoinPoint point) {
        long beginTime = System.currentTimeMillis();

        try {
            // 执行方法,controller层的方法
            point.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        // 执行时长
        long time = System.currentTimeMillis() - beginTime;

        // 保存日志
        saveLog(point, time);
    }

    public void saveLog(ProceedingJoinPoint joinPoint, long time) {
        // 获取joinPoint的信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        Method method = signature.getMethod(); // 获取到 Method 对象,可以用于获取方法上面的注解
        SysLog sysLog = new SysLog();
        Log logAnnotation = method.getAnnotation(Log.class); // 获取Log注解对象

        if (logAnnotation != null) {
            // 注解上的value描述
            sysLog.setOperation(logAnnotation.value());
        }

        // getTarget()该方法返回被织入增强处理的目标对象
        String className = joinPoint.getTarget().getClass().getName(); // 获取类名
        String methodName = signature.getName(); // 获取方法名

        sysLog.setMethod(className + "." + methodName + "()");

        // 请求方法参数名
        Object[] args = joinPoint.getArgs();

        // 获取方法的参数名
        LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] parameterNames = localVariableTableParameterNameDiscoverer.getParameterNames(method);

		// 拼接: 参数名:参数值
        if (args != null && parameterNames != null) {
            String params = "";
            for (int i = 0 ; i < args.length; i++) {
                params += "  " + parameterNames[i] + ": " + args[i];
            }
            sysLog.setParams(params);
        }

        // 获取 resquest 对象
        HttpServletRequest httpServletRequest = HttpContextUtil.getHttpServletRequest();

        // 设置IP地址
        sysLog.setIp(IpUtil.getIpAddress(httpServletRequest));

        // 模拟用户名
        sysLog.setUsername("mr.xiao");

        // 方法所用时间
        sysLog.setTimes((int) time);

        // 日志创建时间
        Date date = new Date();
        sysLog.setCreateTime(date);

        // 保存日志
        sysLogDao.saveSysLog(sysLog);

    }

}

8. Controller 测试:

测试地址:http://192.168.1.101:8888/SpringAop/ (博主的电脑本地IP地址,仅做参考)

@RestController
public class TestContoller {
    @Log("方法一执行")
    @GetMapping("/method_one")
    public void methodOne(String name) {

    }

    @Log("方法二执行")
    @GetMapping("/method_two")
    public void methodTwo() throws InterruptedException {
        Thread.sleep(2000);
    }

    @Log("方法三执行")
    @GetMapping("/method_three")
    public void methodThree(String name, String age) throws InterruptedException {
    }
}

为了更好的看到 IPUtil 工具类IP获取的效果,我使用 linux 模拟另一台主机的访问,结果如下:

SpringBoot 2.4.X 整合 AOP --- 监听用户操作记录并保存数据库_bc_03