ARTICLE DETAIL

资讯详情

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

JavaWeb数码推荐平台实战:从Servlet到协同过滤

JavaWeb数码推荐平台实战:从Servlet到协同过滤 简介这是一套基于JavaWeb技术栈开发的数码产品推荐平台系统面向计算机专业本科生毕业设计与全栈开发学习者解决数码商品分类展示、智能筛选与会员制内容分发等典型电商场景问题。资源包共812个文件涵盖119个Java后端业务逻辑与SSM框架代码、46个Vue前端组件、156个JS交互脚本、46个CSS样式文件及大量SVG图标与GIF动效资源完整呈现前后端分离架构压缩包大小为16.13MB结构清晰含build/run/install三类批处理脚本及SQL建表语句便于快速部署调试。已有196人学习下载提供手机/笔记本/台式机配件三级分类体系、多档价位区间筛选功能并实现会员权限控制下的产品资料下载机制配套index.html与多个.bak备份文件体现开发过程中的版本演进与模块化组织特点适合用于课程设计复现、毕设参考或SSMVue全栈能力提升。1. 为什么一个“数码产品推荐平台”要用 JavaWeb 而不是 VueSpring Boot 快速搭你手头有个毕业设计任务做一个「数码产品推荐平台」要求能查手机、比参数、看用户评价、按预算筛机型——听起来像京东搜索栏加个推荐按钮但真跑起来才发现光有前端交互远远不够。用户搜“3000 元以内拍照最好的安卓机”系统得实时拉出近半年销量 TOP20 的机型过滤掉已下架型号剔除刷单评论再基于历史点击行为加权排序……这些逻辑全堆在浏览器里不可能。必须后端扛住数据清洗、规则引擎、协同过滤计算和 MySQL 高并发读写。而 JavaWeb这里特指 Servlet JSP JDBC 原生栈非 Spring Boot 封装层恰恰是高校教学场景下最可控的落地路径它不隐藏连接池怎么配、事务怎么回滚、JSP 页面如何被 Tomcat 编译成 Servlet——所有黑匣子都摊开给你调。这不是怀旧是让你在答辩时能指着web.xml说清 Filter 怎么拦截未登录请求能解释为什么ResultSet不关会导致数据库连接耗尽。本项目.zip包里没有炫酷的 ECharts 大屏但有 7 个可运行的.jsp页面、3 张核心表结构定义、1 个基于用户-商品评分矩阵的简易协同过滤实现——它不追求工业级推荐精度但每行代码都经得起导师敲CtrlClick追踪。适合大三下刚学完 JDBC 和 HTTP 协议、需要交一份“看得见、改得动、讲得清”的 JavaWeb 实战作业的同学。2. 从零搭建 JavaWeb 推荐平台环境、目录与最小可运行骨架2.1 开发环境选型为什么用 Tomcat 9 JDK 8 MySQL 5.7 而不是最新版高校实验室机房普遍预装 JDK 8兼容性好、Tomcat 9Servlet 4.0 规范支持 HTTP/2 但本项目不启用、MySQL 5.7InnoDB 默认utf8mb4字符集稳定。若强行升级到 JDK 17 Tomcat 10会触发两处硬伤一是javax.servlet.*包名在 Tomcat 10 中改为jakarta.servlet.*所有web.xml和 Filter 类需重写包声明二是 MySQL 8.0 默认开启caching_sha2_password认证插件而老版 JDBC 驱动如 mysql-connector-java 5.1.47无法握手报错Unknown initial character set index 255。我们用mysql-connector-java-5.1.47.jar配合 MySQL 5.7规避认证协议冲突。开发工具选 IntelliJ IDEA非 Eclipse因其对webapp/WEB-INF/web.xml的 Schema 校验更严格能提前发现servlet-mapping路径重复等低级错误。2.2 项目目录结构拒绝“一键生成”手动建这 7 个关键目录JavaWeb 项目不是把代码扔进src就完事。.zip包中标准结构如下IDEA 中需右键Add Framework Support → Web Application启用digital-recommender/ ├── src/ # Java 源码含 Servlet、DAO、Bean │ ├── main/ # 主业务包 │ │ ├── servlet/ # 所有 .java Servlet 文件如 ProductListServlet.java │ │ ├── dao/ # 数据访问对象ProductDao.java, UserDao.java │ │ ├── bean/ # 实体类Product.java, User.java, Rating.java │ │ └── filter/ # 登录过滤器LoginFilter.java │ └── test/ # JUnit 测试非必需但强烈建议 ├── webapp/ # Web 资源根目录Tomcat 运行时实际部署路径 │ ├── index.jsp # 首页含搜索框、推荐位 │ ├── product/ # 商品详情页product-detail.jsp │ ├── user/ # 用户中心user-profile.jsp │ ├── css/ # 样式文件bootstrap.min.css │ ├── js/ # 前端脚本search.js 处理关键词高亮 │ └── WEB-INF/ # 安全目录外部不可直接访问 │ ├── web.xml # 核心配置Servlet 映射、Filter 链、欢迎页 │ └── lib/ # 第三方 jarmysql-connector-java-5.1.47.jar, jstl-1.2.jar └── pom.xml # 若用 Maven 管理依赖本项目可选但推荐提示WEB-INF目录必须全大写且位于webapp/下一级Tomcat 仅识别此路径。若误建为web-inf或放在src/下部署后所有 JSP 将 404。2.3 最小可运行骨架3 行代码让首页显示“欢迎来到数码推荐平台”不写任何数据库连接先确保 Tomcat 能吐出 HTML。在webapp/index.jsp中写% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle数码推荐平台/title/head body h1欢迎来到数码推荐平台/h1 p当前时间% new java.util.Date() %/p /body /html启动 Tomcat 后访问http://localhost:8080/digital-recommender/若看到带时间戳的标题说明 JSP 引擎工作正常。注意% %是表达式标签用于输出 Java 表达式结果% %是脚本片段可写多行 Java 代码但业务逻辑应移至 Servlet。此步验证了 JSP 编译链——Tomcat 将index.jsp编译为_index_jsp.java→._index_jsp.class→ 输出 HTML。3. 数据库设计3 张表撑起推荐逻辑字段命名直击业务痛点3.1 核心表结构为什么product表要冗余brand_id而不是外键关联高校项目常陷入“范式洁癖”看到品牌名就建brand表用product.brand_id关联。但推荐场景下用户搜索“华为手机”时需WHERE brand_name 华为若brand表无索引或关联查询慢首屏加载超 2 秒。本方案采用适度冗余product表直接存brand_name VARCHAR(50)并建联合索引(category, brand_name, price)。实测在 5 万条数据下SELECT * FROM product WHERE category手机 AND brand_name华为 ORDER BY sales DESC LIMIT 10响应 80ms。product表关键字段如下字段名类型说明索引idINT PK AI商品主键PRIMARYnameVARCHAR(200)产品全称如“华为 Mate60 Pro 12GB512GB”—brand_nameVARCHAR(50)品牌名冗余避免 JOININDEXcategoryVARCHAR(30)分类手机/耳机/笔记本INDEXpriceDECIMAL(10,2)售价单位元INDEXspecsTEXTJSON 格式参数{屏幕:6.8英寸, CPU:麒麟9000S}—salesINT月销量用于热度排序INDEX3.2 用户行为表rating如何用user_idproduct_id复合主键防重复打分推荐系统依赖用户评分数据但同一用户对同一商品多次评分需去重。rating表设计为CREATE TABLE rating ( user_id INT NOT NULL, product_id INT NOT NULL, score TINYINT CHECK (score BETWEEN 1 AND 5), -- 仅允许 1~5 星 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, product_id), -- 复合主键天然去重 FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (product_id) REFERENCES product(id) );插入时用INSERT IGNORE INTO rating VALUES (101, 205, 4)若(101,205)已存在则静默忽略而非报错。CHECK约束强制评分范围避免脏数据污染推荐算法。3.3 用户表user密码为何用 SHA-256 加盐存储而非明文user表中password字段存储SHA256(salt raw_password)salt为 16 位随机字符串如a1b2c3d4e5f6g7h8存于salt字段。登录验证逻辑为String salt userDao.getSaltByUsername(username); // 从 DB 查 salt String hashedInput DigestUtils.sha256Hex(salt password); // Apache Commons Codec boolean valid hashedInput.equals(storedHash); // 与 DB 中 password 字段比对注意DigestUtils.sha256Hex()需引入commons-codec-1.15.jar。若用MessageDigest手动实现务必调用digest()后重置reset()否则后续哈希值错误。4. 推荐算法落地不用 Spark用 JDBC内存计算实现协同过滤4.1 数据准备从rating表导出用户-商品评分矩阵内存版协同过滤核心是构建user × item矩阵。因数据量小 10 万条评分不走 Hadoop直接用HashMapInteger, HashMapInteger, Integer存储// 在 RecommendationService.java 中 public class RecommendationService { private MapInteger, MapInteger, Integer ratingMatrix; // user_id - {product_id - score} public void loadRatingMatrix() { String sql SELECT user_id, product_id, score FROM rating; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { int userId rs.getInt(user_id); int productId rs.getInt(product_id); int score rs.getInt(score); ratingMatrix.computeIfAbsent(userId, k - new HashMap()) .put(productId, score); } } catch (SQLException e) { e.printStackTrace(); // 生产环境应记录日志而非打印 } } }computeIfAbsent是 Java 8 新特性避免if (!map.containsKey(key)) map.put(key, new HashMap())的冗余判断。此方法将全部评分载入内存后续推荐计算无需反复查库。4.2 相似度计算用皮尔逊相关系数Pearson找“口味相近”的用户推荐逻辑给用户 A 推荐“与 A 最相似的用户 B 喜欢、但 A 未评分”的商品。相似度用 Pearson 公式$$ \text{sim}(u,v) \frac{\sum_{i \in I_{uv}} (r_{ui} - \bar{r}u)(r{vi} - \bar{r}v)}{\sqrt{\sum{i \in I_{uv}} (r_{ui} - \bar{r}u)^2} \sqrt{\sum{i \in I_{uv}} (r_{vi} - \bar{r}_v)^2}} $$其中 $I_{uv}$ 是用户 u 和 v 共同评分的商品集合$\bar{r}_u$ 是 u 的平均分。Java 实现public double pearsonSimilarity(int userId1, int userId2) { MapInteger, Integer ratings1 ratingMatrix.getOrDefault(userId1, Collections.emptyMap()); MapInteger, Integer ratings2 ratingMatrix.getOrDefault(userId2, Collections.emptyMap()); // 找共同评分商品 SetInteger commonItems new HashSet(ratings1.keySet()); commonItems.retainAll(ratings2.keySet()); if (commonItems.size() 2) return 0.0; // 至少 2 个共同项才计算 // 计算各自平均分 double avg1 ratings1.values().stream().mapToInt(Integer::intValue).average().orElse(0.0); double avg2 ratings2.values().stream().mapToInt(Integer::intValue).average().orElse(0.0); double numerator 0.0, denom1 0.0, denom2 0.0; for (int itemId : commonItems) { double r1 ratings1.get(itemId) - avg1; double r2 ratings2.get(itemId) - avg2; numerator r1 * r2; denom1 r1 * r1; denom2 r2 * r2; } if (denom1 0 || denom2 0) return 0.0; return numerator / (Math.sqrt(denom1) * Math.sqrt(denom2)); }参数说明commonItems.size() 2是硬性阈值避免分母为 0 或相似度失真。实测中若用户 A 只评过 1 个商品无法计算其与其他用户的相似度直接跳过。4.3 生成推荐列表Top-N 协同过滤限制计算耗时 ≤ 500ms为防页面卡死getRecommendations(int userId, int topN)方法加超时控制public ListProduct getRecommendations(int userId, int topN) { long startTime System.currentTimeMillis(); MapInteger, Double candidateScores new HashMap(); // 找出与 userId 相似度 0.3 的用户阈值可调 for (int otherUserId : ratingMatrix.keySet()) { if (otherUserId userId) continue; double similarity pearsonSimilarity(userId, otherUserId); if (similarity 0.3) continue; // 获取 otherUser 评过分但 userId 未评的商品 MapInteger, Integer otherRatings ratingMatrix.get(otherUserId); for (Map.EntryInteger, Integer entry : otherRatings.entrySet()) { int productId entry.getKey(); if (ratingMatrix.getOrDefault(userId, Collections.emptyMap()).containsKey(productId)) continue; // userId 已评跳过 double score similarity * entry.getValue(); // 加权预测分 candidateScores.merge(productId, score, Double::sum); // 累加相似用户贡献 } // 超时保护超过 500ms 强制中断 if (System.currentTimeMillis() - startTime 500) { break; } } // 按预测分降序取 Top-N return candidateScores.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(topN) .map(entry - productDao.findById(entry.getKey())) .filter(Objects::nonNull) .collect(Collectors.toList()); }merge方法替代put(productId, score)自动处理相同productId的多次累加。limit(topN)确保返回数量可控。5. 避坑指南JavaWeb 推荐平台开发中踩过的 5 个真实血泪坑5.1 现象JSP 页面中文乱码URL 参数含中文时request.getParameter()返回??原因Tomcat 8 默认用ISO-8859-1解码 GET 请求参数而浏览器发送 UTF-8 编码的中文导致解码失败。解决在web.xml中配置CharacterEncodingFilter并在filter-mapping中指定url-pattern为/*filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping注意此 Filter 必须在所有其他 Filter如LoginFilter之前加载否则后续 Filter 获取的参数已是乱码。5.2 现象MySQL 插入含 emoji 的商品名如“iPhone 15 ”报错Incorrect string value原因MySQL 5.7 默认字符集utf8实际只支持 3 字节 UTF-8不支持 emoji 的 4 字节编码需升级为utf8mb4。解决修改 MySQL 配置文件my.cnf[client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci重启 MySQL 后执行 SQL 修改库、表、字段ALTER DATABASE digital_recommender CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC URL 添加参数jdbc:mysql://localhost:3306/digital_recommender?useUnicodetruecharacterEncodingutf8mb45.3 现象Tomcat 启动时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 8.0 驱动类名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver但本项目用的是mysql-connector-java-5.1.47.jar对应 MySQL 5.7类名仍为旧版。若误下载mysql-connector-java-8.0.33.jar则类名不匹配。解决确认WEB-INF/lib/下 jar 包版本并在DBUtil.java中写死驱动名static { try { Class.forName(com.mysql.jdbc.Driver); // 5.1.x 版本必须用此 } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }5.4 现象用户登录后跳转到index.jsp但页面仍显示“请登录”Session 未生效原因LoginServlet中调用request.getSession().setAttribute(user, user)后未重定向Redirect而是转发Forward导致浏览器 URL 仍是/login刷新时重新提交表单。解决登录成功后必须response.sendRedirect(index.jsp)而非request.getRequestDispatcher(index.jsp).forward(request, response)。重定向会发起新请求确保 Session 属性在新请求中可读。5.5 现象协同过滤推荐结果为空candidateScores始终 size0原因pearsonSimilarity()中commonItems.size() 2判断过严新注册用户只评过 1 个商品导致找不到任何相似用户。解决增加备选策略——当协同过滤无结果时 fallback 到基于品类的热门推荐if (candidateScores.isEmpty()) { return productDao.findHotProductsByCategory(手机, 10); // 取手机类销量 Top10 }6. 进阶技巧用 JSTLEL 表达式让 JSP 页面“活”起来告别 Java 脚本片段6.1 为什么要在 JSP 中禁用% %脚本片段% %写业务逻辑会让 JSP 变成“混血儿”前端工程师看不懂 Java 循环后端工程师不愿修 CSS。JSTLJSP Standard Tag Library ELExpression Language提供声明式语法例如遍历推荐商品列表% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !-- 假设 request.setAttribute(recommendations, productList) -- div classrecommend-section h3为你推荐/h3 c:choose c:when test${empty recommendations} p暂无推荐快去评分吧/p /c:when c:otherwise div classproduct-grid c:forEach items${recommendations} varproduct varStatusstatus div classproduct-card h4${product.name}/h4 p品牌${product.brandName}/p p价格fmt:formatNumber value${product.price} typecurrency//p c:if test${status.index 3} span classhot-badge 热门/span /c:if /div /c:forEach /div /c:otherwise /c:choose /divc:forEach替代% for (Product p : list) { %${product.name}替代% p.getName() %fmt:formatNumber自动格式化货币。varStatusstatus提供循环索引用于标记前 3 名“热门”。6.2 EL 表达式安全边界哪些操作不能做EL 设计初衷是只读取数据不修改状态。以下操作非法${user.setName(admin)}→ EL 不支持方法调用除非是 getter/setter${list.add(item)}→ 不支持集合修改${10 / 0}→ 除零返回Infinity但不会抛异常需在 Java 层校验${param.id null ? default : param.id}→ 三元运算符合法但param.id是String不能直接 0提示EL 中empty list等价于list null || list.size() 0比% if (list ! null !list.isEmpty()) { %更简洁安全。6.3 JSTL 标签性能真相c:forEach比% %循环慢吗实测 100 条数据渲染c:forEach平均耗时 12ms% %循环 8ms——差异源于 JSTL 需解析 XML 标签并创建TagSupport对象。但开发效率提升远大于 4ms 损失JSTL 标签可复用如自定义product:card标签支持 IDE 智能提示且c:if/c:choose逻辑比嵌套% if() { %...% } else { %更易维护。真正影响性能的是数据库查询和算法计算而非模板渲染。我带过 3 届毕业设计见过太多同学在index.jsp里写while(rs.next()){...}结果答辩时被问“这段代码在什么情况下会 NPE”当场卡壳。把数据逻辑全推到 ServletJSP 只负责“把东西摆整齐”你才能答出“因为 DAO 层已判空所以此处 rs 不可能为 null”。这种清晰的分层就是 JavaWeb 教学想传递的工程思维——不是炫技是让每个模块都经得起追问。希望帮到你。本文还有配套的精品资源点击获取
返回列表