1. 结构型模式⭐️🔴

结构型模式用于描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式分为以下 7 种:

  • 代理模式(Proxy)
  • 适配器模式(Adapter)
  • 装饰者模式(Decorator)
  • 桥接模式(Bridge)
  • 外观模式(Facade)
  • 组合模式(Composite)
  • 享元模式(Flyweight)
    %%
    2102-🏡⭐️◼️结构型设计模式:代理、适配器、装饰、桥接、外观、组合、享元◼️⭐️-point-202301232220%%

2. 代理模式

2.1. 场景概述

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

原因包括:需要安全保护的对象;需要功能增强的对象
ps:功能增强也可以使用装饰者模式,可以动态的插拔能力

Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。

2.2. 结构

代理(Proxy)模式分为三种角色:

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

3. 静态代理

3.1. 实现方法

【例】火车站卖票

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的==代理模式,火车==站是目标对象,代售点是代理对象。类图如下:

代码如下:

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
//卖票接口
public interface SellTickets {
void sell();
}

//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {

public void sell() {
System.out.println("火车站卖票");
}
}

//代售点
public class ProxyPoint implements SellTickets {

private TrainStation station = new TrainStation();

public void sell() {
System.out.println("代理点收取一些服务费用");
station.sell();
}
}

//测试类
public class Client {
public static void main(String[] args) {
ProxyPoint pp = new ProxyPoint();
pp.sell();
}
}

从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。

3.2. 静态代理优缺点⭐️🔴

优点:在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展
缺点:

  1. 必须基于接口
  2. 一旦接口增加方法,目标对象与代理对象都要维护
  3. 基于单一职责原则和接口隔离原则,一个代理类最好固定代理某一类接口。如果需要代理其他接口,就必须重新编写一个代理类%%
    1209-🏡⭐️◼️静态代理的缺点?如果接口新增方法,则代理类和被代理类都需要修改;基于单一职责原则和接口隔离原则,一个代理类只能代理一个接口,如果代理其他接口,则需要新增一个代理类◼️⭐️-point-202301260902%%

4. JDK动态代理

4.1. 代理过程

[[02_尚硅谷大数据技术之Spring(老师原版).docx]]

JAVA大数据集合:
https://www.bilibili.com/video/av58225341?p=261

由代理对象决定是否、何时 将方法调用转到原始对象上

image-20200404161821542

4.2. 使用方法

资料来源:基本原理-2、多态与代理
示例代码:spring02: [[ArithmeticCalculatorProxy.java]]

[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/design_patterns/src/main/java/com/itheima/pattern/proxy/jdk_proxy/ProxyFactory.java]]

4.2.1. 生成代理对象⭐️🔴

有2种方式:

4.2.1.1. 先生成代理类的Class对象

Proxy.getProxyClass(loader, interfaces)获取到Class后再获取Constructor,然后通过constructor.newInstance获取代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Class proxyClass = Proxy.getProxyClass(loader, interfaces);  

//Class 创建对象? newInstance()

Constructor con =
proxyClass.getDeclaredConstructor(InvocationHandler.class);
proxy = con.newInstance(new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//将方法的调用转回到目标对象上.

//获取方法的名字
String methodName = method.getName();
//记录日志
System.out.println("LoggingProxy2==> The method " + methodName+" begin with "+ Arrays.asList(args));
Object result = method.invoke(target, args); // 目标对象执行目标方法. 相当于执行ArithmeticCalculatorImpl中的+ - * /
//记录日志
System.out.println("LoggingProxy2==> The method " + methodName +" ends with :" +result );
return result ;
}
});

4.2.1.2. 直接生成代理对象

维护一个目标对象 , Object,在Client端设置到ProxyFactory的构造参数中

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
//维护一个目标对象 , Object,在Client端设置到ProxyFactory的构造参数中
private Object target;
/*
* loader: ClassLoader对象。 类加载器对象. 帮我们加载动态生成的代理类。
* interfaces: 接口们. 提供目标对象的所有的接口. 目的是让代理对象保证与目标对象都有接口中相同的方法. 可以代理目标类实现的所有接口的所有方法
* h: InvocationHandler类型的对象.
*/
ClassLoader loader = target.getClass().getClassLoader();
Class[] interfaces = target.getClass().getInterfaces();

Object proxy = Proxy.newProxyInstance(loader, interfaces, new InvocationHandler() {
/**
* invoke: 代理对象调用代理方法, 会回来调用invoke方法。 * * proxy: 代理对象 , 在invoke方法中一般不会使用.
*
* method: 正在被调用的方法对象.
*
* args: 正在被调用的方法的参数.
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

//将方法的调用转回到目标对象上.

//获取方法的名字
String methodName = method.getName();
//记录日志
System.out.println("LoggingProxy==> The method " + methodName+" begin with "+ Arrays.asList(args));
Object result = method.invoke(target, args); // 目标对象执行目标方法. 相当于执行ArithmeticCalculatorImpl中的+ - * /
//记录日志
System.out.println("LoggingProxy==> The method " + methodName +" ends with :" +result );
return result ;
}
});

4.2.2. 编写代理逻辑⭐️🔴

new InvocationHandler()匿名内部类中的invoke方法

4.3. 代理类查看方法

4.3.1. 保存代理类

1
2
3
//将动态生成的代理类保存下来  
Properties properties = System.getProperties();
properties.put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

缺点:还需要使用反编译工具进行反编译

4.3.2. 使用工具类

通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)

image.png

1
2
java -jar arthas-boot.jar
jad com.sun.proxy.$Proxy0

可以直接copy出来保存❕%%
2221-🏡⭐️◼️阿尔萨斯 jad 可以保存内存中类的信息◼️⭐️-point-202301232221%%

4.4. 代理类作用原理⭐️🔴⭐️🔴

^8s8bbj

https://www.bilibili.com/video/BV1Dx411f7ib?p=265&spm_id_from=pageDriver&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204

代理类$Proxy00样例:[[$Proxy00.java]]

基于接口的动态代理中,因为代理类与被代理类实现了同一个接口,所以构成了多态的形式
那么在动态的运行过程中,调用代理类的方法是如何转移到被代理类上去的呢?

  1. 通过InvocationHandler进行传递
  2. 通过代理类$Proxy00中的静态代码块中的信息进行进行关联
  3. 通过多态机制找到最终的被代理类 ❕%%
    2221-🏡⭐️◼️动态代理的作用原理:newProxyInstance的第三个构造参数是new InvocationHandler的匿名内部类,传进去之后最终传给了Proxy的invocationhandler属性,在代理类中通过super.h调用,就说我们定义的匿名内部类。而super.h.invoke的入参中,目标类的方法,是通过class.forname接口然后getMethond 得到◼️⭐️-point-202301232221%%

4.4.1. InvocationHandler(super.h)

%%
2222-🏡⭐️◼️super.h就是我们自己定义的匿名内部类new InvocationHandler()◼️⭐️-point-202301232222%%

Proxy.newProxyInstance构造参数中的匿名内部类new InvocationHandler()传递给了代理类的有参构造方法$Proxy00(InvocationHandler arg0)

image.png

然后又传给了Proxy的有参构造方法super(arg0)
image.png

最终传给了Proxy的私有成员变量protected InvocationHandler h
image.png

所以动态代理类中super.h.invoke最终就是调用的我们自己定义的匿名内部类new InvocationHandler()中的方法


%%
0748-🏡⭐️◼️动态代理的调用逻辑是什么?🔜📝 我们生成代理对象的newProxyInstance的方法中入参new InvocationHandler匿名内部类中编写了我们的增强逻辑,而这个匿名内部类最终传递给了Proxy的成员属性;在JDK帮我们生成的代理类中,也会生成跟目标对象一样的方法,其中的内容是super.h.invoke(this,m3,m3入参),这个super.h就是我们的匿名内部类,.invoke就是匿名内部类中的方法。而这个m3在proxy中是class.forName(接口).getMethod(xxx),这里用到了反射,然后在匿名内部类的invoke方法中method.invoke第二次用到了反射,匿名内部类中的invoke只是个名字,它不是反射◼️⭐️-point-202301300748%%

4.4.2. 代理类中方法判断

%%
▶3.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230409-1754%%
❕❕ ^qrabdh

静态代码块中指定了所调用方法真正属于的类型为 com.atguigu.spring.aop.proxy.ArithmeticCalculator,这一步完成了关联%%
1726-🏡⭐️◼️调用动态代理的方法如何关联到目标对象上的 ?🔜MSTM📝 JDK 动态代理类与目标类实现了相同的接口们,这个是我们在 newProxyInstance 时传入的 interfaces 参数确定的。然后动态代理对象中有一个静态代码块,通过 Class.forName 接口.getMethod 的方式获取目标对象方法的反射出来的引用,然后动态代理自己定义了同名的方法,在方法里使用 super.h.invoke 的方式进行调用。这个 super.h 就是我们 newProxyInstance 时传入的带有我们自己特定增强的 new InvocationHandler 匿名内部类,invoke 就是匿名内部类里面 invoke 方法。而真正的反射动作是 method.invoke 方法◼️⭐️-point-20230219-1726%%

4.4.3. method.invoke

http://rui0.cn/archives/1112
https://binshao.site/2019/04/03/Reflection/#%E5%8F%8D%E5%B0%84%E8%B0%83%E7%94%A8%E7%9A%84%E6%B5%81%E7%A8%8B

4.5. 代理类生成原理

#todo

- [ ] 🚩 - 代理类生成原理 待研究 - 🏡 2023-01-26 10:56 addProxyMethod https://www.cnblogs.com/Xianhuii/p/16918191.html https://blog.csdn.net/qq_43259865/article/details/113944901 https://www.cnblogs.com/Xianhuii/p/16918191.html ![](https://raw.githubusercontent.com/TaylorLuo/typora-imgs/master/img20221210223633.png)

https://gentryhuang.com/posts/d38b32e5/index.html
https://www.jianshu.com/p/471c80a7e831

4.6. 为什么要用接口⭐️🔴

通过父类Proxy的构造方法,保存了创建代理对象过程中传进来的InvocationHandler的实例,使用protected修饰保证了它可以在子类中被访问和使用。但是同时,因为java是单继承的,因此代理类$Proxy00在继承了Proxy后,只能通过实现目标接口的方式来实现方法的扩展,达到我们增强目标方法逻辑的目的。
https://www.cnblogs.com/trunks2008/p/15930642.html

5. CGlib动态代理

https://juejin.cn/post/7026846580426833956#heading-15
https://puppylpg.github.io/2020/08/02/java-reflection-dynamic-proxy/#%E8%83%BD%E5%8A%9B%E9%97%AE%E9%A2%98

5.1. 基本介绍

● 1)静态代理和 JDK 代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理——这就是 Cglib 代理
● 2)Cglib 代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展,有些书也将 Cglib 代理归属到动态代理。
● 3)Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口。它广泛的被许多 AOP 的框架使用,例如 Spring AOP,实现方法拦截
● 4)在 AOP 编程中如何选择代理模式:
○ 目标对象需要实现接口,用 JDK 代理
○ 目标对象不需要实现接口,用 Cglib 代理
● 5)Cglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类
%%
1059-🏡⭐️◼️cglib底层使用的是ASM字节码处理框架,来转换字节码并生成新的类◼️⭐️-point-202301261059%%

5.2. 使用方法

5.2.1. 引入jar包

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>

5.2.2. 示例代码

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
public class ProxyFactory implements MethodInterceptor {  

//维护一个目标对象
private Object target;

//构造器,传入一个被代理的对象
public ProxyFactory(Object target) {
this.target = target;
}

//返回一个代理对象: 是 target 对象的代理对象
public Object getProxyInstance() {
//1. 创建一个工具类
Enhancer enhancer = new Enhancer();
//2. 设置父类
enhancer.setSuperclass(target.getClass());
//3. 设置回调函数
enhancer.setCallback(this);
//4. 创建子类对象,即代理对象
return enhancer.create();

}
//重写 intercept 方法,会调用目标对象的方法
@Override
public Object intercept(Object arg0, Method method, Object[] args, MethodProxy arg3) throws Throwable {
// TODO Auto-generated method stub
System.out.println("Cglib代理模式 ~~ 开始");
Object returnVal = method.invoke(target, args);
System.out.println("Cglib代理模式 ~~ 提交");
return returnVal;
}
}

6. 三种代理的对比⭐️🔴

  • jdk代理和CGLIB代理

    使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。

    在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。

  • 动态代理和静态代理

    1. 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
    2. 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
    3. 动态代理可以代理目标类的所有接口中的方法,而静态代理,最好一个代理负责一个接口,否则很可能违背单一职责原则和接口隔离原则 ❕%%

1155-🏡⭐️◼️Proxy.newProxyInstance(loader, interfaces, new InvocationHandler() {xxx})中的第二个参数是目标类所实现的所有接口,意义是一个代理类可以代理目标类所有接口中的所有方法。代理类与目标类实现了相同的接口,起点是在这里配置的◼️⭐️-point-202301261155%%

如果目标类实现多个接口,那么代理类也会实现多个接口

image.png

image.png

7. 优缺点⭐️🔴

优点:

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

缺点:

  • 增加了系统的复杂度;

8. 实战经验

9. 参考与感谢

9.1. 黑马程序员

https://www.bilibili.com/video/BV1Np4y1z7BU?p=35

课件已下载:/Volumes/Seagate Bas/001-ArchitectureRoad/资料-java设计模式(图解+框架源码分析+实战)
示例代码:/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/013-DemoCode/design_patterns

[[pages/002-schdule/001-Arch/001-Subject/009-内功心法专题/设计模式/黑马/设计模式]]

9.2. 尚硅谷

https://www.bilibili.com/video/BV1Dx411f7ib/?p=266&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
示例代码:/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/013-DemoCode/Spring02