尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

学生选课系统JavaWeb源码拆解:Servlet+JSP+MySQL+Tomcat

学生选课系统JavaWeb源码拆解:Servlet+JSP+MySQL+Tomcat 简介这是一套基于ServletJSP的学生选课管理系统完整源码包面向计算机相关专业正在准备毕业设计的学生以及需要JavaWeb项目实战练习的初学者。项目采用经典三层架构整合MySQL数据库实现了系统管理员、教师、学生三类角色的核心功能管理员维护学生、教师与课程信息教师可查看课程及学生并录入成绩学生则能浏览课程、完成选课并查询成绩业务闭环完整可直接运行或二次开发。压缩包约2.39MB共112个文件包含22个Java源文件、21个JSP页面、10个JAR依赖库以及SQL数据库脚本、CSS和JS前端样式、图片与配置文件等目录结构清晰便于导入IDEA或Eclipse后快速部署调试。目前该资源已有554人学习下载适合需要快速获得可演示项目或理解ServletJSPMySQL开发流程的读者。整体而言这份资源既能支撑毕业设计答辩也能作为JavaWeb入门实战的参考模板。1. 学生选课管理系统一份能跑通全流程的 JavaWeb 源码每到选课季教务处总会被「课程满了」「什么时候开新班」这类问题淹没对学生开发者来说「学生选课管理系统」恰好是 JavaWeb 课设里业务最完整的题目之一——前端页面、Servlet 控制层、MySQL 表设计、Tomcat 部署四个环节全能练到。这份源码带完整工程和数据库脚本技术栈是经典的 Java Servlet JSP MySQL Tomcat没有 Spring 全家桶包装裸 Servlet 更容易看清一个请求从浏览器到数据库再回来的完整路径。适合三类人做 JavaWeb 课程设计的在校生、想补 Servlet/JSP 实战经验的新手、要拿可改代码当讲稿的讲师。下面按拆解路线把角色权限、数据库脚本、部署过程和踩坑点逐一写清楚。2. 角色权限与业务流先搞清楚三个身份各自能干什么在拆代码之前建议先在 login.jsp 上把三个账号各登一遍。学生选课系统的核心不是某个算法而是「谁在什么条件下能做什么」这套权限规则。三个角色对应完全不同的页面、Servlet 和数据范围学生登录后看到本学期开放课程列表可以选课、退课期末查看成绩教师看到的是自己名下的课程和学生名单负责录入成绩管理员做课程维护和账号维护。搞清这套规则再读数据库和代码才有坐标。2.1 登录鉴权与会话控制的做法登录表单把 username、password、role 三个参数 POST 到 LoginServlet。常见做法是先做 trim 和判空再去登录账号表按username, password, role三条件查询密码用 MD5 后比较查到说明凭证有效就把用户信息放进 HttpSession最后按 role 跳转到对应首页。下面是我拆同类项目最常见的写法WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); String role request.getParameter(role); if (username null || password null || role null || username.trim().isEmpty() || password.trim().isEmpty()) { request.setAttribute(msg, 用户名和密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); String sql SELECT id, real_name, ref_id FROM sys_user WHERE username ? AND password ? AND role ?; ps conn.prepareStatement(sql); ps.setString(1, username.trim()); ps.setString(2, MD5Util.encode(password.trim())); ps.setString(3, role); rs ps.executeQuery(); if (rs.next()) { User user new User(); user.setUsername(username.trim()); user.setRealName(rs.getString(real_name)); user.setRole(role); user.setRefId(rs.getInt(ref_id)); HttpSession session request.getSession(); session.setMaxInactiveInterval(30 * 60); session.setAttribute(user, user); if (student.equals(role)) { response.sendRedirect(student/index.jsp); } else if (teacher.equals(role)) { response.sendRedirect(teacher/index.jsp); } else { response.sendRedirect(admin/index.jsp); } } else { request.setAttribute(msg, 用户名、密码或角色不匹配); request.getRequestDispatcher(/login.jsp).forward(request, response); } } catch (Exception e) { e.printStackTrace(); request.setAttribute(msg, 系统异常请稍后再试); request.getRequestDispatcher(/login.jsp).forward(request, response); } finally { DBUtil.close(conn, ps, rs); } } }说明几个关键点。第一setCharacterEncoding(UTF-8) 必须放在整套代码最前面放在其它位置对 request 参数已经来不及这是中文乱码最常见的来源之一。第二用 PreparedStatement 而不是字符串拼接进 SQL是为了躲开注入像 usernameadmin or 11 这类输入在预编译下就是一个普通字符串。第三password 存 MD5 值登录时把输入 MD5 后再比对数据库脚本泄露时至少不直接暴露明文。这套做法在课设层面够用但要清醒一点MD5 本身不算安全方案只是比明文强。会话控制上登录成功设置了 30 分钟过期Session 里挂的是 user 对象角色和 ref_id 都在里面。30 分钟后用户没操作会被强制重新登录这只是 Session 默认行为真正拦访问路径要靠 Filter后面部署章节我会给完整示例。要提一下登录账号表的命名脚本里有的叫 sys_user 有的叫 user叫 sys_user 是为了避开 MySQL 自带 user 系统表的命名冲突查询时注意表名别写错成带反引号的 user。2.2 学生端选课与退课的完整流程学生端最核心的页面是课程列表页每行课程带一个「选课」按钮提交时把 courseId 给到 CourseServlet。选课逻辑上要过三关是不是重复选、课程容量满了没有、有没有时间冲突。代码层面的执行顺序我固定为先做唯一性检查从 sc 表查一次再做容量扣减原子 UPDATE最后插入选课记录整个过程包在事务里。先查 sc 表是在给用户一个友好提示真正防重复靠的是数据库唯一索引这两件事不冲突。时间冲突的做法是给 course 表加 weekday 和 time_slot 字段选课前查「当前学生已选课程里有没有 weekdaytime_slot 相同的」有就提示冲突。退课则是删掉 sc 记录同时把 course 表的 selected 减回去。这里有个隐藏边界selected 可能被减成负数。如果退课前没判断学生确实选过这门课两次退课请求并发进来容量计数就会错乱所以退课也必须先查 sc 存在再删并且 UPDATE 扣减的 SQL 里加 WHERE selected 0。顺带说一下 Servlet 的事件分发习惯。很多课设源码喜欢一个入口 Servlet 加 action 参数区分操作比如 CourseServlet?actionselect、CourseServlet?actionback这样类少写几个但方法里会出现一长串 if-else。更清晰的做法是选课、退课各拆一个方法在 doPost 里按 action 分发给不同处理方法。读这份源码时先看它是哪种风格如果是 if-else 全堆在一个方法里重构第一步就是把选课、退课各自抽出来后面加逻辑会好受得多。2.3 教师端成绩录入与管理员的边界教师登录后看到的课程列表靠 sys_user 的 ref_id 关联 teacher 表主键再按 teacher 表的 tno 去 course 表匹配 teacher 字段。成绩录入是标准的单表更新update sc set score ? where student_id ? and course_id ?。因为 sc 表里一个学生一门课只有一条选课记录不会出现影响多行的问题。分数录入页通常要用循环把学生列表渲染出来每个输入框的 name 带上 student_idServlet 端循环逐条 update而不是只接一个 score 参数。管理员端的课程维护涉及增删改查四个常规操作。最容易翻车的是删除课程如果 sc 表里已经有人选了这门课直接 delete course 会产生孤儿选课记录外键约束开着的库会直接拒绝删除报外键违反错误。处理方式两种一是删除前 count sc 表有选课记录就拒绝删除提示先停课二是给 course 表加 status 字段做逻辑删除学生端只查 status1 的课程。后者在答辩里更容易讲出深度也是真实教务系统更常见的做法。3. 数据库脚本落地从 SQL 导入到表结构拆解整套系统的数据基础是五张表sys_user、student、teacher、course、sc。源码包里一般会附带一个 course_system.sql 或 db_course.sql拿到手第一件事就是把它导入 MySQL同时确认表结构跟第 2 章的角色规则对得上。我的习惯是先走命令行导入因为命令行能顺手验证 MySQL 账号权限和字符集比在 IDEA 面板里直接点导入更能暴露底层的坑。3.1 MySQL 建库与导入的两种方式命令行导入就这两条mysql -u root -p -e CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -u root -p course_system course_system.sql第一条命令建库第二条把 SQL 文件重定向导入到 course_system 库。如果 SQL 文件里已经自带 CREATE DATABASE 语句第一条可以跳过直接走第二条。执行时注意两点mysql 客户端要在 PATH 里Windows 下不在就写全路径SQL 文件路径带空格时用双引号包住。导入完成后用 show tables; 看五张表是否齐全再用 desc sc; 确认选课表的唯一索引在不在。在 IDEA 里操作是另一种常见方式右侧 Database 面板新建 MySQL 连接填好主机、端口、账号密码先建空库 course_system然后右键 schema 选择 Run SQL Script... 执行。好处是能看到每条语句的执行结果和报错行号坏处是连接本身要先排对驱动版本和时区。新手我更推荐命令行报错信息更直接也顺便练了 mysql 常用命令。导入后顺手确认初始账号一般脚本里会带几条测试数据比如管理员 admin/123456学生账号 student01/123456教师账号 teacher01/123456。用 SELECT 把 sys_user 表列出来看看确认角色字段和密码是否已经是 MD5 串如果发现密码明文直接存着建议先跑 md5 函数刷一遍再进下一步。3.2 核心表结构拆解五张表里sys_user 管登录凭据student 和 teacher 管身份详情course 管课程sc 管选课与成绩。典型建表语句如下字段做了精简但结构完整CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT MD5后的密码, role VARCHAR(10) NOT NULL COMMENT student/teacher/admin, ref_id INT COMMENT 关联student或teacher表的主键, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, sname VARCHAR(30) NOT NULL COMMENT 姓名, major VARCHAR(50), class_name VARCHAR(30) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, cname VARCHAR(50) NOT NULL COMMENT 课程名, teacher VARCHAR(30) COMMENT 授课教师, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, capacity INT DEFAULT 60 COMMENT 课程容量, selected INT DEFAULT 0 COMMENT 已选人数, weekday INT COMMENT 星期几1-7, time_slot INT COMMENT 第几大节 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sc ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) COMMENT 成绩教师录入, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course (course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段的用意说一下。sys_user 的 role 字段决定登录后跳哪个首页也是后续权限拦截的依据password 设计成 VARCHAR(64)因为 MD5 十六进制输出固定 32 位如果想升级成加盐 MD5 或 SHA-25664 位长度不用改表结构。course 表的 selected 是个冗余计数它不是必须的可以用 count(sc) 现算但冗余在这里课程列表页能直接展示容量状态每次选课退课都要维持它和 sc 表一致这就是事务必须包住两个写操作的原因。数据流向也理一遍登录请求进来sys_user 表按三条件过滤拿到 ref_id再去 student 或 teacher 表补全详情选课请求新增 sc 记录student_id 对应 student 表主键course_id 对应 course 表主键成绩录入是给 sc 表已存在记录补 score。从登录到出成绩正好串起五张表纸上画一遍关系比直接读十个 Java 类高效得多。3.3 唯一约束与外键防重复选课的底层保障sc 表上最关键的是 UNIQUE KEY uk_student_course (student_id, course_id)这一行约束在数据库层面拒绝同一学生对同一门课的重复选课。就算 Servlet 忘了查重第二次 INSERT 会直接报 Duplicate entry 错误事务一抛异常就回滚。我读源码时会特意先看这个唯一索引在不在很多网上下载的版本建表脚本没有它全靠代码判断并发一上来就漏。外键的作用有两个。一是保证 sc 引用的 student_id、course_id 必须真实存在不产生孤儿数据二是删除课程或学生时如果还有选课记录引用数据库会拒绝 DELETE逼你在业务层先处理选课记录。不少课设源码为了省事会去外键只留索引跑起来确实没差但数据库约束属于「平时不显眼、出问题就是大事」的那种东西建议保留外键读代码时体会它带来的行为变化。4. Tomcat 部署与核心代码走读让 Servlet 真正跑起来4.1 从源码到能访问的部署流程这套系统不用 Maven 管理的话源码结构一般是 src 目录放 Java 类web 目录放 JSP 和 WEB-INF典型的动态 Web 工程。最常见的是用 IDEA 跑File → Open 选择工程IDEA 识别出 Web 工程后Project Structure → Artifacts 里确认 Web Application Exploded 已配置然后 Run → Edit Configurations 新建 Tomcat Server → LocalDeployment 标签把 artifact 加进去Application context 建议改成 /course。启动后访问 http://localhost:8080/course/login.jsp 就能看到登录页。另一种方式是打 WAR 包丢进 Tomcat。IDEA 里 Build Artifacts 导出 WAR如果手里是 Maven 结构就执行 mvn clean package把生成的 .war 复制到 Tomcat 的 webapps 目录启动 Tomcat 自动解压。这种方式更适合验收环境不依赖 IDE。版本匹配是重点Tomcat 8.5/9 用 JDK 8 没问题Tomcat 10 及以上把 javax.servlet 换成了 jakarta.servlet网上大量老源码是 javax 的直接扔进 Tomcat 10 会报 NoClassDefFoundError只能换回 Tomcat 9。这是老 javaWeb 项目最容易踩的版本坑没有之一。4.2 登录拦截与 Session 生命周期部署好后会发现一个尴尬的事不登录也能直接敲 URL 访问 student 页面。这就是第 2 章说的 Filter 的价值。常见登录拦截写法如下WebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; HttpSession session request.getSession(false); String uri request.getRequestURI(); String ctx request.getContextPath(); if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.startsWith(ctx /static/)) { chain.doFilter(req, res); return; } if (session null || session.getAttribute(user) null) { response.sendRedirect(ctx /login.jsp); return; } chain.doFilter(req, res); } }逻辑是白名单路径直接放行包括登录页、登录接口、静态资源其它所有请求必须带有效 Session否则一律重定向回登录页。注意 request.getSession(false) 里这个 false 很关键它表示「拿不到 Session 就返回 null而不是新造一个」否则你访问任何资源都会被强制生成新 Session白名单判定就形同虚设。实际项目里我还会在 Filter 里加角色路径校验比如 /teacher/* 只允许 roleteacher 访问防止学生手动改 URL 访问教师接口。这份源码如果只有登录拦截属于够用但不算完整角色级拦截是后续值得加的点。4.3 选课事务用一条 UPDATE 挡住并发超选学生选课是这套系统里唯一值得认真看的高并发逻辑。不少源码版本的做法是先 SELECT 查 selected 和 capacity判断没满再 INSERT最后 UPDATE selected1。单用户测试没问题但教务系统在选课高峰是多用户同时操作的两个请求同时 SELECT 到「还没满」然后都执行 INSERTselected 就可能超出 capacity。解决不复杂把「判断容量」和「扣减名额」合并成一条原子 UPDATEConnection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String capSql UPDATE course SET selected selected 1 WHERE id ? AND selected capacity; PreparedStatement capPs conn.prepareStatement(capSql); capPs.setInt(1, courseId); int rows capPs.executeUpdate(); if (rows 0) { conn.rollback(); request.setAttribute(msg, 课程已被选满请选择其它课程); request.getRequestDispatcher(courseList.jsp).forward(request, response); return; } String insertSql INSERT INTO sc (student_id, course_id) VALUES (?, ?); PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, studentId); insertPs.setInt(2, courseId); insertPs.executeUpdate(); conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); request.setAttribute(msg, 选课失败 e.getMessage()); request.getRequestDispatcher(courseList.jsp).forward(request, response); } finally { DBUtil.close(conn, null, null); }这段的核心是 UPDATE 里的 AND selected capacity。InnoDB 下 UPDATE 会给匹配行加行锁两个并发请求抢同一门课时后到的会等锁等它拿到锁再执行selected 已经被前一个请求加过 1条件 selected capacity 自然不成立影响行数返回 0。rows 0 就说明容量满了直接回滚提示。配合 sc 表唯一索引兜底重复选课这在课设层面已经很稳。细节注意两点setAutoCommit(false) 之后任何一步异常都要在 catch 里 rollback否则事务残留未提交状态把连接池里的连接弄脏finally 里关闭连接时注意 PreparedStatement 和 ResultSet 的释放顺序。DBUtil 的 close 方法一般会做三级 try-finally读源码时看下它的实现——很多翻车其实发生在资源没关干净。如果选课失败但 console 没有明显报错优先怀疑事务没回滚异常发生时连接带着未提交事务回到连接池下一次操作拿到同一个连接看到的还是旧数据表现成诡异的「选课没反应」。5. 避坑指南部署与选课逻辑里的四个常见问题这套源码整体能直接跑通但换一台机器、换一个 MySQL 版本问题就出来了。拆这类 javaWeb 项目遇到最多的问题集中在四个地方按出现频率从高到低写。5.1 数据库连接失败驱动、时区与密码三连坑现象Tomcat 启动后点击页面Tomcat localhost 日志或页面抛 Communications link failure或抛 Access denied for user rootlocalhostusing password: YES。原因第一个是 MySQL 8 驱动类名变了老代码 Class.forName(com.mysql.jdbc.Driver) 会报 ClassNotFoundException要改成 com.mysql.cj.jdbc.Driver第二个是 MySQL 8 对时区有强制要求连接串缺 serverTimezoneAsia/Shanghai 会在取连接时报错第三个是 DBUtil 里写死的 root 密码跟本机 MySQL 不一致。解决把 DBUtil.java 或 db.properties 的连接串统一改成带时区参数的版本驱动 jar 换 mysql-connector-java 8.x密码同步。这三个问题经常叠加建议按顺序查先把驱动 jar 放进 WEB-INF/lib再看类名最后核对连接串。jdbc.urljdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的MySQL密码5.2 中文乱码请求、响应、数据库三处编码现象登录页输入中文选课后页面显示「??」或写进数据库的中文变成问号。原因三处编码没对齐。请求侧setCharacterEncoding(UTF-8) 位置不对或没写数据库侧表字符集不是 utf8mb4或连接串缺 characterEncodingutf8响应侧JSP 页面头部没写 % page contentTypetext/html; charsetUTF-8 %。解决统一编码链。连接串加 useUnicodetruecharacterEncodingutf8建库建表用 utf8mb4所有 Servlet 的 doGet/doPost 第一行写 request.setCharacterEncoding(UTF-8)JSP 检查 page 指令。我把它当配置清单每拆一个老项目都先核对一遍能省掉大半乱码问题。Tomcat 8.5 及以上版本 URI 编码默认 UTF-8老 Tomcat 7 还要在 server.xml 里配 URIEncodingUTF-8这个细节容易忽略。5.3 选课并发超选测试时看不出、压测才暴露的隐性问题现象单机点击选课一切正常多开几个浏览器窗口同时点选同一门课或脚本并发请求selected 超过了 capacity。原因先 SELECT 再 INSERT 的「检查后写入」不是原子的。两个请求检查时容量都未满于是都执行插入就超了。这个 bug 在演示环境很难复现因为人手点击速度远达不到触发并发的程度属于典型的「测试全绿、压测翻车」。解决把容量判空合并进 UPDATE用影响行数判断再补唯一索引防重复。拿到源码后先看选课功能是哪种写法如果是先查后插直接改成原子 UPDATE 再跑并发验证这是整个系统里最值得改的一处逻辑。5.4 Tomcat 端口占用与热部署失效现象IDEA 启动报 Port 8080 was already in use或者改 Java 代码后点重启页面还是旧逻辑。原因8080 被占用通常是上一个 Tomcat 实例没关干净或别的软件占了端口热部署失效多半是 Debug 模式没配 Update resources或改了 web.xml、Filter、注解这类配置级内容必须重启才生效。解决Windows 下用 netstat -ano | findstr 8080 找 PID再 taskkill /F /PID 对应进程也可以改 Tomcat 安装目录 conf/server.xml 里 换成没被占用的端口。IDEA Run 配置里把 On frame deactivation 设为 Update classes and resources并且记住改配置文件时必须重启服务器。6. 验证与扩展把系统改成带容量下限校验的版本源码拿到手跑通只是第一步。拆完这套系统后我给自己定的规矩是先按业务流过一遍验收清单再挑一处做小改造。这样能确认自己真读懂了代码也给课设答辩或二次开发留出可讲的素材。以下是从登录到成绩闭环的验收清单建议按顺序测一遍。模块操作预期结果关注点登录输错密码页面提示错误不跳 500错误信息处理登录正确登录学生账号跳转学生首页Session 正常写入选课选一门未选课程列表状态变已选selected 正确 1选课重复选同一门提示已选过唯一索引与代码查重选课对已满课程选课提示课程已满原子 UPDATE 返回 0退课退掉已选课程退课成功可重选selected 正确 -1成绩教师录入成绩学生端可查到分数sc.score 落库正确验收原则是每项操作都要看异常分支退课时退一个没选的课会怎样教师录一个超过 100 的分数能不能拦住。这类边界往往是答辩时老师最爱追问的点。验收之后值得做的小改造我推荐给 course 表加容量下限校验。管理员修改课程容量时存在隐蔽问题把 60 人的课改成 30 人而当前已选 45 人数据就进入「已选大于容量」的矛盾状态。服务端不能只取页面传来的新容量直接 UPDATE要把旧值一起读出来对比。更稳妥的做法是让数据库自己拒绝UPDATE course SET capacity ? WHERE id ? AND selected ?;把 capacity 改成新值 x 时要求当前 selected x也就是新容量至少装得下已选学生。受影响行数为 0 时Servlet 端返回「新容量不能小于已选人数」。这个写法跟第 4 章防超量同思路把业务条件写进 WHERE让数据库在行锁保护下做判断而不是应用层先查再改。我第一次拆学生选课系统时自以为把代码读透了结果并发选修测试里把容量干超了一倍后来定位到是先查再写的竞态。从那以后我只要碰带容量、库存、余票这类计数的业务都强制要求自己走「原子扣减 唯一约束 事务回滚」这条固定流程宁可多写几行 SQL也不赌检查后写入不会出问题。这套源码结构清楚、表设计完整适合用来做第一次 Servlet 项目全流程走读希望这次拆解能帮你在部署和答辩时少走几个弯路。本文还有配套的精品资源点击获取
返回列表