当前位置: 首页 > news >正文

Java八股文——Spring「Spring 篇」

说一下你对 Spring 的理解

面试官您好,对于Spring,我的理解是,它并不仅仅是一个框架,而是一个庞大而全面的、旨在简化企业级Java开发的生态系统

它的核心设计哲学,是 “非侵入式”“一站式”。它通过提供一套完整的、模块化的解决方案,让我们开发者可以从复杂、繁琐的底层技术(如JDBC、Servlet、JTA等)中解脱出来,更专注于业务逻辑的实现。

要理解Spring,我认为关键在于理解它的两大基石:IoC(控制反转)AOP(面向切面编程)。Spring生态中的几乎所有其他功能,都是在这两大基石之上构建的。

1. Spring的基石一:IoC容器 (Inversion of Control)
  • 它解决了什么问题?
    • 在没有Spring的时代,对象之间的依赖关系是由我们自己手动管理的。比如,一个OrderService需要一个OrderDao,我们就得在OrderService的构造函数或某个方法里去new OrderDao()。这种方式导致了类与类之间的高度耦合,代码难以测试、难以维护、难以扩展。
  • Spring如何解决?—— 控制反转
    • Spring的核心是一个IoC容器(也叫Spring容器),它接管了所有对象的创建权依赖关系的管理权
    • 我们不再在代码里去new对象,而是通过XML配置或注解(如@Component, @Service),告诉Spring:“请帮我管理这个类(我们称之为Bean)”。
    • 当一个Bean(比如OrderService)需要另一个Bean(OrderDao)时,我们通过 依赖注入(DI, Dependency Injection) 的方式(比如使用@Autowired注解),告诉Spring:“请把那个OrderDao的实例注入到我这里来。”
  • 带来的价值
    • 解耦:这是最大的价值。OrderService不再关心OrderDao是如何被创建和管理的,它只关心接口和功能。这使得单元测试变得极其容易,我们可以轻松地注入一个Mock的OrderDao来进行测试。
    • Bean的生命周期管理:Spring容器负责Bean的创建、初始化、销毁等整个生命周期,让我们可以通过@PostConstruct, @PreDestroy等方式方便地进行扩展。
2. Spring的基石二:AOP (Aspect-Oriented Programming)
  • 它解决了什么问题?
    • 在我们的业务逻辑中,总是散落着大量与核心业务无关,但又必不可少的“横切”代码,比如日志记录、权限校验、性能监控、事务管理等。这些代码重复、臃肿,与业务逻辑耦合在一起,使得代码难以维护。
  • Spring如何解决?—— 面向切面编程
    • AOP允许我们将这些“横切关注点”从业务逻辑中抽离出来,定义成独立的切面(Aspect)
    • 然后,通过配置,我们可以告诉Spring:“请在某某方法(我们称之为切点,Pointcut)执行之前、之后、或者抛出异常时,动态地织入我这个切面的逻辑。”
  • 带来的价值
    • 模块化与可重用:将通用的横切逻辑(如日志)封装成一个切面,可以在系统的任何地方按需重用。
    • 业务代码更纯粹:业务开发人员可以更专注于核心业务逻辑,而无需关心那些通用的、非功能性的需求。
3. 在两大基石之上的丰富生态

基于IoC和AOP,Spring构建了一个强大的一站式解决方案生态:

  • 声明式事务管理:这是AOP最经典的应用。我们只需要在方法上加一个@Transactional注解,Spring就会通过AOP,自动地为这个方法加上事务的开启、提交、回滚逻辑,极其方便。
  • Spring MVC:一个强大而灵活的Web框架,通过IoC来管理Controller、Service等组件,实现了Web层的解耦。
  • 数据访问支持:对JDBC、MyBatis、JPA等都提供了优秀的模板化支持(如JdbcTemplate, MyBatis-Spring集成),极大地简化了数据访问层的代码。
  • Spring Boot:它在Spring的基础上,通过“约定大于配置”的理念,提供了大量的自动配置(Auto-Configuration)“起步依赖”(Starters),让我们能够以极快的速度搭建一个可独立运行的、生产级的Spring应用,彻底解决了传统Spring配置繁琐的问题。
  • Spring Cloud:在Spring Boot的基础上,提供了一整套构建分布式系统的微服务治理方案(如服务发现、配置中心、网关、熔断器等)。

总结一下,Spring通过IoCAOP这两大核心思想,从根本上解决了传统Java开发的耦合问题,并以此为基础,构建了一个从单体应用到微服务、从Web开发到数据访问的全方位、一站式的开发生态,是当之无愧的Java企业级应用开发的事实标准。


Spring IoC和AOP 介绍一下

面试官您好,IoC和AOP是Spring框架的两大基石,也是Spring能够实现“解耦”这一核心目标的左膀右臂。它们分别从不同的维度,解决了软件开发中的耦合问题。

我可以用一个 “造汽车” 的比喻来解释它们:

  • IoC(控制反转):负责的是 “零部件的组装”
  • AOP(面向切面编程):负责的是 “全车功能的统一加装”
1. IoC (Inversion of Control) —— 对象的组装者
  • 核心思想:IoC的核心是 “控制反转”
    • 在传统开发中,我们需要自己去new一个对象,并手动处理它所依赖的其他对象。就像我们自己去买来发动机、轮胎、底盘,然后亲手把它们组装成一辆车。这导致零部件之间(对象之间)高度耦合
    • 而有了IoC,我们就把这个 “组装权” 交给了 Spring容器这个“自动化装配工厂”。我们只需要告诉工厂:“我需要一个发动机,一个轮胎……”,工厂就会自动地、按照我们的要求(通过@Autowired等依赖注入方式),把它们精准地组装在一起,交付给我们一辆完整的汽车。
  • 价值:通过这种方式,Service层不再关心DAO层是如何被创建的,它们之间的耦合度被大大降低。这使得我们的代码更容易进行单元测试和替换升级。
2. AOP (Aspect-Oriented Programming) —— 功能的增强者
  • 核心思想:AOP的核心是 “横切关注点的分离”
    • 继续用造车的比喻。一辆车除了核心的行驶功能,还需要很多通用的、非核心的功能,比如 “喷漆”、“贴膜”、“安装车载导航”、“加装安全气囊” 等等。
    • 如果我们在组装每一个零部件(发动机、车门)的时候,都顺便去考虑一下喷漆、贴膜的问题,那整个生产流程就会变得极其混乱和重复。
    • AOP就是把这些通用的“加装”工序,变成了独立的“切面”(Aspect)。比如,有一个专门的“喷漆车间”、一个“贴膜车间”。
    • 当我们的汽车(目标对象)在生产线上流过时,AOP会通过动态代理技术,像一个机械臂一样,在适当的时机(比如方法执行前/后),自动地为这辆车完成喷漆、贴膜等操作。
  • 实现原理动态代理是其核心。
    • 如果目标对象实现了接口,Spring优先使用JDK动态代理,生成一个与目标对象实现了相同接口的代理对象。
    • 如果目标对象没有实现接口,Spring就会使用CGLib,通过继承的方式,生成一个目标对象的子类作为代理。
  • 价值:AOP让我们能够将日志、事务、权限校验等通用逻辑从业务代码中彻底剥离出来,使得业务代码更纯粹,通用逻辑也得到了复用。
3. 它们的结合

IoC和AOP在Spring中是天作之合,协同工作:

  1. 首先,IoC容器负责创建和组装好我们所有的业务组件(Bean),比如OrderServiceOrderDao,并处理好它们之间的依赖关系。
  2. 然后,AOP再基于这些已经由IoC容器管理好的Bean,对它们进行“二次加工”。比如,AOP会为OrderServiceplaceOrder方法动态地创建一个代理,在这个代理中织入事务管理的逻辑。

最终,我们从Spring容器中获取到的,实际上是经过AOP“增强”后的代理对象。这样,我们就得到了一个既解耦了对象依赖,又分离了横切关注点的、高内聚、低耦合的应用程序。

Spring的 AOP介绍一下

面试官您好,Spring AOP(面向切面编程)是Spring框架的两大基石之一,它提供了一种优雅地将通用功能与业务逻辑解耦的强大机制。

1. AOP解决了什么痛点?

在没有AOP的时代,我们的业务代码往往是这样的:

public class UserServiceImpl implements UserService {public void addUser(User user) {// --- 非核心业务逻辑 ---System.out.println("[日志] 开始执行addUser方法...");Transaction tx = transactionManager.beginTransaction(); // 开启事务try {// --- 核心业务逻辑 ---userDao.insert(user);// --- 非核心业务逻辑 ---tx.commit(); // 提交事务System.out.println("[日志] addUser方法执行成功。");} catch (Exception e) {// --- 非核心业务逻辑 ---tx.rollback(); // 回滚事务System.err.println("[日志] addUser方法执行异常: " + e.getMessage());throw e;}}
}

可以看到,核心的业务逻辑(userDao.insert(user))被大量重复的、与业务无关的“样板代码”(如日志、事务)所包围。这些样板代码就是 “横切关注点”,它们污染了业务代码,使其难以阅读和维护。

AOP的核心目标,就是将这些“横切关注点”抽离出去,让业务代码回归纯粹。

2. AOP的核心概念与工作流程

为了实现这种抽离,AOP定义了一套自己的语言和工作流程。我们可以用“我要给所有Service层的public方法都加上事务”这个需求来理解这些概念:

  1. 连接点 (Join Point):首先,我们程序中所有可以被“织入”额外逻辑的点,都叫连接点。在Spring AOP中,这个点就是方法的执行。所以,UserServiceImpladdUser方法,就是一个连接点。

  2. 切点 (Pointcut):我们不可能对所有方法都加事务,所以需要一个“筛选器”来精确地定位我们感兴趣的那些连接点。这个筛选器就是切点。

    • 如何定义? 我们会用一种表达式语言(比如AspectJ的切点表达式)来定义,例如:execution(* com.example.service.*.*(..)),这就精确地匹配了com.example.service包下所有类的所有public方法。
  3. 通知 (Advice):这就是我们抽离出来的、具体的横切逻辑,也就是“我们想干什么”。

    • 类型:通知有几种类型:
      • @Before:在目标方法执行执行。
      • @AfterReturning:在目标方法成功返回后执行。
      • @AfterThrowing:在目标方法抛出异常后执行。
      • @After:无论成功还是异常,最终都会执行。
      • @Around:功能最强大,它可以环绕整个目标方法的执行,我们可以在这里手动控制何时执行目标方法,并处理其返回值和异常。Spring的声明式事务就是通过@Around通知实现的。
  4. 切面 (Aspect):切面是切点和通知的结合体。它回答了“在什么地方(Pointcut),干什么事(Advice)”这个问题。我们通常用一个带有@Aspect注解的类来定义一个切面。

  5. 织入 (Weaving):最后一步,就是将“切面”应用到“目标对象”上,生成一个代理对象的过程。当外部调用这个代理对象的方法时,织入的通知逻辑就会被触发。

3. Spring AOP的实现原理

Spring AOP的织入过程是在运行时动态完成的,它主要依赖两种动态代理技术:

  • JDK动态代理:这是Java官方提供的代理方式。它要求目标对象必须实现至少一个接口。Spring会生成一个实现了相同接口的代理类,来拦截方法调用。
  • CGLIB代理:这是一个第三方的代码生成库。如果目标对象没有实现任何接口,Spring就会使用CGLIB,通过继承这个目标类的方式,来创建一个子类作为代理。

总结一下,AOP通过定义切面,将散落在各处的通用功能(如事务、日志)模块化,然后通过动态代理技术,在运行时将这些功能“织入”到我们指定的核心业务方法中,从而实现了两者的完美解耦。这也是Spring框架能够如此强大和灵活的关键所在。


IOC和AOP是通过什么机制来实现的?

面试官您好,Spring的IoC和AOP这两个核心特性,其底层实现是一套非常精巧的技术组合。

一、 IoC 的实现机制:一个“自动化对象装配工厂”

Spring IoC容器的目标是接管所有对象的创建和依赖管理权。为了实现这个目标,它主要依赖以下几种机制:

  1. Bean定义信息的加载与解析 (蓝图读取)

    • 首先,Spring容器需要知道它要管理哪些对象(Bean),以及它们之间的依赖关系。这个信息可以来自XML配置文件Java注解(如@Component, @Service)。
    • 容器在启动时,会有一个 BeanDefinitionReader 的角色,负责读取这些配置信息,并将它们解析成Spring内部统一的数据结构—— BeanDefinitionBeanDefinition就像是每个Bean的“身份证”或“配置蓝图”,里面包含了类的全名、作用域、依赖关系、是否懒加载等所有元数据。
  2. 对象的实例化 (工厂生产) —— 反射 + 工厂模式

    • 当容器需要创建一个Bean的实例时(比如在启动时或第一次被请求时),它会根据BeanDefinition中的类名,利用 Java的反射机制(Class.forName()newInstance()或构造器调用) 来动态地创建出对象实例。
    • 整个Spring IoC容器,本质上就是一个巨大的、高度可配置的工厂(BeanFactoryApplicationContext,它代替了我们自己去new对象。
  3. 依赖关系的注入 (自动化装配)

    • 对象实例化后,还只是一个“半成品”,它所依赖的其他Bean还没有被设置。
    • Spring容器会再次分析这个Bean的BeanDefinition,找出它的依赖项(比如带有@Autowired注解的字段)。
    • 然后,容器会去自己管理的一堆单例Bean中,找到匹配的依赖Bean实例。
    • 最后,还是通过反射机制(比如调用field.set()或相关的setter方法),将这个依赖Bean “注入” 到当前对象中。
    • 这个过程,就是我们常说的依赖注入(DI)

总结一下IoC的实现:它就像一个流水线工厂,先读取设计图(BeanDefinition,然后用反射技术生产出毛坯零件(实例化Bean),最后再根据设计图,自动地将这些零件组装起来(依赖注入)

二、 AOP 的实现机制:一个“动态功能增强器”

Spring AOP的目标是在不修改源代码的情况下,为我们的业务对象动态地增加一些通用功能(如事务、日志)。它完全是构建在动态代理技术之上的。

Spring会根据目标对象的特点,智能地选择两种代理方式:

  1. JDK动态代理 (基于接口)

    • 使用条件:当我们要代理的目标类,实现了至少一个接口时,Spring会优先使用这种方式。
    • 实现原理:它利用java.lang.reflect.Proxy类和InvocationHandler接口。Spring会动态地创建一个新的代理类,这个代理类和我们的目标类实现了相同的接口
    • 当外部通过接口调用代理对象的方法时,这个调用会被InvocationHandler拦截。在InvocationHandlerinvoke方法里,Spring就可以在调用真正的目标方法前后,织入我们定义的切面逻辑(Advice)。
  2. CGLIB动态代理 (基于继承)

    • 使用条件:当我们要代理的目标类,没有实现任何接口时,Spring就会切换到CGLIB。
    • 实现原理:CGLIB是一个强大的第三方代码生成库。它会在运行时,动态地创建一个被代理类的子类,并重写父类(即目标类)中所有非final的方法。
    • 在这个重写的方法里,CGLIB同样实现了拦截机制,让我们可以在调用super.method()(即真正的目标方法)前后,织入切面逻辑。

总结一下AOP的实现:无论是JDK代理还是CGLIB,Spring AOP的核心都是在运行时,为目标Bean动态地创建一个代理对象。我们从Spring容器中获取到的,实际上是这个代理对象。当我们调用这个代理对象的方法时,AOP的增强逻辑(比如事务管理)就会被触发,从而在无侵入的情况下,实现了功能的扩展。

依赖倒置,依赖注入,控制反转分别是什么?

面试官您好,这三个概念确实非常容易混淆,但它们实际上是处于不同抽象层面、但又紧密相关的。它们共同指向了软件设计的一个终极目标——解耦

我倾向于用这样一个递进关系来理解它们:

  • 依赖倒置原则 (DIP):是一种顶层的、指导性的设计原则
  • 控制反转 (IoC):是一种遵循了DIP原则的、更宏观的设计思想或模式
  • 依赖注入 (DI):是实现IoC思想的一种最主要的、具体的技术手段
1. 依赖倒置原则 (Dependency Inversion Principle, DIP) —— “我们应该依赖什么?”
  • 它是一种设计原则,是著名的SOLID五大原则之一。
  • 正如您所说,它的核心指导思想是:
    • 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
    • 抽象不应该依赖于细节,细节应该依赖于抽象。
  • 用大白话来说就是:“面向接口编程,而不是面向实现编程”。
  • 例子:一个OrderService(高层模块)不应该直接依赖一个MySqlOrderDao(低层模块、具体实现)。它应该依赖一个抽象的OrderDao接口。而MySqlOrderDao则去实现这个接口。这样,高层和低层都依赖于OrderDao这个抽象,未来如果需要更换成OracleOrderDaoOrderService的代码完全不需要改动。
2. 控制反转 (Inversion of Control, IoC) —— “谁来控制程序的流程?”
  • 它是一种设计思想/模式,是对依赖倒置原则的一种经典诠释和应用。
  • 核心在于“控制权的反转”
    • 传统控制:我们自己的代码完全控制着整个程序的流程。比如,OrderService需要OrderDao时,它会自己去 new MySqlOrderDao()控制权在我们自己手里
    • 控制反转:我们不再自己控制对象的创建和依赖关系。我们把这个控制权“反转”给了一个第三方的、专门的容器(比如Spring的IoC容器)。
  • 结果:我们不再关心对象是怎么来的,只关心怎么用。整个程序的“主动权”从我们的业务代码,转移到了框架容器手中。
3. 依赖注入 (Dependency Injection, DI) —— “如何实现控制反转?”
  • 它是一种具体的技术实现。它是实现IoC思想最主流、最重要的方式。
  • 核心思想是“被动接受”。既然我们已经把控制权交给了容器,那么我们的类如何得到它所需要的依赖对象呢?答案就是——不是自己去“取”,而是等着容器“给”
  • 容器会主动地、将一个对象所依赖的其他对象, “注入” 到这个对象中。
  • 注入的方式,正如您总结的,主要有三种:
    1. 构造器注入 (Constructor Injection)
    2. Setter方法注入 (Setter Injection)
    3. 字段注入 (Field Injection) (比如Spring的@Autowired注解)

关系总结与Spring的角色

我们可以用一个简单的流程来总结它们的关系:

  1. 我们想让我们的系统易于维护和扩展,于是我们遵循了依赖倒置原则(DIP),让我们的Service层依赖DAO的接口。
  2. 为了更好地实践DIP,我们采用了一种叫控制反转(IoC) 的设计思想,决定把所有对象的创建和管理都交给一个容器来做。
  3. 这个容器(比如Spring的IoC容器)是如何把DAO的实现类给到Service的呢?它通过依赖注入(DI) 的技术,自动地将DAO实例注入到了Service的字段或构造函数中。

所以,这三者是一个从 “指导原则”“设计思想” 再到 “具体实现” 的、层层递进的逻辑关系。而Spring框架,就是这一整套先进设计思想的完美实践者。


依赖注入了解吗?怎么实现依赖注入的?

面试官您好,我非常了解依赖注入(DI)。在我看来,DI是实现控制反转(IoC)思想最核心、最直接的技术手段

它的本质,就是由外部容器(如Spring IoC容器)来负责创建和提供一个对象所需要的依赖,而不是由对象自己去创建,从而极大地降低了对象之间的耦合度。

在Spring中,主要有以下三种实现依赖注入的方式,每一种都有其独特的优缺点和适用场景。

我们以一个OrderService依赖OrderDao的例子来看:

@Service
public class OrderService {// 依赖 OrderDaoprivate final OrderDao orderDao; // ...
}
1. 构造器注入 (Constructor Injection) —— 首选
  • 如何实现:通过在类的构造函数中声明依赖。

    @Service
    public class OrderService {private final OrderDao orderDao;// Spring 4.3之后,如果只有一个构造函数,@Autowired可以省略@Autowired public OrderService(OrderDao orderDao) {this.orderDao = orderDao;}
    }
    
  • 优点

    • 保证依赖不可变:可以将依赖字段声明为final,确保在对象创建后,其依赖关系不会被意外改变,这使得对象更加健壮。
    • 保证依赖完备性:对象在被创建出来的那一刻,其所有必需的依赖就已经被完全注入了。它不会存在一个“半成品”状态,避免了后续因依赖为null而导致的NullPointerException
    • 清晰地暴露依赖:构造函数的参数列表清晰地展示了这个类有哪些是“必需”的依赖,有助于我们识别出“职责过多”的类(如果构造函数参数太多,就该考虑重构了)。
    • 完美解决循环依赖问题(在构造器注入场景下):如果A依赖B,B又依赖A,并且它们都使用构造器注入,Spring容器在启动时会直接抛出BeanCurrentlyInCreationException,让我们在开发阶段就能立即发现并修复循环依赖问题。
  • 缺点

    • 如果依赖较多,构造函数会显得很长。但这通常是一个“信号”,提醒我们这个类的设计可能需要优化。
  • 结论:这是Spring官方和众多专家都推荐的方式,也是我在实践中的首选。它最符合“依赖倒置”和“面向不可变”的设计原则。

2. Setter方法注入 (Setter Injection)
  • 如何实现:通过为依赖字段提供一个公共的setter方法。
    @Service
    public class OrderService {private OrderDao orderDao;@Autowiredpublic void setOrderDao(OrderDao orderDao) {this.orderDao = orderDao;}
    }
    
  • 优点
    • 灵活性高:允许在对象创建后,通过调用setter方法来动态地改变或重新注入依赖。
    • 非常适合可选依赖的场景。
  • 缺点
    • 无法保证依赖的完备性。对象在创建后,其依赖字段可能是null,直到setter被调用。
    • 不能将依赖字段声明为final
3. 字段注入 (Field Injection) —— 最简洁,但也最不推荐
  • 如何实现:直接在成员变量上使用@Autowired注解。
    @Service
    public class OrderService {@Autowiredprivate OrderDao orderDao;
    }
    
  • 优点
    • 代码极其简洁,这也是很多初学者喜欢它的原因。
  • 缺点(非常致命)
    • 破坏了封装性:它绕过了构造器和setter方法,直接通过反射来设置字段值,这违反了面向对象的设计原则。
    • 依赖关系不明确:无法从外部直观地看出这个类的依赖有哪些。
    • 难以进行单元测试:在进行单元测试时,你无法通过构造函数或setter来方便地注入一个Mock对象,只能依赖于Spring容器或使用反射工具,使得测试变得非常困难。
    • 可能隐藏循环依赖问题:与构造器注入不同,字段注入可以“容忍”循环依赖(通过Spring的三级缓存解决),但这通常会将设计上的问题掩盖到运行时,而不是在编译或启动时就暴露出来。

我的选型总结

在我的项目中,我遵循以下原则:

  • 强制使用构造器注入来注入所有必需的依赖。
  • 只在可选的、非必需的依赖上,才考虑使用Setter方法注入
  • 完全避免使用字段注入,尽管它看起来最简单,但它的长期维护成本和对可测试性的破坏是得不偿失的。

如果让你设计一个Spring loC,你觉得会从哪些方面考虑这个设计?

面试官您好,这是一个非常棒的设计题。如果让我来设计一个类似Spring IoC的容器,我会把它看作一个 “Bean的自动化工厂和生命周期管理器”。我会从 “定义”、“存储”、“生产”和“扩展” 这四个大的阶段来考虑其设计。

第一阶段:Bean的“蓝图”设计与加载 (Bean Definition)

这是整个容器的基础。首先,我需要一种方式来描述和定义我要管理的Bean。

  1. 定义核心数据结构 - BeanDefinition

    • 我会设计一个BeanDefinition类,它就像每个Bean的“配置蓝图”或“身份证”。这里面会包含:
      • Bean的唯一标识(beanName)。
      • Bean的类名(className)。
      • 作用域(Scope):是单例(singleton)还是原型(prototype)。
      • 依赖关系:它依赖哪些其他的Bean。
      • 其他配置:比如是否懒加载、初始化方法、销毁方法等。
  2. 配置信息的加载

    • 我需要支持多种配置方式来加载这些BeanDefinition。我会设计一个BeanDefinitionReader的接口,并提供不同的实现:
      • XmlBeanDefinitionReader:用于解析XML配置文件。
      • AnnotationBeanDefinitionReader:用于扫描带有@Component@Service等注解的类。
      • 这样设计具有很好的可扩展性,未来可以支持如YAML、Properties等其他配置源。
第二阶段:Bean的“仓库”设计 (Bean Registry & Storage)

蓝图有了,我需要一个地方来存储这些蓝图和最终生产出来的产品(Bean实例)。

  1. Bean定义的注册表:我会设计一个类似Map<String, BeanDefinition>的结构,用于存储所有解析好的BeanDefinition
  2. 单例Bean的缓存池:对于作用域为singleton的Bean,我需要一个缓存池来存放已经创建好的实例,通常是一个Map<String, Object>,我们称之为 “单例池”。这是实现单例模式和解决循环依赖的关键。
第三阶段:Bean的“生产线”设计 (Bean Instantiation & DI)

这是IoC容器最核心的部分,即如何根据BeanDefinition生产出完整的Bean实例。这个过程就是Bean的生命周期管理

  1. 获取Bean的核心流程 (getBean)

    • 当外部请求一个Bean时(getBean("userService")),容器会先去单例池里查找。如果找到了,直接返回。
    • 如果没找到,就拿出对应的BeanDefinition,开始创建过程。
  2. 实例化 (Instantiation) - 反射

    • 根据BeanDefinition中的类名,通过Java的反射机制调用其构造函数,创建出一个“毛坯”对象。
  3. 依赖注入 (DI) - 属性填充 (Population)

    • 这是最关键的一步。容器会分析这个“毛坯”对象的BeanDefinition,找出它所有的依赖。
    • 然后,它会递归地调用getBean() 去获取这些依赖的实例。
    • 最后,还是通过反射,将这些依赖实例注入到“毛坯”对象的字段或setter方法中。
  4. 初始化 (Initialization)

    • 依赖注入完成后,对象基本完整了。此时,我会检查BeanDefinition中是否配置了初始化方法(比如@PostConstruct注解或init-method属性),并调用它,让用户可以进行一些自定义的初始化操作。
    • AOP的织入:正是在这个初始化阶段的后期,我会加入对AOP的支持。我会检查这个Bean是否需要被AOP增强。如果需要,我会使用动态代理(JDK或CGLIB)为这个原始Bean创建一个代理对象。最终放入单例池并返回给用户的,是这个代理对象
  5. 销毁 (Destruction)

    • 当容器关闭时,我会遍历所有单例Bean,检查它们是否配置了销-毁方法(如@PreDestroy),并调用它们,以释放资源。
第四阶段:容器的“扩展能力”设计

一个优秀的框架必须是可扩展的。

  1. Bean作用域的扩展:除了单例和原型,我会设计一个Scope接口,允许用户自定义新的作用域,比如Web环境下的requestsession作用域。
  2. 增强处理 - BeanPostProcessor:我会设计一个BeanPostProcessor(Bean后置处理器)的扩展点接口。它允许用户在Bean的初始化前后插入自己的逻辑,对Bean进行“二次加工”。Spring的AOP、@Autowired等许多功能,都是通过这个机制实现的。
  3. 异常处理:在整个流程的每一步,我都会建立完善的try-catch机制,当Bean创建失败或依赖找不到时,抛出明确的、带有详细上下文信息的异常(如BeanCreationException),方便用户定位问题。

通过这四大阶段的设计,我们就能构建出一个功能完备、逻辑清晰、高度可扩展的IoC容器了。


动态代理是什么?

面试官您好,动态代理是Java中一种非常强大的元编程技术,它的核心机制,正如您所说,是在程序运行时,动态地在内存中创建出一个代理对象,而不需要我们手动编写代理类的源代码

这个代理对象可以拦截对原始对象(我们称之为“目标对象”)的方法调用,并在调用前后织入我们自定义的增强逻辑。这使得我们可以在不修改任何业务代码的前提下,为其增加如日志、事务、权限校验等通用功能。Spring AOP的实现就完全依赖于它。

1. 与静态代理的区别 —— “动态”体现在哪里?

要理解动态代理,我们可以先看一下静态代理

  • 静态代理:我们需要手动地为每一个目标类都编写一个代理类,代理类和目标类实现同一个接口。这种方式的缺点是,如果接口增加一个方法,目标类和代理类都要修改。而且,每个目标类都需要一个代理类,会导致类的数量爆炸。

  • 动态代理的“动态”之处

    • 代理类是动态生成的:我们不需要手写任何代理类的.java文件。代理类的字节码是在运行时,由JVM在内存中直接生成的。
    • 一个处理器可以代理多个类:我们可以编写一个通用的调用处理器(InvocationHandler),用它来代理实现了任意接口的多个不同目标对象,具有极高的复用性。
2. Java中两种主流的动态代理实现

主要有两种方式:

  • a. JDK动态代理 (基于接口)

    • 使用前提:目标对象必须实现至少一个接口
    • 核心组件
      1. java.lang.reflect.Proxy: 这是创建代理对象的工厂类。我们主要使用它的静态方法newProxyInstance()
      2. java.lang.reflect.InvocationHandler: 这是一个接口,我们需要自己实现它。它里面只有一个invoke()方法,所有对代理对象的方法调用,最终都会被转发到这个invoke()方法里来。这里就是我们编写“增强逻辑”的地方。
  • b. CGLIB动态代理 (基于继承)

    • 使用前提:当目标对象没有实现任何接口时,就必须使用CGLIB。
    • 核心原理:CGLIB是一个强大的第三方代码生成库。它会在运行时,通过字节码技术,动态地创建一个目标对象的子类,并重写父类(即目标对象)的所有非final方法。在这个重写的子类方法中,它实现了拦截,允许我们织入增强逻辑。
3. 一个简单的JDK动态代理实现示例

我们来模拟一下如何为一个UserService接口创建一个动态代理,在调用addUser方法前后打印日志:

// 1. 目标接口
public interface UserService {void addUser(String username);
}// 2. 目标对象实现
public class UserServiceImpl implements UserService {@Overridepublic void addUser(String username) {System.out.println("核心业务:正在添加用户 - " + username);}
}// 3. 实现 InvocationHandler
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class LogInvocationHandler implements InvocationHandler {private final Object target; // 真正的目标对象public LogInvocationHandler(Object target) {this.target = target;}@Overridepublic 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. 创建并使用代理对象
import java.lang.reflect.Proxy;public class Main {public static void main(String[] args) {// 创建目标对象UserService target = new UserServiceImpl();// 创建InvocationHandlerInvocationHandler handler = new LogInvocationHandler(target);// 使用Proxy工厂创建代理对象UserService proxyInstance = (UserService) Proxy.newProxyInstance(target.getClass().getClassLoader(), // 目标类的类加载器target.getClass().getInterfaces(),   // 目标类实现的接口handler                              // 我们自己的处理器);// 调用代理对象的方法proxyInstance.addUser("Alice");}
}

输出结果

[日志] 方法 addUser 即将执行...
核心业务:正在添加用户 - Alice
[日志] 方法 addUser 执行完毕。

这个例子完美地展示了,我们没有修改一行UserServiceImpl的代码,就成功地为其增加了日志功能。这就是动态代理的强大之处。


AOP实现有哪些注解?

面试官您好,在Spring AOP中,我们主要使用一套基于AspectJ语法的注解来定义切面。这些注解让我们可以用一种非常声明式、清晰的方式来编写AOP。

我通常会把这些注解分为两大类:定义切面的注解定义通知(Advice)的注解

第一类:定义切面的核心注解

这类注解用于“勾勒”出整个切面的轮廓。

  • @Aspect

    • 作用:这是最重要的一个注解,它用于声明一个类是一个切面。只有被@Aspect标记的类,Spring AOP才会将其识别为一个切面,并去解析它内部的切点和通知。
  • @Pointcut

    • 作用:用于定义一个可重用的切点。切点就像一个“查询语句”,它定义了“哪些方法(连接点)需要被增强”。
    • 如何使用:我们通常会把@Pointcut注解在一个空的方法上,这个方法的名字就成为了这个切点的名字。
    • 示例
      @Pointcut("execution(* com.example.service.*.*(..))")
      public void serviceLayer() {} // 定义了一个名为serviceLayer的切点
      
      之后,我们就可以在通知注解中,通过方法名serviceLayer()来引用这个切点,提高了切点表达式的复用性。
第二类:定义通知(Advice)的注解

这类注解定义了“在切点上,具体要执行什么逻辑,以及在什么时机执行”。它们都必须引用一个已定义的@Pointcut,或者直接在注解中写切点表达式。

  • @Before

    • 时机:在目标方法执行之前执行。
    • 用途:非常适合做一些前置校验,比如权限检查、参数日志记录等。
  • @AfterReturning

    • 时机:在目标方法成功执行并返回结果之后执行。
    • 用途:可以获取到目标方法的返回值,适合做一些返回结果的后处理、日志记录等。
  • @AfterThrowing

    • 时机:在目标方法抛出异常之后执行。
    • 用途:可以获取到抛出的异常对象,非常适合用来做统一的异常日志记录、或者将特定异常转换为其他类型的异常。
  • @After

    • 时机无论目标方法是成功返回还是抛出异常,它最终都会被执行
    • 用途:类似于try-catch-finally中的finally块,非常适合做一些资源清理工作。
  • @Around (环绕通知) —— 功能最强大的通知

    • 时机:它环绕着整个目标方法的执行过程。
    • 特点
      1. 它是唯一一种可以控制目标方法是否执行、何时执行的通知。
      2. 它必须接收一个ProceedingJoinPoint类型的参数,并由我们自己 手动调用pjp.proceed() 来执行目标方法。
      3. 它可以修改目标方法的参数,也可以替换其返回值。
    • 用途:功能极其强大,可以实现其他所有通知类型的功能。Spring的声明式事务(@Transactional)就是通过@Around通知来实现的。它在方法执行前开启事务,在方法执行后(或捕获到异常后)提交或回滚事务。
关于@Advice的澄清

需要说明的是,在标准的Spring AOP + AspectJ注解的体系中,并没有一个叫@Advice的通用注解。我们是通过@Before, @After, @Around等这些具体的注解来定义不同类型的通知的。这个可能是与AspectJ原生或其他AOP框架中的概念有所混淆。

总结一下,通过组合使用@Aspect, @Pointcut以及五种不同时机的通知注解,我们就可以非常灵活、精确地定义出我们想要的任何AOP增强逻辑。


什么是反射?有哪些使用场景?

面试官您好,Java的反射机制(Reflection)是Java语言一个非常强大、也是其动态性的核心特征之一。

1. 什么是反射?
  • 核心定义:反射机制允许一个正在运行的Java程序,在运行时检查和操作它自身内部的结构,比如类、接口、字段和方法,而不需要在编译时就知道这些类的具体信息。
  • 一个生动的比喻
    • 没有反射:就像我们有一本说明书,上面写好了如何操作一台机器。我们只能按照说明书上已有的按钮和功能来操作。
    • 有了反射:就像我们不仅有说明书,还有一把 “万能工具箱”。我们可以用这个工具箱,把机器拆开,去查看它内部所有的零件(字段)、线路图(方法),甚至可以动态地去连接一条新的线路(调用私有方法),或者更换一个零件(修改私有字段)
  • 核心API入口:所有反射操作的起点,都是java.lang.Class这个类。获取一个类的Class对象后,我们就可以像“解剖”一样,去获取它的所有信息。
2. 反射能做什么?(核心能力)

反射机制赋予了我们在运行时动态执行以下操作的能力:

  1. 动态创建对象:在运行时,根据一个类的全限定名字符串,动态地创建出它的实例。
    • Class.forName("com.example.User").newInstance()
  2. 动态获取和调用方法:获取一个类的所有方法(包括公有、私有、保护),并动态地调用它们。
    • clazz.getMethod("setName", String.class).invoke(userObject, "Alice");
    • 甚至可以调用私有方法:clazz.getDeclaredMethod("privateMethod").setAccessible(true);
  3. 动态获取和修改字段:获取一个类的所有字段(包括公有、私有),并动态地读取或修改它们的值。
    • clazz.getDeclaredField("name").set(userObject, "Bob");
  4. 判断对象类型:判断一个对象属于哪个类,实现了哪些接口。
  5. 处理注解:在运行时获取类、方法、字段上的注解信息,并根据注解信息执行相应的逻辑。
3. 反射的典型使用场景

正是因为反射的这些强大能力,它成为了众多框架和底层技术的基石。我们平时直接编写业务代码时可能用得不多,但我们所用的框架,几乎无时无刻不在使用反射。

  1. Spring框架的IoC容器

    • Bean的实例化:Spring容器在启动时,会读取XML或扫描注解,得到要管理的Bean的类名(一个字符串)。它就是通过反射,根据这个类名来创建Bean的实例。
    • 依赖注入(DI):当Spring需要为一个Bean的@Autowired字段注入依赖时,它也是通过反射来获取这个字段,并调用field.set()来动态地设置值的。
  2. 动态代理

    • JDK动态代理的实现,就严重依赖反射。它通过java.lang.reflect.Proxy类和InvocationHandler接口,在运行时动态创建一个代理对象,并使用反射来调用目标对象的真实方法。这是Spring AOP的实现基础。
  3. JDBC的数据库驱动加载

    • 在早期的JDBC中,我们通过Class.forName("com.mysql.jdbc.Driver")这行代码来加载数据库驱动。这行代码的本质,就是利用反射,让JVM去加载并执行这个驱动类中的静态代码块,从而完成驱动的注册。
  4. 各种ORM框架(如MyBatis, Hibernate)

    • 当框架需要将数据库查询结果集(ResultSet)中的数据,映射到一个Java实体对象(POJO)时,它会通过反射来获取实体类的所有字段,并调用相应的setter方法,将查询结果逐一设置进去。
  5. 各种序列化/反序列化库(如Jackson, Gson)

    • 将一个JSON字符串反序列化为一个Java对象时,这些库也是通过反射来创建对象实例,并为对象的各个字段赋值。
4. 反射的缺点

当然,反射也是一把双刃剑,它有几个明显的缺点:

  1. 性能开销:反射操作涉及到JVM在运行时的动态类型检查和方法查找,其性能远低于直接的Java代码调用。
  2. 破坏封装性:通过setAccessible(true),反射可以强行访问和修改类的私有成员,这破坏了面向对象的封装原则,可能导致代码难以维护和理解。
  3. 安全性问题:在某些需要安全管理的环境下(如Applet),反射的使用可能会受到安全策略的限制。

总结一下,反射是Java语言动态性的根基,它赋予了程序在运行时“审视和操作自身”的超能力。虽然它有性能和安全上的代价,不建议在业务代码中滥用,但它却是实现各种通用框架和底层技术的、不可或缺的核心机制。


Spring是如何解决循环依赖的?

面试官您好,Spring解决循环依赖的问题,是一个非常精巧的设计,但它并不是万能的

Spring的循环依赖解决机制,有其明确的边界:

  • 能解决单例(Singleton)作用域下,通过Setter方法或字段注入(@Autowired 的循环依赖。
  • 不能解决
    • 构造器注入的循环依赖。
    • 原型(Prototype)作用域下的循环依赖。
  • 对于不能解决的场景,Spring在启动时会直接抛出BeanCurrentlyInCreationException异常,让我们在开发阶段就能发现并修复设计问题。

下面,我重点讲解一下Spring是如何通过 “三级缓存”“提前暴露” 这两个核心机制,来解决单例Setter循环依赖的。

Spring的三级缓存

Spring容器内部维护了三个Map,也就是我们常说的“三级缓存”,用于存放不同状态的Bean:

  1. 一级缓存: singletonObjects

    • 存放内容最终的成品Bean。这是一个经历了完整生命周期(实例化、属性注入、初始化、AOP代理等)的、可以直接使用的Bean实例。
    • 作用:这是一个单例池,所有获取Bean的请求,都会先从这里查找。
  2. 二级缓存: earlySingletonObjects

    • 存放内容提前暴露的“半成品”Bean。这个Bean只完成了实例化,但还未进行属性注入和初始化
    • 作用:用于解决循环依赖。如果一个Bean A在创建过程中,发现它依赖的Bean B也正在创建,并且Bean B又依赖Bean A,那么就从这个二级缓存里获取那个“半成品”的A,来完成B的创建。
  3. 三级缓存: singletonFactories

    • 存放内容能够生产“半成品”Bean的工厂(ObjectFactory
    • 作用:这是解决循环依赖,特别是与AOP代理结合时的关键。它本身不直接存Bean,而是存一个Lambda表达式。当二级缓存中找不到,并且需要获取一个“半成品”Bean时,就会调用这个工厂的getObject()方法。这个方法会负责创建AOP代理(如果需要的话),并返回那个代理后的“半成品”,然后将其放入二级缓存。
解决循环依赖的推演过程 (A依赖B, B依赖A)
  1. 创建AgetBean("a")请求开始。

    • 一级缓存:找不到a
    • 二级缓存:找不到a
    • 开始创建A
      a. 实例化A:通过反射创建出Bean A的“毛坯”对象(我们称之为a_instance)。
      b. 放入三级缓存:为了能让其他Bean提前拿到它,Spring会创建一个能生产a_instance(或其代理)的工厂,并以"a"为key,放入三级缓存singletonFactories
  2. 为A注入属性:Spring开始为a_instance注入属性,发现它依赖Bean B。于是触发getBean("b")

  3. 创建BgetBean("b")请求开始。

    • 一级缓存:找不到b
    • 二级缓存:找不到b
    • 开始创建B
      a. 实例化B:创建出Bean B的“毛坯”对象(b_instance)。
      b. 放入三级缓存:同样,将生产b_instance的工厂放入三级缓存。
  4. 为B注入属性 (循环点):Spring为b_instance注入属性,发现它依赖Bean A。于是再次触发getBean("a")

  5. 解决循环第二次getBean("a")请求开始。

    • 一级缓存:找不到a
    • 二级缓存:找不到a
    • 三级缓存找到了!在步骤1.b中,我们已经为a放入了一个工厂。
    • Spring会调用这个工厂的getObject()方法,这个方法会:
      i. 判断a是否需要AOP代理,如果需要,就创建代理对象,否则返回原始的a_instance
      ii. 将这个返回的“半成品”a对象,放入二级缓存earlySingletonObjects
      iii. 从三级缓存中移除a的工厂。
    • 返回“半成品”A:第二次getBean("a")成功返回了这个“半成品”的a对象。
  6. B创建完成b_instance成功地被注入了“半成品”a。Bean B继续完成它的生命周期(初始化、AOP等),最终成为一个 “成品”B。这个成品B会被放入一级缓存singletonObjects

  7. A创建完成getBean("b")的请求成功返回了成品B。现在,a_instance也成功地被注入了B。Bean A继续完成它的生命周期,最终也成为一个 “成品”A,并被放入一级缓存。

至此,循环依赖被成功解决。

为什么必须是三级缓存,二级不够吗?

这是最关键的深度问题。如果没有AOP,二级缓存确实就够了。

如果A有了AOP代理,情况就复杂了:

  • 如果只有二级缓存,我们在步骤1.b时,就必须决定是把 原始对象a_instance 还是 代理对象proxy_a 放入二级缓存。
  • 如果我们放的是原始对象,那么B注入的就是一个没有被代理的A,这不符合AOP的期望。
  • 如果我们放的是代理对象,那么意味着我们过早地创建了代理。如果后续的Bean后置处理器(比如处理@Async的)还需要对这个Bean进行再次代理,就会出问题。

三级缓存的精妙之处就在于,它不直接暴露对象,而是暴露一个工厂。它将 “是否创建代理、何时创建代理” 这个决策,推迟到了这个Bean 真正被下游依赖需要的那一刻。这样,既保证了AOP代理的正确创建,又没有破坏Spring Bean的完整生命周期。


Spring框架中都用到了哪些设计模式

面试官您好,Spring框架之所以如此强大和灵活,其底层是建立在对各种经典设计模式的精妙运用之上的。在我看来,这些设计模式可以分为两大核心基石模式多种辅助实现模式

一、 Spring的两大核心基石设计模式

可以说,没有这两个模式,就没有Spring的灵魂。

  1. 工厂模式 (Factory Pattern)

    • 在Spring中的体现整个Spring IoC容器,其本质就是一个巨大的、超级灵活的工厂。我们最常使用的BeanFactoryApplicationContext就是工厂模式最直接的体现。
    • 它解决了什么? 它代替了我们自己去new对象。我们不再关心一个Bean是如何被创建、如何被依赖注入、如何初始化的,我们只需要向这个“工厂”(IoC容器)索取(getBean()),工厂就会返回一个已经装配好的、可用的产品(Bean实例)。这是实现 控制反转(IoC) 的基础。
  2. 单例模式 (Singleton Pattern)

    • 在Spring中的体现:Spring容器中管理的Bean,默认的作用域(Scope)就是单例。这意味着,对于一个特定定义的Bean,在整个容器的生命周期内,只会创建唯一一个实例
    • 它解决了什么? 在企业级应用中,大量的对象(如Service、DAO、Controller)都是无状态的,它们不需要为每个请求都创建一个新实例。使用单例模式可以极大地节省内存开销,提高对象复用的效率。Spring通过其容器,以一种更优雅、更易于管理的方式实现了全局的单例。
二、 实现IoC与AOP的几种关键辅助模式

为了实现IoC和AOP这两大核心功能,Spring巧妙地运用了其他多种设计模式。

  1. 代理模式 (Proxy Pattern)

    • 在Spring中的体现Spring AOP功能完全是基于代理模式实现的。当我们需要为一个Bean织入切面逻辑(如事务、日志)时,Spring不会修改原始Bean,而是会为它动态地创建一个代理对象。我们从容器中获取的,实际上是这个代理对象。
    • 具体实现:它会智能地选择JDK动态代理(基于接口)或CGLIB动态代理(基于继承)来创建这个代理。
  2. 模板方法模式 (Template Method Pattern)

    • 在Spring中的体现:这是Spring用来 简化重复性“样板代码” 的利器。最典型的就是JdbcTemplate, RedisTemplate, RestTemplate等以Template结尾的类。
    • 它解决了什么? 比如在使用原生JDBC时,我们需要手动地获取连接、创建Statement、执行SQL、处理异常、关闭连接等,这些步骤非常繁琐且重复。JdbcTemplate将这个固定的流程封装成一个模板,我们只需要提供最核心的“变量”部分(比如SQL语句和结果集映射逻辑),而无需关心那些通用的、资源管理相关的样板代码。
  3. 观察者模式 (Observer Pattern)

    • 在Spring中的体现Spring的事件驱动模型 (ApplicationEventApplicationListener) 就是观察者模式的经典应用。
    • 它解决了什么? 实现了模块间的松耦合通信。一个模块(事件发布者)可以发布一个事件,而不需要关心谁会来处理它。其他任何对这个事件感兴趣的模块(事件监听者),都可以订阅并处理这个事件。这在需要进行业务解耦或异步处理的场景中非常有用。
三、 其他模式的应用

除了以上几种,Spring还用到了很多其他模式:

  • 适配器模式 (Adapter Pattern):在Spring MVC中,HandlerAdapter用于适配各种不同类型的处理器(Controller)。在Spring AOP中,不同类型的通知(Advice)也通过适配器模式,被统一转换成拦截器来执行。
  • 装饰器模式 (Decorator Pattern):与代理模式很像,但更侧重于在不改变接口的情况下,动态地为对象添加功能。Spring中对DataSource的包装(如TransactionAwareDataSourceProxy)就体现了这种思想。
  • 策略模式 (Strategy Pattern):Spring在实例化Bean时,根据不同的情况选择不同的实例化策略。AOP中选择JDK代理还是CGLIB,也可以看作是一种策略选择。

总结一下,Spring框架就像一个设计模式的“集大成者”。它以工厂模式单例模式为基础构建了IoC容器,以代理模式为核心实现了AOP,并广泛运用模板方法、观察者、适配器等模式来简化开发、实现解耦,最终为我们提供了一个强大、灵活而又优雅的开发平台。


Spring 常用注解有什么?

面试官您好,Spring框架发展至今,已经形成了一套非常丰富和强大的注解体系,它们极大地简化了我们的开发和配置工作。在我的日常开发中,最常用的Spring注解,我习惯于把它们分为以下几大类:

第一类:Bean定义与依赖注入注解(IoC/DI相关)

这是最核心、最基础的一类,用于将我们的类交给Spring容器管理,并处理它们之间的依赖关系。

  • @Component: 这是最通用的一个Bean定义注解。它标记一个类为Spring容器管理的组件。

    • 它的三个“衍生”注解:为了让组件的角色更清晰,Spring提供了三个语义更明确的衍生注解,它们在功能上与@Component完全等价:
      • @Service: 通常用于标记业务逻辑层(Service层) 的组件。
      • @Repository: 通常用于标记数据访问层(DAO层) 的组件,并且它能让Spring将特定的数据库操作异常转换为统一的DataAccessException
      • @Controller: 通常用于标记Web控制层(Controller层) 的组件。
  • @Autowired: 这是进行依赖注入最常用的注解。Spring会自动地从容器中寻找类型匹配的Bean,并注入到被它标记的字段、构造器或Setter方法上。

  • @Qualifier("beanName"): 当一个接口有多个实现类,而@Autowired按类型查找时会产生歧义,@Qualifier就派上用场了。它可以按名称来指定要注入哪一个具体的Bean。

  • @Resource(name = "beanName"): 与@Autowired类似,但它是JSR-250规范定义的。它默认按名称注入,如果名称找不到,再按类型注入。

  • @Scope("prototype"): 用于定义Bean的作用域。默认是singleton(单例)。其他常用的还有prototype(原型,每次请求都创建新实例)、requestsession等(在Web环境中使用)。

  • @Lazy: 默认情况下,单例Bean在容器启动时就会被创建。@Lazy注解可以让Bean被延迟加载,即在第一次被使用时才创建。

第二类:Java配置类注解 (Java-based Configuration)

这类注解让我们能够用纯Java代码来代替XML进行Spring的配置。

  • @Configuration: 标记一个类为配置类,它相当于一个XML配置文件。
  • @Bean: 用在方法上。Spring会调用这个方法,并将该方法的返回值注册为一个Bean,Bean的名称默认就是方法名。
  • @ComponentScan("com.example"): 用于启用组件扫描。它告诉Spring去哪个包(及其子包)下扫描带有@Component等注解的类。
  • @Import: 用于导入其他的配置类,实现配置的模块化。
第三类:AOP相关注解

这类注解用于定义切面,实现面向切面编程。

  • @Aspect: 声明一个类为切面
  • @Pointcut: 定义一个可重用的切点
  • @Before, @After, @Around 等:定义不同类型的通知(Advice),即在什么时机执行增强逻辑。
第四类:Web与MVC相关注解 (Spring MVC)
  • @RestController: 是@Controller@ResponseBody的组合,表示这个控制器下的所有方法都直接返回JSON/XML等数据,而不是视图名。这是编写RESTful API的首选。
  • @RequestMapping("/path"): 将HTTP请求的URL映射到控制器的方法上。
    • 它的几个“衍生”注解:为了让意图更清晰,Spring 4.3后提供了更具体的映射注解:
      • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
  • @RequestParam: 从请求的查询参数中获取值。
  • @PathVariable: 从URL的路径变量中获取值(如/users/{id}中的id)。
  • @RequestBody: 将请求的JSON/XML体,反序列化并绑定到方法的参数对象上。
第五类:数据访问与事务管理注解
  • @Transactional: 这是最重要的一个注解。它以AOP的方式,为方法提供了声明式事务管理。我们只需要加上这个注解,Spring就会自动处理事务的开启、提交和回滚。
第六类:Spring Boot特有注解
  • @SpringBootApplication: 这是一个组合注解,通常用在主启动类上。它包含了@SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan这三个核心注解,是Spring Boot应用的“一键启动”开关。
  • @Value("${property.name}"): 用于将配置文件(application.propertiesapplication.yml)中的属性值注入到Bean的字段中。
  • @ConfigurationProperties(prefix = "my.service"): 提供了更强大的类型安全的属性绑定,可以将一组相关的配置项,直接映射到一个Java配置类对象上。

这些注解覆盖了从Bean管理、AOP、Web开发到数据访问的方方面面,熟练地运用它们,是进行高效Spring开发的基础。


@Component 和 @Bean 的区别是什么?

面试官您好,@Component@Bean是我们在Spring中进行Bean定义的两种最主要的方式。它们虽然最终都能将一个对象注册到Spring容器中,但它们的使用方式、控制粒度和适用场景有着本质的区别。

我通常会从 “这个Bean的源代码,控制权在谁手上?” 这个角度来理解和选择它们。

1. @Component —— “这个类,请Spring帮我管一下”
  • 作用目标作用于类上
  • 使用方式:我们在自己的类上标注@Component(或其衍生注解@Service, @Repository, @Controller),然后通过@ComponentScan来告诉Spring去哪里扫描。Spring在扫描到这些被标记的类后,会自动地为它们创建实例并注册为Bean。
  • 控制权归属:使用@Component时,我们将Bean的实例化和配置的控制权,几乎完全交给了Spring。Spring会使用默认的构造函数来创建对象,整个过程是自动化的。
  • 核心适用场景用于标注我们自己编写的类。对于我们自己项目中的Service、Controller、DAO等,使用@Component及其衍生注解,是最简单、最直接、最常规的做法。
2. @Bean —— “这个Bean,由我亲手来创建”
  • 作用目标作用于方法上
  • 使用方式@Bean注解必须用在一个被@Configuration标记的配置类中的方法上。这个方法的返回值,就是Spring需要管理的那个Bean实例。`
  • 控制权归属:使用@Bean时,Bean的实例化的控制权,完全掌握在我们自己手中。我们可以在这个方法体内部,执行任意复杂的逻辑来创建和配置这个对象,比如:
    • 调用一个复杂的构造函数。
    • 调用一系列的setter方法进行精细的初始化。
    • 甚至可以根据不同的环境或配置,返回不同类型的实现类。
  • 核心适用场景
    1. 将第三方库中的类注册为Bean(最经典)
      • 如果我们想把一个第三方库(比如JedisPool, DataSource)中的类,交给Spring管理,我们无法去修改它的源代码给它加上@Component注解。
      • 此时,唯一的办法就是在我们自己的@Configuration配置类中,编写一个方法,手动new出这个第三方类的实例,并用@Bean注解标记,将其注册到容器中。
      @Configuration
      public class RedisConfig {@Beanpublic JedisPool jedisPool() {// 在这里,我们可以完全控制JedisPool的创建过程JedisPoolConfig config = new JedisPoolConfig();config.setMaxTotal(100);return new JedisPool(config, "localhost", 6379);}
      }
      
    2. 需要进行复杂初始化逻辑时:当一个Bean的创建过程非常复杂,不是一个简单的无参构造函数能搞定时,使用@Bean方法可以让我们用Java代码来清晰地表达这个复杂的创建逻辑。

总结对比

特性@Component@Bean
作用目标方法
使用方式类路径扫描@Configuration类中方法定义
控制权Spring控制 (自动化)开发者控制 (手动化)
适用代码我们自己写的类第三方库的类需要复杂初始化的类
灵活性较低极高

一句话总结

  • @Component“声明” 一个我们自己写的类应该被Spring管理。
  • @Bean“注册” 一个由我们自己手动创建和配置的、更复杂的或来自外部的Bean实例。

Spring的事务什么情况下会失效?

面试官您好,Spring的@Transactional注解虽然使用起来非常方便,但它背后是基于AOP动态代理实现的。因此,很多事务失效的场景,其根本原因都是AOP代理没有生效,或者异常处理不当导致Spring没有正确地触发回滚。

在我遇到的实践中,主要有以下几类经典的失效情况:

第一类:因AOP代理机制导致的失效(最常见)

这类问题的根源在于,调用没有经过代理对象,导致事务增强的逻辑没有被执行。

  1. 方法内部调用 (Self-invocation)

    • 场景:在一个没有@Transactional注解的方法A中,调用了同一个类中@Transactional注解的方法B。
    • 为什么失效? 因为Spring AOP是基于代理的。外部调用方法A时,是通过代理对象调用的。但在方法A内部,通过this.B()来调用方法B时,这个this原始的目标对象,而不是Spring创建的代理对象。这就等于绕过了代理,直接调用了原始方法,AOP的事务拦截器自然就不会生效。
    • 解决方案
      • 将方法B抽离到另一个独立的Bean中,通过注入这个新Bean来调用。
      • 在当前类中注入自己(@Autowired private MyService self;),然后通过self.B()来调用(需要配置允许循环依赖)。
      • 使用AopContext.currentProxy()来获取当前代理对象,再进行调用。
  2. 访问修饰符问题

    • 场景@Transactional注解应用在了 public 的方法上(如private, protected, default)。
    • 为什么失效? 在默认的AOP代理模式下(特别是CGLIB),它通过继承来创建代理类,并重写父类的public方法。非public方法无法被子类重写,因此AOP无法对其进行拦截和增强。
第二类:因异常处理机制不当导致的失效

这类问题的根源在于,抛出的异常类型不符合Spring默认的回滚规则

  1. 抛出了受检异常 (Checked Exception)

    • 场景:事务方法中抛出了一个IOException, SQLException等受检异常。
    • 为什么不回滚? Spring的声明式事务,在默认情况下,只对 RuntimeException(运行时异常)Error(错误) 进行回滚。它认为受检异常是业务逻辑的一部分,应该由开发者自己决定如何处理(比如通过try-catch捕获)。
    • 解决方案
      • @Transactional注解中,明确指定需要回滚的异常类型:@Transactional(rollbackFor = Exception.class)
      • 将受检异常包装成一个运行时异常再抛出。
  2. 方法内部try-catch“吞掉”了异常

    • 场景:在事务方法内部,用try-catch捕获了可能导致回滚的RuntimeException,但在catch块中没有将异常重新抛出
    • 为什么不回滚? 因为从Spring事务切面的角度看,这个方法是 “正常执行完毕” 的,它根本没有感知到任何异常的发生,自然也就不会触发回滚逻辑。
    • 解决方案:在catch块中,处理完自定义逻辑后(如记录日志),必须将异常重新throw出去,或者手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();来标记事务为回滚。
第三类:因数据库引擎或配置问题导致的失效
  1. 数据库引擎不支持事务

    • 场景:这种情况现在比较少见,但仍然可能发生。比如,MySQL的MyISAM存储引擎就是不支持事务的。
    • 解决方案:确保数据库表使用的存储引擎是支持事务的,如InnoDB
  2. 事务传播行为配置不当

    • 场景:在一个已有的事务方法A中,调用了另一个事务方法B,但方法B的事务传播行为被设置为了Propagation.NOT_SUPPORTED(以非事务方式运行)或Propagation.NEVER(不允许在事务中运行)。
    • 结果:方法B内部的数据库操作将不会被包含在方法A的事务中。

总结一下,要确保@Transactional生效,我通常会检查三点:

  1. AOP代理是否生效:确保调用是通过代理对象,且方法是public的。
  2. 异常处理是否正确:确保会导致回滚的异常能被Spring的事务切面正确捕获到。
  3. 底层支持是否到位:确保数据库引擎支持事务。

Bean的生命周期说一下?

面试官您好,Spring Bean的生命周期是一个非常精巧且高度可扩展的过程。我们可以把它看作是一个Bean从 “诞生”到“成熟”再到“消亡” 的全过程。

我通常会把这个复杂的流程,归纳为以下四个主要阶段

第一阶段:实例化 (Instantiation)

这是Bean的“诞生”阶段。

  1. 创建实例:当Spring容器根据配置信息(XML或注解)需要创建一个Bean时,它首先会通过反射机制,调用这个类的构造函数,创建一个 “毛坯”对象 。此时,这个对象仅仅是一个普通的Java对象,其属性都还是默认值。
第二阶段:属性填充 (Population)

这是对“毛坯”对象进行“装修”的阶段。

  1. 依赖注入:Spring容器会分析这个对象,找出它所依赖的其他Bean(比如带有@Autowired注解的字段),然后将这些依赖从容器中取出,并通过反射注入到这个对象中。此时,Bean的依赖关系已经建立。
第三阶段:初始化 (Initialization) —— 最复杂、最具扩展性的阶段

这是Bean从“半成品”变为“成品”的关键阶段。这个阶段包含了一系列的回调和处理,赋予了Bean强大的生命力。

这个过程,我喜欢记成 “感知 -> 前置处理 -> 初始化 -> 后置处理”

  1. 各种Aware接口的回调(感知容器信息)

    • 如果Bean实现了BeanNameAware接口,Spring会调用setBeanName()方法,将Bean的ID传入。
    • 如果Bean实现了BeanFactoryAware接口,Spring会调用setBeanFactory(),将BeanFactory容器实例传入。
    • 如果Bean实现了ApplicationContextAware接口(这是最常用的),Spring会调用setApplicationContext(),将应用上下文传入。
    • 通过这些Aware接口,Bean就能够 “感知”到自己在容器中的存在,并获取到容器的资源
  2. BeanPostProcessor的前置处理

    • 在Bean的任何初始化方法被调用之前,Spring会遍历所有注册的BeanPostProcessor,并调用它们的postProcessBeforeInitialization()方法。
    • 这是一个非常重要的扩展点,允许我们对即将初始化的Bean进行“最后的加工”。
  3. Bean自身的初始化

    • @PostConstruct注解:如果Bean的方法被@PostConstruct注解标记,这个方法会被优先调用。
    • InitializingBean接口:如果Bean实现了InitializingBean接口,接着会调用其afterPropertiesSet()方法。
    • 自定义init-method:如果在配置中指定了init-method,最后会调用这个自定义的初始化方法。
    • Spring推荐使用@PostConstruct注解,因为它与Spring框架的耦合度最低。
  4. BeanPostProcessor的后置处理

    • 在Bean的所有初始化逻辑都执行完毕后,Spring会再次遍历所有BeanPostProcessor,并调用它们的postProcessAfterInitialization()方法。
    • 这是Bean生命周期中另一个极其重要的扩展点Spring的AOP(动态代理)就是在这个阶段实现的。如果一个Bean需要被AOP增强,那么在这个方法里,返回的就不是原始的Bean实例,而是一个被包装后的代理对象

至此,一个完整的、可用的、甚至可能被代理过的成品Bean才算真正创建完成,它会被放入单例池中,等待被应用程序使用。

第四阶段:销毁 (Destruction)

当Spring容器关闭时,会进入Bean的“消亡”阶段。

  1. Bean自身的销毁
    • @PreDestroy注解:如果Bean的方法被@PreDestroy注解标记,这个方法会被调用。
    • DisposableBean接口:如果Bean实现了DisposableBean接口,会调用其destroy()方法。
    • 自定义destroy-method:如果在配置中指定了destroy-method,最后会调用这个自定义的销毁方法。
    • 这个阶段主要是为了让Bean能够释放它所持有的资源,比如关闭数据库连接、停止后台线程等。

总结一下,Spring Bean的生命周期,是一个从实例化、属性填充,到一系列复杂的初始化回调,最终到销毁的完整过程。其中, BeanPostProcessor 这个扩展点,贯穿了初始化前后,为Spring实现AOP、依赖注入等核心功能提供了强大的基础。


Bean的单例 vs 多例(原型)

面试官您好,Spring中Bean默认情况下是单例的,但我们可以根据需要将其配置为其他作用域,其中最常见的就是多例(原型)。

1. 为什么Spring选择“单例”作为默认作用域?

Spring之所以这样做,是基于对企业级应用特点的深刻理解,主要有两大原因:

  1. 性能与资源效率:在绝大多数应用场景中,大量的Bean(如ServiceDAOController等)都是无状态的。它们不包含可变的成员变量,其方法执行不依赖于自身状态。对于这样的对象,完全没有必要为每一次请求或每一次使用都创建一个新实例。复用同一个单例对象,可以极大地减少对象创建和垃圾回收的开销,从而提升应用的整体性能和内存效率。

  2. 依赖注入的一致性:Spring的IoC容器需要管理一个复杂的Bean依赖关系图。将Bean设计为单例,使得整个容器中所有依赖该Bean的地方,注入的都是同一个实例,这保证了依赖关系的一致性和稳定性。

2. 如何配置为多例(原型/Prototype)?

当然,Spring也提供了灵活的配置来改变默认行为。如果我需要每次都获取一个新的Bean实例,我可以通过设置@Scope注解来实现:

@Service
@Scope("prototype") // 或者 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyPrototypeService {// ...
}

或者在XML配置中使用scope="prototype"

3. 单例 vs. 原型:一个重要的对比

这两种作用域在生命周期管理上有着本质的区别:

特性单例 (Singleton)原型 (Prototype)
创建时机Spring容器启动时(或第一次被请求时,如果懒加载)每次请求时 (getBean())
实例数量整个容器中唯一每次请求都全新创建
生命周期管理由Spring容器完整管理:从创建、依赖注入、初始化,一直到容器关闭时的销毁。Spring容器只负责“生”,不负责“养”和“死”:容器只负责在请求时创建并返回一个完整的实例,但之后不再跟踪这个实例。它的销毁完全依赖于Java的GC。
销毁方法调用容器关闭时,会调用@PreDestroydestroy-method不会被容器调用。
4. 选型思考与线程安全问题
  • 什么时候用原型?

    • 当Bean是有状态的,并且这个状态是不可共享的。最典型的例子就是一个需要记录每次操作历史的Action对象,每个用户的操作都应该对应一个独立的实例。
  • 单例Bean的线程安全问题(非常重要)

    • 这是一个在实践中必须高度警惕的问题。因为单例Bean是所有线程共享的,所以绝对不能在单例Bean中定义可变的成员变量(实例变量) 来存储与请求相关的状态。
    • 反例
      @Service // 默认单例
      public class UnsafeStatefulService {private int count = 0; // 【危险】有状态的成员变量public void process() {count++; // 多线程下会产生竞态条件// ...}
      }
      
    • 如果一个单例Bean必须要有状态,那么对这个状态的所有读写操作,都必须通过加锁 (synchronized, Lock) 或者使用 线程安全的并发工具(如AtomicInteger, ThreadLocal 来保证线程安全。

总结一下,Spring的“默认单例”是一个非常合理且高效的设计。但作为开发者,我们必须清醒地认识到不同作用域的特点和生命周期,并在设计有状态的单例Bean时,时刻绷紧“线程安全”这根弦。


Spring bean的作用域有哪些?

面试官您好,Spring Bean的作用域(Scope)是用来定义一个Bean实例的生命周期和可见范围的。Spring提供了多种作用域来适应不同的应用场景,我通常会把它们分为两大类:通用作用域Web应用专属作用域

一、 通用作用域(在任何Spring应用中都可用)

这是最基础、最重要的两种作用域。

  1. singleton (单例) —— 默认作用域

    • 定义:在整个Spring IoC容器的生命周期内,一个Bean定义只会创建一个唯一的实例
    • 生命周期:由Spring容器完整管理,从创建、初始化,一直到容器关闭时的销毁。
    • 适用场景:绝大多数的Bean都适合使用单例作用域,特别是那些无状态的Bean,如Service层、DAO层、以及各种工具类。这是Spring的默认选择,也是最高效的选择。
  2. prototype (原型/多例)

    • 定义每次通过getBean()或依赖注入向容器请求该Bean时,容器都会创建一个全新的实例返回。
    • 生命周期:这是一个非常关键的区别点。对于原型Bean,Spring容器只负责“生”,不负责“死”。也就是说,容器只负责创建、配置并返回一个完整的实例,之后就不再跟踪这个实例了。它的后续生命周期(包括销毁)完全交由Java的垃圾回收器(GC)来管理。因此,Spring不会为原型Bean调用其配置的销毁方法(如@PreDestroy)。
    • 适用场景:适用于那些有状态的Bean,并且这个状态是不可共享的。比如,一个需要记录每次操作历史的Action对象。
二、 Web应用专属作用域(仅在Web环境中生效)

这些作用域的生命周期与Web请求的生命周期紧密相关,只有在基于Spring MVC或Spring WebFlux的Web应用中才有效。

  1. request (请求)

    • 定义:对于每一次HTTP请求,容器都会创建一个全新的Bean实例。这个实例只在当前请求的处理过程中有效。
    • 生命周期:与HTTP请求的生命周期完全绑定。请求开始时创建,请求结束时销毁。
    • 适用场景:非常适合用来封装与当前请求相关的信息,比如用户的请求参数、查询条件等。
  2. session (会话)

    • 定义:在一个HTTP Session的生命周期内,一个Bean定义只会创建一个唯一的实例。
    • 生命周期:与HTTP Session的生命周期绑定。不同的用户Session会拥有各自独立的Bean实例。
    • 适用场景:非常适合用来存储与用户会话相关的状态信息,比如用户的登录信息、购物车等。
  3. application (应用)

    • 定义:在一个 ServletContext 的生命周期内,一个Bean定义只会创建一个唯一的实例。
    • 生命周期:与整个Web应用的生命周期绑定。
    • 适用场景:类似于singleton,但它的作用域是限定在ServletContext级别的,用于存放需要在整个Web应用范围内共享的全局配置信息。
三、 其他作用域
  • 正如您提到的,在现代Web应用中,还出现了像 websocket 这样的新作用域,用于在WebSocket的生命周期内共享Bean。
  • 此外,Spring还提供了强大的自定义作用域(Custom Scopes) 机制,允许我们通过实现org.springframework.beans.factory.config.Scope接口,来定义符合我们自己特定业务需求的全新作用域。

总结一下,Spring通过提供这一系列丰富的作用域,让我们能够非常精细地控制Bean实例的生命周期和共享范围,从而满足从简单单体应用到复杂Web应用的各种需求。在日常开发中, singletonprototype 是我们必须深刻理解和掌握的两种最核心的作用域。


Spring容器里存的是什么?

面试官您好,这是一个非常好的问题,它触及了Spring IoC的核心。我们可以把Spring容器想象成一个高度智能化的自动化工厂

那么这个工厂里存放的,就不仅仅是最终的 “产品”(Bean实例) ,更重要的是生产这些产品所需要的全套 “设计蓝图”“生产工具”

具体来说,Spring容器中主要存储了以下三类核心东西:

1. Bean的“设计蓝图” —— BeanDefinition 对象

这是容器工作的基础

  • 当Spring容器启动时,它会首先解析我们的配置(无论是XML还是注解),但它不会立即创建Bean实例
  • 相反,它会将每个Bean的所有元数据信息,封装成一个org.springframework.beans.factory.config.BeanDefinition对象。
  • 这个BeanDefinition就像一张详尽的“设计蓝图”,里面包含了:
    • Bean的类名
    • Bean的作用域(Scope),是单例还是原型等。
    • Bean的依赖关系,它依赖哪些其他的Bean。
    • Bean的生命周期回调,比如init-methoddestroy-method
    • 是否懒加载、是否是主候选项(@Primary)等。
  • 容器内部有一个Map<String, BeanDefinition>,专门用来存储和管理所有这些“蓝图”
2. Bean的“成品” —— 单例Bean实例 (Singleton Instances)

这是我们最常接触到的,也是您回答的核心——最终的Bean对象

  • 对于单例(Singleton) 作用域的Bean,Spring在创建它们之后,会将这些 “成品” 缓存起来,以便后续重复使用。
  • 这个缓存通常被称为 “单例池”(Singleton Cache),其内部也是一个Map<String, Object>,用于存放已经经历了完整生命周期的Bean实例。
  • 一个非常关键的点:很多时候,我们从容器中获取到的,存放在这个单例池里的,并不是原始的Java对象,而是一个经过AOP增强后的代理对象。比如,一个被@Transactional注解标记的Service Bean,容器里存放的就是它的事务代理。
3. Bean的“生产与加工工具” —— 各种BeanPostProcessor

这是体现Spring框架强大扩展能力的关键。

  • Spring容器内部还存储了大量的 BeanPostProcessor(Bean后置处理器) 实例。
  • 这些后置处理器就像是工厂生产线上的 “质检员”或“加工站”。在Bean的生命周期中,特别是在初始化前后,Spring会调用这些后置处理器,让它们有机会对Bean进行“二次加工”。
  • 我们熟知的很多Spring核心功能,都是通过BeanPostProcessor来实现的:
    • @Autowired依赖注入:是通过AutowiredAnnotationBeanPostProcessor来完成的。
    • AOP代理的创建:是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来实现的。
    • @PostConstruct注解的执行:是通过CommonAnnotationBeanPostProcessor来处理的。

总结一下
Spring容器就像一个精密的工厂,它里面存放着:

  • 所有产品的设计蓝图 (BeanDefinition)
  • 一个存放成品的仓库 (Singleton Cache),里面装的可能是原始产品,也可能是加装了额外功能的“精装版”(代理对象)。
  • 以及一套完整的、用于加工和增强产品的自动化生产工具 (BeanPostProcessor)

正是这套完整的体系,使得Spring容器能够高效、灵活地管理和装配复杂的Java应用。


在Spring中,在bean加载/销毁前后,如果想实现某些逻辑,可以怎么做

面试官您好,Spring为我们提供了多种在Bean加载(初始化)和销毁前后执行自定义逻辑的方式。这些方式各有其特点和适用场景,我通常会根据与Spring框架的耦合度以及需求的通用性来做选择。

一、 在Bean“加载”(初始化)前后执行逻辑

Bean的初始化是一个非常重要的阶段,Spring在这里提供了丰富的扩展点。

方法一:使用@PostConstruct@PreDestroy注解 (JSR-250规范,首选)

  • @PostConstruct (初始化后)

    • 如何做:在一个Bean中,在一个无参的public方法上标注@PostConstruct注解。

    • 执行时机:这个方法会在依赖注入完成之后,Bean的任何其他初始化方法(如afterPropertiesSetinit-method)执行之前被调用。

    • 优点:这是Java EE的规范,与Spring框架完全解耦,代码更通用、更优雅。这是我个人的首选方式

    • 示例

      @Service
      public class MyService {@Autowiredprivate MyDependency dependency;@PostConstructpublic void init() {// 此时,dependency已经被注入,可以安全使用System.out.println("Bean初始化完成,依赖已注入。");// 可以在这里执行一些资源的预加载、连接的建立等操作}
      }
      
  • @PreDestroy (销毁前)

    • 如何做:在一个无参的public方法上标注@PreDestroy注解。
    • 执行时机:在Spring容器关闭,Bean实例被销毁之前被调用。
    • 用途:非常适合用来执行资源的清理和释放工作,比如关闭数据库连接、停止后台线程等。

方法二:实现InitializingBeanDisposableBean接口 (Spring原生,耦合度高)

  • InitializingBean (初始化后)

    • 如何做:让Bean类实现InitializingBean接口,并重写afterPropertiesSet()方法。
    • 执行时机:在依赖注入完成之后,@PostConstruct之后被调用。
    • 缺点:这种方式让我们的业务代码与Spring的API产生了耦合,不利于代码的移植和单元测试。现在已不推荐使用。
  • DisposableBean (销毁前)

    • 如何做:让Bean类实现DisposableBean接口,并重写destroy()方法。
    • 执行时机:在Bean销毁前,@PreDestroy之后被调用。同样存在与Spring框架耦合的问题。

方法三:使用XML配置的init-methoddestroy-method属性 (传统方式)

  • 如何做:在XML的<bean>标签中,通过init-method="methodName"destroy-method="methodName"来指定初始化和销毁时要调用的方法名。
  • 优点:同样对业务代码无侵入。
  • 缺点:在现在以注解驱动为主的开发模式下,使用XML配置已经比较少了。

方法四:BeanPostProcessor (最强大,用于通用逻辑)

  • 如何做:实现BeanPostProcessor接口,并重写postProcessBeforeInitializationpostProcessAfterInitialization方法。
  • 执行时机:这两个方法会作用于容器中所有的Bean
    • postProcessBeforeInitialization:在Bean的任何初始化回调(如@PostConstruct之前执行。
    • postProcessAfterInitialization:在Bean的所有初始化回调之后执行。
  • 用途:这不是用来实现单个Bean的特定逻辑的,而是用来实现通用的、横切的增强逻辑Spring的AOP代理、@Autowired的实现等,都是通过BeanPostProcessor来完成的。如果我想对一批具有相同特征的Bean进行统一的初始化前/后处理,我就会考虑使用它。

总结与选型

方式优点缺点适用场景
@PostConstruct / @PreDestroy与Spring解耦、简洁无明显缺点强烈推荐,用于单个Bean的初始化/销毁逻辑
InitializingBean / DisposableBean简单与Spring框架耦合已不推荐,仅用于维护旧代码
init-method / destroy-method与代码解耦依赖XML配置适用于仍在使用XML配置的项目
BeanPostProcessor功能最强大、可作用于所有Bean实现稍复杂用于实现框架级别的、通用的Bean增强逻辑

在我的实践中,对于单个Bean的初始化和销毁需求,我总是优先使用@PostConstruct@PreDestroy。而当我需要编写一个影响整个容器中多个Bean的通用逻辑时,我才会去实现BeanPostProcessor


Bean注入和xml注入最终得到了相同的效果,它们在底层是怎样做的

面试官您好,您说的非常对,无论是通过注解(Annotation)还是XML文件来配置Bean,最终达到的效果是完全一样的。这背后,是因为Spring IoC容器在底层设计了一个统一的、与配置方式无关的Bean定义模型

我们可以把这个过程,想象成用不同的语言(注解/XML)来填写一张标准化的“申请表”

1. 核心的“中间语言” —— BeanDefinition

Spring IoC容器的核心,并不直接与XML或注解打交道。在容器内部,它使用一个统一的、标准的中间数据结构来描述每一个需要管理的Bean。这个数据结构就是 org.springframework.beans.factory.config.BeanDefinition

BeanDefinition就像是每个Bean的 “身份证”或“配置蓝图” ,它里面包含了Spring管理一个Bean所需的所有信息,比如:

  • Bean的类名 (className)
  • 作用域 (scope)
  • 依赖关系 (propertyValues, constructorArgumentValues)
  • 是否懒加载 (lazyInit)
  • 初始化/销毁方法 (initMethodName, destroyMethodName)
  • 等等…
2. “翻译官”的角色 —— BeanDefinitionReaderClassPathBeanDefinitionScanner

Spring之所以能支持多种配置方式,是因为它为每种方式都提供了一个专门的 “翻译官”。这些“翻译官”的唯一职责,就是读取特定格式的配置,并将其“翻译”成统一的BeanDefinition对象

  • 对于XML配置

    • Spring使用 XmlBeanDefinitionReader 这个类来担任“翻译官”。
    • 工作流程
      1. 它会加载并解析我们提供的XML文件。
      2. 对于XML文件中的每一个<bean>标签,它会读取其id, class, <property>, <constructor-arg>等所有属性和子标签。
      3. 然后,它会根据这些信息,在内存中创建一个与之对应的、内容完整的 BeanDefinition 对象。
  • 对于注解配置

    • Spring使用 ClassPathBeanDefinitionScanner 这个类来担任“翻译官”。
    • 工作流程
      1. 当我们使用@ComponentScan时,这个扫描器就会被激活。
      2. 它会扫描指定的包路径下所有的.class文件。
      3. 对于每一个带有@Component@Service@Controller等注解的类,它同样会解析这个类和其内部的@Autowired, @Scope, @Lazy等注解。
      4. 然后,它也会根据这些注解信息,在内存中创建一个与之对应的 BeanDefinition 对象。
3. 统一的“生产线”

一旦所有的配置(无论是来自XML还是注解)都被“翻译”成了统一的BeanDefinition格式,并被注册到容器内部的一个Map<String, BeanDefinition>中之后,对于后续的Bean生命周期管理(实例化、依赖注入、初始化等),整个流程就完全一样了

后续的“生产线”只关心BeanDefinition这张“标准蓝图”,它完全不知道也不关心这张蓝图最初是用XML语言写的,还是用注解语言写的。

总结

所以,注解和XML注入能达到相同的效果,其底层原理可以总结为:

  1. 引入统一的中间层:Spring设计了BeanDefinition这个统一的元数据结构,作为所有Bean配置的“标准格式”。
  2. 提供不同的解析器:Spring为XML和注解等不同的配置方式,提供了各自专属的解析器(ReaderScanner)。
  3. 殊途同归:所有不同来源的配置,在容器启动的早期阶段,都会被这些解析器 “翻译”并转换成统一的BeanDefinition对象
  4. 后续流程标准化:一旦进入BeanDefinition阶段,后续所有的Bean创建和管理流程,就都是标准化的、与配置方式无关的了。

这种 “屏蔽底层差异,提供统一抽象” 的设计思想,正是Spring框架能够如此灵活和可扩展的关键所在。


Spring给我们提供了很多扩展点,这些有了解吗?

面试官您好,是的,Spring框架一个非常强大的特点就是它的高度可扩展性。它在整个生命周期的各个关键节点,都为我们预留了丰富的扩展点。

我通常会把这些扩展点按照它们作用的范围和生命周期阶段,分为以下几大类:

第一类:影响容器启动和Bean定义的扩展点(在Bean实例化之前)

这类扩展点在容器的早期阶段工作,它们能够修改Bean的“设计蓝图”(BeanDefinition),影响力非常大。

  1. BeanFactoryPostProcessor

    • 作用时机:在Spring容器加载完所有的BeanDefinition之后,但在任何Bean实例被创建之前
    • 能做什么:它允许我们读取并修改BeanDefinition的元数据。比如,动态地修改某个Bean的属性值、改变其作用域、甚至替换其类定义。
    • 典型应用
      • Spring中的占位符替换(比如${...})就是通过它的一个子接口PropertySourcesPlaceholderConfigurer来实现的。它会扫描所有BeanDefinition,并将占位符替换为配置文件中的真实值。
      • MyBatis与Spring集成时,MapperScannerConfigurer也是一个BeanFactoryPostProcessor,它会扫描指定的DAO接口,并为它们动态地注册BeanDefinition
  2. ImportSelectorImportBeanDefinitionRegistrar

    • 作用时机:配合@Import注解,在解析配置类时被调用。
    • 能做什么:它们提供了动态地、按条件地向容器中注册BeanDefinition的能力。
    • 典型应用Spring Boot的自动配置大量使用了这种机制。@EnableAutoConfiguration就是通过ImportSelector,根据classpath中存在的jar包和配置,来智能地决定需要导入哪些自动配置类,从而动态地注册一大批我们需要的Bean。
第二类:影响单个Bean生命周期的扩展点(在Bean实例化之后)

这类扩展点作用于每一个Bean的创建过程中,是我们实现Bean级别定制化逻辑的关键。

  1. BeanPostProcessor (Bean后置处理器) —— 最重要、最常用的扩展点
    • 作用时机:它贯穿于每个Bean的初始化阶段前后
      • postProcessBeforeInitialization:在Bean的初始化方法(如@PostConstruct之前调用。
      • postProcessAfterInitialization:在Bean的初始化方法之后调用。
    • 能做什么:它能够对已经实例化并完成依赖注入的Bean实例进行“二次加工”。
    • 典型应用
      • Spring AOP:就是通过BeanPostProcessor(具体是AnnotationAwareAspectJAutoProxyCreator)在postProcessAfterInitialization阶段,为符合切点的Bean动态地创建一个代理对象,并用代理对象替换掉原始对象。
      • @Autowired等注解的实现:也是由特定的后置处理器来解析注解并完成依赖注入的。
      • 我们自己也可以用它来实现一些通用逻辑,比如对一批特定类型的Bean进行统一的属性校验或封装。
第三类:Spring MVC请求处理流程的扩展点

这类扩展点让我们能够干预Web请求的处理流程。

  1. HandlerInterceptor (拦截器)

    • 作用时机:它提供了三个方法,让我们可以在 Controller的方法执行前 (preHandle)、执行后 (postHandle),以及整个请求完成视图渲染后 (afterCompletion) 插入自定义逻辑。
    • 典型应用:实现用户登录状态校验、权限检查、日志记录、性能监控等。
  2. @ControllerAdvice / @RestControllerAdvice (全局处理器)

    • 作用时机:这是一个AOP思想的体现,用于全局性地增强Controller。
    • 典型应用
      • 全局异常处理:通过@ExceptionHandler注解,集中处理整个应用中Controller抛出的各种异常,返回统一的错误响应。
      • 全局数据绑定:通过@InitBinder,为所有Controller统一添加数据转换或格式化规则。
      • 全局数据预设:通过@ModelAttribute,为所有请求的模型(Model)统一添加一些公共的属性。
第四类:Spring Boot的自动化扩展点
  1. 自定义自动配置 (Auto-Configuration) 和起步依赖 (Starter)
    • 这是Spring Boot生态的核心。我们可以通过创建一个spring.factories文件(在新版中是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件),并编写自己的@Configuration类,来为某个第三方库或我们自己的通用组件,提供 “开箱即用”的自动配置能力
    • 这使得我们可以将复杂的配置逻辑封装起来,让使用者只需要引入一个starter依赖,就能零配置地使用我们的组件。

总结一下,Spring通过在框架生命周期的各个关键节点提供这些精心设计的扩展点,赋予了开发者极大的灵活性和控制力。在我的实践中, BeanPostProcessorHandlerInterceptor 是我用得最多的,它们分别解决了后端Bean处理和前端请求处理中最常见的定制化需求。

参考小林coding和JavaGuide

相关文章:

  • 2024蓝桥杯C/C++ B组国赛
  • EtherCAT转CANopen网关实现与伺服系统连通的配置实例探究
  • Spring Cache+Redis缓存方案 vs 传统redis缓存直接使用RedisTemplate 方案对比
  • Oracle集群OCR磁盘组掉盘问题处理
  • git pull 和 git fecth 的区别,远程仓库创建了新分支,可以用git fetch更新,可以看到远程创建的新分支
  • K8S中应用无法获取用户真实ip问题排查
  • 基于微信小程序的天气预报app
  • Vue 数据代理机制实现
  • BYC8-1200PQ超快二极管!光伏逆变/快充首选,35ns极速恢复,成本直降20%!
  • 3-16单元格区域尺寸调整(发货单记录保存-方法2)学习笔记
  • 3-15单元格偏移设置(发货单记录保存-方法1)学习笔记
  • 云原生核心技术 (12/12): 终章:使用 GitLab CI 将应用自动部署到 K8s (保姆级教程)
  • 力扣-121.买卖股票的最佳时机
  • Linux常用命令详解
  • 【PmHub面试篇】集成 Sentinel+OpenFeign实现网关流量控制与服务降级相关面试题解答
  • SSE 数据的传输无法流式获取
  • 全连接层和卷积层等效情况举例
  • 【知识图谱构建系列1】数据集介绍
  • Gogs:一款极易搭建的自助 Git 服务
  • TBrunReporter 测试生成报告工具使用教程(Windows)
  • 网站站内logo怎么做/搜索引擎营销的四种方式
  • 北京医疗网站建设/免费建站的网站哪个好
  • 做微信广告网站有哪些内容/软文营销平台
  • 小兔自助建站系统/推广策划
  • 主流网站开发技术/适合发表个人文章的平台
  • 华大基因背景调查/百度竞价是seo还是sem