ARTICLE DETAIL

资讯详情

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

JSP+Servlet网上书店源码解析:从环境搭建到进阶优化

JSP+Servlet网上书店源码解析:从环境搭建到进阶优化 简介这是一套面向Java Web初学者与课程设计者的完整网上书店项目源码基于JSPServletDAO技术栈采用经典MVC架构适合用于毕业设计、课程实训或自学练手。压缩包共161个文件约768KB包含27个java源文件与27个class编译文件、17个jsp页面、64个gif及6个jpg图片资源另有js、xml、sql脚本与tld标签库等配置覆盖视图、控制、模型与数据库各层。项目实现了用户登录验证、图书搜索、分页浏览、购物车增删改与结算等核心功能数据库连接借助JNDI管理连接池分页通过SQL的LIMIT与OFFSET配合Servlet动态生成查询条件购物车则依托Session跟踪状态。前端使用JavaScript与CSS完成动态加载和表单验证界面交互较为完整。已有162人学习下载读者可借此理解MVC分层、DAO持久化、过滤器与自定义标签等知识点并对照源码梳理完整电商流程具备较高的实践参考价值。1. 从一份 JSPServlet 网上书店源码说起它到底能跑通什么如果你手头正拿着一份“jspservlet网上书店”的课程设计或毕设源码第一反应大概率是这玩意儿能直接跑吗我拆过不少这类包结论是——能跑但得先搞清楚它的骨架。这套东西本质上是 Java Web 的经典三层架构JSP 负责页面展示Servlet 接请求做控制DAO 层封装 JDBC 操作数据库。它解决的是“图书浏览、加入购物车、下单、后台管理”这条完整业务链适合刚学完 Servlet 想找个完整项目练手的人也适合被毕设逼到墙角、需要一份能讲清楚“MVC 到底怎么落地”的参考实现。别指望它像 Spring Boot 那样开箱即用它的价值在于把 request、session、转发重定向这些基础概念全串起来了你改一行代码就能看到页面变化这种反馈对新手特别友好。2. 环境搭建与数据库初始化把架子先立稳2.1 选 Tomcat 8.5 JDK 8 的理由这套源码我见过的大多数版本Servlet 用的还是javax.servlet包不是 Jakarta EE 的jakarta.servlet。这意味着你上 Tomcat 10 会直接报ClassNotFoundException因为包名从javax.*换成了jakarta.*。所以稳妥组合是 JDK 8 配 Tomcat 8.5 或 9别追新。MySQL 用 5.7 或 8.0 都行但 8.0 要改驱动类名和连接串参数下面会讲。IDE 用 Eclipse 或 IntelliJ IDEA 都可以。Eclipse 对老项目更宽容IDEA 需要手动配 Artifact 和 Web Facet。我一般用 IDEA因为调试 Servlet 断点更顺手但第一次导入时要在 Project Structure 里把Web Resource Directory指到web或WebContent目录否则 JSP 找不到。2.2 建库建表与初始数据先建一个名为bookstore的库字符集用utf8mb4不然中文书名会变问号。核心表通常有四张user用户、book图书、category分类、order订单。下面这段 SQL 是我从常见源码里整理出来的最小可用结构你可以直接抄CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4; USE bookstore; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, email VARCHAR(100), role INT DEFAULT 0 -- 0普通用户 1管理员 ); CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2), stock INT DEFAULT 0, category_id INT, image VARCHAR(200) ); CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) ); CREATE TABLE order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, total DECIMAL(10,2), status INT DEFAULT 0, -- 0未支付 1已支付 2已发货 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO user (username, password, role) VALUES (admin, admin123, 1); INSERT INTO category (name) VALUES (计算机), (文学), (历史); INSERT INTO book (name, author, price, stock, category_id) VALUES (Java Web开发, 张三, 59.90, 100, 1), (数据结构, 李四, 45.00, 50, 1);建完表后去源码里找db.properties或DBUtil.java把连接串改成你自己的。常见配置长这样drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password你的密码注意serverTimezone这个参数MySQL 8.0 不写会报时区错误。驱动类名如果是 5.7 版本用com.mysql.jdbc.Driver8.0 用com.mysql.cj.jdbc.Driver写错直接连不上。2.3 导入项目与部署验证把源码解压后在 IDEA 里选File - New - Project from Existing Sources选到项目根目录一路 Next。然后检查src目录是否被标记为 Sources Rootlib目录下的 jar 包通常是mysql-connector-java和jstl有没有加到 Module Dependencies 里。JSTL 包特别容易漏漏了 JSP 里的c:forEach标签会直接报错。配置 Tomcat 运行环境时在 Run Configuration 里加一个 Tomcat LocalDeployment 标签页添加 ArtifactApplication context 设成/bookstore。启动后浏览器访问http://localhost:8080/bookstore/如果看到首页轮播图和图书列表说明架子立稳了。如果 404先看 Tomcat 日志里有没有ClassNotFoundException再看 web.xml 里welcome-file配的是不是index.jsp。3. Servlet 与 DAO 分层拆解请求是怎么一步步落到 SQL 的3.1 一个典型请求的完整链路拿“用户登录”这个动作举例。浏览器提交/login的 POST 请求LoginServlet的doPost方法接住从request.getParameter(username)拿到用户名密码然后 new 一个UserDao调用userDao.findByUsernameAndPassword(username, password)。DAO 里用PreparedStatement拼 SQL 查user表查到就返回 User 对象查不到返回 null。Servlet 拿到结果后session.setAttribute(user, user)然后response.sendRedirect(index.jsp)。整个链路清晰得就像一条直线没有任何框架魔法这正是它适合学习的原因。我见过不少同学把 JDBC 代码直接写在 Servlet 里一个doPost方法两百行查完用户查订单查完订单更新库存。这种写法能跑但改起来要命。分层之后Servlet 只负责“接参数、调方法、控跳转”DAO 只负责“拼 SQL、关连接”各司其职。你以后换数据库只改 DAO 就行Servlet 一行不用动。3.2 DAO 层通用增删改查模板下面这个BaseDao是我从多个版本里提炼出来的通用写法你可以直接拿去替换掉源码里重复的 JDBC 代码public class BaseDao { protected Connection getConn() throws Exception { Class.forName(com.mysql.cj.jdbc.Driver); return DriverManager.getConnection( jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai, root, 你的密码); } // 通用查询返回ListMap protected ListMapString, Object executeQuery(String sql, Object... params) { ListMapString, Object list new ArrayList(); try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } ResultSet rs ps.executeQuery(); ResultSetMetaData md rs.getMetaData(); int cols md.getColumnCount(); while (rs.next()) { MapString, Object row new HashMap(); for (int i 1; i cols; i) { row.put(md.getColumnName(i), rs.getObject(i)); } list.add(row); } } catch (Exception e) { e.printStackTrace(); } return list; } // 通用更新返回影响行数 protected int executeUpdate(String sql, Object... params) { try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } } }这段代码的关键在于try-with-resources它会自动关闭 Connection 和 PreparedStatement不用你手动写finally块。参数用Object...可变参数传入ps.setObject按顺序绑定避免拼字符串导致的 SQL 注入。executeQuery返回ListMap而不是实体类是为了通用性你在具体 DAO 里再转成 Book 或 User 对象。3.3 JSP 页面如何安全地展示数据JSP 里展示图书列表常见写法是用 JSTL 的c:forEach遍历 request 域里的 list% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach items${bookList} varbook tr td${book.name}/td td${book.author}/td td${book.price}/td tda hrefcart?actionaddid${book.id}加入购物车/a/td /tr /c:forEach /table这里${book.name}默认会经过 EL 表达式输出但不会做 HTML 转义。如果书名里有人恶意插入script标签就会造成 XSS。稳妥做法是用c:out value${book.name} /它会自动转义和。另外cart?actionaddid${book.id}这种 URL 传参id 是数字还好如果是字符串参数记得在 Servlet 里做非空和格式校验别直接拼进 SQL。4. 避坑与常见问题排查那些让我熬夜的报错4.1 中文乱码从请求到响应全链路排查现象登录时输入中文用户名后台打印出来是????或者图书详情页中文书名显示为乱码。原因乱码可能出现在三个环节——Tomcat 的 URI 编码、request 的字符编码、response 的字符编码。Tomcat 8 默认 URI 编码是 UTF-8但 POST 请求体默认不是。response 如果没设contentType浏览器会按 ISO-8859-1 解析。解决在 Servlet 的doPost第一行加request.setCharacterEncoding(UTF-8)在doGet里对参数做new String(name.getBytes(ISO-8859-1), UTF-8)转换或者直接在 Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。response 那边加response.setContentType(text/html;charsetUTF-8)。JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %。三处都设了乱码基本绝迹。4.2 数据库连接池未关闭导致 Tomcat 启动失败现象项目跑一会儿就报Too many connections或者 Tomcat 停止时日志里一堆Abandoned connection警告。原因源码里很多 DAO 方法在finally块里只关了ResultSet和Statement忘了关Connection。每个请求都 new 一个连接数据库最大连接数很快被耗尽。解决要么用上面BaseDao的try-with-resources写法要么在finally里确保conn.close()执行。更彻底的做法是引入 Druid 或 C3P0 连接池在context.xml里配Resource但这对课程设计来说有点超纲。我一般建议先手动关跑通了再考虑池化。4.3 JSTL 标签不解析直接原样输出现象JSP 页面里c:forEach没生效浏览器源码里看到的就是c:forEach这串字符。原因要么没引入 JSTL 的 jar 包要么 web.xml 版本太低不支持 EL 表达式要么 taglib 的 uri 写错了。解决确认WEB-INF/lib下有jstl.jar和standard.jarJSTL 1.2 只需要jstl-1.2.jar。web.xml 的web-app版本至少 2.4推荐 3.0 以上。taglib 的 uri 用http://java.sun.com/jsp/jstl/core别写成http://java.sun.com/jstl/core少个/jsp就废了。4.4 购物车数量修改后总价不更新现象在购物车页面把某本书数量从 1 改成 3页面刷新后数量变了但总价还是按 1 本算的。原因总价通常是在 Servlet 里遍历 session 中的购物车 Map 累加计算的如果修改数量的方法只更新了 Map 里的数量没有重新计算总价并覆盖 session 里的 total 属性页面拿到的就是旧值。解决在updateCart方法末尾加一行session.setAttribute(total, calcTotal(cart))确保每次数量变动都重算。另外注意calcTotal里用BigDecimal而不是double不然 59.9 乘 3 可能得到 179.69999999999999。4.5 后台管理页面未做权限拦截现象普通用户直接在地址栏输入/admin/bookList.jsp就能看到后台管理页面甚至能删书。原因源码里只在登录时判断了 role但后台的每个 JSP 和 Servlet 没有独立校验 session 中的 user 角色。解决写一个AdminFilter拦截/admin/*路径从 session 取 user如果为 null 或 role 不等于 1直接response.sendRedirect(../login.jsp)。Filter 的doFilter里放行前先判断别等请求打到 Servlet 再拦那样已经晚了。5. 进阶技巧把这份源码改造成能写进简历的项目5.1 用 Filter 统一处理编码与登录校验与其在每个 Servlet 里写request.setCharacterEncoding不如抽一个EncodingFilter在web.xml里配/*拦截所有请求。代码就三行public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }配好之后所有 Servlet 里那行编码设置都可以删掉。登录校验也同理写一个AuthFilter拦/cart/*、/order/*没登录就跳登录页。这样你的项目结构就从“能跑”升级到了“有拦截器思想”面试时能多聊五分钟。5.2 把 JDBC 换成 Druid 连接池连接池的引入能让你的项目看起来不那么“学生气”。在WEB-INF/lib下丢一个druid-1.2.20.jar然后写一个DruidUtilpublic class DruidUtil { private static DataSource ds; static { Properties p new Properties(); try { p.load(DruidUtil.class.getClassLoader().getResourceAsStream(druid.properties)); ds DruidDataSourceFactory.createDataSource(p); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConn() throws SQLException { return ds.getConnection(); } }druid.properties里配url、username、password、initialSize5、maxActive20。改完之后BaseDao里的getConn换成DruidUtil.getConn()其他代码一行不用动。连接池会自动管理连接的创建和回收你再也不用担心Too many connections。5.3 分页查询的 SQL 与页码计算图书列表数据一多必须分页。MySQL 用LIMIT实现SELECT * FROM book LIMIT #{offset}, #{pageSize}offset (pageNum - 1) * pageSize。在 Servlet 里先查SELECT COUNT(*) FROM book得到总记录数total再算总页数totalPage (total pageSize - 1) / pageSize。把pageNum、totalPage、bookList一起塞进 requestJSP 里渲染上一页/下一页链接。注意pageNum要做边界校验小于 1 按 1 算大于总页数按总页数算不然 SQL 查出来是空列表页面看着像崩了。5.4 一个我踩过的坑JSP 图片路径别用绝对路径图书封面图我一开始写成/images/${book.image}部署到 Tomcat 后图片全裂。原因是浏览器把/images/解析到了http://localhost:8080/images/而你的项目上下文是/bookstore。正确写法是用${pageContext.request.contextPath}/images/${book.image}这样无论项目部署在哪个 context 下路径都能自动适配。从那以后我每次写 JSP 里的静态资源路径都强制走一遍${pageContext.request.contextPath}再也没裂过图。希望帮到你。本文还有配套的精品资源点击获取
返回列表