📌 一、Spring核心概念篇
1.1 什么是Spring?Spring有哪些核心模块?
✅ 正确回答思路:
面试官您好,我从Spring的定位和核心模块两个方面来回答:
首先说Spring是什么:
Spring是一个轻量级的Java企业级应用开发框架,最初由Rod Johnson在2002年提出。它的核心思想是IOC(控制反转)和AOP(面向切面编程),目的是为了简化企业级应用的开发,降低代码的耦合度。
Spring不是一个单一的框架,而是一个生态系统,包括Spring Framework、Spring Boot、Spring Cloud等一系列技术栈。
Spring的核心模块,我重点说几个:
1. Spring Core(核心容器) 这是Spring最基础的模块,提供了IOC容器的实现,包括:
- Spring Core: 提供框架的基本功能,包括IOC和依赖注入
- Spring Beans: 负责Bean的配置和管理,BeanFactory就在这里
- Spring Context: 在Core的基础上提供了更多企业级功能,比如国际化、事件传播、资源加载
- Spring Expression Language(SpEL): Spring表达式语言
java
// 创建IOC容器的例子
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 或者注解方式
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
2. Spring AOP 面向切面编程模块,用于实现横切关注点,比如:
- 日志记录
- 事务管理
- 权限控制
- 性能监控
java
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("方法执行前: " + joinPoint.getSignature().getName());
}
}
3. Spring Data Access(数据访问) 包括:
- Spring JDBC: 简化JDBC操作,提供JdbcTemplate
- Spring ORM: 集成Hibernate、JPA、MyBatis等ORM框架
- Spring Transaction: 声明式事务管理
4. Spring Web
- Spring Web: 基础的Web功能,文件上传、IOC容器初始化等
- Spring WebMVC: 也就是我们常说的SpringMVC,实现了MVC模式
- Spring WebFlux: 响应式Web框架(Spring 5.0+)
5. Spring Test
- 提供对JUnit、TestNG的支持
- Mock对象、Spring容器的测试支持
实际项目经验:
在我的项目中,我们主要用到了:
- Spring Core + Spring Context: IOC容器管理所有Bean
- Spring AOP: 实现统一的日志记录、权限校验
- Spring Transaction: 声明式事务,用@Transactional注解
- Spring MVC: 构建RESTful API
- Spring Data JPA: 简化数据库操作
现在我们都用Spring Boot,它整合了Spring的这些模块,提供了自动配置,开发效率大大提升。
💡 加分项: "Spring的设计理念非常优秀,比如面向接口编程、依赖注入、AOP等,这些思想不仅适用于Spring,在其他框架和系统设计中也很有借鉴意义。"
1.2 什么是IOC(控制反转)?什么是DI(依赖注入)?两者有什么区别?
✅ 正确回答思路:
这是Spring面试的必考题,我从三个角度来回答:
一、传统方式 vs IOC方式
传统方式(没有IOC):
java
public class UserService {
// 直接new对象,强耦合
private UserDao userDao = new UserDaoImpl();
public User getUser(Long id) {
return userDao.findById(id);
}
}
问题:
- UserService和UserDaoImpl强耦合,如果要换一个UserDao的实现,必须修改UserService的代码
- UserDao对象的创建、生命周期都由UserService控制,不灵活
- 测试困难,无法mock UserDao
IOC方式:
java
@Service
public class UserService {
// 不再自己创建,由Spring注入
@Autowired
private UserDao userDao;
public User getUser(Long id) {
return userDao.findById(id);
}
}
好处:
- 解耦: UserService不需要知道UserDao的具体实现是谁
- 灵活: 想换实现?在配置里改就行,不用动代码
- 可测试: 可以轻松mock UserDao进行单元测试
二、什么是IOC(控制反转)?
IOC(Inversion of Control) 翻译过来就是"控制反转",它是一种设计思想,不是具体的技术。
"控制"指的是什么? 对象的创建、管理、销毁的控制权。
"反转"反转的是什么? 传统方式下,对象的控制权在程序员手里(我们用new来创建)。使用IOC后,对象的控制权转移给了IOC容器(Spring容器),由容器来创建和管理对象。
打个比方:
- 传统方式: 你饿了,自己做饭。从买菜、洗菜、炒菜到吃,全部自己控制。
- IOC方式: 你饿了,去饭店吃饭。你只需要告诉服务员"我要一份宫保鸡丁",至于怎么做、用什么锅、什么调料,你不用管,饭店(IOC容器)给你做好端上来。
三、什么是DI(依赖注入)?
DI(Dependency Injection) 翻译过来是"依赖注入",它是IOC的具体实现方式。
什么是依赖? 上面的例子中,UserService需要用到UserDao,我们就说UserService依赖UserDao。
什么是注入? 不是由UserService自己创建UserDao,而是由IOC容器创建好UserDao,然后"注入"给UserService。
三种注入方式:
1. 构造器注入(推荐!)
java
@Service
public class UserService {
private final UserDao userDao;
// Spring会自动调用这个构造器,注入UserDao
@Autowired // Spring 4.3+可以省略@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
优点:
- 依赖不可变(final)
- 依赖不为null(必须通过构造器注入,否则对象创建不了)
- 依赖完全初始化后才能使用
2. Setter注入
java
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
缺点:
- 可能注入失败,导致userDao为null
- 依赖可能被修改
3. 字段注入(最常见但不推荐!)
java
@Service
public class UserService {
@Autowired
private UserDao userDao;
}
缺点:
- 无法用final修饰
- 违背了封装性(直接操作字段)
- 测试不方便(无法通过构造器注入mock对象)
为什么字段注入最常见? 因为代码最简洁,很多人图省事。但IDEA会有警告,建议改成构造器注入。
四、IOC和DI的区别
其实它们是同一个概念的不同角度:
- IOC : 是一种设计思想,强调"控制权的转移"
- DI : 是IOC的具体实现方式,强调"依赖的注入"
打个比方:
- IOC是"我不自己做饭了,我去饭店吃"(思想)
- DI是"服务员把菜端给我"(实现方式)
Martin Fowler(软件大师)说:
"IOC这个词太泛化了,不够具体,所以我建议用DI(依赖注入)这个词,更能表达清楚这个模式做了什么。"
所以在Spring里,我们经常说IOC容器 ,但具体的实现方式叫DI。
五、实际项目经验
在我的项目中,基本上所有的Service、Dao、Component都是交给Spring容器管理的:
java
// Controller层
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
// Service层
@Service
public class UserService {
private final UserDao userDao;
private final RedisTemplate redisTemplate;
public UserService(UserDao userDao, RedisTemplate redisTemplate) {
this.userDao = userDao;
this.redisTemplate = redisTemplate;
}
}
好处:
- 代码解耦,易于维护
- 方便单元测试
- 可以灵活替换实现(比如UserDao换成缓存实现)
💡 记忆口诀:
- IOC: 控制权反转,对象不自己创建,交给Spring
- DI: 依赖注入,Spring把依赖注入给对象
- IOC是思想,DI是实现
1.3 Spring IOC容器的启动流程是什么?
✅ 正确回答思路:
这个问题比较底层,但面试中也经常问。我从宏观到细节来说明:
一、IOC容器是什么?
IOC 容器从使用者角度看"类似一个 Map",但内部实际上由多个 Map + Bean 生命周期管理 + 扩展点机制(如 BeanPostProcessor)共同组成,
并不只是一个简单的 Map。Spring通过这个Map来管理所有的Bean。
java
// 简化版的IOC容器概念
Map<String, Object> iocContainer = new HashMap<>();
iocContainer.put("userService", new UserService());
iocContainer.put("userDao", new UserDaoImpl());
二、IOC容器的启动流程(宏观)
我用一个简单的例子来说明:
java
// 启动Spring IOC容器
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 或者注解方式
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 获取Bean
UserService userService = context.getBean(UserService.class);
启动流程概览:
1. 加载配置 (读取XML或扫描注解)
2. 解析配置 (解析Bean定义)
3. 注册BeanDefinition (把Bean的元信息注册到容器)
4. 实例化Bean (通过反射创建Bean对象)
5. 属性赋值 (依赖注入)
6. 初始化Bean (调用初始化方法)
7. Bean可用 (放入IOC容器的Map)
三、详细的启动流程
阶段1: 资源定位(Resource Locating)
Spring要知道去哪里找Bean的配置信息:
java
// XML方式
ClassPathResource resource = new ClassPathResource("applicationContext.xml");
// 注解方式
// Spring会扫描@ComponentScan指定的包
@Configuration
@ComponentScan("com.example")
public class AppConfig {
}
阶段2: BeanDefinition的载入和解析
Spring读取配置文件或扫描注解,把Bean的信息封装成BeanDefinition对象。
BeanDefinition包含什么?
- Bean的类名
- Bean的作用域(singleton、prototype等)
- 构造器参数
- 属性值
- 依赖关系
- 初始化方法、销毁方法
- 是否懒加载
java
// 伪代码:解析XML
BeanDefinition beanDefinition = new RootBeanDefinition();
beanDefinition.setBeanClassName("com.example.service.UserService");
beanDefinition.setScope("singleton");
beanDefinition.setLazyInit(false);
// ... 设置其他属性
阶段3: BeanDefinition的注册
把BeanDefinition注册到BeanDefinitionRegistry(也是一个Map):
java
// 伪代码
Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
beanDefinitionMap.put("userService", beanDefinition);
重点: 此时只是注册了Bean的定义,还没有创建Bean对象!
阶段4: 实例化Bean
Spring容器会在合适的时机(立即或延迟)通过反射创建Bean对象:
java
// 伪代码:通过反射实例化
Class<?> clazz = Class.forName("com.example.service.UserService");
Object bean = clazz.getDeclaredConstructor().newInstance();
什么时候实例化?
- 单例Bean(singleton) : 在 默认情况下 会在容器启动时创建,
但如果标注了 @Lazy,则会延迟到第一次使用时才创建。 - 原型Bean(prototype): 每次getBean时才创建
阶段5: 属性赋值(依赖注入)
Spring会根据BeanDefinition中的依赖关系,注入依赖的Bean:
java
// 伪代码:属性注入
UserService userService = (UserService) bean;
UserDao userDao = getBean("userDao"); // 递归获取依赖的Bean
userService.setUserDao(userDao); // 注入依赖
阶段6: 初始化Bean
依赖注入完成后,Spring会执行一些初始化操作:
初始化流程:
1. Aware接口回调
- BeanNameAware.setBeanName()
- BeanFactoryAware.setBeanFactory()
- ApplicationContextAware.setApplicationContext()
2. BeanPostProcessor前置处理
- postProcessBeforeInitialization()
3. 初始化方法
- @PostConstruct注解的方法
- InitializingBean.afterPropertiesSet()
- init-method指定的方法
4. BeanPostProcessor后置处理
- postProcessAfterInitialization() (AOP代理就是在这里生成!)
@Component
public class User implements InitializingBean {
@PostConstruct
public void postConstruct() {
System.out.println("1. @PostConstruct");
}
@Override
public void afterPropertiesSet() {
System.out.println("2. afterPropertiesSet");
}
public void initMethod() {
System.out.println("3. init-method");
}
}
在 Bean 完成依赖注入之后,初始化方法的执行顺序为:
@PostConstruct → InitializingBean.afterPropertiesSet() → init-method
阶段7: Bean可用
初始化完成后,Bean就可以使用了,Spring会把Bean放入单例池(singletonObjects):
java
// 伪代码:放入IOC容器
Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
singletonObjects.put("userService", userService);
阶段8: 销毁Bean(容器关闭时)
1. @PreDestroy注解的方法
2. DisposableBean.destroy()
3. destroy-method指定的方法
四、三级缓存解决循环依赖(高级话题)
这是IOC容器启动流程中一个重要的细节,下一个问题会详细讲。
五、实际项目经验
在我的项目中,我们用Spring Boot,它在启动时会:
java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// 这一行代码,Spring Boot会:
// 1. 创建Spring IOC容器
// 2. 扫描@Component、@Service等注解
// 3. 注册BeanDefinition
// 4. 实例化Bean
// 5. 启动内嵌的Tomcat
SpringApplication.run(Application.class, args);
}
}
我们可以用ApplicationListener监听容器启动完成事件:
java
@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("Spring容器启动完成!");
// 可以在这里做一些初始化工作,比如预热缓存
}
}
💡 总结: IOC容器的启动流程核心就是:
从源码角度看,Spring 容器启动的核心入口是 ApplicationContext#refresh() 方法,
几乎所有 IOC 初始化逻辑都在该方法中完成。
- 定位: 找到配置信息
- 载入: 解析成BeanDefinition
- 注册: 把BeanDefinition放入注册表
- 实例化: 通过反射创建对象
- 注入: 依赖注入
- 初始化: 执行初始化方法
- 完成: Bean可用
1.4 Spring如何解决循环依赖?什么是三级缓存?
✅ 正确回答思路:
这是Spring面试的高频难题,很多人答不清楚。我用最简单的方式讲明白:
一、什么是循环依赖?
java
@Service
public class A {
@Autowired
private B b; // A依赖B
}
@Service
public class B {
@Autowired
private A a; // B依赖A
}
A依赖B,B依赖A,就形成了循环依赖。
问题:
- 创建A时,需要注入B
- 创建B时,需要注入A
- A和B谁先创建?陷入死循环!
二、Spring如何解决的?
关键 : Spring允许Bean在半成品状态就被引用!
什么是半成品状态?
- Bean对象已经创建(new出来了)
- 但是属性还没注入(依赖还没设置)
流程图:
1. 创建A对象(半成品,依赖还没注入)
2. 把A放入缓存(这是关键!)
3. 开始给A注入依赖B
4. 发现B还没创建,去创建B
5. 创建B对象(半成品)
6. 把B放入缓存
7. 开始给B注入依赖A
8. 发现A在缓存里!直接拿来用(虽然A还是半成品)
9. B注入A完成,B变成成品
10. 回到第3步,A注入B完成,A变成成品
三、什么是三级缓存?
Spring用了三个Map来存储Bean的不同状态:
java
/** 一级缓存:单例池,存放完全初始化好的Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 二级缓存:用于存放"提前暴露"的 Bean,其中的对象可能是原始对象,也可能已经是 AOP 代理对象 */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
/** 三级缓存:单例工厂,存放Bean工厂对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
为什么需要三级缓存?
如果只有两级缓存:
- 一级:存放完整的Bean
- 二级:存放半成品的Bean
问题 : 如果Bean需要被AOP代理,那存入二级缓存的应该是原始对象还是代理对象?
答案: 应该是代理对象!因为最终注入的应该是代理对象。
但什么时候创建代理对象呢?
- 正常情况下,AOP代理是在Bean初始化完成后才创建的
- 但如果有循环依赖,可能需要提前创建代理对象
三级缓存的作用:
- 三级缓存存的不是Bean对象,而是一个ObjectFactory(工厂)
- 当需要从三级缓存拿Bean时,调用工厂的getObject()方法
- 这时候可以判断:需要AOP代理吗?需要就返回代理对象,不需要就返回原始对象
java
// 伪代码:三级缓存存的是工厂
singletonFactories.put(beanName, () -> {
// 如果需要AOP,返回代理对象
if (needProxy(bean)) {
return createProxy(bean);
}
// 不需要AOP,返回原始对象
return bean;
});
关键难点解释:为什么要在三级缓存里创建代理?
- 正常生命周期 : AOP 代理通常是在 Bean 初始化后(
BeanPostProcessor的postProcessAfterInitialization)创建的。 - 循环依赖困境 : 当 B 注入 A 时,A 还没初始化完。如果 A 需要被代理(例如有事务),B 必须拿到 A 的代理对象,而不是 A 的原始对象。
- 解决方案 : 三级缓存的工厂 (
getEarlyBeanReference) 允许我们在 A 初始化完成前,提前创建代理对象 给 B 使用。Spring 会记录"A 已经提前代理了",在后续的初始化步骤中就不会重复创建代理,保证单例。
四、完整的获取Bean流程
java
// 简化版源码
protected Object getSingleton(String beanName) {
// 1. 先从一级缓存取(完整的Bean)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 2. 一级没有,从二级缓存取(半成品Bean)
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 3. 二级也没有,从三级缓存取(工厂)
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 4. 调用工厂的getObject()方法,拿到Bean(可能是原始对象,也可能是代理对象)
singletonObject = singletonFactory.getObject();
// 5. 放入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 6. 从三级缓存删除
this.singletonFactories.remove(beanName);
}
}
}
return singletonObject;
}
五、循环依赖的完整流程
创建A:
1. 实例化A(半成品)
2. A放入三级缓存: singletonFactories.put("A", factory)
3. 给A注入属性B
4. 发现B还没创建,去创建B
创建B:
5. 实例化B(半成品)
6. B放入三级缓存: singletonFactories.put("B", factory)
7. 给B注入属性A
8. 调用getSingleton("A"):
- 一级缓存没有
- 二级缓存没有
- 三级缓存有!调用factory.getObject()拿到A(可能是代理对象)
- 把A放入二级缓存,从三级缓存删除
9. B注入A完成
10. B初始化完成
11. B放入一级缓存,从二级、三级缓存删除
回到A:
12. A注入B完成
13. A初始化完成
14. A放入一级缓存,从二级、三级缓存删除
完成!
六、哪些循环依赖Spring无法解决?
1. 构造器注入的循环依赖
java
@Service
public class A {
private B b;
@Autowired
public A(B b) { // 构造器注入
this.b = b;
}
}
@Service
public class B {
private A a;
@Autowired
public B(A a) { // 构造器注入
this.a = a;
}
}
无法解决! 因为:
- 创建A对象需要先创建B对象
- 创建B对象需要先创建A对象
- 对象都还没创建出来,无法放入缓存
解决办法:
- 改成字段注入或Setter注入
- 或者用@Lazy延迟注入
java
@Service
public class A {
private B b;
@Autowired
public A(@Lazy B b) { // 延迟注入
this.b = b;
}
}
2. prototype作用域的循环依赖
java
@Service
@Scope("prototype")
public class A {
@Autowired
private B b;
}
@Service
@Scope("prototype")
public class B {
@Autowired
private A a;
}
无法解决! 因为:
- prototype Bean每次都是新创建的
- 不会放入缓存
- 无法利用三级缓存解决循环依赖
七、实际项目经验
在我的项目中,我们尽量避免循环依赖:
1. 检测循环依赖
Spring启动时会检测循环依赖,如果无法解决会报错:
BeanCurrentlyInCreationException: Error creating bean with name 'A':
Requested bean is currently in creation: Is there an unresolvable circular reference?
2. 重构代码消除循环依赖
java
// ❌ 循环依赖
@Service
public class OrderService {
@Autowired
private UserService userService;
}
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
// ✅ 提取公共逻辑,消除循环依赖
@Service
public class OrderService {
@Autowired
private CommonService commonService;
}
@Service
public class UserService {
@Autowired
private CommonService commonService;
}
@Service
public class CommonService {
// 公共逻辑
}
💡 总结:
- Spring用三级缓存解决单例Bean的循环依赖
- 一级缓存:完整的Bean
- 二级缓存:半成品的Bean
- 三级缓存:Bean工厂(为了支持AOP代理)
- 构造器注入和prototype作用域无法解决循环依赖
💡 面试技巧 : 这个问题可以答得很深,但不要一上来就讲源码,先讲清楚为什么需要三级缓存 ,再讲具体怎么解决的。
📌 二、AOP篇
2.1 什么是AOP?AOP的应用场景有哪些?
✅ 正确回答思路:
AOP是Spring的另一个核心,我从概念、原理、应用三个方面来说:
一、什么是AOP?
AOP(Aspect-Oriented Programming) 翻译过来是"面向切面编程",它是对OOP(面向对象编程)的一种补充。
OOP的问题:
假设我们有很多Service:
java
@Service
public class UserService {
public void addUser() {
System.out.println("开始事务");
System.out.println("记录日志:addUser");
// 业务逻辑
System.out.println("提交事务");
}
public void deleteUser() {
System.out.println("开始事务");
System.out.println("记录日志:deleteUser");
// 业务逻辑
System.out.println("提交事务");
}
}
@Service
public class OrderService {
public void createOrder() {
System.out.println("开始事务");
System.out.println("记录日志:createOrder");
// 业务逻辑
System.out.println("提交事务");
}
}
问题:
- 事务管理、日志记录这些代码,在每个方法里都要写一遍
- 代码重复,难以维护
- 业务逻辑和系统服务(事务、日志)耦合在一起
AOP的解决方案:
把这些横切关注点(Cross-cutting Concerns)抽取出来,单独定义,然后在需要的地方"织入"进去。
java
// 业务代码变得干净
@Service
public class UserService {
public void addUser() {
// 只关注业务逻辑
System.out.println("添加用户");
}
}
// 事务、日志等功能用AOP实现
@Aspect
@Component
public class TransactionAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("开始事务");
Object result = pjp.proceed(); // 执行目标方法
System.out.println("提交事务");
return result;
}
}
打个比方:
- OOP: 从上到下切蛋糕(纵向),每一层是一个对象
- AOP: 从左到右切蛋糕(横向),横着切一刀,给所有层都加上奶油(统一的功能)
二、AOP的核心概念
1. 切面(Aspect) 横切关注点的模块化,比如事务管理、日志记录就是一个切面。
java
@Aspect
@Component
public class LogAspect {
// ...
}
2. 连接点(Join Point) 程序执行的某个点,比如方法调用、方法执行、异常抛出等。 在Spring AOP中,连接点总是方法的执行。
3. 切点(Pointcut) 匹配连接点的表达式,决定在哪些方法上应用切面。
java
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
常用切点表达式:
java
// 匹配UserService的所有方法
execution(* com.example.service.UserService.*(..))
// 匹配service包及子包的所有方法
execution(* com.example.service..*.*(..))
// 匹配返回值为User类型的所有方法
execution(com.example.entity.User *(..))
// 匹配所有public方法
execution(public * *(..))
// 匹配所有save开头的方法
execution(* save*(..))
4. 通知(Advice) 在切点处执行的动作,分为5种类型:
java
@Aspect
@Component
public class LogAspect {
// 1. 前置通知:方法执行前
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
System.out.println("方法执行前: " + joinPoint.getSignature().getName());
}
// 2. 后置通知:方法执行后(无论成功还是异常)
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("方法执行后: " + joinPoint.getSignature().getName());
}
// 3. 返回通知:方法正常返回后
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("方法返回值: " + result);
}
// 4. 异常通知:方法抛出异常后
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("方法抛出异常: " + ex.getMessage());
}
// 5. 环绕通知:最强大,可以控制方法是否执行
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("方法执行前");
Object result = pjp.proceed(); // 执行目标方法
System.out.println("方法执行后");
return result;
}
}
执行顺序 (Spring 5.3+ 版本):
AOP 的执行顺序可以理解为 try--catch--finally 结构:
- @Around 包裹整个方法调用
- @Before 在目标方法前执行
- 目标方法执行
- @AfterReturning / @AfterThrowing 处理结果或异常
- @After 相当于 finally,一定会执行
5. 目标对象(Target Object) 被一个或多个切面通知的对象,比如UserService。
6. 织入(Weaving) 把切面应用到目标对象的过程。 Spring AOP是运行时织入,通过动态代理实现。
三、AOP的应用场景
1. 日志记录
java
@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("@annotation(com.example.annotation.Log)")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
// 方法名
String methodName = pjp.getSignature().getName();
// 参数
Object[] args = pjp.getArgs();
log.info("方法: {}, 参数: {}", methodName, Arrays.toString(args));
long startTime = System.currentTimeMillis();
Object result = pjp.proceed();
long endTime = System.currentTimeMillis();
log.info("方法: {}, 耗时: {}ms, 返回值: {}", methodName, endTime - startTime, result);
return result;
}
}
// 使用
@Service
public class UserService {
@Log // 自定义注解
public User getUser(Long id) {
// 业务逻辑
}
}
2. 事务管理
java
// Spring的@Transactional就是用AOP实现的
@Service
public class UserService {
@Transactional
public void addUser(User user) {
// Spring AOP会自动开启事务、提交或回滚
}
}
3. 权限校验
java
@Aspect
@Component
public class PermissionAspect {
@Before("@annotation(requirePermission)")
public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
String permission = requirePermission.value();
// 从ThreadLocal或Session获取当前用户
User currentUser = UserContext.getCurrentUser();
if (!currentUser.hasPermission(permission)) {
throw new PermissionDeniedException("没有权限: " + permission);
}
}
}
// 使用
@Service
public class UserService {
@RequirePermission("user:delete")
public void deleteUser(Long id) {
// 只有有权限的用户才能执行
}
}
4. 性能监控
java
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = pjp.proceed();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
if (duration > 1000) {
// 超过1秒,发送告警
log.warn("慢方法: {}, 耗时: {}ms", pjp.getSignature(), duration);
}
return result;
}
}
5. 缓存
java
@Aspect
@Component
public class CacheAspect {
@Autowired
private RedisTemplate redisTemplate;
@Around("@annotation(cacheable)")
public Object cache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
String key = cacheable.key();
// 先查缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached;
}
// 缓存未命中,执行方法
Object result = pjp.proceed();
// 放入缓存
redisTemplate.opsForValue().set(key, result, cacheable.timeout(), TimeUnit.SECONDS);
return result;
}
}
6. 异常处理
java
@Aspect
@Component
public class ExceptionAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void handleException(JoinPoint joinPoint, Exception ex) {
// 记录异常日志
log.error("方法: {} 抛出异常", joinPoint.getSignature(), ex);
// 发送告警
alertService.sendAlert("方法异常: " + joinPoint.getSignature() + ", " + ex.getMessage());
}
}
四、实际项目经验
在我的项目中,我们大量使用了AOP:
1. 统一日志记录
java
@Aspect
@Component
@Slf4j
public class WebLogAspect {
@Pointcut("execution(* com.example.controller..*.*(..))")
public void webLog() {}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录请求信息
log.info("URL: {}", request.getRequestURL());
log.info("HTTP Method: {}", request.getMethod());
log.info("IP: {}", request.getRemoteAddr());
log.info("Class Method: {}", pjp.getSignature());
log.info("Request Args: {}", Arrays.toString(pjp.getArgs()));
long startTime = System.currentTimeMillis();
Object result = pjp.proceed();
long endTime = System.currentTimeMillis();
log.info("Response: {}", result);
log.info("Time Cost: {}ms", endTime - startTime);
return result;
}
}
2. 接口幂等性
java
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private RedisTemplate redisTemplate;
@Around("@annotation(idempotent)")
public Object idempotent(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
// 从请求头获取幂等key
String idempotentKey = getIdempotentKey();
// 尝试加锁
Boolean success = redisTemplate.opsForValue()
.setIfAbsent("idempotent:" + idempotentKey, "1", 10, TimeUnit.SECONDS);
if (!success) {
throw new BusinessException("请勿重复提交");
}
try {
return pjp.proceed();
} finally {
// 释放锁
redisTemplate.delete("idempotent:" + idempotentKey);
}
}
}
💡 总结:
- AOP是面向切面编程,用于处理横切关注点
- 核心概念:切面、切点、通知、连接点、织入
- 常用场景:日志、事务、权限、缓存、异常处理、性能监控
- Spring AOP基于动态代理实现
2.2 Spring AOP的实现原理是什么?JDK动态代理和CGLIB代理有什么区别?
✅ 正确回答思路:
这是AOP的底层原理,我从代理模式讲到具体实现:
一、代理模式
什么是代理?
打个比方:你要买房,但你不直接和卖家谈,而是找个房产中介,中介代表你去谈。
- 目标对象: 卖家(真正提供服务的对象)
- 代理对象: 中介(代理目标对象,提供额外服务)
代码示例:
java
// 目标接口
public interface UserService {
void addUser(String name);
}
// 目标对象
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
}
// 静态代理(手写代理类)
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void addUser(String name) {
System.out.println("开始事务"); // 增强功能
target.addUser(name);
System.out.println("提交事务"); // 增强功能
}
}
静态代理的问题:
- 每个目标类都要写一个代理类,代码重复
- 目标类很多时,代理类也很多,维护困难
解决方案: 动态代理!在运行时动态生成代理类。
二、JDK动态代理
原理:
- Java提供了
java.lang.reflect.Proxy类来生成代理对象 - 要求目标类必须实现接口
- 代理对象也会实现同样的接口
实现步骤:
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 1. 定义接口
public interface UserService {
void addUser(String name);
User getUser(Long id);
}
// 2. 目标类
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
@Override
public User getUser(Long id) {
System.out.println("查询用户: " + id);
return new User(id, "张三");
}
}
// 3. 实现InvocationHandler
public class MyInvocationHandler implements InvocationHandler {
private Object target; // 目标对象
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法执行前: " + method.getName());
// 调用目标对象的方法
Object result = method.invoke(target, args);
System.out.println("方法执行后: " + method.getName());
return result;
}
}
// 4. 生成代理对象
public class ProxyTest {
public static void main(String[] args) {
// 目标对象
UserService target = new UserServiceImpl();
// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 目标类的接口
new MyInvocationHandler(target) // InvocationHandler
);
// 调用代理对象的方法
proxy.addUser("李四");
User user = proxy.getUser(1L);
}
}
输出:
方法执行前: addUser
添加用户: 李四
方法执行后: addUser
方法执行前: getUser
查询用户: 1
方法执行后: getUser
JDK动态代理的特点:
- ✅ Java原生支持,不需要引入第三方库
- ✅ 代理对象和目标对象实现同样的接口,符合多态
- ❌ 目标类必须实现接口,否则无法使用
三、CGLIB动态代理
原理:
- CGLIB(Code Generation Library)是一个代码生成库
- 通过字节码技术,在运行时动态生成目标类的子类
- 不要求目标类实现接口
实现步骤:
java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 1. 目标类(没有接口!)
public class UserService {
public void addUser(String name) {
System.out.println("添加用户: " + name);
}
public User getUser(Long id) {
System.out.println("查询用户: " + id);
return new User(id, "张三");
}
}
// 2. 实现MethodInterceptor
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("方法执行前: " + method.getName());
// 调用目标对象的方法(通过super调用父类方法)
Object result = proxy.invokeSuper(obj, args);
System.out.println("方法执行后: " + method.getName());
return result;
}
}
// 3. 生成代理对象
public class CglibProxyTest {
public static void main(String[] args) {
// 创建Enhancer
Enhancer enhancer = new Enhancer();
// 设置父类(目标类)
enhancer.setSuperclass(UserService.class);
// 设置回调
enhancer.setCallback(new MyMethodInterceptor());
// 创建代理对象(实际上是目标类的子类)
UserService proxy = (UserService) enhancer.create();
// 调用代理对象的方法
proxy.addUser("李四");
User user = proxy.getUser(1L);
}
}
CGLIB动态代理的特点:
- ✅ 不要求目标类实现接口
- ✅ 代理对象是目标类的子类,可以访问目标类的public和protected方法
- ❌ 无法代理final类和final方法(子类无法继承)
- ❌ 需要引入CGLIB库(Spring已内置)
四、JDK动态代理 vs CGLIB代理
| 对比项 | JDK动态代理 | CGLIB代理 |
|---|---|---|
| 实现方式 | 基于接口,生成接口的实现类 | 基于继承,生成目标类的子类 |
| 要求 | 目标类必须实现接口 | 目标类不能是final类 |
| 性能 | 调用速度快 | 创建代理对象慢,但调用速度也很快 |
| 适用场景 | 有接口的情况 | 没有接口的情况 |
五、Spring AOP使用哪种代理?
这里的策略分为传统Spring 和Spring Boot两种情况,面试时一定要分情况讨论:
-
传统 Spring Framework (非 Boot 项目):
- 默认规则:如果目标类实现了接口,使用 JDK动态代理 ;如果没有实现接口,使用 CGLIB代理。
-
Spring Boot 2.x 及更高版本 (主流现状):
- Spring Boot 默认开启 spring.aop.proxy-target-class=true,因此即使目标类实现了接口,也会优先使用 CGLIB 代理,除非显式将该配置关闭。
- 为什么? 为了防止注入类型转换异常(例如:Service注入时用的是实现类类型而不是接口类型)。
- 如何改变? 只有显式配置
spring.aop.proxy-target-class=false,才会退回到 "有接口用JDK" 的策略。
💡 满分回答: "理论上是按接口判断,但在现代 Spring Boot 项目中,默认强制使用 CGLIB,除非手动修改配置。"
六、Spring AOP的完整流程
1. 创建代理对象
Spring在创建Bean时,会通过BeanPostProcessor后置处理器判断是否需要创建代理:
java
// 伪代码:Spring AOP的核心逻辑
public Object createProxy(Object bean) {
// 1. 找到所有适用于这个Bean的Advisor(切面)
List<Advisor> advisors = findAdvisors(bean);
if (advisors.isEmpty()) {
// 没有切面,不需要代理
return bean;
}
// 2. 选择代理方式
if (bean有接口) {
return JDK动态代理(bean, advisors);
} else {
return CGLIB代理(bean, advisors);
}
}
2. 调用代理对象的方法
当我们调用代理对象的方法时:
java
// 伪代码:方法调用流程
public Object invoke(Method method, Object[] args) {
// 1. 获取这个方法的拦截器链
List<MethodInterceptor> chain = getInterceptors(method);
// 2. 如果没有拦截器,直接调用目标方法
if (chain.isEmpty()) {
return method.invoke(target, args);
}
// 3. 执行拦截器链
return chain.get(0).invoke(
new MethodInvocation() {
public Object proceed() {
if (还有下一个拦截器) {
return chain.get(下一个).invoke(this);
} else {
// 所有拦截器都执行完了,调用目标方法
return method.invoke(target, args);
}
}
}
);
}
拦截器链的执行顺序(责任链模式):
@Around前半部分
↓
@Before
↓
@Around前半部分(如果有多个)
↓
目标方法
↓
@Around后半部分
↓
@AfterReturning
↓
@After
↓
@Around后半部分(如果有多个)
七、实际项目经验
1. 查看生成的代理对象
java
@SpringBootTest
public class AopTest {
@Autowired
private UserService userService;
@Test
public void testProxy() {
// 打印代理对象的类型
System.out.println(userService.getClass());
// 输出: com.example.service.UserService$$EnhancerBySpringCGLIB$$xxxx
// 判断是否是代理对象
System.out.println(AopUtils.isAopProxy(userService)); // true
System.out.println(AopUtils.isCglibProxy(userService)); // true或false
System.out.println(AopUtils.isJdkDynamicProxy(userService)); // true或false
}
}
2. 代理失效的问题
这是一个常见的坑!
java
@Service
public class UserService {
@Transactional
public void method1() {
System.out.println("method1有事务");
this.method2(); // ❌ 这样调用,method2的事务不生效!
}
@Transactional
public void method2() {
System.out.println("method2有事务");
}
}
为什么method2的事务不生效?
因为this.method2()是通过this调用的,this是目标对象,不是代理对象!事务是通过AOP代理实现的,所以不生效。
解决办法:
java
@Service
public class UserService {
@Autowired
private ApplicationContext context;
@Transactional
public void method1() {
System.out.println("method1有事务");
// ✅ 通过Spring容器获取代理对象
UserService proxy = context.getBean(UserService.class);
proxy.method2(); // 这样调用才生效
}
@Transactional
public void method2() {
System.out.println("method2有事务");
}
}
// 或者用AopContext(需要配置exposeProxy=true)
@EnableAspectJAutoProxy(exposeProxy = true)
@Service
public class UserService {
@Transactional
public void method1() {
UserService proxy = (UserService) AopContext.currentProxy();
proxy.method2();
}
@Transactional
public void method2() {
// ...
}
}
💡 总结:
-
Spring AOP基于动态代理实现
-
JDK动态代理: 基于接口,生成接口实现类
-
CGLIB代理: 基于继承,生成目标类的子类
-
在传统 Spring Framework 中:
- 有接口默认使用 JDK 动态代理
- 无接口才使用 CGLIB
而在 Spring Boot 项目中,默认更倾向使用 CGLIB。
-
注意代理失效问题: 类内部调用不会走代理