IoC 容器与 AOP 原理¶
Bean 生命周期¶
🧩 1. Spring IoC 容器的工作原理是什么?Bean 的生命周期有哪些阶段?¶
控制反转(IoC) 的核心是“把创建对象的权力交给容器”,而不是在代码里到处 new。Spring IoC 容器的内部就像一个高度自动化的工厂:它根据你提供的图纸(配置类/XML/注解),扫描原材料(类路径),生成零件蓝图(BeanDefinition),然后通过一系列流水线加工,最终生产出可以随时使用的 Bean。

Bean 的完整生命阶段大致如下:
| 阶段 | 说明 |
|---|---|
| 实例化 | 容器通过反射调用构造方法(或工厂方法)创建 Bean 的原始对象,此时属性尚未注入 |
| 属性赋值 | 根据配置注入依赖(@Autowired、@Value 等),包括其他 Bean 的引用和简单值 |
| Aware 回调 | 如果 Bean 实现了 BeanNameAware、BeanFactoryAware、ApplicationContextAware 等接口,容器会依次调用,让 Bean 感知自己在容器中的身份 |
| 前置处理 | 遍历所有 BeanPostProcessor,执行 postProcessBeforeInitialization 方法,可以在初始化前对 Bean 进行包装或修改 |
| 初始化 | 顺序执行: ① @PostConstruct 注解的方法 ② InitializingBean 接口的 afterPropertiesSet() ③ @Bean(initMethod = "...") 指定的方法 |
| 后置处理 | 再次遍历所有 BeanPostProcessor,执行 postProcessAfterInitialization 方法,这是 AOP 代理生成的关键节点 |
| 就绪 | Bean 完全可用,放入单例缓存池(singletonObjects) |
| 销毁 | 容器关闭时,依次执行: ① @PreDestroy 注解的方法 ② DisposableBean 接口的 destroy() ③ @Bean(destroyMethod = "...") 指定的方法 |
这些阶段共同保证了 Bean 在经过依赖注入、上下文感知、功能增强之后,才被暴露给应用使用。
⚖️ 2. BeanPostProcessor、InitializingBean、@PostConstruct 的执行顺序是什么?各自的应用场景?¶
这三者都工作在 Bean 的初始化附近,但它们分工不同。按照执行顺序排列如下:
执行顺序:
1. BeanPostProcessor.postProcessBeforeInitialization()
2. @PostConstruct 方法
3. InitializingBean.afterPropertiesSet()
4. @Bean(initMethod = "...") 方法
5. BeanPostProcessor.postProcessAfterInitialization()
用一个具体的例子来说明。假设我们需要一个 LLM 客户端的 Bean,它需要在属性注入完成后自动预热连接池:
@Component
public class LLMClientBean implements InitializingBean {
@Value("${llm.api.key}")
private String apiKey;
private HttpClientPool pool;
@PostConstruct
public void initPool() {
// @PostConstruct: 在依赖注入完成后立即创建连接池
this.pool = new HttpClientPool(apiKey, 10);
System.out.println("1. @PostConstruct - 连接池已创建");
}
@Override
public void afterPropertiesSet() throws Exception {
// InitializingBean: 执行连接预连,确保前几条请求不排队
pool.prewarm(5);
System.out.println("2. afterPropertiesSet - 已预热5条连接");
}
public void customInit() {
// 在 @Bean 的 initMethod 里指定
System.out.println("3. customInit - 可以执行额外校验");
}
public String call(String prompt) {
// 实际调用 LLM
return pool.getConnection().invoke(prompt);
}
}
此时如果在配置类中声明 @Bean(initMethod = "customInit"),或者通过一个 BeanPostProcessor 进行包装,可以清晰地看到它们按序执行。
各自的应用场景:
| 机制 | 适用场景 | 特点 |
|---|---|---|
| @PostConstruct | 资源初始化、数据加载、启动后立即执行的业务逻辑 | 注解清晰,与 Spring 耦合低,推荐日常使用 |
| InitializingBean | 需要访问容器更底层能力的初始化(如 afterPropertiesSet 内部可能需要 BeanFactory) | 与 Spring 强耦合,适合框架级开发 |
| BeanPostProcessor | 对所有 Bean 进行统一包装,例如 AOP 代理、日志切面、安全代理 | 范围最广,可对所有 Bean 做增强 |
| initMethod | 需要解耦 Bean 类与 Spring 接口的场景,例如复用一个纯 POJO | 配置灵活,适合遗留代码集成 |
一句话总结:@PostConstruct 解决大多数初始化需求,InitializingBean 给框架开发者留口子,而 BeanPostProcessor 则站在上帝视角,在初始化前后对所有 Bean 施以全局影响。
🚀 3. Agent 的 LLM 客户端 Bean 需要在启动时预热连接池,如何利用 Bean 生命周期钩子实现?¶
在 Agent 系统中,LLM 客户端通常使用 HTTP 连接池(如 OkHttp、Apache HttpClient)与远程模型服务通信。如果不在启动时预热,第一个真实请求会遭遇“冷启动”——连接建立、SSL 握手等开销导致延迟暴涨。利用 Bean 生命周期,我们可以在 Bean 创建完成后、对外服务前,自动完成预热。
实现方案:InitializingBean + @PostConstruct 组合
@Component
public class LLMClient implements InitializingBean {
@Value("${llm.endpoint}")
private String endpoint;
@Value("${llm.api.key}")
private String apiKey;
@Value("${llm.pool.size:20}")
private int poolSize;
private OkHttpClient httpClient;
// 阶段一:依赖注入后,立即构建连接池
@PostConstruct
public void buildPool() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(60))
.connectionPool(new ConnectionPool(poolSize, 5, TimeUnit.MINUTES))
.addInterceptor(chain -> {
// 自动附加 API Key
Request req = chain.request().newBuilder()
.header("Authorization", "Bearer " + apiKey)
.build();
return chain.proceed(req);
})
.build();
}
// 阶段二:初始化完成后,预热连接
@Override
public void afterPropertiesSet() {
// 向端点发送一个空的探测请求,提前建立 TCP 连接和 SSL 会话
Request ping = new Request.Builder()
.url(endpoint + "/health")
.head()
.build();
for (int i = 0; i < poolSize / 2; i++) {
try (Response resp = httpClient.newCall(ping).execute()) {
System.out.println("预热连接 " + (i+1) + ": " + resp.code());
} catch (IOException e) {
// 预热失败不应阻塞启动,记录告警即可
System.err.println("预热连接失败: " + e.getMessage());
}
}
System.out.println("LLM 客户端连接池预热完成,共预热 " + poolSize/2 + " 条连接");
}
public String complete(String prompt) throws IOException {
// 真实调用
RequestBody body = RequestBody.create(
MediaType.parse("application/json"),
"{\"prompt\":\"" + prompt + "\"}"
);
Request req = new Request.Builder()
.url(endpoint + "/completions")
.post(body)
.build();
try (Response resp = httpClient.newCall(req).execute()) {
return resp.body().string();
}
}
}
为什么这样设计?
-
@PostConstruct 构建连接池:此时所有
@Value都已注入,是创建底层资源的恰当时机。将它放在这里而不是afterPropertiesSet,是为了遵守“构造后依赖注入,初始化时预热”的清晰边界。 -
afterPropertiesSet 预热:利用
InitializingBean接口,在 Bean 完全组装后执行网络调用。它比@PostConstruct更晚,确保所有其他 Bean 的后置处理(比如可能涉及的代理)已经完成。 -
预热不影响启动:预热代码捕获了异常,即使远程 LLM 服务暂时不可用,应用也能正常启动,只是后续真实请求需要等待连接建立。
如果需要更彻底的启动时检查,可以引入 ApplicationRunner:在 Spring Boot 完全启动后,自动跑一次完整的 LLM 调用,并验证返回格式,这可以作为“就绪探针”的一部分。
@Component
public class LLMHealthChecker implements ApplicationRunner {
@Autowired
private LLMClient llmClient;
@Override
public void run(ApplicationArguments args) {
try {
String response = llmClient.complete("ping");
if (response == null || response.isEmpty()) {
throw new RuntimeException("LLM 服务返回空响应");
}
System.out.println("LLM 服务启动验证通过");
} catch (Exception e) {
// 根据业务决定是否终止启动
System.err.println("LLM 服务启动验证失败: " + e.getMessage());
}
}
}
利用 @PostConstruct 搭建骨架,用 InitializingBean 填充肌肉,再用 ApplicationRunner 做最后的全身检查——这套组合拳让 Agent 的 LLM 客户端在启动时就蓄势待发,不为第一个真实请求付出冷启动的代价。这正是 Spring 生命周期钩子在 AI 工程中的一次务实运用。
三级缓存与循环依赖¶
🧩 1. 什么是 Spring 的循环依赖?Spring 是如何解决的?¶
循环依赖,简单说就是两个或多个 Bean 互相持有对方的引用,形成一个闭环。比如 Bean A 的构造器或属性需要注入 Bean B,而 Bean B 的构造器或属性又需要注入 Bean A。

如果只是简单地用构造器注入互相依赖,容器会陷入“鸡生蛋、蛋生鸡”的死锁,最终抛出 BeanCurrentlyInCreationException。但 Spring 通过提前暴露尚未完全初始化的 Bean 引用,巧妙地解决了单例属性注入的循环依赖。它的核心秘密武器是三级缓存。
解决过程大致分为三步:
-
实例化(构造对象):Spring 先通过反射(或工厂方法)调用构造器创建 Bean A 的原始对象,此时 A 的属性还没注入,是个“半成品”。
-
提前暴露:把这个半成品的引用包装在一个
ObjectFactory里,放入缓存(第三级缓存),供其他 Bean 提前引用。 -
属性填充:然后 Spring 为 A 注入属性,发现 A 需要 B。于是它去创建 B。同样地,B 实例化后也会提前暴露自己,然后在填充 B 的属性时发现 B 需要 A。此时 Spring 从缓存中拿到 A 的半成品引用,注入给 B,完成 B 的初始化。最后再回到 A,把已经完全初始化的 B 注入给 A。
这就像盖两栋互相依赖的楼:先给 A 楼搭一个脚手架(半成品),然后去建 B 楼,在 B 楼封顶前把 A 的脚手架架在 B 楼里,等 B 楼封顶后再回来把 A 楼盖好。整个过程依赖“半成品引用”这个缓冲,而保护这个缓冲的,正是 Spring 巧妙的三级缓存设计。
⚙️ 2. Spring 三级缓存(singletonObjects / earlySingletonObjects / singletonFactories)各自的作用是什么?为什么需要三级而不是两级?¶
在 DefaultSingletonBeanRegistry 中,Spring 维护了三个关键的 Map:

创建流程中缓存的交互过程:
实例化 A → 将 A 的 ObjectFactory 放入三级缓存
→ 属性填充 A,发现需要 B
→ 创建 B
→ 实例化 B → 将 B 的 ObjectFactory 放入三级缓存
→ 属性填充 B,发现需要 A
→ 从三级缓存找到 A 的 factory
→ 调用 factory.getObject() → 得到 A 的早期引用(可能已代理)
→ 将 A 的早期引用放入二级缓存,从三级缓存移除 A 的 factory
→ 注入 A 给 B
→ B 初始化完成 → B 放入一级缓存
→ 注入完全体的 B 给 A
→ A 初始化完成 → A 从二级缓存升级到一级缓存
为什么需要三级,而不是两级?
很多人会问:如果把半成品直接放在二级缓存里,等需要时拿出来用,不就够了?关键原因在于 AOP 代理。
Spring 在初始化 Bean 时,会通过 BeanPostProcessor 对 Bean 进行后置处理,其中最关键的是生成 AOP 代理。这个代理的生成时机在初始化之后。但是,当发生循环依赖时,一个 Bean 可能需要提前暴露给另一个 Bean,此时它还没有执行初始化,更不可能执行后置处理生成代理。
如果只有两级缓存(成品和半成品),那么提前暴露的只能是一个原始对象,而不是代理对象。当循环依赖涉及 AOP(比如 A 需要 B,B 需要 A,而 A 有事务切面需要代理),注入给 B 的就只是一个没有切面功能的原始 A,这会破坏 AOP 的正确性。
三级缓存(singletonFactories)的作用,就是用一个 ObjectFactory 把“获取早期引用”的动作延迟到真正需要的时候。这个 ObjectFactory 内部会调用 getEarlyBeanReference,它会遍历所有 SmartInstantiationAwareBeanPostProcessor,有机会提前生成代理对象,即使 Bean 还没有完成初始化。这样,当 B 需要 A 时,从三级缓存中拿到的 ObjectFactory 就能返回一个已经生成代理的 A 的引用,从而保证了 AOP 的完整性。
一句话收束:二级缓存只能存一个死的半成品,而三级缓存的工厂方法能在“用到时”才决定要不要生产一个代理,这就保障了循环依赖场景下 AOP 依然有效。这是 Spring 设计中最令人拍案的地方。
🛠️ 3. Agent 服务中 ToolRegistry 和 AgentExecutor 互相依赖导致循环依赖,如何设计规避?¶
假设我们正在开发一个 Agent 框架,有两个核心类:
-
ToolRegistry:管理所有可用工具(Tool),并负责根据名称查找工具。 -
AgentExecutor:执行 Agent 的任务,需要从ToolRegistry获取工具来调用;同时,某些工具的执行又需要回调AgentExecutor的能力(比如执行子任务)。
这就形成了一个典型的循环依赖:ToolRegistry ↔ AgentExecutor。直接使用 @Autowired 互相注入会触发 Spring 的循环依赖解决方案,虽然 Spring 能解决,但代码结构会变得脆弱(比如未来改为构造器注入就会炸),而且隐藏了不合理的分层。
重构思路:核心是打破“直接持有”的依赖链条。 有三种经典策略:
策略一:使用 @Lazy 注解(最快但非最优)
在其中一个注入点加上 @Lazy,让 Spring 先注入一个代理对象,真正的 Bean 在第一次调用时才被创建。这利用了 Spring 的延迟初始化来绕过循环依赖的创建时序问题。
@Component
public class AgentExecutor {
@Autowired
@Lazy
private ToolRegistry toolRegistry;
// ...
}
优点:改动最小,一行代码解决问题。
缺点:隐藏了设计上的耦合,@Lazy 会导致 Bean 的初始化错误推迟到运行时才暴露,且不能用于构造器注入,不利于不可变设计。
策略二:提取中介者,使用事件/回调解耦(推荐)
分析业务逻辑:为什么 ToolRegistry 需要 AgentExecutor?很可能是因为某个工具需要“执行子 Agent”的能力。我们可以把“执行子 Agent”这个能力抽象成一个接口 TaskExecutor,让 AgentExecutor 实现它,然后 ToolRegistry 只依赖这个接口。同时,为了避免直接的循环引用,可以让 ToolRegistry 在需要执行任务时,通过一个回调或者事件去触发,而不是直接持有 AgentExecutor。
// 抽象任务执行器接口
public interface TaskExecutor {
String execute(String task);
}
// AgentExecutor 实现该接口
@Component
public class AgentExecutor implements TaskExecutor {
@Autowired
private ToolRegistry toolRegistry;
@Override
public String execute(String task) {
// 使用工具执行
Tool tool = toolRegistry.getTool("search");
// ...
}
}
// ToolRegistry 只依赖 TaskExecutor,不直接依赖 AgentExecutor
@Component
public class ToolRegistry {
// 注意:这里没有 @Autowired AgentExecutor,而是依赖 TaskExecutor
private TaskExecutor taskExecutor;
// 构造器注入,没有循环
public ToolRegistry(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
// 某个工具内部需要调用任务执行能力
public void executeSubTask(String subTask) {
taskExecutor.execute(subTask);
}
}
但这样还有循环吗?TaskExecutor 由 AgentExecutor 实现,而 AgentExecutor 又依赖 ToolRegistry,编译层面没有循环(构造器可以正常工作),因为 Spring 能够通过三级缓存解决。但如果想彻底消灭循环,可以进一步使用事件模型:当工具需要执行子任务时,发布一个 TaskRequestEvent,由 AgentExecutor 监听并处理。
// 彻底解耦:事件驱动
@Component
public class ToolRegistry {
@Autowired
private ApplicationEventPublisher publisher;
public void executeSubTask(String subTask) {
publisher.publishEvent(new SubTaskEvent(subTask));
}
}
@Component
public class AgentExecutor {
@EventListener
public void handleSubTask(SubTaskEvent event) {
// 执行子任务,期间可以调用 toolRegistry(这里无循环)
}
}
策略三:使用 ObjectProvider 延迟获取
在 Spring 中,可以使用 ObjectProvider<T> 来延迟获取依赖,它本质上是一种“懒加载”的委托。当 ToolRegistry 需要 AgentExecutor 时,再从容器的提供者中获取。
@Component
public class ToolRegistry {
@Autowired
private ObjectProvider<AgentExecutor> agentExecutorProvider;
public void executeSubTask(String subTask) {
AgentExecutor executor = agentExecutorProvider.getIfAvailable();
if (executor != null) {
executor.execute(subTask);
}
}
}
这样,ToolRegistry 并不在初始化时就依赖 AgentExecutor,而是等到真正需要时才获取,打破了创建时的循环。
总结:在设计 Agent 系统时,遇到循环依赖,第一反应不应该是加 @Lazy 或依赖 Spring 的三级缓存来解决,而是停下来审视分层。通常循环依赖意味着两个类之间的关系过于紧密,应该引入接口、事件或中介者来明确职责边界。ToolRegistry 只负责工具的管理和查找,它不应该去主动执行任务;执行任务的职责应该由 AgentExecutor 或独立的任务调度器来承担。理顺这些边界,循环依赖往往就自然消失了。
2、进阶题:Spring 中@Lazy 注解的作用是什么?如何解决循环依赖?¶
难度级别:⭐⭐⭐⭐(三级缓存、提前暴露、构造器注入失效、@Lazy 原理)
1️⃣ Common Answer
@Lazy 是懒加载,用的时候才创建 Bean。循环依赖的话,Spring 会自动解决,加@Lazy 也可以。但是构造器注入的循环依赖解决不了。
2️⃣ Impressive Answer
-
@Lazy 的本质:注入一个代理对象,真实 Bean 第一次使用时才创建。可打破循环依赖,也可优化启动速度(大 Bean 懒加载)。
-
Spring 循环依赖解决方案:setter/字段注入的循环依赖靠三级缓存解决:
- 一级缓存:成品 Bean
- 二级缓存:早期暴露的 Bean(未填充属性)
-
三级缓存:ObjectFactory,用于生成 AOP 代理的早期引用
-
构造器注入失效原因:构造器执行时 Bean 还没暴露,无法注入循环依赖。解决:字段/ setter 注入,或其中一个加
@Lazy。 -
最佳实践:推荐构造器注入(不可变、易测试),循环依赖时局部用@Lazy 打破环,而不是全局改用字段注入。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 说了@Lazy 和三级缓存但较浅 | @Lazy 原理→三级缓存→构造器失效→最佳实践 |
| 技术深度 | 不知道三级缓存的具体作用 | 清楚三级缓存各自存放什么 |
| 实践经验 | 没说如何优雅解决 | 推荐构造器注入 + 局部@Lazy 的组合 |
| 面试官印象 | 知道现象 | 理解 Spring 设计哲学,有架构权衡能力 |
3、进阶题:@Autowired、@Resource、@Inject 三种注入方式的区别?什么时候用构造器注入?¶
难度级别:⭐⭐⭐(按类型注入 vs 按名称注入、JSR-250/JSR-330 规范、构造器注入不可变性)
1️⃣ Common Answer
@Autowired 是 Spring 的注解,按类型注入。@Resource 是 Java 标准的,按名称注入。@Inject 也是 Java 标准的,和 @Autowired 差不多。构造器注入比较好,Spring 官方推荐。
2️⃣ Impressive Answer
- 三种注入方式对比:
| 维度 | @Autowired | @Resource | @Inject |
|---|---|---|---|
| 来源 | Spring 框架 | JSR-250(Java 标准) | JSR-330(Java 标准) |
| 匹配策略 | 先按类型,再按名称 | 先按名称,再按类型 | 按类型(同 @Autowired) |
| 必要性 | required=false 可选注入 | 无此属性 | 无此属性 |
| 配合限定 | @Qualifier | name 属性 | @Named |
- 构造器注入的四大优势:
- 不可变性:依赖声明为
final,对象创建后不可修改,线程安全。 - 避免 NPE:构造器注入保证依赖在使用前一定已注入,不会出现字段为 null 的情况。
- 便于单测:不依赖 Spring 容器,直接
new传入 mock 对象即可测试。 -
暴露设计问题:构造器参数过多时,说明类职责过重,提醒重构。
-
Spring 官方推荐:Spring 4.3+ 单构造器可省略 @Autowired;Spring 团队明确推荐构造器注入作为首选方式。
-
Agent 场景:工具注册器需要注入多个
ToolProvider实现时,用构造器注入List<ToolProvider>保证不可变性,配合@Qualifier或@Primary控制优先级。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 零散说了几个区别 | 表格对比 + 四大优势,结构清晰 |
| 技术深度 | 不知道匹配策略的差异 | 清楚先按类型/名称的匹配顺序 |
| 实践经验 | 只说"官方推荐" | 解释推荐的四个具体原因 |
| 面试官印象 | 知道有三种但说不清区别 | 有选型依据,理解设计意图 |
3.6 Spring 依赖注入方式¶
1、基础题:Spring 的依赖注入有哪几种方式?@Autowired 和 @Resource 有什么区别?¶
难度级别:⭐⭐(构造器注入、Setter 注入、字段注入、注解差异)
1️⃣ Common Answer
Spring 的依赖注入有三种方式:构造器注入、Setter 注解注入和字段注入。@Autowired 是 Spring 的注解,@Resource 是 JDK 的注解。@Autowired 默认按类型注入,@Resource 默认按名称注入。一般推荐用构造器注入。
2️⃣ Impressive Answer
- 三种注入方式:
- 构造器注入:通过构造函数注入,依赖对象在 Bean 创建时就必须提供,保证 Bean 不可变且完全初始化。
- Setter 注入:通过 setter 方法注入,依赖对象可以在 Bean 创建后设置,支持可选依赖和循环依赖。
-
字段注入:直接在字段上加 @Autowired,不推荐使用——无法进行单元测试(无法 mock 依赖)、隐藏依赖关系、违反单一职责原则。
-
@Autowired vs @Resource 核心区别:
- 来源不同:@Autowired 是 Spring 注解,@Resource 是 JSR-250 标准(JDK 自带)。
- 匹配策略:@Autowired 默认按类型匹配(byType),类型冲突时按 @Qualifier 指定名称;@Resource 默认按名称匹配(byName),找不到名称再按类型。
-
注入位置:@Autowired 可以用在构造器、字段、方法、参数上;@Resource 只能用在字段和 setter 方法上。
-
最佳实践:优先使用构造器注入(强制依赖、不可变、便于测试),可选依赖用 Setter 注入,避免字段注入。Spring 4.3+ 单构造器可省略 @Autowired。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 简单列举三种方式 | 区分匹配策略、注入位置、最佳实践 |
| 技术深度 | 不知道字段注入的缺点 | 清楚构造器注入的优势和字段注入的问题 |
| 实践经验 | 没有提到 Spring 4.3+ 的特性 | 知道省略 @Autowired 的版本特性 |
| 面试官印象 | 知道基本用法 | 有编码规范意识,理解设计权衡 |
2、进阶题:Spring 的依赖注入底层是如何实现的?AutowiredAnnotationBeanPostProcessor 的工作原理?¶
难度级别:⭐⭐⭐(BeanPostProcessor、依赖解析、三级缓存)
1️⃣ Common Answer
依赖注入是通过 BeanPostProcessor 实现的。AutowiredAnnotationBeanPostProcessor 会扫描 Bean 的字段和方法,找到 @Autowired 注解,然后从容器中找到对应的 Bean 注入进去。如果找不到就报错。
2️⃣ Impressive Answer
- AutowiredAnnotationBeanPostProcessor 工作流程:
- 注册阶段:实现了
MergedBeanDefinitionPostProcessor,在 Bean 定义合并后(postProcessMergedBeanDefinition)扫描类中的 @Autowired、@Value、@Inject 注解,构建注入元数据(InjectionMetadata)缓存。 -
注入阶段:实现了
InstantiationAwareBeanPostProcessor,在 Bean 实例化后、属性填充前(postProcessProperties)执行注入逻辑。 -
依赖解析完整链路:
AutowiredAnnotationBeanPostProcessor.postProcessProperties()
→ 遍历 InjectionMetadata 中的注入点
→ DependencyDescriptor.resolveDependency()
→ DefaultListableBeanFactory.doResolveDependency()
→ 根据类型查找候选 Bean(findAutowireCandidates)
→ 如果有多个候选,按 @Qualifier、@Primary、优先级排序
→ 从三级缓存获取/创建 Bean
- 关键细节:
- 懒加载支持:@Lazy 注解会生成代理对象,延迟到实际使用时才注入真实 Bean。
- 循环依赖处理:构造器注入无法解决循环依赖(因为 Bean 还没创建),Setter/字段注入通过三级缓存解决。
-
Optional 支持:@Autowired + Optional
允许依赖不存在时注入 Optional.empty()。 -
Agent 场景:LLM Provider 接口有多个实现(OpenAIProvider、ClaudeProvider、QwenProvider),通过 @Qualifier("openai") 或 @Primary 指定默认实现,避免
NoUniqueBeanDefinitionException。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"扫描注入" | 完整链路:注册→注入→依赖解析→三级缓存 |
| 技术深度 | 不知道两个 PostProcessor 接口 | 清楚 MergedBeanDefinition 和 InstantiationAware 的作用 |
| 实践经验 | 没有提到 @Lazy 和 Optional | 知道循环依赖的限制和解决方式 |
| 面试官印象 | 知道有这个类 | 理解依赖注入的完整生命周期 |
3、场景题:Agent 服务中有多个 LLM Provider 实现(OpenAI、Claude、通义千问),如何用 Spring 依赖注入实现策略模式动态切换?¶
难度级别:⭐⭐⭐(策略模式、@Conditional、动态切换、配置驱动)
1️⃣ Common Answer
可以定义一个 LLMProvider 接口,然后每个厂商实现这个接口。用一个配置文件指定用哪个 Provider,然后在代码里根据配置注入对应的实现。或者用 @Qualifier 注解指定。
2️⃣ Impressive Answer
- 策略模式设计:
// 统一接口
public interface LLMProvider {
String chat(String prompt, String model);
}
// 多个实现
@Service("openai")
public class OpenAIProvider implements LLMProvider { ... }
@Service("claude")
public class ClaudeProvider implements LLMProvider { ... }
@Service("qwen")
public class QwenProvider implements LLMProvider { ... }
- 动态切换方案一:Map 注入(推荐)
@Service
public class AgentService {
private final Map<String, LLMProvider> providerMap;
@Autowired
public AgentService(Map<String, LLMProvider> providerMap) {
this.providerMap = providerMap; // 自动注入所有实现,key = bean name
}
public String execute(String prompt, String providerName) {
LLMProvider provider = providerMap.get(providerName);
if (provider == null) {
throw new IllegalArgumentException("Provider not found: " + providerName);
}
return provider.chat(prompt, getModel(providerName));
}
}
- 动态切换方案二:配置驱动 + @Conditional
@Configuration
public class LLMConfig {
@Bean
@ConditionalOnProperty(name = "agent.llm.provider", havingValue = "openai")
public LLMProvider openaiProvider() { return new OpenAIProvider(); }
@Bean
@ConditionalOnProperty(name = "agent.llm.provider", havingValue = "claude")
public LLMProvider claudeProvider() { return new ClaudeProvider(); }
}
- 多租户场景:不同租户使用不同 Provider,通过 ThreadLocal + Map 实现:
@Service
public class TenantLLMProvider {
private final Map<String, LLMProvider> providerMap;
public String chat(String prompt) {
String tenantId = TenantContext.getTenantId();
String providerName = tenantConfigService.getProvider(tenantId);
return providerMap.get(providerName).chat(prompt);
}
}
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了接口+实现 | Map 注入、配置驱动、多租户三种方案 |
| 技术深度 | 不知道 Map 自动注入机制 | 清楚 Spring 的 Map/List 自动注入特性 |
| 实践经验 | 没有多租户场景 | 有生产环境多租户隔离经验 |
| 面试官印象 | 会用策略模式 | 有动态切换和多租户的完整方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @Qualifier 和 @Primary 的区别是什么? | 多个 Bean 时的冲突解决策略 |
| Spring 如何解决循环依赖? | 依赖注入的边界情况 |
| @Lazy 注解的作用是什么? | 延迟注入和循环依赖的解决方案 |
3.7 Spring Cache 缓存抽象¶
1、基础题:Spring Cache 的 @Cacheable、@CachePut、@CacheEvict 分别有什么作用?¶
难度级别:⭐⭐(缓存注解、缓存策略、key 生成)
1️⃣ Common Answer
@Cacheable 是缓存方法结果,下次调用直接返回缓存。@CachePut 是每次都执行方法,然后把结果放入缓存。@CacheEvict 是删除缓存。可以指定缓存名称和 key。
2️⃣ Impressive Answer
- 三个核心注解:
- @Cacheable:先查缓存,命中则直接返回,不命中则执行方法并将结果存入缓存。适用于读多写少的查询方法。
- @CachePut:每次都执行方法,然后将结果存入缓存(覆盖旧值)。适用于需要更新缓存的写操作。
-
@CacheEvict:删除缓存(可以指定 key 或 allEntries=true 清空整个缓存区)。适用于删除或更新操作,保证数据一致性。
-
关键参数:
value/cacheNames:指定缓存区名称(如 "users"、"tools")。key:缓存 key 的 SpEL 表达式(如#userId、#user.id),默认是方法参数。condition:满足条件才缓存(如condition = "#userId > 1000")。-
unless:不满足条件才缓存(如unless = "#result == null")。 -
使用示例:
@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User getUser(Long userId) { ... }
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) { ... }
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(Long userId) { ... }
@Caching(evict = {
@CacheEvict(value = "users", key = "#userId"),
@CacheEvict(value = "userOrders", allEntries = true)
})
public void deleteUserAndOrders(Long userId) { ... }
- 注意事项:@Cacheable 和 @CachePut 不能用在同一个类的方法上互相调用(因为 Spring AOP 代理限制,内部调用不会触发缓存逻辑)。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 简单描述三个注解 | 包含关键参数、SpEL 表达式、@Caching 组合 |
| 技术深度 | 不知道内部调用不生效 | 清楚 AOP 代理限制和解决方案 |
| 实践经验 | 没有具体代码示例 | 有完整的 CRUD 缓存示例 |
| 面试官印象 | 知道基本用法 | 有生产环境缓存实践经验 |
2、进阶题:Spring Cache 的底层抽象是什么?如何集成 Redis 作为缓存后端?CacheManager 的工作原理?¶
难度级别:⭐⭐⭐(Cache 接口、CacheManager、序列化、过期策略)
1️⃣ Common Answer
Spring Cache 有一个 Cache 接口,然后有 RedisCache、ConcurrentMapCache 这些实现。集成 Redis 需要引入 spring-boot-starter-data-redis,然后配置 Redis 连接。CacheManager 是管理缓存的。
2️⃣ Impressive Answer
- 核心抽象:
- Cache 接口:定义缓存的基本操作(get、put、evict、clear),实现类有
ConcurrentMapCache(本地内存)、RedisCache(Redis)、CaffeineCache(Caffeine)。 -
CacheManager 接口:管理多个 Cache 实例,根据 cacheName 获取对应的 Cache。
-
集成 Redis 完整配置:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 默认过期时间
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put("users", config.entryTtl(Duration.ofHours(1)));
cacheConfigurations.put("tools", config.entryTtl(Duration.ofMinutes(10)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigurations)
.transactionAware() // 支持事务,事务回滚时清除缓存
.build();
}
- CacheManager 工作原理:
- 启动时根据 @Cacheable 的 value/cacheNames 创建对应的 Cache 实例。
- 运行时根据 cacheName 从 CacheManager 获取 Cache,然后执行缓存操作。
-
RedisCache 的 key 生成规则:
缓存区名::实际key(如users::1001)。 -
序列化选择:
- StringRedisSerializer:key 序列化,可读性好。
- GenericJackson2JsonRedisSerializer:value 序列化,支持对象类型信息(但会存储 @class 字段)。
-
Jackson2JsonRedisSerializer:value 序列化,不存储类型信息(需要指定 Class)。
-
Agent 场景:工具调用结果缓存时,key 使用
toolName::hash(params)(参数哈希避免 key 过长),value 使用 JSON 序列化,过期时间根据工具特性设置(如天气 10 分钟,搜索 1 小时)。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了引入依赖 | 完整配置:TTL、序列化、多缓存区、事务支持 |
| 技术深度 | 不知道 key 生成规则 | 清楚 Redis 的 key 命名和序列化选择 |
| 实践经验 | 没有提到过期策略 | 有不同缓存区的差异化 TTL 配置 |
| 面试官印象 | 会用 Redis 缓存 | 有生产环境缓存架构设计能力 |
3、场景题:Agent 的工具调用结果需要缓存(相同参数的工具调用直接返回缓存),如何用 Spring Cache 实现并设置合理的过期策略?¶
难度级别:⭐⭐⭐(参数哈希、TTL 策略、缓存穿透、降级)
1️⃣ Common Answer
可以用 @Cacheable 注解在工具调用方法上。key 用工具名称和参数生成。设置一个过期时间,比如 5 分钟。如果缓存查不到就正常调用工具。
2️⃣ Impressive Answer
- 工具调用缓存设计:
@Service
public class ToolExecutor {
@Cacheable(
value = "toolResults",
key = "#toolName + '::' + #params.hashCode()",
unless = "#result == null || #result.error != null"
)
public ToolResult executeTool(String toolName, Map<String, Object> params) {
// 调用外部工具 API
}
@CacheEvict(value = "toolResults", key = "#toolName + '::' + #params.hashCode()")
public void invalidateTool(String toolName, Map<String, Object> params) {
// 手动清除缓存
}
}
- 差异化 TTL 策略(根据工具特性):
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
// 天气工具:数据变化快,短 TTL
configs.put("weather", config.entryTtl(Duration.ofMinutes(10)));
// 搜索工具:数据相对稳定,长 TTL
configs.put("search", config.entryTtl(Duration.ofHours(1)));
// 知识库查询:数据稳定,超长 TTL
configs.put("kb", config.entryTtl(Duration.ofHours(24)));
// 计算工具:结果不变,永久缓存
configs.put("calculator", config.entryTtl(Duration.ofDays(7)));
return RedisCacheManager.builder(factory)
.withInitialCacheConfigurations(configs)
.build();
}
-
防止缓存穿透:工具调用失败时不缓存(unless 条件),避免缓存空值导致后续请求一直失败。
-
缓存降级:Redis 不可用时降级到本地缓存(Caffeine),保证服务可用性:
@Configuration
public class CacheConfig {
@Bean
@Primary
public CacheManager compositeCacheManager(
RedisCacheManager redisCacheManager,
CaffeineCacheManager caffeineCacheManager) {
return new CompositeCacheManager(
caffeineCacheManager, // 本地缓存优先
redisCacheManager // Redis 缓存
);
}
}
- 监控与告警:通过 Micrometer 暴露
cache.hits、cache.misses、cache.puts指标,计算缓存命中率;设置告警(命中率 < 60% 时检查 TTL 策略)。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了 @Cacheable | 参数哈希→差异化 TTL→防穿透→降级→监控 |
| 技术深度 | 不知道参数哈希和缓存穿透 | 清楚缓存设计的完整链路 |
| 实践经验 | 没有差异化 TTL 和降级 | 有生产环境多级缓存和监控经验 |
| 面试官印象 | 会用基础缓存 | 有完整的缓存架构设计能力 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Redis 的持久化机制(RDB、AOF)是什么? | 缓存后端的数据可靠性 |
| 如何解决缓存雪崩、缓存击穿、缓存穿透? | 缓存三大经典问题 |
| Spring @Transactional 和缓存如何配合? | 事务回滚时缓存一致性问题 |
3.8 Spring 异步编程¶
1、基础题:@Async 注解如何使用?需要哪些前置配置?¶
难度级别:⭐⭐(@EnableAsync、线程池配置、返回值)
1️⃣ Common Answer
在方法上加 @Async 注解就可以异步执行。需要在启动类或配置类上加 @EnableAsync 开启异步支持。方法可以返回 Future 或 CompletableFuture。
2️⃣ Impressive Answer
- 前置配置:
- 在启动类或配置类上加
@EnableAsync注解。 -
配置线程池(可选,默认使用
SimpleAsyncTaskExecutor,每次创建新线程,不推荐)。 -
自定义线程池配置:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(50); // 最大线程数
executor.setQueueCapacity(200); // 队列容量
executor.setKeepAliveSeconds(60); // 空闲线程存活时间
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
executor.initialize();
return executor;
}
}
- 使用方式:
@Service
public class AsyncService {
@Async("taskExecutor") // 指定线程池
public void asyncMethod() {
// 异步执行
}
@Async
public CompletableFuture<String> asyncWithReturn() {
return CompletableFuture.completedFuture("result");
}
}
- 注意事项:
- @Async 不能用在同一个类的方法上互相调用(AOP 代理限制)。
- 异步方法的异常无法被调用方捕获,需要通过
AsyncUncaughtExceptionHandler处理。 - 返回
CompletableFuture可以链式调用和组合多个异步任务。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了加注解 | 完整配置:@EnableAsync→线程池→使用方式→注意事项 |
| 技术深度 | 不知道默认线程池的问题 | 清楚 SimpleAsyncTaskExecutor 的缺陷 |
| 实践经验 | 没有线程池参数配置 | 有生产环境线程池调优经验 |
| 面试官印象 | 会用 @Async | 有异步编程的完整实践经验 |
2、进阶题:@Async 的底层实现原理是什么?如何自定义线程池?异步方法的异常如何处理?¶
难度级别:⭐⭐⭐(AOP 代理、AsyncAnnotationBeanPostProcessor、异常处理)
1️⃣ Common Answer
@Async 是通过 AOP 实现的,会生成代理对象。自定义线程池就是配置一个 ThreadPoolTaskExecutor Bean。异步方法的异常可以用 try-catch 捕获,或者实现 AsyncUncaughtExceptionHandler。
2️⃣ Impressive Answer
- 底层实现原理:
@EnableAsync导入AsyncConfigurationSelector,注册ProxyAsyncConfiguration。AsyncAnnotationBeanPostProcessor扫描 @Async 注解,为 Bean 创建 AOP 代理。-
代理拦截器
AnnotationAsyncExecutionInterceptor将方法调用提交到线程池执行。 -
线程池选择策略:
- 如果 @Async 指定了线程池名称,使用指定的线程池。
- 如果未指定,查找名为
taskExecutor的TaskExecutorBean。 -
如果没有,使用默认的
SimpleAsyncTaskExecutor(每次创建新线程)。 -
异常处理机制:
- 无返回值方法:异常无法被调用方捕获,需要全局处理器:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Async method error: {}", method.getName(), ex);
// 发送告警、记录日志等
};
}
}
- 有返回值方法:异常封装在
CompletableFuture中,调用方通过exceptionally()处理:
@Async
public CompletableFuture<String> asyncMethod() {
return CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("error");
});
}
// 调用方
asyncMethod()
.exceptionally(ex -> {
log.error("Error", ex);
return "fallback";
});
- 事务与异步:@Async 和 @Transactional 不能同时用在同一个方法上(异步方法在新线程执行,事务上下文丢失)。如果需要事务,在异步方法内部调用的方法上加 @Transactional。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了 AOP | 完整链路:注解→配置→代理→拦截器→线程池 |
| 技术深度 | 不知道异常处理的两种方式 | 清楚无返回值和有返回值的异常处理差异 |
| 实践经验 | 没有提到事务问题 | 知道 @Async 和 @Transactional 的冲突 |
| 面试官印象 | 会用 @Async | 理解异步编程的底层机制和边界情况 |
3、场景题:Agent 并行调用多个工具(如同时查询天气、搜索知识库、查询数据库),如何用 @Async + CompletableFuture 实现并行工具调用?¶
难度级别:⭐⭐⭐(并行执行、超时控制、结果聚合、异常处理)
1️⃣ Common Answer
可以把每个工具调用方法加上 @Async 注解,然后返回 CompletableFuture。用 CompletableFuture.allOf 等待所有任务完成。设置一个超时时间。
2️⃣ Impressive Answer
- 异步工具调用设计:
@Service
public class AgentToolService {
@Async("toolExecutor")
public CompletableFuture<ToolResult> queryWeather(Map<String, Object> params) {
WeatherResult result = weatherService.query(params);
return CompletableFuture.completedFuture(result);
}
@Async("toolExecutor")
public CompletableFuture<ToolResult> searchKb(Map<String, Object> params) {
SearchResult result = kbService.search(params);
return CompletableFuture.completedFuture(result);
}
@Async("toolExecutor")
public CompletableFuture<ToolResult> queryDatabase(Map<String, Object> params) {
DbResult result = dbService.query(params);
return CompletableFuture.completedFuture(result);
}
}
- 并行调用与结果聚合:
@Service
public class AgentExecutor {
public List<ToolResult> executeParallel(List<ToolCall> calls) {
List<CompletableFuture<ToolResult>> futures = calls.stream()
.map(call -> switch (call.getType()) {
case "weather" -> toolService.queryWeather(call.getParams())
.exceptionally(ex -> ToolResult.error(ex));
case "kb" -> toolService.searchKb(call.getParams())
.exceptionally(ex -> ToolResult.error(ex));
case "db" -> toolService.queryDatabase(call.getParams())
.exceptionally(ex -> ToolResult.error(ex));
default -> CompletableFuture.completedFuture(ToolResult.error("Unknown type"));
})
.toList();
// 等待所有任务完成(带超时)
CompletableFuture<Void> allOf = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
try {
allOf.get(30, TimeUnit.SECONDS); // 30 秒超时
} catch (TimeoutException e) {
log.warn("Tool execution timeout");
futures.forEach(f -> f.cancel(true));
}
// 收集结果
return futures.stream()
.map(CompletableFuture::join)
.toList();
}
}
- 线程池配置(工具调用场景):
@Bean("toolExecutor")
public ThreadPoolTaskExecutor toolExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20); // 核心线程数(根据工具数量)
executor.setMaxPoolSize(100); // 最大线程数(峰值并发)
executor.setQueueCapacity(500); // 队列容量
executor.setKeepAliveSeconds(120); // 空闲线程存活时间
executor.setThreadNamePrefix("tool-async-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
- 超时与降级:使用
orTimeout设置单个工具超时,使用exceptionally提供降级结果:
@Async("toolExecutor")
public CompletableFuture<ToolResult> queryWeatherWithTimeout(Map<String, Object> params) {
return CompletableFuture.supplyAsync(() -> {
return weatherService.query(params);
}, taskExecutor)
.orTimeout(10, TimeUnit.SECONDS) // 单个工具 10 秒超时
.exceptionally(ex -> ToolResult.fallback("Weather service unavailable"));
}
- 监控与告警:通过 Micrometer 暴露
async.task.duration、async.task.success、async.task.failure指标;设置告警(失败率 > 5% 时检查工具可用性)。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了 @Async + allOf | 完整方案:异步方法→并行调用→超时→降级→监控 |
| 技术深度 | 不知道超时控制和降级 | 清楚 CompletableFuture 的链式调用和异常处理 |
| 实践经验 | 没有线程池参数配置 | 有生产环境并发调优经验 |
| 面试官印象 | 会用异步编程 | 有完整的并发编程和容错方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| CompletableFuture 的常用方法有哪些? | 异步编程的核心 API |
| 线程池的拒绝策略有哪些? | 高并发场景下的资源管理 |
| 如何实现异步任务的链式调用和组合? | 复杂业务场景的异步编排 |
4.3 AOP 原理¶
4、进阶题:Spring AOP 的两种代理方式(JDK 动态代理 vs CGLIB)有什么区别?¶
难度级别:⭐⭐⭐(接口代理 vs 类代理、ASM 字节码、自调用失效问题、@Transactional 失效场景)
1️⃣ Common Answer
JDK 代理需要实现接口,CGLIB 不需要。CGLIB 是用字节码生成子类的方式。Spring 默认用 JDK 代理,但如果没有接口就用 CGLIB。@Transactional 有时候不生效是因为代理的问题。
2️⃣ Impressive Answer
-
代理机制不同:JDK 动态代理基于接口,运行时通过反射生成代理类,要求目标类必须实现接口;CGLIB 通过 ASM 字节码框架生成目标类的子类,不依赖接口,但 final 类/方法无法代理。
-
Spring 的选择策略:Spring Boot 2.x 起默认优先使用 CGLIB(
spring.aop.proxy-target-class=true),减少因没有接口导致代理失败的问题;也可以强制指定。 -
自调用失效是核心陷阱:同一个 Bean 内部方法 A 调用方法 B,B 上的 AOP 不生效,因为调用绕过了代理对象,直接走
this;解决方案是注入自身代理(AopContext.currentProxy())或拆分到另一个 Bean。 -
@Transactional 失效场景:private 方法不能被 CGLIB 子类重写,方法必须是 public 且非 final;另外同类自调用也会导致事务失效。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了表面区别,没有逻辑层次 | 分四点,机制→策略→陷阱→实战,层层递进 |
| 技术深度 | 不知道 Spring Boot 2.x 默认 CGLIB | 说出版本变化、ASM、AopContext 解决方案 |
| 实践经验 | 没有提自调用失效的解决方法 | 给出 AopContext 和拆 Bean 两种修复方案 |
| 面试官印象 | 知道区别但不知道坑 | 有踩坑经验,理解代理原理,能防患于未然 |
2、进阶题:Spring AOP 的完整执行链路是什么?ProxyFactory、PointcutAdvisor、MethodInterceptor 各自的角色?¶
难度级别:⭐⭐⭐(AOP 链路、核心组件、责任链模式)
1️⃣ Common Answer
Spring AOP 通过代理对象拦截方法调用。ProxyFactory 创建代理,PointcutAdvisor 定义切点和方法拦截器,MethodInterceptor 执行拦截逻辑。拦截器链按顺序执行。
2️⃣ Impressive Answer
- 完整执行链路:
调用代理方法
→ ReflectiveMethodInvocation.proceed()
→ 按顺序执行 Interceptor 链
→ ExposeInvocationInterceptor(暴露当前调用)
→ MethodBeforeAdvice(前置通知)
→ 目标方法
→ AfterReturningAdvice(后置通知)
→ AfterThrowingAdvice(异常通知)
→ AfterAdvice(最终通知)
→ 返回结果
- 核心组件角色:
- ProxyFactory:代理工厂,根据 Advisor 创建代理对象(JDK 或 CGLIB)。
- PointcutAdvisor:切面定义,包含 Pointcut(切点,匹配哪些方法)和 Advice(通知,做什么)。
- MethodInterceptor:方法拦截器,Advice 的实现,负责拦截方法调用(如
MethodBeforeAdviceInterceptor)。 -
AdvisedSupport:持有所有 Advisor 和配置信息。
-
责任链模式:
ReflectiveMethodInvocation维护 interceptorList 和 currentIndex,proceed()递归调用下一个拦截器。 -
代码示例:
// AOP 执行链路示例
Proxy proxy = ProxyFactory.getProxy(classLoader, interfaces);
proxy.method() →
ReflectiveMethodInvocation.proceed() →
interceptor1.invoke() →
interceptor2.invoke() →
target.method() →
interceptor2.invoke() →
interceptor1.invoke() →
result
- Agent 场景:
ToolCallInterceptor拦截工具调用方法,记录耗时、捕获异常、重试逻辑。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了三个组件 | 完整链路+责任链模式+代码示例 |
| 技术深度 | 不知道责任链模式 | 清楚 ReflectiveMethodInvocation 的递归机制 |
| 实践经验 | 没有代码示例 | 有完整的执行流程图 |
| 面试官印象 | 背过组件名 | 理解 AOP 的完整执行机制 |
3、场景题:如何用 Spring AOP 实现 Agent 工具调用的统一耗时监控和异常捕获?¶
难度级别:⭐⭐⭐(自定义切面、注解驱动、异常处理)
1️⃣ Common Answer
可以定义一个切面,用 @Around 注解拦截工具调用方法。里面用 StopWatch 记录耗时,try-catch 捕获异常,记录日志。用 @Pointcut 定义切点表达式。
2️⃣ Impressive Answer
- 自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToolCall {
String toolName() default "";
int maxRetries() default 0;
}
- 切面实现:
@Aspect
@Component
@Slf4j
public class ToolCallAspect {
@Autowired
private MeterRegistry meterRegistry;
@Around("@annotation(toolCall)")
public Object monitorToolCall(ProceedingJoinPoint pjp, ToolCall toolCall) throws Throwable {
String toolName = toolCall.toolName().isEmpty()
? pjp.getSignature().getName()
: toolCall.toolName();
Timer.Sample sample = Timer.start(meterRegistry);
long startTime = System.currentTimeMillis();
try {
// 重试逻辑
int retries = toolCall.maxRetries();
Throwable lastException = null;
for (int i = 0; i <= retries; i++) {
try {
Object result = pjp.proceed();
// 记录成功指标
sample.stop(Timer.builder("tool.call.duration")
.tag("tool", toolName)
.tag("status", "success")
.register(meterRegistry));
meterRegistry.counter("tool.call.count",
"tool", toolName, "status", "success").increment();
return result;
} catch (Exception e) {
lastException = e;
if (i < retries) {
log.warn("Tool call failed, retrying {}/{}", i + 1, retries, e);
Thread.sleep(100 * (i + 1)); // 指数退避
}
}
}
throw lastException;
} catch (Throwable e) {
// 记录失败指标
sample.stop(Timer.builder("tool.call.duration")
.tag("tool", toolName)
.tag("status", "error")
.register(meterRegistry));
meterRegistry.counter("tool.call.count",
"tool", toolName, "status", "error").increment();
log.error("Tool call failed: {}", toolName, e);
throw e;
}
}
}
- 使用示例:
@Service
public class WeatherService {
@ToolCall(toolName = "weather_query", maxRetries = 2)
public WeatherResult queryWeather(String city) {
// 调用天气 API
return weatherApi.query(city);
}
}
- 监控指标:
tool.call.duration:工具调用耗时(P50/P95/P99)tool.call.count:工具调用次数(成功/失败)- 配合 Prometheus + Grafana 可视化监控
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了 @Around | 自定义注解+完整切面+重试逻辑+指标监控 |
| 技术深度 | 不知道指标监控 | 有 Micrometer + Prometheus 集成 |
| 实践经验 | 没有重试逻辑 | 有指数退避重试和完整异常处理 |
| 面试官印象 | 会用 AOP 但不深入 | 有生产环境完整的监控方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Spring 事务 @Transactional 的实现原理? | AOP 的典型应用场景,考察对 AOP 的理解 |
| JDK 动态代理和 CGLIB 的性能对比? | 代理方式的选择,和 AOP 代理创建相关 |
| @AspectJ 注解有哪些?各自的执行顺序? | AOP 切面编程的基础知识 |
4、场景题:Agent 的工具调用需要统一做鉴权和耗时日志,如何用 AOP 实现?¶
难度级别:⭐⭐⭐(自定义注解 + @Around、切点表达式、ProceedingJoinPoint、异常处理)
1️⃣ Common Answer
可以用 @Aspect 写一个切面,然后用 @Before 和 @After 分别做鉴权和日志。在切点那里写个表达式匹配工具调用的方法就行。鉴权失败就抛个异常。耗时的话用 System.currentTimeMillis 算一下。
2️⃣ Impressive Answer
-
定义标记注解:自定义
@ToolAuth(permission="xxx"),标注在需要鉴权的工具方法上,便于精确切入,不污染无关方法。 -
@Around 统一拦截:用
@Around("@annotation(toolAuth)")匹配注解,在ProceedingJoinPoint.proceed()前后分别做鉴权和耗时统计;用try-finally确保耗时日志必然记录,不受异常影响。 -
鉴权逻辑:proceed 前从上下文(ThreadLocal 或请求头)取 token,校验失败直接抛
AuthException,不执行工具;鉴权通过才放行。 -
日志结构化:记录工具名(
joinPoint.getSignature().getName())、入参摘要、耗时、是否成功,便于后续 tracing 关联;生产上配合 MDC 注入 traceId。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | @Before/@After 分散,逻辑割裂 | @Around 统一,try-finally 保证完整性 |
| 技术深度 | 不知道自定义注解切点的好处 | 注解驱动切点,精准、低侵入、易扩展 |
| 实践经验 | 没有考虑异常时耗时丢失的问题 | finally 块保证耗时必记,考虑到异常路径 |
| 面试官印象 | 会用 AOP 但没有工程化意识 | 注解 + 切面 + 结构化日志,有生产落地思维 |
5、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Spring 事务为什么在同类方法自调用时失效? | AOP 代理自调用失效的直接应用,考察对代理机制的深度理解 |
| BeanPostProcessor 和 AOP 的关系是什么? | AOP 代理本质上是通过 BeanPostProcessor 创建的,考察 Spring 扩展点体系 |
| @Transactional 和自定义 AOP 切面的执行顺序如何控制? | 考察 @Order 注解和 Ordered 接口在多切面场景下的优先级控制 |
4.4 @Transactional 失效场景¶
1、基础题:@Transactional 注解有哪些常见的失效场景?¶
难度级别:⭐⭐(方法修饰符、异常类型、自调用、传播机制)
1️⃣ Common Answer
@Transactional 失效的情况有:方法不是 public、异常被捕获了、异常类型不对、在同一个类里面调用。还有就是数据库不支持事务。
2️⃣ Impressive Answer
-
方法修饰符问题:
@Transactional只能作用于public方法,private、protected、package-private会失效(Spring AOP 代理限制)。 -
异常类型问题:默认只回滚
RuntimeException和Error,检查型异常(如IOException)不回滚。需指定rollbackFor = Exception.class。 -
自调用问题:同一个类内部方法调用不经过代理,事务失效。
@Service
public class UserService {
@Transactional
public void methodA() {
methodB(); // 事务失效,直接调用不经过代理
}
@Transactional
public void methodB() { }
}
-
传播机制问题:
NOT_SUPPORTED、NEVER会挂起当前事务;嵌套事务(NESTED)需要数据库支持(如 MySQL)。 -
事务管理器配置:多数据源时未指定
@Transactional(transactionManager = "xxxTransactionManager")。 -
数据库引擎不支持:如 MySQL 的 MyISAM 引擎不支持事务。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 零散列举了几个 | 6 个场景,每个都有解释和解决方案 |
| 技术深度 | 不知道自调用原理 | 清楚代理机制导致自调用失效 |
| 实践经验 | 没有代码示例 | 有自调用问题的解决方案 |
| 面试官印象 | 背过场景 | 理解失效原理和解决方案 |
2、进阶题:Spring 事务的 7 种传播机制分别是什么?REQUIRED 和 REQUIRES_NEW 的区别?¶
难度级别:⭐⭐⭐(传播机制、事务隔离、嵌套事务)
1️⃣ Common Answer
传播机制有 REQUIRED、REQUIRESNEW、SUPPORTS、NOTSUPPORTED、MANDATORY、NEVER、NESTED。REQUIRED 是加入当前事务,REQUIRES_NEW 是新建事务。
2️⃣ Impressive Answer
- 7 种传播机制:
- REQUIRED(默认):如果当前有事务,加入;否则新建。
- REQUIRES_NEW:总是新建事务,挂起当前事务。
- SUPPORTS:如果当前有事务,加入;否则非事务执行。
- NOT_SUPPORTED:总是非事务执行,挂起当前事务。
- MANDATORY:必须当前有事务,否则抛异常。
- NEVER:必须非事务执行,否则抛异常。
-
NESTED:嵌套事务,当前有事务时创建 savepoint,失败回滚到 savepoint。
-
REQUIRED vs REQUIRES_NEW:
- REQUIRED:加入外层事务,内外层一起提交或回滚。
-
REQUIRES_NEW:独立事务,外层失败不影响内层,内层失败不影响外层(除非异常传播)。
-
嵌套事务(NESTED)场景:
@Transactional
public void outer() {
try {
inner(); // 嵌套事务,失败只回滚 inner
} catch (Exception e) {
// outer 可以继续执行
}
}
@Transactional(propagation = Propagation.NESTED)
public void inner() {
// 失败回滚到 savepoint
}
- Agent 场景:
- REQUIRED:Agent 执行流程(工具调用→结果存储→状态更新)在一个事务中,保证一致性。
- REQUIRES_NEW:记录审计日志(即使主流程失败,日志也要记录)。
- NESTED:批量处理工具调用,单个失败不影响其他。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了两个传播机制 | 7 种传播机制+对比+嵌套事务示例 |
| 技术深度 | 不知道 NESTED 的 savepoint 机制 | 清楚嵌套事务的回滚机制 |
| 实践经验 | 没有场景应用 | 有 Agent 事务边界设计 |
| 面试官印象 | 背过传播机制 | 理解传播机制的实际应用 |
3、场景题:Agent 执行多步骤任务(工具调用→结果存储→状态更新),如何设计事务边界保证数据一致性?¶
难度级别:⭐⭐⭐(事务边界设计、异常处理、补偿机制)
1️⃣ Common Answer
可以在最外层方法上加 @Transactional,把所有步骤包在一个事务里。如果中间某步失败,整个事务回滚。也可以用分布式事务。
2️⃣ Impressive Answer
- 方案一:单事务(简单场景)
@Service
public class AgentExecutor {
@Transactional(rollbackFor = Exception.class)
public AgentResult execute(AgentRequest request) {
// 1. 工具调用
ToolResult toolResult = toolCallService.call(request.getTool());
// 2. 结果存储
executionRepository.save(toolResult);
// 3. 状态更新
agentRepository.updateStatus(request.getAgentId(), "COMPLETED");
return AgentResult.success(toolResult);
}
}
- 方案二:独立事务 + 补偿(复杂场景)
@Service
public class AgentExecutor {
@Transactional(rollbackFor = Exception.class)
public AgentResult execute(AgentRequest request) {
ToolResult toolResult = null;
try {
// 1. 工具调用(可能调用外部服务,不放在事务中)
toolResult = toolCallService.call(request.getTool());
// 2. 结果存储(独立事务)
saveExecution(toolResult);
// 3. 状态更新(独立事务)
updateAgentStatus(request.getAgentId(), "COMPLETED");
return AgentResult.success(toolResult);
} catch (Exception e) {
// 补偿:回滚状态
updateAgentStatus(request.getAgentId(), "FAILED");
throw e;
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveExecution(ToolResult result) {
executionRepository.save(result);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateAgentStatus(String agentId, String status) {
agentRepository.updateStatus(agentId, status);
}
}
- 方案三:Saga 模式(长流程)
@Service
public class AgentSaga {
public AgentResult execute(AgentRequest request) {
List<SagaStep> steps = Arrays.asList(
new ToolCallStep(request),
new SaveExecutionStep(),
new UpdateStatusStep("COMPLETED")
);
return sagaExecutor.execute(steps, new AgentCompensation(request));
}
}
// 补偿操作
public class AgentCompensation implements Compensable {
public void compensate(AgentRequest request) {
updateAgentStatus(request.getAgentId(), "FAILED");
deleteExecution(request.getExecutionId());
}
}
- 选型建议:
- 单事务:所有操作在同一个数据库,简单场景
- 独立事务:涉及外部调用,需要部分成功
- Saga:长流程、多服务、需要补偿机制
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了单事务 | 三种方案+完整代码+选型建议 |
| 技术深度 | 不知道 Saga 模式 | 清楚不同场景的事务边界设计 |
| 实践经验 | 没有补偿机制 | 有完整的补偿和回滚策略 |
| 面试官印象 | 会用 @Transactional | 有分布式事务设计能力 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Spring 事务隔离级别有哪些? | 事务并发控制,和事务传播机制配套 |
| 数据库事务的 ACID 特性? | 事务理论基础,理解事务的必要性 |
| 分布式事务解决方案有哪些? | 跨服务事务,和事务边界设计相关 |
6、进阶题:BeanPostProcessor 和 BeanFactoryPostProcessor 的区别?各自的典型应用场景?¶
难度级别:⭐⭐⭐(扩展点体系、Bean 定义修改 vs 实例增强、AOP 代理创建时机)
1️⃣ Common Answer
BeanPostProcessor 是在 Bean 初始化前后做一些处理,BeanFactoryPostProcessor 是在 Bean 定义加载后做修改。Spring AOP 就是通过 BeanPostProcessor 实现的。两个都是 Spring 的扩展点。
2️⃣ Impressive Answer
-
作用时机不同:
BeanFactoryPostProcessor在 Bean 定义加载完成后、实例化之前执行,操作的是BeanDefinition(元数据);BeanPostProcessor在 Bean 实例化之后、初始化前后执行,操作的是 Bean 实例本身。 -
典型应用:
BeanFactoryPostProcessor的经典实现是PropertySourcesPlaceholderConfigurer,负责解析${}占位符替换配置值;BeanPostProcessor的经典实现是AutowiredAnnotationBeanPostProcessor(处理 @Autowired 注入)和AbstractAutoProxyCreator(创建 AOP 代理)。 -
AOP 代理创建时机:AOP 代理是在
BeanPostProcessor.postProcessAfterInitialization()阶段创建的,这就是为什么BeanFactoryPostProcessor中注入的 Bean 不会被 AOP 代理——它们在代理创建之前就已经实例化了。 -
使用注意:
BeanFactoryPostProcessor中不要提前触发 Bean 实例化(如 getBean()),否则这些 Bean 会跳过后续的 BeanPostProcessor 处理链,导致注入、AOP 等功能失效。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"前后处理",没有区分操作对象 | 清晰区分:操作 BeanDefinition vs 操作 Bean 实例 |
| 技术深度 | 不知道 AOP 代理的创建时机 | 能解释为什么 BeanFactoryPostProcessor 中的 Bean 不被代理 |
| 实践经验 | 没有提到具体实现类 | 举出 PropertySourcesPlaceholderConfigurer、AutowiredAnnotationBeanPostProcessor 等经典实现 |
| 面试官印象 | 知道有两个扩展点 | 理解 Spring 容器启动的完整生命周期,能避免扩展点误用 |
7、场景题:如何实现一个自定义注解,让 Agent 的工具方法自动注册到工具列表中?¶
难度级别:⭐⭐⭐(自定义 BeanPostProcessor + 注解扫描 + 注册表模式)
1️⃣ Common Answer
可以自定义一个注解比如 @AgentTool,然后写一个 BeanPostProcessor,在 Bean 初始化后扫描方法上有没有这个注解,有的话就注册到一个 Map 里面。调用的时候从 Map 里取就行。
2️⃣ Impressive Answer
-
定义工具注解:
@AgentTool(name="search", description="搜索工具"),标注在方法上,携带工具名称和描述信息,用于生成 Function Definition 给模型。 -
BeanPostProcessor 自动扫描:
@Component
public class AgentToolRegistrar implements BeanPostProcessor {
private final ToolRegistry registry;@
Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {
for(Method method: bean.getClass().getDeclaredMethods()) {
AgentTool annotation = method.getAnnotation(AgentTool.class);
if(annotation != null) {
ToolDefinition definition = ToolDefinition.builder().name(annotation.name()).description(annotation.description()).parameters(extractParameters(method)).executor((args) - > ReflectionUtils.invokeMethod(method, bean, args)).build();
registry.register(definition);
}
}
return bean;
}
}
-
参数自动提取:通过反射读取方法参数的类型和
@Param注解,自动生成 JSON Schema,减少手动维护成本。 -
生产级考虑:加
@Order(Ordered.LOWEST_PRECEDENCE)确保在 AOP 代理之后执行(扫描的是代理后的 Bean);工具注册表支持热更新,配合配置中心动态启停工具。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 思路正确但缺少细节 | 注解定义→扫描注册→参数提取→生产考虑,完整链路 |
| 技术深度 | 不知道扫描时机和 AOP 的关系 | 明确 @Order 保证在代理之后扫描 |
| 实践经验 | 只说了 Map 存储 | 有 ToolDefinition 结构化设计和反射调用 |
| 面试官印象 | 会用 BeanPostProcessor | 能设计完整的工具注册框架,有架构能力 |
2.3 Spring 事务管理¶
1、基础题:@Transactional 注解的作用是什么?Spring 事务的传播机制有哪些?¶
难度级别:⭐⭐(事务边界、PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、隔离级别)
Answer
@Transactional 是 Spring 声明式事务的核心注解,标记在方法或类上,Spring 会通过 AOP 代理在方法执行前后自动开启/提交/回滚事务。
传播机制(8 种):
-
REQUIRED(默认):当前有事务就加入,没有就新建
-
REQUIRES_NEW:不管有没有事务,都新建一个(当前事务会被挂起)
-
SUPPORTS:有事务就加入,没有就以非事务方式执行
-
NOT_SUPPORTED:以非事务方式执行,如果有事务就挂起
-
MANDATORY:必须有事务,否则抛异常
-
NEVER:必须没有事务,否则抛异常
-
NESTED:嵌套事务,基于 Savepoint 实现,可部分回滚
隔离级别:DEFAULT(数据库默认)、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE
2、进阶题:@Transactional 在什么情况下会失效?¶
难度级别:⭐⭐⭐⭐(自调用、非 public 方法、异常类型、代理机制、异步方法)
1️⃣ Common Answer
@Transactional 失效的情况挺多的。比如同类方法自调用的时候,因为 AOP 代理的问题会失效。还有方法不是 public 的也会失效。另外如果抛的异常不是 RuntimeException 也可能不回滚。
2️⃣ Impressive Answer
我从 5 个维度梳理@Transactional 失效场景:
-
自调用失效(最常见):同一个 Bean 内部方法 A 调用带@Transactional 的方法 B,B 的事务不生效。原因:AOP 代理对象才是事务载体,自调用走的是
this.B(),绕过了代理。解决:注入自身代理调用,或用AopContext.currentProxy()。 -
访问权限限制:@Transactional 只能标注在 public 方法上。protected/private 方法即使加上也不会生效,因为 Spring AOP 默认只代理 public 方法。
-
异常类型不匹配:默认只回滚 RuntimeException 和 Error。如果抛的是受检异常(Checked Exception),需要显式指定
@Transactional(rollbackFor = Exception.class)。 -
捕获异常未抛出:方法内部 try-catch 把异常吞了,事务管理器感知不到异常,自然不会回滚。解决:catch 后手动调用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。 -
异步方法失效:@Async 方法的事务 propagation 可能不符合预期,因为异步方法在另一个线程执行,事务上下文无法传递。解决:在异步方法上单独加@Transactional。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 覆盖面 | 只说了 3 种常见情况 | 系统梳理 5 大类失效场景 |
| 技术深度 | 知道现象但说不清原理 | 每个场景都解释根本原因 |
| 实践经验 | 没有给出解决方案 | 每个失效都有对应的修复方案 |
| 面试官印象 | 踩过一些坑但不系统 | 有系统性总结,能预判和规避问题 |
3、场景题:Agent 执行多步工具调用,如何保证部分失败时数据一致性?¶
难度级别:⭐⭐⭐⭐(分布式事务、Saga 模式、补偿机制、本地事务表)
1️⃣ Common Answer
可以用@Transactional 把整个流程包起来,失败了就回滚。但是如果调用了外部服务,本地事务管不到,这时候可以用 Seata 这样的分布式事务框架。
2️⃣ Impressive Answer
这个问题本质是长事务 + 跨服务的场景,我分三层设计:
-
本地事务兜底:单服务内多步操作(如写 DB + 发消息)用
@Transactional保证原子性;消息发送用事务消息表,本地事务提交后由定时任务异步投递,保证最终一致。 -
跨服务 Saga 模式:Agent 多步工具调用涉及多个微服务时,用 Saga 编排:每个步骤有正向操作和补偿操作,步骤 3 失败则逆向执行步骤 2→1 的补偿。Spring Cloud State Machine 或自研引擎可实现。
-
幂等 + 重试:每个工具调用设计幂等性(唯一键/状态机),失败后安全重试;配合本地消息表 + 事务性轮询,确保补偿操作必然执行。
-
用户感知设计:长事务不阻塞 HTTP 响应,采用异步回调或 SSE 推送执行进度,用户可随时查看中间状态。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 问题拆解 | 只说了@Transactional 和 Seata | 分层设计:本地→跨服务→用户体验 |
| 技术深度 | 不知道 Saga 和补偿机制 | 熟悉 Saga 模式、事务消息表、幂等设计 |
| 实践经验 | 没有考虑长事务的用户体验 | 提出异步回调 + SSE 推送方案 |
| 面试官印象 | 知道分布式事务概念 | 有长链路事务落地经验,考虑周全 |
4、进阶题:Spring 事务的底层实现原理?TransactionManager 和 TransactionSynchronizationManager 的作用?¶
难度级别:⭐⭐⭐(PlatformTransactionManager、ThreadLocal 绑定连接、事务同步回调)
1️⃣ Common Answer
Spring 事务底层是通过 TransactionManager 来管理的,它负责开启、提交和回滚事务。具体实现有 DataSourceTransactionManager 和 JpaTransactionManager。事务信息是通过 ThreadLocal 绑定到当前线程的。
2️⃣ Impressive Answer
-
PlatformTransactionManager 三板斧:
getTransaction()(获取/创建事务)、commit()、rollback()。DataSourceTransactionManager实现中,getTransaction()从 DataSource 获取 Connection 并设置autoCommit=false,然后将 Connection 绑定到 ThreadLocal。 -
TransactionSynchronizationManager 的核心作用:通过 ThreadLocal 维护当前线程的事务资源(Connection、Session 等)和同步回调。
@Transactional方法内所有 DAO 操作通过DataSourceUtils.getConnection()获取的都是同一个 Connection,这就是事务生效的根本原因。 -
事务同步回调:
TransactionSynchronization接口提供beforeCommit()、afterCommit()、afterCompletion()等钩子。典型应用:事务提交后发消息(避免事务未提交就发消息导致消费者查不到数据)。
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
messageProducer.send(event); // 事务提交后才发消息
}
});
- 声明式事务的 AOP 实现:
TransactionInterceptor(MethodInterceptor)拦截 @Transactional 方法,调用 TransactionManager 管理事务生命周期,本质是 AOP + 模板方法模式。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了 TransactionManager 的职责 | Manager→ThreadLocal 绑定→同步回调→AOP 实现,全链路 |
| 技术深度 | 不知道 Connection 如何绑定到线程 | 清楚 ThreadLocal + DataSourceUtils 的协作机制 |
| 实践经验 | 没提到事务同步回调 | 有 afterCommit 发消息的实战经验 |
| 面试官印象 | 知道有 TransactionManager | 理解事务底层实现,能解释"为什么事务能生效" |
5、场景题:Agent 执行工具调用时需要"部分提交",前 3 步成功就保存,第 4 步失败不影响前 3 步,如何设计?¶
难度级别:⭐⭐⭐⭐(REQUIRES_NEW、编程式事务、Savepoint、事务模板)
1️⃣ Common Answer
可以把每一步都用 REQUIRES_NEW 传播级别,这样每步都是独立事务,失败了不影响前面的。或者用 try-catch 把每步包起来,失败了记录日志继续执行。
2️⃣ Impressive Answer
这个问题有三种方案,适用不同场景:
方案一:REQUIRES_NEW 独立事务
@Service
public class ToolExecutionService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public StepResult executeStep(ToolStep step) {
// 每步独立事务,成功即提交
return toolInvoker.invoke(step);
}
}
优点:简单直接;缺点:每步一个连接,连接池压力大。
方案二:编程式事务 + Savepoint
public void executeWorkflow(List<ToolStep> steps) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
for (int i = 0; i < steps.size(); i++) {
Object savepoint = status.createSavepoint();
try {
toolInvoker.invoke(steps.get(i));
// 成功则释放 savepoint,继续
status.releaseSavepoint(savepoint);
} catch (Exception e) {
// 失败则回滚到 savepoint,前面的步骤不受影响
status.rollbackToSavepoint(savepoint);
recordFailure(steps.get(i), e);
}
}
transactionManager.commit(status); // 统一提交成功的步骤
}
优点:单连接,性能好;缺点:需要数据库支持 Savepoint(MySQL InnoDB 支持)。
方案三:TransactionTemplate 分段提交 每 N 步用 TransactionTemplate.execute() 包一次,兼顾性能和隔离性。
选型建议:步骤间无依赖用方案一(简单);步骤间有依赖且需要部分回滚用方案二(精细);批量处理用方案三(折中)。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 方案数量 | 只说了 REQUIRES_NEW | 三种方案对比,各有适用场景 |
| 技术深度 | 不知道 Savepoint | 有 Savepoint 的完整代码实现 |
| 实践经验 | 没考虑连接池压力 | 分析了每种方案的优缺点 |
| 面试官印象 | 只会一种方案 | 能根据场景选型,有工程权衡能力 |
2.4 Spring 循环依赖¶
1、进阶题:Spring 如何解决循环依赖?三级缓存的作用分别是什么?¶
难度级别:⭐⭐⭐(三级缓存、singletonObjects、earlySingletonObjects、singletonFactories、提前暴露)
1️⃣ Common Answer
Spring 用三级缓存解决循环依赖。一级缓存 singletonObjects 存放完全初始化好的 Bean,二级缓存 earlySingletonObjects 存放半成品 Bean,三级缓存 singletonFactories 存放 ObjectFactory。当 A 依赖 B,B 又依赖 A 时,B 可以从三级缓存拿到 A 的早期引用。需要三级而不是两级是因为 AOP 代理需要延迟创建。
2️⃣ Impressive Answer
- 三级缓存的核心作用:
- 一级缓存 singletonObjects:存放完全初始化好的 Bean(实例化+属性填充+初始化完成)。
- 二级缓存 earlySingletonObjects:存放提前暴露的半成品 Bean(已实例化但未填充属性),用于解决循环依赖。
-
三级缓存 singletonFactories:存放 ObjectFactory 工厂,用于生成提前暴露的 Bean,关键是可能生成 AOP 代理对象。
-
循环依赖解决流程:A 创建时发现依赖 B → 暴露 A 的 ObjectFactory 到三级缓存 → B 创建时发现依赖 A → 从三级缓存获取 A 的 ObjectFactory → 生成 A 的早期引用(如果是 AOP 则生成代理)→ 放入二级缓存 → B 完成初始化 → A 完成初始化 → A 从二级缓存移到一级缓存。
-
为什么需要三级而非两级:如果 A 需要被 AOP 代理,代理对象必须在实例化后、初始化前创建。三级缓存中的 ObjectFactory 延迟执行,只有真正发生循环依赖时才创建代理,避免无循环依赖时也创建代理的性能浪费。两级缓存无法做到这个延迟创建。
-
Prototype 无法解决:Prototype 作用域的 Bean 不缓存,每次都创建新实例,Spring 不解决其循环依赖,会直接抛异常。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了三级缓存的名字 | 清晰解释每级缓存的作用和时机 |
| 技术深度 | 不知道为什么需要三级 | 解释了 ObjectFactory 延迟创建代理的设计 |
| 实践经验 | 没提到 Prototype 无法解决 | 知道 Prototype 循环依赖会直接报错 |
| 面试官印象 | 背过概念但理解不深 | 理解设计意图,知道延迟创建的优化 |
2、进阶题:为什么构造器注入无法解决循环依赖?@Lazy 如何解决?¶
难度级别:⭐⭐⭐(构造器注入时机、代理延迟加载、Provider 模式)
1️⃣ Common Answer
构造器注入在实例化阶段就需要依赖,这时候 Bean 还没创建完,没法放入三级缓存。@Lazy 可以解决,它是注入一个代理,第一次调用的时候才去拿真实的 Bean。
2️⃣ Impressive Answer
-
构造器注入无法解决的根本原因:构造器注入在实例化阶段就需要依赖对象,此时 Bean 还没创建完成,无法提前暴露到三级缓存。而 Setter 注入在实例化之后、属性填充阶段才需要依赖,此时已经可以提前暴露了。
-
@Lazy 的解决原理:注入的不是真实 Bean,而是一个 CGLIB 生成的代理对象。首次调用代理方法时,代理才从 Spring 容器获取真实 Bean。这样在构造阶段只需要一个代理占位,不触发真实 Bean 的创建,打破了循环依赖。
-
其他延迟加载方案:
ObjectProvider<T>:注入 ObjectProvider,调用getObject()时才获取 Bean。ApplicationContext.getBean():在需要时手动从容器获取,但增加了容器耦合。-
@Autowired(required = false):设为非必须,配合@PostConstruct延迟初始化。 -
最佳实践:优先通过重构消除循环依赖(提取中间层、事件驱动),而非用 @Lazy 绕过。@Lazy 只是技术手段,治标不治本,可能掩盖设计问题。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了构造器阶段需要依赖 | 区分实例化阶段和属性填充阶段 |
| 技术深度 | 不知道 CGLIB 代理的具体机制 | 解释代理延迟获取真实 Bean 的原理 |
| 实践经验 | 只提到 @Lazy | 给出多种延迟加载方案和最佳实践 |
| 面试官印象 | 知道现象但不知道原理 | 理解设计权衡,有架构思维 |
3、场景题:Spring Boot 2.6+ 默认禁止循环依赖,如何优雅地重构消除循环依赖?¶
难度级别:⭐⭐⭐(依赖倒置、事件驱动解耦、中间层抽取)
1️⃣ Common Answer
Spring Boot 2.6 默认不允许循环依赖了。可以通过重构代码来解决,比如提取一个公共的 Bean,或者用事件机制。也可以设置配置允许循环依赖,但最好还是重构。
2️⃣ Impressive Answer
-
Spring Boot 2.6 的变化:
spring.main.allow-circular-references默认从true改为false,强制要求消除循环依赖,这是为了推动更健康的架构设计。 -
重构方案一:提取中间层
- 重构方案二:事件驱动解耦
@Service
public class AgentService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void executeAgent() {
// 执行逻辑
eventPublisher.publishEvent(new AgentCompletedEvent(data));
}
}
@Service
public class ToolService {
@EventListener
public void handleAgentCompleted(AgentCompletedEvent event) {
// 响应事件,不直接依赖 AgentService
}
}
- 重构方案三:依赖倒置
public interface ToolExecutor {
void execute(ToolRequest request);
}
@Service
public class AgentService {
private final ToolExecutor executor; // 依赖接口
}
@Service
public class ToolService implements ToolExecutor {
// 不依赖 AgentService
}
- Agent 场景实战:AgentService 和 ToolService 互相依赖时,通过
ToolRegistry中间层解耦,ToolService 注册自己到 Registry,AgentService 从 Registry 获取工具,不直接依赖 ToolService。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"提取公共 Bean" | 三种方案,每种都有完整代码示例 |
| 技术深度 | 不知道事件驱动的优势 | 解释事件驱动解耦的原理和适用场景 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 场景的具体解耦方案 |
| 面试官印象 | 知道要重构但不会设计 | 能根据场景选型,有架构设计能力 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Spring 三级缓存中 ObjectFactory 和 AOP 代理的关系? | 深入理解为什么需要三级缓存而非两级 |
| @DependsOn 注解的作用和使用场景? | Bean 初始化顺序控制,和循环依赖的区别 |
| Spring 的 Bean 作用域(Singleton/Prototype)对循环依赖的影响? | Prototype 作用域无法解决循环依赖 |
Spring 容器启动与配置体系¶
Spring 容器启动与配置体系¶
1、基础题:@Configuration 和 @Component 的区别?@Bean 方法的 Full 模式代理机制?¶
难度级别:⭐⭐(配置类 vs 普通组件、CGLIB 代理、Lite 模式)
Answer
- @Configuration vs @Component:
@Configuration是配置类,Spring 会用 CGLIB 代理,保证@Bean方法调用返回同一个实例。-
@Component是普通组件,@Bean方法每次调用都会创建新实例。 -
Full 模式代理机制:
@Configuration默认是 Full 模式,Spring 生成子类代理,拦截@Bean方法调用,从容器获取已注册的 Bean。-
如果配置类没有
@Bean方法互相调用,可以用@Configuration(proxyBeanMethods = false)切换到 Lite 模式,避免代理开销。 -
代码示例:
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() { return new HikariDataSource(); }
@Bean
public JdbcTemplate jdbcTemplate() {
// Full 模式:调用 dataSource() 返回同一个实例
return new JdbcTemplate(dataSource());
}
}
2、进阶题:Spring 容器 refresh() 的 12 个核心步骤是什么?¶
难度级别:⭐⭐⭐⭐(容器启动流程、BeanFactoryPostProcessor、BeanPostProcessor、单例初始化)
1️⃣ Common Answer
refresh() 就是 Spring 容器启动的方法,大概就是准备环境、加载 Bean 定义、创建 Bean、初始化这些步骤。具体的 12 个步骤记不太清了,反正就是容器启动的流程。
2️⃣ Impressive Answer
-
prepareRefresh():准备刷新上下文,设置启动时间、初始化属性源、验证必需属性。
-
obtainFreshBeanFactory():创建 BeanFactory,加载 BeanDefinition(解析 @Component、@Bean 等)。
-
prepareBeanFactory():配置 BeanFactory,注册 BeanPostProcessor(如 ApplicationContextAwareProcessor)、设置类加载器。
-
postProcessBeanFactory():子类扩展点,可以修改 BeanFactory(如添加 Web 环境特有的 Scope)。
-
invokeBeanFactoryPostProcessors():执行 BeanFactoryPostProcessor(如 @PropertySource、@ConfigurationClassPostProcessor 解析配置类)。
-
registerBeanPostProcessors():注册 BeanPostProcessor(如 AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor)。
-
initMessageSource():初始化国际化资源(MessageSource)。
-
initApplicationEventMulticaster():初始化事件广播器(ApplicationEventMulticaster)。
-
onRefresh():子类扩展点,创建 Web 容器(如 Tomcat、Jetty)。
-
registerListeners():注册监听器,广播早期事件。
-
finishBeanFactoryInitialization():初始化所有非懒加载的单例 Bean(实例化、属性注入、初始化回调)。
-
finishRefresh():完成刷新,发布 ContextRefreshedEvent,启动生命周期 Bean(如 SmartLifecycle)。
-
关键理解:
- 第 5 步
invokeBeanFactoryPostProcessors是最关键的,@Configuration、@ComponentScan都在这里解析。 - 第 11 步
finishBeanFactoryInitialization完成 Bean 的完整生命周期(包括 AOP 代理)。 -
第 12 步
finishRefresh后,容器就完全就绪了。 -
Agent 场景:在
onRefresh()步骤启动 Agent Server(如 gRPC Server),在finishRefresh()后发布AgentReadyEvent,通知下游服务 Agent 已就绪。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"加载 Bean、创建 Bean" | 完整 12 步,每步都有明确作用 |
| 技术深度 | 不知道 BeanFactoryPostProcessor 的执行时机 | 清楚配置解析在第 5 步,Bean 初始化在第 11 步 |
| 实践经验 | 没有结合 Web 容器启动 | 知道 onRefresh() 启动 Tomcat,finishRefresh() 发布事件 |
| 面试官印象 | 背过流程但不理解 | 理解容器启动的设计意图和扩展点 |
3、场景题:Agent 平台多环境配置如何用 @Profile + @ConditionalOnProperty 管理?¶
难度级别:⭐⭐⭐(环境隔离、条件装配、配置优先级)
1️⃣ Common Answer
可以用 @Profile 区分开发、测试、生产环境,用 @ConditionalOnProperty 根据配置决定是否加载某个 Bean。比如开发环境用 Mock LLM,生产环境用真实 LLM。
2️⃣ Impressive Answer
- @Profile 环境隔离:
@Profile("dev")
@Configuration
public class DevConfig {
@Bean
public LLMProvider mockLLM() {
return new MockLLMProvider(); // 开发环境用 Mock
}
}
@Profile("prod")
@Configuration
public class ProdConfig {
@Bean
public LLMProvider openaiLLM() {
return new OpenAIProvider(); // 生产环境用真实 LLM
}
}
// 激活环境
@SpringBootApplication
public class AgentApplication {
public static void main(String[] args) {
SpringApplication.run(AgentApplication.class, "--spring.profiles.active=dev");
}
}
- @ConditionalOnProperty 功能开关:
@Configuration
public class ToolConfig {
@Bean
@ConditionalOnProperty(name = "agent.tools.enabled", havingValue = "true")
public ToolRegistry toolRegistry() {
return new DefaultToolRegistry(); // 工具功能开启时才加载
}
@Bean
@ConditionalOnMissingBean(ToolRegistry.class)
public ToolRegistry emptyToolRegistry() {
return new EmptyToolRegistry(); // 工具功能关闭时用空实现
}
}
// application-dev.yml
agent:
tools:
enabled: true // 开发环境开启工具
// application-prod.yml
agent:
tools:
enabled: false // 生产环境关闭工具
- 组合使用:
@Profile("dev")
@ConditionalOnProperty(name = "agent.mock.enabled")
@Configuration
public class DevMockConfig {
// 仅在 dev 环境且 agent.mock.enabled=true 时生效
}
-
配置优先级:
application-{profile}.yml>application.yml,环境特定配置会覆盖默认配置。 -
Agent 场景实战:开发环境用
@Profile("dev")+ Mock LLM + 本地向量库,测试环境用真实 LLM + 测试向量库,生产环境用企业级 LLM + 云向量库,通过@ConditionalOnProperty控制审计日志、计费等功能的开关。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"区分环境" | @Profile 和 @ConditionalOnProperty 结合使用,有完整代码 |
| 技术深度 | 不知道配置优先级 | 解释环境配置覆盖默认配置的机制 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 平台多环境配置的完整方案 |
| 面试官印象 | 知道注解但不会组合使用 | 能设计灵活的多环境配置方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @Import 注解的三种用法(普通类、ImportSelector、ImportBeanDefinitionRegistrar)? | 动态注册 Bean 的扩展方式 |
| Spring Boot 的自动配置原理(spring.factories / AutoConfiguration.imports)? | 基于条件装配的自动配置机制 |
| BeanFactoryPostProcessor 和 BeanPostProcessor 的区别? | 容器启动流程中的两个关键扩展点 |
3.2 Spring 设计模式与扩展点¶
1、基础题:Spring 中用到了哪些设计模式?举例说明¶
难度级别:⭐⭐(工厂、单例、代理、模板方法、观察者、策略、适配器)
1️⃣ Common Answer
Spring 用了很多设计模式。比如工厂模式用在 BeanFactory,单例模式是 Bean 默认的作用域,代理模式用在 AOP,模板方法用在 JdbcTemplate。还有观察者模式用在事件机制。
2️⃣ Impressive Answer
Spring 框架是设计模式的教科书级应用,我从 7 个核心模式来说:
-
工厂模式:
BeanFactory/ApplicationContext是 Bean 的工厂,根据 BeanDefinition 创建对象。FactoryBean接口让用户自定义复杂 Bean 的创建逻辑。 -
单例模式:Bean 默认 Singleton 作用域,通过三级缓存(
singletonObjects)保证全局唯一。不是传统的私有构造器单例,而是容器级别的单例。 -
代理模式:AOP 的核心,JDK 动态代理(接口)和 CGLIB(类)。
@Transactional、@Async、@Cacheable都是通过代理实现的。 -
模板方法模式:
JdbcTemplate、RestTemplate、TransactionTemplate——定义算法骨架,子类/回调实现具体步骤。 -
观察者模式:
ApplicationEvent+ApplicationListener,事件发布-订阅机制,实现组件间松耦合通信。 -
策略模式:
Resource接口的多种实现(ClassPathResource、FileSystemResource、UrlResource),根据前缀自动选择策略。 -
适配器模式:
HandlerAdapter适配不同类型的 Controller(注解式、实现 Controller 接口式),统一调用接口。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 零散列举了几个 | 7 个模式,每个都有具体类名和解释 |
| 技术深度 | 不知道 FactoryBean 和 BeanFactory 的区别 | 区分了容器级单例和传统单例 |
| 实践经验 | 只说了名字没有解释 | 每个模式都关联到具体的 Spring 组件 |
| 面试官印象 | 背过设计模式名称 | 理解 Spring 如何运用设计模式,有架构视角 |
Spring 事件机制¶
Spring 事件机制¶
2、进阶题:Spring 的事件机制(ApplicationEvent)如何实现?同步还是异步?¶
难度级别:⭐⭐⭐(ApplicationEventPublisher、ApplicationEventMulticaster、@EventListener、@Async)
1️⃣ Common Answer
Spring 的事件机制就是发布-订阅模式。用 ApplicationEventPublisher 发布事件,用 @EventListener 监听。默认是同步的,加 @Async 就变成异步了。
2️⃣ Impressive Answer
-
发布与监听:
ApplicationEventPublisher.publishEvent()发布事件;监听方式有两种:@EventListener注解(推荐)或实现ApplicationListener<T>接口。 -
默认同步执行:
SimpleApplicationEventMulticaster默认在发布线程中依次调用所有监听器。这意味着监听器抛异常会影响发布者,耗时操作会阻塞主流程。 -
异步化方案:
- 方式一:
@EventListener+@Async,需要@EnableAsync开启异步支持。 - 方式二:配置
ApplicationEventMulticaster的TaskExecutor,全局异步。 -
注意:异步事件的异常不会传播到发布者,需要在监听器内部单独处理。
-
事务事件:
@TransactionalEventListener(phase = AFTER_COMMIT)保证事务提交后才触发监听器,避免"事务未提交,消费者查不到数据"的问题。这是发消息场景的最佳实践。 -
执行顺序:多个监听器通过
@Order控制顺序;同步场景下按 Order 值从小到大执行。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"发布-订阅" | 同步→异步→事务事件→执行顺序,层层递进 |
| 技术深度 | 不知道同步的风险 | 清楚异常传播和阻塞问题 |
| 实践经验 | 没提到事务事件 | 知道 AFTER_COMMIT 的最佳实践 |
| 面试官印象 | 会用但不深入 | 理解事件机制的设计权衡和生产注意事项 |
3、场景题:如何用 Spring 事件机制实现 Agent 工具调用的审计日志?¶
难度级别:⭐⭐⭐(事件驱动解耦、异步审计、结构化日志)
1️⃣ Common Answer
可以定义一个工具调用事件,在工具调用完成后发布事件,然后用 @EventListener 监听这个事件,把调用信息写到数据库里。加上 @Async 就不会影响主流程了。
2️⃣ Impressive Answer
- 定义审计事件:
@Data
public class ToolInvocationEvent extends ApplicationEvent {
private String toolName;
private Object[] arguments;
private Object result;
private long durationMs;
private String userId;
private String traceId;
private boolean success;
}
-
AOP 切面发布事件:在
@Around切面中,工具调用完成后publishEvent,将调用信息封装为事件。切面只负责发布,不关心后续处理。 -
多监听器各司其职:
- 审计日志监听器:
@EventListener+@Async,异步写入审计日志表(DB)。 - 实时分析监听器:推送到 Kafka,供实时数据分析平台消费。
-
告警监听器:检测异常调用(如耗时 > 阈值、连续失败),触发告警通知。
-
核心优势:工具调用逻辑和审计逻辑完全解耦。新增审计需求(如合规审计、成本统计)只需加一个监听器,不修改任何已有代码,符合开闭原则。
-
生产考虑:异步监听器配置独立线程池,避免审计任务影响业务线程;监听器内部做好异常处理和重试,保证审计数据不丢失。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"发事件、写数据库" | 事件定义→AOP 发布→多监听器→生产考虑 |
| 技术深度 | 不知道多监听器的好处 | 清楚开闭原则和解耦优势 |
| 实践经验 | 没考虑线程池隔离 | 有独立线程池和异常处理方案 |
| 面试官印象 | 能实现基本功能 | 有完整的审计架构设计,考虑扩展性 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Spring 的 Aware 接口有哪些?各自的作用? | Spring 扩展点体系的一部分,考察对容器感知能力的理解 |
| @Order 和 Ordered 接口如何控制 Bean 的加载顺序? | 多个监听器/切面的执行顺序控制 |
| Spring SPI 机制(spring.factories)和 Java SPI 的区别? | 扩展点加载机制的对比 |
1、基础题:ApplicationEvent 和 @EventListener 的基本用法?¶
难度级别:⭐⭐(事件发布、事件监听、解耦)
Answer
- 自定义事件:
public class AgentCompletedEvent extends ApplicationEvent {
private final String agentId;
private final AgentResult result;
public AgentCompletedEvent(Object source, String agentId, AgentResult result) {
super(source);
this.agentId = agentId;
this.result = result;
}
}
- 发布事件:
@Service
public class AgentService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public AgentResult execute(String agentId, AgentRequest request) {
AgentResult result = doExecute(request);
eventPublisher.publishEvent(new AgentCompletedEvent(this, agentId, result));
return result;
}
}
- 监听事件:
@Component
public class AgentEventListener {
@EventListener
public void handleAgentCompleted(AgentCompletedEvent event) {
System.out.println("Agent " + event.getAgentId() + " completed");
}
}
2、进阶题:同步事件 vs 异步事件(@Async)的区别?@TransactionalEventListener 的作用?¶
难度级别:⭐⭐⭐(事件同步性、事务绑定、异步线程池)
1️⃣ Common Answer
同步事件就是发布后立即执行监听器,异步事件用 @Async 可以在别的线程执行。@TransactionalEventListener 是在事务提交后才执行事件监听器。
2️⃣ Impressive Answer
- 同步事件:
- 默认行为,发布事件的线程立即执行所有监听器。
- 优点:事务一致性,监听器可以回滚事务。
-
缺点:阻塞主流程,影响性能。
-
异步事件:
@Component
public class AsyncEventListener {
@Async("eventExecutor") // 指定线程池
@EventListener
public void handleAsync(AgentCompletedEvent event) {
// 异步执行,不阻塞主流程
}
}
- @TransactionalEventListener:
@Component
public class TransactionalEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(AgentCompletedEvent event) {
// 事务提交后才执行,确保数据已持久化
}
}
- 线程池配置:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("eventExecutor")
public Executor eventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("event-");
return executor;
}
}
- Agent 场景实战:Agent 执行完成后,同步事件更新数据库状态,异步事件记录日志、发送通知,
@TransactionalEventListener在事务提交后同步到审计系统。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"同步 vs 异步" | 解释三种事件机制,每种都有代码示例 |
| 技术深度 | 不知道 @TransactionalEventListener 的阶段 | 清楚事务绑定和事务阶段的选择 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 事件处理的完整方案 |
| 面试官印象 | 知道基本用法但不会选型 | 能根据场景选择合适的事件机制 |
3、场景题:Agent 工具调用完成后如何用事件机制解耦通知下游(日志、计费、审计)?¶
难度级别:⭐⭐⭐(事件驱动、解耦、异步处理)
1️⃣ Common Answer
工具调用完成后,发布一个 ToolCallCompletedEvent,然后有多个监听器分别处理日志、计费、审计。这样就不会在工具调用代码里写这些逻辑了。
2️⃣ Impressive Answer
- 定义事件:
public class ToolCallCompletedEvent extends ApplicationEvent {
private final String toolId;
private final ToolRequest request;
private final ToolResponse response;
private final long duration;
public ToolCallCompletedEvent(Object source, String toolId,
ToolRequest request, ToolResponse response, long duration) {
super(source);
this.toolId = toolId;
this.request = request;
this.response = response;
this.duration = duration;
}
}
- 发布事件:
@Service
public class ToolExecutor {
@Autowired
private ApplicationEventPublisher eventPublisher;
public ToolResponse execute(String toolId, ToolRequest request) {
long start = System.currentTimeMillis();
ToolResponse response = doExecute(toolId, request);
long duration = System.currentTimeMillis() - start;
eventPublisher.publishEvent(new ToolCallCompletedEvent(this, toolId, request, response, duration));
return response;
}
}
- 监听器解耦:
@Component
public class ToolCallListeners {
// 日志监听器(同步,确保日志不丢失)
@EventListener
public void logToolCall(ToolCallCompletedEvent event) {
log.info("Tool {} executed in {}ms", event.getToolId(), event.getDuration());
}
// 计费监听器(异步,不阻塞主流程)
@Async("billingExecutor")
@EventListener(condition = "#event.response.success")
public void chargeToolUsage(ToolCallCompletedEvent event) {
billingService.charge(event.getToolId(), event.getDuration());
}
// 审计监听器(事务提交后执行)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void auditToolCall(ToolCallCompletedEvent event) {
auditService.record(event.getToolId(), event.getRequest(), event.getResponse());
}
}
- 优势:
- 解耦:工具执行逻辑和日志、计费、审计逻辑完全分离。
- 扩展:新增下游处理只需添加监听器,无需修改工具执行代码。
-
性能:异步处理不阻塞主流程。
-
Agent 场景实战:Agent 调用工具后,通过事件机制通知日志系统记录调用链、计费系统计算费用、审计系统记录操作,工具执行代码保持简洁,易于维护。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"发布事件、监听事件" | 完整事件驱动架构,包含同步、异步、事务绑定 |
| 技术深度 | 不知道 @EventListener 的条件过滤 | 使用 condition 过滤成功调用,异步计费 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 工具调用事件处理的完整方案 |
| 面试官印象 | 知道事件驱动但不会设计 | 能设计解耦的事件驱动架构 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Spring 事件监听器的执行顺序(@Order)? | 多个监听器的执行顺序控制 |
| @Async 的线程池如何配置和监控? | 异步事件需要合理配置线程池 |
| Spring 事件和消息队列(如 Kafka)的区别? | 进程内事件 vs 进程间消息传递 |
Spring 条件装配与自动配置¶
Spring 条件装配与自动配置¶
1、基础题:@Conditional 系列注解有哪些?各自触发条件?¶
难度级别:⭐⭐(条件装配、类路径判断、Bean 存在性判断)
Answer
-
@Conditional:核心注解,配合 Condition 接口实现自定义条件。
-
常用条件注解:
@ConditionalOnClass:类路径存在指定类。@ConditionalOnMissingClass:类路径不存在指定类。@ConditionalOnBean:容器中存在指定 Bean。@ConditionalOnMissingBean:容器中不存在指定 Bean。@ConditionalOnProperty:配置属性满足条件。@ConditionalOnExpression:SpEL 表达式为 true。@ConditionalOnWebApplication:Web 环境。-
@ConditionalOnNotWebApplication:非 Web 环境。 -
示例:
@Configuration
public class LLMConfig {
@Bean
@ConditionalOnClass(name = "com.openai.OpenAI")
public LLMProvider openaiLLM() {
return new OpenAIProvider(); // 类路径存在 OpenAI 类时才创建
}
@Bean
@ConditionalOnMissingBean(LLMProvider.class)
public LLMProvider mockLLM() {
return new MockLLMProvider(); // 没有 LLMProvider 时才创建
}
}
2、进阶题:Spring Boot 自动配置的原理(spring.factories / AutoConfiguration.imports)?¶
难度级别:⭐⭐⭐⭐(自动配置机制、条件装配、EnableAutoConfiguration)
1️⃣ Common Answer
Spring Boot 启动时会加载 spring.factories 文件,里面配置了自动配置类,然后根据条件决定哪些 Bean 要创建。这样就不用手动配置了。
2️⃣ Impressive Answer
- 自动配置原理:
- Spring Boot 启动时,通过
@EnableAutoConfiguration导入AutoConfigurationImportSelector。 AutoConfigurationImportSelector从META-INF/spring.factories(Spring Boot 2.7 之前)或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 2.7+)加载自动配置类。-
自动配置类使用
@Conditional系列注解,根据类路径、配置、Bean 存在性等条件决定是否生效。 -
spring.factories 示例(Spring Boot 2.7 之前):
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.agent.autoconfigure.AgentAutoConfiguration,\
com.example.agent.autoconfigure.ToolAutoConfiguration
- AutoConfiguration.imports 示例(Spring Boot 2.7+):
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.agent.autoconfigure.AgentAutoConfiguration
com.example.agent.autoconfigure.ToolAutoConfiguration
- 自动配置类示例:
@Configuration
@ConditionalOnClass(AgentService.class)
@ConditionalOnProperty(prefix = "agent", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AgentProperties.class)
public class AgentAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AgentService agentService(AgentProperties properties) {
return new AgentService(properties);
}
@Bean
@ConditionalOnProperty(prefix = "agent.tools", name = "enabled", havingValue = "true")
public ToolRegistry toolRegistry() {
return new DefaultToolRegistry();
}
}
- 调试自动配置:
- 启动参数:
--debug或--logging.level.org.springframework.boot.autoconfigure=DEBUG。 -
查看报告:启动日志会输出哪些自动配置类生效/未生效。
-
Agent 场景实战:自定义 Agent Starter,通过自动配置让用户只需引入依赖和配置
agent.enabled=true,就能开箱即用 Agent 功能。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"加载 spring.factories" | 完整自动配置流程,包含条件装配和调试 |
| 技术深度 | 不知道 AutoConfiguration.imports 的变化 | 清楚 Spring Boot 2.7+ 的变化 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent Starter 的自动配置实现 |
| 面试官印象 | 背过概念但不深入 | 理解自动配置的设计意图和实现机制 |
3、场景题:如何自定义一个 Spring Boot Starter,让 Agent 工具库开箱即用?¶
难度级别:⭐⭐⭐(Starter 开发、自动配置、属性绑定)
1️⃣ Common Answer
创建一个 Spring Boot 项目,写一个自动配置类,用 @Bean 创建需要的 Bean,然后在 spring.factories 里配置这个类。这样用户引入依赖就能用了。
2️⃣ Impressive Answer
- 项目结构:
agent-spring-boot-starter/
├── src/main/java/
│ └── com/example/agent/
│ ├── autoconfigure/
│ │ └── AgentAutoConfiguration.java
│ ├── properties/
│ │ └── AgentProperties.java
│ └── service/
│ └── AgentService.java
└── src/main/resources/
└── META-INF/
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
- 属性配置类:
@ConfigurationProperties(prefix = "agent")
public class AgentProperties {
private boolean enabled = true;
private String llmProvider = "openai";
private String apiKey;
private double temperature = 0.7;
private Tools tools = new Tools();
public static class Tools {
private boolean enabled = true;
private List<String> includes = new ArrayList<>();
private List<String> excludes = new ArrayList<>();
// getters and setters
}
// getters and setters
}
- 自动配置类:
@Configuration
@ConditionalOnClass(AgentService.class)
@ConditionalOnProperty(prefix = "agent", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AgentProperties.class)
public class AgentAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AgentService agentService(AgentProperties properties) {
return new AgentService(properties);
}
@Bean
@ConditionalOnProperty(prefix = "agent.tools", name = "enabled", havingValue = "true")
public ToolRegistry toolRegistry(AgentProperties properties) {
DefaultToolRegistry registry = new DefaultToolRegistry();
properties.getTools().getIncludes().forEach(registry::register);
return registry;
}
}
- AutoConfiguration.imports:
- 用户使用:
<!-- pom.xml -->
<dependency>
<groupId>com.example</groupId>
<artifactId>agent-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
# application.yml
agent:
enabled: true
llm-provider: openai
api-key: ${OPENAI_API_KEY}
temperature: 0.7
tools:
enabled: true
includes:
- search-tool
- calculator-tool
- Agent 场景实战:开发
agent-tool-spring-boot-starter,自动注册常用工具(搜索、计算、天气等),用户只需引入依赖和配置工具列表,就能在 Agent 中使用这些工具。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"写自动配置类" | 完整 Starter 开发流程,包含项目结构和使用示例 |
| 技术深度 | 不知道 @ConfigurationProperties 的用法 | 清楚属性绑定和条件装配的结合 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent Tool Starter 的完整实现 |
| 面试官印象 | 知道概念但不会开发 | 能独立开发 Spring Boot Starter |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @SpringBootApplication 注解的三个核心注解是什么? | 理解自动配置的入口 |
| 如何排除自动配置类(@EnableAutoConfiguration.exclude)? | 精细化控制自动配置 |
| @ConditionalOnProperty 的 matchIfMissing 参数作用? | 配置不存在时的默认行为 |
Spring Bean 作用域与代理¶
Spring Bean 作用域与代理¶
1、基础题:Bean 的 6 种作用域分别是什么?各自适用场景?¶
难度级别:⭐⭐(作用域、生命周期、适用场景)
Answer
- 6 种作用域:
singleton:单例,容器中只有一个实例(默认)。prototype:原型,每次请求创建新实例。request:请求,每个 HTTP 请求一个实例(Web 环境)。session:会话,每个 HTTP Session 一个实例(Web 环境)。application:应用,ServletContext 生命周期内一个实例(Web 环境)。-
websocket:WebSocket,每个 WebSocket 一个实例(Web 环境)。 -
适用场景:
singleton:无状态服务(如 Service、Repository)。prototype:有状态对象(如命令对象、表单对象)。request:请求相关数据(如用户信息、请求参数)。session:会话相关数据(如购物车、用户登录信息)。-
application:全局共享数据(如配置缓存、统计计数器)。 -
示例:
@Scope("prototype")
@Component
public class ToolRequest {
private String toolId;
private Map<String, Object> parameters;
}
2、进阶题:Singleton Bean 注入 Prototype Bean 的陷阱?@Lookup 和 ObjectProvider 如何解决?¶
难度级别:⭐⭐⭐⭐(作用域代理、依赖注入陷阱、解决方案)
1️⃣ Common Answer
Singleton Bean 注入 Prototype Bean 时,Prototype Bean 只会创建一次,因为 Singleton Bean 只初始化一次。可以用 @Lookup 注解或者 ObjectProvider 来解决。
2️⃣ Impressive Answer
- 陷阱:
@Component
@Scope("singleton")
public class AgentService {
@Autowired
private ToolRequest toolRequest; // Prototype Bean
public void execute() {
// 每次都是同一个 toolRequest 实例,不是新的!
}
}
- Singleton Bean 只初始化一次,注入的 Prototype Bean 也只创建一次。
- 后续使用 `toolRequest` 时,仍然是同一个实例,违背了 Prototype 的设计意图。
- 解决方案一:@Lookup 注解:
@Component
@Scope("singleton")
public abstract class AgentService {
public void execute() {
ToolRequest toolRequest = getToolRequest(); // 每次调用都返回新实例
}
@Lookup
protected abstract ToolRequest getToolRequest(); // Spring 会生成子类实现
}
- 解决方案二:ObjectProvider:
@Component
@Scope("singleton")
public class AgentService {
@Autowired
private ObjectProvider<ToolRequest> toolRequestProvider;
public void execute() {
ToolRequest toolRequest = toolRequestProvider.getObject(); // 每次获取新实例
}
}
- 解决方案三:@Scope 代理:
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ToolRequest {
}
- Agent 场景实战:AgentService 是 Singleton,每次调用工具时需要新的 ToolRequest(Prototype),使用
ObjectProvider<ToolRequest>动态获取新实例。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"用 @Lookup 或 ObjectProvider" | 三种解决方案,每种都有完整代码和原理 |
| 技术深度 | 不知道 @Lookup 的 CGLIB 实现 | 解释代理机制和延迟获取的原理 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 工具请求的场景化解决方案 |
| 面试官印象 | 知道解决方案但不懂原理 | 理解作用域陷阱的设计原因和解决思路 |
3、场景题:Agent 会话级别的上下文(如对话历史)如何用 Session 作用域 Bean 管理?¶
难度级别:⭐⭐⭐(Session 作用域、会话管理、上下文隔离)
1️⃣ Common Answer
可以用 Session 作用域 Bean 存储对话历史,每个用户的会话都有独立的上下文。这样不同用户的对话不会互相干扰。
2️⃣ Impressive Answer
- Session 作用域 Bean:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AgentSessionContext {
private List<Message> conversationHistory = new ArrayList<>();
private String userId;
private Map<String, Object> metadata = new HashMap<>();
public void addMessage(Message message) {
conversationHistory.add(message);
}
public List<Message> getConversationHistory() {
return Collections.unmodifiableList(conversationHistory);
}
// getters and setters
}
- 在 Service 中使用:
@Service
public class AgentService {
@Autowired
private AgentSessionContext sessionContext; // 代理对象
public AgentResponse chat(AgentRequest request) {
// 每次请求都会从当前 Session 获取对应的上下文
sessionContext.addMessage(new Message("user", request.getContent()));
AgentResponse response = doChat(request);
sessionContext.addMessage(new Message("assistant", response.getContent()));
return response;
}
}
- 代理模式:
proxyMode = ScopedProxyMode.TARGET_CLASS:Spring 生成 CGLIB 代理,代理对象持有 Session 引用。-
每次调用代理对象的方法时,代理会从当前 Session 获取对应的真实 Bean。
-
Session 销毁:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AgentSessionContext implements HttpSessionListener {
@PreDestroy
public void cleanup() {
// Session 销毁时清理资源
conversationHistory.clear();
}
}
- Agent 场景实战:每个用户的 Agent 对话历史存储在 Session 作用域 Bean 中,不同用户的对话完全隔离,Session 过期后自动清理上下文,避免内存泄漏。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"用 Session 作用域" | 完整 Session 上下文管理,包含代理和清理 |
| 技术深度 | 不知道 ScopedProxyMode 的作用 | 解释代理模式和 Session 绑定的原理 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 会话管理的完整方案 |
| 面试官印象 | 知道作用域但不会设计 | 能设计会话级别的上下文管理方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| Singleton Bean 注入 Request 作用域 Bean 如何解决? | 类似作用域陷阱,解决方案相同 |
| @RefreshScope 的实现原理? | 基于 Scope 代理的动态刷新机制 |
| Spring 的 ThreadLocal 和 Session 作用域的区别? | 线程隔离 vs 会话隔离 |
Spring 配置属性绑定¶
Spring 配置属性绑定¶
1、基础题:@Value、@ConfigurationProperties、Environment 三种配置读取方式的区别?¶
难度级别:⭐⭐(配置读取、类型安全、松散绑定)
Answer
- @Value:
- 读取单个配置值,支持 SpEL 表达式。
-
示例:
@Value("${agent.api-key}")。 -
@ConfigurationProperties:
- 批量绑定配置到 Bean,支持类型安全、松散绑定、JSR-303 校验。
-
示例:
@ConfigurationProperties(prefix = "agent")。 -
Environment:
- 编程式读取配置,可以访问所有属性源。
-
示例:
environment.getProperty("agent.api-key")。 -
选择建议:
- 少量配置用
@Value。 - 批量配置用
@ConfigurationProperties(推荐)。 - 动态读取用
Environment。
2、进阶题:PropertySource 的加载优先级?如何实现配置动态刷新(@RefreshScope)?¶
难度级别:⭐⭐⭐⭐(配置优先级、动态刷新、Scope 代理)
1️⃣ Common Answer
PropertySource 的优先级是命令行参数 > 环境变量 > 配置文件。@RefreshScope 可以让配置动态刷新,不用重启应用。
2️⃣ Impressive Answer
- PropertySource 加载优先级(从高到低):
- 命令行参数(
--agent.api-key=xxx) - ServletConfig 初始化参数
- ServletContext 初始化参数
- JNDI
java:comp/env - JVM 系统属性(
System.getProperties()) - 操作系统环境变量
- JAR 包外的
application-{profile}.properties - JAR 包内的
application-{profile}.properties - JAR 包外的
application.properties - JAR 包内的
application.properties @PropertySource注解-
默认属性
-
@RefreshScope 动态刷新:
@RefreshScope // 配置变更时刷新 Bean
@ConfigurationProperties(prefix = "agent")
@Component
public class AgentProperties {
private String apiKey;
private double temperature;
// getters and setters
}
- 原理:`@RefreshScope` 基于 `GenericScope`,Bean 是代理对象,配置变更时销毁旧 Bean,创建新 Bean。
- 触发:通过 `/actuator/refresh` 端点或配置中心(如 Nacos、Apollo)推送变更。
- 配置中心集成:
# bootstrap.yml
spring:
cloud:
nacos:
config:
server-addr: ${NACOS_SERVER}
namespace: ${NAMESPACE}
group: ${GROUP}
data-id: agent-config.yaml
auto-refresh: true # 自动刷新
- 动态刷新限制:
@Value注入的值不会自动刷新(需要重新绑定)。@ConfigurationProperties绑定的 Bean 会自动刷新。-
静态变量、构造器注入的值不会刷新。
-
Agent 场景实战:Agent 的 LLM 配置(模型名、temperature、API Key)通过 Nacos 配置中心管理,使用
@RefreshScope实现动态热更新,无需重启服务。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"命令行 > 环境变量 > 配置文件" | 完整 12 级优先级,每级都有明确来源 |
| 技术深度 | 不知道 @RefreshScope 的代理原理 | 解释 GenericScope 的代理机制和刷新流程 |
| 实践经验 | 没有结合配置中心 | 给出 Nacos 配置中心的集成方案 |
| 面试官印象 | 知道优先级但不深入 | 理解配置加载的设计意图和动态刷新机制 |
3、场景题:Agent 的 LLM 配置(模型名、temperature、API Key)如何用配置中心动态热更新?¶
难度级别:⭐⭐⭐(配置中心、动态刷新、@RefreshScope)
1️⃣ Common Answer
把 LLM 配配置放到配置中心,用 @RefreshScope 注解配置类,配置变更后调用 /actuator/refresh 端点就能热更新。
2️⃣ Impressive Answer
- 配置类定义:
@RefreshScope // 动态刷新
@ConfigurationProperties(prefix = "agent.llm")
@Component
@Validated
public class LLmProperties {
@NotBlank
private String provider = "openai";
@NotBlank
private String apiKey;
@Min(0) @Max(2)
private double temperature = 0.7;
private int maxTokens = 2048;
private Map<String, String> customParams = new HashMap<>();
// getters and setters
}
- 使用配置:
@Service
public class LLMService {
@Autowired
private LLMProperties llmProperties; // 代理对象
public String chat(String prompt) {
// 每次调用都会从代理获取最新配置
return llmProperties.getProvider().equals("openai")
? callOpenAI(prompt)
: callClaude(prompt);
}
}
- Nacos 配置中心集成:
# bootstrap.yml
spring:
application:
name: agent-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: dev
group: AGENT_GROUP
data-id: agent-service.yaml
auto-refresh: true # 自动刷新
file-extension: yaml
# Nacos 配置:agent-service.yaml
agent:
llm:
provider: openai
api-key: sk-xxx
temperature: 0.7
max-tokens: 2048
- 手动刷新:
- 监听配置变更:
@Component
public class ConfigChangeListener {
@EventListener
public void onRefreshScopeRefreshed(RefreshScopeRefreshedEvent event) {
log.info("Configuration refreshed: {}", event.getName());
// 可以在这里做额外的处理,比如重新初始化连接池
}
}
- Agent 场景实战:Agent 服务部署到生产环境后,通过 Nacos 配置中心动态调整 LLM 的 temperature 和 maxTokens,优化回答质量,无需重启服务,实现零停机配置更新。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"用 @RefreshScope" | 完整配置中心集成方案,包含 Nacos 和手动刷新 |
| 技术深度 | 不知道 auto-refresh 的机制 | 解释 Nacos 的长轮询监听机制 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent LLM 配置热更新的完整方案 |
| 面试官印象 | 知道注解但不会集成 | 能设计生产级的配置管理方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @ConfigurationProperties 的松散绑定规则? | 支持驼峰、下划线、连字符等多种格式 |
| @Value 支持的 SpEL 表达式有哪些? | 动态计算配置值 |
| Spring Cloud Config 和 Nacos 的区别? | 配置中心选型 |
Spring 异步与调度¶
Spring 异步与调度¶
1、基础题:@Scheduled 注解如何使用?cron 表达式如何编写?¶
难度级别:⭐⭐(@EnableScheduling、fixedRate、fixedDelay、cron)
1️⃣ Common Answer
在方法上加 @Scheduled 注解就可以定时执行。需要在启动类上加 @EnableScheduling。可以用 fixedRate 指定间隔,或者用 cron 表达式指定执行时间。cron 表达式是秒分时日月周。
2️⃣ Impressive Answer
-
前置配置:在启动类或配置类上加
@EnableScheduling注解。 -
三种调度方式:
@Scheduled(fixedRate = 5000) // 固定频率(上次开始后 5 秒执行,不考虑上次的执行时间)
public void fixedRateTask() { ... }
@Scheduled(fixedDelay = 5000) // 固定延迟(上次结束后 5 秒执行)
public void fixedDelayTask() { ... }
@Scheduled(initialDelay = 1000, fixedRate = 5000) // 初始延迟 1 秒,然后每 5 秒执行
public void initialDelayTask() { ... }
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点执行
public void cronTask() { ... }
- Cron 表达式格式:
秒 分 时 日 月 周 [年]
| 字段 | 允许值 | 特殊字符 |
|---|---|---|
| 秒 | 0-59 | , - * / |
| 分 | 0-59 | , - * / |
| 时 | 0-23 | , - * / |
| 日 | 1月31日 | , - * / ? L W C |
| 月 | 1月12日 | , - * / |
| 周 | 1月7日 | , - * / ? L C # |
- 常用 Cron 示例:
0 0 2 * * ?:每天凌晨 2 点0 0/5 * * * ?:每 5 分钟0 0 12 ? * MON-FRI:周一到周五中午 12 点0 0 12 L * ?:每月最后一天中午 12 点-
0 15 10 ? * 6L:每月最后一个周五上午 10:15 -
注意事项:默认单线程执行,上一个任务未完成时下一个任务会阻塞。需要并行执行时配置线程池。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 简单描述三种方式 | 包含参数含义、Cron 格式、常用示例 |
| 技术深度 | 不知道单线程阻塞问题 | 清楚默认单线程和线程池配置 |
| 实践经验 | 没有 Cron 示例 | 有生产环境 Cron 表达式经验 |
| 面试官印象 | 会用 @Scheduled | 理解定时任务的配置和限制 |
2、进阶题:Spring 定时任务的底层实现是什么?TaskScheduler 和 ThreadPoolTaskScheduler 的关系?分布式环境下如何避免重复执行?¶
难度级别:⭐⭐⭐(TaskScheduler、线程池配置、分布式锁、XXL-Job)
1️⃣ Common Answer
Spring 定时任务底层是用 TaskScheduler 实现的。ThreadPoolTaskScheduler 是 TaskScheduler 的实现,用线程池执行任务。分布式环境可以用分布式锁,或者用 XXL-Job 这种调度框架。
2️⃣ Impressive Answer
- 底层实现原理:
@EnableScheduling导入SchedulingConfiguration,注册ScheduledAnnotationBeanPostProcessor。ScheduledAnnotationBeanPostProcessor扫描 @Scheduled 注解,注册定时任务到TaskScheduler。-
TaskScheduler根据调度策略(fixedRate、fixedDelay、cron)提交任务到线程池执行。 -
TaskScheduler 层次结构:
TaskScheduler:定时任务调度接口,定义了schedule()、scheduleAtFixedRate()等方法。ThreadPoolTaskScheduler:基于ThreadPoolTaskExecutor的实现,支持并行执行。-
ConcurrentTaskScheduler:基于ScheduledExecutorService的实现。 -
线程池配置:
@Configuration
@EnableScheduling
public class ScheduleConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 线程池大小
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy());
scheduler.setWaitForTasksToCompleteOnShutdown(true); // 优雅停机
scheduler.setAwaitTerminationSeconds(60);
scheduler.initialize();
return scheduler;
}
}
- 分布式环境避免重复执行: 方案一:分布式锁(Redis)
@Scheduled(cron = "0 0 2 * * ?")
public void dailyCleanup() {
String lockKey = "lock:daily:cleanup";
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofHours(1));
if (!locked) {
log.info("Task already running on another instance");
return;
}
// 执行任务
cleanupExpiredData();
} finally {
redisTemplate.delete(lockKey);
}
}
@Scheduled(cron = "0 0 2 * * ?")
public void dailyCleanup() {
try {
taskExecutionRepository.insert("daily:cleanup", LocalDateTime.now());
} catch (DuplicateKeyException e) {
log.info("Task already running on another instance");
return;
}
// 执行任务
cleanupExpiredData();
}
- Agent 场景:定期清理过期对话记录、刷新工具列表、统计 Token 消耗。使用 XXL-Job 的分片广播功能,每个实例处理部分数据,提升效率。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了底层和分布式锁 | 完整方案:原理→线程池→分布式锁→XXL-Job |
| 技术深度 | 不知道 TaskScheduler 层次 | 清楚接口和实现的关系 |
| 实践经验 | 没有提到 XXL-Job | 有生产环境分布式调度框架经验 |
| 面试官印象 | 知道定时任务 | 有分布式定时任务的完整方案 |
2、进阶题:@Async 的线程池隔离如何配置?@Async 和事务的关系?¶
难度级别:⭐⭐⭐(线程池配置、事务传播、异步事务)
1️⃣ Common Answer
@Async 可以指定线程池,用 @Async("executor") 就行。@Async 和事务的关系是异步方法不会继承主方法的事务,因为不在同一个线程。
2️⃣ Impressive Answer
- 线程池隔离配置:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("agentExecutor")
public Executor agentExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("agent-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean("toolExecutor")
public Executor toolExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("tool-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
- 指定线程池:
@Service
public class AgentService {
@Async("agentExecutor") // 使用 agentExecutor 线程池
public CompletableFuture<AgentResult> executeAsync(AgentRequest request) {
return CompletableFuture.completedFuture(doExecute(request));
}
}
@Service
public class ToolService {
@Async("toolExecutor") // 使用 toolExecutor 线程池
public CompletableFuture<ToolResponse> callAsync(ToolRequest request) {
return CompletableFuture.completedFuture(doCall(request));
}
}
- @Async 和事务的关系:
- 异步方法不会继承调用方的事务,因为不在同一个线程。
- 异步方法需要自己开启事务(
@Transactional)。 - 示例:
@Service
public class AgentService {
@Transactional
public AgentResult execute(AgentRequest request) {
// 主事务
AgentResult result = doExecute(request);
agentRepository.save(result); // 在主事务中
asyncService.saveAsync(result); // 异步方法,不在主事务中
return result;
}
}
@Service
public class AsyncService {
@Async
@Transactional // 需要自己开启事务
public void saveAsync(AgentResult result) {
asyncRepository.save(result); // 在独立事务中
}
}
- 异步事务的注意事项:
- 异步方法无法回滚主事务。
- 主事务无法感知异步方法的异常。
-
如果需要事务一致性,应该使用同步方法或分布式事务。
-
Agent 场景实战:Agent 执行完成后,异步保存日志到数据库(
@Async("agentExecutor")),异步发送通知(@Async("notificationExecutor")),两者使用不同的线程池隔离,避免互相影响。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"用 @Async 指定线程池" | 完整线程池隔离配置,包含拒绝策略 |
| 技术深度 | 不知道异步事务的传播机制 | 解释异步方法不会继承主事务的原因 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 异步任务的线程池隔离方案 |
| 面试官印象 | 知道注解但不会配置 | 能设计生产级的异步任务方案 |
3、场景题:Agent 需要定期清理过期的对话记录、定期刷新工具列表、定期统计 Token 消耗,如何用 Spring 定时任务实现并保证分布式环境下不重复执行?¶
难度级别:⭐⭐⭐(多任务调度、分布式锁、故障转移、监控)
1️⃣ Common Answer
可以写三个 @Scheduled 方法,分别执行清理、刷新、统计。用 Redis 分布式锁保证不重复执行。如果任务失败可以记录日志。
2️⃣ Impressive Answer
- 定时任务设计:
@Component
public class AgentScheduledTasks {
// 每天凌晨 3 点清理过期对话(保留 30 天)
@Scheduled(cron = "0 0 3 * * ?")
@SchedulerLock(name = "cleanupExpiredConversations",
lockAtMostFor = "2h", lockAtLeastFor = "1h")
public void cleanupExpiredConversations() {
LocalDateTime cutoff = LocalDateTime.now().minusDays(30);
int deleted = conversationRepository.deleteByCreatedAtBefore(cutoff);
log.info("Deleted {} expired conversations", deleted);
}
// 每小时刷新工具列表
@Scheduled(cron = "0 0 * * * ?")
@SchedulerLock(name = "refreshToolList",
lockAtMostFor = "55m", lockAtLeastFor = "50m")
public void refreshToolList() {
List<ToolDefinition> tools = toolRegistry.fetchLatestTools();
toolRegistry.updateTools(tools);
log.info("Refreshed {} tools", tools.size());
}
// 每天凌晨 4 点统计 Token 消耗
@Scheduled(cron = "0 0 4 * * ?")
@SchedulerLock(name = "aggregateTokenUsage",
lockAtMostFor = "1h", lockAtLeastFor = "50m")
public void aggregateTokenUsage() {
LocalDate yesterday = LocalDate.now().minusDays(1);
TokenUsageReport report = tokenUsageService.aggregate(yesterday);
reportService.save(report);
log.info("Token usage report: {}", report);
}
}
- 分布式锁实现(使用 Shedlock):
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "1h", defaultLockAtLeastFor = "50m")
public class ShedlockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime()
.build()
);
}
}
- 故障转移与重试:
@Scheduled(cron = "0 0 3 * * ?")
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void cleanupExpiredConversations() {
try {
// 执行任务
} catch (Exception e) {
log.error("Task failed, will retry", e);
throw e; // 触发重试
}
}
@Recover
public void recover(Exception e) {
log.error("Task failed after 3 retries, sending alert", e);
alertService.send("Scheduled task failed: " + e.getMessage());
}
- 监控与告警:
@Scheduled(cron = "0 0 3 * * ?")
public void cleanupExpiredConversations() {
Timer.Sample sample = Timer.start(meterRegistry);
try {
// 执行任务
sample.stop(Timer.builder("scheduled.task.duration")
.tag("task", "cleanupConversations")
.register(meterRegistry));
meterRegistry.counter("scheduled.task.success",
"task", "cleanupConversations").increment();
} catch (Exception e) {
meterRegistry.counter("scheduled.task.failure",
"task", "cleanupConversations").increment();
throw e;
}
}
- 优雅停机:配置
setWaitForTasksToCompleteOnShutdown(true),停机时等待正在执行的任务完成。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了三个定时任务 | 完整方案:任务设计→分布式锁→重试→监控→停机 |
| 技术深度 | 不知道 Shedlock 和重试 | 清楚分布式锁和故障转移机制 |
| 实践经验 | 没有监控和告警 | 有生产环境定时任务的完整运维经验 |
| 面试官印象 | 会写定时任务 | 有分布式定时任务的工程化能力 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| XXL-Job 的核心原理是什么? | 分布式调度框架的对比 |
| 如何实现定时任务的动态配置? | 生产环境的灵活调度需求 |
| Spring Boot 的优雅停机如何实现? | 定时任务的停机策略 |
3、场景题:Agent 批量工具调用如何用 @Async + CompletableFuture 实现并发执行,控制超时和异常?¶
难度级别:⭐⭐⭐⭐(并发控制、超时处理、异常处理、CompletableFuture)
1️⃣ Common Answer
用 @Async 异步调用每个工具,用 CompletableFuture 收集结果,用 allOf 等待所有完成,用 orTimeout 设置超时。
2️⃣ Impressive Answer
- 异步工具调用:
@Service
public class ToolService {
@Async("toolExecutor")
public CompletableFuture<ToolResponse> callAsync(ToolRequest request) {
try {
ToolResponse response = doCall(request);
return CompletableFuture.completedFuture(response);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
- 批量并发调用:
@Service
public class AgentService {
@Autowired
private ToolService toolService;
public Map<String, ToolResponse> batchCall(List<ToolRequest> requests, long timeout, TimeUnit unit) {
// 1. 提交所有异步任务
List<CompletableFuture<ToolResponse>> futures = requests.stream()
.map(toolService::callAsync)
.toList();
// 2. 等待所有完成,设置超时
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
try {
allFutures.get(timeout, unit);
} catch (TimeoutException e) {
// 超时处理
futures.forEach(f -> f.cancel(true));
throw new AgentException("Tool call timeout", e);
} catch (Exception e) {
throw new AgentException("Tool call failed", e);
}
// 3. 收集结果
Map<String, ToolResponse> results = new HashMap<>();
for (int i = 0; i < requests.size(); i++) {
ToolRequest request = requests.get(i);
CompletableFuture<ToolResponse> future = futures.get(i);
try {
ToolResponse response = future.get();
results.put(request.getToolId(), response);
} catch (Exception e) {
// 单个工具失败,记录但不影响其他工具
log.error("Tool {} failed: {}", request.getToolId(), e.getMessage());
results.put(request.getToolId(), ToolResponse.failed(e));
}
}
return results;
}
}
- 超时控制:
@Service
public class ToolService {
@Async("toolExecutor")
public CompletableFuture<ToolResponse> callAsyncWithTimeout(ToolRequest request, long timeout, TimeUnit unit) {
return CompletableFuture.supplyAsync(() -> doCall(request))
.orTimeout(timeout, unit) // 单个调用超时
.exceptionally(e -> {
if (e instanceof TimeoutException) {
return ToolResponse.timeout();
}
return ToolResponse.failed(e);
});
}
}
- 异常处理:
@Service
public class AgentService {
public Map<String, ToolResponse> batchCallWithErrorHandling(List<ToolRequest> requests) {
List<CompletableFuture<ToolResponse>> futures = requests.stream()
.map(toolService::callAsync)
.toList();
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
).exceptionally(e -> {
// 全局异常处理
log.error("Batch tool call failed: {}", e.getMessage());
return null;
});
allFutures.join(); // 等待所有完成
// 收集结果,处理异常
return futures.stream()
.collect(Collectors.toMap(
f -> f.join().getToolId(),
f -> {
try {
return f.join();
} catch (Exception e) {
return ToolResponse.failed(e);
}
}
));
}
}
- Agent 场景实战:Agent 需要同时调用搜索工具、计算工具、天气工具获取信息,使用
@Async并发执行,设置 5 秒超时,单个工具失败不影响其他工具,最后聚合所有结果返回给 LLM。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"用 CompletableFuture.allOf" | 完整批量并发方案,包含超时和异常处理 |
| 技术深度 | 不知道 orTimeout 的用法 | 解释单调用超时和整体超时的区别 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 批量工具调用的完整方案 |
| 面试官印象 | 知道 CompletableFuture 但不会设计 | 能设计生产级的并发控制方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @Scheduled 的 cron 表达式语法? | 定时任务的时间配置 |
| CompletableFuture 的常用方法(thenApply、thenCompose、exceptionally)? | 异步编程的组合式 API |
| Spring 的线程池监控和调优? | 生产环境线程池管理 |
Spring 测试体系¶
Spring 测试体系¶
1、基础题:@SpringBootTest vs @WebMvcTest vs @DataJpaTest 的区别?¶
难度级别:⭐⭐(测试切片、集成测试、单元测试)
Answer
- @SpringBootTest:
- 完整集成测试,启动整个 Spring 上下文。
-
适合测试完整流程,但启动慢。
-
@WebMvcTest:
- Web 层切片测试,只加载 Controller 相关的 Bean。
-
适合测试 Controller,启动快。
-
@DataJpaTest:
- 数据访问层切片测试,只加载 Repository 相关的 Bean。
-
适合测试 Repository,自动配置内存数据库。
-
选择建议:
- 测试 Controller 用
@WebMvcTest。 - 测试 Repository 用
@DataJpaTest。 - 测试完整流程用
@SpringBootTest。
2、进阶题:如何用 @MockBean 和 @SpyBean 隔离外部依赖?两者的区别?¶
难度级别:⭐⭐⭐(Mock 隔离、Spy 部分模拟、测试隔离)
1️⃣ Common Answer
@MockBean 可以 Mock 掉 Spring 容器中的 Bean,@SpyBean 可以部分 Mock。测试时用它们隔离外部依赖,比如 Mock 掉 LLM 调用。
2️⃣ Impressive Answer
- @MockBean 完全模拟:
@WebMvcTest(AgentController.class)
public class AgentControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // 完全 Mock,所有方法都是模拟的
private AgentService agentService;
@Test
public void testChat() throws Exception {
// Mock 返回值
when(agentService.chat(any()))
.thenReturn(AgentResponse.success("Hello"));
mockMvc.perform(post("/api/agent/chat")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"prompt\":\"Hi\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value("Hello"));
}
}
- @SpyBean 部分模拟:
@SpringBootTest
public class AgentServiceTest {
@Autowired
private AgentService agentService;
@SpyBean // 部分 Mock,真实方法可以调用
private LLMService llmService;
@Test
public void testChat() {
// Mock LLM 调用,但其他方法真实执行
when(llmService.chat(any()))
.thenReturn(LLMResponse.success("Hello"));
AgentResponse response = agentService.chat(new AgentRequest("Hi"));
assertEquals("Hello", response.getContent());
}
}
- 区别对比:
@MockBean:完全模拟,所有方法都需要 Mock,适合隔离外部依赖。-
@SpyBean:部分模拟,可以调用真实方法,适合需要部分真实行为的场景。 -
Mock LLM 响应:
@WebMvcTest(AgentController.class)
public class AgentControllerTest {
@MockBean
private AgentService agentService;
@Test
public void testChatWithToolCall() {
// Mock Agent 调用工具
ToolResponse toolResponse = ToolResponse.success("Weather: Sunny");
when(agentService.callTool("weather-tool", any()))
.thenReturn(CompletableFuture.completedFuture(toolResponse));
// Mock LLM 响应
when(agentService.chat(any()))
.thenReturn(AgentResponse.success("Today is sunny"));
// 测试完整流程
}
}
- Agent 场景实战:测试 Agent 工具调用链路时,用
@MockBeanMock 掉 LLMService 和 ToolService,模拟工具调用和 LLM 响应,验证 Agent 的决策逻辑,无需真实调用外部服务。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"@MockBean 完全 Mock,@SpyBean 部分 Mock" | 完整测试示例,包含 Mock 和 Spy 的使用场景 |
| 技术深度 | 不知道测试切片的隔离机制 | 解释 @WebMvcTest 只加载 Controller 的原理 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 工具调用链路的测试方案 |
| 面试官印象 | 知道注解但不会设计测试 | 能设计完整的测试隔离方案 |
3、场景题:如何对 Agent 工具调用链路做集成测试,Mock LLM 响应并验证工具调用顺序?¶
难度级别:⭐⭐⭐⭐(集成测试、Mock 链路、验证顺序、InOrder)
1️⃣ Common Answer
用 @SpringBootTest 启动整个应用,用 @MockBean Mock 掉 LLMService,然后测试 Agent 的工具调用,用 InOrder 验证调用顺序。
2️⃣ Impressive Answer
- 集成测试类:
@SpringBootTest
@AutoConfigureMockMvc
public class AgentIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private LLMService llmService;
@MockBean
private ToolService toolService;
@Captor
private ArgumentCaptor<ToolRequest> toolRequestCaptor;
@Test
public void testAgentToolCallSequence() throws Exception {
// 1. Mock LLM 响应:第一次请求要求调用工具
LLMResponse llmResponse1 = LLMResponse.builder()
.content("I need to search for information")
.toolCalls(List.of(
ToolCall.builder().toolId("search-tool").parameters(Map.of("query", "Java")).build()
))
.build();
when(llmService.chat(any())).thenReturn(llmResponse1);
// 2. Mock 工具响应
ToolResponse toolResponse = ToolResponse.success("Java is a programming language");
when(toolService.call(any())).thenReturn(CompletableFuture.completedFuture(toolResponse));
// 3. Mock LLM 响应:第二次请求基于工具结果回答
LLMResponse llmResponse2 = LLMResponse.builder()
.content("Java is a programming language created by Sun Microsystems")
.build();
when(llmService.chat(any())).thenReturn(llmResponse2);
// 4. 执行 Agent 调用
mockMvc.perform(post("/api/agent/chat")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"prompt\":\"What is Java?\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value("Java is a programming language created by Sun Microsystems"));
// 5. 验证工具调用顺序
InOrder inOrder = inOrder(llmService, toolService);
// 验证:第一次调用 LLM
inOrder.verify(llmService).chat(argThat(req ->
req.getPrompt().equals("What is Java?")
));
// 验证:调用工具
inOrder.verify(toolService).call(argThat(req ->
req.getToolId().equals("search-tool") &&
req.getParameters().get("query").equals("Java")
));
// 验证:第二次调用 LLM(包含工具结果)
inOrder.verify(llmService).chat(argThat(req ->
req.getMessages().stream().anyMatch(msg ->
msg.getContent().contains("Java is a programming language")
)
));
}
}
- 验证工具调用参数:
@Test
public void testToolCallParameters() {
// ... Mock 设置
mockMvc.perform(post("/api/agent/chat")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"prompt\":\"Search for Java\"}"))
.andExpect(status().isOk());
// 验证工具调用参数
verify(toolService).call(toolRequestCaptor.capture());
ToolRequest capturedRequest = toolRequestCaptor.getValue();
assertEquals("search-tool", capturedRequest.getToolId());
assertEquals("Java", capturedRequest.getParameters().get("query"));
}
- 验证调用次数:
@Test
public void testToolCallCount() {
// ... Mock 设置
mockMvc.perform(post("/api/agent/chat")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"prompt\":\"Search for Java\"}"))
.andExpect(status().isOk());
// 验证工具只调用了一次
verify(toolService, times(1)).call(any());
// 验证 LLM 调用了两次(初始请求 + 工具结果后的请求)
verify(llmService, times(2)).chat(any());
}
- 异常场景测试:
@Test
public void testToolCallFailure() throws Exception {
// Mock 工具调用失败
when(toolService.call(any()))
.thenReturn(CompletableFuture.failedFuture(new RuntimeException("Tool failed")));
// 验证 Agent 能够处理工具失败
mockMvc.perform(post("/api/agent/chat")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"prompt\":\"Search for Java\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value("Sorry, I encountered an error"));
}
- Agent 场景实战:测试 Agent 的工具调用决策逻辑,Mock LLM 响应要求调用工具,Mock 工具返回结果,验证 Agent 是否正确调用工具、是否将工具结果传递给 LLM、最终回答是否符合预期。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"用 InOrder 验证顺序" | 完整集成测试,包含 Mock、验证、异常场景 |
| 技术深度 | 不知道 ArgumentCaptor 的用法 | 解释参数捕获和验证的机制 |
| 实践经验 | 没有结合 Agent 场景 | 给出 Agent 工具调用链路的完整测试方案 |
| 面试官印象 | 知道测试注解但不会设计 | 能设计复杂的集成测试场景 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @TestConfiguration 和 @TestPropertySource 的作用? | 测试专用配置和属性覆盖 |
| MockMvc 的使用方法和常用断言? | Web 层测试的核心工具 |
| Spring Test 的事务管理(@Transactional)? | 测试事务回滚,避免污染数据库 |
3.2 Spring MVC 全局异常处理¶
1、基础题:@ExceptionHandler 和 @ControllerAdvice 是什么?如何做全局异常处理?¶
难度级别:⭐⭐(异常处理、统一响应格式)
Answer
-
@ExceptionHandler:用于处理 Controller 方法抛出的特定异常,可以返回自定义的响应。
-
@ControllerAdvice:全局异常处理注解,标注在类上,该类中的
@ExceptionHandler方法会应用到所有 Controller。 -
全局异常处理示例:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<ErrorResponse> handleException(Exception e) {
ErrorResponse error = ErrorResponse.builder()
.code(500)
.message("Internal Server Error")
.build();
return ResponseEntity.status(500).body(error);
}
}
2、进阶题:Spring MVC 异常处理的优先级链是什么?多个 @ControllerAdvice 的执行顺序如何控制?¶
难度级别:⭐⭐⭐(异常处理优先级、@Order、HandlerExceptionResolver)
1️⃣ Common Answer
异常处理会先找最近的 @ExceptionHandler,找不到再用 @ControllerAdvice。多个 @ControllerAdvice 可以用 @Order 控制顺序。
2️⃣ Impressive Answer
- 异常处理优先级链:
Controller 方法内的 @ExceptionHandler
→ Controller 类内的 @ExceptionHandler
→ @ControllerAdvice 中的 @ExceptionHandler(按 @Order 排序)
→ HandlerExceptionResolver(自定义异常解析器)
→ 默认异常处理(返回 500 错误)
- @ControllerAdvice 的执行顺序:
- 默认顺序:Spring 按照类的名称字母顺序排序。
- @Order 控制:使用
@Order注解指定优先级,值越小优先级越高。 -
@Priority:JSR-250 的
@Priority注解也可以控制顺序。 -
精确匹配优先:
@ExceptionHandler会优先匹配最具体的异常类型。例如:
@ExceptionHandler({BusinessException.class, RuntimeException.class})
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
// 如果抛出 BusinessException,会进入这里
return ResponseEntity.badRequest().body(ErrorResponse.from(e));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
// 其他异常进入这里
return ResponseEntity.internalServerError().body(ErrorResponse.from(e));
}
- 自定义 HandlerExceptionResolver:
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
// 自定义异常处理逻辑
if (ex instanceof BusinessException) {
response.setStatus(400);
return new ModelAndView(new MappingJackson2JsonView(ErrorResponse.from(ex)));
}
return null; // 返回 null 表示不处理,交给下一个 Resolver
}
}
- 实践建议:
- 分层处理:业务异常用
@ControllerAdvice统一处理,系统异常用HandlerExceptionResolver处理。 - 精确匹配:
@ExceptionHandler尽量精确匹配异常类型,避免用Exception.class兜底。 - 响应格式:统一错误响应格式,便于前端处理。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"就近原则" | 完整优先级链:Controller → @ControllerAdvice → Resolver |
| 技术深度 | 不知道 @Order 的作用 | 解释 @Order 和精确匹配的优先级机制 |
| 实践经验 | 没有提到自定义 Resolver | 知道如何扩展异常处理机制 |
| 面试官印象 | 知道基本用法 | 理解异常处理的设计和扩展机制 |
3、场景题:Agent 调用 LLM 超时、工具执行失败、参数校验失败,如何统一封装错误响应格式?¶
难度级别:⭐⭐⭐(异常分类、统一响应、错误码设计)
1️⃣ Common Answer
创建一个全局异常处理器,用 @ExceptionHandler 捕获不同异常,返回统一的错误响应格式。可以用枚举定义错误码。
2️⃣ Impressive Answer
- 定义错误码枚举:
public enum ErrorCode {
SUCCESS(0, "Success"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
LLM_TIMEOUT(1001, "LLM call timeout"),
TOOL_EXECUTION_FAILED(1002, "Tool execution failed"),
VALIDATION_FAILED(1003, "Parameter validation failed"),
INTERNAL_ERROR(500, "Internal server error");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
}
- 自定义异常类:
public class AgentException extends RuntimeException {
private final ErrorCode errorCode;
public AgentException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() { return errorCode; }
}
public class LlmTimeoutException extends AgentException {
public LlmTimeoutException(String message) {
super(ErrorCode.LLM_TIMEOUT, message);
}
}
public class ToolExecutionException extends AgentException {
public ToolExecutionException(String message) {
super(ErrorCode.TOOL_EXECUTION_FAILED, message);
}
}
- 统一响应格式:
@Data
@Builder
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.code(ErrorCode.SUCCESS.getCode())
.message(ErrorCode.SUCCESS.getMessage())
.data(data)
.build();
}
public static <T> ApiResponse<T> error(ErrorCode errorCode, String message) {
return ApiResponse.<T>builder()
.code(errorCode.getCode())
.message(message)
.build();
}
}
- 全局异常处理器:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(LlmTimeoutException.class)
public ApiResponse<Void> handleLlmTimeout(LlmTimeoutException e) {
log.error("LLM timeout: {}", e.getMessage());
return ApiResponse.error(e.getErrorCode(), e.getMessage());
}
@ExceptionHandler(ToolExecutionException.class)
public ApiResponse<Void> handleToolExecution(ToolExecutionException e) {
log.error("Tool execution failed: {}", e.getMessage());
return ApiResponse.error(e.getErrorCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.error("Validation failed: {}", message);
return ApiResponse.error(ErrorCode.VALIDATION_FAILED, message);
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
log.error("Unexpected error", e);
return ApiResponse.error(ErrorCode.INTERNAL_ERROR, "Internal server error");
}
}
- Controller 使用示例:
@RestController
@RequestMapping("/api/agent")
public class AgentController {
@Autowired
private LlmService llmService;
@Autowired
private ToolService toolService;
@PostMapping("/chat")
public ApiResponse<ChatResponse> chat(@Valid @RequestBody ChatRequest request) {
try {
// 调用 LLM
ChatResponse response = llmService.chat(request);
return ApiResponse.success(response);
} catch (TimeoutException e) {
throw new LlmTimeoutException("LLM call timeout: " + e.getMessage());
}
}
@PostMapping("/tool")
public ApiResponse<ToolResponse> executeTool(@Valid @RequestBody ToolRequest request) {
try {
ToolResponse response = toolService.execute(request);
return ApiResponse.success(response);
} catch (Exception e) {
throw new ToolExecutionException("Tool execution failed: " + e.getMessage());
}
}
}
- 实践优化:
- 错误码国际化:错误消息支持多语言,根据请求头
Accept-Language返回对应语言。 - 错误详情:开发环境返回详细错误栈,生产环境返回通用错误消息。
- 监控告警:对 LLM 超时、工具失败等异常进行监控和告警。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"用 @ExceptionHandler 捕获" | 完整方案:错误码枚举、自定义异常、统一响应 |
| 技术深度 | 不知道异常分类和错误码设计 | 精细异常分类,支持国际化 |
| 实践经验 | 没有考虑监控和告警 | 提到开发/生产环境差异化、监控告警 |
| 面试官印象 | 能写出基础异常处理 | 能设计生产级的异常处理体系 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| 如何实现参数校验(@Valid)? | @ExceptionHandler 可以处理校验异常 |
| 如何实现自定义错误码? | 错误码设计是异常处理的一部分 |
| 如何实现异常监控和告警? | 异常处理需要配合监控系统 |
3.3 Spring 配置管理与 Environment¶
1、基础题:@Value、@ConfigurationProperties、Environment 三种读取配置的方式有什么区别?¶
难度级别:⭐⭐(配置注入、类型安全、松耦合)
Answer
-
@Value:直接注入单个配置值,支持 SpEL 表达式,适合简单配置。
-
@ConfigurationProperties:批量注入配置到 Bean,类型安全,支持嵌套对象,适合复杂配置。
-
Environment:编程式读取配置,灵活但繁琐,适合动态读取。
-
区别:
@Value适合单个值,@ConfigurationProperties适合批量配置。@ConfigurationProperties支持类型转换和校验,@Value不支持。Environment可以在运行时动态读取,@Value和@ConfigurationProperties是启动时注入。
2、进阶题:Spring 的 PropertySource 优先级链是什么?如何实现配置的动态刷新(@RefreshScope)?¶
难度级别:⭐⭐⭐(配置优先级、动态刷新、Spring Cloud Config)
1️⃣ Common Answer
配置的优先级是命令行参数 > 环境变量 > 配置文件。@RefreshScope 可以实现配置的动态刷新,需要配合 Spring Cloud Config 使用。
2️⃣ Impressive Answer
- PropertySource 优先级链(从高到低):
命令行参数(--server.port=8081)
→ ServletConfig 初始化参数
→ ServletContext 初始化参数
→ JNDI(java:comp/env)
→ JVM 系统属性(System.getProperties())
→ 操作系统环境变量
→ RandomValuePropertySource(random.*)
→ JAR 包外的 application-{profile}.properties
→ JAR 包内的 application-{profile}.properties
→ JAR 包外的 application.properties
→ JAR 包内的 application.properties
→ @PropertySource 注解指定的配置文件
→ 默认属性(SpringApplication.setDefaultProperties)
- 配置覆盖规则:
- 后加载的优先级高:后面的 PropertySource 会覆盖前面的同名属性。
- Profile 优先:激活的 Profile 配置会覆盖默认配置。
-
命令行参数最高:方便运维临时调整配置。
-
@RefreshScope 动态刷新:
- 原理:
@RefreshScope标注的 Bean 会被代理,配置刷新时,代理会重新创建 Bean 实例,注入新的配置值。 - 使用:
@RefreshScope
@Component
@ConfigurationProperties(prefix = "agent.llm")
public class LlmConfig {
private String apiKey;
private String model;
// getters and setters
}
-
刷新触发:调用
/actuator/refresh端点,Spring Cloud Config 会重新加载配置并刷新@RefreshScopeBean。 -
动态刷新实现细节:
- RefreshScope:是一个自定义 Scope,维护了一个
Bean的缓存,刷新时清除缓存。 - ContextRefresher:刷新
Environment,重新加载 PropertySource。 -
Lifecycle:销毁旧 Bean,创建新 Bean,重新注入依赖。
-
实践注意:
- 限制:
@RefreshScope只能刷新@Value和@ConfigurationProperties注入的配置,不能刷新@Bean方法的参数。 - 性能:频繁刷新会影响性能,建议配合消息队列实现配置变更通知。
- 兼容性:
@RefreshScope是 Spring Cloud 的功能,需要引入spring-cloud-context依赖。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"命令行参数 > 环境变量" | 完整优先级链,12 个层级 |
| 技术深度 | 不知道 @RefreshScope 的原理 | 解释代理机制和缓存清除 |
| 实践经验 | 没有提到刷新的限制和性能 | 知道刷新的限制和性能影响 |
| 面试官印象 | 知道基本优先级 | 理解配置加载和刷新的底层机制 |
3、场景题:Agent 的 LLM API Key、模型参数需要多环境隔离 + 运行时热更新,如何设计配置方案?¶
难度级别:⭐⭐⭐(多环境配置、动态刷新、安全隔离)
1️⃣ Common Answer
用不同的配置文件区分环境,比如 application-dev.yml、application-prod.yml。用 @RefreshScope 实现热更新,调用 /actuator/refresh 端点刷新配置。
2️⃣ Impressive Answer
- 多环境配置设计:
# application-common.yml(公共配置)
agent:
llm:
timeout: 30000
max-retries: 3
# application-dev.yml(开发环境)
spring:
profiles: dev
agent:
llm:
api-key: ${LLM_API_KEY:sk-dev-xxx}
model: gpt-4
endpoint: https://api.openai.com/v1
# application-prod.yml(生产环境)
spring:
profiles: prod
agent:
llm:
api-key: ${LLM_API_KEY} # 从环境变量读取,不在配置文件中硬编码
model: gpt-4-turbo
endpoint: https://api.openai.com/v1
- 配置类设计:
@RefreshScope
@Component
@ConfigurationProperties(prefix = "agent.llm")
@Validated
public class LlmConfig {
@NotBlank(message = "API Key cannot be blank")
private String apiKey;
@NotBlank(message = "Model cannot be blank")
private String model;
@NotBlank(message = "Endpoint cannot be blank")
private String endpoint;
@Min(value = 1000, message = "Timeout must be at least 1000ms")
private Integer timeout = 30000;
@Min(value = 0, message = "Max retries must be non-negative")
private Integer maxRetries = 3;
// getters and setters
}
- 动态刷新实现:
@RestController
@RequestMapping("/api/admin/config")
public class ConfigController {
@Autowired
private LlmConfig llmConfig;
@Autowired
private ContextRefresher contextRefresher;
@PostMapping("/refresh")
public ApiResponse<Void> refreshConfig() {
// 刷新配置
Set<String> refreshedKeys = contextRefresher.refresh();
log.info("Refreshed config keys: {}", refreshedKeys);
return ApiResponse.success(null);
}
@GetMapping("/llm")
public ApiResponse<LlmConfig> getLlmConfig() {
return ApiResponse.success(llmConfig);
}
}
- 配置热更新触发方式:
- 手动触发:调用
/api/admin/config/refresh端点。 - 自动触发:配置中心(如 Nacos、Apollo)推送配置变更,监听变更事件自动刷新。
-
定时刷新:定时任务检查配置变更,自动刷新。
-
安全隔离:
- 敏感配置加密:使用 Jasypt 加密 API Key,配置文件中存储加密值。
-
环境变量注入:生产环境的 API Key 通过环境变量注入,不在配置文件中暴露。
-
权限控制:配置刷新接口需要管理员权限,防止未授权访问。
-
配置验证:
@Component
public class ConfigValidator implements ApplicationListener<EnvironmentChangeEvent> {
@Autowired
private LlmConfig llmConfig;
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (event.getKeys().contains("agent.llm.api-key")) {
// 验证 API Key 是否有效
if (!llmConfig.getApiKey().startsWith("sk-")) {
log.error("Invalid API Key format");
throw new IllegalStateException("Invalid API Key");
}
}
}
}
- 实践优化:
- 配置中心:使用 Nacos、Apollo 等配置中心,支持配置版本管理、灰度发布。
- 配置审计:记录配置变更历史,支持回滚。
- 降级策略:配置刷新失败时,使用旧配置,避免服务不可用。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"多配置文件 + @RefreshScope" | 完整方案:多环境、动态刷新、安全隔离、验证 |
| 技术深度 | 不知道配置加密和验证 | 使用 Jasypt 加密,配置变更事件监听 |
| 实践经验 | 没有考虑配置中心和审计 | 提到 Nacos/Apollo、配置审计、降级策略 |
| 面试官印象 | 能实现基础多环境配置 | 能设计生产级的配置管理方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| 如何实现配置加密(Jasypt)? | 敏感配置需要加密存储 |
| 如何实现配置中心(Nacos/Apollo)? | 配置热更新的高级方案 |
| 如何实现配置的版本管理和回滚? | 配置审计和回滚是配置管理的重要功能 |
3.4 Spring 异步与线程池¶
1、基础题:@Async 注解的作用是什么?如何配置自定义线程池?¶
难度级别:⭐⭐(异步执行、线程池配置)
Answer
-
@Async 作用:将方法标记为异步执行,调用时会立即返回,方法在独立线程中执行。
-
配置自定义线程池:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
- 使用示例:
@Service
public class AsyncService {
@Async("taskExecutor")
public CompletableFuture<String> asyncMethod() {
// 异步执行
return CompletableFuture.completedFuture("done");
}
}
2、进阶题:@Async 的实现原理是什么(AOP + TaskExecutor)?为什么 @Async 在同类自调用时失效?¶
难度级别:⭐⭐⭐(AOP 代理、自调用失效、CGLIB)
1️⃣ Common Answer
@Async 是通过 AOP 实现的,Spring 会为标注了 @Async 的 Bean 创建代理,代理会在调用时将方法提交到线程池执行。同类自调用失效是因为没有经过代理。
2️⃣ Impressive Answer
- @Async 的实现原理:
- AOP 代理:
@Async是一个 AOP 注解,Spring 会为标注了@Async的 Bean 创建代理(JDK 动态代理或 CGLIB)。 - 拦截器:
AsyncAnnotationBeanPostProcessor会注册一个AnnotationAsyncExecutionInterceptor,拦截@Async方法调用。 - TaskExecutor:拦截器将方法调用包装成
Runnable,提交到TaskExecutor线程池执行。 -
返回值处理:如果方法返回
CompletableFuture,拦截器会等待异步执行完成并返回结果。 -
同类自调用失效原因:
- 代理机制:Spring AOP 是基于代理的,只有通过代理调用的方法才会被拦截。
- 自调用绕过代理:在同一个类中,
this.asyncMethod()是直接调用,绕过了代理,因此@Async不生效。 -
解决方案:
- 注入自己:通过
ApplicationContext获取代理 Bean,调用代理的方法。 - 拆分方法:将异步方法拆分到另一个类中。
- AopContext:使用
AopContext.currentProxy()获取当前代理。
- 注入自己:通过
-
AopContext 解决方案:
@Service
public class AsyncService {
@Async
public CompletableFuture<String> asyncMethod() {
return CompletableFuture.completedFuture("done");
}
public void callAsyncMethod() {
// 获取代理,调用异步方法
AsyncService proxy = (AsyncService) AopContext.currentProxy();
proxy.asyncMethod();
}
}
注意:需要配置 @EnableAspectJAutoProxy(exposeProxy = true)。
- 线程池选择:
- 默认线程池:Spring Boot 默认使用
SimpleAsyncTaskExecutor,每次调用创建新线程,不推荐生产使用。 - 自定义线程池:推荐配置
ThreadPoolTaskExecutor,复用线程,提高性能。 -
线程池隔离:不同业务使用不同线程池,避免相互影响。
-
异常处理:
@Async
public CompletableFuture<String> asyncMethodWithException() {
try {
// 异步逻辑
return CompletableFuture.completedFuture("done");
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"AOP 代理" | 完整原理:AOP 代理、拦截器、TaskExecutor |
| 技术深度 | 不知道自调用失效的原因 | 解释代理机制和自调用绕过代理 |
| 实践经验 | 没有提到线程池选择和异常处理 | 知道线程池隔离、异常处理、AopContext |
| 面试官印象 | 知道 @Async 的基本用法 | 理解 @Async 的底层机制和最佳实践 |
3、场景题:Agent 并发调用多个工具(并行 Tool Call),如何用 Spring 异步 + CompletableFuture 实现并发编排,并处理部分失败?¶
难度级别:⭐⭐⭐(并发编排、部分失败、超时控制)
1️⃣ Common Answer
用 @Async 标注工具调用方法,然后用 CompletableFuture.allOf() 等待所有任务完成。如果某个失败,可以捕获异常处理。
2️⃣ Impressive Answer
- 工具调用服务:
@Service
public class ToolService {
@Autowired
private TaskExecutor toolExecutor;
@Async("toolExecutor")
public CompletableFuture<ToolResult> callTool(ToolRequest request) {
try {
// 调用工具
ToolResult result = doCallTool(request);
return CompletableFuture.completedFuture(result);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
private ToolResult doCallTool(ToolRequest request) {
// 实际工具调用逻辑
return ToolResult.builder()
.name(request.getName())
.data("result")
.build();
}
}
- 并发编排:
@Service
public class AgentOrchestrator {
@Autowired
private ToolService toolService;
public CompletableFuture<AgentResponse> orchestrateTools(List<ToolRequest> requests) {
// 并发调用所有工具
List<CompletableFuture<ToolResult>> futures = requests.stream()
.map(toolService::callTool)
.collect(Collectors.toList());
// 等待所有任务完成(包括失败的任务)
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 处理结果
return allFutures.thenApply(v -> {
List<ToolResult> results = futures.stream()
.map(future -> {
try {
return future.get();
} catch (Exception e) {
return ToolResult.builder()
.name("failed")
.error(e.getMessage())
.build();
}
})
.collect(Collectors.toList());
return AgentResponse.builder()
.results(results)
.build();
});
}
}
- 超时控制:
public CompletableFuture<AgentResponse> orchestrateToolsWithTimeout(
List<ToolRequest> requests, long timeout, TimeUnit unit) {
List<CompletableFuture<ToolResult>> futures = requests.stream()
.map(request -> toolService.callTool(request)
.orTimeout(timeout, unit) // 单个工具超时
.exceptionally(e -> ToolResult.builder()
.name(request.getName())
.error("Timeout")
.build()))
.collect(Collectors.toList());
// 整体超时
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
).orTimeout(timeout * futures.size(), unit);
return allFutures.thenApply(v -> {
List<ToolResult> results = futures.stream()
.map(future -> future.join())
.collect(Collectors.toList());
return AgentResponse.builder()
.results(results)
.build();
});
}
- 部分失败处理:
public CompletableFuture<AgentResponse> orchestrateToolsWithPartialFailure(
List<ToolRequest> requests) {
List<CompletableFuture<ToolResult>> futures = requests.stream()
.map(request -> toolService.callTool(request)
.handle((result, e) -> {
if (e != null) {
return ToolResult.builder()
.name(request.getName())
.error(e.getMessage())
.build();
}
return result;
}))
.collect(Collectors.toList());
return CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
).thenApply(v -> {
List<ToolResult> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
// 检查是否有失败的工具
boolean hasFailure = results.stream()
.anyMatch(r -> r.getError() != null);
return AgentResponse.builder()
.results(results)
.hasFailure(hasFailure)
.build();
});
}
- 线程池配置:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("toolExecutor")
public TaskExecutor toolExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("tool-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
- 实践优化:
- 熔断降级:使用 Resilience4j 实现熔断,连续失败时快速失败。
- 限流:限制并发工具调用的数量,避免资源耗尽。
- 重试:对失败的工具调用进行重试,提高成功率。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"@Async + CompletableFuture.allOf" | 完整方案:超时控制、部分失败处理、线程池配置 |
| 技术深度 | 不知道如何处理部分失败 | 使用 handle 方法处理异常,支持部分失败 |
| 实践经验 | 没有提到熔断、限流、重试 | 提到 Resilience4j、限流、重试 |
| 面试官印象 | 能实现基础并发调用 | 能设计生产级的并发编排方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| CompletableFuture 的常用方法有哪些? | 并发编排的核心 API |
| 如何实现线程池监控? | 线程池配置需要监控 |
| 如何实现异步任务的超时和重试? | 并发调用的容错机制 |
3.5 Spring 事务底层原理¶
1、基础题:编程式事务(TransactionTemplate)和声明式事务(@Transactional)的区别是什么?¶
难度级别:⭐⭐(事务管理、编程式 vs 声明式)
Answer
-
编程式事务(TransactionTemplate):在代码中显式管理事务,使用
TransactionTemplate.execute()包裹事务代码。 -
声明式事务(@Transactional):通过注解声明事务,Spring AOP 自动管理事务,代码更简洁。
-
区别:
- 编程式:灵活但繁琐,适合复杂事务逻辑。
- 声明式:简洁但不够灵活,适合大多数场景。
2、进阶题:Spring 事务的底层实现原理是什么?TransactionInterceptor + PlatformTransactionManager + ThreadLocal 如何协作?¶
难度级别:⭐⭐⭐(AOP 代理、事务管理器、ThreadLocal)
1️⃣ Common Answer
Spring 事务是通过 AOP 实现的,TransactionInterceptor 拦截 @Transactional 方法,调用 PlatformTransactionManager 管理事务。ThreadLocal 用于绑定数据库连接,确保同一个事务使用同一个连接。
2️⃣ Impressive Answer
- Spring 事务的底层实现:
- AOP 代理:
@Transactional是一个 AOP 注解,Spring 会为标注了@Transactional的 Bean 创建代理。 - TransactionInterceptor:拦截
@Transactional方法调用,在方法执行前开启事务,执行后提交或回滚事务。 - PlatformTransactionManager:事务管理器,负责事务的开启、提交、回滚。常用实现:
DataSourceTransactionManager:JDBC 事务管理器。JpaTransactionManager:JPA 事务管理器。JtaTransactionManager:JTA 分布式事务管理器。
-
ThreadLocal:
TransactionSynchronizationManager使用ThreadLocal绑定数据库连接和事务状态,确保同一个事务使用同一个连接。 -
事务执行流程:
请求 → TransactionInterceptor 拦截
→ PlatformTransactionManager.getTransaction()(开启事务)
→ TransactionSynchronizationManager.bindResource()(绑定连接到 ThreadLocal)
→ 执行目标方法
→ PlatformTransactionManager.commit()(提交事务)
→ TransactionSynchronizationManager.unbindResource()(解除绑定)
- ThreadLocal 的作用:
- 绑定连接:将数据库连接绑定到当前线程,确保同一个事务的所有操作使用同一个连接。
- 事务状态:保存事务状态(是否活跃、是否只读、隔离级别等)。
-
线程隔离:每个线程有独立的事务上下文,互不干扰。
-
事务传播机制:
- REQUIRED(默认):如果当前存在事务,加入事务;否则创建新事务。
- REQUIRES_NEW:创建新事务,如果当前存在事务,挂起当前事务。
- SUPPORTS:如果当前存在事务,加入事务;否则非事务执行。
- NOT_SUPPORTED:非事务执行,如果当前存在事务,挂起当前事务。
- MANDATORY:必须在一个已有事务中执行,否则抛出异常。
- NEVER:不能在事务中执行,否则抛出异常。
-
NESTED:如果当前存在事务,嵌套事务执行;否则创建新事务。
-
事务失效场景:
- 自调用:同类中非事务方法调用事务方法,绕过了代理。
- 异常处理:异常被捕获未抛出,事务不会回滚。
- private 方法:
@Transactional标注在private方法上无效。 - final 方法:CGLIB 代理无法继承
final方法,事务失效。 - 多线程:新线程无法获取主线程的事务上下文。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"AOP + TransactionManager" | 完整流程:AOP 代理、拦截器、ThreadLocal 绑定 |
| 技术深度 | 不知道 ThreadLocal 的作用 | 解释 ThreadLocal 绑定连接和事务状态 |
| 实践经验 | 没有提到事务传播和失效场景 | 详细说明 7 种传播机制和 5 种失效场景 |
| 面试官印象 | 知道事务的基本原理 | 理解事务的底层机制和边界条件 |
3、场景题:Agent 执行完工具调用后需要在事务提交后才发消息(避免消息先到但数据未提交),如何用 TransactionSynchronization 实现?¶
难度级别:⭐⭐⭐(事务同步、消息发送、事务提交后执行)
1️⃣ Common Answer
用 @Transactional 注解方法,在方法最后发消息。或者用 TransactionSynchronizationManager 注册一个同步器,在事务提交后发消息。
2️⃣ Impressive Answer
- TransactionSynchronization 实现:
@Service
public class AgentService {
@Autowired
private MessageProducer messageProducer;
@Transactional
public void executeToolAndSendMessage(ToolRequest request) {
// 1. 执行工具调用(事务操作)
ToolResult result = toolService.execute(request);
// 2. 注册事务同步器
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事务提交后才发消息
messageProducer.send(result);
}
@Override
public void afterCompletion(int status) {
// 事务完成后清理资源
if (status == STATUS_ROLLED_BACK) {
log.warn("Transaction rolled back, message not sent");
}
}
});
// 3. 返回结果
return result;
}
}
- 封装 TransactionSynchronization 工具类:
@Component
public class TransactionHelper {
public void afterCommit(Runnable callback) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
callback.run();
}
});
} else {
// 如果没有事务,直接执行
callback.run();
}
}
public void afterCompletion(Runnable callback) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
callback.run();
}
});
} else {
callback.run();
}
}
}
- 使用工具类:
@Service
public class AgentService {
@Autowired
private TransactionHelper transactionHelper;
@Autowired
private MessageProducer messageProducer;
@Transactional
public void executeToolAndSendMessage(ToolRequest request) {
// 执行工具调用
ToolResult result = toolService.execute(request);
// 注册事务提交后回调
transactionHelper.afterCommit(() -> {
messageProducer.send(result);
});
return result;
}
}
- 事务回滚处理:
@Transactional
public void executeToolAndSendMessage(ToolRequest request) {
ToolResult result = toolService.execute(request);
transactionHelper.afterCompletion(status -> {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
// 事务提交成功,发送消息
messageProducer.send(result);
} else if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
// 事务回滚,记录日志或发送补偿消息
log.warn("Transaction rolled back for tool: {}", request.getName());
messageProducer.sendCompensation(result);
}
});
return result;
}
- 实践优化:
- 异步发送:事务提交后异步发送消息,避免阻塞事务提交。
- 消息幂等:确保消息幂等,避免重复发送。
-
补偿机制:如果消息发送失败,需要补偿机制(重试或人工介入)。
-
@TransactionalEventListener(Spring 4.2+):
@Component
public class TransactionEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void executeToolAndSendMessage(ToolRequest request) {
ToolResult result = toolService.execute(request);
// 发布事务事件
eventPublisher.publishEvent(new ToolCompletedEvent(result));
return result;
}
}
@Component
public class TransactionEventListener {
@Autowired
private MessageProducer messageProducer;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleToolCompleted(ToolCompletedEvent event) {
// 事务提交后才执行
messageProducer.send(event.getResult());
}
}
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"注册 TransactionSynchronization" | 完整方案:工具类封装、回滚处理、异步发送 |
| 技术深度 | 不知道 @TransactionalEventListener | 介绍 Spring 4.2+ 的事件驱动事务同步 |
| 实践经验 | 没有考虑异步发送和幂等 | 提到异步发送、消息幂等、补偿机制 |
| 面试官印象 | 能实现基础事务同步 | 能设计生产级的事务同步方案 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| 事务的隔离级别有哪些? | 事务管理的重要概念 |
| 如何实现分布式事务(Seata)? | 事务管理的进阶话题 |
| 如何优化事务性能(减少事务范围)? | 事务性能优化的最佳实践 |
2.4 Spring 容器启动流程¶
1、基础题:ApplicationContext 和 BeanFactory 的区别是什么?¶
难度级别:⭐(容器接口、功能增强、国际化支持)
Answer
-
BeanFactory:是 Spring 容器的基础接口,提供了最核心的 Bean 容器能力(getBean、containsBean 等),采用延迟加载策略(Lazy Init),只有在第一次 getBean 时才创建 Bean 实例,启动速度快,适合资源受限场景。
-
ApplicationContext:是 BeanFactory 的子接口,在基础功能上做了大量增强:
- 国际化支持:MessageSource 接口,支持 i18n
- 事件发布:ApplicationEventPublisher,支持事件监听机制
- 资源加载:ResourceLoader,支持从 classpath、URL 等加载资源
- 环境抽象:Environment,支持 profile、配置属性
-
预实例化:默认在容器启动时创建所有 singleton Bean(可通过 lazy-init 覆盖)
-
选择建议:开发中优先使用 ApplicationContext(如 ClassPathXmlApplicationContext、AnnotationConfigApplicationContext),BeanFactory 主要在框架内部使用。
2、进阶题:Spring 容器的 refresh() 方法做了哪些事?核心步骤是什么?¶
难度级别:⭐⭐⭐(容器启动12步骤、BeanDefinition加载、BeanFactoryPostProcessor、BeanPostProcessor)
1️⃣ Common Answer
refresh() 方法是 Spring 容器启动的核心方法,做了很多事情,比如加载配置文件、创建 BeanFactory、实例化 Bean、注册 BeanPostProcessor 等。大概有十几个步骤,具体记不清了。
2️⃣ Impressive Answer
-
refresh() 核心定位:
AbstractApplicationContext.refresh()是容器启动的模板方法,定义了容器启动的完整流程,12 个步骤环环相扣,确保容器正确初始化。 -
12 个核心步骤:
① prepareRefresh():准备刷新环境,设置启动时间、初始化属性源
② obtainFreshBeanFactory():创建 BeanFactory,加载 BeanDefinition(解析 @Component、@Bean)
③ prepareBeanFactory():配置 BeanFactory(注册 Aware 接口、设置类加载器)
④ postProcessBeanFactory():子类扩展点,允许修改 BeanFactory
⑤ invokeBeanFactoryPostProcessors():执行 BeanFactoryPostProcessor(如 @PropertySource 解析)
⑥ registerBeanPostProcessors():注册 BeanPostProcessor(AOP、@Autowired 都依赖它)
⑦ initMessageSource():初始化国际化资源
⑧ initApplicationEventMulticaster():初始化事件广播器
⑨ onRefresh():子类扩展点,如 Web 容器启动
⑩ registerListeners():注册事件监听器
⑪ finishBeanFactoryInitialization():实例化所有非延迟加载的 singleton Bean(核心步骤)
⑫ finishRefresh():发布 ContextRefreshedEvent,容器启动完成
- 关键扩展点:
- 步骤⑤:
BeanFactoryPostProcessor可以修改 BeanDefinition(如 @ConfigurationClassPostProcessor 处理 @Configuration) - 步骤⑥:
BeanPostProcessor在 Bean 创建前后拦截,AOP 代理在postProcessAfterInitialization创建 -
步骤⑪:真正的 Bean 实例化,包括依赖注入、初始化回调
-
实践注意:如果容器启动慢,可以检查是否有过多的 Bean 在步骤⑪被预实例化,或者 BeanPostProcessor 执行时间过长。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只说了"加载配置、创建 Bean" | 完整 12 步骤,每步都有明确职责 |
| 技术深度 | 不知道 BeanFactoryPostProcessor 和 BeanPostProcessor 的区别 | 清楚两者在不同步骤执行,作用不同 |
| 实践经验 | 没有提到扩展点 | 知道如何通过扩展点定制容器启动流程 |
| 面试官印象 | 背过概念但说不清细节 | 理解容器启动的设计思路和关键机制 |
3、场景题:如何在 Spring 容器完全启动后执行初始化逻辑?ApplicationRunner、CommandLineRunner、@EventListener(ContextRefreshedEvent) 有什么区别?¶
难度级别:⭐⭐⭐(容器启动后回调、执行顺序、参数差异、适用场景)
1️⃣ Common Answer
可以用 ApplicationRunner 或者 CommandLineRunner,它们都是在容器启动后执行的。@EventListener 也可以监听 ContextRefreshedEvent 事件,大概差不多。
2️⃣ Impressive Answer
-
三种方式的执行时机:都在容器启动完成后执行,但执行顺序和适用场景不同。
-
ApplicationRunner:
- 接口定义:
run(ApplicationArguments args) - 参数:
ApplicationArguments封装了命令行参数(支持--key=value格式,可获取非选项参数) - 推荐场景:需要解析命令行参数的初始化逻辑
@Component
public class DataInitializer implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
if (args.containsOption("init-data")) {
// 执行数据初始化
}
}
}
- CommandLineRunner:
- 接口定义:
run(String... args) - 参数:原始命令行参数数组(
String[]) - 推荐场景:简单的命令行参数处理
@Component
public class CacheInitializer implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 预热缓存
}
}
- @EventListener(ContextRefreshedEvent):
- 事件机制:监听容器刷新完成事件
- 执行时机:在
finishRefresh()时发布,早于 ApplicationRunner/CommandLineRunner - 注意:如果是父子容器(如 Spring Boot + Spring MVC),事件会被触发多次,需判断
event.getApplicationContext().getParent() == null
@Component
public class SystemInitializer {
@EventListener
public void onContextRefreshed(ContextRefreshedEvent event) {
if (event.getApplicationContext().getParent() == null) {
// 只在根容器触发一次
}
}
}
-
执行顺序:
@EventListener(ContextRefreshedEvent)→ApplicationRunner/CommandLineRunner(可通过@Order指定顺序) -
实践建议:优先使用
ApplicationRunner,参数更友好;如果需要监听父子容器事件,用@EventListener。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"都能在启动后执行" | 清楚执行顺序和参数差异 |
| 技术深度 | 不知道 ContextRefreshedEvent 的父子容器问题 | 解释了事件的多次触发问题和解决方案 |
| 实践经验 | 没有区分适用场景 | 给出不同场景的选择建议 |
| 面试官印象 | 知道有这些接口但不深入 | 理解三种机制的设计意图和使用边界 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| BeanFactoryPostProcessor 和 BeanPostProcessor 的区别? | 容器启动的两个关键扩展点 |
| @DependsOn 的作用是什么? | 控制 Bean 的初始化顺序 |
| Spring Boot 启动流程(SpringApplication.run())? | Spring Boot 对 refresh() 的封装 |
2.5 Spring 循环依赖¶
1、基础题:什么是循环依赖?Spring 默认是如何解决的?¶
难度级别:⭐(循环依赖定义、setter注入可解决、构造器注入无法解决)
Answer
-
循环依赖定义:A 依赖 B,B 又依赖 A(或者 A→B→C→A),形成闭环。
-
Spring 默认解决策略:
- 可以解决:setter 注入(单例 Bean),通过三级缓存机制
- 无法解决:构造器注入(实例化时就需要依赖对象,无法提前暴露)
-
无法解决:prototype 作用域(每次都创建新实例,缓存机制失效)
-
解决原理:在创建 Bean 时,先将半成品的 Bean(属性未注入)暴露出去,让其他 Bean 可以引用,等所有 Bean 创建完成后再注入属性。
2、进阶题:Spring 三级缓存的结构是什么?为什么需要三级而不是两级?¶
难度级别:⭐⭐⭐(三级缓存Map、earlySingletonObjects、singletonFactories、AOP代理时机)
1️⃣ Common Answer
三级缓存就是三个 Map,分别是 singletonObjects、earlySingletonObjects、singletonFactories。第一级存完整的 Bean,第二级存半成品,第三级存工厂。为什么需要三级?可能是为了解决 AOP 代理的问题。
2️⃣ Impressive Answer
- 三级缓存结构(
DefaultSingletonBeanRegistry):
// 一级缓存:完整的 Bean(已初始化完成)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:早期暴露的 Bean(实例化但未初始化,可能被代理)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:Bean 工厂(ObjectFactory,用于提前暴露 Bean 或创建 AOP 代理)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
- 三级缓存的工作流程(以 A 依赖 B,B 依赖 A 为例):
① 创建 A:实例化 A,将 A 的 ObjectFactory 放入三级缓存
② 注入 B:发现依赖 B,创建 B
③ 创建 B:实例化 B,注入 A
④ 获取 A:从三级缓存获取 A 的 ObjectFactory,调用 getObject()
⑤ AOP 判断:如果 A 需要 AOP 代理,ObjectFactory 创建代理对象
⑥ 升级缓存:将代理对象从三级移到二级缓存
⑦ B 完成:A 的代理对象注入到 B,B 初始化完成,放入一级缓存
⑧ A 完成:A 注入 B(从一级缓存获取),A 初始化完成,从二级移到一级
- 为什么需要三级而不是两级?
- 如果只有两级:在实例化 A 时,如果 A 需要 AOP 代理,必须立即创建代理对象并放入二级缓存。但此时 A 还没初始化,无法确定是否真的需要代理(可能被其他逻辑覆盖)。
-
三级缓存的优势:
ObjectFactory是延迟执行的,只有在其他 Bean 真正引用 A 时,才调用getObject()创建代理。这样可以:- 避免不必要的代理创建(如果 A 不被循环引用,就不需要代理)
- 确保 AOP 代理在正确的时机创建(
getEarlyBeanReference回调)
-
AOP 代理的时机:如果 A 被
@Async、@Transactional等注解标注,且发生了循环依赖,A 的代理对象会在getEarlyBeanReference中提前创建,而不是在BeanPostProcessor.postProcessAfterInitialization。 -
实践注意:如果项目中没有 AOP 或循环依赖,三级缓存其实可以简化为两级(Spring 源码注释也提到过)。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"三个 Map" | 清楚每个缓存的用途和 ObjectFactory 的延迟特性 |
| 技术深度 | 不知道为什么需要三级 | 解释了 AOP 代理的延迟创建机制 |
| 实践经验 | 没有提到 AOP 代理的时机 | 理解循环依赖对 AOP 代理时机的影响 |
| 面试官印象 | 背过概念但不懂原理 | 理解三级缓存的设计意图和 AOP 的关系 |
3、场景题:构造器注入为什么无法解决循环依赖?项目中遇到循环依赖应该怎么处理?¶
难度级别:⭐⭐⭐(构造器注入时机、@Lazy、重新设计、设计模式)
1️⃣ Common Answer
构造器注入不行是因为创建对象的时候就需要依赖对象,但是对象还没创建好。可以用 @Lazy 注解,或者改成 setter 注入。
2️⃣ Impressive Answer
- 构造器注入无法解决的原因:
- 实例化时机:构造器注入在实例化阶段就需要依赖对象,而三级缓存是在实例化后才将半成品暴露出去。
-
死锁问题:A 实例化需要 B,B 实例化需要 A,两者都无法实例化,形成死锁。
-
解决方案: 方案 1:使用 @Lazy 注解(推荐)
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(@Lazy ServiceB serviceB) { // 延迟注入
this.serviceB = serviceB;
}
}
- 引入中间层:A 和 B 都依赖 C,通过 C 通信- 事件驱动:A 发布事件,B 监听事件- 设计模式:使用观察者模式、中介者模式解耦
// 事件驱动解耦
@Service
public class ServiceA {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void doSomething() {
eventPublisher.publishEvent(new MyEvent());
}
}
@Service
public class ServiceB {
@EventListener
public void handleEvent(MyEvent event) {
// 处理事件
}
}
- 实践建议:
- 优先使用构造器注入(保证不可变性),遇到循环依赖时用
@Lazy - 如果循环依赖频繁出现,说明架构设计有问题,应该重构
@DependsOn可以强制指定 Bean 初始化顺序,但治标不治本
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"构造器不行" | 解释了实例化时机和死锁原理 |
| 技术深度 | 只提到 @Lazy 和 setter | 给出事件驱动、中间层等架构级方案 |
| 实践经验 | 没有强调架构重构 | 指出频繁循环依赖是设计问题 |
| 面试官印象 | 会用注解但不懂原理 | 理解循环依赖的本质和解决思路 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @DependsOn 的作用是什么? | 控制 Bean 初始化顺序,可配合解决循环依赖 |
| @Async 导致的循环依赖问题? | @Async 会提前创建代理,可能引发循环依赖 |
| Spring 代理的创建时机? | 循环依赖时代理提前创建,正常情况在 BeanPostProcessor |
2.6 Spring 事件机制¶
1、基础题:Spring 的事件发布/监听机制是什么?如何使用?¶
难度级别:⭐(ApplicationEvent、ApplicationListener、观察者模式、解耦)
Answer
-
核心原理:Spring 事件机制是观察者模式的实现,通过
ApplicationEventPublisher发布事件,ApplicationListener监听事件,实现组件间的解耦。 -
使用方式: 方式 1:继承 ApplicationListener(传统方式)
// 定义事件
public class UserRegisteredEvent extends ApplicationEvent {
private final String username;
public UserRegisteredEvent(Object source, String username) {
super(source);
this.username = username;
}
}
// 监听事件
@Component
public class EmailNotificationListener implements ApplicationListener<UserRegisteredEvent> {
@Override
public void onApplicationEvent(UserRegisteredEvent event) {
System.out.println("Send email to " + event.getUsername());
}
}
// 发布事件
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void register(String username) {
// 注册逻辑
eventPublisher.publishEvent(new UserRegisteredEvent(this, username));
}
}
@Component
public class NotificationListener {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
System.out.println("Send email to " + event.getUsername());
}
}
- 核心优势:解耦业务逻辑,发布者不需要知道有哪些监听者,监听者之间互不影响。
2、进阶题:@EventListener 的实现原理是什么?同步事件和异步事件(@Async)有什么区别?¶
难度级别:⭐⭐⭐(EventListenerMethodProcessor、同步vs异步、异常处理、事务传播)
1️⃣ Common Answer
@EventListener 是通过注解扫描的,Spring 会自动注册监听器。同步事件就是发布后立即执行,异步事件用 @Async 可以在另一个线程执行。异步事件不影响主流程。
2️⃣ Impressive Answer
- @EventListener 实现原理:
- 注册时机:
EventListenerMethodProcessor(BeanFactoryPostProcessor)在容器启动时扫描所有带@EventListener的方法。 - 包装为 ApplicationListener:将方法包装成
ApplicationListenerMethodAdapter,注册到ApplicationEventMulticaster。 - 事件匹配:通过反射检查方法参数类型,匹配到对应事件时调用方法。
@Component
public class MyListener {
@EventListener(condition = "#event.success") // 支持 SpEL 条件
public void handleEvent(MyEvent event) {
// 处理逻辑
}
}
- 同步事件 vs 异步事件:
| 特性 | 同步事件(默认) | 异步事件(@Async) |
|---|---|---|
| 执行线程 | 发布者线程 | 独立线程池(默认 SimpleAsyncTaskExecutor) |
| 阻塞特性 | 阻塞发布者 | 不阻塞发布者 |
| 事务传播 | 共享发布者事务 | 独立事务(无事务) |
| 异常处理 | 监听器异常会向上传播 | 监听器异常不影响发布者 |
| 适用场景 | 需要事务一致性、数据同步 | 非核心逻辑、耗时操作 |
@Component
public class AsyncListener {
@Async // 异步执行
@EventListener
public void handleAsyncEvent(MyEvent event) {
// 在独立线程执行,不影响主流程
}
}
- 事务传播特性:
- 同步事件:监听器在发布者的事务中执行,如果监听器抛异常,发布者的事务会回滚。
-
异步事件:监听器在独立线程执行,不共享发布者的事务,即使监听器抛异常也不影响发布者。
-
异常处理:
- 同步事件:监听器异常会中断后续监听器,异常传播到发布者。
-
异步事件:监听器异常被捕获,不影响其他监听器和发布者(可以通过
@Async("exceptionHandler")指定异常处理器)。 -
实践注意:
- 异步事件如果需要事务,需要在监听器方法上加
@Transactional(独立事务)。 - 同步事件如果不想让监听器异常影响发布者,可以用
try-catch包裹监听器逻辑。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"同步阻塞、异步不阻塞" | 清楚事务传播、异常处理的差异 |
| 技术深度 | 不知道 EventListenerMethodProcessor | 解释了注解扫描和监听器注册机制 |
| 实践经验 | 没有提到事务和异常处理 | 给出不同场景的最佳实践 |
| 面试官印象 | 会用注解但不理解机制 | 理解事件机制的设计意图和边界 |
3、场景题:Agent 工具调用完成后需要异步通知多个下游系统,如何用 Spring 事件解耦?¶
难度级别:⭐⭐⭐(异步事件、多监听器、事务传播、异常隔离)
1️⃣ Common Answer
可以定义一个 ToolCallCompletedEvent,然后发布这个事件。下游系统监听这个事件,用 @Async 异步执行。这样 Agent 调用不会被阻塞。
2️⃣ Impressive Answer
- 场景分析:
- Agent 调用工具后,需要通知多个下游系统(日志系统、监控系统、审计系统等)。
-
下游系统的处理不应该影响 Agent 的响应速度,也不应该因为某个下游系统失败而影响主流程。
-
实现方案: 步骤 1:定义事件
public class ToolCallCompletedEvent extends ApplicationEvent {
private final String toolName;
private final Object result;
private final long duration;
public ToolCallCompletedEvent(Object source, String toolName, Object result, long duration) {
super(source);
this.toolName = toolName;
this.result = result;
this.duration = duration;
}
// getters...
}
@Service
public class AgentService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Async
public CompletableFuture<Object> callTool(String toolName, Map<String, Object> params) {
long start = System.currentTimeMillis();
Object result = toolExecutor.execute(toolName, params);
long duration = System.currentTimeMillis() - start;
// 发布事件(异步发布,不阻塞当前线程)
eventPublisher.publishEvent(new ToolCallCompletedEvent(this, toolName, result, duration));
return CompletableFuture.completedFuture(result);
}
}
@Component
public class MonitoringListener {
@Async("agentEventExecutor") // 指定线程池
@EventListener
public void recordMetrics(ToolCallCompletedEvent event) {
metricsService.recordToolCall(event.getToolName(), event.getDuration());
}
}
@Component
public class AuditListener {
@Async("agentEventExecutor")
@EventListener
public void auditToolCall(ToolCallCompletedEvent event) {
auditService.logToolCall(event.getToolName(), event.getResult());
}
}
@Component
public class LogListener {
@Async("agentEventExecutor")
@EventListener
public void logToolCall(ToolCallCompletedEvent event) {
log.info("Tool {} called, duration: {}ms", event.getToolName(), event.getDuration());
}
}
@Configuration
public class AsyncConfig {
@Bean("agentEventExecutor")
public Executor agentEventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("agent-event-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
- 关键设计点:
- 异步发布:
callTool方法本身就是@Async,事件发布也在异步线程中,不会阻塞用户请求。 - 异常隔离:某个监听器失败不会影响其他监听器,也不会影响 Agent 主流程。
- 独立线程池:使用专用线程池,避免和业务线程池竞争资源。
-
事务独立:监听器在独立事务中执行,即使需要数据库操作也不会影响 Agent 的事务。
-
实践注意:
- 如果监听器需要重试机制,可以用 Spring Retry(
@Retryable)。 - 如果监听器执行顺序有要求,可以用
@Order指定顺序(但异步执行时顺序不保证)。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"发布事件、异步监听" | 完整方案,包含事件定义、发布、监听、线程池配置 |
| 技术深度 | 没有提到异常隔离和事务独立 | 解释了异步事件的异常处理和事务特性 |
| 实践经验 | 没有考虑线程池和资源隔离 | 给出生产级的线程池配置建议 |
| 面试官印象 | 会用事件机制但不深入 | 理解异步事件的设计边界和最佳实践 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| TransactionalEventListener 的作用? | 监听事务提交后的事件 |
| @Async 的线程池如何配置? | 异步事件的线程池管理 |
| Spring 事件机制和消息队列的区别? | 本地事件 vs 分布式事件 |
2.7 Spring Bean 作用域¶
1、基础题:Spring Bean 有哪些作用域?singleton 和 prototype 的区别?¶
难度级别:⭐(singleton、prototype、request、session、application)
Answer
- Spring Bean 的作用域:
- singleton(默认):单例,整个容器只有一个实例
- prototype:原型,每次 getBean 都创建新实例
- request:每个 HTTP 请求一个实例(仅 Web 环境)
- session:每个 HTTP Session 一个实例(仅 Web 环境)
-
application:ServletContext 生命周期一个实例(仅 Web 环境)
-
singleton vs prototype 区别:
| 特性 | singleton | prototype |
|---|---|---|
| 实例数量 | 容器中只有一个 | 每次 getBean 都创建新实例 |
| 创建时机 | 容器启动时(默认) | 每次 getBean 时 |
| 生命周期管理 | 容器管理(初始化、销毁) | 容器只创建,不管理销毁 |
| 线程安全 | 需要保证线程安全 | 每个线程独立实例,天然线程安全 |
| 适用场景 | 无状态对象(Service、DAO) | 有状态对象(Command、Form) |
2、进阶题:singleton Bean 注入 prototype Bean 时,为什么每次拿到的不是新实例?如何解决?¶
难度级别:⭐⭐⭐(依赖注入时机、@Lookup、ObjectFactory、Provider)
1️⃣ Common Answer
因为 singleton Bean 只初始化一次,注入 prototype Bean 时也只注入了一次,所以每次拿到的都是同一个实例。可以用 @Lookup 注解或者 ObjectFactory 来解决。
2️⃣ Impressive Answer
- 问题根源:
- 依赖注入时机:singleton Bean 在容器启动时初始化,此时注入的 prototype Bean 只会被创建一次。
- 缓存机制:注入后,prototype Bean 的引用被 singleton Bean 持有,后续调用时复用同一个实例。
@Service
public class SingletonService {
@Autowired
private PrototypeBean prototypeBean; // 只注入一次
public void doSomething() {
prototypeBean.execute(); // 每次都是同一个实例
}
}
- 解决方案: 方案 1:@Lookup 注解(推荐)
@Service
public abstract class SingletonService {
public void doSomething() {
PrototypeBean prototypeBean = getPrototypeBean(); // 每次调用都创建新实例
prototypeBean.execute();
}
@Lookup
protected abstract PrototypeBean getPrototypeBean();
}
@Service
public class SingletonService {
@Autowired
private ObjectFactory<PrototypeBean> prototypeBeanFactory;
public void doSomething() {
PrototypeBean prototypeBean = prototypeBeanFactory.getObject(); // 每次获取新实例
prototypeBean.execute();
}
}
@Service
public class SingletonService {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public void doSomething() {
PrototypeBean prototypeBean = prototypeBeanProvider.get(); // 每次获取新实例
prototypeBean.execute();
}
}
@Service
public class SingletonService {
@Autowired
private ApplicationContext applicationContext;
public void doSomething() {
PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
prototypeBean.execute();
}
}
- 方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| @Lookup | 代码简洁,Spring 原生支持 | 需要抽象类或方法 |
| ObjectFactory | 灵活,支持延迟加载 | 需要额外接口 |
| Provider | JSR-330 标准 | 依赖额外依赖 |
| ApplicationContext | 简单直接 | 耦合容器,不推荐 |
- 实践建议:优先使用
@Lookup,代码最简洁;如果需要更灵活的控制(如缓存、条件获取),用ObjectFactory。
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"注入时机问题" | 解释了依赖注入时机和缓存机制 |
| 技术深度 | 只提到 @Lookup | 给出 4 种方案,对比优缺点 |
| 实践经验 | 没有区分适用场景 | 给出不同场景的选择建议 |
| 面试官印象 | 会用注解但不理解原理 | 理解依赖注入机制和作用域的边界 |
3、场景题:Agent 的会话上下文(每个用户会话独立)如何用 Spring Scope 管理?¶
难度级别:⭐⭐⭐(自定义 Scope、ThreadLocal、Session Scope、请求上下文)
1️⃣ Common Answer
可以用 Session Scope,每个用户的会话一个实例。或者用 ThreadLocal 存上下文。
2️⃣ Impressive Answer
- 场景分析:
- Agent 每次对话需要维护会话上下文(历史消息、用户偏好、工具调用状态等)。
-
不同用户的会话上下文必须隔离,同一用户的多次请求共享上下文。
-
解决方案: 方案 1:Session Scope(Web 环境)
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AgentSessionContext {
private List<Message> history = new ArrayList<>();
private Map<String, Object> preferences = new HashMap<>();
public void addMessage(Message message) {
history.add(message);
}
// getters...
}
@Service
public class AgentService {
@Autowired
private AgentSessionContext sessionContext; // 每个用户会话一个实例
public void chat(String message) {
sessionContext.addMessage(new Message(message));
// 处理逻辑
}
}
- **注意**:需要在 singleton Bean 中注入 Session Scope Bean 时,使用 `proxyMode = ScopedProxyMode.TARGET_CLASS` 创建代理对象。
// 定义自定义 Scope
public class ConversationScope implements Scope {
private final ThreadLocal<Map<String, Object>> conversationContext = ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return conversationContext.get().computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return conversationContext.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// 销毁回调
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return String.valueOf(Thread.currentThread().getId());
}
}
// 注册自定义 Scope
@Configuration
public class ScopeConfig {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("conversation", new ConversationScope());
return configurer;
}
}
// 使用自定义 Scope
@Component
@Scope("conversation")
public class AgentContext {
private List<Message> history = new ArrayList<>();
// ...
}
@Component
public class AgentContextHolder {
private static final ThreadLocal<AgentContext> CONTEXT = new ThreadLocal<>();
public static void setContext(AgentContext context) {
CONTEXT.set(context);
}
public static AgentContext getContext() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
// 拦截器中设置上下文
@Component
public class AgentInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String conversationId = request.getHeader("X-Conversation-Id");
AgentContext context = contextRepository.findByConversationId(conversationId);
AgentContextHolder.setContext(context);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
AgentContextHolder.clear();
}
}
- 方案对比:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Session Scope | Web 环境,每个用户会话 | Spring 原生支持,简单 | 依赖 Web 容器 |
| 自定义 Scope | 非Web环境,灵活控制 | 可完全自定义生命周期 | 实现复杂 |
| ThreadLocal | 简单场景,请求级别 | 轻量级,无侵入 | 需要手动管理,内存泄漏风险 |
- 实践建议:
- Web 环境优先用 Session Scope
- 如果需要跨请求共享上下文(如 WebSocket),用自定义 Scope 或 Redis 存储
- ThreadLocal 适合简单的请求级别上下文,但要注意内存泄漏(及时清理)
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"Session Scope" | 给出 3 种方案,覆盖不同场景 |
| 技术深度 | 没有提到代理模式 | 解释了 ScopedProxyMode 的作用 |
| 实践经验 | 没有区分 Web 和非 Web 环境 | 给出不同环境的选择建议 |
| 面试官印象 | 会用 Session Scope 但不深入 | 理解 Scope 的扩展机制和自定义能力 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @RefreshScope 的作用是什么? | Spring Cloud 的动态刷新 Scope |
| ThreadLocal 的内存泄漏问题? | 自定义 Scope 需要注意的问题 |
| Spring 代理的两种模式? | ScopedProxyMode 的 JDK 动态代理 vs CGLIB |
2.8 Spring 条件装配与自动配置¶
1、基础题:@ConditionalOnProperty、@ConditionalOnClass 等条件注解的作用是什么?¶
难度级别:⭐(条件装配、@Conditional系列、自动配置)
Answer
-
条件装配核心:
@Conditional注解可以根据特定条件决定是否注册 Bean,避免不必要的 Bean 创建,提升启动速度和灵活性。 -
常用条件注解:
@ConditionalOnProperty:根据配置属性判断@ConditionalOnClass:根据 classpath 中是否存在某个类判断@ConditionalOnMissingBean:根据容器中是否缺少某个 Bean 判断@ConditionalOnBean:根据容器中是否存在某个 Bean 判断-
@ConditionalOnWebApplication:根据是否是 Web 应用判断 -
使用示例:
@Configuration
public class AgentConfiguration {
@Bean
@ConditionalOnProperty(name = "agent.enabled", havingValue = "true")
public AgentService agentService() {
return new AgentService();
}
@Bean
@ConditionalOnClass(name = "com.alibaba.dubbo.config.ApplicationConfig")
public DubboAgentService dubboAgentService() {
return new DubboAgentService();
}
@Bean
@ConditionalOnMissingBean(ToolExecutor.class)
public DefaultToolExecutor defaultToolExecutor() {
return new DefaultToolExecutor();
}
}
2、进阶题:Spring Boot 自动配置的原理是什么?从 @EnableAutoConfiguration 到 Bean 注册的完整链路?¶
难度级别:⭐⭐⭐(自动配置原理、spring.factories、@EnableAutoConfiguration、条件过滤)
1️⃣ Common Answer
Spring Boot 自动配置就是根据 classpath 里的 jar 包自动配置 Bean。@EnableAutoConfiguration 开启自动配置,spring.factories 里定义了配置类。条件注解过滤不需要的配置。
2️⃣ Impressive Answer
-
自动配置核心定位:Spring Boot 通过"约定优于配置"的理念,根据 classpath 中的依赖自动装配 Bean,减少手动配置。
-
完整链路:
① @SpringBootApplication 启动
② @EnableAutoConfiguration 导入 AutoConfigurationImportSelector
③ AutoConfigurationImportSelector 从 spring.factories 加载配置类
④ 配置类经过条件过滤(@Conditional 系列)
⑤ 符合条件的配置类注册为 BeanDefinition
⑥ 配置类中的 @Bean 方法创建 Bean
- 关键步骤详解: 步骤 1:@EnableAutoConfiguration 导入选择器
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) // 导入自动配置选择器
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
public class AutoConfigurationImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 从 spring.factories 加载所有自动配置类
List<String> configurations = getCandidateConfigurations(
annotationMetadata,
getAttributes()
);
// 去重、排序
configurations = removeDuplicates(configurations);
configurations = sort(configurations);
// 条件过滤
configurations = filter(configurations, getAutoConfigurationMetadata());
return configurations.toArray(new String[0]);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, Attributes attributes) {
// 从 spring.factories 加载
return SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader()
);
}
}
# spring-boot-autoconfigure/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
# ... 更多配置类
@Configuration
@ConditionalOnClass(DataSource.class) // classpath 中有 DataSource 类
@ConditionalOnMissingBean(DataSource.class) // 容器中没有 DataSource Bean
@EnableConfigurationProperties(DataSourceProperties.class) // 绑定配置属性
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.datasource.type") // 配置了 type 属性
public DataSource dataSource(DataSourceProperties properties) {
// 创建 DataSource
}
}
-
配置类本身注册为 BeanDefinition- 配置类中的
@Bean方法解析为 BeanDefinition- 条件过滤后,符合条件的 Bean 被实例化 -
自动配置报告:
- 启动参数
--debug可以查看自动配置报告 -
报告显示哪些配置类被匹配,哪些被排除
-
实践注意:
- 如果不需要某个自动配置,可以用
@EnableAutoConfiguration(exclude = {...})排除 - 自定义 Starter 时,需要在
spring.factories中声明配置类
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"从 spring.factories 加载" | 完整链路,从注解到 Bean 注册 |
| 技术深度 | 不知道 AutoConfigurationImportSelector | 解释了选择器的工作机制 |
| 实践经验 | 没有提到条件过滤和调试 | 给出自动配置报告和排除配置的方法 |
| 面试官印象 | 背过概念但不深入 | 理解自动配置的设计思路和扩展能力 |
3、场景题:如何实现一个自定义 Starter,让 Agent 的 LLM 客户端在引入依赖后自动装配?¶
难度级别:⭐⭐⭐(自定义Starter、自动配置、spring.factories、条件装配)
1️⃣ Common Answer
创建一个 Spring Boot 项目,写配置类,在 spring.factories 里注册配置类。然后打包成 jar,其他项目引入这个依赖就能自动配置了。
2️⃣ Impressive Answer
- 场景分析:
- 需要创建一个
agent-llm-starter,让其他项目引入依赖后自动配置 LLM 客户端。 - 支持配置 LLM 的 API Key、模型名称、超时时间等参数。
-
支持多种 LLM 提供商(OpenAI、通义千问等),通过配置切换。
-
实现步骤: 步骤 1:创建 Starter 项目结构
agent-llm-starter/
├── pom.xml
└── src/main/java/
└── com/example/agent/llm/
├── autoconfigure/
│ ├── LlmAutoConfiguration.java
│ └── LlmProperties.java
├── client/
│ ├── LlmClient.java
│ ├── OpenAiLlmClient.java
│ └── QwenLlmClient.java
└── META-INF/
└── spring.factories
@ConfigurationProperties(prefix = "agent.llm")
public class LlmProperties {
private String provider = "openai"; // 默认 OpenAI
private String apiKey;
private String model = "gpt-3.5-turbo";
private Integer timeout = 30000;
private Integer maxRetries = 3;
// getters and setters...
}
public interface LlmClient {
String chat(String prompt);
}
public class OpenAiLlmClient implements LlmClient {
private final String apiKey;
private final String model;
public OpenAiLlmClient(String apiKey, String model) {
this.apiKey = apiKey;
this.model = model;
}
@Override
public String chat(String prompt) {
// 调用 OpenAI API
return "Response from OpenAI";
}
}
public class QwenLlmClient implements LlmClient {
private final String apiKey;
private final String model;
public QwenLlmClient(String apiKey, String model) {
this.apiKey = apiKey;
this.model = model;
}
@Override
public String chat(String prompt) {
// 调用通义千问 API
return "Response from Qwen";
}
}
@Configuration
@EnableConfigurationProperties(LlmProperties.class)
@ConditionalOnClass(LlmClient.class) // classpath 中有 LlmClient 类
@ConditionalOnProperty(prefix = "agent.llm", name = "enabled", havingValue = "true", matchIfMissing = true)
public class LlmAutoConfiguration {
@Bean
@ConditionalOnMissingBean(LlmClient.class) // 容器中没有 LlmClient Bean
public LlmClient llmClient(LlmProperties properties) {
switch (properties.getProvider().toLowerCase()) {
case "openai":
return new OpenAiLlmClient(properties.getApiKey(), properties.getModel());
case "qwen":
return new QwenLlmClient(properties.getApiKey(), properties.getModel());
default:
throw new IllegalArgumentException("Unsupported provider: " + properties.getProvider());
}
}
@Bean
@ConditionalOnProperty(prefix = "agent.llm", name = "async", havingValue = "true")
public LlmClient asyncLlmClient(LlmClient llmClient) {
return new AsyncLlmClient(llmClient);
}
}
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.agent.llm.autoconfigure.LlmAutoConfiguration
<!-- pom.xml -->
<dependency>
<groupId>com.example</groupId>
<artifactId>agent-llm-starter</artifactId>
<version>1.0.0</version>
</dependency>
# application.yml
agent:
llm:
enabled: true
provider: openai
api-key: ${LLM_API_KEY}
model: gpt-4
timeout: 30000
async: true
@Service
public class AgentService {
@Autowired
private LlmClient llmClient; // 自动注入
public String chat(String prompt) {
return llmClient.chat(prompt);
}
}
- 最佳实践:
- 条件装配:使用
@ConditionalOnMissingBean允许用户自定义 Bean - 配置属性:使用
@ConfigurationProperties绑定配置,支持 IDE 自动提示 - 文档完善:提供 README 和配置示例
-
测试覆盖:编写自动配置的集成测试
-
实践注意:
- Starter 的命名规范:
{name}-spring-boot-starter - 如果需要条件装配,确保条件逻辑正确(如
@ConditionalOnClass的类存在) - 自动配置类不要扫描包(
@ComponentScan),避免意外的 Bean 注册
3️⃣ Key Differences
| 维度 | Common Answer | Impressive Answer |
|---|---|---|
| 结构性 | 只知道"创建配置类、写 spring.factories" | 完整实现,包含配置属性、客户端、条件装配 |
| 技术深度 | 没有提到条件装配和配置绑定 | 解释了 @ConditionalOnMissingBean 的作用 |
| 实践经验 | 没有给出生产级的代码示例 | 给出完整的 Starter 项目结构和最佳实践 |
| 面试官印象 | 会写 Starter 但不规范 | 理解 Starter 的设计规范和生产级实践 |
4、容易一起考的题¶
| 关联题 | 和本题的关系 |
|---|---|
| @ConfigurationProperties 的作用? | 配置属性绑定,支持自动提示 |
| @EnableConfigurationProperties 的作用? | 启用配置属性类 |
| Spring Boot 的 SPI 机制? | spring.factories 的加载机制 |