手把手教你搭建公司日志打印组件

153 阅读4分钟

前言

各位朋友有没有在工作中遇到一个这样的情况呢,老大要求你对每个接口的请求、响应信息进行日志打印或者记录,以便上线后问题的快速定位已排查,还可能要求你对每个接口进行请求时间的计算和显示。这时候你可能会想到利用注解配合AOP进行日志的打印。于是你就在你们项目的代码中添加了日志打印的相关的注解和切面逻辑代码,但是你发现,当你又开始另外一个项目的时候,好像又得重新编写那部分代码。秉承着不重复造轮子的思想,我们可以自己写一个日志打印的基础组件啊。

那么问题又来了,怎么写一个公用的组件呢?这个时候就要提到大家熟知的SpringBoot Starter了

何为 Starter ?

在 SpringBoot 项目中,使用最多的无非就是各种各样的 Starter 了。那何为 Starter 呢?你可以理解为一个可拔插式的插件(组件),或者理解为场景启动器。

通过 Starter,能够简化以前繁杂的配置,无需过多的配置和依赖,它会帮你合并依赖,并且将其统一集成到一个 Starter 中,我们只需在 Maven 或 Gradle 中引入 Starter 依赖即可。SpringBoot 会自动扫描需要加载的信息并启动相应的默认配置。例如,如果你想使用 jdbc 插件,你只需引入 spring-boot-starter-jdbc 即可;如果你想使用 mongodb,你只需引入 spring-boot-starter-data-mongodb 依赖即可。

SpringBoot 官方提供了大量日常企业应用研发各种场景的 spring-boot-starter 依赖模块。这些依赖模块都遵循着约定成俗的默认配置,并允许我们根据自身情况调整这些配置。

总而言之,Starter 提供了以下功能:

  • 整合了模块需要的所有依赖,统一集合到 Starter 中。
  • 提供了默认配置,并允许我们调整这些默认配置。
  • 提供了自动配置类对模块内的 Bean 进行自动装配,注入 Spring 容器中。

如何写一个日志打印 Starter?

既然要自定义一个Starter,那可不得起一个响亮的名字,niubi-starter?不不不,还是遵循一下权威机构的命名规则。Spring 官方定义的 Starter 通常命名遵循的格式为 spring-boot-starter-{name},例如 spring-boot-starter-data-mongodb。Spring 官方建议,非官方 Starter 命名应遵循 {name}-spring-boot-starter 的格式,例如,myjson-spring-boot-starter。那我们就起一个规范点的名字吧,叫mylog-spring-boot-starter。

名字起好了,接下来全是重点了。

第一步 建项目

image.png

第二步 改pom

添加依赖,依赖的版本可以从父项目继承,也可以自己指定。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.7</version>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
    </dependency>

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

第三步 写代码

①新建注解

package com.ganjunhao.springboot.starter.mylog.annotation;

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

/**
 * Log 注解打印,可以标记在类或者方法上
 * 标记在类上,类下所有方法都会打印
 * 标记在方法上,仅打印标记方法
 * 如果类或者方法上都有标记,以方法上注解为准
 *
 * @author ganjunhao
 * @date 2023/6/5 10:30
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {

    /**
     * 入参打印
     *
     * @return 打印结果中是否包含入参
     */
    boolean inPrint() default true;

    /**
     * 出参打印
     *
     * @return 打印结果中是否包含出参
     */
    boolean outPrint() default true;
}

②新建切面

package com.ganjunhao.springboot.starter.mylog.aspect;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.SystemClock;
import com.alibaba.fastjson2.JSON;
import com.ganjunhao.springboot.starter.mylog.annotation.MyLog;
import lombok.Data;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Optional;

/**
 * 日志打印AOP切面
 *
 * @author ganjunhao
 * @date 2023/6/5 10:30
 */
@Aspect
public class MyLogPrintAspect {

    /**
     * 类或方法上生效
     */
    @Around("@within(com.ganjunhao.springboot.starter.mylog.annotation.MyLog) || @annotation(com.ganjunhao.springboot.starter.mylog.annotation.MyLog)")
    public Object printMyLog(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = SystemClock.now();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Logger log = LoggerFactory.getLogger(methodSignature.getDeclaringType());
        String beginTime = DateUtil.now();
        Object result = null;
        try {
            result = joinPoint.proceed();
        } finally {
            Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
            MyLog logAnnotation = Optional.ofNullable(targetMethod.getAnnotation(MyLog.class)).orElse(joinPoint.getTarget().getClass().getAnnotation(MyLog.class));
            if (logAnnotation != null) {
                MyLogPrint logPrint = new MyLogPrint();
                logPrint.setBeginTime(beginTime);
                if (logAnnotation.inPrint()) {
                    logPrint.setInputParams(buildInPrint(joinPoint));
                }
                if (logAnnotation.outPrint()) {
                    logPrint.setOutputParams(result);
                }
                String methodType = "", requestURI = "";
                try {
                    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                    methodType = servletRequestAttributes.getRequest().getMethod();
                    requestURI = servletRequestAttributes.getRequest().getRequestURI();
                } catch (Exception ignored) {
                }
                log.info("[{}] {}, executeTime: {}ms, info: {}", methodType, requestURI, SystemClock.now() - startTime, JSON.toJSONString(logPrint));
            }
        }
        return result;
    }

    private Object[] buildInPrint(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Object[] printArgs = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if ((args[i] instanceof HttpServletRequest) || args[i] instanceof HttpServletResponse) {
                continue;
            }
            if (args[i] instanceof byte[]) {
                printArgs[i] = "byte array";
            } else if (args[i] instanceof MultipartFile) {
                printArgs[i] = "file";
            } else {
                printArgs[i] = args[i];
            }
        }
        return printArgs;
    }

    @Data
    private class MyLogPrint {

        private String beginTime;

        private Object[] inputParams;

        private Object outputParams;
    }
}

③新建配置类

package com.ganjunhao.springboot.starter.mylog.config;

import com.ganjunhao.springboot.starter.mylog.aspect.MyLogPrintAspect;
import org.springframework.context.annotation.Bean;

/**
 * @author ganjunhao
 * @date 2023/6/5 10:30
 */
public class LogAutoConfiguration {

    /**
     * 日志打印AOP切面
     */
    @Bean
    public MyLogPrintAspect myLogPrintAspect() {
        return new MyLogPrintAspect();
    }
}

④在resources/META-INF目录下创建spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ganjunhao.springboot.starter.mylog.config.LogAutoConfiguration

最后可以参考我创建的目录结构进行调整

image.png

第四步 install

将项目打包到本地maven私仓或者部署到远程仓库即可

image.png

第五步 使用

①在其他项目pom中引入组件依赖

<dependency>
    <groupId>com.ganjunhao</groupId>
    <artifactId>mylog-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

②将@MyLog注解加在类上或者方法上

至此日志打印公共组件就算完成了。 如果有什么有问题或者建议可以在评论区留言。^_^

相关代码放置在作者的代码仓库:github.com/gan87906561…