代理模式

代理设计模式(Proxy Design Pattern)

1. 介绍

1.1 定义

代理模式,是指客户端(Client)并不直接调用实际的对象(RealSubject),而是通过调用代理(Proxy),来间接的调用实际的对象。

应用实例: Windows 里面的快捷方式

1.2 主要作用

通过增加代理对象,间接访问目标对象,在访问目标对象时做一些控制

1.3 解决的问题

防止直接访问目标对象给系统带来的不必要复杂性。

2.模式原理

UML类图

3.实例讲解

接下来我用一个实例来对代理模式进行更深一步的介绍。
实例概况 程序员写代码之前要写文档

3.1 实现方式一:静态代理

步骤1: 创建抽象对象接口(Subject):声明要做的事

public interface IDeveloper {
    public void writeCode();
}

步骤2: 创建真实对象类(RealSubject)

public class Developer implements IDeveloper {
    private String name;

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

    @Override
    public void writeCode() {
        System.out.println("Developer " + name + " writes code");
    }
}

步骤3:创建代理对象类(Proxy),并通过代理类创建真实对象实例并访问其方法

public class DeveloperProxy implements IDeveloper {
    private IDeveloper developer;

    public DeveloperProxy(IDeveloper developer) {
        this.developer = developer;
    }

    @Override
    public void writeCode() {
        System.out.println("Write documentation...");
        this.developer.writeCode();
    }
}

步骤4:客户端调用

public class DeveloperTest {
    public static void main(String[] args) {
        IDeveloper andi = new Developer("Andi");
//        andi.writeCode();

        DeveloperProxy andiProxy = new DeveloperProxy(andi);
        andiProxy.writeCode();
    }
}

结果输出

Write documentation...
Developer Andi writes code
优点
  1. 易于理解和实现
  2. 代理类和真实类的关系是编译期静态决定的,和下文的动态代理比较起来,执行时没有任何额外开销。
缺点

每一个真实类都需要一个创建新的代理类。还是以上述文档更新为例,假设老板对测试工程师也提出了新的要求。那么采用静态代理的方式,测试工程师的实现类ITester也得创建一个对应的ITesterProxy类。(正是因这个缺点,才诞生了Java的动态代理实现方式)

public interface ITester {
    public void doTesting();
}

public class Tester implements ITester {
    private String name;
    public Tester(String name){
        this.name = name;
    }
    @Override
    public void doTesting() {
        System.out.println("Tester " + name + " is testing code");
    }
}

public class TesterProxy implements ITester{
    private ITester tester;
    public TesterProxy(ITester tester){
        this.tester = tester;
    }
    @Override
    public void doTesting() {
        System.out.println("Tester is preparing test documentation...");
        tester.doTesting();
    }
}

3.2 实现方式二:动态代理之InvocationHandler

步骤1: 通过InvocationHandler, 我可以用一个EngineerProxyDynamic代理类来同时代理Developer和Tester的行为

public class EngineerProxyDynamic implements InvocationHandler {
    Object obj;

    public Object bind(Object obj) {
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }

  //真实类的writeCode和doTesting方法在动态代理类里通过反射的方式进行执行。
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Engineer writes document...");
        Object res = method.invoke(obj, args);
        return res;
    }
}

步骤2:客户端调用

public class DeveloperTest {
    public static void main(String[] args) {
        IDeveloper andi = new Developer("Andi");
        Tester bob = new Tester("Bob");

        IDeveloper andiProxy = (IDeveloper) new EngineerProxyDynamic().bind(andi);
        ITester bobProxy = (ITester) new EngineerProxyDynamic().bind(bob);
        andiProxy.writeCode();
        bobProxy.doTesting();
    }
}

结果输出

Engineer writes document...
Developer Andi writes code
Engineer writes document...
Tester Bob is testing code
优点

每一个真实类不需要创建一个新的代理类

缺点
  1. 执行时有额外开销
  2. 无法代理没有实现任何接口的真实类
public class ProductOwner {
    private String name;
    public ProductOwner(String name){
        this.name = name;
    }
    public void defineBackLog(){
        System.out.println("PO: " + name + " defines Backlog.");
    }
}

我们仍然采取EngineerProxyDynamic代理类去代理它,编译时不会出错,运行时出错

Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy2 cannot be cast to com.beibei.design.structural.ProductOwner
    at com.beibei.design.structural.DeveloperTest.main(DeveloperTest.java:23)

3.3 实现方式三:动态代理之CGLIB

CGLIB是一个Java字节码生成库,提供了易用的API对Java字节码进行创建和修改。关于这个开源库的更多细节,请移步至CGLIB在github上的仓库:https://github.com/cglib/cglib

我们现在尝试用CGLIB来代理之前采用InvocationHandler没有成功代理的ProductOwner类(该类未实现任何接口)。

现在我改为使用CGLIB API来创建代理类:

public class EngineerCGLibProxy {
    Object obj;

    public Object bind(final Object target) {
        this.obj = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(obj.getClass());
        enhancer.setCallback(
                new MethodInterceptor() {
                    @Override
                    public Object intercept(Object obj, Method method, Object[] args,
                                            MethodProxy proxy) throws Throwable {
                        System.out.println("Engineer 2 writes document");
                        Object res = method.invoke(target, args);
                        return res;
                    }
                }
        );
        return enhancer.create();
    }
}

客户端调用

public class DeveloperTest {
    public static void main(String[] args) {
        ProductOwner ross = new ProductOwner("Ross");
        ProductOwner rossProxy = (ProductOwner) new EngineerCGLibProxy().bind(ross);
        rossProxy.defineBackLog();
    }
}

遇到问题

Exception in thread "main" java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given

解决,目标对象,定义一个无参数构造函数参考解决方法

public ProductOwner() {}

结果输出

Enginner 2 writes document
PO: Ross defines Backlog.
优点

可以代理没有实现任何接口的真实类

缺点

通过CGLIB成功创建的动态代理,实际是被代理类的一个子类。那么如果被代理类被标记成final,也就无法通过CGLIB去创建动态代理。

如果我们了解了CGLIB创建代理类的原理,那么其局限性也就一目了然。我们现在做个实验,将ProductOwner类加上final修饰符,使其不可被继承:

再次执行测试代码,这次就报错了: Cannot subclass final class XXXX。

3.4 实现方式四:动态代理之 通过编译期提供的API动态创建代理类

假设我们确实需要给一个既是final,又未实现任何接口的ProductOwner类创建动态代码。除了InvocationHandler和CGLIB外,我们还有最后一招:

我直接把一个代理类的源代码用字符串拼出来,然后基于这个字符串调用JDK的Compiler(编译期)API,动态的创建一个新的.java文件,然后动态编译这个.java文件,这样也能得到一个新的代理类。


public class ProductOwnerSourceCodeProxy {

    public static void main(String[] arg) throws Exception {
        Class<?> c = getProxyClass();
        Constructor<?>[] constructor = c.getConstructors();
        Object POProxy = constructor[0].newInstance("Ross");
        Method defineBackLog = c.getDeclaredMethod("defineBackLog");
        defineBackLog.invoke(POProxy);
    }

    private static String getSourceCode() {
        String src = "package com.beibei.design.structural.proxy;\n\n"
                + "public final class ProductOwnerSCProxy {\n"
                + "\tprivate String name;\n\n"
                + "\tpublic ProductOwnerSCProxy(String name){\n"
                + "\t\tthis.name = name;\n" + "\t}\n\n"
                + "\t\tpublic void defineBackLog(){\n"
                + "\t\tSystem.out.println(\"PO writes some document before defining BackLog\");"
                + "\t\tSystem.out.println(\"PO: \" + name + \" defines Backlog.\");}}\n";
        return src;
    }

    private static String createJavaFile(String sourceCode) {
        String fileName = "/Users/anbeibei/AndroidStudioProjects/2019/DesignPattern/src/com/beibei/design/structural/proxy/ProductOwnerSCProxy.java";
        File javaFile = new File(fileName);
        Writer writer;
        try {
            writer = new FileWriter(javaFile);
            writer.write(sourceCode);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return fileName;
    }

    private static void compile(String fileName) {
        try {
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
            Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
            JavaCompiler.CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
            ct.call();
            sjfm.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static Class<?> loadClass() {
        URL[] urls;
        String path = "file:///Users/anbeibei/AndroidStudioProjects/2019/DesignPattern/src/com/beibei/design/structural/";
        Class<?> c = null;
        try {
            urls = new URL[]{(new URL(path))};
            URLClassLoader ul = new URLClassLoader(urls);
            c = ul.loadClass("com.beibei.design.structural.proxy.ProductOwnerSCProxy");//需要指定包名
            ul.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return c;
    }

    private static Class<?> getProxyClass() {
        String sourceCode = getSourceCode();
        String javaFile = createJavaFile(sourceCode);
        compile(javaFile);
        return loadClass();
    }
}

测试结果

PO writes some document before defining BackLog
PO: Ross defines Backlog.

遇到问题
无法加载生成的.class文件,URLClassLoader加载需要指定包名

Java代理设计模式的四种具体实现:静态代理和动态代理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容