十九、Spring

88 阅读17分钟

在Spring面试中,面试官可能会询问以下内容以评估应聘者对Spring框架的理解和应用能力:

53203676117e467388a1c0a6120c712f~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp.png

Spring框架的基础知识

包括Spring的定义、目的、以及它如何提高开发效率和系统的可维护性。Spring是一个轻量级的框架,它由多个模块组成,如核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块等‌。

Spring模块的详细介绍

需要熟悉Spring框架中的各个模块,如Spring Core、Spring Beans、Spring Context、Spring AOP、Spring DAO等,并能够解释它们的功能和用途‌

依赖注入(DI)和 控制反转(IoC)的概念

解释DI和IoC在Spring框架中的实现和应用,以及它们如何提高代码的可测试性和可维护性‌。

Spring Beans的作用和特性

探讨Spring Beans的定义、配置方式(注解和XML配置)、以及单例模式和原型模式的使用场景‌。

Spring Security和Spring Cloud的应用

是否熟悉Spring Security在安全方面的应用,以及Spring Cloud在构建微服务架构中的应用‌。

Spring Bean的作用域

解释Spring支持的五种作用域(singleton, prototype, request, session, global session),以及它们在不同场景下的应用‌。

Spring框架的扩展性

探讨如何利用Spring的扩展点(如拦截器、事件监听等)来增强应用程序的功能‌。

Spring框架的缺点

是否能够客观地看待Spring框架的不足,以及他们是否有解决方案或改进建议‌。

1、Spring 的 AOP 和 IOC 是什么?使用场景有哪些?

(1)Spring AOP (Aspect-Oriented Programming) 是一种面向切面编程的技术,允许开发者在不修改源代码的情况下在程序运行时织入相关处理,如日志、事务、安全、缓存等。

(2)Spring IOC (Inversion of Control)即控制反转,是一种设计模式,通过把对象的创建和对象的管理交给容器来实现。IOC的主要目的是解耦,使得代码变得更加模块化和可维护。

IOC 三种注入方式

(1)XML:Bean 实现类来自第三方类库,例如 DataSource 等。需要命名空间等配置,例如:context,aop,mvc。

(2)注解:在开发的类使用@Controller,@Service 等注解

(3)Java 配置类:通过代码控制对象创建逻辑的场景。例如:自定义修改依赖类库。

使用场景:
(1)AOP 可以用来处理跨越多个类的公共行为,比如日志记录、安全控制、事务处理等。
(2)IOC 可以用来管理对象的生命周期,并且通过配置文件简化对象的创建和关系维护。

2、Spring容器的启动过程

Spring容器的启动过程主要分为三个阶段:

  1. 加载配置文件并创建容器对象:Spring容器在启动时,会读取指定的配置文件,然后通过反射机制创建相应的Java对象,并将这些对象存放在容器中,同时会进行一些预处理工作,例如解析XML配置文件、扫描指定包路径下的所有类等。
  2. 容器对象的初始化:容器对象初始化是指容器对象中所有的Bean对象的实例化和属性注入过程。Spring容器会根据XML文件或注解的定义,实例化并配置Bean的属性,然后对Bean进行依赖注入,将Bean的依赖关系绑定在一起。
  3. 容器对象的使用:容器对象初始化完成后,就可以开始使用容器中的Bean对象了。当需要使用某个Bean时,容器会通过Bean定义中的配置信息,实例化相应的Bean对象并返回给调用者,供其使用。

需要注意的是,Spring容器的启动过程是一次性的,一旦容器启动后就不能再进行配置或修改。因此,在进行Bean的配置时,需要仔细考虑每个Bean对象的依赖关系和初始化过程,确保在容器启动后能够正常使用所有的Bean对象。

3、Spring容器中Bean的生命周期主要分为以下几个阶段

  1. 实例化:在容器中创建Bean的实例。
  2. 属性赋值:为Bean的属性注入值或引用。
  3. Aware接口的回调:如果Bean实现了Aware接口,则会回调相关方法,让Bean能够获取容器的资源。
  4. BeanPostProcessor的前置处理:如果容器中存在实现了BeanPostProcessor接口的类,则在Bean实例化和初始化前,会回调这些类的postProcessBeforeInitialization方法,可以对Bean进行自定义的前置处理。
  5. 初始化:在Bean的初始化之前,会执行所有的前置处理,然后会执行Bean自定义的初始化方法,例如在XML文件中使用init-method属性指定的方法,或者使用@PostConstruct注解指定的方法。
  6. BeanPostProcessor的后置处理:如果容器中存在实现了BeanPostProcessor接口的类,则在Bean初始化完成后,会回调这些类的postProcessAfterInitialization方法,可以对Bean进行自定义的后置处理。
  7. 销毁:当Bean不再需要时,会执行Bean的销毁方法,例如在XML文件中使用destroy-method属性指定的方法,或者使用@PreDestroy注解指定的方法。

2、JDK 动态代理和 CGLIB 代理 ?

JDK 动态代理

  1. Interface:对于 JDK 动态代理,目标类需要实现一个Interface。
  2. InvocationHandler:InvocationHandler是一个接口,可以通过实现这个接口,定义横切逻辑,再通过反射机制(invoke)调用目标类的代码,在次过程,可能包装逻辑,对目标方法进行前置后置处理。
  3. Proxy:Proxy利用InvocationHandler动态创建一个符合目标类实现的接口的实例,生成目标类的代理对象。

CgLib 动态代理

  1. 使用JDK创建代理有一大限制,它只能为接口创建代理实例,而CgLib 动态代理就没有这个限制。
  2. CgLib 动态代理是使用字节码处理框架 ASM,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
  3. CgLib 创建的动态代理对象性能比 JDK 创建的动态代理对象的性能高不少,但是 CGLib 在创建代理对象时所花费的时间却比 JDK 多得多,所以对于单例的对象,因为无需频繁创建对象,用 CGLib 合适,反之,使用 JDK 方式要更为合适一些。同时,由于 CGLib 由于是采用动态创建子类的方法,对于 final 方法,无法进行代理
这两种动态代理都可以用来在不改变原始类代码的情况下,在运行时为目标类添加额外的功能,如日志记录、权限验证、缓存等。在选择动态代理的时候,需要根据具体的业务需求和性能要求来确定使用 JDK 动态代理还是 CGLIB 动态代理。

3、Spring 事务

Spring对事务提供了两种支持方式:编程式事务和 声明式事务。编程式事务是指通过在编程时直接在业务逻辑代码中写入事务控制的相关代码,与业务逻辑代码耦合,一般不建议使用这种方式;声明式事务则通过AOP的方式来进行事务控制,对事务的控制逻辑不会侵入到业务逻辑代码中。

Spring 事务传播机制是包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。事务的传播级别有 7 个,支持当前事务的:REQUIRED、SUPPORTS、MANDATORY;不支持当前事务的:REQUIRES_NEW、NOT_SUPPORTED、NEVER,以及嵌套事务 NESTED,其中 REQUIRED 是默认的事务传播级别。

Spring 事务的属性包括:

(1)事务隔离级别:决定了事务处理过程中数据库数据如何隔离,常用的隔离级别有:读未提交、读已提交、可重复读、串行化。

Spring中的事务隔离级别
事务隔离级别:用来解决并发事务时出现的问题,其使用TransactionDefinition中的静态变量来指定。Spring中支持五种事务隔离级别方式:
​
ISOLATION_DEFAULT:默认隔离级别,即使用底层数据库默认的隔离级别;
ISOLATION_READ_UNCOMMITTED:未提交读;
ISOLATION_READ_COMMITTED:提交读,一般情况下我们使用这个;
ISOLATION_REPEATABLE_READ:可重复读;
ISOLATION_SERIALIZABLE:序列化。

(2)事务传播行为:决定了事务的执行上下文如何传播,常用的传播行为有:Required、Supports、Mandatory、Requires New、Not Supported、Never 、NESTED。

(3)事务超时时间:决定了事务的执行时间,超过该时间的事务将被回滚。

(4)事务只读属性:决定了事务是否只读,只读事务可以提高效率。

Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。

既然是“事务传播”,所以事务的数量应该在两个或两个以上,Spring 事务传播机制的诞生是为了规定多个事务在传播过程中的行为的。比如方法 A 开启了事务,而在执行过程中又调用了开启事务的 B 方法,那么 B 方法的事务是应该加入到 A 事务当中呢?还是两个事务相互执行互不影响,又或者是将 B 事务嵌套到 A 事务中执行呢?所以这个时候就需要一个机制来规定和约束这两个事务的行为,这就是 Spring 事务传播机制所解决的问题。

3.1 Spring 事务传播机制

可使用 @Transactional(propagation=Propagation.REQUIRED) 来定义,Spring 事务传播机制的级别包含以下 7 种:

  1. Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  3. Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  4. Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  7. Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。

以上 7 种传播机制,可根据“是否支持当前事务”的维度分为以下 3 类:

1cae7cf0114640eea60c8736cf81b78e~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image.png

3.2 Spring声明式事务的原理

AOP的核心就是解耦合

利用AOP实现事务的代理(声明式事务就是,那个方法需要添加事务,那个方法不需要添加事务可以动态的配置)

事务 流程:

首先开启一个事务(open)

业务的执行

监听到是否有异常,没异常就提交,有异常就回滚(commit/rollback)

最后事务的关闭(close)

下划线部分表示是AOP帮我们做了这个,这其实也是一个模板方法的模式

3.3 事务传播机制使用与演示

接下来我们演示一下事务传播机制的使用,以下面 3 个最典型的事务传播级别为例:

  • 支持当前事务的 REQUIRED;
  • 不支持当前事务的 REQUIRES_NEW;
  • 嵌套事务 NESTED。

下来我们分别来看。

事务传播机制的示例,需要用到以下两张表:

-- 用户表
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `createtime` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
​
-- 日志表
CREATE TABLE `log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` text NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

创建一个 Spring Boot 项目,核心业务代码有 3 个:UserController、UserServcie 以及 LogService。在 UserController 里面调用 UserService 添加用户,并调用 LogService 添加日志。

3.2.1 REQUIRED 使用演示

REQUIRED 支持当前事务。 UserController 实现代码如下,其中 save 方法开启了事务:

@RestController
public class UserController {
    @Resource
    private UserService userService;
    @Resource
    private LogService logService;
​
    @RequestMapping("/save")
    @Transactional
    public Object save(User user) {
        // 插入用户操作
        userService.save(user);
        // 插入日志
        logService.saveLog("用户插入:" + user.getName());
        return true;
    }
}

UserService 实现代码如下:

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;
​
    @Transactional(propagation = Propagation.REQUIRED)
    public int save(User user) {
        return userMapper.save(user);
    }
}

LogService 实现代码如下:

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;
​
    @Transactional(propagation = Propagation.REQUIRED)
    public int saveLog(String content) {
        // 出现异常
        int i = 10 / 0;
        return logMapper.saveLog(content);
    }
}

执行结果:程序报错,两张表中都没有插入任何数据。

执行流程描述:

  1. 首先 UserService 中的添加用户方法正常执行完成。
  2. LogService 保存日志程序报错,因为使用的是 UserController 中的全局事务,所以整个事务回滚,步骤 1 中的操作也跟着回滚。
  3. 所以数据库中没有添加任何数据。
3.2.1 REQUIRED_NEW 使用演示

REQUIRED_NEW 不支持当前事务。 UserController 实现代码:

@RequestMapping("/save")
@Transactional
public Object save(User user) {
    // 插入用户操作
    userService.save(user);
    // 插入日志
    logService.saveLog("用户插入:" + user.getName());
    return true;
}
复制代码

UserService 实现代码:

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;
​
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int save(User user) {
        System.out.println("执行 save 方法.");
        return userMapper.save(user);
    }
}
复制代码

LogService 实现代码:

@Service
public class LogService {
    @Resource
    private LogMapper logMapper;
​
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int saveLog(String content) {
        // 出现异常
        int i = 10 / 0;
        return logMapper.saveLog(content);
    }
}
复制代码

程序执行结果:

User 表中成功添加了一条用户数据,Log 表执行失败,没有加入任何数据,但它并没有影响到 UserController 中的事务执行。

通过以上结果可以看出:LogService 中使用的是单独的事务,虽然 LogService 中的事务执行失败了,但并没有影响 UserController 和 UserService 中的事务。

3.2.3 NESTED 使用演示

NESTED 是嵌套事务。 UserController 实现代码如下:

@RequestMapping("/save")
@Transactional
public Object save(User user) {
    // 插入用户操作
    userService.save(user);
    return true;
}
复制代码

UserService 实现代码如下:

@Transactional(propagation = Propagation.NESTED)
public int save(User user) {
    int result = userMapper.save(user);
    System.out.println("执行 save 方法.");
    // 插入日志
    logService.saveLog("用户插入:" + user.getName());
    return result;
}
复制代码

LogService 实现代码如下:

@Transactional(propagation = Propagation.NESTED)
public int saveLog(String content) {
    // 出现异常
    int i = 10 / 0;
    return logMapper.saveLog(content);
}
复制代码

最终执行结果,用户表和日志表都没有添加任何数据。

执行流程描述:

  1. UserController 中调用了 UserService 的添加用户方法,UserService 使用 NESTED 循环嵌套事务,并成功执行了添加用户的方法。
  2. UserService 中调用了 LogService 的添加方法,LogService 使用了 NESTED 循环嵌套事务,但在方法执行中出现的异常,因此回滚了当前事务。
  3. 因为 UserService 使用的是嵌套事务,所以发生回滚的事务是全局的,也就是说 UserService 中的添加用户方法也被回滚了,最终执行结果是用户表和日志表都没有添加任何数据。

4、Spring MVC 的工作原理

当用户发送请求到服务器时,DispatcherServlet将请求转发到一个控制器。控制器通过模型和视图来处理请求。模型代表请求的数据,视图代表显示数据的方式。控制器返回一个模型和视图给 DispatcherServlet,它将模型和视图呈现给用户。 Spring MVC 中还有一些中间件,如解析请求,验证请求参数,处理异常等,它们在请求流程中按顺序执行。

5、Spring bean的生命周期?

Spring bean 的生命周期包括了以下几个步骤:

(1)Bean 实例化:Spring 通过构造器或工厂方法来创建一个新的 Bean 实例。

(2)Bean 属性设置:Spring 将 Bean 配置的属性设置到创建的 Bean 实例中。

(3)Bean 初始化:当 Bean 实例创建完成并且所有的属性设置完毕后,Spring 会调用 Bean 的初始化方法,例如调用 init-method 指定的初始化方法。

1、执行各种通知;
2、执行初始化的前置方法;
3、执行初始化方法;
4、执行初始化的后置方法。

(4)Bean 使用:在 Bean 初始化完成后,它可以被容器中的其他 Bean 使用。

(5)Bean 销毁:当 Bean 不再被使用时,容器会调用 Bean 的销毁方法,例如调用 destroy-method 指定的销毁方法。

请注意,Bean 的生命周期在默认情况下是在单例模式下定义的,因此 Bean 的生命周期在整个应用程序生命周期中仅存在一次。如果 Bean 是多例的,则每个 Bean 实例都会有自己的生命周期。

5.1 Spring怎么解决循环依赖的呢?

单例Bean初始化完成,要经历三步:

c8931807ce294cb19f7df77982ad621a~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp.png

注入就发生在第二步,属性赋值,结合这个过程,Spring 通过三级缓存解决了循环依赖:

  1. 一级缓存 : Map<String,Object> singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例
  2. 二级缓存 : Map<String,Object> earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例
  3. 三级缓存 : Map<String,ObjectFactory<?>> singletonFactories,早期曝光对象工厂,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象。

156c4c4e5276447bbe91a9d39f66da5f~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp.png 我们来看一下三级缓存解决循环依赖的过程:

当 A、B 两个类发生循环依赖时:

74eeb173ff324cecb79d0d8ec97583fd~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp.png

A实例的初始化过程:

  1. 创建A实例,实例化的时候把A对象⼯⼚放⼊三级缓存,表示A开始实例化了,虽然我这个对象还不完整,但是先曝光出来让大家知道

da3a3ea3b9f68c04784d63a4f03595ad.png

  1. A注⼊属性时,发现依赖B,此时B还没有被创建出来,所以去实例化B

  2. 同样,B注⼊属性时发现依赖A,它就会从缓存里找A对象。依次从⼀级到三级缓存查询A,从三级缓存通过对象⼯⼚拿到A,发现A虽然不太完善,但是存在,把A放⼊⼆级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入⼀级缓存。

ea968c8d754345d3aff8af8d46685fc6~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp.png

  1. 接着A继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除⼆级缓存中的A,同时把A放⼊⼀级缓存

  2. 最后,⼀级缓存中保存着实例化、初始化都完成的A、B对象

所以,我们就知道为什么Spring能解决setter注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。

5.2 为什么要三级缓存?⼆级不⾏吗?

不行,主要是为了⽣成代理对象。如果是没有代理的情况下,使用二级缓存解决循环依赖也是OK的。但是如果存在代理,三级没有问题,二级就不行了。

因为三级缓存中放的是⽣成具体对象的匿名内部类,获取Object的时候,它可以⽣成代理对象,也可以返回普通对象。使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。

假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象,Bean初始化过程中,通过 BeanPostProcessor 去⽣成代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么可能就导致取到的Bean对象不一致了。

53203676117e467388a1c0a6120c712f~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp.png