ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Web入门必看:JSP+Servlet学生管理系统全链路解析与乱码排查

Java Web入门必看:JSP+Servlet学生管理系统全链路解析与乱码排查 简介这是一套基于 Java Web 的简单学生信息管理系统采用 JSP Servlet 经典架构搭配 layui 与 jQuery 构建前端界面并以 MySQL 作为数据存储。项目面向正在完成课程设计或初学 Java Web 的在校学生实现了学生自主注册登记、教师管理学生等核心功能可帮助理解“JSP 页面—Servlet 控制—数据库操作”的完整开发链路。压缩包内共 2 个文件主文件为 zip 格式的完整工程源码附带一个 htm 格式的 readme 说明页整体大小仅 2.13MB结构轻量、便于下载后快速搭建与调试源码工程中已在 lib 文件夹内置所需 jar 包并额外提供数据库脚本文件的网盘链接。资源运行环境为 JDK7 Tomcat8 MySQL IntelliJ IDEA已有 6431 人学习/下载适合作为 Java Web 课程设计的参考模板也可用于练习 JSP/Servlet 与 MySQL 的整合开发以及 layui 表格、弹窗等前端组件的实际应用。 上周带一个学弟过他的Java Web课程设计题目是学生信息管理系统技术栈指定jspservlet。这个项目听起来再普通不过他却硬生生在Tomcat部署和中文乱码上耗了快一个周末最后我陪他把代码一行行捋完才发现问题全出在那些大家都这么写但没人讲为什么的地方。这篇文章就借这个学生信息管理系统把jspservlet从浏览器请求到数据库、再到页面渲染的整条链路拆开讲清楚。你会看到完整的项目结构、核心代码、数据库表设计以及我在排查过程中踩过的那些典型坑。适合刚学完Java SE准备做第一个完整Web项目的初学者也适合正在交JSPServlet课程设计作业、希望能真正讲明白自己代码的同学。1. 为什么还要写jspservlet这门老技术其实是理解Spring Boot的钥匙1.1 Servlet和JSP到底各自负责什么很多同学一上来就质疑现在外面公司不都用Spring Boot了吗为什么课程设计还用JSPServlet这种老古董这里要说个反直觉的事实Servlet和JSP是Java Web最底层的两个组件Spring MVC也好Spring Boot也好本质都是在它们外面包了一层又一层的壳。你学这个项目的时候能看到一个HTTP请求怎么被接收、参数怎么被取出、数据库连接怎么被管理、页面怎么被响应这些底层感知在框架里反而看不清楚。等你后面用Spring Boot遇到请求路径写错、参数接收不到、响应乱码之类的问题脑子里会自动浮现这套Servlet的模型排查起来快得多。这套组合里Servlet负责写Java逻辑处理请求和响应JSP负责生成HTML页面。早期Java Web是直接在JSP里写Java代码的后来发现这样维护起来太痛苦于是把逻辑抽到Servlet里JSP只负责展示。这其实就是MVC思想的雏形JSP是ViewServlet是ControllerDAO和JavaBean是Model。你把这个项目吃透再去看Spring MVC的DispatcherServlet、Controller、ModelAndView会发现一切都很眼熟。1.2 这个项目的学习边界学生信息管理系统本质上就是一个增删改查系统核心是四个字列表查询、新增、编辑、删除。这四个操作覆盖了Web开发最常用的HTTP GET/POST、JDBC连接、参数传递、请求转发和重定向不复杂但链路完整。建议你不要把这个项目当成作业而是当成一张地图。这张地图里每个点都是真实的Web开发会遇到的问题数据库连接怎么管理、SQL怎么防注入、中文乱码在哪一层处理、刷新页面为什么会导致重复提交。弄懂这些问题比单纯用Spring Boot从一个模板项目里把CRUD跑通有意义得多。2. 项目骨架、依赖和数据库动手前先把边界定死2.1 目录结构与分层约定我的建议是直接按Maven的目录结构来不用纠结IDE里到底是叫WebContent还是webapp。包结构分成四层就够了src/main/java com.myschool.entity.Student com.myschool.dao.StudentDao com.myschool.util.DBUtil com.myschool.servlet.StudentListServlet com.myschool.servlet.StudentAddServlet com.myschool.servlet.StudentEditServlet com.myschool.servlet.StudentDeleteServlet src/main/webapp list.jsp add.jsp edit.jsp WEB-INF/web.xml注意这个项目我刻意没有写Service层。逻辑实在太薄Servlet直接调DAO完全hold得住。如果你为了看起来规范硬加一层StudentService你会发现它只在Servlet和DAO之间当了一回传声筒。判断标准很简单当业务逻辑需要事务、需要组合多个DAO操作时再引入Service层不迟。2.2 数据库表和初始化数据表字段不要贪多够用且真实就行。我平时带人做课设学生表就六个字段主键、学号、姓名、性别、年龄、专业。CREATE DATABASE IF NOT EXISTS school_db DEFAULT CHARACTER SET utf8mb4; USE school_db; CREATE TABLE t_student ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, sno VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender VARCHAR(10) DEFAULT 男 COMMENT 性别, age INT DEFAULT 18 COMMENT 年龄, major VARCHAR(50) DEFAULT NULL COMMENT 专业 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; INSERT INTO t_student(sno, name, gender, age, major) VALUES (2025001, 张三, 男, 20, 计算机科学与技术), (2025002, 李四, 女, 21, 软件工程);性别这里直接存字符串男/女对课设完全没问题。如果想体现设计能力可以存tinyint 0/1再在页面映射但那属于锦上添花不改变系统本质。字符集方面表一定要用utf8mb4而不是utf8不然遇到生僻字或者emoji当然我写博文不用emoji但用户数据里可能有人输入会报错或者存不进去。2.3 Java Web的技术选型细节环境方面我用的是JDK 8 Tomcat 8.5 MySQL 8.0。这里有个特别容易踩的版本坑如果你用的是Tomcat 10以上Servlet的包名从javax.变成了jakarta.网上很多教程代码直接粘过来是编译不过的。建议你图省心直接用Tomcat 8.5或9老老实实跟着javax写。MySQL驱动jar包如果是Maven项目就在pom.xml里加依赖如果不是Maven手动把mysql-connector-java这个jar扔到WEB-INF/lib目录下。这一步漏掉后面必现ClassNotFoundException。至于还有同学问clickhouse之类的驱动怎么在Web项目里连接原理完全一样把对应驱动jar放进lib目录Class.forName加载驱动类后面就是JDBC连接的套路了。3. 列表查询全链路一个请求从浏览器到数据库再回到页面的完整旅程3.1 DBUtil最容易被写糊的一层我从这个项目的DAO说起因为它是所有DAO操作的地基。看代码之前先要理解JDBC连接对象的获取逻辑。package com.myschool.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/school_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }这段代码里有几个细节要解释。首先连接URL里的characterEncodingutf8是后面解决中文乱码的关键之一少了它即使页面和Servlet都设置对了写入数据库的中文也可能变成问号。其次MySQL 8.x的驱动类是com.mysql.cj.jdbc.Driver而MySQL 5.x是com.mysql.jdbc.Driver版本不一样类名不一样。JDBC 4.0之后其实可以省略Class.forName这一步驱动包会有SPI自动加载但写上更稳妥也不容易让初学者困惑。3.2 StudentDao和PreparedStatementDAO层我习惯每个方法对应一种数据操作。先看查询列表和按ID查询package com.myschool.dao; import com.myschool.entity.Student; import com.myschool.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class StudentDao { public ListStudent list() { String sql SELECT * FROM t_student ORDER BY id DESC; ListStudent list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setSno(rs.getString(sno)); stu.setName(rs.getString(name)); stu.setGender(rs.getString(gender)); stu.setAge(rs.getInt(age)); stu.setMajor(rs.getString(major)); list.add(stu); } } catch (SQLException e) { e.printStackTrace(); } return list; } }我一直强调用PreparedStatement而不是直接拼接字符串这不仅是为了防止SQL注入也是为代码可读性。如果在真实企业里SQL拼接引号少打一个整个功能就挂了排查起来心态会崩。ResultSet转Java对象这一步没有花活就是把列名对应到实体类的setter。try-with-resources语法是我推荐的方式Connection、PreparedStatement、ResultSet都会自动关闭不会出现连接泄漏。很多老教程亲手写conn.close()一旦中间抛异常或漏写连接池很快就被耗光。你作为初学者直接从好习惯开始比以后纠正容易得多。3.3 Servlet接请求和转发Servlet这里我用的是Servlet 3.0的WebServlet注解方式不用去web.xml里配置映射。有同学想了解web.xml方式怎么配置文章后面会专门讲和注解的区别。package com.myschool.servlet; import com.myschool.dao.StudentDao; import com.myschool.entity.Student; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; WebServlet(/student/list) public class StudentListServlet extends HttpServlet { private StudentDao studentDao new StudentDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); ListStudent list studentDao.list(); req.setAttribute(studentList, list); req.getRequestDispatcher(/list.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }重点看最后两行forward转发和sendRedirect重定向的区别是Java Web必考也是必懵的点。这里用forward是因为Servlet和JSP属于同一个请求生命周期Servlet查完数据把List塞进request域然后告诉Tomcat接下来让list.jsp来渲染整个过程浏览器地址栏不会变request里的studentList在JSP里能直接取到。如果这里误用sendRedirect浏览器会重新发起一个新请求request域里的数据直接丢失JSP里取出来就是null。这个区别值得你在本子上写一遍转发是同一次请求内的内部调岗重定向是你找隔壁部门。3.4 list.jsp把数据渲染成表格JSP页面里取Servlet放进去的数据推荐用EL表达式和JSTL标签。这里先用一个简洁版本% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生信息列表/title /head body h2学生信息列表/h2 table border1 cellpadding8 cellspacing0 tr thID/th th学号/th th姓名/th th性别/th th年龄/th th专业/th th操作/th /tr c:forEach items${studentList} varstu tr td${stu.id}/td td${stu.sno}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.major}/td td a hrefstudent/edit?id${stu.id}编辑/a a hrefstudent/delete?id${stu.id} onclickreturn confirm(确定要删除该学生吗)删除/a /td /tr /c:forEach /table a hrefstudent/add新增学生/a /body /html注意两点。第一使用JSTL需要引入jstl的jar包Tomcat不会自动带这个依赖不加jar包页面会报Unable to find taglib错误。第二很多人第一次写容易把href写成/list.jsp跳过了Servlet直接访问JSP。这样虽然能看到页面但studentList属性是空的一个学生都显示不出来。前端永远只和Servlet的URL打交道不要直接去访问JSP文件这样才能保证数据是从Servlet完整带过来的。4. 新增、编辑、删除表单请求怎么写才不容易乱4.1 新增doGet显示表单doPost处理提交到了表单操作一个新规则出现了同一个URL可以用doGet和doPost分别应对不同场景。新增学生页面用/student/add这个地址当浏览器以GET方式访问它时我们返回add.jsp表单当用户填写完点击提交表单以POST方式发回来时Servlet负责收参数、调DAO、再重定向回列表。WebServlet(/student/add) public class StudentAddServlet extends HttpServlet { private StudentDao studentDao new StudentDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher(/add.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); Student stu new Student(); stu.setSno(req.getParameter(sno)); stu.setName(req.getParameter(name)); stu.setGender(req.getParameter(gender)); stu.setAge(Integer.parseInt(req.getParameter(age))); stu.setMajor(req.getParameter(major)); studentDao.insert(stu); resp.sendRedirect(student/list); } }add.jsp的表单里action写student/addmethod写post。这里有个小坑action路径是相对当前URL解析的。如果列表页在/student/list表单在/student/add渲染页面里的相对路径可能会解析错。我建议一律写项目上下文路径开头的绝对路径比如${pageContext.request.contextPath}/student/add这样无论从哪个页面发起都不会迷路。虽然课设里偷懒也能跑通但这个好习惯能让你以后少踩一堆路径的坑。4.2 编辑id怎么在请求之间传递编辑功能比新增多了一步回显。列表页点编辑链接URL是/student/edit?id3注意这里的id是查询参数。编辑Servlet的doGet要做两件事拿id、调DAO查单个学生、把student对象放进request、转发到edit.jsp。然后edit.jsp把Student的属性填进input标签的value属性里用户看到的就是填好信息的表单。WebServlet(/student/edit) public class StudentEditServlet extends HttpServlet { private StudentDao studentDao new StudentDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int id Integer.parseInt(req.getParameter(id)); Student stu studentDao.findById(id); req.setAttribute(stu, stu); req.getRequestDispatcher(/edit.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); Student stu new Student(); stu.setId(Integer.parseInt(req.getParameter(id))); stu.setSno(req.getParameter(sno)); stu.setName(req.getParameter(name)); stu.setGender(req.getParameter(gender)); stu.setAge(Integer.parseInt(req.getParameter(age))); stu.setMajor(req.getParameter(major)); studentDao.update(stu); resp.sendRedirect(student/list); } }很多初学者做编辑时会让编辑Servlet的doGet去调用新增页面的add.jsp想着共用一个表单页面。这样不是不行但你必须处理新增时所有input为空、编辑时input有值的差异。最省心是单独写一个edit.jsp结构几乎和add.jsp一样只是input的value里用${stu.xxx}做回显form的action指向同一个/student/edit。表单里还得埋一个隐藏域否则提交时Servlet不知道在更新哪条记录。还有个隐藏的坑也是新手必踩Integer.parseInt对空字符串和非数字字符串会抛NumberFormatException。如果页面上年龄输入框留空提交Servlet直接500。要么前端加required校验要么Servlet里做非空判断建议两者都做。4.3 删除为什么我建议不要用纯GET链接删列表页上的删除我一开始写的也是a hrefstudent/delete?id1这种GET请求方式方便配合onclick confirm弹窗课设演示完全够用。但从真实开发的角度说删除属于修改操作应该用POST。GET请求会被浏览器预加载、会被搜索引擎爬虫访问、也容易被恶意构造链接诱导点击这些都可能造成意外的数据删除。最稳妥的课设写法是做一个隐藏的表单删除按钮触发这个表单的POST提交form actionstudent/delete methodpost styledisplay:inline; onsubmitreturn confirm(确定要删除该学生吗); input typehidden nameid value${stu.id} button typesubmit删除/button /form对应Servlet里就只实现doPostdoGet直接返回405或者也转发过去。这样做还有个好处地址栏里看不到被删的id刷新页面不会触发二次删除。4.4 转发和重定向的选择防止刷新重复提交在新增、编辑、删除的doPost末尾我都用resp.sendRedirect(student/list)而不是forward到list.jsp。原因是浏览器如果在表单提交后收到的是一个转发响应地址栏还停留在/student/add这时候用户按F5刷新浏览器会再次向/student/add发送POST请求于是数据库多出一条重复记录。sendRedirect返回的是302响应浏览器自动发起新的GET请求跳转到/student/list地址栏变成列表地址再按F5也只是重新查询列表不会重复提交。这一行代码成了区分能跑和能用的关键。5. 这五个高频报错和乱码我建议你提前收藏5.1 中文乱码三个位置必须同时管中文乱码是JSPServlet项目里出现频率最高的噩梦而且它不是单一原因导致的。我见过一个同学改了一下午最后发现三个原因叠加。完整的检查顺序应该是第一层页面本身。JSP文件顶部必须有% page contentTypetext/html;charsetUTF-8 languagejava %HTML的head里也建议写上charsetUTF-8。第二层请求参数。POST请求的参数编码在Servlet里通过req.setCharacterEncoding(UTF-8)设置注意这行必须在读取第一个参数之前执行。GET请求的参数编码是在Tomcat的server.xml里配置URIEncodingUTF-8。第三层数据库。连接URL里指定characterEncodingutf8建表时使用utf8mb4。为什么三处都要管因为数据从浏览器到Servlet到数据库每一层都有自己的编码规则某一层还是默认的ISO-8859-1中文字符就到那里断送了。排查的时候也别瞎试在新增加一条中文数据后分别看页面显示、Servlet里的值、数据库里的值逐层缩小范围。5.2 404和驱动类找不到的排查顺序404是最让人摸不着头脑的问题。我建议按这个顺序排查项目到底部署到Tomcat没访问的URL和Servlet映射是否完全一致页面里的href是否拼错。很多同学把WebServlet(/student/list)写成了/student/lsit或者列表页链接写成studentListServlet这类错误一行行对着检查比重新部署快得多。驱动类找不到通常是你根本没把jar包放进WEB-INF/lib目录。注意使用Maven时依赖会打到lib里手动建的项目如果漏了这一步运行时会报ClassNotFoundException。这个问题和我在前面提到的clickhouse驱动连接思路一样不管什么数据库驱动只要jar包不在类路径上你Class.forName写得再对也没用。5.3 jsp改了不生效多半不是代码的问题这个话题几乎每个初学JSP的人都搜过。改了JSP刷新页面还是老样子第一反应是自己的改动有问题回头检查代码半天才发现改了也没用。实际原因基本是两个一是Tomcat没有热部署成功你改的是工作区或target目录里的文件但Tomcat已经加载的Class没有更新二是浏览器缓存了旧页面。我的习惯是项目直接以war exploded方式部署到TomcatIDEA里改完代码不要只点浏览器刷新要让Tomcat把改动重新加载完再按CtrlF5强刷浏览器。新建或者修改了Servlet之后千万不要依赖自动热部署手动重启Tomcat更稳。5.4 web.xml和注解别重复配置现在网上很多老教程还是用web.xml方式配置Servlet映射很多同学就照着写一份xml然后又看到新教程教用WebServlet注解也在代码上加了注解。结果启动Tomcat直接报错提示Servlet映射冲突。其实这两种方式是等价的注解写在类上就等价于在web.xml里配置了一个servlet-mapping。你只需要选一种。对课设来说我倾向用WebServlet少写一坨XML配置代码里还能直接看到路径。如果课程要求必须展示web.xml配置的完整示例那你就把注解去掉class文件里不要出现WebServlet不然重复映射是跑不掉的。6. 从能跑到拿得出手课程设计进阶的几个方向6.1 分页和模糊查询一个小系统跑通之后下一步我建议加分页。学生表数据一旦多起来一页显示几百上千行体验会非常差老师也会觉得你欠缺考虑。分页核心SQL就一句话SELECT * FROM t_student ORDER BY id DESC LIMIT ?, ?;Servlet里接收pageNum和pageSize参数计算出offset (pageNum - 1) * pageSize查总数算出总页数然后把当前页数据和分页信息一起给JSP。这些逻辑不难但会逼着你理解为什么很多框架里都有PageHelper或者Page对象——它们做的就是这个事情。模糊查询也是一样的思路核心SQL从固定条件变成SELECT * FROM t_student WHERE name LIKE ?;注意这里要手动拼上%通配符用PreparedStatement传参时写成% keyword %而不是把拼接后的SQL直接塞进去。这个区别虽然代码上只差一点但理解后你就明白防SQL注入是怎么回事了。6.2 把JSP里的Java代码换成EL和JSTL我见过很多课设代码里JSP页面大量使用% %脚本片段把Java逻辑直接写在HTML里。如果你做课设只是应付答辩这样确实最快。但如果你想拿这个项目去面试或者写进简历建议花半天时间把页面里的脚本片段全部换成EL表达式和JSTL标签。EL表达式和JSTL的好处是职责清晰JSP只负责取值和循环展示不写任何与业务相关的Java代码。最重要的是这能让你更容易理解现代前后端分离趋势下模板引擎的概念。后面你如果接触Thymeleaf或Vue的模板语法会发现思路完全是通的。另外之前热搜里有jsp实现数据导出为excel的需求这个在小系统里也不难加在Servlet里从数据库查出List用POI或者EasyExcel把数据一行行写进Workbook再通过response.getOutputStream()直接输出给浏览器下载。核心不是写代码而是理解HTTP响应的Content-Type和Content-Disposition这两个响应头怎么设置以及为什么下载文件的文件名要做URLEncoder编码。6.3 把透明JDBC迁移到框架这些过渡是顺水推舟的写完这个纯Servlet项目你再看现在主流的Java Web框架迁移路径会特别清晰。DAO层的JDBC代码重复性高数据库操作一多非常臃肿这就是MyBatis和JdbcTemplate存在的意义——它们把ResultSet转对象这件重复劳动封装了。Servlet里手动封装参数的逻辑被Spring MVC的RequestParam和对象绑定替代。DBUtil里手动管理连接的过程被连接池和框架的数据源管理替代。我个人的体会是这个项目做完后你真正带走的有两样东西第一你亲眼见过一个HTTP请求从浏览器出发到Servlet、DAO、数据库再一路返回渲染成HTML页面的完整生命周期第二你踩过乱码、404、空指针、重复提交这些坑知道它们发生的原因。把这两样东西讲清楚比用什么新技术栈都值钱。如果时间允许最后建议你把写好的代码删掉不看任何参考自己重新从空目录写一遍。照抄一遍只能让你看懂重写一遍才能让你真正记住。等你有一天不需要查资料就能把DBUtil、StudentDao、Servlet、JSP这四层一口气写出来就可以放心对别人说Java Web的底子我算是打牢了。本文还有配套的精品资源点击获取
返回列表