Java 设计模式入门 —— 代理模式

143 阅读7分钟

代理模式(Proxy Pattern)是一种结构性的设计模式。代理模式为目标对象提供了一个替身,用于控制对这个目标对象的访问。也就是说,原本可以直接访问目标对象的,现在转换为通过代理对象间接访问目标对象。这样的好处就是,在目标对象的基础上,增强额外的功能操作,从而实现扩展目标对象的功能。当然,有些场景下目标对象无法直接访问,那么也可以使用代理对象充当中间媒介。

目标对象因为是被代理对象所代理的对象,有些地方也把目标对象称为被代理对象或者远程对象。

代理模式主要呈现的形态有三种:静态代理、动态代理和 cglib 代理(本质上也是动态代理范畴)。

静态代理

静态代理(Static Proxy)是先定义一个父类或者一个接口,然后让目标对象与代理对象一起继承相同的父类或者实现同一个接口。目标对象和代理对象实现相同的接口,然后通过调用相同的方法来调用目标对象的方法。

案例

以从“京东购买 10 台电脑”为例。

  1. 定义一个接口;
public interface Sell {
 float sell(int amount);
}
  1. 创建一个目标对象;
public class ComputerManufacturer implements Sell {
 @Override
 public float sell(int amount) {
 //The ex-factory is ¥85.
 return 8500 * amount;
 }
}

电脑生产商出售每台电脑的价格是 8500 元,且不接受用户直接从电脑生产商购买电脑。用户需要购买电脑,需要通过京东商城。

  1. 创建一个代理对象;
public class JD implements Sell{
    private ComputerManufacturer manufacturer = new ComputerManufacturer();
    @Override
    public float sell(int amount) {
        float price = manufacturer.sell(amount);
        float finalPrice = price + price * 20; // Function enhancement.
        return finalPrice; // Price that the end user need to pay.
    }
}

京东代理了电脑生产商的电脑,且在出厂价格 8500 元的基础上提价了 20%。 此处,就是代理对象对目标对象的增强。

  1. 用户购买电脑。
public class EndUser {
    public static void main(String[] args) {
        JD jd = new JD();
        float pay = jd.sell(10);// Ten computers need to be purchased.
        System.out.println("I purchased ten computers and paid " + pay + "CNY.");
    }
}

这样,消费者就可以从京东购买电脑了。 大家可能会有疑问为什么不直接创建 ComputerManufacturer 对象,直接调用 Sell() 方法来实现相同的功能。这是因为除了京东,可能还有淘宝、拼多多等商城需要从电脑厂商进电脑,并且每个商城提点不一样,那么使用代理模式就可以实现对代码的解耦,仅需要在关键的位置增加方法功能即可。

优点和缺点

静态代理的优点是可不修改目标对象的功能,通过代理对象就可以实现对目标功能的扩展。缺点是当目标对象的接口变化时,代理对象也需要同步变化,增加了维护成本。

结合上面的例子,比如当接口 Sell 发生变化的时候,比如需要多增加一个方法 Sell1(),那么此时目标对象 ComputerManufacturer 和代理对象 JD 都需要同步增加该方法。这样就导致了代码的维护成本增加。一处改,处处改。

动态代理

动态代理因为使用 Java JDK 的 API 接口来实现代理模式,因此这种代理模式也称为 JDK 代理、接口代理等。与静态代理不同的是,动态代理利用的是 API 方式在内存中构建代理对象,因此动态代理可以不用实现接口。在 Java JDK 中, 动态代理使用的是 java.lang.reflect.Proxy 包中的方法 newProxyInstance 来构建目标对象,该方法接收如下三个重要参数:

  1. ClassLoader loader

指定当前目标对象所使用的类加载器。

  1. Class<?>[] interfaces

目标对象实现的接口类型,使用泛型方法确认类型。

  1. InvocationHanlder h

事情处理,执行目标对象的方法时,会触发事情处理器方法,把当前执行的目标对象方法作为参数传入。

案例

还是以从“京东购买 10 台电脑”为例。

  1. 定义一个接口;
public interface Sell {
 float sell(int amount);
}
  1. 创建一个目标对象;
public class ComputerManufacturer implements Sell {
 @Override
 public float sell(int amount) {
 //The ex-factory is ¥85.
 return 8500 * amount;
 }
}
  1. 创建一个代理对象;
public class JDFactory {
    private Object target;

    public JDFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //Start proxy.
                Object priceObj = method.invoke(target, args);// Using reflect to call the target method.
                if (priceObj != null) {
                    float price = (float) priceObj;
                    price = price + price * 0.2f;
                    priceObj = price;
                }
                //End proxy.
                return priceObj;
            }
        });
    }
}
  1. 用户购买电脑。
public class EndUser {
    public static void main(String[] args) {
        ComputerManufacturer cm = new ComputerManufacturer();
        Sell sell = (Sell) new JDFactory(cm).getProxyInstance();
        float pay = sell.sell(10);
        System.out.println("I purchased ten computers and paid " + pay + "CNY.");
    }
}

当然,上述的 3 和 4,可以通过编写一个 Handler 类实现 InvocationHandler 接口来实现:

public class JDHandler implements InvocationHandler {
    private Object target;

    public JDHandler(Object target) {
        this.target = target;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //Start proxy.
        Object priceObj = method.invoke(target, args);// Using reflect to call the target method.
        if (priceObj != null) {
            float price = (float) priceObj;
            price = price + price * 0.2f;
            priceObj = price;
        }
        //End proxy.
        return priceObj;
    }
}

此时,用户购买电脑就是:

public class EndUser {
    public static void main(String[] args) {
        ComputerManufacturer cm = new ComputerManufacturer();
        JDHandler jdHandler = new JDHandler(cm);
        Sell sell = (Sell) Proxy.newProxyInstance(cm.getClass().getClassLoader(),
                cm.getClass().getInterfaces(), jdHandler);
        float pay = sell.sell(10);
        System.out.println("I purchased ten computers and paid " + pay + "CNY.");
    }
}

优点和缺点

动态代理最大的优点就是可以让代理对象不用实现具体的接口,减少类与类之间的耦合关系。缺点是只能基于接口实现代理。

CGLIB 代理

CGLIB (Code Generation Library)是一个功能强大、高性能的开源框架。它很好的弥补了 JDK 在动态代理时无法为没有接口的类提供代理的缺点。CGLIB 的原理是通过动态生成一个目标对象的子类,该子类重写了要代理类的所有不是 final 的方法。然后,在子类中采用方法拦截的方式拦截所有的父类方法的调用。CGLIB 的底层原理是对字节码的处理,使用的是字节码处理框架 ASM。

github.com/cglib/cglib…

案例

依旧以从“京东购买 10 台电脑”为例。

  1. 创建一个目标对象;
public class ComputerManufacturer {
    public float sell(int amount) {
        return amount * 8500;
    }
}

注意:这里的目标对象并没有实现任何的接口。

  1. 创建一个代理对象;
public class ProxyFactory implements MethodInterceptor {
    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object returnVal = method.invoke(target, args);
        return returnVal;
    }
}
  1. 用户购买电脑。
public class Main {
    public static void main(String[] args) {
        ComputerManufacturer manufacturer = new ComputerManufacturer();
        ComputerManufacturer proxyInstance = (ComputerManufacturer) new ProxyFactory(manufacturer).getProxyInstance();
        float pay = proxyInstance.sell(10);
        System.out.println("I purchased ten computers and paid " + pay + "CNY.");
    }
}

优点和缺点

CGLIB 的优点显而易见就是目标对象不需要实现任何的接口就可以实现代理。缺点就是需要导入外部依赖,且 CGLIB 与 ASM 的依赖包可能会存在冲突。

应用场景

代理模式的应用场景较为广泛,例如:Spring 框架中的 AOP 实现(日志的拦截、声明式事务的处理等)、RMI 远程方法调用,Redis 缓存请求等等。

动态代理的对比

基于 JDK 原生和使用 CGLB 动态代理的对比:

JDK 原生动态代理CGLB 动态代理
核心原理基于接口实现基于类集成方式实现
优点Java 原生支持,不需要依赖第三方包对于代理的目标对象无限制,无需实现接口
不足之处只能基于接口进行实现无法处理 final 方法
实现方式Java 原生支持,不需要任何依赖需要引用第三方依赖

推荐文章

  1. [java] simple implementation of proxy mode
  2. 设计模式-代理模式(静态代理、动态代理、cglib代理)_吾仄lo咚锵的博客-CSDN博客