简介:这是一套面向Java Web课程设计场景的个人与家乡展示管理平台源码,采用JSP加MySQL技术栈,适合正在完成课程作业或需要练手Java Web全栈开发的学生与初学者。前端涵盖首页照片轮播、个人简介、技能管理、家乡简介、特色介绍与游客留言回复等模块;后端提供用户管理、技能分类管理、家乡特色管理及留言管理,均以Web界面实现增删改查,管理员可统一维护全部信息。压缩包共约2000个文件,以1621个svg图标、108个scss与38个css样式、82个js脚本、48个jsp页面、40个java源码及1个sql数据库脚本为主,整体约29.69MB,目录结构完整,便于按模块阅读与二次修改。目前已有289人学习下载,可作为课程设计参考方案,帮助读者快速理解JSP页面与后端管理的协作方式,并在此基础上完成个性化改造。
1. 从一张家乡照片说起:JSP+MySQL 做展示管理平台到底在做什么
去年帮老家一个做文旅宣传的朋友处理过一个需求:他们想把各乡镇的图文资料、视频、特产信息统一放到一个后台里管理,前台按乡镇分类展示,后台能增删改查。预算有限,不可能上云原生那一套,最后落地的方案就是最朴素的 Java(JSP)+ MySQL。这套组合放到今天依然有它的位置——不是因为它先进,而是因为它足够稳、部署门槛低、招人好招、出问题好查。
这个标题讲的就是这么一件事:用 JSP 做服务端页面渲染,用 MySQL 存结构化数据,搭一个既能对外展示(个人主页、家乡风采、图文视频)又能对内管理(登录、增删改查、分类维护)的平台。它适合谁?适合课程设计、中小型信息展示类项目、地方文旅/校园/社区的资料管理场景,也适合刚接触 JavaWeb 想跑通一个完整闭环的开发者。核心链路其实就四条:请求进来 → JSP/Servlet 处理 → JDBC 操作 MySQL → 页面渲染返回。把这四条理顺,平台就立住了。
2. 技术选型与工程骨架:为什么是 JSP 而不是前后端分离
2.1 JSP+MySQL 在展示管理类项目里的真实定位
先说清楚一个前提:JSP 不是过时技术,它只是被用在了不合适的地方。在需要高并发、复杂交互的场景里,前后端分离 + Spring Boot 是更合理的选择。但展示管理类平台的特点是——页面数量固定、交互以表单提交为主、用户量不大、开发周期短。这种场景下,JSP 的「请求即渲染」模式反而省掉了接口定义、跨域处理、前端构建这一整套链路。
从热词里能看到大量「javaweb项目完整案例mysql」「基于jsp的毕业论文管理过程系统设计与实现」这类检索,说明这个组合在课程设计和中小项目里依然是主流。它的技术栈边界很清晰:
| 层次 | 选型 | 职责 |
|---|---|---|
| 视图层 | JSP + JSTL + EL | 页面渲染、表单展示 |
| 控制层 | Servlet | 请求分发、参数校验 |
| 数据层 | JDBC / DBUtils | SQL 执行、结果映射 |
| 存储层 | MySQL 5.7 / 8.0 | 结构化数据持久化 |
我一般会建议新手不要一上来就上 MyBatis 或 MyBatis-Plus,虽然热词里「mybatisplus根据java实体类生成创建表的sql语句」很火,但在 JSP 项目里,手写 JDBC 配合一个简单的 DBUtils 封装,反而能让你真正理解连接、语句、结果集这三件事。等你把增删改查跑通了,再换 ORM 也不迟。
2.2 工程目录结构与依赖清单
一个能跑起来的 JSP 项目,目录结构必须规范,否则部署到 Tomcat 上会出现各种 404 和类找不到的问题。下面是我常用的骨架:
HometownPlatform/ ├── src/ │ └── main/ │ ├── java/ │ │ └── com/hometown/ │ │ ├── dao/ # 数据访问层 │ │ ├── entity/ # 实体类 │ │ ├── servlet/ # 控制层 │ │ └── util/ # 工具类(DBUtil等) │ ├── resources/ │ │ └── db.properties # 数据库配置 │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # 依赖 jar 包 │ ├── css/ │ ├── js/ │ ├── images/ │ └── *.jsp # 页面文件 └── pom.xml (或手动管理 jar)依赖方面,最小集合是这几个:mysql-connector-java(驱动)、javax.servlet-api(Servlet 规范)、jstl(标签库)、commons-dbutils(可选,简化 JDBC)。如果用 Maven,pom.xml 里加上对应坐标即可;如果手动管理,把这些 jar 丢进WEB-INF/lib目录。
提示:MySQL 8.0 之后驱动类名是
com.mysql.cj.jdbc.Driver,URL 里要带时区和 SSL 参数,否则连接会报错。这是新手最容易翻车的地方之一。
2.3 数据库连接工具类的写法与参数说明
连接管理是整个项目的命脉。我见过太多项目在每个 Servlet 里DriverManager.getConnection(),结果连接数暴涨、性能崩掉。正确做法是写一个工具类,统一管理配置和连接获取。
package com.hometown.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; // 静态块在类加载时执行一次,读取配置文件 static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); // MySQL 8.0 驱动类名,5.x 用 com.mysql.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (Exception e) { throw new RuntimeException("数据库配置加载失败", e); } } // 每次调用返回一个新连接,生产环境应替换为连接池 public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, username, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) {} } } } }对应的db.properties:
jdbc.url=jdbc:mysql://localhost:3306/hometown?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码逻辑说明:静态块保证配置只加载一次,Class.forName注册驱动。getConnection每次返回新连接,适合学习和中小项目;如果并发上来了,换成 Druid 或 HikariCP 连接池,把getConnection改成从池里取即可,上层代码不用动。参数上,characterEncoding=utf8防止中文乱码,serverTimezone不设会报时区错误,useSSL=false在本地开发时避免证书警告。
3. 核心功能落地:从建表到增删改查的完整链路
3.1 数据表设计与建表 SQL
展示管理平台的数据模型不复杂,核心就几张表:用户表、家乡信息表、分类表、媒体资源表。下面给出关键两张的建表语句,字段类型和索引都经过实际项目验证。
-- 用户表:管理后台登录用 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 家乡信息表:核心展示数据 CREATE TABLE t_hometown ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, category_id INT NOT NULL, cover_img VARCHAR(255), content TEXT, video_url VARCHAR(255), view_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数说明:utf8mb4而不是utf8,因为后者在 MySQL 里只支持三字节,存 emoji 或部分生僻字会出问题。view_count用于统计浏览量,展示类平台常用。idx_category索引加速按分类查询,这是前台列表页最频繁的操作。热词里「mysql排序」「mysql存储过程」的检索量很高,排序在展示平台里主要用于按时间或浏览量倒序,直接ORDER BY create_time DESC即可,不必上存储过程。
3.2 实体类与 DAO 层的增删改查
实体类就是数据库表的 Java 映射,字段名对应列名,类型对应好。这里以Hometown为例:
package com.hometown.entity; import java.util.Date; public class Hometown { private Integer id; private String title; private Integer categoryId; private String coverImg; private String content; private String videoUrl; private Integer viewCount; private Date createTime; // 省略 getter/setter,实际项目用 IDE 生成 }DAO 层负责所有 SQL 操作。下面给出查询列表和新增两个核心方法:
package com.hometown.dao; import com.hometown.entity.Hometown; import com.hometown.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class HometownDao { // 分页查询,page 从 1 开始 public List<Hometown> findByPage(int page, int size) throws Exception { String sql = "SELECT * FROM t_hometown ORDER BY create_time DESC LIMIT ?, ?"; List<Hometown> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); ps.setInt(2, size); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Hometown h = new Hometown(); h.setId(rs.getInt("id")); h.setTitle(rs.getString("title")); h.setCoverImg(rs.getString("cover_img")); h.setViewCount(rs.getInt("view_count")); h.setCreateTime(rs.getTimestamp("create_time")); list.add(h); } } } return list; } // 新增一条记录 public int insert(Hometown h) throws Exception { String sql = "INSERT INTO t_hometown(title, category_id, cover_img, content, video_url) VALUES(?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, h.getTitle()); ps.setInt(2, h.getCategoryId()); ps.setString(3, h.getCoverImg()); ps.setString(4, h.getContent()); ps.setString(5, h.getVideoUrl()); return ps.executeUpdate(); } } }逻辑说明:全程用PreparedStatement而不是Statement,这是防 SQL 注入的底线,不要图省事用字符串拼接。分页用LIMIT ?,?,第一个参数是偏移量,第二个是每页条数,这是 MySQL 的标准分页写法。try-with-resources保证连接和语句自动关闭,避免连接泄漏。参数上,page和size由前端传入,服务端要做边界校验,防止page=0或size过大导致全表扫描。
3.3 Servlet 控制层与 JSP 页面渲染
Servlet 是请求的入口,负责接收参数、调用 DAO、把结果放进 request 域,然后转发给 JSP。下面是一个列表页的 Servlet:
package com.hometown.servlet; import com.hometown.dao.HometownDao; import com.hometown.entity.Hometown; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; @WebServlet("/hometown/list") public class HometownListServlet extends HttpServlet { private HometownDao dao = new HometownDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 页码参数,默认第 1 页 int page = 1; String pageStr = req.getParameter("page"); if (pageStr != null && pageStr.matches("\\d+")) { page = Integer.parseInt(pageStr); } int size = 10; try { List<Hometown> list = dao.findByPage(page, size); req.setAttribute("list", list); req.setAttribute("currentPage", page); // 转发到 JSP,由 JSP 负责渲染 req.getRequestDispatcher("/hometown_list.jsp").forward(req, resp); } catch (Exception e) { throw new ServletException("查询家乡列表失败", e); } } }对应的 JSP 页面用 JSTL 遍历:
<%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>家乡风采</title></head> <body> <div class="list"> <c:forEach items="${list}" var="h"> <div class="item"> <img src="${h.coverImg}" alt="${h.title}"/> <h3>${h.title}</h3> <span>浏览:${h.viewCount}</span> </div> </c:forEach> </div> <a href="?page=${currentPage - 1}">上一页</a> <a href="?page=${currentPage + 1}">下一页</a> </body> </html>逻辑说明:Servlet 里做参数解析和业务调用,JSP 只负责展示,这是 MVC 的基本分工。@WebServlet注解省去了 web.xml 配置,Servlet 3.0 之后都支持。JSP 里用 EL 表达式${}取值,比脚本片段<% %>更清晰也更安全。分页链接直接拼page参数,服务端再解析,形成闭环。热词里「jsp个人信息展示页面」「jsp实现mp4视频播放」的需求,在这个结构上扩展即可——视频播放用<video>标签,src指向video_url字段。
4. 避坑与排查:JSP+MySQL 项目最容易翻车的五个地方
4.1 中文乱码:现象、原因与解决
现象:表单提交后数据库里存的是问号,或者页面显示乱码。原因通常有三层:JSP 页面编码、请求编码、数据库连接编码。解决要三处一起改。JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>;Servlet 里在取参数前设置req.setCharacterEncoding("UTF-8");数据库 URL 里带characterEncoding=utf8,表用utf8mb4。三处缺一处都可能翻车,这是血泪经验。
4.2 驱动加载失败:ClassNotFoundException 的排查顺序
现象:启动时报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因一般是 jar 包没放进WEB-INF/lib,或者放进了但没加到构建路径。排查顺序:先确认 jar 在不在WEB-INF/lib下,再看 IDE 的 Artifact 配置里有没有把这个目录打进去,最后确认驱动类名和 MySQL 版本匹配——5.x 用com.mysql.jdbc.Driver,8.x 用com.mysql.cj.jdbc.Driver。热词里「java启动失败怎么解决」「docker安装mysql失败」的很多问题都出在这一步。
4.3 连接泄漏:为什么跑一会儿就报 Too many connections
现象:系统运行一段时间后报Too many connections。原因是在 Servlet 或 DAO 里拿了连接没关,或者异常路径下没走到 close。解决:所有 JDBC 操作必须放在try-with-resources里,或者用 DBUtil 的 close 方法在 finally 里统一关闭。更彻底的做法是上连接池,Druid 或 HikariCP 都行,池会自动回收。这个问题在开发阶段不容易发现,一上压力就暴露。
4.4 分页边界:page 参数不校验导致的全表扫描
现象:有人手动改 URL 把page改成 0 或负数,或者传个超大值,页面要么报错要么卡死。原因是服务端没做参数校验,LIMIT的偏移量算出来是负数或极大值。解决:在 Servlet 里对page做正则校验,只允许正整数,并且设一个上限,比如最大 1000 页。同时size也要限制,不能由前端随意传,否则一次查几万条直接把内存打满。
4.5 部署路径:Tomcat 下 404 的常见原因
现象:本地 IDE 里跑得好好的,部署到 Tomcat 的 webapps 下就 404。原因通常是 context path 变了,JSP 里的相对路径失效。解决:JSP 里所有资源引用用${pageContext.request.contextPath}拼绝对路径,比如<img src="${pageContext.request.contextPath}/images/a.jpg"/>。另外确认web.xml的welcome-file配置和实际首页文件名一致。热词里「java并进入网站」「java最新网站更新入口」这类检索,很多就是部署后访问不到页面。
5. 进阶技巧:用浏览量统计和缓存把展示平台做出「活」的感觉
基础功能跑通之后,展示类平台最容易显得「死板」——页面万年不变,用户来了就走。我一般会加两个小机制让它活起来:浏览量统计和首页缓存。
浏览量统计的做法很简单,在详情页的 Servlet 里,每次查询详情时顺手UPDATE t_hometown SET view_count = view_count + 1 WHERE id = ?。注意要用view_count + 1而不是先查再写,后者在并发下会丢更新。这个字段在前台列表页按热度排序时特别有用,ORDER BY view_count DESC就能把最受欢迎的家乡推到前面。
首页缓存则是为了减少数据库压力。展示平台的首页通常变化不频繁,可以用 ServletContext 做应用级缓存,启动时加载一次,后台修改数据时再刷新。下面是一个简单的缓存工具:
public class CacheUtil { // 用 ServletContext 存首页数据,key 为 "homeList" public static void refreshHomeList(ServletContext ctx, List<Hometown> list) { ctx.setAttribute("homeList", list); } @SuppressWarnings("unchecked") public static List<Hometown> getHomeList(ServletContext ctx) { return (List<Hometown>) ctx.getAttribute("homeList"); } }逻辑说明:ServletContext 的生命周期和应用一致,适合存全局共享数据。后台新增或修改数据后,调用refreshHomeList重新加载,前台首页直接从内存取,不走数据库。参数上,缓存的数据量要控制,一般首页只放前 20 条,多了占内存。这个技巧在中小项目里性价比很高,不用引入 Redis 就能有明显效果。
还有一个我踩过的坑:视频播放页面不要直接把video_url丢给<video>标签就完事,要判断文件格式和浏览器兼容性。MP4 用 H.264 编码兼容性最好,如果是其他格式,要么转码要么给下载链接。热词里「jsp实现mp4视频播放」的检索背后,很多人卡在格式不支持上。
最后说个习惯:每次改完 DAO 层的 SQL,我都会在 MySQL 客户端里先手动跑一遍,确认结果对了再写进代码。这个习惯帮我省了无数次「代码没问题但数据不对」的排查时间。JSP+MySQL 这套东西不复杂,但细节多,慢就是快。希望帮到你。
本文还有配套的精品资源,点击获取