1. 程式人生 > >Java中的註解原來是這樣回事的

Java中的註解原來是這樣回事的

前言

在我們平常的程式碼開發過程中,遇見過無數的註解,大多數註解都是我們使用的框架所給我們整合好了的,相信也很少有人使用自己編寫的註解,我也如此,但是隻有當你瞭解了註解背後的祕密後,一定會對它有不同的看法。

註解,也被稱為元資料,可以為我們在程式碼中新增資訊提供一種形式化的方法,使我們可以在稍後某個時刻非常方便地使用這些資料

註解的優點

使用註解有許多的優點:

  1. 註解能使編譯器來測試和驗證格式,儲存有關程式的額外資訊。
  2. 註解可以用來生成描述符檔案,有助於減輕編寫“樣板”程式碼的負擔、
  3. 使用註解可以將這些元資料儲存在Java原始碼中。並利用annotation API為我們的註解構造處理工具。
  4. 註解提供編譯器型別檢查以及更加乾淨易讀的便利。

Java中的註解

目前Java提供三種內建註解:

  • @Override,表示當前的方法定義將覆蓋超類中的方法。
  • @Deprecated,如果程式設計師使用了註解為它的元素,那麼編譯器會發出警告資訊。
  • @SuppressWarnings,關閉不當的編譯器警告資訊。

除此之外,Java還另外提供了四種元註解,專門負責新註解的建立。可理解為註解的註解。

@Target:表示該註解可以用於什麼地方。

引數 說明
CONSTRUCTOR 構造器的宣告
FIELD 域宣告(包括enum例項)
LOCAL_VARIABLE 區域性變數宣告
METHOD 方法宣告
PACKAGE 包宣告
PARAMETER 引數宣告
TYPE 類、介面(包括註解型別)或enum宣告

@Retention:表示需要在什麼級別儲存該註解資訊。

引數 說明
SOURCE 註解將被編譯器丟棄
CLASS 註解在class檔案中可用,但會被JVM丟棄
RUNTIME JVM將在執行期也保留註解,因此可以通過反射機制讀取註解的資訊

@Documented:將此註解包含在Javadoc中

@Inherited:允許子類繼承父類中的註解

如何定義註解

註解的定義很像介面的定義,並且與其他任何Java介面一樣,註解也將會編譯成class檔案。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test{
}

除了@符號外,註解的定義很像一個空的介面。在定義一個註解的時候,會需要使用到我們上面的元註解,如@Target@Retention。像我們這裡定義的註解稱為標記註解,因為在註解內沒有任何元素。上面這個註解的使用方式:@Test。

下面我們來看一下hibernate中的@Table註解:

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

    String catalog() default "";

    String schema() default "";

    UniqueConstraint[] uniqueConstraints() default {};

    Index[] indexes() default {};
}

@Target中可以看出這個註解是應用於類、介面上的,並且是在執行期儲存註解資訊。該註解中有5個元素,default為預設值。註解的元素在使用時表現為名-值對的形式,例如我們可以使用@Table(name="myTable")的方式設定該實體類對應的資料庫表名為myTable

註解處理器

當我們編寫好我們的註解後如果沒有用來讀取註解的工具的話,那麼註解對於我們來說也就沒有太大意義了。在Java SE5擴充套件了反射機制的API,以幫助程式設計師構造這類工具。

我們首先定義一個簡單的註解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Person{
    String name() default "I don't have name";
	int age() default 21;
}

我們將該註解用於一個實體類中:

public class MyLove {

    @Person(name = "My name is zhy")
    public String zhy(){
        return "zhy";
    }

    @Person(name = "My name is xyx", age = 19)
    public String xyx(){
        return "xyx";
    }

}

接下來我們編寫註解處理器,通過反射機制來查詢註解中的資訊。

public class MyLoveTest {

    public static void myLoveTest(List<Integer> ages, Class<?> cl){
        Method[] methods = cl.getDeclaredMethods();
        for(Method method : methods){
            Person person = method.getAnnotation(Person.class);
            if(person != null){
                System.out.println("My name is " + person.name() + " and I'm " + person.age());
                ages.remove(new Integer(person.age()));
            }
        }

        for(int i : ages){
            System.out.print("Missing age is " + i);
        }
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Collections.addAll(list, 20, 21, 22);
        myLoveTest(list, MyLove.class);
    }
}

輸出結果如下:

My name is xyx and I'm 19
My name is zhy and I'm 21
Missing age is 20

在這個註解處理器程式中,我們用到了兩個反射的方法:getDeclaredMethods()getAnnotation(),這兩個都是AnnotatedElement介面(Class、Method和Field等類都實現了該介面)。getAnnotation()方法返回指定型別的註解物件,在這裡就是Person。如果被註解的方法上沒有該型別的註解,則返回null值。然後我們通過呼叫name()age()方法從Person物件中提取元素的值。

案例驅動

下面我們寫一個註解小栗子,它將讀取一個實體類,檢查其上的資料庫註解,並生成用來建立資料庫的SQL命令:

下面是我們要使用到的註解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author: zhangocean
 * @Date: 2018/9/23 15:36
 */

//@DBTable註解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    String name() default "";
}

//@Constraints註解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;
    boolean allowNull() default true;
}

//@SQLInteger註解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    int value() default 10;
    String name() default "";
    Constraints constraints() default @Constraints;
}

//@SQLString註解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    int value() default 255;
    String name() default "";
    Constraints constraints() default @Constraints;
}

註解應用到的實體類:

/**
 * @author: zhangocean
 * @Date: 2018/9/23 15:38
 */
@DBTable(name = "user")
public class User {
    @SQLInteger(constraints = @Constraints(primaryKey = true), name = "id")
    private int id;

    @SQLString(30)
    private String username;

    @SQLInteger
    private Integer age;
}

最後就是註解處理器:

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * @author: zhangocean
 * @Date: 2018/9/23 15:43
 */
public class TableCreator {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> userClass = Class.forName("User");
        DBTable dbTable = userClass.getAnnotation(DBTable.class);
		if(dbTable == null){
            System.out.println("No DBTable annotations in class");
        }
        String tableName = dbTable.name();
        if(tableName.length()<1){
            tableName = userClass.getName().toUpperCase();
        }
        List<String> columnNameSqls = new ArrayList<>();
        StringBuilder createTable = new StringBuilder();
        createTable.append("CREATE TABLE ").append(tableName).append("(");
        for(Field field : userClass.getDeclaredFields()){
            String columnName = null;
            Annotation[] annotations = field.getDeclaredAnnotations();
            if(annotations.length < 1){
                continue;
            }
            if(annotations[0] instanceof SQLInteger){
                SQLInteger sqlInteger = (SQLInteger) annotations[0];
                if(sqlInteger.name().length()<1){
                    columnName = field.getName().toUpperCase();
                } else {
                    columnName = sqlInteger.name();
                }
                columnNameSqls.add(columnName + " INT(" +sqlInteger.value() + ") "  + getConstraints(sqlInteger.constraints()));
            }
            if(annotations[0] instanceof SQLString){
                SQLString sqlString = (SQLString) annotations[0];
                if(sqlString.name().length() < 1){
                    columnName = field.getName().toUpperCase();
                }else {
                    columnName = sqlString.name();
                }
                columnNameSqls.add(columnName + " VARCHAR(" + sqlString.value() + ") " + getConstraints(sqlString.constraints()));
            }
        }
        for(int i=0;i<columnNameSqls.size();i++){
            if(i != (columnNameSqls.size()-1)){
                createTable.append("\n      ").append(columnNameSqls.get(i)).append(",");
            } else {
                createTable.append("\n      ").append(columnNameSqls.get(i));
            }
        }
        createTable.append("\n);");
        System.out.println(createTable);
    }

    private static String getConstraints(Constraints con){
        String constranints = "";
        if(!con.allowNull()){
            constranints += "NOT NULL";
        }
        if(con.primaryKey()){
            constranints += "PRIMARY KEY";
        }
        return constranints;
    }

}

先來看看執行結果把:

CREATE TABLE user(
      id INT(10) PRIMARY KEY,
      USERNAME VARCHAR(30) ,
      AGE INT(10)
);

main()方法中,使用forName()載入User實體類,並使用getAnnotation(DBTable.class)檢查該實體類是否帶有@DBTable註解。如果有,就將表名儲存下來。然後讀取這個類的所有域,並用getDeclaredAnnotations()進行檢查,該方法返回一個域上的所有註解。最後用instanceof操作符來判斷這些註解的型別。

在註解中巢狀使用的@Constraints註解被傳遞到getConstraints()方法中,由它負責構造一個包含SQL的String物件。

總結

通過一個簡單的案例來說明註解的使用再合適不過了,對於註解的理解在我們平常的使用過程中也能更加得心應手。

更多文章請關注我的個人部落格:www.zhyocean.cn