简介:这是一份基于JavaWeb的试题库管理系统期末大作业,适合正在做课程设计或期末大作业的计算机专业学生,也适合希望进行JavaWeb项目实战练习的学习者。项目来自大三学期,经导师指导并通过评审,获得98分高分。压缩包共331个文件,大小19.16MB,包含108个png和59个gif界面截图、32个jsp页面、25个java源码与25个class文件、22个html静态页、17个js、6个css、6个jar,另有sql数据库脚本、xml与properties配置、docx实验报告等,内容覆盖前端页面、后端业务、数据库设计和文档说明。已有365人学习下载。借助这份资料,可完整还原试题库管理系统的实现过程,学习Servlet、JSP、JDBC等核心技术的实际应用,参考其代码分层、数据库表设计及实验报告撰写方法,为课程答辩和总结提供有力支撑;整体结构清晰,既能作为课程设计的参考模板,也能帮助初学者提升JavaWeb项目实战能力。
1. 期末大作业选JavaWeb试题库,是聪明的选择
很多计算机专业的大三学生,期末大作业选来选去最后都落在“试题库管理系统”上。原因很简单:它能把一学期的JavaWeb知识点串起来——用户登录、权限控制、试题的增删改查、分页、导出Excel,全是课程设计和面试里最爱看的点。这套JavaWeb试题库管理系统源码配套MySQL数据库脚本和实验报告,属于完整的JavaWeb项目完整案例,导师评审给了98分。我拿到它之后,第一件事不是跑起来看界面,而是把分层结构过了一遍,发现它就是大三课设的“标准答案”:Servlet + JSP + JDBC + DAO,不花哨但足够稳。下面按设计、核心代码、数据库脚本、答辩技巧的顺序拆一遍,你改造成自己的大作业时能省不少力气。
2. DAO、Servlet、JSP三层各管什么:先把骨架搭明白
2.1 实体与表结构:角色、试题、试卷的关系
类清单里的UserDao、StuDao、TeaDao已经暗示了项目角色:学生、教师、管理员。数据库设计上,我首先会拆成三张核心表:sys_user保存登录账号密码,role区分admin、teacher、student;student和teacher表冗余姓名、学号、工号;question表存题目,type标记单选/多选/判断/简答,difficulty用于按难度过滤,knowledge_point支持知识点维度筛选。如果需要组卷,再加一张paper表和一个中间表。下面是简化后的表结构,实际项目里还会加created_time、updated_time这类审计字段。
| 表名 | 核心字段 | 作用 |
|---|---|---|
| sys_user | id, username, password, role | 统一登录认证 |
| student | id, user_id, name, student_no | 学生信息 |
| teacher | id, user_id, name, teacher_no | 教师信息 |
| question | id, content, type, difficulty, answer, knowledge_point | 试题主体 |
| paper | id, title, teacher_id, create_time | 组卷记录 |
| paper_question | paper_id, question_id | 试卷与试题多对多关联 |
这个设计足够支撑“登录鉴权 + 试题维护 + 组卷答题”三个课设高频模块。评审老师问“为什么不做一张大表把所有信息塞进去”,你可以回答:第三范式通过拆分角色和试题主题,避免数据冗余,后续加一个新题型不用改表结构,只需要在type字段上扩展枚举值。这种话术在答辩现场很能说明你理解关系型数据库。
2.2 JDBCUtils:最容易被低估的连接管理工具
类清单里的JDBCUtils是整个项目的地基。它做的事情很简单:从classpath下的db.properties读取driver、url、username、password,用静态代码块注册驱动,然后提供getConnection()和close()。很多同学只关注业务功能,忽略连接管理,导致演示多跑几次就卡死。这段代码在课程设计里出现频率很高,我一般会建议把close方法写成重载,因为漏关ResultSet和Statement是连接泄漏的主要来源。
public class JDBCUtils { private static String url; private static String user; private static String password; static { try { Properties props = new Properties(); props.load(JDBCUtils.class.getClassLoader() .getResourceAsStream("db.properties")); Class.forName("com.mysql.cj.jdbc.Driver"); url = props.getProperty("jdbc.url"); user = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, 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) { } } } }Class.forName在MySQL 8以后虽然可以不显式写,但保留它能让老版本数据库驱动也能跑。db.properties里的jdbc.url必须带上useSSL=false和serverTimezone=Asia/Shanghai,否则MySQL 8会给你报SSL和时区错误。close方法里三个try分开写是必要的——rs关闭失败不能影响stmt和conn的关闭。这个工具类虽然基础,却是后面所有DAO稳定运行的保证。如果你想扩展连接池,后续在getConnection里换成Druid或HikariCP,DAO层代码可以完全不动,这就是封装的价值。
2.3 DAO层:每个Dao管一张表,别越权
UserDao负责sys_user,StuDao负责student,TeaDao负责teacher,QuestionDao负责question。DAO只暴露有业务语义的方法,比如UserDao.findByUsername()、QuestionDao.pageList(),不能把Connection对象漏给Servlet。评审老师特别爱问“为什么Servlet不直接写JDBC”,标准答案就是职责分离和可测试性:换数据库时只改DAO实现和连接配置,Servlet和JSP完全不受影响。
类清单里的SecDao应该是负责安全相关的数据操作,比如记录登录日志或查询权限。我习惯把权限判断放在Servlet入口统一做,而不是在DAO中层层嵌套if。比如教师端操作前先判断session里的角色是否为teacher,这样DAO只需要关心SQL本身,逻辑更清晰。DAO层是所有功能的数据出口,这一层的命名规范决定了整个项目可读性,把findAll、findById、updateById这类方法名统一起来,后面写导出功能会非常顺。
2.4 Servlet层:一个Servlet处理一类请求,配合导出专用Servlet
类清单里SecServlet和TeaServlet属于“一类一Servlet”模式,用一个action参数区分具体操作,避免为几十个功能写几十个Servlet。这种设计在课设里很实用,也方便在web.xml或@WebServlet里统一管理路径。下面是我对这个系统请求分发的理解:
| Servlet | 典型路径 | 职责 |
|---|---|---|
| SecServlet | /sec/login, /sec/logout, /sec/auth | 登录鉴权、会话控制 |
| TeaServlet | /tea/question/add, /tea/question/update, /tea/question/delete | 教师端试题维护 |
| StuServlet | /stu/paper/list, /stu/paper/submit | 学生端组卷与答题 |
| ExportquestionServlet | /export/question | 导出题库Excel |
| ExportStudentServlet | /export/student | 导出学生成绩或名单 |
这种拆分的好处是:每个Servlet的职责边界清楚,action参数既可以是method方法的区分,也可以配合表单隐藏字段。比如TeaServlet的doPost里写一个switch或者if-else链,按action的值调用不同方法。这里我会刻意把参数校验和业务逻辑分开,参数错误直接返回JSON错误信息,不让异常堆栈暴露到页面上。
@WebServlet("/tea/action") public class TeaServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("add".equals(action)) { // 调用 QuestionDao.insert(question) } else if ("update".equals(action)) { // 调用 QuestionDao.update(question) } else if ("delete".equals(action)) { // 调用 QuestionDao.deleteById(id) } resp.sendRedirect("questionList.jsp"); } }action参数从请求中获取后必须做空值判断,否则未传参时会走进else分支甚至抛空指针。add对应新增,update对应修改,delete对应删除,三个分支都通过RequestDispatcher或重定向回到列表页。这样写的好处是在实验报告里只需要画一张请求流程图,把action的每种取值对应到DAO方法,老师就能看懂整个教师端逻辑。
3. 登录、分页、导出Excel:核心功能的代码级拆解
3.1 登录与Session控制:避免表单重复提交
登录功能是javaweb项目里被问得最多的功能。源码里SecServlet负责登录,我把它拆成“查用户—比对密码—写Session—跳转”四步。下面是一个简化版实现,核心思想用PreparedStatement防SQL注入,用redirect防止刷新时重复提交表单。
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao dao = new UserDao(); User user = dao.findByUsername(username); if (user != null && user.getPassword().equals(password)) { req.getSession().setAttribute("currentUser", user); resp.sendRedirect("index.jsp"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } }findByUsername内部使用PreparedStatement,SQL结构固定,用户输入只作为参数传递,从根上避开拼接注入。这里密码比对是明文,课设演示足够,但答辩时最好主动说一句“实际生产会用BCrypt加盐”,能体现你有安全意识。登录失败时把error放到request域再forward回login.jsp,这样错误信息只在当前请求存在,刷新后不会残留。登录成功后用redirect跳转,地址栏会变成index.jsp,同样能防止重复提交。学生和教师可以靠user.getRole()在index.jsp里显示不同菜单,这一点非常实用。
3.2 分页查询:LIMIT + COUNT,别一次全查出来
题库列表如果没有分页,几万条数据会让浏览器直接卡死。常见做法是count和data两条SQL分开查,一个拿总量算总页数,一个取当前页数据。下面是简化版DAO分页逻辑:
public PageResult<Question> pageList(int page, int size, String keyword) { PageResult<Question> result = new PageResult<>(); String countSql = "SELECT COUNT(*) FROM question WHERE content LIKE ?"; String dataSql = "SELECT id, content, type, difficulty, answer " + "FROM question WHERE content LIKE ? " + "LIMIT ? OFFSET ?"; String like = "%" + keyword + "%"; try (Connection conn = JDBCUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(countSql)) { ps.setString(1, like); ResultSet rs = ps.executeQuery(); if (rs.next()) { result.setTotal(rs.getInt(1)); } try (PreparedStatement ps2 = conn.prepareStatement(dataSql)) { ps2.setString(1, like); ps2.setInt(2, size); ps2.setInt(3, (page - 1) * size); ResultSet rs2 = ps2.executeQuery(); List<Question> list = new ArrayList<>(); while (rs2.next()) { Question q = new Question(); q.setId(rs2.getInt("id")); q.setContent(rs2.getString("content")); q.setType(rs2.getString("type")); list.add(q); } result.setList(list); } } catch (SQLException e) { e.printStackTrace(); } return result; }countSql先算总条数,dataSql再按limit取当前页。LIMIT后的两个参数,第一个是pageSize每页行数,第二个是offset偏移量,公式是(page - 1) * size,也就是说第一页偏移0,第二页偏移size。keyword参数两边手动拼接%,但赋值时用PreparedStatement的setString,这样用户在输入框里输入%会被当成普通字符处理。如果你的数据量超过几十万,这种基础分页会有性能问题,但作为期末大作业完全够用。前端页面拿到total后除以size向上取整生成页签,再拼接“page=2&keyword=xxx”的链接就能翻页。
3.3 导出Excel:ExportquestionServlet的关键写法
项目里的ExportteacherServlet、ExportquestionServlet、ExportStudentServlet三个类,本质都是“查列表 + 写文件头 + 输出流”。用Apache POI导出Excel是最常见的方案,核心代码如下:
@WebServlet("/export/question") public class ExportQuestionServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { QuestionDao dao = new QuestionDao(); List<Question> questions = dao.findAll(); resp.setContentType("application/vnd.ms-excel;charset=UTF-8"); resp.setHeader("Content-Disposition", "attachment;filename=questions.xlsx"); Workbook wb = new XSSFWorkbook(); Sheet sheet = wb.createSheet("题库"); String[] headers = {"ID", "题型", "题干", "答案", "难度"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } for (int i = 0; i < questions.size(); i++) { Row row = sheet.createRow(i + 1); Question q = questions.get(i); row.createCell(0).setCellValue(q.getId()); row.createCell(1).setCellValue(q.getType()); row.createCell(2).setCellValue(q.getContent()); row.createCell(3).setCellValue(q.getAnswer()); row.createCell(4).setCellValue(q.getDifficulty()); } wb.write(resp.getOutputStream()); wb.close(); } }Content-Disposition里的filename建议用URLEncoder.encode转码,否则IE和Chrome容易出现中文文件名乱码。XSSFWorkbook对应.xlsx,HSSFWorkbook对应旧版.xls,课设里随便选一个,引入对应POI依赖即可。导出前一定要做权限校验,否则任何知道URL的人都能把题库拉走。ExportteacherServlet、ExportStudentServlet的逻辑与这个完全一样,只是查的表不同,完全可以用一个ExportServlet加type参数统一复用,能少写两个类。
4. 数据库脚本、JDBC配置和实验报告:不到位的部分怎么补
4.1 建表SQL和初始化数据:演示前先把这几条跑通
源码里携带的数据库脚本质量,直接决定你能不能在一分钟内核到项目。拿到脚本后,我习惯先检查三条:是否有CREATE DATABASE语句,是否有初始管理员账号,是否有至少十几条试题示例数据。下面是一个简化但完整的初始化脚本,可以作为核对标准:
CREATE TABLE IF NOT EXISTS sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL ); CREATE TABLE IF NOT EXISTS question ( id INT PRIMARY KEY AUTO_INCREMENT, content VARCHAR(500) NOT NULL, type VARCHAR(10) NOT NULL COMMENT '单选/多选/判断/简答', difficulty TINYINT DEFAULT 1, answer VARCHAR(500), knowledge_point VARCHAR(50) ); INSERT INTO sys_user (username, password, role) VALUES ('admin', '123456', 'admin'); INSERT INTO question (content, type, difficulty, answer, knowledge_point) VALUES ('Java中继承的关键字是?', '单选', 1, 'extends', '面向对象');type字段用字符串更方便JSP下拉框展示,difficulty用TINYINT方便按“易中难”排序。INSERT语句至少准备一条admin账号和十几道覆盖不同知识点的题,否则演示时列表页是空的,印象分会掉很多。如果你需要修改表结构,比如给question加一个category字段,可以直接用ALTER TABLE,也可以在navicat之类的数据库工具里可视化操作,然后把新的建表语句更新回脚本文件。数据库课程设计里最容易被查的就是有没有合理的注释和初始化数据,这两点都做到了,实验报告里放上截图就是加分项。
4.2 JDBC连接配置的坑:mysql版本、时区、中文乱码
很多同学在别人机器上跑得好好的,到自己电脑上就报错,90%是db.properties配置问题。下面是一份验证过的MySQL 8配置,注意url里已经处理了时区和编码:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/question_bank?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=yourpassword如果用的是MySQL 5.x,驱动类名要改成com.mysql.jdbc.Driver,同时url里不需要serverTimezone参数。下面是我整理过的常见错误对照表:
| 现象 | 根因 | 处理方式 |
|---|---|---|
| ClassNotFoundException | 驱动类名与mysql版本不匹配 | 更换driver配置或依赖版本 |
| SQLException: serverTimezone | MySQL 8时区未指定 | url加serverTimezone=Asia/Shanghai |
| JSP显示问号 | 连接编码不是utf8 | url加characterEncoding=utf8,JSP也设UTF-8 |
| Communications link failure | 3306端口未开放或IP写错 | 检查mysql服务与防火墙配置 |
| Public Key Retrieval is not allowed | MySQL 8默认认证插件 | url加allowPublicKeyRetrieval=true |
这里有个我经常提醒别人的细节:不能只在连接串里写utf8,mysql数据库本身和表的字符集也要是utf8mb4,否则emoji和生僻字还是会丢。运行前先打开数据库执行几条查询,确认连接成功后,再去跑Tomcat,省得来回找错。
4.3 实验报告里评审盯的几个点:从模板到高分
这套资源附带的实验报告拿到了98分,说明它的结构和学校评分表是匹配的。我建议你把报告按四段式改:需求分析、数据库设计、核心功能实现、测试与总结。其中数据库设计要放ER图和建表SQL;核心功能实现一定贴代码但不贴大段完整类,只贴关键方法的逻辑;测试部分给出测试用例表格更直观。
| 用例编号 | 操作 | 预期结果 | 实际结果 |
|---|---|---|---|
| TC-001 | 管理员登录 | 跳转管理员首页 | 通过 |
| TC-002 | 教师新增试题 | 列表出现新记录 | 通过 |
| TC-003 | 学生提交试卷 | 成绩记录生成 | 通过 |
评审老师普遍对两个点感兴趣:一是“预编译防止SQL注入”,二是“连接关闭顺序”。我写报告时会把这两点用单独的小节说明,并配一张简化流程图。如果你的实验报告是网上模板改的,至少要把项目名、表名、类名全部替换成源码里的真实名称,否则一眼就能看出没有亲自跑过项目。高分的逻辑很简单:代码能跑通,设计能讲清,报告里能看到踩坑记录,三点齐了,98分就不稀奇。
5. 演示和答辩前,再抠四个边界细节
演示环节最怕的不是功能不全,而是“一操作就报错”的尴尬。第一个建议是把所有SQL改成PreparedStatement,并把拼接SQL的代码全部删掉。你可以用IDE的全局搜索找Statement或createStatement,凡是出现的地方都换成prepareStatement,这样即使老师现场输入特殊字符也不会崩。
第二个建议是给空列表页面加一个友好提示。比如题库无数据时显示“暂无题目,请先添加题目”,而不是让一个白屏掉在那里。更稳妥的做法是做一个questionList.jsp的局部片段:如果${empty pageResult.list}成立,显示一个提示按钮跳转到添加页。这个改动不影响原有逻辑,但演示观感提升一个档次。
第三个建议是给所有导出Servlet加同一套权限校验。复制下面这段到doGet开头,能挡住未登录用户直接访问导出接口:
HttpSession session = req.getSession(false); if (session == null || session.getAttribute("currentUser") == null) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; }第四个建议是固定管理员账号的展示位置。答辩前重新初始化数据库,确保账号admin/123456可用,并把账号信息写在演示文稿的第一页。老师上台后第一件事往往是登录系统,你直接把账号密码给出来,甚至提前录好一段演示视频备用,遇到网络或环境问题也有回退方案。把这些边界处理完,再跑一遍从登录到导出Excel的完整流程,你会发现评审问的问题大多集中在这几个改动点上,而不是业务正逻辑。
本文还有配套的精品资源,点击获取