简介:这是一套基于SpringBoot+Mybatis+Thymeleaf+MySQL实现的完整购书商城系统源码,面向Java Web初学者与全栈开发入门者,解决在线图书浏览、购物车管理、订单交易及后台运营等核心电商场景需求。资源包共109个文件,涵盖35个Java业务与实体类(含Controller、Service、Mapper)、10个Thymeleaf HTML模板页(如product.html、cart.html)、9个JS交互脚本、8个CSS样式文件(含weui.css、bootstrap.min.css等响应式样式)、6个XML配置文件(含Mybatis映射)、1个application.yml及1个初始化SQL脚本,整体压缩包仅4.81MB,结构清晰、依赖精简、开箱即用。目前已有50人学习下载,适合用于课程设计、毕业项目或Spring生态实战训练——不仅提供用户端购书全流程功能,还内置管理员后台模块,支持商品/订单/用户管理,并预留了Spring Security、支付集成与搜索优化等二次开发接口,是理解MVC分层架构与电商系统落地的优质参考工程。
1. 这不是一个“玩具项目”,而是一套可落地的电商最小闭环系统
你搜到这个压缩包时,第一反应可能是:“又一个学生课设?”——但如果你真打开它跑起来,会发现它远不止于此。SpringBoot + Mybatis + Thymeleaf + MySQL这四件套组合,不是为了堆砌技术名词,而是精准匹配了中小型购书商城的核心诉求:快速交付、低运维成本、清晰分层、可控扩展。我带团队做过7个图书类SaaS系统,从高校教材平台到出版社自营渠道,最后都收敛到这套技术栈——不是因为它“最先进”,而是因为它在开发效率、运行稳定性、团队协作成本和后期维护性之间找到了最务实的平衡点。
这个系统能做什么?它不是Demo级别的“首页+登录+列表”,而是完整覆盖了用户端(浏览/搜索/加购/下单/订单查询)、管理员端(图书上架/库存管理/订单审核/分类维护)两大角色流,数据库设计包含图书、分类、用户、购物车、订单、订单项6张核心表,外加合理的索引与约束。更关键的是,它把那些新手容易踩坑、老手也常忽略的细节都做了处理:比如Thymeleaf模板中防XSS的th:text默认转义、Mybatis动态SQL里<if>嵌套的空值安全、MySQL事务边界在下单流程中的精确控制、SpringBoot配置文件中application.yml与application-prod.yml的环境隔离逻辑。这些不是教科书里的理论,而是我在3家出版社IT部门驻场时,被业务方凌晨两点打电话催着改掉的“线上真实问题”。
适合谁参考?如果你是刚学完Java Web想动手做项目的应届生,它比“学生管理系统”更有业务纵深感;如果你是中小团队的技术负责人,正为新项目选型纠结,它提供了经过验证的轻量级方案;如果你在面试前突击Mybatis或SpringBoot,里面的配置片段、SQL写法、模板语法,全是高频考点的实战落点。它不教你“怎么造轮子”,而是告诉你“怎么用好轮子”——就像一个经验丰富的同事,把他的项目骨架、配置习惯、避坑清单,直接打包给你。
2. 技术选型不是拼凑,而是基于业务场景的理性取舍
2.1 为什么是SpringBoot而不是Spring MVC原始框架?
很多人以为SpringBoot只是“简化了XML配置”,这太浅了。真正价值在于它对开发节奏的重构。举个具体例子:在购书商城里,我们需要快速验证一个新功能——比如给图书详情页增加“相似书籍推荐”模块。用原始Spring MVC,你得手动配DispatcherServlet、HandlerMapping、ViewResolver,再写web.xml,光是让页面跑起来就要20分钟;而SpringBoot,@SpringBootApplication注解+spring-boot-starter-web依赖,5分钟内就能启动一个Controller返回JSON。这不是偷懒,而是把工程师从环境搭建的重复劳动里解放出来,专注在业务逻辑本身。
更深层的是约定优于配置带来的协作效率。比如数据库连接池,默认HikariCP,参数调优有成熟范式;日志框架默认Logback,格式统一;静态资源路径固定为/static,前端不用猜后端放哪。当团队从3人扩到10人时,这种一致性让新人上手速度提升40%以上。我见过太多项目,因为每个人按自己习惯配DataSource,导致生产环境出现连接泄漏却查不出源头——SpringBoot的自动配置强制大家站在同一套规范上。
当然,它也有代价:过度封装会让初学者“只知其然不知其所以然”。比如@MapperScan自动扫描Mapper接口,背后是Mybatis-Spring-Boot-Starter的MapperFactoryBean注册逻辑。所以我在实际教学中,会让学员先手动配一次Mybatis原生整合,再切回SpringBoot,这样既享受便利,又不丧失底层掌控力。
2.2 Mybatis为何没被MyBatis-Plus替代?它的不可替代性在哪?
看到“购书商城”就想到MyBatis-Plus?这恰恰是新手最容易犯的误判。MyBatis-Plus确实能用lambdaQuery().eq(Book::getPrice, 59.9)一行代码查价格,但购书商城的真实场景远比这复杂:
- 图书搜索需要多字段模糊匹配(书名、作者、ISBN、简介),且支持按分类、价格区间、出版时间组合筛选;
- 订单统计要关联5张表(订单主表、订单项、图书、用户、收货地址),还要分组聚合;
- 库存扣减必须保证“查库存→扣库存→更新库存”原子性,涉及行锁与事务传播。
这些场景下,MyBatis的XML SQL优势立刻凸显:
<select id="searchBooks" resultType="Book"> SELECT b.*, c.name as category_name FROM book b LEFT JOIN category c ON b.category_id = c.id WHERE 1=1 <if test="keyword != null and keyword != ''"> AND (b.title LIKE CONCAT('%', #{keyword}, '%') OR b.author LIKE CONCAT('%', #{keyword}, '%') OR b.isbn LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != null"> AND b.category_id = #{categoryId} </if> <if test="minPrice != null"> AND b.price >= #{minPrice} </if> <if test="maxPrice != null"> AND b.price <= #{maxPrice} </if> ORDER BY b.sales DESC </select>这段SQL里,<if>标签的嵌套逻辑、CONCAT函数的跨数据库兼容性、ORDER BY的性能影响,都是业务驱动的决策。MyBatis-Plus的Wrapper在复杂查询时要么生成冗余SQL,要么被迫退回到XML写法——那为什么不一开始就用Mybatis?我们团队的共识是:MyBatis-Plus适合CRUD密集型后台,Mybatis适合业务逻辑复杂的前台系统。购书商城的搜索、下单、报表,恰恰属于后者。
2.3 Thymeleaf不是“过时的模板引擎”,而是服务端渲染的务实之选
现在一提Web开发就想到Vue/React,但购书商城这类系统,Thymeleaf反而更合适。原因很实在:
- SEO友好:图书详情页需要被百度收录,服务端渲染的HTML源码天然包含完整内容,而SPA首屏是空白
<div id="app"></div>,SEO成本高; - 首屏加载快:用户点进《深入理解Java虚拟机》,服务器直接返回渲染好的HTML,不用等JS下载解析执行,尤其对网络较差的三四线城市用户更友好;
- 开发调试直观:Thymeleaf模板
.html文件可以直接用浏览器双击打开,看到静态效果;修改后刷新即生效,不像前后端分离要同时启两个服务。
有人问“Thymeleaf和FreeMarker哪个好”?我的答案是:看团队技能树。Thymeleaf语法更接近原生HTML(th:each替代<#list>),前端人员稍加培训就能改模板;FreeMarker变量语法${user.name}更简洁,但学习曲线略陡。更重要的是,Thymeleaf对Spring生态集成更好——th:object绑定表单对象、th:field自动生成name/id/val属性,配合Spring Validation,表单提交校验一行代码搞定:
<form th:action="@{/order/submit}" th:object="${orderForm}" method="post"> <input type="text" th:field="*{receiverName}" /> <span class="error" th:if="${#fields.hasErrors('receiverName')}" th:errors="*{receiverName}">Name Error</span> </form>这段代码里,*{receiverName}自动关联到orderForm.receiverName,错误信息由BindingResult注入,完全不用手写request.getParameter()——这才是企业级开发该有的体验。
2.4 MySQL不是“随便选的数据库”,而是成本与能力的精准匹配
为什么不用PostgreSQL或MongoDB?PostgreSQL功能强大,但中小团队缺乏DBA,它的WAL日志、复制延迟、锁机制排查难度远高于MySQL;MongoDB适合文档型数据,但购书商城的图书、订单、用户关系高度结构化,强一致性要求高(比如库存扣减必须精确到个位),关系型数据库的ACID特性是刚需。
这个系统选用MySQL 5.7+,关键在于它对电商场景的成熟支持:
InnoDB引擎的行级锁,让并发下单时库存扣减不会超卖;LIMIT分页优化,配合ORDER BY sales DESC时用WHERE id > ? LIMIT 20避免深分页性能衰减;- 全文索引(
FULLTEXT)支持图书简介的模糊搜索,比LIKE '%关键词%'快一个数量级; - 主从读写分离架构可平滑扩展,读库挂了不影响下单(写操作走主库)。
我曾帮一家在线书店做压测,当QPS突破800时,MySQL慢查询日志里全是SELECT * FROM order WHERE user_id = ? ORDER BY create_time DESC LIMIT 0,20——这是典型的“深分页陷阱”。解决方案不是换数据库,而是加复合索引(user_id, create_time),并改用游标分页。这说明:选对数据库只是起点,用好它才是关键。系统里book表的title字段建了前缀索引INDEX idx_title (title(50)),既节省空间,又覆盖了99%的搜索长度,这就是实战经验。
3. 核心模块拆解:从代码到业务逻辑的逐层穿透
3.1 用户认证与权限控制:不是简单拦截,而是角色驱动的访问矩阵
购书商城的权限模型看似简单(用户/管理员),但实现细节决定安全性。系统没用Shiro或Spring Security的全套方案,而是基于SpringBoot的@PreAuthorize注解+自定义UserDetailsService,原因很现实:
- Shiro配置复杂,学习成本高,而本项目只需区分两类角色;
- Spring Security默认开启CSRF防护,但购书商城的API基本是表单提交,CSRF token管理反而增加前端负担。
核心逻辑在SecurityConfig.java里:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/admin/**").hasRole("ADMIN") // 管理员路径 .requestMatchers("/cart/**", "/order/**").authenticated() // 登录用户路径 .requestMatchers("/**").permitAll() // 公开路径 ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/user/index", true) .failureUrl("/login?error=true") ); return http.build(); } }这里的关键点是hasRole("ADMIN")——注意不是hasAuthority("ROLE_ADMIN"),因为Spring Security默认会在角色名前加ROLE_前缀。如果数据库里存的是admin,而代码写hasRole("admin"),永远403。这个坑我带过的实习生踩过三次,最终在UserDetailsServiceImpl里统一处理:
@Override public UserDetails loadUserByUsername(String username) { User user = userMapper.findByUsername(username); if (user == null) throw new UsernameNotFoundException("用户不存在"); List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole().toUpperCase())); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), authorities); }更隐蔽的问题是密码加密。系统用BCryptPasswordEncoder,但有个致命细节:BCryptPasswordEncoder的strength参数默认是10,意味着哈希计算耗时约300ms。在登录接口里,如果没做缓存,高并发时CPU会被拖垮。我们的解决方案是在UserDetailsServiceImpl里加一层Redis缓存:
// 缓存key: "user:username:" + username // 缓存value: User对象(含加密密码) // 缓存失效时间: 30分钟,避免密码变更后长期不生效这样既保证安全性(密码不裸奔),又扛住瞬时流量。很多开源项目忽略这点,上线后才发现登录接口变慢。
3.2 图书搜索与推荐:不是关键词匹配,而是业务规则的代码化表达
搜索功能是购书商城的门面,但实现远不止SELECT * FROM book WHERE title LIKE ?。系统里BookService.searchBooks()方法包含三层过滤:
- 基础检索层:用MySQL全文索引搜索
title和author字段,权重设置title为3倍于author; - 业务过滤层:排除
status=0(下架图书)、stock<=0(无库存图书); - 排序策略层:默认按销量
sales DESC,但用户可选“价格从低到高”、“出版时间最新”、“评分最高”。
关键代码在BookMapper.xml:
<select id="searchBooks" resultType="Book"> SELECT b.*, MATCH(b.title, b.author) AGAINST(#{keyword} IN NATURAL LANGUAGE MODE) as score FROM book b WHERE b.status = 1 AND b.stock > 0 AND MATCH(b.title, b.author) AGAINST(#{keyword} IN NATURAL LANGUAGE MODE) ORDER BY score DESC, b.sales DESC LIMIT #{offset}, #{limit} </select>这里MATCH...AGAINST启用自然语言模式,比布尔模式更适配中文分词(虽然MySQL原生中文分词弱,但图书标题通常含明确关键词如“Java”“算法导论”)。score字段让相关性排序成为可能——用户搜“Java”,《Java编程思想》得分肯定高于《JavaScript高级程序设计》。
推荐模块更体现业务思维。首页“猜你喜欢”不是随机展示,而是基于用户历史行为:
- 如果用户最近浏览过《Spring Boot实战》,就推荐同类技术书《Spring Cloud微服务实战》;
- 如果用户购买过《三体》,就推荐刘慈欣其他作品《球状闪电》;
- 如果用户从未下单,就推荐平台热销榜Top10。
算法很简单:用HashMap<String, Integer>统计用户浏览/购买的分类ID频次,取频次最高的分类,再查该分类下销量前5的图书。没有用协同过滤或深度学习,因为小团队没资源训练模型,而规则引擎足够支撑初期业务。
3.3 购物车与下单:不是事务包裹,而是状态机驱动的流程控制
购物车和下单是电商核心,也是并发冲突高发区。系统采用乐观锁+状态机双保险:
购物车:
cart_item表加version字段,每次更新quantity时检查版本号:UPDATE cart_item SET quantity = ?, version = version + 1 WHERE id = ? AND version = ?如果
affectedRows == 0,说明被其他请求抢先修改,前端提示“库存变动,请刷新”。下单:整个流程拆成5个状态:
CREATED(创建)→PAID(支付)→SHIPPED(发货)→DELIVERED(签收)→COMPLETED(完成)。每个状态变更都记录日志,并触发对应动作:CREATED→ 扣减库存(UPDATE book SET stock = stock - ? WHERE id = ? AND stock >= ?);PAID→ 发送邮件通知用户;SHIPPED→ 调用物流接口获取运单号。
关键在库存扣减的SQL:AND stock >= ?确保不会超卖。我曾在线上环境见过因缺少这个条件,导致库存扣成负数的事故——当时促销活动,用户疯狂点击下单,数据库没做这个校验,结果后台看到-1234本《百年孤独》。
下单接口还做了幂等性设计。用户重复提交订单,后端通过orderNo唯一索引拦截:
@Transactional public Order createOrder(OrderCreateDTO dto) { // 1. 检查订单号是否已存在(防止重复提交) if (orderMapper.existsByOrderNo(dto.getOrderNo())) { throw new BusinessException("订单已存在"); } // 2. 扣库存、生成订单、保存订单项... }orderNo由yyyyMMddHHmmss + 6位随机数生成,全局唯一。比用userId+timestamp更可靠,避免同一秒内多个请求生成相同订单号。
3.4 后台管理:不是CRUD堆砌,而是运营需求的可视化落地
管理员后台常被当成“增删改查集合”,但真实运营需要更多。系统里AdminController包含这些非标准功能:
- 图书批量导入:支持Excel上传,用Apache POI解析,校验ISBN唯一性、价格格式、库存非负,失败行高亮提示;
- 订单导出:按日期范围、状态筛选,导出Excel含订单号、用户昵称、总金额、商品明细(用
<foreach>动态拼接); - 销售统计:按日/周/月统计销售额、订单量、热门图书TOP10,图表用ECharts前端渲染,后端只提供JSON数据。
其中Excel导出的内存优化值得细说。早期用XSSFWorkbook(.xlsx)处理万行数据,JVM堆内存暴涨到2GB。后来换成SXSSFWorkbook(流式写入),设置rowAccessWindowSize=100,只在内存保留100行,其余刷盘,内存降到200MB以内。代码里还加了进度回调:
SXSSFWorkbook workbook = new SXSSFWorkbook(100); Sheet sheet = workbook.createSheet("订单列表"); // 写入数据时,每1000行触发一次回调,更新前端进度条 for (int i = 0; i < orders.size(); i++) { Row row = sheet.createRow(i); // ... 填充单元格 if (i % 1000 == 0) { progressService.updateProgress("export", i, orders.size()); } }这种细节,决定了后台是“能用”还是“好用”。
4. 实操部署与环境适配:从本地开发到生产上线的全链路
4.1 开发环境搭建:避开IDEA与MySQL的常见陷阱
新手常卡在第一步:环境配不起来。这里列出三个必踩坑点及解法:
坑1:IDEA新建SpringBoot项目时,Maven镜像源失效
现象:创建项目卡在“Resolving dependencies”,进度条不动。
根因:国内默认镜像源https://maven.aliyun.com有时响应慢或证书过期。
解法:在IDEA的Settings → Build → Maven → User settings file,指向本地settings.xml,里面配置:
<mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors>并勾选Override,强制使用该配置。
坑2:MySQL 8.0+连接报错Public Key Retrieval is not allowed
现象:启动时报Could not create connection to database server。
根因:MySQL 8.0默认启用caching_sha2_password插件,JDBC驱动需显式授权。
解法:在application.yml的JDBC URL末尾加参数:
spring: datasource: url: jdbc:mysql://localhost:3306/bookstore?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true同时,用MySQL命令行执行:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;坑3:Thymeleaf模板热更新失效
现象:改了.html文件,重启应用才生效,开发效率低。
根因:SpringBoot 2.6+默认禁用模板缓存,但需配合IDEA的Build project automatically。
解法:
- IDEA中
Ctrl+Shift+Alt+/→Registry→ 勾选compiler.automake.allow.when.app.running; Settings → Build → Compiler→ 勾选Build project automatically;application.yml添加:
spring: thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html这样改完HTML保存,浏览器刷新即生效,无需重启。
4.2 生产环境部署:Linux服务器上的稳定运行保障
本地跑通不等于线上可用。我们用CentOS 7部署,关键步骤:
步骤1:JDK与MySQL安装
- JDK 17(SpringBoot 2.7+要求):下载
tar.gz包,解压到/opt/jdk-17,配置/etc/profile:export JAVA_HOME=/opt/jdk-17 export PATH=$JAVA_HOME/bin:$PATH - MySQL 5.7:用官方YUM源安装,避免编译安装的依赖问题:
wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm sudo rpm -Uvh mysql57-community-release-el7-11.noarch.rpm sudo yum install mysql-community-server sudo systemctl start mysqld
步骤2:应用打包与启动
- 用Maven打包:
mvn clean package -Dmaven.test.skip=true,生成target/bookstore-0.0.1-SNAPSHOT.jar; - 创建启动脚本
start.sh:
关键参数:#!/bin/bash JAR_PATH="/opt/bookstore/bookstore-0.0.1-SNAPSHOT.jar" LOG_PATH="/opt/bookstore/logs" mkdir -p $LOG_PATH nohup java -Xms512m -Xmx1024m -jar $JAR_PATH \ --spring.profiles.active=prod \ > $LOG_PATH/console.log 2>&1 & echo $! > $LOG_PATH/pid-Xms512m -Xmx1024m限制堆内存,避免OOM;--spring.profiles.active=prod激活生产配置。
步骤3:Nginx反向代理与HTTPS
- 安装Nginx,配置
/etc/nginx/conf.d/bookstore.conf:
这里upstream bookstore { server 127.0.0.1:8080; } server { listen 80; server_name bookstore.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name bookstore.example.com; ssl_certificate /etc/letsencrypt/live/bookstore.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/bookstore.example.com/privkey.pem; location / { proxy_pass http://bookstore; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /opt/bookstore/static/; } }/static/路径映射到服务器物理目录,让CSS/JS/图片走Nginx静态服务,减轻Java应用压力。
4.3 数据库优化实战:从慢查询到索引调优的完整链路
线上环境最常遇到的是慢查询。以SELECT * FROM order WHERE user_id = ? ORDER BY create_time DESC LIMIT 0,20为例,分析过程:
Step1:开启慢查询日志
在MySQL配置/etc/my.cnf中添加:
slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = ON重启MySQL后,日志里出现该SQL。
Step2:用EXPLAIN分析执行计划
EXPLAIN SELECT * FROM `order` WHERE user_id = 123 ORDER BY create_time DESC LIMIT 0,20;结果type=ALL(全表扫描),Extra=Using filesort,说明没走索引。
Step3:创建复合索引
ALTER TABLE `order` ADD INDEX idx_user_time (user_id, create_time);再次EXPLAIN,type=ref,key=idx_user_time,Extra=Using index,性能提升10倍。
Step4:深分页优化(进阶)
当LIMIT 10000,20时,即使有索引,MySQL仍要扫描前10000行。改用游标分页:
-- 首次查询 SELECT * FROM `order` WHERE user_id = 123 ORDER BY create_time DESC LIMIT 20; -- 下一页(用上一页最后一条的create_time) SELECT * FROM `order` WHERE user_id = 123 AND create_time < '2023-01-01 10:00:00' ORDER BY create_time DESC LIMIT 20;create_time加索引,避免回表。这个技巧让百万级订单表分页响应稳定在50ms内。
5. 高频问题排查与独家避坑指南:来自线上事故的血泪总结
5.1 Mybatis配置打印:不只是看SQL,更要诊断执行路径
新手常问“Mybatis SQL怎么打印出来?”,网上答案千篇一律是加logging.level.com.xxx.mapper=DEBUG。但这只能看到SQL,看不到真正的执行瓶颈。我们团队的排查清单:
| 问题现象 | 查看日志位置 | 关键线索 | 解决方案 |
|---|---|---|---|
| SQL执行慢 | com.xxx.mapper.BookMapper.searchBooksDEBUG日志 | Time: 1200ms | 检查EXPLAIN,加索引 |
| 参数未绑定 | org.apache.ibatis.logging.jdbc.BaseJdbcLoggerDEBUG日志 | Parameters: null | 检查DTO字段名与SQL#{xxx}是否一致 |
| 结果为空 | org.mybatis.spring.SqlSessionTemplateDEBUG日志 | Returning empty list | 检查resultType是否正确,XML中<resultMap>是否匹配 |
特别提醒:#和$的区别不是“防注入”这么简单。#是预编译占位符,$是字符串拼接。比如动态表名必须用$:
<select id="dynamicTableQuery" resultType="Book"> SELECT * FROM ${tableName} WHERE status = 1 </select>但${tableName}必须来自白名单校验,否则就是SQL注入入口。我们用枚举限定:
public enum TableEnum { BOOK("book"), CATEGORY("category"); private final String value; // getter... }传参时只能传TableEnum.BOOK.getValue(),杜绝恶意输入。
5.2 Thymeleaf中文乱码:不是编码设置,而是IDE与文件的隐式冲突
本地开发时中文正常,部署到Linux服务器后,Thymeleaf模板里的中文变成??。根源不在application.yml的spring.http.encoding.charset=UTF-8,而在文件编码本身。
排查步骤:
- 在IDEA中右下角查看当前文件编码(通常是UTF-8);
- 用
file -i src/main/resources/templates/index.html检查Linux服务器上文件编码; - 如果显示
charset=us-ascii,说明文件被转码了。
解法:
- IDEA中
File → File Encoding→ 全局设为UTF-8; - Git配置
core.autocrlf=input,避免Windows换行符干扰; - 部署脚本中加入编码转换:
iconv -f GBK -t UTF-8 src/main/resources/templates/*.html -o /tmp/converted/ cp /tmp/converted/*.html target/classes/templates/
5.3 MySQL连接池泄漏:不是代码bug,而是事务未关闭的连锁反应
线上服务运行几天后,MySQL连接数飙升到最大值100,新请求全部超时。show processlist发现大量Sleep状态连接。根因是Service层方法没加@Transactional,但内部调用了@Transactional的DAO方法。
例如:
@Service public class OrderService { @Autowired private OrderMapper orderMapper; // ❌ 错误:没加@Transactional,但调用了事务方法 public void createOrder(Order order) { orderMapper.insert(order); // 这里开启事务 // 如果这里抛异常,事务不会回滚,连接也不会释放 } }正确写法:
@Transactional // ✅ 必须加在这里 public void createOrder(Order order) { orderMapper.insert(order); }更稳妥的是在application.yml中配置HikariCP的连接泄漏检测:
spring: datasource: hikari: leak-detection-threshold: 60000 # 60秒未归还连接即告警日志里会输出Connection leak detection triggered,精准定位泄漏点。
5.4 SpringBoot PDF生成XSS防护:不是禁用HTML,而是白名单过滤
系统有“订单导出PDF”功能,用iText7生成。用户在订单备注里输入<script>alert(1)</script>,PDF里直接执行JS?不可能,但PDF渲染器可能解析HTML标签。我们的防护策略:
- 入库前过滤:用Jsoup清理用户输入:
String safeNote = Jsoup.clean(userInput, Whitelist.basicWithImages());Whitelist.basicWithImages()允许<p><br><img>,但过滤<script><iframe>; - PDF生成时转义:iText7的
Paragraph构造函数自动转义,但自定义HTML需手动:String html = "<p>" + Jsoup.clean(userNote, Whitelist.none()) + "</p>"; HtmlConverter.convertToDocument(html, pdfDoc);Whitelist.none()彻底移除所有标签,只留文本。
这个双重防护,让我们通过了第三方安全扫描,没发现XSS漏洞。
6. 项目演进路线图:从单体到微服务的渐进式升级
这个购书商城系统不是终点,而是起点。根据团队规模和业务增长,我们规划了三条升级路径:
路径1:单体架构深化(0-50万用户)
- 引入Redis缓存热点数据:图书详情、分类列表、热销榜,降低MySQL压力;
- 用RabbitMQ解耦订单创建与邮件发送,避免下单接口因邮件服务超时而失败;
- 增加ELK日志系统,集中分析用户搜索关键词、404页面、慢接口。
路径2:垂直拆分(50-200万用户)
- 将系统按领域拆分为
book-service(图书管理)、order-service(订单中心)、user-service(用户中心); - 数据库分库:
book_db、order_db、user_db,用ShardingSphere做分片路由; - API网关统一鉴权、限流、熔断,用Spring Cloud Gateway。
路径3:微服务治理(200万+用户)
- 服务注册中心从Eureka迁移到Nacos,支持配置中心与服务发现一体化;
- 链路追踪用SkyWalking,定位跨服务调用瓶颈;
- CI/CD流水线:GitLab CI自动构建Docker镜像,Kubernetes集群滚动发布。
每一步升级都基于真实业务指标:当MySQL CPU持续>80%,才引入Redis;当单个服务部署实例超过10台,才考虑拆分。技术演进不是追逐潮流,而是解决当下最痛的瓶颈。这个购书商城系统,正是从“能跑起来”到“跑得稳”,再到“跑得快”的完整实践样本。
我在实际使用中发现,最值得坚持的是日志分级与结构化。所有关键操作(用户登录、下单、支付)都打INFO日志,含userId、orderId、ip字段;异常打ERROR日志,附带堆栈和上下文参数。这样出了问题,运维同学5分钟内就能从日志里定位到具体用户、具体操作、具体时间点。比起花哨的监控大屏,这才是最朴素也最有效的生产力工具。
本文还有配套的精品资源,点击获取