【Spring学习笔记 六】静态/动态代理实现机制

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 【Spring学习笔记 六】静态/动态代理实现机制

为什么要学习代理模式,因为AOP的底层机制就是动态代理,所以在谈论AOP之前,我们先来谈论下代理的实现机制。在学习上一个系列MyBatis分析其源码时其实就遇到过代理模式,准确的是动态代理模式,【MyBatis学习笔记 四】MyBatis基本运行原理源码解析,在这篇Blog里我分析了其实我们MyBatis的的Mapper代理的实现方式就是一个无实现类的动态代理,通过类型全限定名用反射去获取类型,然后在执行具体方法时,触发附加在该方法上的代理去执行,又因为该方法无实际执行内容,所以实现的逻辑其实是代理里的逻辑,因为接下来要讨论的AOP这个比较大的概念需要用到动态代理,所以我们花一篇Blog的精力去梳理下整个静态代理和动态代理的实现机制。

代理模式

在了解动态代理和静态代理的具体实现之前我们先了解下什么是代理模式,代理模式就是:给目标对象提供一个代理对象,代理对象包含该目标对象,并控制对该目标对象的访问,我们为什么要使用代理模式呢?

  • 通过代理对象的隔离,可以在对目标对象访问前后增加额外的业务逻辑,实现功能增强类似于before和after的触发器,在标准功能之外提供用户自己的扩展
  • 通过代理对象访问目标对象,可以防止系统大量地直接对目标对象进行不正确地访问,出现不可预测的后果,类似接口的入口,可以增加参数控制,权限控制

代理模式其实优点类似于Servlet过滤器,对请求做一些处理。

代理模式实现步骤

要想实现代理模式,需要走以下4个步骤,分别创建代理接口、实现类以及代理类(这里是最大化步骤,例如无实现类的MyBatis不需要下述的步骤2):

  1. 定义真实类和代理类的公共接口,也就是代理接口
  2. 定义真实类,并实现代理接口
  3. 定义代理类,并实现代理接口 ,代理类内部保存对真实目标类的引用
  4. 访问者仅能通过代理对象访问真实目标对象,不可直接访问目标对象

代理对象并不是真正提供服务的一个对象,它只是替访问者访问目标对象的一个中间人,真正提供服务的还是目标对象,而代理对象的作用就是在目标对象提供服务之前和之后能够执行额外的逻辑

代理模式实践

按照上述步骤我们来实践一个代理模式,看看代理模式是如何具体工作的,例如我们想要在创建MySQL数据库连接时做如下两个动作:在连接前判断当前登录人是否有权限创建连接,在获取连接后加一个日志,输出确实创建连接成功了

1 定义代理接口

首先我们定义真实类和代理类的公共接口,也就是代理接口

package com.example.Spring.aggregate;
public interface MySqlConnectionCreate {
    void createMySqlConnection(String dbName);
}

2 定义真实类

然后我们定义真实类,并实现代理接口

package com.example.Spring.aggregate;
public class MySqlConnectionCreateImpl implements MySqlConnectionCreate {
    @Override
    public void createMySqlConnection(String dbName) {
        System.out.println("创建MySQL数据库连接成功,连接到:"+dbName);
    }
}

3 定义代理类

最后我们定义代理类,并实现代理接口 ,代理类内部保存对真实目标类的引用

package com.example.Spring.aggregate;
public class SqlConnectionProxy implements MySqlConnectionCreate {
    private MySqlConnectionCreateImpl mySqlConnectionCreateImpl;
    public SqlConnectionProxy(MySqlConnectionCreateImpl mySqlConnectionCreateImpl) {
        this.mySqlConnectionCreateImpl = mySqlConnectionCreateImpl;
    }
    @Override
    public void createMySqlConnection(String dbName) {
       this.doSomethingBefore();
       mySqlConnectionCreateImpl.createMySqlConnection(dbName);
       this.doSomethingAfter();
    }
    private void doSomethingBefore() {
        System.out.println("连接数据库前的额外操作:权限验证");
    }
    private void doSomethingAfter() {
        System.out.println("连接数据库前的额外操作:日志记录");
    }
}

4 调用代理类

然后我们测试一下,访问者仅能通过代理对象访问真实目标对象,不可直接访问目标对象

package com.example.Spring.aggregate;
public class PersonDao {
    public static void main(String[] args) {
            MySqlConnectionCreateImpl mySqlConnectionCreateImpl = new MySqlConnectionCreateImpl();
            SqlConnectionProxy proxy = new SqlConnectionProxy(mySqlConnectionCreateImpl);
            proxy.createMySqlConnection("TML");
        }
}

我们来看下测试结果:

静态代理

其实上述的步骤就是我们实现一个静态代理的场景,我们在不改变原来的代码的情况下,实现了对原有功能的增强,但是还有一个问题,如果我们新增了一个需求,要求代理类还能代理Oracle的数据库连接,这个时候静态代理该如何处理呢?

  1. 定义Oracle真实类和代理类的公共接口,也就是Oracle连接创建的代理接口
  2. 定义Oracle真实类,并实现Oracle连接创建的代理接口
  3. 修改当前的代理类SqlConnectionProxy,并增加实现Oracle连接创建的代理接口 ,代理类内部增加对Oracle真实目标类的引用
  4. 调用时需要使用不同的方法进行调用以区别MySql的连接和Oracle的连接

我们的代理类就需要修改了,这样来看代理类就比较臃肿了,不符合我们的开闭原则,我们来感受下静态代理增加多个的实现成本:

静态代理扩展步骤

依然和上述步骤相同

1 定义代理接口

首先我们定义真实类和代理类的公共接口,也就是代理接口,我们扩展一个Oracle的实现:

package com.example.Spring.aggregate;
public interface OracleConnectionCreate {
    void createOracleConnection(String dbName);
}

2 定义真实类

然后我们定义真实类,并实现代理接口,我们扩展一个Oracle的实现:

package com.example.Spring.aggregate;
public class OracleConnectionCreateImpl implements OracleConnectionCreate{
    @Override
    public void createOracleConnection(String dbName) {
        System.out.println("创建Oracle数据库连接成功,连接到:"+dbName);
    }
}

3 定义代理类

最后我们定义代理类,并实现代理接口 ,代理类内部保存对真实目标类的引用,我们扩展一个Oracle的实现,这里我们还需要修改构造函数,总之维护起来不符合开闭原则:

package com.example.Spring.aggregate;
public class SqlConnectionProxy implements MySqlConnectionCreate,OracleConnectionCreate {
    private MySqlConnectionCreateImpl mySqlConnectionCreateImpl;
    private OracleConnectionCreateImpl oracleConnectionCreateImpl;
    public SqlConnectionProxy(MySqlConnectionCreateImpl mySqlConnectionCreateImpl,OracleConnectionCreateImpl oracleConnectionCreateImpl) {
        this.mySqlConnectionCreateImpl = mySqlConnectionCreateImpl;
        this.oracleConnectionCreateImpl = oracleConnectionCreateImpl;
    }
    @Override
    public void createMySqlConnection(String dbName) {
       this.doSomethingBefore();
       mySqlConnectionCreateImpl.createMySqlConnection(dbName);
       this.doSomethingAfter();
    }
    @Override
    public void createOracleConnection(String dbName) {
        this.doSomethingBefore();
        oracleConnectionCreateImpl.createOracleConnection(dbName);
        this.doSomethingAfter();
    }
    private void doSomethingBefore() {
        System.out.println("连接数据库前的额外操作:权限验证");
    }
    private void doSomethingAfter() {
        System.out.println("连接数据库前的额外操作:日志记录");
    }
}

4 调用代理类

然后我们测试一下,访问者仅能通过代理对象访问真实目标对象,不可直接访问目标对象,我们扩展一个Oracle的实现:

package com.example.Spring.aggregate;
public class PersonDao {
    public static void main(String[] args) {
        MySqlConnectionCreateImpl mySqlConnectionCreateImpl = new MySqlConnectionCreateImpl();
        OracleConnectionCreateImpl oracleConnectionCreateImpl = new OracleConnectionCreateImpl();
        SqlConnectionProxy proxy = new SqlConnectionProxy(mySqlConnectionCreateImpl,oracleConnectionCreateImpl);
        proxy.createMySqlConnection("TML MySQL");
        proxy.createOracleConnection("TML Oracle");
    }
}

打印结果如下,代理操作都生效了:

静态代理的问题

软件工程中有一个开闭原则,静态代理违反了开闭原则,原因是:面对新的需求时,需要修改代理类,增加实现新的接口和方法,导致代理类越来越庞大,变得难以维护

开闭原则:在编写程序的过程中,软件的所有对象应该是对扩展是开放的,而对修改是关闭的

虽然说目前代理类只是实现了两个接口,如果日后还需要代理SqlServer的数据库连接,代理MongoDB的数据库连接等等,实现的接口会变得越来越多,内部的结构变得越来越复杂,整个类显得愈发臃肿,变得不可维护,之后的扩展也会成问题,只要任意一个接口有改动,就会牵扯到这个代理类,维护的代价很高。所以,为了提高类的可扩展性和可维护性,满足开闭原则,Java 提供了动态代理机制

动态代理

动态代理解决的问题是面对新的需求时,不需要修改代理对象的代码,只需要新增接口和真实对象,在客户端调用即可完成新的代理。这样做的目的:满足软件工程的开闭原则,提高类的可维护性和可扩展性。换句话说:动态代理的代理类是动态生成的 . 静态代理的代理类是我们提前写好的,动态代理分为两类 : 一类是基于接口动态代理 , 一类是基于类的动态代理

  • JDK动态代理,即JDK Proxy,基于接口的动态代理
  • CGLIB动态代理,基于类的动态代理,不是 JDK 自带的动态代理,它需要导入第三方依赖,它是一个字节码生成类库,能够在运行时动态生成代理类对 Java类 和 Java接口 扩展

这里我们主要讨论JDK Proxy,代理对象是在程序运行过程中,由代理工厂动态生成,代理对象本身不存在 Java 源文件

代理工厂实现

与静态代理不同的是代理类的生成方式,所以我们这里不重复上述扩展接口和实现类的内容,专注于如何实现一个代理工厂:

首先,代理工厂需要实现InvocationHanlder接口并实现其invoke()方法

package com.example.Spring.aggregate;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyInvocationHandler implements InvocationHandler {  
    private Object target;
    public ProxyInvocationHandler(Object target) {
        this.target = target;
    }
    //invoke() 方法有3个参数:Object proxy:代理对象, Method method:真正执行的方法
    //Object[] agrs:调用第二个参数 method 时传入的参数列表值
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        this.doSomethingBefore();
        Object object=method.invoke(target, args);
        this.doSomethingAfter();
        return object;
    }
    private void doSomethingBefore() {
        System.out.println("连接数据库前的额外操作:权限验证");
    }
    private void doSomethingAfter() {
        System.out.println("连接数据库前的额外操作:日志记录");
    }
}

invoke() 方法是一个代理方法,也就是说最后客户端请求代理时,执行的就是该方法。

代理方法执行调用

代理工厂类到这里为止已经结束了,我们接下来看第二点:如何通过代理工厂动态生成代理对象并执行调用,

package com.example.Spring.aggregate;
import java.lang.reflect.Proxy;
public class PersonDao {
    public static void main(String[] args) {
        MySqlConnectionCreate mySqlConnectionCreateImpl = new MySqlConnectionCreateImpl();
        OracleConnectionCreate oracleConnectionCreateImpl = new OracleConnectionCreateImpl();
        //一个动态代理 , 一般代理某一类业务 , 一个动态代理可以代理多个类,代理的是接口!
        MySqlConnectionCreate mySqlProxy = (MySqlConnectionCreate) Proxy
                .newProxyInstance(
                        mySqlConnectionCreateImpl.getClass().getClassLoader(),   //类加载器
                        mySqlConnectionCreateImpl.getClass().getInterfaces(),    //代理的接口
                        new ProxyInvocationHandler(mySqlConnectionCreateImpl)); //动态代理处理逻辑
        mySqlProxy.createMySqlConnection("TML MySQL");
        OracleConnectionCreate oracleProxy = (OracleConnectionCreate) Proxy
                .newProxyInstance(
                        oracleConnectionCreateImpl.getClass().getClassLoader(),   //类加载器
                        oracleConnectionCreateImpl.getClass().getInterfaces(),    //代理的接口
                        new ProxyInvocationHandler(oracleConnectionCreateImpl)); //动态代理处理逻辑
        oracleProxy.createOracleConnection("TML Oracle");
    }
}

实现效果如下:

即使我们再增加新的扩展,也不需要修改代理工厂类了,总结一下 JDK 的动态代理:

  • JDK 动态代理的使用方法,代理工厂需要实现 InvocationHandler接口,调用代理方法时会转向执行invoke()方法,生成代理对象需要使用Proxy对象中的newProxyInstance()方法,返回对象可强转成传入的其中一个接口,然后调用接口方法即可实现代理
  • JDK 动态代理的特点,目标对象强制需要实现一个接口,否则无法使用 JDK 动态代理

到这里我们就知道如何使用动态代理了,关于无实现类动态代理,参照前述的MyBatis笔记吧。

动态代理源码分析

Proxy.newProxyInstance() 是生成动态代理对象的关键,我们可来看看它里面到底干了些什么:

@CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
        /*
         * Look up or generate the designated proxy class.
         */
          // 获取代理类的 Class 对象
        Class<?> cl = getProxyClass0(loader, intfs);
        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
         // 获取代理对象的显示构造器,参数类型是 InvocationHandler
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            // 反射,通过构造器实例化动态代理对象
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

然后我们看代理类Class对象如何获取的:

private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        return proxyClassCache.get(loader, interfaces);
    }

发现里面用到一个缓存 proxyClassCache,从结构来看类似于是一个 map 结构,根据类加载器loader和真实对象实现的接口interfaces查找是否有对应的 Class 对象,我们接着往下看 get() 方法:

public V get(K key, P parameter) {
     // 先从缓存中查询是否能根据 key 和 parameter 查询到 Class 对象
     // ...
     // 生成一个代理类
     Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
 }

get() 方法中,如果没有从缓存中获取到 Class 对象,则需要利用 subKeyFactory 去实例化一个动态代理对象,而在 Proxy 类中包含一个 ProxyClassFactory 内部类,由它来创建一个动态代理类,所以我们接着去看 ProxyClassFactory 中的 apply() 方法。

private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
    // 非常重要,这就是我们看到的动态代理的对象名前缀!
  private static final String proxyClassNamePrefix = "$Proxy";
    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        // 一些状态校验
        // 计数器,该计数器记录了当前已经实例化多少个代理对象
        long num = nextUniqueNumber.getAndIncrement();
        // 动态代理对象名拼接!包名 + "$Proxy" + 数字
        String proxyName = proxyPkg + proxyClassNamePrefix + num;
        // 生成字节码文件,返回一个字节数组
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            // 利用字节码文件创建该字节码的 Class 类对象
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            throw new IllegalArgumentException(e.toString());
        }
    }
}

apply() 方法中注意有两个非常重要的方法:

  • ProxyGenerator.generateProxyClass():它是生成字节码文件的方法,它返回了一个字节数组,字节码文件本质上就是一个字节数组,以 proxyClassFile数组就是一个字节码文件
  • defineClass0():生成字节码文件的 Class 对象,它是一个 native 本地方法,调用操作系统底层的方法创建类对象

而 proxyName 是代理对象的名字,我们可以看到它利用了 proxyClassNamePrefix + 计数器 拼接成一个新的名字

总结一下

代理可以分为静态代理和动态代理两大类:

静态代理

  • 优点:代码结构简单,较容易实现
  • 缺点:无法适配所有代理场景,如果有新的需求,需要修改代理类,不符合软件工程的开闭原则

动态代理

  • 优点:能够动态适配特定的代理场景,扩展性较好,符合软件工程的开闭原则
  • 缺点:动态代理需要利用到反射机制和动态生成字节码,导致其性能会比静态代理稍差一些,但是相比于优点,这些劣势几乎可以忽略不计

大多数场景我们使用动态代理,而反射+动态代理+注解又是大多数框架的底层实现机制,实际就是过滤器或者拦截器对原有逻辑做了些修改而便捷我们的操作,我认为这可以算作框架的本质,框架依据反射明确要作用的类或方法,依据注解读取定义在类或方法属性上的注释从而知道调用者想如何操作这些类或方法,在调用方法前后读取到动态代理的附加逻辑并执行本质上是框架在运行时依据一系列的配置内容加上和调用者约定的规则搞清楚调用者的意图并执行调用者意图的过程

相关文章
|
5天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
6月前
|
运维 Java 程序员
Spring5深入浅出篇:Spring动态代理详解
# Spring动态代理详解 本文探讨了Spring中的MethodBeforeAdvice和MethodInterceptor在动态代理中的应用和差异。MethodBeforeAdvice在方法执行前执行额外功能,而MethodInterceptor则可在方法执行前后或抛出异常时运行额外逻辑。MethodInterceptor还能影响原始方法的返回值。
|
3月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
30天前
|
Java 数据安全/隐私保护 Spring
Spring进阶:初识动态代理
本文介绍了Spring框架中AOP切面编程的基础——动态代理。通过定义Vehicle接口及其实现类Car和Ship,展示了如何使用动态代理在不修改原代码的基础上增强功能。文章详细解释了动态代理的工作原理,包括通过`Proxy.newProxyInstance()`方法创建代理对象,以及`InvocationHandler`接口中的`invoke()`方法如何处理代理对象的方法调用。最后,通过一个测试类`TestVehicle`演示了动态代理的具体应用。
|
2月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
6月前
|
Java 测试技术 开发者
Spring IoC容器通过依赖注入机制实现控制反转
【4月更文挑战第30天】Spring IoC容器通过依赖注入机制实现控制反转
61 0
|
3月前
|
Java 开发工具 Spring
Spring的Factories机制介绍
Spring的Factories机制介绍
70 1
|
4月前
|
安全 Java API
构建基于Spring Boot的REST API安全机制
构建基于Spring Boot的REST API安全机制
|
5月前
|
Java 应用服务中间件 Spring
解析Spring Boot自动装配的原理与机制
解析Spring Boot自动装配的原理与机制
114 4
|
4月前
|
Java Spring
解析Spring Boot中的事务管理机制
解析Spring Boot中的事务管理机制