简介:适合刚学完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 为什么坚持“纯Servlet+JSP”,而不是直接上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/com(Maven标准结构),而web.xml里又没配置正确的classpath,就会出现ClassNotFoundException——这个错误我在调试环境里故意留了一个陷阱,在pom.xml的 部分设置了错误的resource目录,等你运行时报错时再教你怎么定位。
2.2 页面导航逻辑的设计哲学:状态驱动 vs 路径驱动
初学者最容易混淆的是“页面跳转”和“业务流转”的区别。比如点击列表页的“编辑”按钮,表面看是从list.jsp跳到edit.jsp,但背后有两条线在并行:
- 视图线:浏览器发起GET请求到/edit?id=1001,Servlet查出员工数据,setAttribute到request,forward到edit.jsp;
- 数据线:edit.jsp里的表单action指向/modify(注意不是/modify.jsp),提交时用POST方法,Servlet接收参数、更新数据库、再redirect回list.jsp。
这里的关键设计是避免重定向导致的重复提交。如果edit.jsp表单提交后用forward跳转,刷新页面会二次执行update操作;而用redirect,浏览器地址栏变成/list,F5刷新只会重新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.jsp;
- ListServlet:只负责查询全部员工,调用EmployeeService.findAll(),将List 放入request,forward到list.jsp;
- DetailServlet:只根据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-name>ListServlet</servlet-name>
<servlet-class>com.servlet.ListServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ListServlet</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?id=1001,而Tomcat只认/list,不认/detail。解决方案要么改href为”/detail?id=1001”(绝对路径),要么在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内部用Unicode,ISO-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、lib(jar包)、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 List<Employee> findAll() {
List<Employee> 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时抛SQLException,String可以为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;而



被折叠的 条评论
为什么被折叠?



