Java:静态代理和动态代理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/new_Aiden/article/details/72566621

来个一个简单的需求。界面有个简单的按钮,实现对按钮的监听。这个需求太简单了。

首先我们定义一个OnClickListener接口。

public interface OnClickListener {
    void onClick();
}

再写一个OnClickListenerImpl实现OnClickListener接口

public class OnClickListenerImpl implements OnClickListener  {
    @Override
    public void onClick() {
        System.out.println("onClick");
    }
}

现在需要加功能了。为了有日志的回溯,提出了需要记录点击前和点击后的时间戳。这里的时间记录,就通过简单的System.out.println()实现好了。首先想到的方法肯定是重写OnClickListenerImpl。变成这样

public class OnClickListenerImpl implements OnClickListener  {
    @Override
    public void onClick() {
        System.out.println(System.currentTimeMillis());
        System.out.println("onClick");
        System.out.println(System.currentTimeMillis());
    }
}

但是,这样的修改并不符合开闭原则——即对新增功能开放,对修改功能关闭。我们这里直接改动了原来的代码,这是非常不合理的。这里又想到了一个办法,我们可以将OnClickListener再包装到一个类里面,通过那个类去实现时间的记录。代码就变成了这样:

public class OnClickListenerProxy implements OnClickListener {

    private OnClickListener listener;

    public OnClickListenerProxy(OnClickListener listener) {
        this.listener = listener;
    }

    @Override
    public void onClick() {
        System.out.println(System.currentTimeMillis());
        if (null != listener) {
            listener.onClick();
        }
        System.out.println(System.currentTimeMillis());
    }
}

这里,我们通过新建一个类,没有对原有的代码进行修改,达到了记录日志的效果。符合开闭原则。
上面的思想就是静态代理的思想,定义一个代理类,而具体的实现交给代理类的成员实现。上面的方案已经能解决很多问题了。

但是,假设原有实现OnClickListener接口的类有100个,要为这100个类都增加日志效果,可能就需要新建100个代理类去实现了。
这就涉及到动态代理。所谓的动态代理其实就是把真正的调用转交给一个其他的类,我们可以在这次调用前和调用后执行我们想要的代码,达到记录日志的目的。
在JDK中有原生的动态代理类——InvocationHandler。我们先来简单的了解一下这个类的使用。

class Handler implements InvocationHandler {
    private OnClickListener listener;

    public Handler(OnClickListener listener) {
        this.listener = listener;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(System.currentTimeMillis());
        // 这里才是真正的调用了onClick
        Object object = method.invoke(listener, args);
        System.out.println(System.currentTimeMillis());
        return object;
    }
}

我们建立测试类:

InvocationHandler handler = new Handler(new OnClickListenerImpl());
OnClickListener listener = (OnClickListener) Proxy.newProxyInstance(
    OnClickListener.class.getClassLoader(),
    new Class[]{OnClickListener.class},
    handler
);
listener.onClick();

我们新建了一个handler对象,然后通过Proxy.newProxyInstance()方法返回对应的OnClickListener。但是由于返回的OnClickListener对象由Proxy代理产生的,所以在调用onClick()函数的时候,就会调用Handler的invoke方法。

从而实现在Java中动态的为代码块增加功能。
首先需要确定的是动态代理肯定是使用了Java的反射机制,很多人都说反射机制都会变慢。我们先做一个对比吧。

次数        100    1000    10000    100000    1000000
直接调用      0      0        2        3          7
反射调用      3      5       12        14        20

事件消耗都是毫秒级的消耗。也就是说,反射机制不会太影响性能,并且灵活度更高

上述使用了Proxy.newProxyInstance就返回了一个OnClickListener的对象了。我们还是需要好好学习原理的。在这之前,我们先看看Java的反射使用的。我们现在的调用方法都是通过直接调用的,有没有一直方式是通过方法反射回去的呢?答案肯定是有的。

Method method = OnClickListener.class.getMethod("onClick");
method.invoke(new OnClickListenerImpl());

上面只是简单的使用了Method.invoke()方法,第一个参数就是调用方,剩下的参数就是onClick这个函数需要的参数,由于上面没有参数的,所以就没有填写。具体更多的使用方式,可以再好好研究。比如说:通过Field反射更改对象属性的值,通过AnnotatedType获取对象实现的接口等等。。。顺便提一句,Java中所有的注解类型本质都是接口类型。

反射能做的事情太多太多了,真正的调用方不需要直接调用,而是通过方法反射调用,这就是很基本的动态代理的思想。而Proxy.newProxyInstance()的实现呢?

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
        throws IllegalArgumentException
    {
        // 先判断Handler是否为null
        Objects.requireNonNull(h);
        // 将传入的接口clone一份
        final Class<?>[] intfs = interfaces.clone();
        // 获取安全管理器
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // 这里判断是否有权限产生代理类
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        // 这里获取JDK生成的Proxy的Class对象,而生成的Proxy是实现了OnClickListener接口的。
        Class<?> cl = getProxyClass0(loader, intfs);

        try {
            if (sm != null) {
                // 判断权限
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
            // 获取参数为InvocationHandler的Proxy的构造器
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            // 将方法都设置为public
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            // 返回的是OnClickListenerImpl对象,参数为Handler
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

但是有个很疑惑的问题,为什么在构造Proxy的时候需要传入一个Handler对象呢?我们并没有写那个构造器。唯一的解释就是——JDK在编译阶段为Proxy生成了一个带有Handler的构造器。我们怎样查看呢?

在main函数开始运行前,加上System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 就可以了,这里是我运行的结果。
这里写图片描述

生成了Proxy0.class。我们点开Proxy0.class文件。而我们很明显的就可以看到 Proxy0.class有一个带InvocationHandler参数的构造器。并且,我们可以看出Proxy0.class中所有的的实现,都是通过InvocationHandler.invoke()方法实现的,最终又会回到了我们自定义的Handler。
但是,如果我们没有使用Proxy.newProxyInstance的话,是不会生成Proxy.class的。

其实反射机制能完成很多很多很多事情:
1、结合类加载机制,实现热修复功能。我们可以将编译好的class文件,在代码运行时加载到jvm中,再运行新的代码。
2、完成很多很多框架。比如说Android中的xUtils通过注解的方式获取控件、Spring的面向切面等等。
3、调用编译时没有的代码,可以变成新加的功能。
4、。。。。。。

猜你喜欢

转载自blog.csdn.net/new_Aiden/article/details/72566621
今日推荐