第三章 · Web 请求层(web Starter)
上一章的
CommonResult定义了”返回长什么样”,这一章回答”请求进来、结果出去,中间经历了什么“。核心是三个机制:统一 API 前缀(为什么你的 Controller 天然带/admin-api)、全局异常翻译(任何异常都被打成CommonResult)、以及 请求上下文贯通(用户、结果如何被塞进 request 供后续 Filter 读取)。
1. 模块概述
yudao-spring-boot-starter-web 是 web 层的”宿主” Starter(62 个文件),它并不只是 URL 路由,而是承载了一组横切能力,按子包划分:
| 子包 | 负责内容 |
|---|---|
web.config |
WebProperties、YudaoWebAutoConfiguration(路径前缀 + Filter 注册) |
web.core.handler |
GlobalExceptionHandler(异常→CommonResult)、GlobalResponseBodyHandler(记录返回) |
web.core.util |
WebFrameworkUtils(request 属性贯通) |
web.core.filter |
ApiRequestFilter、CacheRequestBodyFilter、DemoFilter |
apilog.* |
ApiAccessLogFilter / Interceptor(记录访问日志) |
desensitize.* |
数据脱敏(手机号、身份证等) |
xss.* |
XSS 清洗 |
banner.* |
启动 Banner |
swagger.* / jackson.* |
接口文档与 JSON 序列化 |
它在请求链路中的位置(全局视角):
flowchart LR
R["HTTP 请求"] --> A["静态资源/CORS"]
A --> F["Filter 链
CacheRequestBody→ApiAccessLog→Security 等"]
F --> DC["DispatcherServlet"]
DC --> M["MVC HandlerMapping
配置了 API 前缀"]
M --> C["Controller 返回 CommonResult"]
C --> G["GlobalResponseBodyHandler 记录结果"]
C -.异常抛出.-> E["GlobalExceptionHandler 翻译成 CommonResult"]
G --> OUT["返回前端"]
2. 命名体系与易混淆组件对比
2.1 命名规律拆解
| 前缀 | 模块/对象 | 操作 | 含义 |
|---|---|---|---|
Global |
Exception |
Handler |
全局异常处理器(@RestControllerAdvice) |
Global |
ResponseBody |
Handler |
全局响应体处理器(ResponseBodyAdvice) |
WebFramework |
Utils |
(静态工具) | 专属于 web 包的工具类 |
| 包名 | web.core.* / web.config.* |
(分层) | 运行核心 vs 装配配置 |
命名规律总结:
GlobalXxxHandler是”对全系统生效的处理器”,通过 Spring 的@ControllerAdvice/ResponseBodyAdvice织入。config包管”怎么装配”(AutoConfiguration+Properties),core包管”运行时做什么”。- 与 第二章 的
xxxExceptionUtil不同,异常处理的高层入口统一收敛到GlobalExceptionHandler。
2.2 易混淆组件对比:GlobalExceptionHandler vs GlobalResponseBodyHandler
| 对比维度 | GlobalExceptionHandler |
GlobalResponseBodyHandler |
|---|---|---|
| 触发时机 | 方法抛异常时 | 方法正常返回时 |
| 底层机制 | @RestControllerAdvice + @ExceptionHandler |
ResponseBodyAdvice(ControllerAdvice) |
| 职责 | 异常 → 统一的 CommonResult |
记录 Controller 返回结果(给访问日志用) |
| 是否改数据 | 重写响应结构 | 不改返回,只旁路记录 |
| 一句话区分 | 是”失败的统一出口”;它是”成功响应的记录员”。 |
3. API Signatures
核心组件的调用关系:
graph TD
U[YudaoServerApplication 启动] --> W[YudaoWebAutoConfiguration]
W --> P[WebProperties 绑定 yudao.web]
W --> E[GlobalExceptionHandler]
W --> R[GlobalResponseBodyHandler]
W --> F[注册 CorsFilter / CacheRequestBodyFilter 等]
E --> WEBUTIL[WebFrameworkUtils]
R --> WEBUTIL
SEC[Security 层] -->|setLoginUser| WEBUTIL
APILOG[ApiAccessLogFilter] -->|读取| WEBUTIL
1 | // —— 属性绑定(yudao.web.*)—— |
4. 数据结构深度解析
4.a 结构体存在的理由:WebProperties.Api
为什么需要”API 前缀”这样一层抽象?直接写死
/admin-api不就好了?源码注释给出了精妙答案:前缀是为了隔离——Swagger、Actuator 等治理端点如果不带前缀,就会被 Nginx 误转发到公网,造成安全隐患。有了/admin-api、/app-api两个前缀,Nginx 只需要放行/api/*,内部端点天然被挡在里面。更进一步,用
prefix + controller 包名 Ant 规则把”绑哪个前缀”和”哪些 Controller 该绑”解耦:只要 Controller 落在cn...module.X.controller.admin包,就自动挂上/admin-api前缀——这就是”约定优于配置”在 URL 层的体现。
4.b 代码:configurePathMatch
📍 源码:YudaoWebAutoConfiguration.java:43-59
1 |
|
它做了什么:对所有标注 @RestController 且所在包匹配 **.controller.admin.** 的类,统一在其所有映射前注入 /admin-api 前缀。于是你在 AuthController 上写 @RequestMapping("/system/auth"),实际对外就是 /admin-api/system/auth。
4.c 字段三层分析表(WebProperties.Api)
| 字段 | 设计动机 | 反事实 | 替代方案 |
|---|---|---|---|
prefix |
统一前缀、隔离内部治理端点 | 无前缀,Swagger/Actuator 裸奔公网 | Nginx 层做转发规则,但不灵活 |
controller |
用包 Ant 规则限定哪些类挂前缀 | 所有类都挂同一前缀,App/Admin 无法区分 | 手动在每个类上写完整路径,重复易错 |
4.d 生命周期(一次 ApiAccessLog 的请求上下文流动)
sequenceDiagram
participant F as ApiAccessLogFilter
participant T as TokenAuthenticationFilter
participant C as Controller
participant H as GlobalResponseBodyHandler
participant L as 访问日志
F->>F: 请求开始,记录开始时间(此时拿不到 userId)
T->>T: 解析 token,setLoginUser
T->>C: SecurityFrameworkUtils.setLoginUser 顺带 WebFrameworkUtils.setLoginUserId(request)
C->>H: 返回 CommonResult
H->>H: WebFrameworkUtils.setCommonResult(request, body)
C->>F: 请求结束
F->>F: 从 request 属性取 userId/userType/commonResult + 耗时
F->>L: 写入访问日志(虽然 Filter 在 Security 之前,仍能拿到用户,靠的就是 request 属性)
💡 这一串的精妙之处:
ApiAccessLogFilter的注册顺序在 Spring Security 之前(见 第四章 的过滤器顺序),理论上那时还没有登录用户。但通过WebFrameworkUtils把 userId 写进 request 属性(request 对象在整个请求生命周期内共享),后执行的 Security Filter 得以把信息”塞回”给先执行的兜底日志 Filter。这是典型的”用共享对象跨 Filter 传递状态”。
5. 关键机制剖析:异常 → CommonResult 的翻译
GlobalExceptionHandler(第三章已读源码)用 @ExceptionHandler 为每一类异常提供专门出口,核心分发逻辑是 allExceptionHandler(供 Filter 使用,因为 Filter 不走 SpringMVC):
1 | 请求参数缺失 → 400 |
要点:
ServiceException返回的是业务自定义 code 与友好 msg,直接展示给用户;且为降低日志噪音,IGNORE_ERROR_MESSAGES里的消息跳过堆栈打印。- 兜底
Exception会先把异常写入ApiErrorLog(apiErrorLogApi.createApiErrorLogAsync,异步),再返回500 系统异常,避免泄漏内部细节。 - 特判”表不存在”:根据表名前缀(
bpm_/mp_/pay_…)提示”该模块未导表/未开启”,是工程化排障的小亮点。
6. 设计决策分析
- 为什么 Controller 主动包
CommonResult,而不用 AOP 自动包? 源码注释说得明白:GlobalResponseBodyHandler本质是 AOP,不应改变 Controller 返回的数据结构;统一在 Controller 层显式CommonResult.success(...)代码更直白,也便于 IDE/read 理解。 - 为什么把通用工具类
WebFrameworkUtils注册成 Bean? 因为它需要webProperties,而静态工具类无法注入;注册 Bean 后构造器把属性缓存进 static 字段,对外仍是静态方法调用。 - 为什么前缀用
AntPathMatcher(".")按包名匹配而非类名? 包是稳定的”领域边界”,比逐个类判断更集中、更省心。
7. 学习检查点
📝 本章小结
WebProperties用prefix + 包 Ant 规则统一注入/admin-api、/app-api前缀。GlobalExceptionHandler把所有异常翻译成统一的CommonResult,业务异常透出友好 msg,兜底写异常日志。GlobalResponseBodyHandler不改返回,只旁路记录结果给访问日志。WebFrameworkUtils用 request 属性跨 Filter 传递 userId/userType/commonResult,解决”日志 Filter 在 Security 之前却要拿到用户”的矛盾。
🤔 思考题
为什么一段请求既能被”先执行”的 ApiAccessLogFilter 记录用户,又能被”后执行”的 TokenAuthenticationFilter 解析用户?(提示:过滤器顺序 + request 属性)
参考答案
关键在 request 属性转发:Security 的
TokenAuthenticationFilter解析出LoginUser后,通过WebFrameworkUtils.setLoginUserId(request, token)写进当前 request 属性;而ApiAccessLogFilter在请求结束时(而不是开始)才从request.getAttribute(...)读取用户与结果。因为两者共享同一个 request 对象,所以在 Filter 链的最后也能拿到最前面 Filter 写入的信息。这是”后执行者回填、先执行者在收尾时读取”的经典协作。configurePathMatch用包名而非逐个方法判断前缀,带来的取舍是什么?参考答案
利:新写一个
controller.admin下的类,什么都不用配就自动获得/admin-api前缀,”约定即生效”。弊:不够灵活——若某天想把某个 admin 接口暴露为 app 端,就必须把它挪到controller.app包(或去掉@RestController重新装配)。这是一种”面向包/约定”而非”面向注解/配置”的取舍。