使用aop和注解实现日志记录

问题描述:
    生产上遇到一个问题,就是第三方厂商调用我们服务创建数据库实例,后面创建成功后,因为某些条件不适合,又调用卸载接口进行卸载了。卸载后再次进行创建,创建成功。但是过了一周后,第三方厂商人员反馈创建的数据库实例集群映射的DNS域名的ip还是之前旧的删除的哪套的,不是最新的。那么我去通过前几天的日志定位问题。
    但是发现日志记录不全。所以我萌生了使用aop切面来记录请求和参数的想法,从而避免漏掉记录重要日志。
   于是就干起来了,实现主要有3点
 
1:打印日志的注解实现
@Target({ElementType.TYPE, ElementType.METHOD}) // 注解类型, 级别
@Retention(RetentionPolicy.RUNTIME) // 运行时注解
public @interface PrintLog {
 
 
    String module() default ""; //方法标识,比如赋值为 新增数据库实例
    boolean printParam() default true; //默认打印请求参数在日志中
 
 
}
 
 
2:切面定义
 
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
 
import java.util.HashMap;
import java.util.Map;
 
 
/**
* @author mengfl
* @Description:
* @date 2021/9/184:54 下午
*/
@Aspect // 切面标识
@Component // 交给spring容器管理
@Slf4j
 
 
public class PrintLogAspect {
 
 
    @Autowired
 
 
    /**
     * 选取切入点为自定义注解
     */
    @Pointcut("@annotation(cn.com.huacloud.cdd.annotation.PrintLog)")
    public void PrintLog(){}
 
 
    @Around(value="PrintLog()")
    public Object printlog(ProceedingJoinPoint joinPoint) throws Throwable {
 
 
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
 
 
        PrintLog printLog = methodSignature.getMethod().getDeclaredAnnotation(PrintLog.class);
        String module = printLog.module();
        if(StringUtils.isEmpty(module)){
            //获取当前方法类名
            String classname = joinPoint.getTarget().getClass().getName();
            //获取方法名称
            String methodName= methodSignature.getMethod().getName();
//            如果被代理的方法没有给module赋值,那么按照下面方式组装,以方便在日志打印出来后,知道被打印出来的日志
//            属于哪个类的哪个方法
            module = classname+":"+methodName;
        }
        String paramsString = "";
        // 是否要打印方法的参数数据,默认打印
        if (printLog.printParam()) {
            // 参数名
            String[] paramNames = methodSignature.getParameterNames();
            if (paramNames != null && paramNames.length > 0) {
                // 参数值
                Object[] args = joinPoint.getArgs();
                Map<String, Object> params = new HashMap<>();
                for (int i = 0; i < paramNames.length; i++) {
                    Object value = args[i];
                    params.put(paramNames[i], value);
                }
                // 以json的形式记录参数
                paramsString = JSONObject.toJSONString(params);
            }
        }
        String username = "";
        try{
            LoginUser loginUser = SecurityUtil.getLoginUser();
            //当前登录用户名称
            username = loginUser.getNickName();
        }catch(Exception e){
            log.info("切面获取用户异常!");
            e.printStackTrace();
        }
        try {
 
 
            String startTime = DateUtil.now();
            //统一打印执行前日志
            log.info(StrUtil.format("当前时间{},用户{}请求进入方法模块{}开始,请求参数为{}!", startTime,username, module,paramsString));
            // 执行原方法
            Object object = joinPoint.proceed();
            //统一打印执行后日志
            String endTime = DateUtil.now();
            log.info(StrUtil.format("当前时间{},用户{}请求进入方法模块{}结束,请求参数为{}", endTime,username, module,paramsString));
 
 
            return object;
        } catch (Exception e) {
            // 备注记录失败原因
            String exceptionTime = DateUtil.now();
            log.info(StrUtil.format("异常时间{},用户{}请求进入方法模块{}异常,请求参数为{},异常原因是{}",exceptionTime,username, module,paramsString, e));
            throw e;
        }
    }
 
 
 
 
}
 
 
 
 
3:注解使用方式
@PrintLog(module = "新增yaml文件",printParam = true)
public R<Void> addYaml(@RequestBody @Validated({YamlDto.Add.class,YamlDto.Update.class}) YamlDto yamlDto){
    yamlService.addYaml(yamlDto);
    return R.ok();
}
这样你在调用queryIMysqlInstance方法前后就会输出日志了。
代码输出日志如下:
2021-09-18 23:59:46.426 nacos [TID: N/A] [http-nio-8091-exec-1] INFO  c.c.h.cdd.aspect.PrintLogAspect line:87  |
 
当前时间2021-09-18 23:59:44,用户超级管理员请求进入方法模块cn.com.huacloud.cdd.k8s.controller.YamlController:addYaml开始,请求参数为{"yamlDto":{"content":"test2","description":"test2","id":0,"kind":"test2","lastVersionId":"100","templateName":"test2","versionId":"100"}}!
 
 
 
 
备注:
在AOP编程中,我们经常会遇到下面的概念:
 

连接点 - Joinpoint

连接点是指程序执行过程中的一些点,比如方法调用,异常处理等。在 Spring AOP 中,仅支持方法级别的连接点。以上是官方说明,通俗地讲就是能够被拦截的地方,每个成员方法都可以称之为连接点。
 
接下来我们看看连接点的定义:
public interface JoinPoint {
    String METHOD_EXECUTION = "method-execution";
    String METHOD_CALL = "method-call";
    String CONSTRUCTOR_EXECUTION = "constructor-execution";
    String CONSTRUCTOR_CALL = "constructor-call";
    String FIELD_GET = "field-get";
    String FIELD_SET = "field-set";
    String STATICINITIALIZATION = "staticinitialization";
    String PREINITIALIZATION = "preinitialization";
    String INITIALIZATION = "initialization";
    String EXCEPTION_HANDLER = "exception-handler";
    String SYNCHRONIZATION_LOCK = "lock";
    String SYNCHRONIZATION_UNLOCK = "unlock";
    String ADVICE_EXECUTION = "adviceexecution";
    String toString();
    String toShortString();
    String toLongString();
    //获取代理对象
    Object getThis();
    /**
    返回目标对象。该对象将始终与target切入点指示符匹配的对象相同。除非您特别需要此反射访问,否则应使用        target切入点指示符到达此对象,以获得更好的静态类型和性能。
    如果没有目标对象,则返回null。
    **/
    Object getTarget();
    //获取传入目标方法的参数对象
    Object[] getArgs();
    //获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
    Signature getSignature();
    /**
    返回与连接点对应的源位置。
    如果没有可用的源位置,则返回null。
    返回默认构造函数的定义类的SourceLocation。
    **/
    SourceLocation getSourceLocation();
    String getKind();
    JoinPoint.StaticPart getStaticPart();
    public interface EnclosingStaticPart extends JoinPoint.StaticPart {
    }
    //该帮助对象仅包含有关连接点的静态信息。它可以从JoinPoint.getStaticPart()方法中获得,也可以使用特殊形式在建议中单独访问 thisJoinPointStaticPart。
    public interface StaticPart {
        Signature getSignature();
        SourceLocation getSourceLocation();
        String getKind();
        int getId();
        String toString();
        String toShortString();
        String toLongString();
    }
}
JoinPoint 接口中常用 api 有:getSignature()、 getArgs()、 getTarget() 、 getThis() 。但是我们平时使用并不直接使用 JoinPoint 的实现类,中间还有一个接口实现,叫做 ProceedingJoinPoint,其定义如下:
public interface ProceedingJoinPoint extends JoinPoint {
    void set$AroundClosure(AroundClosure var1);
    //执行目标方法
    Object proceed() throws Throwable;
    //传入的新的参数去执行目标方法
    Object proceed(Object[] var1) throws Throwable;
}
 
  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:增强,指特定连接点上执行的动作;
  • Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
  • Weaving:织入,指将切面整合到程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式;
  • Target Object:目标对象,即真正执行业务的核心逻辑对象;
  • AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。
 
引申思考:
这里提到了代理模式,后续文章我会对这里如何使用代理模式进行重点阐述。
 
参考文献:
 
《Spring AOP核心概念》https://zhuanlan.zhihu.com/p/109741656
posted @ 2021-09-18 19:07  kurl88  阅读(250)  评论(0)    收藏  举报