概念

代理模式(Proxy)是一种设计模式,提供了对目标对象另外的访问方式:即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能,例:统计,log 或对参数进行优化,更改。

这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需要修改,可以通过代理的方式来扩展该方法。

举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接联系明星,而是联系明星的经纪人,来达到同样的目的。明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决。这就是代理思想在现实中的一个例子。

图片示例:
20200815215421

代理模式的关键点是:代理对象与目标对象。代理对象是对目标对象的扩展,并会调用目标对象。

代理模式的使用场景

当无法或不想直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问,为了保证客户端使用的透明性,委托对象与代理对象需要实现同一个接口。

三种代理模式

Java 代理分为静态代理和动态代理和 Cglib 代理。

静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类

例:模拟文件操作:
定义一个文件操作的接口:IUserDao.java,然后目标对象实现这个接口的方法:UserDao.java,此时如果使用静态代理方式,就需要在代理对象(UserDaoProxy.java)中也实现 IUserDao 接口。调用的时候通过调用代理对象的方法来调用目标对象。
需要注意的是,代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法。

  • 1 个静态代理只服务 1 种类型的目标对象。
  • 若要服务多类型的目标对象,则需要为每种目标对象都实现一个静态代理对象。

静态代理代码示例

1
2
3
4
// IUserDao接口
public interface IUserDao {
void operate();
}
1
2
3
4
5
6
7
// 目标对象:UserDao
public class UserDao implements IUserDao {
@Override
public void operate() {
System.out.println("----保存数据!----");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 静态代理对象:UserDaoProxy
public class UserDaoProxy implements IUserDao{
//接收保存目标对象
private IUserDao target;
public UserDaoProxy(IUserDao target){
this.target=target;
}

public void operate() {
System.out.println("开始事务.");
target.operate();//执行目标对象的方法
System.out.println("提交事务.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 测试类
public class App {
public static void main(String[] args) {
//目标对象
UserDao target = new UserDao();

//代理对象,把目标对象传给代理对象,建立代理关系
UserDaoProxy proxy = new UserDaoProxy(target);

//执行的是代理的方法
proxy.operate();
}
}

静态代理总结

优点:

  • 协调调用者和被调用者,降低了系统的耦合度。
  • 可以做到在不修改目标对象的功能前提下,对目标功能扩展。

缺点:

  • 由于在客户端和真实主题之间增加了代理对象,因此会造成请求的处理速度变慢。
  • 在目标对象较多的情况下(或代理实现复杂),若采用静态代理,则会出现静态代理对象量多、代码量大,从而导致代码复杂的问题。

动态代理

为解决静态代理对象必须实现接口的问题,Java 给出了动态代理。

特点:

  • 设计动态代理类(DynamicProxy)时,不需要显式实现与目标对象类(RealSubject)相同的接口,而是将这种实现推迟到程序运行时在内存中由 JVM 来实现。
  • 通过 Java 反射机制的method.invoke(),通过调用动态代理类对象方法,从而自动调用目标对象的方法。

动态代理应用场景

  • 基于静态代理应用场景下,需要代理对象数量较多的情况下使用动态代理。
  • AOP 领域(切面编程)。

动态代理与静态代理区别

类型 代码创建&绑定时机 原理 效率 具体使用
静态代理 代码运行前
在代理类实现时就指定与目标对象类相同的接口
/ / 代理单一目标对象
动态代理 运行时
不需要显式实现与对象类相同的接口
反射 代理多个对象

动态代理代码示例

模拟文件操作:
文件操作接口IUserDao.java,实现类UserDao1保存文件,另一个实现类UserDao2下载文件。

步骤:

  1. 声明调用处理器类
  2. 声明目标对象类的抽象接口
  3. 声明目标对象类
  4. 通过动态代理对象,调用目标对象的方法
1. 声明调用处理器类 DynamicProxy.java
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
// 作用:
// 1. 生成动态代理对象
// 2. 指定 代理对象运行目标对象方法时需要完成的 具体任务
// 注:需实现InvocationHandler接口 = 调用处理器 接口
// 所以称为 调用处理器类

public class DynamicProxy implements InvocationHandler {

// 声明代理对象
// 作用:绑定关系,即关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke()
private Object ProxyObject;

public Object newProxyInstance(Object ProxyObject){
this.ProxyObject =ProxyObject;
return Proxy.newProxyInstance(ProxyObject.getClass().getClassLoader(),
ProxyObject.getClass().getInterfaces(),this);
// Proxy类 = 动态代理类的主类
// Proxy.newProxyInstance()作用:根据指定的类装载器、一组接口 & 调用处理器 生成动态代理类实例,并最终返回
// 参数说明:
// 参数1:指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器
// 参数2:指定目标对象的实现接口
// 即要给目标对象提供一组什么接口.若提供了一组接口给它,那么该代理对象就默认实现了该接口,这样就能调用这组接口中的方法
// 参数3:指定InvocationHandler对象.即动态代理对象在调用方法时,会关联到哪个InvocationHandler对象

}

// 复写InvocationHandler接口的invoke()
// 动态代理对象调用目标对象的任何方法前,都会调用调用处理器类的invoke()
@Override
public Object invoke(Object proxy, Method method, Object[] args)
// 参数说明:
// 参数1:动态代理对象(即哪个动态代理对象调用了method()
// 参数2:目标对象被调用的方法
// 参数3:指定被调用方法的参数
throws Throwable {
System.out.println("开始操作数据");
Object result = null;
before();
// 通过Java反射机制调用目标对象方法
result = method.invoke(ProxyObject, args);
after();
return result;
}
void before() {
System.out.println("开始事务.");
}

void after() {
System.out.println("结束事务.");

}
}
2. 声明目标对象抽象接口
1
2
3
public interface IUserDao {
void operate();
}
3. 声明目标对象类

UserDao1.java

1
2
3
4
5
6
public class UserDao1 implements IUserDao {
@Override
public void operate() {
System.out.println("----保存数据!----");
}
}

UserDao2.java

1
2
3
4
5
6
public class UserDao2 implements IUserDao {
@Override
public void operate() {
System.out.println("----下载数据!----");
}
}
4. 通过动态代理对象,调用目标对象的方法
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 App  {
public static void main(String[] args) {
// 1. 创建调用处理器类对象
DynamicProxy DynamicProxy = new DynamicProxy();

// 2. 创建目标对象的对象
IUserDao target1 = new UserDao1();

// 3. 创建动态代理类 & 对象:通过调用处理器类对象newProxyInstance()
// 传入上述目标对象的对象
IUserDao target1DynamicProxy = (IUserDao) DynamicProxy.newProxyInstance(target1);

// 4. 通过调用动态代理对象方法从而调用目标对象方法
// 实际上是调用了invoke(),再通过invoke()里的反射机制调用目标对象的方法
target1DynamicProxy.operate();
// 以上为UserDao1保存数据

// 以下为UserDao2下载数据
IUserDao target2 = new UserDao2();
IUserDao target2DynamicProxy = (IUserDao) DynamicProxy.newProxyInstance(target2);
target2DynamicProxy.operate();

}
}

具体原理参考代理模式(静态代理 and 动态代理)

动态代理优缺点

优点:

  • 只需要 1 个动态代理类就可以解决创建多个静态代理的问题,避免重复、多余代码更强的灵活性。

缺点:

  • 效率低
    相比静态代理中 直接调用目标对象方法,动态代理则需要先通过 java 反射机制 从而 间接调用目标对象方法。
  • 应用场景局限
    因为 Java 的单继承特性(每个代理类都继承了 Proxy 类),即只能针对接口创建代理类,不能针对类创建代理类。

    即只能动态代理实现了接口的类

Cglib 代理

上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib 代理。

Cglib 代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。

  • JDK 的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用 Cglib 实现。
  • Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口.它广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 synaop,为他们提供方法的 interception(拦截)。
  • Cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM 来转换字节码并生成新的类.不鼓励直接使用 ASM,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。

Cglib 子类代理实现方法:

  1. 需要引入 cglib 的 jar 文件,但是 Spring 的核心包中已经包括了 Cglib 功能,所以直接引入 pring-core 依赖即可。

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.2.8.RELEASE</version>
    </dependency>
  2. 引入功能包后,就可以在内存中动态构建子类。

  3. 代理的类不能为 final,否则报错。

  4. 目标对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法。

Cglib 代理代码示例

1
2
3
4
5
6
// 目标对象类UserDao
public class UserDao {
public void operate() {
System.out.println("----保存数据!----");
}
}
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
// Cglib代理工厂
public class ProxyFactory implements MethodInterceptor{
//维护目标对象
private Object target;

public ProxyFactory(Object target) {
this.target = target;
}

//给目标对象创建一个代理对象
public Object getProxyInstance(){
//1.工具类
Enhancer en = new Enhancer();
//2.设置父类
en.setSuperclass(target.getClass());
//3.设置回调函数
en.setCallback(this);
//4.创建子类(代理对象)
return en.create();

}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("开始事务...");

//执行目标对象的方法
Object returnValue = method.invoke(target, args);

System.out.println("提交事务...");

return returnValue;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 测试类
public class App {
public static void main(String[] args) {
// 目标对象
UserDao target = new UserDao();

// 代理对象
UserDao proxy = (UserDao)new ProxyFactory(target).getProxyInstance();

// 执行代理对象的方法
proxy.operate();
}
}

在 Spring 的 AOP 编程中:

  • 如果加入容器的目标对象有实现接口,用 JDK 代理。
  • 如果目标对象没有实现接口,用 Cglib 代理。

Spring 也可以通过<aop:config proxy-target-class="true">强制使用 Cglib 代理,使用 Java 字节码编辑类库 ASM 操作字节码来实现,直接以二进制形式动态地生成 stub 类或其他代理类,性能比 JDK 更强。

参考

对象克隆