简介:这是一套面向计算机相关专业学生与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 里,如果还能正常注册登录下单,说明你的项目没有依赖任何本地环境。这个「换环境测试」的习惯帮我提前发现过无数次路径写死和依赖遗漏的问题。做练手项目最怕的就是在自己机器上跑通就以为完事了,真正的检验是换台机器还能跑起来。希望帮到你。
本文还有配套的精品资源,点击获取