☰
JSP+Servlet实战:手写学生信息管理系统,掌握CRUD与登录鉴权
2026/10/17 7:04:59 网站建设 项目流程

简介:这是一份基于 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. 学生信息管理系统:JSP+Servlet把课堂作业做成能上线的CRUD框架

刚学完Java Web那阵子,我用JSP+Servlet写过一个学生信息管理系统。写完那一刻很爽——登录、增删改查、分页、模糊搜索全都有,但换个电脑跑就原地翻车,不是数据库连不上,就是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放Controller,com.example.student.service放业务逻辑,com.example.student.dao放数据访问。JSP直接放在webapp根目录下,或者放在WEB-INF/views里防止直接访问。请求流转路径非常典型:

  1. 浏览器发起HTTP请求,Tomcat根据web.xml里的<servlet-mapping>找到对应的Servlet。
  2. Servlet调用Service层处理业务,比如校验用户名密码、计算分页页码。
  3. Service层调用DAO层执行SQL,DAO使用JDBC获得ResultSet后封装成JavaBean。
  4. 业务处理完毕,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.sql

bean包里的实体类对应数据库表字段,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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

连接池用Druid,配置文件druid.properties要特别注意url参数末尾的useUnicode=true&characterEncoding=utf8,没有这两项,插入中文必变问号。我一般还会加上serverTimezone=Asia/Shanghai,否则MySQL 8.0会报时区错误。

driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxActive=20 maxWait=3000

DBUtil工具类用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); PageBean<Student> 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里的?page=999,SQL会算出一个离谱的偏移量,页面虽然不报错,但数据全空,让人误以为是Bug。DAO层执行的是拼接了LIMIT ? OFFSET ?的预编译SQL,参数通过PreparedStatement传递,能有效防SQL注入。

3.4 JSP页面的EL与JSTL渲染

列表页面用c:forEach遍历数据,用${pageBean.currentPage}这样简洁的表达式读取数据。注意PageBean里必须提供对应的getter方法,EL表达式只认getter,不认公有字段。

<table class="table table-bordered"> <tr> <th>学号</th> <th>姓名</th> <th>性别</th> <th>年龄</th> <th>班级</th> <th>操作</th> </tr> <c:forEach items="${pageBean.list}" var="s"> <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 href="javascript:void(0)" onclick="del(${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 17,Tomcat 9对Servlet 4.0支持没问题,但pom.xml里如果写着<source>1.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里配置Tomcat:File -> 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如果不包含charset=UTF-8,Tomcat会按ISO-8859-1解码,导致所有中文参数全坏。

解决:项目里写了一个EncodingFilter,在过滤器链最前面设置request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8"),并且把web.xml的<filter-mapping>放在所有Servlet映射之前。另外检查druid.properties的url是否带了characterEncoding=utf8,这会影响写入数据库的编码。

5.2 404与Servlet映射冲突

现象:启动Tomcat后,访问/student/list返回404,但访问/login.jsp正常。

原因:@WebServlet("/student/list")注解和web.xml里同时配置了<servlet-name>StudentListServlet</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里加上testWhileIdle=true、validationQuery=SELECT 1,让Druid在取出连接时先检测连接是否有效。

5.4 JSP脚本片段报错

现象:在JSP里写<% String name = request.getParameter("name"); %>,编译时报错说name变量未定义,或者页面渲染时空白。

原因:web.xml里设置了<scripting-invalid>true</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里设置了reloadable="false",或者Idea的部署模式没有选Redeploy。

解决:在Tomcat配置里把Deployment的Update policies选成Update classes and resources,同时确认server.xml中Context标签没有reloadable="false"。如果还不行,直接点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?action=list就能调用list方法。这个模式当年是Struts的简化版,写起来轻量,也方便后续替换成SpringMVC。

6.3 把JDBC换成分层框架

如果想把项目升级到能上生产维护的程度,最值得做的是把DAO层的裸JDBC替换成能管理事务的方案。我的建议是分两步:第一步引入Spring JDBC的JdbcTemplate,保留现有包结构,只需要让DAO继承一个带JdbcTemplate的基类,SQL语句基本不用改;第二步再把Service层改造成@Transactional声明式事务,这样登录日志写入和学生操作就能保证原子性。

替换过程中有个细节:原来DAO层返回的List<Student>是手写遍历ResultSet的,换成JdbcTemplate后,用BeanPropertyRowMapper就能自动映射数据库列到实体属性。但前提是实体类的属性名和数据库列名的驼峰转换规则要对得上,比如数据库的student_no对应实体类的studentNo,BeanPropertyRowMapper默认支持这种下划线到驼峰的映射。

做完这些改造后,项目基本就是一个结构清晰的Java Web应用了。每次我拿到一个老项目,第一件事就是把输入到输出的链路图在心里过一遍,看哪些Servlet是纯转发、哪些有真实业务,然后才动手改代码。自从经历过一次乱改导致生产环境连续三天报空指针的事故,我就养成了习惯:任何涉及过滤器顺序和数据库连接的改动,都必须先在本地把增删改查全流程走一遍再往上合。这个项目虽然技术栈偏旧,但分层思想一点不过时,希望这份解析能帮你在跑通之后,把它的组织方式也学到手。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询