簡單介紹 Java 中的註解 (Annotation)

1. 例子

首先來看一個例子:java

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

這裏用了 @Override, 目的是告訴編譯器這個方法重寫了父類的方法, 若是編譯器發現父類中沒有這個方法就會報錯. 這個註解的做用大抵是防止手滑寫錯方法, 同時加強了程序的可讀性. 這裏須要指出一點, @Override 去掉並不會影響程序的執行, 只是起到標記的做用sql

找到 @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/interfaceapp

參照 @Override, 咱們能夠試着實現本身的註解.框架

2. 自定義註解

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

這個註解與 @Override 相似, 可是把 ElememtType.METHOD 改爲了 ElementType.FIELD, 意思是在成員變量上使用. 把 RetentionPolicy.SOURCE 改爲了 RetentionPolicy.RUNTIME, 表示註解一直持續到代碼運行時.
這樣就定義好了一個註解, 能夠這樣使用jvm

public class Game {
    @Player
    private String A;
}

固然這個註解太簡單了, 咱們能夠加點料, 好比這樣ide

@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;

}

自定義註解大體如上. 給代碼打上註解至關於作了標記, 只有搭配上相應的註解處理流程, 才能算是真正發揮做用. 接下來介紹如何處理註解ui

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() + "\n");
            }
        }
    }

    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);
相關文章
相關標籤/搜索