news 2026/9/29 19:21:55

JavaWeb项目完整案例:Servlet+JSP+MySQL购物网站从零到部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb项目完整案例:Servlet+JSP+MySQL购物网站从零到部署

简介:这是一套面向计算机相关专业学生与Java Web入门者的购物网站系统源码,采用Java+Servlet+JSP+MySQL技术栈,适合作为课程设计、期末大作业或项目实战练习的参考方案,曾获98分评价。压缩包共69个文件,约292KB,其中15个java源文件承载Servlet与业务逻辑,13个jsp页面负责前台展示与后台交互,另有16个class编译文件、1个sql建库脚本、1个xml配置、1个css与1个js资源,以及若干gif、jpg、bmp图片素材,结构完整、层次清晰。项目涵盖用户注册登录、商品浏览、购物车、订单结算等典型电商模块,页面包含首页、详情页、购物车、登录注册等,便于读者快速理解MVC分层与请求流转过程。目前已有231人学习下载,可作为二次开发与功能扩展的起点,帮助读者掌握Java Web项目从建库到部署的完整链路。

1. 从零手搓一个 Java 购物网站:Servlet+JSP+MySQL 这套老组合为什么还值得写一遍

如果你正在搜「javaweb项目完整案例mysql」,大概率不是为了做一个真能上线的电商平台,而是想找一个能把 Java 基础、Servlet 生命周期、JSP 页面渲染和 MySQL 增删改查串起来的完整练手项目。购物网站系统恰好是那个最合适的载体:它有用户、商品、订单、购物车这几条清晰的数据线,每一条都能逼着你把「请求怎么进来、业务怎么处理、数据怎么落库、页面怎么展示」走一遍。这套 Servlet+JSP+MySQL 的组合虽然被很多人说「过时」,但它的价值恰恰在于没有框架帮你兜底——你得自己写 JDBC 连接、自己管 Session、自己处理中文乱码,这些坑踩过一遍,后面学 Spring 才不是背注解。这篇文章面向能写 Java 基础语法、装过 MySQL、但还没独立跑通过一个完整 Web 项目的同学,我会按「环境搭起来 → 数据库建起来 → 后端接口写出来 → 前端页面连起来 → 部署打包跑起来」的顺序,把每一步的命令、参数和翻车点都摊开讲。你跟着走完,手里会有一个能注册、能登录、能加购物车、能下单的 war 包,而不是一堆散落的代码片段。

2. 环境与工程骨架:IDEA 新建 JSP 项目到 Tomcat 跑通第一页

这一章的目标很明确:在你本地把「浏览器请求 → Tomcat → Servlet/JSP → 响应」这条链路先跑通,哪怕页面只输出一句 hello。很多人卡在第一步不是因为不会写代码,而是 IDEA 新建项目的选项、Tomcat 的配置、依赖的放置位置这三件事没对齐。下面按我实际搭项目的顺序来。

2.1 用 IDEA 建一个能跑 Servlet 的 Web 工程

打开 IDEA,选 File → New → Project,左侧选 Java Enterprise(不同版本叫 Jakarta EE 或 Java EE),勾选 Web Application,确保勾上 Create web.xml。JDK 选 8 或 11 都行,我一般用 8,因为老项目兼容性最好。建完后目录结构应该是这样:

src/ main/ java/ ← 放 Servlet、DAO、实体类 webapp/ WEB-INF/ web.xml ← 部署描述符 index.jsp ← 默认首页

这里第一个坑就来了:Servlet 的 jar 包不在 JDK 里,它在 Tomcat 的 lib 目录下。你需要把servlet-api.jar和jsp-api.jar加到项目的 Libraries 里,但作用域要选 Provided,因为 Tomcat 运行时自己会提供这两个包,你打包进 war 反而会冲突。具体操作:File → Project Structure → Libraries → 加号 → Java → 找到 Tomcat 安装目录的 lib 文件夹,选中这两个 jar,然后在 Modules 的 Dependencies 里把它们的 Scope 改成 Provided。

2.2 配置 Tomcat 并跑通第一个 JSP

Run → Edit Configurations → 加号 → Tomcat Server → Local。在 Server 标签页里指定 Tomcat 的安装路径,在 Deployment 标签页点加号选 Artifact,把项目名:war exploded加进去,Application context 设成/shop。这样你访问的地址就是http://localhost:8080/shop/。

启动前确认端口没被占用,Tomcat 默认 8080,如果本机装了其他服务占了,在 Server 标签页改 HTTP port。启动后浏览器打开http://localhost:8080/shop/index.jsp,能看到页面就说明链路通了。如果报 404,先检查 Application context 是不是写成了/shop而访问时漏了这段路径;如果报 500 且堆栈里有 ClassNotFoundException,八成是 servlet-api 的作用域没设成 Provided。

2.3 中文乱码:从请求到响应要设三处

乱码是这套技术栈最经典的玄学问题,血泪经验是它有三个独立的源头,少设一处就前功尽弃。

第一处是 JSP 页面本身的编码,在页面顶部写:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" language="java" %>

第二处是请求体编码,POST 请求要在读取参数之前设置:

request.setCharacterEncoding("UTF-8");

注意这行必须放在getParameter之前,放后面无效。第三处是响应编码:

response.setContentType("text/html;charset=UTF-8");

如果是 GET 请求传中文,Tomcat 8 以后默认 URIEncoding 就是 UTF-8,一般不用改;但如果你的 Tomcat 是 7 或更早,需要在 server.xml 的 Connector 标签加URIEncoding="UTF-8"。这三处设完,中文基本就不会再出问题。

3. 数据库设计:购物网站系统需要哪几张表、字段怎么定

后端能不能写顺,一半取决于表设计得对不对。购物网站系统的核心数据关系是:一个用户有多个订单,一个订单有多个订单项,一个订单项对应一个商品,购物车是用户和商品之间的临时多对多。下面给出我实际用的建表语句,字段类型和约束都标了理由。

3.1 用户表、商品表、订单表的建表语句

CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE shop; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image VARCHAR(255), description TEXT, category VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个参数说明:字符集用utf8mb4而不是utf8,因为后者在 MySQL 里是阉割版,存 emoji 会报错;价格字段用DECIMAL(10,2)而不是FLOAT,浮点数算钱会出现0.1+0.2=0.30000000000000004这种精度问题;order_item里冗余存了一份price,因为商品价格会变,订单要记录下单时的价格,这是电商系统的通用做法,别为了「范式」把它去掉。

3.2 购物车表:登录用户和游客怎么区分

购物车有两种设计思路。简单做法是只给登录用户存购物车,游客加购先存 Session,登录后再合并。我一般用一张表加一个 session_id 字段来兼容两种情况:

CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, session_id VARCHAR(100), product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

user_id为空时用session_id标识游客,登录后把该 session 的购物车记录更新到用户名下。查询时用WHERE user_id = ? OR session_id = ?就能同时覆盖两种状态。

3.3 JDBC 连接与连接池:别每次请求都 DriverManager.getConnection

新手最容易犯的错是每个 Servlet 里写一遍DriverManager.getConnection,请求一多数据库连接就被打满。正确做法是用连接池。项目里引入druid或c3p0的 jar,我一般用 Druid,配置放在druid.properties:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/shop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username=root password=你的密码 initialSize=5 maxActive=20 maxWait=3000

注意useSSL=false这行,MySQL 8 默认要求 SSL 连接,不加这个参数会报mysql ssl连接错误;serverTimezone也必须设,否则时间字段会差 8 小时。工具类这样写:

public class JDBCUtils { private static DataSource dataSource; static { try { Properties props = new Properties(); props.load(JDBCUtils.class.getClassLoader() .getResourceAsStream("druid.properties")); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException("连接池初始化失败", e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 依次关闭,注意判空,顺序不能反 } }

连接池的maxActive设 20 对练手项目足够,生产环境要按数据库的max_connections反推。maxWait是获取连接的超时时间,单位毫秒,设太小高并发时会直接抛异常。

4. 后端接口实现:Servlet 生命周期与用户、商品、购物车的增删改查

这一章是项目的核心,把 Servlet 的生命周期吃透,后面写多少个接口都是同一套模板。Servlet 的生命周期就三个阶段:init在第一次访问时调用一次,service每次请求都调用,destroy在 Tomcat 关闭时调用。你写的doGet和doPost是被service根据请求方法分发的。理解这点,就知道为什么不要在doGet里初始化数据库连接——每次请求都会执行。

4.1 用户注册登录:一个 Servlet 处理多个动作

与其给每个功能建一个 Servlet,不如用一个 Servlet 加action参数来分发,减少 web.xml 的配置量。注册的核心逻辑:

@WebServlet("/user") public class UserServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String action = request.getParameter("action"); if ("register".equals(action)) { String username = request.getParameter("username"); String password = request.getParameter("password"); // 密码不能明文存,用 MD5 加盐 String encrypted = MD5Utils.encrypt(password); User user = new User(username, encrypted); boolean success = userDao.insert(user); if (success) { response.sendRedirect("login.jsp"); } else { request.setAttribute("msg", "用户名已存在"); request.getRequestDispatcher("register.jsp") .forward(request, response); } } else if ("login".equals(action)) { String username = request.getParameter("username"); String password = MD5Utils.encrypt(request.getParameter("password")); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { request.getSession().setAttribute("user", user); response.sendRedirect("product?action=list"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp") .forward(request, response); } } } }

逻辑说明:注册时先加密再入库,登录时把加密后的密码拿去比对,数据库里永远不存明文。sendRedirect和forward的区别要记牢——前者是让浏览器重新发一次请求,地址栏会变,后者是服务器内部转发,地址栏不变。登录成功用重定向是为了防止用户刷新页面重复提交表单。

参数说明:@WebServlet("/user")是 Servlet 3.0 的注解方式,等价于在 web.xml 里配<url-pattern>,用注解就不用改配置文件了。request.getSession()会自动创建或获取当前会话,把 user 对象放进去,后续页面就能用session.getAttribute("user")判断登录状态。

4.2 商品列表与分页:别一次 select 全表

商品列表要分页,核心 SQL 是LIMIT:

public List<Product> findByPage(int page, int pageSize) { String sql = "SELECT * FROM product LIMIT ?, ?"; // 第一个参数是偏移量,第二个是每页条数 int offset = (page - 1) * pageSize; // ... 执行查询,offset 和 pageSize 作为参数传入 }

偏移量算法是(当前页 - 1) × 每页条数,这个公式记死。总页数用SELECT COUNT(*) FROM product拿到总数后除以 pageSize 向上取整。分页参数从请求里取:

String pageStr = request.getParameter("page"); int page = (pageStr == null || pageStr.isEmpty()) ? 1 : Integer.parseInt(pageStr); int pageSize = 8;

一定要做空值和非法值判断,用户手动改 URL 传个page=abc进来,不处理就是 500 错误。

4.3 购物车加减与下单:事务保证数据一致性

购物车加购的逻辑是「有则加数量,无则插记录」:

public void addToCart(int productId, Integer userId, String sessionId) { String checkSql = "SELECT * FROM cart WHERE product_id = ? AND " + (userId != null ? "user_id = ?" : "session_id = ?"); // 查到就 UPDATE quantity = quantity + 1,查不到就 INSERT }

下单是最需要事务的地方,因为要同时做三件事:插订单、插订单项、扣库存。任何一步失败都要回滚:

public boolean createOrder(int userId, List<CartItem> items) { Connection conn = null; try { conn = JDBCUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 插入 orders 表,拿到自增主键 // 2. 遍历 items,插入 order_item // 3. 更新 product 的 stock,且要判断 stock >= quantity // 如果库存不足,抛异常触发回滚 conn.commit(); return true; } catch (Exception e) { if (conn != null) conn.rollback(); return false; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }

关键点:setAutoCommit(false)之后所有操作在同一个连接上才属于同一事务,所以 DAO 方法必须接收 Connection 参数,不能各自去连接池拿新连接。扣库存的 SQL 要写成UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?,用数据库的行锁保证并发下不会超卖,这比在 Java 里先查再改可靠得多。

5. 避坑与排查:这套技术栈最容易翻车的五个地方

5.1 报 404:路径对了但 Servlet 没生效

现象是浏览器访问某个 Servlet 地址返回 404,但代码明明写了。原因通常是三种:一是@WebServlet的路径和访问路径不一致,比如注解写/user但访问/userServlet;二是web.xml里metadata-complete="true",这会让容器忽略所有注解;三是 war exploded 没有重新部署,改了代码没生效。解决方法是先看 Tomcat 启动日志里有没有Servlet [xxx] registered这行,没有就是没注册上,检查注解和 web.xml 的冲突。

5.2 报 500 且堆栈是 NullPointerException

现象是页面 500,日志里 NPE 但看不出哪一行。原因八成是request.getParameter拿到的值是 null 直接用了,或者 Session 里取对象没判空。解决方法是所有外部输入都要判空,session.getAttribute("user")之后先if (user != null)再强转。养成习惯:从 request、session、数据库查出来的对象,用之前都假设它可能是 null。

5.3 数据库连接报 Too many connections

现象是跑一会儿就报连接数超限。原因是每次请求都新建连接且没关闭,或者连接池 maxActive 设太大超过了 MySQL 的 max_connections。解决方法是确认所有 Connection 都在 finally 里 close,用连接池统一管理,maxActive 设成 MySQLmax_connections的 70% 左右。查 MySQL 当前连接数用SHOW STATUS LIKE 'Threads_connected'。

5.4 JSP 页面 EL 表达式不解析,直接显示 ${user.name}

现象是页面上原样输出了${user.name}。原因是 web.xml 的版本太低,比如 2.3 版本默认不启用 EL。解决方法是把 web.xml 的 schema 声明改成 3.0 或 4.0,或者直接在页面用<%= %>脚本表达式。我一般建议统一用 EL 和 JSTL,页面干净,但记得在 pom 或 lib 里加 jstl 的 jar。

5.5 打包 war 部署到 Tomcat 后图片和 CSS 加载不出来

现象是本地 IDEA 里正常,war 丢到 Tomcat webapps 下就样式全丢。原因是静态资源路径用了相对路径,或者放在了 WEB-INF 下面。WEB-INF 目录下的资源浏览器是访问不到的,静态文件必须放在 webapp 根目录或其子目录。解决方法是统一用${pageContext.request.contextPath}/css/style.css这种带上下文路径的写法,避免硬编码/shop。

6. 打包部署与一个提效技巧:war 包怎么出、JSTL 怎么少写一半代码

项目写完最后一步是打包成 war 丢到独立 Tomcat 里跑,验证它不依赖 IDEA 也能运行。在 IDEA 里 File → Project Structure → Artifacts → 加号 → Web Application: Archive → 选你的模块,然后 Build → Build Artifacts → Build,产物在out/artifacts/下。把 war 复制到 Tomcat 的 webapps 目录,启动 Tomcat,访问http://localhost:8080/war包名/。这一步能暴露很多 IDEA 帮你隐藏的问题,比如依赖没打进 WEB-INF/lib、路径写死成绝对路径等。

关于提效,我最想分享的是 JSTL 的<c:forEach>和<c:if>。用 JSP 脚本表达式写循环是这样的:

<% for (Product p : list) { %> <div><%= p.getName() %> - <%= p.getPrice() %></div> <% } %>

用 JSTL 是这样:

<c:forEach items="${productList}" var="p"> <div>${p.name} - ${p.price}</div> </c:forEach>

后者不仅短,而且不会因为少写一个分号就编译报错,页面里也不会有 Java 代码。用之前记得在页面顶部引入:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

以及把jstl.jar和standard.jar放进 WEB-INF/lib。我踩过的坑是只放了 jstl 没放 standard,结果标签不生效还不报错,排查了半天。另外<c:forEach>有个varStatus属性,能拿到循环下标,做隔行变色或者序号列的时候特别有用,${status.index}从 0 开始,${status.count}从 1 开始,别搞混。

最后说一个验证项目是否真的跑通的方法:把项目里的数据库连接配置改成另一台机器的 MySQL,war 包丢到一个全新的 Tomcat 里,如果还能正常注册登录下单,说明你的项目没有依赖任何本地环境。这个「换环境测试」的习惯帮我提前发现过无数次路径写死和依赖遗漏的问题。做练手项目最怕的就是在自己机器上跑通就以为完事了,真正的检验是换台机器还能跑起来。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 19:19:55

AI系统性能工程:从P99延迟到生产稳定性的实战方法论

1. 项目概述&#xff1a;这不是“调参指南”&#xff0c;而是一套可落地的AI系统性能工程方法论 “AI 系统性能工程&#xff08;二&#xff09;”这个标题乍看像系列文章的续篇&#xff0c;但实际它指向一个被严重低估的现实问题&#xff1a;我们花了大量精力训练大模型、设计A…

作者头像 李华
网站建设 2026/9/29 19:16:39

AI编程新利器:Skill如何让AI从‘会写代码‘到‘懂开发‘

1. 从"AI看起来很聪明&#xff0c;但总觉得差口气"说起天天用AI编程的人&#xff0c;大概率都经历过同一个瞬间&#xff1a;对话窗口里问了半天&#xff0c;AI还是写不出你想要的代码。不是它笨&#xff0c;是它缺少一种叫"经验"的东西。这个词近半年在AI编…

作者头像 李华
网站建设 2026/9/29 19:15:23

安路FPGA TD软件实战:时序约束与硬件调试全攻略

拿到一个国产FPGA项目&#xff0c;尤其是从Intel/Xilinx平台切换过来时&#xff0c;最让人头疼的往往不是RTL代码本身&#xff0c;而是EDA工具链的使用习惯差异。安路FPGA配上自家的TD软件&#xff0c;整体思路和Quartus/Vivado接近&#xff0c;但细节上坑不少。我在一个基于EG…

作者头像 李华
网站建设 2026/9/29 19:15:04

红外+可见光跨模态融合:基于YOLOv11的目标追踪实践

简介&#xff1a;《跨模态融合实践-YOLOv11红外与可见光双传感器目标追踪》是一份面向计算机视觉学习者和开发者的技术文档&#xff0c;旨在帮助读者解决单传感器在夜间、低光照及复杂背景下目标追踪鲁棒性差的问题。文档共38页&#xff0c;结构完整&#xff1a;先从跨模态融合…

作者头像 李华
网站建设 2026/9/29 19:14:36

企业级LLM落地实战:网关、RAG、Agent与成本治理全解析

1. 企业级 LLM 落地&#xff0c;先想清楚“企业级”三个字到底意味着什么这两年跟不少团队聊过大模型落地的事&#xff0c;一个很明显的感受是&#xff1a;个人玩 LLM 和企业上 LLM&#xff0c;完全是两码事。个人场景里&#xff0c;你调个 API、写个提示词、跑通一个 demo&…

作者头像 李华
网站建设 2026/9/29 19:13:50

智能体基建实战:用Herdr实现AI编程工具的多路复用编排

这两年我经手的AI编程工具&#xff0c;一只手加一只脚都数不过来。有补全行云流水的&#xff0c;有重构大刀阔斧的&#xff0c;有给整个代码仓库做体检的。工具是好工具&#xff0c;但用起来越来越拧巴&#xff1a;在A工具里把项目背景聊透了&#xff0c;切到B工具又得重新铺垫…

作者头像 李华