简介:基于Java Web的图书管理系统的设计与实现.docx是一份面向高校计算机相关专业学生及Java Web初学者的毕业设计/课程设计参考文档,针对学校图书管理中的读者信息、图书流通、借还登记等场景,给出完整解决方案。资源包仅1个docx文档,整体约2.34MB,包含从需求分析、总体设计、数据库设计到系统实现的完整论述,并详细介绍了系统设置、读者管理、图书管理、图书借还、系统查询与更改口令六大功能模块,以及MVC设计模式、Struts框架和SQL Server数据库的整合应用。文档还列出了图书信息表、读者信息表、借阅信息表等多个关键数据表结构,方便读者直接参考建表语句与功能流程。目前已有384人学习阅读,适合正在撰写相关论文或需要快速搭建图书管理系统原型的开发者使用。
1. 图书管理系统这个经典项目,为什么还值得认真做一遍
这几年我在带新人时发现一个现象:很多人觉得“基于Java Web的图书管理系统”是毕业设计里最老掉牙的题目,网上源码一抓一大把,不值得自己动手。但真让他们讲清楚“用户登录后借书,系统怎么保证库存不被超借;管理员改一本书的价格,事务边界在哪里”,多数人是说不明白的。这个标题背后不是一套CRUD,而是Java Web开发里最完整的一条主线:请求怎么路由到Servlet或Controller,业务层怎么处理事务,持久层怎么避免SQL注入,前端页面怎么跟后端数据绑定。把这套系统从头设计并实现一遍,等于把Java Web的骨架重新搭了一遍。这篇笔记适合两类人:一是准备做课程设计或毕业设计的学生,想拿出一套能演示、能答辩的系统;二是已经会写几个Servlet但没串过全流程的开发者,想看看一个完整项目里事务、分页、日志这些细节是怎么落地的。接下来我按自己做过的一个方案,从技术选型到避坑,完整走一遍。
2. 技术选型与分层设计:先定JDK、Servlet容器和数据库,再谈功能
2.1 SSM还是Servlet+JSP?先看你的目标和运行环境
图书管理系统最常见的实现组合有三种:纯Servlet + JSP + JDBC、SSM(Spring + Spring MVC + MyBatis)、Spring Boot + MyBatis。如果是课程设计,老师通常会指定“Java Web”,这个范围很宽,用纯Servlet也能交差,但我不建议。理由很简单:纯Servlet方案里事务、依赖注入、参数绑定全要手写,代码量会膨胀到几千行,而且很容易写出连接泄漏的隐患。SSM是过去十年Java Web课程设计里最主流的组合,它把Spring的IOC容器、Spring MVC的请求映射、MyBatis的SQL管理分开,每一部分都能在答辩时讲清楚“为什么这么选”。Spring Boot虽然更现代,但很多学校的Java Web课程还没更新到Boot,老师看到xml配置反而觉得更“符合课程预期”。
我的建议是:如果用Tomcat 8或9,JDK 1.8,就选SSM;如果环境允许并且你想省去大量xml配置,也可以选Spring Boot,但本文后续的配置思路同样适用,只是自动配置帮你去掉了手工步骤。我自己更倾向于SSM,因为遇到问题时你能看到完整的处理链,而不是被自动配置的黑匣子挡住。
2.2 分层架构:表现层、业务层、持久层各自管什么
图书管理系统的分层一定要严格,否则项目一大就会乱。常见的分层是三层:
- 表现层(Controller):接收HTTP请求,校验参数格式,调用业务层,返回页面或JSON。这里不写SQL,不写业务规则。
- 业务层(Service):承载核心逻辑,比如借书时检查库存、检查读者是否已借满、操作借书记录和更新库存。事务就加在这一层的方法上。
- 持久层(DAO/Mapper):只负责SQL执行和数据映射,不处理业务判断。
我见过很多翻车项目把业务逻辑写在Controller里,比如直接在Servlet里写了UPDATE book SET number = number - 1,然后跳转成功页。这会导致两个问题:一是复用性差,以后如果增加一个微信小程序端,还得复制一遍逻辑;二是事务很难控制,因为Servlet是单例多线程的,手动管理连接非常容易漏关。所以从最开始就要坚持:Controller只做转发和参数接收,Service处理一切规则,Mapper只做数据访问。
2.3 数据库表设计:book、user、borrow记录的三张核心表
图书管理系统最少需要三张表:图书表、用户表、借阅记录表。如果要做分类统计,再加一张分类表。我给出一个经过实践的表结构,字段名和类型可以直接用:
CREATE TABLE `book` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL COMMENT '书名', `author` varchar(64) DEFAULT NULL COMMENT '作者', `publisher` varchar(128) DEFAULT NULL COMMENT '出版社', `isbn` varchar(32) DEFAULT NULL COMMENT 'ISBN号', `total` int(11) NOT NULL DEFAULT '1' COMMENT '总藏书量', `available` int(11) NOT NULL DEFAULT '1' COMMENT '可借数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL, `password` varchar(128) NOT NULL COMMENT 'BCrypt加密后存储', `role` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0普通用户 1管理员', `max_borrow` int(11) NOT NULL DEFAULT '5' COMMENT '最大借书数', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `borrow_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `book_id` bigint(20) NOT NULL, `borrow_time` datetime NOT NULL, `return_time` datetime DEFAULT NULL, `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0借出 1已还', PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_book` (`book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个细节容易被忽略。一是available字段必须有,不能靠total减去记录数来算,因为还书时状态可能不一致,单独维护可借数量才能用UPDATE book SET available = available - 1 WHERE id = ? AND available > 0这样的原子操作防超借。二是密码字段要用BCrypt加密,不能用MD5。MD5在今天的算力下已经可以被彩虹表轻易碰撞,这个点也常常是答辩老师喜欢追问的地方。
3. 搭建项目骨架并用Maven管理依赖:从零跑通最小页面
3.1 用Maven创建war包项目:pom.xml关键配置
SSM项目的标准打包方式是war包,部署到Tomcat的webapps目录下。我用Maven创建项目后,先调整pom.xml。下面是一个精简且能直接用的配置,版本号都经过验证:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>book-manager</artifactId> <version>1.0.0</version> <packaging>war</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.2.22.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies> </project>注意这里有个容易踩的坑:pagehelper-spring-boot-starter虽然带了“spring-boot”字样,但它也能在普通SSM项目里使用,只要你在MyBatis配置里引入PageInterceptor即可。另外javax.servlet-api的scope必须是provided,因为Tomcat里已经有了Servlet容器类,如果设成compile,打出的war包会把servlet-api也带上,运行时会报类冲突。
3.2 配置web.xml与DispatcherServlet:Spring MVC的入口
SSM项目的web.xml负责启动Spring容器和Spring MVC的DispatcherServlet。我用一个示例说明:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>forceEncoding设为true,意味着不仅响应编码是UTF-8,请求体里的参数也会被强制按UTF-8解码。这一个配置如果不加,POST提交的中文大概率会乱码。而DispatcherServlet的url-pattern用/是为了让Spring MVC接管所有请求,静态资源(比如CSS、JS)需要额外在spring-mvc.xml里配置放行。
3.3 写一个最简单的图书列表页验证环境
环境搭完后,我先不写复杂业务,而是做一个最小验证:从数据库查出图书列表,在JSP页面上渲染出来。这样才能确认Spring MVC、MyBatis、JSP整条链路是通的。先定义Book实体和Mapper:
package com.example.book.entity; public class Book { private Long id; private String name; private String author; private String publisher; private String isbn; private Integer total; private Integer available; // getter/setter 省略 }package com.example.book.mapper; import com.example.book.entity.Book; import org.apache.ibatis.annotations.Param; import java.util.List; public interface BookMapper { List<Book> selectAll(); Book selectById(@Param("id") Long id); }对应的BookMapper.xml放在resources/mapper目录下:
<?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.book.mapper.BookMapper"> <select id="selectAll" resultType="com.example.book.entity.Book"> SELECT id, name, author, publisher, isbn, total, available FROM book ORDER BY id DESC </select> <select id="selectById" resultType="com.example.book.entity.Book"> SELECT id, name, author, publisher, isbn, total, available FROM book WHERE id = #{id} </select> </mapper>Controller很简单:
@Controller @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; @GetMapping("/list") public String list(Model model) { model.addAttribute("books", bookService.listAll()); return "book/list"; } }JSP里用JSTL遍历:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>图书列表</title> </head> <body> <table border="1"> <tr> <th>书名</th><th>作者</th><th>可借数</th> </tr> <c:forEach items="${books}" var="b"> <tr> <td>${b.name}</td> <td>${b.author}</td> <td>${b.available}</td> </tr> </c:forEach> </table> </body> </html>这里要说明的是,MyBatis里#{id}是预编译参数,会生成?占位符,能防SQL注入;而${id}是直接拼接字符串,除非你确认值是白名单内容,否则坚决不用。很多图书管理系统的注入漏洞就是有人把排序字段用${}拼进去导致的。
4. 实现图书增删改查与借还流程:核心代码与参数说明
4.1 图书实体与MyBatis映射:字段类型对应关系
上面的实体类已经建好了,但要注意Java类型与MySQL类型的映射细节。比如MySQL的tinyint映射到Java的Integer没问题,但datetime映射到java.util.Date时,如果字段为null,MyBatis能正常处理。有一个容易被忽视的坑是available字段,它是业务上的可借数量,但你如果用了UPDATE book SET available = available - 1这样的一句SQL,MyBatis里就只需要传入id,不需要传入当前数量。这在并发环境下是安全的,因为数据库的行锁会串行化这步操作。
图书的增删改查中,新增和修改的表单字段差不多,但更新时要避免把total和available搞混。比如管理员修改一本书的总量,如果原来的total是10,available是3,现在把total改成20,available是应该变成多少?合理的做法是差值增加:available = available + (#{newTotal} - #{oldTotal})。业务上最好单独提供一个“增加库存”的操作,而不是直接覆盖available。
4.2 实现分页查询:PageHelper的用法与pageNum/pageSize参数
图书列表超过几十条后必须分页,否则页面卡顿且不专业。我用的是PageHelper插件,它从MyBatis插件机制里拦截SQL,自动拼LIMIT。用法如下:
package com.example.book.service; import com.example.book.entity.Book; import com.example.book.mapper.BookMapper; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class BookService { @Autowired private BookMapper bookMapper; public PageInfo<Book> page(int pageNum, int pageSize) { // 这一行必须在查询之前调用,只对紧接着的下一条查询生效 PageHelper.startPage(pageNum, pageSize); List<Book> books = bookMapper.selectAll(); // PageInfo里包含总条数、总页数、当前页数据等 return new PageInfo<>(books); } }Controller里接收参数:
@GetMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Model model) { PageInfo<Book> pageInfo = bookService.page(pageNum, pageSize); model.addAttribute("pageInfo", pageInfo); return "book/list"; }这里有两个参数必须说清楚。pageNum从1开始,如果你在页面上传0,PageHelper会当作第1页处理,但建议前端统一从1开始。pageSize不要给太大,一般10到20即可;如果允许用户自定义到100,数据库压力会明显上升。另外,PageHelper有很著名的线程安全问题,后面避坑章节专门讲。
JSP里加一个简单的分页导航,用pageInfo.pageNum、pageInfo.pages这些属性。这一步能让你在答辩时展示“我考虑了数据量增长后的查询效率”。
4.3 借书/还书的业务逻辑:事务边界与状态校验
借书是整个系统最核心的逻辑,我先画一个业务规则:普通用户最多借5本,一本书当前可借数大于0才能借,同一个用户不能同时借走同一本书两本。对应Service代码:
package com.example.book.service; import com.example.book.entity.Book; import com.example.book.entity.BorrowRecord; import com.example.book.entity.User; import com.example.book.mapper.BookMapper; import com.example.book.mapper.BorrowRecordMapper; import com.example.book.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @Service public class BorrowService { @Autowired private BookMapper bookMapper; @Autowired private BorrowRecordMapper borrowRecordMapper; @Autowired private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public void borrowBook(Long userId, Long bookId) { // 1. 校验用户 User user = userMapper.selectById(userId); if (user == null) { throw new RuntimeException("用户不存在"); } int currentBorrowCount = borrowRecordMapper.countBorrowingByUser(userId); if (currentBorrowCount >= user.getMaxBorrow()) { throw new RuntimeException("已达到最大借书数量"); } // 2. 校验图书可借数量,原子更新 Book book = bookMapper.selectById(bookId); if (book == null) { throw new RuntimeException("图书不存在"); } int updated = bookMapper.decreaseAvailable(bookId); if (updated == 0) { throw new RuntimeException("图书已借完"); } // 3. 插入借阅记录 BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setStatus(0); borrowRecordMapper.insert(record); } @Transactional(rollbackFor = Exception.class) public void returnBook(Long recordId) { BorrowRecord record = borrowRecordMapper.selectById(recordId); if (record == null || record.getStatus() != 0) { throw new RuntimeException("借阅记录状态异常"); } record.setStatus(1); record.setReturnTime(new Date()); borrowRecordMapper.updateStatus(record); // 归还后恢复可借数量 bookMapper.increaseAvailable(record.getBookId()); } }Mapper里对应两个原子更新SQL:
<update id="decreaseAvailable"> UPDATE book SET available = available - 1 WHERE id = #{id} AND available > 0 </update> <update id="increaseAvailable"> UPDATE book SET available = available + 1 WHERE id = #{id} </update>这段代码有四个关键点。第一,decreaseAvailable里的AND available > 0是防止超借的防线,即使并发下两个请求同时读到available=1,数据库行锁也会让其中一个更新失败。第二,update返回int,等于0说明没有更新到行,所以可以断定“没借到”。第三,整个方法加上@Transactional(rollbackFor = Exception.class),注意rollbackFor必须指定,因为Spring默认只回滚RuntimeException,而如果Exception异常抛出来,事务不会回滚,会造成“库存扣了但记录没插入”的脏数据。第四,还书时先理清状态再增库存,这个顺序不能反,否则如果在更新还书记录后、增加库存前系统崩溃,会出现记录已还但库存没恢复的问题;把两步放在同一个事务里,崩溃也会一起回滚。
5. 避坑/常见问题排查:环境、编码、事务与SQL的四个典型翻车现场
5.1 现象:启动Tomcat后访问404,控制台没有错误,war包解压后只有空目录
这个坑在Maven新手身上特别常见。原因是IDEA或Eclipse里部署到Tomcat时,依赖的jar包没有输出到WEB-INF/lib。你看到Tomcat能启动,Spring的xml也没报错,是因为类加载还没有触发到需要jar包的环节。解决方法是检查项目的Artifacts(IDEA里叫这个)。在“Project Structure -> Artifacts”里选中你的war exploded,右侧“Available Elements”里把lib目录加入WEB-INF/lib。如果用的是mvn tomcat:run这种插件方式,则要确认pom.xml里没有把依赖范围设成provided(当然,servlet-api除外)。这个坑的排查技巧是:访问时看Tomcat日志的INFO级别,如果看到Deploying web application archive后直接跳成功,但目录里没有jar,就是这个问题。
5.2 现象:页面中文全部是问号,URL里手动传中文参数能显示,但表单POST之后乱码
问题出在编码过滤器没有覆盖POST请求。你很可能只配置了request.setCharacterEncoding("UTF-8"),但没放在过滤器里,或者过滤器映射的url-pattern写成了/而不是/*。/在Servlet映射里匹配的是路径里的最后一个斜杠,不匹配带参数的子路径;只有/*才能拦住所有请求。另一个隐蔽点是MySQL连接串必须加characterEncoding=utf8,否则即使Java侧是UTF-8,JDBC驱动和MySQL交互时也可能转成latin1再转回,中文照样乱。确保同时满足:CharacterEncodingFilter的forceEncoding=true、连接串上加characterEncoding=utf8、数据库表和字段都是utf8mb4。
5.3 现象:借书按钮点击后,前端提示成功,但数据库里借阅记录没有插入,再查库存也少了
这是典型的“事务没生效”症状。原因极可能是@Transactional被加在了Controller层,或者Service方法内部通过this调用另一个方法——比如在borrowBook()里调了一个本类的checkAndInsert(),Spring的AOP默认只在外部调用时创建代理,内部this调用绕过了代理,事务注解就被忽略了。解决:把事务注解加到Service类的public方法上,并且确保是通过Spring注入的Bean来调用,不能在类内部用this。如果采用了Spring Boot,可以用@Transactional的proxyTargetClass=true强制CGLIB代理,但最好还是遵守“类外部调用”这条规则。
5.4 现象:分页查询时第一页正常,第二页数据却和第1页重复,或者点击第3页后总页数变成0
PageHelper的坑在于它使用ThreadLocal保存分页参数,然后拦截下一次查询。如果你的Service方法里先执行了别的查询,比如在startPage()之后又去查了用户信息,那么分页参数就会作用到用户查询上,导致图书查询没有分页或者错误分页。此外,如果你用了PageHelper.startPage(pageNum, pageSize)但后面跟着的是一条update语句,分页参数不会生效,但也不会报错,容易让人误以为没问题。正确的做法是:startPage()紧贴着你要分页的那条select,中间不要穿插任何其他查询。如果用了MyBatis的@Select注解而非XML,同样受影响。
5.5 现象:系统运行一段时间后,页面偶尔报“连接超时”或“Too many connections”,重启Tomcat才恢复
根因是连接池没有配置最大连接数和超时时间。很多人用了默认的Druid或HikariCP配置,但忘记设置maxActive。在SSM环境,如果用Druid,标准配置是:
"initialSize": 5, "minIdle": 5, "maxActive": 20, "maxWait": 60000注意maxWait单位是毫秒,60000表示等待6秒拿不到连接就抛异常。如果应用并发不高但每次请求都慢,问题往往不是连接池,而是数据库连接没有释放——检查所有Mapper方法是否都通过Spring管理了事务,有没有人直接写了SqlSession手动开关。在SSM里,只要Service方法标了事务,连接会在方法结束后释放;但如果有人在代码里直接用@Autowired的SqlSessionTemplate并手动commit,就非常容易漏关。统一走Mapper接口,不要手动操作SqlSession。
6. 把这套系统从“能跑”做到“能演示”:数据初始化、日志与验收自测清单
6.1 用SQL脚本初始化演示数据:管理员账号与样例图书
演示系统最怕打开页面是空表,所以要准备一份data.sql。管理员账号的密码不能用明文,我用BCrypt生成一个固定值,然后把哈希值写入脚本:
INSERT INTO `user` (`username`, `password`, `role`, `max_borrow`) VALUES ('admin', '$2a$10$iK5YhZA3wBqzOz9L7yJ7UuY9JcY1x2QkG9wXm7zvXkXo5F3bNm9Kq', 1, 100);这个哈希对应的是admin123,但你在数据库里看不出来,答辩时可以拿这点讲“我没存明文密码”。图书样例要有一点情节:放入《Java核心技术》这种跟主题相关的书,库存设成3,可借数设成3;再放一本《深入理解Java虚拟机》之类的,让借书时有真实的库存变化可看。初始化脚本里还要注意清空已有表时的顺序,先删borrow_record、再删book、user,因为有外键约束时顺序反了会报错。
6.2 给关键操作加上日志:用AOP记录谁在什么时候借了什么书
图书管理系统在答辩时亮点通常体现在日志上。我用Spring AOP拦截借还操作,把操作记录打到日志文件里。先定义一个切面:
package com.example.book.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class BorrowLogAspect { private static final Logger logger = LoggerFactory.getLogger(BorrowLogAspect.class); @AfterReturning("execution(* com.example.book.service.BorrowService.borrowBook(..))") public void logBorrow(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); logger.info("用户ID {} 借阅图书ID {}", args[0], args[1]); } @AfterReturning("execution(* com.example.book.service.BorrowService.returnBook(..))") public void logReturn(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); logger.info("借阅记录ID {} 还书", args[0]); } }注意这里只能打日志,不能把业务逻辑放在AOP里做。如果AOP抛异常,会干扰原方法的执行。日志级别用info可以从日志文件里摘出所有借书动作,方便答辩时演示“系统记录了每一笔操作”。如果你没有引入spring-aspectj依赖,AOP不生效。另一个办法是在Service方法里直接打日志,简单有效,不过AOP的好处是以后你在Controller层调服务时,日志是全局的,不用每调一次写一遍。
6.3 验收自测:一个包含功能路径的检查清单
系统做完,我会按下面的清单走一遍,每项都标注“通过/失败”:
- 管理员登录:输入错误密码能提示“用户名或密码错误”;输入正确密码能跳转到管理首页。
- 新增图书:字段不填书名会校验报错;填完整后列表出现新书,库存数和可借数一致。
- 借书全流程:用普通用户账号登录,借一本库存只剩1本的书,能成功;再点一次借同一本书,提示“图书已借完”或“已达最大借书数”。
- 还书流程:借出后库存可借数减少1,还书后增加1,列表恢复。
- 并发测试:开两个浏览器,用两个不同用户同时借同一本书,且这本书库存只有1本,只允许一个成功。这个可以在答辩时用两个窗口演示,非常出效果。
- 分页边界:每页大小为10,总页数28条时,第3页有8条,页码导航正确显示1、2、3。
- 空白数据场景:清空图书表后,列表页不报空指针,而是显示“暂无数据”。
最后一点很容易被忽略。我在最初写列表时忘了判断pageInfo.getList().isEmpty(),结果空表时JSP的<c:forEach>确实不报错,但分页导航里的pageInfo.pages是0,页面上会出现“第0/0页”,看起来很业余。需要在Controller或JSP里加判断:总记录数为0时,给model加一个emptyTip字段。这个小细节往往是答辩时加分的地方。
做这个系统的过程里,我最大的教训是“越觉得简单的地方越要手写清楚”。事务注解和编码过滤器这两个配置,我第一次做的时候也翻过车,后来把每一次启动异常记录在笔记里,这个项目的排错效率才真正提上来。希望这份方案帮你少走几步弯路,把图书管理系统做成一个能展示、能扛住提问的完整作品。
本文还有配套的精品资源,点击获取