设计模式(15)结构型模式 - 代理模式:静态、动态代理、Cglib代理

前言

设计模式的结构型模式到了最后一个、也是最难的一个(我在学习过程中感觉的):代理模式
学习代理模式的前置知识:Java反射

先复习一下前面的几个结构型模式:

  1. 适配器模式:创建适配器类,将客户无法使用的类转换成客户需要的类(充电器:220V - > 5V)
  2. 桥接模式:通过聚合的方式构建一个“桥梁”,将一种事物的多个维度的变化简单的组合(画画需要多个颜色和画笔的大小型号)
  3. 装饰模式:“套娃模式”,往修饰者里传入被修饰者,动态的给被修饰者添加功能(往咖啡里加糖、牛奶)
  4. 组合模式:递归的思想,将客户需求的树型结构多个层次继承同一个类,一致的对待容器对象(上层节点)和叶子对象(叶子节点),容器对象中维护一个容器存储下层节点,递归的表现树形结构(Linux目录的多层次的描述)
  5. 外观模式:设置一个外观角色将子系统的功能集中,使客户类可以简单的调用外观角色方法实现复杂的子系统功能(家庭影院一个遥控器可以控制多个仪器)
  6. 享元模式:享元工厂维护一个享元池,保证具体共享角色的复用(String与常量池)

这篇文章将会:代理模式概念 -》静态代理 - 》动态代理 -》Cglib代理 - 》总结

现实中的问题

一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用

代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务

代理模式很常用,例如:

出行时坐火车,购买火车票不一定要去火车站买
在这里插入图片描述

可以通过 12306 网站或者去火车票代售点买

这个就是代理模式:客户不想直接去火车站(使用某个对象),而是在12306买票(通代理角色调用原对象的方法)

代理模式

代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate

代理模式结构:
在这里插入图片描述

代理模式的角色:

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

有点类似外观模式,与外观模式的区别:

外观模式是将复杂的子系统功能集中到外观角色的方法中,客户类只需要使用外观角色的方法,就可以完成复杂的功能(例如家庭影院遥控器按开启:灯会关闭、屏幕开启、DVD开启、投影仪开启),注重的是子系统功能集中与客户端简化

而代理模式是对于客户无法、不想使用的对象,通过代理角色控制原对象的引用,注重的是通过第三方对原对象的调用,降低耦合

静态代理

静态代理比较简单,就单纯的实现一下购票案例

package com.company.Structural.Proxy;
//抽象类:定义火车站功能
interface Station {
    public void buyTicket();
}
//火车站售票处
class TrainStation implements Station{

    @Override
    public void buyTicket() {
        System.out.println(" 购买火车票。。。");
    }
}
//代购火车票:12306
class WebSite12306 implements Station{
    //内置一个火车站对象
    private Station station = new TrainStation();

    @Override
    public void buyTicket() {
        System.out.println(" 打开12306。。。");
        station.buyTicket();
        System.out.println(" 关闭12306。。。");
    }
}
class User{
    public static void main(String[] args) {
        WebSite12306 webSite12306 = new WebSite12306();
        webSite12306.buyTicket();
    }
}

在这里插入图片描述

12306内部存在购买火车票的方法(内置火车站对象),客户通过12306购买火车票本质上还是在火车站买的票,只是通过第三者-代理角色购票

静态代理到动态代理

静态代理的缺陷

很显然,静态代理通过组合一个原对象,然后调用原对象的方法实现自己的方法,这样的缺陷是一个原对象就需要一个代理对象,扩展需要修改源代码,不符合“开闭原则”
而且对象必须是已知的,这样才能组合该对象

例如加一个飞机票售票处,那代理对象又要内置一个飞机票售票对象,且只能通过修改源代码的方式添加,或者新建一个代理对象

解决这个问题的方法就是动态代理

动态代理

动态代理也称为JDK代理、接口代理
它有以下几个特点:

  1. 代理角色不需要实现接口,但真实主题角色要实现接口(否则不能用动态代理)
  2. 代理对象的生成是通JDK中反射实现,动态的在内存中构建代理角色
  3. 代理类所在的包是java.lang.reflect,实现Java反射中的Proxy.newProxyInstance方法

动态代理实现

用动态代理模拟在12306购票过程

package com.company.Structural.Proxy;

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

//抽象类:定义火车站功能
interface Station {
    public void buyTicket();
}
//火车站售票处
class TrainStation implements Station{

    @Override
    public void buyTicket() {
        System.out.println(" 购买火车票。。。");
    }
}
//代购火车票:12306
class WebSite12306{
    //目标对象
    private Object target;

    public WebSite12306(Object target) {
        this.target = target;
    }

    //生成代理对象
    public Object getProxyInstance() {
        //事件处理,传入目标对象的方法,可以自定义额外方法
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("---------------打开12306---------------");
                Object result = method.invoke(target, args);
                System.out.println("---------------关闭12306---------------");
                return result;
            }
        };
        //通过反射机制
        /*
        * public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
          1. ClassLoader是目标对象的类加载器
          2. interfaces是目标对象实现的接口,用泛型方式确认类型
          3. InvocationHandler是事件处理(自己编写),用于触发事件处理器方法,当前执行的目标对象的方法会作为参数传入
        * */
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                handler);

    }
}
class User{
    public static void main(String[] args) {
        //目标对象
        Station trainStation = new TrainStation();
        //创建代理工厂
        WebSite12306 webSite12306 = new WebSite12306(trainStation);
        //得到代理角色
        Station proxyInstance = (Station) webSite12306.getProxyInstance();
        //执行方法
        proxyInstance.buyTicket();
    }
}

在这里插入图片描述

可以看出,动态代理通过反射得到目标对象的方法,这个时候传入什么类型的目标对象都可以实现方法(target是Object型对象)

这里面涉及到反射的知识,稍微展开学习一下

动态代理相关

我们是通过Proxy获得目标对象的类加载对象、实现接口,再自己编写事件处理对象完成动态代理,其中有这些要注意:

Proxy.newProxyInstance

Proxy是代理类,动态创建一个代理对象的类,它提供了一些方法
在这里插入图片描述

其中使用最多的是newProxyInstance方法,它会返回一个动态的代理对象

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)

在这里插入图片描述
它传递了3个参数:ClassLoader 、Class<?>[] interfaces、InvocationHandler

ClassLoader都知道,是类加载器对象,决定了用哪个加载器去处理对象
我们传入的就是target.getClass().getClassLoader(),目标对象的类加载器

interfaces是目标对象实现的接口的集合,用泛型方式确认类型Class<?>[]
这个参数是将要代理的目标对象一组接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了,我们通过target.getClass().getInterfaces()获得了目标对象实现的接口

InvocationHandler 是事件处理接口,是我们自己编写的事件处理对象(目标对象的方法就包含在这)

在这里插入图片描述
这个接口定义了一个invoke方法规范

InvocationHandler

InvocationHandler 接口中定义的invoke方法传递了3个参数,且返回值是Object

 public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

当有多个代理类的时候,每一个代理类都会关联一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用,即执行代理对象的方法都会被替换成执行invoke方法

这就可以很好的扩展代理模式的系统(不需要关心到底传入了什么方法)

我们来看看这3个参数:Object proxy, Method method, Object[] args

Object proxy:从名字都看得出,这就是我们需要代理的目标对象

Method method:要调用目标对象的某个方法的Method对象,在上面的案例中调用了proxyInstance.buyTicket();的buyTicket方法就是这个Method对象

可以通过Debug查看
在这里插入图片描述

里面会包含Method的各种属性

Object[] args:就是上面方法中传递进入的参数,也是调用目标对象需要的参数,上面的案例体现不出,但是可以修改一下Debug试试

在buyTicket方法中需要加入name
在这里插入图片描述

执行方法:proxyInstance.buyTicket("zhangsan");
就可以看到args数组中加入了“zhangsan”字符串
在这里插入图片描述

Proxy0

我们可以输出一下getProxyInstance()得到的类到底是什么
在这里插入图片描述
加一句getClass方法

在这里插入图片描述

执行后可以看到Class名是:com.company.Structural.Proxy.$Proxy0
为什么会返回的是Proxy.$Proxy0类呢?

Proxy.newProxyInstance 创建的代理对象是在jvm运行时动态生成的一个对象,命名方式都是以$开头,proxy为中,最后一个数字表示对象的标号$Proxy0

至于前面的:com.company.Structural.是我的包名
Proxy是因为我们使用的是Proxy类的方法

在看看我们的Method的名字是Proxy.Station.buyTicket()

在这里插入图片描述
这是因为我们方法是代理类的getProxyInstance方法到newProxyInstance方法里的handler对象的invoke执行的
而handler又接受了目标对象,表示执行的是这个对象,就会调用 handler 中的 invoke 方法去执行目标对象的相应的方法

所以Method的对象名是:Proxy.Station.buyTicket()
Proxy表示调用的Proxy类的newProxyInstance方法,Station是我们编写的接口,是我们代理对象强转的类型,buyTicket是具体使用的方法

Station类型强转

Station proxyInstance = (Station) webSite12306.getProxyInstance();

我们在得到代理对象时使用类型强转为Station,为什么可以这样强转?

Proxy.newProxyInstance传递的参数:Class<?>[] interfaces表示目标对象实现的接口,通过这个参数,我们这个代理对象也会实现这个接口

这也是前面为什么说:代理角色不需要实现接口,但真实主题角色要实现接口

Cglib代理

为什么要使用Cglib代理

注意:前面的动态代理,目标对象要实现接口,代理对象通过Proxy.newProxyInstance的interfaces参数,内部实现了接口

但很多时候,目标对象并没有实现接口,那么动态代理就无法使用。。。

这时可以通过目标对象的子类来实现代理,这就是Cglib代理

Cglib代理也称子类代理,它是在内存中构建一个子类对象从而实现对目标对象的功能扩展,Cglib代理也属于动态代理

Cglib是一个强大的高性能的代码生成包,它能在运行期间扩展Java类和实现Java接口,被众多AOP框架使用,如Spring AOP就是使用的Cglib的封装,实现方法拦截
Cglib包底层是通过使用字节码处理框架ASM来转换字节码并生成新的类

Cglib代理实现

首先要引入4个jar包
在这里插入图片描述

简单实现上面的购票案例

package com.company.Structural.Proxy.Cglib;

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

import java.lang.reflect.Method;

class TrainStation{
    public void buyTicket(String name) {
        System.out.println(name+" 购买火车票。。。");
    }
}

class WebSite12306 implements MethodInterceptor {
    //维护一个目标对象
    private Object target;
    //构造器
    public WebSite12306(Object target) {
        this.target = target;
    }
    //返回代理对象
    public Object getProxyInstance(){
        //创建工具类
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(target.getClass());
        //设置回调函数,自己回调
        enhancer.setCallback(this);
        //创建代理对象
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("-----------Cglib:12306开启-----------");
        Object result = method.invoke(target, args);
        System.out.println("-----------Cglib:12306关闭-----------");
        return result;
    }
}
class Client{
    public static void main(String[] args) {
        //创建目标对象
        TrainStation trainStation = new TrainStation();
        //获得代理对象
        TrainStation proxyInstance = (TrainStation)new WebSite12306(trainStation).getProxyInstance();
        //执行代理对象方法
        proxyInstance.buyTicket(" zhangsan ");
    }
}

在这里插入图片描述

依然是可以完成的,在通过Debug查看运行顺序

getProxyInstance返回代理对象:
在这里插入图片描述
执行代理方法:

在这里插入图片描述
运行重写的执行方法:
在这里插入图片描述

并执行目标对象传入的方法

Cglib代理与动态代理类似,在动态代理的基础上进行了改进

选择

在AOP编程中选择哪种代理:

  1. 目标对象需要实现接口,使用JDK代理
  2. 目标对象不需要实现接口,使用Cglib代理

代理模式变种

前面说的静态代理、动态代理、Cglib代理是最常用的代理模式

针对不同的应用环境还会有一些代理模式的变种(但本质上还是静态、动态代理)

  1. 防火墙代理
    内网通过代理穿透防火墙,实现对公网的访问。
  2. 缓存代理 比如:当请求图片文件等资源时,先到缓存代理取,如果取到资源则ok,如果取不到资源,再到公网或者数据库取,然后缓存。
  3. 远程代理
    远程对象的本地代表,通过它可以把远程对象当本地对象来调用。远程代理通过网络和真正的远程对象沟通信息。
  4. 同步代理:主要使用在多线程编程中,完成多线程间同步工作
  5. 虚拟代理:当一个对象的加载十分耗费资源的时候,虚拟代理的优势就非常明显地体现出来了。虚拟代理模式是一种内存节省技术,那些占用大量内存或处理复杂的对象将推迟到使用它的时候才创建。
    在应用程序启动的时候,可以用代理对象代替真实对象初始化,节省了内存的占用,并大大加速了系统的启动时间

这些变种是在动态代理的基础上在不同环境下变种延伸

总结

  • 代理模式是创建一个代理角色,把客户不想或者不能直接使用的目标对象,客户通过代理对象控制对目标对象的引用
  • 代理模式分为静态代理模式和动态代理模式,动态代理模式又可分了JDK代理和Cglib代理
  • 静态代理是代理对象内部组合目标对象并内部调用目标对象方法,客户通过代理对象控制对目标对象的引用
  • 静态代理的缺陷是需要明确目标对象,且目标对象需要组合到代理对象中,无法修改或扩展(需要修改代理对象源代码),而动态代理解决了这个问题
  • 动态代理中JDK代理是使用了Java反射和代理,Proxy的newProxyInstance方法可以获得目标对象的类加载器和实现的接口,并自己编写事件处理,将客户需要的目标对象的方法封装在invoke方法中,实现代理对象
  • JDK代理需要目标对象实现接口(代理对象内部方法实现了目标对象的接口),但很多类是不需要实现接口的,对于这些类,JDK代理无法处理,通过Cglib代理完成
  • Cglib代理是使用字节码处理框架ASM处理字节码形成一个新的类,在内存中构建一个目标对象的子类对象,完成目标对象的功能扩展
  • 大部分AOP框架例如Spring AOP就是通过Cglib代理完成AOP功能
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值