简介这是一份基于 JSP 与 Servlet 实现的学生信息管理系统完整源码包适合正在学习 Java Web 开发或需要完成课程设计的读者。项目涵盖登录认证、学生信息维护、成绩管理等功能模块可作为理解 MVC 分层、JDBC 数据库操作、HTTP 请求响应流程的实战案例。压缩包共 576 个文件大小约 2.75 MB包含 156 个 HTML 页面、132 个 CSS 样式、79 个 JavaScript 脚本、20 个 Java 源码及相关编译 class同时提供 SQL 初始化脚本和项目配置文件便于导入数据库后直接部署调试。内容明确包含 StudentDao、LoginServlet、StudentListServlet、StudentSaveServlet、GradeListServlet 等核心类能清晰看到从界面、控制层到数据访问层的完整实现路径。目前已有 997 人学习适合需要参考完整编码思路、梳理 Servlet 生命周期或快速搭建课设项目的 Java 学习者。1. 学生信息管理系统JSPServlet把课堂作业做成能上线的CRUD框架刚学完Java Web那阵子我用JSPServlet写过一个学生信息管理系统。写完那一刻很爽——登录、增删改查、分页、模糊搜索全都有但换个电脑跑就原地翻车不是数据库连不上就是Tomcat路径对不上后来才意识到这种项目真正的价值不是技术有多新而是把Servlet生命周期、请求转发、Session鉴权、JDBC操作这些Java Web核心概念完整串了一遍。这份源码包适合两类人一类是刚学完Servlet/JSP、想把理论落地的初学者另一类是接了个老系统维护任务的开发者——你不需要重造轮子照着这个项目的分层思路几分钟就能把需求改出来。2. 技术选型与架构为什么Servlet做控制层JSP只负责渲染2.1 技术栈与版本搭配这个项目用的是经典Java Web套装JDK 8 Tomcat 9 Servlet 4.0 JSP 2.3 MySQL 5.7外加Druid连接池和JSTL标签库。相比Spring Boot那一套这套组合看起来复古但好处是依赖少、启动快Servlet容器自己就能跑不需要额外装配Spring的上下文环境。对于理解HTTP请求如何被Java代码处理这反而是个很干净的学习样本。组件版本建议用途JDK1.8编译运行环境Tomcat9.0.xServlet容器Servlet API4.0请求响应处理JSP2.3视图渲染JSTL1.2标签库替代脚本片段MySQL5.7 / 8.0数据存储Druid1.2.x数据库连接池我一般会把Druid的配置文件单独放一份不要和web.xml混在一起。这个项目里druid.properties放在src根目录里面配了初始连接数、最大连接数、空闲检测周期等参数。很多新手会把账号密码直接写在Servlet里这个项目没有这么做而是用一个工具类来读取配置文件后期换数据库只需要改一行配置。2.2 三层架构与请求流转整个项目没有引入Spring但包结构是标准的三层架构com.example.student.servlet放Controllercom.example.student.service放业务逻辑com.example.student.dao放数据访问。JSP直接放在webapp根目录下或者放在WEB-INF/views里防止直接访问。请求流转路径非常典型浏览器发起HTTP请求Tomcat根据web.xml里的servlet-mapping找到对应的Servlet。Servlet调用Service层处理业务比如校验用户名密码、计算分页页码。Service层调用DAO层执行SQLDAO使用JDBC获得ResultSet后封装成JavaBean。业务处理完毕Servlet通过request.getRequestDispatcher().forward()转发到JSP页面由JSP渲染HTML返回给浏览器。这套流程的核心思路是「Servlet负责流程控制JSP只负责展示」。如果你在JSP页面里看到%开头的Java代码块说明项目没有做好职责分离这个源码包里的JSP全部使用EL表达式和JSTL标签脚本片段只在极少数被注释掉的旧版本代码里出现过。2.3 目录结构与源码布局打开源码包后第一眼先看目录不要太着急跑起来。完整结构如下student-manager/ ├── pom.xml ├── src/main/java │ └── com/example/student │ ├── bean/ # Student、Admin等实体类 │ ├── dao/ # StudentDao、AdminDao接口及实现 │ ├── service/ # 业务逻辑层 │ ├── servlet/ # LoginServlet、StudentListServlet等 │ ├── filter/ # EncodingFilter、LoginFilter │ └── util/ # DBUtil、PageBean ├── src/main/resources │ └── druid.properties ├── src/main/webapp │ ├── WEB-INF │ │ ├── web.xml │ │ └── views/ # 受保护的JSP页面 │ ├── login.jsp │ ├── css/ │ └── js/ └── sql/ └── init.sqlbean包里的实体类对应数据库表字段PageBean是手写的分页工具类包含currentPage、pageSize、totalCount、totalPage和list这几个核心字段。分页逻辑没有依赖插件而是通过MySQL的LIMIT语句自己计算偏移量这对理解分页原理特别有帮助。3. 核心模块拆解登录鉴权、学生CRUD与分页查询的完整实现3.1 数据库设计与连接池配置数据库初始化脚本位于sql/init.sql建了两张表admin保存登录账号student保存学生信息。学生表字段包括学号、姓名、性别、年龄、班级、联系方式、入学时间。注意学号设置了唯一索引这是避免重复数据的关键。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(32) NOT NULL, gender VARCHAR(4), age INT, class_name VARCHAR(64), phone VARCHAR(20), enroll_date DATE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;连接池用Druid配置文件druid.properties要特别注意url参数末尾的useUnicodetruecharacterEncodingutf8没有这两项插入中文必变问号。我一般还会加上serverTimezoneAsia/Shanghai否则MySQL 8.0会报时区错误。driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 maxWait3000DBUtil工具类用Class.forName加载驱动再从DruidDataSourceFactory取出连接代码简洁但适合教学。实际生产环境里连接池参数需要根据并发量调整maxActive和maxWait这两个值如果太小高并发时会出现getConnection超时异常。3.2 登录过滤器与Session管理登录逻辑分两层第一层是LoginServlet校验用户名密码第二层是用LoginFilter拦截所有未登录请求。后者是很多同学忽略的——以为做了登录页就完事了其实直接访问student_list.jsp就能绕过鉴权。public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI(); if (path.contains(login) || path.endsWith(.css) || path.endsWith(.js)) { chain.doFilter(request, response); return; } HttpSession session request.getSession(false); if (session ! null session.getAttribute(admin) ! null) { chain.doFilter(request, response); } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }这段代码里有个容易踩的坑request.getSession(false)传了false意思是拿不到现有Session就返回null而不是新建一个。如果去掉这个参数未登录用户会每次访问都被强制创建一个Session白白浪费内存。排除静态资源和登录接口的逻辑也要注意否则CSS样式会全部失效登录页看起来像裸的HTML。3.3 学生列表的分页查询Servlet分页请求是这个项目里最有代表性的一个Servlet。它接收page和keyword两个参数page是当前页码keyword是可选的学生姓名模糊查询关键词。WebServlet(/student/list) public class StudentListServlet extends HttpServlet { private StudentService studentService new StudentService(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int page 1; String pageStr request.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { page Integer.parseInt(pageStr); } String keyword request.getParameter(keyword); if (keyword null) { keyword ; } map.put(page, page); map.put(pageSize, 8); map.put(keyword, keyword); PageBeanStudent pageBean studentService.queryPage(map); request.setAttribute(pageBean, pageBean); request.setAttribute(keyword, keyword); request.getRequestDispatcher(/WEB-INF/views/student_list.jsp).forward(request, response); } }Service层对page做了修正如果小于1强制设为1如果大于总页数设为总页数。这个防御性处理非常重要否则用户手动改URL里的?page999SQL会算出一个离谱的偏移量页面虽然不报错但数据全空让人误以为是Bug。DAO层执行的是拼接了LIMIT ? OFFSET ?的预编译SQL参数通过PreparedStatement传递能有效防SQL注入。3.4 JSP页面的EL与JSTL渲染列表页面用c:forEach遍历数据用${pageBean.currentPage}这样简洁的表达式读取数据。注意PageBean里必须提供对应的getter方法EL表达式只认getter不认公有字段。table classtable table-bordered tr th学号/th th姓名/th th性别/th th年龄/th th班级/th th操作/th /tr c:forEach items${pageBean.list} vars tr td${s.studentNo}/td td${s.name}/td td${s.gender}/td td${s.age}/td td${s.className}/td td a href${pageContext.request.contextPath}/student/edit?id${s.id}编辑/a a hrefjavascript:void(0) onclickdel(${s.id})删除/a /td /tr /c:forEach /table页面底部有分页导航用JSTL的c:if判断上一页和下一页是否可用页码用c:forEach遍历。这里有个小技巧翻页链接要带上当前的keyword参数否则你在关键词搜索的结果里翻到第3页点下一页会跳回全量列表搜索状态就丢了。4. 项目运行指南从导入到Tomcat部署的每一步4.1 环境准备与源码导入如果你想跑起来第一步不是双击pom.xml而是先把基础环境列个清单检查一遍。这个项目需要JDK 8以上、Maven 3.x、Tomcat 9.0、MySQL 5.7以上。建议用Idea导入选择Maven项目让它把依赖自动下载完。导入后先确认pom.xml里的编译级别是不是1.8。很多新手的机器装了JDK 17Tomcat 9对Servlet 4.0支持没问题但pom.xml里如果写着source1.8/source编译也能过。真正容易翻车的是Maven仓库里缺JSTL依赖Druid版本找不着此时mvn clean compile会直接报错。看到Missing artifact开头的红色日志别慌右键项目选择Maven - Reload Project。4.2 数据库初始化与配置项修改用Navicat或者命令行执行sql/init.sql脚本注意执行前确认字符集选的是utf8mb4不是latin1。执行完可以在admin表里插一条测试账号或者直接用脚本里预设的admin / 123456。接下来改druid.properties里的username和password。这里有个血泪经验有的人密码里带了#或符号在.properties文件里会被当作注释分隔符或参数连接符导致连接失败。解决办法是改用Druid的connectionProperties参数或者把密码特殊字符转义最常见的做法是换一个不包含特殊字符的测试密码。4.3 Tomcat部署与启动在Idea里配置TomcatFile - Settings - Build Tools - Servers - Tomcat选本地的安装目录然后添加一个Artifact。如果没有设置Deployment里的Application context访问路径会变得很长。我一般会把Application context设置成/这样访问地址就是http://localhost:8080/login.jsp省得每次记长路径。启动Tomcat后看到[INFO] Starting ProtocolHandler不算启动成功要看[INFO] Server startup in [xxx] milliseconds。如果控制台出现Error: Could not create a local deployed object通常是Artifact的名字和项目有重复清理Tomcat的webapps目录后重新Deploy就行。4.4 验证功能与接口自测部署完成后按照先慢后快的顺序验证先打开登录页故意输入错误密码看是否停留在原页面并提示错误。用正确账号登录后访问学生列表页看分页是否正常、搜索能否生效。然后测增删改新增一个学号在列表里搜索它编辑姓名后再搜索最后删除确认数据库里数据同步变化。我常用一个笨办法自测在浏览器F12的Network面板里看每个请求的状态码。如果新增请求返回200但列表没变化大概率是Servlet转发后页面路径写错了比如转发到了/WEB-INF/views/student_list.jsp但你的实际目录是/views/student_list.jsp。还有一种常见情况是web.xml里配了旧的servlet映射和WebServlet注解冲突导致请求被匹配到不存在的Servlet上返回404。5. 避坑与排查JSP项目最常见的五个踩坑记录5.1 中文乱码从请求到响应的全链路排查现象登录页输入中文用户名页面显示乱码表单提交后插入数据库的中文变成???。原因JSP页面没有指定编码过滤器Servlet层也没有对请求参数解码。Tomcat 9默认的URI编码是UTF-8但POST表单的Content-Type如果不包含charsetUTF-8Tomcat会按ISO-8859-1解码导致所有中文参数全坏。解决项目里写了一个EncodingFilter在过滤器链最前面设置request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)并且把web.xml的filter-mapping放在所有Servlet映射之前。另外检查druid.properties的url是否带了characterEncodingutf8这会影响写入数据库的编码。5.2 404与Servlet映射冲突现象启动Tomcat后访问/student/list返回404但访问/login.jsp正常。原因WebServlet(/student/list)注解和web.xml里同时配置了servlet-nameStudentListServlet/servlet-name两套映射不一致Tomcat优先使用web.xml里的映射如果该映射路径写错请求就找不到Servlet。解决删掉web.xml里的那组映射统一用注解或者在web.xml里把servlet-mapping的url-pattern改成和注解完全一致。我的习惯是只在web.xml保留过滤器映射Servlet全部用注解管理避免两处信息不一致。5.3 数据库连接超时与连接池泄漏现象项目刚启动能正常查数据运行几个小时后访问列表页卡住几十秒然后报java.sql.SQLException: Connection is closed。原因DAO层在获取连接后如果发生异常没有在finally块里调用close()Druid连接池的连接就会被耗尽。Tomcat默认的连接空闲超时是8小时但Druid的minEvictableIdleTimeMillis如果配得太短空闲连接会被回收而代码里还持有旧连接再使用就会报错。解决所有getConnection()都要配合try-with-resources或者finally里connection.close()。同时druid.properties里加上testWhileIdletrue、validationQuerySELECT 1让Druid在取出连接时先检测连接是否有效。5.4 JSP脚本片段报错现象在JSP里写% String name request.getParameter(name); %编译时报错说name变量未定义或者页面渲染时空白。原因web.xml里设置了scripting-invalidtrue/scripting-invalid有些项目为了安全会禁止脚本片段或者JSP版本在2.3之后默认禁用脚本片段。这个项目禁止了脚本片段所以你如果从网上复制了一段老式JSP代码进来就会报错。解决改用EL表达式和JSTL。比如${param.name}就等价于request.getParameter(name)。如果确实需要在JSP里做复杂逻辑建议把逻辑挪到Servlet的doGet里算好结果后通过request.setAttribute传给页面。5.5 Tomcat热部署失效现象改了Java代码后Idea里点Update Resources不生效页面还是旧逻辑。原因项目配置的Artifact类型是Exploded但Tomcat的server.xml里设置了reloadablefalse或者Idea的部署模式没有选Redeploy。解决在Tomcat配置里把Deployment的Update policies选成Update classes and resources同时确认server.xml中Context标签没有reloadablefalse。如果还不行直接点Redeploy或者重启Tomcat不要逞强。6. 二次开发改造从Demo到可维护的项目6.1 用Filter统一编码与权限源码包里的EncodingFilter和LoginFilter是分开的两个类实际维护时我建议合并成一个链。把编码设置放到过滤器链的最前面权限校验放在后面这样后续加一个LogFilter做日志记录时顺序不会乱。改造时只需要在web.xml里调整filter-mapping的顺序不用动业务代码。6.2 抽取BaseServlet减少重复现在很多Servlet里都有相同的逻辑设置编码、接收page参数、转发JSP。这些重复代码可以抽到一个BaseServlet里用反射根据action参数分发到具体方法。改造后的StudentListServlet只需要写list()、add()、edit()、delete()四种方法每个方法接收HttpServletRequest和HttpServletResponse代码量能减少一半左右。public class BaseServlet extends HttpServlet { protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.isEmpty()) { action list; } try { Method method this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class); method.setAccessible(true); method.invoke(this, req, resp); } catch (Exception e) { throw new ServletException(找不到对应方法: action, e); } } }使用时让子类继承BaseServlet在类上标注WebServlet(/student)访问/student?actionlist就能调用list方法。这个模式当年是Struts的简化版写起来轻量也方便后续替换成SpringMVC。6.3 把JDBC换成分层框架如果想把项目升级到能上生产维护的程度最值得做的是把DAO层的裸JDBC替换成能管理事务的方案。我的建议是分两步第一步引入Spring JDBC的JdbcTemplate保留现有包结构只需要让DAO继承一个带JdbcTemplate的基类SQL语句基本不用改第二步再把Service层改造成Transactional声明式事务这样登录日志写入和学生操作就能保证原子性。替换过程中有个细节原来DAO层返回的ListStudent是手写遍历ResultSet的换成JdbcTemplate后用BeanPropertyRowMapper就能自动映射数据库列到实体属性。但前提是实体类的属性名和数据库列名的驼峰转换规则要对得上比如数据库的student_no对应实体类的studentNoBeanPropertyRowMapper默认支持这种下划线到驼峰的映射。做完这些改造后项目基本就是一个结构清晰的Java Web应用了。每次我拿到一个老项目第一件事就是把输入到输出的链路图在心里过一遍看哪些Servlet是纯转发、哪些有真实业务然后才动手改代码。自从经历过一次乱改导致生产环境连续三天报空指针的事故我就养成了习惯任何涉及过滤器顺序和数据库连接的改动都必须先在本地把增删改查全流程走一遍再往上合。这个项目虽然技术栈偏旧但分层思想一点不过时希望这份解析能帮你在跑通之后把它的组织方式也学到手。本文还有配套的精品资源点击获取