1. 程式人生 > >用自定義註解做點什麼

用自定義註解做點什麼

請看前言

你不一定聽過註解,但你一定對@Override不陌生。

當我們重寫父類方法的時候我們就看到了@Override。我們知道它表示父類方法被子類重寫了。

現在告訴你,@Override就是一個註解。

也許你會疑惑註解是什麼?
註解(annotation)是JDK5之後引進的新特性,是一種特殊的註釋,之所以說它特殊是因為不同於普通註釋(comment)能存在於原始碼,而且還能存在編譯期跟執行期,會最終編譯成一個.class檔案,所以註解能有比普通註釋更多的功能。
簡單來說,註解是一個比春藥還猛的東西。

接下來,先入個門,然後通過實戰來證明註解有多“猛”。

PS : 如果已經瞭解的小夥伴可自行跳到

自定義註解實戰

自定義註解入門

我們對於註解的認識大多數來源於標準註解(也稱為內建註解)。

標準註解 表示的意義
@Override 用於標識該方法繼承自超類
當父類的方法被刪除或修改了,編譯器會提示錯誤資訊
@Deprecated 表示該類或者該方法已經不推薦使用
如果使用者還是要使用,會生成編譯的警告
@SuppressWarnings 用於忽略的編譯器警告資訊

Java不僅僅提供我們原有的註解使用,它還允許我們自定義註解。比如你可以像這樣:


public @interface DoSomething {
    public String name() default "write";
}

這是最簡單的註解宣告。
儘管看上去像是介面的寫法,但完全不是一回事。這一點要注意。
而使用註解也很簡單,可以像這樣:


@DoSomething(name = "walidake")//可以顯式傳值進來,此時name=walidake
public class UseAnnotation {

}

@DoSomething//如果不傳值,則預設name=我們定義的預設值,即我們上面定義的"write"
public class UseAnnotation {

}

需要注意的是當註解有value()方法時,不需要指明具體名稱


public @interface DoSomething {
    public String value();
    public String name() default "write";
}

@DoSomething("walidake")
public class UseAnnotation {

}

然而“最簡單的自定義註解”並沒有特別的意義。所以,這時候我們需要引入一個元註解的概念。

我們需要知道這些概念:
“普通註解”只能用來註解“程式碼”,而“元註解”只能用來註解 “普通註解”
自定義註解是“普通註解”

JDK5時支援的元註解有@Documented @Retention @Target @Inherited,接下來分別介紹它們修飾註解的效果。


@Documented
@interface DocumentedAnnotation{

}

@interface UnDocumentedAnnotation{

}

@DocumentedAnnotation
@UnDocumentedAnnotation
public class UseDocumentedAnnotation{}

開啟小黑窗,執行javadoc UseDocumentedAnnotation.java

執行結果:
這裡寫圖片描述

結論:可以看到,被@Documented修飾的註解會生成到javadoc中,如@DocumentedAnnotation。
而不被@Documented修飾的註解(@UnDocumentedAnnotation)不會生成到javadoc中。

註解的級別
@Retention可以設定註解的級別,分為三種,都有其特定的功能。
這個元註解是我們關注的重點,後面實戰我們會用到。

註解級別 存在範圍 主要用途
SOURCE 原始碼級別 註解只存在原始碼中 功能是與編譯器互動,用於程式碼檢測。
如@Override,@SuppressWarings。
額外效率損耗發生在編譯時
CLASS 位元組碼級別 註解存在原始碼與位元組碼檔案中 主要用於編譯時生成額外的檔案,如XML,Java檔案等,但執行時無法獲得。
這個級別需要新增JVM載入時候的代理(javaagent),使用代理來動態修改位元組碼檔案
RUNTIME 執行時級別 註解存在原始碼,位元組碼與Java虛擬機器中 主要用於執行時反射獲取相關資訊

限制註解使用的範圍
註解預設可以修飾各種元素,而使用@Target可以限制註解的使用範圍。

例如,可以限定註解只能修飾方法。


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

上面的程式碼將註解的使用範圍限制在了方法上,而不能用來修飾類。

試著用@Override修飾類會得到“The annotation @Override is disallowed for this location”的錯誤。

@Target支援的範圍(參見ElementType):

1) 類,介面,註解;
2) 屬性域;
3) 方法;
4) 引數;
5) 建構函式;
6) 區域性變數;
7) 註解型別;
8) 包

註解的繼承
@Inherited可以讓註解類似被“繼承”一樣。
通過使用@Inherited,可以讓子類物件使用getAnnotations()獲取父類@Inherited修飾的註解。


@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Inheritable{

}

@interface UnInheritable{

}

public class UseInheritedAnnotation{
    @UnInheritable
    @Inheritable
    public static class Super{

    }

    public static class Sub extends  Super {

    }

    public static void main(String... args){

        Super instance=new Sub();
        //result : [@com.walidake.annotation.util.Inheritable()]
        System.out.println(Arrays.toString(instance.getClass().getAnnotations()));
    }
}

我們乾脆用@Documented檢視類結構。發現:

這裡寫圖片描述

這裡寫圖片描述

這是不是恰恰證明了這種是偽繼承的說法,而不是真正的繼承。

自定義註解實戰

引言
Java Web開發中,對框架的理解和掌握是必須的。而在使用大多數框架的過程中,一般有兩種方式的配置,一種是基於xml的配置方式,一種是基於註解的方式。然而,越來越多的程式設計師(我)在開發過程中享受到註解帶來的簡便,並義無反顧地投身其中。

ORM框架,像Hibernate,Mybatis就提供了基於註解的配置方式。我們接下來就使用自定義註解實現袖珍版的Mybatis,袖珍版的Hibernate。

這很重要
說明:實戰的程式碼會被文章末尾附上。而實際上在之前做袖珍版框架的時候並沒有想到會拿來做自定義註解的Demo。因此給出的程式碼涉及了其他的一些技術,例如資料庫連線池,動態代理等等,比較雜。
在這個篇幅我們只討論關於自定義註解的問題,至於其他的技術後面會開多幾篇博文闡述。(當然這麼多前輩面前不敢造次,有個討論學習的氛圍是很好的~)

那麼在自定義註解框架前,我們需要花點時間瀏覽以下幾個和Annotation相關的方法。

方法名 用法
Annotation getAnnotation(Class annotationType) 獲取註解在其上的annotationType
Annotation[] getAnnotations() 獲取所有註解
isAnnotationPresent(Class annotationType) 判斷當前元素是否被annotationType註解
Annotation[] getDeclareAnnotations() 與getAnnotations() 類似,但是不包括父類中被Inherited修飾的註解

Mybatis 自定義註解

本節目標:自定義註解實現Mybatis插入資料操作。
本節要求:細心觀察使用自定義註解的步驟。

Step 1 :宣告自定義註解。


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Insert {
    public String value();
}

Step 2 : 在規定的註解使用範圍內使用我們的註解


public interface UserMapper {

    @Insert("insert into user (name,password) values (?,?)")
    public void addUser(String name,String password);

}

Step 3 : 通過method.getAnnotation(Insert.class).value()使用反射解析自定義註解,得到其中的sql語句


//檢查是否被@Insert註解修飾
if (method.isAnnotationPresent(Insert.class)) {
    //檢查sql語句是否合法
    //method.getAnnotation(Insert.class).value()取得@Insert註解value中的Sql語句
    sql = checkSql(method.getAnnotation(Insert.class).value(),
            Insert.class.getSimpleName());
    //具體的插入資料庫操作
    insert(sql, parameters);
}

Step 4 : 根據實際場景呼叫Step 3的方法


UserMapper mapper = MethodProxyFactory.getBean(UserMapper.class);
mapper.addUser("walidake","665908");

執行結果:
這裡寫圖片描述

以上節選自annotation中Mybatis部分。具體CRUD操作請看原始碼。

總結一下從上面學到的東西:
1.宣告自定義註解,並限制適用範圍(因為預設是通用)
2.規定範圍內使用註解
3.isAnnotationPresent(Insert.class)檢查註解,getAnnotation(Insert.class).value()取得註解內容
4.根據實際場景應用

Hibernate 自定義註解

本節目標:自定義註解使實體自動建表(即生成建表SQL語句)
本節要求:動手操作,把未給全的程式碼補齊。
本節規劃:仿照Hibernate,我們大概會需要@Table,@Column,還有id,我們這裡暫且宣告為@PrimaryKey

仿照自定義Mybatis註解的步驟:


/**
 * 可根據需要自行定製功能
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
    String name() default "";
}


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
    // 列名 預設為""
    String name() default "";

    // 長度 預設為255
    int length() default 255;

    // 是否為varchar 預設為true
    boolean varchar() default true;

    // 是否為空 預設可為空
    boolean isNull() default true;
}


/**
 * 有需要可以拆分成更小粒度
 * @author walidake
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface PrimaryKey {
    String name() default "";
}

完成Step 1,接下來是Step 2。


@Table
public class Person {
    @PrimaryKey
    private int id;

    @Column(isNull = false, length = 20)
    private String username;
    ...
}

Step 3,新建一個叫做SqlUtil的類,使用Class(實體類).isAnnotationPresent(Table.class)取到@Table註解的內容。

而我們如何取到@Column和@PrimaryKey的內容?
使用反射,我們可以很容易做到。


    // 反射取得所有Field
    Field[] fields = clazz.getDeclaredFields();
    ...
    ...
    // 獲取註解物件
    column = fields[i].getAnnotation(Column.class);
    // 設定訪問私有變數
    fields[i].setAccessible(true);
    // 取得@Column的內容
    columnName = "".equals(column.name()) ? fields[i].getName(): column.name();

反射的內容後面再寫。(感覺每一篇都給自己挖了很多坑後面去填)

Step 4套入使用場景


String createSql = SqlUtil.createTable(clazz);
...
connection.createStatement().execute(createSql);

執行結果:
這裡寫圖片描述

執行結果正確!

自此我們完成了實戰模組的內容。當然關於Hibernate的CRUD也可以用同樣的方法做到,更進一步還可以把二級快取整合進來,實現自己的一個微型框架。儘管現有的框架已經很成熟了,但自己實現一遍還是能收穫很多東西。

可以看出來,註解簡化了我們的配置。每次使用註解只需要@註解名就可以了,就跟吃春藥一樣“爽”。不過由於使用了反射,後勁太“猛”,jvm無法對程式碼優化,影響了效能。這一點最後也會提及。

另外提一點,之前想格式化hibernate生成的SQL,做大量搜尋後被告知“Hibernate 使用的是開源的語法解析工具 Antlr,需要進行 SQL 語法解析,將 SQL 語句整理成語法樹”。也算一個坑吧~
不過後來找到一個除了建表SQL以外的格式化工具類,覺得還不錯就也分享了。可以在原始碼中找到。

最後說點什麼
可以發現我們使用執行時註解來搭建我們的袖珍版ORM框架,因為執行時註解來搭建框架相對容易而且適用性也比較廣,搭建的框架使用起來也比較簡單。但在此基礎上因為需要用到反射,其效率效能相對不高。因此,多數Web應用使用執行時註解,而像Android等對效率效能要求較高的平臺一般使用原始碼級別註解來搭建。下一節我們討論怎麼玩一玩原始碼級註解。