1. 簡介
最近在學習架構技術時頻繁地遇到注解與反射相關的原理,之前學習的javaSE中雖然學過,但是記憶模糊,是以打算重新複習一下這方面的知識。
2. 注解
2.1 概念
Java 注解(Annotation)又稱 Java 标注,是 JDK5.0 引入的一種注釋機制。Java 語言中的類、方法、變量、參數和包等都可以被标注。
注解與注釋有相似的,他們都是對其标注的代碼的一種說明。但是,注解更加複雜:
- 有明确的編寫文法:注解的聲明需要符合java的命名規範,且自定義注解需要滿足Java的文法;
- 有明确的使用位置:注解的使用位置是需要指明的;
- 有明确的有效期:一個注解的生命周期包括:源代碼,class檔案,和運作時。
- 具有強制性:注解所規定的内容使用者在使用時必須遵守,否則編譯無法通過。
java中是使用反射的機制來讀取注解的。
2.2 核心機制
1. 元注解 meta-annotation
java中提供了四個元注解,即注解的注解,我們在編寫自己的注解時,需要關注這四個注解,并按照需求對這四個注解進行指派。
- @Document:說明該注解将寫入javadoc中;
- @Retention:說明該注解可以被儲存到什麼時候(SOURCE<CLASS<RUNTIME);
- @Target: 描述該注解的使用範圍(METHOD,CLASS,FIELD);
- @Inherited:說明子類可以繼承父類中的該注解。
我們來看一下Target注解的内容:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
ElementType是一個枚舉類型,其可枚舉的值包括:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE,
/**
* Module declaration.
*
* @since 9
*/
MODULE
}
2. 自定義注解
java中使用@interface來建立自己的注解,此時會自動幫我們繼承
java.lang.annotation.Annotation
接口。格式為:
@元注解
@interface MyAnnotation {
}
我們可以在注解中定義參數,其格式為
參數類型 參數名();
且隻能定義String、基本資料類型,數組和enum。注意:注解中參數的指派是沒有順序的。如果該注解隻有一個值,那麼說明時可以使用
value=xxx
的方式指派。
@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
// 注解的參數,如果使用了注解并且沒有為參數設定初始值,那麼就會報錯
String name();
// 如果該參數的值預設是空,那麼不需要初始化
String addr() default "";
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
}
使用value作為注解的參數名時,當隻有一個參數,在指派時可忽略字元串。
@MyAnnotation3
public class AnnotationTest {
// 在使用時可以忽略
@MyAnnotation2("zhangsan")
@MyAnnotation3
public static void main(String[] args) {
}
}
@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
// 注解的參數,如果使用了注解并且沒有為參數設定初始值,那麼就會報錯
String name();
// 如果該參數的值預設是空,那麼不需要初始化
String addr() default "";
int id() default 0; // 0表示預設為0
int sal() default -1; // -1表示報不存在
String[] client() default {"zhangsan", "lisi"};
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
// 使用value作為參數名
String value();
}
//Target注解的value是一個數組,是以可以指定多個作用域
@Target(value={ElementType.TYPE, ElementType.METHOD})
@interface MyAnnotation3 {
}
3. 反射
3.1 概念
反射機制使得java具備了動态性,即在java程式運作的時候,程式可以根據運作時的代碼,改變自身的一些結構。
反射機制允許java程式在運作時,借助一些API來擷取一個類的全部資訊,并直接對該類中的屬性、方法進行操作。
3.2 核心機制
我們知道,java的代碼在編寫完成,通過編譯器編譯之後會生成
.class
檔案,這稱為位元組碼檔案。一個位元組碼檔案會通過類加載器加載到Java虛拟機中,在被調用時進行執行個體化。
**反射機制想建立對象也必須擷取到該對象的位元組碼檔案。**在java中,class檔案也是一個類。通常通過
Class c = Class.forName("java.lang.String");
,或者
Object.getClass()
獲得。
類加載機制
首先我們先了解一下class檔案。
先通過反射的方式擷取到class檔案:如下的三個HashCode值是相同的,說明class檔案在JVM運作時隻會儲存一份。一個類在被加載到記憶體中,其所有的内容都會被封裝到Class對象。
Class person1 = Class.forName("com.yindarui.annotation.Person");
Person person = new Person();
Class person2 = person.getClass();
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
//結果是一樣的
1967205423
1967205423
那麼到底什麼是Class類呢?所有的其他類都由Class類建立,但是Class類本身也是一個類。其封裝了一個對象(編寫的代碼)裡面的全部内容。JVM可以通過Class對象建立别的對象。同樣的,對于任何一個對象,JVM都可以通過對象的全限定名找到其Class檔案,并擷取該Class檔案的全部内容。
3.3 使用Class類
Class類用方法:
類型 | 方法 | 功能 |
---|---|---|
static | forName(String name) | 傳回指定類名的對象 |
public | Object newInstance() | 通過class對象調用預設的構造函數傳回一個對象 |
public | String getName() | 傳回該class對象的全限定名 |
public | Class getSuperClass() | 傳回該class對象的父類的class對象 |
public | Class[] getInterfaces() | 傳回該class對象實作的全部接口 |
public | ClassLoader getClassLoader() | 傳回該class對象的類加載器 |
public | Constructor[] getConstructors() | 傳回該class對象實作的全部接口 |
public | Method getMethod(String name, Class<?>… parameterTypes)) | 傳回該class對象指定的方法對象 |
public | Method[] getMethods()) | 傳回該class對象的全部方法對象 |
public | Field getDeclaredFields() | 傳回Field的一個數組 |
如果你熟悉java類包含的内容,那麼你一定會很好了解Class類中提供的方法。這幾乎包括了一個類中所有的資訊。(還可以擷取Annotation,這就是架構中大量使用的)
1. 擷取Class檔案
ArrayList<Class> classes = new ArrayList<>();
classes.add(Object.class); //根類
classes.add(Iterable.class); // 接口
classes.add(Override.class); // 注解
classes.add(ElementType.class); // 枚舉類
classes.add(String[].class); // 數組
classes.add(String[][].class); // 數組
classes.add(int.class); // 基本資料類型
classes.add(void.class); // 空類型
classes.add(Class.class); // Class本身也是一個
for (int i = 0; i < 9; i++) {
System.out.println(classes.get(i).hashCode());
}
// 結果:對于數組而言,一維數組和二維數組即使類型相同,其對應的Class檔案也不同。
1426407511
589431969
1967205423
42121758
20671747
257895351
874088044
783286238
705927765
2. java 的記憶體模型
類的加載過程是:
- load:将class檔案讀入記憶體,并建立一個Java.lang.Class對象。class檔案是靜态的代碼,需要将這些代碼轉化成運作時的一些資料結構,比如為一些值設計資料結構。
-
link:将類的二進制資料處理并放入JRE;
1. 驗證:檢查代碼,確定沒有錯誤;
2. 準備:為類變量配置設定記憶體空間并設定預設的初始值;
3. 解析:虛拟機中的常量池的符号引用替換直接引用。
- initialize:JVM對類進行初始化。首先會執行
方法,即類構造器,完成對類的初始化。初始化類的時候,必須先初始化其父類。<clint>()
我們來看一下這個過程:靜态代碼塊會在類被加載時就執行,我們在擷取一個類的Class對象時就會把這個Class檔案加載進來。
要明确:加載隻是把檔案讀入,并進行配置設定一定的記憶體空間,而初始化一個類則會消耗大量的資源,使用反射的方式加載類屬于被動加載,很多時候不會觸發多餘的類的初始化:圖檔來自b站狂神視訊
3. 類加載器
在JVM的記憶體模型中,類加載器就是負責找到這個類的class檔案并加載到JVM中。存儲class檔案的地方叫方法區,這個區中儲存這加載過的class檔案,并提供一定的緩存機制,以減少頻繁的從磁盤中加載類檔案。
java提供了幾種類加載器:圖檔來自b站狂神視訊
所謂的不同的類加載器,其實就是負責加載不同地方的class檔案。
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent); // AppClassLoader
ClassLoader parent1 = parent.getParent(); // PlatformClassLoader
System.out.println(parent1); // null
System.out.println(Object.class.getClassLoader()); // null
雙親委派機制
JVM在加載類的時候是從啟動加載器向下找的,即如果我們自定義一個java.lang.String類型,是無法被加載進JVM的。因為JVM通過boostrap ClassLoader 擷取時就發現了這個類,加載後就停止尋找了。
這樣做保證了Java代碼的執行的正确性與安全性。
4. 通過反射建立對象
使用newInstance方法建立對象需要該類有無參構造器。
此外,我們還可以用别的方法建立:
- Class類的getDeclaredConstructor() 擷取構造器;
- 向構造器中傳遞參數的class檔案,包括該構造器的所有參數;
- 通過Constructor進行執行個體化。
package com.yindarui.annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
*
*/
public class Constructor {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class c1 = Class.forName("com.yindarui.annotation.Student");
java.lang.reflect.Constructor stuCon = c1.getDeclaredConstructor(int.class, String.class);
Object yindarui = stuCon.newInstance(1, "yindarui");
System.out.println(yindarui.toString());
Method setId = c1.getDeclaredMethod("setId", int.class);
System.out.println("通過invoke來修改id參數");
setId.invoke(yindarui,20);
System.out.println(yindarui.toString());
System.out.println("通過invoke來修改name參數, 此時setName方法是私有的");
Method setName = c1.getDeclaredMethod("setName", String.class);
setName.setAccessible(true);
setName.invoke(yindarui,"helloworld");
System.out.println(yindarui.toString());
}
}
class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public void setId(int id) {
this.id = id;
}
private void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
結果:
總之,通過反射來操作屬性和方法時,一定要傳遞你要操作的對象。注意:如果你通路的屬性、方法或者構造器是私有的,那麼在調用之前一定要使用setAccessible來擷取權限。
5. 反射擷取注解
注解開發的核心就是利用反射機制擷取一個class檔案中的注解内容。通常有幾種方式:
- 通過class擷取全部注解;
- 通過指定某個注解名稱擷取注解;
- 通過某個屬性或者方法擷取标記在其上的注解;
package com.yindarui.annotation;
import java.lang.annotation.*;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
@MyAnnotation5("hello")
public class ReflectionForAnnotation {
@MyAnnotation4("id = 10")
public int id;
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class aClass = Class.forName("com.yindarui.annotation.ReflectionForAnnotation");
Field id = aClass.getDeclaredField("id");
// 擷取全部的注解
Annotation[] annotation2 = aClass.getAnnotations();
// 擷取指定的注解
MyAnnotation5 annotation1 = (MyAnnotation5) aClass.getAnnotation(MyAnnotation5.class);
System.out.println(annotation1);
System.out.println(annotation1.value());
// 通過屬性名擷取标記在屬性上的注解,方法也一樣
MyAnnotation4 annotation = id.getAnnotation(com.yindarui.annotation.MyAnnotation4.class);
// 擷取在id屬性上的全部注解
Annotation[] annotations = id.getAnnotations();
System.out.println(annotation.value());
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MyAnnotation4{
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation5{
String value();
}
結果: