Java进阶实战:从零构建MVC架构学生成绩管理系统 简介这是一套面向Java Web开发初学者与课程设计实践者的学生成绩管理系统进阶版完整工程基于主流企业级技术栈实现覆盖用户管理、课程维护、成绩录入与统计分析等核心教务场景。资源采用Spring SpringMVC MyBatis整合架构前端依托Layui构建响应式界面结合Freemarker模板引擎实现动态页面渲染后端逻辑清晰分层数据库使用MySQL并附带初始化SQL脚本。压缩包共524个文件包含57个Java业务类如StudentController、ScoreServiceImpl等、109个XML配置与映射文件、62个JS交互脚本、40个HTML视图页及150个Layui组件GIF资源整体3.82MB结构规范、模块解耦便于理解MVC全流程与前后端协作机制。目前已有870人学习下载提供可直接运行的完整源码、配套静态资源与编译后class文件助开发者快速掌握SSM框架集成、Layui组件调用及Freemarker模板开发实践。1. 项目概述从“能用”到“好用”的蜕变“学生成绩管理系统”这几乎是每个计算机专业学生都绕不开的课程设计或毕业设计选题。我见过太多版本从简单的控制台增删改查到带个简陋Swing界面的“半成品”。这些项目往往功能单一、代码结构混乱、扩展性几乎为零仅仅是为了完成作业而存在。今天我想聊的不是一个交差式的作品而是一个我称之为“进阶版”的实现。它脱胎于基础需求但着眼于一个真实、可维护、具备一定工程价值的Java应用。这个项目不仅包含了全部源代码更重要的是它背后蕴含的设计思想、技术选型和工程实践是初学者迈向合格Java开发者的关键一步。这个系统要解决的核心问题远不止记录分数那么简单。它需要处理复杂的业务逻辑比如成绩的权重计算平时分、期中、期末按比例汇总、成绩的统计分析平均分、最高分、分数段分布、多角色权限控制学生查分、教师录入、管理员管理课程。更重要的是它必须健壮、可靠、易于后续迭代。因此这个“进阶版”会采用MVC分层架构、面向接口编程、数据库连接池、事务管理等在企业级开发中司空见惯的技术。通过拆解这个项目的全部源代码我希望你能看到的不是一个孤立的程序而是一个微缩的、规范的Java Web应用开发范本。无论你是正在寻找课程设计灵感的在校生还是希望巩固Java SE及Web基础、为面试积累项目经验的求职者这个项目都能为你提供一条清晰的、从理论到实践的路径。2. 核心架构设计与技术选型解析2.1 为什么选择MVC三层架构在基础版里你可能见过把所有代码——从接收输入、处理逻辑到操作数据库——都堆在main方法或一个庞大类里的情况。这种“面条式”代码的维护噩梦在“进阶版”中必须被杜绝。我们采用经典的MVCModel-View-Controller模式并将其在Java Web语境下具体化为三层架构表现层Web、业务逻辑层Service、数据访问层DAO。表现层View Controller我们使用Servlet作为ControllerJSP作为View。Servlet负责接收HTTP请求如/score/query调用相应的Service方法并将处理结果数据模型转发给JSP页面进行渲染。虽然现在流行Spring MVC但直接从Servlet/JSP入手能让你更深刻地理解Web请求的生命周期、转发与重定向的区别这是理解任何高级框架的基础。业务逻辑层Service这是系统的“大脑”。所有与成绩计算、权限校验、业务规则相关的逻辑都封装在这里。例如ScoreService中会有一个calculateFinalScore(平时分 期中分 期末分 权重)的方法。这一层依赖于数据访问层但对外提供简洁的API。它的存在使得业务规则变更时只需修改Service而不会波及上下两层。数据访问层DAO Data Access Object这一层唯一的工作就是和数据库打交道执行CRUD操作。我们为每个实体如StudentCourseScore创建对应的DAO接口和实现类如StudentDaoStudentDaoImpl。实现类中使用JDBC进行数据库操作但关键点在于我们通过接口进行抽象。这意味着未来如果想把数据库从MySQL换成PostgreSQL或者引入MyBatis这样的持久层框架只需要提供新的DAO实现即可Service层代码几乎无需改动。注意很多新手会疑惑Service层和DAO层的区别。一个简单的判断原则凡是包含“业务逻辑”的就属于Service层凡是单纯“增删改查”数据库的就属于DAO层。例如“检查学生是否已选修某门课”是业务逻辑Service而“根据学生ID和课程ID查询选课记录”是数据访问DAO。2.2 数据库设计不仅仅是建表一个糟糕的数据库设计会拖垮整个系统。我们这里采用MySQL设计上遵循第三范式以减少数据冗余同时也要兼顾查询性能。核心表结构设计学生表 (student)CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, student_id VARCHAR(20) UNIQUE NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) COMMENT 性别, class_name VARCHAR(50) COMMENT 班级, enrollment_date DATE COMMENT 入学日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表;设计要点student_id学号作为业务唯一标识建立唯一索引。AUTO_INCREMENT的id作为无意义的主键用于关联其他表性能更好。课程表 (course)CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) UNIQUE NOT NULL COMMENT 课程代码, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit FLOAT COMMENT 学分, teacher VARCHAR(50) COMMENT 任课教师 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;成绩表 (score) - 核心表CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT 关联学生ID, course_id INT NOT NULL COMMENT 关联课程ID, usual_score DECIMAL(5,2) DEFAULT 0 COMMENT 平时成绩, midterm_score DECIMAL(5,2) DEFAULT 0 COMMENT 期中成绩, final_score DECIMAL(5,2) DEFAULT 0 COMMENT 期末成绩, total_score DECIMAL(5,2) GENERATED ALWAYS AS (usual_score*0.3 midterm_score*0.3 final_score*0.4) STORED COMMENT 总评成绩生成列, FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE, UNIQUE KEY uk_student_course (student_id, course_id) COMMENT 防止重复录入 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;设计要点使用DECIMAL(5,2)精确存储分数避免浮点数精度问题。外键约束确保数据完整性删除学生或课程时其成绩记录自动级联删除ON DELETE CASCADE。唯一约束uk_student_course确保一个学生对一门课只有一条成绩记录。生成列total_score是MySQL 5.7的特性自动根据公式计算并物理存储。这避免了每次查询时在Java代码中计算提升了查询性能也保证了计算逻辑的一致性。如果数据库版本不支持则需要在Service层计算。2.3 工具与依赖管理告别Jar包地狱基础项目常常是手动下载一堆jar包然后Build Path。进阶版我们使用Maven进行依赖管理。pom.xml文件是项目的核心配置。关键依赖dependencies !-- Servlet JSP API -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope !-- 容器提供打包时不包含 -- /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL标签库用于在JSP中简化逻辑 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 数据库连接池Druid 阿里出品功能强大 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- 日志框架SLF4J Logback -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.9/version /dependency dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.4.11/version /dependency /dependencies为什么用Maven它自动解决依赖传递和版本冲突标准化的项目结构src/main/javasrc/main/resources便于协作。为什么用Druid连接池直接使用DriverManager.getConnection()每次操作都新建连接开销巨大。连接池预先创建并管理一批连接用时取出用完归还极大提升性能。Druid还提供监控功能。为什么用SLF4JLogback取代System.out.println()可以分级别DEBUG INFO ERROR输出日志方便定位问题且能控制输出到文件或控制台是生产级应用的标配。3. 核心模块实现与代码深度解析3.1 数据访问层DAO实现接口与JDBC的优雅结合我们以StudentDao为例。首先定义接口明确契约public interface StudentDao { /** * 根据学号查询学生 * param studentId 学号 * return 学生对象未找到返回null */ Student findByStudentId(String studentId); /** * 查询所有学生分页 * param pageNum 页码 * param pageSize 每页大小 * return 学生列表 */ ListStudent findAll(int pageNum, int pageSize); int insert(Student student); int update(Student student); int deleteById(int id); }接着是基于JDBC的实现类StudentDaoImpl。这里的关键是资源管理和异常处理。public class StudentDaoImpl implements StudentDao { // 依赖一个数据库连接工具类这个工具类内部从Druid连接池获取连接 private JdbcTemplate jdbcTemplate; // 这里为了简化假设我们封装了一个简单的JdbcTemplate Override public Student findByStudentId(String studentId) { String sql SELECT id, student_id, name, gender, class_name, enrollment_date FROM student WHERE student_id ?; try { // 使用try-with-resources自动关闭ResultSet和PreparedStatement // 连接由JdbcTemplate管理在方法结束后归还连接池而非关闭 return jdbcTemplate.queryForObject(sql, new Object[]{studentId}, (rs, rowNum) - { Student student new Student(); student.setId(rs.getInt(id)); student.setStudentId(rs.getString(student_id)); student.setName(rs.getString(name)); student.setGender(rs.getString(gender)); student.setClassName(rs.getString(class_name)); student.setEnrollmentDate(rs.getDate(enrollment_date).toLocalDate()); return student; }); } catch (EmptyResultDataAccessException e) { // 查询结果为空返回null return null; } catch (DataAccessException e) { // 记录日志并将底层SQLException转换为运行时异常向上抛出 log.error(根据学号查询学生失败 studentId: {}, studentId, e); throw new DaoException(数据查询异常, e); } } Override public int insert(Student student) { String sql INSERT INTO student(student_id, name, gender, class_name, enrollment_date) VALUES(?, ?, ?, ?, ?); try { // 返回生成的主键 KeyHolder keyHolder new GeneratedKeyHolder(); jdbcTemplate.update(connection - { PreparedStatement ps connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, student.getStudentId()); ps.setString(2, student.getName()); ps.setString(3, student.getGender()); ps.setString(4, student.getClassName()); ps.setDate(5, Date.valueOf(student.getEnrollmentDate())); return ps; }, keyHolder); return keyHolder.getKey().intValue(); // 返回自增ID } catch (DataAccessException e) { log.error(插入学生信息失败 student: {}, student, e); throw new DaoException(数据插入异常, e); } } // ... 其他方法实现 }实操心得永远使用PreparedStatement防止SQL注入攻击且数据库会对预编译的SQL进行缓存提升性能。使用Try-With-Resources或确保资源关闭ConnectionStatementResultSet必须关闭否则会导致连接泄漏。我们的JdbcTemplate封装类会在底层妥善处理。统一的异常处理将 checked exception 的SQLException转换为 unchecked 的DaoException向上抛让Service层只关心业务异常简化代码。使用Lambda简化RowMapperJava 8的Lambda表达式可以让RowMapper的实现非常简洁。3.2 业务逻辑层Service实现事务与业务规则的守护者Service层是业务逻辑的核心。以ScoreServiceImpl为例它负责成绩录入这个操作必须具有原子性事务。public class ScoreServiceImpl implements ScoreService { private ScoreDao scoreDao; private CourseDao courseDao; // 通过构造器注入依赖 public ScoreServiceImpl(ScoreDao scoreDao, CourseDao courseDao) { this.scoreDao scoreDao; this.courseDao courseDao; } Override Transactional // 声明式事务注解需要配合Spring或类似AOP框架这里是概念示意 public void enterScore(ScoreEntryDto dto) throws BusinessException { // 1. 参数校验 if (dto null || dto.getStudentId() null || dto.getCourseCode() null) { throw new BusinessException(成绩录入参数不完整); } if (dto.getUsualScore() 0 || dto.getUsualScore() 100) { throw new BusinessException(平时成绩必须在0-100之间); } // ... 其他分数校验 // 2. 业务校验课程是否存在 Course course courseDao.findByCode(dto.getCourseCode()); if (course null) { throw new BusinessException(课程代码不存在 dto.getCourseCode()); } // 3. 业务校验是否已录入过成绩唯一约束兜底这里先查询提高用户体验 Score existingScore scoreDao.findByStudentAndCourse(dto.getStudentId(), course.getId()); if (existingScore ! null) { throw new BusinessException(该学生此课程成绩已录入如需修改请使用更新功能); } // 4. 组装实体对象 Score score new Score(); score.setStudentId(dto.getStudentId()); score.setCourseId(course.getId()); score.setUsualScore(dto.getUsualScore()); score.setMidtermScore(dto.getMidtermScore()); score.setFinalScore(dto.getFinalScore()); // total_score由数据库生成列或Service计算 // score.setTotalScore(calculateTotalScore(dto)); // 5. 调用DAO持久化 scoreDao.insert(score); // 6. 记录业务日志非必须但很有用 log.info(成绩录入成功 学号: {}, 课程: {}, 录入人: {}, dto.getStudentId(), dto.getCourseCode(), dto.getOperator()); } private double calculateTotalScore(ScoreEntryDto dto) { // 复杂的成绩计算逻辑例如根据不同课程类型采用不同权重 return dto.getUsualScore() * 0.3 dto.getMidtermScore() * 0.3 dto.getFinalScore() * 0.4; } }关键点解析事务管理TransactionalenterScore方法可能涉及多个DAO操作查询课程、查询已有成绩、插入新成绩。我们必须保证这些操作要么全部成功要么全部失败回滚。在Spring框架中一个Transactional注解就能在方法开始前开启事务方法结束后提交事务异常时回滚。在纯Servlet项目中我们需要手动从连接池获取连接并设置connection.setAutoCommit(false)在try-catch-finally中处理提交和回滚。业务异常BusinessException与系统异常如数据库连接失败不同业务异常是预期内的如“成绩已存在”、“分数无效”。我们定义自己的BusinessException继承RuntimeException在Controller层捕获并转换为友好的错误信息返回给前端。DTOData Transfer ObjectScoreEntryDto是专门用于前端传入的数据对象它可能只包含学号、课程代码、分数等必要字段与数据库实体Score解耦。这避免了实体对象被意外修改也更灵活。3.3 表现层Servlet JSP实现请求分发与页面渲染ControllerServlet扮演交通警察的角色。我们设计一个ScoreServlet通过URL中的action参数来区分不同操作。web.xml配置或使用Servlet 3.0注解WebServlet:servlet servlet-nameScoreServlet/servlet-name servlet-classcom.yourproject.web.ScoreServlet/servlet-class /servlet servlet-mapping servlet-nameScoreServlet/servlet-name url-pattern/score/url-pattern /servlet-mappingScoreServlet的核心doPost方法protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String action request.getParameter(action); try { switch (action) { case enter: enterScore(request, response); break; case query: queryScore(request, response); break; case delete: deleteScore(request, response); break; default: request.setAttribute(errorMsg, 无效的操作类型); request.getRequestDispatcher(/error.jsp).forward(request, response); } } catch (BusinessException e) { // 业务异常给用户友好提示 request.setAttribute(errorMsg, e.getMessage()); request.getRequestDispatcher(/score/enter.jsp).forward(request, response); } catch (Exception e) { // 系统异常记录日志给用户通用错误提示 log.error(Servlet处理请求失败 action: {}, action, e); request.setAttribute(errorMsg, 系统繁忙请稍后再试); request.getRequestDispatcher(/error.jsp).forward(request, response); } } private void enterScore(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, BusinessException { // 1. 获取参数并封装为DTO String studentId request.getParameter(studentId); String courseCode request.getParameter(courseCode); double usualScore Double.parseDouble(request.getParameter(usualScore)); // ... 获取其他参数 ScoreEntryDto dto new ScoreEntryDto(); dto.setStudentId(studentId); dto.setCourseCode(courseCode); dto.setUsualScore(usualScore); // ... dto.setOperator((String)request.getSession().getAttribute(username)); // 从会话获取操作员 // 2. 调用Service ScoreService scoreService ServiceFactory.getScoreService(); // 使用工厂获取Service实例 scoreService.enterScore(dto); // 3. 设置成功信息并重定向到结果页面防止表单重复提交 request.getSession().setAttribute(message, 成绩录入成功); response.sendRedirect(request.getContextPath() /score/success.jsp); }JSP页面enter.jsp示例% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title成绩录入/title /head body h2成绩录入/h2 c:if test${not empty errorMsg} div stylecolor: red;${errorMsg}/div /c:if form action${pageContext.request.contextPath}/score methodpost input typehidden nameaction valueenter 学号input typetext namestudentId requiredbr 课程代码input typetext namecourseCode requiredbr 平时成绩input typenumber step0.1 min0 max100 nameusualScorebr 期中成绩input typenumber step0.1 min0 max100 namemidtermScorebr 期末成绩input typenumber step0.1 min0 max100 namefinalScorebr input typesubmit value提交 /form /body /html实操心得POST/REDIRECT/GET (PRG) 模式在Servlet处理完POST请求如添加、修改后使用response.sendRedirect()重定向到一个结果页面GET请求。这可以防止用户刷新页面时重复提交表单。使用JSTL和EL表达式在JSP中尽量避免写Java代码% ... %。使用EL表达式${}获取数据使用JSTL标签c:ifc:forEach进行逻辑判断和循环使页面更清晰、更易于维护。统一的异常处理在Servlet层进行try-catch将不同类型的异常转化为用户能理解的信息并跳转到相应的错误页面或原表单页面。4. 进阶特性与工程化考量4.1 数据库连接池与工具类封装直接在每个DAO方法中获取和关闭连接是灾难性的。我们封装一个JdbcUtil类基于Druid连接池。public class JdbcUtil { private static DataSource dataSource; static { try { // 读取druid.properties配置文件 Properties props new Properties(); props.load(JdbcUtil.class.getClassLoader().getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(初始化Druid连接池失败); } } public static Connection getConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { log.error(获取数据库连接失败, e); throw new RuntimeException(数据库连接失败, e); } } // 提供一个简单的查询模板方法简化版实际可用Spring JdbcTemplate public static T T query(String sql, ResultSetHandlerT handler, Object... params) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rs ps.executeQuery(); return handler.handle(rs); } catch (SQLException e) { throw new DaoException(数据库查询异常, e); } finally { close(rs, ps, conn); } } // ... 封装update等方法 }druid.properties配置示例# 连接池基本配置 druid.urljdbc:mysql://localhost:3306/score_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai druid.usernameroot druid.passwordyourpassword druid.driverClassNamecom.mysql.cj.jdbc.Driver # 初始连接数 druid.initialSize5 # 最小空闲连接 druid.minIdle5 # 最大活跃连接 druid.maxActive20 # 获取连接超时时间毫秒 druid.maxWait600004.2 日志记录系统的“黑匣子”使用logback.xml配置日志替代System.out.println。configuration appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/score-system.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/score-system.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender root levelINFO appender-ref refCONSOLE / appender-ref refFILE / /root !-- 将我们DAO层的日志级别设为DEBUG便于调试SQL -- logger namecom.yourproject.dao levelDEBUG / /configuration在代码中使用import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class StudentDaoImpl implements StudentDao { private static final Logger log LoggerFactory.getLogger(StudentDaoImpl.class); // ... 在方法中 log.debug(执行SQL: {}, sql); log.error(操作失败, e); }4.3 前端交互与数据验证基础HTML表单体验很差。我们可以引入Bootstrap快速美化界面并使用jQuery进行前端交互和异步提交Ajax。Ajax成绩查询示例// 在JSP页面中 $(#queryBtn).click(function() { var studentId $(#studentIdInput).val(); $.ajax({ url: ${pageContext.request.contextPath}/score, type: GET, data: {action: queryJson, studentId: studentId}, dataType: json, success: function(data) { if (data.success) { // 动态渲染表格 renderScoreTable(data.scoreList); } else { alert(查询失败 data.message); } }, error: function() { alert(网络请求失败); } }); });对应的Servlet需要增加一个queryJson分支将查询到的ListScore转换为JSON字符串使用Jackson库并写回响应。// 在ScoreServlet中 case queryJson: String studentId request.getParameter(studentId); ListScoreVo scoreList scoreService.queryScoresByStudent(studentId); // Vo是专门用于前端展示的视图对象 // 使用Jackson将scoreList转为JSON ObjectMapper mapper new ObjectMapper(); String json mapper.writeValueAsString(new Result(true, 查询成功, scoreList)); response.getWriter().write(json); break;5. 项目部署、测试与常见问题排查5.1 环境搭建与项目部署开发环境JDK 8或11建议使用LTS版本配置好JAVA_HOME。Maven 3.6用于构建和管理依赖。MySQL 5.7执行提供的schema.sql创建数据库和表。IDEIntelliJ IDEA或Eclipse需配置好Maven和Tomcat。导入项目将项目作为Maven项目导入IDE。配置数据库修改src/main/resources目录下的druid.properties文件填写正确的数据库URL、用户名和密码。部署到Tomcat在IDEA中配置一个本地Tomcat服务器。将项目添加为Artifact选择war exploded模式热部署方便。启动Tomcat访问http://localhost:8080/your-project-name。5.2 核心功能测试用例手动测试是基础但编写简单的单元测试如JUnit能极大提升代码质量。以Service层测试为例public class ScoreServiceTest { private ScoreService scoreService; private ScoreDao mockScoreDao; // 使用Mockito模拟DAO BeforeEach public void setUp() { mockScoreDao Mockito.mock(ScoreDao.class); scoreService new ScoreServiceImpl(mockScoreDao, mock(CourseDao.class)); } Test public void testEnterScoreWithInvalidScore() { ScoreEntryDto dto new ScoreEntryDto(); dto.setUsualScore(150); // 无效分数 dto.setStudentId(2023001); dto.setCourseCode(CS101); // 验证会抛出业务异常 BusinessException exception assertThrows(BusinessException.class, () - { scoreService.enterScore(dto); }); assertTrue(exception.getMessage().contains(必须在0-100之间)); // 验证mock的DAO的insert方法没有被调用 Mockito.verify(mockScoreDao, Mockito.never()).insert(Mockito.any()); } Test public void testEnterScoreSuccess() throws BusinessException { // 准备模拟数据 Course mockCourse new Course(); mockCourse.setId(1); Mockito.when(mockCourseDao.findByCode(CS101)).thenReturn(mockCourse); Mockito.when(mockScoreDao.findByStudentAndCourse(2023001, 1)).thenReturn(null); ScoreEntryDto dto new ScoreEntryDto(); dto.setStudentId(2023001); dto.setCourseCode(CS101); dto.setUsualScore(80.0); dto.setMidtermScore(85.0); dto.setFinalScore(90.0); // 执行 scoreService.enterScore(dto); // 验证DAO的insert方法被调用了一次且参数正确 Mockito.verify(mockScoreDao, Mockito.times(1)).insert(Mockito.argThat(score - score.getStudentId().equals(2023001) score.getCourseId() 1 )); } }5.3 常见问题与排查技巧实录在实际开发和部署中你几乎一定会遇到以下问题。这里是我的排查清单问题现象可能原因排查步骤与解决方案页面中文乱码1. 数据库连接字符集未设置UTF-8。2. Servlet未设置请求/响应编码。3. JSP页面编码设置错误。1. 检查JDBC URL确保包含characterEncodingutf8。2. 在Servlet的doPost/doGet开头添加request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)。3. 确保JSP文件头有% page pageEncodingUTF-8%和meta charsetUTF-8。插入数据失败但控制台无报错1. 事务未提交。2. 数据库字段长度或约束不满足如唯一约束冲突。1. 检查是否开启了事务setAutoCommit(false)但忘记commit()或在异常分支中未rollback()。2. 查看MySQL的general log或使用客户端直接执行程序生成的SQL看具体错误。检查表结构约束。启动Tomcat时报ClassNotFoundException或NoClassDefFoundError1. Maven依赖未正确下载或导入。2. 项目未正确打包WEB-INF/lib下缺少jar包。1. 在IDE中检查Maven Dependencies库是否正常。运行mvn clean compile看是否报错。2. 如果是部署到独立Tomcat确保将项目打包成war文件或WEB-INF/lib目录包含所有依赖jar。使用mvn clean package生成war。页面提交后Servlet获取到的参数为null1. 前端表单字段的name属性与Servlet中getParameter的键名不匹配。2. 表单enctype可能为multipart/form-data文件上传此时不能用getParameter。1. 使用浏览器开发者工具F12的Network标签查看Form Data部分确认提交的键值对是否正确。2. 如果是文件上传需使用Apache Commons FileUpload或Servlet 3.0的PartAPI处理。连接池很快耗尽报Timeout waiting for connection1. 代码中未正确关闭Connection Statement ResultSet导致连接泄漏。2. 连接池最大连接数maxActive设置过小。1.这是最严重也最常见的问题。确保所有数据库操作在finally块中关闭资源或使用Try-With-Resources。2. 启用Druid的监控功能配置druid.web-stat-filter和druid.stat-view-servlet通过/druid/index.html查看活跃连接和泄漏检测。查询性能缓慢1. 表数据量大缺少索引。2. SQL语句写法有问题如SELECT *。3. N1查询问题在循环中查询数据库。1. 对经常作为查询条件的字段如student_idcourse_id建立索引。使用EXPLAIN分析SQL执行计划。2. 只查询需要的字段。3. 对于关联查询尽量使用JOIN一次查出或在Service层先批量查询出所有关联数据再在内存中组装。我个人在实际操作中的体会是这个“进阶版”项目最大的价值不在于功能有多复杂而在于它引入了一套规范的、可扩展的工程实践。从Maven依赖管理、分层架构、接口编程到连接池、日志、事务、异常处理每一步都是在为编写“工业级”代码打基础。当你下次面对一个更复杂的业务系统时你会自然而然地想到“这里应该分一层”、“这个异常需要自定义”、“那个操作需要加事务”。这才是从“学生项目”走向“企业级项目”的关键一步。代码是写给人看的顺便让机器执行。清晰的架构和良好的习惯是对未来维护代码的同事很可能就是几个月后的你自己最大的仁慈。本文还有配套的精品资源点击获取