设计模式–代理模式

代理模式是常用的java设计模式,它允许你通过创建一个代理对象来控制对另一个对象(即目标对象)的访问,代理类与委托类有同样的接口。

代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。

代理对象充当了目标对象的接口,客户端通过代理对象与目标对象进行交互,而不是直接访问目标对象。

代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。

在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。代理模式可以在不改变目标对象代码的前提下,对目标对象的功能进行增强或扩展。

img
img

可以发现,其中有

  • 代理对象

  • 被代理的行为

  • 被代理的对象

  • 行为的完全控制

静态代理

静态代理:由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口,被代理类,代理类等确定下来。在程序运行之前,代理类的.class文件就已经生成。

简单实现

首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有上交班费的行为。这样,学生上交班费就可以让班长来代理执行。

1
2
3
4
5
6
7
8
/**
* 创建Person接口
* @author Gonjan
*/
public interface Person {
//上交班费
void giveMoney();
}

Student类实现Person接口。Student可以具体实施上交班费的动作。

1
2
3
4
5
6
7
8
9
10
11
public class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}

@Override
public void giveMoney() {
System.out.println(name + "上交班费50元");
}
}

StudentsProxy类,这个类也实现了Person接口,但是还另外持有一个学生类对象,由于实现了Peson接口,同时持有一个学生对象,那么他可以代理学生类对象执行上交班费(执行giveMoney()方法)行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 学生代理类,也实现了Person接口,保存一个学生实体,这样既可以代理学生产生行为
public class StudentsProxy implements Person{
//被代理的学生
Student stu;

public StudentsProxy(Person stu) {
// 只代理学生对象
if(stu.getClass() == Student.class) {
this.stu = (Student)stu;
}
}

//代理上交班费,调用被代理学生的上交班费行为
public void giveMoney() {
stu.giveMoney();
}
}

测试主类

1
2
3
4
5
6
7
8
9
10
11
12
public class StaticProxyTest {
public static void main(String[] args) {
//被代理的学生张三,他的班费上交有代理对象monitor(班长)完成
Person zhangsan = new Student("张三");

//生成代理对象,并将张三传给代理对象
Person monitor = new StudentsProxy(zhangsan);

//班长代理上交班费
monitor.giveMoney();
}
}

这里并没有直接通过张三(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了。这就是代理模式。

代理模式最主要的就是有一个公共接口(Person),一个具体的类(Student),一个代理类(StudentsProxy),代理类持有具体类的实例,代为执行具体类实例方法。

代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。

这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以很方便的加上一些其他用途

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StudentsProxy implements Person{
//被代理的学生
Student stu;

public StudentsProxy(Person stu) {
// 只代理学生对象
if(stu.getClass() == Student.class) {
this.stu = (Student)stu;
}
}

//代理上交班费,调用被代理学生的上交班费行为
public void giveMoney() {
System.out.println("张三最近学习有进步!"); // 代理类顺便告诉张三最近学习有进步
stu.giveMoney();
}
}

可以看到,只需要在代理类中帮张三上交班费之前,执行其他操作就可以了。这种操作,也是使用代理模式的一个很大的优点。最直白的就是在Spring中的面向切面编程(AOP),我们能在一个切点之前执行一些操作,在一个切点之后执行一些操作,这个切点就是一个个方法。这些方法所在类肯定就是被代理了,在代理过程中切入了一些其他操作。

动态代理

介绍

代理类在程序运行时创建的代理方式被成为动态代理。

我们上面静态代理的例子中,代理类(studentProxy)是自己定义好的,在程序运行之前就已经编译完成。

然而动态代理,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。

相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。 比如说,想要在每个代理的方法前都加上一个处理方法:

1
2
3
4
5
public void giveMoney() {
//调用被代理方法前加入处理方法
beforeMethod();
stu.giveMoney();
}

这里只有一个giveMoney方法,就写一次beforeMethod方法,但是如果出了giveMonney还有很多其他的方法,那就需要写很多次beforeMethod方法,麻烦。那看看下面动态代理如何实现。

简单实现

在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。

相关方法:

创建一个InvocationHandler对象

1
2
//创建一个与代理对象相关联的InvocationHandler
InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);

InvocationHandler接口定义了一个invoke方法,当通过代理对象调用目标方法时,invoke 方法会被调用。invoke 方法接收三个参数:代理对象、被调用的方法对象以及方法的参数数组。在 invoke 方法中,我们可以编写额外的逻辑,然后调用目标方法并返回结果。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler接口 { // 实现InvocationHandler接口
private Object target;
public MyInvocationHandler(Object target) {
// 存储目标对象。这个目标对象就是我们要为其添加额外功能的对象。
this.target = target;
}
// 需要重写invoke方法
@Override
// Object proxy:代理对象本身 Method method:被调用的目标方法对象 Object[] args:调用目标方法时传递的参数数组
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 调用目标方法前的逻辑,例如记录日志
System.out.println("Before method invocation: " + method.getName());
// 调用目标方法
Object result = method.invoke(target, args); // 代理target对象实际方法
// 调用目标方法后的逻辑,例如记录日志
System.out.println("After method invocation: " + method.getName());
return result;
}
}

使用Proxy类getProxyClass静态方法生成一个动态代理类stuProxyClass

1
Class<?> stuProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class<?>[] {Person.class});

该类提供了静态方法用于创建动态代理类和代理对象。

其中,newProxyInstance 方法是创建代理对象的关键方法,它接收三个参数:类加载器、目标对象实现的接口数组以及 InvocationHandler 实例。

例如

1
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Proxy;
// 为传入的目标对象创建一个动态代理对象。
public class ProxyFactory {
public static Object createProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
// 动态代理只能为实现了接口的类创建代理对象,因此需要指定目标对象实现的接口,这样代理对象才能实现相同的接口。
target.getClass().getInterfaces(), // 获取目标对象所实现的所有接口的数组
new MyInvocationHandler(target));
}
}

获得stuProxyClass 中一个带InvocationHandler参数的构造器constructor

1
Constructor<?> constructor = PersonProxy.getConstructor(InvocationHandler.class);

在 Java 反射机制中,Class 类提供了一系列方法来获取类的构造器信息。这里通过 PersonProxy.getConstructor(InvocationHandler.class) 来获取 PersonProxy 类中,参数为 InvocationHandler.class 的构造器。getConstructor 方法会返回一个 Constructor 对象,它代表了对应的构造器。

通过构造器constructor来创建一个动态实例stuProxy

1
Person stuProxy = (Person) cons.newInstance(stuHandler);

Constructor 类的 newInstance 方法用于根据构造器来创建类的实例。这里传入了 stuHandler 作为参数,stuHandler 应该是实现了 InvocationHandler 接口的对象实例。它会调用之前获取到的构造器(要求参数为 InvocationHandler 类型 )来初始化一个 PersonProxy 实例,并将其强制转换为 Person 类型(前提是 PersonProxy 类实现了 Person 接口 )

一个动态代理对象就创建完毕,当然,上面四个步骤可以通过Proxy类的newProxyInstances方法来简化

1
2
3
4
//创建一个与代理对象相关联的InvocationHandler
InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);
//创建一个代理对象stuProxy,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
Person stuProxy= (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);

完整的动态代理例子

定义一个Person接口,里面定义一个抽象方法giveMoney();

1
2
3
4
public interface Person {
//上交班费
void giveMoney();
}

创建需要被代理的实际类 Student类,实现了Person接口,是被代理的目标对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}

@Override
public void giveMoney() {
try {
//假设数钱花了一秒时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "上交班费50元");
}
}

再定义一个检测方法执行时间的工具类,在任何方法执行前先调用start方法,执行后调用finsh方法,就可以计算出该方法的运行时间,这也是一个最简单的方法执行时间检测工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MonitorUtil {

private static ThreadLocal<Long> tl = new ThreadLocal<>();

public static void start() {
tl.set(System.currentTimeMillis());
}

//结束时打印耗时
public static void finish(String methodName) {
long finishTime = System.currentTimeMillis();
System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms");
}
}

创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例targetInvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。再在invoke方法中执行被代理对象target的相应方法。

所以说InvocationHandler 负责处理代理对象方法的调用,当调用代理对象的方法时,实际上会调用 InvocationHandlerinvoke 方法。

当然,在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。这也是Spring中的AOP实现的主要原理,这里还涉及到一个很重要的关于java反射方面的基础知识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StuInvocationHandler<T> implements InvocationHandler {
// invocationHandler持有的被代理对象
T target;

public StuInvocationHandler(T target) {
this.target = target;
}

/**
* proxy:代表动态代理对象
* method:代表正在执行的方法
* args:代表调用目标方法时传入的实参
*/
@Override // 实现 InvocationHandler 接口需要重写invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理执行" +method.getName() + "方法");
//代理过程中插入监测方法,计算该方法耗时
MonitorUtil.start();
Object result = method.invoke(target, args);
MonitorUtil.finish(method.getName());
return result;
}
}

我们使用简化的方式创建动态代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
public class ProxyTest {
public static void main(String[] args) {
//创建一个实例对象,这个对象是被代理的对象
Person zhangsan = new Student("张三");
//创建一个与代理对象相关联的InvocationHandler
InvocationHandler stuHandler = new StuInvocationHandler<Person>(zhangsan);
//创建一个代理对象stuProxy来代理张三,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
//代理执行上交班费的方法
stuProxy.giveMoney();
}
}

过程讲解:

stuHandler为代理对象相关联的InvocationHandler,将目标对象 张三 传递给 StuInvocationHandler 的构造函数。InvocationHandler 的作用是处理代理对象方法的调用,当调用代理对象的方法时,会自动调用 InvocationHandlerinvoke 方法

使用 Proxy.newProxyInstance 方法创建一个代理对象 stuProxy,需要传入类加载器,实现的接口,指定处理代理对象方法调用的InvocationHandle对象,代理对象的每个执行方法都会替换执行Invocation中的invoke方法

之后调用代理对象的 giveMoney 方法,实际上会调用 StuInvocationHandlerinvoke 方法,在 invoke 方法中会先调用 MonitorUtil.start() 记录开始时间,然后调用目标对象的 giveMoney 方法,最后调用 MonitorUtil.finish() 计算并打印方法执行的耗时。

img
img

动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了。

例如,这里的方法计时,所有的被代理对象执行的方法都会被计时,然而只做了很少的代码量。

动态代理的过程,代理对象和被代理对象的关系不像静态代理那样一目了然,清晰明了。

因为动态代理的过程中,我们并没有实际看到代理类,也没有很清晰地的看到代理类的具体样子,而且动态代理中被代理对象和代理对象是通过InvocationHandler来完成的代理过程的,其中具体是怎样操作的,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行,这就涉及到java动态代理的原理了

动态代理原理分析

Java动态代理创建出来的动态代理类

上面我们利用Proxy类的newProxyInstance方法创建了一个动态代理对象,查看该方法的源码,发现它只是封装了创建动态代理类的步骤(红色标准部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 返回一个指定接口的代理类实例,该实例会将方法调用分派到指定的调用处理器。
*
* @param loader 定义代理类的类加载器
* @param interfaces 代理类要实现的接口列表
* @param h 调度方法调用的调用处理器
* @return 一个实现了指定接口的代理类实例,它会将方法调用分派给指定的调用处理器
* @throws IllegalArgumentException 如果违反了对该方法参数的任何限制
*/
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// 检查调用处理器是否为 null,如果为 null 则抛出 NullPointerException
Objects.requireNonNull(h);

// 克隆接口数组,防止外部对原数组进行修改影响后续操作
final Class<?>[] intfs = interfaces.clone();
// 获取系统的安全管理器
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 如果存在安全管理器,检查调用者是否有创建代理类的权限
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* 查找或生成指定的代理类。
*/
// 通过类加载器和接口数组获取代理类的 Class 对象
Class<?> cl = getProxyClass0(loader, intfs);

/*
* 使用指定的调用处理器调用其构造函数。
*/
try {
if (sm != null) {
// 如果存在安全管理器,检查调用者是否有创建代理实例的权限
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

// 获取代理类的构造函数,该构造函数接收一个 InvocationHandler 类型的参数
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;
}
});
}
// 使用构造函数创建代理类的实例,并传入调用处理器
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
// 处理非法访问或实例化异常,抛出内部错误
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
// 处理构造函数调用抛出的异常
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
// 如果是运行时异常,直接抛出
throw (RuntimeException) t;
} else {
// 否则抛出内部错误
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
// 处理构造函数未找到的异常,抛出内部错误
throw new InternalError(e.toString(), e);
}
}
1
Class<?> cl = getProxyClass0(loader, intfs);
  • loader:这是一个 ClassLoader 对象,它指定了代理类应该使用哪个类加载器来加载。类加载器负责将类的字节码文件加载到 JVM 中,不同的类加载器可能会从不同的位置(如文件系统、网络等)加载类。通常,我们会使用被代理接口的类加载器,这样可以确保代理类和被代理接口在同一个类加载器的命名空间中,从而避免类加载的冲突。
  • intfs:这是一个 Class[] 数组,包含了代理类需要实现的接口列表。代理类会实现这些接口中定义的所有方法,当调用代理对象的这些方法时,实际上会将调用转发到 InvocationHandlerinvoke 方法中进行处理。

其实,我们最应该关注的是 Class<?> cl = getProxyClass0(loader, intfs);这句,这里产生了代理类,后面代码中的构造器也是通过这里产生的类来获得,可以看出,这个类的产生就是整个动态代理的关键,由于是动态生成的类文件,这个类文件时缓存在java虚拟机中的

  • 在 Java 动态代理中,Proxy.newProxyInstance 方法的核心目标是创建一个实现了指定接口的代理对象。而 getProxyClass0 方法就是用来生成这个代理对象所对应的代理类的。这个代理类是在运行时动态生成的,并且会被加载到 Java 虚拟机(JVM)中,后续的代理对象实例就是基于这个动态生成的类来创建的。

那么是如何动态生成代理类的:

  • getProxyClass0 方法会根据传入的类加载器和接口数组,在运行时动态生成一个代理类的字节码。这个代理类会实现 intfs 数组中指定的所有接口,并且会重写这些接口中的方法。在重写的方法中,会调用 InvocationHandlerinvoke 方法,从而实现对目标方法的增强。
  • 这其中有一个缓存机制,为了提高性能,Java 虚拟机对动态生成的代理类采用了缓存机制。也就是说,如果已经为相同的类加载器和接口列表生成过代理类,那么 getProxyClass0 方法会直接从缓存中获取这个代理类,而不是再次生成。这样可以避免重复生成相同的代理类,减少开销。

之后对这个class文件反编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.Person;

// $Proxy0 是动态生成的代理类,它继承自 Proxy 类并实现了 Person 接口。Proxy 类是 Java 中所有代理类的基类
// 它持有一个 InvocationHandler 类型的成员变量 h,用于处理方法调用
public final class $Proxy0 extends Proxy implements Person
{
// 这些静态变量用于存储通过反射获取的方法对象。m3 对应 Person 接口的 giveMoney 方法,m0、m1、m2 分别对应 Object 类的 hashCode、equals 和 toString 方法。
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

/**
*注意这里是生成代理类的构造方法,方法参数为InvocationHandler类型,代理类的构造方法接收一个 InvocationHandler 类型的参数,并调用父类 Proxy 的构造方法将其传递给父类的 h 成员变量。这意味着代理类持有一个 InvocationHandler 对象,后续的方法调用将由该对象处理。
*
*super(paramInvocationHandler),是调用父类Proxy的构造方法。
*父类持有:protected InvocationHandler h;
*Proxy构造方法:
* protected Proxy(InvocationHandler h) {
* Objects.requireNonNull(h);
* this.h = h;
* }
*
*/
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}

//这个静态块本来是在最后的,我把它拿到前面来,方便描述
static
{
try
{
//看看这儿静态块儿里面有什么,是不是找到了giveMoney方法。请记住giveMoney通过反射得到的名字m3,其他的先不管
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("proxy.Person").getMethod("giveMoney", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}

/**
*
*当调用代理对象的 giveMoney 方法时,实际上会调用 InvocationHandler 对象的 invoke 方法,并将代理对象本身(this)、giveMoney 方法的 Method 对象(m3)和方法参数(这里为 null)作为参数传递给 invoke 方法。
*this.h.invoke(this, m3, null);这里简单,明了。
*InvocationHandler 对象持有被代理对象的引用,在 invoke 方法中可以调用被代理对象的相应方法,并可以在方法调用前后添加额外的逻辑,从而实现对被代理对象方法的增强。
*/
public final void giveMoney()
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}

//注意,这里为了节省篇幅,省去了toString,hashCode、equals方法的内容。原理和giveMoney方法一毛一样。

}

jdk为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。

我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。

代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。

引用

[java动态代理实现与原理详细分析

Java动态代理详细讲解-使用方式及应用场景