简介:这是一套基于J2EE技术栈开发的完整在线图书销售系统——“云工厂-网上书城”,面向Java Web初学者与企业级应用进阶学习者,聚焦多层架构设计、前后端协同与数据库集成等核心实践问题。资源包含588个文件,涵盖83个Java源码(如BookInfo、AdminInfoDAO等业务类)、45个JSP页面(实现用户购书、订单管理等交互视图)、82个编译后class文件、313个GIF动图(含界面操作示意或图标资源),以及SQL建表脚本、CSS样式、JAR依赖库和数据库文件(.db)等,整体压缩包仅4.09MB,轻量易部署。已有242人学习下载,适合在Tomcat等J2EE容器中快速运行调试。读者可直接获取从Servlet请求分发、JSP动态渲染、JavaBeans数据封装、JDBC数据库操作到EJB事务管理的全链路代码实现,尤其利于理解MVC分层结构与典型电商模块(如购物车GwcServlet、收藏夹FavoritesInfoDAO、订单保存SaveOrderServlet)的设计逻辑。
1. 一个跑在 Tomcat 上的 J2EE 书城系统:它不炫技,但能让你亲手把 Servlet、JSP、JDBC、JavaBean 全链路串通
你可能刚学完《Java Web 程序设计》第三章,对着书上“用户登录验证”代码改了八遍还是 404;也可能在公司接到个临时需求——要快速搭个内部图书借阅页,领导说“别用 Spring Boot,老服务器只装了 JDK 1.8 和 Tomcat 7”。这时候,“云工厂-网上书城”不是个玩具 Demo,而是一份能直接扔进webapps/目录、改两行数据库配置就能跑起来的 J2EE 实战标本。它没用任何框架遮掩底层细节:登录走的是HttpServlet的doPost,商品列表靠JDBC + ResultSet手动封装成ArrayList<Book>,购物车用HttpSession存List<CartItem>,连分页都是手写 SQL 的LIMIT ?, ?。项目结构干净得像教科书目录——/WEB-INF/web.xml里明明白白写着 5 个 servlet 映射,/src/com/cloudfactory/下包名层级就是 MVC 的骨架。如果你正卡在“知道概念但写不出完整请求链路”,这个项目就是你的调试断点集:从浏览器输入http://localhost:8080/bookstore/login.jsp开始,你能一路跟到LoginServlet.java→UserService.java→UserDAO.java→DBUtil.java,每个箭头都对应一个真实文件、一行可打断点的代码。它不教你“怎么用 MyBatis 自动生成 mapper”,它逼你写PreparedStatement.setString(1, username)—— 这种“原始感”,恰恰是很多开发者缺的那块拼图。
2. 项目结构与技术栈解剖:J2EE 四件套如何在没有框架时协同工作
2.1 目录结构即架构:从web.xml到src的物理分层
项目采用标准 Java Web 工程布局,所有路径都遵循 Servlet 规范,无需额外构建工具即可部署。核心结构如下(已按实际资源整理):
cloudfactory-bookstore/ ├── src/ │ └── com/cloudfactory/ │ ├── controller/ // Servlet 层:处理 HTTP 请求与响应 │ │ ├── LoginServlet.java │ │ ├── BookListServlet.java │ │ └── CartServlet.java │ ├── model/ // JavaBean:纯 POJO,无业务逻辑 │ │ ├── User.java │ │ ├── Book.java │ │ └── CartItem.java │ ├── service/ // Service 层:协调 DAO,含简单业务规则(如密码校验) │ │ ├── UserService.java │ │ └── BookService.java │ ├── dao/ // DAO 层:JDBC 操作封装,与 DBUtil 耦合 │ │ ├── UserDAO.java │ │ └── BookDAO.java │ └── util/ // 工具类:DB 连接池、字符串处理 │ └── DBUtil.java ├── WebContent/ // Web 根目录(Eclipse 旧版命名,等同于 standard webapp root) │ ├── login.jsp │ ├── index.jsp │ ├── book_list.jsp │ ├── cart.jsp │ └── WEB-INF/ │ ├── web.xml // 核心配置:servlet 映射、欢迎文件、过滤器 │ └── lib/ // 仅含 2 个 jar:mysql-connector-java-5.1.47.jar + servlet-api.jar └── README.md // 极简说明:数据库建表语句、默认账号密码提示:
WebContent是 Eclipse Dynamic Web Project 默认输出目录名,等价于标准src/main/webapp。部署时直接将整个WebContent文件夹重命名为bookstore,拷入 Tomcatwebapps/即可。不要试图用 Maven 重构——这个项目的生命力恰恰在于“零构建依赖”。
2.2 技术栈选择逻辑:为什么坚持用原生 J2EE 而非 Spring?
这不是怀旧,而是场景倒逼。某高校实验室需要一套能运行在老旧教学机房(CentOS 6.5 + Tomcat 7.0.39 + JDK 1.7)上的课程设计模板。Spring Boot 2.x 要求 JDK 8+,Spring MVC 5 需要 Servlet 3.1+,而 Tomcat 7.0.39 仅支持 Servlet 3.0。项目选型完全服从环境约束:
- Servlet API:使用
javax.servlet.http.HttpServlet,doGet/doPost方法体清晰暴露请求生命周期; - JSP:未启用 EL 表达式简化(
${user.name}),全部用<%= user.getName() %>,强制理解 JSP 编译为 Servlet 的过程; - JDBC:
DBUtil.java封装Connection获取与关闭,但不使用连接池(HikariCP 等需额外 jar),而是每次请求新建连接——这是教学场景的刻意降级,避免初学者被池化机制干扰对“一次请求一次连接”的理解; - JavaBean:
User.java中private String password;后紧跟public void setPassword(String password) { this.password = MD5Util.encode(password); },把密码加密逻辑直接写进 setter,体现“Bean 不只是数据容器”的早期实践思想。
这种“落后”恰恰是优势:当你在BookListServlet.java里看到request.setAttribute("bookList", bookList),再在book_list.jsp里用<% List<Book> list = (List<Book>) request.getAttribute("bookList"); %>取出数据时,你不会困惑“Model 是怎么注入的”,因为答案就写在那一行setAttribute里。
2.3 核心配置文件解析:web.xml如何定义整个应用的入口
WEB-INF/web.xml是整个项目的中枢神经,共 67 行,无注释但每行皆关键。以下是其主干逻辑拆解:
<?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"> <!-- 1. 定义 Servlet 类 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.cloudfactory.controller.LoginServlet</servlet-class> </servlet> <!-- 2. 绑定 URL 模式 --> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> <!-- 注意:不是 /login.jsp --> </servlet-mapping> <!-- 3. 设置欢迎页面 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- 4. 配置全局初始化参数(数据库驱动) --> <context-param> <param-name>driver</param-name> <param-value>com.mysql.jdbc.Driver</param-value> </context-param> <!-- 5. 配置会话超时(分钟) --> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>关键点说明:
<url-pattern>/login</url-pattern>表明所有/login请求由LoginServlet处理,JSP 页面不能直接通过 URL 访问(如http://.../login.jsp会绕过 Servlet,导致 session 未创建);<context-param>中的driver参数被DBUtil.java通过getServletContext().getInitParameter("driver")读取,实现配置与代码分离;version="3.1"声明 Servlet 规范版本,决定容器是否支持@WebServlet注解——本项目禁用注解,全部用 XML 配置,确保在任意 Servlet 容器中行为一致。
2.4 数据库设计与连接:手写 SQL 与硬编码连接字符串的权衡
项目附带db_schema.sql,仅 4 张表,字段精简到极致:
-- 用户表(密码已 MD5 加密存储) CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(32) NOT NULL, -- 存储 32 位 MD5 值 email VARCHAR(100) ); -- 图书表 CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(8,2), stock INT DEFAULT 0 ); -- 购物车表(无持久化,仅 Session 内存存储) -- 订单表(本项目未实现,留作二次开发接口)DBUtil.java中的连接逻辑直白到“危险”:
public class DBUtil { private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=UTF-8"; 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); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) {} if (ps != null) try { ps.close(); } catch (SQLException e) {} if (conn != null) try { conn.close(); } catch (SQLException e) {} } }参数说明:
useUnicode=true&characterEncoding=UTF-8:强制解决中文乱码,若省略此参数,INSERT INTO books(title) VALUES('深入理解JVM')会存成??????JVM;DriverManager.getConnection()每次新建连接,无连接复用,这是教学项目对性能的主动放弃,只为突出“连接对象生命周期”这一知识点;close()方法中try-catch忽略SQLException,因关闭失败不影响业务逻辑,但实际项目应记录日志。
3. 核心模块代码逐行剖析:从登录请求到数据库查询的完整链路
3.1 登录流程:LoginServlet如何串联前端表单与后端验证
login.jsp中的表单指向/login(由web.xml映射):
<form action="/bookstore/login" method="post"> <input type="text" name="username" placeholder="用户名" required> <input type="password" name="password" placeholder="密码" required> <button type="submit">登录</button> </form>LoginServlet.java接收并处理该请求:
public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取请求参数(注意:getParameter() 返回 String,空值为 null) String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 参数校验(教学场景简化版,生产环境需更严格) if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { request.setAttribute("error", "用户名或密码不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; // ⚠️ 必须 return,否则后续代码仍会执行 } // 3. 调用 Service 层验证用户(密码已 MD5 加密) UserService userService = new UserService(); User user = userService.login(username, password); // 4. 根据验证结果跳转 if (user != null) { // 登录成功:将用户信息存入 Session,供其他页面使用 HttpSession session = request.getSession(); session.setAttribute("currentUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { // 登录失败:设置错误信息,返回登录页 request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }逻辑说明:
request.getParameter("username")是 J2EE 最基础的数据获取方式,不经过任何自动类型转换,所有参数均为String;request.getRequestDispatcher(...).forward()是服务器端跳转,URL 不变,适合表单验证失败后回显错误信息;response.sendRedirect(...)是客户端重定向,URL 变为/bookstore/index.jsp,且新请求中session依然有效(因 Cookie 未变);request.getContextPath()动态获取应用上下文路径(如/bookstore),避免硬编码,保证部署到不同路径时链接仍有效。
3.2 业务服务层:UserService如何协调 DAO 并处理密码逻辑
UserService.java是典型的薄 Service 层,仅做参数传递与简单判断:
public class UserService { private UserDAO userDAO; public UserService() { this.userDAO = new UserDAO(); // 无 DI 容器,手动 new } /** * 用户登录验证 * @param username 用户名(明文) * @param password 密码(明文) * @return 匹配的 User 对象,未匹配返回 null */ public User login(String username, String password) { // 1. 对密码进行 MD5 加密(模拟前端未加密传输,后端加密比对) String encryptedPassword = MD5Util.encode(password); // 2. 调用 DAO 查询数据库 return userDAO.findByUsernameAndPassword(username, encryptedPassword); } }关键点:
MD5Util.encode()是一个静态工具方法,内部调用MessageDigest.getInstance("MD5"),不加盐(salt),仅为教学演示 MD5 流程;- Service 层未做事务控制(如
@Transactional),因单表查询无需事务,也避免引入复杂概念; - 方法签名明确标注参数含义,
@return注释强调null的语义——这是 J2EE 时代防御性编程的习惯。
3.3 数据访问层:UserDAO如何用 JDBC 执行参数化查询
UserDAO.java是 JDBC 操作的核心:
public class UserDAO { public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, email FROM users WHERE username = ? AND password = ?"; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setString(1, username); // ⚠️ 参数索引从 1 开始,非 0! ps.setString(2, password); rs = ps.executeQuery(); if (rs.next()) { // 2. 封装结果为 User 对象(手动映射,无 ORM) User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setEmail(rs.getString("email")); return user; } return null; // 未查到返回 null,由上层判断 } catch (SQLException e) { e.printStackTrace(); // 教学项目直接打印,生产环境应记录日志 return null; } finally { // 3. 必须在 finally 中关闭资源,防止连接泄漏 DBUtil.close(conn, ps, rs); } } }参数说明:
ps.setString(1, username):?占位符按顺序绑定,索引从 1 开始,这是 JDBC 规范,新手常错写成ps.setString(0, ...);rs.next()返回true表示有下一行数据,必须调用才能移动游标,否则rs.getString()会抛SQLException;finally块中调用DBUtil.close()是硬性要求,即使executeQuery()抛异常,连接也必须关闭,否则 Tomcat 启动几次后就会报Cannot create PoolableConnectionFactory。
3.4 前端视图层:login.jsp如何安全显示错误信息
login.jsp不是静态 HTML,而是包含 Java 脚本片段的动态页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>用户登录</title></head> <body> <h2>用户登录</h2> <!-- 1. 显示错误信息(来自 request.setAttribute) --> <% if (request.getAttribute("error") != null) { %> <div style="color: red; font-weight: bold;"> <%= request.getAttribute("error") %> </div> <% } %> <!-- 2. 表单提交到 /login --> <form action="/bookstore/login" method="post"> <label>用户名:<input type="text" name="username" value="<%= request.getParameter("username") != null ? request.getParameter("username") : "" %>"></label><br> <label>密码:<input type="password" name="password"></label><br> <button type="submit">登录</button> </form> </body> </html>关键技巧:
<%= request.getAttribute("error") %>直接输出错误信息,未做 HTML 转义(教学简化),实际项目应使用StringEscapeUtils.escapeHtml4()防 XSS;- 表单中
value="<%= ... %>"实现登录失败后用户名回填,提升用户体验,这是 JSP 时代最朴素的状态保持方案; contentType="text/html;charset=UTF-8"声明页面编码,与数据库连接参数characterEncoding=UTF-8形成闭环,缺一不可。
4. 运行与部署实操指南:从零开始启动这个 J2EE 书城
4.1 环境准备清单:Tomcat、JDK、MySQL 版本确认
项目对运行环境有明确约束,必须严格匹配:
| 组件 | 推荐版本 | 为什么必须这个版本 | 验证命令 |
|---|---|---|---|
| JDK | 1.7 或 1.8 | UserDAO.java使用try-with-resources语法(JDK 7+),web.xml声明version="3.1"要求 JDK 7+ | java -version |
| Tomcat | 7.0.39 ~ 7.0.109 | Tomcat 7 支持 Servlet 3.0,与web.xml版本一致;Tomcat 8+ 默认启用异步 Servlet,可能干扰同步流程理解 | $TOMCAT_HOME/bin/version.sh |
| MySQL | 5.5 ~ 5.7 | mysql-connector-java-5.1.47.jar仅兼容 MySQL 5.x;MySQL 8.0 需要mysql-connector-java-8.0.x | mysql --version |
注意:若使用 JDK 11+ 或 Tomcat 9+,
servlet-api.jar会与容器内置 API 冲突,导致ClassNotFoundException: javax.servlet.http.HttpServlet。务必使用配套版本。
4.2 数据库初始化:四步完成建库、建表、插数据
创建数据库并指定编码(避免中文乱码):
CREATE DATABASE bookstore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;执行建表 SQL(
db_schema.sql):USE bookstore; -- 粘贴 db_schema.sql 全部内容插入测试用户(默认账号用于快速验证):
INSERT INTO users(username, password, email) VALUES ('admin', '21232f297a57a5a743894a0e4a801fc3', 'admin@example.com'); -- 密码 'admin' 的 MD5 值:21232f297a57a5a743894a0e4a801fc3验证数据:
SELECT * FROM users; -- 应返回 1 行,password 字段为 32 位十六进制字符串
4.3 项目部署步骤:三分钟完成 Tomcat 部署
解压项目到 Tomcat webapps 目录:
# 假设项目压缩包名为 cloudfactory-bookstore.zip unzip cloudfactory-bookstore.zip -d $TOMCAT_HOME/webapps/ # 解压后生成 $TOMCAT_HOME/webapps/WebContent/ 目录重命名 WebContent 为应用名(关键!):
cd $TOMCAT_HOME/webapps/ mv WebContent/ bookstore/ # 应用上下文路径变为 /bookstore复制 MySQL 驱动到 Tomcat lib(否则
Class.forName()找不到驱动):cp mysql-connector-java-5.1.47.jar $TOMCAT_HOME/lib/启动 Tomcat 并验证:
$TOMCAT_HOME/bin/startup.sh # Linux/Mac # 或 startup.bat(Windows)浏览器访问
http://localhost:8080/bookstore/login.jsp,输入admin/admin应成功登录。
4.4 常见问题排查:五条血泪经验帮你绕过部署雷区
现象 1:访问/bookstore/login.jsp显示 404,但/bookstore/index.jsp正常
原因:web.xml中<servlet-mapping>的<url-pattern>与表单action不匹配。例如web.xml写<url-pattern>/login</url-pattern>,但表单写action="login.jsp"(应为action="/bookstore/login")。
解决:检查login.jsp表单action是否为/bookstore/login,且web.xml中<servlet-name>与<servlet-mapping>的<servlet-name>完全一致(大小写敏感)。
现象 2:登录时控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:mysql-connector-java-5.1.47.jar未放入$TOMCAT_HOME/lib/,或放入了WEB-INF/lib/(Tomcat 7 下优先加载$TOMCAT_HOME/lib/,重复放置会导致类加载冲突)。
解决:确认 jar 包只在$TOMCAT_HOME/lib/,删除WEB-INF/lib/下的同名 jar。
现象 3:登录成功后跳转到index.jsp,但页面显示NullPointerException
原因:index.jsp中尝试访问session.getAttribute("currentUser"),但LoginServlet中session.setAttribute("currentUser", user)未执行(如user为 null 时未正确 return)。
解决:在LoginServlet.java的if (user != null)分支前加System.out.println("User: " + user);,确认user对象是否为空。
现象 4:图书列表页中文显示为???
原因:三个环节任一缺失 UTF-8 编码:①web.xml未声明contentType;②DBUtil.java连接 URL 缺少characterEncoding=UTF-8;③ MySQL 数据库/表字符集非utf8mb4。
解决:依次检查:login.jsp第一行<%@ page contentType="text/html;charset=UTF-8" %>;DBUtil.java的URL字符串;MySQL 中SHOW CREATE DATABASE bookstore;。
现象 5:修改代码后重启 Tomcat,页面无变化
原因:Tomcat 的work/Catalina/localhost/bookstore/目录缓存了 JSP 编译后的.java和.class文件,未自动更新。
解决:停止 Tomcat → 删除$TOMCAT_HOME/work/Catalina/localhost/bookstore/全部内容 → 启动 Tomcat。
5. 二次开发实战:给书城添加搜索功能的完整改造流程
5.1 需求分析与接口设计:从用户输入到结果展示
假设产品经理提出:“首页增加一个搜索框,支持按书名模糊查询”。这不是简单加个 input,而是要打通从前端到数据库的完整链路。我们按 J2EE 分层原则设计:
- Controller 层:新增
SearchServlet,接收keyword参数,调用 Service; - Service 层:新增
BookService.searchByTitle(String keyword),处理关键词清洗(如 trim、防 SQL 注入); - DAO 层:新增
BookDAO.searchByTitle(String keyword),执行LIKE查询; - View 层:修改
index.jsp,添加搜索表单,并在book_list.jsp中兼容搜索结果。
关键决策:不修改现有BookListServlet,而是新增独立 Servlet,避免污染原有逻辑。这是 J2EE 时代“小步快跑”的典型做法——每个功能对应一个最小可部署单元。
5.2 DAO 层改造:手写安全的 LIKE 查询
BookDAO.java新增方法:
/** * 按书名模糊查询(防 SQL 注入:使用 PreparedStatement + % 占位符) * @param keyword 关键词(前后自动添加 %) * @return 匹配的图书列表 */ public List<Book> searchByTitle(String keyword) { // 1. 安全处理关键词:去除首尾空格,替换特殊字符(教学简化,生产环境需更严格) if (keyword == null) keyword = ""; keyword = keyword.trim(); if (keyword.isEmpty()) return new ArrayList<>(); // 2. 构造 SQL:使用 ? 占位符,避免字符串拼接 String sql = "SELECT id, title, author, price, stock FROM books WHERE title LIKE ?"; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; List<Book> bookList = new ArrayList<>(); try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setString(1, "%" + keyword + "%"); // ⚠️ % 符号在 Java 字符串中,不在 SQL 中! rs = ps.executeQuery(); while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getBigDecimal("price")); book.setStock(rs.getInt("stock")); bookList.add(book); } return bookList; } catch (SQLException e) { e.printStackTrace(); return new ArrayList<>(); } finally { DBUtil.close(conn, ps, rs); } }参数说明:
ps.setString(1, "%" + keyword + "%"):%是 SQLLIKE的通配符,必须在 Java 层拼接,而非写在 SQL 字符串里(如"title LIKE '%?%'"是错误的,会导致参数失效);rs.getBigDecimal("price"):price字段为DECIMAL,用getBigDecimal精确获取,避免getDouble()的浮点误差;- 空关键词直接返回空列表,避免执行
WHERE title LIKE '%%'(全表扫描)。
5.3 Service 与 Controller 层联动:传递参数与处理空结果
BookService.java新增:
public class BookService { private BookDAO bookDAO; public BookService() { this.bookDAO = new BookDAO(); } /** * 搜索图书(业务层可添加缓存、日志等) * @param keyword 搜索关键词 * @return 图书列表 */ public List<Book> searchByTitle(String keyword) { // 教学场景:简单清洗,生产环境可加入敏感词过滤 if (keyword != null && keyword.contains("'")) { System.out.println("警告:检测到单引号,已忽略搜索"); return new ArrayList<>(); } return bookDAO.searchByTitle(keyword); } }SearchServlet.java(新建文件):
public class SearchServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取关键词(GET 请求,参数在 URL 中) String keyword = request.getParameter("keyword"); // 2. 调用 Service 搜索 BookService bookService = new BookService(); List<Book> bookList = bookService.searchByTitle(keyword); // 3. 将结果存入 request,转发到 book_list.jsp request.setAttribute("bookList", bookList); request.setAttribute("searchKeyword", keyword); // 用于搜索框回显 // 4. 转发(非重定向),保持 URL 为 /bookstore/search?keyword=java request.getRequestDispatcher("/book_list.jsp").forward(request, response); } }注意:
SearchServlet只处理doGet,因搜索表单是method="get",URL 可书签化;若用POST,则需覆盖doPost。
5.4 前端视图改造:搜索表单与结果页兼容
修改
index.jsp,添加搜索表单:<form action="/bookstore/search" method="get"> <input type="text" name="keyword" placeholder="搜索图书..." value="${param.keyword != null ? param.keyword : ''}"> <button type="submit">搜索</button> </form>修改
book_list.jsp,兼容搜索与普通列表:<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>图书列表</title></head> <body> <h2>图书列表</h2> <!-- 搜索关键词回显 --> <% if (request.getAttribute("searchKeyword") != null) { %> <p>搜索结果:"<%= request.getAttribute("searchKeyword") %>"</p> <% } %> <!-- 图书表格 --> <table border="1"> <tr><th>书名</th><th>作者</th><th>价格</th><th>库存</th></tr> <% List<Book> bookList = (List<Book>) request.getAttribute("bookList"); if (bookList == null || bookList.isEmpty()) { out.print("<tr><td colspan='4'>暂无图书</td></tr>"); } else { for (Book book : bookList) { %> <tr> <td><%= book.getTitle() %></td> <td><%= book.getAuthor() %></td> <td>¥<%= book.getPrice() %></td> <td><%= book.getStock() %></td> </tr> <% } } %> </table> </body> </html>更新
web.xml,注册新 Servlet:<servlet> <servlet-name>SearchServlet</servlet-name> <servlet-class>com.cloudfactory.controller.SearchServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>SearchServlet</servlet-name> <url-pattern>/search</url-pattern> </servlet-mapping>
5.5 验证与调试技巧:如何确保搜索功能真正可用
改造完成后,按以下步骤验证:
- URL 测试:直接访问
http://localhost:8080/bookstore/search?keyword=java,应显示包含 “java” 的图书; - 表单测试:在
index.jsp输入 “深入”,点击搜索,URL 应变为/search?keyword=深入,页面显示对应图书; - 边界测试:输入空格、单引号
'、SQL 关键字select,应返回空列表或友好提示(当前代码对单引号有简单拦截); - 日志追踪:在
SearchServlet.java的doGet开头加System.out.println("Search keyword: " + keyword);,重启 Tomcat 后查看catalina.out日志,确认参数接收正确。
从那以后我每次给 J2EE 项目加新功能,都强制走一遍这四步:① 在web.xml里先写好 servlet 映射(哪怕类还没建);② 创建空的 Servlet 类,只写doGet打印日志;③ 部署后用 curl 测试 URL;④ 确认请求能进来,再逐步填充业务逻辑。这套流程帮我避开了 90% 的“找不到页面”和“参数为空”问题。希望帮到你。
本文还有配套的精品资源,点击获取