自定义注解+aop实现-快速上手

2022-09-16 09:18:18

本文不介绍spring aop的基础知识,只演示实现一个自定义注解的流程,但是读者应该对注解的基本概念,切面、切点、通知等的基本概念有所了解。

项目基于Spring boot+maven+java8

项目需求

我想要在某些controller方法中记录请求日志,包括ip,方法,请求入参,返回结果,执行耗时等等。

那么我们可以通过自定义一个注解实现,加在需要打印日志的方法上即可。

各个公司内部的实现逻辑可能有些差别,但大同小异。

项目结构

首先展示一下项目的基本结构:

项目的pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.ztt</groupId>
	<artifactId>aop_demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>aop_demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

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

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

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

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.68</version>
		</dependency>

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

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

第一步:自定义一个注解

首先,我们需要自定义一个注解,代码如下。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface EagleEye {
    // 注解的属性。属性不是必须的,可以没有,根据公司的实际业务需要去定义
//    String value();
//    String name() default "";

}

@Retention(RetentionPolicy.RUNTIME) :

这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。

@Target

定义注解的作用目标。@Target(ElementType.METHOD)表示该注解可以作用于方法上。

@Documented

 用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。

这个时候,注解就定义完成了,可以加在方法上使用了:

import com.alibaba.fastjson.JSONObject;
import com.ztt.annotation.EagleEye;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping(value = "/hello")
public class HelloController {

    @GetMapping(value = "/testEagleEye")
    // 使用自定义注解
    @EagleEye
    public Object testEagleEye(@RequestBody JSONObject params) {
        log.info("params:{}", params);

        return "success";
    }

}

然后浏览器或者postman调用这个方法,会发现ip,返回结果,执行耗时什么的根本没有打印出来。

那肯定是不会打印的,因为我们只是定义了一个注解,并没有任何的代码去解析这个注解(所谓解析,就是说在编译期或运行时检测到注解,并进行特殊操作)。因此,单单定义一个注解是没有任何卵用的,我们必须去实现aop做相关操作才有意义。

那么怎么去解析这个注解呢?当然是需要我们写aop代码了。

第二步 aop实现

上一步我们自定义了一个注解@EagleEye ,并把它加到了controller的方法testEagleEye上,但是发现这个注解并没有啥卵用。

只有实现了aop,注解才能真正的起作用。

这一步,我们就去写代码实现aop,解析这个注解,代码如下:

import com.ztt.annotation.EagleEye;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class EagleEyeAop {

    /**
     * 切点表达式。此处的切点表达式会,匹配所有@EagleEye注解修饰的方法
     */
    @Pointcut("@annotation(com.ztt.annotation.EagleEye)")
    public void eagleEye() {

    }


    /**
     * 环绕通知
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("eagleEye()")
    public Object eagleEyeAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTimeMillis = System.currentTimeMillis();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();

        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        EagleEye eagleEyeAnnotation = method.getAnnotation(EagleEye.class);


        log.info(">>>>>> request start >>>>>>");
        log.info("请求链接:{}", httpServletRequest.getRequestURL().toString());
        log.info("请求类型:{}", httpServletRequest.getMethod());
        log.info("请求方法:{}", signature.getDeclaringTypeName(), signature.getName());
        log.info("请求ip:{}", httpServletRequest.getRemoteAddr());
        log.info("请求入参:{}", joinPoint.getArgs());

        Object proceedResult = null;
        try {
            // 执行目标方法
            proceedResult = joinPoint.proceed();
        } catch (Exception e) {
            log.error("目标方法执行异常:", e);
            throw e;
        }

        //
        long endTimeMillis = System.currentTimeMillis();
        log.info("请求耗时:{}ms", endTimeMillis - startTimeMillis);
        //
        log.info("目标方法执行结果:{}", proceedResult);
        log.info(">>>>>> request end >>>>>>");


        return proceedResult;

    }

}

第三不 验证aop

实现aop之后,我们再去调用上面的controller方法,可以看到控制台打印了日志:

  • 作者:架构帅
  • 原文链接:https://blog.csdn.net/AttleeTao/article/details/119328710
    更新时间:2022-09-16 09:18:18