Springboot 通过Aop + 自定义注解来实现日志的记录

发布时间 2023-11-30 12:43:10作者: 潇潇love涛涛

一、AOP

这是一个Java面试题老生常谈的问题,下面我就来简单说一下什么是AOP。

1.1 什么是AOP

AOP(Aspect Oriented Programming)是一个面向切面编程的思想,是对OOM(Object-Oriented Model)的一种补充,它可以不修改源码的方式来增强代码。这样说可能有点抽象,下面来举个例子

比如,现在有上面这么多模块的代码,我们现在需要在每个代码中来增加日志和事务的功能。
传统的方法,肯定是在每个模块的对应的方法中添加对应的功能,而AOP则是在模块中找到切点,把日志和事务的方法织入到对应的方法中。这就是AOP

1.2 名词解释

AOP 有自己的一套术语,我们必须了解一下这些行话,才能更好地理解 AOP。

1.通知 (Advice)

AOP在特定的切入点上具体执行哪些方法,什么时候执行。大致分为before、after、afterReturning、afterThrowing、around

2.连接点 (JoinPoint)

哪些方法需要被AOP增强,这些方法就叫做连接点。

3.切点(PointCut)

切点用于定义切面的位置,也就是捕获哪些连接点的调用然后执行"通知"的操作(什么地点)。

4.切面(Aspect)

AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。

5.目标对象( Target )

就是被增强的对象

6.织入(Weaving)

织入是把切面应用到切点对应的连接点的过程。切面在指定连接点被织入到目标对象中。

1.3 通知类型

通知(advice)是你在你的程序中想要应用在其他模块中的横切关注点的实现。Advice主要有以下5种类型:

  • 前置通知(Before Advice):在连接点之前执行的Advice,不过除非它抛出异常,否则没有能力中断执行流。使用@Before注解使用这个Advice。
  • 返回之后通知(After Retuning Advice):在连接点正常结束之后执行的Advice。例如,如果一个方法没有抛出异常正常返回。通过 @AfterReturning注解使用它。
  • 抛出(异常)后执行通知(After Throwing Advice):如果一个方法通过抛出异常来退出的话,这个Advice就会被执行。通过 @AfterThrowing注解来使用。
  • 后置通知(After Advice):无论连接点是通过什么方式退出的(正常返回或者抛出异常)都会执行在结束后执行这些Advice。通过 @After注解使用。
  • 围绕通知(Around Advice):围绕连接点执行的Advice,就你一个方法调用。这是最强大的Advice。通过@Around注解使用。

1.4 AOP底层原理

它是基于代理设计模式,而代理设计模式又分为静态代理和动态代理,静态代理比较简单就是一个接口,分别由一个真实实现和一个代理实现,而动态代理分为基于接口的JDK动态代理和基于类的CGLIB的动态代理。

二、自定义注解

下面就以我们实际开发为例,我们先需要给每个接口加上注解,并获取接口的信息。

2.1 创建注解

/**
 * 统一日志注解
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnnotation {

    /**
     * value : 目标url,如:“/open/api/cif/sync/lessee”
     * @return
     */
    String value() default "";

    /**
     * type : 接口类型,如:“in”
     * @return
     */
    String type() default "";

    /**
     * target : 目标资方 获取资方枚举
     * @return
     */
    String target() default "";
}

@Retention: 表示该注解的生命周期,是RetentionPolicy类型的,该类型是一个枚举类型,可提供三个值选择,分别是:CLASS、RUNTIME、SOURCE

  • RetentionPolicy.CLASS: 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
  • RetentionPolicy.RUNTIME: 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
  • RetentionPolicy.SOURCE: 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃; 由此可见生命周期关系:SOURCE < CLASS < RUNTIME,我们一般用RUNTIME
    @Target: 表示该注解的作用范围,是ElementType类型的,该类型是一个枚举类型,一共提供了10个值选择,我们最常用的几个:FIELD、TYPE、PARAMETER、METHOD
  • ElementType.FIELD:用于字段、枚举的常量
  • ElementType.TYPE:用于接口、类、枚举、注解
  • ElementType.PARAMETER:用于方法形参:值传递不是简单的把实参传递给形参,而是,实参建立了一个副本,然后把副本传递给了形参,所以形参改变并不会影响到实参
  • ElementType.METHOD:用于方法

2.2 定义注解行为

这一步就是我们需要如何去处理我们的注解,这里面有五个方法,分别是@Before、@after、@Around、AfterReturning、AfterThrowing,我们常用的一般是前三个,看具体需求选择适合自己的方式。

import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.shaded.com.google.gson.Gson;
import com.midea.mhpdp.mbf.api.request.CommonReq;
import com.midea.mhpdp.mbf.api.response.Result;
import com.midea.mhpdp.mic.api.constant.common.aop.LogAnnotation;
import com.midea.mhpdp.mic.app.aop.request.LogAnnotationCmdReq;
import com.midea.mhpdp.mic.app.token.command.service.LogAdviceCmdAppService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author LayTao
 * @date 2023/11/28
 * @description 日志注解类的AOP实现
 */
@Component
@Aspect
@Slf4j
public class LogAdvice
{

    @Resource
    private LogAdviceCmdAppService logAdviceCmdAppService;

    @Around(value = "execution(* com.midea.mhpdp.mic.facade..*.*(..))")
    public Object savaLog(ProceedingJoinPoint joinPoint) throws Throwable{
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        LogAnnotation logAnnotation = methodSignature.getMethod().getDeclaredAnnotation(LogAnnotation.class);

        // 获取请求
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        assert sra != null;
        HttpServletRequest request = sra.getRequest();
        String url = request.getRequestURL().toString();
        String method = request.getMethod();

        Enumeration<String> headerNames = request.getHeaderNames();
        List<String> headList = new ArrayList<>();
        while (headerNames.hasMoreElements()){
            String headValue = request.getHeader(headerNames.nextElement());
            headList.add(headerNames.nextElement()+":"+headValue);
        }


        // 过滤掉部分请求参数,方法参数为HttpServletRequest HttpServletResponse MultipartFile
        List<String> filterList = Arrays.asList(joinPoint.getArgs()).stream().map(obj ->{
            String objStr =  parseParams(obj);
            return objStr;
        }).collect(Collectors.toList());

        // 请求参数
        String queryString = filterList.size() > 1 ? filterList.stream().collect(Collectors.joining(";")) :
                (filterList.size() == 1 ? filterList.get(0) : "");

        // 请求头
        String queryHead = headList.size() > 1 ? headList.stream().collect(Collectors.joining(";")) :
                (headList.size() == 1 ? headList.get(0) : "");

        //执行方法
        Object object = joinPoint.proceed();

        // 响应结果
        int response = parseResponse(object);

        LogAnnotationCmdReq cmdReq = null;
        if(Objects.isNull(logAnnotation)){
            // 如果没有加@LogAnnotation 不记录日志 直接执行方法
            cmdReq = LogAnnotationCmdReq.builder()
                    .object(object.toString())
                    .joinPoint(joinPoint)
                    .queryString(queryString)
                    .method(method)
                    .head(queryHead)
                    .isSuccess(response)
                    .build();
        }else{
            cmdReq = LogAnnotationCmdReq.builder()
                    .url(logAnnotation.value())
                    .type(logAnnotation.type())
                    .targetSys(logAnnotation.target())
                    .object(object.toString())
                    .joinPoint(joinPoint)
                    .queryString(queryString)
                    .method(method)
                    .head(queryHead)
                    .isSuccess(response)
                    .build();
        }

        // 保存接口日志信息
        logAdviceCmdAppService.saveIntegrateLogInfo(cmdReq);
        return object;
    }


    /**
     * 解析响应结果
     * @return
     */
    public int parseResponse (Object obj){
        if(obj == null){
            return 1;
        }
        String json = "";
        Gson gson = new Gson();
        JSONObject parseObject = JSONObject.parseObject(gson.toJson(obj, Result.class));
        String code = parseObject.getString("code");
        JSONObject data = parseObject.getJSONObject("data");
        if(!StringUtils.isEmpty(code) && code.startsWith("9")){
            return 1;
        }
        if( !StringUtils.isEmpty(data) && !code.startsWith("9") && data.getString("code").equals("0")){
            return 2;
        }

        return 1;
    }

    /**
     *  解析参数
     * @return
     */
    public String parseParams (Object obj){
        if(obj == null){
            return "";
        }
        String json = "";
        // 如果参数是CommonReq封装参数
        if(obj.toString().contains("CommonReq")){
            Gson gson = new Gson();
            JSONObject parseObject = JSONObject.parseObject(gson.toJson(obj, CommonReq.class));
            json = parseObject.getString("restParams");
        }else{
            json = obj.toString();
        }
        return json;
    }
}

2.3 实现日志落库

/**
 * @author LayTao
 * @date 2023/11/28
 * @description 接口日志信息Ability
 */
@Slf4j
@DomainService(id="LogAdviceAbility",domainServiceName = "LogAdviceAbility",domainServiceDes = "接口日志信息Ability")
public class LogAdviceAbility
{

    @Value("${hxfl.gateway:https://dcms-test.hxfl.com.cn:38080}")
    private String gateway;

    @Autowired
    private LogAdviceOps logAdviceOps;

    /**
     * 保存接口日志信息
     *
     * dmReq
     */
    public void saveIntegrateLogInfo(LogAdviceInfoDmReq dmReq)
    {
        // 接口URL
        String url = gateway + dmReq.getUrl();

        IntegrateInoutRecordPo recordPo = IntegrateInoutRecordPo.builder()
                .recordType(StringUtils.isEmpty(dmReq.getType()) ? "" : dmReq.getType())
                .bizCode(StringUtils.isEmpty(dmReq.getUrl()) ? "" : dmReq.getUrl())
                .reqProtocol("Http")
                .httpMethod(dmReq.getMethod())
                .httpHeader(dmReq.getHead())
                .targetUrl(StringUtils.isEmpty(dmReq.getUrl()) ? "" : url)
                .paramJsonStr(dmReq.getQueryString())
                .responseJsonStr(dmReq.getObject())
                .isSuccess(dmReq.getIsSuccess())
                .targetSys(dmReq.getTargetSys())
                .createdBy("system")
                .createTime(DateUtil.getDateTime())
                .build();

        logAdviceOps.saveLogInfo(recordPo);

    }
}

这样就实现了 接口日志功能