首页 > 面试常考题 设计模式(四)-代理模式
头像
好未来-研发专家-高管
发布于 2021-07-06 13:01
+ 关注

面试常考题 设计模式(四)-代理模式

一. 什么是代理模式

1.1 概念

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用

也就是说客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。

通俗的来讲代理模式就是我们生活中常见的中介。

1.2 为什么不直接调用, 而要间接的调用对象呢?

一般是因为客户端不想直接访问实际的对象, 或者不方便直接访问实际对象,因此通过一个代理对象来完成间接的访问。

代理模式的UML图

代理类和真正实现都实现了同一个接口, 并且他们有相同的方法. 这样对于客户端调用来说是透明的.

二. 什么情况下使用动态代理

想在访问一个类时做一些控制, 在真实调用目标方法之前或者之后添加一些操作.
我们想买房, 但是买房的手续实在太复杂, 索性都交给中介公司. 中介公司就是代理, 我们的直接目的是买到房子, 中介公司在买房前后增加一些处理操作.

来看看代码实现

/**
 * 买房接口
 */
public interface IBuyHouse {
    public void buyHouse();
}

/**
 * 真正买房的人
 */
public class RealBuyHouse implements IBuyHouse{

    private String name;

    public RealBuyHouse(String name) {
        this.name = name;
    }

    @Override
    public void buyHouse() {
        System.out.println(this.name + "买房子");
    }
}

/**
 * 代理买房
 */
public class ProxyBuyHouse implements IBuyHouse{

    private IBuyHouse buyHouse;

    public ProxyBuyHouse(IBuyHouse buyHouse) {
        this.buyHouse = buyHouse;
    }

    @Override
    public void buyHouse() {
        beforeBuyHouse();

        buyHouse.buyHouse();

        afterBuyHouse();
    }

    public void beforeBuyHouse() {
        System.out.println("买房前操作--选房");
    }

    public void afterBuyHouse() {
        System.out.println("买房后操作--交税");
    }

}

public class Client {
    public static void main(String[] args) {
        IBuyHouse buyHouse = new RealBuyHouse("张三");

        IBuyHouse proxyBuyHouse = new ProxyBuyHouse(buyHouse);
        proxyBuyHouse.buyHouse();
    }
}

我们看到, 代理做的事情, 是代替主体完成买房操作, 所以, 类内部有一个主体实体对象.

代理模式有三种角色

Real Subject:真实类,也就是被代理类、委托类。用来真正完成业务服务功能;

Proxy:代理类。将自身的请求用 Real Subject 对应的功能来实现,代理类对象并不真正的去实现其业务功能;

Subject:定义 RealSubject 和 Proxy 角色都应该实现的接口

通俗来说,代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些额外的操作,并且不用修改这个方法的原有代码。如果大家学过 Spring 的 AOP,一定能够很好的理解这句话。

三. 代理模式的种类

按照代理创建的时期来进行分类的可以分为:静态代理、动态代理。

静态代理是由程序员创建或特定工具自动生成源代码,在对其编译。在程序运行之前,代理类.class文件就已经被创建了。

动态代理是在程序运行时通过反射机制动态创建的。

3.1 静态代理

先来看静态代理的实现步骤:

1)定义一个接口(Subject)

2)创建一个委托类(Real Subject)实现这个接口

3)创建一个代理类(Proxy)同样实现这个接口

4)将委托类 Real Subject 注入进代理类 Proxy,在代理类的方法中调用 Real Subject 中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

从实现和应用角度来说,静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

从 JVM 层面来说, 静态代理在编译时就将接口、委托类、代理类这些都变成了一个个实际的 .class 文件。

上面我们举的买房的例子就是静态代理.

源代码见上面第二点

静态代理总结:
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:我们得为每一个实现类都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。比如: 接口Subject增加一个方法. 所有的实现类, 代理类都要想听的增加.

3.2 动态代理

代理类是在调用委托类方法的前后增加了一些操作。委托类的不同,也就导致代理类的不同。

那么为了做一个通用性的代理类出来,我们把调用委托类方法的这个动作抽取出来,把它封装成一个通用性的处理类,于是就有了动态代理中的 InvocationHandler 角色(处理类)。

于是,在代理类和委托类之间就多了一个处理类的角色,这个角色主要是对代理类调用委托类方法的动作进行统一的调用,也就是由 InvocationHandler 来统一处理代理类调用委托类方法的操作。看下图:

从 JVM 角度来说,动态代理是在运行时动态生成 .class 字节码文件 ,并加载到 JVM 中的。
虽然动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助,Spring AOP、RPC 等框架的实现都依赖了动态代理。

就 Java 来说,动态代理的实现方式有很多种,比如:

  • JDK 动态代理
  • CGLIB 动态代理
  • Javassit 动态代理

很多知名的开源框架都使用到了动态代理, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

下面详细讲解这三种动态代理机制。

1. JDK动态代理

先来看下 JDK 动态代理机制的使用步骤:

第一步: 定义一个接口(Subject)

第二步: 创建一个委托类(Real Subject)实现这个接口

第三步: 创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些处理逻辑),并将委托类注入处理类

下面来看看InvocationHandler接口

package java.lang.reflect;

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

在InvocationHandler里面定义了invoke方法. 该方法有三个参数:

  • proxy:代理类对象(见下一步)
  • method:还记得我们在上篇文章反射中讲到的 Method.invoke 吗?就是这个,我们可以通过它来调用委托类的方法(反射)
  • args:传给委托类方法的参数列表

第四步: 创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象

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

Proxy.newProxyInstance()有三个参数

  1. 类加载器 ClassLoader
  2. 委托类实现的接口数组,至少需要传入一个接口进去
  3. 调用的 InvocationHandler 实例处理接口方法(也就是第 3 步我们创建的类的实例)

下面来看看案例实现

/**
 * 抽象接口
 */
public interface ISubject {
    void operate();
}

/**
 * 委托类, 也叫被代理类
 * 真正的处理逻辑
 */
public class RealSubject implements ISubject{
    @Override
    public void operate() {
        System.out.println("实际操作");
    }
}

/**
 * 代理对象的处理类
 */
public class ProxySubject implements InvocationHandler {
    private ISubject realSubject;

    public ProxySubject(ISubject subject) {
        this.realSubject = subject;
    }

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

        System.out.println("调用方法前---前置操作");

        //动态代理调用RealSubject中的方法
        Object result = method.invoke(realSubject, args);

        System.out.println("调用方法后---后置操作");
        return result;
    }
}


/**
 * 客户端调用类
 */
public class JdkProxyClient {
    public static void main(String[] args) {
        ISubject subject = new RealSubject();
        ISubject result = (ISubject)Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), new ProxySubject(subject));
        result.operate();
    }
}

最后的运行结果是:

调用方法前---前置操作

实际操作

调用方法后---后置操作

JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是实现类中有自己私有的方法,而接口中没有的话,该方法不能进行代理调用。

2. CGLIB动态代理

CGLIB(Code Generation Library)是一个基于 ASM 的 Java 字节码生成框架,它允许我们在运行时对字节码进行修改和动态生成。原理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑。关键词大家注意到没有,拦截!CGLIB 引入一个新的角色就是方法拦截器 MethodInterceptor。和 JDK 中的处理类 InvocationHandler 差不多,也是用来实现方法的统一调用的。

CGLIB 动态代理的使用步骤:

第一步: 首先创建一个委托类(Real Subject)

第二步: 创建一个方法拦截器实现接口 MethodInterceptor,并重写 intercept 方法。intercept 用于拦截并增强委托类的方法(和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似)

package org.springframework.cglib.proxy;

import java.lang.reflect.Method;

public interface MethodInterceptor extends Callback {
    Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;
}

该方法拥有四个参数:

  1. Object var1:委托类对象
  2. Method var2:被拦截的方法(委托类中需要增强的方法)
  3. Object[] var3:方法入参
  4. MethodProxy var4:用于调用委托类的原始方法(底层也是通过反射机制,不过不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)

第三步: 创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象.

也就是说:我们在通过 Enhancer 类的 create() 创建的代理对象在调用方法的时候,实际会调用到实现了 MethodInterceptor 接口的处理类的 intercept()方法,可以在 intercept() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

可以发现,CGLIB 动态代理机制和 JDK 动态代理机制的步骤差不多,CGLIB 动态代理的核心是方法拦截器 MethodInterceptor 和 Enhancer,而 JDK 动态代理的核心是处理类 InvocationHandler 和 Proxy。

代码示例

不同于 JDK的是, JDK 动态代理不需要添加额外的依赖,CGLIB 是一个开源项目,如果你要使用它的话,需要手动添加相关依赖。

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

第一步: 创建委托类

public class RealSubject {
    public void operate() {
        System.out.println("实际操作的动作");
    }
}

第二步: 创建拦截器类, 实现MethodInterceptor 接口. 在这里面可以对方法进行增强处理

public class ProxyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        System.out.println("调用真实操作之前---操作前处理");

       // 调用真实用户需要处理的业务逻辑
        Object object = methodProxy.invokeSuper(o, args);


        System.out.println("调用真实操作之后---操作后处理");
        return object;
    }
}

第三步: 创建代理对象Proxy:通过 Enhancer.create() 创建委托类对象的代理对象

public class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        // 创建cglib动态代理的增强类
        Enhancer enhancer = new Enhancer();

        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());

        // 设置委托类
        enhancer.setSuperclass(clazz);

        // 设置方法拦截器
        enhancer.setCallback(new ProxyMethodInterceptor());

        // 创建代理类
        return enhancer.create();
    }
}

从 setSuperclass 我们就能看出,为什么说 CGLIB 是基于继承的。

第四步: 客户端调用

public class CglibClient {
    public static void main(String[] args) {
        RealSubject proxy = (RealSubject)CglibProxyFactory.getProxy(RealSubject.class);
        proxy.operate();
    }
}

最后的运行结果:

调用真实操作之前---操作前处理
实际操作的动作
调用真实操作之后---操作后处理

3. JDK 动态代理和 CGLIB 动态代理对比

1)JDK 动态代理是基于实现了接口的委托类,通过接口实现代理;而 CGLIB 动态代理是基于继承了委托类的子类,通过子类实现代理。

2)JDK 动态代理只能代理实现了接口的类,且只能增强接口中现有的方法;而 CGLIB 可以代理未实现任何接口的类。

3)就二者的效率来说,大部分情况都是 JDK 动态代理的效率更高,随着 JDK 版本的升级,这个优势更加明显。

4. 什么情况下使用动态代理?

1)我们知道, 设计模式的开闭原则,对修改关闭,对扩展开放,在工作中, 经常会接手前人写的代码,有时里面的代码逻辑很复杂不容易修改,那么这时我们就可以使用代理模式对原来的类进行增强。

2)在使用 RPC 框架的时候,框架本身并不能提前知道各个业务方要调用哪些接口的哪些方法 。那么这个时候,就可用通过动态代理的方式来建立一个中间人给客户端使用,也方便框架进行搭建逻辑,某种程度上也是客户端代码和框架松耦合的一种表现。

3)Spring AOP 采用了动态代理模式

5. 静态代理和动态代理对比

1)灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的

2)JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 .class 字节码文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

四. 代理模式的优缺点

优点:

1、职责清晰。

2、高扩展性。

3、智能化。

缺点

1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。

2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

五. 代理模式使用了哪几种设计原则?

  1. 单一职责原则: 一个接口只做一件事
  2. 里式替换原则: 任何使用了基类的地方,都可以使用子类替换. 不重写父类方法
  3. 依赖倒置原则: 依赖于抽象, 而不是依赖与具体
  4. 接口隔离原则: 类和类之间应该建立在最小的接口上
  5. 迪米特法则: 一个对象应该尽可能少的和对其他对象产生关联, 对象之间解耦
  6. 开闭原则: 对修改封闭, 对扩展开放(体现的最好的一点)
    代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类

六. 代理模式和其他模式的区别

1. 代理模式和装饰器模式的区别

我们来看看代理模式和装饰器模式的UML图

1) 代理模式

2) 装饰器模式

两种模式的相似度很高. 接下来具体看看他们的区别

让别人帮助你做你并不关心的事情,叫代理模式
为让自己的能力增强,使得增强后的自己能够使用更多的方法,拓展在自己基础功能之上,叫装饰器模式

对装饰器模式来说,装饰者(decorator)和被装饰者(decoratee)都实现同一个 接口。
对代理模式来说,代理类(proxy class)和真实处理的类(real class)都实现同一个接口。
他们之间的边界确实比较模糊,两者都是对类的方法进行扩展,具体区别如下:

1、装饰器模式强调的是增强自身,在被装饰之后你能够在被增强的类上使用增强后的功能。增强后你还是你,只不过能力更强了而已;代理模式强调要让别人帮你去做一些本身与你业务没有太多关系的职责(记录日志、设置缓存)。代理模式是为了实现对象的控制,因为被代理的对象往往难以直接获得或者是其内部不想暴露出来。

2、装饰模式是以对客户端透明的方式扩展对象的功能,是继承方案的一个替代方案;代理模式则是给一个对象提供一个代理对象,并由代理对象来控制对原有对象的引用;

3、装饰模式是为装饰的对象增强功能;而代理模式对代理的对象施加控制,但不对对象本身的功能进行增强;

2. 代理模式和适配器模式的区别

适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口
来看看代理模式和适配器模式的UML图

更多模拟面试

全部评论

(3) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

近期精华帖

热门推荐