设计模式(三):代理模式

概述

在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。

在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。

策略模式旨在不改变被代理对象的基础上进行方法增强。

当我们一个原有的类不能满足功能,又处于职责或其他原因不便修改的时候,我们可以使用代理模式。通过代理类去调用被代理类的方法,在原方法的基础上进行各种加强,又不会改变原代码。

spring 中的 AOP 就运用了代理模式。

一、静态代理

为了简单的说明代理模式,我们举个例子:

首先有一个接口,叫做购房者,然后这个类地下有一个实现类,叫做小明:

1
2
3
4
5
6
7
8
9
10
11
12
13
//购房者接口
public interface Buyers {
// 买房
void buy();
}

//小明实现了购房者接口
public class XiaoMing implements Buyers {
@Override
public void buy() {
System.out.println("小明买了一套房");
}
}

买房子还要货比三家,小明没时间,于是就把买房子这件事情委托给中介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Intermediary implements Buyers {

private Buyers xiaoMing ;

// 小明把权限委托给了中介
public IntermediaryProxy(Buyers xiaoMing) {
this.xiaoMing = xiaoMing;
}

@Override
public void buy() {
// 在这基础上又添加了挑选方法
System.out.println("房中介先挑了最合适的房");

// 中介让XiaoMing这个类执行了购房的动作
xiaoMing.buy();

// 在这基础上又添加了抽成方法
System.out.println("买完房中介要那点抽成");
}
}

小明买房(静态代理)

在这个例子里:

  • 被代理对象是小明这个类,是我们最终要访问的;
  • 代理对象就是中介这个类,他是被代理对象的加强,我们通过代理对象去访问目标对象,在这基础上对其进行加强。

为了做到代理的效果,代理对象和目标对象需要实现同一个接口或者继承同一个类,以达到通过代理对象的方法去调用被代理对象的方法。这种方法叫做静态代理

二、动态代理

通过静态代理,我们可以在不改变代理类代码的基础上进行方法增强,但是如果有很多这样的代理行为,那么就需要很多代理类或者代理类去实现很多接口的方法,这样做显然会来带麻烦,为此,我们可以选择只在需要的时候才生产代理对象,这就是动态代理。

针对动态代理,有两种实现方法:

  • JDK 代理,通过 JDK 的 API 实现;
  • CGLib 代理,通过 CGLib 包实现。

1. JDK代理

JDK 代理仍然需要被代理去实现某个接口,因为 JDK 代理的方式本质仍然还是生成一个与被代理类继承同一个接口的类,去代替被代理类的方法提供调用,但是和静态代理不同,这个代理对象由代码在调用的时候动态生成。

我们可以先看看实现代理效果的核心方法Proxy.newProxyInstance()的注释:

返回指定接口的代理类的实例,该实例将方法调用分派到指定的调用处理程序。

参数:

ClassLoader loader:指定加载被代理对象的类加载器

Class<?>[] interfaces:被代理对象实现的接口的类型,使用泛型方式确认类型

InvocationHandler h:事件处理,执行被代理对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入

我们需要提供三个参数,被代理类的类的加载器,被代理类实现的结构,一个代理方法要实现的函数式接口。

对于函数式接口 InvocationHandler,我们不妨再看看他的参数:

proxy:在方法上调用方法的代理实例method-与在代理实例上调用的接口方法相对应的Method实例。

Method:对象的声明类将是在其中声明该方法的接口,它可能是代理类通过其继承该方法的代理接口的超接口。

args :包含在代理实例的方法调用中传递的参数值的对象数组;如果接口方法不带参数,则为null。基本类型的参数包装在适当的基本包装器类的实例中,例如java.lang.Integer或java.lang.Boolean。

可以说非常直观了,下面我们来简单实现一下:

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
public static void main( String[] args ) {
XiaoMing xiaoMing = new XiaoMing();
//获取代理对象
Buyers proxyBuyers = (Buyers) Proxy.newProxyInstance(
//获取被代理对象的类加载器
xiaoMing.getClass().getClassLoader(),
//获取被代理对象实现的接口
xiaoMing.getClass().getInterfaces(),
//让代理对象实现被代理对象实现的接口
(proxy, method, params) -> {
//只有对buy方法进行加强,如果不是buy方法就直接执行被代理对象的方法
if (method.getName() != "buy") {
return method.invoke(xiaoMing, params);
}
System.out.println("中介帮小明找到了最合适的房源");
method.invoke(xiaoMing, params);
System.out.println("中介抽了一笔中介费");
return null;
}
);
//通过代理对象买房
proxyBuyers.buy();
}

//输出结果
中介帮小明找到了最合适的房源
小明买了一套房
中介抽了一笔中介费

2. CGLib 代理

对于动态代理,我们可以通过JDK的Proxy.newProxyInstance()方法动态的生成一个实现了被代理类指定接口的代理类,那么,如果被代理类没有实现任何接口,那我们该怎么办呢?答案就是使用 CGLib 代理。

CGLib 代理的原理就是在内存里创建一个被代理类的对象的子类,然后重写父类要加强的方法,而无需加强的方法就直接使用父类方法。

要使用 CGLib 代理,需要引入 jar 包 ( Spring 的动态代理就是基于 CGLib 实现的,所以 Spring 的依赖里默认就会引入)

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

然后我们实现一下:

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
XiaoMing xiaoMing = new XiaoMing();
//获取代理对象
Buyers proxyBuyers = (Buyers) Enhancer.create(
//获取被代理对象的类
xiaoMing.getClass(),
//实现代理逻辑切入类

new MethodInterceptor() {
/**
*
* @param o 代理类对象
* @param method 要拦截的被代理类的方法
* @param objects 被代理类的方法参数
* @param methodProxy 要触发父类的方法对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (method.getName() != "buy") {
//注意,这里参数是o,调用的是invokeSuper
return methodProxy.invokeSuper(o, objects);
}
System.out.println("中介帮小明找到了最合适的房源");
//注意,这里参数是xiaoMing,调用的是invoke
methodProxy.invoke(xiaoMing, objects);
System.out.println("中介抽了一笔中介费");
return null;
}
}
);

// 调用被代理对象
proxyBuyers.buy();

一般为了方便使用,我们会将代理过程整合到一个代理工厂类中。

由于需要生成子类,所以要被代理的类最好不能用 final 修饰

如何选择代理模式

  • JDK 在创建代理对象时的性能要高于 CGLib 代理,而生成代理对象的运行性能却比 CGLib 的低。
  • 如果是单例的代理,推荐使用 CGLib

也就是说,如果需要频繁的创建代理对象(多例),那么就该选择JDK代理,否则(单例)使用 CGLib 代理

四、总结

对于两种代理模式:

  • 静态代理:被代理类和代理类需要实现相同接口,通过组合的方式,在代理类实例化时将被代理类作为参数传入;
  • 动态代理:通过 JDK 自带的 Proxy.newProxyInstance,在需要的时候动态生成代理类,解决了静态代理需要创建大量代理类的缺点;
  • CGLib 代理:通过 CGLib 动态生成被代理类的子类来实现代理,解决了动态代理还需要被代理类去实现接口的缺点。

对于动态代理中 JDK 代理和 CGLib 代理的选择:

  • 需要频繁的创建代理对象选择 JDK 代理
0%