反射是指計算機程序在運行時訪問、檢測和修改它本身狀態或行為的一種能力,是一種元編程語言特性,有很多語言都提供了對反射機制的支持,它使程序能夠編寫程序。Java的反射機制使得Java能夠動態的獲取類的信息和調用對象的方法。
一、Java反射機制及基本用法
在Java中,Class(類類型)是反射編程的起點,代表運行時類型信息(RTTI,Run-Time Type Identification)。java.lang.reflect包含了Java支持反射的主要組件,如Constructor、Method和Field等,分別表示類的構造器、方法和域,它們的關系如下圖所示。
Constructor和Method與Field的區別在於前者繼承自抽象類Executable,是可以在運行時動態調用的,而Field僅僅具備可訪問的特性,且默認為不可訪問。下面了解下它們的基本用法:
- 獲取Class對象有三種方式,Class.forName適合於已知類的全路徑名,典型應用如加載JDBC驅動。對同一個類,不同方式獲得的Class對象是相同的。
// 1. 采用Class.forName獲取類的Class對象
Class clazz0 = Class.forName("com.yhthu.java.ClassTest");
System.out.println("clazz0:" + clazz0);
// 2. 采用.class方法獲取類的Class對象
Class clazz1 = ClassTest.class;
System.out.println("clazz1:" + clazz1);
// 3. 采用getClass方法獲取類的Class對象
ClassTest classTest = new ClassTest();
Class clazz2 = classTest.getClass();
System.out.println("clazz2:" + clazz2);
// 4. 判斷Class對象是否相同
System.out.println("Class對象是否相同:" + ((clazz0.equals(clazz1)) && (clazz1.equals(clazz2))));
注意:三種方式獲取的Class對象相同的前提是使用了相同的類加載器,比如上述代碼中默認采用應用程序類加載器(sun.misc.Launcher$AppClassLoader)。不同類加載器加載的同一個類,也會獲取不同的Class對象:
// 自定義類加載器
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
// 采用自定義類加載器加載
Class clazz3 = Class.forName("com.yhthu.java.ClassTest", true, myLoader);
// clazz0與clazz3並不相同
System.out.println("Class對象是否相同:" + clazz0.equals(clazz3));
- 通過Class的getDeclaredXxxx和getXxx方法獲取構造器、方法和域對象,兩者的區別在於前者返回的是當前Class對象申明的構造器、方法和域,包含修飾符為private的;后者只返回修飾符為public的構造器、方法和域,但包含從基類中繼承的。
// 返回申明為public的方法,包含從基類中繼承的
for (Method method: String.class.getMethods()) {
System.out.println(method.getName());
}
// 返回當前類申明的所有方法,包含private的
for (Method method: String.class.getDeclaredMethods()) {
System.out.println(method.getName());
}
- 通過Class的newInstance方法和Constructor的newInstance方法方法均可新建類型為Class的對象,通過Method的invoke方法可以在運行時動態調用該方法,通過Field的set方法可以在運行時動態改變域的值,但需要首先設置其為可訪問(setAccessible)。
二、 注解
注解(Annontation)是Java5引入的一種代碼輔助工具,它的核心作用是對類、方法、變量、參數和包進行標注,通過反射來訪問這些標注信息,以此在運行時改變所注解對象的行為。Java中的注解由內置注解和元注解組成。內置注解主要包括:
- @Override - 檢查該方法是否是重載方法。如果發現其父類,或者是引用的接口中並沒有該方法時,會報編譯錯誤。
- @Deprecated - 標記過時方法。如果使用該方法,會報編譯警告。
- @SuppressWarnings - 指示編譯器去忽略注解中聲明的警告。
- @SafeVarargs - Java 7 開始支持,忽略任何使用參數為泛型變量的方法或構造函數調用產生的警告。
- @FunctionalInterface - Java 8 開始支持,標識一個匿名函數或函數式接口。
這里,我們重點關注元注解,元注解位於java.lang.annotation包中,主要用於自定義注解。元注解包括:
- @Retention - 標識這個注解怎么保存,是只在代碼中,還是編入class文件中,或者是在運行時可以通過反射訪問,枚舉類型分為別SOURCE、CLASS和RUNTIME;
- @Documented - 標記這些注解是否包含在用戶文檔中。
- @Target - 標記這個注解應該是哪種Java 成員,枚舉類型包括TYPE、FIELD、METHOD、CONSTRUCTOR等;
- @Inherited - 標記這個注解可以繼承超類注解,即子類Class對象可使用getAnnotations()方法獲取父類被@Inherited修飾的注解,這個注解只能用來申明類。
- @Repeatable - Java 8 開始支持,標識某注解可以在同一個聲明上使用多次。
自定義元注解需重點關注兩點:1)注解的數據類型;2)反射獲取注解的方法。首先,注解中的方法並不支持所有的數據類型,僅支持八種基本數據類型、String、Class、enum、Annotation和它們的數組。比如以下代碼會產生編譯時錯誤:
@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
// 1. 注解數據類型不能是Object;2. 默認值不能為null
Object value() default null;
// 支持的定義方式
String value() default "";
}
其次,上節中提到的反射相關類(Class、Constructor、Method和Field)和Package均實現了AnnotatedElement接口,該接口定義了訪問反射信息的方法,主要如下:
// 獲取指定注解類型
getAnnotation(Class<T>):T;
// 獲取所有注解,包括從父類繼承的
getAnnotations():Annotation[];
// 獲取指定注解類型,不包括從父類繼承的
getDeclaredAnnotation(Class<T>):T
// 獲取所有注解,不包括從父類繼承的
getDeclaredAnnotations():Annotation[];
// 判斷是否存在指定注解
isAnnotationPresent(Class<? extends Annotation>:boolean
當使用上例中的AnnotationTest 標注某個類后,便可在運行時通過該類的反射方法訪問注解信息了。
@AnnotationTest("yhthu")
public class AnnotationReflection {
public static void main(String[] args) {
AnnotationReflection ar = new AnnotationReflection();
Class clazz = ar.getClass();
// 判斷是否存在指定注解
if (clazz.isAnnotationPresent(AnnotationTest.class)) {
// 獲取指定注解類型
Annotation annotation = clazz.getAnnotation(AnnotationTest.class);
// 獲取該注解的值
System.out.println(((AnnotationTest) annotation).value());
}
}
}
當自定義注解只有一個方法value()時,使用注解可只寫值,例如:@AnnotationTest("yhthu")
三、動態代理
代理是一種結構型設計模式,當無法或不想直接訪問某個對象,或者訪問某個對象比較復雜的時候,可以通過一個代理對象來間接訪問,代理對象向客戶端提供和真實對象同樣的接口功能。經典設計模式中,代理模式有四種角色:
- Subject抽象主題類——申明代理對象和真實對象共同的接口方法;
- RealSubject真實主題類——實現了Subject接口,真實執行業務邏輯的地方;
- ProxySubject代理類——實現了Subject接口,持有對RealSubject的引用,在實現的接口方法中調用RealSubject中相應的方法執行;
- Cliect客戶端類——使用代理對象的類。
在實現上,代理模式分為靜態代理和動態代理,靜態代理的代理類二進制文件是在編譯時生成的,而動態代理的代理類二進制文件是在運行時生成並加載到虛擬機環境的。JDK提供了對動態代理接口的支持,開源的動態代理庫(Cglib、Javassist和Byte Buddy)提供了對接口和類的代理支持,本節將簡單比較JDK和Cglib實現動態代理的異同,后續章節會對Java字節碼編程做詳細分析。
3.1 JDK動態代理接口
JDK實現動態代理是通過Proxy類的newProxyInstance方法實現的,該方法的三個入參分別表示:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
- ClassLoader loader,定義代理生成的類的加載器,可以自定義類加載器,也可以復用當前Class的類加載器;
- Class<?>[] interfaces,定義代理對象需要實現的接口;
- InvocationHandler h,定義代理對象調用方法的處理,其invoke方法中的Object proxy表示生成的代理對象,Method表示代理方法, Object[]表示方法的參數。
通常的使用方法如下:
private Object getProxy() {
return Proxy.newProxyInstance(JDKProxyTest.class.getClassLoader(), new Class<?>[]{Subject.class},
new MyInvocationHandler(new RealSubject()));
}
private static class MyInvocationHandler implements InvocationHandler {
private Object realSubject;
public MyInvocationHandler(Object realSubject) {
this.realSubject = realSubject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Some thing before method invoke");
Object result = method.invoke(realSubject, args);
System.out.println("Some thing after method invoke");
return result;
}
}
類加載器采用當前類的加載器,默認為應用程序類加載器(sun.misc.Launcher$AppClassLoader);接口數組以Subject.class為例,調用方法處理類MyInvocationHandler實現InvocationHandler接口,並在構造器中傳入Subject的真正的業務功能服務類RealSubject,在執行invoke方法時,可以在實際方法調用前后織入自定義的處理邏輯,這也就是AOP(面向切面編程)的原理。
關於JDK動態代理,有兩個問題需要清楚:
- Proxy.newProxyInstance的代理類是如何生成的?Proxy.newProxyInstance生成代理類的核心分成兩步:
// 1. 獲取代理類的Class對象
Class<?> cl = getProxyClass0(loader, intfs);
// 2. 利用Class獲取Constructor,通過反射生成對象
cons.newInstance(new Object[]{h});
與反射獲取Class對象時搜索classpath路徑的.class文件不同的是,這里的Class對象完全是“無中生有”的。getProxyClass0根據類加載器和接口集合返回了Class對象,這里采用了緩存的處理。
// 緩存(key, sub-key) -> value,其中key為類加載器,sub-key為代理的接口,value為Class對象
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
// 如果實現了代理接口的類已存在就返回緩存對象,否則就通過ProxyClassFactory生成
private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
return proxyClassCache.get(loader, interfaces);
}
如果實現了代理接口的類已存在就返回緩存對象,否則就通過ProxyClassFactory生成。ProxyClassFactory又是通過下面的代碼生成Class對象的。
// 生成代理類字節碼文件
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
try {
// defineClass0為native方法,生成Class對象
return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
generateProxyClass方法是用來生成字節碼文件的,根據生成的字節碼文件,再在native層生成Class對象。
- InvocationHandler的invoke方法是怎樣調用的?
回答這個問題得先看下上面生成的Class對象究竟是什么樣的,將ProxyGenerator生成的字節碼保存成文件,然后反編譯打開(IDEA直接打開),可見生成的Proxy.class主要包含equals、toString、hashCode和代理接口的request方法實現。
public final class $Proxy extends Proxy implements Subject {
// m1 = Object的equals方法
private static Method m1;
// m2 = Object的toString方法
private static Method m2;
// Subject的request方法
private static Method m3;
// Object的hashCode方法
private static Method m0;
// 省略m1/m2/m0,此處只列出request方法實現
public final void request() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
}
由於生成的代理類繼承自Proxy,super.h即是Prxoy的InvocationHandler,即代理類的request方法直接調用了InvocationHandler的實現,這就回答了InvocationHandler的invoke方法是如何被調用的了。
3.2 Cglib動態代理接口和類
Cglib的動態代理是通過Enhancer類實現的,其create方法生成動態代理的對象,有五個重載方法:
create():Object
create(Class, Callback):Object
create(Class, Class[], Callback):Object
create(Class, Class[], CallbackFilter, Callback):Object
create(Class[], Object):Object
常用的是第二個和第三個方法,分別用於動態代理類和動態代理接口,其使用方法如下:
private Object getProxy() {
// 1. 動態代理類
return Enhancer.create(RealSubject.class, new MyMethodInterceptor());
// 2. 動態代理接口
return Enhancer.create(Object.class, new Class<?>[]{Subject.class}, new MyMethodInterceptor());
}
private static class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Some thing before method invoke");
Object result = proxy.invokeSuper(obj, args);
System.out.println("Some thing after method invoke");
return result;
}
}
從上小節可知,JDK只能代理接口,代理生成的類實現了接口的方法;而Cglib是通過繼承被代理的類、重寫其方法來實現的,如:create方法入參的第一個參數就是被代理類的類型。當然,Cglib也能代理接口,比如getProxy()方法中的第二種方式。
四、案例:Android端dubbo:reference化的網絡訪問
Dubbo是一款高性能的Java RPC框架,是服務治理的重量級中間件。Dubbo采用dubbo:service描述服務提供者,dubbo:reference描述服務消費者,其共同必填屬性為interface,即Java接口。Dubbo正是采用接口來作為服務提供者和消費者之間的“共同語言”的。
在移動網絡中,Android作為服務消費者,一般通過HTTP網關調用后端服務。在國內的大型互聯網公司中,Java后端大多采用了Dubbo及其變種作為服務治理、服務水平擴展的解決方案。因此,HTTP網關通常需要Android的網絡請求中提供調用的服務名稱、服務方法、服務版本、服務分組等信息,然后通過這些信息反射調用Java后端提供的RPC服務,實現從HTTP協議到RPC協議的轉換。
關於Android訪問網關請求,其分層結構可參考《基於Retrofit+RxJava的Android分層網絡請求框架》。
那么,Android端能否以dubbo:reference化的方式申明需要訪問的網絡服務呢?如何這樣,將極大提高Android開發人員和Java后端開發之間的溝通效率,以及Android端的代碼效率。
首先,自定義服務的消費者注解Reference,通過該注解標記某個服務。
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Reference {
// 服務接口名
String service() default "";
// 服務版本
String version() default "";
// 服務分組
String group() default "";
// 省略字段
}
其次,通過接口定義某個服務消費(如果可以直接引入后端接口,此步驟可省略),在注解中指明該服務對應的后端服務接口名、服務版本、服務分組等信息;
@Reference(service = "com.yhthu.java.ClassTestService", group = "yhthu", version = "v_test_0.1")
public interface ClassTestService {
// 實例方法
Response echo(String pin);
}
這樣就完成了服務的申明,接下來的問題是如何實現服務的調用呢?上述申明的服務接口如何定義實現呢?這里就涉及依賴注入和動態代理。我們先定義一個標記注解@Service,標識需要被注入實現的服務申明。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
}
// 在需要使用服務的地方(比如Activity中)申明需要調用的服務
@Service
private ClassTestService classTestService;
在調用classTestService的方法之前,需要注入該接口服務的實現,因此,該操作可以在調用組件初始化的時候進行。
// 接口與對應實現的緩存
private Map<Class<?>, Object> serviceContainer = new HashMap<>();
// 依賴注入
public void inject(Object obj) {
// 1. 掃描該類中所有添加@Service注解的域
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Service.class)) {
Class<?> clazz = field.getType();
if (clazz.getAnnotation(Reference.class) == null) {
Log.e("ClassTestService", "接口地址未配置");
continue;
}
// 2. 從緩存中取出或生成接口類的實現(動態代理)
Object impl = serviceContainer.get(clazz);
if (impl == null) {
impl = create(clazz);
serviceContainer.put(clazz, impl);
}
// 3. 設置服務接口實現
try {
field.setAccessible(true);
field.set(obj, impl);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
inject方法的關鍵有三步:
- 掃描該類中所有添加@Service注解的字段,即可得到上述代碼示例中的ClassTestService字段;
- 從緩存中取出或生成接口類的實現。由於通過接口定義了服務,並且實現不同服務的實現方式基本一致(即將服務信息發送HTTP網關),在生成實現上可選擇JDK的動態代理。
- 設置服務接口實現,完成為接口注入實現。
private <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 獲取服務信息
Annotation reference = service.getAnnotation(Reference.class);
String serviceName = ((Reference) reference).service();
String versionName = ((Reference) reference).version();
String groupName = ((Reference) reference).group();
// 2. 獲取方法名
String methodName = method.getName();
// 3. 根據服務信息發起請求,返回調用結果
return Request.request(serviceName, versionName, groupName, methodName, param);
}
});
}
在HTTP網關得到服務名稱、服務方法、服務版本、服務分組等信息之后,即可實現對后端服務的反射調用。總的來講,即可實現Android端dubbo:reference化的網絡訪問。
// 調用ClassTestService服務的方法
classTestService.echo("yhthu").callback(// ……);
上述代碼實現均為偽代碼,僅說明解決方案思路。
在該案例中,綜合使用了自定義注解、反射以及動態代理,是對上述理論知識的一個具體應用。