news 2026/10/3 10:25:30

Spring核心机制实战解析:IOC、三级缓存、AOP、微服务与AI

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring核心机制实战解析:IOC、三级缓存、AOP、微服务与AI

很多Java开发者手头都有过这种感觉:Spring框架用了好几年,Controller、Service、Repository写得很溜,但别人一问你“三级缓存到底是怎么回事”“@Transactional为什么有时候会失效”,心里就发虚。Spring从二十年前的轻量级容器,一步步长成了今天覆盖Spring Boot、Spring Cloud、Spring Security、Spring AI的庞大家族,它早就不只是一个框架,而是Java后端这整片生态的地基。这篇文章我想用这些年实际开发踩坑攒下来的经验,把Spring的控制反转、依赖注入、AOP代理、三级缓存、事务失效这些核心机制讲明白,再把Spring Boot的WebSocket配置、监控、接口拆分、微服务跨语言接入、Spring AI集成大模型这些实战场景串起来。不论你是刚学Java的萌新,还是写了几年业务想补底层原理的工程师,这篇内容应该都能帮你把脑子里那些零散知识点拼成一张完整的图。

1. 先搞清楚Spring的本质:控制反转到底反转了什么

控制反转(IOC)Spring面试必问,但很多人理解停留在“不用new对象了”这个层面。其实IOC反转的是依赖关系的管理权:在没有Spring的时候,A类要用B类,得自己在构造方法里new B,或者在方法里new B,这种主动去查找依赖的行为叫正向控制。责任全部压在编写A的开发者身上,改B的构造方法时,所有new过B的代码全要跟着改。Spring做的东西很简单:把对象的创建、装配、生命周期管理全收归容器,开发者只在配置里或注解里声明“我需要什么”,容器在合适的时机把现成对象送过来。

1.1 容器和Bean的世界观

只要用过Spring,就一定写过ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class),拿到context之后再getBean出来用。ApplicationContext就是那个容器,容器里面装的是一个个Bean。Bean这个词听着玄,实际上就是一个被Spring接管生命周期的普通Java对象。容器启动时干的事是扫描类、解析BeanDefinition、实例化、属性填充、执行初始化方法,最后把成品放进单例池。

用生活里的场景理解:容器像一个招聘平台,Bean是求职者。调用方公司不亲自去人才市场扫人,而是把岗位需求(依赖声明)发给平台,平台按需求匹配好简历(元数据),把人培训好(初始化),然后直接交付给公司。公司不知道候选人是谁招的、怎么培训的,只知道自己拿到一个满足条件的员工。这个“不知道、不关心、不干预”的状态,就是控制被反转了。

1.2 依赖注入的三种姿势,哪种最靠谱

注入方式有三种:构造器注入、Setter注入、字段注入。构造器注入是Spring官方最推荐的方式:把依赖作为构造参数传入,用final修饰,依赖一旦注入就不可变,对象创建时必须把所有依赖备齐,不存在半成品状态。项目里我基本推荐这种写法,配合Lombok的@RequiredArgsConstructor,代码很干净。

Setter注入适合那种可选依赖,比如一个组件的日志对象、缓存客户端,没有也能跑。字段注入(也就是在属性上用@Autowired)最省事,但在我自己项目里反而比较警惕。字段注入的问题是依赖隐藏,看接口签名根本不知道这个类需要哪些协作对象;而且单元测试时没法直接new对象传依赖,得借助Spring容器跑测试,慢且重。顺便说一句,为什么@Autowired写在字段上也能生效?因为Spring有一个BeanPostProcessor叫AutowiredAnnotationBeanPostProcessor,在Bean初始化阶段扫描字段上的注解,通过反射把依赖塞进去,这个机制也是理解Spring一堆“神秘注入”的关键。

1.3 Bean的生命周期,面试官常问的完整链路

一个单例Bean从容器里出生到销毁,完整链路大概是:实例化(构造方法)→ 属性填充(依赖注入)→ Aware回调(BeanNameAware、ApplicationContextAware这类)→ BeanPostProcessor的postProcessBeforeInitialization → 初始化方法(@PostConstruct、InitializingBean、自定义initMethod)→ BeanPostProcessor的postProcessAfterInitialization → 放入单例池供使用 → 上下文关闭时走销毁方法。

注意两个重要节点:属性填充阶段做的是注入,如果这个Bean启用了AOP代理,真正生成代理对象的时机是在postProcessAfterInitialization阶段,也就是初始化完成之后,容器把Bean放进单例池之前。这一点和后面要讲的三级缓存强相关。很多开发者以为AOP是在实例化时套上去的,其实不是,AOP代理的生成是对原始Bean的“后处理”,先有原始Bean对象,再通过代理包装它,最后对外暴露的是代理。

2. 三级缓存与循环依赖:Spring最被低估的机制

Spring的三级缓存是面试高频中的高频,也是整个容器设计里最有味道的一个细节。三级缓存严格来说是三个Map,存的是不同的Bean状态:一级缓存存完整成品,二级缓存存已经实例化但还没属性填充完的半成品,三级缓存存的是生产早期引用的工厂。很多人背了“三级缓存”这四个字,却说不清三级为什么是三级,下面把它拆开讲。

2.1 循环依赖是怎么产生的

循环依赖就是A依赖B,B又依赖A。比如一个订单服务要调用户服务查用户信息,用户服务某个场景又反过来要查订单信息,两个Bean互相引用。如果完全不处理,容器创建A时发现缺B,去创建B,B又发现缺A,再回来找A,A还没创建完,于是死锁。

这里要区分注入方式。构造器注入的循环依赖基本是救不了的,因为构造器注入在实例化阶段就把依赖“焊死”了,A构造时需要B实例,B构造时需要A实例,两边都处于实例化中,谁都没法先给出完整对象。处理循环依赖的其实是Setter注入或字段注入:实例化A时就算依赖B还没准备好,也可以先把A的“壳”创建出来,后面再填B的属性,这就给了容器操作空间。

2.2 三级缓存到底在工作时经历了什么

假设A和B互相引用,且都用Setter或字段注入,Spring的处理流程是这样的:

  1. 创建A,调用构造方法,A对象在内存里被new出来了,此时属性都还是空的。
  2. 把A封装成一个ObjectFactory放进三级缓存,相当于“A的半成品在这里,谁需要谁来取”。
  3. 开始给A填充属性,发现要注入B,于是转向创建B。
  4. 创建B,同样实例化出B对象,放入三级缓存,开始填B的属性,发现要注入A。
  5. B去三级缓存找A,发现A的ObjectFactory存在,调用工厂取出A的早期引用(此时可能是原始A,也可能是代理对象),把这个引用注入给B。
  6. B的属性填充完成,B走完初始化流程,作为一个完整Bean放入一级缓存。
  7. 回到A的创建流程,A从缓存的引用里获取到B(此刻B已经是完整的),把B注入A。
  8. A走完初始化流程,放入一级缓存,循环依赖就此破解。

关键在第五步:B拿到的不是最终状态的A,而是提前暴露的早期引用。这恰恰是缓存设计的核心。一级缓存不能提前放半成品,否则并发场景下别的线程拿到未初始化完的Bean,直接引发空指针;二级缓存放的是已经实例化但未初始化的半成品,有总比没有好;三级缓存存的是工厂,工厂的意义在于延迟决定“到底返回原始对象还是代理对象”。

2.3 为什么需要三级而不是两级,以及哪些循环依赖救不了

如果去掉三级缓存,只有一级和二级,能不能解决循环依赖?非AOP场景下其实也能解决,B需要A时直接把早期的原始A放进二级缓存给B就行。真正的问题是AOP:假如A这个Bean需要被代理,代理时机在Bean初始化完成之后。如果只有二级缓存,A在被B引用时还没有生成代理对象,B拿到的就是原A,后续A虽然生成了代理放入一级缓存,但B里持有的A还是原始对象,AOP增强在B内部完全失效。这显然不能接受。

三级缓存里放的是ObjectFactory,工厂会在“被拿去注入”的那一刻才执行,在工厂逻辑里可以判断:如果Bean需要代理,就返回代理引用;如果不需要代理,就返回原始对象。这样B拿到的一定是最终的、正确的对象形态。所以说,三级缓存本质上是把代理的生成时机处理延后到了“被循环依赖引用的瞬间”,保证无论是否被提前引用,最终暴露的对象都是同一个完整形态——代理或原始Bean保持一致。

有三个场景循环依赖是救不了的:构造器注入的循环依赖,仍是死局;prototype作用域的Bean因为不缓存,无法提前暴露;还有Spring Boot 2.6之后默认把spring.main.allow-circular-references设成false,直接拒绝循环依赖。所以别把三级缓存当成业务代码里写循环依赖的借口,这个机制是Spring在极端场景下的自保手段,不是推荐方案。

3. AOP底层与动态代理:ProxyFactory在偷偷做什么

AOP全称面向切面编程,说白了就是在不修改业务代码的前提下,给某个方法前后织入额外逻辑。Spring里最常见的用法是@Transactional、@Async、@Cacheable,这些注解能“凭空生效”,靠的就是AOP。但AOP不是魔法,它是动态代理:Spring创建一个代理对象,拦截目标方法调用,在执行真实方法前先处理增强逻辑,再把调用转发给原始对象。

3.1 JDK代理还是CGLIB,Spring怎么选

JDK动态代理要求目标类实现至少一个接口,代理对象基于接口生成,只能拦截接口里声明的方法。CGLIB的原理则是生成目标类的子类,通过继承和字节码增强来覆写方法,不要求实现接口,但目标类和方法不能是final的。这俩方式各有边界,Spring在纯Framework环境下会遵循一个默认逻辑:目标类有接口就JDK代理,无接口用CGLIB。Spring Boot从2.x开始把spring.aop.proxy-target-class默认设为true,所以很多开发者实际跑起来看到的代理对象其实是CGLIB的。

这一点的实际影响是什么?如果你用JDK代理注入的是一个具体实现类,运行时会报BeanNotOfRequiredTypeException,因为容器里Bean类型是代理接口。这也是Spring官方建议“依赖注入优先面向接口编程”的原因之一:接口存在,代理才能透明地替换实现,代码才能真正解耦。

3.2 ProxyFactory背后的职责链

Spring AOP里有两个容易搞混的概念:Advice是增强逻辑本身,比如“方法执行前打印日志”“方法异常后发送告警”;Pointcut是切入点,描述“哪些类的哪些方法要被增强”;Advisor把两者绑在一起——一个Advisor就是“在哪个地方执行什么增强”。ProxyFactory是AOP代理的枢纽,它根据Advisor信息判断用JDK还是CGLIB创建代理,并把Advice织入代理的方法调用流程,遇到匹配Pointcut的方法就执行增强链,不匹配就直接放行。

生产环境里常用@Aspect声明切面,切面里的@Before、@AfterReturning、@Around最终都会被Spring转换成Advice并包装成Advisor,整个过程由@EnableAspectJAutoProxy引入的AnnotationAwareAspectJAutoProxyCreator触发。这个Creator本身是一个BeanPostProcessor,在1.3节说的postProcessAfterInitialization阶段发现Bean匹配切面时,就会返回代理对象而不是原始对象,这也是AOP为什么能“无侵入”的原因。

3.3 手写一个简化版Spring容器,理解才有快感

“手写Spring”是很多人的进阶学习法,我自己也带着团队做过一个mini版,核心流程非常清晰,而且对理解IOC和AOP极有帮助。整体就四步:扫包、注册BeanDefinition、反射实例化、依赖注入。下面这段代码展示的是最核心的扫包注入部分,真实Spring在此基础上加了几十倍细节,但骨架完全一致。

public class MiniSpringContext { private Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); public MiniSpringContext(String basePackage) throws Exception { // 1. 扫描包下所有类,过滤出带有 @MiniComponent 注解的类 Set<Class<?>> classes = doScan(basePackage); // 2. 实例化所有组件,按类名首字母小写作为BeanName放入Map for (Class<?> clazz : classes) { String beanName = lowerFirst(clazz.getSimpleName()); Object instance = clazz.getDeclaredConstructor().newInstance(); singletonObjects.put(beanName, instance); } // 3. 遍历所有Bean,对标注 @MiniInject 的字段执行注入 for (Object bean : singletonObjects.values()) { for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(MiniInject.class)) { field.setAccessible(true); String dependName = lowerFirst(field.getType().getSimpleName()); field.set(bean, singletonObjects.get(dependName)); } } } } public <T> T getBean(Class<T> clazz) { return (T) singletonObjects.get(lowerFirst(clazz.getSimpleName())); } }

实际Spring的doCreateBean远没有这么简单,中间有BeanDefinition解析、构造器选择、属性类型转换、各种PostProcessor回调、代理判断,但核心思想就是这几步。我强烈建议有时间的读者照着这个骨架自己写一遍,写完之后你再看refresh()方法源码,障碍会小很多。

4. Spring Boot实战边界:WebSocket、监控、对外接口

Spring Boot把Spring框架的复杂配置变成了约定大于默认,但这不代表不用懂配置。实践中有一批场景特别容易踩坑,比如WebSocket的集成方式、Actuator监控到底开哪些端点、对外接口应该放哪个服务。这几个问题在社区里天天被问,统一聊一聊。

4.1 WebSocket集成与yml配置的正确姿势

Spring Boot接WebSocket有两条路线。一条是JSR-356标准风格的@ServerEndpoint,需要先注册ServerEndpointExporter,否则注解不会被扫描,这个导出器要在配置类里显式声明Bean;另一条是Spring封装的WebSocketHandler注册方式,通过实现WebSocketHandler接口并注册到WebSocketHandlerRegistry,类似拦截器的握手处理器HandshakeInterceptor可以用来做身份校验、传递参数。

很多文章在yml里写一堆spring.websocket配置,其实Spring Boot对WebSocket的原生配置并不多,真正在yml里调整的通常是内嵌服务器端口和WebSocket相关的buffer上限。例如:

server: port: 8080 tomcat: websocket: max-text-message-size: 10240

这点很容易被忽略:如果你的服务器是Tomcat,并且用的是@ServerEndpoint这套标准API,那么消息大小限制改的是server.tomcat.websocket.max-text-message-size,把它理解成“Tomcat作为WebSocket服务器的参数”就对了。另一个常见问题是同源校验和跨域,生产环境一般建议在握手拦截器里做token校验,顺手把来源Origin校验做掉。

4.2 用Actuator把监控指标暴露出来

Spring Boot应用想被监控,第一步是引入spring-boot-starter-actuator,然后决定暴露哪些端点。默认情况下只有health是暴露的,metrics、info、loggers这些都是关闭的。配置很直白:

management: endpoints: web: exposure: include: health,info,metrics,loggers endpoint: health: show-details: always

监控不是看个health就完事,实际生产里更常用的是把/metrics接入Prometheus,加一个micrometer-registry-prometheus依赖,然后暴露prometheus端点,Prometheus服务就能定期抓取JVM内存、线程、GC等指标。如果业务需要自定义监控指标,比如统计某个核心接口的调用量,可以用MeterRegistry注册一个Counter:

@RestController public class OrderController { private final Counter orderCreateCounter; public OrderController(MeterRegistry registry) { this.orderCreateCounter = Counter.builder("order.create.total") .description("订单创建总量") .register(registry); } @PostMapping("/order") public void createOrder() { orderCreateCounter.increment(); // 业务逻辑 } }

这个Counter上报到Prometheus之后,配合Grafana面板就能看到实时曲线。很多团队把监控做成“上线之后再补”,我的建议是写业务接口时顺手把关键埋点加上,等线上出问题再想指标就晚了。

4.3 对外接口究竟放哪:独立服务还是业务模块

要不要为第三方单独拆一个服务,这是架构评审经常吵起来的问题。我的判断标准很简单:看调用方是谁、接口是否承担开放平台的职责。如果是给企业外部第三方调用的OpenAPI——有独立鉴权、有调用频率控制、有版本演进需求、要对接口单独做文档和SLA承诺——那拆独立服务或者至少拆成独立模块是必要的。因为开放接口的生命周期和内部接口完全不一样,外部调用方不可能跟着你内部重构同步升级,接口版本要兼容很久,独立的服务边界能把这些复杂度隔离掉。

如果只是内部服务间调用,比如订单服务要调用户服务的查询用户接口,那就老老实实放在用户服务这个业务模块里,用OpenFeign或者DegRibbon这类服务间调用组件去访问,没必要为这种内部接口单独部署一套服务。还有一种融合方案:内部接口照旧放在各业务服务,额外加一层API网关,所有对外请求都经过网关做鉴权、限流、协议转换,转发到内部服务。这种方式既不用为每一个第三方接口单独起服务,又保持了对外形象的统一可控。所以我一般不推荐拍脑袋说“一律拆服务”或“一律放模块”,而是按调用方和生命周期来判断。

4.4 IDEA社区版 + Spring Boot的开发方案

网上总有新人问IDEA社区版能不能做Spring Boot,我的答案是不仅能做,日常开发完全够用。社区版和旗舰版最直观的差距在于没有Spring Initializr快速建项目,也没有Spring Assistant这种代码提示插件。解决方式很简单:打开https://start.spring.io,选好Spring Boot版本、Java版本、需要的依赖,生成压缩包,解压后在IDEA里用File -> Open把项目导入,社区版照样识别Maven工程,依赖照拉、代码照写、运行照跑。唯一要适应的就是新建类、配置类等模板不会那么智能,但写起来没差的。

Spring Framework 5.3.41这类具体版本的jar也无需手工从官网下载,在pom.xml里让spring-boot-parent管理版本,写上<version>5.3.41</version>告诉Maven强制使用这个框架版本即可,依赖传递时会自动拖下来对应jar。这也算是我特别想强调的一点:现代Java开发里任何jar都走Maven或Gradle依赖管理,直接在本地手工拷jar的行为,在稍微正规一点的团队里都是不被接受的。

5. Spring Cloud微服务体系:Python应用怎么融入

微服务体系发展到今天,已经很少是纯Java的独角戏了,很多团队都有Python做的算法服务、数据服务,甚至Node.js写的BFF层。如何把这些异构应用平滑融入以Spring Cloud Alibaba为底座的技术体系,是真实的架构问题。

5.1 Spring Cloud Alibaba的组件版图

先说清楚Spring Cloud Alibaba里面到底有哪些核心组件,分别解决什么问题。Nacos承担两个角色:服务注册发现和配置中心,服务启动后把自己的IP端口注册到Nacos,调用方通过服务名去找实例,配置中心则管理各个环境的配置文件。OpenFeign是声明式HTTP客户端,在Java侧把远程接口定义成一个interface,加注解就能像调用本地方法一样调远程服务。Spring Cloud Gateway是API网关,负责外部流量接入、路由转发、统一鉴权。Sentinel做限流和熔断,保护核心服务不被突发流量打垮。Seata解决分布式事务,跨服务的数据一致性由它协调。

对应到Python应用,关键点在于:Nacos、Gateway、Sentinel这些基础设施都不是Java内部物,它们是独立运行的中间件,通过HTTP或SDK对外提供服务,Python应用完全可以直接利用它们。

5.2 跨语言接入的三种务实方案

第一种方案是Python服务作为独立应用,不直接参与Java服务发现,只对外提供REST API,Java侧通过OpenFeign或RestTemplate按HTTP地址调用。这个方案工作量最小,适合那些调用量不大、数据边界清晰的算法服务。缺点是没有自动服务发现和负载均衡,如果Python侧扩容多个实例,Java侧需要手动维护地址列表。

第二种方案是让Python服务注册到Nacos,参与统一服务发现。Nacos提供了OpenAPI,也有Python SDK,Python服务启动时调用注册接口把自己的地址上报,并发送心跳续约。Java侧Feign仍然用服务名去调用,负载均衡交给Feign或LoadBalancer处理。这套方案在调用方视角和纯Java服务没有差别,但对Python服务的容错、健康检查、优雅下线有要求,属于工程上“更重但更正规”的做法。

第三种方案是彻底解耦,通过消息队列中转。Java和Python服务不直接互相调接口,而是把需要协作的数据以事件的形式投递到RocketMQ或Kafka,各自消费各自关心的事件。这种异步解耦模式牺牲了一部分实时性,换来了极大的弹性:某一方挂了,消息在队列里堆积,恢复后还能继续消费;某一方要升级,也不用考虑对方是否同步修改接口。如果业务链路本身具备异步特征,我强烈建议优先考虑这种方案。

实际选型取决于团队边界和业务确定性:算法团队能力弱一点的,先走HTTP方案;职业化程度高、流量大的,走Nacos注册;上下游有明确事件化场景的,直接上MQ。

5.3 一个企业办公用品管理系统的服务划分

顺便把“基于Spring Boot的企业办公用品管理系统”这种常见的业务项目展开讲一下。这类系统的本质是“采购-库存-领用-审批-统计”的闭环,核心模块大概有:用户与部门管理、角色权限、办公用品分类和SKU管理、采购入库流程、申请单和审批流、出库记录、库存预警、以及报表统计。如果做成单体,单Spring Boot应用里按模块分包就可以,没必要一上来就上微服务;如果是团队协同开发、需要独立部署和扩展,可以按“认证服务、库存服务、审批服务、报表服务”来切。

报表服务是单独拆出来的典型例子,因为报表查询普遍慢、吃内存,和线上交易库放在一起容易互相拖累。审批服务适合依赖工作流引擎,比如Flowable或Activiti,这也是为什么很多类似项目把审批单独做成一个服务。至于库存和采购,业务上强绑定,拆开反而增加分布式事务复杂度,合并放在同一个服务里更合理。这也是我一直说的:拆不拆服务,不取决于“为了微服务而微服务”,而是看团队的交付节奏和运维成本。

6. Spring Security:认证授权不是堆Filter

Spring Security在Spring Boot生态里基本上是安全方案的默认选项,但很多人对它敬而远之,觉得过滤器配起来绕。它的原理其实并不复杂:Spring Security本质上是一组Filter组成的链路,在请求到达Controller之前把认证和授权处理完。

6.1 过滤器链的请求旅程

一个典型请求进来后,会依次经过SecurityContextHolderFilter(或旧版的SecurityContextPersistenceFilter)、CsrfFilter、UsernamePasswordAuthenticationFilter、ExceptionTranslationFilter、AuthorizationFilter,最后才放行到业务Controller。UsernamePasswordAuthenticationFilter负责从登录请求里取出用户名密码,交给AuthenticationManager做校验,成功后把Authentication对象放进SecurityContextHolder;AuthorizationFilter则拿当前用户拥有的权限和请求要求的权限做比对,不匹配就直接抛出AccessDeniedException。ExceptionTranslationFilter的角色很特殊,它不参与业务判断,专门把安全异常转成403或401响应。

新版本Spring Security 6里不少类名和过滤链顺序有调整,但链路思维是完全一致的。我自己排查安全相关问题时的第一动作永远是打印当前安全过滤链的装配结果,看看自定义过滤器到底插在哪个位置。过滤器的顺序错位,是绝大多数“配置看着对但就是不生效”的根源。

6.2 JWT无状态认证落地要点

单体Session模式里,服务端存会话,客户端拿JSESSIONID,每次请求由SessionRepository还原登录态。JWT方案则是把用户信息签名后发给客户端,服务端不再存任何会话状态,只要验签通过就认为用户合法。

Spring Security整合JWT的经典做法:写一个OncePerRequestFilter,在请求进来时取Header里的Authorization,解析并验签JWT,解析出用户信息和权限后封装成Authentication放进SecurityContextHolder,最后放行。注意这个自定义Filter要加在UsernamePasswordAuthenticationFilter之前,这样后续授权过滤器才能从SecurityContextHolder里读到当前用户。

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil) { this.jwtTokenUtil = jwtTokenUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = resolveToken(request); if (token != null && jwtTokenUtil.validateToken(token)) { Authentication auth = jwtTokenUtil.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } chain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearer = request.getHeader("Authorization"); if (bearer != null && bearer.startsWith("Bearer ")) { return bearer.substring(7); } return null; } }

JWT的无状态特性带来一个副作用:无法主动让token失效。一旦用户的JWT泄露,只能等它过期。所以生产环境一般会把过期时间设短一些(比如半小时),再加一个refresh token机制来续期。我看过不少项目直接把token有效期设成7天,等于把一个长生命周期的钥匙交到客户端手里,风险很大。

6.3 常见的安全配置坑

第一个坑是把Spring Security的过滤器链配置成对所有请求都放行,结果等于没安全。正确做法是定义好哪些是公开端点(比如登录、注册、健康检查),其余一律走authenticated。第二个坑是CSRF的取舍。如果没有浏览器端会话Cookie场景,可以关掉CSRF,但如果你的系统需要兼容浏览器表单提交,关闭CSRF等于把CSRF攻击的窗户打开。第三个坑是CORS配置和Security配置的顺序问题,如果CORS过滤器没有排在认证过滤器前面,预检请求会被安全链路拦住。第四个坑是方法级安全忘了开,@PreAuthorize这类注解需要@EnableMethodSecurity开启才生效,Spring Boot默认是不开的。

还有一个非常隐蔽的问题:自定义过滤器里用ThreadLocal存了用户信息,但没有在请求结束时清理,线程池复用线程时下一个请求就“继承了”上一个用户的身份。SecurityContextHolder默认就是ThreadLocal,所以一定要养成立即设置、请求结束清空的好习惯,否则线上会出现用户串号这种灾难。

7. Spring AI:Java开发者的大模型集成新选择

Spring AI是这几年Spring生态里最让人兴奋的新成员。它把大语言模型的接入抽象成了一组统一的API,开发者不需要关心是OpenAI、QWen还是Claude,只需要针对ChatModel编程,就像以前针对DataSource写SQL一样。

7.1 Spring AI与LangChain的对比

LangChain在Python生态里很火,是一个专门为构建LLM应用而生的框架,提供Prompt模板、链式调用、Agent工具、向量检索等能力。Spring AI很多人叫它“Java版的LangChain”,但我觉得它和LangChain有一个本质区别:Spring AI不是一个孤立的AI框架,它长在Spring Boot的自动配置、依赖注入、配置管理、可观测性之上。你用它接大模型,不用额外搭一套配置中心,不用自己处理连接池,不用改造成本去适配监控体系,原来那套Spring工程化经验全部可以平移过来。Spring AI也把RAG的核心元件抽象成了VectorStore、EmbeddingModel、DocumentReader等接口,和Spring生态无缝衔接。

7.2 接入QWen的实操配置

接入通义千问的百炼平台非常简单,关键是用DashScope的OpenAI兼容端点。Spring AI支持把OpenAI的base-url指到DashScope上,这样不用换底层调用逻辑。配置长这样:

spring: ai: openai: base-url: https://dashscope.aliyuncs.com/api/v1 api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus

代码里注入ChatModel,直接调用即可:

@Service public class QwenService { private final ChatModel chatModel; public QwenService(ChatModel chatModel) { this.chatModel = chatModel; } public String chat(String message) { return chatModel.call(message); } }

注意Spring AI版本迭代比较快,2.0之后API有过调整,有的事务方法也从ChatClient.Builder变化到了别处。我自己的经验是:不要凭旧版本写法套新版本,直接看当前锁定版本的官方文档或参考测试用例,Spring系列的测试代码永远是最权威的用法示范。模型名称也要以百炼控制台实际开通的白名单模型为准,你搜到的资料说qwen-plus,实际你的账号里可能叫qwen-max或者更新一代的模型名。

7.3 从Dify工作流到Spring AI Agent化

不少团队先用Dify这类LowCode平台快速搭出了AI应用原型,跑通之后又想把核心链路迁回Java服务内部,减少对第三方平台的依赖,这条迁移路线在GitHub上也已经有一些案例和技术方案了。从技术上讲,Dify里的一条工作流是由多个节点组成的,比如大模型节点、知识检索节点、条件分支节点、工具调用节点,它们之间用连线表示数据流转。

迁移到Spring AI时,映射关系非常直观:大模型节点对应ChatModel或ChatClient的调用,知识检索节点对应VectorStore查询加内容组装,工具调用节点用Spring AI的@Tool注解声明Java方法即可。Dify里的条件分支,在Java里通常变成一个路由方法,根据模型返回结果或者业务字段决定走哪个Handler。如果工作流里有Agent节点,对应的是Spring AI里基于模型Function Calling的Agent循环:模型决定要调用哪个工具、传入什么参数,Java框架帮我们执行并回传结果,模型再根据结果继续推理,直到任务完成。

我自己实践下来的建议是:Dify适合做POC验证和产品学习,线上要求高时,把核心链路由Dify迁到代码里,换来的不只是性能和可控性,还有便于测试、便于版本化、便于自定义逻辑这几点实打实的工程优势。

8. 面试题与进阶路线:别再背题,理解它

Spring相关的面试题见过太多人拿“八股文”去背,但面试官追问两句就露馅。我个人觉得比较好的学习方式是把面试题当成线索,每道题背后都对应一个底层机制,理解了机制,题目怎么变你都能接住。

8.1 高频面试题的底层逻辑

整理几个最常见的题和它们对应的底层原理,这些都是真实面试里反复出现的:

常见面试问题底层机制
Spring Bean的作用域有哪些?默认是什么?singleton、prototype、request、session,默认singleton
三级缓存解决什么问题?单例Bean循环依赖时的早期引用暴露
@Transactional在什么场景下失效?自调用不走代理、非public方法、异常被catch、方法内部try-catch吞异常、传播行为配置错误
Spring AOP和AspectJ区别是什么?Spring AOP基于动态代理运行期织入,AspectJ是编译期/加载期织入
Spring容器启动流程大概是什么?refresh()方法:配置解析、BeanFactory准备、BeanDefinition注册、实例化单例
BeanFactory和ApplicationContext有什么区别?ApplicationContext是增强版BeanFactory,加了事件、资源、国际化、自动注册PostProcessor等能力

我见过最可惜的面试场景是候选人对“事务失效”背得滚瓜烂熟,但问他“为什么自调用会导致事务失效”时卡住了。原因其实很简单:@Transactional是通过AOP代理生效的,自调用时方法内部走的是this的方法,没有经过代理对象,增强逻辑自然没执行。这个例子也生动地说明,Spring里很多现象都和“代理”这个核心概念有关,理解了代理,一大片问题都通了。

8.2 源码阅读路线

如果想读源码,我不建议从第一行开始往后啃。推荐的路线是抓住主线:先找AnnotationConfigApplicationContext的refresh()方法,这个方法就是整个容器启动的骨架,读它会自然看到invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization这些关键节点;然后顺着finishBeanFactoryInitialization走到finishBeanFactoryInitialization里的getBean,再走到doCreateBean,三级缓存的核心逻辑就在getSingleton和doCreateBean的循环依赖处理里。

读完容器创建之后,再读AOP代理的部分,重点看AbstractAutoProxyCreator这个类,Spring的AOP就是通过这个BeanPostProcessor在后置处理阶段创建代理的,之前说的三级缓存里的代理逻辑也在这里面呼应。最后读Spring事务,事务是AOP在运行时最经典的应用,核心类就是TransactionInterceptor。

读源码的时候有个小技巧:先读测试用例。Spring仓库里每个模块都有大量测试,测试用例里展示的用法就是设计者期望的标准用法,从测试用例进入源码比从网上搜零散博客高效得多。

8.3 我的几个实践建议与避坑心得

最后聊几条日常实践里真正有价值的建议。第一,写新代码时优先用构造器注入,依赖关系一目了然,也别为了省事在实现类内部new协作对象,那等于把控制权又拿回去了。第二,遇到@Transactional不生效的问题,第一反应是检查调用链:是不是同类里的自调用,是不是方法不是public,是不是异常被吞了。这几条检查完,大部分事务问题都能定位。第三,升级Spring Boot版本时不要一下跳太多版本,比如2.x直接跳3.x会面临javax到jakarta命名空间的全量迁移,低版本配置文件里的参数在更高版本可能被废弃,升级成本往往比想象中高。第四,Spring AI相关的项目版本变化快,锁定版本后一定要以该版本对应的文档为准,别只看网上教程。

我早些年看Spring源码时一度很焦虑,觉得类太多记不住,后来才明白,Spring的核心套路就是那么几个:容器管Bean,代理做增强,过滤器链做安全,自动配置做Boot。把这条主线抓住,剩下的能力都是在这个骨架上生长出来的。遇到问题先回源码里找调用栈,再对着文档确认用法,这种习惯比刷一百道面试题都管用。希望这篇内容能帮你把自己的Spring知识串成网,而不是继续停留在会写接口但是心里没底的阶段。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 10:25:30

.NET 10 Minimal APIs实战:从微服务到边缘场景的选型与避坑指南

做.NET服务端开发这些年&#xff0c;我写过不少传统MVC控制器&#xff0c;也见过太多为了一个几十行逻辑的文件硬凑出Controller、Service、Repository三层结构的项目。后来Minimal APIs出现&#xff0c;我第一反应是终于可以少写几张样板代码了。到了.NET 10正式版&#xff08…

作者头像 李华
网站建设 2026/10/3 10:25:12

AI判断模型Jev:只做判断不写代码,如何与Codex搭档并本地部署

最近开发者圈子里经常刷到一个名字&#xff1a;Jev。大多数人第一次听说它的时候都会愣一下——一个AI模型&#xff0c;不写代码、不做生成&#xff0c;只负责“做判断”&#xff0c;这算什么本事&#xff1f;在代码生成模型满天飞的阶段&#xff0c;这种反向定位反而让它火得特…

作者头像 李华
网站建设 2026/10/3 10:24:43

基于SpringBoot+SSM的校园智能物流管理系统设计与部署实战

先说个身边最常见的场景&#xff1a;校园里的菜鸟驿站一到下课时间就排长队&#xff0c;取件报号全靠嗓门&#xff0c;错拿漏拿全靠运气。很多学校试着用微信群、Excel表格来管快递&#xff0c;效果嘛&#xff0c;懂的都懂——消息刷屏、表格过期、高峰期直接乱成一锅粥。这个基…

作者头像 李华
网站建设 2026/10/3 10:24:23

DeepSeek Harness桌面端安装配置与内网Skill部署全指南

1. 桌面端来了&#xff0c;但先别急着双击安装包DeepSeek Harness 出官方桌面端这件事&#xff0c;在圈子里传开的速度比我预想得快。之前大家用 DSH 基本靠命令行&#xff0c;或者挂在编辑器插件里跑&#xff0c;配置环境、拉依赖、调 API Key&#xff0c;一套流程下来对不写代…

作者头像 李华
网站建设 2026/10/3 10:24:08

零基础小白副业实操:写作、剪辑、闲鱼三招稳定赚钱

你打开这篇文章&#xff0c;多半是想找一个能真正落到口袋里的副业&#xff0c;不是那种看完热血沸腾、做两天就放弃的。我干自由职业六年&#xff0c;前两年其实都是副业状态&#xff0c;白天上班晚上折腾&#xff0c;试过刷单、试过配音、试过卖课&#xff0c;最后真正稳定出…

作者头像 李华
网站建设 2026/10/3 10:23:34

车载Android开发进阶:从AAOS架构到Framework实战指南

做车载Android开发这些年&#xff0c;有个特别有意思的现象&#xff1a;很多在手机App领域干了三四年的工程师&#xff0c;简历一投到车载方向&#xff0c;面试官问的第一个问题往往不是"你用过什么框架"&#xff0c;而是"你知道Android Automotive OS和普通And…

作者头像 李华