Spring常见题
题目 1:@Component
和 @Bean
的区别是什么?
问题描述:
请解释 @Component
和 @Bean
注解的区别,并说明它们各自的使用场景。
答案:
-
@Component
:- 是一个类级别的注解,用于将类标记为 Spring 容器中的组件。
- Spring 会通过类路径扫描(
@ComponentScan
)自动发现并注册该类为 Bean。 - 示例:
@Component public class MyService { // ... }
-
@Bean
:- 是一个方法级别的注解,通常用在配置类中(标注了
@Configuration
的类)。 - 用于显式地定义一个 Bean,并由方法返回值提供 Bean 实例。
- 示例:
@Configuration public class AppConfig { @Bean public MyService myService() { return new MyService(); } }
- 是一个方法级别的注解,通常用在配置类中(标注了
主要区别
特性 | @Component | @Bean |
---|---|---|
定义方式 | 类级别注解 | 方法级别注解 |
自动注册 | 需要类路径扫描 | 手动定义在配置类中 |
使用场景 | 适用于简单的组件 | 适用于复杂的对象创建逻辑或第三方库 |
总结
- 如果是自己开发的类,推荐使用
@Component
。 - 如果需要手动控制 Bean 的创建过程(如第三方库或复杂逻辑),推荐使用
@Bean
。
题目 2:@Autowired
和 @Resource
的区别是什么?
问题描述:
请解释 @Autowired
和 @Resource
注解的区别,并说明它们各自的使用场景。
答案:
-
@Autowired
:- 是 Spring 提供的注解,支持按类型注入(byType)。
- 默认情况下,如果找不到匹配的 Bean,会抛出异常。
- 可以通过
@Qualifier
指定具体的 Bean 名称。 - 示例:
@Autowired @Qualifier("myService") private MyService myService;
-
@Resource
:- 是 JSR-250 标准提供的注解,支持按名称注入(byName)。
- 如果未指定名称,则默认按字段名查找 Bean。
- 如果按名称找不到,则回退到按类型注入。
- 示例:
@Resource(name = "myService") private MyService myService;
主要区别
特性 | @Autowired | @Resource |
---|---|---|
规范来源 | Spring | JSR-250 标准 |
注入方式 | 默认按类型注入 | 默认按名称注入 |
异常处理 | 找不到 Bean 时抛出异常 | 找不到 Bean 时回退到按类型注入 |
总结
- 如果需要更灵活的注入方式(如按名称注入),推荐使用
@Resource
。 - 如果完全依赖 Spring 生态系统,推荐使用
@Autowired
。
题目 3:@Controller
、@Service
和 @Repository
的区别是什么?
问题描述:
请解释 @Controller
、@Service
和 @Repository
注解的区别,并说明它们的作用。
答案:
-
@Controller
:- 用于标记控制器层的类,通常用于处理 HTTP 请求。
- 结合
@RequestMapping
或其他映射注解,定义请求的处理逻辑。 - 示例:
@Controller public class MyController { @RequestMapping("/hello") public String hello() { return "Hello, World!"; } }
-
@Service
:- 用于标记服务层的类,通常包含业务逻辑。
- 主要用于区分业务逻辑层和其他层(如控制器层、数据访问层)。
- 示例:
@Service public class MyService { public String processData() { return "Processed Data"; } }
-
@Repository
:- 用于标记数据访问层的类,通常与数据库交互。
- Spring 会自动为
@Repository
注解的类添加异常转换功能,将数据访问异常转换为 Spring 的数据访问异常。 - 示例:
@Repository public class MyRepository { public void saveData() { // 数据库操作 } }
主要区别
注解 | 层次 | 作用 |
---|---|---|
@Controller | 控制器层 | 处理 HTTP 请求 |
@Service | 服务层 | 包含业务逻辑 |
@Repository | 数据访问层 | 数据库交互,自动转换异常 |
总结
- 这三个注解本质上都是
@Component
的特化版本,用于分层管理代码。 - 使用这些注解有助于提高代码的可读性和可维护性。
题目 4:Spring 中的单例 Bean 是否线程安全?为什么?
问题描述:
请解释 Spring 中的单例 Bean 是否线程安全,并说明原因。
答案:
-
单例 Bean 的线程安全性:
- Spring 容器中的单例 Bean 默认是线程不安全的。
- 原因:Spring 容器只会创建一个实例,并将其共享给所有线程使用。如果该 Bean 中有可变状态(如成员变量),则可能会导致线程安全问题。
-
示例:
@Component public class MySingletonBean { private int count = 0; public void increment() { count++; System.out.println("Count: " + count); } }
如果多个线程同时调用
increment()
方法,count
的值可能会出现竞争条件。 -
解决方法:
- 无状态设计:避免在单例 Bean 中使用可变状态(推荐)。
@Component public class MySingletonBean { public void process() { // 无状态逻辑 } }
- 同步机制:使用
synchronized
或锁确保线程安全。 - ThreadLocal:为每个线程维护独立的状态副本。
- 无状态设计:避免在单例 Bean 中使用可变状态(推荐)。
总结
- 单例 Bean 的线程安全性取决于其设计。如果 Bean 是无状态的,则天然线程安全;否则需要额外的同步措施。
题目 5:Spring 的 @Transactional
注解有哪些常见的坑?
问题描述:
请列举 Spring 中使用 @Transactional
注解时常见的坑,并说明如何避免。
答案:
-
事务方法必须是 public 的:
- Spring 的 AOP 代理机制要求事务方法必须是
public
的,否则事务不会生效。 - 解决方法:确保事务方法是
public
。
- Spring 的 AOP 代理机制要求事务方法必须是
-
事务传播行为未正确设置:
- 默认的传播行为是
PROPAGATION_REQUIRED
,可能不符合实际需求。 - 解决方法:根据业务需求明确设置传播行为。
- 默认的传播行为是
-
自调用问题:
- 如果一个类中的方法调用了另一个带有
@Transactional
注解的方法,事务不会生效。 - 原因:Spring 的事务管理是基于代理的,自调用绕过了代理。
- 解决方法:将事务方法抽取到另一个 Bean 中,或者使用
AopContext.currentProxy()
。
- 如果一个类中的方法调用了另一个带有
-
异常未被捕获导致事务提交:
- 默认情况下,只有未捕获的运行时异常(
RuntimeException
)和错误(Error
)才会触发回滚。 - 解决方法:使用
rollbackFor
属性指定需要回滚的异常类型。@Transactional(rollbackFor = Exception.class) public void myMethod() throws Exception { // ... }
- 默认情况下,只有未捕获的运行时异常(
-
事务隔离级别未正确设置:
- 默认隔离级别是
ISOLATION_DEFAULT
,可能导致脏读、不可重复读等问题。 - 解决方法:根据业务需求明确设置隔离级别。
- 默认隔离级别是
总结
- 使用
@Transactional
注解时需要特别注意方法签名、传播行为、异常处理等细节,避免陷入常见陷阱。
题目 6:解释 Spring 中的循环依赖问题,并说明如何解决
问题描述:
请详细解释 Spring 中的循环依赖问题(Circular Dependency),并说明 Spring 如何解决它。如果无法解决,有哪些替代方案?
答案:
循环依赖问题
- 循环依赖是指两个或多个 Bean 相互依赖,形成一个闭环。例如:
在这种情况下,@Component public class A { private final B b; @Autowired public A(B b) { this.b = b; } } @Component public class B { private final A a; @Autowired public B(A a) { this.a = a; } }
A
依赖B
,而B
又依赖A
,导致 Spring 容器无法完成注入。
Spring 的解决方式
-
Spring 使用三级缓存机制来解决循环依赖问题:
- 一级缓存(Singleton Objects):存储完全初始化好的单例 Bean。
- 二级缓存(Early Singleton Objects):存储提前暴露的 Bean(尚未完全初始化)。
- 三级缓存(Singleton Factories):存储 Bean 的工厂对象,用于生成早期引用。
-
当 Spring 检测到循环依赖时:
- 它会先将未完全初始化的 Bean 放入二级缓存中。
- 其他 Bean 可以从二级缓存中获取该 Bean 的引用,完成注入。
- 最终,所有 Bean 都会被完全初始化并放入一级缓存。
局限性
- Spring 只能解决单例(Singleton)作用域的循环依赖。
- 对于原型(Prototype)作用域的 Bean,Spring 无法解决循环依赖。
替代方案
- 重构代码:通过引入中间层(如接口或服务类)来打破循环依赖。
- 使用 Setter 注入:相比于构造器注入,Setter 注入可以延迟依赖注入的时间,从而避免部分循环依赖问题。
题目 7:Spring AOP 的底层实现原理是什么?Proxy 和 CGLIB 的区别是什么?
问题描述:
请解释 Spring AOP 的底层实现原理,并比较 Proxy 和 CGLIB 的区别。
答案:
Spring AOP 的底层实现
-
Spring AOP 是基于动态代理实现的,支持两种代理方式:
- JDK 动态代理:
- 基于接口实现,要求目标对象必须实现至少一个接口。
- Spring 创建一个实现了相同接口的代理对象,在调用方法时拦截并应用切面逻辑。
- CGLIB 动态代理:
- 基于子类实现,不要求目标对象实现接口。
- Spring 创建一个目标对象的子类,在子类中重写方法以应用切面逻辑。
- JDK 动态代理:
-
代理选择规则:
- 如果目标对象实现了接口,Spring 默认使用 JDK 动态代理。
- 如果目标对象没有实现接口,Spring 使用 CGLIB。
Proxy 和 CGLIB 的区别
特性 | JDK 动态代理 | CGLIB 动态代理 |
---|---|---|
实现代理的方式 | 基于接口 | 基于子类 |
性能 | 较慢(反射调用) | 较快(字节码生成) |
目标对象要求 | 必须实现接口 | 不需要实现接口 |
方法拦截 | 只能拦截接口中的方法 | 可以拦截所有非 final 方法 |
注意事项
- CGLIB 不能代理
final
方法,因为子类无法重写final
方法。 - 在 Spring Boot 中,默认使用 CGLIB(即使目标对象实现了接口),可以通过配置切换回 JDK 动态代理。
题目 8:Spring 中的事务传播行为有哪些?请举例说明 PROPAGATION_REQUIRED
和 PROPAGATION_REQUIRES_NEW
的区别
问题描述:
请列举 Spring 中的事务传播行为,并说明 PROPAGATION_REQUIRED
和 PROPAGATION_REQUIRES_NEW
的区别。
答案:
事务传播行为
Spring 提供了以下七种事务传播行为:
- PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;否则创建一个新事务。
- PROPAGATION_REQUIRES_NEW:总是创建一个新事务,如果当前存在事务,则挂起当前事务。
- PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;否则以非事务方式运行。
- PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则挂起当前事务。
- PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;否则抛出异常。
- PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务中运行;否则创建一个新事务。
PROPAGATION_REQUIRED
和 PROPAGATION_REQUIRES_NEW
的区别
-
PROPAGATION_REQUIRED
:- 如果外层方法已经开启了一个事务,则内层方法会复用该事务。
- 如果外层方法没有事务,则内层方法会创建一个新事务。
- 示例:
@Transactional(propagation = Propagation.REQUIRED) public void outerMethod() { innerMethod(); // 复用同一个事务 } @Transactional(propagation = Propagation.REQUIRED) public void innerMethod() { // 执行数据库操作 }
-
PROPAGATION_REQUIRES_NEW
:- 内层方法总是会创建一个新事务,即使外层方法已经有一个事务。
- 外层事务会被挂起,直到内层事务完成。
- 示例:
@Transactional(propagation = Propagation.REQUIRED) public void outerMethod() { innerMethod(); // 内层方法启动新事务 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void innerMethod() { // 启动独立事务 }
实际影响
- 如果
innerMethod
抛出异常,PROPAGATION_REQUIRED
会导致整个事务回滚,而PROPAGATION_REQUIRES_NEW
只会回滚内层事务,外层事务不受影响。
题目 9:Spring Boot 的自动配置是如何工作的?请解释其核心原理
问题描述:
请解释 Spring Boot 自动配置的工作原理,并说明其核心组件和实现细节。
答案:
自动配置的核心原理
- Spring Boot 的自动配置是基于条件化配置(Conditional Configuration)实现的。
- 它的核心思想是根据项目的依赖、环境变量等条件,自动注册适当的 Bean。
关键组件
-
@EnableAutoConfiguration
:- 启用自动配置功能,通常与
@SpringBootApplication
注解一起使用。 - 它会扫描
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,加载所有符合条件的自动配置类。
- 启用自动配置功能,通常与
-
@Conditional
注解族:- 例如
@ConditionalOnClass
、@ConditionalOnMissingBean
等,用于根据条件决定是否加载某个配置类。 - 示例:
@Configuration @ConditionalOnClass(DataSource.class) public class DataSourceAutoConfiguration { @Bean @ConditionalOnMissingBean public DataSource dataSource() { return new EmbeddedDatabaseBuilder().build(); } }
- 例如
-
spring.factories
文件(Spring Boot 2.7 之前):- 在
spring.factories
文件中定义了自动配置类的列表。 - 示例:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.MyAutoConfiguration
- 在
-
AutoConfiguration.imports
文件(Spring Boot 3.0+):- 替代了
spring.factories
,更高效地加载自动配置类。
- 替代了
工作流程
- Spring Boot 启动时,会扫描类路径下的所有依赖库。
- 根据
spring.factories
或AutoConfiguration.imports
文件加载自动配置类。 - 自动配置类通过条件注解判断是否满足条件,如果满足则注册相应的 Bean。