ARTICLE DETAIL

资讯详情

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

Java+JSP+Mysql教材管理系统:老技术栈如何跑通Web开发全链路

Java+JSP+Mysql教材管理系统:老技术栈如何跑通Web开发全链路 简介这是一套面向计算机专业学生与Java Web初学者、课程设计或毕业设计开发者的学校教材管理系统源码基于Java、JSP与MySQL构建运行于Tomcat环境帮助读者理解如何用经典技术栈完成一套可运行的教务类Web应用。压缩包共81个文件约3.91MB包含26个jsp页面、22个class与11个java源文件、10个css样式、8个xml配置、2个jar依赖以及1个sql建库脚本覆盖前端页面、后端逻辑、数据访问与数据库初始化等完整环节。系统围绕管理员登录认证与教材增删改查展开涉及用户权限控制、SQL操作、MVC分层设计以及输入校验、异常处理与防注入等安全考量目录结构清晰便于按模块阅读与二次开发。目前已有1229人学习下载适合作为课程实践参考或项目脚手架快速掌握JSPMySQL的整合开发流程。1. JavaJSPMysql 教材管理系统为什么“老技术栈”反而更适合第一次做 Web 项目如果你正在搜“基于 JSP 的毕设选题”或者刚学完 Java 基础、JSP 入门想找一个能跑通、能讲清楚、能写进简历的 Web 项目那这套 JavaJSPMysql 的学校教材管理系统大概率比 Spring Boot 更适合你第一次动手。原因很直接它把 Web 开发最核心的链路——浏览器发请求、Servlet 接请求、JSP 渲染页面、JDBC 读写 MySQL——全部暴露在你面前没有自动配置帮你“藏”起来。你调通一个教材入库功能就等于把 HTTP 请求、会话管理、数据库连接、SQL 参数绑定、页面转发这五件事同时走了一遍。这套系统适合两类人一是课程设计或毕设需要完整 CRUD 和权限模块的学生二是刚转 Java Web、想用一个小系统把 JSP 和 MySQL 串起来的新手。它不追求高并发但能让你真正看懂一个 Web 项目从登录到数据落库的每一步。2. 教材管理系统的功能边界与数据库设计先想清楚谁在用、存什么2.1 三类角色决定功能范围学校教材管理系统不是泛泛的“增删改查”它的业务角色很明确管理员、教师、学生。管理员负责教材基础信息维护、用户管理和库存盘点教师负责查看所授课程的教材指定情况、提交领用申请学生只能查看自己课程的教材发放记录。这个角色划分直接决定了后面 JSP 页面里 session 里存什么、每个 Servlet 进来先判断什么。我一般会先把角色和权限写成一张表避免后面页面越写越乱角色可访问模块关键操作session 中必须存的字段管理员教材管理、用户管理、库存管理新增/修改/删除教材、重置密码userId, roleadmin教师教材查询、领用申请按课程查教材、提交申请userId, roleteacher, deptId学生我的教材查看发放记录userId, rolestudent, classId这张表看起来简单但它是后面所有 JSP 页面和 Servlet 判断的“宪法”。很多新手一上来就写登录页写到一半发现教师和学生看到的教材列表不一样又回头改数据库血泪经验就是先把角色权限表定死。2.2 五张核心表撑起整个系统教材管理系统的数据库不需要几十张表五张核心表就能跑通全部流程。下面是我在实际项目中反复用到的建表语句字段类型和约束都按 MySQL 5.7/8.0 通用写法来-- 用户表三种角色共用用 role 字段区分 CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 或 SHA-256不要明文 real_name VARCHAR(50) NOT NULL, role ENUM(admin,teacher,student) NOT NULL, dept_id INT DEFAULT NULL, -- 教师所属院系 class_id INT DEFAULT NULL, -- 学生所属班级 status TINYINT DEFAULT 1 -- 1 正常 0 禁用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教材表教材基础信息 CREATE TABLE t_book ( book_id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), price DECIMAL(8,2) DEFAULT 0.00, stock INT DEFAULT 0, -- 库存数量 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表课程与教材的关联 CREATE TABLE t_course ( course_id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, book_id INT DEFAULT NULL, semester VARCHAR(20), FOREIGN KEY (teacher_id) REFERENCES t_user(user_id), FOREIGN KEY (book_id) REFERENCES t_book(book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 领用申请表教师提交管理员审批 CREATE TABLE t_apply ( apply_id INT PRIMARY KEY AUTO_INCREMENT, teacher_id INT NOT NULL, book_id INT NOT NULL, apply_num INT NOT NULL DEFAULT 1, apply_status TINYINT DEFAULT 0, -- 0 待审 1 通过 2 驳回 apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (teacher_id) REFERENCES t_user(user_id), FOREIGN KEY (book_id) REFERENCES t_book(book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 发放记录表审批通过后生成学生可查 CREATE TABLE t_distribute ( dist_id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, book_id INT NOT NULL, dist_num INT DEFAULT 1, dist_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (student_id) REFERENCES t_user(user_id), FOREIGN KEY (book_id) REFERENCES t_book(book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有两个参数必须注意。第一字符集用utf8mb4而不是utf8否则教材名称里出现生僻字或特殊符号会插入失败。第二t_user.password长度给到 64因为 SHA-256 十六进制是 64 位MD5 是 32 位留足空间以后不用改表。外键约束在开发阶段可以帮你挡住脏数据但如果后面要做批量导入可以暂时关掉FOREIGN_KEY_CHECKS导入完再打开。2.3 库存扣减放在哪一层教材领用涉及库存扣减这个逻辑不要写在 JSP 里也不要写在 Servlet 的doGet里。我一般放在 Service 层的一个方法里用synchronized或者数据库行锁保证并发安全。对于课程设计级别的项目直接用一个带事务的 Service 方法就够了public boolean applyBook(int teacherId, int bookId, int num) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查库存 String checkSql SELECT stock FROM t_book WHERE book_id? FOR UPDATE; PreparedStatement ps conn.prepareStatement(checkSql); ps.setInt(1, bookId); ResultSet rs ps.executeQuery(); if (!rs.next() || rs.getInt(stock) num) { conn.rollback(); return false; } // 2. 扣库存 String updateSql UPDATE t_book SET stockstock-? WHERE book_id?; ps conn.prepareStatement(updateSql); ps.setInt(1, num); ps.setInt(2, bookId); ps.executeUpdate(); // 3. 写申请记录 String insertSql INSERT INTO t_apply(teacher_id,book_id,apply_num) VALUES(?,?,?); ps conn.prepareStatement(insertSql); ps.setInt(1, teacherId); ps.setInt(2, bookId); ps.setInt(3, num); ps.executeUpdate(); conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (Exception ex) {} e.printStackTrace(); return false; } finally { DBUtil.close(conn); } }这段代码的关键在FOR UPDATE它会在事务提交前锁住这一行防止两个教师同时领同一本教材导致库存扣成负数。setAutoCommit(false)开启事务三步操作要么全成功要么全回滚。DBUtil.close(conn)放在 finally 里保证连接一定释放。如果你用的是 MySQL 8.0默认隔离级别是 REPEATABLE READFOR UPDATE依然有效。3. 从零搭起 JSP 项目IDEA 建项目、配 Tomcat、连 MySQL 的完整命令3.1 用 IDEA 新建 JSP 项目的正确姿势很多人搜“idea 新建 jsp 项目”结果建出来的是 Maven 项目但不会配 webapp 目录。我一般直接用 IDEA 的 Java Enterprise 模板步骤比 Maven 少适合第一次跑通。打开 IDEAFile → New → Project左侧选 Java Enterprise勾选 Web Application确保 Application Server 里已经配好 Tomcat。如果没有 Tomcat先去 Apache 官网下载 Tomcat 9 或 10解压到不含中文和空格的路径然后在 IDEA 里点 New → Tomcat Server 指向解压目录。建好项目后目录结构应该是这样src/ main/ java/ -- 放 Servlet、Service、Dao webapp/ -- 放 JSP、WEB-INF、静态资源 WEB-INF/ web.xml -- 部署描述符 index.jsp如果webapp目录没有自动生成手动在src/main下新建webapp然后在 Project Structure → Facets 里把 Web Resource Directory 指过去。这一步不做Tomcat 启动后会报 404。3.2 配置 MySQL 连接池别每次请求都 DriverManager新手最容易犯的错是在每个 Servlet 里写DriverManager.getConnection()请求一多数据库就崩。正确做法是用连接池。课程设计级别用 Druid 或 C3P0 都行我一般用 Druid配置简单。在src/main/resources下新建druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/textbook_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的密码 initialSize5 maxActive20 maxWait3000注意serverTimezoneAsia/Shanghai必须加否则 MySQL 8.0 会报时区错误。useSSLfalse在本地开发时关掉避免 SSL 连接错误。然后写一个DBUtil工具类public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn) { if (conn ! null) try { conn.close(); } catch (SQLException e) {} } }static块保证连接池只初始化一次。getConnection()从池里拿连接close()是归还而不是真正关闭。这样即使 100 个请求同时进来也只会用 20 个数据库连接。3.3 登录 Servlet 与 JSP 页面转发登录是第一个要跑通的功能。写一个LoginServlet继承HttpServlet在doPost里处理WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // 密码做 SHA-256 摘要 String hash DigestUtils.sha256Hex(password); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, hash); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(user, user); session.setAttribute(role, user.getRole()); // 按角色跳不同页面 if (admin.equals(user.getRole())) { resp.sendRedirect(admin/book_list.jsp); } else if (teacher.equals(user.getRole())) { resp.sendRedirect(teacher/apply.jsp); } else { resp.sendRedirect(student/my_book.jsp); } } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }req.getSession()创建或获取当前会话把 user 对象和 role 存进去后面每个 JSP 页面用session.getAttribute(role)判断权限。sendRedirect是重定向浏览器地址栏会变forward是转发地址栏不变。登录失败用 forward 把错误信息带回 login.jsp登录成功用 redirect 防止刷新重复提交。对应的login.jsp里表单 action 写loginmethod 写post密码框 type 写password。提交前可以用 JavaScript 做非空校验但服务端必须再校验一次因为前端校验可以被绕过。4. 教材管理核心模块分页查询、文件上传、审批流的 JSP 实现4.1 教材列表分页SQL LIMIT 与 JSP 页码条教材列表不能一次查全部必须分页。MySQL 用LIMIT offset, sizeoffset 是起始行size 是每页条数。先查总数再查当前页数据public ListBook findByPage(int page, int size) { ListBook list new ArrayList(); String sql SELECT * FROM t_book ORDER BY book_id DESC LIMIT ?,?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); ps.setInt(2, size); ResultSet rs ps.executeQuery(); while (rs.next()) { Book b new Book(); b.setBookId(rs.getInt(book_id)); b.setBookName(rs.getString(book_name)); b.setAuthor(rs.getString(author)); b.setStock(rs.getInt(stock)); list.add(b); } } catch (Exception e) { e.printStackTrace(); } return list; }(page - 1) * size是 offset 计算公式page 从 1 开始。JSP 页面里用 JSTL 的c:forEach渲染表格页码条根据总页数循环生成链接。注意LIMIT后面的参数用setInt绑定不要用字符串拼接否则有 SQL 注入风险。4.2 教材封面上传Commons FileUpload 的配置与坑教材信息里通常要传封面图。JSP 表单必须加enctypemultipart/form-dataServlet 里用MultipartConfig注解WebServlet(/book/upload) MultipartConfig(maxFileSize 5 * 1024 * 1024, maxRequestSize 10 * 1024 * 1024) public class BookUploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part filePart req.getPart(cover); String fileName filePart.getSubmittedFileName(); // 防止重名覆盖 String newName UUID.randomUUID() _ fileName; String savePath getServletContext().getRealPath(/upload); File dir new File(savePath); if (!dir.exists()) dir.mkdirs(); filePart.write(savePath File.separator newName); // 把 newName 存回数据库 resp.getWriter().write(upload ok: newName); } }maxFileSize限制单个文件 5MBmaxRequestSize限制整个请求 10MB。getRealPath(/upload)拿到的是 Tomcat 部署目录下的路径项目重新部署后文件会丢。生产环境应该存到项目外的固定目录数据库只存文件名。UUID.randomUUID()防止同名文件互相覆盖。4.3 教师领用申请与管理员审批的状态流转教师提交申请后t_apply.apply_status为 0。管理员在审批页面看到待审列表点“通过”后状态改为 1同时扣减库存并生成发放记录。这个流转用一条 SQL 加事务完成START TRANSACTION; UPDATE t_apply SET apply_status1 WHERE apply_id? AND apply_status0; -- 检查影响行数如果为 0 说明已被处理过 UPDATE t_book SET stockstock-? WHERE book_id? AND stock?; INSERT INTO t_distribute(student_id,book_id,dist_num) SELECT u.user_id, a.book_id, a.apply_num FROM t_apply a JOIN t_user u ON u.class_id (SELECT class_id FROM t_user WHERE user_ida.teacher_id) WHERE a.apply_id?; COMMIT;apply_status0作为条件放在 UPDATE 里防止重复审批。stock?防止库存扣成负数。发放记录用INSERT ... SELECT批量生成把该教师所带班级的学生都插一条记录。如果班级人数多这条 SQL 会慢可以改成先查学生列表再批量插入。5. 避坑与排查JSPMySQL 项目最容易翻车的五个地方5.1 中文乱码从 Tomcat 到 MySQL 的完整链路现象教材名称在 JSP 页面显示正常插入数据库变成问号或者数据库里正常但页面显示乱码。原因乱码可能出现在三个环节——JSP 页面编码、请求编码、数据库连接编码。任何一个环节不是 UTF-8 都会出问题。解决JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 里在doPost第一行加req.setCharacterEncoding(UTF-8)数据库连接 URL 加characterEncodingutf8mb4建表时用DEFAULT CHARSETutf8mb4。四个地方全部对齐乱码基本消失。如果用的是 Tomcat 8 以上GET 请求默认 UTF-8但 POST 必须手动设。5.2 ClassNotFoundException: com.mysql.cj.jdbc.Driver现象Tomcat 启动时报ClassNotFoundException找不到 MySQL 驱动。原因MySQL 驱动 jar 包没有放到WEB-INF/lib目录下。IDEA 里在 Project Structure → Libraries 加了 jar 包但部署到 Tomcat 时没有复制过去。解决把mysql-connector-java-8.0.xx.jar直接复制到webapp/WEB-INF/lib目录下IDEA 会自动识别。或者用 Maven 的dependency并设置scopecompile/scope打包时自动进 lib。注意 MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver不是com.mysql.jdbc.Driver写错也会报这个错。5.3 JSP 页面报 404web.xml 版本与 Servlet 映射现象Tomcat 启动正常访问login.jsp可以但提交到login报 404。原因WebServlet(/login)注解没有被扫描到或者web.xml的metadata-completetrue导致注解失效。解决检查web.xml的web-app标签version属性至少是 3.0metadata-complete不要设为 true。如果用的是 Servlet 4.0web.xml可以完全不写但WEB-INF目录必须存在。另外Servlet 类必须放在src/main/java下并且有包名不能放在默认包。5.4 数据库连接池耗尽Connection not available现象系统跑一段时间后报GetConnectionTimeoutException页面卡死。原因Dao 层拿了连接没有关闭或者关闭写在了 try 里面而不是 finally 里一旦抛异常连接就泄漏了。解决所有Connection、PreparedStatement、ResultSet必须在 finally 里关闭或者用 try-with-resources 语法。Druid 连接池的maxActive不要设太大课程设计 20 足够。可以在druid.properties里加removeAbandonedtrue和removeAbandonedTimeout300让池自动回收超过 5 分钟没归还的连接但这只是后悔药根本还是要写对关闭逻辑。5.5 session 失效后操作报空指针现象用户登录后放置一段时间再点任何按钮都报NullPointerException。原因session 默认 30 分钟过期过期后session.getAttribute(user)返回 null后面直接调用user.getRole()就空指针。解决在每个需要登录的 Servlet 开头加判断HttpSession session req.getSession(false); if (session null || session.getAttribute(user) null) { resp.sendRedirect(login.jsp); return; }getSession(false)表示不自动创建新 session没有就返回 null。JSP 页面里可以用一个公共的checkLogin.jsp用% include %引入统一做拦截。6. 让教材管理系统更像真实项目三个进阶技巧与验证方法第一个技巧是把 JSP 里的 Java 代码降到最少。新手容易在 JSP 里写% Class.forName(...) %直接查数据库这样页面和数据库耦合太紧改一个字段要动好几个文件。我一般用 Servlet 查好数据req.setAttribute(bookList, list)JSP 里只用 JSTL 的c:forEach渲染。这样 JSP 只负责展示逻辑全在 Java 里调试时看日志就能定位问题。第二个技巧是给所有列表页加一个统一的查询条件构造器。教材管理里经常要按 ISBN、书名、作者、出版社组合查询如果每个 Servlet 都写一遍if (isbn ! null !isbn.isEmpty())会疯掉。我一般写一个BookQuery类用 StringBuilder 拼 WHERE 子句参数用 List 收集最后统一setObject。这样新增查询条件只需要改一个类。第三个技巧是验证方法。课程设计答辩时老师常问“你怎么保证库存不会扣成负数”。你可以现场演示开两个浏览器用两个教师账号同时领同一本库存为 1 的教材一个成功一个失败。这个演示比任何文字说明都有说服力。具体做法是在applyBook方法里加日志打印FOR UPDATE前后的库存值观察第二个请求是否阻塞到第一个事务提交后才执行。验证项操作预期结果并发扣库存两个浏览器同时提交领用只有一个成功库存不为负权限拦截学生账号直接访问 admin 页面跳转登录页或提示无权限分页边界访问 page0 或 page999返回空列表或第一页不报错文件上传上传 6MB 文件提示超过大小限制session 过期登录后等 31 分钟再操作跳转登录页不报空指针最后说一个我自己的习惯每次改完 Dao 层的 SQL先在 Navicat 或 MySQL Workbench 里把 SQL 粘进去跑一遍确认结果对了再写进 Java。因为 Java 里调试 SQL 要重启 Tomcat慢。直接在数据库客户端里调改一个字段立刻看结果效率高很多。这个项目不大但把 JSP、Servlet、JDBC、连接池、事务、分页、文件上传、权限拦截全串了一遍做完之后再看 Spring Boot你会发现它只是帮你把这些步骤自动化了底层逻辑一模一样。希望帮到你。本文还有配套的精品资源点击获取
返回列表