ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JavaWeb图书系统实战:从Servlet到MyBatis全链路解析

JavaWeb图书系统实战:从Servlet到MyBatis全链路解析

简介:这是一份面向Java Web初学者的图书管理系统实战项目,适合作为高校计算机专业大二学生期末课程设计或Web开发入门练手。系统完整实现了图书借阅、归还、续借、逾期管理及用户留言等核心功能,涵盖学生、管理员双角色权限体系,并集成登录注册、信誉等级控制、日志记录等实用模块。资源包共59个文件,包含26个Java业务逻辑类、13个HTML前端页面、7个XML配置文件(含MyBatis映射)、5张界面截图与3个JSP动态页,整体压缩后仅879KB,结构清晰、代码规范,便于快速理解MVC分层架构与前后端交互流程。目前已有1915人学习下载,配套SQL建表脚本、README说明及完整pom依赖配置,开箱即用,特别适合巩固Servlet、JSP、MySQL及基础Ajax异步交互等关键技术点。

1. 这不是又一个“Hello World”式图书系统:它用真实业务逻辑倒逼你搞懂 JavaWeb 全链路闭环

大二上学期交的期末大作业,能跑通登录注册就算及格?这个Javaweb图书馆.zip不是玩具——它把「学生借书逾期自动标记」「信誉等级动态控制续借额度」「留言与书籍强绑定」「日志文件落盘」全写进了实体类和 Controller 层。我去年带三届实训班,90% 的同学卡在「为什么 Ajax 提交后数据库没更新」,而这个项目里StudentController.java里那行@ResponseBody public Map<String, Object> register(@RequestBody Student student)就是答案:它强制你面对@RequestBody和Content-Type: application/json的真实配合逻辑。它不教 Servlet 生命周期理论,但当你改完web.xml里的<url-pattern>/student/*</url-pattern>后发现所有学生接口突然 404,你就得翻源码查 DispatcherServlet 的映射规则。适合刚学完 JDBC、还没碰过 MyBatis 的人——因为它的mybatis.sql是手写的建表语句(含state tinyint COMMENT '0未借出/1已借出/2续借/3逾期/4丢失/5损毁/6其他'),不是 Hibernate 自动生成的黑匣子;也适合想验证自己 Spring Boot 前置知识的人——因为它的pom.xml里spring-webmvc版本是 5.3.37,正好卡在 JDK 8 兼容性临界点。别急着 run,先看清楚它怎么用Cookie实现「十天免密登录」——不是response.addCookie(new Cookie("user", "xxx"))就完事,而是cookie.setMaxAge(10 * 24 * 3600)+cookie.setPath("/")+cookie.setHttpOnly(true)三连,漏一个就可能被 XSS 拿走凭证。


2. 从解压到启动:五步落地 JavaWeb 图书系统(含 IDEA 配置避坑清单)

2.1 解压结构解析:看清 src/main 下的三层骨架

项目解压后核心目录结构如下(非标准 Maven 结构,需手动适配):

Javaweb图书馆/ ├── LMS-master/ # 主项目根目录(注意:不是直接放 webapp) │ ├── src/ │ │ └── main/ # 关键!Java 类、配置、静态资源都在这里 │ │ ├── java/ # com.example.lms.controller 等包路径起点 │ │ ├── resources/ # mybatis-config.xml、log4j2.xml(如有) │ │ └── webapp/ # WEB-INF/web.xml + jsp 页面(重点!) │ ├── mybatis.sql # 建库建表 SQL(含 state 字段注释) │ ├── pom.xml # Maven 依赖(Spring 5.3.x + MyBatis 3.4.x) │ └── README.md # 作者手写说明(含数据库用户名密码默认值)

提示:LMS-master/src/main/webapp/WEB-INF/web.xml是 Servlet 3.0 之前的经典配置入口,里面<servlet-mapping>映射了DispatcherServlet到/,这意味着所有请求都走 Spring MVC,但index.jsp必须放在webapp/根目录下才能被直接访问——这是新手常踩的第一个坑:把index.jsp放错位置导致 404。

2.2 IDEA 导入与 Tomcat 配置:绕开「Artifact not found」玄学报错

步骤 1:导入为 Maven 项目
右键pom.xml→Add as Maven Project,IDEA 自动识别依赖。关键检查点:

  • pom.xml中<properties>是否含java.version=1.8(必须匹配 JDK 8)
  • <dependency>中spring-webmvc版本是否为5.3.37(高于 5.3.38 会因 JDK 8 不兼容报NoSuchMethodError)

步骤 2:配置 Tomcat Server
Run → Edit Configurations → + → Tomcat Server → Local

  • Deployment Tab:点击+→Artifact→ 选择LMS-master:war exploded(不是jar!)
  • Application context:填/lms(避免根路径冲突,后续访问地址为http://localhost:8080/lms)
  • Before launch:勾选Build project(确保每次启动前编译最新代码)

步骤 3:修正 webapp 路径映射
IDEA 默认将src/main/webapp当作 Web Resources Root,但若识别失败:
Project Structure → Modules → LMS-master → Web → Web Resource Directory→ 手动指定为src/main/webapp
→ 点击OK后,WEB-INF目录应显示为蓝色图标(表示正确识别)

步骤 4:数据库初始化
执行mybatis.sql(推荐用 MySQL Workbench):

CREATE DATABASE lms_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE lms_db; -- 执行建表语句(含 book.state tinyint 注释)

参数说明:utf8mb4是必须项,否则中文备注(如COMMENT '0未借出/1已借出...')会乱码;tinyint类型比int更省空间,且state只有 0~6 共 7 种状态,用tinyint正好。

步骤 5:启动验证
点击绿色三角形启动 Tomcat,观察控制台输出:

  • 若出现INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'dispatcher': initialization completed in XXX ms→ Spring MVC 初始化成功
  • 若出现Caused by: java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver→ 检查pom.xml是否含mysql-connector-java8.0.28(JDK 8 兼容版本)
  • 成功后访问http://localhost:8080/lms,应看到作者写的index.jsp(含项目介绍 + 学生/管理员注册入口)

3. 登录注册模块深度拆解:Ajax + Session + Cookie 的真实协作链

3.1 学生注册:为什么用 @RequestBody 而不用 @RequestParam?

StudentController.java中注册方法签名是:

@PostMapping("/student/register") @ResponseBody public Map<String, Object> register(@RequestBody Student student) { // 1. 校验学号唯一性(AJAX 异步调用 /student/checkNumber) // 2. 密码加密(BCryptPasswordEncoder.encode(student.getPassword())) // 3. 插入数据库(MyBatis insert) // 4. 返回 { "code": 200, "msg": "success" } }

关键逻辑说明:

  • @RequestBody表示前端必须以Content-Type: application/json发送数据,例如:
    fetch('/lms/student/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ number: '2023001', username: '张三', password: '123456' }) })
  • 对比@RequestParam:后者要求 URL 参数或表单编码(application/x-www-form-urlencoded),但本项目明确要求「不要用 form 表单提交」,所以必须用 JSON 方式——这迫使你理解 HTTP 协议中Content-Type与后端注解的严格对应关系。

参数说明:

  • @ResponseBody:告诉 Spring 不走视图解析器(ViewResolver),直接序列化 Map 为 JSON 返回给前端
  • Map<String, Object>:灵活返回状态码和消息,前端用response.code === 200判断成功

3.2 学生登录:Session 记录用户 + Cookie 实现十天免密的双重保险

登录成功后的核心代码在StudentController.java:

@PostMapping("/student/login") @ResponseBody public Map<String, Object> login(@RequestBody Map<String, String> loginData, HttpServletRequest request, HttpServletResponse response) { String number = loginData.get("number"); String password = loginData.get("password"); Student student = studentService.findByNumber(number); if (student != null && passwordEncoder.matches(password, student.getPassword())) { // 1. 创建 Session 存储用户信息 HttpSession session = request.getSession(); session.setAttribute("student", student); session.setMaxInactiveInterval(10 * 24 * 3600); // Session 有效期 10 天 // 2. 设置 Cookie 记住账号(仅存学号,密码绝不存!) Cookie cookie = new Cookie("student_number", number); cookie.setMaxAge(10 * 24 * 3600); // Cookie 有效期 10 天 cookie.setPath("/"); // 作用域为整个应用 cookie.setHttpOnly(true); // 防 XSS,JS 无法读取 cookie.setSecure(false); // 开发环境用 HTTP,生产环境需 true response.addCookie(cookie); return Map.of("code", 200, "msg", "登录成功"); } return Map.of("code", 401, "msg", "学号或密码错误"); }

逻辑说明:

  • session.setAttribute("student", student):将整个 Student 对象存入 Session,后续所有请求可通过request.getSession().getAttribute("student")获取当前用户
  • Cookie仅存student_number(学号),绝不存密码或 token——这是安全底线,也是课程设计隐含的考核点
  • setHttpOnly(true)是防 XSS 的关键:即使页面被注入恶意脚本,也无法通过document.cookie窃取 Cookie

3.3 管理员登录:为什么和学生共用一套 Controller 逻辑?

AdminController.java的登录方法与StudentController几乎一致,区别仅在:

  • 查询逻辑:adminService.findByUsername(username)而非findByNumber()
  • Session key:session.setAttribute("admin", admin)
  • Cookie 名:new Cookie("admin_username", username)

设计意图解析:
这不是偷懒,而是体现「职责分离」思想——登录认证逻辑(密码校验、Session 创建、Cookie 设置)是通用的,差异只在「用户类型识别」和「数据查询来源」。如果你强行合并成一个UserController,就会在login()方法里写一堆if (type.equals("student")) {...} else if (type.equals("admin")) {...},违反开闭原则。本项目用两个 Controller,恰恰是教你如何用「类隔离」应对不同角色,而非用「if-else」硬编码。


4. 图书管理核心业务:state 字段的七种状态如何驱动借还逻辑?

4.1 Book 实体类 state 字段:从数据库设计到业务流转的完整映射

Book.java中state定义为private Integer state;,其取值范围(0~6)不是随意编号,而是严格对应业务动作:

state 值业务含义触发条件前置状态约束
0未被借出新书入库,默认值无
1已被借出学生点击「借书」按钮必须为 0
2被续借学生在借阅期内点击「续借」必须为 1(且未逾期)
3逾期系统定时任务检测borrowBookDate超过 30 天必须为 1 或 2
4丢失学生主动报失或管理员手动标记可从任意状态转为 4
5损毁图书馆工作人员录入损毁记录可从任意状态转为 5
6其他情况自定义异常状态(如待维修、下架审核)无

关键验证逻辑在BookService.java:

// 借书操作 public boolean borrowBook(Integer bookId, Integer studentId) { Book book = bookMapper.selectById(bookId); if (book.getState() != 0) { // 只有 state=0 才能借 return false; } book.setState(1); book.setOwner(studentId); book.setBorrowBookDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date())); bookMapper.updateById(book); return true; } // 续借操作(需满足:state=1 且未逾期) public boolean renewBook(Integer bookId) { Book book = bookMapper.selectById(bookId); if (book.getState() != 1) return false; // 检查是否逾期(简化版:借阅日期 + 30 天 < 当前日期) try { Date borrowDate = new SimpleDateFormat("yyyy-MM-dd").parse(book.getBorrowBookDate()); Calendar cal = Calendar.getInstance(); cal.setTime(borrowDate); cal.add(Calendar.DAY_OF_MONTH, 30); if (cal.getTime().before(new Date())) { return false; // 已逾期,不能续借 } } catch (ParseException e) { return false; } book.setState(2); book.setReturnBookDate(null); // 续借后还书日期清空 bookMapper.updateById(book); return true; }

参数说明:

  • borrowBookDate和returnBookDate使用String类型而非Date,是因为 JSP 页面传参时直接接收字符串(如"2024-05-20"),避免类型转换异常
  • state更新必须用bookMapper.updateById(book)而非update(book, wrapper),因为updateById保证只更新主键对应的记录,防止并发修改

4.2 日志文件落盘:为什么不用数据库存日志而用文件?

BookService.java中借还操作后调用:

private void writeLog(String action, String bookName, String studentNumber) { try (FileWriter writer = new FileWriter("logs/book_log.txt", true)) { String log = String.format("[%s] %s - %s - %s%n", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), action, bookName, studentNumber); writer.write(log); } catch (IOException e) { e.printStackTrace(); // 生产环境应记录到 log4j2 } }

选型理由:

  • 性能考量:日志是高频写入操作(每借/还一次就写一行),数据库事务开销远大于文件追加(FileWriterwithtrue参数)
  • 可读性优先:运维人员可直接tail -f logs/book_log.txt实时查看操作流,无需连接数据库查表
  • 课程设计边界:大二学生尚未学日志框架(Log4j2/SLF4J),用原生FileWriter降低理解门槛,但留了扩展口(注释提示「生产环境应记录到 log4j2」)

避坑点:

  • FileWriter构造函数第二个参数true表示追加模式,漏写会导致日志被覆盖
  • logs/目录需在 Tomcat 启动前手动创建(IDEA 默认不创建),否则首次写入抛FileNotFoundException

5. 避坑指南:五个让大二学生调试到凌晨三点的真实问题

5.1 现象:Ajax 注册成功但数据库无数据,控制台无报错

原因:前端发送 JSON 时Content-Type缺失或错误。浏览器默认text/plain,而 Spring MVC 的@RequestBody要求application/json。
解决:检查前端 fetch/fetch 代码,必须显式设置headers: { 'Content-Type': 'application/json' }。用浏览器开发者工具 Network Tab 查看 Request Headers,确认Content-Type值为application/json。

5.2 现象:登录成功后跳转到空白页,URL 变为/lms/student/menu但页面 404

原因:web.xml中<servlet-mapping>的<url-pattern>与 Controller 的@RequestMapping不匹配。本项目StudentController使用@RequestMapping("/student"),而web.xml中DispatcherServlet映射为/,理论上应匹配。但若web.xml里<servlet-mapping>写成<url-pattern>/student/*</url-pattern>,则实际请求路径需为/lms/student/student/menu(重复/student)。
解决:统一web.xml中DispatcherServlet的<url-pattern>为/,Controller 层用@RequestMapping("/student"),确保访问/lms/student/menu时能命中StudentController.menu()方法。

5.3 现象:MySQL 中state字段存入null,而不是预期的0或1

原因:Book实体类中state是Integer(对象类型),而数据库字段是TINYINT NOT NULL DEFAULT 0。当 Java 对象未显式赋值时,Integer默认为null,MyBatis 插入时若未设jdbcType=TINYINT,会因类型不匹配导致插入失败或存null。
解决:在BookMapper.xml的<insert>语句中,为state字段显式指定jdbcType:

<insert id="insert" parameterType="Book"> INSERT INTO book (name, pos, type, info, state, remark, owner, borrowBookDate, returnBookDate) VALUES (#{name}, #{pos}, #{type}, #{info}, #{state,jdbcType=TINYINT}, #{remark}, #{owner}, #{borrowBookDate}, #{returnBookDate}) </insert>

5.4 现象:验证码图片不显示,控制台报java.lang.NoClassDefFoundError: javax/imageio/ImageIO

原因:JDK 11+ 移除了java.desktop模块中的部分 AWT 类,而本项目验证码生成使用ImageIO.write(),需显式添加模块依赖。
解决:在pom.xml的maven-compiler-plugin中添加--add-modules java.desktop:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>8</source> <target>8</target> <compilerArgs> <arg>--add-modules</arg> <arg>java.desktop</arg> </compilerArgs> </configuration> </plugin>

5.5 现象:Tomcat 启动后index.jsp能访问,但点击「学生注册」按钮跳转404

原因:index.jsp中表单或链接的action/href路径写死为/student/register,而实际部署上下文是/lms,正确路径应为/lms/student/register。
解决:JSP 中统一用 EL 表达式获取上下文路径:

<!-- 错误写法 --> <form action="/student/register"> <!-- 正确写法 --> <form action="${pageContext.request.contextPath}/student/register">

contextPath会自动补全/lms,避免硬编码路径。


6. 进阶技巧:用 MyBatis 日志定位 SQL 执行瓶颈与参数绑定真相

6.1 开启 MyBatis SQL 日志:三步看清「到底执行了什么」

MyBatis 默认不打印 SQL,但调试借还逻辑时,你必须知道「book.setState(1)最终生成的 UPDATE 语句长什么样」。开启日志只需三步:

Step 1:在src/main/resources/log4j2.xml中添加 MyBatis 日志配置
(若无此文件,则新建,放在resources/目录下)

<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <!-- 关键:开启 MyBatis 日志 --> <Logger name="org.apache.ibatis" level="DEBUG" additivity="false"> <AppenderRef ref="Console"/> </Logger> <!-- 关键:开启 Mapper 接口日志(显示参数绑定) --> <Logger name="com.example.lms.mapper" level="DEBUG" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Root level="INFO"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>

Step 2:确保pom.xml引入 log4j2 依赖

<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.20.0</version> </dependency>

Step 3:重启 Tomcat,触发一次借书操作
控制台将输出类似内容:

14:22:31.123 [http-nio-8080-exec-2] DEBUG org.apache.ibatis.logging.jdbc.BaseJdbcLogger - ==> Preparing: UPDATE book SET state = ?, owner = ?, borrowBookDate = ? WHERE id = ? 14:22:31.124 [http-nio-8080-exec-2] DEBUG org.apache.ibatis.logging.jdbc.BaseJdbcLogger - ==> Parameters: 1(Integer), 1001(Integer), 2024-05-20(String), 123(Integer) 14:22:31.125 [http-nio-8080-exec-2] DEBUG org.apache.ibatis.logging.jdbc.BaseJdbcLogger - <== Updates: 1

参数说明:

  • Preparing行显示预编译 SQL 模板(问号占位)
  • Parameters行显示实际绑定的参数值及类型(1(Integer)表示state=1作为整数传入)
  • Updates: 1表示影响 1 行,证明更新成功

6.2 用日志反向验证业务逻辑:state 状态机是否被绕过?

假设你怀疑「学生能对 state=3(逾期)的书发起续借」,此时不要猜,直接看日志:

  1. 在BookService.renewBook()方法开头加一行日志:System.out.println("renewBook called for bookId: " + bookId + ", current state: " + book.getState());
  2. 手动将某本书state改为3(用 MySQL Workbench)
  3. 点击续借按钮,观察控制台输出:
    • 若输出current state: 3,说明状态校验逻辑未生效(bug)
    • 若无此输出,说明请求根本没进renewBook()(前端路径错误或 Controller 未映射)
    • 若输出current state: 1,说明数据库数据被其他操作修改(并发问题)

这种「日志即证据」的习惯,比打断点更高效——尤其当你面对 200 行的 Service 方法时,一句System.out.println就能定位到哪一行逻辑被跳过。

6.3 一个血泪经验:永远在mybatis.sql执行后验证state字段的DEFAULT值

mybatis.sql中建表语句为:

CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, state TINYINT NOT NULL DEFAULT 0, ... );

但很多同学执行 SQL 后没验证DEFAULT 0是否生效。结果新书入库时state为null,导致借书逻辑if (book.getState() != 0)永远为true(因为null != 0返回true),借书失败却不报错。
我的固定动作:执行mybatis.sql后,立即在 MySQL 中运行:

INSERT INTO book (name, pos, type, info) VALUES ('测试书', 'A-01', '计算机', '测试简介'); SELECT id, name, state FROM book WHERE name = '测试书';

确认state列返回0,再继续开发。从那以后我每次初始化数据库,都强制走一遍这个验证,哪怕只是多敲三行命令——它省下的调试时间,够你喝两杯咖啡。希望帮到你。

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

返回列表