十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java动态代理实战:本地接口优雅代理远程HTTP调用

Java动态代理实战:本地接口优雅代理远程HTTP调用 1. 为什么我要用代理模式包装远程调用大概两年前我在接手一个老项目时遇到了一个非常典型的场景系统本身是单体架构但订单数据在另一个独立的订单中心服务里数据库不共享只能通过HTTP接口取数。业务方希望本地模块像调用本地Service一样去调用订单中心的接口不关心底层走的是HTTP还是RPC。当时的代码写得很直接到处都是restTemplate.getForObject(...)参数拼接、异常处理、超时配置散落在几十个service里面改一个接口要动半层楼的文件。代理模式就是在这种背景下被重新捡起来的。它的核心作用一句话就能讲清楚在不修改目标对象代码的前提下通过一个代理对象控制对目标对象的访问。放到“本地接口代理远程接口的调用”这个场景里代理对象就是本地接口的“门面”用户代码只依赖本地定义的接口类型真正的方法调用会被代理拦截转发为一次远程请求再把结果反序列化返回到本地。这带来的最直接价值有三个第一业务代码彻底解耦不再感知远程协议的存在第二所有横切逻辑参数校验、鉴权、超时、重试、日志可以收敛到一个代理处理器里统一实现第三后续如果订单中心升级成gRPC或者迁移到别的服务本地接口不变只替换代理实现就行。本文适合已经理解Java基础、会Spring Boot的读者重点讲JDK动态代理的完整落地思路不依赖任何重量级框架。我会把接口定义、远程调用封装、代理工厂、异常处理、排查手段全部讲透最后再聊聊这个模式在微服务场景下的扩展价值。2. 代理模式的整体设计与技术选型2.1 为什么非要“本地接口代理远程接口”大多数Java开发者第一次接触代理模式都是从AOP开始的Spring用JDK动态代理或者CGLIB生成代理类在目标方法执行前后织入事务、日志等逻辑。但“本地接口代理远程接口”和AOP有本质区别AOP代理的是本地已有的Bean方法真实存在而远程接口代理中本地接口背后根本没有一个实际实现类代理对象收到的每个方法调用都要通过网络传输才能在远端找到对应的执行逻辑。这个区别决定了设计上的几个关键选择。接口必须定义得足够“干净”方法名、参数类型、返回值类型就是远程HTTP接口的“契约”。比如本地接口里写一个OrderInfo getOrderById(String orderId)对应远程接口/api/order/{orderId}接口的语义要自解释不能让调用方去猜。同时接口不能出现本地对象特有而远端无法序列化的类型比如直接依赖内部的DataSource或者某个不可序列化的实体否则代理层根本没法把参数传给远端。另外代理对象的职责边界要清晰。它只做“调用转发”不做业务处理业务参数组装、返回值转换由接口定义和远端DTO负责通用的横切逻辑超时、重试、熔断放在InvocationHandler里。我见过有同事把业务校验也塞进代理里结果代理类越写越肥最后完全丧失了代理“透明性”的意义。2.2 静态代理和动态代理怎么取舍代理模式本身有静态代理和动态代理两条路线很多人一开始容易在这上面犹豫。先说静态代理手动写一个类实现同一个接口内部持有远程客户端的引用把接口方法一个个转发出去。代码不多理解简单但问题也很突出——每加一个远程接口就要写一个对应的代理类方法多了以后重复代码爆炸而且代理逻辑只能针对当前接口定制可复用性极差。动态代理的意义在于“只写一次代理逻辑运行时为任意接口生成代理对象”。我用JDK内置的Proxy.newProxyInstance来实现只要传入接口Class、InvocationHandler实例就能在内存里生成一个实现该接口的代理类代理对象每次方法调用都会进入invoke方法由我决定怎么处理。相比静态代理动态代理的代码量少一个数量级而且代理逻辑天然和具体接口解耦本质上是“面向接口的通用远程调用框架”。至于为什么没选CGLIB或者字节码增强原因很简单我只需要代理接口不需要代理类。JDK动态代理原生支持接口代理零额外依赖性能也够用CGLIB是为无法改接口的类设计的这里用不上。如果你要代理的具体对象是类而非接口或者需要更细粒度的字节码控制另说。2.3 远程调用底层该选什么协议本地接口代理远程接口最终总要把方法调用转化成一次远程通信。我优先推荐HTTPJSON。理由很现实跨语言友好调试方便直接 curl 看一眼序列化格式直观。项目里只要引入一个轻量HTTP客户端就行我实际用的是Spring自带RestTemplate也能换成OkHttp或HttpClient。另一种选择是RPC框架比如Dubbo或gRPC。它们性能确实更好自带了负载均衡、服务发现、链路追踪但如果你的目标是“用JDK动态代理做一层薄封装”来分享原理和快速落地RPC框架反而掩盖了代理模式的底层逻辑。而且RPC框架通常需要额外的注册中心、协议定义、代码生成在小团队或者中台化不彻底的项目里增加的成本往往超过收益。我的建议是先利用HTTP动态代理把这条链路跑通等并发量上来了再考虑替换底层通信方式——代理模式的妙处就在于替换通信方式时本地接口可以完全不变。3. 核心实现从接口定义到代理调用3.1 定义一个“干净的”本地接口第一步很关键定义接口时要把这个接口当作“本地API”来设计而不是直接照搬远端接口的URL。一个典型的订单服务接口长这样public interface OrderService { OrderInfo getOrderById(String orderId); boolean updateOrderStatus(String orderId, int status); }这里的OrderInfo是一个与远端DTO对应的本地模型字段名和远端接口的JSON字段保持一致或者通过注解映射。接口里千万不要出现和远程协议相关的东西比如URL、超时时间、请求头这些属于代理层的配置不应该暴露给业务调用方。设计接口时还要考虑方法重载的情况。JDK动态代理的invoke方法里能拿到Method对象即使方法名相同、参数不同也能区分但如果走的是“URL路由映射”模式方法名参数列表需要能唯一对应到一个远端接口。所以我会在实现时定义一套简单的映射规则接口名.方法名对应到远端/{serviceName}/{methodName}参数按顺序拼到URL里。如果远端接口风格不统一可以在每个方法上加自定义注解注解里写清楚对应路径。3.2 写一个可复用的远程客户端远程客户端负责真正的HTTP通信。这一层我不建议直接在InvocationHandler里写HTTP请求而是单独封装一个类理由有两点一是HTTP细节连接池、超时、SSL不该和代理逻辑混在一起二是方便单独做单元测试。以RestTemplate为例简单的客户端长这样public class RemoteClient { private RestTemplate restTemplate; private String baseUrl; public RemoteClient(String baseUrl) { this.baseUrl baseUrl; this.restTemplate new RestTemplate(); } public String get(String path, MapString, Object params) { String url buildUrl(path, params); ResponseEntityString response restTemplate.getForEntity(url, String.class); return response.getBody(); } public String post(String path, Object requestBody) { String url baseUrl path; ResponseEntityString response restTemplate.postForEntity(url, requestBody, String.class); return response.getBody(); } private String buildUrl(String path, MapString, Object params) { StringBuilder sb new StringBuilder(baseUrl).append(path); if (params ! null !params.isEmpty()) { sb.append(?); params.forEach((k, v) - sb.append(k).append().append(v).append()); sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } }这里有几个细节容易踩坑。第一是URL编码参数值里有中文或者特殊字符时必须URLEncoder.encode否则服务端很可能拿到乱码。第二是超时时间RestTemplate默认不设置超时的话一旦远端服务假死本地线程会一直卡住这是线上事故的常见原因后面我会单独讲。第三是序列化如果返回值直接是String再手动转JSON通用性最强如果偏爱强类型可以让RestTemplate直接反序列化成对象但InvocationHandler里需要根据返回类型做转换复杂度会高一些。3.3 InvocationHandler把方法调用翻译成远程请求这是整个代理模式的核心。每个方法的调用都会进入invoke方法我需要在里面完成“请求参数提取”、“远程调用”、“结果转换”、“异常处理”四件事。一个简化的实现如下public class RemoteInvocationHandler implements InvocationHandler { private RemoteClient remoteClient; private String serviceName; private ObjectMapper objectMapper; public RemoteInvocationHandler(RemoteClient remoteClient, String serviceName) { this.remoteClient remoteClient; this.serviceName serviceName; this.objectMapper new ObjectMapper(); } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass() Object.class) { // Object类的方法toString、hashCode、equals直接本地处理 return handleObjectMethod(proxy, method, args); } String methodName method.getName(); String path / serviceName / methodName; if (args ! null args.length 1) { // 单参数按POST发送请求体为参数对象 String json objectMapper.writeValueAsString(args[0]); String response remoteClient.post(path, json); return convertResult(response, method.getReturnType()); } else if (args ! null args.length 1) { // 多参数按GET方式拼到URL上 MapString, Object params new HashMap(); for (int i 0; i args.length; i) { params.put(arg i, args[i]); } String response remoteClient.get(path, params); return convertResult(response, method.getReturnType()); } else { String response remoteClient.post(path, null); return convertResult(response, method.getReturnType()); } } // ...convertResult, handleObjectMethod等 }这个版本用了最简单粗暴的参数处理策略单参数走POST、多参数走GET拼接。convertResult里根据方法的返回值类型决定是把JSON字符串转换成具体对象还是返回原始字符串。核心思路是让InvocationHandler完全“面向接口方法”工作不关心具体业务。从实际落地来看参数策略最好固定成项目里统一的一种规范。我后来在重构时把单对象参数统一要求成DTO方法签名尽量保持“一个参数”或“无参数”避免多参数拼接URL带来的繁琐和容易出错。如果你要承载复杂的查询场景可以考虑用Map类型做入参代理层直接透传。3.4 Proxy工厂怎么拿到可用的代理对象Proxy.newProxyInstance的使用不算复杂但有几个前提必须处理好。最常见的问题就是接口类型不符合代理要求比如不是接口、是接口但被final修饰、或者ClassLoader无法加载代理类。针对这些我在工厂里加了前置校验和较为清晰的异常文案public class RemoteProxyFactory { public static T T create(ClassT interfaceType, String baseUrl) { validateInterface(interfaceType); RemoteClient remoteClient new RemoteClient(baseUrl); String serviceName interfaceType.getSimpleName(); RemoteInvocationHandler handler new RemoteInvocationHandler(remoteClient, serviceName); Object proxy Proxy.newProxyInstance( interfaceType.getClassLoader(), new Class?[]{interfaceType}, handler ); return interfaceType.cast(proxy); } private static void validateInterface(Class? interfaceType) { if (interfaceType null) { throw new IllegalArgumentException(interfaceType must not be null); } if (!interfaceType.isInterface()) { throw new IllegalArgumentException(target type must be an interface); } if (interfaceType.getInterfaces().length ! 0) { // 注意这里其实就是非主接口的约束比如不能继承其他接口 // 实际上Java接口是可以继承的这里如果业务上限定可以自行选择是否限制 } } }实际使用的时候OrderService orderService RemoteProxyFactory.create(OrderService.class, http://order-center:8080); OrderInfo order orderService.getOrderById(20240501);业务方拿到OrderService后完全无感知底层是HTTP调用就好像在注入一个本地Bean。这也是代理模式这个场景最让人舒服的地方——接入方不需要知道“代理”的存在他们只看到接口、调方法、拿结果。3.5 把代理对象交给Spring管理在上面这个例子里RemoteProxyFactory.create每次都是手动创建如何和Spring整合呢我常用的方式是利用FactoryBean声明一个OrderServiceFactoryBeanSpring在启动时会调用getObject()返回代理对象。这样业务代码可以直接用Autowired注入接口不需要手动写任何创建逻辑。Component public class OrderServiceFactoryBean implements FactoryBeanOrderService { Value(${remote.order.base-url}) private String baseUrl; Override public OrderService getObject() { return RemoteProxyFactory.create(OrderService.class, baseUrl); } Override public Class? getObjectType() { return OrderService.class; } Override public boolean isSingleton() { return true; } }如果你服务的接口比较多还可以写一个自动扫描器扫描包下所有RemoteService注解的接口批量注册FactoryBean这样新加一个远程服务只需要写接口和加注解代理逻辑完全不用动。这个方向再往前走一步就是类似FeignClient的框架雏形了。4. 代理调用链路的完整实操4.1 定义一个完整的调用流程案例为了让你看得更明白我以“查询订单并更新状态”这个完整业务为例从接口定义到业务调用串一遍。第一步定义本地接口public interface OrderService { OrderInfo getOrderById(String orderId); boolean updateOrderStatus(String orderId, int status); }第二步定义与远程JSON对应的DTOpublic class OrderInfo { private String orderId; private String customerName; private int status; private BigDecimal amount; // getter/setter省略 }第三步通过Proxy工厂生成代理对象。这里的baseUrl可以配置在application.yml里remote.order.base-urlhttp://localhost:8081第四步写业务代码调用RestController public class OrderController { Autowired private OrderService orderService; GetMapping(/order/{orderId}) public OrderInfo getOrder(PathVariable String orderId) { return orderService.getOrderById(orderId); } PostMapping(/order/{orderId}/status) public boolean updateStatus(PathVariable String orderId, RequestParam int status) { return orderService.updateOrderStatus(orderId, status); } }业务层完全不知道OrderService是代理对象这一点正是代理模式想要的效果。4.2 参数解析和序列化的实战细节参数解析是整个链路里最容易出错又最容易被忽略的环节。单参数对象走POST时要把对象序列化成JSON字符串再发出去此时要注意序列化框架的选择。我用的是Jackson它有两点需要注意第一属性里如果是LocalDateTime默认序列化结果是ISO字符串如果远端用的Fastjson可能解析不了。我一般会在DTO的字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)统一格式。第二遇到循环引用或者懒加载对象时序列化可能直接抛异常。拒绝在DTO里放任何ORM实体或代理对象DTO就应该是纯粹的数据载体这也是我设计接口时很看重的一点。再说返回值的处理。getOrderById返回的是OrderInfo代理层拿到远端的JSON字符串后要反序列化成OrderInfoprivate Object convertResult(String response, Class? returnType) throws JsonProcessingException { if (returnType void.class || returnType Void.class) { return null; } if (returnType String.class) { return response; } return objectMapper.readValue(response, returnType); }如果你的返回值有范型比如ListOrderInfo用单纯Class是不够的需要构建TypeReference这部分我会在常见问题里专门说。4.3 通用横切能力超时、重试与日志代理模式最划算的地方在于横切逻辑可以集中到一个Handler里。前面RemoteClient很简单实际项目中我至少会加三项能力超时是第一个必须处理的。RestTemplate默认是不限制超时的必须用HttpComponentsClientHttpRequestFactory设置连接超时、读取超时Bean public RestTemplate restTemplate() { HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(10000); return new RestTemplate(factory); }重试是第二个。网络抖动、对端偶发超时重试一次往往就能救回来。但重试要有前提只对幂等请求重试。读操作无所谓写操作如果方法语义不是幂等的盲目重试可能造成重复下单、重复扣款。所以我会在接口方法上定义注解AutoRetry(maxAttempts 3)代理层只对有该注解且抛出超时异常或连接异常的方法做重试。日志是第三个。代理层打的日志能准确记录“哪个接口方法、耗时多少、远端路径、参数摘要、返回值摘要”这是排查线上问题的主要抓手。但因为入参和返回值中可能都有敏感字段我会开启脱敏逻辑把手机号、身份证字段打码后再打印。4.4 实际运行效果与调试手段启动Spring Boot项目配上远端订单中心用Mock接口模拟即可调用getOrderById代理层会发生这样几步动作生成代理对象实现了OrderService接口。业务线程调用getOrderById进入RemoteInvocationHandler.invoke。根据方法名构造路径/OrderService/getOrderById。单参数序列化String orderId为JSON。通过RemoteClientPOST到http://localhost:8081/OrderService/getOrderById。拿到响应字符串反序列化成OrderInfo返回给业务方。调试时我一般在代理层开debug日志确认路径、参数、响应状态是否正常。还有一种非常实用的调试手段在测试环境写一个简单的HTTP工具类先直接调远端接口确认远端没有问题时再排查代理层可以避免代理层逻辑和远端问题相互混淆。5. 常见问题与排查技巧实录5.1 代理对象创建失败报ClassCastExceptionProxy.newProxyInstance返回的是Object必须强转为接口类型。如果传入的interfaceType不是接口或者ClassLoader选得不对运行时会报ClassCastException或IllegalArgumentException。排查经验先在validateInterface里打日志确认接口的isInterface()是否为true再检查interfaceType.getClassLoader()是否和调用方一致。Spring Boot里一般不会冲突但如果你在自定义类加载器环境里做插件化开发这点会很敏感。5.2 泛型返回类型丢失反序列化报错比如接口方法定义成ListOrderInfo listOrders(Query query)代理层拿method.getReturnType()只能得到List.class没法知道里面是OrderInfo。反序列化时如果直接readValue(response, List.class)得到的其实是ListLinkedHashMap业务层一取字段就报错。解决办法是用Method.getGenericReturnType()获取Type再构建JavaTypeJavaType javaType objectMapper.getTypeFactory() .constructType(method.getGenericReturnType()); Object result objectMapper.readValue(response, javaType);这个方法同样适用于MapString, OrderInfo这种复杂泛型。只要远端返回结构稳定基本不会出错。5.3 远端抛出的业务异常变成了InvocationTargetExceptionJDK动态代理有个特点invoke方法里如果抛出一个非 RuntimeException 的受检异常代理层会把它包装成UndeclaredThrowableException或者InvocationTargetException业务层直接catch不到原始异常。我通常会在接口定义一个统一的业务异常比如RemoteServiceException在invoke里捕获所有异常转成RemoteServiceException并携带远端的状态码和错误信息。这样业务层只需catch一种异常排查信息也能拿全try { // ...远程调用 } catch (RemoteServiceException e) { throw e; } catch (Exception e) { throw new RemoteServiceException(远程调用失败: path, e); }5.4 方法名和URL映射不直观维护困难当接口多了以后完全靠方法名拼URL可能造成歧义或难以排查问题。我的建议是用自定义注解把路径、请求方式显式声明出来RemotePath(value /api/order/get, method HttpMethod.GET) OrderInfo getOrderById(String orderId);代理层优先读取注解没有注解才走约定好的规则。这样接口的“语义”和“远端端点”显式绑定后续人员接手不用猜。5.5 调试利器打印动态生成的代理类JDK动态代理生成的代理类是在运行时生成的默认不落盘所以很难直接看它长什么样。如果怀疑代理对象行为不对可以在启动参数里加-Dsun.misc.ProxyGenerator.saveGeneratedFilestrueJava会把$Proxy0这类类文件写出来你能看到它实现了哪些方法、每个方法如何调用invoke。我自己排过一次“代理对象没有走handler”的问题就是靠这个参数确认了代理类确实实现了接口方法问题出在别处。5.6 常见问题速查表现象可能原因处理方式创建代理对象直接异常目标类型不是接口校验isInterface()调用方法无响应线程卡住未设置HTTP超时时间设置连接和读取超时反序列化返回泛型类型错误只拿到returnType没有泛型信息使用getGenericReturnType业务层catch不到远端异常受检异常被包装统一转成自定义异常抛出日志里看到重复请求重试逻辑误用在非幂等接口只给幂等接口加重试注解中文参数变乱码URL未做编码参数拼接前URLEncoder.encode代理对象变成nullSpring注入失败检查FactoryBean是否注册成功6. 代理模式在微服务架构下的扩展价值虽然我前面写的代码很简单但这一点想专门展开说这个模式的思想在微服务和分布式系统里到处都在用。你看Spring Cloud里的FeignClient本质就是一个带动态代理的远程调用框架——你定义接口框架自动生成代理代理帮你做负载均衡、超时重试、熔断降级。再看Dubbo的DubboReference也是本地接口代理远程接口只不过底层协议从HTTP换成了自定义RPC协议。如果你理解了本文的RemoteProxyFactory再去看那些框架的源码会发现思路高度一致只是把InvocationHandler里的逻辑换成了更复杂的路由、容错、追踪机制。这也是我把这个模式写成博文的目的不是单纯教你写一个玩具而是想让你摸到这些重量级框架的底层骨架。如果后续要落地得更完善我建议朝这几个方向扩展一是服务发现。代理工厂创建时写死baseUrl只能用于内部测试真实环境地址应该来自注册中心每次调用前动态获取实例列表。二是容错策略。可以在代理层集成熔断器比如Resilience4j某个远端服务失败率过高时快速失败而不是让请求一直卡到超时。三是链路追踪。在代理层生成一个traceId追加到HTTP头远端服务也把这个traceId打到日志里就能把一个请求的完整调用链串起来。排查问题的时候从入口日志追到出口日志效率高很多。四是更细粒度的权限控制。代理层可以检查当前用户是否有权限调用这个接口避免把内部接口完全暴露给上游。这些能力加进来以后这个“本地接口代理远程接口”的代码就不再是简单的Demo而是一个轻量级的服务治理组件。它比直接引入一个全家桶RPC框架更可控、更轻量也更适合用在中小型项目的内部集成场景。7. 一个容易被忽略的小技巧代理对象不要反复创建不少人在Spring里直接把RemoteProxyFactory.create(...)写在service方法里调用每次请求都生成一个新代理对象。虽然JDK动态代理的创建开销不大但完全没有必要。代理对象是和接口、baseUrl一一对应的不会因为方法调用不同而变化它本质上是无状态的真正的状态都在InvocationHandler里。所以我建议把代理对象缓存起来。在工厂里加一个ConcurrentHashMapkey是接口Class加baseUrlvalue是代理对象或者像上文那样直接用Spring的FactoryBean让Spring容器保证单例。这个细节虽然不影响功能但在高并发下能省去不必要的反射开销和类加载过程属于那种“不明显但值得做”的优化。另一个小技巧是在InvocationHandler里把Method级别的元数据缓存起来比如方法对应的路径、请求方式、参数类型列表避免每次调用都重新反射解析。方法少的时候无所谓接口达到几十个、调用量再上来以后这个优化收益还是挺明显的。缓存可以用MapMethod, MethodMetadata在handler初始化时一次性构建。我在实际项目中还遇到过一个问题同一个接口被多个模块依赖不同模块想指向不同的远端环境比如联调环境、预发布环境。此时代理对象的缓存key就要把“环境标识”也带上否则A模块切换到预发布环境会破坏B模块到联调环境的连接。如果baseUrl是可变的最好在每次创建代理时把baseUrl显式传入而不是让代理对象持有全局配置。8. 一些我自己踩过的坑提前帮你避掉代理模式本身不复杂但真正放到项目里细节才是魔鬼。我把自己踩过的几个坑总结出来希望你能少走弯路。第一个坑是“接口方法无参但有返回值”的情况。我最早写的invoke逻辑里如果args为null就直接返回null导致所有无参方法返回了空值业务层拿到一个非null但内部都是null的对象看起来一坨莫名其妙。后来我明确了规则无参方法也要发请求返回值正常反序列化只是请求体为空。第二个坑是“Object类方法被代理”的问题。老实的代理对象即使是代理也逃不掉equals、hashCode、toString会被调用的宿命。如果你不做特殊处理这三个方法会被转发到远端接口前端打个日志都可能触发一次请求浪费连接不说远端接口可能根本不存在。我的处理方式非常明确invoke开头判断method.getDeclaringClass() Object.class是的话走本地处理不发起远程调用。第三个坑是“多个远端服务使用同一个DTO”造成的模型冲突。A服务的OrderInfo有status字段B服务的OrderInfo没有两个服务都映射到同一个Java类B服务返回JSON里多出的字段会被忽略缺失字段则变成null。这类问题最难查因为不会报错只会出现某些字段有问题。现在我要求每个远程服务的DTO单独建包哪怕字段一样也各自建互不影响。第四个坑是“远端接口响应结构不统一”。有的远端接口成功时直接返回业务数据失败时返回类似{code:500,msg:error}的结构。如果代理层只反序列化业务数据失败时就会因字段匹配不上得到一堆null等于把错误吞掉了。我建议远端契约里统一包一层代理层先判断code是否为0不是0则抛异常再反序列化真正的业务数据。第五个坑是“测试环境没有远端服务”导致本地联调无法进行。如果代理层是唯一的调用入口没有远端服务时本地项目可能启动都失败。我会在InvocationHandler里加一个“本地回退”开关配置了mocktrue时直接从本地Map里返回预设数据不发起真实调用这样本地开发不会阻塞在依赖上。这些坑如果不在前期设计时规避等到系统大了以后再改牵一发而动全身。代理模式就像一条高速公路设计得好流量跑得又快又稳设计得糙每一辆车都可能出问题而这辆“车”就是你的业务请求。
返回列表