这篇文章我们来聊一下 Java 中的动态代理。
动态代理在 Java 中有着广泛的应用,比如 AOP 的实现原理、RPC远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。
在了解动态代理前,我们需要先了解一下什么是代理模式。
代理模式
代理模式(Proxy Pattern)
是 23 种设计模式的一种,属于结构型模式
。他指的是一个对象本身不做实际的操作,而是通过其他对象来得到自己想要的结果。这样做的好处是可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
这里能体现出一个非常重要的编程思想:不要随意去改源码,如果需要修改,可以通过代理的方式来扩展该方法。
如上图所示,用户不能直接使用目标对象,而是构造出一个代理对象,由代理对象作为中转,代理对象负责调用目标对象真正的行为,从而把结果返回给用户。
也就是说,代理的关键点就是代理对象和目标对象的关系。
代理其实就和经纪人一样,比如你是一个明星,有很多粉丝。你的流量很多,经常会有很多金主来找你洽谈合作等,你自己肯定忙不过来,因为你要处理的不只是谈合作这件事情,你还要懂才艺、拍戏、维护和粉丝的关系、营销等。为此,你找了一个经纪人,你让他负责和金主谈合作这件事,经纪人做事很认真负责,他圆满的完成了任务,于是,金主找你谈合作就变成了金主和你的经纪人谈合作,你就有更多的时间来忙其他事情了。如下图所示
这是一种静态代理,因为这个代理(经纪人)
是你自己亲自挑选的。
但是后来随着你的业务逐渐拓展,你无法选择每个经纪人,所以你索性交给了代理公司来帮你做。如果你想在 B 站火一把,那就直接让代理公司帮你找到负责营销方面的代理人,如果你想维护和粉丝的关系,那你直接让代理公司给你找一些托儿就可以了,那么此时的关系图会变为如下
此时你几乎所有的工作都是由代理公司来进行打理,而他们派出谁来帮你做这些事情你就不得而知了,这得根据实际情况来定,因为代理公司也不只是负责你一个明星,而且每个人所擅长的领域也不同,所以你只有等到有实际需求后,才会给你指定对应的代理人,这种情况就叫做动态代理
。
静态代理
从编译期是否能确定最终的执行方法可以把代理模式分为静态代理和动态代理,我们先演示一下静态代理,这里有一个需求,领导想在系统中添加一个用户,但是他不自己添加,他让下面的程序员来添加,我们看一下这个过程。
首先构建一个用户接口,定义一个保存用户的模版方法。
public interface UserDao { void saveUser(); }
构建一个用户实现类,这个用户实现类是真正进行用户操作的方法
public class UserDaoImpl implements UserDao{
@Overridepublic void saveUser() {
System.out.println(" ---- 保存用户 ---- ");
}
}
构建一个用户代理类,用户代理类也有一个保存用户的方法,不过这个方法属于代理方法,它不会执行真正的保存用户,而是内部持有一个真正的用户对象,进行用户保存。
public class UserProxy {
private UserDao userDao;
public UserProxy(UserDao userDao){
this.userDao = userDao;
}
public void saveUser() {
System.out.println(" ---- 代理开始 ---- ");
userDao.saveUser();
System.out.println(" ---- 代理结束 ----");
}
}
下面是测试方法。
public class UserTest {
public static void main(String[] args) {
UserDao userDao = new UserDaoImpl();
UserProxy userProxy = new UserProxy(userDao);
userProxy.saveUser();
}
}
新创建一个用户实现类 (UserDaoImpl),它不执行用户操作。然后再创建一个用户代理(UserProxy),执行用户代理的用户保存(saveUser),其内部会调用用户实现类的保存用户(saveUser)方法,因为我们 JVM 可以在编译期确定最终的执行方法,所以上面的这种代理模式又叫做静态代理
。
代理模式具有无侵入性的优点,以后我们增加什么新功能的话,我们可以直接增加一个代理类,让代理类来调用用户操作,这样我们就实现了不通过改源码的方式增加了新的功能。然后生活很美好了,我们能够直接添加我们想要的功能,在这美丽
的日子里,cxuan 添加了用户代理、日志代理等等无数个代理类。但是好景不长,cxuan 发现每次改代码的时候都要改每个代理类,这就很烦啊!我宝贵的时光都浪费在改每个代理类上面了吗?
动态代理
JDK 动态代理
于是乎 cxuan 上网求助,发现了一个叫做动态代理的概念,通读了一下,发现有点意思,于是乎 cxuan 修改了一下静态代理的代码,新增了一个 UserHandler
的用户代理,并做了一下 test
,代码如下
public class UserHandler implements InvocationHandler {
private UserDao userDao;
public UserHandler(UserDao userDao){
this.userDao = userDao;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
saveUserStart();
Object obj = method.invoke(userDao, args);
saveUserDone();
return obj;
}
public void saveUserStart(){
System.out.println("---- 开始插入 ----");
}
public void saveUserDone(){
System.out.println("---- 插入完成 ----");
}
}
测试类如下
public static void dynamicProxy(){
UserDao userDao = new UserDaoImpl();
InvocationHandler handler = new UserHandler(userDao);
ClassLoader loader = userDao.getClass().getClassLoader();
Class<?>[] interfaces = userDao.getClass().getInterfaces();
UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler);
proxy.saveUser();
}
UserHandler
是用户代理类,构造函数中的 UserDao 是真实对象,通过把 UserDao 隐藏进 UserHandler ,通过 UserHandler 中的 UserDao 执行真正的方法。
类加载器、接口数组你可以把它理解为一个方法树,每棵叶子结点都是一个方法,通过后面的 proxy.saveUser() 来告诉 JVM 执行的是方法树上的哪个方法。
用户代理是通过类加载器、接口数组、代理类来得到的。saveUser 方法就相当于是告诉 proxy 你最终要执行的是哪个方法,这个 proxy.saveUser 方法并不是最终直接执行的 saveUser 方法,最终的 saveUser 方法是由 UserHandler 中的 invoke 方法触发的。
上面这种在编译期无法确定最终的执行方法,而只能通过运行时动态获取方法的代理模式被称为 动态代理
。
动态代理的优势是实现无侵入式
的代码扩展,也可以对方法进行增强。此外,也可以大大减少代码量,避免代理类泛滥成灾的情况。
所以我们现在总结一下静态代理和动态代理各自的特点。
静态代理
- 静态代理类:由程序员创建或者由第三方工具生成,再进行编译;在程序运行之前,代理类的 .class 文件已经存在了。
- 静态代理事先知道要代理的是什么。
- 静态代理类通常只代理一个类。
动态代理
- 动态代理通常是在程序运行时,通过
反射机制
动态生成的。 - 动态代理类通常代理
接口
下的所有类。 - 动态代理事先不知道要代理的是什么,只有在运行的时候才能确定。
- 动态代理的调用处理程序必须事先继承 InvocationHandler 接口,使用 Proxy 类中的 newProxyInstance 方法动态的创建代理类。
在上面的代码示例中,我们是定义了一个 UserDao 接口,然后有 UserDaoImpl 接口的实现类,我们通过 Proxy.newProxyInstance 方法得到的也是 UserDao 的实现类对象,那么其实这是一种基于接口的动态代理。也叫做 JDK 动态代理
。
是不是只有这一种动态代理技术呢?既然都这么问了,那当然不是。
除此之外,还有一些其他代理技术,不过是需要加载额外的 jar 包的,那么我们汇总一下所有的代理技术和它的特征
- JDK 的动态代理使用简单,它内置在 JDK 中,因此不需要引入第三方 Jar 包。
- CGLIB 和 Javassist 都是高级的字节码生成库,总体性能比 JDK 自带的动态代理好,而且功能十分强大。
- ASM 是低级的字节码生成工具,使用 ASM 已经近乎于在使用字节码编程,对开发人员要求最高。当然,也是
性能最好
的一种动态代理生成工具。但 ASM 的使用很繁琐,而且性能也没有数量级的提升,与 CGLIB 等高级字节码生成工具相比,ASM 程序的维护性较差,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。
下面我们就来依次介绍一下这些动态代理工具的使用
</div>