对JAVA反射机制的认识

来源:互联网 发布:微博php笔试 编辑:程序博客网 时间:2024/05/05 14:36

  最近在看spring的源代码,发现其基本原理如依赖注入、aop、以及xml技术,还有hibernate、javabean等都运用了java的动态反射的机制。一直觉得都在用,却“不识庐山真面目”,所以对此做了总结。

1.      概述

Reflection(反射) 是Java被视为动态语言的一个关键性质。这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,允许运行中的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。先来解释一下反射,反射是一个生涩的名词,通俗的说反射就是根据给出的类名(字符串)、方法名、属性等信息来生成对象、方法、属性。这种编程方式可以让对象在生成时才决定要生成哪一种对象。反射是一种强大的工具,Java 的这一能力在实际应用中也许用得不是很多,但是在其它的程序设计语言中根本就不存在这一特性。例如,Pascal、C 或者 C++ 中就没有办法在程序中获得函数定义相关的信息。它使您能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代表链接。但反射的某些方面存在一些疑问。在本文中,我将深入讨论反射的利弊。在了解了权衡性分析之后,您可以自行决定是否利大于弊。

 

2.      原理

在展示java的jvm提供的这一反射原理之前,你首先要了解几个重要的类:java.lang.Class,以及java.lang.reflect中的Method、Field、Constructor等classes。如果你还不太清楚,请先查阅一下java的api。

2.1.   基于类的反射

Class 对象为您提供接入类元数据的反射的所有基本hook。这类元数据包括关于类自身的信息,如包和类的父类,以及该类实施的接口。它还包括该类定义的构造函数、字段和方法的详细信息。这些最后的项目都是编程中最经常使用的项目,因此我将在本小节的稍后部分给出一些与它们协作的实例。

对于以下三类组件中的任何一类来说 -- 构造函数、字段和方法 --java.lang.Class 提供四种独立的反射调用,以不同的方式来获得信息。调用都遵循一种标准格式。以下是用于查找构造函数的一组反射调用:

  • Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的公共构造函数,
  • Constructor[] getConstructors() -- 获得类的所有公共构造函数
  • Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(与接入级别无关)
  • Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关)

每类这些调用都返回一个或多个 java.lang.reflect.Constructor 函数。这种 Constructor 类定义 newInstance 方法,它采用一组对象作为其唯一的参数,然后返回新创建的原始类实例。该组对象是用于构造函数调用的参数值。作为解释这一工作流程的实例,假设您有一个 TwoString 类和一个使用一对 String s的构造函数,如清单1所示:


清单1:从一对字符串创建的类

public class TwoString {

    private String m_s1, m_s2;

    public TwoString(String s1, String s2) {

        m_s1 = s1;

        m_s2 = s2;

    }

}

 

清单2中的代码获得构造函数并使用它来创建使用 String s "a" 和 "b" 的 TwoString 类的一个实例:


清单2:构造函数的反射调用

    Class[] types = new Class[] { String.class, String.class };

    Constructor cons = TwoString.class.getConstructor(types);

    Object[] args = new Object[] { "a", "b" };

    TwoString ts = cons.newInstance(args);

 

清单2中的代码忽略了不同反射方法抛出的多种可能选中的例外类型。例外在 Javadoc API 描述中详细记录,因此为了简明起见,我将在所有程序实例中忽略它们。

尽管我在讨论构造函数主题,Java编程语言还定义了一种您可以用来使用 无参数(或缺省)构造函数创建类的一个实例的特殊快捷方式。这种快捷方式嵌入到 Class 定义中,如下:

Object newInstance() -- 使用缺省函数创建新的实例

即使这种方法只允许您使用一种特殊的构造函数,如果这正是您需要的,那么它将提供一种非常方便的快捷方式。当与JavaBeans协作时这项技术尤其有用,JavaBeans需要定义公共、无参数构造函数。

2.2.   基于字段的反射

字段,可以理解为类的属性。 获得字段信息的 Class 反射调用不同于那些用于接入构造函数的调用,在参数类型数组中使用了字段名:

  • Field getField(String name) -- 获得命名的公共字段
  • Field[] getFields() -- 获得类的所有公共字段
  • Field getDeclaredField(String name) -- 获得类声明的命名的字段
  • Field[] getDeclaredFields() -- 获得类声明的所有字段

尽管与构造函数调用类似,在字段方面仍存在一个重要的区别:前两个变量返回可以通过类接入的公共字段的信息 -- 即使它们来自于祖先类。后两个变量返回类直接声明的字段的信息 -- 与字段的接入类型无关。调用返回的 java.lang.reflect.Field 实例定义所有主类型的 getXXX 和 setXXX 方法,以及与对象引用协作的通用 get 和 set 方法。您可以根据实际的字段类型自行选择一种适当的方法,而getXXX 方法将自动处理扩展转换(如使用 getInt 方法来检索一个字节值)。

清单3显示使用字段反射方法的一个实例,以方法的格式根据名称增加对象的 int 字段 :


清单3:通过反射增加一个字段

public int incrementField(String name, Object obj) throws... {

    Field field = obj.getClass().getDeclaredField(name);

    int value = field.getInt(obj) + 1;

    field.setInt(obj, value);

    return value;

}

 

这种方法开始展示了反射带来的某些灵活性。与特定的类协作不同, incrementField 使用传 入的对象的 getClass 方法来查找类信息,然后直接在该类中查找命名的字段。

2.3.   基于方法的反射

获得方法信息的 Class 反射调用与用于构造函数和字段的调用非常类似:

  • Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法
  • Method[] getMethods() -- 获得类的所有公共方法
  • Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法
  • Method[] getDeclaredMethods() -- 获得类声明的所有方法

与字段调用一样,前两个变量返回可以通过类接入的公共方法的信息 -- 即使它们来自于祖先类。后两个变量返回类声明的方法的信息,与方法的接入类型无关。调用返回的 java.lang.reflect.Method 实例定义一种 invoke 方法,您可以用来在正在定义的类的一个实例上调用方法。这种 invoke 方法使用两个参数,为调用提供类实例和参数值数组。

清单4进一步阐述字段实例,显示反射正在运行的方法的一个实例。这种方法增加一个定义有 get 和 set 方法的 int JavaBean属性。例如,如果对象为一个整数 count 值定义了 getCount 和 setCount 方法,您可以在一次调用中向该方法传递“count”作为 name 参数,以增加该值。


清单4:通过反射增加一个JavaBean 属性

public int incrementProperty(String name, Object obj) {

    String prop = Character.toUpperCase(name.charAt(0)) +

        name.substring(1);

    String mname = "get" + prop;

    Class[] types = new Class[] {};

    Method method = obj.getClass().getMethod(mname, types);

    Object result = method.invoke(obj, new Object[0]);

    int value = ((Integer)result).intValue() + 1;

    mname = "set" + prop;

    types = new Class[] { int.class };

    method = obj.getClass().getMethod(mname, types);

    method.invoke(obj, new Object[] { new Integer(value) });

    return value;

}

 

为了遵循JavaBeans惯例,我把属性名的首字母改为大写,然后预先考虑 get 来创建读方法名, set 来创建写方法名。JavaBeans读方法仅返回值,而写方法使用值作为唯一的参数,因此我规定方法的参数类型以进行匹配。最后,该惯例要求方法为公共,因此我使用查找格式,查找类上可调用的公共方法。

这一实例是第一个我使用反射传递主值的实例,因此现在我们来看看它是如何工作的。基本原理很简单:无论什么时候您需要传递主值,只需用相应封装类的一个实例(在 java.lang 包中定义)来替换该类主值。这可以应用于调用和返回。因此,当我在实例中调用 get 方法时,我预计结果为实际 int 属性值的 java.lang.Integer 封装。

2.4.   数组的反射

       数组是Java编程语言中的对象。与所有对象一样,它们都有类。如果您有一个数组,使用标准 getClass 方法,您可以获得该数组的类,就象任何其它对象一样。但是, 不通过现有的实例来获得类不同于其它类型的对象。即使您有一个数组类,您也不能直接对它进行太多的操作 -- 反射为标准类提供的构造函数接入不能用于数组,而且数组没有任何可接入的字段,只有基本的 java.lang.Object 方法定义用于数组对象。数组的特殊处理使用 java.lang.reflect.Array 类提供的静态方法的集合。该类中的方法使您能够创建新数组,获得数组对象的长度,读和写数组对象的索引值。

清单5显示了一种重新调整现有数组大小的有效方法。它使用反射来创建相同类型的新数组,然后在返回新数组之前,在老数组中复制所有数据。


清单 5:通过反射来扩展一个数组

public Object growArray(Object array, int size) {

    Class type = array.getClass().getComponentType();

    Object grown = Array.newInstance(type, size);

    System.arraycopy(array, 0, grown, 0,

        Math.min(Array.getLength(array), size));

    return grown;

}

 

3.      实例中的经典运用

1.    得到某个对象的属性

public Object getProperty(Object owner, String fieldName) throws Exception {      

    Class ownerClass = owner.getClass();                                          

                                                                                  

    Field field = ownerClass.getField(fieldName);                                 

                                                                                  

    Object property = field.get(owner);                                           

                                                                                   

    return property;                                                              

}                                                                                  

Class ownerClass = owner.getClass():得到该对象的Class。

Field field = ownerClass.getField(fieldName):通过Class得到类声明的属性。

Object property = field.get(owner):通过对象得到该属性的实例,如果这个属性是非公有的,这里会报IllegalAccessException。

2. 得到某个类的静态属性

public Object getStaticProperty(String className, String fieldName)    

            throws Exception {                                         

    Class ownerClass = Class.forName(className);                       

                                                                       

    Field field = ownerClass.getField(fieldName);                       

                                                                       

    Object property = field.get(ownerClass);                           

                                                                       

    return property;                                                   

}                                                                       

Class ownerClass = Class.forName(className) :首先得到这个类的Class。

Field field = ownerClass.getField(fieldName):和上面一样,通过Class得到类声明的属性。

Object property = field.get(ownerClass) :这里和上面有些不同,因为该属性是静态的,所以直接从类的Class里取。

3. 执行某对象的方法

public Object invokeMethod(Object owner, String methodName, Object[] args) throws Exception {   

    Class ownerClass = owner.getClass();   

    Class[] argsClass = new Class[args.length];   

    for (int i = 0, j = args.length; i < j; i++) {   

        argsClass[i] = args[i].getClass();   

    }    

    Method method = ownerClass.getMethod(methodName, argsClass);      

    return method.invoke(owner, args);     

}  

Class owner_class = owner.getClass() :首先还是必须得到这个对象的Class。

3~6行:配置参数的Class数组,作为寻找Method的条件。

Method method = ownerClass.getMethod(methodName, argsClass):通过Method名和参数的Class数组得到要执行的Method。

method.invoke(owner, args):执行该Method,invoke方法的参数是执行这个方法的对象,和参数数组。返回值是Object,也既是该方法的返回值。

4. 执行某个类的静态方法

 

public Object invokeStaticMethod(String className, String methodName,            

            Object[] args) throws Exception {                                    

    Class ownerClass = Class.forName(className);                                 

    Class[] argsClass = new Class[args.length];                                  

    for (int i = 0, j = args.length; i < j; i++) {                               

        argsClass[i] = args[i].getClass();                                        

    }                                                                           

    Method method = ownerClass.getMethod(methodName, argsClass);                 

    return method.invoke(null, args);                                             

}                                                                                 

基本的原理和实例3相同,不同点是最后一行,invoke的一个参数是null,因为这是静态方法,不需要借助实例运行。

5. 新建实例

public Object newInstance(String className, Object[] args) throws Exception {    

    Class newoneClass = Class.forName(className);                                                                                                   

    Class[] argsClass = new Class[args.length];                                  

    for (int i = 0, j = args.length; i < j; i++) {                               

        argsClass[i] = args[i].getClass();                                       

    }                                                                            

    Constructor cons = newoneClass.getConstructor(argsClass);                    

    return cons.newInstance(args);                                               

}                                                                                 

这里说的方法是执行带参数的构造函数来新建实例的方法。如果不需要参数,可以直接使用newoneClass.newInstance()来实现。

Class newoneClass = Class.forName(className):第一步,得到要构造的实例的Class。

第6~第10行:得到参数的Class数组。

Constructor cons = newoneClass.getConstructor(argsClass):得到构造子。

cons.newInstance(args):新建实例。

6. 判断是否为某个类的实例

public boolean isInstance(Object obj, Class cls) {        

    return cls.isInstance(obj);                           

}                                                          

7. 得到数组中的某个元素

public Object getByArray(Object array, int index) {       

    return Array.get(array,index);                        

}                                

4.      性能问题和缺点

反射是一种强大的工具,但也存在一些不足。一个主要的缺点是对性能有影响。使用反射基本上是一种解释操作,您可以告诉JVM您希望做什么并且它满足您的要求。这类操作总是慢于只直接执行相同的操作。为了阐述使用反射的性能成本,我为本文准备了一组基准程序。

清单6是字段接入性能测试的一个摘用,包括基本的测试方法。每种方法测试字段接入的一种形式 -- accessSame 与同一对象的成员字段协作, accessOther 使用可直接接入的另一对象的字段, accessReflection 使用可通过反射接入的另一对象的字段。在每种情况下,方法执行相同的计算 -- 循环中简单的加/乘顺序。


清单 6:字段接入性能测试代码

public int accessSame(int loops) {

    m_value = 0;

    for (int index = 0; index < loops; index++) {

        m_value = (m_value + ADDITIVE_VALUE) *

            MULTIPLIER_VALUE;

    }

    return m_value;

}

public int accessReference(int loops) {

    TimingClass timing = new TimingClass();

    for (int index = 0; index < loops; index++) {

        timing.m_value = (timing.m_value + ADDITIVE_VALUE) *

            MULTIPLIER_VALUE;

    }

    return timing.m_value;

}

public int accessReflection(int loops) throws Exception {

    TimingClass timing = new TimingClass();

    try {

        Field field = TimingClass.class.

            getDeclaredField("m_value");

        for (int index = 0; index < loops; index++) {

            int value = (field.getInt(timing) +

                ADDITIVE_VALUE) * MULTIPLIER_VALUE;

            field.setInt(timing, value);

        }

        return timing.m_value;

    } catch (Exception ex) {

        System.out.println("Error using reflection");

        throw ex;

    }

}

 

测试程序重复调用每种方法,使用一个大循环数,从而平均多次调用的时间衡量结果。平均值中不包括每种方法第一次调用的时间,因此初始化时间不是结果中的一个因素。在为本文进行的测试中,每次调用时我使用1000万的循环数,在1GHz PIIIm系统上运行。三个不同Linux JVM的计时结果如图1所示。所有测试使用每个JVM的缺省设置。


 1:字段接入时间 

上表的对数尺度可以显示所有时间,但减少了差异看得见的影响。在前两副图中(Sun JVM),使用反射的执行时间超过使用直接接入的1000倍以上。通过比较,IBM JVM可能稍好一些,但反射方法仍旧需要比其它方法长700倍以上的时间。任何JVM上其它两种方法之间时间方面无任何显著差异,但IBM JVM几乎比Sun JVM快一倍。最有可能的是这种差异反映了Sun Hot Spot JVM的专业优化,它在简单基准方面表现得很糟糕。

    反射有两个缺点。第一个是性能问题。当用于字段和方法接入时反射要远慢于直接代码。性能问题的程度取决于程序中是如何使用反射的。如果它作为程序运行中相对很少涉及的部分,缓慢的性能将不会是一个问题。即使测试中最坏情况下的计时图显示的反射操作只耗用几微秒。仅反射在性能关键的应用的核心逻辑中使用时性能问题才变得至关重要。

许多应用更严重的一个缺点是使用反射会模糊程序内部实际要发生的事情。程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术会带来维护问题。反射代码比相应的直接代码更复杂,正如性能比较的代码实例中看到的一样。解决这些问题的最佳方案是保守地使用反射-- 仅在它可以真正增加灵活性的地方 -- 记录其在目标类中的使用。

5.      参考文档

http://www.ibm.com/developerworks/library/j-dyn0603/index.html?S_TACT=105AGX52&S_CMP=cn-a-j  《Java编程 的动态性,第 2部分引入反射 》 Dennis Sosnoski

http://orangewhy.javaeye.com/blog/56011 《Java Reflection (JAVA反射)  》  orangewhy

原创粉丝点击