Day 05 / 共 20 天 · 第 1 周 后端架构

横切与 Service 业务层

第 1 周收官。今天看两样东西:一是"每个请求都会经过"的 AOP 切面(鉴权/日志/异常统一在这里),二是 console 自身业务 service(登录、系统配置)。理解切面 = 理解后端的"请求生命周期"。

📍 你在整门课的位置(第 1 周 后端架构 · 收官)
D01 全景 D02 启动 D03 分层 D04 REST D05 横切 D06 SDK D07 存储 D08 客户端 D09 模型 D10 流转
💡 一句话兜住今天(切面 = 机场安检口) Day 04 看的是"每个控制器长啥样",今天看的是"所有请求进控制器之前都要先过的那道口子"。AOP 切面就是机场安检:无论你飞哪个航班(调哪个接口),都先过同一个安检口——发一张行李条 traceId(全程日志都印这个号,一搜就串起来)、查门禁(没登录不让进)、出口再统一兜底登记(任何异常都翻译成规整的错误码)。把这些"人人都要做"的事收在一处,就是横切。
L01

请求生命周期的枢纽

console/aop/ApiStandardizationAspect.java@Aspect @Component)用一个切点拦截所有控制器方法:

// :69-70
@Around("execution(* com.alibaba.higress.console.controller..*Controller.*(..))")
public Object around(ProceedingJoinPoint pjp) { ... }
什么是 AOP 切面? 有些逻辑(打日志、鉴权、异常兜底)几乎每个接口都要做,如果每个方法里都写一遍就太啰嗦。AOP(面向切面编程)让你把这些"横切逻辑"抽出来,声明"拦截所有 Controller 方法",Spring 就自动在每次调用前后插入执行。好比机场安检——不管你去哪个登机口,都先过同一个安检口。这个切面就是 Console 后端的"安检口"。
一个请求的安检流水线(@Around 前半段 → 执行 → 后半段兜底) ① traceId 发行李条 ② 上下文 HttpContext ③ 门禁 鉴权校验 ④ 执行 Controller 方法 ⑤ 统一异常兜底 Throwable→状态码 ①②③ 在 @Around 里"执行前"完成,⑤ 用 try/catch 包住"执行"这一步
图注:一个切面 = 一整条安检流水线,任何接口都先过它,再到自己的登机口。
🧍 现在你是一个请求,跟着走一遍 你带着 POST /v1/routes 撞进后端 → 安检口先给你贴张行李条 traceId=abc123(之后你产生的每条日志都印 abc123)→ 把你的 request/response 收进 HttpContext 随身包 → 查你的登录凭证(没有?当场轰出去 401)→ 放行到 RoutesController.add 干正事 → 万一你在里面摔了(抛异常),出口的兜底员把你摔的姿势翻译成 400/401/409/500 再送出门。全程你都揣着 abc123,出事一搜就还原整条路径。
L02

traceId 与上下文

切面进来第一件事(:72-77):生成 traceId 放进 MDC,并初始化 HttpContext(用 ThreadLocal 保存 request/response)。

读法:traceId 是每个请求的唯一编号,写进日志(对应 application.properties 日志 pattern 里的 %mdc)。这样一条请求从进来到报错的所有日志都带同一个 traceId,排查问题时一搜就串起来了。HttpContext 用 ThreadLocal 让后续代码能随时拿到当前请求对象,不用一路当参数传。
📝 举个例子:traceId 怎么把一次报错的日志串起来 一次 POST /v1/routes 进来 → 切面发号 traceId=abc123 → 后续每条日志都印它:
[abc123] validate route demo[abc123] createIngress...[abc123] ERROR ApiException 409
排查时只需 grep abc123 app.log,这次请求从进门到报错的全过程就连成一条线,不会和别的并发请求日志搅在一起。
L03

鉴权:登录校验

:80-89:若当前接口不是匿名接口,就调 sessionService.validateSession 校验登录,失败抛 AuthException,成功把 user 放进 SessionUserHelper

if (isLoginRequired(method)) {
    User user = sessionService.validateSession();  // 校验会话
    SessionUserHelper.setCurrentUser(user);
}
读法:"要不要登录"由 isLoginRequired:134-162)读取类/方法上的 @AllowAnonymous 注解决定,结果缓存进 ConcurrentHashMap 避免每次反射。集中鉴权的好处:新增一个接口默认就受保护,不用记得手动加校验。
L04

统一异常处理

:96-109 catch 所有 Throwable,按类型分级打日志,转成 Response.failure + 对应 HTTP 状态。状态映射在 getHttpStatus:164-178):

ValidationException      → 400
AuthException            → 401
NotFoundException        → 502
ResourceConflictException→ 409
其余                      → 500
为什么 NotFound 是 502 而不是 404? 这里的 NotFoundException 通常指"去 K8s / Higress 控制器找东西没找到",也就是"上游依赖异常",所以用 502(Bad Gateway,网关自身作为客户端访问后端失败)。而"用户查的资源不存在"是在 ControllerUtil 里返回 404(Day 04)。两种"没找到"语义不同,用不同的码区分。K8s 的 ApiException 还会把 Code/Body 一起打进日志(:117-131)。
Service/SDK 抛出含义切面翻译成
ValidationException入参不合法400
AuthException没登录/凭证失效401
ResourceConflictException撞名/版本冲突409
NotFoundException上游依赖(K8s/控制器)找不到502
其它 Throwable兜底500
⚠️ 常见误解:看到 NotFound 就以为该返 404。这里 404 vs 502 是两种"没找到":用户查的资源不存在 → ControllerUtil 返 404(Day 04);后端去 K8s 找依赖没找到 → 502(我这个"网关"访问上游失败)。语义不同,别混。
L05

@AllowAnonymous

登录、就绪探针、落地页这类接口不能要求先登录,用 @AllowAnonymous 标注跳过鉴权。切面读到它就放行。

读法:这是"默认安全"设计——不加注解 = 需要登录,只有明确标 @AllowAnonymous 的才放行。LandingControllerHealthzControllerSessionController.login 都是匿名的。
L06

SessionService:登录

SessionController/session)提供 login/logout,背后 SessionService 校验用户名密码(来自 Helm 注入的 admin.username/passwordhelm/values.yaml:113-116)并签发会话。

读法:Console 的登录很轻——单管理员账号,凭据从部署配置来。这也呼应"无数据库":连用户信息都不建表,直接用配置。UserController/user)提供 info/changePassword。
L07

ConfigService:系统配置存 ConfigMap

console 自身的持久化配置走 console/service/ConfigServiceImpl.java@Service):

// :42-43 ConfigMap 名默认 higress-console
// :53-64 getString 从 ConfigMap 的 data 读值
// :151-175 setConfigs:读/初始化 ConfigMap → 改 data → replaceConfigMap
读-改-写模式 改配置时它先 readConfigMap 拿到当前 ConfigMap,改动 data,再 replaceConfigMap 写回。K8s 的 replace 带 resourceVersion 乐观锁——如果中途有人也改了,版本号对不上,写入会失败,避免覆盖别人的改动。这就是用 K8s 当数据库时的并发控制方式。注意这个 ConfigService 只管控制台自己的键值,跟"网关业务配置"是两码事。
L08

今日小结 + 动手(第 1 周收官)

🧠 第 1 周你应该能回答

  • 切面拦截了什么?请求生命周期的五步(traceId→上下文→鉴权→执行→统一异常)?
  • 异常类型到 HTTP 状态的映射?为什么 NotFound 是 502?
  • @AllowAnonymous 的"默认安全"设计?
  • console 系统配置怎么存进 ConfigMap?乐观锁怎么防并发覆盖?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/higress-console/backend/console/src/main/java/com/alibaba/higress/console
sed -n '69,178p' aop/ApiStandardizationAspect.java
sed -n '42,175p' service/ConfigServiceImpl.java
ls aop/ service/
下周预告 · 第 2 周:钻进 SDK 与配置存储——HigressServiceProvider 手写依赖注入、KubernetesClientService 怎么连 K8s、KubernetesModelConverter 怎么把路由翻译成 Ingress+注解,以及"创建路由"的端到端追踪。
← Day 04 Day 06 · SDK 抽象 →