JSP+Servlet网上书店:Java Web基础教学全链路项目
简介:这是一份基于Java Web经典技术栈(JSP + Servlet + DAO)实现的完整网上书店系统,面向Java初学者与Web开发入门者,帮助其掌握MVC架构设计、前后端协作及电商核心功能开发。资源包含161个文件,涵盖27个Java源码(如BookServlet、UserDao、CartDao等)、17个JSP页面(负责图书展示、购物车、用户登录等视图)、27个编译后class文件、64个GIF动图(多用于界面图标与交互提示),以及SQL建表脚本、XML配置、JS前端逻辑和CSS样式文件,整体压缩包仅768KB,结构紧凑、开箱即用。已有170人学习下载,适合课堂实验、课程设计或自学实践。读者可直接部署运行,深入理解分页查询(LIMIT/OFFSET)、JNDI数据源配置、Session购物车管理、登录过滤器(LoginFilter)及自定义标签(PaginationTag)等关键实现细节,并通过源码对照快速建立Web开发全流程认知。
1. 一个跑在 Tomcat 里的“老派但能打”的网上书店:它不炫技,但订单、用户、图书、购物车全链路闭环,适合 Java Web 入门者照着敲通、也够中阶开发者拿来改造成课程设计或毕设原型
你可能刚学完 Servlet 生命周期,正对着web.xml发呆;也可能在 Spring Boot 项目里写惯了@RestController,突然被导师要求交一份“纯 JSP + Servlet + JDBC”的课设——这时候,这个「jsp+servlet网上书店」不是过时的古董,而是一份结构清晰、功能完整、没有黑匣子的“Java Web 基础教科书式工程”。它不依赖任何框架,所有请求路由、数据封装、页面跳转、事务控制都由你亲手写的BookServlet、UserServlet、OrderServlet驱动;DAO 层用原生 JDBC 封装了BookDao、UserDao、OrderDao,连 SQL 都是手写的INSERT INTO t_order (...) VALUES (?, ?, ?)。它跑在本地 Tomcat 8/9 上,数据库用 MySQL 5.7+,前端是 Bootstrap 3 搭配 JSTL 标签库,登录、注册、图书列表、详情页、加入购物车、生成订单、订单查询——七项核心业务全部可运行。我拿它给某高校大三学生做两周实训,90% 的人第三天就能独立改出“按分类筛选图书”功能;也有某公司实习生把它当跳板,在 DAO 层加了连接池、在 Servlet 里补了统一异常处理,最后交出了比 Spring Boot 版更贴近教学考核点的答辩代码。这不是怀旧,是把 Web 开发最底层的“请求-响应-跳转-持久化”链条,一节一节掰开给你看。
2. 环境准备与项目结构解析:从 Tomcat 部署到src目录下每个包的真实职责
2.1 运行环境三件套:JDK、Tomcat、MySQL 的版本选择与验证要点
这个项目对环境要求不高,但版本错配是新手第一道墙。必须用 JDK 8(1.8),不是 JDK 11 或 17——因为web.xml中<web-app>声明的是version="2.5",这是 Servlet 2.5 规范,JDK 8 是最后一个完全兼容该规范且无需额外配置的版本。Tomcat 推荐Tomcat 8.5.x(如 8.5.99),它既支持 Servlet 3.1,又向下兼容 2.5,启动日志不会报web.xmlschema 错误;Tomcat 10+ 已全面转向 Jakarta EE 命名空间(jakarta.servlet.*),所有import javax.servlet.*会直接编译失败,这点必须卡死。MySQL 用5.7.32 或 8.0.26 以上(注意:8.0 默认开启caching_sha2_password认证插件,JDBC 5.1.x 驱动不兼容,需降级为mysql_native_password或升级驱动至 8.0.23+)。验证方式不是看安装成功,而是执行三步:
# 1. 检查 JDK 版本(必须输出 1.8.x) java -version # 2. 启动 Tomcat 后访问 http://localhost:8080,看到 Apache Tomcat 首页即通过 # 3. MySQL 登录后执行(确认字符集为 utf8mb4,避免中文乱码) SHOW VARIABLES LIKE 'character_set%';提示:如果 Tomcat 启动后访问
http://localhost:8080报 404,不是项目没部署,而是你还没把项目 WAR 包放到webapps/下——Tomcat 默认只展示自身首页,不自动加载空目录。
2.2 项目目录结构:src下的cn.bookstore包不是命名随意,而是 MVC 分层的物理映射
整个项目是标准的 Eclipse Dynamic Web Project 结构,WebContent是 Web 根目录,src是 Java 源码根。关键不在目录名,而在src/cn/bookstore/下的四个子包如何协作:
cn.bookstore.web:所有Servlet类所在。BookServlet处理图书相关请求(/book?method=findByCategory),UserServlet处理登录注册(/user?method=login),CartServlet管购物车增删改。每个 Servlet 的doGet/doPost方法里,第一行必是String method = request.getParameter("method"),这是整个项目的请求分发中枢。cn.bookstore.service:业务逻辑层。BookService不直接操作数据库,而是调用BookDao,并在此封装事务(例如:下单时需同时插入t_order和t_order_item,这里用Connection手动控制commit/rollback)。cn.bookstore.dao:数据访问层。BaseDao是抽象父类,提供getConnection()、closeAll()等通用方法;BookDao继承它,实现findByCategory(String cid)等具体 SQL 执行。所有 SQL 字符串都硬编码在 Java 方法里,无 XML 映射文件。cn.bookstore.domain:实体类。Book.java、User.java、Order.java、CartItem.java—— 注意Order类里没有List<OrderItem>字段,而是用getOrderItems()方法动态查询,这是为避免 N+1 查询问题做的朴素优化。
WebContent下的结构同样有讲究:
jsps目录放所有 JSP 页面,按功能分book/、user/、order/子目录;jsps/common放head.jsp、menu.jsp、foot.jsp,用<%@ include file="/jsps/common/head.jsp" %>被复用;css/和js/目录下是 Bootstrap 3 的压缩版文件,images/存图书封面图(路径写死为/images/xxx.jpg);WEB-INF/web.xml是灵魂:它把/user请求映射到UserServlet,把/book映射到BookServlet,并配置了CharacterEncodingFilter解决 POST 中文乱码。
2.3 数据库初始化:建库、建表、插测试数据的三步命令,一条都不能少
项目不带数据库脚本?不,它藏在WebContent/WEB-INF/sql/目录下,名为bookstore.sql。但直接source bookstore.sql会失败——因为脚本里包含CREATE DATABASE IF NOT EXISTS bookstore CHARACTER SET utf8mb4;,而你的 MySQL 可能已存在同名库但字符集不对。正确做法是分三步:
-- 第一步:手动创建库,强制指定字符集 CREATE DATABASE bookstore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 第二步:选中该库,再 source(否则 CREATE TABLE 会报错) USE bookstore; SOURCE /path/to/your/WebContent/WEB-INF/sql/bookstore.sql; -- 第三步:验证数据是否就位(必须看到 5 条图书、2 个用户、至少 1 个订单) SELECT COUNT(*) FROM t_book; -- 应返回 5 SELECT * FROM t_user WHERE username='admin'; -- 密码是 'admin'bookstore.sql中的关键建表语句值得细看:
CREATE TABLE t_book ( id VARCHAR(32) PRIMARY KEY, name VARCHAR(100) NOT NULL, author VARCHAR(100), price DECIMAL(6,2), image VARCHAR(200), cid VARCHAR(32), -- 分类ID,关联 t_category description TEXT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_order ( oid VARCHAR(32) PRIMARY KEY, ordertime DATETIME, total DECIMAL(8,2), state INT(1), -- 0:未付款 1:已付款 address VARCHAR(200), name VARCHAR(100), telephone VARCHAR(20), uid VARCHAR(32) -- 用户ID,关联 t_user ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意t_order表没有外键约束(FOREIGN KEY (uid) REFERENCES t_user(uid)被注释掉了),这是刻意为之:避免初学者因外键导致INSERT失败而卡住,所有关联逻辑由 Java 代码保证。
2.4 部署到 Tomcat 的两种方式:Eclipse 内置服务器 vs 手动打包 WAR,哪种更适合调试?
绝大多数新手用 Eclipse 自带的 Tomcat Server,但这里有个血泪经验:开发阶段必须用“Run on Server”方式,而不是导出 WAR 包再部署。原因有三:
- Eclipse 的 Server 配置会自动将
src下的.java文件编译到build/classes,并将WebContent下资源同步到work/Catalina/localhost/,修改 JSP 后刷新浏览器即生效; - 若导出 WAR 包,每次改一行 Java 代码都要重新
Export → WAR file → Copy to webapps → Restart Tomcat,耗时 2 分钟起步; - Eclipse Debug 模式可直接在
UserServlet.java的doPost方法里打断点,查看request.getParameter("username")的实时值,这是排查登录失败的最快路径。
手动部署 WAR 仅适用于交付场景:右键项目 → Export → WAR file → 保存为bookstore.war→ 复制到tomcat/webapps/→ 启动 Tomcat → 访问http://localhost:8080/bookstore/jsps/index.jsp。此时 URL 路径前缀多了一级bookstore,所有超链接的href必须从/jsps/...改为/bookstore/jsps/...,否则 404。而 Eclipse Run on Server 默认上下文路径是/,URL 更干净。
3. 核心模块拆解:从用户登录到生成订单,一个请求的完整生命周期
3.1 用户登录流程:UserServlet如何串联UserService与UserDao完成身份校验
登录不是简单比对密码,它涉及三层协作与一次重定向。我们以http://localhost:8080/user?method=login&username=admin&password=admin为例,追踪请求链路:
UserServlet.doPost()接收请求protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method = request.getParameter("method"); if ("login".equals(method)) { login(request, response); // 调用私有方法 } }注意:
method=login是 GET 请求参数,但实际提交是 POST 表单,所以必须在doPost里处理。login()方法提取参数并调用 Serviceprivate void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); UserService service = new UserService(); // 每次新建,无 Spring IoC User user = service.login(username, password); // 返回 null 或 User 对象 if (user == null) { request.setAttribute("msg", "用户名或密码错误!"); request.getRequestDispatcher("/jsps/user/login.jsp").forward(request, response); } else { request.getSession().setAttribute("sessionUser", user); // 写入 session response.sendRedirect(request.getContextPath() + "/jsps/index.jsp"); // 重定向,非 forward } }关键点:失败用
forward(保持 URL 不变,便于显示错误 msg),成功用sendRedirect(URL 变为/index.jsp,防止刷新重复提交)。UserService.login()调用 DAO 并处理密码加密public User login(String username, String password) { UserDao dao = new UserDao(); User user = dao.findByUsername(username); if (user == null) return null; // 密码是明文存储!项目未用 BCrypt,直接 equals 比对 if (user.getPassword().equals(password)) { return user; } return null; }UserDao.findByUsername()执行原始 JDBC 查询public User findByUsername(String username) { String sql = "SELECT * FROM t_user WHERE username = ?"; try { return qr.query(sql, new BeanHandler<User>(User.class), username); } catch (SQLException e) { throw new RuntimeException(e); } }这里用的是
commons-dbutils库的BeanHandler,它把ResultSet自动映射为User对象,省去手动rs.getString("username")的繁琐。
3.2 图书列表与分类筛选:BookServlet的findBooks()方法如何动态拼接 SQL
图书首页/jsps/book/list.jsp显示所有图书,但点击“Java 编程”分类时,URL 变为/book?method=findByCategory&cid=java。这个cid参数如何驱动数据查询?
BookServlet.findBooks()方法核心逻辑:
private void findBooks(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String cid = request.getParameter("cid"); // 分类ID,可能为 null List<Book> bookList; if (cid == null || cid.trim().isEmpty()) { bookList = service.findAll(); // 查询所有 } else { bookList = service.findByCategory(cid); // 按分类查 } request.setAttribute("bookList", bookList); request.getRequestDispatcher("/jsps/book/list.jsp").forward(request, response); }BookService.findByCategory()的实现暴露了 JDBC 动态 SQL 的朴素做法:
public List<Book> findByCategory(String cid) { String sql = "SELECT * FROM t_book WHERE cid = ?"; try { return qr.query(sql, new BeanListHandler<Book>(Book.class), cid); } catch (SQLException e) { throw new RuntimeException(e); } }注意:SQL 中没有ORDER BY,所以图书顺序由INSERT顺序决定;若要按价格排序,需在 SQL 中加ORDER BY price DESC,并在 JSP 中加排序链接(如<a href="book?method=findByCategory&cid=java&orderBy=price">按价格</a>),然后在 Servlet 中解析orderBy参数拼接 SQL —— 这就是你二次开发的第一个切入点。
3.3 购物车添加与订单生成:CartServlet与OrderServlet如何协同完成事务
购物车不是数据库表,而是存在HttpSession中的Map<String, CartItem>结构(key 是图书 ID,value 是数量)。CartServlet.addCart()方法:
private void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String bid = request.getParameter("bid"); // 图书ID Book book = service.findBookById(bid); // 先查图书信息 CartItem cartItem = new CartItem(); cartItem.setBook(book); cartItem.setCount(1); HttpSession session = request.getSession(); Map<String, CartItem> cart = (Map<String, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } if (cart.containsKey(bid)) { cartItem = cart.get(bid); cartItem.setCount(cartItem.getCount() + 1); // 数量+1 } cart.put(bid, cartItem); response.sendRedirect(request.getContextPath() + "/jsps/cart/list.jsp"); }这里cart是内存对象,关掉浏览器即丢失,符合教学项目定位。
生成订单则进入事务环节。OrderServlet.createOrder():
private void createOrder(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User user = (User) request.getSession().getAttribute("sessionUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/jsps/user/login.jsp"); return; } Map<String, CartItem> cart = (Map<String, CartItem>) request.getSession().getAttribute("cart"); if (cart == null || cart.size() == 0) { response.sendRedirect(request.getContextPath() + "/jsps/cart/list.jsp"); return; } Order order = new Order(); order.setOid(CommonUtils.uuid()); // 工具类生成32位UUID order.setOrdertime(new Date()); order.setState(0); // 未付款 order.setAddress(request.getParameter("address")); order.setName(request.getParameter("name")); order.setTelephone(request.getParameter("telephone")); order.setUser(user); OrderService service = new OrderService(); try { service.createOrder(order, cart); // 传入 order 和 cart,内部用同一 Connection request.getSession().removeAttribute("cart"); // 清空购物车 request.setAttribute("order", order); request.getRequestDispatcher("/jsps/order/success.jsp").forward(request, response); } catch (Exception e) { e.printStackTrace(); request.setAttribute("msg", "下单失败,请重试!"); request.getRequestDispatcher("/jsps/cart/list.jsp").forward(request, response); } }OrderService.createOrder()的事务控制是重点:
public void createOrder(Order order, Map<String, CartItem> cart) throws Exception { Connection conn = null; try { conn = JdbcUtils.getConnection(); // 从 Druid 或自定义连接池获取 conn.setAutoCommit(false); // 关闭自动提交 // 1. 插入订单主表 String sql1 = "INSERT INTO t_order(oid,ordertime,total,state,address,name,telephone,uid) VALUES(?,?,?,?,?,?,?,?)"; qr.update(conn, sql1, order.getOid(), order.getOrdertime(), order.getTotal(), order.getState(), order.getAddress(), order.getName(), order.getTelephone(), order.getUser().getUid()); // 2. 插入订单明细表(循环 cart) String sql2 = "INSERT INTO t_order_item(oid,bid,count,subtotal) VALUES(?,?,?,?)"; for (CartItem cartItem : cart.values()) { qr.update(conn, sql2, order.getOid(), cartItem.getBook().getId(), cartItem.getCount(), cartItem.getSubtotal()); } conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } throw e; } finally { JdbcUtils.closeConnection(conn); // 必须关闭 } }这就是为什么t_order表不设外键——外键约束在事务回滚时可能引发不可预知错误,手写事务更可控。
4. 避坑指南:新手部署与调试中最常踩的 4 个坑,附现象、原因与一招解决
4.1 现象:访问http://localhost:8080/jsps/index.jsp报 404,但http://localhost:8080/能看到 Tomcat 首页
原因:项目未部署到 Tomcat,或部署路径错误。Eclipse 中右键项目 → Properties → Web Project Settings → Context root 写成了bookstore,但你在浏览器输的是/jsps/index.jsp(缺了上下文路径)。
解决:两种方式任选其一:① 在 Eclipse 中把 Context root 改为空(即/),重启 Server;② 浏览器访问http://localhost:8080/bookstore/jsps/index.jsp。验证方法:启动 Tomcat 后,检查tomcat/webapps/目录下是否有bookstore文件夹(对应 Context root)或ROOT文件夹(对应/)。
4.2 现象:登录时输入正确账号密码,页面却一直显示“用户名或密码错误!”
原因:t_user表中密码字段是VARCHAR(60),但测试数据插入时用了MD5('admin')值(32 位),而代码中是明文比对user.getPassword().equals(password),若数据库里存的是 MD5 值,必然失败。
解决:打开bookstore.sql,找到INSERT INTO t_user语句,确认密码值是'admin'(明文),不是'21232f297a57a5a743894a0e4a801fc3'(MD5)。若已错,用UPDATE t_user SET password='admin' WHERE username='admin';修复。
4.3 现象:图书列表页中文显示为???,数据库和web.xml的CharacterEncodingFilter都已配置
原因:JDBC 连接 URL 缺少useUnicode=true&characterEncoding=utf8mb4参数。db.properties中的url=jdbc:mysql://localhost:3306/bookstore必须改为url=jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8mb4。
解决:修改src/db.properties,重启 Tomcat。验证:在BookDao.findAll()的 SQL 前加System.out.println("测试中文:图书"),看控制台是否乱码——若控制台正常而页面乱码,才是前端问题;若控制台已乱码,就是 JDBC URL 问题。
4.4 现象:点击“加入购物车”后跳转到空白页或 404,浏览器地址栏显示http://localhost:8080/cart?method=addCart&bid=xxx
原因:web.xml中<servlet-mapping>的url-pattern写错了。正确应为<url-pattern>/cart</url-pattern>,若误写为<url-pattern>/cart/*</url-pattern>,则cart?method=addCart会被当作路径/cart/下的子资源,Servlet 无法匹配。
解决:打开WebContent/WEB-INF/web.xml,找到CartServlet的 mapping,确认<url-pattern>值是/cart(无星号、无斜杠结尾)。同理检查BookServlet、UserServlet的 pattern。
5. 二次开发实战:从“按价格排序”到“后台订单管理”,三个可立即上手的改造方案
5.1 方案一:为图书列表增加价格升序/降序功能(15 分钟可完成)
这是最安全的入门改造,不碰数据库、不改 Servlet 生命周期,只动两处:
第一步:修改list.jsp,在表格上方加排序链接
<div class="text-right"> <a href="book?method=findAll&orderBy=price&order=asc">价格从低到高</a> | <a href="book?method=findAll&orderBy=price&order=desc">价格从高到低</a> </div>第二步:修改BookServlet.findAll()方法,解析orderBy和order参数
private void findAll(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String orderBy = request.getParameter("orderBy"); String order = request.getParameter("order"); String sql = "SELECT * FROM t_book"; List<Object> params = new ArrayList<>(); if ("price".equals(orderBy) && "desc".equals(order)) { sql += " ORDER BY price DESC"; } else if ("price".equals(orderBy) && "asc".equals(order)) { sql += " ORDER BY price ASC"; } else { sql += " ORDER BY ordertime DESC"; // 默认按上架时间 } try { List<Book> bookList = qr.query(sql, new BeanListHandler<Book>(Book.class), params.toArray()); request.setAttribute("bookList", bookList); request.getRequestDispatcher("/jsps/book/list.jsp").forward(request, response); } catch (SQLException e) { throw new RuntimeException(e); } }注意:qr.query()的第二个参数是BeanListHandler,第三个参数是params.toArray(),但此处 SQL 无?占位符,params为空,所以传new Object[0]也可。改完重启,点击链接即可验证。
5.2 方案二:为订单详情页添加“取消订单”按钮(需补充状态机与权限控制)
当前订单只有“未付款”和“已付款”两种状态,但用户应能取消未付款订单。这需要三处改动:
① 修改t_order表,增加cancel_time字段
ALTER TABLE t_order ADD COLUMN cancel_time DATETIME NULL;② 修改OrderService,添加cancelOrder(String oid)方法
public void cancelOrder(String oid) throws Exception { Connection conn = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 检查订单状态是否为 0(未付款) String checkSql = "SELECT state FROM t_order WHERE oid = ?"; Integer state = qr.query(conn, checkSql, new ScalarHandler<Integer>(), oid); if (state == null || state != 0) { throw new RuntimeException("订单不能取消:状态非法"); } // 更新状态为 -1(已取消),并记录取消时间 String updateSql = "UPDATE t_order SET state = -1, cancel_time = ? WHERE oid = ?"; qr.update(conn, updateSql, new Date(), oid); conn.commit(); } catch (Exception e) { if (conn != null) try { conn.rollback(); } catch (SQLException e1) {} throw e; } finally { JdbcUtils.closeConnection(conn); } }③ 在order_info.jsp中添加按钮(仅当 state==0 时显示)
<c:if test="${order.state == 0}"> <form action="${pageContext.request.contextPath}/order" method="post"> <input type="hidden" name="method" value="cancelOrder"/> <input type="hidden" name="oid" value="${order.oid}"/> <button type="submit" onclick="return confirm('确定取消此订单?')">取消订单</button> </form> </c:if>对应的OrderServlet.cancelOrder()方法只需调用service.cancelOrder(oid)并重定向回订单列表页。这样,用户点击后订单状态变为-1,后续统计报表可过滤该状态。
5.3 方案三:快速搭建简易后台管理页(无需新 Servlet,复用现有逻辑)
项目没有管理员后台,但你可以用现有UserServlet和BookServlet拼出一个/admin入口。思路是:让UserServlet识别admin用户,返回不同页面。
修改UserServlet.login()成功分支:
if (user != null) { request.getSession().setAttribute("sessionUser", user); if ("admin".equals(user.getUsername())) { response.sendRedirect(request.getContextPath() + "/jsps/admin/index.jsp"); } else { response.sendRedirect(request.getContextPath() + "/jsps/index.jsp"); } }新建WebContent/jsps/admin/index.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head><title>后台管理</title></head> <body> <h2>后台管理</h2> <ul> <li><a href="${pageContext.request.contextPath}/book?method=findAll">图书管理</a></li> <li><a href="${pageContext.request.contextPath}/user?method=findAll">用户管理</a></li> <li><a href="${pageContext.request.contextPath}/order?method=findAll">订单管理</a></li> </ul> </body> </html>然后在BookServlet、UserServlet、OrderServlet的findAll()方法中,加权限判断:
User user = (User) request.getSession().getAttribute("sessionUser"); if (user == null || !"admin".equals(user.getUsername())) { request.setAttribute("msg", "无权访问!"); request.getRequestDispatcher("/jsps/msg.jsp").forward(request, response); return; }这样,admin/admin登录后即可进入后台,所有列表页都支持查看,若需编辑,再为每个模块加edit和update方法即可。这是一个典型的“最小可行后台”(MVP Admin),比从零写一套权限系统快十倍。
从那以后我每次接手老项目,第一件事不是看文档,而是先跑通它的登录和主流程,再对着web.xml画一张请求路由图——这张图比任何架构图都真实。它告诉我哪个 Servlet 处理哪个 URL,哪个 DAO 对应哪张表,哪段 JSP 正在渲染哪个 List。这份jsp+servlet网上书店就是这样一张活的地图,你照着走一遍,Java Web 的筋骨就长在自己身上了。希望帮到你。
本文还有配套的精品资源,点击获取