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

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

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

    • 一、Spring Boot AOP
    • 二、AOP的常用术语
    • 三、源码实现埋点日志记录
      • 3.1 项目结构图
      • 3.2 代码实现

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

一、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 项目结构图

SpringBoot AOP 实现埋点日志记录(完整源码)_第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;

/**
 * 

AspectDemo

*

AOP切面类

* @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.*;

/**
 * 

AspectDemo

*

自定义注解

* @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;

/**
 * 

AspectDemo

*

操作类型

* @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;

/**
 * 

AspectDemo

*

被操作的单元

* @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;

/**
 * 

AspectDemo

*

日志记录对象

* @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;

/**
 * 

AspectDemo

*

测试接口请求

* @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;

/**
 * 

AspectDemo

*

service

* @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;

/**
 * 

AspectDemo

*

service实现

* @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 这里保存日志 位置实现改功能。

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

你可能感兴趣的:(Java)