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

有些人总说自己过的不好,一上秤却又胖了不少

“我可以吃一口吗,就一小口~”
在这里插入图片描述

前言

在工作中经常发现,我们经常会使用一些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

可以看到接口请求成功了,但是好像并没有实现注解什么功能
在这里插入图片描述
因此得知,这个注解目前没有任何作用,因为我们仅仅是对注解进行了声明,并没有在任何地方来使用这个注解,注解的本质也是一种广义的语法糖,最终还是要利用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测试,可看到注解生效

在这里插入图片描述

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

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

在这里插入图片描述
可以看到打印的日志值相同的情况下,并不能知道是请求哪个接口输出的日志。现在我们来修改一下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可以获取到请求类、方法信息。现在可以清晰看到是哪个接口方法请求到的了。
在这里插入图片描述

JoinPoint常用方法API
在这里插入图片描述

使用注解获取请求参数

新增接口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 如下
在这里插入图片描述

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

注解详细讲解

定义方式

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

在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
}

@Retention注解

用于描述注解的生命周期,表示注解在什么范围有效,它有3个取值:

  1. Java源文件阶段;
  2. 编译到class文件阶段;
  3. 运行期阶段。

同样使用了RetentionPolicy枚举类型定义了三个阶段:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * (注解将被编译器忽略掉,常见的@Override就属于这种注解)
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * (注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为。@Deprecated和@NonNull就属于这样的注解)
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     * (注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到;@Controller、@Service等都属于这一类)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

@Documented

将注解的元素加入Javadoc中

@Inherited

是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。@Inherited注解只对那些@Target被定义为ElementType.TYPE的自定义注解起作用。

@Repeatable

表示该注解可以重复标记

注解的特殊语法

1.如果注解本身没有注解类型元素,那么在使用注解的时候可以省略(),直接写为:@注解名,它和标准语法@注解名()等效!

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface FirstAnnotation {
}

使用

//等效于@FirstAnnotation()
@FirstAnnotation
public class JavaBean{
	//省略实现部分
}

2.如果注解本本身只有一个注解类型元素,而且命名为value,那么在使用注解的时候可以直接使用:@注解名(注解值),其等效于:@注解名(value = 注解值)

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface SecondAnnotation {
	String value();
}

使用

//等效于@FirstAnnotation()
@FirstAnnotation
public class JavaBean{
	//省略实现部分
}

3.如果注解中的某个注解类型元素是一个数组类型,在使用时又出现只需要填入一个值的情况,那么在使用注解时可以直接写为:@注解名(类型名 = 类型值),它和标准写法:@注解名(类型名 = {类型值})等效

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface ThirdAnnotation {
	String[] name();
}

使用案例

//等效于@ ThirdAnnotation(name = {"this is third annotation"})
@ ThirdAnnotation(name = "this is third annotation")
public class JavaBean{
	//省略实现部分
}

4.如果一个注解的@Target是定义为Element.PACKAGE,那么这个注解是配置在package-info.java中的,而不能直接在某个类的package代码上面配置

自定义注解的运行时解析

只有当注解的保持力处于运行阶段,即使用@Retention(RetentionPolicy.RUNTIME)修饰注解时,才能在JVM运行时,检测到注解,并进行一系列特殊操作。

自定义的注解@CherryAnnotation,并把它配置在了类Student上

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {
    String name();
    int age() default 23;
    int[] score();
}

Student

public class Student {
    @CherryAnnotation(name = "peng",age = 23,score = {99,66,77})
    public void study(int times){
        for(int i = 0; i < times; i++){
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

反射操作获取注解

public class Test {
    public static void main(String[] args){
        try {
            //获取Student的Class对象
            Class stuClass = Class.forName("cn.jp.pojo.Student");

            //说明一下,这里形参不能写成Integer.class,应写为int.class
            Method stuMethod = stuClass.getMethod("study",int.class);
			//判断该元素上是否配置有CherryAnnotation注解;
            if(stuMethod.isAnnotationPresent(CherryAnnotation.class)){
                System.out.println("Student类上配置了CherryAnnotation注解!");
                //获取该元素上指定类型的注解
                CherryAnnotation cherryAnnotation = stuMethod.getAnnotation(CherryAnnotation.class);
                System.out.println("name: " + cherryAnnotation.name() + ", age: " + cherryAnnotation.age()
                    + ", score: " + cherryAnnotation.score()[0]);
            }else{
                System.out.println("Student类上没有配置CherryAnnotation注解!");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

如果我们要获得的注解是配置在方法上的,那么我们要从Method对象上获取;如果是配置在属性上,就需要从该属性对应的Field对象上去获取,如果是配置在类型上,需要从Class对象上去获取。总之在谁身上,就从谁身上去获取!

反射对象上还有一个方法getAnnotations(),该方法可以获得该对象身上配置的所有的注解。它会返回给我们一个注解数组,需要注意的是该数组的类型是Annotation类型,这个Annotation是一个来自于java.lang.annotation包的接口。

注解的核心原理

按照注解的生命周期以及处理方式的不同,通常将注解分为运行时注解和编译时注解

  • 运行时注解的本质是实现了Annotation接口的特殊接口,JDK在运行时为其创建代理类,注解方法的调用实际是通过AnnotationInvocationHandler的invoke方法,AnnotationInvocationHandler其中维护了一个Map,Map中存放的是方法名与返回值的映射,对注解中自定义方法的调用其实最后就是用方法名去查Map并且放回的一个过程
  • 编译时注解通过注解处理器来支持,而注解处理器的实际工作过程由JDK在编译期提供支持,有兴趣可以看看javac的源码。

注解的内容就大致介绍到这吧~~~~~~~~

在这里插入图片描述

有兴趣的老爷,可以关注我的公众号【一起收破烂】,回复【006】获取2021最新java面试资料以及简历模型120套哦~
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

收破烂的小熊猫~

你的鼓励将是我创造最大的东西~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值