Java Annotaions (注解)的本质和实现原理(下)

引言

在上一篇文章 《Java Annotaions (注解)的本质和实现原理(上)》中,我们介绍了Java 注解的本质、原理和作用方式等内容。本篇文章将继续探究 Java 注解在 JVM 层面究竟是如何实现的,并结合应用场景,自己实现一个 Java 注解。

获取注解的数据

不忘初心,牢记使命。

还记得我们为什么要搞个注解出来么?元-数-据!

注解本质上是要给代码带来数据的。

前面我们已经看到,数据是通过注解的成员变量(很多时候是用 value() )来存储的,那么数据来了,该如何使用呢?

首先很显然,如果我们的代码需要在业务中使用注解传递的元数据,那么这个注解的一定是RUNTIME 的,否则数据在编译或者类加载阶段就被丢弃了。

那么我们该如何获取到注解所携带的数据的呢?答案是:通过 反射

首先我们看一下 Java 反射中经常用的那么几个类(Class, Method, Field)的定义:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
...                                  
}
复制代码

可以看到 Class 类实现了 AnnotatedElement 这个接口。

public final class Method extends Executable {
    ...
}
复制代码
class Field extends AccessibleObject implements Member {
    ...
}
复制代码

Executable 继承自AccessibleObject, AccessibleObject 则是实现了**AnnotatedElement**!

盲生,你发现华点了吗!

这些反射中常用的类,都实现了 AnnotatedElement 这个接口!

那我们来看下 AnnotatedElement 这个接口都定义了哪些方法 (jdk 1.8):

public interface AnnotatedElement {
    
    // 是否有注解
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        ...
    }

    // 获取指定类型的注解
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);

    // 获取所有注解
    Annotation[] getAnnotations();

    // 根据类型获得注解
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
       ...
     }

    // 获取声明的注解
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
        ...
     }

    // 通过类型获取声明的注解
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
        ...
    }

    // 获取声明注解列表
    Annotation[] getDeclaredAnnotations();
}
复制代码

可以看到,这个接口中都是获取注解的方法!

那么接下来就让我们动动双手,写一个基于注解的 Hello World 吧!

首先自定义一个注解,注意 Retention 设置为 RUNTIME:

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target(METHOD)
@Retention(RUNTIME)
public @interface TestAnno {
    String value() default "abc";
}
复制代码

我们这个注解时修饰方法的,并且有一个 value 的属性值,默认为 "abc"。

然后写一个 main 函数:

import java.lang.reflect.Method;

public class AnnotationTest {

    @TestAnno("Hello World")
    public static void main(String[] args) {
        try {
            Class cls = AnnotationTest.class;
            Method method = cls.getMethod("main", String[].class);
            TestAnno anno = method.getAnnotation(TestAnno.class);
            System.out.println(anno.value());
        } catch (Exception ignore) {}
    }
}
复制代码

首先,我们通过反射的方式,获取 main 函数的 Method 对象;

然后调用 Method 对象的 getAnnotation 方法,入参为 TestAnno 的类型;

这样就可以拿到我们在 main 函数上面写的那个注解了,然后调用 value 函数即可获取 "Hello World":

Hello World

Process finished with exit code 0
复制代码

看起来大功告成是不是?但是,等等!好像哪里不对啊!

前面我们讲过,注解的本质是什么来着?接口啊!一个接口又没有被实现,我们是怎么通过调用它的 value() 方法,获取到 "Hello World" 的呢?

这种不写实现,而在运行时通过某种机制自动实现的方式,那些熟悉 mybatis 的同学,是不是有点面熟呢?没错!就是 动-态-代-理!

动态代理与注解

那么事不宜迟,就让我们验证一下是不是这么一回事儿吧!

首先我们在 JVM 启动参数里加上这么一行:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
复制代码

用以追踪 JVM 在运行过程中生成的动态代理类:

然后运行上面的main函数,最后我们在工程根目录下面,会看到一个 com的文件夹,逐层打开后,会发现一些名为 $Proxy*.class 文件:

然后打开 $Proxy1.class,我们会看到:

package com.sun.proxy;

import com.source.sourceapi.TestAnno;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy1 extends Proxy implements TestAnno {...}
复制代码

然后再往下翻,会看到:

    public final String value() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
复制代码

果不其然实现了 value() 方法!这就是我们能够通过接口直接获取到 "Hello World" 的原因了!

那么接下来要搞清楚的就是,这个动态代理是怎么实现的问题了。

熟悉动态代理的同学都知道,其实现的核心是:

    public $Proxy1(InvocationHandler var1) throws  {
       super(var1);
   }
复制代码

也就是这个构造函数的 InvocationHandler 对象了。那么注解的 Handler 是哪个具体实现呢? 答案是:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
   private static final long serialVersionUID = 6182022883658399397L;
   private final Class<? extends Annotation> type;
   private final Map<String, Object> memberValues;
   private transient volatile Method[] memberMethods = null;
   ...
}
复制代码

这个 class 里面的核心属性,就是那个 memberValues,我们在使用注解时给注解属性的赋值,都存储在这个map里了。

而回过头来看我们的代理类中的 value() 方法的实现,实际上是调用了 Handler 里的 invoke 方法。而 invoke 方法则是通过各种逻辑判断,最终从 memberValues 方法中获取到想要的元数据!

至于说 invoke 方法是怎么做到的?嘿嘿,请大家动动手翻翻源码,欢迎在留言区讨论哦!

自定义 Validation

到这里我们总算是把 Java 注解的实现给彻底搞清楚了。

那么有的同学要问了,这个东西在框架里倒是很常见,但是在我们的实际工作中到底有什么实际的用处呢?

接下来,我们就结合一个实际应用场景,动动小手吧!

熟悉 Spring Boot 开发的同学都知道,我们通常要对 http 的请求参数进行一些合法性校验。比如某些字段不能为空啦,某些字段的格式必须是 email 啦等等。

通常情况下,我们都会借助 javax.validation 来完成参数校验。而 validation 本身就是通过注解来实现的。例如

@NotBlank
@Email
@Future
...
复制代码

但是啊,我们有时候也会遇到一些 validation 提供的注解无法实现的校验需求。 比如创建用户的时候,通常都会让用户输入两遍密码,如何通过 validation 校验两次输入的密码是否一致呢?

首先我们自定义一个注解:

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch {
   String message() default "{constraints.fieldmatch}";

   Class<?>[] groups() default {};

   Class<? extends Payload>[] payload() default {};

   /**
    * @return The first field
    */
   String first();

   /**
    * @return The second field
    */
   String second();

   /**
    * Defines several <code>@FieldMatch</code> annotations on the same element
    *
    * @see FieldMatch
    */
   @Target({TYPE, ANNOTATION_TYPE})
   @Retention(RUNTIME)
   @Documented
   @interface List
   {
       FieldMatch[] value();
   }
}
复制代码

首先 这个注解的 target 必须是 TYPE 级别的,因为我们是要比较一个Class中的两个属性的。

其次 我们在这个注解里又定义了一个注解 List,它可以通过 FieldMath.List 的方式使用。这是因为有可能一个请求中会有多对参数比较的情况。

最后 让我们看看这个注解中的具体属性,有三个:

  • first(), 第一个参数
  • second(), 第二个参数
  • message(), 如果不匹配返回什么错误

另外注意,这个注解上面,有一个 Constraint 的注解: @Constraint(validatedBy = FieldMatchValidator.class)

这个注解的定义来自 javax.validation

package javax.validation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraint {
    Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
复制代码

他用来标识我们的自定义注解 FieldMatch 是一个 Validation 的约束。它的赋值 validatedBy = FieldMatchValidator.class 则表明约束判断的逻辑在 FieldMatchValidator 这个类里。

那么接下来让我们实现这个逻辑吧:

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

    private String firstFieldName;

    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context) {

        try {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);

            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }

        catch (final Exception ignore) {
            // ignore
        }
        return true;
    }

}
复制代码

首先这个 class 是实现了 ConstraintValidator。 熟悉面对对象编程套路的朋友们其实到这里就应该能猜到 Validation 的工作原理了。对于被 Constraint 类型注解的类或者属性,通过调用其对应的 validation 类的 isValid() 方法来判断参数校验是否通过,如果未通过的话,则抛出指定 message 的异常。

具体是不是这样,就请勤劳的你翻翻代码吧,欢迎在留言区讨论哦!

总结

我们用两篇文章深度了解了 Java 注解的性质、本质和实现原理,并且动手自己实现了符合应用场景的注解。

可以看到,注解主要使用了反射和动态代理技术。

掌握注解还是很有必须要的,因为很多 Java 框架使用注解做元数据传递,掌握了注解的原理,有利于我们充分了解和利用框架,而且还能根据自己的需求,扩展框架的功能。真是太棒啦!

猜你喜欢

转载自juejin.im/post/5ecb84836fb9a047a6445344