Spring中用到了哪些设计模式?
单例模式
Spring中的Bean默认情况下都是单例的。bean被声明为单例的时候,在处理多次请求的时候在Spring容器里只实例化出一个bean,后续的请求都公用这个对象,这个对象会保存在一个map里面。当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象,所以这是个单例的。但是对于原型(prototype)bean来说当每次请求来的时候直接实例化新的bean,没有缓存以及从缓存查的过程。
- 所以单例的bean只有第一次创建新的bean后面就会复用bean,所以不会频繁创建对象。
- 原型的bean每次都会新创建
单例bean的优势:
- 减少了新生成实例的消耗 新生成实例消耗包括两方面,首先,Spring会通过反射或者cglib来生成bean实例这都是耗性能的操作,其次给对象分配内存也会涉及复杂算法
- 减少jvm垃圾回收 由于不会给每个请求都新生成bean实例,所以自然回收的对象少了
- 可以快速获取到bean 因为单例的获取bean操作除了第一次生成之外其余的都是从缓存里获取的所以很快
单例bean的劣势:
单例的bean一个很大的劣势就是他不能做到线程安全!!!,由于所有请求都共享一个bean实例,所以这个bean要是有状态的一个bean的话可能在并发场景下出现问题,有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。
总的来说Spring把bean默认设计成单例,就是为了提高性能!
工厂模式
代理模式
模板方法模式
这篇博文讲的不错,贴个链接:
Spring的IOC
简单来说,IOC 叫做控制反转,指的是通过Spring来管理对象的创建、配置和生命周期,这样相当于把控制权交给了Spring,不需要⼈⼯来管理对象之间复杂的依赖关系,这样做的好处就是解耦。在Spring里面,主要提供了 BeanFactory 和 ApplicationContext 两种 IOC 容器,通过他们来实现对 Bean 的管理。
Spring的AOP
简单的来说,AOP 叫做面向切面编程,他是⼀个编程范式,⽬的就是提高代码的模块性。Srping AOP 基于动态代理的方式实现,如果是实现了接⼝的话就会使用 JDK 动态代理,反之则使用 CGLIB 代理,Spring中 AOP的应用主要体现在 事务、日志、异常处理等方面,通过在代码的前后做⼀些增强处理,可以实现对业务逻辑的隔离,提高代码的模块化能力,同时也是解耦。Spring主要提供了 Aspect 切面、JoinPoint 连接点、PointCut 切入点、Advice 增强等实现方式。
AOP的使用,我在之前的博客也写到,这里贴个链接:
JDK动态代理和CGLIB代理有什么区别?
JDK 动态代理主要是针对类实现了某个接口,AOP 则会使用 JDK 动态代理。他基于反射的机制实现,生成⼀个实现同样接口的⼀个代理类,然后通过重写方法的方式,实现对代码的增强。
而如果某个类没有实现接口,AOP 则会使⽤ CGLIB 代理。他的底层原理是基于 asm 第三方框架,通过修改字节码生成⼀个子类,然后重写父类的方法,实现对代码的增强。
Spring的AOP和Aspectj AOP的区别
Spring AOP 基于动态代理实现,属于运行时增强。
AspectJ 则属于编译时增强,主要有3种方式:
- 编译时织⼊:指的是增强的代码和源代码我们都有,直接使⽤ AspectJ 编译器编译就行了,编译之后⽣成⼀个新的类,他也会作为⼀个正常的 Java 类装载到JVM。
编译后织⼊:指的是代码已经被编译成 class ⽂件或者已经打成 jar 包,这时候要增强的话,就是
编译后织入,比如你依赖了第三方的类库,⼜想对他增强的话,就可以通过这种方式。
1 | <configuration> |
- 加载时织入:指的是在 JVM 加载类的时候进⾏织⼊。
总结下来的话,就是 Spring AOP 只能在运行时织⼊,不需要单独编译,性能相比 AspectJ 编译织⼊的⽅式慢,而AspectJ 只⽀持编译前后和类加载时织⼊,性能更好,功能更加强大。
FactoryBean和BeanFactory有什么区别?
BeanFactory是Bean的工厂,ApplicationContext的父类,IOC容器的核心,负责生产和管理Bean对象。
FactoryBean是Bean,可以通过实现Factory接口定制实例化Bean的逻辑,通过代理一个Bean对象,对方法前后做一些操作。
SpringBean的生命周期
SpringBean 生命周期简单概括为4个阶段:
实例化,创建一个Bean对象
填充属性,为属性赋值
初始化
- 如果实现类xxxAware接口,通过不同类型的Aware接口拿到Spring容器的资源
- 如果实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialization和postProcessAfterInitialization方法
- 如果配置了init-method方法,则会执行init-method配置的方法
销毁
容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destory方法
如果配置了destory-method方法,则会执行destory-method配置的方法
Spirng是怎么解决循环依赖的
首先,Spring解决循环依赖有两个前提条件:
- 不全是构造器方式的循环依赖
- 必须是单例
基于上面的问题,我们知道Bean的生命周期,本质上解决循环依赖问题的就是三级缓存,通过三级缓存提前拿到未初始化的对象。
第一级缓存:用来保存实例化、初始化都完成的对象
第二级缓存:用来保存实例化完成,但未初始化完成的对象
第三级缓存:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象
假设一个简单的循环依赖场景,A、B互相依赖。
A对象的创建过程:
- 创建对象A,实例化的时候把A对象工厂放入三级缓存
A注入属性时,发现依赖B,转而去实例化B
同样创建对象B,注入属性时发现依赖A,一次从一级到三级缓存查询A,从三级缓存通过对象工厂拿到A,把A放入二级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入一级缓存。
接着继续创建A,顺利从一级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除二级缓存中的A,同时把A放入一级缓存
最后,一级缓存中保存着实例化、初始化都完成的A、B对象
因此,由于把实例化和初始化的流程分开了,所以如果都是用构造器的化,就没法分离这个操作,所以都是构造器的话就无法解决循环依赖 的问题了。
下面对上面的做一些补充说明:
创建对象A,实例化的时候把A对象工厂放入三级缓存,这里为啥直接放入第三级缓存,因为A既没有实例化完成,也没用初始化完成,只能放入三级缓存(一二级都要实例化完成),下面是源码:
为什么第三级缓存要使用ObjectFactory?
如果仅仅是解决循环依赖问题,使用二级缓存就可以了,但是如果对象实现了AOP,那么注入到其他bean的时候,并不是最终的代理对象,而是原始的。这时就需要通过三级缓存的ObjectFactory才能提前产生最终的需要代理的对象。
什么时候将Bean的引用提前暴露给第三级缓存的ObjectFactory持有?时机就是在第一步实例化之后,第二步依赖注入之前,完成此操作。
循环依赖问题在Spring中主要有三种情况:
- 通过构造方法进行依赖注入时产生的循环依赖问题。
- 通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
- 通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。
在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。其实也很好解释:
第(1)种构造方法注入的情况下,在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。
第(2)种setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。
参考文章:
为什么要三级缓存?二级不行吗?
不可以,主要是为了生成代理对象。
因为三级缓存中放的是生成具体对象的匿名内部类,他可以⽣成代理对象,也可以是普通的实例对象。
使⽤三级缓存主要是为了保证不管什么时候使用的都是⼀个对象。
假设只有⼆级缓存的情况,往二级缓存中放的显示⼀个普通的Bean对象, BeanPostProcessor 去⽣成
代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不⼀致了。
Spring事务传播机制有哪些
- PROPAGATION_REQUIRED:如果当前没有事务,就创建⼀个新事务,如果当前存在事务,就加
⼊该事务,这也是通常我们的默认选择。
- PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按
REQUIRED属性执行。
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务
挂起。
- PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不
存在事务,就抛出异常。
- PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存
在事务,就以非事务执行。
SpringBoot启动流程
- 准备环境,根据不同的环境创建不同的Environment
- 准备、加载上下文,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean
- 初始化,这个阶段刷新Spring Context,启动应用
- 最后结束流程
贴一篇讲解Spring启动相关注解的博文:
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 | /** |
启动测试,调用第一个接口,发现普通变量的值会随着请求次数的增加而+1,所以是线程不安全的
但我们加上@scope注解时,让它不走默认的singleton,而是走原型,启动程序,发现普通变量的值不会会随着请求次数的增加而+1,现在看来没有出现线程不安全的情况,那我们继续测试,假如static变量,我们知道类加载是懒加载机制,在准别阶段就会对static变量进行初始化,后面就不管了,我们启动程序发现,static变量会随着请求次数的增加而+1,所以加了@scope注解并不一定是线程安全的!
这里就引入了TheadLocal,为每一个线程创建一个副本,实现线程的上下文传递同一个对象,隔离了多线程之间数据共享的问题,所以启动程序会发现t1变量是线程安全的,但static和User对象都是线程不安全的。
TestController 是每次请求的时候都初始化了一个对象,但是静态变量始终是只有一份的,而且这个注入的user对象也是只有一份的。静态变量只有一份这是当然的咯,那么有没有办法让user对象可以每次都new一个新的呢?当然可以:
1 | /** |
我们在注入IOC容器的时候,加入@scope,把它设置成原型即可,测试程序发现User的hashcode是不一样的,每次赋值前取user中的变量值也都是默认值0。
总结下:
- 在@Controller/@Service等容器中,默认情况下,scope是singleton的,是线程不安全的
- 尽量不要在@Controller/@Service等容器中定义静态变量,不论是单例还是多实例,因为都不能保证线程安全
- 默认注入的Bean对象,在不设置scope的时候也是线程不安全的
- 一定要定义变量的话,用TheadLocal进行封装,来保证线程安全