JavaEE 商业场景训练营
JavaEE 现在很少作为“手写 Servlet 项目”出现,但它仍然是 Spring MVC、Spring Boot、Spring Security、JDBC、连接池和 Web 容器排查的底层地基。本页用商业系统里最常见的登录、鉴权、请求链路、数据库访问和连接池问题,把 JavaEE 知识串成可落地能力。
一句话主线:
JavaEE 训练的不是背规范,而是理解一次 HTTP 请求如何进入容器、如何被 Filter 统一处理、如何通过 Servlet/Spring MVC 执行业务、如何安全访问数据库并在生产排查中定位问题。
训练目标
学完本页,你要能做到:
- 画出浏览器请求到 Tomcat、Filter、Servlet、业务代码、JDBC、响应返回的完整链路。
- 写出登录过滤器 Demo,理解 Filter 为什么能做鉴权、日志、跨域和编码。
- 解释 Servlet 单例多线程为什么不能用成员变量保存请求数据。
- 解释 Cookie、Session、SessionId 的配合方式,以及多实例为什么会丢 Session。
- 写出 JDBC 事务 Demo,理解连接、自动提交、提交、回滚和资源释放。
- 排查 404、401、跨域、乱码、连接池耗尽、接口超时。
- 面试时能把 JavaEE 和 Spring Boot、Spring MVC、Spring Security 关系讲清楚。
商业场景一:后台系统登录过滤
后台管理系统常见要求:除登录接口外,其他接口必须登录才能访问。这个能力非常适合放在 Filter,因为 Filter 在请求进入 Servlet 或 Spring MVC Controller 之前执行。
flowchart TD
A["浏览器请求接口"] --> B["Tomcat 创建 Request/Response"]
B --> C["AuthFilter"]
C --> D{"是否登录或白名单"}
D -- "是" --> E["放行到 Servlet/DispatcherServlet"]
D -- "否" --> F["返回 401"]
E --> G["业务 Controller"]
G --> H["写 Response"]Demo:
@WebFilter("/*")
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String path = req.getRequestURI();
if (path.endsWith("/login")) {
chain.doFilter(request, response);
return;
}
HttpSession session = req.getSession(false);
if (session == null || session.getAttribute("userId") == null) {
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"message\":\"未登录\"}");
return;
}
chain.doFilter(request, response);
}
}为什么这样写:
| 写法 | 原因 |
|---|---|
@WebFilter("/*") | 拦截所有请求,由代码判断白名单 |
getSession(false) | 没有 Session 时不创建新 Session |
未登录直接 return | 不再继续进入业务代码 |
chain.doFilter | 明确放行到后续 Filter 或 Servlet |
如果忘记调用 chain.doFilter,请求会停在当前 Filter,Controller 永远不会执行。如果未登录后没有 return,可能先写 401 又继续放行业务,产生安全问题。
商业场景二:Session 登录态和多实例问题
HTTP 本身无状态。用户第一次登录成功后,服务端创建 Session,并通过 Cookie 把 SessionId 交给浏览器。后续请求浏览器带上 Cookie,服务端根据 SessionId 找到登录态。
flowchart TD
A["登录请求"] --> B["服务端校验账号密码"]
B --> C["创建 HttpSession"]
C --> D["写入 userId"]
D --> E["响应 Set-Cookie: JSESSIONID"]
F["后续请求"] --> G["浏览器携带 JSESSIONID"]
G --> H["服务端查找 Session"]
H --> I{"Session 是否存在"}
I -- "存在" --> J["认为已登录"]
I -- "不存在" --> K["返回未登录"]单机部署时 Session 保存在当前 JVM 内存里。多实例部署时会出现问题:
flowchart TD
A["用户登录"] --> B["实例 A 保存 Session"]
C["下一次请求"] --> D["负载均衡转到实例 B"]
D --> E["实例 B 找不到 Session"]
E --> F["用户被判定未登录"]解决方案:
| 方案 | 原理 | 适合场景 | 风险 |
|---|---|---|---|
| 粘性会话 | 同一用户固定打到同一实例 | 小规模系统 | 实例故障会丢会话 |
| 集中 Session | Session 存 Redis | 传统后台系统 | Redis 可用性和序列化 |
| JWT/Token | 服务端不保存 Session | 前后端分离、微服务 | Token 失效和权限变更要设计 |
| Spring Session | 封装集中 Session | Spring 项目 | 仍依赖外部存储 |
商业场景三:JDBC 事务和资源释放
JDBC 是 Java 访问数据库的基础。即使使用 MyBatis、JPA,底层也离不开连接、SQL、事务和结果集。
业务:创建数据资产,并写入审计日志。两个操作必须同时成功或同时失败。
flowchart TD
A["获取数据库连接"] --> B["关闭自动提交"]
B --> C["插入资产表"]
C --> D["插入审计表"]
D --> E{"是否全部成功"}
E -- "是" --> F["commit"]
E -- "否" --> G["rollback"]
F --> H["关闭 Statement/Connection"]
G --> HDemo:
public void createAsset(DataSource dataSource, String code, String name) throws SQLException {
String insertAsset = "insert into data_asset(asset_code, name) values(?, ?)";
String insertAudit = "insert into asset_audit(asset_code, action) values(?, ?)";
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement assetStmt = conn.prepareStatement(insertAsset);
PreparedStatement auditStmt = conn.prepareStatement(insertAudit)) {
assetStmt.setString(1, code);
assetStmt.setString(2, name);
assetStmt.executeUpdate();
auditStmt.setString(1, code);
auditStmt.setString(2, "CREATE");
auditStmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e;
}
}
}关键点:
| 点 | 为什么 |
|---|---|
setAutoCommit(false) | 多条 SQL 才能组成一个事务 |
PreparedStatement | 防 SQL 注入,也利于数据库复用执行计划 |
commit | 明确提交事务 |
rollback | 任一步失败撤销已经执行的 SQL |
try-with-resources | 自动关闭资源,把连接还给连接池 |
如果不关闭连接,连接不会回到连接池;请求量一上来,连接池会被耗尽,后续请求等待连接直到超时。
商业场景四:连接池耗尽排查
连接池耗尽常见表现:
- 接口大量超时。
- 应用日志出现等待连接超时。
- 数据库连接数接近上限。
- 线程堆栈卡在获取连接。
排查流程:
flowchart TD
A["接口开始超时"] --> B["看连接池 active/idle/waiting"]
B --> C{"active 是否接近 max"}
C -- "是" --> D["检查慢 SQL 和连接泄漏"]
C -- "否" --> E["检查外部接口、锁、Tomcat 线程"]
D --> F{"是否有连接未关闭"}
F -- "是" --> G["修复 finally 或 try-with-resources"]
F -- "否" --> H["优化慢 SQL、索引、事务范围"]常见原因:
| 原因 | 说明 | 处理 |
|---|---|---|
| 连接未关闭 | 代码异常路径没有释放连接 | try-with-resources |
| 慢 SQL | SQL 长时间占用连接 | 优化 SQL 和索引 |
| 事务过长 | 事务里调用外部接口或处理大文件 | 缩短事务范围 |
| 连接池太小 | 并发超过池容量 | 根据服务实例数和数据库上限估算 |
| 连接池太大 | 打爆数据库 | 控制总连接数 |
连接池不是越大越好。总连接数 = 应用实例数 * 每个实例最大连接数。比如 10 个实例,每个最大 100,就是 1000 条数据库连接,可能直接压垮数据库。
商业场景五:请求链路排查
当用户说“接口不通”,不要只看 Controller。
flowchart TD
A["接口异常"] --> B{"状态码是什么"}
B -- "404" --> C["检查路径、上下文、Servlet/Controller 映射"]
B -- "401/403" --> D["检查 Filter、Session、Token、权限"]
B -- "跨域" --> E["检查 CORS Filter 和响应头"]
B -- "500" --> F["看异常堆栈和业务日志"]
B -- "超时" --> G["拆 Tomcat 线程、DB 连接、慢 SQL、外部接口"]
C --> H["按请求链路逐段定位"]
D --> H
E --> H
F --> H
G --> H排查表:
| 现象 | 重点位置 |
|---|---|
| 404 | URL、context path、Servlet mapping、Controller mapping |
| 401 | 登录 Filter、Session/Cookie、Token |
| 403 | 权限模型、角色、资源授权 |
| 跨域 | OPTIONS 预检、Access-Control-Allow-* |
| 乱码 | 请求编码、响应编码、数据库字符集 |
| 响应已提交 | 代码先写响应又 forward/redirect |
| 超时 | Tomcat 线程、连接池、慢 SQL、外部接口 |
和 Spring 体系的关系
| JavaEE 基础 | Spring/Spring Boot 中的体现 |
|---|---|
| Servlet | Spring MVC 的 DispatcherServlet |
| Filter | Spring Security 过滤器链、CORS Filter、日志 Filter |
| Listener | Servlet 容器生命周期、Spring 上下文初始化 |
| Session/Cookie | 登录态、Spring Session、RememberMe |
| JDBC | DataSource、JdbcTemplate、MyBatis、JPA 底层 |
| Web 容器 | Spring Boot 内嵌 Tomcat/Jetty/Undertow |
理解 JavaEE 后,再看 Spring MVC 请求流程、Spring Security 过滤器链、Spring Boot 内嵌 Tomcat,就不是黑盒了。
面试标准回答
JavaEE 请求完整链路是什么?
浏览器请求先到 Web 容器,容器接收连接并解析 HTTP,创建 Request 和 Response,然后按顺序执行 Filter 链。Filter 放行后进入 Servlet;Spring MVC 项目里 Servlet 通常是 DispatcherServlet,再分发到 Controller。业务访问数据库时通过连接池拿 Connection,执行 SQL,事务提交或回滚,最后写 Response 返回。
Filter 为什么能做登录校验?
Filter 位于 Servlet 之前,可以读取请求头、Cookie、Session,也可以决定是否继续调用 chain.doFilter。所以它适合做登录校验、日志、编码、跨域、限流等统一逻辑。如果不放行,请求不会进入业务代码。
Session 多实例为什么会丢?
默认 Session 存在单个应用实例内存中。用户登录请求如果打到实例 A,后续请求被负载均衡到实例 B,B 找不到 A 内存里的 Session,就会认为用户未登录。解决方式包括粘性会话、Redis 集中 Session、Spring Session 或无状态 Token。
连接池耗尽怎么排查?
先看连接池 active、idle、waiting 指标和等待连接超时日志;再查慢 SQL、事务过长、连接泄漏和实例总连接数。代码层面用 try-with-resources 确保连接释放,事务里不要做慢外部调用。
关联知识点
| 知识点 | 继续学习 |
|---|---|
| JavaEE 主线 | JavaEE 从零到生产级掌握 |
| 入门 | JavaEE 入门 |
| Servlet | Servlet |
| Filter/Listener | Filter 与 Listener |
| Session/Cookie | Session 与 Cookie |
| JDBC | JDBC |
| 面试 | JavaEE 面试题 |
