java基礎強化——深刻理解java註解(附簡單ORM功能實現)

1.什麼是註解

註解是java1.5引入的新特性,它是嵌入代碼中的元數據信息,元數據是解釋數據的數據。通俗的說,註解是解釋代碼的代碼。這個定義強調了三點,mysql

  • 1.註解是代碼
    這意味着註解能夠被程序讀取並解析。它能夠被編譯器編譯成class文件,也能夠被JVM加載進內存在運行時進行解析。JDK中的"@Override"就是註解。它不只解釋了這是個重寫方法,還能在被錯誤使用(被註解的方法沒有重寫父類方法)時讓編譯器給出錯誤提示。Spring中的「Controller」就是註解,它能夠在運行時被JVM讀取到併爲被其修飾的類建立實例。
  • 2.註解起到的是描述和解釋做用。這點和註釋有點像。但註釋面向的對象主要是開發者,且只能在源碼階段存在;註解面向的對象主要是程序,且能夠再編譯期和運行期存在。
  • 3.註解須要關聯特定的代碼,若是不存在須要解釋的代碼,那麼註解就毫無心義了。

2. 註解的結構以及如何在運行時讀取註解

2.1 註解的組成

下面是一個自定義註解的例子:sql

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
public @interface ClassAnnotation {

    String name() default "";

    boolean singleton() default false;

}

註解由聲明,屬性,元註解三部分構成。數據庫

  • 1.註解聲明
    @interface聲明ClassAnnotation爲註解類型,注意比interface多了個@符號。
  • 2.註解的屬性
    上面定義了兩個屬性:String類型的name屬性,默認值爲空字符串;boolean類型的singleton屬性,默認值爲false.注意雖而後面帶了括號,但並非方法。若是註解內部只定義了一個屬性,該屬性名一般爲value,且在使用的時候能夠省略value=,直接寫值。
    註解的屬性類型支持的類型有:全部基本類型,String,Class,enum,Anotation以及上述類型的數組類型。
  • 3.元註解
    元註解是註解的註解。有點繞,只要知道它是註解,而且使用在註解上,能夠對註解進行解釋就行。上面使用了兩個元註解@Retention@Target。這是最常使用的元註解。關於它們有後面會進行詳細說明。

2.2 註解的類層級結構

任何註解類型都默認繼承自java.lang.annotation包下的Annotation接口,代表這是一個註解類型,這是編譯器自動幫咱們完成的。可是手動繼承Annotation沒有這個效果,即不會把它當成註解類型。甚至Annotation接口自己也並不意味着它是註解類型。很奇怪也很繞,然而很遺憾規則就是這麼定義的。能夠簡單的理解爲:咱們能夠也只能夠經過@interface的方式來定義註解類型,這個註解類型默認會實現Annotation接口。來看看Annotation接口的結構
編程

根據面向接口編程原則,在編寫代碼時能夠用Annotation接口引用不一樣的註解類型,在運行時才經過接口的annotationType()方法得到具體的註解信息。數組

2.3 如何在運行時得到註解信息

註解經過設置能夠一直保留到運行期,此時VM經過反射的方式讀取註解信息。由上面的介紹可知,註解是解釋代碼的代碼,它必須存在於特定的代碼元素之上,能夠是類,能夠是方法,能夠是字段等等。
爲了更好的在運行時解析這些代碼元素上的註解,java在反射包下爲它們提供了一個抽象,以下圖所示

裏面定義了一些獲取該元素上註解信息的方法。app

而Class,Field,Method,Constructor等能夠在運行時被反射獲取的元素都實現了AnnotationElement接口,以下圖所示

所以當咱們在得到了包含註解的Clazz,Method,Field等對象後,能夠直接經過AnnotationElement接口中的方法得到其上的註解信息。框架

3.幾種元註解介紹

3.1 @Retention

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

用來表示被其修飾的註解的生命週期,即該註解的信息會在什麼級別被保留。Retention只有一個屬性value,類型爲RetentionPolicy,這是一個枚舉值,能夠由如下取值ide

  • SOURCE
    源碼有效:表示該註解(被@Retention註解的註解)僅在源碼階段存在,編譯階段就會被編譯器丟棄。
  • CLASS
    編譯期有效:註解信息會被編譯進class文件中,可是不會被JVM加載。當註解未定義Retention值時,這是默認的級別。
  • RUNTIME
    運行期有效:註解信息會被編譯進class文件中,且會被JVM加載並可在運行期被JVM以反射的方式讀取。

3.2 @Target

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

用來表示被其修飾的註解能夠用在什麼地方。該註解只有一個屬性值value,類型爲ElementType數組,這意味着一般註解能夠被用在多個不一樣的地方。來看看ElementType都有哪些值,分別表明什麼意思。工具

  • TYPE
    表示類,接口(包括註解類型),枚舉類型
  • FIELD
    表示類成員
  • METHOD
    表示方法
  • PARAMETER
    表示方法參數
  • CONSTRUCTOR
    表示構造方法
  • LOCAL_VARIABLE
    表示局部變量
  • ANNOTATION_TYPE
    表示註解類型
  • PACKAGE
    表示包
  • TYPE_PARAMETER
    1.8新加,表示類型參數
  • TYPE_USE
    1.8新加,表示類型使用

能夠看到ElementType枚舉值至關多,幾乎囊括了全部元素類型。這也意味着註解幾乎能夠用在全部地方。但最多見得仍是用在類,成員變量和成員方法上。

3.3 @Documented

這是一個標記註解。用來表示被其修飾的註解在被使用時會被Javadoc工具文檔化。

3.4 @Inherited

這也是一個標記註解。表示被其修飾的註解可被繼承。通俗的解釋:若註解A被元註解@Inherited修飾,則當註解A被用在父類上時,其子類也會自動繼承這個註解A。來看下面這個演示的例子。

  • 建立一個被@Inherited描述的自定義註解@InheritedAnnotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface InheritedAnnotation {

}
  • 建立父類,並在類上標註@InheritedAnnotation註解
@InheritedAnnotation
public class SuperClass {

}
  • 子類繼承父類並測試
class TestClass extends SuperClass{

    public static void main(String[] args) {

        Annotation[] annotations = TestClass.class.getAnnotations();

        for(Annotation annotation:annotations){
            System.out.println(annotation);
        }
    }
}
  • 測試結果

能夠看到子類雖然沒有被@InheritedAnnotation註解,可是其繼承的父類上有該註解,故而@InheritedAnnotation註解也做用在了子類上。
原理以下:當JVM要查詢的註解是一個被@Inherited描述的註解,會不斷遞歸的檢查父類中是否存在該註解,若是存在,則會認爲該類也被該註解修飾。

3.5 @Repeatable

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    /**
     * Indicates the <em>containing annotation type</em> for the
     * repeatable annotation type.
     * @return the containing annotation type
     */
    Class<? extends Annotation> value();
}

這是java8種引入的一個新的元註解,被其修飾的註解將可以被在同一個地方重複使用,這在原來是辦不到的。注意每個可重複使用的註解都必須有一個容納這些可重複使用註解的容器註解。這個容器註解就是Repeatable的value屬性值。
來看一個簡單的例子

  • 自定義可重複註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(RepeatableAnnotations.class)
public @interface RepeatableAnnotation {

    String name() default "";
}
  • 自定義可重複註解的容器註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RepeatableAnnotations {

    RepeatableAnnotation[] value();
}

Repeatable(RepeatableAnnotations.class) 指定了@RepeatableAnnotation爲可重複使用的註解,同時指定了該註解的容器註解爲@RepeatableAnnotations。那咱們該如何在運行時得到這些重複註解的信息?

  • 運行時獲取註解
@RepeatableAnnotation("first")
@RepeatableAnnotation("second")
public class AnnotationTest {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {

        Class<?> clazz = Class.forName("com.takumiCX.AnnotationTest");
        //當元素上有重複註解時,使用該方法會返回null
        RepeatableAnnotation annotation1 = clazz.getAnnotation(RepeatableAnnotation.class);
        System.out.println(annotation1);

        //使用該方法獲取元素上的重複註解
        RepeatableAnnotation[] annotations = clazz.getAnnotationsByType(RepeatableAnnotation.class);
        for(Annotation annotation:annotations){
            System.out.println(annotation);
        }
    }
}

注意多個重複註解會被自動存放到與之關聯的容器註解裏。因此咱們這裏要得到全部@RepeatableAnnotation註解,不能使用getAnnotation方法,而應該使用getAnnotationByType方法。最後的結果以下

4.使用反射和註解完成簡單的ORM功能

4.1 ORM原理簡介

ORM是對象關係映射的意思。他創建起了如下映射關係:

  • 類對應於表
  • 對象對應於表中的記錄
  • 對象的屬性對應於表的字段

有了這種映射關係,咱們在編寫代碼時就能夠經過操做對象來映射對數據庫表的操做,好比添加記錄,更新記錄,刪除記錄等等。常見的Mybatis,Hibernate就是ORM框架。而實現ORM功能最經常使用的手段就是註解+反射。由註解維護這種映射關係,而後運行期經過反射技術解析註解,完成對應關係的轉換,從而造成一句完整的sql去執行。
下面以建表爲例,實現簡單的ORM功能。

4.2 ORM實戰

  • 自定義表註解,完成類和表的映射。
/**
 * 自定義表註解,完成類和表的映射
 */
@Retention(RetentionPolicy.RUNTIME) //由於要使用到反射,故註解信息必須保留到運行時
@Target(ElementType.TYPE)//只能用在類上
public @interface MyTable {

    //表名
    String value();
}
  • 自定義字段註解
/**
 * 自定義字段註解,完成類屬性和表字段的映射
 */
@Retention(RetentionPolicy.RUNTIME)//要反射,故註解信息須要保留到運行期
@Target(ElementType.FIELD)//只能用在類屬性上
public @interface MyColumn {

    //字段名
    String value();

    //字段類型,默認爲字符串類型
    String type() default "VARCHAR(30)";//字段類型,默認爲VARCHAR類型

    //類型爲註解類型的字段約束,默認的約束爲:非主鍵,非惟一字段,不能爲null
    Constraints constraint() default @Constraints;
}
  • 自定義字段約束註解
/**
 * 約束註解:主鍵,是否爲空,是否惟一等信息。
 */
@Retention(RetentionPolicy.RUNTIME)//運行期
@Target(ElementType.FIELD)//只能在類屬性上使用
public @interface Constraints {

    //字段是否爲主鍵約束
    boolean primaryKey() default false;
    //字段是否容許爲null
    boolean nullable() default false;

    //字段是否惟一
    boolean unique() default false;

}
  • 帶註解的實體類
/**
 * 帶註解的實體類,創建了對象和表的映射關係,能夠再運行時被解析
 */
@MyTable("t_user")
public class User {

    //主鍵,對應表字段id,類型爲VARCHAR
    @MyColumn(value = "id", constraint = @Constraints(primaryKey = true))
    private String id;

    //對應表字段name,類型爲類型爲VARCHAR
    @MyColumn(value = "name")
    private String name;

    //對應表字段age,類型爲INT,且可爲null
    @MyColumn(value = "age", type = "INT", constraint = @Constraints(nullable = true))
    private int age;

    //對應表字段phone_number,類型爲VARCHAR,且有惟一約束
    @MyColumn(value = "phone_number", constraint = @Constraints(unique = true))
    private String phoneNumber;
}
  • 運行時註解解析器
/**
 * 運行時註解解析器
 */
public class TableGenerator {

    /**
     * 運行時解析註解生成對應的建表語句
     *
     * @param clazz 與表對應的實體的Class對象
     * @return
     */
    public static String genSQL(Class clazz) {

        String table;//表名
        List<String> columnSegments = new ArrayList<>();
        //獲取表註解
        MyTable myTable = (MyTable) clazz.getAnnotation(MyTable.class);
        if (myTable == null) {
            throw new IllegalArgumentException("表註解不能爲空!");
        }
        //獲取表名
        table = myTable.value();
        //獲取全部字段
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            MyColumn column = field.getAnnotation(MyColumn.class);
            if (column == null) {
                continue;//爲null說明該字段不爲映射字段,也就是沒有加上字段註解
            }
            StringBuilder columnSegement = new StringBuilder();//字段分片,eg:"id varchar(50) primary key"
            String columnType = column.type().toUpperCase();//字段類型
            String columnName = column.value().toUpperCase();//字段名
            columnSegement.append(columnName).append(" ").append(columnType).append(" ");
            Constraints constraint = column.constraint();
            boolean primaryKey = constraint.primaryKey();
            boolean nullable = constraint.nullable();
            boolean unique = constraint.unique();
            if (primaryKey) {
                //主鍵惟一且不爲空
                columnSegement.append("PRIMARY KEY ");
            } else if (!nullable) {
                //字段不爲null
                columnSegement.append("NOT NULL ");
            }
            if (unique) {
                //有惟一鍵
                columnSegement.append("UNIQUE ");
            }
            columnSegments.add(columnSegement.toString());
        }

        if (columnSegments.size() < 1) {
            //沒有映射任何表字段,拋出異常
            throw new IllegalArgumentException("沒有映射任何表字段!");
        }
        StringJoiner joiner = new StringJoiner(",", "(", ")");
        for (String segement : columnSegments) {
            joiner.add(segement);
        }
        //生成SQL語句
        return String.format("CREATE TABLE %s", table) + joiner.toString();
    }
}

經過該解析器的genSQL方法在運行時生成建表SQL,經過傳入的Class參數在運行時解析類和屬性上的註解,分別獲得表名,字段名,字段類型,約束條件等信息,而後拼裝成SQL。因爲只是爲了作演示,對SQL語法的支持比較弱,只容許字段爲int和varchar類型。且解析語法時也沒有考慮一些邊界狀況。可是經過這段代碼演示能夠知道ORM框架在解析註解時的大概工做和流程是怎麼樣的。

  • 測試
public class TableGeneratorTest {

    public static void main(String[] args) {
        String sql = TableGenerator.genSQL(User.class);
        System.out.println(sql);
    }
}

最後獲得的建表語句以下

CREATE TABLE t_user(ID VARCHAR(30) PRIMARY KEY ,NAME VARCHAR(30) NOT NULL ,AGE INT ,PHONE_NUMBER VARCHAR(30) NOT NULL UNIQUE )

最後咱們驗證下生成的建表SQL語法是否有問題,在mysql客戶端上執行該sql

如上圖所示,執行成功,說明咱們的建表語句是正確的。

相關文章
相關標籤/搜索