基礎篇:深入解析JAVA註解機制
目錄
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 //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 |