拓冰建站拓冰建站
首页 / 资讯中心 / 正文

JSP+Bean+Servlet构建网上购书系统:经典JavaWeb架构全流程实践

简介这是一份基于JSPBeanServlet的B/S结构Java Web课程设计资源采用经典MVC思想实现了一个简单的网上购书系统核心功能包括用户注册、用户登录、信息查询与信息浏览。压缩包共55个文件其中包含15个Java源文件、与之一一对应的15个class文件、6个JSP页面还附上了SQL数据库脚本、XML和Properties配置文件、PDF综合训练文档以及README说明等整包仅1.21MB目录结构划分清晰能方便地按源码、页面、配置、文档等维度分类查阅。已有170人学习下载对于正在做课程设计或刚开始接触Java Web的读者来说是一份不错的实战参考。通过阅读源码可以直观理解Servlet作为控制层、Bean作为模型层、JSP作为视图层的协作方式借助配套的SQL脚本和PDF文档还能快速搭建数据库与项目环境并逐段梳理注册、登录、信息查询与浏览功能的完整实现思路适合边看边练。1. 一套 JspBeanServlet 的旧货为什么我还推荐把它跑通你在网盘里搜到一份“基于 JspBeanServlet 实现Web简单的网上购书系统”编号通常长这样100010129。解压、导入 Eclipse、配 Tomcat然后被一屏幕红叉劝退。我见过很多人在这一步关掉窗口转身去搜 Spring Boot 的教程——但这类技术栈在 2024 年并没有死课程设计还在布置老系统还在线上跑甚至面试时还会被追问一句HttpServletRequest长什么样你见没见过。JSP、Bean、Servlet 三者拆开看都很简单合起来就是一套没有 Spring 的 MVC。适合三种人要交期末作业的学生、接了老系统维护的一线工程师、以及想把请求到响应的每一环都看清的初学者。这一篇我把这套架构下做购书系统最稳妥的落地路径从头走一遍理论和代码都可直接抄。2. 拆开三块Bean 管数据、Servlet 管控制、Jsp 管展示2.1 一次购书请求的完整流转从 HttpServletRequest 到 Jsp 页面访问一个“图书列表”页面时浏览器发出的 GET 请求先落到 Servlet而不是直接落到 JSP。Servlet 里做三件事调 DAO 查数据、把结果放进 request 域、转发到 JSP 渲染。这是 JspBeanServlet 组合里最核心的节奏也是“简单网上购书系统”里 BookListServlet 的标准写法。WebServlet(/bookList) public class BookListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); ListBook books new BookDao().list(); request.setAttribute(books, books); request.getRequestDispatcher(/book_list.jsp) .forward(request, response); } }这段代码里有三个参数值得记住。setCharacterEncoding(UTF-8)必须写在任何getParameter之前否则 POST 参数按容器默认 ISO-8859-1 解码中文直接乱码setAttribute的 key 要和 JSP 里EL表达式取的名字一致forward不会改变浏览器地址栏 URL刷新会把同一个动作重复执行一次。组件分工很清晰BookDao.list()负责查数据库Book这个 JavaBean 只存数据JSP 只负责把books渲染成表格。实际维护中会遇到另一种写法直接在 JSP 里写%Java 代码访问数据库页面和逻辑全揉在一起。这种写法虽然也能跑通但 JSP 编译后本质上是一个 Servlet 类页面里写太多脚本片段会导致编译报错的位置和你写的代码对不上排查成本高。2.2 这里的 Bean 是什么POJO、session 里的购物车和 Spring 的 bean 不是一回事“Bean”这个词在不同语境下含义不同。本题里的 Bean 指 JavaBean不是 Spring 管理的那种 bean。很多初学者被spring bean生命周期、dispatcherservlet、servlet webapplicationcontext 和 rootwebapplicationcontext这些概念绕晕其实 JavaBean 的规范极其简单类要有无参构造方法、属性必须是 private、每个属性提供 public 的 getter/setter。别的都不强制。以购书系统里最简单的购物车条目为例public class CartItem implements Serializable { private Book book; private int quantity; public CartItem() { } public Book getBook() { return book; } public void setBook(Book book) { this.book book; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity quantity; } }实现Serializable的目的很直接当 Tomcat 重启、session 要做持久化时javax.servlet.http.HttpSession需要把对象序列化保存到磁盘。如果不实现接口session 持久化时会直接报异常。注意这个 Bean 的生命周期完全归你管在 Servlet 里new CartItem()就有方法执行完就没人引用它GC 随时回收。这和 Spring 容器里由ApplicationContext管理、带完整bean生命周期的 bean 是两套完全不同的模型。在这个系统里Bean 的价值不是有没有框架管而是数据和行为被固定到了同一种结构里。Book、User、CartItem都是数据BookDao、UserDao是数据访问类。它们之间通过 getter/setter 传递数据Servlet 在其中扮演调度者。这就是为什么这个系统能保持简单没有 XML 配置业务逻辑没有 AOP也没有自动装配代码的调用关系一眼能看穿。2.3 为什么是三者配合而不是三个里只留一个角色适合做的事不适合做的事JSP渲染表格、表单、页面跳转提示写业务逻辑、查数据库Servlet接收参数、调用业务方法、控制跳转方式拼 HTML、处理数据库连接Bean承载数据、封装简单的计算行为直接输出页面、访问数据库一般不做理论上只用 Servlet Bean 也能做购书系统Servlet 用 out.print 输出整张 HTML 表格但字符串拼 SQL、拼 HTML 会让代码爆炸。只用 JSP Bean 也能做在 JSP 里写jsp:useBean标签引用 Bean但页面里的控制流复杂度一上来脚本片段就成了泥潭。三者配合是 Servlet 规范里最原始的 MVC 划分也是课程设计老师最想看到的作业结构。顺带提一个老话题JSP 本身会被容器翻译成 Servlet所以 JSP 里能直接用request、session这些隐式对象。很多人搜“jsp个人信息展示页面”怎么写本质就是request.getSession().getAttribute(user)然后 EL 输出——这个动作在登录功能里会反复出现后面有完整代码。3. 先把书架搭起来从建表到 Servlet 把图书列表喂给 Jsp3.1 建库与 JDBC 连接一个够用的 BaseDao 写法网上购书系统的核心数据是图书、用户、订单。先建最基础的图书表和用户表字符集统一用 utf8mb4避免后面插入书名里的特殊字符报Incorrect string value。CREATE DATABASE bookshop DEFAULT CHARACTER SET utf8mb4; USE bookshop; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL COMMENT 书名, author VARCHAR(64) COMMENT 作者, price DECIMAL(10,2) COMMENT 单价, stock INT DEFAULT 0 COMMENT 库存 ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, nickname VARCHAR(64) );JDBC 连接代码十个项目里有九个长得差不多。把连库逻辑抽成一个 BaseDao后续每个 DAO 都继承它能少写很多重复代码。常见做法是用静态代码块加载驱动每次连接从DriverManager拿Connection。public class BaseDao { static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(找不到MySQL驱动请检查jar包, e); } } protected Connection getConn() throws SQLException { return DriverManager.getConnection( jdbc:mysql://localhost:3306/bookshop ?useUnicodetruecharacterEncodingUTF-8 serverTimezoneAsia/Shanghai, root, 123456); } }连接串里的三个参数按经验都得写useUnicodetrue和characterEncodingUTF-8保证中文正常serverTimezoneAsia/Shanghai是 MySQL 8.x 强制要求不写就报The server time zone value时区异常。驱动类名要注意com.mysql.jdbc.Driver是老驱动 5.x 的写法8.x 必须是com.mysql.cj.jdbc.Driver搞混了启动时直接ClassNotFoundException。3.2 Book 与 BookDao别把 SQL 写进 ServletJavaBean 就是纯数据类对应表里的字段。BookDao 是一个继承 BaseDao 的类对外只暴露业务方法例如list()返回所有图书。它不关心调用者是 Servlet 还是别的类这是分层的一个基本约束。public class BookDao extends BaseDao { public ListBook list() { ListBook books new ArrayList(); String sql SELECT id, name, author, price, stock FROM book; try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setName(rs.getString(name)); b.setAuthor(rs.getString(author)); b.setPrice(rs.getBigDecimal(price)); b.setStock(rs.getInt(stock)); books.add(b); } } catch (SQLException e) { throw new RuntimeException(查询图书列表失败, e); } return books; } }注意两件事PreparedStatement的?占位符可以防 SQL 注入后面findById里会用ps.setInt(1, id)传参try-with-resources 会自动关闭Connection、PreparedStatement、ResultSetJDBC 里忘关连接是老系统最常见的故障源之一。Book类里的价格字段一定要用BigDecimal用double会在后续算金额时出现浮点误差。3.3 BookListServlet用注解还是 web.xml代码里用了WebServlet(/bookList)这是 Servlet 3.0 之后最常见的映射方式。但在老课程设计和实际老项目里web.xml 方式更常见。servlet servlet-namebookList/servlet-name servlet-classcom.bookshop.web.BookListServlet/servlet-class /servlet servlet-mapping servlet-namebookList/servlet-name url-pattern/bookList/url-pattern /servlet-mapping配置方式优点缺点WebServlet注解配置在代码旁边修改后重编译即可包扫描顺序可能引发映射覆盖问题web.xml 配置集中管理改 URL 不用改代码条目多了以后很长容易漏配置容器判断映射的顺序是先精确匹配再通配符匹配最后匹配默认 Servlet。一个常见坑是写了注解又顺手在 web.xml 里加了一份结果出现 404 或重复初始化因为注解和 xml 同时生效时行为取决于容器的metadata-complete设置。建议新写的代码全用注解接手老项目时沿用 web.xml 的文件里已经有的一大堆servlet配置。url-pattern的写法和 Java 路径稍有不同必须以/开头可以写成/bookList也可以写成/book/*。注意不能带上?查询串路径匹配只认 URI不认 query string。这是排查 404 时最先要确认的点。3.4 在 Jsp 页面输出图书JSTL 比%循环更干净BookListServlet 把books塞进 request 之后JSP 用 EL 表达式直接取。页面里写c:forEach循环比写% for (int i 0; i list.size(); i) %清爽得多也不会在 IDE 里一堆红波浪线。% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 trth书名/thth作者/thth价格/thth操作/th/tr c:forEach items${books} varb tr td${b.name}/td td${b.author}/td td${b.price}/td tda href${pageContext.request.contextPath}/addCart?bookId${b.id}加入购物车/a/td /tr /c:forEach /table${books}等价于request.getAttribute(books)items${books}告诉 JSTL 要遍历哪个集合varb是每一轮循环的临时变量名。访问${b.price}时容器实际调用的是Book的getPrice()方法这也是为什么 JavaBean 的 getter 命名规范不能乱写。${pageContext.request.contextPath}会输出当前 Web 应用的上下文路径比如/bookshop。你如果做过部署就知道硬编码/bookList在多应用共用同一个 Tomcat 时会 404。4. 购物车、登录态与下单Bean 的三种作用域怎么选4.1 session 里的 userLoginServlet 把身份写进会话登录是“简单网上购书系统”里第一个把状态写进 session 的功能。用户在 login.jsp 填写表单POST 到/loginServlet 验证成功后把 User 对象放进 session之后每个 JSP 页面都能通过${sessionScope.user}取到当前用户。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user new UserDao().findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(user, user); response.sendRedirect(request.getContextPath() /bookList); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp) .forward(request, response); } } }request.getSession()的默认行为是没有 session 就创建一个如果想阻止这种行为用getSession(false)——它只返回已有的 session不存在时返回 null适合做登录状态检查。成功登录后我一般选择sendRedirect而不是forward理由很简单如果转发到/bookList浏览器地址栏还是/login用户刷新一次就重复提交一次登录表单。这是个体验问题也是面试里常问的“转发和重定向区别”的实际场景。个人展示页面同样这套逻辑jsp 页面顶部写c:if test${empty sessionScope.user} a hreflogin.jsp登录/a /c:if c:if test${not empty sessionScope.user} 欢迎${sessionScope.user.nickname} /c:ifempty表达式同时覆盖 null 和空字符串比写${sessionScope.user null}更健壮。4.2 request 作用域和 session 作用域购物车为什么必须存进 session购物车是理解作用域最好的例子。把购物车放进 request 里会怎样每次请求结束 request 就被销毁用户加一本书跳转后购物车被 GC 回收这是“一次性购物车”。放进 application 域里会怎样所有用户共享一个购物车A 加的书 B 能看到这显然不能叫购物车。作用域生命周期典型对象注意点request一次请求错误提示、列表数据转发后还存在重定向后就没了session一次会话登录用户、购物车会话超时后丢失application整个 Web 应用计数器、全局配置必须考虑并发购物车用session作用域配合MapInteger, CartItem存放key 是图书 id。加购 Servlet 的处理逻辑是先从 session 取购物车取不到就新建再根据 bookId 判断这个书是否已经在购物车里。WebServlet(/addCart) public class AddCartServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int bookId Integer.parseInt(request.getParameter(bookId)); HttpSession session request.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } CartItem item cart.get(bookId); if (item null) { item new CartItem(); Book book new BookDao().findById(bookId); if (book null) { response.sendError(404, 图书不存在); return; } item.setBook(book); cart.put(bookId, item); } item.setQuantity(item.getQuantity() 1); response.sendRedirect(request.getContextPath() /bookList); } }bookId从客户端传进来不要无条件信它。Integer.parseInt可能抛NumberFormatException真实项目里建议用一个parseIntSafe方法包一层解析失败时重定向回列表并提示“参数错误”。session.setAttribute(cart, cart)之后购物车对象就一直在 session 里直到用户关闭浏览器或会话超时。4.3 下单那一步订单明细为什么要有事务购书系统最后一步是下单往订单表插一条记录拿到订单 id再把购物车里的每一项写进订单明细表。这一步必须是原子的主表插入成功、明细表插入失败就会产生一笔空订单。“简单网上购书系统”里可以不用 Spring 的事务管理直接在 DAO 方法里控制 JDBC 事务。public void createOrder(int userId, ListCartItem cart) { String insertOrder INSERT INTO t_order(user_id, total_amount, created_time) VALUES(?,?,NOW()); String insertItem INSERT INTO t_order_item(order_id, book_id, quantity, price) VALUES(?,?,?,?); try (Connection conn getConn()) { conn.setAutoCommit(false); int orderId; try (PreparedStatement ps conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, userId); ps.setBigDecimal(2, calcTotal(cart)); ps.executeUpdate(); try (ResultSet keys ps.getGeneratedKeys()) { keys.next(); orderId keys.getInt(1); } } try (PreparedStatement ps conn.prepareStatement(insertItem)) { for (CartItem item : cart) { ps.setInt(1, orderId); ps.setInt(2, item.getBook().getId()); ps.setInt(3, item.getQuantity()); ps.setBigDecimal(4, item.getBook().getPrice()); ps.addBatch(); } ps.executeBatch(); } conn.commit(); } catch (SQLException e) { // 这里应该回滚但用 try-with-resources 时 conn 已经会被关闭 // 回滚必须在 catch 块里主动调用 throw new RuntimeException(下单失败事务回滚, e); } }setAutoCommit(false)是 JDBC 事务开关RETURN_GENERATED_KEYS让executeUpdate返回自增主键addBatch把多条明细放进同一个批次最后一次性executeBatch()减少网络往返。注意事务必须在同一个 Connection 上执行不能多个 DAO 各拿各的连接——这也是为什么下单逻辑要放在同一个 DAO 方法里而不是拆成三个类各写各的。订单类、订单明细类同样是 JavaBean属性对应表结构。到这里登录、列表、加购、下单一个“简单网上购书系统”的核心闭环已经打通。5. 部署、排错与 5 分钟做完的分页技巧5.1 Tomcat 把 Jsp 编译成什么class 文件存在哪第一次访问某个 JSP 时Tomcat 会把 JSP 翻译成_jsp.java再编译成_jsp.class。如果你用 Eclipse 里的 Tomcat 启动并且工作区在默认位置编译后的文件一般在tomcat安装目录/work/Catalina/localhost/项目名/org/apache/jsp/比如book_list.jsp对应book_list_jsp.java。文件名的规律是原名加_jsp后缀目录按包结构展开。排查“页面改了但没生效”的时候直接去work目录看这个.java文件里的代码能确定容器拿到的是不是最新版本。IDE 部署项目的话Tomcat 实际加载的位置在wtpwebapps或 target 的webapp目录改源码后需要publish或redeploy这是 Eclipse 里最常见的“我改了怎么不生效”原因。5.2 写一个 EncodingFilter一次处理全站乱码很多购书系统里每个 Servlet 都写一遍setCharacterEncoding(UTF-8)一旦漏一个页面就乱码。作为最后的兜底我一般写一个编码过滤器放在“/*”路径上通过WebFilter注解注册。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; if (POST.equalsIgnoreCase(req.getMethod())) { request.setCharacterEncoding(UTF-8); } response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }doFilter中的编码设置只对 POST 生效是因为 Tomcat 8 以后 GET 请求的 URI 默认按 UTF-8 解码不需要手动处理如果你还在用老版本 Tomcat 维护老项目需要去conf/server.xml的 Connector 上加URIEncodingUTF-8。chain.doFilter必须放在编码设置的后面否则过滤器后面的 Servlet 拿到的还是乱的请求体。过滤器的顺序由容器按类名字典序或映射规则决定如果同时有多个 Filter注意别让后面的 Filter 把 content type 覆盖掉。5.3 给列表加一个 5 分钟够用的分页“简单网上购书系统”的文字描述里一般不会要求分页但加了就是加分项。分页只有两件事LIMIT偏移量和总数COUNT()。按常见做法写一个简洁的PageBeanT。public class PageBeanT { private int pageNum 1; private int pageSize 8; private long total; private ListT rows; // 构造器、getter/setter 省略 }BookDao 里对应两个方法count()用SELECT COUNT(*) FROM book得到总数list(int offset, int pageSize)用SELECT id, name, author, price, stock FROM book ORDER BY id LIMIT ?,?。Servlet 里把total和rows塞进 PageBeanJSP 的分页栏只需要算总页数和当前页的上一页、下一页。一个容易被忽略的参数是首页和末页的边界控制上一页小于 1 就显示为 1下一页大于总页数就显示为总页数否则用户会点出空白列表。最后提醒一个数据库层面的细节扣减库存时不要先查再减并发情况下两条线程会同时读到同一个 stock。用一行 SQL 化解UPDATE book SET stock stock - 1 WHERE id ? AND stock 1影响行数为 0 表示库存不足把这段逻辑并入下单事务比写完整个系统再回头改要省事得多。和 JDBC 驱动版本不匹配这类坑相比这一点才是决定这个购书系统能不能上线跑的数据一致性问题。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门