简介:这是一套基于JSP+Servlet+Tomcat技术栈开发的电子图书管理系统完整源码,面向Java Web初学者、课程设计学生及小组项目开发者,解决图书信息在线管理、用户交互与前后端协同开发等典型教学实践问题。资源共50个文件,含22个Java业务逻辑类(涵盖用户认证、图书增删改查等核心Servlet)、11个XML配置文件(包括web.xml、MyBatis映射及数据库连接配置)、8个JSP页面(如login.jsp、detail.jsp、addBook.jsp等构成完整前端流程),辅以CSS、JS、Bootstrap样式资源及必要工程元数据文件,压缩包仅2.77MB,轻量易部署。已有123人学习下载,适合快速上手Java Web开发全流程:从Tomcat环境搭建、Servlet请求分发、JSP动态渲染,到响应式界面实现与MySQL数据持久化集成。目录结构规范,src与webapp分离清晰,.gitignore和readme.txt等配套文件齐全,便于理解MVC分层思想与实际工程组织方式。
1. 为什么现在还要用 JSP+Servlet+Tomcat 做电子图书管理系统?——不是怀旧,是可控、可审计、可交接的工程选择
你打开招聘网站搜“Java Web 开发”,JD里写“熟悉 Spring Boot”是标配,但翻到教育类、政务类、中小企内部系统岗位,常会看到一句:“有 JSP/Servlet 实战经验者优先”。这不是技术倒退,而是现实约束下的理性选择:一个高校图书馆管理员要自己增删图书、导出借阅报表,不需要微服务拆分、不用对接 Kafka 消息队列,但必须能本地部署、不依赖云厂商、源码看得懂、改起来不踩坑。这个“基于 JSP+Servlet+Tomcat 的电子图书管理系统”,就是为这类场景量身定制的最小可行闭环——它不炫技,但把用户登录、图书 CRUD、借阅记录管理、分类统计这些真实业务,用最直白的 Java Web 原生技术栈串通;所有逻辑在.java文件里明明白白,所有页面在.jsp里清清楚楚,所有请求路由由web.xml或注解一锤定音。它适合刚学完 Servlet 生命周期的学生做课程设计,也适合运维只有 Linux 服务器权限、没配 Docker 的单位信息员快速上线。如果你正被 Spring Boot 的自动配置绕晕,或被前端框架打包后找不到index.html路径,那这套方案就是你的“技术锚点”:没有黑匣子,只有request.getParameter()和response.getWriter()的确定性。
2. 从零搭起 Tomcat 环境:不是下载就完事,关键在 JDK 版本对齐与目录权限
JSP 和 Servlet 的运行根基是 Tomcat,但它不是独立存在的“软件包”,而是一个严格依赖 JVM 版本、目录结构和启动上下文的容器。很多新手卡在第一步:下载了apache-tomcat-8.5.99.tar.gz,解压后双击startup.bat却弹窗报错“JAVA_HOME not set”,或者 Linux 下./startup.sh显示Permission denied。这不是操作失误,而是没理解 Tomcat 的启动契约。
2.1 JDK 与 Tomcat 版本必须双向验证,不能只看官网推荐表
Tomcat 官网文档写的“Tomcat 8.5 支持 JDK 7+”,是下限,不是安全区。实际项目中,我们坚持JDK 8u202 与 Tomcat 8.5.99 组合(对应image: tomcat:8.5-jdk8-corretto镜像的生产级实践),原因有三:
- JSP 编译器(
jasper)在 JDK 8u202 之后才稳定支持 EL 表达式中的方法调用(如${book.author.getName()}); - Tomcat 8.5.99 的
catalina.sh脚本默认读取JAVA_HOME,若用 JDK 11+,其jre/lib/ext目录结构变更会导致commons-dbcp连接池加载失败; tomcat-jdbc.jar在 JDK 8u202 下的ClassLoader双亲委派行为可预测,避免“类加载冲突”这类玄学问题。
提示:执行
java -version后,务必再运行echo $JAVA_HOME(Linux/macOS)或echo %JAVA_HOME%(Windows),确认环境变量指向的是 JDK 根目录(含bin/java和jre/子目录),而非仅 JRE。
2.2 Linux 下启动前必须做的三件事:权限、端口、日志路径
Tomcat 默认监听 8080 端口,但在生产环境,这常与 Nginx 冲突;默认日志写入logs/catalina.out,但若logs/目录属主不是启动用户,会静默失败。以下是不可跳过的初始化步骤:
# 1. 解压并进入目录(假设 tar 包已下载) tar -xzf apache-tomcat-8.5.99.tar.gz cd apache-tomcat-8.5.99 # 2. 创建专用用户(避免 root 启动) sudo useradd -r -s /bin/false tomcat sudo chown -R tomcat:tomcat /opt/tomcat # 3. 修改 server.xml:将 Connector port="8080" 改为 8081(避开常用端口) # 并添加 redirectPort="8443"(为后续 HTTPS 预留) sudo -u tomcat sed -i 's/port="8080"/port="8081"/' conf/server.xml sudo -u tomcat sed -i 's/redirectPort="8443"/redirectPort="8443"/' conf/server.xml # 4. 设置日志目录可写(关键!否则 catalina.out 不生成) sudo -u tomcat mkdir -p logs sudo -u tomcat chmod 755 logs执行完后,用sudo -u tomcat ./bin/startup.sh启动,再立刻检查logs/catalina.out是否有INFO: Server startup in [xxx] ms字样。没有?说明logs/权限或JAVA_HOME仍有问题——这是后续所有 JSP 页面 404 的根源。
2.3 Windows 下绕过“双击 startup.bat 闪退”的血泪经验
Windows 用户常遇到双击startup.bat后窗口一闪而逝。这不是脚本错误,而是startup.bat默认调用catalina.bat run,而run模式会在控制台退出时终止进程。正确做法是:
- 以管理员身份打开命令提示符(CMD);
- 切换到
bin目录:cd D:\apache-tomcat-8.5.99\bin; - 执行
catalina.bat start(注意是start,不是run); - 查看
logs\catalina.out最后一行是否为Server startup in。
注意:若仍失败,请右键“我的电脑 → 属性 → 高级系统设置 → 环境变量”,确认
JAVA_HOME是完整路径(如D:\Program Files\Java\jdk1.8.0_202),且Path中包含%JAVA_HOME%\bin。不要用中文路径、空格路径——这是 Windows 下 Tomcat 启动失败的头号原因。
3. 电子图书核心功能落地:从数据库建模到 JSP 页面渲染的全链路闭环
系统不是“能跑就行”,而是要支撑真实业务:管理员录入新书、读者查书、借阅记录生成、分类统计报表。这要求每一层都严丝合缝——数据库字段类型决定 Servlet 接收参数的转换方式,JSP 表单 name 属性必须与 ServletgetParameter()的 key 一致,而 Tomcat 的web.xml则决定了 URL 路径如何映射到具体类。
3.1 MySQL 数据库设计:为什么book表必须设isbn为主键而非自增 ID?
电子图书系统的核心实体是book,但它的主键选择直接影响后续所有 CRUD 操作的健壮性。常见错误是设id INT AUTO_INCREMENT PRIMARY KEY,看似简单,却埋下三个隐患:
- 图书 ISBN 是全球唯一标识(如
978-7-04-050694-6),用作主键可直接对接出版社数据,避免 ID 冲突; - 借阅表
borrow_record中book_isbn外键引用book.isbn,比book_id更易校验合法性(ISBN 格式可正则校验); - JSP 页面展示图书详情时,URL 可直接用
/book/detail.jsp?isbn=9787040506946,无需额外查询 ID,减少一次数据库 round-trip。
建表 SQL 如下(含关键注释):
-- 创建图书表,isbn 为主键,长度固定 17(含分隔符) CREATE TABLE book ( isbn VARCHAR(17) PRIMARY KEY COMMENT '国际标准书号,格式如 978-7-04-050694-6', title VARCHAR(200) NOT NULL COMMENT '书名', author VARCHAR(100) NOT NULL COMMENT '作者', publisher VARCHAR(100) COMMENT '出版社', publish_date DATE COMMENT '出版日期', price DECIMAL(8,2) COMMENT '定价', category ENUM('文学','科技','教育','少儿','艺术') DEFAULT '教育' COMMENT '分类', stock INT DEFAULT 0 COMMENT '库存数量', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 创建借阅记录表,外键关联 isbn CREATE TABLE borrow_record ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(17) NOT NULL COMMENT '关联图书 ISBN', reader_id VARCHAR(20) NOT NULL COMMENT '读者学号/工号', borrow_date DATE NOT NULL COMMENT '借阅日期', return_date DATE COMMENT '归还日期,NULL 表示未归还', FOREIGN KEY (isbn) REFERENCES book(isbn) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;提示:
ENUM类型在 MySQL 中性能优于VARCHAR,且天然限制非法值(如插入'游戏'会报错),比在 Servlet 层做 if-else 校验更可靠。
3.2 Servlet 层:用@WebServlet注解替代web.xml配置的实操细节
Tomcat 7+ 支持注解式 Servlet 注册,比 XML 配置更直观。但新手常忽略两个关键点:
@WebServlet("/book/add")中的路径是相对于应用根目录(即http://localhost:8081/your-app/book/add),不是相对于WEB-INF;doPost()方法内,request.setCharacterEncoding("UTF-8")必须在getParameter()之前调用,否则中文参数乱码。
以下是一个完整的图书添加 Servlet 示例(含异常处理):
@WebServlet("/book/add") public class BookAddServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 强制设置请求编码(关键!放在 getParameter 前) request.setCharacterEncoding("UTF-8"); // 2. 获取表单参数(name 必须与 JSP 中 input 的 name 一致) String isbn = request.getParameter("isbn"); String title = request.getParameter("title"); String author = request.getParameter("author"); String publisher = request.getParameter("publisher"); String publishDate = request.getParameter("publish_date"); String priceStr = request.getParameter("price"); String category = request.getParameter("category"); String stockStr = request.getParameter("stock"); // 3. 参数校验(业务逻辑入口) if (isbn == null || isbn.trim().isEmpty()) { request.setAttribute("error", "ISBN 不能为空"); request.getRequestDispatcher("/book/add.jsp").forward(request, response); return; } // 4. 数据库操作(此处简化为调用 DAO) BookDAO dao = new BookDAO(); try { Book book = new Book(); book.setIsbn(isbn); book.setTitle(title); book.setAuthor(author); book.setPublisher(publisher); book.setPublishDate(LocalDate.parse(publishDate)); book.setPrice(new BigDecimal(priceStr)); book.setCategory(category); book.setStock(Integer.parseInt(stockStr)); dao.insert(book); // 5. 成功后重定向,避免重复提交 response.sendRedirect(request.getContextPath() + "/book/list.jsp"); } catch (Exception e) { e.printStackTrace(); request.setAttribute("error", "保存失败:" + e.getMessage()); request.getRequestDispatcher("/book/add.jsp").forward(request, response); } } }逻辑说明:
response.sendRedirect()是关键——它让浏览器发起新 GET 请求,避免用户刷新时重复提交表单。而forward()仅用于服务端跳转,URL 不变,适合带错误信息返回原页面。
3.3 JSP 页面:用 JSTL 替代 scriptlet 的现代写法,但保留request.getAttribute()的原始能力
JSP 不该写<% out.print("Hello"); %>这种脚本片段(scriptlet),既难维护又易 XSS。正确做法是:
- 用 JSTL 标签库处理循环、条件(
<c:forEach>、<c:if>); - 用 EL 表达式取值(
${book.title}); - 但
request.setAttribute("books", list)这种底层 API 必须保留——因为 JSTL 无法替代它传递数据。
book/list.jsp示例(含分页与分类筛选):
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <title>图书列表</title> <meta charset="UTF-8"> </head> <body> <h2>全部图书</h2> <!-- 分类筛选表单 --> <form action="${pageContext.request.contextPath}/book/list" method="get"> <label>按分类筛选:</label> <select name="category"> <option value="">全部</option> <option value="文学" ${param.category == '文学' ? 'selected' : ''}>文学</option> <option value="科技" ${param.category == '科技' ? 'selected' : ''}>科技</option> <option value="教育" ${param.category == '教育' ? 'selected' : ''}>教育</option> </select> <button type="submit">筛选</button> </form> <!-- 图书表格 --> <table border="1"> <tr> <th>ISBN</th><th>书名</th><th>作者</th><th>出版社</th><th>价格</th><th>库存</th><th>操作</th> </tr> <c:forEach items="${books}" var="book"> <tr> <td>${book.isbn}</td> <td>${book.title}</td> <td>${book.author}</td> <td>${book.publisher}</td> <td>¥${book.price}</td> <td>${book.stock}</td> <td> <a href="${pageContext.request.contextPath}/book/detail.jsp?isbn=${book.isbn}">详情</a> | <a href="${pageContext.request.contextPath}/book/edit.jsp?isbn=${book.isbn}">编辑</a> </td> </tr> </c:forEach> </table> <c:if test="${empty books}"> <p>暂无图书</p> </c:if> </body> </html>参数说明:
${pageContext.request.contextPath}动态获取应用上下文路径(如/book-system),避免硬编码;${param.category}直接读取 GET 请求参数,实现无状态筛选。
4. 避坑指南:JSP+Servlet 项目中最常翻车的 4 个场景及根因修复
这套技术栈看似简单,但每个环节都有“表面正常、实际埋雷”的陷阱。以下是我在线上系统维护中踩过的真坑,按发生频率排序,每条都附带现象、根因和可立即执行的修复命令。
4.1 现象:JSP 页面显示中文乱码,但数据库和控制台日志都是正常的
原因:JSP 文件本身编码是 GBK(Windows 记事本默认),而 Tomcat 默认用 UTF-8 解析 JSP,导致pageEncoding与文件实际编码不匹配。
解决:
- 用 VS Code 或 Notepad++ 将所有
.jsp文件另存为 UTF-8 无 BOM 格式; - 在 JSP 顶部强制声明编码:
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>; - 在
web.xml中全局配置(Tomcat 8.5+ 可选):
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config>4.2 现象:<c:forEach>循环不执行,表格空白,但System.out.println(list.size())输出 5
原因:JSTL 标签库未正确引入,或web.xml的web-app版本低于 2.4(JSTL 1.2 要求 Servlet 2.4+)。
解决:
- 确认
WEB-INF/lib下有jstl-1.2.jar和standard-1.1.2.jar(或 Maven 依赖jstl:jstl:1.2); - 检查
web.xml第一行是否为:
<?xml version="1.0" encoding="UTF-8"?> <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_3_1.xsd" version="3.1">注意:
version="3.1"是关键,2.5 或 2.4 也能用 JSTL,但 3.1 更稳妥。
4.3 现象:BookAddServlet报ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:MySQL 8.0+ 驱动类名从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver,但旧教程仍教前者。
解决:
- 下载
mysql-connector-java-8.0.33.jar(非 5.x 版本); - 放入
WEB-INF/lib目录; - 在
BookDAO.java中使用新驱动:
// ✅ 正确 Class.forName("com.mysql.cj.jdbc.Driver"); // ❌ 错误(MySQL 8.0+ 已废弃) // Class.forName("com.mysql.jdbc.Driver");- JDBC URL 必须加
serverTimezone=UTC参数:
String url = "jdbc:mysql://localhost:3306/bookdb?useSSL=false&serverTimezone=UTC";4.4 现象:Tomcat 启动后访问http://localhost:8081/book-system返回 404,但http://localhost:8081/能看到 Tomcat 首页
原因:WAR 包未正确部署,或应用上下文路径(Context Path)未匹配。
解决:
- 确认 WAR 包名是
book-system.war(非book.war或book-system-1.0.war); - 将 WAR 包放入
webapps/目录后,等待 Tomcat 自动解压(观察webapps/book-system/目录是否生成); - 若手动解压,确保
webapps/book-system/WEB-INF/web.xml存在且格式正确; - 检查
conf/server.xml中<Host>节点是否有<Context path="/book-system" docBase="book-system" />—— 通常不需要,Tomcat 会自动推断。
5. 进阶技巧:用 Filter 实现统一登录校验,以及 JSP 页面加载后自动刷新的务实方案
系统做到能用只是起点,真正交付给管理员用,还得解决两个高频需求:一是所有后台页面(如/book/add.jsp)必须登录才能访问;二是图书列表页首次加载后,需自动刷新一次以同步最新库存——这不是炫技,而是业务刚需。
5.1 用LoginFilter拦截未登录请求,比在每个 Servlet 里写 if 判断更可靠
Filter 是 Servlet 规范提供的拦截器,它在请求到达 Servlet 前执行,天然适合做权限校验。关键在于:
chain.doFilter()是放行点,必须调用,否则请求中断;HttpSession的getAttribute("user")为空时,应重定向到登录页,而非返回 403;excludeUrls列表必须包含/login.jsp和/LoginServlet,否则形成死循环。
LoginFilter.java实现如下:
@WebFilter(filterName = "LoginFilter", urlPatterns = "/*") public class LoginFilter implements Filter { private static final List<String> EXCLUDE_URLS = Arrays.asList( "/login.jsp", "/LoginServlet", "/css/", "/js/", "/images/" ); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 1. 排除静态资源和登录相关路径 boolean shouldExclude = EXCLUDE_URLS.stream() .anyMatch(exclude -> uri.equals(exclude) || uri.startsWith(exclude)); if (shouldExclude) { chain.doFilter(request, response); return; } // 2. 检查 session 中是否有 user 对象 HttpSession session = req.getSession(false); if (session == null || session.getAttribute("user") == null) { // 未登录,重定向到登录页,并保存原请求路径(便于登录后跳回) String originalUrl = req.getRequestURL().toString(); if (req.getQueryString() != null) { originalUrl += "?" + req.getQueryString(); } session.setAttribute("originalUrl", originalUrl); resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 3. 已登录,放行 chain.doFilter(request, response); } }注意:
req.getSession(false)中的false表示“不创建新 session”,避免未登录用户也生成 session ID,浪费内存。
5.2 JSP 页面加载完后刷新一次:用window.onload+location.reload()的轻量方案
需求场景:管理员在/book/list.jsp页面点击“新增图书”,跳转到/book/add.jsp提交成功后,response.sendRedirect("/book/list.jsp")会回到列表页,但此时页面显示的库存仍是旧值(因为浏览器缓存了上一次响应)。解决方案不是禁用缓存(影响性能),而是让页面加载完成后主动刷新一次:
<%-- 在 book/list.jsp 底部添加 --%> <script> window.onload = function() { // 检查 URL 是否带 reload=1 参数,避免无限刷新 if (!window.location.search.includes('reload=1')) { window.location.href = window.location.href + (window.location.search ? '&reload=1' : '?reload=1'); } }; </script>这段 JS 的逻辑是:
- 页面首次加载时,URL 不含
reload=1,于是拼接该参数并跳转; - 第二次加载时,URL 已含
reload=1,includes()返回 true,不再跳转; - 整个过程对用户无感,仅多一次 HTTP 请求,但保证了数据实时性。
5.3 一个我坚持十年的习惯:每次修改 JSP 后,必用curl -I检查响应头
JSP 页面的问题,80% 能从 HTTP 响应头里看出端倪。比如:
Content-Type: text/html;charset=ISO-8859-1→ 说明pageEncoding未生效;Cache-Control: max-age=3600→ 说明浏览器可能缓存了旧版 JSP;Set-Cookie: JSESSIONID=xxx; Path=/; HttpOnly→ 确认 session 已创建。
我日常调试的命令是:
# 检查首页响应头(替换为你的实际路径) curl -I http://localhost:8081/book-system/ # 检查某个 JSP 的 Content-Type curl -I http://localhost:8081/book-system/book/list.jsp如果看到charset=ISO-8859-1,立刻回去检查 JSP 的pageEncoding和文件保存编码;如果Set-Cookie缺失,说明 Filter 或 Session 配置有问题。这比盯着浏览器 F12 的 Network 面板更快定位根因。
希望帮到你。
本文还有配套的精品资源,点击获取