注解编程 | 自定义注解

注解的概念

注解(Annotation)从 JDK1.5 开始引入,具有以下几个特点

  • 注解是一种元数据形式,即注解属于 JAVA 的一种数据类型,和类、接口、数组、枚举类似
  • 注解用来修饰类、方法、变量、参数、包
  • 注解不会对所修饰的对象产生直接的影响

如何自定义注解

在学习如何自定义注解之前,先学习一下元注解的概念,因为要自定义注解,必须要使用元注解。

元注解

元注解(Meta-Annotation)的作用就是负责修饰其他注解,JDK 提供了如下几个标准的元注解。

  • @Target
  • @Retention
  • @Documented
  • @Inherited
  • @Repeatable
  • @Target

@Target 元注解用于声明注解可以修饰的对象范围,有的注解只能使用的方法上(比如 @Override),有的注解却可以使用在类上(比如 @Deprecated),就是因为这些注解在声明时设置了相应的 @Target 范围。

@Target 可设置多个,类型为 ElementType 数组:

类型可注解元素
TYPE类、接口、枚举、注解
FIELD属性
METHOD方法
PARAMETER方法参数
CONSTRUCTOR构造方法
LOCAL_VARIABLE局部变量
ANNOTATION_TYPE注解
PACKAGE
TYPE_PARAMETER类型参数(即泛型)
TYPE_USE任何类型

注解的属性类型为数组时,如果需要设置多个值,使用大括号进行设置 @Target = { TYPE, FIELD },如果只有一个值,则可以省略大括号 @Target = TYPE

  • @Retention

注解也有生命周期,有的注解只出现在源代码中,在编译阶段会被编译器忽略(比如 @Override 纯粹用于规范语法),有的注解会被编译进 .class 文件中,但类加载过程中会被虚拟机忽略,而更多的注解则需要被虚拟机加载到内存。

@Retention 元注解用于声明注解的生命周期,只能设置一个,类型为 RetentionPolicy

类型生命周期描述
SOURCE源码阶段只出现在源代码中,在编译阶段会被编译器忽略
CLASS编译阶段会被编译进 .class 文件中,类加载过程中会被虚拟机忽略
RUNTIME运行期阶段会被虚拟机加载到 Class 对象中,可以通过反射进行得到这个注解
  • @Documented

一个注解使用 @Documented 元注解进行修饰,则该注解会出现在 JavaDoc 文档中。@Documented 元注解是一个标记注解,没有属性需要设置。

  • @Inherited

JAVA 中的继承是经常被用到的,如果一个注解标记在父类上,通过子类是否可以得到该注解?

正常是得不到的,但如果注解上使用了 @Inherited 元注解进行修饰,则可以得到。

@Inherited 的继承功能只对 TYPE 类型中的 class 有效,如果注解使用在接口(interface)、方法(method)、属性(field)等,即便注解上声明了 @Inherited,也无法通过子类获取。

  • @Repeatable

@Repeatable 元注解用于声明注解可以重复使用,正常情况下,一个元素上只能标记一个同类型注解,使用了 @Repeatable 则可以同时标记多个。

自定义注解

注解跟类、接口一样,也是 JAVA 一种数据类型,声明注解跟声明类,接口差不多,只不过类型为 @interface。另外,前面学习了元注解,需要为注解标记元注解声明注解的作用范围及生命周期。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Version {
}

注解也支持方法,但注解的方法格式跟类有点不一样

  • 注解跟接口一样,方法的默认修饰符为 public,可以省略不写
  • 方法返回类型只能使用八种基本数据类型(byte、short、char、int、long、float、double、boolean)、String、Enum、Class、Annotation 以及前面几种类型的数组类型
  • 方法可以设置默认值,没有设置默认值的方法,在使用时必须设置值
  • value 是一个特殊的方法,如果注解只有一个方法 value 需要设置值,在使用时可以不用带方法名(见下面的示例),所以如果注解只有一个方法,建议使用方法名 value

没有方法的注解称之为标记注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Version {
    // 使用 default 关键字设置默认值
    String value() default "";
}

// 下面两行代码的效果是一样的
// @Version(value = "1.0.0")
@Version("1.0.0")
public class AnnotationTest() {
}

如何使用注解

在注解的概念中,我们提到 注解不会对所修饰的对象产生直接的影响,那使用了注解到底有什么用呢?

@Retention 设置为 RUNTIME 时,注解会被加载到 Class 对象中,通过反射就可以得到注解以及注解的属性值,从而实现对注解的解析。

设置 @Retention 为 SOURCE 及 CLASS 注解,并不会加载到内存,不是代码层面可以操作的,这里只讨论 RUNTIME 如何使用。

AnnotatedElement 接口提供了获取注解的 API,ClassMethodField 等都是 AnnotatedElement 接口的实现类。

  • isAnnotationPresent(Class<?> var1):判断当前元素是否存在指定注解,支持 @Inherited
  • getAnnotation(Class<T> var1):获取当前元素的指定注解,支持 @Inherited
  • getAnnotations():获取当前元素的所有注解,支持 @Inherited
  • getAnnotationsByType(Class<T> var1):获取当前元素的指定类型注解,支持 @Inherited@Repeatable
  • getDeclaredAnnotation(Class<T> var1):获取当前元素的指定注解,仅限标注在当前元素上的注解
  • getDeclaredAnnotations():获取当前元素的所有注解,仅限标注在当前元素上的注解
  • getDeclaredAnnotationsByType(Class<T> var1):获取当前元素的指定类型注解,仅限标注在当前元素上的注解,支持 @Repeatable

通过反射得到注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Version {
    String value() default "";
}

@Version("1.0.0")
public class AnnotationTest() {

    public static void main(String[] args) {
        // 通过 Class 对象得到 Annotation
        A annotation = AnnotationTest.class.getDeclaredAnnotation(A.class);
        // 通过 Annotation 得到 value 属性值
        System.out.println(annotation.value());
    }
}

通过反射得到元注解

元注解是声明在注解上的注解,要想得到元注解,就必须先得到注解,注解本身也是一个 Class,通过解析注解类就可以得到元注解,但这里与解析普通的 Class 有一点点不一样,需要使用 Annotation.annotationType() 方法而不是 Object.getClass() 方法获取类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Version {
    String value() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Version
public @interface VersionRoute {
    String value() default "";
}

@VersionRoute("1.0.0")
public class AnnotationTest {

    public static void main(String[] args) {
        VersionRoute annotation = AnnotationTest.class.getDeclaredAnnotation(VersionRoute.class);
        // 通过注解的 annotationType() 方法得到 Class
        Version versionAnno = annotation.annotationType().getDeclaredAnnotation(Version.class);
        System.out.println(versionAnno);
    }
}

@Inherited 的用法

不声明 @Inherited 元注解的情况下,将注解标记到父类上,通过子类进行解析

// 注解标记在父类上
@Version("P")
public static class PInfo {}

public class AnnotationTest extends PInfo {

    public static void main(String[] args) {
        // 通过子类判断父类上的注解
        boolean present = AnnotationTest.class.isAnnotationPresent(Version.class);
        System.out.println("isAnnotationPresent:" + present);
        Version version = AnnotationTest.class.getAnnotation(Version.class);
        System.out.println("getAnnotation:" + version);
        version = AnnotationTest.class.getDeclaredAnnotation(Version.class);
        System.out.println("getDeclaredAnnotation:" + version);
    }
}

输出结果如下,可以看到注解标记在父类上,通过子类进行判断是完全无感知的

isAnnotationPresent:false
getAnnotation:null
getDeclaredAnnotation:null

Version 添加 @Inherited 元注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Version {
    String value() default "";
}

再次执行上面的程序,输出结果如下,可以看到 getAnnotation 可以到得父类上的注解,而 getDeclaredAnnotation 方法则无法获取。

isAnnotationPresent:true
getAnnotation:@org.springframework.core.annotation.AnnotationTest$Version(value=B)
getDeclaredAnnotation:null

PInfo 类改为接口,再次执行,输出结果如下,可以看到虽然注解声明了 @Inherited,但是标记在接口上也是不启作用的,因为 @Inherited 只支持 TYPE 范围的 class,所以,标记在 MethodField 等位置也同样是无效的。

isAnnotationPresent:false
getAnnotation:null
getDeclaredAnnotation:null

@Repeatable 的用法

为注解声明 @Repeatable 元注解,需要设置一个注解类,这个注解类的 value 方法的返回值必须是当前注解的数组类型,说起来有点绕,直接通过代码看比较清楚。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Versions.class)
public @interface Version {
    String value() default "";
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Versions {
    Version[] value();
}

标记一个 @Version 注解

@Version("P")
public class PInfo {}

public class AnnotationTest {

    public static void main(String[] args) {
        // 解析 Version 注解
        boolean present = PInfo.class.isAnnotationPresent(Version.class);
        System.out.println("isAnnotationPresent:" + present);
        Version vAnno = PInfo.class.getDeclaredAnnotation(Version.class);
        System.out.println("getDeclaredAnnotation:" + vAnno);
        Version[] vAnnos = PInfo.class.getDeclaredAnnotationsByType(Version.class);
        System.out.println("getDeclaredAnnotationsByType:" + Arrays.toString(vAnnos));
        // 解析 Versions 注解
        present = PInfo.class.isAnnotationPresent(Versions.class);
        System.out.println("isAnnotationPresent:" + present);
        Versions vsAnno = PInfo.class.getDeclaredAnnotation(Versions.class);
        System.out.println("getDeclaredAnnotation:" + vsAnno);
        Versions[] vsAnnos = PInfo.class.getDeclaredAnnotationsByType(Versions.class);
        System.out.println("getDeclaredAnnotationsByType:" + Arrays.toString(vsAnnos));
    }
}

输出结果如下,可以看到当只标记了一个 @Version 注解时,@Versions 是不存在的

isAnnotationPresent:true
getDeclaredAnnotation:@Version(value=P)
getDeclaredAnnotationsByType:[@Version(value=P)]
isAnnotationPresent:false
getDeclaredAnnotation:null
getDeclaredAnnotationsByType:[]

标记多个 @Version 注解,再次执行上面的程序

@Version("B")
@Version("P")
public class PInfo {}

输出结果如下,可以看到,标记多个 @Version 注解与标记一个 @Version 标记的解析结果完全不一样

isAnnotationPresent:false
getDeclaredAnnotation:null
getDeclaredAnnotationsByType:[@Version(value=P), @Version(value=B)]
isAnnotationPresent:true
getDeclaredAnnotation:@Versions(value=[@Version(value=P), @Version(value=B)])
getDeclaredAnnotationsByType:[@Versions(value=[@Version(value=P), @Version(value=B)])]

标记一个注解时,与使用没有声明 @Repeatable 的注解是一样的。标记多个注解时,通过 getDeclaredAnnotation(Version.class) 居然获取不到注解,反而 getDeclaredAnnotation(Versions.class) 可以获取到。这是因为 @Repeatable 元注解最终解析成

@Versions({ @Version("P"), @Version("B") })
public class PInfo {}

虽然多注解时,getDeclaredAnnotation() 无法获取到 Version 注解,但 getDeclaredAnnotationsByType() 方法可以。

待思考的问题

当一个注解的 @TargetElementType.TYPE 或者 ANNOTATION_TYPE 时,就可以声明在其它注解之上,形成组合注解。一个注解上可以声明多个元注解,元注解上还可以声明元注解,可以形成非常复杂的组合注解。声明在其它注解之上的注解称之为元注解,而最终可以标记在某个元素之上的称之为根注解。

假设有如下三个注解形成的组合注解 C,C 为根注解,A 及 B 都是元注解。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface A {
    String value() default "";
    String name();
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@A(value = "A", name = "Hello")
public @interface B {
    String value() default "";
    String packages();
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@B(value = "B", packages = "World")
public @interface C {
    String value() default "";
}

@C("C")
public class PInfo {}

如何快速得到注解?

比如,想知道一个元素有没有标记注解 A,注解 A 可能直接标记在元素上,也可能是以组合注解的元注解形式(比如注解 C)标记在元素上,但是,JDK 提供的 API 只能快速得到根注解,怎么样才能快速得到注解,而不用管是根注解还是元注解。

public class AnnotationTest {

    public static void main(String[] args) {
        A annotation = getAnnotation(PInfo.class, A.class);
        System.out.println(annotation);
    }

    private static <T extends Annotation> T getAnnotation(AnnotatedElement source, Class<T> annotationClass) {
        Annotation[] annotations = source.getAnnotations();
        T anno = null;
        for (Annotation annotation : annotations) {
            System.out.print("检查注解:" + annotation);
            if (annotation.annotationType().equals(annotationClass)) {
                return (T) annotation;
            } else if (annotation.annotationType().getPackage().getName().startsWith("java.")) {
                System.out.print(" --- 忽略");
                System.out.println();
                continue;
            } else {
                System.out.println();
                anno = getAnnotation(annotation.annotationType(), annotationClass);
                if (anno != null) {
                    System.out.println();
                    return anno;
                }
            }
        }
        return null;
    }
}

如何动态为元注解的属性设值?

注解 B 拥有 valuepackages 两个方法,假设 packages 方法用于设置需要扫描的包,而注解 B 又标记在注解 C 上,本意是想注解 C 可以结合注解 B 的功能,但是,使用注解 C 进行标记时,注解 C 才是根注解,并不能设置 packages 方法,而注解 B 的 packages 已经被写死了,这就意味着使用注解 C 时,其元注解的属性都是固定值,这明显限制了组合注解的能力。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java自定义注解是指在Java语言中可以通过编写代码来定义自己的注解自定义注解可以提供一些额外的元数据信息,用于标记和描述Java代码中的某个元素。自定义注解可以用于类、方法、属性等各个层面。 实现自定义注解步骤如下: 1. 使用@Retention注解指定注解的保留策略,默认为RetentionPolicy.CLASS。可选的保留策略有三种:RetentionPolicy.SOURCE、RetentionPolicy.CLASS和RetentionPolicy.RUNTIME。 2. 使用@Target注解指定注解的作用目标,默认可以用于所有的Java元素。可选的作用目标包括ElementType.TYPE(类、接口、枚举等)、ElementType.FIELD(字段、枚举常量等)、ElementType.METHOD(方法)、ElementType.PARAMETER(方法参数)、ElementType.CONSTRUCTOR(构造方法)、ElementType.LOCAL_VARIABLE(局部变量)等。 3. 使用@interface关键字定义注解,并定义注解的属性。注解的属性以无参无异常抛出的方法的形式定义,可以指定默认值。 4. 在需要使用注解的地方使用自定义注解自定义注解可以携带信息,这些信息可以在运行时通过反射获取,对注解进行解析和处理。自定义注解可以用于编写各种工具、框架和库,来增强程序的扩展性和灵活性。 实现自定义注解的一个典型应用场景是在Spring框架中的依赖注入(DI)和面向切面编程(AOP)中。通过自定义注解,可以标记需要注入的Bean,或者标记需要进行切面拦截的方法,从而实现依赖注入和切面编程的功能。 总的来说,Java自定义注解是Java语言提供的一种灵活的元编程机制,可以通过注解增加程序的可读性和可维护性,同时也可以用于实现一些特定的功能,如依赖注入和切面编程等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值