SpringBoot AOP 实现埋点日志记录(完整源码)

写在开头:
我是「猿码天地」,一个热爱技术、热爱编程的IT猿。技术是开源的,知识是共享的!
写博客是对自己学习的总结和记录,如果您对Java、分布式、微服务、中间件、Spring Boot、Spring Cloud等技术感兴趣,可以关注我的动态,我们一起学习,一起成长!
用知识改变命运,让家人过上更好的生活,互联网人一家亲!
关注微信公众号【猿码天地】,获取更多干货技能,一起吃肉喝汤,陪你一起撸代码!

随着互联网技术的深入发展,各个系统的日活用户、访问量、点击量成指数上升,为保证系统的安全性、易用性,每个系统都需要对用户的访问做埋点记录、跟踪,从而获取用户常用的操作习惯,同时也方便系统管理人员对系统做日常记录、跟踪。

一、Spring Boot AOP

AOP:面向切面编程,相对于OOP面向对象编程,Spring的AOP的存在目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能继承和实现接口,且类继承只能单继承,阻碍更多行为添加到一组类上,AOP弥补了OOP的不足。还有就是为了清晰的逻辑,让业务逻辑关注业务本身,不用去关心其它的事情,比如事务。
实现方式:Spring的AOP是通过JDK的动态代理和CGLIB实现的。

二、AOP的常用术语

AOP有一堆术语,主要包括以下:
通知(Advice) 需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用。
连接点(Join point) spring中允许使用通知的地方,基本上每个方法前后抛异常时都可以是连接点。
切点(Poincut) 筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。
切面(Aspect) 通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行。
引入(Introduction) 在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面用到目标类中去。
目标(target) 被通知的对象。也就是需要加入额外代码的对象,真正的业务逻辑被组织织入切面。
织入(Weaving) 把切面加入程序代码的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入。

三、源码实现埋点日志记录

3.1 项目结构图

在这里插入图片描述

3.2 代码实现

  • 配置文件
    这里只有三个配置:
    server.port=8081,设置项目启动的端口号,防止被其他服务占用
    server.servlet.context-path: /aop,项目上下文
    spring.aop.auto=true,开启spring的aop配置,简单明了,不需要多配置其他的配置或注解。
server:
  port: 8081
  servlet.context-path: /aop

spring:
  aop:
    auto: true
  • AOP切面类
    这个是最主要的类,可以使用自定义注解或针对包名实现AOP增强。
    1)这里实现了对自定义注解的环绕增强切点,对使用了自定义注解的方法进行AOP切面处理。
    2)对方法运行时间进行监控。
    3)对方法名,参数名,参数值,对日志描述的优化处理。

    在方法上增加@Aspect注解声明切面 使用@Pointcut 注解定义切点,标记方法

    使用切点增强的时机注解:
    @Before 前置通知, 在方法执行之前执行
    @Around 环绕通知, 围绕着方法执行
    @AfterReturning 返回通知, 在方法返回结果之后执行
    @AfterThrowing 异常通知, 在方法抛出异常之后
    @After 后置通知, 在方法执行之后执行

package com.bowen.aspect;

import com.alibaba.fastjson.JSON;
import com.bowen.annotation.OperationLogDetail;
import com.bowen.model.OperationLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * <h3>AspectDemo</h3>
 * <p>AOP切面类</p>
 * @author : zhang.bw
 * @date : 2020-04-16 14:52
 **/
@Aspect
@Component
public class LogAspect {

   private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class);

   /**
    * 定义切点
    * 此处的切点是注解的方式,也可以用包名的方式达到相同的效果
    * '@Pointcut("execution(* com.bowen.service.impl.*.*(..))")'
    */
   //@Pointcut("@annotation(com.bowen.annotation.OperationLogDetail)")
   @Pointcut("execution(* com.bowen.controller.*.*(..))")
   public void operationLog(){}


   /**
    * 环绕增强,相当于MethodInterceptor
    */
   @Around("operationLog()")
   public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
      Object res = null;
      long time = System.currentTimeMillis();
      try {
         res =  joinPoint.proceed();
         time = System.currentTimeMillis() - time;
         return res;
      } finally {
         try {
            //方法执行完成后增加日志
            addOperationLog(joinPoint,res,time);
         }catch (Exception e){
            LOG.error("LogAspect 操作失败:" + e.getMessage());
         }
      }
   }

   /**
    * 方法执行完成后增加日志
    * @param joinPoint
    * @param res
    * @param time
    */
   private void addOperationLog(JoinPoint joinPoint, Object res, long time){
      MethodSignature signature = (MethodSignature)joinPoint.getSignature();
      OperationLog operationLog = new OperationLog();
      operationLog.setRunTime(time);
      operationLog.setReturnValue(JSON.toJSONString(res));
      operationLog.setId(UUID.randomUUID().toString());
      operationLog.setArgs(JSON.toJSONString(joinPoint.getArgs()));
      operationLog.setCreateTime(new Date());
      operationLog.setMethod(signature.getDeclaringTypeName() + "." + signature.getName());
      operationLog.setUserId("#{currentUserId}");
      operationLog.setUserName("#{currentUserName}");
      OperationLogDetail annotation = null;
      try {
         //获取抽象方法
         Method method = signature.getMethod();
         //获取当前类的对象
         Class<?> clazz = joinPoint.getTarget().getClass();
         //获取当前类有 OperationLogDetail 注解的方法
         method = clazz.getMethod(method.getName(), method.getParameterTypes());
         annotation = method.getAnnotation(OperationLogDetail.class);
      } catch (Exception e) {
         LOG.error("获取当前类有 OperationLogDetail 注解的方法 异常",e);
      }
      if(annotation != null){
         operationLog.setLevel(annotation.level());
         operationLog.setDescribe(annotation.detail());
         //operationLog.setDescribe(getDetail((signature).getParameterNames(),joinPoint.getArgs(),annotation));
         operationLog.setOperationType(annotation.operationType().getValue());
         operationLog.setOperationUnit(annotation.operationUnit().getValue());
      }
      //TODO 这里保存日志
      LOG.info("记录日志:" + operationLog.toString());
      //operationLogService.insert(operationLog);
   }

   /**
    * 对当前登录用户和占位符处理
    * @param argNames 方法参数名称数组
    * @param args 方法参数数组
    * @param annotation 注解信息
    * @return 返回处理后的描述
    */
   @Deprecated
   private String getDetail(String[] argNames, Object[] args, OperationLogDetail annotation){

      Map<Object, Object> map = new HashMap<>(4);
      for(int i = 0;i < argNames.length;i++){
         map.put(argNames[i],args[i]);
      }

      String detail = annotation.detail();
      try {
         detail = "'" + "#{currentUserName}" + "'=》" + annotation.detail();
         for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Object k = entry.getKey();
            Object v = entry.getValue();
            detail = detail.replace("{{" + k + "}}", JSON.toJSONString(v));
         }
      }catch (Exception e){
         e.printStackTrace();
      }
      return detail;
   }

   @Before("operationLog()")
   public void doBeforeAdvice(JoinPoint joinPoint){
      LOG.info("进入方法前执行.....");
   }

   /**
    * 处理完请求,返回内容
    * @param ret
    */
   @AfterReturning(returning = "ret", pointcut = "operationLog()")
   public void doAfterReturning(Object ret) {
      LOG.info("方法的返回值 : " + ret);
   }

   /**
    * 后置异常通知
    */
   @AfterThrowing("operationLog()")
   public void throwss(JoinPoint jp){
      LOG.info("方法异常时执行.....");
   }

   /**
    * 后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
    */
   @After("operationLog()")
   public void after(JoinPoint jp){
      LOG.info("方法最后执行.....");
   }

}
  • 自定义注解
package com.bowen.annotation;

import com.bowen.enums.OperationType;
import com.bowen.enums.OperationUnit;

import java.lang.annotation.*;

/**
 * <h3>AspectDemo</h3>
 * <p>自定义注解</p>
 * @Target 此注解的作用目标,括号里METHOD的意思说明此注解只能加在方法上面
 * @Retention 注解的保留位置,括号里RUNTIME的意思说明注解可以存在于运行时,可以用于反射
 * @Documented 说明该注解将包含在javadoc中
 * @author : zhang.bw
 * @date : 2020-04-16 14:55
 **/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLogDetail {

   /**
    * 方法描述:可使用占位符获取参数:{{tel}}
    */
   String detail() default "";

   /**
    * 日志等级:自己定,此处分为1-9
    */
   int level() default 0;

   /**
    * 操作类型(enum):主要是select,insert,update,delete
    */
   OperationType operationType() default OperationType.UNKNOWN;

   /**
    * 被操作的对象(此处使用enum):可以是任何对象,如表名(user),或者是工具(redis)
    */
   OperationUnit operationUnit() default OperationUnit.UNKNOWN;
}
  • 注解用到的枚举类型
package com.bowen.enums;

/**
 * <h3>AspectDemo</h3>
 * <p>操作类型</p>
 * @author : zhang.bw
 * @date : 2020-04-16 14:58
 **/
public enum OperationType {
   /**
    * 操作类型
    */
   UNKNOWN("unknown"),
   DELETE("delete"),
   SELECT("select"),
   UPDATE("update"),
   INSERT("insert");

   private String value;

   public String getValue() {
      return value;
   }

   public void setValue(String value) {
      this.value = value;
   }

   OperationType(String s) {
      this.value = s;
   }
}
package com.bowen.enums;

/**
 * <h3>AspectDemo</h3>
 * <p>被操作的单元</p>
 * @author : zhang.bw
 * @date : 2020-04-16 15:00
 **/
public enum OperationUnit {

   /**
    * 被操作的单元
    */
   UNKNOWN("unknown"),
   USER("user"),
   EMPLOYEE("employee"),
   Redis("redis");

   private String value;

   public String getValue() {
      return value;
   }

   public void setValue(String value) {
      this.value = value;
   }

   OperationUnit(String value) {
      this.value = value;
   }
}
  • 日志记录对象
package com.bowen.model;

import lombok.Data;

import java.util.Date;

/**
 * <h3>AspectDemo</h3>
 * <p>日志记录对象</p>
 * @author : zhang.bw
 * @date : 2020-04-16 15:01
 **/
@Data
public class OperationLog {

   private String id;
   private Date createTime;
   /**
    * 日志等级
    */
   private Integer level;
   /**
    * 被操作的对象
    */
   private String operationUnit;
   /**
    * 方法名
    */
   private String method;
   /**
    * 参数
    */
   private String args;
   /**
    * 操作人id
    */
   private String userId;
   /**
    * 操作人
    */
   private String userName;
   /**
    * 日志描述
    */
   private String describe;
   /**
    * 操作类型
    */
   private String operationType;
   /**
    * 方法运行时间
    */
   private Long runTime;
   /**
    * 方法返回值
    */
   private String returnValue;

   @Override
   public String toString() {
      return "OperationLog{" +
            "id='" + id + '\'' +
            ", createTime=" + createTime +
            ", level=" + level +
            ", operationUnit='" + operationUnit + '\'' +
            ", method='" + method + '\'' +
            ", args='" + args + '\'' +
            ", userId='" + userId + '\'' +
            ", userName='" + userName + '\'' +
            ", describe='" + describe + '\'' +
            ", operationType='" + operationType + '\'' +
            ", runTime=" + runTime +
            ", returnValue='" + returnValue + '\'' +
            '}';
   }
}
  • springboot启动类
package com.bowen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootAopApplication {

   public static void main(String[] args) {
      SpringApplication.run(SpringbootAopApplication.class, args);
   }

}
  • controller类
package com.bowen.controller;

import com.bowen.annotation.OperationLogDetail;
import com.bowen.enums.OperationType;
import com.bowen.enums.OperationUnit;
import com.bowen.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * <h3>AspectDemo</h3>
 * <p>测试接口请求</p>
 * @author : zhang.bw
 * @date : 2020-04-16 14:03
 **/
@Controller
@RequestMapping("user")
public class UserController {

   @Autowired
   private UserService userService;

   /**
    * 访问路径 http://localhost:8081/user/findUserNameByTel?tel=1234567
    * @param tel 手机号
    * @return userName
    */
   @ResponseBody
   @RequestMapping("/findUserNameByTel")
   @OperationLogDetail(detail = "通过手机号获取用户名",level = 3,operationUnit = OperationUnit.USER,operationType = OperationType.SELECT)
   public String findUserNameByTel(@RequestParam("tel") String tel){
      return userService.findUserName(tel);
   }

}
  • Service类
package com.bowen.service;

/**
 * <h3>AspectDemo</h3>
 * <p>service</p>
 * @author : zhang.bw
 * @date : 2020-04-16 15:05
 **/
public interface UserService {
   /**
    * 获取用户信息
    * @return
    * @param tel
    */
   String findUserName(String tel);
}
  • Service实现类
package com.bowen.service.impl;

import com.bowen.aspect.LogAspect;
import com.bowen.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * <h3>AspectDemo</h3>
 * <p>service实现</p>
 * @author : zhang.bw
 * @date : 2020-04-16 15:08
 **/
@Service
public class UserServiceImpl implements UserService {

   private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class);

   @Override
   public String findUserName(String tel) {
      LOG.info("tel:" + tel);
      return "zhangsan";
   }
}
  • MAVEN依赖
<!-- spring-boot aop依赖配置引入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  • 运行及结果
    浏览器输入请求:http://localhost:8081/user/findUserNameByTel?tel=1234567
    返回结果:
进入方法前执行.....
tel:1234567
记录日志:OperationLog{id='5aea4821-206b-408f-9e7f-d1145af786fb', createTime=Fri Apr 17 11:40:12 CST 2020, level=3, operationUnit='user', method='com.bowen.controller.UserController.findUserNameByTel', args='["1234567"]', userId='#{currentUserId}', userName='#{currentUserName}', describe='通过手机号获取用户名', operationType='select', runTime=11, returnValue='"zhangsan"'}
方法最后执行.....
方法的返回值 : zhangsan

该demo没有将日志写入数据库,如有需要,可在LogAspect.java文件的//TODO 这里保存日志 位置实现改功能。

写在结尾:
关注【猿码天地】,做一个潮流的技术人!

猜你喜欢

转载自blog.csdn.net/zbw125/article/details/105608185