基于JavaWeb的毕业设计实战:从零构建高内聚低耦合的教务管理系统
摘要:许多毕业生在完成基于JavaWeb的毕业设计时,常陷入技术堆砌、架构混乱或功能冗余的困境。本文以教务管理系统为实战案例,采用Servlet+JSP+MySQL基础栈,结合MVC分层思想,详解如何实现模块解耦、事务控制与用户权限校验。读者将掌握可复用的工程结构、防SQL注入的安全编码实践,并获得一套可直接部署的轻量级项目模板,显著提升开发效率与答辩竞争力。
一、毕业生常见开发痛点
- 代码耦合:把业务逻辑、SQL、页面跳转全写进一个JSP,后期改一行,全站报错。
- 无异常处理:遇到主键冲突、空指针直接500,浏览器堆栈信息把表结构暴露无遗。
- 安全漏洞:登录SQL拼接、
${param.xxx}直接回显,答辩现场被老师一句“你试过SQL注入吗”问倒。 - 重复造轮子:每个Servlet都写一遍获取Connection、关闭ResultSet,代码量比业务逻辑还多。
- 中文乱码:Windows下写死
new String(request.getParameter("name").getBytes("ISO-8859-1"),"UTF-8"),部署到Linux当场翻车。
二、技术选型:为什么回到“原生”Servlet
| 方案 | 优点 | 缺点 | 毕业设计场景 |
|---|---|---|---|
| Spring Boot | 零配置、生态丰富 | 起步即Parent、注解黑箱,答辩易被问“Starter做了什么” | 老师怀疑你直接抄脚手架 |
| Spring MVC | 分层清晰 | 需要理解IoC、AOP,配置一堆 | 时间紧,容易调不通 |
| Servlet+JSP | 语法直观、无黑箱、服务器随处可见 | 样板代码多 | 正好练手机会:把样板抽象成工具类,体现“造轮子”能力 |
结论:用原生Servlet,能把HTTP生命周期、字符编码、事务边界亲手摸一遍,答辩时底气足。
三、工程骨架:先搭“高内聚低耦合”的目录
edu-manage ├─src │ ├─main │ │ ├─java │ │ │ ├─controller // 仅收参、跳转 │ │ │ ├─service // 事务脚本 │ │ │ ├─dao // 纯SQL,不含业务 │ │ │ ├─util // 连接池、字符过滤 │ │ │ └─entity // POJO │ │ └─webapp │ │ ├─WEBNAME │ │ ├─view // JSP │ │ └─static // css/js └─sql └─edu.sql // 建表+样本数据约定:
- controller层禁止出现
conn.createStatement() - service层做事务开关,dao层只做CRUD
- 所有外部参数先进
XssFilter,再进controller
四、核心实现细节
4.1 用户登录鉴权(含防SQL注入)
- 表结构
CREATE TABLE user( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) UNIQUE NOT NULL, password CHAR(64) NOT NULL, -- 存SHA-256 role ENUM('ADMIN','TEACHER','STUDENT') );- DAO层(使用PreparedStatement,杜绝拼接)
public class UserDao { private DataSource ds = DataSourceUtil.getInstance(); public Optional<User> findByUsername(String username) fro SQLException{ String sql = "SELECT id,username,password,role FROM user WHERE username=?"; try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)){ ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setPassword(rs.getString("password")); u.setRole(Role.valueOf(rs.getString("role"))); return Optional.of(u); } } } return Optional.empty(); } }- Service层统一事务边界
public class UserService { private UserDao userDao = new UserDao(); public User login(String username, String rawPwd) MicException { Optional<User> op = userDao.findByUsername(username); if (!op.isPresent()) { throw new MicException("用户不存在"); } User u = op.get(); String sha = HashUtil.sha256(rawPwd); if (!sha.equals(u.getPassword())) { throw new MicException("密码错误"); } return u; } }- Controller层收参+跳转
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); try { User user = userService.login(username, password); req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } catch (MicException e) { req.setAttribute("msg", e.getMessage()); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }注意:密码在浏览器→服务器→数据库全程密文;登录失败不提示“用户名或密码错误”,而是统一“用户不存在或密码错误”,防用户名枚举。
4.2 课程CRUD与事务管理
- 新增课程需要同时写入course表、teacher_course中间表,两步必须在同一事务。
public class CourseService { private CourseDao courseDao = new CourseDao(); private TeacherCourseDao tcDao = new TeacherCourseDao(); public void addCourseWithTeacher(Course c, int teacherId) SQLException { Connection conn = DataSourceUtil.getConnection(); try { conn.setAutoCommit(false); int courseId = courseDao.insert(c, conn); // 第1步 tcDao.insert(teacherId, courseId, conn); // 第2步 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.close(); } } }- dao层重载带
Connection的签名,保证同链接
public int insert(Course c, Connection conn) SQLException { String sql = "INSERT INTO course(name,credit) VALUES(?,?)"; try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, c.getName()); ps.setInt(2, c.getCredit()); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { keys.next(); return keys.getInt(1); } } }4.3 统一字符编码与Clean Code小套路
- 在web.xml里声明
CharacterEncodingFilter,优先于其他Filter - 所有常量集中:
public static final String SESSION_USER = "loginUser" - 拒绝魔法数:
if (user.getRole() == Role.ADMIN)而不是==1 - 异常链保留:
throw new MicException("xxx", e)方便日志定位
五、安全性与性能
5.1 XSS防护
- 自定义EL函数库
fn:escapeHtml回显用户输入 - 或采用JSTL
<c:out value="${param.name}" />默认转义
5.2 SQL注入
- 100%使用PreparedStatement
- 禁止“WHERE id IN (+拼接+)”场景,用
FIND_IN_SET或临时表
5.3 连接池与性能
- 选用HikariCP,Spring官方也在用,轻量
- 核心配置
jdbcUrl=jdbc:mysql://127.0.0.1:3306/edu?useSSL=false&serverTimezone=UTC maximumPoolSize=20 minimumIdle=5 connectionTimeout=30000- 避免N+1:课程列表一次性
LEFT JOIN teacher,结果集用Map<Integer,List<Teacher>>分组,减少循环查库
六、生产环境避坑指南
Tomcat路径空格
Windows把项目放Program Files,路径含空格导致getRealPath()返回%20,文件上传报404。统一用C:\opt\tomcat\webapps。MySQL8时区
未写serverTimezone=UTC会抛The server time zone value 'Öйú±ê׼ʱ¼ä',在jdbcUrl显式声明。中文乱码
- 数据库
utf8mb4 - 页面
<meta charset="utf-8"> response.setContentType("text/html;charset=utf-8")- 统一Filter在最前链
- 数据库
热部署与生产
IDEA热部署插件改class不重启,演示很爽;生产务必关reloadable=true,否则Full GC狂飙。
七、完整可运行代码获取
仓库地址(Gitee):https://gitee.com/yourname/edu-manage
clone后执行:
- 导入sql/edu.sql
- 改
src/main/resources/db.properties - mvn clean package
- 把target/edu-manage.war丢进Tomcat webapps,启动即访问http://localhost:8080/edu-manage
八、下一步:把项目演进成微服务
拆分边界
- user-service:注册、鉴权、JWT颁发
- course-service:课程CRUD
- score-service:成绩计算、统计
共享数据
用MyBatis-Plus + shardingsphere做分库分表,避免“一个库扛全校”。网关与前端
Spring Cloud Gateway统一路由;前端Vue3+AntV,成绩统计直接出雷达图,答辩秒变亮点。
写在最后
整套教务系统没有黑科技,却能把HTTP、字符编码、事务、安全这些基本功串成线。
把代码跑通后,不妨先给“成绩”模块加个柱状图,体会一把前端调接口的爽点;再把服务拆开,用Docker Compose起三个容器,你就拥有了微服务雏形。
毕业设计不是终点,而是把“写代码”变成“做系统”的第一站——动手吧,下一位拿优秀论文的就是你。