ARTICLE DETAIL

资讯详情

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

Java Web综合实战:Servlet+JSP+JDBC实现员工管理系统

Java Web综合实战:Servlet+JSP+JDBC实现员工管理系统 上机练习第38天从“能跑通”到“会设计”一次综合性实战带来的质变算下来这是我在训练营里上机练习的第38天。从第一天敲下“Hello World”时的生涩到现在能独立完成一个带数据库、前端页面和接口联调的小系统这期间的进步说实话连我自己都没想到。第38天是个挺微妙的节点——基础语法早就不是障碍了真正折磨人的开始变成“怎么把东西拼在一起还能稳定运行”。今天这节上机课老师布置的任务是完成一个小型管理系统的核心模块员工信息管理。具体要求是前端有列表页和新增表单后端能接收请求、处理数据、返回结果数据库里得有一张像样的员工表还得把删除和条件搜索做出来。听起来好像每一样都练过但真正串起来的时候就会发现知识散着学都能懂一组合就到处冒问题。这篇文章就把第38天这一整次实战练习的完整过程拆开聊聊包括我的设计思路、核心代码实现、踩过的坑和排查方法希望能给正在类似阶段的朋友一些参考。1. 项目整体设计与思路拆解1.1 为什么第38天要做“综合练习”而不是继续学新知识点很多自学或者培训班出来的朋友都有这种感受语法看懂了例题敲熟了但拿到一个“做个管理系统”这种需求还是不知道从哪里下手。这其实就是因为前面的练习都是“局部肌肉训练”而实际开发需要的是“全身协调发力”。第38天这个上机练习的核心就是把已经学过的 Java基础、JDBC操作MySQL、Servlet处理请求、JSP渲染页面这四个模块放到同一个项目里打通。老师特意强调一个要求“不要用任何框架就纯Servlet JSP JDBC。”刚开始我不理解觉得都2025年了谁还用这老一套 But 这一套做完之后我彻底明白了用框架就像开自动挡你踩油门就走但发动机怎么换挡你完全没概念纯手工写一遍相当于亲手拆装了一遍变速箱之后切换到 Spring Boot 或者 MyBatis 的时候你会知道它们在底层到底帮你干了什么活。整体项目结构分成三层浏览器端的 JSP 页面负责展示和收集用户输入Servlet 充当中间的“调度员”负责接请求、调方法、转弯页面最底下一层是 JavaBean JDBC 工具类负责跟 MySQL 打交道。这个分层思路本身就是以后所有大型项目的雏形MVC 的概念在这一天算真正落地了。1.2 员工信息管理系统的需求拆解当天收到任务以后我没有急着开写而是先把需求拆成了几个小功能点列了一张简单的清单功能模块具体描述涉及技术点员工列表展示从数据库读取全部员工分页或不分页展示JDBC 查询、ResultSet 遍历新增员工表单收集信息插入数据库再刷新列表POST 请求、请求参数编码处理、INSERT 语句删除员工按ID删除指定记录前端要弹确认框GET/POST 传参、DELETE 语句条件搜索按姓名或部门模糊查询LIKE 语句、参数拼接与防注入数据统计显示员工总数、部门分布等简单统计COUNT、GROUP BY在实际动手做之前先拆解需求这个习惯强烈建议每个人都养成。很多同学一上手就写代码写着写着发现漏了几个字段又回去改数据库改页面来回折腾特别浪费时间。先花10分钟把需求理清后面的效率能提升一倍以上。1.3 技术方案的选型与理由这次练习的技术栈是老师定死的但选型背后的逻辑值得说清楚。为什么数据库用 MySQL因为它是目前中小型项目和小公司最普及的关系型数据库网上资料多出了问题好排查而且后来的生产环境大概率也会碰到。为什么不用连接池而用最原始的 JDBC因为连接池比如 DBCP、C3P0、Druid本质上是对连接的封装管理你如果没写过原生的DriverManager.getConnection()就很难真正理解连接池到底省了什么资源。同样地为什么 Servlet 不用注解配置而用 web.xml因为注解方式虽然省事但 XML 配置能让你看清“URL 到 Servlet 的映射是怎么发生的”。理解了这个映射过程后面用 Spring MVC 时你就知道RequestMapping背后其实还是那套 servlet-mapping 在发挥作用。技术选型的核心原则是在“学东西”的阶段尽量往下挖一层看看底层到底是什么样子。在工作阶段则反过来要尽量用成熟的框架和工具减少重复劳动。2. 核心细节解析与实操要点2.1 数据库设计与表结构创建我先把员工表的字段定下来。设计表结构的时候要考虑这些字段后面会不会被搜索条件用上要不要建索引。CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT 员工工号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男, department VARCHAR(50) COMMENT 部门, position_title VARCHAR(50) COMMENT 职位, hire_date DATE COMMENT 入职日期, salary DECIMAL(10, 2) COMMENT 薪资 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个细节要注意工号要加 UNIQUE 约束避免重复入职的数据脏掉薪资用DECIMAL而不是FLOAT/DOUBLE因为浮点数存钱会有精度误差这个在财务场景是红线问题表字符集用utf8mb4而不是utf8否则插入 emoji 或者生僻字会报错。有的同学在这个环节会图省事直接用 Navicat 可视化建表我建议至少自己用命令行敲一遍建表语句后面排查字符集和字段类型问题时会快很多。2.2 JDBC 工具类的编写逻辑每次操作数据库都要重复“加载驱动、获取连接、创建语句”所以第一步是把这些公共代码抽到一个工具类里。这算是第一次体会到“重构”的价值。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/employee_db?useSSLfalsecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) try { rs.close(); } catch (SQLException ignored) {} if (stmt ! null) try { stmt.close(); } catch (SQLException ignored) {} if (conn ! null) try { conn.close(); } catch (SQLException ignored) {} } }连接串里characterEncodingutf8mb4和serverTimezoneAsia/Shanghai这两个参数我一开始没加结果插入中文乱码、时间字段差8小时查了半小时才发现是这里的问题。顺手提醒一下关资源的顺序必须是从内往外先 ResultSet 再 Statement 最后 Connection漏了关连接会导致数据库连接耗尽后面请求全部卡死。2.3 Servlet 层的职责划分Servlet 层我写了两个类EmployeeListServlet和EmployeeAddServlet还加了个删除的EmployeeDeleteServlet。核心思路是每个 Servlet 只干一件事尽量不要一个 Servlet 通过 switch-case 处理所有请求否则代码会变成一团乱麻。列表查询的 Servlet 核心逻辑如下WebServlet(/employee/list) public class EmployeeListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String keyword request.getParameter(keyword); EmployeeDao dao new EmployeeDao(); ListEmployee employees dao.findByCondition(keyword); request.setAttribute(empList, employees); request.setAttribute(keyword, keyword); request.getRequestDispatcher(/list.jsp).forward(request, response); } }这里需要理解 forward 和 redirect 的区别forward是服务端内部跳转地址栏不变request 里的数据能带到页面redirect是浏览器重新发一次请求地址栏会变数据得靠 session 或 URL 重新传。列表刷新这种情况用 forward 更合理因为用户不需要看到 URL 变化。新增完数据以后反而要用 redirect不然用户按 F5 刷新会把同样的数据再插入一遍这是经典的“重复提交”问题。3. 实操过程与核心环节实现3.1 完整项目结构一览动手之前先把目录建好。我的项目结构如下这个结构也是后面学 Maven 工程的基础src/ ├── main/ │ ├── java/ │ │ ├── com/campus/entity/Employee.java │ │ ├── com/campus/dao/EmployeeDao.java │ │ ├── com/campus/util/DBUtil.java │ │ └── com/campus/servlet/ │ │ ├── EmployeeListServlet.java │ │ ├── EmployeeAddServlet.java │ │ └── EmployeeDeleteServlet.java │ ├── resources/ (空目录暂未使用) │ └── webapp/ │ ├── list.jsp │ ├── add.jsp │ └── WEB-INF/web.xmlJavaBean 实体类的字段要和数据库字段一一对应每个字段提供 getter/setter。这里有个小技巧IDEIDEA 或 Eclipse里可以直接右键生成 getter 和 setter不用手写但如果你用的是文本编辑器就得老老实实写。这个阶段不建议偷懒源码级敲一遍 getter/setter 能帮你固化“POJO 就是普通 Java 对象”这个认知。3.2 DAO 层的写法和 SQL 拼接防注入DAO 层是直接操作数据库的一层。我今天写的最关键方法是findByCondition它既要支持无条件查询全部也要支持按关键词模糊搜索。public ListEmployee findByCondition(String keyword) { ListEmployee list new ArrayList(); String sql SELECT * FROM employee WHERE name LIKE ? OR department LIKE ?; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ps.setString(2, % keyword %); rs ps.executeQuery(); while (rs.next()) { Employee e new Employee(); e.setId(rs.getInt(id)); // 其他字段set... list.add(e); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } return list; }有两个点需要特别说明。第一用PreparedStatement而不是Statement。虽然拼字符串 SQL 也能跑通但PreparedStatement有预编译机制能防止 SQL 注入攻击。你可以把 SQL 注入理解为用户在搜索框里输入一段恶意代码如果你直接拼进 SQL 字符串数据库就会把那段代码当成 SQL 指令执行。用?占位符预处理之后输入内容只会被当作纯数据这个安全红线从一开始就要拉紧。第二LIKE查询的%位置有讲究。%keyword%是包含查询keyword%是前缀查询。用户搜索“张”希望把“张三”“小张”都查出来就得前后都加%。另外如果搜索框传过来的 keyword 是空字符串还是应该走全列表查询不然一条都查不出来这个逻辑我在代码里做了判空处理。3.3 JSP 页面的渲染与表单交互JSP 页面负责展示数据和收集用户输入。列表页的核心是 JSTL 标签遍历 List加上一个搜索表单% taglib urihttp://java.sun.com/jsp/jstl/core prefixc % div classsearch-bar form action/employee/list methodget input typetext namekeyword value${keyword} placeholder输入姓名或部门搜索 button typesubmit搜索/button /form /div table thead tr thID/thth工号/thth姓名/th th部门/thth职位/thth入职日期/thth薪资/thth操作/th /tr /thead tbody c:forEach items${empList} varemp tr td${emp.id}/td td${emp.empNo}/td td${emp.name}/td td${emp.department}/td td${emp.positionTitle}/td td${emp.hireDate}/td td${emp.salary}/td td a href/employee/delete?id${emp.id} onclickreturn confirm(确定要删除该员工吗)删除/a /td /tr /c:forEach /tbody /table这里有个容易翻车的细节如果项目没有引入 JSTL 依赖c:forEach标签会解析失败整个页面直接报 500 错误。需要确认 pom.xml 或者 WEB-INF/lib 下有jstl.jar和standard.jar如果是纯 Tomcat 部署的话。当然也可以不用 JSTL改用脚本片段% for(...)但那样页面里全是 Java 代码维护性极差不推荐。删除确认框onclickreturn confirm(...)是前端常用的小防护。加上这个之后用户点删除会先弹浏览器确认框点“确定”才继续跳转点“取消”则不发生任何请求。这个体验虽然简单但能避免很多手滑误删的情况。3.4 新增员工的完整链路测试新增员工涉及 POST 请求和表单数据的处理流程。JSP 那边是一个普通的 form 表单method 必须写成postaction 指向/employee/add。Servlet 端要做三件事第一解决编码问题。Tomcat 8.5 之后 POST 请求的默认编码是 UTF-8但如果你用的是老版本 Tomcat8.0 之前request 的默认编码是 ISO-8859-1中文必乱码所以代码里要加一行request.setCharacterEncoding(utf-8);第二解析参数并封装成 Employee 对象。日期类型需要特殊处理SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); Date hireDate sdf.parse(request.getParameter(hireDate)); emp.setHireDate(new java.sql.Date(hireDate.getTime()));java.util.Date不能直接塞进java.sql.Date里得先 parse 再转这个坑我今天踩了两次才想起来。第三插入成功后重定向到列表页避免刷新重复提交response.sendRedirect(request.getContextPath() /employee/list);整个过程跑通以后我特意打开数据库客户端看了一眼数据确实插进去了编码正常。那一刻还是挺有成就感的因为这算是第一次真正意义上的前后端数据打通。4. 常见问题与排查技巧实录4.1 前端 500 错误JSTL 标签库无法解析现象列表页打开直接显示 HTTP 500控制台报错The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved。排查过程第一反应是 jar 包缺失。去WEB-INF/lib目录下一看果然只有 servlet-api.jar没有 jstl 相关依赖。因为我的项目是用 IDEA 建的普通 Web 工程不是 Maven 工程所以不会自动下载依赖。解决办法是手动把jakarta.servlet.jsp.jstl-api和jakarta.servlet.jsp.jstl两个 jar 包放进去老版本是jstl.jarstandard.jar然后重启 Tomcat 就好了。心得JSP 页面报 500 时先看控制台完整异常栈异常信息里通常会写清楚是标签解析失败、类找不到还是空指针。不要盯着页面发呆要养成“从异常栈倒推原因”的习惯。4.2 中文乱码从数据库到页面全链路排查现象新增一条“张三”进去页面显示“寮犱笁”数据库里存的也是乱码。排查过程这条链路上有四个可能出乱码的点。第一数据库表字符集如果不是 utf8mb4存进去就变了第二JDBC 连接串没加characterEncodingutf8mb4Java 和 MySQL 通信时编码不一致第三Servlet 层没调request.setCharacterEncoding(utf-8)POST 参数解析成了 ISO-8859-1第四JSP 页面本身没设置pageEncodingutf-8。我一个个排查最后发现连接串里少了编码参数加上之后乱码问题解决了。经验法则遇到乱码从上到下把“页面编码 → 请求编码 → 连接编码 → 数据库编码”过一遍基本能定位。大多数培训班新人乱码的根子都在 request.setCharacterEncoding 漏写。4.3 数据库连接超时或连接被拒绝现象Tomcat 启动正常但一访问列表页就报com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure。排查过程这个报错大多数时候不是代码问题而是 MySQL 服务没启动。Windows 里可以打开服务管理器看看 MySQL 服务状态如果是 Linux 环境用systemctl status mysql查看。另一种可能是连接串里的端口不对MySQL 默认 3306如果你本地装的是多个数据库实例端口可能被改了。心得数据库相关的报错先分“网络通不通”和“认证通过没有”Communications link failure是前者Access denied for user是后者两类问题的排查方向完全不同别混在一起。4.4 页面显示“404”或点击链接没反应现象列表页能打开但点“删除”链接后提示 404。排查过程404 说明 URL 映射没匹配上。我检查了注解WebServlet(/employee/delete)发现路径拼写和 JSP 里的href/employee/delete?id1是一致的但前边漏了request.getContextPath()。在 IDEA 中项目的访问路径默认是http://localhost:8080/项目名/所以完整路径应该是/项目名/employee/delete。如果 JSP 里直接写绝对路径/employee/deleteTomcat 会从根路径去找自然找不到。解决办法有两类一是 JSP 里所有链接都加上\${pageContext.request.contextPath}前缀二是用相对路径不推荐层级一变就断。4.5 Servlet 里获取不到前端传的多个参数现象搜索框输入“技术部”点搜索列表页显示全部数据搜索条件没生效。排查过程搜出来的 SQL 是WHERE name LIKE ? OR department LIKE ?如果 keyword 是“技术部”name 字段里没包含department 字段里包含理论上是能查出来的。打印日志发现 keyword 参数取值是空的。仔细看 form 表单input 的 name 属性写的是keyword但 Servlet 里request.getParameter(keyWord)大小写写错了结果取到 null条件就被跳过了。心得HTML 的 name、Servlet 的 getParameter、数据库字段名这三者的命名必须完全一致。这个 bug 排查了将近二十分钟只是因为大小写差异。建议在代码里加一句System.out.println(keyword keyword)用日志输出定位参数到底有没有传过来。5. 实战后的复盘与经验沉淀5.1 用表格记录每天的上机收获第38天练完以后我开始养成一个习惯每次上机练习结束用十几分钟把当天的知识点、遇到的问题、解决思路记成一张表格也可以用文档软件记录。说实话这半小时的投入比多敲两个小 demo 的收获更大因为记录的过程就是在强制自己做知识梳理。时间遇到问题根因解决方法上午JSP 页面 500缺 JSTL jar 包手动引入 jar 包上午新增中文乱码JDBC 连接串缺编码参数添加 characterEncodingutf8mb4下午删除链接 404绝对路径没带项目名加 ${pageContext.request.contextPath}下午搜索条件失效getParameter 参数名大小写不一致统一参数命名加日志输出再过几天回看这张表你会发现很多问题其实都是同一类原因比如“命名不一致”“编码没设置”“资源没关闭”。把这些问题归类以后后面写代码的时候就会下意识去避免。5.2 从“完成功能”到“优化体验”的转变第38天下午列表功能已经全部跑通了但页面丑得一言难尽而且删除后没有提示搜索没有防抖数据多了翻页也只是走上一页下一页。老师问了一句“你打算就这样交差了”这句话点醒了我。上机练习的真正目标不是把功能做出来而是把一个功能做完整。于是我做了三个体验层面的小优化删除后页面自动带一个提示参数比如/employee/list?msgdeleted列表页检测到这个参数就弹一行提示文字“删除成功”。搜索框加了autofocus用户一打开页面光标就定位在搜索框少点一次鼠标。表格在数据超过10行时给表头加阴影固定CSS position: sticky滚动时表头不会消失。这些优化都不难但完成之后这个练习项目的完成度明显上了一个台阶。真实项目里这种“非功能需求”往往会占掉一大半的工期早点意识到这一点对职业成长非常有帮助。5.3 下一步可以扩展的方向第38天这个项目如果后面还有时间和精力我有几个方向可以继续延伸一是引入分页自己手写一个 LIMIT 分页逻辑这个能加深对 SQL 性能的理解二是把 JDBC 工具类改成连接池版本理解资源复用三是给表单加前后端双重校验后端校验尤其重要纯前端校验是可以被绕过掉的再往后才是学框架把整个项目从 Servlet 迁移到 Spring Boot。另外一个建议是练习之余可以看看行业内真实项目的代码组织方式。培训机构教出来的代码往往偏学院派真实工程里还会考虑日志埋点、配置分离、接口版本这些细节。把这些意识提前建立起来后面进团队协作时会少走很多弯路。写在最后第38天的上机练习对我来说是一个阶段性的总结也是从“学生思维”向“工程思维”转变的开始。之前每节课都是学一个点做一个小练习从来没机会把这些点连成一条线。直到这一天我盯着浏览器里完整渲染出来的员工列表用鼠标完成了一次搜索、一次新增、一次删除看着数据库里的数据跟着变化才突然体会到所谓的“做项目”到底是什么感觉。如果你也正好处在一个类似的阶段我的建议是别急着追求工具多新、框架多流行手动把一个完整的闭环做出来比看一百篇教程都有用。代码写得丑没关系踩坑越多越好只要每次都能搞清楚为什么第38天后积累起来的东西就是以后面试和工作中最扎实的底气。
返回列表