尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

JavaWeb学生选课系统实战:从MVC架构到数据库事务完整指南

JavaWeb学生选课系统实战:从MVC架构到数据库事务完整指南 简介JavaWeb开发是构建动态网站和Web应用的核心技术栈其核心原理基于Servlet、JSP和JDBC等基础组件通过处理HTTP请求、生成动态响应并与数据库交互来实现业务逻辑。这项技术的价值在于为开发者提供了构建企业级应用的标准化方法尤其在教育、管理等业务系统中应用广泛。在典型的应用场景中如学生选课管理系统开发者需要综合运用MVC分层架构、数据库连接池和事务控制等关键技术以确保系统的可维护性和数据一致性。本文聚焦于JavaWeb项目实战深入探讨了使用Druid连接池优化数据库访问、通过事务机制保证选课业务的原子性并详细解析了基于过滤器实现统一权限控制的工程实践为初学者提供了一个从零搭建健壮Web应用的完整路径。1. 项目缘起为什么“学生选课管理系统”是JavaWeb入门的绝佳练手项目如果你正在学习JavaWeb或者想找一个能串联起Servlet、JSP、JDBC、MVC等核心技术的完整项目来巩固技能那么“学生选课管理系统”几乎是一个教科书式的选择。我当年学完基础语法和数据库后面对一堆零散的知识点感觉无从下手直到导师扔给我一个类似的选课系统需求才真正把前后端、数据库给串了起来。这个项目麻雀虽小五脏俱全它模拟了一个非常经典且真实的业务场景学生选课、教师开课、管理员管理。通过实现它你能亲手触摸到从数据库设计、后端逻辑处理到前端页面展示的完整Web应用开发生命周期。更重要的是这个项目的复杂度恰到好处。它不会像电商系统那样涉及复杂的支付、库存、秒杀让你望而却步也不会像简单的留言板那样过于单薄学不到东西。它包含了用户角色权限控制学生、教师、管理员、核心业务的增删改查课程信息的维护、选课退课、以及一些基本的业务逻辑如判断课程容量、冲突检测等。可以说吃透这一个项目你对JavaWeb开发的基本套路就能掌握个七七八八。网上流传的很多“完整源码数据库”包质量参差不齐有的甚至只是能跑通的Demo缺乏工程化的思考和细节处理。接下来我就结合自己多年的开发经验为你深度拆解如何从零开始构建一个结构清晰、易于维护的JavaWeb选课系统并分享那些在“完美源码”里通常不会写的实战坑点。2. 技术选型与项目骨架搭建告别混乱从MVC开始拿到一个项目最忌讳的就是打开IDE直接开写Servlet。一个良好的开端决定了后续开发的效率和代码的可维护性。对于JavaWeb项目我强烈建议从搭建一个标准的MVCModel-View-Controller项目骨架开始。2.1 核心技术栈的确定与理由基于当前最主流、最稳定的技术组合我推荐以下方案后端核心Servlet JSP JavaBean。这是JavaWeb的经典三件套是理解Web开发本质的基石。虽然现在Spring Boot大行其道但直接使用原生Servlet能让你更深刻地理解HTTP请求/响应、会话Session、过滤器Filter等底层机制。很多框架的魔法都建立在它们之上。数据持久层JDBC 连接池如Druid。直接使用JDBC编写DAOData Access Object层能让你对SQL执行、事务控制有最直接的感受。配合Druid这类高性能连接池既能保证学习深度又能接触生产级工具。数据库MySQL 8.0。社区活跃、资料丰富、免费是学习型项目的首选。务必使用8.0版本以兼容更新的SQL语法和性能特性。服务器Tomcat 9.x。轻量、稳定与Servlet规范兼容性好。项目管理与构建Maven。使用Maven管理项目依赖如MySQL驱动、Druid、JSTL标签库能让你提前适应企业级的项目结构避免手动拷贝Jar包的混乱。前端HTML CSS JavaScript JSTL。前端页面使用基础的JSP配合JSTLJSP Standard Tag Library来替代页面中的Java脚本片段使页面更清晰。对于简单的交互使用原生JavaScript或轻量的jQuery库即可。为什么不用Spring Boot对于初学者Spring Boot的“约定大于配置”和自动装配虽然快捷但像黑盒一样隐藏了大量细节。先用手动配置的方式走通全流程未来再学习Spring Boot时你才会恍然大悟明白它到底帮你省了哪些事解决了哪些痛点。2.2 项目目录结构的规范化设计在IDE如IntelliJ IDEA中创建Maven Web项目后第一件事就是规划目录。一个清晰的目录结构是优秀项目的开始。src/main ├── java │ └── com │ └── yourdomain │ └── course │ ├── controller // 控制器层存放Servlet │ ├── service // 业务逻辑层接口和实现类 │ ├── service.impl │ ├── dao // 数据访问层接口 │ ├── dao.impl // 数据访问层实现类 │ ├── model // 模型层存放JavaBean实体类 │ ├── filter // 过滤器如编码过滤、登录验证过滤 │ ├── listener // 监听器可选 │ └── util // 工具类如数据库连接工具、字符串处理工具 ├── resources │ ├── db // 存放数据库SQL脚本 │ └── config.properties // 配置文件数据库连接信息等 └── webapp ├── WEB-INF │ ├── web.xml // 部署描述符 │ └── lib // Maven依赖的jar包通常自动管理 ├── css ├── js ├── images └── views // JSP页面可按模块分子目录 ├── student ├── teacher ├── admin └── common // 公共页面如头部、尾部、登录页注意webapp/views目录是我个人的习惯将JSP页面集中管理并通过Servlet转发访问而不是直接暴露JSP路径这样更安全也便于权限控制。需要在web.xml中配置一个过滤器将所有直接访问.jsp的请求重定向或拦截。2.3 数据库连接池的配置与工具类封装直接在每个DAO方法里打开和关闭连接是灾难性的。我们必须使用连接池。这里以阿里开源的Druid为例。首先在pom.xml中添加依赖dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.18/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency然后在src/main/resources/config.properties中配置数据库信息jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/course_selection_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordyourpassword # Druid连接池配置 druid.initialSize5 druid.maxActive20 druid.minIdle5 druid.maxWait60000接下来创建数据库连接工具类src/main/java/com/yourdomain/course/util/DruidUtils.java。这个类的核心是使用DruidDataSource并在类加载时初始化连接池。这里有个关键点如何优雅地读取配置文件我推荐使用ResourceBundle或Properties加载类路径下的config.properties。package com.yourdomain.course.util; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DruidUtils { private static DataSource dataSource; // 静态代码块初始化连接池 static { try { Properties props new Properties(); // 使用ClassLoader加载资源文件确保无论在IDE还是Tomcat中都能找到 InputStream inputStream DruidUtils.class.getClassLoader().getResourceAsStream(config.properties); props.load(inputStream); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(初始化Druid连接池失败, e); } } // 获取数据源 public static DataSource getDataSource() { return dataSource; } // 从连接池获取连接 public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }实操心得很多新手会在工具类里把DataSource声明为局部变量导致每次调用都新建一个连接池这是完全错误的。连接池必须是全局唯一的单例。此外务必记得在web.xml中配置Druid的监控Servlet可选方便在开发阶段查看连接池状态但生产环境建议关闭。3. 数据库设计如何设计一个健壮且易于扩展的表结构数据库设计是项目的基石。一个糟糕的设计会让后续的编码举步维艰。对于选课系统核心实体是用户学生、教师、管理员、课程、选课记录。这里采用角色区分用户类型而不是为学生、教师分别建表这样更灵活。3.1 核心表结构设计1. 用户表 (user)这张表存储所有系统的使用者。通过user_type字段区分角色0-管理员1-教师2-学生。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名学号/工号, password varchar(255) NOT NULL COMMENT 密码MD5加密存储, name varchar(50) NOT NULL COMMENT 真实姓名, user_type tinyint(1) NOT NULL DEFAULT 2 COMMENT 用户类型0-管理员1-教师2-学生, email varchar(100) DEFAULT NULL COMMENT 邮箱, phone varchar(20) DEFAULT NULL COMMENT 电话, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) COMMENT 用户名唯一 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;为什么密码要MD5加密明文存储密码是极其危险且不专业的。即使是一个学习项目也要养成安全编码的习惯。MD5虽然已被证明可碰撞但对于学习项目及加盐salt处理后仍是一种简单有效的方案。在实际操作中可以使用java.security.MessageDigest进行加密。2. 课程表 (course)课程信息由教师或管理员创建。CREATE TABLE course ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 课程ID, course_code varchar(20) NOT NULL COMMENT 课程代码, course_name varchar(100) NOT NULL COMMENT 课程名称, teacher_id int(11) NOT NULL COMMENT 授课教师ID关联user.id, credit decimal(3,1) NOT NULL DEFAULT 2.0 COMMENT 学分, max_students int(11) NOT NULL DEFAULT 60 COMMENT 最大选课人数, selected_students int(11) NOT NULL DEFAULT 0 COMMENT 已选人数, schedule_time varchar(255) DEFAULT NULL COMMENT 上课时间如“周一 3-4节”, location varchar(100) DEFAULT NULL COMMENT 上课地点, description text COMMENT 课程描述, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 课程状态0-已关闭1-开放选课, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_course_code (course_code), KEY idx_teacher_id (teacher_id), CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES user (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表;注意字段设计selected_students已选人数是一个冗余字段它可以通过统计selection表计算得出。但这里故意设计为冗余是为了在查询课程列表时避免频繁的联表计数查询用空间换时间提升性能。但必须通过事务保证其与selection表数据的一致性。3. 选课记录表 (selection)这是学生和课程之间的多对多关联表也是业务的核心。CREATE TABLE selection ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 记录ID, student_id int(11) NOT NULL COMMENT 学生ID, course_id int(11) NOT NULL COMMENT 课程ID, selection_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, score decimal(4,1) DEFAULT NULL COMMENT 成绩选课时为空教师录入, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 状态1-已选课0-已退课, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id,course_id) COMMENT 同一学生同一课程只能有一条有效记录, KEY idx_course_id (course_id), CONSTRAINT fk_selection_course FOREIGN KEY (course_id) REFERENCES course (id) ON DELETE CASCADE, CONSTRAINT fk_selection_student FOREIGN KEY (student_id) REFERENCES user (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表;关键约束uk_student_course唯一索引是防止学生重复选课的关键。status字段用于软删除当学生退课时将状态改为0而不是物理删除记录这样可以保留历史记录如成绩。3.2 初始化数据与索引优化建议建表后需要插入一些初始数据用于测试。至少插入一个管理员账号用于后台管理、几个教师和若干学生。密码记得用MD5加密后的字符串插入。关于索引除了上述表结构中的主键、外键和唯一索引还可以根据查询场景考虑用户表如果经常按姓名查询可以在name字段上加普通索引。课程表如果经常按课程名模糊查询可以在course_name上加索引。但需注意模糊查询LIKE %关键词%是无法使用索引的LIKE 关键词%可以。选课记录表查询某个学生的所有课程或某门课程的所有学生已有的student_id和course_id索引已经足够。4. 核心业务逻辑实现从DAO到Service的完整链路有了扎实的数据库设计我们就可以开始编写代码了。遵循MVC模式我们从最底层的Model和DAO开始。4.1 实体类Model与基础DAO封装首先根据数据库表创建对应的JavaBean实体类放在com.yourdomain.course.model包下。以User类为例属性与表字段对应并生成getter/setter方法。然后我们创建一个基础的DAO接口和实现用于封装通用的CRUD操作避免在每个DAO里重复写。这里我展示一个使用泛型和反射的简化版基础DAO思路但对于初学者理解每个DAO单独编写的方式更重要。我们先看常规写法。创建UserDao接口和实现类// UserDao.java (接口) package com.yourdomain.course.dao; import com.yourdomain.course.model.User; import java.util.List; public interface UserDao { User findByUsername(String username); User findById(int id); ListUser findAllByType(int userType); int insert(User user); int update(User user); int delete(int id); // 其他业务查询方法... } // UserDaoImpl.java (实现类) package com.yourdomain.course.dao.impl; import com.yourdomain.course.dao.UserDao; import com.yourdomain.course.model.User; import com.yourdomain.course.util.DruidUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import java.sql.SQLException; import java.util.List; public class UserDaoImpl implements UserDao { private QueryRunner queryRunner new QueryRunner(DruidUtils.getDataSource()); Override public User findByUsername(String username) { String sql SELECT * FROM user WHERE username ?; try { return queryRunner.query(sql, new BeanHandler(User.class), username); } catch (SQLException e) { e.printStackTrace(); return null; } } // 其他方法实现... }这里我引入了commons-dbutils这个工具库它封装了JDBC的繁琐操作让DAO层的代码更简洁。记得在pom.xml中添加依赖。4.2 业务逻辑层Service与事务控制Service层负责处理复杂的业务逻辑它是Controller和DAO之间的桥梁。例如学生选课这个操作至少涉及以下几个步骤检查课程是否存在且状态为开放。检查学生是否已选过该课程。检查课程是否已满额。检查学生选课时间是否冲突需要查询该学生已选课程的上课时间。向selection表插入记录。更新course表的selected_students字段1。这些步骤必须在一个数据库事务中完成要么全部成功要么全部回滚。这是本项目第一个业务逻辑难点也是很多“源码”忽略的地方。我们来看SelectionService的关键方法package com.yourdomain.course.service; import com.yourdomain.course.service.impl.SelectionServiceImpl; // ... 其他import public interface SelectionService { /** * 学生选课 * param studentId 学生ID * param courseId 课程ID * return 成功返回true失败返回false并抛出业务异常 * throws BusinessException 自定义业务异常包含失败原因 */ boolean selectCourse(int studentId, int courseId) throws BusinessException; } // SelectionServiceImpl.java package com.yourdomain.course.service.impl; // ... import public class SelectionServiceImpl implements SelectionService { private CourseDao courseDao new CourseDaoImpl(); private SelectionDao selectionDao new SelectionDaoImpl(); // 可能需要一个查询课程安排的Dao Override public boolean selectCourse(int studentId, int courseId) throws BusinessException { Connection conn null; try { // 1. 获取数据库连接并设置为手动提交开启事务 conn DruidUtils.getConnection(); conn.setAutoCommit(false); // 2. 查询课程信息加锁防止并发超选 String lockSql SELECT * FROM course WHERE id ? FOR UPDATE; // 使用QueryRunner执行带锁查询... Course course queryRunner.query(conn, lockSql, new BeanHandler(Course.class), courseId); if (course null) { throw new BusinessException(课程不存在); } if (course.getStatus() ! 1) { throw new BusinessException(该课程未开放选课); } if (course.getSelectedStudents() course.getMaxStudents()) { throw new BusinessException(课程人数已满); } // 3. 检查是否重复选课 Selection existSelection selectionDao.findActiveByStudentAndCourse(studentId, courseId, conn); if (existSelection ! null) { throw new BusinessException(您已选择该课程); } // 4. 检查时间冲突需要根据course.schedule_time和学生已选课程时间判断此处简化 // boolean isConflict checkScheduleConflict(studentId, course.getScheduleTime(), conn); // if (isConflict) { throw new BusinessException(上课时间冲突); } // 5. 插入选课记录 Selection newSelection new Selection(); newSelection.setStudentId(studentId); newSelection.setCourseId(courseId); newSelection.setStatus(1); int insertCount selectionDao.insert(newSelection, conn); // 6. 更新课程已选人数 int updateCount courseDao.incrementSelectedStudents(courseId, conn); if (insertCount 0 updateCount 0) { conn.commit(); // 提交事务 return true; } else { conn.rollback(); // 回滚事务 return false; } } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); throw new BusinessException(系统错误选课失败); } catch (BusinessException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw e; // 业务异常直接抛出 } finally { DruidUtils.close(conn); // 归还连接到连接池 } } }踩坑实录事务与连接传递这是DAO和Service层交互最容易出错的地方。为了保证多个DAO操作在同一个事务里必须让它们使用同一个Connection对象。这意味着你的DAO接口方法需要增加一个Connection参数并在实现类中使用这个传入的连接而不是自己从连接池获取。上面的代码中selectionDao.insert(newSelection, conn)和courseDao.incrementSelectedStudents(courseId, conn)就体现了这一点。同时FOR UPDATE行级锁是防止高并发下超卖问题的关键。4.3 控制器层Controller与请求分发ControllerServlet接收前端请求调用对应的Service处理业务然后根据结果跳转页面或返回JSON数据。这里以用户登录和选课为例。登录Servlet (LoginServlet)WebServlet(/login) // 使用注解配置Servlet比web.xml更方便 public class LoginServlet extends HttpServlet { private UserService userService new UserServiceImpl(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); // 1. 参数校验略 // 2. 密码MD5加密 String encryptedPwd MD5Util.encrypt(password); // 3. 调用Service验证 User user userService.login(username, encryptedPwd); if (user ! null) { // 登录成功将用户信息存入Session HttpSession session request.getSession(); session.setAttribute(loginUser, user); // 根据角色跳转到不同首页 switch (user.getUserType()) { case 0: response.sendRedirect(request.getContextPath() /admin/index.jsp); break; case 1: response.sendRedirect(request.getContextPath() /teacher/index.jsp); break; case 2: response.sendRedirect(request.getContextPath() /student/index.jsp); break; default: response.sendRedirect(request.getContextPath() /login.jsp?error1); } } else { // 登录失败带回错误信息 request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }选课Servlet (SelectCourseServlet)WebServlet(/student/selectCourse) public class SelectCourseServlet extends HttpServlet { private SelectionService selectionService new SelectionServiceImpl(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 0. 权限校验通过过滤器统一做了 HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null || loginUser.getUserType() ! 2) { response.sendRedirect(request.getContextPath() /login.jsp); return; } String courseIdStr request.getParameter(courseId); int courseId Integer.parseInt(courseIdStr); int studentId loginUser.getId(); try { boolean success selectionService.selectCourse(studentId, courseId); if (success) { // 选课成功可以返回JSON或重定向到成功页面 response.getWriter().write({\code\:200, \msg\:\选课成功\}); } } catch (BusinessException e) { // 捕获业务异常返回错误信息给前端 response.getWriter().write({\code\:500, \msg\:\ e.getMessage() \}); } catch (Exception e) { response.getWriter().write({\code\:500, \msg\:\系统异常\}); } } }注意这里使用了WebServlet注解简化了配置。对于返回JSON数据的接口需要设置response.setContentType(application/json;charsetutf-8);。同时所有对学生角色的操作都应放在/student/*路径下并通过过滤器进行统一的登录和权限校验。5. 前端页面与交互用JSP和JSTL构建动态视图前端页面负责展示数据和接收用户操作。我们使用JSP作为视图层并利用JSTL和EL表达式来避免在JSP中编写Java代码。5.1 公共模块与页面布局首先创建公共的头部(common/header.jsp)、尾部(common/footer.jsp)和样式。使用% include file... %指令将它们组合到每个页面中实现代码复用。一个典型的学生课程列表页面 (student/courseList.jsp)% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title可选课程列表/title link relstylesheet href${pageContext.request.contextPath}/css/style.css script src${pageContext.request.contextPath}/js/jquery.min.js/script /head body % include file../common/header.jsp % div classcontainer h2可选课程列表/h2 c:if test${not empty errorMsg} div classalert alert-error${errorMsg}/div /c:if table classtable thead tr th课程代码/thth课程名称/thth授课教师/thth学分/thth时间/地点/thth已选/容量/thth操作/th /tr /thead tbody c:forEach items${courseList} varcourse tr td${course.courseCode}/td td${course.courseName}/td td${course.teacherName}/td %-- 这里需要Service层联查或DTO --% td${course.credit}/td td${course.scheduleTime} ${course.location}/td td${course.selectedStudents} / ${course.maxStudents}/td td c:choose c:when test${course.selectedStudents course.maxStudents} button classbtn btn-disabled disabled已满/button /c:when c:when test${course.status 0} button classbtn btn-disabled disabled已关闭/button /c:when c:otherwise button classbtn btn-primary btn-select>// CourseDTO.java public class CourseDTO extends Course { private String teacherName; // getter/setter } // CourseService中查询列表的方法 public ListCourseDTO findAvailableCourses() { String sql SELECT c.*, u.name as teacher_name FROM course c JOIN user u ON c.teacher_id u.id WHERE c.status 1 ORDER BY c.id; // 使用BeanListHandler但需要自定义ResultSetHandler或使用MapHandler再转换 // 更简单的做法是使用Apache DbUtils提供的BeanProcessor和自定义处理器 }这是Web开发中一个常见的模式用于处理视图层需要的、与实体类不完全一致的数据。6. 权限控制与系统安全不仅仅是登录拦截一个完整的系统必须有严格的权限控制。我们前面通过Session判断了用户是否登录但还需要更细粒度的控制。6.1 使用过滤器Filter实现统一权限校验创建一个AuthFilter拦截所有需要登录才能访问的请求如/student/*,/teacher/*,/admin/*。WebFilter(/*) // 拦截所有请求 public class AuthFilter implements Filter { private static final String[] IGNORE_URI {/login.jsp, /login, /register, /css/, /js/, /images/}; Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); String contextPath req.getContextPath(); String path uri.substring(contextPath.length()); // 检查是否在忽略列表 boolean needCheck true; for (String ignore : IGNORE_URI) { if (path.startsWith(ignore)) { needCheck false; break; } } if (needCheck) { HttpSession session req.getSession(false); // 不创建新session if (session null || session.getAttribute(loginUser) null) { // 未登录跳转到登录页 resp.sendRedirect(contextPath /login.jsp); return; } else { // 已登录可进行更细粒度的角色权限校验例如检查访问/admin/路径的是否是管理员 User user (User) session.getAttribute(loginUser); if (path.startsWith(/admin/) user.getUserType() ! 0) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, 权限不足); return; } if (path.startsWith(/teacher/) user.getUserType() ! 1) { // ... 同理 } } } chain.doFilter(request, response); // 放行 } }注意过滤器需要在web.xml中配置或使用WebFilter注解并注意过滤器的顺序。对于静态资源CSS, JS, 图片一定要在IGNORE_URI中排除否则会影响页面样式和脚本加载。6.2 密码安全与SQL注入防范密码安全前面提到用MD5加密。更安全的做法是使用“加盐”salt哈希例如MD5(password salt)。盐值可以是一个固定的字符串或者每个用户独立的随机字符串存于用户表。这里展示一个简单的工具类public class MD5Util { private static final String SALT YourFixedSaltHere; // 固定盐可从配置读取 public static String encrypt(String password) { try { String data password SALT; MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(data.getBytes(StandardCharsets.UTF_8)); return bytesToHex(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } private static String bytesToHex(byte[] bytes) { ... } // 字节转十六进制 }SQL注入防范永远不要使用字符串拼接来构造SQL语句我们之前使用的QueryRunner其query和update方法内部就是使用PreparedStatement能有效防止SQL注入。如果你直接使用JDBC务必如此操作String sql SELECT * FROM user WHERE username ? AND password ?; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs pstmt.executeQuery();7. 项目部署、测试与常见问题排查7.1 本地部署与测试环境准备确保本地安装好JDK8、Maven、MySQL和Tomcat。数据库初始化运行建表SQL脚本插入初始化数据。项目打包在项目根目录下执行mvn clean package会在target目录下生成项目名.war文件。部署到Tomcat将war文件拷贝到Tomcat的webapps目录下启动Tomcatbin/startup.bat或startup.shTomcat会自动解压部署。访问应用打开浏览器访问http://localhost:8080/你的项目名/。7.2 常见问题与排查思路问题一访问页面出现404错误。检查1确认Tomcat已成功启动且控制台无严重错误。检查2确认访问的URL路径是否正确特别是应用上下文Context Path。在IDEA中调试时上下文路径可能在Edit Configurations中设置。检查3检查web.xml或WebServlet注解中配置的Servlet路径是否正确。问题二页面显示乱码。根源Java Web中的乱码通常由字符编码不一致引起涉及浏览器、JSP、Servlet、数据库等多个环节。解决方案所有JSP页面顶部添加% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8%。在Servlet的doGet/doPost方法最开始处设置请求和响应的编码request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8);数据库连接URL中指定字符集jdbc:mysql://...?useUnicodetruecharacterEncodingUTF-8。确保MySQL数据库、表、字段的字符集为utf8mb4。问题三静态资源CSS, JS, 图片加载失败。检查1浏览器开发者工具F12的Network面板查看资源请求的URL是否正确状态码是否为404。检查2JSP页面中引用资源的路径是否正确使用了${pageContext.request.contextPath}。检查3检查AuthFilter是否错误地拦截了静态资源的请求。确保过滤器的忽略列表配置正确。问题四事务没有回滚。检查1是否在操作开始前设置了conn.setAutoCommit(false)。检查2是否在catch块中调用了conn.rollback()。检查3最关键的一点确保Service层中所有DAO操作使用的是同一个Connection对象并且这个Connection是在Service方法开始时获取在finally块中关闭或归还。如果每个DAO方法自己获取连接那么它们的事务是独立的无法一起回滚。7.3 项目扩展与优化方向当你完成了基础版本后可以考虑以下方向进行深化这会让你的项目更有竞争力引入前端框架用Vue.js或React重构前端页面实现前后端分离。后端Servlet只提供RESTful API返回JSON前端通过Ajax调用。这是现代Web开发的主流模式。升级后端框架将项目迁移到Spring Boot MyBatis-Plus。你会发现之前手动编写的很多样板代码如事务管理、连接池配置、简单的CRUD都被框架优雅地解决了开发效率极大提升。增加复杂业务选课冲突检测实现更精确的时间冲突判断如“周一 1-2节”与“周一 2-3节”冲突。多维度查询与分页为课程列表、学生列表增加多条件查询并实现服务器端分页。成绩管理与分析教师录入成绩后系统自动计算学生的平均绩点GPA并生成成绩单。选课时间段控制管理员可以设置系统开放选课和关闭选课的时间。提升系统性能与可用性数据库索引优化根据慢查询日志为高频查询条件添加合适的索引。引入缓存对于不经常变动的数据如学院、专业列表可以使用Redis进行缓存减轻数据库压力。接口限流与防刷防止恶意脚本频繁调用选课接口。这个项目从设计到实现的完整过程几乎涵盖了初级JavaWeb工程师所需的所有核心技能点。我建议你不要满足于找到一份“能跑通”的源码而是按照上面的思路自己动手从头到尾实现一遍。过程中遇到的每一个错误都是你成长的阶梯。当你真正解决了事务控制、权限过滤、前后端数据交互这些坑之后你对Web开发的理解会深刻得多。最后记得将代码托管到GitHub上一份干净、规范、有完整README文档的项目源码是你技术能力最好的证明。本文还有配套的精品资源点击获取
返回列表