傻瓜,自定义注解你会写了吗?(1)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 傻瓜,自定义注解你会写了吗?(1)

前言

在工作中经常发现,我们经常会使用一些spring体系的注解。如果面试的时候,你跟老板说你会使用注解,老板觉得你这个人还行;但是如果你和老板说你会自定义注解解决问题,老板肯定就会眼前一亮,这是个人才鸭,嗯,小伙子20k够不够…

学习目标

1)自定义一个注解,搭配aop实现一个日志打印功能

2)结合案例,对注解应用深入了解

自定义注解实现

准备工作

先创建一个springboot项目,并引入aop相关依赖。

 <dependencies>
        <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.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.9</version>
        </dependency>
    </dependencies>

项目启动端口配置为8081

server.port=8081

创建一个注解类

import java.lang.annotation.*;
/**
 * 自定义注解: TestLog
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestLog {
    String value() ;
}

自定义注解使用关键字 @interface,定义一个新的annotation类型与定义一个接口非常像,自定义注解后就可以在任何地方使用了。后面细说。

定义2个请求接口

    @TestLog("请求测试日志")
    @RequestMapping("/testOne")
    public String testOne(){
        System.out.println("测试自定义注解");
        return "testOne接口请正常";
    }
   @TestLog("请求测试日志")
      @RequestMapping("testTwo")
      public String testTwo(){
        System.out.println("测试自定义注解接口testTwo");
        return "testTwo接口请正常";
    }

现在自定义的注解已经都写在了testOne、testTwo接口上了,是不是就可以用了呢?

我们来请求testOne接口试试

http://127.0.0.1:8081/testOne

可以看到接口请求成功了,但是好像并没有实现注解什么功能

20200401134307494.png

因此得知,这个注解目前没有任何作用,因为我们仅仅是对注解进行了声明,并没有在任何地方来使用这个注解,注解的本质也是一种广义的语法糖,最终还是要利用Java的反射来进行操作。


不过Java给我们提供了一个AOP机制,可以对类或方法进行动态的扩展,想较深入地了解这一机制的可以看一下这一篇文章: Spring AOP的实现原理及应用场景


创建切面类

/**
 * @PackageName: com.lagou.edu.aop
 * @author: youjp
 * @create: 2021-04-06 18:05
 * @description:
 * @Version: 1.0
 */
@Aspect
@Component
public class TestAspact {
    /**
     * 切点:连接的地方。这里与TestLog注解相关连
     */
    @Pointcut("@annotation(com.jp.demo.annotation.TestLog)")
    public void pointcut(){}
    /**
     *  拦截方法执行前。绑定切点。注意:annotation(log)和传参TestLog log相对应
     * @param log
     */
    @Before("pointcut()&& @annotation(log)")
    public void Before(TestLog log) throws Exception {
        System.out.println("--- 日志的内容为[" + log.value() + "] ---");
    }
}

其中pointcut声明了我们自定义的注解TestLog 。@Before代表在请求前通知,在具体的通知中通过@annotation(log)拿到了自定义的注解对象,所以就能够获取我们在使用注解时赋予的值了。


再次请求http://127.0.0.1:8081/testOne测试,可看到注解生效

20200401134307494.png

使用注解获取更多详细信息

分别请求http://127.0.0.1:8081/testOne测试,

分别请求http://127.0.0.1:8081/testTwo测试.

20200401134307494.png

可以看到打印的日志值相同的情况下,并不能知道是请求哪个接口输出的日志。现在我们来修改一下TestAspact的@Before通知方法

   @Before("pointcut()&& @annotation(log)")
    public void Before(JoinPoint joinPoint,TestLog log) throws Exception {
        System.out.println("["
                + joinPoint.getSignature().getDeclaringType().getSimpleName()
                + "][" + joinPoint.getSignature().getName()
                + "]-日志内容-[" + log.value()
                + "]");
    }

通过JoinPoint可以获取到请求类、方法信息。现在可以清晰看到是哪个接口方法请求到的了。

20200401134307494.png

JoinPoint常用方法API

20200401134307494.png

使用注解获取请求参数

新增接口testThree

 /**
     * 传参类接口
     * @return
     */
    @TestLog("请求testThree日志")
    @RequestMapping("testThree")
    public String testThree(String name,String age){
        System.out.println("测试自定义注解接口testThree,获取传参:"+name);
        return "testThree接口请正常";
    }

对TestAspact切面类修改

import com.jp.demo.annotation.TestLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
/**
 * @PackageName: com.lagou.edu.aop
 * @author: youjp
 * @create: 2021-04-06 18:05
 * @description:
 * @Version: 1.0
 */
@Aspect
@Component
public class TestAspact {
    /**
     * 切点:连接的地方。这里与TestLog注解相关连
     */
    @Pointcut("@annotation(com.jp.demo.annotation.TestLog)")
    public void pointcut(){}
    /**
     *  拦截方法执行前。绑定切点。注意:annotation(log)和传参TestLog log相对应
     * @param log
     */
    @Before("pointcut()&& @annotation(log)")
    public void Before(JoinPoint joinPoint,TestLog log) throws Exception {
        System.out.println("["
                + joinPoint.getSignature().getDeclaringType().getSimpleName()
                + "][" + joinPoint.getSignature().getName()
                + "]-日志内容-[" + log.value()
                + "]");
    }
    @Around("pointcut()&& @annotation(log)")
    public Object around(ProceedingJoinPoint joinPoint, TestLog log) throws Throwable {
        //获取传参字段信息
        Map map= getFieldsName(joinPoint);
        Object args[]=joinPoint.getArgs();
        System.out.println("["
                + joinPoint.getSignature().getDeclaringType().getSimpleName()
                + "][" + joinPoint.getSignature().getName()
                + "]-请求传参" + map.entrySet()+"]");
        return joinPoint.proceed(args);
    }
    /**
     * 获取字段值
     * @param joinPoint
     * @return
     * @throws Exception
     */
    private Map<String, Object> getFieldsName(JoinPoint joinPoint) throws Exception {
        String classType = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        // 参数值
        Object[] args = joinPoint.getArgs();
        Class<?>[] classes = new Class[args.length];
        for (int k = 0; k < args.length; k++) {
            // 对于接受参数中含有MultipartFile,ServletRequest,ServletResponse类型的特殊处理,我这里是直接返回了null。(如果不对这三种类型判断,会报异常)
            if (args[k] instanceof MultipartFile || args[k] instanceof ServletRequest || args[k] instanceof ServletResponse) {
                return null;
            }
            if (!args[k].getClass().isPrimitive() && args[k]!=null) {
                // 当方法参数是基础类型,但是获取到的是封装类型的就需要转化成基础类型
//                String result = args[k].getClass().getName();
//                Class s = map.get(result);
                // 当方法参数是封装类型
                Class s = args[k].getClass();
                classes[k] = s == null ? args[k].getClass() : s;
            }
        }
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
        // 获取指定的方法,第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型
        Method method = Class.forName(classType).getMethod(methodName, classes);
        // 参数名
        String[] parameterNames = pnd.getParameterNames(method);
        // 通过map封装参数和参数值
        HashMap<String, Object> paramMap = new HashMap();
        for (int i = 0; i < parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }
        return paramMap;
    }
}

请求http://localhost:8081/testThree?name=jp&age=12 如下

20200401134307494.png

这里我们已经简单实现了自定义注解的常用功能。接下来,就针对案例进行讲解。

注解详细讲解

定义方式

注解其实就是一种标记,可以用来修饰,类、方法、变量、参数、包,但是它本身并不起任何作用,注解的作用在于注解的处理程序,通过捕获被注解标记的代码然后进行一些处理,这就是注解工作的方式。

在java中,自定义一个注解非常简单,通过@interface就能定义一个注解,实现如下:

public @interface TestLog{
}

根据我们在自定义类的经验,在类的实现部分无非就是书写构造、属性或方法。但是,在自定义注解中,其实现部分只能定义一个东西:注解类型元素(annotation type element)。咱们来看看其语法:

我们在定义属性的时候,如果只有一个元素可以默认写value

public @interface  TestLog {
  String value();
}

这样在使用注解的时候直接写注解类(值)即可。也可以填写多个属性值

public @interface  TestLog {
  public String name();
  int age() default 18;
  int[] array();
}

定义注解类型元素时需要注意如下几点:

访问修饰符必须为public,不写默认为public;

该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;

该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作);

()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法; default代表默认值,值必须和第2点定义的类型一致;

如果没有默认值,代表后续使用注解时必须给该类型元素赋值。

元注解

元注解:对注解进行注解,也就是对注解进行标记,元注解的背后处理逻辑由apt tool提供,对注解的行为做出一些限制,例如生命周期,作用范围等等。

前面定义自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestLog {
    String value() ;
}

@ Target

@target注解用于描述作用的对象类型

public enum ElementType {
    /** 类,接口(包括注解类型)或枚举的声明 */
    TYPE,
    /** 属性的声明 */
    FIELD,
    /** 方法的声明 */
    METHOD,
    /** 方法形式参数声明 */
    PARAMETER,
    /** 构造方法的声明 */
    CONSTRUCTOR,
    /** 局部变量声明 */
    LOCAL_VARIABLE,
    /** 注解类型声明 */
    ANNOTATION_TYPE,
    /** 包的声明 */
    PACKAGE
}





相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6月前
|
IDE Java 程序员
JAVA注解大揭秘:为何程序员都爱它如命?
【6月更文挑战第29天】Java注解是元数据机制,用于在代码中嵌入信息供编译器、IDE和工具使用。它们以`@`标识,可用于类、方法等,用于编译时检查、代码生成(如Lombok的`@Getter`、`@Setter`)、框架集成(如Spring的`@Autowired`)。程序员喜欢注解因其简洁性、可读性和可扩展性,能减少冗余代码并增强代码的可理解性。
65 15
|
JSON 安全 Java
SpringMVC之自定义注解(这期博客带你领略自定义注解的魅力)
SpringMVC之自定义注解(这期博客带你领略自定义注解的魅力)
68 0
SpringMVC之自定义注解(这期博客带你领略自定义注解的魅力)
|
前端开发 Java 编译器
Java的第十六篇文章——枚举、反射和注解(后期再学一遍)
Java的第十六篇文章——枚举、反射和注解(后期再学一遍)
|
安全 Java 编译器
JAVA注解与反射:看这篇文章就够了1
JAVA注解与反射:看这篇文章就够了
148 0
|
安全 Java 编译器
JAVA注解与反射:看这篇文章就够了2
JAVA注解与反射:看这篇文章就够了
81 0
|
消息中间件 JavaScript 小程序
项目终于用上了插入式注解,真香!
项目终于用上了插入式注解,真香!
项目终于用上了插入式注解,真香!
|
XML 缓存 Java
Java注解怎么用
Java注解怎么用
229 0
傻瓜,自定义注解你会写了吗?(2)
傻瓜,自定义注解你会写了吗?(2)
|
NoSQL Java 程序员
想自己写框架?不会写Java注解可不行
想自己写框架?不会写Java注解可不行
想自己写框架?不会写Java注解可不行