接口日志,统一记录(AOP+自定义注解)

需求

指定接口,记录请求的日志。

接口日志的核心内容包括:请求方法,接口路径,请求参数等。

方案

采用的方案是:AOP + 自定义注解

说明:

  1. 在需要记录日志的接口上,加上自定义注解@ApiLog,则此接口的请求所包含的信息,会被记录到日志;
  2. 提供开关配置,可以选择是否开启接口日志;
  3. 接口日志的记录方式,推荐使用消息队列(比如:RocketMQ),异步处理;将接口的日志发送到消息队列里,由专门的日志记录服务器去处理,比如写入专门的数据库。这样可以减少接口的同步处理的时间,避免客户端等待时间过长,提升总体性能。本文仅为示例,所以只做了最简单的日志打印。

核心代码

注解:@ApiLog

package com.example.core.log.annotation;

import java.lang.annotation.*;

/**
 * 接口日志注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {
}

切面:日志记录逻辑

package com.example.core.log.aspect;

import com.example.core.property.BaseFrameworkConfigProperties;
import com.example.core.util.JsonUtil;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
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.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Aspect
@Order(20)
@Component
public class ApiLogAspect {

    @Value("${spring.application.name:}")
    private String applicationName;

    private final BaseFrameworkConfigProperties properties;


    public ApiLogAspect(BaseFrameworkConfigProperties properties) {
        this.properties = properties;
    }


    // 定义一个切点:所有被 ApiLog 注解修饰的方法会织入advice
    @Pointcut("@annotation(com.example.core.log.annotation.ApiLog)")
    private void pointcut() {
    }


    // Before表示 advice() 将在目标方法执行前执行
    @Before("pointcut()")
    public void advice(JoinPoint joinPoint) {

        if (!properties.getApiLog().isEnabled()) {
            return;
        }

        log.info("\n-------------------- 接口日志,开始 --------------------");

        log.info("applicationName:{}", applicationName);

        // 获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();

            // 用户IP
            String clientIp = request.getRemoteAddr();
            log.info("clientIp:{}", clientIp);

            // URL
            String requestURL = request.getRequestURL().toString();
            log.info("url:{}", requestURL);

            // 请求方法
            String requestMethod = request.getMethod();
            log.info("requestMethod:{}", requestMethod);

            // 接口路径
            String path = request.getServletPath();
            log.info("path:{}", path);
        }

        // 接口参数
        Object[] args = joinPoint.getArgs();
        // 获取有效的接口参数(排除 HttpServletRequest 和 HttpServletResponse,否则会导致接口卡死)
        List<Object> validArgs = Stream.of(args).filter(this::isInclusiveArgument).collect(Collectors.toList());
        log.info("args:{}", JsonUtil.toJson(validArgs));

        // 方法签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        log.info("methodSignature:{}", methodSignature);

        // 方法参数名称列表
        String[] parameterNames = methodSignature.getParameterNames();
        log.info("parameterNames:{}", JsonUtil.toJson(parameterNames));

        // 获取接口的注解
        Operation operation = methodSignature.getMethod().getAnnotation(Operation.class);
        if (operation != null) {
            // 接口概述
            String summary = operation.summary();
            log.info("summary:{}", summary);

            // 接口描述
            String description = operation.description();
            log.info("description:{}", description);
        }

        log.info("\n-------------------- 接口日志,结束 --------------------\n");
    }


    /**
     * 是需要包含的参数。

* 不需要包含的参数(会导致接口卡死):
* 1. HttpServletRequest
* 2. HttpServletResponse * * @param arg 参数对象 */
private Boolean isInclusiveArgument(Object arg) { return !(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse); } }

日志开关配置

package com.example.core.property;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


/**
 * BaseFramework 配置文件
 *
 * @author songguanxun
 * 2019/08/27 15:40
 * @since 1.0.0
 */
@Data
@Component
@ConfigurationProperties(prefix = "base-framework")
public class BaseFrameworkConfigProperties {

    /**
     * 接口日志配置
     */
    private ApiLog apiLog = new ApiLog();

    /**
     * 接口日志配置
     */
    @Data
    public static class ApiLog {

        /**
         * 是否开启接口日志
         */
        private boolean enabled = false;

    }

}

配置(yml)

# 自定义配置
base-framework:
  api-log:
    enabled: true

调用示例代码

package com.example.web.exception.controller;

import com.example.core.log.annotation.ApiLog;
import com.example.core.model.PageQuery;
import com.example.web.exception.query.UserQuery;
import com.example.web.model.vo.UserVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("exception")
@Tag(name = "异常统一处理")
public class ExceptionController {


    @ApiLog
    @GetMapping(path = "users")
    @Operation(summary = "查询用户列表", description = "测试:BindException。参数校验异常:Get请求,Query参数,以对象的形式接收。")
    public List<UserVO> listUsers(@Valid UserQuery userQuery, PageQuery pageQuery,
                                  HttpServletRequest request, HttpServletResponse response, HttpSession session) {
        log.info("查询用户列表。userQuery={},pageQuery={}", userQuery, pageQuery);

        String queryName = userQuery.getName();
        String queryPhone = userQuery.getPhone();

        return listMockUsers().stream().filter(user -> {
            boolean isName = true;
            boolean isPhone = true;
            if (StringUtils.hasText(queryName)) {
                isName = user.getName().contains(queryName);
            }
            if (StringUtils.hasText(queryPhone)) {
                isPhone = user.getPhone().contains(queryPhone);
            }
            return isName && isPhone;
        }).collect(Collectors.toList());
    }


    private List<UserVO> listMockUsers() {
        List<UserVO> list = new ArrayList<>();

        UserVO vo = new UserVO();
        vo.setId("1234567890123456789");
        vo.setName("张三");
        vo.setPhone("18612345678");
        vo.setEmail("[email protected]");
        list.add(vo);

        UserVO vo2 = new UserVO();
        vo2.setId("1234567890123456781");
        vo2.setName("李四");
        vo2.setPhone("13412345678");
        vo2.setEmail("[email protected]");
        list.add(vo2);

        return list;
    }

}

调用效果

接口调用

接口日志,统一记录(AOP+自定义注解)_第1张图片

控制台日志

接口日志,统一记录(AOP+自定义注解)_第2张图片

你可能感兴趣的:(Spring,Boot,spring,boot,接口日志)