死磕,JAVA 代理模式(Proxy),一文全懂

疯狂创客圈:如果说Java是一个武林,这里的聚集一群武痴, 交流着编程的各种招式
QQ群链接:
疯狂创客圈QQ群

在Java编程中,代理模式是一种极为重要的设计模式。

什么是代理模式呢?简单的说,当不想直接访问,或者无法直接访问某个方法时,可以通过一个代理对象来间接访问,这便是代理的模式。


为什么在这里讲解代理模式呢? 
Java中的代理模式,与Java的反射技术关系非常的密切,故与反射技术一起分析和研究。


代理场景一:访问日志

打个比方,我们前面设计了一个简单的宠物类Pet类。

有两个方法: sayHello和sayAge,赘述如下:

现在问题来了: 如果在调用sayHello 和sayHi时,需要在文件中加上访问日志,该如何实现呢?

假如,要求实现的访问日志的实例如下:


2018-09-23 03:03:11 嗨,大家好!我是小狗-1
2018-09-23 03:03:11 嗨,我是小狗-1,我的年龄是:91
2018-09-23 03:03:11 嗨,大家好!我是小猫-1
2018-09-23 03:03:11 嗨,我是小猫-1,我的年龄是:4


访问日志,是一种非常普遍、非常常见的日常开发需求。

在我们的日常开发中,往往需要进行访问的统计,统计网站页面或者API接口的详细访问日志,以便进行后续的性能分析和产品优化。这种访问统计,小部分是保存在文件中,更多的是保存在数据库中,而大型的网站的访问日志,则是保存在分布式的文件系统中。


为了简单起见,本实例将日志保存在文件中。 按照日期为单位,每天在log目录下生成一个以当天为文件名称的log文件。

如何实现这个日志的功能呢?
有一种愚笨蠢的方式,就是直接修改原来的代码,加上日志的功能。为什么说这个方法愚笨蠢呢? 这明细不符合面向对象设计的原则:对修改封闭,对扩展开放。去修改原来的老的代码,一是可能会引入新的问题。更何况,老的代码可能来自其他的项目和工程,手上未必有老的代码。
 稍微高级一点的办法,是使用静态代理的模式,对原来的代码进行继承和扩展。


静态代理以及实例

首先说下静态代理的简单做法:
(1)首先,继承老的代码,增加中间层。
(2)在中间层中,加入扩展的功能。
(3)在中间层中,组合老的对象实例,调用老的代码。
(4)中间层代码,必须继承原来的接口。

保持接口的一致,利用面向对象的多态机制和向上转型功能,这样,能保障客户端代码不需要改变。

在思路上非常的简单,按照上面的思路,实现前面讲的案例:设计一个静态代理类,给Pet类增加日志的能力,设计UML图如下:

新设计一个PetLoger类,作为Pet类的代理,包含一个原Pet对象的组合,并且扩展了日志的能力。

PetLoger类代理对象控制对Pet原对象的引用,对客户端的调用,隐藏Pet原对象的具体信息。

PetLoger类的代码如下:

package com.crazymakercircle.Proxy;
import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.common.pet.Pet;
import com.crazymakercircle.util.FileLogger;
 class PetLogger  extends Pet implements IPet  {
    IPet pet;
    public PetLogger(IPet pet) {
        this.pet = pet;
    }

    @Override
    public IPet sayHello() {
        pet.sayHello();
        String logInfo = "嗨,大家好!我是" + pet.getName();
        FileLogger.info(logInfo);
        return this;
    }

    @Override
    public IPet sayAge() {
        pet.sayAge();
        String logInfo = "嗨,我是" + pet.getName() + ",我的年龄是:" + pet.getAge();
        FileLogger.info(logInfo);
        return this;
    }
}

在PetLogger 代理类的sayHello和sayAge方法中,进行了日志能力的增强,调用fileLogger,向文件中进行日志的记录。

客户端的调用代码如下:


package com.crazymakercircle.Proxy;
import com.crazymakercircle.common.pet.Cat;
import com.crazymakercircle.common.pet.Dog;
public class PetLoggerDemo
{
    public static void main(String[] args) {
        PetLogger petLogger=new PetLogger(new Dog());
        petLogger.sayHello();
        petLogger.sayAge();
        petLogger=new PetLogger(new Cat());
        petLogger.sayHello();
        petLogger.sayAge();
      }
}

客户端程序,除了对象的新建需要稍微的调整之外,其他的代码,都不需要改变。


运行客户端的程序,在日志文件中,进行了日志的保存。日志保存的结果,如下所示:

2018-09-23 03:03:11 嗨,大家好!我是小狗-1
2018-09-23 03:03:11 嗨,我是小狗-1,我的年龄是:91
2018-09-23 03:03:11 嗨,大家好!我是小猫-1
2018-09-23 03:03:11 嗨,我是小猫-1,我的年龄是:4

新设计的PetLogger,调用了一个通用的日志类FileLogger,向日志文件,追加日志。 

日志类FileLogger的代码,逻辑上比较简单。

由于与本例核心逻辑不相干,这里不赘述,欢迎来疯狂创客圈QQ群共享下载。 


静态代理的原理

通过上面的实例,对静态代理有一个比较清晰的了解。
静态代理模式,原理上由3大组成部分:
(1)抽象接口类(abstract subject)
该类的主要职责是声明目标类与代理类的共同接口方法,该类既可以是一个抽象类也可以是一个接口。
(2) 真实目标类(real subject)
该类也称为被委托类或被代理类,该类定义了代理所表示的真实对象,由其执行具体业务逻辑方法,而客户端则通过代理类间接地调用真实目标类中定义的方法。 
(3)代理类(proxy subject)
该类也称为委托类或代理类,该类持有一个对真实目标类(realSubject)的引用,在其所实现的接口方法中调用真实目标类中相应的接口方法执行,以此起到代理的作用。同时,代理类执行扩展的业务逻辑,实现能力的扩展。

    
前面讲到,静态代理模式,完美的符合了面向对象的一项基本原则:对修改封闭,对扩展开放。

然而,这种模式有一个显而易见的缺点。

下面做一个假设, 假设一种新的场景:需要对属性的合理性进行校验。静态代理模式的缺点就一览无余了。

代理的场景二:属性校验


在实际的开发中,客户会一个接着一个,不断提出新的要求。这也符合互联网开发的迭代思维,是在一次又一次的迭代开发中,产品的功能一步一步丰富和完善的。

这里假设一种新的场景:
在解决完了日志的能力扩展之后,领导或者客户又提出了一项新的能力扩展,需要对宠物的年龄进行属性校验。

校验的逻辑是:对于在合理范围之外的宠物年龄,要求进行年龄的保密,不暴露给外界。

为了进行属性校验,先进行两项准备工作:
(1)定义一个注解,用来进行年龄的配置注解的代码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AgeRange {

int min();
int max();
}

(2)第二项准备工作是: 给Pet 类的age成员,加上年龄的注解标记


public class Pet  implements IPet{
 .........
    @AgeRange(max = 15,min = 2)
    protected int age;
..........

}


接下来,万事具备,只欠东风了。

为了实现对年龄进行校验,可以使用静态代理模式。


参考前面的案例,再一次对Pet类进行扩展,新增一个AgaChecker 代理类,在 AgaChecker 代理类的sayAge方法中,对age的属性值进行校验。

AgaChecker 代理类的代码如下:

class AgaChecker extends PetLogger implements IPet {
    IPet pet;
    public AgaChecker(IPet pet) {
        super(pet);
        this.pet = pet;
    }

    @Override
    public IPet sayAge() {
        Field field = null;
        try {
            field = Pet.class.getDeclaredField("age");
        } catch (NoSuchFieldException e) {
           e.printStackTrace();
        }
        AgeRange anno = field.getAnnotation(AgeRange.class);
        if (this.getAge() > anno.max() || this.getAge() <anno.min() ) {
            Logger.debug("sorry,my age is secret ");
            return  this;
        }
        pet.sayAge();
        return this;

    }
}


在上面的 AgaChecker 代理类的sayAge方法中, 首先取得了注解的min和max值,然后对age 进行校验:如果不在合理范围之类, 不在输出宠物的年龄,实现了保密的目的。

在这个代码中, 应用了注解的技术、 反射的技术、静态代理的技术。

几项Java的比较牛逼的技术,都全面的得到了引用。 然后,通过新技术的应用,优美的解决了客户的问题。

正在心里洋洋得意之时,客户或者领导不断的暗示你:这个不是终点,后面几轮迭代,还需要对持续Pet类进行扩展和改进,还需要扩展性能统计能力、安全控制能力、异常处理能力 ........

在这种场景下,估计作为开发者的你,也是头都大了。

难道每一种场景,都去增加一个代理类吗? 显然,静态代理模式的缺点,已经昭然若揭的出来了。


静态代理的一个显著的缺点是:

一个静态代理类,只服务于一种场景的能力增强。反过来说,每一次能力的增强,需要设计一个新的代理类。

如果能力增强的场景很多,就需要设计较多的代理类型。在这种情况下,若采用静态代理,则会出现新增静态代理类多,造成代码量大,从而导致代码冗余、重复的问题。

如何解决这个问题呢?
上帝告诉我们:办法总比问题多。 其办法之一就是:动态代理。

动态代理

动态代理则与静态代理相反,不需要手工实现代理类。
动态代理通过反射机制,动态地生成代理类型。在编码阶段,不需要编写代理类。这个代理类,由JDK通过反射技术,在执行阶段动态生成,所以也叫动态代理。

通过JDK 的方式,实现动态代理的代码很简单,大致如下3步:
(1)构造一个能力扩展对象,能力扩展对象需要实现InvocationHandler接口,实现该接口的invoke抽象方法。能力扩展的代码逻辑,写在此invoke方法中。 InvocationHandler接口是JDK的预定义接口,位于 java.lang.reflect 包中。
(2)在JDK生成运行时代理对象前,需要取得真实目标对象(real subject)的接口,作为运行时代理类需要实现的接口。
(3)将第一步构造的InvocationHandler接口的能力扩展对象、第二步获取的真实目标对象的接口、还加上一个类加载器,三者凑齐一起,作为入口参数,传入java.lang.reflect.Proxy类的newProxyInstance方法中,获取JDK 在运行阶段生成动态代理对象,然后,大功告成。

本例中,将以上的第二步、第三个步骤进行了封装,写成了一个通用的获取代理对象的方法,具体如下:

public static Object newProxyInstance(IPet targetObject, InvocationHandler handler ) {
    Class targetClass = targetObject.getClass();
    ClassLoader loader = targetClass.getClassLoader();
    //被代理类实现的接口
    Class<?>[] targetInterfaces = ReflectionUtil.getInterfaces(targetClass);
    Object proxy = Proxy.newProxyInstance(loader, targetInterfaces, handler);
    return proxy;
}

为了代码的复用,将以上的定义的newProxyInstance方法,放置在一个与反射相关的Util类——ReflectionUtil类中,供后续调用。

顺便说一下,上面这个方法,后面会反复用到,用来取得动态代理类型对象。

如果这个三步暂时不好理解,看两个例子就清楚了。


动态代理实例

按照上面的三步,来实现动态代理实例,实现前面日志记录的能力。
其第一步,构造一个能力扩展对象LogHandler,实现InvocationHandler接口,实现该接口的invoke抽象方法,代码如下:

package com.crazymakercircle.Proxy;

import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.util.FileLogger;
import com.crazymakercircle.util.Logger;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
class LogHandler implements InvocationHandler {
    IPet pet;
    public LogHandler(IPet pet) {
        this.pet = pet;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (!(proxy instanceof IPet)) {
            Logger.info("proxy is not IPet, need not file log");
        }
        IPet pet = this.pet;
        if (method.getName().equals("sayHello")) {
//            method.invoke(pet, args);
            pet.sayHello();
            String logInfo = "嗨,大家好!我是" + pet.getName();
            FileLogger.info(logInfo);
        } else if (method.getName().equals("sayAge")) {
            pet.sayAge();
//            method.invoke(pet, args);
            String logInfo = "嗨,我是" + pet.getName() + ",我的年龄是:" + pet.getAge();
            FileLogger.info(logInfo);
        } else {
            return method.invoke(pet, args);
        }
        return null;
    }
}

动态代理方案,简化了编程。不需要继承抽象接口,编写一个一个的新的代理类型。 只需要将扩展的代码逻辑,写在InvocationHandler 实现类的invoke方法中即可。


完成了能力扩展实例的设计,只需要通过java.lang.reflect.Proxy类,传入前面说的3个参数,获取所需要的动态代理对象即可。
下面的代码,使用前面封装好的ReflectionUtil类中的newProxyInstance方法,取得动态代理对象,然后直接调用。
调用动态代理对象的代码如下:

package com.crazymakercircle.Proxy;
import com.crazymakercircle.common.pet.Cat;
import com.crazymakercircle.common.pet.Dog;
import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.util.ReflectionUtil;

import java.lang.reflect.InvocationHandler;

public class PetLoggerProxyDemo {
    public static void main(String[] args) {
//生成代理类的class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        // 创建被代理类的委托类,之后想要调用被代理类的方法时,
        // 都会委托给这个类的invoke方法
        IPet dog=new Dog();
        InvocationHandler handler = new LogHandler(dog);
        IPet petProxy =(IPet) ReflectionUtil.newProxyInstance(dog,handler);
        petProxy.sayHello();
        petProxy.sayAge();
        IPet cat=new Cat();
        // 创建被代理类的委托类,之后想要调用被代理类的方法时,
        // 都会委托给这个类的invoke方法
        handler = new LogHandler(cat);
        petProxy =(IPet)  ReflectionUtil.newProxyInstance(cat,handler);
        petProxy.sayHello();
        petProxy.sayAge();
    }
}

动态代理的原理


动态代理,是本质上是通过java.lang.reflect.Proxy的newProxyInstance方法,生成一个动态代理类的实例。
这里对 java.lang.reflect.Proxy的newProxyInstance方法,做一个详细的介绍。该方法的定义如下:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
....
}


我们已经知道,此方法会产生一个动态代理类,并且创建一个动态代理类的对象,并且返回。
此方法的参数中,最重要的是第二个和第三个参数。
第二个参数是动态代理类需要实现的抽象接口,此接口也是真实目标类的所实现的接口。
第三个参数是一个对真实目标类进行能力增强的InvocationHandler 对象。在此类中, 实现扩展的业务逻辑,比方日志的记录、属性的检查,或者其他的能力增强的逻辑。

JVM执行时,所产生的动态代理类,到底长成啥样呢?是否能看到动态扩展类的class字节码呢?
答案是肯定的。

需要一个额外的操作。在生成动态扩展类之前,加上一句系统属性设置的代码,就可以将JVM在运行时生成的动态扩展类的class文件,保存在当前的工程目录下。
设置系统属性的代码,具体如下:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");


这句代码告诉JVM 在将生成的字节码保存下来,默认的保存为项目根路径的com/sun/proxy/下。
以前面的日志动态代理实例为例,运行程序,可以发现 在com/sun/proxy/路径下生成了$Proxy0.class字节码文件。如果IDE有反编译的能力,可以在IDE中直接打开,后可以看到它的源码如下:

package com.sun.proxy;

import com.crazymakercircle.common.pet.IPet;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy3 extends Proxy implements IPet {
    private static Method m1;
    private static Method m6;
    private static Method m3;
    private static Method m2;
    private static Method m4;
    private static Method m5;
    private static Method m0;

    public $Proxy3(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final IPet sayHello() throws  {
        try {
            return (IPet)super.h.invoke(this, m6, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String getName() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final IPet sayAge() throws  {
        try {
            return (IPet)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int getAge() throws  {
        try {
            return ((Integer)super.h.invoke(this, m5, (Object[])null)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m6 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("sayHello", new Class[0]);
            m3 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("getName", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m4 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("sayAge", new Class[0]);
            m5 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("getAge", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

通过代码可以看出,这个动态代理类,其实只做了两件简单的事情:
(1)实现了抽象接口类的每一个抽象方法
上面的例子,是实现了IPet接口的动态代理类。除了实现了Ipet接口的sayHello、sayAge等四个方法,还有额外的三个方法。
这个额外的三个方法,是从java.lang.Object中继承来的equals()、hashCode()、toString()方法。这个3方法,都在代理类中,生成了对应的代理实现。
(2)在每一个代理实现的方法中,其实代码很简单。仅仅是统一调用了InvocationHandler对象的invoke()方法。并且将调用的参数,进行了二次传递。
而实际上,InvocationHandler对象的invoke()方法,正是我们进行能力增强的方法。


动态代理实现属性检查


使用动态代理,实现多种能力的增强,就不需要再继承抽象接口,实现很多的能力增强类了。
比如,使用动态代理的方法,进行年龄属性的合理性检查,为以下两步:
(1)实现InvocationHandler 的invoke方法,加入能力增强的代码,进行age的合理性检查
(2)通过Proxy.newProxyInstance方法,取得动态代理对象,可以使用。

实现属性检查的能力增强,第一步的代码如下:

package com.crazymakercircle.Proxy;

import com.crazymakercircle.anno.AgeRange;
import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.common.pet.Pet;
import com.crazymakercircle.util.Logger;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

class AgeCheckerHandler implements InvocationHandler {
    IPet pet;

    public AgeCheckerHandler(IPet pet) {
        this.pet = pet;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if (method.getName().equals("sayAge")) {
            Field field = null;

            try {
                field = Pet.class.getDeclaredField("age");
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
                return null;
            }
            AgeRange anno = field.getAnnotation(AgeRange.class);
            if (pet.getAge() > anno.max() || pet.getAge() < anno.min()) {
                Logger.debug("sorry,my age is secret ");
                return null;
            }
           return pet.sayAge();
//            return   method.invoke(pet, args);
        } else {
            return method.invoke(pet, args);
        }
    }
}

在invoke方法中,首先对调用的方法进行判断,只对sayAge进行增强。对年龄进行边界的校验,边界之外的直接保密。
除了sayAge之前的其他的方法,还是直接执行目标对象的原方法。

这里执行原目标对象的方法,使用的是反射的方式:

return  method.invoke(pet, args);


总结起来,使用动态代理,主要通过InvocationHandler 的invoke方法来调用具体的被代理方法。动态代理可以使我们的代码逻辑更加简洁,不需要写一大堆的代理类,不再需要关心到底代理谁。

最后,说明一下,动态代理的,可以直接通过JDK 实现,也可以使用第三方的字节码生成工具cglib等来实现。
使用第三方库进行动态代理,原理与JDK的方法,基本相同,这里不做展开。

猜你喜欢

转载自blog.csdn.net/crazymakercircle/article/details/82826148