一、概述
代理模式我们接触的就比较多了,所谓的代理模式就是,给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。比如,在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之前起到中介的作用。其实代理模式就两个字:中介。
- 代理模式主要解决的问题:
直接访问对象相关的问题,比如无法直接访问对象,或者由于安全问题不建议直接访问的对象,亦或者是访问对象太过于复杂的问题;
- 为什么使用代理:
由于设计模式的开闭原则,不建议直接修改已有的代码,我们可以使用代理,在目标对象基础上合理的扩展功能;
- 代理模式的一些应用场景:
- 比如我最近要买ThinkPadT470p,国内网站上全是9千元左右,这时我可以通过海淘从国外买,可能会便宜不少,这其中的海淘就相当于代理的角色。
- 只要我们随便一想,生活中涉及到代理的实在太多,比如火车站通过黄牛买票,通过4S店买车,通过中介租房,外卖小哥送餐等等,都是代理模式的应用。
- 代理模式的分类:
根据代理对象生成的时期不同,代理模式可以大致分为静态代理和动态代理。其中JDK中的动态代理的实现是通过Java的反射来实现的。
代理模式角色
我们先来一张图看一下代理模式的角色:
注:图片来源:图说设计模式-代理模式-角色图解
通过以上结构,我们可以大概了解到代理模式的几个角色:
- Subject:抽象主题角色,底层一般是接口实现,该接口定义了代理类和真实主题角色的公共的对外方法,是对象和它的代理共用的接口。
- RealSubject:真实主题角色,实现了抽象主题接口,实现了真正的业务逻辑;
- Proxy:代理角色,内部含有对真实对象RealSubject的引用,从而可以实现对真实对象的代理。代理对象提供与真实对象相同的接口,以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,以扩展相应的功能。
- Client:客户端的调用,不算代理模式的角色。
二、静态代理
静态代理是说由程序员手动编写或工具生成的代理类,在程序运行前就已经编译完成,这就是所谓的静态代理类。我们通过一个简单的保存数据的例子来看一下静态代理的实现,代码转载自:
Java三种代理模式:静态代理、动态代理和cglib代理,静态代理代码实现
- 底层接口:
IUserDao
,只有一个保存数据的接口。
public interface IUserDao {
void save();
}
- 真实对象:
UserDaoImpl
,实现了接口中的save方法
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("保存数据");
}
}
- 静态代理对象:
UserDaoProxy
,也实现了接口IUserDao
;
public class UserDaoProxy implements IUserDao {
private IUserDao iUserDao;
public UserDaoProxy(IUserDao iUserDao) {
this.iUserDao = iUserDao;
}
@Override
public void save() {
iUserDao.save();
}
}
- 测试类:
Main
public class Main {
public static void main(String[] args) {
//目标对象
IUserDao target = new UserDaoImpl();
//代理对象
UserDaoProxy proxy = new UserDaoProxy(target);
proxy.save();
}
}
运行测试代码后,打印:
保存数据
这时候,如果要扩展功能,比如说,对原先的保存接口添加事务处理。当然,我们可以直接修改具体实现类UserDaoImpl
,但根据设计模式的开闭原则,不建议我们直接修改已实现的类,这个时候我们就可以通过代理来实现,我们通过给UserDaoProxy
添加对应的方法即可:
public class UserDaoProxy implements IUserDao {
private IUserDao iUserDao;
public UserDaoProxy(IUserDao iUserDao) {
this.iUserDao = iUserDao;
}
@Override
public void save() {
before();
iUserDao.save();
after();
}
private void before() {
System.out.println("开启事务");
}
private void after() {
System.out.println("关闭事务");
}
}
重新运行程序:
开启事务
保存数据
关闭事务
静态代理总结
- 静态代理可以做到在不修改目标实现的情况下,对目标功能进行扩展;并且代理对象作为客户端和目标对象之间的中介,起到了保护目标对象的作用;
- 一般来说,静态代理具体实现类与代理类要一一对应,并且代理对象需要与目标对象实现一样的接口,所以有可能会有很多代理类。并且,一旦接口增加方法,目标对象与代理对象都需要维护,无形之中增加了系统的复杂度;
三、动态代理
相比静态代理,动态代理有更强的灵活性。动态代理是在程序运行时,通过反射机制动态创建生成的。动态代理类使用字节码动态生成加载技术,在运行时生成加载类。生成动态代理类的方法很多,如JDK自带的动态代理、CGLIB、Javassist 或者 ASM 库等。我们先来看下JDK自带的生成动态代理的方式。
JDK动态代理
1. 实现类和方法
我们首先来看下JDK动态代理中涉及到的基础类和方法:
类或接口:
java.lang.reflect.Proxy
,java.lang.reflect InvocationHandler
,位于Java反射包下;
方法:Proxy
中的newProxyInstance
方法,InvocationHandler
中的invoke
方法;
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {...}
该方法返回一个目标接口的代理类实例。参数简单说明:
- ClassLoader loader:指定当前目标对象使用的类加载器
- Class<?>[] interfaces:目标对象实现的接口的类型
- InvocationHandler h:用于事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
public Object invoke(Object proxy, Method method, Object[] args)
这个方法用于实现目标对象具体方法的调用。
2. 实现步骤
JDK代理的大致实现步骤如下:
a. 创建底层接口及真实的对象;
b. 创建一个InvocationHandler接口的实现类,实现invoke()方法;
c. 调用Proxy的静态方法,创建一个代理类
d. 通过代理调用方法
3. 代码实现
接下来,我们来看一下代码实现,我们先新建代理类:
public class DynamicProxy implements InvocationHandler {
/** 要代理的对象 */
private Object object;
/**
* 将被代理者的实例传进动态代理类的构造函数中
* @param object
*/
public DynamicProxy(Object object) {
this.object = object;
}
/**
* 覆盖InvocationHandler接口的invoke方法,代理实现具体方法的调用,并可以添加我们的实现
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(this.object, args);
after();
return result;
}
/**
* 获取代理对象
*
* @return the instance
*/
public Object getInstance() {
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
}
private void before() {
System.out.println("开启事务");
}
private void after() {
System.out.println("关闭事务");
}
}
测试程序:
public static void main(String[] args) {
//目标对象,并且打印下
IUserDao target = new UserDaoImpl();
System.out.println(target.getClass());
// 代理对象,并且打印下
IUserDao userDao = (IUserDao)new DynamicProxy(target).getInstance();
System.out.println(userDao.getClass());
userDao.save();
}
打印结果:
class com.proxy.UserDaoImpl
class com.sun.proxy.$Proxy0
开启事务
保存数据
关闭事务
4.JDK代理总结
- JDK动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到一个集中的方法中处理,即使接口方法数量比较多的时候,我们也可以进行灵活的处理;
- JDK动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。而CGLIB动态代理则是弥补了这部分的不足。
CGLIB动态代理
1. CGLIB简介
我们上面所了解的静态代理和JDK动态代理模式,都是要求目标对象是实现一个接口的类,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这时候就可以考虑通过CGLIB代理来实现了。
CGLIB(Code Generation Library),是一个第三方代码生成库,它的原理是程序运行的时候,为指定的目标类生成一个子类,从而实现对目标对象功能的扩展。所以有时候CGLIB代理也可以叫做子类代理。由于是通过继承来实现的,所以我们不能对
final
修饰的类进行代理。
CGLIB有以下特点:
- Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截);
- CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉。
2. CGLIB代码实现
由于CGLIB是一个第三方包,所以我们在使用的时候需要手动引入cglib的jar,并且由于CGLIB底层使用了ASM字节码开源包,所以还需要引入ASM的jar包。对应的maven引入是:
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>5.2</version>
</dependency>
其实,CGLIB的实现和JDK动态代理的实现是类似的,我们简单来看一下代理类:
public class CGLIBProxy implements MethodInterceptor {
/** 代理的对象 */
private Object object;
public CGLIBProxy(Object object) {
this.object = object;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = method.invoke(object, args);
after();
return result;
}
/**
* 获取代理对象
*
* @return the instance
*/
public Object getInstance() {
//工具类
Enhancer en = new Enhancer();
//设置父类
en.setSuperclass(object.getClass());
//设置回调函数
en.setCallback(this);
//创建子类对象代理
return en.create();
}
private void before() {
System.out.println("开启事务");
}
private void after() {
System.out.println("关闭事务");
}
}
进行测试:
public static void main(String[] args) {
// 实际对象 并打印下
UserDaoImpl userDao = new UserDaoImpl();
System.out.println(userDao.getClass());
// 代理对象 并打印下
UserDaoImpl userProxy = (UserDaoImpl)new CGLIBProxy(userDao).getInstance();
System.out.println(userProxy.getClass());
userProxy.save();
}
打印结果:
class com.proxy.UserDaoImpl
class com.proxy.UserDaoImpl$$EnhancerByCGLIB$$ceeeb256
开启事务
保存数据
关闭事务
3. CGLIB总结
- CGLIB与JDK动态代理的最大区别就是要代理的对象是否实现了接口;
- CGLIB是基于继承来实现的动态代理,所以要求被代理的类不能是
final
类型;- Spring的AOP 是JDK动态代理和CGLIB代理的很好的一个实现。如果对象实现了接口,则默认情况下AOP会采用JDK动态代理,也可以强制使用CGLIB代理;如果目标对象没有实现接口,则AOP会采用CGLIB代理,也就是说Spring会自动在JDK动态代理和CGLIB之间进行选择;
其他动态代理
Javassist代理
- Javassist, 也是一个开源的Java字节码的类库,是和CGLIB类似,属于一种高级的字节码生成库,通过Javassist对字节码操作来实现代理,比如使用Javassist对JBoss动态的实现AOP框架。如果有需要,我们可以看下相应的实现。
ASM代理
- ASM,同样也是一种字节码生成库,ASM能够以二进制形式修改已有类或者动态生成类,ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
- 不过相对于Javassist来说,ASM属于一种相对低级的字节码库,在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。虽然ASM是性能最高的一种动态代理方式,但由于操作繁琐,要求较高,所以一般情况下,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。
四、 应用场景
以下摘录自:IBM-代理模式的应用场合
代理模式有多种应用场合,如下所述:
- 远程代理,也就是为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。比如说 WebService,当我们在应用程序的项目中加入一个 Web 引用,引用一个 WebService,此时会在项目中声称一个 WebReference 的文件夹和一些文件,这个就是起代理作用的,这样可以让那个客户端程序调用代理解决远程访问的问题;
- 虚拟代理,是根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,比如打开一个网页,这个网页里面包含了大量的文字和图片,但我们可以很快看到文字,但是图片却是一张一张地下载后才能看到,那些未打开的图片框,就是通过虚拟代里来替换了真实的图片,此时代理存储了真实图片的路径和尺寸;
- 安全代理,用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候;
- 指针引用,是指当调用真实的对象时,代理处理另外一些事。比如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它,或当第一次引用一个持久对象时,将它装入内存,或是在访问一个实际对象前,检查是否已经释放它,以确保其他对象不能改变它。这些都是通过代理在访问一个对象时附加一些内务处理;
- 延迟加载,用代理模式实现延迟加载的一个经典应用就在 Hibernate 框架里面。当 Hibernate 加载实体 bean 时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采取延迟加载的机制,以提高系统的性能。Hibernate 中的延迟加载主要分为属性的延迟加载和关联表的延时加载两类。实现原理是使用代理拦截原有的 getter 方法,在真正使用对象数据时才去数据库或者其他第三方组件加载实际的数据,从而提升系统性能。
五、回顾与总结
在以上内容中,我们学习了代理模式的两种情况:静态代理和动态代理,并且学习了动态代理中JDK代理和CGLIB代理的实现。我们简单总结下:
- 所谓代理,其实就是为了解决直接访问对象相关的问题,其实数白了就两个字:中介;
- 静态代理和动态代理其实类似,都需要生成代理类,只是生成代理类的时期不同,静态代理是在编译期就已生成,而动态代理是在程序运行期间动态生成的;
- JDK代理和CGLIB代理最大的区别就是要代理的对象是否实现了接口;他们两者不是相互对立的局面,而是相互协作的,而Spring中的AOP正是他们协作的最佳实践;
- 了解与合理的使用设计模式,不但能让我们能容易的理解别人优秀的代码,也可以让我们写出优秀的代码;
参考资料:
《大话设计模式》
Java三种代理模式:静态代理、动态代理和cglib代理
Java设计模式——代理模式实现及原理
代理模式原理及实例讲解