ARTICLE DETAIL

资讯详情

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

Spring扩展点实战指南:从Bean生命周期到动态代理的五个高频接口

Spring扩展点实战指南:从Bean生命周期到动态代理的五个高频接口 Spring 扩展点这东西估计每一个做 Java 后端的人都在面试题里见过但真正在工作里把它用得行云流水的我敢说十个里面最多两个。很多同学对扩展点的理解停留在“哦BeanPostProcessor 可以在 Bean 初始化前后搞点事情”真到了要动手改造一个老项目或者要给团队封装一个基础组件的时候往往一脸懵我该用哪个接口注册进去有没有副作用为什么网上抄来的代码在项目里就是不生效这篇文章我就用自己在实际项目里趟过坑的经验把这些扩展点串起来讲透。不聊源码分析的八股文重点说清楚两件事每个扩展点是干什么活的、在什么场景下选它最合适。我会配合真实的落地案例和踩坑实录尽量让你看完就知道下一步该怎么写。适合刚接触 Spring 底层、准备啃源码的同学也适合那些已经写了三五年业务代码、想给团队沉淀点东西的工程师。1. 扩展点全景图先看清 Spring 到底给了你多少“后门”1.1 从 Bean 的一生理解扩展点的分布很多人觉得 Spring 扩展点抽象是因为没有把 Bean 的生命周期这条主线理清楚。我说的主线不是教科书上的那句话“实例化、属性填充、初始化、销毁”而是你得在心里有一幅图一个 Bean 从加载定义到最终被销毁中间经过了哪些站台每一站 Spring 都安排了什么样的“接车员”。这幅图想清楚了扩展点的逻辑自然就通了。Spring 容器启动时第一步是读取配置把 XML、注解、Java Config 里的信息解析成BeanDefinition。此时 Bean 还是图纸不是实物。图纸阶段你对它做什么都是安全的改构造函数参数、改初始化方法名、甚至直接改 Bean 的全限定名都来得及。BeanFactoryPostProcessor就站在图纸审核这个位置它的工作范围是容器级别可以拿到所有 BeanDefinition 进行批量调整。图纸通过审核之后Spring 才开始根据BeanDefinition反射创建实例。实例创建出来之后还没来得及设置属性InstantiationAwareBeanPostProcessor就跳出来拦一道。这个接口是BeanPostProcessor的强化版它能干很多“后置处理器”干不了的脏活比如返回一个自定义代理替换原来的 Bean。这个坑我在后面专门讲。属性填充完成后Bean 进入初始化阶段。此时 Spring 会依次调用BeanNameAware、BeanFactoryAware、ApplicationContextAware这类 Aware 回调接口往 Bean 里注入容器信息和依赖。紧接着就是BeanPostProcessor的两个核心时机postProcessBeforeInitialization和postProcessAfterInitialization。前者在PostConstruct和InitializingBean之前执行后者在它们之后执行。绝大多数 AOP 动态代理就是靠后者完成的。最后是销毁阶段DisposableBean、PreDestroy、SmartLifecycle各管一段。看到这里你会发现Spring 的扩展点不是散乱的几十个接口而是像接力赛一样分布在 Bean 一生的每个阶段。记住这条主线你以后看到一个不认识的后置处理器接口名马上就能判断它应该挂在哪一站。1.2 按用途分类哪些扩展点值得优先掌握如果你翻开 Spring 的接口清单会发现扩展点有几十个但真正高频使用的其实就是那么十几个。我自己习惯把它们分成四类分类的标准不是接口的包路径而是它在实际项目中扮演的角色。第一类是容器级扩展代表是BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor。它们的调用时机最早能操作的东西也最底层适合做包扫描路径增强、动态注册 Bean 定义、批量修改 Bean 的元信息这类基础设施能力。这一类扩展点的特点是你能碰到它的机会不多但碰上一次就是大活。第二类是 Bean 生命周期扩展代表是InstantiationAwareBeanPostProcessor、BeanPostProcessor、InitializingBean、DisposableBean。这是日常开发中出现频率最高的一组适合做统一的增强处理比如日志切面、字段脱敏、接口耗时监控以及各种“拿到 Bean 之后再包装一层”的需求。第三类是条件装配扩展代表是ImportSelector、ImportBeanDefinitionRegistrar、EnvironmentPostProcessor。这类扩展点是组件自动装配的核心技术实现方式团队开发基础框架、写 starter 的朋友必须熟练使用。你在项目里见过的大量EnableXxx注解本质上都是通过这一组扩展点来工作的。第四类是环境与生命周期回调代表是ApplicationListener、SmartLifecycle。它们适合处理应用启动后的资源加载、服务注册、缓存预热以及事件驱动的业务解耦。这四类扩展点覆盖了我在真实项目里遇到的绝大部分需求。优先级排序也很明确先啃第二类因为几乎每个项目都能用到再攻第三类因为它是写 starter 的必备技能第一类和第四类按需掌握。2. 五个高频扩展点在真实业务中的落地案例2.1 BeanPostProcessor用一张注解实现接口耗时与日志增强我在前一家公司接手过一个老系统是早年用 Spring MVC 写的一套内部运营平台。那时候的技术债欠得厉害几百个接口没有统一的日志输出线上出问题只能翻 Tomcat 的 access log 猜测链路。要改造它最不优雅的方式就是给每个方法手动加日志代码那工作量大到想想就头皮发麻。这时候BeanPostProcessor就成了最优解。思路很简单容器的每个 Bean 在初始化完成后会经过postProcessAfterInitialization我在这里对所有标注了ApiOperation注解的 Controller Bean 进行一次“包装”——生成动态代理在方法执行前后记录耗时、打印入参出参。代码实现其实不复杂。核心是一个BeanPostProcessor的实现类重写postProcessAfterInitialization方法判断当前 Bean 的类上是否包含目标注解。如果包含就用自己的一个ProxyFactory对该 Bean 创建代理在代理的invoke方法里先检测方法是否有ApiOperation注解有则记录开始时间执行目标方法然后打印耗时和结果。如果目标方法抛异常就在代理层统一捕获并记录错误日志。实际返回给容器的是这个代理对象业务代码里注入的还是原来的类型完全无感知。这个方案看起来顺理成章但实际落地时有一个特别容易踩坑的细节如果你创建的代理绕过了 Spring 的 AOP 机制会导致 Bean 上的事务注解失效。原因很简单事务本身也是靠BeanPostProcessor添加的另一个代理实现的。我后来解决的办法是自己创建代理时把目标 Bean 的Advisor列表也一并取出来添加到新的代理链中。这样既保留了原有的事务增强又能叠加自己的日志逻辑。从这以后我彻底想明白了一个道理BeanPostProcessor之间的顺序是完全不保证的你造一个代理出来如果不管容器里已有的增强链那业务行为大概率会被破坏。2.2 FactoryBean搞定第三方组件的复杂初始化逻辑FactoryBean是个很特殊的扩展点。我说它特殊是因为它的名字里带了个Factory但作用和 Spring 的BeanFactory完全不是一回事。它本身的含义是“一个生产 Bean 的工厂”。当容器发现一个 Bean 实现了FactoryBean接口时它不会直接返回这个 FactoryBean 的实例而是在需要注入的时候调用getObject()方法把返回结果作为最终的 Bean 暴露给外部。这个特性特别适合一个场景第三方 SDK 的连接初始化逻辑很复杂可能需要读取配置文件、初始化线程池、建立长连接但项目里每个地方都想要一个干净的客户端对象直接用而不是自己去 new。我有一次接了一个短信服务商的 SDK那家厂商的设计风格比较老派Client的构造函数需要传一堆参数内部还要异步启动一个消息推送线程。我第一版代码是在每个用到SmsClient的服务里手动初始化结果是每个服务都写了一段重复的初始化逻辑而且测试环境配置一改就得全局重新部署。后来我抽出了一个SmsClientFactoryBean把参数都改成从配置中心读取getObject()里完成连接初始化整个系统所有地方只需要注入SmsClient一个对象即可。这里有一个大家容易搞混的坑当你通过Autowired注入SmsClient时拿到的是getObject()返回的对象而不是 FactoryBean 本身。如果你确实需要 FactoryBean 实例得在注入点前面加一个符号写Autowired smsClientFactoryBean。这个设计初看很反直觉但实际上提供了很大的便利你可以在getObject()里做各种初始化逻辑但调用方完全不用关心。2.3 ImportSelector与ImportBeanDefinitionRegistrar打造私有组件域的自动装配Import注解在 Spring Boot 自动装配体系里重要性极高。很多EnableXXX类型的注解底层都是通过Import引入一个配置类再由这个配置类动态决定装配哪些 Bean。ImportSelector和ImportBeanDefinitionRegistrar就是在Import之后帮你实现“动态选择装配对象”的两种手段。我做过一个多租户权限组件当时遇到一个棘手的需求不同客户的部署包需要启用不同的认证方式有的客户用本地账号密码登录有的客户对接了企业的 CAS 单点登录。如果全部代码写死在一个模块里每次交付新客户都要改代码迟早出事故。我用ImportSelector解决了这个问题。实现方案是定义一个EnableTenantAuth注解里面有一个authType属性。ImportSelector的selectImports方法根据authType的值决定返回哪个配置类的全限定名。容器随后只会加载被返回的配置类从而实现运行时选择认证策略的效果。这个方案的关键在于selectImports方法的入参AnnotationMetadata它可以拿到EnableTenantAuth注解上的所有属性。也就是说你在注解上定义的任何配置项都能在运行时动态影响装配结果。这是写框架级代码的人必须掌握的姿势。相比ImportSelectorImportBeanDefinitionRegistrar更加底层它直接操作BeanDefinitionRegistry可以在这里完成对 Bean 定义的注册、修改、覆盖操作。我一般在需要注册的 Bean 定义里还带一些由外部配置决定的构造函数参数时选择用 Registrar 而不是 Selector。因为 Selector 返回的是配置类配置类的属性注入还要多绕一圈而 Registrar 直接注册 BeanDefinition可以通过GenericBeanDefinition的构造函数参数或属性值直接精确控制。2.4 ApplicationListener事件机制在业务解耦中的正确用法Spring 的事件机制是基于观察者模式实现的ApplicationListener监听特定类型的ApplicationEventApplicationEventPublisher负责发布事件。这听起来像是最小白的知识但实际项目里用得好的不多。我见过最普遍的错误是把事件机制当成 RPC 来用在发布事件的代码里顺手等待事件处理完成如果监听器逻辑很慢整个调用链路就被拖住了。其实EventListener注解上有一个不起眼的async属性设成 true 之后可以异步执行。更有经验的团队通常会配合Async配置一个专门的线程池把耗时的下游逻辑挪出去。我负责过的订单系统里有个比较经典的场景订单支付成功之后需要同时做三件事情——更新订单状态、通知库存系统扣减、给用户发送积分。如果把这三件事全写在支付回调的同一个事务里库存接口如果超时支付结果也返回失败这是绝对不能接受的。我的方案是在支付完成的核心代码里发布一个OrderPaidEvent三个监听器各自处理自己的业务通过Async的方式异步执行。事件大概率不会丢失因为支付回调本来就有重试机制会兜底重复发布。监听器侧做幂等控制即可。这里我总结了一条比较实用的经验事件监听器适合做“单机内的逻辑解耦”不适合做跨服务的数据一致性保证。如果订单服务和库存服务是独立部署的正确做法是走消息队列而不是靠 Spring 的事件跨应用传播。Spring 的事件机制只是在同一个 JVM 进程内做广播别拿它当分布式消息中间件用。2.5 BeanFactoryPostProcessor程序化修改Bean定义以实现批量增强BeanFactoryPostProcessor的最大特点是执行时机很早早到所有 Bean 都还没有实例化此时你拿到的仅仅是一堆BeanDefinition。如果你要批量干预某些 Bean 的属性配置、修改它们的构造参数或者想给某些类动态添加新的 BeanDefinition就靠它了。我遇到过这样一个项目一个老的电商后台所有的数据源配置分散在各个 XML 文件里每个环境都有自己的一套配置改动一次需要同时修改十几处。后来我们用配置中心替换了散落的配置文件但是历史遗留的 XML 仍然有用里面配置了大量 Bean。我没有去替换全部 XML而是写了一个BeanFactoryPostProcessor在容器启动时扫描容器中所有DataSource相关的 BeanDefinition把配置中心的参数覆盖进去。这样既保留了原有 XML 的结构又实现了配置的集中管理。整个改造只花了一天时间没有动任何业务代码。这个扩展点看起来跟业务开发没什么直接关系但解决这类“基础设施级的历史包袱”问题特别顺手。需要注意的一点是凡是要在 Bean 实例化前干预配置的需求都别想着用普通 Bean 去实现一定要注册成BeanFactoryPostProcessor因为普通 Bean 的实例化时机远远晚于此。3. 实操手把手实现一个生产级的扩展点3.1 场景设定与整体设计下面我挑一个综合一点的技术场景把前面几个扩展点串起来用一遍省得大家看完觉得每个点都会一上手就不知道怎么编排。假设我们团队正在做一个新的微服务里面有个非常常见的基础需求所有对外提供的 HTTP 接口必须要有统一的返回包装格式且接口执行过程中产生的异常不能直接抛给前端裸奔要有标准化的错误码。同时接口的耗时和调用方 IP 需要记录到日志系统。用扩展点的思路我的设计是分三层来做这件事。第一层用BeanPostProcessor对所有 Controller Bean 做包装统一处理异常和耗时。第二层用ImportSelector为主设计一个EnableStandardResponse注解让所有子服务在启动类上加这个注解就自动具备上述能力。第三层用ApplicationListenerApplicationEventPublisher把日志上报这样的非核心链路异步化避免影响接口本身的性能。这个设计的核心价值在于这套能力不是侵入式的。业务开发人员只要写自己的 Controller不需要继承任何基类不需要调用任何工具方法标准响应格式和异常处理框架自动完成。这就是面向扩展点设计组件和面向业务硬编码的最大区别。3.2 核心代码实现与关键参数说明我先定义目标注解用来标记需要被增强的 ControllerTarget(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface StandardController { }然后在BeanPostProcessor的postProcessAfterInitialization里实现代理逻辑Component public class StandardResponseBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean null) { return bean; } Class? clazz bean.getClass(); if (!clazz.isAnnotationPresent(StandardController.class)) { return bean; } ProxyFactory factory new ProxyFactory(bean); factory.addAdvice((MethodInterceptor) invocation - { try { Object result invocation.proceed(); return ResultWrapper.success(result); } catch (BizException ex) { return ResultWrapper.error(ex.getCode(), ex.getMessage()); } catch (Exception ex) { log.error(接口执行异常, ex); return ResultWrapper.error(ErrorCode.SYSTEM_EXCEPTION); } }); return factory.getProxy(bean.getClass().getClassLoader()); } }这里要啰嗦一句ProxyFactory来自 Spring AOP如果你选择的包路径不对会导致动态代理和事务增强失联。此外要把ResultWrapper.success(result)这类包装逻辑放在代理层而不是塞进业务代码这样业务方法可以继续返回原始类型框架自动完成包装。接下来是EnableStandardResponse注解和ImportSelector实现Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Import(StandardResponseImportSelector.class) public interface EnableStandardResponse { }public class StandardResponseImportSelector implements ImportSelector { Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{StandardResponseAutoConfiguration.class.getName()}; } }在StandardResponseAutoConfiguration里通过ComponentScan扫描到StandardResponseBeanPostProcessor从而完成自动装配。子服务只需要在自己的启动类上添加EnableStandardResponse注解整套能力就自动生效了。最后是异步日志上报的事件实现。我在代理层proceed完成之后发布一个ApiAccessEvent监听器使用EventListener加Async组合EventListener(ApiAccessEvent.class) Async(logExecutor) public void onApiAccess(ApiAccessEvent event) { logService.saveAccessLog(event.getPayload()); }在这个案例里要注意的一点是异步线程池必须显式定义好指定队列容量和拒绝策略。生产环境里如果线程池配置不当流量一大就会导致日志队列积压反而是灾难。我一般用ThreadPoolTaskExecutor核心线程数按 CPU 核数乘以 2 配置队列容量视 QPS 预估最关键的是拒绝策略选择CallerRunsPolicy这样即使线程池满了也只是调用线程自己扛不会丢失数据。3.3 注册方式与优先级控制的经典坑扩展点的注册方式看似简单但细节非常多。纯注解方式是用Component注册配置类方式是在Configuration类里用Bean方法返回后置处理器实例。我更推荐使用Bean方法注册因为它可以直接在该创建方法中进行类似setOrder之类的显式设置通过实现Ordered接口控制执行顺序。BeanPostProcessor之间的执行顺序没有严格规定一致性当多个后置处理器都打算创建代理时顺序问题会变得突出如果你的处理顺序不对可能会覆盖掉其他后置处理器生成的代理。我踩过一个真实的大坑一个 Bean 同时被两个BeanPostProcessor代理增强其中一个在另一个前面执行时没有把后者的Advisor汇入代理链导致后者的增强逻辑实际没有生效。最终排查花了两天结论是顺序和增强链合并这两者必须双管齐下。如果只是改顺序不合并增强链问题依然存在。3.4 自检清单扩展点开发完成后必须确认的事扩展点开发完除了功能验证我还会花 10 分钟做一次自检。首先是启动日志检查看看有没有 Bean 被代理的日志输出或异常堆栈中是否有重复代理的报错如果有说明增强链被叠加了多遍常见的原因是同一类型被两个扩展点重复增强。其次是事务边界检查万一动态代理出了问题事务注解静默失效是最危险的故障因为它不会报错只会表现为数据不一致。生产环境验证方式是写一个测试接口在业务代码里抛异常正常回滚说明事务还在。最后是上下文隔离检查在一个 Web 应用里ApplicationContext还在但如果你写的是 Spring Boot 的多个ApplicationContext同时存在的机制后置处理器的注册会互相干扰。需要确认每个上下文里的BeanPostProcessor不会误处理不该处理的 Bean。4. 扩展点的优先级、失效边界与相互影响4.1 Ordered接口在扩展点体系里的绝对地位前面提到了不止一次Ordered接口这里还是要单独拉出一节来强调因为这是扩展点体系里最容易被忽视、后果又最严重的机制。BeanPostProcessor如果有多个Spring 会按照Ordered返回的 order 值从小到大依次执行。注意这里是绝对排序不是相对排序所以如果你定义了一个 order 为 0 的后置处理器另一个为 1 的前者必然先执行。我推荐的做法是所有扩展点实现类都显式实现Ordered不要依赖默认顺序。特别是当你会写多个后置处理器时一定要分清楚谁应该先包装、谁应该后包装。排序的底层逻辑是先执行的后置处理器需要对后面还有机会修改的 Bean 施加影响而后执行的通常要覆盖范围更广的属性或者代理。4.2 失效场景为什么你写的扩展点没有生效做扩展点的人一定会遇到“代码写得对但就是不生效”的情况。我总结了几类常见失效原因供大家对照排查。一类是容器问题。如果自定义的BeanPostProcessor注册在一个子上下文中而需要被增强的 Bean 是父容器里的那它自然管不到父容器的 Bean。在传统 Spring MVC 项目里DispatcherServlet上下文和ContextLoaderListener上下文是两个容器很容易出现这种管错范围的情况。另一类是配置和类加载问题。ComponentScan扫描的包路径如果没有覆盖到后置处理器的实现类它就不会被注册自然也没有效果。还有一类是代理优先级问题目标 Bean 已经存在 JDK 动态代理而你用 CGLIB 强行转换类型会报错。排查这种问题我的习惯是加一个小技巧在后置处理器里打一行启动日志打印当前处理了哪些 Bean。这行日志能让你一眼确认处理器有没有被注册、执行顺序对不对。如果日志没出来就说明处理器本身没有被容器接纳如果出来了但没看到你预期的 Bean那就是范围或过滤条件的问题。这个排查效率远远高于在代码里加断点一轮一轮调试。4.3 多个扩展点叠加时的副作用处理多个扩展点叠加使用时副作用是常态最常见的两种是重复代理和死循环依赖。重复代理好理解一个 Bean 被 A 的后置处理器套了一层代理又被 B 的后置处理器再套一层代理多层代理链如果处理不当性能会打折类型强转还容易出问题。死循环依赖这个坑更隐蔽。假设某个BeanPostProcessor的实现依赖了另一个需要被增强的 Bean那么在容器初始化阶段就会触发循环依赖。解决方案是让后置处理器尽量不依赖业务 Bean只依赖于BeanFactory或Environment这类基础设施。实践时我还有一个心得能用一个后置处理器完成的事情不要拆成两个。如果必须拆就把后置处理器做成幂等设计即每个处理器都可以反复执行而不会造成副作用。这样即使顺序变化也不会出现重复处理或漏处理。5. 常见问题速查与排查思路实录5.1 扩展点执行期间常见的三类问题我梳理了过去几年项目里遇到的高频问题整理成了一张速查表方便大家对照定位。问题现象可能原因解决思路自定义后置处理器没有执行处理器本身未注册或被排除检查是否被Component扫描到检查配置类上是否开启了正确的Import或ComponentScanBean 上事务失效动态代理没有包含原有事务增强创建代理时将容器中的Advisor合并进代理链注意检查代理链顺序其他子服务没有生效扩展点写在父上下文使用ApplicationContext的层级或统一挂载在根容器下避免跨上下文访问启动变慢后置处理器做了过多反射或代理对目标类做缓存避免对每个 Bean 做无差异处理只处理目标注解标记的类这条表中的第三行在我经历过的项目里发生过一次典型情况。当时我们把一个公共组件封装成了 jar 包放进子项目子项目通过Import引入公共配置但公共配置里的BeanPostProcessor放在了一个与子项目ApplicationContext不是父子关系的上下文里导致组件并不实际生效。这个问题的核心是理解ApplicationContext的层级归属否则排查起来极其痛苦。5.2 从日志堆栈倒查扩展点顺序的方法遇到和扩展点相关的异常第一反应不要看业务代码去看完整堆栈。Spring 的启动日志会打印很多BeanPostProcessor相关的执行痕迹。如果你发现某一个异常线程来自于DynamicAdvisedInterceptor基本可以确定是 AOP 代理链的问题。我在排查一个接口耗时异常翻倍的问题时靠堆栈倒查发现了原因同一个 Bean 被 CGLIB 代理了两次导致每次请求方法调用都额外穿透了多层拦截器链性能损耗翻倍。顺着堆栈往上找能看到一层是自定义后置处理器构建的另一层是EnableTransactionManagement带来的事务代理。两条代理链没有合并于是出现了嵌套代理。解决之后我总结了一个经验如果项目里既要做事务管理又要做自定义代理增强可以用AbstractAutoProxyCreator的机制来对BeanPostProcessor做统一管理。或者更简单一点直接使用Aspect切面配合EnableAspectJAutoProxy完成增强把复杂代理逻辑全部收敛到 Spring AOP 框架内部而不是自己再造一层代理轮子。5.3 组件化思考让扩展点成为团队的通用资产最后一个视角想分享给读到这里的朋友。BeanPostProcessor这一类扩展点如果只停留在“我解决了一个问题”这个层次那就太可惜了。真正有复利效应的做法是把扩展点封装成组件沉淀到团队的公共依赖里。比如EnableStandardResponse这种设计在团队内部就是一个通用资产。新服务启动时加上注解就自动获得标准响应、异常包装、日志采集、耗时监控。这类组件的最大价值在于你的团队等于把工程能力从业务代码中剥离了出来业务开发的人只需要聚焦业务本身。封装组件的时候有一个关键的原则不要把自己的业务假设写死在组件里。比如标准响应包装的ResultWrapper不同团队对它字段的定义可能不一样我见过有些组件直接把ResultWrapper放在组件 jar 里导致不同服务之间无法做接口协议的统一升级。更好的做法是组件只提供机制不定义具体资源模型把真正跟格式相关的类留在业务服务内或者放到另一个专门的协议包中管理。这一点在跨团队协作时特别重要。我自己在项目里常用的套路是组件里只做扩展点的逻辑组装具体的ResultWrapper、BizException、ErrorCode这样和业务语义强绑定的模型尽量留在使用方。扩展点只是螺丝刀业务实体才是螺丝本身工具不要替业务做决定。写在最后的一个小体会Spring 扩展点学到最后很多人会发现技术本身不难难的是判断在什么时机用什么方案。BeanPostProcessor能干很多活但不是所有增强都适合用代理实现ImportSelector很好用但要是把整个项目搞成运行时动态装配后续人接手会非常痛苦。我自己做过几次之后最大的体会是扩展点是把双刃剑用对了是框架级优雅用错了是维护者的噩梦。落地时多想想这个扩展点在这个场景到底解决了一个什么问题这个机制在未来多久内会不会变化想清楚了再动手比对着源码抄一百行要值得多。
返回列表