反射、註解和動態代理

反射是指計算機程序在運行時訪問、檢測和修改它自己狀態或行爲的一種能力,是一種元編程語言特性,有不少語言都提供了對反射機制的支持,它使程序可以編寫程序。Java的反射機制使得Java可以動態的獲取類的信息和調用對象的方法。java

1、Java反射機制及基本用法

在Java中,Class(類類型)是反射編程的起點,表明運行時類型信息(RTTI,Run-Time Type Identification)。java.lang.reflect包含了Java支持反射的主要組件,如Constructor、Method和Field等,分別表示類的構造器、方法和域,它們的關係以下圖所示。android

Java反射機制主要組件

Constructor和Method與Field的區別在於前者繼承自抽象類Executable,是能夠在運行時動態調用的,而Field僅僅具有可訪問的特性,且默認爲不可訪問。下面瞭解下它們的基本用法:編程

Java反射類及核心方法

  • 獲取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)。

2、 註解

註解(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")編程語言

3、動態代理

代理是一種結構型設計模式,當沒法或不想直接訪問某個對象,或者訪問某個對象比較複雜的時候,能夠經過一個代理對象來間接訪問,代理對象向客戶端提供和真實對象一樣的接口功能。經典設計模式中,代理模式有四種角色:

  • 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()方法中的第二種方式。

4、案例: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(// ……);

上述代碼實現均爲僞代碼,僅說明解決方案思路。

在該案例中,綜合使用了自定義註解、反射以及動態代理,是對上述理論知識的一個具體應用。

相關文章
相關標籤/搜索