浅析设计模式-代理模式

作者: RunAlgorithm | 来源:发表于2017-08-21 23:45 被阅读260次
  1. 定义
  2. 简单设计
  3. Java 的实现
    3.1. 静态代理
    3.2. JDK 动态代理
    3.3. CGLIB 动态代理
  4. 应用实例
    4.1. 权限控制
    4.2. 方法执行时间统计

1. 定义

代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

代表我们做一些业务,这些业务我们可能会因为资源的限制或者信息的匮乏等无法完成,也可以通过代理对用户进行保护,隔绝某些信息

现实世界的模型,比较常见的有

比如,我们使用代理服务器进行翻墙,访问外网。因为直接访问的话,会被 GFW 挡住了

比如,海淘、代购,进入海外,可以买到我们因为没有护照无法过去买到的产品

比如,一些商品的代理商,也是帮忙购买商品,但代理商有可能已经囤好货,直接发货过来

代理和我们的行为一样,但做了我们因为资源和某些限制做不到的事情。同时,代理可以做一些业务的增强,比如添加缓存、做安全策略(防火墙)、做任务调度合理分配资源等等

代理可以有很多丰富的功能,所以有很多分类,比如

  • 远程代理(Remote Proxy)

    为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。

  • 保护代理(Protect Proxy)

    控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限

  • 虚拟代理(Virtual Proxy)

    如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。

  • 缓冲代理(Cache Proxy)

    为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。

  • 智能引用代理(Smart Reference Proxy)

    当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。

通过代理模式,可以做 AOP 编程

2. 简单设计

抽象主题角色

声明真实主题和代理主题的公共接口

使用者针对抽象主题角色编程

代理主题角色

持有对真实主题角色的引用,可以代理真实主题角色执行某些任务;可以控制真实主题角色的访问;可以控制真实主题的创建和销毁

真实主题角色

实现了真实的业务操作。由代理主题角色来间接调用完成操作

代理模式-类图.png

3. Java 的实现

3.1. 静态代理

代理对象和被代理对象实现同样的接口,或者继承相同的父类

比如,有一个请求类

public interface Request {
    void get();
}

public class SimpleRequest implements Request {

    @override
    void get() {
        System.out.println("简单请求");
    }
} 

需要代理这个请求类做一些增强业务,可以创建这样的代理类

public class RequestProxy implements Request {
    private SimpleRequest mRequest;
    public RequestProxy(SimpleRequest request) {
        mRequest = request;
    }
    
    @override
    void get() {
        // 前置增强处理
        mRequest.get();
        // 后置增强处理
    }
}

静态代理的缺点是我们需要创建多个代理类,量大了不好维护

3.2. JDK 动态代理

使用 reflect 包,在内存中动态生成代理对象

不需要实现接口,但是需要指定要代理的接口类型

比如我们有一个接口 SayHello

public interface SayHello {
    void hello();
}

还有一个实现类 SayHelloImpl

public class SayHelloImpl implements SayHello {
    public void hello() {
        System.out.println("Hello world!");
    }
}

如何使用 reflect 包提供的工具,在不需要静态创建代理类的情况下,动态创建代理对象?

首先实现 InvocationHandler,用来处理我们要代理的一些细节

public class SayHelloInvocationHandler implements InvocationHandler {
k
    private Object mTarget;

    public SayHelloInvocationHandler(Object target) {
        mTarget = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("hello".equals(method.getName())) {
            System.out.println("开始");
            Object result = method.invoke(mTarget, args);
            System.out.println("结束");
            return result;
        }
        return method.invoke(mTarget, args);
    }
}

method.invoke 相当于我们调用了 mTarget 的方法

然后用 Proxy 创建代理对象

SayHello hello = new SayHelloImpl();
SayHelloInvocationHandler handler = new SayHelloInvocationHandler(hello);
SayHello helloProxy = (SayHello) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);
helloProxy.hello();

所以,我们已经创建好 SayHello 的代理对象 helloProxy 了

执行 helloProxy.hello() 会有这样的输出

开始
Hello world!
结束

小结:

  • 用这种方式实现代理,不需要静态创建代理类
  • 被代理的对象必须实现接口

3.3. CGLIB 动态代理

底层使用 ASM 对字节码进行修改,动态生成被代理类继承被代理的类,重写方法,织入通知

即使代理类没有实现任何接口也能够进行动态代理

但是因为基于继承,所以无法代理 final 类型的类

运行速度会比JDK 动态代理快

核心类有

  • Enhance

    增强器,传入要代理的对象,以及被代理的时候触发的回调实现。会生产出代理对象

  • MethodInterceptor

    方法拦截器,代理触发的回调的一种,可以用来拦截方法做业务

  • MethodProxy

    Method 的代理类,用来调用原本的方法

简单的应用如下:

我们有这样一个类 HelloWorld

public class HelloWorld {

    public void hello() {
        System.out.println("Hello world!");
    }
}

然后实现了MethodInterceptor ,对方法进行拦截,并做增强处理

public class HelloWorldInterceptor implements MethodInterceptor {

    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("开始");
        Object result = methodProxy.invokeSuper(o, args);
        System.out.println("结束");
        return null;
    }
}

最后通过 Enhancer 来创建代理类,生成代理对象

HelloWorldInterceptor interceptor = new HelloWorldInterceptor();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloWorld.class);
enhancer.setCallback(interceptor);

HelloWorld helloWorld = (HelloWorld) enhancer.create();
helloWorld.hello();

运行输出

开始
Hello world!
结束

不过 cglib 不能直接在 android 上使用,因为 android 平台的字节码和 java 平台有区别

4. 应用实例

4.1. 权限控制

保护代理的应用,可以给不同的用户提供不同级别的使用权限

已经有一个类有一个执行方法,希望在不改变源码的情况下扩展,对内部方法的执行加入权限控制,有权限的执行,没有权限的不执行

代理模式是很多权限控制系统的基础,比如服务端的 Spring 的权限控制就是基于代理模式

我们用 JDK 动态代理来实现一个

4.1.1. 需求场景

我们有三个业务操作,评论、举报、删除,已经实现了功能。现在需要对每个业务操作都加入权限控制,只有登录的用户才能进行操作

4.1.2. 思路

我们为每一个业务都创建一个管理类。使用 JDK 动态代理,所有管理都实现了统一的接口

public interface Manager {
    void handle();
}

然后分别创建 CommentManager,ReportManager,DeleteManager,实现了该接口。比如 CommentManager

public class CommentManager implements Manager {
    public void handle() {
        System.out.println("进行评论");
    }
}

使用代理模式,为管理类创建代理类,对 handle 方法进行权限控制

public class LoginProxy implements InvocationHandler {

    private boolean mLogin;
    private Object mTarget;

    public Manager getProxy(Object obj, boolean login) {
        mLogin = login;
        mTarget = obj;
        return (Manager) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (!"handle".equals(method.getName())) {
            return method.invoke(mTarget, args);
        }
        if (mLogin) {
            System.out.println("已经登录,开始执行");
            Object result = method.invoke(mTarget, args);
            System.out.println("执行结束\n");
            return result;
        } else {
            System.out.println("未登录,不执行");
            return null;
        }
    }
}

然后,想要为每一个操作新增登录权限的验证,只需要创建有对登录权限进行校验的代理即可。下面的例子是用户登录的情况,login = true

public static void main(String[] args) {
    LoginProxy proxy = new LoginProxy();

    // 评论
    Manager commentManager = proxy.getProxy(new CommentManager(), true);
    commentManager.handle();

    // 举报
    Manager reportManager = proxy.getProxy(new ReportManager(), true);
    reportManager.handle();

    // 删除
    Manager deleteManager = proxy.getProxy(new DeleteManager(), true);
    deleteManager.handle();
}

4.1.3. 变化

权限发生变化,需要登录,并且只有管理员才可以进行删除

如何在不修改几个管理类的情况下,再新增管理员权限检验代理类

public class AdminProxy implements InvocationHandler {

    private Object mTarget;
    private boolean mAdmin;

    public Manager getProxy(Object obj, boolean admin) {
        mTarget = obj;
        mAdmin = admin;
        return (Manager) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (!"handle".equals(method.getName())) {
            return method.invoke(mTarget, args);
        }
        if (mAdmin) {
            System.out.println("是管理员,开始执行");
            return method.invoke(mTarget, args);
        } else {
            System.out.println("不是管理员,不执行");
            return null;
        }
    }
}

然后把代理嵌套使用。下面的例子是用户已经登录,但无管理员权限

public static void main(String[] args) {
    LoginProxy loginProxy = new LoginProxy();
    AdminProxy adminProxy = new AdminProxy();
    Manager deleteManager = loginProxy.getProxy(adminProxy.getProxy(new DeleteManager(), false), true);
    deleteManager.handle();
}

可以得到以下输出:

已经登录,开始执行
不是管理员,不执行
执行结束

这样还可以继续添加不同的权限,不同的业务逻辑,需要更换或者修改权限,只需要重新组合一下他们的权限代理类

4.1.4. 小结

上面的例子很简单,仅仅是提供了一个思路,具体的场景要复杂得多

使用动态代理做权限控制系统,有这样的好处

  • 符合开闭原则,没有修改 Manager 类就增加了权限控制
  • 符合依赖反转,代理类没有在内部创建要代理的对象,由外部配置,然后传入
  • 符合单一职责,Manager 只做相应的业务,代理对象只处理权限控制
  • 比起静态代理,可以少创建很多的代理类

4.2. 方法执行时间统计

这是智能引用代理的应用,并不会改变被代理对象的行为,而是增加了一些额外的操作

可以使用代理模式,对方法的执行时间进行记录。复杂的话,可以做一套框架,集成记录、存储、上报等功能,还可以加入注解来进行过滤

我们这里做一个小案例,使用 cglib 的动态代理,监控方法执行时间并且打印出来

假设我们有一个类,里面有多个方法,都有耗时操作,比如

public class SimpleObject {

    public void execute1() {
        int time = new Random().nextInt(9);
        try {
            Thread.sleep(time * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void execute2() {
        int time = new Random().nextInt(9);
        try {
            Thread.sleep(time * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void execute3() {
        int time = new Random().nextInt(9);
        try {
            Thread.sleep(time * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这时候,就可以使用 cglib 动态代理,创建一个监控器,对被代理的对象的方法进行耗时统计

public class ExecuteTimeMonitor<T> implements MethodInterceptor {

    @SuppressWarnings("unchecked")
    public T getProxy(Class<T> cls) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(cls);
        enhancer.setCallback(this);
        return (T) enhancer.create();
    }

    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = methodProxy.invokeSuper(o, args);
        long passTime = System.currentTimeMillis() - startTime;
        System.out.println(method.getName() + " pass " + passTime);
        return result;
    }
}

使用的时候,不使用原生的 SimpleObject, 而是由 ExecuteTimeMonitor 生成的代理类

public static void main(String[] args) {
    SimpleObject object = new ExecuteTimeMonitor<SimpleObject>().getProxy(SimpleObject.class);

    object.execute1();
    object.execute2();
    object.execute3();
}

这样,在没有修改 SimpleObject 实现的基础上,对内部的方法都进行了耗时的统计

execute1 pass 2010
execute2 pass 8001
execute3 pass 4001

Process finished with exit code 0

相关文章

网友评论

    本文标题:浅析设计模式-代理模式

    本文链接:https://www.haomeiwen.com/subject/juckdxtx.html