当前位置:   article > 正文

第八章--SSM(SpringMVC+Spring+MyBatis)部分面试题复习_段会做两个事情 通过反射针对没有设置lazy

段会做两个事情 通过反射针对没有设置lazy

本文回顾总结一下, 高频的一些SSM框架的面试题, 用作复习等使用!

1. SpringMVC

1.1 SpringMVC的理解

关键词 : 对Servlet的封装,屏蔽很多好用的细节;
比如 servlet,获取参数需要不断的getParameter,现在只要在SpringMVC⽅法定义对应的JavaBean,只要属性名与参数名⼀致,SpringMVC就可以帮我们实现「将参数封装到JavaBean」上了;
以前使⽤Servlet「上传⽂件」,需要处理各种细节,写⼀⼤堆处理的逻辑(还得导⼊对应的jar),现在在⼀个在SpringMVC的⽅法上定义出MultipartFile接⼝,⼜可以屏蔽掉上传⽂件的细节了。

SpringMVC 是一种基于 Java 语言开发,实现了 Web MVC 设计模式,请求驱动类型的轻量级 Web 框架。
采用了 MVC 架构模式的思想,通过把 ModelViewController 分离,将 Web 层进行职责解耦,从而把复杂的 Web 应用分成逻辑清晰的几个组件,在 Spring MVC 中有9 大重要的组件。

    下面详细说明一下这些组件的作用和初始化方法:
1MultipartResolver 文件处理器
	对应的初始化方法是 initMultipartResolver(context),用于处理上传请求。
2LocaleResolver 当前环境处理器
    其对应的初始化方法是 initLocaleResolver(context)
    SpringMVC 主要有两个地方用到了 Locale:
        一是 ViewResolver 视图解析的时候;
        二是用到国际化资源或者主题的时候。
3ThemeResolver 主题处理器
	其对应的初始化方法是 initThemeResolver(context)
    用于解析主题。 也就是解析样式、图片及它们所形成的显示效果的集合。
4HandlerMapping 处理器映射器
    其对应的初始化方法是 initHandlerMappings(context) ,
    在 SpringMVC 中会有很多请求,每个请求都需要一个 Handler 处理。
    HandlerMapping 的作用便是找到请求相应的处理器 HandlerInterceptor5HandlerAdapter 处理器适配器
    其对应的初始化方法是 initHandlerAdapters(context)
    从名字上看,它就是一个适配器。HandlerAdapters 要做的事情就是如何让固定的
    Servlet 处理方法调用灵活的 Handler 来进行处理
6HandlerExceptionResolver 异常处理器
    对应的初始化方法是 initHandlerExceptionResolvers(context)
    它的主要作用是处理其他组件产生的异常情况。
7RequestToViewNameTranslator 视图名称翻译器
    其对应的初始化方法是 initRequestToViewNameTranslator(context)
    它的作用是从请求中获取 ViewName。
    有的 Handler 处理完后并没有设置 View 也没有设置 ViewName,这时就需要从
    request 中获取,而 RequestToViewNameTranslator 就是为 request 提供获取
    ViewName 的实现。
8ViewResolvers 页面渲染处理器
    其对应的初始化方法是 initViewResolvers(context)
    ViewResolvers 的主要作用是将 String 类型的视图名和 Locale 解析为 View 类型的视图。
9FlashMapManager 参数传递管理器
    其对应的初始化方法是 initFlashMapManager(context)
    在实际应用中,为了避免重复提交,我们可以在处理完 post 请求后重定向到另外一个
    get 请求,这个 get 请求可以用来返回页面渲染需要的信息。
    FlashMap 就是用于这种请求重定向场景中的参数传递。

而以上九大组件中,涉及到请求处理响应核心组件有:
    1. HandlerMapping2. HandlerAdapter3. ViewResolver
    具体执行步骤如下:
        1HandlerMapping 回到调用 HandlerAdapter
        2HandlerAdapter 会返回 ModelAndView
        3ModelAndView 根据用户传入参数得到 ViewResolvers
        4ViewResolvers 会将用户传入的参数封装为 View,交给引擎进行渲染。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

:::info
注意 : ModelAndView 和 View 类不属于九大组件;
:::

1.2 SpringMVC请求流程

image.png
image.png
统⼀的处理⼊⼝,对应SpringMVC下的源码是在DispatcherServlet下实现的,该对象在初始化就会把映射器、适配器、视图解析器、异常处理器、⽂件处理器等等给初始化掉,初始哪些对象主要看配置文件;
DispatcherServlet(⼊⼝)->DispatcherServlet.properties(会初始化的对象)->HandlerMapping(映射器)->HandlerExecutionChain(映射器+拦截器List) ->HttpRequestHandlerAdapter(适配器)-> HttpMessageConverter(数据转换)

2. Spring基础

2.1 为什么用Spring框架

Spring 是一个轻量级应用框架,它提供了 IoC 和 AOP 这两个核心的功能。
核心目的是为了简化企业级应用程序的开发,使得开发者只需要关心业务需求,不需要关心 Bean 的管理,以及通过切面增强功能减少代码的侵入性。

  • 轻量:Spring 是轻量的,基本的版本大约 2MB。非侵入式
  • IOC/DI:Spring 通过 IOC 容器实现了 Bean 的生命周期的管理,以及通过 DI 实现依赖注入,从而实现了对象依赖的松耦合管理。
  • 面向切面的编程(AOP):Spring 支持面向切面的编程,从而把应用业务逻辑和系统服务分开。
  • MVC 框架:Spring MVC 提供了功能更加强大且更加灵活的 Web 框架支持
  • 事务管理:Spring 通过 AOP 实现了事务的统一管理,对应用开发中的事务处理提供了非常灵活的支持

Spring框架的生态非常庞大,业务开发领域,提供非常完善的支持,且社区活跃度和技术成熟度非常高。

2.2 IOC和AOP的理解

Spring IOC 解决的是对象管理和对象依赖的问题。
本来是我们⾃⼰⼿动new出来的对象,现在则把对象交给Spring的IOC容器管理
IOC容器可以理解为⼀个对象⼯⼚,我们都把该对象交给⼯⼚,⼯⼚管理这些对象的创建以及依赖关系
等我们需要⽤对象的时候,从⼯⼚⾥边获取就好了;

控制反转」指的就是:把原有⾃⼰掌控的事交给别⼈去处理,它更多的是⼀种思想或者可以理解为设计模式;⽐如:本来由我们⾃⼰new出来的对象,现在交由IOC容器,把对象的控制权交给它⽅了;
⽽「依赖注⼊」在我的理解下,它其实是「控制反转」的实现⽅式
对象⽆需⾃⾏创建或者管理它的依赖关系,依赖关系将被「⾃动注⼊」到需要它们的对象当中去

IOC 主要的好处在于「将对象集中统⼀管理」并且「降低耦合度」
享受单例 单元测试 屏蔽对象创建 一套对Bean生命周期的扩展
我⽤Spring IOC 可以⽅便 单元测试、对象创建复杂、对象依赖复杂、单例等等的,什么都可以交给Spring IOC
理论上⾃⼰new出来的都可以解决上⾯的问题,Spring在各种场景组合下有可能不是最优解,
但new出来的你要⾃⼰管理,可能你得⾃⼰写⼯⼚,得实现⼀⼤套的东⻄才能满⾜需求
并且Spring核⼼不仅仅IOC啊,除了把对象创建出来,还有⼀整套的Bean⽣命周期管理

Spring AOP 解决的是 ⾮业务代码抽取的问题,AOP 底层的技术是动态代理,在Spring内实现依赖的是BeanPostProcessor
⽤AOP来对我们公司现有的监控客户端进⾏封装,⼀个系统离不开监控,监控基本的指标有QPS、RT、ERROR等等,对外暴露的监控客户端只能在代码⾥写对应的上报信息(灵活,但会与业务代码掺杂在⼀起),利⽤注解+AOP的⽅式封装了⼀把,只要⽅法/类上带有我⾃定义的注解,⽅法被调⽤时,就会上报AQS、RT等信息,实现了⾮业务代码与业务代码分离的效果

2.3 IOC工作流程

IOC 的全称是 Inversion Of Control, 也就是控制反转,它的核心思想是把对象的管理权限交给容器。
应用程序如果需要使用到某个对象实例,直接从 IOC 容器中去获取就行,这样设计的好处是降低了程序里面对象
    与对象之间的耦合性。使得程序的整个体系结构变得更加灵活

Spring 里面很多方式去定义 Bean,(如图)比如 XML 里面的<bean>标签、@Service@Component@Repository@Configuration 配置类中的@Bean 注解等等。
Spring 在启动的时候,会去解析这些 Bean 然后保存到 IOC 容器里面。

Spring IOC 的工作流程大致可以分为两个阶段。
    第一个阶段,就是 IOC 容器的初始化
这个阶段主要是根据程序中定义的 XML 或者注解等 Bean 的声明方式
通过解析和加载后生成 BeanDefinition,然后把 BeanDefinition 注册到 IOC容器。
通过注解或者 xml 声明的 bean 都会解析得到一个 BeanDefinition 实体,实体中包含
这个 bean 中定义的基本属性。
最后把这个 BeanDefinition 保存到一个 Map 集合里面,从而完成了 IOC 的初始化。
IoC 容器的作用就是对这些注册的 Bean 的定义信息进行处理和维护,它 IoC 容器控制
反转的核心。
    第二个阶段,完成 Bean 初始化及依赖注入
然后进入到第二个阶段,这个阶段会做两个事情
1. 通过反射针对没有设置 lazy-init 属性的单例 bean 进行初始化。
2. 完成 Bean 的依赖注入。
    第三个阶段,Bean 的使用
通常我们会通过@Autowired 或者 BeanFactory.getBean()从 IOC 容器中获取指定的 bean 实例。
另外,针对设置 layy-init 属性以及非单例 bean 的实例化,是在每次获取 bean 对象的
时候,调用 bean 的初始化方法来完成实例化的,并且 Spring IOC 容器不会去管理这些 Bean
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

2.4 Bean的定义

Spring提供了4种⽅式,分别是:
1):注解 2):XML 3):JavaConfig 4):基于Groovy DSL配置
⽇常写业务代码⼀般⽤注解来定义各种对象,责任链这种⼀般配置在XML,「注解」解决不了的就⽤JavaConfig
反正是定义元数据,给Spring解析就好;

2.5 Bean 的作用域

首先呢,Spring 框架里面的 IOC 容器,可以非常方便的去帮助我们管理应用里面的Bean 对象实例。
我们只要按照 Spring 里面提供的 xml 或者注解等方式去告诉 IOC 容器,哪些 Bean需要被 IOC 容器管理就行。

其次呢,既然是 Bean 对象实例的管理,那意味着这些实例,是存在生命周期,也就是所谓的作用域。
理论上来说,常规的生命周期只有两种:

  • singleton, 也就是单例,意味着在整个 Spring 容器中只会存在一个 Bean 实例。
  • prototype,翻译成原型,意味着每次从 IOC 容器去获取指定 Bean 的时候,都会返回一个新的实例对象。

但是在基于 Spring 框架下的 Web 应用里面,增加了一个会话纬度来控制 Bean 的生命周期,主要有三个选择:

  • request, 针对每一次 http 请求,都会创建一个新的 Bean
  • session,以 sesssion 会话为纬度,同一个 session 共享同一个 Bean 实例,不同的 session 产生不同的 Bean 实例
  • globalSession,针对全局 session 纬度,共享同一个 Bean 实例

2.6 SpringBean⽣命周期

第一,Spring 生命周期全过程大致分为五个阶段:创建前准备阶段、创建实例阶段、依赖注入阶段、容器缓存阶段和销毁实例阶段。

1.创建前准备阶段(干什么,作用)
    这个阶段主要的作用是,Bean 在开始加载之前,需要从上下文和相关配置中解析并查找 Bean 有关的扩展实现,
    比如像`init-method`-容器在初始化 bean 时调用的方法、`destory-method`,容器在销毁 bean 时调用的方法。
    以及 BeanFactoryPostProcessor 这类的 bean 加载过程中的前置和后置处理
    这些类或配置是Spring提供给开发者,用来实现 Bean 加载过程中的扩展机制,在很多和 Spring 集成的中间件中比较常见,比如 Dubbo2.创建实例阶段
    主要通过反射创建Bean实例对象,并且扫描和解析Bean声明的一些属性
3.依赖注入阶段
    如果被实例化的 Bean 存在依赖其他 Bean 对象的情况,则需要对这些依赖 bean 进行
    对象注入。比如常见的`@Autowired`、setter 注入等依赖注入的配置形式。
    同时,在这个阶段会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors
    (用来实现 bean 初始化前后的扩展回调)、
    InitializingBean(这个类有一个 afterPropertiesSet(),这个在工作中也比较常见)、
    BeanFactoryAware 等等。
4.容器缓存阶段
    容器缓存阶段主要是把 bean 保存到容器以及 Spring 的缓存中,到了这个阶段,Bean就可以被开发者使用了。
    这个阶段涉及到的操作,常见的有,`init-method`这个属性配置的方法, 会在这个阶段调用。
    以及像 BeanPostProcessors 方法中的后置处理器方法如:
    postProcessAfterInitialization,也会在这个阶段触发。
5.销毁实例阶段
    当 Spring 应用上下文关闭时,该上下文中的所有 bean 都会被销毁。
    如果存在 Bean 实现了 DisposableBean 接口,或者配置了`destory-method`属性,会在这个阶段被调用
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

普通Java对象和Spring所管理的Bean实例化的过程是有些区别的
普通Java环境下创建对象简要的步骤可以分为:
1):java源码被编译为被编译为class⽂件
2):等到类需要被初始化时(⽐如说new、反射等)
3):class⽂件被虚拟机通过类加载器加载到JVM
4):初始化对象供我们使⽤
简单来说,可以理解为它是⽤Class对象作为「模板」进⽽创建出具体的实例

⽽Spring所管理的Bean不同的是,除了Class对象之外,还会使⽤BeanDefinition的实例来描述对象的信息
⽐如说,我们可以在Spring所管理的Bean有⼀系列的描述:@Scope、@Lazy、@DependsOn等等
可以理解为:Class只描述了类的信息,⽽BeanDefinition描述了对象的信息

⾸先是Spring Bean的⽣命周期过程,Spring使⽤BeanDefinition来装载着我们给Bean定义的元数据
实例化Bean的时候实际上就是遍历BeanDefinitionMap
Spring的Bean实例化和属性赋值是分开两步来做的
在Spring Bean的⽣命周期,Spring预留了很多的hook给我们去扩展
1):Bean实例化之前有BeanFactoryPostProcessor
2):Bean实例化之后,初始化时,有相关的Aware接⼝供我们去拿到Context相关信息
3):环绕着初始化阶段,有BeanPostProcessor(AOP的关键)
4):在初始化阶段,有各种的init⽅法供我们去⾃定义
image.png
image.png
image.png
:::info
首先通过BeanDefinition获取bean定义信息,这里封装了bean所有信息,比如类全路径,延迟加载,单例等
创建bean时,第一步调用构造函数实例化bean
第二步bean依赖注入,比如set方法注入,注解@Autowired都是这一步完成
第三步处理Aware接口,如果bean实现了Aware接口就会重写方法执行
第四步bean的后置处理器beanPostProcessor这个是前置处理器
第五步初始化方法,比如实现了接口InitializingBean或者自定义了方法init-method标签或@PostContruct
第六步是执行了bean的后置处理器BeanPostProcessor,主要对bean进行增强,有可能在这里产生代理对象
最后一步是销毁bean
:::

2.7 Bean的注入方式

  1. 使用 xml 的方式来声明 Bean 的定义,Spring 容器在启动的时候会加载并解析这个 xml,把 bean 装载到 IOC 容器中。
  2. 使用@CompontScan 注解来扫描声明@Controller、@Service、@Repository、@Component 注解的类。
  3. 使用@Configuration 注解声明配置类,并使用@Bean 注解实现 Bean 的定义,这种方式其实是 xml 配置方式的一种演变,是 Spring 迈入到无配置化时代的里程碑。
  4. 使用@Import 注解,导入配置类或者普通的 Bean
  5. 使用 FactoryBean 工厂 bean,动态构建一个 Bean 实例,Spring Cloud OpenFeign 里面的动态代理实例就是使用 FactoryBean 来实现的。
  6. 实现 ImportBeanDefinitionRegistrar 接口,可以动态注入 Bean 实例。这个在Spring Boot 里面的启动注解有用到。
  7. 实现 ImportSelector 接口,动态批量注入配置类或者 Bean 对象,这个在 SpringBoot 里面的自动装配机制里面有用到。

2.8 (重要高频⭐)Spring 中的 Bean 是线程安全?

Bean 从哪里来
    在 Spring 容器中,除了很多 Spring 内置的 Bean 以外,其他的 Bean 都是我们自己通
过 Spring 配置来声明的,然后,由 Spring 容器统一加载。我们在 Spring 声明配置中
通常会配置以下内容,如:class(全类名)、id(也就是 Bean 的唯一标识)、 scope
(作用域)以及 lazy-init(是否延时加载)等。之后,Spring 容器根据配置内容使用
对应的策略来创建 Bean 的实例。因此,Spring 容器中的 Bean 其实都是根据我们自己
写的类来创建的实例。因此,Spring 中的 Bean 是否线程安全,跟 Spring 容器无关,
只是交由 Spring 容器托管而已。
那么,在 Spring 容器中,什么样的 Bean 会存在线程安全问题呢?回答,这个问题之
前我们得先回顾一下 Spring Bean 的作用域。在 Spring 定义的作用域中,其中有
prototype( 多例 Bean )和 singleton ( 单例 Bean)。那么,定义为 prototype
的 Bean,是在每次 getBean 的时候都会创建一个新的对象。定义为 singleton 的
Bean,在 Spring 容器中只会存在一个全局共享的实例。

什么样的 Bean 存在线程安全问题
     多例 Bean 每次都会新创建新实例,也就是说线程之间不存在 Bean 共
享的问题。因此,多例 Bean 是不存在线程安全问题的。
而单例 Bean 是所有线程共享一个实例,因此,就可能会存在线程安全问题。但是单例
Bean 又分为无状态 Bean 和有状态 Bean。在多线程操作中只会对 Bean 的成员变量进
行查询操作,不会修改成员变量的值,这样的 Bean 称之为无状态 Bean。所以,可想
而知,无状态的单例 Bean 是不存在线程安全问题的。但是,在多线程操作中如果需要
对 Bean 中的成员变量进行数据更新操作,这样的 Bean 称之为有状态 Bean,所以,
有状态的单例 Bean 就可能存在线程安全问题。
所以,最终我们得出结论,在 Spring 中,只有有状态的单例 Bean 才会存在线程安全
问题。我们在使用 Spring 的过程中,经常会使用到有状态的单例 Bean,如果真正遇
到了线程安全问题,我们又该如何处理呢?

如何处理 Spring Bean 的线程安全问题  ?
    处理有状态单例 Bean 的线程安全问题有以下三种方法:
1、将 Bean 的作用域由 “singleton” 单例 改为 “prototype” 多例。
2、在 Bean 对象中避免定义可变的成员变量,当然,这样做不太现实,就当我没说。
3、在类中定义 ThreadLocal 的成员变量,并将需要的可变成员变量保存在
ThreadLocal 中,ThreadLocal 本身就具备线程隔离的特性,这就相当于为每个线
程提供了一个独立的变量副本,每个线程只需要操作自己的线程副本变量,从而解决线
程安全问题。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

:::info
先举例,我们通常在项目中使用的Spring bean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。
综上,当多线程并发情况下,如果对单例的成员属性有修改,就必须考虑线程同步问题,一种是改为原型,一种利用threadlocal去解决。
:::

2.9 ⭐解决循环依赖

问题 ; 如果现在有个A对象,它的属性是B对象,⽽B对象的属性也是A对象,说⽩了就是A依赖B,⽽B⼜依赖A,Spring是怎么做的?
主要通过三级的缓存, 三级缓存其实就是三个Map,

  • singletonObjects(⼀级,⽇常实际获取Bean的地⽅); 正式对象
  • earlySingletonObjects(⼆级,还没进⾏属性注⼊,由三级缓存放进来); 半成品
  • singletonFactories(三级,Value是⼀个对象⼯⼚); 工厂

假设A对象 在实例化后,属性注入前,会把⾃⼰扔到三级缓存(此时的key是BeanName,Value是ObjectFactory)
等到A在在注⼊属性时,发现需要依赖B,也会⾛B的实例化过程,B属性注⼊依赖A,从三级缓存找到A(从三级缓存⾥拿出ObjectFactory,从ObjectFactory得到对应的Bean)
删掉三级缓存,放到⼆级缓存
显然,⼆级缓存存储的key是BeanName,value就是Bean(这⾥的Bean还没做完属性注⼊相关的⼯作)
等到完全初始化之后,就会把⼆级缓存给remove掉,塞到⼀级缓存中
我们⾃⼰去getBean的时候,实际上拿到的是⼀级缓存的

如果在代码中,将两个或多个 Bean 互相之间持有对方的引用就会发生循环依赖。循环的依赖将会导致注入
    死循环。这是 Spring 发生循环依赖的原因。

三种依赖情况:
    1.互相依赖: A 依赖 BB 又依赖 A,它们之间形成了循环依赖
    2.三方依赖 : A 依赖 BB 依赖 CC 又依赖 A,形成了循环依赖。
    3.自我依赖: A 依赖 A 形成了循环依赖

而 Spring 中设计了三级缓存来解决循环依赖问题,
    当我们去调用 getBean()方法的时候,Spring 会先从一级缓存中去找到目标 Bean,如果发现一级缓存
    中没有便会去二级缓存中去找,而如果一、二级缓存中都没有找到,意味着该目标 Bean 还没有实例化。
于是,Spring 容器会实例化目标 Bean(PS:刚初始化的 Bean 称为早期 Bean) 。
    然后,将目标 Bean 放入到二级缓存中,同时,加上标记是否存在循环依赖。如果不存在
循环依赖便会将目标 Bean 存入到二级缓存,否则,便会标记该 Bean 存在循环依赖,

    然后将等待下一次轮询赋值,也就是解析@Autowired 注解。等@Autowired 注解赋
值完成后(PS:完成赋值的 Bean 称为成熟 Bean) ,会将目标 Bean 存入到一级缓存。

    这里我可以做个总结,我们来看这张图
Spring 一级缓存中存放所有的成熟 Bean,
二级缓存中存放所有的早期 Bean,先取一级缓存,再去二级缓存。    

    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

image.png
image.png

2.9.1 三级缓存的优势

⾸先从第三级缓存说起(就是key是BeanName,Value为ObjectFactory)
我们的对象是单例的,有可能A对象依赖的B对象是有AOP的(B对象需要代理)
假设没有第三级缓存,只有第⼆级缓存(Value存对象,⽽不是⼯⼚对象)
那如果有AOP的情况下,岂不是在存⼊第⼆级缓存之前都需要先去做AOP代理?这⾥肯定是需要考虑代理的情况的,⽐如A对象是⼀个被AOP增量的对象,B依赖A时,得到的A肯定是代理对象;
所以,三级缓存的Value是ObjectFactory,可以从⾥边拿到代理对象
⽽⼆级缓存存在的必要就是为了性能,从三级缓存的⼯⼚⾥创建出对象,再扔到⼆级缓存(这样就不⽤每次都要从⼯⼚⾥拿)

综上 : 三级缓存 考虑 代理, 二级缓存 考虑性能.

:::info
三级缓存是用来存储代理 Bean,当调用 getBean()方法时,发现目标 Bean 需要通过代理工厂来创建,此时会将创建好的实例保存到三级缓存,最终也会将赋值好的 Bean同步到一级缓存中。
:::

2.9.2 有没有不能解决循环依赖的问题

1.多例 Bean 通过 setter 注入的情况,不能解决循环依赖问题  -->Bean改单例
2.构造器注入的 Bean 的情况,不能解决循环依赖问题          -->通过@Lazy注解
3.单例的代理 Bean 通过 Setter 注入的情况,不能解决循环依赖问题  -->通过@Lazy注解,或使用@DependsOn 注解指定加载先后关系
4.设置了@DependsOnBean 的情况,不能解决循环依赖问题   -->找到注解循环依赖的地方,迫使它不循环依赖
  • 1
  • 2
  • 3
  • 4

2.10 Spring事务

Spring事务传播行为

  1. REQUIRED:默认的 Spring 事物传播级别,如果当前存在事务,则加入这个事务,如果不存在事务,就新建一个事务。
  2. REQUIRE_NEW:不管是否存在事务,都会新开一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。
  3. NESTED:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一个事务,类似于 REQUIRE_NEW。
  4. SUPPORTS:表示支持当前事务,如果当前不存在事务,以非事务的方式执行。
  5. NOT_SUPPORTED:表示以非事务的方式来运行,如果当前存在事务,则把当前事务挂起。
  6. MANDATORY:强制事务执行,若当前不存在事务,则抛出异常.
  7. NEVER:以非事务的方式执行,如果当前存在事务,则抛出异常。

Spring 里面的事务和分布式事务

首先, 在 Spring 里面并没有提供事务,它只是提供了对数据库事务管理的封装。
通过声明式的事务配置,使得开发人员可以从一些复杂的事务处理中得到解脱,我们不再需要关心连接的获取、连接的关闭、事务提交、事务回滚这些操作。更加聚焦在业务开发层面。
所以,Spring 里面的事务,本质上就是数据库层面的事务,
这种事务的管理,主要是针对单个数据库里面多个数据表操作的,去满足事务的 ACID特性。
分布式事务,是解决多个数据库的事务操作的数据一致性问题,传统的关系型数据库不支持跨库事务的操作,所以需要引入分布式事务的解决方案。
而 Spring 并没有提供分布式事务场景的支持,所以 Spring 事务和分布式事务在使用上并没有直接的关联性。
但是我们可以使用一些主流的事务解决框架,比如 Seata,集成到 Spring 生态里面,去解决分布式事务的问题。

事务如何实现

spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前
开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
  • 1
  • 2

事务失效场景

第一个,如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致
事务失效,所以一般处理了异常以后,别忘了抛出去就行了

第二个,如果方法抛出检查异常,如果报错也会导致事务失效,最后在
spring事务的注解上,就是@Transactional上配置rollbackFor属性为
Exception,这样别管是什么异常,都会回滚事务
    
第三,如果方法上不是public修饰的,也会导致事务
失效
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.11 BeanFactory 和 FactoryBean 的区别

首先,Spring 里面的核心功能是 IOC 容器,所谓 IOC 容器呢,本质上就是一个 Bean的容器或者是一个 Bean 的工厂。
它能够根据 xml 里面声明的 Bean 配置进行 bean 的加载和初始化,然后 BeanFactory来生产我们需要的各种各样的 Bean。
所以我对 BeanFactory 的理解了有两个。
 BeanFactory 是所有 Spring Bean 容器的顶级接口,它为 Spring 的容器定义了一套规范,并提供像 getBean 这样的方法从容器中获取指定的 Bean 实例。
 BeanFactory 在产生 Bean 的同时,还提供了解决 Bean 之间的依赖注入的能力,也就是所谓的 DI。
FactoryBean 是一个工厂 Bean,它是一个接口,主要的功能是动态生成某一个类型的Bean 的实例,也就是说,我们可以自定义一个 Bean 并且加载到 IOC 容器里面。
它里面有一个重要的方法叫 getObject(),这个方法里面就是用来实现动态构建 Bean的过程。
Spring Cloud 里面的 OpenFeign 组件,客户端的代理类,就是使用了 FactoryBean来实现的。

2.12 Spring 中,有两个 id 相同的 bean,会报错吗,如果会报错,在哪个阶段报错

首先,在同一个 XML 配置文件里面,不能存在 id 相同的两个 bean,否则 spring 容器启动的时候会报错
因为 id 这个属性表示一个 Bean 的唯一标志符号,所以 Spring 在启动的时候会去验证id 的唯一性,一旦发现重复就会报错,这个错误发生 Spring 对 XML 文件进行解析转化为 BeanDefinition 的阶段。
但是在两个不同的 Spring 配置文件里面,可以存在 id 相同的两个 bean。 IOC 容器在加载 Bean 的时候,默认会多个相同 id的 bean 进行覆盖。
Spring3.x 里面提供@Configuration 注解去声明一个配置类,然后使用@Bean 注解实现 Bean 的声明,这种方式完全取代了 XMl。
在这种情况下,如果我们在同一个配置类里面声明多个相同名字的 bean,在Spring IOC 容器中只会注册第一个声明的 Bean 的实例。
后续重复名字的 Bean 就不会再注册了。
像这样一段代码,在 Spring IOC 容器里面,只会保存 UserService01 这个实例,后续相同名字的实例不会再加载。
如果使用@Autowired 注解根据类型实现依赖注入,因为 IOC 容器只有UserService01 的实例,所以启动的时候会提示找不到 UserService02 这个实例。
如果使用@Resource 注解根据名词实现依赖注入,在 IOC 容器里面得到的实例对象是UserService01,
于是 Spring 把 UserService01 这个实例赋值给 UserService02,就会提示类型不匹配错误。
这个错误,是在 Spring IOC 容器里面的 Bean 初始化之后的依赖注入阶段发生的。

2.13 过滤器拦截器区别

  1. 运行顺序不同(如图):过滤器是在 Servlet 容器接收到请求之后,但在 Servlet被调用之前运行的;而拦截器则是在 Servlet 被调用之后,但在响应被发送到客户端之前运行的。
    image.png
  2. 配置方式不同:过滤器是在 web.xml 中进行配置;而拦截器的配置则是在 Spring的配置文件中进行配置,或者使用注解进行配置。
  3. Filter 依赖于 Servlet 容器,而 Interceptor 不依赖于 Servlet 容器
  4. Filter 在过滤是只能对 request 和 response 进行操作,而 interceptor 可以对request、response、handler、modelAndView、exception 进行操作
第一点,执行顺序上,依次是过滤器,拦截器和AOP,规则更细致,灵活度也更高

过滤器,底层基于函数回调,依赖于servlet容器,在web.xml进行配置;
    1.拦截URL,对所有请求起作用,可能存在多个,但是仅在servlet初始化时调用12.经过两次

拦截器,底层基于反射,代理模式,不依赖容器,在spring配置文件,或注解配置
    1.拦截URL,对拦截方法进行执行前,渲染前,后的三个方法
    2.仅仅对action作用,静态资源文件不起作用,可以多次调用,只经过一次
    3.可以用IOC里的Bean

AOP ,反射,代理模式,对类方法进行增强,通知丰富,灵活度高,仅对IOC的bean起作用
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2.14 AOP原理

image.png

aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产
生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做
为公共日志保存,事务处理等

在后台管理系统中,就是使用aop来记录了系统的操作日志,
主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要
找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比
如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3. 注解

3.1 @Resource 和 @Autowired 的区别

@Resource 和@Autowired 这两个注解的作用都是在 Spring 生态里面去实现 Bean的依赖注入。
下面我分别说一下@Autowired 和@Resource 这两个注解。

首先,@Autowired 是 Spring 里面提供的一个注解,默认是根据类型来实现Bean 的依赖注入。
@Autowired 注解里面有一个 required 属性默认值是 true,表示强制要求 bean 实例的注入,
在应用启动的时候,如果 IOC 容器里面不存在对应类型的 Bean,就会报错。
当然,如果不希望自动注入,可以把这个属性设置成 false。
image.png
其次呢, 如果在 Spring IOC 容器里面存在多个相同类型的 Bean 实例。由于@Autowired 注解是根据类型来注入 Bean 实例的,所以 Spring 启动的时候,会提示一个错误,大概意思原本只能注入一个单实例 Bean,
但是在 IOC 容器里面却发现有多个,导致注入失败。
当然,针对这个问题,我们可以使用 @Primary 或者@Qualifier 这两个注解来解决。
@Primary 表示主要的 bean,当存在多个相同类型的 Bean 的时候,优先使用声明了@Primary 的 Bean。
@Qualifier 的作用类似于条件筛选,它可以根据 Bean 的名字找到需要装配的目标 Bean。

@Resource 是 JDK 提供的注解,只是 Spring 在实现上提供了这个注解的功能支持。
它的使用方式和@Autowired 完全相同,最大的差异于@Resource 可以支持ByName 和 ByType 两种注入方式。
如果使用 name,Spring 就根据 bean 的名字进行依赖注入,如果使用 type,Spring就根据类型实现依赖注入。
如果两个属性都没配置,就先根据定义的属性名字去匹配,如果没匹配成功,再根据类型匹配。两个都没匹配到,就报错。

最后,我再总结一下。

  • @Autowired 是根据 type 来匹配,@Resource 可以根据 name 和 type 来匹配,默认是 name 匹配。
  • @Autowired 是 Spring 定义的注解,@Resource 是 JSR 250 规范里面定义的注解,而 Spring 对 JSR 250 规范提供了支持。
  • @Autowired 如果需要支持 name 匹配,就需要配合@Primary 或者@Qualifier来实现。

3.2 @Conditional 注解

@Conditional 注解的作用是为 Bean 的装载提供了一个条件判断。
只有满足条件的情况下,Spring 才会把当前 Bean 装载到 IOC 容器中。
这个条件的实现逻辑,我们可以实现 Condition 接口并重写 matches 方法自己去实现。
所以@Conditional 注解增加了 Bean 装载的灵活性。
在 Spring Boot 里面,对@Conditional 注解做了更进一步的扩展,比如增加了
@ConditionalOnClass、@ConditionalOnBean等注解,使得我们在使用的过程中不再需要去写条件的逻辑

3.3 为什么有些公司禁止使用@Transactional 声明式事务

  1. 在方法上增加@Transaction 声明式事务,如果一个方法中的存在较多耗时操作,就容易引发长事物问题,而长事物会带来锁的竞争影响性能,同时也会导致数据库连接池被耗尽,影响程序的正常执行。
  2. 如果方法存在嵌套调用,而被嵌套调用的方法也声明了@Transaction 事物,就会出现事物的嵌套调用行为,容易引起事物的混乱造成程序运行结果出现异常
  3. @Transaction 声明式事务是将事物控制逻辑放在注解中,如果项目的复杂度增加,事务的控制可能会变得更加复杂,导致代码可读性和维护性下降。
    所以,为了避免这类问题,有些公司会推荐使用编程式事务,这样可以更加灵活地控制事务的范围,减少事务的锁定时间,提高系统的性能

3.4 Spring常见注解

第一类,声明bean,@Component @Service @Repository @Controller
第二类,依赖注入相关 @Autowired @Qualifier @Resourse
第三类,设置作用域 @Scope
第四类,配置相关 @Configuration @ComponentScan @Bean
第五类,aop相关 @Aspect @Before @After @Around @Pointcut
  • 1
  • 2
  • 3
  • 4
  • 5

3.5 SpringMVC常见注解

@RequestMapping:用于映射请求路径;
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象;
@RequestParam:指定请求参数的名称;
@PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数;
    @ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。
    @RequestHeader:获取指定的请求头数据,
@PostMapping@GetMapping这些。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

4. Mybatis 面试题

4.1 Mybatis 里面的缓存机制

首先,Mybatis 里面设计了二级缓存来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。
一级缓存,是 SqlSession 级别的缓存,也叫本地缓存,因为每个用户在执行查询的时候都需要使用 SqlSession 来执行,为了避免每次都去查数据库,Mybatis 把查询出来的数据保存到 SqlSession 的本地缓存中,后续的 SQL 如果命中缓存,就可以直接从本地缓存读取了。
如果想要实现跨 SqlSession 级别的缓存?那么一级缓存就无法实现了,因此在Mybatis 里面引入了二级缓存,就是当多个用户
在查询数据的时候,只有有任何一个 SqlSession 拿到了数据就会放入到二级缓存里面,其他的 SqlSession 就可以从二级缓存加载数据。
每个一级缓存的具体实现原理是:
在 SqlSession 里面持有一个 Executor,每个 Executor 中有一个 LocalCache 对象。
当用户发起查询的时候,Mybatis 会根据执行语句在 Local Cache 里面查询,如果没命中,再去查询数据库并写入到 LocalCache,否则直接返回。
所以,以及缓存的生命周期是 SqlSessiion,而且在多个 Sqlsession 或者分布式环境下,可能会导致数据库写操作出现脏数据。
二级缓存的具体实现原理是:
使用 CachingExecutor 装饰了 Executor,所以在进入一级缓存的查询流程之前,会先通过 CachingExecutor 进行二级缓存的查询。
开启二级缓存以后,会被多个 SqlSession 共享,所以它是一个全局缓存。因此它的查询流程是先查二级缓存,再查一级缓存,最后再查数据库。
另外,MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时缓存粒度也能够到 namespace 级别,并且还可以通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强
:::info
mybatis的一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
关于二级缓存需要单独开启
二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储。
如果想要开启二级缓存需要在全局配置文件和映射文件中开启配置才行。
:::

4.2 #{}和${}的区别

首先,Mybatis 提供到的#号占位符和KaTeX parse error: Expected 'EOF', got '#' at position 76: …对这两种占位符进行动态解析。 #̲号占位符,等同于 jdbc 里…的方式传参,相当于直接把参数拼接到了原始的 SQL 里面,Mybatis不会对它进行特殊处理。
所以KaTeX parse error: Expected 'EOF', got '#' at position 2: 和#̲最大的区别在于,前者是动态参数…符号的动态传参,可以适合应用在一些动态 SQL 场景中,比如动态传递表名、动态设置排序字段等。

4.3 Mybatis 是如何进行分页

数据进行分页是最基础的功能,一般可以把分页分成两类:
 逻辑分页,先查询出所有的数据缓存到内存,再根据业务相关需求,从内存数据中筛选出合适的数据进行分页。
 物理分页 ,直接利用数据库支持的分页语法来实现,比如 Mysql 里面提供了分页关键词 Limit
Mybatis 提供了四种分页方式:

  1. 在 Mybatis Mapper 配置文件里面直接写分页 SQL,这种方式比较灵活,实现也简单
  2. RowBounds 实现逻辑分页,也就是一次性加载所有符合查询条件的目标数据,根据分页参数值在内存中实现分页当然,在数据量比较大的情况下,JDBC 驱动本身会做一些优化,也就是不会把所有结果存储在 ResultSet 里面,而是只加载一部分数据,再根据需求去数据库里面加载。这种方式不适合数据量较大的场景,而且有可能会频繁访问数据库造成比较大的压力。
  3. Interceptor 拦截器实现,通过拦截需要分页的 select 语句,然后在这个 sql 语句里面动态拼接分页关键字,从而实现分页查询。Interceptor 是 Mybatis 提供的一种针对不同生命周期的拦截器,比如:

 拦截执行器方法
 拦截参数的处理
 拦截结果集的处理
 拦截 SQL 语法构建的处理
我们可以拦截不同阶段的处理,来实现 Mybatis 相关功能的扩展。
image.png
这种方式的好处,就是可以提供统一的处理机制,不需要我们再单独去维护分页相关的功能。

  1. 插件(PageHelper)及(MyBaits-Plus、tkmybatis)框架实现

这些插件本质上也是使用 Mybatis 的拦截器来实现的。
只是他们帮我们实现了扩展和封装,节省了分页扩展封装的工作量,在实际开发中,只需要拿来即用即可。

我认为有三种方式来实现分页:
 第一种,直接在 Select 语句上增加数据库提供的分页关键字,然后在应用程序里面传递当前页,以及每页展示条数即可。
 第二种,使用 Mybatis 提供的 RowBounds 对象,实现内存级别分页。
 第三种,基于 Mybatis 里面的 Interceptor 拦截器,在 select 语句执行之前动态拼接分页关键字。

4.4 执行流程

①读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
②构造会话工厂SqlSessionFactory,一个项目只需要一个,单例的,一般由spring进行管理
③会话工厂创建SqlSession对象,这里面就含了执行SQL语句的所有方法
④操作数据库的接口,Executor执行器,同时负责查询缓存的维护
⑤Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
⑥输入参数映射
⑦输出结果映射
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

5. 写在最后 :

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/67820
推荐阅读
相关标签