
Spring这套东西你越往深挖越觉得它像一棵老树根系盘根错节。很多人第一眼看到Autowired觉得是魔法第二眼看到三级缓存又觉得神秘再看到 Spring AI 2.0 冒出来发现这棵老树居然还能发新芽。这篇总结“上”部分我想从一个偏底层的视角把 Spring 最核心的控制反转、Bean 生命周期、循环依赖、AOP 代理机制以及近两年绕不开的 Spring Boot/Cloud 版本选型、Spring Security 新玩法、Spring AI 方向一次性捋清楚。内容会偏实操和源码理解适合刚学完 Spring Boot 基础、准备进阶看源码的开发者也适合正在准备面试、想要把原理讲明白的同学。老规矩这篇没有废话只有干货。1. Spring控制反转容器替我们省掉的远不止一个new1.1 为什么“不自己new”这件事能改变整个架构控制反转这个概念如果只从字面看很多人会误以为 Spring 只是帮我们少写了一行new。实际上没那么简单。你自己 new 一个对象对象的创建时机、创建过程、依赖装配、销毁时机全都由你负责而交给 Spring 容器之后这些事全部归容器管。举个例子你开了一家餐厅以前所有菜品的采购、清洗、切配、烹饪、上桌全是后厨自己干的。引入 IoC 之后相当于找了个中央厨房容器你只需要在菜单上写“我要一盘番茄炒蛋”中央厨房会按标准流程把菜配好、炒好、送到你桌上。你甚至不需要知道番茄是从哪里买的、蛋是哪批的。这段类比想说明的核心是控制反转把“对象之间的耦合关系”从代码里抽离了出去。类与类之间不再直接通过new硬绑定而是通过容器注入这样你可以随意替换实现类可以很容易做单元测试也可以对每个 Bean 做统一的生命周期管理。很多 Java 开发者第一年都在写UserService userService new UserService()这种代码等真正脱离这种写法之后才算开始理解分层架构的价值。Spring 的容器并不是天生就这么智能它的背后是几个核心组件在协作BeanDefinition保存 Bean 的元数据BeanFactory负责生产对象ApplicationContext在 BeanFactory 之上增加了事件发布、国际化、资源加载等能力。后面我们在手写简化版 IoC 容器的时候你会发现这套设计其实很朴素难的是细节。1.2 手写一个简化版IoC容器Spring底下转的就是这几步如果你去搜“手写 Spring”这个词会发现网上有很多精简教程。这里我用手写简化版容器的思路把 Spring 控制反转的核心流程讲透。真实 Spring 的流程比这个复杂得多但主线不会变扫描配置或注解解析出每个 Bean 的BeanDefinition把BeanDefinition注册到容器通过反射创建实例对实例做属性填充依赖注入执行初始化逻辑放入单例池对外提供getBean。下面是一个超级简化的容器模型去掉大量异常处理和扩展点public class SimpleIocContainer { // 单例池对应 Spring 的一级缓存 private final MapString, Object singletonObjects new ConcurrentHashMap(); // Bean 定义注册表 private final MapString, BeanDefinition beanDefinitions new ConcurrentHashMap(); public void register(String beanName, BeanDefinition definition) { beanDefinitions.put(beanName, definition); } public Object getBean(String beanName) throws Exception { Object bean singletonObjects.get(beanName); if (bean ! null) { return bean; } return createBean(beanName); } private Object createBean(String beanName) throws Exception { BeanDefinition definition beanDefinitions.get(beanName); if (definition null) { throw new IllegalArgumentException(No bean defined: beanName); } // 1. 反射实例化 Class? clazz Class.forName(definition.getClassName()); Object instance clazz.getDeclaredConstructor().newInstance(); // 2. 属性填充这里只做最简单的字段赋值 for (PropertyValue pv : definition.getPropertyValues()) { Field field clazz.getDeclaredField(pv.getName()); field.setAccessible(true); Object value getBean(pv.getRef()); field.set(instance, value); } // 3. 放入单例池 singletonObjects.put(beanName, instance); return instance; } }这个容器有几个明显缺陷没有循环依赖处理、没有扩展点、没有生命周期回调。但你已经可以看到 Spring 的骨架了注册定义、反射创建、依赖注入、单例缓存。回到实际 Spring 框架中ApplicationContext的refresh()方法里有一句话叫做finishBeanFactoryInitialization它会遍历所有BeanDefinition逐个调用getBean。中文翻译过来就是把该创建的 Bean 全部创建出来放到容器里等着。这个创建过程中如果发现某个属性依赖另一个 Bean就会先创建被依赖的 Bean。这里又引出一个特别经典的问题如果 A 依赖 BB 又依赖 A两边互相等着对方先创建好程序不就卡死了吗这就是下一节三级缓存要解决的问题。1.3 BeanPostProcessorSpring扩展能力的总开关官方文档喜欢强调一句话Spring 框架本身就是依赖很多BeanPostProcessor来工作的。什么意思呢BeanPostProcessor是容器在 Bean 实例化、属性填充之后、初始化前后暴露出来的拦截点。每个ApplicationContext都会自动注册一堆后置处理器比如AutowiredAnnotationBeanPostProcessor负责处理AutowiredCommonAnnotationBeanPostProcessor负责处理ResourceAnnotationAwareAspectJAutoProxyCreator负责处理 AOP 代理。你可以把BeanPostProcessor想象成水龙头的滤芯。水Bean流经滤芯后置处理器时会被过滤、净化、加工最终才送到用户调用方手里。Spring 的“自动注入”“事务代理”“异步代理”这些能力本质上都是靠不同滤芯组合出来的。public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws Exception; Object postProcessAfterInitialization(Object bean, String beanName) throws Exception; }postProcessAfterInitialization是一个特别重要的时机点AOP 代理的生成就发生在此处。Spring 容器在 Bean 初始化完成后会调用所有后置处理器的postProcessAfterInitialization其中 AOP 后置处理器会判断当前 Bean 是否需要被代理如果匹配切点就返回一个代理对象。这也是为什么很多人说“Spring AOP 的本质就是代理 后置处理器”。理解这块对排查问题很有帮助。比如你自建了一个BeanPostProcessor在里面拿到了某个 Bean 实例做处理但发现拿到的不是最终代理对象而是原始对象这时候你就得注意后置处理器的执行顺序以及它是在postProcessAfterInitialization还是更早的时机介入的。2. Spring三级缓存原理循环依赖为什么需要三层Map2.1 singletonObjects、earlySingletonObjects、singletonFactories各管啥循环依赖在面试里出镜率极高几乎成了 Spring 源码理解程度的试金石。它的本质是A依赖BB又依赖A两个 Bean 在创建过程中互相等待。Spring 解决单例 Bean 循环依赖的方案就是DefaultSingletonBeanRegistry里的三级缓存也是三个 Map// 一级缓存存放初始化完成的单例 Bean 实例 private final MapString, Object singletonObjects new ConcurrentHashMap(256); // 二级缓存存放提前暴露的早期单例对象 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); // 三级缓存存放创建早期引用的 ObjectFactory private final MapString, ObjectFactory? singletonFactories new HashMap(16);这三个 Map 各管一摊事一级缓存singletonObjects大家都是从这个池子里拿 Bean 的。只有真正初始化完成、属性填充完毕的 Bean 才有资格进这个池。这也是为什么“直接查一级缓存拿不到”说明 Bean 还在创建途中。二级缓存earlySingletonObjects存放“虽然还没初始化完成但至少已经被实例化出来”的早期对象。当一个 Bean 提前暴露给别的 Bean 使用后它就进了二级缓存防止同一个早期对象被反复创建。三级缓存singletonFactories存放一个ObjectFactory调用它的getObject()时才会真正产生早期引用。它的存在是为了把“是否生成代理”的决策延迟到最后时刻。当 A 和 B 互相依赖时走的是这么一条链路A 开始创建先实例化出原始对象A 发现自己需要 B于是先把 A 的ObjectFactory放进三级缓存A 转去创建 BB 实例化后也把ObjectFactory放进三级缓存B 发现自己需要 A尝试从一级缓存拿没有再从二级缓存拿也没有最后在三级缓存里找到 A 的工厂调用工厂拿到 A 的早期引用B 拿到 A 的早期引用后完成自己的创建进入一级缓存A 继续创建拿到已经创建好的 B完成属性填充和初始化进入一级缓存。整个过程如果画成图就是两个 Bean 在创建过程中互相拉了一把。关键点在于B 拿到的那个 A 的早期引用和最后一级缓存里放进去的 A必须是同一个对象否则应用启动后会出现类型不匹配、注入失效等问题。怎么保证同一个靠后面对二级缓存和代理的处理。2.2 为什么两级不行早期代理才是关键很多人在看过三级缓存原理后都会问为什么不直接把早期引用放到二级缓存省掉三级两级缓存设计A 在实例化后直接把自己的半成品放进二级缓存B 去拿的时候取到原始 A看起来也能解决循环依赖Spring 为什么还要绕一道ObjectFactory核心原因是A 可能需要 AOP 代理。假设 A 被切点命中它的最终形态应该是一个代理对象。如果只有两级缓存B 在依赖 A 时拿到的是原始 A 对象而 A 在初始化完成后会被 AOP 后置处理器替换成代理对象放入一级缓存。这么一来B 持有的是原始 ASpring 容器里最终保存的是代理 A两个对象不是同一个B 里注入的 A 就不具备事务、日志、权限等代理能力运行期会非常诡异。三级缓存做了一件很聪明的事它不着急把“原始对象”放进去而是放入一个ObjectFactory。当 B 真正需要 A 的早期引用时这个工厂会被调用并且在调用时通过SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference提前执行 AOP 判断如果 A 需要代理这里就直接返回代理对象。也就是说B 在循环依赖中拿到的 A从一开始就是代理 A后面整个容器里保存的也是这个代理 A引用完全一致。// Spring 源码中 getSingleton 的简化逻辑 protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { synchronized (this.singletonObjects) { ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }这段源码透露的信息量很大三级缓存本质上是一层延迟执行机制它把“早期暴露”和“代理生成”绑定在了一起。平时没有循环依赖的 Bean三级缓存是无用武之地的一旦出现了循环依赖它就成了保证引用一致的决胜招。我当年第一次在源码里看到这段流程时有种豁然开朗的感觉Spring 并没有用什么黑科技只是在 Java 语言的语法框架里把时机和引用关系安排得明明白白。2.3 三级缓存也有救不了的场景这些坑迟早会遇到三级缓存不是万能的。我在实际项目和面试交流里见过很多开发者默认“只要交给 Spring循环依赖就能自己解决”这个认知是要纠正的。第一构造器注入的循环依赖无解。因为构造器注入在实例化阶段就必须要拿到依赖对象此时还没有机会把早期工厂放进三级缓存根本没有“提前暴露”这回事。Spring 报BeanCurrentlyInCreationException基本就是这种情况。如果你硬要解决只能改成字段注入或者 Setter 注入但更合理的做法是重新设计依赖消除循环。第二非单例 Bean 不缓存。Scope(prototype)的 Bean 不走三级缓存容器每次都会创建新实例循环依赖无从谈起。Scope(request)、Scope(session)也有各自的创建边界不适合用来制造循环依赖。第三Spring Boot 2.6 开始的默认限制。从 Spring Boot 2.6.0 开始spring.main.allow-circular-references默认被设置成了false。也就是说你明明写了标准的三级缓存能解决的循环依赖应用启动时还是会直接报错。这是官方故意搞的收紧动作目的就是逼迫大家消除循环依赖而不是依赖容器兜底。老项目升级到 2.6 之后第一个崩溃点往往就在这里你需要在配置文件里显式打开那个开关或者动手重构代码。以我的经验看大多数业务代码里的循环依赖都是设计问题拆层不干净、领域对象互相强引用、或者把组件编排逻辑写成了环形调用。与其想着怎么绕过限制不如趁着升级把结构理清。第四Async、Transactional和循环依赖叠加时可能出现诡异问题。早期引用阶段已经生成了代理后续初始化阶段又因为 AOP 创建了一次新代理稍有不慎就会出现“注入的是早期代理容器保存的是最终代理”的偏差。虽然 Spring 对此做了很多保护但一旦触达边界逻辑排查起来极其折磨人。我的建议是写代码时优先保证 Bean 依赖是树状的不要依赖“容器帮我循环引用”。三级缓存是兜底方案不是设计常规。3. 代理与AOPSpring事务和权限失败的老问题都在这3.1 ProxyFactory底层JDK动态代理和CGLIB怎么选Spring AOP 的核心类是ProxyFactory它把通知Advice、切点Pointcut、目标对象Target组装起来最终生成一个代理对象。热点词里有人搜Spring 底层 AP 源码解析 ProxyFactory应该就是AOP的误写但指向的就是这块内容。ProxyFactory的使用很直接ProxyFactory factory new ProxyFactory(); factory.setTarget(userService); factory.addAdvice(new MyMethodInterceptor()); UserService proxy (UserService) factory.getProxy();getProxy()底层会走到DefaultAopProxyFactory它的判断逻辑很短也很关键如果目标类实现了接口并且没有强制使用 CGLIB就优先用 JDK 动态代理否则用 CGLIB。public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { return new ObjenesisCglibAopProxy(config); } return new JdkDynamicAopProxy(config); }JDK 动态代理是 Java 原生能力基于接口实现生成一个实现目标接口的匿名代理类调用方法时会进入InvocationHandler#invoke。它的问题也很明显目标类没有接口就没法代理。CGLIB 则是通过字节码生成目标类的子类通过重写方法实现代理所以不要求接口但代价是生成代理类更重且不能代理final方法。Spring Boot 2.x 之后默认spring.aop.proxy-target-classtrue也就是说即使你给了接口默认也走 CGLIB。之所以这么做是因为大量项目里的组件只有类没有接口统一用 CGLIB 反而省心。想改回 JDK 动态代理也可以配置一个参数的事但除非有特殊理由建议保持默认。3.2 事务/权限注解不生效的5个典型场景代理是 Spring 声明式功能的基石。事务注解、安全注解、异步注解全都靠代理在方法调用前后织入处理逻辑。一旦代理失效这些注解就会在关键时刻“装死”。我整理过一份高频踩坑清单几乎每个进阶开发者都会碰到其中一两项失效场景根本原因解决办法this.method()同类内部调用this是原始对象不是代理对象注解不经过代理链路注入自身拆到另一个 Bean用AopContext.currentProxy()private方法加TransactionalJDK 代理看接口CGLIB 无法重写 private 方法改为 publicfinal类或final方法CGLIB 无法生成子类或无法重写去掉 final异常被 catch 吞掉事务回滚只看有没有异常抛到代理边界吞掉后代理无感知让运行时异常继续往外抛自建切面里错误使用this拿的是原始目标对象而不是代理对象使用代理对象完成链路调用这里面最常见的是第一种。之前见过一个同事在OrderService里调用了同一个类的updateStatus方法方法上标了Transactional结果数据库出现部分更新成功、部分失败的数据。排查到最后发现不是事务没配而是调用链根本没走代理。这个问题非常容易踩因为代码读起来没有任何问题updateStatus确实加着注解。后来我们把对这个方法的调用拆到另一个独立 Service 里或者通过Lazy注入自身来解决。一句话总结凡是依赖注解横向增强的能力都必须保证方法调用从代理对象发起。这个理解一旦建立你在阅读 Spring Security 方法级权限时也会非常顺畅。3.3 Spring Security与OAuth2.1新的配置方式和授权玩法Spring Security 是同类代理机制在安全领域的代表也是热搜词里的常客。我建议大家关注一个趋势Spring Security 5.7 开始废弃WebSecurityConfigurerAdapterSpring Security 6 在 Spring Boot 3 环境下全面转型为组件化配置。也就是说你现在打开很多旧教程看到那种“继承WebSecurityConfigurerAdapter重写configure方法”的写法在主流新版本里已经不再适用。新写法是直接在配置类里定义SecurityFilterChainBeanBean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/public/**).permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth - oauth.loginPage(/login)) .csrf(AbstractHttpConfigurer::disable); return http.build(); }OAuth2.1 和 OAuth2.0 相比最大的变化是密码模式被正式移除授权码模式强制要求 PKCE不再支持隐式授权并新增了设备授权流程。对 Java 后端开发者来说这意味着以后对接第三方登录基本上就是授权码 PGCE 的主场。Spring Security 内置了对 OAuth2 客户端的支持你只需要配置好ClientRegistration和登录端点框架会帮你维护授权状态。热点词里还有Spring Security和OAuth2.1建议不要停留在“写个登录拦截器”的阶段至少要理解过滤器链的执行顺序以及AuthenticationManager、OAuth2AuthorizationServer这类核心组件的位置。4. 从Spring Boot到Spring Cloud版本选型与配置避坑4.1 Spring Boot版本到底怎么选2.3.x、2.6.x还是直接3.x热点词里有大量关于 Spring Boot 2.3.x、2.6.x 的搜索这很符合国内存量项目的真实状态。不少老系统还跑在 2.3.x 或 2.6.x 上新项目则普遍考虑要不要上 3.x。我给的选型建议是新项目直接选 Spring Boot 3.x老项目按“需求量力而行”。Spring Boot 2.6.x 是一个分水岭版本它引入了循环依赖默认关闭这个破坏性变化项spring.mvc.pathmatch.matching-strategy默认也改成了PATH_PATTERN_PARSER导致一些老配置在升级时崩溃。Spring Boot 3.x 最低要求 Java 17这本身就把不少存量项目挡在门外。如果你的团队还没升级 Java 17那 3.x 暂时不用想如果已经用了 Java 17 或更高版本可以直接开始熟悉 3.x因为后续新特性、新组件都会集中在这里。还有一个很多人问的IntelliJ IDEA 社区版怎么用 Spring Boot答案是社区版没有 Spring Initializr 支持但你可以去 start.spring.io 网页生成项目下载后直接用社区版打开写业务代码完全没问题。IDEA 社区版和旗舰版的主要差距在 Spring、Java EE 等框架的辅助功能上日常做 Spring Boot 练习社区版够用。关于手动下载 Spring 的 jar 包我的经验是除非项目完全脱离 Maven/Gradle否则不要手动下 jar。Spring Framework 5.3.41 这种版本号你直接去 Maven Central 仓库按路径找就行很多“官网下载”的搜索其实是老开发者习惯。真正要关心的是依赖坐标对不对、传递依赖有没有冲突这个比手动下一堆 jar 重要一万倍。4.2 数据源、WebSocket、Spring Boot Admin几个容易被忽视的细节数据源配置里热点词直接出现了一段 Druid 的配置remove-abandoned:true。这个参数是 Druid 连接池的物理超时连接回收机制含义是开启后Druid 会自动回收超过removeAbandonedTimeout默认 180 秒仍未归还的连接。它主要针对那些拿到连接后忘记关闭、导致连接泄漏的场景。但这里有个非常关键的坑removeAbandonedtrue要配合logAbandonedtrue才安全否则你只能看到连接被回收却不知道是哪个代码漏关的连接。而且生产环境不要轻易把这个参数开到特别激进的值否则长时间业务事务可能被误判为泄漏连接直接被强制回收引发更严重的故障。还有一个细节是removeAbandonedTimeout要大于你的慢查询阈值给正常业务留有足够余量。总之这个配置是治标手段真正要做好的是代码规范用try-with-resources或确保 finally 里关闭连接。WebSocket 的 yml 配置很多新手搜不到干货是因为 WebSocket 大量端点逻辑都要写在配置类里。单纯靠 yml 能配的是一些旁路参数比如路径前缀、序列化器、注册中心。真正项目里你需要一个WebSocketConfigurer实现类来注册HandlerConfiguration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler(), /ws/message) .setAllowedOriginPatterns(*) .addInterceptors(new MyHandshakeInterceptor()); } }yml 里经常需要配合调整的是容器的会话超时、消息大小、以及反向代理下的心跳超时这些点比“找到某个专门的 websocket yml 配置”更有实战价值。Spring Boot Admin 是我个人很喜欢的监控组件它能把多个 Spring Boot 应用的健康状态、内存指标、日志级别都集中到一个 UI 里。要注意的是被监控应用需要显式暴露spring.boot.admin.client相关配置还要注意服务端和客户端版本尽量对齐否则某些指标会采集不到。热点词里那个“Spring Boot 实现监控到底有哪些需求和功能”答案其实就藏在 Spring Boot Actuator Spring Boot Admin 这套组合里健康检查、指标采集、日志查看、配置刷新、环境信息。4.3 Spring Cloud Alibaba维护状态、Sentinel规则同步与若依配置“Spring Cloud Alibaba 是不是停更了”这个问题每年都会有人问热度一直不减。客观来说开源项目只要还在发布版本、还在出 release就不存在“停更”的问题。Spring Cloud Alibaba 整个生态一直保持迭代官方版本会跟随 Spring Cloud 主版本节奏更新。开发者真正要注意的是版本对应关系也就是 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者的版本号不是随便组合的必须按官方发布说明里的版本对应表来。尤其是 Nacos、Sentinel 这些组件升级之后API 和配置结构都可能变化不能盲目追新。Sentinel 做限流降级时规则通常存在控制台里。生产环境多节点部署时如果每个实例各自维护规则就会出现规则不一致。热点词里提到Sentinel datasource redis集群意思是把 Sentinal 规则放到 Redis 集群中通过 Redis Pub/Sub 让多个实例同步规则。这个做法的思路是控制台把推送的规则写入 Redis各实例监听配置变更后加载到本地内存。需要注意的点是规则变更的序列化格式要保持一致Redis 集群要保证主节点写入后的强一致策略同时对 Redis 本身的故障要做降级处理不能因为 Redis 不可用就把限流规则全部丢失。若依RuoYi的 Spring Cloud 版本配置文件是典型的 Nacos 集中式管理。本地运行时bootstrap.yml里要配置 Nacos 服务地址、命名空间和 group然后从 Nacos 拉取共享配置和扩展配置。如果你的项目基于若依二次开发最容易踩的坑是改了 Nacos 上的配置但本地不生效原因大多数是bootstrap.yml中的shared-configs或者extension-configs没有指向正确的 dataId。另外若依 Cloud 版本里各个服务之间的鉴权是通过网关统一处理的自己新加服务时容易被忽略导致权限校验过不去。这些经验属于“文档里一笔带过、实操时才痛”的典型问题。5. Spring AI时代Java开发者今年最值得关注的几个方向5.1 Spring AI 2.0和LangChain4j怎么选热点词里出现了两个阵营Spring AI 和 LangChain4j / LangGraph4j。这个问题我最近被问到的频率非常高。我的判断是如果你所在的团队已经是 Spring Boot 技术栈新项目打算把大模型能力接入 Java 后端优先考虑 Spring AI。原因很简单Spring AI 是 Spring 官方主导的生态与 Spring Boot 自动配置、Spring Cloud 微服务体系的融合最平滑。它的设计思路是把模型接入抽象成类似ChatClient这样的接口结构化输出、函数调用、Agent 等能力都在一步步补齐。LangChain4j 也不是不行它在社区生态上起步更早玩法更丰富很多参考案例和中文资料都很齐全。但要注意LangChain4j 并不是 LangChain 官方的 Java 移植版它是社区发起的 Java 实现虽然质量很高但在路线同步上不会像官方项目那样“跟得那么紧”。LangGraph4j 则更偏向 Agent 图编排适合把复杂工作流用节点、边的方式组织起来定位和 LangChain 的 LangGraph 对齐。选型时我建议先想清楚诉求。如果只是“封装一个聊天对话接口”两个框架都能满足选哪个都不会错。如果你要做复杂 Agent 编排、需要多步骤图结构LangGraph4j 会更顺手。如果你更看重长期维护、和 Spring 生态的一致性并且不想引入太多非官方依赖Spring AI 更合适。我自己在做一个企业内部的智能问答服务时用的是 Spring AI 2.0整体体验是初期资料少一点但 API 设计比较克制和 Spring Boot 配置的习惯非常像几乎没有学习焦虑。5.2 Spring AI实战思路从模型接入到AgentSpring AI 2.0 相比 1.0 在 API 层面做了不少调整建议直接看官方最新文档不要被网上的 1.0 教程带偏。接入模型时你只需要配置好模型厂家的依赖和参数然后通过ChatClient发起对话。以阿里云百炼平台的 Qwen3.7 这类模型为例需要引入 Spring AI Alibaba 的 starter然后在配置文件里声明模型服务商、API Key、模型名等信息。流程并不复杂核心是把 Spring 已经习惯的“配置自动装配”套到大模型服务上。spring: ai: alibaba: cloud: api-key: ${DASHSCOPE_API_KEY} model: qwen3.7代码层面Spring AI 2.0 的ChatClient使用起来非常接近函数式风格ChatClient client ChatClient.builder(chatModel).build(); String response client.prompt() .system(你是一名精通Spring源码的技术博主) .user(请用通俗语言解释三级缓存) .call() .content();Agent 化是今年更火的方向。Spring AI 的 Agent 组件简而言之就是让大模型可以“调用工具”你定义一批 Java 方法它们负责查询数据库、调用订单接口、执行某个算法然后把这些方法注册成Tools大模型在收到用户问题时会先判断是否需要调用工具再根据工具返回结果组织最终回答。这个机制在 Spring AI 里落地并不复杂核心是Function Calling的封装。注意点是你要给每个工具方法写清晰的功能描述和参数说明大模型才能准确选择工具否则可能出现“明明有查订单的工具它偏要瞎编一个订单号”的现象。5.3 Dify工作流转Java代码的现象与我的建议Dify 这类可视化 LLM App 平台现在很流行热点词里有人想把 Dify 工作转成 Spring AI Java 代码。这个思路本质上是想“用可视化编排搞清楚流程再用 Java 工程化落地”。我认为这在项目早期是合理的尤其适合快速验证 Agent 逻辑是否跑得通。但不要指望一个工具能一键生成完美的生产代码因为 Dify 工作流里很多节点是平台运行时提供的比如知识库检索、条件分支、HTTP 请求节点这些在 Java 代码里都有对应的原生写法生成项目给出的是一个很好的起点仍需人工梳理依赖和配置。从效率角度我的建议是先用 Dify 把业务链路跑通确认节点设计、提示词策略、工具调用的边界都能满足需求然后再切到 Spring AI 手工实现。翻译过程中最重要的是把“节点状态”这个概念理清Dify 里的每个节点之间传递的是字段组合它依赖平台内部的上下文管理Spring AI 里则需要你自己维护对话上下文和工具执行结果。如果你一开始就设计了复杂的全局状态转换时会相当痛苦。反过来如果你保持每个节点尽量无状态、只依赖用户输入和明确参数转换到 Java 代码就非常平滑。最后补充一个我自己的感受写这篇总结的时候我把近几年的 Spring 相关热点词过了一遍最大的感受是Spring 从来没有停下过演进的脚步但它的核心设计始终稳定。控制反转和 AOP 依然是整个地基循环依赖、代理机制仍然是每个 Java 开发者理解框架的必经之路。而 Spring AI 这类新方向恰恰是在这个旧地基上长出的新芽它没有推翻原有架构只是把“对象”扩展成了“模型能力”。如果你现在正卡在某个源码细节里或者纠结该不该升级版本我建议先退一步把 Bean 生命周期和代理机制这两座大山翻过去再看什么都是平的。这篇“上”部分先聊到这里后面那些更新、更细的方向等有机会我再继续整理。