首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
1,421 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
524 阅读
3
EasyExcel 实战:导出带图片的 Excel 完整方案
282 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
265 阅读
5
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
219 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
Spring AI
java虚拟机
JVM
Spring AI Alibaba
Java
保姆级教程
SpringBoot
Spring
WebFlux
MCP
大模型
Agent Skills
Nginx
Agent
Ubuntu
Mysql
Apache POI
自定义starter
Mybatis
响应式编程
Luca Ju
累计撰写
51
篇文章
累计收到
2
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
20
篇与
的结果
2026-03-16
Spring AI Alibaba 人工介入实战|Human-in-the-Loop 让 AI 更可靠
引言在构建AI智能体应用时,我们经常面临一个关键挑战:如何让AI在执行某些敏感操作前获得人工确认?Spring AI Alibaba框架提供了强大的人工介入(Human-in-the-Loop)机制,让开发者能够精确控制AI工具的执行流程,在关键节点引入人工审批环节。本文将通过一个完整的实战示例,详细介绍如何在Spring AI Alibaba应用中实现人工介入功能。什么是人工介入?人工介入是一种机制,它允许AI智能体在执行特定工具前暂停执行,等待人工审批后再继续。这种机制特别适用于:敏感操作:如数据删除、资金转账等内容生成:如文章发布、诗歌创作等需要质量把控的场景权限控制:某些需要特定权限才能执行的操作审计要求:需要记录人工决策过程的场景实战示例:诗歌创作的人工审批让我们通过一个具体的例子来理解人工介入Hook的使用。这个示例展示了如何让AI在创作诗歌前获得人工确认。1. 项目依赖配置首先,确保你的项目中包含了Spring AI Alibaba相关依赖:<dependencies> <!-- Spring AI Alibaba Agent Framework --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-agent-framework</artifactId> <version>1.1.2.0</version> </dependency> <!-- DashScope ChatModel 支持(如果使用其他模型,请跳转 Spring AI 文档选择对应的 starter) --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.1.2.0</version> </dependency> </dependencies>2. 代码实现解析步骤1:构建AI模型// 构建DashScope API对象 DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey(System.getenv("AliQwen_API")) .build(); // 创建聊天模型 ChatModel chatModel = DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .build();步骤2:配置工具public class PoetTool implements BiFunction<String, ToolContext, String> { public int count = 0; public PoetTool() { } @Override public String apply( @ToolParam(description = "The original user query that triggered this tool call") String originalUserQuery, ToolContext toolContext) { count++; System.out.println("Poet tool called : " + originalUserQuery); return "在城市的缝隙里, \n" + "一束光悄悄发芽, \n" + "穿过钢筋水泥的沉默, \n" + "在风中轻轻说话。 \n" + "\n" + "夜色如墨,却不再黑, \n" + "星星点亮了每一个角落, \n" + "我站在时间的边缘, \n" + "等一朵云,轻轻落下"; } public static ToolCallback createPoetToolCallback() { return FunctionToolCallback.builder("poem", new PoetTool()) .description("用来写诗的工具") .inputType(String.class) .build(); } public static ToolCallback createPoetToolCallback(String name, PoetTool poetTool) { return FunctionToolCallback.builder(name, poetTool) .description("用来写诗的工具") .inputType(String.class) .build(); } }步骤3:构建带有Hook的智能体// 这里我们配置了poem工具需要人工审批,并提供了审批时的描述信息。 Map<String, ToolConfig> approvalOn = Map.of( "poem", ToolConfig.builder() .description("请确认诗歌工具执行") .build() ); ReactAgent agent = ReactAgent.builder() .name("single_agent") .model(chatModel) .saver(new MemorySaver()) // 使用内存保存状态 .tools(List.of(createPoetToolCallback())) // 添加诗歌创作工具 .hooks(HumanInTheLoopHook.builder() .approvalOn(approvalOn) // 添加人工介入Hook .build()) .outputKey("article") .build();步骤4:创建会话配置String threadId = "user-session-001"; RunnableConfig config = RunnableConfig.builder() .threadId(threadId) .build();步骤5:执行并处理中断// 第一次调用 - 触发中断 Optional<NodeOutput> result = agent.invokeAndGetOutput( "帮我写一首100字左右的诗", config ); // 检查是否触发中断 if (result.isPresent() && result.get() instanceof InterruptionMetadata) { InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get(); System.out.println("检测到中断,需要人工审批"); // 获取工具反馈信息 List<InterruptionMetadata.ToolFeedback> toolFeedbacks = interruptionMetadata.toolFeedbacks(); for (InterruptionMetadata.ToolFeedback feedback : toolFeedbacks) { System.out.println("id: " + feedback.getId()); System.out.println("工具: " + feedback.getName()); System.out.println("参数: " + feedback.getArguments()); System.out.println("描述: " + feedback.getDescription()); } // 模拟人工决策(批准) InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state()); toolFeedbacks.forEach(toolFeedback -> { InterruptionMetadata.ToolFeedback approvedFeedback = InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) .build(); feedbackBuilder.addToolFeedback(approvedFeedback); }); InterruptionMetadata approvalMetadata = feedbackBuilder.build(); // 使用人工反馈恢复执行 RunnableConfig resumeConfig = RunnableConfig.builder() .threadId(threadId) .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata) .build(); Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig); if (finalResult.isPresent()) { System.out.println("执行完成"); // 因为创建智能体的时候,指定了outputKey,所以这里我们直接获取 Object article = finalResult.get().state().data().get("article"); System.out.println("最终结果: " + article); } }3. 执行流程分析这个示例的执行流程如下:触发阶段:用户请求AI创作诗歌中断阶段:AI检测到poem工具需要人工审批,暂停执行审批阶段:系统展示工具信息,等待人工决策恢复阶段:人工批准后,AI继续执行并生成诗歌完成阶段:返回最终结果高级特性多工具审批你可以为多个工具配置审批:Map<String, ToolConfig> approvalOn = Map.of( "poem", ToolConfig.builder().description("诗歌创作工具").build(), "delete", ToolConfig.builder().description("数据删除工具").build(), "publish", ToolConfig.builder().description("内容发布工具").build() );审批结果类型支持多种审批结果:APPROVED:批准执行REJECTED:拒绝执行MODIFIED:修改参数后执行最佳实践1. 明确审批策略只为真正需要人工确认的工具配置审批提供清晰的审批描述信息考虑审批的时效性2. 用户体验优化提供友好的审批界面支持批量审批操作记录审批历史便于审计3. 错误处理try { Optional<NodeOutput> result = agent.invokeAndGetOutput(request, config); // 处理中断和结果 } catch (GraphRunnerException e) { // 处理执行异常 log.error("智能体执行失败", e); }4. 状态管理// 使用合适的Saver .saver(new MemorySaver()) // 内存存储,适合开发测试 .saver(new RedisSaver()) // Redis存储,适合生产环境 .saver(new DatabaseSaver()) // 数据库存储,适合需要持久化的场景5. 执行结果拓展Spring Ai Alibaba还为我们内置了几个其他的HookSummarizationHook(消息压缩)当对话很长时,自动压缩对话历史,防止超出模型上下文限制ModelCallLimitHook(模型调用限制)防止Agent无限调用模型,控制成本另外,我们也可以自定义Hook,这部分内容如果大家感兴趣的话,后面可以单独介绍一下下~参考资料HumanInTheLoopHook API文档
2026年03月16日
23 阅读
0 评论
1 点赞
2026-03-13
使用Spring AI Alibaba构建智能体Agent
前言随着大语言模型(LLM)技术的快速发展,构建智能Agent应用变得越来越简单。本文将通过两个实际的代码示例,展示如何使用Spring AI Alibaba框架构建功能丰富的天气查询Agent,从基础的测试实现到生产级的完整应用。技术栈概述Spring AI Alibaba: 阿里巴巴开源的AI应用开发框架DashScope: 阿里云的AI模型服务平台React Agent: 基于ReAct(Reasoning and Acting)范式的智能代理示例一:SimpleTest - 快速入门添加核心依赖<dependencies> <!-- Spring AI Alibaba Agent Framework --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-agent-framework</artifactId> <version>1.1.2.0</version> </dependency> <!-- DashScope ChatModel 支持(如果使用其他模型,请跳转 Spring AI 文档选择对应的 starter) --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.1.2.0</version> </dependency> </dependencies>代码结构分析@Test void agentTest() throws GraphRunnerException { // 1. 初始化 DashScope API DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey(System.getenv("AliQwen_API")) .build(); // 2. 创建 ChatModel ChatModel chatModel = DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .build(); // 3. 定义天气工具 ToolCallback weatherTool = FunctionToolCallback.builder("get_weather", new WeatherTool()) .description("获取某个城市的天气") .inputType(String.class) .build(); // 4. 构建React Agent ReactAgent agent = ReactAgent.builder() .name("weather_agent") .model(chatModel) .tools(weatherTool) .systemPrompt("你是一个非常有帮助的助手") .saver(new MemorySaver()) .build(); // 5. 调用Agent AssistantMessage response = agent.call("上海今天天气怎么样?"); System.out.println(response.getText()); }核心特性简洁的配置: 通过Builder模式快速构建Agent工具集成: 使用FunctionToolCallback将自定义函数包装为Agent可调用的工具内存存储: 使用MemorySaver保存对话历史中文支持: 完整的中文提示词和工具描述自定义工具实现class WeatherTool implements BiFunction<String, ToolContext, String> { @Override public String apply(String city, ToolContext toolContext) { return city + "今天天气非常好!"; } }这个简单的工具类展示了如何将业务逻辑封装为Agent可调用的函数。示例二:RealAgent - 真实的智能体高级特性概览相比SimpleTest,RealAgent展示了更多生产级特性:精细的模型配置多工具协同结构化输出对话上下文管理核心代码解析1. 系统提示词设计String SYSTEM_PROMPT = """ 你是一位擅长说**天气冷笑话/谐音梗**的专业天气预报员。 你可以使用两个工具: - **get_weather_for_location**:用于获取指定地点的天气 - **get_user_location**:用于获取用户当前所在位置 如果用户询问天气,**必须先确认地点**。 如果从问题中能判断出他们指的是**自己所在的地方**, 就使用 **get_user_location** 工具获取他们的位置。 """;这个提示词体现了几个重要设计原则:角色定位: 明确Agent的身份和特色工具说明: 清晰描述可用工具的功能行为约束: 规定了工具使用的逻辑顺序2. 模型参数优化ChatModel chatModel = DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .defaultOptions(DashScopeChatOptions.builder() .model(DashScopeChatModel.DEFAULT_MODEL_NAME) .temperature(0.5) // 平衡创造性和准确性 .maxToken(1000) // 控制响应长度 .build()) .build();3. 多工具协同// 天气查询工具 ToolCallback getWeatherTool = FunctionToolCallback .builder("getWeatherForLocation", new WeatherForLocationTool()) .description("获取一个给定城市的天气") .inputType(String.class) .build(); // 用户定位工具 ToolCallback getUserLocationTool = FunctionToolCallback .builder("getUserLocation", new UserLocationTool()) .description("根据User Id获取用户位置") .inputType(String.class) .build();4. 结构化输出配置ReactAgent agent = ReactAgent.builder() // ... 其他配置 .outputType(ResponseFormat.class) // 指定输出格式 .hooks(humanInTheLoopHook) .build();5. 对话上下文管理RunnableConfig runnableConfig = RunnableConfig.builder() .threadId(Thread.currentThread().getId() + "") .build(); // 第一次调用 AssistantMessage response1 = agent.call("上海今天天气怎么样", runnableConfig); // 第二次调用(保持上下文) AssistantMessage response2 = agent.call("明天天气怎么样", runnableConfig);通过RunnableConfig的threadId实现多轮对话的上下文保持。总结通过这两个示例,我们可以看到Spring AI Alibaba框架在构建智能Agent应用方面的强大能力:SimpleTest展示了快速原型开发的能力,适合概念验证和学习RealAgent则创建了一个基础的 ReactAgent,接下来可以:探索更多的工具集成学习如何使用不同的 Checkpoint 实现对话持久化了解如何使用 Hooks 扩展 agent 功能学习如何创建多 agent 系统参考资料Spring AI Alibaba官方文档DashScope平台
2026年03月13日
52 阅读
0 评论
1 点赞
2026-03-03
详细解析Spring如何解决循环依赖问题
在日常的Spring开发中,循环依赖是一个高频出现的问题,也是面试中的核心考点。本文将从概念定义、问题表现、核心原理到源码层面,全方位解析Spring是如何通过三级缓存机制优雅地解决单例Bean的循环依赖问题。一、什么是循环依赖?循环依赖,指的是两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系。最典型的场景是Bean A依赖Bean B,同时Bean B又依赖Bean A。代码示例:@Component class A { // A依赖B @Resource private B b; } @Component class B { // B依赖A,形成循环 @Resource private A a; }在默认情况下,如果Spring不做特殊处理,项目启动时会抛出BeanCurrentlyInCreationException异常,提示存在循环依赖无法解决:二、Spring解决循环依赖的核心:三级缓存为了解决单例Bean的循环依赖问题,Spring设计了三级缓存机制,通过提前暴露半成品Bean的方式打破依赖闭环。三级缓存的定义缓存级别缓存名称作用一级缓存singletonObjects存放完全初始化完成的单例Bean(成品对象),供业务直接使用二级缓存earlySingletonObjects存放提前暴露的半成品Bean(已实例化但未完成属性填充和初始化)三级缓存singletonFactories存放ObjectFactory(对象工厂),这是一个函数式接口,仅在调用getObject()时才会创建Bean实例三、核心源码解析(基于 Spring 5.3.x)Spring处理Bean创建和循环依赖的核心逻辑集中在DefaultSingletonBeanRegistry类中,以下是关键源码及解析:public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { // 一级缓存:存放完全初始化好的单例Bean (成品) private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 三级缓存:存放Bean的工厂对象,用于创建提前暴露的Bean (半成品工厂) private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 二级缓存:存放提前暴露的Bean实例 (半成品,未完成属性填充和初始化) private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 记录当前正在创建的Bean名称,解决循环依赖的关键判断 private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); /** * 核心方法:获取单例Bean(解决循环依赖的入口) * @param beanName Bean名称 * @param allowEarlyReference 是否允许提前引用半成品Bean * @return 单例Bean实例 */ @Nullable public Object getSingleton(String beanName, boolean allowEarlyReference) { // 第一步:优先从一级缓存获取成品Bean Object singletonObject = this.singletonObjects.get(beanName); // 如果一级缓存没有,且当前Bean正在创建中(循环依赖的核心判断条件) if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 第二步:从二级缓存获取提前暴露的半成品Bean singletonObject = this.earlySingletonObjects.get(beanName); // 如果二级缓存也没有,且允许提前引用 if (singletonObject == null && allowEarlyReference) { // 加锁保证并发安全 synchronized (this.singletonObjects) { // 双重检查(防止多线程下重复创建) singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { // 第三步:从三级缓存获取ObjectFactory ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 通过工厂创建半成品Bean(提前暴露的核心操作) singletonObject = singletonFactory.getObject(); // 将半成品Bean放入二级缓存,同时移除三级缓存(避免重复创建) this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; } /** * 将Bean工厂放入三级缓存(提前暴露Bean的关键步骤) * 在Bean实例化后、属性填充前调用 */ protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } } /** * 将完全初始化的Bean放入一级缓存,清理二、三级缓存 * 在Bean初始化完成后调用 */ protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); this.singletonsCurrentlyInCreation.remove(beanName); } } }调试关键方法(建议收藏)在实际调试Spring源码时,建议重点关注以下核心方法的调用链路:getBean() - Bean获取的入口方法doGetBean() - 获取Bean的核心实现createBean() - 创建Bean的顶层方法doCreateBean() - 创建Bean的核心逻辑createBeanInstance() - Bean实例化(创建空对象)populateBean() - Bean属性填充(依赖注入的核心)四、循环依赖解决完整流程(以A和B为例)结合上文A依赖B、B依赖A的场景,我们拆解Spring解决循环依赖的完整执行流程(基于单例Bean + 字段注入):步骤 1:Spring启动,开始创建Bean A调用getBean(A),首先将A标记为「正在创建中」(singletonsCurrentlyInCreation.add("A"));通过反射创建A的空实例(ctro.newInstance()),此时A的属性b为null(实例化阶段);关键操作:调用addSingletonFactory()将A的ObjectFactory放入三级缓存;开始为A填充属性,发现依赖B,触发getBean(B)。步骤 2:创建Bean B(触发循环依赖)调用getBean(B),将B标记为「正在创建中」;通过反射创建B的空实例(ctro.newInstance()),此时B的属性a为null;调用addSingletonFactory()将B的ObjectFactory放入三级缓存;开始为B填充属性,发现依赖A,再次触发getBean(A)。步骤 3:解决循环依赖(从缓存获取A)执行getBean(A),检查一级缓存:A未完成初始化,无成品;检查标记:A处于「正在创建中」,符合循环依赖条件;检查二级缓存:无A的半成品实例;检查三级缓存:存在A的ObjectFactory,调用getObject()创建A的半成品实例;将A的半成品实例从三级缓存移至二级缓存;将半成品A返回给B,完成B的属性a填充。步骤 4:B完成初始化,反馈给AB完成属性填充,执行初始化方法(init-method/@PostConstruct);调用addSingleton(B),将B放入一级缓存,并清理其二、三级缓存;将成品B返回给A,完成A的属性b填充。步骤 5:A完成初始化,最终入池A完成属性填充,执行初始化方法;调用addSingleton(A),将A放入一级缓存,清理其二、三级缓存;移除A的「正在创建中」标记,循环依赖问题解决。补充说明:加入三级缓存后的Bean创建流程可参考下图:五、关键细节:为什么需要三级缓存?核心原因是为了支持AOP动态代理:延迟创建代理对象:ObjectFactory的getObject()方法中会调用getEarlyBeanReference(),该方法会判断当前Bean是否需要生成AOP代理。只有发生循环依赖时,才会提前创建代理对象;保证代理对象的唯一性:如果没有三级缓存,所有Bean都需要提前创建代理,破坏了Spring「初始化完成后再创建代理」的设计原则;避免重复代理:三级缓存的工厂模式确保代理对象只会被创建一次,放入二级缓存后就移除三级缓存,避免重复生成。如果仅使用二级缓存,所有Bean都必须在实例化阶段就创建代理,这会导致:代理对象创建时机提前,不符合Spring的初始化生命周期无循环依赖的Bean也会被提前代理,增加不必要的性能开销总结Spring通过三级缓存机制解决单例Bean的循环依赖问题,核心是提前暴露半成品Bean打破依赖闭环;三级缓存各司其职:一级缓存存成品、二级缓存存半成品、三级缓存存工厂(支持AOP延迟代理);解决循环依赖的核心流程是:实例化Bean → 放入三级缓存 → 填充属性触发循环 → 从缓存获取半成品 → 完成初始化放入一级缓存。
2026年03月03日
16 阅读
0 评论
3 点赞
2026-02-03
Jakarta Validation 优雅实现参数校验:从基础使用到自定义扩展
在后端开发中,参数校验是保障接口安全性和数据合法性的核心环节,硬编码的if-else校验逻辑不仅繁琐冗余,还会让代码可读性大打折扣。Jakarta Validation(原Java Validation)为我们提供了一套轻量、优雅的注解式参数校验方案,通过标准化的注解即可实现各类参数校验规则,大幅简化开发流程。本文将从基础注解使用、实战场景落地、统一异常处理到自定义校验注解,全方位讲解Jakarta Validation的使用技巧,结合实际业务代码示例,让你快速上手并灵活运用到项目中。一、核心校验注解速查Jakarta Validation提供了一系列开箱即用的校验注解,覆盖空值、长度、格式、数值、日期等绝大多数日常校验场景,核心注解及功能如下表所示,可直接作为开发速查手册:注解适用类型核心功能@NotNull所有类型字段值不能为null@NotBlank字符串不能为null,且去除首尾空格后长度大于0@NotEmpty字符串/集合/数组不能为null,且长度/元素个数大于0@Size(min, max)字符串/集合/数组长度/元素个数在[min, max]范围内@Pattern(regexp)字符串必须匹配指定的正则表达式@Email字符串必须符合合法的邮箱格式(支持自定义正则)@Min(value)数值类型数值必须大于等于value@Max(value)数值类型数值必须小于等于value@Positive数值类型必须为正数(大于0)@Negative数值类型必须为负数(小于0)@PositiveOrZero数值类型必须为正数或0@NegativeOrZero数值类型必须为负数或0@Future日期/时间类型必须是未来的时间@FutureOrPresent日期/时间类型必须是未来或当前时间@Past日期/时间类型必须是过去的时间@PastOrPresent日期/时间类型必须是过去或当前时间注解使用小技巧@NotBlank/@NotEmpty/@NotNull 区分:字符串优先用@NotBlank(过滤空白字符),集合/数组用@NotEmpty,非字符串非集合类型用@NotNull;注解组合使用:实际业务中可组合多个注解,如用户账号需同时满足「非空、正则匹配、长度限制」;默认提示语自定义:所有注解都支持message属性,用于自定义校验失败的提示信息,贴合业务场景。二、实战场景落地:三种核心使用方式Jakarta Validation的注解可根据参数传递方式灵活使用,核心分为「简单参数直接注解」「实体对象属性注解」两种核心场景,后者是项目中最常用的方式。场景1:GET请求简单参数,直接注解参数对于GET请求的URL拼接参数(如/user/get?name=test&id=1),可直接在接口方法的参数前添加校验注解,适用于参数数量少的简单场景。/** * 根据用户名和ID查询用户 * @param name 用户名(不能为空) * @param id 用户ID(必须为正数) * @return 用户信息 */ @GetMapping("/get") public CommonResult<UserVO> getUser( @NotBlank(message = "用户名不能为空") String name, @Positive(message = "用户ID必须为正数") Long id ) { UserVO user = userService.getByNameAndId(name, id); return CommonResult.success(user); }场景2:POST请求实体参数,注解+@Valid 触发校验对于POST/PUT请求,参数通常封装为实体对象(如新增/编辑用户的入参),只需在实体的属性上添加校验注解,并在接口方法的实体参数前添加@Valid(或@Validated)注解,即可触发整体校验逻辑。这是项目中最常用的方式,适合复杂参数的校验场景。步骤1:实体类添加校验注解@Data @Schema(description = "新增用户请求参数") public class UserSaveReqVO { @Schema(description = "用户编号(编辑时传,新增时不传)", example = "1024") private Long id; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") @NotBlank(message = "用户账号不能为空") @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户账号仅支持数字、字母组成") @Size(min = 4, max = 30, message = "用户账号长度为4-30个字符") private String username; @Schema(description = "用户密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456a") @NotBlank(message = "用户密码不能为空") @Size(min = 6, max = 20, message = "用户密码长度为6-20个字符") private String password; @Schema(description = "用户年龄", example = "25") @Min(value = 18, message = "用户年龄不能小于18岁") @Max(value = 60, message = "用户年龄不能大于60岁") private Integer age; @Schema(description = "邮箱", example = "test@example.com") @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不合法") private String email; }步骤2:接口方法添加@Valid 触发校验@PostMapping("/create") @Operation(summary = "新增用户") public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) { Long userId = userService.createUser(reqVO); return CommonResult.success(userId); }关键区别:@Valid vs @Validated@Valid:属于JSR-380标准注解,支持嵌套实体校验(如实体中包含另一个实体属性);@Validated:属于Spring扩展注解,支持分组校验(如新增和编辑用户时,同一实体的校验规则不同),可替代@Valid使用。嵌套实体校验示例:如果UserSaveReqVO中包含AddressVO实体属性,只需在AddressVO属性上添加@Valid+自身属性注解,即可触发嵌套校验。@Data public class UserSaveReqVO { // 其他属性... @Schema(description = "用户地址") @Valid // 触发嵌套校验 @NotNull(message = "用户地址不能为空") private AddressVO address; } @Data public class AddressVO { @NotBlank(message = "省不能为空") private String province; @NotBlank(message = "市不能为空") private String city; }三、全局异常拦截:统一处理校验失败结果当参数校验失败时,Jakarta Validation会自动抛出异常,不同场景抛出的异常类型不同:简单参数校验失败:抛出ConstraintViolationException;实体对象校验失败:抛出MethodArgumentNotValidException。为了让前端能接收到统一格式的错误返回,我们需要在项目中添加全局异常处理器,拦截这些校验异常,封装成统一的返回结果。全局异常处理器实现结合Spring Boot的@RestControllerAdvice和@ExceptionHandler实现全局异常拦截,统一返回格式(如包含错误码、错误信息的CommonResult)。@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 处理实体对象参数校验失败异常(@Valid + 实体注解) */ @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult<?> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { log.warn("参数校验失败:{}", ex.getMessage()); // 获取校验失败的第一条错误信息 String errorMsg = getFirstValidErrorMessage(ex.getBindingResult()); return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "请求参数不正确:" + errorMsg); } /** * 处理简单参数校验失败异常(直接注解参数) */ @ExceptionHandler(ConstraintViolationException.class) public CommonResult<?> handleConstraintViolation(ConstraintViolationException ex) { log.warn("参数校验失败:{}", ex.getMessage()); // 获取校验失败的第一条错误信息 String errorMsg = ex.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .findFirst() .orElse("参数校验失败"); return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "请求参数不正确:" + errorMsg); } /** * 提取BindingResult中的第一条错误信息 */ private String getFirstValidErrorMessage(BindingResult bindingResult) { // 优先获取字段级别的错误 if (bindingResult.hasFieldErrors()) { return bindingResult.getFieldErrors().get(0).getDefaultMessage(); } // 无字段错误则获取全局错误 if (bindingResult.hasGlobalErrors()) { return bindingResult.getGlobalErrors().get(0).getDefaultMessage(); } return "参数校验失败"; } }异常处理小技巧返回第一条错误信息:避免返回所有错误信息导致前端展示混乱,优先返回第一条校验失败的信息;统一错误码:参数校验失败统一使用400(BAD_REQUEST)错误码,符合HTTP协议规范;日志记录:记录异常日志便于问题排查,但无需打印完整堆栈(非运行时异常,属于业务异常)。四、高级扩展:自定义校验注解Jakarta Validation提供的默认注解无法覆盖所有业务场景(如「参数必须为指定枚举值」「手机号格式校验」「身份证号校验」),此时可通过自定义校验注解实现个性化的校验规则,步骤固定且可复用。实战示例:实现「参数必须为指定枚举值」的自定义注解以最常见的「参数必须是枚举中的某个值」为例,实现自定义注解@InEnum,支持校验参数是否为指定枚举的有效值。步骤1:定义自定义注解通过@Constraint指定校验器实现类,注解的属性可传递自定义参数(如枚举类),同时指定注解的适用目标(字段、参数等)。@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented // 指定校验器实现类:InEnumValidator @Constraint(validatedBy = {InEnumValidator.class}) public @interface InEnum { /** * 校验失败的提示语 */ String message() default "必须为指定枚举值:{value}"; /** * 分组校验(可省略,默认空) */ Class<?>[] groups() default {}; /** * 负载(可省略,默认空) */ Class<? extends Payload>[] payload() default {}; /** * 目标枚举类(必须实现ArrayValuable接口,提供枚举值数组) */ Class<? extends ArrayValuable<?>> value(); } /** * 枚举值获取接口,所有需要被@InEnum校验的枚举需实现此接口 * @param <T> 枚举值类型 */ public interface ArrayValuable<T> { /** * 获取枚举的所有值数组 */ T[] array(); }步骤2:实现注解校验器实现ConstraintValidator<A, T>接口,其中A为自定义注解,T为被校验的参数类型,重写initialize(初始化注解参数)和isValid(核心校验逻辑)方法。@Slf4j public class InEnumValidator implements ConstraintValidator<InEnum, Object> { /** * 枚举的有效值集合 */ private List<?> validValues; /** * 初始化:获取注解中指定的枚举类,提取其有效值 */ @Override public void initialize(InEnum annotation) { Class<? extends ArrayValuable<?>> enumClass = annotation.value(); // 获取枚举的所有实例 ArrayValuable<?>[] enumConstants = enumClass.getEnumConstants(); if (ArrayUtil.isEmpty(enumConstants)) { this.validValues = Collections.emptyList(); return; } // 提取枚举的有效值数组,转为List方便判断 this.validValues = Arrays.asList(enumConstants[0].array()); } /** * 核心校验逻辑 * @param value 被校验的参数值 * @param context 校验上下文 * @return true=校验通过,false=校验失败 */ @Override public boolean isValid(Object value, ConstraintValidatorContext context) { // 1. 参数为null时,默认校验通过(如需非空,可配合@NotNull注解) if (value == null) { return true; } // 2. 参数值在枚举有效值集合中,校验通过 if (validValues.contains(value)) { return true; } // 3. 校验失败,自定义提示语(替换{value}为实际枚举有效值) context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate( context.getDefaultConstraintMessageTemplate() .replace("{value}", validValues.toString()) ).addConstraintViolation(); return false; } }步骤3:枚举实现接口并使用自定义注解让目标枚举实现ArrayValuable接口,提供有效值数组,然后在实体/参数上添加@InEnum注解即可。/** * 性别枚举 */ public enum GenderEnum implements ArrayValuable<Integer> { MALE(1, "男"), FEMALE(2, "女"); private final Integer code; private final String name; GenderEnum(Integer code, String name) { this.code = code; this.name = name; } @Override public Integer[] array() { // 返回枚举的有效值数组 return new Integer[]{MALE.getCode(), FEMALE.getCode()}; } public Integer getCode() { return code; } } // 在实体中使用@InEnum注解 @Data public class UserSaveReqVO { // 其他属性... @Schema(description = "性别(1=男,2=女)", example = "1") @NotNull(message = "性别不能为空") @InEnum(value = GenderEnum.class, message = "性别必须为{value}") private Integer gender; }自定义注解开发通用规范注解属性规范:必须包含message/groups/payload三个基础属性(符合Jakarta Validation标准);空值处理:校验器中默认对null放行,如需非空可配合@NotNull注解,解耦「非空校验」和「业务规则校验」;提示语自定义:通过context.disableDefaultConstraintViolation()禁用默认提示语,实现动态替换(如替换枚举有效值);可复用性:自定义注解应设计为通用型(如手机号、身份证号校验注解),可在项目中全局复用。五、实用进阶技巧1. 分组校验:同一实体不同场景不同校验规则实际开发中,新增和编辑用户时,同一实体的校验规则可能不同(如新增时id无需传,编辑时id必须传),可通过分组校验实现,基于@Validated的分组属性。步骤1:定义分组标识接口/** * 校验分组 - 新增 */ public interface AddGroup { } /** * 校验分组 - 编辑 */ public interface EditGroup { }步骤2:实体注解指定分组@Data public class UserSaveReqVO { @Schema(description = "用户编号", example = "1024") @NotNull(message = "用户ID不能为空", groups = EditGroup.class) // 仅编辑时校验id非空 private Long id; @NotBlank(message = "用户账号不能为空", groups = {AddGroup.class, EditGroup.class}) // 新增+编辑都校验 private String username; }步骤3:接口指定分组触发校验// 新增用户:使用AddGroup分组 @PostMapping("/create") public CommonResult<Long> createUser(@Validated(AddGroup.class) @RequestBody UserSaveReqVO reqVO) { return CommonResult.success(userService.createUser(reqVO)); } // 编辑用户:使用EditGroup分组 @PutMapping("/edit") public CommonResult<Boolean> editUser(@Validated(EditGroup.class) @RequestBody UserSaveReqVO reqVO) { return CommonResult.success(userService.editUser(reqVO)); }六、总结Jakarta Validation通过注解式编程让参数校验从繁琐的硬编码中解放出来,实现了「校验规则和业务逻辑的解耦」,让代码更简洁、优雅、易维护。合理使用Jakarta Validation,不仅能提升开发效率,还能让接口的参数校验更规范、更健壮,为项目的稳定性提供保障。
2026年02月03日
10 阅读
0 评论
2 点赞
2026-01-16
Spring Retry 重试机制:优雅解决接口调用失败问题
在日常开发中,我们经常会遇到第三方接口不稳定、网络抖动导致的调用失败场景。很多人第一反应是在 try-catch 里写 for 循环重试,再搭配 Thread.sleep() 控制间隔——这种写法不仅冗余,还难以维护。今天给大家推荐 Spring Retry 框架,它基于 AOP 实现,能让你零侵入式地为方法添加重试功能,大幅简化代码!一、快速上手:三步集成 Spring Retry1. 添加 Maven 依赖Spring Retry 核心依赖 + AOP 依赖(因为其底层是 AOP 实现),这里推荐使用 2.0.12 稳定版本:<!-- Spring Retry 核心依赖 --> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>2.0.12</version> </dependency> <!-- AOP 依赖(Spring Boot 项目推荐此 starter) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>2. 启用 Spring Retry 功能在 Spring Boot 主启动类上添加 @EnableRetry 注解,一键开启重试功能:import org.springframework.retry.annotation.EnableRetry; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableRetry // 启用重试功能 @SpringBootApplication public class SpringRetryDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringRetryDemoApplication.class, args); } }3. 核心注解:@Retryable 标记重试方法在需要重试的方法上添加 @Retryable 注解,即可实现重试逻辑。基础用法import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Service public class RetryDemoService { // 标记该方法需要重试 @Retryable public void basicRetry() { int random = (int) (Math.random() * 10); System.out.println("当前随机数:" + random); // 模拟异常:随机数为偶数时抛出异常 if (random % 2 == 0) { throw new RuntimeException("随机数为偶数,触发异常"); } System.out.println("方法执行成功!"); } }基础用法说明未指定异常类型时,方法抛出任何异常都会触发重试。默认重试次数:3次(包含首次执行,实际重试 2 次)。默认重试间隔:1秒。当重试次数耗尽仍失败时,会抛出 ExhaustedRetryException 异常。二、进阶配置:灵活定制重试策略@Retryable 注解提供了丰富的属性,可根据业务需求精准控制重试逻辑。1. @Retryable 核心属性说明属性名作用示例value/retryFor指定触发重试的异常类型retryFor = RuntimeException.classinclude同 value,优先级更高include = {NullPointerException.class}exclude指定不触发重试的异常类型exclude = IllegalArgumentException.classmaxAttempts最大重试次数(包含首次执行)maxAttempts = 5backoff配置重试间隔策略@Backoff(delay = 1000, multiplier = 2)stateful是否有状态重试(异常信息保留)stateful = true2. 实战示例:指数退避重试需求:调用第三方接口时,仅在抛出 RuntimeException 时重试,最大重试 5 次,重试间隔按 1s → 2s → 4s → 8s 指数增长(避免高频重试压垮接口)。import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RetryDemoController { @GetMapping("/test/retry") // 仅对RuntimeException重试,最大5次,指数退避间隔 @Retryable( retryFor = RuntimeException.class, maxAttempts = 5, backoff = @Backoff(delay = 1000, multiplier = 2.0) ) public String testRetry() { int random = (int) (Math.random() * 10); System.out.println("[" + System.currentTimeMillis() + "] 当前随机数:" + random); if (random % 2 == 0) { throw new RuntimeException("随机数为偶数,触发重试"); } return "调用成功!随机数:" + random; } }三、兜底处理:@Recover 重试失败后的恢复逻辑当重试次数耗尽仍失败时,我们需要一个兜底方法来处理最终的失败(比如记录日志、返回默认结果),这时候就需要 @Recover 注解。1. @Recover 用法规则恢复方法和 @Retryable 方法应该在同一个类中。后续参数需和 @Retryable 方法的参数列表完全一致。返回值需和 @Retryable 方法的返回值完全一致。2. 实战示例:重试失败后返回默认结果import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Service public class RetryDemoService { @Retryable( retryFor = RuntimeException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000) ) public String callThirdPartyApi(String param) { System.out.println("调用第三方接口,参数:" + param); // 模拟接口调用失败 throw new RuntimeException("第三方接口超时"); } // 重试失败后的恢复方法 @Recover public String recover(RuntimeException e, String param) { System.out.println("重试次数耗尽,执行兜底逻辑!异常信息:" + e.getMessage()); System.out.println("请求参数:" + param); // 返回默认结果 return "接口调用失败,已触发兜底策略"; } }四、注意事项(避坑指南)@Retryable 不能修饰 private 方法:因为 Spring AOP 无法代理 private 方法,重试逻辑会失效。避免同类方法调用:如果在同一个类中调用 @Retryable 方法(非代理调用),重试逻辑也会失效。重试策略要合理:避免设置过短的间隔和过多的重试次数,增加服务压力。五、总结Spring Retry 凭借注解化的方式,让我们摆脱了手写重试逻辑的繁琐,实现了代码的优雅和解耦。核心要点如下:三步集成:加依赖 → 启注解 → 标记方法。灵活配置:通过 @Retryable 属性定制重试次数、间隔、触发异常。兜底保障:通过 @Recover 处理重试失败的最终逻辑。掌握 Spring Retry,能让你在应对不稳定接口时更加从容,大幅提升系统的健壮性!
2026年01月16日
16 阅读
0 评论
2 点赞
1
2
3
4