Spring八股分析

Spring八股分析
mengnankkzhouSpring框架
1.SpringBoot的配置加载优先级
首先我们先确定一下配置加载优先级是按照我以下的顺序,由高到低的。分别是:
先是命令行参数(
--server.port=9000或java -jar app.jar --spring.config.location=...)然后是我们的系统的环境变量和JVM系统属性,比如设置端口为8080,比如我们在这里设置API的KEY
然后
RandomValuePropertySource(random.*占位符,用于生成随机数/字符串,可在配置中引用)接着是外部配置文件(properties / yml)
JAR 包外部的
./config/JAR 包外部的
./JAR 包内部的
classpath:/config/JAR 包内部的
classpath:/
接着是我们@PropertySource注解指定的配置
最后是我们Springboot默认的配置
然后在配置文件中,properties的配置大于yml,因为springboot是按加载顺序来的,后加载的properties把yml的值给覆盖了
对于外部配置文件,查找路径的优先级为:
./config/(当前目录下的config目录)./(当前目录)classpath:/config/classpath:/
实际应用:
基础配置:放在 classpath:/application.yml
环境特定配置:使用 application-{profile}.yml(如 application-prod.yml),通过 --spring.profiles.active=prod 激活
敏感信息:放在环境变量或外部化配置文件(避免入库)
临时调试/测试:使用命令行参数临时覆盖
多环境冲突处理:利用 profile 合并特性,公共配置放在 application.yml,环境差异放在对应 profile 文件
2.Springboot是如何解决跨域问题的?
基本都是基于CORS(跨域资源共享)通过设置响应头(如 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers)告诉浏览器允许访问。
对于复杂跨域请求(非 GET/POST/HEAD 或自定义头),浏览器会先发 OPTIONS 预检请求。
- 局部注解,用@CrossOrigin标记单个接口,秒开跨域权限,适合快速测试。简单高效,优先级高于全局配置
- 全局配置,使用WebMvcConfigurer接口,统一设定允许的域名,请求方法,头信息。统一配置,但是不适合动态的控制
- 用CorsFilter手动处理跨域逻辑处理,适合需要动态校验权限等特殊场景,比如不同权限开放不同接口,在过滤器中动态判断,但是实现成本较高
- 在微服务架构中,也可以在网关层(如 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注解的方法或InitializingBean的afterPropertiesSet方法。 - 调用自定义的
init-method。 - 调用
BeanPostProcessor的后置处理方法 (postProcessAfterInitialization)。<- AOP 代理发生在这里
- 调用各种 Aware 接口(如
- 使用 (In Use): Bean 处于可用状态。
- 销毁 (Destruction):
- 调用
@PreDestroy注解的方法或DisposableBean的destroy方法。 - 调用自定义的
destroy-method。
- 调用
5.@Bean 和 @Component 的区别?
面试官您好,@Component 和 @Bean 都是向Spring IoC容器注册Bean的方式,但它们在使用场景和控制粒度上有本质区别:
注解目标不同:
@Component是一个类级别的注解,Spring通过包扫描发现并自动注册为Bean。它还有三个衍生的注解@Service,@Repository,@Controller,用于更清晰地划分业务分层。@Bean是一个方法级别的注解,通常用在@Configuration注解的配置类中。这个方法需要返回一个对象,Spring会将这个返回的对象注册为Bean。
使用场景不同:
@Component用于我们自己编写的类,希望Spring自动管理它们时使用。@Bean主要用于第三方库的组件。因为我们无法修改第三方库的源码去添加@Component注解,所以通过@Bean方法可以显式地将其实例化并交给Spring管理。此外,当一个Bean的创建过程比较复杂,需要一些前置逻辑判断时,也适合用@Bean。
总结来说,
@Component是让Spring自动发现,控制权在Spring;而@Bean是我们主动声明,控制权在我们开发者手中,更加灵活。
引出 @Configuration: @Bean 必须在被 @Configuration 或 @Component 注解的类中使用。可以进一步说明 @Configuration 的 proxyBeanMethods 属性,来体现你对Spring底层代理的理解。
关于@Configuration的proxyBeanMethods属性,这其实是深入理解Spring IoC容器核心原理的一个关键点。它控制着Spring是否要为我们的配置类创建一个CGLIB代理,从而影响Bean之间的依赖注入行为
我们可以分两种情况来看,也就是proxyBeanMethods为true(默认值)和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()。
- 创建
SpringApplication实例:初始化一个SpringApplication对象,这个过程会判断应用类型(如 Servlet Web 应用),并从META-INF/spring.factories中加载ApplicationContextInitializer和ApplicationListener。 - 执行
run方法:- 准备并配置
Environment(加载application.properties/yml等配置文件)。 - 创建
ApplicationContext(Spring 容器)。 - 准备上下文
prepareContext:加载所有 Bean 的定义信息,包括通过自动配置加载的。 - 刷新上下文
refreshContext:这是最核心的一步,它会触发 Spring 容器的刷新过程,实例化并初始化所有单例 Bean。如果是 Web 应用,内嵌的 Web 服务器(如 Tomcat)也是在这个阶段启动的。 - 执行 Runner:调用所有实现了
CommandLineRunner和ApplicationRunner接口的 Bean 的run方法。
- 准备并配置
- 启动完成:应用开始接收和处理请求。
自动配置原理: 自动配置是 Spring Boot 的“约定优于配置”理念的核心体现。
@EnableAutoConfiguration注解:@SpringBootApplication注解中包含了@EnableAutoConfiguration,这是开启自动配置的总开关。- 加载配置类:
@EnableAutoConfiguration通过@Import注解导入了AutoConfigurationImportSelector类。这个类会去扫描所有引入的 jar 包中META-INF/spring.factories文件。 spring.factories文件:在这个文件中,有一个org.springframework.boot.autoconfigure.EnableAutoConfiguration的键,其值是一个逗号分隔的自动配置类列表(如DataSourceAutoConfiguration,WebMvcAutoConfiguration等)。- 条件化配置 (
@Conditional):每一个自动配置类都使用了大量的条件注解(如@ConditionalOnClass,@ConditionalOnBean,@ConditionalOnMissingBean)。- 例如,
DataSourceAutoConfiguration会使用@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })来判断类路径下是否存在数据库驱动。如果存在,它才会尝试配置一个DataSourceBean。 - 同时,它还会使用
@ConditionalOnMissingBean(DataSource.class),这意味着如果开发者自己已经配置了一个DataSourceBean,那么自动配置就会失效,从而给予开发者最高的控制权。
- 例如,












