ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java图书销售系统实战:Spring Boot业务骨架搭建与事务实现

Java图书销售系统实战:Spring Boot业务骨架搭建与事务实现 简介这是一套面向Java初学者与课程设计实践者的《图书销售管理系统》完整源码项目聚焦Web应用开发实战帮助学习者将Java、MySQL及MVC架构等基础知识落地为可运行的电商类系统。资源包共364个文件涵盖45个JSP页面实现前端交互、45个Java类文件含BookBean、UserLoginBean等核心业务逻辑、29个Class字节码、138个GIF/JPG图片界面资源、21个JS与14个CSS文件前端动态效果与样式以及SQL数据库脚本和配置文件整体压缩包仅7.69MB轻量易部署。已有2019人学习下载项目采用动态代理与MVC分层设计结构清晰、模块职责明确包含用户登录、图书管理、订单处理等典型功能模块配套资源齐全可直接导入IDE运行调试是巩固Java Web开发能力、理解前后端协作流程的优质练手案例。1. 这不是“下载即用”的图书系统而是一套需要你亲手搭起的 Java 业务骨架很多人搜“java图书销售管理系统源码下载”以为点开链接解压就能跑通一个带登录、库存、订单的完整商城——结果发现数据库脚本缺失、Maven 依赖报红、Spring Boot 版本与本地 JDK 不兼容、前端页面空白、甚至pom.xml里还写着!-- TODO: 配置数据源 --。这不是源码质量差而是这类项目本质是教学型业务骨架它不追求高并发或微服务拆分但必须清晰暴露 Java Web 开发中每个关键环节的衔接逻辑——从 JDBC 封装到事务边界从 Servlet 生命周期到 MVC 分层职责从 JSP/Thymeleaf 模板渲染到 RESTful 接口设计。适合刚学完 JDBC 和 Servlet 的开发者通过修改真实字段如book.isbn→book.isbn13、重写 DAO 层实现从 MyBatis 切换到 JdbcTemplate、或补全库存扣减的乐观锁逻辑把“能运行”变成“真理解”。如果你正卡在“写了 CRUD 却不知道 Controller 该返回什么、Service 层要不要抛异常、为什么事务不生效”这个系统就是你缺的那块拼图。2. 用 Maven Spring Boot 搭建图书销售系统的最小可运行结构2.1 为什么选 Spring Boot 而非传统 SSM——降低环境耦合聚焦业务逻辑传统 SSMSpring SpringMVC MyBatis需手动配置web.xml、spring-context.xml、spring-mvc.xml、mybatis-config.xml四个核心 XML 文件光是DispatcherServlet的 URL 映射和ContextLoaderListener的加载顺序就容易出错。而 Spring Boot 通过SpringBootApplication自动装配内嵌 Tomcat、自动扫描组件、自动配置数据源只要application.properties里有spring.datasource.*。对图书销售系统这类单体应用Boot 的优势在于你能用 3 行代码启动一个带 HikariCP 连接池的 Web 服务而不是花 2 小时调试ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet。实际项目中我见过太多人因spring-webmvc版本与spring-core不匹配导致Controller注解失效——Boot 的spring-boot-starter-web已做版本仲裁直接规避这类问题。提示不要盲目追求最新 Boot 版本。当前主流教学项目适配 JDK 8/11推荐使用 Spring Boot 2.7.x对应 Spring 5.3.x避免 Spring Boot 3.x 强制要求 JDK 17 导致javax.*包报错。2.2 创建最小可运行工程的 5 个必要步骤2.2.1 初始化项目结构命令行方式# 使用 Spring Initializr CLI 工具需提前安装 spring init \ --dependenciesweb,jdbc,mysql,thymeleaf,lombok \ --buildmaven \ --java-version11 \ --package-namecom.example.bookstore \ bookstore-demo此命令生成标准 Maven 结构关键目录含义src/main/java/com/example/bookstore/: 主程序包含BookstoreApplication.javasrc/main/resources/application.properties: 核心配置文件src/main/resources/static/: 存放 CSS/JS/图片等静态资源src/main/resources/templates/: Thymeleaf 模板存放目录替代 JSP2.2.2 配置 MySQL 数据源application.properties# 数据库连接 spring.datasource.urljdbc:mysql://localhost:3306/bookstore_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver # 连接池配置HikariCP spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle5 spring.datasource.hikari.idle-timeout30000 spring.datasource.hikari.max-lifetime1800000 # JPA/Hibernate可选若用 MyBatis 则跳过 # spring.jpa.hibernate.ddl-autoupdate # spring.jpa.show-sqltrue # spring.jpa.properties.hibernate.format_sqltrue参数说明useSSLfalse开发环境关闭 SSL生产环境必须启用serverTimezoneAsia/Shanghai强制指定时区避免java.sql.SQLException: The server time zone value XXX is unrecognized错误allowPublicKeyRetrievaltrueMySQL 8.0 必须添加否则连接失败maximum-pool-size20图书销售系统并发量通常 10020 连接足够支撑日常操作2.2.3 定义核心实体类Lombok 简化代码// src/main/java/com/example/bookstore/entity/Book.java import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; Data NoArgsConstructor AllArgsConstructor Entity Table(name t_book) public class Book { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name isbn, unique true, nullable false, length 13) private String isbn; Column(name title, nullable false, length 100) private String title; Column(name author, length 50) private String author; Column(name price, precision 10, scale 2) private BigDecimal price; Column(name stock, nullable false, columnDefinition INT DEFAULT 0) private Integer stock; Column(name created_time, updatable false) private LocalDateTime createdTime; PrePersist void onCreate() { this.createdTime LocalDateTime.now(); } }关键点Data自动生成 getter/setter/toString/equals/hashCodeColumn(length 13)明确 ISBN 字段长度避免 MySQL 默认 VARCHAR(255) 浪费空间columnDefinition INT DEFAULT 0在建表时直接定义默认值比在 Java 层赋值更可靠PrePersist确保创建时间由数据库写入防止客户端伪造时间2.2.4 编写基础 Repository 接口JPA 方式// src/main/java/com/example/bookstore/repository/BookRepository.java import com.example.bookstore.entity.Book; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; Repository public interface BookRepository extends JpaRepositoryBook, Long { // 自定义查询方法按书名模糊搜索 ListBook findByTitleContaining(String keyword); // 按 ISBN 精确查询用于库存扣减前校验 Book findByIsbn(String isbn); // 按库存数量范围查询用于后台筛选缺货/积压图书 ListBook findByStockBetween(Integer min, Integer max); }为什么用 JPA 而非原生 JDBCfindByTitleContaining方法名会被 JPA 自动解析为SELECT * FROM t_book WHERE title LIKE %?%无需手写 SQLJpaRepository已内置save()、findById()、deleteById()等通用方法减少模板代码事务管理更简单Transactional直接标注在 Service 方法上即可控制整个方法原子性2.2.5 启动并验证基础服务// src/main/java/com/example/bookstore/BookstoreApplication.java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class BookstoreApplication { public static void main(String[] args) { SpringApplication.run(BookstoreApplication.class, args); } }启动后访问http://localhost:8080/actuator/health需添加spring-boot-starter-actuator依赖返回{status:UP}表示服务已就绪。此时系统虽无页面但数据库连接、实体映射、基础 CRUD 接口均已激活——这是后续所有功能的基石。3. 实现图书销售核心流程从库存扣减到订单生成的事务闭环3.1 图书销售的三个关键状态流转及事务边界图书销售不是简单的“减库存增订单”而是涉及库存校验 → 扣减库存 → 创建订单 → 订单支付确认的状态机。其中最易出错的是库存扣减若用户 A 查询到库存 10用户 B 同时查询也是 10两人同时下单就会超卖。解决方案必须满足 ACIDA原子性扣库存和建订单必须在一个事务内完成C一致性库存不能为负数订单状态必须与库存变更同步I隔离性并发下单时第二个请求必须等待第一个事务释放锁D持久性事务提交后数据写入磁盘不可丢失常见错误做法是先SELECT stock FROM t_book WHERE id ?再UPDATE t_book SET stock stock - 1 WHERE id ? AND stock 1—— 这看似安全但若两个请求同时执行SELECT都得到 stock1接着都执行UPDATE第二个UPDATE会因stock 1条件失败但此时订单已创建造成数据不一致。3.2 基于乐观锁的库存扣减实现推荐新手掌握3.2.1 在 Book 实体中添加版本字段// 修改 Book.java增加 Version 注解 Column(name version) Version private Integer version;对应数据库表需新增versionINT 列默认值为 0。JPA 在每次更新时自动将WHERE version ?加入 SQL并将 version 值 1。3.2.2 Service 层实现带重试的扣减逻辑// src/main/java/com/example/bookstore/service/BookService.java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.concurrent.TimeUnit; Service public class BookService { Autowired private BookRepository bookRepository; Transactional public boolean deductStock(Long bookId, Integer quantity) { int maxRetry 3; for (int i 0; i maxRetry; i) { try { Book book bookRepository.findById(bookId) .orElseThrow(() - new RuntimeException(图书不存在)); if (book.getStock() quantity) { throw new RuntimeException(库存不足当前库存 book.getStock()); } // 直接更新库存和版本号JPA 自动处理 WHERE version ? book.setStock(book.getStock() - quantity); bookRepository.save(book); return true; } catch (OptimisticLockingFailureException e) { // 版本冲突说明其他线程已更新等待 10ms 后重试 if (i maxRetry - 1) throw e; try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException ignored) {} } } return false; } }参数说明maxRetry 3重试次数不宜过多避免线程长时间阻塞TimeUnit.MILLISECONDS.sleep(10)短暂休眠让 CPU 释放避免自旋浪费资源OptimisticLockingFailureExceptionJPA 在 version 不匹配时抛出此异常是乐观锁的标准信号注意乐观锁适用于冲突概率低的场景如图书销售单日下单量远低于库存量。若业务是秒杀场景冲突率 30%需改用悲观锁SELECT ... FOR UPDATE或 Redis 原子操作。3.3 订单生成与状态机管理3.3.1 定义订单实体及状态枚举// src/main/java/com/example/bookstore/entity/Order.java import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; Data NoArgsConstructor AllArgsConstructor Entity Table(name t_order) public class Order { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name order_no, unique true, nullable false, length 32) private String orderNo; Column(name user_id, nullable false) private Long userId; Column(name total_amount, precision 10, scale 2) private BigDecimal totalAmount; Column(name status, nullable false, columnDefinition TINYINT DEFAULT 1) Enumerated(EnumType.ORDINAL) private OrderStatus status; Column(name created_time, updatable false) private LocalDateTime createdTime; PrePersist void onCreate() { this.createdTime LocalDateTime.now(); } } // src/main/java/com/example/bookstore/enums/OrderStatus.java public enum OrderStatus { CREATED(1), // 已创建待支付 PAID(2), // 已支付待发货 SHIPPED(3), // 已发货 COMPLETED(4), // 已完成 CANCELLED(5); // 已取消 private final int value; OrderStatus(int value) { this.value value; } public int getValue() { return value; } }3.3.2 创建订单的 Service 方法事务内联动库存// src/main/java/com/example/bookstore/service/OrderService.java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.UUID; Service public class OrderService { Autowired private BookService bookService; Autowired private OrderRepository orderRepository; Transactional public Order createOrder(Long userId, Long bookId, Integer quantity) { // 步骤1扣减库存已在事务内失败则回滚 if (!bookService.deductStock(bookId, quantity)) { throw new RuntimeException(库存扣减失败); } // 步骤2查询图书价格计算订单金额 Book book bookRepository.findById(bookId) .orElseThrow(() - new RuntimeException(图书不存在)); BigDecimal amount book.getPrice().multiply(BigDecimal.valueOf(quantity)); // 步骤3创建订单 Order order new Order(); order.setOrderNo(ORD System.currentTimeMillis() UUID.randomUUID().toString().substring(0, 8)); order.setUserId(userId); order.setTotalAmount(amount); order.setStatus(OrderStatus.CREATED); order.setCreatedTime(LocalDateTime.now()); return orderRepository.save(order); } }关键设计点Transactional标注在createOrder方法上确保扣库存和建订单在同一事务orderNo使用时间戳UUID 组合避免分布式环境下订单号重复OrderStatus用Enumerated(EnumType.ORDINAL)存储为数字1,2,3...比存字符串更节省空间且便于数据库索引4. 前端交互与安全加固Thymeleaf 模板 表单校验 CSRF 防护4.1 用 Thymeleaf 渲染图书列表页避免 XSS 漏洞4.1.1 Controller 返回模型数据// src/main/java/com/example/bookstore/controller/BookController.java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; Controller public class BookController { Autowired private BookRepository bookRepository; GetMapping(/books) public String listBooks(RequestParam(required false) String keyword, Model model) { ListBook books; if (keyword ! null !keyword.trim().isEmpty()) { books bookRepository.findByTitleContaining(keyword.trim()); } else { books bookRepository.findAll(); } model.addAttribute(books, books); model.addAttribute(keyword, keyword ! null ? keyword : ); return book/list; // 对应 templates/book/list.html } }4.1.2 Thymeleaf 模板安全渲染防 XSS!-- src/main/resources/templates/book/list.html -- !DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title图书列表/title /head body h1图书销售系统/h1 !-- 搜索表单 -- form th:action{/books} methodget input typetext namekeyword th:value${keyword} placeholder请输入书名关键词 / button typesubmit搜索/button /form !-- 图书列表 -- table border1 tr thISBN/th th书名/th th作者/th th价格/th th库存/th th操作/th /tr tr th:eachbook : ${books} td th:text${book.isbn}ISBN/td td th:text${#strings.abbreviate(book.title, 20)}书名/td td th:text${book.author}作者/td td th:text${#numbers.formatDecimal(book.price, 1, 2)}¥0.00/td td th:text${book.stock}库存/td td a th:href{/books/order/{id}(id${book.id})}下单/a /td /tr /table /body /html安全要点th:text${book.title}自动 HTML 转义即使book.title包含scriptalert(1)/script也会显示为纯文本#strings.abbreviate()截断过长书名避免表格布局错乱#numbers.formatDecimal()格式化价格保证两位小数显示如19.5→19.504.2 表单提交的 CSRF 防护Spring Security 默认启用Spring Boot 2.7.x 默认集成 Spring Security对 POST/PUT/DELETE 请求强制校验 CSRF Token。若忽略此机制提交订单时会返回 403 Forbidden。4.2.1 在 Thymeleaf 表单中嵌入 CSRF Token!-- src/main/resources/templates/book/order.html -- form th:action{/orders} methodpost !-- 必须包含此隐藏字段 -- input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} / input typehidden namebookId th:value${book.id} / input typenumber namequantity min1 max10 value1 required / button typesubmit确认下单/button /form4.2.2 Controller 处理订单提交接收并校验// src/main/java/com/example/bookstore/controller/OrderController.java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; Controller public class OrderController { Autowired private OrderService orderService; GetMapping(/books/order/{bookId}) public String showOrderForm(PathVariable Long bookId, Model model) { model.addAttribute(bookId, bookId); return book/order; } PostMapping(/orders) public String createOrder( RequestParam Long bookId, RequestParam Integer quantity, RequestParam(defaultValue 1) Long userId, // 简化示例实际应从 Session 获取 Model model) { try { Order order orderService.createOrder(userId, bookId, quantity); model.addAttribute(order, order); return order/success; } catch (Exception e) { model.addAttribute(error, e.getMessage()); return book/order; } } }提示生产环境中userId必须从SecurityContext中获取而非前端传入。此处为演示简化实际需集成 Spring Security 的Authentication对象。5. 源码级调试技巧定位事务不生效、SQL 未执行、模板找不到的三类高频问题5.1 事务不生效的 4 个排查路径事务失效是图书销售系统最常遇到的问题表现为“扣了库存但没建订单”或“建了订单但库存没减”。按优先级检查检查项命令/操作说明① 查看方法是否被Transactional修饰grep -r Transactional src/main/java/确保createOrder()方法上有该注解且类被 Spring 扫描到Service② 检查调用是否为内部 self-invocation在OrderService.createOrder()中加System.out.println(Transaction active: TransactionSynchronizationManager.isActualTransactionActive());若输出false说明是本类内方法调用如this.createOrder()Spring AOP 代理失效③ 查看日志中是否有TransactionInterceptor日志启动时添加 JVM 参数-Dlogging.level.org.springframework.transactionDEBUG正常应看到Getting transaction for [com.example.bookstore.service.OrderService.createOrder]④ 检查异常是否被吞掉将catch(Exception e)改为catch(RuntimeException e)并确保抛出的异常是RuntimeException或其子类Spring 默认只对RuntimeException回滚Exception需显式声明Transactional(rollbackFor Exception.class)5.2 SQL 未执行的诊断方法MyBatis/JPA 通用当bookRepository.save(book)执行后数据库无变化按以下顺序验证5.2.1 开启 SQL 日志输出# application.properties # JPA 方式 spring.jpa.show-sqltrue spring.jpa.properties.hibernate.format_sqltrue logging.level.org.hibernate.SQLDEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinderTRACE # MyBatis 方式若替换为 MyBatis mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl logging.level.com.example.bookstore.mapperDEBUG启动后观察控制台若看到insert into t_book (...) values (?, ?, ?)但数据库无数据 → 检查事务是否提交见 5.1若完全看不到 SQL → 检查 Mapper 接口是否被Mapper注解或MapperScan扫描到5.2.2 验证数据库连接是否真实生效# 进入 MySQL 命令行执行 SHOW PROCESSLIST; # 查看是否有来自 localhost:8080 的连接 SELECT * FROM information_schema.PROCESSLIST WHERE HOST LIKE localhost%;若无连接说明spring.datasource.url配置错误如端口写成3307或 MySQL 未启动。5.3 Thymeleaf 模板找不到的根因分析访问/books返回 Whitelabel Error Page提示Template not found按此流程定位5.3.1 确认模板路径与返回值严格匹配Controller 返回book/list→ 框架查找templates/book/list.html若文件实际为templates/book/list.htm少一个 l→ 报错若文件在templates/book/list.html但 Controller 返回book/list.html→ 报错Thymeleaf 不识别扩展名5.3.2 检查 Maven 打包是否包含 templates 目录# 解压 target/bookstore-demo-0.0.1-SNAPSHOT.jar jar -xf target/bookstore-demo-0.0.1-SNAPSHOT.jar # 查看 BOOT-INF/classes/templates/book/list.html 是否存在 ls -l BOOT-INF/classes/templates/book/若不存在说明src/main/resources/templates/未被 Maven 资源插件识别。在pom.xml中添加build resources resource directorysrc/main/resources/directory includes include**/*.properties/include include**/*.xml/include include**/*.html/include !-- 关键显式包含 HTML -- /includes /resource /resources /build5.3.3 验证 Thymeleaf 配置是否启用# application.properties确保未被注释 spring.thymeleaf.enabledtrue spring.thymeleaf.prefixclasspath:/templates/ spring.thymeleaf.suffix.html spring.thymeleaf.modeHTML spring.thymeleaf.encodingUTF-8 spring.thymeleaf.cachefalse # 开发时关闭缓存若cachetrue且修改了 HTML 但页面不更新清空浏览器缓存并重启应用。本文还有配套的精品资源点击获取
返回列表