什麼是注解?
大家好,我是呼噜噜,當我們開發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 的特殊接口,是以注解也叫聲明式接口
注解的分類
一般常用的注解可以分為三大類:
Java自帶的标準注解
例如:
@Override:讓編譯器檢查該方法是否正确地實作了覆寫;
@SuppressWarnings:告訴編譯器忽略此處代碼産生的警告。
@Deprecated:标記過時的元素,這個我們經常在日常開發中經常碰到。
@FunctionalInterface:表明函數式接口注解
元注解
元注解是能夠用于定義注解的注解,或者說元注解是一種基本注解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等 元注解也是Java自帶的标準注解,隻不過用于修飾注解,比較特殊。
@Retention
注解的保留政策, @Retention 定義了Annotation的生命周期。當 @Retention 應用到一個注解上的時候,它解釋說明了這個注解的的存活時間。 它的參數:
如果@Retention不存在,則該Annotation預設為RetentionPolicy.CLASS
示例:
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
我們自定義的TestAnnotation 可以在程式運作中被擷取到
@Documented
它的作用是 用于制作文檔,将注解中的元素包含到 doc 中 一般不怎麼用到,了解即可
@Target
@Target 指定了注解可以修飾哪些地方, 比如方法、成員變量、還是包等等 當一個注解被 @Target 注解時,這個注解就被限定了運用的場景。 常用的參數如下:
@Inherited
@Inherited 修飾一個類時,表明它的注解可以被其子類繼承,預設情況預設是不繼承的。 換句話說:如果一個子類想擷取到父類上的注解資訊,那麼必須在父類上使用的注解上面 加上@Inherit關鍵字 注意:
- @Inherited僅針對@Target(ElementType.TYPE)類型的annotation有效
- @Inherited 不是表明 注解可以繼承,而是子類可以繼承父類的注解
我們來看一個示例: 定義一個注解:
@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這個元注解來申明注解,表示這個聲明的注解是可重複的 @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{
}
自定義注解
我們可以根據自己的需求定義注解,一般分為以下幾步:
- 建立注解檔案, @interface定義注解
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檢查是否重寫
- 适合工具類型的軟體用的,避免繁瑣的代碼,生成代碼配置,比如jpa自動生成sql,日志注解,權限控制
- 程式運作時的處理: 某些注解可以在程式運作的時候接受代碼的讀取,比如我們可以自定義注解
平時我們隻知道如何使用注解,卻不知道其是如何起作用的,理所當然的往往是我們所忽視的。
參考資料: 《Java核心技術 卷一》
本篇文章到這裡就結束啦,如果我的文章對你有所幫助,還請幫忙一鍵三連:點贊、關注、收藏,你的支援會激勵我輸出更高品質的文章,感謝!
計算機内功、JAVA源碼、職業成長、項目實戰、面試相關等更多高品質文章,首發于公衆号「小牛呼噜噜」,我們下期再見。