资讯动态

JSP+MySQL课程设计实战:个人与家乡展示平台开发全解析

发布时间:2026/9/16 2:58:36 来源:尧图企业网站定制
简介面向Java Web课程设计场景这套基于JSP与MySQL实现的个人与家乡展示管理平台完整覆盖前台与后台。前台包括首页照片轮播、个人简介、技能分类展示、家乡简介、特色介绍以及游客留言板后台则提供用户管理、技能管理、家乡特色管理和留言管理其中技能管理支持按科研、成绩、开发技能、获奖等分类进行增删改查。项目采用经典MVC分层思路JSP负责视图、Java类处理业务逻辑并通过JDBC与MySQL交互适合高校学生巩固Java Web核心技术也可直接作为课程设计或毕业设计的项目基底。资源共2000个文件以JSP页面、Java源码、JavaScript交互脚本及SVG矢量图标为主同时包含SCSS/LESS样式、SQL初始化脚本、YML配置文件等压缩包约29.69MB目录结构完整。管理端内置SB Admin 2后台模板和FontAwesome图标库前台的轮播、留言回复、下拉菜单分类等功能均有对应代码实现。已有287人浏览学习值得正在完成Java Web大作业或想系统梳理SSM之前基础知识的开发者参考。1. 不要小看 JSP 课程设计个人与家乡展示管理平台的真实分量如果你以为“个人与家乡展示”只是一个静态网页作业那就低估了这份 Java Web 课程设计的覆盖范围。它虽然挂着 JSP MySQL 的名头实际拆开来看包含至少四套独立的模块角色权限管理员/游客、带分类下拉的技能管理、图文混合的家乡特色介绍、以及支持回复的留言板。换句话说前端展示只是外表后端增删改查、会话状态、文件上传、外键关联才是这门课真正要考察的东西。我在带学生做类似选题时经常说把这份题做完等于把 JavaWeb 第一轮面试里关于 JSP 生命周期、Session 失效、JDBC 预编译、中文乱码的常见坑全部踩了一遍。适合的人群很明确正在做 Java Web 课程设计的学生、准备毕业设计但想先练手 JSP 的开发者以及想从“会写 Servlet”跨到“能组织一个完整小项目”的初级工程师。这篇文我会按我拆项目的习惯来写先立数据库和权限再做技能管理和家乡特色最后处理留言和模板整合。每个环节给的代码都能直接跑参数怎么调、报错看哪里我会一并写清楚。2. 数据库先行四张表怎么设计才不算白建2.1 用户角色与技能分类的本质是“父表-子表”关系这个项目里最容易翻车的地方是把“技能管理”做成一张大宽表。比如把科研情况、学习成绩、开发技能、获奖记录各设计成字段这样后续想加一个“项目经历”分类就要改表结构。正确做法是拆成skill_category分类表和skill_item技能明细表。前者存下拉菜单的选项后者通过category_id外键关联。游客和管理员都挂在user表里用role字段区分而不是建两张表。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0-游客 1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE skill_category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE skill_item ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL, title VARCHAR(100) NOT NULL, content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), CONSTRAINT fk_skill_category FOREIGN KEY (category_id) REFERENCES skill_category(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE message_board ( id INT NOT NULL AUTO_INCREMENT, user_id INT DEFAULT NULL, content TEXT NOT NULL, reply_content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这四张表对应的逻辑说明如下user.role用TINYINT而不是字符串节省空间且查询更快0 表示游客注册用户1 表示管理员后续拦截器判断session.getAttribute(role)等于 1 才有管理权限。skill_item.content用TEXT而不是VARCHAR因为获奖描述、科研情况往往超过 255 字符。message_board.user_id允许为空DEFAULT NULL这样不登录也能留言符合题目里“游客可以留言”的要求。主键统一用自增INT外键加索引否则后续做JOIN查询时数据量稍微上来就会变慢。如果你用的是 MySQL 5.7 及以上utf8mb4是必须的utf8在存储 emoji 符号或某些生僻字时会报Incorrect string value错误这在 JSP 项目里比乱码更隐蔽经常会误判成编码过滤器没写好。2.2 JDBC 连接池参数怎么调才能扛住课程答辩时的并发演示课程设计阶段很少有人关心连接池但答辩现场最常见的场景是老师打开你的系统同时刷新首页、留言板、技能管理三个页面Tomcat 默认的 JDBC 连接直接爆掉报Too many connections。我在自己的项目里统一用 HikariCP 替代传统的DriverManager.getConnection()因为课程设计的体量根本不需要 MyBatis一个轻量连接池足以撑住几十人同时访问。// HikariCP 配置文件 hikari.properties jdbcUrljdbc:mysql://localhost:3306/hometown_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai usernameroot password123456 driverClassNamecom.mysql.cj.jdbc.Driver maximumPoolSize10 minimumIdle2 connectionTimeout30000 idleTimeout600000参数说明maximumPoolSize10是核心课程设计并发量通常不超过 20给 10 足够开太多反而增加 MySQL 端的线程切换开销而且答辩机器的内存可能只有 8G。connectionTimeout30000表示 30 秒内拿不到连接就抛异常避免页面一直转圈。serverTimezoneAsia/Shanghai是必须的MySQL 8.x 驱动不带时区参数会直接报The server time zone value Öйú±ê׼ʱ¼ä。获取连接时不要自己写Class.forName()交给 HikariCP 的HikariDataSource自动处理。每个 DAO 类里通过一个静态工厂拿到DataSource实例保证全项目只有一个连接池。public class DbUtil { private static HikariDataSource dataSource; static { try (InputStream input DbUtil.class.getClassLoader() .getResourceAsStream(hikari.properties)) { Properties props new Properties(); props.load(input); HikariConfig config new HikariConfig(props); dataSource new HikariDataSource(config); } catch (IOException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这段代码的逻辑是静态代码块在类第一次加载时执行读取hikari.properties并初始化连接池。getConnection()每次返回的是池里的代理连接close()调用的是回收方法而非真正关闭所以 DAO 里用完直接connection.close()不会有性能问题。注意不要在每个 Servlet 里重复 new 数据源那会导致连接池数量翻倍最后 MySQL 端max_connections被打满。2.3 注册接口的密码处理先存 MD5 还是先加盐课程设计里密码通常直接明文存但既然要写进简历我建议做一层盐值哈希。这不需要引入 Spring Security用 JDK 自带的MessageDigest就能完成。注册时生成随机盐把盐和密码拼接后做 SHA-256数据库里同时存盐和哈希值。public static String[] hashPassword(String rawPassword) throws NoSuchAlgorithmException { byte[] salt new byte[16]; new SecureRandom().nextBytes(salt); String saltHex HexFormat.of().formatHex(salt); MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hash digest.digest((saltHex rawPassword).getBytes(StandardCharsets.UTF_8)); String hashHex HexFormat.of().formatHex(hash); return new String[]{saltHex, hashHex}; } public static boolean verifyPassword(String rawPassword, String saltHex, String hashHex) throws NoSuchAlgorithmException { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hash digest.digest((saltHex rawPassword).getBytes(StandardCharsets.UTF_8)); return MessageDigest.isEqual(hash, HexFormat.of().parseHex(hashHex)); }这里的HexFormat是 Java 17 才有的如果你还在用 Java 8需要换成BigInteger或String.format(%02x)。我一般建议课设环境直接用 JDK 17因为 Tomcat 10 之后要求 Servlet 5.0javax.servlet改成jakarta.servlet这个变化对课设影响很大后面会专门说。登录校验时用MessageDigest.isEqual()而不是hash.equals()前者的比较是恒定时间的可以防时序攻击虽然是课设但写出这个细节会被面试官另眼相看。如果你只是想让作业能跑跳过加盐直接存MD5也行但答辩时大概率会被问“MD5 有什么缺陷”到时候答不上来反而尴尬。3. 技能管理的下拉菜单联动从表结构到 JSP 页面的完整闭环3.1 Servlet 层要不要抽象 BaseServlet看这几个条件技能管理涉及分类的增删改查和技能项的增删改查如果每个功能写一个 Servlet会得到AddSkillServlet、DeleteSkillServlet、UpdateSkillServlet等七八个类。我建议抽一个BaseServlet利用反射根据请求参数action分发到对应方法。但要注意这个方案有一个前提你的 Java 版本必须支持方法反射且方法参数统一为HttpServletRequest和HttpServletResponse。WebServlet(/admin/skill) public class SkillServlet extends BaseServlet { private SkillService skillService new SkillService(); public void list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListSkillCategory categories skillService.listCategoriesWithItems(); request.setAttribute(categories, categories); request.getRequestDispatcher(/WEB-INF/admin/skill-list.jsp).forward(request, response); } public void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String categoryId request.getParameter(categoryId); String title request.getParameter(title); String content request.getParameter(content); skillService.addItem(Integer.parseInt(categoryId), title, content); response.sendRedirect(request.getContextPath() /admin/skill?actionlist); } }BaseServlet的核心是service()方法里根据request.getParameter(action)找到同名方法并调用。这样做的收益是日志好打、权限好统一加缺点是 IDE 里不能像还有 Spring MVC 那样直接看路由映射。如果你觉得反射不好理解那就老老实实写两个 ServletSkillCategoryServlet和SkillItemServlet。课设评分不会因为类多扣分但代码冗余一定会被老师翻到的时候问一句“为什么不合并”。3.2 DAO 层模拟 “关联查询的结果为什么出现了 N1”技能列表页通常会这么展示左侧是分类下拉菜单右侧是当前分类下的技能卡片。如果查询时先加载所有分类再遍历分类查每个分类下的技能就会产生 N1 条 SQL。我一般用一条LEFT JOIN查全部然后在 Java 里手动分组。public ListSkillCategory listCategoriesWithItems() throws SQLException { String sql SELECT c.id as cid, c.name, c.sort_order, i.id as iid, i.title, i.content FROM skill_category c LEFT JOIN skill_item i ON c.id i.category_id ORDER BY c.sort_order, i.create_time DESC; MapInteger, SkillCategory map new LinkedHashMap(); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { SkillCategory category map.computeIfAbsent(rs.getInt(cid), k - new SkillCategory(rs.getInt(cid), rs.getString(name))); if (rs.getObject(iid) ! null) { category.getItems().add(new SkillItem( rs.getInt(iid), rs.getString(title), rs.getString(content))); } } } return new ArrayList(map.values()); }这段代码的关键在于map.computeIfAbsent避免了重复创建分类对象。LEFT JOIN保证没有技能的分类也能显示出来rs.getObject(iid)判断对应位置是否为 NULL因为getInt在 NULL 时会返回 0这会错误地加入一条 title 为空的记录。LinkedHashMap保持分类的插入顺序否则HashMap会让排序字段失效。面试时若被问到 N1 问题你可以说这个场景里一次查询就够但如果是分页列表加详情查询就需要考虑用MyBatis的嵌套结果映射或干脆做冗余缓存。课设阶段写清楚这种JOIN就是加分项。3.3 下拉菜单的默认选中与编辑回显前端最常见的问题技能管理的表单页需要两个关键交互新增时选择分类编辑时回显原有分类。很多人直接在 JSP 里用selected属性写死导致编辑时永远选不上原来的值。正确做法是在 Servlet 里把选中的categoryId放到 request 域JSP 中用三元表达式判断。select namecategoryId classform-control option value-- 请选择分类 --/option c:forEach items${categories} varcat option value${cat.id} ${cat.id selectedCategoryId ? selected : } ${cat.name} /option /c:forEach /select这里的selectedCategoryId是 edit 方法里设置的一个Integer属性。注意cat.id selectedCategoryId比较的是字符串刚好id是IntegerJSP EL 会把两边的值都转成数字比较所以即使一个是字符串一个是数字也能正确判断。如果写成selectedCategoryId.equals(cat.id)当selectedCategoryId为空时直接报空指针虽然 JSP 里不会崩但整个表单渲染不出来。保存时后端务必要校验categoryId是否为空因为下拉框可以被浏览器开发者工具篡改。校验代码放在 Servlet 里就行不需要单独写 Validatorif (categoryId null || categoryId.isEmpty() || title null || title.trim().isEmpty()) { request.setAttribute(error, 分类和标题不能为空); request.getRequestDispatcher(/WEB-INF/admin/skill-form.jsp).forward(request, response); return; }response.sendRedirect用的是重定向表单提交后刷新页面不会重复提交forward是转发地址栏不变刷新会再次执行上一次的 POST。课程设计里很多重复插入数据的 BUG 就是forward导致的这里一定要区分开。4. 家乡特色与留言板图片上传、权限控制和回复状态的闭环4.1 图片上传用 Part 接口还是 FileItem取决于 Tomcat 版本家乡特色介绍中的美食、美景配图需要上传图片。传统写法是用 Apache Commons FileUpload需要引入commons-fileupload和commons-io两个 jar。但 Servlet 3.0 已经有原生的Part接口不用额外导包这也是我更推荐的方式。唯一需要注意的是 JSP 表单里必须写enctypemultipart/form-data且 Servlet 类上要加MultipartConfig注解。WebServlet(/admin/feature/save) MultipartConfig(maxFileSize 5 * 1024 * 1024, maxRequestSize 10 * 1024 * 1024) public class FeatureSaveServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part imagePart request.getPart(image); String fileName extractFileName(imagePart); if (fileName ! null !fileName.isEmpty()) { String realPath getServletContext().getRealPath(/uploads); File uploadDir new File(realPath); if (!uploadDir.exists()) uploadDir.mkdirs(); String newName UUID.randomUUID().toString().replace(-, ) fileName.substring(fileName.lastIndexOf(.)); imagePart.write(uploadDir.getAbsolutePath() File.separator newName); } } }MultipartConfig(maxFileSize 5MB)限制单个文件大小超过会抛 IllegalStateException。imagePart.write()的参数是绝对路径不要写相对路径getRealPath(/uploads)得到的是部署后的真实目录重启 Tomcat 后这个目录会被清空因为源码里的uploads文件夹不一定被重新编译进 WAR。更稳妥的做法是把图片存到 Tomcat 外部目录然后在 Nginx 或虚拟主机里映射访问路径但课设阶段放webapp/uploads就够了。4.2 游客留言和管理员回复同表实现还是拆两张表留言板如果只做“游客留言管理员回复”用同一张表加reply_content字段最省事。但如果你希望实现“游客回复游客”那就要把留言拆成主帖和回复两张表。题目要求是管理员回复所以保留message_board单表加一个reply_content字段即可。留言列表的查询要按时间倒序并且把留言人的用户名JOIN出来。因为user_id允许为空不能用 INNER JOIN否则匿名留言会丢了。public ListMessageVO listMessages() throws SQLException { String sql SELECT m.id, m.content, m.reply_content, m.create_time, COALESCE(u.username, 匿名) as username FROM message_board m LEFT JOIN user u ON m.user_id u.id ORDER BY m.create_time DESC; ListMessageVO list new ArrayList(); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { MessageVO vo new MessageVO(); vo.setId(rs.getInt(id)); vo.setContent(rs.getString(content)); vo.setReplyContent(rs.getString(reply_content)); vo.setCreateTime(rs.getTimestamp(create_time)); vo.setUsername(rs.getString(username)); list.add(vo); } } return list; }COALESCE(u.username, 匿名)是 MySQL 的标准函数也可以写作IFNULL。这里用LEFT JOIN保证未登录游客的留言也能显示。reply_content为空的记录在前端显示“暂无回复”不为空时显示回复内容和回复时间——有回复时间最好但当前表结构没设计reply_time如果你想让答辩更完善可以加一个reply_time DATETIME字段在管理员提交回复时用NOW()更新。管理员回复的页面其实就是一个表单套在每条留言旁边提交到ReplyServlet先判断session里是否登录且role 1然后用UPDATE message_board SET reply_content ?, reply_time NOW() WHERE id ?执行更新。记得用PreparedStatement课程设计里这道题也是考察 SQL 注入的点拼接字符串在答辩现场演示 OR 11会直接翻车。4.3 未登录游客的留言防刷Token 时间间隔双重限制题目允许游客不注册就留言但这也意味着任何人都能刷屏。我用一个 session 里的lastMessageTime做简单节流60 秒内只能留言一次。如果要更高级可以生成一个隐藏的token字段存在 session提交时校验防止跨站请求伪造。long now System.currentTimeMillis(); Long last (Long) request.getSession().getAttribute(lastMessageTime); if (last ! null now - last 60_000) { request.setAttribute(msgError, 留言过于频繁请稍后再试); request.getRequestDispatcher(/message.jsp).forward(request, response); return; } request.getSession().setAttribute(lastMessageTime, now);这里60_000是下划线分隔的数字Java 7 以后的语法视觉效果比60000更清晰。此接口只适合课设这种低并发场景真上线要基于 IP 或设备指纹做限流因为 session 可以被清除但那是后话了。做完这个逻辑你的留言板就从“能跑”变成了“能防住常见攻击”面试时能说出这个设计思路非常加分。5. 整合模板与部署排错让 SB Admin 2 和轮播组件真正为 JSP 服务5.1 静态资源引用路径为什么 ${pageContext.request.contextPath} 必须写在最前面项目前端用的是 SB Admin 2 模板和 Font Awesome 图标库这些都是纯静态资源理论上复制到 webapp 目录就能用。但很多人的页面明明长这样就是加载不出来打开浏览器控制台看到一堆 404问题基本出在相对路径上。JSP 页面在转发前后可能处于不同的 URL 层级比如首页是index.jsp但转发到/home/list后浏览器地址栏变成了/home/list此时相对路径css/sb-admin-2.css就会去请求/home/css/...自然 404。统一做法是任何资源引用都用绝对路径以${pageContext.request.contextPath}开头link href${pageContext.request.contextPath}/vendor/fontawesome-free/css/all.min.css relstylesheet link href${pageContext.request.contextPath}/css/sb-admin-2.min.css relstylesheet script src${pageContext.request.contextPath}/vendor/jquery/jquery.min.js/script${pageContext.request.contextPath}在部署后就是项目名比如http://localhost:8080/hometown所以完整路径是/hometown/vendor/...。如果你把项目部署为 ROOT 应用即去掉项目名它就是空字符串但建议不要这样搞保持项目名避免本地能跑上传服务器后全部路径断裂。首页轮播用的是 Bootstrap 的 Carousel 组件。初始化代码可以放在页面底部等 DOM 加载完再调用。注意若同时引入了 jQuery 和 Bootstrap 的 JS顺序必须是 jQuery 在前、Bootstrap 在后。script $(document).ready(function () { $(.carousel).carousel({ interval: 3000, pause: hover, wrap: true }); }); /scriptinterval: 3000表示 3 秒切换一张pause: hover是鼠标悬停时暂停移动端没有 hover 所以不影响触屏滑动。wrap: true让轮播循环播放。如果你发现轮播图第一次加载后不自动切换多半是引用了两个版本的 jQuery或者 Bootstrap 的 bundle 文件把style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价