拓冰建站拓冰建站
首页 / 资讯中心 / 正文

广告墙Java课程设计:从JDBC到Servlet的Web开发实战

简介这是一份面向计算机相关专业在校学生与初学者的Java课程设计资源以广告墙系统为实战场景涉及面向对象编程、GUI界面开发、数据库交互等核心知识点。资源共包含138个文件以82个Java源码文件为主另附44个已编译的class文件、6个XML配置、2个iml模块文件及1个SQL数据库脚本整体体积仅1.05MB轻量易部署。其中SQL脚本可直接初始化数据表配合README说明快速运行项目适合用于答辩演示或功能二次拓展。该项目已经过完整测试上传者标注答辩评审平均分94.5分目前已有375人学习或下载。通过研读源码可掌握注册登录、广告信息增删改查、管理员与普通用户权限区分等典型功能模块的实现思路在有基础的前提下还能替换界面或增加统计模块作为课程设计、毕业设计或入门练手项目均有较高参考价值。1. 广告墙这门 Java 课程实验设计卡住新手的从来不是代码大学机房里的 java 课程实验设计常给「广告墙」这类题目用户能发布广告、浏览全部广告、按分类筛选管理员再做审核下架。它比图书管理系统多一层审核流比商城少一堆订单状态规模刚好覆盖 java 基础阶段的 JDBC、Servlet、JSP 和 MySQL 四大件。多数人卡住的不是写不出代码而是表怎么拆、请求怎么分流、刷新页面为什么重复插入——这三个坎恰好是后端入门最常踩的坑。把这条链路完整走通课程实验能交简历上也能多一个完整的 java 项目。之后面对 java 面试题里的分页、注入、转发与重定向至少能拿真实场景去对照而不是背八股文。2. 广告墙的数据库设计与 JDBC 访问层先让增删改查立住2.1 广告墙的表结构按审核流拆字段别把联系方式塞进正文广告墙最核心的实体是「广告」。第一次做的人容易只建一张表把标题、内容、联系方式、发布时间全塞进去等做到筛选和审核时就后悔了。我一般会拆成三张表用户表 user、分类表 category、广告表 ad。分类哪怕只是几个固定枚举值也建议建表而不是写死在代码里因为实验报告要求画 ER 图时分类作为独立实体更好讲清楚表间关系。广告表的关键字段可以这样定字段类型说明idBIGINT主键自增titleVARCHAR(80)标题列表页直接展示contentVARCHAR(2000)正文contact_wayVARCHAR(100)联系方式独立字段方便审核脱敏category_idINT关联分类表publisher_idBIGINT发布者关联用户表statusTINYINT1 待审核 / 2 上架 / 3 下架expire_timeDATETIME过期时间可为空view_countINT浏览数create_timeDATETIME创建时间status 字段是广告墙与普通「公告栏」拉开差距的地方广告墙必须含审核概念前端列表只查 status2 的上架数据管理员页面查全部再逐条审核。contact_way 单独成字段而不写进 content因为审核通过时常常要做局部脱敏联系方式混在正文里会非常难替换。建表 SQL 在 MySQL 8 下这样写CREATE TABLE ad ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(80) NOT NULL, content VARCHAR(2000) NOT NULL, contact_way VARCHAR(100) NOT NULL, category_id INT NOT NULL, publisher_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1, expire_time DATETIME NULL, view_count INT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category_status (category_id, status), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个索引按查询场景设计idx_category_status 支撑「某个分类下只看上架广告」的组合过滤idx_create_time 支撑首页按时间倒序分页。字符集用 utf8mb4 而不是 utf8因为广告内容里可能有人发 Emojiutf8 在 MySQL 中存不下四字节字符这是中文乱码之外最容易忽略的坑。AUTO_INCREMENT 主键保证插入顺序与 create_time 基本一致但不代表业务上可以依赖 id 排序排序仍然要显式 ORDER BY create_time。2.2 JDBC 连接管理参数外部化别把连接串写死在代码里课程实验阶段通常没引入 MyBatis手写 JDBC 是必须过的一关。常见做法是写一个 JdbcUtils 工具类负责拿 Connection驱动类名和连接参数放在 classpath 下的 db.properties 里public final class JdbcUtils { private static final String URL; private static final String USER; private static final String PASSWORD; static { try (InputStream in JdbcUtils.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); URL props.getProperty(jdbc.url); USER props.getProperty(jdbc.user); PASSWORD props.getProperty(jdbc.password); // MySQL 8 驱动类名必须用 8.x 前缀老教程里的类已被移除 Class.forName(com.mysql.cj.jdbc.Driver); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }db.properties 里对应写 jdbc.urljdbc:mysql://localhost:3306/ad_wall?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。这三个参数分别解决三个问题本地开发不需要 SSL 握手MySQL 8 驱动要求显式时区否则连接阶段直接报错characterEncodingutf8 保证中文在传输层不被转成乱码。驱动类名是 com.mysql.cj.jdbc.Driver网上大量老教程写的 com.mysql.jdbc.Driver 在 8.x 驱动里已经废弃照抄会抛 ClassNotFoundException。提示JdbcUtils 每次调用都新建 Connection在课程实验的量级完全够用。要优化就引入 HikariCP 连接池但实验报告里写清楚「连接池 vs 直连」的差异即可不必为了加分而上框架。2.3 广告墙列表的分页参数化 LIMIT 与 COUNT 保持一致广告墙首页必然要分页。分页不只是 LIMIT 两个数字得先统计总条数算出总页数再取当前页数据。我的做法是在 AdDao 里放两个方法count 负责统计pageQuery 负责取当前页这两个方法拼接 WHERE 条件时保持同一套逻辑。public ListAd pageQuery(int pageNum, int pageSize, Integer categoryId, String keyword) { StringBuilder sql new StringBuilder( SELECT id, title, contact_way, category_id, view_count, create_time FROM ad WHERE status 2 ); ListObject params new ArrayList(); if (categoryId ! null) { sql.append(AND category_id ? ); params.add(categoryId); } if (keyword ! null !keyword.isBlank()) { sql.append(AND (title LIKE ? OR content LIKE ?) ); params.add(% keyword %); params.add(% keyword %); } // 偏移量 (页码 - 1) * 每页条数pageNum 从 1 开始 sql.append(ORDER BY create_time DESC LIMIT ?, ?); params.add((pageNum - 1) * pageSize); params.add(pageSize); try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { ListAd list new ArrayList(); while (rs.next()) { Ad ad new Ad(); ad.setId(rs.getLong(id)); ad.setTitle(rs.getString(title)); ad.setContactWay(rs.getString(contact_way)); ad.setCategoryId(rs.getInt(category_id)); ad.setViewCount(rs.getInt(view_count)); ad.setCreateTime(rs.getTimestamp(create_time).toLocalDateTime()); list.add(ad); } return list; } } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } }count 方法把 SELECT 列换成 COUNT(*)WHERE 与参数拼接完全一致。两边不一致的典型症状是总页数算出来 5 页翻到第 5 页却是空列表因为 pageQuery 多了一个过滤条件。LIMIT 的第一个占位符是偏移量 offset(pageNum-1)*pageSize第二个是每页条数pageNum 从 1 开始前端传 page0 时要兜底成 1否则 offset 变成负数MySQL 会把它当作 0 处理。提示排查分页问题最直接的办法是打印最终 SQL。在 ps 执行前加一行 System.out.println(ps.toString())把问号替换成实际参数人工核对90% 的翻页问题当场就能看出来。3. 广告墙的 Service 与 Servlet 控制层请求怎么变成数据变更3.1 为什么广告墙需要一个 Service 层而不是在 JSP 里写校验课程实验最容易出现的坏味道是把发布校验写在 JSP 的% %脚本片段里。表面看代码量少了实际校验逻辑和页面渲染耦合死后面加管理员审核时根本复用不了。我一般会在 DAO 之上加一个 AdService把「发布一条广告」包装成完整规则标题必填且不超过 80 字内容必填不超过 2000 字联系方式必填初始状态置为待审核。public class AdService { private AdDao adDao new AdDao(); public Result publish(Ad ad) { if (ad.getTitle() null || ad.getTitle().isBlank() || ad.getTitle().length() 80) { return Result.fail(标题不能为空且不能超过80字); } if (ad.getContent() null || ad.getContent().length() 2000) { return Result.fail(内容不能为空且不能超过2000字); } if (ad.getContactWay() null || ad.getContactWay().isBlank()) { return Result.fail(联系方式不能为空); } ad.setStatus(AdStatus.PENDING); adDao.insert(ad); return Result.ok(); } }Result 是统一返回包装类内部放 success、message、data 三个字段。返回 Result 而不是直接抛异常是因为用户输入错误属于「预期内失败」应该用返回值告诉页面具体原因而不是让整个请求 500。这也是 java 面试题里「返回码与异常怎么选」的实践答案预期内的业务失败用返回值连接断开、SQL 语法错误这类系统异常才用 Exception。参数上限 80 字与 2000 字不是拍脑袋定的它们和表结构的 VARCHAR 长度必须一一对应改一头不改另一头数据入库时会被 MySQL 截断或直接报错。3.2 Servlet 统一入口WebServlet(/ad/*) 与请求分发Servlet 3.0 之后可以用注解代替 web.xml 配置。广告墙我习惯只写一个 AdServlet用WebServlet(/ad/*)挂在前缀上再按 pathInfo 分发到不同方法路径方法职责/ad/listGET分页列表带分类与关键词/ad/detailGET详情页浏览量自增/ad/publishPOST发布广告/ad/auditGET管理员审核改状态WebServlet(/ad/*) public class AdServlet extends HttpServlet { private AdService adService new AdService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); if (/list.equals(path)) { list(req, resp); } else if (/detail.equals(path)) { detail(req, resp); } else if (/audit.equals(path)) { audit(req, resp); // 管理员审核实验里可省去权限校验 } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); if (/publish.equals(path)) { publish(req, resp); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } }统一入口的好处是加登录 Filter 时只需拦截 /ad/* 一个模式不用在 web.xml 里维护一堆 servlet-mapping。list 方法要做三件事给 page、categoryId、keyword 设默认值并转成目标类型调 service 拿分页结果最后req.setAttribute(pageResult, ...)交给 JSP。detail 里容易忽略的是浏览量自增直接用UPDATE ad SET view_count view_count 1 WHERE id ?做原子更新先 SELECT 再 UPDATE 在并发下会丢计数虽然广告墙实验没人压测但面试官会顺着这个话头问「怎么保证并发正确」。提示POST 方法必须在读取任何参数之前调用req.setCharacterEncoding(UTF-8)否则 Tomcat 容器默认按 ISO-8859-1 解析请求体中文参数进来全是乱码。启动应用前先确认 JAVA_HOME 已配置、java -version 输出的是你期望的 JDK 版本曾经有人把 JAVA_HOME 指到 JRE 上Tomcat 直接起不来。3.3 广告墙发布成功后为什么必须 302 重定向发布广告提交后最常见的错误是req.getRequestDispatcher(list.jsp).forward(req, resp)。转发意味着浏览器地址栏还停留在 /ad/publish这时用户按 F5 刷新浏览器会重新提交上一次的 POST广告墙里立刻出现两条一模一样的广告。正确做法是 Post-Redirect-Get 模式发布成功先重定向到 /ad/list浏览器变成一次干净的 GET刷新多少次都不会重复插入。Result result adService.publish(ad); if (result.isOk()) { resp.sendRedirect(req.getContextPath() /ad/list); } else { // 失败时把错误与草稿放进 session跳回发布页回显 req.getSession().setAttribute(publishError, result.getMessage()); req.getSession().setAttribute(draft, ad); resp.sendRedirect(req.getContextPath() /ad/publish-page); }失败路径同样用重定向但必须把错误信息和用户已填的表单内容存进 session否则页面跳回来时空表单会让人抓狂。JSP 端用${publishError}显示错误、${draft.title}回填标题用完记得 removeAttribute否则下一次发布会带着上一次的残留数据。sendRedirect 前加req.getContextPath()是为了兼容应用部署在非根路径的情况比如访问地址是 /adWall/ad/list 而不是 /ad/list省掉这个前缀会导致跳转 404。4. 广告墙的 JSP 渲染与搜索筛选列表页的细节决定完成度4.1 用 JSTL 渲染广告列表不在 JSP 里写 Java广告墙的列表页是门面也是实验答辩时老师第一眼看到的东西。JSP 里可以写% %脚本片段但维护和查重都很痛苦。我一般用 JSTL 的 core 标签库页面顶部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %用到时间格式化时再加% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %列表部分长这样c:forEach varad items${pageResult.list} div classad-item h3 a href${pageContext.request.contextPath}/ad/detail?id${ad.id} c:out value${ad.title}/ /a /h3 pc:out value${ad.content}//p span classmeta c:out value${ad.categoryName}/ · fmt:formatDate value${ad.createTime} patternyyyy-MM-dd HH:mm/ · 浏览 ${ad.viewCount} /span /div /c:forEachc:out不只是输出变量它默认对 HTML 特殊字符做转义。用户发布的广告内容含script标签时${ad.title}直接输出会被浏览器当脚本执行换成c:out后显示的是普通文本这是广告墙这类 UGC 系统必须做的 XSS 防线。EL 表达式里的 ad.title 对应 Ad 类的 getTitle() 方法属性和 getter 的映射遵循 JavaBean 规范如果列表想显示发布者名字而不是 publisherId就在 Ad 里加一个 categoryName 或 publisherName 字段由 DAO 联表查出后塞进去而不是在 JSP 里再查一次库。4.2 分页导航与筛选条件URL 即状态分页导航是列表页第二个细节。上一页、下一页、页码链接必须把当前筛选条件一起带过去否则用户选了「二手」分类翻到第 3 页点下一页后 categoryId 丢了直接跳回全量列表体验非常割裂。常见做法是用c:url配合c:param生成带 query 的链接c:url value/ad/list varpageUrl c:param namecategoryId value${param.categoryId}/ c:param namekeyword value${param.keyword}/ /c:url div classpagination a href${pageUrl}page${pageResult.pageNum - 1}上一页/a span第 ${pageResult.pageNum} / ${pageResult.pages} 页/span a href${pageUrl}page${pageResult.pageNum 1}下一页/a /div${param.categoryId}是 EL 内置对象直接读 URL 上的请求参数不需要 Servlet 再往 request 里塞一遍。搜索框用 GET 表单提交到同一个 /ad/list输入框 name 叫 keyword这样搜索之后地址栏变成?keywordxxxcategoryId2整个页面状态完全由 URL 描述——刷新、复制链接、发给别人看结果都一样。这是广告墙实验里最值得刻意练习的「URL 即状态」思路后面学 Spring MVC 的 RequestParam 和 ModelAttribute 时会发现是同一套思想的框架化表达。4.3 广告墙关键词搜索的 LIKE 与索引失效边界关键词搜索在广告墙里通常这样实现WHERE title LIKE ? OR content LIKE ?参数是%keyword%。带前导通配符 % 的 LIKE 无法命中 B 树索引也就是搜索时全表扫描。课程实验的数据量下完全没问题但如果简历写「支撑高并发」面试官顺着问搜索性能时要能说出这条边界数据量上来之后要么上 MySQL 的 FULLTEXT 全文索引要么引入 Elasticsearch实验阶段不引中间件写明取舍即可。搜索还有一个容易被忽略的细节LIKE 的参数同样走 PreparedStatement 占位符不能拼进 SQL 字符串。另外用户输入 % 或 _ 时这两个通配符会改变查询语义严格的做法是在拼参数前转义课程实验里接受「输入 % 匹配所有内容」这个小瑕疵也不扣分但必须在实验报告的测试用例里写清楚这个行为而不是留下一个自己都解释不清的现象。排序方面关键词结果同样按 create_time DESC 排分页参数照旧形成一个完整的「筛选 搜索 分页」闭环。5. 广告墙的验收自测curl 打接口与面试答题的着力点5.1 用 curl 快速验证发布与分页链路实验答辩前与其在浏览器里反复手点不如用 curl 把核心接口过一遍。假设 Tomcat 端口 8080应用上下文 adWall# 发布一条广告--data-urlencode 保证中文参数正确编码 curl -X POST http://localhost:8080/adWall/ad/publish \ --data-urlencode title二手自行车 \ --data-urlencode content9成新价格面议 \ --data-urlencode contactWay13800000000 \ --data-urlencode categoryId2 # 拉取分页列表 curl http://localhost:8080/adWall/ad/list?page1pageSize10 # 带分类与关键词筛选 curl http://localhost:8080/adWall/ad/list?page1categoryId2keyword%E4%BA%8C%E6%89%8B用 curl -v 看响应头发布成功后 Location 应指向 /adWall/ad/list状态码 302说明 PRG 生效列表接口返回的 HTML 里出现的是转义后的普通文本说明 JSP 渲染与 XSS 转义都在起作用。这套命令可以直接写进实验报告「测试方法」一节比截图更有说服力。5.2 java 面试题里关于广告墙的四个高频追问把广告墙写进简历后java 面试问项目时问题会集中在这四个点。第一PreparedStatement 为什么能防 SQL 注入参数在服务端被当作数据处理不参与 SQL 结构解析字符串拼接会把用户输入变成 SQL 片段语义就被改写。第二分页为什么用LIMIT offset, size而不用全量查询全查会把内存打满LIMIT 让数据库只回当前页offset 极大时性能劣化能顺带答出「用上一页最后一条记录的主键做游标翻页」就是加分项。第三转发和重定向的区别转发是服务端内部跳转地址栏不变、request 数据不丢重定向是浏览器二次请求地址栏变化适合写操作后防重复提交——广告墙的发布成功用重定向、详情页内部用转发两个例子各占一边。第四Session 与 Cookie 的区别Session 存服务端Cookie 存客户端广告墙登录态用 session.setAttribute 存 userIdCookie 里只留 sessionId再配合 WebFilter 拦截未登录访问。提示如果想再进一步可以给广告墙加登录 Filter用WebFilter(/ad/*)在 chain.doFilter 前校验 session。Filter 的执行顺序与放行逻辑本身就是 Java 学习路线里从「会写接口」走向「会做系统」的临门一脚顺着 Filter 的链式调用去对比 java 动态代理的 InvocationHandler 委派模型会把这两块知识一次串起来。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门