作者:小牛呼嚕嚕 | https://xiaoniuhululu.com
計算機內功、JAVA底層、面試相關資料等更多精彩文章在公眾號「小牛呼嚕嚕 」
當我們開發SpringBoot專案,我們只需對啟動類加上@SpringBootApplication
,就能自動裝配,不需要編寫冗餘的xml設定。當我們為專案新增lombok依賴,使用@Data
來修飾實體類,我們就不需要編寫getter和setter方法,建構函式
等等。@SpringBootApplication,@Data等
像這種以@
開頭的程式碼 就是註解,只需簡簡單單幾個註解,就能幫助我們省略大量冗餘的程式碼,這是一個非常不可思議的事情!
但我們往往知道在哪些地方加上合適的註解,不然IDE會報錯,卻不知道其中的原理,那究竟什麼是註解呢?
註解(Annotation ), 是 Java5 開始引入的新特性,是放在Java原始碼的類、方法、欄位、引數前的一種特殊「註釋」,是一種標記、標籤。註釋往往會被編譯器直接忽略,能夠被編譯器打包進入class檔案,並執行相應的處理。
按照慣例我們去看下註解的原始碼:
先新建一個註解檔案:MyAnnotation.java
public @interface MyAnnotation {
}
發現MyAnnotation 是被@interface
修飾的,感覺和介面interface
很像。
我們再通過idea來看下其的類繼承:
MyAnnotation 是繼承Annotation介面的。
我們再反編譯一下:
$ javac MyAnnotation.java
$ javap -c MyAnnotation
Compiled from "MyAnnotation.java"
public interface com.zj.ideaprojects.test3.MyAnnotation extends java.lang.annotation.Annotation {
}
發現生成的位元組碼中 @interface變成了interface,MyAnnotation而且自動繼承了Annotation
我們由此可以明白:註解本質是一個繼承了Annotation 的特殊介面,所以註解也叫宣告式介面
一般常用的註解可以分為三大類:
例如:
- @Override:讓編譯器檢查該方法是否正確地實現了覆寫;
- @SuppressWarnings:告訴編譯器忽略此處程式碼產生的警告。
- @Deprecated:標記過時的元素,這個我們經常在日常開發中經常碰到。
- @FunctionalInterface:表明函數式介面註解
元註解是能夠用於定義註解的註解,或者說元註解是一種基本註解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等
元註解也是Java自帶的標準註解,只不過用於修飾註解
,比較特殊。
註解的保留策略, @Retention 定義了Annotation的生命週期。當 @Retention 應用到一個註解上的時候,它解釋說明了這個註解的的存活時間。
它的引數:
RetentionPolicy.SOURCE | 註解只在原始碼階段保留,在編譯器進行編譯時它將被丟掉 |
---|---|
RetentionPolicy.CLASS | 註解只被保留到編譯進行的時候,它並不會被載入到 JVM 中 |
RetentionPolicy.RUNTIME | 註解可以保留到程式執行中的時候,它會被載入進 JVM 中,在程式執行中也可以獲取到它們 |
如果@Retention不存在,則該Annotation預設為RetentionPolicy.CLASS
範例:
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
我們自定義的TestAnnotation 可以在程式執行中被獲取到
它的作用是 用於製作檔案,將註解中的元素包含到 doc 中
一般不怎麼用到,瞭解即可
@Target 指定了註解可以修飾哪些地方, 比如方法、成員變數、還是包等等
當一個註解被 @Target 註解時,這個註解就被限定了運用的場景。
常用的引數如下:
ElementType.ANNOTATION_TYPE | 給一個註解進行註解 |
---|---|
ElementType.CONSTRUCTOR | 給構造方法進行註解 |
ElementType.FIELD | 給屬性進行註解 |
ElementType.LOCAL_VARIABLE | 給區域性變數進行註解 |
ElementType.METHOD | 給方法進行註解 |
ElementType.PACKAGE | 給包進行註解 |
ElementType.PARAMETER | 給一個方法內的引數進行註解 |
ElementType.TYPE | 給一個型別進行註解,比如類、介面、列舉 |
@Inherited 修飾一個類時,表明它的註解可以被其子類繼承,預設情況預設是不繼承的。
換句話說:如果一個子類想獲取到父類別上的註解資訊,那麼必須在父類別上使用的註解上面 加上@Inherit關鍵字
注意:
我們來看一個範例:
定義一個註解:
@Inherited
@Target(ElementType.TYPE)
public @interface MyReport {
String name() default "";
int value() default 0;
}
使用這個註解:
@MyReport(value=1)
public class Teacher {
}
則它的子類預設繼承了該註解:
public class Student extends Teacher{
}
idea 檢視類的繼承關係:
使用@Repeatable這個元註解來申明註解,表示這個宣告的註解是可重複的
@Repeatable 是 Java 1.8 才加進來的,所以算是一個新的特性。
比如:一個人他既會下棋又會做飯,他還會唱歌。
@Repeatable(MyReport.class)
@Target(ElementType.TYPE)
public @interface MyReport {
String name() default "";
int value() default 0;
}
@MyReport(value=0)
@MyReport(value=1)
@MyReport(value=2)
public class Man{
}
我們可以根據自己的需求定義註解,一般分為以下幾步:
public @interface MyReport { }
public @interface MyReport {
String name() default "";
int value() default 0;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyReport {
String name() default "";
int value() default 0;
}
我們一般設定 @Target和@Retention就夠了,其中@Retention一般設定為RUNTIME,因為我們自定義的註解通常需要在程式執行中讀取。
讀到這裡,相信大家已經明白了 如何定義
和使用註解
,我們接下來 就需要如何將註解利用起來。
我們知道讀取註解, 需要用到java的反射
推薦閱讀筆者之前寫過關於反射的文章:https://mp.weixin.qq.com/s/_n8HTIjkw7Emcunpb4-Iwg
我們先來寫一個簡單的範例--反射獲取註解:
通過前文的瞭解,先來改造一下MyAnnotation.java
@Retention(RetentionPolicy.RUNTIME)//確保程式執行中,能夠讀取到該註解!!!
public @interface MyAnnotation {
String msg() default "no msg";
}
我們再用@MyAnnotation
來修飾Person類的類名、屬性、和方法
@MyAnnotation(msg = "this person class")//註解 修飾類
public class Person {
private String name;//姓名
private String sex;//性別
@MyAnnotation(msg = "this person field public")//註解 修飾 public屬性
public int height;//身高
@MyAnnotation(msg = "this person field private")//註解 修飾 private屬性
private int weight;//體重
public void sleep(){
System.out.println(this.name+"--"+ "睡覺");
}
public void eat(){
System.out.println("吃飯");
}
@MyAnnotation(msg = "this person method")//註解 修飾方法
public void dance(){
System.out.println("跳舞");
}
}
最後我們寫一個測試類
public class TestAn {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
//獲取Person class 範例
Class<Person> c1 = Person.class;
//反射獲取 類上的註解
MyAnnotation classAnnotation = c1.getAnnotation(MyAnnotation.class);
System.out.println(classAnnotation.msg());
//反射獲取 private屬性上的註解
Field we = c1.getDeclaredField("weight");
MyAnnotation fieldAnnotation = we.getAnnotation(MyAnnotation.class);
System.out.println(fieldAnnotation.msg());
//反射獲取 public屬性上的註解
Field he = c1.getDeclaredField("height");
MyAnnotation field2Annotation = he.getAnnotation(MyAnnotation.class);
System.out.println(field2Annotation.msg());
//反射獲取 方法上的註解
Method me = c1.getMethod("dance",null);
MyAnnotation methodAnnotation = me.getAnnotation(MyAnnotation.class);
System.out.println(methodAnnotation.msg());
}
}
結果:
this person class
this person field private
this person field public
this person method
我們通過反射讀取api時,一般會先去校驗這個註解存不存在:
if(c1.isAnnotationPresent(MyAnnotation.class)) {
//存在 MyAnnotation 註解
}else {
//不存在 MyAnnotation 註解
}
我們發現反射真的很強大,不僅可以讀取類的屬性、方法、構造器等資訊,還可以讀取類的註解相關資訊。
那反射是如何實現工作的?
我們來看下原始碼:
從 c1.getAnnotation(MyAnnotation.class);
通過idea點進去檢視原始碼,把重點的給貼出來,其他的就省略了
Map<Class<? extends Annotation>, Annotation> declaredAnnotations =
AnnotationParser.parseAnnotations(getRawAnnotations(), getConstantPool(), this);
parseAnnotations()去分析註解,其第一個引數是 獲取原始註解,第二個引數是獲取常數池內容
public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) {
return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
public Annotation run() {
return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
}
});
}
Proxy._newProxyInstance_(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1)
建立動態代理,此處var0引數是由常數池獲取的資料轉換而來。
我們監聽此處的var0:
可以推斷出註解相關的資訊 是存放在常數池中的
我們來總結一下,反射呼叫getAnnotations(MyAnnotation.class)
方法的背後主要操作:
解析註解parseAnnotations()的時候 從該註解類的常數池中取出註解相關的資訊,將其轉換格式後,通過newProxyInstance(註解的類載入器,註解的class範例 ,AnotationInvocationHandler範例)
來建立代理物件,作為引數傳進去,最後返回一個代理範例。
其中AnotationInvocationHandler類是一個典型的動態代理類, 這邊先挖個坑,暫不展開,不然這篇文章是寫不完了
關於動態代理類我們只需先知道: 物件的執行方法,交給代理來負責
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
...
private final Map<String, Object> memberValues;//存放該註解所有屬性的值
private transient volatile Method[] memberMethods = null;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
...
}
public Object invoke(Object var1, Method var2, Object[] var3) {
...
//呼叫委託類物件的方法,具體等等一些操作
}
...
}
反射呼叫getAnnotations(MyAnnotation.class)
,返回一個代理範例,我們可以通過這個範例來操作該註解
當我們引入springsecurity來做安全框架,然後只需新增@PreAuthorize("hasRole('Admin')")
註解,就能實現許可權的控制,簡簡單單地一行程式碼,就優雅地實現了許可權控制,覺不覺得很神奇?讓我們一起模擬一個出來吧
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPreVer {
String value() default "no role";
}
public class ResourceLogin {
private String name;
@MyPreVer(value = "User")
private void rsA() {
System.out.println("資源A");
}
@MyPreVer(value = "Admin")
private void rsB() {
System.out.println("資源B");
}
}
public class TestLogin {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
//模擬 使用者的許可權
String role = "User";
//模擬 需要的許可權
final String RoleNeeded = "Admin";
//獲取Class範例
Class<ResourceLogin> c1 = ResourceLogin.class;
//存取資源A
Method meA = c1.getDeclaredMethod("rsA",null);
MyPreVer meAPre = meA.getDeclaredAnnotation(MyPreVer.class);
if(meAPre.value().equals(RoleNeeded)) {//模擬攔截器
meA.setAccessible(true);
meA.invoke(c1.newInstance(),null);//模擬存取資源
}else {
System.out.println("騷瑞,你無權存取該資源");
}
//存取資源B
Method meB = c1.getDeclaredMethod("rsB",null);
MyPreVer meBPre = meB.getDeclaredAnnotation(MyPreVer.class);
if(meBPre.value().equals(RoleNeeded)) {//模擬攔截器
meB.setAccessible(true);
meB.invoke(c1.newInstance());//模擬存取資源
}else {
System.out.println("騷瑞,你無權存取該資源");
}
}
}
結果:
騷瑞,你無權存取該資源
資源B
註解 是一種標記、標籤 來修飾程式碼,但它不是程式碼本身的一部分,即註解本身對程式碼邏輯沒有任何影響,如何使用註解完全取決於我們開發者用Java反射來讀取和使用。
我們發現反射真的很強大,不僅可以讀取類的屬性、方法、構造器等資訊,還可以讀取類的註解相關資訊,以後還會經常遇到它。
註解一般用於
@override
檢查是否重寫平時我們只知道如何使用註解,卻不知道其是如何起作用的,理所當然的往往是我們所忽視的。
參考資料:
《Java核心技術 卷一》
https://blog.csdn.net/qq_20009015/article/details/106038023
https://zhuanlan.zhihu.com/p/258429599
本篇文章到這裡就結束啦,很感謝你能看到最後,如果覺得文章對你有幫助,別忘記關注我!