JavaEE 从零到生产级掌握
JavaEE 不能只学成“会写 Servlet”。即使现在大多数项目都用 Spring Boot,底层也绕不开 Servlet 容器、Filter、Listener、Session/Cookie、JDBC、连接池和事务。Spring MVC 的入口是 DispatcherServlet,Spring Security 的核心是过滤器链,数据库访问最终也要落到连接、SQL、事务和资源释放。
一句话建立主线:
JavaEE 是 Java Web 应用的底层规范体系,负责让 Java 程序接收 HTTP 请求、维护会话、访问数据库,并在 Web 容器中运行。
学习目标
学完这一页,你要能做到:
- 解释浏览器请求如何进入 Tomcat,再进入 Servlet 或 Spring MVC。
- 解释 Servlet 生命周期、单例多线程模型和线程安全问题。
- 解释 Request、Response、ServletContext、HttpSession 的作用。
- 解释 Filter 为什么能做登录、鉴权、跨域、日志、编码和限流。
- 解释 Listener 能监听哪些生命周期,适合做什么,不适合做什么。
- 解释 Cookie 和 Session 如何配合维持登录态,多实例下为什么会丢 Session。
- 解释 JDBC 执行 SQL 的完整流程,为什么必须关闭资源。
- 解释连接池为什么必要,连接泄漏、连接耗尽怎么排查。
- 解释事务的提交、回滚、隔离级别和自动提交。
- 解释 JavaEE 和 Spring MVC、Spring Boot、Spring Security 的关系。
学习路线
flowchart TD
A["HTTP 基础"] --> B["Web 容器<br/>Tomcat/Jetty/Undertow"]
B --> C["Servlet<br/>请求响应入口"]
C --> D["Filter<br/>请求前后拦截"]
D --> E["Session/Cookie<br/>登录状态"]
E --> F["Listener<br/>生命周期监听"]
F --> G["JDBC<br/>访问数据库"]
G --> H["连接池和事务"]
H --> I["Spring MVC/Spring Boot 关系"]
I --> J["生产排查"]这条路线的关键是:先理解一次请求从浏览器到 Java 代码的链路,再理解统一拦截、会话状态和数据库访问。
如果你希望把这些知识放进真实项目里理解,可以配合学习:JavaEE 商业场景训练营。它用登录过滤、Session 多实例、JDBC 事务、连接池耗尽和接口排查,把规范知识转成项目能力。
第一步:HTTP 请求如何进入 Java 应用
用户在浏览器访问:
GET http://example.com/assets?id=1001服务端不是直接进入 Controller。真实链路是:
flowchart TD
A["浏览器发送 HTTP 请求"] --> B["操作系统接收 TCP 连接"]
B --> C["Tomcat Connector 读取请求"]
C --> D["解析 HTTP 请求行、请求头、请求体"]
D --> E["创建 Request 和 Response"]
E --> F["匹配 Web 应用和 Servlet"]
F --> G["执行 Filter 链"]
G --> H["调用 Servlet service 方法"]
H --> I["业务代码写 Response"]
I --> J["Tomcat 返回 HTTP 响应"]Spring Boot 项目里也是这条链,只是 Servlet 是 Spring MVC 的 DispatcherServlet。
为什么要理解这条链:
| 线上问题 | 可能位置 |
|---|---|
| 请求 404 | Web 应用路径、Servlet 映射、Controller 路由 |
| 登录失效 | Cookie、Session、Filter、Spring Security |
| 跨域失败 | CORS Filter、响应头 |
| 中文乱码 | 请求编码、响应编码、数据库字符集 |
| 接口超时 | Tomcat 线程、业务线程、数据库连接、外部接口 |
第二步:Web 容器做什么
Tomcat、Jetty、Undertow 都属于 Web 容器或 Servlet 容器。它们负责:
| 能力 | 说明 |
|---|---|
| 监听端口 | 接收 TCP/HTTP 请求 |
| 解析协议 | 把 HTTP 字节流解析成请求对象 |
| 管理 Servlet | 创建、初始化、调用、销毁 |
| 管理 Filter | 按顺序执行过滤器链 |
| 管理 Session | 保存会话状态和过期时间 |
| 线程模型 | 使用线程处理请求 |
| 返回响应 | 把 Response 写回客户端 |
简化模型:
flowchart TD
A["Connector<br/>连接和协议"] --> B["Container<br/>Servlet 管理"]
B --> C["Filter Chain"]
C --> D["Servlet"]
D --> E["Response"]Tomcat 不是业务框架,它是运行 Java Web 应用的容器。Spring Boot 内嵌 Tomcat,只是把原来外部部署 WAR 的方式变成了应用自己启动容器。
第三步:Servlet 生命周期
Servlet 是 Java 处理 HTTP 请求的基础规范。
生命周期:
flowchart TD
A["容器加载 Servlet"] --> B["创建 Servlet 实例"]
B --> C["init 初始化一次"]
C --> D["service 处理多次请求"]
D --> E["按方法分发到 doGet/doPost"]
E --> F["destroy 销毁一次"]方法说明:
| 方法 | 调用时机 | 次数 |
|---|---|---|
init() | Servlet 创建后 | 一次 |
service() | 每次请求进入 | 多次 |
doGet() / doPost() | 根据 HTTP 方法分发 | 多次 |
destroy() | 应用卸载或容器关闭 | 一次 |
最小 Demo:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("hello servlet");
}
}Servlet 通常是单例多线程。容器不会为每个请求创建一个 Servlet 实例,而是多个线程共享一个实例。
flowchart TD
A["一个 Servlet 实例"] --> B["线程 1 处理用户 A"]
A --> C["线程 2 处理用户 B"]
A --> D["线程 3 处理用户 C"]错误示例:
public class UserServlet extends HttpServlet {
private String currentUser;
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
currentUser = req.getParameter("user");
}
}currentUser 是成员变量,会被多个请求共享,可能出现用户串号。请求级数据应放局部变量、request 或 session。
第四步:Request 和 Response
HttpServletRequest 表示请求,常用来读取:
| 内容 | API |
|---|---|
| 请求参数 | getParameter |
| 请求头 | getHeader |
| Cookie | getCookies |
| Session | getSession |
| 请求路径 | getRequestURI |
| 请求体 | getInputStream / getReader |
HttpServletResponse 表示响应,常用来设置:
| 内容 | API |
|---|---|
| 状态码 | setStatus |
| 响应头 | setHeader |
| Cookie | addCookie |
| 响应类型 | setContentType |
| 响应体 | getWriter / getOutputStream |
示例:
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String id = req.getParameter("id");
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"id\":\"" + id + "\"}");
}生产项目不要手动拼 JSON,Spring MVC 会用 Jackson 等序列化工具处理。但理解 Request/Response 有助于排查 Header、Cookie、编码和响应已提交问题。
第五步:forward 和 redirect
| 对比 | forward | redirect |
|---|---|---|
| 谁发起 | 服务端内部转发 | 服务端通知浏览器重新请求 |
| 请求次数 | 一次 | 两次 |
| 地址栏 | 不变 | 改变 |
| request 数据 | 可共享 | 不共享 |
| 适合 | 内部资源转发 | 登录后跳转、跨站跳转 |
forward:
request.getRequestDispatcher("/WEB-INF/page.jsp")
.forward(request, response);redirect:
response.sendRedirect("/login");如果已经写出响应体,再 forward 可能报错,因为响应已经提交,状态码和响应头可能不能再改。
第六步:Filter 原理
Filter 是 Servlet 规范里的请求过滤器。它能在请求进入 Servlet 前、响应返回客户端前执行统一逻辑。
flowchart TD
A["请求进入"] --> B["Filter 1 前置"]
B --> C["Filter 2 前置"]
C --> D["Servlet/Controller"]
D --> E["Filter 2 后置"]
E --> F["Filter 1 后置"]
F --> G["响应返回"]最小 Demo:登录校验。
@WebFilter("/api/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
HttpSession session = req.getSession(false);
if (session == null || session.getAttribute("userId") == null) {
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.getWriter().write("not login");
return;
}
chain.doFilter(request, response);
}
}chain.doFilter 是关键:
- 调用它,请求继续往后走。
- 不调用它,请求被拦截。
- 调用前是前置逻辑。
- 调用后是响应返回阶段逻辑。
Filter 常见用途:
| 用途 | 为什么适合 Filter |
|---|---|
| 登录鉴权 | 请求进入业务前判断 |
| CORS | 设置跨域响应头 |
| 编码 | 统一设置 UTF-8 |
| 日志 traceId | 请求前生成,请求后记录耗时 |
| 限流 | 在业务前拒绝过量请求 |
| XSS/安全头 | 统一安全处理 |
Filter 和 Spring MVC Interceptor 区别:
| 对比 | Filter | Interceptor |
|---|---|---|
| 来源 | Servlet 规范 | Spring MVC |
| 位置 | Servlet 前后 | Controller 前后 |
| 能否拦静态资源 | 可以 | 通常依赖 MVC 映射 |
| 是否知道 Handler | 不知道 | 知道 Controller 方法 |
| 典型用途 | 编码、跨域、安全入口 | 业务权限、Controller 日志 |
第七步:Listener 原理
Listener 用于监听 Web 应用、请求、Session 生命周期。
常见 Listener:
| 类型 | 监听什么 | 场景 |
|---|---|---|
ServletContextListener | 应用启动和关闭 | 初始化资源、关闭资源 |
HttpSessionListener | Session 创建和销毁 | 在线人数、会话清理 |
ServletRequestListener | 请求创建和销毁 | 请求级监控 |
HttpSessionAttributeListener | Session 属性变化 | 登录状态变化 |
示例:应用启动监听。
@WebListener
public class AppListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("web app started");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("web app stopped");
}
}Listener 适合做生命周期相关工作,不适合写复杂业务逻辑。复杂业务放 Service,避免启动和销毁阶段出问题影响应用可用性。
第八步:Cookie 和 Session
HTTP 是无状态协议。服务端处理完一次请求后,不天然记得下次请求是谁。
Cookie 保存在浏览器,Session 保存在服务端。
典型登录流程:
flowchart TD
A["用户提交账号密码"] --> B["服务端校验成功"]
B --> C["创建 Session 保存 userId"]
C --> D["响应 Set-Cookie: JSESSIONID"]
D --> E["浏览器保存 Cookie"]
E --> F["后续请求携带 JSESSIONID"]
F --> G["服务端根据 ID 找 Session"]代码:
HttpSession session = request.getSession();
session.setAttribute("userId", user.getId());读取:
HttpSession session = request.getSession(false);
if (session == null) {
response.setStatus(401);
return;
}
Long userId = (Long) session.getAttribute("userId");多实例问题:
flowchart TD
A["浏览器带 JSESSIONID"] --> B["负载均衡"]
B --> C["实例 1 有 Session"]
B --> D["实例 2 没有 Session"]
D --> E["用户被认为未登录"]解决方案:
| 方案 | 说明 | 适用 |
|---|---|---|
| 粘性会话 | 同一用户固定打到同实例 | 简单但实例故障体验差 |
| Session 复制 | 实例间复制 Session | 集群小、状态不多 |
| 集中 Session | Redis/Spring Session | 常用 |
| 无状态 Token | JWT/自定义 Token | 前后端分离、网关鉴权 |
Cookie 安全属性:
| 属性 | 作用 |
|---|---|
| HttpOnly | JS 不能读取,降低 XSS 窃取风险 |
| Secure | 只在 HTTPS 发送 |
| SameSite | 降低 CSRF 风险 |
| Max-Age | 过期时间 |
| Path/Domain | 限定发送范围 |
第九步:JDBC 完整流程
JDBC 是 Java 访问数据库的标准 API。即使用 MyBatis、JPA,底层也离不开连接、SQL、事务和结果集。
完整流程:
flowchart TD
A["获取 Connection"] --> B["创建 PreparedStatement"]
B --> C["绑定参数"]
C --> D["执行 SQL"]
D --> E["处理 ResultSet"]
E --> F["提交或回滚事务"]
F --> G["关闭 ResultSet/Statement/Connection"]示例:
String sql = "select id, name from asset where id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, 1001L);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
System.out.println(id + ":" + name);
}
}
}为什么用 PreparedStatement:
- 参数和 SQL 结构分离,降低 SQL 注入风险。
- 数据库可复用执行计划。
- 参数类型处理更可靠。
错误示例:
String sql = "select * from user where name = '" + name + "'";如果 name 是恶意输入,就可能 SQL 注入。
第十步:连接池为什么必要
数据库连接不是普通对象,创建连接需要 TCP 握手、认证、初始化会话等成本。如果每次请求都新建连接,性能会很差。
连接池的作用:
flowchart TD
A["应用启动"] --> B["连接池创建一定数量连接"]
C["业务请求"] --> D["从连接池借连接"]
D --> E["执行 SQL"]
E --> F["归还连接"]
F --> G["连接继续复用"]连接池能:
| 能力 | 说明 |
|---|---|
| 复用连接 | 减少频繁创建销毁 |
| 控制并发 | 最大连接数保护数据库 |
| 等待队列 | 连接不足时等待 |
| 泄漏检测 | 连接长时间未归还报警 |
| 健康检查 | 剔除失效连接 |
连接泄漏是什么:
业务借了连接,但异常路径没有 close,连接一直不归还。后果:
- 活跃连接越来越多。
- 等待连接线程越来越多。
- 接口变慢或超时。
- 数据库连接数被打满。
排查:
| 现象 | 排查方向 |
|---|---|
| 连接池 active 接近 max | SQL 慢、连接泄漏、并发过高 |
| wait count 上升 | 连接不够或归还太慢 |
| 数据库连接数满 | 应用连接池配置过大或泄漏 |
| 只在某接口发生 | 看该接口 SQL 和事务边界 |
第十一步:事务
JDBC 默认可能是自动提交模式。每条 SQL 执行后自动提交。
手动事务:
Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
updateOrder(conn);
updateStock(conn);
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e;
} finally {
conn.close();
}事务解决的是一组操作要么都成功,要么都失败。
flowchart TD
A["关闭自动提交"] --> B["执行 SQL 1"]
B --> C["执行 SQL 2"]
C --> D{"是否全部成功"}
D -- "是" --> E["commit"]
D -- "否" --> F["rollback"]
E --> G["释放连接"]
F --> G事务和连接强相关:同一个事务里的 SQL 必须使用同一个数据库连接。Spring 事务底层也是把连接绑定到当前线程,再在方法结束时提交或回滚。
常见坑:
| 坑 | 后果 |
|---|---|
| 忘记 rollback | 异常后事务状态不清 |
| 忘记 close | 连接泄漏 |
| 自动提交没关 | 多条 SQL 无法整体回滚 |
| 事务太大 | 锁时间长,连接占用久 |
| catch 异常吞掉 | 上层以为成功 |
第十二步:JavaEE 和 Spring 生态的关系
| JavaEE 能力 | Spring 中的体现 |
|---|---|
| Servlet | DispatcherServlet |
| Filter | Spring Security Filter Chain、CORS Filter |
| Listener | Web 初始化、Spring 上下文监听 |
| Session | Spring Session、登录态 |
| JDBC | JdbcTemplate、MyBatis、JPA 底层 |
| 事务 | Spring PlatformTransactionManager |
| Web 容器 | Spring Boot 内嵌 Tomcat |
Spring Boot 启动 Web 应用时,会启动内嵌 Tomcat,注册 DispatcherServlet、Filter、Listener、数据源等组件。它降低配置成本,但不是绕开 JavaEE。
商业项目场景
登录鉴权
浏览器 Cookie -> Filter/Security 读取 -> 校验 Session/Token -> 放行 Controller关键点:
- Cookie 设置 HttpOnly、Secure、SameSite。
- 多实例用 Redis Session 或 Token。
- 鉴权失败在 Filter 阶段拦截,不进入业务。
医疗数据采集接口
医院系统请求 -> Tomcat -> Filter 记录 traceId -> Controller -> JDBC/ORM 写库关键点:
- 请求日志记录 traceId、医院编码、批次号。
- 统一设置超时和连接池。
- 失败要记录批次状态,便于补偿。
后台管理系统
前端请求 -> CORS Filter -> 登录 Filter -> Controller -> Service -> DB关键点:
- CORS 不要放得过宽。
- Session 过期返回统一错误码。
- JDBC/连接池监控要接入告警。
生产排查总流程
请求没有进入 Controller
flowchart TD
A["请求没有进入 Controller"] --> B["确认 URL 和端口"]
B --> C["确认 Tomcat 是否收到请求"]
C --> D["确认 ContextPath"]
D --> E["确认 Filter 是否拦截"]
E --> F["确认 DispatcherServlet/Servlet 映射"]
F --> G["确认 Controller 路由"]登录态丢失
flowchart TD
A["登录态丢失"] --> B["浏览器是否携带 Cookie"]
B --> C["Cookie Domain/Path/SameSite 是否正确"]
C --> D["服务端 Session 是否存在"]
D --> E["是否多实例本地 Session 不共享"]
E --> F["是否 Session 过期或被销毁"]数据库连接耗尽
flowchart TD
A["连接池耗尽"] --> B["看 active/max/wait 指标"]
B --> C["是否慢 SQL"]
C --> D["是否事务太长"]
D --> E["是否连接泄漏"]
E --> F["是否并发超过数据库容量"]
F --> G["优化 SQL、事务和连接池配置"]常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
| Servlet 成员变量保存用户数据 | 用户串号 | 用局部变量/request/session |
Filter 忘记 chain.doFilter | 请求被意外拦截 | 明确放行条件 |
| Filter 顺序错误 | 鉴权、跨域、日志异常 | 明确 order |
| Cookie SameSite 配错 | 跨站登录失败 | 按前后端部署方式配置 |
| 本地 Session 多实例部署 | 登录状态随机丢失 | Redis Session 或 Token |
| JDBC 不关闭资源 | 连接泄漏 | try-with-resources |
| SQL 字符串拼接参数 | SQL 注入 | PreparedStatement |
| 事务范围太大 | 锁时间长,连接占用久 | 缩小事务边界 |
面试标准回答
JavaEE 请求链路怎么回答
浏览器发起 HTTP 请求后,Tomcat 等 Web 容器接收 TCP 连接并解析 HTTP,请求会先进入 Filter 链,Filter 可以做编码、跨域、日志、鉴权等统一处理;放行后进入 Servlet。Spring MVC 中这个 Servlet 就是 DispatcherServlet,它再查找 Controller 并调用业务方法。业务中如果访问数据库,会通过 JDBC、连接池和事务完成 SQL 执行,最后写入 Response 返回浏览器。
Servlet 为什么不是线程安全的
Servlet 通常是单例多线程模型,一个 Servlet 实例会被多个请求线程同时调用。局部变量在线程栈里是安全的,但成员变量被所有请求共享,如果保存用户请求数据就可能串号。所以 Servlet 成员变量只能放无状态、线程安全或只读对象。
Filter 和 Interceptor 区别
Filter 是 Servlet 规范的一部分,作用在 Servlet 前后,位置更靠前,适合编码、跨域、登录入口、日志、限流等通用处理。Interceptor 是 Spring MVC 的组件,作用在 Controller 前后,能拿到 Handler 信息,适合更贴近业务的权限、日志和参数检查。
为什么要用连接池
数据库连接创建成本高,而且数据库能承受的连接数有限。连接池通过复用连接减少创建销毁成本,通过最大连接数限制并发保护数据库,并提供等待队列、健康检查和泄漏检测。线上连接池耗尽时,要看慢 SQL、长事务、连接泄漏和并发是否超过容量。
关联知识点
- JavaEE 入门
- Servlet
- Session 与 Cookie
- Filter 与 Listener
- JDBC
- JavaEE 面试题
- Spring MVC
- Spring Security
- Spring Boot 启动流程
- Spring 事务
本章小结
JavaEE 是现代 Java Web 的地基。你可以不在项目里手写大量 Servlet 和 JDBC,但必须理解请求如何进入容器、Filter 如何拦截、Session/Cookie 如何维持登录、JDBC/连接池/事务如何访问数据库。这样排查登录失效、请求没进 Controller、跨域失败、连接池耗尽、事务异常时,才不会只盯着 Controller 和 Service。
