• Cursor 技巧 1:Interactive Feedback MCP

    在使用 Cursor 时,用户会面临着每月 500 次 “快车道” 请求的限制,一旦用超,需要排队等待或者额外付费。 一个名为 “Interactive Feedback MCP” 的开源项目为Cursor用户提供了一种创新的节省快速请求的方式:旨在通过在单次Token消耗周期内实现无限轮次的追问和反馈,从而大幅提升付费额度的使用效率。其核心原理是在Cursor准备结束当前对话时拦截该信号,并允许用户输入新的反馈或指令,使对话在保持完整上下文的情况下继续进行,而不是开启新的、消耗额外Token的会话。 什么是 Interactive Feedback MCP Interactive Feedback MCP (MCP: Model-Coordinator-Provider) 是一个本地运行的服务,它与Cursor的MCP协议集成,充当用户与AI模型之间的交互协调者。当AI完成一次响应后,此MCP会介入,询问用户是否需要进一步修改或有新的问题,并将用户的反馈无缝地融入当前对话流中。这意味着最初的一次请求所消耗的Token可以支持后续多轮的迭代优化,据称能将500次请求有效交互扩展数倍。 项目地址:https://github.com/noopstudios/interactive-feedback-mcp 安装步骤 依赖软件 Python 3.11 或更新版本。 uv(Python包管理器)。安装方法如下: Windows: pip install uv Linux/Mac: curl -LsSf https://astral.sh/uv/install.sh | sh 获取代码 clone interactive-feedback-mcp 仓库: git clone https://github.com/noopstudios/interactive-feedback-mcp.git 下载源代码也可以 下载完成后进入到代码目录。 安装 mcp 在 interactive-feedback-mcp 代码目录下执行如下命令: uv sync 该指令会创建一个虚拟环境并安装各种包(因此执行耗时也比较长)。 执行完成后,执行如下指令运行 MCP 服务器(还是在interactive-feedback-mcp 代码目录下): uv run server.py 在 Cursor 中配置 MCP 通过 Settings > Tools & MCP > Add Custom MCP 添加 MCP : { “mcpServers”: { “interactive-feedback-mcp”: { “command”: “uv”, “args”: [ “–directory”, “/Project/interactive-feedback-mcp根据安装路径修改”, “run”, “server.py” ], “timeout”: 600, “autoApprove”: [ “interactive_feedback” ] } } } 我这里安装后MCP没能立即启动,将 Cursor 退出重启后才会执行成功。执行成功后会有绿色状态标识。 在 Cursor 中配置 Rule 因为期望 Cursor 在每次请求结束前自动调用 MCP 服务,需要配置下 Cursor 的 Rule (Settings > Rules, Skills, Subagents > Rules > New User Rule ):

    [阅读更多...]
  • Spring AI 智能体模式 2 : Anthropic 智能体 Skill

    Spring AI 新增了对 Anthropic 智能体 Skill 的支持 — 这类模块化能力可让 Claude 直接生成实际文件,而非单纯的文本描述。启用该 Skill 后,Claude 能生成可直接下载使用的 Excel 电子表格、PowerPoint 演示文稿、Word 文档和 PDF 文件。 局限性说明 Anthropic Skill 的实现方案仅适用于其自研的 Claude 系列模型,存在以下局限性: 无移植性: 该 Skill 依赖 Anthropic 的代码执行能力和 Files API 基础设施,无法在其他大模型平台(OpenAI、 Gemini 等)使用; 专属类依赖: 需使用 AnthropicChatOptions 、 AnthropicSkill 、 AnthropicSkillsResponseHelper 等 Anthropic 专属类,而非 Spring AI 的通用接口; 模型限制: 仅 Claude Sonnet 4、Sonnet 4.5 和 Opus 4 模型支持该 Skill ; 文件有效期: 通过 Anthropic Files API 生成的文件,24 小时后会自动过期; 公测功能属性: Skill API 需携带公测版本请求头,且接口规范仍可能持续迭代。 Anthropic Skill 与通用智能体 Skill 的选型建议 Spring AI 支持两种不同的智能体 Skill 实现方案,可根据业务需求选择: 选择 Anthropic 原生Skill API 的场景 需使用预构建的文档生成能力,支持 Excel、PowerPoint、Word、PDF 等格式; 希望 Skill 在沙箱化的安全云端环境中运行; 需实现团队共享的、工作空间级别的 Skill; 业务已确定基于 Claude 系列模型开发; 希望由 Anthropic 负责管理 Skill 的执行基础设施 选择通用智能体 Skill (spring-ai-agent-utils) 的场景 需要让 Skill 适配多款大模型(OpenAI、 Anthropic、 Gemini 等); Skill 需要访问本地资源、网络或自定义软件包; 希望将 Skill 与应用代码一起打包,进行版本控制; 需要对 Skill 的执行环境拥有更高的控制权; 优先考虑方案的可移植性,避免厂商锁定。 能否同时使用两种方案? 可以。在同一应用中,可通过 Anthropic 原生 Skill 实现文档生成,同时借助通用智能体 Skill 完成其他可移植的业务能力开发。二者定位不同、功能互补,可协同使用。

    [阅读更多...]
  • Spring AI 智能体模式 1 : Agent Skills – 模块化,可复用的能力

    独立于大模型,可在自有环境运行的 Skills。 Agent Skills 是由 指令、脚本和资源构成的模块化文件夹。 AI Agent 可以发现并按需加载 Skills。相较于将知识硬编码到 prompts 中 或者为每个任务开发专用工具,Skills 提供了一种灵活扩展智能体能力的实现方式。 Spring AI 的相关实现将 Agent Skills 引入到了 java 生态,实现了大模型的可移植性 — 只需定义一次 Agent Skills,即可在 OpenAI、Anthropic、Google Gemini 及其他所有得到支持的大模型中复用。 什么是 Agent Skills? Agent Skills 是模块化的能力,表现为以 yaml 格式封装的 markdown 文件。 每个 Skill 对应一个文件夹,文件夹中需要包含一个 SKILL.md 文件。在 SKILL.md 中至少需要配置 名称 和 描述 两类元数据,同时还包含指导 Agent 完成特定任务的指令。此外 Skills 文件夹中还可以整合脚本(scripts)、模板(templates)和参考文档(references)等内容。 其中的前置元数据不仅支持简单的字符串值,还支持复杂的 yaml 结构(列表和嵌套对象等)以支持高级使用场景。 my-skill/ ├── SKILL.md # 必选:指令 + 元数据 ├── scripts/ # 可选:可执行代码 ├── references/ # 可选:参考文档 └── assets/ # 可选:模板、各类资源 Skills 采用渐进式披露机制实现上下文的高效管理: 发现阶段: Agent 启动时,仅加载所有可用 Skill 的名称和描述,保留能够判断 Skill 相关性的核心信息即可; 激活阶段: 当待执行任务与某一 Skill 的描述匹配时,Agent 才会将 Skill 的 SKILL.md 文件中的全部指令加载到上下文中; 执行阶段: Agent 按照指令完成任务,并根据具体情况按需加载引用文件或者执行内置代码 通过这个机制,即使注册上百个 Skill , 也能保证上下文窗口的轻量化,避免资源冗余。 Tips: 关于 Agent Skills 的更多内容可以参考 Skill 官方指南 为什么要在 Spring AI 中使用 Agent Skills 无缝集成 只需要注册少量工具,即可将 Agent Skill 集成到现有 Spring AI 应用中 — 且无需对架构做任何修改。

    [阅读更多...]
  • SpringAI 02 – Chat Client API

    ChatClient 提供了与AI模型交互的fluent API。它同时支持同步和流式编程模型。 ChatClient fluent API拥有构建传递给AI模型的提示(Prompt)的组成部分的方法。提示包含指导AI模型输出和行为的指令文本。从API的角度来看,提示由一系列消息组成。 AI模型处理两种主要类型的消息: 用户消息,即用户的直接输入。 系统消息,由系统生成以指导对话。 这些消息通常包含占位符,这些占位符在运行时会根据用户输入进行替换,以自定义AI模型对用户输入的响应。 还可以指定一些提示选项,例如: AI模型的名称,即要使用的AI模型的名称。 温度设置,控制生成输出的随机性或创造性。 这些功能使得ChatClient成为一个强大的工具,允许开发者以灵活的方式与AI模型进行交互,并通过定制化的提示和消息来优化AI模型的响应。 创建 ChatClient 使用 ChatClient.Builder 对象创建 ChatClient。你可以为任何 ChatModel SpringBoot 自动配置获取自动配置的 ChatClient.Builder 实例,或者手动创建一个。 使用自动配置的 ChatClient.Builder 在最简单的用例中,Spring AI 通过 SpringBoot 自动配置创建了一个 ChatClient.Builder 实例原型,使用时可以将其注入到类中。以下是一个简单的示例,用于检索对简单用户请求的字符串响应。 @RestController class MyController { private final ChatClient chatClient; public MyController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder.build(); } @GetMapping(“/ai”) String generation(String userInput) { return this.chatClient.prompt() .user(userInput) .call() .content(); } } 在这个简单的例子中,用户输入设置了用户消息的内容。call() 方法向 AI 模型发送请求,content() 方法将 AI 模型的响应作为字符串返回。 手动创建 ChatClient 可以通过设置属性 spring.ai.chat.client.enabled=false 来禁用 ChatClient.Builder 自动配置。当同时使用多个聊天模型时,这个配置会很有用。然后,为每个需要的 ChatModel 手动创建一个 ChatClient.Builder 实例: ChatModel myChatModel = … // 通常通过自动装配获得 ChatClient.Builder builder = ChatClient.builder(this.myChatModel); // 或者使用默认构建器创建一个 ChatClient 实例: ChatClient chatClient = ChatClient.create(this.myChatModel); ChatClient Fluent API ChatClient fluent API 允许使用重载的 prompt() 方法以三种不同的方式创建提示(Prompt),以启动fluent API: prompt(): 此方法不接受任何参数就可以开始使用fluent API,允许您构建用户、系统和其它提示(prompt)部分。 prompt(Prompt prompt): 此方法接受一个 Prompt 实例作为参数,这个参数可以是一个非fluent API 创建的 Prompt 实例。 prompt(String content): 这是一个快捷方法,类似于之前重载的方法,它接受用户文本内容作为参数。 ChatClient 响应 ChatClient API 提供了几种格式化 AI 模型响应内容的方法。 返回 ChatResponse AI 模型的响应是一个由类型 ChatResponse 定义的复杂结构。ChatResponse中包含关于生成响应的元数据,并且还可以包含多个响应,称为 Generations,每个Generation都有自己的元数据。元数据还包括用于创建响应的token数量(每个token大概是 3/4 个单词)。这个信息很重要,因为托管的 AI 模型会根据每个请求中使用的token数量收费。 以下示例展示了如何获取包含元数据的 ChatResponse 对象的过程,ChatResponse实例是在 call() 方法后执行 chatResponse() 方法获得: ChatResponse chatResponse = chatClient.prompt() .user(“Tell me a joke”) .call() .chatResponse(); 返回Entity 有时会希望将返回的字符串映射为某个特定的实体类的对象。entity() 方法提供了这个功能。

    [阅读更多...]
  • SpringAI 01 – AI概念

    模型 Model 模型是旨在处理和生成信息的算法,通常模仿人类认知功能。通过从大型数据集中学习模式和洞察力,这些模型可以进行预测、生成文本、图像或其他输出,增强各行业的应用。 当前有许多不同类型的 AI 模型,每种模型会适配特定的用例。虽然 ChatGPT 及其生成式 AI 功能通过文本输入和输出吸引了用户,但许多模型和公司提供了多样化的输入和输出。在 ChatGPT 之前,许多人对 Midjourney 和 Stable Diffusion 等文本到图像的生成模型着迷。 下表根据输入和输出类型对几种模型进行了分类: Spring AI 目前支持将输入和输出处理为语言、图像和音频的模型。上表中的最后一行,即接受文本作为输入并输出数字的那行,通常被称为嵌入文本,代表 AI 模型中使用的内部数据结构。Spring AI 支持嵌入(Embedding)以支持更先进的用例。 像 GPT 这种模型的独特之处在于它们的预训练性质,正如 GPT(Chat Generative Pre-trained Transformer)中的 “P” 所表示的。这种预训练特性将 AI 转变为一种通用开发工具,但不需要更多的机器学习或模型训练背景。 提示 Prompt 提示是基于语言的输入的基础,用于引导 AI 模型产生特定输出。对于熟悉 ChatGPT 的人来说,可能提示看起来只是输入到对话框中并发送到 API 的文本。然而,它远不止于此。在许多 AI 模型中,提示的文本不仅仅是一个简单的字符串。 ChatGPT 的 API 在一个提示中会有多个文本输入,每个文本输入都被分配了一个角色。例如,有系统角色,它告诉模型如何表现并设置交互的上下文。还有用户角色,通常就是用户的输入。 创建有效的提示既是科学也是艺术。ChatGPT 是为人类对话而设计的。这与使用 SQL 等特定的数据库查询语言来进行 “提问” 有很大不同。与 AI 模型进行交流必须要像与另一个真实的人交谈一样。 这种交互方式非常重要,以至于出现了像 “提示工程” 这样类似一门学科的名词。有大量提高提示有效性的技术正在涌现。花时间精心设计提示可以极大地改善最终输出的结果。 共享提示已成为一种常用的做法,并且在这个主题上正在进行积极的学术研究。作为创建有效提示可能有多违反直觉的一个例子(例如,与SQL比较),最近的一篇研究论文发现,最有效的一个提示可以用这样的语句开头: “深呼吸,一步一步地做这个”。这应该可以让你了解为什么语言如此重要。不幸的是我们还不完全理解如何最有效地利用这项技术,即使是在之前的迭代版本中(如 ChatGPT 3.5),更不用说正在开发的新版本了。 提示模板 Prompt Template 创建有效提示涉及建立请求的上下文,并将请求中指定的部分内容替换为让用户输入的值。 这个过程使用传统的基于文本的模板引擎进行提示创建和管理。Spring AI 为此使用了 OSS 库中的 StringTemplate。 例如下面就是一个简单的提示模板: Tell me a {adjective} joke about {content}. 在 Spring AI 中,提示模板可以类比为 Spring MVC 架构中的 “视图”。提供一个模型对象(通常是 java.util.Map)来填充模板中的占位符。这样“渲染” 后的字符串就成为提供给 AI 模型的提示内容。 发送给模型的提示的具体数据格式有很大差异。最初是简单的字符串,现在提示已经发展到包括多个消息,一条消息中的每个字符串代表模型的一个不同角色。 嵌入 Embedding 嵌入是文本、图像或视频的数值表示,用于捕获输入之间的关系。 嵌入通过将文本、图像和视频转换为浮点数数组(称为向量)来工作。这些向量被用来捕获文本、图像和视频的含义。嵌入数组的长度称为向量的维度。 通过计算两段文本的向量表示之间的数值距离,应用程序可以确定用于生成嵌入向量的对象之间的相似性。 作为探索 AI 的 Java 开发人员,不需要理解这些向量表示(vector representations)背后的复杂数学理论或具体实现,只需要对它们在 AI 系统中的作用和功能有基本的了解就足够了,特别是当你将 AI 功能集成到应用程序中时。 嵌入在像检索增强生成(RAG Retrieval Augmented Generation)模式这样的实际应用中特别相关。它们使数据能够表示为语义空间中的点,这类似于欧几里得几何中的二维空间,但维度更高。这意味着就像欧几里得几何中平面上的点根据其坐标可以近或远一样,在语义空间中,点的接近程度反映了含义的相似性。对应相似主题的语句在这个多维空间中位置更接近,就像图形上彼此靠近的点一样。这种接近有助于进行文本分类、语义搜索甚至产品推荐等任务,因为它允许 AI 根据它们在这个扩展的语义景观中的 “位置” 来识别和分组相关概念。

    [阅读更多...]
  • 基于生成式注解为类添加toString方法

    在组内讨论时,有同事提建议在把对象写到日志中时最好直接输出对象不要做任何加工,也就是尽量调用对象自己的toString()方法,不要用JsonKit.toJson(obj)这样先把对象转为json字符串再输出的写法。 这个建议不是没有道理的,jackson和fastjson这些json工具将对象序列化为字符串时会有一个自动检查推测的过程,在这个过程中做了如下事情: 所有public方法,带返回值,符合“getXxx”(或“isXxx”,如果返回boolean会被称为“isgetter”)命名约定的成员方法被推测存在名字为“xxx”的属性(属性名按照bean命名约定推测,即开头大写字母转成小写)。 所有public成员字段被推测为要显示的属性,使用字段名字来序列化。 也就是说,一个“getXxx()”方法中如果做了业务性的处理,在被调用的过程中也会被执行。如下面的伪代码: @Component public class FetchUserAction { @Value(“$cus.actId.fu”) public int actionId; public User getCurrentUser(){ // 从应用上下文获得当前用户ID …. // 从数据库查询用户信息 …. return currentUser; } } 在对`FetchUserAction`的实例进行序列化时,会得到下面的json: { “actionId”: “act001”, “currentUser”: { “userId”: 123456, “userName”: “张三” } } 得到这个json的时候意味着至少已经做了 “从应用上下文获得当前用户ID” 和 “从数据库查询用户信息” 两个动作。在输出日志的时候静默的执行了一次涉及到资源的操作,这不是一个合理的事情。 当然也可以为getCurrentUser()这个方法添加类似@JsonIgnore这样的注解以避免出现上面的情况。但问题不在这里,问题的关键在于我们应该只需要对model类的实例或其对应的集合做toJson的处理;其它的执行业务处理的对象不应该被输出到日志中,即使因为种种原因不得不将之输出到日志中也不应该做toJson的处理,以避免出现类似前面的例子中的情况。 如果model类的toString()方法的返回值就已经是经过json序列化的就好了,这样我们在输出日志时就不需要显式地再做这个toJson的操作了,也就不会误将不需要json序列化的对象给序列化了。我一开始想的是通过lombok来解决这个问题,因为model类一般是依赖lombok来生成toString方法的。不幸的是,经过调研,我发现虽然已经有人在lombok的相关issue里提过类似的问题,但是lombok现阶段还不支持这么做。要想解决就只能自己实现了。 期望实现的效果是能够根据类上的一个注解如@ToJson来在编译期自动生成类的toString方法。方法内容是下面这样的: public String toString() { return JsonStringSerializer.toJson(this); } 在toString方法中调用json序列化工具类实现了将当前对象转为json字符串的操作。 最开始我是想用bytebuddy或asm来做这个事情的,但是使用这些字节码工具的时候需要加上javaagent相关的配置,运维肯定是不允许的。后来我又接触到了生成式注解,感觉这应该是解决这个问题的一个出路。 先来看下什么是生成式注解: 生成式注解处理器是JSR-269中定义的API,该API可以在编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程,通过生成式注解处理器可以读取、修改、添加抽象语法树中的任意元素。 “可以修改添加抽象语法树中的任意元素”,这看起来很酷,正是我想要的。 关于如何修改语法树可以参考这篇文档:《Java 中的屠龙之术:如何修改语法树?》。 看下具体是如何实现的吧,在类中添加toString方法的代码如下: /** * 为给定的类型元素生成toString方法 * * 此方法负责创建一个公共的toString方法,该方法适用于大多数对象的字符串表示 * 它通过调用makeToStringBody方法来构建方法体,确保生成的方法能够正确执行 * * @param typeElement 类型元素,用于获取类的相关信息 */ private void makeToStringMethod(TypeElement typeElement) { // 导入必要的类,以支持JSON字符串序列化功能 makeImport(typeElement, JsonStringSerializer.class); // 创建公共方法的修饰符 JCTree.JCModifiers modifiers = getTreeMaker().Modifiers(Flags.PUBLIC, List.nil()); // 定义返回类型为String JCTree.JCExpression returnType = getClassExpression(String.class.getName()); // 初始化参数列表和泛型列表为空 List<JCTree.JCVariableDecl> parameters = List.nil(); List<JCTree.JCTypeParameter> generics = List.nil(); // 获取方法名”toString” Name methodName = getName(“toString”); // 初始化异常抛出列表为空 List<JCTree.JCExpression> exceptThrows = List.nil();

    [阅读更多...]
  • 如何实现一个mock平台

    过去几年里一直在做产业平台订单业务相关的开发工作。随着上下游相关业务日渐增多,系统日趋复杂,在开发测试中经常会遇到如下两个问题: 要实现一个功能可能会需要协调几个甚至十来个业务方来完成联调测试数据的准备 多人或多团队合作时,开发进度不好协调,如果关键节点上的一环开发进度慢了就可能会影响到整个项目的进度 mock是解决以上问题的一个办法: 我们可以不找依赖方生产数据,需要什么数据我们自己mock一个就好了; 合作方没有开发完,那就约定好输入输出结构,先mock一个预期结果写死在代码中,自己凑合着开发。 很多时候我们都是这么做的。合作的测试同学甚至还提出过要求:让我在业务代码中插入一些造数据的代码以支持根据某个特定的传入参数返回指定的值,以便他们进行测试。 目前成熟的mock工具也着实有几个,比如mockito、gmock、spock等等。这几个mock工具各有其特点,但是使用场景通常只限于单元测试,在功能上算是一个开发校验工具,对测试同学的作用不大。此外不管是使用mock工具mock数据、还是使用硬编码mock数据都需要写代码(或者说存在代码侵入)。另外,因为是通过插入的硬编码来实现的数据mock,灵活性什么的是压根儿没法儿说的。 一个理想的mock工具该是什么样的呢?我觉得需要能满足如下的要求: 能够根据返回值类型mock数据(mock数据的基本要求) 在方法层mock数据,而不仅仅是在接口层mock数据 能够基于接口mock数据 能够根据不同的传入参数返回不同的值 可以根据不同的环境(dev,test,stg,prod)来开启或关闭mock能力 可以在方法层随时开启或关闭mock能力 可以界面化配置 低代码侵入或无代码侵入 其中第1、2项是mock数据的基础要求,第3项可以解决多个合作方开发进度不同步的问题,第4、5、6项是对mock能力的灵活性要求,第7、8项是易用性的要求。简而言之就是要灵活好用。 下面介绍一个实现这种mock工具的方案。在这个方案里我们需要如下几种组件: mock-server,独立部署的服务,提供可视化界面,管理方法元数据,维护mock信息等功能。 mock-agent, 集成到项目里,负责实现方法代理,完成方法元数据解析,与mock-server通信等功能。 方法AOP,方法切面,用来标记要mock的方法,获取方法请求参数,反馈mock数据 三个组件的关系大致如下图: 通过这三个组件来mock数据的整体流程如下图: 看完流程后,期望中的mock工具(或者说mock平台)是什么样的已经很清晰了: 工程整体上可以分为两部分:一个独立的mock-server,以及可以嵌入到业务系统里的mock-agent。 mock-server 是一个信息管理平台,可以提供界面化的方式来维护应用信息、方法信息、mock数据信息及mock规则信息、用户信息,这个还是相对比较简单的 mock-agent 则承担了两种职责:实现方法切面、完成和mock-server的通信;在springboot生态中可以通过自定义的springboot starter来实现,core java系统中可以通过asm或者byte-buddy来实现;mock-agent的实现是比较麻烦的,因为被嵌入的项目各有不同,情况比较复杂,需要较高的兼容性,事实上我就是在解决了这一块儿后才确信了整体方案的可行性 至于再具体的功能细节和代码实现就不一一展开说了,如有兴趣可以参考我的开源项目 Mocko 。mocko这个项目就是基于以上思路来做的实现,目前仅是一个MVP(最小可行性产品)版本,待完善的地方还有很多,欢迎大家多提意见,也欢迎有兴趣的朋友一起合作开发。 END!!!

    [阅读更多...]
  • 使用Spring AOP实现注解式的分布式锁

    这里简单说一个springboot生态下基于redis实现的分布式锁方案。预期实现的效果是在要加锁的方法上添加一个注解,然后就能根据请求参数得到并加上锁,方法执行完后,也会自动释放锁。这样在实现方法时,开发者就可以只关注业务逻辑,不用考虑加锁解锁相关的事情。务求整个过程的丝滑程度类似Spring的 @Transactional 注解。 这个分布式锁暂时基于redis来实现,用来和redis交互的组件则是spring-redisson。当然,我也考虑过基于mysql来实现,以后有时间了也会写一个mysql的版本。不过不管是redis还是mysql,两者都只是实现分布式锁的一个基础中间件,对整体实现思路没有什么影响。 先来看下这个分布式锁的大致结构图:  图中左侧的redis/redisson/RedisProperties是我们实现分布式锁的基础,它们的作用是不需要多说的;右下角的Business代表了各种业务需求及对应的方法,他们需要使用分布式锁来实现业务处理时的互斥性,这也不用解释;在下面的内容中会详细介绍下其他部分,也就是我们这个分布式锁的主要组成部分。 LockAdvisor LockAdvisor 是分布式锁的基础组件,它由LockPointcut 和 LockAdvice 组成。当然,不只是当前这个分布式锁,spring aop的核心结构一般都是 Advisor,Advisor中又有Pointcut和Advice两个成员,两者作用大致如下图所示: Pointcut发挥作用是在应用启动时Bean实例化的过程中,而Advice发挥作用是在目标方法被调用执行的过程中。 Spring在创建Bean实例时,会用所有的Advisor和Bean实例进行匹配,匹配成功了就会创建相应的代理。这其中,匹配是依赖Pointcut来实现的,创建出的代理要做什么则是由Advice决定的。可以说Advisor是Spring创建代理的一个起点。 spring创建代理的过程可以查看spring中的这个方法:AbstractAutoProxyCreator.wrapIfNecessary() 。 LockPointcut 前面也说了,Pointcut会根据方法定义中的信息决定要拦截哪些方法。在分布式锁这个case里, LockPointcut 会根据方法中是否存在 @RLock注解来完成拦截并创建代理。当然实际情况会比较复杂一点,这里在识别到@RLock注解后又对注解中的参数进行了解析和缓存以便进行复用,所以在实现的时候是继承的StaticMethodMatcherPointcut这个类。如果只是要匹配@RLock注解,完全可以依赖Spring提供的AnnotationMethodMatcher来实现。 在springboot中提供了多种Pointcut和MethodMatcher的实现类,在使用时可以根据自己的情况选择使用spring提供的实现,不必一定要自己造轮子。 LockAdvice 在LockAdvice里记录了在目标方法执行前后获得锁及释放锁的详细过程。在这个分布式锁的实现中,我把LockAdvice分成了LockInterceptor、LockAspectSupport、SpelEvaluator三个部分,三者的关系如下图: 把LockAdvice分成三部分的目的是为了划分职责,也就是遵循单一性原则。三部分的职责分别如下: LockInterceptor 只是简单定义了切面,仅是个入口,具体的锁处理还是需要父类LockAspectSupport来实现; LockAspectSupport 提供了锁能力的支持,在这个类里会完成与redis的通信从而实现锁获得和锁关闭的处理 SpelEvaluator 提供了SpEL解析的能力,分布式锁的key一般不会是一个常量,需要根据方法参数动态组装,SpelEvaluator就提供了根据方法参数动态组装锁key的能力 LockAutoConfiguration LockAutoConfiguration 看起来像是一个配置类,但实际上在这个类里完成的是分布式锁实现中需要的各种Bean的创建。这里我们我们可以直接看下代码: @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnClass({Redisson.class, RedissonClient.class, RedisProperties.class}) @EnableConfigurationProperties(RedisProperties.class) public class RLockConfiguration { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public RedissonClient redissonClient(RedisProperties properties) { ··· return Redisson.create(cfg); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public LockOperationSource lockOperationSource() { return new AnnotationLockOperationSource(); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public LockInterceptor lockInterceptor(LockOperationSource lockOperationSource, RedissonClient redissonClient) { LockInterceptor interceptor = new LockInterceptor(); interceptor.setLockOperationSource(lockOperationSource); interceptor.setRedissonClient(redissonClient); return interceptor; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public LockAdvisor lockAdvisor(LockInterceptor lockInterceptor, LockOperationSource lockOperationSource) { LockAdvisor advisor = new LockAdvisor(); advisor.setLockOperationSource(lockOperationSource); advisor.setAdvice(lockInterceptor); return advisor; } } 从代码可以看到,LockAutoConfiguration中确实引入了一个配置类 RedisProperties,这个类对应着项目配置文件(application.yml)中redis的配置。通过@EnableConfigurationProperties(RedisProperties.class)可以获得redis的配置信息,从而创建RedisClient的Bean实例。 前面提到的几个类的实例都会在这里完成创建并最终被Spring得到并管理使用起来。 EnableRLock 前面提到在LockAutoConfiguration中创建了多个Bean,但是这些Bean该怎么注入到Spring容器中呢。 我们在写代码的时候应该都用过@ComponentScan注解,在启动类中通过@ComponentScan注解显示注入LockAutoConfiguration及其中的各种Bean确实是一个办法,但不好不优雅。 刷过springboot面试题的同学应该接触过spring.factories,这是一种springboot推荐的做法。springboot的应用在启动时会扫描所有依赖中的spring.factories文件,并自动注入文件中定义的AutoConfiguration类及类中定义的各种Bean。 在这个分布式锁的实现中采用的是另一种做法,是通过在项目启动类上定义的 @EnableRLock注解及@EnableRLock中的@Import注解注入LockAutoConfiguration及其中的各种Bean。体验上类似开启springboot事务支持能力的@EnbaleTransactional注解。(关于@Import注解可以参考 SpringBoot探索01 – @Import注解 ) 这个分布式锁的具体实现可以看redis上的代码: zhyea / rlock-spring-boot-starter   也可以直接添加如下依赖使用: <dependency> <groupId>org.chobit.spring</groupId> <artifactId>rlock-spring-boot-starter</artifactId> <version>0.0.3</version> </dependency> 就这样。 END!!!

    [阅读更多...]
  • MapStruct属性多转一实现

    在项目里遇到了需要使用mapstruct将source对象的多个属性转为target对象的一个属性的场景。针对这个问题研究了一段时间,发现想要解决得好一些还是挺让人头疼的。 先说结论吧:MapStruct支持将多个对象转为一个对象,但是不支持将多个属性转为一个属性。对,mapstruct是不支持这么做的。 最终的解决方案也非常简单:在使用mapstruct完成对象的简单转换后,再做一次加工就行。不过我想将这个事情做得优雅一些,目的是尽量不影响业务代码。 项目的代码不好拿出来,举个例子来说明下,在下面的代码中定义了一个产品的Entity类及相应的Item类。目标是将Entity类的实例通过mapstruct转为Item类的实例。 先看下类的定义: 产品Entity类 ProductEntity: @Data public class ProductEntity { /** * 名称 */ private String name; /** * 保质期 */ private Integer qualityGranteeMonths; /** * 生产日期 */ private LocalDate manufactureDate; } 产品Item类 ProductItem : @Data public class ProductItem { /** * 名称 */ private String name; /** * 保质期 */ private Integer qualityGranteeMonths; /** * 生产日期 */ private LocalDate manufactureDate; /** * 状态 */ private ProductStatusEnum status; } ProductItem 比 ProductEntity 多了一个 status 属性,这个status属性可以由 生产日期 (manufactureDate)和保质期(qualityGranteeMonths)计算出来。计算逻辑可以看下 ProductStatusEnum 的定义及类中静态的analyze方法: public enum ProductStatusEnum { /** * 有效 */ EFFECTIVE, /** * 过期 */ EXPIRED,; /** * 根据生产日期和保质期判断产品状态 * * @param manufactureDate 生产日期 * @param qualityGranteeMonths 保质期 * @return 产品状态 */ public static ProductStatusEnum analyzeStatus(LocalDate manufactureDate, Integer qualityGranteeMonths) { if

    [阅读更多...]
  • springboot入门16 – 包装Controller返回值2

    之前有整理过一次怎样包装SpringBoot Controller的做法。 最近在原有方案的基础上又升级了下,可以通过引入 spring-boot-starter 的形式对接口返回值进行封装。 具体做法如下: 1. 引入 zhy-spring-boot-starter 依赖 <dependency> <groupId>org.chobit.spring</groupId> <artifactId>zhy-spring-boot-starter</artifactId> <version>${version}</version> </dependency> 同时需要确认已引入 spring-boot-starter-web 依赖。这样 zhy-spring-boot-starter 中的返回值封装组件才会生效。 2. 包装返回值 要封装返回值,只需要在接口类或接口方法上添加@ResponseWrapper注解就可以了。 因为只需要对REST接口进行封装。所以组件只会对 接口类或接口方法 生效。 接口类指存在@RestController注解的类 接口方法指存在@RestController注解的类下的方法,或者存在@ResponseBody方法 对其它的接口封装没有意义,封装后还容易出错,所以加了如上限定。 比如对在 public boolean login(String username, String password) 这样一个登录接口封装后的返回结果大致如: { “code”: 0, “data”: true, “msg”: “” } 其中 data 是接口方法的返回值。msg 是存在故障时的异常信息提示。code 是返回结果的状态码,这个状态码可以自定义: rw: successCode: 0 #成功的code默认是0 failCode: 10000 #失败的code默认是10000 如果要调整接口返回值的code,可以在配置文件 application.yaml 中添加如上配置并做调整。 3. 包装异常信息 组件中提供了默认的异常封装能力。 其中自定义的业务异常需要继承 @RwException 或者直接使用 @RwException 这个异常类。封装后的返回值大致如: { “code”: 10002, “msg”: “用户名或密码错误” } 除了业务异常外还对接口参数validator校验异常结果进行了封装。 { “code”: 10000, “msg”: “年龄只能为纯数字” } 参数校验失败时的code默认是 10000 。 以上两种异常是可控的业务异常,所以尽管接口返回json中的code值虽然不同,但是接口返回信息中的http status还是200。 此外还有可能会出现一些空指针异常之类的因为编程意外而产生的异常,此时的异常信息统一都是: { “code”: 10000, “msg”: “未知异常” } 而且接口返回信息中的http status是500。 如果还想对异常做更进一步的处理,可以考虑禁用当前组件的异常封装能力,并自定义异常封装能力。 要实现禁用当前组件的异常封装能力可以在配置文件中添加如下配置: rw: wrap-except: false 目前就只需要做这些配置就够了。 如果想做些自定义的扩展可以参考源码:github / zhyea / zhy-spring-boot-starter END!!

    [阅读更多...]