纯Java Web图书系统:Servlet+JDBC手写全链路实战

发布时间:2026/11/2 15:33:17
纯Java Web图书系统:Servlet+JDBC手写全链路实战 简介本资源是《Java Web数据库系统应用开发与实例》配套学习包面向Java Web初学者及中级开发者聚焦Web层与数据库的协同开发实战解决Servlet/JSP基础薄弱、JDBC连接不熟、MVC项目结构不清等典型痛点。压缩包共884个文件含163个Java源码覆盖Servlet、DAO、Service等分层逻辑、140个JSP页面含登录、列表、增删改查等完整视图、181个class字节码、48个jar依赖库、17个SQL脚本含建表、初始化及事务示例以及48个AVI视频——涵盖系统演示、共通Servlet设计、报表打印、Servlet原理讲解等核心实操录屏总大小222.5MB。已有471人学习下载资源结构清晰视频与源码严格对应支持边看边练所有JSP页面均含注释SQL脚本可直接导入MySQL执行配套XML配置与properties参数文件便于快速部署到Tomcat环境是贯通Java Web全栈开发流程的高实用性入门套件。1. 这不是又一个学生管理系统它用纯 Java Web JDBC 跑通了从登录到报表导出的完整业务闭环连分页、文件上传、SQL 注入防护都写在源码里某高校课程设计答辩现场A同学演示完“图书借阅系统”后被连续追问三个问题登录失败时错误信息为什么没做脱敏借书记录导出 Excel 的模板是硬编码还是可配置管理员删用户前有没有二次确认操作日志——他当场翻出本项目源码包里的LoginServlet.java、ExportExcelServlet.java和UserDeleteServlet.java三处代码逐行指给老师看异常捕获后统一跳转到/error.jsp并记录log4j日志Excel 模板路径从config.properties读取删除前校验 session 权限 插入sys_operation_log表。这不是 Demo是能直接部署进实训机房、经得起学生反复压测的真实 Web 应用骨架。它不依赖 Spring Boot 自动装配不包装 MyBatis 封装层所有 JDBC 连接、PreparedStatement 参数绑定、ResultSet 映射、事务控制全手写——适合刚学完 Servlet 生命周期、正卡在“怎么把数据库查出来的数据塞进 JSP 表格里”这一步的新手也适合想带学生抠透“一次 HTTP 请求背后到底发生了多少次对象创建和资源释放”的实训导师。源码含完整视频讲解含 Tomcat 部署、MySQL 建库脚本、JDBC 驱动版本踩坑不是 PPT 录屏是真实敲键盘录屏。2. 环境准备与项目结构解析从 Tomcat 9.0.83 到 web.xml 的 7 个关键配置项2.1 JDK 8 与 Tomcat 9 的兼容性实测要点本项目基于 JDK 8u202 编译source1.8,target1.8必须使用 Tomcat 9.0.83 或更低版本。Tomcat 10 因 Jakarta EE 命名空间迁移javax.servlet.*→jakarta.servlet.*会导致所有web.xml中的servlet-class报ClassNotFoundException。实测中若误用 Tomcat 10.1.15启动时控制台会抛出SEVERE [main] org.apache.catalina.startup.VersionLoggerListener.log Failed to initialize component [StandardServer[8005]] java.lang.NoClassDefFoundError: javax/servlet/Servlet解决方法下载 Tomcat 9.0.83官方 archive 版本解压后修改conf/server.xml中Connector port8080为port8081避免与本地其他服务冲突再将项目WebContent目录整体复制到webapps/下重命名为booksystem。2.2 项目目录结构src下的 4 个包与WebContent的 3 类资源项目采用经典 Java Web 分层结构无 Maven纯手动依赖管理booksystem/ ├── src/ │ ├── dao/ # 数据访问层UserDao.java, BookDao.java 等 │ ├── entity/ # 实体类User.java, Book.java含 getter/setter toString │ ├── servlet/ # 控制层LoginServlet.java, AddBookServlet.java │ └── util/ # 工具类DBUtil.java封装 getConnection/close、DateUtil.java ├── WebContent/ │ ├── css/ # bootstrap.min.cssv3.3.7 自定义 style.css │ ├── js/ # jquery-1.12.4.min.js validate.js表单校验 │ ├── images/ # 图标、背景图 │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置7 个 servlet 映射 2 个 filter │ │ └── lib/ # 4 个 jarmysql-connector-java-5.1.47.jar, log4j-1.2.17.jar, poi-3.17.jar, commons-fileupload-1.3.3.jar │ ├── index.jsp # 首页未登录跳转页 │ ├── login.jsp # 登录表单含验证码图片生成逻辑 │ └── admin/ # 后台页面book_list.jsp, user_manage.jsp 等提示WEB-INF/lib/中的poi-3.17.jar是 Excel 导出的关键依赖其org.apache.poi.ss.usermodel.Workbook接口被ExportExcelServlet.java直接调用。若替换为 poi-5.x 版本HSSFWorkbook构造函数签名变更需同步修改ExportExcelServlet.java第 42 行代码。2.3 web.xml 的 7 处核心配置解析web.xml是本项目的“总开关”以下 7 项配置决定请求能否正确路由字符编码 FilterCharacterEncodingFilter强制request.setCharacterEncoding(UTF-8)解决中文参数乱码登录拦截器LoginFilter对/admin/*路径做 session 校验未登录则重定向至/login.jspServlet 映射LoginServlet绑定/loginAddBookServlet绑定/addBook注意 URL 模式必须以/开头如url-pattern/login/url-pattern否则 Tomcat 无法识别欢迎文件列表welcome-file-list中index.jsp为首页但实际业务入口是login.jsp错误页面配置error-page将 HTTP 404/500 映射到/error.jsp该页面通过${exception.message}输出异常摘要上下文参数context-param定义db.driver和db.url供DBUtil.java读取监听器ContextLoaderListener无实际作用未配 Spring但保留此配置是为后续扩展预留接口。2.4 MySQL 数据库初始化建库、建表、插入测试数据三步到位项目附带sql/book_system.sql脚本执行前需确保 MySQL 5.7 服务运行-- 1. 创建数据库指定 UTF8MB4 支持 emoji CREATE DATABASE booksystem CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 执行建表语句关键字段注释已加在源码 SQL 中 USE booksystem; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存储 BCrypt 加密后的密文 role ENUM(admin,user) DEFAULT user, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 3. 插入初始管理员账号密码为 123456 经 BCrypt 加密 INSERT INTO users (username, password, role) VALUES (admin, $2a$10$ZzZzZzZzZzZzZzZzZzZzZuZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZ, admin);注意password字段值是BCryptPasswordEncoder.encode(123456)的结果非明文。若需重置密码用在线 BCrypt 工具生成新密文后更新数据库。3. 核心模块拆解LoginServlet 与 DBUtil 的 3 层防御链3.1 LoginServlet从 request.getParameter() 到 session.setAttribute() 的完整链路LoginServlet.java是整个系统的入口守门人其doPost()方法实现三层防御protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 第一层基础参数校验空值防御 String username request.getParameter(username).trim(); String password request.getParameter(password); String verifyCode request.getParameter(verifyCode); if (username.isEmpty() || password null || verifyCode null) { request.setAttribute(msg, 用户名或密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; // 立即终止防止空指针 } // 第二层验证码比对Session 中存储的验证码 String sessionCode (String) request.getSession().getAttribute(verifyCode); if (!verifyCode.equalsIgnoreCase(sessionCode)) { request.setAttribute(msg, 验证码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 第三层数据库校验调用 UserDao UserDao userDao new UserDao(); User user userDao.findByUsername(username); // 查询用户是否存在 if (user null || !BCrypt.checkpw(password, user.getPassword())) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 成功后设置 Session 并跳转 request.getSession().setAttribute(user, user); response.sendRedirect(request.getContextPath() /admin/index.jsp); }关键点说明request.getParameter(username).trim().trim()防止用户输入空格导致登录失败这是新手常漏的细节verifyCode.equalsIgnoreCase(sessionCode)忽略大小写比对提升用户体验BCrypt.checkpw()使用bcrypt算法验证密码而非password.equals()杜绝明文比对风险response.sendRedirect()重定向到后台首页避免 F5 刷新重复提交登录请求。3.2 DBUtil连接池缺失下的 5 个资源释放保障本项目未使用 Druid/HikariCP而是通过DBUtil.java手动管理 Connectionpublic class DBUtil { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/booksystem?useSSLfalseserverTimezoneUTC; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); // 驱动加载仅执行一次 } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL Driver not found, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关键closeAll() 方法确保 ResultSet/Statement/Connection 全部关闭 public static void closeAll(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) try { rs.close(); } catch (SQLException e) { /* 忽略 */ } if (stmt ! null) try { stmt.close(); } catch (SQLException e) { /* 忽略 */ } if (conn ! null) try { conn.close(); } catch (SQLException e) { /* 忽略 */ } } }为什么必须写closeAll()若只关闭ConnectionResultSet和Statement可能因 GC 延迟未释放导致 MySQL 连接数耗尽报错Too many connectionstry-catch中忽略SQLException是合理做法关闭失败不影响业务逻辑且日志已由log4j记录static块加载驱动避免每次getConnection()都重复加载提升性能。3.3 UserDaoPreparedStatement 防注入的 2 种写法对比UserDao.java中findByUsername()方法展示两种 SQL 写法// ✅ 正确使用 PreparedStatement 占位符防 SQL 注入 public User findByUsername(String username) { String sql SELECT id, username, password, role FROM users WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); // 参数绑定自动转义单引号 try (ResultSet rs pstmt.executeQuery()) { if (rs.next()) { return new User(rs.getInt(id), rs.getString(username), rs.getString(password), rs.getString(role)); } } } catch (SQLException e) { log.error(Query user by username failed, e); } return null; } // ❌ 错误字符串拼接高危 public User findByUsernameBad(String username) { String sql SELECT * FROM users WHERE username username ; // 若 usernameadmin OR 11则绕过认证 // ... 后续执行 }血泪经验某次学生调试时把pstmt.setString(1, username)误写成pstmt.setString(0, username)导致IndexOutOfBoundsException。原因PreparedStatement 参数索引从 1 开始不是 03.4 避坑 / 常见问题 / 排查 / 注意部署阶段的 4 个高频翻车点现象 1登录页面显示 “HTTP Status 404 – Not Found”原因Tomcat 启动后webapps/booksystem/目录下缺少WEB-INF/web.xml或web.xml中servlet-mapping的url-pattern写成了/login.do而前端 form action 是/login。解决检查webapps/booksystem/WEB-INF/web.xml是否存在确认url-pattern与 JSP 中action属性完全一致区分大小写。现象 2登录成功后跳转到空白页地址栏显示/booksystem/admin/index.jsp原因admin/index.jsp中% page contentTypetext/html;charsetUTF-8 %缺失或response.setContentType(text/html;charsetUTF-8)未设置导致浏览器用 GBK 解析 UTF-8 页面。解决在admin/index.jsp顶部添加% page contentTypetext/html;charsetUTF-8 %并确保web.xml中CharacterEncodingFilter已启用。现象 3Excel 导出文件打开提示 “文件格式与扩展名不匹配”原因ExportExcelServlet.java中response.setHeader(Content-Disposition, attachment;filenamebooklist.xls)的filename值含中文如filename图书列表.xlsIE 浏览器无法解析。解决对中文文件名进行 URL 编码String fileName URLEncoder.encode(图书列表.xls, UTF-8); response.setHeader(Content-Disposition, attachment;filename fileName);现象 4添加图书时上传封面图片失败控制台报java.lang.NoClassDefFoundError: org/apache/commons/fileupload/FileItem原因WEB-INF/lib/中缺少commons-fileupload-1.3.3.jar或版本不匹配如用了 1.5.0 版本但源码调用的是 1.3.3 的 API。解决严格使用项目自带的commons-fileupload-1.3.3.jar删除其他版本检查AddBookServlet.java第 68 行是否为ListFileItem items upload.parseRequest(request)1.3.3 写法。4. 前端交互与安全加固JSP jQuery 的 3 个实战技巧4.1 login.jsp 的验证码生成不用第三方库纯 Servlet 实现验证码图片由VerifyCodeServlet.java生成核心逻辑protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 创建 BufferedImage120x40 像素 BufferedImage image new BufferedImage(120, 40, BufferedImage.TYPE_INT_RGB); Graphics2D g image.createGraphics(); // 2. 填充背景色浅灰色 g.setColor(new Color(230, 230, 230)); g.fillRect(0, 0, 120, 40); // 3. 生成 4 位随机字符数字大写字母 String code ; String chars 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ; Random random new Random(); for (int i 0; i 4; i) { char c chars.charAt(random.nextInt(chars.length())); code c; } request.getSession().setAttribute(verifyCode, code); // 存入 Session // 4. 绘制干扰线2 条 g.setColor(Color.GRAY); for (int i 0; i 2; i) { int x1 random.nextInt(120), y1 random.nextInt(40); int x2 random.nextInt(120), y2 random.nextInt(40); g.drawLine(x1, y1, x2, y2); } // 5. 绘制字符每个字符颜色、角度随机 g.setFont(new Font(Arial, Font.BOLD, 22)); for (int i 0; i code.length(); i) { g.setColor(new Color(random.nextInt(100), random.nextInt(100), random.nextInt(100))); g.rotate(random.nextDouble() * 0.2 - 0.1, 20 i * 25, 25); g.drawString(code.charAt(i) , 20 i * 25, 25); g.rotate(-random.nextDouble() * 0.2 0.1, 20 i * 25, 25); } // 6. 输出图片 response.setContentType(image/jpeg); ImageIO.write(image, jpg, response.getOutputStream()); }玄学细节g.rotate()后必须g.rotate()反向恢复否则后续字符位置错乱response.setContentType(image/jpeg)必须在ImageIO.write()之前设置否则浏览器无法识别 MIME 类型。4.2 book_list.jsp 的分页实现不依赖 PageHelper手写 LIMIT 逻辑分页由BookDao.java的findBooksByPage(int pageNum, int pageSize)完成public ListBook findBooksByPage(int pageNum, int pageSize) { int offset (pageNum - 1) * pageSize; // 计算偏移量 String sql SELECT * FROM books ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, offset); pstmt.setInt(2, pageSize); try (ResultSet rs pstmt.executeQuery()) { ListBook list new ArrayList(); while (rs.next()) { list.add(new Book(rs.getInt(id), rs.getString(name), rs.getString(author), rs.getDouble(price))); } return list; } } catch (SQLException e) { log.error(Query books by page failed, e); return Collections.emptyList(); } }前端 JSP 分页导航book_list.jsp片段!-- 计算总页数 -- % int total new BookDao().getTotalCount(); // 查询 SELECT COUNT(*) FROM books int totalPages (int) Math.ceil((double) total / 10); // 每页 10 条 % !-- 生成页码链接 -- div classpagination c:forEach begin1 end${totalPages} vari a hrefbook_list.jsp?pageNum${i} class${param.pageNum i ? active : }${i}/a /c:forEach /div注意getTotalCount()方法必须单独查询COUNT(*)不能复用findBooksByPage()的 SQL否则LIMIT会截断总数。4.3 表单提交的双重校验前端 jQuery 后端 Servletadd_book.jsp使用jquery.validate.js做前端校验form idbookForm action${pageContext.request.contextPath}/addBook methodpost enctypemultipart/form-data input typetext namename required minlength2 maxlength50 input typetext nameauthor required minlength1 maxlength30 input typenumber nameprice required min0.01 max9999.99 step0.01 input typefile namecover acceptimage/* required /form script $(#bookForm).validate({ rules: { name: { required: true, minlength: 2, maxlength: 50 }, author: { required: true, minlength: 1, maxlength: 30 }, price: { required: true, min: 0.01, max: 9999.99 } }, messages: { name: 书名长度 2-50 个字符, author: 作者名不能为空 } }); /script后端 Servlet 的兜底校验AddBookServlet.java// 获取文件项后校验文件类型和大小 if (item.getFieldName().equals(cover)) { String fileName item.getName(); if (!fileName.toLowerCase().endsWith(.jpg) !fileName.toLowerCase().endsWith(.png)) { request.setAttribute(msg, 封面必须是 JPG 或 PNG 格式); request.getRequestDispatcher(/admin/add_book.jsp).forward(request, response); return; } if (item.getSize() 5 * 1024 * 1024) { // 5MB 限制 request.setAttribute(msg, 封面文件不能超过 5MB); request.getRequestDispatcher(/admin/add_book.jsp).forward(request, response); return; } }为什么需要双重校验前端校验可即时反馈提升体验后端校验不可绕过禁用 JS 后仍生效是安全底线文件类型校验必须在后端前端accept属性可被篡改。5. 二次开发实战从“图书系统”扩展为“多角色协同平台”的 3 个改造点5.1 新增角色权限控制在 User 表增加 permission 字段并重构 LoginFilter原系统只有admin/user二元角色要支持editor可编辑图书但不可删、auditor可审核借阅申请需第一步数据库改造ALTER TABLE users ADD COLUMN permission VARCHAR(255) DEFAULT ; -- 示例editor 用户 permission BOOK_EDIT,REPORT_VIEW -- auditor 用户 permission BORROW_AUDIT,USER_VIEW第二步修改 LoginFilter.javapublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; String uri request.getRequestURI(); // 白名单登录页、验证码、静态资源放行 if (uri.contains(/login.jsp) || uri.contains(/VerifyCodeServlet) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, res); return; } // 校验登录态 User user (User) request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 新增权限校验精确匹配 String permission user.getPermission(); // 从数据库读取 if (uri.contains(/admin/deleteBook) !permission.contains(BOOK_DELETE)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 无删除权限); return; } if (uri.contains(/admin/exportExcel) !permission.contains(REPORT_EXPORT)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 无导出权限); return; } chain.doFilter(req, res); }关键点permission字段存逗号分隔的权限码用String.contains()快速判断避免引入复杂 RBAC 框架。生产环境可升级为FIND_IN_SET()SQL 查询。5.2 文件上传路径改造从绝对路径改为相对路径 时间戳防重名原AddBookServlet.java将封面保存到C:/uploads/硬编码路径导致跨平台失败。改造为// 获取项目根目录下的 uploads 文件夹Tomcat 部署后路径为 webapps/booksystem/uploads/ String uploadPath request.getServletContext().getRealPath(/uploads); File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); // 生成唯一文件名时间戳 原文件名哈希 String originalFileName item.getName(); String extension originalFileName.substring(originalFileName.lastIndexOf(.)); String newFileName System.currentTimeMillis() _ Integer.toHexString(originalFileName.hashCode()) extension; File uploadedFile new File(uploadDir, newFileName); item.write(uploadedFile); // 保存到数据库的路径为相对路径便于前端访问 String dbPath /uploads/ newFileName;好处getServletContext().getRealPath()自动适配 Windows/Linux 路径分隔符时间戳保证并发上传不冲突哈希值避免相同文件名覆盖数据库存储/uploads/xxx.jpg前端img src${book.coverPath}可直接渲染。5.3 日志增强在 UserDao 中添加 SQL 执行耗时监控为定位慢查询改造UserDao.java的findByUsername()public User findByUsername(String username) { long startTime System.currentTimeMillis(); String sql SELECT id, username, password, role FROM users WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); try (ResultSet rs pstmt.executeQuery()) { long cost System.currentTimeMillis() - startTime; if (cost 500) { // 超过 500ms 记为慢查询 log.warn(Slow query: {} | Cost: {}ms | Username: {}, sql, cost, username); } if (rs.next()) { return new User(rs.getInt(id), rs.getString(username), rs.getString(password), rs.getString(role)); } } } catch (SQLException e) { long cost System.currentTimeMillis() - startTime; log.error(Query user failed | Cost: {}ms | SQL: {}, cost, sql, e); } return null; }落地技巧System.currentTimeMillis()比System.nanoTime()更适合业务耗时统计单位毫秒易读log.warn()记录慢查询log.error()记录异常便于 ELK 日志平台按级别过滤在log4j.properties中配置%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n日志带毫秒级时间戳。从那以后我每次接手老项目做二次开发都强制走一遍「数据库字段扫描 → Servlet URL 映射检查 → web.xml Filter 链路梳理」三步流程。不是为了炫技而是避免在LoginFilter里加了一行权限判断结果发现VerifyCodeServlet被拦截导致验证码刷不出来这种低级翻车。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询