ARTICLE DETAIL

资讯详情

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

从三级缓存到Spring Boot 3:Spring高频检索问题全解析

从三级缓存到Spring Boot 3:Spring高频检索问题全解析 【Spring笔记】从三级缓存到Spring Boot 3那些高频检索背后的真实问题如果你和我一样浏览器收藏夹里堆了几十个Spring相关的链接从三级缓存原理到Boot 2.3和2.6选哪个再到Spring AI连百炼——那这篇笔记写给你。前段时间整理了一轮自己的Spring笔记发现热搜词里大家反复搜的东西其实集中指向几个真实诉求底层原理看不透、版本选型拿不准、工程落地没头绪、新技术AI、监控、微服务治理不知道怎么接。这篇笔记把我的理解、踩过的坑、实际验证过的方法串起来不求面面俱到只求每个话题都能一句人话讲清本质再给一套可复现的操作路径。适合正在学Spring、被循环依赖绕晕、准备跳槽刷面试题、或者刚接手一个Spring Boot项目需要梳理思路的开发者。1. 三级缓存原理为什么是三级少一级会怎样1.1 三级缓存分别存什么先把名字对上号Spring的三级缓存说的是DefaultSingletonBeanRegistry也就是AbstractBeanFactory的父级里面三个MapsingletonObjects一级缓存存放创建完并且完成属性填充、初始化、代理增强的最终Bean。earlySingletonObjects二级缓存存放提前暴露的早期引用这个对象还没走完完整生命周期属性可能还没填完代理可能还没生成。singletonFactories三级缓存存放的是ObjectFactory?一个工厂对象真正需要时才调用它去生成早期Bean。这三个Map在源码里的定义分别是private final MapString, Object singletonObjects new ConcurrentHashMap(256); private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); private final MapString, ObjectFactory? singletonFactories new HashMap(16);我习惯把三级缓存看作一条流水线singletonFactories排在最前它负责按需产出早期引用产出后放进earlySingletonObjects暂存等到Bean创建完成最终入驻singletonObjects。1.2 为什么二级缓存不够一定要三级这是最常被面试官追问的点也是很多人卡住的地方。先说结论如果没有AOP代理只有二级缓存也是够用的。但Spring里大量Bean会被Transactional、Async、AOP切面增强一旦循环依赖 AOP同时出现二级缓存就出问题了。用一个例子讲清楚。假设A类有个Transactional方法A依赖BB又依赖A创建A发现需要B开始创建B。B创建时发现需要A但A还没创建完。如果只有二级缓存B拿到的会是A的原始实例。等A真正创建完毕执行postProcessAfterInitialization时Spring发现A需要做事务代理于是把singletonObjects里的A替换成了代理对象。问题来了B手里持有的还是原始A它调用A的Transactional方法时事务增强完全不生效。三级缓存解决这个问题的关键在于ObjectFactory——getEarlyBeanReference会在B需要A的那一刻提前判断A是否需要代理如果需要就直接生成AOP代理放进二级缓存。之后A走完初始化流程时postProcessAfterInitialization检测到已经有了提前代理直接复用同一代理不再重复生成。理解这个机制有两个关键getEarlyBeanReference和postProcessAfterInitialization返回的代理必须是同一个否则照样会出现两个A。只有Bean还在创建中且被循环依赖引用了才会走提前暴露的逻辑没被引用的Bean不会提前暴露。当年读源码读到这里真正搞明白的那一刻整个Spring的设计思路都顺了。推荐大家去读AbstractAutowireCapableBeanFactory里的doCreateBean方法定位到addSingletonFactory那一行把断点打在getEarlyBeanReference上运行一个带循环依赖的项目观察调用链。1.3 手写Spring时的实现顺序手写Spring是近年面试培训班的高频词但真正写过的人会懂手写不是为了造一个能用的框架而是为了把生命周期逼出来。我建议按这个顺序写第一步BeanDefinition注册。先定义BeanDefinition存类名、是否单例、属性值用Map保存。第二步反射创建实例。根据BeanDefinition用构造器newInstance这一步可以先不管依赖注入。第三步属性填充和依赖注入。遍历Autowired字段从容器里取依赖Bean取不到就先创建被依赖的Bean——这一步会自然产生循环依赖需求。第四步Bean后置处理器BeanPostProcessor。在实例化后、初始化前后各留一个扩展点AOP代理在这里注入。第五步加上三级缓存的提前暴露机制循环依赖问题自然解决。给自己定一个验收标准能支持Component扫描、Autowired注入、Transactional代理、循环依赖这几个特性就算及格了。写的过程中你会真正理解为什么Spring的Bean工厂有那么多层抽象为什么getBean方法会递归调用为什么后置处理器叫后置却干着前置的活。1.4 面试里常见的那几个追问围绕三级缓存面试题通常会这样升级AOP代理和三级缓存有什么关系——答案就是1.2节的内容注意提AbstractAutoProxyCreator实现了SmartInstantiationAwareBeanPostProcessor。Spring Boot 2.6为什么默认关闭循环依赖——因为循环依赖本质是设计味道Spring官方从2.6开始把spring.main.allow-circular-references默认设为false倒逼大家改构造器注入或重构。2.3.x及以前默认允许这也是很多老项目升级时报警告的原因。为什么没有把代理提前到一级缓存——一级缓存放的是完整Bean提前放半成品不符合语义而且getSingleton的语义在整个BeanFactory里都被依赖不能随便改。三级缓存里的ObjectFactory到底返回什么——返回的是getEarlyBeanReference的调用结果不是getBean。这个坑很多人踩建议写个小Demo打印返回值的类型。说到AOP源码热点词里的proxy factory其实就是ProxyFactorySpring AOP对目标对象创建代理的核心类。快速入门可以先忽略一堆Advisor、Pointcut概念盯住ProxyFactory.getProxy()它内部会判断目标是接口就用JDK动态代理没有接口就用CGLIB然后把Advice包装成Advisor挂到代理链上。2. Spring Boot版本矩阵2.3.x、2.6.x、3.x到底怎么选2.1 两年多过去了为什么还有那么多2.3和2.6在跑打开公司仓库扫一眼Spring Boot 2.3.x和2.6.x的存量项目依然不少。原因很现实项目在稳定运行、升级要付出测试成本、老代码里可能存在循环依赖或javax.*依赖升级到Boot 3不是改个版本号那么简单。但还能跑不等于一直能跑。2.3.x对应Spring Framework 5.2.x已经过了社区维护生命周期2.6.x和2.7.x对应Framework 5.3.x5.3.x是社区长期支持分支到现在还有补丁更新比如热词里的Spring Framework 5.3.41下载就是5.3.x的新补丁版本。这里有个判断技巧Framework 5.3.x是最后一个还维护的5.x分支对应Boot 2.7.xBoot 2.3已经跟不上官方安全更新了。如果让我给选型建议就三条遗留系统在跑2.3.x短期内能不动就不动但必须评估安全补丁新功能业务模块可以尝试独立升级到Boot 2.7为后续迁移做铺垫。不得不用JDK8的新项目选Boot 2.7.x这是最后支持JDK8的版本线尽量别选2.62.6默认把循环依赖关了团队改造成本高。新建项目明确选Boot 3.x开局就是JDK17用jakarta.*命名空间面向未来。2.2 2.x、3.x、Spring Framework版本怎么对应很多人搞不清Boot版本和Framework版本的关系其实一句话Spring Boot是启动器Spring Framework才是底层框架。Boot通过依赖管理统一拉取Framework版本。对应关系大致如下Spring BootSpring FrameworkJDK要求命名空间2.3.x5.2.xJDK8javax.*2.4.x5.3.xJDK8javax.*2.6.x5.3.xJDK8/11/17javax.*2.7.x5.3.xJDK8/11/17支持17javax.*3.0.x/3.1.x6.0.xJDK17jakarta.*3.2.x/3.3.x6.1.xJDK17jakarta.*3.4.x/3.5.x6.2.xJDK17jakarta.*jar包下载也顺带说一句不建议去网站上手动下载spring的jar包容易把版本线搞乱。用Maven坐标最省心比如spring-framework-bom版本的pom.xml里由Boot统一管理。补丁包的拉取也一样把Boot版本升级到位Framework自动跟着走。spring证书哪里查这个话题也经常有人问——Spring官方的专业认证查询入口在VMware/Broadcom的认证体系里Spring现任主理团队VMware Tanzu发布的认证证书可以在其官方认证平台上按姓名或证书编号查验。不过对大多数工程师来说与其纠结证书不如用证书同款知识点去刷题面试官更认后者。2.3 Boot 3和FastAPI不是对立面热搜里后端spring boot 3和python fastapi这个提问很有意思。我的看法是这两个框架解决的是不同生态的问题拿它们做选择题不如先想清楚团队和项目类型。Boot 3的优势在于类型安全、企业级组件全事务、安全、消息、分布式适合复杂业务系统、中大型团队、需要长期维护的微服务架构。FastAPI的优势在于轻量、异步原生、AI生态好Python的后端服务、模型服务、小工具接口用它非常顺手。实际项目里不少团队的做法是Python做AI模型推理服务Java做业务主链路。两边通过HTTP接口通信各自发挥长处。如果你纠结选型我建议按三个维度打分团队主流语言、业务复杂度、周边生态依赖。团队全是Java选Boot业务以数据分析和模型调用为主选FastAPI两边都行的情况下Boot 3的工程化能力更适合多人协作。2.4 用IDEA社区版开发Spring Boot的三个落地方法IntelliJ IDEA社区版怎么用Spring Boot也是高频问题。社区版没有Spring Initializr入口但有三条路浏览器打开start.spring.io选好Spring Boot版本和依赖下载zip解压后用IDEA社区版直接打开。这是最稳的方式。用命令行动手拉模板curl https://start.spring.io/starter.zip -d dependenciesweb,data-jpa -d javaVersion17 -o demo.zip本质上和网页端一样。装IDEA插件比如社区版可用的Spring Boot Helper但插件依赖社区版API的兼容性目标IDEA版本升级后插件可能失效我一般不太推荐把它作为核心路径。社区版对Spring Boot日常开发完全够用Maven/Gradle、断点调试、Git、终端都有。缺的其实是企业版的Spring Bean智能提示、端点运行视图这些锦上添花的功能影响不大。2.5 Spring MVC在笔记里的位置Spring MVC本质上就是Spring对Web请求处理的一套组件DispatcherServlet、HandlerMapping、Controller适配器、ViewResolver。学Boot时的RestController、RequestMapping之所以好用正是因为Boot自动装配了这套MVC链路。对新人我有个建议别在Controller层堆业务代码。Controller只做参数接收、校验和视图响应真正逻辑放到Service层。搜Spring面试题的人十有八九会碰到类似场景但真正在代码评审里卡人的不是会不会写RestController而是层与层之间的依赖方向有没有搞反。3. 读开源多商户跨境商城源码的正确打开方式3.1 先分清两种下载需求毕设对照和商业参考搜spring boot mybatis的java开源多商户跨境商城源码下载的人至少分成两类。一类是写毕设的比如基于Spring Boot的大学生就业推荐系统的设计与实现——这类项目本质上需要的是一个结构清晰、能跑通、答辩能讲清楚的骨架。另一类是想做小商城的创业者或外包开发他们需要的是能直接改、能上线的东西。两类需求标准完全不同。毕设对照看重的是技术完整度有没有注册登录、权限管理、CRUD、图表、论文能对应上的结构。商业参考则看重非功能性设计多租户隔离、支付路由、防重复下单、日志审计、可部署性。把100个开源源码拿来比一遍你会发现多数开源项目只满足了前者。3.2 多商户商城的六个核心模块一份合格的多商户B2B2C商城源码我建议重点读这六个模块商户端与平台端的多租户体系——数据库层面怎么隔离。商品中心——SKU、规格、属性、分类的模型设计。交易中心——购物车、订单、支付、退款状态机。结算与分账——平台抽成、商户结算、跨境场景下的多币种处理。会员/营销——用户等级、优惠券、秒杀、拼团。物流与库存——多仓库发货、库存锁定、超卖控制。读代码的时候别先一头扎进Controller而是先画一张模块依赖图哪个服务依赖哪个库哪个模块调用哪个RPC。这一步做完整个项目的大局就清楚了。3.3 MyBatis在多租户场景里怎么落地多商户商城多租户最直接的问题是商户A和商户B的数据怎么隔离常见手段有三种独立数据库安全等级最高但成本高。共享数据库、独立Schema隔离性好但维护成本偏高。共享数据库、共享Schema、加租户ID字段最常见的做法低成本。在MyBatis链路里加租户ID有几种落地方式。最优雅的是利用MyBatis的Interceptor拦截Executor的update/query方法在SQL层面自动拼接租户条件。具体来说就是实现org.apache.ibatis.plugin.Interceptor在intercept方法里改写BoundSql给WHERE条件追加商户ID。这类拦截器做得好时业务代码完全无感知写SELECT不用手动带merchant_id。但要注意一个坑多租户拦截器对原生SQL的解析能力有限。如果你在XML里写了script动态SQL、UNION、子查询、FOR UPDATE解析器很可能改写错。我的建议是拦截器只作为兜底核心查询在SQL设计时就显式带上租户条件人和框架双重保障。3.4 源码读不下去怎么办建立运行地图把源码下下来第一件事不是看代码是让它先跑起来。多数开源商城提供SQL初始化脚本和配置文件把数据库建好、改完配置、启动成功才算拿到活的源码。跑起来后按请求路线走读注册一个商户→登录→发布商品→下单支付→商家发货→用户确认收货。每步去代码里找对应的Controller→Service→Mapper记录调用链。这张运行地图比任何类图都管用。我见过不少同事下载了源码三天后就开始吃灰原因就是直接在IDE里随机打开类文件被继承体系淹没了。我的经验是从用户故事反推代码路径不追求覆盖率追求每一个核心路径都能讲到五层。4. Spring Security难学难在过滤器链的顺序4.1 认证、授权、过滤器的实际关系Spring Security之所以劝退是因为它不是一个简单的工具库而是一套过滤器链架构。你用EnableWebSecurity定义了一个SecurityFilterChain实际上是在配置多个过滤器如何串联UsernamePasswordAuthenticationFilter负责登录认证AuthorizationFilter检查URL授权ExceptionTranslationFilter统一处理异常。理解这套机制要注意三点过滤器链有顺序每个过滤器都有明确的职责。你配置的permitAll、authenticated规则是在授权过滤器里判断的认证过滤器在前面另做一套事。SecurityContextHolder是线程绑定上下文认证成功就把Authentication对象放进去后续业务代码通过SecurityContextHolder.getContext().getAuthentication()拿当前用户。UserDetailsService是用户数据加载入口你只需要返回一个UserDetails用户名、密码、权限剩下的比对逻辑框架来做。入门最快的路径是先把过滤器链调通表单登录、内存用户、H2控制台、放行静态资源一步步把SecurityFilterChain的规则注释掉看效果。遇到401/403不要猜直接看ExceptionTranslationFilter抛出的异常类型。4.2 第三方接口放哪不该只看有没有权限热搜词里Spring Boot对外提供的接口给第三方应该放在哪里是单独的服务还是放在对应的...——这是很多团队在对接外部系统时纠结的问题。答案不是非黑即白取决于你的调用方是谁。我的决策思路是这样场景推荐方案理由接口数量少、业务耦合深、主要是内部系统调用放在原服务用Spring Security配置白名单路径改动小、开发快、不用做数据同步外部合作方多、需要统一鉴权和验签单独出口服务开放平台统一管理AppKey、密钥、限额、日志避免把内部服务细节暴露给外部接口量持续增长、团队分工清晰独立微服务 API网关解耦治理横向扩展独立安全策略可以集中下发选择单独服务的时候真正复杂的不是写Controller而是处理跨服务身份传递第三方传过来的调用凭证access token要在出口服务里校验然后通过内部调用链把服务身份传给下游不能让下游再签一遍。还需要设计验签、时间戳防重放、IP白名单、接口幂等。这些在单体场景不容易想到到开放平台就全是大事。4.3 用Sentinel做接口治理时的注意点再说热搜里的spring cloud sentinel datasource redis集群。Sentinel是阿里开源的流量治理组件限流、熔断、降级都在里面。但很多团队第一次用它是直接把规则写在application.yml或Sentinel控制台界面里这样有两个问题规则和代码耦合改规则要重启。多实例部署时每台机器规则不同步集群里就会出现限流一半的诡异现象。把Sentinel数据源接到Redis集群思路就是把限流降级规则集中存到Redis客户端启动时拉取规则同时通过Redis的发布订阅监听规则变更控制台改完规则后所有实例即时生效。实现上引入sentinel-datasource-redis依赖在配置里指定Redis地址、频道和规则类型FlowRule等数据源会自动完成远端加载和订阅。spring: cloud: sentinel: datasource: flow: redis: redis-port: 6379 redis-host: 192.168.x.x channel: sentinel-rule-channel rule-type: flow这套方案在规则少、变更不频繁的时候很够用。但规则一多几十条以上Redis里的Key管理和规则版本回滚就会麻烦那时候可以考虑Nacos数据源用推模式配合控制台做规则配置中心。选型逻辑就是Redis偏轻、Nacos偏重治理按团队基础设施来。5. Spring AI生态观察从2.0热点到A2A协议5.1 Spring AI到底解决了什么问题Spring AI是Spring官方把LLM能力引入Java生态的尝试它做的事情和当年Spring Data对数据库做的事类似抽象统一接口屏蔽各家大模型API的差异。Spring AI的核心抽象就是ChatModel甭管是OpenAI、通义千问还是百炼上的qwen模型你只需要注入不同实现的ChatModel调用chat.call(prompt)即可。写Java后端的人不用再贴一堆HTTP请求代码去调各家SDK。Spring AI 2.0已经作为一个相对成熟的版本发布支持了工具调用、结构化输出、多模态等能力。我当时上手时的感受通义千问、百炼平台的接入路径比想象中简单官方文档以Spring AI 2.0版本为例配置好api-key和base-url把ChatModel注入Service一个小型客服问答接口就通了。5.2 阿里云百炼和spring-ai-alibaba的状态搜spring ai alibaba停更了吗的人大概率看到过GitHub仓库更新频率不稳定的历史。这个问题我按第二手信息说Spring AI Alibaba是阿里云百炼对接Spring AI官方标准的实现会跟随Spring AI的大版本走。如果你用的是阿里云百炼的qwen3.7等模型目前建议直接使用百炼官方文档推荐的starter不必自己封装请求层。注意一个容易混淆的点阿里云有一个独立的SDK生态如dashscopeSDK和Spring AI官方规范下的spring-ai-alibaba不是一回事。选哪个取决于你是不是要在Spring生态里复用统一抽象。如果是为了快速做Demo用阿里自己的SDK如果项目里未来可能切换多家模型用Spring AI Alibaba更划算。5.3 Agent和A2A新东西要先分清名词Spring AI Agent和A2A Spring是两码事经常被放在一起搜。Agent在Spring AI 2.0里的落地方式是把工具方法用Tool注解暴露给模型模型通过对话推理决定要不要调用工具。比如做一个天气查询Agent写一个Tool(查询城市天气)方法模型在用户问到天气时自动填充城市参数并调用它。你的业务代码只负责定义工具编排由AI完成。A2AAgent2Agent则是Google提出的一套Agent间通信协议解决多个独立Agent之间怎么发现彼此、怎么传递任务、怎么返回结果的问题。Spring AI官方在新版本里对A2A协议有兼容作用——消息按JSON-RPC格式走Agent之间通过URL互相调用。对普通业务开发A2A暂时不影响日常功能但如果你想做多Agent协作的架构它会是以后的基础设施。5.4 把Dify工作流转成Java代码的现实路径Dify工作流转成Spring AI Java代码github这个热搜很有意思。用过Dify的人都知道可视化工作流搭起来非常快一个输入节点→一个大模型节点→一个知识库检索节点→一个条件分支→输出。但到了Java后端要接同一个业务链路不可能在Java代码里再搭一套可视化编排只能翻译成可执行的代码结构。我的映射经验是这样的Dify工作流节点Spring AI对应实现开始节点Controller层入参DTO大模型节点ChatModel PromptTemplate知识库检索节点VectorStore / 向量库工具检索工具调用节点Tool方法比如查数据库、调外部接口条件分支节点Java代码里的if/else或规则引擎结束节点Controller返回统一响应结构核心心法只有一个别试图把可视化编排整个搬到Java里而是把Dify里的数据流转拆成节点的执行顺序和依赖关系用Service层的代码依次串起来。Dify适合产品原型和运营快速验证Java代码适合上线和高并发两者定位不同。实操上我会先打开Dify里的导出JSON看清节点之间的连线画出数据从哪个输出端流向哪个输入端然后按从上到下的顺序手写Java方法。如果工作流带了知识库记得在Spring里初始化好VectorStore和Embedding模型这一步和Dify后台配置知识库是一一对应的。6. Spring Boot监控从需求到落地6.1 你以为的监控和实际要的监控spring boot实现监控都有哪些需求和功能?这个热搜太真实了——新手以为监控就是服务活着没实际上线后才发现远远不止健康检查/actuator/health这是最重要的。应用指标内存、CPU、线程数、GC次数。HTTP请求指标吞吐、平均响应时间、错误率。数据源指标活跃连接、空闲连接、慢SQL。日志级别动态调整不用重启项目就能改logger级别。线程快照和堆快照排查死锁、OOM。外部依赖调用第三方接口延迟出问题先定位是不是上游挂了。更实际的一点监控不只是看面板而是报警→定位→处置的闭环。只接数据不配报警等于没监控。所以我做监控的标准是基础设施类指标用PrometheusGrafana应用健康类指标用Boot的Actuator暴露再加一套Spring Boot Admin做实例注册与可视化的轻量入口。6.2 Actuator端点开哪些别一股脑全开Actuator默认只暴露了health和info生产环境建议按需开放。我的常用配置长这样management: endpoints: web: exposure: include: health,info,metrics,loggers,threaddump,env endpoint: health: show-details: always loggers: enabled: true其中heapdump在生产环境要慎重这个端点会导出整个JVM堆内存快照Java进程占多少内存就导出多大文件频繁调用会把磁盘打爆。shutdown端点默认关闭是正确的别手贱打开。env端口在某些内网环境里可以开但有敏感配置值暴露给管理页面的风险需要配合权限控制。如果你用Spring Boot Admin客户端只要暴露给Admin服务器所需的那几个端点就行不需要全开公网。6.3 Spring Boot Admin的部署套路Spring Boot Admin提供一套现成的可视化UI服务端和客户端两部分组成服务端建一个独立的Spring Boot应用引入spring-boot-admin-starter-server主类加EnableAdminServer。客户端每个需要被监控的服务引入spring-boot-admin-starter-client配置spring.boot.admin.client.url指向服务端地址。dependency groupIdde.codecentric/groupId artifactIdspring-boot-admin-starter-server/artifactId version3.3.3/version /dependency版本匹配是这里最容易踩的坑Spring Boot Admin 2.x对应Boot 2.xAdmin 3.x对应Boot 3.x。用错版本客户端注册老是失败看日志半天找不到原因。6.4 规则动态下发为什么用Redis做Sentinel数据源回到前面提过的Sentinel Redis数据源换一个角度理解它的必要性。如果没有动态数据源限流规则就只能写在本地配置或控制台内存里。控制台内存型规则最大的痛点是Sentinel控制台默认只做展示和单机操作改动不会自动同步到所有实例。你在线上去掉一条限流规则结果只有你连的那一台机器生效其他机器还在限流。Redis数据源的架构价值就是规则集中、动静分离实例启动时从Redis拉一次全量规则之后订阅规则变更频道控制台推送新规则到Redis所有在线实例同时收到更新。配置的运行机制就是发布订阅规则在内存里热加载不需要重启。在集群规模大于5个实例、规则调整频繁的场景下强烈建议把规则数据源落进Redis或Nacos别依赖控制台。改造成本不高收益是治理能力上了一个台阶。收尾之前的几句大实话这篇笔记写到现在其实是有意把热搜词按原理—版本—工程—安全—AI—监控这条线串了一遍。如果只让我留三句话第一Spring的根基在Bean生命周期和AOP这两块弄透三级缓存、代理、事务、Security、AI工具调用全都是同一个套路的不同应用场景。看到BeanPostProcessor、ObjectFactory出现时不要再慌它们其实是同一个故事里的角色。第二版本选型和技术选型没有最正确只有最匹配。老项目别盲目升级新项目别留恋旧版本。把2.3、2.6、3.x的差异点写在项目文档里远比在代码里偷偷调allow-circular-references要省心。第三学习Spring最快的方式是带着问题读源码而不是抱着视频课从头看到尾。视频能带你把项目跑起来但只有断点打在doCreateBean和getEarlyBeanReference上的时候你才真正开始懂Spring。Spring实践视频可以看但不能只看不写写个最小Demo、跑一个循环依赖、打印一次过滤器链这些动手实验比任何十小时速通都值。
返回列表