静态代理和动态代理

字数 2373阅读 22

文章来自:郭霖的公众号https://mp.weixin.qq.com/s/dSt4ifbAaRxPAc7gCAVxoA
如果按照代理创建的时期来进行分类的话, 可以分为静态代理、动态代理

静态代理是由程序员创建或特定工具自动生成代理类,再对其编译,在程序运行之前,代理类.class文件就已经被创建了
动态代理是在程序运行时通过反射机制动态创建代理对象

微信图片_20190703124638.jpg

开源框架应用:

Spring框架是时下很流行的Java开源框架,Spring之所有如此流行,跟它自身的特性是分不开的,一个是IOC,一个是AOP

IOC是Inverse Of Control,即控制反转,也有人把IOC称作依赖注入。我觉得依赖注入这种说法很好理解,但不完全对;依赖注入是Dependency Injection的缩写,是实现IOC的一种方法,但不等同于IOC,IOC是一种思想,DI只是一种实现

AOP是Aspect Oriented Programming的缩写,即面向切面编程;与面向过程和面向对象的编程方式相比,面向切面编程提供了一种全新的思路,解决了OOP编程过程中的一些痛点。

IOC的实现原理是利用了JAVA的反射技术,那么AOP的实现原理是什么呢?就是动态代理技术,目前动态代理技术主要分为Java自己提供的JDK动态代理技术和CGLIB技术

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

/ 静态代理 /

静态代理是代理模式实现方式之一,比较简单,主要分为三个角色:客户端,代理类,目标类;而代理类需要与目标类实现同一个接口,并在内部维护目标类的引用,进而执行目标类的接口方法,并实现在不改变目标类的情况下前拦截,后拦截等所需的业务功能。

假设现在项目经理给你提了一个需求:在项目所有类的有关用户操作的方法前后打印日志;这种情况下就可以通过静态代理实现

做法就是:为每个相关类都编写一个代理类(业务重合的可以共用一个代理类),并且让它们实现相同的接口

public interface ILogin {
    void userLogin();
}
定义目标类:
public class UserLogin implements ILogin {

    @Override
    public void userLogin() {
        System.out.print("用户登录");
    }
}

定义代理类:

public class UserLoginProxy implements ILogin {

    private UserLogin mLogin;
    public UserLoginProxy() {
        mLogin = new UserLogin();
    }

    @Override
    public void userLogin() {
        System.out.print("登录前。。。");
        mLogin.userLogin();
        System.out.print("登录后。。。");
    }
}

客户端:

public class UserLoginProxy implements ILogin {
public class Test {
    public static void main(String[] args){
        ILogin loginProxy = new UserLoginProxy();
        loginProxy.userLogin();
    }
}

这样我们就在尽量不修改现有类的基础上实现了这个需求,同时客户端只用跟代理类打交道,完全不用了解代理类与目标类的实现细节,也不需要知道目标类是谁;当然你如果想指定目标类,将在外部创建目标类,传给代理类

这里只是在同步情况下的一种实现,如果是异步操作,就需要进行一些改动

静态代理总结:

优点:在符合开闭原则的情况下,对目标对象功能进行扩展和拦截
缺点:需要为每个目标类创建代理类和接口,导致类的数量大大增加,工作量大;接口功能一旦修改,代理类和目标类也得相应修改,不易维护

从静态代理的实现过程可以知道工作量太大,如果是在项目早期同步做还好,要是在接手老项目或者项目晚期再做,你可能要为成百上千个类创建对应的代理对象,那真的挺让人崩溃的;所以我们就需要想有没有别的方法:如何少写或者不写代理类却能完成这些功能

做Java的都知道一个.java文件会先被编译成.class文件,然后被类加载器加载到JVM的方法区,存在形式是一个Class对象(所谓的Class对象就是Class文件在内存中的实例);

我们构造出的任何对象实例是保存在Heap中,而实例是由其Class对象创建的(可以通过任意实例的getClass方法获取对应的Class对象),比如平时使用new关键字加构造方法Person p = new Person()就是将这个Class对象在Heap中创建一个实例;

可以看出要创建一个实例,最关键的是得到对应的Class对象,而得到Class对象追根溯源需要一个Java文件;那么我们要做的就是不写Java文件,而是直接得到代理Class对象,然后根据它通过反射创建代理实例

Class对象里面包含了一个类的所有信息,比如构造器,方法,字段等;那我们怎么在不写代理类的前提下获取到这些信息呢?

从静态代理的实现知道目标类和代理类实现了同一个接口,这是为了尽可能保证代理对象的内部结构和目标对象一致,这样代理对象只需要专注于代码增强部分的编写,所以接口拥有代理对象和目标对象共同的类信息,那么我们就可以从接口获取本来应该由代理类提供的信息,但是要命的是接口是不能实例化的啊

这就要讲到动态代理了:在动态代理中,不需要我们再手动创建代理类,只需要编写一个动态处理器及指定要代理的目标对象实现的接口,真正的代理对象由JDK在运行时为我们创建;JDK提供了java.lang.reflect.InvocationHandler和java.lang.reflect.Proxy来实现动态代理

JDK代理

Proxy.getProxyClass(ClassLoader, interfaces)方法只需要接收一个类加载器和一组接口就可以返回一个代理Class对象,然后就可以通过反射创建代理实例;其原理就是从传入的接口Class中,拷贝类结构信息到一个新的Class对象中,并继承Proxy类,拥有构造方法;站在我们的角度就是通过接口Class对象创建代理类Class对象

这里通过一个很骚的比喻来说明下:一个东厂太监(接口Class对象)有一家子财产,但是为了侍奉皇帝这一伟大事业,毅然决然的割了DD(没有构造方法),虽然实现了自己的理想,但是不能繁育下一代(不能构造器创建对象),也就没有后人继承自己的家业;但是好在华佗在世,江湖上有一个高人(Proxy),发明了一个克隆大法(getProxyClass),不仅克隆出了几乎和太监一样的下一代(新的Class),还拥有自己的小DD(构造方法),这样这个下一代就能继承太监的家产(类结构信息,其实是实现了该接口),同时还能娶妻生子,传给下一代(创建实例)

 public static Object loadProxy(Object target) throws Exception {
        //通过接口Class对象创建代理Class对象
        Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        //拿到代理Class对象的有参构造方法
        Constructor<?> constructors = proxyClass.getConstructor(InvocationHandler.class);
        //反射创建代理实例
        Object proxy = constructors.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("执行前日志..."+"\n");
                //执行目标类的方法
                Object result = method.invoke(target, args);
                System.out.println("执行后日志..."+"\n");
                return result;
            }
        });
        return proxy;
    }

    public static void main(String[] args) throws Exception {
        ILogin proxy = (ILogin) loadProxy(new UserLogin());
        proxy.userLogin();
    }

这样无论系统有多少目标类,通过传进来的目标类都可以获取到对应的代理对象,就达到我们在执行目标类前后加日志的效果了,同时还不需要编写代理类

Proxy类还有个更简单的方法newProxyInstance,直接返回代理对象,如下

 public static Object loadProxy(Object object) {
        return Proxy.newProxyInstance(
                object.getClass().getClassLoader(), //和目标对象的类加载器保持一致
                object.getClass().getInterfaces(), //目标对象实现的接口,因为需要根据接口动态生成代理对象
                new InvocationHandler() { //事件处理器,即对目标对象方法的执行

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("执行前日志...");

                        Object result = method.invoke(object, args);

                        System.out.println("执行后日志...");
                        return result;
                    }
                });
    }

JDK动态代理总结:

优点:相对于静态代理,极大的减少类的数量,降低工作量,减少对业务接口的依赖,降低耦合,便于后期维护;同时在某些情况下是最大的优势,即可以统一修改代理类的方法逻辑,而不需要像静态代理需要修改每个代理类

缺点:因为使用的是反射,所以在运行时会消耗一定的性能;同时JDK代理只支持interface的动态代理,如果你再继续深究源码,会发现,所有动态生成的代理对象都有一个共同的父类,即都继承于Proxy;
Java的单继承机制决定了无法支持class的动态代理,也就意味着你拿到动态生成的代理对象,只能调用其实现的接口里的方法,无法像静态代理中的代理类可以在内部扩展更多的功能

推荐阅读更多精彩内容