多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JSP+Servlet+MySQL学生成绩管理系统:从数据库设计到MVC架构实战

JSP+Servlet+MySQL学生成绩管理系统:从数据库设计到MVC架构实战 简介本资源是一套完整的基于Java Web技术的学生成绩管理系统实现方案面向Java初学者、高校课程设计学生及Web开发入门者解决传统成绩管理中数据分散、查询低效、操作繁琐等问题适用于教务系统实训、毕业设计参考与JSPMySQL综合实践。压缩包共301个文件含13个核心Java Servlet类如StuLoginServlet、FindByIdServlet等、9个JSP页面、5个SQL建表与初始化脚本、45个PNG与55个JPG界面截图及UI资源辅以CSS、SCSS样式文件和项目配置文件整体20.31MB结构清晰模块划分明确。已有1164人学习下载。读者可直接部署运行获得包含用户登录、成绩增删改查、多条件检索、数据统计等完整功能的可执行系统配套论文详述架构设计与实现逻辑源码层级分明DAO与Servlet职责分离便于理解MVC雏形与JDBC数据库交互流程。1. 项目背景与核心价值为什么今天还在看JSP看到“JavaMySQLJSP”这个技术栈很多刚入行的朋友可能会皱眉头心想“这不是老掉牙的技术了吗现在不都Spring BootVue前后端分离了吗” 这个想法很自然但恰恰是这个“老”项目藏着新手入门企业级开发最扎实的基石。我做了十多年后端带过不少新人发现一个通病框架用得很溜但一问起HTTP请求怎么从浏览器到数据库走了一圈页面数据又是怎么渲染出来的很多人就含糊其辞了。这个网上学生成绩管理系统就是一个绝佳的“解剖标本”。它的核心价值不在于技术多么前沿而在于其完整性和教学性。它完整地呈现了一个典型MVCModel-View-Controller架构在Web1.0时代的经典实现。JSPJavaServer Pages在这里扮演了V视图和C控制器的双重角色虽然与现代架构相比显得耦合但这种“耦合”恰恰让你能清晰地看到数据流动的完整链条用户点击一个链接发出HTTP请求- Servlet/JSP接收并处理请求控制层- 调用JavaBean访问数据库模型层- 将结果集封装- 在JSP页面中通过脚本或标签展示视图层。理解了这个你再去看Spring MVC的Controller、RequestMapping就会恍然大悟“哦原来它是对这个过程的标准化和优雅封装。”所以这个项目源码对于以下人群特别有价值计算机专业的学生这是许多高校课程设计或毕业设计的经典选题。通过它你可以把《数据库系统概论》、《Java程序设计》、《Web开发技术》这几门课的知识串起来完成一个看得见、摸得着的作品。刚转行Java Web的开发者如果你已经学完了Java基础、SQL但对如何把它们变成一个有页面的系统感到迷茫这个项目就是你的第一块拼图。想夯实基础的中级开发者在微服务、云原生满天飞的时代回头看看最朴素的连接池怎么管理、JDBC操作如何防SQL注入、MVC的思想本源是什么能帮你更好地理解现代框架为何如此设计。接下来我们就抛开“过时”的偏见深入这个项目的肌理我会结合源码假设我们有一个标准的学生成绩管理系统的常见结构为你拆解每一个核心模块并补充大量教科书里不会写的实操细节和避坑指南。2. 系统架构与数据库设计剖析拿到一个源码包我习惯先看两样东西数据库表结构和项目的目录组织。这就像看一个人的骨架和神经系统能快速把握系统的核心逻辑。2.1 数据库表结构设计解析一个典型的学生成绩管理系统至少需要学生表、课程表、成绩表这三张核心表。我们来看看一个健壮的设计应该考虑哪些细节。学生表 (student)CREATE TABLE student ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键ID, student_id varchar(20) NOT NULL COMMENT 学号业务唯一标识, name varchar(50) NOT NULL COMMENT 学生姓名, gender char(1) DEFAULT 男 COMMENT 性别男/女, birthday date DEFAULT NULL COMMENT 出生日期, class_name varchar(50) DEFAULT NULL COMMENT 班级名称, entrance_date date DEFAULT NULL COMMENT 入学日期, password varchar(255) DEFAULT 123456 COMMENT 登录密码应加密存储, PRIMARY KEY (id), UNIQUE KEY uk_student_id (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT学生信息表;为什么这么设计id与student_id分离id是代理主键无业务含义用于内部关联效率高且稳定。student_id是业务主键学号唯一且可能有一定规则如年份学院序号。两者分离是常见实践避免业务规则变化影响核心关联。password字段这里默认值‘123456’是极不安全的。在实际项目中必须使用MD5、SHA-256或更安全的BCrypt进行哈希加密存储。我会在后面的安全章节详细讲。CHARvsVARCHAR性别使用CHAR(1)因为长度固定CHAR在存储和查询效率上略优于VARCHAR。课程表 (course)CREATE TABLE course ( id int(11) NOT NULL AUTO_INCREMENT, course_code varchar(20) NOT NULL COMMENT 课程代码, course_name varchar(100) NOT NULL COMMENT 课程名称, credit decimal(3,1) DEFAULT NULL COMMENT 学分如3.5, teacher varchar(50) DEFAULT NULL COMMENT 任课教师, academic_year varchar(9) DEFAULT NULL COMMENT 学年如2023-2024, semester tinyint(1) DEFAULT NULL COMMENT 学期1或2, PRIMARY KEY (id), UNIQUE KEY uk_course_code_year_semester (course_code,academic_year,semester) ) COMMENT课程信息表;为什么这么设计唯一键约束uk_course_code_year_semester这是核心约束。同一门课程如JAVA101在不同学年、不同学期会重复开设。这个约束确保了课程实例的唯一性是后续成绩关联正确的基石。credit字段类型使用DECIMAL(3,1)表示最多3位数其中1位小数可以存储像2.5、4.0这样的学分值比FLOAT/DOUBLE更适合精确的数值计算。成绩表 (score)CREATE TABLE score ( id int(11) NOT NULL AUTO_INCREMENT, student_id varchar(20) NOT NULL COMMENT 学号, course_id int(11) NOT NULL COMMENT 课程ID关联course.id, usual_score decimal(5,2) DEFAULT NULL COMMENT 平时成绩, exam_score decimal(5,2) DEFAULT NULL COMMENT 考试成绩, total_score decimal(5,2) GENERATED ALWAYS AS (usual_score * 0.3 exam_score * 0.7) STORED COMMENT 总评成绩平时30%考试70%, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 记录创建时间, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id,course_id), KEY idx_student_id (student_id), KEY idx_course_id (course_id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course (id) ON DELETE CASCADE, CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE CASCADE ) COMMENT学生成绩表;为什么这么设计外键约束 (FOREIGN KEY)fk_score_student和fk_score_course确保了数据的参照完整性。这意味着你不能为一个不存在的学生或课程录入成绩。ON DELETE CASCADE是一个需要谨慎使用的选项它表示当主表如student中的一条记录被删除时从表score中所有关联的记录也会被自动删除。在成绩系统中这通常是合理的学生退学其成绩记录也应清除。但有些场景下你可能希望保留历史记录这时可以用ON DELETE SET NULL或ON DELETE RESTRICT禁止删除。生成列 (GENERATED COLUMN)total_score字段是一个计算列。它的值由usual_score和exam_score自动计算得出平时成绩0.3 考试成绩0.7并物理存储。这保证了数据的一致性避免了应用层计算可能出现的错误或遗漏。这是MySQL 5.7版本提供的实用功能。复合唯一键uk_student_course防止同一个学生同一门课程被重复录入成绩这是业务逻辑的强约束。索引优化除了主键和外键自动创建的索引我们额外为student_id和course_id单独创建了索引idx_student_id,idx_course_id。这是因为我们经常会按学生查询其所有课程成绩或按课程查询所有学生成绩。单独的索引能加速这类查询。避坑指南字符集与排序规则很多新手在本地开发没问题一到服务器就出现中文乱码。根源常在数据库创建时。确保你的建库语句是CREATE DATABASE score_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;utf8mb4是utf8的超集支持完整的Unicode字符包括emoji现在是绝对标准。utf8mb4_unicode_ci是通用的排序规则对中文排序更准确。2.2 项目目录结构与MVC映射解压源码.zip后一个标准的JSP项目目录通常如下以Eclipse Dynamic Web Project结构为例StudentScoreManagementSystem/ ├── src/ (Java源代码) │ ├── com.xxx.model/ (模型层JavaBean对应数据库表) │ │ ├── Student.java │ │ ├── Course.java │ │ └── Score.java │ ├── com.xxx.dao/ (数据访问层封装JDBC操作) │ │ ├── BaseDao.java (连接池、公共CRUD方法) │ │ ├── StudentDao.java │ │ ├── CourseDao.java │ │ └── ScoreDao.java │ ├── com.xxx.service/ (业务逻辑层处理复杂业务) │ │ ├── StudentService.java │ │ └── ScoreService.java │ └── com.xxx.servlet/ (控制层继承HttpServlet处理请求) │ ├── LoginServlet.java │ ├── StudentServlet.java │ └── ScoreServlet.java ├── WebContent/ (Web应用根目录) │ ├── META-INF/ │ ├── WEB-INF/ │ │ ├── web.xml (部署描述符配置Servlet) │ │ └── lib/ (第三方JAR包如mysql-connector-java.jar) │ ├── css/ (样式表) │ ├── js/ (JavaScript文件) │ ├── images/ (图片) │ └── jsp/ (视图层JSP页面) │ ├── index.jsp (首页) │ ├── login.jsp (登录页) │ ├── student/ (学生管理相关页面) │ ├── course/ (课程管理相关页面) │ └── score/ (成绩管理相关页面) └── 其他文件如论文、SQL脚本各层职责解读Model (JavaBean)纯粹的数据载体只有私有属性、getter/setter方法、以及可能的重写toString()方法。它对应数据库的一张表。Dao (Data Access Object)负责所有与数据库的交互。它接收Model对象作为参数执行SQL并将ResultSet结果集转换为Model对象返回。这里会集中处理连接获取与释放、SQL拼接、参数预编译、事务控制等底层细节。Service封装业务逻辑。比如“录入成绩”这个操作Service层会先调用StudentDao和CourseDao验证学生和课程是否存在再调用ScoreDao检查是否已有成绩避免重复最后才执行插入。它是协调多个Dao完成一个业务单元的地方。Servlet作为控制器它继承HttpServlet重写doGet()或doPost()方法。它接收来自JSP表单的请求request调用相应的Service方法处理业务然后将结果数据放入request或session作用域最后转发forward或重定向redirect到JSP页面进行展示。JSP负责展示。它通过EL表达式${student.name}和JSTL标签库从request或session中获取数据渲染成HTML。最佳实践是JSP中应尽量避免出现大量的Java脚本% ... %而应使用JSTL和EL。这个结构清晰地体现了“高内聚、低耦合”的思想。虽然JSP时代这种分层不如Spring框架那样通过注解和依赖注入优雅但其思想是一脉相承的。3. 核心功能模块实现与代码精讲理解了骨架我们来看看血肉——关键功能是如何通过代码实现的。我会挑几个最核心的流程带你一行行代码分析并指出其中可以优化和必须注意的地方。3.1 数据库连接池为什么不用DriverManager几乎所有初学者第一个Java数据库程序都是这样写的Class.forName(com.mysql.cj.jdbc.Driver); Connection conn DriverManager.getConnection(url, username, password); // ... 执行操作 conn.close();这在单次请求中没问题。但在Web应用中每个HTTP请求都可能需要操作数据库。频繁地创建和关闭连接是极其昂贵的操作会成为系统性能的瓶颈。因此数据库连接池Connection Pool是Web应用的标配。它的原理是系统启动时预先创建一定数量的数据库连接放入“池”中。当需要时从池中取用一个空闲连接用完后归还而不是真正关闭。常见的连接池有C3P0、DBCP、HikariCP目前性能最好。在BaseDao.java中我们通常会看到连接池的初始化。以阿里的Druid连接池为例需引入druid-xxx.jarpublic class BaseDao { private static DataSource dataSource; static { // 静态代码块类加载时初始化连接池 try { Properties props new Properties(); // 通常将数据库配置放在单独的配置文件中如db.properties InputStream input BaseDao.class.getClassLoader().getResourceAsStream(db.properties); props.load(input); DruidDataSource ds new DruidDataSource(); ds.setUrl(props.getProperty(jdbc.url)); ds.setUsername(props.getProperty(jdbc.username)); ds.setPassword(props.getProperty(jdbc.password)); ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大活跃连接数 ds.setMinIdle(5); // 最小空闲连接数 ds.setMaxWait(60000); // 获取连接超时时间毫秒 dataSource ds; } catch (Exception e) { e.printStackTrace(); throw new ExceptionInInitializerError(数据库连接池初始化失败); } } // 提供获取连接的方法 public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 提供释放连接的方法实际是归还给连接池 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace();} try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace();} try { if (conn ! null) conn.close(); } // 注意这里调用close()是归还连接不是物理关闭 } catch (SQLException e) { e.printStackTrace();} } }关键点与避坑指南资源释放顺序ResultSet-Statement-Connection。必须在finally块中确保释放否则连接泄露池子里的连接会被慢慢耗尽导致后续请求无法获取连接。配置参数调优MaxActive不是越大越好需要根据数据库的最大连接数和应用并发量来设定。MaxWait设置一个合理值防止线程无限期等待。配置文件分离数据库密码等敏感信息绝对不要硬编码在Java文件里。使用db.properties文件并且不要将其提交到Git等版本控制系统。应在.gitignore中忽略它在部署时通过环境变量或外部配置中心注入。3.2 数据访问层DAO模式与防SQL注入我们以StudentDao.java中的addStudent(Student student)方法为例看一个标准的DAO方法实现。错误示范存在SQL注入漏洞public boolean addStudent(Student student) { String sql INSERT INTO student (student_id, name, gender) VALUES ( student.getStudentId() , student.getName() , student.getGender() ); // ... 执行executeUpdate }如果student.getName()是小明); DROP TABLE student; -- 那么拼接后的SQL会变成INSERT INTO student ... VALUES (S001, 小明); DROP TABLE student; -- , 男)--是SQL注释后面的内容被忽略。这条语句会先插入一条数据然后删除整个学生表这是毁灭性的。正确做法使用PreparedStatement预编译语句public boolean addStudent(Student student) { Connection conn null; PreparedStatement pstmt null; String sql INSERT INTO student (student_id, name, gender, birthday, class_name, password) VALUES (?, ?, ?, ?, ?, ?); try { conn BaseDao.getConnection(); pstmt conn.prepareStatement(sql); // 参数索引从1开始 pstmt.setString(1, student.getStudentId()); pstmt.setString(2, student.getName()); pstmt.setString(3, student.getGender()); // 处理可能为null的日期字段 if (student.getBirthday() ! null) { pstmt.setDate(4, new java.sql.Date(student.getBirthday().getTime())); } else { pstmt.setNull(4, Types.DATE); } pstmt.setString(5, student.getClassName()); // 密码加密存储 String encryptedPwd MD5Util.encrypt(student.getPassword()); pstmt.setString(6, encryptedPwd); int result pstmt.executeUpdate(); return result 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { BaseDao.close(conn, pstmt, null); // 释放资源 } }为什么PreparedStatement能防注入因为SQL语句INSERT ... VALUES (?, ?, ...)在prepareStatement时就被数据库编译了结构已经固定。后续的setString、setInt等方法只是传入参数值这些值会被数据库引擎严格当作数据来处理而不会被解释为SQL指令的一部分。即使用户输入包含SQL关键字也会被安全地转义。实操心得永远不要拼接SQL字符串。这是铁律。处理NULL值如果数据库字段允许为NULL而Java对象属性可能为null需要使用setNull方法并指定对应的SQL类型如Types.DATE。密码加密在DAO层进行加密是一个好习惯。MD5Util.encrypt是一个简单的工具方法。但注意单纯的MD5现在已不够安全建议加盐Salt或使用更安全的算法如BCrypt。这属于安全范畴后面会提。3.3 控制层Servlet与请求分发Servlet是MVC中的C控制器。我们看一个典型的StudentServlet它通常根据请求参数action的值来分发不同的操作增删改查。WebServlet(/studentServlet) // Servlet 3.0 注解配置替代web.xml public class StudentServlet extends HttpServlet { private StudentService studentService new StudentService(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); // 通常将GET请求也转到POST处理 } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 解决POST请求中文乱码 response.setContentType(text/html;charsetUTF-8); String action request.getParameter(action); if (add.equals(action)) { addStudent(request, response); } else if (delete.equals(action)) { deleteStudent(request, response); } else if (update.equals(action)) { updateStudent(request, response); } else if (query.equals(action)) { queryStudent(request, response); } else if (list.equals(action)) { listStudents(request, response); } else { // 默认操作或错误处理 request.getRequestDispatcher(/error.jsp).forward(request, response); } } private void addStudent(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取请求参数 String studentId request.getParameter(studentId); String name request.getParameter(name); // ... 获取其他参数 // 2. 封装成Student对象 Student student new Student(); student.setStudentId(studentId); student.setName(name); // ... // 3. 调用Service层 boolean success studentService.addStudent(student); // 4. 根据结果跳转 if (success) { // 添加成功重定向到列表页防止表单重复提交 response.sendRedirect(request.getContextPath() /studentServlet?actionlist); } else { request.setAttribute(errorMsg, 添加学生失败学号可能已存在); request.getRequestDispatcher(/jsp/student/addStudent.jsp).forward(request, response); } } // ... 其他方法delete, update, query, list类似 }关键点解析编码设置request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)是解决中文乱码的经典组合拳。前者处理请求体POST参数的编码后者设置响应流的编码和内容类型。请求分发模式通过一个action参数来区分操作这是一种简单有效的设计模式。更复杂的系统可能会为每个功能模块使用独立的Servlet。转发Forward vs 重定向Redirect转发request.getRequestDispatcher(xxx.jsp).forward(request, response);是服务器内部跳转浏览器地址栏不变request对象和其中的数据通过setAttribute设置的可以传递到下一个资源。适合跳转到JSP展示页面。重定向response.sendRedirect(新的URL);是服务器告诉浏览器去请求一个新地址浏览器地址栏会变是两次独立的请求之前的request数据会丢失。适合在操作如添加、删除成功后跳转防止用户刷新页面导致表单重复提交。这就是上面addStudent方法成功后用sendRedirect的原因。Service层调用Servlet只负责流程控制收集参数、跳转页面具体的业务逻辑如校验学号是否重复交给Service层。这是单一职责原则的体现。4. 前端交互与JSP页面实战JSP页面是视图层负责展示数据和接收用户输入。现代开发中我们极力避免在JSP中写Java代码块% ... %而是使用JSTLJSP Standard Tag Library和ELExpression Language。4.1 使用JSTLEL替代Scriptlet假设我们要在studentList.jsp中展示学生列表。传统Scriptlet方式不推荐% ListStudent studentList (ListStudent)request.getAttribute(studentList); for(Student stu : studentList) { % tr td% stu.getStudentId() %/td td% stu.getName() %/td td% stu.getGender() %/td /tr % } %现代JSTLEL方式推荐首先需要在页面顶部引入JSTL核心标签库% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %然后在Servlet中将学生列表放入请求域ListStudent studentList studentService.getAllStudents(); request.setAttribute(studentList, studentList); request.getRequestDispatcher(/jsp/student/studentList.jsp).forward(request, response);最后在JSP中优雅地遍历table border1 tr th学号/th th姓名/th th性别/th th操作/th /tr c:forEach items${studentList} varstu varStatusstatus tr td${stu.studentId}/td !-- EL表达式直接访问JavaBean属性 -- td${stu.name}/td td${stu.gender}/td td a hrefstudentServlet?actioneditid${stu.id}编辑/a a hrefjavascript:if(confirm(确定删除吗)) location.hrefstudentServlet?actiondeleteid${stu.id}删除/a /td /tr /c:forEach /table为什么推荐JSTLEL可读性强页面干净像HTML模板便于前端人员协作。功能强大JSTL提供了条件判断(c:if)、循环(c:forEach)、格式化(fmt:formatDate)等标签能处理大部分逻辑。易于维护将业务逻辑数据准备和展示逻辑页面渲染分离得更彻底。4.2 表单提交与数据回显在编辑学生信息时我们需要将现有数据回显到表单中。这是一个非常常见的场景。Servlet端编辑前数据准备private void editStudent(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String idStr request.getParameter(id); if (idStr ! null !idStr.trim().isEmpty()) { int id Integer.parseInt(idStr); Student student studentService.getStudentById(id); request.setAttribute(student, student); // 将查询到的学生对象放入请求域 request.getRequestDispatcher(/jsp/student/editStudent.jsp).forward(request, response); } else { response.sendRedirect(request.getContextPath() /studentServlet?actionlist); } }JSP表单页面editStudent.jspform actionstudentServlet?actionupdate methodpost input typehidden nameid value${student.id} !-- 隐藏域传递主键ID -- 学号input typetext namestudentId value${student.studentId} readonlybr !-- 学号通常不可编辑 -- 姓名input typetext namename value${student.name}br 性别 input typeradio namegender value男 c:if test${student.gender 男}checked/c:if男 input typeradio namegender value女 c:if test${student.gender 女}checked/c:if女br 班级input typetext nameclassName value${student.className}br input typesubmit value更新 /form关键技巧隐藏域input typehidden用于传递不需要用户看到但后端处理必需的数据如记录的主键ID。EL表达式在HTML属性中value${student.name}可以直接将JavaBean的属性值输出到输入框的value属性中实现数据回显。JSTL条件判断c:if test${student.gender 男}checked/c:if用于根据数据动态设置单选框的选中状态。4.3 分页查询的实现列表数据量大了必须分页。这是一个综合性的功能涉及前端传递页码、后端计算、SQL使用LIMIT。1. 封装分页工具类Page.javapublic class PageT { private int pageNo 1; // 当前页码 private int pageSize 10; // 每页记录数 private long totalCount; // 总记录数 private int totalPage; // 总页数 private ListT list; // 当前页的数据列表 // 计算总页数 public int getTotalPage() { if (totalCount % pageSize 0) { return (int)(totalCount / pageSize); } else { return (int)(totalCount / pageSize) 1; } } // getter和setter... }2. Service层分页查询方法public PageStudent getStudentsByPage(int pageNo, int pageSize, String keyword) { PageStudent page new Page(); page.setPageNo(pageNo); page.setPageSize(pageSize); // 1. 查询总记录数 long total studentDao.getTotalCount(keyword); page.setTotalCount(total); // 2. 计算起始索引 int start (pageNo - 1) * pageSize; // 3. 查询当前页数据 ListStudent list studentDao.getStudentsByPage(start, pageSize, keyword); page.setList(list); return page; }3. DAO层SQL// 查询总记录数 public long getTotalCount(String keyword) { String sql SELECT COUNT(*) FROM student WHERE name LIKE ?; // 使用PreparedStatementkeyword需要拼接通配符 % keyword % // ... 执行查询并返回count } // 分页查询数据 public ListStudent getStudentsByPage(int start, int pageSize, String keyword) { String sql SELECT * FROM student WHERE name LIKE ? ORDER BY id LIMIT ?, ?; // LIMIT ?, ? 第一个参数是偏移量第二个参数是获取的记录数 // ... 设置参数 % keyword %, start, pageSize // ... 执行查询并返回ListStudent }注意LIMIT是MySQL特有的语法在Oracle中需要使用ROWNUM在SQL Server中使用TOP和子查询。4. JSP页面分页导航栏div classpagination 共${page.totalCount}条记录共${page.totalPage}页当前第${page.pageNo}页 c:if test${page.pageNo 1} a hrefstudentServlet?actionlistpageNo1首页/a a hrefstudentServlet?actionlistpageNo${page.pageNo - 1}上一页/a /c:if c:forEach begin1 end${page.totalPage} vari c:choose c:when test${i page.pageNo} strong[${i}]/strong /c:when c:otherwise a hrefstudentServlet?actionlistpageNo${i}${i}/a /c:otherwise /c:choose /c:forEach c:if test${page.pageNo page.totalPage} a hrefstudentServlet?actionlistpageNo${page.pageNo 1}下一页/a a hrefstudentServlet?actionlistpageNo${page.totalPage}末页/a /c:if /div5. 系统安全、部署与性能优化思考一个能跑起来的系统只是第一步一个健壮、可用的系统还需要考虑安全、部署和性能。5.1 安全加固不止于防SQL注入密码加密存储前面提到MD5已不安全。推荐使用BCrypt它是专门为密码哈希设计的算法内置盐值且计算速度可调可以故意调慢以增加暴力破解成本。// 使用Spring Security的BCryptPasswordEncoder需引入jar包 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class PasswordUtil { private static final BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); public static String encode(String rawPassword) { return encoder.encode(rawPassword); } public static boolean matches(String rawPassword, String encodedPassword) { return encoder.matches(rawPassword, encodedPassword); } } // 存储时PasswordUtil.encode(password); // 校验时PasswordUtil.matches(inputPassword, storedPassword);会话管理与会话固定攻击防护用户登录后信息通常存在HttpSession中。要确保登录成功后调用session.invalidate()使旧会话失效然后request.getSession(true)创建一个新会话。这可以防止会话固定攻击。设置会话超时时间在web.xml中配置session-configsession-timeout30/session-timeout/session-config单位分钟。敏感操作如删除、修改密码需要二次验证如输入密码或使用CSRF Token。XSS跨站脚本攻击防护用户输入的内容如果未经处理就直接输出到HTML页面上可能导致XSS攻击。解决方案输出转义在JSP中使用JSTL的c:out标签默认会对内容进行HTML转义。c:out value${userInput}/会把script转义成lt;scriptgt;使其失去执行能力。富文本处理对于需要保留HTML格式的富文本如文章内容不能简单转义需要使用白名单过滤库如Jsoup。String safeHtml Jsoup.clean(unsafeHtml, Whitelist.basic());权限控制简单的系统可能只在登录时判断。更完善的系统需要基于角色的访问控制RBAC。可以在Servlet的doPost/doGet方法开始处或使用过滤器Filter进行拦截检查当前用户会话中的角色是否有权访问该资源。5.2 项目部署从IDE到服务器开发完成后你需要将项目部署到Tomcat服务器上运行。打包在Eclipse/IDEA中右键项目 - Export - WAR File。这会生成一个YourProject.war文件。部署到Tomcat简单方式将WAR文件复制到Tomcat的webapps目录下启动Tomcat它会自动解压并部署。管理台部署访问http://服务器IP:8080/manager/html使用配置在tomcat-users.xml中的管理员账号登录上传WAR文件部署。数据库迁移将开发环境的数据库结构和数据导出为SQL脚本在服务器上的MySQL中执行。确保连接池配置db.properties中的URL、用户名、密码已修改为生产环境的值。常见部署问题中文乱码确保服务器Tomcat的server.xml中Connector标签设置了URIEncodingUTF-8。确保MySQL连接字符串中指定了characterEncodingUTF-8。JAR包冲突检查WEB-INF/lib下的JAR包避免不同版本的同名库。特别是Servlet API、JSP API的JAR包Tomcat本身已提供项目里不要重复包含。ClassNotFoundException通常是某个依赖的JAR包没有被打进WAR包。检查项目的构建路径Build Path。5.3 性能优化浅谈对于这个量级的系统性能通常不是瓶颈。但了解一些优化思路是有益的。数据库层面索引确保WHERE、ORDER BY、JOIN的字段上有合适的索引。但索引不是越多越好会影响插入和更新速度。SQL优化避免SELECT *只取需要的字段。复杂的多表关联查询考虑是否可以通过冗余字段或分多次查询来优化。连接池配置根据实际并发压力调整initialSize、maxActive、minIdle等参数。应用层面缓存对于一些不常变化但频繁访问的数据如学院、专业列表可以缓存在内存中如使用HashMap或Guava Cache减少数据库查询。静态资源分离将CSS、JS、图片等静态文件放到Nginx等专门的Web服务器上或者使用CDN减轻Tomcat压力。JSP预编译在Tomcat中配置JSP预编译可以避免第一次访问时的编译开销。前端层面减少HTTP请求合并CSS和JS文件。资源压缩开启Gzip压缩。浏览器缓存为静态资源设置合理的缓存头Cache-Control。这个基于JSP的学生成绩管理系统项目虽然技术栈看起来“传统”但它所蕴含的MVC思想、分层架构、数据库设计、安全防范等核心知识是任何时代Web开发的基石。通过亲手实现和深度剖析这样一个完整的项目你获得的不是过时的技能而是一张理解现代Java Web框架的“底层地图”。当你再学习Spring Boot时你会清楚地知道它的Autowired是在解决我们手动new Service()的耦合问题它的JdbcTemplate是在封装我们Dao层里繁琐的try-catch-finally。这才是这个“老”项目在今天的真正价值所在。本文还有配套的精品资源点击获取
返回列表