深入理解 Java 动态代理

2023年 9月 21日 113.9k 0

什么是动态代理?

在 Java 中,动态代理是一种代理模式的实现方式,它允许在运行时动态地创建代理类并动态地处理方法调用。
动态代理常用于解耦合、处理和其他对象交互相关的行为控制。

动态代理例子

下面是一个简单的Java动态代理示例,它演示了如何使用动态代理创建一个代理对象,以及如何在代理对象上调用方法并在方法调用前后执行一些操作:

首先,我们需要创建一个代理接口,定义一些方法:

// 定义代理接口
public interface MyInterface {
    void doSomething();
}

同时,也定义一个 MyInterface 的实现类:

public class MyInterfaceImpl implements MyInterface {
    @Override
    public void doSomething() {
        System.out.println("MyInterfaceImpl doSomething");
    }
}

然后,创建一个实现 InvocationHandler 接口的处理器类,它将负责在代理对象上调用方法时执行额外的操作:

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

public class MyInvocationHandler implements InvocationHandler {
    private final Object realObject;

    public MyInvocationHandler(Object realObject) {
        this.realObject = realObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoking " + method.getName());
        
        // 调用真实对象的方法
        Object result = method.invoke(realObject, args);
        
        System.out.println("After invoking " + method.getName());
        
        return result;
    }
}

接下来,我们可以在主程序中使用动态代理创建代理对象并调用方法:

import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        // 创建一个真实对象
        MyInterface myInterface = new MyInterfaceImpl();

        // 创建代理处理器
        MyInvocationHandler handler = new MyInvocationHandler(myInterface);

        // 创建代理对象
        MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
            MyInterface.class.getClassLoader(),
            new Class[]{MyInterface.class},
            handler
        );

        // 调用代理对象的方法
        proxy.doSomething();
    }
}

在上述示例中,我们首先定义了一个代理接口 MyInterface ,然后创建了一个实现了 InvocationHandler 接口的处理器类 MyInvocationHandler
该处理器负责在方法调用前后输出日志。最后,在主程序中,我们使用 Proxy.newProxyInstance 方法创建了代理对象,并通过代理对象调用了方法。在方法调用前后,会执行处理器中的操作。

以上代码会输出:

Before invoking doSomething
MyInterfaceImpl doSomething
After invoking doSomething

我们可以看到,虽然我们好像是在调用 MyInterface 接口的方法,但是实际上又不单纯是调用这个方法,还在调用这个方法前后做了一些事情。

我们可以通过下图来更加直观地了解上面这个动态代理的例子到底做了什么:

1.png

这个图体现了使用 Java 动态代理的一些关键:

  • 要使用 Java 中的 Proxy 来实现动态代理我们就需要定义 interface
  • 我们需要通过 InvocationHandler 来调用被代理对象的方法,在 InvocationHandlerinvoke 方法中,我们可以在调用被代理对象方法前后加一些自定义逻辑
  • 需要通过 Proxy.newProxyInstance 创建代理对象,这个代理对象实现了我们指定的接口(也就是第二个参数传递的 interface)。

这个示例演示了如何使用动态代理创建代理对象,并在方法调用前后执行自定义的操作。在实际应用中,你可以根据需要自定义处理器类,以执行不同的操作,如日志记录、性能监测、事务管理等。

动态代理的使用场景

动态代理在 Java 中是用得非常多的,使用动态代理我们可以在不侵入代码的情况下,为类增加或修改行为。常见的使用场景如下:

  • RPC
  • 在我们使用 Dubbo 的时候在我们服务端的类上加一个 @DubboService 注解就可以将一个类声明为可以被远程调用的服务了;
    然后在客户端进行注入的时候,加上 @DubboReference 就可以将服务声明为一个远程服务了。

    // 服务端
    @DubboService(version = "${product.service.version}")
    @Service
    public class ProductServiceRpcImpl implements ProductService {
        @Autowired
        private IProductService productService;
    
        @Override
        public Sku test(Long id) {
            return productService.test();
        }
    }
    
    // 客户端
    @Component
    public class ProductClient {
        @DubboReference(
            version = "${product.service.version}", 
            cluster = "failfast", 
            timeout = 5000,
            retries = 2,
            proxy = "jdk" // 一般情况不需要加这个配置
        )
        private ProductService productService;
    
        public void test() {
            System.out.println(productService.test(1L));
        }
    }
    

    在上面 Dubbo 客户端的代码中,我们在 @DubboReference 的注解中加了个 proxy = "jdk" 表示我们要使用 JDK 的动态代理,

    但是我们需要知道的是,JDK 的动态代理性能上并不是最优的,所以 Dubbo 的默认代理方式不是 JDK 动态代理,上面指定 proxy 只是为了演示。

    Dubbo 实现动态代理的方式有三种:bytebuddyjavassistjdk

    我们可以通过下面代码验证一下上面的 ProductClient 是否真的使用了 JDK 的动态代理:

     public void test() {
        // class com.sun.proxy.$Proxy123
        System.out.println(productService.getClass());
        // true
        System.out.println(Proxy.isProxyClass(productService.getClass()));
    }
    

    从输出结果我们可以看到,productService 实际上就是 JDK 代理类的对象。

  • Spring 容器
  • 我们知道 Spring 容器可以帮助我们注入一些对象,而这些被注入的对象都是代理对象,直接 new 出来的对象跟 Spring 注入的对象是不一样的:

    @Slf4j
    @SpringBootApplication
    @MapperScan("com.baiguiren.mapper")
    @RestController
    public class Main {
        @Autowired
        private PersonService personService;
    
        public static void main(String[] args) {
            SpringApplication.run(Main.class, args);
        }
    
        @GetMapping("/test")
        public void test() {
            // class com.sun.proxy.$Proxy66 这是一个代理类
            System.out.println(personService.getClass());
    
            PersonService personService1 = new PersonServiceImpl();
            // class com.baiguiren.service.Impl.PersonServiceImpl,这是我们定义的类
            System.out.println(personService1.getClass());
        }
    }
    

    从上面这个例子我们发现,Spring 注入的对象,它的 Class 并不是我们所定义的那个类,而是一个代理类。

    我的 aop 配置是 spring.aop.proxy-target-class=false,这样 Spring 会使用 JDK 代理,当然我们也可以去掉这个配置或者配置为 false,这样 Spring 会使用 CGLIB 来实现动态代理。

    在使用 CGLIB 动态代理的时候,上面的 personService.getClass() 会输出 class (...).PersonServiceImpl$$EnhancerBySpringCGLIB$$23b7767d
    我们可以在输出中看到 $$EnhancerBySpringCGLIB$$,这就意味着我们拿到的注入的对象实际上是被 Spring 容器增强过的实例。

    同样的,在我们使用 mybatisMapper 的时候,它也是一个代理:

    @Service
    public class PersonServiceImpl implements PersonService {
        @Autowired
        private PersonMapper personMapper;
    
        @Override
        public Person find(int id) {
            System.out.println(personMapper instanceof Proxy); // true
            return personMapper.selectPersonById(id);
        }
    }
    

    所以虽然我们定义了一个接口,里面什么内容也没写,但实际上底层会帮我们去实现一个类,然后基于这个类来创建实例。

  • 事务管理
  • 事实上,这也可以归到 2 中去,当然我们也可以在不使用 Spring 的情况下通过动态代理来实现事务管理:

    @Component
    @Slf4j
    public class FooServiceImpl implements FooService {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        @Transactional(rollbackFor = RollbackException.class) // 事务管理
        public void insertThenRollback() throws RollbackException {
            jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('BBB')");
            throw new RollbackException();
        }
    }
    

    这个例子中的 Transactional 其实也是 Spring 给我们提供的功能。通过 Transactional 注解,我们的 FooServiceImpl::insertThenRollback 会得到增强。

    在我们抛出 RollbackException 的时候,这个方法中执行的操作将会被回滚,但是我们不需要手动去执行开启事务、提交事务、回滚事务的操作,比较方便。

    上面的 insertThenRollback 转换为伪代码如下:

    try {
        beginTransaction();
        jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('BBB')");
        commit();
    } catch (RollbackException e) {
        rollback();
    }
    

    通过上面这几个场景,相信我们已经体会到了动态代理的强大之处了,
    当然实际中的使用远不止上面这几种场景。

    代理模式

    我们在文章开头就说了,动态代理是一种代理模式的实现方式。

    现在我们再来看看代理模式的定义:

    在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

    一个很简单的例子如下,我们需要针对控制器中的所有方法做性能监测:

    @GetMapping("/test1")
    public void test1() throws InterruptedException {
        long start = System.currentTimeMillis();
    
        // 实际业务逻辑处理
        System.out.println("test1...");
        // 模拟耗时长的操作
        Thread.sleep(100);
    
        long end = System.currentTimeMillis();
    
        log.info("GET /test1: " + (end - start)); // 输出日志
    }
    

    我们可以看到,上面这个例子实现的方式其实比较笨拙,如果我们有 100 个接口,上面的代码就得重复 100 遍,这种实现方式对开发人员是非常不友好的。

    使用动态代理,我们可以有更加优雅的实现方式:

    // 定义接口
    public interface ITestController {
        void test1();
    }
    
    // 定义实现类(被代理类)
    public class TestController implements ITestController {
        @Override
        public void test1() {
            System.out.println("test1...");
        }
    }
    
    // 定义 InvocationHandler
    @Slf4j
    public class ControllerInvocationHandler implements InvocationHandler {
        private final ITestController testController;
    
        public ControllerInvocationHandler(ITestController testController) {
            this.testController = testController;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long start = System.currentTimeMillis();
    
            // 调用被代理类的方法
            Object result =method.invoke(testController, args);
    
            long end = System.currentTimeMillis();
            log.info(method.getName() + " invoked: " + (end - start)); // 输出日志
    
            return result;
        }
    }
    
    // 创建代理对象
    ITestController testController = new TestController();
    // 代理对象,它实现了所有的 testController.getClass().getInterfaces()
    ITestController testControllerProxy = (ITestController) Proxy.newProxyInstance(
            testController.getClass().getClassLoader(),
            testController.getClass().getInterfaces(),
            new ControllerInvocationHandler(testController)
    );
    
    // 调用代理对象的方法
    testControllerProxy.test1();
    

    当然,这也只是个例子,如果我们使用 Spring,我们可以通过 aspect 来实现上面这个功能。

    同时,我们也发现,使用了动态代理之后,我们就不必在每一个方法中加上那些性能监测的代码了,这就可以将开发人员从繁琐的操作中解放出来了。

    动态代理实现原理

    了解了动态代理的基本使用、常用的使用场景之后,让我们再来了解一下它的实现原理(基于 JDK 1.8)。

    还是以上一小节的例子来说明,在上面的例子中,我们为 ITestController 接口升成了一个代理对象,但是这个代理对象为什么可以直接调用被代理对象的方法呢?

    这是因为,底层实际上是创建另一个继承自 java.lang.reflect.Proxy 的代理类,这个代理类实现了我们指定的 interfaces,而其中的方法体中,则会去调用实际被代理对象的方法。

    这么说有点抽象,其实我们可以通过 Java 的一个命令行参数来让 Java 帮我们将升成的代理类保存到文件上:

    // 注意:是 Java 8,新版本可能会有一些差异
    -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
    

    对于上一个例子中的 ITestController 的代理类,我们可以看到它的内容实际如下(项目目录下的 com/sun/proxy 文件夹):

    package com.sun.proxy;
    
    // ...
    // $Proxy69 继承了 java.lang.reflect.Proxy 这个类,
    // 实现了 ITestController 接口
    
    public final class $Proxy69 extends Proxy implements ITestController {
        private static Method m3;
        // ... 省略其他代码
      
        public $Proxy69(InvocationHandler var1) throws  {
            super(var1);
        }
    
        public final void test1() throws  {
            try {
                // 实际上调用了 InvocationHandler 的 invoke 方法
                // this 也就是代理对象
                // m3 是实际接口的 reflect.Method
                // 实际调用的时候,没有传递任何参数
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        // ... 省略其他代码
        static {
            try {
                // 也就是 ITestController 中 test1 的反射 Method
                m3 = Class.forName("com.baiguiren.ITestController").getMethod("test1");
            } ...
        }
    }
    

    从生成的代码中,我们可以看到,所有对代理对象的方法调用,都委托给了 InvocationHandler::invoke 方法。

    生成代理类的过程可以用下图表示(Proxy 类中的实现):

    2.png

  • Proxy 类中有两个方法会创建代理类,一个是 getProxyClass,另一个是 newProxyInstancegetProxyClass 获取 Class 对象,而 newProxyInstance 会基于这个代理类创建一个代理对象。
  • 上一步的两个方法中都会调用 getProxyClass0 来获取 Class 对象,这个方法会有缓存,如果之前已经创建则会从缓存获取,否则会新建一个 Class
  • 如果是第一次创建代理类,则会通过 ProxyClassFactory 来创建一个新的 Class
  • 在实际加载到内存之前,会通过 ProxyGeneratorgenerateProxyClass 方法来生成 Class 对象的字节码
  • 通过 defineClass0 方法来创建一个 Class 对象。(这是一个 native 方法,具体实现由 JVM 提供)。
  • 拿到了代理类,也就是上面那一段 $Proxy69 类所表示的类之后,就可以创建代理对象了。

    因为这个代理类实现了我们指定的接口,所以是可以通过类型转换转换成不同的 interface 的,如 (ITestController) Proxy.newProxyInstance(...)

    最终,我们就拿到了一个实现了 ITestController 接口的代理对象,它的行为跟我们原来的对象完全一致,通过这种方式我们获得了一个附加了额外功能的代理对象。

    其他实现动态代理的方式

    除了使用 JDK 的动态代理之外,还有一种实现动态代理的方式是 CGLIB 动态代理,上面也有提及,他们都有一些局限性,需要根据我们的实际应用场景来选择。

    比如,JDK 动态代理需要我们先定义 interface,而 CGLIB 是通过继承的方式来实现动态代理的,如果某个类有 final 标记,那它是无法使用 CGLIB 来做动态代理的。

    本文主要是为了介绍动态代理的一些原理,至于其他代理方式,都是殊途同归的,所以本文不做过多探讨了。

    总结

    动态代理是一种 Java 编程技巧,通过在运行时生成代理类,它能够拦截对原始类的方法调用并在方法执行前后执行自定义操作,
    这一特性使其广泛应用于解耦、性能监测、事务管理、RPC、AOP 等各种场景,提高了代码的可维护性和可扩展性。

    最后,再简单总结一下本文的内容:

    • 动态代理是一种代理模式的实现方式,它允许在运行时动态地创建代理类并动态地处理方法调用。
    • 动态代理被广泛应用于 Spring 中,我们通过 @Autowired 注入的对象就是一个代理对象。
    • 动态代理还被用于 RPC、事务管理等,可以将非业务代码放到代理层去实现
    • JDK 的动态代理是在运行时才根据 ClassLoaderinterface 来创建 Class 的,具体加载到 JVM 的操作是底层帮我们实现的
    • 还有一种很常见的动态代理方式是 CGLIB 动态代理,但它不能代理 final 类。
    • 而使用 JDK 动态代理则需要先定义 interface,代理类会实现这些 interface,但是实际上还是会调用被代理对象的方法(通过 Invocationhandler)。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论