深入理解Java反射

銅板街技術發表於2019-01-31

題外話

最近公司建立了技術部的公眾號用來鼓勵大家進行分享,很多同學比較糾結,覺得找不到比較適合聊的topic。總的來說大概兩個原因:一個是覺得太基礎講出來比較 low 沒有人會關注,另一個是講一些很牛的新技術又怕出錯;然而每一項技術在自己的應用中都會有你自己獨特的視角,也許這一點正是別人關心的。我個人認為分享一些我們在編碼中經常會碰到,而大多數人可能知其然而不知其所以然的話題是很有意義的,今天我打算分享下我們 Java 中一個經常用到的工具,反射和動態代理。

當我們在 IDE 中編寫程式碼的時候,打一個點號,IDE會自動彈出對應的屬性和方法名。當我們在debug的時候,IDE會將方法執行時方法內區域性變數和外部例項上屬性的值都展示出來,spring中的IOC和AOP,以及一個RPC框架中,我們反序列化,consumer的代理,以及provider的呼叫都會用到java的反射功能,有人說使用反射會慢,那麼到底慢在哪裡呢?

反射

反射使 Java 語言有了動態編譯的功能,也就是在我們編碼的時候不需要知道物件的具體型別,但是在執行期可以通過Class.forName()獲取一個類的class物件,在通過newInstance獲取例項。

先看下java.lang.reflect包下的幾個主要類的關係圖,當然動態代理的工具類也在該包下。

深入理解Java反射

  • AnnotatedElement

作為頂級介面,這個介面提供了獲取註解相關的功能,我們在方法,類,屬性,構造方法上都可以加註解,所以下面的Field,Method,Constructor都有實現這個介面,以下是我們經常用的兩個方法,jdk8以後介面裡面可以通過default修飾方法實現了

Annotation[] getAnnotations(); //獲取目標物件(方法和屬性)上的所有註解
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
     Objects.requireNonNull(annotationClass);
     // Loop over all directly-present annotations looking for a matching one
     for (Annotation annotation : getDeclaredAnnotations()) {
         if (annotationClass.equals(annotation.annotationType())) {
             // More robust to do a dynamic cast at runtime instead
             // of compile-time only.
             return annotationClass.cast(annotation);
         }
     }
     return null;
 }
複製程式碼
  • GenericDeclaration

提供了獲取泛型相關的功能,只有方法和構造方法上支援泛型,所以只有Method,Constructor實現了該介面

  • Member

作為一個物件內部方法和屬性的宣告的抽象,包含了名稱,修飾符,所在的類,其中修飾符包含了 static final public private volatile 等,通過一個整數表示,每一個型別在二進位制中佔一個位.

public Class<?> getDeclaringClass();
public String getName();
public int getModifiers();


以下為Modifier類部分程式碼

public static final int PUBLIC           = 0x00000001;
public static final int PRIVATE          = 0x00000002;
public static final int PROTECTED        = 0x00000004;
public static final int STATIC           = 0x00000008;
public static final int FINAL            = 0x00000010;
public static final int SYNCHRONIZED     = 0x00000020;
public static final int VOLATILE         = 0x00000040;
public static final int TRANSIENT        = 0x00000080;
public static final int NATIVE           = 0x00000100;
public static final int INTERFACE        = 0x00000200;
public static final int ABSTRACT         = 0x00000400;
public static final int STRICT           = 0x00000800;
public static boolean isPublic(int mod) {
    return (mod & PUBLIC) != 0;
}
複製程式碼
  • AccessibleObject

    這是一個類,提供了許可權管理的功能,例如是否允許在反射中在外部呼叫一個private方法,獲取一個private屬性的值,所以method,constructor,field都繼承該類,下面這段程式碼展示瞭如何在反射中訪問一個私有的成員變數,class物件的構造方法不允許對外。

private static void setAccessible0(AccessibleObject obj, boolean flag)
    throws SecurityException
{
    if (obj instanceof Constructor && flag == true) {
        Constructor<?> c = (Constructor<?>)obj;
        if (c.getDeclaringClass() == Class.class) {
            throw new SecurityException("Cannot make a java.lang.Class" +
                                        " constructor accessible");
        }
    }
    obj.override = flag;
}

boolean override;

public boolean isAccessible() {
    return override;
}
複製程式碼

以下為 Field裡面通過field.get(原始物件)獲取屬性值得實現,先通過override做校驗,如果沒有過載該許可權,則需要校驗訪問許可權

public Object get(Object obj)
    throws IllegalArgumentException, IllegalAccessException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    return getFieldAccessor(obj).get(obj);
}
複製程式碼

下面我們看看如何通過反射修改Field裡面屬性的值

通過上面的程式碼,我們可以看出jdk將Field屬性的讀取和寫入委託給FieldAccessor,那麼如何獲取FieldAccessor呢

class UnsafeFieldAccessorFactory {
    UnsafeFieldAccessorFactory() {
    }

    static FieldAccessor newFieldAccessor(Field var0, boolean var1) {
        Class var2 = var0.getType();
        boolean var3 = Modifier.isStatic(var0.getModifiers());
        boolean var4 = Modifier.isFinal(var0.getModifiers());
        boolean var5 = Modifier.isVolatile(var0.getModifiers());
        boolean var6 = var4 || var5;
        boolean var7 = var4 && (var3 || !var1);

        if (var3) {
            UnsafeFieldAccessorImpl.unsafe.ensureClassInitialized(var0.getDeclaringClass());

            return (FieldAccessor) ((!var6)
            ? ((var2 == Boolean.TYPE)
            ? new UnsafeStaticBooleanFieldAccessorImpl(var0)
            : ((var2 == Byte.TYPE)
            ? new UnsafeStaticByteFieldAccessorImpl(var0)
            : ((var2 == Short.TYPE)
            ? new UnsafeStaticShortFieldAccessorImpl(var0)
            : ((var2 == Character.TYPE)
            ? new UnsafeStaticCharacterFieldAccessorImpl(var0)
            : ((var2 == Integer.TYPE)
            ? new UnsafeStaticIntegerFieldAccessorImpl(var0)
            : ((var2 == Long.TYPE)
            ? new UnsafeStaticLongFieldAccessorImpl(var0)
            : ((var2 == Float.TYPE)
            ? new UnsafeStaticFloatFieldAccessorImpl(var0)
            : ((var2 == Double.TYPE)
            ? new UnsafeStaticDoubleFieldAccessorImpl(var0)
            : new UnsafeStaticObjectFieldAccessorImpl(var0)))))))))
            : ((var2 == Boolean.TYPE)
            ? new UnsafeQualifiedStaticBooleanFieldAccessorImpl(var0, var7)
            : ((var2 == Byte.TYPE)
            ? new UnsafeQualifiedStaticByteFieldAccessorImpl(var0, var7)
            : ((var2 == Short.TYPE)
            ? new UnsafeQualifiedStaticShortFieldAccessorImpl(var0, var7)
            : ((var2 == Character.TYPE)
            ? new UnsafeQualifiedStaticCharacterFieldAccessorImpl(var0, var7)
            : ((var2 == Integer.TYPE)
            ? new UnsafeQualifiedStaticIntegerFieldAccessorImpl(var0, var7)
            : ((var2 == Long.TYPE)
            ? new UnsafeQualifiedStaticLongFieldAccessorImpl(var0, var7)
            : ((var2 == Float.TYPE)
            ? new UnsafeQualifiedStaticFloatFieldAccessorImpl(var0, var7)
            : ((var2 == Double.TYPE)
            ? new UnsafeQualifiedStaticDoubleFieldAccessorImpl(var0, var7)
            : new UnsafeQualifiedStaticObjectFieldAccessorImpl(var0, var7))))))))));
        } else {
            return (FieldAccessor) ((!var6)
            ? ((var2 == Boolean.TYPE)
            ? new UnsafeBooleanFieldAccessorImpl(var0)
            : ((var2 == Byte.TYPE) ? new UnsafeByteFieldAccessorImpl(var0)
                                   : ((var2 == Short.TYPE)
            ? new UnsafeShortFieldAccessorImpl(var0)
            : ((var2 == Character.TYPE)
            ? new UnsafeCharacterFieldAccessorImpl(var0)
            : ((var2 == Integer.TYPE)
            ? new UnsafeIntegerFieldAccessorImpl(var0)
            : ((var2 == Long.TYPE) ? new UnsafeLongFieldAccessorImpl(var0)
                                   : ((var2 == Float.TYPE)
            ? new UnsafeFloatFieldAccessorImpl(var0)
            : ((var2 == Double.TYPE) ? new UnsafeDoubleFieldAccessorImpl(var0)
                                     : new UnsafeObjectFieldAccessorImpl(var0)))))))))
            : ((var2 == Boolean.TYPE)
            ? new UnsafeQualifiedBooleanFieldAccessorImpl(var0, var7)
            : ((var2 == Byte.TYPE)
            ? new UnsafeQualifiedByteFieldAccessorImpl(var0, var7)
            : ((var2 == Short.TYPE)
            ? new UnsafeQualifiedShortFieldAccessorImpl(var0, var7)
            : ((var2 == Character.TYPE)
            ? new UnsafeQualifiedCharacterFieldAccessorImpl(var0, var7)
            : ((var2 == Integer.TYPE)
            ? new UnsafeQualifiedIntegerFieldAccessorImpl(var0, var7)
            : ((var2 == Long.TYPE)
            ? new UnsafeQualifiedLongFieldAccessorImpl(var0, var7)
            : ((var2 == Float.TYPE)
            ? new UnsafeQualifiedFloatFieldAccessorImpl(var0, var7)
            : ((var2 == Double.TYPE)
            ? new UnsafeQualifiedDoubleFieldAccessorImpl(var0, var7)
            : new UnsafeQualifiedObjectFieldAccessorImpl(var0, var7))))))))));
        }
    }
}

複製程式碼

以上程式碼可以發現,通過工廠模式根據field屬性型別以及是否靜態來獲取,為什麼會有這樣的劃分呢?

首先,jdk是通過 UNSAFE 類對堆記憶體中物件的屬性進行直接的讀取和寫入,要讀取和寫入首先需要確定屬性所在的位置,也就是相對物件起始位置的偏移量,而靜態屬性是針對類的不是每個物件例項一份,所以靜態屬性的偏移量需要單獨獲取。

其實通過該偏移量我們可以大致推斷出一個例項內每個屬性在堆記憶體的相對位置,以及分別佔用多大的空間,有了位置資訊,我們還需要這個欄位的型別,以方便執行器知道讀幾個位元組的資料,並且如何進行解析,目前提供了8大基礎型別(char vs Charector)和陣列和普通引用型別。

Java虛擬機器為了保證每個物件所佔的空間都是8個位元組倍數,有時候為了避免兩個volatile欄位存放在同一個快取行,所以有時候會再某些欄位上做空位填充。

以下為UnSafe類的部分程式碼

public final class Unsafe {
    private static final Unsafe theUnsafe;
    private Unsafe() {
    }

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    public native int getInt(Object var1, long var2);
    public native void putInt(Object var1, long var2, int var4);
    public native Object getObject(Object var1, long var2);
    public native void putObject(Object var1, long var2, Object var4);
    public native boolean getBoolean(Object var1, long var2);
    public native void putBoolean(Object var1, long var2, boolean var4);
    public native byte getByte(Object var1, long var2);

    public native long objectFieldOffset(Field var1);
@Deprecated
public int fieldOffset(Field var1) {
    return Modifier.isStatic(var1.getModifiers())?(int)this.staticFieldOffset(var1):(int)this.objectFieldOffset(var1);
}
複製程式碼

然後我們在來看看通過反射來呼叫方法

同樣,jdk 通過MethodAccessor來進行method的呼叫,Java虛擬機器提供了兩種模式來支援method的呼叫 一個是NativeMethodAccessorImpl 一個是通過 ASM 位元組碼直接動態生成一個類在invoke方法內部呼叫目標方法,由於是動態生成所以jdk中沒有其原始碼,但jdk提供了DelegatingMethodAccessorImpl委派模式以方便在執行過程中可以動態切換位元組碼模式和native模式,我們可以看下生成MethodAccessor的程式碼。

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        if(++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            this.parent.setDelegate(var3);
        }

        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
複製程式碼

可以看到 JDK 內部通過 numInvocations 判斷如果該反射呼叫次數超過ReflectionFactory.inflationThreshold()則用位元組碼實現。如果小於該值則採用native實現,native的呼叫比位元組碼方式慢很多, 動態實現和本地實現相比執行效率要快20倍,因為動態實現無需經過JAVA,C++再到JAVA的轉換,之前在jdk6以前有個工具ReflectAsm就是採用這種方式提升執行效率,不過在jdk8以後,也提供了位元組碼方式,由於許多反射只需要執行一次,然而動態方式生成位元組碼十分耗時,所以jdk提供了一個閾值預設15,當某個反射的呼叫次數小於15的話就走本地實現,大於15則走動態模式,而這個閾值可以在jdk啟動引數裡面做配置。

反射為什麼慢

經過以上優化,其實反射的效率並不慢,在某些情況下可能達到和直接呼叫基本相同的效率,但是在首次執行或者沒有快取的情況下還是會有效能上的開銷,主要在以下方面

  1. Class.forName();會呼叫本地方法,我們用到的method和field都會在此時載入進來,雖然會進行快取,但是本地方法免不了有JAVA到C+=在到JAVA得轉換開銷。
  2. class.getMethod(),會遍歷該class所有的公用方法,如果沒匹配到還會遍歷父類的所有方法,並且getMethods()方法會返回結果的一份拷貝,所以該操作不僅消耗CPU還消耗堆記憶體,在熱點程式碼中應該儘量避免,或者進行快取。
  3. invoke引數是一個object陣列,而object陣列不支援java基礎型別,而自動裝箱也是很耗時的。

反射的運用

  • spring doc

spring載入bean的流程基本都用到了反射機制

  1. 獲取類的例項 通過構造方法getInstance(靜態變數初始化,屬性賦值,構造方法);
  2. 如果實現了BeanNameAware介面,則用反射注入bean賦值給屬性;
  3. 如果實現了BeanFactoryAware介面,則設定 beanFactory;
  4. 如果實現了ApplicationContextAware,則設定ApplicationContext;
  5. 呼叫BeanPostProcesser的預先初始化方法;
  6. 如果實現了InitializingBean,呼叫AfterPropertySet方法;
  7. 呼叫定製的 init-method()方法 對應的直接 @PostConstruct;
  8. 呼叫BeanPostProcesser的後置初始化完畢的方法。
  • 序列化

fastjson可以參考ObjectDeserializer的幾個實現 JavaBeanDeserializer和ASMJavaBeanDeserializer。

動態代理

jdk提供了一個工具類來動態生成一個代理,允許在執行某一個方法時進行額外的處理。

Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

class HWInvocationHandler implements InvocationHandler{
        //目標物件
        private Object target;
        public HWInvocationHandler(Object target){
            this.target = target;
        }
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("------插入前置通知程式碼-------------");
            //執行相應的目標方法
            Object rs = method.invoke(target,args);
            System.out.println("------插入後置處理程式碼-------------");
            return rs;
        }
    }
複製程式碼

我們分析下這個方法的實現,首先生成的代理物件,需要實現引數裡面宣告的所有介面。介面的實現應給委託給InvocationHandler進行處理,invocationHandler裡面可以根據method宣告判斷是否需要做增強,所以所生成的代理類裡面必須能夠獲取到InvocationHandler,在我們無法知道代理類的具體型別的時候,我們可以通過反射從構造方法裡將InvocationHandler傳給代理類的例項。 所以 總的來說生成代理物件需要兩步

  1. 獲取代理類的class物件
  2. 通過class物件獲取構造方法,通過反射生成代理類的例項,並將InvocationHandler傳人
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();

    /*
     * Look up or generate the designated proxy class.
     * 生成代理類
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {

        //獲取代理類的構造方法
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        //獲取代理類的例項,並且將invocationhandler傳人
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
       ...
    }
}
複製程式碼

下面我們在看下 getProxyClass0 如何獲取代理類的class物件,這裡idk通過WeakCache來快取已經生成的class物件,因為生成該class通過位元組碼生成還是很耗時,同時為了解決之前由於動態代理生成太多class物件導致記憶體不足,所以這裡通過弱引用WeakReference來快取所生成的代理物件class,當發生GC的時候如果該class物件沒有其他的強引用將會被直接回收。 生成代理類的class在ProxyGenerator的generateProxyClass方法內實現,該方法返回一個byte[]陣列,最後通過一個本地方法載入到虛擬機器,所以可以看出生成該物件還是非常耗時的。

//生成位元組碼陣列
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
    proxyName, interfaces, accessFlags);
try {
//載入進虛擬機器
    return defineClass0(loader, proxyName,
                        proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
    /*
     * A ClassFormatError here means that (barring bugs in the
     * proxy class generation code) there was some other
     * invalid aspect of the arguments supplied to the proxy
     * class creation (such as virtual machine limitations
     * exceeded).
     */
    throw new IllegalArgumentException(e.toString());
}

private byte[] generateClassFile() {
    this.addProxyMethod(hashCodeMethod, Object.class);
    this.addProxyMethod(equalsMethod, Object.class);
    this.addProxyMethod(toStringMethod, Object.class);
    Class[] var1 = this.interfaces;
    int var2 = var1.length;

    int var3;
    Class var4;
    for(var3 = 0; var3 < var2; ++var3) {
        var4 = var1[var3];
        Method[] var5 = var4.getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method var8 = var5[var7];
            this.addProxyMethod(var8, var4);
        }
    }

    this.methods.add(this.generateConstructor());
...
 }
 //生成一個帶invocationhandler引數的構造方法
private ProxyGenerator.MethodInfo generateConstructor() throws IOException {
    ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
    DataOutputStream var2 = new DataOutputStream(var1.code);
    this.code_aload(0, var2);
    this.code_aload(1, var2);
    var2.writeByte(183);
    var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));
    var2.writeByte(177);
    var1.maxStack = 10;
    var1.maxLocals = 2;
    var1.declaredExceptions = new short[0];
    return var1;
}
複製程式碼

上面的流程可以簡單歸納為

  1. 增加hashcode,equals,toString方法;
  2. 增加所有介面中宣告的未實現方法;
  3. 增加一個方法引數為;java/lang/reflect/InvocationHandler的構造方法
  4. 其他靜態初始化資料。

動態代理的應用

  1. spring-aop

    spring aop預設基於jdk動態代理來實現,我們來看下下面這個經典的面試問題

    一個類裡面,兩個方法A和方法B,方法B上有加註解做事物增強,那麼A呼叫this.B為什麼沒有事物效果?

    因為spring-aop預設基於jdk的動態代理實現,最終執行是通過生成的代理物件的,而代理物件執行A方法和B方法其實是呼叫的InvocationHandler裡面的增強後的方法,其中B方法是經過InvocationHandler做增強在方法前後增加了事物開啟和提交的程式碼,而真正執行程式碼是通過methodB.invoke(原始物件) 而A方法的實現內部雖然包含了this.B方法 但其實是呼叫了methodA.invoke(原始物件),而這一句程式碼相當於呼叫的是原始物件的methodA方法,而這裡面的this.B()方法其實是呼叫的原始物件的B方法,沒有進行過事物增強,而如果是通過cglib做位元組碼增強,生成這個類的子類,這種呼叫this.B方法是有事物效果的。

深入理解Java反射

  1. rpc consumer

    有過RMI開發經驗的人可能會很熟悉,為什麼在對外export rmi服務的時候會分別在client和server生成兩個stub檔案,其中client的檔案其實就是用動態代理生成了一個代理類 這個代理類,實現了所要對外提供服務的所有介面,每個方法的實現其實就是將介面資訊,方法宣告,引數,返回值資訊通過網路發給服務端,而服務端收到請求後通過找到對應的實現然後用反射method.invoke進行呼叫,然後將結果返回給客戶端

    其實其他的RPC框架的實現方式大致和這個類似,只是客戶端的代理類,可能不僅要將方法宣告通過網路傳輸給服務提供方,也可以做一下服務路由,負載均衡,以及傳輸一些額外的attachment資料給provider

深入理解Java反射

作者簡介

小強,銅板街資金端後臺開發工程師,2015年6月加入銅板街。目前負責銅板街資金端清結算相關的開發。

深入理解Java反射

更多精彩內容,請掃碼關注 “銅板街技術” 微信公眾號。

相關文章