静态代理与动态代理

492 阅读5分钟

什么是代理?

设计模式的一种,通过代理对象去访问目标对象,而外部只能访问到代理对象。为了增强目标类的逻辑。主要解决直接访问对象带来的问题,想在访问类之前做一些控制。代理模式前提是必须有接口。

代理对象:增强后的对象。

目标对象:被增强的对象。

代理技术:一个对象代替另一个对象去完成一些工作。

JAVA实现动态代理的两种方式

静态代理和动态代理

静态代理

继承

代理对象继承目标对象,重写需要增强的方法

public class UserDaoImpl {
    public void query() {
        System.out.println("查询数据库");
    }
}
// 通过继承,增强query的功能
public class UserDaoLogImpl extends UserDaoImpl {
    @Override
    public void query() {
        System.out.println("-----Logger-----");
        super.query();
    }
}

测试

UserDaoLogImpl userDaoLog = new UserDaoLogImpl();
userDaoLog.query();

结果 -----Logger----- 查询数据库

聚合

代理对象和目标对象实现同一个接口,代理对象中要包含目标对象(通过构造器传递)

实现

// 定义接口
public interface UserDao {
    void query();
}
// 目标对象
public class UserDaoImpl implements UserDao {
    public void query() {
        System.out.println("查询数据库");
    }
}
// 代理对象
public class UserDaoLog implements UserDao {
    UserDao dao;
    // 构造器,传递目标对象
    public UserDaoLog (UserDao dao) {
        this.dao = dao;
    }
    @Override
    public void query() {
        System.out.println("-----Logger-----");
        dao.query();
    }
}

测试

UserDaoImpl target = new UserDaoImpl();
UserDaoLog proxy = new UserDaoLog(target);
proxy.query();

结果 -----Logger----- 查询数据库

缺点

两个实现代理的方式都会产生类爆炸式增长。如果在不确定的情况下, 尽量不要去使用静态代理。因为一旦你写代码,就会产生类,一旦产生类就爆炸。

动态代理

既然静态代理会产生类爆炸,那么怎么样才能避免。

通过接口反射生成一个类文件(.java),然后调用第三方编译技术,动态编译类文件(.class),然后使用ClassLoader把编译后的.class文件加载到JVM中,最后通过反射把这个类实例化(因为动态产生的类不在工程中,需要使用到UrlClassLoader)。即反射得到java文件 ----> 动态编译为.class文件 ----> ClassLoader加载到JVM中 ----> 反射生成类对象

如何动态的执行我们的逻辑

自定义接口及实现类,然后传递接口,通过method.invoke()来调用我们的目标方法

这就是jdk动态代理的简单版本实现

JDK动态代理解析

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

newProxyInstance,通过指定的interfaces接口返回代理类对象。传入 InvocationHandler的实现类来执行我们自定义的逻辑。

通过JDK动态代理得到代理类

public class UserDaoImpl implements UserDao {
    public void query() {
        System.out.println("查询数据库");
    }
}
public interface UserDao {
    void query();
}
public class MyInvocationHandler implements InvocationHandler {
	// 传入目标对象
    Object target;
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    /**
     * @param proxy 代理对象
     * @param method 目标方法
     * @param args 参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("MyInvocationHandler  ---   jdk");
        return method.invoke(target, args);
    }
}

测试

UserDao jdkUserDao = (UserDao) Proxy.newProxyInstance(Test.class.getClassLoader(),
                new Class[] {UserDao.class}, new MyInvocationHandler(new UserDaoImpl()));
jdkUserDao.query();

结果 MyInvocationHandler --- jdk 查询数据库

查看JDK动态代理源码来验证上述过程

1.首先通过newProxyInstancejava.lang.reflect.Proxy#getProxyClass0 的方法我们得到了一个代理类。然后通过java反射得到代理对象。

// 根据类加载器和需要实现的接口得到代理类class
Class<?> cl = getProxyClass0(loader, intfs);
// 得到代理类的构造器
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
// 根据参数new一个代理类
return cons.newInstance(new Object[]{h});

2.我们发现通过java.lang.reflect.Proxy#getProxyClass0得到了代理类。getProxyClass0方法中对代理类实现了缓存机制。如果存在由实现了给定接口的给定加载器定义的代理类,则将仅返回缓存的副本;否则,它将通过ProxyClassFactory创建代理类。此处不再展示其代码

3.跳过缓存机制,我们可以发现得到代理类的方法主要在java.lang.reflect.Proxy.ProxyClassFactory#apply中。默认我们生成的代理的名字为com.sun.proxy.$Proxy + num.主要代码如下

public static final String PROXY_PACKAGE = "com.sun.proxy";
private static final String proxyClassNamePrefix = "$Proxy";
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
long num = nextUniqueNumber.getAndIncrement();
// 得到代理类的名字
String proxyName = proxyPkg + proxyClassNamePrefix + num;
// 得到代理类的二进制文件
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
// 根据二进制数组得到代理类的class
return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);


我们输出动态代理类产生的二进制文件。

byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{UserDao.class});
FileOutputStream fileOutputStream = new FileOutputStream("f:\\$Proxy0.class");
fileOutputStream.write(bytes);
fileOutputStream.flush();
fileOutputStream.close();

代理类会实现equals、toString、hashCode方法,此处只列出重要方法。也可以运行测试代码查看生成的文件。

public final class $Proxy0 extends Proxy implements UserDao {
    private static Method m3;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
    public final void query() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException) {}
    }
    static {
        try {
            m3 = Class.forName("com.zjy.proxy1.UserDao").getMethod("query");
        } catch (NoSuchMethodException var2) {}
    }
}

发现当调用query方法时会调用传入的InvocationHandler中的invoke方法。同时传入生成的代理对象,目标对象的方法,和参数

defineClass0函数

private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

正常情况下,java会先调用classLoader去加载.class文件,然后调用loadClass函数去加载对应的类名,返回一个Class对象。而defineClass提供了另外一种方法,defineClass可以从byte[]还原出一个Class对象。

JDK动态代理

JDK中的代理是面向接口的代理,创建出来的代理都是java.lang.reflect.Proxy的子类。通过Proxy创建代理对象,当调用代理对象任意方法时候,会被InvocationHandler接口中的invoke方法进行处理。

cglib动态代理

Cglib的代理是面向父类的代理,本质上它是通过动态的生成一个子类去覆盖所要代理的类(非final修饰的类和方法),实际上为通过继承来实现的

区别

jdk动态代理只能对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,java不允许多重继承)。cglib能代理普通类

java的动态代理使用java原生的反射Api进行操作,在生成类上比较高效。cglib使用asm框架直接对字节码进行操作。在类的执行过程中比较高效。