Java 反射机制与 JDK 动态代理

Java 中,通过 Class 实例获取 class 信息的方法被称为反射 (Reflection)。Java 反射机制为我们提供了强大的工具,使我们能够在运行时动态地获取和操作类的信息。接下来,我们将介绍反射机制中 ClassFieldMethodConstructor 类的基本用法,以及反射在动态代理中的应用。

1 Class 类

1.1 Class 类的基本概念

除了 intdouble 等基本类型外,Java 的其他类型全部都是 class(包括 interface)。例如:

  • String
  • Object
  • Runnable
  • Exception

class 的本质是一种数据类型,无继承关系的数据类型之间无法赋值:

1
2
Number n = new Double(123.456); // OK
String s = new Double(123.456); // Compile error

class 是在 JVM 运行过程中动态加载的。JVM 只有在第一次读取到某个 class 类型时才会将其加载到内存中。每当加载一种 class 时,JVM 就会为其创建一个与之关联的 Class 类型的实例。Class 类是反射机制的核心,它内部包含了该 class 的所有信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
+---------------------------+
│      Class Instance       │------> String
+---------------------------+
│name = "java.lang.String"  │
+---------------------------+
│package = "java.lang"      │
+---------------------------+
│super = "java.lang.Object" │
+---------------------------+
│interface = CharSequence...│
+---------------------------+
│field = value[],hash,...   │
+---------------------------+
│method = indexOf()...      │
+---------------------------+

由于 JVM 为每个加载的 class 创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。因此,如果获得了某个 Class 实例,我们就能够通过这个实例获取到该实例对应的 class 的所有信息。

1.2 获取 Class 类的实例

我们可以使用以下方法获取 Class 实例:

1
2
3
4
5
6
7
8
// 通过对象获取 Class 实例
Class<?> cls1 = "Hello".getClass();

// 通过类名获取 Class 实例
Class<?> cls2 = Class.forName("java.lang.String");

// 直接通过类字面量获取 Class 实例
Class<?> cls3 = String.class;

2 Field 类

Field 类用于描述类的字段信息,通过它我们可以获取和设置类的字段值。

2.1 获取 Field 实例

Class 类提供了以下几个方法来获取 Field

方法声明功能介绍
Field getField(name)根据字段名获取某个 public 的 field(包括父类)
Field getDeclaredField(name)根据字段名获取当前类的某个 field(不包括父类)
Field[] getFields()获取所有 public 的 field(包括父类)
Field[] getDeclaredFields()获取当前类的所有 field(不包括父类)

使用示例如下:

1
2
3
4
5
// 获取某个 public 的字段
Field publicField = stdClass.getField("fieldName");

// 获取当前类的所有字段(包括私有字段)
Field[] allFields = stdClass.getDeclaredFields();

2.2 Field 类的常用方法

方法声明功能介绍
Object get(Object obj)获取参数对象 obj 中此 Field 对象所表示成员变量的数值
void set(Object obj, Object value)将参数对象 obj 中此 Field 对象表示成员变量的数值修改为参数 value 的数值
void setAccessible(boolean flag)当实参传递 true 时,则反射对象在使用时应该取消 Java 语言访问检查
int getModifiers()获取成员变量的访问修饰符
Class getType()获取成员变量的数据类型
String getName()获取成员变量的名称

使用示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Object obj = new StdObj();
Class stdClass = obj.getClass();

// 获取某个字段的值
Field stdField = stdClass.getDeclaredField("field");
stdField.setAccessible(true);
Object value = stdField.get(obj);

// 设置某个字段的值
stdField = stdClass.getDeclaredField("field");
stdField.setAccessible(true);
stdField.set(obj, 18);
setAccessible 说明

以上示例通过setAccessible(true)破坏了name的封装性,那么类的封装还有什么意义?

正常情况下,我们总是通过p.name来访问Personname字段,编译器会根据publicprotectedprivate决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。 此外,setAccessible(true)可能会失败。如果 JVM 运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证 JVM 核心库的安全。

3 Method 类的运用

Method 类提供了对类的方法的操作,可以通过它调用和处理方法。

3.1 获取 Method 实例

方法声明功能介绍
Method getMethod(name, Class...)获取某个 publicMethod(包括父类)
Method getDeclaredMethod(name, Class...)获取当前类的某个 Method(不包括父类)
Method[] getMethods()获取所有 publicMethod(包括父类)
Method[] getDeclaredMethods()获取当前类的所有 Method(不包括父类)

使用示例如下:

1
2
3
4
5
// 获取某个 public 的方法
Method publicMethod = stdClass.getMethod("methodName");

// 获取当前类的所有方法(包括私有方法)
Method[] allMethods = stdClass.getDeclaredMethods();

3.2 Method 类常用方法

方法声明功能介绍
String getName()获取方法的名称
int getModifiers()获取方法的访问修饰符
Class getReturnType()获取方法的返回值类型
Class[] getParameterTypes()获取方法所有参数的类型
Class[] getExceptionTypes()获取方法的异常信息
Object invoke(Object obj, Object… args)使用对象 obj 来调用此 Method 对象所表示的成员方法,实参传递 args

使用示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 调用普通 public 方法
Method m = String.class.getMethod("substring", int.class);
String r = (String) m.invoke(s, 6); // "world"

// 调用静态方法
Method m = Integer.class.getMethod("parseInt", String.class);
Integer n = (Integer) m.invoke(null, "12345"); // 12345

// 调用 private 方法
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true); // 需要绕过防护
m.invoke(p, "Bob");

4 Constructor 类

Constructor 类用于获取类的构造方法信息,可以通过它实例化对象。

4.1 获取 Constructor 实例

通过Class实例获取Constructor的方法如下:

方法声明功能介绍
getConstructor(Class...)获取某个 publicConstructor
getDeclaredConstructor(Class...)获取某个 Constructor
getConstructors()获取所有 publicConstructor
getDeclaredConstructors()获取所有 Constructor

使用示例如下:

1
2
3
4
5
// 获取某个 public 的构造方法
Constructor<?> publicConstructor = stdClass.getConstructor();

// 获取当前类的所有构造方法(包括私有构造方法)
Constructor<?>[] allConstructors = stdClass.getDeclaredConstructors();

注意:

  • Constructor 总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
  • 调用非 publicConstructor 时,必须首先通过 setAccessible(true)设置允许访问。setAccessible(true)可能会失败。

4.2 Constructor 类常用方法

方法声明功能介绍
T newInstance(Object… initargs)使用此 Constructor 对象描述的构造方法来构造 Class 对象代表类型的新实例
int getModifiers()获取方法的访问修饰符
String getName()获取方法的名称
Class<?>[] getParameterTypes()获取方法所有参数的类型

使用示例如下:

  1. 反射方式无参构造
    1
    2
    3
    4
    
    Class stdClass = Class.forName("StdObject");
    // 获取 Class 类中对应的无参构造
    Constructor constructor = stdClass.getConstructor();
    StdObject obj = constructor.newInstance();
  2. 反射方式有参构造
    1
    2
    3
    
    Constructor constructor 
        = stdClass.getConstructor(String.class, int.class);
    StdObject obj = constructor.newInstance("param1", 222);
  3. 反射方式一次获取所有公共构造
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    Constructor[] constructors_s = stdClass.getConstructors();
    for (Constructor c : constructors_s){
        System.out.println("获取的构造方法修饰符是:" + c.getModifiers());
        System.out.println("获取的构造方法名字是:" + c.getName());
        Class[] parameterTypes = c.getParameterTypes();
        for(Class c1 : parameterTypes) {
            System.out.println("获取的构造方法的参数有:" + c1);
        }
    }

5 反射在 JDK 动态代理中的应用

5.1 JDK 动态代理简介

代理是一种常用的设计模式,其目的是为其他对象提供一个代理,以控制对某个对象的访问。Java 标准库提供了一种动态代理 (Dynamic Proxy) 的机制,我们称之为 JDK 动态代理。通过 JDK 动态代理,我们可以事先定义好接口,而不必编写实现类,而是直接通过 JDK 提供的 Proxy.newProxyInstance() 创建一个接口的实现。

这种在运行期动态创建的对象,我们称之为动态代码。JDK 提供的动态创建接口对象的方式被称为动态代理

5.2 JDK 动态代理相关的类和接口

类、接口说明
java.lang.reflect.Proxy这是 JDK 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象
java.lang.reflect.InvocationHandler这是调用处理器接口,它自定义了一个 invoke 方法,用于几种处理在动态代理类对象上的方法调用。通常在该方法中实现对委托类的代理访问
java.lang.ClassLoaderProxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中

5.3 JDK 动态代理使用示例

下面我们通过一个示例,了解一下 JDK 动态代理的使用方法:

  1. 定义一个接口:
    1
    2
    3
    
    interface MyInterface {
        void doSomething();
    }
  2. 实现 InvocationHandler 接口:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    class MyInvocationHandler implements InvocationHandler {
        private Object realObject;
    
        public MyInvocationHandler(Object realObject) {
            this.realObject = realObject;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("代理前置");
    
            // 调用实际对象的方法
            Object result = method.invoke(realObject, args);
    
            System.out.println("代理后置");
            return result;
        }
    }
  3. 创建代理对象,并调用其方法:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    public class DynamicProxyExample {
        public static void main(String[] args) {
            // 创建实际对象
            MyInterface realObject = new MyInterface() {
                @Override
                public void doSomething() {
                    System.out.println("实际对象执行");
                }
            };
    
            // 创建 InvocationHandler 实例
            MyInvocationHandler handler = new MyInvocationHandler(realObject);
    
            // 创建代理对象
            MyInterface proxyObject = (MyInterface) Proxy.newProxyInstance(
                    MyInterface.class.getClassLoader(),
                    new Class[]{MyInterface.class},
                    handler
            );
    
            // 通过代理对象调用方法
            proxyObject.doSomething();
        }
    }

5.4 JDK 动态代理实现原理

上述示例的实现过程大致分为以下几个步骤:

  1. 接口定义 (MyInterface):定义了一个简单的接口,其中包含一个 doSomething() 方法。
  2. 实现 InvocationHandler 接口 (MyInvocationHandler):该接口中的 invoke 方法会在代理对象的每个方法调用时被调用。在这个例子中,invoke 方法中输出了 “代理前置” 和 “代理后置”,并通过反射调用了实际对象的相应方法。
  3. 创建实际对象 (realObject):创建了一个实现了 MyInterface 接口的匿名类实例,该实例包含了具体的方法实现。
  4. 创建代理对象 (proxyObject):使用 Proxy.newProxyInstance() 方法创建代理对象。该方法需要传入类加载器、要实现的接口数组和 InvocationHandler 实例。在这个例子中,proxyObject 实际上是 MyInterface 接口的一个代理对象,方法调用会被转发到 MyInvocationHandler 中的 invoke 方法。
  5. 方法调用:当通过代理对象 proxyObject 调用 doSomething() 方法时,实际上会调用 MyInvocationHandler 中的 invoke 方法。

总之,JDK 动态代理利用了 Java 的反射机制,动态地创建了一个代理对象,该代理对象实现了指定的接口,并且在方法调用时将调用转发到 InvocationHandlerinvoke 方法,方便我们在方法调用前后插入自定义逻辑。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容