Java進階知識4:註解

前言:註解目前非常的流行,很多主流框架都支持註解,特別是在Java SSM框架之中存在各種註解,因爲後續會學習這幾個框架,所有需要先將這幾個框架中要用到的知識點反射機制和註解先學習一下。日常項目中我們也可以用到註解,編寫代碼更簡潔高效。

一、註解的概念

Java 註解(Annotation)又稱 Java 標註,是 JDK5.0 引入的一種註釋機制。

Java 語言中的類、方法、變量、參數和包等都可以被標註。和 Javadoc 不同,Java 標註可以通過反射獲取標註內容。在編譯器生成類文件時,標註可以被嵌入到字節碼中。Java 虛擬機可以保留標註內容,在運行時可以獲取到標註內容 。 當然它也支持自定義 Java 標註。

1、從JDK5.0始,Java增加對元數據的支持,也就是註解,註解與註釋是有一定區別的。可以把註解理解爲代碼裏的特殊標記,這些標記可以在編譯,類加載,運行時被讀取,並執行相應的處理。通過註解,開發人員可以在不改變原有代碼和邏輯的情況下在源代碼中嵌入補充信息(Spring的AOP面向切面編程)。

值得注意的是,註解不是代碼本身的一部分。註解對於代碼的運行效果沒有直接影響,同樣無法改變代碼本身,註解主要給編譯器及工具類型的軟件用的。註解的提取需要藉助於 Java 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。

2、註解的使用場景

Java 官方文檔寫明:

註解是一系列元數據,它提供數據用來解釋程序代碼,但是註解並非是所解釋的代碼本身的一部分。註解對於代碼的運行效果沒有直接影響。註解主要針對的是編譯器和其它工具軟件(SoftWare tool)使用的。

註解有許多用處,主要如下:

a、提供信息給編譯器: 編譯器可以利用註解來探測錯誤和警告信息

b、編譯階段時的處理: 軟件工具可以用來利用註解信息來生成代碼、Html文檔或者做其它相應處理。

c、運行時的處理: 某些註解可以在程序運行的時候接受代碼的提取,藉助反射手段

當開發者使用了Annotation 修飾了類、方法、Field 等成員之後,這些 Annotation 不會自己生效,必須由開發者提供相應的代碼來提取並處理 Annotation 信息。這些處理提取和處理 Annotation 的代碼統稱爲 APT(Annotation Processing Tool)。

3、其實在前面我用最基礎的Servlet寫APP服務端時,就接觸過servlet3.0中就引入的@WebServlet ——該註解用來聲明一個Servlet的註冊配置,用來映射客戶端Http訪問路徑和Servlet。使用該註解後就不需要在Dynamic Web project中傳統的web.xml中註冊聲明麻煩的  <servlet-name>和  <servlet-mapping>配置,這樣編寫代碼簡單方便太多了。

有了@WebServlet註解,就不用配置如下圖的web.xml,相當於上面註解官方文檔所說的註解用處b:軟件工具可以用來利用註解信息來生成類似web/xml的配置信息,解放了程序猿。

二、Java內置註解

註解的語法比較簡單,除了@符號的使用之外,它基本與Java固有語法一致。

1、作用在代碼的註解,位於java.lang中

Java 5.0內置了3種標準註解:

  • @Override,表示當前的方法定義將覆蓋超類中的方法。

  • @Deprecated,標記被棄用的代碼。

  • @SuppressWarnings,指示編譯器去忽略所標註內容產生的警告

上面這三個註解或多或少我們都會在寫代碼的時候遇到。

2、作用在其他註解的註解(即元註解或基本註解),位於java.lang.annotation中

Java還提供了4種元註解,專門負責新註解的創建

簡單解釋:

  • @Retention - 標識這個註解怎麼保存,是隻在代碼中,還是編入class文件中,或者是在運行時可以通過反射訪問。

  • @Documented - 標記這些註解是否包含在用戶文檔中。

  • @Target - 標記這個註解應該是哪種 Java 成員。

3、從 Java 7 開始,額外添加了 3 個註解

  • @SafeVarargs - Java 7 開始支持,忽略任何使用參數爲泛型變量的方法或構造函數調用產生的警告。

  • @FunctionalInterface - Java 8 開始支持,標識一個匿名函數或函數式接口。函數式編程很火,所以 Java 8 也及時添加了這個特性。函數式接口 (Functional Interface) 就是一個具有一個方法的普通接口。

    我們進行線程開發中常用的 Runnable 就是一個典型的函數式接口,源碼可以看到它就被 @FunctionalInterface 註解。

  • @Repeatable - Java 8 開始支持,標識某註解可以在同一個聲明上使用多次。(什麼樣的註解會多次應用呢?通常是註解的值可以同時取多個。)

三、自定義註解

註解的基本語法:創建如同接口,但是多了個 @ 符號。並且要想註解能夠正常工作,定義註解的時候還需要用到元註解。

元註解......

public @interface 註解名 {定義體}

使用@interface自定義註解時,自動實現了java.lang.annotation.Annotation接口,由編譯程序自動完成其他細節。實現了之後該自定義註解其實就相當於Java中的一個類。

1、自定義一個註解的方式

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

我們觀察上面自定義註解的創建代碼,除了@符號,註解很像是一個接口,定義註解的時候必須用到元註解。

在註解中一般會有一些元素稱爲屬性以表示某些值。註解的屬性看起來就像接口的方法,唯一的區別在於可以爲其制定默認值。沒有元素的註解稱爲標記註解,上面的@Test就是一個標記註解。

2、註解的屬性

註解的屬性也叫做成員變量。註解只有成員變量,沒有方法。註解的成員變量在註解的定義中以“無形參的方法”形式來聲明,其方法名定義了該成員變量的名字,其返回值定義了該成員變量的類型。

註解的可用的類型包括以下幾種:所有基本類型、String、Class、enum、Annotation、以上類型的數組形式。元素不能有不確定的值,即要麼有默認值,要麼在使用註解的時候提供元素的值。而且元素不能使用null作爲默認值。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation{
    
    int id();
    
    String msg();

}

上面代碼定義了 @TestAnnotation 這個註解中擁有 id 和 msg 兩個屬性。在使用的時候,我們應該給它們進行賦值。

賦值的方式是在註解使用時的括號內以 value="" 形式,多個屬性之前用 ,隔開。註解在只有一個元素且該元素的名稱是value的情況下,在使用註解的時候可以省略“value=”,直接寫需要的值即可。

@TestAnnotation(id=3,msg="hello annotation")
public class Test {

}

3、自定義註解總結

需要注意的是,使用@interface自定義註解時,自動繼承了java.lang.annotation.Annotation接口,由編譯程序自動完成其他細節。在定義註解時,不能繼承其他的註解或接口。@interface用來聲明一個註解,其中的每一個方法實際上是聲明瞭一個屬性。方法的名稱就是屬性的名稱,返回值類型就是屬性的類型(返回值類型只能是8 種基本數據類型、Class、String、enum及它們的數組)。並且要想註解能夠正常工作,定義註解的時候還需要用到元註解

定義註解格式:

        元註解.......

  public @interface 註解名 {定義體}

Annotation類型裏面的屬性該怎麼設定: 

第一,只能用public或默認(default)這兩個訪問權修飾.例如,String value();這裏把方法設爲defaul默認類型;   4

第二,屬性成員只能用基本類型byte,short,char,int,long,float,double,boolean八種基本數據類型和 String,Enum,Class,annotations等數據類型,以及這一些類型的數組。例如,String value();這裏的參數成員就爲String;  

第三,屬性不能有不確定的值,即要麼有默認值,要麼在使用註解的時候提供元素的值。而且元素不能使用null作爲默認值。

舉個例子,下面看一個定義了屬性的註解。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
 public @interface UseCase {
      public String id();
      public String description() default "no description";
 }

定義了註解,必然要去使用註解。

 public class PasswordUtils {
       @UseCase(id = 47, description = "Passwords must contain at least one numeric")
       public boolean validatePassword(String password) {
           return (password.matches("\\w*\\d\\w*"));
       }
   
       @UseCase(id = 48)
       public String encryptPassword(String password) {
           return new StringBuilder(password).reverse().toString();
      }
  }

使用註解最主要的部分在於對註解的處理,那麼就會涉及到註解處理器。

從原理上講,註解處理器就是通過反射機制獲取被檢查方法上的註解信息,然後根據註解屬性的值進行特定的處理。

public static void main(String[] args) {
     List<Integer> useCases = new ArrayList<Integer>();
     Collections.addAll(useCases, 47, 48, 49, 50);
     trackUseCases(useCases, PasswordUtils.class);
 }

 public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
     for (Method m : cl.getDeclaredMethods()) {
         UseCase uc = m.getAnnotation(UseCase.class);
         if (uc != null) {
             System.out.println("Found Use Case:" + uc.id() + " "
                         + uc.description());
             useCases.remove(new Integer(uc.id()));
         }
     }
     for (int i : useCases) {
         System.out.println("Warning: Missing use case-" + i);
     }
 }

輸出結果:

Found Use Case:47 Passwords must contain at least one numeric

Found Use Case:48 no description

Warning: Missing use case-49

Warning: Missing use case-50

四、註解通過反射提取

博文前面的部分講了註解的基本語法,現在是時候檢測我們所學的內容了。

要想在程序運行時正確檢閱註解的內容信息,離不開一個手段,那就是反射。

1>首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個註解

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

2>然後通過 getAnnotation() 方法來獲取 Annotation 對象。

public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}

 或者是 getAnnotations() 方法。

public Annotation[] getAnnotations() {}

注意:前一種方法返回指定類型的註解,後一種方法返回註解到這個元素上的所有註解。

如果獲取到的 Annotation 如果不爲 null,則就可以調用它們的屬性方法了。

比如:

@TestAnnotation()
public class Test {
    
    public static void main(String[] args) {
        
        boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
        
        if ( hasAnnotation ) {
            TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
            
            System.out.println("id:"+testAnnotation.id());
            System.out.println("msg:"+testAnnotation.msg());
        }

    }

}

程序的運行結果是:

id:-1
msg:

這個正是 TestAnnotation 中 id 和 msg 的默認值。

上面的例子中,只是檢閱出了註解在類上的註解,其實屬性、方法上的註解照樣是可以的。同樣還是要假手於反射。

@TestAnnotation(msg="hello")
public class Test {
    
    @Check(value="hi")
    int a;
    
    
    @Perform
    public void testMethod(){}
    
    
    @SuppressWarnings("deprecation")
    public void test1(){
        Hero hero = new Hero();
        hero.say();
        hero.speak();
    }


    public static void main(String[] args) {
        
        boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
        
        if ( hasAnnotation ) {
            TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
            //獲取類的註解
            System.out.println("id:"+testAnnotation.id());
            System.out.println("msg:"+testAnnotation.msg());
        }
        
        
        try {
            Field a = Test.class.getDeclaredField("a");
            a.setAccessible(true);
            //獲取一個成員變量上的註解
            Check check = a.getAnnotation(Check.class);
            
            if ( check != null ) {
                System.out.println("check value:"+check.value());
            }
            
            Method testMethod = Test.class.getDeclaredMethod("testMethod");
            
            if ( testMethod != null ) {
                // 獲取方法中的註解
                Annotation[] ans = testMethod.getAnnotations();
                for( int i = 0;i < ans.length;i++) {
                    System.out.println("method testMethod annotation:"+ans[i].annotationType().getSimpleName());
                }
            }
        } catch (NoSuchFieldException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println(e.getMessage());
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println(e.getMessage());
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
        
    }

}

它們的結果如下:

id:-1
msg:hello
check value:hi
method testMethod annotation:Perform

需要注意的是,如果一個註解要在運行時被成功提取,那麼 @Retention(RetentionPolicy.RUNTIME) 是必須的。

五、Annotation 的作用

Annotation 是一個輔助類,它在 Junit、Struts、Spring 等工具框架中被廣泛使用。

我們在編程中經常會使用到的 Annotation 作用有:

1)編譯檢查

Annotation 具有"讓編譯器進行編譯檢查的作用"。

例如,@SuppressWarnings, @Deprecated 和 @Override 都具有編譯檢查作用。

以@Override舉例,某個方法被 @Override 的標註,則意味着該方法會覆蓋父類中的同名方法。如果有方法被 @Override 標示,但父類中卻沒有"被 @Override 標註"的同名方法,則編譯器會報錯。示例如下:

上面是該程序在 eclipse 中的截圖。從中,我們可以發現 "getString()" 函數會報錯。這是因爲 "getString() 被 @Override 所標註,但在OverrideTest 的任何父類中都沒有定義 getString() 函數"。

"將 getString() 上面的 @Override註釋掉",即可解決該錯誤。

2)程序運行時藉助反射手段提取Annotation

在反射的 Class, Method, Field 等函數中,有許多與 Annotation 相關的接口。

這也意味着,我們可以在反射中解析並使用 Annotation。

import java.lang.annotation.Annotation;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Inherited;
import java.lang.reflect.Method;

/**
 * Annotation在反射函數中的使用示例
 */
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
    String[] value() default "unknown";
}

/**
 * Person類。它會使用MyAnnotation註解。
 */
class Person {
   
    /**
     * empty()方法同時被 "@Deprecated" 和 "@MyAnnotation(value={"a","b"})"所標註
     * (01) @Deprecated,意味着empty()方法,不再被建議使用
     * (02) @MyAnnotation, 意味着empty() 方法對應的MyAnnotation的value值是默認值"unknown"
     */
    @MyAnnotation
    @Deprecated
    public void empty(){
        System.out.println("\nempty");
    }
   
    /**
     * sombody() 被 @MyAnnotation(value={"girl","boy"}) 所標註,
     * @MyAnnotation(value={"girl","boy"}), 意味着MyAnnotation的value值是{"girl","boy"}
     */
    @MyAnnotation(value={"girl","boy"})
    public void somebody(String name, int age){
        System.out.println("\nsomebody: "+name+", "+age);
    }
}

public class AnnotationTest {

    public static void main(String[] args) throws Exception {
       
        // 新建Person
        Person person = new Person();
        // 獲取Person的Class實例
        Class<Person> c = Person.class;
        // 獲取 somebody() 方法的Method實例
        Method mSomebody = c.getMethod("somebody", new Class[]{String.class, int.class});
        // 執行該方法
        mSomebody.invoke(person, new Object[]{"lily", 18});
        iteratorAnnotations(mSomebody);
       

        // 獲取 somebody() 方法的Method實例
        Method mEmpty = c.getMethod("empty", new Class[]{});
        // 執行該方法
        mEmpty.invoke(person, new Object[]{});        
        iteratorAnnotations(mEmpty);
    }
   
    public static void iteratorAnnotations(Method method) {

        // 判斷 somebody() 方法是否包含MyAnnotation註解
        if(method.isAnnotationPresent(MyAnnotation.class)){
            // 獲取該方法的MyAnnotation註解實例
            MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);
            // 獲取 myAnnotation的值,並打印出來
            String[] values = myAnnotation.value();
            for (String str:values)
                System.out.printf(str+", ");
            System.out.println();
        }
       
        // 獲取方法上的所有註解,並打印出來
        Annotation[] annotations = method.getAnnotations();
        for(Annotation annotation : annotations){
            System.out.println(annotation);
        }
    }
}

運行結果:

somebody: lily, 18
girl, boy, 
@com.skywang.annotation.MyAnnotation(value=[girl, boy])

empty
unknown, 
@com.skywang.annotation.MyAnnotation(value=[unknown])
@java.lang.Deprecated()

3) 根據 Annotation 生成幫助文檔,還能夠方便查看代碼

通過給 Annotation 註解加上 @Documented 標籤,能使該 Annotation 標籤出現在 javadoc 中;

通過 @Override, @Deprecated 等,我們能很方便的瞭解程序的大致結構。

4>註解應用實例

註解的功能很強大,Spring和Hebernate這些框架在日誌和有效性中大量使用了註解功能。註解可以應用在使用標記接口的地方。不同的是標記接口用來定義完整的類,但你可以爲單個的方法定義註解,例如是否將一個方法暴露爲服務。

在最新的servlet3.0中引入了很多新的註解,尤其是和servlet安全相關的註解。

HandlesTypes –該註解用來表示一組傳遞給ServletContainerInitializer的應用類。

HttpConstraint – 該註解代表所有HTTP方法的應用請求的安全約束,和ServletSecurity註釋中定義的HttpMethodConstraint安全約束不同。

HttpMethodConstraint – 指明不同類型請求的安全約束,和ServletSecurity 註解中描述HTTP協議方法類型的註釋不同。

MultipartConfig –該註解標註在Servlet上面,表示該Servlet希望處理的請求的 MIME 類型是 multipart/form-data。

ServletSecurity 該註解標註在Servlet繼承類上面,強制該HTTP協議請求遵循安全約束。

WebFilter – 該註解用來聲明一個Server過濾器;

WebInitParam – 該註解用來聲明Servlet或是過濾器的中的初始化參數,通常配合 @WebServlet 或者 @WebFilter 使用。

WebListener –該註解爲Web應用程序上下文中不同類型的事件聲明監聽器。

WebServlet –該註解用來聲明一個Servlet的配置,用來映射訪問路徑和Servlet

5>我們也可以通過自定義 Annotation 來完成某個目的。

下篇博客我會親手自定義註解完成某個目的:Java進階知識5:自定義註解完成某個目的

 

參考鏈接:

Java註解基本原理

Java自定義註解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章