Spring中用到了哪些设计模式?

单例模式

Spring中的Bean默认情况下都是单例的。bean被声明为单例的时候,在处理多次请求的时候在Spring容器里只实例化出一个bean,后续的请求都公用这个对象,这个对象会保存在一个map里面。当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象,所以这是个单例的。但是对于原型(prototype)bean来说当每次请求来的时候直接实例化新的bean,没有缓存以及从缓存查的过程。

  • 所以单例的bean只有第一次创建新的bean后面就会复用bean,所以不会频繁创建对象。
  • 原型的bean每次都会新创建

单例bean的优势:

  1. 减少了新生成实例的消耗 新生成实例消耗包括两方面,首先,Spring会通过反射或者cglib来生成bean实例这都是耗性能的操作,其次给对象分配内存也会涉及复杂算法
  2. 减少jvm垃圾回收 由于不会给每个请求都新生成bean实例,所以自然回收的对象少了
  3. 可以快速获取到bean 因为单例的获取bean操作除了第一次生成之外其余的都是从缓存里获取的所以很快

单例bean的劣势:

单例的bean一个很大的劣势就是他不能做到线程安全!!!,由于所有请求都共享一个bean实例,所以这个bean要是有状态的一个bean的话可能在并发场景下出现问题,有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。

总的来说Spring把bean默认设计成单例,就是为了提高性能!

工厂模式

代理模式

模板方法模式

这篇博文讲的不错,贴个链接:

Spring中用到了哪些设计模式?

Spring的IOC

Spring的IOC 实现原理

简单来说,IOC 叫做控制反转,指的是通过Spring来管理对象的创建、配置和生命周期,这样相当于把控制权交给了Spring,不需要⼈⼯来管理对象之间复杂的依赖关系,这样做的好处就是解耦。在Spring里面,主要提供了 BeanFactory 和 ApplicationContext 两种 IOC 容器,通过他们来实现对 Bean 的管理。

Spring的AOP

Spring AOP 实现原理

简单的来说,AOP 叫做面向切面编程,他是⼀个编程范式,⽬的就是提高代码的模块性。Srping AOP 基于动态代理的方式实现,如果是实现了接⼝的话就会使用 JDK 动态代理,反之则使用 CGLIB 代理,Spring中 AOP的应用主要体现在 事务、日志、异常处理等方面,通过在代码的前后做⼀些增强处理,可以实现对业务逻辑的隔离,提高代码的模块化能力,同时也是解耦。Spring主要提供了 Aspect 切面、JoinPoint 连接点、PointCut 切入点、Advice 增强等实现方式。

AOP的使用,我在之前的博客也写到,这里贴个链接:

SpringBoot之AOP的使用

JDK动态代理和CGLIB代理有什么区别?

JDK 动态代理主要是针对类实现了某个接口,AOP 则会使用 JDK 动态代理。他基于反射的机制实现,生成⼀个实现同样接口的⼀个代理类,然后通过重写方法的方式,实现对代码的增强。

而如果某个类没有实现接口,AOP 则会使⽤ CGLIB 代理。他的底层原理是基于 asm 第三方框架,通过修改字节码生成⼀个子类,然后重写父类的方法,实现对代码的增强。

Spring的AOP和Aspectj AOP的区别

Spring AOP 基于动态代理实现,属于运行时增强。

AspectJ 则属于编译时增强,主要有3种方式:

  1. 编译时织⼊:指的是增强的代码和源代码我们都有,直接使⽤ AspectJ 编译器编译就行了,编译之后⽣成⼀个新的类,他也会作为⼀个正常的 Java 类装载到JVM。
  1. 编译后织⼊:指的是代码已经被编译成 class ⽂件或者已经打成 jar 包,这时候要增强的话,就是

    编译后织入,比如你依赖了第三方的类库,⼜想对他增强的话,就可以通过这种方式。

1
2
3
4
5
6
7
8
9
10
<configuration>
<weaveDependencies>
<weaveDependency>

</weaveDependency>
<weaveDependency>

</weaveDependency>
</weaveDependencies>
</configuration>
  1. 加载时织入:指的是在 JVM 加载类的时候进⾏织⼊。

总结下来的话,就是 Spring AOP 只能在运行时织⼊,不需要单独编译,性能相比 AspectJ 编译织⼊的⽅式慢,而AspectJ 只⽀持编译前后和类加载时织⼊,性能更好,功能更加强大。

FactoryBean和BeanFactory有什么区别?

BeanFactory是Bean的工厂,ApplicationContext的父类,IOC容器的核心,负责生产和管理Bean对象。

FactoryBean是Bean,可以通过实现Factory接口定制实例化Bean的逻辑,通过代理一个Bean对象,对方法前后做一些操作。

SpringBean的生命周期

SpringBean 生命周期简单概括为4个阶段:

  1. 实例化,创建一个Bean对象

  2. 填充属性,为属性赋值

  3. 初始化

    • 如果实现类xxxAware接口,通过不同类型的Aware接口拿到Spring容器的资源
    • 如果实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialization和postProcessAfterInitialization方法
    • 如果配置了init-method方法,则会执行init-method配置的方法
  4. 销毁

    • 容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destory方法

    • 如果配置了destory-method方法,则会执行destory-method配置的方法

请别再问Spring Bean的生命周期了!

Spirng是怎么解决循环依赖的

首先,Spring解决循环依赖有两个前提条件:

  1. 不全是构造器方式的循环依赖
  2. 必须是单例

基于上面的问题,我们知道Bean的生命周期,本质上解决循环依赖问题的就是三级缓存,通过三级缓存提前拿到未初始化的对象。

第一级缓存:用来保存实例化、初始化都完成的对象

第二级缓存:用来保存实例化完成,但未初始化完成的对象

第三级缓存:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象

假设一个简单的循环依赖场景,A、B互相依赖。

A对象的创建过程:

  1. 创建对象A,实例化的时候把A对象工厂放入三级缓存

  1. A注入属性时,发现依赖B,转而去实例化B

  2. 同样创建对象B,注入属性时发现依赖A,一次从一级到三级缓存查询A,从三级缓存通过对象工厂拿到A,把A放入二级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入一级缓存。

  3. 接着继续创建A,顺利从一级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除二级缓存中的A,同时把A放入一级缓存

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

因此,由于把实例化和初始化的流程分开了,所以如果都是用构造器的化,就没法分离这个操作,所以都是构造器的话就无法解决循环依赖 的问题了。

下面对上面的做一些补充说明:

  1. 创建对象A,实例化的时候把A对象工厂放入三级缓存,这里为啥直接放入第三级缓存,因为A既没有实例化完成,也没用初始化完成,只能放入三级缓存(一二级都要实例化完成),下面是源码:

  1. 为什么第三级缓存要使用ObjectFactory?

    如果仅仅是解决循环依赖问题,使用二级缓存就可以了,但是如果对象实现了AOP,那么注入到其他bean的时候,并不是最终的代理对象,而是原始的。这时就需要通过三级缓存的ObjectFactory才能提前产生最终的需要代理的对象。

    1. 什么时候将Bean的引用提前暴露给第三级缓存的ObjectFactory持有?时机就是在第一步实例化之后,第二步依赖注入之前,完成此操作。

    2. 循环依赖问题在Spring中主要有三种情况:

      • 通过构造方法进行依赖注入时产生的循环依赖问题。
      • 通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
      • 通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

      在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。其实也很好解释:

      第(1)种构造方法注入的情况下,在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。

      第(2)种setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。

参考文章:

Spring如何解决循环依赖问题

为什么要三级缓存?二级不行吗?

不可以,主要是为了生成代理对象。

因为三级缓存中放的是生成具体对象的匿名内部类,他可以⽣成代理对象,也可以是普通的实例对象。

使⽤三级缓存主要是为了保证不管什么时候使用的都是⼀个对象。

假设只有⼆级缓存的情况,往二级缓存中放的显示⼀个普通的Bean对象, BeanPostProcessor 去⽣成

代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不⼀致了。

Spring事务传播机制有哪些

  1. PROPAGATION_REQUIRED:如果当前没有事务,就创建⼀个新事务,如果当前存在事务,就加

⼊该事务,这也是通常我们的默认选择。

  1. PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
  2. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按

REQUIRED属性执行。

  1. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务

挂起。

  1. PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  2. PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不

存在事务,就抛出异常。

  1. PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存

在事务,就以非事务执行。

SpringBoot启动流程

  1. 准备环境,根据不同的环境创建不同的Environment
  2. 准备、加载上下文,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean
  3. 初始化,这个阶段刷新Spring Context,启动应用
  4. 最后结束流程

贴一篇讲解Spring启动相关注解的博文:

SpringBoot启动流程及其原理

Spring中的Controller、Service、Dao是不是线程安全的?

对于Bean之前开篇也已经讲到:

原型Bean

对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。

单例Bean

对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

spring单例,为什么controller、service和dao确能保证线程安全?

Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。

实际上大部分时间Bean是无状态的(比如Dao) 所以说在某种程度上来说Bean其实是安全的。

但是如果Bean是有状态的 那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域 把 “singleton”改为’‘protopyte’ 这样每次请求Bean就相当于是 new Bean() 这样就可以保证线程的安全了。

  • 有状态就是有数据存储功能
  • 无状态就是不会保存数据    controller、service和dao层本身并不是线程安全的,只是如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。

对于任何无状态单例都是线程安全的

@Controller @Service是不是线程安全的?

默认配置下不是的,因为默认情况下@Controller没有加上@Scope,没有加@Scope就是默认值singleton,单例的。意思就是系统只会初始化一次Controller容器,所以每次请求的都是同一个Controller容器,当然是非线程安全的。

这里写了个测试代码:

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
/**
* Contrller 默认单例分析测试
* @author 路飞
* @create 2021/3/26 14:54
*/
@RestController
@Scope(value = "singleton") //加上@Scope注解,他有2个取值:单例-singleton 多实例-prototype
public class TestController {

private int var = 0;
private static int staticVar = 0;

ThreadLocal<Long> t1 = new ThreadLocal<Long>();

@Autowired
private User user;

@Value("${test.val}")
private int testVal;

@GetMapping("/testVal01")
public String test01(){
//var随着请求次数,进行+1
System.out.println("普通变量var:" + (++var));
return "普通变量var:"+var;
}

@GetMapping("/testVal02")
public String test02(){
//发现每次请求都是线程安全的
System.out.println("普通变量var:" + (++var));
return "普通变量var:"+var;
}

@GetMapping("/testVal03")
public String test03(){
//static的staticVar还是会随着请求而+1,线程不安全
System.out.println("普通变量var:" + (++var)+ "---静态变量staticVar:" + (++staticVar));
return "普通变量var:" + var + "静态变量staticVar:" + staticVar;
}

@GetMapping("/testVal04")
public String test04(){
t1.set(1L);
System.out.println("先取一下user对象中的值:"+user.getAge()+"---->"+user.hashCode());
user.setAge(1);
System.out.println("普通变量var:" + (++var) + "===静态变量staticVar:" + (++staticVar) + "===配置变量testInt:" + (++testVal)
+ "===ThreadLocal变量tl:" + t1.get()+"===注入变量user:" + user.getAge());
return "普通变量var:" + var + ",静态变量staticVar:" + staticVar + ",配置读取变量testInt:" + testVal + ",ThreadLocal变量tl:"
+ t1.get() + "注入变量user:" + user.getAge();
}
}

启动测试,调用第一个接口,发现普通变量的值会随着请求次数的增加而+1,所以是线程不安全的

但我们加上@scope注解时,让它不走默认的singleton,而是走原型,启动程序,发现普通变量的值不会会随着请求次数的增加而+1,现在看来没有出现线程不安全的情况,那我们继续测试,假如static变量,我们知道类加载是懒加载机制,在准别阶段就会对static变量进行初始化,后面就不管了,我们启动程序发现,static变量会随着请求次数的增加而+1,所以加了@scope注解并不一定是线程安全的!

这里就引入了TheadLocal,为每一个线程创建一个副本,实现线程的上下文传递同一个对象,隔离了多线程之间数据共享的问题,所以启动程序会发现t1变量是线程安全的,但static和User对象都是线程不安全的。

TestController 是每次请求的时候都初始化了一个对象,但是静态变量始终是只有一份的,而且这个注入的user对象也是只有一份的。静态变量只有一份这是当然的咯,那么有没有办法让user对象可以每次都new一个新的呢?当然可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author 路飞
* @create 2021/3/26 15:05
*/
@Configuration
public class MyConfig {

@Bean
@Scope(value = "prototype")
public User user(){
return new User();
}
}

我们在注入IOC容器的时候,加入@scope,把它设置成原型即可,测试程序发现User的hashcode是不一样的,每次赋值前取user中的变量值也都是默认值0。

总结下:

  1. 在@Controller/@Service等容器中,默认情况下,scope是singleton的,是线程不安全的
  2. 尽量不要在@Controller/@Service等容器中定义静态变量,不论是单例还是多实例,因为都不能保证线程安全
  3. 默认注入的Bean对象,在不设置scope的时候也是线程不安全的
  4. 一定要定义变量的话,用TheadLocal进行封装,来保证线程安全