JavaWeb商城项目拆解:Servlet、DAO与前端交互
2026/9/16 12:00:59 网站建设 项目流程

简介:这份资源是一份基于JavaWeb技术栈的小米商城仿真项目,前端采用HTML+CSS+JavaScript构建页面交互,后端使用Servlet和JSP完成业务处理,并辅以DAO等类实现数据访问,适合JavaWeb课程设计、毕业设计或入门进阶练习。压缩包共1643个文件,容量约154MB,其中包含大量jpg/png图片用于商品展示与页面素材,58个java源文件与对应class文件覆盖核心业务逻辑,27个jsp负责动态页面输出,62个js和31个css实现前端效果,另有jar依赖、xml配置文件及数据库文件,项目结构完整清晰,便于快速部署运行。目前已有168人浏览学习,可直接导入开发环境运行复现商城基本流程。资源具备较高借鉴价值,可在现有用户管理、商品分类、购物车等功能基础上修改扩展,也适合学习Servlet、JSP与JDBC整合开发时对照参考。

1. 课程设计里的JavaWeb商城,值得拆开看的三个点

很多同学的JavaWeb课程设计最后长成一个样子:HTML页面铺得漂亮,CSS调得顺手,但一问“登录状态怎么保持的”“商品数据从哪张表来的”,就答不上来。这份“小米商城 HTML+CSS+JavaScrip+Servlet.zip”是典型的电商类JavaWeb项目完整案例,把用户、商品、订单三块基础数据用最直接的方式落了一遍。资源里除了前端页面,更值得看的是ProDao、MsoDao、UserDao三个DAO类,以及贯穿请求入口的SendMessageServlet。它适合在校生做课设、初级开发搭商城原型,也适合需要一份能改而不是只能看的参考代码的人。下面按三层结构拆解,把每一条请求链讲清楚,从工程目录讲到部署排错。

2. 先把项目骨架立起来:包结构、Servlet映射与请求走向

2.1 从bmp素材到工程目录:静态资源与源码怎么摆

很多课程设计资源压缩包打开后,第一眼是一批.bmp文件,比如8.bmp、9.bmp、10.bmp这些。这类位图通常是页面设计底图或商品位图素材,是作者在切图阶段留下的原型参考,不一定要直接参与运行。常见做法是把它们转成jpg或png后放到web目录的images文件夹下,和css、js并列;如果只是用来看页面效果,也可以单独建一个docs目录存放,不建议散落在src根目录污染源码。一个约定俗成的JavaWeb工程目录是这样组织的:

xiaomi-mall/ ├── src/ │ ├── com/xxx/dao/ # 数据访问层:ProDao、MsoDao、UserDao │ ├── com/xxx/servlet/ # 控制层:SendMessageServlet 等 │ ├── com/xxx/entity/ # 数据库表对应的实体类 │ └── com/xxx/util/ # DBUtil、JsonResult 等工具类 ├── web/ │ ├── css/ # 页面样式 │ ├── js/ # 前端交互脚本 │ ├── images/ # 商品图,bmp素材可转成jpg/png后放这里 │ └── WEB-INF/ │ ├── web.xml # Servlet映射文件 │ └── lib/ # mysql-connector-j 等依赖 └── *.bmp # 原始设计稿或演示素材

把bmp文件转成jpg或png再引用,是课设阶段最省事的做法。位图体积大、浏览器支持有限,直接放在images里虽然能显示,但会拖慢页面加载,review代码的老师看到这种引用方式也容易扣印象分。src目录下只保留Java源码,web目录下只放运行期资源,这条边界划清楚,整个项目结构就立住了。

注意:Eclipse或IDEA发布项目时,只有web目录下的内容会被复制到Tomcat的部署目录,src里散落的bmp文件不会被打进war包,页面一旦引用,运行期就是404。

2.2 Servlet映射:注解还是web.xml

这个项目里有一个SendMessageServlet,它的URL怎么被找到,取决于映射方式。两种常见写法:注解声明,或者web.xml集中声明。黑马JavaWeb笔记里大多推荐用注解少写文件,但课程设计我更倾向web.xml,因为答辩时更容易把“URL到Java类的路由过程”讲清楚。两者的对比关系如下:

映射方式写法位置适合场景
@WebServlet("/sendMessage")Servlet类上小项目、快速原型、想少写文件
WEB-INF/web.xml课程设计、需要集中审计映射关系
// 方式一:直接在Servlet类上声明URL映射 @WebServlet("/sendMessage") public class SendMessageServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 业务逻辑 } }
<!-- 方式二:web.xml 集中声明,与注解写法等价 --> <servlet> <servlet-name>sendMessageServlet</servlet-name> <servlet-class>com.xxx.servlet.SendMessageServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>sendMessageServlet</servlet-name> <url-pattern>/sendMessage</url-pattern> </servlet-mapping>

这两种方式最终都会被Tomcat解析成内部的Servlet映射条目,差别只在声明位置。我在实际拆项目时一般建议保留web.xml,它把项目中所有URL统一放在一个文件里,排查“请求发到哪去”这类问题,比逐个翻类快得多。

2.3 一次页面请求的完整生命周期:Servlet到DAO再到JDBC

以商品列表为例,浏览器输入地址后,请求经过的链条是固定的:Tomcat读取web.xml或注解里的url-pattern,找到对应Servlet;Servlet在doGet里调用ProDao的方法;ProDao通过DBUtil拿到Connection,执行PreparedStatement的SQL,把ResultSet逐行封装成Product对象,返回List;Servlet把List放进request域,调用forward转发到商品列表页面;页面用EL表达式或JSTL遍历List,把商品名、价格、图片路径渲染成HTML。

这个链条里每个环节都能单独测试。例如先写一个main方法直接调DAO,确认SQL和连接没问题,再回到Servlet层排查URL映射,这是课设阶段最实用的分层排错顺序。如果一上来就启动Tomcat,访问页面报错,你会同时面对前端、Servlet、DAO、数据库四层问题,反而不好定位。

3. DAO层是电商项目的地基:ProDao、MsoDao、UserDao的SQL组织

3.1 原生JDBC还是DBUtils:课设DAO层的最常见实现

课程设计阶段不推荐引入MyBatis或Hibernate,原因不是用不了,而是答辩时更容易被追问“你怎么写SQL的”,如果没有亲手写过PreparedStatement,反而讲不清楚。原生JDBC的步骤是固定的:注册驱动、获取连接、预编译SQL、执行查询、封装结果集、关闭资源。一般会把获取连接和关闭资源抽到一个DBUtil工具类里,避免每个DAO方法重复样板代码,也让ProDao、MsoDao、UserDao只关注SQL本身。下面是一个最常见的DBUtil写法:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/mall" + "?useUnicode=true&characterEncoding=UTF-8"; private static final String USER = "root"; private static final String PASSWORD = "你的数据库密码"; static { try { // 5.x版本驱动类名;8.x请改成 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

这段代码里有三个参数值得注意:URL中的useUnicode和characterEncoding=UTF-8决定中文能否正确写入数据库,漏掉它,商品名和用户名存进去就是乱码;驱动类名随mysql-connector版本不同而不同,引入8.x后仍写老类名会直接报ClassNotFoundException;静态代码块确保类加载时只注册一次驱动,getConnection每次调用都返回新连接,用完必须关闭,否则连接数很快耗尽。

注意:数据库密码直接写在代码里是课设常态,但提交到公开仓库前至少要改成从配置文件读取,不要让明文密码出现在博客截图里。

三个DAO的职责对应项目里的业务模块,可以用一张表概括:

DAO类对应业务模块典型SQL操作
UserDao注册、登录、用户信息INSERT、SELECT by username/password
ProDao商品列表、热卖推荐、详情SELECT、LIMIT分页、条件过滤
MsoDao订单、订单项、结算查询多表JOIN、聚合计算

3.2 ProDao:商品查询、热卖标识与LIMIT分页

ProDao负责跟商品相关的查询,首页热门推荐、商品列表、详情页取数都走它。最典型的场景是“查N件热卖商品”,SQL写法用WHERE is_hot = 1限定范围,再配合ORDER BY和LIMIT截断,既控制页面长度,也避免全表扫描。

public List<Product> selectHotProducts() { List<Product> list = new ArrayList<>(); String sql = "SELECT id, name, price, image FROM products " + "WHERE is_hot = 1 ORDER BY id DESC LIMIT 8"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getDouble("price")); p.setImage(rs.getString("image")); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; }

逻辑说明:ORDER BY id DESC让新上架商品排在前面,LIMIT 8对应首页的八个商品格子;rs.getInt、rs.getString按列名取值,列名必须和SELECT子句的字段名对齐;try-with-resources会自动关闭Connection、PreparedStatement、ResultSet三个资源,不用再写finally里那堆close,这段代码在JDK 7及以上都能编译。price字段用rs.getDouble取出后赋值给实体,但要注意数据库里价格最好用DECIMAL,double在极端精度下会丢失小数点后的值。

对应的products表字段设计可以这样建模:

字段类型说明
idINT, PRIMARY KEY, AUTO_INCREMENT商品ID
nameVARCHAR(100) NOT NULL商品标题
priceDECIMAL(10,2)售价,避免浮点误差
imageVARCHAR(255)图片路径,对应web/images下的文件
is_hotTINYINT1表示热卖,0表示普通

3.3 UserDao:登录校验与唯一性判断

UserDao处理注册、登录和用户信息查询。登录方法的SQL是重点,必须用占位符。这是我在每个项目里都会反复强调的底线:

public User login(String username, String password) { String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setNickname(rs.getString("nickname")); u.setUsername(rs.getString("username")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

这里能看出PreparedStatement的价值。如果SQL改成字符串拼接,例如"WHERE username = '" + username + "'",传入"admin' OR '1'='1"就会让条件恒成立,整张用户表被查出来,这就是最基础的SQL注入。用setString填充占位符,MySQL驱动会自动对特殊字符做转义,这个习惯在课设阶段就值得养成。注册逻辑里还要先按username查一次,已存在则提示用户,避免主键冲突或重复账号。

3.4 MsoDao:订单、用户、商品的三表关联

MsoDao对应订单模块。课设里订单表通常不会单独存在,订单会关联用户表和商品表。查询某用户订单及商品名称时,常见做法是JOIN三张表:

SELECT o.id, o.create_time, o.total_price, p.name AS product_name, u.username FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id JOIN users u ON o.user_id = u.id WHERE o.user_id = ?

这个SQL的语义拆开看就三层:orders只存订单头,包括谁买的、总价、下单时间;order_items存订单明细,记录买了哪些商品、数量;products提供商品信息,users提供用户昵称。MsoDao里封装这条SQL时,返回对象可以是OrderVO,VO内部再放List的明细列表。组装方式在DAO层或Service层做都行,关键是不要出现一个订单查一次数据库的散装写法,连接开销会成倍增加。

4. SendMessageServlet与前端交互:从表单到JSON回显

4.1 HTML表单怎么把数据送进Servlet

Servlet的请求入口分成两类:用表单或超链接发起的传统请求、用Ajax发起的异步请求。SendMessageServlet如果承担的是留言或消息通知,前端一般会有一张这样的表单:

<form id="msgForm"> <label>用户名</label> <input type="text" name="username" id="username"> <label>留言内容</label> <textarea name="content" id="content"></textarea> <button type="submit">提交</button> </form>

注意表单里没有写action属性,因为提交动作会由JavaScript拦截,不需要浏览器默认的action跳转。input和textarea里的name属性是Servlet端getParameter的取参依据,取值和name不一致时会莫名拿不到参数。遇到这类问题先用浏览器开发者工具确认表单控件是否正常渲染、name拼写是否一致,再回头看后端代码,顺序反了会浪费很多时间。

4.2 用fetch把表单数据送到SendMessageServlet

前端JavaScript部分,常见做法是用fetch拦截表单提交,把FormData发给Servlet,再处理后端返回的JSON。这套交互方式在小米商城这类页面上很顺手,不刷新页面就能提交和反馈。

// 监听表单提交,用fetch异步发送 document.getElementById('msgForm').addEventListener('submit', function (e) { e.preventDefault(); // 阻止浏览器默认提交,避免整页刷新 const formData = new FormData(this); fetch('sendMessage', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.code === 0) { alert('留言提交成功'); } else { alert(data.msg); } }) .catch(err => console.error('请求失败', err)); });

这里的请求地址'sendMessage'是相对路径,页面在/app/index.html时,最终请求地址落在/app/sendMessage,正好匹配web.xml里配置的url-pattern。FormData会把表单控件按name自动编码成multipart/form-data格式,Servlet端不需要手动拼接参数。逻辑说明:e.preventDefault()负责中断浏览器默认的整页跳转;res.json()把后端返回的JSON字符串解析成对象;code字段是前后端约定的业务状态码,0代表成功,后续加购物车、提交订单等交互可以复用同一套约定。

4.3 Servlet侧接收参数并输出统一JSON

SendMessageServlet的doPost要做三件事:设置请求和响应编码、从request取出参数、把结果写成JSON字符串。直接拼接JSON是最朴素的方式,但字段一多就容易错。先看接收端的完整写法:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); String username = request.getParameter("username"); String content = request.getParameter("content"); if (username == null || username.trim().isEmpty()) { out.write("{\"code\":1,\"msg\":\"用户名不能为空\"}"); return; } // 省略调用UserDao或MessageDao保存数据的逻辑 out.write("{\"code\":0,\"msg\":\"ok\"}"); }

两个编码设置必须在读取参数和输出之前调用;setCharacterEncoding写在getParameter之前,请求体才能按UTF-8解码;ContentType声明application/json;charset=UTF-8,前端res.json()才能正确解析。如果你翻过SpringMVC的DispatcherServlet源码,会发现框架本质就是做两件事:把请求分发给Controller方法,再把返回值序列化成JSON。原生Servlet项目里没有框架兜底,分发和序列化都靠手工完成,这也是课设练手最有价值的地方。为了减少拼接错误,一般会定义一个JsonResult工具类:

public class JsonResult { private int code; private String msg; private Object data; public static JsonResult success(Object data) { JsonResult r = new JsonResult(); r.code = 0; r.msg = "ok"; r.data = data; return r; } // 构造器、getter/setter省略,实际可引入Jackson或FastJSON序列化 }

统一返回结构的好处是前端只需要处理一种数据形状,不用为每个Servlet单独写解析逻辑。code为0走成功分支,非0走失败分支并弹出msg,这套约定在项目扩展时能省下不少联调时间。

4.4 前端交互承接:hover伪类与返回顶部按钮

商城页面的前端交互集中在商品卡片上。CSS的鼠标移入事件其实不叫事件,而是:hover伪类选择器,这也是热搜里“css 鼠标移入事件”背后真正的技术点。课程资源里常见做法是给商品卡片加一个位移动画:

.product-card { transition: transform .2s ease, box-shadow .2s ease; } .product-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0, 0, 0, .12); }

transform的位移量和阴影透明度可以按页面风格调整,数值改得越大,卡片“飘起来”的感觉越强。类似的交互还有回到顶部按钮,常见做法是用requestAnimationFrame逐帧改变scrollTop值,而不是用setInterval死循环,前者的帧率与浏览器刷新同步,滚动更平滑。这类细节不会增加多少代码量,但会让整个商城的演示效果明显上一个档次。

5. 从“能跑”到“能演示”的两个小技巧:Session保持与Tomcat排错

5.1 用Session维持登录态并做页面级拦截

登录功能做完之后,最关键的是让其他页面知道用户是否已登录。常见做法是把登录成功的用户对象塞进Session,再在需要授权的Servlet入口处做拦截判断:

// 登录成功后写Session request.getSession().setAttribute("loginUser", user); // 需要登录的接口入口判断 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect("login.html"); return; }

getSession(false)不会创建新会话,未登录用户请求时session为null,直接跳回登录页。这里有个易错点:getSession(true)或getSession()会自动创建Session,即使未登录也会生成一个会话ID,判断逻辑就形同虚设了。课设答辩时,老师常问的就是这个细节,能讲清true和false的区别,比背十页概念有用。

5.2 部署期最常见的四个报错及排查表

把源码导入Eclipse或IDEA后,不建议直接运行整个项目,先编译DAO层,确认数据库连接没问题再启动Tomcat。如果看到“错误: 找不到或无法加载主类 org.xxx”,那通常不是代码问题,而是直接把Servlet类当普通Java类运行了,Servlet依赖servlet-api.jar,需要由Tomcat容器提供,本地main方法里没有这个容器环境,自然加载不到主类。下面是几个高频报错的排查顺序:

报错特征优先排查的地方
ClassNotFoundException: com.mysql.jdbc.DriverWEB-INF/lib下是否缺少mysql-connector-j.jar,驱动类名是否与版本匹配
表单提交中文乱码request.setCharacterEncoding("UTF-8")是否在第一个getParameter之前调用
Address already in use: JVM_BindTomcat端口被占用,改server.xml或结束占用进程
404但文件存在IDE发布目录选错,web目录有没有被标记为Web资源根目录

把这几个排错点做成checklist贴在项目README里,换设备演示时能在一分钟内定位一类问题,这也比答辩时临时翻报错日志体面得多。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询