设计模式之代理模式

一、代理模式介绍

1、代理模式定义

在软件开发中,由于一些原因,客户端不想或不能直接访问一个对象,此时可以通过一个

称为"代理"的第三者来实现间接访问。该方案对应的设计模式被称为代理模式。

代理模式(Proxy Design Pattern ) 原始定义是:让你能够提供对象的替代品或其占位符。

代理控制着对于原对象的访问,并允许将请求提交给对象前后进行一些处理。

代理模式是一种"结构型模式"。

代理模式中引入了一个新的代理对象,代理对象在客户端对象和目标对象之间起到了中介

的作用,它去掉客户不能看到的内容和服务或者增加客户需要的额外的新服务。

二、代理模式原理

代理(Proxy)模式分为三种角色,即:

1)抽象主题(Subject)类: 声明了真实主题和代理主题的共同接口,这样就可以

保证任何使用真实主题的地方都可以使用代理主题,客户端一般针对抽象主题类

进行编程。

2)代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引

用,它可以在任何时候访问、控制或扩展真实主题的功能。

3)真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代

表的真实对象,是最终要引用的对象。

代理模式类图如下:

三、代理模式的实现

代理模式的实现分为三种,分别是:静态代理、jdk动态代理和CGLIB动态代理,下边

分别看下这3种代理的实现

1、静态代理的实现

这种代理的实现需要 "目标对象" 和 "代理对象" 实现同一个接口。

优点:

可以在不修改目标对象的前提下,扩展目标对象的功能

缺点:

1)冗余,由于代理对象与目标对象实现一致的接口,则可能会产生过多的代理类。

2)不易维护,若接口增加或删除方法,则所有的代理类与目标类都需要修改。

静态代理实现如下:

java 复制代码
/**
 * 代理模式之静态代理实现
 * 模拟一个保存用户数据的功能
 *
 * 定义一个接口,用于规定实现具体业务(保存数据)的方法,即抽象主题
 */
public interface IUserDao {
    void save();
}

/*******************************************************
 * 具体目标类,即代理模式中的具体主题
 *
 *******************************************************/
public class UserDaoImpl implements IUserDao{
    @Override
    public void save() {
        System.out.println("保存数据");
    }
}


/*******************************************************
 * 静态代理类
 *******************************************************/
public class ProxyUserDao implements IUserDao{
    private IUserDao target;

    public ProxyUserDao(IUserDao target){
        this.target = target;
    }

    @Override
    public void save() {
        System.out.println("开启保存数据~~");
        target.save();
        System.out.println("结束保存数据~~");
    }
}

//测试
public class Example01 {

    public static void main(String[] args) {

        //目标对象
        UserDaoImpl userDao = new UserDaoImpl();
        //代理对象
        ProxyUserDao proxy = new ProxyUserDao(userDao);
        proxy.save();

    }
}

2、jdk动态代理实现

2.1、jdk动态代理实现原理

jdk动态代理利用了JDK API且需要实现接口,动态地在内存中构建代理对象,从而实现

对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。

静态代理与jdk动态代理的区别:

1)静态代理在编译时就已经实现了,编译完成后代理类是一个实际的class文件

2)jdk动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是

在运行时动态生成类字节码,并加载到JVM中

2.2、jdk动态代理涉及到的jdk api

jdk动态代理主要涉及到的类Proxy的newProxyInstance 方法和接口InvocationHandler的

invoke 方法,详情如下:

1)java.lang.reflect.Proxy 的 newProxyInstance 方法如下:

java 复制代码
//返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
static Object newProxyInstance(
    ClassLoader loader,  		//指定当前目标对象使用类加载器
    Class<?>[] interfaces,    //目标对象实现的接口的类型
    InvocationHandler h      //事件处理器
) 

2)java.lang.reflect.InvocationHandler的invoke方法如下:

java 复制代码
// 在代理实例上处理方法调用并返回结果。
Object invoke(Object proxy, Method method, Object[] args) 

2.3、jdk动态代理示例代码

java 复制代码
/*******************************************************
 *jdk动态代理实现
 * 定义一个动态工厂 ProxyFactory,用于生成动态代理对象
 *
 *******************************************************/
public class ProxyFactory {

    //维护的目标对象
    private Object target;

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

    //为目标对象生成代理对象
    public Object getProxyInstance(){

        return Proxy.newProxyInstance(
                //第一个参数:目标对象的类加载器
                target.getClass().getClassLoader(),
                //第二个参数:目标对象所实现的接口类型
                target.getClass().getInterfaces(),
                //第三个参数:事件处理器
                new InvocationHandler() {
                    /**
                     * 用于执行目标对象方法
                     * 可以在目标对象方法执行前后进行个性化扩展
                     *
                     * @param proxy  代理对象
                     * @param method 对应于在代理对象上调用的接口方法Method实例
                     * @param args  代理对象调用接口方法时传递的实际参数
                     * @return  返回目标对象方法的返回值,没有返回值就返回null
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        System.out.println("执行目标对象方法执行  开始");
                        //执行目标对象方法
                        method.invoke(target,args);
                        System.out.println("执行目标对象方法执行  结束");

                        return null;
                    }
                }
        );
    }
}


/*******************************************************
 * 测试
 *
 *******************************************************/
public class Example02 {

    public static void main(String[] args) {

        IUserDao userDao = new UserDaoImpl();
        System.out.println(userDao.getClass().toString());
        ProxyFactory factory = new ProxyFactory(userDao);
        //生成 IUserDao 的代理对象
        IUserDao proxyObject = (IUserDao) factory.getProxyInstance();
        System.out.println(proxyObject.getClass().toString());

        //执行代理对象的目标方法
        proxyObject.save();
    }
}

2.4、jdk动态代理中代理类是如何生成的

我们知道Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。

其中加载阶段需要完成以下3件事情:

1)通过一个类的全限定名来获取定义此类的二进制字节流

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3)在内存中生成一个代表这个类的 `java.lang.Class` 对象,作为方法区这个类的各

种数据访问入口

由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,

获取类的二进制字节流(class字节码)就有很多途径,即:

1)从本地获取

2)从网络中获取

3)运行时计算生成:这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy

类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为

`$Proxy` 的代理类的二进制字节流,流程如下所示:

从这里可以看出jdk动态代理就是采用"运行时计算生成" 的方式来获取代理对象

的二进制字节流。

所以jdk动态代理本质上就是想办法,根据接口或目标对象,计算出代理类的字节码

,然后再加载到JVM中使用

2.5、jdk代理类的调用过程

通过借用阿里巴巴的一款线上监控诊断产品 Arthas(阿尔萨斯) ,对动态生成的代理类代码

进行查看,如下所示:

上边类 UserDaoImpl 的代理类代码如下:

java 复制代码
public final class $Proxy0
extends Proxy
implements IUserDao {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
         m3 = Class.forName("com.mashibing.proxy.example01.IUserDao").getMethod("save", new Class[0]);
          return;
        }
    }

    public final void save() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
    }

    
    //。。。。。。。。。。。。。其他代码省略 。。。。。。。。。。。。。
}

1)动态代理类对象 继承了 Proxy 类,并且实现了被代理的所有接口,以及equals、

hashCode、toString等方法

2)代理类的构造函数,参数是`InvocationHandler`实例,`Proxy.newInstance`方法就是

通过这个构造函数来创建代理实例的

3)类和所有方法都被 `public final` 修饰,所以代理类只可被使用,不可以再被继承

4)每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以

`m + 数字` 的格式命名

5)调用方法的时候通过 `this.h.invoke(this, m3, null));` **实际上 h.invoke就是在调用

ProxyFactory中我们重写的invoke方法

3、CGLIB动态代理

3.1、CGLIB动态代理实现

CGLIB(Code Generation Library ) 是一个第三方代码生成类库,运行时在内存中动态生

成一个子类对象从而实现对目标对象功能的扩展。cglib 为没有实现接口的类提供代理,

为JDK的动态代理提供了很好的补充。

1)最底层是字节码

2)ASM是操作字节码的工具

3)cglib基于ASM字节码工具操作字节码(即动态生成代理,对方法进行增强)

4)SpringAOP基于cglib进行封装,实现cglib方式的动态代理

因为cglib是第三方插件,使用cglib 需要引入cglib 的jar包,如果你已经有spring-core的

jar包,则无需引入,因为spring中包含了cglib ,如下所示:

XML 复制代码
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>

3.2、CGLIB动态代理代码示例

java 复制代码
/*******************************************************
 * 目标类
 *******************************************************/
public class UserServiceImpl {

    //查询功能
    public List<User> findUserList(){

        return Collections.singletonList(new User("tom",18));
    }
}

/*******************************************************
 * 用于生成代理类
 * 需要实现接口 MethodInterceptor
 *
 *******************************************************/
public class UserLogProxy implements MethodInterceptor {

    //目标类
    private Object target;

    /**
     *
     * @param target  需要创建代理的目标对象
     */
    public UserLogProxy(Object target) {
        this.target = target;
    }

    /**
     * 生成CGLIB动态代理类的方法
     * @return 返回生成的代理对象
     */
    public Object getLogProxy(){

        //增强器类,用来创建动态代理类
        Enhancer enhancer = new Enhancer();

        //设置代理类的父类字节码对象,即目标类的Class对象
        enhancer.setSuperclass(target.getClass());

        //设置回调: 对于代理类上所有的方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦截
        enhancer.setCallback(this);

        //创建动态代理对象并返回
        return enhancer.create();

    }


    /**
     * 实现回调方法
     * @param o     代理对象
     * @param method  目标对象中的方法的Method实例
     * @param objects      实际参数
     * @param methodProxy  代理对象中的方法的method实例
     * @return: java.lang.Object
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        System.out.println(formatter.format(calendar.getTime()) + " [" +method.getName() + "] 查询用户信息...]");

        Object result = methodProxy.invokeSuper(o, objects);
        return result;
    }
}


/*******************************************************
 * 测试
 *
 *******************************************************/
public class Example03 {

    public static void main(String[] args) {

        //目标类
        UserServiceImpl userService = new UserServiceImpl();
        System.out.println(userService.getClass().toString());

        //创建代理对象
        UserLogProxy proxy = new UserLogProxy(userService);
        UserServiceImpl userServiceProxy = (UserServiceImpl) proxy.getLogProxy();
        System.out.println(userServiceProxy.getClass().toString());

        //执行代理对象目标方法
        List<User> userList = userServiceProxy.findUserList();
        System.out.println("用户信息: "+userList);
    }
}

@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {

    private String name;

    private Integer age;
}

3.3、CGLIB动态代理执行流程

CGLIB动态代理执行流程如下图所示:

四、代理模式总结

1、三种代理模式实现方式对比

1)cglib代理和jdk代理对比

cglib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用

Java反射效率要高。唯一需要注意的是,cglib不能对声明为final的类或者方法进行代理,

因为cglib原理是动态生成被代理类的子类。

在JDK1.6之后,逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率

高于cglib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比cglib代理效率低一

点,但是到JDK1.8的时候,JDK代理效率高于cglib代理。所以如果有接口使用JDK动态代

理,如果没有接口使用cglib代理

2)静态代理和动态代理对比

动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理

器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多

的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理

类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题

2、代理模式优缺点

1)优点

I)代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用

II)代理对象可以扩展目标对象的功能

III)代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度

2)缺点

I)增加了系统的复杂度

3、代理模式使用场景

1)功能增强

当需要对一个对象的访问提供一些额外操作时,可以使用代理模式,如AOP

2)远程代理

实际上,RPC 框架也可以看作一种代理模式,GoF 的《设计模式》一书中把它称作远

程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。客户端在使用 RPC

服务的时候,就像使用本地函数一样,无需了解跟服务器交互的细节。除此之外,RPC

服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户

端的交互细节。

3)防火墙代理

当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联

网返回响应时,代理服务器再把它转给你的浏览器。

4)保护代理

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

相关推荐
matrixlzp3 小时前
Java 责任链模式 减少 if else 实战案例
java·设计模式
编程、小哥哥6 小时前
设计模式之组合模式(营销差异化人群发券,决策树引擎搭建场景)
决策树·设计模式·组合模式
hxj..7 小时前
【设计模式】外观模式
java·设计模式·外观模式
吾与谁归in7 小时前
【C#设计模式(10)——装饰器模式(Decorator Pattern)】
设计模式·c#·装饰器模式
无敌岩雀9 小时前
C++设计模式行为模式———命令模式
c++·设计模式·命令模式
In_life 在生活18 小时前
设计模式(四)装饰器模式与命令模式
设计模式
瞎姬霸爱.19 小时前
设计模式-七个基本原则之一-接口隔离原则 + SpringBoot案例
设计模式·接口隔离原则
鬣主任19 小时前
Spring设计模式
java·spring boot·设计模式
程序员小海绵【vincewm】21 小时前
【设计模式】结合Tomcat源码,分析外观模式/门面模式的特性和应用场景
设计模式·tomcat·源码·外观模式·1024程序员节·门面模式
丶白泽21 小时前
重修设计模式-行为型-命令模式
设计模式·命令模式