AOP(面向切面编程)代理链与扩展机制
本篇对应知识图谱
2.3.2。目标不是背诵注解,而是建立“候选通知器如何被发现、代理何时创建、一次调用怎样穿过拦截器链、哪些调用会绕过代理、如何用证据定位”的完整模型。项目案例绑定支付验签与审计、WMS(仓储管理系统)库存操作、跨境物流接口日志和方法权限。
1. 学习地图、前提与边界
flowchart LR
A["Bean(对象实例)定义与原始对象"] --> B["AutoProxyCreator(自动代理创建器)"]
B --> C{"是否有匹配 Advisor(通知器)"}
C -->|"否"| D["返回原始对象"]
C -->|"是"| E["JDK Dynamic Proxy(JDK 动态代理)或 CGLIB(类代理生成库)"]
E --> F["MethodInterceptor(方法拦截器)链"]
F --> G["目标方法"]
G -->|"正常"| H["返回通知与审计"]
G -->|"异常"| I["异常通知与回滚判断"]
J["this 自调用/final/private/构造阶段"] -."旁路或不可拦截".-> G图解:节点从对象创建走到候选匹配、代理生成和运行时调用;实线表示容器管理的正常路径,虚线表示没有经过代理的失败边界。前提是调用者持有容器最终引用且方法可被代理机制覆盖。业务结论是:AOP(面向切面编程)适合横切编排,不负责替代数据库唯一约束、支付幂等或库存状态机。
| 学习问题 | 必须能解释 | 常见误区 | 最终证据 |
|---|---|---|---|
| 为什么需要 AOP(面向切面编程) | 横切关注点与核心业务分离 | 任何重复代码都做切面 | 调用边界和失败策略清晰 |
| 代理何时创建 | 对象后置处理与早期引用 | 注解本身会执行逻辑 | 最终 Bean(对象实例)类型和通知器列表 |
| 调用怎样执行 | 动态匹配、拦截器链、目标调用 | 只有一个环绕通知 | 链顺序、异常传播和耗时 |
| 为什么失效 | 引用、可见性、代理类型和调用时机 | 再加一个注解 | 调用方、代理身份与方法签名证据 |
1. AOP(面向切面编程)术语、设计思想与适用边界
AOP(面向切面编程)把分散在多个业务方法、但语义相同的横切关注点统一建模。Aspect(切面)是规则集合;Join Point(连接点)是在 Spring(Java 应用框架) AOP(面向切面编程)中可被拦截的方法执行;Pointcut(切点)选择连接点;Advice(通知)描述进入、返回、异常或环绕时做什么;Advisor(通知器)把一个 Pointcut(切点)与一个 Advice(通知)组合成可排序执行单元;MethodInterceptor(方法拦截器)是运行时统一调用协议。设计目的不是“少写几行”,而是让审计、权限、事务和观测拥有一致策略、可测试边界和集中治理。
| 概念 | 回答的问题 | 运行时载体 | 不应承担 |
|---|---|---|---|
| Aspect(切面) | 哪一组横切规则属于同一关注点 | 多个 Advisor(通知器) | 领域状态机 |
| Pointcut(切点) | 哪些方法适用规则 | 类型和方法匹配器 | 业务数据最终校验 |
| Advice(通知) | 在调用前后做什么 | MethodInterceptor(方法拦截器) | 跨系统可靠投递 |
| Advisor(通知器) | 哪条规则如何匹配并执行 | 可排序规则对象 | 替代访问控制模型 |
| Join Point(连接点) | 规则落在哪个执行位置 | 被代理的方法调用 | 字段读写与构造器织入 |
flowchart TD
A["横切需求:权限/审计/事务/指标"] --> B["Aspect(切面)"]
B --> C["Pointcut(切点):选择方法"]
B --> D["Advice(通知):定义动作"]
C --> E["Advisor(通知器)"]
D --> E
E --> F["MethodInterceptor(方法拦截器)"]
F --> G{"目标方法成功?"}
G -->|"是"| H["记录结果与耗时"]
G -->|"否"| I["保留异常并记录失败"]
I --> J["不得吞异常伪造成功"]数据演绎 1:日志切面过宽的成本。 某轨迹服务有 1200 QPS(每秒查询率),切面对每次调用同步序列化 8KB 入参和返回值,单次增加 0.7ms 中央处理器时间与 16KB 临时对象。每秒增加约 840ms 中央处理器时间和 19.2MB 分配量,垃圾回收与日志磁盘同时承压。改为只记录业务键、错误码、采样详情并异步输出后,横切能力仍在,资源成本才可控。
热门面试题
问题(基础题):Aspect(切面)、Pointcut(切点)、Advice(通知)和 Advisor(通知器)分别是什么?
- 考点:声明模型与运行时模型。
- 回答思路:先讲选择,再讲动作,最后说明组合与执行。
- 详细答案:Pointcut(切点)回答“哪些方法”,Advice(通知)回答“何时做什么”,Advisor(通知器)把二者组合成容器可发现、排序和适配的规则;Aspect(切面)是面向开发者的一组横切规则,解析后通常形成一个或多个 Advisor(通知器)。运行时再把 Advice(通知)适配为 MethodInterceptor(方法拦截器)进入代理调用链。
- 进阶追问:Join Point(连接点)是否包括字段访问和构造器执行?
- 进阶回答:Spring(Java 应用框架) AOP(面向切面编程)基于代理,主要覆盖方法执行;字段、构造器等更广连接点需要 AspectJ(Java 切面织入框架)编译期或加载期织入。
问题(原理题):AOP(面向切面编程)为什么不是普通工具方法抽取?
- 考点:控制反转、调用边界和策略治理。
- 回答思路:比较业务主动调用与容器自动拦截。
- 详细答案:工具方法仍由每个业务方法主动调用,容易漏调、顺序不一致或异常分支忘记执行。AOP(面向切面编程)通过稳定切点统一装配规则,调用方只表达业务,横切策略由代理链控制;代价是调用关系变隐式,所以必须约束切点、顺序、异常语义并提供观测证据。
- 进阶追问:哪些逻辑不适合放切面?
- 进阶回答:订单状态迁移、库存扣减、支付幂等和金额校验属于领域不变量,应显式写在业务服务与数据库约束中,不能隐藏在通用切面里。
问题(项目题):支付接口的验签、权限、幂等都能放在一个切面吗?
- 考点:横切边界与业务不变量。
- 回答思路:按入口安全、业务幂等和资金事实拆职责。
- 详细答案:验签和防重放可在明确的支付回调入口统一处理,权限适用于内部管理方法;幂等键、金额币种校验、渠道流水唯一约束和状态迁移必须进入支付领域事务。一个万能切面会混淆信任模型、事务时点与错误码,难以审计。切面只采集统一证据并拒绝明显非法请求。
- 进阶追问:切面验证通过是否可以直接发货?
- 进阶回答:不可以;验签只证明报文来源和完整性,仍要查询或核对渠道事实,并在本地事务中幂等推进支付状态,再由可靠事件驱动履约。
2. AutoProxyCreator(自动代理创建器)识别候选与创建代理的时机
AutoProxyCreator(自动代理创建器)本质是 BeanPostProcessor,在 Bean(对象实例)创建过程中参与实例化前判断、早期引用和初始化后处理。典型实现 AbstractAutoProxyCreator 通过 wrapIfNecessary 查询候选 Advisor(通知器),排除基础设施类,再由 ProxyFactory 选择代理方式。注解切面通常由 AnnotationAwareAspectJAutoProxyCreator 解析为 Advisor(通知器)。代理不是扫描到注解时立即生成,而是在具体目标对象创建到相应阶段时决定;最终放入单例缓存和注入其他对象的应是代理引用。
| 阶段 | 关键方法 | 输入 | 输出或失败 |
|---|---|---|---|
| 候选准备 | findCandidateAdvisors | 容器中的通知器和切面 | 候选规则集合 |
| 适用判断 | findEligibleAdvisors | 目标类型和候选规则 | 匹配并排序后的规则 |
| 初始化后 | wrapIfNecessary | 原始对象 | 原对象或代理 |
| 早期引用 | getEarlyBeanReference | 尚未完成初始化的对象 | 与最终身份一致的早期代理 |
| 代理生成 | createProxy | 类型、接口、规则、目标源 | 代理或创建异常 |
sequenceDiagram
participant F as BeanFactory(对象工厂)
participant A as AutoProxyCreator(自动代理创建器)
participant R as Advisor(通知器)注册表
participant P as ProxyFactory(代理工厂)
F->>A: 初始化后提交目标对象
A->>R: 获取候选 Advisor(通知器)
R-->>A: 候选集合
A->>A: 类匹配与方法匹配
alt 没有匹配规则或属于基础设施
A-->>F: 返回原始对象
else 存在匹配规则
A->>P: 创建代理并挂载拦截器
P-->>A: 返回代理
A-->>F: 返回最终 Bean(对象实例)
end数据演绎 2:启动期候选匹配。 容器有 800 个 Bean(对象实例)、60 个 Advisor(通知器)。若每个目标都进行昂贵反射匹配,理论比较达到 48000 次。框架先做基础设施排除、类级快速匹配并缓存结果;若自定义 Pointcut(切点)每次读取远程配置,会把启动和运行调用都变成网络依赖。正确做法是把配置转成本地不可变规则,并在变更时受控重建。
热门面试题
问题(基础题):代理是在 Bean(对象实例)生命周期哪个阶段创建的?
- 考点:对象后置处理和最终引用。
- 回答思路:说明常规初始化后代理与循环依赖早期代理两条路径。
- 详细答案:常规情况下,AutoProxyCreator(自动代理创建器)在初始化后处理阶段判断并返回代理;发生可解决的单例属性循环依赖时,可能通过早期引用提前生成代理。两条路径都要保证其他 Bean(对象实例)最终看到同一代理身份,而不是一个拿原对象、另一个拿代理。
- 进阶追问:为什么不能等第一次调用再创建?
- 进阶回答:懒创建会让已经注入的引用不一致,也难以保证通知器顺序与类型暴露;代理通常随目标对象创建完成,方法拦截链可在调用时再按需计算。
问题(原理题):
AbstractAutoProxyCreator如何判断是否需要代理?- 考点:基础设施排除、候选查找和适用匹配。
- 回答思路:沿
wrapIfNecessary到候选 Advisor(通知器)解释。 - 详细答案:它先排除 Advice(通知)、Pointcut(切点)、Advisor(通知器)等基础设施和已跳过类型,再按目标类查找候选规则;通过类过滤器和方法匹配器筛出可用 Advisor(通知器),排序后交给代理工厂。没有规则就缓存“不需要代理”并返回原对象,避免重复昂贵判断。
- 进阶追问:目标类还没完全初始化,为什么能做匹配?
- 进阶回答:静态切点主要依赖类型和方法元数据,不需要执行业务方法;动态切点的参数判断留到真实调用时。
问题(故障题):同一类型有的对象被代理、有的没有,怎么查?
- 考点:创建来源、容器边界和过早实例化。
- 回答思路:从是否归容器、对象创建时点和候选规则取证。
- 详细答案:先确认对象是否手工
new、是否由另一个 ApplicationContext(应用上下文)创建,再看目标创建时完整 AutoProxyCreator(自动代理创建器)是否已注册。随后打印最终运行类型、目标类和适用 Advisor(通知器),检查条件配置、切点包名、工厂方法返回类型与过早获取对象。修复应统一创建入口,不是手工包一层代理。 - 进阶追问:为什么过早创建会漏代理?
- 进阶回答:对象创建时后置处理器链尚未完整注册,它可能直接成为依赖并被缓存,后续注册自动代理创建器不会自动替换所有旧引用。
3. JDK Dynamic Proxy(JDK 动态代理)与 CGLIB(类代理生成库)选择
JDK Dynamic Proxy(JDK 动态代理)生成实现目标接口的代理,调用进入 InvocationHandler;CGLIB(类代理生成库)生成目标类子类并覆盖可拦截方法,调用进入方法拦截回调。选型首先看调用者依赖接口还是具体类、目标类和方法是否允许继承覆盖,再看容器版本与配置。不能用“哪个一定更快”作结论,现代运行时中的性能通常远小于业务网络、数据库和切面逻辑成本;正确性、类型语义和可维护性优先。
| 维度 | JDK Dynamic Proxy(JDK 动态代理) | CGLIB(类代理生成库) | 决策影响 |
|---|---|---|---|
| 类型前提 | 至少暴露接口 | 可对子类化的具体类 | 注入点类型必须匹配 |
| 代理关系 | 代理是接口实现,不是目标类子类 | 代理是目标类子类 | instanceof 与类型转换不同 |
| 方法限制 | 只经接口暴露的方法调用 | final(最终关键字)/private(私有访问控制)不能覆盖 | 关键方法可能漏拦截 |
| 构造阶段 | 代理尚未承担业务调用 | 子类构造也不等于切面已就绪 | 构造器内调用不可靠 |
| 性能 | 依赖调用链与运行时优化 | 依赖生成类与调用链 | 必须按真实负载测量 |
flowchart TD
A["目标 Bean(对象实例)"] --> B{"是否强制类代理?"}
B -->|"是"| C["检查类和方法可继承性"]
B -->|"否"| D{"是否暴露业务接口?"}
D -->|"是"| E["JDK Dynamic Proxy(JDK 动态代理)"]
D -->|"否"| C
C -->|"可继承覆盖"| F["CGLIB(类代理生成库)"]
C -->|"final/private"| G["无法按预期拦截"]
E --> H["按接口类型注入"]
F --> I["按类或接口注入"]
G --> J["重构边界或改用织入"]数据演绎 3:错误类型假设。 PaymentServiceImpl 实现 PaymentService 接口,使用 JDK Dynamic Proxy(JDK 动态代理)后最终对象可赋给接口,却不能安全强转为实现类。若 ReportExporter 没有接口且配置使用类代理,final 方法 export 无法被覆盖,审计切面静默缺失。两例都应在启动测试验证最终类型和规则命中,而不是上线后看日志猜测。
热门面试题
问题(基础题):两种代理的根本区别是什么?
- 考点:接口实现与子类覆盖。
- 回答思路:从生成类型和调用入口回答。
- 详细答案:JDK Dynamic Proxy(JDK 动态代理)创建新的接口实现,所有接口方法经
InvocationHandler分派;CGLIB(类代理生成库)创建目标类子类,通过覆盖方法插入拦截。前者要求调用面向接口,后者受继承和方法可见性限制。两者最终都把调用转成 MethodInterceptor(方法拦截器)链。 - 进阶追问:有接口时一定使用 JDK Dynamic Proxy(JDK 动态代理)吗?
- 进阶回答:不能脱离版本和配置断言;强制类代理、框架默认变化或特殊代理工厂都会影响选择,应查看最终代理类型和当前配置。
问题(原理题):为什么 final(最终关键字)方法不能被 CGLIB(类代理生成库)拦截?
- 考点:子类覆盖机制。
- 回答思路:解释代理通过重写方法接管调用。
- 详细答案:类代理必须在生成子类中覆盖方法,再把调用导向拦截器链;final(最终关键字)禁止子类覆盖,因此调用直接落到父类实现。private(私有访问控制)方法对子类不可见,同样不能被覆盖。此限制来自语言和虚拟机方法分派,不是切点表达式能绕过的。
- 进阶追问:改成 public(公共关键字)就一定生效吗?
- 进阶回答:还要保证调用经过代理、切点匹配、对象由容器管理且不是构造阶段或同类自调用。
问题(项目题):如何为支付渠道选择代理类型?
- 考点:接口边界、第三方类和可测试性。
- 回答思路:优先业务端口,适配第三方具体类。
- 详细答案:支付渠道对上暴露稳定业务接口,让调用者依赖端口,通常接口代理即可;第三方客户端若只有具体类,应包在适配器中,而不是直接在其 final(最终关键字)方法上堆切面。验签、指标和审计落在自有适配器公共入口,既能代理,也能统一错误和幂等语义。
- 进阶追问:是否为了代理而给每个类都造接口?
- 进阶回答:不是。接口应表达稳定业务能力;仅为技术代理创建无语义接口会增加维护成本,类代理或显式装饰器可能更合适。
4. Pointcut(切点)匹配、Advisor(通知器)适配与缓存
候选 Advisor(通知器)先经过类过滤,再经过方法匹配。静态 MethodMatcher(方法匹配器)只看方法和目标类型,结果可缓存;动态 MethodMatcher(方法匹配器)还看实参数值,每次调用都要判断。桥接方法、接口方法与实现方法、泛型擦除和注解继承会影响“到底匹配哪个 Method(方法元信息)”。源码主线可从 AopUtils.findAdvisorsThatCanApply、ClassFilter、MethodMatcher 和 DefaultAdvisorChainFactory 理解。切点越宽,代理对象和调用判断越多;安全规则不能依赖模糊命名表达式。
| 匹配方式 | 判断时点 | 成本 | 适用边界 |
|---|---|---|---|
| 类型/包名 | 代理创建时 | 低但容易过宽 | 模块级观测 |
| 方法名/签名 | 代理创建时 | 低 | 稳定接口约定 |
| 注解 | 创建时解析元数据 | 中 | 明确声明事务、审计或权限 |
| 参数动态判断 | 每次调用 | 高 | 少量确需按值选择的规则 |
| 组合切点 | 创建和调用阶段 | 取决于最慢条件 | 必须测试交并逻辑 |
flowchart TD
A["候选 Advisor(通知器)"] --> B{"ClassFilter(类过滤器)匹配?"}
B -->|"否"| X["排除"]
B -->|"是"| C{"MethodMatcher(方法匹配器)静态匹配?"}
C -->|"否"| X
C -->|"是"| D["加入候选拦截器链"]
D --> E{"是否动态匹配?"}
E -->|"否"| F["缓存并直接执行"]
E -->|"是"| G["每次根据参数再判断"]
G -->|"匹配"| H["执行 Advice(通知)"]
G -->|"不匹配"| I["跳过该通知器"]数据演绎 4:动态切点热点。 库存查询方法峰值 5000 QPS(每秒查询率),动态切点为每次请求解析表达式并扫描 20 个参数字段,平均增加 35μs,每秒消耗约 175ms 中央处理器时间;若再同步读取权限配置,延迟会被外部依赖放大。应把权限模型预编译为本地结构,动态判断只读取必要业务键,并用方法授权或显式领域校验承载对象级权限。
热门面试题
问题(基础题):静态切点与动态切点有什么区别?
- 考点:匹配时点与运行成本。
- 回答思路:区分类型方法元数据和实参判断。
- 详细答案:静态切点仅依据目标类型、方法签名或注解,代理创建时即可确定并缓存;动态切点还依赖当前参数,每次调用都必须判断。动态能力更灵活,但增加热点路径成本和测试组合,能用稳定元数据表达时不应使用动态匹配。
- 进阶追问:动态切点不匹配时是否完全没有成本?
- 进阶回答:仍有进入代理、构建或读取拦截器链以及动态判断的成本,只是跳过对应 Advice(通知)。
问题(原理题):为什么接口上有注解,实现类切点可能仍不符合预期?
- 考点:方法解析、代理类型和注解查找策略。
- 回答思路:说明调用方法与最具体实现方法可能不同。
- 详细答案:接口代理拿到的调用 Method(方法元信息)可能来自接口,实际目标方法来自实现类;不同切点和注解解析器对接口、实现、桥接方法的查找规则不同。应明确注解允许放在哪一层,使用框架提供的最具体方法解析,并用真实代理类型做测试。
- 进阶追问:泛型桥接方法会造成什么问题?
- 进阶回答:编译器生成的桥接方法可能让签名、注解或方法匹配出现两层表示,规则应解析到被桥接的用户方法并避免重复通知。
问题(故障题):切点表达式过宽有什么生产风险?
- 考点:代理数量、日志泄漏和性能。
- 回答思路:从创建成本、调用成本和数据安全回答。
- 详细答案:过宽切点会给大量基础服务创建代理,每次调用增加链判断;日志切面可能序列化密码、令牌或完整支付报文,重试切面还可能重复外部副作用。应限定包、接口或明确注解,排除数据对象与基础设施,并用命中数量、单次耗时和敏感字段扫描验收。
- 进阶追问:如何证明切点收窄没有漏功能?
- 进阶回答:建立应命中和不应命中的契约测试,启动时导出关键 Bean(对象实例)的 Advisor(通知器)清单,灰度观察审计覆盖率和异常分布。
5. MethodInterceptor(方法拦截器)链与责任链设计
代理收到调用后,DefaultAdvisorChainFactory 把适用 Advisor(通知器)转换为拦截器列表,ReflectiveMethodInvocation 保存代理、目标、方法、参数、目标类和当前位置。每个 MethodInterceptor(方法拦截器)调用 proceed 才会推进到下一个,最后反射调用目标方法;返回时按调用栈反向退出。这是 Chain of Responsibility(责任链模式)与 Decorator(装饰器模式)的结合:每层既可在前后增强,也可短路、改参、改返回或抛异常,因此能力强,也必须约束语义。
| 拦截器行为 | 合法用途 | 风险 | 项目约束 |
|---|---|---|---|
调用一次 proceed | 事务、指标、审计 | 顺序不当 | 明确优先级 |
不调用 proceed | 缓存命中、拒绝权限 | 业务被短路 | 必须有清晰返回契约 |
多次调用 proceed | 极少数重试场景 | 重复扣款/扣库存 | 外部副作用禁止通用重试 |
| 修改参数 | 标准化或上下文补充 | 签名与审计不一致 | 保留原始摘要和变更原因 |
| 吞异常返回默认值 | 明确降级 | 伪造成功、事务不回滚 | 核心写操作禁止 |
sequenceDiagram
participant C as 调用方
participant P as 代理
participant I1 as 权限拦截器
participant I2 as 审计拦截器
participant I3 as 事务拦截器
participant T as 目标方法
C->>P: reserve(orderId)
P->>I1: proceed(0)
alt 权限拒绝
I1-->>P: 抛出拒绝异常
P-->>C: 失败
else 权限通过
I1->>I2: proceed(1)
I2->>I3: proceed(2)
I3->>T: invoke
alt 目标成功
T-->>I3: 预占结果
I3-->>I2: 提交后返回
I2-->>I1: 记录成功审计
I1-->>C: 返回结果
else 目标异常
T-->>I3: 抛出异常
I3-->>I2: 回滚并继续抛出
I2-->>C: 记录失败后继续抛出
end
end数据演绎 5:重复 proceed 的资金风险。 通用重试拦截器在超时时再次调用 proceed,目标方法第一次已把支付请求送到渠道但响应丢失,第二次若换新商户单号会形成两笔扣款。正确策略是目标方法使用稳定支付意图号,超时进入未知并查单;重试是否安全由业务协议决定,不能由无业务语义的通用切面猜测。
热门面试题
问题(基础题):一次代理方法调用如何走完拦截器链?
- 考点:调用对象、索引推进和反向退出。
- 回答思路:沿代理、链、目标、返回栈解释。
- 详细答案:代理先根据方法和目标类取得拦截器链,创建方法调用对象并从索引
-1开始。每个拦截器执行自己的前置逻辑并调用proceed,索引递增;到链尾才调用目标。目标返回或抛异常后,调用栈按相反顺序退出,各层执行返回、异常或最终逻辑。 - 进阶追问:为什么环绕通知能短路目标方法?
- 进阶回答:因为当前拦截器掌握是否调用
proceed的控制权;不推进就不会进入后续拦截器和目标,所以必须定义短路返回和审计语义。
问题(原理题):拦截器链体现了哪些设计模式?
- 考点:责任链、装饰器与适配器。
- 回答思路:把结构、控制和通知适配分别解释。
- 详细答案:多个拦截器按序把调用传给下一层,体现责任链;每层包围后续调用并增强前后行为,具有装饰器特征;不同 Advice(通知)通过适配器转为统一 MethodInterceptor(方法拦截器)协议。代理工厂则封装代理创建策略,避免业务感知具体实现。
- 进阶追问:为什么不是简单事件广播?
- 进阶回答:广播监听器不能自然控制目标是否继续、返回值和异常传播;拦截链是一条有顺序、有返回栈的同步调用路径。
问题(项目题):库存预占链中权限、事务和审计怎样排序?
- 考点:拒绝成本、事务边界和审计事实。
- 回答思路:先拒绝无权请求,再执行事务,审计包围结果。
- 详细答案:粗粒度身份与权限应在昂贵事务前拒绝;审计层需要包围事务与目标,记录请求业务键、最终成功或异常;事务层紧贴业务写入并依据异常决定回滚。对象级仓库权限仍在业务层校验,库存数据库以条件更新和唯一流水守住不超卖。
- 进阶追问:审计写库失败是否让库存回滚?
- 进阶回答:合规强审计可与业务同事务或阻断;普通观测审计应走可靠异步通道并告警,不能因日志系统抖动拖垮核心库存,需要按业务等级明确策略。
6. 通知顺序、返回路径、异常路径与最终处理
多个 Advisor(通知器)先按 Ordered、@Order 与框架内部规则形成链;外层环绕通知先进入、后退出,内层更靠近目标方法。Before Advice(前置通知)在目标前执行,After Returning Advice(返回通知)只在正常返回执行,After Throwing Advice(异常通知)只在匹配异常时执行,After Advice(最终通知)类似 finally。顺序必须用实际代理链验证,不能只凭注解源文件排列。异常通知若吞掉原异常或改成成功返回,会改变事务回滚、调用方重试和业务状态。
| 场景 | 进入顺序 | 退出顺序 | 业务风险 |
|---|---|---|---|
| 正常返回 | 外层环绕 → 内层环绕 → 前置 → 目标 | 返回通知 → 内层退出 → 外层退出 | 返回值被错误篡改 |
| 目标异常 | 外层环绕 → 内层环绕 → 目标 | 异常通知 → 最终通知 → 逐层抛出 | 异常被吞导致提交 |
| 前置拒绝 | 外层进入 → 权限前置 | 不调用目标,逐层异常退出 | 审计误记为业务失败 |
| 最终通知异常 | 目标已完成 | 覆盖原始异常或成功结果 | 根因丢失、结果未知 |
sequenceDiagram
participant C as 调用方
participant A as Advisor-A(通知器-A)
participant B as Advisor-B(通知器-B)
participant T as 目标方法
C->>A: 调用
A->>A: 环绕前置 A
A->>B: proceed
B->>B: 环绕前置 B
B->>T: invoke
alt 正常返回
T-->>B: result
B->>B: 返回通知 B
B-->>A: 环绕后置 B
A->>A: 返回通知 A
A-->>C: 环绕后置 A
else 抛出异常
T-->>B: exception
B->>B: 异常通知 B,保留原异常
B-->>A: rethrow
A->>A: 异常通知 A 与 finally(最终处理)
A-->>C: rethrow
end数据演绎 6:两个通知器的顺序。 设权限 Advisor(通知器)优先级为 10,事务 Advisor(通知器)为 100,审计 Advisor(通知器)为 200。外部调用先经权限,随后事务,再进入审计和目标;目标抛出库存不足异常时,审计记录失败,事务按异常规则回滚,权限层只传递异常。若审计层捕获异常并返回 false,事务看到正常返回便可能提交,调用方也失去错误类型,故“记录后继续抛出原异常”是关键约束。
热门面试题
问题(基础题):多个切面的执行顺序如何理解?
- 考点:进入顺序和反向退出。
- 回答思路:把每个环绕通知看成嵌套函数调用。
- 详细答案:优先级更高的拦截器通常位于链外层,先执行前置部分;它调用
proceed后进入下一层,目标完成时按调用栈反向返回。因此“先进入”不等于“先执行返回逻辑”。返回、异常和最终通知还受适配器与框架规则影响,关键链路应打印实际 Advisor(通知器)顺序验证。 - 进阶追问:同一优先级会怎样?
- 进阶回答:不要依赖未明确保证的发现顺序;有语义依赖的通知器必须设置不同稳定优先级,并用集成测试固定成功和异常路径。
问题(原理题):为什么吞异常会影响事务?
- 考点:异常传播与事务拦截器观察结果。
- 回答思路:说明事务只根据穿过其边界的返回或异常决定动作。
- 详细答案:事务拦截器包围后续调用;若内层捕获目标异常并返回默认值,事务层观察到的是正常返回,便按正常路径提交。即使日志记录了错误,数据库状态也可能已经提交。必须保留原异常,或显式标记事务回滚并定义调用方能识别的失败结果,不能用空对象伪造成功。
- 进阶追问:异常通知本身抛出新异常有什么问题?
- 进阶回答:新异常可能覆盖最初业务根因和堆栈,应把记录失败作为附加动作;确需转换时保留原异常为 cause(原因)并保持回滚语义。
问题(项目题):支付审计切面如何记录成功与失败?
- 考点:资金事实、敏感信息和异常语义。
- 回答思路:记录稳定业务键和结果,不记录完整敏感报文。
- 详细答案:切面记录支付意图号、渠道、金额摘要、请求标识、耗时和结果分类;签名、令牌与卡信息脱敏。正常返回只表示本地方法结果,渠道超时必须标为未知而非失败;异常路径记录原异常类型后继续抛出。最终资金结果仍由支付流水、渠道查单与对账确定。
- 进阶追问:审计异步发送失败怎么办?
- 进阶回答:关键审计先落本地可靠事实或发件箱,再异步发布;普通诊断日志允许降级但要有丢弃计数和告警,不能无限阻塞支付线程。
7. ExposeProxy(暴露代理)、同类自调用与不可拦截阶段
外部对象拿到代理后调用,才能进入拦截器链;目标对象内部执行 this.otherMethod() 时,this 指向原始目标,不会重新回到代理,所以另一个方法上的事务、权限、缓存或审计可能失效。AopContext.currentProxy() 依赖 ExposeProxy(暴露代理)和线程上下文,能显式回到代理,但把业务代码耦合到 AOP(面向切面编程)基础设施,并在异步线程、构造阶段或非代理调用中失败。优先方案是拆分职责为另一个 Bean(对象实例)、由外部编排调用,或注入明确委托端口;确需更广连接点才评估 AspectJ(Java 切面织入框架)。
| 方案 | 收益 | 代价 | 推荐边界 |
|---|---|---|---|
| 拆分 Bean(对象实例) | 代理边界显式、职责清楚 | 增加类型和接口 | 首选,适合事务/权限边界 |
| 显式委托对象 | 调用关系清楚,可单测 | 需设计端口 | 适合支付渠道与库存动作 |
| 自注入代理 | 改动小 | 循环依赖、阅读困难 | 存量过渡,不作默认方案 |
| ExposeProxy(暴露代理) | 可回到当前代理 | 强框架耦合、线程限制 | 极少数兼容场景 |
| AspectJ(Java 切面织入框架) | 可覆盖非代理连接点 | 构建与诊断复杂 | 有明确织入需求时评估 |
flowchart LR
C["外部调用者"] --> P["代理"]
P --> I["拦截器链"]
I --> T["目标对象 checkout"]
T -->|"this.pay"| M["目标对象 pay"]
M -."未经过代理".-> X["方法级事务/权限/审计不生效"]
T -->|"调用独立 PaymentBean(支付对象)"| P2["支付代理"]
P2 --> I2["支付拦截器链"]
I2 --> S["支付目标方法"]
X --> R["重构边界,不以暴露代理作为默认修复"]数据演绎 7:自调用导致事务边界缩小。 checkout 没有事务,内部 this.reserve() 标注新事务。执行订单更新后调用 reserve,库存写入抛异常;因为自调用绕过事务代理,reserve 并未开启预期事务,若连接处于自动提交,前半写入可能已经落库。将库存预占拆到独立 Bean(对象实例),外部通过代理调用,并用数据库条件更新与唯一流水验证,边界才可观察、可测试。
热门面试题
问题(基础题):为什么同类自调用会让切面失效?
- 考点:代理引用与目标引用。
- 回答思路:画出外部代理调用和
this调用两条路径。 - 详细答案:代理只拦截发送到代理对象的方法调用。外部调用进入代理后,目标方法中的
this已是原始目标对象;它调用同类另一个方法只是普通虚方法调用,不会再次经过外部代理。另一个方法上的 Advisor(通知器)因此没有执行机会,与注解是否存在无关。 - 进阶追问:自调用的方法和外层方法都匹配同一切点呢?
- 进阶回答:只会执行外层入口对应的一次拦截链;内部方法不会单独重新匹配,也不会建立它声明的新传播或权限边界。
问题(原理题):为什么不建议默认使用 ExposeProxy(暴露代理)修复?
- 考点:线程上下文、基础设施耦合和设计边界。
- 回答思路:说明它解决路径但恶化职责和可测试性。
- 详细答案:ExposeProxy(暴露代理)把当前代理放入线程上下文,业务方法再主动获取并调用;代码依赖容器配置,脱离代理或切换线程就失败,也掩盖了本应拆分的职责。它还让调用链更隐式。只有存量兼容且无法立即重构时使用,并增加代理存在性和线程边界测试。
- 进阶追问:异步方法里还能拿到当前代理吗?
- 进阶回答:通常不能依赖,线程上下文不会自动跨线程传播;应把任务委托给容器管理的独立 Bean(对象实例)入口,并显式传递必要业务上下文。
问题(故障题):除了自调用,还有哪些常见旁路?
- 考点:对象来源、方法限制和生命周期时机。
- 回答思路:按引用、签名、时机、线程四类回答。
- 详细答案:手工
new、静态方法、private(私有访问控制)或 final(最终关键字)方法、构造器和初始化阶段调用、错误代理类型、切点不匹配、对象来自另一容器、内部持有原始引用都可能旁路。异步切换线程不会自动重建调用入口。排查要证明调用者实际持有的对象身份。 - 进阶追问:构造器中调用公共方法为何也不可靠?
- 进阶回答:目标仍在构造,容器尚未完成初始化后处理并交付最终代理;此时调用发生在目标内部,不能假设通知器链已建立。
8. 循环依赖中的早期代理、对象身份与一致性
单例属性循环依赖中,容器可能在对象尚未完成初始化时调用 getEarlyBeanReference。AutoProxyCreator(自动代理创建器)可在此返回早期代理,使依赖方注入的引用与最终 Bean(对象实例)一致;初始化后处理会识别已提前代理,避免再包一层不同代理。若某个自定义 BeanPostProcessor(Bean 后置处理器)在早期和最终阶段返回不同包装对象,就会出现原对象、早期代理、最终代理三种身份,造成通知丢失、类型不一致或创建异常。三级缓存是兼容机制,不是鼓励循环设计。
| 身份 | 产生时点 | 应见对象 | 风险 |
|---|---|---|---|
| 原始对象 | 实例化后 | 容器内部创建流程 | 不应直接泄漏给普通依赖 |
| 早期引用 | 属性循环解析时 | 依赖方临时注入 | 必须能收敛为最终身份 |
| 最终代理 | 初始化后 | 单例缓存与所有调用方 | 不能与早期引用分裂 |
| 二次包装代理 | 多个处理器不协调时 | 理论上仍需一致组合 | 顺序不同、类型和通知丢失 |
sequenceDiagram
participant F as BeanFactory(对象工厂)
participant A as Bean-A(对象 A)
participant APC as AutoProxyCreator(自动代理创建器)
participant B as Bean-B(对象 B)
F->>A: 实例化原始 A
F->>F: 注册 A 的三级缓存工厂
A->>F: 解析依赖 B
F->>B: 实例化并注入
B->>F: 解析依赖 A
F->>APC: getEarlyBeanReference(A)
APC-->>F: 早期代理 A'
F-->>B: 注入 A'
B-->>F: B 初始化完成
F->>A: 注入 B 并完成初始化
F->>APC: 初始化后检查
APC-->>F: 复用 A',避免不同代理数据演绎 8:身份分裂。 B 在创建时拿到早期代理 A1,自定义处理器在 A 初始化后又创建最终代理 A2。B 调用 A1 只有事务通知,其他调用方调用 A2 同时有权限与审计,系统出现同一 Bean(对象实例)行为不一致。验证方式是比较引用身份、代理目标和 Advisor(通知器)集合。根治优先打破循环;必须兼容时让所有智能后置处理器遵守早期引用协议。
热门面试题
问题(基础题):循环依赖中为什么需要早期代理?
- 考点:提前引用与最终身份。
- 回答思路:说明依赖方不能拿原对象、最终又换代理。
- 详细答案:A 尚未初始化完成时 B 已需要 A 的引用;若先把原始 A 注入 B,随后 A 又被包装为代理,B 会永久持有绕过切面的原对象。早期代理让 B 从一开始就拿到未来应使用的代理身份,A 完成时再复用同一引用。
- 进阶追问:有早期代理就能解决构造器循环吗?
- 进阶回答:不能。构造器循环在实例尚未产生前就互相等待,没有可暴露对象;原型作用域和禁用循环引用等场景也不适用。
问题(原理题):AutoProxyCreator(自动代理创建器)怎样避免二次代理?
- 考点:早期引用记录和初始化后判断。
- 回答思路:解释同一缓存键下的提前包装记录。
- 详细答案:早期引用阶段创建代理后,处理器记录该对象已按当前 Bean(对象实例)标识提前包装;初始化后再次进入时检查记录,返回同一语义结果而不重复创建另一层代理。其他处理器若改变对象身份,仍可能破坏一致性,所以扩展点必须协同。
- 进阶追问:多层代理一定是错误吗?
- 进阶回答:不一定,显式装饰和不同基础设施代理可能合法;错误在于同一 Bean(对象实例)的不同持有者看见不一致身份或规则顺序,必须能解释和验证。
问题(项目题):支付服务与审计服务循环依赖该怎么处理?
- 考点:依赖方向与事件边界。
- 回答思路:先重构,最后才讨论容器兼容。
- 详细答案:支付服务不应依赖具体审计服务再由审计回调支付。可让支付依赖审计端口或写本地审计事实,由独立发布器异步发送;审计只消费事件,不回写支付核心对象。这样消除双向调用,也避免早期代理下的事务与审计规则分裂。
- 进阶追问:暂时无法重构怎么办?
- 进阶回答:限制为单例属性注入,验证早期和最终引用身份、Advisor(通知器)集合与启动配置,并登记技术债和拆除期限,不能把可启动当设计正确。
9. 多代理嵌套、扩展点、重复增强与设计模式
Spring(Java 应用框架) AOP(面向切面编程)通常把多个 Advisor(通知器)合并到一个代理的拦截器链,但自定义 ProxyFactory(代理工厂)、远程客户端、作用域代理或再次包装可能形成代理套代理。代理嵌套会改变类型暴露、equals、堆栈、顺序和诊断方式;重复注册 AutoProxyCreator(自动代理创建器)或切面可能让同一通知执行两次。扩展时优先实现 Advisor(通知器)、Pointcut(切点)或 MethodInterceptor(方法拦截器),复用统一链;不要在业务 Bean(对象实例)外手工无序包代理。
| 扩展方式 | 适用场景 | 主要风险 | 验收方式 |
|---|---|---|---|
| 自定义 Advisor(通知器) | 稳定方法规则 | 匹配过宽、顺序冲突 | 导出链和命中测试 |
| 自定义 MethodInterceptor(方法拦截器) | 调用前后控制 | 多次推进、吞异常 | 成功/异常/短路测试 |
| 自定义 TargetSource(目标源) | 动态目标或池化 | 生命周期和并发复杂 | 目标获取释放指标 |
| 手工 ProxyFactory(代理工厂) | 独立基础设施对象 | 与自动代理重复 | 对象身份和层数检查 |
| 编译期织入 | 非方法连接点 | 构建、调试和版本复杂 | 织入报告和字节码验证 |
flowchart TD
A["调用方"] --> P1["作用域/远程代理"]
P1 --> P2["AOP(面向切面编程)代理"]
P2 --> C["统一 MethodInterceptor(方法拦截器)链"]
C --> T["目标对象"]
R["重复自动代理或手工包装"] --> P3["第二个 AOP(面向切面编程)代理"]
P3 --> P2
P3 --> X{"同一审计是否执行两次?"}
X -->|"是"| Y["合并 Advisor(通知器)并删除重复包装"]
X -->|"否"| Z["保留并记录层次职责"]图解:正常路径把横切规则合并进一个链;失败分支显示重复包装引起双重执行。前提是每层代理有独立职责且类型契约明确。业务结论是关键支付和库存操作要以业务幂等吸收重复,但不能把重复代理当成正常投递机制。
热门面试题
问题(基础题):多个切面是否会创建多个代理?
- 考点:规则合并与代理嵌套。
- 回答思路:区分同一自动代理体系和外部再次包装。
- 详细答案:同一容器自动代理体系通常把多个 Advisor(通知器)装入一个代理链,而不是每个切面创建一层代理。但作用域代理、远程客户端代理、手工 ProxyFactory(代理工厂)或不同处理器再次包装可能形成嵌套。不能只看类名,要逐层查看代理目标与规则。
- 进阶追问:代理层数越多性能一定越差吗?
- 进阶回答:每层都会增加分派和诊断成本,但真正瓶颈取决于通知逻辑和业务调用;首要问题是语义、顺序和重复副作用,性能需基准和线上指标证明。
问题(原理题):为什么 Advisor(通知器)是更合适的扩展单元?
- 考点:规则组合、适配和统一排序。
- 回答思路:说明匹配与动作可独立复用。
- 详细答案:Advisor(通知器)把 Pointcut(切点)与 Advice(通知)组合,容器可以统一发现、筛选、排序并适配为 MethodInterceptor(方法拦截器)。自定义能力因此能进入同一调用模型,继承现有代理创建、缓存和诊断机制,避免手工代理造成对象身份和生命周期失控。
- 进阶追问:何时需要自定义 TargetSource(目标源)?
- 进阶回答:只有目标需要动态路由、池化或特殊生命周期时考虑;它会改变每次调用获取目标的语义,必须处理并发、释放和故障,不应用来实现普通业务分支。
问题(故障题):审计记录出现两条完全相同数据,如何判断是重复代理?
- 考点:调用标识、代理层和消息重复区分。
- 回答思路:先在单进程调用栈定位,再查异步链路。
- 详细答案:给一次调用绑定唯一标识,记录每个拦截器实例、代理类型、目标身份和进入退出序号;若同一线程同一调用中同一通知进入两次,检查切面重复扫描、父子容器重复注册和手工代理。若切面只执行一次而下游两条,则查消息至少一次与消费幂等,不能混为一谈。
- 进阶追问:业务已有幂等,是否可以忽略重复审计?
- 进阶回答:不能。重复审计会污染合规证据、费用和告警,且可能预示事务或权限也重复执行;应修复代理配置,并让审计事件本身有稳定唯一键。
10. 线上诊断、项目边界与可验证话术
AOP(面向切面编程)故障排查按“调用者引用 → 最终代理类型 → 目标对象 → 候选 Advisor(通知器)→ 实际拦截器链 → 方法签名与可见性 → 调用时机 → 异常和返回语义”取证。可用 AopUtils.isAopProxy、AopUtils.getTargetClass、Advised 查看规则,结合启动条件报告、日志、Trace(链路追踪)和堆栈确认。生产环境避免随意拆代理取目标后直接调用。项目设计上,日志、指标、粗粒度权限和事务编排可用切面;支付资金一致性、库存不超卖、订单履约状态机和外部回调幂等必须由显式领域逻辑与持久化约束保证。
| 症状 | 首要证据 | 可能根因 | 修复方向 |
|---|---|---|---|
| 注解完全不生效 | 调用者实际引用和代理类型 | 手工创建、容器边界、切点未匹配 | 统一创建入口和切点 |
| 同类一个方法生效另一个不生效 | 调用路径和方法修饰符 | 自调用、final(最终关键字)、private(私有访问控制) | 拆分边界或改调用入口 |
| 执行两次 | 代理层与 Advisor(通知器)实例 | 重复扫描、父子容器、手工包装 | 合并规则并去重 |
| 异常后仍提交 | 拦截器顺序与异常传播 | 吞异常、错误回滚规则 | 保留异常并验证事务 |
| 延迟突增 | 切面分项耗时和分配量 | 全量日志、远程权限、动态切点 | 采样、本地规则、预算 |
flowchart TD
A["发现切面未生效或重复"] --> B["确认调用者拿到的对象身份"]
B --> C{"是否 AOP(面向切面编程)代理?"}
C -->|"否"| D["查手工 new/容器边界/过早创建"]
C -->|"是"| E["取得目标类和 Advisor(通知器)列表"]
E --> F{"目标方法是否匹配且可拦截?"}
F -->|"否"| G["查切点、接口方法、final/private"]
F -->|"是"| H["记录实际拦截器顺序与调用栈"]
H --> I{"是否 this 自调用或构造阶段?"}
I -->|"是"| J["重构为外部代理入口"]
I -->|"否"| K["检查异常吞噬、重复代理和动态条件"]
K --> L["灰度回归成功/异常/并发路径"]图解:箭头是证据收集顺序,先证明对象身份,再判断规则和调用路径;失败分支分别落到创建、签名、自调用和链语义。结论是“加注解”不是排查动作,必须证明代理链实际发生了什么。
热门面试题
问题(基础题):如何判断一个 Bean(对象实例)是否被代理?
- 考点:代理类型、目标类和规则清单。
- 回答思路:给出非侵入式检查顺序。
- 详细答案:先记录注入点实际运行类型,使用
AopUtils.isAopProxy区分是否代理,用AopUtils.getTargetClass获取用户目标类型;若对象实现Advised,在受控诊断中查看 Advisor(通知器)列表与顺序。再结合调用栈证明方法是否进入拦截器,不能只凭类名中的代理标记下结论。 - 进阶追问:可以取出目标对象直接调用验证吗?
- 进阶回答:不建议在生产直接调用,取目标会主动绕过全部切面并可能破坏作用域与生命周期;应通过测试入口或只读诊断查看元数据。
问题(原理题):排查切面失效为什么先看调用者,而不是先看注解?
- 考点:消息发送对象决定方法分派。
- 回答思路:强调代理是对象引用层的机制。
- 详细答案:注解只是生成规则的元数据,真正拦截发生在调用发送给代理对象时。同一目标方法从外部代理调用可以生效,从
this、原始引用或手工对象调用就不生效。因此先证明调用者持有谁、对象由哪个容器创建,再查切点和顺序,路径更短且能解释同一方法时灵时不灵。 - 进阶追问:日志显示代理类为何仍可能失效?
- 进阶回答:代理存在不代表当前方法有匹配 Advisor(通知器),也可能方法不可拦截、动态切点不匹配或调用在另一个原始引用上;仍需查看实际链和调用栈。
问题(项目题):如何在面试中讲一次 AOP(面向切面编程)事故?
- 考点:影响、证据、根因、修复和防复发。
- 回答思路:用库存自调用或支付重复审计形成闭环。
- 详细答案:先讲影响与止血,例如库存批量操作审计缺失,暂停高风险入口并保留业务流水;再证明调用者拿到代理但内部
this调用绕过审计与事务;短期改为外部委托,长期拆分职责,并用数据库唯一流水守底。最后增加代理命中契约测试、规则清单与审计覆盖率告警,验证成功、异常和并发路径。 - 进阶追问:怎样证明不是日志系统丢消息?
- 进阶回答:对比方法调用标识、切面进入日志、本地审计事实和异步发布记录;若切面入口都没有而业务流水存在,根因在代理路径,若入口存在但下游缺失才查可靠投递。
知识节收口
以上 10 个知识节已经覆盖声明模型、代理创建、运行调用、失败边界、扩展机制和项目诊断。下面的综合题只组织口述,不再引入新的权威机制定义。
2.3.2 综合面试题库
问题:请从设计思想和运行机制完整解释 AOP(面向切面编程)。
- 口述答案:我的结论是,AOP(面向切面编程)不是注解语法,而是把横切策略从核心业务调用中抽离,并由容器在稳定的方法边界统一装配。Aspect(切面)描述一组规则,Pointcut(切点)选择目标类型和方法,Advice(通知)描述进入、返回、异常或环绕动作,Advisor(通知器)把选择与动作组成可排序单元。容器创建 Bean(对象实例)时,AutoProxyCreator(自动代理创建器)查找适用 Advisor(通知器),通过 ProxyFactory(代理工厂)生成接口代理或类代理,最终注入的是代理引用。调用到达代理后,规则被适配为 MethodInterceptor(方法拦截器),以责任链方式调用
proceed,链尾才执行目标,返回或异常按调用栈反向退出。它适合事务边界、方法权限、审计、指标等横切问题,因为这些规则需要一致、可测试和集中治理;但订单状态、库存余量、支付幂等与金额校验是领域不变量,必须显式落在业务服务、状态机、唯一约束和条件更新中。失败边界包括手工创建对象、同类自调用、final(最终关键字)或 private(私有访问控制)方法、构造阶段和切点不匹配。项目中我会同时验证代理命中、异常传播和资源成本,避免切点过宽、日志泄密或通用重试制造重复副作用。验证时还会给关键方法建立正向和负向样本,把规则标识、目标类型、链顺序和业务结果关联起来,确保框架升级后不是“代理还在但语义已经变化”。 - 追问 1:AOP(面向切面编程)和装饰器有什么区别?
- 回答 1:两者都能包围调用;装饰器通常显式组合对象,AOP(面向切面编程)由容器按切点自动选择并组装多个规则,调用关系更隐式,因此更需要顺序和诊断约束。
- 追问 2:为什么不能把库存校验放通用切面?
- 回答 2:库存校验依赖仓、商品、预占状态和并发写,是领域不变量;通用切面缺少状态语义,也不能替代数据库条件更新。
- 追问 3:怎样证明切面真的执行?
- 回答 3:查看最终代理类型、适用 Advisor(通知器)、调用栈和带业务标识的进入退出记录,并验证成功、异常和旁路路径。
- 详情:AOP(面向切面编程)概念与边界
- 口述答案:我的结论是,AOP(面向切面编程)不是注解语法,而是把横切策略从核心业务调用中抽离,并由容器在稳定的方法边界统一装配。Aspect(切面)描述一组规则,Pointcut(切点)选择目标类型和方法,Advice(通知)描述进入、返回、异常或环绕动作,Advisor(通知器)把选择与动作组成可排序单元。容器创建 Bean(对象实例)时,AutoProxyCreator(自动代理创建器)查找适用 Advisor(通知器),通过 ProxyFactory(代理工厂)生成接口代理或类代理,最终注入的是代理引用。调用到达代理后,规则被适配为 MethodInterceptor(方法拦截器),以责任链方式调用
问题:AutoProxyCreator(自动代理创建器)是怎样把普通对象变成代理的?
- 口述答案:AutoProxyCreator(自动代理创建器)是对象后置处理协议中的基础设施实现,典型抽象类是
AbstractAutoProxyCreator。它不是在扫描到注解时立即创建所有代理,而是在具体 Bean(对象实例)的创建流程中判断。常规路径下,原始对象完成依赖注入和初始化后进入postProcessAfterInitialization,处理器调用wrapIfNecessary:先排除 Advice(通知)、Pointcut(切点)、Advisor(通知器)等基础设施类和已经缓存为跳过的类型,再取得候选 Advisor(通知器),按目标类和方法筛选适用规则并排序。没有规则就返回原对象并缓存结论;有规则则把目标类型、接口、规则和目标源交给ProxyFactory,生成 JDK Dynamic Proxy(JDK 动态代理)或 CGLIB(类代理生成库)代理,代理成为容器最终暴露的 Bean(对象实例)。若发生可解决的单例属性循环依赖,处理器还可能在getEarlyBeanReference阶段提前生成代理,并在初始化后复用同一身份,避免依赖方拿原对象、其他调用方拿代理。排查时我会证明自动代理创建器何时注册、对象是否过早实例化、最终类型与规则清单是否正确,而不是只看注解是否存在。上线前还会抽取关键 Bean(对象实例)的最终引用、目标类和通知器顺序形成基线,升级后自动比较,发现少代理、多代理或顺序漂移立即阻断灰度。 - 追问 1:候选 Advisor(通知器)如何产生?
- 回答 1:既可由配置直接注册,也可由注解切面解析为多个 Advisor(通知器);事务、方法安全等基础设施也会贡献规则。
- 追问 2:没有匹配规则时还会创建空代理吗?
- 回答 2:通常不会,处理器返回原对象并缓存不需要代理的结论,减少后续判断成本。
- 追问 3:过早实例化有什么后果?
- 回答 3:对象可能在完整对象后置处理器链注册前被创建,漏掉注入、生命周期或自动代理,并把原始引用传播给其他对象。
- 详情:代理创建时机
- 口述答案:AutoProxyCreator(自动代理创建器)是对象后置处理协议中的基础设施实现,典型抽象类是
问题:JDK Dynamic Proxy(JDK 动态代理)与 CGLIB(类代理生成库)怎样选择?
- 口述答案:我不会用“有接口永远选 JDK Dynamic Proxy(JDK 动态代理)、没有接口选 CGLIB(类代理生成库)”作为绝对答案,而会先核对当前 Spring(Java 应用框架)版本、代理工厂配置和业务类型契约。JDK Dynamic Proxy(JDK 动态代理)生成一个实现目标接口的新对象,方法调用进入
InvocationHandler,因此调用者应面向接口注入,代理通常不能强转为具体实现类。CGLIB(类代理生成库)生成目标类子类,通过覆盖可拦截方法进入回调,适合没有接口或需要按具体类注入的对象,但 final(最终关键字)类不能被继承,final(最终关键字)和 private(私有访问控制)方法不能覆盖,构造阶段也不能假设代理链已生效。两者最终都要转换为统一 MethodInterceptor(方法拦截器)链,性能差异往往小于数据库、网络和通知本身的成本,不能凭旧结论选型。支付渠道项目中,我优先定义稳定业务端口,让渠道适配器实现接口,第三方具体客户端藏在适配器内部;这样切面落在自有公共入口,验签、指标和异常转换可控,也不会为了代理直接侵入第三方类。验收重点是最终代理类型、注入点兼容性、关键方法可拦截性和异常路径,而不是微基准数字。我还会覆盖按接口和按具体类注入、类型判断、序列化以及框架升级四类回归,确保选型改变不会在运行期才暴露强转异常或规则缺失。 - 追问 1:有接口时可以强制类代理吗?
- 回答 1:可以由配置影响,但必须接受子类限制和类型语义,并核对当前版本行为,不能把配置当成通用默认。
- 追问 2:哪种代理一定更快?
- 回答 2:没有跨版本、跨调用链的固定答案;应测真实通知逻辑、分配量和尾延迟,先保证正确性。
- 追问 3:final(最终关键字)方法怎么增强?
- 回答 3:优先重构到可代理的公共业务边界或显式装饰器;确需非代理连接点时评估 AspectJ(Java 切面织入框架)。
- 详情:代理类型选择
- 口述答案:我不会用“有接口永远选 JDK Dynamic Proxy(JDK 动态代理)、没有接口选 CGLIB(类代理生成库)”作为绝对答案,而会先核对当前 Spring(Java 应用框架)版本、代理工厂配置和业务类型契约。JDK Dynamic Proxy(JDK 动态代理)生成一个实现目标接口的新对象,方法调用进入
问题:一次方法调用在拦截器链中怎样执行?
- 口述答案:调用方首先必须持有代理引用。代理接到方法、参数和目标类型后,通过 Advisor Chain Factory(通知器链工厂)取得当前方法适用的 Advisor(通知器),并把不同 Advice(通知)适配成统一 MethodInterceptor(方法拦截器)列表。
ReflectiveMethodInvocation保存代理、目标、方法、参数、目标类和当前索引,第一次proceed从索引前开始;每个拦截器执行前置逻辑后决定是否继续调用proceed,索引递增,链尾才反射调用目标方法。目标正常返回时,调用栈反向退出,各层执行返回通知、计时结束或提交;目标抛异常时,异常沿相反方向穿过异常通知、事务回滚判断和最终清理。这个模型结合了 Chain of Responsibility(责任链模式)、Decorator(装饰器模式)和 Adapter(适配器)。关键风险是拦截器可以不推进、推进多次、修改参数、替换返回或吞异常。库存和支付写操作中,通用重试切面不能随意多次proceed,因为第一次外部副作用可能已经成功但响应丢失;必须用稳定业务键查证。审计层记录后应保留原异常,事务层才能按真实失败回滚,调用方也能进入正确恢复流程。为验证链语义,我会记录一次调用的规则序号、进入退出、原始异常与最终结果,故障注入短路、重复推进和通知自身异常,确认没有重复副作用且根因堆栈不会被覆盖。 - 追问 1:不调用
proceed有合理场景吗? - 回答 1:有,例如权限拒绝或确定的缓存命中,但必须定义返回契约、审计原因和后续拦截器是否应执行。
- 追问 2:为什么返回顺序与进入顺序相反?
- 回答 2:每层以同步函数调用包围下一层,下一层返回后当前层才继续,天然形成调用栈反向退出。
- 追问 3:目标方法是怎样被调用的?
- 回答 3:链尾通过反射或框架优化路径调用目标;代理本身不包含业务实现,只负责分派和增强。
- 详情:方法拦截器链
- 口述答案:调用方首先必须持有代理引用。代理接到方法、参数和目标类型后,通过 Advisor Chain Factory(通知器链工厂)取得当前方法适用的 Advisor(通知器),并把不同 Advice(通知)适配成统一 MethodInterceptor(方法拦截器)列表。
问题:多个 Advisor(通知器)的顺序和异常传播如何保证正确?
- 口述答案:我先把通知器看成嵌套调用,而不是一排顺序日志。容器筛选适用 Advisor(通知器)后按框架内部优先级、
Ordered和@Order等规则排序;越靠外层的拦截器越先进入,但越晚执行返回或异常后的逻辑。假设权限层在外、审计层居中、事务层靠近目标:权限先拒绝无权请求,审计包围完整业务结果,事务只包围需要数据库原子性的部分。目标成功时事务提交后返回,审计记录成功;目标异常时事务观察到原异常并回滚,审计记录失败后继续抛出。不能依赖源文件中切面声明的先后,也不能让同优先级承担强语义顺序,关键链路要输出实际 Advisor(通知器)列表并做正常、异常、短路测试。最危险的是内层捕获异常返回默认值:外层事务只看到正常返回便可能提交,调用方也把失败当成功;最终通知再抛新异常还可能覆盖最初根因。支付审计中,渠道超时应保留“结果未知”异常或状态,不应转换为“支付失败”;库存不足属于可识别业务拒绝,也要按事务规则和接口契约一致传播。落地时我为每类通知器登记“必须位于谁之外、观察哪种结果、失败是否阻断”的顺序契约,并在启动后输出真实链。回归同时模拟权限拒绝、目标异常、提交异常和审计异常,确认每种情况下数据库、接口错误码、审计事实和告警一致。这样顺序是可验证设计,而不是靠记忆@Order数字。 另外要把通知器自身失败纳入顺序契约:审计失败不能覆盖目标原异常,清理失败必须保留主因,事务提交失败则以数据库终态和恢复记录为准,不能沿用目标方法已返回的成功文案。 - 追问 1:
@Order数值越小代表什么? - 回答 1:通常优先级越高、进入越早,但具体基础设施规则与同序行为要按当前版本和实际链验证。
- 追问 2:审计应该在事务内还是事务外?
- 回答 2:取决于审计是否属于业务原子事实;合规强审计可同事务,普通观测应避免拖长事务并使用可靠异步通道。
- 追问 3:异常转换时应保留什么?
- 回答 3:保留原异常为 cause(原因)、业务键和失败分类,并确保转换后仍符合事务回滚与调用方恢复契约。
- 详情:通知顺序和异常路径
- 口述答案:我先把通知器看成嵌套调用,而不是一排顺序日志。容器筛选适用 Advisor(通知器)后按框架内部优先级、
问题:为什么同类自调用会导致事务、权限或审计失效?
- 口述答案:代理机制的核心不是“方法上有注解”,而是“调用消息发送给谁”。外部调用者拿到容器最终代理,调用先进入代理和 MethodInterceptor(方法拦截器)链,随后才到目标对象。目标方法开始执行后,
this指向原始目标对象;它调用this.reserve()或直接调用同类方法,只是对象内部的普通方法分派,不会重新跳回外部代理,因此内部方法上的独立 Advisor(通知器)没有执行机会。即使外层和内层都匹配同一切点,也只执行外层入口的一次链,内部声明的新事务传播、方法权限、缓存或审计不会单独建立。优先修复是把真正独立的事务或权限边界拆到另一个 Bean(对象实例),让外部编排通过其代理调用;也可以引入显式业务端口和委托对象。自注入和 ExposeProxy(暴露代理)能在部分存量场景绕回代理,但会增加循环依赖、线程上下文和框架耦合,异步线程、构造阶段或非代理入口仍可能失败,不能当默认方案。WMS(仓储管理系统)库存预占中,我会让订单编排调用独立库存服务,数据库条件更新和唯一流水守住不超卖,切面只建立事务和审计边界。 验证闭环不能只看日志是否出现。我会分别从外部代理入口和内部入口调用同一业务动作,记录实际 Advisor(通知器)进入序号、事务是否激活、数据库提交结果与审计流水;再把方法拆到独立 Bean(对象实例)后复测,证明调用路径改变才是生效原因。对于存量临时方案,还要覆盖异步线程和无容器单元测试,防止 ExposeProxy(暴露代理)在另一入口再次失败。 - 追问 1:把内部方法改成 public(公共关键字)能解决吗?
- 回答 1:不能,方法可见性只是可拦截前提;
this调用仍没有经过代理。 - 追问 2:注入自身代理是否推荐?
- 回答 2:只适合作为明确记录的存量过渡;它隐藏依赖并可能形成循环,首选职责拆分。
- 追问 3:如何测试自调用问题?
- 回答 3:分别从外部代理入口和同类内部入口调用,记录 Advisor(通知器)进入与数据库事务状态,验证两条路径差异。
- 详情:自调用失效边界
- 口述答案:代理机制的核心不是“方法上有注解”,而是“调用消息发送给谁”。外部调用者拿到容器最终代理,调用先进入代理和 MethodInterceptor(方法拦截器)链,随后才到目标对象。目标方法开始执行后,
问题:final(最终关键字)、private(私有访问控制)和构造阶段为什么容易失效?
- 口述答案:这三个边界来自代理实现和对象生命周期。CGLIB(类代理生成库)通过生成目标类子类并覆盖方法来接管调用;final(最终关键字)类不能被继承,final(最终关键字)方法禁止覆盖,private(私有访问控制)方法对子类不可见,因此类代理无法在这些调用点插入拦截器。JDK Dynamic Proxy(JDK 动态代理)只实现接口,只有发送到接口代理的方法调用才能被分派;目标内部 private(私有访问控制)方法仍是普通内部调用。构造阶段更早:原始对象正在实例化,依赖注入和初始化尚未完成,AutoProxyCreator(自动代理创建器)还没有把最终代理交给调用者,构造器中的方法调用也发生在目标自身,不能指望事务、权限或审计生效。简单把方法改成 public(公共关键字)仍不够,还要确认对象由容器管理、调用者持有代理、切点匹配且调用发生在代理交付之后。设计上,构造器只建立对象基本不变量,不执行外部支付、数据库写或依赖切面的业务;初始化逻辑也应幂等、有限时并明确失败策略。对于不可修改的第三方 final(最终关键字)客户端,我会写自有适配器,在适配器公共业务入口做观测和错误转换,而不是强行切入第三方内部。 验收时我会列出目标类、最终代理类型和关键方法修饰符,构造一个外部代理调用、一个目标内部调用和一个构造阶段调用,比较链路证据。若必须改造第三方对象,则把适配器作为唯一容器入口,禁止业务代码直接持有原始客户端,并用架构检查和依赖扫描防止旁路重新出现。这样修复的不只是一个注解,而是对象创建与调用所有权。
- 追问 1:静态方法能被普通代理拦截吗?
- 回答 1:不能按实例代理模型拦截,静态调用不分派到代理实例;应重构为实例边界或使用更广织入技术。
- 追问 2:初始化回调中调用自身方法可靠吗?
- 回答 2:不能默认可靠,回调仍在创建流程内且可能是目标内部调用;需把业务启动动作放到明确的容器就绪后入口。
- 追问 3:第三方类不能代理怎么办?
- 回答 3:使用 Adapter(适配器)或 Decorator(装饰器模式)包住第三方对象,在自有可测试边界统一增强。
- 详情:代理限制
问题:Pointcut(切点)匹配有哪些底层细节和性能边界?
- 口述答案:Pointcut(切点)通常包含 ClassFilter(类过滤器)和 MethodMatcher(方法匹配器)。代理创建时先判断目标类型是否可能匹配,再遍历候选方法进行静态匹配;只有匹配的 Advisor(通知器)才进入代理链。静态 MethodMatcher(方法匹配器)只依赖方法、目标类型、注解或签名,结果可以缓存。动态 MethodMatcher(方法匹配器)还依赖本次实参,所以代理链中保留一个运行时判断,每次调用都要执行;即使最终不匹配,也已经付出代理分派和条件判断成本。接口方法、最具体实现方法、泛型桥接方法和注解放置位置可能让“看到的 Method(方法元信息)”不同,切点实现应使用框架的用户方法和最具体方法解析,避免重复或漏匹配。切点过宽不仅增加代理数量和启动匹配,还可能让日志切面序列化敏感支付数据,或让通用重试包住不可幂等外部调用。工程上我会限制包、接口和明确注解,预编译权限规则到本地不可变结构,建立应命中与不应命中的契约测试,并在启动或诊断端点导出关键对象的 Advisor(通知器)清单。动态对象级权限仍应在领域或安全组件中显式验证,不能完全隐藏在字符串表达式里。 验证方面,我会准备接口注解、实现类注解、泛型桥接方法、重载方法和参数边界五组样本,输出每组最终匹配的 Advisor(通知器)及动态判断次数。线上还按规则版本统计命中率与单次判断耗时;如果规则更新后命中率突然归零或成本陡增,可以在业务故障前发现元数据解析和配置发布问题,而不是等权限漏拦截后追查。
- 追问 1:动态切点何时适合?
- 回答 1:只有规则确实依赖当前参数且调用量可控时使用,例如少量明确的分级审计;能用静态元数据时优先静态。
- 追问 2:接口注解是否一定能被实现方法切点读取?
- 回答 2:不应假设,取决于代理类型和注解解析规则;要统一注解位置并按真实代理做测试。
- 追问 3:如何量化切点成本?
- 回答 3:记录代理创建数量、候选匹配耗时、每次动态判断耗时、临时对象分配和业务尾延迟,灰度前后对比。
- 详情:切点匹配与缓存
问题:循环依赖场景下为什么要保证早期代理与最终代理一致?
- 口述答案:单例属性循环依赖中,A 实例化后尚未完成注入和初始化,B 创建时却需要 A。容器可以把一个对象工厂放入三级缓存,B 请求 A 时调用
getEarlyBeanReference。如果 A 最终需要 AOP(面向切面编程)增强,AutoProxyCreator(自动代理创建器)应在这里返回早期代理 A1,让 B 从一开始就持有未来应使用的身份;A 完成初始化时,处理器识别已经提前包装并复用 A1。若先把原始 A 注入 B,最终又生成代理 A2,B 的调用会永久绕过事务、权限或审计;若自定义后置处理器在最终阶段再生成不同代理 A2,则 B 和其他调用方持有不同身份、不同 Advisor(通知器)集合,行为会随调用来源变化。早期代理协议解决的是特定单例属性循环的身份收敛,不解决构造器循环、原型对象或禁用循环引用,也不证明设计合理。支付服务与审计服务出现双向依赖时,我优先用审计端口、发件箱或事件反转依赖,让支付核心不回调具体审计服务;短期兼容才验证早期和最终引用身份、代理层数与规则集合,并登记拆除计划。 我还会做身份收敛测试:在 B 中取得注入的 A 引用,再与容器最终返回的 A 比较外部身份,逐层读取目标和 Advisor(通知器)集合;同时让事务、权限和审计各执行一次,确认不同持有者行为一致。若框架配置禁止循环引用,应用应在启动阶段明确失败,而不是用懒加载继续隐藏依赖;长期验收以删除双向边为完成标准。 - 追问 1:三级缓存为什么存对象工厂?
- 回答 1:因为是否需要提前生成代理要在真正请求早期引用时决定,工厂可延迟调用智能后置处理器,而不是一开始就泄漏原对象。
- 追问 2:多层代理是否一定身份不一致?
- 回答 2:不一定;问题是不同持有者看到不同最终引用或规则集合。合法嵌套也必须以同一外部身份稳定暴露。
- 追问 3:为什么不鼓励依赖三级缓存设计业务?
- 回答 3:它掩盖双向职责、增加初始化顺序和代理一致性风险,构造器注入也会直接暴露这种设计问题。
- 详情:Bean(对象实例)生命周期与循环依赖
- 口述答案:单例属性循环依赖中,A 实例化后尚未完成注入和初始化,B 创建时却需要 A。容器可以把一个对象工厂放入三级缓存,B 请求 A 时调用
问题:多个代理和多个切面怎样避免重复增强?
- 口述答案:同一个自动代理体系通常不会为每个切面各建一层代理,而是把多个 Advisor(通知器)筛选、排序后装入一个代理的 MethodInterceptor(方法拦截器)链。但系统中仍可能有作用域代理、远程客户端代理、方法安全代理、自定义 ProxyFactory(代理工厂)或父子容器重复处理,形成代理套代理。嵌套并非天然错误,前提是每一层职责独立、外部类型契约稳定、目标获取和释放正确,并且同一横切规则没有重复。重复增强会造成审计两条、指标翻倍、重试次数放大,严重时对外支付或库存动作重复执行。排查时给一次调用绑定唯一标识,记录每个拦截器实例、代理类型、目标身份和进入退出序号;若同一线程同一调用中相同通知进入两次,查切面重复扫描、AutoProxyCreator(自动代理创建器)重复注册、父子 ApplicationContext(应用上下文)和手工包装。扩展时优先注册自定义 Advisor(通知器)或 MethodInterceptor(方法拦截器)进入统一链,而不是在业务对象外随意包代理。业务幂等仍要吸收重复请求,但不能用幂等掩盖基础设施配置错误,因为重复审计和权限也会污染证据。 修复后的验证不止是“两条日志变一条”。我会检查同一业务调用中每个通知器实例只进入一次,支付渠道请求次数、库存受影响行数、事务提交和消息事件也保持一次;再模拟父子容器、作用域代理与远程客户端代理组合,确认合法层次仍存在且顺序稳定。最后用调用标识关联业务幂等记录,区分代理重复和 MQ(消息队列)重复投递两条故障链。
- 追问 1:怎样查看代理层次?
- 回答 1:逐层判断是否代理、读取目标源和 Advisor(通知器)列表,记录每层运行类型;只看最外层类名不够。
- 追问 2:父子容器为何可能重复?
- 回答 2:同一组件被两层扫描或同一基础设施在两层注册,引用穿过容器边界时可能被不同处理器再次包装。
- 追问 3:幂等后重复调用还有风险吗?
- 回答 3:有,额外延迟、日志、费用、限额和告警仍会发生,参数冲突也不能被简单返回首次结果。
- 详情:多代理与扩展机制
- 问题:如何系统排查“注解存在但切面不生效”?
- 口述答案:我会按证据链排查,不以“再加一次注解”作为方案。第一步确认调用入口和调用者实际引用:对象是否由当前 ApplicationContext(应用上下文)管理,是否手工
new,是否来自另一个容器,调用者持有的是代理还是原始目标。第二步查看最终运行类型,用AopUtils.isAopProxy判断代理,用AopUtils.getTargetClass得到用户类型;在受控诊断中读取Advised的 Advisor(通知器)列表和顺序。第三步检查规则:AutoProxyCreator(自动代理创建器)是否在对象创建前完整注册,候选 Advisor(通知器)是否存在,类过滤、方法匹配、注解位置、桥接方法和动态参数是否符合。第四步检查方法是否可拦截:接口暴露、代理类型、final(最终关键字)、private(私有访问控制)、静态方法和构造阶段。第五步画实际调用栈,确认是否this自调用、原始引用回调或异步线程中的另一个对象。最后验证拦截器是否进入却吞异常、短路或被重复代理覆盖。修复后做应命中、不应命中、正常、异常和并发回归,并观察审计覆盖率和单次耗时。生产中不直接取目标对象调用,以免主动绕过全部切面。 修复后我不会只重放成功请求,还会验证目标异常、动态条件不匹配、权限短路、同类自调用和并发调用。每个样本都关联最终对象类型、目标类、通知器顺序、进入退出记录和数据库事实;灰度期观察规则命中率、重复率与耗时分位。若业务结果恢复但代理证据仍漂移,模块仍不能算完成,因为下一次升级可能再次暴露同类问题。 - 追问 1:看到代理类名是否说明当前方法已增强?
- 回答 1:不能,只说明对象是代理;当前方法可能没有匹配 Advisor(通知器)或动态条件不满足。
- 追问 2:最常见的两个根因是什么?
- 回答 2:手工创建或同类自调用最常见,但仍必须用对象身份和调用栈证明,不能凭经验跳步。
- 追问 3:修复怎样防复发?
- 回答 3:为关键方法建立代理命中契约测试,启动导出规则清单,监控审计覆盖率、重复次数和切面耗时。
- 详情:AOP(面向切面编程)诊断流程
- 问题:日志和指标切面为什么可能成为性能瓶颈,怎样治理?
- 口述答案:日志切面的风险不在代理分派本身,而在每次调用追加的序列化、临时对象、堆栈提取、同步输出和敏感数据处理。假设跨境物流轨迹服务为
1200 QPS(每秒查询率),每次把8KB入参与返回值转成 JSON(JavaScript 对象表示法),就会每秒产生接近19.2MB的原始字符处理量,尚未计算对象头、编码副本和日志框架缓冲;若单次多0.7ms中央处理器时间,每秒再消耗约840ms计算,最终表现为垃圾回收频繁、磁盘队列增长和业务尾延迟上升。治理时先区分业务审计与诊断日志:审计记录稳定业务键、动作、操作者、参数摘要和结果,必要时用本地可靠事实或发件箱保证;诊断日志按错误全量、成功采样,禁止输出令牌、签名、卡信息和完整面单地址。切面内部不得远程查询用户或权限,不得在请求线程做无上限对象序列化;使用字段白名单、大小上限、慢调用阈值和异步有界队列,队列满时按等级降级并计数。验收以切面分项耗时、分配率、日志队列深度、丢弃量和业务P99(99 分位响应时间)为证据,灰度比较,而不是只看平均响应。 具体落地时,我会先在压测环境关闭切面取得基线,再逐项开启参数提取、脱敏、序列化、入队和输出,形成分项成本;对异常全量与成功采样分别测峰值。生产灰度设置回滚阈值,例如分配率、队列年龄和P99(99 分位响应时间)任一超过预算便自动收窄采样,同时保留审计流水的可靠路径,避免性能止血损害合规证据。 - 追问 1:异步日志是否就没有成本?
- 回答 1:仍有序列化、入队和内存成本,队列满还要定义丢弃、阻塞或落盘策略;异步只是移动输出等待。
- 追问 2:所有成功请求都能采样吗?
- 回答 2:普通诊断可以,合规审计不能随意采样;应按业务等级区分,并用稳定审计流水保证可追溯。
- 追问 3:如何避免日志泄露?
- 回答 3:采用字段白名单和类型化脱敏,切面默认不展开对象;结合敏感字段扫描和回归样本验证。
- 详情:切面成本与边界
- 问题:支付项目中哪些能力适合切面,哪些必须显式实现?
- 口述答案:支付场景先按信任入口和资金不变量拆分。适合切面的能力包括内部方法粗粒度权限、统一 Trace(链路追踪)标识、耗时指标、敏感字段脱敏、明确支付回调入口的签名校验前置框架,以及围绕本地数据库写入的事务拦截;这些规则跨多个方法,且可以用稳定注解或接口表达。但验签通过只证明报文来源和完整性,不代表金额、币种、商户、订单关系和渠道终态正确。商户支付单唯一、渠道流水唯一、回调幂等、防重放时间窗、金额币种核对、支付状态条件迁移、账务分录唯一和外部超时查单必须写在支付领域服务和数据库约束里。回调切面可以提取原始报文、验签、生成请求标识并拒绝非法来源,随后把结构化命令交给业务事务;业务事务以稳定键返回首次结果,参数摘要冲突则告警,不能简单吞掉。渠道调用超时只表示结果未知,通用重试切面不能换新支付号再次调用,应按原支付意图查单。资金成功后通过 Outbox(发件箱)或可靠消息推动账务与履约,对账是最终兜底。这样切面统一入口策略,领域逻辑守住资金事实,两者职责可验证。 完整验收会用同一支付单模拟首次成功、重复回调、同键不同金额、渠道超时后成功、通知乱序和审计发布失败。预期是切面只在入口完成安全与观测职责,领域事务始终依据唯一键和状态条件返回确定结果,账务与履约通过可靠事件只推进一次;最后用渠道账单对账证明没有因为切面重试、异常转换或日志故障造成第二笔资金动作。
- 追问 1:验签切面能直接开启事务吗?
- 回答 1:验签通常应在事务前完成,避免非法请求占用连接;业务状态推进再进入明确本地事务。
- 追问 2:幂等注解是否足够?
- 回答 2:不够,注解可统一入口,但最终要有业务唯一键、参数冲突校验、状态条件和持久化首次结果。
- 追问 3:审计失败要回滚支付吗?
- 回答 3:合规强审计应与资金事实形成可靠原子关系;普通诊断日志不应拖垮支付,需分级设计。
- 详情:AOP(面向切面编程)项目边界
- 问题:WMS(仓储管理系统)库存防超卖能否主要依赖 AOP(面向切面编程)?
- 口述答案:不能。AOP(面向切面编程)可以统一事务、权限、审计和指标,但代理只控制单进程中的调用编排,无法在多实例并发下凭自身保证库存余量不为负。库存防超卖的最终不变量应放在权威数据库:更新语句带“可售量大于等于请求量”的条件,以受影响行数判断成功;订单行、仓库、商品和预占动作形成唯一流水,余额变化、预占记录和待发布事件在同一本地事务提交。重复请求命中唯一键后返回首次结果,若同一幂等键携带不同数量则视为冲突。切面可以在方法入口校验操作者的仓库范围、建立事务、记录业务键和耗时,但对象级权限还要在业务查询中绑定租户和仓库条件。若切面因同类自调用失效,数据库条件更新仍能阻止负库存;这正说明横切机制不能成为正确性唯一防线。批量库存操作还要避免切面为每行同步写日志,可按批次记录摘要和失败明细。事故排查时同时看代理命中、数据库受影响行数、唯一流水、事务提交和消息发布,不能因为日志缺失就推断库存没有变化。 我会用两个并发请求对同一仓库商品预占最后一件库存:即使关闭切面或让其中一次绕过代理,也只能有一条条件更新成功,且唯一流水与余额守恒。随后恢复事务切面,故障注入流水写失败和事件写失败,确认余额更新一同回滚。这个对照能证明数据库是不变量底座,AOP(面向切面编程)负责把多条本地事实组成可恢复事务,而不是把两者职责混为一谈。 回归报告同时核对库存守恒、流水唯一、事件数量和代理命中证据,四者缺一不可。
- 追问 1:分布式锁能替代条件更新吗?
- 回答 1:不能,锁可能过期、主从切换或客户端暂停;数据库条件更新和唯一约束才是最终写入门禁。
- 追问 2:事务切面失效时一定超卖吗?
- 回答 2:不一定,单条条件更新仍可原子;但余额、流水和事件可能不再同事务,产生可恢复性问题。
- 追问 3:审计切面应该记录什么?
- 回答 3:记录仓库、商品、订单行、动作、数量摘要、操作者、结果和调用标识,不记录整个库存对象图。
- 详情:库存代理边界
- 问题:方法权限为什么既需要代理,又不能只靠代理?
- 口述答案:方法权限适合代理,是因为“调用某个应用服务前验证主体是否具备某类能力”属于横切入口策略。方法安全基础设施可以把权限元数据解析为 Advisor(通知器),在目标调用前读取 SecurityContext(安全上下文)并拒绝无权请求,减少每个方法手写重复判断。但粗粒度角色不能替代对象级授权:同样具有“仓库管理员”角色的用户,只能操作被分配的仓库和租户;支付客服可以查询部分支付单,却未必能退款高金额订单。对象级规则依赖当前业务对象、状态、金额和数据所有权,应在业务查询或领域策略中校验,并把租户、仓库等条件下推到数据访问边界,防止先查出再过滤造成泄露。代理还受自调用、手工对象和异步线程上下文影响,因此安全不能只看注解。调用进入异步任务时,要显式捕获必要身份或转换为服务身份,最小化权限并在任务结束清理上下文;不能盲目复制整个线程本地状态。验收包括未认证、无角色、跨租户、对象状态不允许、同类自调用和异步执行等负面用例,并审计谁在何时以何依据做了决定。 验收矩阵至少包含未登录、角色不足、跨租户、跨仓库、对象状态不允许、同类自调用和异步任务七类负面用例,并关联权限规则版本和业务查询条件。对拒绝请求确认事务和外部副作用没有启动,对允许请求确认审计记录了主体与对象范围。规则中心不可用时,高风险写按失败关闭,不能由切面捕获异常后默认放行。
- 追问 1:网关鉴权后服务内还需要方法权限吗?
- 回答 1:需要。网关只能做入口粗粒度判断,内部调用、消息消费和对象级数据权限仍需服务内验证。
- 追问 2:权限切面能远程查权限中心吗?
- 回答 2:高频同步远程查询会扩大延迟和故障面;应使用有版本、本地可撤销的规则缓存,并对高风险动作做明确权威校验。
- 追问 3:权限异常能否降级放行?
- 回答 3:高风险写操作应失败关闭;只读或低风险能力是否降级必须有明确策略、审计和时限。
- 详情:方法权限与代理链
- 问题:AOP(面向切面编程)如何与事务模块衔接,又有哪些边界?
- 口述答案:声明式事务本身就是 AOP(面向切面编程)的典型应用:事务属性被解析成 Advisor(通知器),目标方法匹配后由 TransactionInterceptor(事务拦截器)进入统一 MethodInterceptor(方法拦截器)链。拦截器在调用目标前根据事务管理器和传播行为取得或复用事务,把数据库资源绑定到当前线程;目标正常返回时提交,异常穿出时按回滚规则回滚,最后清理线程资源。这个模型解释了为什么同类自调用会让方法级传播失效、为什么吞异常可能提交、为什么异步新线程不会自动继承事务,以及为什么多个数据源必须选对事务管理器。AOP(面向切面编程)只负责把本地事务边界织入调用,不会把外部支付、MQ(消息队列)或承运商接口变成同一原子事务。远程调用放在数据库事务内还会延长锁和连接占用,并产生“远程成功、本地回滚”或“本地提交、响应丢失”的未知窗口。项目中我把本地数据与 Outbox(发件箱)同事务提交,事务外可靠发布;支付调用使用稳定业务号、超时查单和对账。排查事务问题时必须先证明代理入口、实际事务管理器、连接绑定、异常传播和最终数据库状态,再讨论传播枚举。 验证时我会在代理入口记录事务名称和实际管理器,在数据库侧确认连接、锁与提交时间,并让目标分别正常返回、抛回滚异常、抛不回滚业务异常和切换异步线程。外部支付同时模拟请求成功但响应丢失,证明本地事务回滚不能撤销渠道事实,恢复必须依赖原业务号查单、幂等推进和对账。这样 AOP(面向切面编程)边界与分布式一致性边界不会混淆。
- 追问 1:方法上有事务注解为何可能没事务?
- 回答 1:对象可能非容器管理、调用绕过代理、方法不可拦截、切点未匹配或选错事务管理器。
- 追问 2:捕获异常后怎么保证回滚?
- 回答 2:首选继续抛出符合规则的异常;确需转换时保留 cause(原因)并保持回滚语义,不用默认值伪装成功。
- 追问 3:事务切面能覆盖跨线程吗?
- 回答 3:不能自动覆盖;事务资源通常绑定当前线程,新线程应建立自己的明确事务和幂等边界。
- 详情:代理生命周期基础
- 问题:什么时候应考虑 AspectJ(Java 切面织入框架),而不是代理式 AOP(面向切面编程)?
- 口述答案:代理式 AOP(面向切面编程)适合容器管理对象的公共方法边界,部署和诊断相对简单,是事务、方法安全和应用层观测的默认选择。只有需求明确超出代理模型时,我才评估 AspectJ(Java 切面织入框架),例如必须拦截构造器、字段访问、非容器对象、private(私有访问控制)方法或同类内部调用,且这些连接点确实是稳定技术边界。AspectJ(Java 切面织入框架)可在编译期或加载期修改字节码,把通知织入目标连接点,不依赖调用者持有代理,因此能覆盖更广;代价是构建链、类加载代理、调试堆栈、热部署和版本兼容更复杂,错误切点的影响范围也更大。不能为了绕过一次自调用就立即引入织入,通常拆分 Bean(对象实例)或显式装饰器更清晰。若使用织入,我会限定模块和切点,保留织入报告,验证产物字节码与运行类加载来源,测试成功、异常和重复织入,并监控额外分配与延迟。支付和库存核心不变量仍不放到隐式织入逻辑里;织入最多统一观测或防护,最终正确性仍由业务状态和存储约束保证。 决策前我会列出代理方案无法覆盖的真实连接点数量、事故损失和可替代重构成本;只有收益持续大于构建与运维复杂度才引入。验收包括构建产物织入报告、测试和生产类加载来源、单次通知计数、异常堆栈可读性以及回滚开关。若只是少数同类自调用,拆分边界通常更便宜,也让事务和权限更容易被新人理解。
- 追问 1:加载期织入有什么额外风险?
- 回答 1:依赖类加载代理和启动参数,容器、测试与生产可能加载路径不同,需核对织入日志和实际类来源。
- 追问 2:AspectJ(Java 切面织入框架)能解决所有自调用吗?
- 回答 2:技术上可覆盖更多内部连接点,但不解决职责混乱;如果边界设计错误,织入只会隐藏问题。
- 追问 3:如何避免重复织入?
- 回答 3:统一织入入口和构建配置,检查织入报告与字节码,并以单次调用的通知序号做回归。
- 详情:代理边界与替代方案
- 问题:如何设计一个可复用、可诊断的自定义 Advisor(通知器)?
- 口述答案:我会先定义它解决的横切问题和不能改变的业务语义,再实现最窄的 Pointcut(切点)和单一职责 Advice(通知)。切点优先使用稳定接口或明确注解,类过滤先排除数据对象、框架基础设施和内部辅助类;能静态判断就不做动态参数匹配。通知实现为 MethodInterceptor(方法拦截器)时,明确是否恰好调用一次
proceed、是否允许短路、怎样处理返回和异常、是否修改参数,以及自己的超时和资源预算。它不保存请求级可变字段,线程上下文使用后必须清理;外部配置预编译成本地不可变快照,并记录规则版本。顺序通过Ordered明确,不与同优先级规则形成隐含依赖。观测中记录通知器标识、目标方法、业务键摘要、进入退出、耗时和结果分类,但限制日志大小和敏感字段。测试包括应命中、不应命中、正常返回、业务异常、系统异常、并发、嵌套代理和重复注册;生产诊断可导出关键 Bean(对象实例)的 Advisor(通知器)清单。若用途是支付重试或库存幂等,我会拒绝做无业务语义的通用切面,改由领域协议定义稳定键、查证和冲突处理。 上线时我还会为通知器设置独立指标:候选对象数、命中方法数、调用次数、短路次数、异常次数和分位耗时,并把规则版本作为低基数字段。配置变更先在小流量实例加载,比较命中差异,再扩大范围;出现异常可回滚到上一不可变快照。这样扩展点既复用框架链,也具备独立发布、诊断和撤销能力。 - 追问 1:为什么规则版本重要?
- 回答 1:动态权限或审计策略变化时,版本能把一次决定与当时规则关联,支持复盘、灰度和回滚。
- 追问 2:短路返回要注意什么?
- 回答 2:必须与方法契约一致,明确后续通知是否执行,并记录拒绝或缓存证据,不能返回含糊空值。
- 追问 3:自定义通知能保存计数器字段吗?
- 回答 3:单例通知器会并发使用;计数应使用线程安全指标组件,请求状态放方法局部变量或明确上下文。
- 详情:扩展点与设计模式
- 问题:如何验证 AOP(面向切面编程)配置而不过度依赖实现细节?
- 口述答案:验证分三层。第一层是契约层:从容器取得真实 Bean(对象实例),通过公共接口调用,验证应命中的权限、审计、事务或指标行为,也验证不应命中的方法不受影响;这样测试用户可观察结果,而不是直接调用切面方法。第二层是机制层:对关键基础设施少量检查最终对象是否代理、目标类型是否正确、Advisor(通知器)顺序是否满足语义,覆盖 JDK Dynamic Proxy(JDK 动态代理)和 CGLIB(类代理生成库)配置、final(最终关键字)边界、同类自调用和循环依赖早期引用。第三层是故障层:让目标正常返回、抛业务异常、抛系统异常、超时、并发和异步切换,确认异常不被吞、事务状态正确、审计只写一次、线程上下文被清理。测试数据要使用稳定业务标识,支付外部调用使用可查证替身,库存使用真实数据库条件更新或等价集成环境,不能只验证日志文本。上线灰度再看代理创建数量、规则命中率、通知耗时、分配率、重复审计和业务错误率。这样既不把测试绑死在每个内部类,也能及时发现代理类型、顺序和旁路变化。 为避免测试本身制造假象,容器集成测试不手工实例化目标,也不直接调用切面;数据库与外部客户端使用能记录业务号和失败窗口的真实替身。框架升级时先运行代理契约套件,再比较启动导出的关键规则清单,最后灰度观察命中率与资源成本。只有用户可观察行为和机制证据同时稳定,才认为兼容,而不是测试绿灯便结束。
- 追问 1:是否要断言具体代理类名?
- 回答 1:通常不要,类名是实现细节;只在选型边界测试中断言代理类型和可赋值契约。
- 追问 2:怎样测试同类自调用?
- 回答 2:设置外部入口和内部入口两个场景,用事务状态或通知计数证明内部调用没有重新进入代理,再验证重构方案。
- 追问 3:线上规则清单会泄露信息吗?
- 回答 3:诊断端点必须鉴权和脱敏,只展示规则标识、目标摘要和顺序,不暴露表达式中的敏感业务数据。
- 详情:代理诊断与回归
- 问题:代理中的
equals、类型判断和对象身份为什么需要谨慎?
- 口述答案:代理把“业务对象身份”和“运行时包装对象身份”分开了。JDK Dynamic Proxy(JDK 动态代理)是接口实现,通常不是目标具体类的实例;CGLIB(类代理生成库)是目标子类,但仍可能再包作用域代理或远程代理。业务代码若以具体运行类名、对象引用地址或代理层数作为持久化身份,会在配置切换、容器重建或多代理嵌套后失效。
equals和hashCode的行为还取决于目标实现与代理分派,代理与目标互相比对可能不满足开发者直觉;把 Bean(对象实例)放入集合后再更换包装也会造成查找问题。正确做法是领域对象使用稳定业务键定义等价,服务对象通常不参与业务等价比较;注入点面向稳定接口,不把代理强转为实现类。缓存键、幂等键和审计关联使用订单号、支付单号、仓库商品键等业务标识,而不是对象身份。诊断时可以比较引用和目标身份来发现早期代理分裂,但这只是运行证据,不应成为业务逻辑。自定义代理或 TargetSource(目标源)还要明确目标是否每次变化、equals是否穿透以及释放语义,并在集合、序列化和远程边界测试。 我会特别检查循环依赖和作用域代理场景:同一容器多次获取单例应得到稳定外部身份,请求作用域则只承诺当前作用域语义;任何持久化记录都只保存业务标识,不保存运行类或对象哈希。自定义代理若覆盖等价方法,必须满足对称、传递和稳定哈希契约,并测试代理与目标互比,否则宁可让服务对象使用默认引用语义。 - 追问 1:为什么不建议按
getClass()判断业务类型? - 回答 1:代理运行类不是用户类型,配置变化会改变类;应使用接口、
instanceof或框架目标类型工具。 - 追问 2:服务对象可以作为 Map(映射接口)键吗?
- 回答 2:技术上可以,但通常没有业务意义且受代理身份影响;应使用稳定业务键或明确组件标识。
- 追问 3:早期代理一致性如何验证?
- 回答 3:比较依赖方持有引用与容器最终引用是否同一外部身份,并核对目标与 Advisor(通知器)集合。
- 详情:早期代理与对象身份
- 问题:请讲一次由 AOP(面向切面编程)引起的线上事故排查闭环。
- 口述答案:我会按“影响、止血、保存现场、证据、根因、修复、回归、防复发”表达。示例是 WMS(仓储管理系统)批量库存调整上线后,部分操作有业务流水却没有统一审计,且一个内部方法声明的事务没有按预期回滚。先暂停高风险批量入口,不直接改库,保存订单、库存流水、应用版本、调用标识、日志和数据库提交记录;单条库存仍由条件更新保护,所以先确认没有负库存,再处理审计缺口。证据显示外部入口对象确实是 CGLIB(类代理生成库)代理,Advisor(通知器)包含事务和审计;但调用栈进入目标
batchAdjust后执行this.adjustOne,内部方法未再次出现代理或拦截器帧,异常又被批处理代码捕获转成失败计数,导致外层事务观察到正常返回。根因是同类自调用加异常语义改变,不是日志系统丢消息。短期把单条调整委托给独立 Bean(对象实例)代理并继续抛出不可恢复异常;长期拆分批次编排与库存原子动作,审计事实通过本地事务和可靠发布形成闭环。回归覆盖成功、部分失败、并发、重复请求和审计发布失败,增加关键 Bean(对象实例)规则清单、审计覆盖率与重复率告警。 复盘时还会量化影响窗口、缺失审计数量、异常提交数量和人工修复耗时,明确哪些结论来自数据库事实、哪些来自 Trace(链路追踪)推断。防复发门禁包含禁止核心服务同类事务调用的静态检查、关键 Bean(对象实例)规则快照、异常传播契约测试和审计差异对账;这样事故经验变成系统能力,而不是只留一句“注意自调用”。 - 追问 1:为什么不先重启?
- 回答 1:重启不能改变稳定代码路径,还会丢失线程和调用现场;先止血并保留业务与代理证据更重要。
- 追问 2:如何补历史审计?
- 回答 2:从不可变库存流水按业务键生成带来源标记的补录事件,核对操作者和版本,不伪造原始调用时间。
- 追问 3:怎样证明修复有效?
- 回答 3:用相同故障样本复现,确认代理链进入、事务状态、数据库结果和审计事件一致,再灰度观察覆盖率。
- 详情:线上诊断闭环
- 问题:面试中如何评价 AOP(面向切面编程)的收益、代价与使用原则?
- 口述答案:我的评价是,AOP(面向切面编程)通过代理和拦截器链把横切策略集中治理,收益是减少漏执行、统一顺序、便于替换和形成观测;事务、方法权限、审计、指标与少量缓存都是典型场景。它背后的高维设计思想是控制反转和关注点分离:业务对象只表达领域动作,容器在调用边界装配策略;Advisor(通知器)把选择与动作组合,MethodInterceptor(方法拦截器)以责任链和装饰器方式执行,ProxyFactory(代理工厂)隔离具体代理算法。代价是控制流变隐式,方法看起来是普通调用,实际可能短路、改参、开事务或抛出不同异常;同时存在代理类型、自调用、方法可见性、早期引用、多代理顺序和性能成本。我的原则是:第一,切点窄而稳定,横切逻辑单一、无请求级共享状态;第二,不吞异常、不无脑重试、不把外部副作用包装成透明本地调用;第三,领域不变量和分布式一致性显式落在状态机、唯一键、条件更新、消息与对账;第四,关键代理链可诊断、可度量、可做负面回归;第五,发现需要大量 ExposeProxy(暴露代理)或复杂动态切点时,反思服务职责与边界,而不是继续堆魔法。这样 AOP(面向切面编程)是基础设施工具,不是隐藏业务复杂度的容器。 最终验收不是统计用了多少注解,而是看横切规则是否有清晰所有者、版本、预算、失败策略和移除路径。每条关键规则都要回答“不命中会怎样、执行两次会怎样、通知自身失败会怎样、如何从业务事实恢复”。只有这些答案能通过故障注入和项目数据验证,AOP(面向切面编程)才真正降低重复与风险;否则它只是把复杂度从业务方法搬到更难看见的地方。
- 追问 1:AOP(面向切面编程)最大的风险是什么?
- 回答 1:隐式改变调用语义却缺乏证据,导致事务、权限或重试与开发者理解不一致。
- 追问 2:如何控制隐式性?
- 回答 2:限制切点和能力、稳定排序、导出规则清单、记录调用证据,并让关键业务边界在代码和文档中可见。
- 追问 3:何时应放弃切面?
- 回答 3:当逻辑依赖复杂业务状态、需要跨系统补偿,或必须频繁绕代理限制时,应改为显式领域服务、装饰器或流程编排。
- 详情:本章完整机制
3. 项目口述模板与复习清单
可直接复述的话术:在支付和 WMS(仓储管理系统)项目中,我把 AOP(面向切面编程)限定为本地方法边界的横切治理。容器通过 AutoProxyCreator(自动代理创建器)为匹配 Advisor(通知器)的对象创建代理,调用时由 MethodInterceptor(方法拦截器)链处理权限、事务、审计和指标。我重点防止同类自调用、异常吞噬、切点过宽和重复代理。资金幂等与库存不超卖不依赖切面,而由业务唯一键、状态条件、数据库事务、可靠事件和对账守底;出现问题时沿对象身份、规则清单、实际调用栈和数据库事实取证。
复习时应能不看文档完成以下内容:
- 解释六个核心术语及 Advisor(通知器)到 MethodInterceptor(方法拦截器)的适配关系。
- 画出 AutoProxyCreator(自动代理创建器)创建代理与早期引用的两条路径。
- 比较 JDK Dynamic Proxy(JDK 动态代理)和 CGLIB(类代理生成库)的类型与方法限制。
- 用两个通知器演绎正常返回、异常和最终处理顺序。
- 画出外部代理调用与
this自调用的路径差异,并给出三种修复方案。 - 解释 final(最终关键字)、private(私有访问控制)、构造阶段和异步线程的边界。
- 说明早期代理为何必须与最终 Bean(对象实例)身份一致。
- 从调用者引用开始完成一次代理失效或重复增强排查。
- 用支付和库存案例说明切面能力与领域不变量的分界。
- 说明何时使用显式装饰器或 AspectJ(Java 切面织入框架),以及新增复杂度。
