您当前的位置: 首页 > 

庄小焱

暂无认证

  • 1浏览

    0关注

    805博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

软件设计模式——代理模式详解

庄小焱 发布时间:2020-11-11 15:56:37 ,浏览量:1

摘要

动态代理是Java语言中非常经典的一种设计模式,也是所有设计模式中最难理解的一种。那什么是代理设计模式?代理设计的基础概念就是通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。(也就是AOP微实现)。代理在原有代码乃至原业务流程都不修改的情况下,直接在业务流程中切入新代码,增加新功能。

一、代理模式的示例

public interface Flyable {
    void fly();
}

public class Bird implements Flyable {

    @Override
    public void fly() {
        System.out.println("Bird is flying...");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

很简单的一个例子,用一个随机睡眠时间模拟小鸟在空中的飞行时间。接下来问题来了,如果我要知道小鸟在天空中飞行了多久,怎么办?有人说,很简单,在Bird->fly()方法的开头记录起始时间,在方法结束记录完成时间,两个时间相减就得到了飞行时间。

   @Override
    public void fly() {
        long start = System.currentTimeMillis();
        System.out.println("Bird is flying...");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }

的确,这个方法没有任何问题,接下来加大问题的难度。如果Bird这个类来自于某个SDK(或者说Jar包)提供,你无法改动源码,怎么办?一定会有人说,我可以在调用的地方这样写:

public static void main(String[] args) {
        Bird bird = new Bird();
        long start = System.currentTimeMillis();
        bird.fly();
        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
}

这个方案看起来似乎没有问题,但其实你忽略了准备这些方法所需要的时间,执行一个方法,需要开辟栈内存、压栈、出栈等操作,这部分时间也是不可以忽略的。因此,这个解决方案不可行。那么,还有什么方法可以做到呢?使用继承

继承是最直观的解决方案,相信你已经想到了,至少我最开始想到的解决方案就是继承。 为此,我们重新创建一个类Bird2,在Bird2中我们只做一件事情,就是调用父类的fly方法,在前后记录时间,并打印时间差:

public class Bird2 extends Bird {

    @Override
    public void fly() {
        long start = System.currentTimeMillis();
        
        super.fly();
        
        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}

这是一种解决方案,还有一种解决方案叫做:聚合,其实也是比较容易想到的。 我们再次创建新类Bird3,在Bird3的构造方法中传入Bird实例。同时,让Bird3也实现Flyable接口,并在fly方法中调用传入的Bird实例的fly方法:

public class Bird3 implements Flyable {
    private Bird bird;

    public Bird3(Bird bird) {
        this.bird = bird;
    }

    @Override
    public void fly() {
        long start = System.currentTimeMillis();

        bird.fly();

        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}

为了记录Bird->fly()方法的执行时间,我们在前后添加了记录时间的代码。同样地,通过这种方法我们也可以获得小鸟的飞行时间。那么,这两种方法孰优孰劣呢?咋一看,不好评判!

继续深入思考,用问题推导来解答这个问题:

问题一:如果我还需要在fly方法前后打印日志,记录飞行开始和飞行结束,怎么办? 有人说,很简单!继承Bird2并在在前后添加打印语句即可。那么,问题来了,请看问题二。

问题二:如果我需要调换执行顺序,先打印日志,再获取飞行时间,怎么办? 有人说,再新建一个类Bird4继承Bird,打印日志。再新建一个类Bird5继承Bird4,获取方法执行时间。

问题显而易见:使用继承将导致类无限制扩展,同时灵活性也无法获得保障。那么,使用聚合是否可以避免这个问题呢? 答案是:可以!但我们的类需要稍微改造一下。修改Bird3类,将聚合对象Bird类型修改为Flyable

public class BirdTimeProxy implements Flyable {
    private Flyable flyable;

    public BirdTimeProxy(Flyable flyable) {
        this.flyable = flyable;
    }

    @Override
    public void fly() {
        long start = System.currentTimeMillis();

        flyable.fly();

        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}

为了让你看的更清楚,我将Bird3更名为BirdTimeProxy,即用于获取方法执行时间的代理的意思。同时我们新建BirdLogProxy代理类用于打印日志:

public class BirdLogProxy implements Flyable {
    private Flyable flyable;

    public BirdLogProxy(Flyable flyable) {
        this.flyable = flyable;
    }

    @Override
    public void fly() {
        System.out.println("Bird fly start...");

        flyable.fly();

        System.out.println("Bird fly end...");
    }
}

接下来神奇的事情发生了,如果我们需要先记录日志,再获取飞行时间,可以在调用的地方这么做:

public static void main(String[] args) {
   Bird bird = new Bird();
   BirdLogProxy p1 = new BirdLogProxy(bird);
   BirdTimeProxy p2 = new BirdTimeProxy(p1);

   p2.fly();
}

 public static void main(String[] args) {
        Bird bird = new Bird();
        BirdTimeProxy p2 = new BirdTimeProxy(bird);
        BirdLogProxy p1 = new BirdLogProxy(p2);

        p1.fly();
}

看到这里,有同学可能会有疑问了。虽然现象看起来,聚合可以灵活调换执行顺序。可是为什么聚合可以做到,而继承不行呢。我们用一张图来解释一下:

二、静态代理
public class BirdTimeProxy implements Flyable {
    private Flyable flyable;

    public BirdTimeProxy(Flyable flyable) {
        this.flyable = flyable;
    }

    @Override
    public void fly() {
        long start = System.currentTimeMillis();

        flyable.fly();

        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}
public class BirdLogProxy implements Flyable {
    private Flyable flyable;

    public BirdLogProxy(Flyable flyable) {
        this.flyable = flyable;
    }

    @Override
    public void fly() {
        System.out.println("Bird fly start...");

        flyable.fly();

        System.out.println("Bird fly end...");
    }
}

上面实现的就是静态代理。在它的fly方法中我们直接调用了flyable->fly()方法。换而言之,BirdTimeProxy其实代理了传入的Flyable对象,这就是典型的静态代理实现。

从表面上看,静态代理已经完美解决了我们的问题。可是,试想一下,如果我们需要计算SDK中100个方法的运行时间,同样的代码至少需要重复100次,并且创建至少100个代理类。往小了说,如果Bird类有多个方法,我们需要知道其他方法的运行时间,同样的代码也至少需要重复多次。因此,静态代理至少有以下两个局限性问题:

  • 如果同时代理多个类,依然会导致类无限制扩展
  • 如果类中有多个方法,同样的逻辑需要反复实现
三、JDK动态代理

那么,我们是否可以使用同一个代理类来代理任意对象呢?我们以获取方法运行时间为例,是否可以使用同一个类(例如:TimeProxy)来计算任意对象的任一方法的执行时间呢?甚至再大胆一点,代理的逻辑也可以自己指定。比如,获取方法的执行时间,打印日志,这类逻辑都可以自己指定。

你脑海中的第一个解决方案应该是使用反射。反射是用于获取已创建实例的方法或者属性,并对其进行调用或者赋值。很明显,在这里,反射解决不了问题。但是,再大胆一点,如果我们可以动态生成TimeProxy这个类,并且动态编译。然后,再通过反射创建对象并加载到内存中,不就实现了对任意对象进行代理了吗?为了防止你依然一头雾水,我们用一张图来描述接下来要做什么:

动态生成Java源文件并且排版是一个非常繁琐的工作,为了简化操作,我们使用JavaPoet这个第三方库帮我们生成TimeProxy的源码。(GitHub - square/javapoet: A Java API for generating .java source files.)  ,如果你的字节码的解析感兴趣,可以自行查阅相关内容。

3.1 生成TimeProxy源码
public class Proxy {

    public static Object newProxyInstance() throws IOException {
        TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy")
                .addSuperinterface(Flyable.class);

        FieldSpec fieldSpec = FieldSpec.builder(Flyable.class, "flyable", Modifier.PRIVATE).build();
        typeSpecBuilder.addField(fieldSpec);

        MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(Flyable.class, "flyable")
                .addStatement("this.flyable = flyable")
                .build();
        typeSpecBuilder.addMethod(constructorMethodSpec);

        Method[] methods = Flyable.class.getDeclaredMethods();
        for (Method method : methods) {
            MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName())
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(method.getReturnType())
                    .addStatement("long start = $T.currentTimeMillis()", System.class)
                    .addCode("\n")
                    .addStatement("this.flyable." + method.getName() + "()")
                    .addCode("\n")
                    .addStatement("long end = $T.currentTimeMillis()", System.class)
                    .addStatement("$T.out.println(\"Fly Time =\" + (end - start))", System.class)
                    .build();
            typeSpecBuilder.addMethod(methodSpec);
        }

        JavaFile javaFile = JavaFile.builder("com.youngfeng.proxy", typeSpecBuilder.build()).build();
        // 为了看的更清楚,我将源码文件生成到桌面
        javaFile.writeTo(new File("/Users/ouyangfeng/Desktop/"));

        return null;
    }

}

在main方法中调用Proxy.newProxyInstance(),你将看到桌面已经生成了TimeProxy.java文件,生成的内容如下:

package com.zhaungxiaoyan.proxy;

import java.lang.Override;
import java.lang.System;

class TimeProxy implements Flyable {
  private Flyable flyable;

  public TimeProxy(Flyable flyable) {
    this.flyable = flyable;
  }

  @Override
  public void fly() {
    long start = System.currentTimeMillis();

    this.flyable.fly();

    long end = System.currentTimeMillis();
    System.out.println("Fly Time =" + (end - start));
  }
}
3.2 编译TimeProxy源码

编译TimeProxy源码我们直接使用JDK提供的编译工具即可,为了使你看起来更清晰,我使用一个新的辅助类来完成编译操作:

public class JavaCompiler {

    public static void compile(File javaFile) throws IOException {
        javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);
        Iterable iterable = fileManager.getJavaFileObjects(javaFile);
        javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, iterable);
        task.call();
        fileManager.close();
    }
}

在Proxy->newProxyInstance()方法中调用该方法,编译顺利完成:

// 为了看的更清楚,我将源码文件生成到桌面
String sourcePath = "/Users/ouyangfeng/Desktop/";
javaFile.writeTo(new File(sourcePath));

// 编译
JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java"));
3.3 加载到内存中并创建对象
  URL[] urls = new URL[] {new URL("file:/" + sourcePath)};
  URLClassLoader classLoader = new URLClassLoader(urls);
  Class clazz = classLoader.loadClass("com.youngfeng.proxy.TimeProxy");
  Constructor constructor = clazz.getConstructor(Flyable.class);
  Flyable flyable = (Flyable) constructor.newInstance(new Bird());
  flyable.fly();

通过以上三个步骤,我们至少解决了下面两个问题:

  • 不再需要手动创建TimeProxy
  • 可以代理任意实现了Flyable接口的类对象,并获取接口方法的执行时间
3.4 增加InvocationHandler接口

查看Proxy->newProxyInstance()的源码,代理类继承的接口我们是写死的,为了增加灵活性,我们将接口类型作为参数传入:

接口的灵活性问题解决了,TimeProxy的局限性依然存在,它只能用于获取方法的执行时间,而如果要在方法执行前后打印日志则需要重新创建一个代理类,显然这是不妥的!

为了增加控制的灵活性,我们考虑针将代理的处理逻辑也抽离出来(这里的处理就是打印方法的执行时间)。新增InvocationHandler接口,用于处理自定义逻辑:

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

想象一下,如果客户程序员需要对代理类进行自定义的处理,只要实现该接口,并在invoke方法中进行相应的处理即可。这里我们在接口中设置了三个参数(其实也是为了和JDK源码保持一致):

  • proxy => 这个参数指定动态生成的代理类,这里是TimeProxy
  • method => 这个参数表示传入接口中的所有Method对象
  • args => 这个参数对应当前method方法中的参数

引入了InvocationHandler接口之后,我们的调用顺序应该变成了这样:

MyInvocationHandler handler = new MyInvocationHandler();
Flyable proxy = Proxy.newProxyInstance(Flyable.class, handler);
proxy.fly();

方法执行流:proxy.fly() => handler.invoke()

为此,我们需要在Proxy.newProxyInstance()方法中做如下改动:

  • 在newProxyInstance方法中传入InvocationHandler
  • 在生成的代理类中增加成员变量handler
  • 在生成的代理类方法中,调用invoke方法
  public static Object newProxyInstance(Class inf, InvocationHandler handler) throws Exception {
        TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(inf);

        FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class, "handler", Modifier.PRIVATE).build();
        typeSpecBuilder.addField(fieldSpec);

        MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(InvocationHandler.class, "handler")
                .addStatement("this.handler = handler")
                .build();

        typeSpecBuilder.addMethod(constructorMethodSpec);

        Method[] methods = inf.getDeclaredMethods();
        for (Method method : methods) {
            MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName())
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(method.getReturnType())
                    .addCode("try {\n")
                    .addStatement("\t$T method = " + inf.getName() + ".class.getMethod(\"" + method.getName() + "\")", Method.class)
                    // 为了简单起见,这里参数直接写死为空
                    .addStatement("\tthis.handler.invoke(this, method, null)")
                    .addCode("} catch(Exception e) {\n")
                    .addCode("\te.printStackTrace();\n")
                    .addCode("}\n")
                    .build();
            typeSpecBuilder.addMethod(methodSpec);
        }

        JavaFile javaFile = JavaFile.builder("com.youngfeng.proxy", typeSpecBuilder.build()).build();
        // 为了看的更清楚,我将源码文件生成到桌面
        String sourcePath = "/Users/ouyangfeng/Desktop/";
        javaFile.writeTo(new File(sourcePath));

        // 编译
        JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java"));

        // 使用反射load到内存
        URL[] urls = new URL[] {new URL("file:" + sourcePath)};
        URLClassLoader classLoader = new URLClassLoader(urls);
        Class clazz = classLoader.loadClass("com.youngfeng.proxy.TimeProxy");
        Constructor constructor = clazz.getConstructor(InvocationHandler.class);
        Object obj = constructor.newInstance(handler);

        return obj;
 }

上面的代码你可能看起来比较吃力,我们直接调用该方法,查看最后生成的源码。在main方法中测试newProxyInstance查看生成的TimeProxy源码:

Proxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird()));

生成的TimeProxy.java源码

package com.youngfeng.proxy;

import java.lang.Override;
import java.lang.reflect.Method;

public class TimeProxy implements Flyable {
  private InvocationHandler handler;

  public TimeProxy(InvocationHandler handler) {
    this.handler = handler;
  }

  @Override
  public void fly() {
    try {
    	Method method = com.youngfeng.proxy.Flyable.class.getMethod("fly");
    	this.handler.invoke(this, method, null);
    } catch(Exception e) {
    	e.printStackTrace();
    }
  }
}

MyInvocationHandler.java

public class MyInvocationHandler implements InvocationHandler {
    private Bird bird;

    public MyInvocationHandler(Bird bird) {
        this.bird = bird;
    }

    @Override
    public void invoke(Object proxy, Method method, Object[] args) {
        long start = System.currentTimeMillis();

        try {
            method.invoke(bird, new Object[] {});
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("Fly time = " + (end - start));
    }
}

至此,整个方法栈的调用栈变成了这样:

看到这里,估计很多同学已经晕了,在静态代理部分,我们在代理类中传入了被代理对象。可是,使用newProxyInstance生成动态代理对象的时候,我们居然不再需要传入被代理对象了。我们传入了的实际对象是InvocationHandler实现类的实例,这看起来有点像生成了InvocationHandler的代理对象,在动态生成的代理类的任意方法中都会间接调用InvocationHandler->invoke(proxy, method, args)方法。其实的确是这样。TimeProxy真正代理的对象就是InvocationHandler,不过这里设计的巧妙之处在于,InvocationHandler是一个接口,真正的实现由用户指定。另外,在每一个方法执行的时候,invoke方法都会被调用 ,这个时候如果你需要对某个方法进行自定义逻辑处理,可以根据method的特征信息进行判断分别处理。

如何使用:上面这段解释是告诉你在执行Proxy->newProxyInstance方法的时候真正发生的事情,而在实际使用过程中你完全可以忘掉上面的解释。按照设计者的初衷,我们做如下简单归纳:

  • Proxy->newProxyInstance(infs, handler) 用于生成代理对象
  • InvocationHandler:这个接口主要用于自定义代理逻辑处理
  • 为了完成对被代理对象的方法拦截,我们需要在InvocationHandler对象中传入被代理对象实例。

查看上面的代码,你可以看到我将Bird实例已经传入到了MyInvocationHandler中,原因就是第三点。如果我们还需要对其它任意对象进行代理,不需要改动newProxyInstance方法的源码,只要你在newProxyInstance方法中指定代理需要实现的接口,指定用于自定义处理的InvocationHandler对象,整个代理的逻辑处理都在你自定义的InvocationHandler实现类中进行处理。至此,而我们终于可以从不断地写代理类用于实现自定义逻辑的重复工作中解放出来了,从此需要做什么,交给InvocationHandler。

四、CGLIB动态代理

CGLIB动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。CGLIB动态代理和jdk代理一样,使用反射完成代理,不同的是他可以直接代理类(jdk动态代理不行,他必须目标业务类必须实现接口),CGLIB动态代理底层使用字节码技术,CGLIB动态代理不能对 final类进行继承。(CGLIB动态代理需要导入jar包)

目标对象

package com.zhuangxiaoyan.designpattern.proxymodel.cglibproxy;

/**
 * @Classname TecherDaoimple
 * @Description TODO
 * @Date 2022/5/1 10:21
 * @Created by xjl
 */
public class TecherDaoimple{

    public void tech() {
        System.out.println("老师正在授课中………… Cglib代理 不需要实现接口");
    }
}

代理类

package com.zhuangxiaoyan.designpattern.proxymodel.cglibproxy;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @Classname ProxyFactory
 * @Description TODO
 * @Date 2022/5/1 10:36
 * @Created by xjl
 */
public class ProxyFactory implements MethodInterceptor {

    // 维护一个目标对象
    private Object target;

    // 构造器 传入一个被代理的对象
    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 返回一个代理对象 是target 对象的代理对象
    public Object getProxyInstance(){
        // 创一个工具类
        Enhancer enhancer=new Enhancer();
        // 设置父类
        enhancer.setSuperclass(target.getClass());
        // 设置回调函数
        enhancer.setCallback(this);
        // 创建子类,即代理对象
        return enhancer.create();
    }
    // 重写intercept方法 会调用目标对象的方法
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLIB代理开始…………");
        Object invokevale = method.invoke(target, args);
        System.out.println("CGLIB代理结束…………");
        return invokevale;
    }
}

测试

package com.zhuangxiaoyan.designpattern.proxymodel.cglibproxy;



/**
 * @Classname ProxyTest
 * @Description TODO
 * @Date 2022/5/1 10:26
 * @Created by xjl
 */
public class CGLIBProxyTest {

    public static void main(String[] args) {
        // 创建目标对象
        TecherDaoimple target=new TecherDaoimple();
        // 获取代理对象,并转为TecherDaoimple类
        ProxyFactory proxyFactory = new ProxyFactory(target);
        TecherDaoimple techerDaoimple = (TecherDaoimple)proxyFactory.getProxyInstance();
        techerDaoimple.tech();
    }
}

JDK和Cglib的区别:

  • jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
  • cglib动态代理是利用ASM开源包,对被代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
五、动态代理的作用

基于这样一种动态特性,我们可以用它做很多事情,例如:

  • 远程代理:就是将工作委托给远程对象(不同的服务器,或者不同的进程)来完成。常见的是用在web Service中。还有就是我们的RPC调用也可以理解为一种远程代理。
  • 保护代理:该模式主要进行安全/权限检查。(接触很少)
  • 缓存代理:这个很好理解,就是通过存储来加速调用,比如Sping中的@Cacheable方法,缓存特定的参数获取到的结果,当下次相同参数调用该方法,直接从缓存中返回数据。
  • 虚拟代理:这种代理主要是为方法增加功能,比如记录一些性能指标等,或进行延迟初始化
博文参考

10分钟看懂动态代理设计模式 - 掘金

关注
打赏
1657692713
查看更多评论
立即登录/注册

微信扫码登录

0.0446s