JAVA之反射
JAVA之反射
Java 中的 Class
类以及反射机制详解
1. Class
类的本质
Java 中,除了基本数据类型 (int
, boolean
,
float
等) 之外,所有的类型都是类 (class
)
或接口 (interface
) 。当 JVM
加载某个类时,它会为该类生成一个对应的 Class
对象。Class
类的一个对象本质上是 JVM
对该类的抽象表示,封装了有关该类的信息,如类名、包名、父类、接口、方法和字段等。
Class
类的定义
Class
类是 Java
中一个非常特殊的类,它用于表示类的类型。它的构造器是私有的,说明该类的对象只能由
JVM 创建,不能通过用户代码直接实例化。它的定义如下:
1 | public final class Class<T> { |
1.
public final class Class<T>
:
public
: 该类是公有的,可以在 Java 中任何地方访问。
final
: 表示Class
类不能被继承。这样设计是为了确保 Java 中的Class
实例行为不被子类改变,防止对反射机制的破坏。
<T>
: 泛型参数,表示Class
类是一个泛型类,T
代表具体的类型。例如,Class<String>
表示String
类的Class
实例,Class<Integer>
表示Integer
类的Class
实例。
1 Class<String> cls = String.class;2.
private Class()
:
private
: 这是一个私有构造器,意味着Class
类的对象不能通过代码直接使用new Class()
进行实例化。这样做的目的是为了防止程序员随意创建Class
类的实例,避免破坏 Java 的运行时类型系统。所有的Class
对象实例化过程由 JVM 负责处理。为什么是私有的?
- 类加载机制的控制权:
Class
对象的创建与管理是 JVM 的职责,它通过类加载器动态加载类,并为每个加载的类创建对应的Class
实例。这个私有构造器确保了外部程序无法通过常规手段创建Class
实例,从而防止程序在运行时随意操作类型。- 唯一性: 在 JVM 中,每个类在内存中有且只有一个
Class
实例与之对应,使用私有构造方法能够保证Class
对象的唯一性。每当 JVM 加载一个类时,都会自动为其创建唯一的Class
实例。3. 如何创建
Class
实例?虽然
Class
的构造方法是私有的,但我们可以通过以下方式获取类的Class
实例:
静态方式:
1 Class<String> cls = String.class; // 获取 String 类的 Class 实例通过实例对象:
1
2 String s = "Hello";
Class<? extends String> cls = s.getClass(); // 获取对象的 Class 实例动态加载类:
1 Class<?> cls = Class.forName("java.lang.String"); // 通过类名字符串获取 Class 实例4.
Class
实例的作用:
Class
类的实例用于在运行时获取某个类的详细信息,通常用于 反射。通过Class
实例,可以获取类的:
- 类名
- 包名
- 超类和接口
- 字段和方法
- 构造方法等
举个简单的例子,获取
String
类的详细信息:
1
2
3
4
5 Class<?> cls = String.class;
System.out.println("类名: " + cls.getName());
System.out.println("包名: " + cls.getPackage().getName());
System.out.println("父类: " + cls.getSuperclass().getName());
System.out.println("是否是接口: " + cls.isInterface());5. JVM 和
Class
的关系:JVM 在运行时加载 Java 类并为每个类创建一个对应的
Class
对象。这个Class
对象包含了类的所有结构信息,JVM 在内存中管理这些对象。通过反射机制,开发者可以在运行时获取和操作类的内部信息(如方法、字段、构造函数等),这是 Java 动态语言特性的基础。
每当 JVM 加载一个类时,都会为该类创建一个 Class
实例并关联这个类的所有信息。这些信息包括类名、包名、继承关系、实现的接口、字段、方法等等。JVM
为每个类(或者接口)创建的 Class
实例是唯一的,即对于每一个类,只会有一个 Class
对象表示它。
2. 反射机制
反射是一种非常强大的机制,它允许程序在运行时动态地获取一个类的内部信息,并且能动态创建对象、调用方法、访问字段等。反射使得 Java 程序能够在运行时动态操作代码,而不仅仅是在编译时操作。
获取 Class
对象的三种方法
Java 提供了三种方式来获取一个类的 Class
对象:
通过
.class
语法:1
Class<String> cls1 = String.class;
这种方式最直接,通过类的静态属性
class
来获取Class
对象。通过对象实例调用
getClass()
:1
2String s = "Hello";
Class<? extends String> cls2 = s.getClass();通过已有的对象调用其
getClass()
方法,可以得到对象实际的类型。通过
Class.forName(String className)
:1
Class<?> cls3 = Class.forName("java.lang.String");
这种方式可以通过类的全限定名(包名 + 类名)动态加载类,尤其适用于程序在运行时根据条件动态加载类。
这三种方式获取
Class
对象在 Java 中使用场景不同,底层机制有一些差异。1. 通过
.class
语法
1 Class<String> cls1 = String.class;
- 作用:
.class
是获取某个类的Class
对象的最直接方式,适用于已知编译时类型的情况。- 机制:
.class
是 Java 编译器的内置语法。它告诉编译器直接在运行时将类的Class
对象加载进来。String.class
在编译时就会生成字节码,JVM 会根据类加载器去获取对应的Class
实例。- 特点:
- 静态绑定:在编译时已经确定类的类型,因此
.class
是编译期的确定类型操作。- 无需抛出异常:
String.class
一定能返回一个合法的Class
实例。- 没有动态性,适合编译时就确定类的情况。
2. 通过对象实例调用
getClass()
1
2 String s = "Hello";
Class<? extends String> cls2 = s.getClass();
- 作用: 通过对象的
getClass()
方法,可以获取该对象的实际运行时类型。即使对象是通过多态形式赋值的,getClass()
返回的依然是对象的实际类类型。- 机制:
getClass()
是每个对象从java.lang.Object
继承的方法。JVM 会在对象被创建时分配它的具体Class
对象,并在getClass()
被调用时返回此对象。- 特点:
- 适用于运行时获取对象的动态类型。
- 支持多态:即使对象被声明为某个父类类型,调用
getClass()
时返回的依然是它的真实子类类型。- 无需处理异常:因为对象已实例化,它的
Class
已经存在。3. 通过
Class.forName(String className)
1 Class<?> cls3 = Class.forName("java.lang.String");
- 作用:
Class.forName()
通过类的完全限定名(即包含包名的类名)在运行时加载类。这是反射机制的一部分,可以用于动态加载类。- 机制:
Class.forName()
在运行时根据传入的类名,使用类加载器动态加载指定的类。它会查找类的字节码文件(.class 文件),并将其加载到 JVM 中。如果类不存在,或者无法加载,会抛出ClassNotFoundException
异常。- 特点:
- 动态加载:适用于在运行时确定类的情况,尤其是在反射或框架(如 JDBC)中动态加载类时常用。
- 需要异常处理:
Class.forName()
可能抛出ClassNotFoundException
,必须处理此异常。- 可以用来加载尚未被 JVM 加载的类,这与
.class
和getClass()
的行为不同,后者只返回已加载的Class
对象。底层机制区别:
.class
:
- 直接访问 JVM 中的
Class
对象。- 该对象可能已经被加载到 JVM 中,编译时确定,且无须重新加载类。
- 适合静态使用,类的类型在编译时确定。
getClass()
:
- 访问的是当前对象的运行时类型。
- 通过对象实例动态确定,适用于运行时获取对象的具体类型。
- 这种方式不会加载新的类,只是返回对象的
Class
对象。Class.forName()
:
- 在运行时根据类的完全限定名,通过类加载器加载类。
- 如果类还没有加载,JVM 会通过类加载器加载该类。
- 适用于需要动态加载类的场景(如反射、插件系统等)。
什么时候使用哪种方式?
.class
: 当你已经知道类的编译时类型时,最直接、最简单的方法。
getClass()
: 当你有一个对象,并且想要获取它的实际运行时类型时使用,尤其适合多态情况下获取真实的类信息。
Class.forName()
: 当你只知道类的名字(通常是字符串格式),并且希望在运行时动态加载类时使用,尤其在框架或反射中常用。例子对比
1
2
3
4
5
6
7
8
9
10
11
12
13 // .class 语法
Class<String> cls1 = String.class;
// 通过对象获取
String str = "hello";
Class<? extends String> cls2 = str.getClass();
// 动态加载类
try {
Class<?> cls3 = Class.forName("java.lang.String");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}总结来说:
.class
静态绑定,编译期已知类型;getClass()
动态绑定,适合运行时确定类型;Class.forName()
动态加载类,适合反射和框架使用。
Class 实例的唯一性
无论使用哪种方式获取 Class
实例,JVM
确保对于每一个类或接口,Class
对象是唯一的。可以使用
==
运算符来比较两个 Class
对象是否是同一个:
1 | Class<String> cls1 = String.class; |
3. 通过 Class
获取类的详细信息
通过反射,Class
对象不仅可以用来标识类,还可以用来获取该类的所有信息。以下是一些常用方法:
getName()
: 返回类的全限定名。getSimpleName()
: 返回类的简单名(不包括包名)。getPackage()
: 返回类的包信息。isInterface()
: 判断是否为接口。isEnum()
: 判断是否为枚举类型。isArray()
: 判断是否为数组类型。isPrimitive()
: 判断是否为基本类型。
1 | public class Main { |
4. 反射创建实例
通过反射可以动态创建对象实例。通常,我们可以使用
Class.newInstance()
方法来创建对象,但它只能调用无参构造函数:
1 | Class<String> cls = String.class; |
如果类没有无参构造函数,或者需要调用带参的构造函数,可以通过
Constructor
类进行操作:
1 | Constructor<Person> constructor = Person.class.getConstructor(String.class); |
5. 动态加载类
JVM 动态加载类的机制非常重要,它允许 Java 程序根据运行时的需求动态加载类,而不需要预先加载所有类。例如,下面的代码展示了根据条件动态加载不同的日志实现类:
1 | LogFactory factory = null; |
这种动态加载机制在很多场景下非常有用,比如插件系统、依赖注入框架(如 Spring)、和跨平台库的实现等。
6. instanceof
与 Class
的区别
在判断一个对象的类型时,instanceof
和 Class
对象的 ==
有不同的用途:
instanceof
不仅可以判断对象是否属于某个类,还可以判断对象是否属于该类的子类或实现类。Class
的==
判断则更加严格,它只能判断对象是否属于某个具体的类,而不会考虑子类。
1 | Integer n = new Integer(123); |
Java 反射机制访问和修改类中的字段
1. 获取字段信息
获取字段的几种方式:
getField(name)
:- 获取指定名称的
public
字段,包括继承自父类的public
字段。 - 例如:
Class.getField("name")
可以获取父类Person
中的public String name;
字段。
- 获取指定名称的
getDeclaredField(name)
:- 获取当前类中的指定字段,不论是否
public
,但不包括父类的字段。 - 例如:
Class.getDeclaredField("grade")
可以获取子类Student
中的private int grade;
字段。
- 获取当前类中的指定字段,不论是否
getFields()
:- 获取当前类及其父类中所有的
public
字段。
- 获取当前类及其父类中所有的
getDeclaredFields()
:- 获取当前类的所有字段(包括
private
和protected
字段),但不包含父类的字段。
- 获取当前类的所有字段(包括
示例代码:
1 | public class Main { |
输出的内容是:
1 | public int Student.score |
2. Field
对象
Field
对象代表了一个类中的字段,包含了字段的名称、类型和修饰符等信息。它的常用方法如下:
getName()
:返回字段名称,例如"name"
。getType()
:返回字段类型,通常是一个Class
实例。例如,String.class
。getModifiers()
:返回字段的修饰符,以整数形式表示。可以使用Modifier
类中的静态方法检查字段是否为public
、private
、final
等。
以 String
类的 value
字段为例:
1 | Field f = String.class.getDeclaredField("value"); |
3. 获取字段值
通过反射,我们可以获取某个实例的字段值。代码如下:
1 | import java.lang.reflect.Field; |
Field.get(Object obj)
:通过反射获取obj
实例的字段值。在调用之前,我们需要使用f.setAccessible(true)
来确保可以访问private
字段。否则会抛出IllegalAccessException
。通过
f.get(p)
,可以获取到p
对应实例中name
字段的值,即"Xiao Ming"
。
4. 修改字段值
通过反射不仅可以获取字段值,还可以直接修改字段的值。代码如下:
1 | import java.lang.reflect.Field; |
在这段代码中:
f.set(Object obj, Object value)
:用于修改指定实例obj
的字段值为value
。- 通过
f.set(p, "Xiao Hong")
,将Person
实例p
的name
字段值从"Xiao Ming"
修改为"Xiao Hong"
。
修改字段值的过程与获取字段值类似,关键点是使用
setAccessible(true)
来允许修改 private
字段的值。
5. 反射的局限性
尽管反射可以绕过 Java 的访问修饰符限制,直接访问和修改
private
字段,但这种做法有以下局限性:
- 性能开销:反射机制比直接访问字段要慢很多,尤其是在频繁调用的情况下,可能会带来较大的性能开销。
- 代码复杂性:反射代码相对复杂且易出错,增加了代码的维护难度。
- 安全性风险:反射允许访问和修改私有字段,打破了封装性。尤其是在
JVM 启用了
SecurityManager
的情况下,setAccessible(true)
可能会被拒绝,防止对敏感类的字段(如java.lang.String
)进行修改。
反射与方法
在Java反射中,通过Class
类可以获取类的所有方法信息,并且可以通过Method
对象来调用这些方法。使用反射进行方法调用主要分为几个步骤:获取Method
对象,设置访问权限(如果需要),然后通过invoke()
进行调用。
1. 获取方法信息
通过Class
对象,可以获取类中的方法信息。Java的Class
类提供了几种方法来获取类的Method
对象:
Method getMethod(String name, Class<?>... parameterTypes)
:获取类或父类中某个public方法(包括继承的公共方法)。Method getDeclaredMethod(String name, Class<?>... parameterTypes)
:获取当前类中定义的某个方法(包括private方法,但不包括父类中的方法)。Method[] getMethods()
:获取当前类及其父类的所有public方法。Method[] getDeclaredMethods()
:获取当前类中声明的所有方法(包括private方法)。
示例代码
1 | // 通过反射获取方法 |
输出的结果类似如下:
1 | public int Student.getScore(java.lang.String) |
2. 获取方法的属性
从Method
对象中,我们可以获取到方法的多种信息:
getName()
:获取方法的名称。getReturnType()
:获取方法的返回类型。getParameterTypes()
:获取方法的参数类型。getModifiers()
:获取方法的修饰符,返回一个整数值,可以使用Modifier
类来判断具体修饰符。
示例代码:获取方法属性
1 | Method m = String.class.getMethod("substring", int.class); |
输出示例:
1 | Method Name: substring |
3. 调用方法
通过Method
对象的invoke()
方法,可以对某个实例对象调用该方法。调用时,invoke()
的第一个参数是调用方法的实例对象,剩余参数是方法所需的参数。
示例代码:调用方法
1 | // reflection |
在上面的例子中,invoke()
的第一个参数是String
对象"Hello world"
,表示在这个对象上调用substring
方法,第二个参数是6
,表示substring
方法的参数。
4. 调用静态方法
对于静态方法,由于不需要实例对象,所以invoke()
的第一个参数可以传入null
。
示例代码:调用静态方法
1 | // reflection |
5. 调用非public方法
如果需要调用非public方法,需要先调用setAccessible(true)
,以允许访问该方法。
示例代码:调用private方法
1 | import java.lang.reflect.Method; |
通过setAccessible(true)
,可以突破Java的访问控制,访问private
方法。
6. 多态行为
即使通过反射,方法的调用仍然遵循Java的多态机制。即,当子类覆写了父类的方法时,调用的是实际对象的子类方法,而不是父类方法。
示例代码:反射中的多态
1 | import java.lang.reflect.Method; |
在这段代码中,虽然获取的是Person
类的hello()
方法,但调用时由于多态,实际上调用的是Student
类的hello()
方法。
- 通过反射可以获取方法信息,并通过
Method.invoke()
调用方法。- 反射支持调用非public方法,需要先设置
setAccessible(true)
。- 反射调用时依然遵循Java的多态机制,调用的是实际对象的覆写方法。
通过反射机制调用构造方法
在Java中,通过反射机制调用构造方法,可以用Constructor
对象来代替传统的new
操作符创建新的实例。使用反射可以调用任何构造方法,包括带参数的、非public
的构造方法。
1.
获取Constructor
对象
要通过反射获取类的构造方法,使用Class
类提供的以下方法:
方法 | 说明 |
---|---|
getConstructor(Class...) |
获取某个public 构造方法,参数类型通过Class... 指定。 |
getDeclaredConstructor(Class...) |
获取任意构造方法,包括private 、protected 和package-private 的。 |
getConstructors() |
获取当前类的所有public 构造方法。 |
getDeclaredConstructors() |
获取当前类的所有构造方法,包括private 等非public 的构造方法。 |
注意:获取的Constructor
对象与方法或字段类似,它封装了构造方法的相关信息,包括参数类型、修饰符等。
2. 创建对象实例
通过获取的Constructor
对象,可以调用其newInstance()
方法来创建一个新的实例。这个方法接受与构造方法参数匹配的参数,创建实例的具体操作如下:
示例代码:
1 | import java.lang.reflect.Constructor; |
在上面的代码中,Integer
类有两个构造方法,一个接受int
参数,另一个接受String
参数。通过反射获取到这两个Constructor
对象后,分别调用newInstance()
方法来创建Integer
实例。
3.
处理非public
构造方法
对于非public
的构造方法,例如private
构造方法,直接通过getDeclaredConstructor()
获取它后,需要通过setAccessible(true)
来允许访问该构造方法,类似于访问private
字段或方法。
示例代码:
1 | import java.lang.reflect.Constructor; |
在这个例子中,Person
类有一个private
构造方法,不能直接通过new
操作符调用,但通过反射,我们使用setAccessible(true)
来允许访问这个private
构造方法,并成功创建了Person
实例。
4. 注意事项
setAccessible(true)
可能会失败:在一些情况下,如果JVM正在运行一个SecurityManager
(安全管理器),它会检查是否允许调用setAccessible(true)
。对于某些核心类(如java
和javax
包),出于安全考虑,安全管理器可能阻止对它们的private
构造方法调用setAccessible(true)
。异常处理:调用
Constructor.newInstance()
可能抛出许多异常,包括:IllegalAccessException
:当没有权限调用该构造方法时抛出。InstantiationException
:如果调用的是一个抽象类的构造方法,抛出此异常。InvocationTargetException
:如果构造方法本身抛出异常,会封装在此异常中。NoSuchMethodException
:如果指定的构造方法不存在,会抛出此异常。
5. Constructor和Method的区别
Constructor
是构造方法:返回一个类的实例。Method
是普通方法:调用时可以返回任意类型的结果(或void
)。
例子
1.
调用带参数的public
构造方法
通过反射调用带参数的public
构造方法,首先需要通过getConstructor()
方法获取构造方法,然后调用newInstance()
来创建实例。
示例:
1 | import java.lang.reflect.Constructor; |
此例中,String
类的构造方法接收一个String
参数,通过反射成功创建了String
对象。
2.
调用private
构造方法
对于private
构造方法,需要使用getDeclaredConstructor()
并通过setAccessible(true)
来解除访问权限。
示例:
1 | import java.lang.reflect.Constructor; |
这个例子演示了如何调用private
构造方法,通过setAccessible(true)
解除访问限制。
3.
调用无参的protected
构造方法
假设有一个protected
的无参构造方法,依然可以通过反射获取它并实例化对象。
示例:
1 | import java.lang.reflect.Constructor; |
此例展示了如何调用protected
无参构造方法,并成功创建对象。
4. 处理构造方法中的异常
如果构造方法可能抛出异常,可以在调用newInstance()
时捕获这些异常。
示例:
1 | import java.lang.reflect.Constructor; |
在这个例子中,Car
类的构造方法可能抛出异常,因此在反射调用时捕获并处理了该异常。
5. 获取所有构造方法并调用
通过getConstructors()
或getDeclaredConstructors()
可以获取类的所有构造方法,然后遍历这些构造方法,并根据需要调用不同的构造方法。
示例:
1 | import java.lang.reflect.Constructor; |
这个示例展示了如何遍历类的所有构造方法,并根据其参数情况进行调用。
6. 调用构造方法并捕获抛出的异常
通过反射调用构造方法时,如果构造方法内部抛出异常,newInstance()
方法会封装该异常为InvocationTargetException
,可以通过getCause()
获取原始异常。
示例:
1 | import java.lang.reflect.Constructor; |
在此例中,当构造方法抛出异常时,InvocationTargetException
捕获并处理了具体异常信息。
通过反射获取继承关系
1.
获取Class
对象的三种方式
在Java中,我们可以通过三种方式获取类的Class
对象,每种方式都适用于不同的场景。关键点是,无论哪种方式,JVM对于每个加载的类只会创建一个Class
对象实例。
方式一:使用.class
通过类名直接获取对应的Class
对象。这种方式适用于已知类的情况:
1 | Class<String> cls = String.class; |
方式二:通过getClass()
通过实例对象获取它的Class
对象,适用于已知某个对象的场景:
1 | String s = "Hello"; |
方式三:使用Class.forName()
通过类的完全限定名(包名+类名)获取Class
对象,适用于动态加载类的情况,通常用于反射框架:
1 | Class<?> cls = Class.forName("java.lang.String"); |
三种方式获取的Class
对象都相同,因为JVM对每个类只创建一个Class
实例。
2.
获取父类的Class
通过getSuperclass()
方法,可以获取类的父类。如果当前类是Object
,则返回null
,因为Object
是所有类的基类,没有父类。
示例:
1 | public class Main { |
输出结果:
1 | Integer的父类: java.lang.Number |
3. 获取实现的接口
类可能实现多个接口,使用getInterfaces()
可以获取当前类直接实现的接口。如果一个类没有实现任何接口,getInterfaces()
返回一个空数组。此方法不包括父类实现的接口。
示例:
1 | public class Main { |
输出结果:
1 | Integer实现的接口: |
4. 获取接口的父接口
接口本身也可以继承其他接口。对接口调用getInterfaces()
方法可以获取它继承的父接口,但对接口调用getSuperclass()
方法总是返回null
。
示例:
1 | public class Main { |
输出结果:
1 | Closeable继承的接口: |
5.
instanceof
与isAssignableFrom()
instanceof
和isAssignableFrom()
方法用于判断类与类之间的继承关系或实现关系,二者使用场景略有不同:
instanceof
:用于判断一个对象是否是某个类的实例,或是否实现了某个接口。isAssignableFrom()
:用于判断一个Class
对象是否可以赋值给另一个Class
对象,即判断类的类型转换是否合法。
示例
1:instanceof
判断
1 | public class Main { |
示例
2:isAssignableFrom()
判断类的可赋值关系
1 | public class Main { |
- 获取
Class
对象的三种方式:.class
、getClass()
、Class.forName()
。- 获取父类和接口:使用
getSuperclass()
获取父类,使用getInterfaces()
获取类实现的接口。- 接口继承与
getSuperclass()
的关系:接口没有父类,getSuperclass()
返回null
,获取接口的父接口需要使用getInterfaces()
。- 继承关系判断:使用
instanceof
判断对象的类型关系,使用isAssignableFrom()
判断类之间的赋值关系。
静态代理与动态代理
Java 的动态代理机制通过 Proxy.newProxyInstance()
在运行期动态生成接口的实现,这种机制为接口的实现提供了一种灵活的方式,使得我们不需要显式地编写实现类即可实例化接口并实现其中的方法。
静态 vs 动态代理
静态代理 是指在编译时就已经确定代理类的代码,并通过实现接口来代理目标对象。编写静态代理类有几个步骤:
- 定义接口
- 实现接口并编写代理逻辑
- 在代理类中通过组合目标对象的方式,代理目标对象的接口方法。
但是,静态代理的弊端在于每次需要代理一个接口时,都必须手动编写一个代理类,而这在项目规模扩大时显得非常繁琐。
动态代理 则是在运行时通过反射机制来创建一个代理对象,而不需要手动去实现接口。它是在运行时生成字节码,并加载到内存中进行执行。因此,动态代理可以为任意接口生成代理对象,而无需提前编写具体的代理类。
动态代理的核心组件
要使用动态代理,涉及以下几个核心组件:
接口(Interface): 代理目标必须是接口,因为动态代理只能代理接口类型。
InvocationHandler 接口: 这个接口用来处理对代理对象的方法调用。实现
InvocationHandler
接口的类,需要重写invoke()
方法,该方法会在代理对象调用任意方法时执行。invoke()
方法的签名是:1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
proxy
:代理对象本身。method
:被调用的方法。args
:调用方法时传入的参数。
Proxy 类:
Proxy
是 Java 提供的核心类,通过它的newProxyInstance()
方法,可以生成代理对象。该方法接收三个参数:ClassLoader loader
:定义了哪个类加载器来加载代理类。Class<?>[] interfaces
:代理对象需要实现的接口。InvocationHandler h
:处理代理对象上所有方法调用的处理器。
运行时如何创建代理对象
通过 Proxy.newProxyInstance()
方法,我们可以在运行时创建代理对象。下面对该方法的工作流程进行详细说明:
步骤 1: 创建
InvocationHandler
首先,我们需要实现 InvocationHandler
接口,负责代理对象方法调用时的处理逻辑:
1 | InvocationHandler handler = new InvocationHandler() { |
在这个示例中,InvocationHandler
负责拦截对
morning
方法的调用,并输出相应的问候。
步骤 2: 调用
Proxy.newProxyInstance()
接下来,通过 Proxy.newProxyInstance()
方法动态生成代理对象:
1 | Hello hello = (Hello) Proxy.newProxyInstance( |
这里我们向 Proxy.newProxyInstance()
方法传入了:
Hello.class.getClassLoader()
:使用接口Hello
的类加载器。new Class[] { Hello.class }
:指定要代理的接口。handler
:处理方法调用的InvocationHandler
实例。
该方法返回的对象是 Hello
接口的代理实现,调用它的
morning()
方法时,实际上是触发了
handler.invoke()
方法。
步骤 3: 调用代理对象的方法
生成代理对象后,我们可以像普通对象一样调用它的方法:
1 | hello.morning("Bob"); |
此时,代理对象的 morning()
方法被调用,实际上是
handler.invoke()
方法在运行,通过 Method
对象反射调用实际的方法。
动态代理 vs 静态代理的区别
特性 | 静态代理 | 动态代理 |
---|---|---|
实现方式 | 需要手动编写代理类 | 通过反射机制在运行时生成代理类 |
代理对象 | 只能代理特定的接口 | 可以代理任意接口,只要有相应的 InvocationHandler
实现 |
扩展性 | 需要为每个接口编写单独的代理类 | 只需编写一次 InvocationHandler
实现,可代理多个接口 |
性能 | 编译时确定,运行效率较高 | 运行时动态生成代理类,性能稍差 |
维护性 | 随着接口的增多,代理类越来越多,难以维护 | 只需维护 InvocationHandler ,扩展性强 |
动态代理原理解析
Java 动态代理的核心是 反射机制 和
类加载器。Proxy.newProxyInstance()
实际上是创建了一个代理类的字节码,并加载到 JVM
中。在调用代理类的方法时,JVM 将拦截方法调用,并将其转发给
InvocationHandler
的 invoke()
方法。
具体来说:
- 类加载器:
Proxy.newProxyInstance()
会使用指定的ClassLoader
动态生成代理类的字节码。 - 字节码生成:在运行时,JVM
动态生成实现了指定接口的代理类,代理类的所有方法都会委托给
InvocationHandler
的invoke()
方法进行处理。 - 方法拦截:在代理类中,所有方法的调用都被重定向到
InvocationHandler
的invoke()
方法,该方法可以根据反射信息自行决定如何处理方法调用。
动态代理的应用场景
动态代理广泛应用于各种场景,尤其是在 AOP(面向切面编程)中。常见的应用包括:
- 日志记录:在方法调用前后打印日志,无需修改方法本身。
- 权限控制:在方法调用前检查用户权限。
- 事务管理:在方法调用前后自动开启和提交事务。
- 远程调用:将方法调用通过网络发送到远程服务器执行。