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


1.什么是注解

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

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

2.1 注解的組成

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

@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在反射包下為它們提供了一個抽象,如下圖所示

里面定義了一些獲取該元素上注解信息的方法。

而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,這是一個枚舉值,可以由以下取值

  • 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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM