现在有一个要求:不改变一个方法中的代码,如何在这个方法执行前后添加一些操作。
静态代理
继承代理
我们可以使用继承的方式如下图所示:
在简单的环境下继承完全可以胜任这个要求,但是随着操作的要就不断增多就会出现无限继承的场景。(当然我们这里不考虑在类中直接填写代码的情景我们的要求都封装在不同的方法中)
还有一点比较致命的就是我们如果想要更改变子类中操作的顺序,继承无法满足这样的灵活性。比如我们要更换上图中后两幅图的次序的话需要重新继承比较麻烦。下面的代码实现的就是在Tank的move方法前后加上了log信息。
坦克类(目标类)package proxy;
import java.util.Random;
public class Tank {
@Override
public void move() {
System.out.println("move......");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
日志代理
package proxy;
public class TankLogExtendsProxy extends TankTimeExtendsProxy{
@Override
public void move() {
System.out.println("logstart......");
super.move();
System.out.println("logend......");
}
}
接口代理
除了继承之外我们还可以使用接口来代理如下图所示:
接口代理非常灵活能够自由的交换操作顺序,在操作很多的情况下也不会出现无限继承的场景。实现代码如下:
接口package proxy;
public interface Moveable {
void move();
}
目标类
package proxy;
import java.util.Random;
public class Tank implements Moveable{
@Override
public void move() {
System.out.println("move......");
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
日志代理
package proxy;
public class TankLogProxy implements Moveable{
Moveable t;
public TankLogProxy(Moveable t) {
this.t = t;
}
@Override
public void move() {
System.out.println("logstart......");
t.move();
System.out.println("logend......");
}
}
时间代理
package proxy;
public class TankTimeExtendsProxy extends Tank{
@Override
public void move() {
long start = System.currentTimeMillis();
super.move();
long end = System.currentTimeMillis();
System.out.println("一共耗时"+(end-start));
}
}
测试类
package proxy;
public class Test {
public static void main(String[] args) {
Tank t = new Tank();
//更换以下代码的参数和位置就可以更换前置(后置)操作的执行顺序
TankLogProxy tlp = new TankLogProxy(t);
TankTimeProxy ttp = new TankTimeProxy(tlp);
ttp.move();
TankLogExtendsProxy tep = new TankLogExtendsProxy();
tep.move();
}
}
动态代理
有了静态代理我们可以轻松的实现文章开头所提的要求,实务上我们一般所用的就是基于接口的代理实现。事实上我们的初衷并不是想要写一些代理类,我们只是想将目标代码的前后加上一些操作。如果按照不断地写代理类的方式在操作多的时候工作量会特别的大。这个时候就用到了动态代理技术。
动态代理技术与静态代理技术的结果相同,只不过是动态的生成了一个代理对象。流程如下:
传入需要动态生成的参数--->生成代理类源码文件---->编译生成.class文件---->将类load到内存中--->生成一个代理对象。当然有些函数库能够直接生成二进制码的代理对象所以会省去中间的很多步骤。
接下来我们手动的实现以上步骤!
生成源码文件:假设现在所有的参数都传入在程序中形成一个代码字符串,可以通过IO流生成.java文件。
生成类文件:可以通过JavaComplier将原来的.java编译成.class文件。
将类文件装载到内存中:如果在src目录下可以通过ClassLoader来装载,如果为了更灵活可以用URLClassLoader来装载。当类装载到内存中我们就可以生成一个代理对象了。
事实上最难的一步就是如何传入参数!
先决条件:站在Java虚拟机的角度上每一个类,对象,方法都是一个对象。
动态生成接口
我们想要生成任何的接口,我们就需要将我们想要生成的接口当做参数传入构造代理对象的方法中。
动态生成方法
由于代理类与目标类都实现了特定的接口,所以我们可以拿到接口中的所有抽象方法。通过反射机制我们拿到了接口中的所有方法,同时包括我们要处理的方法。
方法中的逻辑
虽然我们生成了方法但是我们对于里面的处理方式一无所知,而处理方式往往是用户所指定的所以我们也要将处理方式传入代理类生成器中。实务上我们通常会定义一个处理类例如TankTimeHandler具体的处理逻辑都封装在这个类中,将这个对象传入代理类生成器中。这个处理类对象有一个invoke(当前代理对象,当前方法);这样的方法。最后通过间接调用实现动态代理。具体的结构如下图:
这样我们就大概理解了jdk动态代理的内核。总结一下我们需要传的三个参数
①实现什么样的接口
②如何处理的处理类
③这里需要一个类加载器
有了这三个参数我们就可以轻松地解决大量的问题。
JDK的动态代理
Jdk的动态代理的原理就是上图中所表示的。它的使用非常简单如代码如下:
代理类package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class TankProxy{
private Tank tank = new Tank();//被代理对象
public Object getTankProxy() {//返回代理对象
//类加载器 //实现哪个接口 //如何实现
return Proxy.newProxyInstance(TankProxy.class.getClassLoader(),tank.getClass().getInterfaces(),new InvocationHandler() {
@Override //代理对象 //要处理的方法 //方法中需要的一些参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("处理前");//前置操作
method.invoke(tank,args);
System.out.println("处理后");//后置操作
return null;
}
});
}
}
最后感谢马士兵老师!