概念
代理模式(Proxy)是一种设计模式,提供了对目标对象另外的访问方式:即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能,例:统计,log 或对参数进行优化,更改。
这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需要修改,可以通过代理的方式来扩展该方法。
举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接联系明星,而是联系明星的经纪人,来达到同样的目的。明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决。这就是代理思想在现实中的一个例子。
图片示例:
代理模式的关键点是:代理对象与目标对象。代理对象是对目标对象的扩展,并会调用目标对象。
代理模式的使用场景
当无法或不想直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问,为了保证客户端使用的透明性,委托对象与代理对象需要实现同一个接口。
三种代理模式
Java 代理分为静态代理和动态代理和 Cglib 代理。
静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。
例:模拟文件操作:
定义一个文件操作的接口:IUserDao.java,然后目标对象实现这个接口的方法:UserDao.java,此时如果使用静态代理方式,就需要在代理对象(UserDaoProxy.java)中也实现 IUserDao 接口。调用的时候通过调用代理对象的方法来调用目标对象。
需要注意的是,代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法。
- 1 个静态代理只服务 1 种类型的目标对象。
- 若要服务多类型的目标对象,则需要为每种目标对象都实现一个静态代理对象。
静态代理代码示例
1 | // IUserDao接口 |
1 | // 目标对象:UserDao |
1 | // 静态代理对象:UserDaoProxy |
1 | // 测试类 |
静态代理总结
优点:
- 协调调用者和被调用者,降低了系统的耦合度。
- 可以做到在不修改目标对象的功能前提下,对目标功能扩展。
缺点:
- 由于在客户端和真实主题之间增加了代理对象,因此会造成请求的处理速度变慢。
- 在目标对象较多的情况下(或代理实现复杂),若采用静态代理,则会出现静态代理对象量多、代码量大,从而导致代码复杂的问题。
动态代理
为解决静态代理对象必须实现接口的问题,Java 给出了动态代理。
特点:
- 设计动态代理类(
DynamicProxy
)时,不需要显式实现与目标对象类(RealSubject
)相同的接口,而是将这种实现推迟到程序运行时在内存中由 JVM 来实现。 - 通过 Java 反射机制的
method.invoke()
,通过调用动态代理类对象方法,从而自动调用目标对象的方法。
动态代理应用场景
- 基于静态代理应用场景下,需要代理对象数量较多的情况下使用动态代理。
- AOP 领域(切面编程)。
动态代理与静态代理区别
类型 | 代码创建&绑定时机 | 原理 | 效率 | 具体使用 |
---|---|---|---|---|
静态代理 | 代码运行前 在代理类实现时就指定与目标对象类相同的接口 |
/ | / | 代理单一目标对象 |
动态代理 | 运行时 不需要显式实现与对象类相同的接口 |
反射 | 低 | 代理多个对象 |
动态代理代码示例
模拟文件操作:
文件操作接口IUserDao.java
,实现类UserDao1
保存文件,另一个实现类UserDao2
下载文件。
步骤:
- 声明调用处理器类
- 声明目标对象类的抽象接口
- 声明目标对象类
- 通过动态代理对象,调用目标对象的方法
1. 声明调用处理器类 DynamicProxy.java
1 | // 作用: |
2. 声明目标对象抽象接口
1 | public interface IUserDao { |
3. 声明目标对象类
UserDao1.java
1 | public class UserDao1 implements IUserDao { |
UserDao2.java
1 | public class UserDao2 implements IUserDao { |
4. 通过动态代理对象,调用目标对象的方法
1 | public class App { |
具体原理参考代理模式(静态代理 and 动态代理)
动态代理优缺点
优点:
- 只需要 1 个动态代理类就可以解决创建多个静态代理的问题,避免重复、多余代码更强的灵活性。
缺点:
- 效率低
相比静态代理中 直接调用目标对象方法,动态代理则需要先通过 java 反射机制 从而 间接调用目标对象方法。 - 应用场景局限
因为 Java 的单继承特性(每个代理类都继承了 Proxy 类),即只能针对接口创建代理类,不能针对类创建代理类。即只能动态代理实现了接口的类
Cglib 代理
上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib 代理。
Cglib 代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。
- JDK 的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用 Cglib 实现。
- Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口.它广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 synaop,为他们提供方法的 interception(拦截)。
- Cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM 来转换字节码并生成新的类.不鼓励直接使用 ASM,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。
Cglib 子类代理实现方法:
需要引入 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>引入功能包后,就可以在内存中动态构建子类。
代理的类不能为 final,否则报错。
目标对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法。
Cglib 代理代码示例
1 | // 目标对象类UserDao |
1 | // Cglib代理工厂 |
1 | // 测试类 |
在 Spring 的 AOP 编程中:
- 如果加入容器的目标对象有实现接口,用 JDK 代理。
- 如果目标对象没有实现接口,用 Cglib 代理。
Spring 也可以通过<aop:config proxy-target-class="true">
强制使用 Cglib 代理,使用 Java 字节码编辑类库 ASM 操作字节码来实现,直接以二进制形式动态地生成 stub 类或其他代理类,性能比 JDK 更强。