代理模式作为框架中经常使用的设计模式,它对代码的拓展性提升可以说是相当高,从aop的实现,到单元测试,再到Mybatis的Mapper接口实现类,都有代理模式的影子。
本篇主要讲解代理模式的使用,动态代理的功能和JVM底层如何处理动态代理。
目录
1.黑中介生成器(newProxyInstance方法)详解
一、静态代理的实现
代理是什么意思呢,举个例子,我是房东,我有一栋房子急租,但是我很忙,怎么办呢?当然是找黑……中介啦。
客户如果需要租房子,那么就去找黑中介就行了,这个中介也不一定只会做房产中介,也可以做些别的,反正就是不务正业的那种。
1、实现一个房东
房东的功能相当简单,只需要租售房子(rentHouse())就行了。
public interface Rent {
void rentSth();
}
public class Landlord implements Rent{
//此处实现Rent接口,之后就可以对Rent接口进行拓展,比如租车,或者py交易也行
@Override
public void rentSth() {
rentHouse();
}
public void rentHouse(){
System.out.println("租售房子");
}
}
2.实现黑中介
黑中介只需要帮助房东租房子即可,但是为了方便和客户对接,因此,黑中介也需要继承Rent接口
public class Intermediary implements Rent{
//因为中介本身是没有房子的,因此,需要有一个房东的存在,让黑中介能干活
private Rent rent;
//如果对静态代理理解比较深的话,这个代理的对象可以有很多个,根据业务场景的需要,用代理的不同对象处理不同的事情。
public Intermediary(Rent rent) {
this.rent = rent;
}
@Override
public void rentSth() {
rent.rentSth();
}
public void getMoney(int money){
System.out.println("给房东"+money*0.1+"元");
System.out.println("获利"+money*0.9+"元");
}
}
3.租房试一试
public class Tenant {
public static void main(String[] args) {
//找黑中介租房子
Intermediary intermediary = new Intermediary(new Landlord());
intermediary.rentSth();
intermediary.getMoney(3000);
}
}
运行结果如下:
4.静态代理如何玩出花来?
虽然看了静态代理之后,觉得实现起来好像没啥用?但是实际上,静态代理已经可以做很多事情了,比如,可以用静态代理,让代理的对象的方法按照你设定的顺序进行执行(参考Spring AOP的功能),玩法很多。
但是静态代理有一个致命的缺点,当然,这也是所有静态代码都有的缺点:不够灵活,代理的对象必须要预先编译好。
二、动态代理的实现
动态代理的在功能上基本上和静态代理相差无几,但是它是基于反射包下的产物,所谓反射,也就是程序在运行的过程中,使JVM加载其他class文件并且操作的一个过程。
基于反射的代理模式就解决了静态代理不够灵活的一个缺点,下面我们看下动态代理的使用方法。
1.黑中介生成器(newProxyInstance方法)详解
这里依旧拿上面的代码为例子,房东和Rent接口都是必须要的,而黑中介我们就不需要了,动态代理会替我们在程序运行的时候,生成一个黑中介。
Proxy.newProxyInstance();//Proxy(代理的意思)这个类提供的静态方法可以产生一个黑中介
//详细看一下这个方法需要的参数:
public static Object newProxyInstance(
ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
-
ClassLoader loader:该参数是被代理对象的类加载器,如果是我们写的普通类,使用applicationClassLoader就可以了,也就是ClassLoader.getSystemClassLoader(),这个加载器也是默认加载器;有兴趣的可以了解一下Java类加载机制(双亲委派模型)。
-
Class<?>[] interfaces:该参数是被代理对象的class对象,因为可以代理多个类,因此用数组传值。
-
InvocationHandler h:虽然反射生成的黑中介可以代理房东的所有行为,但是程序员得告诉黑中介,应该怎么做。InvocationHandler是黑中介的行为处理器。(此处括号里的内容可以忽略。实际上,动态代理生成的对象,本质上重写了该对象的所有方法,使其所有方法中都只有一个功能,调用InvocationHandler对象的invoke方法。)
2.InvocationHandler接口详解
该接口只有一个invoke方法,这个方法我们需要实现它,才能代理我们的对象。
因为之后我们调用任何黑中介的方法,黑中介都会调用invoke方法。
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
- Object proxy:这个对象是黑中介自己,尽量不要去使用它,因为它的所有方法都是调用invoke方法,调用它的任何方法都会进入死递归调用。
- Method method:因为不论你调用黑中介的哪个方法,最后都会调用invoke方法,因此,需要依靠Method对象来识别到底调用了什么方法;这个method对象也就是被调用的方法的实例对象。
- Object[] args:这个是被黑中介调用方法的参数;
- invoke方法的返回值就是黑中介被调用的方法的返回值。
3.使用动态代理
在解释完黑中介生成器之后,现在可以开始自己动手试一试了。
public class Tenant {
public static void main(String[] args) {
Rent rent = (Rent) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Rent.class}, new InvocationHandler() {
//代理房东
Rent rent = new Landlord();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
rent.rentSth();
System.out.println("我是黑中介,我被抓了");
return null;
}
});
rent.rentSth();
}
}
运行结果:
4.程序分析和整理
现在我们的黑中介已经如鱼得水了,但是还是存在问题:
因为调用黑中介的任何方法都会调用invoke方法,因此,如果想代理多个方法的话,就很麻烦了,比如现在调用黑中介的toString方法,他依旧会执行invoke方法,给客户推销房子。
所以,修改程序:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String menthodName = method.getName();
//根据方法的名字选择做什么事情。
if(menthodName.equals("rentSth")){
method.invoke(rent,null);
System.out.println("我是黑中介,我被抓了");
return null;
}
return null;
}
三、动态代理原理分析
从二中我们可以发现,整个动态代理对我们来说几乎都是透明的,唯一的黑匣子便是newProxyInstance方法。
不过Proxy既然是位于reflet包下,那么它的实现必定离不开反射技术。
事实上在最后,它会调用sun.misc.ProxyGenerator.generateProxyClass()方法来完成生成字节码的动作,并且在运行时产生一个描述代理类的字节码byte[]数组。
我们可以在main()方法中添加下面的代码,将这个类文件写入到本地。
byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{Rent.class});
try(
FileOutputStream fos =new FileOutputStream(new File("E:\\$Proxy0.class"))
){
fos.write(bytes);
fos.flush();
}catch (Exception e){
e.printStackTrace();
}
再次运行main方法后,在E盘下会多出一个字节码$Proxy0文件
通过反编译可以得出下面的代码(为了方便观看,我去掉了trycatch代码块):
public final class $Proxy extends Proxy
implements Rent
{
public $Proxy(InvocationHandler invocationhandler)
{
super(invocationhandler);
}
public final boolean equals(Object obj)
{
return ((Boolean)super.h.invoke(this, m1, new Object[] {
obj
})).booleanValue();
}
public final String toString()
{
return (String)super.h.invoke(this, m2, null);
}
public final void rentSth()
{
super.h.invoke(this, m3, null);
}
public final int hashCode()
{
return ((Integer)super.h.invoke(this, m0, null)).intValue();
}
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
static
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
Class.forName("java.lang.Object")
});
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("proxy.Rent").getMethod("rentSth", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
}
}
不难发现实际上动态生成的对象在调用任何方法的时候,都会交给成员对象InvocationHandler的invoke方法去处理,动态代理也就不那么神秘了。
jad工具使用方法:
在命令行下输入:
jad -o -r -s java -d 生成Java文件路径位置 字节码文件位置/类名.class
例如:jad -o -r -s java -d src *.class