ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java学生成绩管理系统进阶:从分层架构到工程化实践

Java学生成绩管理系统进阶:从分层架构到工程化实践 简介这是一套面向Java Web开发初学者与课程设计实践者的学生成绩管理系统进阶版完整工程基于主流企业级技术栈实现覆盖用户管理、课程维护、成绩录入与统计分析等核心教务场景。资源采用Spring SpringMVC MyBatis整合架构前端依托LayUI构建响应式界面Freemarker作为模板引擎实现动态页面渲染MySQL承载数据存储具备良好的分层结构与可扩展性。压缩包共524个文件包含57个Java业务类如StudentController、ScoreServiceImpl等、109个XML配置与映射文件、62个JS交互脚本、40个HTML视图页及150个LayUI配套GIF图标资源整体体积3.82MB结构清晰、模块职责分明。目前已有870人学习下载提供开箱即用的完整源码、数据库SQL脚本、Excel导入导出工具类及标准化RESTful接口设计助开发者快速掌握SSM全栈开发流程与真实项目工程规范。1. 项目缘起从“玩具”到“工具”的蜕变几年前我接手了一个给学校做的学生成绩管理系统。最初的版本怎么说呢就是个典型的“课程设计”水平——一个Student类一个Score类用ArrayList存数据所有逻辑都写在main方法里增删改查全靠Scanner在控制台敲命令。功能是实现了但代码结构一团乱麻数据一关程序就丢更别提什么并发安全和用户体验了。后来随着项目需求的迭代比如要支持Web端、要对接教务系统、要生成复杂的统计报表我不得不对那个最初的“玩具”进行了一次彻头彻尾的重构最终形成了一个结构清晰、易于维护和扩展的“工具级”系统。今天我想分享的正是这个“进阶版”学生成绩管理系统的全部Java源代码以及背后的设计思考。这不仅仅是一堆可以运行的代码更是一套如何将一个小型课程设计项目通过合理的架构设计、设计模式应用和工程化实践升级为一个具备实用价值的小型系统的完整范例。无论你是正在学习Java、苦于课程设计没有思路的学生还是刚入行、想了解一个真实小项目该如何搭建的初级开发者相信这份代码和其中的“踩坑”经验都能给你带来实实在在的启发。2. 架构演进从面条代码到分层架构最初的版本所有代码都堆在一起我们称之为“面条式代码”。这种代码的维护成本极高。进阶版的核心改进首先就体现在架构上。2.1 为什么选择分层架构分层架构Layered Architecture是一种最经典、也最易于理解和实施的架构模式。它将系统按职责横向切分每一层只与相邻的上下两层通信上层依赖下层提供的服务下层对上层无感知。这样做的好处非常明显解耦与高内聚数据访问逻辑、业务逻辑、展示逻辑被分离到不同的层中修改某一层比如将数据库从MySQL换成PostgreSQL不会波及其他层。易于分工协作后端开发可以专注于Service和DAO层前端或客户端开发可以基于定义好的接口进行并行开发效率高。可测试性增强可以对每一层进行独立的单元测试。例如测试业务逻辑层时可以用Mock对象模拟数据访问层无需启动真实的数据库。在我们的学生成绩管理系统中我采用了经典的四层架构表现层 (Presentation Layer)负责接收用户请求、展示数据。在进阶版中我们提供了两种实现基于Swing的图形界面GUI和基于Spring Boot的RESTful API为可能的Web前端或移动端预留接口。业务逻辑层 (Service Layer)系统的核心包含所有的业务规则和流程。例如“计算学生平均分”、“判断成绩是否有效0-100分”、“生成成绩单”等逻辑都在这里。数据访问层 (Data Access Layer, DAO)封装所有对数据库或其他持久化存储的操作如增、删、改、查。业务层通过调用DAO接口来存取数据而不需要关心数据具体存在哪里、怎么存的。领域模型层 (Domain Model Layer)也可以叫实体层Entity Layer。它包含系统核心的业务对象如Student学生、Course课程、Score成绩。这些是贯穿所有层的“血液”。2.2 核心领域模型设计领域模型是系统的基石设计得好后续开发会顺畅很多。我们系统中有三个核心实体1. Student学生public class Student { private String studentId; // 学号唯一标识 private String name; private String gender; private LocalDate birthDate; // 使用Java 8的日期API private String className; // 班级 // 省略Getter/Setter和构造方法 }注意studentId学号被设计为业务主键它本身具有业务含义如202301001。同时在数据库表中我们通常还会有一个无业务意义的自增id作为物理主键用于建立外键关联性能更好。这是一个常见的实践。2. Course课程public class Course { private String courseId; // 课程号 private String courseName; private Double credit; // 学分 private Integer courseHour; // 学时 // 省略Getter/Setter }3. Score成绩这是关联Student和Course的实体是多对多关系的中间表。public class Score { private Long id; // 物理主键 private String studentId; // 关联学生学号 private String courseId; // 关联课程号 private Double score; // 成绩 private LocalDateTime examDate; // 考试时间 // 省略Getter/Setter }心得在Score中存储studentId和courseId而不是Student和Course对象的引用这是一种“弱关联”设计。这保证了Score对象的独立性即使学生或课程信息被软删除或修改成绩记录依然存在且可追溯。在需要完整信息时通过DAO层进行关联查询。2.3 数据访问层的抽象DAO模式与接口编程数据访问层是变化可能最多的一层。今天我们用MySQL明天可能换H2做测试后天可能接入Redis做缓存。如何应对这种变化答案是接口与实现分离。我们为每个实体定义一个DAO接口规定它必须提供哪些数据操作方法。public interface StudentDao { Student findById(String studentId); ListStudent findAll(); boolean save(Student student); boolean update(Student student); boolean delete(String studentId); ListStudent findByName(String name); // 按姓名模糊查询 }然后提供不同的实现类。例如StudentDaoJdbcImpl使用原生JDBC操作MySQLStudentDaoJpaImpl使用JPAHibernate框架。在业务逻辑层我们只依赖StudentDao这个接口。public class StudentServiceImpl { private StudentDao studentDao; // 依赖接口而非具体实现 // 通过构造器或Setter注入具体的Dao实现 public StudentServiceImpl(StudentDao dao) { this.studentDao dao; } public void addStudent(Student student) { // 业务校验 if (studentDao.findById(student.getStudentId()) ! null) { throw new RuntimeException(学号已存在); } // 调用DAO保存 studentDao.save(student); } }这种做法的好处是更换数据库或ORM框架时业务逻辑层的代码一行都不用改只需要提供一个新的DAO实现并在组装程序时“注入”进去即可。这是“依赖倒置”原则DIP和“开闭原则”OCP的典型应用。3. 关键技术实现与“踩坑”实录有了好的架构接下来就是填充每一层的具体实现。这里我挑几个容易出问题、又有代表性的技术点展开。3.1 数据库连接管理从DriverManager到连接池初学JDBC我们习惯这样写Connection conn DriverManager.getConnection(url, user, password); // ... 执行SQL conn.close();这在单线程、低频访问的演示程序中没问题。但在任何严肃的应用中这都是灾难。频繁创建和关闭物理连接是极其昂贵的操作会导致性能瓶颈。解决方案使用数据库连接池。连接池在启动时就创建一定数量的连接放在“池”里程序使用时从池中借用用完后归还避免了重复创建的开销。进阶版中我选择了HikariCP它是目前公认性能最好的JDBC连接池之一。集成HikariCP的“坑”与技巧依赖配置需要在pom.xmlMaven或build.gradle中正确引入HikariCP的依赖。配置参数maximumPoolSize最大连接数不是越大越好通常建议是CPU核心数 * 2 有效磁盘数。设置过大反而会增加上下文切换和锁竞争的开销。connectionTimeout获取连接超时时间和idleTimeout连接空闲超时也需要根据实际场景调整。资源关闭务必在finally块或使用try-with-resources语句确保ConnectionStatementResultSet被正确关闭否则连接不会归还到池中导致连接泄漏。// 推荐做法try-with-resources try (Connection conn dataSource.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { // ... 设置参数执行查询 try (ResultSet rs pstmt.executeQuery()) { // ... 处理结果集 } } catch (SQLException e) { // 处理异常 }3.2 事务管理确保数据一致性一个业务操作可能涉及多次数据库写操作。例如“录入一门新课的成绩”这个业务至少需要两步1. 检查课程是否存在2. 插入成绩记录。如果第二步失败了整个操作应该回滚否则就会留下脏数据一门不存在的课程的成绩记录。在只有JDBC的情况下我们需要手动管理事务Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 1. 开启事务 // 2. 执行多个DAO操作 courseDao.insert(course); scoreDao.batchInsert(scores); conn.commit(); // 3. 提交事务 } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 4. 回滚事务 } catch (SQLException ex) { // 记录日志 } } throw e; } finally { // 5. 恢复自动提交并关闭连接 if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { // 记录日志 } } }手动管理非常繁琐且容易出错比如忘了setAutoCommit(false)或者异常处理不完整导致连接未正确关闭。进阶方案声明式事务。在Spring框架中我们可以使用Transactional注解轻松地将一个方法标记为事务性的。框架会为我们自动处理事务的开启、提交、回滚和资源清理。这是进阶版Spring Boot API模块采用的方式极大地简化了代码降低了出错概率。Service public class ScoreServiceImpl implements ScoreService { Autowired private CourseDao courseDao; Autowired private ScoreDao scoreDao; Transactional(rollbackFor Exception.class) // 声明此方法需要事务 public void enterScoresForNewCourse(Course course, ListScore scores) { // 业务校验... courseDao.save(course); scoreDao.batchSave(scores); // 如果任何一步抛出异常所有数据库操作都会回滚 } }3.3 业务逻辑层的复杂计算与报表生成业务逻辑层是体现系统价值的地方。除了简单的CRUD更复杂的是各种计算和报表。例如计算学生的加权平均分GPApublic Double calculateWeightedGPA(String studentId) { ListScoreDetail details scoreDao.findScoresWithCourseByStudent(studentId); if (details.isEmpty()) { return 0.0; } double totalWeightedScore 0.0; double totalCredits 0.0; for (ScoreDetail detail : details) { Double score detail.getScore(); Double credit detail.getCourseCredit(); if (score ! null credit ! null) { // 将百分制成绩转换为绩点例如90-100 - 4.0 80-89 - 3.0 以此类推 double gradePoint convertScoreToGradePoint(score); totalWeightedScore gradePoint * credit; totalCredits credit; } } if (totalCredits 0) { return 0.0; } return totalWeightedScore / totalCredits; } private double convertScoreToGradePoint(double score) { // 实现具体的转换规则 if (score 90) return 4.0; else if (score 80) return 3.0; else if (score 70) return 2.0; else if (score 60) return 1.0; else return 0.0; }注意这里的ScoreDetail可能是一个DTOData Transfer Object它包含了Score的成绩值和关联的Course的学分信息由DAO层通过关联查询一次性返回避免了在循环中频繁查询数据库N1查询问题。报表生成如班级成绩单的优化生成报表往往涉及大量数据的聚合查询。绝对要避免在Java内存中进行大量数据的循环过滤和计算。应该把计算压力转移到数据库。原始的、低效的做法// 错误示范将所有成绩取到内存再用Java过滤 ListScore allScores scoreDao.findAll(); MapString, ListScore classScoresMap new HashMap(); for (Score s : allScores) { // 1. 根据studentId查学生信息拿到班级 Student stu studentDao.findById(s.getStudentId()); // 2. 按班级分组... }这个过程会产生巨量的数据库查询N1问题性能极差。正确的做法在DAO层编写复杂的SQL或使用JPA的Criteria API进行关联查询和聚合。// 在ScoreDao接口中增加方法 public interface ScoreDao { // 查询指定班级、指定课程的所有学生成绩并包含学生姓名 ListClassScoreReport findClassScores(String className, String courseId); }对应的SQL可能类似于SELECT s.name, sc.score, c.course_name FROM student s JOIN score sc ON s.student_id sc.student_id JOIN course c ON sc.course_id c.course_id WHERE s.class_name ? AND sc.course_id ? ORDER BY sc.score DESC;这样数据库一次查询就返回了所有需要的数据在Java层只需要做简单的格式转换和输出即可。4. 前端展示Swing GUI的设计与优化为了提供一个可独立运行的桌面应用我使用Java Swing实现了图形界面。虽然Swing现在看来有些“古老”但它对于理解桌面应用的事件驱动模型和MVC模式非常有帮助。4.1 采用MVC模式组织Swing代码在Swing中很容易把所有代码都写在JFrame或JPanel的监听器里导致界面和逻辑再次耦合。我们应用前面提到的分层思想在表现层内部也做一次MVC分离Model模型就是我们的领域对象StudentCourse等和业务逻辑层Service。View视图Swing的各个组件JFrameJTableJTextField等负责展示数据和接收用户输入。Controller控制器通常是实现了ActionListenerMouseListener等的类它负责响应用户在View上的操作调用ModelService层处理业务然后更新View。例如学生列表界面// View: StudentListView (一个JPanel) public class StudentListView extends JPanel { private JTable studentTable; private StudentTableModel tableModel; // 自定义的TableModel private JButton addButton, deleteButton; private StudentController controller; // 持有控制器引用 public StudentListView(StudentController controller) { this.controller controller; initUI(); bindEvents(); } private void initUI() { /* 初始化表格、按钮等组件 */ } private void bindEvents() { addButton.addActionListener(e - controller.handleAddStudent()); deleteButton.addActionListener(e - { int row studentTable.getSelectedRow(); if (row 0) { String studentId (String) tableModel.getValueAt(row, 0); controller.handleDeleteStudent(studentId); } }); } public void refreshTable(ListStudent students) { tableModel.setData(students); } } // Controller: StudentController public class StudentController { private StudentService studentService; private StudentListView view; public StudentController(StudentService service, StudentListView view) { this.studentService service; this.view view; } public void handleAddStudent() { // 1. 弹出对话框收集学生信息这可以是一个新的View StudentDialog dialog new StudentDialog(); dialog.setVisible(true); if (dialog.isConfirmed()) { Student newStudent dialog.getStudent(); try { // 2. 调用Service层 studentService.addStudent(newStudent); // 3. 操作成功刷新视图 loadAndDisplayStudents(); JOptionPane.showMessageDialog(view, 添加成功); } catch (Exception e) { JOptionPane.showMessageDialog(view, 添加失败: e.getMessage(), 错误, JOptionPane.ERROR_MESSAGE); } } } public void handleDeleteStudent(String studentId) { // 确认对话框 int confirm JOptionPane.showConfirmDialog(view, 确定删除学号为 studentId 的学生吗, 确认删除, JOptionPane.YES_NO_OPTION); if (confirm JOptionPane.YES_OPTION) { try { studentService.deleteStudent(studentId); loadAndDisplayStudents(); JOptionPane.showMessageDialog(view, 删除成功); } catch (Exception e) { JOptionPane.showMessageDialog(view, 删除失败: e.getMessage(), 错误, JOptionPane.ERROR_MESSAGE); } } } private void loadAndDisplayStudents() { ListStudent students studentService.getAllStudents(); view.refreshTable(students); } }通过这种方式视图只关心显示和事件触发控制器负责协调业务逻辑完全交给Service层代码清晰职责分明。4.2 使用JTable与自定义TableModelSwing的JTable默认使用DefaultTableModel它要求数据是Vector或Object[][]这和我们面向对象的领域模型不匹配。更好的做法是自定义AbstractTableModel。public class StudentTableModel extends AbstractTableModel { private ListStudent studentList; private final String[] columnNames {学号, 姓名, 性别, 出生日期, 班级}; public void setData(ListStudent students) { this.studentList new ArrayList(students); // 防御性拷贝 fireTableDataChanged(); // 通知表格数据已更新重绘 } Override public int getRowCount() { return studentList null ? 0 : studentList.size(); } Override public int getColumnCount() { return columnNames.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Student student studentList.get(rowIndex); switch (columnIndex) { case 0: return student.getStudentId(); case 1: return student.getName(); case 2: return student.getGender(); case 3: return student.getBirthDate(); // 直接返回LocalDate渲染器会处理 case 4: return student.getClassName(); default: return null; } } Override public String getColumnName(int column) { return columnNames[column]; } // 如果需要编辑单元格可以重写setValueAt和isCellEditable方法 }使用自定义TableModel后JTable就能直接绑定我们的Student对象列表数据更新时只需调用setData并触发fireTableDataChanged非常方便。4.3 多线程与SwingWorker避免界面卡死在Swing中所有UI组件的创建、更新都必须在事件分发线程Event Dispatch Thread, EDT上进行。如果你在按钮的监听器里执行一个耗时的数据库查询或网络请求EDT就会被阻塞整个界面就会“卡死”用户无法进行任何操作。解决方案使用SwingWorker。SwingWorker是Swing提供的专门用于在后台执行耗时任务并在完成后安全更新UI的类。// 在Controller中 public void handleLoadData() { // 禁用按钮显示加载中提示 view.setLoadButtonEnabled(false); view.showLoadingIndicator(true); SwingWorkerListStudent, Void worker new SwingWorker() { Override protected ListStudent doInBackground() throws Exception { // 在后台线程执行耗时操作 return studentService.getAllStudents(); // 假设这个操作很慢 } Override protected void done() { // 在EDT线程执行可以安全更新UI view.showLoadingIndicator(false); view.setLoadButtonEnabled(true); try { ListStudent result get(); // 获取doInBackground的返回结果 view.refreshTable(result); } catch (InterruptedException | ExecutionException e) { JOptionPane.showMessageDialog(view, 加载数据失败: e.getCause().getMessage(), 错误, JOptionPane.ERROR_MESSAGE); } } }; worker.execute(); // 启动后台任务 }这个模式非常重要它能显著提升桌面应用的响应性和用户体验。5. 工程化与部署考量一个可用的系统除了核心功能还需要考虑很多工程化细节。5.1 配置文件管理不要把数据库连接字符串、用户名密码等硬编码在Java文件里使用配置文件如.properties或.yml。# config.properties db.urljdbc:mysql://localhost:3306/student_management?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai db.usernameroot db.passwordyour_secure_password db.pool.maximumPoolSize10在代码中通过java.util.Properties或ResourceBundle读取。在Spring Boot中则可以使用application.yml和Value注解或ConfigurationProperties。5.2 日志记录使用System.out.println调试和记录日志是极不专业的。应该使用成熟的日志框架如SLF4J Logback。引入依赖。配置logback.xml可以灵活控制日志级别DEBUG INFO WARN ERROR、输出格式、输出目的地控制台、文件。在代码中使用LoggerFactory获取Logger实例。import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class StudentServiceImpl { private static final Logger LOGGER LoggerFactory.getLogger(StudentServiceImpl.class); public void addStudent(Student student) { LOGGER.info(开始添加学生学号{}, student.getStudentId()); try { // ... 业务逻辑 LOGGER.info(学生[{}]添加成功。, student.getStudentId()); } catch (Exception e) { LOGGER.error(添加学生[{}]时发生异常, student.getStudentId(), e); throw e; } } }良好的日志是线上问题排查的生命线。5.3 异常处理策略不要生吞异常catch了什么都不做也不要只打印栈轨迹。应该定义清晰的业务异常体系并在合适的层面处理。DAO层捕获SQLException将其转换为自定义的DataAccessException一个运行时异常并向上抛出。这样业务层就不需要处理检查型异常。Service层处理业务规则校验校验不通过时抛出自定义的BusinessException如StudentNotFoundExceptionDuplicateStudentIdException。表现层Controller或GUI事件处理器捕获所有异常根据异常类型友好地提示用户例如“学号已存在请检查”并记录错误日志。5.4 打包与分发对于Swing桌面应用最终我们需要将它打包成一个可执行的JAR文件。可以使用Maven的maven-assembly-plugin或maven-shade-plugin来创建包含所有依赖的“胖JAR”Uber JAR。!-- 在pom.xml中配置 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer !-- 指定主类 -- mainClasscom.yourcompany.studentmgmt.MainApp/mainClass /transformer /transformers /configuration /execution /executions /plugin执行mvn clean package后会在target目录下生成一个*-shaded.jar文件用户只需安装JRE即可通过java -jar your-app-shaded.jar命令运行。对于Spring Boot的Web API部分打包就更简单了它内置的插件会直接生成可执行的JAR或WAR文件。6. 从“实现”到“设计”的思考完成这个项目后我最大的体会是编程不仅仅是让代码跑起来更是让代码“活”得久、改得动。进阶版与最初版的区别本质上是从“面向过程实现功能”到“面向对象设计系统”的思维转变。单一职责原则每个类、每个方法只做一件事。StudentDao只负责学生数据存取StudentService只负责学生相关业务逻辑。开闭原则对扩展开放对修改关闭。通过DAO接口我们很容易扩展出StudentDaoFileImpl文件存储而不修改业务层。依赖倒置高层模块Service不依赖低层模块DAO实现二者都依赖抽象DAO接口。关注点分离将数据持久化、业务逻辑、用户界面这些不同的“关注点”分离到不同的层中。这些原则不是教条而是无数前辈在应对软件复杂度时总结出的最佳实践。在项目开始时多花一点时间思考架构在后续的维护和扩展中会节省十倍、百倍的时间。这个学生成绩管理系统进阶版的源代码就是我尝试将这些理念付诸实践的一次完整记录。本文还有配套的精品资源点击获取
返回列表