主题
四、Spring(15 题)
4.1 ★★★★Spring IoC 和 AOP 的核心原理是什么?IoC 和 DI 的区别是什么?
📖 点击查看答案
IoC(Inversion of Control,控制反转):
- 思想:对象创建和依赖关系的管理从代码中转移到容器
- 传统方式:对象自己 new 依赖对象
- IoC:容器创建对象并注入依赖
java
// 传统
class UserService {
UserDao dao = new UserDaoImpl(); // 自己创建
}
// IoC
class UserService {
@Autowired UserDao dao; // 容器注入
}- "控制"指对象创建、生命周期管理的控制权
- "反转"指控制权从代码转移到容器
DI(Dependency Injection,依赖注入):
- IoC 的具体实现方式
- 容器主动把依赖注入到对象中
- 三种注入方式:构造器、setter、字段
IoC vs DI:
| 维度 | IoC | DI |
|---|---|---|
| 范畴 | 思想/原则 | 实现方式 |
| 关注点 | 控制权转移 | 如何注入依赖 |
| 关系 | IoC 是目标 | DI 是实现 IoC 的手段 |
AOP(Aspect-Oriented Programming,面向切面编程):
- 思想:将横切关注点(日志、事务、安全)从业务代码中分离
- 通过动态代理在方法前后插入逻辑
核心概念:
| 概念 | 说明 |
|---|---|
| Aspect(切面) | 横切关注点的模块化(如日志切面) |
| JoinPoint(连接点) | 程序执行点,Spring 中是方法执行 |
| Pointcut(切点) | 匹配连接点的表达式 |
| Advice(通知) | 在切点处执行的逻辑(前置、后置等) |
| Target(目标对象) | 被代理的对象 |
| Proxy(代理) | AOP 创建的代理对象 |
| Weaving(织入) | 把切面应用到目标对象的过程 |
AOP 示例:
java
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethod() {}
@Before("serviceMethod()")
public void before(JoinPoint jp) {
log.info("调用方法: {}", jp.getSignature());
}
@Around("serviceMethod()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
log.info("耗时: {}ms", System.currentTimeMillis() - start);
return result;
}
}IoC 容器的核心接口:
BeanFactory:基础容器,延迟加载ApplicationContext:扩展容器,启动时加载
IoC 的工作流程:
- 读取配置(XML、注解、Java Config)
- 解析为 BeanDefinition
- 实例化 Bean
- 注入依赖
- 初始化(BeanPostProcessor)
- 放入容器供使用
AOP 的应用场景:
- 事务管理(@Transactional)
- 日志记录
- 权限校验
- 性能监控
- 缓存(@Cacheable)
- 异步(@Async)
4.2 ★★Spring IoC 容器初始化的完整流程是怎样的?
📖 点击查看答案
IoC 容器初始化流程(三大阶段):
- 资源定位(Resource Localization)
- BeanDefinition 加载和解析
- BeanDefinition 注册
详细流程:
1. 资源定位
- 通过 ResourceLoader 加载配置文件(XML、注解、Java Config)
- Resource 抽象不同来源:FileSystemResource、ClassPathResource、UrlResource
java
// XML 方式
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("application.xml");
// 注解方式
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);2. BeanDefinition 加载和解析
- 通过 BeanDefinitionReader 读取配置
- 解析为 BeanDefinition 对象(包含类名、scope、属性、依赖等元数据)
XML 配置 → XmlBeanDefinitionReader
注解配置 → AnnotatedBeanDefinitionReader / ClassPathBeanDefinitionScanner- BeanDefinition 是 Bean 的"图纸"
3. BeanDefinition 注册
- 把 BeanDefinition 注册到 BeanDefinitionRegistry
- 内部用 ConcurrentHashMap 存储
java
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();ApplicationContext 的初始化流程(AbstractApplicationContext.refresh()):
java
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 1. 准备刷新(设置启动时间、活动标志、校验必要属性)
prepareRefresh();
// 2. 获取 BeanFactory(创建 DefaultListableBeanFactory)
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3. BeanFactory 预准备(注册系统组件如 ApplicationContextAwareProcessor)
prepareBeanFactory(beanFactory);
// 4. 子类扩展点(注册特殊的 BeanPostProcessor)
postProcessBeanFactory(beanFactory);
// 5. 调用 BeanFactoryPostProcessor(修改 BeanDefinition)
invokeBeanFactoryPostProcessors(beanFactory);
// 6. 注册 BeanPostProcessor(用于 Bean 初始化时的扩展)
registerBeanPostProcessors(beanFactory);
// 7. 初始化 MessageSource(国际化)
initMessageSource();
// 8. 初始化事件广播器
initApplicationEventMulticaster();
// 9. 子类扩展点(初始化特殊 Bean,如 ThemeSource)
onRefresh();
// 10. 注册事件监听器
registerListeners();
// 11. 实例化所有非懒加载的单例 Bean
finishBeanFactoryInitialization(beanFactory);
// 12. 完成刷新(发布 ContextRefreshedEvent)
finishRefresh();
}
}关键步骤说明:
invokeBeanFactoryPostProcessors:调用 BeanFactoryPostProcessor,最典型的是 ConfigurationClassPostProcessor,处理 @Configuration、@ComponentScan、@Import、@Bean
registerBeanPostProcessors:注册 BeanPostProcessor,用于 Bean 实例化后、初始化前后的处理
finishBeanFactoryInitialization:实例化所有非懒加载的单例 Bean(最耗时)
Bean 的实例化过程:
- 调用
getBean(name)触发 - 从单例缓存查找(一、二、三级缓存)
- 没有 → 创建
- 实例化(反射调用构造器)
- 属性注入(解决循环依赖)
- 初始化(BeanPostProcessor 前置、InitializingBean、init-method、BeanPostProcessor 后置)
- 放入单例池
Bean 的生命周期(详见 4.4):
实例化 → 属性赋值 → 各种 Aware 回调 → BeanPostProcessor.before → 初始化 → BeanPostProcessor.after → 使用 → 销毁4.3 ★★BeanFactory 和 ApplicationContext 的区别是什么?FactoryBean 和 BeanFactory 的区别?
📖 点击查看答案
BeanFactory vs ApplicationContext:
| 维度 | BeanFactory | ApplicationContext |
|---|---|---|
| 定位 | IoC 容器的根接口 | BeanFactory 的子接口,扩展功能 |
| 加载时机 | 懒加载(getBean 时才创建) | 启动时预加载(默认) |
| 国际化 | 不支持 | 支持(MessageSource) |
| 事件机制 | 不支持 | 支持(ApplicationEvent) |
| 资源访问 | 不支持 | 支持(ResourceLoader) |
| AOP | 不支持 | 支持 |
| 注解支持 | 不支持 | 支持 |
| 性能 | 启动快 | 启动慢(初始化所有 Bean) |
| 实际使用 | 极少 | 默认 |
ApplicationContext 的常用实现:
ClassPathXmlApplicationContext:从 classpath 加载 XMLFileSystemXmlApplicationContext:从文件系统加载 XMLAnnotationConfigApplicationContext:基于注解配置GenericWebApplicationContext:Web 应用AnnotationConfigServletWebServerApplicationContext:Spring Boot
BeanFactory 的核心方法:
java
public interface BeanFactory {
Object getBean(String name) throws BeansException;
<T> T getBean(Class<T> requiredType);
boolean containsBean(String name);
boolean isSingleton(String name);
Class<?> getType(String name);
// ...
}FactoryBean vs BeanFactory:
| 维度 | FactoryBean | BeanFactory |
|---|---|---|
| 类型 | 接口 | 接口 |
| 作用 | 创建复杂 Bean 的工厂 | IoC 容器 |
| 关系 | BeanFactory 管理 FactoryBean | FactoryBean 被 BeanFactory 管理 |
| 调用 | getBean 返回 getObject() | getBean 返回 Bean 实例 |
FactoryBean 的作用:
- 用于创建复杂的 Bean(如需要特定初始化逻辑的 Bean)
- 实现接口后,
getBean(name)返回getObject()的结果,而不是 FactoryBean 本身 getBean("&name")返回 FactoryBean 本身
java
public interface FactoryBean<T> {
T getObject() throws Exception; // 返回真正的 Bean
Class<?> getObjectType();
boolean isSingleton();
}FactoryBean 示例:
java
@Component
public class MyConnectionFactoryBean implements FactoryBean<Connection> {
@Override
public Connection getObject() throws Exception {
// 复杂的创建逻辑
return DriverManager.getConnection(url, user, pwd);
}
@Override
public Class<?> getObjectType() { return Connection.class; }
@Override
public boolean isSingleton() { return false; } // 每次返回新实例
}
// 使用
Connection conn = ctx.getBean(Connection.class); // 调用 getObject()
MyConnectionFactoryBean fb = (MyConnectionFactoryBean) ctx.getBean("&myConnectionFactoryBean");FactoryBean 的典型应用:
- Spring AOP 的
ProxyFactoryBean - MyBatis 的
SqlSessionFactoryBean、MapperFactoryBean - Spring 集成第三方库时常用
- Spring Boot 自动配置中的条件 Bean
FactoryBean 与 @Bean 的区别:
- @Bean:方法返回值就是 Bean
- FactoryBean:实现接口,getObject() 返回 Bean,可控制是否单例、类型推断
4.4 ★★★★Spring Bean 的完整生命周期是怎样的?BeanPostProcessor 在其中起什么作用?
📖 点击查看答案
Bean 的完整生命周期(4 大阶段):
1. 实例化(Instantiation)
↓
2. 属性赋值(Populate Properties)
↓
3. 初始化(Initialization)
├─ Aware 接口回调
├─ BeanPostProcessor.postProcessBeforeInitialization
├─ InitializingBean.afterPropertiesSet / @PostConstruct / init-method
└─ BeanPostProcessor.postProcessAfterInitialization
↓
4. 使用(In Use)
↓
5. 销毁(Destruction)
├─ @PreDestroy
├─ DisposableBean.destroy
└─ destroy-method详细流程:
1. 实例化
- 容器调用构造器或工厂方法创建对象
- 此时对象已分配内存但属性未注入
java
@Service
public class UserService {
public UserService() { System.out.println("1. 实例化"); }
}2. 属性赋值
- 根据 @Autowired、@Resource、XML 配置注入依赖
- 解决循环依赖(通过三级缓存)
java
@Autowired
private UserDao userDao; // 此阶段注入3. 初始化
- 3.1 Aware 接口回调:注入容器组件
java
@Component
public class MyBean implements BeanNameAware, ApplicationContextAware {
public void setBeanName(String name) { /* 注入 Bean 名 */ }
public void setApplicationContext(ApplicationContext ctx) { /* 注入上下文 */ }
}- 3.2 BeanPostProcessor 前置处理:
postProcessBeforeInitialization- 此处处理 @PostConstruct、@Autowired(其实是后置)
java
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) {
// 初始化前处理
return bean;
}
}- 3.3 初始化方法(按顺序):
@PostConstruct注解方法InitializingBean.afterPropertiesSet()- XML/
@Bean(initMethod=)指定的 init-method
java
@PostConstruct
public void init() { System.out.println("3.1 @PostConstruct"); }
public class MyBean implements InitializingBean {
public void afterPropertiesSet() { System.out.println("3.2 afterPropertiesSet"); }
}- 3.4 BeanPostProcessor 后置处理:
postProcessAfterInitialization- 此处生成 AOP 代理(AbstractAutoProxyCreator)
4. 使用
- Bean 完全初始化,放入单例池
- 应用代码使用
5. 销毁
- 容器关闭时调用
- 顺序:
@PreDestroy注解方法DisposableBean.destroy()destroy-method指定的方法
java
@PreDestroy
public void cleanup() { /* 释放资源 */ }BeanPostProcessor 的作用:
- 在 Bean 初始化前后插入扩展逻辑
- 所有 Bean 都会经过 BeanPostProcessor
- 典型应用:
AutowiredAnnotationBeanPostProcessor:处理 @AutowiredCommonAnnotationBeanPostProcessor:处理 @PostConstruct、@PreDestroy、@ResourceAbstractAutoProxyCreator:生成 AOP 代理
java
public interface BeanPostProcessor {
default Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
default Object postProcessAfterInitialization(Object bean, String beanName) {
return bean; // 可返回代理对象
}
}关键细节:
- @Autowired 注入实际上发生在 populateBean 阶段,通过 AutowiredAnnotationBeanPostProcessor 的 postProcessProperties 方法
- AOP 代理生成在 postProcessAfterInitialization 阶段
- 三级缓存解决循环依赖,让提前暴露的半成品 Bean 也能被代理
完整代码示例:
java
@Component
public class LifeCycleBean implements InitializingBean, DisposableBean {
@Autowired private UserDao userDao;
public LifeCycleBean() { log.info("1. 实例化"); }
@PostConstruct
public void postConstruct() { log.info("3.1 @PostConstruct"); }
@Override
public void afterPropertiesSet() { log.info("3.2 afterPropertiesSet"); }
@PreDestroy
public void preDestroy() { log.info("5.1 @PreDestroy"); }
@Override
public void destroy() { log.info("5.2 destroy"); }
}4.5 ★★★Bean 的作用域有哪些?单例 Bean 是线程安全的吗?
📖 点击查看答案
Bean 的作用域(Scope):
| 作用域 | 说明 |
|---|---|
| singleton(默认) | 整个容器中只有一个实例 |
| prototype | 每次获取都创建新实例 |
| request | 每个 HTTP 请求一个实例(Web) |
| session | 每个 HTTP Session 一个实例(Web) |
| application | 每个 ServletContext 一个实例(Web) |
| websocket | 每个 WebSocket 一个实例(Web) |
作用域的指定:
java
@Component
@Scope("singleton") // 默认
public class SingletonBean { }
@Component
@Scope("prototype")
public class PrototypeBean { }
// JDK 8+ 也可以用常量
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)singleton vs prototype:
| 维度 | singleton | prototype |
|---|---|---|
| 实例数 | 1 | 每次新建 |
| 生命周期 | 容器管理 | 只创建,不管理销毁 |
| 状态 | 应保持无状态 | 可有状态 |
| 性能 | 高(复用) | 较低 |
singleton 注入 prototype 的问题:
- singleton Bean 只初始化一次,注入的 prototype 也是一次性的
- prototype 后续的修改在 singleton 中感知不到
java
@Component
@Scope("singleton")
public class SingletonService {
@Autowired
private PrototypeBean proto; // 只在初始化时注入一次
public void doWork() {
proto.process(); // 每次都是同一个实例
}
}解决方案:
- 使用
@Lookup注解让 Spring 每次返回新的 prototype 实例
java
@Component
public abstract class SingletonService {
@Lookup
public abstract PrototypeBean getPrototype(); // 每次返回新实例
public void doWork() {
getPrototype().process();
}
}- 注入
ObjectFactory或Provider
java
@Autowired
private ObjectFactory<PrototypeBean> prototypeFactory;
public void doWork() {
prototypeFactory.getObject().process();
}- 通过
ApplicationContext.getBean()获取
单例 Bean 是线程安全的吗?
- 不是! 单例 Bean 默认不是线程安全的
- 单例 = 所有线程共享同一个实例
- 如果 Bean 有可变状态(成员变量),多线程访问会有并发问题
java
@Component
public class UnsafeService {
private int count = 0; // 可变状态,线程不安全
public void increment() { count++; } // 非原子操作
}保证单例 Bean 线程安全的方法:
- 无状态 Bean(推荐):不定义可变成员变量
java
@Component
public class SafeService {
public String process(String input) { // 无状态,线程安全
return input.toUpperCase();
}
}使用 ThreadLocal:每个线程独立副本
加锁:synchronized 或 ReentrantLock
使用并发容器:ConcurrentHashMap 等
改用 prototype 作用域:每个请求一个实例(性能差)
4.6 ★★★Spring 中有哪些依赖注入方式?@Autowired、@Resource、@Inject 的区别是什么?
📖 点击查看答案
三种依赖注入方式:
1. 构造器注入(推荐)
java
@Component
public class UserService {
private final UserDao userDao;
@Autowired // 单构造器可省略
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}- 优点:依赖不可变(final)、不会出现 NPE、便于单元测试、避免循环依赖
- 缺点:依赖多时构造器臃肿
2. Setter 注入
java
@Component
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}- 优点:灵活,可重新注入
- 缺点:可能被多次修改,非 final
3. 字段注入(不推荐)
java
@Component
public class UserService {
@Autowired
private UserDao userDao;
}- 优点:简洁
- 缺点:无法 final、隐藏依赖关系、不便单元测试、容易 NPE
推荐顺序:构造器 > Setter > 字段
@Autowired、@Resource、@Inject 区别:
| 注解 | 来源 | 匹配方式 | 默认策略 | 是否 Spring |
|---|---|---|---|---|
| @Autowired | Spring | 按类型 → 按名称 | required=true | 是 |
| @Resource | JSR-250 | 按名称 → 按类型 | required=true | 否(标准) |
| @Inject | JSR-330 | 按类型 → 按名称 | required=true | 否(标准) |
@Autowired(Spring 提供):
- 默认按类型匹配
- 类型找不到或多个实现时,回退到按名称匹配字段名
- 可配合 @Qualifier 指定 bean 名称
required = false允许不注入(值可为 null)
java
@Autowired
@Qualifier("mysqlUserDao") // 指定 bean 名称
private UserDao userDao;
@Autowired(required = false) // 可选注入
private Optional<Cache> cache;@Resource(JSR-250 标准):
- 默认按名称匹配(先 name,再 type)
- 通过 name 和 type 属性指定
java
@Resource(name = "mysqlUserDao")
private UserDao userDao;
@Resource // 先按字段名 "userDao" 找,找不到再按类型
private UserDao userDao;@Inject(JSR-330 标准):
- 默认按类型匹配
- 类似 @Autowired 但不支持 required = false
- 配合 @Named 指定名称
java
@Inject
@Named("mysqlUserDao")
private UserDao userDao;多实现选择策略:
- @Primary 标注某个实现为优先
java
@Primary
@Component
public class MysqlUserDao implements UserDao { }@Qualifier 指定名称
字段名与 bean 名匹配
4.7 ★★★★Spring 的三级缓存是如何解决循环依赖的?为什么需要三级而不是两级缓存?
📖 点击查看答案
循环依赖问题:
- A 依赖 B,B 依赖 A
- 创建 A 时需要 B,创建 B 时又需要 A,导致无法完成初始化
java
@Component
class A {
@Autowired B b;
}
@Component
class B {
@Autowired A a;
}Spring 三级缓存:
java
// DefaultSingletonBeanRegistry
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 二级
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级| 缓存 | 名称 | 内容 |
|---|---|---|
| 一级 | singletonObjects | 完全初始化好的 Bean |
| 二级 | earlySingletonObjects | 提前暴露的半成品 Bean(已实例化但未初始化) |
| 三级 | singletonFactories | Bean 的 ObjectFactory(工厂,可生成代理) |
解决循环依赖的流程(A 依赖 B,B 依赖 A):
- 创建 A:实例化 A,把 A 的 ObjectFactory 放入三级缓存
java
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));- A 注入 B:从缓存找 B,没有 → 创建 B
- 创建 B:实例化 B,把 B 的 ObjectFactory 放入三级缓存
- B 注入 A:从缓存找 A
- 一级缓存没有
- 二级缓存没有
- 三级缓存有 → 调用 ObjectFactory.getObject() 获取早期 A
- 把早期 A 放入二级缓存,从三级缓存移除
- B 拿到 A 的早期引用,完成属性注入和初始化
- B 完全初始化,放入一级缓存
- A 拿到 B,完成属性注入和初始化
- A 完全初始化,放入一级缓存
核心源码:
java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName); // 一级
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName); // 二级
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); // 三级
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}为什么需要三级而不是两级?
关键在于 AOP 代理:
- 如果用两级缓存,Spring 必须在实例化后就立刻判断是否需要代理、是否创建代理
- 但 Spring 的设计原则是:AOP 代理在初始化后生成(postProcessAfterInitialization)
- 三级缓存存储的是 ObjectFactory,延迟生成代理:
- 没有循环依赖时:代理在初始化后正常生成,ObjectFactory 不被调用
- 有循环依赖时:ObjectFactory 被调用,提前生成代理(getEarlyBeanReference)
java
// AbstractAutoProxyCreator.getEarlyBeanReference
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean); // 标记已提前代理
return wrapIfNecessary(bean, beanName, cacheKey); // 生成代理
}- 如果只有两级缓存,且二级直接存半成品对象(非代理),那么循环依赖中拿到的就不是代理对象
- 三级缓存的 ObjectFactory 让 Spring 在需要时才生成代理,且保证只生成一次
简而言之:
- 一级缓存:完整 Bean
- 二级缓存:早期 Bean(可能是代理)
- 三级缓存:ObjectFactory,决定是否生成代理
循环依赖的限制:
- 只能解决单例 setter 注入的循环依赖
- 不能解决构造器循环依赖(实例化时就需要依赖)
- prototype 作用域不能解决(不使用缓存)
4.8 ★★★★Spring AOP 的实现原理是什么?JDK 动态代理和 CGLIB 代理分别在什么情况下使用?
📖 点击查看答案
Spring AOP 实现原理:
- 基于动态代理:运行时生成代理对象,在方法调用时插入切面逻辑
- 核心:
ProxyFactory、AopProxy接口 - 两种代理方式:JDK 动态代理、CGLIB 代理
AOP 的核心组件:
Advisor:切面,包含 Pointcut 和 AdvicePointcut:切点,决定拦截哪些方法Advice:通知,决定拦截后做什么(前置、后置、环绕)Advised:被代理的目标对象
代理创建流程:
- Bean 实例化后,
AbstractAutoProxyCreator.postProcessAfterInitialization检查是否需要代理 - 如果匹配切点,创建代理:
ProxyFactory.getProxy() - 根据 proxyTargetClass 配置和目标类是否实现接口,选择 JDK 或 CGLIB
java
// DefaultAopProxyFactory
public AopProxy createAopProxy(AdvisedSupport config) {
if (config.isOptimize() || config.isProxyTargetClass()
|| hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config); // JDK 动态代理
}
return new ObjenesisCglibAopProxy(config); // CGLIB 代理
} else {
return new JdkDynamicAopProxy(config); // JDK 动态代理
}
}JDK 动态代理使用场景:
- 目标类实现了至少一个接口
- 默认情况下(proxyTargetClass=false)
- Spring Boot 2.0+ 默认 proxyTargetClass=true,强制 CGLIB
CGLIB 代理使用场景:
- 目标类没有实现任何接口
- proxyTargetClass=true 显式指定
- @Configuration 类(保证 @Bean 方法间调用返回同一实例)
- Spring Boot 2.0+ 默认
JDK 动态代理原理:
- 代理类实现目标类的接口
- 调用转发给 InvocationHandler
java
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取拦截器链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
return method.invoke(target, args); // 直接调用
}
// 执行拦截器链
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
return invocation.proceed();
}
}CGLIB 代理原理:
- 通过 ASM 生成目标类的子类
- 重写方法,插入 MethodInterceptor 拦截逻辑
- FastClass 机制加速方法调用(避免反射)
性能对比:
- 创建:JDK 快,CGLIB 慢
- 调用:CGLIB 略快(FastClass 优化)
- 内存:CGLIB 占用更多
Spring AOP 的限制:
- 只能代理方法,不能代理字段
- 只能代理 public 方法(CGLIB 不能代理 private/final)
- 只能代理 Spring 容器管理的 Bean
- 方法内部调用(this.xxx)不经过代理
java
@Service
public class UserService {
@Transactional
public void methodA() { ... }
public void methodB() {
this.methodA(); // 不走代理,@Transactional 失效!
}
}解决方案:
- 注入自身代理:
@Autowired UserService self; self.methodA(); - AopContext.currentProxy():
((UserService) AopContext.currentProxy()).methodA();
4.9 ★★Spring AOP 的通知类型有哪些?执行顺序是怎样的?
📖 点击查看答案
五种通知类型:
| 通知 | 注解 | 说明 |
|---|---|---|
| 前置通知 | @Before | 方法执行前 |
| 后置通知 | @AfterReturning | 方法正常返回后 |
| 异常通知 | @AfterThrowing | 方法抛异常后 |
| 最终通知 | @After | 方法执行后(无论成功失败,类似 finally) |
| 环绕通知 | @Around | 环绕方法,可控制是否执行 |
示例:
java
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Around before");
try {
Object result = pjp.proceed();
System.out.println("Around after-returning");
return result;
} catch (Throwable e) {
System.out.println("Around after-throwing");
throw e;
} finally {
System.out.println("Around after");
}
}
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint jp) {
System.out.println("Before");
}
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
System.out.println("AfterReturning: " + result);
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint jp, Throwable ex) {
System.out.println("AfterThrowing: " + ex.getMessage());
}
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint jp) {
System.out.println("After");
}
}正常返回时的执行顺序:
Around before
Before
目标方法执行
AfterReturning
After
Around after-returning
Around after抛出异常时的执行顺序:
Around before
Before
目标方法执行(异常)
AfterThrowing
After
Around after-throwing
Around after多个切面的执行顺序:
- 通过
@Order(n)指定切面优先级,n 越小优先级越高 - 类似过滤器链:外层切面先进入后退出
切面 A @Order(1): Around before → Before → [切面 B...] → AfterReturning → After → Around after
切面 B @Order(2): Around before → Before → [目标方法] → AfterReturning → After → Around after执行顺序总结:
- 外层切面的 Before 在内层之前
- 外层切面的 After 在内层之后
- 形成洋葱模型:A-before → B-before → target → B-after → A-after
Spring 5.2.7+ 的顺序变化:
- 之前:@After 在 @AfterReturning/@AfterThrowing 之前
- 之后:@After 在 @AfterReturning/@AfterThrowing 之后(更符合 finally 语义)
4.10 ★★★Spring 事务的传播行为有哪些?举例说明 REQUIRED、REQUIRES_NEW、NESTED 的区别?
📖 点击查看答案
事务传播行为(Propagation):
- 当事务方法被另一个事务方法调用时,如何处理事务的传播
- 定义在
Propagation枚举中,共 7 种
| 传播行为 | 说明 |
|---|---|
| REQUIRED(默认) | 有事务加入,没有就新建 |
| SUPPORTS | 有事务加入,没有就非事务运行 |
| MANDATORY | 必须在事务中,否则抛异常 |
| REQUIRES_NEW | 总是新建事务,挂起当前事务 |
| NOT_SUPPORTED | 非事务运行,挂起当前事务 |
| NEVER | 非事务运行,有事务则抛异常 |
| NESTED | 嵌套事务,没有就新建,有就创建保存点 |
REQUIRED(默认):
- 有事务:加入当前事务
- 无事务:新建一个事务
java
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB(); // 加入 A 的事务
insertA();
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
insertB();
}
// 调用 methodA():A 和 B 在同一事务中,B 失败则 A 也回滚REQUIRES_NEW:
- 总是新建事务
- 如果当前有事务,先挂起当前事务
java
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
insertA();
methodB(); // 新开事务
insertA2();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
insertB();
}
// 调用 methodA():
// - A 开启事务1
// - B 挂起事务1,开启事务2
// - B 提交事务2
// - 恢复事务1,继续执行 insertA2
// - B 失败不影响 A(独立事务)- 适用场景:日志记录(无论业务成功与否,日志都要写入)
NESTED:
- 有事务:创建保存点(SAVEPOINT),形成嵌套事务
- 无事务:等价于 REQUIRED
java
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
insertA();
try {
methodB(); // 嵌套事务
} catch (Exception e) {
// B 回滚到保存点,A 可以继续
}
insertA2();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
insertB();
}
// 调用 methodA():
// - A 开启事务
// - B 在 A 内创建保存点
// - B 失败回滚到保存点,A 不受影响
// - A 继续执行 insertA2
// - A 失败则 B 也回滚(因为 B 在 A 的事务内)三者区别:
| 维度 | REQUIRED | REQUIRES_NEW | NESTED |
|---|---|---|---|
| 事务数 | 1 个 | 2 个(独立) | 1 个(嵌套) |
| B 失败影响 A | 是 | 否 | 否(catch 后) |
| A 失败影响 B | 是 | 否 | 是 |
| 底层机制 | 同一事务 | 独立事务 | 保存点(Savepoint) |
其他传播行为:
java
// SUPPORTS:有事务就加入,没有就非事务运行
@Transactional(propagation = Propagation.SUPPORTS)
public void query() { ... }
// MANDATORY:必须在事务中
@Transactional(propagation = Propagation.MANDATORY)
public void mustInTransaction() { ... }
// NOT_SUPPORTED:挂起当前事务,非事务运行
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void nonTransactional() { ... }
// NEVER:不能在事务中运行
@Transactional(propagation = Propagation.NEVER)
public void mustNoTransaction() { ... }4.11 ★★★★Spring 事务失效的常见场景有哪些?@Transactional 注解常见的坑有哪些?
📖 点击查看答案
Spring 事务失效的常见场景:
1. 方法非 public
- Spring AOP 默认只代理 public 方法
- private、protected、default 方法上的 @Transactional 无效
java
@Transactional
private void method() { } // 失效!2. 自调用(this 调用)
- 通过 this 调用的方法不经过代理,事务失效
java
@Service
public class UserService {
public void methodA() {
this.methodB(); // 不走代理,@Transactional 失效
}
@Transactional
public void methodB() { }
}- 解决:
- 注入自身:
@Autowired UserService self; self.methodB(); - AopContext:
((UserService) AopContext.currentProxy()).methodB();
- 注入自身:
3. 异常被吞掉
- try-catch 吞掉异常,事务无法感知,不会回滚
java
@Transactional
public void method() {
try {
dao.insert(...);
throw new RuntimeException("error");
} catch (Exception e) {
log.error("error", e); // 异常被吞,事务不回滚
}
}4. 异常类型不匹配
- Spring 默认只对
RuntimeException和Error回滚 - 受检异常(如 IOException)默认不回滚
java
@Transactional
public void method() throws IOException {
dao.insert(...);
throw new IOException("error"); // 默认不回滚
}
// 解决:指定 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void method2() throws IOException { }5. 传播行为错误
- NOT_SUPPORTED、NEVER 会让事务失效
java
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void method() { } // 非事务运行6. 数据库不支持事务
- MySQL 的 MyISAM 引擎不支持事务
- 必须用 InnoDB
7. Bean 未被 Spring 管理
- new 出来的对象不在 Spring 容器中,没有代理
java
UserService service = new UserService();
service.method(); // 不走代理,事务失效8. 事务管理器未配置
- 没有配置 DataSourceTransactionManager 或 PlatformTransactionManager
9. 多线程
- 事务绑定在当前线程,多线程中跨线程操作不在同一事务
java
@Transactional
public void method() {
new Thread(() -> {
dao.insert(...); // 不在主线程的事务中
}).start();
}10. 嵌套调用中的 REQUIRES_NEW
- 内层 REQUIRES_NEW 独立提交,外层失败不影响内层
@Transactional 的常见坑:
- 不指定 rollbackFor
java
// 反例
@Transactional // 默认只回滚 RuntimeException
public void method() throws Exception { }
// 正例
@Transactional(rollbackFor = Exception.class)
public void method() throws Exception { }- long 类型字段精度丢失
- 事务本身没问题,但要注意事务中的数据一致性
- 大事务导致性能问题
- 事务范围过大,占用数据库连接时间长
- 解决:缩小事务范围,把查询移到事务外
java
// 反例
@Transactional
public void method() {
// 大量查询(不需要事务)
List<User> users = userDao.findAll();
// 少量更新
userDao.update(...);
}
// 正例
public void method() {
List<User> users = userDao.findAll(); // 事务外
updateInTransaction(users);
}
@Transactional
public void updateInTransaction(List<User> users) { }- 事务方法调用静态方法或 final 方法
- final 方法不能被重写,CGLIB 无法代理
- rollbackFor 指定过宽
rollbackFor = Exception.class会回滚所有异常,包括业务上不该回滚的
4.12 ★★@Transactional 注解的实现原理是什么?rollbackFor 属性为什么很重要?
📖 点击查看答案
@Transactional 的实现原理:
1. AOP 代理
- @Transactional 通过 AOP 实现
- Spring 创建事务方法的代理对象
- 调用事务方法时,代理拦截并管理事务
2. 核心组件:
TransactionInterceptor:事务拦截器,实现 MethodInterceptorPlatformTransactionManager:事务管理器(如 DataSourceTransactionManager)TransactionStatus:事务状态
3. 执行流程:
1. 调用代理方法
2. TransactionInterceptor.invoke() 拦截
3. 获取事务管理器,开启事务
4. 执行目标方法
5. 成功 → 提交事务
异常 → 根据 rollbackFor 判断是否回滚TransactionInterceptor 源码简化:
java
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 获取事务属性(@Transactional 的配置)
TransactionAttributeSource tas = getTransactionAttributeSource();
TransactionAttribute txAttr = tas.getTransactionAttribute(method, targetClass);
// 2. 获取事务管理器
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 3. 创建事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, methodId);
try {
// 4. 执行目标方法
Object retVal = invocation.proceed();
// 5. 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
} catch (Throwable ex) {
// 6. 回滚事务(根据 rollbackFor 判断)
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
}回滚判断逻辑:
java
protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
// 回滚
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
} else {
// 提交(异常不匹配 rollbackFor)
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
// DefaultTransactionAttribute.rollbackOn
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}rollbackFor 属性的重要性:
- 默认情况下,Spring 只对
RuntimeException和Error回滚 - 受检异常(IOException、SQLException、ClassNotFoundException)不会触发回滚
java
// 默认行为
@Transactional
public void method() throws IOException {
userDao.update(...);
throw new IOException("文件不存在"); // 不回滚!数据已更新
}- 指定 rollbackFor 后,匹配的异常也会回滚
java
@Transactional(rollbackFor = Exception.class)
public void method() throws IOException {
userDao.update(...);
throw new IOException("文件不存在"); // 回滚
}为什么默认只回滚 RuntimeException?
- 设计哲学:受检异常通常是可恢复的业务异常,不应回滚
- RuntimeException 通常是不可恢复的程序错误,应该回滚
- 但实际业务中,受检异常(如 IOException)也常常需要回滚
最佳实践:
- 总是指定
@Transactional(rollbackFor = Exception.class) - 或自定义 RuntimeException 包装所有异常
事务传播的实现:
TransactionSynchronizationManager用 ThreadLocal 保存当前事务- 同一线程内调用其他事务方法时,根据传播行为决定加入/新建/挂起
java
private static final ThreadLocal<Object> actualTransactionStatus = new NamedThreadLocal<>("Actual transaction status");4.13 ★★★Spring MVC 的请求处理完整流程是什么?DispatcherServlet 在其中起什么作用?
📖 点击查看答案
Spring MVC 请求处理流程:
客户端请求
↓
DispatcherServlet (前端控制器)
↓
HandlerMapping (处理器映射)
↓ 找到 HandlerExecutionChain
HandlerAdapter (处理器适配器)
↓
Interceptor.preHandle
↓
Controller 方法
↓
返回 ModelAndView
↓
Interceptor.postHandle
↓
ViewResolver (视图解析器)
↓
View (视图渲染)
↓
Interceptor.afterCompletion
↓
响应客户端详细流程:
客户端发送请求到 DispatcherServlet
DispatcherServlet 调用 HandlerMapping 查找 Handler
- 根据 URL 找到对应的 Controller 方法
- 返回 HandlerExecutionChain(包含 Handler 和拦截器链)
DispatcherServlet 调用 HandlerAdapter 执行 Handler
- HandlerAdapter 适配不同类型的 Handler(如 @RequestMapping 方法)
- 执行拦截器的 preHandle
HandlerAdapter 执行 Controller 方法
- 参数解析(@RequestParam、@RequestBody 等)
- 调用 Controller 方法
- 处理返回值(@ResponseBody → JSON,ModelAndView → 视图)
DispatcherServlet 调用 ViewResolver 解析视图(如果是视图渲染)
- 把逻辑视图名解析为物理 View
View 渲染,填充 Model 数据
执行拦截器的 postHandle 和 afterCompletion
返回响应给客户端
核心组件:
| 组件 | 作用 |
|---|---|
| DispatcherServlet | 前端控制器,统一入口 |
| HandlerMapping | URL → Handler 映射 |
| HandlerAdapter | 适配不同类型的 Handler |
| HandlerInterceptor | 拦截器,前置/后置处理 |
| ViewResolver | 视图解析器 |
| View | 视图渲染 |
| HandlerExceptionResolver | 异常解析 |
| MultipartResolver | 文件上传 |
DispatcherServlet 的核心方法 doDispatch:
java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 查找 Handler
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
// 2. 查找 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 执行拦截器 preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) return;
// 4. 执行 Handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 5. 执行拦截器 postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 6. 处理结果(视图渲染或直接响应)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}@ResponseBody 的处理:
- 现代应用多用 RESTful API,返回 JSON
RequestMappingHandlerAdapter通过HandlerMethodReturnValueHandler处理返回值RequestResponseBodyMethodProcessor处理 @ResponseBody- 使用 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)转换为 JSON
DispatcherServlet 的作用:
- 统一入口:所有请求经过 DispatcherServlet
- 协调者:调度各组件协作
- 解耦:Handler、ViewResolver 等组件可替换
- 扩展性:通过实现接口或继承可扩展功能
4.14 ★★★Spring 中用到了哪些设计模式?请至少列举 6 种并说明其应用场景。
📖 点击查看答案
Spring 中的设计模式:
1. 单例模式(Singleton)
- Spring Bean 默认作用域是 singleton
- 通过 ConcurrentHashMap 缓存 Bean 实例
- 注意:Spring 的单例是容器级单例,不是 JVM 级单例
java
// DefaultSingletonBeanRegistry
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();2. 工厂模式(Factory)
- BeanFactory 是 IoC 容器的根接口,工厂模式
- FactoryBean 用于创建复杂 Bean
java
public interface FactoryBean<T> {
T getObject() throws Exception;
}3. 代理模式(Proxy)
- Spring AOP 基于动态代理实现
- JDK 动态代理:基于接口
- CGLIB:基于继承
java
// JdkDynamicAopProxy
Object proxy = Proxy.newProxyInstance(classLoader, interfaces, handler);4. 模板方法模式(Template Method)
- JdbcTemplate、RestTemplate、RedisTemplate 等
- 父类定义骨架,子类实现具体步骤
java
public abstract class JdbcTemplate {
public <T> T execute(ConnectionCallback<T> action) {
Connection con = DataSourceUtils.getConnection(getDataSource());
try {
return action.doInConnection(con);
} finally {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}5. 观察者模式(Observer)
- Spring 的事件机制:ApplicationEvent + ApplicationListener
- ApplicationContext 发布事件,监听器响应
java
// 发布事件
context.publishEvent(new UserCreatedEvent(user));
// 监听事件
@EventListener
public void handleUserCreated(UserCreatedEvent event) { }6. 适配器模式(Adapter)
- HandlerAdapter 适配不同类型的 Handler
- Spring MVC 中适配 @RequestMapping、SimpleController 等
java
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler);
}7. 装饰器模式(Decorator)
- BeanWrapper 包装 Bean,提供属性访问功能
- HttpServletRequestWrapper 等
8. 责任链模式(Chain of Responsibility)
- Spring AOP 的拦截器链
- Spring MVC 的 HandlerInterceptor 链
java
public interface HandlerInterceptor {
boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler);
void postHandle(...);
void afterCompletion(...);
}9. 策略模式(Strategy)
- Resource 接口的不同实现:ClassPathResource、FileSystemResource
- InstantiationStrategy:SimpleInstantiationStrategy、CglibSubclassingInstantiationStrategy
- 事务传播行为 Propagation
java
public interface Resource extends InputStreamSource {
InputStream getInputStream() throws IOException;
}10. 委派模式(Delegate)
- DispatcherServlet 委派请求给具体 Controller
- 不是 GOF 23 种模式之一,但 Spring 大量使用
11. 原型模式(Prototype)
- Spring Bean 的 prototype 作用域
- 每次获取都创建新实例
总结表:
| 设计模式 | Spring 应用 |
|---|---|
| 单例 | Bean 默认作用域 |
| 工厂 | BeanFactory、FactoryBean |
| 代理 | AOP(JDK/CGLIB) |
| 模板方法 | JdbcTemplate、RestTemplate |
| 观察者 | ApplicationEvent |
| 适配器 | HandlerAdapter |
| 装饰器 | BeanWrapper |
| 责任链 | 拦截器链 |
| 策略 | Resource、InstantiationStrategy |
| 委派 | DispatcherServlet |
| 原型 | prototype 作用域 |
4.15 ★★Spring 6 / Spring Boot 3 有哪些重要变化?GraalVM Native Image 支持意味着什么?
📖 点击查看答案
Spring 6 / Spring Boot 3 的重要变化:
1. 基线升级
- 要求 JDK 17+(不再支持 JDK 8/11)
- Jakarta EE 9+(javax.* → jakarta.*)
java
// 之前 (javax)
import javax.servlet.http.HttpServletRequest;
import javax.persistence.Entity;
// 现在 (jakarta)
import jakarta.servlet.http.HttpServletRequest;
import jakarta.persistence.Entity;- 所有 javax 包名改为 jakarta(Servlet、JPA、Validation 等)
2. GraalVM Native Image 支持
- Spring Boot 3 原生支持 AOT 编译
- 应用可编译为本地可执行文件
3. 其他重要特性:
- HTTP Interface(声明式 HTTP 客户端)
java
public interface UserClient {
@GetExchange("/users/{id}")
User getUser(@PathVariable("id") Long id);
}
// 自动生成实现类- Micrometer Observation API(统一可观测性)
- 虚拟线程支持(JDK 21)
- ProblemDetails(RFC 7807 错误响应标准)
GraalVM Native Image 意味着什么:
1. 极速启动
- 传统 Spring Boot 启动:1-3 秒
- Native Image:0.01-0.1 秒
- 适合 Serverless、FaaS 场景
2. 低内存占用
- 传统 JVM:200MB-1GB
- Native Image:50MB-100MB
- 适合容器化、微服务
3. 原理:AOT 编译
- 编译时分析,把字节码编译为本地机器码
- 不需要 JIT,启动即最高性能
- 但失去 JIT 的运行时优化能力
4. 工作流程:
1. 编译 Java 代码为字节码
2. Spring AOT 处理:生成反射、资源、代理等元数据
3. GraalVM native-image 工具编译为本地可执行文件
4. 运行时不需要 JVMbash
# Spring Boot 3 构建 Native Image
mvn -Pnative native:compile
# 或用 buildpacks
mvn spring-boot:build-image -Pnative5. Native Image 的限制:
- 不支持运行时动态类加载:所有类必须在编译时确定
- 反射需配置:通过 reachability metadata 声明
- 动态代理受限:需提前生成代理类
- CGLIB、字节码增强需适配:Spring AOT 会处理
- 构建时间长:几分钟到十几分钟
- 调试困难:无 JVM 调试支持
6. Spring AOT 的作用:
- 在编译时处理 Spring 应用:
- 解析 Bean 定义
- 生成 Bean 注册代码(避免运行时反射)
- 处理 @Configuration、@Bean
- 生成代理类
- 把运行时的工作提前到编译时
7. 适用场景:
- Serverless / FaaS:Lambda、Azure Functions
- 微服务:快速启动、低内存
- CLI 工具:命令行应用
- 容器化:镜像更小
8. 不适用场景:
- 长期运行的服务(JIT 优化更好)
- 大量动态特性的应用
- 需要运行时加载类的应用
Spring Boot 3 其他亮点:
@ConfigurationProperties支持 RecordWebMvcConfigurer改进- Log4j2 增强
- 可观测性大幅增强(Micrometer、OpenTelemetry)