基于Spring AI开发本地Jenkins MCP Server服务

前言

首先介绍下MCP是什么?

MCP是由开发了 Claude 模型的 Anthropic 公司2024年12月提出并开源的一项开放标准,全称:Model Context Protocol,它是一个开放协议,它使 LLM 应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。

大白话

如果不使用 MCP 是什么样的?

就像自己(LLM大模型)学做菜,首先需要学会如何使用刀、锅、锅铲、炉灶,甚至需要自己生火,每一个步骤都需要从头摸索工具,无法专注烹饪本身。​

使用 MCP

MCP 协议相当于给大模型配了一个服务员(MCP Client),当食客(LLM大模型)需要吃什么菜时,可以直接根据菜单上的菜品告诉服务员(MCP Client),知道菜品后,服务员(MCP Client)根据菜品是哪个菜系找不同菜系的厨师(MCP Server),厨师(MCP Server)接到炒菜的任务就使用冰箱、食材、锅、锅铲等(工具)完成菜品制作任务,并将菜品(结果)精准端给服务员(MCP Client),让大模型无需直接操作工具就能完成复杂任务。

初衷

最近接触到 MCP 协议,我觉得它在未来AI实际应用中潜力巨大,很可能成为行业趋势。不过,我留意到mcp.so网站上,大部分 MCP Server 是用 Python 编写的,用 Java 开发的极为少见。就连 Spring AI,也是在 2025 年 2 月才开始支持并封装 MCP 协议的大部分逻辑。所以,我希望有更多从事 Java 开发的人员能够关注这项技术,将其广泛运用到实际项目里 。

正文

流程图

在这里插入图片描述

环境准备

  • Jenkins(需启用「远程访问API」权限)
  • JDK 17
  • SpringBoot 3.3.6
  • IDEA
  • Maven 3
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId><version>1.0.0-M6</version>
</dependency>
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.0</version>
</dependency>
<dependency><groupId>com.google.inject</groupId><artifactId>guice</artifactId><version>5.1.0</version>
</dependency>
<dependency><groupId>io.github.cdancy</groupId><artifactId>jenkins-rest</artifactId><version>1.0.2</version><exclusions><exclusion><artifactId>guice</artifactId><groupId>com.google.inject</groupId></exclusion></exclusions>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.36</version><scope>provided</scope></dependency>

核心代码

  • JenkinsMcpServerConfig.java

MCP Server必须的配置类

package com.agua.ai.mcp.server.config;import com.agua.ai.mcp.server.service.JenkinsApiService;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class JenkinsMcpServerConfig {@Beanpublic ToolCallbackProvider jenkinsTools(JenkinsApiService jenkinsApiService) {return MethodToolCallbackProvider.builder().toolObjects(jenkinsApiService).build();}
}
  • JenkinsProperties.java

定义Jenkins的配置

package com.agua.ai.mcp.server.properties;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties("jenkins")
public class JenkinsProperties {/*** 服务URI*/private String serverUri;/*** 用户名*/private String username;/*** 密码/token*/private String password;
}
  • JenkinsTemplate.java

对com.cdancy.jenkins(封装了Jenkins Rest API的工具类)已经集成的方法进行再次封装,方便调用

package com.agua.ai.mcp.server.util;import com.cdancy.jenkins.rest.JenkinsClient;
import com.cdancy.jenkins.rest.domain.common.IntegerResponse;
import com.cdancy.jenkins.rest.domain.common.RequestStatus;
import com.cdancy.jenkins.rest.domain.job.BuildInfo;
import com.cdancy.jenkins.rest.domain.job.JobInfo;
import com.cdancy.jenkins.rest.domain.job.JobList;
import com.cdancy.jenkins.rest.domain.job.ProgressiveText;
import com.cdancy.jenkins.rest.features.JobsApi;
import com.agua.ai.mcp.server.properties.JenkinsProperties;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Map;/*** Jenkins 模板类,用于封装 Jenkins API 的调用*/
@Component
public class JenkinsTemplate {private final JenkinsClient jenkinsClient;private JobsApi jobsApi;@Autowiredpublic JenkinsTemplate(JenkinsProperties jenkinsProperties) {this.jenkinsClient = JenkinsClient.builder().endPoint(jenkinsProperties.getServerUri()).credentials(jenkinsProperties.getUsername() + ":" + jenkinsProperties.getPassword()).build();}@PostConstructpublic void init() {this.jobsApi = jenkinsClient.api().jobsApi();}/*** 获取任务列表** @param optionalFolderPath 可选的文件夹路径* @return 任务列表*/public JobList getJobList(String optionalFolderPath) {return jobsApi.jobList(optionalFolderPath);}/*** 获取任务信息** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 任务信息*/public JobInfo getJobInfo(String optionalFolderPath, String jobName) {return jobsApi.jobInfo(optionalFolderPath, jobName);}/*** 使用 XML 文件创建任务** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @param configXML 任务的配置 XML* @return 请求状态*/public RequestStatus createJob(String optionalFolderPath, String jobName, String configXML) {return jobsApi.create(optionalFolderPath, jobName, configXML);}/*** 删除任务** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 请求状态*/public RequestStatus deleteJob(String optionalFolderPath, String jobName) {return jobsApi.delete(optionalFolderPath, jobName);}/*** 启用任务** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 是否成功*/public boolean enableJob(String optionalFolderPath, String jobName) {return jobsApi.enable(optionalFolderPath, jobName);}/*** 禁用任务** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 是否成功*/public boolean disableJob(String optionalFolderPath, String jobName) {return jobsApi.disable(optionalFolderPath, jobName);}/*** 获取任务配置文件内容** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 配置文件内容*/public String getJobConfig(String optionalFolderPath, String jobName) {return jobsApi.config(optionalFolderPath, jobName);}/*** 更新任务配置文件内容** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @param configXML 新的配置 XML* @return 是否成功*/public boolean updateJobConfig(String optionalFolderPath, String jobName, String configXML) {return jobsApi.config(optionalFolderPath, jobName, configXML);}/*** 构建任务** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 构建响应*/public IntegerResponse buildJob(String optionalFolderPath, String jobName) {return jobsApi.build(optionalFolderPath, jobName);}/*** 构建带参数的任务** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @param properties 参数列表* @return 构建响应*/public IntegerResponse buildJobWithParams(String optionalFolderPath, String jobName, Map<String, List<String>> properties) {return jobsApi.buildWithParameters(optionalFolderPath, jobName, properties);}/*** 获取任务上次构建序号** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 构建序号*/public Integer getLastBuildNumber(String optionalFolderPath, String jobName) {return jobsApi.lastBuildNumber(optionalFolderPath, jobName);}/*** 获取任务上次构建时间戳** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @return 时间戳*/public String getLastBuildTimestamp(String optionalFolderPath, String jobName) {return jobsApi.lastBuildTimestamp(optionalFolderPath, jobName);}/*** 获取构建信息** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @param buildNumber 构建编号* @return 构建信息*/public BuildInfo getBuildInfo(String optionalFolderPath, String jobName, int buildNumber) {return jobsApi.buildInfo(optionalFolderPath, jobName, buildNumber);}/*** 获取构建控制台输出内容** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @param buildNumber 构建编号* @param start 开始位置* @return 控制台输出内容*/public ProgressiveText getBuildLog(String optionalFolderPath, String jobName, int buildNumber, int start) {return jobsApi.progressiveText(optionalFolderPath, jobName, buildNumber, start);}/*** 重命名任务** @param optionalFolderPath 可选的文件夹路径* @param currentJobName 当前任务名称* @param newJobName 新任务名称* @return 是否成功*/public boolean renameJob(String optionalFolderPath, String currentJobName, String newJobName) {return jobsApi.rename(optionalFolderPath, currentJobName, newJobName);}/*** 停止任务** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @param buildNumber 构建编号* @return 是否成功*/public RequestStatus killJob(String optionalFolderPath, String jobName, int buildNumber) {return jobsApi.kill(optionalFolderPath, jobName, buildNumber);}/*** 查看执行日志** @param optionalFolderPath 可选的文件夹路径* @param jobName 任务名称* @param start 开始位置* @return 是否成功*/public ProgressiveText progressiveTextJob(String optionalFolderPath, String jobName, int start) {return jobsApi.progressiveText(optionalFolderPath, jobName, start);}
}
  • JenkinsApiService.java

直接暴露给LLM大模型的可调用的工具的Service

package com.agua.ai.mcp.server.service;import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.agua.ai.mcp.server.util.JenkinsTemplate;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;@Service
@AllArgsConstructor
public class JenkinsApiService {private final JenkinsTemplate jenkinsTemplate;@Tool(description = "获取任务列表")public String getJobList(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.getJobList(optionalFolderPath));}@Tool(description = "获取任务信息")public String getJobInfo(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.getJobInfo(optionalFolderPath, jobName));}@Tool(description = "使用 XML 文件创建任务")public String createJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName,@ToolParam(description = "任务的配置 XML") String configXML) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.createJob(optionalFolderPath, jobName, configXML));}@Tool(description = "删除任务")public String deleteJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.deleteJob(optionalFolderPath, jobName));}@Tool(description = "启用任务")public String enableJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.enableJob(optionalFolderPath, jobName));}@Tool(description = "禁用任务")public String disableJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.disableJob(optionalFolderPath, jobName));}@Tool(description = "获取任务配置文件内容")public String getJobConfig(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.getJobConfig(optionalFolderPath, jobName));}@Tool(description = "更新任务配置文件内容")public String updateJobConfig(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName,@ToolParam(description = "新的配置 XML") String configXML) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.updateJobConfig(optionalFolderPath, jobName, configXML));}@Tool(description = "构建任务")public String buildJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.buildJob(optionalFolderPath, jobName));}@Tool(description = "构建带参数的任务")public String buildJobWithParams(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName,@Schema(description = "参数列表(格式:Map<String, List<String>>)") String properties) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.buildJobWithParams(optionalFolderPath, jobName, JSON.parseObject(properties, new TypeReference<>() {})));}@Tool(description = "获取任务上次构建序号")public String getLastBuildNumber(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.getLastBuildNumber(optionalFolderPath, jobName));}@Tool(description = "获取任务上次构建时间戳")public String getLastBuildTimestamp(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.getLastBuildTimestamp(optionalFolderPath, jobName));}@Tool(description = "获取构建信息")public String getBuildInfo(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName,@ToolParam(description = "构建编号(必须是整数)") String buildNumber,@ToolParam(description = "是否返回变更历史(boolean类型)") String changeSetFlag) {Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() {@Overridepublic boolean shouldSkipField(FieldAttributes f) {return Boolean.parseBoolean(changeSetFlag) && "changeSet".equals(f.getName());}@Overridepublic boolean shouldSkipClass(Class<?> clazz) {return false;}}).create();return gson.toJson(jenkinsTemplate.getBuildInfo(optionalFolderPath, jobName, Integer.parseInt(buildNumber)));}@Tool(description = "获取构建控制台输出内容")public String getBuildLog(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName,@ToolParam(description = "构建编号(必须是整数)") String buildNumber,@ToolParam(description = "开始位置(必须是整数)") String start) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.getBuildLog(optionalFolderPath, jobName, Integer.parseInt(buildNumber), Integer.parseInt(start)));}@Tool(description = "重命名任务")public String renameJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "当前任务名称") String currentJobName,@ToolParam(description = "新任务名称") String newJobName) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.renameJob(optionalFolderPath, currentJobName, newJobName));}@Tool(description = "停止任务(必须二次确认)")public String killJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName,@ToolParam(description = "构建编号(必须是整数)") String buildNumber) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.killJob(optionalFolderPath, jobName, Integer.parseInt(buildNumber)));}@Tool(description = "查看执行日志")public String progressiveTextJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath,@ToolParam(description = "任务名称") String jobName,@ToolParam(description = "开始位置(必须是整数)") String start) {Gson gson = new GsonBuilder().create();return gson.toJson(jenkinsTemplate.progressiveTextJob(optionalFolderPath, jobName, Integer.parseInt(start)));}
}
  • application.yml
spring:ai:mcp:server:stdio: truename: jenkins-apiversion: 0.0.1type: SYNCmain:web-application-type: nonebanner-mode: off
logging:level:root: INFO
jenkins:# jenkins的访问urlserver-uri: ${JENKINS_API_SERVER_URI}username: ${JENKINS_API_USERNAME}password: ${JENKINS_API_TOKEN}

使用配置

如果是用 Cursor 作为客户端,那么可以通过一下方式启动 MCP Server ,本地 MCP Server 服务请将{你的路径}替换成实际的 jar 包存放路径

  • command方式
java -Dspring.ai.mcp.server.transport=STDIO -Dspring.main.web-application-type=none -jar {你的路径}\mcp-jenkins-server-0.0.1-SNAPSHOT.jar
  • mcp.json配置
{"mcpServers": {"jenkins-mcp": {"command": "java","args": ["-Dspring.ai.mcp.server.stdio=true","-Dspring.main.web-application-type=none","-jar","{你的路径}\\mcp-jenkins-server-0.0.1-SNAPSHOT.jar"],"env": {"jenkins.server-uri": "jenkins-uri","jenkins.username": "username","jenkins.password": "password/token" }}}
}

最终演示效果

用户提问:请部署v1.2.3版本到测试环境

MCP Client解析后调用:

{"tool": "buildJobWithParams","params": {"optionalFolderPath": "","jobName": "qa-system","properties": {"version": ["v1.2.3"], "env": ["test"]}}
}

Jenkins MCP Server执行结果:

{"value": "12345","errors": []
}

总结

目前大模型的优势就是它能够一定程度地理解用户所说的内容,并转换成调用工具所需的请求参数,减少人工解析的工作量并且降低人工适配的成本。现在 MCP 还处于初期发展阶段,因此需要广大开发者的支持,才能支撑起庞大 AI 应用生态构建。

相关链接
MCP 介绍
Spring AI MCP
Jenkins 官网
jenkins-rest(Github 地址)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/75876.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vcpkg安装指定版本的库

一.vcpkg安装 使用git将vcpkg源码克隆到本地制定目录&#xff08;D:\vcpkg&#xff09;&#xff0c;并初始化 git clone https://github.com/microsoft/vcpkg.git cd vcpkg ./bootstrap-vcpkg.sh # Linux/macOS .\bootstrap-vcpkg.bat # Windows 如下图&#xff1a; 二.安…

数据结构C语言练习(单双链表)

本篇练习题(单链表)&#xff1a; 1.力扣 203. 移除链表元素 2.力扣 206. 反转链表 3.力扣 876. 链表的中间结点 4.力扣 21. 合并两个有序链表 5. 牛客 链表分割算法详解 6.牛客 链表回文结构判断 7. 力扣 160. 相交链表 8. 力扣 141 环形链表 9. 力扣 142 环形链表 II…

nginx部署前端项目(linux、docker)

引言 在CentOS 7系统上使用docker安装nginx&#xff0c;使用nginx部署一个由Vue开发、打包的项目 docker安装nginx 这里不多赘述&#xff0c;直接上docker-compose.yml代码 nginx:container_name: nginximage: nginx:1.27.2ports:- "80:80"volumes:- /docker/ngin…

WPF ContentPresenter详解2

ContentPresenter与ContentControl的区别 ContentControl 和 ContentPresenter 是 WPF 中两个相关的控件&#xff0c;但它们在用途和功能上有一些关键的区别。理解这两者的区别和联系有助于更好地设计和开发用户界面。 1. 类层次结构 ContentControl&#xff1a;位于 WPF 控件…

【HTML5游戏开发教程】零基础入门合成大西瓜游戏实战 | JS物理引擎+Canvas动画+完整源码详解

《从咖啡杯到财务自由&#xff1a;一个程序员的合成之旅——当代码遇上物理引擎的匠心之作》 &#x1f31f; 这是小游戏开发系列的第四篇送福利文章&#xff0c;感谢一路以来支持和关注这个项目的每一位朋友&#xff01; &#x1f4a1; 文章力求严谨&#xff0c;但难免有疏漏之…

鸿蒙OS 5.0 服务能力框架深入剖析

鸿蒙OS 5.0 服务能力框架中关键类的作用分析 1\. 鸿蒙OS 5.0 服务能力框架导论 鸿蒙OS 5.0&#xff0c;亦称鸿蒙智联 5 1&#xff0c;标志着华为在分布式操作系统领域迈出的重要一步。与早期版本采用兼容安卓的AOSP层、Linux内核以及LiteOS内核不同&#xff0c;鸿蒙OS 5.0 专注…

RTMP推流+EasyDSS云服务+边缘AI分析的无人机监控系统设计

在现代科技不断发展的背景下&#xff0c;无人机技术已经广泛应用于各个领域&#xff0c;从航拍摄影到工业巡检&#xff0c;从农业监测到应急救援&#xff0c;无人机以其高效的工作能力&#xff0c;为人们的生活和工作带来了诸多便利与创新&#xff0c;而其视频传输与分析系统更…

HCIP(VLAN综合实验)

实验拓补图 实验分析 一、实验目的 掌握VLAN的创建和配置方法理解VLAN在局域网中的作用学习如何通过VLAN实现网络隔离和通信 二、实验环境 交换机&#xff08;SW1、SW2、SW3&#xff09;个人电脑&#xff08;PC1、PC2、PC3、PC4、PC5、PC6&#xff09;路由器&#xff08;R1…

Linux系统编程 | 线程的基本概念

&#x1f493;个人主页&#xff1a;mooridy &#x1f493;专栏地址&#xff1a;Linux 关注我&#x1f339;&#xff0c;和我一起学习更多计算机的知识! &#x1f51d;&#x1f51d;&#x1f51d; 什么是线程 程序中的一个执行路线就叫做线程 一个进程至少要有一个执行线程,单…

小林coding-12道Spring面试题

1.说一下你对 Spring 的理解?spring的核心思想说说你的理解&#xff1f; 2.Spring IoC和AOP 介绍一下?Spring的aop介绍一下?IOC和AOP是通过什么机制来实现的?怎么理解SpringIoc&#xff1f;依赖倒置&#xff0c;依赖注入&#xff0c;控制反转分别是什么&#xff1f;依赖注…

第十二章——位运算

按位的与& 若x的第i位和y的第i位都是1&#xff0c;那么&#xff08;x&y&#xff09;1&#xff0c;否则&#xff08;x&y&#xff09; 0 应用&#xff1a;希望让某一位或某些位为0 。取一个数中的一段。 按位的或| 若x的第i位1或y的第i位1&#xff0c;那么&…

计算机等级考试数据库三级(笔记3)

插入 修改 现要创建一个具有如下功能的触发器&#xff1a;每当在销售表中插入一条销售记录时&#xff0c;修改商品表中对应商品的销售总量&#xff0c;假设一次只插入一条销售记录。请补全下列代码。CREATE TRIGGER tri insert on xx FOR xx AS xx 商品表 xx 销售总量xx (SELEC…

【Leetcode 每日一题】2716. 最小化字符串长度

问题背景 给你一个下标从 0 0 0 开始的字符串 s s s&#xff0c;重复执行下述操作 任意 次&#xff1a; 在字符串中选出一个下标 i i i&#xff0c;并使 c c c 为字符串下标 i i i 处的字符。并在 i i i 左侧&#xff08;如果有&#xff09;和 右侧&#xff08;如果有&…

Flutter中实现拍照识题的功能

文章目录 **1. 功能拆解****2. 具体实现步骤****(1) 拍照或选择图片****(2) 图片预处理&#xff08;可选&#xff09;****(3) 文字识别&#xff08;OCR&#xff09;****(4) 数学公式识别 → LaTeX****方案1&#xff1a;Mathpix API&#xff08;高精度&#xff0c;付费&#xff…

【Mysql:内置函数】

日期函数&#xff1a; 查看当前日期&#xff1a; select current_date();查看当前时间&#xff1a; select current_time(); 查看当前时间戳&#xff1a; select current_timestamp(); 计算两个日期的差值&#xff1a; select datediff(date1,date2); 当前的日期时间&a…

71. 我的第一个Linux驱动实验

一、字符设备驱动框架 字符设备驱动的编写主要就是驱动对应的open、close、read。。。其实就是 file_operations结构体的成员变量的实现。 其中关于 C 库以及如何通过系统调用“陷入” 到内核空间这个我们不用去管&#xff0c;我们重点关注的是应用程序和具体的驱动&#xff0…

jdk21使用Vosk实现语音文字转换,免费的语音识别

1.下载vosk的model vosk官网&#xff1a;https://alphacephei.com/vosk/models 我这里使用较小的vosk-model-small-cn-0.22 2.添加相关pom文件 <!-- 获取音频信息 --><dependency><groupId>org</groupId><artifactId>jaudiotagger</artifac…

如何一键安装所有Python项目的依赖!

在开发项目时&#xff0c;常常需要在多个环境中安装各种依赖。对开发者来说&#xff0c;每次手动一个个安装这些依赖是不是很麻烦&#xff1f;&#x1f605; 其实有个超简单的办法&#xff01;只需要一个脚本&#xff0c;就能快速解决问题&#xff01;&#x1f4a1; 这就是我们…

Blender配置渲染设置并输出动画

在Blender中&#xff0c;渲染设置和渲染动画的选项位于不同的面板中。以下是具体步骤&#xff1a; 渲染设置 渲染设置用于配置输出格式、分辨率、帧率等参数。 打开右侧的 属性面板&#xff08;按 N 键可切换显示&#xff09;。 点击 “输出属性” 选项卡&#xff08;图标是…

C++修炼:string类的使用

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》 欢迎点赞&#xff0c;关注&am…