Java的注解機制——Spring自動裝配的實現原理


Java的注解機制——Spring自動裝配的實現原理

  JDK1.5加入了對注解機制的支持,實際上我學習Java的時候就已經使用JDK1.6了,而且除了@Override和@SuppressWarnings(后者還是IDE給生成的……)之外沒接觸過其他的。

  進入公司前的面試,技術人員就問了我關於注解的問題,我就說可以生成chm手冊……現在想起來真囧,注釋和注解被我搞得完全一樣了。

  

  使用注解主要是在需要使用Spring框架的時候,特別是使用SpringMVC。因為這時我們會發現它的強大之處:預處理。

  注解實際上相當於一種標記,它允許你在運行時(源碼、文檔、類文件我們就不討論了)動態地對擁有該標記的成員進行操作。

  實現注解需要三個條件(我們討論的是類似於Spring自動裝配的高級應用):注解聲明、使用注解的元素、操作使用注解元素的代碼。

  

  首先是注解聲明,注解也是一種類型,我們要定義的話也需要編寫代碼,如下:

復制代碼
 1 package annotation;
 2 
 3 import java.lang.annotation.ElementType;
 4 import java.lang.annotation.Retention;
 5 import java.lang.annotation.RetentionPolicy;
 6 import java.lang.annotation.Target;
 7 
 8 /**
 9  * 自定義注解,用來配置方法
10  * 
11  * @author Johness
12  *
13  */
14 @Retention(RetentionPolicy.RUNTIME) // 表示注解在運行時依然存在
15 @Target(ElementType.METHOD) // 表示注解可以被使用於方法上
16 public @interface SayHiAnnotation {
17     String paramValue() default "johness"; // 表示我的注解需要一個參數 名為"paramValue" 默認值為"johness"
18 }
復制代碼

 

  然后是使用我們注解的元素:

復制代碼
 1 package element;
 2 
 3 import annotation.SayHiAnnotation;
 4 
 5 /**
 6  * 要使用SayHiAnnotation的元素所在類
 7  * 由於我們定義了只有方法才能使用我們的注解,我們就使用多個方法來進行測試
 8  * 
 9  * @author Johness
10  *
11  */
12 public class SayHiEmlement {
13 
14     // 普通的方法
15     public void SayHiDefault(String name){
16         System.out.println("Hi, " + name);
17     }
18     
19     // 使用注解並傳入參數的方法
20     @SayHiAnnotation(paramValue="Jack")
21     public void SayHiAnnotation(String name){
22         System.out.println("Hi, " + name);
23     }
24     
25     // 使用注解並使用默認參數的方法
26     @SayHiAnnotation
27     public void SayHiAnnotationDefault(String name){
28         System.out.println("Hi, " + name);
29     }
30 }
復制代碼

  最后,是我們的操作方法(值得一提的是雖然有一定的規范,但您大可不必去浪費精力,您只需要保證您的操作代碼在您希望的時候執行即可):

復制代碼
 1 package Main;
 2 
 3 import java.lang.reflect.InvocationTargetException;
 4 import java.lang.reflect.Method;
 5 
 6 import element.SayHiEmlement;
 7 import annotation.SayHiAnnotation;
 8 
 9 public class AnnotionOperator {
10     public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException {
11         SayHiEmlement element = new SayHiEmlement(); // 初始化一個實例,用於方法調用
12         Method[] methods = SayHiEmlement.class.getDeclaredMethods(); // 獲得所有方法
13         
14         for (Method method : methods) {
15             SayHiAnnotation annotationTmp = null;
16             if((annotationTmp = method.getAnnotation(SayHiAnnotation.class))!=null) // 檢測是否使用了我們的注解
17                 method.invoke(element,annotationTmp.paramValue()); // 如果使用了我們的注解,我們就把注解里的"paramValue"參數值作為方法參數來調用方法
18             else
19                 method.invoke(element, "Rose"); // 如果沒有使用我們的注解,我們就需要使用普通的方式來調用方法了
20         }
21     }
22 }
復制代碼

  結果為:Hi, Jack
      Hi, johness
      Hi, Rose

  可以看到,注解是進行預處理的很好方式(這里的預處理和編譯原理有區別)!

 

  接下來我們看看Spring是如何使用注解機制完成自動裝配的:

  

    首先是為了讓Spring為我們自動裝配要進行的操作,無外乎兩種:繼承org.springframework.web.context.support.SpringBeanAutowiringSupport類或者添加@Component/@Controller等注解並(只是使用注解方式需要)在Spring配置文件里聲明context:component-scan元素。

    我說說繼承方式是如何實現自動裝配的,我們打開Spring源代碼查看SpringBeanAutowiringSupport類。我們會發現以下語句:

1 public SpringBeanAutowiringSupport() {
2         processInjectionBasedOnCurrentContext(this);
3     }

 

    眾所周知,Java實例構造時會調用默認父類無參構造方法,Spring正是利用了這一點,讓"操作元素的代碼"得以執行!(我看到第一眼就震驚了!真是奇思妙想啊。果然,高手都要善於用Java來用Java)

    后面的我就不就不多說了,不過還是要糾正一些人的觀點:說使用注解的自動裝配來完成注入也需要setter。這明顯是錯誤的嘛!我們看Spring注解裝配(繼承方式)的方法調用順序: org.springframework.web.context.support.SpringBeanAutowiringSupport#SpringBeanAutowiringSupport=>

        org.springframework.web.context.support.SpringBeanAutowiringSupport#processInjectionBasedOnCurrentContext=>

      org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#processInjection=>

               org.springframework.beans.factory.annotation.InjectionMetadata#Injection(繼承,方法重寫)。最后看看Injection方法的方法體:

復制代碼
 1 /**
 2          * Either this or {@link #getResourceToInject} needs to be overridden.
 3          */
 4         protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
 5             if (this.isField) {
 6                 Field field = (Field) this.member;
 7                 ReflectionUtils.makeAccessible(field);
 8                 field.set(target, getResourceToInject(target, requestingBeanName));
 9             }
10             else {
11                 if (checkPropertySkipping(pvs)) {
12                     return;
13                 }
14                 try {
15                     Method method = (Method) this.member;
16                     ReflectionUtils.makeAccessible(method);
17                     method.invoke(target, getResourceToInject(target, requestingBeanName));
18                 }
19                 catch (InvocationTargetException ex) {
20                     throw ex.getTargetException();
21                 }
22             }
23         }
復制代碼

      雖然不完全,但可以基本判定此種自動裝配是使用了java放射機制。

 

 

 

 

 

 

 

 

 

 

 

JAVA注解

分類: java

      本文將向你介紹J2SE5.0中的新特性之一:注解。本文將從什么是注解;J2SE5.0中預定義的注解;如何自定義注解;如何對注解進行注解以及如何在程序中讀取注解5個方面進行討論。

 

 

一、什么是注解

    說起注解,得先提一提什么是元數據(metadata)。所謂元數據就是數據的數據。也就是說,元數據是描述數據的。就象數據表中的字段一樣,每個字段描述了這個字段下的數據的含義。而J2SE5.0中提供的注解就是java源代碼的元數據,也就是說注解是描述java源代碼的。在J2SE5.0中可以自定義注解。使用時在@后面跟注解的名字。

 

 

二、J2SE5.0中預定義的注解

    在J2SE5.0的java.lang包中預定義了三個注解。它們是Override、Deprecated和SuppressWarnings。下面分別解解它們的含義。

   

    Override

    這個注解的作用是標識某一個方法是否覆蓋了它的父類的方法。那么為什么要標識呢?讓我們來看看如果不用Override標識會發生什么事情。

    假設有兩個類Class1和ParentClass1,用Class1中的myMethod1方法覆蓋ParentClass1中的myMethod1方法。


 class ParentClass1
 {
     public void myMethod1() {}
 }

 class Class1 extends ParentClass1
 {
     public void myMethod2() {}        
 }

 


 建立Class1的實例,並且調用myMethod1方法

    ParentClass1 c1 = new Class1();

    c1.myMethod1();

    以上的代碼可以正常編譯通過和運行。但是在寫Class1的代碼時,誤將myMethod1寫成了myMethod2,然而在調用時,myMethod1並未被覆蓋。因此,c1.myMethod1()調用的還是ParentClass1的myMethod1方法。更不幸的是,程序員並未意識到這一點。因此,這可能會產生bug。

    如果我們使用Override來修飾Class1中的myMethod1方法,當myMethod1被誤寫成別的方法時,編譯器就會報錯。因此,就可以避免這類錯誤。

 

class Class1 extends ParentClass1
{
    @Override   // 編譯器產生一個錯誤
    public void myMethod2() {}        
}


    以上代碼編譯不能通過,被Override注解的方法必須在父類中存在同樣的方法程序才能編譯通過。也就是說只有下面的代碼才能正確編譯。

class Class1 extends ParentClass1
{
    @Override
    public void myMethod1() {}        
}

 


Deprecated

 

這個注解是一個標記注解。所謂標記注解,就是在源程序中加入這個標記后,並不影響程序的編譯,但有時編譯器會顯示一些警告信息。

那么Deprecated注解是什么意思呢?如果你經常使用eclipse等IDE編寫java程序時,可能會經常在屬性或方法提示中看到這個詞。如果某個類成員的提示中出現了個詞,就表示這個並不建議使用這個類成員。因為這個類成員在未來的JDK版本中可能被刪除。之所以在現在還保留,是因為給那些已經使用了這些類成員的程序一個緩沖期。如果現在就去了,那么這些程序就無法在新的編譯器中編譯了。

說到這,可能你已經猜出來了。Deprecated注解一定和這些類成員有關。說得對!使用Deprecated標注一個類成員后,這個類成員在顯示上就會有一些變化。在eclipse中非常明顯。讓我們看看圖1有哪些變化。

 

 

Deprecated注解圖片

圖1 加上@Deprecated后的類成員在eclipse中的變化

 

從上圖可以看出,有三個地方發生的變化。紅色框里面的是變化的部分。

1.       方法定義處

2.       方法引用處

3.       顯示的成員列表中

 

發生這些變化並不會影響編譯,只是提醒一下程序員,這個方法以后是要被刪除的,最好別用。

Deprecated注解還有一個作用。就是如果一個類從另外一個類繼承,並且override被繼承類的Deprecated方法,在編譯時將會出現一個警告。如test.java的內容如下:

 

class Class1
{
    @Deprecated
    public void myMethod(){}
}

class Class2 extends Class1
{
    public void myMethod(){}
}

 

 

運行javac test.java 出現如下警告:

注意:test.java 使用或覆蓋了已過時的 API。

注意:要了解詳細信息,請使用 -Xlint:deprecation 重新編譯

使用-Xlint:deprecation顯示更詳細的警告信息:

test.java:4: 警告:[deprecation] Class1 中的 myMethod() 已過時

        public void myMethod()

                    ^

1 警告

這些警告並不會影響編譯,只是提醒你一下盡量不要用myMethod方法。

 


SuppressWarnings

 

這個世界的事物總是成對出現。即然有使編譯器產生警告信息的,那么就有抑制編譯器產生警告信息的。

SuppressWarnings注解就是為了這樣一個目的而存在的。讓我們先看一看如下的代碼。

 

public void myMethod()
{
    List wordList = new ArrayList();
    wordList.add("foo");
}

 


這是一個類中的方法。編譯它,將會得到如下的警告。

注意:Testannotation.java 使用了未經檢查或不安全的操作。

注意:要了解詳細信息,請使用 -Xlint:unchecked 重新編譯。

這兩行警告信息表示List類必須使用范型才是安全的,才可以進行類型檢查。如果想不顯示這個警告信息有兩種方法。一個是將這個方法進行如下改寫:


public void myMethod()
{
    List<String> wordList = new ArrayList<String>();
        wordList.add("foo");
}


另外一種方法就是使用@SuppressWarnings。


@SuppressWarnings (value={"unchecked"})
public void myMethod()
{
    List wordList = new ArrayList();
    wordList.add("foo");
}

 


要注意的是SuppressWarnings和前兩個注解不一樣。這個注解有一個屬性。當然,還可以抑制其它警告,如@SuppressWarnings(value={"unchecked", "fallthrough"})

 

 

三、如何自定義注解

注解的強大之處是它不僅可以使java程序變成自描述的,而且允許程序員自定義注解。注解的定義和接口差不多,只是在interface前面多了一個“@”。

public @interface MyAnnotation

{

}

上面的代碼是一個最簡單的注解。這個注解沒有屬性。也可以理解為是一個標記注解。就象Serializable接口一樣是一個標記接口,里面未定義任何方法。

當然,也可以定義而有屬性的注解。
public @interface MyAnnotation

{
    String value();
}

 

可以按如下格式使用MyAnnotation
@MyAnnotation(“abc”)

public void myMethod()

{
}

 

看了上面的代碼,大家可能有一個疑問。怎么沒有使用value,而直接就寫”abc”了。那么”abc”到底傳給誰了。其實這里有一個約定。如果沒有寫屬性名的值,而這個注解又有value屬性,就將這個值賦給value屬性,如果沒有,就出現編譯錯誤。

除了可以省略屬性名,還可以省略屬性值。這就是默認值。
public @interface MyAnnotation

{
    public String myMethod() default "xyz";
}

 

可以直接使用MyAnnotation
@MyAnnotation // 使用默認值xyz

public void myMethod()

{

}

 

也可以這樣使用
@MyAnnotation(myMethod=”abc”)

public void myMethod()

{

}

 

如果要使用多個屬性的話。可以參考如下代碼。

public @interface MyAnnotation
{
    public enum MyEnum{A, B, C}
    public MyEnum value1();
    public String value2();

}

@MyAnnotation(value1=MyAnnotation.MyEnum.A, value2 = “xyz”)
public void myMethod()
{
}

 


這一節討論了如何自定義注解。那么定義注解有什么用呢?有什么方法對注解進行限制呢?我們能從程序中得到注解嗎?這些疑問都可以從下面的內容找到答案。

 

 

四、如何對注解進行注解

這一節的題目讀起來雖然有些繞口,但它所蘊涵的知識卻對設計更強大的java程序有很大幫助。

在上一節討論了自定義注解,由此我們可知注解在J2SE5.0中也和類、接口一樣。是程序中的一個基本的組成部分。既然可以對類、接口進行注解,那么當然也可以對注解進行注解。

使用普通注解對注解進行注解的方法和對類、接口進行注解的方法一樣。所不同的是,J2SE5.0為注解單獨提供了4種注解,即元注解。

元注解理解:
注解可以用於注解類(annotate Classes)
可以用於注解接口(annotate Interfaces)
可以用於注解枚舉類型(annotate Enums)
因此注解同樣也可以用於注解注解(annotate Annotations)

 

它們是Target、Retention、Documented和Inherited。下面就分別介紹這4種注解。

 

Target

這個注解理解起來非常簡單。由於target的中文意思是“目標”,因此,我們可能已經猜到這個注解和某一些目標相關。那么這些目標是指什么呢?大家可以先看看下面的代碼。


@Target({ElementType.METHOD})
@interface MyAnnotation {}

@MyAnnotation         // 錯誤的使用
public class Class1
{
    @MyAnnotation        // 正確的使用
    public void myMethod1() {}
}

 


以上代碼定義了一個注解MyAnnotation和一個類Class1,並且使用MyAnnotation分別對Class1和myMethod1進行注解。如果編譯這段代碼是無法通過的。也許有些人感到驚訝,沒錯啊!但問題就出在@Target({ElementType.METHOD})上,由於Target使用了一個枚舉類型屬性,它的值是ElementType.METHOD。這就表明MyAnnotation只能為方法注解。而不能為其它的任何語言元素進行注解。因此,MyAnnotation自然也不能為Class1進行注解了。

 

說到這,大家可能已經基本明白了。原來target所指的目標就是java的語言元素。如類、接口、方法等。當然,Target還可以對其它的語言元素進行限制,如構造函數、字段、參數等。如只允許對方法和構造函數進行注解可以寫成:

@Target({ElementType.METHOD,  ElementType.CONSTRUCTOR})

@interface MyAnnotation {}

 

Retention

既然可以自定義注解,當然也可以讀取程序中的注解(如何讀取注解將在下一節中討論)。但是注解只有被保存在class文件中才可以被讀出來。而Retention就是為設置注解是否保存在class文件中而存在的。下面的代碼是Retention的詳細用法。

@Retention(RetentionPolicy.SOURCE)

@interface MyAnnotation1 { }

@Retention(RetentionPolicy.CLASS)

@interface MyAnnotation2 {}

@Retention(RetentionPolicy.RUNTIME)

@interface MyAnnotation3 {}

其中第一段代碼的作用是不將注解保存在class文件中,也就是說象“//”一樣在編譯時被過濾掉了。第二段代碼的作用是只將注解保存在class文件中,而使用反射讀取注解時忽略這些注解。第三段代碼的作用是即將注解保存在class文件中,也可以通過反射讀取注解。

 

Documented

這個注解和它的名子一樣和文檔有關。在默認的情況下在使用javadoc自動生成文檔時,注解將被忽略掉。如果想在文檔中也包含注解,必須使用Documented為文檔注解。
@interface MyAnnotation{ }

@MyAnnotation
class Class1
{    
    public void myMethod() { }
}
使用javadoc為這段代碼生成文檔時並不將@MyAnnotation包含進去。生成的文檔對Class1的描述如下:

 class Class1 extends java.lang.Object

 

而如果這樣定義MyAnnotation將會出現另一個結果。

@Documented

@interface MyAnnotation {}

生成的文檔:

@MyAnnotation // 這行是在加上@Documented后被加上的

class Class1 extends java.lang.Object

 

Inherited

繼承是java主要的特性之一。在類中的protected和public成員都將會被子類繼承,但是父類的注解會不會被子類繼承呢?很遺憾的告訴大家,在默認的情況下,父類的注解並不會被子類繼承。如果要繼承,就必須加上Inherited注解。


@Inherited
@interface MyAnnotation { }

@MyAnnotation
public class ParentClass {}

public class ChildClass extends ParentClass { }


在以上代碼中ChildClass和ParentClass一樣都已被MyAnnotation注解了。

 

 

五、如何使用反射讀取注解

前面討論了如何自定義注解。但是自定義了注解又有什么用呢?這個問題才是J2SE5.0提供注解的關鍵。自定義注解當然是要用的。那么如何用呢?解決這個問題就需要使用java最令人興奮的功能之一:反射(reflect)。

在以前的JDK版本中,我們可以使用反射得到類的方法、方法的參數以及其它的類成員等信息。那么在J2SE5.0中同樣也可以象方法一樣得到注解的各種信息。

在使用反射之前必須使用import java.lang.reflect.* 來導入和反射相關的類。

如果要得到某一個類或接口的注解信息,可以使用如下代碼:

Annotation annotation = TestAnnotation.class.getAnnotation(MyAnnotation.class);

如果要得到全部的注解信息可使用如下語句:

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

Annotation[] annotations = TestAnnotation.class.getDeclaredAnnotations();

getDeclaredAnnotations與getAnnotations類似,但它們不同的是getDeclaredAnnotations得到的是當前成員所有的注解,不包括繼承的。而getAnnotations得到的是包括繼承的所有注解。

 

如果要得到其它成員的注解,可先得到這個成員,然后再得到相應的注解。如得到myMethod的注解。

Method method = TestAnnotation.class.getMethod("myMethod", null);

Annotation annotation = method.getAnnotation(MyAnnotation.class);

注:要想使用反射得到注解信息,這個注解必須使用

@Retention(RetentionPolicy.RUNTIME)進行注解。

 

 

六、注解的本質:注解是一種類型
JDK5.0中的類型:1、類(class)2、接口(interface)3、枚舉(enum)4、注解(Annotation)
因此,注解與其他3種類型一樣,都可以定義、使用,以及包含有自己的屬性、方法
 

七、注解的分類:
(1)標記注釋:注解的內部沒有屬性,稱作標記注解
使用方法:@注解名
使用例子:@MarkAnnotation
 
(2)單值注解:注解的內部只有一個屬性,稱作單值注解
使用方法:@注解名(屬性名=屬性值)
使用例子:@SingleAnnotation(value="abc")  //也可以寫成@SingleAnnotation("abc")
*(屬性名=屬性值)可以簡化為(屬性值),但是需要滿足以下兩個條件:
1、該注解必須為單值注解
2、該注解的屬性名必須為value
 
(3)多值注解:注解的內部有多個屬性,稱作多值注解
使用方法:@注解名(屬性名1=屬性值1, 屬性名2=屬性值2……)
使用例子:@MultipliedAnnotation(value1 = "abc", value2 = 30……)
 

 

 

八、自動測試機的寫法:
自動測試機的原理:
使用Annotation來Annotate元素的實質是:每一個ElementType內部的元素都有兩個方法,分別為
(注:為方便理解,以下使用的TestCase為某個特定的自定義注釋)
(1)isAnnotationPresent(TestCase.class)  //判斷該元素是否被TestCase所注釋
(2)getAnnotation(TestCase.class)  //獲得TestCase的類對象
 
因此,自動測試機的工作過程是:
(1)首先通過反射,獲得被測類o中的每一個方法
(2)對每一個方法通過使用isAnnotationPresent(TestCase.class)判斷其是否被TestCase所注釋 (注意是.class!)
(3)如果某方法method被TestCase所注釋,則通過method的getAnnotation(TestCase.class)獲得TestCase的類對象tc
(4)通過tc的value()方法,獲得該類對象的屬性value
(注:此處使用的value()方法,正是在TestCase中定義的value屬性,再次理解在注釋中定義的 value既是屬性,也是方法
(5)調用method方法的invoke(o,value),用value對method進行測試
 
九、注解使用實例:
要求:
(1)定義一個單值注解TestCase,使其可以注解方法,並且可以被保留到程序運行時
注解的屬性類型為String,要求可以使用簡寫方式為屬性賦值
(2)定義一個類MyClass,要求有三個方法Method1、2、3
方法的參數、返回值類型均為String類型,返回值為傳入的參數
使用(1)中的注解來注釋Method1、3,並對屬性參數賦值
(3)定義一個測試類TestMyClass,要求使用反射來測試MyClass中所有的被TestCase注解的方法
並將注解的屬性值作為參數,調用相應方法來返回測試結果
 
例子:
---------------------------1---------------------------
 
import java.lang.annotation.*;
 
@Target ({ElementType.METHOD})
@Retention (RetentionPolicy.RUNTIME)
 
public @interface TestCase {
  String value();
}
 
---------------------------2---------------------------
 
public class MyClass {
  @TestCase("This is Method 1")
  public String Method1 (String s) {
    return s;
  }
 
  public String Method2 (String s) {
    return s;
  }
 
  @TestCase("This is Method 3")
  public String Method3 (String s) {
    return s;
  }
}
 
---------------------------3---------------------------
 
import java.lang.reflect.*;
 
public class TestMyClass {
  public static void main(String [] args) {
    Class c = Class.forName("MyClass");
    Method [] ms = c.getDeclaredMethods();
    for (Method m : ms) {
      if(m.isAnnotationPresent(TestCase.class) {
        TestCase tc = m.getAnnotation(TestCase.class);
        Object o = c.newInstance();
        String s = tc.value();
        m.invoke(o, s);
        或者以上三句可以直接寫成:
        m.invoke(c.newInstace, tc.value());
      }
    }
  }
}
 

 

 

總結

注解是J2SE5.0提供的一項非常有趣的功能。它不但有趣,而且還非常有用。如即將出台的EJB3.0規范就是借助於注解實現的。這樣將使EJB3.0在實現起來更簡單,更人性化。還有Hibernate3.0除了使用傳統的方法生成hibernate映射外,也可以使用注解來生成hibernate映射。總之,如果能將注解靈活應用到程序中,將會使你的程序更加簡潔和強大。

 


免責聲明!

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



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