基礎篇:深入解析JAVA註解機制

cscw發表於2020-10-04

java實現註解的底層原理和概念

  • java註解是JDK1.5引入的一種註釋機制,java語言的類、方法、變數、引數和包都可以被註解標註。和Javadoc不同,java註解可以通過反射獲取標註內容
  • 在編譯器生成.class檔案時,註解可以被嵌入位元組碼中,而jvm也可以保留註解的內容,在執行時獲取註解標註的內容資訊
  • java提供的註解可以分成兩類:


作用在程式碼上的功能註解(部分):

註解名稱 功能描述
@Override 檢查該方法是否是重寫方法。如果發現其父類,或者是引用的介面中並沒有該方法時,會報編譯錯誤
@Deprecated 標記過時方法。如果使用該方法,會報編譯警告
@SuppressWarnings 指示編譯器去忽略註釋解中宣告的警告
@FunctionalInterface java8支援,標識一個匿名函式或函式式介面


讓給程式設計師開發自定義註解的元註解(和關鍵字@interface配合使用的註解)

元註解名稱 功能描述
@Retention 標識這個註釋解怎麼儲存,是隻在程式碼中,還是編入類檔案中,或者是在執行時可以通過反射訪問
@Documented 標識這些註解是否包含在使用者文件中
@Target 標識這個註解的作用範圍
@Inherited 標識註解可被繼承類獲取
@Repeatable 標識某註解可以在同一個宣告上使用多次
  • Annotation是所有註解類的共同介面,不用顯示實現。註解類使用@interface定義(代表它實現Annotation介面),搭配元註解使用,如下
package java.lang.annotation;
public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    // 返回定義的註解型別,你在程式碼宣告的@XXX,相當於該型別的一例項
    Class<? extends Annotation> annotationType();
}
-----自定義示例-----
@Retention( value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
public @interface ATest {
	String hello() default  "siting";
}

ATest的位元組碼檔案,編譯器讓自定義註解實現了Annotation介面

public abstract @interface com/ATest implements java/lang/annotation/Annotation {
  // compiled from: ATest.java
  @Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)
  @Ljava/lang/annotation/Target;(value={Ljava/lang/annotation/ElementType;.TYPE})
  // access flags 0x401
  public abstract hello()Ljava/lang/String;
    default="siting"
}
  • 自定義註解型別時,一般需要用@Retention指定註解保留範圍RetentionPolicy,@Target指定使用範圍ElementType。RetentionPolicy保留範圍只能指定一個,ElementType使用範圍可以指定多個
  • 註解資訊怎麼和程式碼關聯在一起,java所有事物都是類,註解也不例外,加入程式碼System.setProperty("sum.misc.ProxyGenerator.saveGeneratedFiles","true"); 可生成註解相應的代理類

    在程式碼裡定義的註解,會被jvm利用反射技術生成一個代理類,然後和被註釋的程式碼(類,方法,屬性等)關聯起來

五種元註解詳解

  • @Retention:指定註解資訊保留階段,有如下三種列舉選擇。只能選其一
public enum RetentionPolicy {
    /** 註解將被編譯器丟棄,生成的class不包含註解資訊 */
    SOURCE,
    /** 註解在class檔案中可用,但會被JVM丟棄;當註解未定義Retention值時,預設值是CLASS */
    CLASS,
    /** 註解資訊在執行期(JVM)保留(.class也有),可以通過反射機制讀取註解的資訊,
      * 操作方法看AnnotatedElement(所有被註釋類的父類) */
    RUNTIME
}
  • @Documented:作用是告訴JavaDoc工具,當前註解本身也要顯示在Java Doc中(不常用)
  • @Target:指定註解作用範圍,可指定多個
public enum ElementType {
    /** 適用範圍:類、介面、註解型別,列舉型別enum */
    TYPE,
    /** 作用於類屬性 (includes enum constants) */
    FIELD,
    /** 作用於方法 */
    METHOD,
    /** 作用於引數宣告 */
    PARAMETER,
    /** 作用於建構函式宣告 */
    CONSTRUCTOR,
    /** 作用於區域性變數宣告 */
    LOCAL_VARIABLE,
    /** 作用於註解宣告 */
    ;,
    /** 作用於包宣告 */
    PACKAGE,
    /** 作用於型別引數(泛型引數)宣告 */
    TYPE_PARAMETER,
    /** 作用於使用型別的任意語句(不包括class) */
    TYPE_USE
}

TYPE_PARAMETER的用法示例

class D<@PTest T> { } // 註解@PTest作用於泛型T

TYPE_USE的用法示例

//用於父類或者介面 
class Test implements @Parent TestP {} 

//用於建構函式
new @Test String("/usr/data")

//用於強制轉換和instanceof檢查,注意這些註解中用於外部工具
//它們不會對型別轉換或者instanceof的檢查行為帶來任何影響
String path=(@Test String)input;
if(input instanceof @Test String) //註解不會影響

//用於指定異常
public Person read() throws @Test IOException.

//用於萬用字元繫結
List<@Test ? extends Data>
List<? extends @Test Data>

@Test String.class //非法,不能標註class
  • @Inherited:表示當前註解會被註解類的子類繼承。即在子類Class<T>通過getAnnotations()可獲取父類被@Inherited修飾的註解。而註解本身是不支援繼承
@Inherited
@Retention( value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
public @interface ATest {  }
----被ATest註解的父類PTest----
@ATest
public class PTest{ }

---Main是PTest的子類----
public class Main extends PTest {
    public static void main(String[] args){
        Annotation an = Main.class.getAnnotations()[0];
  		//Main可以拿到父類的註解ATest,因為ATest被元註解@Inherited修飾
        System.out.println(an);
    }
}  
---result--
@com.ATest()  
  • @Repeatable:JDK1.8新加入的,表明自定義的註解可以在同一個位置重複使用。在沒有該註解前,是無法在同一個型別上使用相同的註解多次
  //Java8前無法重複使用註解
  @FilterPath("/test/v2")
  @FilterPath("/test/v1")
  public class Test {}

使用動態代理機制處理註解

  • 反射機制獲取註解資訊
--- 作用於註解的註解----
@Inherited
@Retention( value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.ANNOTATION_TYPE})
public @interface AnnotationTest {
    String value() default "AnnotationTest";
}
------父類-------
public class PTest {}
------被註解修飾的package-info.java------
//package-info.java
@AnTest("com-package-info")
package com;
-------------
@AnnotationTest("AnnotationTest")
@Inherited
@Retention( value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE_USE,ElementType.PACKAGE,ElementType.FIELD,
        ElementType.TYPE_PARAMETER,ElementType.CONSTRUCTOR,ElementType.LOCAL_VARIABLE})
public @interface AnTest  {
    String value() default  "siting";
}

執行示例

//註解類
@AnTest("mainClass")
//註解泛型引數                                       //註解繼承父類
public class Main<@AnTest("parameter") T > extends @AnTest("parent") PTest {
    @AnTest("constructor")  //註解建構函式
    Main(){ }
    //註解欄位域
    private @AnTest("name") String name;
    //註解泛型欄位域
    private @AnTest("value") T value;
    //註解萬用字元
    private @AnTest("list")List<@AnTest("generic") ?>list;
    //註解方法
    @AnTest("method")                       //註解方法引數
    public String hello(@AnTest("methodParameter") String name)
            throws @AnTest("Exception") Exception { // 註解丟擲異常
        //註解區域性變數,現在執行時暫時無法獲取(忽略)
        @AnTest("result") String result;
        result = "siting";
        System.out.println(name);
        return  result;
    }

    public static void main(String[] args) throws Exception {

        Main<String>  main = new Main<> ();
        Class<Main<Object>> clazz = (Class<Main<Object>>) main.getClass();
        //class的註解
        Annotation[] annotations = clazz.getAnnotations();
        AnTest testTmp = (AnTest) annotations[0];
        System.out.println("修飾Main.class註解value: "+testTmp.value());
        //構造器的註解
        Constructor<Main<Object>> constructor = (Constructor<Main<Object>>) clazz.getDeclaredConstructors()[0];
        testTmp = constructor.getAnnotation(AnTest.class);
        System.out.println("修飾構造器的註解value: "+testTmp.value());
        //繼承父類的註解
        AnnotatedType annotatedType = clazz.getAnnotatedSuperclass();
        testTmp = annotatedType.getAnnotation(AnTest.class);
        System.out.println("修飾繼承父類的註解value: "+testTmp.value());
        //註解的註解
        AnnotationTest annotationTest = testTmp.annotationType().getAnnotation(AnnotationTest.class);
        System.out.println("修飾註解的註解AnnotationTest-value: "+annotationTest.value());
        //泛型引數 T 的註解
        TypeVariable<Class<Main<Object>>> variable = clazz.getTypeParameters()[0];
        testTmp = variable.getAnnotation(AnTest.class);
        System.out.println("修飾泛型引數T註解value: "+testTmp.value());
        //普通欄位域 的註解
        Field[] fields = clazz.getDeclaredFields();
        Field nameField = fields[0];
        testTmp = nameField.getAnnotation(AnTest.class);
        System.out.println("修飾普通欄位域name註解value: "+testTmp.value());
        //泛型欄位域 的註解
        Field valueField = fields[1];
        testTmp = valueField.getAnnotation(AnTest.class);
        System.out.println("修飾泛型欄位T註解value: "+testTmp.value());
        //萬用字元欄位域 的註解
        Field listField = fields[2];
        AnnotatedParameterizedType annotatedPType = (AnnotatedParameterizedType)listField.getAnnotatedType();
        testTmp = annotatedPType.getAnnotation(AnTest.class);
        System.out.println("修飾泛型註解value: "+testTmp.value());
        //萬用字元註解 的註解
        AnnotatedType[] annotatedTypes = annotatedPType.getAnnotatedActualTypeArguments();
        AnnotatedWildcardType annotatedWildcardType = (AnnotatedWildcardType) annotatedTypes[0];
        testTmp = annotatedWildcardType.getAnnotation(AnTest.class);
        System.out.println("修飾萬用字元註解value: "+testTmp.value());
        //方法的註解
        Method method = clazz.getDeclaredMethod("hello", String.class);
        annotatedType = method.getAnnotatedReturnType();
        testTmp = annotatedType.getAnnotation(AnTest.class);
        System.out.println("修飾方法的註解value: "+testTmp.value());
        //異常的註解
        annotatedTypes =  method.getAnnotatedExceptionTypes();
        testTmp = annotatedTypes[0].getAnnotation(AnTest.class);
        System.out.println("修飾方法丟擲錯誤的註解value: "+testTmp.value());
        //方法引數的註解
        annotatedTypes = method.getAnnotatedParameterTypes();
        testTmp = annotatedTypes[0].getAnnotation(AnTest.class);
        System.out.println("修飾方法引數註解value: "+testTmp.value());
        //包的註解
        Package p = Package.getPackage("com");
        testTmp = p.getAnnotation(AnTest.class);
        System.out.println("修飾package註解value: "+testTmp.value());
        main.hello("hello");
    }
}

結果

修飾Main.class註解value: mainClass
修飾構造器的註解value: constructor
修飾繼承父類的註解value: parent
修飾註解的註解AnnotationTest-value: AnnotationTest
修飾泛型引數T註解value: parameter
修飾普通欄位域name註解value: name
修飾泛型欄位T註解value: value
修飾泛型註解value: list
修飾萬用字元註解value: generic
修飾方法的註解value: method
修飾方法丟擲錯誤的註解value: Exception
修飾方法引數註解value: methodParameter
修飾package註解value: com-package-info
hello

spring.AOP和註解機制

spring.AOP相當於動態代理和註解機制在spring框架的結合實現

  • 前要知識:面向切面程式設計(AOP)和動態代理
    • C是程式導向程式設計的,java則是物件導向程式設計,C++則是兩者兼備,它們都是一種規範和思想。面向切面程式設計也一樣,可以簡單理解為:切面程式設計專注的是區域性程式碼,主要為某些點植入增強程式碼
    • 考慮要區域性加入增強程式碼,使用動態代理則是最好的實現。在被代理方法呼叫的前後,可以加入需要的增強功能;因此spring的切面程式設計是基於動態代理的
    • 切面的概念
概念 描述
通知(Advice) 需要切入的增加程式碼邏輯被稱為通知
切點(Pointcut) 定義增強程式碼在何處執行
切面(Aspect) 切面是通知和切點的集合
連線點(JoinPoint) 在切點基礎上,指定增強程式碼在切點執行的時機(在切點前,切點後,丟擲異常後等)
目標(target) 被增強目標類
  • spring.aop提供的切面註解
切面程式設計相關注解 功能描述
@Aspect 作用於類,宣告當前方法類是增強程式碼的切面類
@Pointcut 作用於方法,指定需要被攔截的其他方法。當前方法則作為攔截集合名使用
  • spring的通知註解其實是通知+指定連線點組成,分五種(Before、After、After-returning、After-throwing、Around)
spring通知(Advice)註解 功能描述
@After 增強程式碼在@Pointcut指定的方法之後執行
@Before 增強程式碼在@Pointcut指定的方法之前執行
@AfterReturning 增強程式碼在@Pointcut指定的方法 return返回之後執行
@Around 增強程式碼可以在被攔截方法前後執行
@AfterThrowing 增強程式碼在@Pointcut指定的方法丟擲異常之後執行
  • 在spring切面基礎上,開發具有增強功能的自定義註解 (對註解進行切面)
新建spring-web + aop 專案;新建如下class
------ 目標Controller ------
@RestController
public class TestController {
    @STAnnotation
    @RequestMapping("/hello")
    public String hello(){  return "hello@csc";  }
}
------ Controller註解 -------
@Retention( value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface STAnnotation {
    String value() default "註解hello!";
}
------ Controller切面 ------
@Aspect
@Component
public class ControllerAspect {
    //切點:註解指定關聯 (對註解進行切面)
    @Pointcut("@annotation(STAnnotation)")
    public void controllerX(){}
    //切點:路徑指定關聯
    @Pointcut("execution(public * com.example.demo.TestController.*(..))")
    public void controllerY(){}
    //在controllerY()切點執行之前的連線點加入通知
    @Before("controllerY()")
    public void yBefore(JoinPoint joinPoint) throws Throwable {
    	//可以加入增強程式碼
        MethodSignature methodS = (MethodSignature)joinPoint.getSignature();
        Method method = methodS.getMethod();
        if (method.isAnnotationPresent(STAnnotation.class)) {
            STAnnotation annotation = method.getAnnotation(STAnnotation.class);
            System.out.println(annotation.value());
        }
        System.out.println("controllerY");
    }
    //controllerX()切點執行之後的連線點加入通知
    @After("controllerX()")
    public void xBefore(JoinPoint joinPoint) throws Throwable {
    	//可以加入增強程式碼
        System.out.println("controllerX");
    }
}

啟動專案;執行curl http://127.0.0.1:8080/hello,控制檯輸出如下

(題外)@FunctionalInterface原理介紹

  • Lambda 表示式的結構:(...args)-> { ... code }
    • lambda在python,C++都對應的定義,java也有,lambda一般由入參,處理過程組成。如果處理程式碼只有一行,中括號{} 可以省略。其實就是簡化的函式。在java裡,lambda用函式式介面實現
  • @FunctionalInterface作用於介面,介面可以接受lambda表示式作為右值,此類介面又叫函式式介面,其規定修飾的介面只能有一個抽象的方法(不包扣靜態方法和預設、私有方法)。attention:不加@FunctionalInterface修飾,只定義一個抽象方法的介面預設也是函式式介面
@FunctionalInterface
public interface Func {  void hello(String name); }
---------------------
public static void main(String[] args) {
        Func func = (name) -> System.out.println(name);
        func.hello("siting");
    }

檢視對應的Main.class位元組碼檔案 javap.exe -p -v -c Main.class

Constant pool:
   #1 = Methodref          #8.#28         // java/lang/Object."<init>":()V
   //常量值中前面的#0表示引導方法取BootstrapMethods屬性表的第0項(位元組碼在最下面)
   #2 = InvokeDynamic      #0:#33         // #0:hello:()Lcom/Func;
   #3 = String             #34            // siting
   #4 = InterfaceMethodref #35.#36        // com/Func.hello:(Ljava/lang/String;)V
   #5 = Fieldref           #37.#38        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Methodref          #39.#40        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #41            // com/Main
 .... // main執行位元組碼
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
      	 // 動態獲得一個CallSite物件,該物件是一個內部類,實現了Func介面
         0: invokedynamic #2,  0              // InvokeDynamic #0:hello:()Lcom/Func;
         5: astore_1
         6: aload_1
         7: ldc           #3                  // String siting
         // 呼叫CallSite物件的hello方法
         9: invokeinterface #4,  2            // InterfaceMethod com/Func.hello:(Ljava/lang/String;)V
        14: return
.... //lambda表示式 會編譯出私有靜態類        
private static void lambda$main$0(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return
.... //lambda表示式 會編譯出一個對應的內部類         
SourceFile: "Main.java"
InnerClasses:
     public static final #59= #58 of #62; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #30 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lan
g/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #31 (Ljava/lang/String;)V
      //呼叫Main方法裡的lambda$main$0靜態方法(真正執行lambda的邏輯的方法)
      #32 invokestatic com/Main.lambda$main$0:(Ljava/lang/String;)V
      #31 (Ljava/lang/String;)V

從上面的位元組碼可看出,1:lambda表示式會被編譯成一個私有靜態方法和一個內部類;2:內部類實現了函式式介面,而實現方法會呼叫一個Main.class裡一靜態方法 3:靜態方法lambda$main$0裡是我們自己寫的程式碼邏輯。執行引數加上-Djdk.internal.lambda.dumpProxyClasses可以檢視lambda對應內部類的具體資訊

  • 常用函式式介面
介面 描述
Predicate 判斷:傳入一個引數,返回一個bool結果, 方法為boolean test(T t)
Consumer 消費:傳入一個引數,無返回值, 方法為void accept(T t)
Function 轉化處理:傳入一個引數,返回一個結果,方法為R apply(T t)
Supplier 生產:無引數傳入,返回一個結果,方法為T get()
BiFunction 轉化處理:傳入兩個個引數,返回一個結果,方法R apply(T t, U u)
BinaryOperator 二元操作符, 傳入的兩個引數的型別和返回型別相同, 繼承 BiFunction

歡迎指正文中錯誤

關注公眾號,一起交流

參考文章

相關文章