SpingBoot

starter

Starter是Spring Boot中的一个非常重要的概念,Starter相当于模块,它能将模块所需的依赖整合起来并对模块内的Bean根据环境( 条件)进行自动配置。使用者只需要依赖相应功能的Starter,无需做过多的配置和依赖,Spring Boot就能自动扫描并加载相应的模块。

Spring Boot通过提供众多起步依赖(Starter)降低项目依赖的复杂度。起步依赖本质上是一个Maven项目对象模型(Project Object Model, POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。很多起步依赖的命名都暗示了它们提供的某种或某类功能。

实现一个Starter

编写Starter非常简单,与编写一个普通的Spring Boot应用没有太大区别,总结如下:

1
2
3
4
1.新建Maven项目,在项目的POM文件中定义使用的依赖;
2.新建配置类,写好配置项和默认的配置值,指明配置项前缀;
3.新建自动装配类,使用 @Configuration 和 @Bean 来进行自动装配;
4.新建spring.factories文件,指定Starter的自动装配类;

spring.factories文件位于resources/META-INF目录下,需要手动创建;
org.springframework.boot.autoconfigure.EnableAutoConfiguration后面的类名说明了自动装配类,如果有多个 ,则用逗号分开;
使用者应用(SpringBoot)在启动的时候,会通过org.springframework.core.io.support.SpringFactoriesLoader读取classpath下每个Starter的spring.factories文件,加载自动装配类进行Bean的自动装配;

SpringBoot启动过程?

1
2
3
4
5
6
@SpringBootApplication
public class SpringDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDemoApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 静态方法
*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[]{primarySource}, args);
}

/**
* 调用此方法启动会使用默认设置和用户提供的参数args
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
// 实例化SpringApplication,然后调用run
return new SpringApplication(primarySources).run(args);
}

SpringBoot的启动过程主要分为了两个过程:

SpringBoot应用程序的启动流程主要包括初始化SpringApplication和运行SpringApplication两个过程。其中初始化SpringApplication包括配置基本的环境变量、资源、构造器和监听器,为运行SpringApplciation实例对象作准备;而运行SpringApplication实例为应用程序正式启动加载过程,包括SpringApplicationRunListeners 引用启动监控模块、ConfigrableEnvironment配置环境模块和监听及ConfigrableApplicationContext配置应用上下文。当完成刷新应用的上下文和调用SpringApplicationRunListener#contextPrepared方法后表示SpringBoot应用程序已经启动完成。

  1. 调用SpringApplication的构造函数实例化SpringApplication
  2. 用实例化后的对象调用run()方法

调用SpringApplication的构造函数实例化SpringApplication

1
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {...}
  1. 配置primarySources
  2. 推断配置环境是否为web环境
  3. 获取启动加载器
  4. 创建初始化构造器setInitializers
  5. 创建应用监听器
  6. 配置应用主方法所在类(就是main方法所在类)

用实例化后的对象调用run()方法

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
50
51
52
53
54
55
56
57
58
/**
* 运行spring应用程序,创建并刷新一个新的 {@link ApplicationContext}.
*
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
// 计时工具
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 创建启动上下文对象
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 第一步:获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//创建应用程序环境 配置文件在此处读取(application.properties application.yml)
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
// 第三步:打印banner,就是启动的时候在console的spring图案
Banner printedBanner = printBanner(environment);
// 第四步:创建spring容器
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 第五步:spring容器前置处理
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 第六步:刷新容器
refreshContext(context);
// 第七步:spring容器后置处理
afterRefresh(context, applicationArguments);
stopWatch.stop(); // 结束计时器并打印,这就是我们启动后console的显示的时间
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 发出启动结束事件
listeners.started(context);
// 执行runner的run方法
callRunners(context, applicationArguments);
} catch (Throwable ex) {
// 异常处理,如果run过程发生异常
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
} catch (Throwable ex) {
// 异常处理,如果run过程发生异常
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
// 返回最终构建的容器对象
return context;
}

  1. 启动一个计时器,启动完成后会打印耗时
  2. 获取并启动监听器 SpringApplicationRunListeners。springboot在启动过程中会调用监听器模块,将开始事件、环境准备事件、启动完成/失败、准备完成等事件发布出去
  3. 配置环境 ConfigurableEnvironment,
  4. Banner配置,就是控制台的那个spirng
  5. 创建spring容器
  6. 前置处理、刷新、后置处理ConfigurableApplicationContext
  7. 发出启动结束事件并结束计时

SpringBoot启动过程中最核心的部分就是刷新容器。这里做了很多:工厂配置,bean处理器配置,类的扫描,解析,bean定义,bean类信息缓存,服务器创建,bean实例化,动态代理对象的创建等,

SpringBoot的自动配置原理

image-20220910115723050

image-20220910115739175

使用Spring Boot时,我们只需引入对应的Starters,Spring Boot启动时便会自动加载相关依赖,配置相应的初始化参数,以最快捷、简单的形式对第三方软件进行集成,

img

整个自动装配的过程是:Spring Boot通过@EnableAutoConfiguration注解开启自动配置,加载spring.factories中注册的各种AutoConfiguration类,当某个AutoConfiguration类满足其注解@Conditional指定的生效条件(Starters提供的依赖、配置或Spring容器中是否存在某个Bean等)时,实例化该AutoConfiguration类中定义的Bean(组件等),并注入Spring容器,就可以完成依赖框架的自动配置。

SpringBoot相关注解

Spring Boot 常用注解汇总 - 云天 - 博客园 (cnblogs.com)

@SpringBootApplication:@SpringBootApplication是一个复合注解,包含了@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan这三个注解

@RequestBody 注解则是将 HTTP 请求正文插入方法中,使用适合的 HttpMessageConverter 将请求体写入某个对象。
作用:该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上; 再把HttpMessageConverter返回的对象数据绑定到 controller中方法的参数上。

@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML数据。

@Controller:控制器,处理http请求。

@RestController 复合注解。@RestController注解相当于@ResponseBody+@Controller合在一起的作用,RestController使用的效果是将方法返回的对象直接在浏览器上展示成json格式.

@RequestMapping 是 Spring Web 应用程序中最常被用到的注解之一。这个注解会将 HTTP 请求映射到 MVC 和 REST 控制器的处理方法上

@PathVariable:获取url中的数据

@RequestParam:获取请求参数的值

@RequestHeader 把Request请求header部分的值绑定到方法的参数上

@CookieValue 把Request header中关于cookie的值绑定到方法的参数上

@Repository:DAO层注解,DAO层中接口继承JpaRepository<T,ID extends Serializable>,需要在build.gradle中引入相关jpa的一个jar自动加载。

@Service是@Component注解的一个特例,作用在类上。使用注解配置和类路径扫描时,被@Service注解标注的类会被Spring扫描并注册为Bean

@Scope作用在类上和方法上,用来配置 spring bean 的作用域,它标识 bean 的作用域

@PropertySource注解引入单个properties文件:

@ImportResource导入xml配置文件

@Import 导入额外的配置信息

@Transactional:在Spring中,事务有两种实现方式,分别是编程式事务管理和声明式事务管理两种方式

  • 编程式事务管理: 编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
  • 声明式事务管理: 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务,通过@Transactional就可以进行事务操作,更快捷而且简单。推荐使用

@ControllerAdvice 统一处理异常

@ExceptionHandler 注解声明异常处理方法

Spring 使用

Spring的核心

Spring框架包含众多模块,如Core、Test、Data Access、Web Servlet等,其中Core是整个Spring框架的核心模块。Core模块提供了IoC容器、AOP功能、数据绑定、类型转换等一系列的基础功能,而这些功能以及其他模块的功能都是建立在IoC和AOP之上的,所以IoC和AOP是Spring框架的核心。

IoC(Inversion of Control)是控制反转的意思,这是一种面向对象编程的设计思想。在不采用这种思想的情况下,我们需要自己维护对象与对象之间的依赖关系,很容易造成对象之间的耦合度过高,在一个大型的项目中这十分的不利于代码的维护。IoC则可以解决这种问题,它可以帮我们维护对象与对象之间的依赖关系,降低对象之间的耦合度。

说到IoC就不得不说DI(Dependency Injection),DI是依赖注入的意思,它是IoC实现的实现方式,就是说IoC是通过DI来实现的。而实现 DI 的关键是IoC容器,它的本质就是一个工厂。

Spring主要模块

Spring Data Access/Integration 由 5 个模块组成:

  • spring-jdbc : 提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
  • spring-tx : 提供对事务的支持。
  • spring-orm : 提供对 Hibernate 等 ORM 框架的支持。
  • spring-oxm : 提供对 Castor 等 OXM 框架的支持。
  • spring-jms : Java 消息服务。

Spring Web 由 4 个模块组成:

  • spring-web :对 Web 功能的实现提供一些最基础的支持。
  • spring-webmvc : 提供对 Spring MVC 的实现。
  • spring-websocket : 提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
  • spring-webflux :提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步.

Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

img

Bean的作用域

参考答案

默认情况下,Bean在Spring容器中是单例的,我们可以通过@Scope注解修改Bean的作用域。该注解有如下5个取值,它们代表了Bean的5种不同类型的作用域:

类型 说明
prototype 每次调用getBean()时,都会执行new操作,返回一个新的实例。
singleton 在Spring容器中仅存在一个实例,即Bean以单例的形式存在。
request 每次HTTP请求都会创建一个新的Bean。
session 同一个HTTP Session共享一个Bean,不同的HTTP Session使用不同的Bean。
globalSession 同一个全局的Session共享一个Bean,一般用于Portlet环境。

Bean的生命周期

主要是四个阶段:

  • 实例化 Instantiation
  • 属性赋值 Populate
  • 初始化 Initialization
  • 销毁 Destruction

1.如果创建了一个类继承了InstantiationAwareBeanPostProcessorAdapter接口,并在配置文件中配置了该类的注入,即InstantiationAwareBeanPostProcessorAdapter和bean关联,则Spring将调用该接口的postProcessBeforeInstantiation()方法。
2.根据配置情况调用 Bean 构造方法或工厂方法实例化 Bean。
3.如果InstantiationAwareBeanPostProcessorAdapter和bean关联,则Spring将调用该接口postProcessAfterInstantiation方法。
4.利用依赖注入完成 Bean 中所有属性值的配置注入。

5.如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值。
6.如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。
7.如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。

8.如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的
9.如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。

10.如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。

11.如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。

注意:以上工作完后才能以后就可以应用这个bean了,那这个bean是一个singleton的,所以一般这种情况下我们调用同一个id的bean会是在内容地址相同的实例,当然在spring配置文件中也可以配置非Singleton。

12.如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法将 Spring 中的 Bean 销毁;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

img

img

对于普通的 Java 对象,当 new 的时候创建对象,然后该对象就能够使用了。一旦该对象不再被使用,则由 Java 自动进行垃圾回收。

而 Spring 中的对象是 bean,bean 和普通的 Java 对象没啥大的区别,只不过 Spring 不再自己去 new 对象了,而是由 IoC 容器去帮助我们实例化对象并且管理它,我们需要哪个对象,去问 IoC 容器要即可。IoC 其实就是解决对象之间的耦合问题,Spring Bean 的生命周期完全由容器控制。

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
  • session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
  • global-session: 全局 session 作用域,仅仅在基于 Portlet 的 web 应用中才有意义,Spring5 已经没有了。Portlet 是能够

生成语义代码(例如:HTML)片段的小型 Java Web 插件。它们基于 portlet 容器,可以像 servlet 一样处理 HTTP 请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。

Spring三级缓存

Spring系列第56篇:一文搞懂spring到底为什么要用三级缓存?? - 腾讯云开发者社区-腾讯云 (tencent.com)

Spring三级缓存是为了解决对象间的循环依赖问题。

构造器注入构成的循环依赖,此种循环依赖方式是无法解决的,创建 A 的时候需要先有 B,而创建 B 的时候需要先有 A,导致无法创建成功。只能抛出BeanCurrentlyInCreationException异常表示循环依赖。这也是构造器注入的最大劣势

根本原因:Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而这个中间态指的是已经实例化,但还没初始化的状态。而构造器是完成实例化的,所以构造器的循环依赖无法解决

Spring是如何发现循环依赖的?

1、从singletonObjects查看是否有a,此时没有

2、准备创建a

3、判断a是否在singletonsCurrentlyInCreation列表,此时明显不在,则将a加入singletonsCurrentlyInCreation列表

4、调用a的构造器A(B b)创建A

5、spring发现A的构造器需要用到b

6、则向spring容器查找b,从singletonObjects查看是否有b,此时没有

7、spring准备创建b

8、判断b是否在singletonsCurrentlyInCreation列表,此时明显不在,则将b加入singletonsCurrentlyInCreation列表

9、调用b的构造器B(A a)创建b

10、spring发现B的构造器需要用到a,则向spring容器查找a

11、则向spring容器查找a,从singletonObjects查看是否有a,此时没有

12、准备创建a

13、判断a是否在singletonsCurrentlyInCreation列表,上面第3步中a被放到了这个列表,此时a在这个列表中,走到这里了,说明a已经存在创建列表中了,此时程序又来创建a,说明这么一直走下去会死循环,此时spring会弹出异常,终止bean的创建操作。

三级缓存概述

spring 中使用了 3 个 map 来作为三级缓存,每一级对应一个 map

第几级缓存 对应的 map 说明
第 1 级 Map<String, Object> singletonObjects 用来存放已经完全创建好的单例 beanName->bean 实例
第 2 级 Map<String, Object> earlySingletonObjects 用来存放早期的 beanName->bean 实例
第 3 级 Map<String, ObjectFactory<?>> singletonFactories 用来存放单例 bean 的 ObjectFactorybeanName->ObjectFactory 实例
  • singletonObject:一级缓存,存放完全实例化且属性赋值完成的 Bean ,可以直接使用
  • earlySingletonObjects:二级缓存,存放早期 Bean 的引用,尚未装配属性的 Bean
  • singletonFactories:三级缓存,存放实例化完成的 Bean 工厂

img

单例 bean 创建过程源码解析

step1:

doGetBean

这个方法内部会调用getSingleton(beanName),而这个方法内部会调用getSingleton(beanName, true)获取 bean,注意第二个参数是true,这个表示是否可以获取早期的 bean,这个参数为 true,会尝试从三级缓存singletonFactories中获取 bean,然后将三级缓存中获取到的 bean 丢到二级缓存中。

step2:

getSingleton(beanName, true)

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
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//从第1级缓存中获取bean
Object singletonObject = this.singletonObjects.get(beanName);
//第1级中没有,且当前beanName在创建列表中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//从第2级缓存汇总获取bean
singletonObject = this.earlySingletonObjects.get(beanName);
//第2级缓存中没有 && allowEarlyReference为true,也就是说2级缓存中没有找到bean且beanName在当前创建列表中的时候,才会继续想下走。
if (singletonObject == null && allowEarlyReference) {
//从第3级缓存中获取bean
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
//第3级中有获取到了
if (singletonFactory != null) {
//3级缓存汇总放的是ObjectFactory,所以会调用其getObject方法获取bean
singletonObject = singletonFactory.getObject();
//将3级缓存中的bean丢到第2级中
this.earlySingletonObjects.put(beanName, singletonObject);
//将bean从三级缓存中干掉
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}

step3:

getSingleton(String beanName, ObjectFactory<?> singletonFactory)

上面调用getSingleton(beanName, true)没有获取到 bean,所以会继续走 bean 的创建逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
//从第1级缓存中获取bean,如果可以获取到,则自己返回
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//将beanName加入当前创建列表中
beforeSingletonCreation(beanName);
//①:创建单例bean
singletonObject = singletonFactory.getObject();
//将beanName从当前创建列表中移除
afterSingletonCreation(beanName);
//将创建好的单例bean放到1级缓存中,并将其从2、3级缓存中移除
addSingleton(beanName, singletonObject);
}
return singletonObject;
}

step4:

doCreateBean

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// ①:创建bean实例,通过反射实例化bean,相当于new X()创建bean的实例
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);

// bean = 获取刚刚new出来的bean
Object bean = instanceWrapper.getWrappedInstance();

// ②:是否需要将早期的bean暴露出去,所谓早期的bean相当于这个bean就是通过new的方式创建了这个对象,但是这个对象还没有填充属性,所以是个半成品
// 是否需要将早期的bean暴露出去,判断规则(bean是单例 && 是否允许循环依赖 && bean是否在正在创建的列表中)
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));

if (earlySingletonExposure) {
//③:调用addSingletonFactory方法,这个方法内部会将其丢到第3级缓存中,getEarlyBeanReference的源码大家可以看一下,内部会调用一些方法获取早期的bean对象,比如可以在这个里面通过aop生成代理对象
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// 这个变量用来存储最终返回的bean
Object exposedObject = bean;
//填充属性,这里面会调用setter方法或者通过反射将依赖的bean注入进去
populateBean(beanName, mbd, instanceWrapper);
//④:初始化bean,内部会调用BeanPostProcessor的一些方法,对bean进行处理,这里可以对bean进行包装,比如生成代理
exposedObject = initializeBean(beanName, exposedObject, mbd);


//早期的bean是否被暴露出去了
if (earlySingletonExposure) {
/**
*⑤:getSingleton(beanName, false),注意第二个参数是false,这个为false的时候,
* 只会从第1和第2级中获取bean,此时第1级中肯定是没有的(只有bean创建完毕之后才会放入1级缓存)
*/
Object earlySingletonReference = getSingleton(beanName, false);
/**
* ⑥:如果earlySingletonReference不为空,说明第2级缓存有这个bean,二级缓存中有这个bean,说明了什么?
* 大家回头再去看看上面的分析,看一下什么时候bean会被放入2级缓存?
* (若 bean存在三级缓存中 && beanName在当前创建列表的时候,此时其他地方调用了getSingleton(beanName, false)方法,那么bean会从三级缓存移到二级缓存)
*/
if (earlySingletonReference != null) {
//⑥:exposedObject==bean,说明bean创建好了之后,后期没有被修改
if (exposedObject == bean) {
//earlySingletonReference是从二级缓存中获取的,二级缓存中的bean来源于三级缓存,三级缓存中可能对bean进行了包装,比如生成了代理对象
//那么这个地方就需要将 earlySingletonReference 作为最终的bean
exposedObject = earlySingletonReference;
} else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
//回头看看上面的代码,刚开始exposedObject=bean,
// 此时能走到这里,说明exposedObject和bean不一样了,他们不一样了说明了什么?
// 说明initializeBean内部对bean进行了修改
// allowRawInjectionDespiteWrapping(默认是false):是否允许早期暴露出去的bean(earlySingletonReference)和最终的bean不一致
// hasDependentBean(beanName):表示有其他bean以利于beanName
// getDependentBeans(beanName):获取有哪些bean依赖beanName
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
//判断dependentBean是否已经被标记为创建了,就是判断dependentBean是否已经被创建了
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
/**
*
* 能走到这里,说明早期的bean被别人使用了,而后面程序又将exposedObject做了修改
* 也就是说早期创建的bean是A,这个A已经被有些地方使用了,但是A通过initializeBean之后可能变成了B,比如B是A的一个代理对象
* 这个时候就坑了,别人已经用到的A和最终容器中创建完成的A不是同一个A对象了,那么使用过程中就可能存在问题了
* 比如后期对A做了增强(Aop),而早期别人用到的A并没有被增强
*/
if (!actualDependentBeans.isEmpty()) {
//弹出异常(早期给别人的bean和最终容器创建的bean不一致了,弹出异常)
throw new BeanCurrentlyInCreationException(beanName,"异常内容见源码。。。。。");
}
}
}
}

return exposedObject;
}

下面来看 A、B 类 setter 循环依赖的创建过程

1、getSingleton(“a”, true) 获取 a:会依次从 3 个级别的缓存中找 a,此时 3 个级别的缓存中都没有 a

2、将 a 丢到正在创建的 beanName 列表中(Set singletonsCurrentlyInCreation)

3、实例化 a:A a = new A();这个时候 a 对象是早期的 a,属于半成品

4、将早期的 a 丢到三级缓存中(Map<String, ObjectFactory<?> > singletonFactories)

5、调用 populateBean 方法,注入依赖的对象,发现 setB 需要注入 b

6、调用 getSingleton(“b”, true) 获取 b:会依次从 3 个级别的缓存中找 a,此时 3 个级别的缓存中都没有 b

7、将 b 丢到正在创建的 beanName 列表中

8、实例化 b:B b = new B();这个时候 b 对象是早期的 b,属于半成品

9、将早期的 b 丢到三级缓存中(Map<String, ObjectFactory<?> > singletonFactories)

10、调用 populateBean 方法,注入依赖的对象,发现 setA 需要注入 a

11、调用 getSingleton(“a”, true) 获取 a:此时 a 会从第 3 级缓存中被移到第 2 级缓存,然后将其返回给 b 使用,此时 a 是个半成品(属性还未填充完毕)

12、b 通过 setA 将 11 中获取的 a 注入到 b 中

13、b 被创建完毕,此时 b 会从第 3 级缓存中被移除,然后被丢到 1 级缓存

14、b 返回给 a,然后 b 被通过 A 类中的 setB 注入给 a

15、a 的 populateBean 执行完毕,即:完成属性填充,到此时 a 已经注入到 b 中了

16、调用a= initializeBean("a", a, mbd)对 a 进行处理,这个内部可能对 a 进行改变,有可能导致 a 和原始的 a 不是同一个对象了

17、调用getSingleton("a", false)获取 a,注意这个时候第二个参数是 false,这个参数为 false 的时候,只会从前 2 级缓存中尝试获取 a,而 a 在步骤 11 中已经被丢到了第 2 级缓存中,所以此时这个可以获取到 a,这个 a 已经被注入给 b 了

18、此时判断注入给 b 的 a 和通过initializeBean方法产生的 a 是否是同一个 a,不是同一个,则弹出异常

preload

从上面的过程中我们可以得到一个非常非常重要的结论

当某个 bean 进入到 2 级缓存的时候,说明这个 bean 的早期对象被其他 bean 注入了,也就是说,这个 bean 还是半成品,还未完全创建好的时候,已经被别人拿去使用了,所以必须要有 3 级缓存,2 级缓存中存放的是早期的被别人使用的对象,如果没有 2 级缓存,是无法判断这个对象在创建的过程中,是否被别人拿去使用了。

三级缓存是为了判断循环依赖的时候,早期暴露出去已经被别人使用的 bean 和最终的 bean 是否是同一个 bean,如果不是同一个则弹出异常,如果早期的对象没有被其他 bean 使用,而后期被修改了,不会产生异常,如果没有三级缓存,是无法判断是否有循环依赖,且早期的 bean 被循环依赖中的 bean 使用了。

IOC容器

原理

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

什么是 IOC

  • 控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理
  • 使用 IOC 目的:为了耦合度降低
  • IOC 底层原理 :xml 解析、工厂模式、反射

IOC 思想基于 IOC 容器完成,IOC 容器底层就是对象工厂

Spring 提供 IOC 容器实现两种方式:(两个接口)

  • BeanFactory:IOC 容器基本实现,是 Spring 内部的使用接口,不提供开发人员进行使用 * 加载配置文件时候不会创建对象,在获取对象(使用)才去创建对象
  • ApplicationContext:BeanFactory 接口的子接口,提供更多更强大的功能,一般由开发人 员进行使用 * 加载配置文件时候就会把在配置文件对象进行创建
1
2
3
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); //xml文件
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); //完全注解开发

什么是 Bean 管理 ?

Bean 管理指的是两个操作 :(1)Spring 创建对象 (2)Spirng 注入属性

DI 存在两个主要变体:基于构造函数的依赖注入基于 Setter 的依赖注入

在 setter 方法上使用@Required注解可用于使属性成为必需的依赖项。

Spring 针对 Bean 管理中创建对象提供注解,下面四个注解功能是一样的,都可以用来创建 bean 实例

  • @Component
  • @Service
  • @Controller
  • @Repository

什么是自动装配 ?

(1)根据指定装配规则(属性名称或者属性类型),Spring 自动将匹配的属性值进行注入

  • @Autowired:根据属性类型进行自动装配
  • @Qualifier:根据名称进行注入 这个@Qualifier 注解的使用,和上面@Autowired 一起使用
  • @Resource:可以根据类型注入,可以根据名称注入
  • @Value:注入普通类型属性

在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

注解@Autowired与@Resource的区别:

  • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
  • Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。
  • 当一个接口存在多个实现类的情况下,@Autowired@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显示指定名称,@Resource可以通过 name 属性来显示指定名称

AOP

AOP(Aspect Oriented Programing)是面向切面编程思想,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得 业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。这种思想是对OOP的补充,它可以在OOP的基础上进一步提高编程的效率。简单来说,它可以统一解决一批组件的共性需求(如权限检查、记录日志、事务管理等)。在AOP思想下,我们可以将解决共性需求的代码独立出来,然后通过配置的方式,声明这些代码在什么地方、什么时机调用。当满足调用条件时,AOP会将该业务代码织入到我们指定的位置,从而统一解决了问题,又不需要修改这一批组件的代码。

第一种 有接口情况,使用 JDK 动态代理,创建接口实现类代理对象,增强类的方法

第二种 没有接口情况,使用 CGLIB 动态代理 , 通过创建子类对象作为代理对象,增强类的方法

动态代理示例:

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
/*
要实现代理类需要解决的问题:
1:如何通过被加载到内存中的被代理类,动态的创建代理类及其对象
2:当通过代理类的对象调用某些方法时,如何动态的调用被代理类的相应的同名方法。
*/
interface Human{
void LikedFood(String Food);
}
class superMan implements Human{
@Override
public void LikedFood(String Food) {
System.out.println("我喜欢吃"+Food);
}
public void fight(){
System.out.println("保护地球");
}
}
class MyInvoketionHandler implements InvocationHandler {
private Object obj;
public MyInvoketionHandler(Object o){
obj=o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue= method.invoke(obj,args);
System.out.println("sssss");
return returnValue;
}
}

public class ProxyTest {
public static Object getProxyInstance(Object object){//obj:被代理类的实例
MyInvoketionHandler handler=new MyInvoketionHandler(object);
return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),handler);//创建代理类的对象。解决问题一
}
}

class Test{
public static void main(String[] args) {

Human superman=new superMan();
Human ProxyInstance=(Human) ProxyTest.getProxyInstance(superman);//动态的创建代理类的对象
//由代理对象去执行这个方法
ProxyInstance.LikedFood("四川麻辣烫");
}
}

Spring AOP 和 AspectJ AOP 有什么区别?

AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

AOP 领域中的特性术语:

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
  • 切点(PointCut): 可以插入增强处理的连接点。
  • 切面(Aspect): 切面是通知和切点的结合。
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

AOP带来的好处:

  • 低耦合性
  • 可扩展性
  • 简化代码

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

请你说说AOP的应用场景

Spring AOP为IoC的使用提供了更多的便利,一方面,应用可以直接使用AOP的功能,设计应用的横切关注点,把跨越应用程序多个模块的功能抽象出来,并通过简单的AOP的使用,灵活地编制到模块中,比如可以通过AOP实现应用程序中的日志功能。另一方面,在Spring内部,一些支持模块也是通过Spring AOP来实现的,比如事务处理。从这两个角度就已经可以看到Spring AOP的核心地位了。

Spring AOP不能对哪些类进行增强?

  1. Spring AOP只能对IoC容器中的Bean进行增强,对于不受容器管理的对象不能增强。
  2. 由于CGLib采用动态创建子类的方式生成代理对象,所以不能对final修饰的类进行代理。

动态代理

代理模式是一种设计模式,能够使得在不修改源目标的前提下,额外扩展源目标的功能。即通过访问源目标的代理类,再由代理类去访问源目标。这样一来,要扩展功能,就无需修改源目标的代码了。只需要在代理类上增加就可以了。

其实代理模式的核心思想就是这么简单,在java中,代理又分静态代理和动态代理2种,其中动态代理根据不同实现又区分基于接口的的动态代理和基于子类的动态代理。

静态代理这种模式虽然好理解,但是缺点也很明显:

  • 会存在大量的冗余的代理类,这里演示了2个接口,如果有10个接口,就必须定义10个代理类。
  • 不易维护,一旦接口更改,代理类和目标类都需要更改。

Jdk中的动态代理

JDK中的动态代理是通过反射类Proxy以及InvocationHandler回调接口实现的,但是JDK中所有要进行动态代理的类必须要实现一个接口,也就是说只能对该类所实现接口中定义的方法进行代理,这在实际编程中有一定的局限性,而且使用反射的效率也不高

Cglib实现

使用cglib是实现动态代理,不受代理类必须实现接口的限制,因为cglib底层是用ASM框架,利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理。比使用Java反射的效率要高,cglib不能对声明final的方法进行代理,因为cglib原理是动态生成被代理类的子类

什么时候用cglib什么时候用jdk动态代理?

1、目标对象生成了接口 默认用JDK动态代理

2、如果目标对象使用了接口,可以强制使用cglib

3、如果目标对象没有实现接口,必须采用cglib库,Spring会自动在JDK动态代理和cglib之间转换

JDK动态代理和cglib字节码生成的区别?

  • JDK动态代理只能对实现了接口的类生成代理,而不能针对类
  • Cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要生成final,对于final类或方法,是无法继承的

Cglib比JDK快?

  • cglib底层是ASM字节码生成框架,但是字节码技术生成代理类,在JDL1.6之前比使用java反射的效率要高
  • 在jdk6之后逐步对JDK动态代理进行了优化,在调用次数比较少时效率高于cglib代理效率
  • 只有在大量调用的时候cglib的效率高,但是在1.8的时候JDK的效率已高于cglib
  • Cglib不能对声明final的方法进行代理,因为cglib是动态生成代理对象,final关键字修饰的类不可变只能被引用不能被修改

Spring如何选择是用JDK还是cglib?

  • 当bean实现接口时,会用JDK代理模式
  • 当bean没有实现接口,用cglib实现
  • 可以强制使用cglib(在spring配置中加入<aop:aspectj-autoproxy proxyt-target-class=”true”/>)

还有: 在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理。

动态代理,通俗点说就是:无需声明式的创建java代理类,而是在运行过程中生成”虚拟”的代理类,被ClassLoader加载。从而避免了静态代理那样需要声明大量的代理类。JDK从1.3版本就开始支持动态代理类的创建。主要核心类只有2个:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler

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
/*处理代理实例上的方法并返回实例并返回结果,我们一般在此方法中添加一些额外的逻辑
* @param proxy: the proxy instance that the method was invoked on

* @param method the {@code Method} instance corresponding to
* the interface method invoked on the proxy instance. The declaring
* class of the {@code Method} object will be the interface that
* the method was declared in, which may be a superinterface of the
* proxy interface that the proxy class inherits the method through.

* @param args an array of objects containing the values of the
* arguments passed in the method invocation on the proxy instance,
* or {@code null} if interface method takes no arguments.
* Arguments of primitive types are wrapped in instances of the
* appropriate primitive wrapper class, such as
* {@code java.lang.Integer} or {@code java.lang.Boolean}.
*/

Object invoke(Object proxy, Method method, Object[] args)

/* @param loader: the class loader to define the proxy class
* @param interfaces: the list of interfaces for the proxy class to implement
* @param h : the invocation handler to dispatch method invocations to
*/
Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

//一般步骤为先new一个被代理对象,再生成调用处理器,最后得到代理对象的实例
//invoke()这个方法的第一个参数proxy可以用于返回,实现连续调用的效果

JDK的动态代理使用的最多的一种代理方式。也叫做接口代理。

JDK动态代理说白了只是根据接口”凭空“来生成类,至于具体的执行,都被代理到了InvocationHandler 的实现类里。上述例子我是需要继续执行原有bean的逻辑,才将原有的bean构造进来。只要你需要,你可以构造进任何对象到这个代理实现类。也就是说,你可以传入多个对象,或者说你什么类都不代理。只是为某一个接口”凭空“的生成多个代理实例,这多个代理实例最终都会进入InvocationHandler的实现类来执行某一个段共同的代码。

基于接口的代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface Person {
void wakeup();
void sleep();
}
public class JDKProxy implements InvocationHandler {

public JDKProxy(){ }
//处理代理实例上的方法并返回实例并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("invoke执行");
String s="test";
return s;
}
}
public class test {
public static void main(String[] args) {
JDKProxy proxy = new JDKProxy();
Person personProxy=(Person)Proxy.newProxyInstance(Person.class.getClassLoader(),
new Class[]{Person.class}, proxy);
personProxy.sleep();
}
}

CGLIB与JDK动态代理特点

在性能方面,CGLib创建的代理对象比JDK动态代理创建的代理对象高很多。但是,CGLib在创建代理对象时所花费的时间比JDK动态代理多很多。所以,对于单例的对象因为无需频繁创建代理对象,采用CGLib动态代理比较合适。反之,对于多例的对象因为需要频繁的创建代理对象,则JDK动态代理更合适。

事务

事务是数据库操作最基本单元,逻辑上一组操作,要么都成功,如果有一个失败所有操作都失败

事务四个特性(ACID)

  • (1)原子性
  • (2)一致性
  • (3)隔离性
  • (4)持久性

在spring中进行事务管理操作

有两种方式:编程式事务管理 和 声明式事务管理(使用)

其中的声明式事务管理 :

  • 基于注解方式(使用)
  • 基于 xml 配置文件方式

事务操作(声明式事务管理参数配置)

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
<bean id="transactionManager" 
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--
在 spring 配置文件,开启事务注解
在 spring 配置文件引入名称空间 tx
-->

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<!--开启事务注解-->
<tx:annotation-driven transactionmanager="transactionManager"></tx:annotation-driven>

1、在 service 类上面添加注解@Transactional,在这个注解里面可以配置事务相关参数

2、propagation:事务传播行为

image-20220415170446377

3、ioslation:事务隔离级别

4、timeout:超时时间

5、readOnly:是否只读

6、rollbackFor:回滚

7、noRollbackFor:不回滚

脏读,不可重复读,幻读

  • 脏读:一个未提交事务读取到另一个未提交事务的数据
  • 不可重复读:一个未提交事务读取到另一提交事务修改数据
  • 虚读:一个未提交事务读取到另一提交事务添加数据

事务的传播行为

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

正确的事务传播行为可能的值如下:

1.TransactionDefinition.PROPAGATION_REQUIRED

使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

2.TransactionDefinition.PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

3.TransactionDefinition.PROPAGATION_NESTED

如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

4.TransactionDefinition.PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚:

  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

Spring 事务管理接口介绍

Spring 框架中,事务管理相关最重要的 3 个接口如下:

  • **PlatformTransactionManager**: (平台)事务管理器,Spring 事务策略的核心。
  • **TransactionDefinition**: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
  • **TransactionStatus**: 事务运行状态。

我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinitionTransactionStatus 这两个接口可以看作是事务的描述。

PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。

PlatformTransactionManager

Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是: PlatformTransactionManager

通过这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

PlatformTransactionManager接口中定义了三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}

TransactionDefinition

接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。

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
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为 REQUIRED。
int getPropagationBehavior();
//返回事务的隔离级别,默认值是 DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();

@Nullable
String getName();
}

TransactionStatus:事务状态

TransactionStatus接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。

PlatformTransactionManager.getTransaction(…)方法返回一个 TransactionStatus 对象。

1
2
3
4
5
6
7
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}

@Transactional 的常用配置参数总结(只列出了 5 个我平时比较常用的):

属性名 说明
propagation 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过
isolation 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过
timeout 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly 指定事务是否为只读事务,默认值为 false。
rollbackFor 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型

spring项目中使用@Transactional注解,事务不生效

  1. 数据库引擎不支持事务
  2. 没有被 Spring 管理
  3. 方法不是 public 的
  4. 在没有事务的方法中去调用拥有事务的方法
  5. 数据源没有配置事务管理器
  6. 默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置

SpringMVC

DispatcherServlet处理流程?

img

在整个 Spring MVC 框架中,DispatcherServlet 处于核心位置,它负责协调和组织不同组件完成请求处理并返回响应工作。DispatcherServlet 是 SpringMVC统一的入口,所有的请求都通过它。DispatcherServlet 是前端控制器,配置在web.xml文件中,Servlet依自已定义的具体规则拦截匹配的请求,分发到目标Controller来处理。 初始化 DispatcherServlet时,该框架在web应用程序WEB-INF目录中寻找一个名为[servlet-名称]-servlet.xml的文件,并在那里定义相关的Beans,重写在全局中定义的任何Beans。

在看DispatcherServlet 类之前,我们先来看一下请求处理的大致流程:

  1. Tomcat 启动,对 DispatcherServlet 进行实例化,然后调用它的 init() 方法进行初始化,在这个初始化过程中完成了:对 web.xml 中初始化参数的加载;建立 WebApplicationContext(SpringMVC的IOC容器);进行组件的初始化;
  2. 客户端发出请求,由 Tomcat 接收到这个请求,如果匹配 DispatcherServlet 在 web.xml中配置的映射路径,Tomcat 就将请求转交给 DispatcherServlet 处理;
  3. DispatcherServlet 从容器中取出所有 HandlerMapping 实例(每个实例对应一个 HandlerMapping接口的实现类)并遍历,每个 HandlerMapping 会根据请求信息,通过自己实现类中的方式去找到处理该请求的 Handler(执行程序,如Controller中的方法),并且将这个 Handler 与一堆 HandlerInterceptor (拦截器)封装成一个 HandlerExecutionChain 对象,一旦有一个 HandlerMapping 可以找到 Handler则退出循环;
  4. DispatcherServlet 取出 HandlerAdapter 组件。根据已经找到的 Handler,再从所有HandlerAdapter 中找到可以处理该 Handler 的 HandlerAdapter 对象;
  5. 执行 HandlerExecutionChain 中所有拦截器的 preHandler() 方法,然后再利用HandlerAdapter 执行 Handler ,执行完成得到 ModelAndView,再依次调用拦截器的postHandler() 方法;
  6. 利用 ViewResolver 将 ModelAndView 或是 Exception(可解析成 ModelAndView)解析成View,然后 View 会调用 render() 方法再根据 ModelAndView 中的数据渲染出页面;
  7. 最后再依次调用拦截器的 afterCompletion() 方法,这一次请求就结束了。

什么是token

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

JWT token的组成
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准([(RFC 7519](https://link.jianshu.com?t=https://tools.ietf.org/html/rfc7519)).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

头部(Header),格式如下:
{
“typ”: “JWT”,
“alg”: “HS256”
}
由上可知,该token使用HS256加密算法,将头部使用Base64编码可得到如下个格式的字符串:eyJhbGciOiJIUzI1NiJ91

有效载荷(Playload):
{
“iss”: “Online JWT Builder”,
“iat”: 1416797419,
“exp”: 1448333419,
…….
“userid”:10001
}
有效载荷中存放了token的签发者(iss)、签发时间(iat)、过期时间(exp)等以及一些我们需要写进token中的信息。有效载荷也使用Base64编码得到如下格式的字符串:eyJ1c2VyaWQiOjB91

签名(Signature):
将Header和Playload拼接生成一个字符串str=“eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyaWQiOjB9”,使用HS256算法和我们提供的密钥(secret,服务器自己提供的一个字符串)对str进行加密生成最终的JWT,即我们需要的令牌(token),形如:str.”签名字符串”。

token在服务与客户端的交互流程

1:客户端通过用户名和密码登录
2:服务器验证用户名和密码,若通过,生成token返回给客户端。
3:客户端收到token后以后每次请求的时候都带上这个token,相当于一个令牌,表示我有权限访问了
4:服务器接收(通常在拦截器中实现)到该token,然后验证该token的合法性(为什么能验证下面说)。若该token合法,则通过请求,若token不合法或者过期,返回请求失败。


服务如何判断这个token是否合法?
由上面token的生成可知,token中的签名是由Header和有效载荷通过Base64编码生成再通过加密算法HS256和密钥最终生成签名,这个签名位于JWT的尾部,在服务器端同样对返回过来的JWT的前部分再进行一次签名生成,然后比较这次生成的签名与请求的JWT中的签名是否一致,若一致说明token合法。由于生成签名的密钥是服务器才知道的,所以别人难以伪造。

token中能放敏感信息吗?
不能,因为有效载荷是经过Base64编码生成的,并不是加密。所以不能存放敏感信息。


Token的优点

(1)相比于session,它无需保存在服务器,不占用服务器内存开销。
(2)无状态、可拓展性强:比如有3台机器(A、B、C)组成服务器集群,若session存在机器A上,session只能保存在其中一台服务器,此时你便不能访问机器B、C,因为B、C上没有存放该Session,而使用token就能够验证用户请求合法性,并且我再加几台机器也没事,所以可拓展性好就是这个意思。
(3)这样做可就支持了跨域访问。


token生成于服务端,存储在客户端,服务端不用存储,用户后面每次登录都携带首次都登录生成的token字符串用于验证,能做到这点,关键就是token使用的某种算法根据用户签名和其它一些信息生成的令牌信息是一致的,可以验证通过,对于用户量庞大的系统,或者分布式,避免了大量session对象的存储带来的内存消耗,和各服务器之间session的复制或者专门用于存储session的服务器宕机带来的问题

所谓的Token,其实就是服务端生成的一串加密字符串、以作客户端进行请求的一个“令牌”。当用户第一次使用账号密码成功进行登录后,服务器便生成一个Token及Token失效时间并将此返回给客户端,若成功登陆,以后客户端只需在有效时间内带上这个Token前来请求数据即可,无需再次带上用户名和密码。

为什么要使用Token?这个问题其实很好回答——因为它能解决问题!当下用户对产品的使用体验要求在逐渐提高,从产品体验方面来讲,Token带来的体验更容易能让用户接受。

那么Token都可以解决哪些问题呢?

  • Token具有随机性、不可预测性、时效性、无状态、跨域等特点
  • Token完全由应用管理,所以它可以避开同源策略
  • Token可以避免CSRF攻击
  • Token可以是无状态的,可以在多个服务间共享
  • Token是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回Token给前端。前端可以在每次请求的时候带上Token证明自己的合法地位。如果这个Token在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌

什么是同源策略?

所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,现在所有支持 JavaScript 的浏览器都会使用这个策略。如果缺少了同源策略,浏览器很容易受到 XSS、 CSFR 等攻击。同源策略是为了安全,确保一个应用中的资源只能被本应用的资源访问。否则,岂不是谁都能访问。

Token的生命周期

1)用户未登录

用户执行注册/登录→

一旦基础数据校验成功,后端生成Token,并且Token包含此次注册/登录用户的用户名并通过JsonResponse返回给前端→

前端拿到返回的Token后,存入浏览器本地存储

2)用户每次访问博客页面

从本地存储中拿出Token→

JS将Token 放入request的Authorization头,发送http请求向后端索要数据→

服务器接到前端请求(当前URL加了loging_check,并且请求方法在methods参数中),进行校验→从requestAuthorization头拿出Token→校验→校验不通过,返回前端异常代码/校验通过,正常执行对应的视图函数→前端一旦接到关于Token的异常码,则删除本地存储中的Token,且将用户转至登录界面。

如何设置Token的有效期?

其实Token作为一个概念模型,开发者完全可以针对自己开发的应用自定义Token,只要能做到不让不法分子钻系统漏洞即可。

那么为Token设置有效期还有必要吗?

对于这个问题,大家不妨先看两个例子:

Token的有效期多长合适呢?

一般来说,基于系统安全的需要当然需要尽可能的短,但也不能短得离谱:如果在用户正常操作的过程中,Token过期失效要求重新登录,用户体验岂不是很糟糕?

为了解决在操作过程不让用户感到Token失效的问题,有一种方案是在服务器端保存Token状态,用户每次操作都会自动刷新(推迟)Token的过期时间。

如此操作会存在一个问题,即在前后端分离、单页App等情况下,每秒可能发起多次请求,如果每次都去刷新过期时间会产生非常大的代价,同样地,如果Token的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率、减少消耗,会把Token的过期时保存在缓存或者内存中。

另一种方案是使用RefreshToken,它可以避免频繁的读写操作。这种方案中,服务端无需刷新Token的过期时间,一旦Token过期,就反馈给前端,前端使用RefreshToken申请一个全新Token继续使用。

这种方案中,服务端只需要在客户端请求更新Token的时候对RefreshToken的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然RefreshToken也是有有效期的,但是这个有效期就可以长一点了。

防范Cookie劫持

Cookie防劫持预防
基于XSS攻击, 窃取Cookie信息, 并冒充他人身份。

1) 方法一:给Cookie添加HttpOnly属性, 这种属性设置后, 只能在http请求中传递, 在脚本中, document.cookie无法获取到该Cookie值. 对XSS的攻击, 有一定的防御值. 但是对网络拦截, 还是泄露了.

2)方法二:在cookie中添加校验信息, 这个校验信息和当前用户外置环境有些关系,比如ip,user agent等有关. 这样当cookie被人劫持了, 并冒用, 但是在服务器端校验的时候, 发现校验值发生了变化, 因此要求重新登录, 这样也是种很好的思路, 去规避cookie劫持.

3)方法三:cookie中session id的定时更换, 让session id按一定频率变换, 同时对用户而言, 该操作是透明的, 这样保证了服务体验的一致性.

4)方法四:使用HTTPS来进行传输

cookie和session的区别是什么?

参考答案

  1. 存储位置不同:cookie存放于客户端;session存放于服务端。
  2. 存储容量不同:单个cookie保存的数据<=4KB,一个站点最多保存20个cookie;而session并没有上限。
  3. 存储方式不同:cookie只能保存ASCII字符串,并需要通过编码当时存储为Unicode字符或者二进制数据;session中能够存储任何类型的数据,例如字符串、整数、集合等。
  4. 隐私策略不同:cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的;session存储在服务器上,对客户端是透明的,不存在敏感信息泄露的风险。
  5. 生命周期不同:可以通过设置cookie的属性,达到cookie长期有效的效果;session依赖于名为JSESSIONID的cookie,而该cookie的默认过期时间为-1,只需关闭窗口该session就会失效,因此session不能长期有效。
  6. 服务器压力不同:cookie保存在客户端,不占用服务器资源;session保管在服务器上,每个用户都会产生一个session,如果并发量大的话,则会消耗大量的服务器内存。
  7. 浏览器支持不同:cookie是需要浏览器支持的,如果客户端禁用了cookie,则会话跟踪就会失效;运用session就需要使用URL重写的方式,所有用到session的URL都要进行重写,否则session会话跟踪也会失效。
  8. 跨域支持不同:cookie支持跨域访问,session不支持跨域访问。

cookie和session各自适合的场景是什么?

对于敏感数据,应存放在session里,因为cookie不安全。

对于普通数据,优先考虑存放在cookie里,这样会减少对服务器资源的占用。

请介绍session的工作原理

session依赖于cookie。

当客户端首次访问服务器时,服务器会为其创建一个session对象,该对象具有一个唯一标识SESSIONID。并且在响应阶段,服务器会创建一个cookie,并将SESSIONID存入其中。

客户端通过响应的cookie而持有SESSIONID,所以当它再次访问服务器时,会通过cookie携带这个SESSIONID。服务器获取到SESSIONID后,就可以找到与之对应的session对象,进而从这个session中获取该客户端的状态。

get请求与post请求有什么区别?

  • GET参数通过URL传递,POST放在Request body中。
  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生的URL地址可以被Bookmark,而POST不可以。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST没有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

get请求的参数能放到body里面吗?

GET请求是可以将参数放到BODY里面的,官方并没有明确禁止,但给出的建议是这样不符合规范,无法保证所有的实现都支持。这就意味着,如果你试图这样做,可能出现各种未知的问题,所以应该当避免。

如何自定义拦截器

一个拦截器,只有preHandle方法返回truepostHandleafterCompletion才有可能被执行;如果preHandle方法返回false,则该拦截器的postHandleafterCompletion必然不会被执行。拦截器不是Filter,却实现了Filter的功能,其原理在于:

  • 所有的拦截器(Interceptor)和处理器(Handler)都注册在HandlerMapping中。
  • Spring MVC中所有的请求都是由DispatcherServlet分发的。
  • 当请求进入DispatcherServlet.doDispatch()时候,首先会得到处理该请求的Handler(即Controller中对应的方法)以及所有拦截该请求的拦截器。拦截器就是在这里被调用开始工作的。

自定义一个拦截器非常简单,只需要实现HandlerInterceptor这个接口即可,该接口有三个可以实现的方法,如下:

  • preHandle()方法:改方法会在控制方法前执行,器返回值表示是否知道如何写一个接口。中断后续操作。当其返回值为true时,表示继续向下执行;当其返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等 )
  • postHandle()方法: 该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图作出进一步的修改。
  • afterCompletion()方法:该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。

MyBaits

MyBatis是什么?

img

什么是ORM?

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。

ORM框架:为了解决面型对象与关系数据库存在的互不匹配的现象的框架。

当前ORM框架主要有五种:
(1)Hibernate 全自动 需要写hql语句
(2)iBATIS 半自动 自己写sql语句,可操作性强,小巧
(3)mybatis
(4)eclipseLink
(5)JFinal

ORM的优缺点:

优点:
1)提高开发效率,降低开发成本
2)使开发更加对象化
3)可移植
4)可以很方便地引入数据缓存之类的附加功能
缺点:
1)自动化进行关系数据库的映射需要消耗系统性能。其实这里的性能消耗还好啦,一般来说都可以忽略之。
2)在处理多表联查、where条件复杂之类的查询时,ORM的语法会变得复杂。

JPA

JPA (Java Persistence API)Java持久化API。是一套Sun公司Java官方制定的ORM 方案,是规范,是标准 ,sun公司自己并没有实现

市场上的主流的JPA框架(实现者)有:

Hibernate (JBoos)、EclipseTop(Eclipse社区)、OpenJPA (Apache基金会)。

JPA是ORM的一套标准,既然JPA为ORM而生,那么JPA的作用就是实现使用对象操作数据库,不用写SQL!!!.

MyBatis和JPA的区别

ORM映射不同:

MyBatis是半自动的ORM框架,提供数据库与结果集的映射;

JPA(默认采用Hibernate实现)是全自动的ORM框架,提供对象与数据库的映射。

可移植性不同:

JPA通过它强大的映射结构和HQL语言,大大降低了对象与数据库的耦合性;

MyBatis由于需要写SQL,因此与数据库的耦合性直接取决于SQL的写法,如果SQL不具备通用性而用了很多数据库的特性SQL的话,移植性就会降低很多,移植时成本很高。

日志系统的完整性不同:

JPA日志系统非常健全、涉及广泛,包括:SQL记录、关系异常、优化警告、缓存提示、脏数据警告等;

MyBatis除了基本的记录功能外,日志功能薄弱很多。

SQL优化上的区别:

由于Mybatis的SQL都是写在XML里,因此优化SQL比Hibernate方便很多。

而Hibernate的SQL很多都是自动生成的,无法直接维护SQL。虽有HQL,但功能还是不及SQL强大,见到报表等复杂需求时HQL就无能为力,也就是说HQL是有局限的Hhibernate虽然也支持原生SQL,但开发模式上却与ORM不同,需要转换思维,因此使用上不是非常方便。总之写SQL的灵活度上Hibernate不及Mybatis。

sqlsession了解吗?

[Mybatis的SqlSession运行原理 - JJian - 博客园 (cnblogs.com)](https://www.cnblogs.com/jian0110/p/9452592.html#:~:text=SqlSession是Mybatis最重要的构建之一,可以简单的认为Mybatis一系列的配置目的是生成类似 JDBC生成的Connection对象的SqlSession对象,这样才能与数据库开启“沟通”,通过SqlSession可以实现增删改查(当然现在更加推荐是使用Mapper接口形式),那么它是如何执行实现的,这就是本篇博文所介绍的东西,其中会涉及到简单的源码讲解。.,了解SqlSession的运作原理是学习Mybatis插件的必经之路,因为Mybatis的插件会在SqlSession运行过程中“插入”运行,如果没有很好理解的话,Mybatis插件可能会覆盖相应的源码造成严重的问题。. 鉴于此,本篇博文尽量详细介绍SqlSession运作原理!.)

 SqlSession是Mybatis最重要的构建之一,可以简单的认为Mybatis一系列的配置目的是生成类似 JDBC生成的Connection对象的SqlSession对象,这样才能与数据库开启“沟通”,通过SqlSession可以实现增删改查

mybatis 一级缓存和二级缓存?

在这里插入图片描述

一级缓存:Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。
每一次会话都对应自己的一级缓存,作用范围比较小,一旦会话关闭就查询不到了

二级缓存:二级缓存存在于SqlSessionFactory 的生命周期中,即它是SqlSessionFactory级别的缓存。它可以提高对数据库查询的效率,以提高应用的性能。他是基于namespace名称空间级别的缓存:一个namespace对应一个二级缓存即一个mapper.xml对应一个缓存:

二级缓存具有如下效果:

  • 映射语句文件中的所有SELECT 语句将会被缓存。
  • 映射语句文件中的所有时INSERT 、UPDATE 、DELETE 语句会刷新缓存。
  • 缓存会使用Least Recently Used ( LRU ,最近最少使用的)算法来收回。
  • 根据时间表(如no Flush Interval ,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024 个引用。
  • 缓存会被视为read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

Mybatis的一级缓存和二级缓存执行顺序

1、先判断二级缓存是否开启,如果没开启,再判断一级缓存是否开启,如果没开启,直接查数据库

2、如果一级缓存关闭,即使二级缓存开启也没有数据,因为二级缓存的数据从一级缓存获取

3、一般不会关闭一级缓存

4、二级缓存默认不开启

5、如果二级缓存关闭,直接判断一级缓存是否有数据,如果没有就查数据库

6、如果二级缓存开启,先判断二级缓存有没有数据,如果有就直接返回;如果没有,就查询一级缓存,如果有就返回,没有就查询数据库

综上:先查二级缓存,再查一级缓存,再查数据库;即使在一个sqlSession中,也会先查二级缓存;一个namespace中的查询更是如此;缓存执行顺序是:二级缓存–>一级缓存–>数据库

Mybatis的工作原理

Mybatis运行原理(我保证全宇宙最详细) - 知乎 (zhihu.com)

(1)读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。

(2)加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。

(3)构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。

(4)创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

(5)Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

(6)MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

(7)输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

(8)输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

MyBatis框架的执行流程图

我们把Mybatis的功能架构分为三层:

  • API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
  • 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
  • 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

preload

Mybatis中#{}与${}的区别

#{} 是 sql 的参数占位符,MyBatis 会将 sql 中的#{}替换为? 号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的? 号占位符设置参数值,将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号

${} 是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,在SQL替换中将传入的数据直接显示生成在sql中

#方式能够很大程度防止sql注入,$方式无法防止Sql注入。

只能使用${}的场景

由于#{}会给参数内容自动加上引号,会在有些需要表示字段名、表名的场景下,SQL将无法正常执行。现举一例说明:

期望查询结果按sex字段升序排列,参数String orderCol = “sex”,mapper映射文件使用 #{}:

1
2
3
<select id="findAddByName3" parameterType="String" resultMap="studentResultMap">
SELECT * FROM USER WHERE username LIKE '%Am%' ORDER BY #{value} ASC
</select>

则SQL解析及执行结果如下所示,很明显 ORDER 子句的字段名错误的被加上了引号,致使查询结果没有按期排序输出

1
SELECT * FROM USER WHERE username LIKE '%Am%' ORDER BY 'sex' ASC;

在mapper中如何传递多个参数

preload

方法3:Map传参法

preload

不同的Xml映射文件,id是否可以重复?

  • 不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。
  • 原因就是namespace+id是作为Map<String, MappedStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。

MyBatis编程步骤

  • 1、 创建SqlSessionFactory
  • 2、 通过SqlSessionFactory创建SqlSession
  • 3、 通过sqlsession执行数据库操作
  • 4、 调用session.commit()提交事务
  • 5、 调用session.close()关闭会话

为什么需要预编译

preload

延迟加载?

在真正的使用数据时才发起查询,不用的时候不查。按需加载(懒加载)。

Mybatis延迟加载的实现原理是什么

在真正的使用数据时才发起查询,不用的时候不查。按需加载(懒加载)

MyBatis中的ResultMap关系映射中有两个标签:associationcollection,前者适合一对一查询的关系映射,后者适合一对多查询的关系映射。

  • Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
  • 它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
  • 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。

Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

  • 第一种是使用 标签,逐一定义列名和对象属性名之间的映射关系。
  • 第二种是使用sql列的别名功能,将列别名书写为对象属性名,比如T_NAME AS NAME,对象属性名一般是name,小写,但是列名不区分大小写,Mybatis会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成T_NAME AS NaMe,Mybatis一样可以正常工作。

Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理吗?

  • Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签
  • trim|where|set|foreach|if|choose|when|otherwise|bind。
  • 其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

Xml 映射文件中,除了常见的 select|insert|update|delete 标签之外,还有哪些标签?

还有很多其他的标签, <resultMap><parameterMap><sql><include><selectKey> ,加上动态 sql 的 9 个标签, trim|where|set|foreach|if|choose|when|otherwise|bind 等,其中 <sql> 为 sql 片段标签,通过 <include> 标签引入 sql 片段, <selectKey> 为不支持自增的主键生成策略标签

Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

Dao 接口的工作原理是 JDK 动态代理,MyBatis 运行时会使用 JDK 动态代理为 Dao 接口生成代理 proxy 对象,代理对象 proxy 会拦截接口方法,转而执行 MappedStatement 所代表的 sql,然后将 sql 执行结果返回

Dao 接口里的方法可以重载,但是 Mybatis 的 XML 里面的 ID 不允许重复。

  1. 仅有一个无参方法和一个有参方法
  2. 多个有参方法时,参数数量必须一致。且使用相同的 @Param ,或者使用 param1

MyBatis 是如何进行分页的?分页插件的原理是什么?

首先是将所有结果查询出来,然后通过计算offset和limit,只返回部分结果,操作在内存中进行,所以也叫内存分页。

分页插件的基本原理是使用 MyBatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

简述 MyBatis 的插件运行原理,以及如何编写一个插件。

MyBatis 仅可以编写针对 ParameterHandlerResultSetHandlerStatementHandlerExecutor 这 4 种接口的插件,MyBatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandlerinvoke() 方法,当然,只会拦截那些你指定需要拦截的方法。

实现 MyBatis 的 Interceptor 接口并复写 intercept() 方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件

MyBatis 都有哪些 Executor 执行器?它们之间的区别是什么?

答:MyBatis 有三种基本的 Executor 执行器,SimpleExecutorReuseExecutorBatchExecutor

  • SimpleExecutor :每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。

  • ReuseExecutor :执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement>内,供下一次使用。简言之,就是重复使用 Statement 对象。

  • BatchExecutor :执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同

MyBatis 动态 sql 是做什么的?都有哪些动态 sql?能简述一下动态 sql 的执行原理不?

答:MyBatis 动态 sql 可以让我们在 Xml 映射文件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,MyBatis 提供了 9 种动态 sql 标签 trim|where|set|foreach|if|choose|when|otherwise|bind

其执行原理为,使用 OGNL 从 sql 参数对象中计算表达式的值,根据表达式的值动态拼接 sql,以此来完成动态 sql 的功能。

Mybatis是如何进行分页的?分页插件的原理是什么?

  • Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
  • 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
  • 举例:select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10

Restful API