作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」
大家好,我是呼噜噜,在之前的一篇文章-Java注解中,我们详细讲解了Java注解及其原理,其中反射调用注解的时候(class.getAnnotation
),会继承动态代理类AnotationInvocationHandler
,创建注解的代理实例,来让开发者后续操作注解。本篇文章将深入聊聊什么是动态代理
代理模式
首先我们要明白动态代理
属于设计模式中的代理模式
所谓代理模式
是指通过访问目标对象的代理对象,再由代理对象去访问目标对象
通俗点讲,本来我们只可直接去商店买药 ;突然有一天,我们的车坏了,导致我们无法直接去商店买药。这个时候,又急着需要药,我们可以打电话叫
代理人:小张
去商店帮我们买药,然后再让他把药给我们带回来。这样最终我们拿到了药。
这样一来就可以在不修改原目标对象的前提下,提供额外的功能操作,实现扩展目标对象的功能。
静态代理
代理模式有静态代理和动态代理
两种实现方式
静态代理和动态代理的区别?什么是静态、动态?
从 JVM 层面来说:
- 静态代理:
- 在编译时就已经实现,编译完成后代理类是一个实际的class文件
- 代理类和委托类的关系在程序运行前就已确定
- 动态代理:
- 在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中
- 代理类和委托类的关系是在程序运行时确定
静态代理的使用步骤
我们先聊聊静态代理, 其一般使用步骤:
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后就可以在代理类的对应方法调用目标类中的对应方法
示例
我们来模拟一下上文买药的例子,另外我们想代理人顺便帮我们在买点水果啥的
定义一个接口,来代表我们的目标
public interface OurService {
void buyMed();
}
再实现我们的接口
public class OurServiceImpl implements OurService {
@Override
public void buyMed() {
System.out.println("买药。。。");
}
}
创建代理类并额外附表其他目标,比如买蛋糕、水果啊之类的
public class MyStaticProxy implements OurService {
private OurService ourService;
public MyStaticProxy(OurService ourService) {
this.ourService = ourService;
}
@Override
public void buyMed() {
System.out.println("买药前,先去买蛋糕。。。");
ourService.buyMed();
System.out.println("买药后,再去买水果。。。");
}
}
最后测试类
public class TestStaticProxy {
public static void main(String[] args) {
OurService ourService = new OurServiceImpl();
//userService.buyMed(); 直接执行
MyStaticProxy myStaticProxy = new MyStaticProxy(ourService);
myStaticProxy.buyMed();//委托 代理类 去执行
}
}
结果:
买药前,先去买蛋糕。。。
买药。。。
买药后,再去买水果。。。
静态代理的缺陷
从上面的例子,我们可以发现静态代理
非常容易地实现了对一个类的代理操作,但是也有几个缺点:
- 静态代理不能使一个代理类反复作用于多个目标对象,代理对象直接持有目标对象的引用,这导致代理对象和目标对象类型紧密耦合了在一起,需要对每个目标类都单独写一个代理类。
- 不易维护,一旦接口更改,代理类和目标类都需要更改,比较繁琐。
解决静态代理的缺陷的思路
通过上文我们可以发现静态代理最大的缺点,就是不能使一个代理类反复作用于多个目标对象,要想实现不同的增强功能,必须编写不同的代理类,耦合性高。那我们能不能对于不同的源程序,让JVM自动生成对应的代理类?
如果可以的话,这样不就可以解决问题了嘛。
首先我们得思考一个问题,java怎样才能动态地生成代理类?
我们先来回顾一下对象的创建过程
创建一个实例对象的底层逻辑,其实与.class文件和Class对象息息相关
"没有对象, 那就new一个",对于每个javar来说都太熟悉了,但这样往往忽视了底层的细节--最核心就是得到对应的Class对象
在文章https://mp.weixin.qq.com/s/v91bqRiKDWWgeNl1DIdaDQ中,我们聊到了JVM类的加载过程
加载阶段:指的是将类对应的.class文件中的二进制字节流读入到内存中,将这个字节流转化为方法区的运行时数据结构,然后在堆区创建一个java.lang.Class 对象,作为对方法区中这些数据的访问入口
其中将类对应的.class文件中的二进制字节流读入到内存中
,JVM虚拟机规范并没有
指明二进制字节流必须得从某个Class文件中获取,确切地说是根本没有指明要从哪里获取、如何获取。
所以获取类的二进制字节流(class字节码)
有很多途径:
- 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
- 从网络中获取,典型的应用是 Applet
- 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流
- 由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
- 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择 把程序安装到数据库中来完成程序代码在集群间的分发。
- 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文 件来保障程序运行逻辑不被窥探。
在笔者之前讲解Java反射的文章https://mp.weixin.qq.com/s/_n8HTIjkw7Emcunpb4-Iwg中,我们知晓:
- 类也是可以用来存储数据的,Class类就像 普通类的模板 一样