当前位置:   article > 正文

【实战教程】使用Spring AOP和自定义注解监控接口调用

【实战教程】使用Spring AOP和自定义注解监控接口调用

一、背景

随着项目的长期运行和迭代,积累的功能日益繁多,但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。

为了提高系统性能和代码质量,我们往往需要对那些不常用的功能进行下线处理。

那么,该下线哪些功能呢?

此时,我们就需要对接口的调用情况进行统计和分析了!

二、实战

以下内容为主要代码,完整代码请参考:https://gitee.com/regexpei/daily-learning-test

以下使用 自定义注解 + AOP 的方式,对接口调用进行记录。

1. 创建项目,添加依赖

<dependencies>  
    <!-- 提供自动配置、日志、YAML等核心功能 -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter</artifactId>  
    </dependency>  
    
    <!-- 提供面向切面编程支持 -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-aop</artifactId>  
    </dependency>  
  
    <!-- 用于构建Web,包括RESTful和基于Servlet的Web应用,包含了Spring MVC、Tomcat等 -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
  
    <!-- 通过注解减少样板代码的Java库,自动生成getter、setter等方法 -->  
    <dependency>  
        <groupId>org.projectlombok</groupId>  
        <artifactId>lombok</artifactId>  
        <optional>true</optional>  
    </dependency>  
  
    <!-- Swagger的注解库,允许开发者为API添加文档和元数据 -->  
    <dependency>  
        <groupId>io.swagger</groupId>  
        <artifactId>swagger-annotations</artifactId>  
        <version>1.6.2</version>  
    </dependency>  
  
    <!-- 用于Java对象的JSON序列化/反序列化的库,Fastjson的继任者 -->  
    <dependency>  
        <groupId>com.alibaba.fastjson2</groupId>  
        <artifactId>fastjson2</artifactId>  
        <version>2.0.41</version>  
    </dependency>  
  
    <!-- 为Spring Boot应用提供了测试所需的依赖项,包括JUnit等,但仅限于测试阶段 -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
        <exclusions>  
            <!-- 排除已包含的SLF4J API版本,避免版本冲突 -->  
            <exclusion>  
                <groupId>org.slf4j</groupId>  
                <artifactId>slf4j-api</artifactId>  
            </exclusion>  
        </exclusions>  
    </dependency>  
  
    <!-- Java工具包,提供了许多实用的工具类和方法 -->  
    <dependency>  
        <groupId>cn.hutool</groupId>  
        <artifactId>hutool-all</artifactId>  
        <version>5.8.25</version>  
    </dependency>  
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

2. 自定义注解和实体类

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface ApiOprLogAnno {

    @ApiModelProperty(value = "接口类型")
    String apiType() default "";

    @ApiModelProperty(value = "接口说明")
    String apiDetail() default "";

    @ApiModelProperty(value = "是否保存请求参数")
    boolean isSaveRequest() default false;

    @ApiModelProperty(value = "是否保存响应结果")
    boolean isSaveResponse() default false;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
@Setter
@Getter
public class ApiOprLog {

    @ApiModelProperty(name = "主键")
    private String id;

    @ApiModelProperty(name = "源IP")
    private String sourceIp;

    @ApiModelProperty(name = "用户名")
    private String username;

    @ApiModelProperty(name = "方法")
    private String method;

    @ApiModelProperty(name = "请求参数")
    private String reqParams;

    @ApiModelProperty(name = "响应结果")
    private String resResult;

    @ApiModelProperty(name = "异常信息")
    private String exMessage;

    @ApiModelProperty(name = "异常详细")
    private String exJson;

    @ApiModelProperty(name = "接口模块")
    private String apiModule;

    @ApiModelProperty(name = "接口类型")
    private String apiType;

    @ApiModelProperty(name = "接口说明")
    private String apiDetail;

    @ApiModelProperty(name = "创建时间")
    private Date createTime;

    @ApiModelProperty(name = "更新时间")
    private Date updateTime;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

3. 创建切面类

@Slf4j
@Aspect
@Component
public class ApiOprAspect {

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

    /**
     * 从请求中获取 IP
     *
     * @return IP
     */
    private static String getIpFromRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            HttpServletRequest request = (HttpServletRequest) requestAttributes
                    .resolveReference(RequestAttributes.REFERENCE_REQUEST);
            return IpUtil.getRealIp(request);
        }
        return Constants.UNKNOWN;
    }

    @Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLogAnno)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String id = IdUtil.fastSimpleUUID();
        Object result;
        try {
            // 执行方法前操作
            executeBefore(proceedingJoinPoint, id);
            result = proceedingJoinPoint.proceed();
            // 执行方法后操作
            executeAfter(proceedingJoinPoint, id, result);
        } catch (Throwable ex) {
            // 执行方法异常后操作
            executeAfterEx(ex, id);
            throw ex;
        }
        return result;
    }

    private void executeBefore(ProceedingJoinPoint proceedingJoinPoint, String id) {
        // 获取目标方法的签名信息
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        // 从方法签名中获取 ApiOprLogAnno 注解的信息
        ApiOprLogAnno apiOprLogAnno = signature.getMethod().getAnnotation(ApiOprLogAnno.class);

        // 封装 ApiOprLog 对象
        ApiOprLog apiOprLog = packaging(id, getIpFromRequest(), signature.toString(), apiOprLogAnno);
        if (apiOprLogAnno.isSaveRequest()) {
            // 保存请求参数
            // 获取方法签名的参数名数组
            String[] parameterNames = signature.getParameterNames();
            // 获取连接点传递的实参数组 
            Object[] args = proceedingJoinPoint.getArgs();
            Map<String, Object> paramMap = new HashMap<>(parameterNames.length);

            for (int i = 0; i < parameterNames.length; i++) {
                if (!RequestAttributes.REFERENCE_REQUEST.equals(parameterNames[i])) {
                    paramMap.put(parameterNames[i], args[i]);
                }
            }
            apiOprLog.setReqParams(JSON.toJSONString(paramMap));
        }

        // 入库操作
        log.debug("executeBefore apiOprLog: {}", JSON.toJSONString(apiOprLog));
    }

    private void executeAfter(ProceedingJoinPoint proceedingJoinPoint, String id, Object result) {
        // 获取目标方法的签名信息
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        // 从方法签名中获取 ApiOprLogAnno 注解的信息
        ApiOprLogAnno apiOprLogAnno = signature.getMethod().getAnnotation(ApiOprLogAnno.class);
        if (!apiOprLogAnno.isSaveResponse()) {
            return;
        }

        ApiOprLog apiOprLog = new ApiOprLog();
        apiOprLog.setId(id);
        apiOprLog.setResResult(JSON.toJSONString(result));
        apiOprLog.setUpdateTime(DateTime.now());

        // 入库操作
        log.debug("executeAfter apiOprLog: {}", JSON.toJSONString(apiOprLog));
    }

    private void executeAfterEx(Throwable ex, String id) {
        ApiOprLog apiOprLog = new ApiOprLog();
        apiOprLog.setId(id);
        apiOprLog.setExMessage(ex.toString());
        apiOprLog.setExJson(ExceptionUtil.stacktraceToString(ex));
        apiOprLog.setUpdateTime(DateTime.now());

        // 入库操作
        log.debug("executeAfterEx apiOprLog: {}", JSON.toJSONString(apiOprLog));
    }

    /**
     * 封装 ApiOprLog
     *
     * @param id            主键
     * @param sourceIp      IP
     * @param method        方法
     * @param apiOprLogAnno 注解
     * @return 接口操作日志对象
     */
    private ApiOprLog packaging(String id,
                                String sourceIp,
                                String method,
                                ApiOprLogAnno apiOprLogAnno) {
        ApiOprLog apiOprLog = new ApiOprLog();
        apiOprLog.setId(id);
        apiOprLog.setSourceIp(sourceIp);
        apiOprLog.setUsername("Regexp");
        apiOprLog.setMethod(method);
        apiOprLog.setApiModule(moduleName);
        apiOprLog.setApiType(apiOprLogAnno.apiType());
        apiOprLog.setApiDetail(apiOprLogAnno.apiDetail());
        apiOprLog.setCreateTime(DateTime.now());
        return apiOprLog;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127

4. 进行测试

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/get")
    @ApiOprLogAnno(apiType = "查询", apiDetail = "查询单个用户", isSaveResponse = true)
    public Person get() {
        return new Person("Regexp", 18);
    }

    @PostMapping("/save")
    @ApiOprLogAnno(apiType = "保存", apiDetail = "保存单个用户", isSaveRequest = true)
    public String save(@RequestBody Person person) {
        log.debug("save person: {}", JSON.toJSONString(person));
        return "ok";
    }

    @GetMapping("/getEx")
    @ApiOprLogAnno(apiType = "查询", apiDetail = "查询单个用户(异常情况)")
    public Person getEx() {
        throw new IllegalArgumentException();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

三、问题记录

1. 引用不是注解类型
描述

启动项目时,报错如下:

Caused by: java.lang.IllegalArgumentException: error Type referred to is not an annotation type: cn$regexp$dailylearningtest$anno$ApiOprLog

在这里插入图片描述

分析

从报错信息来看,显示为:错误的类型,引用的不是一个注解类型。
Ctrl + Shift + F 全局搜索 ApiOprLog,看看哪些地方有用到 ApiOprLog。
经过搜索,发现在@annotation中引用了 ApiOprLog(注解重命名后,这里忘记改了),但 ApiOprLog 并不是注解类型,所以导致启动项目时,Spring找到了这个类但这个类却不是注解,就报了这个错。

@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLog)")
public void pointcut(){}
  • 1
  • 2

将 ApiOprLog 修改为正确的注解名称即可。

@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLogAnno)")
public void pointcut(){}
  • 1
  • 2
2. 依赖冲突
描述

SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found
binding in
[jar:file:/D:/OpenSource/maven-repository/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in
[jar:file:/D:/OpenSource/maven-repository/org/slf4j/slf4j-reload4j/1.7.36/slf4j-reload4j-1.7.36.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an
explanation. SLF4J: Actual binding is of type
[ch.qos.logback.classic.util.ContextSelectorStaticBinder]

在这里插入图片描述

分析

从以上信息来看,应该是发生了依赖冲突导致的。
在控制台输入 mvn dependency:tree 查看项目中所有使用的依赖以及依赖中引用的依赖,查找哪些依赖使用了 slf4j,在其中一个依赖中使用exclusions进行排除即可,如下:

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
     <exclusions>
         <exclusion>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
         </exclusion>
     </exclusions>
 </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/615651
推荐阅读
相关标签
  

闽ICP备14008679号