Java+JSP+Servlet+MySQL高校社团管理系统开发部署实战
2026/9/13 1:56:02 网站建设 项目流程

简介:这是一份基于Java Web技术栈的高校社团管理系统的完整项目资料,包含全套源码、演示视频与配套说明文档,面向计算机相关专业学生、毕业设计/课程设计开发者,也适合作为Java Web课程综合实践的参考范例。系统围绕学生、社长、管理员三种角色设计,功能覆盖注册登录、社团创建与申请、活动报名、新闻与留言管理、社团财务、成员管理等常见业务模块,前后台界面美观,业务流程清晰,可直接运行也可二次开发。压缩包共981个文件,约56.7MB,主要文件类型包括jsp页面、Java类与SQL数据库脚本,以及前端样式脚本、图片素材和mp4讲解视频、docx使用说明等,能够完整支撑从环境配置到功能演示的全过程。已有2402人学习使用,适合需要完成课设/毕设、理解JSP+Servlet+MySQL三层架构,或想搭建校园社团管理系统的读者参考借鉴。

1. 高校社团管理系统为什么还是 Java+JSP+Servlet+MySQL

每年九月社团招新、年底课程答辩前后,一批又一批人会打开名为 java+jsp+servlet+mysql高校社团管理系统源码+视频+说明.zip 的压缩包。这谈不上新技术组合,却是 Java Web 课程设计里流传最广的搭配:JSP 画页面、Servlet 接请求、MySQL 存数据,不依赖 Spring 框架就能把一套完整业务跑在 Tomcat 上。选这个技术栈的价值在于结构透明,从表单提交到 DAO 查询再到页面跳转,每一环都暴露在眼前,答辩时问不到死角。

这套方案覆盖三类诉求。课程设计答辩前想快速跑通项目、逐行看懂别人代码的人;准备转 Spring Boot 但 Servlet 原理还没理顺的人;以及给校内社团做日常管理、需要部署在实验室服务器或旧电脑上的学生团队。三类人最终要解决的是同一个问题:源码目录、数据库脚本和 Tomcat 部署路径如何正确对应起来。附带视频通常按页面演示操作流程,说明文档给出环境配置和功能清单,但真正卡壳的地方一般是版本不匹配、字符集错乱和 Servlet 映射漏配。下面按启动项目的正常顺序,把这条链路完整拆一遍。

2. 先理顺 JSP+Servlet 的请求链路,再改社团业务代码

2.1 一次“申请入社”点击背后,Servlet 容器做了哪些事

用户在社团列表页点击“申请加入”,浏览器发出一个 POST 请求,Servlet 容器的处理分三步。第一步走过滤器链,web.xml 里注册的 CharacterEncodingFilter、LoginFilter 依次执行;第二步容器根据 URL 匹配到注册的 Servlet,创建请求和响应对象并调用 service 方法;第三步 service 根据 HTTP 方法分发到 doPost。很多人把 Servlet 理解为普通的 Java 接口,实际开发中写的是一个继承 HttpServlet 的类,类的实例化和销毁都由容器管理,不在代码里 new。

以源码包常见的社团控制类为例,web.xml 中的映射如下:

<servlet> <servlet-name>ClubServlet</servlet-name> <servlet-class>com.shetuan.controller.ClubServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ClubServlet</servlet-name> <url-pattern>/club/*</url-pattern> </servlet-mapping>

这里三个配置点需要看懂。<servlet-class>写的是全限定类名,编译后的 class 必须放在对应包路径下,否则启动时报 ClassNotFoundException;<url-pattern>/club/*前缀匹配,意味着/club/applyJoin/club/auditMember都进这个 Servlet;<load-on-startup>告诉容器启动时创建实例,不写则推迟到第一次请求。项目小的时候这三项影响有限,但答辩追问“Servlet 什么时候初始化、什么时候销毁”时,能解释清楚 init 和 destroy 的执行时机就是加分项。

2.2 请求参数到 JDBC 查询:Model 层的职责边界

Servlet 拿到请求后,习惯上不在 doPost 里直接写 JDBC。一个维护性好的社团系统会拆分三层:Controller 负责从 request 取值、做基础校验、决定跳转方向;Service 负责业务规则,例如“社长不能退出自己创建的社团”“同一用户不能重复申请同一社团”;Dao 只做 PreparedStatement 的执行和 ResultSet 到对象的映射。源码包如果只有两层,至少要保证 JDBC 集中在 DAO 类里,不要让 JSP 页面出现数据库连接代码。

写一个新功能时,常见做法是四个步骤严格分开:先获取参数,再校验参数格式和空值,然后调 Service,最后按结果 forward 或 sendRedirect。每一步失败都要有明确返回,不能一路往下走到数据库层才报错。比如社团活动报名,先确认 activityId 是正整数,再检查报名截止时间,最后插入 t_signup 表。答辩时最容易被追问的是“参数校验为什么放在 Controller 而不放在 SQL 里”,回答是 SQL 层只能防注入,无法处理业务语义,两层配合才是正解。

2.3 JSP 页面上拿数据的四种途径,分别用在哪一页

社团管理系统的 JSP 页面数量不多,login.jsp、index.jsp、club_list.jsp、member_manage.jsp、activity_list.jsp、announcement_detail.jsp 基本就覆盖全部。数据从后端传到 JSP 有四种途径,对应不同使用场景。

第一种是 request 域,通过 request.setAttribute 存数据,再用 RequestDispatcher.forward 带到下一个 JSP,适合列表查询和详情展示。第二种是 session 域,用户登录后把对象放进去,导航栏显示当前用户、权限判断都靠它。第三种是 application 域,全项目共享,适合存网站名称等配置信息。第四种是 URL 参数,用?clubId=3传递主键,适合从列表页跳详情页。

2.3.1 列表页和详情页之间的参数传递示例
JSP 页面数据来源传递方式易错点
club_list.jsp社团列表request.setAttribute + forward分页页码丢失时列表回到第一页
club_detail.jsp单个社团信息GET 参数 clubIdInteger.parseInt 可能抛 NumberFormatException
member_manage.jsp成员列表session 取登录人再查库社长只能查自己社团,不能全表查询
announcement_detail.jsp公告正文查询参数 id正文含 HTML 时需要在 JSP 转义

有一点非常容易踩坑:用 response.sendRedirect 跳转后,request 域数据全部丢失,因为重定向会产生新的请求。源码包里出现的“登录成功但没有用户名显示”多半就是这个原因。区分方法看地址栏:重定向后 URL 变化,转发地址栏不变。需要保留数据就放 session,或者改用 forward,两者不可混用。

3. 本地启动前先对齐版本:JDK、Tomcat、MySQL 选型与数据库脚本导入

3.1 先对齐版本,再谈环境变量:JSP+Servlet 项目最常见的启动失败点

把压缩包解压后不要急着双击启动,先判断项目是 Servlet 2.5 还是 3.0 时代写的老代码。判断方法简单:代码用注解@WebServlet就是 3.0 及以上,用web.xml就是 2.5。老式课程设计项目最稳的运行组合是 JDK 8 + Tomcat 8.5 + MySQL 5.7,这套配置兼容性最高,网上能找到对应安装教程。如果坚持用 JDK 17 和 Tomcat 10,会遇到包名变更问题:Tomcat 10 把 javax.servlet 改为 jakarta.servlet,老代码统一报编译错误,改起来要动所有 import 行,得不偿失。

技术组件推荐版本备选方案选择理由
JDK1.811(不需要)老代码按 Java 6-8 语法编写
Tomcat8.5.x9.0.x两个版本都支持 javax.servlet 包名
MySQL5.7.x8.0.x(改驱动)5.7 驱动配置更省心
连接驱动mysql-connector-java-5.1.498.0.335.7 对应的驱动免去时区参数

这里要把“找不到或无法加载主类”一类报错和 JSP+Servlet 项目区分开。那类错误常见于 Spring Boot 主类和 Maven 打包产物缺失,跑本项目的典型报错则是ClassNotFoundException: com.mysql.jdbc.Driverorg.apache.catalina.LifecycleException。前者是驱动 jar 没放进 WEB-INF/lib,后者是 web.xml 里配置的类路径写错。看 Tomcat 日志时先找第一行 Caused by,后面才是真实原因。

3.2 数据库表结构导入:社团系统最少需要的五张表与关键约束

一个能演示完流程的社团系统,MySQL 至少要建五张表:用户表、社团表、成员表、活动表、公告表。多数源码包会提供 club_system.sql 脚本,导入即可;如果压缩包里缺脚本,可以按下述结构自己建,先建三张核心表:

-- 创建数据库,字符集统一用 utf8mb4 CREATE DATABASE IF NOT EXISTS club_system DEFAULT CHARACTER SET utf8mb4; USE club_system; -- 用户表:学生和管理员共用一张表,role 区分身份 CREATE TABLE IF NOT EXISTS t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role TINYINT DEFAULT 2 COMMENT '1=管理员,2=普通学生', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 社团表:leader_id 指向社长用户 CREATE TABLE IF NOT EXISTS t_club ( id INT PRIMARY KEY AUTO_INCREMENT, club_name VARCHAR(100) NOT NULL, category VARCHAR(50), intro TEXT, leader_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (leader_id) REFERENCES t_user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成员表:status 0待审核 1通过 2拒绝 CREATE TABLE IF NOT EXISTS t_member ( id INT PRIMARY KEY AUTO_INCREMENT, club_id INT NOT NULL, user_id INT NOT NULL, status TINYINT DEFAULT 0, role TINYINT DEFAULT 0 COMMENT '0=普通成员,1=副社长', apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_club_user (club_id, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

成员表的唯一索引 uk_club_user 是关键设计,它从数据库层拦截同一用户对同一社团的重复申请,即使 Servlet 漏写判断,插入时也会回 Duplicate entry 错误。活动表和公告表结构类似,t_activity 存活动名、时间、地点、人数上限,t_announcement 存标题、正文和发布时间,按外键关联到社团即可。

字符集有三个位置必须保持一致:数据库建库语句、JDBC 连接 URL、JSP 页面头部的 pageEncoding。出现乱码先定位是哪个环节,页面显示乱码而数据库不乱,重点查 JSP 和过滤器;数据库存进去就是乱码,则查建表语句的 DEFAULT CHARSET 和连接 URL 的 characterEncoding 参数。MySQL 5.7 连接 URL 建议写?useUnicode=true&characterEncoding=utf8&useSSL=false,八个参数如实配置。

3.3 用 Maven 还是手动引 jar:源码包常见的两种组织形态

流传的源码包有两种组织方式。带 Maven 的项目根目录有 pom.xml,依赖由 Maven 下载,导入 IDE 后会自动构建;手动引 jar 的项目把 mysql-connector-java 等文件放在 WebContent/WEB-INF/lib 下,不需要 Maven 配置。区分方法是找 pom.xml,存在就用 Maven 刷新,不存在则确认 lib 目录里的 jar 是否完整。

用 Eclipse 导入时,老项目容易遇到构建路径不完整,右键项目选 Build Path 检查 JRE System Library 和 Tomcat 运行时是否绑定。用 IDEA 则需要配置 Artifacts,把项目输出打包成 exploded war 并部署到 Tomcat。Application context 的路径必须和源码包里的跳转逻辑一致,比如登录成功跳转/ClubSystem/index.jsp,如果部署后实际路径是/club_war_exploded,就会出现 404。视频里演示到访问登录页为止,后面的路径问题最好在浏览器直接改 URL 测试,确认哪个上下文能通。

4. 三个核心功能直接给代码:登录会话、入社审核、公告分页

4.1 登录校验:把字符串拼接 SQL 换成 PreparedStatement

登录入口是所有管理系统演示的第一步,但老源码包的登录实现通常有两种问题:密码明文存储、SQL 用字符串拼接。前者不说了,后者必须改。可以用下面这段标准实现替换:

// com.shetuan.controller.LoginServlet @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 空值校验,trim 后为空也拦下 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { request.setAttribute("errorMsg", "用户名和密码不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } UserDao dao = new UserDao(); User user = dao.findByUsernameAndPassword(username, password); if (user == null) { // 转发而不重定向,错误信息才会出现在登录页 request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 登录成功后把用户放进 session,后续页面统一从这里取 HttpSession session = request.getSession(true); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); response.sendRedirect(request.getContextPath() + "/index.jsp"); } }

代码逻辑分成四段:设置编码、取值校验、查询用户、分支跳转。注意 session.setMaxInactiveInterval(30 * 60) 的单位是秒,表示 30 分钟无操作会话失效;如果想关闭浏览器后保持登录,需要额外用 Cookie 实现记住功能。UserDao 中的查询使用 PreparedStatement:

public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, password, real_name, role FROM t_user WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRealName(rs.getString("real_name")); user.setRole(rs.getInt("role")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

用 PreparedStatement 后,用户名中的单引号只作为参数值处理,不再参与 SQL 语法解析,注入攻击失去效果。这段代码使用 try-with-resources 自动关闭连接,连接对象从 DBUtil 获取,后续换成连接池也不用改 DAO 层。

4.2 入社申请与审核:状态字段加事务和唯一索引兜底

入社流程是学生的核心操作:在社团列表申请加入生成 status=0 记录,社长审核通过改为 1,拒绝改为 2。核心代码在业务层,需要处理“重复申请”的并发问题:

public boolean applyToJoin(int clubId, int userId) { // 查重和插入必须在一个事务里,否则并发下会重复 String existsSql = "SELECT id FROM t_member WHERE club_id=? AND user_id=?"; String insertSql = "INSERT INTO t_member(club_id, user_id, status, apply_time) VALUES(?,?,0,NOW())"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(existsSql)) { ps.setInt(1, clubId); ps.setInt(2, userId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return false; // 已申请或已是成员 } } } try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setInt(1, clubId); ps.setInt(2, userId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); } return false; }

事务在这里的意义是保证“查重”和“插入”不会被拆成两个独立连接,否则两个并发请求可能都通过查重再各自插入。即使业务代码漏了查重,t_member 表的唯一索引也会在数据库层拦截第二次插入。社长审核的更新语句:

UPDATE t_member SET status = 1, audit_time = NOW() WHERE id = ? AND club_id = ?

这个 SQL 有个 MySQL 特有的坑:同一语句中不能通过子查询读取正在更新的同表数据。如果写成UPDATE t_member SET status=1 WHERE club_id=(SELECT id FROM t_member WHERE ...),MySQL 会直接报错。先查询出符合条件的 member id,再按主键更新,绕开同表子查询的限制。

4.3 公告列表分页:把 JSP 里的 Java 逻辑收敛到 Servlet

公告页按发布时间倒序展示,数据量不大,但如果题目要求分页就要写分页逻辑。三个核心参数是 page(当前页码)、pageSize(每页条数)、totalPages(总页数)。Servlet 端代码:

int page = 1; int pageSize = 8; if (request.getParameter("page") != null) { page = Integer.parseInt(request.getParameter("page")); } AnnouncementDao dao = new AnnouncementDao(); int total = dao.countAll(); // 向上取整算出总页数 int totalPages = (total + pageSize - 1) / pageSize; List<Announcement> list = dao.findByPage((page - 1) * pageSize, pageSize); request.setAttribute("announcementList", list); request.setAttribute("page", page); request.setAttribute("totalPages", totalPages); request.getRequestDispatcher("/announcement_list.jsp").forward(request, response);

分页最容易错的位置是偏移量换算。前端传的 page 从 1 开始,数据库 LIMIT 从 0 开始,因此查询时偏移量是(page - 1) * pageSize。findByPage 内部 SQL 为SELECT * FROM t_announcement ORDER BY create_time DESC LIMIT ?,?,第一个问号传偏移量,第二个传 pageSize。直接传 page 进去时,第二页开始数据错位,这是代码审查时经常发现的问题。

JSP 渲染时只负责循环输出对象列表,不要再查数据库。支持 JSTL 的页面用<c:forEach items="${announcementList}" var="item">,没引入 JSTL 就用传统 for 循环。如果出现“jsp jsonarray import”的需求,比如列表页要局部刷新数据,正确做法是让 Servlet 用 Gson 把 List 对象序列化成 JSON 字符串输出,再由 JSP 里的 JavaScript 解析,而不是在 JSP 顶部 import JSON 库。

4.4 未登录拦截:一个 Filter 管住后台所有 JSP 页面

社团管理后台的页面非常多,逐页校验登录状态会漏掉。一个 LoginFilter 统一处理:

@WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页、登录接口、css 和 js 静态资源 if (uri.endsWith("/login.jsp") || uri.endsWith("/login") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(req, resp); return; } // 未登录用户统一踢回登录页 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

关键细节有两个。第一,request.getSession(false)不会创建新 session,没有会话时返回 null,避免每个匿名请求都制造一个 session 对象;第二,未登录跳转使用 sendRedirect 而不是 forward,保证地址栏变成 login.jsp,用户刷新不会重复提交。如果配置了多个过滤器,注意注册顺序会影响执行顺序,编码过滤器要排在 LoginFilter 前面。

5. 部署前验证的三个技巧:看 work 目录、抓字符集、查连接回收

5.1 改完 JSP 不生效,先检查 work/Catalina 再谈清理缓存

JSP 第一次被访问时,Tomcat 会把页面翻译成 Java 源文件,再编译成 class,产物统一放在work/Catalina/localhost/应用上下文/org/apache/jsp目录下。改完 JSP 浏览器没变化,原因通常是三种:IDE 没有重新发布、浏览器缓存、JSP 编译失败。排查顺序是:看 IDE 的 Artifacts 是否更新,再看 work 目录下对应 java 文件的生成时间,最后看 Tomcat 日志有没有编译报错。

如果源码包里引用了外部 JavaScript 并绑定了window.onbeforeunload事件,页面每次刷新都会弹出“离开此页面”的确认提示,干扰验收。屏蔽方式是在被引入页面里覆盖绑定:window.onbeforeunload = null;。这类问题不属于编译错误,但演示排错时经常被误认为 JSP 代码问题,先确认不是这个原因再往深层查。

5.2 用 SetCharacterEncodingFilter 收口三种中文乱码入口

GET 请求参数、POST 表单体、JSP 响应输出,三个位置都可能产生中文乱码。一个过滤器统一收口:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

编码参数必须设为 UTF-8,URL 模式用/*覆盖所有路径。Tomcat 8.5 起连接器默认 URIEncoding 已为 UTF-8,GET 参数乱码问题已大幅减少,但要检查 server.xml 中是否有显式覆盖配置。POST 表单的乱码根源是 request.setCharacterEncoding 的调用时机,必须在 getParameter 之前执行,这个过滤器正是通过拦截所有请求解决时机问题。已经配置了过滤器就不需要在 LoginServlet 里重复设置,重复设置不会报错,但答辩被追问优先级时会答不上来。

5.3 连接池改造后的验证:SHOW PROCESSLIST 比日志更直接

把 DBUtil 从DriverManager.getConnection改成连接池后,验证改造成果用SHOW PROCESSLIST最直接。进入 MySQL 执行:

SHOW PROCESSLIST;

观察连接数会稳定在一个区间内,频繁访问期间不会出现几十条 Time 在 0 左右的循环新连接。HikariCP 配置时把 maximumPoolSize 设为 10 就够社团系统的流量,数据库连接 URL 拼接useSSL=false&serverTimezone=Asia/Shanghai两个参数,Java 8 环境不能省略时区配置。连接池改造后如果出现Connection is not available, request timed out,问题几乎都出在业务代码里连接未关闭,排查重点是找哪些 Connection 没有放在 try-with-resources 中。改造完成后跑一遍入社申请、审核、公告发布三个流程,每次操作后用 SHOW PROCESSLIST 看连接数变化,即可确认回收机制正常。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询