学习笔记-Java设计模式-结构型模式2

Java设计原则&&模式学习笔记

说明

近期扫地生决定整合一下年初学习的设计模式,一来用于复习巩固,二来也希望可以把自己的整合与有需要的同学共勉。

扫地生在学习的过程中主要参考的是公众号“一角钱技术”的相关推文,同时也参考了下列几篇文章。对这些作者扫地生表示衷心的感谢。

参考文章1-Java设计原则

参考文章2-Java设计模式总结

参考文章3-23种设计模式速记

4、结构型模式

4. 4 结构型模式4——代理模式(Proxy)

速记关键词:快捷方式

简介

定义:为对象提供一个代理以便控制这个对象的访问。

代理模式就是给一个对象提供一个代理,并由代理对象控制对原对象的引用。它使得客户不能直接与真正的目标对象通信。代理对象是目标对象的代表,其他需要与这个目标对象打交道的操作都是和这个代理对象在交涉。

代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的,同时也在一定程度上面减少了系统的耦合度。

img

在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。

根据代理的创建时期,代理模式分为静态代理和动态代理:

  • 静态代理:有程序员创建代理类或特定工具自动生成源代码在对其编译,在程序运行前代理类的.class文件就已经存在。
  • 动态代理:在程序运行时,运用反射机制动态创建而成。

解决的问题

在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建的开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以放在访问此对象时加上一个对此对象的访问层。

模式组成

组成(角色) 作用
抽象主题(Subject)类 通过接口或抽象类声明真实主题和代理对象的业务方法。
真实主题(Real Subject)类 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
代理(Proxy)类 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩散真实主题的功能。

静态代理

样例实现
package top.saodisheng.designpattern.proxy.staticproxy;

/**
 * description:
 * 静态代理
 *
 * @author 扫地生_saodisheng
 * @date 2021-02-05
 */
public class ProxyPattern {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        Proxy proxy = new Proxy();
        proxy.setRealSubject(realSubject);
        proxy.request();
    }
}

/**
 * 1. 定义抽象主题类
 */
interface Subject {
    void request();
}

/**
 * 2. 定义真正主题类
 */
class RealSubject implements Subject {

    @Override
    public void request() {
        System.out.println("访问真实主题方法。");
    }
}

/**
 * 3. 定义代理类
 */
class Proxy implements Subject {
    /** 组合真正的代理对象 **/
    private RealSubject realSubject;

    /**
     * 通过setter注入目标代理对象
     *
     * @param subject
     */
    public void setRealSubject(RealSubject subject) {
        this.realSubject = subject;
    }

    @Override
    public void request() {
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        preRequest();
        realSubject.request();
        postRequest();
    }

    public void preRequest() {
        System.out.println("访问真实主类前的预处理。");
    }
    public void postRequest() {
        System.out.println("访问真实主题之后的后续处理。");
    }
}

image-20211107171340890

可以看到,主题接口是Subject,真实主题是RealSubject 实现了Subject接口,代理类是Proxy,在代理类的方法里实现了Subject类,并且在代码里写死了代理前后的操作,这就是静态代理的简单实现,可以看到静态代理的实现优缺点十分明显。

静态代理优缺点

优点:使得真正主题处理的业务更加纯粹,不再去关注一些公共的事情,公共的业务由代理来完成,实现业务的分工,公共业务发生扩展时变得更加集中和方便。

缺点:这种实现方式很直观也很简单,但其缺点是代理类必须提前写好,如果主题接口发生了变化,代理类的代码也要随着变化,有着高昂的维护成本。

针对静态代理的缺点,是否有一种方式弥补?能够不需要为每一个接口写上一个代理方法,那就动态代理。

动态代理

动态代理,在java代码里动态代理类使用字节码动态生成加载技术,在运行时生成加载类。

生成动态代理类的方法很多,比如:JDK 自带的动态处理、CGLIB、Javassist、ASM 库。

  • JDK 的动态代理使用简单,它内置在 JDK 中,因此不需要引入第三方 Jar 包,但相对功能比较弱。
  • CGLIB 和 Javassist 都是高级的字节码生成库,总体性能比 JDK 自带的动态代理好,而且功能十分强大。
  • ASM 是低级的字节码生成工具,使用 ASM 已经近乎于在使用 Java bytecode 编程,对开发人员要求最高,当然,也是性能最好的一种动态代理生成工具。但 ASM 的使用很繁琐,而且性能也没有数量级的提升,与 CGLIB 等高级字节码生成工具相比,ASM 程序的维护性较差,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。

我们这里介绍两种非常常用的动态代理技术,面试时也会常常用到的技术:JDK 自带的动态处理CGLIB 两种。在这里的话如果有学习过Spring源码的同学,扫地生推荐可以结合Spring的AOP实现机制去深入了解这两种代理方式。

JDK动态代理

Java提供了一个Proxy类,使用Proxy类的newInstance方法可以生成某个对象的代理对象,该方法需要三个参数:

  1. 类装载器【一般我们使用的是被代理类的装载器】
  2. 指定接口【指定要被代理类的接口】
  3. 代理对象的方法里干什么事【实现handler接口】
package top.saodisheng.designpattern.proxy.jdkdynamic;

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

/**
 * description:
 * 使用JDK自带的动态处理
 *
 * 动态代理
 * 在Java代码里动态代理使用字节码动态生成加载技术,在运行时生成加载类。
 * 生成动态代理类的方法很多,比如:JDK自带的动态处理,DGLIB,Javassist,ASM库
 * JDK的动态代理使用简单,它内置在JDK中,因此不需要导入第三方jar包,但相对功能比较弱。
 * CGLIB和javassist都是高级的字节码生成库,总体性能比JDK好,而且功能十分强大。
 * ASM是第几的字节码生成工具,使用ASM已经近乎在使用Java bytecode编程,对于开发人员
 * 要求最高,当然,也是性能最好的一种动态代理生成工具。但ASM的使用很繁琐,而且性能也没有
 * 数量级的提升。与CGLIB等高级字节码生成工具相比,ASM编程的维护性较差。如果不是对性能有
 * 苛刻要求的场合,大多数使用CGLIB或者javassist
 *
 * @author 扫地生_saodisheng
 * @date 2021-02-05
 */
public class ProxyPattern {
    public static void main(String[] args) {
        Subject subject = ProxyHandler.createProxy();
        subject.request();
    }
}

/**
 * 1. 定义抽象主题类
 */
interface Subject {
    void request();
}

/**
 * 2. 定义真正主题类
 */
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("访问真实主题方法...");
    }
}

/**
 * 3. 使用Proxy.newProxyInstance生成代理对象
 */
class ProxyHandler implements InvocationHandler {
    /**
     * 定义主题接口
     */
    private Subject subject;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 如果是第一次调用,生成真实主题
        if (subject == null) {
            subject = (Subject) new RealSubject();
        }

        if ("request".equalsIgnoreCase(method.getName())) {
            System.out.println("访问真实主题之前的预处理。");
            Object result = method.invoke(subject, args);
            System.out.println("访问真实主题之后的后续处理。");
            return result;
        } else {
            // 如果不是调用request方法,返回真实主题完成实际操作
            return method.invoke(subject, args);
        }
    }

    /**
     * 使用Proxy.newProxyInstance生成代理对象
     * @return
     */
    static Subject createProxy() {
        Subject proxy = (Subject) Proxy.newProxyInstance(
                // 当前类的类加载器
                ClassLoader.getSystemClassLoader(),
                //被代理的主题接口
                new Class[]{Subject.class},
                // 代理对象,这里是当前对象
                new ProxyHandler()
        );
        return proxy;
    }
}

image-20211107172307238

用debug的方式启动,可以看到方法被代理到代理类中实现,在代理类中执行真实主题的方法前后可以进行很多操作。

虽然这种方法实现看起来很方便,但是细心的同学应该也已经观察到了,JDK动态代理技术的实现是必须要一个接口才行的,所以JDK动态代理的优缺点也非常明显

JDK动态代理优缺点

优点:

  • 不需要为真实主题写一个形式上完全一样的封装类,减少维护成本;
  • 可以在运行时制定代理类的执行逻辑,提升系统的灵活性;

缺点:

  • JDK动态代理,真实主题 必须实现的主题接口,如果真实主题 没有实现主图接口,或者没有主题接口,则不能生成代理对象

CGLIB动态代理、

使用 CGLIB 生成动态代理,首先需要生成 Enhancer 类实例,并指定用于处理代理业务的回调类。在 Enhancer.create() 方法中,会使用 DefaultGeneratorStrategy.Generate() 方法生成动态代理类的字节码,并保存在 byte 数组中。接着使用 ReflectUtils.defineClass() 方法,通过反射,调用 ClassLoader.defineClass() 方法,将字节码装载到 ClassLoader 中,完成类的加载。最后使用 ReflectUtils.newInstance() 方法,通过反射,生成动态类的实例,并返回该实例。基本流程是根据指定的回调类生成 Class 字节码—通过 defineClass() 将字节码定义为类—使用反射机制生成该类的实例。

package top.saodisheng.designpattern.proxy.cglibdynamic;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * description:
 * CGLIB动态代理
 *
 * @author 扫地生_saodisheng
 * @date 2021-02-05
 */
public class CglibProxy {
    public static void main(String[] args) {
        WorkImplProxyLib cglib = new WorkImplProxyLib();
        WorkImpl workCglib = (WorkImpl) cglib.getWorkProxyImplInstance();
        workCglib.addWorkExperience();
    }
}

/**
 * 1. 定义真实主题
 */
class WorkImpl {

    void addWorkExperience() {
        System.out.println("增加工作经验的普通方法...");
    }
}

/**
 * 2. 创建代理类
 */
class WorkImplProxyLib implements MethodInterceptor {

    /**
     * 创建代理对象
     * @return
     */
    Object getWorkProxyImplInstance() {
        Enhancer enhancer = new Enhancer();
        // 目标代理对象
        enhancer.setSuperclass(WorkImpl.class);
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }

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

image-20211107172821016

CGLIB优缺点

优点:CGLIB通过继承的方式进行代理、无论目标对象没有没实现接口都可以代理,弥补了JDK动态代理的缺陷。

缺点:

  • CGLib创建的动态代理对象性能比JDK创建的动态代理对象的性能高不少,但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
  • 由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。

应用场景

当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

代理模式有多种应用场合,如下所述:

  1. 远程代理,也就是为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。比如说 WebService,当我们在应用程序的项目中加入一个 Web 引用,引用一个 WebService,此时会在项目中声称一个 WebReference 的文件夹和一些文件,这个就是起代理作用的,这样可以让那个客户端程序调用代理解决远程访问的问题;
  2. 虚拟代理,是根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,比如打开一个网页,这个网页里面包含了大量的文字和图片,但我们可以很快看到文字,但是图片却是一张一张地下载后才能看到,那些未打开的图片框,就是通过虚拟代里来替换了真实的图片,此时代理存储了真实图片的路径和尺寸;
  3. 安全代理,用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候;
  4. 指针引用,是指当调用真实的对象时,代理处理另外一些事。比如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它,或当第一次引用一个持久对象时,将它装入内存,或是在访问一个实际对象前,检查是否已经释放它,以确保其他对象不能改变它。这些都是通过代理在访问一个对象时附加一些内务处理;
  5. 延迟加载,用代理模式实现延迟加载的一个经典应用就在 Hibernate 框架里面。当 Hibernate 加载实体 bean 时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采取延迟加载的机制,以提高系统的性能。Hibernate 中的延迟加载主要分为属性的延迟加载和关联表的延时加载两类。实现原理是使用代理拦截原有的 getter 方法,在真正使用对象数据时才去数据库或者其他第三方组件加载实际的数据,从而提升系统性能。

其他代理模式

  • 防火墙代理:内网通过代理穿透防火墙,实现对公网的访问;
  • 缓存代理:比如当我们请求图片文件资源时,先到缓存代理取,如果取不到资源再到公网或者数据库取,然后缓存;
  • 远程代理:远程对象的本地代表,通过它可以把远程对象来调用。远程代理通过网络和真正的远程对象沟通;
  • 同步代理:主要用在多线程编程中,完成多线程间同步工作。
posted @ 2021-11-07 20:29  技术扫地生—楼上老刘  阅读(33)  评论(0编辑  收藏  举报