面试知识

Spring(Java 应用框架) MVC(网页模型视图控制器)请求、校验、异常与异步

22-Spring生态与后端工程 面试知识整理。

Spring(Java 应用框架) MVC(网页模型视图控制器)请求、校验、异常与异步

1. 学习定位与面试主线

本篇对应知识图谱 2.3.5,讨论 Servlet(服务器小程序)技术栈中的一次请求怎样从连接进入控制器,又怎样在同步、异步、异常和断连路径上结束。学习时始终区分四层保证:协议层负责 HTTP(超文本传输协议)语义,框架层负责路由与对象转换,应用层负责用例编排,领域层负责业务不变量。返回 200 只说明协议请求被成功处理,绝不自动表示付款、出库或履约已经成功。

flowchart LR
    A["客户端请求"] --> B["Servlet(服务器小程序)容器"]
    B --> C["Filter(过滤器)链"]
    C --> D["DispatcherServlet(前端控制器)"]
    D --> E["映射、适配、参数与校验"]
    E --> F["控制器与应用服务"]
    F --> G{"同步还是异步?"}
    G -->|"同步"| H["返回值与消息转换"]
    G -->|"异步"| I["释放容器线程并等待结果"]
    I --> H
    F -->|"异常"| J["异常解析与证据保留"]
    J --> H
    H --> K["状态码、响应头和响应体"]

图解:节点表示一次请求跨越的协议、容器、框架和业务阶段;箭头表示控制权及数据所有权转移。前提是 Servlet(服务器小程序)容器已完成启动且请求被当前应用接收。正常路径由返回值处理器和消息转换器形成响应;失败路径必须进入异常解析并保留内部证据;异步路径只释放原容器线程,不会消除业务等待。业务结论是请求生命周期、业务状态生命周期和后台任务生命周期必须分别建模。


2. 请求入口与路由骨架

2.1 DispatcherServlet(前端控制器)完整请求链与状态边界

DispatcherServlet(前端控制器)采用 Front(前端) Controller(控制器)模式,把路径匹配、参数解析、控制器调用、返回值处理和异常解析放在统一骨架中。核心入口 doDispatch 先取得处理器执行链,再选择处理器适配器,执行前置拦截、目标处理器和后置拦截;若返回视图则渲染,若返回响应体则由返回值处理链写回。任何阶段抛出的异常都不会凭空消失,而是交给异常解析器链决定能否转成协议响应。

sequenceDiagram
    participant C as Client(客户端)
    participant S as ServletContainer(服务器小程序容器)
    participant F as FilterChain(过滤器链)
    participant D as DispatcherServlet(前端控制器)
    participant M as HandlerMapping(处理器映射器)
    participant A as HandlerAdapter(处理器适配器)
    participant H as Handler(处理器)
    participant E as ExceptionResolver(异常解析器)
    C->>S: HTTP(超文本传输协议)请求
    S->>F: 分派请求
    F->>D: service / doDispatch
    D->>M: getHandler
    alt 未匹配
        M-->>D: null
        D-->>C: 404
    else 已匹配
        M-->>D: HandlerExecutionChain(处理器执行链)
        D->>A: getHandlerAdapter
        A->>H: 参数解析、校验并调用
        alt 正常
            H-->>A: 返回值
            A-->>D: ModelAndView(模型与视图)或响应已处理
        else 异常
            H--xD: Throwable(可抛出对象)
            D->>E: resolveException
            E-->>D: 错误响应或未解析
        end
        D-->>F: 响应
        F-->>C: 状态码、响应头、响应体
    end

图解:参与者从客户端到异常解析器构成请求主链,实线箭头表示调用,虚线表示返回,叉线表示异常传播。前提是过滤器链允许请求进入。正常路径依次完成映射、适配和写回;未映射形成 404,业务异常进入解析器,未解析异常继续交由容器错误处理。业务结论是定位接口问题时要先确定失败阶段,不能看到 500 就直接怀疑控制器。

阶段核心输入核心输出典型失败证据
容器分派方法、路径、连接请求/响应对象端口、连接、容器错误页
过滤链原始请求包装后的请求或提前响应请求未进入 DispatcherServlet(前端控制器)
处理器映射路径、方法、请求头处理器执行链404、方法不允许、媒体类型不匹配
处理器适配处理器、参数元数据返回值或异常参数解析、类型转换、校验异常
异常解析异常、处理器错误模型与状态码错误码映射缺失、异常被错误降级
写回返回值、协商结果字节响应转换失败、客户端断连、响应已提交

数据演绎 1:一次订单确认请求的阶段证据

T0=0ms 收到 POST /api/v1/orders/O20260714001/confirm,生成请求标识 R-9001T1=2ms 过滤器完成请求体上限和追踪字段检查;T2=4ms 映射到确认方法;T3=7ms 将路径变量绑定为订单号;T4=12ms 应用服务发现订单已经取消并抛出领域冲突;T5=14ms 异常解析器返回 409 与业务码 ORDER_STATE_CONFLICT。若 T2 没有处理器是 404,若 T3 类型错误是 400,若 T5 已经写出部分响应则不能再安全改状态码。每个时刻都带同一请求标识,才能证明故障发生在哪一层。

热门面试题

  1. 问题(基础题):DispatcherServlet(前端控制器)在 Spring(Java 应用框架) MVC(网页模型视图控制器)中承担什么职责?

    • 考点:统一入口和委派设计。
    • 回答思路:先说它不直接完成所有工作,再沿映射、适配、异常和写回展开。
    • 详细答案:DispatcherServlet(前端控制器)是 Servlet(服务器小程序)请求的统一调度入口。它从处理器映射器取得执行链,用处理器适配器调用具体控制器,把返回值交给视图或响应体处理,并在异常时调用异常解析器。组件通过策略接口组合,因此新增参数解析器或异常解析器不需要改主调度骨架。
    • 进阶追问:统一入口会不会成为业务单点?
    • 进阶回答:它是每个应用实例内的调度组件,不是跨实例网络单点;容量由容器线程、实例数和下游资源共同决定,真正风险是主链上加入阻塞操作或无界日志。
  2. 问题(原理题)doDispatch 为什么要先找 HandlerMapping(处理器映射器),再找 HandlerAdapter(处理器适配器)?

    • 考点:查找与调用解耦。
    • 回答思路:区分“谁处理”与“怎样调用”。
    • 详细答案:HandlerMapping(处理器映射器)根据请求条件回答“由哪个处理器处理”,并返回拦截器组成的执行链;HandlerAdapter(处理器适配器)回答“怎样调用这个类型的处理器”。这种分离允许注解控制器、函数式处理器或其他处理器类型共享调度入口,同时各自保留参数与返回值协议。
    • 进阶追问:没有适配器会怎样?
    • 进阶回答:已找到处理器但没有支持其类型的适配器时,请求不能执行,会在调度阶段失败;应检查适配器注册、处理器类型和实际框架版本,而不是改路径。
  3. 问题(项目题):接口返回 200 为什么不代表订单确认成功?

    • 考点:协议成功与业务成功分层。
    • 回答思路:对比传输、应用接受、领域终态和异步终态。
    • 详细答案200 只表达服务器成功处理了当前 HTTP(超文本传输协议)交互。若响应体中业务码失败、请求只是受理为异步任务、渠道结果仍未知,订单都未必确认成功。同步冲突应使用合适状态码和业务码;异步受理可返回 202 与任务标识;最终结果通过查单、回调或事件收敛。
    • 进阶追问:是否应该所有业务失败都返回 500
    • 进阶回答:不应该。可预期的输入错误、状态冲突和权限失败应映射到稳定的 4xx500 留给未预期服务器故障,同时内部记录根异常和请求标识。

2.2 HandlerMapping(处理器映射器)、HandlerAdapter(处理器适配器)与拦截器执行链

映射不是只按字符串路径查找。注解请求映射会综合 HTTP(超文本传输协议)方法、路径模式、参数、请求头、消费媒体类型和生产媒体类型选择最具体的方法,并在启动期构建映射注册表。适配器随后读取方法参数和返回类型,选择对应解析器与处理器。HandlerExecutionChain(处理器执行链)把处理器和拦截器绑定,使前置检查、成功后处理和请求完成清理具有确定顺序。

flowchart TD
    A["请求方法、路径、参数、请求头和媒体类型"] --> B["HandlerMapping(处理器映射器)候选检索"]
    B --> C{"零个、一个还是多个候选?"}
    C -->|"零个"| X["404 或媒体条件错误"]
    C -->|"多个且无法判定"| Y["歧义映射失败"]
    C -->|"唯一最具体"| D["HandlerExecutionChain(处理器执行链)"]
    D --> E["preHandle 正序"]
    E --> F{"是否放行?"}
    F -->|"否"| G["短路并执行完成清理"]
    F -->|"是"| H["HandlerAdapter(处理器适配器)调用"]
    H --> I["postHandle 逆序"]
    I --> J["afterCompletion 逆序"]
    H -->|"异常"| J

图解:候选检索节点体现多条件匹配,执行链节点把路由与拦截器组合;箭头标明正常、短路和异常路径。前提是启动期映射注册没有冲突。正常路径前置拦截正序、后置和完成回调逆序;失败路径可能在映射期结束,也可能在前置拦截中短路。业务结论是幂等、审计等机制必须明确其短路后是否仍需完成清理。

匹配维度正常用途冲突示例治理建议
方法区分查询与变更同一路径同时接受所有方法变更接口显式限制方法
路径定位资源层级变量路径与固定路径含义重叠使用稳定版本前缀和领域名词
请求参数兼容特定动作隐式参数决定完全不同语义大差异拆成独立资源或动作
请求头版本、租户或能力协商代理层删除自定义头给缺失路径明确错误与监控
消费媒体类型选择请求体解析客户端声明与真实字节不一致返回 415 并记录安全摘要
生产媒体类型选择响应形式客户端只接受不支持格式返回 406,不静默降级

数据演绎 2:歧义映射和错误定位

系统注册 A:POST /orders/{id} 且消费 JSON(JavaScript 对象表示法),B:POST /orders/confirm 未限制消费类型。请求 POST /orders/confirmContent-Type: application/json 到达时,固定路径通常更具体,应选择 B;若另一个方法也声明相同固定路径和条件,启动或首次映射检测会报告歧义。请求改为 XML(可扩展标记语言)时,如果 B 不消费该类型而没有其他候选,应得到 415,不是 404。排查必须同时记录方法、规范化路径、内容类型和接受类型。

热门面试题

  1. 问题(基础题):HandlerMapping(处理器映射器)返回的为什么是执行链而不只是控制器方法?

    • 考点:路由与横切前后处理的组合。
    • 回答思路:说明处理器周围还需要按映射配置的拦截器。
    • 详细答案:一次请求不仅要调用目标方法,还可能需要租户检查、审计、区域路由或耗时统计。执行链把目标处理器和适用于该路径的拦截器固定在一起,调度器据此执行前置、后置和完成回调。这样拦截规则仍与路径匹配结果一致。
    • 进阶追问:拦截器抛异常后 afterCompletion 一定执行吗?
    • 进阶回答:只对已经成功执行过前置并登记的拦截器执行;若响应进程被强杀则任何进程内回调都不保证,关键清理不能只依赖它。
  2. 问题(原理题):请求映射歧义为什么应尽量在启动期失败?

    • 考点:确定性和失败前置。
    • 回答思路:对比启动拒绝与运行随机选择的风险。
    • 详细答案:同一请求条件对应多个等价处理器时,任意选择会让版本、注册顺序或部署实例影响业务结果。启动期构建映射注册表并拒绝冲突,可以在接流量前暴露配置错误,保证相同请求在所有实例上得到同一路由语义。
    • 进阶追问:两个接口路径不同就一定没有歧义吗?
    • 进阶回答:不一定。路径变量、通配模式以及方法、请求头和媒体条件组合后仍可能重叠,必须以完整请求条件比较。
  3. 问题(项目题):支付回调接口为什么要严格限制方法和媒体类型?

    • 考点:攻击面、原始字节与签名协议。
    • 回答思路:从渠道契约、验签和错误可观测性回答。
    • 详细答案:回调通常规定固定方法、内容类型和签名头,宽松接收会让代理转换、表单与 JSON(JavaScript 对象表示法)混淆,甚至让验签使用的字节与渠道发送字节不同。应在映射和过滤入口尽早拒绝不符合契约的请求,并记录渠道、请求标识和原因,不记录敏感原文。
    • 进阶追问:渠道偶尔发错内容类型怎么办?
    • 进阶回答:先保留证据并与渠道确认协议;若必须兼容,应按明确渠道和版本做受控适配,不能全局放宽所有回调入口。

2.3 参数解析、返回值处理与方法调用模型

RequestMappingHandlerAdapter(请求映射处理器适配器)把控制器方法抽象成 InvocableHandlerMethod(可调用处理器方法)。对每个参数,它按顺序询问 HandlerMethodArgumentResolver(处理器方法参数解析器)是否支持;解析结果可能来自路径、查询参数、请求头、会话、主体、当前用户或自定义租户上下文。调用结束后,HandlerMethodReturnValueHandler(处理器方法返回值处理器)按返回类型决定写响应体、选择视图、启动异步,或声明响应已经由方法处理。

flowchart LR
    A["方法参数元数据"] --> B["参数解析器组合"]
    B --> C{"首个 supportsParameter 命中"}
    C -->|"无命中"| X["参数解析失败"]
    C -->|"命中"| D["从路径、查询、头、主体或上下文取值"]
    D --> E["类型转换、绑定与校验"]
    E --> F["反射调用控制器"]
    F --> G["方法返回类型和值"]
    G --> H{"返回值处理器首个命中"}
    H -->|"响应体"| I["消息转换器写回"]
    H -->|"视图"| J["模型与视图渲染"]
    H -->|"异步"| K["启动异步处理"]
    H -->|"无命中"| Y["返回值处理失败"]

图解:左侧是参数从元数据到实际值的解析链,右侧是返回值到协议输出的处理链;箭头表示“首个支持者负责”的责任链语义。前提是解析器和处理器顺序稳定。正常路径产生方法参数并选择唯一返回策略;失败路径是无支持者、转换失败或输出阶段异常。业务结论是自定义扩展必须限定支持条件,避免抢占框架内置语义。

输入形式常见解析来源失败状态关键边界
路径变量路径模板400404 取决于阶段资源标识不可悄悄截断
查询参数查询字符串400列表长度、排序字段白名单
请求头请求头集合400/401代理是否保留、是否可信
请求体输入流和消息转换器400/415通常只能消费一次,需缓存才可复读
当前用户/租户安全或自定义上下文401/403不可相信客户端直接传来的租户标识
文件多部分请求解析400/413大小、数量、类型、临时文件清理

数据演绎 3:批量轨迹查询参数的资源边界

请求携带 trackingNo=1001,1002,...5000 个号码,原始查询字符串 78KB。即使容器允许该长度,把它绑定为列表也会触发大量转换并生成超大下游查询。入口应把单次上限设为 200,超过时返回 400 与错误码 BATCH_SIZE_EXCEEDED;合法 200 个号码经去重后为 183 个,再交应用服务分批查询。解析器只负责形成可信输入,不负责决定轨迹是否属于当前货主,该权限属于应用或领域层。

热门面试题

  1. 问题(基础题):参数解析器和消息转换器有什么区别?

    • 考点:方法参数来源与主体字节转换的层次。
    • 回答思路:说明前者决定参数从哪里来,后者只处理主体表示。
    • 详细答案:HandlerMethodArgumentResolver(处理器方法参数解析器)面向控制器方法参数,决定参数是否支持以及从路径、查询、头、主体或上下文怎样取得;当参数来自请求体时,它会进一步委托 HttpMessageConverter(消息转换器)把字节转换为对象。两者是编排和表示转换关系。
    • 进阶追问:自定义租户参数需要消息转换器吗?
    • 进阶回答:通常不需要。它应从可信安全上下文解析租户对象,而不是重新解析请求体;还要验证用户对租户的访问权。
  2. 问题(原理题):为什么参数解析器采用“首个支持者负责”?

    • 考点:责任链与扩展顺序。
    • 回答思路:从开放扩展和歧义控制回答。
    • 详细答案:不同参数注解和类型需要不同来源与构造方式,责任链让每个解析器只处理自己的契约,适配器无需写巨大条件分支。首个命中也意味着顺序是协议的一部分,自定义解析器若支持条件过宽,可能抢走请求体、分页或安全参数并改变全局行为。
    • 进阶追问:如何验证自定义解析器没有抢占?
    • 进阶回答:对支持和不支持的参数类型分别做真实请求测试,记录最终解析器类型,并在依赖升级后比较解析器顺序和关键接口行为。
  3. 问题(项目题):怎样设计 WMS(仓储管理系统)批量接口的参数边界?

    • 考点:输入资源预算和领域权限。
    • 回答思路:按字节、元素数、去重、字段白名单和权限分层。
    • 详细答案:入口先限制请求体字节、数组元素数和单字段长度,再绑定为命令对象;对编号去空、去重并校验格式,但货主、仓库和订单状态权限放应用服务按可信身份判断。批量过大应拒绝或转异步任务,不能让一个请求占满数据库连接和堆内存。
    • 进阶追问:为什么不在控制器里直接分成多批循环查询?
    • 进阶回答:控制器缺少事务、重试和任务状态设计;大任务应交应用服务或持久异步任务统一预算、监控和恢复。

2.4 HttpMessageConverter(消息转换器)、内容协商与接口版本兼容

HttpMessageConverter(消息转换器)依据 Java(编程语言)目标类型和 Media Type(媒体类型)读取或写出主体。内容协商根据请求路径、参数或 Accept 请求头选择可生产的表示;请求的 Content-Type 描述“我发送了什么”,Accept 描述“我能接收什么”,两者错误分别常落到 415406。接口版本兼容不能只靠反序列化“忽略未知字段”,还要管理字段语义、默认值、必填变化、枚举扩展和时间金额精度。

flowchart TD
    A["Content-Type、Accept、目标类型"] --> B["读取转换器 canRead"]
    B --> C{"存在可读转换器?"}
    C -->|"否"| X["415 Unsupported Media Type(不支持的媒体类型)"]
    C -->|"是"| D["字节读取为命令对象"]
    D --> E["控制器与应用服务"]
    E --> F["返回对象和可生产类型"]
    F --> G["内容协商选择 Media Type(媒体类型)"]
    G --> H{"存在可写转换器?"}
    H -->|"否"| Y["406 Not Acceptable(不可接受)或写出失败"]
    H -->|"是"| I["对象写成响应字节"]
    I --> J["客户端按版本契约解释"]

图解:读取链以请求内容类型和目标类型为前提,写出链以协商结果和返回类型为前提;箭头展示输入与输出两次独立选择。正常路径找到兼容转换器并按版本契约解释;失败路径明确区分 415406 和写出异常。业务结论是媒体类型、对象结构和业务版本是三种不同兼容维度。

兼容变化老客户端风险推荐策略不推荐做法
新增可选字段通常可忽略明确默认语义并做契约测试让缺失值改变旧业务决策
字段改名老客户端仍发旧名双读一段窗口并观测旧字段比例直接删除旧字段
类型变化反序列化或精度失败新增字段或新版本同名字段从字符串改数字
枚举扩展老客户端未知值预留 UNKNOWN(未知值)策略或版本隔离默认映射成第一个枚举
金额单位变化资金数量级错误字段名携带单位并新版本切换只在文档里说明单位改变
删除字段旧客户端逻辑缺失先监测使用量、弃用、再移除一次发布同时删服务端和客户端

数据演绎 4:金额字段兼容事故

版本 v1amount=1999 表示分,版本 v2 想改为元并仍使用整数。老客户端发送 1999,新服务若按元解释会放大 100 倍。安全方案新增 amountMinor=1999currency=CNY,服务端双读 30 天并统计旧字段调用比例;冲突时拒绝而不是猜测。比例从 42% 降到 0.1% 后才停止旧入口。内容能成功反序列化不代表业务语义兼容。

热门面试题

  1. 问题(基础题)Content-TypeAccept 分别解决什么问题?

    • 考点:请求表示与期望响应表示。
    • 回答思路:分别从读取和写出链回答。
    • 详细答案Content-Type 声明请求主体的媒体类型,服务端据此选择读取转换器;Accept 声明客户端可接受的响应媒体类型,服务端据此做内容协商并选择写出转换器。前者不支持通常是 415,后者无法满足通常是 406
    • 进阶追问:没有 Accept 是否一定失败?
    • 进阶回答:不一定,框架可按配置选择默认可生产类型;但关键接口应通过契约测试固定行为,不能把默认值当永久事实。
  2. 问题(原理题):为什么成功反序列化不等于接口兼容?

    • 考点:结构兼容与语义兼容。
    • 回答思路:用金额单位、默认值和枚举扩展说明。
    • 详细答案:反序列化只证明字节能形成对象,不证明字段含义未变。金额单位、时区、空值含义、枚举新增和默认规则都可能在对象创建成功后产生错误决策。兼容治理要包含消费者契约、版本窗口、调用比例和业务守恒校验。
    • 进阶追问:忽略未知字段是否总是安全?
    • 进阶回答:不是。若未知字段表达新的安全、金额或履约约束,老服务忽略后可能做出不合法决策;需要版本隔离或能力协商。
  3. 问题(项目题):跨境物流接口如何进行版本演进?

    • 考点:多方接入、长迁移周期和可回退性。
    • 回答思路:稳定资源语义、兼容窗口、双读观测和契约回归。
    • 详细答案:先区分可兼容新增与破坏性变化。可选字段新增要定义缺省语义;破坏性变化使用路径、请求头或媒体类型建立明确版本,服务端在窗口期双读并记录调用方版本。面单和轨迹接口还要保存原始渠道版本,保证历史消息可重放。
    • 进阶追问:是否要永久维护所有版本?
    • 进阶回答:不需要。应公布弃用窗口,统计真实使用,提供迁移工具和回放验证,达到退出门槛后关闭旧版本并保留可回滚发布物。

3. 输入可信化与失败表达

3.1 WebDataBinder(网页数据绑定器)、类型转换与字段攻击面

数据绑定把名称和值写入目标对象,类型转换把文本变成日期、枚举、编号或值对象。两者发生在业务方法之前,但不能把外部输入直接绑定到持久化实体:客户端若提交 status=PAIDownerId=other 等不应修改的字段,宽泛绑定可能形成 Mass Assignment(批量赋值)漏洞。命令对象应只暴露当前用例允许的字段,并对日期格式、枚举未知值、空字符串、集合上限和嵌套路径设定明确契约。

flowchart TD
    A["外部名称和值"] --> B["选择目标命令类型"]
    B --> C["允许字段与禁止字段检查"]
    C --> D{"字段是否允许?"}
    D -->|"否"| X["拒绝并记录字段摘要"]
    D -->|"是"| E["ConversionService(转换服务)"]
    E --> F{"转换成功?"}
    F -->|"否"| Y["BindingResult(绑定结果)记录错误"]
    F -->|"是"| G["写入命令对象"]
    G --> H["对象校验"]
    H --> I["应用服务重新加载权威实体"]

图解:节点从外部键值到命令对象,再进入应用服务;箭头表示输入逐步可信化。前提是控制器使用专用命令类型和允许字段集合。正常路径完成显式转换和校验;失败路径拒绝越权字段或返回绑定错误。业务结论是绑定成功只代表数据形状可用,权威状态仍必须从服务端存储重新读取。

类型推荐输入转换失败示例设计边界
日期时间ISO 8601(国际标准日期时间格式)并带时区夏令时歧义、无时区存储统一时刻,展示按业务时区
枚举稳定业务码新增值、大小写猜测未知值显式失败或进入兼容分支
金额最小货币单位加币种浮点精度、单位混淆不用二进制浮点直接计算
标识有长度和字符集的字符串前导零丢失不因“看似数字”转整数
布尔明确 true/false0/on/yes 语义不一禁止多种隐式真值
集合有元素和深度上限超大集合、深层嵌套入口预算后再交业务处理

数据演绎 5:实体直绑导致越权状态修改

更新收货地址接口原本直接绑定 OrderEntity。攻击请求除 address 外增加 status=SHIPPEDownerId=T2,绑定阶段全部成功;若持久层按整个实体更新,订单被伪造出库。改造后命令仅含 orderId/address/version,请求多出的字段在严格模式下返回 400;应用服务按可信租户重新加载订单,检查版本 17 和状态 PAID,只更新地址并把版本改为 18。这同时防止越权字段和并发覆盖。

热门面试题

  1. 问题(基础题):数据绑定和类型转换有什么区别?

    • 考点:对象属性写入与值表示变化。
    • 回答思路:用字符串日期写入命令对象说明两阶段。
    • 详细答案:类型转换把一个表示变成目标 Java(编程语言)类型,例如把日期文本变为时间对象;数据绑定负责按属性路径把请求中的多个值写入目标对象,并收集缺失、拒绝或转换错误。绑定过程会调用转换服务,但还管理允许字段和嵌套属性。
    • 进阶追问:转换成功后能直接更新数据库吗?
    • 进阶回答:不能。还需对象校验、身份权限、当前领域状态和并发版本检查,持久化实体应由服务端权威数据演进。
  2. 问题(原理题):为什么控制器不应直接绑定持久化实体?

    • 考点:批量赋值、领域状态和持久化耦合。
    • 回答思路:从可写字段面、攻击面和版本演进回答。
    • 详细答案:实体通常包含状态、租户、审计、版本和内部关联等客户端无权写入的字段。直接绑定会扩大可写面,并让接口契约随表结构变化。专用命令对象只暴露用例字段,应用服务再加载实体执行领域操作,能明确权限和不变量。
    • 进阶追问:禁止字段名单是否足够?
    • 进阶回答:允许名单更稳健;实体新增敏感字段时,禁止名单容易遗漏,而专用命令默认不可写。
  3. 问题(项目题):海外仓日期字段怎样避免时区事故?

    • 考点:时间点、当地时间和业务日期。
    • 回答思路:先定义字段语义,再决定格式和存储。
    • 详细答案:揽收时刻应使用带偏移的绝对时间,仓库截单时间可能是仓库时区内的当地规则,账务日又是独立业务日期。接口分别建模,不用一个无时区字符串承载三种语义;转换失败应返回字段级错误并记录仓库时区配置版本。
    • 进阶追问:统一转 UTC(协调世界时)能解决所有问题吗?
    • 进阶回答:绝对时刻可以统一存储,但“每天当地 18:00 截单”仍需保留时区和规则,不能只存换算后的固定时刻。

3.2 Bean(对象实例) Validation(对象校验规范)、分组校验与领域不变量

Bean(对象实例) Validation(对象校验规范)适合校验单个输入对象的结构约束,例如非空、长度、格式、数值范围和对象内字段关系。分组校验可表达创建与更新的输入差异,但组过多会把业务状态机塞进注解。库存是否足够、订单能否取消、付款金额是否与权威订单一致都依赖数据库当前状态、身份和并发顺序,属于应用或领域不变量,不能由对象校验替代。

flowchart LR
    A["请求字节"] --> B["转换为命令对象"]
    B --> C["Bean(对象实例) Validation(对象校验规范)结构校验"]
    C -->|"失败"| X["400 字段错误"]
    C -->|"通过"| D["身份与数据权限"]
    D -->|"失败"| Y["403 权限失败"]
    D -->|"通过"| E["加载权威聚合和当前版本"]
    E --> F["领域不变量与状态迁移"]
    F -->|"冲突"| Z["409 状态冲突"]
    F -->|"通过"| G["事务提交"]

图解:节点按结构可信、身份可信、状态可信逐层推进;箭头展示各层不同的失败语义。前提是命令对象不携带可直接信任的领域状态。正常路径最终在事务中执行状态迁移;失败路径分别形成 400403409。业务结论是校验不是领域不变量,不能用一个“校验通过”掩盖并发和权限判断。

校验层可使用的数据适合规则不适合规则
字段约束当前字段长度、范围、格式库存余额、订单状态
对象约束当前命令多个字段起止时间、条件组合跨订单总额、渠道真实状态
应用权限当前身份与资源归属仓库、货主、订单访问权字符串格式
领域不变量权威聚合与版本状态迁移、数量守恒传输媒体类型
数据库约束并发提交后的最终数据唯一性、非空、条件更新友好字段提示

数据演绎 6:库存扣减不能靠对象校验

请求命令 sku=S1, qty=8 通过数量范围校验。T0 数据库可售库存为 10;请求 A 和 B 同时读取 10,两者对象校验都通过;若各自写 10-8,最终可能丢更新或超卖。正确做法使用条件更新 available >= 8 并检查影响行数:A 成功后库存为 2,B 影响 0 行并返回 409 INVENTORY_NOT_ENOUGH。对象校验只阻止 qty<=0 或超出单次上限,领域和数据库共同守住并发不变量。

热门面试题

  1. 问题(基础题):Bean(对象实例) Validation(对象校验规范)通常在请求链哪个阶段执行?

    • 考点:参数解析、绑定和方法调用顺序。
    • 回答思路:说明对象形成后、控制器调用前的校验。
    • 详细答案:请求体或参数先被解析、转换并绑定成目标对象;当参数带校验触发条件时,适配器在控制器方法调用前执行约束校验。失败会形成方法参数或绑定类异常,通常由统一异常处理映射为字段错误响应。
    • 进阶追问:控制器方法没有执行,事务会回滚吗?
    • 进阶回答:若事务只在后续应用服务代理上,事务尚未开始,无需回滚;若更外层错误地开启事务,则应检查边界是否过宽。
  2. 问题(原理题):为什么分组校验不能代替订单状态机?

    • 考点:静态输入视图与动态权威状态。
    • 回答思路:对比创建/更新字段差异和并发状态迁移。
    • 详细答案:分组适合决定某场景下哪些字段必须提供,但订单能否从已支付变为已取消取决于数据库当前状态、版本、履约进度和并发请求。把状态机写成校验组既拿不到可靠权威状态,也无法与事务提交原子化,规则会分散且竞态仍存在。
    • 进阶追问:跨字段规则应放哪里?
    • 进阶回答:只依赖当前命令的跨字段结构规则可用对象级约束;依赖外部状态或身份的规则放应用/领域服务,并由数据库约束兜底。
  3. 问题(项目题):支付回调金额怎样校验?

    • 考点:结构、签名、权威金额和幂等顺序。
    • 回答思路:按原始字节验签、字段结构、查支付单、金额币种比较和状态迁移回答。
    • 详细答案:先对原始请求体和签名头按渠道协议验签,再解析金额、币种和渠道流水格式;之后按商户单号加载权威支付单,以最小货币单位比较金额和币种,并校验当前状态。重复成功回调返回幂等确认,金额冲突则冻结处理、告警并进入对账。
    • 进阶追问:金额不一致能直接返回失败让渠道重试吗?
    • 进阶回答:不能只依赖重试。要保存脱敏证据和原始摘要,阻止入账,主动查单并人工核对;重复错误回调可能持续发生。

3.3 统一异常模型、状态码、TraceId(链路标识)与证据保留

统一异常处理不是把所有异常包装成相同 JSON(JavaScript 对象表示法),而是把内部失败分类映射为稳定外部契约,同时保留足够调查证据。响应至少区分 HTTP(超文本传输协议)状态、业务错误码、面向用户消息、可重试提示和 TraceId(链路标识);日志保留异常类型、根因、关键业务标识、阶段和版本,但必须脱敏。ControllerAdvice(控制器增强)只能转换异常,不能吞掉异常后继续提交事务,也不能把失败统一伪装为 200

flowchart TD
    A["请求链抛出异常"] --> B["按异常解析器顺序匹配"]
    B --> C{"已知输入/权限/冲突/依赖/未知?"}
    C -->|"输入"| D["400 + 字段错误码"]
    C -->|"权限"| E["401/403 + 最小信息"]
    C -->|"冲突"| F["409 + 当前状态摘要"]
    C -->|"依赖暂不可用"| G["502/503/504 + 可重试语义"]
    C -->|"未知"| H["500 + 通用消息"]
    D --> I["统一错误响应"]
    E --> I
    F --> I
    G --> I
    H --> I
    A --> J["记录 TraceId(链路标识)、根因和业务键"]
    J --> K["指标、告警与事件证据"]

图解:异常进入分类节点后映射到不同协议状态,同时独立进入证据链;箭头强调“给用户的最小信息”和“给排障的完整证据”并行。前提是业务异常类型稳定且不包含敏感数据。正常路径返回可解释错误;失败路径若没有匹配则使用 500 并保留原异常。业务结论是异常映射不能吞证据,更不能改变原事务失败事实。

异常类别建议状态客户端动作服务端证据
语法/绑定/校验400修正请求,不盲重试字段、规则、请求摘要
未认证/无权限401/403重新认证或停止访问主体、资源、拒绝规则
状态/版本冲突409刷新状态后决策当前版本、期望状态
不存在404检查标识或停止资源类型、脱敏标识
下游网关错误502按幂等策略有限重试下游、耗时、结果确定性
暂不可用/超时503/504遵守退避或查单资源饱和、超时阶段
未知服务器故障500携带请求标识反馈完整堆栈、版本、发布批次

数据演绎 7:200 + success=false 隐藏事故

支付查询接口发生数据库超时,旧处理器返回 200 {success:false}。网关成功率按状态码统计为 99.99%,客户端把 200 缓存 5 分钟,真实失败 820 次未触发告警。改造后超时返回 503、业务码 PAYMENT_QUERY_UNAVAILABLE、请求标识 R-71Retry-After=2;指标区分协议失败与领域拒绝,客户端只对幂等查询退避重试。若支付状态未知,响应明确 UNKNOWN 并要求后续查单,而非伪造失败终态。

热门面试题

  1. 问题(基础题):统一异常处理应该统一哪些内容?

    • 考点:外部契约与内部证据分离。
    • 回答思路:列状态、业务码、消息、请求标识和日志证据。
    • 详细答案:应统一异常分类到 HTTP(超文本传输协议)状态和业务码的映射、错误响应结构、请求标识、脱敏策略和日志级别。不同失败不能被压成一个状态;客户端消息保持稳定最小化,内部日志保留根因、阶段和业务键。
    • 进阶追问:堆栈能返回给前端吗?
    • 进阶回答:不能。堆栈可能泄露类名、路径、数据库和配置,只在受控日志与追踪系统保存,并通过请求标识关联。
  2. 问题(原理题):ControllerAdvice(控制器增强)为什么不能保证事务回滚?

    • 考点:异常传播与事务代理边界。
    • 回答思路:说明事务代理先观察异常还是异常先被捕获。
    • 详细答案:事务是否回滚取决于异常是否穿过事务代理及回滚规则。若业务方法内部捕获异常并返回失败对象,事务代理看到正常返回就可能提交;ControllerAdvice(控制器增强)甚至收不到异常。正确做法让失败异常越过事务边界,外层异常处理只负责协议转换。
    • 进阶追问:手工标记回滚能否替代异常设计?
    • 进阶回答:只能用于少数明确场景,普遍使用会隐藏控制流;应优先明确异常类型和边界,并测试最终数据库状态。
  3. 问题(事故题):线上大量 500 如何快速定位到真实异常?

    • 考点:请求标识、异常链和发布关联。
    • 回答思路:先看影响和分布,再按请求标识进入根异常。
    • 详细答案:先按接口、实例、版本、异常分类和时间窗口统计,确认是否与发布或下游异常重合;抽取请求标识查看完整异常链,定位首次业务异常而不是最外层包装。核对响应是否已提交、事务最终状态和下游结果,再决定回滚、限流或降级。
    • 进阶追问:日志没有堆栈怎么办?
    • 进阶回答:先利用追踪、指标、数据库和网关证据缩小范围,受控提高目标异常采样;长期修复是统一异常记录策略并验证告警不只依赖状态码总量。

3.4 Filter(过滤器)、Interceptor(拦截器)、ArgumentResolver(参数解析器)与 AOP(面向切面编程)边界

四类扩展点处于不同坐标。Filter(过滤器)属于 Servlet(服务器小程序)容器链,可处理原始路径、请求体包装、CORS(跨源资源共享)和进入 Spring(Java 应用框架) MVC(网页模型视图控制器)之前的安全入口;Interceptor(拦截器)已知处理器,适合处理器级审计和轻量上下文检查;ArgumentResolver(参数解析器)负责把可信上下文构造成方法参数;AOP(面向切面编程)围绕对象方法代理,适合事务、方法授权和业务服务级横切。幂等不能只放一个拦截器:入口可校验键,最终唯一性仍需状态机和数据库约束。

flowchart LR
    A["原始请求"] --> B["Filter(过滤器):编码、包装、跨源和粗粒度安全"]
    B --> C["DispatcherServlet(前端控制器)"]
    C --> D["Interceptor(拦截器):处理器上下文和审计"]
    D --> E["ArgumentResolver(参数解析器):构造可信参数"]
    E --> F["控制器"]
    F --> G["AOP(面向切面编程)代理:事务、授权、方法审计"]
    G --> H["应用与领域服务"]
    H --> I["数据库唯一约束和状态机"]
    B -->|"拒绝"| X["协议错误响应"]
    D -->|"短路"| X
    G -->|"异常"| Y["事务回滚并向外传播"]

图解:横向节点按请求层到方法层排列,箭头表示每类扩展点可见的信息逐渐丰富;失败箭头展示容器短路和代理异常的不同后果。前提是每项能力只放在能获得必要证据的最窄层。正常路径层层建立可信输入;失败路径保留状态与异常。业务结论是选择扩展点要按介入位置和保证范围,而不是按“哪个注解写起来快”。

机制可见信息适合职责不适合职责
Filter(过滤器)原始请求/响应、容器分派请求包装、CORS(跨源资源共享)、入口限流领域状态与对象级权限
Interceptor(拦截器)处理器方法、模型接口审计、轻量租户检查可靠消息、资金不变量
ArgumentResolver(参数解析器)参数元数据、可信上下文当前用户、租户、分页对象修改任意请求体业务字段
AOP(面向切面编程)代理方法、参数、返回/异常事务、方法授权、统一方法指标未进入容器的请求、同类自调用旁路
领域/数据库权威状态、事务与约束幂等终局、库存与资金守恒解析媒体类型

数据演绎 8:支付幂等键的分层处理

客户端以 Idempotency-Key=K77 发起扣款。过滤器检查长度和字符集;拦截器确认该接口要求幂等键;参数解析器把可信商户和键组成 M1:K77;应用服务查询幂等记录,数据库唯一索引只允许一行。并发请求 A 插入成功并执行,B 唯一冲突后读取 A 的状态;A 超时但渠道结果未知时记录 PROCESSING/UNKNOWN,B 不得再次扣款,只能查单。单靠拦截器内存集合在多实例和重启后都会失效。

热门面试题

  1. 问题(基础题):Filter(过滤器)和 Interceptor(拦截器)最核心的区别是什么?

    • 考点:容器层与 Spring(Java 应用框架) MVC(网页模型视图控制器)处理器层。
    • 回答思路:从执行位置、可见信息和覆盖范围回答。
    • 详细答案:Filter(过滤器)由 Servlet(服务器小程序)容器调用,位于 DispatcherServlet(前端控制器)外,可覆盖静态资源或其他分派;Interceptor(拦截器)由 Spring(Java 应用框架) MVC(网页模型视图控制器)围绕已映射处理器执行,能看到处理器元数据。二者异常处理和异步回调时机也不同。
    • 进阶追问:鉴权应该放哪一层?
    • 进阶回答:通用认证通常在安全过滤器链,处理器或方法层补细粒度授权,领域层仍需校验资源归属和状态;不能只选一层包办。
  2. 问题(原理题):为什么幂等不能只通过 Interceptor(拦截器)实现?

    • 考点:多实例、崩溃窗口和业务状态。
    • 回答思路:列出入口判重无法覆盖的失败场景。
    • 详细答案:拦截器可以读取幂等键并做前置校验,但无法仅靠进程内状态抵抗多实例并发、重启、事务回滚或外部渠道未知结果。最终保证需要稳定业务键、数据库唯一约束、状态机、结果复用和查单补偿,拦截器只是入口编排。
    • 进阶追问:使用 Redis(远程字典服务)锁是否足够?
    • 进阶回答:锁只能减少并发执行,不能证明业务结果已提交;锁过期或进程崩溃后仍需数据库幂等记录和渠道查单。
  3. 问题(项目题):CORS(跨源资源共享)为什么不等于安全授权?

    • 考点:浏览器读取限制与服务端访问控制。
    • 回答思路:说明执行主体和攻击者能力边界。
    • 详细答案:CORS(跨源资源共享)主要约束浏览器脚本能否读取跨源响应,非浏览器客户端可直接发送请求。即使来源允许,请求仍需认证、对象级授权、防重放和领域校验;支付回调通常也不是浏览器请求,不能依赖 CORS(跨源资源共享)保护。
    • 进阶追问:预检请求失败如何排查?
    • 进阶回答:检查来源、方法、请求头、凭证模式、网关和服务两层配置以及过滤器顺序,并用真实浏览器请求确认响应头,不只用命令行模拟。

4. 大数据与异步请求生命周期

4.1 文件上传、流式下载与堆内存风险

文件接口的首要问题是资源所有权,不是注解。上传链可能在反向代理、Servlet(服务器小程序)容器、多部分解析器、临时磁盘和业务存储任一层限流;若先把文件全部读入 byte[],并发文件会直接占用堆。下载若先生成完整文件再读入内存,同样会把“文件大小 × 并发数”转成内存压力。安全设计需要大小、数量、类型、文件名、压缩比和存储配额限制,并在成功、异常、超时和断连路径清理临时资源。

flowchart TD
    A["客户端文件流"] --> B["网关字节与速率限制"]
    B --> C["Servlet(服务器小程序)容器解析多部分请求"]
    C --> D{"内存阈值内还是落临时盘?"}
    D -->|"小块"| E["受限缓冲区"]
    D -->|"大块"| F["临时文件"]
    E --> G["类型、摘要和业务权限校验"]
    F --> G
    G -->|"失败"| X["删除临时资源并返回错误"]
    G -->|"成功"| H["流式写对象存储或业务存储"]
    H --> I["持久化文件元数据"]
    I --> J["响应文件标识"]
    H -->|"断连/超时"| K["中止写入、标记未知并补偿清理"]

图解:节点表现文件从网络到临时介质再到持久存储的所有权变化;箭头区分内存、磁盘、正常、校验失败和断连路径。前提是每层限制协调且临时目录有配额。正常路径先完成内容校验再提交元数据;失败路径必须清除部分对象和临时文件。业务结论是流式处理降低峰值内存,但仍需总量、速率和生命周期治理。

风险触发方式证据控制措施
堆内存暴涨全量读入字节数组大数组、GC(垃圾回收)停顿固定缓冲流式复制
临时盘写满多部分阈值落盘临时目录增长、磁盘告警配额、限并发、定时清理
压缩炸弹小压缩包高解压比解压字节异常增长文件数、层级、解压比限制
路径穿越使用原始文件名拼路径异常路径、覆盖文件服务端生成标识并规范化名称
慢上传长时间占连接活跃连接高、吞吐低读取超时、速率限制
断连残留客户端取消部分对象、孤儿元数据分段提交、清理任务、状态机

数据演绎 9:并发导出为何触发 OOM(内存溢出)

旧导出一次查询 300000 行,每行对象、字符串和集合平均占 1.2KB,单任务约 360MB;3 个并发任务就超过 1GB,再加工作簿内存和应用常驻对象触发 OOM(内存溢出)。改造后每页 2000 行,单页约 2.4MB,使用流式写入临时文件,最多并发 2 个;任务表记录总行数、已写行数、文件摘要和过期时间。客户端断连只影响下载连接,不删除已完成导出;下载按固定缓冲区传输并在过期后回收文件。

热门面试题

  1. 问题(基础题):为什么文件上传下载不应直接使用大 byte[]

    • 考点:峰值内存、并发放大和复制次数。
    • 回答思路:用文件大小乘并发数计算风险。
    • 详细答案:大字节数组必须连续占用堆,解析、复制和编码还可能产生多份副本。单个 200MB 文件在 10 个并发下就可能造成数 GB 峰值,并触发频繁 GC(垃圾回收)或 OOM(内存溢出)。固定缓冲流式传输能把内存降为与缓冲区和并发数相关。
    • 进阶追问:流式处理就不会 OOM(内存溢出)了吗?
    • 进阶回答:不保证。元数据、压缩库、工作簿、队列和并发任务仍会占内存,必须同时限制总量、并发、页大小和库的内部缓存。
  2. 问题(原理题):客户端断开下载连接后,服务端任务会自动取消吗?

    • 考点:网络断连与业务执行的独立性。
    • 回答思路:区分写响应失败、线程中断和后台任务状态。
    • 详细答案:通常不会自动取消。服务端可能在下一次写出时才感知连接异常,后台生成任务甚至与请求线程完全分离。取消需要显式令牌、任务状态和安全检查点;已经提交的数据库或外部副作用不能靠断连回滚。
    • 进阶追问:是否应在断连时立即删除生成文件?
    • 进阶回答:取决于任务协议。可复用的异步导出应保留到过期;仅服务本次流的临时文件应在确认无引用后清理,避免与并发下载竞争删除。
  3. 问题(项目题):WMS(仓储管理系统)百万行导出怎样设计?

    • 考点:分页、快照、任务状态、存储和下载。
    • 回答思路:从异步受理、数据一致性、流式生成和清理回答。
    • 详细答案:接口返回 202 和任务标识,任务记录查询条件快照、权限快照、数据截止时刻和进度;执行器按稳定游标分页读取,流式写对象存储,完成后保存摘要与过期时间。并发按租户配额控制,失败可从检查点重试,下载重新鉴权并记录审计。
    • 进阶追问:分页期间数据变化怎么办?
    • 进阶回答:明确导出语义,可用截止时间和稳定排序形成业务快照;强一致快照成本高,应按业务要求选择并在结果中标明数据时点。

4.2 同步请求、Servlet(服务器小程序)异步、Callable(可调用任务)与 DeferredResult(延迟结果)

同步请求由容器线程一直执行到响应完成。Servlet(服务器小程序)异步允许请求进入异步状态,容器线程返回线程池,后续工作由应用线程或外部事件完成,再进行异步分派并写回。Callable(可调用任务)由框架提交到配置的异步执行器;DeferredResult(延迟结果)本身不执行工作,而是保存一个未来可设置的结果,适合由回调、消息或其他线程完成。二者都不是“自动提升吞吐”:若瓶颈是数据库连接或下游容量,只会把等待移到另一线程池。

sequenceDiagram
    participant C as Client(客户端)
    participant T as ContainerThread(容器线程)
    participant D as DispatcherServlet(前端控制器)
    participant W as WorkerThread(工作线程)
    participant R as AsyncResult(异步结果)
    C->>T: 请求
    T->>D: 首次分派
    alt Callable(可调用任务)
        D->>W: 提交任务
        D-->>T: startAsync 并释放
        W->>R: 计算结果或抛异常
    else DeferredResult(延迟结果)
        D->>R: 注册结果、超时和完成回调
        D-->>T: startAsync 并释放
        W->>R: 外部事件设置结果
    end
    alt 在期限内完成
        R->>T: 异步再分派
        T->>D: 返回值或异常处理
        D-->>C: 响应
    else 超时
        R->>T: 超时分派
        T->>D: 超时响应与清理
        D-->>C: 503/504 或任务状态
    end

图解:参与者区分容器线程、工作线程和结果持有者;箭头展示首次分派、释放、结果设置、再分派和超时。前提是容器启用异步且执行器有界。正常路径由异步再分派回到返回值/异常处理链;失败路径由超时处理形成响应,但后台工作可能仍在运行。业务结论是请求完成状态和工作完成状态必须分别记录。

模式容器线程占用工作来源适用场景主要风险
同步全程占用当前线程短事务、低延迟查询慢下游耗尽容器线程
Callable(可调用任务)首次/再次分派占用框架异步执行器有界阻塞计算或调用执行器饱和、上下文丢失
DeferredResult(延迟结果)首次/再次分派占用任意事件线程等待外部回调或状态变化结果泄漏、重复设置、超时竞态
持久异步任务受理时短暂占用Runner(执行器)或 MQ(消息队列)消费者秒级以上、可恢复任务状态机、重试和幂等复杂度

数据演绎 10:慢轨迹查询的线程与连接变化

容器线程 200,每个渠道轨迹请求平均等待 3s,同步模式每秒到达 100 个时,很快需要约 300 个并发等待,线程池耗尽。改为 Callable(可调用任务)并设置工作线程 80、队列 200 后,容器线程被释放,但渠道并发仍只能是 80;超过队列预算时应快速返回 503,而不是无限排队。若每个工作仍占一个数据库连接,连接池 40 会成为新瓶颈。因此异步优化必须同时计算容器线程、工作线程、连接和下游并发。

热门面试题

  1. 问题(基础题):Callable(可调用任务)和 DeferredResult(延迟结果)有什么区别?

    • 考点:任务执行与结果占位。
    • 回答思路:分别说明谁执行工作、谁设置结果。
    • 详细答案:Callable(可调用任务)表示一段由框架异步执行器执行并返回结果的工作;DeferredResult(延迟结果)是可由任意线程或事件设置的结果容器,创建它的控制器不必执行实际工作。两者完成后都通过异步再分派进入响应处理。
    • 进阶追问:等待 MQ(消息队列)结果适合哪一个?
    • 进阶回答:短时等待可用 DeferredResult(延迟结果)由关联事件设置,但必须有超时、清理和实例路由;长流程更适合返回任务标识让客户端查询,避免持有大量连接。
  2. 问题(原理题):为什么 Servlet(服务器小程序)异步不一定提高吞吐?

    • 考点:资源瓶颈迁移而非消失。
    • 回答思路:列容器线程、工作线程、连接和下游容量。
    • 详细答案:异步只让容器线程不必全程等待,实际工作仍消耗工作线程、数据库连接、内存和下游并发。如果新执行器无界或下游容量不变,请求只是排队位置改变,延迟和内存可能更差。只有容器线程等待是主要瓶颈且其他资源有预算时才有收益。
    • 进阶追问:执行器队列越大越好吗?
    • 进阶回答:不是。大队列把过载隐藏成高延迟并占用内存,应按可接受等待时间计算容量,满载后快速失败或转持久任务。
  3. 问题(故障题):异步请求超时后后台任务还在执行怎么办?

    • 考点:超时、取消和副作用边界。
    • 回答思路:区分可中断计算、远程调用和已提交副作用。
    • 详细答案:超时回调先原子标记请求已结束并清理结果注册;对支持取消的任务发取消信号,在安全检查点响应中断。远程调用和数据库提交未必可撤销,必须用幂等键、结果查证和补偿处理,不能因客户端已收到超时就再次执行。
    • 进阶追问:可以直接调用 Future.cancel(true) 吗?
    • 进阶回答:可以作为取消请求,但只设置中断语义;任务、驱动和下游必须配合,已经发生的外部副作用不会自动回滚。

4.3 线程上下文传播、事务/安全/MDC(映射诊断上下文)与超时取消

异步请求不会自动继承事务、安全上下文、租户、Locale(区域设置)和 MDC(映射诊断上下文)。这些状态常绑定原线程的 ThreadLocal(线程本地变量),线程池任务在另一条复用线程执行;若不显式捕获、最小化传播并在 finally 清理,会出现日志断链、未授权执行或租户串用。数据库事务连接也绑定原线程,跨线程后必须重新进入明确事务边界,不能传递连接或实体会话。

flowchart TD
    A["请求线程上下文 R1/Tenant-A/User-7"] --> B["提交异步任务"]
    B --> C["捕获允许传播的不可变快照"]
    C --> D["工作线程原有上下文必须先清理"]
    D --> E["安装请求标识、租户和最小安全身份"]
    E --> F["重新鉴权并开启新事务"]
    F --> G{"结果、异常、超时或取消"}
    G --> H["提交/回滚新事务"]
    H --> I["finally 清除所有上下文"]
    I --> J["线程归还线程池"]
    B -->|"直接执行未传播"| X["日志断链或使用默认租户"]
    E -->|"执行后未清理"| Y["下一任务继承旧身份"]

图解:节点展示快照、安装、重新鉴权、新事务和清理;箭头包含缺失传播与未清理两类失败。前提是只传播业务必需且不可变的字段,不复制整个请求对象。正常路径在工作线程建立独立安全与事务边界;失败路径造成上下文缺失或串租户。业务结论是传播和清理同等重要,异步不能沿用原事务幻觉。

上下文是否自动安全继承正确做法错误后果
事务连接工作线程重新经代理开启事务无事务写入、连接跨线程使用
安全身份默认不保证传播最小身份并重新授权越权或匿名执行
租户/仓库命令显式携带并服务端校验串租户、错仓处理
MDC(映射诊断上下文)任务装饰器复制并在结束清理日志断链或污染下一任务
Locale(区域设置)只在确需展示时传播日期、消息语言错误
请求/响应对象不应跨生命周期持有提取不可变字段内存泄漏、响应已关闭

数据演绎 11:线程复用导致串租户

请求 A 在容器线程设置 tenant=T-A,提交任务到工作线程 W1;装饰器只复制没有清理。任务完成后 W1 回池。请求 B 未携带租户且校验遗漏,也被调度到 W1,于是读取到旧值 T-A 并导出 126 条不属于 B 的数据。修复后任务命令必须含服务端确认的租户,执行前重新授权,结束时在 finally 清理;缺租户立即失败。回归测试连续复用同一工作线程执行 A、无租户 B、租户 C,三次日志和数据范围必须完全隔离。

热门面试题

  1. 问题(基础题):异步线程为什么不会自动继承原事务?

    • 考点:事务资源线程绑定。
    • 回答思路:说明事务代理和连接都围绕当前调用线程。
    • 详细答案:本地事务通常由代理在调用线程开启,并把连接等资源绑定到该线程。任务切到线程池后既没有经过原代理调用链,也查不到原线程资源;原请求可能已经提交或回滚。工作线程必须通过受代理的服务方法开启自己的事务。
    • 进阶追问:把数据库连接传给异步线程可以吗?
    • 进阶回答:不可以。连接及会话通常非线程安全,生命周期也属于原事务;应传业务命令,在新线程重新取得资源。
  2. 问题(原理题):MDC(映射诊断上下文)传播为什么必须在结束后清理?

    • 考点:线程池复用和 ThreadLocal(线程本地变量)残留。
    • 回答思路:用同一工作线程执行不同租户任务说明。
    • 详细答案:MDC(映射诊断上下文)底层依赖线程本地状态,工作线程会被后续任务复用。只复制不清理会让下一任务继承旧请求标识或租户,造成日志关联错误,若业务错误依赖该值甚至会串数据。应保存旧值、安装快照并在 finally 恢复或清空。
    • 进阶追问:传播整个请求上下文是否更省事?
    • 进阶回答:不应。请求对象可变且生命周期已结束,还可能携带敏感数据;只传播稳定标识,业务权限在任务执行时重新验证。
  3. 问题(项目题):Runner(执行器)任务如何处理超时和取消?

    • 考点:持久状态、租约、检查点和副作用。
    • 回答思路:区分请求超时与任务取消协议。
    • 详细答案:任务表记录待执行、运行中、取消请求、成功、失败和未知状态;执行器持租约领取,在批次检查点读取取消标记。超时先停止领取新批次并保存进度,已提交副作用靠幂等键避免重复。进程强杀后租约过期,由其他实例从检查点接管。
    • 进阶追问:中断线程能否作为唯一取消机制?
    • 进阶回答:不能。进程重启后中断信号消失,外部调用也未必响应;持久取消状态和幂等检查点才可恢复。

4.4 支付回调、WMS(仓储管理系统)导出、Runner(执行器)任务与事件边界

项目落地要把请求协议、业务状态和跨系统事实分开。支付回调必须在请求体被消费前保存原始字节或安全摘要,并按渠道规范验签;解析后用渠道流水与商户单号做幂等,金额币种与权威支付单核对。WMS(仓储管理系统)导出应把长任务从请求中拆出,用 202、任务状态、检查点和对象存储承接。Runner(执行器)调度必须有租约、重试预算和终态。Spring(Java 应用框架)进程内事件只能解耦同进程监听关系,不能提供 MQ(消息队列)的持久化、确认、重投和跨实例消费保证。

flowchart TD
    A["外部请求或调度触发"] --> B{"业务时长和可靠性要求"}
    B -->|"短且本地可完成"| C["同步应用服务事务"]
    B -->|"短等待外部事件"| D["Servlet(服务器小程序)异步结果"]
    B -->|"长任务可查询"| E["持久任务表 + Runner(执行器)"]
    B -->|"跨服务可靠事实"| F["Outbox(发件箱) + MQ(消息队列)"]
    C --> G["明确业务状态和响应"]
    D --> G
    E --> H["202 + 任务标识,查询终态"]
    F --> I["消费者幂等、重试、死信与对账"]
    A -->|"支付回调"| J["原始字节验签、幂等和查单"]
    J --> C
    F -->|"误用进程内事件"| X["重启或事务提交窗口丢事件"]

图解:决策节点按耗时和可靠性选择同步、请求异步、持久任务或可靠消息;箭头表示保证逐级增强、成本也增加。前提是先定义业务终态和失败恢复方式。正常路径给客户端明确响应或任务标识;失败路径展示把进程内事件误当可靠消息会丢事实。业务结论是技术选择来自状态和恢复要求,不来自“异步更快”的口号。

场景请求响应权威状态幂等键恢复机制
支付回调渠道确认文本/状态支付单、回调记录渠道流水+商户单号主动查单、对账、人工复核
WMS(仓储管理系统)导出202 + 任务标识导出任务表用户+条件摘要+请求键检查点、重试、文件过期清理
Runner(执行器)调度调度确认任务状态与租约任务实例+业务版本租约接管、重试预算、死信
进程内事件通常无外部响应进程内调用状态非可靠边界同进程异常传播或日志
MQ(消息队列)事件接受事实后异步Outbox(发件箱)与消费记录事件标识+业务键重投、幂等、死信、对账

数据演绎 12:支付回调从原始字节到资金终态

T0 收到渠道流水 P900、商户单 M700、金额 1999 分;过滤器用可重复读取包装保存原始 812 字节并计算摘要,不改变空格和字段顺序。T1 用签名头验签通过;T2 事务插入回调唯一键 channel:P900,并加载支付单金额 1999 CNYT3 条件更新 PENDING -> SUCCESS,同时写 Outbox(发件箱)事件 E88T4 提交后返回渠道确认。重复回调唯一冲突后读取已成功结果并再次确认。若 T3 金额不符则不入账,状态置为待核对并主动查单。若返回确认丢失,渠道重试仍不会重复记账。

热门面试题

  1. 问题(基础题):Spring(Java 应用框架)进程内事件和 MQ(消息队列)有什么本质区别?

    • 考点:进程内回调与持久跨系统通信。
    • 回答思路:从持久化、事务窗口、重启和消费确认回答。
    • 详细答案:进程内事件通常在同一应用上下文把事件交给监听器,适合解耦本进程组件,但进程崩溃后无法恢复,也没有天然跨实例确认和重投。MQ(消息队列)提供独立持久化与消费协议;配合 Outbox(发件箱)、幂等和对账才能承载跨服务可靠事实。
    • 进阶追问:事务提交后监听能保证不丢吗?
    • 进阶回答:只能改变监听时机,进程可能在提交后、监听执行前崩溃;可靠跨系统传播仍需持久事件记录和可恢复发布。
  2. 问题(原理题):支付回调为什么要基于原始请求体验签?

    • 考点:规范化差异和消息完整性。
    • 回答思路:说明解析再序列化会改变字节表示。
    • 详细答案:渠道签名通常针对其实际发送的字节或规定拼接串。先反序列化再序列化可能改变字段顺序、空白、数字格式或字符编码,导致误判或产生绕过空间。入口应在请求体首次消费时保存受限原始字节,按渠道版本验签,再解析业务字段。
    • 进阶追问:为了排查能否记录完整回调原文?
    • 进阶回答:应按数据分级处理。保存加密原文或安全摘要并限制访问,普通日志只记录渠道、流水、摘要和结果,禁止泄露卡号、密钥等敏感字段。
  3. 问题(项目题):如何选择同步接口、Servlet(服务器小程序)异步、持久任务和 MQ(消息队列)?

    • 考点:延迟、恢复、耦合和一致性权衡。
    • 回答思路:以完成时长、客户端等待、崩溃恢复和跨服务事实做决策。
    • 详细答案:短且本地可原子完成的用同步;短时等待事件且连接预算可控时用请求异步;秒级以上、需进度取消和重启恢复的用持久任务;跨服务可靠事实用 Outbox(发件箱)加 MQ(消息队列)。每种方案都要定义幂等、超时和终态查询。
    • 进阶追问:为了提升速度是否应全部异步?
    • 进阶回答:不应。异步只改变等待和调度,增加状态、重试和一致性成本;简单强一致用例同步更清晰,只有资源或流程特征需要时才异步。

5. 章节收口与题库使用说明

5.1 综合复述边界

上面 12 个知识小节建立了从协议字节、框架请求链到应用与领域状态的完整模型。下面综合题库用于 3 至 5 分钟口述,不重复计入章节级六字段题。

6. 综合口述题库

  1. 问题(综合题):请完整讲一次 Spring(Java 应用框架) MVC(网页模型视图控制器)请求从进入容器到返回响应的流程。

    口述答案:结论先行:一次请求不是直接进入控制器,而是依次跨越网络容器、过滤器、前端控制器、映射、适配、参数、业务和响应转换等阶段,每阶段有不同失败证据。Servlet(服务器小程序)容器先创建请求与响应对象并执行 Filter(过滤器)链;过滤器可做编码、请求包装、粗粒度安全和 CORS(跨源资源共享),放行后进入 DispatcherServlet(前端控制器)的 doDispatch。它向 HandlerMapping(处理器映射器)传入方法、路径、请求头和媒体条件,得到处理器及拦截器组成的执行链;找不到处理器走 404,条件不满足可能是 405406415。随后选择支持目标处理器的 HandlerAdapter(处理器适配器),按参数逐个调用解析器,必要时经 HttpMessageConverter(消息转换器)、绑定、转换和 Bean(对象实例) Validation(对象校验规范)形成参数,再执行拦截器前置方法和控制器。控制器应只编排应用服务,领域冲突由异常向外传播。正常返回由返回值处理器选择响应体、视图或异步模式;异常由解析器映射为状态码与业务码,但同时保留根因、TraceId(链路标识)和事务结果。最后消息转换器按内容协商写出字节,过滤器完成清理。项目排查时我会按请求标识分别检查“是否进应用、匹配到谁、参数如何形成、事务是否提交、响应是否已写”,不会只看最外层 500

    • 追问 1404 一定由控制器返回吗?直接回答:不一定,常见情况是映射阶段没有处理器,控制器根本未执行。
    • 追问 2:异常处理后还会经过消息转换吗?直接回答:若异常解析器返回响应体对象,仍会通过相应返回值和消息转换机制写出。
    • 追问 3:响应写出失败还能改状态码吗?直接回答:响应一旦提交通常不能可靠修改,只能记录断连或写出异常并处理资源清理。
    • 关联专题完整请求链
  2. 问题(综合题):HandlerMapping(处理器映射器)和 HandlerAdapter(处理器适配器)为什么要分开设计?

    口述答案:结论:两者分开是把“找到谁处理”和“怎样调用它”解耦,使统一调度骨架既能支持不同路由策略,又能支持不同处理器形态。HandlerMapping(处理器映射器)读取 HTTP(超文本传输协议)方法、规范化路径、参数、请求头、消费和生产媒体类型,选择最具体候选并返回 HandlerExecutionChain(处理器执行链);执行链还包含适用于该请求的拦截器。HandlerAdapter(处理器适配器)不重新决定路由,而是判断自己是否支持该处理器,若支持则处理方法参数、数据绑定、校验、反射调用、返回值和异步协议。设计上这是 Strategy(策略)与 Adapter(适配器)的组合:DispatcherServlet(前端控制器)只编排稳定步骤,扩展组件承担变化。失败边界也因此可分层:零候选是映射问题,多个等价候选是歧义,找到处理器却无适配器是调用协议未注册,参数解析失败则已进入适配阶段。项目中支付回调可以用严格媒体和请求头条件与普通用户接口隔离,但原始字节验签仍由入口组件完成;不能指望路由条件替代签名。验证时应在启动期检查歧义映射,对典型请求打印处理器和适配器类型,并对 404/405/406/415 分别做契约测试。这样既能解释源码职责,也能给出事故定位路径。设计评审还要记录映射来源、注册顺序和支持条件,依赖升级后比较候选快照;若同一请求在不同实例落到不同处理器,应视为发布阻断,而不是靠重启碰运气。

    • 追问 1:为什么映射结果包含拦截器?直接回答:拦截器是否适用通常与路径和处理器有关,映射阶段组合后能保证执行规则一致。
    • 追问 2:两个映射器都找到结果怎么办?直接回答:按映射器顺序和具体规则选择;等价冲突应尽早失败,不能依赖偶然注册顺序。
    • 追问 3:适配器是否只能调用注解控制器?直接回答:不是,接口设计允许适配不同处理器类型,注解控制器只是常见实现。
    • 关联专题映射与适配
  3. 问题(综合题):控制器方法参数是怎样被解析、绑定和校验的?

    口述答案:结论:控制器参数不是一次反射赋值,而是“选择来源、读取表示、转换类型、绑定对象、执行结构校验”的责任链。RequestMappingHandlerAdapter(请求映射处理器适配器)把方法包装为可调用模型,对每个参数按顺序询问 HandlerMethodArgumentResolver(处理器方法参数解析器)的支持条件;路径变量从映射变量取值,查询参数从参数集合取值,请求头从头集合取值,自定义当前租户应从可信安全上下文取值,请求体参数则进一步委托 HttpMessageConverter(消息转换器)按 Content-Type 把字节转成对象。文本到日期、枚举和值对象由 ConversionService(转换服务)等机制完成,复杂对象由 WebDataBinder(网页数据绑定器)写入允许字段并收集 BindingResult(绑定结果)。若参数触发 Bean(对象实例) Validation(对象校验规范),框架在调用控制器前检查长度、范围、格式和对象内关系;失败通常映射为 400。边界是这些步骤只建立“结构可信”,不证明用户有权访问订单,也不证明库存足够。项目中批量轨迹查询会先限制请求体和元素数、去重并校验号码格式,再由应用服务按货主权限查询;超大批次转持久任务。验证需覆盖缺字段、错误类型、未知枚举、超长集合、非法嵌套字段和越权资源,确保错误码稳定且控制器未执行时没有开启无意义事务。

    • 追问 1:请求体为什么通常只能读取一次?直接回答:底层是输入流,消费后位置已推进;需要验签和解析共用时必须受限缓存或包装。
    • 追问 2:自定义参数解析器最大的风险是什么?直接回答:支持条件过宽会抢占内置解析器,并可能把不可信请求头变成可信身份。
    • 追问 3:校验失败会进入业务事务吗?直接回答:正常分层下不会,因为应用服务尚未调用;若事务包在控制器外层应重新审视边界。
    • 关联专题参数解析模型
  4. 问题(综合题):HttpMessageConverter(消息转换器)和内容协商怎样工作,常见错误如何区分?

    口述答案:结论:消息转换解决对象与 HTTP(超文本传输协议)主体字节之间的表示转换,内容协商解决服务端应该选择哪一种响应表示,两者与路由和业务版本不同。读取阶段,框架综合方法参数目标类型和请求 Content-Type,按顺序寻找 canRead 为真的 HttpMessageConverter(消息转换器);没有合适转换器通常返回 415,有转换器但字节格式错误通常是 400。写出阶段,框架综合返回值类型、处理器可生产媒体类型和客户端 Accept,选择共同媒体类型并寻找 canWrite 转换器;无法满足可形成 406,写出过程中序列化异常或断连则可能成为 500 或连接错误。内容能转换不等于语义兼容:金额单位、时区、枚举和空值含义变化仍会造成业务事故。项目中跨境物流版本升级会给破坏性变化建立明确版本,兼容窗口内双读并统计调用方,不把同名金额字段从分改成元。证据上记录规范化媒体类型、目标类型、转换器、接口版本和脱敏请求摘要,不记录敏感原文。验证矩阵至少覆盖错误 Content-Type、不可接受 Accept、畸形 JSON(JavaScript 对象表示法)、未知字段、未知枚举和老客户端回放;只有协议和业务守恒都通过才算兼容。工程上还要把转换器顺序和对象映射配置纳入版本审查,防止依赖升级悄悄改变日期、空值或枚举行为;灰度时按调用方版本比较失败率和关键金额守恒,异常立即回退。

    • 追问 1Content-TypeAccept 能否互换?直接回答:不能,前者声明请求主体,后者声明可接受响应,分别作用于读和写。
    • 追问 2:忽略未知字段是否总是向后兼容?直接回答:不是,未知字段若承载安全或金额新语义,忽略可能导致错误决策。
    • 追问 3:为什么不把所有接口都固定返回 200直接回答:这会破坏协议层可观测性和客户端重试语义,业务码不能替代状态码。
    • 关联专题消息转换与内容协商
  5. 问题(综合题):请解释数据绑定的攻击面,以及为什么不能直接绑定数据库实体。

    口述答案:结论:数据绑定是把外部名称和值写入对象属性的便利机制,也会扩大可写字段面;数据库实体同时包含状态、租户、版本和审计字段,直接绑定会造成批量赋值越权和接口契约泄漏。框架先确定目标类型,再由 WebDataBinder(网页数据绑定器)按属性路径选择字段,调用 ConversionService(转换服务)把文本变成日期、枚举和值对象,最后写入目标对象并记录拒绝字段和转换错误。若接口直接接收 OrderEntity,攻击者可能在更新地址时附带 status=SHIPPEDownerId=otherversion,一旦持久层做全字段更新,就绕过领域方法和权限。正确做法是为每个用例定义命令对象,只暴露允许字段;入口设置长度、集合深度和格式预算,应用服务再按可信租户加载权威实体,执行状态机和乐观版本条件更新。允许名单比禁止名单稳健,因为实体新增字段默认不可写。项目中面单修改命令只含订单号、地址和期望版本,仓库、货主和履约状态由服务端查询。验证既要发合法请求,也要主动注入内部字段、嵌套路径、超长集合和未知枚举,确认它们被拒绝且数据库未变化;审计日志只记录字段名和请求标识,不回显敏感值。设计思想是把接口命令当成反腐层:它隔离外部协议、领域模型和表结构,版本升级时可独立转换。若新字段确实可写,也要经过显式用例、权限和状态规则,而非因实体新增属性自动开放。

    • 追问 1:专用命令对象是否会增加很多类?直接回答:会增加显式类型,但换来最小可写面、稳定契约和独立演进,通常值得。
    • 追问 2:只在持久层忽略敏感字段够吗?直接回答:不够,入口应拒绝异常字段并告警,领域层仍需重建权威状态,形成纵深控制。
    • 追问 3:日期转换成功就代表时间正确吗?直接回答:不代表,还要区分绝对时刻、当地规则和业务日期,并保留时区语义。
    • 关联专题数据绑定边界
  6. 问题(综合题):Bean(对象实例) Validation(对象校验规范)与领域校验应如何分工?

    口述答案:结论:Bean(对象实例) Validation(对象校验规范)负责“这个输入对象的形状是否合格”,应用与领域规则负责“当前身份和权威状态下能否执行这次业务”,数据库约束负责并发提交后的最终兜底。结构校验适合非空、长度、格式、数值范围以及只依赖当前命令的起止时间关系;分组可以表达创建和更新字段差异,但组过多会把状态机藏进注解。库存是否足够、订单是否已出库、付款金额是否等于权威应付金额,都需要读取数据库当前版本并与身份和并发顺序结合,不能在请求对象上完成。执行顺序应是字节转换、绑定、结构校验、认证授权、加载聚合、领域方法、数据库条件更新和事务提交。以库存扣减为例,两个请求都携带 qty=8 且通过正数校验,但可售只有 10;必须使用 available>=8 条件更新并检查影响行数,才能让第二个请求返回 409。项目回调中,格式和签名通过后还要核对支付单金额、币种和状态,重复成功则幂等返回。验证要覆盖字段错误、权限失败、状态冲突和并发竞争,分别确认 400/403/409 语义与数据库终态;不能只写控制器校验测试就宣称业务安全。评审时我会为每条规则标注它依赖的数据和最终执行点:只看命令的放结构层,依赖身份的放授权层,依赖当前聚合的放领域层,并发唯一性落数据库。这样可防止同一规则在控制器、服务和表约束间互相矛盾。错误响应还应指出可修正字段而不泄露权威值,领域冲突则返回当前版本或允许动作提示,帮助客户端采取正确下一步。

    • 追问 1:唯一性校验放 Bean(对象实例) Validation(对象校验规范)可以吗?直接回答:可做友好预检查,但并发下仍必须依赖数据库唯一约束并转换冲突。
    • 追问 2:跨字段校验都属于领域规则吗?直接回答:不一定,只依赖当前命令的结构关系可用对象约束,依赖外部状态才属于应用或领域。
    • 追问 3:校验异常应该返回哪个状态?直接回答:结构输入错误通常是 400,状态机冲突通常是 409,权限失败是 401/403
    • 关联专题结构校验与领域不变量
  7. 问题(综合题):怎样设计统一异常处理,既方便客户端又不吞掉证据?

    口述答案:结论:统一异常不是统一成 200 和一条失败消息,而是建立“内部异常分类 -> 协议状态 -> 稳定业务码 -> 最小用户消息 -> 完整内部证据”的映射。解析器应区分绑定校验、未认证、无权限、资源不存在、状态冲突、下游网关错误、暂不可用、超时和未知故障,分别使用合适 4xx/5xx;响应携带 TraceId(链路标识)和是否可重试等必要信息,但不暴露堆栈、数据库或密钥。内部日志保留异常链、首次根因、接口、版本、实例、关键脱敏业务键和事务最终状态,指标按异常类别而非仅状态总量聚合。ControllerAdvice(控制器增强)位于请求外层,只负责协议转换;业务方法内部若捕获异常并返回失败对象,事务代理可能看到正常返回而提交,所以失败必须越过事务边界。响应若已经提交,异常处理器不能假装重写成功,只能记录写出失败并清理资源。项目中支付查询数据库超时应返回 503 和稳定错误码,客户端对幂等查询退避;支付结果未知则显式返回未知并引导查单。验收要注入每类异常,核对响应、日志、指标、事务和告警五份证据一致,并确保敏感信息不会出现在客户端或普通日志。治理上还要给业务码指定所有者和兼容策略,禁止多个异常映射成相同但含义漂移的代码;发布前用契约测试检查状态、业务码、可重试标志和日志级别,避免客户端因服务升级改变动作。对于已提交响应和异步超时,还要单独统计“客户端未收到但业务可能完成”的事件,进入查单或补偿队列,而不是归入普通失败后重试。

    • 追问 1:业务异常需要打印完整堆栈吗?直接回答:可预期拒绝通常记录结构化上下文即可,未知故障和违反不变量应保留堆栈并控制采样。
    • 追问 2:异常映射后原异常还存在吗?直接回答:应存在于日志和追踪证据中,解析器只改变外部表达,不应丢失原因链。
    • 追问 3:为什么 200 + success=false 风险大?直接回答:网关、监控、缓存和通用客户端会误判成功,重试与告警语义被破坏。
    • 关联专题统一异常证据模型
  8. 问题(综合题):Filter(过滤器)、Interceptor(拦截器)、ArgumentResolver(参数解析器)与 AOP(面向切面编程)如何选型?

    口述答案:结论:选型依据是执行位置、可见信息、失败语义和保证范围,而不是哪个扩展点写法最熟。Filter(过滤器)位于 Servlet(服务器小程序)容器链,在 DispatcherServlet(前端控制器)之前,可看到原始请求与分派类型,适合编码、受限请求体包装、CORS(跨源资源共享)、入口限流和通用认证链;它不知道最终控制器领域语义。Interceptor(拦截器)已经拿到处理器执行链,适合接口级审计、轻量租户前置检查和耗时统计,但不适合承担资金或库存不变量。ArgumentResolver(参数解析器)根据参数元数据把可信安全上下文构造成当前用户、租户或分页对象,应严格限定支持条件。AOP(面向切面编程)围绕容器对象方法代理,适合事务、方法授权和应用服务指标,但有同类自调用和不可代理方法边界,也看不到未进入方法的请求失败。最终幂等、资金和库存守恒要落到应用状态机与数据库约束。项目中支付幂等键由入口检查格式、解析器组合可信商户,应用服务和唯一索引决定终局,任何一层都不能单独宣称完成幂等。验证应画出正常、前置短路、方法异常和异步再分派四条路径,确认上下文建立、清理、事务和审计各执行一次。设计上还要为每项横切能力指定唯一主责任层,其他层只提供输入或兜底;否则四处都做日志、鉴权和幂等,会产生重复执行、顺序冲突和异常被提前吞掉,排障反而更困难。

    • 追问 1:日志放过滤器还是拦截器?直接回答:网络请求摘要可在过滤器,处理器与业务标签可在拦截器;要避免重复打印和敏感数据。
    • 追问 2:事务能放 Interceptor(拦截器)吗?直接回答:不应自造请求级事务,事务应围绕应用服务方法和数据库资源,由专用事务代理管理。
    • 追问 3:异步再分派会不会重复执行拦截器?直接回答:可能进入异步相关回调或再次分派,必须按实际分派类型和框架契约验证幂等清理。
    • 关联专题四类扩展点边界
  9. 问题(综合题):接口幂等应该怎样从请求入口一直设计到业务终态?

    口述答案:结论:幂等不是“发现相同请求就返回”,而是同一业务意图在重试、并发、超时和崩溃后只产生一次有效副作用,并能复用或查明结果。入口先定义稳定幂等键来源、长度和作用域,例如商户加客户端请求键,防止不同租户碰撞;参数解析后把键与命令摘要绑定,避免同一键承载不同金额。应用服务在本地事务中插入幂等记录或业务唯一流水,数据库唯一约束解决多实例并发,状态至少区分处理中、成功、失败可重试、失败终止和外部结果未知。成功请求保存可复用响应摘要;并发重复读取处理中状态并返回查询位置,不能再次执行。对支付渠道调用,网络超时不等于扣款失败,状态应进入未知并按商户单号主动查单;只有确认未受理才允许按渠道协议重试。锁可以减少并发,但锁过期、进程崩溃和数据库提交窗口仍需状态机兜底。项目验证要并发发送几十个同键请求、在渠道已受理后模拟断连、在本地提交前后强杀进程,再核对业务流水唯一、金额守恒、重复响应一致和对账可收敛。这样才能说明幂等覆盖了请求、事务和外部副作用,而非一个拦截器缓存。设计评审还应明确幂等记录与业务记录谁是权威、保留多久、处理中卡死怎样接管,以及同键不同参数如何拒绝;指标持续观察重复率、未知状态年龄和人工对账量,才能发现机制是否真的收敛。对于查询不到旧结果的历史请求,必须按业务流水和对账证据恢复,不能因缓存过期便把同一键当成新请求;人工重放也要沿用原键并留下审批审计。

    • 追问 1:幂等记录永久保存吗?直接回答:按业务追溯和最大重试窗口设置生命周期,资金流水通常需长期审计,缓存可有期限但权威记录不能过早删除。
    • 追问 2:失败结果是否复用?直接回答:终止性业务拒绝可复用;暂时性故障和未知结果需按状态机查证,不能简单重复执行。
    • 追问 3:为什么锁不能替代唯一约束?直接回答:锁是过程互斥且可能失效,唯一约束在最终提交点阻止重复事实,是不同层保证。
    • 关联专题幂等分层
  10. 问题(综合题):大文件上传为什么容易引发内存和磁盘事故,应该怎样治理?

口述答案:结论:上传不是单一内存问题,而是网关连接、容器解析、堆缓冲、临时磁盘、内容校验、对象存储和元数据提交组成的资源链,任一层无界都会被并发放大。请求先经过网关的总字节、速率和连接时长限制,Servlet(服务器小程序)容器按多部分配置把小块留在受限缓冲,把大块落到临时目录;业务代码不应调用全量字节读取,而应使用固定缓冲区流式计算摘要、检测类型并写入隔离存储。必须同时限制文件数、单文件大小、总大小、文件名长度、压缩层级、解压后字节和租户配额,不能只信任扩展名或客户端媒体类型。元数据提交应在对象写入确认后发生,失败、校验拒绝、超时和客户端断连都要进入清理路径;若对象存储结果未知,先标记待核对而不是立即重传同一业务文件。临时目录需要容量告警和按状态清理,不能只靠请求结束回调,因为进程强杀时回调不执行。项目中面单附件上传会以服务端生成文件标识,原始文件名仅作展示;敏感文件访问按货主重新鉴权。容量验证用不同大小和并发度压测,观察堆、临时盘、连接、吞吐和孤儿对象,并故障注入断连与存储超时,证明资源最终可回收。设计时还要明确“网络请求、临时文件、对象分段和元数据”四种资源各自的所有者与提交点,清理任务只能删除已过安全期限且没有引用的对象。上线后按租户统计上传字节、失败阶段和清理滞后,防止单租户拖垮共享磁盘。若扫描或转码属于高耗时步骤,应把文件置为隔离待处理状态并异步执行,扫描通过前业务不可引用;这样既不阻塞上传连接,也不会让未经验证的内容进入正式存储域。

  • 追问 1:流式上传是否完全不占内存?直接回答:不是,会占固定缓冲、解析元数据和库内部缓存,只是峰值不再与完整文件大小线性相关。
  • 追问 2:扩展名和媒体类型能证明文件安全吗?直接回答:不能,两者都可伪造,还要做内容签名、解压预算和隔离处理。
  • 追问 3:临时文件何时删除?直接回答:成功转存或明确失败后删除,并用后台清理任务处理强杀遗留;不能删除仍被异步任务引用的文件。
  • 关联专题文件资源链
  1. 问题(综合题):怎样设计一个不会因百万行数据而 OOM(内存溢出)的导出功能?

口述答案:结论:百万行导出应被设计成有状态、可恢复的异步任务,而不是在一个请求中把全部记录和工作簿放入堆。受理接口校验查询条件、数据权限和租户配额,保存条件快照、数据截止时刻、字段版本和幂等键,返回 202 与任务标识;Runner(执行器)按租约领取任务,使用稳定排序或游标分页,每批例如 2000 行,读取后立即流式写临时文件或对象存储分段,不在内存保留全量对象。任务状态记录总量估计、已处理游标、输出字节、摘要、重试次数和过期时间;失败在安全检查点续跑,重复执行通过任务版本和分段标识避免重复拼接。数据一致性要先定义:业务快照可用截止时间和稳定版本,若要求数据库强一致快照则要评估长事务和清理压力。完成后原子发布文件元数据,下载接口重新鉴权并流式传输;客户端断连不改变任务终态,文件按过期策略回收。容量上限制租户和全局并发,监控堆、GC(垃圾回收)、数据库查询时长、临时盘和对象存储错误。验证以百万行、并发任务、进程强杀、分页中数据变化和下载断连做演练,最终证明无重复漏行、内存峰值受控、任务可接管且过期资源清理。还要把字段模板和权限快照版本写进任务,避免执行期间配置变化导致同一文件前后列定义不一致;任务完成后用总行数、首尾游标和文件摘要进行核对,失败文件不得发布下载地址。这样恢复不是简单“从头再跑”,而是有证据地从已提交边界继续。

  • 追问 1:为什么 offset 大分页不适合超大导出?直接回答:越往后扫描成本越高且数据变更易漂移,稳定游标或主键范围通常更可控。
  • 追问 2:导出任务能否无限重试?直接回答:不能,应有重试预算、错误分类和死信/人工处理,避免坏数据永久占资源。
  • 追问 3:如何证明没有漏行重复?直接回答:使用稳定排序、游标边界、行数与摘要核对,并在数据变化场景回放验证。
  • 关联专题WMS(仓储管理系统)流式导出
  1. 问题(综合题):同步请求、Callable(可调用任务)、DeferredResult(延迟结果)和持久任务怎样选择?

口述答案:结论:选择依据是工作时长、谁产生结果、客户端是否需要保持连接、进程崩溃后是否要恢复,而不是统一追求异步。短且本地可完成、需要即时确定结果的用同步请求,容器线程从参数处理一直执行到响应;若主要是数百毫秒到数秒的阻塞等待,且释放容器线程能改善容量,可用 Callable(可调用任务)把工作交给有界执行器。若结果由外部事件、长轮询或另一线程设置,可用 DeferredResult(延迟结果)保存结果占位并注册超时和完成回调,它本身不执行任务。超过连接可接受时间、需要进度、取消、重试和重启恢复的,应返回 202 与任务标识,把状态持久化,由 Runner(执行器)或 MQ(消息队列)消费者执行。Servlet(服务器小程序)异步只释放容器线程,工作线程、数据库连接和下游并发仍然消耗资源;无界队列会把过载变成延迟与内存事故。项目中轨迹聚合短等待可以使用有界异步,百万行导出必须用持久任务,支付扣款通常同步受理但外部未知结果要查单。验证需测容器线程、工作线程、连接、队列和下游限额,模拟超时、断连和进程重启,并确认业务终态不依赖连接仍存在。评审时我会把每个候选方案的响应期限、最大在途数、崩溃恢复、取消语义、幂等键和观测指标放在同一决策表,只有明确收益大于状态管理成本才引入异步。这样避免把架构选择简化成“线程释放了所以更快”。还要明确客户端协议:同步超时后如何查证,异步结果到期后如何获取,持久任务何时可取消;没有这些契约,技术模式再先进也会把不确定性推给调用方。

  • 追问 1:DeferredResult(延迟结果)会自己创建线程吗?直接回答:不会,它只是结果容器,工作由事件源或应用线程产生。
  • 追问 2:请求异步能跨进程恢复吗?直接回答:通常不能,状态主要在当前进程内;需要恢复时应使用持久任务或可靠消息。
  • 追问 3:同步一定比异步性能差吗?直接回答:不一定,短任务同步开销更低且语义清晰,异步增加调度、上下文和状态管理成本。
  • 关联专题异步请求模式
  1. 问题(综合题):Servlet(服务器小程序)异步请求的完整生命周期是什么?

口述答案:结论:异步请求至少经历“首次分派、进入异步、应用工作、结果或超时、再次分派、响应完成”六个阶段,释放原容器线程不等于请求已结束。首次分派仍经过过滤器、DispatcherServlet(前端控制器)、映射、参数和控制器;当控制器返回 Callable(可调用任务)或 DeferredResult(延迟结果)时,框架调用异步启动并保存必要处理状态,原容器线程返回池。Callable(可调用任务)由指定执行器运行,DeferredResult(延迟结果)等待事件线程设置结果。结果完成后容器触发异步再分派,新的容器线程重新进入调度流程,框架取出并处理返回值或异常;若到期未完成,超时回调产生错误响应或任务状态。完成、超时和网络断连可能并发竞争,因此结果设置必须原子,只允许一个终态,并移除注册表避免内存泄漏。后台工作不会因响应超时自动停止,已经提交的数据库或渠道副作用更不会撤销;取消只能作为协作信号,在安全检查点生效。项目中长轮询轨迹更新会为每个查询登记到期时间和最大等待数量,实例关闭前停止新登记并完成或迁移在途。验证应记录两次分派的线程、分派类型、拦截器回调、结果设置者和完成原因,覆盖结果与超时同时发生的竞态。实现上还要区分首次分派和异步分派,确保鉴权、幂等和审计不会重复产生副作用;完成回调只做有界清理,不能再启动不可恢复业务。停机时登记表和执行器应先停止接收,再给在途明确超时或接管结果。

  • 追问 1:异步再分派会重新路由吗?直接回答:会再次进入容器和框架分派,但框架保留异步处理结果以继续返回值或异常链。
  • 追问 2:超时回调一定早于任务完成吗?直接回答:存在竞态,必须用原子终态确保只一个结果生效,另一方只做幂等清理。
  • 追问 3:过滤器会执行几次?直接回答:取决于分派类型配置,异步分派可能再次经过;关键逻辑要按实际分派类型避免重复副作用。
  • 关联专题Servlet(服务器小程序)异步时序
  1. 问题(综合题):为什么把慢接口改成异步后,系统吞吐可能没有提升甚至更差?

口述答案:结论:异步只改变等待发生的位置,不能创造数据库连接、下游配额或计算能力;若没有资源预算,系统会从容器线程耗尽变成工作线程、队列、连接或内存耗尽。同步模式中,容器线程在下游等待,容量大致受线程数和平均时长约束;改用 Callable(可调用任务)后,容器线程很快释放,但任务进入工作线程池。如果每个任务仍占数据库连接和渠道连接,连接池 40 或渠道并发 50 仍是硬上限;把执行器开到 500 只会让更多线程阻塞并增加切换。无界队列在到达率持续高于处理率时线性增长,最终造成高延迟和 OOM(内存溢出),客户端超时后任务还继续做无效工作。正确治理先画资源链,按服务等级目标配置容器线程、工作线程、队列、连接、下游并发和超时层级;队列满时快速失败、限流或转持久任务,并使用 Little’s Law(利特尔法则)估算并发。项目中轨迹渠道平均 3s、每秒 100 请求意味着约 300 个在途等待,但渠道只允许 80,所以服务应限并发 80,其余短队列或拒绝,而非承诺全部实时。验证用阶梯压测观察吞吐拐点、排队时间、连接占用和超时后残留任务,证明背压真实生效。设计思想是让过载在最便宜、最可解释的位置显现:入口有配额,队列有等待上限,下游有并发隔离,超时从外到内逐层收紧且留下执行余量。监控要同时展示到达率、完成率和队列年龄,避免只看线程空闲误判健康。压测还要持续到队列达到稳态或明确发散,短时峰值通过不能证明容量;发布门槛应以尾延迟、拒绝率和下游保护效果共同判断。

  • 追问 1:执行器线程数应该等于容器线程数吗?直接回答:没有固定等式,应按任务阻塞比例、下游容量、CPU(中央处理器)和连接预算计算。
  • 追问 2:为什么大队列会掩盖过载?直接回答:请求仍被接受但等待越来越久,成功率暂时正常,最终集中超时并占用大量内存。
  • 追问 3:异步最适合什么瓶颈?直接回答:容器线程被可控阻塞等待占用,而工作和下游资源仍有明确余量的场景。
  • 关联专题异步容量边界
  1. 问题(综合题):异步请求超时、客户端断连和任务取消之间是什么关系?

口述答案:结论:三者是不同事件。请求超时表示服务端为当前连接设置的等待期限到达;客户端断连表示网络响应接收方离开;任务取消是应用向后台工作发出的协作协议。任何一个都不自动证明业务未执行。超时处理应原子关闭响应结果槽,返回 503/504 或任务状态,注销 DeferredResult(延迟结果)监听并记录完成原因;如果任务可取消,再发送取消令牌或线程中断。后台代码要在批次边界检查令牌,并在 finally 释放资源,但数据库提交、外部渠道扣款和已发送消息不可由中断撤销。断连通常到写响应时才被发现,后台任务可能早已完成;因此不能因客户端没收到结果就自动重做。对外部调用超时,状态应区分确认失败与结果未知,使用稳定业务单号主动查单。持久任务取消还需要数据库状态,如 CANCEL_REQUESTED,执行器停止领取新批次并保存检查点,进程重启后仍能看到。项目中导出断连不取消已完成文件,支付断连绝不重新扣款,轨迹查询可取消尚未发出的渠道批次。验证需在提交前、提交后、响应前和响应后四个时点断连,核对任务、事务、外部副作用和重复请求结果。工程上必须定义取消确认:只收到取消请求不等于已取消,只有执行器到达安全点并记录终态后客户端才能展示完成。若副作用已发生,应返回“不可取消或已部分完成”,交由补偿流程处理,而不是篡改历史状态。运营界面要区分请求超时、任务仍运行、取消处理中和结果未知,避免客服根据一个红色失败图标重复触发业务。

  • 追问 1Future.cancel(true) 是否保证任务停止?直接回答:不保证,只请求中断;代码和阻塞库必须响应,已发生副作用不会回滚。
  • 追问 2:超时后还能设置 DeferredResult(延迟结果)吗?直接回答:结果槽可能已完成,设置应失败或被忽略;业务侧仍需处理任务自己的终态。
  • 追问 3:客户端重试如何避免重复副作用?直接回答:使用幂等键、业务状态机和外部查单,不依赖旧连接状态。
  • 关联专题超时与取消
  1. 问题(综合题):异步线程为什么不会自动继承事务、安全上下文和 MDC(映射诊断上下文)?

口述答案:结论:这些上下文通常依赖当前线程的 ThreadLocal(线程本地变量)或资源绑定,线程池切换后既没有相同线程,也没有相同生命周期;盲目复制还会带来串租户和权限过期风险。事务代理在原请求线程开启本地事务,把数据库连接绑定该线程,方法返回后提交或回滚;异步工作在线程池运行时原事务可能已结束,传递连接既非线程安全也破坏资源所有权。安全身份、租户和 MDC(映射诊断上下文)同样不会自动出现,工作线程还可能残留上一个任务的值。正确模式是提交任务时捕获最小不可变快照,例如请求标识、经服务端确认的租户和主体标识;执行前先清理线程旧值,安装快照,再按当前权限重新鉴权,并通过受代理应用服务开启新事务。结束时无论成功、异常、超时都在 finally 恢复或清除,避免污染下一任务。不能传整个请求对象、响应对象、持久化会话或可变安全对象。项目中 WMS(仓储管理系统)导出任务把租户、仓库、条件摘要和发起人写入任务表,执行时重新校验权限,不依赖已结束网页登录会话。验证强制复用同一线程依次执行租户 A、无租户和租户 B,核对日志、数据库范围和清理结果,并确认每个任务有独立事务。上下文传播还要纳入安全审计:哪些字段允许跨线程、由谁生成、何时过期、失败是否默认拒绝都必须明确。任务日志同时记录原请求标识和独立任务标识,既能串起因果,又避免把长任务误认为仍属于原连接。

  • 追问 1:使用可继承线程本地变量能解决吗?直接回答:在线程池中线程早已创建且复用,继承时机不匹配,也不能解决清理和权限变化。
  • 追问 2:为什么要重新鉴权?直接回答:任务执行时权限或资源归属可能已变化,旧请求快照只能用于追踪,不能永久授权。
  • 追问 3:上下文装饰器最重要的代码是什么?直接回答:不是复制,而是 finally 中恢复或清空,防止线程复用污染。
  • 关联专题线程上下文传播
  1. 问题(综合题):异步任务中的事务边界应该怎样设计?

口述答案:结论:异步任务必须把每个可恢复批次作为独立事务单元,不能试图把请求线程事务跨线程、跨秒甚至跨进程延续。请求只负责校验并在短事务中创建任务记录或 Outbox(发件箱),提交后执行器才领取;执行线程通过受代理应用服务重新开启事务,加载当前任务版本,确认租约和取消状态,处理一个有界批次,保存业务结果与检查点后提交。外部远程调用不宜长时间放在持锁事务中;可先记录调用意图,使用稳定请求号调用,返回后在新事务核对并更新。若远程结果未知,任务进入待查证,不得盲目重放。批次失败根据异常分类回滚本批,重试次数和下一执行时间持久化;进程强杀后租约过期,其他实例从最后已提交检查点接管。数据库唯一键或条件状态保证重复领取不会重复写业务事实。项目中轨迹批量同步每次处理 100 个运单,分批保存渠道游标;WMS(仓储管理系统)导出每页提交进度但文件分段也用确定标识。验证在事务提交前后强杀、重复领取和网络超时,检查无半批提交、检查点不越过业务事实、租约可接管且连接及时释放。批次大小不是固定经验值,要通过锁时间、连接占用、单批重放成本和下游限额共同确定;过大失去恢复价值,过小增加提交和调度开销。监控必须关联任务版本、批次号、事务耗时和未知结果年龄,才能判断卡在本地还是外部。任何人工修复也必须从任务状态机入口执行,禁止直接改检查点跳过失败批次,否则业务数据与进度会永久失配。

  • 追问 1:一个任务一个大事务有什么问题?直接回答:长时间占连接和锁,失败回滚成本高,无法从检查点恢复,还扩大数据库清理压力。
  • 追问 2:检查点能先于业务提交保存吗?直接回答:不能,否则恢复会跳过未完成数据;应与本批业务事实同事务或可证明原子关联。
  • 追问 3:远程调用为什么要稳定请求号?直接回答:超时重试时可让对方幂等查证,避免同一业务意图产生多次副作用。
  • 关联专题异步事务边界
  1. 问题(综合题):Runner(执行器)任务如何实现可恢复调度、幂等和取消?

口述答案:结论:可靠 Runner(执行器)不是定时扫描后直接开线程,而是以持久任务状态机、租约、幂等批次、检查点和重试预算为核心。任务创建时保存业务类型、租户、参数快照、幂等键、计划时间和版本;执行器使用条件更新从待执行抢占为运行中,写入持有者和租约到期时间,多实例只能一个成功。运行时把任务拆成可重放批次,每批先检查租约和取消请求,使用任务标识加批次号作为下游幂等键,业务写入与检查点共同提交。成功记录结果摘要和完成时间;可重试错误按指数退避更新下次时间,确定性错误进入失败终态,超过预算进入死信或人工队列。取消不是删除记录,而是写 CANCEL_REQUESTED,执行器在安全检查点停止新批次;已完成副作用不反向假装未发生。进程强杀后租约过期,其他实例验证旧持有者失效并从已提交检查点接管。项目中面单轨迹同步还要按渠道限流,导出任务要清理临时文件,支付类任务未知结果必须查单。验证包括并发抢占、续租失败、强杀接管、重复批次、取消竞态和死信恢复,指标覆盖队列年龄、租约过期、重试分布和终态比例。为了防止误接管,续租和提交都要校验当前持有者与任务版本,旧实例恢复后不能继续写;人工重放必须生成审计记录并沿用原业务幂等键。容量上把扫描、领取和执行分离,防止大量到期任务同时压垮数据库。还应为每类任务定义最大运行时间和僵死判定,不同业务使用独立并发舱,避免一个大导出阻塞支付查单等高优先级恢复任务。

  • 追问 1:租约和数据库锁有什么区别?直接回答:租约是可过期、可接管的业务所有权记录,数据库锁通常随事务结束,不适合覆盖长任务。
  • 追问 2:任务成功后还能重复执行吗?直接回答:调度层应拒绝,业务层仍要用幂等键防止历史消息或人工操作绕过。
  • 追问 3:取消后如何处理已生成的部分文件?直接回答:按状态标记不可发布并进入清理流程,不能让客户端看到半成品,也要保留审计证据。
  • 关联专题Runner(执行器)任务设计
  1. 问题(综合题):支付回调怎样同时处理原始请求体验签、幂等、事务和渠道重试?

口述答案:结论:支付回调要以“先验证消息来源,再解析业务字段,再用本地事务形成唯一事实,最后给渠道确定确认”为主线,任何网络重试都不能重复入账。入口使用有上限的可重复读取请求包装,在请求体第一次消费时保留原始字节或加密存储与安全摘要;验签严格按渠道版本使用原始字节、签名头、时间戳和密钥版本,不能先反序列化再序列化,因为字段顺序、空白和数字格式会改变。验签后解析渠道流水、商户单号、金额和币种,按 渠道+渠道流水 建唯一回调记录,按商户单号加载权威支付单并核对金额、币种和当前状态。本地事务中通过条件更新把 PENDING 改为 SUCCESS,同时保存回调证据和 Outbox(发件箱)事件;提交成功才返回渠道确认。并发重复回调命中唯一约束后读取已有终态并返回相同确认。若金额不符或签名失败,不入账并告警;若数据库提交结果未知,先查本地流水;若渠道结果未知,按稳定单号主动查单,绝不能因本次 HTTP(超文本传输协议)超时再次发起扣款。日志只记录脱敏标识、摘要、密钥版本和 TraceId(链路标识)。验证要覆盖篡改字节、重复并发、事务回滚、提交后响应丢失、密钥轮换和查单收敛,并做金额守恒对账。渠道确认文本和状态也要版本化测试,因为有些渠道把任何非预期响应视为失败并持续重试。密钥轮换期间按签名携带的版本选择旧、新密钥,过渡结束再撤销旧密钥;验签失败率和未知支付年龄必须单独告警。

  • 追问 1:为什么验签失败不能先返回成功再异步处理?直接回答:未证明来源可信就确认会让渠道停止重试,也可能吞掉真实通知;应按协议拒绝并保留证据。
  • 追问 2:回调唯一键只用商户单号可以吗?直接回答:要结合渠道协议;一个商户单可能有多次渠道尝试,通常同时保存渠道流水唯一性和业务状态约束。
  • 追问 3:Outbox(发件箱)事件发布失败怎么办?直接回答:事件与支付状态同事务保存,独立发布器重试;消费者按事件和业务键幂等并通过对账兜底。
  • 关联专题支付回调状态链
  1. 问题(综合题):为什么 HTTP(超文本传输协议)返回 200 不代表业务成功,状态码和业务码如何配合?

口述答案:结论:HTTP(超文本传输协议)状态描述本次协议交互的处理结果,业务码描述领域内更细的原因,二者互补而不是互相替代。200 适合请求已同步成功并形成确定响应;201 可表示资源创建,202 表示已经受理但最终结果尚未完成。输入形状错误通常是 400,未认证和无权限分别用 401/403,资源不存在用 404,当前状态或版本冲突用 409,媒体条件用 406/415。下游错误、服务暂不可用和网关等待超时可根据责任边界使用 502/503/504,未知服务器错误使用 500。响应体再提供稳定业务码、可展示消息、TraceId(链路标识)和必要的重试或查询提示。不能所有失败都返回 200 {success:false},因为网关成功率、缓存、浏览器、通用客户端和重试策略会误判;也不能所有业务拒绝都返回 500,否则客户端会盲重试确定性错误。异步导出返回 202 与任务查询地址,支付结果未知明确 UNKNOWN 而非伪造失败终态,库存不足返回 409。验证时将协议状态、业务码、事务终态和客户端动作做契约矩阵,监控分别统计技术失败、业务拒绝和异步处理中,保证告警能看到真实故障。业务码要稳定且可机器判断,消息可以本地化但不能作为程序分支;可重试标志必须结合幂等能力,不能鼓励客户端重试扣款等未知副作用。缓存策略也要服从状态语义,错误和处理中响应不得被错误长期缓存。

  • 追问 1:业务冲突一定是 409 吗?直接回答:常见但需遵守团队契约;关键是稳定区分可修正输入、权限、冲突和服务器故障。
  • 追问 2:异步任务失败后原 202 是否错误?直接回答:不是,202 只表示当时已受理,最终状态由任务查询或通知表达。
  • 追问 3:错误消息能直接给用户吗?直接回答:应使用稳定、脱敏的用户消息,内部根因通过请求标识关联,不能返回堆栈。
  • 关联专题状态码与异常映射
  1. 问题(综合题):CORS(跨源资源共享)的请求流程和安全边界是什么?

口述答案:结论:CORS(跨源资源共享)是浏览器执行的跨源读取控制协议,不是服务器认证授权,也不能阻止命令行或服务端攻击者直接发请求。简单跨源请求由浏览器直接发送并根据响应中的允许来源决定是否把结果交给脚本;非简单请求会先发送预检,携带目标方法和自定义头,服务端返回允许来源、方法、头、凭证和缓存时间,浏览器通过后才发送实际请求。配置时来源必须精确,携带凭证时不能随意使用通配来源;网关与应用若重复添加冲突头,浏览器仍会拒绝。CORS(跨源资源共享)通常应在安全和请求处理前的过滤器阶段统一处理,预检不应被普通业务认证错误拦截,但实际请求仍要认证、对象级授权、防重放和领域校验。它也不能替代 CSRF(跨站请求伪造)防护:浏览器是否能读取响应和是否能借用户凭证发起副作用是两个问题。项目中后台管理站点只允许明确域名和环境列表,支付回调不是浏览器流量,不依赖 CORS(跨源资源共享)。排查时用真实浏览器查看预检和实际响应头,核对协议、域名、端口、方法、头、凭证模式和缓存;验证恶意来源、无凭证、过期令牌和对象越权仍被服务端拒绝。生产配置应从受控来源生成并区分环境,禁止根据请求中的任意来源动态回显允许头;预检缓存时间改变后要考虑撤销延迟。监控把预检拒绝与实际接口拒绝分开,否则大量浏览器配置错误会掩盖真实权限攻击。发布前还应检查错误响应是否同样携带必要跨源头,否则浏览器只显示泛化错误,真正的认证或校验原因会被隐藏。

  • 追问 1:预检成功是否代表实际请求会成功?直接回答:不代表,只说明跨源条件允许;实际请求仍会经过认证、授权、校验和业务处理。
  • 追问 2:服务端返回了数据但浏览器报 CORS(跨源资源共享)错误,数据是否泄露?直接回答:浏览器脚本通常读不到,但请求可能已产生副作用,因此服务端授权仍不可缺失。
  • 追问 3:为什么本地命令行测试正常、浏览器失败?直接回答:命令行不执行浏览器 CORS(跨源资源共享)策略,需要检查预检和响应头而非只测业务接口。
  • 关联专题入口扩展边界
  1. 问题(综合题):接口版本兼容应该怎样设计和验证?

口述答案:结论:兼容是消费者在旧契约下仍能正确解释并完成业务,不是服务端“还能反序列化”。先把变化分成兼容新增、需要迁移的语义变化和破坏性变化:新增可选字段必须有与旧行为一致的默认语义;字段改名可在窗口期双读并统计旧字段使用;类型、单位、必填规则或核心枚举语义变化应新增字段或明确版本,不能悄悄复用原名。版本可以放路径、请求头或 Media Type(媒体类型),选择后要统一路由、文档、缓存键和监控标签。服务端保存调用方版本和关键原始事实,异步消息也携带事件版本,使历史数据可重放。迁移流程包括发布新版本、消费者契约测试、灰度、双读或双写、调用比例观测、弃用通知、退出门槛和回滚方案。项目中金额字段从分到元不能把 amount=1999 原地改义,应新增 amountMinor 与币种,冲突输入直接拒绝;轨迹新增状态时要给老消费者未知值策略或版本隔离。验证不仅跑新客户端,还要回放真实老请求、未知字段、未知枚举、精度、时区和缓存场景,并核对资金、库存和状态机守恒。只有旧调用比例达到门槛且回放通过,才关闭旧入口。版本治理还应覆盖错误响应和幂等语义,不能只比较成功对象;网关、缓存、文档和消息消费者必须使用同一版本判定。关闭旧版本前保留可回滚发布物和调用方负责人清单,出现遗漏时能快速恢复而非临时修改新协议。对历史数据回放还要固定当时的版本转换器,不能用当前对象模型猜测旧字段;否则兼容测试通过,新系统恢复旧消息时仍可能失败。

  • 追问 1:路径版本和请求头版本哪个更好?直接回答:各有取舍,关键是全链路可路由、可观测、可缓存并有明确退出策略,而非形式本身。
  • 追问 2:新增字段总是兼容吗?直接回答:不是,若字段成为新的必需约束或改变默认决策,老消费者忽略它可能不安全。
  • 追问 3:如何避免永久维护旧版本?直接回答:设弃用日期、观测调用方、提供迁移回放并定义关闭门槛,完成后移除而非无限兼容。
  • 关联专题内容协商与版本兼容
  1. 问题(综合题):Spring(Java 应用框架)进程内事件、请求异步、持久任务和 MQ(消息队列)怎样划分边界?

口述答案:结论:四者解决不同问题。进程内事件用于同一应用上下文中的组件通知,减少直接调用耦合,但事件和监听器仍受当前进程生命周期约束;即使配置提交后监听,进程也可能在数据库提交后、监听执行前崩溃,所以不能承担跨系统可靠事实。请求异步用于暂时释放 Servlet(服务器小程序)容器线程并在同一连接期限内等待结果,状态多在当前实例,重启后难恢复。持久任务把参数、状态、租约和检查点存入数据库,适合长耗时、需要进度取消和接管的工作。MQ(消息队列)提供独立持久传输、确认和重投,适合跨服务事实传播,但仍需 Outbox(发件箱)解决本地状态与消息发布窗口,消费者必须幂等、重试、死信和对账。选择时先问结果是否必须在当前连接返回、是否跨实例、是否要崩溃恢复、是否有多个消费者及一致性级别。项目中对象创建后的本地缓存刷新可用进程内事件,短轨迹等待可用请求异步,百万行导出用持久任务,支付成功通知账务用 Outbox(发件箱)加 MQ(消息队列)。验证分别模拟监听器异常、实例重启、连接超时、任务接管和消息重复,不能用“异步执行成功一次”证明可靠。评审还要比较运营成本:持久任务需要清理和接管,MQ(消息队列)需要分区、消费与死信治理,请求异步需要连接预算。若同步本地调用已能满足强一致和延迟,就不应为“解耦”平白增加最终一致状态。每次选型都要写出故障后的权威查询位置和人工闭环;如果回答不出“重启后从哪里恢复、重复后由谁去重”,说明该异步方案尚未完成设计。

  • 追问 1:进程内事件异步执行后是否等于 MQ(消息队列)?直接回答:不等于,只是换线程,没有独立持久化、确认、重投和跨实例恢复。
  • 追问 2:持久任务是否可以完全替代 MQ(消息队列)?直接回答:单库内任务可用,但跨服务订阅、传输和消费治理仍更适合消息系统,需按边界选择。
  • 追问 3:事务提交后事件有什么价值?直接回答:可避免监听器看到未提交数据,但不消除提交后进程崩溃的丢事件窗口。
  • 关联专题异步机制决策
  1. 问题(排障题):线上接口突然大量 400/404/415,如何按请求链排查?

口述答案:结论:这三类状态多发生在控制器业务之前,排查应从边缘到映射再到参数表示,先证明请求实际长什么样,而不是立即检查数据库。第一步按时间、接口、调用方、网关、应用版本和状态码分布确认影响,保存一条脱敏原始请求的 method(方法)、规范化路径、查询参数键、Content-TypeAccept、请求体长度和 TraceId(链路标识)。404 先确认请求是否到达正确服务、上下文路径与网关改写是否正确,再检查 HandlerMapping(处理器映射器)注册和版本前缀;405 则查方法条件。415 检查客户端声明的媒体类型、控制器消费条件、目标参数类型和 HttpMessageConverter(消息转换器)支持;不能只把头改成 JSON(JavaScript 对象表示法)而实际仍发送表单。400 再沿读取、反序列化、类型转换、绑定和 Bean(对象实例) Validation(对象校验规范)查看具体字段错误,比较发布前后命令对象、枚举和日期规则。若所有实例并非同时发生,检查灰度版本和配置差异。止血可回滚破坏性版本或恢复旧兼容入口,但禁止全局放宽媒体和字段校验。修复后用真实调用方请求回放,覆盖老版本、畸形输入和安全边界,并建立各阶段错误指标,避免以后只有一个 4xx 总数。复盘要解释为什么契约测试和灰度指标没有提前发现,并为调用方版本、媒体类型和字段错误建立独立看板。若兼容开关用于止血,必须限制路径、租户和期限,同时保留严格新契约,不能让临时放宽成为永久安全缺口。

  • 追问 1404 为什么可能是网关问题?直接回答:网关可能改写路径、路由到错误版本或未转发,请求甚至未进入目标应用映射。
  • 追问 2:临时忽略所有未知字段安全吗?直接回答:不安全,可能掩盖字段拼写和安全语义;兼容应限定接口、版本和窗口。
  • 追问 3:如何证明控制器没执行?直接回答:结合处理器映射日志、控制器入口指标、应用服务调用和数据库无事务证据判断。
  • 关联专题请求入口证据
  1. 问题(排障题):异步接口出现超时、日志断链和串租户,怎样止血并定位?

口述答案:结论:这通常是容量预算和线程上下文生命周期同时失控,先降低新增风险,再分别核对请求状态、任务状态和工作线程状态。止血阶段按租户和接口限流,缩小异步队列或暂停非核心任务,必要时回退同步或改为 202 持久任务;不能继续扩大线程池掩盖下游瓶颈。保存现场包括容器线程、工作线程、队列长度、活跃数据库连接、下游耗时、超时数、DeferredResult(延迟结果)登记数量、线程池拒绝、任务终态和发布版本。对单个 TraceId(链路标识)比较首次分派线程、工作线程和再次分派线程,检查任务装饰器是否复制请求标识、可信租户和最小安全身份,是否在 finally 清理;再强制复用同一工作线程执行不同租户,重现污染。事务方面确认异步线程是否重新通过代理开启事务,而不是沿用请求对象或连接。超时路径检查结果与超时竞态是否原子、登记是否移除、后台任务是否响应取消;下游未知结果必须查证。长期修复是显式任务命令、重新鉴权、有界执行器、统一上下文装饰器、持久状态和资源预算。回归需要连续租户隔离、超时竞态、队列饱和、进程强杀和断连演练,指标证明无线程本地残留和结果泄漏。事故报告还应量化泄露影响范围,按任务和租户审计数据访问,必要时冻结下载并通知安全流程。修复发布采用小流量实例验证上下文清理计数和线程池年龄分布,再逐步扩容,避免一次性替换失去对照证据。

  • 追问 1:扩大线程池为什么可能更糟?直接回答:会增加连接竞争、上下文切换和下游并发,把明确排队变成全面超时。
  • 追问 2:日志断链只影响排障吗?直接回答:若租户或权限也依赖同类线程上下文,可能同时造成数据隔离和安全事故。
  • 追问 3:如何检测 DeferredResult(延迟结果)泄漏?直接回答:监控当前登记量、完成原因和最长年龄,超时/完成后必须原子移除并做压力回归。
  • 关联专题上下文与超时排障
  1. 问题(项目题):请用 Spring(Java 应用框架) MVC(网页模型视图控制器)串讲支付回调和 WMS(仓储管理系统)异步导出的设计取舍。

口述答案:结论:两个场景共用请求入口能力,但完成语义完全不同。支付回调要求短路径、强幂等和资金可查证:Filter(过滤器)用受限包装保留原始字节,映射严格限制渠道路径、方法和媒体类型;验签通过后解析命令,应用服务按渠道流水和商户单号建立唯一记录,核对权威金额币种,在本地事务中更新支付状态并写 Outbox(发件箱),提交后返回渠道确认。网络超时只形成未知,不再次扣款,靠主动查单和对账收敛。WMS(仓储管理系统)百万行导出则不应持有请求连接:控制器校验结构和权限,在短事务中创建带条件快照、截止时间和幂等键的任务,返回 202;Runner(执行器)持租约领取,以稳定游标分页、流式写文件并保存检查点,完成后发布文件元数据。下载重新鉴权,断连不取消任务,文件到期清理。两者的共同治理是稳定错误码、TraceId(链路标识)、线程上下文清理、有限资源和故障演练;差异是支付围绕外部副作用未知与资金守恒,导出围绕长任务恢复与内存预算。验证支付要并发重复、响应丢失、事务强杀和金额对账;验证导出要百万行、进程接管、数据时点、断连和资源回收。这样能体现我不是只会框架注解,而是从请求生命周期推到业务终态和恢复证据。面试中我还会给出选择依据:短且需要渠道确认的流程保留同步入口,长且可恢复的流程转任务;二者都不依赖请求连接证明成功。上线指标分别围绕资金未知年龄和任务队列年龄,人工兜底也有明确负责人和审计路径。

  • 追问 1:两个场景都用 DeferredResult(延迟结果)可不可以?直接回答:不合适,支付应尽快确认本地处理,导出耗时长且需重启恢复,持久任务更稳健。
  • 追问 2:为什么支付回调写 Outbox(发件箱)?直接回答:让支付状态与待发布事实同事务保存,避免数据库成功但跨服务通知永久丢失。
  • 追问 3:导出为什么需要重新鉴权下载?直接回答:任务创建时权限可能变化,文件地址泄露也不能绕过当前资源访问控制。
  • 追问 4:两套设计最重要的监控是什么?直接回答:支付看未知状态、重复、金额差异和对账;导出看队列年龄、进度停滞、内存、临时盘和清理失败。
  • 关联专题项目机制总览

7. 源码阅读与复习清单

源码阅读以具体项目依赖小版本为准,主线抓“入口、策略选择、状态迁移和失败清理”,不背易变调用栈:

  • 能沿 DispatcherServlet#doDispatch 说明映射、适配、执行、异常解析与响应写回。
  • 能说明 RequestMappingHandlerMapping#getHandlerInternal 的候选匹配职责和歧义边界。
  • 能说明 RequestMappingHandlerAdapter#invokeHandlerMethod 如何组织参数、返回值与异步管理器。
  • 能区分 HandlerMethodArgumentResolver(处理器方法参数解析器)、HandlerMethodReturnValueHandler(处理器方法返回值处理器)和 HttpMessageConverter(消息转换器)。
  • 能说明 WebDataBinder(网页数据绑定器)只建立结构可信,不能替代权限和领域不变量。
  • 能按异常类型解释 400/401/403/404/409/415/500/503/504,并说明为什么不能全部返回 200
  • 能比较 Filter(过滤器)、Interceptor(拦截器)、ArgumentResolver(参数解析器)与 AOP(面向切面编程)的介入位置。
  • 能计算文件大小、页大小、并发数对堆、临时盘和连接的放大效应。
  • 能画出 Callable(可调用任务)与 DeferredResult(延迟结果)的首次分派、释放、工作、再分派和超时。
  • 能解释异步线程为什么不自动继承事务、安全、租户和 MDC(映射诊断上下文)。
  • 能用支付回调原始字节验签、唯一记录、状态机、Outbox(发件箱)、查单和对账完整串讲资金一致性。
  • 能用 202、任务表、租约、检查点、流式文件、重新鉴权和过期清理完整串讲 WMS(仓储管理系统)导出。