简介:基于JSP技术实现的CIMS课程设计源码,面向高校计算机专业学生,目标是帮助初学者理解动态网站开发与业务系统设计流程,可用于课程设计、毕业设计或实训项目参考。压缩包共有212个文件,大小15.2MB,以XML配置文件、JAR依赖包、JSP页面、JAVA源文件为主,同时包含编译后的CLASS文件、CSS样式表、JavaScript脚本、属性文件与SQL脚本等辅助资源,目录结构清晰,可以按模块对照学习。当前已有103人学习下载。项目完整展示了从前端页面展示、后端业务逻辑到数据库操作的实现细节,代码采用控制器、服务层等分层写法,涵盖用户管理、客户管理等典型功能;通过阅读和运行源码,能够掌握JSP页面中标签使用、请求处理、前端样式与后端协同开发的思路,并能结合SQL脚本理解数据初始化过程,对课程设计的实现与排错有直接参考价值,适合边阅读边动手实践。
1. JSP写的CIMS课程设计:先弄清楚这个缩写到底指什么
CIMS这个缩写,在不同课程里指的东西差别很大。在制造业信息化课程里它是Computer Integrated Manufacturing System,计算机集成制造系统;但在Java Web课程设计最常见的使用场景里,它是Course Information Management System,课程信息管理系统。不管是哪一种,落到JSP这门课上,核心要解决的问题是一样的:用JSP+Servlet+JDBC这套技术栈,做一个能登录、能增删改查、有权限区分、能导出统计结果的完整Web项目。这套源码的价值不在于用了多新的框架,而在于它是课程设计里最标准的三层结构,教师验收时能一步步讲清楚请求怎么走、数据怎么存、权限怎么控。
适合看这篇文章的人是两类。一类是正在做Java Web课程设计的学生,手里拿到一套带完整源码的项目,但不知道从哪开始看、改哪些地方能变成自己的设计;另一类是想自己从零搭建CIMS但不想一上来就上SpringBoot的初学者,想先理解Servlet和JSP的原生工作机制。本文不教你怎么写“精美”的框架代码,只讲怎么把一套基于JSP的CIMS课程设计源码跑起来、看得懂、改得动、答得上老师的提问。
2. 这套源码的技术栈为什么是JSP+Servlet+JDBC:选型理由与项目结构
2.1 课程设计不选SpringBoot的四个现实理由
我在带Java课程设计时最常被问的一句话是:“老师,我能不能直接用SpringBoot写?”能,但我不建议你用。不是SpringBoot不好,而是课程设计的验收指标通常不是“用了最新框架”,而是“这个项目是不是你自己写的、核心逻辑能不能讲清楚”。SpringBoot的自动配置让人省心,但也把Servlet的生命周期、请求转发、Session管理、JDBC连接这些最基础的知识盖住了。答辩时被问一句“你的登录校验是怎么拦截的”“你的数据库连接有没有关闭”,如果答不上来,框架反而成了减分项。
第二个理由是部署环境的不可控性。课程设计实验室里的Tomcat版本、JDK版本经常和你在家用的不一样。JSP+Servlet+JDBC这套组合对版本兼容性要求很低,JDK 8、Tomcat 8.5/Tomcat 9就能稳定运行。而SpringBoot项目一旦JDK版本对不上,启动直接报错,答辩现场翻车是常有的事。
第三个理由是代码量。CIMS课程设计的功能范围一般是:用户登录、课程信息维护、选课退课、权限区分、简单统计。用JSP+Servlet+JDBC写,整个项目的文件数在20到40个之间,每个文件都能在答辩时单独展示。用SpringBoot写,自动生成的目录结构本身就会让一部分代码藏进框架里,讲解时反而要解释“这里为什么不用写代码”,比解释“这段代码做了什么”更费劲。
第四个理由是源码量。课程设计源码的评分通常会看代码的完整度和注释量,JSP项目的代码是摊开来的,从DBUtil到Servlet到JSP页面,每一层都能看到实际内容。SpringBoot项目里大量逻辑靠注解驱动,看源码的人如果对注解不熟,读起来很吃力。所以在课程设计这个特定场景下,JSP+Servlet+JDBC不是退步,而是最稳妥的选择。
2.2 项目目录结构与URL映射规划
一套合格的CIMS源码,目录结构应该遵循“WebContent(或src/main/webapp)下放JSP页面、src下放Java类、WEB-INF/lib下放依赖Jar包”的约定。我见过很多学生的源码翻车,不是代码写错,而是JSP放错了位置导致404,或者jar包放在了别的位置导致运行时报ClassNotFoundException。
标准结构是这样:
CIMS/ ├── src/ │ ├── com/cims/bean/ // 实体类:User, Course, Enrollment │ ├── com/cims/dao/ // 数据访问类:UserDao, CourseDao, EnrollmentDao │ ├── com/cims/servlet/ // 控制器:LoginServlet, CourseServlet, EnrollmentServlet │ ├── com/cims/filter/ // 过滤器:LoginFilter, EncodingFilter │ └── com/cims/util/ // 工具类:DBUtil ├── WebContent/ │ ├── WEB-INF/ │ │ ├── lib/ // mysql-connector-java-8.0.x.jar │ │ └── web.xml // Servlet映射与欢迎页面配置 │ ├── css/ // 静态样式(可选) │ ├── login.jsp // 登录页 │ ├── index.jsp // 首页/课程列表 │ ├── course_add.jsp // 添加课程页面 │ └── course_edit.jsp // 修改课程页面 └── sql/ └── cims.sql // 建库建表脚本URL映射规划上,我习惯用“资源名即路径”的方式。登录用/loginServlet,课程列表用/courseServlet?action=list,添加课程用/courseServlet?action=add。这种用一个Servlet接收多个action参数的风格,适合课程设计,因为文件数量少,每个Servlet里用switch区分动作,比每个功能建一个Servlet更容易讲解。
提示:如果用的是Eclipse Dynamic Web Project,Java文件的路径写入到
src下;如果用的是IDEA,默认是src/main/java和src/main/webapp。两种工程的源码结构等价,但导入时不要搞混。
3. 把CIMS的核心功能拆成三层:建表、DBUtil、登录与权限
3.1 数据库设计:用户、课程、选课三张表的字段与关系
CIMS系统不论偏制造还是偏教学管理,数据库核心就三张表:用户表、课程表、选课关系表。设计时注意“不要为了关系而关系”——课程设计阶段把字段控制在6到8个以内,反而更容易把SQL写清楚。
-- 用户表:区分管理员和普通用户 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role CHAR(1) DEFAULT '1', -- 0=管理员, 1=普通用户 realname VARCHAR(30), -- 真实姓名,用于页面展示 created_at DATETIME DEFAULT NOW() ); -- 课程表:CIMS里的核心业务对象 CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, -- 课程编号 course_name VARCHAR(50) NOT NULL, -- 课程名称 credit DECIMAL(3,1) DEFAULT 2.0, -- 学分 teacher VARCHAR(30), -- 授课教师 capacity INT DEFAULT 50, -- 课程容量 selected_count INT DEFAULT 0, -- 已选人数 remark VARCHAR(200) -- 备注 ); -- 选课表:学生和课程的多对多关系 CREATE TABLE t_enrollment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, course_id INT NOT NULL, enroll_time DATETIME DEFAULT NOW(), UNIQUE KEY uk_user_course (user_id, course_id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_course FOREIGN KEY (course_id) REFERENCES t_course(id) );建表脚本里的关键设计点在两处:一是t_user.role用字符型CHAR(1)而不是INT,因为角色在JSP页面上显示时直接就能用“0是管理员、1是普通用户”来解释,不用再做类型转换;二是t_enrollment表加了联合唯一约束uk_user_course,防止同一个人重复选同一门课。这个约束在代码层面也有判断,但数据库约束是最后一道防线,缺了它就算代码有漏洞也不会产生脏数据。
selected_count这个字段是冗余设计。选课时执行UPDATE t_course SET selected_count = selected_count + 1 WHERE id = ?来维护它。有人会问为什么不直接SELECT COUNT(*) FROM t_enrollment统计?因为课程列表页每次加载都要显示每门课的已选人数,如果用聚合查询,一次列表加载就要执行N+1次子查询,对课程设计规模的系统虽然影响不大,但答辩时讲“用冗余字段避免N+1查询”是一个很好的加分点。
3.2 JDBC工具类:PreparedStatement + 连接关闭的细节
DBUtil是整套JSP源码里最不该写错的文件。它有两种常见写法,一种是用DriverManager手动获取连接,另一种是用DataSource连接池。课程设计项目不要用连接池,因为Tomcat配置数据源需要改context.xml,答辩环境里配置文件一旦出错,整个项目起不来,排查难度大。手动获取连接虽然每次请求都建立物理连接,但CIMS这种并发量个位数的系统根本感觉不到差异。
package com.cims.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * 数据库连接工具类 * 课程设计项目采用DriverManager方式,简化部署步骤 */ public class DBUtil { // 按自己的MySQL版本调整驱动类名 private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // MySQL 8.0以上版本需要带serverTimezone参数 private static final String URL = "jdbc:mysql://localhost:3306/cims?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,请检查WEB-INF/lib下的jar包"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } /** * 关闭资源:ResultSet -> PreparedStatement -> Connection * 关闭顺序必须从里到外反着来 */ public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps != null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }这个工具类里有三个细节是答辩时老师常追问的。第一,Class.forName(DRIVER)在JDBC 4.0以后其实可以省略,因为驱动包里的META-INF/services/java.sql.Driver文件会自动注册驱动,但保留它没有任何坏处,反而能提前暴露“jar包没放对位置”的问题。第二,URL里的serverTimezone=Asia/Shanghai是MySQL 8.0的必填参数,不写会报时区错误,这是课程设计源码里最常见的运行时报错之一。第三,close方法把SQLException吞掉是故意的,因为关闭阶段的异常对业务流程没有影响,打印堆栈反而会让控制台信息混乱。
3.3 登录流程与Session管理:从login.jsp到LoginServlet
登录是验证整条请求链路是否打通的第一关,也是CIMS源码中最能体现“JSP+Servlet协作”的部分。登录页面的表单提交到LoginServlet,Servlet负责校验用户名密码,成功后把用户信息放进Session,再转发到首页。
<%-- login.jsp 登录页面核心部分 --%> <form action="${pageContext.request.contextPath}/loginServlet" method="post"> <div> <label>用户名</label> <input type="text" name="username" required /> </div> <div> <label>密码</label> <input type="password" name="password" required /> </div> <div> <label>角色</label> <select name="role"> <option value="0">管理员</option> <option value="1">普通用户</option> </select> </div> <button type="submit">登 录</button> <p style="color: red;">${requestScope.msg}</p> </form>这里用${pageContext.request.contextPath}来拼表单提交路径。很多初学者直接写action="loginServlet",如果项目部署在根路径下没问题,但如果部署后路径变成了http://localhost:8080/CIMS/loginServlet,不带项目名的写法就会404。pageContext.request.contextPath是EL表达式里的标准写法,用来动态获取项目根路径。${requestScope.msg}用来回显登录失败的提示信息,这是JSP里最基础的数据回显。
package com.cims.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.cims.bean.User; import com.cims.dao.UserDao; @WebServlet("/loginServlet") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; private UserDao userDao = new UserDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 设置请求编码,解决POST提交中文乱码 request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String role = request.getParameter("role"); // 2. 基本非空校验(数据库层还会做二次校验) if (username == null || password == null || username.trim().isEmpty()) { request.setAttribute("msg", "用户名和密码不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 3. 调用DAO层查询并校验密码 User user = userDao.findByUsername(username); if (user != null && user.getPassword().equals(password) && role.equals(user.getRole())) { // 4. 登录成功:把用户对象存入Session,不存密码字段以外的敏感数据 HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); // 30分钟超时 response.sendRedirect(request.getContextPath() + "/courseServlet?action=list"); } else { // 5. 登录失败:回到登录页并回显错误信息 request.setAttribute("msg", "用户名、密码或角色不匹配"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }这段代码有四个面试级细节。request.setCharacterEncoding("UTF-8")只对POST请求有效,GET请求的编码是Tomcat在server.xml里控制的,这是很多人中文乱码的根源。登录成功后用sendRedirect而不是forward,原因是转发浏览器地址栏不变,用户刷新页面会再次提交表单造成重复登录;重定向则是重新发起一次GET请求。session.setMaxInactiveInterval是Session超时时间,不设置的话默认是Tomcat配置的30分钟,这里显式设置是为了在答辩时能说出“我控制了Session的有效期”。密码校验用字符串equals是课程设计够用的方案,但要在代码注释或答辩时说明“实际生产环境必须用加盐哈希存储密码”。
4. JSP页面与Servlet的协作:表单提交、数据回显与列表循环
4.1 用request.getParameter收集表单数据,为什么不用表达式语言也能跑
课程设计源码里最容易写“飘”的部分就是JSP页面的取值方式。很多教程会教你在JSP里写${param.xxx}取表单参数,但在Servlet里必须用request.getParameter("xxx")。这个区分搞不清,页面提交的数据在Servlet里取到的是null。
Servlet接收表单数据的标准做法是写在doPost里。如果在doGet里接收POST表单数据,Tomcat不会报错,但request.getParameter取回来的永远是null。实现登录、添加课程、提交选课这些功能时,表单的method="post"和Servlet的doPost方法必须配对。
// CourseServlet 中添加课程的入口 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); // 同一个Servlet根据action参数分发到不同处理方法 if ("add".equals(action)) { addCourse(request, response); } else if ("update".equals(action)) { updateCourse(request, response); } else if ("delete".equals(action)) { deleteCourse(request, response); } else { response.sendRedirect(request.getContextPath() + "/courseServlet?action=list"); } } private void addCourse(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 从请求中收集表单字段 String courseNo = request.getParameter("courseNo"); String courseName = request.getParameter("courseName"); String credit = request.getParameter("credit"); String teacher = request.getParameter("teacher"); String capacity = request.getParameter("capacity"); // 参数校验:空值和非法数字检查 if (courseNo == null || courseName == null || courseNo.trim().isEmpty() || courseName.trim().isEmpty()) { request.setAttribute("msg", "课程编号和课程名称不能为空"); request.getRequestDispatcher("/course_add.jsp").forward(request, response); return; } // 这里应该有BigDecimal和Integer转换的try-catch,课程设计可以简化为 // 把转换失败后的异常信息直接作为错误提示返回 Course course = new Course(); course.setCourseNo(courseNo); course.setCourseName(courseName); course.setCredit(Double.parseDouble(credit)); course.setTeacher(teacher); course.setCapacity(Integer.parseInt(capacity)); boolean ok = courseDao.add(course); if (ok) { // 添加成功:重定向到列表页,避免F5重复提交 response.sendRedirect(request.getContextPath() + "/courseServlet?action=list"); } else { // 添加失败:把用户填了一半的数据回显到表单 request.setAttribute("course", course); request.setAttribute("msg", "课程编号已存在,添加失败"); request.getRequestDispatcher("/course_add.jsp").forward(request, response); } }这个Servlet里我最想强调的是“添加失败后的数据回显”。很多源码在添加失败后直接跳到course_add.jsp,用户填写的内容全丢了,体验极差。正确做法是把Course对象放进request.setAttribute("course", course),然后在JSP页面里用${course.courseNo}回填到input标签的value属性里。这体现了Servlet和JSP各自职责的划分:Servlet负责处理数据,JSP负责把数据展示出来。
4.2 JSP列表页面的for循环输出:out.print和 配合的写法
课程列表页是CIMS系统的门面,也是JSP源码中scriptlet代码最密集的地方。课程设计阶段不要求用JSTL标签库,用<% for(...) %>的传统scriptlet写法反而更容易让新手理解循环与页面的关系。但要注意一点:JSP页面里不要出现业务逻辑,比如计算库存剩余量、判断某个用户是否已选课,这些应该在Servlet里准备好,JSP只负责显示。
<%-- course_list.jsp 课程列表核心循环 --%> <%@ page import="java.util.List" %> <%@ page import="com.cims.bean.Course" %> <%@ page import="com.cims.bean.User" %> <% // 从Servlet转发的request中获取列表数据 List<Course> courseList = (List<Course>) request.getAttribute("courseList"); User loginUser = (User) session.getAttribute("loginUser"); if (courseList == null) { response.sendRedirect(request.getContextPath() + "/courseServlet?action=list"); return; } %> <table border="1" cellspacing="0" cellpadding="6"> <tr> <th>课程编号</th> <th>课程名称</th> <th>学分</th> <th>教师</th> <th>容量</th> <th>已选</th> <th>操作</th> </tr> <% for (Course c : courseList) { %> <tr> <td><%= c.getCourseNo() %></td> <td><%= c.getCourseName() %></td> <td><%= c.getCredit() %></td> <td><%= c.getTeacher() %></td> <td><%= c.getCapacity() %></td> <td><%= c.getSelectedCount() %></td> <td> <a href="<%= request.getContextPath() %>/courseServlet?action=edit&id=<%= c.getId() %>">修改</a> <a href="<%= request.getContextPath() %>/courseServlet?action=delete&id=<%= c.getId() %>" onclick="return confirm('确定删除该课程?')">删除</a> </td> </tr> <% } %> </table>这段代码有三处容易被新手改坏的细节。第一处是页面顶部的指令<%@ page import="..." %>,JSP页面用到Java类就必须导入,漏了会直接报编译错。第二处是列表页开头对courseList的判空处理,这个判断的意义在于:如果用户没有请求courseServlet?action=list直接访问course_list.jsp,页面会重定向到正确入口,防止NullPointerException。第三处是onclick="return confirm(...)",这是前端最简单的防误删手段,删除操作属于不可逆操作,必须加二次确认。
<tr>标签被<% for %>和<% } %>包住的形式看起来有点丑,但这正是JSP原始语法的特点。大多数课程设计项目为了控制在“让老师能看懂”的范围内,都会使用这种写法。如果你愿意引入JSTL的<c:forEach>,会让页面干净很多,但这需要在WEB-INF/lib里多放两个jstl的jar包,在不熟悉的项目里不推荐。
4.3 Filter统一做登录校验:让每个页面不再重复写Session检查
CIMS系统里有登录页、课程列表页、课程编辑页、选课页等多个页面,如果不加拦截,用户直接在浏览器地址栏输入courseServlet?action=list就能跳过登录访问。最LOW的解决方案是每个Servlet开头都复制粘贴一段Session判断代码,但更好的做法是用一个Filter统一拦截,这也是源码质量评分的分水岭。
package com.cims.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.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * 登录过滤器:拦截所有请求,除了login.jsp和loginServlet */ @WebFilter("/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 获取请求路径 String uri = request.getRequestURI(); String contextPath = request.getContextPath(); String path = uri.substring(contextPath.length()); // 白名单:这些路径无需登录即可访问 boolean isAllowed = path.equals("/login.jsp") || path.equals("/loginServlet") || path.startsWith("/css/") || path.endsWith(".js") || path.endsWith(".png") || path.endsWith(".gif") || path.endsWith(".jpg") || path.endsWith(".ico"); if (isAllowed) { chain.doFilter(req, resp); return; } // 非白名单请求:检查Session中是否有登录用户 HttpSession session = request.getSession(false); // 不创建新Session Object loginUser = (session != null) ? session.getAttribute("loginUser") : null; if (loginUser == null) { // 未登录:跳转到登录页 response.sendRedirect(contextPath + "/login.jsp"); return; } // 已登录:放行请求 chain.doFilter(req, resp); } public void init(FilterConfig config) throws ServletException { } public void destroy() { } }这个Filter解决的是“哪些请求不需要登录”和“哪些请求必须登录”的边界问题。白名单的设计很关键,因为登录页本身、登录的Servlet、CSS和图片这些静态资源不能拦截,否则会出现“登录页面加载不出来样式”或者“登录请求被拦截”的死循环。request.getSession(false)这个写法是第二个细节,false参数表示“如果当前没有Session就返回null而不是创建一个”,如果不加false,未登录用户访问任意页面都会触发Session创建,每个请求都会生成一个无用的Session对象,白白占用服务器内存。
5. 课程设计源码的避坑手册:从部署到验收的五个典型问题
5.1 页面404:WEB-INF下的JSP不能直接URL访问
现象:项目部署后访问首页正常,但从列表页点击“登录”跳转时报404,地址栏输入http://localhost:8080/CIMS/WEB-INF/login.jsp也打不开页面。
原因:WEB-INF目录是Servlet容器的受保护目录,容器禁止客户端直接访问该目录下的任何文件。这是Servlet规范的安全约定。很多学生习惯把JSP页面全放进WEB-INF里防止被直接访问,但同时也把自己坑了——如果web.xml或Servlet转发路径写错,页面永远无法通过URL直接访问验证。
解决:课程设计方案中,把login.jsp、index.jsp这类允许直接访问的JSP放在webapp根目录下;把不允许直接访问的JSP片段(被include的header.jsp、footer.jsp、错误页)放在WEB-INF下。在Servlet中使用request.getRequestDispatcher("/WEB-INF/course_edit.jsp").forward(request, response)可以合法访问到WEB-INF下的文件,但直接输入URL不行。
5.2 中文乱码:POST提交乱码和GET提交乱码的两种解法
现象:添加课程时填写中文课程名称,保存到数据库后显示“???”,或者从数据库查出来在页面上显示为乱码;登录时用户名为中文也会乱码。
原因:POST请求乱码是请求体编码不是UTF-8,GET请求乱码是URL参数编码由Tomcat的URIEncoding控制。很多学生只执行request.setCharacterEncoding("UTF-8")就以为万事大吉,遇到GET请求乱码就彻底懵了。
解决:POST乱码的解法是在Servlet的doPost方法第一行加request.setCharacterEncoding("UTF-8");GET乱码则需要改Tomcat的conf/server.xml,在<Connector>标签里加URIEncoding="UTF-8"。注意过滤器(Filter)里同样可以统一设置请求编码,正确顺序是:EncodingFilter先执行设置编码,再执行LoginFilter判断登录状态。如果两个Filter的顺序反了,LoginFilter里如果读取了参数值,就会读到乱码。
提示:数据库端也需要统一编码。连接URL里的
characterEncoding=utf8和MySQL默认字符集都要检查,否则后端都是UTF-8,MySQL端还是latin1,数据照样存成乱码。
5.3 MySQL 8.0驱动报ClassNotFound / SSL警告
现象:Tomcat启动时项目正常,但第一次访问数据库相关页面时白屏,控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver;或者项目能跑但控制台刷SSL connection error警告。
原因:com.mysql.jdbc.Driver是MySQL 5.x的驱动类名,MySQL 8.x驱动改名为com.mysql.cj.jdbc.Driver。项目里某个或多个Class.forName还是旧类名,驱动不能加载。SSL警告是MySQL 8.0连接URL没有显式禁用或用serverTimezone导致的时区与SSL协商问题,本身不是致命错误,但会干扰排错时看日志。
解决:统一替换所有Class.forName和JDBC连接URL里的驱动路径:com.mysql.cj.jdbc.Driver,URL加serverTimezone=Asia/Shanghai&useSSL=false。如果用的是Maven项目,检查pom.xml中的mysql-connector-java版本是否以8.0.x开头。还有一点容易被忽略:多个版本的MySQL驱动jar包一起放在WEB-INF/lib下会冲突,只保留一个。
5.4 表单重复提交,点F5就重复插入一条选课记录
现象:学生选课成功后点击浏览器刷新按钮,弹出“是否重新发送表单”的提示,确认后数据库里出现了同一条重复选课记录。虽然数据库有唯一约束会拒绝第二次插入,但添加课程这种没有唯一约束的数据,刷几次就有几条一样的数据。
原因:添加操作用的是response.sendRedirect还好,但如果实现时用了forward,浏览器地址栏还是courseServlet?action=add这个POST请求,用户点击刷新时浏览器会重新发送上一次的POST请求,造成重复提交。
解决:遵循PRG原则(Post/Redirect/Get),表单提交成功后一律用response.sendRedirect跳转到列表页,不要用forward。如果希望做更严格的防重复,可以在表单里生成一个一次性token存到Session中,提交时校验并从Session中移除该token。课程设计项目中做到PRG就足够应付答辩了。
5.5 Tomcat 10与javax.servlet包名不一致
现象:源码在自己电脑的Tomcat 9上能跑,拿到实验室的Tomcat 10后启动报错:java.lang.NoClassDefFoundError: javax/servlet/ServletException。
原因:Tomcat 10把Java EE的包名从javax.servlet迁移到jakarta.servlet,向下不兼容。这是Servlet规范从Java EE转向Jakarta EE后的重大变化。写于2020年前的课程设计源码几乎全部基于旧包名,直接放到Tomcat 10+跑必然报错。
解决:课程设计源码统一按Tomcat 8.5/9.0来写。如果被迫用Tomcat 10,要么批量替换所有Java文件和JSP页面里的javax.servlet为jakarta.servlet,要么换回Tomcat 9。在答辩时这个问题的标准回答是:“我用的是Tomcat 9稳定版,因为Tomcat 10引入了Jakarta命名空间,旧代码不兼容。”
6. 让源码在验收时更好讲:验证清单与三处可拔高的改造点
源码能跑只是及格线,课程设计要拿高分,关键是你能否把系统讲出“设计感”。我在验收CIMS这类项目时,最看重的是学生能不能回答三个问题:权限是怎么控制的、数据库表是怎么关联的、如果数据量增长到一万条,哪些地方会变慢。把这三点准备好,比堆代码量有效得多。
验证清单第一项是功能闭环测试。把管理员、普通用户两种角色分别建好,走一遍完整流程:登录、添加课程、列表显示、修改课程、退选课程。每一步都要截图保留。第二项是异常路径测试,故意输入错误密码、直接访问未授权的JSP页面、删除已经被选课的课程——这些场景提前验证好,答辩时被问到“系统崩溃怎么办”才能答得上来。
值得动手改造的第一个点是课程列表分页。目前5.2里的列表是SELECT * FROM t_course一次性查出全部数据,课程数量在几十条时没问题,但答辩时肯定会问“课程有500条怎么办”。用LIMIT offset, pageSize加一个previousPage/nextPage的按钮,代码量增加约30行,讲解时的加分效果远高于30行代码本身。
第二处是选课前的人数校验。课程有capacity和selected_count字段,选课逻辑里应该加一个判断:如果selected_count >= capacity,提示“课程已满员”,阻止选课。这个逻辑放在EnrollmentServlet的doPost里执行,用UPDATE t_course SET selected_count = selected_count + 1 WHERE id = ? AND selected_count < capacity这种带条件更新的SQL,比先查后更更安全。Redis或乐观锁这类聊法是加分项,但课程设计答辩不深究也问题不大。
第三处是日志记录。用一个LogFilter把每次请求的访问时间、操作人、IP地址打到控制台或数据库里。代码量不多,但要讲清楚“我可以从日志里追溯谁在什么时间做了什么操作”,这是CIMS系统从CRUD演示升级为带管理思维的最直观证据。
多年的课程设计带教经验下来,我最想跟你说的一个习惯是:源码交付前,重命名好项目的工程名(比如CIMS_你的学号),把所有注释里出现作者的位置都改掉,确保换一台电脑按说明步骤能跑通。我见过太多翻车现场是换了电脑跑不起来,然后当众改代码。项目能独立跑通、每一项功能都能按路径讲出缘由,这套源码才算真正完成了它的使命。希望帮到你。
本文还有配套的精品资源,点击获取