news 2026/10/30 5:53:59

SpringBoot+MyBatis+Thymeleaf超市后台快速落地指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+MyBatis+Thymeleaf超市后台快速落地指南

简介:这是一套基于Spring Boot开发的超市信息管理系统完整源码资源,面向Java初学者与Web开发入门者,解决中小型商超日常经营中商品采购、销售管理、库存盘点及财务分析等核心业务场景的信息化需求。资源包大小为7.92MB,为RAR压缩格式,包含可直接运行的后端工程、配套数据库脚本及基础配置文件,支持远程调试与本地部署,结构清晰、注释规范,便于理解MVC分层设计与RESTful接口实践。已有918人学习下载,适合用于课程设计、毕业设计或Spring Boot实战训练。读者可获得完整可运行系统(含登录、商品录入、库存管理、销售模块等核心功能)、MySQL建表语句与初始化数据、前后端交互逻辑说明,以及论文中对应的技术实现思路梳理,有助于快速掌握企业级Java Web项目开发全流程。

1. 一个能当天跑通、次日就敢往里填真实商品数据的超市后台:SpringBoot + MyBatis + Thymeleaf 三件套落地实录

上周帮某高校实验室搭实训环境,导师甩来一个「超市信息管理系统」压缩包,说“学生交作业用,别整太复杂”。我点开一看:没 README.md,没 application.yml 示例,连数据库建表语句都藏在 resources/sql/ 下面一个叫 init.sql 的文件里——但双击 jar 包居然真弹出了登录页。输入 admin/123456,货架、员工、进货单全在眼前。更关键的是,它没用任何云服务或外部依赖:H2 内存库开箱即用,MySQL 切换只要改两行配置;Thymeleaf 模板全在 templates/ 下,改个商品列表页不用重启;连远程调试参数都写死在 pom.xml 里——-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005。这不是教学 Demo,是能直接塞进小超市老板手机相册里、让他指着屏幕说“这个价格栏我要加个红色警示”的生产级骨架。适合 Java 入门者练手 SpringBoot 全流程,也适合老手拿它当基座,三天内接入扫码枪或微信支付。


2. 项目结构与技术栈解剖:从 pom.xml 到 templates 目录的每一层意图

2.1 为什么选 SpringBoot 而不是 SpringMVC 原生?——pom.xml 里的 5 个关键依赖真相

打开pom.xml,最核心的不是<spring-boot-starter-web>,而是这 5 行:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>

提示:spring-boot-starter-jdbc是关键伏笔——它让 MyBatis 不需要 XML 配置就能自动绑定 DataSource;而 H2 和 MySQL 同时存在,意味着项目默认走 H2(内存库,启动快),但只要把application.yml里spring.datasource.url换成 MySQL 地址,再删掉h2依赖,整个系统立刻切到真实数据库,零 SQL 重写。

常见误区是以为mybatis-spring-boot-starter会自动扫描 Mapper 接口——其实它只负责注入 SqlSessionFactory,真正的扫描靠@MapperScan("com.example.supermarket.mapper")注解,这个注解在SupermarketApplication.java主类上写着。如果你删了它,启动时会报Could not find mapper interface,但错误日志里根本不会提“扫描路径”,只会说BeanCreationException——这是新手翻车第一现场。

2.2 目录结构即设计语言:templates/ 与 static/ 的分工铁律

项目前端部分严格遵循 SpringBoot 默认约定:

src/main/resources/ ├── application.yml # 主配置,含数据库、服务器端口、Thymeleaf 开关 ├── sql/ │ └── init.sql # H2 初始化脚本:建库、建表、插 3 条测试数据 └── static/ ├── css/ │ └── style.css # 全局样式,仅覆盖基础排版(无 Bootstrap) └── js/ └── main.js # 商品搜索的 debounce 实现,防连续请求 src/main/resources/templates/ ├── index.html # 首页:商品总览卡片 + 最近 5 条进货记录 ├── goods/ │ ├── list.html # 商品列表页:分页 + 搜索框 + “新增”按钮 │ └── edit.html # 商品编辑页:form 表单,method="post",action="/goods/save" ├── employees/ │ └── list.html # 员工列表:带“启用/禁用”开关(AJAX PATCH) └── layout.html # Thymeleaf 布局模板:定义 header/footer,用 th:fragment="content" 插入子页

注意static/和templates/的本质区别:

  • static/css/style.css是纯静态资源,浏览器直取,不经过 Spring MVC 拦截器,所以不能用@Value("${app.version}")注入变量;
  • templates/goods/list.html是 Thymeleaf 模板,渲染时可调用@serviceBean、读取@Controller传来的 Model 数据,比如${goods}就是 Controller 里model.addAttribute("goods", goodsList)放进去的 List。

layout.html的写法值得抄作业:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title th:text="${title} ?: '超市管理系统'">默认标题</title> </head> <body> <header th:fragment="header"> <h1>超市信息管理系统 v1.0</h1> <nav><a href="/">首页</a> | <a href="/goods/list">商品管理</a></nav> </header> <main th:fragment="content"> <!-- 子页面通过 th:replace="~{::content}" 注入此处 --> </main> <footer th:fragment="footer"> <p>&copy; 2023 超市后台 · 仅供学习交流</p> </footer> </body> </html>

子页goods/list.html只需写:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> <head> <title>商品列表</title> </head> <body> <div layout:fragment="content"> <h2>商品管理</h2> <table> <tr th:each="g : ${goods}"> <td th:text="${g.name}">商品名</td> <td th:text="${g.price} + '元'">价格</td> <td><a th:href="@{/goods/edit(id=${g.id})}">编辑</a></td> </tr> </table> </div> </body> </html>

这种结构让 UI 修改和业务逻辑彻底解耦:改样式动static/css/,改页面结构动templates/,改数据逻辑动controller/mapper/service——三者互不影响。

2.3 数据库设计:init.sql 里藏着的 4 张表与 2 个设计权衡

resources/sql/init.sql文件虽小,但暴露了作者对小型超市业务的理解深度。它建了 4 张表:

-- 商品表:price 用 DECIMAL(10,2) 而非 FLOAT,避免 0.1+0.2=0.30000000000000004 CREATE TABLE goods ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category VARCHAR(50), created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 员工表:status 字段用 TINYINT(1) 代替 ENUM,兼容 MySQL 5.7 且方便 JDBC 映射 CREATE TABLE employees ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT(1) DEFAULT 1 -- 1=启用, 0=禁用 ); -- 进货单主表:关联员工,记录谁在什么时候进了多少货 CREATE TABLE purchase_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, employee_id BIGINT NOT NULL, order_date DATE NOT NULL, total_amount DECIMAL(12,2) DEFAULT 0.00 ); -- 进货单明细表:一对多,解决“一单进多种商品”问题 CREATE TABLE purchase_order_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(10,2) NOT NULL );

两个关键设计权衡点:

  1. 没做库存扣减事务:purchase_order_item插入后,并不自动更新goods.stock。这是故意为之——作者在PurchaseOrderService.java里留了 TODO 注释:“TODO: 库存同步需加事务控制,当前为简化演示”。这意味着你如果直接改数据库,库存数可能和实际不符,但代码层面没锁表,适合教学场景快速验证流程,不适合高并发入库。
  2. 员工状态用 TINYINT 而非布尔:Java 里映射为Integer,避免某些 MySQL 驱动把TINYINT(1)当 Boolean 导致true/false误判(尤其在旧版驱动中)。这是血泪经验:某次升级 MySQL 驱动后,所有禁用员工突然变启用,查了 3 小时才发现是类型映射 bug。

3. 启动与调试全流程:从双击 jar 到远程断点的 7 步实操

3.1 本地一键启动:H2 内存库模式的 3 个隐藏开关

项目默认使用 H2 内存数据库,无需安装 MySQL 即可运行。启动只需一步:

java -jar supermarket-system-1.0.jar

但必须确保application.yml中这 3 项配置处于激活状态:

spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true # 关键!开启 H2 控制台 path: /h2-console # 访问 http://localhost:8080/h2-console 即可查库 thymeleaf: cache: false # 关键!关闭模板缓存,改 HTML 不用重启

注意:DB_CLOSE_DELAY=-1表示 JVM 进程不退出时 H2 不关闭连接;DB_CLOSE_ON_EXIT=FALSE防止应用关闭时清空内存库——否则每次重启商品数据全丢。这两个参数是 H2 内存库能“持久化”到本次运行周期的关键。

访问http://localhost:8080/h2-console后,填入:

  • JDBC URL:jdbc:h2:mem:testdb
  • User Name:sa
  • Password: 空

点 Connect,就能看到GOODS,EMPLOYEES等表,甚至执行UPDATE GOODS SET PRICE = 9.9 SET NAME = '矿泉水' WHERE ID = 1实时改价——这是比改 Java 代码更快的调试方式。

3.2 切换 MySQL 真实库:5 行配置 + 1 个 SQL 文件的最小改动

要换成 MySQL,只需 5 步:

  1. 在 MySQL 中新建数据库:CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4;
  2. 修改application.yml:
spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_mysql_password # 替换为你的真实密码 driver-class-name: com.mysql.cj.jdbc.Driver # 删除或注释掉 h2.console 配置 # thymeleaf.cache: true # 生产环境建议开启
  1. 删除pom.xml中 H2 依赖(保留 MySQL 依赖):

    <!-- <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> -->
  2. 把resources/sql/init.sql中的 H2 语法转为 MySQL:

    • AUTO_INCREMENT保持不变(MySQL 原生支持)
    • TIMESTAMP DEFAULT CURRENT_TIMESTAMP改为TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(MySQL 5.7+ 兼容)
    • 执行该 SQL 到 MySQL 数据库中
  3. 重启应用:java -jar supermarket-system-1.0.jar,此时所有数据将持久化到 MySQL。

提示:serverTimezone=Asia/Shanghai是必加参数,否则 MySQL 8.0+ 会报The server time zone value 'XXX' is unrecognized错误——这是国内开发者踩坑率最高的 JDBC 连接参数。

3.3 远程调试实战:IDEA 里 3 分钟配好 5005 端口断点

项目pom.xml已预埋远程调试参数,但需手动触发:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments> -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 </jvmArguments> </configuration> </plugin>

在 IDEA 中配置 Remote JVM Debug:

  1. 点击右上角Add Configuration→+→Remote JVM Debug
  2. 填写:
    • Host:localhost
    • Port:5005
    • Module classpath: 选择本项目模块
  3. 启动 jar 时加-agentlib参数(或直接用 Maven 命令):
    mvn spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
  4. 在GoodsController.java的list()方法第一行打断点
  5. 点击 Debug 按钮,访问http://localhost:8080/goods/list,IDEA 自动停在断点

此时你能看到Model model对象里goodsList 的真实 size,甚至右键goods.get(0)→Evaluate Expression查看商品详情——这才是调试的正确姿势,比 System.out.println() 快 10 倍。


4. 核心模块代码逐行解析:从商品列表到库存扣减的完整链路

4.1 GoodsController:RESTful 风格路由与 Model 传递的 4 个关键点

com.example.supermarket.controller.GoodsController.java是流量入口,其设计体现 SpringBoot 最佳实践:

@Controller @RequestMapping("/goods") public class GoodsController { @Autowired private GoodsService goodsService; // 1. GET /goods/list → 返回商品列表页(Thymeleaf 渲染) @GetMapping("/list") public String list(Model model) { List<Goods> goodsList = goodsService.findAll(); // 调用 service 层 model.addAttribute("goods", goodsList); // 绑定到模板变量 return "goods/list"; // 返回模板名(不带 .html) } // 2. GET /goods/edit?id=1 → 返回编辑页,查单条商品 @GetMapping("/edit") public String edit(@RequestParam Long id, Model model) { Goods goods = goodsService.findById(id); model.addAttribute("goods", goods); return "goods/edit"; } // 3. POST /goods/save → 处理表单提交,@Valid 启用校验 @PostMapping("/save") public String save(@Valid @ModelAttribute Goods goods, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { return "goods/edit"; // 校验失败返回编辑页,错误信息自动注入 } goodsService.save(goods); redirectAttributes.addFlashAttribute("message", "保存成功"); return "redirect:/goods/list"; // 重定向防重复提交 } // 4. POST /goods/delete?id=1 → 删除商品,返回 JSON(前后端分离预备) @PostMapping("/delete") @ResponseBody public Map<String, Object> delete(@RequestParam Long id) { goodsService.delete(id); Map<String, Object> response = new HashMap<>(); response.put("success", true); return response; } }

关键点解析:

  • @ModelAttribute的妙用:save()方法参数Goods goods自动绑定表单字段(name, price, stock),无需@RequestParam逐个写——前提是表单 input 的name属性与 Goods 类字段名一致(如<input name="name">→goods.setName())。
  • BindingResult必须紧跟@Valid参数之后:Spring 规定,校验结果对象必须紧挨被校验对象,否则result.hasErrors()永远为 false。这是新手写校验时 90% 的失败原因。
  • RedirectAttributes.addFlashAttribute():让message在重定向后仍能被下个请求读取(Thymeleaf 中用${message}取出),这是实现“操作成功提示”的标准方案。
  • @ResponseBody与@Controller共存:同一 Controller 既服务页面(Thymeleaf),又提供 API(JSON),为后续接入 Vue/React 预留接口——不需要新建@RestController。

4.2 GoodsMapper 与 MyBatis XML:动态 SQL 与 resultMap 的精准控制

MyBatis 配置分散在两处:接口GoodsMapper.java和 XMLmapper/GoodsMapper.xml。这种分离让 SQL 与 Java 逻辑解耦:

GoodsMapper.java:

@Mapper public interface GoodsMapper { List<Goods> findAll(); Goods findById(@Param("id") Long id); void insert(Goods goods); void update(Goods goods); void delete(@Param("id") Long id); }

对应mapper/GoodsMapper.xml:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.supermarket.mapper.GoodsMapper"> <!-- 1. resultMap 定义字段映射,解决数据库下划线转驼峰 --> <resultMap id="GoodsResultMap" type="com.example.supermarket.entity.Goods"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="price" column="price"/> <result property="stock" column="stock"/> <result property="category" column="category"/> <result property="createdTime" column="created_time"/> <!-- 数据库字段 created_time → Java 字段 createdTime --> </resultMap> <!-- 2. findAll 使用 resultMap,避免手写 AS 别名 --> <select id="findAll" resultMap="GoodsResultMap"> SELECT * FROM goods ORDER BY created_time DESC </select> <!-- 3. 动态 SQL:update 语句只更新非 null 字段 --> <update id="update" parameterType="com.example.supermarket.entity.Goods"> UPDATE goods <set> <if test="name != null and name != ''">name = #{name},</if> <if test="price != null">price = #{price},</if> <if test="stock != null">stock = #{stock},</if> <if test="category != null and category != ''">category = #{category},</if> </set> WHERE id = #{id} </update> </mapper>

提示:<set>标签会自动删除末尾逗号,<if>标签实现“有值才更新”,避免把空字符串写进数据库。这是 MyBatis 动态 SQL 的核心价值——比手拼 SQL 字符串安全 10 倍。

4.3 GoodsService:事务边界与库存扣减的伪原子性实现

GoodsService.java是业务逻辑中枢,其@Transactional注解定义了事务范围:

@Service @Transactional public class GoodsService { @Autowired private GoodsMapper goodsMapper; @Autowired private PurchaseOrderItemMapper orderItemMapper; public List<Goods> findAll() { return goodsMapper.findAll(); } public Goods findById(Long id) { return goodsMapper.findById(id); } public void save(Goods goods) { if (goods.getId() == null) { goodsMapper.insert(goods); } else { goodsMapper.update(goods); } } // 伪原子性扣减:先查库存,再判断,再更新 public boolean reduceStock(Long goodsId, Integer quantity) { Goods goods = goodsMapper.findById(goodsId); if (goods == null || goods.getStock() < quantity) { return false; // 库存不足 } goods.setStock(goods.getStock() - quantity); goodsMapper.update(goods); return true; } }

这里有个重要事实:reduceStock()方法没有加@Transactional(它继承了类级别的注解),但它的“查-判-改”三步并非真正原子——在高并发下,两个线程同时查到stock=10,都判断10>=5成立,然后都执行stock=5,最终库存变成5而非0。这是典型的“超卖”漏洞。

修复方案(不改架构的前提下):

// 在 GoodsMapper.xml 中加一条乐观锁更新 <update id="reduceStockWithLock" parameterType="map"> UPDATE goods SET stock = stock - #{quantity} WHERE id = #{goodsId} AND stock >= #{quantity} </update>

然后在 Service 中调用:

public boolean reduceStock(Long goodsId, Integer quantity) { int updated = goodsMapper.reduceStockWithLock(goodsId, quantity); return updated > 0; // updated=1 表示扣减成功,0 表示库存不足 }

这种基于 WHERE 条件的更新,由数据库保证原子性,是中小型系统最经济的库存控制方案。


5. 避坑指南:上线前必须验证的 4 类典型故障

5.1 现象:启动时报Failed to configure a DataSource

原因:application.yml中spring.datasource配置缺失或格式错误,例如:

  • 忘了缩进(YAML 对空格敏感):url:前多了空格
  • driver-class-name写成driverClassName(Spring Boot 2.4+ 强制要求带连字符)
  • H2 的 JDBC URL 写成jdbc:h2:mem:testdb但没加DB_CLOSE_DELAY=-1,导致连接被提前关闭

解决:

  1. 用 YAML 校验网站(如 https://yamlchecker.com/)粘贴配置检查语法
  2. 确保spring.datasource.url以jdbc:开头,且驱动类名与依赖版本匹配(H2 用org.h2.Driver,MySQL 8.0+ 用com.mysql.cj.jdbc.Driver)
  3. 在application.yml顶部加spring.profiles.active: dev,并在application-dev.yml中单独写数据库配置,避免主配置混乱

5.2 现象:Thymeleaf 页面显示${goods.name}而非真实值

原因:Thymeleaf 模板未被正确解析,常见于:

  • templates/目录不在src/main/resources/下(应为src/main/resources/templates/)
  • application.yml中spring.thymeleaf.enabled设为false(默认 true,但有人会手动关)
  • Controller 返回"goods/list"时,实际文件是list.html但放在templates/goods/外层

解决:

  1. 检查项目结构是否符合 SpringBoot 默认约定(IDEA 中src/main/resources必须标为 Resources Root)
  2. 在application.yml中显式开启:
    spring: thymeleaf: enabled: true cache: false
  3. 在 Controller 中临时加日志:System.out.println("goods size: " + goodsList.size());确认数据已查出,排除 service 层问题

5.3 现象:MySQL 中文乱码,商品名显示???

原因:MySQL 服务端、数据库、表、连接四层编码不统一。即使建库时指定了utf8mb4,JDBC 连接参数也可能覆盖它。

解决:

  1. 登录 MySQL 执行:
    SHOW VARIABLES LIKE 'character_set%'; SHOW VARIABLES LIKE 'collation%';
    确保character_set_server和collation_server为utf8mb4
  2. 修改application.yml的 JDBC URL,强制指定编码:
    spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
  3. 对已有表执行:
    ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

5.4 现象:远程调试连不上 5005 端口,提示Connection refused

原因:

  • pom.xml中spring-boot-maven-plugin的jvmArguments未生效(Maven 版本过低或插件配置错位)
  • 防火墙拦截了 5005 端口(Windows Defender 或公司网络策略)
  • IDEA 的 Remote JVM Debug 配置中 Host 写成了127.0.0.1,但 jar 是用address=*:5005启动的(*表示监听所有网卡,127.0.0.1只监听本地回环)

解决:

  1. 改用命令行启动并确认参数生效:
    java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar supermarket-system-1.0.jar
  2. Windows 下临时关闭防火墙,或添加入站规则放行 TCP 5005
  3. IDEA 中 Remote JVM Debug 的 Host 改为localhost(与address=*:5005兼容)

6. 二次开发实战:给商品列表页加 Excel 导出功能的 3 种实现路径

6.1 路径一:Apache POI —— 零依赖、纯 Java、适合小数据量

这是最轻量的方案,不引入新依赖,直接用 JDK 写.xls(Excel 97-2003 格式):

  1. 在GoodsController.java中新增导出方法:

    @GetMapping("/export") public void export(HttpServletResponse response) throws IOException { List<Goods> goodsList = goodsService.findAll(); // 创建工作簿 HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("商品列表"); // 表头 HSSFRow headerRow = sheet.createRow(0); String[] headers = {"ID", "名称", "价格", "库存", "分类", "创建时间"}; for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowNum = 1; for (Goods g : goodsList) { HSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(g.getId()); row.createCell(1).setCellValue(g.getName()); row.createCell(2).setCellValue(g.getPrice().doubleValue()); row.createCell(3).setCellValue(g.getStock()); row.createCell(4).setCellValue(g.getCategory() != null ? g.getCategory() : ""); row.createCell(5).setCellValue(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(g.getCreatedTime())); } // 设置响应头 response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment; filename=goods_export.xls"); workbook.write(response.getOutputStream()); }
  2. 在goods/list.html中加导出按钮:

    <a href="/goods/export" class="btn btn-primary">导出 Excel</a>

注意:POI 3.17 对应HSSFWorkbook(.xls),若要支持.xlsx(Excel 2007+),需升级 POI 到 4.x 并用XSSFWorkbook,但会增大 jar 包体积。对于超市系统,.xls完全够用,且兼容性更好。

6.2 路径二:EasyExcel —— 自动映射、支持大文件、一行代码搞定

如果数据量超过 1 万行,POI 内存占用会飙升。EasyExcel 是阿里巴巴开源的流式 Excel 处理库,内存友好:

  1. 加依赖(替换pom.xml中的 POI):

    <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency>
  2. 新建 Excel 实体类GoodsExportData.java:

    @Data public class GoodsExportData { @ExcelProperty("商品ID") private Long id; @ExcelProperty("商品名称") private String name; @ExcelProperty("价格(元)") private BigDecimal price; @ExcelProperty("库存数量") private Integer stock; @ExcelProperty("商品分类") private String category; @ExcelProperty("创建时间") @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private Date createdTime; }
  3. Controller 中改写导出方法:

    @GetMapping("/export-easy") public void exportEasy(HttpServletResponse response) throws IOException { List<Goods> goodsList = goodsService.findAll(); List<GoodsExportData> exportList = goodsList.stream() .map(g -> { GoodsExportData data = new GoodsExportData(); data.setId(g.getId()); data.setName(g.getName()); data.setPrice(g.getPrice()); data.setStock(g.getStock()); data.setCategory(g.getCategory()); data.setCreatedTime(g.getCreatedTime()); return data; }) .collect(Collectors.toList()); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("商品导出", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), GoodsExportData.class) .sheet("商品列表") .doWrite(exportList); }

6.3 路径三:前端 SheetJS —— 浏览器内生成,不走后端,零服务器压力

如果只是导出当前页数据(非全部),用前端 JS 更高效:

  1. 在templates/layout.html的<head>中引入 SheetJS:

    <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  2. 在goods/list.html底部加 JS:

    <script> function exportToExcel() { // 构造二维数组:第一行是表头,后面是数据 const data = [['ID', '名称', '价格', '库存', '分类', '创建时间']]; document.querySelectorAll('#goods-table tbody tr').forEach(row => { const rowData = []; row.querySelectorAll('td:not(:last-child), th:not(:last-child)').forEach(cell => { rowData.push(cell.textContent.trim()); }); data.push(rowData); }); // 创建工作簿并导出 const ws = XLSX.utils.aoa_to_sheet(data); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "商品列表"); XLSX.writeFile(wb, "goods_current_page.xlsx"); } </script> <button onclick="exportToExcel()" class="btn btn-success">导出当前页</button>

三种路径对比:

方案适用场景优点缺点
Apache POI数据量 < 5000 行,无额外依赖需求纯 Java,学习成本低内存占用高,不支持大文件
EasyExcel数据量 > 1 万行,需导出全量内存友好,自动映射,社区活跃需引入新依赖
SheetJS只导出当前页,服务器零压力前端完成,响应极快无法导出分页后的全部数据

从那以后我每次加导出功能,都强制走一遍这三步验证:先用 POI 快速验证流程,再用 EasyExcel 压测大数据,最后用 SheetJS 给用户加个“当前页快捷导出”按钮——三个方案不是替代关系,而是互补组合。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 5:53:50

智能体技术演进与多智能体系统开发实践

1. Agent技术演进全景图智能体技术从诞生至今已经历了三个主要发展阶段&#xff0c;每个阶段都对应着不同的技术特征和应用场景&#xff1a;第一阶段&#xff08;1990-2010&#xff09;&#xff1a;基于规则的简单智能体典型代表&#xff1a;Eliza聊天机器人、工业控制系统核心…

作者头像 李华
网站建设 2026/10/30 5:53:38

Linux动态加载技术:dlopen函数详解与应用实践

1. 动态加载技术概述在Linux/Unix系统编程中&#xff0c;动态加载共享库&#xff08;.so文件&#xff09;是一项基础但极其重要的技术。通过动态加载机制&#xff0c;我们可以在程序运行时按需加载功能模块&#xff0c;而不是在编译时将所有依赖静态链接到可执行文件中。这种技…

作者头像 李华
网站建设 2026/10/30 5:53:37

C++静态多态与CRTP模式:高性能编程实践

1. 静态多态的本质与价值在C编程中&#xff0c;多态性是我们经常使用的重要特性。传统上&#xff0c;我们通过虚函数(virtual function)实现运行时多态&#xff0c;这种机制虽然灵活&#xff0c;但也带来了运行时开销。而静态多态(Static Polymorphism)则是一种在编译期就确定的…

作者头像 李华
网站建设 2026/10/30 5:53:15

OpenShell:专为Windows资源管理器深度优化的图形外壳替代方案

1. OpenShell 是什么&#xff1f;它不是 Shell&#xff0c;也不是“开源 Shell”的缩写OpenShell 这个名字在当前技术社区里特别容易引发误解——很多人第一反应是“哦&#xff0c;又一个 Linux 终端替代品&#xff1f;”或者“是不是类似 Oh My Zsh 那种 Shell 配置框架&#…

作者头像 李华
网站建设 2026/10/30 5:53:12

Claude Skills高效处理多表合并与数据清洗实战

1. 为什么选择Claude Skills处理多表合并与数据清洗数据清洗是数据分析过程中最耗时但又最关键的环节。根据IBM的研究数据&#xff0c;数据科学家平均要花费60%的时间在数据清洗和准备上。传统方式要么依赖Excel手动操作&#xff08;容易出错且效率低下&#xff09;&#xff0c…

作者头像 李华
网站建设 2026/10/30 5:51:21

YOLOv8卷积层改进:FDConv频域动态卷积技术解析

1. YOLOv8卷积层改进背景与FDConv核心价值目标检测领域近年来在速度和精度平衡上取得显著进展&#xff0c;YOLOv8作为当前工业界应用最广泛的实时检测框架&#xff0c;其核心优势在于高效的网络架构设计。但在复杂场景下&#xff0c;传统卷积操作对多尺度特征的表征能力仍存在局…

作者头像 李华