十、“仿小红书”单体全栈项目开发实战(四)
作者:互联网
2026-04-16
1.1 笔记模块编辑、删除功能概述
- 笔记模块编辑功能:实现一个仿小红书的笔记修改界面,包含标题编辑、内容编辑、话题标签管理、分类管理等核心功能。
- 笔记模块删除功能:实现笔记的删除。
核心功能与设计特点
-
编辑界面布局:
- 顶部导航栏包含返回和保存按钮
- 清晰的展示标题、图片、内容、话题标签和分类
-
图片展示功能:
- 图片网格布局展示已上传图片
-
内容编辑:
- 标题输入框支持修改
- 内容编辑区域支持修改
- 输入验证确保内容完整性
-
话题标签管理:
- 支持添加多个话题标签(用空格分隔)
- 标签删除功能
-
交互体验:
- 操作反馈提示
- 删除确认提示
- 表单验证和错误提示
1.2 使用Bootstrap、Font Awesome以及Thymeleaf轻松实现笔记编辑界面
下面我将为你实现一个仿小红书的笔记修改界面,包含标题编辑、内容编辑、话题标签管理、分类管理等核心功能。
界面设计与实现
可以基于note-publish.html进行修改,只需要删除”图片上传区域“相关的样式、组件即可,
以下是笔记修改界面note-edit.html的完整实现代码:
https://images.jiaoben.nethtml>
https://images.jiaoben.nethtml https://images.jiaoben.netlang=https://images.jiaoben.net"en" https://images.jiaoben.netxmlns:th=https://images.jiaoben.net"http://www.thymeleaf.org">
https://images.jiaoben.nethead >
https://images.jiaoben.netmeta https://images.jiaoben.netcharset=https://images.jiaoben.net"UTF-8">
https://images.jiaoben.netmeta https://images.jiaoben.netname=https://images.jiaoben.net"viewport" https://images.jiaoben.netcontent=https://images.jiaoben.net"width=device-width, initial-scale=1.0">
https://images.jiaoben.nettitle >RN - 笔记编辑https://images.jiaoben.nettitle>
https://images.jiaoben.net
https://images.jiaoben.netlink https://images.jiaoben.nethref=https://images.jiaoben.net"https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.6/css/bootstrap.min.css"
https://images.jiaoben.netth:href=https://images.jiaoben.net"@{/css/bootstrap.min.css}" https://images.jiaoben.netrel=https://images.jiaoben.net"stylesheet">
https://images.jiaoben.net
https://images.jiaoben.netlink https://images.jiaoben.nethref=https://images.jiaoben.net"https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
https://images.jiaoben.netth:href=https://images.jiaoben.net"@{/css/font-awesome.min.css}" https://images.jiaoben.netrel=https://images.jiaoben.net"stylesheet">
https://images.jiaoben.net
https://images.jiaoben.netstyle >https://images.jiaoben.net
https://images.jiaoben.net/* 基础样式 */
https://images.jiaoben.netbody {
https://images.jiaoben.netbackground-color: https://images.jiaoben.net#fef6f6;
https://images.jiaoben.netfont-family: -apple-system, BlinkMacSystemFont, https://images.jiaoben.net"Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
https://images.jiaoben.net.container {
https://images.jiaoben.netmax-width: https://images.jiaoben.net768px;
https://images.jiaoben.netmargin: https://images.jiaoben.net0 auto;
https://images.jiaoben.netpadding: https://images.jiaoben.net0 https://images.jiaoben.net16px;
}
https://images.jiaoben.net/* 顶部导航栏 */
https://images.jiaoben.net.header {
https://images.jiaoben.netbackground-color: white;
https://images.jiaoben.netborder-bottom: https://images.jiaoben.net1px solid https://images.jiaoben.net#eee;
https://images.jiaoben.netpadding: https://images.jiaoben.net12px https://images.jiaoben.net0;
https://images.jiaoben.netposition: sticky;
https://images.jiaoben.nettop: https://images.jiaoben.net0;
https://images.jiaoben.netz-index: https://images.jiaoben.net100;
}
https://images.jiaoben.net.header https://images.jiaoben.net.btn {
https://images.jiaoben.netpadding: https://images.jiaoben.net6px https://images.jiaoben.net16px;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net20px;
https://images.jiaoben.netfont-weight: https://images.jiaoben.net600;
}
https://images.jiaoben.net.btn-cancel {
https://images.jiaoben.netcolor: https://images.jiaoben.net#333;
https://images.jiaoben.netborder: https://images.jiaoben.net1px solid https://images.jiaoben.net#ddd;
}
https://images.jiaoben.net.btn-publish {
https://images.jiaoben.netbackground-color: https://images.jiaoben.net#ff2442;
https://images.jiaoben.netcolor: white;
https://images.jiaoben.netborder: none;
}
https://images.jiaoben.net.btn-publishhttps://images.jiaoben.net:hover {
https://images.jiaoben.netbackground-color: https://images.jiaoben.net#e61e3a;
}
https://images.jiaoben.net/* 内容区域 */
https://images.jiaoben.net.content {
https://images.jiaoben.netpadding: https://images.jiaoben.net16px https://images.jiaoben.net0;
}
https://images.jiaoben.net/* 标题输入框 */
https://images.jiaoben.net.note-title {
https://images.jiaoben.netborder: none;
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
https://images.jiaoben.netfont-size: https://images.jiaoben.net20px;
https://images.jiaoben.netfont-weight: https://images.jiaoben.net600;
https://images.jiaoben.netpadding: https://images.jiaoben.net12px https://images.jiaoben.net0;
https://images.jiaoben.netoutline: none;
}
https://images.jiaoben.net.note-titlehttps://images.jiaoben.net::placeholder {
https://images.jiaoben.netcolor: https://images.jiaoben.net#999;
}
https://images.jiaoben.net/* 已上传图片展示 */
https://images.jiaoben.net.uploaded-images {
https://images.jiaoben.netdisplay: flex;
https://images.jiaoben.netflex-wrap: wrap;
https://images.jiaoben.netgap: https://images.jiaoben.net8px;
https://images.jiaoben.netmargin-top: https://images.jiaoben.net16px;
}
https://images.jiaoben.net.uploaded-image {
https://images.jiaoben.netwidth: https://images.jiaoben.net80px;
https://images.jiaoben.netheight: https://images.jiaoben.net80px;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net8px;
https://images.jiaoben.netoverflow: hidden;
https://images.jiaoben.netposition: relative;
}
https://images.jiaoben.net.uploaded-image https://images.jiaoben.netimg {
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
https://images.jiaoben.netheight: https://images.jiaoben.net100%;
https://images.jiaoben.netobject-fit: cover;
}
https://images.jiaoben.net.uploaded-image https://images.jiaoben.net.delete-btn {
https://images.jiaoben.netposition: absolute;
https://images.jiaoben.nettop: https://images.jiaoben.net4px;
https://images.jiaoben.netright: https://images.jiaoben.net4px;
https://images.jiaoben.netwidth: https://images.jiaoben.net20px;
https://images.jiaoben.netheight: https://images.jiaoben.net20px;
https://images.jiaoben.netbackground-color: https://images.jiaoben.netrgba(https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0.6);
https://images.jiaoben.netcolor: white;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net50%;
https://images.jiaoben.netdisplay: flex;
https://images.jiaoben.netalign-items: center;
https://images.jiaoben.netjustify-content: center;
https://images.jiaoben.netcursor: pointer;
https://images.jiaoben.netfont-size: https://images.jiaoben.net12px;
}
https://images.jiaoben.net/* 笔记内容编辑器 */
https://images.jiaoben.net.note-content {
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
https://images.jiaoben.netmin-height: https://images.jiaoben.net200px;
https://images.jiaoben.netborder: none;
https://images.jiaoben.netoutline: none;
https://images.jiaoben.netfont-size: https://images.jiaoben.net16px;
https://images.jiaoben.netline-height: https://images.jiaoben.net1.6;
https://images.jiaoben.netpadding: https://images.jiaoben.net12px https://images.jiaoben.net0;
}
https://images.jiaoben.net.note-contenthttps://images.jiaoben.net::placeholder {
https://images.jiaoben.netcolor: https://images.jiaoben.net#999;
}
https://images.jiaoben.net/* 话题选择 */
https://images.jiaoben.net.topic-input {
https://images.jiaoben.netposition: relative;
https://images.jiaoben.netmargin-bottom: https://images.jiaoben.net20px;
}
https://images.jiaoben.net.topic-input https://images.jiaoben.netinput {
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
https://images.jiaoben.netpadding: https://images.jiaoben.net12px;
https://images.jiaoben.netborder: https://images.jiaoben.net1px solid https://images.jiaoben.net#eee;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net8px;
https://images.jiaoben.netoutline: none;
}
https://images.jiaoben.net/* 分类选择 */
https://images.jiaoben.net.category-selector {
https://images.jiaoben.netmargin-bottom: https://images.jiaoben.net20px;
}
https://images.jiaoben.net.category-input https://images.jiaoben.neti {
https://images.jiaoben.netcolor: https://images.jiaoben.net#ff2442;
}
https://images.jiaoben.net/* 添加到 style 标签中 */
https://images.jiaoben.net.category-selector select {
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
https://images.jiaoben.netpadding: https://images.jiaoben.net12px;
https://images.jiaoben.netborder: https://images.jiaoben.net1px solid https://images.jiaoben.net#eee;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net8px;
https://images.jiaoben.netbackground-color: white;
appearance: none;
-webkit-appearance: none;
https://images.jiaoben.netbackground-image: https://images.jiaoben.neturl(https://images.jiaoben.net"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23666'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
https://images.jiaoben.netbackground-repeat: no-repeat;
https://images.jiaoben.netbackground-position: right https://images.jiaoben.net12px center;
https://images.jiaoben.netbackground-size: https://images.jiaoben.net16px;
https://images.jiaoben.netcursor: pointer;
}
https://images.jiaoben.net.category-selector selecthttps://images.jiaoben.net:focus {
https://images.jiaoben.netoutline: none;
https://images.jiaoben.netborder-color: https://images.jiaoben.net#ff2442;
https://images.jiaoben.netbox-shadow: https://images.jiaoben.net0 https://images.jiaoben.net0 https://images.jiaoben.net0 https://images.jiaoben.net2px https://images.jiaoben.netrgba(https://images.jiaoben.net255, https://images.jiaoben.net36, https://images.jiaoben.net66, https://images.jiaoben.net0.1);
}
https://images.jiaoben.net.btn-view-note {
https://images.jiaoben.netbackground-color: https://images.jiaoben.net#ff2442;
https://images.jiaoben.netcolor: white;
}
https://images.jiaoben.net.error-message {
https://images.jiaoben.netcolor: https://images.jiaoben.net#ff2442;
https://images.jiaoben.netfont-size: https://images.jiaoben.net12px;
https://images.jiaoben.netmargin-top: https://images.jiaoben.net4px;
}
https://images.jiaoben.netstyle>
https://images.jiaoben.nethead>
https://images.jiaoben.netbody >
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"header">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"d-flex justify-content-between align-items-center">
https://images.jiaoben.netbutton https://images.jiaoben.netclass=https://images.jiaoben.net"btn btn-cancel" https://images.jiaoben.netid=https://images.jiaoben.net"cancelPublishBtn">
取消
https://images.jiaoben.netbutton>
https://images.jiaoben.netbutton https://images.jiaoben.netclass=https://images.jiaoben.net"btn btn-publish" https://images.jiaoben.netid=https://images.jiaoben.net"publishNoteBtn">
保存
https://images.jiaoben.netbutton>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container content">
https://images.jiaoben.netform https://images.jiaoben.netid=https://images.jiaoben.net"noteForm" https://images.jiaoben.netmethod=https://images.jiaoben.net"post" https://images.jiaoben.netth:object=https://images.jiaoben.net"${note}"
https://images.jiaoben.netth:action=https://images.jiaoben.net"@{/note/{noteId}(noteId=${note.noteId})}">
https://images.jiaoben.net
https://images.jiaoben.netinput https://images.jiaoben.nettype=https://images.jiaoben.net"text" https://images.jiaoben.netclass=https://images.jiaoben.net"note-title" https://images.jiaoben.netid=https://images.jiaoben.net"title" https://images.jiaoben.netname=https://images.jiaoben.net"title"
https://images.jiaoben.netth:field=https://images.jiaoben.net"*{title}" https://images.jiaoben.netplaceholder=https://images.jiaoben.net"分享你的生活点滴...">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"error-message" https://images.jiaoben.netth:if=https://images.jiaoben.net"${#fields.hasErrors('title')}" https://images.jiaoben.netth:errors=https://images.jiaoben.net"*{title}">
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"uploaded-images" https://images.jiaoben.netid=https://images.jiaoben.net"uploadedImages">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"uploaded-image" https://images.jiaoben.netth:each=https://images.jiaoben.net"image : ${note.images}">
https://images.jiaoben.netimg https://images.jiaoben.netth:src=https://images.jiaoben.net"${image}" https://images.jiaoben.netclass=https://images.jiaoben.net"preview-img">
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"error-message" https://images.jiaoben.netth:if=https://images.jiaoben.net"${#fields.hasErrors('images')}" https://images.jiaoben.netth:errors=https://images.jiaoben.net"*{images}">
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.nettextarea https://images.jiaoben.netclass=https://images.jiaoben.net"note-content" https://images.jiaoben.netid=https://images.jiaoben.net"content" https://images.jiaoben.netname=https://images.jiaoben.net"content"
https://images.jiaoben.netth:field=https://images.jiaoben.net"*{content}" https://images.jiaoben.netplaceholder=https://images.jiaoben.net"详细描述你的分享内容...">https://images.jiaoben.nettextarea>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"error-message" https://images.jiaoben.netth:if=https://images.jiaoben.net"${#fields.hasErrors('content')}" https://images.jiaoben.netth:errors=https://images.jiaoben.net"*{content}">
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"topic-input">
https://images.jiaoben.netinput https://images.jiaoben.nettype=https://images.jiaoben.net"text" https://images.jiaoben.netclass=https://images.jiaoben.net"form-control" https://images.jiaoben.netid=https://images.jiaoben.net"topicInput" https://images.jiaoben.netname=https://images.jiaoben.net"topics"
https://images.jiaoben.netth:field=https://images.jiaoben.net"*{topics}" https://images.jiaoben.netplaceholder=https://images.jiaoben.net"添加话题,多个话题用空格隔开">
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-selector">
https://images.jiaoben.netlabel https://images.jiaoben.netfor=https://images.jiaoben.net"categorySelect" https://images.jiaoben.netclass=https://images.jiaoben.net"form-label">请选择一个分类:https://images.jiaoben.netlabel>
https://images.jiaoben.netselect https://images.jiaoben.netclass=https://images.jiaoben.net"form-control" https://images.jiaoben.netid=https://images.jiaoben.net"categorySelect" https://images.jiaoben.netname=https://images.jiaoben.net"category"
https://images.jiaoben.netth:field=https://images.jiaoben.net"*{category}">
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"穿搭">穿搭https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"美食">美食https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"彩妆">彩妆https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"影视">影视https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"职场">职场https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"情感">情感https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"家居">家居https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"游戏">游戏https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"旅行">旅行https://images.jiaoben.netoption>
https://images.jiaoben.netoption https://images.jiaoben.netvalue=https://images.jiaoben.net"健身">健身https://images.jiaoben.netoption>
https://images.jiaoben.netselect>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"error-message" https://images.jiaoben.netth:if=https://images.jiaoben.net"${#fields.hasErrors('category')}" https://images.jiaoben.netth:errors=https://images.jiaoben.net"*{category}">
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.netform>
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netth:if=https://images.jiaoben.net"${success}" https://images.jiaoben.netclass=https://images.jiaoben.net"alert alert-success mt-4">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-check-circle">https://images.jiaoben.neti>
[[${success}]]
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netth:if=https://images.jiaoben.net"${error}" https://images.jiaoben.netclass=https://images.jiaoben.net"alert alert-danger mt-4">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-exclamation-circle">https://images.jiaoben.neti>
[[${error}]]
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.netscript https://images.jiaoben.netsrc=https://images.jiaoben.net"https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.6/js/bootstrap.bundle.min.js"
https://images.jiaoben.netth:src=https://images.jiaoben.net"@{/js/bootstrap.bundle.min.js}">https://images.jiaoben.netscript>
https://images.jiaoben.netscript >https://images.jiaoben.net
https://images.jiaoben.net// 笔记发布表单的校验
https://images.jiaoben.net// 在发布按钮上设置点击事件
https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net"publishNoteBtn").https://images.jiaoben.netaddEventListener(https://images.jiaoben.net"click", https://images.jiaoben.netfunction (https://images.jiaoben.netevent) {
https://images.jiaoben.net// 获取笔记标题
https://images.jiaoben.netconst title = https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net"title").https://images.jiaoben.netvalue;
https://images.jiaoben.netif (title.https://images.jiaoben.nettrim() === https://images.jiaoben.net"") {
https://images.jiaoben.netalert(https://images.jiaoben.net"请输入笔记标题");
https://images.jiaoben.netreturn;
}
https://images.jiaoben.net// 获取笔记内容
https://images.jiaoben.netconst content = https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net"content").https://images.jiaoben.netvalue;
https://images.jiaoben.netif (content.https://images.jiaoben.nettrim() === https://images.jiaoben.net"") {
https://images.jiaoben.netalert(https://images.jiaoben.net"请输入笔记内容");
https://images.jiaoben.netreturn;
}
https://images.jiaoben.net// 提交表单
https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net"noteForm").https://images.jiaoben.netsubmit();
})
https://images.jiaoben.net// 取消发布的事件处理
https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net"cancelPublishBtn").https://images.jiaoben.netaddEventListener(https://images.jiaoben.net"click", https://images.jiaoben.netfunction (https://images.jiaoben.netevent) {
https://images.jiaoben.net// 用户确认是否取消发布
https://images.jiaoben.netif (https://images.jiaoben.netconfirm(https://images.jiaoben.net"确定要取消发布吗?所有内容将不会被保存")) {
https://images.jiaoben.netwindow.https://images.jiaoben.nethistory.https://images.jiaoben.netback();
}
})
https://images.jiaoben.netscript>
https://images.jiaoben.netbody>
https://images.jiaoben.nethtml>
1.3 NoteController控制器来处理笔记编辑请求
在原有的NoteController基础上,增加方法以实现相关功能。
创建笔记编辑DTO
https://images.jiaoben.netpackage com.waylau.rednote.dto;
https://images.jiaoben.netimport jakarta.validation.constraints.NotEmpty;
https://images.jiaoben.netimport jakarta.validation.constraints.NotNull;
https://images.jiaoben.netimport jakarta.validation.constraints.Size;
https://images.jiaoben.netimport lombok.Getter;
https://images.jiaoben.netimport lombok.Setter;
https://images.jiaoben.netimport java.util.ArrayList;
https://images.jiaoben.netimport java.util.List;
https://images.jiaoben.net/**
* NoteEditDto 笔记编辑DTO
*
* https://images.jiaoben.net@author Way Lau
* https://images.jiaoben.net@version 2025/08/19
**/
https://images.jiaoben.net@Getter
https://images.jiaoben.net@Setter
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netNoteEditDto {
https://images.jiaoben.net@NotNull
https://images.jiaoben.netprivate Long noteId;
https://images.jiaoben.net@NotEmpty(message = "标题不能为空")
https://images.jiaoben.net@Size(max = 60, message = "标题长度不能超过60个字符")
https://images.jiaoben.netprivate String title;
https://images.jiaoben.net@NotEmpty(message = "内容不能为空")
https://images.jiaoben.net@Size(max = 900, message = "内容长度不能超过900个字符")
https://images.jiaoben.netprivate String content;
https://images.jiaoben.netprivate String topics;
https://images.jiaoben.net@NotEmpty(message = "分类不能为空")
https://images.jiaoben.netprivate String category;
https://images.jiaoben.netprivate List images = https://images.jiaoben.netnew https://images.jiaoben.netArrayList<>();
}
处理用户访问笔记编辑界面展示
新增方法如下。
https://images.jiaoben.net/**
* 显示笔记编辑页面
*/
https://images.jiaoben.net@GetMapping("/{noteId}/edit")
https://images.jiaoben.netpublic String https://images.jiaoben.netshowEditFormhttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long noteId, Model model) {
https://images.jiaoben.net// 查询指定noteId的笔记
Optional optionalNote = noteService.findNoteById(noteId);
https://images.jiaoben.net// 判定笔记是否存在,不存在则抛出异常
https://images.jiaoben.netif (!optionalNote.isPresent()) {
https://images.jiaoben.netthrow https://images.jiaoben.netnew https://images.jiaoben.netNoteNotFoundException(https://images.jiaoben.net"");
}
https://images.jiaoben.net// 获取当前用户信息
https://images.jiaoben.netUser https://images.jiaoben.netuser https://images.jiaoben.net= userService.getCurrentUser();
https://images.jiaoben.netNote https://images.jiaoben.netnote https://images.jiaoben.net= optionalNote.get();
https://images.jiaoben.net// 判定笔记是否属于当前用户,不属于则抛出异常
https://images.jiaoben.netif (!note.getAuthor().getUserId().equals(user.getUserId())) {
https://images.jiaoben.netthrow https://images.jiaoben.netnew https://images.jiaoben.netNoteNotFoundException(https://images.jiaoben.net"");
}
https://images.jiaoben.net// 将Note对象转为NoteEditDto对象
https://images.jiaoben.netNoteEditDto https://images.jiaoben.netnoteEditDto https://images.jiaoben.net= https://images.jiaoben.netnew https://images.jiaoben.netNoteEditDto();
noteEditDto.setNoteId(note.getNoteId());
noteEditDto.setTitle(note.getTitle());
noteEditDto.setContent(note.getContent());
noteEditDto.setCategory(note.getCategory());
noteEditDto.setImages(note.getImages());
https://images.jiaoben.net// 话题的List要转为String
noteEditDto.setTopics(StringUtil.joinToString(note.getTopics(), https://images.jiaoben.net" "));
model.addAttribute(https://images.jiaoben.net"note", noteEditDto);
https://images.jiaoben.netreturn https://images.jiaoben.net"note-edit";
}
当用户使用GET请求访问/note/{noteId}/edit时,则会返回note-edit.html模板页面。
需要注意是的,返回前端的NoteEditDto的topics是字符串类型,因此从Note获取到值之后,需要通过StringUtil.joinToString()工具做转换。
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netStringUtil {
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
https://images.jiaoben.net// List转字符串
https://images.jiaoben.netpublic https://images.jiaoben.netstatic String https://images.jiaoben.netjoinToStringhttps://images.jiaoben.net(List source, String regex) {
https://images.jiaoben.netreturn String.join(regex, source);
}
}
控制器处理用户笔记编辑请求
新增方法如下。
https://images.jiaoben.net/**
* 处理笔记编辑请求
*/
https://images.jiaoben.net@PostMapping("/{noteId}")
https://images.jiaoben.netpublic String https://images.jiaoben.netupdateNotehttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long noteId,
https://images.jiaoben.net@Valid https://images.jiaoben.net@ModelAttribute("note") NoteEditDto noteEditDto,
BindingResult result,
Model model,
RedirectAttributes redirectAttributes) {
https://images.jiaoben.net// 验证表单
https://images.jiaoben.netif (result.hasErrors()) {
model.addAttribute(https://images.jiaoben.net"note", noteEditDto);
https://images.jiaoben.netreturn https://images.jiaoben.net"note-edit";
}
https://images.jiaoben.net// 检查笔记是否存在
Optional optionalNote = noteService.findNoteById(noteId);
https://images.jiaoben.netif (!optionalNote.isPresent()) {
https://images.jiaoben.netthrow https://images.jiaoben.netnew https://images.jiaoben.netNoteNotFoundException(https://images.jiaoben.net"");
}
https://images.jiaoben.netNote https://images.jiaoben.netnote https://images.jiaoben.net= optionalNote.get();
https://images.jiaoben.nettry {
noteService.updateNote(note, noteEditDto);
redirectAttributes.addFlashAttribute(https://images.jiaoben.net"success", https://images.jiaoben.net"笔记更新成功");
https://images.jiaoben.netreturn https://images.jiaoben.net"redirect:/note/" + noteId;
} https://images.jiaoben.netcatch (Exception e) {
log.error(https://images.jiaoben.net"笔记更新失败:{}", e.getMessage(), e);
model.addAttribute(https://images.jiaoben.net"error", https://images.jiaoben.net"笔记更新失败:" + e.getMessage());
model.addAttribute(https://images.jiaoben.net"note", noteEditDto);
https://images.jiaoben.netreturn https://images.jiaoben.net"note-edit";
}
}
当用户使用POST请求访问/note/{noteId}时,将修改后的笔记数据保存入库。
1.4 实现笔记编辑数据的保存方法
修改NoteService,增加如下接口:
https://images.jiaoben.netpublic https://images.jiaoben.netinterface https://images.jiaoben.netNoteService {
https://images.jiaoben.net/**
* 更新笔记
*
* https://images.jiaoben.net@param note
* https://images.jiaoben.net@param noteEditDto
*/
https://images.jiaoben.netvoid https://images.jiaoben.netupdateNotehttps://images.jiaoben.net(Note note, NoteEditDto noteEditDto);
}
``
修改 NoteServiceImpl,实现笔记编辑数据的保存方法:
```java
https://images.jiaoben.netimport com.waylau.rednote.dto.NoteEditDto;
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
https://images.jiaoben.net@Service
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netNoteServiceImpl https://images.jiaoben.netimplements https://images.jiaoben.netNoteService {
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
https://images.jiaoben.net@Override
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netupdateNotehttps://images.jiaoben.net(Note note, NoteEditDto noteEditDto) {
https://images.jiaoben.net// 更新基本信息
note.setTitle(noteEditDto.getTitle());
note.setContent(noteEditDto.getContent());
note.setCategory(noteEditDto.getCategory());
https://images.jiaoben.net// 字符串转为List
note.setTopics(StringUtil.splitToList(noteEditDto.getTopics(),https://images.jiaoben.net" "));
https://images.jiaoben.net// 保存更新
noteRepository.save(note);
}
}
需要注意是的,前端传入的NoteEditDto的topics是字符串类型,在赋值到Note时,需要通过StringUtil.splitToList()工具做转换。
1.5 修改不可变集合导致UnsupportedOperationException错误分析
运行应用,试图保存笔记修改后的数据时,报错如下图10-1所示。
问题背景
执行 noteRepository.save(note) 时候报 java.lang.UnsupportedOperationException:
https://images.jiaoben.netjavahttps://images.jiaoben.net.langhttps://images.jiaoben.net.UnsupportedOperationException: https://images.jiaoben.netnull
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjavahttps://images.jiaoben.net.utilhttps://images.jiaoben.net.AbstractListhttps://images.jiaoben.net.remove(AbstractList.https://images.jiaoben.netjava:https://images.jiaoben.net169) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjavahttps://images.jiaoben.net.utilhttps://images.jiaoben.net.AbstractList$https://images.jiaoben.netItrhttps://images.jiaoben.net.remove(AbstractList.https://images.jiaoben.netjava:https://images.jiaoben.net389) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjavahttps://images.jiaoben.net.utilhttps://images.jiaoben.net.AbstractListhttps://images.jiaoben.net.removeRange(AbstractList.https://images.jiaoben.netjava:https://images.jiaoben.net600) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjavahttps://images.jiaoben.net.utilhttps://images.jiaoben.net.AbstractListhttps://images.jiaoben.net.clear(AbstractList.https://images.jiaoben.netjava:https://images.jiaoben.net245) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.typehttps://images.jiaoben.net.CollectionTypehttps://images.jiaoben.net.replaceElements(CollectionType.https://images.jiaoben.netjava:https://images.jiaoben.net506) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.typehttps://images.jiaoben.net.CollectionTypehttps://images.jiaoben.net.replace(CollectionType.https://images.jiaoben.netjava:https://images.jiaoben.net719) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.typehttps://images.jiaoben.net.TypeHelperhttps://images.jiaoben.net.replace(TypeHelper.https://images.jiaoben.netjava:https://images.jiaoben.net117) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.eventhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.DefaultMergeEventListenerhttps://images.jiaoben.net.copyValues(DefaultMergeEventListener.https://images.jiaoben.netjava:https://images.jiaoben.net596) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.eventhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.DefaultMergeEventListenerhttps://images.jiaoben.net.entityIsPersistent(DefaultMergeEventListener.https://images.jiaoben.netjava:https://images.jiaoben.net286) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.eventhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.DefaultMergeEventListenerhttps://images.jiaoben.net.merge(DefaultMergeEventListener.https://images.jiaoben.netjava:https://images.jiaoben.net220) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.eventhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.DefaultMergeEventListenerhttps://images.jiaoben.net.doMerge(DefaultMergeEventListener.https://images.jiaoben.netjava:https://images.jiaoben.net152) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.eventhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.DefaultMergeEventListenerhttps://images.jiaoben.net.onMerge(DefaultMergeEventListener.https://images.jiaoben.netjava:https://images.jiaoben.net136) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.eventhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.DefaultMergeEventListenerhttps://images.jiaoben.net.onMerge(DefaultMergeEventListener.https://images.jiaoben.netjava:https://images.jiaoben.net89) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.eventhttps://images.jiaoben.net.servicehttps://images.jiaoben.net.internalhttps://images.jiaoben.net.EventListenerGroupImplhttps://images.jiaoben.net.fireEventOnEachListener(EventListenerGroupImpl.https://images.jiaoben.netjava:https://images.jiaoben.net127) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.internalhttps://images.jiaoben.net.SessionImplhttps://images.jiaoben.net.fireMerge(SessionImpl.https://images.jiaoben.netjava:https://images.jiaoben.net854) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.internalhttps://images.jiaoben.net.SessionImplhttps://images.jiaoben.net.merge(SessionImpl.https://images.jiaoben.netjava:https://images.jiaoben.net840) ~https://images.jiaoben.net[hibernate-core-6.6.15.Final.jar:6.6.15.Final]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjdkhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.reflecthttps://images.jiaoben.net.DirectMethodHandleAccessorhttps://images.jiaoben.net.invoke(DirectMethodHandleAccessor.https://images.jiaoben.netjava:https://images.jiaoben.net104) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjavahttps://images.jiaoben.net.langhttps://images.jiaoben.net.reflecthttps://images.jiaoben.net.Methodhttps://images.jiaoben.net.invoke(Method.https://images.jiaoben.netjava:https://images.jiaoben.net565) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.ormhttps://images.jiaoben.net.jpahttps://images.jiaoben.net.ExtendedEntityManagerCreator$https://images.jiaoben.netExtendedEntityManagerInvocationHandlerhttps://images.jiaoben.net.invoke(ExtendedEntityManagerCreator.https://images.jiaoben.netjava:https://images.jiaoben.net364) ~https://images.jiaoben.net[spring-orm-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netjdkhttps://images.jiaoben.net.proxy2/https://images.jiaoben.netjdkhttps://images.jiaoben.net.proxy2.$https://images.jiaoben.netProxy120https://images.jiaoben.net.merge(Unknown Source) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjdkhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.reflecthttps://images.jiaoben.net.DirectMethodHandleAccessorhttps://images.jiaoben.net.invoke(DirectMethodHandleAccessor.https://images.jiaoben.netjava:https://images.jiaoben.net104) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjavahttps://images.jiaoben.net.langhttps://images.jiaoben.net.reflecthttps://images.jiaoben.net.Methodhttps://images.jiaoben.net.invoke(Method.https://images.jiaoben.netjava:https://images.jiaoben.net565) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.ormhttps://images.jiaoben.net.jpahttps://images.jiaoben.net.SharedEntityManagerCreator$https://images.jiaoben.netSharedEntityManagerInvocationHandlerhttps://images.jiaoben.net.invoke(SharedEntityManagerCreator.https://images.jiaoben.netjava:https://images.jiaoben.net320) ~https://images.jiaoben.net[spring-orm-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netjdkhttps://images.jiaoben.net.proxy2/https://images.jiaoben.netjdkhttps://images.jiaoben.net.proxy2.$https://images.jiaoben.netProxy120https://images.jiaoben.net.merge(Unknown Source) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.jpahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.supporthttps://images.jiaoben.net.SimpleJpaRepositoryhttps://images.jiaoben.net.save(SimpleJpaRepository.https://images.jiaoben.netjava:https://images.jiaoben.net654) ~https://images.jiaoben.net[spring-data-jpa-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjdkhttps://images.jiaoben.net.internalhttps://images.jiaoben.net.reflecthttps://images.jiaoben.net.DirectMethodHandleAccessorhttps://images.jiaoben.net.invoke(DirectMethodHandleAccessor.https://images.jiaoben.netjava:https://images.jiaoben.net104) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netjavahttps://images.jiaoben.net.base/https://images.jiaoben.netjavahttps://images.jiaoben.net.langhttps://images.jiaoben.net.reflecthttps://images.jiaoben.net.Methodhttps://images.jiaoben.net.invoke(Method.https://images.jiaoben.netjava:https://images.jiaoben.net565) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.aophttps://images.jiaoben.net.supporthttps://images.jiaoben.net.AopUtilshttps://images.jiaoben.net.invokeJoinpointUsingReflection(AopUtils.https://images.jiaoben.netjava:https://images.jiaoben.net359) ~https://images.jiaoben.net[spring-aop-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.RepositoryMethodInvoker$https://images.jiaoben.netRepositoryFragmentMethodInvokerhttps://images.jiaoben.net.lambda$https://images.jiaoben.netnew$https://images.jiaoben.net0(RepositoryMethodInvoker.https://images.jiaoben.netjava:https://images.jiaoben.net277) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.RepositoryMethodInvokerhttps://images.jiaoben.net.doInvoke(RepositoryMethodInvoker.https://images.jiaoben.netjava:https://images.jiaoben.net170) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.RepositoryMethodInvokerhttps://images.jiaoben.net.invoke(RepositoryMethodInvoker.https://images.jiaoben.netjava:https://images.jiaoben.net158) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.RepositoryComposition$https://images.jiaoben.netRepositoryFragmentshttps://images.jiaoben.net.invoke(RepositoryComposition.https://images.jiaoben.netjava:https://images.jiaoben.net515) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.RepositoryCompositionhttps://images.jiaoben.net.invoke(RepositoryComposition.https://images.jiaoben.netjava:https://images.jiaoben.net284) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.RepositoryFactorySupport$https://images.jiaoben.netImplementationMethodExecutionInterceptorhttps://images.jiaoben.net.invoke(RepositoryFactorySupport.https://images.jiaoben.netjava:https://images.jiaoben.net734) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.aophttps://images.jiaoben.net.frameworkhttps://images.jiaoben.net.ReflectiveMethodInvocationhttps://images.jiaoben.net.proceed(ReflectiveMethodInvocation.https://images.jiaoben.netjava:https://images.jiaoben.net184) ~https://images.jiaoben.net[spring-aop-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.QueryExecutorMethodInterceptorhttps://images.jiaoben.net.doInvoke(QueryExecutorMethodInterceptor.https://images.jiaoben.netjava:https://images.jiaoben.net174) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.corehttps://images.jiaoben.net.supporthttps://images.jiaoben.net.QueryExecutorMethodInterceptorhttps://images.jiaoben.net.invoke(QueryExecutorMethodInterceptor.https://images.jiaoben.netjava:https://images.jiaoben.net149) ~https://images.jiaoben.net[spring-data-commons-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.aophttps://images.jiaoben.net.frameworkhttps://images.jiaoben.net.ReflectiveMethodInvocationhttps://images.jiaoben.net.proceed(ReflectiveMethodInvocation.https://images.jiaoben.netjava:https://images.jiaoben.net184) ~https://images.jiaoben.net[spring-aop-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.transactionhttps://images.jiaoben.net.interceptorhttps://images.jiaoben.net.TransactionAspectSupporthttps://images.jiaoben.net.invokeWithinTransaction(TransactionAspectSupport.https://images.jiaoben.netjava:https://images.jiaoben.net380) ~https://images.jiaoben.net[spring-tx-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.transactionhttps://images.jiaoben.net.interceptorhttps://images.jiaoben.net.TransactionInterceptorhttps://images.jiaoben.net.invoke(TransactionInterceptor.https://images.jiaoben.netjava:https://images.jiaoben.net119) ~https://images.jiaoben.net[spring-tx-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.aophttps://images.jiaoben.net.frameworkhttps://images.jiaoben.net.ReflectiveMethodInvocationhttps://images.jiaoben.net.proceed(ReflectiveMethodInvocation.https://images.jiaoben.netjava:https://images.jiaoben.net184) ~https://images.jiaoben.net[spring-aop-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.daohttps://images.jiaoben.net.supporthttps://images.jiaoben.net.PersistenceExceptionTranslationInterceptorhttps://images.jiaoben.net.invoke(PersistenceExceptionTranslationInterceptor.https://images.jiaoben.netjava:https://images.jiaoben.net138) ~https://images.jiaoben.net[spring-tx-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.aophttps://images.jiaoben.net.frameworkhttps://images.jiaoben.net.ReflectiveMethodInvocationhttps://images.jiaoben.net.proceed(ReflectiveMethodInvocation.https://images.jiaoben.netjava:https://images.jiaoben.net184) ~https://images.jiaoben.net[spring-aop-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.datahttps://images.jiaoben.net.jpahttps://images.jiaoben.net.repositoryhttps://images.jiaoben.net.supporthttps://images.jiaoben.net.CrudMethodMetadataPostProcessor$https://images.jiaoben.netCrudMethodMetadataPopulatingMethodInterceptorhttps://images.jiaoben.net.invoke(CrudMethodMetadataPostProcessor.https://images.jiaoben.netjava:https://images.jiaoben.net165) ~https://images.jiaoben.net[spring-data-jpa-3.5.0.jar:3.5.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.aophttps://images.jiaoben.net.frameworkhttps://images.jiaoben.net.ReflectiveMethodInvocationhttps://images.jiaoben.net.proceed(ReflectiveMethodInvocation.https://images.jiaoben.netjava:https://images.jiaoben.net184) ~https://images.jiaoben.net[spring-aop-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.aophttps://images.jiaoben.net.frameworkhttps://images.jiaoben.net.JdkDynamicAopProxyhttps://images.jiaoben.net.invoke(JdkDynamicAopProxy.https://images.jiaoben.netjava:https://images.jiaoben.net223) ~https://images.jiaoben.net[spring-aop-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netjdkhttps://images.jiaoben.net.proxy2/https://images.jiaoben.netjdkhttps://images.jiaoben.net.proxy2.$https://images.jiaoben.netProxy132https://images.jiaoben.net.save(Unknown Source) ~https://images.jiaoben.net[na:na]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.waylauhttps://images.jiaoben.net.rednotehttps://images.jiaoben.net.servicehttps://images.jiaoben.net.implhttps://images.jiaoben.net.NoteServiceImplhttps://images.jiaoben.net.updateNote(NoteServiceImpl.https://images.jiaoben.netjava:https://images.jiaoben.net86) ~https://images.jiaoben.net[classes/:na]
分析
核心代码位置:
https://images.jiaoben.net@Override
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netupdateNotehttps://images.jiaoben.net(Note note, NoteEditDto noteEditDto) {
https://images.jiaoben.net// 更新基本信息
note.setTitle(noteEditDto.getTitle());
note.setContent(noteEditDto.getContent());
note.setCategory(noteEditDto.getCategory());
https://images.jiaoben.net// 字符串转为List
note.setTopics(StringUtil.splitToList(noteEditDto.getTopics(),https://images.jiaoben.net" "));
https://images.jiaoben.net// 保存更新
noteRepository.save(note);
}
其中,实体Note的topics是由StringUtil.splitToList()生成的。splitToList实现如下:
https://images.jiaoben.netpublic https://images.jiaoben.netstatic List https://images.jiaoben.netsplitToListhttps://images.jiaoben.net(String source, String regex) {
https://images.jiaoben.netif (source.isEmpty()) {
https://images.jiaoben.netreturn Collections.emptyList();
}
https://images.jiaoben.netreturn Arrays.asList(source.split(regex));
}
Arrays.asList() 返回的集合是不可变集合,而 Hibernate 在执行持久化操作时需要修改这些集合。
整改方案
在保存前临时替换集合:
https://images.jiaoben.net@Override
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netupdateNotehttps://images.jiaoben.net(Note note, NoteEditDto noteEditDto) {
https://images.jiaoben.net// 更新基本信息
note.setTitle(noteEditDto.getTitle());
note.setContent(noteEditDto.getContent());
note.setCategory(noteEditDto.getCategory());
https://images.jiaoben.net// 字符串转为List
https://images.jiaoben.net// 确保体使用可变集合实现
https://images.jiaoben.net// note.setTopics(StringUtil.splitToList(noteEditDto.getTopics()," "));
note.setTopics(https://images.jiaoben.netnew https://images.jiaoben.netArrayList<>(StringUtil.splitToList(noteEditDto.getTopics(),https://images.jiaoben.net" ")));
https://images.jiaoben.net// 保存更新
noteRepository.save(note);
}
运行调测
下图10-2所示的是笔记编辑页面。
下图10-3所示的是笔记编辑成功后的页面。
总结
UnsupportedOperationException 通常表示你正在尝试修改一个不可变集合。确保你的实体使用可变集合实现(如 ArrayList),并在DTO到实体转换过程中创建新的可变集合实例。
1.6 从笔记详情页面触发编辑、删除笔记的请求
在笔记详情页面操作栏上已经预留了编辑、删除笔记的按钮。如下图10-4所示。
接下来实现从编辑、删除笔记的按钮执行触发编辑、删除笔记的请求。
修改编辑笔记按钮事件
https://images.jiaoben.net
https://images.jiaoben.neta https://images.jiaoben.netth:href=https://images.jiaoben.net"@{/note/{noteId}/edit(noteId=${note.noteId})}">
https://images.jiaoben.netbutton https://images.jiaoben.netclass=https://images.jiaoben.net"btn btn-light btn-sm" https://images.jiaoben.netth:if=https://images.jiaoben.net"${#authentication.name == note.author.username}">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-edit">https://images.jiaoben.neti>
https://images.jiaoben.netbutton>
https://images.jiaoben.neta>
修改删除笔记的按钮事件
修改删除的按钮事件,在设置id属性和onclick事件处理:
https://images.jiaoben.net
https://images.jiaoben.netbutton https://images.jiaoben.netclass=https://images.jiaoben.net"btn btn-light btn-sm" https://images.jiaoben.netth:if=https://images.jiaoben.net"${#authentication.name == note.author.username}"
https://images.jiaoben.netth:onclick=https://images.jiaoben.net"deleteNote([[${note.noteId}]])">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-trash">https://images.jiaoben.neti>
https://images.jiaoben.netbutton>
deleteNote()函数定义如下:
https://images.jiaoben.net// 处理笔记删除
https://images.jiaoben.netfunction https://images.jiaoben.netdeleteNote(https://images.jiaoben.netnoteId) {
https://images.jiaoben.netif (https://images.jiaoben.netconfirm(https://images.jiaoben.net"确定要删除此笔记吗?")) {
https://images.jiaoben.netfetch(https://images.jiaoben.net`/note/https://images.jiaoben.net${noteId}`, {
https://images.jiaoben.netmethod: https://images.jiaoben.net'DELETE'
})
.https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netresponse => {
https://images.jiaoben.netif (response.https://images.jiaoben.netok) {
response.https://images.jiaoben.netjson().https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netdata => {
https://images.jiaoben.net// 从响应中获取提示信息
https://images.jiaoben.netalert(data.https://images.jiaoben.netmessage || https://images.jiaoben.net'删除成功');
https://images.jiaoben.net// 从响应中获取重定向URL
https://images.jiaoben.netwindow.https://images.jiaoben.netlocation.https://images.jiaoben.nethref = data.https://images.jiaoben.netredirectUrl;
});
} https://images.jiaoben.netelse {
response.https://images.jiaoben.netjson().https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netdata => {
https://images.jiaoben.netalert(data.https://images.jiaoben.netmessage || https://images.jiaoben.net'删除失败,请重试');
});
}
})
.https://images.jiaoben.netcatch(https://images.jiaoben.nethttps://images.jiaoben.neterror => {
https://images.jiaoben.netconsole.https://images.jiaoben.neterror(https://images.jiaoben.net'删除失败:', error);
https://images.jiaoben.netalert(https://images.jiaoben.net'删除失败,请稍后重试');
})
}
}
通过fetch()来发送DELETE请求。fetch 是一个现代化的 JavaScript API,用于发送网络请求并获取资源。它是浏览器提供的全局方法,可以替代传统的 XMLHttpRequest。fetch 支持 Promise,因此更易用且代码更清晰。
1.7 掌握@DeleteMapping针对DELETE请求的特殊处理
增加控制器方法
在原有的NoteController基础上,增加方法以实现相关功能。
https://images.jiaoben.net/**
* 处理删除笔记的请求
*/
https://images.jiaoben.net@DeleteMapping("/{noteId}")
https://images.jiaoben.netpublic ResponseEntity https://images.jiaoben.netdeleteNotehttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long noteId) {
https://images.jiaoben.net// 检查笔记是否存在
Optional optionalNote = noteService.findNoteById(noteId);
https://images.jiaoben.netif (!optionalNote.isPresent()) {
https://images.jiaoben.netthrow https://images.jiaoben.netnew https://images.jiaoben.netNoteNotFoundException(https://images.jiaoben.net"");
}
https://images.jiaoben.netNote https://images.jiaoben.netnote https://images.jiaoben.net= optionalNote.get();
https://images.jiaoben.net// 获取当前用户信息
https://images.jiaoben.netUser https://images.jiaoben.netuser https://images.jiaoben.net= userService.getCurrentUser();
https://images.jiaoben.net// 判定笔记是否属于当前用户,不属于则抛出异常
https://images.jiaoben.netif (!note.getAuthor().getUserId().equals(user.getUserId())) {
https://images.jiaoben.netthrow https://images.jiaoben.netnew https://images.jiaoben.netNoteNotFoundException(https://images.jiaoben.net"");
}
https://images.jiaoben.net// 使用服务删除笔记
noteService.deleteNote(note);
https://images.jiaoben.net// 返回响应的内容
https://images.jiaoben.netDeleteResponseDto https://images.jiaoben.netdeleteResponseDto https://images.jiaoben.net= https://images.jiaoben.netnew https://images.jiaoben.netDeleteResponseDto();
deleteResponseDto.setMessage(https://images.jiaoben.net"笔记删除成功");
deleteResponseDto.setRedirectUrl(https://images.jiaoben.net"/user/profile");
https://images.jiaoben.netreturn ResponseEntity.ok(deleteResponseDto);
}
注:在 Spring MVC 中使用 @DeleteMapping 处理删除请求后,但不能使用RedirectAttributes进行重定向。这是因为:HTTP 规范中,DELETE 请求不应该有重定向响应。浏览器在处理 DELETE 请求的重定向时可能会遇到各种问题,如安全限制、缓存问题或行为不一致。因此,使用ResponseEntity作为响应体。
通用删除响应对象DeleteResponseDto
ResponseEntity作为响应体所包裹的对象是DeleteResponseDto,代码如下:
https://images.jiaoben.netpackage com.waylau.rednote.dto;
https://images.jiaoben.netimport lombok.Getter;
https://images.jiaoben.netimport lombok.Setter;
https://images.jiaoben.net/**
* DeleteResponseDto 执行删除的响应对象
*
* https://images.jiaoben.net@author Way Lau
* https://images.jiaoben.net@version 2025/06/12
**/
https://images.jiaoben.net@Getter
https://images.jiaoben.net@Setter
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netDeleteResponseDto {
https://images.jiaoben.net/**
* 信息
*/
https://images.jiaoben.netprivate String message;
https://images.jiaoben.net/**
* 重定向URL
*/
https://images.jiaoben.netprivate String redirectUrl;
}
上述对象可以用于任意DELETE请求的场景。
增加NoteRepository方法
修改NoteRepository增加方法如下:
https://images.jiaoben.net/**
* 删除笔记
*
* https://images.jiaoben.net@param note
*/
https://images.jiaoben.netvoid https://images.jiaoben.netdeletehttps://images.jiaoben.net(Note note);
删除笔记的服务
修改NoteService,增加如下接口:
https://images.jiaoben.netpublic https://images.jiaoben.netinterface https://images.jiaoben.netNoteService {
https://images.jiaoben.net/**
* 删除笔记
*
* https://images.jiaoben.net@param note
*/
https://images.jiaoben.netvoid https://images.jiaoben.netdeleteNotehttps://images.jiaoben.net(Note note);
}
``
修改NoteServiceImpl,实现笔记删除的方法:
```java
https://images.jiaoben.net@Override
https://images.jiaoben.net@Transactional
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netdeleteNotehttps://images.jiaoben.net(Note note) {
https://images.jiaoben.net// 注意:先删除数据库数据再删图片文件。以防止删除文件异常时,方便回滚数据库数据
https://images.jiaoben.net// 先删除数据库数据
noteRepository.delete(note);
https://images.jiaoben.net// 再删图片文件
List images = note.getImages();
https://images.jiaoben.netfor (String image : images) {
fileStorageService.deleteFile(image);
}
}
需要注意是的,上述方法既有删除文件的,又有删除数据库数据的。因此,需要加@Transactional进行事务管理,同时,先删库再删文件。这样,以在删除文件异常时,方便回滚数据库。
1.8 处理CSRF保护引发的HttpRequestMethodNotSupportedException异常
问题背景
运行应用,试图删除笔记时,报错如下图10-5所示。
同时在控制台日志里面看大如下信息:
2025-06-12T14:34:19.883+08:00 WARN 21324 --- https://images.jiaoben.net[rednote] https://images.jiaoben.net[io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved https://images.jiaoben.net[org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'DELETE' is not supported]
原因
系统已经启用了CSRF保护,在WebSecurityConfig配置如下:
https://images.jiaoben.net// 启用 CSRF 防护
.csrf(Customizer.withDefaults())
因此,使用JavaScript fetch API所发送的 DELETE 方法需要有效的 CSRF 令牌,否则会报错。
如何设置并获取 CSRF 令牌
首先,确保在你的 HTML 模板中有一个 meta 标签来存储 CSRF 令牌。Spring Security 默认会提供一个名为 _csrf 的令牌,你可以通过 Thymeleaf 将其插入到 meta 标签中。
修改user-profile.html,增加如下内容:
https://images.jiaoben.net
https://images.jiaoben.netmeta https://images.jiaoben.netname=https://images.jiaoben.net"_csrf" https://images.jiaoben.netth:content=https://images.jiaoben.net"${_csrf.token}">https://images.jiaoben.netmeta>
接着,在JavaScript fetch API所发送的 DELETE 方法头信息里面设置 CSRF 令牌:
https://images.jiaoben.net// 笔记删除
https://images.jiaoben.netfunction https://images.jiaoben.netdeleteNote(https://images.jiaoben.netnoteId) {
https://images.jiaoben.netif (https://images.jiaoben.netconfirm(https://images.jiaoben.net"确定要删除此笔记吗?")) {
https://images.jiaoben.netfetch(https://images.jiaoben.net`/note/https://images.jiaoben.net${noteId}`, {
https://images.jiaoben.netmethod: https://images.jiaoben.net'DELETE',
https://images.jiaoben.net// 添加请求头, 用于Spring Security CSRF
https://images.jiaoben.netheaders: {
https://images.jiaoben.net'X-CSRF-TOKEN': https://images.jiaoben.netdocument.https://images.jiaoben.netquerySelector(https://images.jiaoben.net'meta[name="_csrf"]').https://images.jiaoben.netgetAttribute(https://images.jiaoben.net'content')
}
})
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
运行调测
运行应用,删除笔记时,可以看到如下图10-6所示的提示框,说明笔记已经能够成功删除了。
点击提示框“确认”按钮,可以重定向到了用户信息管理界面,如下图10-7所示。
1.9 细粒度的访问控制确保只能作者修改、删除自己的笔记
在前面课程中介绍了,在对笔记进行编辑、删除的时候,是加了代码判断,确保只有笔记的作者才能修改、删除笔记的按钮。代码如下:
https://images.jiaoben.net// 获取当前用户信息
https://images.jiaoben.netUser https://images.jiaoben.netuser https://images.jiaoben.net= userService.getCurrentUser();
https://images.jiaoben.netNote https://images.jiaoben.netnote https://images.jiaoben.net= optionalNote.get();
https://images.jiaoben.net// 判定笔记是否属于当前用户,不属于则抛出异常
https://images.jiaoben.netif (!note.getAuthor().getUserId().equals(user.getUserId())) {
https://images.jiaoben.netthrow https://images.jiaoben.netnew https://images.jiaoben.netNoteNotFoundException(https://images.jiaoben.net"");
}
https://images.jiaoben.net// 执行后续业务
但这种编程方式固然可行,但略微繁琐。本节介绍一种通过声明式的方式来实现细粒度的访问控制。
Spring Security 的 @PreAuthorize 深入解析
@PreAuthorize 是 Spring Security 提供的一个强大注解,用于在方法调用前进行权限检查。它允许你基于表达式语言(SpEL)定义细粒度的访问控制规则,是实现方法级安全的核心工具之一。
@PreAuthorize 是一个方法级别的安全注解,用于在方法执行前验证当前用户是否具有执行该方法的权限。如果验证失败,Spring Security 会抛出 AccessDeniedException。
使用场景
- 基于角色的访问控制
- 基于权限的访问控制
- 动态权限检查
- 复杂业务逻辑的权限控制
基本语法
https://images.jiaoben.net@PreAuthorize("expression")
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netsomeMethodhttps://images.jiaoben.net() {
https://images.jiaoben.net// 方法实现
}
常用表达式
基于角色的访问控制
https://images.jiaoben.net@PreAuthorize("hasRole('ADMIN')")
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netadminOnlyMethodhttps://images.jiaoben.net() {
https://images.jiaoben.net// 只有ADMIN角色可以访问
}
基于权限的访问控制
https://images.jiaoben.net@PreAuthorize("hasAuthority('READ_PRIVILEGE')")
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netreadDatahttps://images.jiaoben.net() {
https://images.jiaoben.net// 只有拥有READ_PRIVILEGE权限的用户可以访问
}
组合多个条件
https://images.jiaoben.net@PreAuthorize("hasRole('USER') and hasAuthority('WRITE_PRIVILEGE')")
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netwriteDatahttps://images.jiaoben.net() {
https://images.jiaoben.net// 用户必须同时具有USER角色和WRITE_PRIVILEGE权限
}
使用方法参数
https://images.jiaoben.net@PreAuthorize("#id == authentication.principal.id")
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netdeleteUserhttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long id) {
https://images.jiaoben.net// 只有用户可以删除自己的账户
}
自定义权限检查
https://images.jiaoben.net@PreAuthorize("@customSecurityService.checkPermission(authentication, #resourceId, 'DELETE')")
https://images.jiaoben.netpublic https://images.jiaoben.netvoid https://images.jiaoben.netdeleteResourcehttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long resourceId) {
https://images.jiaoben.net// 调用自定义服务检查权限
}
自定义权限检查是否是作者自己
在NoteService中增加接口:
https://images.jiaoben.net/**
* 验证用户是否为笔记作者
*
* https://images.jiaoben.net@param noteId
* https://images.jiaoben.net@param username
* https://images.jiaoben.net@return
*/
https://images.jiaoben.netboolean https://images.jiaoben.netisAuthorhttps://images.jiaoben.net(Long noteId, String username);
在NoteServiceImpl中增加方法:
https://images.jiaoben.net@Override
https://images.jiaoben.netpublic https://images.jiaoben.netboolean https://images.jiaoben.netisAuthorhttps://images.jiaoben.net(Long noteId, String username) {
Optional optionalNote = noteRepository.findByNoteId(noteId);
https://images.jiaoben.netif (!optionalNote.isPresent()) {
https://images.jiaoben.netthrow https://images.jiaoben.netnew https://images.jiaoben.netNoteNotFoundException(https://images.jiaoben.net"");
}
https://images.jiaoben.netreturn username.equals(optionalNote.get().getAuthor().getUsername());
}
修改NoteController,在需要方法级别控制的方法上面加@PreAuthorize注解:
https://images.jiaoben.net@GetMapping("/{noteId}/edit")
https://images.jiaoben.net@PreAuthorize("@noteServiceImpl.isAuthor(#noteId, authentication.name)")
https://images.jiaoben.netpublic String https://images.jiaoben.netshowEditFormhttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long noteId, Model model) {
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
}
https://images.jiaoben.net@PostMapping("/{noteId}")
https://images.jiaoben.net@PreAuthorize("@noteServiceImpl.isAuthor(#noteId, authentication.name)")
https://images.jiaoben.netpublic String https://images.jiaoben.netupdateNotehttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long noteId,
https://images.jiaoben.net@Valid https://images.jiaoben.net@ModelAttribute("note") NoteEditDto noteEditDto,
BindingResult result,
Model model,
RedirectAttributes redirectAttributes) {
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
}
https://images.jiaoben.net@DeleteMapping("/{noteId}")
https://images.jiaoben.net@PreAuthorize("@noteServiceImpl.isAuthor(#noteId, authentication.name)")
https://images.jiaoben.netpublic ResponseEntity https://images.jiaoben.netdeleteNotehttps://images.jiaoben.net(https://images.jiaoben.net@PathVariable Long noteId) {
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
}
上述@noteServiceImpl中的noteServiceImpl是指NoteServiceImpl在Spring中的Bean的名称。
配置要求
要使用 @PreAuthorize,需要在配置类上启用方法级安全:
https://images.jiaoben.netimport org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
https://images.jiaoben.net@Configuration
https://images.jiaoben.net@EnableWebSecurity
https://images.jiaoben.net// 启用@PreAuthorize等注解
https://images.jiaoben.net// 等同于老版本的@EnableGlobalMethodSecurity(prePostEnabled = true)
https://images.jiaoben.net@EnableMethodSecurity
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netWebSecurityConfig {
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
}
Spring Security 的@EnableMethodSecurity注解用于开启方法级安全授权(Method Security),替代了旧版本中的@EnableGlobalMethodSecurity。以下是关键信息:
核心功能
- 灵活配置:支持基于Bean的配置方式,允许为不同授权类型(如JSR-250、Spring EL表达式等)单独设置配置。
- 权限校验:通过注解(如@PreAuthorize、@PostAuthorize)实现方法执行前后的权限验证。
与@EnableGlobalMethodSecurity的区别
- 版本差异:
@EnableMethodSecurity是Spring Security 5.6版本引入的替代方案,而@EnableGlobalMethodSecurity在5.6之前使用。 - 配置方式:
@EnableMethodSecurity支持更细粒度的配置(如JSR-250、Spring EL表达式等),而@EnableGlobalMethodSecurity仅提供三种预定义机制(prePostEnabled、securedEnabled、jsr250Enabled)。
总结
@PreAuthorize 提供了强大的方法级安全控制能力,通过 SpEL 表达式可以实现非常灵活的权限控制逻辑。它的主要优势包括:
- 细粒度控制:可以精确到方法甚至方法参数级别的权限控制
- 动态性:可以基于运行时信息(如用户属性、方法参数)进行权限检查
- 可读性:表达式语言直观易懂,便于维护
- 可扩展性:支持自定义 SpEL 函数和权限评估器
合理使用 @PreAuthorize 可以显著提高应用程序的安全性,同时保持代码的清晰和可维护性。
1.10 安全最佳实践总结及扩展建议
安全最佳实践总结
-
永远不要信任前端验证:
- 前端隐藏编辑按钮只是用户体验优化
- 真正的安全验证必须在后端完成
-
使用 HTTPS:
- 防止中间人攻击和会话劫持
-
会话管理:
- 使用 JWT 或 Session 管理用户身份
- 设置合理的过期时间
-
日志记录:
- 记录所有修改和删除操作
- 记录异常的访问尝试
- 记录权限检查失败的情况,便于审计和故障排查
-
CSRF 防护:
- 启用 Spring Security 的 CSRF 保护
- 对于 AJAX 请求,确保包含 CSRF 令牌
-
参数验证:
- 使用
@Valid注解验证请求参数 - 防止 SQL 注入和 XSS 攻击
- 使用
-
最小权限原则:
- 只授予用户完成工作所需的最小权限
-
性能考虑:
- 对于高频调用的方法,避免复杂的 SpEL 表达式
扩展建议
功能扩展建议
-
图片编辑功能:
- 添加图片裁剪、滤镜等编辑功能
- 支持图片排序调整
-
富文本编辑:
- 集成富文本编辑器,支持格式化文本
- 添加表情符号和贴纸功能
-
标签推荐:
- 基于内容自动推荐相关标签
- 热门标签快速选择
-
草稿保存:
- 自动保存草稿功能
- 草稿列表管理
-
发布设置:
- 隐私设置(公开、仅自己可见)
- 发布时间设置(立即发布、定时发布)
安全扩展建议
-
多级权限控制:
- 管理员可以修改/删除任何笔记
- 实现角色系统(ROLE_USER, ROLE_ADMIN)
-
软删除:
- 不物理删除笔记,而是标记为已删除
- 便于数据恢复和审计
-
操作审计:
- 记录谁在什么时间修改/删除了笔记
- 使用 Spring Data JPA 的
@CreatedBy和@LastModifiedBy
-
并发控制:
- 使用乐观锁(
@Version注解)防止并发修改冲突
- 使用乐观锁(
通过以上实现,可以确保只有笔记的作者才能修改或删除自己的笔记,同时提供良好的用户体验和安全防护。
2.1 首页笔记探索功能概述
实现一个仿小红书的首页功能,包含笔记流展示、搜索、分类导航、推荐内容等核心功能。
核心功能与设计特点
-
界面布局:
- 顶部固定搜索栏和功能按钮
- 分类导航栏
- 网格布局的笔记卡片
- 底部固定导航栏
-
笔记卡片设计:
- 网格图片布局
- 图片上的标签显示
- 标题、作者信息和互动数据
- 点击跳转详情页
-
交互体验:
- 无限滚动加载更多内容
- 分类切换刷新内容
- 底部导航栏状态切换
- 平滑的页面过渡
-
响应式设计:
- 适配移动设备和桌面设备
- 网格布局自动调整
- 触摸友好的交互元素
2.2 使用Bootstrap、Font Awesome以及Thymeleaf轻松实现首页笔记探索界面设计
主要分为以下几个部分
- 顶部导航栏
- 分类导航
- 笔记卡片网格
- 加载更多内容提示
- 没有更多内容提示
- 底部导航栏
界面整体布局
在src/main/resources/templates目录下,新建一个explore.html,代表首页笔记探索界面。以下是页面整体布局代码:
https://images.jiaoben.nethtml>
https://images.jiaoben.nethtml https://images.jiaoben.netlang=https://images.jiaoben.net"en" https://images.jiaoben.netxmlns:th=https://images.jiaoben.net"http://www.thymeleaf.org" https://images.jiaoben.netxmlns:sec=https://images.jiaoben.net"http://www.thymeleaf.org/extras/spring-security">
https://images.jiaoben.nethead >
https://images.jiaoben.netmeta https://images.jiaoben.netcharset=https://images.jiaoben.net"UTF-8">
https://images.jiaoben.netmeta https://images.jiaoben.netname=https://images.jiaoben.net"viewport" https://images.jiaoben.netcontent=https://images.jiaoben.net"width=device-width, initial-scale=1.0">
https://images.jiaoben.nettitle >RN - 标记我的生活https://images.jiaoben.nettitle>
https://images.jiaoben.net
https://images.jiaoben.netlink https://images.jiaoben.nethref=https://images.jiaoben.net"https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css"
https://images.jiaoben.netth:href=https://images.jiaoben.net"@{/css/bootstrap.min.css}" https://images.jiaoben.netrel=https://images.jiaoben.net"stylesheet">
https://images.jiaoben.net
https://images.jiaoben.netlink https://images.jiaoben.nethref=https://images.jiaoben.net"https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css"
https://images.jiaoben.netth:href=https://images.jiaoben.net"@{/css/font-awesome.min.css}" https://images.jiaoben.netrel=https://images.jiaoben.net"stylesheet">
https://images.jiaoben.net
https://images.jiaoben.netstyle >
https://images.jiaoben.netstyle>
https://images.jiaoben.nethead>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netbody >
https://images.jiaoben.netmain >
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netdiv>
https://images.jiaoben.netmain>
https://images.jiaoben.netfooter >
https://images.jiaoben.net
https://images.jiaoben.netfooter>
https://images.jiaoben.net
https://images.jiaoben.netscript https://images.jiaoben.netsrc=https://images.jiaoben.net"https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"
https://images.jiaoben.netth:src=https://images.jiaoben.net"@{/js/bootstrap.bundle.min.js}">https://images.jiaoben.netscript>
https://images.jiaoben.netscript >https://images.jiaoben.net
https://images.jiaoben.net// TODO 程序运行脚本
https://images.jiaoben.netscript>
https://images.jiaoben.netbody>
https://images.jiaoben.nethtml>
顶部导航栏
https://images.jiaoben.netstyle >https://images.jiaoben.net
https://images.jiaoben.net/* 全局样式 */
https://images.jiaoben.netbody {
https://images.jiaoben.netfont-family: -apple-system, BlinkMacSystemFont, https://images.jiaoben.net"Segoe UI", Roboto, https://images.jiaoben.net"Helvetica Neue", Arial, sans-serif;
https://images.jiaoben.netbackground-color: https://images.jiaoben.net#f5f5f5;
}
https://images.jiaoben.netstyle>
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netheader >
https://images.jiaoben.netnav https://images.jiaoben.netclass=https://images.jiaoben.net"navbar navbar-expand-lg">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.neta https://images.jiaoben.netclass=https://images.jiaoben.net"navbar-brand" https://images.jiaoben.nethref=https://images.jiaoben.net"/" https://images.jiaoben.netth:href=https://images.jiaoben.net"@{/}">
https://images.jiaoben.netimg https://images.jiaoben.netsrc=https://images.jiaoben.net"../static/images/rn_logo.png" https://images.jiaoben.netth:src=https://images.jiaoben.net"@{/images/rn_logo.png}" https://images.jiaoben.netalt=https://images.jiaoben.net"RN" https://images.jiaoben.netheight=https://images.jiaoben.net"24">
https://images.jiaoben.neta>
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"col-md-3">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"input-group">
https://images.jiaoben.netinput https://images.jiaoben.netclass=https://images.jiaoben.net"form-control" https://images.jiaoben.nettype=https://images.jiaoben.net"text" https://images.jiaoben.netplaceholder=https://images.jiaoben.net"搜索感兴趣的内容" https://images.jiaoben.netaria-label=https://images.jiaoben.net"Search">
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.netbutton https://images.jiaoben.netclass=https://images.jiaoben.net"navbar-toggler" https://images.jiaoben.nettype=https://images.jiaoben.net"button" https://images.jiaoben.netdata-bs-toggle=https://images.jiaoben.net"collapse" https://images.jiaoben.netdata-bs-target=https://images.jiaoben.net"#navbarNav"
https://images.jiaoben.netaria-controls=https://images.jiaoben.net"navbarNav" https://images.jiaoben.netaria-expanded=https://images.jiaoben.net"false" https://images.jiaoben.netaria-label=https://images.jiaoben.net"Toggle navigation">
https://images.jiaoben.netspan https://images.jiaoben.netclass=https://images.jiaoben.net"navbar-toggler-icon">https://images.jiaoben.netspan>
https://images.jiaoben.netbutton>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"collapse navbar-collapse" https://images.jiaoben.netid=https://images.jiaoben.net"navbarNav">
https://images.jiaoben.netul https://images.jiaoben.netclass=https://images.jiaoben.net"navbar-nav me-auto">
https://images.jiaoben.netul>
https://images.jiaoben.netul https://images.jiaoben.netclass=https://images.jiaoben.net"navbar-nav mb-2 mb-lg-0">
https://images.jiaoben.netli https://images.jiaoben.netclass=https://images.jiaoben.net"nav-item dropdown">
https://images.jiaoben.neta https://images.jiaoben.netclass=https://images.jiaoben.net"nav-link dropdown-toggle" https://images.jiaoben.nethref=https://images.jiaoben.net"#" https://images.jiaoben.netdata-bs-target=https://images.jiaoben.net"dropdown" https://images.jiaoben.netdata-bs-toggle=https://images.jiaoben.net"dropdown"
https://images.jiaoben.netaria-expanded=https://images.jiaoben.net"false">
[[${#authentication.name}]]
https://images.jiaoben.neta>
https://images.jiaoben.netul https://images.jiaoben.netclass=https://images.jiaoben.net"dropdown-menu" https://images.jiaoben.netid=https://images.jiaoben.net"dropdown">
https://images.jiaoben.netli https://images.jiaoben.netclass=https://images.jiaoben.net"dropdown-item">
https://images.jiaoben.neta https://images.jiaoben.netclass=https://images.jiaoben.net"nav-link" https://images.jiaoben.nethref=https://images.jiaoben.net"/user/profile" https://images.jiaoben.netth:href=https://images.jiaoben.net"@{/user/profile}">个人资料https://images.jiaoben.neta>
https://images.jiaoben.netli>
https://images.jiaoben.netli https://images.jiaoben.netclass=https://images.jiaoben.net"dropdown-item">
https://images.jiaoben.netform https://images.jiaoben.netth:action=https://images.jiaoben.net"@{/logout}" https://images.jiaoben.netaction=https://images.jiaoben.net"/logout" https://images.jiaoben.netmethod=https://images.jiaoben.net"post">
https://images.jiaoben.netbutton https://images.jiaoben.nettype=https://images.jiaoben.net"submit" https://images.jiaoben.netclass=https://images.jiaoben.net"nav-link">退出登录https://images.jiaoben.netbutton>
https://images.jiaoben.netform>
https://images.jiaoben.netli>
https://images.jiaoben.netul>
https://images.jiaoben.netli>
https://images.jiaoben.netul>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.netnav>
https://images.jiaoben.netheader>
https://images.jiaoben.net
分类导航
https://images.jiaoben.net
https://images.jiaoben.netstyle >https://images.jiaoben.net
https://images.jiaoben.net/* ...为节约篇幅,此处省略非核心内容 */
https://images.jiaoben.net/* 分类导航 */
https://images.jiaoben.net.category-nav {
https://images.jiaoben.netbackground-color: white;
https://images.jiaoben.netpadding: https://images.jiaoben.net8px https://images.jiaoben.net0;
https://images.jiaoben.netoverflow-x: auto;
https://images.jiaoben.netwhite-space: nowrap;
-webkit-https://images.jiaoben.netoverflow-scrolling: touch;
}
https://images.jiaoben.net.category-item {
https://images.jiaoben.netdisplay: inline-block;
https://images.jiaoben.netpadding: https://images.jiaoben.net6px https://images.jiaoben.net12px;
https://images.jiaoben.netmargin-right: https://images.jiaoben.net8px;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net20px;
https://images.jiaoben.netfont-size: https://images.jiaoben.net14px;
https://images.jiaoben.netcursor: pointer;
https://images.jiaoben.nettransition: background-color https://images.jiaoben.net0.2s;
}
https://images.jiaoben.net.category-itemhttps://images.jiaoben.net.active {
https://images.jiaoben.netbackground-color: https://images.jiaoben.net#ff2442;
https://images.jiaoben.netcolor: white;
}
https://images.jiaoben.netstyle>
https://images.jiaoben.nethead>
https://images.jiaoben.net
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.net
https://images.jiaoben.netheader >
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item active">推荐https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">穿搭https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">美食https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">彩妆https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">影视https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">职场https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">情感https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">家居https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">游戏https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">旅行https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"category-item">健身https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.netheader>
https://images.jiaoben.net
笔记卡片网格
https://images.jiaoben.net
https://images.jiaoben.netstyle >https://images.jiaoben.net
https://images.jiaoben.net/* ...为节约篇幅,此处省略非核心内容 */
https://images.jiaoben.net/* 笔记卡片网格 */
https://images.jiaoben.net.notes-grid {
https://images.jiaoben.netdisplay: grid;
https://images.jiaoben.netgrid-template-columns: https://images.jiaoben.netrepeat(auto-fill, https://images.jiaoben.netminmax(https://images.jiaoben.net180px, https://images.jiaoben.net1fr));
https://images.jiaoben.netgap: https://images.jiaoben.net8px;
https://images.jiaoben.netpadding: https://images.jiaoben.net8px;
}
https://images.jiaoben.net.note-card {
https://images.jiaoben.netbackground-color: white;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net8px;
https://images.jiaoben.netoverflow: hidden;
https://images.jiaoben.netbox-shadow: https://images.jiaoben.net0 https://images.jiaoben.net1px https://images.jiaoben.net2px https://images.jiaoben.netrgba(https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0.05);
}
https://images.jiaoben.net.note-image-container {
https://images.jiaoben.netposition: relative;
https://images.jiaoben.netpadding-bottom: https://images.jiaoben.net100%;
https://images.jiaoben.net/* 保持正方形比例 */
https://images.jiaoben.netoverflow: hidden;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net12px;
}
https://images.jiaoben.net.note-image {
https://images.jiaoben.netposition: absolute;
https://images.jiaoben.nettop: https://images.jiaoben.net0;
https://images.jiaoben.netleft: https://images.jiaoben.net0;
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
https://images.jiaoben.netheight: https://images.jiaoben.net100%;
https://images.jiaoben.netobject-fit: cover;
}
https://images.jiaoben.net.note-tag {
https://images.jiaoben.netposition: absolute;
https://images.jiaoben.netbottom: https://images.jiaoben.net8px;
https://images.jiaoben.netleft: https://images.jiaoben.net8px;
https://images.jiaoben.netbackground-color: https://images.jiaoben.netrgba(https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0.5);
https://images.jiaoben.netcolor: white;
https://images.jiaoben.netpadding: https://images.jiaoben.net2px https://images.jiaoben.net8px;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net10px;
https://images.jiaoben.netfont-size: https://images.jiaoben.net12px;
}
https://images.jiaoben.net.note-content {
https://images.jiaoben.netpadding: https://images.jiaoben.net8px;
}
https://images.jiaoben.net.note-title {
https://images.jiaoben.netfont-size: https://images.jiaoben.net14px;
https://images.jiaoben.netfont-weight: https://images.jiaoben.net500;
https://images.jiaoben.netmargin-bottom: https://images.jiaoben.net4px;
https://images.jiaoben.netline-height: https://images.jiaoben.net1.4;
https://images.jiaoben.netoverflow: hidden;
https://images.jiaoben.nettext-overflow: ellipsis;
https://images.jiaoben.netdisplay: -webkit-box;
-webkit-line-clamp: https://images.jiaoben.net2;
-webkit-box-orient: vertical;
}
https://images.jiaoben.net.note-author {
https://images.jiaoben.netdisplay: flex;
https://images.jiaoben.netalign-items: center;
https://images.jiaoben.netmargin-bottom: https://images.jiaoben.net4px;
}
https://images.jiaoben.net.author-avatar {
https://images.jiaoben.netwidth: https://images.jiaoben.net20px;
https://images.jiaoben.netheight: https://images.jiaoben.net20px;
https://images.jiaoben.netborder-radius: https://images.jiaoben.net50%;
https://images.jiaoben.netmargin-right: https://images.jiaoben.net6px;
}
https://images.jiaoben.net.author-name {
https://images.jiaoben.netfont-size: https://images.jiaoben.net12px;
https://images.jiaoben.netcolor: https://images.jiaoben.net#666;
}
https://images.jiaoben.net.note-author-stats {
https://images.jiaoben.netdisplay: flex;
https://images.jiaoben.netjustify-content: space-between;
}
https://images.jiaoben.net.note-stats {
https://images.jiaoben.netdisplay: flex;
https://images.jiaoben.netalign-items: center;
https://images.jiaoben.netfont-size: https://images.jiaoben.net12px;
https://images.jiaoben.netcolor: https://images.jiaoben.net#999;
}
https://images.jiaoben.net.stat-item {
https://images.jiaoben.netmargin-right: https://images.jiaoben.net12px;
}
https://images.jiaoben.netstyle>
https://images.jiaoben.net
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netbody >
https://images.jiaoben.netmain >
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"notes-grid" https://images.jiaoben.netid=https://images.jiaoben.net"notesGrid">
https://images.jiaoben.net
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netdiv>
https://images.jiaoben.netmain>
https://images.jiaoben.net
https://images.jiaoben.netbody>
https://images.jiaoben.nethtml>
加载更多内容提示
https://images.jiaoben.net
https://images.jiaoben.netstyle >https://images.jiaoben.net
https://images.jiaoben.net/* ...为节约篇幅,此处省略非核心内容 */
https://images.jiaoben.net/* 加载更多 */
https://images.jiaoben.net.load-more {
https://images.jiaoben.nettext-align: center;
https://images.jiaoben.netpadding: https://images.jiaoben.net16px https://images.jiaoben.net0;
https://images.jiaoben.netcolor: https://images.jiaoben.net#666;
https://images.jiaoben.netfont-size: https://images.jiaoben.net14px;
}
https://images.jiaoben.netstyle>
https://images.jiaoben.net
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netbody >
https://images.jiaoben.netmain >
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"load-more" https://images.jiaoben.netid=https://images.jiaoben.net"loadMore">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-spinner fa-spin">https://images.jiaoben.neti> 加载更多
https://images.jiaoben.netdiv>
https://images.jiaoben.net
https://images.jiaoben.netdiv>
https://images.jiaoben.netmain>
https://images.jiaoben.net
https://images.jiaoben.netbody>
https://images.jiaoben.nethtml>
没有更多内容提示
https://images.jiaoben.net
https://images.jiaoben.netstyle >https://images.jiaoben.net
https://images.jiaoben.net/* ...为节约篇幅,此处省略非核心内容 */
https://images.jiaoben.net/* 没有更多 */
https://images.jiaoben.net.no-more {
https://images.jiaoben.nettext-align: center;
https://images.jiaoben.netpadding: https://images.jiaoben.net0 https://images.jiaoben.net0 https://images.jiaoben.net50px https://images.jiaoben.net0;
https://images.jiaoben.netcolor: https://images.jiaoben.net#666;
https://images.jiaoben.netfont-size: https://images.jiaoben.net14px;
https://images.jiaoben.netdisplay: none;
}
https://images.jiaoben.netstyle>
https://images.jiaoben.nethead>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netbody >
https://images.jiaoben.netmain >
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"no-more" https://images.jiaoben.netid=https://images.jiaoben.net"noMoreContent">
https://images.jiaoben.netp >已经到底啦~https://images.jiaoben.netp>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.netmain>
https://images.jiaoben.netfooter >
https://images.jiaoben.net
https://images.jiaoben.netfooter>
https://images.jiaoben.net
https://images.jiaoben.netbody>
https://images.jiaoben.nethtml>
https://images.jiaoben.netbody>
https://images.jiaoben.nethtml>
底部导航栏
https://images.jiaoben.net
https://images.jiaoben.netstyle >https://images.jiaoben.net
https://images.jiaoben.net/* ...为节约篇幅,此处省略非核心内容 */
https://images.jiaoben.net/* 底部导航栏 */
https://images.jiaoben.net.bottom-nav {
https://images.jiaoben.netposition: fixed;
https://images.jiaoben.netbottom: https://images.jiaoben.net0;
https://images.jiaoben.netleft: https://images.jiaoben.net0;
https://images.jiaoben.netright: https://images.jiaoben.net0;
https://images.jiaoben.netdisplay: flex;
https://images.jiaoben.netjustify-content: space-around;
https://images.jiaoben.netpadding: https://images.jiaoben.net8px https://images.jiaoben.net0;
https://images.jiaoben.netbox-shadow: https://images.jiaoben.net0 -https://images.jiaoben.net1px https://images.jiaoben.net2px https://images.jiaoben.netrgba(https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0, https://images.jiaoben.net0.05);
https://images.jiaoben.netz-index: https://images.jiaoben.net100;
https://images.jiaoben.netbackground-color: https://images.jiaoben.net#f5f5f5;
}
https://images.jiaoben.net.nav-item {
https://images.jiaoben.netdisplay: flex;
https://images.jiaoben.netflex-direction: column;
https://images.jiaoben.netalign-items: center;
https://images.jiaoben.netcolor: https://images.jiaoben.net#666;
https://images.jiaoben.netcursor: pointer;
}
https://images.jiaoben.net.nav-itemhttps://images.jiaoben.net.active {
https://images.jiaoben.netcolor: https://images.jiaoben.net#ff2442;
}
https://images.jiaoben.net.nav-icon {
https://images.jiaoben.netfont-size: https://images.jiaoben.net20px;
https://images.jiaoben.netmargin-bottom: https://images.jiaoben.net2px;
}
https://images.jiaoben.net.nav-text {
https://images.jiaoben.netfont-size: https://images.jiaoben.net10px;
}
https://images.jiaoben.netstyle>
https://images.jiaoben.nethead>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netheader >
https://images.jiaoben.net
https://images.jiaoben.netheader>
https://images.jiaoben.netbody >
https://images.jiaoben.netmain >
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container">
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netdiv>
https://images.jiaoben.netmain>
https://images.jiaoben.netfooter >
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"container bottom-nav">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"nav-item active">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-home nav-icon">https://images.jiaoben.neti>
https://images.jiaoben.netspan https://images.jiaoben.netclass=https://images.jiaoben.net"nav-text">首页https://images.jiaoben.netspan>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"nav-item">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-compass nav-icon">https://images.jiaoben.neti>
https://images.jiaoben.netspan https://images.jiaoben.netclass=https://images.jiaoben.net"nav-text">发现https://images.jiaoben.netspan>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"nav-item">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-plus nav-icon">https://images.jiaoben.neti>
https://images.jiaoben.netspan https://images.jiaoben.netclass=https://images.jiaoben.net"nav-text">发布https://images.jiaoben.netspan>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"nav-item">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-comment-o nav-icon">https://images.jiaoben.neti>
https://images.jiaoben.netspan https://images.jiaoben.netclass=https://images.jiaoben.net"nav-text">消息https://images.jiaoben.netspan>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"nav-item">
https://images.jiaoben.neti https://images.jiaoben.netclass=https://images.jiaoben.net"fa fa-user-o nav-icon">https://images.jiaoben.neti>
https://images.jiaoben.netspan https://images.jiaoben.netclass=https://images.jiaoben.net"nav-text">我的https://images.jiaoben.netspan>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
https://images.jiaoben.netfooter>
https://images.jiaoben.net
https://images.jiaoben.netscript >https://images.jiaoben.net
https://images.jiaoben.net// TODO 程序运行脚本
https://images.jiaoben.netscript>
https://images.jiaoben.netbody>
https://images.jiaoben.nethtml>
2.3 掌握无限滚动刷新加载笔记内容生成笔记卡片网格的秘笈
修改explore.html,在增加如下内容:
2.4 创建一个Spring MVC控制器类处理首页笔记探索请求
新建一个控制器ExploreController,用于处理首页笔记探索的请求。
返回首页笔记探索页面
新增方法如下。
https://images.jiaoben.netpackage com.waylau.rednote.controller;
https://images.jiaoben.netimport org.springframework.stereotype.Controller;
https://images.jiaoben.netimport org.springframework.web.bind.annotation.GetMapping;
https://images.jiaoben.netimport org.springframework.web.bind.annotation.RequestMapping;
https://images.jiaoben.net/**
* ExploreController 首页笔记探索
*
* https://images.jiaoben.net@author Way Lau
* https://images.jiaoben.net@version 2025/08/20
**/
https://images.jiaoben.net@Controller
https://images.jiaoben.net@RequestMapping("/explore")
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netExploreController {
https://images.jiaoben.net/**
* 显示笔记探索页面
*/
https://images.jiaoben.net@GetMapping
https://images.jiaoben.netpublic String https://images.jiaoben.netshowExplorehttps://images.jiaoben.net() {
https://images.jiaoben.netreturn https://images.jiaoben.net"explore";
}
}
返回首页笔记探索页面的笔记数据
新增方法如下。
https://images.jiaoben.netprivate https://images.jiaoben.netstatic https://images.jiaoben.netfinal https://images.jiaoben.netint https://images.jiaoben.netPAGE_SIZE https://images.jiaoben.net= https://images.jiaoben.net20;
https://images.jiaoben.netprivate https://images.jiaoben.netstatic https://images.jiaoben.netfinal https://images.jiaoben.netString https://images.jiaoben.netDEFAULT_CATEGORY https://images.jiaoben.net= https://images.jiaoben.net"推荐";
https://images.jiaoben.net@Autowired
https://images.jiaoben.netprivate NoteService noteService;
https://images.jiaoben.net/**
* 返回首页笔记探索页面的笔记数据
*/
https://images.jiaoben.net@GetMapping("/note")
https://images.jiaoben.netpublic ResponseEntity https://images.jiaoben.netgetNotesByCategoryhttps://images.jiaoben.net(
https://images.jiaoben.net@RequestParam(defaultValue = "1") https://images.jiaoben.netint page,
https://images.jiaoben.net@RequestParam(required = false) String category) {
https://images.jiaoben.net// 把“推荐”当成空
https://images.jiaoben.netif (DEFAULT_CATEGORY.equals(category)) {
category = https://images.jiaoben.netnull;
}
Page notes = noteService.getNotesByPage(page, PAGE_SIZE, category);
https://images.jiaoben.netNoteResponseDto https://images.jiaoben.netnotesResponseDto https://images.jiaoben.net= https://images.jiaoben.netnew https://images.jiaoben.netNoteResponseDto();
notesResponseDto.setHasMore(notes.hasNext());
notesResponseDto.setNotes(notes.getContent());
https://images.jiaoben.netreturn ResponseEntity.ok(notesResponseDto);
}
上述接口,可以根据分类进行分页查询,并将查询结果通过NoteResponseDto数据结构返回给前端。
如果分类是“推荐”,实际上就是不需要分类,直接赋值为null即可。
探索笔记的响应对象DTO
新增NoteResponseDto如下。
https://images.jiaoben.netpackage com.waylau.rednote.dto;
https://images.jiaoben.netimport com.waylau.rednote.entity.Note;
https://images.jiaoben.netimport lombok.Getter;
https://images.jiaoben.netimport lombok.Setter;
https://images.jiaoben.netimport java.util.List;
https://images.jiaoben.net/**
* NoteResponseDto 探索笔记的响应对象
*
* https://images.jiaoben.net@author Way Lau
* https://images.jiaoben.net@version 2025/08/20
**/
https://images.jiaoben.net@Getter
https://images.jiaoben.net@Setter
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netNoteResponseDto {
https://images.jiaoben.net/**
* 笔记列表
*/
https://images.jiaoben.netprivate List notes;
https://images.jiaoben.net/**
* 是否还有更多
*/
https://images.jiaoben.netprivate https://images.jiaoben.netboolean hasMore;
}
首页重定向
首页重定向到首页笔记探索页面:
https://images.jiaoben.netpackage com.waylau.rednote.controller;
https://images.jiaoben.netimport org.springframework.stereotype.Controller;
https://images.jiaoben.netimport org.springframework.web.bind.annotation.GetMapping;
https://images.jiaoben.netimport org.springframework.web.bind.annotation.RequestMapping;
https://images.jiaoben.net/**
* IndexController 首页控制器
*
* https://images.jiaoben.net@author Way Lau
* https://images.jiaoben.net@version 2025/08/20
**/
https://images.jiaoben.net@Controller
https://images.jiaoben.net@RequestMapping("/")
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netIndexController {
https://images.jiaoben.net@GetMapping
https://images.jiaoben.netpublic String https://images.jiaoben.netindexhttps://images.jiaoben.net() {
https://images.jiaoben.net// 重定向到首页笔记探索页面
https://images.jiaoben.netreturn https://images.jiaoben.net"redirect:/explore";
}
}
2.5 调整安全配置类细化首页笔记探索的访问权限
- 在 Spring Security 配置类中,进一步细化首页笔记索页面的访问权限
- 确保只有普通用户角色可以访问首页笔记索页面
修改WebSecurityConfig如下:
https://images.jiaoben.net@Bean
https://images.jiaoben.netpublic SecurityFilterChain https://images.jiaoben.netfilterChainhttps://images.jiaoben.net(HttpSecurity http) https://images.jiaoben.netthrows Exception {
http
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
.authorizeHttpRequests(authorize -> authorize
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
https://images.jiaoben.net// 允许USER角色的用户访问 /explore/** 的资源
.requestMatchers(https://images.jiaoben.net"/explore/**").hasRole(https://images.jiaoben.net"USER")
https://images.jiaoben.net// 其他请求需要认证
.anyRequest().authenticated()
)
;
https://images.jiaoben.netreturn http.build();
}
2.6 提供分类分页查询笔记的服务
修改NoteRepository
修改NoteRepository,增加如下接口:
https://images.jiaoben.net/**
* 根据分类、分页查询笔记
*
* https://images.jiaoben.net@param category
* https://images.jiaoben.net@param pageable
* https://images.jiaoben.net@return
*/
Page https://images.jiaoben.netfindByCategoryhttps://images.jiaoben.net(String category, Pageable pageable);
https://images.jiaoben.net/**
* 分页查询笔记
*
* https://images.jiaoben.net@param pageable
* https://images.jiaoben.net@return
*/
Page https://images.jiaoben.netfindAllhttps://images.jiaoben.net(Pageable pageable);
上述两个接口的区别是,如果不提供分类,实际上就是全查。
修改NoteService
修改NoteService,增加如下接口:
https://images.jiaoben.net/**
* 分类分页查询笔记
*
* https://images.jiaoben.net@param page
* https://images.jiaoben.net@param pageSize
* https://images.jiaoben.net@param category
* https://images.jiaoben.net@return
*/
Page https://images.jiaoben.netgetNotesByPagehttps://images.jiaoben.net(https://images.jiaoben.netint page, https://images.jiaoben.netint pageSize, String category);
修改NoteServiceImpl
修改NoteServiceImpl,实现分类分页查询笔记的方法:
https://images.jiaoben.net@Override
https://images.jiaoben.netpublic Page https://images.jiaoben.netgetNotesByPagehttps://images.jiaoben.net(https://images.jiaoben.netint page, https://images.jiaoben.netint pageSize, String category) {
https://images.jiaoben.net// 构造Pageable对象,按照创建时间倒序排序
https://images.jiaoben.netPageable https://images.jiaoben.netpageable https://images.jiaoben.net= PageRequest.of(page - https://images.jiaoben.net1, pageSize, Sort.by(https://images.jiaoben.net"createAt").descending());
https://images.jiaoben.netif (category != https://images.jiaoben.netnull && !category.isEmpty()) {
https://images.jiaoben.netreturn noteRepository.findByCategory(category, pageable);
}
https://images.jiaoben.netreturn noteRepository.findAll(pageable);
}
2.7 处理Hibernate懒加载与Jackson序列化冲突的问题
Hibernate 懒加载与 Jackson 序列化冲突的问题
当前端使用JavaScript fetch API试图访问返回首页笔记探索页面的笔记数据时,会报以下错误:
https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.exchttps://images.jiaoben.net.InvalidDefinitionException: https://images.jiaoben.netNo https://images.jiaoben.netserializer https://images.jiaoben.netfound https://images.jiaoben.netfor https://images.jiaoben.netclass https://images.jiaoben.netorghttps://images.jiaoben.net.hibernatehttps://images.jiaoben.net.proxyhttps://images.jiaoben.net.pojohttps://images.jiaoben.net.bytebuddyhttps://images.jiaoben.net.ByteBuddyInterceptor https://images.jiaoben.netand https://images.jiaoben.netno https://images.jiaoben.netproperties https://images.jiaoben.netdiscovered https://images.jiaoben.netto https://images.jiaoben.netcreate https://images.jiaoben.netBeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference https://images.jiaoben.netchain: com.waylau.rednote.dto.NotesResponseDto[https://images.jiaoben.net"notes"]->java.util.Collections$UnmodifiableRandomAccessList[https://images.jiaoben.net0]->com.waylau.rednote.entity.Note[https://images.jiaoben.net"author"]->com.waylau.rednote.entity.User$HibernateProxy[https://images.jiaoben.net"hibernateLazyInitializer"])
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.exchttps://images.jiaoben.net.InvalidDefinitionExceptionhttps://images.jiaoben.net.from(InvalidDefinitionException.https://images.jiaoben.netjava:https://images.jiaoben.net77) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.SerializerProviderhttps://images.jiaoben.net.reportBadDefinition(SerializerProvider.https://images.jiaoben.netjava:https://images.jiaoben.net1359) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.DatabindContexthttps://images.jiaoben.net.reportBadDefinition(DatabindContext.https://images.jiaoben.netjava:https://images.jiaoben.net415) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.implhttps://images.jiaoben.net.UnknownSerializerhttps://images.jiaoben.net.failForEmpty(UnknownSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net52) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.implhttps://images.jiaoben.net.UnknownSerializerhttps://images.jiaoben.net.serialize(UnknownSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net29) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.BeanPropertyWriterhttps://images.jiaoben.net.serializeAsField(BeanPropertyWriter.https://images.jiaoben.netjava:https://images.jiaoben.net732) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.stdhttps://images.jiaoben.net.BeanSerializerBasehttps://images.jiaoben.net.serializeFields(BeanSerializerBase.https://images.jiaoben.netjava:https://images.jiaoben.net760) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.BeanSerializerhttps://images.jiaoben.net.serialize(BeanSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net183) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.BeanPropertyWriterhttps://images.jiaoben.net.serializeAsField(BeanPropertyWriter.https://images.jiaoben.netjava:https://images.jiaoben.net732) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.stdhttps://images.jiaoben.net.BeanSerializerBasehttps://images.jiaoben.net.serializeFields(BeanSerializerBase.https://images.jiaoben.netjava:https://images.jiaoben.net760) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.BeanSerializerhttps://images.jiaoben.net.serialize(BeanSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net183) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.implhttps://images.jiaoben.net.IndexedListSerializerhttps://images.jiaoben.net.serializeContents(IndexedListSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net119) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.implhttps://images.jiaoben.net.IndexedListSerializerhttps://images.jiaoben.net.serialize(IndexedListSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net79) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.implhttps://images.jiaoben.net.IndexedListSerializerhttps://images.jiaoben.net.serialize(IndexedListSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net18) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.BeanPropertyWriterhttps://images.jiaoben.net.serializeAsField(BeanPropertyWriter.https://images.jiaoben.netjava:https://images.jiaoben.net732) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.stdhttps://images.jiaoben.net.BeanSerializerBasehttps://images.jiaoben.net.serializeFields(BeanSerializerBase.https://images.jiaoben.netjava:https://images.jiaoben.net760) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.BeanSerializerhttps://images.jiaoben.net.serialize(BeanSerializer.https://images.jiaoben.netjava:https://images.jiaoben.net183) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.DefaultSerializerProviderhttps://images.jiaoben.net._serialize(DefaultSerializerProvider.https://images.jiaoben.netjava:https://images.jiaoben.net503) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.serhttps://images.jiaoben.net.DefaultSerializerProviderhttps://images.jiaoben.net.serializeValue(DefaultSerializerProvider.https://images.jiaoben.netjava:https://images.jiaoben.net342) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.ObjectWriter$https://images.jiaoben.netPrefetchhttps://images.jiaoben.net.serialize(ObjectWriter.https://images.jiaoben.netjava:https://images.jiaoben.net1587) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netcomhttps://images.jiaoben.net.fasterxmlhttps://images.jiaoben.net.jacksonhttps://images.jiaoben.net.databindhttps://images.jiaoben.net.ObjectWriterhttps://images.jiaoben.net.writeValue(ObjectWriter.https://images.jiaoben.netjava:https://images.jiaoben.net1061) ~https://images.jiaoben.net[jackson-databind-2.19.0.jar:2.19.0]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.httphttps://images.jiaoben.net.converterhttps://images.jiaoben.net.jsonhttps://images.jiaoben.net.AbstractJackson2HttpMessageConverterhttps://images.jiaoben.net.writeInternal(AbstractJackson2HttpMessageConverter.https://images.jiaoben.netjava:https://images.jiaoben.net485) ~https://images.jiaoben.net[spring-web-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.httphttps://images.jiaoben.net.converterhttps://images.jiaoben.net.AbstractGenericHttpMessageConverterhttps://images.jiaoben.net.write(AbstractGenericHttpMessageConverter.https://images.jiaoben.netjava:https://images.jiaoben.net126) ~https://images.jiaoben.net[spring-web-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.webhttps://images.jiaoben.net.servlethttps://images.jiaoben.net.mvchttps://images.jiaoben.net.methodhttps://images.jiaoben.net.annotationhttps://images.jiaoben.net.AbstractMessageConverterMethodProcessorhttps://images.jiaoben.net.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.https://images.jiaoben.netjava:https://images.jiaoben.net345) ~https://images.jiaoben.net[spring-webmvc-6.2.7.jar:6.2.7]
https://images.jiaoben.netat https://images.jiaoben.netorghttps://images.jiaoben.net.springframeworkhttps://images.jiaoben.net.webhttps://images.jiaoben.net.servlethttps://images.jiaoben.net.mvchttps://images.jiaoben.net.methodhttps://images.jiaoben.net.annotationhttps://images.jiaoben.net.HttpEntityMethodProcessorhttps://images.jiaoben.net.handleReturnValue(HttpEntityMethodProcessor.https://images.jiaoben.netjava:https://images.jiaoben.net263) ~https://images.jiaoben.net[spring-webmvc-6.2.7.jar:6.2.7]
接口返回的是ResponseEntity.ok(notesResponseDto)。ResponseEntity.ok() 是 Spring 框架中用于构建 HTTP 响应的一个便捷方法。它属于 org.springframework.http.ResponseEntity 类,主要用于封装 HTTP 响应的状态码、头部信息和响应体,提供更灵活的 API 响应控制。ResponseEntity 的内容会自动序列化为 JSON/XML 等格式。从上述报错信息可以知道,默认的自动序列化工具为Jackson。
错误原因分析
这个错误是典型的Hibernate懒加载与Jackson序列化冲突的问题。具体来说:
- 错误根源:当Jackson尝试序列化返回的Note数据时,遇到了Hibernate生成的代理对象(
User$HibernateProxy) - 问题路径: NotesResponseDto -> notes列表 -> Note实体 -> author属性 -> User实体的Hibernate代理对象
- 技术细节:
- Hibernate使用代理对象实现懒加载关联实体
- Jackson无法识别Hibernate的代理类(
ByteBuddyInterceptor) - 代理对象中的
hibernateLazyInitializer属性触发了序列化错误
从代码断点调试可以看到auther对象属性是空的,如下图11-1所示。
解决方案
解决方案有几下几种。
- 优先使用DTO模式:通过专门的DTO类定义API响应格式,避免直接序列化实体对象
- 合理设计关联关系:根据业务需求选择合适的加载策略(EAGER/FETCH)
- 使用@JsonView进行精细控制:在复杂场景中使用Jackson的@JsonView实现选择性序列化
- 结合性能考虑:懒加载是提高性能的重要手段,但需要配合合理的初始化策略
在本例中,使用的DTO模式。
1. 创建NoteExploreDto
创建NoteExploreDto,代码如下:
https://images.jiaoben.netpackage com.waylau.rednote.dto;
https://images.jiaoben.netimport com.waylau.rednote.entity.Note;
https://images.jiaoben.netimport lombok.Getter;
https://images.jiaoben.netimport lombok.Setter;
https://images.jiaoben.net/**
* NoteExploreDto 笔记探索DTO
*
* https://images.jiaoben.net@author Way Lau
* https://images.jiaoben.net@version 2025/08/20
**/
https://images.jiaoben.net@Getter
https://images.jiaoben.net@Setter
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netNoteExploreDto {
https://images.jiaoben.netprivate Long noteId;
https://images.jiaoben.netprivate String title;
https://images.jiaoben.net/**
* 封面
*/
https://images.jiaoben.netprivate String cover;
https://images.jiaoben.net/**
* 作者用户名
*/
https://images.jiaoben.netprivate String username;
https://images.jiaoben.net/**
* 作者头像
*/
https://images.jiaoben.netprivate String avatar;
https://images.jiaoben.netpublic https://images.jiaoben.netstatic NoteExploreDto https://images.jiaoben.nettoExploreDtohttps://images.jiaoben.net(Note note) {
https://images.jiaoben.netNoteExploreDto https://images.jiaoben.netnoteExploreDto https://images.jiaoben.net= https://images.jiaoben.netnew https://images.jiaoben.netNoteExploreDto();
noteExploreDto.setNoteId(note.getNoteId());
noteExploreDto.setTitle(note.getTitle());
noteExploreDto.setCover(note.getImages().get(https://images.jiaoben.net0));
noteExploreDto.setUsername(note.getAuthor().getUsername());
noteExploreDto.setAvatar(note.getAuthor().getAvatar());
https://images.jiaoben.netreturn noteExploreDto;
}
}
2. 返回DTO类给前端
ExploreController修改如下:
https://images.jiaoben.net/**
* 返回首页笔记探索页面的笔记数据
*/
https://images.jiaoben.net@GetMapping("/note")
https://images.jiaoben.netpublic ResponseEntity https://images.jiaoben.netgetNotesByCategoryhttps://images.jiaoben.net(
https://images.jiaoben.net@RequestParam(defaultValue = "1") https://images.jiaoben.netint page,
https://images.jiaoben.net@RequestParam(required = false) String category) {
https://images.jiaoben.net// 把“推荐”当成空
https://images.jiaoben.netif (DEFAULT_CATEGORY.equals(category)) {
category = https://images.jiaoben.netnull;
}
Page notes = noteService.getNotesByPage(page, PAGE_SIZE, category);
https://images.jiaoben.netNoteResponseDto https://images.jiaoben.netnotesResponseDto https://images.jiaoben.net= https://images.jiaoben.netnew https://images.jiaoben.netNoteResponseDto();
notesResponseDto.setHasMore(notes.hasNext());
https://images.jiaoben.net//notesResponseDto.setNotes(notes.getContent());
https://images.jiaoben.net// 处理序列化问题
List noteExploreDtoList = https://images.jiaoben.netnew https://images.jiaoben.netArrayList<>();
https://images.jiaoben.netfor (Note note : notes.getContent()) {
noteExploreDtoList.add(NoteExploreDto.toExploreDto(note));
}
notesResponseDto.setNotes(noteExploreDtoList);
https://images.jiaoben.netreturn ResponseEntity.ok(notesResponseDto);
}
NoteResponseDto修改如下:
https://images.jiaoben.netpackage com.waylau.rednote.dto;
https://images.jiaoben.netimport com.waylau.rednote.entity.Note;
https://images.jiaoben.netimport lombok.Getter;
https://images.jiaoben.netimport lombok.Setter;
https://images.jiaoben.netimport java.util.List;
https://images.jiaoben.net/**
* NoteResponseDto 探索笔记的响应对象
*
* https://images.jiaoben.net@author Way Lau
* https://images.jiaoben.net@version 2025/06/13
**/
https://images.jiaoben.net@Getter
https://images.jiaoben.net@Setter
https://images.jiaoben.netpublic https://images.jiaoben.netclass https://images.jiaoben.netNoteResponseDto {
https://images.jiaoben.net/**
* 笔记列表
*/
https://images.jiaoben.net// private List notes;
https://images.jiaoben.netprivate List notes;
https://images.jiaoben.net/**
* 是否还有更多
*/
https://images.jiaoben.netprivate https://images.jiaoben.netboolean hasMore;
}
通过以上方法,你应该能够解决Jackson序列化Hibernate代理对象的问题,确保API响应能够正确返回笔记数据。
从代码断点调试可以看到DTO对象属性都是有值的,如下图11-2所示。
如下图11-3所示的是首次访问首页的效果。
如下图11-4所示的是加载了笔记数据之后的效果。
2.8 掌握笔记无限滚动刷新的技巧
修改explore.html,在增加如下内容:
如下图11-5所示的是无限滚动刷新,查询完笔记数据之后的效果。
2.9 格式化数字展示优化信息传达的效率和用户体验
在数字展示中进行格式化(如将10000显示为1w),本质上是为了优化信息传达的效率和用户体验。这种处理方式并非简单的符号替换,而是基于人类认知规律、场景需求和技术实现的综合考量。以下从多个维度解析其背后的逻辑:
一、认知心理学:简化信息处理负荷
-
短期记忆容量限制
人类短期记忆通常只能处理7±2个组块(George Miller的“神奇数字”理论)。例如:- 原始数字“1568924”包含7个独立数字,需拆解为“156万8924”或“156.89万”,将信息组块从7个减少到3-4个,降低记忆负担。
- 对比实验显示:用户识别“3.2k”的速度比“3200”快23%(来源:尼尔森诺曼集团用户体验研究)。
-
量级感知优先于精确值
在许多场景中,用户更关注数字的“量级”而非“精确值”:- 社交平台的粉丝数(12.5w vs 125432):前者能快速传递“十万级”的量级概念。
- 商品销量(5.8k件 vs 5842件):消费者更关心“是否畅销”,而非具体差额。
二、场景适配:不同场景的显示策略差异
| 场景 | 格式化需求 | 示例 | 核心目的 |
|---|---|---|---|
| 社交媒体动态 | 轻量化展示,快速抓取注意力 | 点攒数:2.4w | 减少视觉干扰,突出互动热度 |
| 金融数据大屏 | 兼顾量级与精度,可能需要动态切换单位 | 市值:1.28万亿(自动切换万亿/亿/万) | 适应不同数据规模的展示 |
| 电商商品列表 | 简洁化展示,避免价格信息碎片化 | 价格:¥1.5k | 促进购买决策效率 |
| 科学论文图表 | 严格保留精度,使用标准单位(如10³) | 数据点:1.02×10⁴ | 保证学术严谨性 |
三、视觉设计:优化界面信息层级
-
减少数字长度,提升排版美观
- 原始数字:“阅读量1289456”在移动端可能占用2-3行,格式化后“128.9w”仅占1行,节省空间。
- 案例:小红书笔记列表中,将“收藏数9876”显示为“9.9k”,使卡片布局更紧凑(见下图逻辑示意): ┌──────────────┐ ┌──────────────┐ │ 标题 │ │ 标题 │ │ 正文摘要 │ │ 正文摘要 │ │ 赞9876 评123 │ → │ 赞9.9k 评123 │ └──────────────┘ └──────────────┘
-
引导视觉焦点
格式化后的数字通过“单位缩写”(如w/k/m)形成视觉区分,使用户更易捕捉关键数据:- 未格式化:“粉丝156234,获赞897654”
- 格式化:“粉丝15.6w,获赞89.8w”
后者通过“w”符号强化量级认知,减少用户对具体数字的关注度。
四、技术实现:平衡精度与可读性
-
动态单位切换策略
- 数字 < 1000:显示原值(如568)
- 1000 ≤ 数字 < 10000:显示为“X.Xk”(如3.2k)
- 10000 ≤ 数字 < 10^8:显示为“X.Xw”(如12.5w)
- 10^8 ≤ 数字:显示为“X.X亿”(如1.2亿)
(注:不同平台可能有细微差异,如抖音、小红书使用“万”“亿”,而GitHub用“k”“M”)
-
精度控制算法
- 四舍五入:12543 → 1.3w(保留1位小数)
- 截断处理:12543 → 1.2w(适用于需要快速显示的场景)
- 智能显示:根据数字大小自动调整小数位数:
- 1024 → 1k(整数)
- 1250 → 1.25k(两位小数)
- 12345 → 1.23w(两位小数)
-
国际化适配
- 中文环境:10000 → 1万,100000000 → 1亿
- 英文环境:1000 → 1k,1000000 → 1M(Million),1000000000 → 1B(Billion)
- 日语环境:10000 → 1万,100000000 → 1億
五、反例:何时不应格式化?
-
需要精确值的场景
- 财务报表(金额必须精确到分:¥12345.67)
- 科学实验数据(如温度:25.3℃)
- 身分证号、订单编号等标识性数字
-
小数字场景
- 数字 < 100:显示原值更直观(如“评论5条”比“0.005w条”更易读)
- 例外:某些平台为统一风格,可能仍显示为“0.5k”(如500)
-
专业领域术语冲突
- 计算机领域中“1k”通常指1024(2^10),而日常场景中“1k”=1000,需避免歧义。
六、项目里面的应用
定义数字格式化函数:
https://images.jiaoben.net// 数字格式化,自动转换为k、w单位
https://images.jiaoben.netfunction https://images.jiaoben.netnumberFormat(https://images.jiaoben.netnum) {
https://images.jiaoben.netif (num > https://images.jiaoben.net100000) {
https://images.jiaoben.netreturn (num / https://images.jiaoben.net10000).https://images.jiaoben.nettoFixed(https://images.jiaoben.net1) + https://images.jiaoben.net'w';
} https://images.jiaoben.netelse https://images.jiaoben.netif (num > https://images.jiaoben.net1000) {
https://images.jiaoben.netreturn (num / https://images.jiaoben.net1000).https://images.jiaoben.nettoFixed(https://images.jiaoben.net1) + https://images.jiaoben.net'k';
} https://images.jiaoben.netelse {
https://images.jiaoben.netreturn num;
}
}
使用函数:
https://images.jiaoben.net// 创建笔记卡片元素
https://images.jiaoben.netfunction https://images.jiaoben.netcreateNoteElement(https://images.jiaoben.netnote) {
https://images.jiaoben.netconst noteElement = https://images.jiaoben.netdocument.https://images.jiaoben.netcreateElement(https://images.jiaoben.net"div");
noteElement.https://images.jiaoben.netclassName = https://images.jiaoben.net"masonry-item";
noteElement.https://images.jiaoben.netinnerHTML = https://images.jiaoben.net`
${note.cover}" alt="https://images.jiaoben.net${note.title}">
https://images.jiaoben.net${note.title}
`;
https://images.jiaoben.netreturn noteElement;
}
如下图11-6所示的是无限滚动刷新,查询完笔记数据之后的效果。
总结
数字格式化本质是一种“信息压缩”技术,通过牺牲部分精度来换取更高的传达效率。其核心价值在于:
- 认知层面:符合人类对量级的感知习惯,降低信息处理成本;
- 体验层面:优化界面布局,引导用户关注核心数据;
- 技术层面:通过动态策略平衡不同场景的显示需求。
在实际应用中,需根据业务场景、用户群体和数据特性定制格式化规则,避免因过度简化导致信息失真。
2.10 最佳实践总结及扩展建议
最佳实践
- 优先使用DTO模式:通过专门的DTO类定义API响应格式,避免直接序列化实体对象
- 合理设计关联关系:根据业务需求选择合适的加载策略(EAGER/FETCH)
- 使用@JsonView进行精细控制:在复杂场景中使用Jackson的@JsonView实现选择性序列化
- 结合性能考虑:懒加载是提高性能的重要手段,但需要配合合理的初始化策略
- 格式化数字展示:优化信息传达的效率和用户体验
- 无限滚动加载:优化了用户体验
- 适配移动设备和桌面设备:网格布局自动调整
如下图11-7所示的是适配移动设备之后的效果。
扩展建议
-
个性化推荐:
- 基于用户兴趣和行为的内容推荐
- 关注的用户发布的内容优先展示
-
搜索功能:
- 实现全文搜索
- 热门搜索词和搜索历史
-
内容筛选:
- 添加更多筛选条件(最新、最热、附近等)
-
视频内容:
- 支持视频内容的展示和播放
- 视频缩略图和播放控制
-
内容安全:
- 内容审核机制
- 敏感内容过滤
-
性能优化:
- 图片懒加载
- 内容预加载
- 分页数据缓存
3.1 首页搜索及瀑布流功能概述
- 首页搜索:在首页搜索框进行关键字搜索
- 从首页跳转到笔记详情页
- 从首页跳转到作者详情页
- 从笔记详情页跳转到作者详情页
- 首页布局:改为瀑布流布局
- 从底部导航栏导航到其他页面
3.2 掌握前端搜索功能的核心要点
在网页中实现 搜索功能通常涉及以下几个核心步骤:用户输入、搜索逻辑处理、结果展示和交互反馈。以下从前端实现到后端交互的完整流程进行解析,并提供代码示例。
前端HTML设置
修改explore.html中搜索框的内容:
https://images.jiaoben.net
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"col-md-3">
https://images.jiaoben.netdiv https://images.jiaoben.netclass=https://images.jiaoben.net"input-group">
https://images.jiaoben.netinput https://images.jiaoben.netclass=https://images.jiaoben.net"form-control" https://images.jiaoben.nettype=https://images.jiaoben.net"text" https://images.jiaoben.netplaceholder=https://images.jiaoben.net"搜索感兴趣的内容" https://images.jiaoben.netaria-label=https://images.jiaoben.net"Search"
https://images.jiaoben.netid=https://images.jiaoben.net"searchInput">
https://images.jiaoben.netbutton https://images.jiaoben.netclass=https://images.jiaoben.net"btn btn-outline-secondary" https://images.jiaoben.nettype=https://images.jiaoben.net"button" https://images.jiaoben.netid=https://images.jiaoben.net"searchButton">
搜索
https://images.jiaoben.netbutton>
https://images.jiaoben.netdiv>
https://images.jiaoben.netdiv>
修改点:
增加了id属性- 增加了
搜索触发方式
- 实时获取:使用
input事件用户输入,实时获取到搜索内容 - 按钮触发:添加搜索按钮,点击后执行搜索
以下代码实时获取到搜索内容,并缓存在searchContent变量中:
https://images.jiaoben.net// 缓存搜索的内容(确保在loadMoreNotes()执行前声明)
https://images.jiaoben.netlet searchContent = https://images.jiaoben.net'';
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
https://images.jiaoben.net// 获取搜索输入框的值
https://images.jiaoben.netconst searchInput = https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net'searchInput');
searchInput.https://images.jiaoben.netaddEventListener(https://images.jiaoben.net'input', https://images.jiaoben.netfunction(https://images.jiaoben.net) {
searchContent = https://images.jiaoben.netthis.https://images.jiaoben.netvalue;
});
以下代码当点击搜索按钮时,触发执行搜索:
https://images.jiaoben.net// 搜索按钮执行搜索
https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net'searchButton').https://images.jiaoben.netaddEventListener(https://images.jiaoben.net'click', https://images.jiaoben.netfunction(https://images.jiaoben.net) {
https://images.jiaoben.net// 获取搜索输入框的值
searchContent = https://images.jiaoben.netdocument.https://images.jiaoben.netgetElementById(https://images.jiaoben.net'searchInput').https://images.jiaoben.netvalue;
https://images.jiaoben.net// 执行搜索
https://images.jiaoben.netperformSearch();
});
https://images.jiaoben.net// 执行搜索
https://images.jiaoben.netfunction https://images.jiaoben.netperformSearch(https://images.jiaoben.net) {
https://images.jiaoben.net// 重置笔记网格数据
notesGrid.https://images.jiaoben.netinnerHTML = https://images.jiaoben.net'';
https://images.jiaoben.net// 恢复初始状态值
currentPage = https://images.jiaoben.net0;
isLoading = https://images.jiaoben.netfalse;
hasMore = https://images.jiaoben.nettrue;
https://images.jiaoben.net// 加载更多笔记
https://images.jiaoben.netloadMoreNotes();
};
分页导航点击事件处理
将与performSearch()代码逻辑一致的部分,重构为performSearch()。
https://images.jiaoben.net// 为分页导航添加点击事件
categoryItems.https://images.jiaoben.netforEach(https://images.jiaoben.nethttps://images.jiaoben.netitem => {
item.https://images.jiaoben.netaddEventListener(https://images.jiaoben.net'click', https://images.jiaoben.net() => {
categoryItems.https://images.jiaoben.netforEach(https://images.jiaoben.nethttps://images.jiaoben.netitem => {
item.https://images.jiaoben.netclassList.https://images.jiaoben.netremove(https://images.jiaoben.net'active');
});
item.https://images.jiaoben.netclassList.https://images.jiaoben.netadd(https://images.jiaoben.net'active');
https://images.jiaoben.net// 以下代码重构为performSearch()
https://images.jiaoben.net/*
// 重置笔记网格数据
notesGrid.innerHTML = '';
// 恢复初始状态值
currentPage = 0;
isLoading = false;
hasMore = true;
loadMoreNotes();
*/
https://images.jiaoben.netperformSearch();
});
});
重构loadMoreNotes()
重构loadMoreNotes()函数:
https://images.jiaoben.net// 加载更多笔记
https://images.jiaoben.netfunction https://images.jiaoben.netloadMoreNotes(https://images.jiaoben.net) {
https://images.jiaoben.netif (isLoading || !hasMore) {
https://images.jiaoben.net// 隐藏加载更多
https://images.jiaoben.nethideLoadMore();
https://images.jiaoben.net// 显示没有更多内容
https://images.jiaoben.netshowNoMoreContent();
https://images.jiaoben.netreturn;
}
isLoading = https://images.jiaoben.nettrue;
https://images.jiaoben.net// 显示加载更多
https://images.jiaoben.netshowLoadMore();
https://images.jiaoben.net// 获取当前分类
https://images.jiaoben.netlet category = https://images.jiaoben.netdocument.https://images.jiaoben.netquerySelector(https://images.jiaoben.net'.category-item.active').https://images.jiaoben.nettextContent.https://images.jiaoben.nettrim();
https://images.jiaoben.net// 发送请求
https://images.jiaoben.net/*fetch(`/explore/note?page=${currentPage + 1}&category=${category}`)*/
https://images.jiaoben.netfetch(https://images.jiaoben.net`/explore/note?page=https://images.jiaoben.net${currentPage + https://images.jiaoben.net1}&category=https://images.jiaoben.net${category}&query=https://images.jiaoben.net${searchContent}`)
https://images.jiaoben.net// ...为节约篇幅,此处省略非核心内容
}
在发送AJAX请求时,传递query参数,值是searchContent。
3.3 重构ExploreController处理搜索请求
控制器层
修改getNotesByCategory()方法,增加了query参数。
https://images.jiaoben.net/**
* 返回首页笔记探索页面的笔记数据
*/
https://images.jiaoben.net@GetMapping("/note")
https://images.jiaoben.netpublic ResponseEntity https://images.jiaoben.netgetNotesByCategoryhttps://images.jiaoben.net(
https://images.jiaoben.net@RequestParam(defaultValue = "1") https://images.jiaoben.netint page,
https://images.jiaoben.net@RequestParam(required = false) String category,
https://images.jiaoben.net@RequestParam(required = false) String query) {
https://images.jiaoben.net// 把“推荐”当成空
https://images.jiaoben.netif (DEFAULT_CATEGORY.equals(category)) {
category = https://images.jiaoben.netnull;
}
Page notes = https://images.jiaoben.netnull;
https://images.jiaoben.net// 区分是关键字搜索还是分类查询
https://images.jiaoben.netif (query == https://images.jiaoben.netnull || query.isEmpty()) {
notes = noteService.getNotesByPage(page, PAGE_SIZE, category);
} https://images.jiaoben.netelse {
notes = noteService.getNotesByPageAndQuery(page, PAGE_SIZE, category, query);
}
https://images.jiaoben.netNoteResponseDto https://images.jiaoben.netnotesResponseDto https://images.jiaoben.net= https://images.jiaoben.netnew https://images.jiaoben.netNoteResponseDto();
notesResponseDto.setHasMore(notes.hasNext());
https://images.jiaoben.net// 处理序列化问题
List noteExploreDtoList = https://images.jiaoben.netnew https://images.jiaoben.netArrayList<>();
https://images.jiaoben.netfor (Note note : notes.getContent()) {
noteExploreDtoList.add(NoteExploreDto.toExploreDto(note));
}
notesResponseDto.setNotes(noteExploreDtoList);
https://images.jiaoben.netreturn ResponseEntity.ok(notesResponseDto);
}
如果没有传入query参数值,则执行原有的NoteService.getNotesByPage()方法;否则,执行NoteService.getNotesByPageAndQuery()新方法。
服务层
修改NoteService,增加如下接口:
https://images.jiaoben.net/**
* 搜索分页查询笔记
*
* https://images.jiaoben.net@param page
* https://images.jiaoben.net@param pageSize
* https://images.jiaoben.net@param category
* https://images.jiaoben.net@param query
* https://images.jiaoben.net@return
*/
Page https://images.jiaoben.netgetNotesByPageAndQueryhttps://images.jiaoben.net(https://images.jiaoben.netint page, https://images.jiaoben.netint pageSize, String category, String query);
修改NoteServiceImpl,增加如下方法:
https://images.jiaoben.net@Override
https://images.jiaoben.netpublic Page https://images.jiaoben.netgetNotesByPageAndQueryhttps://images.jiaoben.net(https://images.jiaoben.netint page, https://images.jiaoben.netint pageSize, String category, String query) {
https://images.jiaoben.net// 构造Pageable对象,按照创建时间倒序排序
https://images.jiaoben.netPageable https://images.jiaoben.netpageable https://images.jiaoben.net= PageRequest.of(page - https://images.jiaoben.net1, pageSize, Sort.by(https://images.jiaoben.net"createAt").descending());
https://images.jiaoben.netif (category != https://images.jiaoben.netnull && !category.isEmpty() && query != https://images.jiaoben.netnull && !query.isEmpty()) {
https://images.jiaoben.netreturn noteRepository.findByCategoryAndTopicsContaining(category, query, pageable);
} https://images.jiaoben.netelse https://images.jiaoben.netif (query != https://images.jiaoben.netnull && !query.isEmpty()) {
https://images.jiaoben.netreturn noteRepository.findByTopicsContaining(query, pageable);
} https://images.jiaoben.netelse {
https://images.jiaoben.netreturn noteRepository.findAll(pageable);
}
}
如果没有传入category参数值,则执行原有的NoteRepository.findByTopicsContaining()方法;否则,执行NoteRepository.findByCategoryAndTopicsContaining()新方法。
仓库层
在 Spring Data JPA 中查询List类型的属性需要使用特殊的方法。针对Note实体中的topics属性,新增如下接口:
https://images.jiaoben.net/**
* 根据分类和话题标签分页查询笔记
*
* https://images.jiaoben.net@param category
* https://images.jiaoben.net@param query
* https://images.jiaoben.net@param pageable
* https://images.jiaoben.net@return
*/
Page https://images.jiaoben.netfindByCategoryAndTopicsContaininghttps://images.jiaoben.net(String category, String query, Pageable pageable);
https://images.jiaoben.net/**
* 根据话题标签分页查询笔记
*
* https://images.jiaoben.net@param query
* https://images.jiaoben.net@param pageable
* https://images.jiaoben.net@return
*/
Page https://images.jiaoben.netfindByTopicsContaininghttps://images.jiaoben.net(String query, Pageable pageable);
运行调测
在首页“推荐”分类执行搜素“Java”关键字,效果如下图12-1所示。
在首页“职场”分类执行搜素“Java”关键字,效果下图12-2所示。
两个搜素结果不一致,说明有些包含“Java”主题的笔记,并不在“职场”分类中。
3.4 从首页跳转到笔记详情页
类似于用户详情页的笔记列表的做法,从首页跳转到笔记详情页,只需要在原有的笔记封面上套一层即可。
https://images.jiaoben.net// 创建笔记卡片元素
https://images.jiaoben.netfunction https://images.jiaoben.netcreateNoteElement(https://images.jiaoben.netnote) {
https://images.jiaoben.netconst noteElement = https://images.jiaoben.netdocument.https://images.jiaoben.netcreateElement(https://images.jiaoben.net"div");
noteElement.https://images.jiaoben.netclassName = https://images.jiaoben.net"masonry-item";
noteElement.https://images.jiaoben.netinnerHTML = https://images.jiaoben.net`
https://images.jiaoben.net${note.title}
`;
https://images.jiaoben.netreturn noteElement;
}
点击笔记封面,就能跳转到笔记详情页了。
3.5 从首页跳转到作者详情页
前端修改
点击笔记的作者头像时,我们希望就能跳转到该作者的详情页。实现方式,只需要在作者信息的 跳转到 运行应用,效果如下图12-3所示,跳转逻辑没有问题,只是用户名下面有条下划线不是太美观。 去除用户名下面下划线的效果如下图12-4所示。 类似上一节的做法,也可以在笔记详情页作者信息区域,设置点击跳转到作者的详情页。 修改note-detail.html。 点击笔记的作者头像时,我们希望就能跳转到该作者的详情页。实现方式,只需要在作者头像上的 运行应用,作者头像效果如下图12-6所示。 点击作者头像就可以跳转到作者的详情页了,效果如下图12-7所示。 瀑布流布局是小红书等内容平台常用的设计方式,它可以根据内容高度自动调整位置,形成错落有致的视觉效果,提升用户浏览体验。 视觉优势: 用户体验: 响应式设计: 修改explore.html增加如下样式: 在 下面是将小红书首页笔记卡片改为瀑布流布局的效果演示方案。 大尺寸设备效果如下图12-8所示。 中等尺寸设备效果如下图12-9所示。 小尺寸设备效果如下图12-10所示。 通过以上实现,你可以将小红书首页的笔记卡片从传统网格布局改为瀑布流布局,提升用户体验和内容展示效果。 修改explore.html,实现从底部导航栏导航到其他页面的功能。 当点击暂未开放的功能时,比如“消息”,提示框效果如下图12-11所示。 实现一个高效的搜索功能需要综合考虑: 通过合理设计和技术选型,可以构建出既满足功能需求又具有良好用户体验的搜索系统。 图片懒加载: 性能优化: 动态加载内容: 相关标签:即可。
https://images.jiaoben.net// 创建笔记卡片元素
https://images.jiaoben.netfunction https://images.jiaoben.netcreateNoteElement(https://images.jiaoben.netnote) {
https://images.jiaoben.netconst noteElement = https://images.jiaoben.netdocument.https://images.jiaoben.netcreateElement(https://images.jiaoben.net"div");
noteElement.https://images.jiaoben.netclassName = https://images.jiaoben.net"masonry-item";
noteElement.https://images.jiaoben.netinnerHTML = https://images.jiaoben.net`
/user/profile页面需要传递用户ID,显然当前的note对象DTO里面并没有这个属性,因此需要做进一步的扩展。扩展NoteExploreDto
https://images.jiaoben.net/**
* 作者用户ID
*/
https://images.jiaoben.netprivate Long userId;
https://images.jiaoben.netpublic https://images.jiaoben.netstatic NoteExploreDto https://images.jiaoben.nettoExploreDtohttps://images.jiaoben.net(Note note) {
https://images.jiaoben.netNoteExploreDto https://images.jiaoben.netnoteExploreDto https://images.jiaoben.net= https://images.jiaoben.netnew https://images.jiaoben.netNoteExploreDto();
noteExploreDto.setNoteId(note.getNoteId());
noteExploreDto.setTitle(note.getTitle());
noteExploreDto.setCover(note.getImages().get(https://images.jiaoben.net0));
noteExploreDto.setUsername(note.getAuthor().getUsername());
noteExploreDto.setAvatar(note.getAuthor().getAvatar());
noteExploreDto.setUserId(note.getAuthor().getUserId());
https://images.jiaoben.netreturn noteExploreDto;
}
运行调测
https://images.jiaoben.net3.6 实现从笔记详情页跳转到作者详情页
加个CSS样式
https://images.jiaoben.net前端修改
上套一层即可。https://images.jiaoben.net
https://images.jiaoben.net运行调测
3.7 设计瀑布流布局实现方案
瀑布流布局的优势与特点
CSS样式
https://images.jiaoben.net/* 瀑布流布局 */
https://images.jiaoben.net.masonry {
https://images.jiaoben.netcolumn-count: https://images.jiaoben.net4;
https://images.jiaoben.netcolumn-gap: https://images.jiaoben.net1em;
https://images.jiaoben.netpadding: https://images.jiaoben.net10;
}
https://images.jiaoben.net.masonry-item {
https://images.jiaoben.netdisplay: inline-block;
https://images.jiaoben.netmargin: https://images.jiaoben.net0 https://images.jiaoben.net0 https://images.jiaoben.net1.5em;
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
}
https://images.jiaoben.net.masonry-note-image {
https://images.jiaoben.netborder-radius: https://images.jiaoben.net12px;
https://images.jiaoben.netwidth: https://images.jiaoben.net100%;
https://images.jiaoben.netheight: auto;
}
https://images.jiaoben.net@media https://images.jiaoben.netonly screen https://images.jiaoben.netand (https://images.jiaoben.netmax-width: https://images.jiaoben.net320px) {
https://images.jiaoben.net.masonry {
https://images.jiaoben.netcolumn-count: https://images.jiaoben.net1;
}
}
https://images.jiaoben.net@media https://images.jiaoben.netonly screen https://images.jiaoben.netand (https://images.jiaoben.netmin-width: https://images.jiaoben.net321px) https://images.jiaoben.netand (https://images.jiaoben.netmax-width: https://images.jiaoben.net768px){
https://images.jiaoben.net.masonry {
https://images.jiaoben.netcolumn-count: https://images.jiaoben.net2;
}
}
https://images.jiaoben.net@media https://images.jiaoben.netonly screen https://images.jiaoben.netand (https://images.jiaoben.netmin-width: https://images.jiaoben.net769px) https://images.jiaoben.netand (https://images.jiaoben.netmax-width: https://images.jiaoben.net1200px){
https://images.jiaoben.net.masonry {
https://images.jiaoben.netcolumn-count: https://images.jiaoben.net3;
}
}
https://images.jiaoben.net@media https://images.jiaoben.netonly screen https://images.jiaoben.netand (https://images.jiaoben.netmin-width: https://images.jiaoben.net1201px) {
https://images.jiaoben.net.masonry {
https://images.jiaoben.netcolumn-count: https://images.jiaoben.net4;
}
}
HTML应用样式
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.net创建笔记元素应用样式
// 创建笔记卡片元素
function createNoteElement(note) {
const noteElement = document.createElement("div");
noteElement.className = "masonry-item";
noteElement.innerHTML = `
https://images.jiaoben.net
https://images.jiaoben.net
https://images.jiaoben.netimg上增加masonry-note-image类型样式,同时去除note-image-container类型的div。瀑布流布局演示
3.8 从底部导航栏导航到其他页面
底部导航栏设置点击事件
https://images.jiaoben.net
https://images.jiaoben.net添加JS脚本处理导航
https://images.jiaoben.net// 导航函数
https://images.jiaoben.netfunction https://images.jiaoben.netnavigateTo(https://images.jiaoben.netpage) {
https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net'navigateTo: ' + page);
https://images.jiaoben.netif (page === https://images.jiaoben.net'home') {
https://images.jiaoben.netwindow.https://images.jiaoben.netlocation.https://images.jiaoben.nethref = https://images.jiaoben.net'/';
} https://images.jiaoben.netelse https://images.jiaoben.netif (page === https://images.jiaoben.net'publish') {
https://images.jiaoben.netwindow.https://images.jiaoben.netlocation.https://images.jiaoben.nethref = https://images.jiaoben.net'/note/publish';
} https://images.jiaoben.netelse https://images.jiaoben.netif (page === https://images.jiaoben.net'profile') {
https://images.jiaoben.netwindow.https://images.jiaoben.netlocation.https://images.jiaoben.nethref = https://images.jiaoben.net'/user/profile';
} https://images.jiaoben.netelse {
https://images.jiaoben.net// 待实现的功能页面
https://images.jiaoben.netalert(https://images.jiaoben.net'暂未开放,敬请期待!');
https://images.jiaoben.netreturn;
}
}
3.9 搜索功能的扩展与进阶及笔记卡片展示的优化建议
搜索功能的扩展与进阶
1. 全文搜索引擎
https://images.jiaoben.net// Elasticsearch 查询示例
https://images.jiaoben.net@Autowired
https://images.jiaoben.netprivate RestHighLevelClient client;
https://images.jiaoben.netpublic List2. 模糊搜索与纠错
3. 搜索分析与优化
总结
首页笔记卡片展示的优化建议
https://images.jiaoben.net// 使用Intersection Observer实现图片懒加载
https://images.jiaoben.netconst observer = https://images.jiaoben.netnew https://images.jiaoben.netIntersectionObserver(https://images.jiaoben.net(https://images.jiaoben.netentries) => {
entries.https://images.jiaoben.netforEach(https://images.jiaoben.nethttps://images.jiaoben.netentry => {
https://images.jiaoben.netif (entry.https://images.jiaoben.netisIntersecting) {
https://images.jiaoben.netconst img = entry.https://images.jiaoben.nettarget;
img.https://images.jiaoben.netsrc = img.https://images.jiaoben.netdataset.https://images.jiaoben.netsrc;
observer.https://images.jiaoben.netunobserve(img);
}
});
});
https://images.jiaoben.netdocument.https://images.jiaoben.netquerySelectorAll(https://images.jiaoben.net'img[data-src]').https://images.jiaoben.netforEach(https://images.jiaoben.nethttps://images.jiaoben.netimg => {
observer.https://images.jiaoben.netobserve(img);
});
相关推荐
2026-04-17
专题
+ 收藏
+ 收藏
+ 收藏
+ 收藏
+ 收藏
+ 收藏
最新数据
相关文章
一天一个开源项目(第23篇):PageLM - 开源 AI 教育平台,把学习材料变成互动资源
开源大模型涨价策略分析:Llama 3.5 与 GLM-5 的商业化博弈
每周AI论文速递(260209-260213)
anthropic-academy:RAG检索增强生成
90%程序员还在让 AI 补代码,1%已经在指挥 AI 军团
# 从 0 到 1:**黎跃春**详解 AI 智能体运用工程师的工程化方法
Memo Code 安全设计:子进程、命令防护与权限审批的统一方案
Samba WINS 漏洞利用与防御全解析
拒绝“盲盒式”编程:规范驱动开发(SDD)如何重塑 AI 交付
ComfyUI 的缓存架构和实现
AI精选
