口语八股——Spring 面试实战指南(一):核心概念篇、AOP 篇

📌 一、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);
    }
}

问题:

  1. UserService和UserDaoImpl强耦合,如果要换一个UserDao的实现,必须修改UserService的代码
  2. UserDao对象的创建、生命周期都由UserService控制,不灵活
  3. 测试困难,无法mock UserDao

IOC方式:

java 复制代码
@Service
public class UserService {
    // 不再自己创建,由Spring注入
    @Autowired
    private UserDao userDao;
    
    public User getUser(Long id) {
        return userDao.findById(id);
    }
}

好处:

  1. 解耦: UserService不需要知道UserDao的具体实现是谁
  2. 灵活: 想换实现?在配置里改就行,不用动代码
  3. 可测试: 可以轻松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;
    }
}

好处:

  1. 代码解耦,易于维护
  2. 方便单元测试
  3. 可以灵活替换实现(比如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 初始化逻辑都在该方法中完成。

  1. 定位: 找到配置信息
  2. 载入: 解析成BeanDefinition
  3. 注册: 把BeanDefinition放入注册表
  4. 实例化: 通过反射创建对象
  5. 注入: 依赖注入
  6. 初始化: 执行初始化方法
  7. 完成: 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 初始化后(BeanPostProcessorpostProcessAfterInitialization)创建的。
  • 循环依赖困境 : 当 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使用哪种代理?

这里的策略分为传统SpringSpring Boot两种情况,面试时一定要分情况讨论:

  1. 传统 Spring Framework (非 Boot 项目):

    • 默认规则:如果目标类实现了接口,使用 JDK动态代理 ;如果没有实现接口,使用 CGLIB代理
  2. 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。

  • 注意代理失效问题: 类内部调用不会走代理

相关推荐
utmhikari1 小时前
【架构艺术】治理后端稳定性的一些实战经验
java·开发语言·后端·架构·系统架构·稳定性·后端开发
文艺倾年1 小时前
【源码精讲+简历包装】LeetcodeRunner—手搓调试器轮子(20W字-上)
java·jvm·人工智能·tomcat·编辑器·guava
dfyx9992 小时前
Maven Spring框架依赖包
java·spring·maven
茶杯梦轩2 小时前
从零起步学习并发编程 || 第二章:多线程与死锁在项目中的应用示例
java·服务器·后端
日月云棠2 小时前
JAVA JDK 11 特性详解
java
sp422 小时前
Spring Task 任务调度可视化管理
后端·spring
q***76562 小时前
工作中常用springboot启动后执行的方法
java·spring boot·后端
菜鸡儿齐2 小时前
leetcode-和为k的子数组
java·算法·leetcode
时艰.2 小时前
电商促销系统知识点整理
java