
工厂模式创建动态代理实现类这个组合在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()匹配只是简化演示。生产环境如果要做方法分发建议用一个MapMethod, 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(MapString, 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作为Keyprivate static final MapClass?, 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(ClassT 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));这条烂招我用了好几年屡试不爽。代码这个领域很多时候最简单直接的办法反而最可靠。