ARTICLE DETAIL

资讯详情

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

动态代理Bean注入Spring容器:原理、实战与踩坑

动态代理Bean注入Spring容器:原理、实战与踩坑 做 Java 开发这么多年动态代理和 Spring 容器这两个东西分开看不难但一旦要“在容器运行过程中动态生成一个代理对象再把它注入到 Spring 容器里”就特别容易踩坑。我最近在给一个老项目做接口监控增强时正好把这个流程完整趟了一遍从最初的Bean手动返回代理对象到后来用FactoryBeanBeanDefinitionRegistryPostProcessor实现全自动注册中间翻过源码、排过循环依赖、还差点被事务注解坑到怀疑人生。今天把这套方案连同底层原理一起拆开讲清楚希望对需要做动态代理 Bean 注入的朋友有点帮助。如果你之前只写过Aspect注解切面或者只在测试代码里用Proxy.newProxyInstance()创建过代理那这篇文章能帮你把这两件事串起来动态代理负责生成增强对象Spring 容器负责管理这个增强对象的生命周期。全文以一个完整的“动态代理 Bean 注册”实战为主线覆盖 JDK 动态代理与 CGLIB 的选型、四种注入方式、Bean 生命周期源码级解析、常见踩坑与排查方法代码可以直接抄走改改就能用。1. 什么时候需要动态代理 Bean三个我遇到过的真实场景1.1 最典型的场景给一批接口统一做性能监控我这次的真实需求是这样的老项目里有几十个 Service 接口分布在不同的包里业务代码没有统一日志埋点。领导要求上线前给所有 Service 加上耗时监控和异常告警但不能大规模改动业务代码。第一反应当然是写一个Aspect切面定义切点匹配所有 Service。但问题是这些 Service 本身内部直接new了实现类没有完全走 Spring 注入一部分是静态方法调用还有几个是历史遗留的单例手工管理。这种情况下切面只能代理从容器里拿出来的 Bean对于手工 new 出来的对象完全无能为力。这时候动态代理 Bean 就有了用武之地写一个通用的ServiceProxyFactory在 Spring 容器扫描完 Bean 定义之后、创建 Bean 实例的过程中动态拦截所有符合条件的 Service生成代理对象再把代理对象注册回容器。业务方即使拿着旧的 Bean 名称去getBean()拿到的也是增强后的代理。1.2 动态代理 Bean 和普通 AOP 的本质区别我们要区分两件事。Spring AOP 做的也是动态代理但它是“在 Bean 实例化之后通过BeanPostProcessor包装一层代理返回”。而我们要讲的“动态代理 Bean 注入”是“在容器运行时主动创建出一个新的代理对象然后将这个代理对象注册为一个新的 Bean”。换句话说普通的 AOP 是“对已有的 Bean 做增强”动态代理 Bean 是“凭空生出一个新的增强型 Bean 放进容器”。这个区别直接影响了实现方式一个靠注解和切点声明式实现一个靠编码和容器扩展点手动实现。1.3 还有哪些场景会用到这个能力除了统一监控我遇到过需要动态代理 Bean 注入的场景还有这些接口 Mock在测试环境启动时动态为某些远程接口生成返回假数据的代理 Bean生产环境则不用加载通过配置开关控制。动态数据源切换根据路由键为每个数据源生成代理 Mapper动态决定走哪个库。灰度发布在运行时根据用户标识将请求转发到新老逻辑的代理 Bean 上不用停机切换。自定义 RPC 框架在消费端通过动态代理生成远程服务的本地调用对象典型的就是 Feign、Dubbo 的Reference注入。这些场景的共同点都是在编译期无法确定 Bean 的完整实现只有在运行时才能通过规则、配置或动态逻辑来生成代理对象。2. 选型前必须想清楚JDK 动态代理和 CGLIB 到底用哪个2.1 两者底层原理的差异动态代理 Bean 的核心是“代理对象从哪来”而 Java 生态里最常用的代理生成方式就是 JDK 动态代理和 CGLIB。很多新人搞不清我直接说人话。JDK 动态代理从 JDK 1.3 开始就有它要求目标对象必须实现至少一个接口。它生成的代理对象和目标对象实现了同样的接口但代理类本身是一个独立的类不属于目标类的子类。外部调用时代理对象会把方法调用转发给InvocationHandler.invoke()。它只认接口所以如果你要代理的是一个没有实现任何接口的类JDK 动态代理直接无能为力。CGLIB 则走的是另一条路它直接生成目标类的子类覆写非 final 的方法在覆写方法里插入增强逻辑。所以 CGLIB 不需要目标类实现接口但也带来了一个限制——目标类和方法都不能是final的否则无法覆写。2.2 从方法调用链路看区别JDK 动态代理生成代理类之后调用过程是这样的// 用户调用 - 代理类同名方法 - InvocationHandler.invoke() - 目标类方法CGLIB 的调用链是// 用户调用 - 代理子类覆写方法 - MethodInterceptor.intercept() - FastClass 直接调用目标方法特别注意 CGLIB 的这条链路它用了 FastClass 机制通过生成索引直接跳到目标方法字节码位置反射开销比 JDK 动态代理要小。这也是 CGLIB 在 JDK 17 之前高频调用场景下略占优势的原因。但随着 JDK 自身反射机制的不断优化这个性能差异在大部分业务场景里已经可以忽略。2.3 怎么选记住这几个判断标准选择不是看谁“更好”而是看你的场景适配哪种。我用了这么多年总结的标准很简单待代理对象有接口优先用 JDK 动态代理。这是 Spring 的默认策略相关类生成逻辑更成熟排查问题资料也多。没有接口或者需要代理toString()、equals()这类 Object 方法JDK 动态代理不会代理它们就用 CGLIB。目标类是final的两种都没戏只能手动包装或者用 ByteBuddy 这种字节码库硬生成。高并发 高频调用实测两者差异不大但 CGLIB 创建代理对象时由于需要生成字节码文件首次加载时间明显更长。Spring 容器里的EnableAspectJAutoProxy(proxyTargetClass true)全局开启 CGLIB默认则是 JDK 代理优先。但在我们这篇文章讲的手动注册场景里代理方式完全自己决定不用受全局配置约束。3. Spring 容器里注入动态代理 Bean 的四种写法3.1 入门写法在 Configuration 里 Bean 方法返回代理对象最简单的方式直接在配置类里写一个工厂方法Configuration public class ProxyConfig { Bean(userServiceProxy) public UserService userServiceProxy() { UserService target new UserServiceImpl(); InvocationHandler handler (proxy, method, args) - { long start System.currentTimeMillis(); Object result method.invoke(target, args); System.out.println(方法 method.getName() 耗时 (System.currentTimeMillis() - start) ms); return result; }; return (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); } }这是最直白的动态代理 Bean 注入方式容器创建userServiceProxy这个 Bean 的时候实际返回的是一个代理对象。直接注入Autowired private UserService userServiceProxy;也能拿到这个代理。但这种写法的问题很明显每个需要代理的接口都得手写一个Bean方法接口一多配置类爆炸。如果需要代理的类不是自己写的是在第三方 jar 包里你连Bean方法都不好加。无法实现“按规则批量代理”例如“扫描某个包下所有 Service全部代理”。所以这种写法适合小规模、固定接口的快速调试生产系统不建议依赖它。3.2 进阶写法FactoryBean 封装代理创建逻辑FactoryBean是 Spring 提供的一个特殊 Bean容器最终暴露给使用方的不是FactoryBean本身而是getObject()返回的对象。这一点特别契合“动态代理 Bean”的需求。Component public class ProxyFactoryBean implements FactoryBeanUserService { Override public UserService getObject() { UserService target new UserServiceImpl(); ProxyFactory factory new ProxyFactory(); factory.setTarget(target); factory.addAdvice(new MethodInterceptor() { Override public Object invoke(MethodInvocation invocation) throws Throwable { long start System.currentTimeMillis(); Object result invocation.proceed(); System.out.println(方法 invocation.getMethod().getName() 耗时 (System.currentTimeMillis() - start) ms); return result; } }); return (UserService) factory.getProxy(); } Override public Class? getObjectType() { return UserService.class; } Override public boolean isSingleton() { return true; } }ProxyFactory是 Spring AOP 自带的代理工厂它会根据目标对象自动选择 JDK 动态代理还是 CGLIB。用它可以省去自己写Proxy.newProxyInstance或者Enhancer的繁琐代码而且能够直接跟 Spring 的 AOP 基础设施融合。这个方案比Bean方法优雅的地方在于它把代理对象的创建逻辑封装到了一个类里并且通过getObject()方法统一收口。但问题依然没有解决——还是针对某一个具体接口写的做不到批量自动处理。还有一个必须记住的小坑如果容器里有FactoryBeanUserService那么通过Autowired UserService拿到的是getObject()返回的代理对象如果你想要拿FactoryBean本身需要在注入点上用Autowired FactoryBeanUserService或者通过getBean(proxyFactoryBean)来获取注意名称前面带的这个前缀这个是 Spring 的约定。3.3 高逼格写法BeanDefinitionRegistryPostProcessor 动态注册 BeanDefinition如果我要实现“扫描指定包下所有 XxxService 接口自动为每个接口生成一个代理 Bean”的效果手写Bean和FactoryBean都顶不住这时候就要请出BeanDefinitionRegistryPostProcessor这个容器扩展点了。BeanDefinitionRegistryPostProcessor在 Spring 容器的“Bean 定义加载完成后、Bean 实例化之前”执行。它的核心能力就是往容器里动态注册BeanDefinition。换句话说普通 Bean 是通过 XML、注解、Java Config 静态声明的而用它可以做到运行时才确定 Bean 怎么注册。结合FactoryBean的用法注册逻辑是这样的给每一个需要代理的接口创建一个指向自定义FactoryBean的BeanDefinition并把这个FactoryBean要代理的接口类型作为构造参数传进去。Component public class DynamicProxyBeanRegistrar implements BeanDefinitionRegistryPostProcessor { Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { // 模拟扫描到的接口列表 Class?[] serviceInterfaces scanServiceInterfaces(); for (Class? serviceInterface : serviceInterfaces) { registerProxyBean(registry, serviceInterface); } } private void registerProxyBean(BeanDefinitionRegistry registry, Class? serviceInterface) { GenericBeanDefinition definition new GenericBeanDefinition(); // 注意这里注册的是 FactoryBean不是目标接口本身 definition.setBeanClass(ServiceProxyFactoryBean.class); // 把接口类型传给 FactoryBean 构造器 definition.getConstructorArgumentValues() .addGenericArgumentValue(serviceInterface); String beanName lowerFirst(serviceInterface.getSimpleName()) Proxy; registry.registerBeanDefinition(beanName, definition); } Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { // 这个阶段可以拿到所有 BeanDefinition但还不建议直接实例化 } }对应的ServiceProxyFactoryBean需要写成接受 Class 参数的版本public class ServiceProxyFactoryBean implements FactoryBeanObject { private final Class? targetInterface; public ServiceProxyFactoryBean(Class? targetInterface) { this.targetInterface targetInterface; } Override public Object getObject() { // 通过 ProxyFactory 生成代理代理的接口是 targetInterface ProxyFactory factory new ProxyFactory(); factory.setInterfaces(targetInterface); factory.addAdvice((MethodInterceptor) invocation - { long start System.currentTimeMillis(); Object result invocation.proceed(); System.out.println(targetInterface.getSimpleName() # invocation.getMethod().getName() 耗时 (System.currentTimeMillis() - start) ms); return result; }); return factory.getProxy(); } Override public Class? getObjectType() { return targetInterface; } Override public boolean isSingleton() { return true; } }关键点解释一下为什么注册的 BeanDefinition 的beanClass是ServiceProxyFactoryBean而不是直接注册代理对象因为代理对象本身无法被 Spring 完整管理它只是一个普通 Java 对象而FactoryBean是 Spring 容器的“一等公民”容器会主动调用它的getObject()方法把产物暴露为 Bean。我们把“创建代理对象”这件事交给FactoryBean把“决定什么时候注册、注册哪些”交给BeanDefinitionRegistryPostProcessor两者配合Spring 容器就能像管理普通 Bean 一样管理动态代理 Bean。3.4 优雅写法ImportBeanDefinitionRegistrar 配合启动类注解BeanDefinitionRegistryPostProcessor是直接实现接口注册而ImportBeanDefinitionRegistrar是 Spring 提供的“面向注解”的注册机制。平时做框架开发比如写一个EnableServiceProxy注解在注解里引入一个 Registrar开启后自动扫描并注册动态代理 Bean这比让用户手动加Component要友好得多。Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Import(ServiceProxyRegistrar.class) public interface EnableServiceProxy { String[] basePackages() default {}; }public class ServiceProxyRegistrar implements ImportBeanDefinitionRegistrar { Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { MapString, Object attributes importingClassMetadata .getAnnotationAttributes(EnableServiceProxy.class.getName()); if (attributes null) { return; } String[] basePackages (String[]) attributes.get(basePackages); // 按包扫描接口、生成代理 BeanDefinition for (String basePackage : basePackages) { scanAndRegister(registry, basePackage); } } }这四种写法不是互斥的实际项目里往往组合使用。我个人的建议是简单场景用Bean需要封装复用用FactoryBean批量注册用BeanDefinitionRegistryPostProcessor做框架和中间件用ImportBeanDefinitionRegistrar。3.5 四种方案对比与适用场景表格整理一下方便大家做技术选型方案代码量适用场景需要谨慎的点Configuration Bean 返回代理对象少接口数量少、固定不变Bean 较多时代码冗余FactoryBean 封装中单一接口的代理逻辑复用无法批量注册BeanDefinitionRegistryPostProcessor中高按包/注解批量注册代理 Bean需要理解容器扩展点ImportBeanDefinitionRegistrar中高做成 EnableXxx 注解给其他人用需要设计注解参数4. 动态代理 Bean 背后的 Spring 生命周期机制4.1 从 BeanDefinition 到 Bean 实例很多读者可能疑惑为什么我用BeanDefinitionRegistryPostProcessor往容器里塞一个 BeanDefinitionSpring 容器就会自动帮我创建代理对象这背后的关键在于 Bean 的完整生命周期。Spring 容器启动之后第一步是加载配置生成一大堆消息者叫BeanDefinition的东西。BeanDefinition只描述“这个 Bean 的类是什么、依赖哪些属性、作用域是什么”并不等于 Bean 本身。然后容器进入实例化阶段。当某个 Bean 被请求或者容器在finishBeanFactoryInitialization()阶段预实例化所有非懒加载单例 Bean 时会按照 BeanDefinition 的描述去创建实例。如果是普通的beanClassSpring 通过反射调用构造器新建对象如果beanClass指向一个FactoryBeanSpring 会先创建FactoryBean本身然后调用getObject()方法把返回对象作为最终的 Bean。所以动态代理 Bean 的注册过程本质上就是通过BeanDefinitionRegistryPostProcessor向容器注册一个beanClass为ServiceProxyFactoryBean的 BeanDefinition。容器推进生命周期到实例化时创建ServiceProxyFactoryBean实例。Spring 意识到它实现了FactoryBean接口于是调用getObject()。getObject()内部动态生成代理对象返回给容器。容器把代理对象放进单例池之后所有通过名字或类型注入拿到的都是这个代理对象。4.2 Spring 对“类型”的识别逻辑用getBean(Class)按类型获取动态代理 Bean 时一个容易踩坑的点是Spring 是怎么判断这个 Bean 的类型是否匹配的答案是通过FactoryBean.getObjectType()。Spring 在getType()时不会真正创建代理对象也不会真的调用getObject()而是先调用getObjectType()看看它声称自己是什么类型。所以我们在ServiceProxyFactoryBean里必须正确实现getObjectType()否则按类型注入会失败。还有一个容易被忽略的细节如果getObjectType()返回的是接口UserService而容器里同时有多个实现该接口的 Bean那么Autowired UserService按类型注入依然会报NoUniqueBeanDefinitionException因为 Spring 不知道应该选哪一个。这时需要配合Qualifier或者把代理 Bean 设置成Primary。4.3 动态代理 Bean 的生命周期和普通 Bean 有什么不同动态代理 Bean 本质上还是通过FactoryBean创建出来的普通单例 Bean所以它的生命周期和普通 Bean 几乎一致实例化 - 属性填充 - 初始化 - 使用 - 销毁。唯一有区别的是“实例化”这一步普通 Bean 通过构造器反射完成而动态代理 Bean 是在FactoryBean.getObject()里完成。这意味着如果你在PostConstruct或者InitializingBean.afterPropertiesSet()里对代理对象做初始化逻辑这个逻辑应该写在getObject()之后由使用方触发而不是直接写在FactoryBean的初始化回调里。举个例子如果代理对象内部连接了外部资源比如数据库连接池你可能会想当然地在ServiceProxyFactoryBean的afterPropertiesSet()里创建连接。但这个连接是在getObject()返回之后才真正被使用的而且如果代理的接口比较多每个FactoryBean都会走一次初始化回调容易把资源重复初始化。正确的做法是让代理对象的内部状态保持轻量仅在需要时才建立连接。5. 实战笔记与踩坑排查实录5.1 代理连代理Transactional 和事务注解失效问题动态生成的代理 Bean 看起来和普通 Bean 没什么两样但如果你给代理的接口方法加了Transactional很可能遇到一个诡异的现象代理 Bean 里的事务注解完全不生效。原因要追溯到事务实现的机制。Spring 的声明式事务也是通过代理实现的即TransactionInterceptor会在方法调用前开启事务方法返回后提交事务。如果我们手动创建的代理是先于 Spring 的事务代理生成的那么外部调用时先进入我们自定义的MethodInterceptor再进入事务拦截器。但这要求我们的代理必须是在“容器创建 Bean 的过程中”用ProxyFactory创建的并且ProxyFactory添加了事务增强器。如果用的是最简单的BeanProxy.newProxyInstance方式生成代理那么代理的InvocationHandler里根本没有事务逻辑Transactional注解自然被忽略。这不算 Spring 的缺陷而是我们自己绕过了 AOP 自动代理机制。解决办法有两个在手动ProxyFactory上主动添加TransactionInterceptor。更推荐的做法是不要把动态代理 Bean 和声明式事务混在一起事务增强交给 Spring AOP动态代理只负责日志、监控、路由等横切逻辑。两层代理各有分工避免互相干扰。5.2 循环依赖FactoryBean 之间的隐性相互引用动态代理 Bean 如果依赖了另一个动态代理 Bean就非常容易触发循环依赖问题。一个典型案例如下假设OrderService和UserService都是通过FactoryBean创建的代理 Bean而OrderService的内部逻辑调用UserServiceUserService又回调OrderService。这两个 Bean 在容器创建时会互相等待Spring 默认的单例循环依赖处理机制是提前暴露ObjectFactory利用三级缓存解决。但如果其中一个 Bean 是通过FactoryBean创建的情况会变得更复杂。三级缓存处理循环依赖时会调用getEarlyBeanReference()方法提前暴露 Bean 的早期引用。对于FactoryBean创建的 Bean早期暴露的其实是FactoryBean的早期引用而不是getObject()返回的代理对象。如果别的 Bean 在依赖注入时拿到的还是早期引用等代理对象真正生成之后两边拿到的就不是同一个对象了。这种问题非常隐蔽通常表现为运行时出现类转换异常或者对象状态不一致。排查思路先看控制台有没有BeanCurrentlyInCreationException如果有优先考虑重构依赖关系让代理 Bean 尽量不在构造器里注入另一个代理 Bean。如果无法避免可以尝试在FactoryBean内使用ObjectProviderT延迟获取依赖把循环依赖的时机推后到实际调用阶段。5.3 代理对象内的自调用增强逻辑莫名消失动态代理 Bean 的增强逻辑只对“从外部调用代理对象的方法”生效。如果代理对象内部的一个方法调用了另一个被增强的方法例如public class UserServiceImpl { public void doA() { System.out.println(execute doA); doB(); // 内部自调用 } public void doB() { System.out.println(execute doB); } }外部调用doA()代理工厂只增强了doA()内部直接调用的doB()并不会再次经过代理。这其实不算动态代理 Bean 特有的问题Spring AOP 也有一样的限制。解决思路有三个一是把doB()的逻辑独立成另一个代理 Bean二是通过ApplicationContext.getBean()获取代理对象来调用doB()三是在代理工厂里对自调用做特殊处理但实现复杂度较高不推荐一般业务这么干。5.4 序列化问题JDK Proxy 与 CGLIB 代理的序列化差异在分布式场景里如果动态代理 Bean 需要被序列化比如放进 Redis 缓存、通过 Dubbo 传递就不得不在意序列化的差异。JDK 动态代理生成的代理类实现了java.io.Serializable接口理论上可以被序列化但前提是InvocationHandler本身也要可序列化且被代理的目标对象不能持有不可序列化的资源。很多人在InvocationHandler里直接引用了DataSource或Connection序列化时直接爆NotSerializableException。CGLIB 代理生成的子类是否可序列化取决于目标类是否继承了Serializable。如果目标类没有实现Serializable那么代理子类也无法序列化。实际做法尽量不直接序列化代理对象而是序列化代理对象背后的数据模型或者 DTO。代理对象的职责是方法拦截和增强不应该承担数据持久化的角色。5.5 类加载器问题ClassCastException 在理不清的类加载器里动态代理 Bean 还有一个容易忽视的坑类加载器。如果你的项目运行在 Java EE 容器如 Tomcat里容器使用WebAppClassLoader加载应用中引用的类。Proxy.newProxyInstance的第一个参数是目标对象的类加载器必须与目标对象的类加载器保持一致否则生成的代理类可能会被不同的类加载器加载导致强制类型转换失败。Spring 的ProxyFactory在传入目标对象时会默认使用AopUtils里的工具方法获取正确的类加载器通常不会出问题。但如果你是手工使用Proxy.newProxyInstance或Enhancer创建代理一定要显式传递与目标类一致的类加载器例如ClassLoader classLoader target.getClass().getClassLoader(); // 传递 classLoader 给 Proxy.newProxyInstance 或 Enhancer这个坑在单体应用里很少暴露一旦迁移到微服务容器化或 Java Agent 场景就会出现偶发性的ClassCastException而且特别难排查。5.6 注册时机的微妙区别在 Spring 启动的哪个阶段注册最安全BeanDefinitionRegistryPostProcessor可以在非常早的阶段介入但如果你注册的代理 Bean 依赖了容器里其他尚未定义的 Bean而你又没有声明DependsOn很可能启动失败。更稳妥的做法是尽量依赖接口类型而不是具体实现或者在FactoryBean中使用ApplicationContextAware延迟获取依赖。以我的经验注册动态代理的BeanDefinition最安全的时机是在容器扫描完成之后的BeanFactoryPostProcessor阶段因为这时候大部分用户 BeanDefinition 已经注册完成但 Bean 实例尚未大量创建不容易出现“先有实例后有定义”的顺序问题。6. 最后分享一点我实测下来的经验动态代理 Bean 注入这套方案掌握之后能解决很多原本只能靠反射硬写的场景但它确实比普通Service注入复杂一个量级。如果只是给单体应用加几条日志直接用 AOP 切面就够了不必为了炫技而引入BeanDefinitionRegistryPostProcessor。如果你的场景确实是运行时动态生成 Bean那么我的个人建议是优先用FactoryBean承载代理创建逻辑而不是直接注册代理实例因为FactoryBean能让 Spring 正确地管理生命周期和处理类型匹配。我踩过的最深的一个坑是在生产环境里突然出现“某个接口注入为 null”的问题。排查了两天后来发现是FactoryBean的getObjectType()返回了nullSpring 在按类型注入的时候无法匹配到它。所以想提醒看到这里的朋友无论你的代理工厂有多少花活请至少确保getObject()、getObjectType()、isSingleton()这三个方法的行为是明确的它们是整个机制正常运转的底线。另外如果你把动态代理用在类似“热更新”的场景需要频繁注册新 Bean 并销毁旧 Bean那么需要注意单例池和disposableBean的清理。Spring 默认不会在运行时重新扫描你新注册的 BeanDefinition除非你手动触发DefaultListableBeanFactory.freezeConfiguration()的重新执行或者使用ConfigurableApplicationContext.refresh()——但这个操作代价很大不建议线上频繁使用。更可行的方法是把动态代理 Bean 的作用域设置为prototype让容器每次都通过getObject()创建新的代理实例避免缓存带来的不一致。我一直觉得动态代理 Bean 注入是一个能同时考验“Java 代理机制”和“Spring 容器生命周期”两个知识点的实践场景。能把这两块融会贯通的人排查线上问题的时候思路会通透很多。
返回列表