简介面向Java Web初学者的一份学生信息管理系统完整项目基于JSP与Servlet技术栈实现覆盖MVC分层设计、JDBC数据库交互、HTTP请求响应流程、前端页面构建等核心知识点。压缩包内共576个文件体积仅2.75MB20个Java源文件和20个class文件对应业务逻辑与Servlet控制器4个JSP页面负责视图展示156个HTML、132个CSS、79个JS构建界面层另有SQL数据库初始化脚本、XML配置、jar依赖包等目录结构清晰导入Eclipse或IDEA即可运行。已有996人学习使用。项目中用户通过浏览器发起请求Servlet接收并调用JDBC操作数据库再将结果交由JSP渲染完整展现了Java Web分层开发的典型流程。同时提供学生增删改查、登录鉴权、成绩管理等功能模块适合作为课程设计、毕业设计或入门实战参考尤其能帮助理解Servlet与JSP如何协作以及数据库脚本在项目初始化中的作用。1. 学生信息管理系统还选JSPServlet这套老代码反而能让你看懂全链路接手过一个维护了三年的老项目界面还是JSP Bootstrap后端清一色Servlet数据库MySQL 5.7放在内网服务器上。当时满脑子都是Spring Boot可真去改那些代码时才发现恰恰是这套朴素到极点的技术组合把HTTP请求怎么进来、Servlet生命周期怎么走、Session怎么保持状态、WAR包怎么被Tomcat解压这些底层逻辑暴露得明明白白。如果你正准备做基于JSP的毕设选题或者javaweb头歌实训答案里正好卡在学生管理这类题目上这套项目比复杂框架更适合拿来复现。它覆盖登录校验、学生增删改查、分页列表、统一异常处理、打包部署全流程没有框架黑匣子每一层代码都能翻出来看。顺着下面的步骤搭一遍从新建项目到最后跑起来大概一个下午就能完成。2. 架构与运行环境从Servlet生命周期说起搭建分层项目2.1 为什么还选JSPServlet这套老组合看到标题里的“老组合”别急着划走。JSP本质是Servlet的模板化封装容器会把JSP文件转成对应的Java Servlet再编译执行。这意味着你在JSP里写的每一行Java脚本最终都会变成某个Servlet的service方法里的代码只是容器帮你隐藏了翻译过程。用这套技术栈做学生信息管理系统最大的价值在于能看清一条完整链路浏览器发起请求Tomcat根据web.xml里的映射规则找到对应Servlet容器调用其生命周期方法Servlet执行业务逻辑后forward到JSP渲染页面。这套链路一旦建立之后再看Spring MVC的DispatcherServlet反而容易理解得多。Servlet生命周期有五个阶段加载与实例化、调用init方法初始化、调用service方法处理请求、调用destroy方法销毁、由容器回收。其中service方法是每次请求都会执行的init只执行一次。对应到具体代码里你会在init或构造方法里初始化数据库连接池在service或者doGet/doPost里处理参数和业务逻辑在destroy里关闭资源。这套学生管理系统代码里没有刻意展示全部生命周期但你在写自己的登录Servlet时顺藤摸瓜就能观察到init被调用了几次。2.2 初始化从零新建IDEA JSP项目配置Tomcat网上搜“idea新建jsp项目”会出来一堆版本各异的界面截图这里用当前主流IDEA版本流程。新建项目时选择Java Enterprise在Additional Libraries勾选Web Application应用服务器选你本机装好的Tomcat。注意一个细节IDEA默认会生成Tomcat的Smart Tomcat插件配置但很多情况下你仍需要手动指定本地Tomcat路径。新建完成后项目结构雏形已经包含web目录和WEB-INF目录。# 以下是IDEA中需要手动确认的几项配置便于排查问题 Tomcat版本: apache-tomcat-9.0.xx JDK版本: 1.8 或 11 项目Languag level: 8 Artifact类型: war exploded逻辑说明war exploded是开发模式IDEA会把项目按解压目录结构部署到Tomcat修改JSP或Java类后重启Tomcat即可生效不需要手动打包。第一行注释里列出这几个配置是为了建立统一基线因为很多路径报错、端口冲突都来自这些基础参数不一致。参数说明Tomcat版本直接用9或10都行但JDK版本如果选太高部分兼容性差的Servlet API会出现奇怪报错建议1.8起步。Artifact里同时会有war和war exploded两个选项生产打包用war日常调试用exploded。2.3 包结构与分层controller、service、dao、entity各管一摊这套项目的包结构参照了经典三层架构虽然用Servlet做控制器但分层思想保持一致。我拆开代码时见过不少把JDBC直接写在Servlet里的写法运行没问题可一旦改动学生字段所有相关页面都要跟着改维护成本飙升。这份资料里的包结构是下面这样的包名职责典型的类entity数据实体对应数据库表Student.java、User.javadao数据访问层只负责SQL与结果集映射StudentDao.java、UserDao.javaservice业务层处理登录校验、分页计算等逻辑StudentService.java、UserService.javacontrollerServlet层接收请求、调service、转发JSPLoginServlet.java、StudentServlet.javautil工具类比如DBUtil连接池DBUtil.javafilter过滤器统一编码、登录拦截CharacterEncodingFilter.java分层的好处在于SQL变更只动dao页面字段调整只动entity和JSP登录逻辑改动只动service。调试时按层排查而不是在Servlet里来回翻几百行代码。初次接触这套结构的读者建议先读controller里的Servlet再看service的接口方法最后翻dao的SQL实现顺序恰好是一次请求的数据流方向。3. 核心功能实现登录、分页、增删改查怎么拆成Servlet3.1 登录态控制LoginServlet与Session登录是管理系统的入口也是Servlet里最典型的请求处理场景。用户提交表单后LoginServlet接收用户名和密码调UserService验证成功则写入Session失败则返回错误提示。这里不推荐直接在JSP里写数据库查询不仅是因为耦合更因为一旦验证逻辑复杂化比如验证码、三个月密码过期JSP里改脚本会让人崩溃。WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 设置请求编码避免中文用户名乱码 req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 2. 调用业务层完成密码校验 User user userService.login(username, password); if (user ! null) { // 3. 登录成功把用户信息放进Session HttpSession session req.getSession(); session.setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /student/list); } else { // 4. 登录失败回到登录页并把错误信息带到页面 req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 访问/login时直接转发到登录页面 req.getRequestDispatcher(/login.jsp).forward(req, resp); } }逻辑说明doPost处理表单提交doGet负责页面跳转两者职责清晰是为了让登录页的地址栏看起来更干净。第2行到第6行的UserService实例在Servlet中只有一个整个生命周期复用如果多个Servlet需要共享Service或Dao常见做法是写一个工厂类统一管理避免每个Servlet都new一遍。说明参数sendRedirect是重定向浏览器地址会变成项目路径下的/student/list而forward是服务端跳转浏览器地址不变这里登录成功后用重定向是为了防止刷新页面时重复提交表单。登录失败用forward是为了直接把错误提示塞进requestJSP里可以立刻用el表达式取出来。3.2 学生列表与分页PageBean怎么算页码列表页是信息管理系统的核心页面学生数量一旦超过几十条就必须分页。分页逻辑不复杂但写起来容易乱前端的“上一页、下一页、第X页/共X页”全都依赖后台算出来的页码变量。常见做法是封装一个PageBean对象里面放pageNum、pageSize、totalCount、totalPage、data列表Servlet负责填值JSP负责展示。public class PageBeanT { private int pageNum; // 当前页码默认1 private int pageSize; // 每页条数默认5 private int totalCount; // 总记录数 private int totalPage; // 总页数 private ListT data; // 当前页数据 public PageBean(int pageNum, int pageSize) { this.pageNum pageNum; this.pageSize pageSize; } } // StudentDao中的分页查询 public ListStudent findByPage(int pageNum, int pageSize) { String sql SELECT id, stu_no, name, gender, birthday, phone FROM student LIMIT ?, ?; // 计算起始行下标第1页从0开始第2页从pageSize开始 int offset (pageNum - 1) * pageSize; // 通过PreparedStatement设置两个参数防止SQL注入 // ? 的位置分别绑定offset和pageSize return queryList(sql, Student.class, offset, pageSize); }逻辑说明LIMIT后的第一个问号是offset起始行第二个是pageSize返回条数。pageNum每增加1offset就增加pageSize所以sql里的计算是(pageNum - 1) * pageSize。JSP里渲染分页按钮时用PageBean里的totalPage控制“最后一页”的链接地址否则点击最后一页时页码溢出查询结果为空列表页面就显得很突兀。这里的queryList方法内部用反射把结果集映射到Student对象上你手写时可以直接用BeanUtils或者手动set。3.3 新增和编辑共用同一个doPost新增和编辑是有共性的同样是提交学生的各个字段只是是否有id值。识别这个id值成为关键编辑时id非空新增时id为空。通常做法是在同一个StudentServlet里只写一个doPost根据id是否为空来决定执行insert还是update。Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 从表单中接收id字段编辑时hidden输入框有值新增时无值 String idStr req.getParameter(id); Integer id null; if (idStr ! null !idStr.trim().isEmpty()) { id Integer.valueOf(idStr); } String stuNo req.getParameter(stuNo); String name req.getParameter(name); String gender req.getParameter(gender); String birthday req.getParameter(birthday); String phone req.getParameter(phone); Student student new Student(); student.setId(id); student.setStuNo(stuNo); student.setName(name); // gender的取值可能是“男”“女”或者“M”“F”建议页面传中文存中文 student.setGender(gender); student.setBirthday(birthday); student.setPhone(phone); if (id null) { // 新增调用service里的add方法 studentService.add(student); } else { // 编辑调用service里的update方法 studentService.update(student); } // 完成后重定向回列表页防止刷新页面重复提交 resp.sendRedirect(req.getContextPath() /student/list); }逻辑说明新增和编辑共用doPost的关键是表单里始终保留input typehidden nameid value${student.id}编辑时后台把已有id塞进去新增时id为空。Birthday字段建议直接用字符串接收与前端date控件配合在MySQL里存为DATE类型也能自动转换但要注意时区问题稍后避坑部分专门讲。重定向用的是sendRedirect而不用forward是为了让浏览器发起新的GET请求刷新列表页永远不会触发重复新增。参数说明idStr为空时不能强制转换必须先做空判断stuNo这类字段是否唯一需要在service层额外做校验避免数据库里出现重复学号后抛异常。3.4 删除为什么坚持用POST很多初学者喜欢在列表页写a hrefstudent/delete?id1删除/a直接GET请求完成删除。这样写代码最省事但有两个问题一是浏览器地址栏留下完整的删除链接别人复制粘贴就相当于执行了删除命令二是某些浏览器或爬虫预取链接时也会自动发起GET请求可能删掉不该删的数据。常见做法是包一个表单点击按钮时通过JavaScript提交POST请求或者用一个带隐藏表单域的页面。!-- 列表页删除操作不写a标签直链 -- form action${pageContext.request.contextPath}/student/delete methodpost onsubmitreturn confirm(确定删除这条记录吗); input typehidden nameid value${student.id} button typesubmit classbtn btn-danger btn-xs删除/button /form逻辑说明这段JSP在每条学生记录后面渲染一个表单删除按钮绑定提交动作。参数说明confirm返回false时不提交返回true时才发出POST请求action地址必须带上contextPath否则部署到Tomcat后你很可能遇到404。删除的doPost逻辑里不需要重定向回列表页以外的东西但删除之后一定要清理与该学生相关的业务数据比如选课记录、成绩记录否则数据库外键约束会直接报错。4. 打包与部署传统JSP项目如何打WAR并跑起来4.1 WAR包与Tomcat的关系WAR包本质是个zip压缩文件内部结构严格按照J2EE规范组织WEB-INF下有web.xml、classes目录、lib目录pages目录存放JSP页面。Tomcat启动时会扫描webapps目录下的War包并自动解压同时生成一个以war包文件名命名的应用上下文。所以同一个Tomcat里两个不同项目即使JSP页面同名请求地址因为有项目名前缀也不会冲突。顺带回答“nginx支持jsp吗”这个高频疑问nginx本身不会解析JSP它是静态Web服务器与反向代理JSP的解析工作由Tomcat这类Servlet容器完成。常见做法是nginx监听80端口把以.jsp结尾的请求用proxy_pass转发给Tomcat的8080端口静态资源由nginx直接返回。这套部署方式在真实生产环境中很常见但本项目的war包部署阶段可以先用Tomcat独立跑通之后再考虑加nginx。4.2 IDEA导出WAR包的操作步骤开发模式下IDEA用war exploded直接部署到Tomcat但要把项目交给别人部署时需要打包成标准WAR。操作路径在IDEA右侧的Maven工具窗口如果你的项目是用Maven构建直接执行package生命周期即可如果不是Maven项目用Project Structure里的Artifacts手动构建。# 传统非Maven项目的打包命令入口 # IDEA菜单: Build - Build Artifacts - 选择项目名:war - Build # 输出路径通常在项目的 out/artifacts/项目名_war/ 目录下 # 打包前先执行 Build - Rebuild Project 再做一次干净编译逻辑说明点击Build后IDEA会在指定输出目录生成war文件。这个war文件的名称取决于Artifacts里设置的Name比如叫student-manager生成的就是student-manager.war。如果不确定输出目录直接看IDEA底部Messages里的日志它会打印具体路径。说明参数Maven项目里注意pom.xml的packagingwar/packaging漏掉这个配置打出来的是jar包Tomcat不会自动识别。4.3 手动部署到Tomcat的目录摆放拿到war包后部署就变成简单的文件拷贝。把war文件复制到Tomcat安装目录的webapps文件夹下启动Tomcat容器会自动解压并部署。注意Tomcat启动时会扫描webapps下所有war和目录所以不要同时放两个功能相同但名字不同的war否则会出现访问路径上的混淆。cp student-manager.war /opt/tomcat9/webapps/ cd /opt/tomcat9/bin ./startup.sh tail -f /opt/tomcat9/logs/catalina.out逻辑说明启动脚本的执行依赖JAVA_HOME环境变量如果启动失败先检查这个变量是否配置正确。tail命令查看日志能同时看到解压过程、Servlet初始化日志以及数据库连接情况。浏览器访问http://服务器IP:8080/student-manager/login.jsp这里的上下文路径必须和war文件名完全一致区分大小写。如果端口不是8080修改server.xml里的Connector端口并确认防火墙放行。Tomcat启动成功会打印“Server startup in 1234 ms”类似的日志看到这行再访问页面才靠谱。5. 避坑排查乱码、404、驱动类找不到五个真实翻车现场5.1 全站中文乱码页面全是问号现象登录页打开后页面上的中文文本全变成“???”或乱码插入到数据库里的学生姓名也是乱码。原因JSP页面的pageEncoding、Servlet请求的characterEncoding、MySQL连接串的编码三处没有统一。Tomcat 8.5之后默认URI编码是UTF-8但POST请求的request.setCharacterEncoding如果没有执行到取出来的参数就是ISO-8859-1。解决在util包写一个CharacterEncodingFilter并在web.xml里配置url-pattern为/*。这个Filter必须在所有Servlet之前执行强制设置request和response的编码同时给JSP页面的pageEncoding统一设置为UTF-8数据库连接串加上useUnicodetruecharacterEncodingUTF-8。filter filter-nameencodingFilter/filter-name filter-classutil.CharacterEncodingFilter/filter-class /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping解决记录里最关键的是一旦编码问题出现先看Filter有没有生效再检查JSP顶部的pageEncoding最后看数据库连接串这三处都一致乱码才可能消失。5.2 JSP改了没生效浏览器还是旧页面现象在IDEA里修改了JSP页面的标题重启Tomcat后浏览器刷新显示的还是旧内容。原因IDEA的war exploded模式部署时JSP文件被拷贝到target目录或out目录Tomcat运行的是那份拷贝而不是你修改的原文件。此外浏览器会缓存JSP响应头旧内容可能来自本地缓存。解决先强制刷新浏览器CtrlF5清缓存。如果仍然旧内容在IDEA里执行Build - Rebuild Project删除target和out目录后重新构建。有时还需要检查Tomcat的webapps下是否残留了旧版本解压目录把对应文件夹删掉再重新部署。5.3 访问路径返回404项目名对不上现象部署完成后访问http://localhost:8080/student/list时出现404或者跳转登录页时地址不对。原因web.xml里的Servlet映射路径写法有误或者JSP里直接写了绝对路径而没有加contextPath。response.sendRedirect(/login.jsp)这种写法会被当成服务器根路径下访问而不是当前项目下。解决统一的路径写法是在所有跳转和表单action前加${pageContext.request.contextPath}。Servlet里用req.getContextPath() /login.jsp拼路径重定向也同理。检查清单web.xml的url-pattern是否有/student/list与Servlet注解匹配form表单的action是否带项目名。5.4 MySQL 8驱动类ClassNotFoundException现象Tomcat启动时控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver页面访问数据库相关Servlet直接500。原因MySQL 8之后的驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver并且驱动jar包需要手动放进WEB-INF/lib目录。Maven项目忘记添加mysql-connector-j依赖也会出现同样问题。解决确认pom.xml或者手动添加MySQL Connector/J 8.0以上版本jarJDBC URL里必须带serverTimezone参数比如jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/Shanghai。驱动类统一写com.mysql.cj.jdbc.Driver。如果Tomcat的lib目录和项目lib目录同时存在老版本驱动还会出现两个驱动冲突的异常这类情况直接删掉Tomcat/lib下的mysql驱动保证只留一份。5.5 Timestamp字段在页面上显示IllegalArgumentException现象Student实体里有一个java.sql.Timestamp类型的字段直接在JSP里输出时抛异常。原因Timestamp类的toString方法没有实现可读的时间格式在页面渲染时无法直接输出自定义日期格式getParameter拿到birthday字符串后存进Date类型时也容易转换失败。解决在entity里把时间字段定义成String类型接收时直接存字符串展示时不用转换。如果需要排序或范围查询再由SQL层用STR_TO_DATE或DATE_FORMAT函数处理。这种取舍对管理类系统完全够用。6. 进阶改造给项目加一个统一编码Filter和请求日志Filter6.1 CharacterEncodingFilter一次配置全站编码编码Filter是整个系统最基础的组件放在任何Servlet之前。它的实现很简单就是强制指定字符集并设置响应类型。这段代码在所有Filter里优先级最高因为request参数一旦被读取过再调用setCharacterEncoding就会失效所以必须放在最前面。WebFilter(/*) public class CharacterEncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html; charsetUTF-8); chain.doFilter(request, response); } }逻辑说明字符集设置发生在chain.doFilter之前确保后续所有Servlet读取param时拿到的已经是UTF-8解码后的字符串。注意顺序如果这里放到chain之后Servlet已经读完参数Filter再设置就毫无意义。说明参数setContentType在响应当前页面是HTML时使用如果是JSON返回则改成application/json。6.2 日志Filter记录每个请求的耗时与参数学生信息管理系统上线后有人在页面操作很慢你却不知道慢在哪个请求上。加一个日志Filter每次请求进来记录URL、耗时、关键参数问题一眼就能锁定。这个Filter不需要侵入任何Servlet纯横切逻辑适合学习Filter机制。WebFilter(/*) public class LogFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; long start System.currentTimeMillis(); // 按顺序记录请求路径、查询参数或表单参数 String paramDesc req.getParameter(id) ! null ? id req.getParameter(id) : 无参数; chain.doFilter(request, response); long end System.currentTimeMillis(); System.out.printf([req] %s %s 耗时%dms%n, req.getRequestURI(), paramDesc, end - start); } }逻辑说明System.currentTimeMillis抓的是开始毫秒数与结束毫秒数差值就是处理耗时。写在chain.doFilter前后的代码天然区分请求进入和响应返回两个阶段这正是Filter机制的核心价值。把耗时与参数一起打出来后面维护时一看到某个学生资料的请求耗时超过200毫秒直接排查对应SQL或索引。这里只是体验版生产环境通常是配合Log4j输出到独立文件并按天切割。6.3 验证效果用浏览器和命令行观察日志输出项目在IDEA里启动Tomcat后打开浏览器访问学生列表页操作几次翻页和编辑。此时IDEA的Console窗口会实时打印LogFilter输出的信息例如[req] /student/list 无参数 耗时23ms。再对照CharacterEncodingFilter的效果随意提交一个包含中文的姓名保存后数据库里显示正常。双Filter的部署顺序需要注意web.xml里配置的Filter顺序就是执行顺序注解方式的顺序类不能保证如果用的是纯注解需要确认两个Filter都挂着不同的拦截范围避免互相覆盖。从那以后我每次接手老项目第一件事就是先看Filter链怎么配的、Servlet映射有没有带contextPath、数据库连接串的时区和编码是什么绕开这三座大山后面再跑业务逻辑就顺得多。这套学生信息管理系统虽然技术栈朴素但作为理解Java Web全链路的起点比直接套用Spring Boot更值得过一遍手。希望帮到你。本文还有配套的精品资源点击获取