5-spring
一、AOP
关于 Spring AOP,我将从以下几个方面进行详细的阐述:是什么、为什么、核心概念、实现原理、以及应用场景。
一、AOP 是什么?
AOP,面向切面编程,是一种编程范式。它的核心思想是:将那些遍布在应用多个模块中的、与核心业务逻辑无关的横切关注点(如日志、事务、安全等)分离出来,形成独立的“切面”,然后通过“织入”的方式应用到目标对象上。
我们可以这样理解:
• OOP(面向对象) 的核心单元是类(Class),它通过封装、继承、多态来建立对象的层级结构,解决的是纵向的代码复用问题。
• AOP(面向切面) 的核心单元是切面(Aspect),它像一把刀,横向地将多个不同类中的相同逻辑“切”出来,形成一个独立的模块,解决的是横向的代码复用问题。
二、为什么需要 AOP?
在没有 AOP 之前,我们的代码可能是这样的:
@Service
public class OrderService {
public void createOrder() {
// 1. 记录日志
System.out.println("[INFO] 开始创建订单...");
// 2. 开启事务
TransactionManager.beginTransaction();
try {
// --- 核心业务逻辑 ---
// 3. 验权
if (!userHasPermission()) {
throw new SecurityException("无权限");
}
// 4. 执行核心操作
// ... (创建订单的代码)
// --- 核心业务逻辑结束 ---
// 5. 提交事务
TransactionManager.commit();
// 6. 记录日志
System.out.println("[INFO] 订单创建成功。");
} catch (Exception e) {
// 7. 回滚事务
TransactionManager.rollback();
// 8. 记录错误日志
System.out.println("[ERROR] 创建订单失败: " + e.getMessage());
throw e;
}
}
// UserService, ProductService 等都有大量重复的日志、事务代码...
}
存在的问题:
- 代码重复:日志、事务等代码散落在各个业务方法中。
- 代码耦合:业务代码与非业务代码(横切逻辑)高度纠缠,核心业务逻辑不清晰。
- 维护困难:如果需要修改日志格式或事务策略,需要改动所有相关方法,容易出错。
使用 AOP 之后:
业务方法变得非常纯粹和清晰:
@Service
public class OrderService {
@Transactional // 通过AOP管理事务
public void createOrder() {
// 纯粹的、只关注业务的代码
// ... (创建订单的代码)
}
}
而日志、事务等横切逻辑被抽取到独立的切面(Aspect) 中。
三、AOP 核心概念详解
概念 解释 生活化比喻(办理业务)
Aspect(切面) 横切关注点的模块化。一个切面是通知和切点的结合。 一个“VIP客户服务流程”手册,里面规定了VIP客户在办理业务前后需要做的特殊事情。
Joinpoint(连接点) 程序执行过程中一个明确的点,如方法调用、异常抛出等。Spring AOP 中,连接点总是代表方法的执行。 银行窗口中“办理业务”这个动作本身。
Pointcut(切点) 一个表达式,用于匹配哪些连接点需要被通知。切点定义了通知在“哪里”执行。 VIP客户服务手册中规定:“所有存款业务的办理过程”。这是一个匹配规则。
Advice(通知) 切面在特定的连接点上执行的动作。通知定义了“什么时候”做什么事。 手册中规定的具体动作,比如“客户来时送杯茶”,“办完后赠送小礼品”。
Target Object(目标对象) 被一个或多个切面所通知的对象。 正在办理存款业务的普通窗口工作人员。
Weaving(织入) 将切面应用到目标对象,从而创建代理对象的过程。 银行将“VIP客户服务流程”手册落实到对窗口工作人员的培训中,让工作人员具备了新的行为。
AOP Proxy(AOP 代理) AOP 框架创建的对象,它是织入的结果。在 Spring 中,代理可以是 JDK 动态代理 或 CGLIB 代理。 这位经过培训后的窗口工作人员,他现在既会办业务,也会提供VIP服务,他就是一个“增强版”的代理对象。
通知的类型(Advice):
• @Before:在目标方法执行前执行。
• @AfterReturning:在目标方法成功执行后执行。
• @AfterThrowing:在目标方法抛出异常后执行。
• @After(Finally):在目标方法执行后(无论成功与否) 执行,类似于 try-catch-finally 中的 finally。
• @Around:最强大的通知,它包围了连接点,可以在方法执行前后自定义行为,并控制是否执行目标方法。它需要显式调用 ProceedingJoinPoint.proceed()。
四、Spring AOP 的实现原理
Spring AOP 的底层是基于代理模式实现的。
-
如果目标对象实现了接口:默认使用 JDK 动态代理。
◦ 运行时动态创建一个实现了相同接口的代理类。◦ 代理类持有目标对象的引用,并在方法调用前后插入切面逻辑。
-
如果目标对象没有实现任何接口:则使用 CGLIB 字节码生成库。
◦ 通过继承目标类,生成一个子类作为代理。◦ 重写父类的方法,在方法中加入增强逻辑。
Spring 的选择策略:优先使用 JDK 动态代理,如果不行(无接口)则使用 CGLIB。可以通过配置强制使用 CGLIB。
五、一个完整的代码示例
假设我们有一个简单的计算器服务,我们需要为它的所有方法添加日志功能。
- 定义业务组件(目标对象)
// 接口
public interface CalculatorService {
int add(int a, int b);
int divide(int a, int b);
}
// 实现类
@Service
public class CalculatorServiceImpl implements CalculatorService {
@Override
public int add(int a, int b) {
System.out.println("执行 add 方法");
return a + b;
}
@Override
public int divide(int a, int b) {
System.out.println("执行 divide 方法");
return a / b;
}
}
-
定义切面(Aspect)
@Aspect // 声明这是一个切面
@Component
public class LoggingAspect {// 定义切点:匹配 CalculatorService 接口下的所有方法
@Pointcut("execution(* com.example.service.CalculatorService.*(..))")
public void calculatorMethods() {}// @Before 通知:在方法执行前执行
@Before("calculatorMethods()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[日志] 准备执行方法: " + methodName + ", 参数: " + Arrays.toString(args));
}// @AfterReturning 通知:在方法成功返回后执行
@AfterReturning(pointcut = "calculatorMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[日志] 方法执行成功: " + methodName + ", 结果: " + result);
}// @AfterThrowing 通知:在方法抛出异常后执行
@AfterThrowing(pointcut = "calculatorMethods()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[日志] 方法执行异常: " + methodName + ", 异常: " + ex.getMessage());
}// @Around 通知:最强大的通知,可以控制方法是否执行
@Around("calculatorMethods()")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
// 继续执行目标方法
Object result = pjp.proceed();
return result;
} finally {
long duration = System.currentTimeMillis() - start;
System.out.println("[性能] 方法 " + pjp.getSignature().getName() + " 执行耗时: " + duration + "ms");
}
}
} -
启用 AOP
在配置类上添加 @EnableAspectJAutoProxy 注解。
@Configuration
@EnableAspectJAutoProxy // 开启基于注解的 AOP 功能
@ComponentScan("com.example")
public class AppConfig {
} -
测试结果
当我们调用 calculatorService.add(1, 2) 时,控制台会输出:
[日志] 准备执行方法: add, 参数: [1, 2]
执行 add 方法
[日志] 方法执行成功: add, 结果: 3
[性能] 方法 add 执行耗时: 15ms
六、AOP 的应用场景总结
- 声明式事务管理(@Transactional):这是 Spring AOP 最经典的应用。
- 日志记录:统一、无侵入地记录方法入参、出参、执行时间等。
- 安全控制和权限检查(@PreAuthorize):在方法执行前进行权限验证。
- 缓存(@Cacheable):方法执行前先查缓存,执行后更新缓存。
- 错误处理和异常封装:统一将系统异常转换为用户友好的异常信息。
- 性能监控:使用 @Around 监控方法执行时间。
总结
总结来说,Spring AOP 通过动态代理技术,提供了一种优雅的解决方案,将横切关注点与核心业务逻辑分离开。它的核心在于切点(Pointcut) 定义了在哪里增强,通知(Advice) 定义了增强的时机和内容。这使得我们的代码更加高内聚、低耦合,易于维护和扩展,是 Spring 框架体系的基石之一。
二、Spring ObjectFactory 是什么
我来解释一下 Spring 框架中的 ObjectFactory。
简单来说,ObjectFactory 是一个用于延迟依赖查找或延迟创建对象的函数式接口。它的核心目的是将对目标对象的获取操作封装起来,实现延迟和按需供给。
- 核心定义与作用
• 接口定义:它是一个非常简单的接口,通常定义为 ObjectFactory
• 核心思想:延迟:它并不在容器初始化时就确定并持有目标对象,而是将“如何获取目标对象”这个行为封装起来。只有在真正调用 getObject() 方法时,才会去执行实际的查找或创建逻辑。
- 主要应用场景
ObjectFactory 最典型的应用场景是解决特定作用域下的依赖注入问题,尤其是原型Bean(prototype)注入到单例Bean(singleton) 的场景。
场景举例:
假设我们有一个单例的 OrderService,它每次处理订单时,都需要一个新的、独立的 OrderValidator(配置为原型作用域)。
如果直接通过 @Autowired 注入 OrderValidator,由于 OrderService 是单例的,在初始化时依赖注入只会发生一次,这会导致 OrderService 始终持有同一个 OrderValidator 实例,违背了我们将 OrderValidator 设为原型的初衷。
解决方案就是使用 ObjectFactory
@Service
public class OrderService { // 单例
// 不直接注入 OrderValidator,而是注入它的“工厂”
private final ObjectFactory<OrderValidator> validatorFactory;
// 通过构造器注入工厂
public OrderService(ObjectFactory<OrderValidator> validatorFactory) {
this.validatorFactory = validatorFactory;
}
public void processOrder(Order order) {
// 在需要的时候,通过工厂获取一个全新的原型Bean实例
OrderValidator validator = validatorFactory.getObject();
validator.validate(order);
// ... 其他业务逻辑
}
}
通过这种方式:
• 单例Bean(OrderService)在启动时就能正常初始化,不会因为依赖一个原型Bean而被卡住。
• 按需获取:每次调用 processOrder 方法时,通过 validatorFactory.getObject() 都能从 Spring 容器中拿到一个全新的 OrderValidator 实例,完美实现了原型作用域的效果。
- 与相关接口的对比
为了更好地理解,可以把它和几个容易混淆的接口做个比较:
接口/类 核心区别
FactoryBean FactoryBean 本身是一个特殊的 Bean,它的任务是创建另一种类型的对象。从容器中按名字查找,得到的是它 getObject() 返回的对象。而 ObjectFactory 本身不是 Bean,它是一个依赖注入的“抓手”,用于延迟获取其他已存在的Bean。
Provider
BeanFactory 这是容器的根接口,是所有 Bean 的“超级工厂”,功能庞大。而 ObjectFactory 是一个非常简单、职责单一的接口,可以看作是 BeanFactory 在特定依赖查找场景下的一个抽象和简化。
总结
面试官,总结一下,Spring 的 ObjectFactory 是一个用于实现延迟依赖查找的工具接口。它通过封装对象的获取逻辑,主要解决了将生命周期较短的Bean(如原型Bean)安全地注入到生命周期较长的Bean(如单例Bean)中所带来的作用域不匹配问题,确保了每次都能获取到符合预期作用域的新实例。
三、Spring 事务
好的,面试官。关于 Spring 事务,我将从核心概念、关键注解、传播机制、隔离级别、实现原理以及常见坑点等方面进行详细解析。
一、Spring 事务的核心思想
Spring 事务管理的核心价值在于提供一套声明式事务的编程模型。它解决了传统编程式事务(需要手动 beginTransaction(), commit(), rollback())带来的代码重复和耦合问题。
编程式事务(繁琐、耦合) vs 声明式事务(简洁、解耦)
// 编程式事务(不推荐)
public void transfer(Account from, Account to, BigDecimal amount) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 1. 扣款
accountDao.deduct(from, amount);
// 2. 存款
accountDao.deposit(to, amount);
// 手动提交
transactionManager.commit(status);
} catch (Exception e) {
// 手动回滚
transactionManager.rollback(status);
throw e;
}
}
// 声明式事务(推荐,使用@Transactional)
@Transactional
public void transfer(Account from, Account to, BigDecimal amount) {
// 纯粹的、只关注业务的代码
accountDao.deduct(from, amount);
accountDao.deposit(to, amount);
}
声明式事务的优势:业务代码干净,事务管理(开启、提交、回滚)由 Spring 框架自动完成,实现了与业务逻辑的完全解耦。
二、核心注解:@Transactional
这是使用 Spring 声明式事务最主要的注解。
- 常用位置
• 推荐加在方法上:粒度更细,可以针对不同方法配置不同的事务行为。
• 可以加在类上:表示该类的所有 public 方法都开启了事务。
- 关键属性配置
属性 说明 默认值
propagation 事务的传播行为(核心难点) Propagation.REQUIRED
isolation 事务的隔离级别 Isolation.DEFAULT(使用数据库默认)
timeout 事务超时时间(秒) -1(不超时)
readOnly 是否只读事务(优化提示) false
rollbackFor 指定哪些异常触发回滚 RuntimeException 和 Error
noRollbackFor 指定哪些异常不触发回滚
示例:
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 30,
readOnly = false,
rollbackFor = {Exception.class} // 让受检异常也回滚
)
public void businessMethod() {
// ...
}
三、事务传播机制(Propagation)【面试重点】
这是 Spring 事务最核心、最容易出问题的概念。它定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。
传播行为 值 说明
REQUIRED (默认) 0 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。
SUPPORTS 1 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式继续运行。
MANDATORY 2 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
REQUIRES_NEW 3 创建一个新事务,如果当前存在事务,则把当前事务挂起。两个事务相互独立。
NOT_SUPPORTED 4 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NEVER 5 以非事务方式运行,如果当前存在事务,则抛出异常。
NESTED 6 如果当前存在事务,则在当前事务的嵌套事务内执行;如果当前没有事务,则行为同 REQUIRED。嵌套事务是外部事务的一部分,回滚会影响到外部事务。
重要场景举例:
@Service
public class UserService {
@Autowired
private LogService logService;
@Transactional
public void createUser(User user) { // 方法A,有事务
// 操作user表...
userDao.insert(user);
try {
// 调用B方法
logService.recordLog("用户创建成功"); // 方法B
} catch (Exception e) {
// 即使记录日志失败,也不希望影响主业务
e.printStackTrace();
}
}
}
@Service
public class LogService {
// 场景1: @Transactional(propagation = Propagation.REQUIRED)
// 结果:recordLog方法会加入createUser的事务。
// 风险:如果recordLog抛出异常,会导致整个事务回滚,用户创建也会失败!
// 场景2: @Transactional(propagation = Propagation.REQUIRES_NEW)
// 结果:recordLog方法会开启一个全新的、独立的事务。
// 效果:即使recordLog失败回滚,也不会影响createUser的主事务。这是最常用的解耦方式。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordLog(String content) {
// 操作log表...
logDao.insert(content);
}
}
四、事务隔离级别(Isolation)
解决数据库并发访问时可能出现的问题。
隔离级别 脏读 不可重复读 幻读 说明
READ_UNCOMMITTED(读未提交) ❌ 可能 ❌ 可能 ❌ 可能 性能最高,但问题最多
READ_COMMITTED(读已提交) ✅ 避免 ❌ 可能 ❌ 可能 Oracle、SQL Server 默认
REPEATABLE_READ(可重复读) ✅ 避免 ✅ 避免 ❌ 可能 MySQL InnoDB 默认
SERIALIZABLE(串行化) ✅ 避免 ✅ 避免 ✅ 避免 性能最低,安全性最高
Spring 中设置:
@Transactional(isolation = Isolation.READ_COMMITTED)
五、Spring 事务的实现原理
Spring 事务的本质是 AOP + 线程绑定。
-
代理创建:当你在类或方法上使用 @Transactional 时,Spring 会为该 Bean 创建一个代理对象(JDK 动态代理或 CGLIB)。
-
拦截执行:当你调用代理对象的方法时,会先经过 AOP 的拦截器 TransactionInterceptor。
-
事务管理:在拦截器中:
◦ 方法执行前:判断是否需要开启新事务,获取数据库连接,并设置 autoCommit=false,将连接绑定到当前线程(ThreadLocal)。◦ 方法执行中:你的业务代码执行 SQL,使用的数据库连接是从当前线程绑定的 ThreadLocal 中获取的,从而保证多个 DAO 操作在同一个连接和事务中。
◦ 方法执行后:如果没有异常,则提交事务;如果有异常,并根据回滚规则决定是否回滚。最后,清理线程绑定资源。
关键点:通过 ThreadLocal 实现了连接和事务的“线程绑定”,这是保证同一线程内多个数据库操作处于同一事务的基础。
六、常见坑点与最佳实践
-
事务失效的常见场景
◦ 方法非 public:@Transactional 只能用于 public 方法。◦ 自调用问题:在同一个类中,一个非事务方法调用另一个 @Transactional 方法,事务会失效。因为自调用不走代理。
public void methodA() {
this.methodB(); // 事务失效!因为this是目标对象,不是代理对象
}
@Transactional
public void methodB() {
// ...
}解决方案:将方法B放到另一个Bean中。◦ 异常被捕获:在方法中 try-catch 了异常,但没有重新抛出,事务拦截器感知不到异常,不会回滚。
@Transactional
public void method() {
try {
// ... 可能出错的SQL
} catch (Exception e) {
e.printStackTrace(); // 异常被"吞掉",事务会提交!
// 应抛出:throw new RuntimeException(e);
}
}◦ 默认只回滚运行时异常:受检异常(如 IOException)默认不回滚,需要用 rollbackFor 属性指定。
-
选择正确的传播行为:深刻理解 REQUIRED 和 REQUIRES_NEW 的区别,是设计复杂业务事务边界的关键。
-
事务中不宜进行远程调用或耗时操作:长时间占用数据库连接会导致连接池快速耗尽,成为系统瓶颈。
总结
面试官,总结一下 Spring 事务:
• 核心价值:提供了声明式事务管理,通过 @Transactional 注解让事务控制变得简单、非侵入。
• 两大基石:传播行为 定义了事务的边界和创建方式,隔离级别 定义了事务的并发控制级别。
• 实现原理:基于 AOP 动态代理 和 ThreadLocal 线程绑定 技术。
• 使用关键:理解默认行为,避免自调用、异常捕获等常见陷阱,根据业务场景合理配置传播机制。
掌握这些要点,就能在项目中正确、高效地使用 Spring 事务。
四、Spring AOP 和 AspectJ 有什么区别?
好的,面试官。这是一个非常经典的问题,它考察的是对 Spring AOP 的定位和实现深度的理解。Spring AOP 和 AspectJ 是两种不同维度但又有关联的 AOP 实现。
一句话概括核心区别:Spring AOP 是一个“轻量级”的运行时动态代理框架,而 AspectJ 是一个“重量级”的、功能完整的 AOP 编程语言扩展。
下面我将从多个维度进行详细对比。
对比总览
特性维度 Spring AOP AspectJ
核心目标 提供简单的 AOP 实现,与 Spring IoC 容器无缝集成 提供完整、强大的 AOP 解决方案,是 Java 的 AOP 事实标准
织入时机 运行时动态代理 编译时/编译后织入(CTW)或加载时织入(LTW)
连接点支持 仅支持方法执行级别的连接点 支持所有连接点:方法执行、构造器执行、字段读写、静态初始化块、对象初始化等
性能 有运行时代理开销,但针对 Spring Bean 场景优化良好 性能更高,因为织入在编译期完成,生成的字节码与手写无异
依赖 轻量,仅依赖 Spring Core,无需特殊编译过程 重量,需要额外的编译器(ajc)或代理(LTW),依赖 AspectJ 运行时库
集成难度 非常简单,通过 @EnableAspectJAutoProxy 即可开启 相对复杂,需要配置编译器或代理
能力范围 功能相对有限,但满足大部分企业应用需求(日志、事务等) 功能极其强大,能实现更复杂的切面编程
一、能力与织入方式:最根本的区别
这是理解两者区别的钥匙。
- Spring AOP:基于动态代理的“运行时织入”
• 工作原理:在 Spring IoC 容器初始化时,它会为被 @Aspect 注解的切面所匹配的 Spring Bean 创建代理对象(JDK 动态代理或 CGLIB)。当你调用 Bean 的方法时,实际上调用的是代理对象的方法,代理对象负责在目标方法执行前后插入通知(Advice)逻辑。
• 关键限制:因为它基于代理,所以它只能拦截 从容器外部调用的、由 Spring 管理的 Bean 的 public 方法。
◦ 自调用问题:在同一个 Bean 内部,一个方法调用另一个方法,不会经过代理,因此切面逻辑不会生效。
◦ 无法拦截非 Spring 管理的对象。
◦ 无法拦截私有(private)方法、构造器、字段访问等。
示例:Spring AOP 能做什么,不能做什么?
@Component
public class MyService {
public void publicMethod() {
// Spring AOP 可以拦截这个方法
privateMethod(); // 这个内部调用,AOP 无法拦截!
}
private void privateMethod() {
// Spring AOP 无法拦截私有方法
}
public void anotherMethod() {
new MyOtherObject().someMethod(); // 这个非Spring管理的对象,AOP无法拦截
}
}
- AspectJ:基于字节码操作的“编译时/加载时织入”
• 工作原理:它不依赖代理模式。它使用自己的编译器(ajc)或在类加载时通过 Java Agent,直接修改目标类的字节码,将切面逻辑“编织”进类的原始代码中。
• 关键优势:因为是在字节码层面进行修改,所以它不受代理模式的限制。它可以拦截任何连接点,就像这些逻辑是你自己写进去的一样。
◦ 可以拦截任何方法(public、private、protected)、构造器。
◦ 可以拦截对象的初始化(static initializer)、字段的赋值和读取。
◦ 没有自调用问题。
示例:AspectJ 的强大能力
// AspectJ 可以定义这样的切点,Spring AOP 完全做不到
@Aspect
public class PowerfulAspect {
// 拦截设置某个字段的值
@Before("set(* com.example.MyObject.sensitiveData)")
public void beforeFieldSet() {
System.out.println("有人正在设置敏感数据字段!");
}
// 拦截对象的构造
@Before("call(com.example.MyObject.new(..))")
public void beforeConstructor() {
System.out.println("正在创建一个 MyObject 对象");
}
}
二、性能对比
• Spring AOP:由于是运行时动态代理,每次方法调用都会有一层代理的间接开销。但对于大多数企业级应用(方法调用不是纳秒级性能瓶颈),这个开销是可以接受的。
• AspectJ:由于切面逻辑在编译期就被织入,生成的字节码与手写代码效率几乎一样,没有运行时代理开销,性能更高。在对性能有极致要求的场景下(如性能分析工具、底层框架),AspectJ 是唯一选择。
三、依赖与易用性
• Spring AOP:极其简单。如果你已经在用 Spring 框架,只需要添加 @EnableAspectJAutoProxy,然后定义 @Aspect 组件即可。它完全是 Spring 生态的一部分。
• AspectJ:需要引入额外的依赖(如 org.aspectj:aspectjrt 和 org.aspectj:aspectjweaver),并且可能需要使用 AspectJ 编译器(ajc)来编译项目,或者配置 Java Agent 以实现加载时织入(LTW)。集成步骤更复杂。
四、Spring AOP 和 AspectJ 的关系:并非对立,而是合作
一个常见的误解是两者是竞争关系。实际上,Spring AOP 使用了 AspectJ 的“部分”核心概念和注解,但提供了自己更简单的实现。
- API 借用:Spring AOP 使用了和 AspectJ 相同的注解,如 @Aspect, @Pointcut, @Before, @After 等。这使得开发者可以用一套熟悉的语法来定义切面。
- 切点表达式语言:Spring AOP 支持 AspectJ 的切点表达式语言(如 execution(...)),但只支持其一个子集(主要就是方法执行相关的表达式)。
- 可以集成完整的 AspectJ:在 Spring 应用中,如果你发现 Spring AOP 的能力无法满足需求(例如需要拦截字段访问),你可以选择使用完整的 AspectJ 织入,同时仍然享受 Spring 的依赖注入等功能。Spring 框架对此提供了良好的支持。
如何选择?【面试回答点睛】
面试官,总结一下如何根据场景进行选择:
• 绝大多数 Spring 应用场景,请使用 Spring AOP。
◦ 场景:你需要处理的事务管理(@Transactional)、安全检查、日志记录、性能监控等,这些通常只针对 Spring Bean 的 public 方法。
◦ 理由:它足够简单、轻量,并且与 Spring 容器无缝集成,能解决 95% 以上的实际问题。
• 只有当你需要 Spring AOP 不提供的功能时,才考虑使用完整的 AspectJ。
◦ 场景:
1. 需要拦截非 Spring 管理的对象。
2. 需要拦截构造器或字段的访问。
3. 需要拦截对象内部的自我调用(自调用)。
4. 对性能有极致要求,无法接受任何代理开销。
◦ 理由:AspectJ 功能更强大,但复杂性也更高。不要“杀鸡用牛刀”。
最终结论:Spring AOP 是面向 Spring 开发者的、“够用且好用” 的 AOP 框架,而 AspectJ 是面向所有 Java 开发者的、“功能全面且强大” 的 AOP 解决方案。它们在不同的层面上解决不同复杂度的问题。
五、@Controller 和 @Component 有什么区别
好的,面试官。这是一个考察对 Spring 注解层次和 MVC 架构理解的问题。
一句话概括核心区别:@Controller 是一种特殊的 @Component,它专门用于标记 Web 层的组件,赋予了其处理 HTTP 请求的特殊能力。
下面我从继承关系、功能用途、以及 Spring MVC 的工作机制来详细解释。
一、继承关系:@Controller 是 @Component 的“特化”
从源码层面看,@Controller 本身就被 @Component 所标注,这意味着它们本质上是同一家族的。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 看这里!@Controller 本身就是一个 @Component
public @interface Controller {
String value() default "";
}
因此,它们的继承关系是:
@Controller -> @Service -> @Repository -> @Component
• @Component:是最基础的泛型注解,它仅仅告诉 Spring:“请把这是一个类作为一个 Bean 管理起来”。
• @Controller, @Service, @Repository:都是 @Component 的特化(Specialization)。它们除了具备 @Component 的“Bean 管理”功能外,还承载了额外的、特定于其应用分层的语义和可能的技术支持。
二、功能与用途:核心区别所在
这是理解它们差异的关键。
特性 @Component @Controller
核心角色 通用的、无特定语义的 Bean 标记 MVC 模式中的控制器(Controller)
主要用途 标记任何需要被 Spring IoC 容器管理的类,尤其是那些不属于典型 Web、Service、DAO 分层的组件(如工具类、配置类、中间件客户端等)。 专门用于标记 Web 控制层的类,它的职责是接收、解析 HTTP 请求,并返回 HTTP 响应。
特殊能力 无。它被扫描后,只是一个普通的 Bean。 核心能力:与 @RequestMapping 等注解结合,定义请求映射。 当一个类被 @Controller 标记后,Spring MVC 的 DispatcherServlet 就能识别它,并将 HTTP 请求路由到其内部的处理方法上。
语义价值 低。仅表示“这是一个 Spring Bean”。 高。明确指示了这个类在架构中扮演控制器的角色,代码可读性更强。
代码示例对比:
// 使用 @Component:一个普通的工具类Bean,无法处理Web请求
@Component
public class EmailValidator {
public boolean isValid(String email) {
// 验证邮箱的逻辑
return true;
}
}
// 使用 @Controller:一个Web控制器
@Controller
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
// 这个方法的特殊能力来自于 @Controller 的身份
@GetMapping("/{id}")
public String getUserProfile(@PathVariable Long id, Model model) {
User user = userService.findById(id);
model.addAttribute("user", user);
return "user-profile"; // 返回视图名
}
}
关键点:你可以把 @Component 注解在 UserController 上,Spring 依然会把它当成一个 Bean 来管理。但是,DispatcherServlet 在扫描处理器时,默认只认 @Controller 注解的类(实际上是查找 @Controller 和 @RequestMapping 的组合)。所以,如果你错误地用 @Component 替换了 @Controller,你的 Web 请求将无法被正确路由,导致 404 错误。
三、Spring MVC 工作机制中的角色
为了更深入地理解,我们需要看 Spring MVC 如何处理一个请求:
- 请求到达:HTTP 请求到达 DispatcherServlet(前端控制器)。
- 查找处理器:DispatcherServlet 查询 HandlerMapping(处理器映射器),问:“这个 URL 应该由哪个方法来处理?”
- HandlerMapping 的工作:HandlerMapping 会扫描所有被 @Controller(或 @RestController)注解的 Bean,分析其上的 @RequestMapping 注解,建立 URL 到具体方法的映射关系。
- 路由到方法:找到对应的 @Controller Bean 中的方法并执行。
结论:@Controller 注解是 Bean 进入 Spring MVC 请求处理流水线的“入场券”。而一个仅被 @Component 标记的类,HandlerMapping 会直接忽略它。
四、其他特化注解:@Service 和 @Repository
为了形成完整认知,这里也简单对比一下:
• @Service:
◦ 语义:标记业务逻辑层(Service 层) 的组件。
◦ 功能:与 @Component 完全相同。使用它主要是为了代码的可读性和清晰的架构分层。
• @Repository:
◦ 语义:标记数据访问层(DAO 层) 的组件。
◦ 特殊能力:它是所有特化注解中唯一有额外技术价值的。它会将平台特定的异常(如 JDBC 的 SQLException、JPA 的 PersistenceException)统一转换为 Spring 的 DataAccessException 层次结构中的异常。这是未经检查的异常,使得你在 Service 层可以进行一致的异常处理。
@Repository // 使用它,异常会被自动转换
public class JpaUserRepository implements UserRepository {
@PersistenceContext
private EntityManager em;
@Override
public User findById(Long id) {
return em.find(User.class, id); // 如果出错,抛出的可能是PersistenceException
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUser(Long id) {
// 这里我们只需要处理Spring的DataAccessException,无需关心底层是JPA还是JDBC
return userRepository.findById(id);
}
}
总结
面试官,我来总结一下:
-
从属关系:@Controller 是 @Component 的特化,类似于 @Service 和 @Repository。
-
核心区别:
◦ @Component 是通用注解,用于将任意类声明为 Spring Bean。◦ @Controller 是专用注解,除了是 Bean,更重要的是它赋予了类处理 HTTP 请求的能力,是 Spring MVC 控制器的标识。
-
设计哲学:这种设计体现了关注点分离和语义化编程。使用 @Controller 能让开发者和管理框架(如 DispatcherServlet)一眼就看出这个类的架构角色,提高了代码的可读性和可维护性。
因此,在开发 Web 层组件时,我们应该始终使用 @Controller 而不是 @Component,这不仅是功能上的必需,也是遵循最佳实践的表现。
六、Spring 中的自动装配有哪些限制
好的,面试官。这是一个考察对 Spring IoC 容器核心机制理解深度的问题。自动装配(Auto-wiring)非常强大,但它并非万能,理解其限制对于编写健壮、可维护的 Spring 应用至关重要。
Spring 中的自动装配主要有以下限制和需要注意的地方:
一、根本性限制:歧义性(Ambiguity)
这是自动装配最核心、最常见的限制。当 Spring 容器中存在多个同一类型的 Bean 时,它无法智能地判断应该注入哪一个,从而导致失败。
- 由 @Autowired 引起的歧义(按类型匹配)
// 假设有两个数据源实现
@Component
public class MySQLDataSource implements DataSource
@Component
public class PostgreSQLDataSource implements DataSource { ... }
@Service
public class MyService {
@Autowired // 报错!NoUniqueBeanDefinitionException
private DataSource dataSource; // 容器中有两个DataSource类型的Bean
}
解决方案:
• 使用 @Qualifier:通过 Bean 的名称来指定。
@Service
public class MyService {
@Autowired
@Qualifier("mySQLDataSource") // 指定Bean的名称
private DataSource dataSource;
}
• 使用 @Primary:将其中一个 Bean 标记为首选。
@Component
@Primary // 当有多个候选时,优先选择这个
public class MySQLDataSource implements DataSource { ... }
• 更精确的依赖声明:将字段/参数类型声明为具体的实现类,而不是接口。
@Autowired
private MySQLDataSource dataSource; // 明确指定要MySQL的实现
- 由 @Resource 引起的歧义(按名称匹配)
@Resource 默认按名称匹配,如果找不到指定名称的 Bean,会回退到按类型匹配。此时如果找到多个同类型 Bean,同样会失败。
@Service
public class MyService {
@Resource // 1. 先按名称找 ‘dataSource’ Bean。2. 如果找不到,按类型找,发现两个DataSource,报错!
private DataSource dataSource;
}
二、设计层面的限制
-
基本数据类型和字符串无法自动装配
自动装配是针对 Spring 容器管理的 Bean 的。像 int, String, boolean 这些基本类型和字面量,它们不是 Bean,无法通过 @Autowired 从容器中获取。
@Component
public class MyService {
@Autowired // 报错!No qualifying bean of type 'java.lang.String'
private String serverUrl;private int timeout; // 同样无法自动装配
}
解决方案:使用 @Value 注解从配置文件(如 application.properties)中注入。
@Value("${app.server.url}") // 从配置文件中读取
private String serverUrl;
@Value("${app.timeout:5000}") // 带默认值
private int timeout;
- 循环依赖(Circular Dependencies)
这是另一个经典问题。当两个或多个 Bean 相互依赖时,如果都使用构造器注入,自动装配将无法解决,导致容器启动失败。
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired // 构造器注入
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired // 构造器注入
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
// 启动报错:Requested bean is currently in creation: Is there an unresolvable circular reference?
解决方案与限制:
• 使用 Setter/Field 注入:Spring 对单例 Bean 的 setter/field 注入方式的循环依赖有三级缓存机制来解决。但这被视为一种设计上的妥协。
• 最佳实践:重构代码,打破循环依赖。循环依赖通常是糟糕设计的信号。可以考虑:
◦ 引入第三个类,包含共享逻辑。
◦ 使用 @Lazy 注解延迟加载其中一个依赖。
◦ 将相互依赖的部分抽取到接口中。
三、配置与上下文限制
- 缺少必要的 Bean 定义
如果容器中根本没有你所要注入类型的 Bean 定义,自动装配会失败。
@Autowired
private SomeService someService; // 如果SomeService接口没有任何实现类被定义为Bean
// 报错:No qualifying bean of type 'SomeService'
解决方案:确保需要的类已经被正确地定义为 Spring Bean(使用 @Component, @Service, @Bean 等)。
-
作用域(Scope)不匹配
特别是当较短生命周期的 Bean 依赖较长生命周期的 Bean 时,需要特别注意。例如,一个原型(prototype)Bean 依赖一个单例(singleton)Bean 是安全的,但反过来则可能有问题,因为单例 Bean 只在初始化时注入一次原型 Bean,之后将始终持有同一个实例。 -
在非 Spring 管理的类中使用 @Autowired
@Autowired 注解只有在 Spring 管理的 Bean 中才生效。如果你在一个自己通过 new 关键字创建的对象中使用 @Autowired,注入会失败,因为 Spring 容器根本不知道这个对象的存在。
// 错误示例
public class MyUtility {
@Autowired // 这个注解完全无效!
private SomeService service; // service 将为 nullpublic void doSomething() {
new MyUtility(); // 自己new的对象,Spring不管
}
}
四、最佳实践与总结
面试官,总结一下,自动装配的限制提醒我们在使用时需要遵循以下最佳实践:
- 优先使用构造器注入:对于必需依赖,使用构造器注入,这可以避免循环依赖,并保证 Bean 在构建完成后就处于完全初始化的状态(Immutable)。
- 接口指向实现:尽量依赖抽象(接口),而不是具体实现,并结合 @Qualifier 或 @Primary 来消除歧义。
- 避免循环依赖:将循环依赖视为设计缺陷,并优先通过重构代码来解决。
- 明确依赖:当自动配置变得复杂和不清晰时,显式地使用 Java Config(@Bean)进行配置是更可取的方式,它提供了更强的类型安全和可读性。
核心结论:自动装配是一个强大的工具,但它要求应用程序有一个良好的、避免歧义的设计。理解这些限制,能帮助我们更好地使用 Spring,并写出更健壮的代码。
七、@Autowired 和 @Resource 的区别
好的,面试官。@Autowired 和 @Resource 都是用于依赖注入的注解,但它们在来源、默认行为和处理歧义性的方式上有显著区别。
一、核心区别总览
特性 @Autowired @Resource
来源 Spring 框架专属 JSR-250 标准注解(Java 规范)
默认装配方式 按类型(byType) 按名称(byName)
required 属性 有(@Autowired(required=false)) 无
处理多个同类型 Bean 需要与 @Qualifier 配合使用 可通过 name 属性指定,或按名称回退
二、装配机制详解(最核心的区别)
- @Autowired:默认按类型匹配
@Autowired 首先会根据字段/参数的类型去容器里查找匹配的 Bean。
工作流程:
- 在容器中查找与字段类型匹配的 Bean。
- 如果找到唯一一个,直接注入。
- 如果找到多个(歧义),报错 NoUniqueBeanDefinitionException。
- 如果没找到,报错 NoSuchBeanDefinitionException(除非设置了 required=false)。
示例:按类型匹配
// 接口
public interface UserService {
void serve();
}
// 实现类A
@Service("userServiceA") // Bean名称为 "userServiceA"
public class UserServiceA implements UserService { ... }
// 实现类B
@Service("userServiceB") // Bean名称为 "userServiceB"
public class UserServiceB implements UserService { ... }
// 使用 @Autowired
@Component
public class ClientA {
@Autowired // 报错!找到两个UserService类型的Bean,按类型匹配出现歧义。
private UserService userService; // NoUniqueBeanDefinitionException
}
解决 @Autowired 歧义:使用 @Qualifier
@Component
public class ClientA {
@Autowired
@Qualifier("userServiceA") // 明确指定要注入名为 "userServiceA" 的Bean
private UserService userService; // 成功注入 UserServiceA 的实例
}
- @Resource:默认按名称匹配
@Resource 首先会根据字段的名称(或 name 属性)去容器里查找匹配的 Bean。
工作流程:
- 如果指定了 name 属性,则直接按该名称查找 Bean。
- 如果未指定 name,则使用字段的名称或 Setter 方法对应的属性名作为 Bean 名称进行查找。
- 如果按名称找不到,则会回退到按类型进行查找。
- 如果按类型找到多个,报错。
示例:按名称匹配
@Component
public class ClientB {
@Resource // 1. 先按字段名 ‘userService’ 找Bean。2. 没找到名为 ‘userService’ 的Bean。3. 回退到按UserService类型找,找到两个,报错!
private UserService userService; // 歧义,报错
@Resource(name = "userServiceA") // 明确指定Bean名称,直接注入UserServiceA
private UserService myService; // 成功注入
@Resource // 按字段名 ‘userServiceA’ 查找,成功找到名为 ‘userServiceA’ 的Bean
private UserService userServiceA; // 成功注入UserServiceA
}
三、其他重要区别
- 来源与可移植性
• @Autowired:是 Spring 自家的注解。你的代码如果大量使用它,就和 Spring 框架强绑定。
• @Resource:来自 javax.annotation 包(JSR-250标准)。如果你希望代码更具可移植性,未来可能切换到其他遵循 JSR-250 的 DI 容器,那么使用 @Resource 是更好的选择。但在纯粹的 Spring 生态中,这个区别影响不大。
- 对可选依赖的支持
• @Autowired:提供了 required 属性,可以标记一个依赖是否为必须的。
@Autowired(required = false) // 如果容器中没有MailService,则注入null,不会报错
private MailService mailService;
• @Resource:没有类似的 required 属性。如果找不到匹配的 Bean,它默认会抛出异常。
- 与构造器/Setter方法的配合
• @Autowired:可以用在构造器、Setter 方法、字段和普通方法上。Spring 特别推荐用在构造器上,用于注入强制依赖,这样可以实现不可变对象并避免循环依赖。
@Component
public class OrderService {
private final UserService userService;
@Autowired // 构造器注入,推荐!
public OrderService(UserService userService) {
this.userService = userService;
}
}
• @Resource:根据 JSR-250 规范,它只能用在字段和 Setter 方法上,不能用在构造器上。
四、如何选择?【面试回答点睛】
面试官,总结一下如何选择:
-
强烈建议使用 @Autowired,并与 @Qualifier 结合。
◦ 理由:这是 Spring 官方推荐的方式。按类型匹配是面向接口/抽象编程的自然体现,更符合设计原则。当出现歧义时,使用 @Qualifier 显式指定,意图非常清晰。特别是构造器注入,被 Spring 列为最佳实践。 -
在需要按名称进行注入的场景下,可以考虑使用 @Resource。
◦ 理由:如果你的注入策略就是严格按 Bean 的名称来,那么 @Resource 的语义更直接。 -
如果你特别关注代码与特定框架的解耦,希望遵循 Java EE 标准,则选择 @Resource。
◦ 理由:尽管在 Spring 项目中不常见,但这确实是 @Resource 的一个理论优势。
最终结论:在当今的 Spring 生态(特别是 Spring Boot)中,@Autowired 是事实上的标准和首选。它的功能更全面(支持构造器注入、可选依赖),并且与 Spring 的编程模型结合得更加紧密。理解两者区别的关键在于掌握它们默认装配方式(byType vs byName)的不同。

浙公网安备 33010602011771号