baby sword‘s blog baby sword‘s blog
首页
  • java基础
  • java进阶
大数据
  • mysql

    • mysql索引
    • mysql日志
  • redis

    • 单机下的redis
    • 集群下的redis
  • Spring
  • springboot
  • RPC
  • netty
  • mybatis
  • maven
  • 消息队列
  • kafka
  • zookeeper
  • rocketmq
  • 七大设计原则
  • 创建型模式
  • 结构型模式
  • 行为型模式
  • SpringCloud

    • eureka
  • SpringCloud Alibaba

    • nacos
  • 计算机网络
  • 操作系统
  • 算法
  • 个人项目
  • 个人面试面经
  • 八股记忆
  • 工作积累
  • 逻辑题
  • 面试

    • 百度后端实习二面
GitHub (opens new window)

zhengjian

不敢承担失去的风险,是不可能抓住梦想的
首页
  • java基础
  • java进阶
大数据
  • mysql

    • mysql索引
    • mysql日志
  • redis

    • 单机下的redis
    • 集群下的redis
  • Spring
  • springboot
  • RPC
  • netty
  • mybatis
  • maven
  • 消息队列
  • kafka
  • zookeeper
  • rocketmq
  • 七大设计原则
  • 创建型模式
  • 结构型模式
  • 行为型模式
  • SpringCloud

    • eureka
  • SpringCloud Alibaba

    • nacos
  • 计算机网络
  • 操作系统
  • 算法
  • 个人项目
  • 个人面试面经
  • 八股记忆
  • 工作积累
  • 逻辑题
  • 面试

    • 百度后端实习二面
GitHub (opens new window)
  • 设计模式七大原则
  • 创建型模式

  • 结构型模式

    • 代理模式
      • 介绍
      • 结构
      • 模式实现
        • 静态代理
        • 动态代理
        • JDK动态代理(基于接口)
        • CGLIB动态代理
    • 适配器模式
    • 装饰器模式
    • 桥接模式
  • 行为型模式

  • UML

  • 设计模式
  • 结构型模式
xugaoyi
2022-12-31
目录

代理模式

# 介绍

代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

考虑生活中一个常见的例子,客户想买房,房东有很多房,提供卖房服务,但房东不会带客户看房,于是客户通过中介买房。

# 结构

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面来分析其基本结构。

代理模式的主要角色如下。

  1. 抽象主题(Subject)类(业务接口类):通过接口或抽象类声明真实主题和代理对象实现的业务方法,服务端需要实现该方法。
  2. 真实主题(Real Subject)类(业务实现类):实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  3. 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

image-20230630085504544

在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。

# 模式实现

根据代理的创建时期,代理模式分为静态代理和动态代理。

  • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。(即程序员自己可以写出来的代码)
  • 动态:在程序运行时,运用反射机制动态创建而成。(由JVM自行生成,程序员无法写出源代码)

# 静态代理

静态代理服务于单个接口,我们来考虑实际工程中的一个例子,现在已经有业务代码实现一个增删功能,原有的业务代码由于仍有大量程序无法改变,现在新增需求,即以后每执行一个方法输出一个日志。

我们不改变原有代码而添加一个代理来实现:

//业务接口
interface DateService {
    void add();
    void del();
}

class DateServiceImplA implements DateService {
    @Override
    public void add() {
        System.out.println("成功添加!");
    }

    @Override
    public void del() {
        System.out.println("成功删除!");
    }
}

class DateServiceProxy implements DateService {
    DateServiceImplA server = new DateServiceImplA();

    @Override
    public void add() {
        server.add();
        System.out.println("程序执行add方法,记录日志.");
    }
    @Override
    public void del() {
        server.del();
        System.out.println("程序执行del方法,记录日志.");
    }
}

//客户端
public class Test {
    public static void main(String[] args) {
        DateService service = new DateServiceProxy();
        service.add();
        service.del();
    }
}

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

现在,我么成功的在不改变程序原有代码的情况下,扩展了一些功能!

我们来思考一下这种情况,当原有的业务处理由于某种原因无法改变,而目前又需要扩展一些功能,此时可以通过代理模式实现:

image-20230630085515866

如上图所示,我们原有的业务十分庞大,牵一发而动全身,难以修改,而现在需要扩展一些功能,这里就需要代理模式实现,在纵向代码之间,横向扩展一些功能,这也是所谓的面向切面编程。

如果你设计思想比较良好的话,你很快就能发现上面代码的不足:一个代理只能服务于一个特定的业务实现类,假设我们又另外一个类也实现了业务接口,即class DateServiceImplB implements DateService,发现想要扩展该类必须要为其也编写一个代理,扩展性极低。想要解决这个问题也是很简单的,我们面向接口编程而不是面向实现,我们给代理类持有接口而不是持有具体的类:

class DateServiceProxy implements DateService {
    DateService server;

    public DateServiceProxy(DateService server) {
        this.server = server;
    }
}
1
2
3
4
5
6
7

这样一个代理就可以同时代理多个实现了同一个业务接口的业务,但这种方式必须要求客户端传入一个具体的实现类,这样客户就必须要获得具体目标对象实例,目标对象就直接暴露在访问对象面前了,对于某些情况这是不可接受的,例如你想获得某资源,但需要一定的权限,这时由代理控制你对目标资源对象的访问,不能由你直接区去访问,这是代理就必须将目标资源对象牢牢的控制在自己手中,**后面会讲到这其实就是保护代理。**但在这里,这种方法是可以接受的,并且带给程序较高的灵活性。

# 动态代理

我们为什么需要动态代理?要理解这一点,我们必须要知道静态代理有什么不好,要实现静态代理,我们必须要提前将代理类硬编码在程序中,这是固定死的,上面也提到过,有一些代理一个代理就必须要负责一个类,这种情况下代理类的数量可能是非常多的,但我们真的每个代理都会用上吗?例如,在普通的项目中,可能99%的时间都仅仅只是简单的查询,而不会设计到增删功能,此时是不需要我们的增删代理类的,但在静态代理中,我们仍然必须硬编码代理类,这就造成了不必要的资源浪费并且增加了代码量。

动态代理可以帮助我们仅仅在需要的时候再创建代理类,减少资源浪费,此外由于动态代理是一个模板的形式,也可以减少程序的代码量,例如在静态代码示例中,我们在每个方法中加入System.out.println("程序执行***方法,记录日志.");,当业务方法非常多时,我们也得为每个业务方法加上记录日志的语句,而动态代理中将方法统一管理,无论几个业务方法都只需要一条记录语句即可实现,具体请看代码。

# JDK动态代理(基于接口)

动态代理采用反射的机制,在运行时创建一个接口类的实例。在JDK的实现中,我们需要借助Proxy类和InvocationHandler接口类。

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个类去实现InvocationHandler接口,这个接口下有一个invoke(Object proxy, Method method, Object[] args)方法,它负责调用对应接口的接口方法;

    调用代理类的方法时,处理程序会利用反射,将代理类、代理类的方法、要调用代理类的参数传入这个函数,并运行这个函数,这个函数是实际运行的,我们在这里编写代理的核心代码。

  2. 通过Proxy.newProxyInstance()创建某个interface实例,它需要3个参数:

    1. 使用的ClassLoader,通常就是接口类的ClassLoader;
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 一个处理程序的接口。

    这个方法返回一个代理类$Proxy0,它有三个参数,第一个通常是类本身的ClassLoader,第二个是该类要实现的接口,例如这里我们要实现增删接口,第三个是一个处理程序接口,即调用这个类的方法时,这个类的方法会被委托给该处理程序,该处理程序做一些处理,这里对应了上面这个方法,通常设置为this。

  3. 将返回的Object强制转型为接口。

image-20230630085524649

来看一下具体实现:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//业务接口
interface DateService {
    void add();
    void del();
}

class DateServiceImplA implements DateService {
    @Override
    public void add() {
        System.out.println("成功添加!");
    }

    @Override
    public void del() {
        System.out.println("成功删除!");
    }
}

class ProxyInvocationHandler implements InvocationHandler {
    private DateService service;

    public ProxyInvocationHandler(DateService service) {
        this.service = service;
    }

    public Object getDateServiceProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        var result = method.invoke(service, args); // 让service调用方法,方法返回值
        System.out.println(proxy.getClass().getName() + "代理类执行" + method.getName() + "方法,返回" + result +  ",记录日志!");
        return result;
    }
}

//客户端
public class Test {
    public static void main(String[] args) {
        DateService serviceA = new DateServiceImplA();
        DateService serviceProxy = (DateService) new ProxyInvocationHandler(serviceA).getDateServiceProxy();
        serviceProxy.add();
        serviceProxy.del();
    }
}
/*
成功添加!
$Proxy0代理类执行add方法,返回null,记录日志!
成功删除!
$Proxy0代理类执行del方法,返回null,记录日志!
*/
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

我们代理类是通过Proxy.newProxyInstance(this.getClass().getClassLoader(),service.getClass().getInterfaces(), this);方法得到的,这个方法中,第二个参数我们传入了类service的接口部分,即DateService,在底层通过该接口的字节码帮我们创建一个新类$Proxy0,该类具有接口的全部方法。第三个参数是一个处理程序接口,此处传入this即表明将方法交给ProxyInvocationHandler 的接口即InvocationHandler的invoke方法执行。

$Proxy并不具备真正处理的能力,当我们调用$$Proxy0.add()时,会陷入invoke处理程序,这是我们编写核心代码的地方,在这里var result = method.invoke(service, args);调用目标对象的方法,我们可以编写代理的核心代码。

我们还可以编写一个更加万能的接口,让其能扩展不同的业务接口,在静态代理中,如果要扩展两个接口我们最少要编写两个代理类,尽管这两个代理类的代码是一样的,通过一个向上转型,动态代理可以更好的实现这一功能,能够极大的较少代码量。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//业务接口
interface DateService {
    void add();
    void del();
}

class DateServiceImplA implements DateService {
    @Override
    public void add() {
        System.out.println("成功添加!");
    }

    @Override
    public void del() {
        System.out.println("成功删除!");
    }
}


interface OperateService {
    void plus();
    void subtract();
}

class OperateServiceImplA implements OperateService {
    @Override
    public void plus() {
        System.out.println("+ 操作");
    }

    @Override
    public void subtract() {
        System.out.println("- 操作");
    }
}

//万能的模板
class ProxyInvocationHandler implements InvocationHandler {
    private Object service;

    public ProxyInvocationHandler(Object service) {
        this.service = service;
    }

    public Object getDateServiceProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        var result = method.invoke(service, args); // 方法返回值
        System.out.println(proxy.getClass().getName() + "代理类执行" + method.getName() + "方法,返回" + result +  ",记录日志!");
        return result;
    }
}

//客户端
public class Test {
    public static void main(String[] args) {
        DateService dateServiceA = new DateServiceImplA();
        DateService dateServiceProxy = (DateService) new ProxyInvocationHandler(dateServiceA).getDateServiceProxy();
        dateServiceProxy.add();
        dateServiceProxy.del();

        OperateService operateServiceA = new OperateServiceImplA();
        OperateService operateServiceProxy = (OperateService) new ProxyInvocationHandler(operateServiceA).getDateServiceProxy();
        operateServiceProxy.plus();
        operateServiceProxy.subtract();
    }
}
/*
成功添加!
$Proxy0代理类执行add方法,返回null,记录日志!
成功删除!
$Proxy0代理类执行del方法,返回null,记录日志!
+ 操作
$Proxy1代理类执行plus方法,返回null,记录日志!
- 操作
$Proxy1代理类执行subtract方法,返回null,记录日志!
*/
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
84

# CGLIB动态代理

CGLIB (opens new window)(Code Generation Library)是一个基于ASM (opens new window)的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB通过继承方式实现代理。

来看示例,假设我们有一个没有实现任何接口的类HelloConcrete:

public class HelloConcrete {
	public String sayHello(String str) {
		return "HelloConcrete: " + str;
	}
}

1
2
3
4
5
6

因为没有实现接口该类无法使用JDK代理,通过CGLIB代理实现如下:

  1. 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
  2. 然后在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象。
// CGLIB动态代理
// 1. 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
class MyMethodInterceptor implements MethodInterceptor{
  ...
	@Override
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		logger.info("You said: " + Arrays.toString(args));
		return proxy.invokeSuper(obj, args);
	}
}
// 2. 然后在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行上述代码输出结果:

日志信息: You said: [I love you!]
HelloConcrete: I love you!
1
2

上述代码中,我们通过CGLIB的Enhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是HelloConcrete的具体方法。CGLIG中MethodInterceptor (opens new window)的作用跟JDK代理中的InvocationHandler很类似,都是方法调用的中转站。

注意:对于从Object中继承的方法,CGLIB代理也会进行代理,如hashCode()、equals()、toString()等,但是getClass()、wait()等方法不会,因为它是final方法,CGLIB无法代理。

如果对CGLIB代理之后的对象类型进行深挖,可以看到如下信息:

# HelloConcrete代理对象的类型信息
class=class cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52
superClass=class lh.HelloConcrete
interfaces: 
interface net.sf.cglib.proxy.Factory
invocationHandler=not java proxy class
1
2
3
4
5
6

我们看到使用CGLIB代理之后的对象类型是cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52,这是CGLIB动态生成的类型;父类是HelloConcrete,印证了CGLIB是通过继承实现代理;同时实现了net.sf.cglib.proxy.Factory接口,这个接口是CGLIB自己加入的,包含一些工具方法。

注意,既然是继承就不得不考虑final的问题。我们知道final类型不能有子类,所以CGLIB不能代理final类型,遇到这种情况会抛出类似如下异常:

java.lang.IllegalArgumentException: Cannot subclass final class cglib.HelloConcrete
1

同样的,final方法是不能重载的,所以也不能通过CGLIB代理,遇到这种情况不会抛异常,而是会跳过final方法只代理其他方法。

如果你还对代理类cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52具体实现感兴趣,它大致长这个样子:

// CGLIB代理类具体实现
public class HelloConcrete$$EnhancerByCGLIB$$e3734e52
  extends HelloConcrete
  implements Factory
{
  ...
  private MethodInterceptor CGLIB$CALLBACK_0; // ~~
  ...
  
  public final String sayHello(String paramString)
  {
    ...
    MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;
    if (tmp17_14 != null) {
	  // 将请求转发给MethodInterceptor.intercept()方法。
      return (String)tmp17_14.intercept(this, 
              CGLIB$sayHello$0$Method, 
              new Object[] { paramString }, 
              CGLIB$sayHello$0$Proxy);
    }
    return super.sayHello(paramString);
  }
  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

上述代码我们看到,当调用代理对象的sayHello()方法时,首先会尝试转发给MethodInterceptor.intercept()方法,如果没有MethodInterceptor就执行父类的sayHello()。这些逻辑没什么复杂之处,但是他们是在运行时动态产生的,无需我们手动编写。如何获取CGLIB代理类字节码可参考[Access the generated byte] array directly (opens new window)。

## 代理模式优缺点

代理模式的主要优点有:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性;

其主要缺点是:

  • 静态代理模式会造成系统设计中类的数量增加,但动态代理可以解决这个问题;
  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
  • 增加了系统的复杂度
编辑 (opens new window)
上次更新: 2024/02/22, 14:03:19
原型模式
适配器模式

← 原型模式 适配器模式→

最近更新
01
spark基础
02-22
02
mysql读写分离和分库分表
02-22
03
数据库迁移
02-22
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式