Spring面试攻略:如何展现你对Spring的深入理解
阅读原文时间:2023年08月15日阅读:1

什么是Spring?谈谈你对IOC和AOP的理解。

Spring是一种Java开发框架,旨在简化企业级应用程序的开发和部署。它具有以下优点:

  1. 对象托管:Spring能够管理和赋值所有对象,使开发人员不再需要手动管理对象的创建和依赖关系。
  2. 动态代理:Spring的动态代理功能可以实现大部分可复用的逻辑功能,从而避免了重复的代码。
  3. 强大的框架生态系统:市面上几乎所有的框架都是基于Spring构建的,Spring提供了强有力的支持和集成能力。
  4. 低侵入性:Spring的代码对于我们的应用代码几乎是无侵入的,只需要使用几个注解就能让Spring启动。

控制反转(IoC)是Spring的一个重要特性,它使得对象的创建和依赖关系的管理由Spring容器来完成。IoC有三种实现方式:注解形式、构造器形式和set方法注入。通过IoC,我们不再需要使用new关键字手动创建对象,而是将对象的创建和管理交给Spring容器处理。

面向切面编程(AOP)是Spring的另一个重要特性,它通过动态代理实现。AOP常用于日志收集、事务管理等方面。通过AOP,我们可以在被代理对象的方法执行前后,加入一些统一的业务逻辑处理,例如日志记录或权限校验。

Spring容器的启动流程是怎么样的?

启动流程几乎跟源码息息相关,如果没有看过源码可能对启动流程只能靠自己的理解去背,如果对源码右深入理解,那么这道题可以这么说:

1:初始化reader和scanner

2:使用scanner组件扫描basePackage下的所有对象,将配置类的BeanDefinition注册到容器中。

3:refresh(); 刷新容器。

可以分为以下几个步骤:

  • 定位配置文件:Spring容器首先需要定位配置文件的位置和名称。可以通过ClassPathXmlApplicationContext、FileSystemXmlApplicationContext等类来指定配置文件的路径。
  • 加载配置文件:Spring容器会读取配置文件的内容,并将其转换为内部的数据结构,一般使用DOM或SAX解析XML文件。
  • 创建容器对象:根据配置文件中的定义,Spring容器会创建一个或多个容器对象。常用的容器对象是ApplicationContext和BeanFactory。ApplicationContext是BeanFactory的子接口,提供了更多的功能。
  • 注册Bean定义:Spring容器会解析配置文件中的Bean定义,包括Bean的名称、类型、依赖关系等,并将其注册到容器中。这些Bean定义会被封装为BeanDefinition对象。
  • 实例化Bean:根据注册的Bean定义,Spring容器会实例化相应的Bean对象,并将其放入容器中管理。实例化可以通过构造方法、工厂方法或者通过AOP代理来完成。
  • 注入依赖:Spring容器会根据Bean定义中的依赖关系,将相应的依赖注入到Bean中。可以通过构造方法、setter方法或者注解方式来完成注入。
  • 调用初始化方法:如果Bean定义中指定了初始化方法,Spring容器会在实例化和依赖注入完成后,调用Bean的初始化方法。可以通过配置文件或者注解指定初始化方法。
  • 容器就绪:当所有的Bean都被实例化、注入依赖并初始化完成后,Spring容器就处于就绪状态,可以提供相应的服务。

然后在细说自己知道的部分源码,比如我还了解到一些关于源码的细节。例如,在获取Bean定义后,Spring会在实例化之前通过合并Bean定义来进行初始化,并且AOP的逻辑是在初始化之后通过后置处理器进行动态代理。

此外,如果我们需要监听Spring的启动过程以及在启动后实现自己的业务逻辑,除了可以使用初始化对象的方法afterPropertiesSet外,还可以通过注册一个监听器来监听Spring发布的各种事件。这样,我们就可以在特定的事件触发时执行我们自己的逻辑。

Spring框架中Bean的创建过程是怎样的?

在Spring框架中,Bean的创建过程涉及到多个环节和细节。下面我将更详细地介绍每个步骤的具体内容。可以大致分为五个步骤:获取Bean定义、实例化、赋值、初始化和销毁。

  1. Bean定义是在包扫描的过程中进行注册的。通过扫描配置文件或使用注解等方式,将Bean的定义信息注册到Spring容器中。
  2. 根据Bean定义进行实例化。这一步骤会根据Bean的构造器创建对象实例,如果没有指定构造器,则使用默认的构造器。在实例化过程中,还可以应用前后实例化处理器对对象进行一些额外的处理。
  3. 在赋值阶段,会对Bean中的依赖进行赋值。这可以通过使用@Autowired注解注入依赖属性来实现。赋值完成后,Bean的属性就可以访问到依赖的对象。
  4. 通过调用init-method(@PostConstruct)方法或实现InitializingBean接口(afterPropertiesSet)来进行初始化操作。在初始化过程中,可以执行一些特定的逻辑,例如数据加载等。同样地,也可以应用前后初始化处理器对Bean进行一些额外的处理。
  5. 销毁阶段是在应用程序关闭或需要销毁Bean时执行的。通过调用destroy-method方法(@PreDestroy),可以执行对象的销毁逻辑,DisposableBean: 当Bean实现了这个接口,在对象销毁前就会调用destory()方法。

Spring框架中的Bean是线程安全的吗?如果线程不安全,要如何处理

Spring框架中的Bean默认是单例模式,因此不是线程安全的。如果要处理线程安全问题,可以采取以下几种方式:

  • 使用prototype作用域:通过将Bean的作用域设置为prototype,确保每次请求都创建一个新的对象,从而保证线程安全。
  • 避免使用全局资源属性:尽量避免在Bean中使用全局资源属性,而是使用无状态的属性,比如使用注解形式的Bean注入。
  • 使用ThreadLocal类:可以使用ThreadLocal类将属性与线程进行绑定,确保每个线程独有一份属性副本,从而避免线程安全问题。

Spring如何处理循环依赖问题?

大家都知道spring采用的是三级缓存,那么如何理解三级缓存处理了循环依赖问题呢?

一级缓存:缓存最终的单例池对象: private final Map singletonObjects = new ConcurrentHashMap<>(256);

二级缓存:缓存初始化的对象:private final Map earlySingletonObjects = new ConcurrentHashMap<>(16);

三级缓存:缓存对象的ObjectFactory: private final Map> singletonFactories = new HashMap<>(16);

其实当一个对象实例化后就会存储在 singletonFactories三级缓存,当被引用时,会执行一个后置处理器方法,这里也是给aop创建代理对象的时机:

  protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
        Object exposedObject = bean;
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
                exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
        return exposedObject;
    }

如果是正常的普通对象,会直接进行如二级缓存,并返回一个实例化后的对象,所以之所以使用到了三级缓存,而不是光是用二级缓存就是考虑到了循环依赖可能是一个代理对象,我们无法直接提供实例化的对象而是一个代理对象。

    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // Quick check for existing instance without full singleton lock
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                synchronized (this.singletonObjects) {
                    // Consistent creation of early reference within full singleton lock
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
                            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
                                singletonObject = singletonFactory.getObject();
                                this.earlySingletonObjects.put(beanName, singletonObject);
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }
        return singletonObject;
    }

Spring如何处理事务?

Spring框架提供了两种方式来处理事务:编程式事务和声明式事务。

编程式事务是通过使用TransactionTemplate来进行事务管理的方式。它需要手动在代码中显式地指定事务的开启、提交和回滚操作。这种方式对代码的侵入性较高,因为需要在每个需要进行事务管理的方法中编写事务处理的代码。一般情况下,不推荐使用编程式事务,除非在特定的场景下需要对事务进行更精细的控制。

声明式事务是通过使用注解或XML配置的方式来声明事务的行为。在Spring中,最常用的是使用注解来声明事务。通过在方法或类上添加@Transactional注解,可以指定事务的传播行为、隔离级别、超时时间等属性。事务的传播行为指的是当一个方法调用另一个带有事务注解的方法时,事务应该如何进行传播和管理。

Spring框架提供了以下几种事务的传播级别:

  • REQUIRED(默认值):如果当前存在事务,则加入该事务,如果不存在事务,则创建一个新的事务。
  • REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。
  • SUPPORTS:如果当前存在事务,则加入该事务,如果不存在事务,则以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将当前事务挂起。
  • MANDATORY:当前必须存在事务,否则抛出异常。
  • NEVER:当前不能存在事务,否则抛出异常。
  • NESTED:如果当前存在事务,则在一个新的嵌套事务中执行。如果不存在事务,则执行与REQUIRED一样的操作。

Spring框架提供了以下几种事务的隔离级别:

  • READ_UNCOMMITTED(读未提交):此级别最低,允许读取其他事务尚未提交的数据,可能会读取到脏数据。
  • READ_COMMITTED(读已提交):在一个事务内,只能读取到已经提交的数据,避免了脏数据,但是可能会产生幻读。
  • REPEATABLE_READ(可重复读):在一个事务内,多次读取同一数据结果保持一致,解决了幻读的问题。
  • SERIALIZABLE(串行化):最高的隔离级别,所有事务都是串行执行,避免了并发问题,但并发度最低。

SpringMVC中的控制器是不是单例模式?如果是,如何保证线程安全?

在Spring MVC中,默认情况下,控制器是以单例模式创建的。这意味着在应用程序的整个生命周期中,只会创建一个控制器实例来处理所有的请求。为了保证控制器的线程安全性,可以采取以下措施:

1:保持控制器的无状态属性:控制器应该尽量避免使用实例变量来保存状态信息,尽量使用方法参数或局部变量来处理请求。这样可以确保每个请求都有独立的数据副本,避免多个线程之间的竞争和冲突。

2:设置控制器的作用域为非单例模式:可以将控制器的作用域设置为非单例模式,如prototype或request。这样每次请求都会创建一个新的控制器实例,确保每个请求都有独立的控制器对象,避免线程安全问题。

总结

本次面试涉及了Spring框架的多个方面,包括IOC和AOP的理解、Spring容器的启动流程、Bean的创建过程、Bean的线程安全性、循环依赖的处理、事务的处理以及Spring MVC中控制器的线程安全性。通过这些问题的回答,展示了对Spring框架的深入理解和应用经验。同时,也凸显了对面试题目的认真思考和清晰表达的能力。