1 智能辅助系统tlist:


前后端在进行交互是使用的是restful风格的接口进行交互的




1 部门管理:

1 查询部门:




前后端联调:
就是通过前端程序和后端程序连接起来通过访问前端工程来访问服务端程序进儿进行调试:

2 删除部门:


有请求参数为部门id
pathVariable是接受请求参数
请求方式为delete的话我们要DeleteMapping这个注解:
新增部门:



如何传递一个json格式的请求参数呐我们可以通过实体类并在实体类上添加一个RequestBody即将实体类转化为json格式


2 员工管理:
1 分页查询:


后端回访会多个结果则此时我们把结果封装到实体类中:







简化分页查询:

这个插件会自动的记录总数
使用这个插件要导入这个插件所需的依赖



2 分页查询带条件:


就是在分页查询中添加一些动态的查询条件:
为啥实体类pagebean中只有两个属性但是在service中可以包含五个参数是为啥那
Gemini said
这个问题问得非常好!很多刚学 Java 三层架构和分页查询的小伙伴,都会在这里产生同样的疑惑。
你之所以会觉得奇怪,是因为你把**"方法的输入(参数)"和"方法的输出(返回值)"**搞混了。
一句话解释:参数是你给 Service 提的"查询条件",而 PageBean 是 Service 查完后打包还给你的"查询结果"。它们俩的属性数量不需要一致。
让我们详细拆解一下:
1. 入参(Input):你要查什么条件?
在你的 Controller 代码中,传入了 6 个参数:page, pageSize, name, gender, begin, end。 这些是前端传过来的搜索条件。你在告诉 Service:
"请帮我从数据库里找员工,条件是:名字叫
name的、性别是gender的、入职时间在begin和end之间的。并且,请帮我把结果分页,我要第page页,每页显示pageSize条。"
条件越复杂,这里的参数就越多,这完全取决于产品经理要求页面上能提供多少个搜索框。
2. 返回值(Output):查到了什么结果?
无论你上面给的搜索条件有多么复杂(哪怕传入了 100 个参数),对于分页查询来说,前端(页面)最终真正需要的数据永远只有两样东西:
-
数据列表 (
List<Emp> rows):满足这些条件的那一页员工的具体信息(比如查到了张三、李四)。 -
总记录数 (
Long total):满足这些条件的员工一共有多少人(用来在前端渲染"共 100 条,分 10 页"的那个分页条)。
所以,PageBean 这个实体类的作用,仅仅是一个**"快递盒"。Service 去数据库里辛辛苦苦根据那 6 个条件查完之后,把查到的 总数和 数据列表**往 PageBean 这个盒子里一装,然后把盒子扔回给 Controller。
🍔 举个生活中的例子:点外卖
-
入参(6个参数) :就像你在点单时备注的要求:"要微辣、不要葱花、多加香菜、送到5楼、手机号138xxxx、张先生收"。(参数可以有很多个)
-
返回值(PageBean) :餐厅老板做好后交给你的一份外卖 。这份外卖里固定只有两样东西:一份饭(
rows)和一张小票(total)。
所以,餐厅老板并不需要把"不要葱花"、"微辣"这些要求重新包装到给你的外卖盒子里,他只需要根据要求把饭做出来就行了。
💡 附赠提示:你的 log.info 语法写错啦
我注意到你第二张截图里的 log.info 下面有黄色的波浪线。 在 Java 里,字符串和变量用逗号拼接是会报错或不生效的。使用 log.info 时,正确的写法是使用 {} 作为占位符:
Java
// 正确的写法:前面有几个大括号,后面就跟几个变量
log.info("分页查询参数: {}, {}, {}, {}, {}, {}", page, pageSize, name, gender, begin, end

3删除员工:
1 删除单个员工
2 批量删除员工:
但是这里我们就添加一个批量删除的接口就行因为删除单个员工是删除批量员工的特殊形式
要传入删除的员工的id主键

1 删除不需要返回结果
要将id封装到一个数据或集合中 在mapper接口重要遍历数组或集合来动态的组装sql语句
2 新增员工和编辑修改员工
完全理解你的困惑!刚接触前后端交互时,这两个符号确实很容易让人搞混。其实,这主要是一个业界约定俗成的规范(RESTful 风格),为了让网址(URL)看起来更清晰、更有逻辑。
咱们先抛开代码,用生活中最常见的例子来帮你彻底搞懂**"为什么"以及"什么时候"**用哪个。
核心法则:/ 是为了"找人/定位",? 是为了"提要求/筛选"
你可以用一个简单的测试来判断:如果我把这个参数拿掉,这个请求的意思是不是完全变了?
场景一:使用 / (路径变量) ------ "没它不行,它是身份的象征"
想象你是一个快递员,/ 就像是门牌号,是一层一层递进的精准定位。
-
例子: 你要去"修水县"的"第 15 号"员工家里。
-
网址设计:
http://服务器地址/emps/15 -
为什么用
/?-
因为
15是这个员工的唯一标识(ID)。 -
如果你把
/15拿掉,变成了/emps,这就变成了"去所有员工家里",意思完全变了! -
所以,当你明确知道你要操作的是哪一个或哪几个具体目标 (比如查看详情、删除、修改),这个 ID 就是路径中必不可少的一部分,我们就用
/连起来。
-
场景二:使用 ? (查询参数) ------ "有它更好,没它也行"
想象你去淘宝买衣服,? 就像是你加的筛选条件。
-
例子: 你进了"员工衣服专区"(
/emps),然后你跟导购说:"我要找名字叫张三的,并且是男装"。 -
网址设计:
http://服务器地址/emps?name=张三&gender=1 -
为什么用
??-
这里的
name和gender只是附加的过滤条件。 -
如果你把
?name=张三&gender=1拿掉,只剩下/emps,你依然是在"员工衣服专区"里,只是导购会把所有衣服都拿给你看。请求的核心目标没有变,只是范围变大了。 -
所以,当你进行搜索、条件查询、分页 时,这些参数是"可选的补充说明",我们就用
?拼在最后面。
-
咱们回到你的项目总结一下
在你的那个员工(Emp)和部门(Dept)管理系统里,大家都是这么约定的:
| 你的目的 | 应该用啥符号? | URL 长什么样? | 后端怎么接? |
|---|---|---|---|
| 我要看所有员工 | 啥都不用 | /emps |
无需参数 |
| 我要找名字带"张"的员工 | 用 ? (筛选) |
/emps?name=张 |
String name |
| 我要看第 15 号员工长啥样 | 用 / (定位) |
/emps/15 |
@PathVariable Integer id |
| 我要把第 15、16、17 号员工开除 | 用 / (定位) |
/emps/15,16,17 |
@PathVariable List<Integer> ids |
Export to Sheets
简而言之:找特定目标用 /,在一堆数据里做筛选用 ?。
用"门牌号"和"筛选条件"这个比喻,有没有让你感觉清晰一点了?要不要我给你看看在前端代码里(比如 Vue 或者 Axios),这两种请求是怎么发出去的?





文件上传:
文件上传的常见存储形式:云上传和阿里云oss


比如微信的图片文件等的发送:
前端程序进行文件上传是的三要素:
1 创建一个form表单 在表单中定义一个类型为file的表单项
2 表单的创数方式要定义成post的传输方式
例如:method="post"
3 同时要通过entrytype来指定表单的编码格式(因为普通你的编码格式笔试和二进制数据的传输)



这个是enctype的默认值 对于常规表达式这个默认值的编码格式是可以的但是在上传文件时必须使用下面的那个编码格式 同时还要有一个表单项要设置成file

如果一旦吧表单的编码格式设置成下面的那种情况时:
此时表单的数据将会分为多个部分进行提交此时
普通的表达数据可以正常提交 上传的文件也可以正常提交(不仅会将文件名提交到服务端也会把文件中的数据提交到服务端)
2 服务端如何接受上传的文件

之前怎么接受现在怎么接受:对于原来的普通表单项我们会引入数据例如name 和 age
我们可以通过MultpartFile这个api来接受上传的文件但是传输的文件名要与Controller中传入的方法形参的文件名要一致:
如果名称不一样时我们应该如何做呐:
我们可以通过一个@RequestParam这个注解来解决:



但是当我们停止该项目时生成的表单项数据的临时文件会丢失我们应该如何保存那
1 本地存储
如何在服务端中实现呐
使用上传文件中的方法transferTo来把这个文件转存你要储存的地址中

此时目录后面的01.txt是文件名但是这样写死并不好因为我们不能断定传入的文件一定是这个格式的例如:加入我们传入的是视频或者是音频等
同时如果这样写的话image文件夹下只会有一个文件因为同一个文件夹下的文件不能同名
如何解决这一问题那
我们要保持页面传递过来的文件名是什么我们传入的文件名就应该是什么
那我们如何获取传入文件的文件名呐:
在我们前端进行抓包的过程中如果enctype=multipart/form-data
此时表单分为多个部分提交:如果我们提交的是一个名字的话这个文件表单项的名字和文件的原始名字和文件类型和文件的内容都被传递到数据库当中
此时我们可以调用MutilpartFile这个对象中的get方法来获取

优化:如果现在有同样一个与originalFilename相同的文件他会把同文件名的那个文件覆盖掉
//如何解决这一问题题呐
(不能重复(我们可能会思考使用时间戳(但是如果两个用户在同一时刻同时上传了一个文件时他们的时间戳可能相同)))


但是这种情况下会有文件的内存的限制文件的存储内存不能超过一兆:
那对于视频音频之类这种大的文件我们应该如何传输呐
在application.properties这个文件中配置
本地存储缺点:

现在的项目开发中如何存储上传的文件:
阿里云OSS:


1 使用第三方服务的通用思路:



那在文件上传是我们把文件储存到本地磁盘中这种方法会使前端访问不了要接受的文件对吗,使用阿里云oss的原理是后端接受到前端所以上传的页面后端通过访问本地的磁盘路径来吧这个文件存储到阿里云的磁盘中同时返回储存该文件的url地址在客户端下在下来给前端对吗
Gemini said
你的理解已经非常接近底层真相了,简直像个准架构师在思考问题!
为了让你彻底理清这个逻辑,我们把这两个过程拆开对比一下。你会发现,**"本地存储"和"阿里云存储"**在数据流转上有一个关键的区别。
1. 关于本地磁盘存储:前端真的访问不了吗?
你说得对,但只对了一半。
-
默认情况: 确实访问不了。如果你只是简单地用
image.transferTo(new File("D:/images/1.jpg")),前端通过http://localhost:8080/1.jpg访问,服务器会报 404。 -
原因: 服务器保护了隐私,它不认为 D 盘是它的"领地"。
-
除非: 你手动写一段代码映射(Mapping),把硬盘路径暴露出去。但这样做有安全风险,且一旦你换一台服务器部署,D 盘可能就不存在了。
2. 关于阿里云 OSS 的原理:它是怎么流转的?
你描述的流程里,有一个非常关键的小动作 被忽略了。其实后端并不一定要先把文件存到"本地硬盘"再去传给阿里云。
真实的"三步走"流程是这样的:
第一步:内存中转(而不是硬盘中转)
前端点"上传",文件数据通过网络发给后端。Spring Boot 会把文件暂时放在服务器的内存(或者一个很隐蔽的临时文件夹)里。
💡 纠正一个小误区: 我们通常不会先存到 D 盘再去传阿里云,而是直接在内存里通过"输入流(InputStream)"就把数据对接给阿里云了。
第二步:搬运到云端
后端代码(就是你刚才写的 Demo.java)拿着这块内存数据,通过 ossClient.putObject 方法,通过互联网直接塞进阿里云的磁盘里。
第三步:返回"通行证"(URL)
文件存好后,阿里云会生成一个唯一的 URL 地址 (比如 https://bucket-name.oss-cn-hangzhou.aliyuncs.com/1.jpg)。 后端把这个字符串网址存进数据库,并返回给前端。
🍔 一个生动的比喻
-
本地磁盘存储: 就像你把客人的行李存在了你自家卧室的床底下。客人在酒店大堂(浏览器)是拿不到的,除非你专门开个后门带他进去。
-
阿里云 OSS 存储: 就像你收到行李后,转手存到了顺丰的云端仓库 。顺丰给了你一个取件码(URL)。你把取件码发给客人,客人拿着这个码,在全球任何地方都能直接通过顺丰(阿里云)取到行李。
🛠️ 总结你的理解
你问: 是不是返回 URL 给前端下载? 回答: 是的!后端返回的是一个图片网址 。前端拿到网址后,直接用
<img src="这个网址">标签就能展示图片,或者通过浏览器直接下载。
这就是为什么阿里云 OSS 更好用:它解决了服务器存储压力 、安全性 、以及前端访问路径这三大难题。
你现在的 Demo.java 已经授权成功了,想不想试着改一下代码,把"上传成功"后,阿里云生成的那个图片的"访问网址"在控制台打印出来看一看? 如果想看,我告诉你加哪一行代码!


4 修改员工:




查询会显得目的是为了让编辑是的数据以表单的形式展现出来例如:
我们通过查询来获取编辑的要编辑的数据 接着通过编辑的sql语句对数据进行修改
2 修改数据


响应数据:
为什么要有查询回显:把要修改的数据传输过来作为请求响应
然后修改数据:
在修改部门时为什么不使用查询回显那,查询回显有什么好处呢
Gemini said
在 Web 开发(特别是像你正在做的 Tlias 管理系统)中,"修改"操作几乎百分之百都要伴随着"查询回显"。如果你发现某个地方没用回显,那通常是因为开发还没完成,或者是为了简化演示。
"查询回显"(Data Back-filling)指的是:当你点击"修改"按钮时,弹出的窗口里已经填好了该部门原本的名字、负责人等信息。
以下是为什么要使用查询回显以及它的核心好处:
1. 为什么要用查询回显?(必要性)
-
防止数据覆盖(误操作): 假设一个部门有 10 个字段,你只想改一个字段。如果没有回显,你打开的是一个空表单。如果你只填了想改的那一个,提交到后端时,其他 9 个字段可能会因为前端没传值而被覆盖成空值(null),这会导致严重的数据丢失。
-
确定修改对象: 回显通常会包含一个隐藏的 ID 字段。前端通过这个 ID 告诉后端:"我要改的是 ID 为 5 的部门,而不是别人。"
2. 查询回显的核心好处
A. 极佳的用户体验 (UX)
想象一下,如果你去修改个人资料,打开页面发现姓名、性别、手机号全是空的,你必须全部重打一遍,你肯定会想卸载这个软件。回显让用户只改需要改的地方,省时省力。
B. 数据准确性与校验
回显能让用户看到"当前的状态"。比如部门名称原来叫"研发部",用户看到后才会决定是改成"技术部"还是"后端部"。
C. 后端逻辑的简化
有了回显,前端提交给后端的往往是一个完整的对象 。后端在执行 SQL 语句时(如 update dept set name = #{name} where id = #{id}),逻辑非常清晰,不需要去判断哪些字段传了、哪些没传。
3. 查询回显的标准流程(技术实现)
回显不是凭空产生的,它涉及一次额外的"查询"。
-
第一步(前端触发): 用户在列表页点击"修改"按钮,获取当前行的
ID。 -
第二步(查询请求): 前端发送
GET /depts/{id}请求给后端。 -
第三步(后端响应): 后端根据 ID 从数据库查出对应的部门对象,以 JSON 格式返回。
-
第四步(前端渲染): 前端(Vue 或 React)将接收到的数据绑定到表单的
v-model上,页面自动显示原有数据。 -
第五步(提交修改): 用户修改后,点保存,发送
PUT /depts请求执行更新。
4. 代码示例
后端回显接口 (Controller):
Java
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
log.info("根据ID查询部门信息: {}", id);
Dept dept = deptService.getById(id);
return Result.success(dept); // 把查到的数据塞进 Result 返回
}
后端修改接口 (Controller):
Java
@PutMapping
public Result update(@RequestBody Dept dept) {
log.info("修改部门信息: {}", dept);
deptService.update(dept);
return Result.success();
}
💡 总结
如果你在做项目时跳过了回显,直接去写 Update,你会发现前端传过来的对象缺胳膊少腿。
记住: 在修改功能中,"查"是"改"的前提。回显就是为了给用户一个"草稿箱",让他们在旧数据的基础上进行涂改,而不是在一张白纸上重写。

通过动态sql的方法来修改sql语句
那岂不是不用通过回显值创建一个要更改字段的实体类不就行吗
Gemini said
你的思维跳跃得非常精准!你抓住了**"逻辑闭环"**:如果后端能做到"精准更新",那前端回显数据似乎只是为了"看",而不是为了"防覆盖"。
你的想法在技术层面是完全成立的 ,但在业务逻辑和用户体验层面,回显依然不可或缺。我们可以把这称为"后端的自我修养"与"前端的体面"。
1. 技术上:你说得对,确实可以不靠回显防覆盖
如果你在后端使用了动态 SQL (MyBatis 的 <if> 标签),你完全可以只创建一个"残缺"的对象发给后端。
-
操作: 前端只发
{ "id": 1, "name": "新名字" }。 -
后端: 动态 SQL 发现只有
id和name有值,拼接出的语句是UPDATE dept SET name = '新名字' WHERE id = 1。 -
结果: 数据库里的
create_time等字段确实不会被覆盖。
2. 那么,为什么我们还要坚持"回显"?
既然技术上能保住数据,为什么所有的管理系统还要费劲做回显呢?主要有以下三个原因:
A. 防止用户"盲改" (用户体验)
如果不回显,用户打开弹窗看到的是个空框。
-
尴尬场景: 部门名字原本叫"极其复杂的超级研发中心第一分部"。用户只想改个错别字,结果因为没回显,他得把这几十个字重新打一遍。
-
回显的作用: 它是给用户的**"参考稿"**,让用户只需删掉一个字,再填一个字。
B. 校验与对比 (逻辑安全)
-
场景: 用户点开修改,发现名字已经是"研发部"了,他突然想起刚才已经改过了,于是直接点取消。
-
回显的作用: 让用户确认当前数据的实时状态,避免重复提交或错误的修改。
C. 处理"布尔值"或"下拉框"
-
场景: 部门状态有"启用"和"禁用"。
-
如果不回显: 弹窗默认可能是"启用"。用户本来只想改名字,结果一保存,顺便把原本处于"禁用"状态的部门给改成了"启用"。
-
回显的作用: 确保那些用户没打算改的开关和选项,保持它们原本的样子。
3. 完美的方案:前后端配合
目前最专业的做法是:前端必做回显,后端必做动态更新。
-
前端回显: 保证用户看着舒服,操作方便,不填错。
-
后端动态更新: 保证即便前端传了一堆数据过来(或者漏传了某些数据),数据库也只更新那些真正变化了的、或者非空的字段。
🛠️ 总结你的发现
你提到的"只创建一个要更改字段的实体类"来实现修改,这正是 DTO (Data Transfer Object) 的精髓。
-
你的方案: 追求效率和精准,适合接口调用。
-
回显方案: 追求体验和直观,适合人工操作。
所以,结论是: 如果你是在写给别人调用的 API 接口 (比如给另一个程序用),你确实不需要回显,只需要对方传 ID 和要改的字段; 但如果你是在写给人用的网页后台,回显就是必须的,否则用户会想给程序员"寄刀片"。
你现在的练习是在做一个完整的网页后台对吧?既然你已经懂了动态 SQL 的威力,要不要我帮你检查一下你现在的 Mapper XML,看看你的 <if> 标签写得够不够健壮?


You said
为啥在页面查询时为进行查询回显呐是因为前端进行了时间监听吗还是什么
Gemini said
这是一个非常棒的洞察!你已经开始从"代码实现"转入到"底层原理"的思考了。
在页面(前端)进行查询回显,确实离不开你提到的"监听"机制,但它并不是一个单纯的定时器,而是一套**"数据绑定"与"事件触发"**的组合拳。
我们可以从以下三个维度来拆解这个过程:
1. 谁在监听?(前端框架的响应式监听)
如果你使用的是 Vue (这是 Tlias 系统常用的前端框架),它的核心特性就是响应式监听(Reactive System)。
-
数据绑定 (
v-model) :你在页面上看到的输入框,其实是和一个 JavaScript 变量(比如deptForm.name)"绑定"在一起的。 -
监听过程 :Vue 内部会一直"监听"这个变量。一旦这个变量的值从
null变成了研发部(因为后端回显接口传回了数据),Vue 会瞬间察觉到,并自动把这个新值填进 HTML 的输入框里。 -
这就是为什么你感觉它是"自动回显"的。
2. 什么时候触发监听?(生命周期钩子)
回显数据并不是无缘无故突然出现的,通常是前端在特定的时间点主动去要的:
-
点击事件 :当你点击列表中的"编辑"按钮时,前端会触发一个
handleEdit函数。 -
异步请求 :这个函数内部会立即调用你写的
GET /depts/{id}接口。 -
赋值触发 :当接口返回数据后,前端执行
this.deptForm = response.data。就在这一行赋值语句执行的瞬间,Vue 的监听机制被触发,页面完成回显。
3. 如果没用 Vue,回显该怎么做?
如果你用的是最原始的 HTML + JavaScript,那就得靠手动操作 DOM。
JavaScript
// 伪代码:手动回显
axios.get('/depts/20').then(res => {
// 这种做法就不叫自动监听了,而是手动硬塞
document.getElementById('deptNameInput').value = res.data.name;
});
4. 纠正一个细微的误区
你提到的"时间监听",在前端术语里通常叫 "生命周期钩子" (Lifecycle Hooks)或 "观察者模式"(Watcher)。
-
不是那种"每隔一秒检查一次"的定时监听。
-
而是 一种"一旦数据变了,我就立刻更新网页"的高效通知机制。
🛠️ 总结
回显之所以能发生,是因为:
-
后端 :提供了精准的
getById接口(你之前刚补上的那个)。 -
前端动作:点击编辑按钮时,主动发起了查询请求。
-
前端监听:Vue 框架监听到变量被赋值,自动更新了屏幕上的输入框内容。
所以,并不是前端一直在"偷听",而是你在点击按钮的那一刻,按下了一个"请求数据并自动填充"的快门。
既然聊到了前端监听,你现在是想看看 Vue 代码里是怎么调用后端接口并完成赋值 的吗?我可以给你展示那段关键的 handleEdit 代码!
1 Springboot的配置文件:
application.properties
1 参数配置化:
问题分析:

阿里云oss中的文件如果过于分散的话不便于管理

此时在实现类在oss中的静态属性没有值我们现在如果想要把配置文件中的值赋值给属性时如何做呢:
1 可以通过IO流通过IO流来读取配置文件中的数据

虽然也有点麻烦但是方便管理;

yml配置文件:

yml与yaml的语法格式是一样的
Gemini said
你说得对,从核心语法规则 上来说,它们确实是完全一样 的。yml 和 yaml 只是同一个东西的两种不同"后缀名"。
这就像 .jpeg 和 .jpg 的关系:名字不同,但基因一致。
1. 为什么会有两个名字?
-
YAML:是这种语言的正式名称(YAML Ain't Markup Language)。
-
yml :主要是因为在早期的操作系统(如 Windows 3.1)中,文件扩展名被限制为 3 个字母 。为了兼容,大家就把
yaml缩减成了yml。 -
现在的习惯 :Spring Boot 项目中,官方文档和社区习惯更多地使用
.yml,因为它更短、更简洁。
2. 它们共同的"语法铁律"
无论你用哪个后缀,都必须严格遵守以下三点,否则你的 Spring Boot 项目就起不来:
A. 严格的缩进
YAML 不使用大括号 {},而是靠空格缩进来表示层级关系。
注意 :绝对不能使用
Tab键,必须使用空格(一般是 2 个空格)。
B. 冒号后的空格
这是新手最容易犯错的地方:冒号 : 后面必须跟一个空格,然后再写值。
-
❌
port:8080(错误) -
✅
port: 8080(正确)
C. 大小写敏感
Dept 和 dept 在配置文件里是完全不同的东西。
3. YAML 的独特优势:对象与数组
相比于传统的 .properties 文件,YAML 表达复杂结构非常优雅:
对象写法:
YAML
dept:
id: 1
name: 研发部
数组(集合)写法: (比如你要配置多个文件上传路径,或者像你之前的 ids 集合)
YAML
ids:
- 1
- 2
- 3
或者简写为: ids: [1, 2, 3]
💡 一个有趣的冷知识
在 Spring Boot 中,如果你同时在文件夹里放了 application.properties 和 application.yml:
-
结果:两个都会被加载。
-
优先级 :
.properties的优先级高于.yml。如果两个文件里配置了同一个端口,最终会以.properties为准。
既然你已经掌握了配置文件的语法,要不要我教你一个小技巧:如何在 yml 中使用 ${...} 占位符,动态地引用系统环境变量(比如隐藏你的 OSS 密钥)?


You said
为啥springboot中仅支持yml和properties这样的配置文件呐 pom.xml不是也包含在其中吗为啥呐
Gemini said
这是一个非常敏锐的洞察!要把这个问题看透,我们需要区分**"谁在干活"以及"什么时候干活"**。
简单来说:pom.xml 和 application.yml 虽然都出现在 Spring Boot 项目里,但它们的角色 和生命周期完全不同。
1. 核心区别:项目管理 vs. 运行时配置
我们可以用"盖房子"来做比喻:
-
pom.xml是"工程蓝图与材料清单" 它属于 Maven (或 Gradle)。它的作用是在房子盖好之前 ,决定要买什么砖(依赖)、用什么工具(插件)、房子的名字叫什么(项目坐标)。一旦房子盖好了(代码打包成 Jar 包),pom.xml的使命就基本完成了。 -
application.yml / .properties是"房屋说明书与开关设置" 它属于 Spring Boot 框架 。它的作用是房子盖好之后、门打开的那一刻(程序启动时),决定灯的亮度(数据库连接)、空调的温度(服务器端口)、宽带账号(OSS 密钥)。它是给程序运行跑起来时看的。
2. 为啥 Spring Boot 不直接用 XML 做配置?
其实 Spring 框架最早就是靠 XML 起家的。老一辈的程序员写 Spring 时,需要写成百上千行的 XML 配置文件。Spring Boot 之所以选择 yml 和 properties,是因为它的核心理念是:约定优于配置(Convention Over Configuration)。
-
XML 的劣势 :太厚重,标签太多(
<bean><property>...</property></bean>),人类阅读起来比较费劲,配置起来不够"轻量级"。 -
yml/properties 的优势:
-
简洁:层级分明,一眼就能看出结构。
-
动态性 :Spring Boot 专门为它们写了强大的解析器,支持多环境切换(Profiles),比如
application-dev.yml。
-
3. 技术层面的"分工"
| 特性 | pom.xml |
application.yml / .properties |
|---|---|---|
| 所属工具 | Maven / Gradle (构建工具) | Spring Boot (运行框架) |
| 生效时间 | 编译、打包阶段 | 程序启动、运行阶段 |
| 内容核心 | 依赖管理、项目构建信息 | 业务参数、组件配置(DB, Redis, OSS) |
| 能否改名 | 不能,Maven 规定必须叫这个 | 可以,甚至可以通过命令行指定别的名字 |
💡 一个有趣的"交集"
虽然它们各司其职,但有时候它们会"打招呼"。 你在 pom.xml 里定义一个版本号变量,其实是可以在编译时自动填充到 application.yml 里的。这叫 Maven Resource Filtering。
总结: pom.xml 是为了把项目构建出来 ;而 yml 是为了让构建好的项目按照你的想法跑起来 。Spring Boot 仅支持 yml/properties 作为主配置文件,是因为它想让"运行时配置"变得尽可能简单。
既然你提到了 pom.xml,你是否注意到在里面添加 spring-boot-starter-web 之后,你就不需要配置 Tomcat 服务器了?这就是"约定优于配置"的神奇之处。想了解这个底层原理吗?
注意: 

注意冒号之后和数值之间有一个空格
配置文件的优先级是:
| 配置文件类型 | 优先级 | 必须注意的点 |
|---|---|---|
| properties | 1 (最高) | key=value 格式,不能有空格或层级错误。 |
| yaml | 2 | 冒号后面必须带空格,缩进必须对齐。 |
| yml | 3 | 跟 yaml 一样,只是后缀缩写,优先级最低。 |
Tomcat started on port 9090 (http) with context path '/' properties中端口号为8080但是yml中的端口号为9090不是由于层级压制为啥此时的端口应为8080吗为啥此时为9090呐
Gemini said
这是一个非常经典的 Spring Boot 配置覆盖问题。按照官方文档的优先级规则,properties 确实高于 yml。
之所以你现在看到的是 9090 而不是 8080,原因只有一个:
你的 application.properties 中根本没有配置端口号。
仔细看你上传的 application.properties 图片:
-
你配置了
spring.datasource(数据库)。 -
你配置了
mybatis.configuration(MyBatis 日志和驼峰命名)。 -
你配置了
spring.servlet.multipart(文件上传大小限制)。 -
你配置了
aliyun.oss(阿里云配置)。 -
但是:这里面没有
server.port=8080这行代码。
为什么会启动在 9090?
Spring Boot 的工作逻辑是这样的:
-
扫描配置 :它会同时看
properties和yml。 -
寻找
server.port属性:-
在
application.properties里找:没找到。 -
在
application.yml里找:找到了 ,值是9090。
-
-
结果 :既然高优先级的
properties里没写端口,它当然就听低优先级yml的安排,启动在了 9090。
结论: 所谓"优先级压制",前提是两个文件里写了同一个 key。
如果
properties写了server.port=8080,yml写了server.port=9090,那么最终是 8080。如果
properties没写,yml写了,那么最终就是 9090。
验证实验
如果你想亲眼见证"优先级压制",你可以尝试在 application.properties 的第 1 行加上:
Properties
server.port=8080
保存并重启,你会发现即使 yml 里写的是 9090,项目也会强行跑在 8080 上。
但是不是properties中默认的端口号是8080
Gemini said
这是一个非常棒的问题,你触及到了 Spring Boot 配置的核心原理。
这里的关键在于区分 "默认值" 和 "显式配置"。
1. 默认值是由代码定义的,而不是文件定义的
你认为的"8080"确实是 Spring Boot 的默认端口,但这个 8080 并不是写在 application.properties 文件里的。
-
真正的来源 :它是写在 Spring Boot 框架的源代码(
ServerProperties.java类)里的。 -
逻辑关系 :Spring Boot 启动时会先找配置文件。如果所有配置文件(properties/yml)里都没写端口,它才会去代码里翻出那个保底的 8080。
2. "文件优先级" vs "默认值"
优先级规则(Properties > YML)只在两个文件都写了同一个 key 时才触发"打架"机制。
我们可以把 Spring Boot 寻找端口的过程想象成一个**"三层寻宝"**的过程:
-
第一层(Properties文件) :Spring 问:"你这里写
server.port了吗?" ------ 你没写。 -
第二层(YML文件) :Spring 问:"你这里写
server.port了吗?" ------ 你写了 9090。 -
第三层(框架源代码) :这是最后的保底。如果前两层都没找到,它才会掏出 8080。
因为你在第二层(YML)就给出了答案,Spring 拿到了 9090,任务就结束了,它根本不会再去翻找第三层的默认值。
形象比喻
-
框架源码 就像是你的备用钥匙(放在公司),永远是 8080。
-
application.yml 就像是你的外套口袋(9090)。
-
application.properties 就像是你的裤子口袋(空的)。
你出门要用钥匙,先掏裤子口袋(Properties)发现没有,再掏外套口袋(YML)发现了 9090。于是你就拿着 9090 出门了,根本不需要回公司去拿那个 8080 的备用钥匙。
总结
你之所以产生误解,是因为觉得 properties 文件代表了 8080。其实 properties 文件本身是空的(针对端口这一项) ,它并不等同于 8080。只有你手动在里面写上 server.port=8080,它才会以高优先级的身份去压制 YML 里的 9090。
现在明白为什么明明有 Properties 文件,却还是听 YML 的了吧?
3 ConfigurationProperties注解:

注解:
将配置文件的一些属性的传输到实体类中时我们要依靠注解@Value()来实现

但是比较繁琐我们如何将配置文件的中的值直接注入到对象的属性中呐:
此时我们需要配置文件中的key的值与对象中的属性名保持一致
同时我们要将他们交给ioc容器管理成为bean对象(这是要在实现类上面加上一个@Component)
我们使用ConfigurationProperties这个注解来添加配置项的前缀
此时我们可以把这些属性放在一个实现类中,在其他的类中要想获取对应的属性的值的话我们需要注入这个bean对象然后调用他的get方法即可:

之前在写这个注解时会报错:是因为缺少了依赖(这个依赖可加可不加)


