☰
Java Web在线报名系统课程设计:从表结构到核心代码实现全解析
2026/9/28 5:31:25 网站建设 项目流程

简介:基于Java Web的在线报名系统毕业设计/课程设计资料包,面向计算机专业学生和Web开发初学者。系统功能涵盖考生注册登录、个人信息管理、成绩查询、在线问答,以及管理员对考生信息与成绩的增删改查、成绩报表生成、网上缴费等模块,能较为完整地演示Java Web项目从页面展示到后台业务处理的实现流程。压缩包共130个文件,以JSP页面、Java源码和class文件为主体,辅以jar依赖库、数据库脚本、配置文件与课程设计报告(doc),整体约5.24MB。目前已吸引618人浏览学习。项目工程结构完整,代码层次清晰,可直接导入IDE运行调试,便于对照源码理解会话管理、权限校验与在线支付等典型功能的设计方式,适合课程答辩、毕业设计参考及Java Web项目实训练习。

1. 课程设计里的Java web在线报名系统:功能闭环完整、最容易拿高分的选题

如果你正在为Java web课程设计选题目发愁,在线报名系统是最不容易翻车的一类。它不像商城那样要处理订单状态机,也不像后台管理系统那样单纯堆CRUD页面,而是把注册、登录、活动发布、报名提交、后台审核这一整条链路串起来,正好把Servlet、JSP、JDBC、MySQL增删改查、事务、Filter拦截、分页查询这些Java基础知识点全部覆盖到。我用这套方案带过不少学生,从建表到跑通主流程,三周内完成源码、数据库脚本和课程设计报告完全来得及。这篇就把选题拆解、表结构设计、核心代码实现和几个必踩的坑一次讲透。

2. 先建模型再写代码:报名系统的表结构与Java web工程骨架

2.1 报名系统的核心关系:一次活动对应多条报名记录

在线报名系统的业务模型不复杂,但很多人在第一步就做错了——想把“用户报名活动”直接存成一条记录里的两个字段。看似简单,后面做“查看我已报名的活动”“某个活动有哪些人报名”时,SQL写得非常别扭。正确的做法是把用户和活动之间的多对多关系拆开,中间加一张报名表。

我一般这样理解三个实体的关系:

  • 用户(普通用户 / 管理员)与活动:用户可以看到多个活动,活动可以被多个用户查看,这是多对多。
  • 用户与报名记录:一个用户提交多条报名记录,一对多。
  • 活动与报名记录:一个活动接收多条报名记录,一对多。

所以表结构上必然有t_user、t_activity、t_enrollment三张核心表。t_enrollment里只存user_id和activity_id两个外键,再加上报名时间、审核状态、备注信息。后面不管做“活动报名人数统计”还是“用户报名历史”,都是对t_enrollment这张表做条件查询。这个建模是后续所有功能的地基,地基歪了后面代码全歪。

选型上还有一点要注意:课程设计没必要引入Spring Security或Shiro这种权限框架。用户表里加一个role字段,0表示普通用户、1表示管理员,配合Filter做后台路径拦截就够用了。答辩时老师问权限怎么做,你能答出Filter和Session校验,就已经超出大多数同学的完成度。

2.2 MySQL库表设计:五张表搞定用户、活动与报名记录

下面是可直接执行的建表脚本,基于MySQL 8.0。如果你用的是MySQL 5.7,需要把utf8mb4_0900_ai_ci改成utf8mb4_general_ci,否则导入会报排序规则错误。

-- 用户表:普通用户和管理员共用一张表,用role区分 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'MD5加密后的密码', real_name VARCHAR(20) DEFAULT '' COMMENT '姓名', phone VARCHAR(11) DEFAULT '' COMMENT '手机号', email VARCHAR(50) DEFAULT '' COMMENT '邮箱', role TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 活动分类表:给活动加一个分类,方便后台管理 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID', name VARCHAR(30) NOT NULL UNIQUE COMMENT '分类名称' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动分类表'; -- 活动表 CREATE TABLE t_activity ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '活动ID', category_id INT COMMENT '分类ID,关联t_category', title VARCHAR(100) NOT NULL COMMENT '活动标题', content TEXT COMMENT '活动详情', location VARCHAR(100) DEFAULT '' COMMENT '活动地点', start_time DATETIME COMMENT '开始时间', end_time DATETIME COMMENT '结束时间', max_people INT DEFAULT 100 COMMENT '人数上限', status TINYINT DEFAULT 0 COMMENT '0-未开始 1-进行中 2-已结束', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动表'; -- 报名表:核心关联表 CREATE TABLE t_enrollment ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '报名ID', user_id INT NOT NULL COMMENT '用户ID,关联t_user', activity_id INT NOT NULL COMMENT '活动ID,关联t_activity', enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '报名时间', status TINYINT DEFAULT 0 COMMENT '0-待审核 1-通过 2-拒绝', remark VARCHAR(200) DEFAULT '' COMMENT '备注或拒绝原因', UNIQUE KEY uk_user_activity (user_id, activity_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报名记录表';

这段DDL里有几个设计点需要在报告里写清楚。Unlock唯一索引uk_user_activity是关键,它在数据库层面杜绝了同一用户重复报名同一个活动。很多同学只在代码里做一次SELECT判断就认为防住了,两个并发请求同时通过检查、同时INSERT,照样产生两条记录,唯一索引是最后的兜底防线。

status字段用TINYINT数字而不是字符串,也是课程设计里容易被忽略的细节。用数字状态配合Java枚举或常量类,比在数据库里存“待审核”“通过”这种中文要规范,也方便后面做状态筛选。t_category表不是必须的,但加上它能让后台“活动管理”模块多出一个分类管理功能,报告的模块划分更完整。

2.3 Java web项目结构:Servlet + JSP + JDBC的三层划分

工程结构上,我建议直接用经典Servlet + JSP + JDBC,不要在这个时候引入Spring Boot。课程设计报告一般要求画出分层架构图,Servlet + JSP + JDBC的分层最直观,答辩时老师也更容易看懂你在做什么。项目结构如下:

src/ ├── com.course.signup.filter/ // Filter拦截器 ├── com.course.signup.servlet/ // 控制层,接收请求、调用Service ├── com.course.signup.service/ // 业务层,处理业务逻辑 ├── com.course.signup.dao/ // 数据访问层,封装JDBC操作 ├── com.course.signup.entity/ // 实体类,对应数据库表 ├── com.course.signup.util/ // 工具类,DBUtil、MD5Util等 └── db.properties // 数据库连接配置 WebContent/ ├── css/ js/ images/ ├── WEB-INF/lib/ // mysql驱动jar、jstl jar ├── WEB-INF/web.xml // Servlet映射、Filter注册 ├── index.jsp // 登录页 ├── register.jsp // 注册页 ├── activity_list.jsp // 活动列表 ├── activity_detail.jsp // 活动详情+报名入口 └── admin/ // 管理端页面

分层的核心是依赖方向:Servlet不直接写JDBC代码,它调用Service,Service调用DAO,DAO返回实体对象。很多同学图省事,在Servlet里直接写Connection和PreparedStatement,代码确实能跑,但报告里“分层设计”那章完全写不出来,答辩被问“为什么要分层”时只能沉默。Servlet层就干三件事:取参数、调Service、根据结果跳转页面。

这里再提一个实践上的选择:Servlet用XML还是注解?我建议用XML(web.xml)方式。课程设计用的Tomcat版本不确定,注解映射在Servlet 3.0以上才支持,XML方式兼容性最好。页面统一用JSP + JSTL,JSP里只写HTML和标签,不写Java代码片段,有同学用<%%>把Java代码写满整个页面,报告会非常难看,代码可读性也差。

3. 把报名主流程跑通:JDBC连接、用户注册与提交报名

3.1 数据库连接工具类:配置外置,换库不换代码

数据库连接这块,课程设计阶段的常见做法是自己封装一个DBUtil工具类,用db.properties文件保存连接配置。这样以后换数据库、换账号密码,只需要改配置文件,不需要重新编译Java代码。先建配置文件:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/signup_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的数据库密码

注意url里的serverTimezone=Asia/Shanghai,MySQL 8默认时区是UTC,不加这个参数连接会报时区错误。useSSL=false则避免本地开发时SSL握手提示。字符编码参数characterEncoding=utf8配合后面讲的页面编码,一起解决中文乱码问题。

然后是DBUtil工具类:

package com.course.signup.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String driver; 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); driver = props.getProperty("jdbc.driver"); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName(driver); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置初始化失败"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { } } if (conn != null) { try { conn.close(); } catch (SQLException e) { } } } }

参数说明:getResourceAsStream从classpath根目录读取db.properties,配置文件放在src目录下即可。关闭资源的顺序是先ResultSet后Statement最后Connection,这个顺序错了在MySQL连接数耗尽时会暴露问题。Class.forName(driver)在MySQL 8下可以省略,驱动版本会自适应,但保留这行能让报告里讲清楚“驱动加载”这个知识点,建议别删。这套写法里连接是每次请求新建的,课程设计并发量不大完全够用,不要在报告里吹自己支持高并发就行。

3.2 用户注册登录:参数校验与MD5加盐

注册功能是报名系统第一个完整模块。处理流程:JSP表单提交到RegistServlet,Servlet提取参数,校验两次密码是否一致、用户名是否已存在,然后加密密码并插入数据库。下面是核心代码:

package com.course.signup.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.course.signup.dao.UserDao; import com.course.signup.entity.User; import com.course.signup.util.MD5Util; public class RegisterServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); String password2 = req.getParameter("password2"); // 参数校验:空值和两次密码不一致直接返回错误 if (username == null || username.trim().isEmpty() || password == null || password2 == null) { req.setAttribute("msg", "用户名和密码不能为空"); req.getRequestDispatcher("register.jsp").forward(req, resp); return; } if (!password.equals(password2)) { req.setAttribute("msg", "两次密码不一致"); req.getRequestDispatcher("register.jsp").forward(req, resp); return; } if (userDao.findByUsername(username) != null) { req.setAttribute("msg", "用户名已存在"); req.getRequestDispatcher("register.jsp").forward(req, resp); return; } User user = new User(); user.setUsername(username); user.setPassword(MD5Util.md5(password)); // 存储加密后的密码 user.setRole(0); boolean ok = userDao.insert(user); if (ok) { resp.sendRedirect("index.jsp"); // 注册成功跳转登录页 } else { req.setAttribute("msg", "注册失败,请重试"); req.getRequestDispatcher("register.jsp").forward(req, resp); } } }

MD5加密工具类核心方法:

public static String md5(String source) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(source.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } }

逻辑说明:密码不能以明文存数据库,这是答辩时老师必问的一个点。MD5已经不算安全,但课程设计用到这个级别可以接受,如果想把加分项做足,就写个加盐版本,比如MD5Util.md5(username + password),同一密码不同用户名得到不同密文。注册成功用sendRedirect而不用forward,是为了防止F5刷新页面导致表单重复提交。

参数说明:UserDao.findByUsername先查用户名是否存在,这个查询用PreparedStatement防SQL注入,代码里不要用字符串拼接SQL。req.setCharacterEncoding("UTF-8")只能解决POST请求乱码,GET请求乱码要在Tomcat的server.xml里给Connector加URIEncoding="UTF-8",后面避坑章会细说。

3.3 提交报名:用事务保证一人一号

报名提交是系统里业务价值最高的代码,也是唯一需要用到事务的地方。流程:用户从活动详情页点“我要报名”,后台先判断是否登录、活动是否存在、是否已满、是否重复报名,然后插入一条报名记录。

public boolean enroll(int userId, int activityId) { Connection conn = null; PreparedStatement checkPs = null; PreparedStatement insertPs = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); // 开启事务:报名涉及多次查询和插入,必须保证原子性 conn.setAutoCommit(false); // 检查活动是否存在 String checkActivity = "SELECT COUNT(*) FROM t_activity WHERE id = ?"; checkPs = conn.prepareStatement(checkActivity); checkPs.setInt(1, activityId); rs = checkPs.executeQuery(); if (rs.next() && rs.getInt(1) == 0) { return false; // 活动不存在 } // 检查是否重复报名(配合数据库唯一索引双保险) String checkEnroll = "SELECT COUNT(*) FROM t_enrollment WHERE user_id = ? AND activity_id = ?"; checkPs = conn.prepareStatement(checkEnroll); checkPs.setInt(1, userId); checkPs.setInt(2, activityId); rs = checkPs.executeQuery(); if (rs.next() && rs.getInt(1) > 0) { return false; // 已报名 } // 检查人数是否已满 String checkCount = "SELECT COUNT(*) FROM t_enrollment WHERE activity_id = ? AND status = 1"; checkPs = conn.prepareStatement(checkCount); checkPs.setInt(1, activityId); rs = checkPs.executeQuery(); if (rs.next() && rs.getInt(1) >= getMaxPeople(activityId)) { return false; // 人数已满 } // 插入报名记录,状态默认为待审核 String insertSql = "INSERT INTO t_enrollment (user_id, activity_id, status) VALUES (?, ?, 0)"; insertPs = conn.prepareStatement(insertSql); insertPs.setInt(1, userId); insertPs.setInt(2, activityId); int rows = insertPs.executeUpdate(); conn.commit(); // 所有检查通过,提交事务 return rows > 0; } catch (SQLException e) { try { if (conn != null) conn.rollback(); // 任何一步失败回滚 } catch (SQLException ex) { } e.printStackTrace(); return false; } finally { DBUtil.close(conn, insertPs, null); try { if (rs != null) rs.close(); } catch (SQLException e) { } try { if (checkPs != null) checkPs.close(); } catch (SQLException e) { } } }

这段代码有三个关键参数和设计取舍。conn.setAutoCommit(false)是事务的开关,重点不在检查逻辑多少,而在于:一旦开启了事务,finally里就不能直接关连接,必须先commit或rollback再关,否则数据没提交连接就释放了。回滚只包住SQLException,业务上返回false的情况不需要回滚,因为还没执行INSERT。

至于getMaxPeople方法,它是单独去查活动表的人数上限,不在同一个事务里也能跑,但为了数据一致性放进同一连接更严谨。很多同学会问检查人数满不满有没有并发问题:两个用户同时报名最后一个名额,两个请求同时查到未满,然后都插入成功。解决思路是把“更新名额”做成原子操作,但课程设计到“事务+唯一索引”这套组合,已经比大多数同学完整了,超出这个范围的并发场景在报告里提一句即可。

4. 管理端审核与分页查询:课程设计得高分的分水岭

4.1 Filter拦截器实现后台权限控制

在线报名系统的管理端必须先解决权限问题。不加拦截的话,用户直接在浏览器地址栏输入/admin/activity_list.jsp就能绕过登录进后台,这是课程设计审查里最刺眼的漏洞。用Filter统一拦截后台请求,最稳妥。

package com.course.signup.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class AdminFilter implements Filter { @Override public void init(FilterConfig config) { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); // 未登录或非管理员直接重定向到登录页 if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/index.jsp"); return; } Integer role = (Integer) session.getAttribute("role"); if (role == null || role != 1) { response.sendRedirect(request.getContextPath() + "/index.jsp"); return; } chain.doFilter(req, resp); } @Override public void destroy() { } }

在web.xml里配置Filter的映射路径:

<filter> <filter-name>adminFilter</filter-name> <filter-class>com.course.signup.filter.AdminFilter</filter-class> </filter> <filter-mapping> <filter-name>adminFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping>

逻辑说明:request.getSession(false)是关键,括号里传false表示“如果当前没有Session就返回null,而不是新建一个Session”。如果写成getSession(),每个未登录请求都会创建一个新Session,Filter就永远拦不住人。role从Session里取,登录成功时把user对象和role都塞进Session,Filter判断role是否等于1。

参数说明:url-pattern用/admin/*覆盖管理端所有路径,如果管理端有单独的Servlet,也放在/admin下或直接通过JSP页面调用——建议把管理端的Servlet路径统统用/admin/xxx来命名,这样Filter一处拦截就够。登录功能里不要只存username,把id和role一起存进Session,后面做“我的报名”“审核操作”都要用。Session超时时间在web.xml里配置session-config,默认30分钟够用。

4.2 报名列表的分页查询与状态筛选

管理端报名列表是这个系统数据量最大的页面。不写分页,数据一多页面就卡死,老师查看报告时也看不到优化意识。分页查询的Servlet核心逻辑:

public PageResult<Enrollment> queryPage(int pageNum, int pageSize, Integer status) { PageResult<Enrollment> result = new PageResult<>(); String where = " WHERE 1=1 "; // status为null表示查询全部,否则按状态筛选 if (status != null) { where += " AND e.status = " + status; } String countSql = "SELECT COUNT(*) FROM t_enrollment e" + where; String dataSql = "SELECT e.id, e.user_id, e.activity_id, e.enroll_time, " + "e.status, e.remark, u.username, a.title " + "FROM t_enrollment e " + "JOIN t_user u ON e.user_id = u.id " + "JOIN t_activity a ON e.activity_id = a.id " + where + " ORDER BY e.enroll_time DESC LIMIT ?, ?"; // 执行countSql获取总记录数total // 计算总页数:total % pageSize == 0 ? total / pageSize : total / pageSize + 1 // 执行dataSql,设置LIMIT参数为(pageNum - 1) * pageSize和pageSize // 封装到PageResult对象的data、pageNum、pageSize、totalPage字段 return result; }

PageResult里建议放五个字段:data列表、pageNum、pageSize、totalCount、totalPage。JSP页面用JSTL的c:forEach遍历data,用c:if控制上一页下一页按钮的显示。状态筛选做成下拉框,提交表单带status参数,Controller里把空字符串转成null再传给Service。

注意这里SQL用了JOIN一次性查出用户名和活动标题,而不是查出报名记录后再循环查两张表。很多同学第一版写的是循环查库,页面多了几百条也不慢,但报告里如果写“我的查询性能优化”,这就是第一个优化点。LIMIT ?, ?的参数在PreparedStatement里是第1个参数(偏移量)和第2个参数(每页条数),偏移量从0开始,所以pageNum从1开始传时记得先减1。

4.3 审核状态更新与Excel导出

管理员看到待审核记录后,操作就两种:通过或拒绝。对应的SQL再简单不过:

UPDATE t_enrollment SET status = ?, remark = ? WHERE id = ?

status传1表示通过,传2表示拒绝,remark填写拒绝原因。这里有个细节容易被忽略:状态更新后,活动列表页的“已报名人数”就会变,SQL用COUNT(*)统计t_enrollment表里status=1的记录即可,不要额外存一个报名字段在活动表里,否则两边数据对不上就是灾难。后面做导出功能时,也只导状态为通过的记录,这是业务上最合理的口径。

Excel导出用Apache POI实现。在pom.xml或手动导入jar后,核心代码:

public void exportEnrollments(HttpServletResponse resp, Integer activityId) throws IOException { List<Enrollment> list = dao.findByActivityId(activityId); try (Workbook wb = new XSSFWorkbook()) { Sheet sheet = wb.createSheet("报名名单"); String[] headers = {"序号", "用户名", "姓名", "联系电话", "报名时间", "状态"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); } int rowIdx = 1; for (Enrollment e : list) { Row row = sheet.createRow(rowIdx++); row.createCell(0).setCellValue(rowIdx - 1); row.createCell(1).setCellValue(e.getUsername()); row.createCell(2).setCellValue(e.getRealName()); row.createCell(3).setCellValue(e.getPhone()); row.createCell(4).setCellValue(e.getEnrollTime().toString()); row.createCell(5).setCellValue(e.getStatus() == 1 ? "通过" : "待审核"); } resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=enrollments.xlsx"); wb.write(resp.getOutputStream()); } }

XSSFWorkbook对应xlsx格式,HSSFWorkbook对应xls格式,课程设计用xlsx就行。Content-Disposition里的filename如果是中文,需要URL编码,否则浏览器下载时文件名乱码。POI的jar包依赖poi、poi-ooxml和xmlbeans,版本要一致,建议poi和poi-ooxml都用4.1.2,混用旧版会报NoClassDefFoundError。

这一个导出功能,能在报告里写出“数据导出模块”,还能画出POI导出Excel的流程时序图。很多同学的课程设计只做到“显示列表”就交差,你多了导出,整个系统的完整度直接高一个档次。

5. 避坑指南:Java web课程设计跑不起来的六个现场

5.1 编译能过但Tomcat启动报ClassNotFoundException

现象:Eclipse或IDEA里项目编译正常,启动Tomcat时报错,提示java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,但确认jar已经放进构建路径了。

原因:构建路径和部署路径是两个概念。IDE的构建路径只保证编译期能找到类,Tomcat运行时是从WEB-INF/lib目录加载jar的。很多同学的mysql-connector-java jar只Add到了Build Path,没有复制到WebContent/WEB-INF/lib下。

解决:把mysql驱动jar直接复制到项目的WebContent/WEB-INF/lib目录里,然后在IDE里Refresh。同时检查db.properties是否在src目录下,配置文件也会被编译到classes目录,缺了同样报错。这问题每年都有大批人踩,是课程设计翻车第一大头。

5.2 页面中文全是问号,改了编码还是乱码

现象:JSP页面显示正常,但数据库里读出来的中文是“???”或者页面显示乱码。

原因:乱码链路有四个环节,只改一个没用。JSP文件本身的编码(pageEncoding)、HTTP请求编码(POST的setCharacterEncoding、GET的URIEncoding)、数据库连接URL里的characterEncoding、MySQL表字符集,缺一不可。其中最容易漏的是GET请求编码,因为Servlet里setCharacterEncoding只对POST请求体生效。

解决:按四个环节逐个排查。JSP头部写<%@ page pageEncoding="UTF-8"%>,POST请求在Servlet开头req.setCharacterEncoding("UTF-8"),GET请求在Tomcat的conf/server.xml里找到Connector加URIEncoding="UTF-8"属性,JDBC的url里带characterEncoding=utf8,全套配合后问题必解。表已经是乱码的话,先改表字符集再重新插入数据:

ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4;

5.3 MySQL 8连接报Public Key Retrieval is not allowed

现象:运行程序第一次连数据库就抛异常,提示Public Key Retrieval is not allowed。

原因:MySQL 8默认使用caching_sha2_password认证插件,客户端首次连接时需要通过RSA公钥传输密码,连接URL少了allowPublicKeyRetrieval参数时,驱动会拒绝自动获取公钥。

解决:在JDBC的url末尾追加一个参数:

jdbc.url=jdbc:mysql://localhost:3306/signup_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

顺带说一句,alternate的话也可以把MySQL用户改成mysql_native_password插件,但作为课程设计没必要改数据库用户,加参数最快。

5.4 多人用同一台电脑,8080端口报错

现象:Tomcat启动失败,提示Port 8080 required by Tomcat v9.0 Server is already in use。

原因:机器上已经有一个进程占用8080,常见的有别的Tomcat实例、Skype、Oracle等。

解决:命令行执行netstat -ano | findstr 8080,看PID,然后在任务管理器里结束进程。但更稳妥的做法是改Tomcat端口避免冲突。在apache-tomcat/conf/server.xml里找到Connector标签,把port改为8081或8090,然后改项目访问地址。这也是课程设计答辩现场设备不统一时救命的操作。

5.5 后台页面直接访问不拦截,登录了也跳回登录页

现象:用户登录成功后,访问/admin下的页面一会儿能用一会儿跳回登录页,或者没登录也能直接看后台页面。

原因:两个反方向的问题。第一个是Filter的url-pattern写成了/admin或/index,没带/*后缀,拦截路径不匹配;第二个是登录成功代码里没有把role存进Session,Filter里拿了null,自然不放行。还有一个小概率问题:用户登录后访问的是admin.jsp而不是/admin/admin.jsp,两者路径不同,Filter根本不触发,所以后台页面统一放在/admin目录下,别用平铺结构。

解决:Filter的url-pattern用/admin/*。检查登录成功代码,确保session.setAttribute("role", user.getRole())存在,并且user对象的role正确从数据库查出。调试时在Filter的doFilter里加一行System.out.println(session.getAttribute("role")),看实际值是多少。这种问题打日志看比猜快得多。

5.6 报告查重和答辩问倒:代码是抄的,逻辑讲不清

现象:报告交上去查重率超标,或者答辩时老师问“这个分页是怎么实现的”“为什么用事务”,学生只回答“这部分是网上找的”。

原因:课程设计查重是硬指标。报告里贴了大段代码,如果代码是从GitHub或学长项目里复制的,查重率立刻爆表。另一个角度是,就算代码是自己写的,不经梳理直接上答辩,紧张状态下也会讲不清。

解决:报告里代码只贴核心片段,不贴长方法。查重系统对代码块的查重逻辑跟文字不同,整段贴上去最危险。每个模块自己动手敲一遍,不需要背下来,但要能回答三个问题:这个功能入口在哪、数据怎么流转、最后存到哪个表。答辩前拿这三问对着自己的项目过一遍,比背概念有用。如果被问住,就实话实说“这里用了xxx方案,如果进一步优化我会xxx”,老师要的是你参与过项目的证据,不是你背诵八股文。

6. 进阶技巧:验证码、防重复提交与报告撰写

主流程跑通后,往系统里加两个低成本、高回报的细节:登录页加图片验证码,报名按钮防重复提交。

图片验证码用Kaptcha开源组件最省事,在web.xml里配置一个Servlet,指定验证码图片的宽高、字符个数、干扰线密度,然后登录页面用img标签指向这个Servlet。登录时把用户输入的验证码和Session里存的验证码对比,忽略大小写。这个功能在答辩时非常显眼,因为大多数同学的登录页只是一张表单。

<servlet> <servlet-name>captcha</servlet-name> <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class> <init-param> <param-name>kaptcha.textproducer.char.length</param-name> <param-value>4</param-value> </init-param> <init-param> <param-name>kaptcha.noise.impl</param-name> <param-value>com.google.code.kaptcha.impl.NoNoise</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>captcha</servlet-name> <url-pattern>/captcha.jpg</url-pattern> </servlet-mapping>

防重复报名提交,除了数据库唯一索引,前端也可以做一层保护:用户点击“提交报名”按钮后,用JavaScript把按钮置灰并改为“提交中”,防止连点。

报告撰写方面,重点写清楚“现状分析→需求分析→数据库设计→系统实现→测试”这条线。需求分析一章把角色和用例图画清楚;数据库设计一章把ER图和每个表字段的含义列出来;系统实现按功能模块写,每个模块配一张截图加一段功能描述;测试部分写测试环境、测试用例和执行结果,不需要虚构复杂的压力测试,功能测试和兼容性测试就够。

课程设计的验收标准从来不是功能多炫,而是你能说清楚每一行代码存在的理由。这个项目做完后,把表结构和三个核心Servlet流程在脑子里过一遍,面试遇到java基础问题、数据库增删改查问题、连接池配置问题,都能拿出来当案例讲。希望这份笔记能让你少走几个弯路,把报名系统稳稳跑通,把报告写得明明白白。

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

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

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

立即咨询