JAVA- AOP 面向切面编程 Aspect切面工具类 记录特定方法执行时的入参、执行时间、返参等内容

背景:JAVA项目,使用AOP对指定函数进行切面。能够记录特定方法执行时的入参、执行时间、返参结果等内容。


AOP可以实现对于函数执行整个过程中的数据流信息流,
比如调用函数方法前,需要根据头部信息来调用外部接口获取到所需的信息,来决定后续方法执行的逻辑;调用函数方法后,日志信息的记录(请求参数、返回结果、执行时长等)。

1、自定义注解类

可以在需要使用的函数上加上对应注解即可使用,不会将AOP应用于全部的函数。
例如这儿名为AutoLog,后续只需要在对应所需函数上使用@AutoLog即可

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解类,在对应的函数上使用@AutoLog即可
 */

@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
//@Documented //生成文档
public @interface AutoLog {
    
    
    String value() default "";
}

1.1 @Target

@Target 说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
  作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

取值(ElementType)有:
    1.CONSTRUCTOR:用于描述构造器
    2.FIELD:用于描述域
    3.LOCAL_VARIABLE:用于描述局部变量
    4.METHOD:用于描述方法
    5.PACKAGE:用于描述包
    6.PARAMETER:用于描述参数
    7.TYPE:用于描述类、接口(包括注解类型) 或enum声明

1.2 @Retention

@Retention(RetentionPolicy.RUNTIME) 注解按生命周期来划分可分为3类:

1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

这3个生命周期分别对应于:Java源文件(.java文件) —> .class文件 —> 内存中的字节码。

那怎么来选择合适的注解生命周期呢?

首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。

2、Aspect切面工具

2.1 JointPoint

JointPoint是程序运行过程中可识别的点,这个点可以用来作为AOP切入点。JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。

2.2 @Pointcut

通过@Pointcut定义切入点,可作用于全部方法或指定方法
execution:用于匹配方法执行的连接点
@annotation:用于匹配当前执行方法持有指定注解的方法

//作用于service中所有方法
@Pointcut("execution(* com.demo.service.*(..))")

//作用与所有接口
@Pointcut("execution( public * com.demo.controller.*Controller.*(..))")

//作用与加了注解的指定接口,注解形式配置参考第1部分
@Pointcut("@annotation( com.demo.utils.AutoLog)")

2.3 切面中的相关注解

@Before 定义了一个前置通知,是在所拦截方法执行之前执行一段逻辑。
@After 是在所拦截方法执行之后执行一段逻辑。
@Around 是可以同时在所拦截方法的前后执行一段逻辑。
@AfterReturning 是可以获取该执行方法的返回值。
@AfterThrowing 定义了一个异常通知,若方法抛出了Exception类型的异常,都会回调afterThrowing方法。

Aspect切面工具类如下:

import com.demo.services.AutoTestLogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Component
@Slf4j
public class AutoTestLogAspect {
    
    
    long startTime;
    String body;

    @Autowired
    private AutoTestLogService autoTestLogService;

    //作用于所有接口
	//@Pointcut("execution( public * com.demo.controller.*Controller.*(..))")
    //定义切点 @Pointcut 在注解的位置切入代码 指定注解
    @Pointcut("@annotation( com.demo.utils.AutoLog)")

    @Before("logPointCut()")
    public void doBefore(JoinPoint joinPoint) {
    
    
    	//开始时间
        startTime = System.currentTimeMillis();
        //获取入参
        Object[] objects= joinPoint.getArgs();
       //将数组转成String
        body = Arrays.toString(objects);
    }

    @After("logPointCut()")
    public void doAfter() {
    
    
        long spendTime = System.currentTimeMillis() - startTime;
        log.info("spendTime:" + spendTime + "ms");
    }

    //切面 配置通知
    @AfterReturning(returning = "object", pointcut = "logPointCut()")
    public void doAfterReturning(Object object) {
    
    
        long spendTime = System.currentTimeMillis() - startTime;
        log.info("response={}", object.toString());
        //将被调用记录插入数据库
        autoTestLogService.insertAutoTestData(spendTime,true, object.toString());
    }

    @AfterThrowing(value = "logPointCut()", throwing = "exception")
    public void doAfterThrowingAdvice(JoinPoint joinPoint, Exception exception){
    
    
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        long spendTime = System.currentTimeMillis() - startTime;
        if (requestAttributes != null) {
    
    
            HttpServletRequest request = requestAttributes.getRequest();
            //记录请求方法
            log.info("REQUEST: {}", request.getMethod());
            log.info("REQUEST_METHOD: {}", joinPoint.getSignature());
            //异常信息
            log.info("EXCEPTION: {}", exception.getMessage());
        }
    }

    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        long start = System.currentTimeMillis();
        try {
    
    
            Object result = joinPoint.proceed();
            long end = System.currentTimeMillis();
            log.error("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
            return result;
        } catch (Throwable e) {
    
    
            long end = System.currentTimeMillis();
            log.error("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
            throw e;
        }
    }
}

主要作用是在于记录切面获取的信息,比如执行方法入参、传参、返参等等。
service

import com.demo.dao.mapper.AutoTestLogMapper;
import com.demo.dao.po.AutoTestLog;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.Date;

@Slf4j
@Service
public class AutoTestLogService {
    
    

    @Autowired
    AutoTestLogMapper autoTestLogMapper;

    /**
     * 埋点数据落表
     * @return
     */
    @Async("taskExecutor")
    public Boolean insertAutoTestData(double spendingTime, boolean isSuccess, Object object){
    
    
        Gson gson = new Gson();
        AutoTestLog autoTestLog = new AutoTestLog();
        autoTestLog.setSpendTime(spendingTime);
        autoTestLog.setSuccess(isSuccess);
        autoTestLog.setCreateDate(new Date());
        autoTestLog.setUpdatedDate(new Date());
        autoTestLog.setResponse(object.toString());
        try {
    
    
            autoTestLogMapper.insert(autoTestLog);
            log.info(String.format("AOP调用结果:%s", gson.toJson(autoTestLog)));
        }catch (Exception e){
    
    
            log.error(e.getMessage());
            return false;
        }
        return true;
    }

}

定义PO

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
@TableName("auto_log")
public class AutoTestLog implements Serializable {
    
    
    @TableId(type = IdType.AUTO)
    private Integer id;

    @TableField(value = "is_success")
    private boolean isSuccess;

    @TableField(value = "response")
    private String response;

    @TableField(value = "spend_time")
    private double spendTime;

	@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+08:00")
    @TableField(value = "create_date",fill = FieldFill.INSERT)
    private Date createDate;
    
	@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+08:00")
    @TableField(value = "updated_date",fill = FieldFill.UPDATE)
    private Date updatedDate;
}

mapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.demo.dao.po.AutoTestLog;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;

@Mapper
@Component
public interface AutoTestLogMapper extends BaseMapper<AutoTestLog> {
    
    
}

sql

DROP TABLE IF EXISTS `auto_log`;
CREATE TABLE `auto_log` (
  `id` int NOT NULL AUTO_INCREMENT,
  `is_success` tinyint DEFAULT NULL COMMENT '判断升级是否成功;0:失败;1:成功',
  `response` varchar(500) COMMENT '请求结果',
  `spend_time` DOUBLE(16,2) DEFAULT '0' COMMENT '接口耗时',
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
  INDEX (is_success)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

3、同一个类里调用AOP

需要在启动类加上该注解,其会往容器中注入了AnnotationAwareAspectJAutoProxyCreator组件,该组件就是AOP的核心

@EnableAspectJAutoProxy(exposeProxy=true)

具体service中内容如下:
getservice()为了解决事务失效,注入bean;
demo()就是我们切面记录方法;
exec()就是业务函数,在其中用事务方式调用AOP方法即可。



    //解决事务失效
    private xxService getService(){
    
    
        return SpringUtil.getBean(this.getClass());
    }

    public exec{
    
    
        //需要执行的AOP函数
        getService.demo();
    }

    @AutoLog("synAutoTestLog")
    public String demo(){
    
    
        //业务代码
    }

4、其他可获取参数

可获取执行过程中的诸多参数,例如:
前置操作中参数,例如请求URL、请求IP、请求类名、请求方法名、请求参数等
执行后的返回值、请求耗时、浏览器类型、操作系统、header等

package com.xkcoding.log.aop.aspectj;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import cn.hutool.json.JSONUtil;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;

@Aspect
@Component
@Slf4j
public class AopLog {
    
    
    private static final String START_TIME = "request-start";

    /**
     * 切入点
     */
    @Pointcut("xxx")
    public void log() {
    
    

    }

    /**
     * 前置操作
     *
     * @param point 切入点
     */
    @Before("log()")
    public void beforeLog(JoinPoint point) {
    
    
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        log.info("请求 URL:{}", request.getRequestURL());
        log.info("请求 IP:{}", request.getRemoteAddr());
        log.info("请求类名:{},请求方法名:{}", point.getSignature().getDeclaringTypeName(), point.getSignature().getName());
        log.info("body:{},", JSONUtil.toJsonStr(point.getArgs()));
        Map<String, String[]> parameterMap = request.getParameterMap();
        log.info("请求参数:{},", JSONUtil.toJsonStr(parameterMap));
        Long start = System.currentTimeMillis();
        request.setAttribute(START_TIME, start);
    }

    /**
     * 环绕操作
     *
     * @param point 切入点
     * @return 原方法返回值
     * @throws Throwable 异常信息
     */
    @Around("log()")
    public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
    
    
        Object result = point.proceed();
        log.info("返回值:{}", JSONUtil.toJsonStr(result));
        return result;
    }

    /**
     * 后置操作
     */
    @AfterReturning("log()")
    public void afterReturning() {
    
    
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

        Long start = (Long) request.getAttribute(START_TIME);
        Long end = System.currentTimeMillis();
        log.info("请求耗时:{}毫秒", end - start);

        String header = request.getHeader("User-Agent");
        UserAgent userAgent = UserAgent.parseUserAgentString(header);
        log.info("浏览器类型:{},操作系统:{},原始User-Agent:{}", userAgent.getBrowser().toString(), userAgent.getOperatingSystem().toString(), header);
    }
}

参考:
https://blog.csdn.net/zhangting19921121/article/details/124351092
https://my.oschina.net/lizaizhong/blog/1611165
https://blog.csdn.net/weixin_44195615/article/details/113650701
https://blog.csdn.net/qq_18671415/article/details/111866546
https://blog.csdn.net/mameng1988/article/details/85548812
https://blog.csdn.net/Weixiaohuai/article/details/122494670

猜你喜欢

转载自blog.csdn.net/weixin_44436677/article/details/128694375