Spring框架常见面试题

Spring框架中的单例bean是线程安全的吗?

答案显然是,但是为啥我们平时使用的是并没刻意去注意线程安全问题呢?其实最主要的原因是在于我们平时使用的时候大多注入的是无状态的Bean对象,无状态的Bean对象天然就是线程安全的。

无状态和有状态

简单来说:无状态 Bean 像自动售货机(谁来都一样),有状态 Bean 像私人理发师(记得你的具体偏好)。

  1. 核心区别对比表
特性无状态 Bean (Stateless)有状态 Bean (Stateful)
定义不保存特定客户端的数据,每次调用都是独立的。保存了特定客户端或会话的数据,方法调用之间有关联。
实例变量通常没有成员变量,或者只有不可变的配置/依赖(如 Service, DAO)。有成员变量用于存储数据(如用户购物车、计数器)。
线程安全天然线程安全。多个线程可以共享同一个实例。线程不安全。通常不能在多线程间共享同一个实例。
作用域 (Scope)Singleton (单例) - 全局只需要一个实例。Prototype (原型), Session, Request - 每个用户/请求需要新实例。
性能/开销性能高,内存占用小(实例少)。开销较大,需要创建和销毁大量实例。
典型场景Service 层, DAO 层, Controller (大多情况), 工具类。购物车, 向导式表单, 用户会话信息 (Session)。
  1. 无状态 Bean (Stateless Bean)

定义: 一个 Bean 如果不包含任何可变的成员变量(Instance Variable),或者虽然有成员变量但它们是不可变的(如注入的 Service 或 DAO),那么它就是无状态的。

特点:

  • 一次性逻辑:它的方法执行只依赖于传入的参数,不依赖于上一次调用的结果。
  • 高并发友好:因为没有“私有数据”,所以在 Spring 中,默认的 Bean(Singleton)都是无状态的。Spring 容器只需要创建一个实例,就可以服务成千上万个并发请求。
// 这是一个典型的无状态 Bean
@Service
public class UserService {

    // 这是一个依赖,通常是单例且不可变的,不属于"用户状态"
    @Autowired
    private UserRepository userRepository;

    public User findUser(Long id) {
        // 方法内的局部变量是线程安全的(栈内存),不会影响 Bean 的状态
        return userRepository.findById(id);
    }
}
  1. 有状态 Bean (Stateful Bean)

    定义: 一个 Bean 如果包含成员变量,并且这些变量用于存储特定客户端在多次调用之间产生的数据,那么它就是有状态的。

    特点:

    • 记忆能力:它“记得”你上一次调用做了什么。
    • 独占性:张三的 Bean 不能给李四用,否则李四会看到张三的数据。因此,通常需要为每个用户创建一个新的 Bean 实例(@Scope("prototype"))。
    // 这是一个有状态的 Bean
    // 注意:在 Spring 单例模式下直接这样写会导致严重的线程安全问题!
    // 除非将其 Scope 设置为 prototype 或 session
    @Component
    @Scope("prototype") 
    public class ShoppingCart {
    
        // 这是一个状态!每个用户的购物车内容不同
        private List<String> items = new ArrayList<>();
    
        public void addItem(String item) {
            this.items.add(item);
        }
    
        public List<String> getItems() {
            return this.items;
        }
    }
    

注意一下:在日常开发过程中,一般都是直接使用无状态的Bean即可,有状态的Bean很少使用,至少我还没用过。

AOP

概念:AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

  • 常见的AOP使用场景:
    • 记录操作日志
    • 缓存处理
    • Spring中内置的事务处理

示例:

第一步:引入依赖

如果你使用的是 Spring Boot,只需要引入 AOP 的 Starter,它会自动配置好 AspectJ 相关的库。

maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

gradle:

implementation 'org.springframework.boot:spring-boot-starter-aop'

第二步:定义切面类 (Aspect)

创建一个普通的 Java 类,加上 @Aspect@Component 注解。

  • @Aspect:告诉 Spring 这是一个切面类。
  • @Component:将其注册为 Spring 容器中的一个 Bean。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
@Component
public class LogAspect {

    private final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    /**
     * 1. 定义切点 (Pointcut)
     * 表达式含义:execution(修饰符 返回值 包名.类名.方法名(参数))
     * 这里表示:拦截 com.example.service 包下所有类的所有方法
     */
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void serviceLayer() {}

    /**
     * 2. 前置通知 (Before Advice)
     * 在目标方法执行之前执行
     */
    @Before("serviceLayer()")
    public void doBefore() {
        logger.info("========== 开始执行 Service 方法 ==========");
    }

    /**
     * 3. 环绕通知 (Around Advice) - 最常用,功能最强
     * 它可以控制目标方法是否执行,还能修改返回值,计算时间等
     */
    @Around("serviceLayer()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        // 获取方法签名
        String methodName = joinPoint.getSignature().getName();
        // 获取参数
        Object[] args = joinPoint.getArgs();

        try {
            // *** 核心:执行目标方法 ***
            Object result = joinPoint.proceed(); 
            
            long end = System.currentTimeMillis();
            logger.info("方法 [{}] 执行耗时: {} ms", methodName, (end - start));
            
            return result; // 返回目标方法的执行结果
        } catch (Throwable e) {
            logger.error("方法 [{}] 执行出错: {}", methodName, e.getMessage());
            throw e; // 抛出异常,否则调用方以为执行成功了
        }
    }
}

第三步:理解核心概念 (关键词)

为了能灵活使用,你需要理解上面代码中的几个关键术语:

  1. 切面 (Aspect):也就是上面的 LogAspect 类,是包含切点和通知的模块。
  2. 切点 (Pointcut):定义“在哪里”执行切面逻辑。
    • 常用表达式:execution(* com.service.*.*(..))
    • 另一种常用方式:@annotation(com.example.MyLog) (只拦截加了特定注解的方法)。
  3. 通知 (Advice):定义“做什么”以及“什么时候做”。
    • @Before:方法前。
    • @AfterReturning:方法成功返回后。
    • @AfterThrowing:抛出异常后。
    • @After:无论成功失败,最终都会执行(类似 finally)。
    • @Around最强大,包围整个方法,能决定是否执行目标方法。
  4. 连接点 (JoinPoint):程序执行的某个具体位置。在 Spring AOP 中,总是指方法的执行。

实际开发中我一般还是会配合注解来是先AOP。步骤也很简单:

第一步定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLog {
    String value() default "";
}

第二步修改切点表达式:

// 只要方法上加了 @AutoLog 注解,就会被拦截
@Pointcut("@annotation(com.example.annotation.AutoLog)")
public void annotationPointcut() {}

第三步使用:

@Service
public class UserService {
    
    @AutoLog("查询用户详情") // 只有加了这个注解的方法才会被 AOP 处理
    public User getUser(Long id) {
        return userRepository.findById(id);
    }
}

注意事项 (非常重要!)

  1. AOP 失效问题(自调用): Spring AOP 是基于代理 (Proxy) 实现的。如果在同一个类中,方法 A 调用了方法 B,而方法 B 上配了 AOP,AOP 是不会生效的
    • 原因this.methodB() 是直接调用对象内部方法,绕过了 Spring 生成的代理对象。
  2. 访问权限: AOP 通常只能拦截 public 方法。虽然通过配置可以拦截 protected/private,但不推荐。

Spring事务管理以及事务失效场景

详情见:聊聊@Transactional注解和事务的使用

Spring中Bean的生命周期

这个问题是牢中牢,太夸张了。我在这里梳理一下这个问题的答案。

我们先看一个Bean的生命周期主要包括哪些部分:

image-20251222174827768

但是你觉得spring的bean就这么简单几步?其实主要的就这么几步,还有一些细节的操作。完整的流程图如图所示:

image-20251222182322053

注意一下,两幅图有对应关系的哦。

Spring Bean 生命周期详细步骤

1. 实例化阶段:

  • Bean 的实例化是通过反射机制创建的。Spring 根据 @Component@Bean 或者 XML 中的 <bean> 元素配置,来确定要创建的 Bean。

2. 属性赋值阶段:

  • 在实例化完成后,Spring 会进行依赖注入。这包括将属性值注入到 Bean 的字段中,可能是通过 setter 方法注入,或者直接字段注入。

3. 初始化前的扩展机制:

  • Bean 可以实现 BeanNameAwareBeanFactoryAwareAware 接口,从而在初始化之前获取 Bean 的名称、BeanFactory、ApplicationContext 等容器资源。例如,ApplicationContextAware 接口允许 Bean 获取 ApplicationContext,以便进一步与 Spring 容器交互。

4. BeanPostProcessor 的作用:

  • BeanPostProcessor 接口允许开发者在 Bean 初始化前后添加自定义逻辑。例如,可以在 postProcessBeforeInitialization 方法中执行某些前置操作,如代理包装、AOP 切面等。在 postProcessAfterInitialization 中,可以进一步修改或替换 Bean 实例。

5. 初始化的细节:

  • InitializingBean 接口提供了一个 afterPropertiesSet 方法,用于在 Bean 的所有属性设置完成后执行一些自定义初始化逻辑。开发者也可以通过 @PostConstruct 注解或者 XML/Java 配置中的 init-method 属性,来指定初始化方法。

6. Bean 的就绪状态:

  • Bean 完成初始化后,即进入就绪状态,可以供应用程序使用。在此状态下,Bean 已经完成了所有的属性设置和初始化步骤,处于可用状态。

7. 销毁阶段的清理:

  • Bean 的销毁通常在容器关闭时进行。DisposableBean 接口提供了 destroy 方法,用于清理资源。开发者也可以通过 @PreDestroy 注解或配置中的 destroy-method 属性,指定清理逻辑。

Bean 的生命周期扩展点汇总

Spring 提供了多个扩展点,让开发者可以自定义和控制 Bean 的生命周期:

1. BeanPostProcessor

  • 通过实现 BeanPostProcessor 接口,开发者可以在 Bean 初始化前后添加自定义逻辑,如动态代理、AOP 增强等。

2.BeanFactoryPostProcessor

  • BeanFactoryPostProcessor 允许开发者在 Bean 实例化之前,修改 Bean 的定义信息(如属性值),它在所有 Bean 实例化之前执行。

3.Aware 接口

  • Spring 提供了多个 Aware 接口,如 BeanNameAwareBeanFactoryAwareApplicationContextAware 等,允许 Bean 获取 Spring 容器的相关信息,进一步定制生命周期。

4.@PostConstruct 和 @PreDestroy

  • 这些注解提供了一种声明式的方法来定义初始化和销毁逻辑,通常用于替代 XML 或 Java 配置中的 init-methoddestroy-method

Spring三级缓存解决循环依赖

一级缓存

  • singletonObjects,单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
  • 作用:限制bean在beanFactory中只存一份,即实现singleton scope,解决不了循环依赖

二级缓存

  • earlySingletonObject,缓存早期的bean对象(生命周期还没走完)
  • 作用:配合一级缓存实现普通对象的循环依赖,代理对象的循环依赖还不能解决

三级缓存

  • singletonFactories,缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的
  • 作用:实例化对象,如果对象是代理对象,则生成代理对象,如果不是则生成普通对象
//单实例对象注册器 
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {     
    private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;     
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);     
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);     
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
 }

二级缓存辅助解决普通对象循环依赖图示:

image-20251222185452816

三级缓存解决代理对象循环依赖

image-20251222190453156

  • 构造方法注入出现循环依赖

    • 解决:加@Lazy注解

      @Component public class A {      
      // B成员变量   
          private B b;      
          public A(@Lazy B b){         
              System.out.println("A的构造方法执行了...");         
              this.b = b ;
           }
       }
       
      @Component public class B {  
          // A成员变量   
          private A a;      
          public B(A a){
               System.out.println("B的构造方法执行了...");
               this.a = a ;     
          }
       }
      

SpringMVC的执行流程

这里有两种,一种是前后端不分离的,我不写,如果你面试的还是前后端不分离的,我建议你换一家,没有意义。纯傻逼。

  • 用户发送出请求到前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
  • HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。
  • DispatcherServlet调用HandlerAdapter(处理器适配器)
  • HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
  • 方法上添加了@ResponseBody
  • 通过HttpMessageConverter来返回结果转换为JSON并响应

image-20251222191638078

Spring Boot自动装配的原理

我这里只说Spring Boot3的,毕竟现在Spring Boot3是主流的开发技术。

Spring Boot 3 的自动装配(Auto-configuration)是其“约定大于配置”核心理念的基石。简单来说,它能根据你项目中引入的依赖(Jar包),自动将需要的 Bean 配置到 Spring 容器中,省去了大量手动编写配置文件的麻烦。

其核心原理可以概括为:通过 SPI 机制,加载 META-INF/spring 下的配置文件,并根据条件注解按需注入。

1. 核心注解:@SpringBootApplication

自动装配的入口在于启动类上的 @SpringBootApplication 注解,它实际上是一个复合注解,其中最关键的是 @EnableAutoConfiguration

  • @SpringBootConfiguration: 标记这是一个配置类。
  • @ComponentScan: 扫描当前包及其子包下的组件(@Component, @Service等)。
  • @EnableAutoConfiguration: 真正开启自动装配的开关。

2. 执行流程:从注解到 Bean

Spring Boot 启动时,自动装配遵循以下关键步骤:

第一步:引入选择器

@EnableAutoConfiguration 注解通过 @Import 引入了 AutoConfigurationImportSelector 类。这个类的作用是帮助 Spring 寻找并筛选出需要自动配置的类。

第二步:读取配置文件 (SPI 机制)

在 Spring Boot 3 中,传统的 spring.factories 文件已被弃用(虽然目前版本仍做兼容,但已不是主流)。

  • 新位置:Spring Boot 3 优先读取资源目录下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。
  • 内容:该文件中列出了所有可选的自动配置全限定类名(例如 JdbcTemplateAutoConfiguration)。

第三步:按需过滤 (Condition 机制)

并不是配置文件里所有的类都会被加载。Spring Boot 会利用 Conditional 注解(条件装配)进行过滤:

  • @ConditionalOnClass:classpath下有指定的类才装配。
  • @ConditionalOnMissingBean:容器中没有这个 Bean 时才装配(方便用户自定义覆盖)。
  • @ConditionalOnProperty:配置文件中指定的属性为特定值时才装配。