反射、註解和動態代理
反射是指計算機程式在執行時訪問、檢測和修改它本身狀態或行為的一種能力,是一種超程式設計語言特性,有很多語言都提供了對反射機制的支援,它使程式能夠編寫程式。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訪問閘道器請求,其分層結構可參考 ofollow,noindex" target="_blank">《基於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(// ……);
上述程式碼實現均為虛擬碼,僅說明解決方案思路。
在該案例中,綜合使用了自定義註解、反射以及動態代理,是對上述理論知識的一個具體應用。