Java反射机制

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java反射机制相关的知识,希望对你有一定的参考价值。

1 类的加载

在学习反射机制之前,我们需要简单理解一下类的加载过程,这有助于我们更好的理解反射原理。

1.1类加载器

负责将类的字节码.class文件加载到内存,并为之生成对应的Class对象。利用类加载器,我们可以实现动态加载。

类加载器的组成与作用

  • 1.Bootstrap ClassLoader : 引导类加载器

    也被称为引导类加载器,负责Java核心类的加载,如System,String等。负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,非ClassLoader子类。
    
  • 2.Extension ClassLoader : 扩展类加载器

    负责JRE的扩展目录中jar包的加载。负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre\lib\ext\*.jar或-Djava.ext.dirs指定目录下的jar包。
    
  • 3.Sysetm ClassLoader : 系统类加载器(应用类加载器)

    负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径
    
  • 4.Custom ClassLoader : 自定义ClassLoader

    应用程序根据自身需要自定义的ClassLoader。加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。
    加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
    

1.2 类的加载过程

当程序要使用某个类时,若该类还未被加载到内存中,则系统会通过装载(Load),链接(Link)和初始化(Initialize)来实现对该类进行初始化,其中链接又包含验证、准备、解析过程。它们的作用如下列出:

  • 1.装载:把Java字节代码转换成JVM中的java.lang.Class类的对象。

    查找并加载类的二进制数据。将class文件读入内存,并为之创建一个Class对象。任何类被使用时系统都会建立一个Class对象。在装载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,并在 Java 堆中也创建一个 java.lang.Class 类的对象,这样便可以通过该对象访问方法区中的这些数据。
    

    加载该类的字节码.class文件的途径有多种:

    • 从本地文件系统加载的class文件
    • 从jar包加载class文件
    • 从网络加载class文件
    • 动态编译一个Java源文件,并执行加载
  • 2.链接 : Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。

    • 验证 是否有正确的内部结构,并和其他类协调一致
    • 准备 负责为类的静态成员分配内存,并设置默认初始化值
    • 解析 将类的二进制数据中的符号引用替换为直接引用
  • 3.初始化 : 执行类的静态代码块和初始化静态域

    当一个Java类第一次被真正使用到的时候,JVM会进行该类的初始化操作。初始化过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。
    

1.3 类的初始化时机

在第一次主动使用类时会触发它的初始化(装载、链接在此之前完成),如下大概列出了的常见时机:

  • 创建类的实例,即new一个对象时
  • 调用类的静态方法
  • 访问某个类或接口的静态变量,或者对该静态变量赋值(静态常量不会触发此过程)
  • 初始化一个类的子类(会首先初始化子类的父类)
  • 使用反射的某些方法(如Class.forName)
  • JVM启动时标明的启动类,即文件名和类名相同的那个类或直接使用java.exe命令来运行某个主类

1.4 类的生命周期图

技术分享

2 反射

我们要解剖一个类,必须先要获取到该类的字节码文件对象,解剖使用的就是该类对应的Class类。在上面类初始化完毕后,该类对应的Class对象已经生成了。

通常,我们是通过类直接创建一个对象实例,然后通过该对象来调用类的方法、变量等。现在我们要反其道而行,我们通过该类对应的Class对象来获取它的构造、方法、字段等所对应的反射类(Constructor类、Method类、Field类等),来间接操作类,动态获取类信息,这就是反射的目的。

反射正是运用此Class对象来获取该类的信息的,包括类的构造器、方法、字段、修饰符等。

那什么是Class呢?

Class是一个泛型类,它的的实例表示正在运行的 Java 应用程序中的类和接口。

Java运行时系统对所有的对象进行运行时类型标识,CLass类就是用来保存这些信息的,这项信息纪录了每个对象所属的类。每个类型都对应着一个Class实例,当装载类时,Class类型的对象会被创建。

Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机 以及通过调用 类加载器 中的 defineClass 方法自动构造的

2.1 反射概述

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制

要使用反射,如下多个类是需要我们了解的,它们在java.lang.reflect 包下:

  • Class类:代表一个类(我们需要反射的类所对应的Class对象的抽象)
  • Constructor类:代表类的构造方法
  • Method类:代表类的方法。
  • Field类:代表类的字段
  • Array类 : 提供了动态创建和访问 Java 数组的方法。
  • Modifier类:提供了 static 方法和常量,对类和成员访问修饰符进行解码
  • 等等

当然了,Class类是反射的核心类,我们是通过剖析它,加上其他类的辅助,来动态获取类的各项信息。还有更多类可以查看java.lang.reflect 包。

2.2 使用反射能获取什么?

使用 Java 反射机制可以在运行时期获得类的信息,能获得关于该类的哪些信息是我们关心的。通过反射,我们可以获取类信息的内容以及对应的方法如下列出:
有的方法可能有多个,只例举了一个。

  • 该类对应的Class对象 ———— forName(String className) (多个方法)
  • 构造器 —————————— getConstructors() (多个方法)
  • 方法 ——————– getMethods() (多个方法)
  • 字段 ——————– getFields() (多个方法)
  • 修饰符 —————– getModifiers()
  • 类名 ——————– getCanonicalName()
  • 包信息 —————– getPackage()
  • 父类 ——————– getSuperclass() 返回Class对象
  • 实现的接口 ————getInterfaces() , 返回Class对象数组
  • 类的类加载器 ———getClassLoader()
  • 注解 ——————– getAnnotation(Class annotationClass) (多个方法)
  • 等等

下面我们看看它们的部分获取方式。

2.3 Class对象的获取方式

上面我们说过,要解剖一个类,需要它对于的Class对象,因此获取Class对象是使用反射的首要任务。一般我们有三种方式来获取Class对象

  • 1、通过类的.class静态属性来获得Class对象
  • 2、通过实例对象的getClass来获取Class对象
  • 3、通过Class.forName(dir)来获取Class对象, 注意路径要为全路径

第三种较为常用,但我们分不同的场合可作相应的取舍。

如下看三种方式的示例:

1 通过类的.class静态属性来获得Class对象,这种方法在你知道类名的时候适用

private static void method1() throws InstantiationException,
        IllegalAccessException {
    Class<?> clazz = Info.class; // 获取Class对象
    Info newInstance = (Info) clazz.newInstance(); // 通过clazz对象创建类的实例
    newInstance.setName("niko");  
    newInstance.setAdress("zero space");  // 调用方法
    newInstance.setId(0);   
    System.out.println(newInstance.toString());
}

2 通过getClass来获取Class对象,这种方法你需要知道类的实例对象

private static void method2() throws InstantiationException,
        IllegalAccessException {
    Info info = new Info();
    Class<? extends Info> clazz = info.getClass(); // 通过getClass获取Class对象
    Info newInstance = clazz.newInstance();
    newInstance.setName("pecuyu");
    newInstance.setAdress("my world");
    newInstance.setId(1);
    System.out.println(newInstance.toString());
}

3 通过Class.forName(dir)来获取Class对象, 注意路径要为全路径

private static void method3() throws ClassNotFoundException,
        InstantiationException, IllegalAccessException {
    // 类的路径为全路径
    Class<?> clazz = Class.forName("com.yu.reflect.Info");
    Info newInstance = (Info) clazz.newInstance();
    newInstance.setName("forName");
    newInstance.setAdress("全路径");
    newInstance.setId(2);
    System.out.println(newInstance.toString());
}

在获取了Class对象后,就可以用它来获得类的构造、方法、字段、修饰符等等,下面我们进行探究。

2.4 获取类的构造器

构造器是我们创建对象实例所需的。因此我们首先来获得它。在获取构造器对象后,我们可以使用它的newInstance方法来创建类的实例对象。有了对象实例我们就可以来操作类的方法与字段了。
在Class类中提供了如下四种方法来获取构造器,它们的返回值是Constructor类型的对象或数组:

  • ① getConstructors

    返回一个包含某些 Constructor对象的数组,这些对象反映此 Class对象所表示的类的所有公共构造方法

  • ② getConstructor

    返回一个 Constructor对象,它反映此 Class对象所表示的类的指定公共构造方法

  • ③ declaredConstructors

    返回 Constructor 对象的一个数组,这些对象反映此 Class 对象表示的类声明的所有构造方法,包含私有

  • ④ getDeclaredConstructor

    返回一个 Constructor对象,该对象反映此 Class对象所表示的类或接口的指定构造方法,包含私有

注意事项:如果获取的是私有构造,而你需要通过它来创建类实例,那么需设置访问权限(setAccessible)为true

使用示例:
1 getDeclaredConstructors 获取反射类所有已声明的构造器,包括私有

private static void method3() throws ClassNotFoundException {
    Class<?> clazz = Class.forName("com.yu.reflect.Info");
    Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
    for (Constructor<?> constructor : declaredConstructors) {
        System.out.println(constructor);
    }

}

2 getDeclaredConstructor 获取已声明的指定参数类型的构造器,包含私有构造器

对于私有构造器,要使用需设置访问权限(setAccessible)为true

    private static void method4() throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        // 获得Class对象
        Class<?> clazz = Class.forName("com.yu.reflect.Info");
        // 通过clazz获取指定参数的构造器
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(int.class,String.class,String.class);
        System.out.println(declaredConstructor);
        // 设置私有构造器的访问权限,共有构造器的使用可省略此步
        declaredConstructor.setAccessible(true);
        // 通过构造器创建Info对象实例
        Info newInstance = (Info) declaredConstructor.newInstance(6,"qy","beautiful world");
        System.out.println(newInstance);
    }

另外两个使用方式类似,不贴出示例了。

2.5 获取方法

在获取构造器并通过newInstance方法创建类的实例后,我们可以来操作类的方法了。获取方法(Method)对象后,我们可以调用它的invoke方法进行类的方法调用

在Class类中提供了如下方法来获取类的方法

  • getMethods ( )

    返回所有的共有方法,包括父类方法

  • getMethod(name, parameterTypes)

    获取指定参数类型的共有方法

  • getDeclaredMethods( )

    获取所有已声明的方法,返回包含所有方法的数组对象

  • getDeclaredMethod(name, parameterTypes)

    获得已声明的方法,包括私有方法都可获取

注意事项:对已获取的私有方法调用前,需设置访问权限(setAccessible)为true

使用示例:
1 getDeclaredMethods获取所有已声明的方法,返回包含所有方法的数组对象

/**
     * 
     */
    private static void method3() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        // 获取Class对象
        Class<?> clazz = Class.forName("com.yu.reflect.Info");
        // 创建Info类实例
        Info newInstance = (Info) clazz.newInstance();
        // 获取包含所有方法的数组
        Method[] declaredMethods = clazz.getDeclaredMethods();

        for (Method method : declaredMethods) {
            System.out.println(method);
        }
    }

2 getDeclaredMethod 获得已声明的方法,通过Method对象的invoke方法来调用类的方法。

/**
     * 
     */
    private static void method4() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        // 获取Class对象
        Class<?> clazz = Class.forName("com.yu.reflect.Info");
        // 创建Info类实例
        Info newInstance = (Info) clazz.newInstance();
        // 获取已声明的方法
        Method declaredMethod = clazz.getDeclaredMethod("privateMethod", String.class);
        // 去掉安全检查,获得访问权
        declaredMethod.setAccessible(true);
        // 调用方法
        declaredMethod.invoke(newInstance, "do you know"); // 近似等价于newInstance.privateMethod("do you know"); 只不过privateMethod是一个私有方法,无法这样直接调用
    }

2.6 获取字段

仍然是操作类对应的Class对象,在Class类中提供了如下方法来获取字段:

  • getFields

    获取所有的共有字段

  • getField

    获取指定名称的共有字段

  • getDeclaredFields

    获取所有已声明的方法

  • getDeclaredField

    获取指定名称的已声明的字段

使用示例:
1 getDeclaredFields 获取所有已声明的方法

private static void method3() throws ClassNotFoundException {
    Class<?> clazz =Class.forName("com.yu.reflect.Info");
    // 获取所有已声明的字段
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field field : declaredFields) {
        System.out.println(field);
    }
}

2 getDeclaredField获取指定的已声明的字段

private static void method4() throws IllegalArgumentException, IllegalAccessException,NoSuchFieldException, SecurityException, InstantiationException, ClassNotFoundException {
    // 获取Class对象
    Class<?> clazz =Class.forName("com.yu.reflect.Info");
    // 创建Info实例
    Info newInstance = (Info) clazz.newInstance();
    // 获取已声明的字段
    Field declaredField = clazz.getDeclaredField("name");
    // 去掉安全检查
    declaredField.setAccessible(true);
    // 设置字段值
    declaredField.set(newInstance, "fly"); // 相当于 newInstance.setName("fly");
    System.out.println(newInstance);
}

2.7 获取其他信息

我们还可以获取一些其他的信息,大致列了如下几项:

  • ①返回 Class对象所对应的实体名 getName/getCanonicalName,包含包名,只需获取类名可以使用getSimpleName
  • ②返回父类所对应的Class对象 getSuperclass
  • ③ getModifiers 返回类对应的修饰符的int常量值,需用Modifier.toString(modifier)来解析为字符串
  • ④ 获取注解 getAnnotation
  • ⑤ 获取包信息 getPackage

更多内容请参考api文档

使用示例:

private static void getOthers() throws ClassNotFoundException {
        Class<?> clazz = Class.forName("com.yu.reflect.Info"); 
//      String canonicalName = clazz.getName(); // 返回该类的名称,包含包名
        String canonicalName = clazz.getSimpleName(); // 返回该类的名称
        System.out.println(canonicalName);

        Class<?> superclass = clazz.getSuperclass();  // 获取父类对应的Class对象
        System.out.println(superclass.getCanonicalName()); // 获取父类名
        int modifier = clazz.getModifiers();  // 获得权限修饰符 int
        System.out.println(Modifier.toString(modifier));// 获得权限修饰符 String

        Annotation[] annotations = clazz.getDeclaredAnnotations(); // 获取所有注解 
        XmlElement annotation = clazz.getAnnotation(XmlElement.class); // 指定的
        System.out.println(annotation);
//      for (Annotation annotation : annotations) {
//          System.out.println(annotation);
//      }

        Package packageInfo = clazz.getPackage(); // 获取包信息
        System.out.println(packageInfo.getName());
    }

注:上面getModifiers获取的是修饰符对应的常量值,通过Modifier.toString(modifier)来转换成String,对应关系如下

修饰符 常量值
public static final int ABSTRACT 1024
public static final int FINAL 16
public static final int INTERFACE 512
public static final int NATIVE 256
public static final int PRIVATE 2
public static final int PROTECTED 4
public static final int PUBLIC 1
public static final int STATIC 8
public static final int STRICT 2048
public static final int SYNCHRONIZED 32
public static final int TRANSIENT 128
public static final int VOLATILE 64

2.8 使用Array创建数组

Array 类提供了动态创建和访问 Java 数组的方法。

Array 允许在执行 get 或 set 操作期间进行扩展转换,但如果发生收缩转换,则抛出 IllegalArgumentException。

两个构造方法如下,还有一系列的set与get方法:

static Object newInstance(Class<?> componentType, int... dimensions) 
          创建一个具有指定的组件类型和维度的新数组。 
static Object newInstance(Class<?> componentType, int length) 
          创建一个具有指定的组件类型和长度的新数组。 

2.81 创建一维数组

使用newInstance(componentType, length) 构造来创建一维数组

private static void one() {
    Object array = Array.newInstance(int.class, 5);
    Array.setInt(array, 0, 5); 
    Array.setInt(array, 1, 4);
    Array.setInt(array, 2, 3);
    Array.setInt(array, 3, 2);
    Array.setInt(array, 4, 1);  // 设置array数组指定索引为4的值

    System.out.println(Array.getLength(array)); // 获取数组长度
    System.out.println(Array.getInt(array, 2)); // 获取指定位置的元素
}

2.82 创建多维数组

创建二维数组举例:


private static void two() {
    Object newInstance = Array.newInstance(String.class, 2, 3); // 创建一个2*3的二维数组
    Object first = Array.get(newInstance, 0); // 二位数组的第0行对应的一维数组
    Array.set(first, 0, "one"); // 设置(0,0)
    Array.set(first, 1, "two");  // 设置(0,1)
    Array.set(first, 2, "three");  // 设置(0,2)

    Object second = Array.get(newInstance, 2); // 二位数组的第1行对应的一维数组
    Array.set(second, 0, "four");// 设置(1,0)
    Array.set(second, 1, "five");// 设置(1,1)
    Array.set(second, 2, "six"); // 设置(1,2)

    String[][] str = (String[][]) newInstance; // 强转为一个二维数组
    for (String[] strings : str) {
        for (String string : strings) {
            System.out.println(string);
        }
    }
// System.out.println(Array.getLength(newInstance));
}

三维数组的创建原理类似,有兴趣的可以下载源码看看。再多维数的数组用的不太多了。

2.93 动态扩展数组的一个办法

扩展数组方法

/**
 * @param array  要扩大的数组
 * @param size    要扩大的大小
 * @return
 */
public static Object growArray(Object array, int size) {
    if (Array.getLength(array) >= size) { // 指定要拓展的大小小于原数组的长度
        return array;
    }
    Class<?> type = array.getClass().getComponentType(); // 获取array的类型
    Object growArray = Array.newInstance(type, size); // 根据大小来创建新数组
    System.arraycopy(array, 0, growArray, 0, Array.getLength(array));// 将原数组的内容拷贝到新数组
    return growArray;
}

注:arraycopy函数是System类的一个静态函数,用于将一个数组的内容拷贝到另一个数组。

函数原型 public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。

源数组中位置在 srcPos 到 srcPos+length-1 之间的组件被分别复制到目标数组中的 destPos 到 destPos+length-1 位置。

参数:

  • src - 源数组。
  • srcPos - 源数组中的起始位置。
  • dest - 目标数组。
  • destPos - 目标数据中的起始位置。
  • length - 要复制的数组元素的数量。

扩展数组的使用示例:

    private static void extend() {
        String[] s = new String[] { "niko", "pecuyu", "qy", "lj" };
        String[] growArray = (String[]) growArray(s, 6);
        for (String string : growArray) {
            System.out.print(string + "  ");
        }
    }

2.9 反射的使用实例

2.9.1反射获取ArrayList的方法removeRange方法

ArrayList的方法removeRange,删除指定范围的元素

protected void removeRange(int fromIndex, int toIndex) 

我们想用它,但是它的访问权限是protected,我们不能直接访问它,所以我们可以通过反射来获取该方法。

    /**
     * 删除ArrayList指定范围的元素
     * @param list         目标ArrayList
     * @param fromIndex    开始索引,包含
     * @param toIndex      结束索引,不包含 
     */
    public static <T> void removeRange(ArrayList<T> list,int fromIndex, int toIndex) {
        try {
            // 1.获取CLass对象
            Class<?> clazz = Class.forName("java.util.ArrayList");
            // Class<?> clazz = list.getClass(); // 也可以
            // 2.获得removeRange方法,
            Method removeRangeMethod = clazz.getDeclaredMethod("removeRange", int.class,int.class);
            // 3.去掉安全检查,获得访问权限
            removeRangeMethod.setAccessible(true);
            // 4.调用removeRange方法
            removeRangeMethod.invoke(list, fromIndex,toIndex);
        } catch (NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

像这样获取因为权限问题而无法访问的方法有很多,诸如获取安卓手机的sd卡或内存路径问题,可以参考安卓学习笔记获取手机内存与SD卡路径及大小

2.9.2 反射HashMap

通过反射创建HashMap对象,并添加元素,取出元素打印

public class UseReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        // 获取HashMap对应的Class对象
        Class<?> clazz = Class.forName("java.util.HashMap");
        // 通过clazz对象创建实例
        Object mapInstance = clazz.newInstance();
        // 反射得到put(K key, V value)方法
        Method putMethod = clazz.getDeclaredMethod("put", Object.class,Object.class); //put(K key, V value)
        // 反射得到get(Object key)方法
        Method getMethod = clazz.getDeclaredMethod("get", Object.class); //get(Object key)
        // 通过putMethod添加值
        putMethod.invoke(mapInstance, "lj","qy");
        putMethod.invoke(mapInstance, "niko","kitty");
        putMethod.invoke(mapInstance, "sunning","hel");

        // 通过getMethod获取值
        Object result = getMethod.invoke(mapInstance, "lj");
        System.out.println("lj="+result);
    }
}

2.9.3 通过配置文件来动态加载类与方法

我们想要在不改代码的前提下,动态的实现某些功能时,就可以用反射,通过改配置文件来动态加载所需的类与方法。
使用示例:
主类,通过读取配置,用反射获得类与方法

public class UseReflectDemo3 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        // 模拟数据,写入配置文件
//      Properties properties = new Properties();
//      properties.setProperty("className", "com.yu.dynamic_test.C");
//      properties.setProperty("method", "doingSomething");
//      FileWriter writer=new FileWriter("prop.txt");
//      properties.store(writer, "new");

        // 读取配置文件
        Properties properties = new Properties();
        FileReader reader = new FileReader("prop.txt");
        properties.load(reader);
        reader.close();

        // 通过配置文件获取类名
        String className = properties.getProperty("className");
        // 通过配置文件获取方法名
        String method = properties.getProperty("method");
        // 通过className名反射获取Class对象
        Class<?> clazz = Class.forName(className);
        // 创建类的实例
        Object newInstance = clazz.newInstance();
        // 通过method名获取方法
        Method declaredMethod = clazz.getDeclaredMethod(method);
        // 调用方法
        declaredMethod.invoke(newInstance);
    }
}

配置文件如下:

#new
#Tue Oct 03 9:09:26 CST 2016
className=com.yu.dynamic_test.C
method=doingSomething

有三个测试类用于进行动态加载,com.yu.dynamic_test.A,com.yu.dynamic_test.B,com.yu.dynamic_test.C,代码就不贴出了。

3 反射的应用之动态代理

有了反射的基础,就能更好的理解动态代理了。Jdk为我们提供了一个类(Proxy)和一个接口(InvocationHandler)来实现动态代理。
先来了解下它们:

1 类 Proxy :

提供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。

Proxy 通过如下方法来创建代理实例

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

返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序,即将代理调用方法交给InvocationHandler来处理。

2 接口 InvocationHandler

代理实例的调用处理程序实现的接口。

每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的 invoke 方法。

public Object invoke(Object proxy, Method method, Object[] args) 
          在代理实例上处理方法调用并返回结果。 

上面两者的协调关系可以由如下叙述:

动态代理类是在运行时动态创建的并实现所有在其创建时指定的接口的类。 代理实例是代理类的一个实例, 每个代理实例都有一个关联的调用处理程序对象(即InvocationHandler实现类),通过其中一个代理接口的代理实例上的方法调用将被指派到实例的调用处理程序的 invoke 方法,并传递代理实例(proxy)、识别调用方法的Method 对象(method)以及包含参数的 Object 类型的数组(args),即上面invoke方法的三个参数。调用处理程序(InvocationHandler的实现类)以适当的方式处理编码的方法调用,并且它返回的结果将作为代理实例上方法调用的结果返回。

简短的说,就是代理类实现了其创建时传递的接口列表,同时在构建时关联了一个InvocationHandler实现类(以下称为h),代理类实例调用它的某个实现的接口中的方法时,会自动切换到 h 的invoke方法的调用,并给此方法传递了三个参数proxy, method, args),invoke返回值即是代理实例调用方法的返回值。

3 下面通过实例来分析
我们希望在不修改现有代码(开闭原则)的前提下,对数据库操作类的方法调用前后加上权限验证与写入日志操作,这该如何实现了?这个时候动态代理就发挥作用了。实现如下

由于在创建代理对象时需要传递接口列表,这就要求我们被代理的类必须实现某个接口。

如下定义的一个数据库操作接口

public interface DaoInterface {
    void insert();
    void find();
    void update();
    void delete();
}

实现类如下:

public class DaoInterfaceImpl implements DaoInterface {
    @Override
    public void insert() {
        System.out.println("插入数据");
    }

    @Override
    public void find() {
        System.out.println("查找数据");
    }

    @Override
    public void update() {
        System.out.println("更新数据");
    }

    @Override
    public void delete() {
        System.out.println("删除数据");
    }
}

创建代理类还需要关联一个InvocationHandler接口的实现类,如下实现:

public class CustomInvocationHandler implements InvocationHandler {
    private Object obj;  // 被代理的对象

    public CustomInvocationHandler(Object obj) {
        super();
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        CheckUtils utils=new CheckUtils();
        utils.checkAccess();  // 添加权限验证方法

        Object result = method.invoke(obj, args); // 调用被代理方法

        utils.writeLog();  // 添加写入日志方法
        return result;
    }

}

上面CheckUtils是一个工具类,主要是为了模拟在调用的方法前后添加一些操作。它的代码如下:

public class CheckUtils {
    public void checkAccess() {
        System.out.println("验证权限");
    }

    public void  writeLog() {
        System.out.println("写入访问日志");
    }
}

下面对代理类的封装,让我们更方便的获得代理实例。

class DynamicProxy {

    public static Object getProxyInstance(Object obj) {
        // 通过newProxyInstance方法来创建代理对象
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
                .getClass().getInterfaces(), new CustomInvocationHandler(obj));
    }
}

看主类实现:

public class AopDemo {
    public static void main(String[] args) {
        // 被代理的对象
        DaoInterfaceImpl impl=new DaoInterfaceImpl();
        // 获取代理对象,并把它强转为DaoInterface类型
        DaoInterface proxyInstance = (DaoInterface) DynamicProxy.getProxyInstance(impl);
        // 调用方法
        proxyInstance.insert(); // 此处的调用会引起CustomInvocationHandler类中的invoke方法的调用
        System.out.println("--------");
        proxyInstance.update();

    }
}

输出如下:

验证权限
插入数据
写入访问日志
--------
验证权限
更新数据
写入访问日志

到此,功能就已经完成了。在不改原有的代码的前提下,添加了验证权限和写入访问日志功能,动态代理确实很有帮助。但是被代理的对象必须至少有一个父接口,代理类已经继承了Proxy,而Java是单继承的,不能再用继承来实现代理,这是为什么必须要有实现的接口。但实际中的类,不是所有的类都实现了某个接口,因此对这些没有实现接口的类不做代理,这也是动态代理的一个不足之处。jdk提供的代理只能针对接口做代理,需要更佳的代理可以使用cglib。

源码


后记,这篇博文总结了反射的一些相关知识,参考了诸多前辈的文章,再次感谢他们。写这篇是给自己的一个总结,同时也希望看到的朋友也能有点收获,因此写的时候对一些细枝末节也是考虑好久,以免误导他人。但写的东西难免有纰漏之处,若有错误,希望不吝赐教,感谢!学习是一个不断进步的过程,欢迎交流学习~。












以上是关于Java反射机制的主要内容,如果未能解决你的问题,请参考以下文章

反射机制入门

反射机制入门

java 反射代码片段

深入理解java的反射机制

Java反射机制

Java核心技术梳理-类加载机制与反射