JavaWeb商城项目拆解:Servlet、DAO与前端交互 简介这份资源是一份基于JavaWeb技术栈的小米商城仿真项目前端采用HTMLCSSJavaScript构建页面交互后端使用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调得顺手但一问“登录状态怎么保持的”“商品数据从哪张表来的”就答不上来。这份“小米商城 HTMLCSSJavaScripServlet.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-namesendMessageServlet/servlet-name servlet-classcom.xxx.servlet.SendMessageServlet/servlet-class /servlet servlet-mapping servlet-namesendMessageServlet/servlet-name url-pattern/sendMessage/url-pattern /servlet-mapping这两种方式最终都会被Tomcat解析成内部的Servlet映射条目差别只在声明位置。我在实际拆项目时一般建议保留web.xml它把项目中所有URL统一放在一个文件里排查“请求发到哪去”这类问题比逐个翻类快得多。2.3 一次页面请求的完整生命周期Servlet到DAO再到JDBC以商品列表为例浏览器输入地址后请求经过的链条是固定的Tomcat读取web.xml或注解里的url-pattern找到对应ServletServlet在doGet里调用ProDao的方法ProDao通过DBUtil拿到Connection执行PreparedStatement的SQL把ResultSet逐行封装成Product对象返回ListServlet把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 ?useUnicodetruecharacterEncodingUTF-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和characterEncodingUTF-8决定中文能否正确写入数据库漏掉它商品名和用户名存进去就是乱码驱动类名随mysql-connector版本不同而不同引入8.x后仍写老类名会直接报ClassNotFoundException静态代码块确保类加载时只注册一次驱动getConnection每次调用都返回新连接用完必须关闭否则连接数很快耗尽。注意数据库密码直接写在代码里是课设常态但提交到公开仓库前至少要改成从配置文件读取不要让明文密码出现在博客截图里。三个DAO的职责对应项目里的业务模块可以用一张表概括DAO类对应业务模块典型SQL操作UserDao注册、登录、用户信息INSERT、SELECT by username/passwordProDao商品列表、热卖推荐、详情SELECT、LIMIT分页、条件过滤MsoDao订单、订单项、结算查询多表JOIN、聚合计算3.2 ProDao商品查询、热卖标识与LIMIT分页ProDao负责跟商品相关的查询首页热门推荐、商品列表、详情页取数都走它。最典型的场景是“查N件热卖商品”SQL写法用WHERE is_hot 1限定范围再配合ORDER BY和LIMIT截断既控制页面长度也避免全表扫描。public ListProduct selectHotProducts() { ListProduct 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取出后赋值给实体但要注意数据库里价格最好用DECIMALdouble在极端精度下会丢失小数点后的值。对应的products表字段设计可以这样建模字段类型说明idINT, PRIMARY KEY, AUTO_INCREMENT商品IDnameVARCHAR(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 11就会让条件恒成立整张用户表被查出来这就是最基础的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时返回对象可以是OrderVOVO内部再放List的明细列表。组装方式在DAO层或Service层做都行关键是不要出现一个订单查一次数据库的散装写法连接开销会成倍增加。4. SendMessageServlet与前端交互从表单到JSON回显4.1 HTML表单怎么把数据送进ServletServlet的请求入口分成两类用表单或超链接发起的传统请求、用Ajax发起的异步请求。SendMessageServlet如果承担的是留言或消息通知前端一般会有一张这样的表单form idmsgForm label用户名/label input typetext nameusername idusername label留言内容/label textarea namecontent idcontent/textarea button typesubmit提交/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侧接收参数并输出统一JSONSendMessageServlet的doPost要做三件事设置请求和响应编码、从request取出参数、把结果写成JSON字符串。直接拼接JSON是最朴素的方式但字段一多就容易错。先看接收端的完整写法protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setContentType(application/json;charsetUTF-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;charsetUTF-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里换设备演示时能在一分钟内定位一类问题这也比答辩时临时翻报错日志体面得多。本文还有配套的精品资源点击获取