☰
工厂模式 + JDK动态代理:构建运行期代理实现类的完整指南
2026/10/10 12:43:25 网站建设 项目流程

工厂模式创建动态代理实现类,这个组合在Java后端开发里出现的频率非常高,尤其是当你跟Spring、Dubbo这类框架打交道时。说白了,就是接口明明摆在那,但真正干活的实现类却不是在编译期写死的,而是在运行期通过动态代理“凭空造”出来,再交给工厂统一管理。标题里的“工厂模式”和“动态代理”合在一起,解决的也就是“代理实现类怎么来、怎么管理、怎么用”的问题。

这篇文章我打算从最基础的需求场景讲起,用一段完整可跑的示例代码,把JDK动态代理和工厂模式如何配合、每一步为什么这么写、踩过哪些坑,全部说清楚。不管你是刚接触动态代理的新手,还是已经在项目里用过但没深究过实现细节的开发者,都能从中找到可以“抄作业”的落地方案。

1. 为什么需要“工厂 + 动态代理”来创建实现类

1.1 接口本身不能直接new,实现类又不想写死

接口在Java里只是一个契约,它规定了方法签名,但没有方法体。你不可能直接写InterfaceB b = new InterfaceB(),这是语法错误,编译器直接不认。常规的做法是写一个实现类,比如AbcServiceImpl implements InterfaceB,然后在代码里new AbcServiceImpl()。

这在业务简单时完全没有问题。但当你面对的是这样的场景:接口的方法逻辑需要根据运行时配置动态变化、方法调用需要统一做鉴权或日志处理、实现类是在另一个系统里远程提供的(比如RPC框架),这时候你就没法提前写好一个固定的实现类了。你需要的是一段在运行期动态生成“符合InterfaceB接口契约”的代码——这就是动态代理的用武之地。

动态代理生成的并不是磁盘上的一个.class文件,而是运行时在内存中直接构造一个类,这个类实现了你指定的接口,所有接口方法都流转到同一个处理逻辑上。听起来很玄乎,但JDK原生就支持,只要接口设计合理,使用起来其实很轻量。

1.2 工厂模式在这里扮演的角色

动态代理本身能“生成”实现类,但谁来生成?生成完放在哪?调用方怎么获取?如果每处调用都自己去调Proxy.newProxyInstance(),那代码会变得非常散,代理的创建参数散落各处,以后想统一调整日志开关、切换Mock逻辑、给代理加缓存,就非常痛苦。

工厂模式正好解决这个问题。它的核心思想是:把对象的创建过程集中到一个地方,调用方只需要告诉工厂“我要一个InterfaceB的实现类”,工厂负责创建并返回,调用方完全不必关心这个类到底是手动写的实现类,还是动态代理生成的。这样你手里拿到的是一个标准接口引用,代理生成细节被封装得干干净净。

1.3 这个组合的典型业务场景

我举三个最常见的场景,你在实际项目里几乎躲不开:

  • 统一拦截:一个接口里十几个方法,每个方法调用前都要校验权限、打印日志、做埋点。你不想在每个实现方法里重复写这些代码,就让所有方法统一走一个InvocationHandler,在invoke()里做统一处理。
  • 远程调用:服务消费者端只有一个API接口,没有实现类,调用接口方法时需要把方法名、参数序列化后通过网络发给服务端。这时候动态代理生成一个本地假实现,方法体里干的就是“打包请求、发请求、解析响应”。
  • Mock测试:接口联调对方没准备好,或者你想模拟异常返回。用动态代理根据配置直接返回假数据,不需要真的写一堆Mock实现类。

这类需求共同点都是:接口是确定的,具体行为不在编译期确定,需要运行期统一接管。

2. JDK动态代理的核心原理拆解

2.1 动态代理的三个核心要素

JDK动态代理使用起来,只需要围绕三个东西转:

第一个是接口。代理类必须实现一个或多个接口。JDK动态代理的底层原理是:在运行时生成一个新的类,这个新类继承Proxy父类,同时实现你指定的全部接口。因为Java是单继承,但可以多个接口,所以Proxy.newProxyInstance()的第二个参数接受的是Class<?>[],也就是接口数组。

第二个是InvocationHandler。这是动态代理的“大脑”。代理类的所有接口方法被调用时,都会汇聚到InvocationHandler的invoke()方法里。你在这个方法里决定:真正执行什么逻辑、参数怎么处理、返回值怎么构造。

第三个是Proxy.newProxyInstance()。这是创建代理实例的入口。它接收三个参数:类加载器、接口数组、InvocationHandler。返回的对象已经实现了所有传入的接口,可以直接强转为接口类型来使用。

这里有一个非常反直觉的点,我早年刚接触时也栽过跟头:代理对象只能转成接口类型,不能转成一个具体的实现类类型。因为JDK动态代理动态生成的类是$Proxy0这样的名字,它跟你的业务实现类没有任何继承关系,你强转成业务类必然抛ClassCastException。所以这个方案的前提就是:通过接口来编程。

2.2newProxyInstance的三个参数怎么选

第一个参数类加载器,一般用目标接口的类加载器就行:

InterfaceB.class.getClassLoader()

你可能会疑惑为什么需要类加载器。因为代理类是要在运行时生成的,JVM得把这个新类加载进来。选哪个loader其实有讲究,原则是:用接口所在类的加载器,或者用当前上下文加载器,只要保证生成的代理类能被正确加载和访问即可。如果你用错了加载器,可能遇到ClassNotFoundException或者ClassCastException,比如代理类和接口由不同ClassLoader加载。

第二个参数接口数组,直接写接口的Class对象数组:

new Class<?>[]{InterfaceB.class}

第三个参数InvocationHandler,它一般是需要状态的,比如要传入真实目标对象、方法名到实际执行器的映射表、统一的横切逻辑等。工厂模式里,这个Handler往往由工厂统一装配。

2.3 为什么优先选JDK动态代理而不是CGLIB

你可能听过CGLIB也可以做代理,而且Spring里大量用到。两者最大的区别是:JDK动态代理要求目标必须有接口,它基于接口生成代理;CGLIB是通过生成目标类的子类来创建代理,不需要接口。

标题里明确说了是创建“实现类”,也就是针对接口操作,那么我用JDK动态代理是最合理的。理由有三条:

  • 接口是标准契约,代理和真实实现在接口层面完全对等,替换无感知。
  • 不需要引入额外的字节码库,JDK原生支持,代码更轻。
  • 现代JVM对接口调用的优化非常好,大多数场景下性能差距可以忽略。

如果你的目标是类而不是接口,JDK动态代理就不适用了,那才需要转CGLIB。但本篇文章的上下文是工厂模式创建“实现类”,所以JDK动态代理就是首选白月光。

3. 从零实现:工厂 + JDK动态代理的完整示例

3.1 目标结构说明

为了讲清楚整个模式,我模拟一个贴近真实业务的场景。假设有一个接口InterfaceB,里面定义了三个方法,其中一个还是带默认实现的。需求是这样的:这个接口的实现类不能直接写死业务逻辑,而是所有调用都进入一个统一的处理中枢,由处理中枢根据方法名分发逻辑;同时创建代理的过程由工厂统一负责,调用方完全感知不到动态代理的存在。

项目结构大致如下:

com.example.factory ├── InterfaceB.java // 目标接口 ├── InterfaceBInvocationHandler.java // 统一调用处理器 ├── ProxyFactory.java // 工厂,负责创建代理实现 └── Client.java // 调用方示例

3.2 接口定义

接口命名我直接用”InterfaceB“,别觉得这个名字奇怪,这跟标题里的“实现接口interfaceB的abc类代码”是同一个关注点,接口的干净程度直接影响动态代理的使用体验。

package com.example.factory; public interface InterfaceB { void doSomething(String param); String getResult(int code); default String getDefaultInfo() { return "this is default info"; } }

第三个default方法是个伏笔,后面我会讲它在动态代理里的表现。这里先留个印象:Java 8之后接口可以带默认实现,动态代理对它的处理方式跟普通抽象方法是不一样的。

3.3 编写InvocationHandler

这是整个动态代理的“心脏”。InvocationHandler接口只有一个方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable

三个参数的含义分别是:

  • proxy:生成的代理对象本身。注意,在invoke内部尽量不要用proxy调用它自己的方法,容易造成递归。
  • method:被调用的接口方法对应的Method对象。通过它可以拿到方法名、参数类型、返回类型,还可以调用真实目标。
  • args:调用方法时传入的参数列表,没有参数时是null。

我的处理逻辑就是在这个方法里做统一拦截。为了演示工厂模式的意义,我在Handler里加入了一个“真实目标对象”的概念:如果这个目标存在,就反射调用它的同名方法;如果不存在,就走一段模拟逻辑。这样,同一个Handler就可以支持两种模式:包装已有实例,或者是完全凭空生成逻辑。

package com.example.factory; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class InterfaceBInvocationHandler implements InvocationHandler { // 可选的真实目标对象,如果不为null,则优先调用它 private final Object target; public InterfaceBInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.currentTimeMillis(); try { // 统一前置处理 System.out.println("[MyHandler] 进入方法: " + method.getName()); if (args != null && args.length > 0) { System.out.println("[MyHandler] 参数: " + java.util.Arrays.toString(args)); } Object result; if (target != null) { // 有真实目标,反射调用 result = method.invoke(target, args); } else { // 没有真实目标,模拟实现逻辑 result = simulateMethod(method, args); } // 统一后置处理 System.out.println("[MyHandler] 方法执行完成: " + method.getName()); return result; } finally { System.out.println("[MyHandler] 耗时: " + (System.currentTimeMillis() - start) + " ms"); } } private Object simulateMethod(Method method, Object[] args) { String methodName = method.getName(); if ("doSomething".equals(methodName)) { System.out.println("[Simulated] doSomething 收到参数: " + (args != null ? args[0] : null)); return null; } if ("getResult".equals(methodName)) { int code = args != null && args.length > 0 ? (Integer) args[0] : 0; return "模拟结果: " + code * 2; } // 其他方法(比如default方法)返回null由代理自行处理 return null; } }

注意我在simulateMethod里对已知方法做了字符串判断分发。这在Method对象上做method.getName()匹配,只是简化演示。生产环境如果要做方法分发,建议用一个Map<Method, Function>把Method对象映射到对应的处理lambda上,避免大量字符串比较。后面我会讲到这个优化。

3.4 工厂:封装创建过程

工厂在这里做的事情非常纯粹:接收必要的参数,返回一个类型是InterfaceB的实例。调用方拿到这个实例后,以为它是一个普通实现类,实际上它是个代理。

package com.example.factory; import java.lang.reflect.Proxy; public class ProxyFactory { // 静态工厂,创建一个绑定真实目标的代理实现类 public static InterfaceB createWithTarget(Object target) { return (InterfaceB) Proxy.newProxyInstance( InterfaceB.class.getClassLoader(), new Class<?>[]{InterfaceB.class}, new InterfaceBInvocationHandler(target) ); } // 静态工厂,创建一个纯模拟逻辑的代理实现类 public static InterfaceB createSimulated() { return (InterfaceB) Proxy.newProxyInstance( InterfaceB.class.getClassLoader(), new Class<?>[]{InterfaceB.class}, new InterfaceBInvocationHandler(null) ); } }

这个工厂现在有两个方法:一个用于包装已有实例,一个用于生成纯模拟实现。调用方根本不需要知道InvocationHandler的存在:

package com.example.factory; public class Client { public static void main(String[] args) { // 模拟模式:没有任何真实业务实现类 InterfaceB simulated = ProxyFactory.createSimulated(); simulated.doSomething("hello factory"); System.out.println("getResult -> " + simulated.getResult(21)); // 包装模式:基于已有实现类做代理,可以穿插切面逻辑 InterfaceB raw = new InterfaceB() { @Override public void doSomething(String param) { System.out.println("真实实现执行: " + param); } @Override public String getResult(int code) { return "真实结果: " + code; } }; InterfaceB proxied = ProxyFactory.createWithTarget(raw); proxied.doSomething("hello proxy"); System.out.println("getResult2 -> " + proxied.getResult(5)); } }

运行这段代码,你能看到日志顺序是:前置打印、参数打印、真实/模拟逻辑执行、后置打印、耗时打印。整个调用流程完全被Handler接管了。

3.5 从“硬编码Handler”到“通用工厂”的演进思路

上面的代码足够跑通,但真正的项目里不会为每一个接口写一个专属Handler。你会很快发现,如果InterfaceC、InterfaceD都要这么处理,复制粘贴Handler代码会非常痛苦。

通用的做法有两个方向:一是让Handler通过构造器接收一个处理函数,把“该执行什么”这个决策延后到工厂层;二是利用Java 8的Function、BiFunction等函数式接口,把方法名到处理逻辑的映射表做成一个参数。

比如我可以把工厂改成这样:

public static InterfaceB createWithLogic(Map<String, MethodInvoker> logicMap) { InvocationHandler handler = (proxy, method, args) -> { if (logicMap.containsKey(method.getName())) { return logicMap.get(method.getName()).invoke(args); } return method.getDefaultValue(); }; return (InterfaceB) Proxy.newProxyInstance(...); }

这里MethodInvoker是你自定义的函数式接口:

@FunctionalInterface public interface MethodInvoker { Object invoke(Object[] args); }

这样一来,工厂就从“只能创建固定Handler的类”进化成了“根据外部配置创建不同行为代理”的调度中心。Spring里大量使用这种模式,比如FactoryBean、@Bean配合ProxyFactory,本质都是这个套路。工厂模式的精髓不在于类结构多复杂,而在于把“创建”这个动作集中,把“变化”隔离在参数和配置上。

3.6 工厂缓存的引入与Key设计

直接Proxy.newProxyInstance()每次都会生成一个新的代理实例,这在调用方高频获取的场景下会产生大量垃圾对象。工厂模式天然适合缓存代理实例,因为它把创建过程收口了,只要在工厂内部加一层缓存即可。

最简单的做法是使用ConcurrentHashMap,用接口Class作为Key:

private static final Map<Class<?>, Object> CACHE = new ConcurrentHashMap<>(); public static InterfaceB createCached() { return (InterfaceB) CACHE.computeIfAbsent(InterfaceB.class, clz -> Proxy.newProxyInstance(clz.getClassLoader(), new Class<?>[]{clz}, new InterfaceBInvocationHandler(null))); }

如果工厂面向多个接口,可以做成泛型方法:

public static <T> T createProxy(Class<T> interfaceType, InvocationHandler handler) { return (T) CACHE.computeIfAbsent(interfaceType, clz -> Proxy.newProxyInstance(clz.getClassLoader(), new Class<?>[]{clz}, handler)); }

但要特别注意,如果Handler里绑定了target对象,缓存Key就不能只用接口Class,否则多个不同target会互相覆盖。正确的做法是把target也纳入Key,比如用[接口Class, target对象]构建复合Key,或者干脆不缓存带target的代理。按“一个接口 + 一类上下文”的维度做缓存,才不容易出错。

4. 实战中的常见坑与排查记录

4.1 强转ClassCastException:代理对象不是你的实现类

这是我见过最多的报错。新手把Proxy.newProxyInstance()返回的对象强转成一个具体的实现类类型,比如:

AbcServiceImpl abc = (AbcServiceImpl) ProxyFactory.createWithTarget(new AbcServiceImpl());

必炸。原因上面说过了,JDK动态代理生成的是$Proxy0,它跟AbcServiceImpl没有半点继承关系。强制转换时JVM去检查类型兼容性,发现代理类和目标类毫无交集,直接抛异常。

正确姿势是面向接口:

InterfaceB b = ProxyFactory.createWithTarget(new AbcServiceImpl());

如果项目里大量出现这种问题,反思一下代码结构:是不是调用方明明拿接口就行,却偏要依赖实现类类型。

4.2 代理对象内部方法调用不会再次走代理

这个问题隐蔽得多。假设你让InterfaceB的getResult方法内部自己去调用doSomething,在代理模式下,getResult是走Handler的,但在Handler里反射调用target的同名方法时,target是一个普通实现类,它方法体里的doSomething调用是普通的Java内部调用,不会再次经过代理和Handler。

为什么?因为动态代理是“包了一层壳”,代理对象引用了target,但target内部对自己的调用是基于this的,根本不知道外面还有壳。所以你在Handler里做的鉴权、日志等统一处理,只能拦截外部对target的调用,拦截不到target内部的this调用。

解决方案:如果你确实需要内部调用也走拦截,要么从Handler里把代理对象传进去(在很多框架里叫AopContext.currentProxy()),要么把内部调用改成通过外部传入的接口引用执行。Spring AOP里有一个著名的坑就是“自调用不走代理”,本质就是这个原因。

4.3 default方法在代理里表现奇怪

接口里定义default方法时,动态代理调用它,默认情况下不会走你Handler里写的方法分发逻辑。因为default方法是有方法体的,JDK动态代理在处理时,会直接调用接口的默认实现,而不是把default方法当作抽象方法转发给InvocationHandler。

这就导致一个现象:你的Handler日志里可能看不到default方法的打印。如果想拦截default方法,需要用到InvocationHandler里的特殊处理分支,在method.isDefault()为true时,你可以调用MethodHandles.Lookup去反射执行默认方法体,或者自己在这个分支里写业务逻辑。但通常我的建议是别在接口里放复杂的default方法配合动态代理,容易把问题搞复杂。接口保持纯粹的方法契约,业务默认逻辑放实现类或工具类里,动态代理的世界会清净很多。

4.4 代理创建太多导致MetaSpace膨胀

每次Proxy.newProxyInstance()都会在运行时生成一个新的类,这个类要被ClassLoader加载进JVM。如果你在一个大循环里创建几千个代理对象,就会生成几千个不同的代理Class,它们都进入Metaspace,不会被轻易回收。尤其当类加载器是自定义且不回收时,这个膨胀是致命的。

我之前排查过一个稍微偏门的线上问题,某个报表服务内存上涨,dump下来一查,Metaspace里有几万个$Proxy类。原因就是某个工具类每次调用都直接newProxyInstance,而且类加载器一直持有引用。解决方式上下已经写了,工厂里加缓存,用接口Class做Key,保证同一个接口只有一份代理类。

判断是否为这个问题,可以在JVM参数里加-XX:TraceClassLoading,日志里会刷出大量$Proxy类加载记录。一旦发现,优先考虑缓存方案,别去改JVM参数硬抗。

4.5invoke里抛异常:包装与透传的取舍

Handler的invoke方法签名声明了throws Throwable,你可以在里面捕获所有异常做统一处理,也可以直接往上抛。但我提醒一点:如果method.invoke(target, args)反射调用真实方法时抛了业务异常,反射会把原始异常包装成InvocationTargetException。如果你直接捕获Exception,拿到的是外层包装,而不是业务异常本身。

正确做法是:

try { return method.invoke(target, args); } catch (InvocationTargetException e) { throw e.getCause(); // 把真正的原因抛出去 }

我见过有人直接e.printStackTrace()然后返回null,接口方法在外部收到null后整个逻辑错乱。动态代理里的异常处理一定要设计清楚策略:是统一封装成自己的异常类型,还是把原始异常透传出去。推荐是透传,这样对调用方最友好。

4.6 工厂模式与Spring容器结合的最佳实践

如果项目用了Spring,工厂模式创建动态代理实现类还有一种更优雅的玩法:把工厂方法定义成@Bean,或者用FactoryBean接口。

@Configuration public class ProxyConfig { @Bean public InterfaceB interfaceBProxy() { return ProxyFactory.createSimulated(); } }

这样其他Service里直接@Autowired注入InterfaceB就行,完全不知道也不关心它是不是代理。Spring的自动装配机制看到@Bean方法返回接口类型,会把这个代理对象注册到容器,大家和谐共处。

我实际项目里用得比较多的是在网关服务中:一堆上游渠道的API接口,每个接口都需要统一加签名、鉴权、日志。我用工厂模式把所有渠道的代理对象集中创建,渠道标识作为工厂参数的Key,每次新增渠道只加配置,不改调用方代码。框架层级的整洁感就是这么来的。

5. 再多说两句:这套方案的关键价值

写到这里,我觉得有必要聊聊除了技术实现之外的东西。工厂模式和动态代理组合,它的真正价值是“把变化收敛到一个点”。接口代表稳定契约,动态代理代表运行期的灵活行为,工厂代表统一的创建出口。

这三个组合到一起,你基本不需要为每个需求的稳定变化去大量新增实现类了。新增一个接口方法,Handler里加一个分支配对;调整统一逻辑,只改Handler;替换真实实现,只动工厂的装配逻辑。调用方像被保护在象牙塔里,永远只跟接口打交道。

当然它也有代价。动态代理的代码可读性比直接写实现类要差,调用链上多了一层Handler,排查问题时要多跳一步,IDE跳转会跳到Handler里的通用代码,看不到具体业务逻辑。所以我的态度一贯是:业务简单就直接写实现类,别炫技;业务确实有大量横切逻辑、或者实现类确实无法在编译期确定时,再用这套组合拳。

我个人在实际操作中还有一个体会:工厂+动态代理的样板代码很容易被提取成公共组件,但提取时一定要谨慎,千万不要为了通用而把所有Handler都合并成一个巨型Handler。每个业务域还是要有自己的Handler,哪怕它内部只是调用通用组件。否则所有代理对象都堆在一个类里,方法名的字符串判断写个几百行,那时候想拆都拆不动。

最后再分享一个小技巧:如果你用工厂模式创建了代理对象,但想在测试里验证代理逻辑是否生效,可以用一个简单的断言——检查返回的对象类名是否包含$Proxy。这个方法有点土,但在单元测试里快速判断确实很方便。

assertTrue(proxyObj.getClass().getName().contains("$Proxy"));

这条烂招我用了好几年,屡试不爽。代码这个领域,很多时候最简单直接的办法反而最可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询