ARTICLE DETAIL

资讯详情

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

JavaWeb图书管理系统项目实战:从JDBC到JSP全链路完整教程

JavaWeb图书管理系统项目实战:从JDBC到JSP全链路完整教程 图书管理系统大概是JavaWeb领域最烂大街的项目了但我带新人入行这么多年每次有人问我Java语法学完了该做什么我给出的答案永远是它。别看这个项目简单从JDBC到Servlet再到JSP整个JavaWeb的经典链路它一条不落全走了一遍。这篇文章不搞虚的直接把我平时带人做这个项目的完整过程摊开讲从环境搭建到数据库设计从每一层代码怎么写到最后怎么部署包括我自己踩过的坑、学员常犯的错全写在这里。你要是零基础跟着这篇从头敲一遍不敢说精通Java但至少能独立写出一个像样的管理系统也够你拿去应付课程设计或者面试里的项目提问了。1. 项目整体设计与技术选型1.1 为什么偏偏是图书管理项目图书管理项目在Java学习路线里的地位差不多等同于烹饪课里的番茄炒蛋。它不大不小刚好能覆盖JavaWeb开发的完整链路前端页面怎么写、后端接口怎么接、数据库怎么设计、数据怎么流动。你要是一上来就上手电商系统光是商品SKU、订单状态机、支付回调就能把人绕晕要是只写控制台练习又永远碰不到真实开发里的HTTP、会话这些东西学完语法等于没学。一个标准的图书管理项目包含的功能通常就这么几块图书的新增、删除、修改、查询读者的借书还书用户的登录注册管理员和普通读者的权限区分。听着简单但每一块展开都有值得琢磨的点。就拿删书来说表面是dao层里一行delete语句的事实际你得考虑这本书被借走了怎么办是禁止删除还是标记下架这就是业务规则是用户没写进需求但你必须替他想清楚的地方。这个项目真正训练的是分层思维。控制层只负责接收参数和跳转页面业务层只负责规则判断数据层只负责跟数据库打交道。我在实际带教中发现很多自学的同学写代码不分层Servlet里直接拼SQL功能是能跑但加一个需求就要重构一次。图书管理项目因为规模小恰好可以让你毫无压力地把三层架构练熟等以后做Spring Boot项目时这种分层设计的肌肉记忆会帮你大忙。1.2 技术选型老技术为什么依然能打技术选型这里我多说几句因为很多人一上来就纠结要不要学最新框架。我推荐的组合是JDK 8 Tomcat 9 JSP Servlet JDBC MySQL。这套组合你翻现在的招聘要求可能觉得过时但对零基础学员来说它恰恰是最好的教学载体。为什么因为这套技术栈没有任何框架封装遮住你的视线。JSP能让你看到页面模板怎么嵌Java代码Servlet能让你亲眼看到HTTP请求怎么被接收、响应怎么写回去JDBC能让你手写数据库连接、手写预编译SQL。这些底层机制你亲手摸过一遍之后再学Spring Boot、MyBatis、Spring Data JPA这些封装好的轮子理解速度完全不一样——你知道它们替你干了什么也知道出问题该去哪一层排查。也有一种选择是直接上Spring Boot MyBatis很多培训班这么做理由是对接就业。但我不建议零基础直接跳过去。假如你连Servlet都没写过遇到Spring MVC的DispatcherServlet就觉得是魔法遇到MyBatis的Mapper代理也觉得是魔法整个学习过程全在背注解和配置底层一问三不知。技术可以升级路线不建议跳级。我遇到过不止一个学员Spring Boot项目能跑但让他解释一下请求从浏览器发出到数据库返回结果之间发生了什么完全说不清楚。这种状态去面试项目一深问就露馅。技术方案学习成本底层可见性就业匹配度推荐人群JSPServletJDBC低高所有环节手写中偏课程设计与基础岗零基础入门先吃透原理Spring BootMyBatis中高低框架封装多高主流企业技术栈已有JavaWeb基础准备就业1.3 功能模块与角色权限拆解动手写代码前先把功能边界画清楚。我习惯让学员画一张简单的模块图系统分成前台和后台两大部分后台是管理员用的管图书和借还前台是读者用的查书和借书。不要一上来就想着做花哨的功能核心就四件事图书管理、读者管理、借阅管理、登录认证。权限这块是很多新手容易忽略的。图书管理项目虽然小但权限设计完全可以做得规范和合理。管理员能对图书做增删改查能查看所有借阅记录能处理还书普通读者只能查询图书、借书、还书、查看自己的借阅历史。这个约束在哪一层实现我要求学员必须在Servlet里做登录校验通过Session判断当前用户角色再决定是否放行。放页面里用隐藏按钮去控制权限是错的因为接口暴露了绕过前端直接请求URL照样能执行操作。这一点理解透了你以后做任何系统的权限控制都有基础。功能清单列出来大概是这样的管理员登录、图书信息维护增删改查、读者信息维护增删改查、图书借阅、图书归还、借阅记录查询、超期判断。够了吗对于入门来说完全够了。你要是想加功能后期可以加图书分类、模糊搜索、分页、借阅排行榜这些都是同一个套路学会了第一个后面都是复制粘贴加改动。2. 环境准备与数据库设计2.1 开发环境搭建与常见坑先说环境。零基础同学最容易在环境配置上耗尽热情这里我给出一套我实测最省事的组合JDK 8、MySQL 5.7或者8.0、Tomcat 9、IDEA社区版、Maven 3.6。JDK 8虽然老但它跟Tomcat 9的兼容性最稳而且市面上大量教程、代码示例都是按JDK 8写的你跟着做不会遇到莫名其妙的版本差异。JDK装好后一定要在命令行里验证一下。打开cmd输入java -version能正确输出版本号才算装好。很多同学装完就在IDEA里写代码结果IDEA用的是自带JDK或者配错了路径后面启动Tomcat报各种错误。环境变量里JAVA_HOME和PATH这两个必须配JAVA_HOME指向JDK安装根目录PATH里加上%JAVA_HOME%\bin这是基础操作但每年都有一堆人卡在这。MySQL装的时候有两个地方我特别提醒一个是字符集安装时或建库时务必指定utf8mb4不然后面存中文全是问号另一个是端口默认3306即可别随意改动改了你连接字符串也得跟着改新手容易忘记。Tomcat下载解压版就行解压后目录里有个bin文件夹startup.bat是启动脚本conf/server.xml里可以改端口默认8080。端口被占用是最常见的启动失败原因后面我会专门讲排查方法。IDEA里创建一个普通的Maven项目用maven-archetype-webapp模板这个模板会帮你生成标准的Web项目结构。IDEA社区版不带Tomcat集成插件这个问题不大我一般让学员用IDEA里配置Tomcat的方式或者直接打WAR包扔到Tomcat的webapps目录里启动两种方式都要会尤其后者更接近真实部署。2.2 三张核心表的设计数据库设计是整个项目的根基表结构建错了后面所有代码都要推倒重来。图书管理项目最少需要三张表图书表、用户表读者、借阅记录表。有些教程喜欢把用户和读者分开建表我觉得入门阶段没必要一个用户表加一个角色字段就能表达清楚。图书表字段这样设计book_id主键自增book_name书名author作者publisher出版社price价格stock库存category分类。这里我着重说stock——库存存在图书表里而不是每次现算这是一个常见的设计取舍。借书时库存减一还书时加一看起来有并发风险但入门项目完全够用而且查询效率高。你要是用每次查询借阅记录再统计剩余数量的做法逻辑上更严谨但SQL复杂得多对新手不友好。用户表user_id主键username唯一passwordreal_name真实姓名role角色字段管理员写1普通读者写0。密码存储这里我必须强调真实项目绝不能存明文至少要用MD5加盐或者BCrypt哈希。但入门阶段如果直接上加密很多人会混淆加密和业务逻辑的学习重点。我的建议是先存明文把功能跑通然后作为一个进阶任务自己研究怎么改成MD5存储这样既不耽误主线又能真实练到安全问题。借阅记录表borrow_id主键book_id外键关联图书表user_id外键关联用户表borrow_time借书时间return_time还书时间。这张表就是整个系统的核心业务表一次借书操作插入一条记录还书时更新return_time。你要查询某本书被谁借了、某个读者借了几本书、哪些书超期没还全部靠这张表出数据。实际在建表时我会让学员手写SQL不要用Navicat的图形界面点出来原因很简单面试里让你手写建表语句是常事而且建表SQL写熟了后面写复杂的联表查询才不怵。下面是完整的建表脚本可以直接复制执行CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4; USE library_db; CREATE TABLE book ( book_id INT NOT NULL AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50) DEFAULT NULL, publisher VARCHAR(100) DEFAULT NULL, price DECIMAL(10,2) DEFAULT NULL, stock INT NOT NULL DEFAULT 1, category VARCHAR(50) DEFAULT NULL, PRIMARY KEY (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user ( user_id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT 1admin, 0reader, PRIMARY KEY (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( borrow_id INT NOT NULL AUTO_INCREMENT, book_id INT NOT NULL, user_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, return_time DATETIME DEFAULT NULL, PRIMARY KEY (borrow_id), KEY idx_book_id (book_id), KEY idx_user_id (user_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book (book_id), CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节user表我加了UNIQUE约束在username上这是为了防止注册重复用户名。外键我也真实建了但必须提醒你有些教程为了省事不建外键靠代码逻辑维护关联——零基础阶段我建议建上因为外键能帮你理解表间关系等以后做高并发项目再考虑为了性能去掉外键。索引这块borrow_record表上我给book_id和user_id建了普通索引因为查询借阅记录时这两个字段是高频搜索条件没索引的话数据量一大就会全表扫描体验差异很明显。2.3 项目目录结构与分层约定有了表接下来规划代码目录。我见过太多新手把所有类堆在一个包里的写法这里我直接给出一个我平时带人用的标准结构每个包干什么写在注释里src/main/java ├── com.library.entity # 实体类Book, User, BorrowRecord ├── com.library.util # 工具类DBUtil 数据库连接 ├── com.library.dao # 数据访问层BookDao, UserDao, BorrowDao ├── com.library.dao.impl # DAO实现类接口与实现分离 ├── com.library.service # 业务层接口 ├── com.library.service.impl # 业务层实现 └── com.library.servlet # Controller层BookServlet, UserServlet, BorrowServlet为什么要接口和实现分开这是很多新手不理解的地方。你现在可能觉得接口里写个方法签名实现类里写逻辑这不是脱裤子放屁吗。我直接说答案为了解耦和扩展。今天你用的是JDBC明天要是换成MyBatis业务层调用的还是BookDao接口你只需要把实现类换掉业务层一行代码不用改。面试的时候HR问你用过哪些设计模式你可以说项目里通过接口编程实现了依赖倒置这就比干巴巴背定义有说服力得多。分层之间的调用规则这么定JSP页面发起请求到ServletServlet调Service接口ServiceImpl里面写业务逻辑、调DAO接口DAOImpl直接操作数据库。每一层只认相邻的下一层禁止Servlet跨层直接调DAO。这个规则坚持住你的代码结构就永远是清爽的。我自己检查学员代码时第一件事就是看有没有Servlet直接new DAO去查数据发现一次就要求重构一次练的就是这个习惯。3. 核心功能实现与代码解析3.1 实体类与JDBC连接工具先把实体类写了。实体类的作用是让Java代码里的对象跟数据库表记录一一对应这是ORM思想的雏形。Book实体长这样public class Book { private Integer bookId; private String bookName; private String author; private String publisher; private BigDecimal price; private Integer stock; private String category; public Book() {} // 每个字段的 getter 和 setter 省略IDE一键生成 }注意我用的是包装类型Integer而不是基本类型int。这是有讲究的数据库字段可能为NULL基本类型int接不住空值会直接抛NullPointerException包装类型可以优雅地表达没有值这个状态。这个细节很多工作两三年的开发都不一定注意你从这个项目开始养成习惯以后踩坑就少。接下来是最容易被轻视但最重要的DBUtil。很多新手喜欢在DAO里每次DriverManager.getConnection用完不关连接这是错误的。正确的做法是写一个工具类把连接的创建和关闭收拢在一起同时规定必须在finally块里释放资源public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }连接串里的参数每一个都有用。useSSLfalse是关掉SSL警告本地开发不加这句话控制台会刷一堆红字虽然不影响运行但很烦人。serverTimezoneAsia/Shanghai是解决MySQL 8.0的时区报错不加会提示时区无法识别。characterEncodingutf8是保证传输给数据库的SQL里中文不乱码。这三个参数是MySQL 8.0下的标准配置我建议你直接抄后面真遇到问题再逐个研究省时间。Class.forName(com.mysql.cj.jdbc.Driver)这一行是注册驱动。很多教程让你照抄就行但我想解释清楚JDBC规范里DriverManager会通过SPI机制自动加载驱动所以这一行在现代MySQL驱动下其实可以省略。但写上它有个好处——如果驱动JAR包没引入或者版本不对这行会立刻抛ClassNotFoundException让你第一时间知道是依赖问题而不是代码问题。所以我建议保留它是个很好的烟雾报警器。3.2 DAO层的CRUD实操DAO层是整个项目代码量最大的部分也是锤炼SQL功力的地方。我先说一个原则这个原则值得你用荧光笔划出来永远使用PreparedStatement永远不要用Statement拼接SQL字符串。// 错误示范SQL拼接存在注入风险 String sql SELECT * FROM book WHERE book_name name ; // 正确写法预编译参数用?占位 String sql SELECT * FROM book WHERE book_name ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, name); ResultSet rs ps.executeQuery();为什么拼接有风险因为用户输入的内容会被当成SQL代码执行。假设查询条件输入 OR 11 --拼接后的SQL变成WHERE book_name OR 11 -- 恒真条件会把整张表全查出来。这就是SQL注入。PreparedStatement用预编译机制把参数值跟SQL结构分离用户输入永远是数据而不是代码从根上杜绝了这个问题。你以后做任何项目、用任何数据库访问框架这个安全意识都是一脉相承的。我以BookDaoImpl为例展示两个最有代表性的方法。先说查询所有图书Override public ListBook findAll() { ListBook list new ArrayList(); String sql SELECT book_id, book_name, author, publisher, price, stock, category FROM book; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); rs ps.executeQuery(); while (rs.next()) { Book book new Book(); book.setBookId(rs.getInt(book_id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setPublisher(rs.getString(publisher)); book.setPrice(rs.getBigDecimal(price)); book.setStock(rs.getInt(stock)); book.setCategory(rs.getString(category)); list.add(book); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; }再说新增图书这里有个关键点自增主键怎么拿回来。很多时候你新增完一本书马上要用这本书的book_id去插入借阅记录所以得在插入后把自增ID取出来Override public int insert(Book book) { String sql INSERT INTO book(book_name, author, publisher, price, stock, category) VALUES(?,?,?,?,?,?); Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); // 第二个参数 RETURN_GENERATED_KEYS 是关键 ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setString(3, book.getPublisher()); ps.setBigDecimal(4, book.getPrice()); ps.setInt(5, book.getStock()); ps.setString(6, book.getCategory()); ps.executeUpdate(); ResultSet keys ps.getGeneratedKeys(); if (keys.next()) { return keys.getInt(1); } return 0; } catch (SQLException e) { e.printStackTrace(); return 0; } finally { DBUtil.close(null, ps, conn); } }prepareStatement的第二个参数RETURN_GENERATED_KEYS是告诉数据库插入完成后把生成的主键还给Java端。这个技巧在以后用MyBatis时也有对应的体现就是useGeneratedKeys属性原理一模一样。你现在把这个机制理解了后面学框架那是一通百通。3.3 Service业务层与Servlet控制层DAO层写完Service层重点写规则。还书这个场景是练Service最好的例子还书时先查借阅记录是否存在、是否已经还过如果借了多本书要逐本处理还书成功后更新库存和return_time。这些跨多张表、带判断条件的逻辑如果在Servlet里写代码会又长又乱放进Service才是正路。public class BorrowServiceImpl implements BorrowService { private BorrowDao borrowDao new BorrowDaoImpl(); private BookDao bookDao new BookDaoImpl(); Override public boolean borrowBook(int userId, int bookId) { // 先检查库存 Book book bookDao.findById(bookId); if (book null || book.getStock() 0) { return false; } // 再检查该读者是否已经借了这本书没还 BorrowRecord exists borrowDao.findActiveByUserAndBook(userId, bookId); if (exists ! null) { return false; } // 扣库存 插入借阅记录这里真实项目要加事务见下文 bookDao.decreaseStock(bookId); borrowDao.insert(userId, bookId); return true; } }你仔细看这个逻辑它就是真实业务开发的缩影。这里头还有一个事务问题我必须专门讲decreaseStock和insert是两个独立的数据库操作如果insert失败了但decreaseStock成功了库存扣了但没借书记录数据就不一致了。解决办法是让这两个操作处于同一个数据库事务里要么全成功要么全回滚。JDBC里实现事务很简单在操作前conn.setAutoCommit(false)操作完conn.commit()出异常conn.rollback()。但我上面这个BorrowServiceImpl里DAO层每次getConnection拿的都是新连接两个操作天然不在一个事务里。入门阶段怎么解决我的要求是把这两个逻辑合并到一个DAO方法borrowTransaction里用同一个Connection完成两个操作。写起来不优雅但目的是让你理解事务的本质——多个数据库操作要么一起成功、要么一起失败。理解了这个你学Spring的Transactional注解就是水到渠成的事。Servlet层是MVC里的Controller它的职责就是取参数、调Service、做页面跳转。一个经典的处理流程如下WebServlet(/book) public class BookServlet extends HttpServlet { private BookService bookService new BookServiceImpl(); Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String op req.getParameter(op); if (op null) op list; switch (op) { case list: listBooks(req, resp); break; case toEdit: toEdit(req, resp); break; case add: addBook(req, resp); break; case update: updateBook(req, resp); break; case delete: deleteBook(req, resp); break; default: listBooks(req, resp); } } // 各方法实现省略 }我习惯重写service方法在它里面做个简单的请求分发用op参数区分不同操作。这其实就是Front Controller的前身等你以后学了Spring MVC会发现RequestMapping就是干这件事的只不过Spring把分发逻辑做成了框架。你在Servlet里手写过一遍再回看Spring MVC的HandlerMapping、HandlerAdapter会非常有亲切感。跳转方式这里还有一个高频考点request.getRequestDispatcher(xxx.jsp).forward()和resp.sendRedirect(xxx)有什么区别什么时候用哪个。我的规则很简单执行完查询需要带数据到页面上展示的用forward转发因为request域里的参数和数据在转发后还能用执行完增删改之后一定要用redirect重定向否则用户刷新页面表单会重复提交导致重复插入数据。这个表单重复提交问题是图书管理项目里最容易被新手撞出来的坑之一后面我详细说。3.4 前端页面与JSP交互JSP页面是这个项目里最看得见的部分。零基础学员做页面最容易犯的错是追求CSS美观花三天调样式把后端逻辑晾在一边。我的建议是能用Bootstrap就用Bootstrap把精力放在JSP跟Servlet的数据交互上。页面丑一点没关系逻辑通了才是本事。JSP的核心语法就几个% %脚本片段、% %输出表达式、JSTL标签库。我强烈建议你从一开始就学习使用JSTL和EL表达式而不是在JSP里嵌大段% %Java代码。道理很直白JSP是视图层职责是展示数据你要是把Java逻辑全塞进去就违背了分层原则并且页面会混乱到无法维护。在pom.xml中引入JSTL依赖dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency然后在JSP页面顶部声明% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %图书列表页用c:forEach循环渲染整张表table classtable table-bordered thead tr th编号/th th书名/th th作者/th th出版社/th th价格/th th库存/th th操作/th /tr /thead tbody c:forEach items${bookList} varbook tr td${book.bookId}/td td${book.bookName}/td td${book.author}/td td${book.publisher}/td tdfmt:formatNumber value${book.price} typecurrency//td td${book.stock}/td td a hrefbook?optoEditbookId${book.bookId}编辑/a a hrefbook?opdeletebookId${book.bookId} onclickreturn confirm(确定删除吗)删除/a /td /tr /c:forEach /tbody /table注意借书/还书页面的信息提示。我一般要求学员在Session或request域里放一个message属性操作成功或失败后由Servlet设置消息并转发到对应页面页面里用c:if判断有没有消息再展示。这样用户体验会好很多也让学员明白页面和后台怎么通过域对象通信。登录功能是权限控制的核心前提。登录Servlet里验证用户名密码后把userId和role存进Session然后过滤器检查每个请求的Session状态。过滤器Filter是这个项目里很容易被忽略但极其重要的一环。写一个LoginFilter拦截所有/admin/*路径Session里没有user对象就重定向到登录页这是最基础的认证方式。你以后学Spring Security、Shiro第一课讲的还是这个套路。4. 常见问题排查与项目部署4.1 环境与启动阶段的典型问题环境问题占据了零基础学员至少三成的报错时间我把高频问题列个速查表全是实战里反复出现的。现象原因解决方案Tomcat启动一闪而过JAVA_HOME没配或配错命令行检查java -version重新配置环境变量端口8080被占用别的服务占了端口改Tomcat的server.xml端口或找到占用端口的进程结束它404错误访问路径跟Servlet注解不匹配核对WebServlet()里的URL和页面请求地址完全一致ClassNotFoundException: cj.jdbc.DriverMySQL驱动JAR没引入pom.xml添加mysql-connector-java依赖检查依赖是否下载成功控制台中文乱码页面或连接串没指定编码JSP加contentTypeUTF-8连接串加characterEncodingutf8端口占用这个问题我展开讲讲排查方法。在命令行执行netstat -ano | findstr 8080能看到占用8080端口的进程PID再去任务管理器里找到对应进程结束掉。如果那是个系统进程或者你不敢动就直接改Tomcat端口编辑conf/server.xml里Connector标签的port属性改成8081重启Tomcat就行。很多人卡在这还以为是代码问题浪费大量时间其实先检查环境才是最快的。JDK和Tomcat版本不匹配也容易出问题。Tomcat 9对应JDK 8及以上Tomcat 10对应JDK 11及以上并且Tomcat 10把包名从javax.servlet改成了jakarta.servlet你要是用Tomcat 10跑旧教程的代码编译直接报错找不到包。我之前就犯过这个错给学员配了最新的Tomcat 10结果教材里的Servlet全编译不过折腾半天才反应过来是版本问题。所以零基础阶段我统一用Tomcat 9 JDK 8最稳。4.2 数据库连接与SQL执行问题数据库相关的报错九成以上集中在驱动找不到、时区错误、SQL语法错误这三类。驱动找不到就是上一节说的依赖问题时区错误在连接串里加serverTimezoneAsia/Shanghai解决。SQL语法错误则需要细心调试我建议学员先把SQL语句在Navicat或命令行里单独执行一遍确认无误再贴回代码。用PreparedStatement时还要注意参数占位符的数量和setXxx的顺序必须一致多一个少一个都会在executeQuery时报参数索引无效。还有一个特别隐蔽的问题MySQL 8.0默认使用caching_sha2_password认证插件而某些老版本的驱动不支持这个认证方式连接时会报Public Key Retrieval is not allowed。解决方案是在连接串加allowPublicKeyRetrievaltrue。这个参数我第一次遇到时也查了很久因为提示信息完全没有指向这个原因。你如果也遇到这个报错直接加上参数就行。资源的关闭顺序也是要养成的好习惯。ResultSet、PreparedStatement、Connection这三者必须逆序关闭先关结果集再关语句最后关连接。只使用我在DBUtil里提供的方法可以帮你少写很多代码。有的同学图省事Connection不关等连接池把连接耗尽时SQL执行就卡住或者报连接数超限。本地开发可能感觉不明显但一旦部署到服务器上那是分分钟把数据库搞挂的节奏。4.3 中文乱码问题的根源与根治中文乱码是JavaWeb新手必遇的坑而且往往不止遇到一次。我总结一下乱码就三个来源请求参数乱码、响应输出乱码、数据库存取乱码。请求参数乱码分两种请求方式。POST请求在读取参数之前执行req.setCharacterEncoding(UTF-8)就能解决注意必须在第一次读取参数前设置放在Servlet方法的第一行最保险。GET请求的参数是拼在URL里的Tomcat 8及以上默认用UTF-8解码基本不出问题但如果遇到乱码需要改Tomcat的conf/server.xml里Connector加URIEncodingUTF-8属性。响应输出乱码的解决办法是JSP页面顶部写pageEncodingUTF-8和contentTypetext/html;charsetUTF-8Servlet里写resp.setContentType(text/html;charsetUTF-8)。数据库存取乱码建库时指定utf8mb4、连接串加characterEncodingutf8、表字段也用utf8mb4字符集三者配合才能保证中文从浏览器到数据库再回来的完整链路不乱码。我见过一个特别典型的场景页面输入中文新增到数据库里是正常的????问号查出来也是问号。很多人第一反应是改代码但真正的原因就是建库时没指定字符集数据库默认用了latin1。所以建库语句我从一开始就让你加上DEFAULT CHARACTER SET utf8mb4就是为了从源头把这个坑堵死。4.4 部署打包与Spring Boot升级路线项目开发完总要把WAR包丢到Tomcat里跑起来才算真正结束。IDEA里Build Artifact选择war exploded会输出WAR包到target目录。把WAR包复制到Tomcat的webapps目录重启Tomcat它会自动解压部署。访问地址就是http://localhost:8080/项目名/。这一步做完你才算完成了从代码能跑到部署能跑的跨越也更能理解真实项目里运维同学在做什么。我建议你有余力的话把这个项目用Spring Boot重写一遍作为进阶挑战。改造思路很清晰用Spring Boot的Web Starter替代Tomcat嵌入式服务器用Spring MVC替代手写Servlet用MyBatis替代JDBC用Thymeleaf或Vue替代JSP。业务逻辑你完全不用大改BookService接口基本能直接搬过去这正好能让你验证分层设计的好处——当框架换了业务层代码还能保留大部分。这个升级过程做完你对JavaWeb的理解就完整了。最后再分享一个小技巧。很多学员问我做完了图书管理项目下一步做什么。我的建议是不要急着开新项目而是拿着这个已经跑通的项目做三件事第一把所有SQL改成预编译并解释为什么第二给密码加盐做MD5存储第三模拟一个并发借同一本书的场景观察数据会不会出错。这三件事做完你对这个项目理解深度会翻一倍面试聊起来也更有底气。别贪多把一个项目吃透比同时刷十个半吊子项目强太多。
返回列表