【代理模式】静态代理&动态代理原理分析

【代理模式】实现方法增强

【一】需求描述

【1】目标

(1)顶层接口 IDatabaseTemplate,包含各类数据库 CRUD 方法;

(2)对接口所有方法做增强:方法执行前后记录耗时、打印入参 / 返回值 / 耗时日志;

(3)分别实现三种代理:静态代理、JDK 动态代理、CGLIB 动态代理;对比三者实现、优缺点、适用场景。

【2】基础接口 & 实现类

(1)顶层接口 IDatabaseTemplate

java 复制代码
import java.util.List;
import java.util.Map;

public interface IDatabaseTemplate {
    List<Map<String, Object>> queryList(String sql, Object... params);
    Map<String, Object> queryOne(String sql, Object... params);
    int executeUpdate(String sql, Object... params);
}

(2)原始实现类 DefaultDatabaseTemplate(被代理目标对象)

java 复制代码
import java.util.List;
import java.util.Map;

public class DefaultDatabaseTemplate implements IDatabaseTemplate {
    @Override
    public List<Map<String, Object>> queryList(String sql, Object... params) {
        // 模拟数据库查询逻辑
        sleepRandomMs();
        return null;
    }

    @Override
    public Map<String, Object> queryOne(String sql, Object... params) {
        sleepRandomMs();
        return null;
    }

    @Override
    public int executeUpdate(String sql, Object... params) {
        sleepRandomMs();
        return 1;
    }

    private void sleepRandomMs() {
        try {
            Thread.sleep((long) (Math.random() * 200));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

(3)统一日志增强工具(三种代理共用耗时打印逻辑)

java 复制代码
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;

@Slf4j
public class DatabaseLogUtil {
    public static Object doInvoke(String methodName, Object[] args, Runnable before, Callable<Object> targetMethod) throws Exception {
        before.run();
        long start = System.currentTimeMillis();
        Object result = null;
        try {
            result = targetMethod.call();
            long cost = System.currentTimeMillis() - start;
            log.info("【数据库操作正常】方法:{},入参:{},耗时:{}ms,返回结果:{}",
                    methodName, Arrays.toString(args), cost, result);
            return result;
        } catch (Exception e) {
            long cost = System.currentTimeMillis() - start;
            log.error("【数据库操作异常】方法:{},入参:{},耗时:{}ms,异常信息:{}",
                    methodName, Arrays.toString(args), cost, e.getMessage(), e);
            throw e;
        }
    }
}

【二】实现方案

【1】方案 1:静态代理实现

(1)静态代理类 DatabaseTemplateStaticProxy

必须实现和目标相同的接口 IDatabaseTemplate,持有目标对象,手动重写每一个接口方法,在每个方法内写增强逻辑。

java 复制代码
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;

@Slf4j
public class DatabaseTemplateStaticProxy implements IDatabaseTemplate {
    // 持有被代理的真实对象
    private final IDatabaseTemplate target;

    // 构造注入目标实现
    public DatabaseTemplateStaticProxy(IDatabaseTemplate target) {
        this.target = target;
    }

    @Override
    public List<Map<String, Object>> queryList(String sql, Object... params) {
        try {
            return (List<Map<String, Object>>) DatabaseLogUtil.doInvoke(
                    "queryList", new Object[]{sql, params},
                    () -> log.debug("开始执行queryList查询"),
                    () -> target.queryList(sql, params)
            );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Map<String, Object> queryOne(String sql, Object... params) {
        try {
            return (Map<String, Object>) DatabaseLogUtil.doInvoke(
                    "queryOne", new Object[]{sql, params},
                    () -> log.debug("开始执行queryOne查询"),
                    () -> target.queryOne(sql, params)
            );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public int executeUpdate(String sql, Object... params) {
        try {
            return (Integer) DatabaseLogUtil.doInvoke(
                    "executeUpdate", new Object[]{sql, params},
                    () -> log.debug("开始执行executeUpdate更新"),
                    () -> target.executeUpdate(sql, params)
            );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

(2)使用方式

java 复制代码
// 1. 创建原始实现
IDatabaseTemplate realTarget = new DefaultDatabaseTemplate();
// 2. 创建静态代理包装对象
IDatabaseTemplate proxy = new DatabaseTemplateStaticProxy(realTarget);
// 3. 调用代理方法,自动打印耗时日志
proxy.queryOne("select * from table where id = ?", 1001);

(3)静态代理 优缺点

(1)优点

1-实现简单,无任何第三方依赖,JDK 原生支持;

2-编译期即可校验接口方法,方法写错直接编译报错,排错简单;

3-执行无反射开销,性能最高。

(2)缺点(致命短板)

1-接口新增方法必须同步修改代理类:如果IDatabaseTemplate新增batchInsert(),代理类必须手动新增对应实现,代码维护爆炸;

2-接口方法越多,代理类代码冗余严重,大量重复增强逻辑;

3-只能代理实现了同一接口的类,无法代理没有接口的普通类。

【2】方案 2:JDK 动态代理(接口代理,Spring AOP 默认首选)

核心:基于接口动态生成代理类,不需要手动写代理实现类,通过InvocationHandler统一拦截所有接口方法,一处统一写增强逻辑。

(1)统一拦截处理器 DatabaseInvocationHandler

java 复制代码
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

@Slf4j
public class DatabaseInvocationHandler implements InvocationHandler {
    // 被代理的真实目标对象
    private final Object target;

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

    // 所有接口方法执行都会进入该统一拦截方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return DatabaseLogUtil.doInvoke(
                method.getName(), args,
                () -> log.debug("开始执行数据库方法:{}", method.getName()),
                () -> method.invoke(target, args)
        );
    }
}

(2)代理工厂,快速生成代理对象

java 复制代码
import java.lang.reflect.Proxy;

public class JdkProxyFactory {
    @SuppressWarnings("unchecked")
    public static <T> T getProxy(T target) {
        Class<?> targetCls = target.getClass();
        // JDK动态代理要求:目标类必须实现至少一个接口
        return (T) Proxy.newProxyInstance(
                targetCls.getClassLoader(),
                targetCls.getInterfaces(),
                new DatabaseInvocationHandler(target)
        );
    }
}

(3)使用方式

java 复制代码
// 原始目标
IDatabaseTemplate realTarget = new DefaultDatabaseTemplate();
// 动态生成代理对象,不需要手动写代理类
IDatabaseTemplate proxy = JdkProxyFactory.getProxy(realTarget);
// 任意接口方法都会被统一拦截,自动打印耗时日志
proxy.queryList("select * from dim_table where dt = ?", "2026-06-29");

(4)JDK 动态代理 优缺点

(1)优点

1-只写一次拦截逻辑,接口新增方法无需修改代理代码,自动拦截所有接口方法;

2-无重复冗余代码,统一增强逻辑,易维护;

3-JDK 原生,无需引入第三方 Jar 包。

(2)缺点

1-强制限制:只能代理实现了接口的类;如果目标类没有接口,无法使用;

2-底层依赖反射执行方法,相比静态代理有微小性能损耗(业务系统完全可忽略);

3-无法代理类的普通非接口方法(如目标类自己写的私有 / 公共非接口方法)。

(5)真实使用案例

java 复制代码
@Slf4j
public class DatabaseTemplateWrapper implements InvocationHandler {
    private IDatabaseTemplate databaseTemplate;

    public DatabaseTemplateWrapper(IDatabaseTemplate databaseTemplate) {
        this.databaseTemplate = databaseTemplate;
    }

    public static IDatabaseTemplate createProxy(IDatabaseTemplate target) {
        return (IDatabaseTemplate) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{IDatabaseTemplate.class},
                new DatabaseTemplateWrapper(target));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        long start = System.currentTimeMillis();
        try {
            Object result = method.invoke(databaseTemplate, args);
            return result;
        } catch (Exception e) {
            throw e.getCause();
        } finally {
            log.info("数据库访问插件,执行耗时:{} 毫秒", (System.currentTimeMillis() - start));
        }
    }
java 复制代码
        // 生成数据模板
        DatabaseClassLoader databaseClassLoader = getDatabaseClassLoader(datasourcePluginConfig, dataSource);
        IDatabaseTemplate iDatabaseTemplate = getDatabaseTemplate(databaseClassLoader, connectProperties, poolProperties);
        if (EngineConfUtil.getBoolVal(ConfItem.插件执行SQL耗时追踪)) {
            return new Tuple2<>(dataSource.getTepltCd(), DatabaseTemplateWrapper.createProxy(iDatabaseTemplate));
        } else {
            return new Tuple2<>(dataSource.getTepltCd(), iDatabaseTemplate);
        }

【3】方案 3:CGLIB 动态代理(无接口类也能代理)

核心:底层通过字节码生成目标类的子类,重写父类所有非 final 方法实现拦截,不需要依赖接口,支持纯普通类代理。

(1)引入 Maven 依赖(Spring 内置已集成,单独使用需引入)

xml 复制代码
<!-- SpringBoot项目无需手动引入,spring-core自带cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

(2)CGLIB 方法拦截器 DatabaseMethodInterceptor

java 复制代码
import lombok.extern.slf4j.Slf4j;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

@Slf4j
public class DatabaseMethodInterceptor implements MethodInterceptor {
    private final Object target;

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

    // 拦截目标类所有非final方法
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        return DatabaseLogUtil.doInvoke(
                method.getName(), args,
                () -> log.debug("CGLIB拦截数据库方法:{}", method.getName()),
                () -> proxy.invokeSuper(target, args)
        );
    }
}

(3)CGLIB 代理工厂

java 复制代码
import net.sf.cglib.proxy.Enhancer;

public class CglibProxyFactory {
    @SuppressWarnings("unchecked")
    public static <T> T getProxy(T target) {
        Enhancer enhancer = new Enhancer();
        // 设置父类(目标类,无需接口)
        enhancer.setSuperclass(target.getClass());
        // 设置方法拦截器
        enhancer.setCallback(new DatabaseMethodInterceptor(target));
        // 生成子类代理对象
        return (T) enhancer.create();
    }
}

(4)使用方式(两种场景都支持)

(1)场景 1:目标有接口(你的 IDatabaseTemplate)

java 复制代码
DefaultDatabaseTemplate realTarget = new DefaultDatabaseTemplate();
// 代理返回目标类子类对象,可强转IDatabaseTemplate接口
IDatabaseTemplate proxy = (IDatabaseTemplate) CglibProxyFactory.getProxy(realTarget);
proxy.executeUpdate("insert into test values(?)", 100);

(2)场景 2:目标无任何接口(纯普通类,JDK 代理做不到)

java 复制代码
// 假设没有接口的普通数据库工具类
class SimpleDbUtil {
    public void select() {}
}
SimpleDbUtil proxy = CglibProxyFactory.getProxy(new SimpleDbUtil());

(5)CGLIB 代理 优缺点

(1)优点

1-不强制要求目标实现接口,任意普通类都可以代理;

2-拦截目标类全部非 final 方法,不管是接口实现方法还是类自有方法;

3-Spring AOP 默认自动切换:有接口用 JDK,无接口自动用 CGLIB。

(2)缺点

1-底层字节码生成子类,无法代理被 final 修饰的类 / 方法;因为final 方法无法被子类重写,无法拦截增强;

2-依赖第三方 cglib 字节码框架,JDK 原生不自带;

3-生成子类字节码有少量创建开销,频繁创建代理对象性能不如 JDK;

4-必须有无参构造函数,无无参构造会抛异常;构造方法会多执行一次(字节码创建子类时默认调用父类无参构造)。

【4】选型建议

使用场景:基于接口IDatabaseTemplate做数据库方法增强

(1)优先推荐:JDK 动态代理

1-面向标准接口开发,无第三方依赖,代码易维护;

2-新增数据库方法无需改动代理拦截代码,完全适配后续迭代;

(2)不推荐静态代理:接口一旦新增方法,代理类必须同步修改,长期维护成本爆炸;

(3)CGLIB 备选:如果后续有不需要接口的数据库工具类,再使用 CGLIB 统一增强;

(4)Spring 项目可直接使用 AOP 封装 JDK 动态代理,底层自动完成代理创建,无需手动写工厂。

【三】动态代理分析

【1】JDK动态代理为什么只能代理实现了接口的类?

(1)JDK Proxy 生成的代理类 固定实现目标接口

java.lang.reflect.Proxy 在运行时会动态生成一个全新的 Java 字节码类,这个代理类有两个硬性设计约束:

(1)代理类固定继承父类:java.lang.reflect.Proxy

Java 语法规定:一个类只能有一个直接父类(单继承)。

生成的代理类已经硬编码继承了Proxy,就再也不能继承你的业务目标类(比如DefaultDatabaseTemplate)。

(2)想要具备和目标对象一模一样的方法,只能让动态代理类去实现目标对象的所有接口。

例如,目标类如下:

java 复制代码
public class DefaultDatabaseTemplate implements IDatabaseTemplate {
    @Override
    public List<Map> queryList(String sql, Object... params) { ... }
}

JDK 动态代理自动生成的代理类伪代码:

java 复制代码
// 强制继承Proxy,单继承限制导致无法继承DefaultDatabaseTemplate
public final class $Proxy0 extends Proxy implements IDatabaseTemplate {
    private InvocationHandler h;

    // 构造传入InvocationHandler
    public $Proxy0(InvocationHandler h) {
        super(h);
        this.h = h;
    }

    // 实现接口所有方法,统一转发给invoke
    @Override
    public List<Map> queryList(String sql, Object... params) {
        Object[] args = new Object[]{sql, params};
        return (List<Map>) h.invoke(this, 接口方法对象, args);
    }
}

关键矛盾:

$Proxy0 extends Proxy 已经占用唯一父类名额,不可能再写 extends DefaultDatabaseTemplate。

想要拥有 queryList/queryOne 这些数据库方法,唯一方案就是实现 IDatabaseTemplate 接口。

(2)设计定位:JDK 动态代理初衷是「面向接口代理」

Java 原生反射模块设计Proxy的初衷:做标准的接口门面、远程 RPC、标准接口拦截,遵循面向接口编程思想:

(1)接口定义行为规范,实现类是内部细节;

(2)代理只关心「接口定义的行为」,不关心具体实现子类;

(3)如果允许代理普通无接口类,会破坏面向接口的设计思想,同时增加字节码实现复杂度。

(3)方法拦截的实现逻辑:只拦截接口声明的方法

(1)动态生成的$Proxy0只会重写接口中定义的方法;

(2)目标类里不属于接口的自有方法(普通 public 方法、protected 方法),代理类不会生成对应重写逻辑;

(3)如果目标没有接口,Proxy 无法知道需要生成哪些方法,直接无法创建代理类。

对比 CGLIB:

CGLIB 不走接口,使用 ASM 字节码框架生成目标类的子类,重写父类所有非 final 方法,不受单继承限制,所以不需要接口。

(4)总结原因

(1)Java 是单继承(extend)语言,JDK 动态代理生成的代理类已经固定继承了java.lang.reflect.Proxy类,无法再继承业务实现类;

(2)要想获取并增强业务实现类的方法,要么使用extend继承父类,要么使用implements实现接口,现在继承的路子走不通了,就只能使用implements实现接口了,所以要求只能代理实现了接口的类。

【2】JDK动态代理为什么不能代理私有 / 公共非接口方法?

根据上一个问题,JDK动态代理只能使用implements来获取代理类的方法,而implements代理的特性限制了只能获取public的方法。

【3】JDK动态代理和CGLIB动态代理的核心区别?

JDK动态代理受限于JAVA是单继承的语言特性,所以只能代理实现接口的类的公共方法。

而 CGLIB 基于 ASM 字节码框架,直接读取父类 Class 字节码,运行时动态生成一个目标业务类的子类作为代理对象,直接继承父类即可获得全部方法,不需要借助接口定义行为,可以直接扫描并重写父类所有非 final 方法,所以不需要业务类必须实现接口

【四】spring源码中代理模式设计的案例

Spring 核心两大场景大量使用代理:AOP 切面增强、IoC 容器动态包装 Bean,同时内置多种代理实现

Spring 两套代理底层封装:

(1)JdkDynamicAopProxy:JDK 动态代理,目标实现接口时默认使用;

(2)CglibAopProxy:CGLIB 字节码代理,无接口 / 强制 CGLIB 时使用;

统一对外门面:AopProxy 顶层接口,统一 getProxy() 获取代理对象。

【1】核心案例 1:Spring AOP 切面代理

(1)场景说明

给 Bean 方法做增强:日志、耗时、事务、权限、异常捕获,底层全部靠动态代理包装目标 Bean。

(2)源码流程

(1)容器创建 Bean 后,走 AbstractAutoProxyCreator(如 AnnotationAwareAspectJAutoProxyCreator)后置处理器;

(2)wrapIfNecessary() 判断是否需要切面增强;

(3)有切面逻辑时,创建 AopProxy 实现类:

1-目标类存在接口 → JdkDynamicAopProxy(JDK 动态代理);

2-目标无接口 / 配置proxy-target-class=true → CglibAopProxy(CGLIB 代理);

(4)返回代理对象放入单例池,业务注入拿到的是代理而非原始对象。

(3)关键类

(1)顶层统一接口:org.springframework.aop.framework.AopProxy

(2)JDK 代理实现:JdkDynamicAopProxy,内部持有 InvocationHandler 实现 AopInvocationHandler;

(3)CGLIB 代理实现:CglibAopProxy,内部持有 MethodInterceptor 实现 DynamicAdvisedInterceptor;

(4)入口生成器:DefaultAopProxyFactory,自动判断选用哪种代理。

【2】核心案例 2:Spring 声明式事务 @Transactional

(1)实现原理

@Transactional 依靠 AOP 代理实现事务拦截,本质就是上面的 AOP 代理。

(1)开启事务注解后,自动注册 BeanFactoryTransactionAttributeSourceAdvisor 切面;

(2)容器生成 Service 代理对象;

(3)调用带有@Transactional的方法时,代理拦截器先开启事务、方法执行后提交 / 回滚。

(2)关键现象印证代理特性

(1)同一个类内 this.xxx() 调用事务方法失效:this 是原始对象,不是代理对象,无法触发拦截;

(2)接口 Service 默认 JDK 代理,仅拦截接口定义方法;

(3)配置 proxy-target-class=true 强制 CGLIB 代理,可拦截类自有方法。

【3】案例 3:Spring BeanFactory 工厂代理、FactoryBean 代理包装

FactoryBean 代理包装(动态代理)

FactoryBean 可以返回代理对象而非原始目标 Bean,典型场景:Mybatis MapperFactoryBean

Mybatis Mapper 只有 Mapper 接口,无实现类:

(1)MapperFactoryBean#getObject() 内部使用 JDK 动态代理生成 Mapper 接口代理;

(2)调用 Mapper 接口方法时,代理拦截器转换为 Mybatis JDBC 查询。

和 Feign 思路完全一致:纯接口靠 JDK 动态代理实现逻辑。

【4】案例 4:CGLIB 代理专用场景:无接口类代理、Spring Boot 强制 CGLIB

(1)SpringBoot 2.x 后默认开启 proxy-target-class=true,全局优先 CGLIB 代理;

(2)当目标类没有实现任何接口时,Spring 自动切换 CGLIB;

(3)Configuration 配置类内部@Bean依赖调用:使用 CGLIB 代理拦截方法,保证单例 Bean 不会重复创建。

经典坑:@Configuration 类普通方法调用重复实例化

依靠 CGLIB 代理拦截方法,先从容器拿 Bean,而非直接 new,是 CGLIB 代理典型落地。

【5】总结

(1)Spring AOP 是动态代理集大成者,内置两套 JDK/CGLIB 动态代理自动切换;

(2)RPC、Mybatis Mapper 纯接口场景统一使用JDK 动态代理;

(3)数据源事务感知包装等固定增强使用静态代理;

(4)无接口类、配置类依赖拦截使用CGLIB 动态代理。