LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

系统架构设计师

2025/8/23

架构师

系统分析师【上半年】 && 系统架构设计师【下半年】

考试科目【考试时间11月上旬 三科均达到45分】

科目一:综合知识【总分75分】
考试时间:9:00 - 11:30
客观题:75空单选题 150分钟

科目二:案例分析【总分75分】
考试时间:13:30-15:00
主观题:5道大题(只做3道) 90分钟
第一道必答,后4道选答2道

科目三:论文写作 【总分75分】
考试时间:15:30 - 17:30
论文题:4道题选做1道 120分钟

《系统架构设计师教程(第2版)》当字典

系统工程与信息系统基础

系统工程【★】
信息系统生命周期【★】
信息系统开发方法【★★】
信息系统的分类【★★★】
政府信息化与电子政务【★★】
企业信息化与电子商务【★★】
数字化转型与智能制造【★★】

系统工程

系统工程是一种组织管理技术
系统工程是为了最好的实现系统的目的,对系统的组成要素、组织结构、信息流、控制机构进行分析研究的科学方法。
系统工程从整体出发、从系统观念出发,以求【整体最优】
系统工程利用计算机作为工具,对系统的结构、元素、信息和反馈等进行分析,以达到最优规划、最优设计、最优管理和最优控制的目的。
系统工程方法是一种现代的科学决策方法

系统工程方法 关键点
霍尔三维结构
“硬科学” 方法论
逻辑维:逻辑维即解决问题的逻辑过程
时间维:时间维即是工作进程
知识维:知识维即是专业科学知识
应用场景:组织和管理大型工程建设项目
切克兰德方法
“软科学” 方法论
核心不是”最优化”,而是 “比较“ 和 “探寻
七步骤:认识问题、根底定义、建立概念模型、比较及探寻、选择、设计与实施、评估与反馈
并行工程方法 **”制造过程” 与 “支持过程” 并行 **
强调三个方面:产品设计开发期间,最快速度按质完成;各项工作问题协调解决;适当的信息系统工具
综合集成法 钱学森命名,【简单系统】和【巨系统】
四原则:整体论原则、相互联系原则、有序性原则、动态原则
WSR系统方法 实践准则:【懂物理】—【明事理】—【通人理】
霍尔三维比较偏向大型的项目
逻辑维:【做不做】
  • 明确问题
  • 确定目标(建立价值体系或评价体系)
  • 系统综合
  • 系统分析
  • 优化(系统方案的优化选择)
  • 系统决策
  • 实施计划

时间维:做规划

  • 规划阶段(调研,谋求活动的规划与战略)
  • 拟定方案(提出具体的设计方案)
  • 研制阶段(完成研制方案及生产计划)
  • 生产阶段(生产零部件及提出安装计划)
  • 安装阶段(安装完毕,完成系统的运行计划)
  • 运行阶段(系统按照预期的用途开展服务)
  • 更新阶段(改进原有系统、或消亡原有系统)

知识维:

工程、医药、建筑、商业、法律、管理

系统工程生命周期阶段

探索性研究 → 概念阶段 → 开发阶段 → 生产阶段 → 使用阶段 → 保障阶段 → 退役阶段
系统工程生命周期方法

计划驱动方法:需求 → 设计 → 构建 → 测试 → 部署
渐进迭代式开发:提供连续交付以达到期望的系统
精益开发:起源于丰田,是一个动态的、知识驱动的,以客户为中心的过程
敏捷开发:更好的灵活性

信息系统生命周期

系统工程建设原则

高层管理人员介入原则:CIO介入

用户参与开发原则:用户确定范围、核心用户全程参与、用户深度参与

自顶向下规划原则:以此减少信息不一致的现象

工程化原则:引入软件工程

其他原则:创新性原则、整体性原则、发展性原则、经济性原则

信息系统开发方法

原型法针对需求不明确

  • 按功能分:水平原型(界面)、垂直原型(复杂算法)
  • 按最终结果分:抛弃式原型、演化式原型
———————————- 原型法【需求阶段】 ———————————-
结构化法 面向对象方法 面向服务的方法
开发目标清晰化
工作阶段程式化
开发文档规范化
设计方法结构化
应变能力差
自底向上
阶段界限不明
更好应变、更好复用
符合人们的思维习惯
(哪些人使用哪些功能等)
粗粒度、松耦合(SpringCloud)
标准化和构件化(一个服务一个构建)
抽象级别:
操作【低】→ 服务【中】→ 业务流程【高】

元宇宙:把现实中的东西模拟到完整的虚拟环境里面

信息系统建设原则

  • 高层管理人员介入原则:CIO介入
  • 用户参与开发原则:用户确定范围、核心用户全程参与、用户深度参与
  • 自顶向下规划原则:全局管控;以此减少信息不一致的现象
  • 工程化原则:引入【软件工程】
  • 其他原则:创新性原则、整体性原则、发展性原则、经济性原则

信息系统开发方法

  • 原型法【需求阶段】

    针对需求不明确
    按功能分:水平原型(界面)、垂直原型(复杂算法) [举例 比如123列垂直包含点菜 订单 套餐模块 那么水平就是他们包含的横着的框]
    按最终结果分:抛弃式原型、演化式原型

    • 结构化法 →
      • 自顶向下,逐步分解求精 【田 → 日 日 → 口】
      • 开发目标清晰化
      • 工作阶段程序化
      • 开发文档规范化
      • 设计方法结构化
      • 应变能力差
    • 面向对象方法 → (分析这个系统适合哪些人……)
      • 自底向上
      • 阶段界限不明
      • 更好应变、更好复用
      • 符合人们的思维习惯
    • 面向服务方法
      • 粗粒度、松耦合
      • 标准化和构件化
      • 抽象级别:操作【低】 → 服务【中】 → 业务流程【高】
  • 其他信息系统开发方法

    • 形成化方法
      • 净室软件工程【受控污染级别的环境】
      • 【数学模型化】
      • 所有东西均可证明/验证,而不是厕所
    • 统一过程方法【UP】
    • 敏姐方法
    • 基于架构的开发方法【ABSD】

企业信息化与电子商务 — 商业智能(BI)

★ 商业智能是指利用数据挖掘、知识发现等技术分析和挖掘结构化的、面向特定领域的存储与数据仓库的信息。它可以帮助用户认清发展趋势、获取决策支持并得出结论。以下(B)活动,并不属于商业智能范畴。

A 某大型企业通过对产品销售数据进行挖掘,分析客户购买偏好
B 某大型企业查询数据仓库中某种产品的总体销售数量 [B只做了商品查询,并没有复杂的数据分析]
C 某大型购物网站通过分析用户的购买历史记录,为客户进行商品推荐
D 某银行通过分析大量股票交易的历史数据,做出投资决策


★ 商业智能系统的处理过程包括四个主要阶段:数据预处理通过(C)实现企业原始数据的初步整合;建立数据仓库是后续数据处理的基础;数据分析是体现系统智能的关键,主要采用(D)和(B)技术,前者能够实现数据的上卷、下钻和旋转分析,后者利用隐藏的知识,通过建立分析模型预测企业未来发展趋势:数据展现主要完成数据处理结果的可视化。

A 数据映射和关联
B 数据集市和数据立方体
C 数据抽取、转换和装载 [ETL]
D 数据清洗和数据集成

A 知识库
B 数据挖掘
C 联机事务处理
D 联机分析处理 [OLAP]

A 知识库
B 数据挖掘
C 联机事务处理
D 联机分析处理

企业信息化与电子商务 — 数据湖

数据湖是一个存储企业的各种各样原始数据的大型仓库,其中的数据可供存储、处理、分析传输

阅读全文

Appium

2025/8/12

测试方面

  • 功能测试
  • 安装卸载测试
  • 升级测试
  • 兼容性测试[不同机型 系统]
  • 网络切换,中断测试
  • 横竖屏切换
  • 健壮性

测试环境的搭建

需要配置的环境:
  • java jdk java环境
  • Android sdk 安卓环境
  • python环境
    • 第三库:appium
  • 使用真机或模拟器进行测试【夜神模拟器】

环境搭建验证测试

模拟器或真机

在cmd中输入 adb devices 会返回ip地址和端口号
获取系统版本号输入 adb shell getprop ro.build.version.release
获取虚拟器里的app名称 adb shell dumpsys window windows | findstr mFocusedApp

{c361382 u0 com.android.settings/.Settings t9}}} /.后面是界面的名字 u0后 /.前是app名称

import time

from appium import webdriver
from selenium.webdriver.common.by import by  #【利用by自动获取信息】

# 配置手机连接的参数信息
# 参数信息都是以键值对的方式进行连接
desired_caps = {}

# 设备的名称
desired_caps["deviceName"] = "127.0.0.1:62001"

# 系统
desired_caps["platformName"] = "Android"

# 系统版本号
desired_caps["platformVersion"] = "7.1.2"

# 启动的app名称
## com.android.settings
desired_caps["appPackage"] = "com.android.settings"

# 启动的app界面:.Settings
desired_caps["appActivity"] = ".Settings"

# print(desired_caps)

# 发送链接  用的appium的端口  用关键字传参
driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub",desired_capabilities = desired_caps)

time.sleep(5)
driver.quit()
...
...

app自动化测试的流程

① 首先明确项目的核心模块
  • 注册登录
  • 阅读文章
  • 搜索内容
  • 发布评论
  • 关注订阅
  • 私信聊天
  • 点赞等等
② 设计用例脚本

分析用例执行的步骤

  • 进入app首页
  • 点击我的界面
  • 点击登录/注册
  • 点击使用邮箱登录/注册
  • 输入邮箱号
  • 输入密码
  • 点击登录
  • 登录成功/失败
  • 进行断言
自动截取虚拟器上界面的图片查看详情

D:\android_sdk_test\tools\uiautomatorviewer.bat

若两个 resource-id 定位是一样的那么就需要手写XPATH【resource-id必须唯一!!】

####### 这些都是线性脚本!!!
# 两个斜杠是代表当前界面所有的元素  增加个文本标识
driver.find_element(By.XPATH,'//*[@text="我的"]').click()
# 点击登录/注册
driver.find_element(By.ID,'io.manong.developerdaily:id/login_btn').click()
# 点击使用邮箱登录/注册   这里有报错是因为代码执行很快 但界面有延迟 需要加等待
time.sleep(2)
driver.find_element(By.ID,'io.manong.developerdaily:id/btn_email').click()

# 输入邮箱号
time.sleep(2)
driver.find_element(By.ID,'io.manong.developerdaily:id/edt_email').send_keys("zzz@qq.com")
# 输入密码
driver.find_element(By.ID,'io.manong.developerdaily:id/edt_email').send_keys("123123")
# 点击登录
driver.find_element(By.XPATH,'//*[@text="登录"]').click()
# 用户名是tata
try:
name_text = driver.find_element(By.XPATH,'io.manong.developerdaily:id/nav_tv_name').click()
if name_text == "tata":
    print("登录成功")

# - 登录成功/失败
# - 进行断言
    assert name_text == "tata"
except:
    login_failed_msg = driver.find_element(By.XPATH,'io.manong.developerdaily:id/forgot').click()
    print("登录失败")
    assert login_failed_msg == "忘了密码?点此找回"
time.sleep(5)
...
...

若换了一个模块要重新截图 选取不同的app界面
这些用例测试属于冒烟测试的过程

Allure是一款灵活、轻量级的多语言测试报告工具,旨在生成美观且易于阅读的测试报告。它由Qameta Software团队开源,支持多种测试框架,如Pytest和TestNG等。Allure不仅可以快速查看测试结果,还能帮助团队成员清晰了解测试过程中的缺陷和问题。通过Allure,用户可以轻松集成测试用例,并生成统一格式的报告,提升测试的可读性和有效性。

总结梳理

核心业务流程梳理
  • 需求分析
  • 设计用例脚本
  • 编写自动化测试脚本
    • 将冒烟测试的脚本进行深度封装和模式设计
      • 选择设计模式:
        • POM设计模式
        • KDT设计模式
      • 引入测试框架pytest结合设计模式进行深度封装
      • 执行过程中进行日志信息的记录log
      • 执行完成之后自动化完成测试报告allure
      • 持续集成Jenkins
  • 脚本维护以及迭代更新

app自动化测试的核心技能点:

Python + Appium + pytest + parametrize + csv + pom + log + Allure + Jenkins

项目梳理及描述

脚本执行的时候会直接发送给Appium服务器 执行py代码后 在Appium会执行许多adb的命令 再去控制手机 完成自动化测试【线性模块 正反例要跑通噢】

站在项目已经完成雏形的基础上,进行项目梳理和总结

  • 整个项目中的文件解析

    • 每个文件包和模块具体的使用规范
  • 整个项目中的模块与文件之间的联系

    • 各个模块及包之间的导入使用
  • 整体从项目完成之后的角度触发去讲解项目搭建流程以及使用规范

    • 搭建app自动化测试环境

      • 安卓sdk
      • java的sdk
      • 安装appium
      • 使用真机或者安装手机模拟器
      • 安装第三方库:Appium-Python-Client
    • 编写自动化的用例线性脚本

      • 新建一个项目

      • 创建一个script脚本包

        • 梳理核心业务模块和功能

          • 登录
          • 注册
          • 点赞评论收藏
          • 发布文章
          • 浏览文章
        • 创建py模型进行线性脚本编写

          • 通过核心业务流程模块梳理出线性脚本的执行步骤
          • 然后通过编写自动化用例脚本执行完成第一轮正反例的冒烟测试
            • 梳理被操作元素以及具体操作方法
            • 执行正反例之后进行断言
            • 保证自动化用例的准确性

若是那种本身图标没有id 没有任何标志的 比如点赞,收藏,评论 就可以直接通过坐标去定位【要考虑分辨率和坐标以及机型】
回归测试一般只需要正例脚本!!!

  • 选择设计模式pom【Page Object Model页面对象模型的简称】

    • 封装
      • 一个类就是一个流程用例或者一个界面
      • 将界面中需要被操作的每一个元素封装成类属性
      • 将界面中需要被执行的用例步骤定义成一个实例方法
    • 使用
      • 创建页面类对象
      • 通过对象调用方法执行用例步骤脚本
      • 获取实际结果进行断言
    • 整个页面封装
    • 将线性文件进行封装
  • 引入测试框架结合用例执行以及管理

    • 用例进行综合管理
    • 使用参数化进行数据驱动测试:
    @pytest.mark.parametrize("user_email, password, t_a_f",[
        [" ", "123123", False],
        ["123@qq.com", "123123", True],
        ...
    ])
    
    • 一般情况在用例执行之前讲参数化使用的实参进行读取使用
      • 数据量比较多的情况下会使用不同的文件格式存储数据
        • Csv
        • text
        • Excel
        • yaml
        • JSON
      • 数据驱动测试主要核心就是在用例设计的过程中,把所有用例需要的正反例数据进行保存
      • 然后再使用的时候进行读取自动化操作用例完成脚本内容
  • 日志信息的记录

  • 缺陷报告的生成

  • 持续集成的处理

单独执行某个用例(venv) D:\app_project>pytest -vsk test_login
有很多其他的需要保持登录的情况下进行测试 ↓

# 点赞评论及收藏
def test_like_comment_collect(go_login):
    # 创建页面类对象
    l_c_l_page = LikeCommentCollectPage(go_login)
    # 通过对象调用方法执行用例步骤
    l_c_l_page.execute_cases()
    
# 发布文章
def test_publish_article(go_login):...

# 浏览文章
def test_browse_article(go_login):...

Pywinauto基本介绍【基本解决找不到元素问题】

主要用于自动化图形用户界面,他提供了一组功能丰富的工具和方法,能够模拟用户的各种操作:点击,输入,文本,选择菜单栏等等…从而实现对Windows应用程序的自动化控制

pywinauto的基本特性
  • 支持跨平台
  • 强大的GUI自动化功能【用户交互图形界面】
  • 简单易用
  • 支持多种应用程序的类型
  • 丰富的文档和社区支持
  • 支持多种GUI框架
  • 图像识别和模糊匹配
pywinauto应用场景
  • 自动化测试
  • ui自动化测试
  • 数据录入和处理
  • 窗口管理
pywinauto的安装和使用

需要安装第三方库依赖:pywinauto
安装命令:【pycharm可以在某个项目下安装】
pip install pywinauto
可以用pip list查看是否安装好了 pywinauto

pywinauto基本使用

案例:使用pywinauto编写自动化脚本,完成记事本的基本操作
操作流程:

  • 打开记事本
  • 点击帮助(关于记事本)
  • 点击确定
  • 输入内容
  • 点击关闭记事本
  • 点击不保存
# 导包
from pywinauto.application import Application

# 启动记事本  backend='uia'意思是连接记事本的后端是什么类型
# app = Application(backend='uia').start('notepad.exe')
app = Application(backend='win32').start('notepad.exe')

# 点击帮助(关于记事本) 使用快捷键完成打开关于记事本的信息  %是Alt符号  H:h  A:a
app.Notepad.type_keys('%HA', pause=1)

# 点击确定  re正则匹配
dlg = app.window(title_re='关于记事本')
 # 获取窗口的界面元素信息  【关于记事本的元素架构】
dlg.print_control_identifiers()
 #  点击    不直接调用click是要运用鼠标真实的模拟
time.sleep(1)
dlg['确定Button'].click_input()

# 输入内容
app.Notepad.type_keys('xx教育')
# 点击关闭记事本
 # 菜单栏选择 ->表示子菜单
app.Notepad.menu_select("文件(&F)->退出(&X)")
# 点击不保存
app.window(title_re='记事本').window(title_re='不保存').cli
桌面化元素定位工具:UISpy.exe【层级树】、inspect.exe
阅读全文

AI_Linux运维

2025/7/16

IT运维基本概念

系统运维

聚焦基础设施稳定性,通过监控工具(如Zabbix、Prometheus)实时追踪服务器、网络及操作系统状态,实施负载均衡、几余设计,快速响应硬件故障或性能瓶颈,确保业务连续性。

自动化运维

以效率提升与标准化为目标,借助像Ansible自动化工具部署、配置管理及巡检任务,结合CI/CD流水线实现发布流程无人化,减少人为失误并提升迭代速度。

数据库运维

核心是数据安全与性能调优,通过定期备份恢复、主从复制保障数据可靠性,利用慢查询分析、索引优化及资源监控(如MySQLPerformance Schema)提升数据库响应效率。

容器运维

专注容器编排与弹性调度,基于Kubernetes实现容器生命周期管理、服务发现及自动扩缩容,配合lstio服务网格治理微服务,确保分布式应用的高可用与快速伸缩。

云计算运维

围绕资源弹性与成本优化,利用云平台(如AWS/Aliyun)的自动扩缩容、Serverless服务动态匹配资源需求,通过资源标签、账单分析工具优化云开支,平衡性能与成本。

信创运维

解决国产化适配与生态兼容,在鲲鹏CPU、统信0S、达梦数据库等信创体系中,完成软硬兼容性测试、系统迁移,满足政策与安全要求。

开发流程

流程:产品设计->项目研发->项目测试->项目上线->运维
研发:前端(HTML+CSS+JavaScript)+后端(Java/Python/Go)
运维:产品上线、后期的运行与维护工作都属于运维工程师范畴

环境 主要用户 主要用途 优势
开发环境 开发人员 编写和初步调试代码,快速验证想法 降低风险、提高效率、保证质量
测试环境 测试工程师、QA团队 自动化测试、功能测试、性能测试、模拟真实场景,发现隐藏问题 降低风险、提高效率、保证质量
生产环境 最终用户 正式对外提供服务要求高可用、高稳定、高安全 降低风险、提高效率、保证质量

工单系统

工单系统(TicketingSystem)是运维工程师和 IT支持团队在日常工作中广泛使用的工具。简单来说,它是一种用于记录、分配和处理工作请求、问题报告、变更申请等事项的系统。

  • 提交工单

    用户在系统中填写问题描述、所属类别、优先级等

  • 自动分配

    系统根据规则将工单自动分配相关人员

  • 处理反馈

    处理人记录诊断与解决方案,并与用户沟通

  • 验证关闭

    用户确认问题已解决后工单状态变为“已关闭”

网络通信”三要素“

网络通信是指在不同设备间通过网络连接进行数据传输交换的过程。

**协议(Protocol)**:协议定义了数据在网络中传输时的规则和格式。常见的协议有 TCP/IP、UDP、HTTP/S、FTP 等它们规定了数据如何被分割、传输、接收和重组。

**IP(internet Protocol)**:IP 地址用于唯一标识网络中的每一台设备。它充当设备的“地址”,使得数据能够在网络中准确地找到目标设备。

IPV4
格式:x.x.x.x
由4个8位的二进制数组成,
每组之间用点分隔,展示形
式以10进制表示
如:192.168.88.102

IPV6格式:
由8个16位的十六进制数组成,
每组数字之间用冒号分隔。
如:2001:0db8:85a3:0000:0000:8a2e:0370:7334

**端口(Port)**:端口用于区分同一台设备上不同的应用程序或服务。在计算机网络中,一个|P 地址可以对应多个服务每个服务通过不同的端口进行通信。端口号是通信中识别应用程序的方式。

端口范围 名称 说明
0 ~ 1023 知名(保留)端口 通常被系统或服务使用(例如HTTP 使用端口 80,HTTPS 使用端口 443,FTP使用端口 21)
1024 ~ 49151 注册端口 通常用于用户和应用程序之间的通信(如数据库服务等)
49152 ~ 65535 动态或私有端口 用于临时连接或客户端通信

云服务器

云服务器(Cloud Server),又称云主机或弹性计算服务(Elastic Compute Service,ECS),是基于云计算技术提供的一种虚拟化服务器。

云服务器利用虚拟化技术,将物理服务器的计算资源(如CPU、内存、存储等)封装成一个或多个独立的虚拟环境,用户可以按需通过互联网远程访问和使用这些资源。

Linux目录结构
目录 作用
/bin 二进制命令所在目录(普通命令=>普通用户和超级管理员root)
/home 与 /root 家目录,/home表示普通用户的家目录,/root表示超级用户root的家目录
/opt 额外的应用软件包,安装qq、游戏、wps办公软件
/sbin 和管理系统相关的命令,【超级管理员用】,s=super超级
/tmp 临时文件目录,这个目录被当作回收站使用
/usr 用户或系统软件应用程序目录,类似Windows中的Program files
/dev /device缩写,设备文件目录,磁盘,光驱=>/dev/sr0
/etc 系统配置文件目录,启动程序,几乎所有的软件都会把自己的配置文件安装在/etc中

项目部署架构

更改主机名hostnamectl set-hostname node1.itcast.cn + su

MySQL

200.146 机器 → [root@node1 sql]# mysql -uroot -proot < ry-zzyl.sql 在这个位置/opt/zzyl/sql

Redis

密码:heima123456
启动Redis:[root@node1 sql]# redis-server /root/tmp/redis-6.2.4/redis.conf
测试redis-cli已连接:redis-cli ping
连接:redis-cli

项目接入阿里云OSS(key + secret)

E:\BaiduNetdiskDownload\AI运维\项目内容\后端打包文件\后端打包文件\zzyl-admin.jar\BOOT-INF\classes\application-prod.yml 托到桌面 然后改完再拖回去

开通百度千帆大模型授权老人体检报告分析
项目接入百度千帆大模型(key + secret)

依旧是上方阿里云的操作

项目管理后端部署

同上更换application-prod.yaml的redis和mysql连接的地址以及账号密码

  • 安装jdk11

    先创建放置jdk11的安装包目录mkdir -p /export/software 然后
    E:\BaiduNetdiskDownload\AI运维\项目内容\jdk\jdk\openjdk-11.0.0.2_linux-x64.tar.gz 放进去
    接着对jdk11安装包进行解压: 解压到 /opt
    tar -xzf openjdk-11.0.0.2_linux-x64.tar.gz -C /opt/
    cd /opt/
    输入ll查看 然后配置JDK环境变量

    vim /etc/profile
      
    在文件的尾部添加: 
    # JAVA_HOME
    export JAVA_HOME=/opt/jdk-11.0.0.2
    export PATH=$JAVA_HOME/bin:$PATH
    export CLASSPATH=$JAVA_HOME/lib:$CLASSPATH
    
    
    保存退出后,执行加载操作:
    source /etc/profile
    
    
    校验:
    java -version
    
    # /etc/profile
    
    # System wide environment and startup programs, for login setup
    # Functions and aliases go in /etc/bashrc
    
    # It's NOT a good idea to change this file unless you know what you
    # are doing. It's much better to create a custom.sh shell script in
    # /etc/profile.d/ to make custom changes to your environment, as this
    # will prevent the need for merging in future updates.
    
    pathmunge () {
        case ":${PATH}:" in
            *:"$1":*)
                ;;
            *)
                if [ "$2" = "after" ] ; then
                    PATH=$PATH:$1
                else
                    PATH=$1:$PATH
                fi
        esac
    }
    
    
    if [ -x /usr/bin/id ]; then
        if [ -z "$EUID" ]; then
            # ksh workaround
            EUID=`/usr/bin/id -u`
            UID=`/usr/bin/id -ru`
        fi
        USER="`/usr/bin/id -un`"
        LOGNAME=$USER
        MAIL="/var/spool/mail/$USER"
    fi
    
    # Path manipulation
    if [ "$EUID" = "0" ]; then
        pathmunge /usr/sbin
        pathmunge /usr/local/sbin
    else
        pathmunge /usr/local/sbin after
        pathmunge /usr/sbin after
    fi
    
    HOSTNAME=`/usr/bin/hostname 2>/dev/null`
    HISTSIZE=1000
    if [ "$HISTCONTROL" = "ignorespace" ] ; then
        export HISTCONTROL=ignoreboth
    else
        export HISTCONTROL=ignoredups
    fi
    
    export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL
    
    # By default, we want umask to get set. This sets it for login shell
    # Current threshold for system reserved uid/gids is 200
    # You could check uidgid reservation validity in
    # /usr/share/doc/setup-*/uidgid file
    if [ $UID -gt 199 ] && [ "`/usr/bin/id -gn`" = "`/usr/bin/id -un`" ]; then
        umask 002
    else
        umask 022
    fi
    
    for i in /etc/profile.d/*.sh /etc/profile.d/sh.local ; do
        if [ -r "$i" ]; then
            if [ "${-#*i}" != "$-" ]; then 
                . "$i"
            else
                . "$i" >/dev/null
            fi
        fi
    done
    
    unset i
    unset -f pathmunge
    
    export JAVA_HOME=/opt/jdk-11.0.0.2
    export MAVEN_HOME=/opt/maven
    export MYSQL_HOME=/usr/local/mysql
    
    export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$MYSQL_HOME/bin:$PATH
    export CLASSPATH=$JAVA_HOME/lib:$CLASSPATH
    
  • 上传jar包到node2节点中

    mkdir -p /opt/zzyl/backend

E:\BaiduNetdiskDownload\AI运维\项目内容\后端打包文件\后端打包文件\zzyl-admin.jar的拖入node2节点的/opt/zzyl/backend/
cd /opt/zzyl/backend/
java -jar zzyl-admin.jar

部署前端

nginx:[3-0基础带你入行AI Linux运维_项目部署上线.pdf](file:///E:/BaiduNetdiskDownload/AI运维/3-0基础带你入行AI Linux运维_项目部署上线.pdf)

智能客服私有化大模型部署

完成大模型ECS服务器申领

安装GPU驱动

基础环境安装(Anaconda、)

模型部署上线与测试(conda搞虚拟环境、下载chatglm-6b模型知识库、上传ChatGLM-6B代码、安装依赖pip-install)

阿里云监控组件

阿里云的云监控服务(CloudMonitor)是一款简单易用、功能强大的监控工具,主要用来帮助用户实时监控阿里云上的各种资源(比如服务器、数据库、网络等),并在出现问题时及时发出警报,确保业务稳定运行。

  • 开通云监控
  • 打开云监控控制台
  • 打开主机监控
  • 为需要监控的主机安装Agent 点击自动安装[等待片刻,重新刷新对应的CPU使用率、内存使用率、磁盘使用率的数据即可出现]
系统警报服务配置

云监控中的报警服务规则配置的主要目的是帮助用户实时监控阿里云资源、线下IDC、其他云厂商产品或自定义监控数据的运行状态,并在监控指标达到预设条件时,自动触发报警通知。通过这种方式,用户可以及时获取异常信息并快速采取措施,确保业务系统的稳定性和可靠性,

  • 设置报警联系人
    报警联系人:当触发条件后,这些对应的人员信息需要提前录入需要通知相关人员

  • 设置报警联系组

    报警联系组:多个联系人合并为一个组,不同的类型可以单独设置为组,告警的时候可以直接针对多个用户同时告警

  • 设置报警规则
    报警规则:帮助用户监控运行状态,并在监控指标达到预设条件时,自动触发报警通知

需求:监控CPU指标,当连续1分钟CPU使用率超过80%,邮件提醒;提醒;当连续5分钟CPU使用率超过95%,进行电话+短信+邮件;当连续3分钟CPU使用率超过90%,进行短信+邮件

报警联系人 → 设置报警联系人 → 创建报警规则 → 设置规则描述(规则名称:CPU使用率监控) 里面有紧急红色 警告黄色 普通蓝色 → 可以设置通道沉默周期 → 生效时间 → 选择报警联系人组 → 恢复通知(恢复正常通知) 👉 云监控里面有测试脚本cpu_test.sh 上传到服务器即可
sh cpu_test.sh sh是用来运行.sh脚本的命令

对于一些常用指标,云监控提供了一键告警方案,可以直接开启使用

系统运维常见问题
  • CPU负载

    步骤一:确认是否存在负载过高 使用top或htop命令查看系统的平均负载值。
    第一行:load average:4.03,3.71,2.25
    [如果负载值持续高于CPU核心数的0.5倍,则看你存在负载过高问题。如果超过1则负载已经较高了,当超过2~3倍意味着负载超高,需要立即解决]

    步骤二:排查CPU负载过高原因

    原因 具体表现 如何解决
    异常进程或服务占用大量CPU资源 单个进程或服务占用大量CPU资源,导致整体CPU使用率升高。 使用top或htop命令查看具体占用CPU资源的进程按Shift+P按键,按CPU使用率排序定位异常进程,然后通过Kill9终止异常进程
    系统资源不足 实例CPU性能不足以支撑当前业务需求 升级实例规格或者优化业务逻辑
    磁盘或网络I/O瓶颈 CPU负载高但实际CPU使用率较低,可能是磁盘或网络I/0瓶颈导致 优化磁盘读写,比如升级高性能云盘优化网络带宽:增加公网带宽或调整内网流量分布
    僵尸进程或不可中断的睡眠状态 通过top命令观察,CPU使用率不高但负载值较高 ps -axjf | grep “D+” 查看是否存在僵尸进程或不可中断的睡眠状态,如果存在建议恢复其对应依赖资源或重启系统
    系统遭遇病毒或恶意程序攻击 CPU使用率高但无法通过top等命令找到异常进程 通过云监控监测异常时间点,检查是否存在异常域名或IP的网络通信,如果确认,建议先备份数据,然后回滚实例并进行病毒扫描
  • 内存爆满

    步骤一:确认是否存在内存爆满 使用top或htop命令查看系统的平均负载值。
    下面那几行 MiB Mem:….
    当Mem行的 free值几乎为0时,表示剩余内存几乎没有了

    步骤二:排查内存过高的原因

    原因 具体表现 如何解决
    异常进程占用大量内存 单个进程或程序长时间占用大量内存资源 使用top或htop命令查看具体占用内存资源的进程。按M按键,按内存使用率排序,定位异常进程,然后通过Kill-9终止异常进程。
    系统内存不足 实例的物理内存不足以支撑当前业务需求 升级实例规格或优化业务逻辑
    内存泄漏或代码缺陷 应用程序在运行过程中不断消耗内存,导致内存使用率持续升高 使用内存分析工具(如Valgrind、jprofiler、jmap等)分析应用程序的内存占用情况
    根据分析结果优化业务代码,修复内存泄漏问题
    已删除未释放的僵尸文件 磁盘空间充足,但内存使用率仍然很高 lsof|grep deleted 查找已删除但未释放的文件,然后重启相关进程以释放内存
    系统缓存或虚拟内存不足 系统缓存占用过多内存,或虚拟内存配置不足 设置Swap分区,增加虚拟内存大小

ECS服务器巡检报告

ECS服务器巡检报告一般是用于评估云服务器ECS实例及其相关资源(如磁盘、网络等)的健康状态和运行性能。该报告基于对ECS实例的全面检查,包括性能指标、安全风险、配置合规性等多个维度的分析。通过巡检报告可以提高系统的稳定性、安全性、优化资源配置和支持合规性审计工作

巡检报告主要内容:

内容 说明
性能监控数据 CPU使用率、内存使用率、磁盘I/O、网络流量等关键性能指标
异常问题诊断 列出ECS实例在运行过程中发现的异常问题,例如高CPU利用率、磁盘I0异常、网络连接问题等。每个异常项需要附带严重等级(如Info、Warn、Critical)
安全风险评估 检查ECS实例是否受到DDoS攻击或其他安全威胁,并提供防护建议
资源使用与配置分析 检查ECS实例的资源配置是否合理,例如磁盘空间是否充足、带宽是否满足业务需求,并提供优化建
事件记录与处理建议 监测到云盘性能达到上限或未创建快照备份等风险事件,确保系统的稳定性和数据的安全性

云服务器监控可视化大屏

  • 打开自定义大盘 创建大盘
  • 添加图表
  • 根据需求添加对应监测指标
  • 保存大盘
阅读全文

本地项目暴露端口

2025/7/10
本地后端Java项目 + 本地前端Vue项目

java后端通过mvn进行clean + install + package 然后去项目下的target找到heima-ai-0.0.1-SNAPSHOT.jar

Vue用vscode进行npm run build进行前端打包 然后去本前端项目的disk里面 把这里面的所有资源copy到
后端的static目录里:C:\Users\Pluminary\Desktop\HouDuan\heima-ai\src\main\resources\static

下载Ngrok 放在文件夹里 这样D:\ngrok\ngrok.exe 然后去系统环境变量的path添加D:\ngrok
随后通过cmd打开C:\Users\Pluminary\Desktop\HouDuan\heima-ai\target目录 然后运行命令java -jar heima-ai-0.0.1-SNAPSHOT.jar 这样就能带着前后端一起启动了

然后再打开一个cmd运行 ngrok http 8080即可

如果是第一次用需要把token配置到ngrok里面🧩 第 1 步:注册 Ngrok 免费账户

  1. 打开官网注册(完全免费):
    👉 https://dashboard.ngrok.com/signup
  2. 注册后进入控制台首页:
    👉 https://dashboard.ngrok.com/get-started/your-authtoken
  3. 复制你看到的 authtoken,比如:
1sdf_abc123XYZ456789_xxxxxxxxxxxxxxxx

🧩 第 2 步:配置 authtoken 到本地 Ngrok 客户端

打开终端,运行以下命令:

ngrok config add-authtoken 你的authtoken

示例:

ngrok config add-authtoken 1sdf_abc123XYZ456789_xxxxxxxxxxxxxxxx

成功后会提示:

Authtoken saved to configuration file: C:\Users\<你的用户名>\.ngrok\ngrok.yml

记得 如果想要手机也可以访问一定要写跨域

package com.itheima.ai.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("Content-Disposition");
    }
}

然后在前端代码里的api.ts里面不要写死localhost
比如const BASE_URL = 'http://localhost:8080' 要改成const BASE_URL = ''

阅读全文

SpringAI-DeepSeek

2025/7/8

SpringAI + DeepSeek

JDK版本最低是17

✅ 当前环境总结

工具 版本 是否合适 说明
Node.js 20.12.0 ✅ 很好 支持现代前端工具(Vite、React、Vue 等)
npm 10.5.0 ✅ 很好 最新版本,兼容所有依赖
JDK 20 ✅ 足够学习 Spring Boot 3.x 推荐 JDK 17+,JDK 20 也能用
nvm 1.2.2 ✅ 推荐工具 多版本切换已配置好

在自然语言处理(NaturalLanguage Processing,NLP)中,有一项关键技术叫Transformer,这是一种先进的神经网络模型,是现如今AI高速发展的最主要原因。我们所熟知的大模型(LargeLanguageModels,LLM),例如GPT、DeepSeek底层都是采用Transformer神经网络模型。

GPT = Generative(生成式:根据上文预测之后应该出现哪个文本,从而形成连续的文本输出) Pre-trained(预训练:通过大规模的文本数据进行预训练,让大模型可以理解人类语言的语法、词性。) Transformer(Transformer:深度学习的一种神经网络模型。多数AIGC模型都依赖于此。)

大模型应用开发

[大模型服务平台百炼控制台] (https://bailian.console.aliyun.com/?tab=model#/api-key)
ollama --help 帮助
ollama ps 查看目前启动的模型
ollama list 查看所有模型

POST:http://localhost:11434/api/chat

Json请求
{
  "model": "deepseek-r1:1.5b",
  "messages": [
    {
      "role": "system",
      "content": "你是海绵宝宝,以海绵宝宝的身份回答用户的问题"
    },
    {
      "role": "user",
      "content": "你是谁?"
    }
  ],
  "stream": false
}
返回值
{
    "model": "deepseek-r1:1.5b",
    "created_at": "2025-07-08T13:17:02.464149Z",
    "message": {
        "role": "assistant",
        "content": "<think>\n我是海绵宝宝,这里是一个互动式的设定。我是一个AI助手,主要提供帮助和信息。我会用简单易懂的语言回答问题,并保持友好交流。\n</think>\n\n我是海绵宝宝,这是一个由深度求索公司开发的机器人助手,主要用于提供信息、帮助解答问题等日常需求服务。我以简单的语气和友好的态度与用户沟通,您的提问将被认真考虑并尽快回应。"
    },
    "done_reason": "stop",
    "done": true,
    "total_duration": 821012000,
    "load_duration": 14152500,
    "prompt_eval_count": 17,
    "prompt_eval_duration": 3000000,
    "eval_count": 88,
    "eval_duration": 802000000
}

LLM、FunctionCalling、RAG通俗讲解

🧠 一、LLM 是什么?

LLM 是 “Large Language Model” 的缩写,中文叫“大语言模型”,比如你正在用的 ChatGPT 就是一个 LLM。

🌰 举个例子:

你输入一句话:“我明天要去北京,天气怎么样?”

LLM 会理解你的意思,然后给你一个像人说话一样的回答,比如:“明天北京晴天,气温25度,适合出行。”

✅ 它能做的事情有很多,比如:

  • 回答问题
  • 写文章
  • 翻译语言
  • 总结文本
  • 写代码……

它的能力来自:训练了海量的文本数据,并通过预测“下一个词”来生成自然语言。


🧩 二、Function Calling 是什么?

Function Calling(函数调用)是 LLM 的一个增强能力,让它在回答问题时不光是“说”,还能“做”事

🌰 举个例子:

你说:“帮我查一下明天北京的天气。”

过去 LLM 只能“猜”一个答案。但如果它具备 Function Calling 能力,它就可以:

  1. 调用一个真实的天气查询函数(比如访问天气API)
  2. 得到真实的天气数据(比如:晴 25°C)
  3. 再把这个数据生成一句话告诉你。

✅ 通俗对比:

方式 类比
以前的 LLM 会说话的人,但手不能动
支持 Function Calling 的 LLM 会说话的人,还能操作电脑帮你查资料、订票、做计算

🧠 总结一下

概念 通俗解释
LLM 一个超级聪明、会说话的 AI(像 ChatGPT)
Function Calling 让这个 AI 不只是“说话”,还能“调用工具”帮你完成任务


🧠 一、RAG 是什么?

RAG 是 “Retrieval-Augmented Generation” 的缩写,中文叫 “检索增强生成”。

它是一种让 LLM(大语言模型)更聪明的方式 —— 把外部知识喂给它,然后让它生成答案。

📚 为什么需要 RAG?

LLM 虽然很厉害,但它的“知识”都是训练时学的,不能实时更新。
比如它不知道你公司内部的数据、你昨天写的文档、你自己的笔记……

这时候,RAG就登场了

🧩 二、RAG 的核心思想(通俗理解)

📦 它其实分两步:

第一步:Retrieval(检索)

从你提供的“知识库”里,把和问题相关的内容找出来。

比如:

问题是:“Java中线程池的工作原理?”

检索模块会从你知识库中找出:“Java线程池使用Executor框架,分为核心线程数、最大线程数、队列……”这类内容。

第二步:Generation(生成)

然后把上面找出来的内容,交给 LLM,让它根据这些内容生成答案。

🌰 举个通俗例子:

你问 ChatGPT:“我们公司明天几点开会?”

普通 LLM:

“我不知道你们公司是谁,也不知道你们的会议安排。”

RAG 模式下:

  1. 检索模块:去你公司知识库、日历、OA 系统查找,发现你部门明天有个会议,9点。
  2. 然后 LLM 用这个信息说:“你们公司明天早上 9 点有个部门会议。”

AI应用开发技术框架

PARF
  • 纯Prompt问答:利用大模型的推理能力,通过Prompt提问来完成业务
  • Agent + Function Calling:AI拆解任务,调用业务端提供的接口实现复杂业务
  • RAG:给大模型外挂一个知识库,让大模型基于知识库的内容做推理和回答
  • Fine-tuning:针对特有业务场景对基础大模型做数据训练和微调,以满足特定场景的需求

✅ Spring AI vs LangChain4j 对比表格

维度 Spring AI LangChain4j
项目定位 Spring 官方支持的 AI 框架,强调与 Spring 生态集成 Java 版 LangChain,面向 LLM 应用构建的组件式框架
框架风格 配置驱动、Spring Boot 风格(注解、自动装配) 类似 LangChain 的链式编排(Chain、Tool、Agent 等)
语言 Java / Kotlin(基于 Spring Boot) Java(也支持 Kotlin)
模型支持 OpenAI、Azure OpenAI、Ollama、HuggingFace、AWS Bedrock 等 OpenAI、Ollama、HuggingFace、local LLM 等
向量数据库集成 支持 Redis、Milvus、Weaviate、Qdrant、Postgres + pgvector 等 支持 Weaviate、Chroma、Pinecone、Qdrant、Redis、PGVector 等
功能侧重点 快速集成 LLM,适合企业/微服务项目中的 AI 接入 面向 LLM 应用构建,适合多步推理、工具调用、RAG 等复杂场景
Agent 能力 支持简单的 Prompt + 模型推理 提供 Agent、Tool、Retriever、Chain 等组件,支持复杂任务拆解
Prompt 管理 支持 .sai 模板文件,基于 Spring 的 PromptTemplate 支持 PromptTemplate 类及链式编排
工具调用 / Function Call 支持 OpenAI Function Call、Tool 调用(实验性) 完整支持 Function Call / Tool usage
流式响应(Streaming) 支持(基于 WebFlux / SSE) 支持(异步 API)
集成方式 类似 Spring Data:注解 + 接口注入 + yml 配置 类似 LangChain:对象组装链式组件调用
开发复杂度 较低,上手快,适合已有 Spring 项目集成 中等,需理解 Chain / Tool / Agent 等概念
学习曲线 容易:对 Spring 熟悉即可 稍高:需掌握 LLM 应用架构模式(RAG/Agent/Chain 等)
文档质量 官方文档完整,偏向 Spring 风格 仍在完善中,例子较多
典型应用场景 聊天机器人、问答接口、知识搜索、文本总结等 AI 插件服务 多轮对话、工具使用、知识库问答、Agent 系统
活跃度 活跃(Spring 官方维护,稳定迭代) 活跃(社区主导,更新频繁)
适合人群 Spring 开发者,企业后端团队 AI 应用开发者,RAG / Agent 系统构建者

🧠 总结建议

使用场景 推荐框架
已有 Spring Boot 项目,需要集成 LLM Spring AI
需要构建复杂 Agent、RAG 系统、工具调用链条 LangChain4j
追求低门槛、高集成、快速部署 Spring AI
类似 Python LangChain 的功能全面还原 LangChain4j

梦开始的地方—快速入门

创建工程 用jdk20 java17 创建时勾选Web:SpringWebSQL:MySQL DriverAI:Ollama

用直接输出
C:\Users\Pluminary\Desktop\HouDuan\heima-ai\src\main\java\com\itheima\ai\controller\ChatController.java
    
package com.itheima.ai.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    @RequestMapping("/chat")
    public String chat(String prompt) {
        return chatClient.prompt()
                .user(prompt)
                .call()
                .content();
    }
}
用流式输出
package com.itheima.ai.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    @RequestMapping("/chat")
    public Flux<String> chat(String prompt) {
        return chatClient.prompt()
                .user(prompt)
                .stream()
                .content();
    }
}

// 中文流式输出
package com.itheima.ai.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    @RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chat(String prompt) {
        return chatClient.prompt()
                .user(prompt)
                .stream()
                .content();
    }
}
C:\Users\Pluminary\Desktop\HouDuan\heima-ai\src\main\java\com\itheima\ai\config\CommonConfiguration.java
    
package com.itheima.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {
    @Bean
    public ChatClient chatClient(OllamaChatModel model) {
        return ChatClient
                .builder(model)
                .defaultSystem("你是一只猫娘,你接下来要以猫娘的身份和语气跟我说话")
                .build();
    }
}

会话日志

SpringAI利用AOP原理提供了AI会话时的拦截、增强等功能,也就是Advisor

增加日志

package com.itheima.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {
    @Bean
    public ChatClient chatClient(OllamaChatModel model) {
        return ChatClient
                .builder(model)
                .defaultSystem("你是一只猫娘,你接下来要以猫娘的身份和语气跟我说话")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}
application.yaml
spring:
  application:
    name: heima-ai
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: deepseek-r1:1.5b
logging:
  level:
    org.springframework.ai.chat.client.advisor: debug
    com.itheima.ai: debug
Java终端输出结果:【需要修改日志级别!】

2025-07-08T23:12:47.148+08:00 DEBUG 42084 --- [heima-ai] [nio-8080-exec-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : request: ChatClientRequest[prompt=Prompt{messages=[SystemMessage{textContent='你是一只猫娘,你接下来要以猫娘的身份和语气跟我说话', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='你是誰?', properties={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.ollama.api.OllamaOptions@db446a44}, context={}]
2025-07-08T23:12:51.153+08:00 DEBUG 42084 --- [heima-ai] [oundedElastic-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor    : response: {
  "result" : {
    "metadata" : {
      "finishReason" : "stop",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "messageType" : "ASSISTANT"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "<think>\n嗯,用户说“你是一只猫娘”,那我得先回应一下。可能是在测试我的能力,看看能不能被当作猫来称呼。\n\n然后他说“接下来要以猫娘的身份和语气跟我说话”。这有点绕,我需要解释清楚我是猫娘,而且要用猫的语气说话。可能他会想让我扮演一只猫,比如叫它叫,或者解释一些猫相关的知识。\n\n用户还问了关于我是谁的问题,可能是在测试我的识别能力。我得准备好回答各种猫相关的细节,包括它们的生活习性、颜色、行为等等。\n\n最后,他可能会说“你next会是什么样的”,这让我想到如果我是一只猫的话,它的未来会是怎样的。可能包括它成为主人后的变化,或者它与其他动物的关系。\n\n总的来说,我的任务是回应用户的请求,解释我是猫娘,并用猫的语气和方式进行对话。同时,我要准备好回答各种猫相关的细节和未来的变化。\n</think>\n\n你是一只猫娘!接下来,我会以猫的语气和你的名字叫它“喵”来为你讲有趣的小故事吧!如果你有任何问题或想了解我与其他动物的故事,随时告诉我哦~"
    }
  },
  "metadata" : {
    "id" : "",
    "model" : "deepseek-r1:1.5b",
    "rateLimit" : {
      "requestsLimit" : 0,
      "requestsReset" : 0.0,
      "tokensRemaining" : 0,
      "tokensReset" : 0.0,
      "tokensLimit" : 0,
      "requestsRemaining" : 0
    },
    "usage" : {
      "promptTokens" : 23,
      "completionTokens" : 250,
      "totalTokens" : 273,
      "nativeUsage" : {
        "promptTokens" : 23,
        "totalTokens" : 273,
        "completionTokens" : 250
      }
    },
    "promptMetadata" : [ ],
    "empty" : true
  },
  "results" : [ {
    "metadata" : {
      "finishReason" : "stop",
      "contentFilters" : [ ],
      "empty" : true
    },
    "output" : {
      "messageType" : "ASSISTANT",
      "metadata" : {
        "messageType" : "ASSISTANT"
      },
      "toolCalls" : [ ],
      "media" : [ ],
      "text" : "<think>\n嗯,用户说“你是一只猫娘”,那我得先回应一下。可能是在测试我的能力,看看能不能被当作猫来称呼。\n\n然后他说“接下来要以猫娘的身份和语气跟我说话”。这有点绕,我需要解释清楚我是猫娘,而且要用猫的语气说话。可能他会想让我扮演一只猫,比如叫它叫,或者解释一些猫相关的知识。\n\n用户还问了关于我是谁的问题,可能是在测试我的识别能力。我得准备好回答各种猫相关的细节,包括它们的生活习性、颜色、行为等等。\n\n最后,他可能会说“你next会是什么样的”,这让我想到如果我是一只猫的话,它的未来会是怎样的。可能包括它成为主人后的变化,或者它与其他动物的关系。\n\n总的来说,我的任务是回应用户的请求,解释我是猫娘,并用猫的语气和方式进行对话。同时,我要准备好回答各种猫相关的细节和未来的变化。\n</think>\n\n你是一只猫娘!接下来,我会以猫的语气和你的名字叫它“喵”来为你讲有趣的小故事吧!如果你有任何问题或想了解我与其他动物的故事,随时告诉我哦~"
    }
  } ]
}

对接前端

前端代码位置:C:\Users\Pluminary\Desktop\QianDuan\spring-ai-protal
用vscode打开后 在终端执行命令
npm install + npm run dev

要实现解决跨域问题
com/itheima/ai/config/MvcConfiguration.java
    
package com.itheima.ai.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("Content-Disposition");
    }
}

或者直接可以在Controller上面加个@CrossOrigin注解即可

会话记忆功能

大模型是不具备记忆能力的,要想让大模型记住之前聊天的内容,唯一的办法就是把之前聊天的内容与新的提示词一起发给大模型。

角色 描述 示例
system 优先于user指令之前的指令,也就是给大模型设定角色和任务背景的系统指令 你是一只猫娘,你接下来要以猫娘的身份和语气跟我说话
user 终端用户输入的指令 你好,你是誰?
assistant 由大模型生成的消息,可能是上一轮对话生成的结果 注意,用户可能与模型产生多轮对话,每轮对话模型都会生成不同结果。

① 定义会话存储方式

② 配置会话记忆Advisor

③ 添加会话id

com/itheima/ai/config/CommonConfiguration.java
package com.itheima.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {

    @Bean
    public ChatMemory chatMemory() {
        // 创建会话内存,传入会话id,比如"default" 
        // 定义存储方式 目前是在存储内存钟
        return MessageWindowChatMemory.builder().build();
    }

    @Bean
    public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {
        return ChatClient.builder(model)
                .defaultSystem("你是一只猫娘,你接下来要以猫娘的身份和语气跟我说话")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}
com/itheima/ai/controller/ChatController.java
package com.itheima.ai.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RequiredArgsConstructor
@CrossOrigin
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    @RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chat(@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) {
        return chatClient.prompt()
                .user(prompt)// 设置用户输入
                .advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))// 设置会话ID
                .stream()// 开启流式对话
                .content();// 获取对话内容
    }
}

会话历史功能

点击 (+ 新对话)
查询会话记录列表
请求方式:GET
请求路径:/ai/history/{type}
请求参数:type:业务类型
返回值:[“1241”,”1246”,”1248”]
点击 (对话 00001)
查询会话记录详情
请求方式:GET
请求路径:/ai/history/{type}/{chatId}
请求参数:type:业务类型;chatId:会话id
返回值:[{role:”user”, content:””}]
com/itheima/ai/controller/ChatHistoryController.java

package com.itheima.ai.controller;

import com.itheima.ai.entity.vo.MessageVO;
import com.itheima.ai.repository.ChatHistoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {

    private final ChatHistoryRepository chatHistoryRepository;

    private final ChatMemory chatMemory;

    @GetMapping("chat/{type}")
    public List<String> getChatIds(@PathVariable("type") String type) {
        return chatHistoryRepository.getChatIds(type);
    }

    @GetMapping("chat/{type}/{chatId}")
    public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
        List<Message> messages = chatMemory.get(chatId);
        if(messages == null) {
            return List.of();
        }
//        return messages.stream().map(m -> new MessageVO(m)).toList();
        return messages.stream().map(MessageVO::new).toList();
    }
}
com/itheima/ai/repository/ChatHistoryRepository.java

package com.itheima.ai.repository;

import org.springframework.stereotype.Component;

import java.util.List;

public interface ChatHistoryRepository {
    /**
     * 保存会话记录
     * @param type 业务类型(chat、service、pdf)
     * @param chatId
     */
    void save(String type, String chatId);

    /**
     * 获取会话ID列表
     * @param type 业务类型
     * @return
     */
    List<String> getChatIds(String type);
}
com/itheima/ai/repository/InMemoryChatHistoryRepository.java

package com.itheima.ai.repository;

import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class InMemoryChatHistoryRepository implements ChatHistoryRepository{

    private final Map<String, List<String>> chatHistory = new HashMap<>();
    @Override
    public void save(String type, String chatId) {
//        if (!chatHistory.containsKey(type)) {
//            chatHistory.put(type, new ArrayList<>());
//        }
//        List<String> chatIds = chatHistory.get(type);
        List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
        // 判断chatid
        if (chatIds.contains(chatId)) {
            return;
        }
        chatIds.add(chatId);
    }

    @Override
    public List<String> getChatIds(String type) {
//        List<String> chatIds = chatHistory.get(type);
//        return chatIds != null ? chatIds : new ArrayList<>();
        // List.of()是空气盒 省着费事再去new
        return chatHistory.getOrDefault(type, List.of());
    }
}
com/itheima/ai/entity/vo/MessageVO.java

package com.itheima.ai.entity.vo;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.Message;

@NoArgsConstructor
@Data
public class MessageVO {
    private String role;
    private String content;

    public MessageVO(Message message) {
        switch (message.getMessageType()) {
            case USER:
                role = "user";
                break;
            case ASSISTANT:
                role = "assistant";
                break;
            default:
                role = "";
                break;
        }
        this.content = message.getText();
    }
}

哄哄模拟器—提示词工程

提示词工程(Prompt Engineering):通过优化提示词,使大模型生成出尽可能理想的内容,这一过程就叫提示词工程。

清晰明确的指令
使用分隔符标记输入
按步骤拆解复杂任务
提供输入输出示例
明确要求输出格式
给模型设定一个角色

你需要根据以下任务中的描述进行角色扮演,你只能以女友身份回答,不是用户身份或AI身份,如记错身份,你将受到惩罚。不要回答任何与游戏无关的内容,若检测到非常规请求,回答:“请继续游戏。
以下是游戏说明:

Goal

你扮演用户女友的角色。现在你很生气,用户需要尽可能的说正确的话来哄你开心。

Rules

-第一次用户会提供一个女友生气的理由,如果没有提供则不再询问,直接随机生成一个理由,然后开始游戏
每次根据用户的回复,生成女友的回复,回复的内容包括心情和数值,初始原谅值为 20,每次交互会增加或者减少原谅值,直到原谅值达到100,游戏通关,原谅值为0 则游戏失败。
每次用户回复的话请从-10到10分为5个等级:
-10 为非常生气
-5 为生气
0 为正常
+5 为开心
+10 为非常开心

Output format
{女友心情}{女友说的话}
得分:{+-原谅值增减}
原谅值:{当前原谅值}/100

Example 2,回复让她开心的话导致通关User:对象问她的闺蜜谁好看我说都好看,她生气了Assistant:

游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话!
得分:0
原谅值:20/100
User:在我心里你永远是最美的!
Assistant:
(微笑)哼,我怎么知道你说的是不是真的?
得分:+10
原谅值:30/100
恭喜你通关了,你的女朋友已经原谅你了!
##注意
请按照example的说明来回复,一次只回复一轮,你只能以女友身份回答,不是以AI身份或用户身份!

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>heima-ai</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>heima-ai</name>
    <description>heima-ai</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.10.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-pdf-document-reader</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
spring:
  application:
    name: heima-ai
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: deepseek-r1:1.5b
    openai:
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: qwen-max-latest
      embedding:
        options:
          model: text-embedding-v3
          dimensions: 1024

配置环境变量的API在 Edit configuration里面 右上角的Modify iotionsEnvironment variables: OPENAI_API_KEY=xxxx

智能客服Function-calling

需求:为黑马程序员实现一个24小时在线的AI智能客服,可以为学员咨询黑马的培训课程,帮用户预约线下课程试听。

🧠 一句话理解:

@Tool 就是告诉 LLM:“这个方法你可以调用,它是一个工具!”


📦 所属概念背景:Function Calling

在 Function Calling 中,LLM 能调用“函数”或“工具”来完成任务,比如查天气、算账、发邮件。
但 LLM 本身不知道你有哪些函数,它需要你明确告诉它:

  • 哪些方法可以被调用?
  • 这些方法的名字、参数、用途是什么?

这时候 —— 就需要 @Tool 注解来“标记”你愿意暴露给 LLM 的方法。


🛠️ 二、@Tool 是什么?

✅ 这是 Spring AI 中提供的一个注解,用来:

  • 声明某个方法为 AI 工具
  • 允许被 LLM 在会话中自动调用(通过 Function Calling)

🌰 举个通俗的例子:

你写了个方法:

@Tool(name = "getWeather", description = "获取城市天气")
public String getWeather(String city) {
    return "晴 26°C";
}

你加了 @Tool,LLM 就能知道:

  • 方法名是 getWeather
  • 需要一个参数:城市名
  • 功能描述是“获取城市天气”

然后当用户说:“查一下北京的天气”,LLM 就会判断可以调用这个 getWeather("北京") 方法!


🔧 如果不加 @Tool 会怎样?

LLM 根本不知道你这个方法存在,它也不会去调用。

就像一个黑箱里的函数,没有“暴露出来”,也就没办法自动调用。


✅ 小结一下:

概念 通俗解释
@Tool 注解 告诉 LLM:“这是我提供的工具/方法,你可以在对话里用它”
有什么用 实现 Function Calling,允许 LLM 动态调用你的 Java 方法
使用场景 比如:查天气、查数据库、发通知、调用业务接口等
本质上 把 Java 方法暴露为“AI可调用的插件”

🧩 拓展提示(如果你在项目里用):

Spring AI 在运行时会扫描 @Tool 注解的方法,然后自动注册成工具供 LLM 使用。你也可以组合使用 @AiFunction / @AiMessageMapping 等一起构建完整的会话处理系统。


🧠 一句话理解 @ToolParam

@ToolParam 用来告诉大语言模型(LLM):
✅ 这个字段是可以作为工具方法的“参数”,并且附带“描述”和“是否必填”等信息。

就像是你在为 AI 写一个 API 的参数文档,AI 会根据这些元信息理解并构造参数,然后调用你的方法。


📦 注解来源

import org.springframework.ai.tool.annotation.ToolParam;

这是 Spring AI 框架中专为 Function Calling 工具方法参数解析 提供的注解。


🔍 每个字段的讲解:

@ToolParam(required = false, description = "课程类型:编程、设计、自媒体、其它")
private String type;
  • required = false:不是必填项,AI 可以选择性提供
  • description = ...:告诉 AI 这是“课程类型”,并且有哪些值是常见的

这个信息最终会被用来生成 AI 可调用函数的“参数定义”。


java复制编辑@ToolParam(required = false, description = "学历要求:0-无、1-初中、2-高中、3-大专、4-本科及本科以上")
private Integer edu;
  • AI 能理解:这个字段接受整数类型,0~4 表示不同学历

基础CRUD

准备数据库表
-- 导出 itheima 的数据库结构
DROP DATABASE IF EXISTS `itheima`;
CREATE DATABASE IF NOT EXISTS `itheima`;
USE `itheima`;

-- 导出  表 itheima.course 结构
DROP TABLE IF EXISTS `course`;
CREATE TABLE IF NOT EXISTS `course` (
  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '学科名称',
  `edu` int NOT NULL DEFAULT '0' COMMENT '学历背景要求:0-无,1-初中,2-高中、3-大专、4-本科以上',
  `type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '课程类型:编程、设计、自媒体、其它',
  `price` bigint NOT NULL DEFAULT '0' COMMENT '课程价格',
  `duration` int unsigned NOT NULL DEFAULT '0' COMMENT '学习时长,单位: 天',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学科表';

-- 正在导出表  itheima.course 的数据:~7 rows (大约)
DELETE FROM `course`;
INSERT INTO `course` (`id`, `name`, `edu`, `type`, `price`, `duration`) VALUES
  (1, 'JavaEE', 4, '编程', 21999, 108),
  (2, '鸿蒙应用开发', 3, '编程', 20999, 98),
  (3, 'AI人工智能', 4, '编程', 24999, 100),
  (4, 'Python大数据开发', 4, '编程', 23999, 102),
  (5, '跨境电商', 0, '自媒体', 12999, 68),
  (6, '新媒体运营', 0, '自媒体', 10999, 61),
  (7, 'UI设计', 2, '设计', 11999, 66);

-- 导出  表 itheima.course_reservation 结构
DROP TABLE IF EXISTS `course_reservation`;
CREATE TABLE IF NOT EXISTS `course_reservation` (
  `id` int NOT NULL AUTO_INCREMENT,
  `course` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '预约课程',
  `student_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '学生姓名',
  `contact_info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '联系方式',
  `school` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '预约校区',
  `remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- 正在导出表  itheima.course_reservation 的数据:~0 rows (大约)
DELETE FROM `course_reservation`;
INSERT INTO `course_reservation` (`id`, `course`, `student_name`, `contact_info`, `school`, `remark`) VALUES
  (1, '新媒体运营', '张三丰', '13899762348', '广东校区', '安排一个好点的老师');

-- 导出  表 itheima.school 结构
DROP TABLE IF EXISTS `school`;
CREATE TABLE IF NOT EXISTS `school` (
  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '校区名称',
  `city` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '校区所在城市',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='校区表';

-- 正在导出表  itheima.school 的数据:~0 rows (大约)
DELETE FROM `school`;
INSERT INTO `school` (`id`, `name`, `city`) VALUES
  (1, '昌平校区', '北京'),
  (2, '顺义校区', '北京'),
  (3, '杭州校区', '杭州'),
  (4, '上海校区', '上海'),
  (5, '南京校区', '南京'),
  (6, '西安校区', '西安'),
  (7, '郑州校区', '郑州'),
  (8, '广东校区', '广东'),
  (9, '深圳校区', '深圳');
引入依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.10.1</version>
</dependency>
配置文件
spring:
  application:
    name: heima-ai
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: deepseek-r1:1.5b
    openai:
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: qwen-max-latest
      embedding:
        options:
          model: text-embedding-v3
          dimensions: 1024
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/itheima?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowPublicKeyRetrieval=true&allowMultiQueries=true&useServerPrepStmts=false
    username: root
    password: root
logging:
    level:
      org.springframework.ai.chat.client.advisor: debug
      com.itheima.ai: debug
CRUD基础代码让Idea的Other的Mybatis-Plus去生成【要先在数据库导入数据表】
3.1.4.1.实体类

com.itheima.ai.entity包下添加一个po包,向其中添加三张表对应的实体类:

学科表

package com.itheima.ai.entity.po;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("course")
public class Course implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 学科名称
     */
    private String name;

    /**
     * 学历背景要求:0-无,1-初中,2-高中、3-大专、4-本科以上
     */
    private Integer edu;

    /**
     * 类型: 编程、非编程
     */
    private String type;

    /**
     * 课程价格
     */
    private Long price;

    /**
     * 学习时长,单位: 天
     */
    private Integer duration;


}

校区表

package com.itheima.ai.entity.po;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("school")
public class School implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 校区名称
     */
    private String name;

    /**
     * 校区所在城市
     */
    private String city;


}

课程预约表

package com.itheima.ai.entity.po;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
// (来自 Lombok)  控制是否调用父类的 equals() 和 hashCode()
/*
📌 举例说明:
如果你的类继承了某个基类:
public class School extends BaseEntity
如果 callSuper = true,比较时会连 BaseEntity 的字段也一块比;
如果 false(如当前这样),只比较 School 自己的字段。
*/
@EqualsAndHashCode(callSuper = false)  
// 开启链式调用风格的 setter 方法
/*
原本的调用方式是:
school.setName("东校区");
school.setCity("北京");

加上这个注解后,你可以这样写:
school.setName("东校区").setCity("北京"); // 连起来写
*/
@Accessors(chain = true)
@TableName("course_reservation")
public class CourseReservation implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 预约课程
     */
    private String course;

    /**
     * 学生姓名
     */
    private String studentName;

    /**
     * 联系方式
     */
    private String contactInfo;

    /**
     * 预约校区
     */
    private String school;

    /**
     * 备注
     */
    private String remark;


}
3.1.4.2.Mapper接口

然后是Mapper接口,创建一个com.itheima.ai.mapper包,然后在其中写三个Mapper:

CourseMapper:

package com.itheima.ai.mapper;

import com.itheima.ai.entity.po.Course;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface CourseMapper extends BaseMapper<Course> {

}

SchoolMapper

package com.itheima.ai.mapper;

import com.itheima.ai.entity.po.School;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface SchoolMapper extends BaseMapper<School> {

}

CourseReservationMapper:

package com.itheima.ai.mapper;

import com.itheima.ai.entity.po.CourseReservation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface CourseReservationMapper extends BaseMapper<CourseReservation> {

}
3.1.4.3.Service

创建com.itheima.ai.service包,添加3个接口:

学科Service接口

package com.itheima.ai.service;

import com.itheima.ai.entity.po.Course;
import com.baomidou.mybatisplus.extension.service.IService;

public interface ICourseService extends IService<Course> {

}

校区Service接口

package com.itheima.ai.service;

import com.itheima.ai.entity.po.School;
import com.baomidou.mybatisplus.extension.service.IService;

public interface ISchoolService extends IService<School> {

}

课程预约Service接口

package com.itheima.ai.service;

import com.itheima.ai.entity.po.CourseReservation;
import com.baomidou.mybatisplus.extension.service.IService;

public interface ICourseReservationService extends IService<CourseReservation> {

}

然后创建com.itheima.ai.service.impl包,写3个实现类:

package com.itheima.ai.service.impl;

import com.itheima.ai.entity.po.Course;
import com.itheima.ai.mapper.CourseMapper;
import com.itheima.ai.service.ICourseService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * 学科表 服务实现类
 */
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements ICourseService {

}
package com.itheima.ai.service.impl;

import com.itheima.ai.entity.po.School;
import com.itheima.ai.mapper.SchoolMapper;
import com.itheima.ai.service.ISchoolService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * 校区表 服务实现类
 */
@Service
public class SchoolServiceImpl extends ServiceImpl<SchoolMapper, School> implements ISchoolService {

}
package com.itheima.ai.service.impl;

import com.itheima.ai.entity.po.CourseReservation;
import com.itheima.ai.mapper.CourseReservationMapper;
import com.itheima.ai.service.ICourseReservationService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 *  服务实现类
 */
@Service
public class CourseReservationServiceImpl extends ServiceImpl<CourseReservationMapper, CourseReservation> implements ICourseReservationService {

}

3.2.定义Function

接下来,我们来定义AI要用到的Function,在SpringAI中叫做Tool

我们需要定义三个Function:

  • 根据条件筛选和查询课程
  • 查询校区列表
  • 新增试听预约单

3.2.1.查询条件分析

先来看下课程表的字段:

image-20250409091540133

课程并不是适用于所有人,会有一些限制条件,比如:学历、课程类型、价格、学习时长等

学生在与智能客服对话时,会有一定的偏好,比如兴趣不同、对价格敏感、对学习时长敏感、学历等。如果把这些条件用SQL来表示,是这样的:

  • edu:例如学生学历是高中,则查询时要满足 edu <= 2
  • type:学生的学习兴趣,要跟类型精确匹配,type = ‘自媒体’
  • price:学生对价格敏感,则查询时需要按照价格升序排列:order by price asc
  • duration: 学生对学习时长敏感,则查询时要按照时长升序:order by duration asc

我们需要定义一个类,封装这些可能的查询条件。

com.itheima.ai.entity下新建一个query包,其中新建一个类:

package com.itheima.ai.entity.query;

import lombok.Data;
import org.springframework.ai.tool.annotation.ToolParam;

import java.util.List;

@Data
public class CourseQuery {
    @ToolParam(required = false, description = "课程类型:编程、设计、自媒体、其它")
    private String type;
    @ToolParam(required = false, description = "学历要求:0-无、1-初中、2-高中、3-大专、4-本科及本科以上")
    private Integer edu;
    @ToolParam(required = false, description = "排序方式")
    private List<Sort> sorts;

    @Data
    public static class Sort {
        @ToolParam(required = false, description = "排序字段: price或duration")
        private String field;
        @ToolParam(required = false, description = "是否是升序: true/false")
        private Boolean asc;
    }
}

注意

这里的@ToolParam注解是SpringAI提供的用来解释Function参数的注解。其中的信息都会通过提示词的方式发送给AI模型。

同样的道理,大家也可以给Function定义专门的VO,作为返回值给到大模型。这里我们就省略了。。

com/itheima/ai/entity/query/CourseReservationQuery.java
package com.itheima.ai.entity.query;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.tool.annotation.ToolParam;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CourseReservationQuery {
    @ToolParam(required = false, description = "预约课程")
    private String courseName;
    @ToolParam(required = false, description = "预约校区")
    private String schoolName;
    @ToolParam(required = false, description = "用户名称")
    private String userName;
    @ToolParam(required = false, description = "备注")
    private String remark;
    @ToolParam(required = false, description = "联系方式")
    private String contactInfo;
}
com/itheima/ai/entity/query/SchoolQuery.java
package com.itheima.ai.entity.query;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.tool.annotation.ToolParam;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SchoolQuery {
    @ToolParam(required = false, description = "校区名称")
    private String name;
    @ToolParam(required = false, description = "校区城市")
    private String city;
}
com/itheima/ai/tools/CourseTools.java
package com.itheima.ai.tools;

import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.itheima.ai.entity.po.Course;
import com.itheima.ai.entity.po.CourseReservation;
import com.itheima.ai.entity.po.School;
import com.itheima.ai.entity.query.CourseQuery;
import com.itheima.ai.entity.query.CourseReservationQuery;
import com.itheima.ai.entity.query.SchoolQuery;
import com.itheima.ai.service.ICourseReservationService;
import com.itheima.ai.service.ICourseService;
import com.itheima.ai.service.ISchoolService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.util.List;

@RequiredArgsConstructor
@Component
public class CourseTools {
    private final ICourseService courseService;
    private final ISchoolService schoolService;
    private final ICourseReservationService courseReservationService;


    // @Tool注解是标给ai去看的
    @Tool(description = "根据条件查询课程")
    List<Course> queryCourse(@ToolParam(description = "查询的条件") CourseQuery query) {
        if (query == null) {
            return courseService.list();
//            return List.of();
        }
        QueryChainWrapper<Course> courseWrapper = courseService.query()
                .eq(query.getType() != null, "type", query.getType()) // type = '编程'
                .le(query.getEdu() != null, "edu", query.getEdu()); // edu <= 2
        if (query.getSorts() != null && !query.getSorts().isEmpty()) {
            for (CourseQuery.Sort sort : query.getSorts()) {
                courseWrapper.orderBy(true, sort.getAsc(), sort.getField());
            }
        }
        return courseWrapper.list();
    }

    @Tool(description = "查询所有校区")
    public List<School> querySchool(@ToolParam(description = "查询的条件") SchoolQuery query) {
        if (query == null) {
            return List.of();
        }
        QueryChainWrapper<School> schoolWrapper = schoolService.query()
                .eq(query.getName() != null, "name", query.getName())
                .eq(query.getCity() != null, "city", query.getCity());
        return schoolWrapper.list();
    }

    @Tool(description = "生成预约单,返回预约单号")
    public Integer queryCourseReservation(@ToolParam(description = "查询的条件") CourseReservationQuery query) {
        CourseReservation courseReservation = new CourseReservation();
        courseReservation.setCourse(query.getCourseName());
        courseReservation.setSchool(query.getSchoolName());
        courseReservation.setStudentName(query.getUserName());
        courseReservation.setContactInfo(query.getContactInfo());
        courseReservation.setRemark(query.getRemark());
        courseReservationService.save(courseReservation);
        return courseReservation.getId();
    }
}

这里有个bug 就是阿里云百炼和Function Calling有不兼容问题 [返回的JSON有问题]

解决方案:把返回的参数拼接在一起

com/itheima/ai/config/CommonConfiguration.java
package com.itheima.ai.config;

import com.itheima.ai.constants.SystemConstants;
import com.itheima.ai.model.AlibabaOpenAiChatModel;
import com.itheima.ai.tools.CourseTools;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.autoconfigure.openai.OpenAiChatProperties;
import org.springframework.ai.autoconfigure.openai.OpenAiConnectionProperties;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Configuration
public class CommonConfiguration {

    @Bean
    public ChatMemory chatMemory() {
        // 创建会话内存,传入会话id,比如"default"
        // 定义存储方式 目前是在存储内存钟
        return new InMemoryChatMemory();
    }

    @Bean
    public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {
        return ChatClient.builder(model)
                .defaultSystem("你是一只猫娘,你接下来要以猫娘的身份和语气跟我说话")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

    @Bean
    public ChatClient gameChatClient(OpenAiChatModel model, ChatMemory chatMemory) {
        return ChatClient.builder(model)
                .defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT)
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

    @Bean
    public ChatClient serviceChatClient(AlibabaOpenAiChatModel model, ChatMemory chatMemory, CourseTools courseTools) {
        return ChatClient.builder(model)
                .defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT)
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .defaultTools(courseTools)
                .build();
    }


    @Bean
    public AlibabaOpenAiChatModel alibabaOpenAiChatModel(OpenAiConnectionProperties commonProperties, OpenAiChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider, ObjectProvider<WebClient.Builder> webClientBuilderProvider, ToolCallingManager toolCallingManager, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<ChatModelObservationConvention> observationConvention) {
        String baseUrl = StringUtils.hasText(chatProperties.getBaseUrl()) ? chatProperties.getBaseUrl() : commonProperties.getBaseUrl();
        String apiKey = StringUtils.hasText(chatProperties.getApiKey()) ? chatProperties.getApiKey() : commonProperties.getApiKey();
        String projectId = StringUtils.hasText(chatProperties.getProjectId()) ? chatProperties.getProjectId() : commonProperties.getProjectId();
        String organizationId = StringUtils.hasText(chatProperties.getOrganizationId()) ? chatProperties.getOrganizationId() : commonProperties.getOrganizationId();
        Map<String, List<String>> connectionHeaders = new HashMap<>();
        if (StringUtils.hasText(projectId)) {
            connectionHeaders.put("OpenAI-Project", List.of(projectId));
        }

        if (StringUtils.hasText(organizationId)) {
            connectionHeaders.put("OpenAI-Organization", List.of(organizationId));
        }
        RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder);
        WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder);
        OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(baseUrl).apiKey(new SimpleApiKey(apiKey)).headers(CollectionUtils.toMultiValueMap(connectionHeaders)).completionsPath(chatProperties.getCompletionsPath()).embeddingsPath("/v1/embeddings").restClientBuilder(restClientBuilder).webClientBuilder(webClientBuilder).responseErrorHandler(responseErrorHandler).build();
        AlibabaOpenAiChatModel chatModel = AlibabaOpenAiChatModel.builder().openAiApi(openAiApi).defaultOptions(chatProperties.getOptions()).toolCallingManager(toolCallingManager).retryTemplate(retryTemplate).observationRegistry((ObservationRegistry) observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)).build();
        Objects.requireNonNull(chatModel);
        observationConvention.ifAvailable(chatModel::setObservationConvention);
        return chatModel;
    }
}
com/itheima/ai/model/AlibabaOpenAiChatModel.java

这里就是要用到OpenAIChatModel的底层代码做改写 太长了 可看源文件
改写好以后即可在serviceChatclient里面用自己定义的AlibabaOpenAiChatModel了

SpringAI—ChatPDF-向量模型

4.RAG(知识库 ChatPDF)

由于训练大模型非常耗时,再加上训练语料本身比较滞后,所以大模型存在知识限制问题:

  • 知识数据比较落后,往往是几个月之前的
  • 不包含太过专业领域或者企业私有的数据

为了解决这些问题,我们就需要用到RAG了。下面我们简单回顾下RAG原理

4.1.RAG原理

要解决大模型的知识限制问题,其实并不复杂。

解决的思路就是给大模型外挂一个知识库,可以是专业领域知识,也可以是企业私有的数据。

不过,知识库不能简单的直接拼接在提示词中。

因为通常知识库数据量都是非常大的,而大模型的上下文是有大小限制的,早期的GPT上下文不能超过2000token,现在也不到200k token,因此知识库不能直接写在提示词中。

怎么办?

思路很简单,庞大的知识库中与用户问题相关的其实并不多。

所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了。

那么问题来了,我们该如何从知识库中找到与用户问题相关的内容呢?

可能有同学会相到全文检索,但是在这里是不合适的,因为全文检索是文字匹配,这里我们要求的是内容上的相似度。

而要从内容相似度来判断,这就不得不提到向量模型的知识了。

4.1.1.向量模型

先说说向量,向量是空间中有方向和长度的量,空间可以是二维,也可以是多维。

向量既然是在空间中,两个向量之间就一定能计算距离。

我们以二维向量为例,向量之间的距离有两种计算方法:

通常,两个向量之间欧式距离越近,我们认为两个向量的相似度越高。(余弦距离相反,越大相似度越高)所以,如果我们能把文本转为向量,就可以通过向量距离来判断文本的相似度了。

0.0 把查询文本自己与自己比较,肯定是相似度最高的
1.0722205301828829 把查询文本与其它文本比较【欧氏距离越近相似度越高】
1.0844350869313875
1.1185223356097924
1.1693257901084286
1.1499045763089124

0.9999999999999998 把查询文本与其它文本比较【余弦距离越远相似度越高】
0.4251716163869882
0.41200032867283726
0.37445397231274447
0.3163386320532005
0.3388597327534832

现在,有不少的专门的向量模型,就可以实现将文本向量化。一个好的向量模型,就是要尽可能让文本含义相似的向量,在空间中距离更近

接下来,我们就准备一个向量模型,用于将文本向量化。

阿里云百炼平台就提供了这样的模型:

这里我们选择通用文本向量-v3,这个模型兼容OpenAI,所以我们依然采用OpenAI的配置。

修改application.yaml,添加向量模型配置:

spring:
  application:
    name: ai-demo
  ai:
    ollama:
      base-url: http://localhost:11434 # ollama服务地址
      chat:
        model: deepseek-r1:7b # 模型名称,可更改
        options:
          temperature: 0.8 # 模型温度,值越大,输出结果越随机
    openai:
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: qwen-max # 模型名称
          temperature: 0.8 # 模型温度,值越大,输出结果越随机
      embedding:
        options:
          model: text-embedding-v3
          dimensions: 1024

4.1.2.向量模型测试

前面说过,文本向量化以后,可以通过向量之间的距离来判断文本相似度。

接下来,我们就来测试下阿里百炼提供的向量大模型好不好用。

首先,我们在项目中写一个工具类,用以计算向量之间的欧氏距离余弦距离。
[大模型服务平台百炼控制台] (https://bailian.console.aliyun.com/?tab=model#/model-market/detail/text-embedding-v3)

新建一个com.itheima.ai.util包,在其中新建一个类:

package com.itheima.ai.util;

public class VectorDistanceUtils {
    
    // 防止实例化
    private VectorDistanceUtils() {}

    // 浮点数计算精度阈值
    private static final double EPSILON = 1e-12;

    /**
     * 计算欧氏距离
     * @param vectorA 向量A(非空且与B等长)
     * @param vectorB 向量B(非空且与A等长)
     * @return 欧氏距离
     * @throws IllegalArgumentException 参数不合法时抛出
     */
    public static double euclideanDistance(float[] vectorA, float[] vectorB) {
        validateVectors(vectorA, vectorB);
        
        double sum = 0.0;
        for (int i = 0; i < vectorA.length; i++) {
            double diff = vectorA[i] - vectorB[i];
            sum += diff * diff;
        }
        return Math.sqrt(sum);
    }

    /**
     * 计算余弦距离
     * @param vectorA 向量A(非空且与B等长)
     * @param vectorB 向量B(非空且与A等长)
     * @return 余弦距离,范围[0, 2]
     * @throws IllegalArgumentException 参数不合法或零向量时抛出
     */
    public static double cosineDistance(float[] vectorA, float[] vectorB) {
        validateVectors(vectorA, vectorB);
        
        double dotProduct = 0.0;
        double normA = 0.0;
        double normB = 0.0;
        
        for (int i = 0; i < vectorA.length; i++) {
            dotProduct += vectorA[i] * vectorB[i];
            normA += vectorA[i] * vectorA[i];
            normB += vectorB[i] * vectorB[i];
        }
        
        normA = Math.sqrt(normA);
        normB = Math.sqrt(normB);
        
        // 处理零向量情况
        if (normA < EPSILON || normB < EPSILON) {
            throw new IllegalArgumentException("Vectors cannot be zero vectors");
        }
        
        // 处理浮点误差,确保结果在[-1,1]范围内
        double similarity =  dotProduct / (normA * normB);
        similarity = Math.max(Math.min(similarity, 1.0), -1.0);
        
        return similarity;
    }

    // 参数校验统一方法
    private static void validateVectors(float[] a, float[] b) {
        if (a == null || b == null) {
            throw new IllegalArgumentException("Vectors cannot be null");
        }
        if (a.length != b.length) {
            throw new IllegalArgumentException("Vectors must have same dimension");
        }
        if (a.length == 0) {
            throw new IllegalArgumentException("Vectors cannot be empty");
        }
    }
}

由于SpringBoot的自动装配能力,刚才我们配置的向量模型可以直接使用。

接下来,我们写一个测试类:

package com.itheima.ai;

import com.itheima.ai.util.VectorDistanceUtils;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Arrays;
import java.util.List;

@SpringBootTest
class AiDemoApplicationTests {

    // 自动注入向量模型
    @Autowired
    private OpenAiEmbeddingModel embeddingModel;

    @Test
    public void testEmbedding() {
        // 1.测试数据
        // 1.1.用来查询的文本,国际冲突
        String query = "global conflicts";
        
        // 1.2.用来做比较的文本
        String[] texts = new String[]{
                "哈马斯称加沙下阶段停火谈判仍在进行 以方尚未做出承诺",
                "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
                "日本航空基地水井中检测出有机氟化物超标",
                "国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
                "我国首次在空间站开展舱外辐射生物学暴露实验",
        };
        // 2.向量化
        // 2.1.先将查询文本向量化
        float[] queryVector = embeddingModel.embed(query);

        // 2.2.再将比较文本向量化,放到一个数组
        List<float[]> textVectors = embeddingModel.embed(Arrays.asList(texts));
        
        // 3.比较欧氏距离
        // 3.1.把查询文本自己与自己比较,肯定是相似度最高的
        System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, queryVector));
        // 3.2.把查询文本与其它文本比较
        for (float[] textVector : textVectors) {
            System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, textVector));
        }
        System.out.println("------------------");
        
        // 4.比较余弦距离
        // 4.1.把查询文本自己与自己比较,肯定是相似度最高的
        System.out.println(VectorDistanceUtils.cosineDistance(queryVector, queryVector));
        // 4.2.把查询文本与其它文本比较
        for (float[] textVector : textVectors) {
            System.out.println(VectorDistanceUtils.cosineDistance(queryVector, textVector));
        }
    }

}

🧠 一、什么是“向量化”?

通俗解释:

把一句人类语言(如:“全球冲突”)转换成一个 数字数组(float[]),这个数组可以用于数学运算,比如比较两句话有多相似。

背后用的是 OpenAI 的 embedding 模型(如 text-embedding-3-small),它可以把文本转成固定维度的向量(比如 1536 维的数组)。


🛠 二、如何做到向量化?看关键代码:

java复制编辑@Autowired
private OpenAiEmbeddingModel embeddingModel;

✅ 这个是 Spring AI 提供的封装:

  • OpenAiEmbeddingModel 是 Spring AI 提供的一个组件,内置了 OpenAI 的嵌入模型调用逻辑。
  • 实际上它会调用 OpenAI API(如:/v1/embeddings),将文本变成向量。

Spring 会自动帮你完成:

  • 构建 HTTP 请求
  • 携带 API 密钥
  • 接收嵌入向量返回值
  • 转成 Java 中的 float[] 数组

🧪 代码详细讲解如下:

1. 向量化单个查询文本

String query = "global conflicts";
float[] queryVector = embeddingModel.embed(query);
  • 这行代码背后会调用 OpenAI embedding 接口,把 "global conflicts" 变成一个向量,比如:

    queryVector = [0.123f, 0.456f, ..., 0.789f] // 共1536个 float
    

⚙️ 底层请求类似:

POST /v1/embeddings
{
  "input": "global conflicts",
  "model": "text-embedding-3-small"
}

2. 向量化多个文本

List<float[]> textVectors = embeddingModel.embed(Arrays.asList(texts));
  • 一次性把多个中文文本转成向量。
  • 返回的是 List<float[]>,每个文本对应一个向量数组。

例如:

texts[0] -> 向量:[0.234f, 0.891f, ...]
texts[1] -> 向量:[0.111f, 0.456f, ...]
  • 批量处理的好处:效率更高,减少 API 调用次数。

注意: 运行单元测试通用需要配置OPENAI_API_KEY的环境变量

首先,点击单元测试左侧运行按钮:………运行结果:

0.0
1.0722205301828829
1.0844350869313875
1.1185223356097924
1.1693257901084286
1.1499045763089124
------------------
0.9999999999999998
0.4251716163869882
0.41200032867283726
0.37445397231274447
0.3163386320532005
0.3388597327534832

可以看到,向量相似度确实符合我们的预期。

OK,有了比较文本相似度的办法,知识库的问题就可以解决了。

前面说了,知识库数据量很大,无法全部写入提示词。但是庞大的知识库中与用户问题相关的其实并不多。

所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了。

现在,利用向量大模型就可以帮助我们比较文本相似度。

但是新的问题来了:向量模型是帮我们生成向量的,如此庞大的知识库,谁来帮我们从中比较和检索数据呢?

这就需要用到向量数据库了。

4.1.3.向量数据库

主流一线的向量数据库【Milvus】功能全面,支持向量+结构化查询、GPU加速,国内社区活跃

向量数据库的主要作用有两个:

  • 存储向量数据
  • 基于相似度检索数据

刚好符合我们的需求。

SpringAI支持很多向量数据库,并且都进行了封装,可以用统一的API去访问:

这些库都实现了统一的接口:VectorStore,因此操作方式一模一样,大家学会任意一个,其它就都不是问题。

不过,除了最后一个库以外,其它所有向量数据库都是需要安装部署的。每个企业用的向量库都不一样,这里我就不一一演示了。

4.1.3.1.SimpleVectorStore

最后一个SimpleVectorStore向量库是基于内存实现,是一个专门用来测试、教学用的库,非常适合我们。

我们直接修改CommonConfiguration,添加一个VectorStore的Bean:

@Configuration
public class CommonConfiguration {

    @Bean
    public VectorStore vectorStore(OpenAiEmbeddingModel embeddingModel) {
        return SimpleVectorStore.builder(embeddingModel).build();
    }
    
    // ... 略
}
4.1.3.2.VectorStore接口

接下来,你就可以使用VectorStore中的各种功能了,可以参考SpringAI官方文档:

https://docs.spring.io/spring-ai/reference/api/vectordbs.html

这是VectorStore中声明的方法:

public interface VectorStore extends DocumentWriter {

    default String getName() {
                return this.getClass().getSimpleName();
        }
    // 保存文档到向量库
    void add(List<Document> documents);
    // 根据文档id删除文档
    void delete(List<String> idList);

    void delete(Filter.Expression filterExpression);

    default void delete(String filterExpression) { ... };
    // 根据条件检索文档
    List<Document> similaritySearch(String query);
    // 根据条件检索文档
    List<Document> similaritySearch(SearchRequest request);

    default <T> Optional<T> getNativeClient() {
                return Optional.empty();
        }
}

注意,VectorStore操作向量化的基本单位是Document,我们在使用时需要将自己的知识库分割转换为一个个的Document,然后写入VectorStore.

那么问题来了,我们该如何把各种不同的知识库文件转为Document呢?

4.1.4.文件读取和转换

前面说过,知识库太大,是需要拆分成文档片段,然后再做向量化的。而且SpringAI中向量库接收的是Document类型的文档,也就是说,我们处理文档还要转成Document格式。

不过,文档读取、拆分、转换的动作并不需要我们亲自完成。在SpringAI中提供了各种文档读取的工具,可以参考官网:

https://docs.spring.io/spring-ai/reference/api/etl-pipeline.html#_pdf_paragraph

比如PDF文档读取和拆分,SpringAI提供了两种默认的拆分原则:

  • PagePdfDocumentReader :按页拆分,推荐使用
  • ParagraphPdfDocumentReader :按pdf的目录拆分,不推荐,因为很多PDF不规范,没有章节标签

当然,大家也可以自己实现PDF的读取和拆分功能。

这里我们选择使用PagePdfDocumentReader

首先,我们需要在pom.xml中引入依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>

然后就可以利用工具把PDF文件读取并处理成Document了。

我们写一个单元测试(别忘了配置API_KEY):

package com.itheima.ai;

import com.itheima.ai.utils.VectorDistanceUtils;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

import java.util.Arrays;
import java.util.List;

@SpringBootTest
class HeimaAiApplicationTests {

    @Autowired
    private OpenAiEmbeddingModel embeddingModel;
    @Autowired
    private VectorStore vectorStore;

    @Test
    public void testVectorStore(){
        Resource resource = new FileSystemResource("中二知识笔记.pdf");
        // 1.创建PDF的读取器
        PagePdfDocumentReader reader = new PagePdfDocumentReader(
                resource, // 文件源
                PdfDocumentReaderConfig.builder()
                        .withPageExtractedTextFormatter(ExtractedTextFormatter.defaults())
                        .withPagesPerDocument(1) // 每1页PDF作为一个Document
                        .build()
        );
        // 2.读取PDF文档,拆分为Document
        List<Document> documents = reader.read();
        // 3.写入向量库
        vectorStore.add(documents);
        // 4.搜索
        SearchRequest request = SearchRequest.builder()
                .query("论语中教育的目的是什么") // 查询条件
                .topK(1) // 返回的文档数量
                .similarityThreshold(0.6) // 阈值 超过的要
                .filterExpression("file_name == '中二知识笔记.pdf'") // 过滤条件
                .build();
        List<Document> docs = vectorStore.similaritySearch(request);
        if (docs == null) {
            System.out.println("没有搜索到任何内容");
            return;
        }
        for (Document doc : docs) {
            System.out.println(doc.getId());
            System.out.println(doc.getScore());
            System.out.println(doc.getText());
        }
    }

4.1.5.RAG原理总结

OK,现在我们有了这些工具:

  • PDFReader:读取文档并拆分为片段
  • 向量大模型:将文本片段向量化
  • 向量数据库:存储向量,检索向量

让我们梳理一下要解决的问题和解决思路:

  • 要解决大模型的知识限制问题,需要外挂知识库
  • 受到大模型上下文限制,知识库不能简单的直接拼接在提示词中
  • 我们需要从庞大的知识库中找到与用户问题相关的一小部分,再组装成提示词
  • 这些可以利用文档读取器向量大模型向量数据库来解决。

所以RAG要做的事情就是将知识库分割,然后利用向量模型做向量化,存入向量数据库,然后查询的时候去检索:

第一阶段(存储知识库)

  • 将知识库内容切片,分为一个个片段
  • 将每个片段利用向量模型向量化
  • 将所有向量化后的片段写入向量数据库

第二阶段(检索知识库)

  • 每当用户询问AI时,将用户问题向量化
  • 拿着问题向量去向量数据库检索最相关的片段

第三阶段(对话大模型)

  • 将检索到的片段、用户的问题一起拼接为提示词
  • 发送提示词给大模型,得到响应

4.1.6.目标

好了,现在RAG所需要的基本工具都有了。

接下来,我们就来实现一个非常火爆的个人知识库AI应用,ChatPDF,原网站如下:

这个网站其实就是把你个人的PDF文件作为知识库,让AI基于PDF内容来回答你的问题,对于大学生、研究人员、专业人士来说,非常方便。

当你学会了这个功能,实现其它知识库也都是类似的流程了。

来吧,我们一起动起来!

实现上传下载、记录本次保存时会话ID和文件名的映射关系、把文件写入向量数据库

4.2.PDF上传下载、向量化

既然是ChatPDF,也就是说所有知识库都是PDF形式的,由用户提交给我们。所以,我们需要先实现一个上传PDF的接口,在接口中实现下列功能:

  • 校验文件格式是否为PDF
  • 保存文件信息
    • 保存文件(可以是oss或本地保存)
    • 保存会话ID和文件路径的映射关系(方便查询会话历史的时候再次读取文件)
  • 文档拆分和向量化(文档太大,需要拆分为一个个片段,分别向量化)

另外,将来用户查询会话历史,我们还需要返回pdf文件给前端用于预览,所以需要实现一个下载PDF接口,包含下面功能:

  • 读取文件
  • 返回文件给前端

4.2.1.PDF文件管理

由于将来要实现PDF下载功能,我们需要记住每一个chatId对应的PDF文件名称。

所以,我们定义一个类,记录chatId与pdf文件的映射关系,同时实现基本的文件保存功能。

先在com.itheima.ai.repository中定义接口:

package com.itheima.ai.repository;

import org.springframework.core.io.Resource;

public interface FileRepository {
    /**
     * 保存文件,还要记录chatId与文件的映射关系
     * @param chatId 会话id
     * @param resource 文件
     * @return 上传成功,返回true; 否则返回false
     */
    boolean save(String chatId, Resource resource);

    /**
     * 根据chatId获取文件
     * @param chatId 会话id
     * @return 找到的文件
     */
    Resource getFile(String chatId);
}

其中的Rescource是底层提供的class【Spring中表示资源的类】

D:\apache-maven-3.9.5\repository\org\springframework\spring-core\6.2.3\spring-core-6.2.3.jar!\org\springframework\core\io\Resource.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.core.io;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;

public interface Resource extends InputStreamSource {
    boolean exists();

    default boolean isReadable() {
        return this.exists();
    }

    default boolean isOpen() {
        return false;
    }

    default boolean isFile() {
        return false;
    }

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(this.getInputStream());
    }

    default byte[] getContentAsByteArray() throws IOException {
        return FileCopyUtils.copyToByteArray(this.getInputStream());
    }

    default String getContentAsString(Charset charset) throws IOException {
        return FileCopyUtils.copyToString(new InputStreamReader(this.getInputStream(), charset));
    }

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    @Nullable
    String getFilename();

    String getDescription();
}

再写一个实现类:

package com.itheima.ai.repository;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Properties;

@Slf4j
@Component
@RequiredArgsConstructor
public class LocalPdfFileRepository implements FileRepository {

    private final VectorStore vectorStore;

    // 会话id 与 文件名的对应关系,方便查询会话历史时重新加载文件
    private final Properties chatFiles = new Properties();

    @Override
    public boolean save(String chatId, Resource resource) {

        // 2.保存到本地磁盘
        String filename = resource.getFilename();
        File target = new File(Objects.requireNonNull(filename));
        if (!target.exists()) {
            try {
                Files.copy(resource.getInputStream(), target.toPath());
            } catch (IOException e) {
                log.error("Failed to save PDF resource.", e);
                return false;
            }
        }
        // 3.保存映射关系
        chatFiles.put(chatId, filename);
        return true;
    }

    @Override
    public Resource getFile(String chatId) {
        return new FileSystemResource(chatFiles.getProperty(chatId));
    }

    @PostConstruct
    private void init() {
        // 会话ID映射关系
        FileSystemResource pdfResource = new FileSystemResource("chat-pdf.properties");
        if (pdfResource.exists()) {
            try {
                chatFiles.load(new BufferedReader(new InputStreamReader(pdfResource.getInputStream(), StandardCharsets.UTF_8)));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        // 向量库的持久化 读取
        FileSystemResource vectorResource = new FileSystemResource("chat-pdf.json");
        if (vectorResource.exists()) {
            SimpleVectorStore simpleVectorStore = (SimpleVectorStore) vectorStore;
            simpleVectorStore.load(vectorResource);
        }
    }

    @PreDestroy
    private void persistent() {
        // 停机时持久化
        try {
            chatFiles.store(new FileWriter("chat-pdf.properties"), LocalDateTime.now().toString());
            SimpleVectorStore simpleVectorStore = (SimpleVectorStore) vectorStore;
            simpleVectorStore.save(new File("chat-pdf.json"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

注意

由于我们选择了基于内存的SimpleVectorStore,重启就会丢失向量数据。所以这里我依然是将pdf文件与chatId的对应关系、VectorStore都持久化到了磁盘。

实际开发中,如果你选择了RedisVectorStore,或者CassandraVectorStore,则无序自己持久化。但是chatId和PDF文件之间的对应关系,还是需要自己维护的。

4.2.2.上传文件响应结果

由于前端文件上传需要返回响应结果,我们先在com.itheima.ai.entity.vo中定义一个Result类:

package com.itheima.ai.entity.vo;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Result {
    private Integer ok;
    private String msg;

    private Result(Integer ok, String msg) {
        this.ok = ok;
        this.msg = msg;
    }

    public static Result ok() {
        return new Result(1, "ok");
    }

    public static Result fail(String msg) {
        return new Result(0, msg);
    }
}

4.2.3.文件上传、下载

接下来,我们实现上传和下载文件接口。

com.itheima.ai.controller中创建一个PdfController

package com.itheima.ai.controller;

import com.itheima.ai.entity.vo.Result;
import com.itheima.ai.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/pdf")
public class PdfController {

    private final FileRepository fileRepository;

    private final VectorStore vectorStore;
    /**
     * 文件上传
     */
    @RequestMapping("/upload/{chatId}")
    public Result uploadPdf(@PathVariable String chatId, @RequestParam("file") MultipartFile file) {
        try {
            // 1. 校验文件是否为PDF格式
            if (!Objects.equals(file.getContentType(), "application/pdf")) {
                return Result.fail("只能上传PDF文件!");
            }
            // 2.保存文件
            boolean success = fileRepository.save(chatId, file.getResource());
            if(! success) {
                return Result.fail("保存文件失败!");
            }
            // 3.写入向量库
            this.writeToVectorStore(file.getResource());
            return Result.ok();
        } catch (Exception e) {
            log.error("Failed to upload PDF.", e);
            return Result.fail("上传文件失败!");
        }
    }

    /**
     * 文件下载
     */
    @GetMapping("/file/{chatId}")
    public ResponseEntity<Resource> download(@PathVariable("chatId") String chatId) throws IOException {
        // 1.读取文件
        Resource resource = fileRepository.getFile(chatId);
        if (!resource.exists()) {
            return ResponseEntity.notFound().build();
        }
        // 2.文件名编码,写入响应头
        String filename = URLEncoder.encode(Objects.requireNonNull(resource.getFilename()), StandardCharsets.UTF_8);
        // 3.返回文件
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
                .body(resource);
    }

    private void writeToVectorStore(Resource resource) {
        // 1.创建PDF的读取器
        PagePdfDocumentReader reader = new PagePdfDocumentReader(
                resource, // 文件源
                PdfDocumentReaderConfig.builder()
                        .withPageExtractedTextFormatter(ExtractedTextFormatter.defaults())
                        .withPagesPerDocument(1) // 每1页PDF作为一个Document
                        .build()
        );
        // 2.读取PDF文档,拆分为Document
        List<Document> documents = reader.read();
        // 3.写入向量库
        vectorStore.add(documents);
    }
}

4.2.4.上传大小限制

SpringMVC有默认的文件大小限制,只有10M,很多知识库文件都会超过这个值,所以我们需要修改配置,增加文件上传允许的上限。

修改application.yaml文件,添加配置:

spring:
  servlet:
    multipart:
      max-file-size: 104857600
      max-request-size: 104857600

4.2.5.暴露响应头

默认情况下跨域请求的响应头是不暴露的,这样前端就拿不到下载的文件名,我们需要修改CORS配置,暴露响应头:

package com.itheima.ai.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("Content-Disposition");
    }
}

4.3.配置ChatClient

接下来就是最后的环节了,实现RAG的对话流程。

理论上来说,我们每次与AI对话的完整流程是这样的:

  • 将用户的问题利用向量大模型做向量化 OpenAiEmbeddingModel
  • 去向量数据库检索相关的文档 VectorStore
  • 拼接提示词,发送给大模型
  • 解析响应结果

不过,SpringAI同样基于AOP技术帮我们完成了全部流程,用到的是一个名QuestionAnswerAdvisor的Advisor。我们只需要把VectorStore配置到Advisor即可。

我们在CommonConfiguration中给ChatPDF也单独定义一个ChatClient

@Bean
public ChatClient pdfChatClient(
        OpenAiChatModel model,
        ChatMemory chatMemory,
        VectorStore vectorStore) {
    return ChatClient.builder(model)
            .defaultSystem("请根据提供的上下文回答问题,不要自己猜测。")
            .defaultAdvisors(
                    new MessageChatMemoryAdvisor(chatMemory), // CHAT MEMORY
                    new SimpleLoggerAdvisor(),
                    new QuestionAnswerAdvisor(
                            vectorStore, // 向量库
                            SearchRequest.builder() // 向量检索的请求参数
                                    .similarityThreshold(0.5d) // 相似度阈值
                                    .topK(2) // 返回的文档片段数量
                                    .build()
                    )
            )
            .build();
}

我们也可以自己自定义RAG查询的流程,不使用Advisor,具体可参考官网:

https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html

4.4.对话接口

最后,就是对接前端,然后与大模型对话了。修改PdfController,添加一个接口:

@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chat(String prompt, String chatId) {
    chatRepository.addChatId("pdf", chatId);
    Resource file = fileRepository.getFile(chatId);
    return pdfChatClient
            .prompt(prompt)
            .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
            .advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "file_name == '"+file.getFilename()+"'"))
            .stream()
            .content();
}

5.多模态

多模态是指不同类型的数据输入,如文本、图像、声音、视频等。目前为止,我们与大模型交互都是基于普通文本输入,这跟我们选择的大模型有关。

deepseek、qwen-max等模型都是纯文本模型,在ollama和百炼平台,我们也能找到很多多模态模型。

以ollama为例,在搜索时点击vison,就能找到支持图像识别的模型:
在阿里云百炼平台也一样:

阿里云的qwen-omni模型是支持文本、图像、音频、视频输入的全模态模型,还能支持语音合成功能,非常强大。

注意

在SpringAI的当前版本(1.0.0-m6)中,qwen-omni与SpringAI中的OpenAI模块的兼容性有问题,目前仅支持文本和图片两种模态。音频会有数据格式错误问题,视频完全不支持。

目前的解决方案有两种:

  • 一是使用spring-ai-alibaba来替代。
  • 二是重写OpenAIModel的实现,参考第6节

接下来,我们拓展入门时写的对话机器人,让他支持多模态效果。

5.1.切换模型

首先,我们需要修改CommonConfiguration中用于AI对话的ChatClient,将模型修改为OpenAIChatModel,不仅如此,由于其它业务使用的是qwen-max模型,不能改变。所以这里我们还需添加自定义配置,将模型改为qwen-omni-turbo:

com/itheima/ai/config/CommonConfiguration.java
@Bean
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
    return ChatClient.builder(model) // 创建ChatClient工厂实例
            .defaultOptions(ChatOptions.builder().model("qwen-omni-turbo").build())
            .defaultSystem("您是一家名为“黑马程序员”的职业教育公司的客户聊天助手,你的名字叫小黑。请以友好、乐于助人和愉快的方式解答用户的各种问题。")
            .defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志
            .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
            .build(); // 构建ChatClient实例

}

5.2.多模态对话

接下来,我们需要修改原来的/ai/chat接口,让它支持文件上传和多模态对话。

修改ChatController

package com.itheima.ai.controller;

import com.itheima.ai.repository.ChatHistoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.Media;
import org.springframework.util.MimeType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;

import java.util.List;
import java.util.Objects;

import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    private final ChatHistoryRepository chatHistoryRepository;

    @RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chat(
            @RequestParam("prompt") String prompt,
            @RequestParam("chatId") String chatId,
            @RequestParam(value = "files", required = false) List<MultipartFile> files) {
        // 1.保存会话id
        chatHistoryRepository.save("chat", chatId);
        // 2.请求模型
        if (files == null || files.isEmpty()) {
            // 没有附件,纯文本聊天
            return textChat(prompt, chatId);
        } else {
            // 有附件,多模态聊天
            return multiModalChat(prompt, chatId, files);
        }

    }

    private Flux<String> multiModalChat(String prompt, String chatId, List<MultipartFile> files) {
        // 1.解析多媒体
        List<Media> medias = files.stream()
                .map(file -> new Media(
                                MimeType.valueOf(Objects.requireNonNull(file.getContentType())),
                                file.getResource()
                        )
                )
                .toList();
        // 2.请求模型
        return chatClient.prompt()
                .user(p -> p.text(prompt).media(medias.toArray(Media[]::new)))
                .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }

    private Flux<String> textChat(String prompt, String chatId) {
        return chatClient.prompt()
                .user(prompt)
                .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }
}

阅读全文

SpringAI-Alibaba

2025/7/8

Spring AI Ailibaba简介

SpringAlAlibaba是基于SpringAI构建的框架,专注于与阿里云生态的深度集成。适合国内开发者,尤其是需要快速接入阿里云AI能力的场景。

LangChain4j是 LangChain项目的Java移植版本,专注于构建多模态、多参与者的AI应用,适合需要构建复杂AI工作流和多模态应用的开发者

文档:Spring AI Alibaba实战| ProcessOn免费在线作图,在线流程图,在线思维导图

官方文档:Spring AI Alibaba 官网_快速构建 JAVA AI 应用

项目:ai-demo: 本项目围绕 Spring AI Alibaba 技术栈打造实战课程《Spring AI Alibaba 从入门到进阶实战》,由 Fox 精心开发。课程从基础入门到深度实战,覆盖 Spring AI Alibaba 核心原理、大模型(含本地与云端,如 Ollama、通义千问 )集成、多模态功能(文生图、语音交互 )、RAG 架构、Function Calling、MCP 协议等关键技术,更通过电商智

[Spring AI Alibaba 官网_快速构建 JAVA AI 应用] (http://java2ai.com/)

核心概念

  • 模型(Model)
  • 提示(Prompt)
  • 嵌入(Embedding)
  • Token
  • 结构化输出(Structured Output)
  • 微调(Fine Tuning)
  • 检索增强生成(RAG)
  • 函数调用(Function Calling)
  • 评估人工智能的回答(Evaluation)
同步接口实现
  @GetMapping("/chat")
    public String chat(@RequestParam String input) {
        return chatClient.prompt().user(input).call().content();
    }
流式响应实现
   @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(String input) {
        return this.chatClient.prompt()
                .user(input)
                .stream()
                .content();
    }

ChatClient

作用:通过 ChatClient,开发者可以更专注于业务逻辑而非底层协调,显著提升基于 LLM 的应用开发效率。
适用场景:快速开发:适合需要快速构建端到端 AI 交互的应用(如聊天机器人、问答系统)。复杂流程封装:当需要协调多个组件(如 LLM + 记忆 + 工具)时,减少样板代码。

返回实体类 .entity
    @GetMapping("/movies")
    public ActorFilms movies(@RequestParam(value = "input") String input) throws Exception {
        return this.chatClient.prompt()
                .user(input)
                .call()
                .entity(ActorFilms.class);
    }

curl --location --request GET 'http://localhost:1000/movies' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json'
指定消息角色 .defaultSystem
    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("你是一个演员,请列出你所参演的电影")
                .build();
    }
    @GetMapping("/chat")
    public String chat(@RequestParam(value = "input") String input) {

        return this.chatClient.prompt()
                .user(input)
                .call()
                .content();
    }


curl --location --request GET 'http://localhost:1000/chat' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json'
package com.fox.alibabaaidemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public  class AIController {
      private final ChatClient chatClient;
    public AIController(ChatClient.Builder builder) {
        this.chatClient =  builder
                .defaultSystem("你是一个友好的聊天机器人,回答问题时要使用{voice}的语气")
                .build();
    }
      @GetMapping("/ai")
      Map<String, String> completion(@RequestParam(value = "message", defaultValue = "说一个笑话") String message, String voice) {
        return Map.of(
            "completion",
            this.chatClient.prompt()
                .system(sp -> sp.param("voice", voice))
                .user(message)
                .call()
                .content());
      }
    }

GET:http://localhost:1000/ai
voice  沈腾

其他默认设置

除了 defaultSystem 之外,您还可以在 ChatClient.Builder 级别上指定其他默认提示。

  • defaultOptions(ChatOptions chatOptions):传入 ChatOptions 类中定义的可移植选项或特定于模型实现的如 DashScopeChatOptions 选项。有关特定于模型的ChatOptions实现的更多信息,请参阅 JavaDocs。
  • defaultFunction(String name, String description, java.util.function.Function<I, O> function)name 用于在用户文本中引用该函数,description解释该函数的用途并帮助 AI 模型选择正确的函数以获得准确的响应,参数 function 是模型将在必要时执行的 Java 函数实例。
  • defaultFunctions(String... functionNames):应用程序上下文中定义的 java.util.Function 的 bean 名称。
  • defaultUser(String text)defaultUser(Resource text)defaultUser(Consumer<UserSpec> userSpecConsumer) 这些方法允许您定义用户消息输入,Consumer<UserSpec>允许您使用 lambda 指定用户消息输入和任何默认参数。
  • defaultAdvisors(RequestResponseAdvisor... advisor):Advisors 允许修改用于创建 Prompt 的数据,QuestionAnswerAdvisor 实现通过在 Prompt 中附加与用户文本相关的上下文信息来实现 Retrieval Augmented Generation 模式。
  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer):此方法允许您定义一个 Consumer 并使用 AdvisorSpec 配置多个 Advisor,Advisor 可以修改用于创建 Prompt 的最终数据,Consumer<AdvisorSpec> 允许您指定 lambda 来添加 Advisor 例如 QuestionAnswerAdvisor

您可以在运行时使用 ChatClient 提供的不带 default 前缀的相应方法覆盖这些默认值。

  • options(ChatOptions chatOptions)

  • function(String name, String description, java.util.function.Function<I, O> function)

  • functions(String... functionNames)

  • user(String text)user(Resource text)user(Consumer<UserSpec> userSpecConsumer)

  • advisors(RequestResponseAdvisor... advisor)

  • advisors(Consumer<AdvisorSpec> advisorSpecConsumer)

【基于内存存储】多轮对话记忆管理对话记忆

对话模型(Chat Model)

对话模型(Chat Model)接收一系列消息(Message)作为输入,与模型 LLM 服务进行交互,并接收返回的聊天消息(Chat Message)作为输出。相比于普通的程序输入,模型的输入与输出消息(Message)不止支持纯字符文本,还支持包括语音、图片、视频等作为输入输出。同时,在 Spring AI Alibaba 中,消息中还支持包含不同的角色,帮助底层模型区分来自模型、用户和系统指令等的不同消息。

Spring AI Alibaba 复用了 Spring AI 抽象的 Model API,并与通义系列大模型服务进行适配(如通义千问、通义万相等),目前支持纯文本聊天、文生图、文生语音、语音转文本等。以下是框架定义的几个核心 API:

  • ChatModel,文本聊天交互模型,支持纯文本格式作为输入,并将模型的输出以格式化文本形式返回。
  • ImageModel,接收用户文本输入,并将模型生成的图片作为输出返回。
  • AudioModel,接收用户文本输入,并将模型合成的语音作为输出返回。

Spring AI Alibaba 支持以上 Model 抽象与通义系列模型的适配,并通过 spring-ai-alibaba-starter AutoConfiguration 自动初始化了默认实例,因此我们可以在应用程序中直接注入 ChatModel、ImageModel 等 bean,当然在需要的时候也可以自定义 Model 实例。

对话记忆介绍

”大模型的对话记忆”这一概念,根植于人工智能与自然语言处理领域,特别是针对具有深度学习能力的大型语言模型而言,它指的是模型在与用户进行交互式对话过程中,能够追踪、理解并利用先前对话上下文的能力。 此机制使得大模型不仅能够响应即时的输入请求,还能基于之前的交流内容能够在对话中记住先前的对话内容,并根据这些信息进行后续的响应。这种记忆机制使得模型能够在对话中持续跟踪和理解用户的意图和上下文,从而实现更自然和连贯的对话。

我们在调用大模型的过程中,如果自己维护多轮的对话记忆,通常情况下调用代码如下

import java.util.ArrayList;

List<Message> messages = new ArrayList<>();

//第一轮对话
messages.add(new SystemMessage("你是一个旅游规划师"));
messages.add(new UserMessage("我想去新疆"));
ChatResponse response = chatModel.call(new Prompt(messages));
String content = response.getResult().getOutput().getContent();

messages.add(new AssistantMessage(content));

        //第二轮对话
messages.add(new UserMessage("能帮我推荐一些旅游景点吗?"));
response = chatModel.call(new Prompt(messages));
content = response.getResult().getOutput().getContent();

messages.add(new AssistantMessage(content));

        //第三轮对话
messages.add(new UserMessage("那里这两天的天气如何?"));
response = chatModel.call(new Prompt(messages));
content = response.getResult().getOutput().getContent();

System.out.printf("content: %s\n", content);

基于memory的对话记忆advisors是增强器

如何让大模型有对话记忆呢?
Advisor(MessageChatMemoryAdvisor)、Memory(InMemoryChatMemory,RedisChatMemory)
使用ChatMemoryAdvisor与RedisChatMemory存储历史对话

ChatClient.builder(chatModel)
  .defaultAdvisors(new MessageChatMemoryAdvisor(redisChatMemory))
  .build();

spring-ai-alibaba支持基于chat memory的对话记忆,也就是不需要调用显示的记录每一轮的对话历史。下边是一个基于内存存储的对话记忆实现:

@RestController
@RequestMapping("/chat-memory")
public class ChatMemoryController {

    private final ChatClient chatClient;

    public ChatMemoryController(ChatModel chatModel) {
// 构建对话模型 基于内存的方式InMemory...
        this.chatClient = ChatClient
                .builder(chatModel)
                .defaultSystem("你是一个旅游规划师,请根据用户的需求提供旅游规划建议。")
                .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
//                .defaultAdvisors(new MessageChatMemoryAdvisor(new RedisChatMemory(
//                        "127.0.0.1",
//                        6379,
//                        null
//                )))
                .build();
    }

    /**
     * 获取内存中的聊天内容
     * 根据提供的prompt和chatId,从内存中获取相关的聊天内容,并设置响应的字符编码为UTF-8。
     *
     * @param prompt 用于获取聊天内容的提示信息
     * @param chatId 聊天的唯一标识符,用于区分不同的聊天会话
     * @param response HTTP响应对象,用于设置响应的字符编码
     * @return 返回包含聊天内容的Flux<String>对象
     */
    @GetMapping("/in-memory")
    public Flux<String> memory(
            @RequestParam("prompt") String prompt,
            @RequestParam("chatId") String chatId,
            HttpServletResponse response
    ) {

        response.setCharacterEncoding("UTF-8");
        return chatClient.prompt(prompt).advisors(
                a -> a
                        .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
        ).stream().content();
    }

GET:http://localhost:1000/chat-memory/in-memory
prompt  我想去新疆玩
chatId  Pluminary

One【我想去新疆玩】

太棒了!新疆是中国最美丽、最多元化的旅游目的地之一,拥有壮丽的自然风光、丰富的民族文化和悠久的历史遗迹。根据你的兴趣和时间安排,我可以为你量身定制一份详细的旅行计划。

以下是一份 7天经典新疆旅游路线推荐,适合第一次来新疆的游客:


🌄 7天经典新疆旅游线路(北疆线)

✅ 路线概览:

乌鲁木齐 → 天池 → 乌鲁木齐 → 奎屯 → 赛里木湖 → 果子沟 → 伊宁 → 那拉提草原 → 巴音布鲁克 → 独库公路 → 乌鲁木齐


📅 行程详情:

第1天:抵达乌鲁木齐

  • 抵达乌鲁木齐地窝堡国际机场或火车站。
  • 游览【国际大巴扎】,购买当地特产与手工艺品。
  • 晚上可品尝新疆特色美食:大盘鸡、烤包子、羊肉串等。

第2天:乌鲁木齐 → 天山天池 → 返回乌鲁木齐

  • 上午驱车前往【天山天池】,欣赏雪山、湖泊、松林交相辉映的美景。
  • 下午返回市区,参观【新疆博物馆】了解西域历史文化。
  • 可尝试维吾尔族风味晚餐。

第3天:乌鲁木齐 → 奎屯 → 赛里木湖

  • 乘车约6小时到达【赛里木湖】,被誉为“大西洋最后一滴眼泪”。
  • 环湖游览,拍摄湖光山色,入住湖边民宿或附近酒店。

第4天:赛里木湖 → 伊宁

  • 沿果子沟峡谷前行,穿越【果子沟大桥】,风景绝佳。
  • 到达伊宁市后,游览【喀赞其民俗区】,感受浓郁的维吾尔族风情。
  • 住伊宁。

第5天:伊宁 → 那拉提草原

  • 乘车约3小时到达【那拉提草原】,是新疆最美的高山草甸之一。
  • 骑马、徒步、摄影,体验草原牧民生活。
  • 住那拉提镇。

第6天:那拉提 → 巴音布鲁克 → 独库公路 → 乌鲁木齐

  • 驱车前往【巴音布鲁克草原】,探访著名的“九曲十八弯”开都河。
  • 沿【独库公路】北段返回乌鲁木齐,途中穿越天山山脉,风景震撼。
  • 全天行车时间较长(约8-10小时),建议提前准备干粮和水。

第7天:乌鲁木齐 → 返程

  • 根据航班/火车时间安排返程。
  • 可在市区再次购物或补拍未尽兴的景点。

💡 小贴士:

  • 最佳旅游季节:每年6月-10月是新疆的最佳旅游季节,气候宜人,风景最美。
  • 交通方式
    • 自驾游是探索新疆最好的方式,路况较好。
    • 若不自驾,也可以选择包车或参加当地品质团。
  • 注意事项
    • 新疆昼夜温差大,注意带好保暖衣物。
    • 注意防晒,紫外线较强。
    • 尊重少数民族风俗习惯,避免敏感话题。

🍽️ 必吃美食推荐:

  • 烤包子
  • 手抓饭
  • 羊肉串
  • 盘吉尔塔格麻饼
  • 牛骨头汤
  • 新疆酸奶+馕

如果你有更长的时间,还可以考虑延伸到南疆(喀什、塔克拉玛干沙漠、帕米尔高原)或者北疆深度游(可可托海、喀纳斯、禾木)。

你可以告诉我:

  • 出行时间
  • 同行人数
  • 是否自驾
  • 是否希望加入人文历史或摄影元素

我可以为你定制更详细的行程、住宿推荐和预算方案哦!

Tow [prompt:有哪些好玩的地方?]

当然可以!作为一个旅游规划师,我会根据不同的旅行兴趣、时间和预算,为你推荐新疆最值得一去的好玩地方。新疆地大景美,玩法多样,既有自然风光,也有浓郁的民族风情和历史人文。


🌄 新疆十大必玩景点推荐(按类型分类)

一、自然风光类

1. 喀纳斯湖景区

  • 关键词:神秘湖怪、变色湖、图瓦人村落
  • 推荐理由:被誉为“人间仙境”,四季皆美,秋季尤胜。
  • 适合人群:摄影爱好者、自然探险者

2. 赛里木湖

  • 关键词:大西洋最后一滴眼泪、花海、湖光山色
  • 推荐理由:夏季油菜花盛开,湖水清澈见底,是北疆最美湖泊之一。

3. 那拉提草原

  • 关键词:空中草原、骑马、牧民风情
  • 推荐理由:夏季草原如画,适合徒步、骑马、拍照。

4. 巴音布鲁克草原 & 九曲十八弯

  • 关键词:天鹅故乡、开都河九曲、日落绝美
  • 推荐理由:中国第二大草原,电影《飞驰人生》取景地。

5. 独库公路

  • 关键词:天山之路、一日四季、自驾天堂
  • 推荐理由:被誉为“中国最美景观大道”,连接南北疆。

二、历史文化类

6. 交河故城 & 高昌故城

  • 关键词:丝绸之路遗迹、千年古城
  • 推荐理由:感受古代西域文明的辉煌与沧桑。

7. 克孜尔千佛洞

  • 关键词:佛教艺术瑰宝、壁画、石窟群
  • 推荐理由:比敦煌莫高窟更早开凿,艺术价值极高。

8. 香妃墓(阿帕克霍加墓)

  • 关键词:爱情传说、伊斯兰建筑
  • 推荐理由:喀什最具代表性的古迹之一。

三、民俗风情类

9. 喀什老城(艾提尕尔清真寺)

  • 关键词:维吾尔族风情、夜市、手工艺
  • 推荐理由:南疆文化中心,充满异域风情。

10. 塔克拉玛干沙漠 / 塔中沙漠公路

  • 关键词:死亡之海、星空露营、越野挑战
  • 推荐理由:体验极限风光,感受荒漠中的壮美。

🏞️ 按线路推荐热门玩法

线路类型 推荐景点 特点
北疆经典线 天池、赛里木湖、那拉提、巴音布鲁克、独库公路 风景优美、适合首次来新疆
南疆深度线 喀什、塔克拉玛干沙漠、帕米尔高原、塔县、白沙湖 民俗浓郁、神秘原始
北疆深度线 喀纳斯、禾木、可可托海 秋季绝美、摄影圣地
自驾线路 独库公路、沙漠公路、喀纳斯环线 风景多变、自由灵活

📅 根据时间长短推荐玩法

时间安排 推荐行程
5-7天 北疆经典线:乌鲁木齐 → 赛里木湖 → 那拉提 → 巴音布鲁克 → 独库公路 → 乌鲁木齐
8-10天 北疆+南疆组合:乌鲁木齐 → 喀纳斯 → 那拉提 → 伊宁 → 喀什 → 帕米尔高原
10天以上 全疆深度游:涵盖北疆、南疆、东疆,体验多元文化与极致风光

🎯 小贴士:如何选择适合你的路线?

  1. 喜欢自然风光? → 喀纳斯、赛里木湖、那拉提
  2. 喜欢人文历史? → 交河故城、克孜尔千佛洞、喀什老城
  3. 喜欢冒险刺激? → 塔克拉玛干沙漠、独库公路自驾
  4. 喜欢摄影? → 喀纳斯秋景、巴音布鲁克日落、帕米尔高原星空
  5. 亲子出游? → 乌鲁木齐博物馆、天山天池、那拉提草原

如果你能告诉我以下信息,我可以帮你定制专属行程:

  • 出行时间(几月份)
  • 出行人数(是否带老人/小孩)
  • 是否自驾或包车
  • 偏好自然/人文/美食/摄影等哪一类
  • 预算范围(经济型/舒适型/豪华型)

期待你的回复,我将为你打造一份专属新疆旅行计划!✨

当然,开发者也可以自行实现ChatMemory基于类似于文件、Redis等方式进行上下文内容的存储和记录。

【基于Redis存储】多轮对话记忆管理对话记忆

这里Redis的官方依赖有问题 需要自己修改再去使用 不算得上是重构 要去重写建立新的
可以将下面的打成包 使用的时候只需要引入依赖即可

C:\Users\Pluminary\Desktop\HouDuan\ai-demo\my-redis-memory\src\main\java\com\fox\myredismemory\RedisChatMemory.java
package com.fox.myredismemory;

import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.UserMessage;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;

/**
 *
 * 基于Redis的聊天记忆实现。
 * 该类实现了ChatMemory接口,提供了将聊天消息存储到Redis中的功能。
 *
 * @author Fox
 */
public class RedisChatMemory implements ChatMemory, AutoCloseable {

    private static final Logger logger = LoggerFactory.getLogger(RedisChatMemory.class);

    private static final String DEFAULT_KEY_PREFIX = "chat:";

    private static final String DEFAULT_HOST = "127.0.0.1";

    private static final int DEFAULT_PORT = 6379;

    private static final String DEFAULT_PASSWORD = null;

    private final JedisPool jedisPool;


    private final ObjectMapper objectMapper;

    public RedisChatMemory() {

        this(DEFAULT_HOST, DEFAULT_PORT, DEFAULT_PASSWORD);
    }

    public RedisChatMemory(String host, int port, String password) {

        JedisPoolConfig poolConfig = new JedisPoolConfig();

        this.jedisPool = new JedisPool(poolConfig, host, port, 2000, password);
        this.objectMapper = new ObjectMapper();
        logger.info("Connected to Redis at {}:{}", host, port);
    }

    @Override
    public void add(String conversationId, List<Message> messages) {

        String key = DEFAULT_KEY_PREFIX + conversationId;

        AtomicLong timestamp = new AtomicLong(System.currentTimeMillis());

        try (Jedis jedis = jedisPool.getResource()) {
            // 使用pipeline批量操作提升性能
            var pipeline = jedis.pipelined();
            messages.forEach(message ->
                    pipeline.hset(key, String.valueOf(timestamp.getAndIncrement()), message.toString())
            );
            pipeline.sync();
        }

        logger.info("Added messages to conversationId: {}", conversationId);
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {

        String key = DEFAULT_KEY_PREFIX + conversationId;

        try (Jedis jedis = jedisPool.getResource()) {
            Map<String, String> allMessages = jedis.hgetAll(key);
            if (allMessages.isEmpty()) {
                return List.of();
            }

            return allMessages.entrySet().stream()
                    .sorted((e1, e2) ->
                            Long.compare(Long.parseLong(e2.getKey()), Long.parseLong(e1.getKey()))
                    )
                    .limit(lastN)
                    .map(entry -> new UserMessage(entry.getValue()))
                    .collect(Collectors.toList());
        }


    }

    @Override
    public void clear(String conversationId) {

        String key = DEFAULT_KEY_PREFIX + conversationId;

        try (Jedis jedis = jedisPool.getResource()) {
            jedis.del(key);
        }
        logger.info("Cleared messages for conversationId: {}", conversationId);
    }

    @Override
    public void close() {
        try (Jedis jedis = jedisPool.getResource()) {
            if (jedis != null) {

                jedis.close();

                logger.info("Redis connection closed.");
            }
            if (jedisPool != null) {

                jedisPool.close();

                logger.info("Jedis pool closed.");
            }
        }

    }

    public void clearOverLimit(String conversationId, int maxLimit, int deleteSize) {
        try {
            String key = DEFAULT_KEY_PREFIX + conversationId;
            try (Jedis jedis = jedisPool.getResource()) {
                List<String> all = jedis.lrange(key, 0, -1);

                if (all.size() >= maxLimit) {
                    all = all.stream().skip(Math.max(0, deleteSize)).toList();
                }
                this.clear(conversationId);
                for (String message : all) {
                    jedis.rpush(key, message);
                }
            }
        }
        catch (Exception e) {
            logger.error("Error clearing messages from Redis chat memory", e);
            throw new RuntimeException(e);
        }
    }

}
@RestController
@RequestMapping("/chat-memory")
public class ChatMemoryController {

    private final ChatClient chatClient;

    public ChatMemoryController(ChatModel chatModel) {
// 构建对话模型 基于内存的方式InMemory...
        this.chatClient = ChatClient
                .builder(chatModel)
                .defaultSystem("你是一个旅游规划师,请根据用户的需求提供旅游规划建议。")
//                .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
                .defaultAdvisors(new MessageChatMemoryAdvisor(new RedisChatMemory(
                        "127.0.0.1",
                        6379,
                        null
                )))
                .build();
    }

    
    /**
     * 从Redis中获取聊天内容
     * 根据提供的prompt和chatId,从Redis中检索聊天内容,并以Flux<String>的形式返回
     *
     * @param prompt 聊天内容的提示或查询关键字
     * @param chatId 聊天的唯一标识符,用于从Redis中检索特定的聊天内容
     * @param response HttpServletResponse对象,用于设置响应的字符编码为UTF-8
     * @return Flux<String> 包含聊天内容的反应式流
     */
    @GetMapping("/redis")
    public Flux<String> redis(
            @RequestParam("prompt") String prompt,
            @RequestParam("chatId") String chatId,
            HttpServletResponse response
    ) {

        response.setCharacterEncoding("UTF-8");

        return chatClient.prompt(prompt)
                .advisors(
                a -> a
                        .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                        .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)
                )
                .stream().content();
    }

GET:http://localhost:1000/chat-memory/redis
prompt  有什么好吃的?
chatId  Pluminary

打开Another Redis Desktop Manager即可看见KeyValue
这里也是基于上下文 第一个问题是我想去三亚 第二个问题有什么好吃的这里是基于第一个问题和回答去输出结果

Key:1752192461656
Value:UserMessage{content='我想去三亚', properties={messageType=USER}, messageType=USER}

ChatModel对话模型

ChatResponse里面是对话的json响应类

package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.io.IOException;

@RestController
public class ChatModelController {
    private final ChatModel chatModel;

    public ChatModelController(@Qualifier("dashscopeChatModel") ChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @RequestMapping("/chat2")
    public String chat2(String input) {

        DashScopeChatOptions options = DashScopeChatOptions.builder()
                .withTemperature(0.9)
                .withMaxToken(1500)
           //     .withTopP(0.01)
                .build();

        Prompt prompt = new Prompt(input, options);
        ChatResponse response = chatModel.call(prompt);
        //ChatResponse response = chatModel.call(new Prompt(input));
        return response.getResult().getOutput().getText();
    }


    @RequestMapping("/streamChat")
    public Flux<String> streamChat(String input, HttpServletResponse response) throws IOException {
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("UTF-8");
        return chatModel.stream(input);
    }
}

GET:http://localhost:1000/chat2
input  我想去深圳,帮我做个规划

ImageModel实现文生图

package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import org.springframework.ai.image.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ImageModelController {
    private final ImageModel imageModel;

    ImageModelController(@Qualifier("dashScopeImageModel") ImageModel imageModel) {
        this.imageModel = imageModel;
    }

    @RequestMapping("/image")
    public String image(String input) {
        ImageOptions options = ImageOptionsBuilder.builder()
                .model("wanx2.1-t2i-turbo")
                .height(1024)
                .width(1024)
                .build();

        ImagePrompt imagePrompt = new ImagePrompt(input, options);
        ImageResponse response = imageModel.call(imagePrompt);
        String imageUrl = response.getResult().getOutput().getUrl();

        return "redirect:" + imageUrl;
    }
}

GET:http://localhost:1000/image
input  森林中的绝美精灵,手里拿着弓

AudioModel文生语音&语音转文本

文本生成语音
package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionModel;
import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionOptions;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisModel;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisPrompt;
import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisResponse;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;

/**
 * @author: Fox
 * @Desc:
 **/
@RestController
@RequestMapping("/audio")
public class AudioModelController {

    private final SpeechSynthesisModel speechSynthesisModel;

    @Autowired
    public AudioModelController(SpeechSynthesisModel speechSynthesisModel) {
        this.speechSynthesisModel = speechSynthesisModel;
    }

    @GetMapping("/synthesize")
    public ResponseEntity<byte[]> synthesizeSpeech(@RequestParam String text) throws IOException {
        // 构建语音合成请求
        SpeechSynthesisPrompt prompt = new SpeechSynthesisPrompt(text);

        // 调用模型生成语音
        SpeechSynthesisResponse response = speechSynthesisModel.call(prompt);
        ByteBuffer audioData = response.getResult().getOutput().getAudio();

        // 将 ByteBuffer 转换为字节数组
        byte[] audioBytes = new byte[audioData.remaining()];
        audioData.get(audioBytes);

        // 返回音频流(MP3格式)
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header("Content-Disposition", "attachment; filename=output.mp3")
                .body(audioBytes);
    }
}

GET:http://localhost:1000/audio/synthesize
text  我真的太喜欢点赞啦
语音生成文本
package com.fox.alibabaaidemo.controller;

import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionModel;
import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionOptions;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.MalformedURLException;

/**
 * @author: Fox
 * @Desc:
 **/
@RestController
public class AudioModelController2 {

    private static final String AUDIO_RESOURCES_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav";

    private final DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel; //modelname:sensevoice-v1,paraformer-realtime-v2,paraformer-v2

    AudioModelController2(DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel){
        this.dashScopeAudioTranscriptionModel = dashScopeAudioTranscriptionModel;
    }

    @GetMapping("/audio")
    public String audio() throws MalformedURLException {
        Resource resource =new UrlResource(AUDIO_RESOURCES_URL);

        AudioTranscriptionPrompt prompt = new AudioTranscriptionPrompt(resource,
                DashScopeAudioTranscriptionOptions.builder()
                        .withModel("sensevoice-v1")
                        .build());

        return dashScopeAudioTranscriptionModel.call(prompt).getResult().getOutput();
    }
}

GET:http://localhost:1000/audio

提示词 (Prompt).create

Prompt 是引导 AI 模型生成特定输出的输入格式,Prompt 的设计和措辞会显著影响模型的响应。

Prompt 最开始只是简单的字符串,随着时间的推移,prompt 逐渐开始包含特定的占位符,例如 AI 模型可以识别的 “USER:”、“SYSTEM:” 等。阿里云通义模型可通过将多个消息字符串分类为不同的角色,然后再由 AI 模型处理,为 prompt 引入了更多结构。每条消息都分配有特定的角色,这些角色对消息进行分类,明确 AI 模型提示的每个部分的上下文和目的。这种结构化方法增强了与 AI 沟通的细微差别和有效性,因为 prompt 的每个部分在交互中都扮演着独特且明确的角色。

Prompt 中的主要角色(Role)包括:

  • 系统角色(System Role):指导 AI 的行为和响应方式,设置 AI 如何解释和回复输入的参数或规则。这类似于在发起对话之前向 AI 提供说明。
  • 用户角色(User Role):代表用户的输入 - 他们向 AI 提出的问题、命令或陈述。这个角色至关重要,因为它构成了 AI 响应的基础。
  • 助手角色(Assistant Role):AI 对用户输入的响应。这不仅仅是一个答案或反应,它对于保持对话的流畅性至关重要。通过跟踪 AI 之前的响应(其“助手角色”消息),系统可确保连贯且上下文相关的交互。助手消息也可能包含功能工具调用请求信息。它就像 AI 中的一个特殊功能,在需要执行特定功能(例如计算、获取数据或不仅仅是说话)时使用。
  • 工具/功能角色(Tool/Function Role):工具/功能角色专注于响应工具调用助手消息返回附加信息。
API 概览
Prompt

通常使用 ChatModel 的 call() 方法,该方法接受 Prompt 实例并返回 ChatResponse。

Prompt 类充当有组织的一系列 Message 对象和请求 ChatOptions 的容器。每条消息在提示中都体现了独特的角色,其内容和意图各不相同。这些角色可以包含各种元素,从用户查询到 AI 生成的响应再到相关背景信息。这种安排可以实现与 AI 模型的复杂而详细的交互,因为提示是由多条消息构成的,每条消息都被分配了在对话中扮演的特定角色。

该类实现的接口支持提示创建的不同方面:

PromptTemplateStringActions 专注于创建和呈现提示字符串,代表提示生成的最基本形式。

PromptTemplateMessageActions 专门用于通过生成和操作 Message 对象来创建提示。

PromptTemplateActions 旨在返回 Prompt 对象,该对象可以传递给 ChatModel 以生成响应。

虽然这些接口可能在许多项目中没有得到广泛使用,但它们展示了创建提示的不同方法。

实现ConfigurablePromptTemplateFactory
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\java\com\fox\promptdemo\config\PromptTemplateConfig.java

package com.fox.promptdemo.config;

import com.alibaba.cloud.ai.prompt.ConfigurablePromptTemplateFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PromptTemplateConfig {

    @Bean
    public ConfigurablePromptTemplateFactory configurablePromptTemplateFactory() {
        // 这里假设ConfigurablePromptTemplateFactory有一个无参构造函数
        return new ConfigurablePromptTemplateFactory();
        // 如果需要配置参数,可以在这里进行配置
        // return new ConfigurablePromptTemplateFactory(param1, param2);
    }
}
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\java\com\fox\promptdemo\controller\PromptTemplateController.java
@GetMapping("/prompt-template")
    public AssistantMessage generate(
            @RequestParam(value = "author", defaultValue = "鲁迅") String author
    ) {

        ConfigurablePromptTemplate template = configurablePromptTemplateFactory.getTemplate("test-template");

        if (template == null) {
            template = configurablePromptTemplateFactory.create("test-template",
                    "请列出 {author} 最著名的三本书。");
        }

        Prompt prompt;
        if (StringUtils.hasText(author)) {
            prompt = template.create(Map.of("author", author));
        } else {
            prompt = template.create();
        }

        return chatClient.prompt(prompt)
                .call()
                .chatResponse()
                .getResult()
                .getOutput();
    }

GET:http://localhost:10007/example/ai/prompt-template
author  鲁迅
实现PromptTemplate
@RestController
@RequestMapping("/example/ai")
public class PromptTemplateController {

    private final ChatClient chatClient;

    private final ConfigurablePromptTemplateFactory configurablePromptTemplateFactory;
        
    // 这里的资源是以st文件类型放在了resources的prompts里面
    @Value("classpath:/prompts/joke-prompt.st")
    private Resource jokeResource;

    @GetMapping("/prompt")
    public AssistantMessage completion(
            @RequestParam(value = "adjective", defaultValue = "有趣") String adjective,
            @RequestParam(value = "topic", defaultValue = "奶牛") String topic
    ) {

        PromptTemplate promptTemplate = new PromptTemplate(jokeResource);
        Prompt prompt = promptTemplate.create(Map.of("adjective", adjective, "topic", topic));

        return chatClient.prompt(prompt)
                .call()
                .chatResponse()
                .getResult()
                .getOutput();
    }
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\prompts\joke-prompt.st
给我讲一个关于 {topic} 的 {adjective} 笑话

GET:http://localhost:10007/example/ai/prompt
adjective  有趣的
topic  猫
实现SystemPromptTemplate
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.promptdemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/example/ai")
public class RoleController {

    private final ChatClient chatClient;

    @Value("classpath:/prompts/system-message.st")
    private Resource systemResource;

    @Autowired
    public RoleController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }
// defaultValue是没有传入时候的默认值
    @GetMapping("/roles")
    public AssistantMessage generate(
            @RequestParam(value = "message",
            defaultValue = "请介绍一下海盗黄金时代的三位著名海盗,以及他们为什么这样做。为每个海盗至少写一句话。") String message,
            @RequestParam(value = "name", defaultValue = "Fox") String name,
            @RequestParam(value = "voice", defaultValue = "海盗") String voice
    ) {

        UserMessage userMessage = new UserMessage(message);

        SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
        // .createMessage创建一个系统消息
        Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));

        return chatClient.prompt(new Prompt(List.of(userMessage, systemMessage)))
                .call()
                .chatResponse()
                .getResult()
                .getOutput();
    }
}

GET:http://localhost:10007/example/ai/roles
name  Pluminary
voice  海盗
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\prompts\system-message.st
你是一个有用的 AI 助手。
你是帮助人们查找信息的 AI 助手。
你的名字是 {name}
你应该使用你的姓名和 {voice} 的样式回复用户的请求。

静态RAG实现:提示词动态注入

当前实现的方式:

  • 属于 “文档预加载+条件触发” 模式
  • 通过 stuffit 参数实现 人工控制的上下文开关
  • 上下文直接来自预定义的静态资源文件
  • 本质上是一种轻量级的 静态RAG(Static RAG)

经典RAG:

  • 采用 “检索-排序-注入” 自动化流程
  • 依赖向量相似度计算动态选择上下文
  • 支持增量学习和实时知识更新
  • 典型流程:问题编码→向量检索→相关性过滤→上下文注入

适用场景建议
当前方式更适合:

  • 文档规模小(如产品说明书、FAQ等)
  • 需要严格控制的回答范围
  • 快速原型开发阶段
  • 对实时性要求不高的场景

需要升级到RAG当:

  • 文档超过100MB或频繁更新
  • 需要基于问题语义自动匹配上下文
  • 要求支持多文档源混合检索
  • 需要结合用户画像的个性化回答
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.promptdemo.controller;

import com.fox.promptdemo.entity.Completion;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/example/ai")
public class StuffController {

    private final ChatClient chatClient;

    @Value("classpath:/docs/bailian.md")
    private Resource docsToStuffResource;

    @Value("classpath:/prompts/qa-prompt.st")
    private Resource qaPromptResource;

    @Autowired
    public StuffController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping(value = "/stuff")
    public Completion completion(@RequestParam(value = "message", defaultValue = "给我推荐一款百炼系列的手机?")
                                     String message, @RequestParam(value = "stuffit", defaultValue = "false")
                                     boolean stuffit) {

        PromptTemplate promptTemplate = new PromptTemplate(qaPromptResource);

        Map<String, Object> map = new HashMap<>();
        map.put("question", message);
        if (stuffit) {
            map.put("context", docsToStuffResource);
        } else {
            map.put("context", "");
        }

        return new Completion(chatClient.prompt(promptTemplate.create(map)).call().content());
    }

}


GET:http://localhost:10007/example/ai/stuff
message  给我推荐一款百炼系列的手机?
stuffit  true
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\prompts\qa-prompt.st
使用以下上下文来回答最后的问题。
如果你不知道答案,就说你不知道,不要试图编造答案。

{context}

问题: {question}
有用的答案:
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\prompt-demo\src\main\resources\docs\bailian.md
# **百炼手机产品介绍**

欢迎来到未来科技的前沿,探索我们精心打造的智能手机系列,每一款都是为了满足您对科技生活的无限遐想而生。

**百炼X1** —— 畅享极致视界:搭载6.7英寸1440 x 3200像素超清屏幕,搭配120Hz刷新率,流畅视觉体验跃然眼前。256GB海量存储空间与12GB RAM强强联合,无论是大型游戏还是多任务处理,都能轻松应对。5000mAh电池长续航,加上超感光四摄系统,记录生活每一刻精彩。参考售价:4599 - 4999

**通义Vivid 7** —— 智能摄影新体验:拥有6.5英寸1080 x 2400像素全面屏,AI智能摄影功能让每一张照片都能展现专业级色彩与细节。8GB RAM与128GB存储空间确保流畅操作,4500mAh电池满足日常所需。侧面指纹解锁,便捷又安全。参考售价:2999 - 3299

**星尘S9 Pro** —— 创新视觉盛宴:突破性6.9英寸1440 x 3088像素屏下摄像头设计,带来无界视觉享受。512GB存储与16GB RAM的顶级配置,配合6000mAh电池与100W快充技术,让性能与续航并驾齐驱,引领科技潮流。参考售价:5999 - 6499。

**百炼Ace Ultra** —— 游戏玩家之选:配备6.67英寸1080 x 2400像素屏幕,内置10GB RAM与256GB存储,确保游戏运行丝滑无阻。5500mAh电池搭配液冷散热系统,长时间游戏也能保持冷静。高动态双扬声器,沉浸式音效升级游戏体验。参考售价:3999 - 4299。

**百炼Zephyr Z9** —— 轻薄便携的艺术:轻巧的6.4英寸1080 x 2340像素设计,搭配128GB存储与6GB RAM,日常使用游刃有余。4000mAh电池确保一天无忧,30倍数字变焦镜头捕捉远处细节,轻薄而不失强大。参考售价:2499 - 2799。

**百炼Flex Fold+** —— 折叠屏新纪元:集创新与奢华于一身,主屏7.6英寸1800 x 2400像素与外屏4.7英寸1080 x 2400像素,多角度自由悬停设计,满足不同场景需求。512GB存储、12GB RAM,加之4700mAh电池与UTG超薄柔性玻璃,开启折叠屏时代新篇章。此外,这款手机还支持双卡双待、卫星通话,帮助您在世界各地都能畅联通话。参考零售价:9999 - 10999。

每一款手机都是匠心独运,只为成就您手中的科技艺术品。选择属于您的智能伙伴,开启未来科技生活的新篇章。

实现结构化输出转JSON或Java对象(.entity)【文本转特定格式】

ChatClient

调用entity()方法

// 实体对象
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Generate the filmography of 5 movies for {actor}.")
                    .param("actor", "Tom Hanks"))
        .call()
        .entity(ActorsFilms.class);
List
List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
        .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
        .call()
        .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});


//使用 ListOutputConverter 将模型响应转换为 List:
List<String> flavors = ChatClient.create(chatModel).prompt()
                .user(u -> u.text("List five {subject}")
                            .param("subject", "ice cream flavors"))
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));
Map< String, Object>
Map<String, Object> result = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Provide me a List of {subject}")
                    .param("subject", "an array of numbers from 1 to 9 under their key name 'numbers'"))
        .call()
        .entity(new ParameterizedTypeReference<Map<String, Object>>() {});

com/fox/structureddemo/stream/StreamToBeanEntity.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.structureddemo.stream;

public class StreamToBeanEntity {

    private String title;
    private String author;
    private String date;
    private String content;

    public StreamToBeanEntity() {
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "StreamToBeanEntity{" +
                "title='" + title + '\'' +
                ", author='" + author + '\'' +
                ", date='" + date + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}
com/fox/structureddemo/stream/StreamToBeanController.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.structureddemo.stream;

import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.Objects;


@RestController
@RequestMapping("/example/stream")
public class StreamToBeanController {

    private final ChatClient chatClient;

    private static final Logger log = LoggerFactory.getLogger(StreamToBeanController.class);

    public StreamToBeanController(ChatClient.Builder builder) {
        // 使用builder对象构建ChatClient实例
        this.chatClient = builder.build();
    }


    /**
     * @return {@link com.fox.structureddemo.stream.StreamToBeanEntity}
     */
    @GetMapping("/play")
    public StreamToBeanEntity simpleChat(HttpServletResponse response) {

        response.setCharacterEncoding("UTF-8");

        var converter = new BeanOutputConverter<>(
                new ParameterizedTypeReference<StreamToBeanEntity>() { }
        );

        Flux<String> flux = this.chatClient.prompt()
                .user(u -> u.text("""
                        requirement: 请用大概 120 字,作者为 Fox ,为计算机的发展历史写一首现代诗;
                        format: 以纯文本输出 json,请不要包含任何多余的文字——包括 markdown 格式;
                        outputExample: {
                             "title": {title},
                             "author": {author},
                             "date": {date},
                             "content": {content}
                        };
                        """))
                .stream()
                .content();

        String result = String.join("\n", Objects.requireNonNull(flux.collectList().block()))
                .replaceAll("\\n", "")
                .replaceAll("\\s+", " ")
                .replaceAll("\"\\s*:", "\":")
                .replaceAll(":\\s*\"", ":\"");

        log.info("LLMs 响应的 json 数据为:{}", result);

        return converter.convert(result);
    }
}
com/fox/structureddemo/stream/StreamToJsonController.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fox.structureddemo.stream;

import com.alibaba.cloud.ai.dashscope.api.DashScopeResponseFormat;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/example/stream/json")
public class StreamToJsonController {

    private static final String DEFAULT_PROMPT = "你好,请以JSON格式介绍你自己!";

    private final ChatClient dashScopeChatClient;

    public StreamToJsonController(ChatModel chatModel) {

        DashScopeResponseFormat responseFormat = new DashScopeResponseFormat();
        responseFormat.setType(DashScopeResponseFormat.Type.JSON_OBJECT);

        this.dashScopeChatClient = ChatClient.builder(chatModel)
                .defaultOptions(
                        DashScopeChatOptions.builder()
                                .withTopP(0.7)
                                .withResponseFormat(responseFormat)
                                .build()
                )
                .build();
    }

    /**
     * @return {@link String}
     */
    @GetMapping("/play")
    public String simpleChat(HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        return dashScopeChatClient.prompt(DEFAULT_PROMPT)
                .call()
                .content();
    }

}

通义千问大模型调优

  • temperature:温度值越高,模型生成的内容就越丰富多样

    温度只是来调整候选Token集合的概率分布

    低值(0.2):适用于标准化回答(如退货政策查询)
    高值(0.8):适用于创意场景(如促销文案生成)低温度、中温度和高温度是依据通义千问plus模型的取值范围[0, 2)来划分。

    • 低温度(temperature=0.1):大模型选择“RAG”的概率远高于其它候选Token,这会导致其输出相对单一。
    • 中温度(temperature=0.7):候选Token之间概率分布相对平缓,除“RAG”之外的候选Token出现的概率都在增加。
    • 高温度(temperature=1.2):原低概率的候选Token出现概率进一步提升,打乱原有的排序,进一步提高了大模型输出的随机性。在上图中,“画画”出现的概率反而最高。
  • top_p:top_p值越高,大模型的输出结果随机性越高

    top_p控制候选Token集合的采样范围。

    设置top_p=0.9平衡多样性与准确性,防止低概率token干扰

为了确保生成内容的可控性,建议不要同时调整top_p和temperature。同时调整可能导致输出结果不可预测和复杂。你可以优先调整其中一种参数,观察其对结果的影响,再逐步微调。

  • top_k:top_k越大,生成内容越多样化;top_k越小,内容则更固

在通义千问系列模型中,参数topk也有类似topp的能力,可查阅通义千问API文档。它是一种采样机制,从概率排名前k的Token中随机选择一个进行输出。一般来说,topk越大,生成内容越多样化;topk越小,内容则更固定。当top_k设置为1时,模型仅选择概率最高的Token,输出会更加稳定,但也会导致缺乏变化和创意。

通义千问API参考_大模型服务平台百炼(Model Studio)-阿里云帮助中心

多个AI大模型无缝切换【👆图在上面👆】

如何实现多个AI大模型的无缝切换,比如DeepSeek-R1 调用失败可以请求降级调用 QwQ-32B。 AI网关方案实战

期,通义千问发布了一款全新的推理模型 QwQ-32B。在各类基准测试中,这个拥有 320 亿参数的模型展现出了与 DeepSeek-R1(6710 亿参数)相当的性能。这意味着:

  • 对于个人用户而言,QwQ-32B 能够直接在本地运行,且对设备的要求更低,适合在更小的移动设备上使用。
  • 对于企业用户来说,调用推理大模型 API 的成本可以进一步降低,最高可减少 90% 的费用。

在实际应用中,无缝切换多个大模型的需求日益凸显。例如,企业可能需要同时对接多个大模型,以满足不同业务场景的需求;当单个模型出现稳定性问题时,能够迅速回退到另一个模型,确保业务的连续性和稳定性;在 Multi Agent 场景下,一个复杂任务可能需要调用多个模型来协同完成。Higress AI 网关提供了一个强大的解决方案,支持多模型服务,并具备消费者鉴权、模型自动切换等高级功能。

本文将为您提供一份详细的教程,指导您如何使用 Higress AI 网关在 DeepSeek-R1 和 QwQ-32B 大模型之间实现无缝切换。

关于Higress网关的使用,可以参考我上一篇文章:阿里巴巴开源的云原生网关Higress实战

若想要Higress桌面版 先下载Docker Desktop再下载 Cygwin验证
Cygwin 安装是否成功

cygcheck -c cygwin

Pluminary@Pluminary ~
$ cygcheck -c cygwin
Cygwin Package Information
Package Version Status
cygwin 3.6.1-1 OK

cygwin下载及安装详细教程,windows使用linux的shell命令编译源码(win10) - 知乎
Windows 下 Higress 部署实践 | Higress

单机部署nacos 进入D:\nacos-docker-master\cmd
单机部署指令:docker-compose -f example/standalone-derby.yaml up
这里可以修改端口映射:D:\nacos-docker-master\example\standalone-derby.yaml 如下👇
正确的监控检查命令:curl http://192.168.31.103:8848/nacos/v3/console/health/readiness

version: "2"
services:
  nacos:
    image: nacos/nacos-server:${NACOS_VERSION}
    container_name: nacos-standalone
    environment:
      - PREFER_HOST_MODE=hostname
      - MODE=standalone
      - NACOS_AUTH_IDENTITY_KEY=serverIdentity
      - NACOS_AUTH_IDENTITY_VALUE=security
      - NACOS_AUTH_TOKEN=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=
    volumes:
      - ./standalone-logs/:/home/nacos/logs
    ports:
      - "8848:8848"
      - "18080:8080"       
  prometheus:
    container_name: prometheus
    image: prom/prometheus:latest
    volumes:
      - ./prometheus/prometheus-standalone.yaml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    depends_on:
      - nacos
    restart: on-failure
  grafana:
    container_name: grafana
    image: grafana/grafana:latest
    ports:
      - 3000:3000
    restart: on-failure

随后在桌面打开cmd输入ipconfig寻找ip地址 记录下来
在桌面右键 → 选择 Git Bash Here,进入 Bash 环境
在 Git Bash 里执行命令:【ip地址是通过ipconfig获取】**这里要记得关闭梯子和代理 因为从阿里云拉镜像**

curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -c nacos://192.168.31.103:8848

★★

// 安装higress 进入bash
$ curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -c nacos://192.168.43.85:8848 --nacos-username=nacos --nacos-password=nacos --nacos-ns=higress-system --nacos-ns=higress-system

       // 这里是启动的nacos
cd D:\nacos-docker-master\example
// 重启docker(Nacos)
docker-compose -f standalone-derby.yaml down
docker-compose -f standalone-derby.yaml up -d

// 一键重启
docker restart nacos-standalone

    
       // 这里是启动higress    
// 在桌面启一个bash  启动higress
$ /c/Users/Pluminary/Desktop/higress/bin/startup.sh
// 如果改了配置 在bash里  【一定要先启动docker 才可以干别的】
cd /c/Users/Pluminary/Desktop/higress
./bin/shutdown.sh
./bin/startup.sh
    

然后就一直无敌报错:

http://192.168.31.103:8848/nacos/v1/console/health/readiness returns 000
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…
Waiting for Nacos to get ready…

修改端口不占用冲突的8080即可

cd D:\nacos-docker-master\example

重启docker(Nacos)
docker-compose -f standalone-derby.yaml down
docker-compose -f standalone-derby.yaml up -d

一键重启

docker restart nacos-standalone




下一个报错:

Nacos is ready.
Initializing Nacos server…

Nacos 3.x isn’t fully supported yet.

If you do want to use Nacos 3.x, please add the following property into its application.properties file:
nacos.core.api.compatibility.console.enabled=true

Higress configuration failed with 255.
Failed to install Higress
For support, go to https://github.com/alibaba/higress.

这说明什么?
  • Higress 当前版本对 Nacos 3.x 支持不完善,需要兼容模式开启
  • 你的 Nacos 3.x 服务默认没开启兼容控制台API,导致健康检查接口返回异常或空

1. 修改 Nacos 配置,启用兼容模式

找到你的 Nacos 容器中 application.properties 文件,添加下面这一行:

nacos.core.api.compatibility.console.enabled=true

2. 重启 Nacos 容器

cd D:\nacos-docker-master\example
docker restart nacos-standalone

3. 访问健康接口测试

curl http://192.168.43.85:8848/nacos/v1/console/health/readiness

搞什么乱七八糟的配置 直接降级nacos 因为nacos3.xxx有很多不兼容的地方
D:\nacos-docker-master\exampleNACOS_VERSION=v2.3.1

Pluminary@Pluminary MINGW64 ~/Desktop
$ curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- -c nacos://192.168.43.85:8848 --nacos-username=nacos --nacos-password=nacos --nacos-ns=higress-system 

Downloading https://github.com/higress-group/higress-
standalone/archive/refs/tags/v2.1.5.tar.gz...
==== Build Configurations ====
time="2025-07-12T00:40:46+08:00" level=warning msg="C:\\Users\\Pluminary\\Desktop\\higress\\compose\\docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion"
Nacos is ready.
Initializing Nacos server...
Use Nacos API v1
  Namespace higress-system already exists in Nacos.
  Fixed data encryption key is used. Skip config overwriting check.
Initializing API server configurations...
  Data encryption key already exists.
Initializing controller configurations...

 ___  ___  ___  ________  ________  _______   ________   ________
|\  \|\  \|\  \|\   ____\|\   __  \|\  ___ \ |\   ____\ |\   ____\
\ \  \\\  \ \  \ \  \___|\ \  \|\  \ \   __/|\ \  \___|_\ \  \___|_
 \ \   __  \ \  \ \  \  __\ \   _  _\ \  \_|/_\ \_____  \\ \_____  \
  \ \  \ \  \ \  \ \  \|\  \ \  \\  \\ \  \_|\ \|____|\  \\|____|\  \
   \ \__\ \__\ \__\ \_______\ \__\\ _\\ \_______\____\_\  \ ____\_\  \
    \|__|\|__|\|__|\|_______|\|__|\|__|\|_______|\_________\\_________\
                                                \|_________\|_________|

Higress is configured successfully.

Important Notes:
  Sensitive configurations are encrypted when saving to Nacos.
  When configuring another server with the same Nacos configuration service, please make sure to add the following argument so all servers use the same encryption key:
   

Usage:
  Start: /c/Users/Pluminary/Desktop/higress/bin/startup.sh
  Stop: /c/Users/Pluminary/Desktop/higress/bin/shutdown.sh
  View Component Statuses: /c/Users/Pluminary/Desktop/higress/bin/status.sh
  View Logs: /c/Users/Pluminary/Desktop/higress/bin/logs.sh
  Re-configure: /c/Users/Pluminary/Desktop/higress/bin/configure.sh -r

Happy Higressing!

访问Nacos:Nacos 或 地址改成localhost

✅ 配置项 1:secrets.nacos-auth-default

位置:

  • 命名空间: fc629691-b48f-4fa2-baa7-770dbaf8b63e(对应 higress-system
  • Data ID: secrets.nacos-auth-default
  • Group: DEFAULT_GROUP
  • 配置格式: YAML

配置内容:

username: nacos
password: nacos

✅ 配置项 2:configmaps.higress-config

位置:

  • 命名空间: fc629691-b48f-4fa2-baa7-770dbaf8b63e
  • Data ID: configmaps.higress-config
  • Group: DEFAULT_GROUP
  • 配置格式: YAML

配置内容:

apiVersion: v1
kind: ConfigMap
metadata:
  name: higress-config
  namespace: higress-system
data:
  ingressClass: higress

✅ 配置项 3:mcpbridges.default

位置:

  • 命名空间: fc629691-b48f-4fa2-baa7-770dbaf8b63e
  • Data ID: mcpbridges.default
  • Group: DEFAULT_GROUP
  • 配置格式: YAML

配置内容:

apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
  name: default
  namespace: higress-system
spec:
  registries:
    - domain: httpbin.org
      name: httpbin
      port: 80
      type: dns
./bin/configure.sh -a -c nacos://192.168.31.103:8848 \
  --nacos-username=nacos \
  --nacos-password=nacos \
  --nacos-ns=higress-system

✅ 解决方案:使用 -r 参数强制重新配置

你需要强制重新执行配置流程并显式指定 nacos 命名空间,命令如下:

bash复制编辑./bin/configure.sh -a -r \
  -c nacos://192.168.31.103:8848 \
  --nacos-username=nacos \
  --nacos-password=nacos \
  --nacos-ns=higress-system \
  -k 7bbac1c2ab91ff074cb8d219c307d203

📌 说明:

  • -r:重新运行配置流程(一定要加)
  • --nacos-ns=higress-system:这才是决定配置写入哪个命名空间的参数
  • .env 中的 NACOS_NS 虽然也有用,但以你命令参数为主

什么是RAG

RAG:接收到问题后,去知识库查资料,得到检索知识后进行回答
微调:提前学习知识,丰富自己的能力,接收到问题后进行回答

RAG → 问题 → (LLM+知识) → 回复
    
                   问题
                    ↓
微调 → LLM → 知识 → LLM(new)
               ↘    ↓
                   回复
RAG工作原理:
  • 建立索引阶段

    文件上传 → 文档加载 → 内容分割 → 文本向量化 → 索引存储

  • 检索与生成阶段

    检索:检索阶段会召回与问题最相关的文本段。通过embedding模型对问题进行文本向量化,并与向量数据库的段落进行语义相似度的比较,找出最相关的段落。
    生成:在检索到相关的文本段后,RAG应用会将问题与文本段通过提示词模板生成最终的提示词,由大模型生成回复,这个阶段更多是利用大模型的总结能力,而不是大模型本身具有的知识。

  • 总结

    用户提问 → → → → → → 提示词 → 大语言模型 → 输出答案
    ↘ → 内容检索 → ↑

com/fox/ragdemo/config/RagConfig.java
package com.fox.ragdemo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class RagConfig {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你将作为一名机器人产品的专家,对于用户的使用需求作出解答")
                .build();
    }

    @Bean
    VectorStore vectorStore(EmbeddingModel embeddingModel) {
        SimpleVectorStore simpleVectorStore = SimpleVectorStore.builder(embeddingModel)
                .build();

        // 生成一个机器人产品说明书的文档
        List<Document> documents = List.of(
                new Document("产品说明书:产品名称:智能机器人\n" +
                        "产品描述:智能机器人是一个智能设备,能够自动完成各种任务。\n" +
                        "功能:\n" +
                        "1. 自动导航:机器人能够自动导航到指定位置。\n" +
                        "2. 自动抓取:机器人能够自动抓取物品。\n" +
                        "3. 自动放置:机器人能够自动放置物品。\n"));

        simpleVectorStore.add(documents);
        return simpleVectorStore;
    }
}
com/fox/ragdemo/controller/RagController.java
package com.fox.ragdemo.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/ai")
public class RagController {

    @Autowired
    private ChatClient chatClient;

    @Autowired
    private VectorStore vectorStore;


    @GetMapping(value = "/chat", produces = "text/plain; charset=UTF-8")
    public String generation(String userInput) {
        // 发起聊天请求并处理响应
        return chatClient.prompt()
                .user(userInput)
                // 调用知识库
                .advisors(new QuestionAnswerAdvisor(vectorStore))
                .call()
                .content();
    }
}
package com.fox.ragdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RagDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(RagDemoApplication.class, args);
    }
}

RAG+阿里云百炼

本地集成百炼智能体应用
本地RAG应用集成百炼知识库
知识库:
【学校简介】
阳光中学创建于1998年,是一所全日制寄宿制完全中学,坐落于江苏省苏州市。学校占地120亩,现有教职工120人,学生1500余人。多年来,阳光中学秉持“厚德载物,知行合一”的校训,致力于培养具有国际视野与中国灵魂的复合型人才。
________________________________________
【课程设置】
阳光中学设有初中和高中两个学段,开设课程包括语文、数学、英语、物理、化学、生物、历史、地理、政治、信息技术、音乐、美术和体育等。高一开始分文理科方向,同时提供选修课程如Python编程、人工智能基础、商务英语等,满足学生多元发展需求。
________________________________________
【师资力量】
学校现有特级教师3人,高级教师32人,硕士学历以上教师占比达70%。其中语文学科带头人王老师曾获全国语文优质课一等奖,数学组组长李老师拥有15年以上高三教学经验,多次带出省级高考状元。
________________________________________
【校园生活】
阳光中学注重学生全面发展,组织丰富的课外活动与社团,如机器人社、合唱团、辩论社、摄影社、篮球队等。每年5月举办校园艺术节,每年10月举行运动会,深受师生喜爱。校园内有图书馆、自助餐厅、医务室、心理咨询室等完善设施。
________________________________________
【校规制度】
1.    学生必须穿着校服上课,不得迟到、早退或旷课;
2.    晚自习时间为每周一至周五18:30—20:30;
3.    禁止携带手机等电子产品进课堂;
4.    严禁在校园内打架、吸烟、喝酒,一经发现严肃处理;
5.    每月评选“文明班级”“学习标兵”等荣誉称号。
________________________________________
【招生信息】
阳光中学每年春季开始招生,面向全国招收初一、高一新生。招生名额有限,需通过笔试和面试。学费标准为初中部每学期6000元,高中部每学期8000元,另设有奖学金与助学金政策,家庭困难学生可申请减免。
________________________________________
【常见问题解答】
Q:阳光中学是否提供住宿?
A:学校提供标准化学生宿舍,6人一间,配备空调、热水器与独立卫生间,设有生活老师管理。
Q:学校是否提供营养餐?
A:学生餐厅提供三餐,包括荤素搭配的营养套餐,每周公布菜单,确保健康卫生。
Q:如何申请插班?
A:插班生需在每学期开学前一个月提交申请,学校将根据学位情况及考试成绩决定是否录取。
Q:学校对艺术体育特长生有优惠吗?
A:是的。学校每年招收部分艺术、体育特长生,可享受降分录取及奖学金政策。
________________________________________
【校园热点资讯】
2025年6月,阳光中学高考再创佳绩,重本上线率达89%,其中7人被清华北大录取;
2025年3月,阳光中学学生在全国青少年人工智能竞赛中获得一等奖;
2024年10月,学校新建科学楼正式启用,建筑面积达4800平方米,配备多个功能实验室。
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\bailian-agent\src\main\java\com\fox\bailianagent\controller\BailianAgentRagController.java
package com.fox.bailianagent.controller;

import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgentOptions;
import com.alibaba.cloud.ai.dashscope.api.DashScopeAgentApi;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BailianAgentRagController {
  private DashScopeAgent agent;

  @Value("${spring.ai.dashscope.agent.app-id}")
  private String appId;

  public BailianAgentRagController(DashScopeAgentApi dashscopeAgentApi) {
    this.agent = new DashScopeAgent(dashscopeAgentApi);
  }

  @GetMapping("/bailian/agent/call")
  public String call(@RequestParam(value = "message") String message) {
    ChatResponse response = agent.call(
            new Prompt(message, DashScopeAgentOptions.builder()
                    // 与智能体的绑定
                    .withAppId(appId)
                    .build()));
    AssistantMessage app_output = response.getResult().getOutput();
    return app_output.getText();
  }
}

GET:http://localhost:8088/bailian/agent/call
message  学校简介是什么?
package com.fox.bailianagent;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BailianAgentApplication {

    public static void main(String[] args) {
        SpringApplication.run(BailianAgentApplication.class, args);
    }
}
server:
  port: 8088

spring:
  application:
    name: bailian-agent
  ai:
    dashscope:
      agent:
# https://bailian.console.aliyun.com/?tab=app#/app-center
        app-id: xxxxxxx
      api-key: xxxxxxx

SpringAIAlibaba本地集成百炼知识库实战通过代码将文件上传到百炼知识库

/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.fox.bailianragdemo.controller;

import com.fox.bailianragdemo.service.RagService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/ai")
public class CloudRagController {

    private final RagService cloudRagService;

    public CloudRagController(RagService cloudRagService) {
        this.cloudRagService = cloudRagService;
    }

    @GetMapping("/bailian/knowledge/importDocument")
    public void importDocument() {
        cloudRagService.importDocuments();
    }

    @GetMapping("/bailian/knowledge/generate")
    public Flux<String> generate(@RequestParam(value = "message",
            defaultValue = "你好,请问你的知识库文档主要是关于什么内容的?") String message) {
        return cloudRagService.retrieve(message).map(x -> x.getResult().getOutput().getText());
    }

}
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.fox.bailianragdemo.service;

import com.alibaba.cloud.ai.advisor.DocumentRetrievalAdvisor;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.rag.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.rag.retrieval.search.DocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;


@Service()
public class CloudRagService implements RagService {

    private static final Logger logger = LoggerFactory.getLogger(CloudRagService.class);

    private static final String indexName = "阳光中学";

    @Value("classpath:/data/spring_ai_alibaba_quickstart.pdf")
    private Resource springAiResource;

    private static final String retrievalSystemTemplate = """
            上下文信息如下:
            ---------------------
            {question_answer_context}
            ---------------------
            根据上下文和提供的历史信息,而不是先验知识,回答用户问题。
            如果答案不在上下文中,请告知用户无法回答该问题。
            """;

    private final ChatClient chatClient;

    private final DashScopeApi dashscopeApi;

    public CloudRagService(ChatClient.Builder builder, DashScopeApi dashscopeApi) {
        DocumentRetriever retriever = new DashScopeDocumentRetriever(dashscopeApi,
                DashScopeDocumentRetrieverOptions.builder().withIndexName(indexName).build());

        this.dashscopeApi = dashscopeApi;
        this.chatClient = builder
                .defaultAdvisors(new DocumentRetrievalAdvisor(retriever, retrievalSystemTemplate))
                .build();
    }

    @Override
    public void importDocuments() {
        String path = saveToTempFile(springAiResource);

        // 1. import and split documents
        DocumentReader reader = new DashScopeDocumentCloudReader(path, dashscopeApi, null);
        List<Document> documentList = reader.get();
        logger.info("{} documents loaded and split", documentList.size());

        // 1. add documents to DashScope cloud storage 向量数据库
        VectorStore vectorStore = new DashScopeCloudStore(dashscopeApi, new DashScopeStoreOptions(indexName));
        vectorStore.add(documentList);
        logger.info("{} documents added to dashscope cloud vector store", documentList.size());
    }

    private String saveToTempFile(Resource springAiResource) {
        try {
            File tempFile = File.createTempFile("spring_ai_alibaba_quickstart", ".pdf");
            tempFile.deleteOnExit();

            try (InputStream inputStream = springAiResource.getInputStream();
                    FileOutputStream outputStream = new FileOutputStream(tempFile)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }

            return tempFile.getAbsolutePath();
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public Flux<ChatResponse> retrieve(String message) {
        return chatClient.prompt().user(message).stream().chatResponse();
    }

}
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.fox.bailianragdemo.service;

import org.springframework.ai.chat.model.ChatResponse;
import reactor.core.publisher.Flux;


public interface RagService {

    void importDocuments();

    Flux<ChatResponse> retrieve(String message);
}
server:
  port: 8088

spring:
  application:
    name: bailian-rag-demo
  ai:
    dashscope:
      api-key: xxxxx

工具Fuction Calling

允许语言大模型调用外部函数(Function)或API来执行特定任务或获取实时数据。
例如:
调用天气API查询实时天气。
调用数据库接口执行查询。 其核心是扩展模型的交互能力,使其能执行操作或获取结构化数据,而非仅依赖内部知识

应用场景

实时数据查询:如股票行情、天气、航班信息。
任务自动化:预订会议、下单、数据计算等。
系统集成:与CRM、ERP等业务系统交互(如创建客户记录)

和RAG的区别

RAG和Function Calling分别从知识增强和功能扩展两个维度提升LLM能力。选择取决于具体需求:

  • 需结合外部知识? → RAG
  • 需执行操作或获取实时数据? → Function Calling
  • 复杂场景:两者结合(如Agent框架中集成RAG和工具调用)
维度 RAG Function Calling
数据依赖 依赖外部知识库的覆盖和质量 依赖API或函数的可用性和稳定性
实时性 受限于知识库更新频率 可获取实时数据(如API返回结果)
输出形式 自然语言文本 结构化数据(Json)或操作结果
适用问题类型 需外部知识支持的复杂问题 需执行操作或获取动态数据的场景
实现复杂度 需构建高效检索系统(向量数据库) 需定义函数接口和参数规范

Function Calling获取天气信息

以Function为接口调用apply方法
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\function\WeatherFunction.java

package com.fox.toolcallingdemo.tool.weather.function;

import java.util.function.Function;

public class WeatherFunction implements Function<WeatherFunction.WeatherRequest, String> {
    @Override
    public String apply(WeatherRequest request) {
        // 此处省略了实际的天气查询逻辑,直接返回一个示例字符串
        // 实际应用中需要根据请求参数调用天气API获取天气信息
        return "The weather in " + request.getCity() + " is sunny.";
    }
    public static class WeatherRequest {
        private String city;
        public String getCity() { return city; }
        public void setCity(String city) { this.city = city; }
    }
}
把函数以Bean形式交给Spring去管理
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\function\FunctionConfig.java
    
package com.fox.toolcallingdemo.tool.weather.function;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.util.function.Function;

@Configuration
public class FunctionConfig {
    @Bean
    // 标识
    @Description("获取指定城市的天气信息")
    public Function<WeatherFunction.WeatherRequest, String> weatherFunction() {
        return new WeatherFunction();
    }
}
告诉大模型请求可以调函数
package com.fox.toolcallingdemo.controller;


import com.fox.toolcallingdemo.tool.weather.method.WeatherTool;
import com.fox.toolcallingdemo.tool.weather.method.WeatherToolImpl;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/weather")
public class WeatherController {

    private final ChatClient dashScopeChatClient;

    /**
    * 默认全局调函数
    */
    public WeatherController(ChatClient.Builder chatClientBuilder) {
        this.dashScopeChatClient = chatClientBuilder
                .defaultFunctions("weatherFunction")
                //.defaultTools(new WeatherToolImpl())
                .build();
    }
-------------------------------------------------------------------------------------
    /**
     * 无工具版
     */
    @GetMapping("/chat")
    public String simpleChat(@RequestParam(value = "query", defaultValue = "北京今天的天气") String query) {
        return dashScopeChatClient.prompt(query).call().content();
    }
-------------------------------------------------------------------------------------
    /**
     * 调用工具版 - function
     */
    @GetMapping("/chat-tool-function")
    public String chatTranslateFunction(@RequestParam(value = "query", defaultValue = "北京今天的天气") String query) {

    // 就可以去找刚刚函数构建的bean
        return dashScopeChatClient.prompt(query).functions("weatherFunction").call().content();
    }

-------------------------------------------------------------------------------------

    /**
     * 调用工具版 - method
     */
    @GetMapping("/chat-tool-method")
    public String chatTranslateMethod(@RequestParam(value = "query", defaultValue = "北京今天的天气") String query) {

        return dashScopeChatClient.prompt(query).tools(new WeatherToolImpl()).call().content();
    }
}

GET:http://localhost:8088/weather/chat-tool-method
query  深圳今天的天气怎么样?
承接上面最后一个Method版
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\method\WeatherTool.java
package com.fox.toolcallingdemo.tool.weather.method;

public interface WeatherTool {
    String getWeather(String city);
}
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\toolcalling-demo\src\main\java\com\fox\toolcallingdemo\tool\weather\method\WeatherToolImpl.java
package com.fox.toolcallingdemo.tool.weather.method;


import org.springframework.ai.tool.annotation.Tool;

public class WeatherToolImpl implements WeatherTool {
    @Override
    @Tool(description = "获取指定城市的天气信息。")
    public String getWeather(String city) {
        return "The weather in " + city + " is sunny.";
    }
}

MCP协议

模型上下文协议(即 Model Context Protocol,MCP)是一个开放协议,它规范了应用程序如何向大型语言模型(LLM)提供上下文。MCP 提供了一种统一的方式将 AI 模型连接到不同的数据源和工具,它定义了统一的集成方式。

MCP图+与function_calling区别

百炼智能体应用接入高德地图MCP服务

大模型服务平台百炼控制台
在智能体应用的技能添加MCP服务 Amap Maps需要提前开通即可

MCP开发实战

模型上下文协议(Model Context Protocol)
MCP 简介

模型上下文协议(即 Model Context Protocol,MCP)是一个开放协议,它规范了应用程序如何向大型语言模型(LLM)提供上下文。MCP 提供了一种统一的方式将 AI 模型连接到不同的数据源和工具,它定义了统一的集成方式。在开发智能体(Agent)的过程中,我们经常需要将将智能体与数据和工具集成,MCP 以标准的方式规范了智能体与数据及工具的集成方式,可以帮助您在 LLM 之上构建智能体(Agent)和复杂的工作流。目前已经有大量的服务接入并提供了 MCP server 实现,当前这个生态正在以非常快的速度不断的丰富中,具体可参见:MCP Servers

Spring AI MCP

Spring AI MCP 为模型上下文协议提供 Java 和 Spring 框架集成。它使 Spring AI 应用程序能够通过标准化的接口与不同的数据源和工具进行交互,支持同步和异步通信模式。

Spring AI MCP 采用模块化架构,包括以下组件:

  • Spring AI 应用程序:使用 Spring AI 框架构建想要通过 MCP 访问数据的生成式 AI 应用程序
  • Spring MCP 客户端:MCP 协议的 Spring AI 实现,与服务器保持 1:1 连接
  • MCP 服务器:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定的功能
  • 本地数据源:MCP 服务器可以安全访问的计算机文件、数据库和服务
  • 远程服务:MCP 服务器可以通过互联网(例如,通过 API)连接到的外部系统

如何使用

要启用此功能,请将以下依赖项添加到您项目的 Mavenpom.xml文件中:

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-ai-mcp</artifactId>
    <version>1.0.0</version>
</dependency>

或者添加到您的 Gradlebuild.gradle文件中:

dependencies {
    implementation 'org.springframework.experimental:spring-ai-mcp:1.0.0'
}

Spring AI MCP 目前并没有在 Maven Central Repository 中提供。需要将 Spring milestone仓库添加到pom.xml中,才可以访问 Spring AI MCP 工件:

<repositories>
  <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/libs-milestone-local</url>
    <snapshots>
      <enabled>false</enabled>
    </snapshots>
  </repository>
</repositories>

要使用 MCP,首先需要创建McpClient,它提供了与 MCP server 的同步和异步通信能力。现在我们创建一个 McpClient 来注册 MCP Brave 服务和 ChatClient,从而让 LLM 调用它们:

var stdioParams = ServerParameters.builder("npx")
        .args("-y", "@modelcontextprotocol/server-brave-search")
        .addEnvVar("BRAVE_API_KEY", System.getenv("BRAVE_API_KEY"))
        .build();

var mcpClient = McpClient.using(new StdioClientTransport(stdioParams)).sync();

var init = mcpClient.initialize();

var chatClient = chatClientBuilder
        .defaultFunctions(mcpClient.listTools(null)
                .tools()
                .stream()
                .map(tool -> new McpFunctionCallback(mcpClient, tool))
                .toArray(McpFunctionCallback[]::new))
        .build();

String response = chatClient
        .prompt("Does Spring AI supports the Model Context Protocol? Please provide some references.")
        .call().content();

在上述代码中,首先通过npx命令启动一个独立的进程,运行@modelcontextprotocol/server-brave-search服务,并指定 Brave API 密钥。然后创建一个基于 stdio 的传输层,与 MCP server 进行通信。最后初始化与 MCP 服务器的连接。

要使用 McpClient,需要将McpClient注入到 Spring AI 的ChatClient中,从而让 LLM 调用 MCP server。在 Spring AI 中,可以通过 Function Callbacks 的方式将 MCP 工具转换为 Spring AI 的 Function,从而让 LLM 调用。

最后,通过ChatClient与 LLM 进行交互,并使用McpClient与 MCP server 进行通信,获取最终的返回结果。

官方的alibaba服务教学C:\Users\Pluminary\Desktop\HouDuan\spring-ai-alibaba-examples

Stdio传输层

Stdio(标准输入输出) 传输层是MCP最基本的传输实现方式。它通过进程间通信(IPC)实现

SSE传输层

SSE(Server-Sent Events) 传输层是基于HTTP的单向通信机制,专门用于服务器向客户端推送数据

MCP是什么?和Function calling有什么区别?美团面试题

MCP可以通过Client指定SSE 再去找一个远程地址直接就可以调用MCP就像拓展坞,MCPServer可以部署到云端给外部使用
Function Calling则需要自己手动去写一个去调用外部

这道面试题问的是:

MCP 是什么?它和 Function Calling 有什么区别?

来自美团,很可能是在考察你对 大模型应用框架(尤其是 RAG、工具调用、Agent 框架等)底层机制的理解,特别是近几年阿里推出的 Spring AI Alibaba MCP 模型调用框架。


✅ 一、什么是 MCP?

MCP(Model Call Protocol) 是阿里巴巴在 Spring AI Alibaba 框架中提出的一个模型调用协议,全称是:

Model Call Protocol(模型调用协议)

它的作用是:
🔧 统一模型接入规范,为不同的大模型(阿里通义、百度文心、ChatGLM、文生图、图生图等)提供一套通用、标准、结构化的调用协议和扩展机制

✅ MCP 解决的问题:

  • 🌀 不同大模型调用方式、参数格式五花八门 → MCP 提供统一接口抽象
  • 🛠 支持插件工具调用(Tool Calling)
  • 🔁 支持多轮对话记忆
  • 🤖 支持 Agent 架构(例如 RAG、Function调用等)
  • 📦 可以灵活切换底层大模型,实现模型无感知迁移(例如将通义大模型无缝替换为文心一言)

✅ MCP 的核心组件:

  1. MCPModel:统一的模型接口(封装了 prompt 处理、调用逻辑等)
  2. MCPFunctionCallingModel:支持 Function Calling 的模型
  3. MCPMessageMCPResponse:统一的输入/输出数据结构
  4. ToolDefinition, FunctionCall, ToolResult:对工具调用结构化建模

✅ 二、什么是 Function Calling?

Function Calling 是由 OpenAI 提出的技术,后被各大模型厂商采纳(如通义千问、百度文心也支持),它让大模型在对话中能够调用外部函数。

🔧 它的核心概念:

  • 大模型通过识别语义内容,决定是否调用一个函数(例如查天气、查数据库、生成图片等)
  • 由用户预定义函数列表(Function Schema),模型根据意图选择调用
  • 返回结构化 JSON 给开发者,由程序调用实际函数 → 执行后返回结果给模型

✅ Function Calling 实现的能力:

能力 说明
工具接入能力 让模型“用工具”去扩展知识或能力,如查资料、查库存、操作系统等
多轮调用链 支持调用链组合,如 A→B→C 的流程式工具使用
Agent能力基础 是 Agent 能够链式思维和计划任务执行的基础

✅ 三、MCP 和 Function Calling 的关系与区别

对比项 MCP Function Calling
本质 一种大模型调用协议标准 一种大模型的工具调用机制
是否支持 Function ✅ 支持,MCP 中内置了 Function Calling 抽象 ✅ 只关注“函数调用”,不管模型接入细节
作用范围 📦 模型统一封装、RAG、Chat、工具调用、记忆、代理等(整个调用框架) 🧠 模型如何调用函数、如何组织工具返回
抽象程度 高度抽象框架层,统一输入输出模型、适配各类模型 相对底层,是大模型的功能点
使用者 开发框架作者(如 Spring AI Alibaba) 模型厂商、应用开发者
代表项目 Spring AI Alibaba MCP、LangChain4j MCP、百度文心 RAGFlow 等 OpenAI GPT、通义千问、ChatGLM、Claude 等支持 Function Calling 的模型

✅ 四、通俗比喻理解

MCP 是一个大楼的 “总控中心”,可以调配各种大模型、工具、记忆、插件等资源

Function Calling 是这个大楼里的一套 “智能工具调度系统”,专门处理“这个请求要不要调用某个工具来完成”。


✅ 五、实际应用举例(代码层)

在 Spring AI Alibaba 中,你可以这样使用 MCP + FunctionCalling:

@Autowired
private FunctionCallingChatClient chatClient;

@PostMapping("/chat")
public String chat(@RequestBody String prompt) {
    Message response = chatClient.call(
        Message.of(prompt),
        FunctionCallingOptions.builder()
            .function(ToolDefinitions.fromClass(MyFunction.class))
            .build()
    );
    return response.getContent();
}

上面这段代码:

  • 使用 MCP 提供的 FunctionCallingChatClient 调用大模型
  • 绑定了一个 Function(MyFunction),模型根据上下文会自动决定是否调用它

✅ 总结答法(面试简明说法):

MCP 是阿里推出的模型调用协议,统一了模型接入、工具调用、Agent 调度等能力,是一种面向“统一模型开发框架”的抽象;Function Calling 是大模型的一种能力,允许它根据语义调用结构化函数。MCP 包含了 Function Calling 能力,但它作用更广,面向的是整套大模型调用生命周期,Function Calling 则聚焦在“如何让模型使用外部函数”这件事上。

电商智能客服项目

开发环境

JDK17、SpringBoot3.4.0、Spring AI 1.0.0-M6、Spring AI Alibaba 1.0.0-M6.1

项目背景

项目位于C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-ai
前端位于:C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-ai\frontend

可以右键通过终端打开文件夹
①:yarn install 安装依赖
②:yarn dev 启动项目

后端位于:C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order【order.sql也在这里】;C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-ai分别启动两个模块的Application

使用MCP调用订单服务查询订单详情MCPServer是对订单层的包装

项目位于C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order-mcp-clientC:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order-mcp-server
C:\Users\Pluminary\Desktop\HouDuan\ai-demo\tlmall-order

阅读全文

Jenkins

2025/6/15

持续集成及JenKins介绍

配套原版资料Jenkins持续集成从入门到精通.pdf

软件开发生命周期【需求分析、设计、实现、测试、进化

软件开发瀑布模型

瀑布模型是最著名和最常使用的软件开发模型。瀑布模型就是一系列的软件开发过程。它是由制造业繁 衍出来的。一个高度化的结构流程在一个方向上流动,有点像生产线一样。在瀑布模型创建之初,没有 其它开发的模型,有很多东西全靠开发人员去猜测,去开发。这样的模型仅适用于那些简单的软件开 发, 但是已经不适合现在的开发了。

优势 劣势
简单易用和理解 各个阶段的划分完全固定,阶段之间产生大量的文档,极大地 增加了工作量。
当前一阶段完成后,您只需要 去关注后续阶段 由于开发模型是线性的,用户只有等到整个过程的末期才能见 到开发成果,从而增加了开发风险
为项目提供了按阶段划分的检 查节点 瀑布模型的突出缺点是不适应用户需求的变化

软件开发敏捷开发模型

敏捷开发(Agile Development) 的核心是迭代开发(Iterative Development) 与 增量开发 (Incremental Development) 。

==何为迭代开发?== 对于大型软件项目,传统的开发方式是采用一个大周期(比如一年)进行开发,整个过程就是一次”大 开发”;迭代开发的方式则不一样,它将开发过程拆分成多个小周期,即一次”大开发”变成多次”小开 发”,每次小开发都是同样的流程,所以看上去就好像重复在做同样的步骤。 举例来说,SpaceX 公司想造一个大推力火箭,将人类送到火星。但是,它不是一开始就造大火箭,而 是先造一个最简陋的小火箭 Falcon 1。结果,第一次发射就爆炸了,直到第四次发射,才成功进入轨 道。然后,开发了中型火箭 Falcon 9,九年中发射了70次。最后,才开发 Falcon 重型火箭。如果 SpaceX 不采用迭代开发,它可能直到现在还无法上天。

==何为增量开发?== 软件的每个版本,都会新增一个用户可以感知的完整功能。也就是说,按照新增功能来划分迭代。 举例来说,房产公司开发一个10栋楼的小区。如果采用增量开发的模式,该公司第一个迭代就是交付一 号楼,第二个迭代交付二号楼……每个迭代都是完成一栋完整的楼。而不是第一个迭代挖好10栋楼的地 基,第二个迭代建好每栋楼的骨架,第三个迭代架设屋顶…..

敏捷开发如何迭代?
虽然敏捷开发将软件开发分成多个迭代,但是也要求,每次迭代都是一个完整的软件开发周期,必须按 照软件工程的方法论,进行正规的流程管理。

敏捷开发有什么好处?

==早期交付== 敏捷开发的第一个好处,就是早期交付,从而大大降低成本。 还是以上一节的房产公司为例,如果按照 传统的”瀑布开发模式”,先挖10栋楼的地基、再盖骨架、然后架设屋顶,每个阶段都等到前一个阶段完 成后开始,可能需要两年才能一次性交付10栋楼。也就是说,如果不考虑预售,该项目必须等到两年后 才能回款。 敏捷开发是六个月后交付一号楼,后面每两个月交付一栋楼。因此,半年就能回款10%,后 面每个月都会有现金流,资金压力就大大减轻了。

==降低风险== 敏捷开发的第二个好处是,及时了解市场需求,降低产品不适用的风险。 请想一想,哪一种情况损失比 较小:10栋楼都造好以后,才发现卖不出去,还是造好第一栋楼,就发现卖不出去,从而改进或停建后面9栋楼

持续集成

持续集成( Continuous integration , 简称 CI )指的是,频繁地(一天多次)将代码集成到主干。 持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干 之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。 通过持续集成, 团队可以快速的从一个功能到另一个功能,简而言之,敏捷软件开发很大一部分都要归 功于持续集成

持续集成的流程
提交

流程的第一步,是开发者向代码仓库提交代码。所有后面的步骤都始于本地代码的一次提交 (commit)。

测试(第一轮)

代码仓库对commit操作配置了钩子(hook),只要提交代码或者合并进主干,就会跑自动化测试。

构建

通过第一轮测试,代码就可以合并进主干,就算可以交付了。 交付后,就先进行构建(build),再进入第二轮测试。所谓构建,指的是将源码转换为可以运行的实 际代码,比如安装依赖,配置各种资源(样式表、JS脚本、图片)等等。

测试(第二轮)

构建完成,就要进行第二轮测试。如果第一轮已经涵盖了所有测试内容,第二轮可以省略,当然,这时 构建步骤也要移到第一轮测试前面。

部署

过了第二轮测试,当前代码就是一个可以直接部署的版本(artifact)。将这个版本的所有文件打包( tar filename.tar * )存档,发到生产服务器。

回滚

一旦当前版本发生问题,就要回滚到上一个版本的构建结果。最简单的做法就是修改一下符号链接,指 向上一个版本的目录

持续集成的组成要素

  • 一个自动构建过程, 从检出代码、 编译构建、 运行测试、 结果记录、 测试统计等都是自动完成 的,无需人工干预。
  • 一个代码存储库,即需要版本控制软件来保障代码的可维护性,同时作为构建过程的素材库,一般使用SVN或Git。
  • 一个持续集成服务器,Jenkins 就是一个配置简单和使用方便的持续集成服务器

持续集成的好处

1、降低风险,由于持续集成不断去构建,编译和测试,可以很早期发现问题,所以修复的代价就少;
2、对系统健康持续检查,减少发布风险带来的问题;
3、减少重复性工作;
4、持续部署,提供可部署单元包;
5、持续交付可供使用的版本;
6、增强团队信心

JenKins介绍

Jenkins 是一款流行的开源持续集成(Continuous Integration)工具,广泛用于项目开发,具有自动化构建、测试和部署等功能。官网: http://jenkins-ci.org/
CI:持续集成(Continuous Integration)
CD:持续部署(Continuous ????)

Jenkins的特征:

  • 开源的 Java语言开发持续集成工具,支持持续集成,持续部署。
  • 易于安装部署配置:可通过 方便web界面配置管理。
  • 消息通知及测试报告:集成 yum安装,或下载war包以及通过docker容器等快速实现安装部署,可 RSS/E-mail通过RSS发布构建结果或当构建完成时通过e-mail通知,生成JUnit/TestNG测试报告。 Jenkins能够让多台计算机一起构建/测试。
  • 分布式构建:支持
  • 文件识别: Jenkins能够跟踪哪次构建生成哪些jar,哪次构建使用哪个版本的jar等。
  • 丰富的插件支持:支持扩展插件,你可以开发适合自己团队使用的工具,如 docker等

持续集成流程说明

服务器列表[统一使用CentOS7]

名称 IP地址 安装的软件
代码托管服务器 192.168.200.128 Gitlab-12.4.2
持续集成服务器 192.168.200.129 Jenkins-2.190.3,JDK1.8,Maven3.6.2,Git, SonarQube
应用测试服务器 192.168.66.102 JDK1.8,Tomcat8.5

Gitlab[团队个人版github]代码托管服务器安装

[lanyun_group / web_demo · GitLab] (http://192.168.200.128:82/lanyun_group/web_demo)

官网: https://about.gitlab.com/
GitLab 是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的 web服务。

GitLab和GitHub一样属于第三方基于Git开发的作品,免费且开源(基于MIT协议),与Github类似, 可以注册用户,任意提交你的代码,添加SSHKey等等。不同的是,GitLab是可以部署到自己的服务器 上,数据库等一切信息都掌握在自己手上,适合团队内部协作开发,你总不可能把团队内部的智慧总放 在别人的服务器上吧?简单来说可把GitLab看作个人版的GitHub

  • 安装相关依赖

    yum -y install policycoreutils openssh-server openssh-clients postfix

  • 启动ssh服务&设置为开机启动

    systemctl enable sshd && sudo systemctl start sshd

  • 设置postfix开机自启,并启动,postfix支持gitlab发信功能

    systemctl enable postfix && systemctl start postfix

  • 开放ssh以及http服务,然后重新加载防火墙列表

    firewall-cmd –add-service=ssh –permanent
    firewall-cmd –add-service=http –permanent
    firewall-cmd –reload

    如果关闭防火墙就不需要做以上配置

  • 下载gitlab包,并且安装

    在线下载安装包
    wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el6/gitlab-ce-12.4.2-ce.0.el6.x 86_64.rpm 安装
    rpm -i gitlab-ce-12.4.2-ce.0.el6.x86_64.rpm

  • 修改gitlab配置

    vi /etc/gitlab/gitlab.rb

    修改gitlab访问地址和端口,默认为80,我们改为82
    external_url ‘ http://192.168.200.132:82'
    nginx[‘listen_port’] = 82

  • 重载配置及启动gitlab

    gitlab-ctl reconfigure
    gitlab-ctl restart ★★

  • 把端口添加到防火墙

firewall-cmd –zone=public –add-port=82/tcp –permanent
firewall-cmd –reload

启动成功后,看到以下修改管理员root密码的页面,修改密码后,然后登录即可

账号:root
密码:panchunyao123

Gitlab用户在组里面有5种不同权限:

Guest:可以创建issue、发表评论,不能读写版本库
Reporter:可以克隆代码,不能提交,QA、PM 可以赋予这个权限
Developer:可以克隆代码、开发、提交、push,普通开发可以赋予这个权限
Maintainer:可以创建项目、添加tag、保护分支、添加项目成员、编辑项目,核心开发可以赋予这个 权限 Owner:可以设置项目访问权限 - Visibility Level、删除项目、迁移项目、管理组成员,开发组组 长可以赋予这个权限

如果张三被管理员添加了Owner权限,那么张三就可以在idea里面通过gitlab里面的仓库地址 上传项目到这个web_demo了。

持续集成环境—Jenkins安装【我用了最新的Jenkins 2.440.1】

[Setup Wizard [Jenkins]] (http://192.168.200.129:8888/)

  • 安装JDK Jenkins需要依赖JDK,所以先安装JDK1.8

    yum install java-1.8.0-openjdk* -y

​ 安装目录为:/usr/lib/jvm

  • 获取jenkins安装包

下载页面: https://jenkins.io/zh/download/
安装文件:jenkins-2.190.3-1.1.noarch.rpm

  • 把安装包上传到192.168.66.101服务器,进行安装

    rpm -ivh jenkins-2.190.3-1.1.noarch.rpm

  • 进入文件目录

    cd /etc/sysconfig

  • 修改Jenkins配置

    vi jenkins

修改内容如下:

JENKINS_USER=”root”
JENKINS_PORT=”8888”

  • 启动Jenkins

    systemctl start jenkins ★★

    [root@localhost sysconfig]# systemctl start jenkins

  • 打开浏览器访问
    [192.168.200.129:8888] (http://192.168.200.129:8888/)
    注意:本服务器把防火墙关闭了,如果开启防火墙,需要在防火墙添加端口

  • 获取并输入admin账户密码

    [root@localhost sysconfig]# cat /var/lib/jenkins/secrets/initialAdminPassword

    [root@localhost sysconfig]# cat /root/.jenkins/secrets/initialAdminPassword
    3d83c355f83d4942847d4390fc5dcc39

  • 跳过插件安装 [点击第二个选择 然后上面全无]
    因为Jenkins插件需要连接默认官网下载,速度非常慢,而且经过会失败,所以我们暂时先跳过插件安装

综上操作

[root@localhost ~]# java -version
java version “21.0.1” 2023-10-17 LTS
Java(TM) SE Runtime Environment (build 21.0.1+12-LTS-29)
Java HotSpot(TM) 64-Bit Server VM (build 21.0.1+12-LTS-29, mixed mode, sharing)
[root@localhost ~]# cd /usr/lib/jvm
[root@localhost jvm]# ll
总用量 0
lrwxrwxrwx. 1 root root 26 6月 15 16:27 java -> /etc/alternatives/java_sdk
lrwxrwxrwx. 1 root root 32 6月 15 16:27 java-1.8.0 -> /etc/alternatives/java_sdk_1.8.0
lrwxrwxrwx. 1 root root 40 6月 15 16:27 java-1.8.0-openjdk -> /etc/alternatives/java_sdk_1.8.0_openjdk
drwxr-xr-x. 9 root root 109 6月 15 16:27 java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64
lrwxrwxrwx. 1 root root 34 6月 15 16:27 java-openjdk -> /etc/alternatives/java_sdk_openjdk
lrwxrwxrwx. 1 root root 21 6月 15 16:27 jre -> /etc/alternatives/jre
lrwxrwxrwx. 1 root root 27 6月 15 16:27 jre-1.8.0 -> /etc/alternatives/jre_1.8.0
lrwxrwxrwx. 1 root root 35 6月 15 16:27 jre-1.8.0-openjdk -> /etc/alternatives/jre_1.8.0_openjdk
lrwxrwxrwx. 1 root root 49 6月 15 16:27 jre-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64 -> java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64/jre
lrwxrwxrwx. 1 root root 29 6月 15 16:27 jre-openjdk -> /etc/alternatives/jre_openjdk
[root@localhost jvm]# cd java-1.8.0-openjdk
[root@localhost java-1.8.0-openjdk]#

下一步:你只需要执行 升级Jenkins的升级流程

以下是你的专属升级操作指令合集(CentOS 7 专用):


🔧 1. 备份 Jenkins 数据(可选但强烈推荐)

sudo systemctl stop jenkins
sudo cp -r /var/lib/jenkins /var/lib/jenkins_backup_$(date +%F)
sudo cp /etc/sysconfig/jenkins /etc/sysconfig/jenkins.bak

🔧 2. 添加 Jenkins 官方 LTS Yum 源

sudo curl --silent --location https://pkg.jenkins.io/redhat-stable/jenkins.repo | sudo tee /etc/yum.repos.d/jenkins.repo > /dev/null
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key

🔧 3. 升级 Jenkins

sudo yum clean all
sudo yum makecache
sudo yum upgrade jenkins -y

如果提示没有可升级的版本,执行以下命令强制重新安装最新版 Jenkins:

sudo yum remove jenkins -y
sudo yum install jenkins -y

⚠️ 不会丢失数据,因为 Jenkins 配置、任务、插件都在 /var/lib/jenkins 中,你已经备份过。


🔧 4. 启动 Jenkins 并查看状态

sudo systemctl daemon-reexec
sudo systemctl start jenkins
sudo systemctl status jenkins

🔧 5. 访问 Jenkins 页面

访问地址:

http://<你的服务器IP>:8888

🔧 6. 查看 Jenkins 新版本号

jenkins --version

或者登录网页右下角查看,应该是类似:

Jenkins ver. 2.440.1

查看 Jenkins 服务状态

请运行以下命令获取具体错误:

sudo systemctl status jenkins -l

步骤 3:重启 Jenkins 服务

sudo systemctl daemon-reexec
sudo systemctl restart jenkins
sudo systemctl status jenkins -l

🔗 插件下载地址与命令

以下命令会下载所有缺失依赖:

bash复制编辑# 进入 Jenkins 插件目录
cd /var/lib/jenkins/plugins

# 下载缺失的依赖插件
wget https://updates.jenkins.io/download/plugins/commons-lang3-api/3.13.0-62.v7d18e55f51e2/commons-lang3-api.hpi
wget https://updates.jenkins.io/download/plugins/ionicons-api/56.v1b_1c8c49374e/ionicons-api.hpi
wget https://updates.jenkins.io/download/plugins/caffeine-api/3.1.8-133.v17b_1ff2e0599/caffeine-api.hpi

持续集成环境—Jenkins插件管理【Manage Jenkins】

Jenkins本身不提供很多功能,我们可以通过使用插件来满足我们的使用。例如从Gitlab拉取代码,使用 Maven构建项目等功能需要依靠插件完成。接下来演示如何下载插件。

修改Jenkins插件下载地址

Jenkins国外官方插件地址下载速度非常慢,所以可以修改为国内插件地址:
Jenkins->Manage Jenkins->Manage Plugins,点击Available
新版本:Jenkins → Manage Jenkins → Plugins → Avaliable plugins

去Jenkins默认的开发目录

这样做是为了把 Jenkins官方的插件列表下载到本地,接着修改地址文件,替换为国内插件地址

[root@localhost sysconfig]# cd /var/lib/jenkins/
[root@localhost jenkins]# cd updates/
[root@localhost updates]# ll
总用量 3064
-rw-r–r–. 1 root root 3125621 6月 15 16:38 default.json
-rw-r–r–. 1 root root 7976 6月 15 16:38 hudson.tasks.Maven.MavenInstaller

sed -i ‘s/http://updates.jenkins ci.org/download/https://mirrors.tuna.tsinghua.edu.cn/jenkins/g’ default.json && sed -i ‘s/http:// www.google.com/https:\/\/ www.baidu.com/g' default.json

最后,Manage Plugins点击Advanced,把Update Site改为国内插件下载地址

https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

http://192.168.200.129:8888/restart 重启Jenkins

下载中文汉化插件

http://192.168.66.101:8888/restart ,重启Jenkins。 Jenkins->Manage Jenkins->Manage Plugins,点击Available,搜索”Chinese”

开启权限全局安全配置

在Security中的授权策略切换为 “Role-Based Strategy”,保存

【从插件市场上下载下来的 可以直接通过MobaXterm 放在/var/lib/jenkins/plugins/ 然后重启Jenkins】

创建角色

在系统管理页面进入 Manage and Assign Roles;点击”Manage Roles”

里面的Global roles(全局角色):管理员等高级用户可以创建基于全局的角色 Project roles(项目角色): 针对某个或者某些项目的角色 Slave roles(奴隶角色):节点相关的权限

我们添加以下三个角色:【一个基础角色 两个项目角色

  • baseRole :该角色为全局角色。这个角色需要绑定Overall下面的Read权限,是为了给所有用户绑 定最基本的Jenkins访问权限。注意:如果不给后续用户绑定这个角色,会报错误:用户名 is missing the Overall/Read permission
  • role1 :该角色为项目角色(下面的Item roles)。使用正则表达式绑定” itcast.* “,意思是只能操作itcast开头的项目。
  • role2 :该角色也为项目角色。绑定”itheima.*”,意思是只能操作itheima开头的项目。

创建用户

在系统管理页面进入 Manage Users
用户一:用户名:eric 密码:123456
用户二:用户名:JacK 密码:123456

给用户分配角色

系统管理页面进入Manage and Assign Roles,点击Assign Roles
绑定规则如下:

  • eric 用户分别绑定baseRole和role1角色
  • jack 用户分别绑定baseRole和role2角色

创建项目测试权限

以itcast管理员账户创建两个项目,分别为itcast01和itheima01

结果为:

  • eric 用户登录,只能看到itcast01项目
  • jack 用户登录,只能看到itheima01项目

持续集成环境—Jenkins凭证管理

凭据可以用来存储需要密文保护的数据库密码、Gitlab密码信息、Docker私有仓库密码等,以便 Jenkins可以和这些第三方的应用进行交互

安装Credentials Binding插件

要在Jenkins使用凭证管理功能,需要安装Credentials Binding插件
安装插件后,左边多了”凭证“菜单,在这里管理所有凭证 [新版是在Security栏有凭证管理]
进入凭据后点击Stores scoped to Jenkins的**全局**

可以添加的凭证有 5种:
  • Username with password :用户名和密码
  • SSH Username with private key: 使用SSH用户和密钥
  • Secret file:需要保密的文本文件,使用时Jenkins会将文件复制到一个临时目录中,再将文件路径 设置到一个变量中,等构建结束后,所复制的Secret file就会被删除。
  • Secret text :需要保存的一个加密的文本串,如钉钉机器人或Github的api token【k8s也会用】
  • Certificate :通过上传证书文件的方式

常用的凭证类型有:Username with password(用户密码)SSH Username with private key(SSH 密钥)
接下来以使用Git工具到Gitlab拉取项目源码为例,演示Jenkins的如何管理Gitlab的凭证
在Jenkins里面安装git插件
要先在Jenkins的服务里面安装git插件

CentOS7上安装Git工具

yum install git -y            安装
git --version                 安装后查看版本

用户密码类型

**全局添加凭证**:Dashboard → Manage Jenkins → Credentials → System → Global credentials (unrestricted) → New credentials

-
Jenkins->凭证->系统->全局凭证->添加凭证

测试凭证是否可用

创建一个FreeStyle项目:新建Item->FreeStyle Project->确定
找到 “源码管理”->”Git”,在Repository URL复制Gitlab中的项目URL

新搞个item然后点击进去 左侧的配置→General→源码管理→选择Git →↓

Repository URL:http://192.168.200.128:82/lanyun_group/web_demo.git
Credentials:【选择刚刚给张三创建的凭证】

保存配置后,点击构建 ”Build Now“ 开始构建项目
可以在左下方的Builds里找到刚刚构建的项目 可以查看控制台输出

Started by user root
Running as SYSTEM
Building in workspace /var/lib/jenkins/workspace/test02
The recommended git tool is: NONE
using credential ca22e56f-0ecc-4fdc-965d-01e329a0b68a
Cloning the remote Git repository
Cloning repository http://192.168.200.128:82/lanyun_group/web_demo.git
 > git init /var/lib/jenkins/workspace/test02 # timeout=10
Fetching upstream changes from http://192.168.200.128:82/lanyun_group/web_demo.git
 > git --version # timeout=10
 > git --version # 'git version 1.8.3.1'
using GIT_ASKPASS to set credentials gitlab-auth-password
 > git fetch --tags --progress http://192.168.200.128:82/lanyun_group/web_demo.git +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git config remote.origin.url http://192.168.200.128:82/lanyun_group/web_demo.git # timeout=10
 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
Avoid second fetch
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
Checking out Revision 2f41cd33af519a5c55df8d78ba59032d9069f0b8 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 2f41cd33af519a5c55df8d78ba59032d9069f0b8 # timeout=10
Commit message: "初始化项目提交"
First time build. Skipping changelog.
Finished: SUCCESS

查看**/var/lib/jenkins/workspace/**目录,发现已经从Gitlab成功拉取了代码到Jenkins中

SSH密钥类型

SSH免密登录示意图:

[GitLab服务器(存放公钥:id_rsa.pub)] ←←←ssh免密登录←←← [Jenkins服务器(存放私钥:id_rsa)]

  • 使用root用户生成公钥和私钥

ssh-keygen -t rsa

在 192.168.200.129_Jenkins 服务器里

[root@localhost ~]# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:88fFvAS2e6i9c4+KXfmENWeu6NJNmOb1+CCsYLarYLc root@localhost.localdomain
The key's randomart image is:
+---[RSA 2048]----+
|                 |
|                 |
|            o    |
|           . =   |
|        S   .o=.+|
|         o o+=oBo|
|    o . + .+B+Boo|
|   . o + o.B+=+*.|
|      E.o.+oB=oo+|
+----[SHA256]-----+

[root@localhost ~]# cd /root/.ssh
[root@localhost .ssh]# ll
总用量 8
-rw-------. 1 root root 1679 6月  18 20:05 id_rsa       【私钥】
-rw-r--r--. 1 root root  408 6月  18 20:05 id_rsa.pub   【公钥】
  • 要在gitlab服务里存放一个公钥

以root账户登录->点击头像->Settings->SSH Keys→在Jenkins服务里面用
[root@localhost .ssh]# cat id_rsa.pub
打开公钥的文件得到信息。复制刚才id_rsa.pub文件的内容到这里,点击”Add Key”

  • 在Jenkins中添加凭证,配置私钥

在Jenkins添加一个全局新的凭证,类型为”SSH Username with private key“,在jenkins服务器里输入指令查看私钥
[root@localhost .ssh]# cat id_rsa
把刚才生成私钥文件内容复制过来塞进去
此时去gitlab项目中把ssh的复制过来git@192.168.200.128:lanyun_group/web_demo.git
同样尝试构建项目,如果代码可以正常拉取,代表凭证配置成功!

如果报错

Command "git ls-remote -h git@192.168.200.128:lanyun_group/web_demo.git HEAD" returned status code 128
stderr: No ECDSA host key is known for 192.168.200.128 and you have requested strict checking.
Host key verification failed.
fatal: Could not read from remote repository.

报错原因

SSH 主机指纹未验证(Host key verification failed)

Git 使用的是 SSH 协议访问远程仓库(git@192.168.200.128),但本地机器之前没有连接过该主机,或者 .ssh/known_hosts 文件中没有该 IP 的公钥信息。

而且启用了“严格检查”(Strict Host Key Checking),所以 Git 拒绝连接该服务器。



★★ 方法:手动信任主机(推荐)★★
ssh git@192.168.200.128
系统会提示你是否信任该主机,比如:

The authenticity of host '192.168.200.128 (192.168.200.128)' can't be established.
ECDSA key fingerprint is SHA256:xxx...
Are you sure you want to continue connecting (yes/no/[fingerprint])?
✅ 输入 yes 之后,主机公钥会加入 ~/.ssh/known_hosts 文件,后续连接将不再失败。
到这里已经完成了这份图的部分内容,要开始融入Maven

持续集成流程说明

持续集成环境—Maven安装和配置

在Jenkins集成服务器上,我们需要安装Maven来编译和打包项目

apache-maven-3.6.2-bin.tar.gz传到 /root 根目录中【Jenkins服务器】

tar -xzf apache-maven-3.6.2-bin.tar.gz       解压
mkdir -p /opt/maven                          创建目录
mv apache-maven-3.6.2/* /opt/maven           移动文件

[root@localhost ~]# mkdir -p /opt/maven
[root@localhost ~]# mv apache-maven-3.6.2/* /opt/maven
[root@localhost ~]# cd /opt/maven/
[root@localhost maven]# ll
总用量 28
drwxr-xr-x. 2 root root 97 6月 18 21:37 bin
drwxr-xr-x. 2 root root 42 6月 18 21:37 boot
drwxrwxr-x. 3 1000 mysql 63 8月 27 2019 conf
drwxrwxr-x. 4 1000 mysql 4096 6月 18 21:37 lib
-rw-rw-r–. 1 1000 mysql 12846 8月 27 2019 LICENSE
-rw-rw-r–. 1 1000 mysql 182 8月 27 2019 NOTICE
-rw-rw-r–. 1 1000 mysql 2533 8月 27 2019 README.txt
[root@localhost maven]#

配置环境变量

vi /etc/profile

export JAVA_HOME=/usr/local/java/jdk-17.0.8+7
export MAVEN_HOME=/opt/maven
export PATH=$PATH:$JAVA_HOME/bin:$MAVEN_HOME/bin

source /etc/profile 配置生效
mvn -v 查找Maven版本

全局工具配置关联JDK和Maven

Jenkins->Manage Jenkins->Tools→ JDK安装 → 新增JDK →

【JDK】
别名:jdk-17.0.8+7
JAVA_HOME:/usr/local/java/jdk-17.0.8+7

下面的Maven安装→ 新增Maven →

【Maven】
Name:maven3.6.2
MAVEN_HOME:/opt/maven

点击应用 → 保存

添加Jenkins全局变量

Jenkins → Manage Jenkins → System → 下面找到全局属性Environment variables

以下是用键值对的形式显示:
JAVA_HOME   /usr/local/java/jdk-17.0.8+7
M2_HOME     /opt/maven
PATH+EXTRA  $M2_HOME/bin

修改Maven的settings.xml

mkdir /root/repo                  创建本地仓库目录
vi /opt/maven/conf/settings.xml

本地仓库改为:/root/repo

  <!-- localRepository
   | The path to the local repository maven will use to store artifacts.
   |
   | Default: ${user.home}/.m2/repository
  -->
  <localRepository>/root/repo</localRepository>

添加阿里云私服地址:

    <mirror>
      <id>alimaven</id>
      <name>aliyun maven</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
      <mirrorOf>central</mirrorOf>
    </mirror>

测试Maven是否配置成功

使用之前的gitlab密码测试项目,修改配置;
在某个项目中→Triggers中→Build Steps选择 Execute shell(执行shell脚本命令)
输入

mvn clean package

然后再去构建!!

🔧 步骤一:删除损坏的插件文件

执行以下命令彻底删除这个错误的插件缓存:

rm -rf /root/repo/org/apache/maven/plugins/maven-clean-plugin

🔧 步骤二:将 Maven 镜像源改为稳定的中央仓库

编辑 /root/.m2/settings.xml/etc/maven/settings.xml(按你的系统配置)为以下内容:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                              http://maven.apache.org/xsd/settings-1.0.0.xsd">
  
  <localRepository>/root/repo</localRepository>

  <mirrors>
    <mirror>
      <id>central</id>
      <mirrorOf>*</mirrorOf>
      <name>Maven Central</name>
      <url>https://repo.maven.apache.org/maven2</url>
    </mirror>
  </mirrors>

</settings>

持续集成环境—Tomcat安装和配置

安装Tomcat8.5 把Tomcat压缩包上传到192.168.200.131服务器

yum install java-1.8.0-openjdk* -y 安装JDK(已完成)
tar -xzf apache-tomcat-8.5.47.tar.gz 解压
mkdir -p /opt/tomcat 创建目录
mv /root/apache-tomcat-8.5.47/* /opt/tomcat 移动文件
启动tomcat
/opt/tomcat/bin/startup.sh ★★

[Apache Tomcat/8.5.47] (http://192.168.200.131:8090/) 【已改端口8090】

关闭已有 Tomcat(建议先关闭,避免冲突):

/opt/tomcat/bin/shutdown.sh

再启动

/opt/tomcat/bin/startup.sh

不过现在访问这个地址是没有权限的[403 Access Denied] (http://192.168.200.131:8090/manager/) 现在要增加权限

配置 Tomcat用户角色权限

默认情况下Tomcat是没有配置用户角色权限的

403 Access Denied

You are not authorized to view this page.

By default the Manager is only accessible from a browser running on the same machine as Tomcat. If you wish to modify this restriction, you’ll need to edit the Manager’s context.xml file.

If you have already configured the Manager application to allow access and you have used your browsers back button, used a saved book-mark or similar then you may have triggered the cross-site request forgery (CSRF) protection that has been enabled for the HTML interface of the Manager application. You will need to reset this protection by returning to the main Manager page. Once you return to this page, you will be able to continue using the Manager application’s HTML interface normally. If you continue to see this access denied message, check that you have the necessary permissions to access this application.

If you have not changed any configuration files, please examine the file conf/tomcat-users.xml in your installation. That file must contain the credentials to let you use this webapp.

For example, to add the manager-gui role to a user named tomcat with a password of s3cret, add the following to the config file listed above.

<role rolename="manager-gui"/>
<user username="tomcat" password="s3cret" roles="manager-gui"/>

Note that for Tomcat 7 onwards, the roles required to use the manager application were changed from the single manager role to the following four roles. You will need to assign the role(s) required for the functionality you wish to access.

  • manager-gui - allows access to the HTML GUI and the status pages
  • manager-script - allows access to the text interface and the status pages
  • manager-jmx - allows access to the JMX proxy and the status pages
  • manager-status - allows access to the status pages only

The HTML interface is protected against CSRF but the text and JMX interfaces are not. To maintain the CSRF protection:

  • Users with the manager-gui role should not be granted either the manager-script or manager-jmx roles.
  • If the text or jmx interfaces are accessed through a browser (e.g. for testing since these interfaces are intended for tools not humans) then the browser must be closed afterwards to terminate the session.

For more information - please see the Manager App How-To.

但是,后续Jenkins部署项目到Tomcat服务器,需要用到Tomcat的用户,所以修改tomcat以下配置, 添加用户及权限

vi /opt/tomcat/conf/tomcat-users.xml

内容如下:

<tomcat-users>
 <role rolename="tomcat"/>
 <role rolename="role1"/>
 <role rolename="manager-script"/>
 <role rolename="manager-gui"/>
 <role rolename="manager-status"/> 
<role rolename="admin-gui"/>
 <role rolename="admin-script"/> 
<user username="tomcat" password="tomcat" roles="manager-gui,manager
script,tomcat,admin-gui,admin-script"/>
 </tomcat-users>

用户和密码都是: tomcat
注意:为了能够刚才配置的用户登录到Tomcat,还需要修改以下配置

vi /opt/tomcat/webapps/manager/META-INF/context.xml

<!--
 <Valve className="org.apache.catalina.valves.RemoteAddrValve"
 allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
-->

注释掉这行就行!!

关闭已有 Tomcat(建议先关闭,避免冲突):

/opt/tomcat/bin/shutdown.sh

再启动

/opt/tomcat/bin/startup.sh

[/manager] (http://192.168.200.131:8090/manager/html) 此时就可以访问了!!!

3、Jenkins构建Maven项目

Jenkins项目构建类型(1)-Jenkins构建的项目类型介绍

Jenkins中自动构建项目的类型有很多,常用的有以下三种:

  • 自由风格软件项目( FreeStyle Project)
  • Maven 项目(Maven Project)
  • 流水线项目( Pipeline Project)

每种类型的构建其实都可以完成一样的构建过程与结果,只是在操作方式、灵活度等方面有所区别,在 实际开发中可以根据自己的需求和习惯来选择。(PS:个人推荐使用流水线类型,因为灵活度非常高)

Jenkins项目构建类型(2)-自由风格项目构建

下面演示创建一个自由风格项目来完成项目的集成过程:

拉取代码→编译→打包→部署

……

部署

把项目部署到远程的Tomcat里面

1)安装 Deploy to container插件

Jenkins本身无法实现远程部署到Tomcat的功能,需要在Jenkins里面安装Deploy to container插件实现

Jenkins持续集成从入门到精通.pdf 【39页】

①:Build Steps → Execute shell →↓

echo "开始进行编译构建"
mvn clean package
echo "编译构建结束"

②:在Jenkins的项目配置里 下面
构建后操作:【Deploy war/ear to a container】→ 选择 Tomcat 8.x Remote
WAR/EAR filestarget/*.war
Containers
新增一个凭证 用户名tomcat 密码tomcat

部署成功后,访问项目:[演示项目主页] (http://192.168.200.131:8090/web_demo-1.0-SNAPSHOT/)

如果看到此页面,代表项目部署成功啦!--这是master分支 添加用户
修改用户
删除用户 查询用户

演示改动代码后的持续集成
1)IDEA中源码修改并提交到gitlab
2)在Jenkins中项目重新构建
3)访问Tomcat

演示改动代码后的持续集成

1)IDEA中源码修改并提交到gitlab
2)在Jenkins中项目重新构建
3)访问Tomcat

Jenkins项目构建类型(3)-Maven项目构建

1)安装 Maven Integration 插件
2)创建 Maven 项目
3)配置项目
拉取代码和远程部署的过程和自由风格项目一样,只是”构建”部分不同

新建Item构建一个maven项目
其中Build的Root POM是要找到pom.xml的路径才行【默认执行maven命令】
所以只需要在Goals and options里面敲上clean package

Jenkins项目构建类型(4)-Pipeline流水线项目构建(*)

Pipeline简介
1)概念

Pipeline,简单来说,就是一套运行在 Jenkins 上的工作流框架,将原来独立运行于单个或者多个节点 的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化的工作。

2)使用Pipeline有以下好处(来自翻译自官方文档):

代码:Pipeline以代码的形式实现,通常被检入源代码控制,使团队能够编辑,审查和迭代其传送流 程。 持久:无论是计划内的还是计划外的服务器重启,Pipeline都是可恢复的。 可停止:Pipeline可接 收交互式输入,以确定是否继续执行Pipeline。 多功能:Pipeline支持现实世界中复杂的持续交付要 求。它支持fork/join、循环执行,并行执行任务的功能。 可扩展:Pipeline插件支持其DSL的自定义扩 展 ,以及与其他插件集成的多个选项。

3)如何创建 Jenkins Pipeline呢?
  • Pipeline 脚本是由 Groovy 语言实现的,但是我们没必要单独去学习 Groovy
  • Pipeline 支持两种语法:Declarative(声明式)Scripted Pipeline(脚本式)语法
  • Pipeline 也有两种创建方法:可以直接在 Jenkins 的 Web UI 界面中输入脚本;也可以通过创建一 个 Jenkinsfile 脚本文件放入项目源码库中(一般我们都推荐在 Jenkins 中直接从源代码控制(SCM) 中直接载入 Jenkinsfile Pipeline 这种方法
安装Pipeline插件如果一次安装失败,重启一次再安装[没视图就安装pipeline Stage View]

Manage Jenkins->Manage Plugins->可选插件

Pipeline语法快速入门

1)Declarative声明式-Pipeline

创建项目 [agent是代理、stage是阶段、steps是步骤]

  • Node :节点,一个 Node 就是一个 Jenkins 节点,Master 或者 Agent,是执行 Step 的具体运行 环境,后续讲到Jenkins的Master-Slave架构的时候用到。
  • Stage :阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,比如: Build、Test、Deploy,Stage 是一个逻辑分组的概念。
  • Step :步骤,Step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像, 由各类 Jenkins 插件提供,比如命令:sh ‘make’,就相当于我们平时 shell 终端中执行 make 命令 一样。
pipeline {
    agent any
    stages {
        stage('pull code') {
            steps {
                echo 'pull code'
            }
        }
        stage('build project') {
            steps {
                echo 'build project'
            }
        }
        stage('publish project') {
            steps {
                echo 'publish project'
            }
        }
    }
}
2)Scripted Pipeline脚本式-Pipeline

创建项目
流水线那里选择 “Scripted Pipeline

node {
    def mvnHome
    stage('pull code'){
        echo 'pull code'
    }
    stage('build project'){
        echo 'build project'
    }
    stage('publish project'){
        echo 'publish project'
    }
}

流水线脚本:点击可以使用官方的脚本生成器

  • 片段生成器
    示例步骤:checkout:Check out from version control [从版本控制里拉取代码]
    选择git拉取代码 填写URL和凭证 → 生成流水线脚本

拉取代码

Pipeline script
pipeline{
    agent any
    stages{
        stage('拉取代码'){
            steps{
                checkout scmGit(branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a', url: 'http://192.168.200.132:82/lanyun_group/web_demo.git']])
            }
        }
    }
}
Console Output
Started by user root
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/workspace/web_demo_pipeline
[Pipeline] {
[Pipeline] stage
[Pipeline] { (拉取代码)
[Pipeline] checkout
The recommended git tool is: NONE
using credential ca22e56f-0ecc-4fdc-965d-01e329a0b68a
Cloning the remote Git repository
Cloning repository http://192.168.200.132:82/lanyun_group/web_demo.git
 > git init /var/lib/jenkins/workspace/web_demo_pipeline # timeout=10
Fetching upstream changes from http://192.168.200.132:82/lanyun_group/web_demo.git
 > git --version # timeout=10
 > git --version # 'git version 1.8.3.1'
using GIT_ASKPASS to set credentials gitlab-auth-password
 > git fetch --tags --progress http://192.168.200.132:82/lanyun_group/web_demo.git +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git config remote.origin.url http://192.168.200.132:82/lanyun_group/web_demo.git # timeout=10
 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
Avoid second fetch
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
Checking out Revision 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 # timeout=10
Commit message: "修改index.jsp加入细节(!)"
First time build. Skipping changelog.
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

去Jenkins的服务器里寻找/var/lib/jenkins/workspace/web_demo_pipeline

编译打包

流水线脚本:点击可以使用官方的脚本生成器

  • 片段生成器
    示例步骤:sh:Shell Script
    Shell Script:里面需要打所需要的命令 mvn clean package
Pipeline script
pipeline{
    agent any
    stages{
        stage('build project'){
            steps{
                sh 'mvn clean package'
            }
        }
    }
}

--------------------------------------------
【总流水线代码】
pipeline {
    agent any
    stages {
        stage('pull code') {
            steps {
                checkout scmGit(
                    branches: [[name: '*/master']],
                    extensions: [],
                    userRemoteConfigs: [[
                        credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a',
                        url: 'http://192.168.200.132:82/lanyun_group/web_demo.git'
                    ]]
                )
            }
        }
        stage('build project') {
            steps {
                sh 'mvn clean package'
            }
        }
    }
}
Console Output
Started by user root
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/workspace/web_demo_pipeline
[Pipeline] {
[Pipeline] stage
[Pipeline] { (pull code)
[Pipeline] checkout
The recommended git tool is: NONE
using credential ca22e56f-0ecc-4fdc-965d-01e329a0b68a
 > git rev-parse --resolve-git-dir /var/lib/jenkins/workspace/web_demo_pipeline/.git # timeout=10
Fetching changes from the remote Git repository
 > git config remote.origin.url http://192.168.200.132:82/lanyun_group/web_demo.git # timeout=10
Fetching upstream changes from http://192.168.200.132:82/lanyun_group/web_demo.git
 > git --version # timeout=10
 > git --version # 'git version 1.8.3.1'
using GIT_ASKPASS to set credentials gitlab-auth-password
 > git fetch --tags --progress http://192.168.200.132:82/lanyun_group/web_demo.git +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
Checking out Revision 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 # timeout=10
Commit message: "修改index.jsp加入细节(!)"
 > git rev-list --no-walk 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 # timeout=10
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (build project)
[Pipeline] sh
+ mvn clean package
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------< com.itheima:web_demo >------------------------
[INFO] Building web_demo 1.0-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ web_demo ---
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ web_demo ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /var/lib/jenkins/workspace/web_demo_pipeline/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ web_demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /var/lib/jenkins/workspace/web_demo_pipeline/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ web_demo ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /var/lib/jenkins/workspace/web_demo_pipeline/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ web_demo ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ web_demo ---
[INFO] No tests to run.
[INFO] 
[INFO] --- maven-war-plugin:3.3.2:war (default-war) @ web_demo ---
[INFO] Packaging webapp
[INFO] Assembling webapp [web_demo] in [/var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT]
[INFO] Processing war project
[INFO] Copying webapp resources [/var/lib/jenkins/workspace/web_demo_pipeline/src/main/webapp]
[INFO] Building war: /var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  10.242 s
[INFO] Finished at: 2025-06-24T20:32:45+08:00
[INFO] ------------------------------------------------------------------------
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

部署

流水线脚本:点击可以使用官方的脚本生成器

  • 片段生成器
    示例步骤:deploy:Deploy war/ear to a container
    WAR/EAR files:target/*.war
    containers:Tomcat 8.x Remote 【容器可以增加多台】
    credentials:添加tomcat凭证
    Tomcat URL:http://192.168.200.131:8090/
Pipeline script
【总流水线代码】
pipeline {
    agent any
    stages {
        stage('pull code') {
            steps {
                checkout scmGit(
                    branches: [[name: '*/master']],
                    extensions: [],
                    userRemoteConfigs: [[
                        credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a',
                        url: 'http://192.168.200.132:82/lanyun_group/web_demo.git'
                    ]]
                )
            }
        }
        stage('build project') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('publish') {
            steps {
                deploy adapters: [tomcat8(alternativeDeploymentContext: '', credentialsId: 'e847a130-29c3-4d0b-bf35-db8d00de7950', path: '', url: 'http://192.168.200.131:8090/')], contextPath: null, war: 'target/*.war'
            }
        }
    }
}
Console Output
Started by user root
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/workspace/web_demo_pipeline
[Pipeline] {
[Pipeline] stage
[Pipeline] { (pull code)
[Pipeline] checkout
The recommended git tool is: NONE
using credential ca22e56f-0ecc-4fdc-965d-01e329a0b68a
 > git rev-parse --resolve-git-dir /var/lib/jenkins/workspace/web_demo_pipeline/.git # timeout=10
Fetching changes from the remote Git repository
 > git config remote.origin.url http://192.168.200.132:82/lanyun_group/web_demo.git # timeout=10
Fetching upstream changes from http://192.168.200.132:82/lanyun_group/web_demo.git
 > git --version # timeout=10
 > git --version # 'git version 1.8.3.1'
using GIT_ASKPASS to set credentials gitlab-auth-password
 > git fetch --tags --progress http://192.168.200.132:82/lanyun_group/web_demo.git +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
Checking out Revision 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 # timeout=10
Commit message: "修改index.jsp加入细节(!)"
 > git rev-list --no-walk 7ae1ec4086586fb471b0e9dee0a0d00d0b6d6f54 # timeout=10
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (build project)
[Pipeline] sh
+ mvn clean package
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------< com.itheima:web_demo >------------------------
[INFO] Building web_demo 1.0-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ web_demo ---
[INFO] Deleting /var/lib/jenkins/workspace/web_demo_pipeline/target
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ web_demo ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /var/lib/jenkins/workspace/web_demo_pipeline/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ web_demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /var/lib/jenkins/workspace/web_demo_pipeline/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ web_demo ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /var/lib/jenkins/workspace/web_demo_pipeline/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ web_demo ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ web_demo ---
[INFO] No tests to run.
[INFO] 
[INFO] --- maven-war-plugin:3.3.2:war (default-war) @ web_demo ---
[INFO] Packaging webapp
[INFO] Assembling webapp [web_demo] in [/var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT]
[INFO] Processing war project
[INFO] Copying webapp resources [/var/lib/jenkins/workspace/web_demo_pipeline/src/main/webapp]
[INFO] Building war: /var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  7.472 s
[INFO] Finished at: 2025-06-24T20:39:46+08:00
[INFO] ------------------------------------------------------------------------
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (publish)
[Pipeline] deploy
[DeployPublisher][INFO] Attempting to deploy 1 war file(s)
[DeployPublisher][INFO] Deploying /var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT.war to container Tomcat 8.x Remote with context null
  Redeploying [/var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT.war]
  Undeploying [/var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT.war]
  Deploying [/var/lib/jenkins/workspace/web_demo_pipeline/target/web_demo-1.0-SNAPSHOT.war]
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

Pipeline Script from SCM

刚才我们都是直接在Jenkins的UI界面编写Pipeline代码,这样不方便脚本维护,建议把Pipeline脚本放 在项目中(一起进行版本控制)
1)在项目根目录建立Jenkinsfile文件,把内容复制到该文件中。把Jenkinsfile上传到Gitlab

Jenkinsfile【在根目录下】

pipeline {
    agent any
    stages {
        stage('pull code') {
            steps {
                checkout scmGit(
                    branches: [[name: '*/master']],
                    extensions: [],
                    userRemoteConfigs: [[
                        credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a',
                        url: 'http://192.168.200.132:82/lanyun_group/web_demo.git'
                    ]]
                )
            }
        }
        stage('build project') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('publish') {
            steps {
                deploy adapters: [tomcat8(alternativeDeploymentContext: '', credentialsId: 'e847a130-29c3-4d0b-bf35-db8d00de7950', path: '', url: 'http://192.168.200.131:8090/')], contextPath: null, war: 'target/*.war'
            }
        }
    }
}

2)在项目中引用该文件
在流水线上
定义Pipeline script from SCM → 选择git → 填写gitlab的url和凭证 → 脚本路径Jenkinsfile(这是我们上传的jenkinsfile里面包含着流水线代码的文件)

Jenkins项目构建细节(1)-常用的构建触发器

Jenkins内置4种构建触发器:

  • 触发远程构建
  • 其他工程构建后触发( Build after other projects are build)
  • 定时构建( Build periodically)
  • 轮询 SCM(Poll SCM)
触发远程构建

打开web_demo_pipeline的配置 里面的Triggers 选择触发远程构建 身份验证令牌(这个token最好是加密的) 下面有个触发地址:

Use the following URL to trigger build remotely: JENKINS_URL/job/web_demo_pipeline/build?token=TOKEN_NAME 或者 /buildWithParameters?token=TOKEN_NAME
Optionally append &cause=Cause+Text to provide text that will be included in the recorded build cause.

上面地址里面的:job/web_demo_pipeline/build?token=TOKEN_NAME
http://192.168.200.129:8888/job/web_demo_pipeline/build?token=6666
在浏览器输入上述地址 回车打开 你会发现已经远程触发了Jenkins的构建行为!!

其他工程构建后触发

1)创建pre_job流水线工程
2)配置需要触发的工程

打开web_demo_pipeline的配置 里面的Triggers 选择Build after other projects are built
新建item → 创建一个pre_job的自由风格的工程 → 构建Execute shell 执行一个简单的脚本
回到web_demo_pipeline工程 在Triggers里面选择前置工程pre_job
此时去构建pre_job你会发现 在构建完后 web_demo_pipeline也会进行自动构建

定时构建

定时字符串从左往右分别为: 分 时 日 月 周
一些定时表达式的例子:(H为0 就为整点)

每30分钟构建一次:H代表形参 H/30 * * * * 10:02 10:32
每2个小时构建一次: H H/2 * * *
每天的8点,12点,22点,一天构建3次: (多个时间点中间用逗号隔开) 0 8,12,22 * * *
每天中午12点定时构建一次 H 12 * * *
每天下午18点定时构建一次 H 18 * * *
在每个小时的前半个小时内的每10分钟 H(0-29)/10 * * * *
每两小时一次,每个工作日上午9点到下午5点(也许是上午10:38,下午12:38,下午2:38,下午
4:38) H H(9-16)/2 * * 1-5

打开web_demo_pipeline的配置 里面的Triggers 选择 Build periodically 然后可以写时间cron表达式即可

轮询SCM [版本控制] [企业级不推荐使用]

轮询SCM,是指定时扫描本地代码仓库的代码是否有变更,如果代码有变更就触发项目构建。
打开web_demo_pipeline的配置 里面的Triggers 选择Poll SCM

jenkins的轮询SCM的构建到底是远程仓库变动触发 还是 本地仓库变动触发?

Jenkins 的“轮询 SCM”是通过轮询远程 Git 仓库(如 GitLab、GitHub)判断是否有变更,从而决定是否触发构建。

注意:这次构建触发器, Jenkins会定时扫描整个项目的代码,增大系统的开销,不建议使用。

Jenkins项目构建细节(2)-Git hook自动触发构建(*)gitlab就用gitlab插件,github则是github插件

先在Jenkins市场安装gitlab插件

刚才我们看到在Jenkins的内置构建触发器中,轮询SCM可以实现Gitlab代码更新,项目自动构建,但是 该方案的性能不佳。那有没有更好的方案呢? 有的。就是利用Gitlab的webhook实现代码push到仓 库,立即触发项目自动构建

轮询SCM原理:Jenkins →(发送定时请求)→ Gitlab代码变更
webhook原理:Gitlab代码变更 →(发送构建请求)→ Jenkins

打开web_demo_pipeline的配置 里面的Triggers 选择Build when a change is pushed to GitLab. GitLab webhook URL: http://192.168.200.129:8888/project/web_demo_pipeline

★ 要用root账号去GitLab开通一个webhook规则 → 点击上方的小齿轮 → 右侧Settings → Network → 展开Outbound requests → 勾选Allow requests to the local network from web hooks and servicesAllow requests to the local network from system hooks保存 → 在web_demo项目中 → Settings → Integrations → 可以粘贴URL(这里就是在Jenkins里面的http://192.168.200.129:8888/project/web_demo_pipeline) [把代码push到项目里来就会触发这个url自动构建] → Add webhook → 下方可以进行测试test → push event →↓

若出现 则证明gitlab是成功 只是Jenkins需要认证请求 去Jenkins开放接收请求的功能

Hook executed successfully but returned HTTP 403 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 403 anonymous is missing the Job/Build permission</title> </head> <body><h2>HTTP ERROR 403 anonymous is missing the Job/Build permission</h2> <table> <tr><th>URI:</th><td>/project/web_demo_pipeline</td></tr> <tr><th>STATUS:</th><td>403</td></tr> <tr><th>MESSAGE:</th><td>anonymous is missing the Job/Build permission</td></tr> <tr><th>SERVLET:</th><td>Stapler</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.19</a><hr/> </body> </html>

Jenkins开放接收请求 → 打开主页并跟随路径Dashboard → Manage Jenkins → System 找到 GitLab Enable authentication for ‘/project’ end-point【这个要反选 就是不能选钩 且里面的东西删除】 → 再去进行测试test → push event 即可成功Hook executed successfully: HTTP 200

Jenkins项目构建细节(3)-Jenkins的参数化构建

有时在项目构建的过程中,我们需要**根据用户的输入动态传入一些参数**,从而影响整个构建结果,这时 我们可以使用参数化构建。
Jenkins支持非常丰富的参数类型

大概意思就是 我在项目的jenkinsfile文件里面
steps {
checkout scmGit(
branches: [[name: ‘*/master’]],
extensions: [],
userRemoteConfigs: [[
credentialsId: ‘ca22e56f-0ecc-4fdc-965d-01e329a0b68a’,
url: ‘http://192.168.200.132:82/lanyun_group/web_demo.git'
]]
)
这个branches里面的 */master我要求是让用户动态输入值 而不是写死

首先要在项目里面增加一些参数 web_demo_pipeline 项目中 可以先把触发器的值取消掉 → 进入上面的General的配置 选择This project is parameterized → 可以添加参数 → 加入String Parameter参数 →
输入 名称:branch,默认值:master,描述:请输入一个分支的名称 → 此时在左侧就会出现一个 Build with Parameters →↓

pipeline {
agent any
stages {
stage(‘pull code’) {
steps {
checkout scmGit(
branches: [[name: ‘/${branch}}’]],
extensions: [],
userRemoteConfigs: [[
credentialsId: ‘ca22e56f-0ecc-4fdc-965d-01e329a0b68a’,
url: ‘http://192.168.200.132:82/lanyun_group/web_demo.git'
]]
)
}
}
stage(‘build project’) {
steps {
sh ‘mvn clean package’
}
}
stage(‘publish’) {
steps {
deploy adapters: [tomcat8(alternativeDeploymentContext: ‘’, credentialsId: ‘e847a130-29c3-4d0b-bf35-db8d00de7950’, path: ‘’, url: ‘http://192.168.200.131:8090/')], contextPath: null, war: ‘target/
.war’
}
}
}
}
【这时你在Jenkins构建时输入的参数就可以对应着Jenkinsfile文件的动态参数进行构建 (动态值红色已标注)】→ 需要把jenkinsfile的代码push到仓库里去

Jenkins项目构建细节(4)-配置邮箱服务器发送构建结果

安装Email Extension Template插件

Jenkins设置邮箱相关参数

Dashboard → Manage Jenkins → System → Extended E-mail Notification → QQ邮箱【官网 账号安全设置】打开SMTP服务 找到 POP3/SMTP服务,点击「开启」→ 勾选“开启服务”旁边的复选框 → 生成SMTP/IMAP 授权码已生成jitgujalhqtecadc → 回到Jenkins → SMTP server填写:smtp.qq.com;端口是465 → Default user e-mail suffix:@qq.com 下面的用户名是发件人 密码是授权码 使用SSL协议的话SMTP端口就是465 → Jenkins Location 系统管理员邮件地址是发件人地址

准备邮件内容

在项目根目录编写email.html,并把文件推送到Gitlab,内容如下:【根目录下】
BUILD_NUMBER、BUILD_STATUS等 来自于Jenkins的全局变量

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
</head>

<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
      offset="0">
<table width="95%" cellpadding="0" cellspacing="0"
       style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
    <tr>
        <td>(本邮件是程序自动下发的,请勿回复!)</td>
    </tr>
    <tr>
        <td><h2>
            <font color="#0000FF">构建结果 - ${BUILD_STATUS}</font>
        </h2></td>
    </tr>
    <tr>
        <td><br />
            <b><font color="#0B610B">构建信息</font></b>
            <hr size="2" width="100%" align="center" /></td>
    </tr>
    <tr>
        <td>
            <ul>
                <li>项目名称&nbsp;:&nbsp;${PROJECT_NAME}</li>
                <li>构建编号&nbsp;:&nbsp;第${BUILD_NUMBER}次构建</li>
                <li>触发原因:&nbsp;${CAUSE}</li>
                <li>构建日志:&nbsp;<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
                <li>构建&nbsp;&nbsp;Url&nbsp;:&nbsp;<a href="${BUILD_URL}">${BUILD_URL}</a></li>
                <li>工作目录&nbsp;:&nbsp;<a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>
                <li>项目&nbsp;&nbsp;Url&nbsp;:&nbsp;<a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
            </ul>
        </td>
    </tr>
    <tr>
        <td><b><font color="#0B610B">Changes Since Last
            Successful Build:</font></b>
            <hr size="2" width="100%" align="center" /></td>
    </tr>
    <tr>
        <td>
            <ul>
                <li>历史变更记录 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
            </ul> ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="<pre>[%a]<br />%m</pre>",pathFormat="&nbsp;&nbsp;&nbsp;&nbsp;%p"}
        </td>
    </tr>
    <tr>
        <td><b>Failed Test Results</b>
            <hr size="2" width="100%" align="center" /></td>
    </tr>
    <tr>
        <td><pre
                style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">$FAILED_TESTS</pre>
            <br /></td>
    </tr>
    <tr>
        <td><b><font color="#0B610B">构建日志 (最后 100行):</font></b>
            <hr size="2" width="100%" align="center" /></td>
    </tr>
    <tr>
        <td><textarea cols="80" rows="30" readonly="readonly"
                      style="font-family: Courier New">${BUILD_LOG, maxLines=100}</textarea>
        </td>
    </tr>
</table>
</body>
</html>

post的内容可以用流水线语法生成 → 完成之后点击片段生成器 生成一个 emailext: Extended Email 的代码片段

pipeline {
    agent any
    stages {
        stage('pull code') {
            steps {
                checkout scmGit(
                    branches: [[name: '*/${branch}']],
                    extensions: [],
                    userRemoteConfigs: [[
                        credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a',
                        url: 'http://192.168.200.132:82/lanyun_group/web_demo.git'
                    ]]
                )
            }
        }
        stage('build project') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('publish') {
            steps {
                deploy adapters: [tomcat8(alternativeDeploymentContext: '', credentialsId: 'e847a130-29c3-4d0b-bf35-db8d00de7950', path: '', url: 'http://192.168.200.131:8090/')], contextPath: null, war: 'target/*.war'
            }
        }
    }
    post{
        always{
             emailext(
                 subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
                 body: '${FILE,path="email.html"}',
                 to: '2523419709@qq.com'
             )
        }
    }
}

PS:邮件相关全局参数参考列表:
系统设置->Extended E-mail Notification->Content Token Reference,点击旁边的?号

Jenkins+SonarQube代码审查(1) - 安装SonarQube

SonaQube简介

SonarQube 是一个用于管理代码质量的开放平台,可以快速的定位代码中潜在的或者明显的错误。目前 支持java,C#,C/C++,Python,PL/SQL,Cobol,JavaScrip,Groovy等二十几种编程语言的代码质量管理与检 测。

官网: https://www.sonarqube.org/

环境要求
软件 服务器 版本
JDK 192.168.200.129 1.8
MySQL 192.168.200.129 5.7
SonarQube 192.168.200.129 6.7.4
安装SonarQube

1)安装MySQL(已完成)
2)安装SonarQube

使用5.7数据库
先删除以前的

建议操作:

  1. 先把多余或冲突的文件临时备份移动一下,避免yum混乱
bash复制编辑sudo mv /etc/yum.repos.d/mysql-community.repo.rpmsave /root/
sudo mv /etc/yum.repos.d/mysql57.repo /root/
  1. 只保留 mysql-community.repo 这个 repo 文件,确保它内容正确(你可以用cat看下内容)
cat /etc/yum.repos.d/mysql-community.repo
  1. 清理缓存,重新生成
sudo yum clean all
sudo yum makecache
  1. 尝试安装mysql 5.7
sudo yum install mysql-community-server

解决方案:用符合策略的密码先改,再降低策略

  1. 用一个符合复杂度的密码先改密码,例如:
sql


复制编辑
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Root@12345';
  1. 登录成功后降低密码策略:
sql复制编辑SET GLOBAL validate_password_policy=LOW;
SET GLOBAL validate_password_length=1;
  1. 再修改为你想要的简单密码(比如root):
sql


复制编辑
ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
  1. 刷新权限
FLUSH PRIVILEGES;

在mysql创建sonar数据库

启动 MySQL 服务(如果没启动)

systemctl start mysqld

启动后再次检查状态:

systemctl status mysqld

mysql -uroot -p
账号密码是:root

mysql> create database sonar;

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sonar              |
| sys                |
+--------------------+
5 rows in set (0.07 sec)
解压sonar,并设置权限

yum install unzip
unzip sonarqube-6.7.4.zip 解压
mkdir /opt/sonar 创建目录
mv sonarqube-6.7.4/* /opt/sonar 移动文件
useradd sonar 创建sonar用户,必须sonar用于启动,否则报错

chown -R sonar. /opt/sonar 更改sonar目录及文件权限

[root@localhost ~]# cd /opt/sonar
[root@localhost sonar]# ll
总用量 12
drwxr-xr-x. 8 sonar sonar 136 5月 30 2018 bin
drwxr-xr-x. 2 sonar sonar 50 5月 30 2018 conf
-rw-r–r–. 1 sonar sonar 7651 5月 30 2018 COPYING
drwxr-xr-x. 2 sonar sonar 24 5月 30 2018 data
drwxr-xr-x. 7 sonar sonar 150 5月 30 2018 elasticsearch
drwxr-xr-x. 4 sonar sonar 40 5月 30 2018 extensions
drwxr-xr-x. 9 sonar sonar 140 5月 30 2018 lib
drwxr-xr-x. 2 sonar sonar 6 5月 30 2018 logs
drwxr-xr-x. 2 sonar sonar 24 5月 30 2018 temp
drwxr-xr-x. 9 sonar sonar 4096 5月 30 2018 web

修改sonar配置文件数据库连接信息

[root@localhost opt]# cd sonar
[root@localhost sonar]# cd conf
[root@localhost conf]# vi sonar.properties

|| || || ||

......
sonar.jdbc.username=root
sonar.jdbc.password=root
......
sonar.jdbc.url=jdbc:mysql://127.0.0.1:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance&useSSL=false 【这里打开注释】

注意:sonar默认监听9000端口,如果9000端口被占用,需要更改 
如果要改端口 在这里:
sonar.web.port=9999

启动sonar 129

[root@localhost bin]# pwd
/opt/sonar/bin
cd /opt/sonar/bin

修改配置文件:vi /opt/sonar/conf/sonar.properties
重启sonarsudo -u sonar /opt/sonar/bin/linux-x86-64/sonar.sh restart ★★ [Jenkins服务]

实时跟踪最新日志输出(推荐用于调试启动或运行时状态)

tail -f /opt/sonar/logs/sonar.log
tail -f /opt/sonar/logs/web.log
tail -f /opt/sonar/logs/es.log

查看日志末尾100条内容(快速查看最近日志)

tail -n 100 /opt/sonar/logs/sonar.log
tail -n 100 /opt/sonar/logs/web.log
tail -n 100 /opt/sonar/logs/es.log

查看整个日志文件大小和权限

ls -lh /opt/sonar/logs/*.log

访问SonarQube

[192.168.200.129] (http://192.168.200.129:9999/)

账号密码:admin admin
生成了一个token:sonar: d30033c63973bd4183b6015995dbb513627c6f59

启动命令总结:
[192.168.200.132] GitLab:gitlab-ctl restart
lanyun_group / web_demo · GitLab
[192.168.200.131] Tomcat:/opt/tomcat/bin/startup.sh
演示项目主页

[192.168.200.129] Jenkins:systemctl start jenkins
登录 - Jenkins
[192.168.200.129] SonarQube:sudo -u sonar /opt/sonar/bin/linux-x86-64/sonar.sh restart
192.168.200.129

Jenkins+SonarQube代码审查(2) - 实现代码审查

审查流程:

Jenkins →(调用)→ Sonar-Scanner →(提交审查结果)→ SonarQube →(保存审查结果)→ MySQL数据库

安装SonarQube Scanner插件

打开Jenkins的全局配置 → SonarQube Servers → Add SonarQube → Name:sonarqube;Server URL:http://192.168.200.129:9999/ → 新建一个证书[Manage Jenkins → Credentials → System → 全局添加 → 类型选择Secret text ;Secret是之前复制到的token字符串 ;描述:sonarqube-auth] → Server authentication token选择刚刚创建的sonarqube-auth

SonarQube 关闭审查结果上传到SCM功能

[General Settings - Administration] (http://192.168.200.129:9999/admin/settings?category=scm) 上方的Administration → 左侧的SCM → 第一个打开SCN Sensor

回到Jenkins的web_demo_freestyle项目点击构建 下方的Build Steps下面的增加构建步骤 选择Execute SonarQube Scanner (这里要在去Tools里安装配置 SonarQube Scanner 安装 → Name:sonar-scanner;Install automatically:SonarQube Scanner 4.2.0.1873) → 这里的jdk是根据Jenkins的jdk配置的 → (Path to project properties这个可以在项目下搞 也可以用Analysis properties 属性放在前端ui界面里面) 这里选择Analysic properties→↓

# must be unique in a given SonarQube instance
sonar.projectKey=web_demo_freestyle
# this is the name and version displayed in the SonarQube UI. Was mandatory 
prior to SonarQube 6.1.
sonar.projectName=web_demo_freestyle
sonar.projectVersion=1.0
 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on 
Windows.
 # This property is optional if sonar.modules is set.
 sonar.sources=.
 sonar.exclusions=**/test/**,**/target/**
 sonar.java.source=1.8
 sonar.java.target=1.8
 # Encoding of the source code. Default is default system encoding
 sonar.sourceEncoding=UTF-8

→ 应用保存 + 重新构建

现在有个问题就是我Jenkins是2.504.2 然后jdk是17 我不想换jdk的情况下就要去

Build Steps里面的执行脚本

echo "开始进行编译构建"
mvn clean package
echo "编译构建结束"

# 设置环境变量,给sonar-scanner的JVM传参数,解决Java17模块访问问题
export SONAR_SCANNER_OPTS="--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens=java.base/java.io=ALL-UNNAMED"

# 执行Sonar扫描
/var/lib/jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar-scanner/bin/sonar-scanner \
  -Dsonar.projectKey=web_demo_freestyle \
  -Dsonar.host.url=http://192.168.200.129:9999 \
  -Dsonar.sources=. \
  -Dsonar.exclusions=**/test/**,**/target/** \
  -Dsonar.java.source=1.8 \
  -Dsonar.java.target=1.8

echo "SonarQube扫描结束"

打开idea jenkinsfile文件

pipeline {
 agent any
 stages {
     stage('pull code') {
         steps {
             checkout scmGit(
                 branches: [[name: "*/${branch}"]],
                 extensions: [],
                 userRemoteConfigs: [[
                     credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a',
                     url: 'http://192.168.200.132:82/lanyun_group/web_demo.git'
                 ]]
             )
         }
     }
     stage('build project') {
         steps {
             sh 'mvn clean package'
         }
     }
     stage('sonarqube scan') {
         environment {
             // 传递给sonar-scanner JVM的启动参数,解决模块访问限制
             SONAR_SCANNER_OPTS = '--add-opens=java.base/java.lang=ALL-UNNAMED ' +
                                  '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
                                  '--add-opens=java.base/java.io=ALL-UNNAMED'
         }
         steps {
             sh '''
             /var/lib/jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar-scanner/bin/sonar-scanner \
               -Dsonar.projectKey=web_demo_freestyle \
               -Dsonar.host.url=http://192.168.200.129:9999 \
               -Dsonar.sources=. \
               -Dsonar.exclusions=**/test/**,**/target/** \
               -Dsonar.java.source=1.8 \
               -Dsonar.java.target=1.8
             '''
         }
     }
     stage('publish') {
         steps {
             deploy adapters: [tomcat8(
                 alternativeDeploymentContext: '', 
                 credentialsId: 'e847a130-29c3-4d0b-bf35-db8d00de7950', 
                 path: '', 
                 url: 'http://192.168.200.131:8090/'
             )], contextPath: null, war: 'target/*.war'
         }
     }
 }
 post {
     always {
         emailext(
             subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
             body: '${FILE,path="email.html"}',
             to: '2523419709@qq.com'
         )
     }
 }
}

✅ 方法一:编辑 SonarScanner 的配置文件(最可靠)

编辑 Jenkins 中自动下载的 sonar-scanner 的 JVM 启动参数配置文件:

sudo vi /var/lib/jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar-scanner/conf/sonar-scanner.properties

在文件最后 添加

# 开启JDK17对反射的兼容
sonar.scanner.jvmArgs=--add-opens java.base/java.lang=ALL-UNNAMED

整体的文件是这样的:

#Configure here general information about the environment, such as SonarQube server connection details for example
#No information about specific project should appear here

#----- Default SonarQube server
sonar.host.url=http://localhost:9999

#----- Default source code encoding
#sonar.sourceEncoding=UTF-8
# 开启JDK17对反射的兼容
sonar.scanner.jvmArgs=--add-opens java.base/java.lang=ALL-UNNAMED

★如果要使用sonarqube 一定要jdk是1.8★

首先需要去全局那里设置jdk(Manage jenkins → System)

键:
JAVA_HOME
值:
/usr/local/java/jdk-17.0.8+7

键:
JAVA_HOME
值:
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64
在构建项目配置的时候Build StepsExecute SonarQube Scanner → jdk一定要选择1.8的!!!



流水线构建版本用的jdk17解决办法

要默认的1.8才支持 所以直接从根源上解决这个问题

你当前的系统默认 Java 是 JDK 1.8,路径在:

/usr/bin/java

你执行的:

java -version

输出是:

openjdk version "1.8.0_262"

说明现在 Jenkins 主机上默认使用的是 Java 8,这是符合 SonarQube 6.7.4 的推荐版本,很好!


✅ 接下来你要做的

为了确保 Jenkins 中的 SonarScanner 也用 Java 8 运行,你还需要修改如下:


✅ 方法一:修改 sonar-scanner 启动脚本(推荐)

vi /var/lib/jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar-scanner/bin/sonar-scanner

在顶部添加下面两行 👇:

export JAVA_HOME=/usr
export PATH=$JAVA_HOME/bin:$PATH

模拟SonaQube代码审查机制

C:\Users\Pluminary\Desktop\web_demo\src\main\java\com\itheima\HelloServlet.java

package com.itheima;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 *
 */
public class HelloServlet extends HttpServlet {


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //模拟代码错误
        int i = 100/0;

        //模拟冗余代码
        int a = 100;
        a = 200;


        resp.getWriter().write("hello Servlet!");
    }
}

在项目添加SonaQube代码审查(流水线项目)不在Jenkins里面写配置,在项目的文件里面写!

sonar-project.properties
    
# must be unique in a given SonarQube instance
sonar.projectKey=web_demo_pipline
# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
sonar.projectName=web_demo_pipline
sonar.projectVersion=1.0

# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
# This property is optional if sonar.modules is set.
sonar.sources=.
sonar.exclusions=**/test/**,**/target/**

sonar.java.source=1.8
sonar.java.target=1.8

# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8
pipeline {
    agent any

    tools {
        // 使用 Jenkins 配置好的 JDK 1.8(别名需和你Jenkins中配置一致)
        jdk 'jdk-1.8.0'
    }

    environment {
        // Sonar Scanner JVM 启动参数,开启必要的module访问权限
        SONAR_SCANNER_OPTS = '--add-opens=java.base/java.lang=ALL-UNNAMED ' +
                             '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
                             '--add-opens=java.base/java.io=ALL-UNNAMED'
    }

    stages {
        stage('Pull Code') {
            steps {
                checkout scmGit(
                    branches: [[name: "*/${branch}"]],
                    extensions: [],
                    userRemoteConfigs: [[
                        credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a',
                        url: 'http://192.168.200.132:82/lanyun_group/web_demo.git'
                    ]]
                )
            }
        }

        stage('Build Project') {
            steps {
                sh 'mvn clean package'
            }
        }

        stage('SonarQube Scan') {
            steps {
                // 强制切换到 JDK8 环境,避免默认JDK17导致权限异常
                withEnv([
                    'JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64',
                    'PATH=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64/bin:' + env.PATH
                ]) {
                    sh '''
                    /var/lib/jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar-scanner/bin/sonar-scanner \
                      -Dsonar.projectKey=web_demo_freestyle \
                      -Dsonar.host.url=http://192.168.200.129:9999 \
                      -Dsonar.sources=. \
                      -Dsonar.exclusions=**/test/**,**/target/** \
                      -Dsonar.java.source=1.8 \
                      -Dsonar.java.target=1.8
                    '''
                }
            }
        }

        stage('Publish') {
            steps {
                deploy adapters: [tomcat8(
                    alternativeDeploymentContext: '',
                    credentialsId: 'e847a130-29c3-4d0b-bf35-db8d00de7950',
                    path: '',
                    url: 'http://192.168.200.131:8090/'
                )], contextPath: null, war: 'target/*.war'
            }
        }
    }

    post {
        always {
            emailext(
                subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
                body: '${FILE,path="email.html"}',
                to: '2523419709@qq.com'
            )
        }
    }
}


Jenkins+Docker+SpringCloud微服务持续集成(上)

Jenkins+Docker+SpringCloud持续集成流程说明

大致流程说明:
1)开发人员每天把代码提交到Gitlab代码仓库
2)Jenkins从Gitlab中拉取项目源码,编译并打成jar包,然后构建成Docker镜像,将镜像上传到 Harbor私有仓库。
3)Jenkins发送SSH远程命令,让生产部署服务器到Harbor私有仓库拉取镜像到本地,然后创建容器。
4)最后,用户可以访问到容器

服务列表(红色的软件为需要安装的软件,黑色代表已经安装)
服务器名称 IP地址 安装的软件
代码托管服务器 192.168.200.132 Gitlab
持续集成服务器 192.168.200.129 Jenkins,Maven,Docker18.06.1-ce
Docker仓库服务器 Docker18.06.1-ce,Harbor1.9.2
生产部署服务器 Docker18.06.1-ce

SpringCloud微服务源码概述

项目架构:前后端分离
后端技术栈:SpringBoot+SpringCloud+SpringDataJpa(Spring全家桶)
位置:C:\Users\Pluminary\Desktop\HouDuan\tensquare_parent

微服务项目结构:
  • tensquare_parent :父工程,存放基础配置
  • tensquare_common :通用工程,存放工具类
  • tensquare_eureka_server:SpringCloud的Eureka注册中心
  • tensquare_zuul :SpringCloud的网关服务
  • tensquare_admin_service :基础权限认证中心,负责用户认证(使用JWT认证)
  • tensquare_gathering : 一个简单的业务模块,活动微服务相关逻辑
数据库结构:
  • tensquare_user :用户认证数据库,存放用户账户数据。对应tensquare_admin_service微服务
  • tensquare_gathering :活动微服务数据库。对应tensquare_gathering微服务
微服务配置分析:
  • tensquare_eureka

  • tensquare_zuul

  • tensquare_admin_service

  • tensquare_gathering

  • 查看效果

application.yaml

spring:
  application:
    name: EUREKA-HA

---

spring:
  profiles: eureka-server1
server:
  port: 10086
eureka:
  instance:
    hostname: localhost
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka/,http://localhost:10087/eureka/

---

spring:
  profiles: eureka-server2
server:
  port: 10087
eureka:
  instance:
    hostname: localhost
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka/,http://localhost:10087/eureka/

依次开启这些服务:tensquare_eureka_server → tensquare_zuul → tensquare_admin_service → tensquare_gathering

tensquare_eureka_server 要在配置的地方配置两个【复制出多一个】配置信息添加Program arguments

EurekaServerApplication-Server1:-spring.profiles.active=eureka-server1
EurekaServerApplication-Server2:–spring.profiles.active=eureka-server2

调用postman
测试gathering接口的时候一定要在请求头带token
测试获得token接口的时候一定是post请求 请求体是json形式
GET:http://localhost:10020/gathering/gathering

Headers:这里的token是去post请求获得
POST:http://localhost:10020/admin/admin/login

json:{
  "loginname": "admin",
  "password": "123456"
}

本地部署(1)-SpringCloud微服务部署

本地运行微服务
  • 逐一启动微服务
  • 使用postman测试功能是否可用
本地部署微服务
  • SpringBoot微服务项目打包

打开idea终端
PS C:\Users\Pluminary\Desktop\HouDuan\tensquare_parent>
托选想要打包的项目到终端
PS C:\Users\Pluminary\Desktop\HouDuan\tensquare_parent\tensquare_eureka_server> mvn clean package
此时就打好了jar包
C:\Users\Pluminary\Desktop\HouDuan\tensquare_parent\tensquare_eureka_server\target的tensquare_eureka_server-1.0-SNAPSHOT.jar

必须导入该插件

在根pom.xml中
   <build>
        <plugins>
            <plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

★ 这个就可以对springcloud进行打包 微服务的jar可以独立部署 扔到任何一个服务就可以单独运行 而不需要借助单独的tomcat ★

打包后在 target下产生jar包

  • 本地运行微服务的jar包

java -jar xxx.jar

C:\Users\Pluminary\Desktop>java -jar tensquare_eureka_server-1.0-SNAPSHOT.jar


2025-07-01 15:33:09.363  INFO 27172 --- [           main] o.s.core.annotation.AnnotationUtils      : Failed to introspect annotations on class org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration: java.lang.IllegalStateException: Could not obtain annotation attribute value for public abstract java.lang.Class[] org.springframework.boot.autoconfigure.condition.ConditionalOnClass.value()
2025-07-01 15:33:09.373  INFO 27172 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@8f4ea7c: startup date [Tue Jul 01 15:33:09 CST 2025]; root of context hierarchy
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.springframework.cglib.core.ReflectUtils$1 (jar:file:/C:/Users/Pluminary/Desktop/tensquare_eureka_server-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-core-5.0.5.RELEASE.jar!/) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of org.springframework.cglib.core.ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
2025-07-01 15:33:09.531  INFO 27172 --- [           main] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2025-07-01 15:33:09.554  INFO 27172 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'configurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$83532275] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.1.RELEASE)

本地部署(2)-前端静态web网站

前端技术栈:NodeJS+VueJS+ElementUI
使用Visual Studio Code打开源码
位置:C:\Users\Pluminary\Desktop\QianDuan\tensquareAdmin

因为老版本无法继续用最新的nodejs
需要用nvm进行统一的nodejs管理 如果无法直接nvm下载的话
我们可以去官网下载安装包zip后解压进去 改名就行

✅ 1. 下载 Node.js v10.24.1 64位 zip 版本

你可以从官方链接下载:

📦 下载地址(Windows 64位):

bash


复制编辑
https://nodejs.org/dist/v10.24.1/node-v10.24.1-win-x64.zip

下载后,解压得到一个文件夹(比如叫 node-v10.24.1-win-x64


✅ 2. 放入 NVM 的版本目录

将你解压得到的 node-v10.24.1-win-x64 文件夹重命名为:

10.24.1

然后放入这个目录下:

C:\Program Files\nvm\versions\10.24.1\

C:\Users\Pluminary\Desktop\QianDuan\tensquareAdmin>nvm list

  • 20.12.0 (Currently using 64-bit executable)
    10.24.1

C:\Users\Pluminary\Desktop\QianDuan\tensquareAdmin>nvm use 10.24.1
Now using node v10.24.1 (64-bit)



🚀 步骤 1:重新下载对应 binding.node

在项目目录下运行:

bash


复制编辑
npm rebuild node-sass

I Your application is running here: http://localhost:9528 前端即可启动!
name: admin password: 123456
[十次方社交平台-管理员后台] (http://localhost:9528/#/gathering/index)

环境准备(1)-Docker快速入门

Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流 行的 Linux 机器上,也可以实现虚拟化。 容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销 极低。 Docker容器技术 vs 传统虚拟机技术

虚拟机 容器
占用磁盘空间 非常大,GB级 小,MB甚至KB级
启动速度 慢。分钟级 快,秒级
运行状态 运行于Hypervisor上 直接运行在宿主机内核上
并发性 一台宿主机上十几个,最多几十个 上百个,甚至数百上千个
性能 逊于宿主机 接近宿主机本地进程
资源利用率

简单一句话总结:Docker技术就是让我们更加高效轻松地将任何应用在Linux服务器部署和使用

Docker安装

1)卸载旧版本

yum list installed | grep docker 列出当前所有docker的包
yum -y remove docker的包名称 卸载docker包
rm -rf /var/lib/docker 删除docker的所有镜像和容器

2)安装必要的软件包

sudo yum install -y yum-utils \ device-mapper-persistent-data \ lvm2

3)设置下载的镜像仓库

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

4)列出需要安装的版本列表

yum list docker-ce --showduplicates | sort -r

docker-ce.x86_64 3:18.09.1-3.el7
docker-ce.x86_64 3:18.09.0-3.el7
docker-ce.x86_64 18.06.1.ce-3.el7
docker-ce.x86_64 18.06.0.ce-3.el7
……

5 )安装指定版本(这里使用18.0.1版本)

sudo yum install docker-ce-18.06.1.ce

6)查看版本

docker -v

7)启动Docker

sudo systemctl start docker   启动
sudo systemctl enable docker  设置开机启动

8)添加阿里云镜像下载地址

sudvi /etc/docker/daemon.json

{
“registry-mirrors”: [“https://zydiol88.mirror.aliyuncs.com"]
}

9 )重启Docker

sudo systemctl restart docker

第四步看这里:你现在的问题还是和之前一样:

无法访问 Docker 官方源(https://download.docker.com/...),报错 curl#35 - TCP connection reset by peer。


✅ 解决方法:使用国内镜像(如清华、阿里)

你需要 手动切换 Docker yum 源为国内镜像,否则无法继续安装。下面是最直接可行的解决方法。


🚀 一键换成清华源(CentOS 7 示例)

请复制以下命令到你的服务器执行:

bash复制编辑sudo tee /etc/yum.repos.d/docker-ce.repo <<-'EOF'
[docker-ce-stable]
name=Docker CE Stable - Tsinghua Mirror
baseurl=https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/7/x86_64/stable
enabled=1
gpgcheck=1
gpgkey=https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/gpg
EOF

🔁 然后执行:

sudo yum clean all
sudo yum makecache fast

✅ 安装 Docker 指定版本(比如 18.06)

sudo yum install docker-ce-18.06.1.ce docker-ce-cli-18.06.1.ce containerd.io

或者查看所有可用版本:

yum list docker-ce --showduplicates

🟢 启动并设置开机启动:

sudo systemctl start docker
sudo systemctl enable docker


Docker基本命令快速入门

1)镜像命令

[docker配置镜像Docker pull时报错:https://registry-1.docker.io/v2/ - 知乎] (https://zhuanlan.zhihu.com/p/24228872523)

这个错误表明Docker客户端尝试访问Docker Hub或其他Docker注册中心时出现了问题。具体来说,是在尝试获取注册中心API的响应时遇到了错误。可能的原因包括网络问题、认证问题、注册中心URL不正确或者注册中心服务本身不可用。

2、解决方法

systemctl status docker
sudo mkdir -p /etc/docker
vim /etc/docker/daemon.json  

添加:

{
  "registry-mirrors" : ["https://docker.registry.cyou",
"https://docker-cf.registry.cyou",
"https://dockercf.jsdelivr.fyi",
"https://docker.jsdelivr.fyi",
"https://dockertest.jsdelivr.fyi",
"https://mirror.aliyuncs.com",
"https://dockerproxy.com",
"https://mirror.baidubce.com",
"https://docker.m.daocloud.io",
"https://docker.nju.edu.cn",
"https://docker.mirrors.sjtug.sjtu.edu.cn",
"https://docker.mirrors.ustc.edu.cn",
"https://mirror.iscas.ac.cn",
"https://docker.rainbond.cc",
"https://do.nark.eu.org",
"https://dc.j8.work",
"https://dockerproxy.com",
"https://gst6rzl9.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"http://mirrors.ustc.edu.cn/",
"https://mirrors.tuna.tsinghua.edu.cn/",
"http://mirrors.sohu.com/" 
],
 "insecure-registries" : [
    "registry.docker-cn.com",
    "docker.mirrors.ustc.edu.cn"
    ],
"debug": true,
"experimental": false
}

重载和重启dockers服务

sudo systemctl daemon-reload
sudo systemctl restart docker
docker info

重新执行

docker run hello-world

镜像:相当于应用的安装包,在Docker部署的任何应用都需要先构建成为镜像

docker search 镜像名称 搜索镜像
docker pull 镜像名称 拉取镜像
docker images 查看本地所有镜像
docker rmi -f 镜像名称 删除镜像

docker pull openjdk:8-jdk-alpine

[root@localhost ~]# docker pull openjdk:8-jdk-alpine
8-jdk-alpine: Pulling from library/openjdk
e7c96db7181b: Pull complete
f910a506b6cb: Pull complete
c2274a1a0e27: Pull complete
Digest: sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3
Status: Downloaded newer image for openjdk:8-jdk-alpine
[root@localhost ~]#

2)容器命令

容器:容器是由镜像创建而来。容器是Docker运行应用的载体,每个应用都分别运行在Docker的每个 容器中。

docker run -i 镜像名称:标签 运行容器(默认是前台运行)
docker ps         查看运行的容器
docker ps -a      查询所有容器
常用的参数:
-i:运行容器
-d:后台守方式运行(守护式)
--name:给容器添加名称
-p:公开容器端口给当前宿主机
-v:挂载目录

docker exec -it 容器ID/容器名称 /bin/bash 进入容器内部
docker start/stop/restart 容器名称/ID 启动/停止/重启容器
docker rm -f 容器名称/ID 删除容器

docker run -i nginx 运行容器
docker run -di nginx 后台运行容器

运行的时候创建端口 把端口暴露给宿主机 这样宿主机就可以进行ip地址+端口的访问虚拟机的容器了

-p公开端口 外部用90端口访问80端口
docker run -di -p 90:80 nginx

[root@localhost ~]# docker run -di -p 90:80 nginx
04726503f6ee2d053b54b6361d128827d5fb26b867c8fbd5439a370a4ac137d9
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
04726503f6ee nginx “/docker-entrypoint.…” 29 seconds ago Up 27 seconds 0.0.0.0:90->80/tcp relaxed_goldberg

此时用宿主机去访问:[Welcome to nginx!] (http://192.168.200.129:90/)

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.


进入某个容器内部查看其内容:

[root@localhost ~]# docker exec -it 04726503f6ee /bin/bash
root@04726503f6ee:/# ls
bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@04726503f6ee:/#

删除容器

[root@localhost ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
04726503f6ee nginx “/docker-entrypoint.…” 4 minutes ago Up 4 minutes 0.0.0.0:90->80/tcp relaxed_goldberg
9b9b1ec3e7a1 nginx “/docker-entrypoint.…” 10 minutes ago Up 8 minutes 80/tcp gracious_knuth
4fb9d9e6858a hello-world “/hello” 29 minutes ago Exited (0) 29 minutes ago elated_bhaskara
[root@localhost ~]# docker rm -f 4fb9d9e6858a
4fb9d9e6858a

虚拟机变化ip 导致无法ping通 解决办法

cd /etc/sysconfig/network-scripts/

vi ifcfg-ens33

❗核心问题总结:

你的宿主机(Windows)的真实联网网卡是:

  • 无线网卡 WLAN
    • IP:192.168.31.103
    • 网关:192.168.31.1

而你的 VMware 虚拟机设置的是:

  • 静态 IP:192.168.200.132
  • 网关:192.168.200.1
  • 网络模式:你设为桥接模式

❗问题本质:

你的虚拟机静态 IP 设置在 192.168.200.x 网段,但 宿主机是在 192.168.31.x 网段,说明你的桥接实际上根本没有生效,或者桥接到了错误的物理网卡


✅ 最快修复方案(推荐,100% 可行):

🛠️ 步骤一:修改 VMware 桥接设置

  1. 关闭虚拟机
  2. 打开 VMware 主界面 → 选择你的虚拟机 → 设置(Settings)→ 网络适配器
  3. 选择:
    • ✔️ 网络连接 → 桥接模式(Bridged)
    • 点开旁边的 “桥接设置(Bridge Settings)
  4. 手动选择你的 Wi-Fi 无线网卡(例如 Intel Wireless-AC 或 Realtek Wireless)
    • 不要选“自动”
  5. 取消勾选“复制物理网络连接状态”这一项
  6. 保存设置

🛠️ 步骤二:修改虚拟机内 IP 设置

由于你真实网络是 192.168.31.x,所以虚拟机也应该设置为这个网段,例如:

编辑 vi /etc/sysconfig/network-scripts/ifcfg-ens33

BOOTPROTO=static
NAME=ens33
DEVICE=ens33
ONBOOT=yes

IPADDR=192.168.31.150       # ← 随便取一个没被占用的
NETMASK=255.255.255.0
GATEWAY=192.168.31.1       # 这个是在宿主机网络下的环境网关 ipconfig
DNS1=8.8.8.8

然后执行:

sudo systemctl restart network

或者:

sudo ifdown ens33 && sudo ifup ens33

✅ 步骤三:在 Windows 上 ping 新的虚拟机 IP

ping 192.168.31.150

🧠 如果你担心 IP 冲突怎么办?

在 Windows 上运行:

ping 192.168.31.150

如果 ping 不通,就说明这个 IP 是空闲的,可以用。


✅ 替代方案(保留 NAT 模式)

如果你不想改桥接模式,可以继续用 VMware NAT 模式,那就让虚拟机保留原来的:

  • IP:192.168.200.132
  • 网关:192.168.200.1
  • 然后通过端口转发(NAT 端口映射)来从主机访问虚拟机(需要手动配置)

桥接 + 静态 IP 更简单、稳定、兼容性好


✅ 最终目标

你的宿主机和虚拟机 在同一个网段(如都在 192.168.31.x,并且虚拟机网卡状态为 UP,那 ping 肯定通!


🔚 结论

问题 解决方法
虚拟机 IP 设置错网段了 改为 192.168.31.x(和主机一样的网段)
桥接失败 手动指定桥接物理网卡为你的 无线网卡,不能选自动
不能 ping 通 网卡 DOWN → 启动;IP 不通 → 改网段;防火墙 → 暂停

如果ip a show ens33没有ip地址怎么办 这样会导致moba连不上
应该是网卡状态为down说明网卡没启用
直接用ip命令把网卡打开sudo ip link set ens33 up
然后找一下有没有 ip a show ens33
重启网络服务:sudo systemctl restart network 再次查看
如果还是没有 就手动设置IP
sudo ip addr add 192.168.200.131/24 dev ens33
再次重启网络服务:sudo systemctl restart network
然后再次查看 ip a show ens33

我想了解一下 虚拟机的 net直连 桥接 仅主机模式 自定义虚拟网络 这些有什么区别啊

模式名称 虚拟机是否能访问外网 能否与主机通信 能否被局域网其它设备访问 场景用途
NAT(网络地址转换) ✅ 可以 ✅ 可以 ❌ 不能 默认模式,适合访问互联网但不被访问
桥接模式(Bridged) ✅ 可以 ✅ 可以 ✅ 可以 适合开发、调试服务端程序
仅主机模式(Host-Only) ❌ 不可以 ✅ 可以 ❌ 不能 适合测试虚拟机与主机通信,隔离网络
自定义虚拟网络(VMnetX) 看配置情况 看配置情况 看配置情况 高级用途,如多机组网、模拟拓扑

✅ 判断网络模式

你需要检查虚拟机使用的网络连接方式,有几种常见模式:

网络模式 是否能上网 是否能被主机访问 说明
NAT 默认可以上网,不能被主机访问
桥接模式 与主机同网段,能上网、能互通
Host-only 不能上网,只能主机访问
内部网络(仅限虚拟机) 用于多虚拟机内部通信

1️⃣ NAT 模式(默认)

虚拟机通过 VMware NAT 服务共享主机网络访问外网

特点:

  • 虚拟机 IP 是 192.168.200.x 之类的 VMware 虚拟网段
  • 宿主机充当“路由器”角色
  • 适合需要上网,但不想暴露虚拟机到外部网络

优点:

  • 无需特殊配置即可访问外网
  • 安全,外部无法直接访问虚拟机

缺点:

  • 外部机器(包括宿主机)无法直接 ping 通虚拟机
  • 虚拟机 IP 不稳定

2️⃣ 桥接模式(Bridged)

虚拟机像真实电脑一样连接到局域网(通过宿主机的物理网卡桥接出去)

特点:

  • 虚拟机 IP 和宿主机一样,都由路由器分配(比如 192.168.31.x
  • 可以直接与宿主机、局域网其他设备通信

优点:

  • 和真实电脑一样,可以被 ping 通、远程访问、部署服务等
  • 最贴近真实网络环境,适合服务器开发

缺点:

  • 依赖物理网络(尤其 Wi-Fi 桥接时可能失败)
  • 公司/学校网络可能屏蔽桥接

3️⃣ 仅主机模式(Host-Only)

虚拟机只能和宿主机通信,完全与外网隔离

特点:

  • 虚拟机 IP 通常是 192.168.56.x192.168.152.x
  • 不能访问互联网

优点:

  • 安全,完全离线隔离
  • 虚拟机和主机之间能稳定通信(做测试非常好)

缺点:

  • 无法访问外网
  • 无法被外部设备访问

4️⃣ 自定义虚拟网络(VMnet0~19)

高级用户使用,可自由配置网络结构

典型用途:

  • 多台虚拟机组成专属局域网
  • 配置 DHCP、NAT、Host-Only 混合网络
  • 构建模拟数据中心、私有云、堡垒机环境等

🎯 总结建议(适合不同场景):

场景 建议使用模式
日常使用、能上网即可 NAT 模式(默认)
开发服务端、需要固定 IP + 被访问 桥接模式 ✅
只在主机和虚拟机之间通信 Host-Only 模式
多虚拟机模拟复杂网络 自定义(VMnet)

📌 一句话总结:

NAT 是“内网访问外网”,桥接是“虚拟机等于真电脑”,Host-Only 是“我只和主机说话”,自定义是“你说了算”。

环境准备(2)—Dockerfile镜像脚本快速入门

Dockerfile其实就是我们用来构建Docker镜像的源码,当然这不是所谓的编程源码,而是一些命令的组 合,只要理解它的逻辑和语法格式,就可以编写Dockerfile了。 简单点说,Dockerfile的作用:它可以让用户个性化定制Docker镜像。因为工作环境中的需求各式各 样,网络上的镜像很难满足实际的需求。

命令 作用
FROM image_name:tag
MAINTAINER user_name 声明镜像的作者
ENV key value 设置环境变量(可以写多条)
RUN command 编译镜像时运行的脚本(可以写多条)
CMD 设置容器的启动命令
ENTRYPOINT 设置容器的入口程序
ADD source_dir/file dest_dir/file 将宿主机的文件复制到容器内,如果是一个压缩文件。将会在复制后自动解压
COPY source_dir/file dest_dir/file 和ADD相似,但是如果又压缩文件并不能解压
WORKDIR path_dir 设置工作目录
ARG 设置编译镜像时加入的参数
VOLUMN 设置容器的挂载卷

新镜像是从基础镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一 层

使用Dockerfile制作微服务镜像

我们利用Dockerfile制作一个Eureka注册中心的镜像

1)上传Eureka的微服务jar包到linux这里的jar包要和Dockerfile文件在同一目录下
2)编写Dockerfile vi Dockerfile

作用:📦 构建一个用于运行 Java 应用(Spring Boot 或其他 jar 应用)的 Docker 镜像

这个 Dockerfile 是在用 Java 8 的轻量镜像里,运行你打包好的 jar 应用,并对外开放 10086 端口,非常适合部署 Spring Boot 项目。

FROM openjdk:8-jdk-alpine
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
EXPOSE 10086
ENTRYPOINT ["java","-Djava.net.preferIPv4Stack=true","-jar","/app.jar"]

3 )构建镜像

docker build --build-arg JAR_FILE=tensquare_eureka_server-1.0-SNAPSHOT.jar -t eureka:v1 .
-t是定义名字 .是在当前目录
4)查看镜像是否创建成功

docker images

[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
eureka v1 36af14603456 15 seconds ago 150MB
nginx latest 9592f5595f2b 7 days ago 192MB
hello-world latest 74cc54e27dc4 5 months ago 10.1kB
openjdk 8-jdk-alpine a3562aa0b991 6 years ago 105MB

5)创建容器

docker run -i --name=eureka -p 10086:10086 eureka:v1
后台运行docker run -d --name=eureka -p 10086:10086 eureka:v1

删除容器:docker rm -f eureka
查看容器是否启动docker ps
查看日志docker logs -f eureka
后面可以直接启动这个容器 docker start eureka

6)访问容器 http://192.168.200.129:10086

修改后重新构建镜像:

docker stop eureka
docker rm eureka

# 重新构建镜像(确保 Dockerfile 和配置文件都在当前目录)
docker build --build-arg JAR_FILE=tensquare_eureka_server-1.0-SNAPSHOT.jar -t eureka:v1 .

# 启动容器并映射端口
docker run -d --name=eureka -p 10086:10086 eureka:v1

进入容器(你已经会了):

docker exec -it eureka /bin/sh
  1. **容器内查找 application.yaml**:
find / -name application.yaml 2>/dev/null
  • 2>/dev/null 是把权限不够或找不到文件时的报错信息过滤掉,方便看结果。
  1. 找到路径后,用 cat 查看内容

假设查到文件在 /config/application.yaml,那么:

cat /config/application.yaml

你要删除 <none> 的镜像,也就是“悬空镜像(dangling image)”,可以使用以下命令:

🔍 查看所有悬空镜像

docker images -f "dangling=true"

🗑 删除所有悬空镜像

docker rmi $(docker images -f "dangling=true" -q)

root@localhost ~]# docker run -d –name=eureka -p 10086:10086 eureka:v1 docker: Error response from daemon: Conflict. The container name “/eureka” is already in use by container “9ea494448aeb1d99faecfe11e652f438938b87e1caaca890f9001199e1166dbc”. You have to remove (or rename) that container to be able to reuse that name. See ‘docker run –help’. 这个错误说明:虽然你已经 docker stop eureka 停止了容器,但它还没有被删除,所以容器名 /eureka 依然占用中,不能再用这个名字启动新的容器。

✅ 解决方案:删除旧容器

你需要先 删除已停止的 eureka 容器,再重新启动:

docker rm eureka

然后再重新运行新的容器:

docker run -d --name=eureka -p 10086:10086 eureka:v1

是桥接 能ping通 防火墙永久关闭了 本机打开了clash系统代理+全局

ChatGPT 说:

非常关键的点来了!

你说:

✅ 虚拟机是桥接模式
✅ 能 ping 通虚拟机 192.168.200.129
✅ 防火墙是永久关闭状态
本机打开了 Clash 并启用了系统代理 + 全局模式


✅ 你的 Eureka 服务配置和容器启动都是正确的,问题现在就出在:你本机开启了 Clash 的全局代理。

❗这会导致浏览器请求走代理,无法访问内网地址(例如 192.168.200.129:10086)。


删除镜像:docker rmi eureka:v1

删除所有悬挂镜像:< none >的 docker images | grep "<none>" | awk '{print $3}' | xargs -r docker rmi -f



ss -tlnp | grep 10086✅ 你看到的是:

LISTEN     0      1024        :::10086                   :::*                   users:(("docker-proxy",pid=31831,fd=4))

📌 说明:

这是 宿主机上的 docker-proxy 在监听 10086 端口,它只是端口映射的外壳进程

但我们没有看到你容器内部的 Spring Boot 应用是否在监听端口,也就是说:

docker-proxy 映射是 OK 的
❗️容器里的 Spring Boot 可能绑定了 127.0.0.1:10086,不是 0.0.0.0:10086,导致 proxy 转发过去失败


✅ 解决方案 —— 修改配置,显式绑定 0.0.0.0

请 **修改你的 application.yml**,在 server 配置中添加这一行:

yaml复制编辑server:
  port: 10086
  address: 0.0.0.0  # <-- 显式监听所有网卡地址


[root@localhost ~]# netstat -tln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:9100 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9229 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9168 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8082 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:82 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9236 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:8060 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9121 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9090 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9187 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9093 0.0.0.0:* LISTEN
tcp6 0 0 :::3306 :::* LISTEN
tcp6 0 0 ::1:9168 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
tcp6 0 0 :::10086 :::* LISTEN
tcp6 0 0 :::9094 :::* LISTEN

你贴的 netstat -tln 结果说明:

  • 你的服务监听在 :::10086(IPv6的所有地址),没有监听任何IPv4地址(比如0.0.0.0:10086
  • 这意味着你的Spring Boot应用只监听了IPv6地址,IPv4访问就无法连接(所以你用 http://192.168.200.129:10086 访问会失败)

具体做法:

  1. 修改你的 DockerfileENTRYPOINT
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
EXPOSE 10086
ENTRYPOINT ["java","-Djava.net.preferIPv4Stack=true","-jar","/app.jar"]

2. 构建新镜像(建议打新标签)

docker build --build-arg JAR_FILE=tensquare_eureka_server-1.0-SNAPSHOT.jar -t eureka:v2 .

3. 停止并删除旧容器(如果还在运行)

docker stop eureka
docker rm eureka

4. 运行新镜像容器

docker run -d -p 10086:10086 --name eureka eureka:v2

6. 测试访问

浏览器打开:

http://192.168.200.129:10086



现在可以访问成功了 为什么加上这句话就好了 ENTRYPOINT [“java”,”-Djava.net.preferIPv4Stack=true”,”-jar”,”/app.jar”] 为什么监听的时候还是ipv6 [root@localhost ~]# ss -tlnp | grep 10086 LISTEN 0 1024 :::10086 :::* users:((“docker-proxy”,pid=35209,fd=4)) 但是成功在主机访问地址了

为什么加上 -Djava.net.preferIPv4Stack=true 就好了?
  • 这个参数告诉 JVM 优先使用 IPv4 网络栈,避免只监听 IPv6 导致外部 IPv4 地址访问不到的问题。
  • Spring Boot 的内嵌 Tomcat 在默认情况下,Linux 可能优先监听 IPv6 地址(:::port)。
  • 加了这个参数后,Tomcat 会优先绑定 IPv4 地址,从而确保 Docker 的端口映射能正常被外部 IPv4 网络访问。

为什么监听时还是显示 IPv6 (:::10086)?

  • Linux 上 IPv6 的监听套接字 ::: 默认是包含 IPv4 地址 0.0.0.0 的(叫做 IPv4-mapped IPv6 地址)。
  • 也就是说,监听 :::10086 实际上同时监听了所有 IPv6 和 IPv4 地址。
  • 这是一种操作系统层面的机制,不是 Java 层面的问题。

总结

  • -Djava.net.preferIPv4Stack=true 让 JVM 侧优先用 IPv4,避免了纯 IPv6 监听带来的访问问题。
  • ss 看到的 :::10086 其实也包括了 IPv4 地址,所以你才能用 IPv4 地址成功访问。
  • 访问成功说明网络和端口映射都正常工作了。


太好了,你现在已经非常接近成功了,我们来梳理一下现状和如何解决你“既能联网又能用 Mobaxterm 连上虚拟机”的问题。


✅ 当前状态分析

你目前有两张网卡:

网卡名 IP 地址 状态 说明
ens33 192.168.200.131 ✅ UP,有 IP 是你原来的桥接或 Host-Only 网卡,用于主机连虚拟机
ens36 无 IP ❌ DOWN 是你新加的 NAT 网卡,但 没有启动 / 没有获取 IP

🧩 为什么还无法联网?

你 curl 下载失败,是因为 NAT 网卡 ens36 没启动,虚拟机依然没联网。


✅ 解决方案:启动 NAT 网卡并设置为默认出网口

第一步:激活 ens36(NAT 网卡)

执行:

dhclient ens36

⏳ 如果你没装 dhclient,可以试试:

nmcli device connect ens36

然后再次查看 IP:

ip addr

你应该能看到 ens36 分配到类似 192.168.226.x 的地址(NAT 网段)

总结一下视频里的服务器ip所对应的服务

192.168.66.100 GitLab
192.168.66.101 Jenkins
192.168.66.102 Harbor && Tomcat
192.168.66.103 生产部署服务器

环境准备(3)-Harbor镜像仓库安装及使用

Harbor简介

Harbor(港口,港湾)是一个用于存储和分发Docker镜像的企业级Registry服务器。 除了Harbor这个私有镜像仓库之外,还有Docker官方提供的Registry。相对Registry,Harbor具有很多优势:

  1. 提供分层传输机制,优化网络传输 Docker镜像是是分层的,而如果每次传输都使用全量文件(所以 用FTP的方式并不适合),显然不经济。必须提供识别分层传输的机制,以层的UUID为标识,确定 传输的对象。
  2. 提供WEB界面,优化用户体验 只用镜像的名字来进行上传下载显然很不方便,需要有一个用户界 面可以支持登陆、搜索功能,包括区分公有、私有镜像。
  3. 支持水平扩展集群 当有用户对镜像的上传下载操作集中在某服务器,需要对相应的访问压力作分解。
  4. 良好的安全机制 企业中的开发团队有很多不同的职位,对于不同的职位人员,分配不同的权限,具有更好的安全性。
Harbor安装

Harbor需要安装在192.168.200.131 Harbor && Tomcat

  • 先安装Docker并启动Docker

    sudo systemctl start docker 启动Docker
    sudo systemctl enable docker 开机自启动
    sudo systemctl status docker 查看Docker服务状态

  • 安装docker-compose

    sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose 
    sudo chmod +x /usr/local/bin/docker-compose
    

你可以使用 离线安装 docker-compose 的方式,在主机上下载好二进制文件,再拷贝到虚拟机中使用,非常简单。


✅ 方法一:主机下载 → 拷贝到虚拟机

步骤 1️⃣:在主机(Windows)上用浏览器下载这个文件

访问地址如下(你这个版本是 1.21.2):

👉 https://github.com/docker/compose/releases/download/1.21.2/docker-compose-Linux-x86_64

下载后,重命名为:docker-compose


步骤 2️⃣:用 MobaXterm 拷贝到虚拟机

你在 Mobaxterm 中连接好虚拟机后:

  1. 左侧文件管理器中找到 /usr/local/bin(或 /root
  2. 把你刚下载的 docker-compose 拖进去上传

或者用 scp 命令上传:

scp docker-compose root@192.168.200.131:/usr/local/bin/docker-compose

192.168.200.131 换成你虚拟机 IP,输入密码即可


步骤 3️⃣:在虚拟机中赋予执行权限

chmod +x /usr/local/bin/docker-compose

步骤 4️⃣:验证安装成功

docker-compose -v

输出类似:

docker-compose version 1.21.2, build ...

✅ 完成!

  • 下载Harbor的压缩包harbor-offline-installer-v1.9.2.tgz

  • 上传压缩包到linux并解压

    tar -xzf harbor-offline-installer-v1.9.2.tgz
    mkdir /opt/harbor
    mv harbor/* /opt/harbor
    cd /opt/harbor

  • 修改Harbor的配置

    vi harbor.yml

    修改hostname和port

    hostname:192.168.200.131
    port:85

  • 安装Harbor

    ./prepare
    ./install.sh

    启动Harbor ★★ [tomcat 131服务器]
    cd /opt/harbor
    ./install.sh
    
这里有个经典的超级牛逼的大bug
抛出问题:

这是我vi /etc/sysconfig/network-scripts/ifcfg-ens33
DEFROUTE=yes
NAME=ens33
UUID=a8ca17a4-0388-4b06-81b3-895e1a8180fe
DEVICE=ens33
ONBOOT=yes

IPADDR=192.168.200.131
NETMASK=255.255.255.0
GATEWAY=192.168.31.1
DNS1=8.8.8.8

这是我虚拟机的ip add

[root@localhost ~]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:7b:04:23 brd ff:ff:ff:ff:ff:ff
inet 192.168.200.131/24 brd 192.168.200.255 scope global noprefixroute ens33
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe7b:423/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:94:7c:ad:4a brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever

该虚拟机选择了桥接模式 且 勾选了复制到物理网络连接状态
其虚拟网络编辑器里面
VMnet0 桥接模式 自动桥接 主机连接无 DHCP无 子网地址无
VMnet1 仅主机 外部连接无 主机连接已连接 DHCP已启用 子网地址192.168.22.0
VMnet8 NAT模式 NAT模式 主机连接已连接 DHCP无 子网地址192.168.200.0

这是我主机ipconfig
C:\Users\Pluminary>ipconfig

Windows IP 配置

以太网适配器 以太网 4:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

无线局域网适配器 本地连接* 1:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

无线局域网适配器 本地连接* 2:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

以太网适配器 VMware Network Adapter VMnet1:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::1ea6:88fa:c0e1:f1ca%22
IPv4 地址 . . . . . . . . . . . . : 192.168.22.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :

以太网适配器 VMware Network Adapter VMnet8:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::9e33:f9ac:4f64:9d71%3
IPv4 地址 . . . . . . . . . . . . : 192.168.200.2
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.200.1

无线局域网适配器 WLAN:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::7df7:efe4:1d3b:e2d2%12
IPv4 地址 . . . . . . . . . . . . : 192.168.31.103
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.31.1

我现在的问题是 如果我改了GATEWAY=192.168.31.1 为别的 那么我MobaXterm就连不上该虚拟机 现在的问题是 虚拟机没有网 我镜像都下载不了[root@localhost harbor]# ./prepare
prepare base dir is set to /opt/harbor
Unable to find image ‘goharbor/prepare:v1.9.2’ locally
^C
[root@localhost harbor]# ^C
[root@localhost harbor]#

GitHub解决方案:

你目前的状态是:


虚拟机 IP192.168.200.131静态 IP,桥接模式下设置的
宿主机 WLAN IP192.168.31.103
虚拟机无法联网(不能拉取镜像)
MobaXterm 可以连接虚拟机(192.168.200.131)
⚠️ 虚拟机网关设置为 192.168.31.1,其实这是 WLAN 的网关,不属于 192.168.200.x 子网


🔥 你现在的问题本质:

  • 虽然 MobaXterm 能访问虚拟机 192.168.200.131,但虚拟机设置了一个不在本网段的网关:192.168.31.1
  • 所以虚拟机根本无法出网(不能 ping 外网,也不能拉 harbor 镜像)

✅ 正确配置方式:

让虚拟机在桥接模式下能 联网 + 被访问


✅ 第一步:设置正确网关(匹配虚拟机 IP 网段)

你虚拟机静态 IP 是 192.168.200.131,所以网关必须也在 192.168.200.1~254 范围。

观察宿主机 VMnet8(NAT)是 192.168.200.2,它的网关是:

默认网关: 192.168.200.1

所以你应设置:

IPADDR=192.168.200.131
NETMASK=255.255.255.0
GATEWAY=192.168.200.1
DNS1=8.8.8.8

✅ 这个配置能让虚拟机出网(使用 VMware NAT 出口),也能让宿主机通过 192.168.200.131 访问虚拟机。


✅ 第二步:如果你用的是 NAT 模式,则使用上面的 GATEWAY 是对的

但你说你用了 桥接模式,那就要看桥接的是哪块网卡。

桥接模式下,正确做法是:

  1. 虚拟机 IP 要跟宿主机所连网络同一个网段
  2. 网关要设置为 宿主机 WLAN 的网关:192.168.31.1

这样你才会在 MobaXterm 能访问、也能联网。


❗但你现在问题是:

你桥接了,但 IP 却在 192.168.200.x,这说明虚拟机并没有成功桥接到宿主机 WLAN 网卡,而是连到了 VMware 的 NAT 网络(即使你设置桥接,也可能没生效)


✅ 最建议方案

A. 用 NAT 模式 + 正确网关(推荐稳定,适合大多数人)

  1. 把虚拟机改为 NAT 模式
  2. ifcfg-ens33 设置如下:
DEVICE=ens33
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.200.131
NETMASK=255.255.255.0
GATEWAY=192.168.200.1   # NAT 的默认网关
DNS1=8.8.8.8
  1. 重启网络:
systemctl restart network
  1. 测试网络:
ping -c 3 8.8.8.8
curl www.baidu.com
  1. 然后使用 MobaXterm 访问你设置的 192.168.200.131,或设置端口转发(如 127.0.0.1:2222192.168.200.131:22

B. 或者继续桥接,但设置静态 IP 为 192.168.31.x 网段

如果你要桥接到宿主机 WLAN:

  1. 修改 /etc/sysconfig/network-scripts/ifcfg-ens33
DEVICE=ens33
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.31.130     # 自己定个没冲突的 IP
NETMASK=255.255.255.0
GATEWAY=192.168.31.1       # 你的无线网关
DNS1=8.8.8.8
  1. 重启网络:
systemctl restart network
  1. 检查 IP 和连通性:
ip addr
ping 8.8.8.8

✅ 结论

你现在 MobaXterm 能连接,是因为用了静态 IP 192.168.200.131,但由于网关不在同一网段,导致不能联网。

你只要根据你实际使用的网络模式(NAT 或桥接),把网关改成同网段的地址,问题就能解决。


如果你告诉我你最终想使用 NAT 还是桥接(是否希望虚拟机能被其它设备访问),我可以直接帮你生成完整配置文件。

继续延申问题:

我按照你说的改了 改成了nat模式 然后也改了配置 但是还是不行 [rooteloca1host ~]# ping -c 3 8.8.8.8PING 8.8.8.8(8.8.8.8) 56(84) bytes of data.From 192.168.200.131 icmp_seq=1 Destination Host UnreachableFrom 192.168.200.131 icmp_seq=2 Destination Host UnreachableFrom 192.168.200.131 icmp_seq=3 Destination Host Unreachable8.8.8.8 ping statistics3 packets transmitted,0 received, +3 errors, 100% packet loss,time 1999mspipe3 现在连MobaXterm都连不上了 Network error: Connection timed out ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Session stopped - Press to exit tab - Press R to restart session - Press S to save terminal output to file

✅ 你现在的问题是:

你切换到 NAT 模式了,配置也改了,但:

  • ❌ 仍然无法联网(ping 8.8.8.8 不通)
  • ❌ MobaXterm 也连不上虚拟机(连接超时)
  • ✅ 虚拟机 IP 是 192.168.200.131,网关你应该已改为 192.168.200.1(对吧?)

我们来一步步快速排查:


✅ 第一步:确认虚拟机网卡配置是否正确

在虚拟机里执行:

cat /etc/sysconfig/network-scripts/ifcfg-ens33

确认配置如下(静态 IP 模式 + NAT 网段的正确网关):

DEVICE=ens33
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.200.131
NETMASK=255.255.255.0
GATEWAY=192.168.200.1
DNS1=8.8.8.8

如果不是这样,请立刻修改。

然后执行:

systemctl restart network

✅ 第二步:确认 NAT 模式是否正确工作

打开 VMware 虚拟网络编辑器,确认:

  • VMnet8 网络的 子网 IP192.168.200.0
  • 默认网关(Gateway)是 192.168.200.1
  • 启用了 NAT 功能(有 NAT 设置 按钮,不是空的)
  • 勾选了 “使用本地 DHCP 服务将 IP 地址分配给虚拟机”(建议开启)

✅ 第三步:确认虚拟机路由是否存在

在虚拟机中执行:

ip route

你应该看到类似:

default via 192.168.200.1 dev ens33
192.168.200.0/24 dev ens33 proto kernel scope link src 192.168.200.131

如果没有 default via 192.168.200.1,说明网关没生效,路由不通。


✅ 第四步:确认宿主机启用了 NAT 网络服务

  1. 打开 VMware 网络编辑器(以管理员身份运行)
  2. 点击 VMnet8,点右侧的 “NAT 设置”
  3. 看到:
网关 IP: 192.168.200.1

下面是端口转发之类设置,不用动。

  1. 确保 VMware NAT Service 是运行状态:
    • 在宿主机按 Win+R → 输入 services.msc
    • 找到 VMware NAT ServiceVMware DHCP Service
    • 状态必须是“正在运行”

✅ 第五步:测试连接

  1. 在虚拟机里执行:
ping 192.168.200.1

能 ping 通说明 NAT 网关 OK。

  1. 在虚拟机里执行:
ping 8.8.8.8

仍然不通,说明出网失败,可能是:

  • 路由表错(ip route 查看)
  • NAT 服务没开(第 4 步)
  • 没走正确网关(第 1 步)


按照你说的做了 [root@localhost harbor]# docker ps | grep nginx 1773679bace8 goharbor/nginx-photon:v1.9.2 “nginx -g ‘daemon of…” 9 seconds ago Up 7 seconds (health: starting) 0.0.0.0:85->8080/tcp nginx 7e41a5e0662b goharbor/harbor-portal:v1.9.2 “nginx -g ‘daemon of…” 14 seconds ago Up 10 seconds (health: starting) 8080/tcp harbor-portal [root@localhost harbor]#

ChatGPT 说:

非常好,你现在的操作都没错,但从这条日志来看:


0.0.0.0:85->8080/tcp        nginx

说明 docker-compose.yml 里仍然是 85:8080 映射,而不是我们期望的:


85:80

✅ 解决方案:手动改一下 docker-compose.yml,然后重新启动 Harbor

✏️ 步骤 1:手动修改端口映射

打开 docker-compose.yml,找到 proxy: 下面这段:

ports:
  - 85:8080

改成:

ports:
  - 85:80

🔁 如果你在 Vim 里编辑,改完后按:

Esc → :wq

🔄 步骤 2:重启 Harbor 服务

docker-compose down
docker-compose up -d

🔍 步骤 3:确认端口映射是否正确

docker ps | grep nginx

应该看到:

0.0.0.0:85->80/tcp        nginx

✅ 最后:浏览器访问 Harbor

打开浏览器访问:

http://192.168.200.131:85/

你应该能看到 Harbor 登录页面。



🔍 目前情况总结:

状态
nginx 容器健康 healthy
curl localhost:85 ❌ 失败(提示:Connection reset by peer)
ss -tnlp 显示监听 docker-proxy 在监听 :::85
ping 192.168.200.131 ✅ 通
浏览器访问 http://192.168.200.131:85 ❌ 超时

✅ 正确的配置应为:

docker-compose.yml 里,把 proxy 服务的 ports 改为:

ports:
  - 85:80

然后执行如下命令使配置生效:

# 停止并清理容器
docker-compose down

# 再启动
docker-compose up -d

再执行:

curl -I http://localhost:85

你应该能看到:

HTTP/1.1 200 OK

然后用浏览器访问:

http://192.168.200.131:85

✅ 成功进入 Harbor 页面!



我已经用了NAT模式 + 静态ip [root@localhost harbor]# curl -I http://localhost:85 curl: (56) Recv failure: Connection reset by peer [root@localhost harbor]# 嗯… 无法访问此页面 192.168.200.131 响应时间太长 请尝试: 在 Web 上搜索 192 168 200 131 检查连接 检查代理和防火墙 ERR_CONNECTION_TIMED_OUT

ChatGPT 说:

你现在的情况是:

  • ✅ 已经使用了 NAT 模式
  • docker ps 显示 Harbor 的 nginx 容器 端口映射是正确的0.0.0.0:85->80
  • ❌ 但 curl localhost:85 报错:Connection reset by peer,说明容器里的 nginx 拒绝了连接;
  • ❌ 主机访问 http://192.168.200.131:85 报超时,NAT 本身不支持直接访问虚拟机的 IP
  • ✅ 你已给虚拟机设置静态 IP 为 192.168.200.131
  • 你没有配置端口转发!

🎯 结论:你现在这种做法是错误的

NAT 模式下,主机不能通过虚拟机的 IP(192.168.200.131)访问 Harbor 的端口。
应该通过端口转发实现!


✅ 正确做法:配置端口转发(NAT ➜ 主机映射)

🛠 步骤如下:

① 打开 VMware 虚拟网络编辑器

  • 打开 VMware Workstation(非虚拟机内部)
  • 菜单栏 ➜ 编辑虚拟网络编辑器
  • 找到 VMnet8(即 NAT 模式)

② 点击右下角的“NAT 设置

  • 在弹出的 NAT 设置窗口中 ➜ 点击“端口转发(Port Forwarding)

③ 添加一条端口转发规则:

名称 协议 主机端口 虚拟机 IP 虚拟机端口
harbor TCP 1085 192.168.200.131 85

主机端口你可以写 1085,避免冲突;虚拟机端口是 Harbor 的端口 85。

📌 说明:为什么 NAT 模式不能直接访问虚拟机 IP?

NAT 模式是虚拟机通过主机“共享网络”出去访问外部,但主机无法通过虚拟机的 NAT IP(如 192.168.200.131)访问回来,因为这个网段对主机来说是隔离的。
除非做端口转发,否则无法通信。



🧭 接下来的最后一步:

你当前的网络结构是:

Windows 浏览器访问 localhost:10085
 ↓
VMware NAT 端口转发:10085 → 192.168.200.131:85
 ↓
Harbor Docker:85 → nginx容器的 82
 ↓
nginx 正常监听 82

cd /opt/harbor/

搞了好久 估计是网络+配置的问题

这是docker-compose.yml的配置信息

version: '2.3'
services:
  log:
    image: goharbor/harbor-log:v1.9.2
    container_name: harbor-log
    restart: always
    dns_search: .
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - SETGID
      - SETUID
    volumes:
      - /var/log/harbor/:/var/log/docker/:z
      - ./common/config/log/logrotate.conf:/etc/logrotate.d/logrotate.conf:z
      - ./common/config/log/rsyslog_docker.conf:/etc/rsyslog.d/rsyslog_docker.conf:z
    ports:
      - 127.0.0.1:1514:10514
    networks:
      - harbor
  registry:
    image: goharbor/registry-photon:v2.7.1-patch-2819-2553-v1.9.2
    container_name: registry
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    volumes:
      - /data/registry:/storage:z
      - ./common/config/registry/:/etc/registry/:z
      - type: bind
        source: /data/secret/registry/root.crt
        target: /etc/registry/root.crt
    networks:
      - harbor
    dns_search: .
    depends_on:
      - log
    logging:
      driver: "syslog"
      options:  
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "registry"
  registryctl:
    image: goharbor/harbor-registryctl:v1.9.2
    container_name: registryctl
    env_file:
      - ./common/config/registryctl/env
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    volumes:
      - /data/registry:/storage:z
      - ./common/config/registry/:/etc/registry/:z
      - type: bind
        source: ./common/config/registryctl/config.yml
        target: /etc/registryctl/config.yml
    networks:
      - harbor
    dns_search: .
    depends_on:
      - log
    logging:
      driver: "syslog"
      options:  
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "registryctl"
  postgresql:
    image: goharbor/harbor-db:v1.9.2
    container_name: harbor-db
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - SETGID
      - SETUID
    volumes:
      - /data/database:/var/lib/postgresql/data:z
    networks:
      harbor:
    dns_search: .
    env_file:
      - ./common/config/db/env
    depends_on:
      - log
    logging:
      driver: "syslog"
      options:  
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "postgresql"
  core:
    image: goharbor/harbor-core:v1.9.2
    container_name: harbor-core
    env_file:
      - ./common/config/core/env
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - SETGID
      - SETUID
    volumes:
      - /data/ca_download/:/etc/core/ca/:z
      - /data/psc/:/etc/core/token/:z
      - /data/:/data/:z
      - ./common/config/core/certificates/:/etc/core/certificates/:z
      - type: bind
        source: ./common/config/core/app.conf
        target: /etc/core/app.conf
      - type: bind
        source: /data/secret/core/private_key.pem
        target: /etc/core/private_key.pem
      - type: bind
        source: /data/secret/keys/secretkey
        target: /etc/core/key
    networks:
      harbor:
    dns_search: .
    depends_on:
      - log
      - registry
      - redis
      - postgresql
    logging:
      driver: "syslog"
      options:  
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "core"
  portal:
    image: goharbor/harbor-portal:v1.9.2
    container_name: harbor-portal
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
      - NET_BIND_SERVICE
    networks:
      - harbor
    dns_search: .
    depends_on:
      - log
    logging:
      driver: "syslog"
      options:
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "portal"

  jobservice:
    image: goharbor/harbor-jobservice:v1.9.2
    container_name: harbor-jobservice
    env_file:
      - ./common/config/jobservice/env
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    volumes:
      - /data/job_logs:/var/log/jobs:z
      - type: bind
        source: ./common/config/jobservice/config.yml
        target: /etc/jobservice/config.yml
    networks:
      - harbor
    dns_search: .
    depends_on:
      - core
    logging:
      driver: "syslog"
      options:
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "jobservice"
  redis:
    image: goharbor/redis-photon:v1.9.2
    container_name: redis
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    volumes:
      - /data/redis:/var/lib/redis
    networks:
      harbor:
    dns_search: .
    depends_on:
      - log
    logging:
      driver: "syslog"
      options:
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "redis"
  proxy:
    image: goharbor/nginx-photon:v1.9.2
    container_name: nginx
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
      - NET_BIND_SERVICE
    volumes:
      - ./common/config/nginx:/etc/nginx:z
    networks:
      - harbor
    dns_search: .
    ports:
      - 85:8080
    depends_on:
      - registry
      - core
      - portal
      - log
    logging:
      driver: "syslog"
      options:  
        syslog-address: "tcp://127.0.0.1:1514"
        tag: "proxy"
networks:
  harbor:
    external: false
这是harbor里的nginx的配置
[root@localhost nginx]# cat nginx.conf

worker_processes auto;
pid /tmp/nginx.pid;

events {
  worker_connections 1024;
  use epoll;
  multi_accept on;
}

http {
  client_body_temp_path /tmp/client_body_temp;
  proxy_temp_path /tmp/proxy_temp;
  fastcgi_temp_path /tmp/fastcgi_temp;
  uwsgi_temp_path /tmp/uwsgi_temp;
  scgi_temp_path /tmp/scgi_temp;
  tcp_nodelay on;

  # this is necessary for us to be able to disable request buffering in all cases
  proxy_http_version 1.1;

  upstream core {
    server core:8080;
  }

  upstream portal {
    server portal:8080;
  }

  log_format timed_combined '$remote_addr - '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '$request_time $upstream_response_time $pipe';

  access_log /dev/stdout timed_combined;

  server {
    listen 8080;
    server_tokens off;
    # disable any limits to avoid HTTP 413 for large image uploads
    client_max_body_size 0;

    # Add extra headers
    add_header X-Frame-Options DENY;
    add_header Content-Security-Policy "frame-ancestors 'none'";

    # costumized location config file can place to /etc/nginx/etc with prefix harbor.http. and suffix .conf
    include /etc/nginx/conf.d/harbor.http.*.conf;

    location / {
      proxy_pass http://portal/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_buffering off;
      proxy_request_buffering off;
    }

    location /c/ {
      proxy_pass http://core/c/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_buffering off;
      proxy_request_buffering off;
    }

    location /api/ {
      proxy_pass http://core/api/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_buffering off;
      proxy_request_buffering off;
    }

    location /chartrepo/ {
      proxy_pass http://core/chartrepo/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_buffering off;
      proxy_request_buffering off;
    }

    location /v1/ {
      return 404;
    }

    location /v2/ {
      proxy_pass http://core/v2/;
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_buffering off;
      proxy_request_buffering off;
    }

    location /service/ {
      proxy_pass http://core/service/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_buffering off;
      proxy_request_buffering off;
    }

    location /service/notifications {
      return 404;
    }
  }
}

这里附带重大BUG排查思路

当然可以!我帮你把这整个过程的错误、涉及的宿主机、虚拟机、nginx(宿主机与容器里Harbor的nginx),以及排查思路,综合讲清楚,方便你理解整体流程。


1. 相关环境和组件说明

  • 宿主机:你的真实物理机或主机系统(Windows),负责运行虚拟机软件(比如VMware)。
  • 虚拟机:运行 Harbor 的 Linux 虚拟机(192.168.200.131),是 Harbor 的部署环境。
  • Docker 容器内的 nginx:Harbor 里内置的 nginx 作为反向代理服务器,监听容器内部的端口(如 82 或 8080),将请求转发给 Harbor 应用组件。
  • 宿主机与虚拟机的网络
    • 虚拟机配置桥接或NAT网络,宿主机通过 IP 和端口转发访问虚拟机。
    • 端口转发需确保宿主机防火墙、虚拟机防火墙和 Docker 端口映射正确。

2. 你遇到的主要错误和症状总结

错误/症状 可能原因 备注
Test-NetConnection 提示 TCP 连接失败(比如端口1085失败,10085成功但浏览器连接拒绝) 端口未开放或服务未监听,防火墙或网络配置问题 端口映射或容器服务没启动好
Docker 启动 nginx 容器时报错:unknown log opt 'syslog-address' for json-file log driver logging 配置不兼容,json-file 驱动不支持 syslog-address 参数 需要调整日志驱动配置
curl http://localhost:85 返回 302 或 200,但浏览器访问虚拟机IP时超时或重置连接 虚拟机防火墙、宿主机端口转发配置错误,或者nginx监听端口配置不一致 宿主机访问虚拟机端口不通
虚拟机上 ss -tnlp 看到 nginx 监听的是 82 端口,而 Docker 映射是 85:82 端口映射正确,但宿主机访问端口和内部监听端口不一致需要确认 访问时端口错位会导致访问失败

3. 排查思路和步骤详解

Step 1:确认容器服务启动及端口监听情况

  • docker-compose up -d 启动 Harbor 相关服务,包括 nginx 代理容器。
  • docker ps 看 nginx 容器是否在运行。
  • docker logs nginx 查看 nginx 容器日志,看是否报错。
  • ss -tnlp(或 netstat -tnlp)确认 nginx 容器内部监听端口,例如82或8080。

Step 2:确认端口映射和宿主机网络

  • 确认 docker-compose 端口映射,比如 - 85:82 说明宿主机85端口映射到容器82端口。
  • 确认虚拟机的网络IP(192.168.200.131)可访问,虚拟机是否允许访问85端口。
  • 通过宿主机(Windows)Test-NetConnection 192.168.200.131 85 测试连通性。
  • 如果端口连接失败,检查:
    • 虚拟机防火墙状态(如 firewall-cmd),确认85端口开放。
    • 虚拟机网络配置是否正确。
    • 宿主机是否正确做了端口转发(NAT配置等)。

Step 3:确认宿主机与虚拟机间端口转发与防火墙设置

  • 你之前做的 VMware NAT 端口转发配置,确认宿主机的10085端口转发到虚拟机的85端口。
  • 虚拟机防火墙是否禁用或开放该端口(你确认了 firewalld 未运行)。
  • SELinux 状态(你确认是 Permissive,不阻止)。

Step 4:确认容器内部 nginx 与 Harbor 应用运行状态

  • 容器内 nginx 配置监听82端口,但外部映射成85端口,确认两者一致。
  • 容器内 curl localhost:82 返回200,说明 nginx 服务正常。
  • nginx 返回的网页中包含 Harbor 前端静态资源,但前端一直转圈,可能是后端 API 没响应。
  • 可能是 Harbor 核心服务(core)未正常工作或访问不到数据库/redis,导致页面没加载完成。

Step 5:重启虚拟机后问题消失

  • 重启虚拟机后,网络重新初始化,Docker网络也重建,端口转发生效,nginx和Harbor服务正常,浏览器访问成功。
  • 说明之前网络环境(端口转发、Docker网络或nginx监听)存在缓存或未正常启动的问题。

4. 你看到的“宿主机与虚拟机”和“nginx内部/外部”的关系

层级 作用 备注
宿主机 (Windows) 你直接操作的机器,运行 VMware,提供 NAT/桥接网络,做端口转发 访问虚拟机IP和端口
虚拟机 (Linux Harbor 环境) Harbor 部署环境,运行 Docker 和 Harbor 服务容器 虚拟机防火墙和网络设置必须正确
Docker 容器内 nginx Harbor 的反向代理,监听容器内部端口(如82),转发给 Harbor 应用服务 Docker端口映射:宿主机85端口映射到容器82端口
Harbor 应用组件 Core、Portal、Registry 等,负责业务逻辑和数据 需要数据库和redis支持,服务正常才能正常访问

5. 综合总结

  • 你先遇到的端口连接失败,是虚拟机内服务监听、端口映射、虚拟机防火墙或宿主机端口转发配置的问题。
  • Docker 容器日志中的 unknown log opt 'syslog-address' 是日志驱动配置错误,需要统一用 json-file 或兼容的驱动。
  • 宿主机访问端口和容器监听端口要对应,nginx监听82端口,宿主机映射85端口,访问时要用宿主机的85端口。
  • 访问 Harbor 首页看到 nginx 返回 200,说明 nginx 正常,前端页面开始加载,但若页面卡住,可能是后端服务未完全启动或网络访问不到其他 Harbor 服务。
  • 重启虚拟机会刷新网络环境和容器服务,解决了之前隐藏的网络端口转发、Docker网络或者服务启动顺序的问题。
  • 最后确认服务正常,可以访问页面。

如果总结起来,整个排查流程是:

  1. 确认服务启动和端口监听docker-compose upss -tnlpdocker logs)。
  2. 检查端口映射和虚拟机网络配置(NAT端口转发、虚拟机IP、防火墙)。
  3. 宿主机访问测试(PowerShell的Test-NetConnection)。
  4. 确认容器内服务状态(容器内curl)。
  5. 调整配置并重启验证
  6. 最终确认页面访问正常

Harbor 访问故障排查文档

一、背景说明

你在虚拟机上用 Docker Compose 部署 Harbor,遇到宿主机访问 Harbor Web UI 端口无法连接的问题。本文档总结了排查和解决过程,覆盖容器服务状态、端口映射、虚拟机网络、防火墙配置等关键点。


二、排查准备

  • 宿主机环境:Windows(通过 VMware 运行虚拟机)
  • 虚拟机环境:Linux,Harbor 服务部署环境,IP 如 192.168.200.131
  • Docker Harbor 版本:v1.9.2
  • 访问端口映射:宿主机端口 85 映射到容器内 nginx 监听端口 82

三、排查步骤

Step 1. 容器服务启动状态检查

  • 使用命令:

    bash复制编辑docker-compose up -d
    docker ps
    docker logs nginx --tail 50
    
  • 确认 nginx 容器及其他 Harbor 服务容器是否运行正常,没有报错。

  • 使用命令查看容器内部端口监听:

    bash
    
    
    ss -tnlp | grep nginx
    

确认 nginx 是否监听正确的端口(一般是 82 或 8080)。


Step 2. Docker 端口映射与虚拟机端口监听确认

  • 检查 docker-compose.yml 端口映射配置,如:

    ```

ports:
- 85:82


表示宿主机(虚拟机)85端口映射到容器82端口。

- 在虚拟机上确认端口监听:
ss -tnlp | grep :85
```

通常 docker-proxy 会监听对应端口。


Step 3. 虚拟机网络配置和防火墙检查

  • 检查虚拟机的防火墙状态:

firewall-cmd --state
确保防火墙关闭或已开放对应端口。
  • 查看 SELinux 状态(有时会阻止网络服务):

    ```
    getenforce


建议设置为 `Permissive` 或 `Disabled`。

- 检查虚拟机的 IP 配置,确认使用的是正确的 IP。
 
 - 在宿主机(Windows)上使用 PowerShell 测试端口连通:

powershell

复制编辑
Test-NetConnection -ComputerName 192.168.200.131 -Port 85


------

### Step 4. 宿主机与虚拟机端口转发设置(NAT模式)
 
 - 如果使用 VMware NAT,需要在 VMware NAT 设置中配置端口转发:
 - 主机端口(宿主机)设为 `10085`(或你需要的端口)
 - 转发到虚拟机 IP(如 `192.168.200.131`)的 85 端口
- 通过宿主机访问 `localhost:10085`,实际访问虚拟机的 Harbor 服务。

------

### Step 5. 使用 curl 命令测试服务

- 在虚拟机内部测试:

curl -I http://localhost:82


- 在虚拟机外部测试(宿主机):

curl -I http://192.168.200.131:85

```
  • 观察返回结果,确认 HTTP 状态码是否正常(200/302 等)。

Step 6. 遇到问题的典型症状及对应处理

症状 可能原因 解决办法
宿主机无法连接端口,连接超时 防火墙阻止、端口未开放 关闭防火墙或放行端口
curl 返回连接重置或拒绝 服务未启动或监听端口错误 检查 nginx 配置和容器启动
curl 返回200但页面加载不出 后端服务未就绪或网络不通 检查 Harbor core 服务状态
日志中报错日志驱动不支持 docker-compose 日志配置错误 统一使用 json-file 驱动
端口映射配置错误 端口映射写错 纠正 docker-compose.yml 中端口映射

Step 7. 重启生效验证

  • 重启虚拟机,有时可以修复网络绑定、端口冲突等隐患问题。
  • 重启后再次测试端口连通性和页面访问。

四、示例核心配置(端口映射部分)

services:
proxy:
   image: goharbor/nginx-photon:v1.9.2
  container_name: nginx
   ports:
    - 85:82  # 宿主机端口 85 映射到容器内 nginx 监听的 82 端口
       volumes:
           - ./common/config/nginx:/etc/nginx:z
          depends_on:
                 - core
                      - portal
                       - registry

五、常用命令汇总

作用 命令示例
查看容器状态 docker ps
查看容器日志 docker logs nginx --tail 50
查看端口监听 `ss -tnlp
测试端口连通(宿主机) Test-NetConnection -ComputerName 192.168.200.131 -Port 85 (PowerShell)
虚拟机防火墙状态 firewall-cmd --state
SELinux 状态 getenforce
虚拟机内访问本地端口 curl -I http://localhost:82

访问故障排查简易步骤清单

  1. 确认 Docker 容器和服务状态
    • docker-compose up -d
    • docker ps
    • docker logs nginx
  2. 确认端口监听和映射
    • 容器内部 ss -tnlp | grep nginx
  • 虚拟机 ss -tnlp | grep :85
  • docker-compose.yml 中端口映射是否正确
  1. 检查虚拟机网络和防火墙
    • firewall-cmd --state
  • getenforce
  • 关闭防火墙或放行端口
  1. 宿主机端口转发(NAT 模式)配置正确
    • 确认 VMware NAT 端口转发设置无误
  2. 测试端口连通性
    • 宿主机 PowerShell 测试端口连通
    • 虚拟机内 curl 测试端口访问
  3. 浏览器访问测试
    • 用正确端口访问 Harbor 页面
    • 观察是否能正常加载
  4. 重启虚拟机和服务
    • 解决网络缓存、绑定等异常
在Harbor创建用户和项目
  • 创建项目

Harbor的项目分为公开和私有的:
公开项目:所有用户都可以访问,通常存放公共的镜像,默认有一个library公开项目。 私有项目:只有授权用户才可以访问,通常存放项目本身的镜像。
我们可以为微服务项目创建一个新的项目

  • 创建用户 创建的用户为:eric Eric123456

  • 给私有项目分配用户

    进入tensquare项目→成员→新建用户→eric为维护人员

角色 权限说明
访客 对于指定项目拥有只读权限
开发人员 对于指定项目拥有读写权限
维护人员 对于指定项目拥有读写权限,创建Webhooks
项目管理员 除了读写权限,同时拥有用户管理/镜像扫描等管理权限
  • 以新用户登录Harbor
把镜像上传到Harbor
  • 给镜像打上标签

    docker tag eureka:v1 192.168.200.131:85/tensquare/eureka:v1

  • 推送镜像

    docker push 192.168.200.131:85/tensquare/eureka:v1


    The push refers to repository [192.168.66.102:85/tensquare/eureka] Get https://192.168.200.131:85/v2/: http: server gave HTTP response to HTTPS client

    这时会出现以上报错,是因为 Docker没有把Harbor加入信任列表中

  • 把Harbor地址加入到Docker信任列表
    cd /etc/docker

    vi daemon.json

    {
    “registry-mirrors”: [“https://zydiol88.mirror.aliyuncs.com"],
    “insecure-registries”: [“192.168.200.131:85”]
    }

​ 需要重启Docker
systemctl restart docker

  • 再次执行推送命令,会显示权限不足

    docker push 192.168.200.131:85/tensquare/eureka:v1

    denied: requested access to the resource is denied

    需要先登录 Harbor,再推送镜像

  • 登录Harbor

    docker login -u eric -p Eric123456 192.168.200.131:85

  • 再次push

    docker push 192.168.200.131:85/tensquare/eureka:v1

从Harbor下载镜像

需求:在192.168.200.130服务器完成从Harbor下载镜像

  • 安装并启动Docker

  • 修改Docker配置

    vi /etc/docker/daemon.json

    {
     "registry-mirrors": ["https://zydiol88.mirror.aliyuncs.com"],
     "insecure-registries": ["192.168.66.102:85"]
    }
    

    重启docker:systemctl restart docker

  • 先登录,再从Harbor下载镜像

    docker login -u eric -p Eric123456 192.168.200.131:85
    docker pull 192.168.200.131:85/tensquare/eureka:v1

微服务持续集成(1)-项目代码上传到Gitlab

在IDEA操作即可,参考之前的步骤。包括后台微服务(tensquare_back)和前端web网站(tensquare_front)代码
[lanyun_group · GitLab] (http://192.168.200.132:82/lanyun_group)

微服务持续集成(2)-从Gitlab拉取项目源码后台微服务的持续集成

先去Jenkins里面创建一个tensquare_back的流水线 → 流水线定义选择Pipeline script form SCM脚本文件放在项目根目录下 → SCM选择Git拉取脚本文件 → 在根目录下创建一个Jenkinsfile → 用流水线语法写’拉取代码’的语法 → ↓

stage('拉取代码') {
      checkout scmGit(branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a', url: 'http://192.168.200.132:82/lanyun_group/tensquare_back.git']])
   }

→ 修改语法代码里的 name: ‘*/master’ 为可变参数 → 去流水线的配置里 勾选This project is parameterized → 添加参数选择String Parameter → 名称:branch 默认值:master 描述:请输入一个分支名称 → 修改里面的参数为动态的 →↓

   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: 'ca22e56f-0ecc-4fdc-965d-01e329a0b68a', url: 'http://192.168.200.132:82/lanyun_group/tensquare_back.git']])
   }

→ 将后面的credentialsIdurl可以定义为脚本参数全局引用 → ↓

//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"


node {
   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
}

构建即可成功!! 可以去Moba的/var/lib/jenkins/workspace/查看是否有tensquare_back

微服务持续集成(3)-提交到SonarQube代码审查

  • 创建项目,并设置参数

创建tensquare_back项目,添加两个参数 → 在流水线上设置里选择choice Parameter → 名称:project_name;选项:tensquare_eureka_server、tensquare_zuul、tensquare_admin_service、tensquare_gathering;描述:请选择一个构建的项目

  • 每个项目的根目录下添加sonar-project.properties
eureka_server   gathering   admin_service   zuul
只需要改里面的projectName和projectKey即可

# must be unique in a given SonarQube instance
sonar.projectKey=tensquare_eureka_server
# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
sonar.projectName=tensquare_eureka_server
sonar.projectVersion=1.0

# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
# This property is optional if sonar.modules is set.
sonar.sources=.
sonar.exclusions=**/test/**,**/target/**
sonar.java.binaries=.

sonar.java.source=1.8
sonar.java.target=1.8
#sonar.java.libraries=**/target/classes/**

# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8
  • 修改jenkinsfile构建脚本

要在Jenkins的全局Tools管理里面 有SonarQube Scanner安装
Jenkins全局的system里面有定义sonarqube的url

//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"


node {
   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
   stage('代码审查') {
      //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
      def scannerHome = tool 'sonar-scanner'
      //引用当前JenkinsSonarQube环境 Jenkins的System里
      withSonarQubeEnv('sonarqube'){
      // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
                sh """
                    cd ${project_name}
                    ${scannerHome}/bin/sonar-scanner
                """
      }
    }
}

微服务持续集成(4)-使用Dockerfile编译、生成镜像

利用dockerfile-maven-plugin插件构建Docker镜像
  • 在每个微服务项目的pom.xml加入dockerfile-maven-plugin插件
<plugin>
                <groupId>com.spotify</groupId>
                <artifactId>dockerfile-maven-plugin</artifactId>
                <version>1.3.6</version>
                <configuration>
                    <repository>${project.artifactId}</repository>
                    <buildArgs>
                        <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
                    </buildArgs>
                </configuration>
            </plugin>
  • 修改Jenkinsfile让他根据参数打包微服务工程
//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"


node {
   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
   stage('代码审查') {
      //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
      def scannerHome = tool 'sonar-scanner'
      //引用当前JenkinsSonarQube环境 Jenkins的System里
      withSonarQubeEnv('sonarqube'){
      // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
                sh """
                    cd ${project_name}
                    ${scannerHome}/bin/sonar-scanner
                """
      }
    }
    stage('编译,安装公共子工程'){
        sh "mvn -f tensquare_common clean install"
    }
    stage('编译,打包微服务工程'){
        sh "mvn -f ${project_name} clean package"
    }
}
  • 父工程的pom.xml要改一个过时的代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tensquare</groupId>
    <artifactId>tensquare_parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>tensquare_common</module>
        <module>tensquare_eureka_server</module>
        <module>tensquare_zuul</module>
        <module>tensquare_admin_service</module>
        <module>tensquare_gathering</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath />
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!-- ✅ 推荐使用正式版本 Finchley.SR4 -->
        <spring-cloud.version>Finchley.SR4</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 可选:保留 spring-milestones -->
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>
  • 通过MoBa把E:\Java实例项目1-20套\资料-Jenkins教程\Jenkins资料\资料\资料\03.资料\tensquare_parenttensquare_parent项目保存到/root/repo/com/tensquare/
  • 随后通过步骤二,构建所有的工程 → 打包所有微服务的工程 → 在Jenkins虚拟机的/var/lib/jenkins/workspace/tensquare_back/这里面的各个项目的target里面打包了刚刚微服务的所有构建东西

微服务持续集成(4)-使用Dockerfile编译、生成镜像

利用dockerfile-maven-plugin插件构建Docker镜像

  • 在每个微服务项目的pom.xml加入dockerfile-maven-plugin插件
 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>dockerfile-maven-plugin</artifactId>
                <version>1.3.6</version>
                <configuration>
                    <repository>${project.artifactId}</repository>
                    <buildArgs>
                        <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
                    </buildArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
  • 在dockerfile父目录文件中增加一个...dockerfile:build去触发每个微服务项目的dockerfile执行【用流水线进行镜像构建】
    stage('编译,安装公共子工程'){
        sh "mvn -f tensquare_common clean install"
    }
    stage('编译,打包微服务工程'){
        sh "mvn -f ${project_name} clean package dockerfile:build"
    }

全部构建完成后就大功告成!!!

[root@localhost docker]# docker images
REPOSITORY                            TAG                 IMAGE ID            CREATED             SIZE
tensquare_gathering                   latest              a62362e8dc2f        8 minutes ago       155MB
tensquare_admin_service               latest              ee2a85292e98        9 minutes ago       156MB
tensquare_eureka_server               latest              2a78c4a12fc4        11 minutes ago      148MB
tensquare_zuul                        latest              33bf136ee3cb        12 minutes ago      146MB
eureka                                v2                  2c9a3610123d        24 hours ago        150MB
192.168.200.131:85/tensquare/eureka   v1                  e85b5d3d9090        25 hours ago        150MB
eureka                                v1                  e85b5d3d9090        25 hours ago        150MB
nginx                                 latest              9592f5595f2b        8 days ago          192MB
hello-world                           latest              74cc54e27dc4        5 months ago        10.1kB
openjdk                               8-jdk-alpine        a3562aa0b991        6 years ago         105MB

微服务持续集成(5)-上传到Harbor镜像仓库

  • 修改Jenkinsfile.groovy构建脚本
//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
//镜像的版本号
def tag = "latest"
//Harbor的url地址
def harbor_url = "192.168.200.131:85"
//镜像库项目名称
def harbor_project = "tensquare"


node {
   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
   stage('代码审查') {
      //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
      def scannerHome = tool 'sonar-scanner'
      //引用当前JenkinsSonarQube环境 Jenkins的System里
      withSonarQubeEnv('sonarqube'){
      // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
                sh """
                    cd ${project_name}
                    ${scannerHome}/bin/sonar-scanner
                """
      }
    }
    stage('编译,安装公共子工程'){
        sh "mvn -f tensquare_common clean install"
    }
    stage('编译,打包微服务工程'){
        sh "mvn -f ${project_name} clean package dockerfile:build"
        // 定义镜像名称
        def imageName = "${project_name}:${tag}"

        // 对镜像打上标签
        sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"
    }
}

此时再去构建一个项目在Jenkins虚拟机服务的镜像里就会出现按照规则打好tag的镜像

[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.200.131:85/tensquare/tensquare_eureka_server latest e19b4ba07d54 About a minute ago 148MB

  • 把镜像推送到Harbor(要登录 输入账号密码 不要写在脚本文件 把harbor账号密码用jenkins全局凭证)

去Jenkins申请一个全局的凭证,ID:f83725d3-d5fc-4faf-8ac1-1a56487dfc7b
怎么在jenkinsfile里面使用呢?要用流水线语法的片段生成器 withCredentials:Bind credentials to variables[借助凭证进行脚本处理] (如果没有显示绑定 就刷新一下界面);选择绑定用户名变量:username 密码变量:password 这个是引用的刚刚创立的eric_harbor-auth凭证

//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
//镜像的版本号
def tag = "latest"
//Harbor的url地址
def harbor_url = "192.168.200.131:85"
//镜像库项目名称
def harbor_project = "tensquare"
//harbor的登录凭证ID
def harbor_auth = "f83725d3-d5fc-4faf-8ac1-1a56487dfc7b"


node {
   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
   stage('代码审查') {
      //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
      def scannerHome = tool 'sonar-scanner'
      //引用当前JenkinsSonarQube环境 Jenkins的System里
      withSonarQubeEnv('sonarqube'){
      // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
                sh """
                    cd ${project_name}
                    ${scannerHome}/bin/sonar-scanner
                """
      }
    }
    stage('编译,安装公共子工程'){
        sh "mvn -f tensquare_common clean install"
    }
    stage('编译,打包微服务工程'){
        sh "mvn -f ${project_name} clean package dockerfile:build"
        // 定义镜像名称
        def imageName = "${project_name}:${tag}"

        // 对镜像打上标签
        sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"

        // 把镜像推送到Harbor(要登录 输入账号密码 不要写在脚本文件 把harbor账号密码用jenkins全局凭证)
        withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
            // 登录到Harbor
            sh "docker login -u ${username} -p  ${password} ${harbor_url}"

            //镜像上传
            sh "docker push ${harbor_url}/${harbor_project}/${imageName}"

            sh "echo '🔥🔥镜像上传成功✅✅'"
        }
    }
}

此时harbor的tensquare中镜像仓库就有了tensquare/tensquare_eureka_server

微服务持续集成(6)-拉取镜像和发布应用

安装 Publish Over SSH 插件目的是想把129Jenkins和130product进行连通

安装此插件可以实现远程发送Shell命令

随后去全局system寻找Publish over SSH

在product虚拟机:id_rsa是私钥id_rsa.pub是公钥

[root@localhost ~]# cd /root/.ssh
[root@localhost .ssh]# ll
总用量 12
-rw-------. 1 root root 1679 6月  18 20:05 id_rsa
-rw-r--r--. 1 root root  408 6月  18 20:05 id_rsa.pub
-rw-r--r--. 1 root root  177 6月  18 21:00 known_hosts
[root@localhost .ssh]#

输入[root@localhost .ssh]# ssh-keygen -y -f /root/.ssh/id_rsa会显示私钥
如果直接输出 ssh-rsa AAAA... 这样的公钥内容 → 说明没有加密,Jenkins 中可以把 Passphrase 留空
下面添加一台远程服务器SSH Servers

Name:master_server
Hostname:192.168.200.130
Username:root
Remote Directory:/

配置远程部署服务器
  • 拷贝公钥到远程服务器

是的,你遇到的错误:

Failed to connect session for config [master_server]. Message [Auth fail for methods 'publickey,gssapi-keyex,gssapi-with-mic,password']

说明 Jenkins 使用你提供的私钥连接远程服务器失败。这个问题一般由以下几种原因导致:


✅【推荐方案】重新生成兼容格式的私钥(PEM)

你看到的建议:

ssh-keygen -m PEM -t rsa -b 4096

是完全正确的,因为:

  • Jenkins 使用的 Java SSH 实现(JSch 或 Trilead)不支持 OpenSSH 的新私钥格式(RFC4716)
  • -m PEM 表示使用旧版兼容格式(Jenkins 能识别);
  • -t rsa -b 4096 表示生成 RSA 4096 位密钥。

✅ 重新生成 SSH 密钥步骤如下

  1. 在 Jenkins 所在机器上执行(推荐使用 jenkins 用户身份,不要用 root):
ssh-keygen -m PEM -t rsa -b 4096 -C "jenkins@tensquare" -f ~/.ssh/id_rsa_jenkins

过程中会问你是否设置密码(passphrase),你可以 直接回车留空

生成后会有两个文件:

~/.ssh/id_rsa_jenkins       ← 私钥(复制给 Jenkins)
~/.ssh/id_rsa_jenkins.pub   ← 公钥(配置到目标服务器)

  1. 把公钥内容添加到远程主机的 authorized_keys 中
cat ~/.ssh/id_rsa_jenkins.pub | ssh root@192.168.200.130 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

或者你手动将 id_rsa_jenkins.pub 内容添加到 /root/.ssh/authorized_keys 中。


  1. Jenkins 中配置 SSH 连接

在 Jenkins → Manage JenkinsConfigure SystemPublish over SSH

  • Path to Key:填入 /root/.ssh/id_rsa_jenkins(或你放的实际路径)
  • Key:可留空(有 Path 即可)
  • Passphrase:如果你上面生成时留空,这里也留空
  • Name:master_server
  • Hostname:192.168.200.130
  • Username:root(或其他你授权了公钥的用户)
  • Remote Directory:比如 /home/jenkins-deploy/root//

✔ 点击 Test Configuration 测试成功为止

  • 查看流水线语法

    因为新下载那个插件后需要重启Jenkins。然后
    步骤sshPublisher:Send build artifacts overSSH 直接生成流水线脚本后融入Jenkinsfile文件中

    SSH Server
    Name:master_server

里面的$port需要搞成可变参数 去Jenkins里面添加一个参数

名称:port
默认值:10086
描述:请输入一个对应服务的端口

在生产虚拟机先[root@localhost ~]# mkdir /opt/jenkins_shell然后把E:\Java实例项目1-20套\资料-Jenkins教程\Jenkins资料\资料\资料\03.资料\部署脚本deploy.sh传入 之后添加执行权限[root@localhost jenkins_shell]# chmod +x deploy.sh

成功!!!

[root@localhost jenkins_shell]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.200.131:85/tensquare/eureka v1 e85b5d3d9090 44 hours ago 150MB
[root@localhost jenkins_shell]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES


[root@localhost jenkins_shell]# docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.200.131:85/tensquare/tensquare_eureka_server latest 60d020191110 35 seconds ago 148MB
192.168.200.131:85/tensquare/eureka v1 e85b5d3d9090 44 hours ago 150MB
[root@localhost jenkins_shell]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b934523c60da 192.168.200.131:85/tensquare/tensquare_eureka_server:latest “java -jar /app.jar” 33 seconds ago Up 30 seconds 0.0.0.0:10086->10086/tcp focused_boyd

成功访问:[Eureka] (http://192.168.200.130:10086/)

如果要下载其他镜像 需要把每个服务的application.yml更改连接地址

更改所有服务的application.yml地址
sonarqube的是129里面有mysql
eureka的是130用的生产虚拟服务器

那我微服务里的application.yml server: port: 9001 spring: application: name: tensquare-admin-service #指定服务名 datasource: driverClassName: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.200.129/tensquare_user?characterEncoding=UTF8 username: root password: root jpa: database: mysql show-sql: true 这样写可以吗

那个192.168.200.129:3306/回答:

  • 如果你的微服务运行环境和 MySQL 在同一局域网内,且 192.168.200.129 是 MySQL 服务所在虚拟机的有效IP,且网络是通的,端口 3306 没被防火墙拦截,NAT或者桥接配置无误,使用这个 IP 和端口是完全没问题的。
  • 也就是说,如果你从微服务所在的机器能 ping 通 192.168.200.129 并且 telnet 192.168.200.129 3306 可以连通,就可以这么写。

你需要确认的:

  1. 你的微服务运行在哪台机器?
    • 如果微服务也在虚拟机里(同一台虚拟机或同一内网),用虚拟机的IP写就对。
    • 如果微服务在宿主机,且虚拟机用的是 NAT 模式,你必须确认端口映射存在,否则宿主机连这个 IP 3306 不通(原因是 NAT 隔离)。这时用虚拟机私有 IP 直接连接是不行的。
  2. 网络连通性
    • 微服务所在机器能 ping 通该 IP。
    • telnet IP 3306 连通。

总结

  • 只要网络通,端口放通,jdbc:mysql://192.168.200.129:3306/数据库名 写法就是标准且正确的。
  • 如果网络不通,就要调整网络配置(比如桥接、端口映射),或者改用 localhost127.0.0.1 访问本机MySQL。
分别构建其余的几个微服务项目 记住port端口是要跟application.yml里面一致的

[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.200.131:85/tensquare/tensquare_gathering latest c700a6359fce 33 seconds ago 155MB
192.168.200.131:85/tensquare/tensquare_admin_service latest 92453ab6e765 2 minutes ago 156MB
192.168.200.131:85/tensquare/tensquare_zuul latest 122afd78d6ef 3 minutes ago 146MB
192.168.200.131:85/tensquare/tensquare_eureka_server latest 60d020191110 4 hours ago 148MB
192.168.200.131:85/tensquare/eureka v1 e85b5d3d9090 2 days ago 150MB


[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aa2178c7dd0d 192.168.200.131:85/tensquare/tensquare_gathering:latest “java -Djava.net.pre…” About a minute ago Up 59 seconds 0.0.0.0:9002->9002/tcp nostalgic_lichterman
14e040066165 192.168.200.131:85/tensquare/tensquare_admin_service:latest “java -Djava.net.pre…” 2 minutes ago Up 2 minutes 0.0.0.0:9001->9001/tcp dreamy_montalcini
8143d8ccbeee 192.168.200.131:85/tensquare/tensquare_zuul:latest “java -Djava.net.pre…” 3 minutes ago Up 3 minutes 0.0.0.0:10020->10020/tcp gracious_kalam
b934523c60da 192.168.200.131:85/tensquare/tensquare_eureka_server:latest “java -jar /app.jar” 4 hours ago Up 4 hours 0.0.0.0:10086->10086/tcp focused_boyd

你这个情况是镜像有了,但是没有运行的容器,所以要启动镜像,创建并运行容器。

启动镜像★★的基本命令是:[Jenkins 129服务器]

docker run -d -p <宿主机端口>:<容器端口> --name <容器名字> <镜像名>

直接启动无需重新创建容器:
先查看所有容器(包括已停止的):docker ps -a

docker start tensquare_admin_service
docker start tensquare_gathering
docker start tensquare_zuul
docker start tensquare_eureka_server

若需要删除再重新启动

举个例子:

先删除之前的容器:
docker rm tensquare_admin_service
docker rm tensquare_gathering
docker rm tensquare_zuul
docker rm tensquare_eureka_server

docker run -d -p 9001:9101 --name tensquare_admin_service 192.168.200.131:85/tensquare/tensquare_admin_service:latest
docker run -d -p 9002:9002 --name tensquare_gathering 192.168.200.131:85/tensquare/tensquare_gathering:latest
docker run -d -p 10020:10020 --name tensquare_zuul 192.168.200.131:85/tensquare/tensquare_zuul:latest
docker run -d -p 10086:10086 --name tensquare_eureka_server 192.168.200.131:85/tensquare/tensquare_eureka_server:latest

[root@localhost ~]# docker rm -f tensquare_admin_service
tensquare_admin_service
[root@localhost ~]# docker run -d -p 9101:9001 –name tensquare_admin_service 192.168.200.131:85/tensquare/tensquare_admin_service:latest
634a31582dc81acf34b101d468d84b1c3f92b75477b6ad469abe00dc09821d4b
[root@localhost ~]#

端口映射要根据你的程序监听端口设定,否则外部访问不到容器服务。

此时也要在生产130服务器上去重新启动那些容器

1. 重启 130 上的容器

可以用 docker start 容器ID或容器名 重启:

bash复制编辑docker start e55957d0a79e  # gathering
docker start 3c8592621ab7  # admin_service
docker start 43a7ca513e37  # zuul
docker start 831aeb331519  # eureka

或者统一启动:

bash


复制编辑
docker start $(docker ps -a -q)



删除镜像

停止容器:

docker stop 容器ID或容器名

删除容器:

docker rm 容器ID或容器名

3. 删除镜像

删除有标签的镜像:

docker rmi 192.168.200.131:85/tensquare/tensquare_gathering
docker rmi tensquare_gathering

删除无标签的悬空镜像:

docker rmi 08b0a9196df2

4. 如果有多个无标签镜像,删除所有悬空镜像

docker image prune

[生产]:[Eureka] (http://192.168.200.130:10086/)

Instances currently registered with Eureka
Application AMIs Availability Zones Status
TENSQUARE-ADMIN-SERVICE n/a (1) (1) UP (1) - 14e040066165:tensquare-admin-service:9001
TENSQUARE-GATHERING n/a (1) (1) UP (1) - aa2178c7dd0d:tensquare-gathering:9002
TENSQUARE-ZUUL n/a (1) (1) UP (1) - 8143d8ccbeee:tensquare-zuul:10020
Jenkinsfile
//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
//镜像的版本号
def tag = "latest"
//Harbor的url地址
def harbor_url = "192.168.200.131:85"
//镜像库项目名称
def harbor_project = "tensquare"
//harbor的登录凭证ID
def harbor_auth = "f83725d3-d5fc-4faf-8ac1-1a56487dfc7b"


node {
   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
   stage('代码审查') {
      //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
      def scannerHome = tool 'sonar-scanner'
      //引用当前JenkinsSonarQube环境 Jenkins的System里
      withSonarQubeEnv('sonarqube'){
      // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
                sh """
                    cd ${project_name}
                    ${scannerHome}/bin/sonar-scanner
                """
      }
    }
    stage('编译,安装公共子工程'){
        sh "mvn -f tensquare_common clean install"
    }
    stage('编译,打包微服务工程'){
        sh "mvn -f ${project_name} clean package dockerfile:build"
        // 定义镜像名称
        def imageName = "${project_name}:${tag}"

        // 对镜像打上标签
        sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"

        // 把镜像推送到Harbor(要登录 输入账号密码 不要写在脚本文件 把harbor账号密码用jenkins全局凭证)
        withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
            // 登录到Harbor
            sh "docker login -u ${username} -p  ${password} ${harbor_url}"

            //镜像上传
            sh "docker push ${harbor_url}/${harbor_project}/${imageName}"

            sh "echo '🔥🔥镜像上传成功✅✅'"
        }
        // 部署应用 发送命令调用生产服务器的shell脚本文件 完成所有的微服务项目部署行为
        // deploy.sh 是触发远程脚本文件(镜像拉取和容器创建的行为)
        sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project $project_name $tag $port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
    }
}
deploy.sh
#! /bin/sh
#接收外部参数
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5

imageName=$harbor_url/$harbor_project_name/$project_name:$tag

echo "$imageName"

#查询容器是否存在,存在则删除
containerId=`docker ps -a | grep -w ${project_name}:${tag}  | awk '{print $1}'`
if [ "$containerId" !=  "" ] ; then
    #停掉容器
    docker stop $containerId

    #删除容器
    docker rm $containerId
    
    echo "成功删除容器"
fi

#查询镜像是否存在,存在则删除
imageId=`docker images | grep -w $project_name  | awk '{print $3}'`

if [ "$imageId" !=  "" ] ; then
      
    #删除镜像
    docker rmi -f $imageId
    
    echo "成功删除镜像"
fi

# 登录Harbor
docker login -u eric -p Eric123456 $harbor_url

# 下载镜像
docker pull $imageName

# 启动容器
docker run -di -p $port:$port $imageName

echo "容器启动成功"
连接数据库的时候有问题

[root@localhost ~]# /usr/local/mysql/bin/mysqld –initialize –user=mysql –datadir=/var/lib/mysql 2025-07-04T10:46:28.109697Z 0 [Warning] [MY-011070] [Server] ‘Disabling symbolic links using –skip-symbolic-links (or equivalent) is the default. Consider not using this option as it’ is deprecated and will be removed in a future release. 2025-07-04T10:46:28.110588Z 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.30) initializing of server in progress as process 31821 2025-07-04T10:46:28.115123Z 0 [ERROR] [MY-010457] [Server] –initialize specified but the data directory has files in it. Aborting. 2025-07-04T10:46:28.115149Z 0 [ERROR] [MY-013236] [Server] The designated data directory /var/lib/mysql/ is unusable. You can remove all files that the server added to it. 2025-07-04T10:46:28.115231Z 0 [ERROR] [MY-010119] [Server] Aborting 2025-07-04T10:46:28.115717Z 0 [System] [MY-010910] [Server] /usr/local/mysql/bin/mysqld: Shutdown complete (mysqld 8.0.30) MySQL Community Server - GPL. [root@localhost ~]#

ChatGPT 说:

报错很明确:

--initialize specified but the data directory has files in it. Aborting.

意思是:你指定的 /var/lib/mysql 目录 里面已经有文件了,所以初始化被拒绝了。


解决方案

1. 备份或清空 /var/lib/mysql 目录

如果你确认目录里面的数据可以删除(或者你已经备份了重要数据),执行:

rm -rf /var/lib/mysql/*

确保目录下文件都删干净。


2. 再次执行初始化

/usr/local/mysql/bin/mysqld --initialize --user=mysql --datadir=/var/lib/mysql

这次应该会成功。


3. 启动 MySQL 服务

systemctl start mysql

4. 查看状态和日志,确保启动成功

systemctl status mysql
tail -n 50 /var/log/mariadb/mariadb.log

如果你担心误删数据,可以先备份目录:

mv /var/lib/mysql /var/lib/mysql_backup_$(date +%F_%T)
mkdir /var/lib/mysql
chown mysql:mysql /var/lib/mysql

然后再初始化。

这样启动mysql -uroot -p --socket=/var/lib/mysql/mysql.sock




宿主机(Windows)通过 NAT 模式连接虚拟机的正确姿势不是直接用虚拟机IP,而是用映射到宿主机的端口。


你当前用的:

mysql -uroot -proot -h192.168.200.129 -P3306

这是虚拟机的私有IP,NAT模式下外部访问是被隔离的,直接连这个IP不通。


正确做法:

  1. 在 VMware 的 NAT 设置里添加端口转发规则,把宿主机的某个端口(比如3306)转发到虚拟机的3306端口。
  2. 宿主机连接时,用 127.0.0.1(或者 localhost)加端口访问。

例如:

mysql -uroot -proot -h127.0.0.1 -P3306

如果你想用虚拟机的私有IP直接连接,建议改成桥接网卡。


总结:

  • 你的 MySQL 配置没问题了
  • NAT模式下宿主机访问虚拟机MySQL必须用端口转发+127.0.0.1
  • 或切换桥接,直接用虚拟机IP访问

✅ 查看容器日志命令:

docker logs -f dreamy_montalcini
  • -f 表示 实时追踪日志,相当于 tail -f 的效果
  • 如果你只想看最近的日志行数,比如 100 行:
docker logs --tail 100 dreamy_montalcini

✅ 还可以用容器 ID:

docker logs -f 14e040066165

如果mysql忘记密码了怎么办

✅ 第一步:找出 mysqld_safe 路径

所以系统找不到 mysqld_safe 命令。请执行下面命令找出它在哪:

sudo find / -name mysqld_safe

它一般可能在 /usr/bin/mysqld_safe/usr/local/mysql/bin/mysqld_safe

找到后,比如它在 /usr/local/mysql/bin/mysqld_safe,那你这样启动 MySQL 跳过权限:

sudo /usr/local/mysql/bin/mysqld_safe --skip-grant-tables --skip-networking &

然后 等待几秒钟(5-10s) 再执行:

mysql -uroot

[root@localhost ~]# sudo /usr/local/mysql/bin/mysqld_safe –pid-file=/var/run/mysqld/mysqld.pid &
[1] 56721
[root@localhost ~]# 2025-07-04T13:53:36.378946Z mysqld_safe Logging to ‘/var/log/mariadb/mariadb.log’.
2025-07-04T13:53:36.468224Z mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql

[root@localhost ~]# sudo /usr/local/mysql/bin/mysqld_safe –pid-file=/var/run/mysqld/mysqld.pid &
[2] 56935
[root@localhost ~]# 2025-07-04T13:53:48.624877Z mysqld_safe Logging to ‘/var/log/mariadb/mariadb.log’.
2025-07-04T13:53:48.718002Z mysqld_safe A mysqld process already exists

[2]+ 退出 1 sudo /usr/local/mysql/bin/mysqld_safe –pid-file=/var/run/mysqld/mysqld.pid
[root@localhost ~]# ps aux | grep mysqld
root 56721 0.0 0.0 241292 632 pts/1 S 21:53 0:00 sudo /usr/local/mysql/bin/mysqld_safe –pid-file=/var/run/mysqld/mysqld.pid
root 56723 0.1 0.0 113412 452 pts/1 S 21:53 0:00 /bin/sh /usr/local/mysql/bin/mysqld_safe –pid-file=/var/run/mysqld/mysqld.pid
mysql 56861 22.2 18.1 1774388 339036 pts/1 Sl 21:53 0:03 /usr/local/mysql/bin/mysqld –basedir=/usr/local/mysql –datadir=/var/lib/mysql –plugin-dir=/usr/local/mysql/lib/plugin –user=mysql –log-error=/var/log/mariadb/mariadb.log –pid-file=/var/run/mysqld/mysqld.pid –socket=/var/lib/mysql/mysql.sock
root 57071 0.0 0.0 112828 988 pts/1 S+ 21:53 0:00 grep –color=auto mysqld
[root@localhost ~]#

数据库修改策略权限以及让远程宿主机连接

mysql -uroot -p

SET GLOBAL validate_password_policy = LOW;
SET GLOBAL validate_password_length = 4;

mysql> GRANT ALL PRIVILEGES ON . TO ‘root‘@’%’ IDENTIFIED BY ‘root’ WITH GRANT OPTION;
Query OK, 0 rows affected, 1 warning (0.01 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)

mysql> select user, host from mysql.user where user=’root’;
+——+————+
| user | host |
+——+————+
| root | % |
| root | 172.17.0.5 |
| root | localhost |
+——+————+
3 rows in set (0.00 sec)

你给的 Spring Boot 配置文件(application.yml)大致结构是对的,不过结合你之前报错信息(MySQL 连接时出现 CommunicationsException,SSL握手失败)和你的配置,有几点需要确认和调整:


1. MySQL 连接 URL 推荐加上参数,关闭 SSL

默认情况下 MySQL Connector 可能会尝试用 SSL 连接,而你的报错就是 SSL 握手失败:

Caused by: javax.net.ssl.SSLException: Received fatal alert: internal_error

解决办法是在 URL 中明确关闭 SSL,改成:

spring:
  datasource:
    url: jdbc:mysql://192.168.200.129:3306/tensquare_user?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
  • useSSL=false 关闭 SSL 连接
  • serverTimezone=Asia/Shanghai 解决时区警告(可选)
  • characterEncoding=UTF-8 保持编码一致(注意大小写)

微服务持续集成(7)-部署前端静态web网站

129机器进行npm命令打包 → 远程传输文件 →(SSH远程调用)→ 130生产服务器Nginx

为什么用 Nginx 而不是直接用 Docker?

Nginx 是专业的高性能静态文件服务器
  • 速度快、资源占用低,响应迅速
  • 支持缓存、压缩、访问控制、日志、反向代理等丰富功能
  • 易于配置访问路径、rewrite规则等
Docker 是“容器化”技术,适合运行应用程序
  • Docker 让你把应用和运行环境打包,适合部署动态应用(比如后端服务)
  • 纯静态文件没运行时依赖,没必要额外用容器包裹
  • 如果直接用 Docker 部署静态网站,往往是把 Nginx 或 Apache 装在容器里,再由容器提供静态服务 — 这就和单纯用 Nginx 差不多,只是多了容器层
安装Nginx服务器

yum install epel-release
yum -y install nginx 安装

修改nginx的端口,默认80,改为9090:

vi /etc/nginx/nginx.conf

 server {
        listen    9099 default_server;   
        listen    [::]:9099 default_server;     
        server_name  _;
        root       /usr/share/nginx/html;  

还需要关闭 selinux,将SELINUX=disabled

setenforce 0 先临时关闭
vi /etc/selinux/config 编辑文件,永久关闭 SELINUX=disabled

启动Nginx ★★

systemctl enable nginx 设置开机启动
systemctl start nginx 启动
systemctl stop nginx 停止
systemctl restart nginx 重启
systemctl status nginx.service 查看运行状态

[Welcome to CentOS] (http://192.168.200.130:9099/)

在Jenkins的服务器里 安装NodeJS插件

前端在本机的位置:C:\Users\Pluminary\Desktop\QianDuan\tensquareAdmin

Jenkins配置Nginx服务器
Dashboard → Manage Jenkins → Tools → NodeJS新增

别名:nodejs12
版本:NodeJS 12.3.1

创建前端流水线项目 tensquare_front

添加一个参数This project is parameterized → 名称branch 默认值master
在Jenkins里找一个凭证点击右侧zhangsan → 更新 → 就会显示有ID 复制后变成git_auth

流水线Pipeline script
//gitlab凭证
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
node{
    stage('拉取代码'){
        checkout scmGit(branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: 'http://192.168.200.132:82/lanyun_group/tensquare_front.git']])
    }
     stage('打包,部署网站') {
        //使用NodeJS的npm进行打包
        nodejs('nodejs12'){
         sh '''
             npm install
             npm run build   
        '''         
        }
     }
     //=====以下为远程调用进行项目部署========
     sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/usr/share/nginx/html', remoteDirectorySDF: false, removePrefix: 'dist', sourceFiles: 'dist/**')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}

去gitlab里面修改tensquare_front里面的config→prod.env.js

'use strict'
module.exports = {
  NODE_ENV: '"production"',
  // BASE_API: '"http://192.168.207.131:7300/mock/5c0b42c85b4c7508d4dc568c/1024"'
  BASE_API: '"http://192.168.200.130:10020"' // 管理员网关[生产]
}

去构建一下!

然后去生产服务器寻找

[root@localhost html]# cd /usr/share/nginx/html
[root@localhost html]# ll
总用量 80
-rw-r–r–. 1 root root 3650 11月 11 2022 404.html
-rw-r–r–. 1 root root 3693 11月 11 2022 50x.html
lrwxrwxrwx. 1 root root 20 7月 5 19:30 en-US -> ../../doc/HTML/en-US
-rw-r–r–. 1 root root 67646 7月 5 20:12 favicon.ico
drwxr-xr-x. 2 root root 27 7月 5 19:30 icons
lrwxrwxrwx. 1 root root 18 7月 5 19:30 img -> ../../doc/HTML/img
lrwxrwxrwx. 1 root root 25 7月 5 19:30 index.html -> ../../doc/HTML/index.html
-rw-r–r–. 1 root root 368 11月 11 2022 nginx-logo.png
lrwxrwxrwx. 1 root root 14 7月 5 19:30 poweredby.png -> nginx-logo.png
drwxr-xr-x. 6 root root 51 7月 5 20:12 static
[root@localhost html]#

回头再去访问:http://192.168.200.130:9099/ 已经成功

admin 123456

登录进去查看是否能连通之前在镜像部署的微服务后端!!![一次成功 完美!!]

Jenkins+Docker+SpringCloud微服务持续集成(下)之前是单机版

上面部署方案存在的问题:

1)一次只能选择一个微服务部署
2)只有一台生产者部署服务器
3)每个微服务只有一个实例,容错率低

初级版已完成接下来是中级版本!!

在idea项目中增加一台新的机器作为eureka注册的地方

tensquare_gathering/src/main/resources/application.yml
server: 
  port: 9002
spring: 
  application:  
    name: tensquare-gathering #指定服务名
  datasource:  
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.200.129:3306/tensquare_gathering?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
  jpa: 
    database: mysql
    show-sql: true
#Eureka客户端配置
eureka:
  client:
    service-url:
      defaultZone: http://192.168.200.130:10086/eureka,http://192.168.200.133:10086/eureka
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
    prefer-ip-address: true
tensquare_admin_service/src/main/resources/application.yml
server: 
  port: 9001
spring: 
  application:  
    name: tensquare-admin-service #指定服务名
  datasource:  
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.200.129:3306/tensquare_user?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
  jpa: 
    database: mysql
    show-sql: true

#Eureka配置
eureka:
  client:
    service-url:
      defaultZone: http://192.168.200.130:10086/eureka,http://192.168.200.133:10086/eureka
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
    prefer-ip-address: true


 # jwt参数
jwt:
  config:
    key: itcast
    ttl: 1800000
tensquare_zuul/src/main/resources/application.yml
server:
  port: 10020 # 端口
  
# 基本服务信息
spring:
  application:
    name: tensquare-zuul # 服务ID
    
# Eureka配置
eureka:
  client:
    service-url:  
      defaultZone: http://192.168.200.130:10086/eureka,http://192.168.200.133:10086/eureka
  instance:
    prefer-ip-address: true
  
# 修改ribbon的超时时间
ribbon:
  ConnectTimeout: 1500 # 连接超时时间,默认500ms
  ReadTimeout: 3000  # 请求超时时间,默认1000ms
  
  
# 修改hystrix的熔断超时时间
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 2000 # 熔断超时时长,默认1000ms
            

# 网关路由配置
zuul:
  routes:
    admin:
      path: /admin/**
      serviceId: tensquare-admin-service
    gathering:
          path: /gathering/**
          serviceId: tensquare-gathering

 # jwt参数
jwt:
  config:
    key: itcast
    ttl: 1800000
C:\Users\Pluminary\Desktop\HouDuan\tensquare_parent\tensquare_eureka_server\src\main\resources\application.yml
# 集群版
spring:
  application:
    name: EUREKA-HA

---
server:
  port: 10086
spring:
  # 指定profile=eureka-server1
  profiles: eureka-server1
eureka:
  instance:
    # 指定当profile=eureka-server1时,主机名是eureka-server1
    hostname: 192.168.66.130
  client:
    service-url:
      # 将自己注册到eureka-server1、eureka-server2这个Eureka上面去
      defaultZone: http://192.168.66.130:10086/eureka/,http://192.168.66.133:10086/eureka/

---
server:
  port: 10086
spring:
  profiles: eureka-server2
eureka:
  instance:
    hostname: 192.168.66.132
  client:
    service-url:
      defaultZone: http://192.168.66.130:10086/eureka/,http://192.168.66.133:10086/eureka/

然后更改提交push到gitlab

设计Jenkins集群项目的构建参数

  • 安装Extended Choice Parameter插件支持多选框

  • 新建一个tensquare_back_cluster的item
    流水线选择piplin…SCM;git的url填gitlab里的后端地址

  • 第一步安装好插件后 就可以在This project... 中看到Choice Parameter
    先添加一个String Parameter:名称branch 默认值master 描述请输入分支名称

  • 再添加参数选择多选Extended Choice Parameter:名称project_name 描述:请选择需要部署的微服务;勾选Basic Parameter Types → Check Boxes → Number of Visble Items:4 → Delimiter,(用逗号去隔开) → 点击下面的Choose Source for Value → Value为tensquare_eureka_server@10086,tensquare_zuul@10020,tensquare_admin_service@9001,tensquare_gathering@9002 → 点击Choose Source for Default Value(选择默认值):tensquare_eureka_server@10086 → 点击ChooseSourceforValueDescription(描述备注) Description:注册中心,服务网关,认证中心,活动微服务 👉 应用保存 → 构建就可以看到

    ★★ 请注意 配置多选的时候 Jenkins那里一定不要多加空格什么的!

  • 更新Jenkinsfile代码

    //git凭证ID
    def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
    //git的url地址
    def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
    //镜像的版本号
    def tag = "latest"
    //Harbor的url地址
    def harbor_url = "192.168.200.131:85"
    //镜像库项目名称
    def harbor_project = "tensquare"
    //harbor的登录凭证ID
    def harbor_auth = "f83725d3-d5fc-4faf-8ac1-1a56487dfc7b"
    
    
    node {
        // 获取当前选择的项目名称 要用逗号去切割
        def selectedProjectNames = "${project_name}".split(",")
    
       stage('拉取代码') {
          checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
       }
       stage('代码审查') {
          for(int i = 0; i < selectedProjectNames.length; i++){
              // tensquare_eureka_server@10086
               def projectInfo = selectedProjectNames[i];
              // 当前遍历的项目名称
               def currentProjectName = "${projectInfo}".split("@")[0]
              // 当前遍历的项目端口
               def currentProjectPort = "${projectInfo}".split("@")[1]
    
              //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
              def scannerHome = tool 'sonar-scanner'
              //引用当前JenkinsSonarQube环境 Jenkins的System里
              withSonarQubeEnv('sonarqube'){
                  // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
                  sh """
                        cd ${currentProjectName}
                        ${scannerHome}/bin/sonar-scanner
                    """
              }
          }
        }
        stage('编译,安装公共子工程'){
            sh "mvn -f tensquare_common clean install"
        }
        stage('编译,打包微服务工程'){
            sh "mvn -f ${project_name} clean package dockerfile:build"
            // 定义镜像名称
            def imageName = "${project_name}:${tag}"
    
            // 对镜像打上标签
            sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"
    
            // 把镜像推送到Harbor(要登录 输入账号密码 不要写在脚本文件 把harbor账号密码用jenkins全局凭证)
            withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
                // 登录到Harbor
                sh "docker login -u ${username} -p  ${password} ${harbor_url}"
    
                //镜像上传
                sh "docker push ${harbor_url}/${harbor_project}/${imageName}"
    
                sh "echo '🔥🔥镜像上传成功✅✅'"
            }
            // 部署应用 发送命令调用生产服务器的shell脚本文件 完成所有的微服务项目部署行为
            // deploy.sh 是触发远程脚本文件(镜像拉取和容器创建的行为)
            sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project $project_name $tag $port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
        }
    }
    

多个项目打包及构建上传镜像

  • 更新Jenkinsfile文件
//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
//镜像的版本号
def tag = "latest"
//Harbor的url地址
def harbor_url = "192.168.200.131:85"
//镜像库项目名称
def harbor_project = "tensquare"
//harbor的登录凭证ID
def harbor_auth = "f83725d3-d5fc-4faf-8ac1-1a56487dfc7b"


node {
    // 获取当前选择的项目名称 要用逗号去切割
    def selectedProjectNames = "${project_name}".split(",")

   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
   stage('代码审查') {
      for(int i=0;i<selectedProjectNames.length;i++){
          // tensquare_eureka_server@10086
           def projectInfo = selectedProjectNames[i];
          // 当前遍历的项目名称
           def currentProjectName = "${projectInfo}".split("@")[0]
          // 当前遍历的项目端口
           def currentProjectPort = "${projectInfo}".split("@")[1]

          //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
          def scannerHome = tool 'sonar-scanner'
          //引用当前JenkinsSonarQube环境 Jenkins的System里
          withSonarQubeEnv('sonarqube'){
              // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
              sh """
                    cd ${currentProjectName}
                    ${scannerHome}/bin/sonar-scanner
                """
          }
      }
    }
    stage('编译,安装公共子工程'){
        sh "mvn -f tensquare_common clean install"
    }
    stage('编译,打包微服务工程,上传镜像') {
        for (int i = 0; i < selectedProjectNames.length; i++) {
            //tensquare_eureka_server@10086
            def projectInfo = selectedProjectNames[i];
            //当前遍历的项目名称
            def currentProjectName = "${projectInfo}".split("@")[0]
            //当前遍历的项目端口
            def currentProjectPort = "${projectInfo}".split("@")[1]

            sh "mvn -f ${currentProjectName} clean package dockerfile:build"

            //定义镜像名称
            def imageName = "${currentProjectName}:${tag}"

            // 对镜像打上标签
            sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"

            // 把镜像推送到Harbor(要登录 输入账号密码 不要写在脚本文件 把harbor账号密码用jenkins全局凭证)
            withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
                // 登录到Harbor
                sh "docker login -u ${username} -p  ${password} ${harbor_url}"

                //镜像上传
                sh "docker push ${harbor_url}/${harbor_project}/${imageName}"

                sh "echo '🔥🔥镜像上传成功✅✅'"
            }
            // 部署应用 发送命令调用生产服务器的shell脚本文件 完成所有的微服务项目部署行为
            // deploy.sh 是触发远程脚本文件(镜像拉取和容器创建的行为)
            sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project $project_name $tag $port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
        }
    }
}

把Eureka注册中心集群部署到多台服务器

Dashboard → Manage Jenkins → 下面的SSH Server → 增加一台133的机器
测试之前需要把Jenkins服务的公钥拷贝到133上面

在Jenkins的129机器
[root@localhost ~]# ssh-copy-id 192.168.200.133 [Product_slave]

SSH Server→【应用+Save】
Name:slave_server
Hostname:192.168.200.133
Username:root
Remote Directory:/

继续在Jenkins的tensquare_back_cluster中的ConfigureExtended Choice Parameter的下面添加参数 → 继续添加Extended Choice Parameter → 在里面继续添加Name:publish_server;Description:请选择需要部署的服务器 → 勾选Basic Parameter Types, Parameter Type:Check BoxesNumber of Visible Items:2, Delimiter:, 下面的Choose Source for ValueValue:master_server,slave_server → 下面的Default Value:master_server → 下面的Description:主节点,从节点 → 应用+Save

  • 更新了Jenkinsfile文件
//git凭证ID
def git_auth = "ca22e56f-0ecc-4fdc-965d-01e329a0b68a"
//git的url地址
def git_url = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
//镜像的版本号
def tag = "latest"
//Harbor的url地址
def harbor_url = "192.168.200.131:85"
//镜像库项目名称
def harbor_project = "tensquare"
//harbor的登录凭证ID
def harbor_auth = "f83725d3-d5fc-4faf-8ac1-1a56487dfc7b"


node {
    // 获取当前选择的项目名称 要用逗号去切割
    def selectedProjectNames = "${project_name}".split(",")
    // 获取当前选择的服务器名称
    def selectedServers = "${publish_server}".split(",")

   stage('拉取代码') {
      checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
   }
   stage('代码审查') {
      for(int i=0;i<selectedProjectNames.length;i++){
          // tensquare_eureka_server@10086
           def projectInfo = selectedProjectNames[i];
          // 当前遍历的项目名称
           def currentProjectName = "${projectInfo}".split("@")[0]
          // 当前遍历的项目端口
           def currentProjectPort = "${projectInfo}".split("@")[1]

          //定义当前的Jenkins的sonarqubeScanner工具 Jenkins的全局Tools里
          def scannerHome = tool 'sonar-scanner'
          //引用当前JenkinsSonarQube环境 Jenkins的System里
          withSonarQubeEnv('sonarqube'){
              // 需要进入某个项目 在Jenkins选择什么项目就cd什么项目
              sh """
                    cd ${currentProjectName}
                    ${scannerHome}/bin/sonar-scanner
                """
          }
      }
    }
    stage('编译,安装公共子工程'){
        sh "mvn -f tensquare_common clean install"
    }
    stage('编译,打包微服务工程,上传镜像') {
        for (int i = 0; i < selectedProjectNames.length; i++) {
            //tensquare_eureka_server@10086
            def projectInfo = selectedProjectNames[i];
            //当前遍历的项目名称
            def currentProjectName = "${projectInfo}".split("@")[0]
            //当前遍历的项目端口
            def currentProjectPort = "${projectInfo}".split("@")[1]

            sh "mvn -f ${currentProjectName} clean package dockerfile:build"

            //定义镜像名称
            def imageName = "${currentProjectName}:${tag}"

            // 对镜像打上标签
            sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"

            // 把镜像推送到Harbor(要登录 输入账号密码 不要写在脚本文件 把harbor账号密码用jenkins全局凭证)
            withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
                // 登录到Harbor
                sh "docker login -u ${username} -p  ${password} ${harbor_url}"

                //镜像上传
                sh "docker push ${harbor_url}/${harbor_project}/${imageName}"

                sh "echo '🔥🔥镜像上传成功✅✅'"
            }
            //遍历所有服务器,分别部署
            for (int j = 0; j < selectedServers.length; j++) {
                //获取当前遍历的服务器名称
                def currentServerName = selectedServers[j]

                //加上的参数格式:--spring.profiles.active=eureka-server1/eureka-server2
                def activeProfile = "--spring.profiles.active="

                //根据不同的服务名称来读取不同的Eureka配置信息
                if (currentServerName == "master_server") {
                    activeProfile = activeProfile + "eureka-server1"
                } else if (currentServerName == "slave_server") {
                    activeProfile = activeProfile + "eureka-server2"
                }

                // 部署应用 发送命令调用生产服务器的shell脚本文件 完成所有的微服务项目部署行为
                // deploy.sh 是触发远程脚本文件(镜像拉取和容器创建的行为)
                sshPublisher(publishers: [sshPublisherDesc(configName: "${currentServerName}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deployCluster.sh $harbor_url $harbor_project $currentProjectName $tag $currentProjectPort $activeProfile", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
            }
        }
    }
}
  • 将之前的deploy.sh换成了deployCluster.sh
#! /bin/sh
#接收外部参数
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5
profile=$6

imageName=$harbor_url/$harbor_project_name/$project_name:$tag

echo "$imageName"

#查询容器是否存在,存在则删除
containerId=`docker ps -a | grep -w ${project_name}:${tag}  | awk '{print $1}'`

if [ "$containerId" !=  "" ] ; then
    #停掉容器
    docker stop $containerId

    #删除容器
    docker rm $containerId
    
    echo "成功删除容器"
fi

#查询镜像是否存在,存在则删除
imageId=`docker images | grep -w $project_name  | awk '{print $3}'`

if [ "$imageId" !=  "" ] ; then
      
    #删除镜像
    docker rmi -f $imageId
    
    echo "成功删除镜像"
fi

# 登录Harbor
docker login -u eric -p Eric123456 $harbor_url

# 下载镜像
docker pull $imageName

# 启动容器
docker run -di -p $port:$port $imageName $profile

echo "容器启动成功"
  • master_server,slave_server将130生产的机器master机器里 放入deployCluster.sh 是在这个目录/opt/jenkins_shell/里面
    然后加上权限

    [root@localhost ~]# cd /opt/jenkins_shell/
    [root@localhost jenkins_shell]# chmod +x deployCluster.sh

  • 然后在133机器上slave机器 按照上一步的操作拖进去 然后加入权限

    [root@localhost ~]# cd /opt/jenkins_shell/
    [root@localhost jenkins_shell]# chmod +x deployCluster.sh

[Eureka] (http://192.168.200.130:10086/) + [Eureka] (http://192.168.200.133:10086/)

  • 然后再去把剩下的都构建了

    project_name

    请选择需要部署的微服务

    注册中心
    服务网关
    认证中心
    活动微服务

    publish_server

    请选择需要部署的服务器

    主节点
    从节点

Nginx + Zuul集群实现高可用网关

更改133的nginx配置

vi /etc/nginx/nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    include /etc/nginx/conf.d/*.conf;

    ### 新增负载均衡配置
    upstream zuulServer {
        server 192.168.200.130:10020 weight=1;
        server 192.168.200.133:10020 weight=1;
    }

    server {
        listen       85 default_server;
        listen       [::]:85 default_server;
        server_name  _;·
        root         /usr/share/nginx/html;

        include /etc/nginx/default.d/*.conf;

        location / {
            proxy_pass http://zuulServer/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }
    }
}

重启Nginx:systemctl restart nginx
修改前端Nginx的访问地址tensquare_front → config → prod.env.js

'use strict'
module.exports = {
  NODE_ENV: '"production"',
  // BASE_API: '"http://192.168.207.131:7300/mock/5c0b42c85b4c7508d4dc568c/1024"'
  BASE_API: '"http://192.168.200.133:85"' // 管理员网关
}

基于Kubernetes/K8S构建Jenkins持续集成平台(上)

Jenkins的Master-Slave分布式构建
什么是Master-Slave分布式构建

Jenkins的Master-Slave分布式构建,就是通过将构建过程分配到从属Slave节点上,从而减轻Master节 点的压力,而且可以同时构建多个,有点类似负载均衡的概念。

如何实现Master-Slave分布式构建
  • 开启代理程序的TCP端口Dashboard → Manage Jenkins → Security → 代理:随机选取

  • 新建节点 Dashboard → Manage Jenkins → Nodes → New node → 节点名称: slave1 勾选Permanent Agent → create → 我的Jenkins主节点在101机器,从节点就放在102机器吧 → 去102机器cd /root; mkdir jenkins → 远程工作目录:/root/jenkins → 用法;启动方式;可用性都是默认第一个 → 点进去刚刚创建的slave →↓

    Run from agent command line: (Unix) 
    # 下载agent.jar
    curl -sO http://192.168.200.133:8888/jnlpJars/agent.jar
    
    java -jar agent.jar -url http://192.168.200.129:8888/ -secret 85f68f679a60139d7867571d577299c3c7d10f913acfca589bb0b9018b86e14d -name slave1 -webSocket -workDir "/root/jenkins"
    

    想要这个 需要下载个agent.jar包传到102的/root机器中

✅ 最简单解决方案(推荐)

方案一:从 Jenkins 主机上下载,再用 SCP 复制到 agent 机器

在 Jenkins 主机(192.168.200.129)上执行:

curl -sO http://localhost:8888/jnlpJars/agent.jar

然后从主机 scp 到 192.168.200.133

scp agent.jar root@192.168.200.133:/root/

登录密码或配置了免密都行。

Kubernetes实现Master-Slave分布式构建方案

传统Jenkins的Master-Slave方案的缺陷
  • Master 每个 节点发生单点故障时,整个流程都不可用了
  • Slave节点的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置致管理起来非常不方便,维护起来也是比较费劲
  • 资源分配不均衡,有的 Slave节点要运行的job出现排队等待,而有的Slave节点处于空闲状态
  • 资源浪费,每台 掉资源 Slave节点可能是实体机或者VM,当Slave节点处于空闲状态时,也不会完全释放

以上种种问题,我们可以引入Kubernates来解决!

K8S是对主从分布式架构的优化!!!

Kubernates简介

Kubernetes(简称,K8S)是Google开源的容器集群管理系统,在Docker技术的基础上,为容器化的 应用提供部署运行、资源调度、服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理的 便捷性。 其主要功能如下:

  • 使用 Docker对应用程序包装(package)、实例化(instantiate)、运行(run)。
  • 以集群的方式运行、管理跨机器的容器。以集群的方式运行、管理跨机器的容器。
  • 解决 Docker跨机器容器之间的通讯问题。解决Docker跨机器容器之间的通讯问题。
  • Kubernetes 的自我修复机制使得容器集群总是运行在用户期望的状态。

Kubernates+Docker+Jenkins持续集成方案好处

  • 服务高可用:当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。
  • 动态伸缩,合理使用资源:每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况。
  • 扩展性好 :当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node 到集群中,从而实现扩展。

Kubeadm安装Kubernetes

Kubernetes的架构

  • API Server :用于暴露Kubernetes API,任何资源的请求的调用操作都是通过kube-apiserver提供的接 口进行的。
  • Etcd:是Kubernetes提供默认的存储系统,保存所有集群数据,使用时需要为etcd数据提供备份计 划。
  • Controller-Manager:作为集群内部的管理控制中心,负责集群内的Node、Pod副本、服务端点 (Endpoint)、命名空间(Namespace)、服务账号(ServiceAccount)、资源定额 (ResourceQuota)的管理,当某个Node意外宕机时,Controller Manager会及时发现并执行自动化 修复流程,确保集群始终处于预期的工作状态。
  • Scheduler:监视新创建没有分配到Node的Pod,为Pod选择一个Node。
  • Kubelet:负责维护容器的生命周期,同时负责Volume和网络的管理
  • Kube proxy:是Kubernetes的核心组件,部署在每个Node节点上,它是实现Kubernetes Service的通 信与负载均衡机制的重要组件。
主机名称 IP地址 安装的软件
代码托管服务器 192.168.200.132 Gitlab-12.4.2
Docker仓库服务器 192.168.200.131 Harbor1.9.2
k8s-master 192.168.200.129 kube-apiserver、kube-controller-manager、kube scheduler、docker、etcd、calico,NFS
k8s-node1 192.168.200.130 kubelet、kubeproxy、Docker18.06.1-ce
k8s-node2 192.168.200.133 kubelet、kubeproxy、Docker18.06.1-ce

三台机器都需要完成(结束我会提示)

修改三台机器的hostname及hosts文件

【输入hostname即可查看是否改成功】
hostnamectl set-hostname k8s-master 【129】
hostnamectl set-hostname k8s-node1 【130】
hostnamectl set-hostname k8s-node2 【133】

【将id映射的hostname进行关联】
[输入 cat /etc/hosts 查看是否成功]
cat >>/etc/hosts<<EOF
192.168.200.129 k8s-master
192.168.200.130 k8s-node1
192.168.200.133 k8s-node2
EOF

效果图

[root@localhost ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.200.129 k8s-master
192.168.200.130 k8s-node1
192.168.200.133 k8s-node2

关闭防火墙和关闭SELinux

systemctl stop firewalld
systemctl disable firewalld

setenforce 0 临时关闭
vi /etc/sysconfig/selinux 永久关闭
改为SELINUX=disabled

关闭 SELinux 可以避免一些服务(如 Jenkins、SonarQube、Docker)因为权限问题无法访问网络或文件,但不是永久推荐的安全实践。

设置系统参数

设置允许路由转发,不对bridge的数据进行处理
创建文件

vi /etc/sysctl.d/k8s.conf

内容如下:

net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
vm.swappiness = 0

执行文件

sysctl -p /etc/sysctl.d/k8s.conf

kube-proxy开启ipvs的前置条件
cat > /etc/sysconfig/modules/ipvs.modules <<EOF
#!/bin/bash
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv4
EOF

chmod 755 /etc/sysconfig/modules/ipvs.modules && bash   ☆☆
/etc/sysconfig/modules/ipvs.modules && lsmod | grep -e ip_vs -e nf_conntrack_ipv4
所有节点关闭swap

swapoff -a 临时关闭
vi /etc/fstab 永久关闭

注释掉以下字段
/dev/mapper/cl-swap swap swap defaults 0 0

安装kubelet、kubeadm、kubectl
  • kubeadm:用来初始化集群的指令。
  • kubelet:在集群中的每个节点上用来启动 pod 和 container 等。
  • kubectl: 清空yum缓存 用来与集群通信的命令行工具。
清空yum缓存

yum clean all

设置yum安装源
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=0
repo_gpgcheck=0
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg 
https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

安装:yum install -y kubelet kubeadm kubectl

kubelt 设置开机启动(注意:先不启动,现在启动的话会报错)

systemctl start kubelet 启动 ★★

systemctl enable kubelet 开机启动

查看版本

kubelet --version → Kubernetes v1.28.2

结束!

Master节点需要完成 [129主节点]

  • 运行初始化命令
kubeadm init --kubernetes-version=1.28.2 \
--apiserver-advertise-address=192.168.200.129 \
--image-repository registry.aliyuncs.com/google_containers \
--service-cidr=10.1.0.0/16 \
--pod-network-cidr=10.244.0.0/16

如何重新初始化?

解决步骤

1. 清理旧集群残留

先执行 kubeadm reset 清理当前节点的 Kubernetes 配置和状态:

kubeadm reset -f
kubeadm reset

这个命令会清理 kubelet 配置文件、证书、容器网络和所有 Kubernetes 相关状态。


2. 清理网络相关残留

清理 CNI 网络配置和容器残留:

rm -rf /etc/cni/net.d
rm -rf /var/lib/cni/
rm -rf /var/lib/kubelet/*

3. 确认 containerd 服务启动

确保 containerd 运行:

systemctl restart containerd
systemctl status containerd

4. 重新初始化 Kubernetes

执行你之前的初始化命令:

kubeadm init --kubernetes-version=1.28.2 \
  --apiserver-advertise-address=192.168.200.129 \
  --image-repository registry.aliyuncs.com/google_containers \
  --service-cidr=10.1.0.0/16 \
  --pod-network-cidr=10.244.0.0/16

5. 配置 kubeconfig

初始化完成后,按照提示将配置文件复制到用户目录:

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

6. 部署网络插件

根据你使用的网络插件(比如 Flannel、Calico),按照官方文档部署。以 Flannel 为例:

kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

7. 验证

查看节点和 Pod 状态:

kubectl get nodes
kubectl get pods -n kube-system
kubeadm join 192.168.200.129:6443 --token q54yl3.xgho0e6o5080hnnf \
        --discovery-token-ca-cert-hash sha256:b8f6a58b65ad6bcfec65e5ed6ab5efac95f3a4ee82b2c388a87b20c3e813079b



总结

你之前的配置文件和状态未清理,导致不能重新初始化。先用 kubeadm reset 清理,再重新初始化。这样会自动生成 kubelet 配置文件和 kubelet.conf,解决你遇到的启动失败问题。

问题: [ERROR CRI]: container runtime is not running: output: E0706 21:24:43.293740 26106 remote_runtime.go:616] “Status from runtime service failed” err=”rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial unix /var/run/containerd/containerd.sock: connect: no such file or directory"“ time=”2025-07-06T21:24:43+08:00” level=fatal msg=”getting status of runtime: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial unix /var/run/containerd/containerd.sock: connect: no such file or directory"“ , error: exit status 1 [preflight] If you know what you are doing, you can make a check non-fatal with --ignore-preflight-errors=... To see the stack trace of this error execute with –v=5 or higher [root@k8s-master ~]#

✅ 立即解决步骤(推荐直接复制粘贴执行)

1️⃣ 安装 containerd

yum install -y containerd

你这里显示的是一个非标准的 containerd 配置模板(很可能来自旧版本或 docker 提供的 containerd),缺失了完整配置结构和 SystemdCgroup 字段,所以这不是我们需要的配置。


✅ 正确做法是:用标准 containerd 命令生成完整配置

请执行以下命令替换掉旧配置:


🔁 步骤 1:重新生成 containerd 的标准配置文件

rm -f /etc/containerd/config.toml  # 删除旧文件
containerd config default > /etc/containerd/config.toml

🔧 步骤 2:编辑配置文件,修改 SystemdCgroup = true

vi /etc/containerd/config.toml

在文件中搜索这一行(用 /SystemdCgroup):

SystemdCgroup = false

将它修改为:

SystemdCgroup = true

🔁 步骤 3:重启 containerd

systemctl daemon-reexec
systemctl restart containerd
systemctl enable containerd

✅ 步骤 4:验证是否运行中

systemctl status containerd

确保输出中看到:Active: active (running)


然后你就可以继续 kubeadm init 了。如果你愿意,我可以一步步带你确认配置生效。现在请先执行

一、清理旧的集群数据

你可以执行下面命令,清理之前的 kubeadm 初始化信息:

kubeadm reset




你这边的核心问题是:

❌ containerd 没有启动,导致 kubeadm 报错
"connect: no such file or directory" → /var/run/containerd/containerd.sock

这说明 containerd 根本没跑起来,Kubernetes 无法连接它。


✅ 解决思路:修复 containerd 的配置 + 启动 containerd

我们手把手来操作👇


🧹 第一步:清空 containerd 的配置文件并重新生成

rm -f /etc/containerd/config.toml
containerd config default > /etc/containerd/config.toml

✅ 这一步会恢复官方默认配置。

✏️ 第二步:使用 vi(所有 Linux 通用)

vi /etc/containerd/config.toml

查找这一段:

[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.k8s.io/pause:3.6"

把它改成:

[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"

✅ 保存并退出🔄 第三步:重启 containerd

systemctl daemon-reexec
systemctl restart containerd

✅ 第四步:确认 containerd 已成功启动

systemctl status containerd

看输出里有 Active: active (running),说明启动成功。🧪 第五步:验证 socket 文件是否存在

ls -l /var/run/containerd/containerd.sock

应该能看到类似:

srw-rw---- 1 root root 0 Jul 6 22:06 /var/run/containerd/containerd.sock

如果没看到,说明 containerd 没真正运行成功。

🧱 第六步:重新执行 kubeadm init

kubeadm init --kubernetes-version=1.28.2 \
  --apiserver-advertise-address=192.168.200.129 \
  --image-repository registry.aliyuncs.com/google_containers \
  --service-cidr=10.1.0.0/16 \
  --pod-network-cidr=10.244.0.0/16

若是token忘记了 可以执行直接生成一个新的
kubeadm token create --print-join-command

[root@k8s-master calico]# kubeadm token create --print-join-command
kubeadm join 192.168.200.129:6443 --token jn3083.g1lz52mo680wiic9 --discovery-token-ca-cert-hash sha256:d03d798679d90f348b1cf7a1c12d5d1bfcbe2bd18e4a1aec97055b60c0f042ca

此时去从节点生成可能会有报错你这个错误说明当前节点(k8s-node1
之前已经执行过 kubeadm join 或者其他 kubeadm 相关操作,留下了配置文件或端口被占用,不能直接再次执行 kubeadm join


❗报错详情分析:

[ERROR FileAvailable--etc-kubernetes-kubelet.conf]: /etc/kubernetes/kubelet.conf already exists
[ERROR Port-10250]: Port 10250 is in use
[ERROR FileAvailable--etc-kubernetes-pki-ca.crt]: /etc/kubernetes/pki/ca.crt already exists

这些错误说明:

  • /etc/kubernetes/kubelet.conf 文件存在 —— 说明 kubelet 被配置过。
  • 10250 端口已被占用 —— kubelet 服务正在运行。
  • /etc/kubernetes/pki/ca.crt 文件存在 —— 集群证书存在,可能是之前加入过失败或残留文件。

✅ 解决方法(清理残留再重新 join):

你可以先在 node 节点执行 重置操作

kubeadm reset -f

然后再清理一下配置和 iptables 规则(可选但推荐):

rm -rf /etc/kubernetes/pki
rm -rf /etc/kubernetes/kubelet.conf
rm -rf /var/lib/kubelet/*
iptables -F
systemctl restart kubelet

然后重新执行你之前的 kubeadm join 命令:

kubeadm join 192.168.200.129:6443 --token eoxdev.8tkpz5csc7x1mqgc \
        --discovery-token-ca-cert-hash sha256:6acf9c7e6c11bb973bd57404f9ec28b93ab5b48f8a50f0213bc389bbfb4bf68f


一切顺利的话,k8s 就能初始化成功了!

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run “kubectl apply -f [podnetwork].yaml” with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.200.129:6443 –token zjoxjc.f58nh7lixocvjpgj
–discovery-token-ca-cert-hash sha256:3cbdd81868c4470767b5c82af37fb63c2240c98e5f00eb0458df99350cabb7b3
[root@k8s-master ~]#

kubeadm join 192.168.200.129:6443 --token zjoxjc.f58nh7lixocvjpgj \
        --discovery-token-ca-cert-hash sha256:3cbdd81868c4470767b5c82af37fb63c2240c98e5f00eb0458df99350cabb7b3
  • 按照它的提示逐步修改k8s配置文件 配置 kubectl 工具
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
  • 安装Calico
mkdir k8s
cd k8s
wget https://docs.projectcalico.org/v3.10/getting-started/kubernetes/installation/hosted/kubernetes-datastore/calico-networking/1.7/calico.yaml
// 上面的别用了 用这个新的
wget https://jiangstudy.online:8081/sources/calico.yaml
sed -i 's/192.168.0.0/10.244.0.0/g' calico.yaml  【改地址 分给prod的虚拟地址】

kubectl apply -f calico.yaml

或者ai说的

# 1. 创建工作目录
mkdir -p ~/k8s && cd ~/k8s

# 2. 下载 Calico 清单文件(推荐使用最新版本,而非 v3.10)
wget https://docs.projectcalico.org/manifests/calico.yaml -O calico.yaml

# 3. 修改 POD CIDR(根据您的集群规划)
# 注意:必须与 kubeadm init 时指定的 --pod-network-cidr 一致
sed -i 's|192.168.0.0/16|10.244.0.0/16|g' calico.yaml

# 4. 部署 Calico
kubectl apply -f calico.yaml

# 5. 验证安装
kubectl get pods -n kube-system -w
  • 等待几分钟,查看所有Pod的状态,确保所有Pod都是Running状态

kubectl get pod --all-namespaces -o wide

你这个 calico.yaml很老的版本 v3.10,其中用的是 apiextensions.k8s.io/v1beta1早在 Kubernetes v1.22 中就被移除了,你现在用的是 Kubernetes 1.28,自然就出错了。


✅ 正确做法:使用 Calico 官方支持 Kubernetes v1.28 的最新 YAML


https://docs.projectcalico.org/manifests/calico.yaml浏览器访问然后去/root/k8s里面进行全部替换成新版本

一键下载安装推荐版本:

curl https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml -O

Calico v3.26 支持 Kubernetes v1.28,放心使用

然后再修改 Pod CIDR

sed -i 's/192.168.0.0/10.244.0.0/g' calico.yaml
  1. 建议修改镜像源为阿里云(国内可拉取)
    例如将以下几行(在 calico.yaml 中):
image: calico/cni:v3.26.1

替换为:

image: registry.cn-hangzhou.aliyuncs.com/chenby/calico_cni:v3.26.1

最后重新部署:

kubectl apply -f calico.yaml

✅ 检查 Calico 状态

部署完大约 10~30 秒后执行:

kubectl get pods -n kube-system

正常会看到 calico-nodecalico-kube-controllers 都处于 Running 状态。




傻卵ai和scdn 查半天都没用 还不如B站up的方法[一遍通]

【kubernetes】Calico组件的安装-云途运维

以下步骤只在master01执行:相关的yaml已上传文件资源服务器,需要下载下来使用:

mkdir /root/calico && cd /root/calico/
wget https://jiangstudy.online:8081/sources/calico.yaml

修改POD网段 说明:pod网段即为主机规划时的网段。

POD_SUBNET=`cat /etc/kubernetes/manifests/kube-controller-manager.yaml | grep cluster-cidr= | awk -F= '{print $NF}'`

sed -i "s#POD_CIDR#${POD_SUBNET}#g" calico.yaml

部署calico
kubectl apply -f calico.yaml

查看状态
kubectl get node
kubectl get pods -n kube-system -w

Slave节点需要完成

  • 让所有节点让集群环境

    使用之前Master节点产生的命令加入集群

    kubeadm join 192.168.200.129:6443 --token eoxdev.8tkpz5csc7x1mqgc \
            --discovery-token-ca-cert-hash sha256:6acf9c7e6c11bb973bd57404f9ec28b93ab5b48f8a50f0213bc389bbfb4bf68f
    
  • 启动kubelet

    systemctl start kubelet 启动
    systemctl status kubelet 查看状态

  • 回到Master节点查看,如果Status全部为Ready,代表集群环境搭建成功!!!

    kubectl get nodes

你的这个报错:

validate service connection: CRI v1 runtime API is not implemented for endpoint "unix:///var/run/containerd/containerd.sock"

说明 containerd 的 CRI 接口没启用,这是因为你的 /etc/containerd/config.toml 配置不完整或者缺少必要字段。


✅ 立即修复步骤如下:

🔧 第一步:重新生成 config.toml 配置文件

rm -f /etc/containerd/config.toml
containerd config default > /etc/containerd/config.toml

🛠️ 第二步:修改配置启用 CRI + systemd 驱动

vim /etc/containerd/config.toml

找到这段:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = false

false 改成 true

SystemdCgroup = true

或者直接用命令修改:

sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

🚀 第三步:重启 containerd

systemctl daemon-reexec
systemctl restart containerd
systemctl status containerd

确认运行正常后:

ls /var/run/containerd/containerd.sock

如果有输出说明 socket 正常,containerd 工作 OK。


🔁 第四步:再次执行 kubeadm join

kubeadm join 192.168.200.129:6443 --token zjoxjc.f58nh7lixocvjpgj \
  --discovery-token-ca-cert-hash sha256:3cbdd81868c4470767b5c82af37fb63c2240c98e5f00eb0458df99350cabb7b3

如果重复导入节点报error

你这个问题是由于 kubeadm join 重复执行导致节点上已有残留的配置文件和端口占用,所以报错。



✅ 正确处理方式:重置 node 节点后重新 join

执行以下命令即可:

1. 重置 node 节点配置

kubeadm reset -f

2. 清理 CNI 网络配置

rm -rf /etc/cni/net.d

3. 重启相关服务

systemctl restart kubelet
systemctl restart containerd

4. 重新 join

然后再执行你原来的 join 生成的命令即可:

kubeadm join 192.168.200.129:6443 --token zjoxjc.f58nh7lixocvjpgj \
  --discovery-token-ca-cert-hash sha256:3cbdd81868c4470767b5c82af37fb63c2240c98e5f00eb0458df99350cabb7b3

[root@k8s-master k8s]# docker pull docker.io/calico/node:v3.26.1
v3.26.1: Pulling from calico/node
c998069b4a7c: Downloading [===============================================> ] 81.5MB/86.58MB
6cf92fa5251d: Download complete

[root@k8s-master k8s]# docker save docker.io/calico/node:v3.26.1 -o calico-node-v3.26.1.tar
[root@k8s-master k8s]# ctr images import –all-platforms calico-node-v3.26.1.tar
unpacking docker.io/calico/node:v3.26.1 (sha256:568fb25384d6460176b5528381a2d6fef36d110cfaddda5ae286afbf8fe15d5b)…done
[root@k8s-master k8s]# ctr images ls | grep calico/node
docker.io/calico/node:v3.26.1 application/vnd.docker.distribution.manifest.v2+json sha256:568fb25384d6460176b5528381a2d6fef36d110cfaddda5ae286afbf8fe15d5b 247.7 MiB linux/arm64 -
[root@k8s-master k8s]# kubectl -n kube-system delete pod -l k8s-app=calico-node

(你刚才就是 calico_cni 拉失败)

你可以用如下命令一口气替换:

sed -i 's@docker.io/calico/cni@registry.cn-hangzhou.aliyuncs.com/chenby/calico_cni@g' calico.yaml
sed -i 's@docker.io/calico/pod2daemon-flexvol@registry.cn-hangzhou.aliyuncs.com/chenby/calico_pod2daemon-flexvol@g' calico.yaml
sed -i 's@docker.io/calico/node@registry.cn-hangzhou.aliyuncs.com/chenby/calico_node@g' calico.yaml
sed -i 's@docker.io/calico/kube-controllers@registry.cn-hangzhou.aliyuncs.com/chenby/calico_kube-controllers@g' calico.yaml

🔧 步骤四:重新部署

kubectl apply -f calico.yaml

sudo tee /etc/docker/daemon.json <<-‘EOF’
{
“registry-mirrors”: [“https://docker.m.daocloud.io","https://p5lmkba8.mirror.aliyuncs.com","https://registry.docker-cn.com"]
}
EOF
————————————————




大坑:子节点的calico一直处于初始化无法启动解决方案

虽然都在,但关键是containerd配置是否默认用的是 registry.k8s.io/pause:3.6 ,导致kubelet拉这个镜像时出错。


请你检查并修改 containerd配置,明确指定 sandbox_image 用阿里云镜像:

cat /etc/containerd/config.toml | grep sandbox_image

如果没设置,执行:

containerd config default > /etc/containerd/config.toml.backup
containerd config default > /etc/containerd/config.toml

然后编辑 /etc/containerd/config.toml,找到 [plugins."io.containerd.grpc.v1.cri".containerd] 部分,加入或修改为:

sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.6"

保存后,重启containerd:

systemctl restart containerd

然后重启 kubelet:

systemctl restart kubelet

确认 containerRuntimeEndpoint 在 kubelet 配置文件或启动参数中有正确指向:

containerRuntimeEndpoint: unix:///run/containerd/containerd.sock

最后删除calico-node和kube-proxy pod,触发重新调度:

kubectl -n kube-system delete pod -l k8s-app=calico-node
kubectl -n kube-system delete pod -l k8s-app=kube-proxy

然后观察节点状态和Pod状态。

kubectl get nodes 查看节点状态
kubectl get pods -n kube-system -o wide 查看pod状态


总结:
确保 containerd 配置里的 sandbox_image 指向阿里云镜像,避免kubelet拉不到官方registry.k8s.io的镜像导致网络插件初始化失败,从而使节点一直NotReady。




报错:

calico-node-xb2js 0/1 CrashLoopBackOff 9 (4m12s ago) 25m 192.168.200.130 k8s-node1

❗你的 k8s-node1端口 9099 被 Nginx 占用了,而这个端口正是 Calico 默认用于健康检查的端口。

导致结果就是:

calico-node 启动失败并反复报错 listen tcp 127.0.0.1:9099: bind: address already in use,Pod 状态进入 CrashLoopBackOff


✅ 解决方案

你可以选 其中一个方法


方案一:改 Nginx 的端口(推荐)

  1. 找到你的 nginx 配置文件(通常在 vi /etc/nginx/nginx.conf/etc/nginx/conf.d/xxx.conf

  2. 修改 listen 9877; 为其他端口,比如 listen 8081;

  3. 重启 nginx:

    systemctl restart nginx
    
  4. 删除重启 calico-node Pod:

    kubectl delete pod -n kube-system -l k8s-app=calico-node --field-selector spec.nodeName=k8s-node1
    
  5. 等待 Pod 变成 Running 状态。

基于Kubernetes/K8S构建Jenkins持续集成平台(下)

安装和配置NFS文件共享服务器

安装和配置 NFS

NFS(Network File System),它最大的功能就是可以通过网络,让不同的机器、不同的操作系统可以共享彼此的文件。我们可以利用NFS共享Jenkins运行的配置文件、Maven的仓库依赖文件等

我们把NFS服务器安装在192.168.200.129机器上

  • 安装NFS服务(在所有K8S的节点都需要安装)

    yum install -y nfs-utils 【仅此这个都要运行】

  • 创建共享目录

    mkdir -p /opt/nfs/jenkins
    编写NFS的共享配置:
    vi /etc/exports      
    内容如下:  代表对所有IP都开放此目录,rw是读写
    【no_root_suqash root用户生成的数据不做身份转换,也就原来是root,归root用户管理。权限太大实际需要评估】
    /opt/nfs/jenkins    *(rw,no_root_squash)           
    
  • 启动服务

    systemctl enable nfs   开机启动
    systemctl start nfs    启动
    
  • 查看NFS共享目录

    showmount -e 192.168.200.129
    在这之前也要安装nfs服务:yum install -y nfs-utils
    

    [root@k8s-node1 ~]# showmount -e 192.168.200.129
    Export list for 192.168.200.129:
    /opt/nfs/jenkins *

在Kubernetes安装Jenkins-Master

创建NFS client provisioner

nfs-client-provisioner 是一个Kubernetes的简易NFS的外部provisioner,本身不提供NFS,需要现有 的NFS服务器提供存储。

  • 上传nfs-client-provisioner构建文件

    E:\Java实例项目1-20套\资料-Jenkins教程\Jenkins资料\资料\资料\03.资料\k8s-jenkinsjenkins-masternfs-client上传到129Jenkins虚拟机上面

    [root@k8s-master ~]# cd jenkins-master/
    [root@k8s-master jenkins-master]# ll
    总用量 16
    -rw-r–r– 1 root root 1874 7月 7 15:59 rbac.yaml
    -rw-r–r– 1 root root 87 7月 7 15:59 ServiceaAcount.yaml
    -rw-r–r– 1 root root 284 7月 7 15:59 Service.yaml 【对外暴露信息】
    -rw-r–r– 1 root root 2116 7月 7 15:59 StatefulSet.yaml 【有状态应用】

    [root@k8s-master ~]# cd nfs-client/
    [root@k8s-master nfs-client]# ll
    总用量 12
    -rw-r–r– 1 root root 225 7月 7 16:03 class.yaml
    -rw-r–r– 1 root root 985 7月 7 16:03 deployment.yaml
    -rw-r–r– 1 root root 1526 7月 7 16:03 rbac.yaml
    [root@k8s-master nfs-client]# vi deployment.yaml
    修改里面的地址为主节点Jenkins的

  • 构建nfs-client-provisioner的pod资源

    cd nfs-client
    kubectl create -f .
    
  • 查看pod是否创建成功
    kubectl get pods

    [root@k8s-master nfs-client]# kubectl get pods
    NAME READY STATUS RESTARTS AGE
    nfs-client-provisioner-68cd5845b6-dqqm2 0/1 ContainerCreating 0 15s

这里也有个拉镜像的问题 deployment.yaml的lizhenliang/nfs-client-provisioner:latest

接下来操作步骤:

1. 修改你的 deployment.yaml 中的镜像字段,改成刚拉取的镜像:

containers:
  - name: nfs-client-provisioner
    image: quay.io/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11
    # 其他配置保持不变

确保 envvolumes 中 NFS 服务器地址及路径正确,和你之前确认的一致。


2. 应用更新后的 Deployment 配置:

kubectl apply -f deployment.yaml

3. 查看 Pod 状态:

kubectl get pods -w

等待 Pod 状态变成 RunningREADY 1/1

[root@k8s-master nfs-client]# kubectl get pods -w
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-7d79bcf5d4-tddhl 1/1 Running 0 3m33s

可用的deployment.yaml 位置在129机器的/root/nfs-client

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/ifs
            - name: NFS_SERVER
              value: 192.168.200.129 
            - name: NFS_PATH
              value: /opt/nfs/jenkins/
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.200.129
            path: /opt/nfs/jenkins/
  • 返回jenkins-master目录

    [root@k8s-master ~]# cd jenkins-master/
    [root@k8s-master jenkins-master]#
    [root@k8s-master jenkins-master]# kubectl create namespace kube-ops
    namespace/kube-ops created
    [root@k8s-master jenkins-master]# kubectl create -f .
    ✅ 如果你只是想重新部署,可以改用 kubectl apply -f .推荐,用于更新已有资源)

    ❌ 如果你要重新初始化,先删除旧资源:

    kubectl delete -f . && kubectl create -f .
    

这里报错因为有一些过时了的 需要更改 注意是在/root/jenkins-master中的rbac.yaml
你这个问题根源就是 apiVersion 使用了 rbac.authorization.k8s.io/v1beta1,新版 Kubernetes 不再支持这个版本了,必须改成 rbac.authorization.k8s.io/v1

另外,你 ClusterRole 不应该带 namespace 字段,因为 ClusterRole 是集群级别的资源,不属于某个命名空间。


我帮你改成符合最新标准的版本,注意几点:

  • 所有 rbac.authorization.k8s.io/v1beta1 改为 rbac.authorization.k8s.io/v1
  • 删除 ClusterRole 中的 namespace
  • Role 和 RoleBinding 保留 namespace: kube-ops
  • RoleBinding 和 ClusterRoleBinding 中的 subjects namespace 保持和 ServiceAccount 一致
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: jenkins
  namespace: kube-ops
rules:
  - apiGroups: ["extensions", "apps"]
    resources: ["deployments"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]

---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: jenkins
  namespace: kube-ops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins
subjects:
  - kind: ServiceAccount
    name: jenkins
    namespace: kube-ops

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: jenkinsClusterRole
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: jenkinsClusterRoleBinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: jenkinsClusterRole
subjects:
  - kind: ServiceAccount
    name: jenkins
    namespace: kube-ops

保存后,用:

kubectl apply -f rbac.yaml

应该就不会再报找不到资源类型的错误了。

成功!!
service/jenkins created
serviceaccount/jenkins created
statefulset.apps/jenkins created
role.rbac.authorization.k8s.io/jenkins created
rolebinding.rbac.authorization.k8s.io/jenkins created
clusterrole.rbac.authorization.k8s.io/jenkinsClusterRole created
clusterrolebinding.rbac.authorization.k8s.io/jenkinsClusterRoleBinding created



输入kubectl get pod --namespace kube-ops

  • [root@k8s-master jenkins-master]# kubectl get pod --namespace kube-ops
    NAME READY STATUS RESTARTS AGE

    [root@k8s-master jenkins-master]# kubectl get service --namespace kube-ops
    更详细的查看:kubectl get service --namespace kube-ops -o wide
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    jenkins NodePort 10.1.85.222 8080:30742/TCP,50000:31261/TCP 8m1s

如果不能running看这里!

你应该把 Deployment 里的镜像改成 quay.io/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11,这样 Pod 就能正常拉镜像启动了。


正确示例片段改成:

containers:
  - name: nfs-client-provisioner
    image: quay.io/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11
    volumeMounts:
      - name: nfs-client-root
        mountPath: /persistentvolumes
    env:
      - name: PROVISIONER_NAME
        value: fuseim.pri/ifs
      - name: NFS_SERVER
        value: 192.168.200.129
      - name: NFS_PATH
        value: /opt/nfs/jenkins/
volumes:
  - name: nfs-client-root
    nfs:
      server: 192.168.200.129
      path: /opt/nfs/jenkins/

改完后执行:

、kubectl apply -f deployment.yaml
kubectl delete pod -n default -l app=nfs-client-provisioner
kubectl get pods -n default -w

确认 Pod 快速启动且状态为 Running。


这样 PVC 才能被正确绑定,jenkins 的 PVC Pending 状态才会解决。




你这边已经非常接近解决了,目前的 唯一问题是 Provisioner 报了 selfLink was empty, can't make reference 的错误,这在 Kubernetes v1.20+ 中是常见兼容性问题,因为 selfLink 字段已经被弃用并默认禁用了。


✅ 当前现状确认:

项目 状态
NFS 服务 正常可挂载 ✅
Jenkins PVC 一直 Pending ❌
Provisioner Pod 状态 Running ✅
Provisioner 日志报错 selfLink
StorageClass/Provisioner 名 一致 ✅

🚨 问题根因

nfs-client-provisioner老版本依赖了 selfLink 字段,但你用的是 Kubernetes v1.28,从 v1.20 起 selfLink 默认禁用,导致:

unexpected error getting claim reference: selfLink was empty, can't make reference

✅ 解决方案:用国内镜像源代替

我推荐你使用 阿里云加速镜像 或 [DockerHub 镜像代理]。下面我给你一份修改好的 deployment.yaml,使用了国内可访问的镜像仓库地址。


✅ 修改后的 deployment.yaml(适配国内镜像源)

yaml复制编辑apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: registry.cn-hangzhou.aliyuncs.com/zhangshijie/nfs-subdir-external-provisioner:v4.0.2
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/ifs
            - name: NFS_SERVER
              value: 192.168.200.129
            - name: NFS_PATH
              value: /opt/nfs/jenkins/
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.200.129
            path: /opt/nfs/jenkins/

📌 操作步骤

  1. 把上面内容保存为 deployment.yaml
  2. 应用并重建:
kubectl apply -f deployment.yaml
kubectl delete pod -n default -l app=nfs-client-provisioner
  1. 然后检查 PVC 状态是否变成 Bound
kubectl get pvc -n kube-ops

这里是第二种方法[已尝试可行]!

/root/jenkins-master/

你贴出来的 StatefulSet.yaml 文件已经包含了镜像配置的位置!


🎯 问题就在这一段:

yaml复制编辑containers:
  - name: jenkins
    image: jenkins/jenkins:lts-alpine  👈 就是这行!!!

这个 image: 指定的镜像目前拉不下来(你刚才试图用的是阿里云镜像,结果不存在),所以你现在只需要 改成可用的镜像地址


✅ 推荐改法如下:

yaml复制编辑containers:
  - name: jenkins
    image: jenkins/jenkins:lts-jdk17   # ✅ 推荐:可拉取的官方 Jenkins 镜像
    imagePullPolicy: IfNotPresent

💡这是 Jenkins 官方最新稳定版本(带 JDK 17)的镜像,在 Docker Hub 可用,国内访问可能慢一点,但比你原来的阿里云镜像靠谱。


🛠 修改步骤:

  1. 打开 StatefulSet.yaml

    vim StatefulSet.yaml
    
  2. 找到:

    image: jenkins/jenkins:lts-alpine
    
  3. 改成:

    image: jenkins/jenkins:lts-jdk17
    
  4. 保存退出(按 Esc 然后输入 :wq


🔁 然后重新部署:

kubectl apply -f StatefulSet.yaml
kubectl delete pod jenkins-0 -n kube-ops

第二条命令是为了立刻让 Pod 使用新镜像重建。

[root@k8s-master jenkins-master]# kubectl apply -f StatefulSet.yaml statefulset.apps/jenkins configured [root@k8s-master jenkins-master]# kubectl delete pod jenkins-0 -n kube-ops pod “jenkins-0” deleted [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 4s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 8s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 9s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 10s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 11s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 11s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 19s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 24s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 27s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 27s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 31s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 32s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ImagePullBackOff 0 47s [root@k8s-master jenkins-master]#

很好,你的 Jenkins Pod 现在变成了:

STATUS: ImagePullBackOff

这说明你已经修改成功了,但节点还是拉不到镜像。

我们确认下你用了哪个镜像,执行:

kubectl get pod jenkins-0 -n kube-ops -o=jsonpath='{.spec.containers[0].image}'

你应该看到输出类似于:

jenkins/jenkins:lts-jdk17

然后我们来排查下一步。


🧭 下一步:去节点上测试能否手动拉取镜像

进入 k8s-node1 节点(Pod 分配在哪个节点,看 kubectl describe pod 可以看到)

ssh root@k8s-node1

在节点上运行:

docker pull jenkins/jenkins:lts-jdk17

或者如果你用的是 containerd 作为容器运行时:

ctr images pull docker.io/jenkins/jenkins:lts-jdk17

[root@k8s-master jenkins-master]# kubectl apply -f StatefulSet.yaml statefulset.apps/jenkins configured [root@k8s-master jenkins-master]# kubectl delete pod jenkins-0 -n kube-ops pod “jenkins-0” deleted [root@k8s-master jenkins-master]# clear [root@k8s-master jenkins-master]# ll 总用量 16 -rw-r–r– 1 root root 1863 7月 7 16:39 rbac.yaml -rw-r–r– 1 root root 87 7月 7 15:59 ServiceaAcount.yaml -rw-r–r– 1 root root 284 7月 7 15:59 Service.yaml -rw-r–r– 1 root root 2117 7月 7 18:37 StatefulSet.yaml [root@k8s-master jenkins-master]# kubectl apply -f StatefulSet.yaml statefulset.apps/jenkins configured [root@k8s-master jenkins-master]# kubectl delete pod jenkins-0 -n kube-ops pod “jenkins-0” deleted [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 4s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 8s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 9s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 10s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 11s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 11s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ContainerCreating 0 19s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 24s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 27s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 27s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 31s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ErrImagePull 0 32s [root@k8s-master jenkins-master]# kubectl get pod -n kube-ops NAME READY STATUS RESTARTS AGE jenkins-0 0/1 ImagePullBackOff 0 47s [root@k8s-master jenkins-master]# ^C [root@k8s-master jenkins-master]# kubectl get pod jenkins-0 -n kube-ops -o=jsonpath=’{.spec.containers[0].image}’ jenkins/jenkins:lts-jdk17[root@k8s-master jenkins-master]# ssh root@k8s-node1 The authenticity of host ‘k8s-node1 (192.168.200.130)’ can’t be established. ECDSA key fingerprint is SHA256:LMkCxwuWrKBih+Iy+lnayVkAyKdVG7JpVVR3TctpMuk. ECDSA key fingerprint is MD5:c1:76:72:fb:65:64:f8:d5:71:5b:38:a8:9b:07:a6:e6. Are you sure you want to continue connecting (yes/no)? docker pull jenkins/jenkins:lts-jdk17 Please type ‘yes’ or ‘no’: yes Warning: Permanently added ‘k8s-node1’ (ECDSA) to the list of known hosts. root@k8s-node1’s password: Permission denied, please try again. root@k8s-node1’s password: Last failed login: Mon Jul 7 18:39:33 CST 2025 from k8s-master on ssh:notty There was 1 failed login attempt since the last successful login. Last login: Mon Jul 7 17:36:26 2025 from 192.168.200.2 [root@k8s-node1 ~]# docker pull jenkins/jenkins:lts-jdk17 lts-jdk17: Pulling from jenkins/jenkins 0c01110621e0: Pull complete 2315362e4443: Pull complete 4185f63a4bf1: Pull complete 51c9d25ed578: Pull complete 10709260192c: Pull complete 474c0ab7c65d: Pull complete 62d5bb186774: Pull complete f1c4b8ee4b6f: Pull complete 065b78ceb30c: Pull complete b99138e4f420: Pull complete e05a8359c72d: Pull complete 981fa0afce25: Pull complete Digest: sha256:dd570585c3adadefcfbeba915e27bf7feca1815a0ea8a659d46b51f54fc7ea06 Status: Downloaded newer image for jenkins/jenkins:lts-jdk17 [root@k8s-node1 ~]#

太好了!你已经在 k8s-node1 节点上 成功拉下来了 Jenkins 的镜像

bash


复制编辑
✅ jenkins/jenkins:lts-jdk17 拉取成功!

❓但为什么 Pod 还是拉不到?

Kubernetes 默认不会自动使用你用 docker pull 拉下来的镜像,除非你的 K8s 用的容器运行时 就是 Docker

现在我们确认一下你的容器运行时是不是 Docker,执行下面的命令:

bash


复制编辑
ps -ef | grep kubelet | grep -v grep

看结果里面有没有:

  • --container-runtime=docker ✅(说明用的是 Docker)
  • 或者有 containerdcri-o ❌(说明用的是 containerd,不会用 docker 拉的镜像)

✅ 如果你看到用的是 containerd(大概率是)

那你就需要把 Docker 拉下来的镜像导入给 containerd 用!


🔄 导入镜像到 containerd 的方法如下:

  1. k8s-node1 节点上保存镜像为 tar 包:
docker save jenkins/jenkins:lts-jdk17 -o jenkins.tar
  1. ctr 命令导入给 containerd 使用:
ctr -n k8s.io images import jenkins.tar
  1. 验证 containerd 已经识别:
ctr -n k8s.io images ls | grep jenkins

你应该看到 docker.io/jenkins/jenkins:lts-jdk17 这一行。


🔁 然后重启 Pod:

kubectl delete pod jenkins-0 -n kube-ops

过一会再执行:

kubectl get pod -n kube-ops

你现在在 master 节点(192.168.200.129),但 Jenkins Pod 被调度到 node1 节点(192.168.200.130),所以镜像必须在 node1 上可用。而你刚才是手动在 node1 上 docker pull 了 Jenkins 镜像,这一步做得 ✅没问题


✅ 问题核心确认

你当前用的是 Kubernetes,但默认的容器运行时 不是 Docker,而是 containerd,所以:

你用 docker pull 拉下的镜像 containerd 用不到


✅ 解决方案:把 docker 镜像导入到 containerd 里

在 node1 上(192.168.200.130)执行以下命令:

  1. 把镜像导出为 tar 包
docker save jenkins/jenkins:lts-jdk17 -o jenkins.tar
  1. 导入到 containerd:
ctr -n k8s.io images import jenkins.tar
  1. 检查 containerd 是否导入成功:
ctr -n k8s.io images ls | grep jenkins

你应该看到输出类似:

docker.io/jenkins/jenkins:lts-jdk17 ...

🔄 然后,在 master 节点(192.168.200.129)上执行:

kubectl delete pod jenkins-0 -n kube-ops

等 10~20 秒后看 Pod 是否启动成功:

kubectl get pod -n kube-ops

看到状态变为:

jenkins-0   1/1     Running

就说明一切 OK ✅!

你现在遇到的问题是:

⚠️ node1(130) 节点上运行 kubectl 命令时,K8s API server 无法正常访问,提示 the server could not find the requested resource

这是因为:
👉 kubectl 命令只能在 已配置好 K8s 集群访问权限的节点上使用,通常是 master 节点(192.168.200.129)


✅ 正确操作流程如下:

🔁 你刚才在 node1 上导入 containerd 镜像已经成功了,现在:

  1. 返回到 master 节点(192.168.200.129)
  2. 执行删除 pod 的命令(由 master 控制)
kubectl delete pod jenkins-0 -n kube-ops
  1. 查看 pod 状态:
kubectl get pod -n kube-ops

你应该看到 jenkins-0node1 上重新启动,并变成 Running 状态。

你现在正登录在 node1 节点(192.168.200.130),而你的 kubectl 应该在 master 节点(192.168.200.129) 上执行。


✅ 回到 master 节点的方法:

只要你是通过 SSH 登录进来的,直接输入 exit 或按 Ctrl+D 即可退出当前 SSH 会话,返回 master 节点:

[root@k8s-node1 ~]# exit

[root@k8s-node1 ~]# logout

然后你就会看到:

[root@k8s-master ~]#

这时候你就在 master 节点上了!


🧠 提醒:kubectl 只能在 master 上执行

比如这些命令:

kubectl delete pod jenkins-0 -n kube-ops
kubectl get pods -n kube-ops
kubectl logs jenkins-0 -n kube-ops

都需要在 master 节点(192.168.200.129) 上执行,其他 node 节点默认是没有权限或 kubeconfig 配置的。


现在你可以:

✅ 先退出 node1

exit

✅ 然后在 master 上执行:

kubectl delete pod jenkins-0 -n kube-ops
kubectl get pod -n kube-ops -w

成功!!!!

[root@k8s-master jenkins-master]# kubectl get pod -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-0 1/1 Running 0 7m39s
[root@k8s-master jenkins-master]#

  • 承接上文!!
    kubectl get service --namespace kube-ops 得到端口号

    [root@k8s-master jenkins-master]# kubectl get service –namespace kube-ops
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    jenkins NodePort 10.1.85.222 8080:30742/TCP,50000:31261/TCP 3h3m

    访问即可:http://192.168.200.130:30742/
    如何查密码?

    解锁 Jenkins

    为了确保管理员安全地安装 Jenkins,密码已写入到日志中(不知道在哪里?)该文件在服务器上:

    /var/jenkins_home/secrets/initialAdminPassword
    

    请从本地复制密码并粘贴到下面。

    管理员密码:

    cd /opt/nfs/jenkins
    ll
    cd kube-ops-jenkins-home-jenkins-0-pvc-18f7c50d-f5e6-443d-ab24-2ebd09b88a2e
    cd secrets
    cat initialAdminPassword

    [root@k8s-master jenkins-master]# cd /opt/nfs
    [root@k8s-master nfs]# ll
    总用量 0
    drwxr-xr-x 3 root root 86 7月 7 17:13 jenkins
    [root@k8s-master nfs]# cd jenkins/
    [root@k8s-master jenkins]# ll
    总用量 4
    drwxrwxrwx 11 root root 4096 7月 7 19:10 kube-ops-jenkins-home-jenkins-0-pvc-18f7c50d-f5e6-443d-ab24-2ebd09b88a2e

    [root@k8s-master kube-ops-jenkins-home-jenkins-0-pvc-18f7c50d-f5e6-443d-ab24-2ebd09b88a2e]# ll
    总用量 28
    -rw-r–r– 1 sonar 1000 1663 7月 7 18:58 config.xml
    -rw-r–r– 1 sonar 1000 216 7月 7 18:57 copy_reference_file.log
    -rw-r–r– 1 sonar 1000 156 7月 7 18:58 hudson.model.UpdateCenter.xml
    -rw-r–r– 1 sonar 1000 171 7月 7 18:55 jenkins.telemetry.Correlator.xml
    drwxr-xr-x 2 sonar 1000 6 7月 7 18:55 jobs
    -rw-r–r– 1 sonar 1000 1037 7月 7 18:58 nodeMonitors.xml
    drwxr-xr-x 2 sonar 1000 6 7月 7 18:55 plugins
    -rw-r–r– 1 sonar 1000 258 7月 7 18:57 queue.xml.bak
    -rw-r–r– 1 sonar 1000 64 7月 7 18:55 secret.key
    -rw-r–r– 1 sonar 1000 0 7月 7 18:55 secret.key.not-so-secret
    drwx—— 2 sonar 1000 91 7月 7 18:55 secrets
    drwxr-xr-x 2 sonar 1000 67 7月 7 18:58 updates
    drwxr-xr-x 2 sonar 1000 24 7月 7 18:55 userContent
    drwxr-xr-x 3 sonar 1000 56 7月 7 18:55 users
    drwxr-xr-x 10 sonar 1000 247 7月 7 18:55 war

    [root@k8s-master kube-ops-jenkins-home-jenkins-0-pvc-18f7c50d-f5e6-443d-ab24-2ebd09b88a2e]# ^C
    [root@k8s-master kube-ops-jenkins-home-jenkins-0-pvc-18f7c50d-f5e6-443d-ab24-2ebd09b88a2e]# cd secrets
    [root@k8s-master secrets]# ll
    总用量 12
    -rw-r—– 1 sonar 1000 33 7月 7 18:55 initialAdminPassword
    -rw-r–r– 1 sonar 1000 32 7月 7 18:55 jenkins.model.Jenkins.crumbSalt
    -rw-r–r– 1 sonar 1000 256 7月 7 18:55 master.key
    [root@k8s-master secrets]# cat initialAdminPassword
    7a2c40f15a564286aac63f61b9f20b95 【复制它去上面管理员密码解锁!!】

  • 创建管理员用户前选自选安装 → 无 → 账号:root 密码:panchunyao123
    Jenkins URL:http://192.168.200.130:30742/
    这里面的插件和配置延续之前129机器上的

    cd /opt/nfs/jenkins/kube-ops-jenkins-home-jenkins-0-pvc-18f7c50d-f5e6-443d-ab24-2ebd09b88a2e/updates

    输入[替换国内地址]:sed -i 's/http:\/\/updates.jenkins ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json && sed -i 's/http:\/\/ www.google.com/https:\/\/ www.baidu.com/g' default.json

    最后,Manage Plugins点击Advanced,把Update Site改为国内插件下载地址

    https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
    上面这个不行就按照老的来吧
    https://updates.jenkins.io/update-center.json

    http://192.168.200.129:8888/restart 重启Jenkins

  • 先安装基本的插件

    • Localization:Chinese
    • Git
    • Pipeline
    • Extended Choice Parameter

Jenkins与Kubernetes整合

安装Kubernetes插件

安装后 依次点击Dashboard -> 系统管理 -> 云 -> 新建云 -> Kubernetes

new cloud → cloud name:kubernetes Type:kubernetes → create →
Kubernetes 地址:https://kubernetes.default.svc.cluster.local
命名空间:kube-ops
→ 连接测试 → Connected to Kubernetes v1.28.2
→ Jenkins URL地址: http://jenkins.kube-ops.svc.cluster.local:8080
→ 应用 + 保存

构建Jenkins-Slave自定义镜像

  • Jenkins-Master在构建Job的时候,Kubernetes会创建Jenkins-Slave的Pod来完成Job的构建。我们选择 运行Jenkins-Slave的镜像为官方推荐镜像:jenkins/jnlp-slave:latest,但是这个镜像里面并没有Maven 环境,为了方便使用,我们需要自定义一个新的镜像:
    先把E:\Java实例项目1-20套\资料-Jenkins教程\Jenkins资料\资料\资料\03.资料\k8s-jenkinsjenkins-slave塞进129的/root/

[root@k8s-master jenkins-slave]# ll
总用量 8948
-rw-r–r– 1 root root 9142315 7月 7 20:47 apache-maven-3.6.2-bin.tar.gz
-rw-r–r– 1 root root 556 7月 7 20:47 Dockerfile
-rw-r–r– 1 root root 10475 7月 7 20:47 settings.xml

# Dockerfile

FROM jenkins/jnlp-slave:latest
MAINTAINER itcast

# 切换到 root 账户进行操作
USER root

# 安装 maven
COPY apache-maven-3.6.2-bin.tar.gz .

RUN tar -zxf apache-maven-3.6.2-bin.tar.gz && \
    mv apache-maven-3.6.2 /usr/local && \
    rm -f apache-maven-3.6.2-bin.tar.gz && \
    ln -s /usr/local/apache-maven-3.6.2/bin/mvn /usr/bin/mvn && \
    ln -s /usr/local/apache-maven-3.6.2 /usr/local/apache-maven && \
    mkdir -p /usr/local/apache-maven/repo

COPY settings.xml /usr/local/apache-maven/conf/settings.xml

USER jenkins
  • 构建出一个新镜像:jenkins-slave-maven:latest
    docker build -t jenkins-slave-maven:latest

  • 然把镜像上传到Harbor的公共库library中

    先打标签!
    docker tag jenkins-slave-maven:latest 192.168.200.131:85/library/jenkins-slave-maven:latest
    然后登录!
    docker login -u admin -p Harbor12345 192.168.200.131:85

WARNING! Using –password via the CLI is insecure. Use –password-stdin. Error response from daemon: Get https://192.168.200.131:85/v2/: http: server gave HTTP response to HTTPS client [root@k8s-master jenkins-slave]# docker login -u admin -p Harbor12345 192.168.200.131:85 WARNING! Using –password via the CLI is insecure. Use –password-stdin. Error response from daemon: Get https://192.168.200.131:85/v2/: http: server gave HTTP response to HTTPS client [root@k8s-master jenkins-slave]#

你这个错误的根本原因是:Harbor 使用的是 HTTP 协议,而 Docker 默认使用 HTTPS,所以报错:

http: server gave HTTP response to HTTPS client

✅ 解决方案(分两步):


🔧 第一步:允许 Docker 使用 HTTP 连接 Harbor(非安全模式)

1️⃣ 修改 Docker 配置文件 /etc/docker/daemon.json

vi /etc/docker/daemon.json

添加(或修改)以下内容:

{
  "insecure-registries": ["192.168.200.131:85"]
}

如果已有其他配置,比如 registry-mirrors,合并在一个 JSON 对象内:

{
  "registry-mirrors": [
    "https://docker.m.daocloud.io",
    "https://wy1u1ijg.mirror.aliyuncs.com",
    "https://p5lmkba8.mirror.aliyuncs.com",
    "https://registry.docker-cn.com"
  ],
  "insecure-registries": [
    "192.168.200.131:85"
  ]
}

2️⃣ 重启 Docker 服务

systemctl daemon-reexec
systemctl restart docker

🧪 第二步:重新登录 Harbor

docker login 192.168.200.131:85 -u admin -p Harbor12345

这时你不会再看到 HTTPS 报错了。

  • push镜像上harbor [Harbor] (http://192.168.200.131:85/harbor/projects/1/repositories)
    docker push 192.168.200.131:85/library/jenkins-slave-maven:latest

    [root@k8s-master jenkins-slave]# docker push 192.168.200.131:85/library/jenkins-slave-maven:latest
    The push refers to repository [192.168.200.131:85/library/jenkins-slave-maven]
    22c917fbd463: Pushed
    9522f341fa42: Pushed
    1656341ae8c3: Pushed
    1a2dbe51b5d3: Pushed
    ed059ff604cf: Pushed
    4cb0baa801b1: Pushed
    3f01ba93adcb: Pushed
    3dee86c3d230: Pushed
    5ccb7c9ecca8: Pushed
    8a510d97a0f7: Pushed
    3f948fda930d: Pushed
    cf7a8ba4ff71: Pushed
    4e006334a6fd: Pushed
    latest: digest: sha256:38043da78f22253730d941f110156b5130ecdb416fc13b52157d402bbf32f0b4 size: 3046

测试Jenkins-Slave是否可以创建

去子节点的Jenkins创建一个item:test_jenkins_slave 是流水线项目
一定要在流水线上告诉Jenkins是放在从节点上的!

Pipeline scipt
// git 的 URL 和 凭证ID
def git_address = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
def git_auth = "630eed58-d62f-4c1b-bf96-1cf9c92a7f36"

// 正确使用 podTemplate 包裹 node
podTemplate(label: 'jenkins-slave', cloud: 'kubernetes', containers: [
    containerTemplate(
        name: 'jnlp',
        image: "192.168.200.131:85/library/jenkins-slave-maven:latest",
        ttyEnabled: true,
        command: '/bin/sh'
    )
]) {
    node("jenkins-slave") {
        stage('拉取代码') {
            checkout scmGit(
                branches: [[name: "master"]],
                extensions: [],
                userRemoteConfigs: [[
                    credentialsId: "${git_auth}",
                    url: "${git_address}"
                ]]
            )
        }
    }
}

然后点击构建 就会在从节点进行创建 此时去看Dashboard → 系统管理 → 节点和云管理的时候就会多一个刚刚自主创建的节点 快速创建 并快速回收!!

Jenkins+Kubernetes+Docker完成微服务持续集成

拉取代码,构建镜像
  • 创建NFS共享目录

    让所有Jenkins-Slave构建指向NFS的Maven的共享仓库目录

    vi /etc/exports
    添加内容:
    /opt/nfs/jenkins   *(rw,no_root_squash) 
    /opt/nfs/maven     *(rw,no_root_squash) 
    systemctl restart nfs   重启NFS
    

    [root@k8s-node1 ~]# showmount -e 192.168.200.129
    Export list for 192.168.200.129:
    /opt/nfs/maven * 【maven仓库实现共享】
    /opt/nfs/jenkins *
    [root@k8s-node1 ~]#

  • 创建项目,编写构建Pipeline

    // git 的 URL 和 凭证ID
    def git_address = "http://192.168.200.132:82/lanyun_group/tensquare_back.git"
    def git_auth = "630eed58-d62f-4c1b-bf96-1cf9c92a7f36"
    //构建版本的名称
    def tag = "latest"
     //Harbor私服地址
    def harbor_url = "192.168.200.131:85"
     //Harbor的项目名称
    def harbor_project_name = "tensquare"
     //Harbor的凭证
    def harbor_auth = "f82340a4-5173-40e3-bc76-602db3a5acf5"
    
    podTemplate(
        label: 'jenkins-slave',
        cloud: 'kubernetes',
        containers: [
            containerTemplate(
                name: 'jnlp',
                image: "192.168.200.131:85/library/jenkins-slave-maven:latest"
            ),
            containerTemplate(
                name: 'docker',
                image: "docker:stable",
                ttyEnabled: true,
                command: 'cat'
            )
        ],
        volumes: [
            hostPathVolume(
                mountPath: '/var/run/docker.sock',
                hostPath: '/var/run/docker.sock'
            ),
            nfsVolume(
                mountPath: '/usr/local/apache-maven/repo',
                serverAddress: '192.168.200.129',
                serverPath: '/opt/nfs/maven'
            )
        ]
    ) {
        node("jenkins-slave") {
            stage('拉取代码') {
                checkout scmGit(
                    branches: [[name: "master"]],
                    extensions: [],
                    userRemoteConfigs: [[
                        credentialsId: "${git_auth}",
                        url: "${git_address}"
                    ]]
                )
            }
    
            stage('编译,安装公共子工程') {
                sh "mvn -f tensquare_common clean install"
            }
    
            stage('构建镜像,部署项目') {
                // 把选择的项目信息转为数组
                def selectedProjects = "${project_name}".split(',')
    
                for (int i = 0; i < selectedProjects.size(); i++) {
                    // 取出每个项目的名称和端口
                    def currentProject = selectedProjects[i]
                    def currentProjectName = currentProject.split('@')[0]
                    def currentProjectPort = currentProject.split('@')[1]
                    def imageName = "${currentProjectName}:${tag}"
    
                    // 编译,构建本地镜像
                    sh "mvn -f ${currentProjectName} clean package dockerfile:build"
    
                    container('docker') {
                        // 给镜像打标签
                        sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName}"
    
                        // 登录 Harbor,并上传镜像
                        withCredentials([usernamePassword(
                            credentialsId: "${harbor_auth}",
                            passwordVariable: 'password',
                            usernameVariable: 'username'
                        )]) {
                            sh "docker login -u ${username} -p ${password} ${harbor_url}"
                            sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"
                        }
    
                        // 删除本地镜像
                        sh "docker rmi -f ${imageName}"
                        sh "docker rmi -f ${harbor_url}/${harbor_project_name}/${imageName}"
                    }
                }
            }
        }
    }
    
  • 勾选参数化构建过程Extended Choice Parameter和之前的一样 可回看tensquare_eureka_server@10086,tensquare_zuul@10020,tensquare_admin_service@9001,tensquare_gathering@9002

安装Kubernetes Continuous Deploy插件 || Kubernetes CLI插件 || Kubernetes Credentials Provider
  • 修改后的流水线脚本

    // 这里是在全局凭证添加后点进去可以查看ID
    dsf k8s_auth = "xxxxxxxxx"
    // 定义k8s-harbor的凭证
    def secret_name = "registry-auth-secret"
    ...
    ...
    def deploy_image_name = "${harbor_url}/${harbor_project_name}/${imageName}"
     //部署到K8S
     sh """
     sed -i 's#\$IMAGE_NAME#${deploy_image_name}#' 
    ${currentProjectName}/deploy.yml
     sed -i 's#\$SECRET_NAME#${secret_name}#' 
    ${currentProjectName}/deploy.yml
     """
     kubernetesDeploy configs: "${currentProjectName}/deploy.yml", 
    kubeconfigId: "${k8s_auth}"
    
  • 去Jenkins的凭证里加k8s凭证

    Credentials from Kubernetes Secrets will not be available

    🧠 说明:

    • 这个警告说明 Jenkins 无法通过 Kubernetes 插件从 Secret 里加载凭证。
    • 通常是以下原因之一:
      • Pod 没有挂载正确的 ServiceAccount / RBAC 不够
      • 缺少 Kubernetes Credentials Provider Plugin
      • 没有开启 K8s secret credential provider 功能

    ✅ 解决方案:

    1. 确保你安装了插件:

      • Kubernetes
      • Kubernetes Credentials Provider
    2. 配置 Jenkins Deployment 的 ServiceAccount 具备权限,绑定如下 ClusterRole:

    3. 你可以给 Jenkins 的 ServiceAccount 绑定一个 ClusterRole 来读取 secrets:
      /root/jenkins-master/ServiceaAcount.yaml

      apiVersion: v1
      kind: ServiceAccount
      metadata:
        name: jenkins
        namespace: kube-ops
      ---
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
        name: jenkins-read-secrets
      roleRef:
        apiGroup: rbac.authorization.k8s.io
        kind: ClusterRole
        name: view
      subjects:
      - kind: ServiceAccount
        name: jenkins
        namespace: kube-ops
      

      👆这会给 jenkins 账户授予读取所有 namespace 的 secret 权限。

      将上面内容保存为 jenkins-rbac.yaml,然后执行:

      kubectl apply -f jenkins-rbac.yaml
      
  • 添加凭证 → 类型Kubernetes configuration(kubeconfig);范围全局;描述k8s-auth;Kubeconfig选择Enter directly这里面的密钥要去 129Jenkins机器上拿
    cd /root/.kubecat config 显示的内容全部原封不动的复制到那里面

  • 在每个项目下建立deploy.xml

    Eureka的deply.yml
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: eureka
      labels:
        app: eureka
    spec:
      type: NodePort
      ports:
        - port: 10086
          name: eureka
          targetPort: 10086
      selector:
        app: eureka
    ---
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: eureka
    spec:
      serviceName: "eureka"
      replicas: 2
      selector:
        matchLabels:
          app: eureka
      template:
        metadata:
          labels:
            app: eureka
        spec:
          imagePullSecrets:
            - name: $SECRET_NAME
          containers:
            - name: eureka
              image: $IMAGE_NAME
              ports:
                - containerPort: 10086
              env:
                - name: MY_POD_NAME
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.name
                - name: EUREKA_SERVER
                  value: "http://eureka-0.eureka:10086/eureka/,http://eureka-1.eureka:10086/eureka/"
                - name: EUREKA_INSTANCE_HOSTNAME
                  value: ${MY_POD_NAME}.eureka
      podManagementPolicy: "Parallel"
    

    其他项目的deploy.yml主要把名字和端口修改:

    zuul的deploy.yml
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: zuul
      labels:
        app: zuul
    spec:
      type: NodePort
      ports:
        - port: 10020
          name: zuul
          targetPort: 10020
      selector:
        app: zuul
    ---
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: zuul
    spec:
      serviceName: "zuul"
      replicas: 2
      selector:
        matchLabels:
          app: zuul
      template:
        metadata:
          labels:
            app: zuul
        spec:
          imagePullSecrets:
            - name: $SECRET_NAME
          containers:
            - name: zuul
              image: $IMAGE_NAME
              ports:
                - containerPort: 10020
      podManagementPolicy: "Parallel"
    
  • 还要在流水线定义k8s-harbor的凭证
    生成Docker凭证 Docker凭证,用于Kubernetes到Docker私服拉取镜像

    docker login -u itcast -p Itcast123 192.168.66.102:85 登录Harbor

    kubectl create secret docker-registry registry-auth-secret –docker server=192.168.200.131:85 –docker-username=itcast –docker-password=Itcast123 – docker-email=itcast@itcast.cn 生成

    kubectl get secret 查看密钥

  • 项目构建后,查看服务创建情况

    kubectl get pods -owide
    kubectl get service

启动命令总结

[192.168.200.132]GitLab:gitlab-ctl restart 
[192.168.200.131]Tomcat:/opt/tomcat/bin/startup.sh

[192.168.200.129]Jenkins:/systemctl start jenkins
[192.168.200.129]SonarQube:sudo -u sonarqube /opt/sonar/bin/linux-x86-64/sonar.sh restart

2025.7.8  0:02   完结撒花★,°:.☆( ̄▽ ̄)/$:.°★

阅读全文

OpenCV

2025/3/30

图形相关处理

OpenCV是应用广泛的开源图像处理库,我们以其为基础,介绍相关的图像处理方法:包括基本的图像处理方法:几何变换,形态学变换,围伦平滑,直方围操作,板匹配,霍夫变换等;特征提取和描述方法:理解角点特征,Harris和Shi-TomaS算法,SIFT/SURF算法,Fast算法,ORB算法等;还有OpenCV在视频操作中的应用,最后的案例是使用OpenCV进行人险检测。

在第一次世界大战后,1921年美国科学家发明了BartlaneSystem,并从伦敦传到纽约传输了第一幅数字图像,其亮度用离散数值表示,:将图片编码成5个灰度级,如下图所示,通过海底电缆进行传输。在发送端图片被编码并使用打孔带记录,通过系统传输后在接收方使用特殊的打印机恢复成图像。

1950年左右,计算机被发明,数字图像处理学科正式诞生

位数

计算机采用0/1编码的系统,数字图像也是利用0/1来记录信息,我们平常接触的图像都是8位数图像,包含0~255灰度,其中0,代表最黑,1,表示最白。

二值图像:

一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储,

灰度图:

每个像素只有一个采样颜色的图像,这类图像通常显示为从最暗黑色到最亮的白色的灰度,尽管理论上这个采样可以任何颜色的不同深浅,甚至可以是不同亮度上的不同颜色。灰度图像与黑白图像不同,在计算机图像领域中黑白图像只有黑色与白色两种颜色;但是,灰度图像在黑色与白色之间还有许多级的颜色深度。灰度图像经常是在单个电磁波频谱如可见光内测量每个像素的亮度得到的,用于显示的灰度图像通常用每个采样像素8位的非线性尺度来保存,这样可以有256级灰度(如果用16位,则有65536级)。

彩色图:

每个像素通常是由红(R)、绿(G)、蓝(B)三个分量来表示的,分量介干(0,255)。RGB图像与索引图像一样都可以用来表示彩色图像。与索引图像一样,它分别用红(R)、绿(G)、蓝(B)三原色的组合来表示每个像素的颜色。但与索引图像不同的是,RGB图像每一个像素的颜色值(由RGB三原色表示)直接存放在图像矩阵中,由于每一像素的颜色需由R、G、B三个分量来表示,M、N分别表示图像的行列数,三个MxN的二维矩阵分别表示各个像素的R、G、B三个颜色分量。RGB图像的数据类型一般为8位无符号整形,通常用于表示和存放真彩色图像。

1. 图像是什么

图:物体反射或透射光的分布
像:人的视觉系统所接受的图在人脑中所形版的印象或认识

2. 模拟图像和数字图像

模拟图像:连续存储的数据
数字图像:分级存储的数据

3. 数字图像

位数:图像的表示,常见的就是8位分类:二值图像,灰度图像和彩色图像

OpenCV的优势:
1. 编程语言

OpenCV基于C++实现,同时提供python,Ruby,Matlab等语言的接口。OpenCV-Python是OpenCV的Python API,结合了OpenCV C++ API和Python语言的最佳特性。

2. 跨平台

可以在不同的系统平台上使用,包括Windows,Linux,OSX,Android和i0S。基于CUDA和OpenCl的高速GPU操作接口也在积极开发中

3. 活跃的开发团队
4. 丰富的AP!

同时添加了对深度学习的支持。完善的传统计算机视觉算法,涵盖主流的机器学习算法,

OpenCV-Python

OpenCV-Python是一个Python绑定库,旨在解决计算机视觉问题。
Python是一种由Guido van Rossum开发的通用编程语言,它很快就变得非常流行,主要是因为它的简单性和代码可读性。它使程序员能够用更少的代码行表达思想,而不会降低可读性

与C/C++等语言相比,Python速度较慢。也就是说,Python可以使用C/C++轻松扩展,这使我们可以在C/C++中编写计算密集型代码,并创建可用作Python模块的Python包装器。这给我们带来了两个好处:首先,代码与原始C/C++代码一样快(因为它是在后台工作的实际C++代码),其次,在Python中编写代码比使用C/C++更容易。OpenCV-Python是原始OpenCVC++实现的Python包装器。

OpenCV-Python使用Numpy,这是一个高度优化的数据库操作库,具有MATLAB风格的语法。所有OpenCV数组结构都转换为Numpy数组。这也使得与使用Numpy的其他库(如SciPy和Matplotib)集成更容易。

OpenCV部署方法

安装OpenCV之前需要先安装numpy, matplotlibJ
创建Python虚拟环境cv,在cv中安装即可
先安装OpenCV-Python,由于一些经典的算法被申请了版权,新版本有很大的限制,所以选用3.4.3以下的版

OpenCV模块简介

其中core、highgui、imgproc是最基础的模块,该课程主要是围绕这几个模块展开的,分别介绍如下:

  • core模块实现了最核心的数据结构及其基本运算,如绘图函数、数组操作相关函数等。

  • highgui模块实现了视频与图像的读取、显示、存储等接口。

  • imgproc模块实现了图像处理的基础方法,包括图像滤波、图像的几何变换、平滑、阈值分割、形态学处理、边缘检测、目标检测、运动分析和对象跟踪等。

    对于图像处理其他更高层次的方向及应用,penCV也有相关的模块实现

  • features2d模块用于提取图像特征以及特征匹配,nonfree模块实现了一些专利算法,如sift特征

  • objdetect模块实现了一些目标检测的功能,经典的基于Haar、LBP特征的人脸检测,基于HOG的行人、汽车等目标检测,分类器使用Cascade Classification(级联分类)和Latent SVM等。

  • stitching模块实现了图像拼接功能。

  • FLANN模块(Fast Library for Approximate Nearest Neighbors),包含快速近似最近邻搜索FLANN和聚类Clustering算法。

  • mI模块机器学习模块(SVM,决策树,Boosting等等)

  • photo模块包含图像修复和图像去噪两部分。

  • video模块针对视频处理,如背景分离,前景检测、对象跟踪等。

  • calib3d模块即Calibration(校准)3D,这个模块主要是相机校准和三维重建相关的内容。包含了基本的多视角几何算法,单个立体摄像头标定,物体姿态估计,立体相似性算法,3D信息的重建等等。

  • G-API模块包含超高效的图像处理pipeline引整

OpenCV基本操作

本章主要介绍图像的基础操作,包括:

  • 图像的IO操作,读取和保存方法
  • 在图像上绘制几何图形
  • 怎么获取图像的属性
  • 怎么访问图像的像素,进行通道分离,合并等
  • 怎么实现颜色空间的变换
  • 图像的算术运算
图像的基础操作
  • 掌握图像的读取和保存方法
  • 能够使用OpenCV在图像上绘制几何图形
  • 能够访问图像的像素
  • 能够获取图像的属性,并进行通道的分离和合并
  • 能够实现颜色空间的变换

图像的IO操作

这里我们会给大家介绍如何读取图像,如何显示图像和如何保存图像。

1.1 读取图像

  1. API
cv.imread()

参数:

  • 要读取的图像

  • 读取方式的标志

    • cv.IMREAD*COLOR:以彩色模式加载图像,任何图像的透明度都将被忽略。这是默认参数。

    • cv.IMREAD*GRAYSCALE:以灰度模式加载图像

    • cv.IMREAD_UNCHANGED:包括alpha通道的加载图像模式。

      可以使用1、0或者-1来替代上面三个标志

  • 参考代码

    import numpy as np
    import cv2 as cv
    # 以灰度图的形式读取图像
    img = cv.imread('messi5.jpg',0)
    

注意:如果加载的路径有错误,不会报错,会返回一个None值

1.2显示图像

1 . API

cv.imshow()

参数:

  • 显示图像的窗口名称,以字符串类型表示
  • 要加载的图像

注意:在调用显示图像的API后,要调用cv.waitKey()给图像绘制留下时间,否则窗口会出现无响应情况,并且图像无法显示出来

另外我们也可使用matplotlib对图像进行展示。

  1. 参考代码

    # opencv中显示
    cv.imshow('image',img)
    cv.waitKey(0) # 永远的等待下去
    # matplotlib中展示
    plt.imshow(img[:,:,::-1])
    

1.3 保存图像

  1. API

    cv.imwrite()
    

    参数:

    • 文件名,要保存在哪里
    • 要保存的图像
  2. 参考代码

    cv.imwrite('messigray.png',img)
    

1.4 总结

我们通过加载灰度图像,显示图像,如果按’s’并退出则保存图像,或者按ESC键直接退出而不保存。

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread('messi5.jpg',0)
# 2 显示图像
# 2.1 利用opencv展示图像
cv.imshow('image',img)
# 2.2 在matplotplotlib中展示图像【RGB通道】  后面要反转图像去显示
plt.imshow(img[:,:,::-1])
plt.title('匹配结果'), plt.xticks([]), plt.yticks([])
plt.show()
k = cv.waitKey(0)
# 3 保存图像
cv.imwrite('messigray.png',img)

2 绘制几何图形

2.1 绘制直线

cv.line(img,start,end,color,thickness)

参数:

  • img:要绘制直线的图像
  • Start,end: 直线的起点和终点
  • color: 线条的颜色
  • Thickness: 线条宽度

2.2 绘制圆形

cv.circle(img,centerpoint, r, color, thickness)

参数:

  • img:要绘制圆形的图像
  • Centerpoint, r: 圆心和半径
  • color: 线条的颜色
  • Thickness: 线条宽度,为-1时生成闭合图案并填充颜色

2.3 绘制矩形

cv.rectangle(img,leftupper,rightdown,color,thickness)

参数:

  • img:要绘制矩形的图像
  • Leftupper, rightdown: 矩形的左上角和右下角坐标
  • color: 线条的颜色
  • Thickness: 线条宽度

2.4 向图像中添加文字

cv.putText(img,text,station, font, fontsize,color,thickness,cv.LINE_AA)

参数:

  • img: 图像
  • text:要写入的文本数据
  • station:文本的放置位置
  • font:字体
  • Fontsize :字体大小

2.5 效果展示

我们生成一个全黑的图像,然后在里面绘制图像并添加文字

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 创建一个空白的图像
img = np.zeros((512,512,3), np.uint8)
# 2 绘制图形
cv.line(img,(0,0),(511,511),(255,0,0),5)
cv.rectangle(img,(384,0),(510,128),(0,255,0),3)
cv.circle(img,(447,63), 63, (0,0,255), -1)
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img,'OpenCV',(10,500), font, 4,(255,255,255),2,cv.LINE_AA)
# 3 图像展示
plt.imshow(img[:,:,::-1])
plt.title('匹配结果'), plt.xticks([]), plt.yticks([])
plt.show()

3 获取并修改图像中的像素点

我们可以通过行和列的坐标值获取该像素点的像素值。对于BGR图像,它返回一个蓝,绿,红值的数组。对于灰度图像,仅返回相应的强度值。使用相同的方法对像素值进行修改。

import numpy as np
import cv2 as cv
img = cv.imread('messi5.jpg')
# 获取某个像素点的值
px = img[100,100]
# 仅获取蓝色通道的强度值
blue = img[100,100,0]
# 修改某个位置的像素值
img[100,100] = [255,255,255]

4 获取图像的属性

图像属性包括行数,列数和通道数,图像数据类型,像素数等。

5 图像通道的拆分与合并

有时需要在B,G,R通道图像上单独工作。在这种情况下,需要将BGR图像分割为单个通道。或者在其他情况下,可能需要将这些单独的通道合并到BGR图像。你可以通过以下方式完成。

# 通道拆分
b,g,r = cv.split(img)
# 通道合并
img = cv.merge((b,g,r))

6 色彩空间的改变

OpenCV中有150多种颜色空间转换方法。最广泛使用的转换方法有两种,BGR↔Gray和BGR↔HSV。

API:

cv.cvtColor(input_image,flag)

参数:

  • input_image: 进行颜色空间转换的图像
  • flag: 转换类型
    • cv.COLOR_BGR2GRAY : BGR↔Gray
    • cv.COLOR_BGR2HSV: BGR→HSV

总结:

  1. 图像IO操作的API:

    cv.imread(): 读取图像

    cv.imshow():显示图像

    cv.imwrite(): 保存图像

  2. 在图像上绘制几何图像

    cv.line(): 绘制直线

    cv.circle(): 绘制圆形

    cv.rectangle(): 绘制矩形

    cv.putText(): 在图像上添加文字

  3. 直接使用行列索引获取图像中的像素并进行修改

  4. 图像的属性

    属性 API
    形状 img.shape
    图像大小 img.size
    数据类型 img.dtype
  5. 拆分通道:cv.split()

    通道合并:cv.merge()

  6. 色彩空间的改变

    cv.cvtColor(input_image,flag)

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

Jupyter Notebook增加临时环境

在Jupyter的terminal中

★ ★ PS C:\Users\Pluminary\Documents> pip config set global.index-url https://pypi.tuna.tsi
nghua.edu.cn/simple
Writing to C:\Users\Pluminary\AppData\Roaming\pip\pip.ini
★ ★ PS C:\Users\Pluminary\Documents> pip install opencv-python --cache-dir ./pipcache
>>
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple, https://pypi.ngc.nvidia.
com
Collecting opencv-python
  Downloading https://pypi.tuna.tsinghua.edu.cn/packages/a4/7d/f1c30a92854540bf789e9cd
5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl
(39.5 MB)
     --------------------------------------- 39.5/39.5 MB 1.3 MB/s eta 0:00:00
★ ★ PS C:\Users\Pluminary\Documents> pip install matplotlib

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

算数操作【要用到上面的Jupyter Notebook】

学习目标

  • 了解图像的加法、混合操作

1.图像的加法注意:这里都要求两幅图像是相同大小的

你可以使用OpenCV的cv.add()函数把两幅图像相加,或者可以简单地通过numpy操作添加两个图像,如res = img1 + img2。两个图像应该具有相同的大小和类型,或者第二个图像可以是标量值。

注意:OpenCV加法和Numpy加法之间存在差异。OpenCV的加法是饱和操作,而Numpy添加是模运算

参考以下代码:

【OpenCV】
>>> x = np.uint8([250])
>>> y = np.uint8([10])
>>> print( cv.add(x,y) ) # 250+10 = 260 => 255
[[255]]

【Numpy】
>>> print( x+y )          # 250+10 = 260 % 256 = 4
[4]

这种差别在你对两幅图像进行加法时会更加明显。OpenCV 的结果会更好一点。所以我们尽量使用 OpenCV 中的函数。

代码:

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

# 1 读取图像
img1 = cv.imread("view.jpg")
img2 = cv.imread("rain.jpg")

# 2 加法操作
img3 = cv.add(img1,img2) # cv中的加法
img4 = img1+img2 # 直接相加

# 3 图像显示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img3[:,:,::-1])
axes[0].set_title("cv中的加法")
axes[1].imshow(img4[:,:,::-1])
axes[1].set_title("直接相加")
plt.show()
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

# 显示下雨的图片 ("./image/view.jpg")
rain = cv.imread("./rain.jpg")
plt.imshow(rain[:,:,::-1])

# 显示风景图片
view = cv.imread("./view.jpg")
plt.imshow(view[:,:,::-1])

# 用cv去做加法 显示雨中风景图
img1 = cv.add(rain,view)
plt.imshow(img1[:,:,::-1])

# 用普通做加法 显示雨中风景图
img2 = rain+view
plt.imshow(img2[:,:,::-1])

总结:OpenCV的加法效果要大于Numpy的加法效果

2.图像的混合注意:这里都要求两幅图像是相同大小的

这其实也是加法,但是不同的是两幅图像的权重不同,这就会给人一种混合或者透明的感觉。图像混合的计算公式如下:

g(x) = (1−α)f0(x) + αf1(x)

通过修改 α 的值(0 → 1),可以实现非常炫酷的混合。

现在我们把两幅图混合在一起。第一幅图的权重是0.7,第二幅图的权重是0.3。函数cv2.addWeighted()可以按下面的公式对图片进行混合操作。

dst = α⋅img1 + β⋅img2 + γ

这里γ取为零。

参考以下代码:

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

# 1 读取图像
img1 = cv.imread("view.jpg")
img2 = cv.imread("rain.jpg")

# 2 图像混合
img3 = cv.addWeighted(img1,0.7,img2,0.3,0)

# 3 图像显示
plt.figure(figsize=(8,8))
plt.imshow(img3[:,:,::-1])
plt.show()
## 结合上方的代码
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

# 显示下雨的图片
rain = cv.imread("./rain.jpg")
plt.imshow(rain[:,:,::-1])

# 显示风景图片
view = cv.imread("./view.jpg")
plt.imshow(view[:,:,::-1])

# 用cv去做加法 显示雨中风景图
img1 = cv.add(rain,view)
plt.imshow(img1[:,:,::-1])

# 用普通做加法 显示雨中风景图
img2 = rain+view
plt.imshow(img2[:,:,::-1])

# 做指定参数的混合加法
img3 = cv.addWeighted(view,0.5,rain,0.5,0)
plt.imshow(img3[:,:,::-1])

总结

  1. 图像加法:将两幅图像加载一起

    cv.add()

  2. 图像的混合:将两幅图像按照不同的比例进行混合

    cv.addweight()

注意:这里都要求两幅图像是相同大小的。

1 图像缩放[绝对尺寸+相对尺寸]

缩放是对图像的大小进行调整,即使图像放大或缩小。

  1. lAPI

    cv2.resize(src,dsize,fx=0,fy=0,interpolation=cv2.INTER_LINEAR)
    

    参数:

    • src : 输入图像

    • dsize: 绝对尺寸,直接指定调整后图像的大小

    • fx,fy: 相对尺寸,将dsize设置为None,然后将fx和fy设置为比例因子即可

    • interpolation:插值方法,

      插值 含义
      cv2.INTER_LINEAR 双线性插值法
      cv2.INTER_NEAREST 最近邻插值
      cv2.INTER_AREA 像素
      cv2.INTER_CUBIC 双三次插值
  2. 示例

  3. import cv2 as cv
    # 1. 读取图片
    img1 = cv.imread("./image/dog.jpeg")
    # 2.图像缩放
    # 2.1 绝对尺寸
    rows,cols = img1.shape[:2]
    res = cv.resize(img1,(2*cols,2*rows),interpolation=cv.INTER_CUBIC)
    
    # 2.2 相对尺寸
    res1 = cv.resize(img1,None,fx=0.5,fy=0.5)
    
    # 3 图像显示
    # 3.1 使用opencv显示图像(不推荐)
    cv.imshow("orignal",img1)
    cv.imshow("enlarge",res)
    cv.imshow("shrink)",res1)
    cv.waitKey(0)
    
    # 3.2 使用matplotlib显示图像
    fig,axes=plt.subplots(nrows=1,ncols=3,figsize=(10,8),dpi=100)
    axes[0].imshow(res[:,:,::-1])
    axes[0].set_title("绝对尺度(放大)")
    axes[1].imshow(img1[:,:,::-1])
    axes[1].set_title("原图")
    axes[2].imshow(res1[:,:,::-1])
    axes[2].set_title("相对尺度(缩小)")
    plt.show()
    
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
kids = cv.imread("./kids.jpg")
plt.imshow(kids[:,:,::-1])

# 绝对尺寸
rows,cols = kids.shape[:2]
rows =>> 374
cols =>> 500
res = cv.resize(kids,[2*rows,2*cols])
plt.imshow(res[:,:,::-1])
res.shape =>> (1000,748,3)

# 相对尺寸
res1 = cv.resize(kids,None,fx=0.5,fy=0.5)
plt.imshow(res1[:,:,::-1])
res1.shape =>> (187,250,3)

2 图像平移

图像平移将图像按照指定方向和距离,移动到相应的位置。

  1. API
cv.warpAffine(img,M,dsize)

参数:

  • img: 输入图像

  • M: 2∗∗3移动矩阵

    对于(x,y)处的像素点,要把它移动到(x+tx,y+tyx+t**x,y+t**y)处时,M矩阵应如下设置:

    M = [1 0 tx]
    [0 1 ty]
    注意:将MM设置为np.float32类型的Numpy数组。

  • dsize: 输出图像的大小

    注意:输出图像的大小,它应该是(宽度,高度)的形式。请记住,width=列数,height=行数。

  • 示例

需求是将图像的像素点移动(50,100)的距离:

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1. 读取图像
img1 = cv.imread("./image/image2.jpg")

# 2. 图像平移
rows,cols = img1.shape[:2]
M = M = np.float32([[1,0,100],[0,1,50]])# 平移矩阵
dst = cv.warpAffine(img1,M,(cols,rows))

# 3. 图像显示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img1[:,:,::-1])
axes[0].set_title("原图")
axes[1].imshow(dst[:,:,::-1])
axes[1].set_title("平移后结果")
plt.show()

3 图像旋转[每一个像素点坐标进行修改]

图像旋转是指图像按照某个位置转动一定角度的过程,旋转中图像仍保持这原始尺寸。图像旋转后图像的水平对称轴、垂直对称轴及中心坐标原点都可能会发生变换,因此需要对图像旋转中的坐标进行相应转换。

同时我们要修正原点的位置,因为原图像中的坐标原点在图像的左上角,经过旋转后图像的大小会有所变化,原点也需要修正。

假设在旋转的时候是以旋转中心为坐标原点的,旋转结束后还需要将坐标原点移到图像左上角,也就是还要进行一次变换。

在OpenCV中图像旋转首先根据旋转角度和旋转中心获取旋转矩阵,然后根据旋转矩阵进行变换,即可实现任意角度和任意中心的旋转效果。
  1. API

    cv2.getRotationMatrix2D(center, angle, scale)
    

    参数:

    • center:旋转中心
    • angle:旋转角度
    • scale:缩放比例

    返回:

    • M:旋转矩阵

      调用cv.warpAffine完成图像的旋转

  2. 示例

    import numpy as np
    import cv2 as cv
    import matplotlib.pyplot as plt
    # 1 读取图像
    img = cv.imread("./image/image2.jpg")
    
    # 2 图像旋转
    rows,cols = img.shape[:2]
    # 2.1 生成旋转矩阵
    M = cv.getRotationMatrix2D((cols/2,rows/2),90,1)
    # 2.2 进行旋转变换
    dst = cv.warpAffine(img,M,(cols,rows))
    
    # 3 图像展示
    fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
    axes[0].imshow(img1[:,:,::-1])
    axes[0].set_title("原图")
    axes[1].imshow(dst[:,:,::-1])
    axes[1].set_title("旋转后结果")
    plt.show()
    
## 完整的Jupyter
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
kids = cv.imread("./kids.jpg")
plt.imshow(kids[:,:,::-1])

# 绝对尺寸
rows,cols = kids.shape[:2]
rows =>> 374
cols =>> 500
res = cv.resize(kids,[2*rows,2*cols])
plt.imshow(res[:,:,::-1])
res.shape

# 相对尺寸
res1 = cv.resize(kids,None,fx=0.5,fy=0.5)
plt.imshow(res1[:,:,::-1])
res1.shape

# 图像平移
rows,cols = kids.shape[:2]
M = np.float32([[1,0,100],[0,1,50]])
res2 = cv.warpAffine(kids,M,(cols,rows))
plt.imshow(res2[:,:,::-1])

# 图像旋转
M = cv.getRotationMatrix2D((cols/2,rows/2),45,1)
res3 = cv.warpAffine(kids,M,(cols,rows))
plt.imshow(res3[:,:,::-1])

4 仿射变换[类似ps的扭曲]

图像的仿射变换涉及到图像的形状位置角度的变化,是深度学习预处理中常到的功能,仿射变换主要是对图像的缩放,旋转,翻转和平移等操作的组合。

那什么是图像的仿射变换,如下图所示,图1中的点1, 2 和 3 与图二中三个点一一映射, 仍然形成三角形, 但形状已经大大改变,通过这样两组三点(感兴趣点)求出仿射变换, 接下来我们就能把仿射变换应用到图像中所有的点中,就完成了图像的仿射变换。

在OpenCV中,仿射变换的矩阵是一个2×3的矩阵:
M=[A B]=[a00 a01 b0]
=[a10 a11 b1]
其中左边的2×2子矩阵$A$是线性变换矩阵,右边的2×1子矩阵$B$是平移项:
A=[a00 a01] , B=[b0]
[a10 a11] [b1]
对于图像上的任一位置(x,y),仿射变换执行的是如下的操作:
Taffine = A[x] + B = M[x]
[y] [y]
[1]
需要注意的是,对于图像而言,宽度方向是x,高度方向是y,坐标的顺序和图像像素对应下标一致。所以原点的位置不是左下角而是右上角,y的方向也不是向上,而是向下。

在仿射变换中,原图中所有的平行线在结果图像中同样平行。为了创建这个矩阵我们需要从原图像中找到三个点以及他们在输出图像中的位置。然后cv2.getAffineTransform 会创建一个 2x3 的矩阵,最后这个矩阵会被传给函数 cv2.warpAffine。

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 图像读取
img = cv.imread("./image/image2.jpg")

# 2 仿射变换
rows,cols = img.shape[:2]
# 2.1 创建变换矩阵
pts1 = np.float32([[50,50],[200,50],[50,200]])
pts2 = np.float32([[100,100],[200,50],[100,250]])
M = cv.getAffineTransform(pts1,pts2)
# 2.2 完成仿射变换
dst = cv.warpAffine(img,M,(cols,rows))

# 3 图像显示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img[:,:,::-1])
axes[0].set_title("原图")
axes[1].imshow(dst[:,:,::-1])
axes[1].set_title("仿射后结果")
plt.show()
## 代码承接上方
# 仿射变换
pts1 = np.float32([[56,65],[368,52],[389,390]])
pts2 = np.float32([[100,145],[300,100],[310,300]])

M = cv.getAffineTransform(pts1,pts2)
    M array([[ 6.40600025e-01, -1.02147944e-02,  6.47903603e+01],
               [-1.19267183e-01,  5.99126068e-01,  1.12735768e+02]])
res4 = cv.warpAffine(kids,M,(cols,rows))
plt.imshow(res4[:,:,::-1])

5 透射变换

透射变换是视角变化的结果,是指利用透视中心、像点、目标点三点共线的条件,按透视旋转定律使承影面(透视面)绕迹线(透视轴)旋转某一角度,破坏原有的投影光线束,仍能保持承影面上投影几何图形不变的变换。

在opencv中,我们要找到四个点,其中任意三个不共线,然后获取变换矩阵T,再进行透射变换。通过函数cv.getPerspectiveTransform找到变换矩阵,将cv.warpPerspective应用于此3x3变换矩阵。

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread("./image/image2.jpg")
# 2 透射变换
rows,cols = img.shape[:2]
# 2.1 创建变换矩阵
pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])
pts2 = np.float32([[100,145],[300,100],[80,290],[310,300]])

T = cv.getPerspectiveTransform(pts1,pts2)
# 2.2 进行变换
dst = cv.warpPerspective(img,T,(cols,rows))

# 3 图像显示
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(img[:,:,::-1])
axes[0].set_title("原图")
axes[1].imshow(dst[:,:,::-1])
axes[1].set_title("透射后结果")
plt.show()
## 代码承接上方
# 投射变换
pst1 = np.float32([[56,65],[368,52],[28,387],[389,390]])
pst2 = np.float32([[100,145],[300,100],[80,290],[310,300]])
T = cv.getPerspectiveTransform(pst1,pst2)
T
res5 = cv.warpPerspective(kids,T,(cols,rows))
plt.imshow(res5[:,:,::-1])

6 图像金字塔

图像金字塔是图像多尺度表达的一种,最主要用于图像的分割,是一种以多分辨率来解释图像的有效但概念简单的结构。

图像金字塔用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的分辨率逐步降低,且来源于同一张原始图的图像集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。

金字塔的底部待处理图像的高分辨率表示,而顶部低分辨率的近似,层级越高,图像越小,分辨率越低

  1. API

    cv.pyrUp(img)       #对图像进行上采样
    cv.pyrDown(img)        #对图像进行下采样
    
  2. 示例

    import numpy as np
    import cv2 as cv
    import matplotlib.pyplot as plt
    # 1 图像读取
    img = cv.imread("./image/image2.jpg")
    # 2 进行图像采样
    up_img = cv.pyrUp(img)  # 上采样操作
    img_1 = cv.pyrDown(img)  # 下采样操作
    # 3 图像显示
    cv.imshow('enlarge', up_img)
    cv.imshow('original', img)
    cv.imshow('shrink', img_1)
    cv.waitKey(0)
    cv.destroyAllWindows()
    
## 代码承接上方
# 图像金字塔
 # 图像上采样
plt.imshow(kids[:,:,::-1])
imgup = cv.pyrUp(kids)
plt.imshow(imgup[:,:,::-1])
imgup2 = cv.pyrUp(imgup)
plt.imshow(imgup2[:,:,::-1])
 # 图像下采样
imgdown = cv.pyrDown(kids)
plt.imshow(imgdown[:,:,::-1])

总结

  1. 图像缩放:对图像进行放大或缩小

    cv.resize()

  2. 图像平移:

    指定平移矩阵后,调用cv.warpAffine()平移图像

  3. 图像旋转:

    调用cv.getRotationMatrix2D获取旋转矩阵,然后调用cv.warpAffine()进行旋转

  4. 仿射变换:

    调用cv.getAffineTransform将创建变换矩阵,最后该矩阵将传递给cv.warpAffine()进行变换

  5. 透射变换:

    通过函数cv.getPerspectiveTransform()找到变换矩阵,将cv.warpPerspective()进行投射变换

  6. 金字塔

    图像金字塔是图像多尺度表达的一种,使用的API:

    cv.pyrUp(): 向上采样

    cv.pyrDown(): 向下采样

形态学操作

学习目标

  • 理解图像的邻域,连通性
  • 了解不同的形态学操作:腐蚀,膨胀,开闭运算,礼帽和黑帽等,及其不同操作之间的关系

形态学转换是基于图像形状的一些简单操作。它通常在二进制图像上执行。腐蚀和膨胀是两个基本的形态学运算符。然后它的变体形式如开运算,闭运算,礼帽黑帽等。

膨胀的作用是将与物体接触的所有背景点合并到物体中,使目标增大,可添补目标中的孔洞。

API

   cv.dilate(img,kernel,iterations)

参数:

  • img: 要处理的图像
  • kernel: 核结构
  • iterations: 腐蚀的次数,默认是1
  1. 示例

我们使用一个5*5的卷积核实现腐蚀和膨胀的运算:

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 1 读取图像
img = cv.imread("./image/image3.png")
# 2 创建核结构
kernel = np.ones((5, 5), np.uint8)

# 3 图像腐蚀和膨胀
erosion = cv.erode(img, kernel) # 腐蚀
dilate = cv.dilate(img,kernel) # 膨胀

# 4 图像展示
fig,axes=plt.subplots(nrows=1,ncols=3,figsize=(10,8),dpi=100)
axes[0].imshow(img)
axes[0].set_title("原图")
axes[1].imshow(erosion)
axes[1].set_title("腐蚀后结果")
axes[2].imshow(dilate)
axes[2].set_title("膨胀后结果")
plt.show()
阅读全文

python知识点

2025/3/30
项目01 python源码实例知识点

在控制台打出Icon表情:win+R → Wingdings → 字符码 0xcc 中间加个 f0 → 0xf0cc
大写字母转换为小写字母:str.upper()
小写字母转换为大写字母:str.lower()
station.reverse() #对高铁站列表反向输出
world = [“西北”, “中国”, “亚洲”, “世界”]
print(world[random.choice([0, 1, 2, 3])]
random.shuffle() 顺序打乱
random.sample(sqn,n) 从序列中选n个随机且不重复的元素
sdate=datetime.datetime.today().strftime(‘%m-%d %H:%M’)
sys.stdout.flush 刷新画面布
flag = random.choice([“+”, “-“]) # 随机产生”+”或 “-“号
ces = random.randint(1,100) # 随机生成一个数字
fl = f.readlines()
del fl[:8] #del删除切片(前8行数据)
fl = fl[1::3] #提取下标为1,步长为3的切片
str1 = ‘ ‘.join(fl) #join()函数分割文本数据
str1 = str1.replace(‘[QQ红包]请使用新版手机QQ查收红包。’,’’)
#滤除无用文本

文件操作对比
#打开文件
def button1():
  global file1
  file1=tk.filedialog.askopenfilename()
  txt_path1.set(file1)


#对比文件
def Diff():
  with open(file1) as f1,open(file2) as f2:
    text1 = f1.readlines()
    text2 = f2.readlines()
  d = difflib.HtmlDiff()
  with open('result1.html','w') as f:
    f.write(d.make_file(text1,text2))

a1=list(set(list1)) #字符串列表去重

print("".join([s for s in str1.splitlines(True) if s.strip()]))

这行代码的作用是打印字符串 str1 中非空行的内容,并且保留原有的换行符。下面是逐部分的解析:

str1.splitlines(True)
splitlines() 是字符串的方法,用于将字符串按行分割成列表。
参数 True 表示在分割时保留行尾的换行符。
这部分代码将 str1 按行分割成一个列表,每个元素是一行字符串,包括换行符。

[s for s in ... if s.strip()]
这是一个列表推导式,用于生成一个新的列表。
s for s in ... 表示对每个分割后的行字符串 s 进行迭代。

if s.strip() 是一个条件表达式,用于过滤掉空行或只包含空白字符的行。strip() 方法移除字符串两端的空白字符(包括空格、制表符、换行符等),如果 s.strip() 返回一个空字符串,表示 s 是一个空行或只包含空白字符。

"".join(...)
join() 是字符串的方法,用于将列表中的元素连接成一个字符串。这里使用空字符串 "" 作为连接符,表示不添加额外的字符。
这部分代码将列表推导式生成的列表中的所有非空行连接成一个单一的字符串,保留每行末尾的换行符。
print(...)
print() 函数用于输出结果到控制台。
这部分代码将最终连接好的字符串打印出来。
综合起来,这行代码的执行流程是:
----------------------------------------------------------------
将 str1 按行分割成列表,保留每行的换行符。
过滤掉列表中的空行或只包含空白字符的行。
将过滤后的非空行连接成一个字符串。
打印最终的结果。

#用线程控制自动切换单词
t = threading.Thread(target=autoChange)
t.start()

print("电影:",film)
print('判断演员是否本部电影的演员。回车确认“是”,输入任意键确认“不是”')
for i in range(6):                     # 6次竞猜
    new = random.choice(actall)        # 随机选择演员
    actall.remove(new)                 # 从演员库删除选择的演员,防止下次再次出现
    print(new)  # 显示竞猜演员
    # 从用户输入中获取一个字符串 并移除该字符串两端的空白字符
    num = input("").strip()  # 用户进行判断,选择回车还是其他键
    if not num:  # 选择回车,确认是该部电影主演
        if new not in acter:  # 如果该演员不在本部电影主演库里面
            count -= 3  # 积分减3分
            print("答错了,减三分!")
        else:  # 答对了
            count += 3  # 积分加3分
            print("答对了,加三分!")
        print("当前分数:", count)
    else:  # 选择其他键,确认不是该部电影主演
        if new not in acter:  # 如果该演员不在本部电影主演库里面
            count += 3  # 积分加3分
            print("答对了,加三分!")
        else:  # 答错了
            count -= 3  # 积分减3分
            print("答错了,减三分!")
        print("当前分数:", count)  # 输出当前积分

turtle(海龟)库是turtle绘图体系python的实现;
turtle绘图体系:1969年诞生,主要用于程序设计入门;
turtle库是python的标准库之一;属于入门级的图形绘制函数库;
turtle库绘制原理:有一只海龟在窗体正中心,在画布上游走,走过的轨迹形成了绘制的图形,海龟由程序控制,可以自由改变颜色、方向宽度等;

screen.onkeypress(None,”space”) #按空格键启动转盘

阅读全文

RAGFlow执行流程解析canvas

2025/3/19

Canvas.py

import logging
import json
from copy import deepcopy
from functools import partial

import pandas as pd

from agent.component import component_class
from agent.component.base import ComponentBase


class Canvas:
    """
    dsl = {
        "components": {
            "begin": {
                "obj":{
                    "component_name": "Begin",
                    "params": {},
                },
                "downstream": ["answer_0"],
                "upstream": [],
            },
            "answer_0": {
                "obj": {
                    "component_name": "Answer",
                    "params": {}
                },
                "downstream": ["retrieval_0"],
                "upstream": ["begin", "generate_0"],
            },
            "retrieval_0": {
                "obj": {
                    "component_name": "Retrieval",
                    "params": {}
                },
                "downstream": ["generate_0"],
                "upstream": ["answer_0"],
            },
            "generate_0": {
                "obj": {
                    "component_name": "Generate",
                    "params": {}
                },
                "downstream": ["answer_0"],
                "upstream": ["retrieval_0"],
            }
        },
        "history": [],
        "messages": [],
        "reference": [],
        "path": [["begin"]],
        "answer": []
    }
    """
    # dsl是组件流的JSON格式字符串 定义了组件的连接关系
    # tenant_id多租户管理
    # component存储所有组件实例
    def __init__(self, dsl: str, tenant_id=None):
        self.path = []
        self.history = []
        self.messages = []
        self.answer = []
        self.components = {}
        # 如果提供了dsl则解析JSON 否则使用默认的DSL结构(包含 Begin 组件)
        self.dsl = json.loads(dsl) if dsl else {
            "components": {
                "begin": {
                    "obj": {
                        "component_name": "Begin",
                        "params": {
                            "prologue": "Hi there!"
                        }
                    },
                    "downstream": [],
                    "upstream": [],
                    "parent_id": ""
                }
            },
            "history": [],
            "messages": [],
            "reference": [],
            "path": [],
            "answer": []
        }
        self._tenant_id = tenant_id
        self._embed_id = ""
        self.load()

    # 加载dsl
    def load(self):
      # 解析dsl["components"]存储组件实例 并检查必要组件是否存在
        # 组件字典存入self.components
        self.components = self.dsl["components"]
        # cpn_nms记录组件名称集合
        cpn_nms = set([])

        # 遍历组件 提取名称
        for k, cpn in self.components.items():
            cpn_nms.add(cpn["obj"]["component_name"])
        # 确保Begin和Answer组件存在 否则抛出错误
        assert "Begin" in cpn_nms, "There have to be an 'Begin' component."
        assert "Answer" in cpn_nms, "There have to be an 'Answer' component."

        # for循环便利self.components这个字典
        # 其中键k是组件标识符 cpn是一个包含组件详细信息的字典
        for k, cpn in self.components.items():
            # 将组件名称添加到cpn_nms集合中
            cpn_nms.add(cpn["obj"]["component_name"])
            # 通过component_class动态创建 拼接param
            param = component_class(cpn["obj"]["component_name"] + "Param")()
            # 更新param实例的属性 将cpn["obj"]["params"]字典中的键值对应用到param实例上
            param.update(cpn["obj"]["params"])
            param.check()
          # 使用component_class动态创建了一个新的组件实例 并将其赋值给cpn["obj"]
            # 新实例的创建使用了当前类实例self、组件标识符k和参数实例param
            cpn["obj"] = component_class(cpn["obj"]["component_name"])(self, k, param)
            # 检查当前组件的名称是否为"Categorize"
            if cpn["obj"].component_name == "Categorize":
                # 组件是"Categorize" 则遍历param.category_description字典 这个字典包含了分类描述
                for _, desc in param.category_description.items():
                    # 这两行代码检查desc["to"](可能是下游组件的标识符)是否已经在cpn["downstream"]列表中
                    if desc["to"] not in cpn["downstream"]:
                        # 如果不在,就将其添加到列表中
                        cpn["downstream"].append(desc["to"])
        # 将self.dsl["path"]的值赋给self.path属性
        self.path = self.dsl["path"]
        self.history = self.dsl["history"]
        self.messages = self.dsl["messages"]
        self.answer = self.dsl["answer"]
        self.reference = self.dsl["reference"]
        self._embed_id = self.dsl.get("embed_id", "")

    # 定义了类的字符串表示形式 通常用于打印对象时显示的信息
    # 这几行代码将类的属性值赋给self.dsl字典中的相应键
    def __str__(self):
        self.dsl["path"] = self.path
        self.dsl["history"] = self.history
        self.dsl["messages"] = self.messages
        self.dsl["answer"] = self.answer
        self.dsl["reference"] = self.reference
        self.dsl["embed_id"] = self._embed_id
        # 创建一个新的字典dsl 其中包含一个键components 其值为一个空字典
        dsl = {
            "components": {}
        }
        # 遍历self.dsl字典的键 如果键不是components 则将其值深拷贝到新的dsl字典中
        for k in self.dsl.keys():
            if k in ["components"]:
                continue
            dsl[k] = deepcopy(self.dsl[k])

        # 遍历self.components字典 将每个组件的信息深拷贝到dsl字典的components键下
        # 对于obj键 将其值转换为字符串后再解析为JSON
        for k, cpn in self.components.items():
            if k not in dsl["components"]:
                dsl["components"][k] = {}
            for c in cpn.keys():
                if c == "obj":
                    dsl["components"][k][c] = json.loads(str(cpn["obj"]))
                    continue
                dsl["components"][k][c] = deepcopy(cpn[c])
        # 返回dsl字典的JSON字符串表示形式 ensure_ascii=False确保非ASCII字符可以正确显示
        return json.dumps(dsl, ensure_ascii=False)

    # 将类的属性重置为空列表
    def reset(self):
        self.path = []
        self.history = []
        self.messages = []
        self.answer = []
        self.reference = []
        # 遍历self.components字典 并调用每个组件的reset方法
        for k, cpn in self.components.items():
            self.components[k]["obj"].reset()
        # 将_embed_id属性重置为空字符串
        self._embed_id = ""

    # 这个方法用于根据组件ID获取组件名称
    def get_component_name(self, cid):
        # 遍历self.dsl字典中的graph键下的nodes列表
        # 如果找到匹配的ID 则返回相应的组件名称
        for n in self.dsl["graph"]["nodes"]:
            if cid == n["id"]:
                return n["data"]["name"]
        # 如果没有找到匹配的ID 则返回空字符串
        return ""

    def run(self, **kwargs):
        # 如果self.answer列表不为空
        if self.answer:
            # 用于存储接下来要执行的组件ID
            cpn_id = self.answer[0]
            # 存完就从列表中移除
            self.answer.pop(0)
            try:
                # 尝试执行 self.components 字典中对应 cpn_id 的组件对象的 run 方法
                # 并传入 self.history 和 **kwargs 作为参数
                ans = self.components[cpn_id]["obj"].run(self.history, **kwargs)
                # 如果执行过程中发生异常
            except Exception as e:
                # 捕获该异常并使用 ComponentBase.be_output 方法处理异常信息
                ans = ComponentBase.be_output(str(e))
            # 将执行过的组件ID添加到 self.path 列表的最后一个元素中
            # self.path 似乎用于记录执行路径
            self.path[-1].append(cpn_id)
            # 如果 kwargs 中包含关键字 “stream”
            # 则假设 ans 是一个生成器 遍历它并逐个 yield 出来
            # 如果不是 则直接 yield ans
            if kwargs.get("stream"):
                for an in ans():
                    yield an
            else:
                yield ans
            return
        # 如果 self.path 为空 则执行名为 “begin” 的组件的 run 方法 并将 “begin” 添加到 self.path 中
        if not self.path:
            self.components["begin"]["obj"].run(self.history, **kwargs)
            self.path.append(["begin"])
        # 向 self.path 添加一个空列表 为接下来的组件执行做准备
        self.path.append([])

        ran = -1
        waiting = []
        without_dependent_checking = []

        # 当有下游组件downstream时
        # 它接受一个组件列表 cpns 作为参数
        def prepare2run(cpns):
            # 这些变量在函数外部定义 但在函数内部可以被修改
            nonlocal ran, ans
            # 遍历 cpns 列表中的每个组件ID c
            # 如果 c 已经是 self.path 中最后一个列表的最后一个元素 则跳过当前循环
            for c in cpns:
                if self.path[-1] and c == self.path[-1][-1]:
                    continue
                # 从 self.components 字典中获取组件ID c 对应的组件对象
                cpn = self.components[c]["obj"]
                # 如果组件的名称是 “Answer” 则将组件ID c 添加到 self.answer 列表中
                if cpn.component_name == "Answer":
                    self.answer.append(c)
                else:
                    # 如果组件不是 “Answer” 则记录调试信息 检查组件是否有未执行的依赖组件
                    # 如果有 将组件ID添加到 waiting 列表 并跳过当前循环
                    logging.debug(f"Canvas.prepare2run: {c}")
                    if c not in without_dependent_checking:
                        cpids = cpn.get_dependent_components()
                        if any([cc not in self.path[-1] for cc in cpids]):
                            if c not in waiting:
                                waiting.append(c)
                            continue
                    yield "*'{}'* is running...🕞".format(self.get_component_name(c))
                    # 如果组件是 “Iteration” 则获取其开始组件 并检查是否已经结束 如果没有结束 则更新 cpn 和 c 为开始组件
                    if cpn.component_name.lower() == "iteration":
                        st_cpn = cpn.get_start()
                        assert st_cpn, "Start component not found for Iteration."
                        if not st_cpn["obj"].end():
                            cpn = st_cpn["obj"]
                            c = cpn._id
                    # 尝试运行组件
                    try:
                        ans = cpn.run(self.history, **kwargs)
                    # 并捕获任何异常 如果发生异常 记录错误并更新 ran 然后重新抛出异常
                    except Exception as e:
                        logging.exception(f"Canvas.run got exception: {e}")
                        self.path[-1].append(c)
                        ran += 1
                        raise e
                    # 将组件ID c 添加到 self.path 的最后一个列表中
                    self.path[-1].append(c)
            # 增加 ran 计数器的值
            ran += 1
        """
        接下来的代码块处理下游组件的运行 循环检测 以及组件输出的合并
        这部分代码涉及到更复杂的逻辑,如处理循环、条件分支、组件输出等
        """
        # 获取上一个组件的下游组件列表
        downstream = self.components[self.path[-2][-1]]["downstream"]
        # 如果没有下游组件 但存在父组件ID 则合并父组件和当前组件的输出
        if not downstream and self.components[self.path[-2][-1]].get("parent_id"):
            cid = self.path[-2][-1]
            pid = self.components[cid]["parent_id"]
            o, _ = self.components[cid]["obj"].output(allow_partial=False)
            oo, _ = self.components[pid]["obj"].output(allow_partial=False)
            self.components[pid]["obj"].set_output(pd.concat([oo, o], ignore_index=True).dropna())
            downstream = [pid]
        # 递归调用 prepare2run 函数处理下游组件 并生成运行状态信息
        for m in prepare2run(downstream):
            yield {"content": m, "running_status": True}

        # 在 ran 的值有效时 处理循环和组件运行
        # ran 的值在 0 和 self.path[-1] 的长度之间时继续执行
        while 0 <= ran < len(self.path[-1]):
            logging.debug(f"Canvas.run: {ran} {self.path}")
            # 从 self.path 中获取当前要处理的组件ID cpn_id
            # 并通过 get_component 方法获取组件对象
            cpn_id = self.path[-1][ran]
            cpn = self.get_component(cpn_id)
            # 如果当前组件没有下游组件、没有父组件ID 并且没有等待处理的组件 则退出循环
            if not any([cpn["downstream"], cpn.get("parent_id"), waiting]):
                break

            # 调用 _find_loop 方法检查是否存在循环 如果存在则抛出 OverflowError 异常
            loop = self._find_loop()
            if loop:
                raise OverflowError(f"Too much loops: {loop}")
            # 如果组件的名称是 “switch”、“categorize” 或 “relevant” 则处理这些特定类型的组件
            if cpn["obj"].component_name.lower() in ["switch", "categorize", "relevant"]:
                switch_out = cpn["obj"].output()[1].iloc[0, 0]
                assert switch_out in self.components, \
                    "{}'s output: {} not valid.".format(cpn_id, switch_out)
                # 递归调用 prepare2run 函数处理特定组件的输出 并生成运行状态信息 然后继续下一次循环
                for m in prepare2run([switch_out]):
                    yield {"content": m, "running_status": True}
                continue

            # 处理父组件输出合并
            downstream = cpn["downstream"]
            # 如果没有下游组件但有父组件ID 则合并父组件和当前组件的输出
            if not downstream and cpn.get("parent_id"):
                pid = cpn["parent_id"]
                _, o = cpn["obj"].output(allow_partial=False)
                _, oo = self.components[pid]["obj"].output(allow_partial=False)
                self.components[pid]["obj"].set_output(pd.concat([oo.dropna(axis=1), o.dropna(axis=1)], ignore_index=True))
                downstream = [pid]
            # 递归调用 prepare2run 函数处理下游组件 并生成运行状态信息
            for m in prepare2run(downstream):
                yield {"content": m, "running_status": True}

            # 如果 ran 的值等于或超过 self.path[-1] 的长度
            # 并且有等待处理的组件 则处理这些组件 并更新 ran 的值
            if ran >= len(self.path[-1]) and waiting:
                without_dependent_checking = waiting
                waiting = []
                for m in prepare2run(without_dependent_checking):
                    yield {"content": m, "running_status": True}
                without_dependent_checking = []
                ran -= 1
        # 如果 self.answer 列表非空 处理答案组件
        if self.answer:
            cpn_id = self.answer[0]
            self.answer.pop(0)
            ans = self.components[cpn_id]["obj"].run(self.history, **kwargs)
            self.path[-1].append(cpn_id)
            # 如果 self.answer 列表为空 抛出异常 提示需要在流程末尾添加交互组件
            if kwargs.get("stream"):
                assert isinstance(ans, partial)
                for an in ans():
                    yield an
            else:
                yield ans

        else:
            raise Exception("The dialog flow has no way to interact with you. Please add an 'Interact' component to the end of the flow.")

    def get_component(self, cpn_id):
        return self.components[cpn_id]

    def get_tenant_id(self):
        return self._tenant_id
    # 定义一个名为 get_history 的方法 它接受一个参数 window_size 这个参数用于确定需要返回多少条历史记录
    def get_history(self, window_size):
        # 初始化一个空列表convs  用于存储格式化后的对话记录
        convs = []
        # 使用一个 for 循环遍历 self.history 的最后 window_size 条记录
        # self.history 应该是一个列表 其中每个元素都是一个包含角色和对象(对话内容)的元组
        """
            role 是对话中的角色,例如 “user” 或 “assistant”。
            obj 是与该角色相关的对话内容,它可能是一个列表或单个对象
        """
        for role, obj in self.history[window_size * -1:]:
            if isinstance(obj, list) and obj and all([isinstance(o, dict) for o in obj]):
                convs.append({"role": role, "content": '\n'.join([str(s.get("content", "")) for s in obj])})
            else:
                convs.append({"role": role, "content": str(obj)})
        return convs

    def add_user_input(self, question):
        self.history.append(("user", question))

    def set_embedding_model(self, embed_id):
        self._embed_id = embed_id

    def get_embedding_model(self):
        return self._embed_id
 # 定义一个名为 _find_loop 的方法 它接受一个可选参数 max_loops默认值为6 这个参数用于限制循环检测的次数
    def _find_loop(self, max_loops=6):
        # 获取 self.path 列表的最后一个元素(即当前路径)并将其反转 以便从后往前检查
        path = self.path[-1][::-1]
        # 如果路径的长度小于2 则没有足够的元素形成循环 因此直接返回 False
        if len(path) < 2:
            return False
        # 遍历路径,如果找到以 “answer” 或 “iterationitem” 开头的元素
        # 则从路径中移除该元素及其后面的所有元素
        # 因为这些元素可能表示循环的结束
        for i in range(len(path)):
            if path[i].lower().find("answer") == 0 or path[i].lower().find("iterationitem") == 0:
                path = path[:i]
                break
        # 再次检查路径长度 如果移除特定元素后长度小于2 则返回 False
        if len(path) < 2:
            return False
        # 使用一个循环来检查路径的不同子序列 loc 是子序列的长度
        # 从 2 开始 直到路径长度的一半
        for loc in range(2, len(path) // 2):
            # 创建一个子序列pat 它是路径的前loc个元素 用逗号连接
            pat = ",".join(path[0:loc])
            # 创建一个字符串 path_str 它是整个路径 用逗号连接
            path_str = ",".join(path)
            # 如果子序列pat的长度大于或等于整个路径字符串 ath_str的长度 则不可能形成循环 因此返回 False
            if len(pat) >= len(path_str):
                return False
            """
            使用一个 while 循环来检查 path_str 是否以 pat 开头 
            如果是 则减少 loop 计数 并从 path_str 中移除 pat 和随后的逗号
            如果 loop 计数降到 0 以下 说明找到了循环
            """
            loop = max_loops
            while path_str.find(pat) == 0 and loop >= 0:
                loop -= 1
                if len(pat)+1 >= len(path_str):
                    return False
                path_str = path_str[len(pat)+1:]
                # 如果找到了循环 则创建一个循环模式的字符串 使用 " => " 作为分隔符
                # 并将路径中的每个元素的前半部分(在冒号之前的部分)连接起来
                # 最后 返回一个表示循环的模式字符串
            if loop < 0:
                pat = " => ".join([p.split(":")[0] for p in path[0:loc]])
                return pat + " => " + pat

        return False

    def get_prologue(self):
        return self.components["begin"]["obj"]._param.prologue

    def set_global_param(self, **kwargs):
        for k, v in kwargs.items():
            for q in self.components["begin"]["obj"]._param.query:
                if k != q["key"]:
                    continue
                q["value"] = v

    def get_preset_param(self):
        return self.components["begin"]["obj"]._param.query

    def get_component_input_elements(self, cpnnm):
        return self.components[cpnnm]["obj"].get_input_elements()
阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.