Java设计模式-代理模式
代理模式
参考:Java三种代理模式:静态代理、动态代理和cglib代理
代理模式
含义
代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。
举例歌手与经纪人
结构
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层
Subject(抽象角色):定义代理角色和真实角色的公共对外方法
Proxy(代理角色):实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作
RealSubject(真实角色):实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。关注于真正的业务逻辑
核心作用
- 通过代理,控制对象的访问:可以详细控制访问某个(某类)对象的方法,在调用这个方法前做前置处理,调用这个方法后做后置处理。(即AOP的微观实现)
- AOP(Aspect Oriented Programming 面向切面编程)的核心实现机制
应用场景
- 安全代理:屏蔽对真实角色的直接访问
- 远程代理:通过代理类处理远程方法调用(RMI:远程方法调用(Remote Method Invocation))
延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象
开发框架中的应用场景:
- struct2中拦截器的实现
- 数据库连接池关闭处理
- Hibernate中延时加载的实现
- mybatis中实现拦截器插件
- AspectJ的实现
- spring中AOP的实现:日志拦截,声明式事务处理
- web service
- RMI远程方法调用
- …
分类
- 静态代理(静态定义代理类)
- 动态代理(动态生成代理类)
- JDK自带的动态代理
- javaassist字节码操作库实现
- CGLIB
- ASM(底层使用指令,可维护性较差)
静态代理
这种代理方式需要代理对象和目标对象实现一样的接口
优点:可以在不修改目标对象的前提下扩展目标对象的功能
缺点:
- 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
- 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
实例
根据前面举例的经纪人和歌手关系来编写简单的实例
Star是抽象角色,RealStar是真实角色,ProxyStar是代理角色,client是用户
Star.java
1 | package StaticProxy; |
RealStar.java
1 | package StaticProxy; |
ProxyStar.java
1 | package StaticProxy; |
Client.java
1 | package StaticProxy; |
输出
1 | ProxyStar.confer |
动态代理
抽象角色(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多方法。
静态代理与动态代理的区别主要在:
- 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件
- 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中
JDK自带的动态代理
1 | public class Proxy extends Object implements Serializable |
Proxy
提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类
为某个接口创建代理Foo
:
1 | InvocationHandler handler = new MyInvocationHandler(...); |
或更简单地:
1 | Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), |
动态代理类 (以下简称为代理类 )是一个实现在类创建时在运行时指定的接口列表的类,具有如下所述的行为。
代理接口是由代理类实现的接口。每个代理实例都有一个关联的调用处理程序对象,该对象实现了InvocationHandler
接口。通过其代理接口之一对代理实例进行方法调用将被调度到实例调用处理程序的invoke
方法,并传递代理实例java.lang.reflect
。标识被调用方法的方法对象,以及包含参数的Object类型数组。调用处理程序根据需要处理编码的方法调用,并且它返回的结果将作为代理实例上的方法调用的结果返回。
代理类具有以下属性:
- 代理类是public,final的,不是抽象的,不可继承
- 代理对象类名是没有明确定义的,但是以
$Proxy
开头的类名要给代理对象保留着,所以标准情况下如果发现某个class的类名以$Proxy
开头,那它肯定是代理对象, - 代理类扩展了
java.lang.reflect.proxy
。所以可以用instanceof
来判断是否是代理对象
JDK中生成代理对象主要涉及的类有
java.lang.reflect Proxy
动态生成代理类和对象,其主要方法:
1 | /** |
java.lang.reflect InvocationHandler(处理类接口)
可以通过invoke
方法对真实角色的代理访问,每次通过Proxy
生成代理类对象时都要指定对应的处理器对象。其主要方法:
1 | /** |
实例
Star.java,RealStar.java与静态代理的一样
StarHandler.java
1 | package DynamicProxy; |
所有的代理类方法内部都会调用处理器类的 invoke 方法并传入被代理类的当前方法,而这个 invoke 方法可以选择去让 method 正常被调用,也可以跳过 method 的调用,甚至可以在 method 真正被调用前后做一些额外的事情。
Client.java
1 | package DynamicProxy; |
输出
1 | 真正方法执行前! |
大概流程
首先,一个处理器类的定义是必不可少的(比如StarHandler
),它的内部必须得关联一个真实对象,即被代理类实例(比如realStar
)。
接着,我们从外部调用代理类的任一方法,代理类方法会转而去调用处理器的 invoke 方法并传入方法签名和方法的形式参数集合(StarHandler.invoke
)。
最后,方法能否得到正常的调用取决于处理器 invoke 方法体是否实实在在去调用了 method 方法。
其实,基于 JDK 实现的的动态代理是有缺陷的,并且这些缺陷是不易修复的,所以才有了 CGLIB 的流行。
缺陷与不足
单一的代理机制
虚拟机生成的代理类为了公用 Proxy 类中的 InvocationHandler
字段来存储自己的处理器类实例而继承了 Proxy 类,那说明了什么?
Java 的单根继承告诉你,代理类不能再继承任何别的类了,那么被代理类父类中的方法自然就无从获取,即代理类无法代理真实类中父类的任何方法。
不友好的返回值
newProxyInstance
返回值是object,要使用它得强制类型转换
那么问题来了,假如我们的被代理类实现了多个接口,请问你该强转为那个接口类型,现在假设被代理类实现了接口 A 和 B,那么最后的实例如果强转为 A ,自然被代理类所实现的接口 B 中所有的方法你都不能调用,反之亦然。
这样就直接导致一个结果,你得清楚哪个方法是哪个接口中的,调用某个方法之前强转为对应的接口,相当不友好的设计。