XiaoLin's Blog

Xiao Lin

动态代理设计模式学习笔记

8
2024-02-26

动态代理模式介绍

动态代理模式是一种在实现阶段不用关心代理类,而在运行阶段才指定哪一个对象的设计模式。在动态代理中,代理类和委托类的关系是在运行时确定的。这种模式通常用于提高系统的灵活性,例如在远程调用、事务处理或缓存等方面。

动态代理模式的主要优点是可以在运行时动态地改变代理对象,从而在不修改原有代码的情况下实现对目标对象的访问和控制。此外,动态代理还可以提高系统的性能,因为它可以避免在运行时进行大量的反射操作。

动态代理模式的实现通常需要用到反射机制,以便在运行时动态地创建代理对象并调用其方法。在 Java 编程语言中,动态代理可以通过 Java 代理类来实现。代理类是一个实现了特定接口的类,它可以在运行时动态地生成并返回一个实现了该接口的代理对象。通过使用代理类,可以在不修改原有代码的情况下实现对目标对象的访问和控制。

动态代理实现 - jdk 实现方式

使用 jdk 的动态代理的要求是目标类一定是有接口的,不然会报错

JDK 动态代理需要接口的原因是因为 Java 的动态代理机制是基于 Java 反射机制实现的,而 Java 反射机制只能处理接口类型的类。
在 Java 中,接口是一种定义了方法签名的抽象类型,它不包含任何实现。Java 反射机制允许在运行时动态地访问和修改类的成员,包括方法、属性、构造函数等。因此,Java 反射机制只能处理接口类型的类,因为接口只包含方法签名,不包含实现。
在 JDK 动态代理中,代理类是在运行时动态生成的,它实现了目标类所实现的接口,并且在代理类中重写了目标类的方法。这样,在运行时,代理类就可以通过反射机制动态地访问和修改目标类的方法。如果目标类没有实现接口,那么代理类就无法通过反射机制访问和修改目标类的方法,因此 JDK 动态代理需要目标类实现接口。
需要注意的是,Java 的动态代理机制只能代理接口类型的类,如果目标类没有实现接口,那么就无法使用 JDK 动态代理。但是,可以使用第三方库,如 CGLIB 等,来实现对目标类的代理。

  1. 定义一个具体的抽象接口。
package xyz.xiaolinz.demo.proxy.dynamicproxy.jdk;

/**
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/8/4
 */
public interface DataQuery {

  String query(String queryKey);


  String queryAll();
}

  1. 定义接口的具体实现类,也就是被代理类
package xyz.xiaolinz.demo.proxy.dynamicproxy.jdk;

/**
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/8/4
 */
public class DataBaseDataQuery implements DataQuery {

  @Override
  public String query(String queryKey) {
    // 使用数据源查询数据
    System.out.println("从数据库查询数据");
    return "query";
  }

  @Override
  public String queryAll() {
    return "queryAll";
  }
}

  1. 编写具体的动态代理类,通过实现 InvocationHandler 接口来获取动态代理的相关能力
package xyz.xiaolinz.demo.proxy.dynamicproxy.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/8/7
 */
public class CacheInvocationHandler implements InvocationHandler {

  private final Map<String, String> cache = new HashMap<>(10);

  private final DataQuery bean;

  public CacheInvocationHandler(DataQuery bean) {
    this.bean = bean;
  }

  /**
   * @param proxy 代理对象
   * @param method 方法
   * @param args 参数
   * @return {@link Object}
   * @throws Throwable 可投掷
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 通过代理对象调用方法时,会调用这个方法

    // 判断是否是 query 方法
    if ("query".equals(method.getName())) {
      // 如果是 query 方法,就使用缓存
      if (cache.containsKey(args[0])) {
        System.out.println("从缓存中查询数据");
        return cache.get(args[0]);
      } else {
        // 如果没有缓存,就调用被代理对象的方法
        final var result = method.invoke(bean, args);
        // 将结果缓存
        cache.put((String) args[0], (String) result);
        return result;
      }
    }

    // 返回被代理对象的方法
    return method.invoke(bean, args);
  }
}

  1. 通过 Proxy 工具类的相关方法来创建动态代理对象并执行相关代理方法
package xyz.xiaolinz.demo.proxy.dynamicproxy.jdk;

import java.lang.reflect.Proxy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

/**
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/8/7
 */
@Component
@Slf4j
public class JdkProxyRunner implements ApplicationRunner {

  @Override
  public void run(ApplicationArguments args) throws Exception {
    // jdk动态代理主要通过 Proxy 类来完成
    /**
     * 1. classLoader: 类加载器,用于加载代理类,可以通过被代理类的class.getClassLoader()获取 2. interfaces:
     * 被代理类的接口,可以通过被代理类的class.getInterfaces()获取 3. invocationHandler: 代理类的调用处理器,需要实现
     * InvocationHandler 接口
     */
    final var classLoader = Thread.currentThread().getContextClassLoader();
    Class[] interfaces = new Class[] {DataQuery.class};
    final var cacheInvocationHandler = new CacheInvocationHandler(new DataBaseDataQuery());
    final var proxy =
        ((DataQuery) Proxy.newProxyInstance(classLoader, interfaces, cacheInvocationHandler));

    log.info(proxy.query("test"));

    log.info(proxy.query("test"));
  }
}

动态代理实现 - CGLIB 实现方式

基于 CGLIB 的动态代理需要使用 Enhancer 类和 MethodInterceptor 接口。
使用 CGLIB 的方式被代理类实现接口不是必须的,即与 JDK 的动态代理最大的不同点即是 jdk 强制需要接口而他不需要

  1. 定义被代理类,不需要实现接口
package xyz.xiaolinz.demo.proxy.dynamicproxy.cglib;

import lombok.extern.slf4j.Slf4j;

/**
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/8/4
 */
@Slf4j
public class DataBaseDataQuery {

  public String query(String queryKey) {
    // 使用数据源查询数据
    log.info("从数据库查询数据");
    return "query";
  }

  public String queryAll() {
    log.info("从数据库查询所有数据");
    return "queryAll";
  }
}
  1. 定义动态代理类,实现 MethodInterceptor 接口
package xyz.xiaolinz.demo.proxy.dynamicproxy.cglib;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

/**
 * 缓存方法拦截器
 *
 * <p>实现了 Spring 的 MethodInterceptor 接口,这个接口是 CGLIB 的方法拦截器,用于拦截被代理对象的方法。 该接口只需要实现 intercept
 * 方法,该方法会在被代理对象的方法被调用时调用。 intercept 方法的参数说明:
 *
 * <ul>
 *   <li>obj:被代理对象
 *   <li>method:被代理对象的方法
 *   <li>args:被代理对象的方法的参数
 *   <li>proxy:代理人
 *   <li>返回值:被代理对象的方法的返回值
 *   <li>throws Throwable:可投掷
 * </ul>
 *
 * @author huangmuhong
 * @date 2023/08/08
 * @see MethodInterceptor
 */
@Slf4j
public class CacheMethodInterceptor implements MethodInterceptor {

  private final Map<String, String> cache = new HashMap<>(10);

  /**
   * 截距
   *
   * @param obj 对象
   * @param method 方法
   * @param args 参数
   * @param proxy 代理人
   * @return {@link Object }
   * @author huangmuhong
   * @date 2023/08/08
   */
  @Override
  public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
      throws Throwable {
    // 我们只对 query 方法进行缓存
    if (method.getName().equals("query")) {
      if (cache.containsKey(args[0])) {
        log.info("从缓存中查询数据");
        return cache.get(args[0]);
      }
      // 从数据库查询数据
      final var result = proxy.invokeSuper(obj, args);
      // 将查询结果缓存
      cache.put((String) args[0], (String) result);
      return result;
    }

    // 调用默认的方法
    return proxy.invokeSuper(obj, args);
  }
}

  1. 通过 Enhancer 创建动态代理对象并调用
package xyz.xiaolinz.demo.proxy.dynamicproxy.cglib;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.stereotype.Component;

/**
 * @author huangmuhong
 * @date 2023/8/8
 */
@Component
public class CglibRunner implements ApplicationRunner {

  @Override
  public void run(ApplicationArguments args) throws Exception {
    // 创建被代理对象
    final var enhancer = new Enhancer();
    // cglib 跟 JDK 动态代理的不同之处在于,它是针对类实现代理的,原理是对指定的目标类生成一个子类,可以没有接口。
    // 我么这里的目标类是 DataBaseDataQuery,所以需要设置父类,且没有接口 CGLIB 也可以为我们创建动态代理的对象
    enhancer.setSuperclass(DataBaseDataQuery.class);
    // 设置方法拦截器
    enhancer.setCallback(new CacheMethodInterceptor());

    // 创建代理对象
    final var proxy = (DataBaseDataQuery) enhancer.create();

    // 调用代理对象的方法
    proxy.query("queryKey");
    proxy.query("queryKey");
    proxy.queryAll();
  }
}