zoukankan      html  css  js  c++  java
  • 简单介绍 Java 中的注解 (Annotation)

    1. 例子

    首先来看一个例子:

     @Override
     public String toString() {
        return "xxxxx";
     }

    这里用了 @Override, 目的是告诉编译器这个方法重写了父类的方法, 如果编译器发现父类中没有这个方法就会报错. 这个注解的作用大抵是防止手滑写错方法, 同时增强了程序的可读性. 这里需要指出一点, @Override 去掉并不会影响程序的执行, 只是起到标记的作用

    找到 @Override 的实现

    package java.lang;
    
    import java.lang.annotation.*;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }

    关注点有三个: @Target@Retention@interface. 从形状可以看出, 前两个也是注解. 它们用于描述注解, 称为 元注解 . @interface 用于定义一个注解, 类似于 public class/interface XXX 中的 class/interface

    参照 @Override, 我们可以试着实现自己的注解.

    2. 自定义注解

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

    这个注解与 @Override 类似, 但是把 ElememtType.METHOD 改成了 ElementType.FIELD, 意思是在成员变量上使用. 把 RetentionPolicy.SOURCE 改成了 RetentionPolicy.RUNTIME, 表示注解一直持续到代码运行时.
    这样就定义好了一个注解, 可以这样使用

    public class Game {
        @Player
        private String A;
    }

    当然这个注解太简单了, 我们可以加点料, 比如这样

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Player {
        String name() default "PT";
        int ATK() default 1;
        int DEF() default 0;
    }

    使用的时候就可以定义一些值了

    public class Game {
        @Player(name = "CC", ATK = 2, DEF = 1)
        private String A;
    
        @Player(DEF = 2)
        private String B;
    }

    注解元素必须有特定的值, 不指定值的话就会使用默认的 default 值.
    注解里有一个相对特殊的方法 value(), 使用注解时只给 value 赋值时, 可以只写值. 例子如下

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Player {
        String name() default "PT";
        int ATK() default 1;
        int DEF() default 0;
        double value();
    }
    public class Game {
    
        @Player(1.0)
        private String A;
    
        @Player(value = 3.0, DEF = 0)
        private String B;
    
    }

    自定义注解大致如上. 给代码打上注解相当于做了标记, 只有搭配上相应的注解处理流程, 才能算是真正发挥作用. 接下来介绍如何处理注解

    3. 注解处理器

    这里使用反射获取注解信息. 只有标注为 RetentionPolicy.RUNTIME 的注解可以这么提取信息.

    /**
     * 注解处理器
     */
    public class PlayerProcessor {
    
        public static void process() {
            // 获取成员变量
            Field[] fields = Game.class.getDeclaredFields();
            for (Field field : fields) {
                // 判断是否是 Player 注解
                if (field.isAnnotationPresent(Player.class)) {
                    Player annotation = field.getAnnotation(Player.class);
                    System.out.println("name = " + annotation.name());
                    System.out.println("attack = " + annotation.ATK());
                    System.out.println("defence = " + annotation.DEF());
                    System.out.println("type = "+ annotation.annotationType() + "
    ");
                }
            }
        }
    
        public static void main(String[] args) {
            process();
        }
    }

    输出如下

    name = CC
    attack = 2
    defence = 2
    type = interface Player
    
    name = PT
    attack = 1
    defence = 10
    type = interface Player

    这样粗略地介绍完了创建注解到处理注解的流程. 下面讲一下注解中的几个概念.

    4. 概念

    1. 元注解

    1. 作用

    描述注解的注解, 在创建新注解的时候使用

    2. 分类

    1. @Target
    • 注解的作用范围

    • 分类

      • FIELD : 成员变量, 包括枚举常量

      • LOCAL_VARIABLE : 局部变量

      • METHOD : 方法

      • PARAMETER : 参数

      • TYPE : 类、接口(包括注解类型) 或 enum 声明

      • ANNOTATION_TYPE : 注解类型

      • 等等

    • 实现

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

    ElementType[] 是枚举类型的数组, 定义了上面的分类( FIELD, METHOD 等 ). @Target(ElementType.ANNOTATION_TYPE) 表示 @Target 只能用于修饰注解

    2. @Retention
    • 注解的生命周期, 即注解到什么时候有效

    • 分类

      • SOURCE

        • 注解只保留在源文件, 当 Java 文件编译成 class 文件的时候, 注解被遗弃

      • CLASS

        • 注解被保留到 class 文件, jvm 加载 class 文件时候被遗弃. 这是默认的生命周期

      • RUNTIME

        • 注解不仅被保存到 class 文件中, jvm 加载 class 文件之后, 仍然存在, 保存到 class 对象中, 可以通过反射来获取

    • 实现

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

    RetentionPolicy 是枚举类型( SOURCE, CLASS, RUNTIME )
    上述代码表示 @Retention 是运行时注解, 且只能用于修饰注解

    3. @Document
    • 表示注解是否能被 javadoc 处理并保留在文档中

    • 实现

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

    表明它自身也会被文档化, 是运行时注解, 且只能用于修饰注解类型

    • 例子

    注解类没有加 @Document

    public @interface Doc {
    }

    被文档化的类

    public class DocExample {
        @Doc
        public void doc() {}
    }

    生成的 javadoc
    clipboard.png

    加上 @Document 后

    @Document
    public @interface Doc {
    }

    生成的 javadoc
    clipboard.png

    4. @Inherited
    • 表示注解能否被继承, 不常见

    • 实现

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Inherited {
    }
    • 例子

    public class TestInherited {
        @Inherited
        @Retention(RetentionPolicy.RUNTIME) // 设成 RUNTIME 才能输出信息
        @interface Yes {}
    
        @Retention(RetentionPolicy.RUNTIME)
        @interface No {}
    
        @Yes
        @No
        class Father {}
    
        class Son extends Father {}
    
        public static void main(String[] args) {
            System.out.println(Arrays.toString(Son.class.getAnnotations()));
        }
    }

    输出: [@TestInherited$Yes()] 
    说明注解被继承了, 也就是用反射的时候子类可以得到父类的注解的信息

    2. 标准注解 (内建注解)

    就是 jdk 自带的注解

    1. @Override

    • 作用是告诉编译器这个方法重写了父类中的方法

    2. @Deprecated

    • 表明当前的元素已经不推荐使用

    3. @SuppressWarnings

    • 用于让编译器忽略警告信息

    5. 什么是注解

    现在对注解的了解应该更深一些了. 下面概括一下什么是注解. 
    注解的定义如下

    注解是一种应用于类、方法、参数、变量、构造器及包声明中的特殊修饰符。是一种由 JSR-175 标准选择用来描述元数据的一种工具

    简单来说, 注解就是代码的 元数据 metadata , 包含了代码自身的信息, 即 描述代码的代码 . 我们可以使用注解给代码打上"标记", 通过解析 class 文件中相应的标记, 就可以做对应的处理.

    需要注意的是, 注解 没有行为, 只有数据 , 是一种被动的信息, 不会对代码的执行造成影响, 需要配套的工具进行处理.
    我们再来看一下 @Override 的声明

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }

    这是一个源码级别的注解, 不会保留到 class 文件中. 
    这里有一个问题, @Override 这里并没有实现, 那是怎们实现对方法名称的检查的 ? 
    显然, 这里能看到注解的只有编译器, 所以编译器是这段注解的 "消费者", 也就是编译器实现了这部分业务逻辑.

    6. 为什么要用注解

    1. 标记, 用于告诉编译器一些信息

    2. 编译时动态处理, 如动态生成代码

    3. 运行时动态处理, 如得到注解信息

    后面两个主要是用于一些框架中

    说到注解的话不得不提到 xml, 许多框架是结合使用这两者的.
    xml 的优点是容易编辑, 配置集中, 方面修改, 缺点是繁琐==, 配置文件过多的时候难以管理.如果只是简单地配置参数, xml 比较适合
    注解的优点是配置信息和代码放在一起, 增强了程序的内聚性, 缺点是分布在各个类中, 不宜维护.
    如果要把某个方法声明为服务, 注解是更优的选择

    7. 对注解底层实现的探讨

    现在我们知道注解是 元数据, 也知道注解需要配合处理器使用, 那注解本质上是什么呢.
    我们回到自定义注解

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Player {
        String name() default "PT";
        int ATK() default 1;
        int DEF() default 0;
    }

    编译后对字节码做一些处理: javap -verbose Player.class
    可以看到

    Last modified 2017-5-26; size 498 bytes
      MD5 checksum 4ca03164249758f784827b6d103358de
      Compiled from "Player.java"
    public interface Player extends java.lang.annotation.Annotation
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
    Constant pool:
       #1 = Class              #23            // Player
       #2 = Class              #24            // java/lang/Object
       #3 = Class              #25            // java/lang/annotation/Annotation
       #4 = Utf8               name
       #5 = Utf8               ()Ljava/lang/String;
       #6 = Utf8               AnnotationDefault
       #7 = Utf8               PT
       #8 = Utf8               ATK
       #9 = Utf8               ()I
      #10 = Integer            1
      #11 = Utf8               DEF
      #12 = Integer            0
      #13 = Utf8               SourceFile
      #14 = Utf8               Player.java
      #15 = Utf8               RuntimeVisibleAnnotations
      #16 = Utf8               Ljava/lang/annotation/Target;
      #17 = Utf8               value
      #18 = Utf8               Ljava/lang/annotation/ElementType;
      #19 = Utf8               FIELD
      #20 = Utf8               Ljava/lang/annotation/Retention;
      #21 = Utf8               Ljava/lang/annotation/RetentionPolicy;
      #22 = Utf8               RUNTIME
      #23 = Utf8               Player
      #24 = Utf8               java/lang/Object
      #25 = Utf8               java/lang/annotation/Annotation
    {
      public abstract java.lang.String name();
        descriptor: ()Ljava/lang/String;
        flags: ACC_PUBLIC, ACC_ABSTRACT
        AnnotationDefault:
          default_value: s#7
      public abstract int ATK();
        descriptor: ()I
        flags: ACC_PUBLIC, ACC_ABSTRACT
        AnnotationDefault:
          default_value: I#10
      public abstract int DEF();
        descriptor: ()I
        flags: ACC_PUBLIC, ACC_ABSTRACT
        AnnotationDefault:
          default_value: I#12}
    SourceFile: "Player.java"
    RuntimeVisibleAnnotations:
      0: #16(#17=[e#18.#19])
      1: #20(#17=e#21.#22)

    这里需要注意的是这个
    public interface Player extends java.lang.annotation.Annotation
    意思已经很明显了, 注解是继承了 Annotation 类的 接口.

    那么通过反射获得的实例是哪来的呢 ? 通过看源码可以发现, 在用 XXX.class.getAnnotation(XXX.class) 创建一个注解实例时, 用到了 AnnotationParser.parseAnnotations 方法.

    在 openjdk 8 的 sun.reflect.annotation.AnnotationParser.java 文件中, 有方法

    public static Annotation annotationForMap(final Class<? extends Annotation> type, final Map<String, Object> memberValues) {
        return AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
            public Annotation run() {
                return (Annotation) Proxy.newProxyInstance(
                    type.getClassLoader(), new Class<?>[] { type },
                    new AnnotationInvocationHandler(type, memberValues));
            }});
    }

    这里的 AnnotationInvocationHandler 实现了 InvocationHandler 接口, 所以运行期间获得的实例其实是通过 动态代理 生成的.

    8. 实际项目举例

    这里实现一个类似于 ORM 的功能, 加深一下对运行时注解的理解

    注解类 

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Table {
        String name();
    }

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Column {
        String name();
    }

    实体类

    /**
     * Created by away on 2017/5/27.
     */
    @Table(name = "city")
    public class City {
        @Column(name = "city_id")
        private int id;
    
        @Column(name = "city_name")
        private String name;
    
        // getset
    }

    SQL 方法类

    /**
     * Created by away on 2017/5/27.
     */
    public class ORMSupport<T> {
        public void save(T entity) {
            StringBuffer sql = new StringBuffer(30);
            sql.append("insert into ");
            
            processTable(entity, sql);
            processField(entity, sql);
    
            System.out.println(sql);
        }
    
        private void processTable(T entity, StringBuffer sql) {
            Table table = entity.getClass().getDeclaredAnnotation(Table.class);
            if (table != null) {
                sql.append(table.name()).append(" (");
            }
        }
    
        private void processField(T entity, StringBuffer sql) {
            Field[] fields = entity.getClass().getDeclaredFields();
    
            StringBuilder val = new StringBuilder();
            val.append("(");
    
            String comma = "";
            for (Field field : fields) {
                if (field.isAnnotationPresent(Column.class)) {
                    String name = field.getAnnotation(Column.class).name();
                    sql.append(comma).append(name);
                    val.append(comma).append(getColumnVal(entity, field.getName()));
                }
                comma = ", ";
            }
    
            sql.append(") values ")
                    .append(val)
                    .append(");");
        }
    
        private Object getColumnVal(T entity, String field) {
            StringBuilder methodName = new StringBuilder();
            String firstLetter = (field.charAt(0) + "").toUpperCase();
            methodName.append("get")
                    .append(firstLetter)
                    .append(field.substring(1));
            try {
                Method method = entity.getClass().getMethod(methodName.toString());
                return method.invoke(entity);
            } catch (Exception e) {
                e.printStackTrace();
                return "";
            }
        }
    }

    DAO

    /**
     * Created by away on 2017/5/27.
     */
    public class CityRepository extends ORMSupport<City> {
    }

    测试类

    /**
     * Created by away on 2017/5/27.
     */
    public class ORMTest {
    
        public static void main(String[] args) {
            City city = new City();
            city.setId(1);
            city.setName("nanjing");
    
            CityRepository cityRepository = new CityRepository();
            cityRepository.save(city);
        }
    }

    输出

    insert into city (city_id, city_name) values (1, nanjing);
  • 相关阅读:
    保留字段数组,一定要用char
    VirtualBox安装CentOS 7及其相关配置
    istringstream是支持>>一个bool的,但为什么不用?
    用vector实现一个变长数组
    C语言为什么被const声明的变量不是一个常量表达式
    不咬文嚼字的理由
    int变量赋值给char变量的本质
    #include <> 和 #include "" 的区别
    C++中匿名对象应当是一个左值
    js实战之-间断文字滑动
  • 原文地址:https://www.cnblogs.com/exmyth/p/11396801.html
Copyright © 2011-2022 走看看