JavaWeb图书管理系统全流程实战:Servlet+JSP+JDBC从零构建
2026/9/2 2:06:26 网站建设 项目流程

简介:这是一份基于JavaWeb技术栈的图书管理系统完整项目包,面向正在学习JSP、Servlet、JavaBean、JDBC及DAO模式的开发者,可用于课程设计、毕业设计或项目实战练习。资源共3个文件,压缩包约4.52MB,内含项目源码(zip)、系统设计报告(docx)和数据库脚本(sql),源码覆盖前端页面、后端逻辑与数据访问层,报告涵盖需求分析、架构设计、数据库设计及功能实现,SQL文件则提供图书和用户等核心数据表。目前已有19439人学习下载,适合希望系统掌握JavaWeb开发全流程、理解前后端交互和MVC架构的读者,通过对照源码和设计文档,可快速完成环境搭建、功能扩展与排错复盘。

1. 立项前先把底子打好:技术选型与项目骨架

1.1 为什么2025年还要用传统JavaWeb做图书管理系统

说实话,现在出去面试或者做课程设计,十个里面八个会让你用Spring Boot写管理系统。但我做这个JavaWeb图书管理系统项目的时候,坚决没用框架,就用Servlet + JSP + JDBC这套最原始的技术栈。原因有三个。

第一,这个项目最核心的定位是打基础。Spring Boot把什么都封装好了,注解一标、依赖一引,业务代码往里面一塞就完事。但问题是,很多人写完Spring Boot项目,连HTTP请求怎么被Servlet接收的都不知道,更别说理解Session生命周期、Filter执行顺序这些底层机制。而用传统JavaWeb,每一步都是手动的:写Servlet、配置web.xml、处理请求参数、手动获取数据库连接,整个过程会让你把Web开发的基础链路彻底摸透。

第二,部署环境极其友好。只需要一个Tomcat,把项目打成war包丢进webapps目录,启动就完事,不需要Maven中央仓库拉几百MB依赖,不用配Redis、Nacos这些中间件。我拿一台Windows 7老机器都跑起来过,放在课程设计答辩现场演示,稳定性完全够用。

第三,图书管理系统本身够典型。它的业务逻辑足够复杂,但又不至于复杂到撑不起传统开发方式。读者管理、图书管理、借阅归还、逾期统计,每个模块都能讲清楚,每个操作都能落到SQL上。用它来做JavaWeb的完整案例,再合适不过。

注意:如果你已经有Spring Boot经验,回头补这个传统JavaWeb项目,理解反而会更透彻。Netty、Tomcat这些容器之间的区别,会在这个项目里有很直观的感受。

1.2 项目结构设计与开发环境搭建

我用的是经典的MVC分层结构,没有用Maven,直接手动管理jar包,这样对初学者更友好,不会在构建工具的坑里绕太久。

BookManager/ ├── src/ │ ├── com.book.entity # 实体类(User、Book、BorrowRecord) │ ├── com.book.dao # 数据访问层(JDBC操作数据库) │ ├── com.book.service # 业务逻辑层 │ ├── com.book.servlet # Servlet控制层 │ ├── com.book.util # 工具类(DBUtil、MD5Util、VerifyCodeUtil) │ └── com.book.filter # Filter过滤器 ├── web/ │ ├── static/ # 静态资源(CSS、JS、图片) │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置文件 │ │ └── jsp/ # 视图层页面 │ └── index.jsp └── lib/ # mysql-connector-java.jar等

开发环境这块,我给出我实测的组合:

  • JDK 1.8(兼容性最好,Tomcat裸跑不需要额外配置)
  • Tomcat 9.0(对应Servlet 4.0规范,支持JDBC 4.2)
  • MySQL 5.7(InnoDB引擎默认,事务支持成熟)
  • IDEA 2023.2 或 VS Code + Tomcat插件

如果你用的是VS Code,需要装这几个扩展:Extension Pack for Java、Tomcat for Java、MySQL Shell for VS Code。配置Tomcat的时候注意,要选Tomcat安装目录的根路径,不是bin目录。IDEA的话,直接在Run Configuration里加一个Tomcat Server即可,然后Deployment里添加Artifact。

1.3 核心配置文件web.xml的写法

传统JavaWeb项目里,web.xml是整个项目的总开关。Servlet 3.0之前只能在这里配,3.0之后可以用注解代替,但我强烈建议你在入门阶段手写一遍web.xml,因为这是理解Servlet映射路径的绝佳机会。

<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"> <display-name>JavaWeb图书管理系统</display-name> <!-- 编码过滤器 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>com.book.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 登录验证过滤器 --> <filter> <filter-name>LoginFilter</filter-name> <filter-class>com.book.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/jsp/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

这里有两个细节值得反复琢磨。第一个是<url-pattern>的匹配规则,/*/的区别,很多人在这个上面翻车。/*是匹配所有路径,包括JSP页面;而/是匹配Servlet路径但排除JSP。如果登录过滤器的url-pattern配成/*,那你连login.jsp都会拦,就会形成死循环——登录页面都进不去,还怎么登录?所以我把登录过滤器配在/jsp/*上,只拦JSP页面,Servlet层自己判断。第二个是Filter的<init-param>,如果你有多个Filter需要不同的编码参数,这地方就要区分清楚,别一把梭全部用同一个。

2. 数据库是系统的地基:图书管理系统的数据建模与关键SQL设计

2.1 三张核心表的设计思路

图书管理系统听起来简单,但数据模型如果设计得不好,后面写业务代码的时候会四处漏风。我这个项目一共用了四张表:用户表、图书表、借阅记录表,还有一个数据字典表(存图书分类)。

用户表的设计,关键是你得考虑角色。图书管理系统里,管理员和读者是两种完全不同的角色,但很多人一上来就建两套表,admin表、user表各来一套,这其实没必要。就用一张表,加一个role字段,类型用TINYINT,1代表管理员,0代表普通用户。登录之后根据role字段决定跳转到哪个页面,操作权限再去控制,逻辑清晰又省事。

CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(64) NOT NULL COMMENT '密码(MD5加密)', `real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '角色: 0-用户 1-管理员', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

图书表的设计,核心是ISBN字段。ISBN是图书的唯一标识,但在实际图书管理场景里,同一本书可能会有多本副本(比如《Java编程思想》有3本)。所以不要把ISBN设成主键,而是建立一个自增主键,ISBN单独加普通索引。书名、作者、出版社、出版日期、库存总量、可借数量,这些字段都要有。其中available_count这个字段很关键,它代表当前可借的数量,每次借书还书都会更新它。

借阅记录表是整个系统里最核心的表,也是出问题最多的地方。

CREATE TABLE `t_borrow_record` ( `id` INT NOT NULL AUTO_INCREMENT, `book_id` INT NOT NULL COMMENT '图书ID', `user_id` INT NOT NULL COMMENT '借阅人ID', `borrow_date` DATETIME NOT NULL COMMENT '借书时间', `due_date` DATETIME NOT NULL COMMENT '应还时间', `return_date` DATETIME DEFAULT NULL COMMENT '实际归还时间', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-借出 1-已还 2-逾期', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_book_id` (`book_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

status字段为什么要设0、1、2三种状态,而不是借出的记录删掉、还书的记录加一条?因为你需要保留完整的借阅历史,这个字段帮助你查询哪些书还没还、哪些逾期了,不用去算借出时间和当前时间的时间差。逾期状态不是借书的时候定好的,而是还书的时候判断的,如果actual_return_date > due_date,就更新status为2。还有人会纠结要不要记录逾期天数,我的建议是不要存,用SQL函数DATEDIFF(return_date, due_date)现算,保证数据一致性。

2.2 数据库连接工具类的封装与连接池选型

传统JavaWeb里连接数据库最原始的写法是:

Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/book_manager?useUnicode=true&characterEncoding=UTF-8"; Connection conn = DriverManager.getConnection(url, "root", "password");

这种写法开发阶段没问题,但生产环境绝对不能这么干。因为每次请求都创建一个物理连接,数据库的连接数是有限的,并发一高就直接把数据库拖垮。实际开发中有个库存管理系统的朋友跟我聊过,他们线上就出过这个问题——20个并发请求进来,数据库连接数直接占满,剩下的请求全部排队等待,接口响应时间从几十毫秒飙到十几秒。

这个项目的做法是引入Druid连接池,阿里开源的,监控功能强大,配置简单。在项目的lib目录下加入druid-1.1.23.jar,然后在src目录下建一个druid.properties配置文件:

driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/book_manager?useUnicode=true&characterEncoding=UTF-8&useSSL=false username=root password=123456 initialSize=5 maxActive=20 maxWait=60000

然后封装一个DBUtil工具类,里面有一个静态方法获取当前线程绑定的连接,这是整个项目数据访问的基础设施。如果没有连接池,你打开连接、关闭连接,几行代码看起来简单,但一旦池化之后,你会发现很多并发问题都自动消失了,connection不再频繁创建销毁。这里的关键一点是利用ThreadLocal保证一个事务里面的多个DAO操作共用同一个Connection,否则你Service层开了事务,但DAO层各拿各的连接,事务就是废的。

2.3 事务处理:借用还书必须保证原子性

借书这个动作并不只是往借阅记录表插入一条数据那么简单。它要同时干三件事:往借阅记录表插记录、把图书表对应记录的available_count减1、检查该用户有没有没还的书。这三步里任何一步失败,都不能让另外两步的修改生效。

JDBC原生写法里,事务控制的代码很冗余。每次都要conn.setAutoCommit(false)try...catchconn.rollback()、最后conn.commit()。我把这个逻辑抽到了一个TransactionTemplate里,用一个极简的模板方法模式解决问题。核心思想是,在Service层开启事务,给整个业务方法包装一层拦截。

public class TransactionManager { public static <T> T execute(TransactionCallback<T> callback) { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); T result = callback.doInTransaction(); conn.commit(); return result; } catch (Exception e) { conn.rollback(); throw new RuntimeException("事务执行失败", e); } finally { conn.setAutoCommit(true); DBUtil.closeConnection(); } } }

Service层调用的时候,把业务代码扔进execute()方法里,事务边界就自动划好了。这个思想其实就是Spring的TransactionTemplate的原型。你先理解这一层,之后再去学Spring的声明式事务,会立刻明白它省掉了哪些样板代码。

注意:事务只在Service层控制,DAO层的方法全部用自动提交模式,不要在每个DAO方法里单独管理事务,否则多层嵌套事务会出现难以排查的锁问题。

3. 功能模块逐个拆解:登录验证、图书检索与借阅闭环

3.1 验证码与登录流程:安全细节不能少

图书管理系统虽然是个教学项目,但登录这个模块不能做太草率。我的方案是图形验证码 + Session校验 + 密码MD5加密,三层防护。

验证码用Java原生Graphics2D画出来,核心代码大概是这样的:

public class VerifyCodeUtil { public static String generate(HttpServletResponse response) throws IOException { int width = 68, height = 42; BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.setColor(Color.LIGHT_GRAY); g.fillRect(0, 0, width, height); String chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; StringBuilder code = new StringBuilder(); Random random = new Random(); for (int i = 0; i < 4; i++) { char c = chars.charAt(random.nextInt(chars.length())); code.append(c); g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256))); g.setFont(new Font("Arial", Font.BOLD, 28 + random.nextInt(6))); g.drawString(String.valueOf(c), 8 + i * 15, 32 + random.nextInt(5)); } // 画干扰线 for (int i = 0; i < 8; i++) { g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height)); } g.dispose(); ImageIO.write(image, "JPEG", response.getOutputStream()); return code.toString(); } }

验证码的校验有一个容易忽略的坑:Session里存的验证码要用完即删。如果你不删除,用户刷新页面之前,同一个验证码会一直有效,这就给了脚本暴力破解的空间。我在登录成功和验证码刷新两个地方都执行了session.removeAttribute("verifyCode")

密码MD5加密,记住一句:MD5不够安全的不是算法,而是没有加盐。直接对密码做MD5,用户群体里只要两三个人用了弱密码,就能被彩虹表反查出来。我在这里加了一个固定的盐值(或者每个用户随机生成一个盐存在用户表里),把盐和密码拼在一起再做MD5,这个做法能有效防彩虹表。

登录流程我画个简单的逻辑链路:用户提交用户名、密码、验证码 -> Filter检查Session里是否有验证码 -> 校验验证码是否匹配 -> 根据用户名查用户 -> 查出来的用户判断密码是否匹配 -> 设置Session用户信息 -> 根据role跳转。这个流程里,任何一步失败都返回错误信息到登录页,不提供任何详细错误的提示。否则别人可以通过报错信息判断账号存不存在。

3.2 图书检索:分页查询与条件组合

图书检索功能,最菜的做法是把所有数据一次性load到前端,然后用JavaScript做过滤。这在小数据量下没问题,图书表超过几千条数据就卡到不行。我采用的是服务端分页 + MySQL的LIMIT语法。

前端页面展示一个搜索框 + 分类下拉框 + 搜索结果表格,表格底部是分页按钮。后端接收pageNumpageSizekeywordcategoryId四个参数,拼SQL的时候注意参数预编译,千万不能用字符串拼接SQL,否则这就是最经典的SQL注入漏洞。

public List<Book> searchBooks(String keyword, Integer categoryId, int pageNum, int pageSize) { StringBuilder sql = new StringBuilder("SELECT * FROM t_book WHERE 1=1"); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append(" AND (title LIKE ? OR author LIKE ? OR isbn LIKE ?)"); params.add("%" + keyword + "%"); params.add("%" + keyword + "%"); params.add("%" + keyword + "%"); } if (categoryId != null) { sql.append(" AND category_id = ?"); params.add(categoryId); } sql.append(" LIMIT ?, ?"); params.add((pageNum - 1) * pageSize); params.add(pageSize); // 使用 PreparedStatement 执行 }

分页的关键还有一个配套的统计总数SQL。用户看到的那个"共X条记录,第Y/Z页",这个总页数就是根据总数算出来的:

int totalCount = bookDao.countByCondition(keyword, categoryId); int totalPages = (int) Math.ceil((double) totalCount / pageSize);

分页参数里(pageNum - 1) * pageSize这个公式必须理解清楚。LIMIT的语法是LIMIT offset, count,offset从0开始,所以页码减1再乘以每页大小就是跳过的行数。这个小地方写错了,翻到第二页就会丢一条数据或者重复一条数据。

3.3 借书与还书的完整闭环

借书流程的Service层逻辑,我把每一步都用注释标注出来:

@Transactional(rollbackFor = Exception.class) public void borrowBook(int bookId, int userId) { // 1. 查图书,判断库存 Book book = bookDao.findById(bookId); if (book == null || book.getAvailableCount() <= 0) { throw new BusinessException("图书不存在或已借完"); } // 2. 查用户当前未归还的借阅记录 int unreturnedCount = borrowDao.countUnreturnedByUserId(userId); if (unreturnedCount >= MAX_BORROW_LIMIT) { throw new BusinessException("已达到最大借阅数量"); } // 3. 插入借阅记录 BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setUserId(userId); record.setBorrowDate(new Date()); Calendar cal = Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, 30); // 默认借期30天 record.setDueDate(cal.getTime()); record.setStatus(0); borrowDao.insert(record); // 4. 扣减库存 bookDao.decreaseAvailable(bookId); }

这一步很多人会漏考虑一个东西:一个用户能借几本书?我设了MAX_BORROW_LIMIT = 5这个常量,用户在借未还的书超过5本就直接拒绝。这个上限在真实图书馆系统里一般是10到20本,课程设计用5本足够展示逻辑。借书成功之后,页面上要能看到自己的借阅列表,且能区分"在借"和"已还"状态。

还书流程就相对简单:更新借阅记录的return_date为当前时间,根据是否超过due_date设置status为1或2,再把图书的available_count加回去。但有一个细节:还书的时候要判断这本书是不是这个用户借的,防止用户A知道了一个借阅记录ID就把用户B借的书给还了。权限校验在Web应用里无处不在,写的时候要养成习惯。

3.4 管理员专属功能:图书上下架与用户管理

管理员登录和用户在同一个Session里靠role字段区分。管理员的额外功能主要是图书的新增、编辑、删除,以及用户列表查看和启用/停用。

图书新增和编辑的区别在于,新增是INSERT,编辑是UPDATE。我懒得写两个Servlet,就用一个BookEditServlet,通过请求里有没有bookId参数来决定是新增还是编辑模式。这个技巧在实际项目里也常用:一个资源接口同时承载创建和更新两种语义,关键是响应不同的HTTP状态码。

删除图书的时候有一个大坑:如果这本书已经有借阅记录,直接DELETE会违反外键约束(如果表里设置了外键)或者产生孤儿数据。我的做法是逻辑删除,给图书表加一个status字段,0正常、1下架。删除操作只是把status改成1,历史借阅记录不受影响,前端查询的时候默认只查status=0的。这个设计其实是从电商系统的商品下架学来的,数据永远不物理删除,只做标记。

4. 联调时会踩的坑:真实开发中的高频问题与排查实录

4.1 中文乱码——三个层面逐个排查

几乎每个做JavaWeb的新人都会被中文乱码折磨。乱码分三种情况,我记得第一次做完这个项目展示给同学看,页面上全是"浣犲ソ"这种火星文,那种心情真的不想再体验。

第一个层面是请求乱码。POST请求的参数乱码,是因为Tomcat默认用ISO-8859-1解码请求体。解决办法就是在web.xml里配置CharacterEncodingFilter,或者手动在Servlet里写request.setCharacterEncoding("UTF-8")。GET请求的乱码更隐蔽,是因为Tomcat的URI编码默认也是ISO-8859-1,需要修改Tomcat的server.xml,在<Connector>标签上加URIEncoding="UTF-8"这个属性。

第二个层面是响应乱码response.setContentType("text/html;charset=UTF-8")这行代码要放在getWriter()调用之前。如果你先拿了Writer再设编码,那已经写出去的内容编码就固定了,怎么设置都没用。

第三个层面是数据库乱码。连接URL上要加characterEncoding=UTF-8,数据库建表时要指定DEFAULT CHARSET=utf8mb4,JDBC驱动连接属性里也需要指定编码。这三个地方缺一个,数据存进去的时候就会变成问号或者乱码。我见过太多人只改了数据库的编码,忘了连接URL上的参数,结果数据一插进去就全变问号。

我建议在项目最开始写一个专门的编码测试Servlet,把所有涉及编码的环节测一遍,确认无误再往下开发。别等到所有功能做完才开始联调,那时候排查范围太大,极其痛苦。

4.2 连接池报错与数据库连接泄露

用上Druid连接池之后,我遇到过一个经典问题:运行一段时间后,控制台报连接池已关闭或者com.alibaba.druid.pool.GetConnectionTimeoutException,连接数达到maxActive上限。

这种问题十有八九是连接没关闭。很多人在DAO层写了conn = DBUtil.getConnection()之后,只关了ResultSetPreparedStatement,唯独忘了关Connection。在连接池模式下,conn.close()不是真的关闭物理连接,而是把连接返还给连接池,所以这个close绝对不能省。

排查方法很简单,在Druid的配置里开启监控页,或者我在代码里打日志,查看每个线程拿到连接后都在干什么。最后发现还有一个隐性坑:代码里某个分支提前return了,而finally块里的关闭代码放在了return之后,根本执行不到。所以正确写法一定是:

Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); // 执行逻辑 } finally { if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); }

不想写这么啰嗦的话,Java 7的try-with-resources语法你值得拥有。注意Connection实现了AutoCloseable接口,JDBC 4.1及以上都支持。配合上连接池的maxActive=20initialSize=5minIdle=5,系统跑个几千次操作都不会有什么连接问题。

4.3 TIMESTAMP字段的范围问题

MySQL 5.7里有个默认行为:TIMESTAMP类型默认范围是1970-01-012038-01-01。如果借书或还书的日期设置超出这个范围,就会报Data truncation: Incorrect datetime value

这个坑特别容易在测试时踩到。你测试还书功能,写了一个return_date = '2099-01-01'的数据,然后数据库直接拒绝插入。解决办法有两个:一是建表的时候把所有日期字段都改成DATETIME类型,DATETIME的范围是1000-01-019999-12-31,几乎不会超;二是在Java代码里不要用java.sql.Timestamp去set日期,直接用java.util.Date配合setObject()方法传参,让JDBC驱动自己转换。

我在项目里统一用的是DATETIME类型,彻底绕开这个困扰。

4.4 常见问题速查表

我把这个项目开发过程中遇到过的问题整理成了一个表,方便你开发的时候对照排查:

现象可能原因解决方式
登录页面加载不出来登录Filter误拦截了JSP文件检查web.xml里Filter的url-pattern是否配成/jsp/*而不是/*
验证码图片不显示输出流没关闭或Content-Type没设置确认response.setContentType("image/jpeg")且不用关闭ServletOutputStream
数据库中文变成??连接URL缺少characterEncoding=UTF-8在JDBC URL上追加编码参数
翻页时数据重复/缺失LIMIT偏移量计算错误(pageNum - 1) * pageSize作为offset
并发借同一本书超卖没有在事务中锁行使用SELECT ... FOR UPDATE锁定图书行,事务提交后释放
借书成功后库存没变借阅记录和库存扣减不在同一个事务确保两个DAO操作使用同一个数据库连接

5. 这个项目还能怎么挖深一层:从课程设计到简历实战

5.1 用Filter + ThreadLocal实现事务与请求追踪

很多人做完CRUD就收工了,但如果能在项目里加上一些"工程化"的小设计,面试的时候讲出来会加分不少。比如我在这个项目里用ThreadLocal让DAO层自动获取当前请求线程绑定的数据库连接,这样Service层开启动态代理之后,事务边界就清晰了。

这个技巧的进阶用法是:实现一个简单的请求日志过滤器,记录每次请求的URL、执行时间、用户信息,输出到日志文件。这种"横切关注点"的概念,其实就是AOP思想的雏形。

public class LogFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long start = System.currentTimeMillis(); HttpServletRequest req = (HttpServletRequest) request; chain.doFilter(request, response); long cost = System.currentTimeMillis() - start; System.out.printf("Request: %s, cost: %dms%n", req.getRequestURI(), cost); } }

5.2 可视化统计报表的扩展思路

图书管理系统如果停留在增删改查,它只是一个数据库练习。真正让它有价值的,是你能从数据里看到运营指标。扩展一个统计页:每月借阅量趋势图、图书分类占比饼图、逾期率TOP10图书排行。这些数据通过一条SQL就能查出来,前端用ECharts展示。

JSP页面引入ECharts很简单,通过CDN直接引用,在页面初始化时用Ajax请求后端一个统计Servlet,拿到JSON数据后绘制图表。这一步做出来,我就觉得这个项目不是"能跑",而是"能展示"了。我当时扩展了一个管理员Dashboard页,放上整个图书馆的借阅量、馆藏量、逾期量三个核心指标,演示效果比一堆表格好太多。

5.3 简历上面试官爱问的几个衍生点

做完这个项目,面试官大概率会顺着问几个问题,你得提前准备:

  • Servlet的生命周期是怎样的?init、service、destroy分别在什么时候被调用?
  • GET和POST的区别?什么时候用GET,什么时候用POST?
  • Session和Cookie的区别?Session的实现原理是什么?
  • 事务的ACID四大特性是什么?你在哪段代码里用到了事务?
  • MySQL的索引底层数据结构是什么?为什么用B+树不用哈希?
  • 什么是SQL注入?你的代码里怎么防止它?

这些问题都能在这个项目里找到对应的代码和设计依据。我当年就是把"借书事务"那一段代码背熟,然后把事务隔离级别、Spring事务传播行为这些概念串联起来讲,面试效果比纯背八股文好得多。

写在最后的实操建议

跑一遍这个项目的完整流程,从建库建表到部署到Tomcat,我粗略估算了一下,一天半左右就能完成核心功能。真正费时间的不是写代码,而是排查那些"你以为没问题但就是不对"的细节,比如编码、连接关闭、路径映射。所以我建议你别急着把所有功能一次性写完,先完成一个最小闭环,登录 -> 图书列表 -> 借书 -> 还书,每一步都跑通了再往上面加功能。

做项目的过程里还有一个习惯值得坚持:每解决一个bug,把它记下来,写清"什么现象、什么原因、怎么解决的"。这个笔记既不占时间,又是你后续面试、写博客的最宝贵素材。说实话,我写这篇复盘文章用的很多案例,就是从当年的bug记录里翻出来的。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询