每年到了计算机毕设选题季,"基于 JSP 的女性交流网站"这类题目总会被反复翻出来:题目看着不复杂,功能听起来也熟悉,可真拿到一份源码之后,很多人卡在"跑不起来"和"讲不清楚"这两件事上。我前后帮人看过几十份类似的 JSP 毕设源码,从最简单的学生信息管理到这类垂直社区型网站,问题几乎一模一样——环境对不上、乱码排查不了、代码分层混乱、答辩时说不清哪部分是自己写的。这篇就把这类项目从头到尾拆一遍,讲讲一个能跑、能改、能讲明白的 JSP 交流社区应该长什么样,哪些地方是必须自己动手补的,哪些坑是所有 JSP 项目都会踩的。
1. 先搞清楚这个"交流网站"到底要做成什么
1.1 从"交流网站"三个字倒推功能边界
"交流网站"是个很宽的说法,它可以是论坛、可以是即时聊天、可以是动态广场,甚至可以是一个带评论区的文章站。放到毕设的场景里,最稳妥、最容易落地、老师最容易验收的形态是板块化的论坛社区:用户注册后进入不同话题板块,在板块下发帖,其他人在帖子下回复。这个形态的好处是功能闭环清晰,数据表关系明确,答辩时画一张 ER 图就能把整个系统讲完。
以我见过的这类项目为例,常见的板块划分会围绕垂直兴趣展开,比如时尚穿搭、美妆护肤、亲子育儿、美食分享、读书影音、职场经验、生活闲聊这几类。这些板块本身不涉及任何复杂的业务逻辑,用户在不同板块里发帖、回帖、点赞、收藏,行为路径高度统一,代码复用率非常高——一个 PostServlet 加上一个 boardId 参数,就能把七个板块的帖子列表全渲染出来。
核心功能清单大致是这样几块:用户侧的注册登录、浏览板块、查看帖子详情、发帖、回帖、点赞收藏、个人中心(改资料、改密码、看自己的帖子);管理侧的用户管理、板块管理、帖子审核与置顶加精、回复删除、数据统计。这些功能凑齐,工作量足够撑起一份毕设,又不会多到写不完。
需要提醒的是,别一上来就想着加私信、加关注、加好友系统。"交流"这两个字最容易让人发散,但私信功能意味着要有消息表、已读未读状态、会话列表、轮询或长连接,代码量会直接翻倍,而且和论坛主体功能耦合度很低,答辩时也讲不出什么深度。功能做深一个闭环,比铺开五个半成品强得多。
1.2 三种角色的权限划分决定了代码结构
这个项目里其实有三个身份:游客、注册用户、管理员。有些人还会加一个"版主",但在毕设规模下,版主和管理员合并成一个角色就够了,权限表里多一个 level 字段的事,没必要拆成两套。
游客能看帖子列表和详情,但不能发帖回帖;注册用户可以发帖、回帖、点赞、收藏、管理自己的内容;管理员可以进后台,删除任何帖子、封禁用户、管理板块。这三种权限的差异,直接决定了你需要在项目里写几个过滤器(Filter)。
我的习惯是写三个过滤器:EncodingFilter全局统一字符编码,LoginFilter拦截需要登录的路径(发帖、回帖、个人中心),AdminFilter拦截/admin/*下的所有请求。过滤器链的顺序不能乱,编码过滤器必须排在最前面,否则后面读参数的时候编码还没设好,中文就已经变成乱码了。这个顺序问题在web.xml里靠<filter-mapping>的声明先后决定,用注解@WebFilter的话顺序是不确定的——这点特别容易踩,用注解配过滤器的项目经常出现"有时候乱码有时候不乱码"的玄学现象,根源就在这里。
1.3 毕设验收看的是完整度,不是技术新潮度
有个很现实的问题:现在用 JSP 做毕设,会不会被老师嫌弃技术太老?我的观察是,绝大多数情况下不会。老师看的是你的系统能不能跑通、功能是否完整、代码是否有分层、数据库设计是否规范、答辩时能不能把原理讲清楚。用 Spring Boot 写一个只有增删改查的系统,和用 JSP 写一个功能完整的社区,后者往往分数更高,因为 JSP 的请求响应链路更透明,你对 HTTP、Session、Servlet 生命周期的理解反而更容易被问出来。
真正的减分项是这些:数据库只有一张表、所有 SQL 拼在 JSP 页面里、密码明文存储、没有任何异常处理、所有代码堆在一个包下面。这些和用不用 JSP 没关系,纯粹是工程习惯问题。
2. JSP 技术栈选型背后的真实理由
2.1 为什么是 JSP + Servlet + JDBC 这套老组合
先把这套技术栈说清楚:**JSP 负责视图渲染,Servlet 负责接收请求和流程控制,JavaBean 负责承载数据,JDBC 负责和 MySQL 打交道。**这就是最经典的 Model 1 / Model 2 结构,本项目应该走的是 Model 2,也就是常说的 MVC 简化版。
为什么不用 Spring MVC?因为毕设的核心考核点之一是"你理解 Web 请求是怎么走完一圈的"。JSP + Servlet 这套东西,你能亲眼看到请求从浏览器发出,被哪个 Servlet 接住,怎么查数据库,怎么request.setAttribute,又怎么在 JSP 上用 EL 表达式取出来。这条链路每一环都裸露在外面,出问题的时候排查路径非常清晰。而 Spring MVC 的 DispatcherServlet 把这一层全封装了,写起来爽,但被问到"请求是怎么映射到你这个方法的",很多人答不上来。
具体到代码层面,典型的 Servlet 长这样:
@WebServlet("/post/list") public class PostListServlet extends HttpServlet { private PostService postService = new PostServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String boardIdStr = req.getParameter("boardId"); String pageStr = req.getParameter("page"); int boardId = boardIdStr == null ? 0 : Integer.parseInt(boardIdStr); int page = pageStr == null ? 1 : Integer.parseInt(pageStr); PageBean<Post> pageBean = postService.queryByBoard(boardId, page, 12); req.setAttribute("pageBean", pageBean); req.setAttribute("boardId", boardId); req.getRequestDispatcher("/WEB-INF/jsp/post/list.jsp").forward(req, resp); } }这段代码里有几个细节值得说。第一,JSP 页面放在WEB-INF/jsp/下面,这样浏览器没法直接访问到 JSP 文件,必须经过 Servlet 转发,安全性更好。第二,forward而不是sendRedirect,因为要带数据过去,重定向会丢request域里的东西。第三,分页参数做了空值判断,避免Integer.parseInt(null)直接抛异常——这个异常在没做全局异常处理的项目里,页面会直接白屏 500,答辩演示的时候非常致命。
2.2 环境版本对不上,是跑不起来的第一大原因
拿到一份 JSP 源码,第一次运行失败,八成是环境问题。我整理过一份对照表,照着核对基本能排掉大部分启动故障:
| 组件 | 推荐版本 | 常见踩坑点 |
|---|---|---|
| JDK | 8 或 11 | 用 17 跑老项目,javax.servlet包找不到,因为高版本 JDK 配套的是jakarta.servlet |
| Tomcat | 9.x | Tomcat 10 之后包名从javax.*改成jakarta.*,老代码全部编译不过 |
| MySQL | 5.7 或 8.0 | 8.0 要换驱动包mysql-connector-java8.x,且 URL 必须加时区参数 |
| IDEA | 2021 以上 | 社区版没有 Tomcat 集成,必须用 Ultimate 或者在 IDEA 里手动配 Tomcat Server |
| 连接池 | Druid 1.2.x | 版本太老和新版 MySQL 驱动不兼容,会出现连接超时 |
特别说一下 MySQL 8.0 的连接 URL,很多人从网上抄的模板没带时区,启动就报The server time zone value 'xxx' is unrecognized:
jdbc.url=jdbc:mysql://localhost:3306/women_community?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueallowPublicKeyRetrieval=true这个参数在 MySQL 8 用caching_sha2_password认证插件时是必须的,不加会和连接池报权限错误,报错信息看起来像密码错了,其实不是。
2.3 数据表设计:六张表撑起整个社区
数据库设计是毕设里权重很高的部分,老师一定会看你的表结构和字段类型。这个项目的核心表我建议这样设计:
t_user用户表,字段包括 id、username、password(存加密后的)、salt(盐值)、nickname、avatar、gender、email、intro、role、status、create_time、last_login_time。注意 role 用 tinyint 存 0/1,0 是普通用户,1 是管理员;status 用 0/1 表示正常/禁用,这样封禁用户只需要改一个字段。
t_board板块表,字段有 id、name、intro、icon、sort_order、status、create_time。sort_order 用来控制板块在首页的排列顺序,改个数字就能调整,不用改代码。
t_post帖子表,字段有 id、board_id、user_id、title、content、cover、view_count、reply_count、like_count、is_top、is_essence、status、create_time、update_time。其中reply_count和like_count是冗余字段,属于反范式设计——每次有人回复就update t_post set reply_count = reply_count + 1。为什么要冗余?因为帖子列表页要显示回复数,如果不冗余就得对每条帖子做一次count(*)子查询,列表页十二条帖子就是十二次聚合查询,性能差很多。这是典型的用写入换读取的思路,答辩时能讲出来是个很好的加分点。
t_reply回复表,字段有 id、post_id、user_id、content、parent_id、reply_to_user_id、like_count、status、create_time。parent_id 支持二级回复(回复某条回复),reply_to_user_id 记录被回复的人,方便做"回复 @某某"的效果。
t_collect收藏表,只有 id、user_id、post_id、create_time 四个字段,但要给(user_id, post_id)加联合唯一索引,防止重复收藏。
t_notice通知表,字段有 id、user_id(接收者)、from_user_id、type(1 回复 2 点赞 3 系统)、content、target_id、is_read、create_time。
这里有个细节容易被忽略:所有存文本的表字段,字符集要用utf8mb4而不是utf8。MySQL 里的utf8其实是阉割版,最多三个字节,存不了 emoji 和一些生僻字。用户发帖带个表情,插入直接报错或者变成问号,这个问题在我看过的项目里出现频率极高。
CREATE TABLE `t_post` ( `id` int(11) NOT NULL AUTO_INCREMENT, `board_id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `title` varchar(120) NOT NULL, `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, `view_count` int(11) DEFAULT 0, `reply_count` int(11) DEFAULT 0, `is_top` tinyint(1) DEFAULT 0, `status` tinyint(1) DEFAULT 1, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_board_time` (`board_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;idx_board_time这个联合索引是有讲究的:板块帖子列表的查询条件是where board_id = ? and status = 1 order by is_top desc, create_time desc,按 board_id 和 create_time 建联合索引,能直接走索引排序,避免 filesort。
2.4 字符编码要贯穿整条链路才有用
乱码是 JSP 项目的经典问题,但很多人只知道在页面顶部加一句pageEncoding="UTF-8",结果发现还是乱码。原因在于编码设置必须是全链路的,任何一环漏掉都会前功尽弃。完整清单是这样的:
第一环,JSP 页面声明:<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>。
第二环,请求参数编码:POST 请求要在读取任何参数之前调用request.setCharacterEncoding("UTF-8"),这一句写在 EncodingFilter 里最合适。GET 请求的参数编码由 Tomcat 决定,Tomcat 8 以后URIEncoding默认就是 UTF-8,所以一般不用管;如果是老版本 Tomcat,需要在server.xml的 Connector 上手动加URIEncoding="UTF-8"。
第三环,响应编码:response.setContentType("text/html;charset=UTF-8")。
第四环,数据库连接:URL 里的characterEncoding=utf8。
第五环,数据库和表的字符集:utf8mb4。
第六环,IDEA 的文件编码设置:Settings → Editor → File Encodings,三个下拉全部选 UTF-8,并且勾上Transparent native-to-ascii conversion。这一环最阴——IDEA 默认可能是 GBK,你的代码在 IDE 里看着正常,编译出来的 class 文件里中文已经乱了,运行时怎么改都改不好。
排查乱码的正确姿势是从后往前推:先确认数据库里存进去的是不是乱码,如果不是,说明问题在读取和展示环节;如果数据库里就是乱的,那问题在写入环节。用这条链路二分定位,比盲目改配置快得多。
3. 核心模块的落地:从注册登录到发帖回帖
3.1 注册登录里那些必须自己补的安全细节
几乎所有开源的 JSP 毕设源码,登录模块都有同一个毛病:密码明文存数据库。这个在答辩时被问到会非常尴尬。正确的做法是加盐哈希,简单点用 MD5 + 随机盐,规范点用 BCrypt。
加盐的逻辑是:用户注册时生成一个随机字符串当 salt,把password + salt一起做哈希,数据库里存哈希值和 salt。登录时取出 salt,用同样的方式算一遍,比对哈希值。这样即使数据库泄露,攻击者也没法用彩虹表反查。
public static String md5WithSalt(String password, String salt) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest((password + salt).getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("哈希算法不可用", e); } }如果预算允许,直接用 jBCrypt 库,一行BCrypt.hashpw(password, BCrypt.gensalt())搞定,盐值自动包含在结果里,连 salt 字段都省了。
另一个必须做的是防 SQL 注入。用PreparedStatement加占位符,别用字符串拼接。这个不只是安全要求,也是老师爱问的点:
// 错误做法,username 传 ' or '1'='1 就能绕过登录 String sql = "select * from t_user where username='" + username + "' and password='" + pwd + "'"; // 正确做法 String sql = "select * from t_user where username = ? and password = ?"; ps.setString(1, username); ps.setString(2, md5WithSalt(pwd, salt));还有会话保持。登录成功后把用户对象塞进 Session:session.setAttribute("loginUser", user)。这里有个坑,Session 里存的对象必须实现 Serializable 接口,因为 Tomcat 在重启或者做会话持久化的时候会序列化 Session 内容,不实现的话会报NotSerializableException,而且这个错往往在项目跑了一段时间、Tomcat 重启时才出现,很难联想到是实体类的问题。保险起见,所有 Entity 类都加上implements Serializable。
3.2 分页查询:列表页的性能命门
社区类网站最频繁的操作就是翻列表,分页写得好不好直接决定页面打开速度。分页有两个必要动作:先查总数,再查当前页数据。
public PageBean<Post> queryByBoard(int boardId, int page, int pageSize) { PageBean<Post> pb = new PageBean<>(); pb.setPage(page); pb.setPageSize(pageSize); String countSql = "select count(*) from t_post where board_id = ? and status = 1"; int total = queryRunner.count(countSql, boardId); pb.setTotal(total); pb.setTotalPage((total + pageSize - 1) / pageSize); String listSql = "select p.*, u.nickname, u.avatar from t_post p " + "left join t_user u on p.user_id = u.id " + "where p.board_id = ? and p.status = 1 " + "order by p.is_top desc, p.create_time desc limit ?, ?"; List<Post> list = queryRunner.query(listSql, new BeanListHandler<>(Post.class), boardId, (page - 1) * pageSize, pageSize); pb.setList(list); return pb; }(total + pageSize - 1) / pageSize这个上取整公式要记牢,直接用total / pageSize会在除不尽的时候少一页,最后一页的数据永远显示不出来。这个 bug 很隐蔽,测试数据少的时候(比如总共 5 条,每页 10 条)根本发现不了。
列表查询用left join一次性把发帖人的昵称和头像带出来,而不是循环里去查用户表——循环里查数据库就是经典的 N+1 问题,十二条帖子十三次查询,数据量大了页面会明显卡顿。
limit ?, ?的深分页问题在毕设规模下可以不管,但如果老师问到,你可以答:数据量超过几十万行时,limit 100000, 12会扫描前十万行再丢弃,效率很低,可以改成基于主键或者时间的游标分页,也就是记录上一页最后一条的 id,下次查where id < lastId limit 12。这个答案能明显拉开和其他同学的区别。
3.3 发帖回帖:内容过滤与 XSS 防护
发帖回帖看着简单,但有两个必须处理的问题:XSS 和内容长度校验。
XSS 是指用户在帖子内容里写一段<script>标签,其他用户打开这条帖子时脚本就会执行,轻则弹窗骚扰,重则窃取 Cookie。处理方式是在输出的时候转义 HTML 特殊字符:
public static String escapeHtml(String input) { if (input == null) return ""; return input.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); }注意顺序,&一定要第一个替换,否则会把后面刚生成的<里的&又转一次,变成&lt;,用户看到的就是一堆乱码实体。
在 JSP 里输出的时候,如果你用的是 EL 表达式${post.content},默认是不转义的,需要加<c:out value="${post.content}" escapeXml="true"/>,或者用${fn:escapeXml(post.content)}。这一点很多人不知道,以为 EL 表达式自带转义,其实没有。
内容校验方面,标题长度建议限制在 120 字符以内,正文限制在 5000 字以内,前后端都要校验。前端用maxlength属性,后端在 Servlet 里也要再判一次——前端校验是给正常用户体验用的,后端校验是防止有人直接调接口绕过页面。这个"双重校验"的思路答辩时可以主动提,属于加分项。
至于敏感词过滤,毕设层面做一个简单的词库匹配就够了:把敏感词放进一个文本文件或者数据库表,用户提交时做一次包含判断,命中就拒绝提交并提示。不需要上什么复杂的算法,说明白"这里只是基础过滤,实际生产环境会接入专业的内容安全服务"就够了。
3.4 个人中心与后台管理的最小可用设计
个人中心不需要做得很花哨,四个功能就够:查看和修改个人资料(昵称、头像、简介)、修改密码、我发布的帖子列表、我收藏的帖子列表。头像上传要注意三点:限制文件类型(只允许 jpg、png、gif)、限制文件大小(比如 2MB)、重命名文件避免同名覆盖。文件名用UUID.randomUUID() + 后缀生成,这样即使两个用户传了同名文件也不会冲突。
String originalName = file.getName(); String suffix = originalName.substring(originalName.lastIndexOf(".")).toLowerCase(); List<String> allow = Arrays.asList(".jpg", ".jpeg", ".png", ".gif"); if (!allow.contains(suffix)) { resp.getWriter().write("{\"code\":0,\"msg\":\"只支持图片格式\"}"); return; } String newName = UUID.randomUUID().toString().replace("-", "") + suffix;这里千万别用originalName.endsWith(".jpg")来判断,因为用户可以把shell.jsp改名成shell.jsp.jpg,虽然这种攻击在毕设场景下不太现实,但养成用后缀名白名单的习惯是好的。
后台管理做四块:用户列表(搜索、封禁/解封)、帖子列表(搜索、删除、置顶、加精)、板块管理(增删改、排序)、数据看板(用户总数、帖子总数、今日新增)。数据看板用几个count查询就能出来,但视觉效果很好,答辩演示时放在第一屏很抓眼球。
4. 部署调试阶段最容易卡住的地方
4.1 Tomcat 部署后 JSP 编译出来的 Java 类到底在哪
这个问题在热搜里出现过,说明困扰了很多人。JSP 在第一次被访问时会被翻译成 Java 文件,再编译成 class 文件,这些文件放在 Tomcat 工作目录下的work文件夹里。
如果在 IDEA 里用 Tomcat 插件跑项目,IDEA 会为每个运行配置单独创建一个 CATALINA_BASE 目录,路径大致是:
- Windows:
C:\Users\你的用户名\AppData\Local\JetBrains\IntelliJIdea2023.2\tomcat\<运行配置名>\work\Catalina\localhost\<应用上下文>\org\apache\jsp\ - macOS:
~/Library/Caches/JetBrains/IntelliJIdea2023.2/tomcat/<运行配置名>/work/Catalina/localhost/<应用上下文>/org/apache/jsp/
进去之后你会看到index_jsp.java、list_jsp.java这类文件。看这个文件是排查 JSP 报错最快的方式——JSP 页面报 500 错误时,浏览器上给的错误行号是 Java 文件的行号,不是 JSP 的行号,两边的行号完全对不上。打开对应的_jsp.java,按错误行号定位,你就能看到 JSP 到底被翻译成了什么代码,问题一目了然。
我遇到过最典型的一次:JSP 里写了个自定义标签但没引入 taglib,浏览器报错停在_jspService方法的某一行,光看 JSP 完全找不到问题,打开翻译后的 Java 文件才发现那一行是_jspx_th_c_002dout_005f0之类的内部变量,一看就知道是 JSTL 标签的问题。
顺带说,修改 JSP 之后一般不需要重启 Tomcat,Tomcat 会检测到文件变更自动重新编译。但如果改了web.xml、加了新的 Servlet 或者换了 jar 包,那就必须重启,因为容器启动时已经把这些元信息加载进内存了,不重启不生效。这个区别要清楚,很多人改完 web.xml 发现没效果,以为代码写错了,其实就是没重启。
4.2 静态资源 404 的三种典型情况
页面能打开但样式全丢,通常有三种原因。
第一种,路径写的是相对路径。比如 JSP 里写<link href="css/style.css">,在/post/list这样的多层路径下,浏览器会去/post/css/style.css找,自然找不到。正确做法是用绝对路径:
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/style.css">${pageContext.request.contextPath}会取到应用的上下文路径,比如/women_community,这样不管页面在哪一层,路径都是对的。
第二种,DispatcherServlet 或者自定义的全局 Servlet 拦截了所有请求,包括静态资源。如果你的过滤器的 urlPattern 配成了/*,并且对静态资源也做了登录判断,那 CSS 请求会被拦下来重定向到登录页,浏览器拿到的是一段 HTML 而不是 CSS,控制台会报 MIME 类型错误。解决办法是在过滤器里放行静态资源:
String uri = req.getRequestURI(); if (uri.contains("/static/") || uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".png") || uri.endsWith(".jpg")) { chain.doFilter(req, resp); return; }第三种,资源放在WEB-INF下面。WEB-INF里的东西浏览器一律访问不到,这是 Servlet 规范规定的。静态资源要么放webapp/static/,要么放webapp/assets/,别放WEB-INF里。
4.3 数据库连接写死在代码里的隐患
很多源码把 JDBC 的 URL、用户名、密码直接写在一个DBUtil类的静态常量里。这样做能跑,但有两个问题:一是换个环境就得改代码重新编译,二是密码硬编码在代码里是不好的习惯。
更规范一点的做法是把配置抽到db.properties文件里,放在src/main/resources下,用类加载器读取:
public class DBUtil { private static DruidDataSource dataSource; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); dataSource = new DruidDataSource(); dataSource.setUrl(props.getProperty("jdbc.url")); dataSource.setUsername(props.getProperty("jdbc.username")); dataSource.setPassword(props.getProperty("jdbc.password")); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置加载失败: " + e.getMessage()); } } public static DataSource getDataSource() { return dataSource; } }用连接池而不是每次DriverManager.getConnection(),是因为建立数据库连接的开销很大,一次请求建一次连接,并发一上来就撑不住。Druid 的initialSize和maxActive分别控制初始连接数和最大连接数,毕设场景设成 5 和 20 完全够用。如果老师在答辩时问"为什么要用连接池",你可以从"连接是稀缺资源、创建销毁成本高、池化复用"这个角度回答。
另外提醒一句,getResourceAsStream读文件时,如果打成 war 包部署,文件其实在WEB-INF/classes里,用相对路径的文件流是读不到的,必须用类加载器的方式。这个问题在 IDEA 里跑完全正常,一打成 war 部署到独立 Tomcat 就报"配置文件找不到",非常容易让人崩溃。
5. 读懂源码结构,才谈得上二次开发
5.1 一套规范的分层目录长什么样
拿到源码第一件事不是急着跑,是先看目录结构。规范的 JSP 项目应该长这样:
src/main/java/com/community/ ├── entity/ 实体类,和数据库表一一对应 ├── dao/ 数据访问层接口 │ └── impl/ 实现类,写 SQL ├── service/ 业务逻辑层接口 │ └── impl/ 实现类,事务控制、业务组合 ├── servlet/ 控制层,接收请求、调用 service、转发页面 ├── filter/ 过滤器 ├── listener/ 监听器 └── util/ 工具类,DBUtil、MD5Util、PageBean src/main/resources/ └── db.properties 数据库配置 src/main/webapp/ ├── static/ css、js、图片 ├── admin/ 后台页面(JSP) └── WEB-INF/ ├── jsp/ 前台页面(JSP) └── web.xml看到这个结构,你就能判断出这份源码的质量。如果所有 Java 文件都堆在一个包里、JSP 里直接写Class.forName和DriverManager,那就是典型的"能跑但没法改",二次开发的成本会很高。
分层之后,各层的职责必须清晰:DAO 层只做数据库操作,不做业务判断;Service 层组合多个 DAO 完成一个业务动作,比如"发帖"这个动作要同时插入帖子记录、更新板块帖子数、给关注的人发通知,这三步应该在一个 Service 方法里;Servlet 层只负责解析参数、调用 Service、决定跳转到哪个页面。判断一个项目的分层是否合格,就看它的 Service 层是不是空的——如果 Service 方法里只有一句return dao.xxx(),那分层就是形式主义。
5.2 想改成别的主题,哪些地方必须动
这类项目的可复用性其实很高,把"女性交流网站"改成"校园二手交易论坛"或者"读书分享社区",改动量并不大,主要是这几处:
数据库层面,改板块表t_board的初始数据,把美妆护肤、穿搭之类换成对应的主题板块。如果要做成二手交易,t_post表要加price、trade_status字段。
页面层面,改站点名称、Logo、首页轮播图、页脚版权信息。这些通常在common/header.jsp和common/footer.jsp两个公共文件里,改两处全站生效,这也是为什么要把页头页尾抽成单独文件用<jsp:include>引入。
文案层面,把所有出现"她""姐妹们"之类的称呼替换掉,保留中性表达。
功能层面,二手交易要加"商品状态(在售/已售)",读书分享要加"书籍信息(书名、作者、ISBN)"。这些属于表结构的小幅扩展,在原有 DAO 上加两个字段就能搞定。
不建议动的是权限体系、分页逻辑、过滤器链这些基础设施。这些代码牵一发动全身,改坏了很难恢复。二次开发的原则是"加功能,不改骨架"。
5.3 答辩时怎么把"我做了什么"说清楚
最后说点实在的。答辩老师手里通常有一份你的任务书,他最关心的是"这个系统是不是你自己理解的"。所以介绍的时候不要念功能清单,要讲清楚三个问题:数据怎么流动的、关键问题怎么解决的、遇到困难怎么排查的。
第一个问题的答法:用户点击发帖 → 请求到 PostAddServlet → 参数校验 → 调用 PostService.add() → 插入 t_post 表并更新冗余计数 → 重定向到帖子详情页。这条链路说得出来,说明你理解 MVC。
第二个问题的答法:举例说密码怎么存的、SQL 注入怎么防的、分页的上取整为什么那么写、冗余字段为什么要加。挑两三个讲透就够了。
第三个问题的答法:讲一次真实的排查经历,比如"页面报 500 但 JSP 行号对不上,后来去 Tomcat 的 work 目录里找到翻译后的 Java 文件,按错误行号定位,发现是 JSTL 标签没引 taglib"。这种细节一说,老师就知道你是真动手跑过的。
我个人的体会是,毕设这件事最怕的不是技术难,而是"抄了源码但没跑通"。跑通一遍、改坏一次、再修好,这个过程中你对 JSP、Servlet、Session、连接池、字符编码这些概念的理解会扎实很多。我带过的一个同学,为了搞明白乱码问题,把编码链路从浏览器一直查到 MySQL 的字符集变量,最后在答辩时被问到"你项目里遇到最大的困难是什么",他讲了这一段,老师当场就给了高分。技术上没有白走的路,那些让你抓狂的报错,往往就是答辩时最值钱的谈资。