写在前面
如果你正在学习Java Web开发,面对Servlet、JSP、Filter、Session这些概念感到眼花缭乱,不知道它们之间有什么关系,也不知道学了能做什么——那么这篇文章就是为你准备的。
本文采用「问题驱动」的方式,带你重新理解Java Web开发的每一个核心知识点。我们不堆砌概念,而是先搞清楚每个技术「解决了什么痛点」,再学习「怎么用」,最后落地到「企业如何实战」。
让我们开始这段Java Web探索之旅。
阶段一:问题锚定——我们为什么要学Java Web?
场景化抛出问题
想象这样一个场景:你要开发一个用户登录功能。用户在浏览器输入用户名密码,点击登录,系统验证身份后,跳转到欢迎页面。
看起来很简单,对吧?但仔细想想,这里面有一堆问题需要解决:
用户输入的数据怎么传给服务器? 用户在表单里填的信息,浏览器怎么包装?服务器怎么解析?
服务器怎么处理登录请求? 谁来接收HTTP请求?谁来调用业务逻辑?
登录成功后,怎么记住用户? HTTP协议是无状态的,第二次请求时服务器怎么知道「这个用户已经登录过」?
页面怎么动态显示用户信息? 不可能给每个用户都写一个静态HTML页面吧?
所有请求都要做权限校验,难道每个页面都写一遍验证代码?
项目启动时要加载一些初始化数据,写在哪儿?
这些问题,就是Java Web要解决的核心问题。
技术定位梳理
Java Web,简单说就是用Java语言开发网站应用的一套技术体系。它的核心价值在于:
接收并处理HTTP请求(用户访问网站,服务器得知道他要干什么)
动态生成响应内容(不同用户看到不同的页面)
管理用户会话状态(记住谁登录了、购物车里有什么)
提供标准化的开发组件(让开发者专注于业务逻辑,不用重复造轮子)
技术边界说明
Java Web能做什么?几乎所有你能想到的网站后端功能:用户登录注册、商品展示、订单处理、数据管理……
但Java Web不是万能的:
它不负责页面好不好看(那是CSS的事)
它不处理用户交互特效(那是JavaScript的事)
它不适合做实时性极高的游戏服务器(那需要Netty等NIO框架)
清楚了这些问题,我们再来看具体的技术组件,就会有「原来它是为解决这个问题而生的」的恍然大悟感。
阶段二:基础认知——构建最小知识体系
在深入每个技术之前,我们需要先建立一个「最小化」的知识框架,理解这些概念到底是干什么的。
一丢丢前端基础:用户看到的和用户操作的
核心概念拆解
前端就是你打开一个网站看到的页面:文字、图片、按钮、输入框。用户在前端填写表单、点击按钮,这些操作会产生HTTP请求,发送到后端服务器。
最简单的HTML表单长这样:
用户点击登录后,浏览器会把用户名和密码打包,发送到login这个地址。
为什么要懂一点前端?
因为Java Web开发中,你既要处理后端逻辑,也要写前端页面(JSP本质上就是HTML+Java代码)。理解请求怎么发出去的,才能知道后端怎么接。
XML:配置文件的「老祖宗」
问题锚定:程序的某些参数(比如数据库连接地址、端口号)如果硬编码在代码里,改起来要重新编译,太麻烦了。能不能把这些「可变的部分」抽出来单独管理?
核心概念:XML(可扩展标记语言)就是一种用来存储结构化数据的文本格式。它用标签来定义数据,长得像HTML,但标签不是预定义的,你可以自己发明。
最小示例:
以前Java Web项目的配置文件web.xml长这样:
核心原理:XML通过标签嵌套表达层级关系,通过属性添加元信息。程序可以解析XML文件,读取里面的配置项,实现「配置分离」。
现在还用吗? 现在更流行用properties或YAML格式,因为更简洁。但XML依然广泛存在于老项目和一些中间件配置中,理解它对维护老系统很有帮助。
JSON:前后端通信的「普通话」
问题锚定:前端和后端要交换数据,怎么格式化这些数据?XML太啰嗦了,能不能有一种更轻量、更易读的格式?
核心概念:JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它长得特别像JavaScript里的对象,但语言无关,几乎所有编程语言都支持。
json
{
"name": "张三",
"age": 25,
"hobbies": ["读书", "编程"]
}
对比XML:
XML:
JSON:{"name":"张三","age":25}
JSON明显更简洁,而且解析速度更快。现在的Web开发中,JSON已经是前后端通信的事实标准。
最小示例:
前端发一个JSON格式的POST请求:
fetch('/api/user', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: '张三', age: 25})
})
后端收到后,可以方便地解析成Java对象。
Servlet:Java Web的「发动机」
问题锚定:用户发了HTTP请求过来,总得有段代码来处理吧?谁来接收请求参数?谁来调用业务方法?谁来返回响应页面?
核心概念:Servlet就是Java Web中专门处理HTTP请求的Java程序。它运行在Web服务器(如Tomcat)里面,负责:
接收HTTP请求
解析请求参数
调用业务逻辑(查询数据库、计算等)
生成响应(HTML页面、JSON数据等)
最小示例:
创建一个最简单的Servlet:
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("
");out.println("
Hello, Java Web!
");out.println("");
}
}
在web.xml中配置访问路径:
启动Tomcat,访问http://localhost:8080/你的应用/hello,就能看到页面了。
核心原理:Servlet的生命周期由容器(Tomcat)管理:
初始化:第一次访问或启动时,调用init()方法
服务:每次请求,调用service()方法(根据请求类型分发给doGet()或doPost())
销毁:应用卸载时,调用destroy()方法
JSP:在HTML里写Java的「便利贴」
问题锚定:用Servlet输出HTML太痛苦了,每一行HTML都要用out.println()包起来,要是页面复杂一点,代码根本没法维护。能不能直接在HTML里嵌入Java代码?
核心概念:JSP(Java Server Pages)本质上就是HTML,但允许在里面插入Java代码片段。它运行时会先被容器翻译成Servlet,再执行。
最小示例:
创建一个hello.jsp文件:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
当前时间:<%= new java.util.Date() %>
<%-- 这是JSP注释 --%>
<%
String name = request.getParameter("name");
if (name != null) {
out.println("你好," + name);
}
%>
访问http://localhost:8080/你的应用/hello.jsp?name=张三,页面会显示当前时间和欢迎语。
核心原理:JSP的工作原理是「先翻译后编译」:
客户端请求JSP页面
JSP容器将JSP文件翻译成Servlet源码(.java文件)
编译成字节码(.class文件)
执行这个Servlet,输出HTML到浏览器
JSTL:让JSP页面更干净的「标签库」
问题锚定:JSP虽然方便,但如果在页面里写大量Java代码(<% ... %>),页面会变得混乱难维护。能不能用类似HTML标签的方式来写逻辑?
核心概念:JSTL(JSP标准标签库)就是一套预定义的标签,用来替代JSP中的Java代码。比如循环、判断、格式化等。
最小示例:
使用JSTL的
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
- ${user.name} - ${user.age}岁
配合EL表达式(${user.name}),JSP页面可以做到几乎不含Java代码,只关注数据展示。
Filter:请求的「安检门」
问题锚定:网站有很多功能,比如大部分页面都需要检查用户是否登录、记录访问日志、处理中文乱码。难道每个Servlet都写一遍这些代码?
核心概念:Filter(过滤器)就像一道安检门,在请求到达Servlet之前、响应返回客户端之前,可以对请求和响应进行拦截和处理。
最小示例:
写一个过滤器处理全站中文乱码:
public class EncodingFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
chain.doFilter(request, response); // 放行,继续执行后续过滤器或目标Servlet
}
}
核心原理:多个Filter可以组成过滤器链,按顺序执行。每个Filter决定是继续执行链(chain.doFilter()),还是直接返回响应(拦截)。
Listener:容器的「监控摄像头」
问题锚定:应用启动时想加载一些初始化数据(比如缓存字典表),应用关闭时要释放资源。谁来监听这些「事件」?
核心概念:Listener(监听器)用来监听Web应用中的各种事件:应用启动/关闭、会话创建/销毁、属性添加/移除等。
最小示例:
监听应用启动,加载配置:
public class AppStartupListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
System.out.println("应用启动,加载初始化数据...");
// 加载配置文件、初始化数据库连接池等
}
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("应用关闭,释放资源...");
}
}
Cookie:存在浏览器里的「小纸条」
问题锚定:用户登录后,下次再来访问,怎么记住他?HTTP是无状态的,每次请求都是独立的。
核心概念:Cookie是服务器发给浏览器、保存在浏览器上的一小段文本。浏览器下次访问同一网站时,会自动携带这些Cookie。
最小示例:
在Servlet中设置Cookie:
Cookie cookie = new Cookie("username", "张三");
cookie.setMaxAge(60 * 60 * 24); // 有效期1天
response.addCookie(cookie);
读取Cookie:
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("username".equals(cookie.getName())) {
String username = cookie.getValue();
}
}
}
Session:存在服务器里的「档案袋」
问题锚定:Cookie存在浏览器,容量有限(4KB左右),而且用户可能禁用Cookie。更严重的是,敏感信息(如用户角色、权限)存在浏览器不安全。怎么办?
核心概念:Session是服务器端为每个用户创建的一个独立存储空间。服务器会给每个Session分配一个唯一的ID(Session ID),通过Cookie(或URL重写)把这个ID传给浏览器。浏览器下次请求时带上这个ID,服务器就知道对应哪个Session了。
最小示例:
存数据到Session:
HttpSession session = request.getSession(); // 获取或创建Session
session.setAttribute("user", userObject); // 存对象
session.setMaxInactiveInterval(30 * 60); // 设置30分钟超时
取数据:
User user = (User) session.getAttribute("user");
if (user != null) {
// 用户已登录
}
核心原理:Session的生命周期:
创建:首次调用request.getSession()时创建
维护:每次请求携带Session ID,服务器更新最后访问时间
销毁:超时未访问、主动调用invalidate()、或应用关闭
阶段三:核心用法拆解——每个技术怎么用
了解了基础概念,我们来深入看看每个技术在企业开发中的常见用法和坑点。
3.1 前端基础:表单提交与请求方式
两种请求方法:
GET:参数在URL后面,?name=value形式。用于获取数据(如查看商品详情),有长度限制,不安全(参数暴露)。
POST:参数在请求体里。用于提交数据(如登录、注册),无长度限制,相对安全。
中文乱码问题:
Tomcat 8之前,GET请求的默认编码是ISO-8859-1,中文会乱码。解决方案:
POST乱码:在Servlet开头加request.setCharacterEncoding("UTF-8");
GET乱码:修改Tomcat的server.xml,添加URIEncoding="UTF-8"
3.2 XML:配置与解析
常见用法:
Web应用配置:web.xml中配置Servlet、Filter、Listener
框架配置文件:如MyBatis的Mapper XML文件
数据传输:早期WebService使用XML格式
解析方式:
DOM:把整个XML加载到内存,形成树,方便操作但耗内存
SAX:事件驱动,边读边解析,适合大文件
JAXB:Java对象和XML互相转换,最常用
3.3 JSON:序列化与反序列化
企业常用库:
Jackson:Spring Boot默认集成,功能强大
Fastjson:阿里出品,速度快但存在一些安全漏洞
Gson:Google出品,简洁易用
示例(Jackson):
// Java对象转JSON
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
// JSON转Java对象
User user = mapper.readValue(json, User.class);
3.4 Servlet:接收请求与返回响应
核心API:
HttpServletRequest:获取请求参数、请求头、Session等
HttpServletResponse:设置响应状态码、响应头、写入响应内容
常见用法:
// 获取参数
String username = request.getParameter("username");
String[] hobbies = request.getParameterValues("hobby");
// 请求转发(服务器内部跳转)
request.getRequestDispatcher("/success.jsp").forward(request, response);
// 重定向(浏览器重新发起请求)
response.sendRedirect("login.jsp");
// 返回JSON数据
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"msg\":\"成功\"}");
坑点:
请求转发 vs 重定向:转发是服务器内部跳转,URL不变;重定向是浏览器重新请求,URL改变。
Servlet线程安全:Servlet默认单例多线程,成员变量要注意线程安全问题。
3.5 JSP:脚本元素与内置对象
JSP组成:
指令:<%@ page ... %>,设置页面属性
声明:<%! ... %>,声明成员变量和方法
脚本:<% ... %>,写Java代码片段
表达式:<%= ... %>,输出表达式结果
注释:<%-- ... --%>,JSP注释(客户端看不到)
9大内置对象(直接可用):
request:HttpServletRequest
response:HttpServletResponse
session:HttpSession
application:ServletContext(应用上下文)
out:JspWriter(输出流)
pageContext:页面上下文
config:ServletConfig
page:当前Servlet实例
exception:异常对象(仅错误页面可用)
坑点:
避免在JSP中写大量Java代码(难以维护)
JSP最终会被编译成Servlet,所以JSP里的变量其实是_jspService()方法的局部变量
3.6 JSTL+EL:无脚本JSP开发
EL表达式:${对象.属性},自动调用getter方法。支持算术运算、逻辑运算。
JSTL核心标签:
<%-- 条件判断 --%>
欢迎,${user.name}
<%-- 多条件选择 --%>
<%-- 循环 --%>
第${status.count}个:${item.name}
<%-- 格式化日期 --%>
3.7 Filter:拦截与控制
配置方式:
web.xml配置:
注解配置(Servlet 3.0+):
@WebFilter("/*")
public class AuthFilter implements Filter { ... }
常见应用场景:
权限控制:检查Session中是否有登录用户,没有则跳转到登录页
编码设置:统一设置请求和响应的编码
日志记录:记录请求的IP、URL、耗时
敏感词过滤:替换响应内容中的敏感词
压缩响应:对响应内容进行GZIP压缩
执行顺序:按filter-mapping的配置顺序(或Filter类名的字母顺序)执行。
3.8 Listener:监听与回调
监听器类型:
ServletContextListener:监听应用启动/关闭
HttpSessionListener:监听会话创建/销毁(可用于统计在线人数)
ServletRequestListener:监听请求到达/结束
各种属性监听器:ServletContextAttributeListener、HttpSessionAttributeListener、ServletRequestAttributeListener
在线人数统计示例:
@WebListener
public class OnlineUserListener implements HttpSessionListener {
private static int onlineCount = 0;
public void sessionCreated(HttpSessionEvent se) {
onlineCount++;
System.out.println("新用户上线,当前在线:" + onlineCount);
}
public void sessionDestroyed(HttpSessionEvent se) {
onlineCount--;
System.out.println("用户下线,当前在线:" + onlineCount);
}
}
3.9 Cookie:客户端存储
重要属性:
setMaxAge(int expiry):有效期(秒),负数为浏览器关闭即失效,0为删除Cookie
setPath(String uri):指定哪些路径携带此Cookie,默认当前路径及其子路径
setDomain(String domain):指定哪些域名携带此Cookie
setHttpOnly(boolean httpOnly):设为true可防止XSS攻击窃取Cookie
setSecure(boolean secure):设为true只会在HTTPS下发送
坑点:
不同浏览器对Cookie数量和大小有限制(一般50个左右,4KB左右)
Cookie中不能存中文(需要URL编码)
敏感信息不要存Cookie
3.10 Session:服务端状态管理
核心方法:
setAttribute(String name, Object value):存数据
getAttribute(String name):取数据
removeAttribute(String name):移除数据
invalidate():销毁Session
setMaxInactiveInterval(int interval):设置超时时间(秒)
getId():获取Session ID
Session实现机制:
默认通过Cookie实现:Cookie中存JSESSIONID=xxxx,服务器根据这个ID找对应的Session。
如果用户禁用了Cookie,可以通过URL重写:
String newURL = response.encodeRedirectURL("index.jsp");
response.sendRedirect(newURL);
这样如果Cookie不可用,Session ID会附加在URL后面:index.jsp;jsessionid=xxxx
分布式Session问题:
在多台服务器部署时,用户请求可能落到不同服务器,需要解决Session共享问题。解决方案:
粘性Session:让同一用户的请求始终落到同一台服务器(但服务器宕机就丢了)
Session复制:在集群间同步Session(网络开销大)
集中式存储:用Redis统一存储Session(最常用方案)
3.11(使用芋道举例:)芋道请求处理的全链路流程图
第一阶段:容器启动与监听(Listener)
在请求还没进来之前,Listener 已经完成了舞台的搭建。
关键类:JdbcContextHolder 或自定义的 ConfigListener。
源码路径:通常在 yudao-module-system 的初始化逻辑中。
动作:
项目启动,ServletContextListener 触发。
芋道会加载数据库中的字典数据(Dict Data)和系统参数到 Redis 缓存。
结果:此时服务器已准备就绪,等待第一个 HTTP 请求。
第二阶段:安全与协议过滤(Filter)
请求到达 Tomcat 后,首先进入 Filter 链。这是 Spring Security 的主战场。
CacheRequestBodyFilter(最先执行):
动作:发现这是一个 POST 请求,它把 request.getInputStream() 读出来存进字节数组,包装成 CacheRequestBodyWrapper。
目的:为了后面拦截器记日志时,还能读到 Body 数据。
TokenAuthenticationFilter:
源码路径:yudao-spring-boot-starter-security
动作:从 Header 提取 Authorization,去 Redis 查这个 Token 对应的 LoginUser。
结果:将用户信息塞入 SecurityContextHolder。
第三阶段:进入 Spring MVC 核心(Servlet)
经过所有过滤器后,请求终于传给了 DispatcherServlet。它像个接线员,开始寻找该由哪个 Controller 处理。
第四阶段:业务逻辑拦截(Interceptor)
在进入 Controller 之前,Interceptor 开始细化处理。
TenantContextWebInterceptor(多租户拦截):
动作:读取 Header 里的 tenant-id。
逻辑:执行 TenantContextHolder.setTenantId(tenantId)。
影响:这行代码非常关键!它利用 ThreadLocal 保证了当前线程后续所有的数据库操作(MyBatis-Plus)都会自动带上这个租户 ID。
ApiAccessLogInterceptor(前置逻辑):
动作:记录下请求开始的时间戳。
第五阶段:业务执行(Controller)
终于到了你写的业务代码,比如 @GetMapping("/get-info")。
此时,你可以直接调用 SecurityFrameworkUtils.getLoginUserId() 获取当前用户。
执行 SQL 时,多租户插件已静默生效。
第六阶段:收尾与日志(Interceptor & Filter 后置)
ApiAccessLogInterceptor(afterCompletion):
动作:计算 结束时间 - 开始时间。
逻辑:从之前的 CacheRequestBodyFilter 留下的包装类里取出 Body,连同执行结果一起,异步写入数据库 system_api_access_log 表。
Listener:
如果是请求结束,ServletRequestListener 会清理 ThreadLocal,防止内存泄漏。
⚖️ 核心差异总结表(实战视角)
特性
Filter
Interceptor
Listener
在芋道里改谁?
认证、XSS、跨域、请求体缓存
租户切换、操作日志、权限细化
缓存预热、系统配置加载
能拿到 Body 吗?
能(通过 Wrapper 改造)
能(前提是 Filter 先包装了)
基本不拿
异常处理
需手动写 JSON 返回
可以交给全局异常处理器
无法感知业务异常
依赖注入
需要通过 FilterRegistrationBean
直接使用 @Autowired
直接使用 @Autowired
阶段四:场景融合——各技术如何协同作战
理解了单个技术的用法,我们来看它们在真实业务场景中如何配合。
场景一:用户登录与权限控制(全流程)
这是一个最经典的场景,几乎串联了所有Java Web核心技术。
流程图示:
1. 用户访问首页 → Filter拦截检查登录 → 未登录 → 重定向到登录页
2. 用户提交登录表单 → LoginServlet接收请求 → 调用业务层验证 → 验证成功 → 创建Session → 跳转首页
3. 首页加载 → JSP从Session取用户信息 → 动态显示欢迎语
4. 用户注销 → LogoutServlet → 销毁Session → 清理Cookie → 跳转登录页
完整实现:
1. 登录页面 login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
用户登录
<%-- 显示错误信息 --%>
${error}
2. 登录Servlet LoginServlet.java
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private UserService userService = new UserService(); // 业务层
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取参数
String username = request.getParameter("username");
String password = request.getParameter("password");
String remember = request.getParameter("remember");
// 调用业务层验证
User user = userService.login(username, password);
if (user != null) {
// 登录成功,创建Session
HttpSession session = request.getSession();
session.setAttribute("user", user);
session.setMaxInactiveInterval(30 * 60); // 30分钟
// 如果勾选了"记住我",设置Cookie
if ("on".equals(remember)) {
Cookie cookie = new Cookie("username", username);
cookie.setMaxAge(7 * 24 * 60 * 60); // 一周
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
// 重定向到首页
response.sendRedirect(request.getContextPath() + "/index");
} else {
// 登录失败,返回错误信息
request.setAttribute("error", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
}
3. 权限控制过滤器 AuthFilter.java
@WebFilter("/*")
public class AuthFilter implements Filter {
// 不需要登录就能访问的路径
private static final List
"/login", "/login.jsp", "/register", "/register.jsp", "/css", "/js"
);
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String path = request.getServletPath();
// 放行静态资源和登录相关路径
if (isAllowedPath(path)) {
chain.doFilter(request, response);
return;
}
// 检查是否登录
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("user") != null) {
// 已登录,放行
chain.doFilter(request, response);
} else {
// 未登录,重定向到登录页
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}
private boolean isAllowedPath(String path) {
for (String allowed : ALLOWED_PATHS) {
if (path.startsWith(allowed)) {
return true;
}
}
return false;
}
}
4. 首页 index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
欢迎,${sessionScope.user.name}!
你的角色:${sessionScope.user.role}
功能列表
5. 注销Servlet LogoutServlet.java
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 销毁Session
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
// 清理记住我的Cookie
Cookie cookie = new Cookie("username", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
// 跳转登录页
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}
场景二:购物车功能(Session的应用)
购物车是Session的典型应用场景——每个用户有自己的购物车,数据保存在服务器端。
@WebServlet("/cart")
public class CartServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String action = request.getParameter("action");
HttpSession session = request.getSession();
// 从Session获取购物车,如果没有则创建
Cart cart = (Cart) session.getAttribute("cart");
if (cart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
if ("add".equals(action)) {
// 添加商品
String productId = request.getParameter("productId");
int quantity = Integer.parseInt(request.getParameter("quantity"));
cart.addItem(productId, quantity);
// 返回购物车总件数(用于页面右上角更新)
response.setContentType("application/json");
response.getWriter().write("{\"totalCount\":" + cart.getTotalCount() + "}");
} else if ("list".equals(action)) {
// 查看购物车
request.setAttribute("cart", cart);
request.getRequestDispatcher("/cart.jsp").forward(request, response);
}
}
}
场景三:应用启动初始化(Listener的应用)
很多应用需要在启动时加载配置、初始化线程池、预热缓存等,这时就用到了ServletContextListener。
@WebListener
public class AppInitListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// 1. 加载配置文件
String configPath = context.getInitParameter("configLocation");
Properties props = loadConfig(configPath);
// 2. 初始化数据库连接池
DataSource dataSource = createDataSource(props);
context.setAttribute("dataSource", dataSource);
// 3. 预热缓存(加载热门数据)
CacheManager.init(dataSource);
// 4. 启动定时任务
ScheduleManager.start();
context.log("应用启动完成");
}
public void contextDestroyed(ServletContextEvent sce) {
// 释放资源
CacheManager.shutdown();
ScheduleManager.stop();
sce.getServletContext().log("应用关闭,资源已释放");
}
}
阶段五:企业级实战——构建完整项目
理论知识学完了,我们动手做一个小型但完整的项目,模拟企业开发的全流程。
项目:员工信息管理系统
功能需求:
员工信息列表展示(分页)
添加新员工
编辑员工信息
删除员工
部门管理
用户登录/权限控制
技术栈:
Servlet 4.0(处理请求)
JSP + JSTL + EL(展示页面)
Filter(权限控制、编码处理)
Listener(加载初始化数据)
Cookie(记住我功能)
Session(用户登录状态)
JDBC(数据库操作)
MySQL(数据库)
Tomcat 9(服务器)
5.1 项目结构
src/
├── main/
│ ├── java/
│ │ └── com/example/ems/
│ │ ├── controller/ # Servlet(控制器)
│ │ │ ├── EmployeeServlet.java
│ │ │ ├── DepartmentServlet.java
│ │ │ ├── LoginServlet.java
│ │ │ └── LogoutServlet.java
│ │ ├── service/ # 业务逻辑层
│ │ │ ├── EmployeeService.java
│ │ │ └── DepartmentService.java
│ │ ├── dao/ # 数据访问层
│ │ │ ├── EmployeeDAO.java
│ │ │ └── DepartmentDAO.java
│ │ ├── entity/ # 实体类
│ │ │ ├── Employee.java
│ │ │ └── Department.java
│ │ ├── filter/ # 过滤器
│ │ │ ├── AuthFilter.java
│ │ │ └── EncodingFilter.java
│ │ ├── listener/ # 监听器
│ │ │ └── AppInitListener.java
│ │ ├── util/ # 工具类
│ │ │ ├── DBUtil.java
│ │ │ └── PageUtil.java
│ │ └── config/ # 配置
│ │ └── Constants.java
│ └── webapp/
│ ├── WEB-INF/
│ │ └── web.xml
│ ├── view/ # JSP页面
│ │ ├── employee/
│ │ │ ├── list.jsp
│ │ │ ├── add.jsp
│ │ │ └── edit.jsp
│ │ ├── department/
│ │ │ └── list.jsp
│ │ ├── login.jsp
│ │ └── index.jsp
│ └── static/ # 静态资源
│ ├── css/
│ └── js/
5.2 核心代码实现
实体类 Employee.java
public class Employee {
private int id;
private String name;
private String gender;
private int age;
private String position;
private int deptId;
private Date hireDate;
private Department department; // 关联部门对象
// getter/setter 省略
}
数据访问层 EmployeeDAO.java
public class EmployeeDAO {
private DataSource dataSource;
public EmployeeDAO() {
this.dataSource = DBUtil.getDataSource();
}
// 分页查询员工(含部门名称)
public List
String sql = "SELECT e.*, d.name as dept_name FROM employee e " +
"LEFT JOIN department d ON e.dept_id = d.id " +
"LIMIT ?, ?";
int offset = (pageNum - 1) * pageSize;
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, offset);
pstmt.setInt(2, pageSize);
ResultSet rs = pstmt.executeQuery();
List
while (rs.next()) {
Employee emp = new Employee();
emp.setId(rs.getInt("id"));
emp.setName(rs.getString("name"));
emp.setGender(rs.getString("gender"));
emp.setAge(rs.getInt("age"));
emp.setPosition(rs.getString("position"));
emp.setDeptId(rs.getInt("dept_id"));
emp.setHireDate(rs.getDate("hire_date"));
// 设置关联部门信息
Department dept = new Department();
dept.setId(rs.getInt("dept_id"));
dept.setName(rs.getString("dept_name"));
emp.setDepartment(dept);
list.add(emp);
}
return list;
} catch (SQLException e) {
throw new RuntimeException("查询员工失败", e);
}
}
// 获取总记录数(用于分页)
public int countEmployees() {
String sql = "SELECT COUNT(*) FROM employee";
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
rs.next();
return rs.getInt(1);
} catch (SQLException e) {
throw new RuntimeException("统计员工失败", e);
}
}
// 新增员工
public void insertEmployee(Employee emp) {
String sql = "INSERT INTO employee(name, gender, age, position, dept_id, hire_date) " +
"VALUES(?, ?, ?, ?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, emp.getName());
pstmt.setString(2, emp.getGender());
pstmt.setInt(3, emp.getAge());
pstmt.setString(4, emp.getPosition());
pstmt.setInt(5, emp.getDeptId());
pstmt.setDate(6, new java.sql.Date(emp.getHireDate().getTime()));
pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("新增员工失败", e);
}
}
// 更新员工
public void updateEmployee(Employee emp) {
String sql = "UPDATE employee SET name=?, gender=?, age=?, position=?, dept_id=?, hire_date=? " +
"WHERE id=?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, emp.getName());
pstmt.setString(2, emp.getGender());
pstmt.setInt(3, emp.getAge());
pstmt.setString(4, emp.getPosition());
pstmt.setInt(5, emp.getDeptId());
pstmt.setDate(6, new java.sql.Date(emp.getHireDate().getTime()));
pstmt.setInt(7, emp.getId());
pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("更新员工失败", e);
}
}
// 删除员工
public void deleteEmployee(int id) {
String sql = "DELETE FROM employee WHERE id=?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("删除员工失败", e);
}
}
}
业务层 EmployeeService.java
public class EmployeeService {
private EmployeeDAO employeeDAO = new EmployeeDAO();
public PageResult
// 校验参数
if (pageNum < 1) pageNum = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100; // 防止页大小过大
// 查询数据
List
int total = employeeDAO.countEmployees();
// 计算总页数
int totalPages = (int) Math.ceil((double) total / pageSize);
return new PageResult<>(list, pageNum, pageSize, total, totalPages);
}
public void addEmployee(Employee emp) {
// 业务校验
if (emp.getName() == null || emp.getName().trim().isEmpty()) {
throw new BusinessException("员工姓名不能为空");
}
if (emp.getAge() < 18 || emp.getAge() > 65) {
throw new BusinessException("年龄必须在18-65岁之间");
}
employeeDAO.insertEmployee(emp);
}
// 其他业务方法...
}
控制器 EmployeeServlet.java
@WebServlet("/employee/*")
public class EmployeeServlet extends HttpServlet {
private EmployeeService employeeService = new EmployeeService();
private DepartmentService departmentService = new DepartmentService();
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String pathInfo = request.getPathInfo();
if (pathInfo == null || "/list".equals(pathInfo)) {
list(request, response);
} else if ("/add".equals(pathInfo)) {
showAddForm(request, response);
} else if ("/edit".equals(pathInfo)) {
showEditForm(request, response);
} else if ("/delete".equals(pathInfo)) {
delete(request, response);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String pathInfo = request.getPathInfo();
if ("/add".equals(pathInfo)) {
add(request, response);
} else if ("/update".equals(pathInfo)) {
update(request, response);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
// 员工列表(带分页)
private void list(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取页码参数
int pageNum = 1;
String pageParam = request.getParameter("page");
if (pageParam != null && !pageParam.isEmpty()) {
pageNum = Integer.parseInt(pageParam);
}
int pageSize = 10; // 可配置化
// 调用业务层
PageResult
// 存入request
request.setAttribute("pageResult", pageResult);
// 转发到JSP
request.getRequestDispatcher("/WEB-INF/view/employee/list.jsp")
.forward(request, response);
}
// 新增员工表单
private void showAddForm(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取部门列表(用于下拉框)
List
request.setAttribute("departments", depts);
request.getRequestDispatcher("/WEB-INF/view/employee/add.jsp")
.forward(request, response);
}
// 新增员工提交
private void add(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
try {
// 参数封装
Employee emp = new Employee();
emp.setName(request.getParameter("name"));
emp.setGender(request.getParameter("gender"));
emp.setAge(Integer.parseInt(request.getParameter("age")));
emp.setPosition(request.getParameter("position"));
emp.setDeptId(Integer.parseInt(request.getParameter("deptId")));
String hireDateStr = request.getParameter("hireDate");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
emp.setHireDate(sdf.parse(hireDateStr));
// 调用业务层
employeeService.addEmployee(emp);
// 成功,重定向到列表页(带成功消息)
response.sendRedirect(request.getContextPath() + "/employee/list?success=添加成功");
} catch (Exception e) {
// 失败,返回表单并显示错误
request.setAttribute("error", e.getMessage());
request.setAttribute("departments", departmentService.getAllDepartments());
request.getRequestDispatcher("/WEB-INF/view/employee/add.jsp")
.forward(request, response);
}
}
// 其他方法...
}
列表页面 list.jsp(JSTL+EL)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
员工列表
<%-- 显示操作消息 --%>
| ID | 姓名 | 性别 | 年龄 | 职位 | 部门 | 入职日期 | 操作 |
|---|---|---|---|---|---|---|---|
| ${emp.id} | ${emp.name} | ${emp.gender} | ${emp.age} | ${emp.position} | ${emp.department.name} |
编辑 |
|
| 暂无数据 | |||||||
<%-- 分页组件 --%>
5.3 企业开发规范落地
在这个项目中,我们遵循了以下企业开发规范:
分层架构:Controller(Servlet)→ Service(业务逻辑)→ DAO(数据访问)→ Entity(实体),各层职责清晰
命名规范:
类名:大驼峰,如EmployeeService
方法名:小驼峰,如getEmployeesByPage
常量:全大写下划线,如MAX_PAGE_SIZE
异常处理:自定义BusinessException,在Controller层统一处理并展示给用户
日志记录:使用ServletContext.log()或集成Log4j记录关键操作
配置分离:数据库连接、分页大小等配置在Constants.java或配置文件中
参数校验:在Service层进行业务校验,避免脏数据进入数据库
防止SQL注入:使用PreparedStatement代替Statement
5.4 测试与部署
单元测试(JUnit):
public class EmployeeServiceTest {
private EmployeeService employeeService;
@Before
public void setUp() {
employeeService = new EmployeeService();
}
@Test
public void testAddEmployee_ValidData() {
Employee emp = new Employee();
emp.setName("张三");
emp.setAge(25);
emp.setGender("男");
emp.setPosition("开发");
emp.setDeptId(1);
emp.setHireDate(new Date());
// 应正常执行,不抛出异常
employeeService.addEmployee(emp);
}
@Test(expected = BusinessException.class)
public void testAddEmployee_InvalidAge() {
Employee emp = new Employee();
emp.setName("李四");
emp.setAge(16); // 年龄太小,应抛出异常
// 其他属性...
employeeService.addEmployee(emp);
}
}
部署步骤:
Maven打包:mvn clean package,生成ems.war
将war包复制到Tomcat的webapps目录
启动Tomcat:bin/startup.sh(Linux)或bin/startup.bat(Windows)
访问:http://localhost:8080/ems/
阶段六:复盘升华——从会用到用好
6.1 最佳实践总结
经过前面的学习和实战,我们来提炼一些企业开发中经过验证的最佳实践。
Servlet最佳实践
一个Servlet只处理一类业务:不要写万能Servlet,按模块拆分(如EmployeeServlet、DepartmentServlet)
使用路径映射简化URL:@WebServlet("/employee/*") + 路径分发
避免在Servlet中写业务逻辑:Servlet只做参数解析、调用Service、页面跳转
JSP最佳实践
杜绝Java脚本(<% ... %>):全部用EL+JSTL替代,页面更干净
使用JSP包含机制复用页面:<%@ include file="header.jsp" %>或
JSP放在WEB-INF下:防止直接访问,只能通过Servlet转发
Filter最佳实践
Filter顺序要合理:编码Filter在前,权限Filter在后
不要过度使用Filter:影响性能,只做横切关注点的事情
使用@WebFilter的urlPatterns精确控制拦截范围
Session最佳实践
Session中只存必要数据:不要存大对象,影响内存和序列化性能
设置合理的超时时间:web.xml中配置
敏感信息加密存储:用户角色、权限等敏感信息可以考虑加密
登录后重置Session ID:防止Session Fixation攻击
集群环境使用集中式存储:Redis统一管理Session
Cookie最佳实践
设置HttpOnly和Secure:防止XSS窃取和HTTP明文传输
不要存敏感信息:密码、令牌等不能存Cookie
设置合理的Path:避免Cookie被无关路径携带
敏感Cookie设置较短有效期
6.2 技术演进路线
Java Web技术一直在演进,了解发展脉络有助于理解为什么有些技术现在用得少了:
1. 视图层演进
JSP(早期)→ JSP+JSTL(中期)→ FreeMarker/Velocity → Thymeleaf(现代)
JSP现在逐渐被模板引擎取代,因为前后端分离趋势下,后端更专注于提供API。
2. 控制层演进
Servlet(原始)→ Struts2 → Spring MVC(现代)
Spring MVC本质上还是Servlet,但封装得极其优雅,成为事实标准。
3. 数据层演进
JDBC(原始)→ MyBatis → JPA/Hibernate → MyBatis-Plus/Spring Data JPA
现代开发很少直接写JDBC,都用ORM框架简化数据库操作。
4. 构建方式演进
手动部署 → Ant → Maven → Gradle
现代项目几乎都用Maven或Gradle管理依赖和构建。
5. 架构演进
单体应用 → 垂直拆分 → 分布式服务 → 微服务 → Service Mesh
Java Web从单一的JSP+Servlet,发展到现在复杂的微服务架构。
6.3 高频面试题
Q1:Servlet是单例还是多例?线程安全吗?
A:Servlet默认单例多线程。容器只创建一个Servlet实例,每个请求由不同线程执行。因此成员变量需要考虑线程安全,尽量不定义可修改的成员变量。
Q2:Cookie和Session的区别?
A:
Cookie存在客户端,Session存在服务端
Cookie容量小(4KB),Session容量理论上无限
Cookie相对不安全(可被篡改),Session安全
典型应用:记住我功能用Cookie,登录状态用Session
Q3:请求转发(forward)和重定向(sendRedirect)的区别?
A:
转发是服务器内部跳转,URL不变,可以携带request范围内的数据
重定向是浏览器重新发起请求,URL改变,无法直接携带request数据
转发只能跳转到站内资源,重定向可以站外
Q4:Filter和Interceptor有什么区别?
A:Filter是Servlet规范的一部分,基于回调函数,只能在Servlet前后起作用;Interceptor是Spring MVC的概念,基于反射,更细粒度,可以深入到方法调用前后。
Q5:Session共享问题怎么解决?
A:
粘性Session:负载均衡配置同一用户始终访问同一服务器
Session复制:Tomcat自带集群会话复制功能
集中式存储:Redis统一存储Session(最推荐)
客户端存储:JWT等令牌机制,服务端无状态
Q6:JSP的9大内置对象有哪些?
A: request、response、session、application、out、pageContext、config、page、exception
Q7:如果客户端禁用了Cookie,Session还能用吗?
A:能,但需要URL重写。通过response.encodeURL()在URL后面附加jsessionid=xxx。
Q8:如何防止Session固定攻击?
A: 用户登录成功后,调用session.invalidate()销毁旧Session,然后创建新Session。
写在最后
Java Web开发涉及的知识点看起来很多,但它们并不是孤立的。Servlet是地基,JSP是展示,Filter是拦截,Listener是监听,Session是状态,Cookie是标识——它们组合在一起,才构成了一个完整的Web应用。
希望这篇长达两万字的指南,能帮你建立起Java Web的知识体系。从「问题锚定」开始,到「基础认知」理解概念,到「核心用法」掌握操作,到「场景融合」看到配合,到「实战落地」完整开发,再到「复盘升华」总结经验——这正是企业开发中需要具备的思维方式。
如果你读完这篇文章,能动手把那个员工管理系统做出来,那么恭喜你,你已经具备了Java Web开发的基础能力。接下来就是不断练习、不断踩坑、不断总结的过程。
技术在变,但解决问题的本质不变。理解每个技术「为什么存在」,比记住「怎么用」更重要。
祝你在Java Web的世界里玩得开心!