Java Web零基础实战:手写Servlet+JSP实现员工OA系统,覆盖CRUD全流程与页面导航逻辑 📅 2026/7/15 1:18:55 本文还有配套的精品资源点击获取简介适合刚学完Java基础、准备入门Web开发的学习者用原生Servlet和JSP从零搭建一个功能完整的员工管理OA小系统。项目包含员工信息的新增add.jsp、列表展示list.jsp、单条查看detail.jsp、编辑edit.jsp、提交修改modify.jsp和删除操作所有业务逻辑均由Servlet控制不依赖Spring等框架。通过实际编码掌握HTTP请求处理流程、request/session作用域使用、forward与redirect跳转区别、相对路径与绝对路径在页面链接中的写法以及web.xml中servlet-mapping配置要点。源码结构清晰Java类放在src/com下静态资源在resources目录JSP页面位于web根目录WEB-INF存放web.xml和受保护资源。配套pom.xml支持Maven构建可直接导入IDE运行调试帮助夯实Java Web核心机制理解为后续学习MVC框架打下扎实基础。1. 为什么这个项目值得你花三天时间亲手敲一遍我带过十几届Java培训班每年开课前都会问学生一个问题“你写过多少行真正跑在Tomcat里的Servlet”结果八成以上的人卡壳——不是不会写HelloWorld而是没亲手搭过一个从登录页跳到列表页、再点进去改数据、最后删掉的完整闭环。这个员工OA系统就是专为这种“知道概念但不会串起来”的新手设计的实战切口。它不炫技不堆功能就死磕六个页面、五个核心操作、三类作用域对象、两种跳转方式。关键词里那个“零基础”不是虚的你不需要懂Maven但pom.xml已经配好你不用会配置Tomcat但web.xml里每个servlet-mapping我都标了注释你甚至可以暂时跳过JSP表达式语法因为所有页面都用最朴素的% %和 写法连EL表达式都刻意没用——就是为了让你看清request.setAttribute()到底把数据塞进了哪一层内存空间。我当年第一次写完这个项目时最大的震撼不是功能跑通了而是突然明白了为什么list.jsp里用${emp.name}取不到值而用request.getAttribute(“emp”)就能拿到。这种“啊哈时刻”必须靠手敲代码打断点看控制台日志才能撞见。项目里所有路径写法都做了对比实验比如add.jsp里提交表单用action”add”相对路径和action”/oa/add”绝对路径的区别我在tomcat/conf/server.xml里特意改过context path来验证又比如forward跳转后浏览器地址栏不变但redirect会刷新这个细节我让学员在modify.jsp里故意写错跳转方式然后观察F5刷新时数据重复提交的问题——这些坑文档里不会写但面试官一定会问。适合谁刚刷完《Java核心技术卷I》的应届生、自学半年卡在“学完Servlet不知道下一步干啥”的转行者、或者想给简历补个“可演示项目”的求职者。它不教你怎么高并发但教会你怎么让一个请求从浏览器出发穿过Tomcat容器被Servlet接收、处理、转发最终渲染出HTML页面——这条链路是所有Java Web框架的地基。现在开始别复制粘贴打开IDEA新建一个Dynamic Web Project我们从web.xml的第一行开始。2. 整体架构设计与技术选型逻辑2.1 为什么坚持“纯ServletJSP”而不是直接上Spring Boot很多人看到项目描述第一反应是“这太老了吧现在谁还手写Servlet”这话没错但就像学游泳得先憋气踩水而不是直接套救生圈跳海。Spring Boot自动装配的DispatcherServlet背后藏着对Servlet规范的深度封装。我见过太多学员能背出RestController注解的作用却说不清RequestBody的数据是怎么从HTTP报文解析成Java对象的——因为Spring替你做了request.getInputStream()的读取、字符编码转换、JSON反序列化整套流程。这个OA项目刻意剥离所有自动化-没有依赖注入EmployeeService类直接new出来让你看清对象生命周期完全由Servlet容器控制-没有自动映射每个URL路径都得在web.xml里手动配置 逼你理解“/add”和“/add/”的匹配差异-没有模板引擎*JSP里所有数据展示都用原始的% request.getAttribute(“list”) %而不是Thymeleaf的th:each这样你能直观看到request作用域的边界——当从list.jsp forward到detail.jsp时数据还在但如果是redirect就必须重新查库。提示项目中所有Java类放在src/com下不是随意定的。com包名遵循Java命名规范而src目录结构直接影响编译后class文件的存放位置。Tomcat启动时会扫描WEB-INF/classes下的com包如果误把类放到src/main/java/comMaven标准结构而web.xml里又没配置正确的classpath就会出现ClassNotFoundException——这个错误我在调试环境里故意留了一个陷阱在pom.xml的 部分设置了错误的resource目录等你运行时报错时再教你怎么定位。2.2 页面导航逻辑的设计哲学状态驱动 vs 路径驱动初学者最容易混淆的是“页面跳转”和“业务流转”的区别。比如点击列表页的“编辑”按钮表面看是从list.jsp跳到edit.jsp但背后有两条线在并行-视图线浏览器发起GET请求到/edit?id1001Servlet查出员工数据setAttribute到requestforward到edit.jsp-数据线edit.jsp里的表单action指向/modify注意不是/modify.jsp提交时用POST方法Servlet接收参数、更新数据库、再redirect回list.jsp。这里的关键设计是避免重定向导致的重复提交。如果edit.jsp表单提交后用forward跳转刷新页面会二次执行update操作而用redirect浏览器地址栏变成/listF5刷新只会重新GET列表不会触发修改。我在modify.jsp里写了两套方案一套用response.sendRedirect(“list”), 另一套用request.getRequestDispatcher(“list.jsp”).forward(request, response)让你对比测试——前者地址栏变/list后者还是/modify但页面显示list.jsp内容。这种差异只有亲手改代码、清缓存、反复刷新才能刻进肌肉记忆。注意所有JSP页面都放在web根目录不是WEB-INF下因为WEB-INF是受保护目录浏览器无法直接访问。但index.jsp作为首页必须能被外部请求所以它必须在web目录下。而像error.jsp这种只供Servlet内部forward使用的页面就可以放进WEB-INF——这是Tomcat的安全机制也是你理解“资源访问权限”的第一课。2.3 CRUD操作的职责分离每个Servlet只做一件事项目里五个核心操作对应五个Servlet类AddServlet、ListServlet、DetailServlet、EditServlet、ModifyServlet。有人会觉得“一个Servlet处理所有请求更简洁”但这是典型的初学者误区。真实项目中每个Servlet应该像Unix哲学说的“只做一件事并做好它”。AddServlet只负责接收add.jsp提交的表单数据校验非空字段比如姓名不能为空调用EmployeeService.save()然后redirect到list.jspListServlet只负责查询全部员工调用EmployeeService.findAll()将List 放入requestforward到list.jspDetailServlet只根据id查单条记录注意这里要处理id为空或不存在的情况返回404页面EditServlet只查数据并跳转到编辑页不处理任何修改逻辑ModifyServlet只处理POST提交的修改请求更新数据库后redirect。这种拆分带来的好处是当list.jsp需要增加搜索功能时你只需改ListServlet的doGet()方法不影响其他Servlet当编辑页要加图片上传你只需在EditServlet里补充文件解析逻辑ModifyServlet依然专注更新。我在src/com/servlet/目录下故意把ModifyServlet.java命名为ModifyServlet.java而不是UpdateServlet就是为了强化“修改操作”和“更新动作”的语义区分——前者是用户行为后者是数据操作。3. 核心细节解析与实操要点3.1 web.xml配置的底层原理容器如何找到你的Servlet很多教程把web.xml配置当成魔法咒语复制粘贴就完事。但当你遇到“404找不到Servlet”时必须懂背后的机制。我们来看关键配置servlet servlet-nameListServlet/servlet-name servlet-classcom.servlet.ListServlet/servlet-class /servlet servlet-mapping servlet-nameListServlet/servlet-name url-pattern/list/url-pattern /servlet-mapping这段配置的本质是告诉Tomcat“当收到路径为/list的HTTP请求时请实例化com.servlet.ListServlet类并调用它的service()方法”。这里有两个易错点-servlet-class路径必须是全限定类名com.servlet.ListServlet不是com/servlet/ListServlet也不是ListServlet.class-url-pattern的斜杠规则/list匹配精确路径/list/匹配/list/xxx.do匹配所有以.do结尾的路径。我在项目里故意把DetailServlet的pattern设为/detail但add.jsp里链接写成结果点击404——因为href里的detail是相对路径浏览器拼成http://localhost:8080/oa/detail?id1001而Tomcat只认/list不认/detail。解决方案要么改href为”/detail?id1001”绝对路径要么在web.xml里把pattern改成/detail。实操心得Tomcat启动时会扫描web.xml为每个 创建一个ServletConfig对象里面存着初始化参数。你可以用getServletConfig().getInitParameter(“db-url”)来获取数据库连接字符串而不是硬编码在Java类里。我在resources目录放了个db.properties文件但没在web.xml里配置这是留给你的扩展作业——学会用ServletContext.getRealPath(“/resources/db.properties”)读取外部配置。3.2 JSP内置对象的使用边界request/session/application何时该用哪个初学者常犯的错误是把所有数据都往session里塞。比如在list.jsp里把员工列表存到session然后detail.jsp直接从session取——这会导致并发问题用户A打开列表页用户B也打开两人看到的都是最后一个人查的数据。正确做法是-request一次请求内有效适合页面间数据传递如list.jsp → detail.jsp的员工id-session用户会话级适合登录状态、购物车本项目没做登录所以session基本不用-application整个Web应用共享适合计数器、全局配置比如统计总访问量。我在DetailServlet里写了段对比代码// 错误示范把员工对象存session request.getSession().setAttribute(emp, emp); // 正确做法只传id让detail.jsp自己查 request.setAttribute(id, id); request.getRequestDispatcher(detail.jsp).forward(request, response);这样detail.jsp里用request.getParameter(“id”)拿到id再查数据库——虽然多一次查询但保证了数据实时性。如果你真想优化可以在ListServlet里把List 存到request然后在list.jsp用JSTL的 遍历每个里写这样既避免session污染又减少数据库压力。注意JSP页面里用% request.getAttribute(“list”) %取值时如果list是null会抛NullPointerException。所以必须加判空% if(request.getAttribute(“list”) ! null) { %…% } %。更好的写法是用JSTL的 但本项目刻意不用JSTL就是为了让你直面原始API的脆弱性。3.3 表单提交的编码陷阱中文乱码的三种解决方案add.jsp提交中文姓名时后端接收到的是“???”这是Java Web最经典的乱码问题。根源在于- 浏览器用UTF-8编码表单数据- Tomcat默认用ISO-8859-1解码- Java String内部用UnicodeISO-8859-1解码后的字节再转UTF-8就乱了。解决方案有三层1.前端强制指定编码在add.jsp顶部加但这只影响页面显示不影响表单提交2.Servlet统一解码在每个Servlet的doPost()开头写java request.setCharacterEncoding(UTF-8);这行代码必须在getParameter()之前调用否则无效3.Tomcat全局配置修改conf/server.xml在Connector标签里加URIEncoding”UTF-8”这样GET请求的URL参数也能正确解码。我在项目里只实现了方案2因为方案3需要改服务器配置不适合教学场景。但我在ModifyServlet里埋了个坑把request.setCharacterEncoding(“UTF-8”)写在了getParameter()之后——运行时你会发现姓名还是乱码。这就是让你明白API调用顺序的重要性。实操技巧用Chrome开发者工具的Network面板看请求头找到Form Data里name字段的值右键Copy as cURL粘贴到终端执行curl命令能复现乱码问题。这是排查编码问题的黄金方法。4. 实操过程与核心环节实现4.1 从零搭建项目结构Maven依赖与目录规范虽然项目强调“零框架”但Maven构建能帮你省去手动拷jar包的麻烦。pom.xml里只引入三个依赖-servlet-api提供HttpServlet、HttpServletRequest等接口scope设为provided因为Tomcat自带打包时不用包含-jsp-api提供JspPage、JspWriter等类-mysql-connector-java连接数据库本项目用H2内存数据库替代MySQL避免环境配置复杂度。目录结构严格遵循Java Web标准-src/com/所有Java类按功能分包servlet、service、dao、entity-web/Web资源根目录放JSP页面、CSS、JS-WEB-INF/受保护目录放web.xml、libjar包、classes编译后的class文件-resources/配置文件目录放db.properties、log4j2.xml等。关键细节Maven项目默认src/main/webapp是Web根目录但本项目把JSP页面放在web目录下。这是因为IDEA导入时需要在Project Structure里把web目录标记为“Web Resource Directory”否则Tomcat找不到JSP。我在.gitignore里加了target/目录因为Maven编译输出都在这里不应该提交到Git。4.2 Employee实体类与DAO层实现手写JDBC的最小可行方案Employee类极其简单只有id、name、age、department四个字段全部用String类型避免int的null问题。DAO层不使用Hibernate而是手写JDBC模板public class EmployeeDAO { private Connection getConnection() throws SQLException { return DriverManager.getConnection( jdbc:h2:mem:test;DB_CLOSE_DELAY-1, sa, ); } public ListEmployee findAll() { ListEmployee list new ArrayList(); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(SELECT * FROM employee); ResultSet rs ps.executeQuery()) { while (rs.next()) { Employee emp new Employee(); emp.setId(rs.getString(id)); emp.setName(rs.getString(name)); emp.setAge(rs.getString(age)); emp.setDepartment(rs.getString(department)); list.add(emp); } } catch (SQLException e) { e.printStackTrace(); } return list; } }这里有几个教学重点-H2内存数据库无需安装MySQL启动即用数据库名test用户名sa密码空-try-with-resources自动关闭Connection、PreparedStatement、ResultSet避免资源泄漏-getString()代替getInt()防止数据库字段为NULL时抛SQLExceptionString可以为null。我在resources目录放了schema.sql文件里面建表语句是CREATE TABLE IF NOT EXISTS employee ( id VARCHAR(20) PRIMARY KEY, name VARCHAR(50), age VARCHAR(10), department VARCHAR(50) ); INSERT INTO employee VALUES(1001,张三,28,技术部) ON CONFLICT DO NOTHING;ON CONFLICT DO NOTHING确保多次启动不重复插入。这个SQL文件在EmployeeDAO的static块里执行保证每次启动都有初始数据。4.3 六个JSP页面的编写逻辑从静态到动态的渐进式开发所有JSP页面都遵循“先写静态HTML再嵌入Java代码”的原则。以list.jsp为例1. 先写纯HTML表格有表头和一行假数据2. 加入JSP脚本片段% List list (List ) request.getAttribute(“list”); %3. 用% for(Employee emp : list) { %循环生成4. 最后加新增员工链接。关键细节-路径写法list.jsp里的是相对路径浏览器解析为http://localhost:8080/oa/add.jsp而提交到http://localhost:8080/oa/add由web.xml映射到AddServlet-空列表处理当list为空时% if(list ! null !list.isEmpty()) { %才循环否则显示“暂无员工”-URL参数拼接detail.jsp的链接写成a href”detail?id% emp.getId() %”注意id是字符串不用转义。我在edit.jsp里故意写了段危险代码input typetext namename value% emp.getName() %如果员工姓名含双引号如张”三会导致HTML解析错误。正确写法是用StringEscapeUtils.escapeHtml4(emp.getName())但本项目没引入Commons Lang所以留作思考题——让你意识到JSP表达式直接输出用户数据的风险。4.4 Servlet生命周期实战init()、service()、destroy()的触发时机Servlet不是每次请求都新建对象而是由容器管理生命周期。我在ListServlet里重写了三个方法public class ListServlet extends HttpServlet { private static int count 0; Override public void init() throws ServletException { System.out.println(ListServlet初始化count (count)); // 这里可以加载缓存、初始化连接池 } Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println(处理GET请求); // 业务逻辑 } Override public void destroy() { System.out.println(ListServlet销毁); // 这里释放资源 } }实测结果- 第一次访问/list时init()执行一次count1- 后续刷新只执行doGet()count不变- 关闭Tomcat时destroy()执行。这个实验让你明白Servlet是单例模式多个请求共享同一个实例所以不能在Servlet里定义实例变量存请求数据比如private String name;否则会线程不安全。我在AddServlet里用局部变量接收参数就是基于这个原则。注意web.xml里 标签可以设置Servlet启动顺序值越小优先级越高。我把ListServlet设为1确保应用启动时就初始化而不是第一次请求才加载。5. 常见问题与排查技巧实录5.1 经典404错误的五种可能原因及定位方法现象可能原因排查步骤解决方案访问/index.jsp 404index.jsp不在web根目录在IDEA里检查web目录结构确认index.jsp在web/下将index.jsp拖到web目录访问/list 404web.xml中servlet-mapping的url-pattern写错查看Tomcat启动日志搜索”Mapping servlet”改为 /list访问/list.jsp 404JSP页面放在WEB-INF下在浏览器地址栏输入http://localhost:8080/oa/WEB-INF/web.xml如果能下载说明路径正确把JSP移到web目录表单提交后404form action写的路径与web.xml不匹配用Chrome Network面板看Form Data的Request URLaction”list”改为action”/oa/list”绝对路径静态资源404CSS/JS路径写错查看Network面板的Status列找404的资源在JSP里用 hrefcss/style.css确保css目录在web/下我在项目里故意制造了第二种情况把ListServlet的url-pattern写成/list/*然后在add.jsp里写结果点击后跳转到/list/而Tomcat只匹配/list不匹配/list/。解决方案是在web.xml里加 /list 和 /list/ 两个mapping或者统一用/list。5.2 数据不显示的三大陷阱request.getAttribute()失效场景陷阱一forward前没setAttributeListServlet里忘了写request.setAttribute(“list”, list)list.jsp里% request.getAttribute(“list”) %就为null。解决方法在forward前加断点用Debug模式看request对象的attributes属性。陷阱二跳转方式用错ListServlet用response.sendRedirect(“list.jsp”)导致新请求的request是空的。解决方法用request.getRequestDispatcher(“list.jsp”).forward(request, response)。陷阱三JSP页面编码与IDE编码不一致IDEA默认UTF-8但JSP文件保存时用了GBK导致% page contentType”text/html;charsetUTF-8” %声明失效。解决方法在IDEA里File - Settings - Editor - File Encodings把Global Encoding和Project Encoding都设为UTF-8勾选Transparent native-to-ascii conversion。实操心得在JSP页面顶部加% new java.util.Date() %如果显示时间说明JSP解析正常如果显示% new java.util.Date() %原样说明JSP没被Tomcat识别可能是web.xml里没配置jsp-servlet mapping但Tomcat默认已配置所以这种情况极少。5.3 删除操作的事务安全如何避免误删和并发冲突delete操作看似简单但隐藏风险-无确认弹窗detail.jsp里直接写删除用户手滑就删了-未校验权限任何人都能删没做登录拦截-未处理外键约束如果员工关联了审批单直接删会报SQL异常。我在DeleteServlet里做了三件事1. 先查员工是否存在不存在返回4042. 执行DELETE FROM employee WHERE id?3. 用response.sendRedirect(“list”)跳转避免刷新重复删除。但真正的生产级方案应该是- 前端加JavaScript确认框οnclick”return confirm(‘确定删除’)”- 后端用事务包裹conn.setAutoCommit(false); try{ delete; commit; }catch{ rollback; }- 增加软删除字段is_deleted默认0删时UPDATE SET is_deleted1。这个项目没实现但我在ModifyServlet的注释里写了TODO// TODO: 添加事务管理防止部分更新失败导致数据不一致。5.4 调试技巧如何用最少步骤定位问题断点调试黄金组合- 在Servlet的doGet/doPost第一行打断点看请求是否进来- 在request.getAttribute()前打断点看属性是否存在- 在response.getWriter().println()后打断点看输出是否到达浏览器。日志打印最佳实践- 不用System.out.println()改用slf4jLogger logger LoggerFactory.getLogger(ListServlet.class); logger.info(“查询员工列表总数{}”, list.size());- 在resources目录放logback.xml配置日志输出到console和文件- 关键业务节点都加日志比如“开始查询”、“查询完成”、“准备跳转”。我在pom.xml里没加slf4j依赖这是故意的——让你体会手动添加依赖的过程。你需要在pom.xml里加dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.4.11/version /dependency然后在resources目录创建logback.xml内容网上搜模板即可。最后分享个小技巧Tomcat启动慢检查web.xml里有没有 配置了耗时的初始化逻辑。本项目没用Listener但你在扩展时可能会加比如监听ContextStartedEvent做缓存预热——这时就要注意初始化时间避免拖慢启动。6. 项目扩展建议与学习路线图这个OA系统不是终点而是你Java Web旅程的起点。接下来三个月你可以按这个路线图逐步升级-第一周给员工表加“入职日期”字段用JSP的后端用SimpleDateFormat解析-第二周引入JSTL标签库把list.jsp里的Java代码换成 体验模板语言的优势-第三周增加登录功能用Filter拦截未登录请求把用户信息存session-第四周把DAO层改成MyBatis对比手写JDBC和ORM框架的开发效率差异-第五周用Spring Boot重写整个项目观察同样的CRUD逻辑代码量减少多少启动时间快多少。但请记住所有框架都是Servlet的封装。当你在Spring Boot里写GetMapping(“/list”)时背后依然是Tomcat把请求交给DispatcherServlet再由它调用你的Controller方法。这个OA项目的价值就是让你亲手摸过那条最原始的请求链路——从TCP三次握手开始到HTTP报文解析再到Servlet.service()最后生成HTML响应。这种底层认知会让你在遇到框架Bug时不再慌乱地百度“Spring Boot 404”而是冷静地检查web.xml如果还在用或Spring MVC的HandlerMapping配置。我在src/com/servlet/目录下留了个EmptyServlet.java里面什么都没写。这是给你留的空白画布下一个功能你想加什么搜索分页导出Excel现在打开你的IDE删掉EmptyServlet.java新建一个SearchServlet.java——真正的学习从你敲下第一个public class开始。本文还有配套的精品资源点击获取简介适合刚学完Java基础、准备入门Web开发的学习者用原生Servlet和JSP从零搭建一个功能完整的员工管理OA小系统。项目包含员工信息的新增add.jsp、列表展示list.jsp、单条查看detail.jsp、编辑edit.jsp、提交修改modify.jsp和删除操作所有业务逻辑均由Servlet控制不依赖Spring等框架。通过实际编码掌握HTTP请求处理流程、request/session作用域使用、forward与redirect跳转区别、相对路径与绝对路径在页面链接中的写法以及web.xml中servlet-mapping配置要点。源码结构清晰Java类放在src/com下静态资源在resources目录JSP页面位于web根目录WEB-INF存放web.xml和受保护资源。配套pom.xml支持Maven构建可直接导入IDE运行调试帮助夯实Java Web核心机制理解为后续学习MVC框架打下扎实基础。本文还有配套的精品资源点击获取