Spring八股分析

Spring框架

1.SpringBoot的配置加载优先级

首先我们先确定一下配置加载优先级是按照我以下的顺序,由高到低的。分别是:

  1. 先是命令行参数--server.port=9000java -jar app.jar --spring.config.location=...

  2. 然后是我们的系统的环境变量和JVM系统属性,比如设置端口为8080,比如我们在这里设置API的KEY

  3. 然后RandomValuePropertySourcerandom.* 占位符,用于生成随机数/字符串,可在配置中引用)

  4. 接着是外部配置文件(properties / yml)

    • JAR 包外部的 ./config/

    • JAR 包外部的 ./

    • JAR 包内部的 classpath:/config/

    • JAR 包内部的 classpath:/

  5. 接着是我们@PropertySource注解指定的配置

  6. 最后是我们Springboot默认的配置

然后在配置文件中,properties的配置大于yml,因为springboot是按加载顺序来的,后加载的properties把yml的值给覆盖了

对于外部配置文件,查找路径的优先级为:

  1. ./config/(当前目录下的config目录)
  2. ./(当前目录)
  3. classpath:/config/
  4. classpath:/

实际应用:

基础配置:放在 classpath:/application.yml

环境特定配置:使用 application-{profile}.yml(如 application-prod.yml),通过 --spring.profiles.active=prod 激活

敏感信息:放在环境变量或外部化配置文件(避免入库)

临时调试/测试:使用命令行参数临时覆盖

多环境冲突处理:利用 profile 合并特性,公共配置放在 application.yml,环境差异放在对应 profile 文件

2.Springboot是如何解决跨域问题的?

基本都是基于CORS(跨域资源共享)通过设置响应头(如 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers)告诉浏览器允许访问。

对于复杂跨域请求(非 GET/POST/HEAD 或自定义头),浏览器会先发 OPTIONS 预检请求

  1. 局部注解,用@CrossOrigin标记单个接口,秒开跨域权限,适合快速测试。简单高效,优先级高于全局配置
  2. 全局配置,使用WebMvcConfigurer接口,统一设定允许的域名,请求方法,头信息。统一配置,但是不适合动态的控制
  3. 用CorsFilter手动处理跨域逻辑处理,适合需要动态校验权限等特殊场景,比如不同权限开放不同接口,在过滤器中动态判断,但是实现成本较高
  4. 在微服务架构中,也可以在网关层(如 Spring Cloud Gateway、Nginx)统一处理跨域,减少业务服务配置。

优先级:

1
@CrossOrigin` > `WebMvcConfigurer` > `CorsFilter

3.Spring 解决循环依赖

既然Spring能解决循环依赖,那为什么我们还经常听说‘构造器注入无法解决循环依赖’?三级缓存对构造器注入为什么无效?

您问到了Spring循环依赖解决方案的一个核心前提。三级缓存之所以能工作,其根本在于它将Bean的实例化(Instantiation)\和*属性填充(Population)*这两个阶段**分离开来了。

  • 第一步:实例化。Spring首先通过无参构造函数创建了Bean A的一个“空壳”实例。这个实例已经有了自己的内存地址。
  • 第二步:暴露早期引用。紧接着,Spring立即将这个“空壳”实例的工厂(ObjectFactory)放入三级缓存,从而提前暴露了A的引用。
  • 第三步:属性填充。然后Spring才开始尝试为A注入属性,此时发现需要B,就去创建B。当B需要A时,可以从三级缓存中获取到A的早期引用,从而打破循环。

    构造器注入的工作流程:

  • 对于构造器注入,Bean的实例化和属性填充这两个阶段是合并在一起的,是原子性的

  • 当Spring尝试创建Bean A时,它必须调用A的构造函数。而A的构造函数需要一个Bean B的实例作为参数。
  • 为了满足这个参数,Spring必须先去创建Bean B。
  • 而当Spring尝试创建Bean B时,又发现B的构造函数需要一个Bean A的实例作为参数。
  • 此时,Bean A的实例根本还没有被创建出来(它还卡在等待B的阶段),内存中不存在任何A的“空壳”实例,三级缓存中自然也就不可能有任何关于A的引用。
  • 这就形成了一个无法解开的死结:A的创建依赖B的创建,B的创建又依赖A的创建。因此,Spring会直接抛出BeanCurrentlyInCreationException

4.Bean的生命周期

依赖注入,三级缓存

流程:

  • 实例化 (Instantiation): Spring 通过反射创建 Bean 的实例。
  • 填充属性 (Populate Properties): Spring 注入 Bean 的依赖(DI)。
  • 初始化 (Initialization):
    • 调用各种 Aware 接口(如 BeanNameAware, BeanFactoryAware)。
    • 调用 BeanPostProcessor 的前置处理方法 (postProcessBeforeInitialization)。
    • 调用 @PostConstruct 注解的方法或 InitializingBeanafterPropertiesSet 方法。
    • 调用自定义的 init-method
    • 调用 BeanPostProcessor 的后置处理方法 (postProcessAfterInitialization)。<- AOP 代理发生在这里
  • 使用 (In Use): Bean 处于可用状态。
  • 销毁 (Destruction):
    • 调用 @PreDestroy 注解的方法或 DisposableBeandestroy 方法。
    • 调用自定义的 destroy-method

5.@Bean@Component 的区别?

面试官您好,@Component@Bean 都是向Spring IoC容器注册Bean的方式,但它们在使用场景和控制粒度上有本质区别:

  1. 注解目标不同:

    • @Component 是一个类级别的注解,Spring通过包扫描发现并自动注册为Bean。它还有三个衍生的注解 @Service, @Repository, @Controller,用于更清晰地划分业务分层。
    • @Bean 是一个方法级别的注解,通常用在 @Configuration 注解的配置类中。这个方法需要返回一个对象,Spring会将这个返回的对象注册为Bean。
  2. 使用场景不同:

    • @Component 用于我们自己编写的类,希望Spring自动管理它们时使用。
    • @Bean 主要用于第三方库的组件。因为我们无法修改第三方库的源码去添加@Component注解,所以通过@Bean方法可以显式地将其实例化并交给Spring管理。此外,当一个Bean的创建过程比较复杂,需要一些前置逻辑判断时,也适合用@Bean

    总结来说@Component 是让Spring自动发现,控制权在Spring;而@Bean 是我们主动声明,控制权在我们开发者手中,更加灵活。

引出 @Configuration: @Bean 必须在被 @Configuration@Component 注解的类中使用。可以进一步说明 @ConfigurationproxyBeanMethods 属性,来体现你对Spring底层代理的理解。

关于@ConfigurationproxyBeanMethods属性,这其实是深入理解Spring IoC容器核心原理的一个关键点。它控制着Spring是否要为我们的配置类创建一个CGLIB代理,从而影响Bean之间的依赖注入行为

我们可以分两种情况来看,也就是proxyBeanMethodstrue(默认值)和false时,Spring的行为有何不同。

proxyBeanMethods = true (Full模式)

这是@Configuration的默认行为。在这种模式下,Spring在启动时会使用CGLIB动态代理技术,为我们的配置类(比如AppConfig)创建一个代理子类,并把这个代理子类放入IoC容器中。这个代理的核心作用是拦截所有对@Bean方法的调用

当Spring容器初始化beanA时,它会调用beanA()方法。当代码执行到beanB()时,因为AppConfig是一个代理对象,这个调用会被代理拦截。代理会检查容器里是否已经存在一个名为beanB的单例Bean。

  • 如果存在,代理会直接返回容器中那个已经存在的beanB实例。
  • 如果不存在,它才会执行真正的beanB()方法体,创建一个新的BeanB实例,将它注册到容器中,然后再返回。

在Full模式下,无论你在配置类内部调用@Bean方法多少次,Spring总能保证你拿到的是容器中那个唯一的、正确的单例Bean实例。这保证了Bean依赖关系的正确性,我们称之为‘容器内的单例保证’

proxyBeanMethods = false (Lite模式)

当我们将它设置为false时,情况就完全不同了。Spring不会为配置类创建CGLIB代理,容器中的AppConfig就是一个普通的Java对象。

在这种模式下,当Spring初始化beanA时,调用beanA()方法。当代码执行到beanB()时,由于没有代理拦截,这就变成了一次普通的Java方法调用。它会直接执行new BeanB(),创建一个全新的BeanB对象。”

这意味着,beanA所依赖的那个BeanB实例,和Spring容器中独立注册的那个名为beanB的Bean实例,是两个完全不同的对象!这就破坏了Bean的单例作用域。

  • 当你的配置类中,Bean之间存在相互依赖关系时,比如beanA的创建依赖于调用beanB()方法。你必须使用默认的true来保证依赖注入的是容器中的单例Bean。
  • 当你的配置类中,所有的@Bean方法都是独立的,彼此之间没有任何调用关系。在这种情况下,设置为false可以跳过CGLIB代理的创建过程,能够提升Spring的启动性能,减少内存占用。事实上,Spring Boot的很多自动配置类(Auto-Configuration)在可能的情况下都会选择使用Lite模式来优化性能。

6.Spring Boot 的启动流程是怎样的?自动配置的原理是什么。

Spring Boot 的启动始于 main 方法中的 SpringApplication.run()

  1. 创建 SpringApplication 实例:初始化一个 SpringApplication 对象,这个过程会判断应用类型(如 Servlet Web 应用),并从 META-INF/spring.factories 中加载 ApplicationContextInitializerApplicationListener
  2. 执行 run 方法
    • 准备并配置 Environment(加载 application.properties/yml 等配置文件)。
    • 创建 ApplicationContext(Spring 容器)。
    • 准备上下文 prepareContext:加载所有 Bean 的定义信息,包括通过自动配置加载的。
    • 刷新上下文 refreshContext:这是最核心的一步,它会触发 Spring 容器的刷新过程,实例化并初始化所有单例 Bean。如果是 Web 应用,内嵌的 Web 服务器(如 Tomcat)也是在这个阶段启动的。
    • 执行 Runner:调用所有实现了 CommandLineRunnerApplicationRunner 接口的 Bean 的 run 方法。
  3. 启动完成:应用开始接收和处理请求。

自动配置原理: 自动配置是 Spring Boot 的“约定优于配置”理念的核心体现。

  1. @EnableAutoConfiguration 注解@SpringBootApplication 注解中包含了 @EnableAutoConfiguration,这是开启自动配置的总开关。
  2. 加载配置类@EnableAutoConfiguration 通过 @Import 注解导入了 AutoConfigurationImportSelector 类。这个类会去扫描所有引入的 jar 包中 META-INF/spring.factories 文件。
  3. spring.factories 文件:在这个文件中,有一个 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的键,其值是一个逗号分隔的自动配置类列表(如 DataSourceAutoConfiguration, WebMvcAutoConfiguration 等)。
  4. 条件化配置 (@Conditional):每一个自动配置类都使用了大量的条件注解(如 @ConditionalOnClass, @ConditionalOnBean, @ConditionalOnMissingBean)。
    • 例如,DataSourceAutoConfiguration 会使用 @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) 来判断类路径下是否存在数据库驱动。如果存在,它才会尝试配置一个 DataSource Bean。
    • 同时,它还会使用 @ConditionalOnMissingBean(DataSource.class),这意味着如果开发者自己已经配置了一个 DataSource Bean,那么自动配置就会失效,从而给予开发者最高的控制权。