Spring AOP统一处理日志

添加依赖:

<!--springboot版本为2.3.3.RELEASE-->
<!-- aop切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--Caused by: java.lang.NoClassDefFoundError: com/fasterxml/jackson/datatype/jsr310/ser/ZoneIdSerializer-->
<!--需要调高版本-->
<!--jackson-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.2</version>
    <!--<version>2.9.1</version>-->
</dependency>

AOP获取请求接口所有信息(入参类型、参数、执行时间、当前方法路径、响应参数、响应数据类型)

1、自定义注解

用于标识某个接口方法需要记录日志

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRequired {

}

2、日志输出实体

@Data
@AllArgsConstructor
public class LiveLog {
    // 请求路径
    private String requestPath;
    // 请求方法
    private String requestMethod;
    // 当前的时间戳
    private Long currentTimeMills;
    // 参数类型
    private Map<String, String> argsType;
    // 所有入参
    private List<Object> allArgs;
    // 响应数据
    private String responseData;
    // 当前执行耗时
    private String executeTimeMills;
    // 当前请求的方法路径:(类路径 + 方法名字)
    private String classMethodLocation;
    // 请求的IP
    private String remoteAddr;
    // 当前日期(yyyy-MM-dd HH:mm:ss)
    private String nowTime;
    // 响应数据类型
    private String responseType;
}

3、AOP切面类

import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
@Aspect
public class LiveLogAspect {

    private static final Logger logger = LoggerFactory.getLogger(LiveLogAspect.class);
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 指定切面对象,Controller类中所有方法
     */
    @Pointcut("execution(* com.harvey..*.controller..*.*(..)))")
    private void logPoint() {
    }

    /**
     * 打印出入参记录
     *
     * @param joinPoint 切面连接点对象
     */
    @Around("logPoint()")
    public Object processLog(ProceedingJoinPoint joinPoint) throws Throwable {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = sra.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogRequired logRequired = method.getAnnotation(LogRequired.class);
        // 该接口方法没有加LogRequired注解,表示不记录日志
        if (ObjectUtils.isEmpty(logRequired)) {
            return joinPoint.proceed();
        }
        String requestURI = request.getRequestURI();
        String requestMethod = request.getMethod();
        String remoteAddr = request.getRemoteAddr();

        Long startTime = System.currentTimeMillis();// 步入时间戳
        Class<?> currentClass = joinPoint.getTarget().getClass();
        Object proceed = joinPoint.proceed();
        List<Object> allArgs = Arrays.asList(joinPoint.getArgs());
        List<Object> args = allArgs.stream().map(arg -> {
            if (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse)) {
                return arg;
            } else {
                return null;
            }
        }).filter(arg -> arg != null).collect(Collectors.toList());

        LiveLog liveLog = new LiveLog(requestURI, requestMethod, startTime,
                getMethodArgumentTypeName(signature), args, proceed != null ? proceed.toString() : "null",
                (System.currentTimeMillis() - startTime) + "ms", currentClass.getName() + "." + method.getName(),
                remoteAddr, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), method.getReturnType().getName());

        logger.info(objectMapper.writeValueAsString(liveLog));
        return proceed;
    }

    @AfterThrowing(value = "logPoint()")
    private void AfterThrowing() {
        logger.info("异常通知");
    }

    private Map<String, String> getMethodArgumentTypeName(MethodSignature method) {
        Map<String, String> map = new HashMap<>();
        String[] argTypeNames = method.getParameterNames();
        Class[] parameterTypes = method.getParameterTypes();
        for (int i = 0; i < parameterTypes.length; i++) {
            map.put(parameterTypes[i].getName(), argTypeNames[i]);
        }
        return map;
    }
}

请求日志唯一追踪ID

由于入参日志和出参日志是分开打印的,而logback底层是异步处理机制,所以需要一个唯一ID标识确定某些出入参日志属于同一个请求,并且把这个唯一标识返回给前端,方便异常追踪。你可能会说,出入参打印为一条日志不就行了么,在AOP @Around注解方法中是可以做到的。但是,Controller层切面和ControllerAdvice的日志记录就无法打印成一条了。所以,单独设置一个追踪ID是有必要的。

1、logback.xml日志输出格式修改

日志文件所有的appender标签输出表达式增加一个traceId占位符

<appender>
    <encoder>
        <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread] [%X{traceId}] %logger{10}:%line %msg%n</pattern>
        <charset>utf-8</charset>
    </encoder>
</appender>

2、过滤器设置traceId

import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

public class LogMDCFilter implements Filter {

    public static final String TRACE_ID = "traceId";

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        MDC.put(TRACE_ID, UUID.randomUUID().toString().replace("-", ""));
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            MDC.remove(TRACE_ID);
        }
    }
}

3、配置过滤器

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<LogMDCFilter> logFilterRegistration() {
        FilterRegistrationBean<LogMDCFilter> registration = new FilterRegistrationBean<>();
        // 注入过滤器
        registration.setFilter(new LogMDCFilter());
        // 拦截规则
        registration.addUrlPatterns("/*");
        // 过滤器名称
        registration.setName("logMDCFilter");
        // 过滤器顺序
        registration.setOrder(0);
        return registration;
    }
}

4、统一返回对象增加traceId

/**
  * 异常响应日志追踪ID
  */
private String traceId = MDC.get(LogMDCFilter.TRACE_ID);

 

posted @ 2022-06-11 14:46  残城碎梦  阅读(396)  评论(0编辑  收藏  举报