【再学一次系列】Java注解,你了解多少?

779 阅读10分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

前言

哈喽大家好,我是卡诺,一名致力于成为全栈的全粘工程师!

通过new对象不行吗?为什么要用反射?Java反射极简操作手册两章内容的讲解,相信大家对反射运用一定是如鱼得水!谈到反射,我们不得不提及与之息息相关的另一个知识点 “注解”

自JDK5开始,Java新增了注解(元数据)支持。作为一名javaer,注解是我们日常开发使用最必不可少功能之一,比如:Spring中的@Bean@Controller@Service等。除此之外我们还常常会自定义一些注解以解决业务中遇到的问题,比如:简化对象属性拷贝,日志记录等,本章我们将由浅入深学习注解的相关知识。

本文已加入 【再学一次Java】专栏,该专栏旨在重温Java知识,夯实基础,包含:Lambda、反射、注解、多线程等进阶知识。如果有需要的小伙伴可以关注一下,专栏持续更新ing!

简介

官方描述👉访问地址Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

Annotations have a number of uses, among them:

  • Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.
  • Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
  • Runtime processing — Some annotations are available to be examined at runtime. 谷歌翻译一下:注解,一种元数据形式,提供有关程序的数据,这些数据不属于程序本身。注解对其标记的代码的操作没有直接影响。 注释有许多用途,其中包括:
  • 为编译器提供信息 —— 编译器可以使用注解来检测错误或抑制警告。
  • 编译时和部署时处理 —— 软件工具可以处理注释信息以生成代码、XML 文件等。
  • 运行时处理 —— 一些注解可以在运行时检查。

简单来说,注解可以看作一个标记,它不会改变被标记的代码本身。当使用注解时,必须有相关的代码针对该注解进行处理,否则无任何效果。注解可以标记在类、方法、属性、方法参数等上,可以在编译、类加载、运行时被读取,并对其进行相关操作。

初识注解

声明

public @interface 注解名 {
    // 属性列表
    类型 属性(); 
    // or
    类型 属性() default 默认值; 
}

属性类型

案例

public @interface Name {
    String value();
    int index() default 0;
}

注解看起来接口定义有些类似,只不过是在interface关键字前增加了@符号,所以注解本质上是不是也是一个接口?我们来看看Name.java的字节码文件。

注解到底是什么?

进入Name.java所在文件目录依次执行:javac Name.javajavap -verbose Name.class > Name.txt,即可得到如下代码:

Classfile /Users/uu/IdeaProjects/uu-study/uu-java-study/thinking-java/advance-java-example/src/main/java/com/uucoding/advance/annoation/Name.class
  Last modified 2022-2-9; size 254 bytes
  MD5 checksum 551d1075da4faa2f1700b77eb1e7652f
  Compiled from "Name.java"
public interface com.uucoding.advance.annoation.Name extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #12            // com/uucoding/advance/annoation/Name
   #2 = Class              #13            // java/lang/Object
   #3 = Class              #14            // java/lang/annotation/Annotation
   #4 = Utf8               value
   #5 = Utf8               ()Ljava/lang/String;
   #6 = Utf8               index
   #7 = Utf8               ()I
   #8 = Utf8               AnnotationDefault
   #9 = Integer            0
  #10 = Utf8               SourceFile
  #11 = Utf8               Name.java
  #12 = Utf8               com/uucoding/advance/annoation/Name
  #13 = Utf8               java/lang/Object
  #14 = Utf8               java/lang/annotation/Annotation
{
  public abstract java.lang.String value();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_ABSTRACT

  public abstract int index();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: I#9}
SourceFile: "Name.java"

上述字节码,如我们所想,注解的确是一个接口,继承了java.lang.annotation.Annotation(为注解提供库支持)。

使用

@Name(value = "name")
@Test
public void testName(){
    System.out.println("test @Name");
}

如果注解的属性有value,且使用时只显示设置value属性,可以简写为@Name("name"),如果value的值为数组类型【String[] value();】那么可以简写为@Name({"name1", "name2"}),但如果显示设置多个属性则需要完整书写,如下:

@Name(value = "name", index = 1)
@Test
public void testName1(){
    System.out.println("test @Name");
}

执行上述测试用例,使用@Name标记不会影响程序的运行,也无任何作用。想让注解用起来,请继续向下看!

元注解

元注解也是注解,不过它是一种特殊的注解,专门用来标记普通注解,对普通注解进行解释说明、标记其作用范围。开发一个可用注解,你必须得使用元注解对其进行标记。元注解包含:@Retention@Target@Documented@Inherited@Repeatable,其中@Retention@Target是标记注解的必要元注解。

@Retention

标记注解的保留策略

可用策略

  • @Retention(value = RetentionPolicy.SOURCE)

    • 注解在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • @Retention(value = RetentionPolicy.CLASS)

    • 注解在class文件中保留,不会被加载到 JVM 中,默认值。
  • @Retention(value = RetentionPolicy.RUNTIME)

    • 注解保留到程序运行的时候,会被加载进入到 JVM 中,该策略能被反射读取

示例

@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
}

日常开发中一般是使用@Retention(RetentionPolicy.RUNTIME)策略,Java内置的一些告警注解【@Deprecated、@Override等】,Lombok的注解用的是@Retention(RetentionPolicy.SOURCE)策略,@Retention(RetentionPolicy.CLASS)策略可以用于在编译期生成代理类(没用过,也没见过在用的框架,小伙伴们如果有用到的可以留言讨论讨论)。

@Target

标记注解使用的位置,注解可以标记在类、方法、属性、方法参数等上,是通过@Target元注解来进行控制。@Target的value属性是数组类型,可以支持设置多个值,表示支持多个位置,比如:Spring中的@RequestMapping注解。

位置可用值

  • @Target(ElementType.TYPE)
    • 表示注解可以在接口、类、枚举、注解上使用 【常用】
  • @Target(ElementType.FIELD)
    • 字段、枚举的常量 【常用】
  • @Target(ElementType.METHOD)
    • 注解可以用在方法上 【常用】
  • @Target(ElementType.PARAMETER)
    • 注解可以用在方法参数 【常用】
  • @Target(ElementType.CONSTRUCTOR)
    • 注解可以用在构造函数
  • @Target(ElementType.LOCAL_VARIABLE)
    • 注解可以用在局部变量上
  • @Target(ElementType.ANNOTATION_TYPE)
    • 表示注解可以用在注解上
  • @Target(ElementType.PACKAGE)
    • 注解可以用在包上
  • @Target(ElementType.TYPE_PARAMETER)
    • Java8新增,任何声明类型的地方
  • @Target(ElementType.TYPE_USE)
    • Java8新增,任意使用类型的地方

示例1

@Target({
        ElementType.TYPE,
        ElementType.FIELD,
        ElementType.METHOD,
        ElementType.PARAMETER,
        ElementType.CONSTRUCTOR,
        ElementType.LOCAL_VARIABLE,
        ElementType.ANNOTATION_TYPE,
        ElementType.PACKAGE
})
public @interface TargetTest {
}

// 示例1
@TargetTest
public void testTarget(@TargetTest String name ){
    // 示例2、ElementType.LOCAL_VARIABLE
    @TargetTest int age = 0;
}
// 示例3 - 新建一个package-info.java文件
// ElementType.PACKAGE
@TargetTest package com.uucoding.advance.annoation;

示例2

// 测试 @Target(ElementType.TYPE_USE)
public void testTargetTypeUseTest(){
    throw new @TargetTypeUseTest RuntimeException();
}
// 测试 @Target(ElementType.TYPE_PARAMETER)
public class TypeParameter<@TargetTypeParameterTest T> {
}

在 Java8 发布之前,注解只能应用于声明,Java8新增类型注释TYPE_USETYPE_PARAMETER,类型注释是为了支持改进的 Java 程序分析,让其在编译期间发现错误,以确保更强的类型检查。这俩我们开发很少用,这里就不做赘述,感兴趣的小伙伴可以看看如下两个链接的介绍!

docs.oracle.com/javase/tuto…

docs.oracle.com/javase/tuto…

@Documented

使用@Documented元注解定义的注解,注解将会被生成到javadoc中,如果不需要生成javadoc,此注解不需要使用。

@Inherited

标记子类可以继承父类中的该注解

示例

// 有@Inherited元注解
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface InheritedTest {
}
// 无@Inherited元注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface NoInheritedTest {
}
// 父类
@NoInheritedTest
@InheritedTest
public class InheritedSuper {
}

// 子类
public class InheritedChild extends InheritedSuper{
}

// 测试子类是否含有@InheritedTest注解
@Test
public void testInherited(){
    Class<InheritedChild> inheritedChildClass = InheritedChild.class;
    // 借助反射,获取InheritedChild类上的所有注解信息
    Annotation[] annotations = inheritedChildClass.getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println(annotation);
    }
    // 输出:@com.uucoding.advance.annoation.InheritedTest()
}

@Repeatable

在Java8之前,如果想要标记在同一个地方标记多个注解,我们通常这样写:

// 需要在类上标记多个@MapperScan
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface MapperScan {
    String[] value();

}
// 准备一个存储@MapperScan的容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface MapperScans {
    MapperScan[] value();
}
// 对类进行标记
@MapperScans({
        @MapperScan("com.kanuo"), @MapperScan("com.uucoding")
})
public class MapperScansTest {
}

对于上述的代码,我们更希望更加语义化,表现为如下形式:

@MapperScan("com.kanuo")
@MapperScan("com.uucoding")
public class MapperScansTest {
}

Java8后,我们只需要在@MapperScan注解上新增一个@Repeatable即可实现上述需求,代码更改如下:

  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.TYPE})
+ @Repeatable(MapperScans.class)
  public @interface MapperScan {
      String[] value();

  }

获取注解的方式是没有任何变化的,Java8也依然通过获取MapperScans,通过MapperScan获取值为null,获取类上注解方式如下:

@Test
@DisplayName("测试Repeatable")
public void testRepeatable(){
    //java 8之前
    Class<MapperScansTest> mapperScansTestClass = MapperScansTest.class;
    MapperScans annotation1 = mapperScansTestClass.getAnnotation(MapperScans.class);
    // @com.uucoding.advance.annoation.MapperScans(value=[@com.uucoding.advance.annoation.MapperScan(value=[com.kanuo]), @com.uucoding.advance.annoation.MapperScan(value=[com.uucoding])])
    System.out.println(annotation1);
    //java 8之后
    Class<MapperScanTest> mapperScanTestClass = MapperScanTest.class;
    MapperScans annotation2 = mapperScanTestClass.getAnnotation(MapperScans.class);
    // @com.uucoding.advance.annoation.MapperScans(value=[@com.uucoding.advance.annoation.MapperScan(value=[com.kanuo]), @com.uucoding.advance.annoation.MapperScan(value=[com.uucoding])])
    System.out.println(annotation2);
}

注:如果MapperScansTest类上只有一个@MapperScan注解,使用getAnnotation(MapperScan.class)获取,如果是多个@MapperScan注解,则需要使用getAnnotation(MapperScans.class)

常用内置注解

Java为我们提供了一些开箱即用的内置注解,常用的注解如下:

@Override

覆写父类方法时候加在子类覆写的方法上,不加会有警告信息Missing '@Override' annotation on 'test()' ”

@Deprecated

将代码标记为过时,不建议使用。 如果使用编译器在会发出警告,现代编辑器中表现为entity.setId()

@SafeVarargs

参数安全类型注解,构造或方法是可变参数时,使用该注解会阻止编译器产生unchecked的警告。

@SuppressWarnings

抑制编译器警告,比如在调用@Deprecated标记的方法时候,加上该注解,编译器则忽略警告。

@FunctionalInterface

JDK8提供的注解,用以标记函数式接口,这个我们在「Lambda必知必会」一章有案例介绍。

上述几个注解,实际开发中可以不使用,但是一种规范。

自定义注解

声明注解

public @interface First {
}

设置注解保留策略

@Retention(RetentionPolicy.RUNTIME)
public @interface First {
}

设置注解使用位置

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface First {
}

可选操作

  • 给注解加个属性
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface First {
    String value();
    // 设置默认值
    String name() default "first";
}
  • 让注解打包到javadoc中
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface First {
    String value();
    String name() default "first";
}
  • 设置为可继承的注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Inherited
public @interface First {
    String value();
    String name() default "first";
    
}
  • 设置为可重复注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Inherited
@Repeatable(First.List.class)
public @interface First {
    String value();
    String name() default "first";

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documented
    @Inherited
    public @interface List {
        First[] value();
    }
}

可重复注解这里需要注意:@List的元注解必须和@First的元注解保持一致!

使用注解

@First("value first")
public class FirstDemo {
}

获取注解

前文我们在@Inherited@Repeatable元注解讲解的时候已经简单的了解通过反射获取注解的示例,接下来将基于前文自定义的一些注解,将业务开发中反射对注解的常用操作通过代码逐一演示。

  • 新增一个子类继承FirstDemo, 并对其标记@NoInheritedTest注解,新增一个@FieldOrMethod注解,支持标记方法和属性。代码如下
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface FieldOrMethod {
    boolean isMethod() default false;
}

@NoInheritedTest
public class FirstChildDemo extends FirstDemo{

    @FieldOrMethod
    private String name;

    @FieldOrMethod(isMethod = true)
    public String getName() {
        return name;
    }
}
  • 类上注解操作
@Test
public void testAnnotation(){
    Class<FirstChildDemo> firstChildDemoClass = FirstChildDemo.class;
    // getDeclaredAnnotations 只获取自身的类注解
    Annotation[] declaredAnnotations = firstChildDemoClass.getDeclaredAnnotations();
    for (Annotation declaredAnnotation : declaredAnnotations) {
        System.out.println(declaredAnnotation); // @com.uucoding.advance.annoation.NoInheritedTest()
    }
    // getAnnotations 获取自身和父类的所有注解(父类的只有具有可继承性@Inherited修饰的注解,才可以被获取)
    declaredAnnotations = firstChildDemoClass.getAnnotations();
    for (Annotation declaredAnnotation : declaredAnnotations) {
        System.out.println(declaredAnnotation);
        // @com.uucoding.advance.annoation.First(name=first, value=value first)
        //@com.uucoding.advance.annoation.NoInheritedTest()
    }
    // getDeclaredAnnotation获取自身的类指定注解
    NoInheritedTest noInheritedTest = firstChildDemoClass.getDeclaredAnnotation(NoInheritedTest.class);
    System.out.println(noInheritedTest);
    // getAnnotation获取自身的类指定注解
    First first = firstChildDemoClass.getAnnotation(First.class);
    System.out.println(first); // @com.uucoding.advance.annoation.First(name=first, value=value first)
    // 判断是否存在某个注解
    boolean annotationPresent = firstChildDemoClass.isAnnotationPresent(First.class);
    System.out.println(annotationPresent); // true
}
  • 方法属性上注解操作
@Test
public void testFieldOrMethod() throws NoSuchFieldException, NoSuchMethodException {
    Class<FirstChildDemo> firstChildDemoClass = FirstChildDemo.class;
    Field name = firstChildDemoClass.getDeclaredField("name");
    FieldOrMethod annotation = name.getAnnotation(FieldOrMethod.class);
    System.out.println(annotation);// @com.uucoding.advance.annoation.FieldOrMethod(isMethod=false)
    Method getName = firstChildDemoClass.getDeclaredMethod("getName");
    FieldOrMethod getNameAnnotation = getName.getAnnotation(FieldOrMethod.class);
    System.out.println(getNameAnnotation);// @com.uucoding.advance.annoation.FieldOrMethod(isMethod=true)
}

源码

总结

  • 本章主要通过概念和案例对注解进行全面的概括,包括:元注解、自定义注解、反射与注解;
  • @Retention@Target这两个元注解是注解开发中最常用的两个,也是注解生效的必要注解;
  • 当需要使用反射进行操作时,@Retention的值必须为 @Retention(value = RetentionPolicy.RUNTIME)

最后

  • 感谢铁子们耐心看到最后,如果大家感觉本文有所帮助,麻烦给个赞👍关注➕
  • 由于本人技术有限,文章和代码可能存在错误,希望大家评论指出,万分感激🙏;
  • 同时也欢迎大家V我(uu2coding)一起讨论学习前端、Java知识,一起卷一起进步。