设计模式之代理模式(Proxy Pattern)

1. 概述

代理模式(Proxy Pattern)也称为委托模式,它提供了一种代理以控制目标对象的访问;即通过代理对象访问目标对象。

在某些情况下,目标对象不能被客户端直接应用,而代理对象可以在客户端和目标对象之间起到中介的作用,好比你将一些繁琐的事情交给第三方去管理,那么第三方就是你的代理。举个生活中的例子:假设我们想邀请一位明星,那么并不是直接联系明星,而是联系明星的经纪人,来达到同样的目的。明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决。

1.1 定义

给目标对象提供一个代理,并由代理对象控制对目标对象的引用。

1.2 作用

可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

代理模式提现了编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。

1.3 代理模式的应用
  • 远程代理(Remote Proxy):为某个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实。(系统可以将Server部分代码隐藏,以便Client可以不必考虑Server存在)
  • 虚拟代理(Virtual Proxy):是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象。(适用一个代理对象表示一个十分耗资源的对象并在真正需要时才创建)
  • 保护代理(Protection Proxy):用来控制真实对象访问时的权限。
  • 智能引用(Smart Reference):是指当调用真实的对象时,代理处理一些另外的事情。
1.4 代理中涉及到的角色

代理模式角色

  • 抽象角色(Subject):为真实对象和代理对象提供一个共同的接口,一般是抽象类或者接口。
  • 代理角色(Proxy Subject):代理角色内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能够代替真实对象。同时,代理对象可以在执行真实对象的操作时,附加其他操作,相当于对真实对象的功能进行拓展。
  • 真实角色(Real Subject):最终引用的对象。

代理一般可以分为两类:静态代理和动态代理

2. 静态代理

所谓的静态代理,就是代理类在程序运行前就已编写好的。

2.1 静态代理实例:

静态代理实现比较简单,可以通过聚合和继承两种方式来实现,但聚合实现方式比继承实现方式更为灵活多变,所以一般推荐使用聚合方式来实现。

// 1. 定义一个抽象角色
public interface Subject {
    void doSomething();
}

// 2. 定义一个真实角色
public class RealSubject implements Subject {
    @Override
    public void doSomething() {
        System.out.println("真实角色类:doSomething");
    }
}

// 3.1 定义一个代理类角色,使用聚合方式实现
public class ProxySubject implements Subject {
    // 对真实角色类的引用
    private Subject subject;

    public ProxySubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public void doSomething() {
        System.out.println("真实角色执行前...");
        subject.doSomething();
        System.out.println("真实角色执行后...");
    }
}

// 3.2 定义一个代理类角色,使用继承方式实现(不推荐使用)
public class ProxySubject extends RealSubject {

    @Override
    public void doSomething() {
        System.out.println("真实角色执行前...");
        super.doSomething();
        System.out.println("真实角色执行后...");
    }
}

// 4. 定义测试类
public class PatternTest {
    
    @Test
    public void test() {
        Subject subject = new RealSubject();
        // 聚合方式实现
        ProxySubject proxy = new ProxySubject(subject);
        // 继承方式实现
//        ProxySubject proxy = new ProxySubject();
        proxy.doSomething();
    }
}

2.2 缺点
  • 代理类和委托类实现了相同的接口,实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有委托类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
  • 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。
3. 动态代理

代理类在程序运行时创建的代理方式被称为动态代理,动态代理主要有两种:JDK自带的动态代理和CGLib动态代理。如果目标对象实现了接口,采用JDK的动态代理;如果目标对象没有实现接口,就采用CGLib动态代理。

3.1 JDK自带动态代理

在Java的动态代理机制中,有两个重要的类:InvocationHandler(Interface)Proxy(Class),它们位于java.lang.reflect包下。

public interface InvocationHandler {

    /**
     * @param proxy: 被代理的对象
     * @param method: 被代理的方法 
     * @param args: 被代理方法的参数数组
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

/** 
 * 它是Proxy中的静态方法,返回由InvocationHandler接口接收的被代理类的一个动态代理类对象
 * @param loader: 类加载器
 * @param interfaces: 被代理类实现的接口
 * @param h: InvocationHandler实例
 */
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • JDK动态代理的实现
  1. 创建被代理的类以及接口
  2. 创建一个实现接口InvocationHandler的类,并实现invoke()

使用JDK动态代理类时,需要实现InvocationHandler接口,所有动态代理类的方法调用,都会交由InvocationHandler接口实现类里的invoke()方法去处理。这是动态代理的关键所在。

  1. 调用Proxy的静态方法(newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)),创建代理类。
  2. 通过代理调用方法。
public interface Moveable {
    void move();
}

public class Car implements Moveable {
    @Override
    public void move() {
        try {
            Thread.sleep(new Random().nextInt(1000));
            System.out.println("汽车行驶中...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 静态代理,主要与JDK动态代理进行对比
public class CarTimeProxy implements Moveable {
    private Moveable m;

    public CarTimeProxy(Moveable m) {
        this.m = m;
    }

    @Override
    public void move() {
        long startTime = System.currentTimeMillis();
        System.out.println("汽车行驶前");
        m.move();
        long endTime = System.currentTimeMillis();
        System.out.println("汽车行驶结束,用时:" + (endTime - startTime) + "毫秒");
    }
}

// JDK动态代理
public class TimeHandler implements InvocationHandler {
    private Object object;

    public TimeHandler(Object object) {
        this.object = object;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long startTime = System.currentTimeMillis();
        System.out.println("汽车行驶前");
        Object o = method.invoke(object, args);
        long endTime = System.currentTimeMillis();
        System.out.println("汽车行驶结束,用时:" + (endTime - startTime) + "毫秒");
        return o;
    }
}

public class PatternTest {

    @Test
    public void test() {
        Car car = new Car();
        
        // 静态代理
//        CarTimeProxy proxy = new CarTimeProxy(car);
//        proxy.move();

        // 动态代理
        TimeHandler timeHandler = new TimeHandler(car);
        Moveable m = (Moveable) Proxy.newProxyInstance(
                car.getClass().getClassLoader(),
                car.getClass().getInterfaces(),
                timeHandler);
        m.move();
    }
}
3.2 CGLib动态代理

CGLIB(Code Generation Library)是一个开源项目,是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口,通俗地说CGLib可以在运行时动态生成字节码。cglib.jar下载地址:https://github.com/cglib/cglib/releases/

JDK动态代理的缺陷:

JDK动态代理可以在运行时动态生成字节码,主要使用到了一个接口InvocationHandlerProxy.newProxyInstance静态方法。使用内置的Proxy实现动态代理有一个问题:被代理的类必须要实现某接口,未实现接口则没办法完成动态代理。

而通过cglib动态代理可以解决JDK动态的缺陷,即CGLib代理的类不需要实现接口。

使用CGLib完成动态代理,大概的原理是:CGLib继承被代理的类,重写方法,织入通知,动态生成字节码并运行。对指定目标类产生一个子类,通过方法拦截技术拦截所有父类的方法调用,因为是继承实现,所以final类是没有办法动态代理的。

目标类不能为final,目标对象的方法如果为final / static,那么就不会被拦截,即不会执行目标对象额外的业务方法

CGLIB动态代理实例:

// 不实现接口的被代理类
public class Train {
    public void move() {
        try {
            Thread.sleep(new Random().nextInt(1000));
            System.out.println("火车行驶中...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class CglibProxy implements MethodInterceptor {

    // 得到代理类的方法
    public Object getProxy(Class<?> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        long startTime = System.currentTimeMillis();
        System.out.println("火车行驶前");
        // 代理类调用父类的方法 (由于Cglib动态代理的实现是通过继承被代理类,因此代理类这里需要调用父类的方法)
        methodProxy.invokeSuper(o, args);
        long endTime = System.currentTimeMillis();
        System.out.println("火车行驶结束,用时:" + (endTime - startTime) + "毫秒");
        return null;
    }
}

public class PatternTest {

    @Test
    public void test() {
        CglibProxy proxy = new CglibProxy();
        Train train = (Train) proxy.getProxy(Train.class);
        train.move();
    }
}
3.3 JDK动态代理与cglib动态代理的区别

JDK动态代理使用Java的反射技术生成代理类,只能代理实现了接口的类,没有实现接口的类不能实现JDK动态代理,CGLib会在运行时动态的生成一个被代理类的子类,子类重写了被代理类中所有非final的方法,在子类中采用方法拦截的技术拦截所有父类方法的调用,不需要被代理类对象实现接口,从而CGLIB动态代理效率比Jdk动态代理反射技术效率要高

4. 三种代理模式对比
代理方式介绍缺点
静态代理可以做到在不修改目标对象的功能前提下,对目标功能扩展代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护
JDK动态代理代理对象不需要实现接口, 利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)目标对象一定要实现接口,否则不能用动态代理
CGLib代理静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象类实现代理需要引进第三方库;不能代理final修饰的类
5. 什么时候使用代理模式
  • 当我们想要隐藏某个类时,可以为其提供代理。
  • 当一个类需要对不同的调用者提供不同的调用权限时,可以使用代理类来实现(代理类不一定只有一个,我们可以建立多个代理类来实现,也可以在一个代理类中进行权限判断来进行不同权限的功能调用)。
  • 当我们要扩展某个类的某个功能时,可以使用代理模式,在代理类中进行简单扩展(只针对简单扩展,可在引用委托类的语句之前与之后进行)。
6. 参考

面向对象编程设计模式------代理模式(静态代理、动态代理)

Java代理模式

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值