1. 项目中为什么使用Spring框架?
这么问的话,就直接说Spring框架的好处就可以了。比如说Spring有以下特点:
- **轻量:**Spring 是轻量的,基本的版本大约2MB。
- **控制反转:**Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
- 面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
- **容器:**Spring 包含并管理应用中对象的生命周期和配置。
- MVC:框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
- **事务管理:**Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)
- **异常处理:**Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。
2. Autowired和Resource关键字的区别?
- @Autowired是Spring定义的注解,而@Resource是JSR-250定义的注解。所以@Autowired只能在Spring框架下使用,而@Resource则可以与其他框架一起使用。
- @Autowired默认按byType自动装配,而@Resource默认byName自动装配。
- @Autowired默认先按byType进行匹配,如果发现找到多个bean,则又按照byName方式进行匹配,如果还有多个,则报出异常。,只是指定了@Resource注解的name,则按name后的名字去bean元素里查找有与之相等的name属性的bean。
3. 依赖注入的方式有几种,各是什么
1.属性注入
通过属性注入,比如用@Autowired、@Resource这些注解
@Autowire
private ExampleService exampleServiceImpl;
- 对象的外部可见性:也就是脱离了Spring容器那么这个对象就不会被注入进去。
- 循环依赖:字段注入不会被检测是否出现依赖循环。比如A类中注入B类,B类中又注入了A类。
- 无法设置注入对象为final:因为final的成员变量必须在实例化时同时赋值。
2.setter方法注入
通过set方法注入,很少使用
private ExampleService exampleServiceImpl;
@Autowire
public setExampleServiceImpl (ExampleService exampleServiceImpl){
this.exampleServiceImpl = exampleServiceImpl;
}
- 利用set来注入保证不依赖Spring容器。
- 每个set单独注入某个对象,便于控制,并且可以实现选择性注入。
- 可以检测循环依赖。
3.构造器注入
通过构造方法注入依赖
- 当需要注入的对象很多时,构造器参数列表将会很长; 不够灵活。
- 对象初始化完成后便可获得可使用的对象。
- 可以检测循环依赖。
4. 什么是Spring
Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是
用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。常见的配置方式有三种:基于
XML的配置、基于注解的配置、基于Java的配置
有这些模块:
Spring Core:核心类库,提供IOC服务;
Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等);
Spring AOP:AOP服务;
Spring DAO:对JDBC的抽象,简化了数据访问异常的处理;
Spring ORM:对现有的ORM框架的支持;
Spring Web:提供了基本的面向Web的综合特性,例如多方文件上传;
Spring MVC:提供面向Web应用的Model-View-Controller实现。
5. Spring MVC运行流程
1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器
拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
6. SpringMVC常用的注解有哪些
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中
的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
7. 对Spring的AOP理解
AOP(Aspect-Oriented Programming,面向切面编程)能够将那些与业务无关,却为业务模块所
共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复
代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK
动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转而使用CGlib
动态代理生成一个被代理对象的子类来作为代理。
8. Spring中bean的生命周期
-
Spring中的bean的生命周期主要包含四个阶段:实例化Bean --> Bean属性填充 --> 初始化Bean -->销毁Bean
-
首先是实例化Bean,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚末初始化的依赖时,容器就会调用doCreateBean()方法根据mate-inf中默认的配置文件进行反射实例化,实际上就是通过反射的方式创建出一个bean对象
-
Bean实例创建出来后,接着就是给这个Bean对象进行属性填充,也就是注入这个Bean依赖的其它bean对象
-
属性填充完成后,进行初始化Bean操作,初始化阶段又可以分为几个步骤:
- 执行Aware接口的方法
Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的些资源。如实现BeanNameAware接口可以获取到BeanName,实现BeanFactoryAware接口可以获取到工厂对象BeanFactory等
- 执行BeanPostProcessor的前置处理方法postProcessBeforelnitialization(),对Bean进行一些自定义的前置处理
- 判断Bean是否实现了InitializingBean接口,如果实现了,将会执行lnitializingBean的afeterPropertiesSet()初始化方法;
- 执行用户自定义的初始化方法,如init-method等;
- 执行BeanPostProcessor的后置处理方法postProcessAfterinitialization()
-
初始化完成后,Bean就成功创建了,之后就可以使用这个Bean, 当Bean不再需要时,会进行销毁操作,
- 首先判断Bean是否实现了DestructionAwareBeanPostProcessor接口,如果实现了,则会执行DestructionAwareBeanPostProcessor后置处理器的销毁回调方法
- 其次会判断Bean是否实现了DisposableBean接口,如果实现了将会调用其实现的destroy()方法
- 最后判断这个Bean是否配置了dlestroy-method等自定义的销毁方法,如果有的话,则会自动调用其配置的销毁方法
9. Spring 是怎么解决循环依赖的?
Spring 中使用了三级缓存的设计,来解决单例模式下的属性循环依赖问题。
这句话有两点需要注意
- 解决问题的方法是「三级缓存的设计」
- 解决的只是单例模式下的 Bean 属性循环依赖问题,对于多例 Bean 和 Prototype 作用域的 Bean的循环依赖问题,并不能使用三级缓存设计解决。
9.1 三级缓存
Spring 中,单例 Bean 在创建后会被放入 IoC 容器的缓存池中,并触发 Spring 对该 Bean 的生命周期管理。
单例模式下,在第一次使用 Bean 时,会创建一个 Bean 对象,并放入 IoC 容器的缓存池中。后续再使用该 Bean 对象时,会直接从缓存池中获取。
保存单例模式 Bean 的缓存池,采用了三级缓存设计,如下代码所示。
/** Cache of singleton objects: bean name --> bean instance */
/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String,Object>(256);
/** Cache of early singleton objects: bean name --> bean instance */
/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
/** Cache of singleton factories: bean name --> ObjectFactory */
/** 三级级缓存:存放 bean 工厂对象,用于解决循环依赖 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
缓存层级 | 名称 | 描述 |
---|---|---|
第一层缓存 | singletonObjects | 单例对象缓存池,存放的 Bean 已经实例化、属性赋值、完全初始化好(成品) |
第二层缓存 | earlySingletonObjects | 早期单例对象缓存池,存放的 Bean 已经实例化但尚未属性赋值、未执行 init 方法(半成品) |
第三层缓存 | singletonFactories | 单例工厂的缓存 |
9.2 使用三级缓存解决循环依赖
分析 getSingleton()
的整个过程,可知三级缓存的使用过程如下
- Spring 会先从一级缓存
singletonObjects
中尝试获取 Bean。 - 若是获取不到,而且对象正在建立中,就会尝试从二级缓存
earlySingletonObjects
中获取 Bean。 - 若还是获取不到,且允许从三级缓存
singletonFactories
中经过singletonFactory
的getObject()
方法获取 Bean 对象,就会尝试从三级缓存singletonFactories
中获取 Bean。 - 若是在三级缓存中获取到了 Bean,会将该 Bean 存放到二级缓存中。
9.3 第三级缓存为什么可以解决循环依赖
Spring 解决循环依赖的诀窍就在于 singletonFactories
这个三级缓存。 三级缓存中使用到了ObjectFactory
接口
此处就是解决循环依赖的关键,这段代码发生在 createBeanInstance
以后
- 此时,单例 Bean 对象已经实例化(可以通过对象引用定位到堆中的对象),但尚未属性赋值和初始化。
- Spring 会将该状态下的 Bean 存放到三级缓存中,提早曝光给 IoC 容器(“提早”指的是不必等对象完成属性赋值和初始化后再交给 IoC 容器)。也就是说,可以在三级缓存
singletonFactories
中找到该状态下的 Bean 对象。 - 在创建过程中,都是从第三级缓存(对象工厂创建不完整对象),将提前暴露的对象放入到第二级缓存;从第二级缓存拿到后,完成初始化,并放入第一级缓存。
9.4 Spring 为什么不能解决prototype作用域循环依赖
Spring IoC 容器只会管理单例 Bean 的生命周期,并将单例 Bean 存放到缓存池中(三级缓存)。Spring 并不会管理 prototype
作用域的 Bean,也不会缓存该作用域的 Bean,而 Spring 中循环依赖的解决正是通过缓存来实现的。
9.5 非单例Bean的循环依赖如何解决
- 对于构造器注入产生的循环依赖,可以使用
@Lazy
注解,延迟加载。 - 对于多例 Bean 和
prototype
作用域产生的循环依赖,可以尝试改为单例 Bean。
9.6 二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存,使用三级缓存呢?
如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。
使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题,其实二级缓存同样也能很好解决循环引用问题。使用三级而非二级缓存并非出于 IOC 的考虑,而是出于 AOP 的考虑,即若使用二级缓存,在 AOP 情形注入到其他 Bean的,不是最终的代理对象,而是原始对象。
10. Spring 事务实现方式
- 编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
- 声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离。你只需要通过注解或者XML配置管理事务。
11. Spring 框架的事务管理有哪些优点
它为不同的事务API(如JTA, JDBC, Hibernate, JPA, 和JDO)提供了统一的编程模型。它为编程式事务管理提供了一个简单的API而非一系列复杂的事务API(如JTA).它支持声明式事务管理。它可以和Spring 的多种数据访问技术很好的融合。
12. 事务三要素是什么?
- 数据源:表示具体的事务性资源,是事务的真正处理者,如MySQL等。
- 事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。
- 事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属性如隔离级别、超时时间等。
13. 说说事务的传播级别
Spring事务定义了7种传播机制:
- PROPAGATION_REQUIRED:默认的Spring事物传播级别,若当前存在事务,则加入该事务,若不存在事务,则新建一个事务。
- PAOPAGATION_REQUIRE_NEW:若当前没有事务,则新建一个事务。若当前存在事务,则新建一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。
- PROPAGATION_NESTED:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一个事务,类似于REQUIRE_NEW。
- PROPAGATION_SUPPORTS:支持当前事务,若当前不存在事务,以非事务的方式执行。
- PROPAGATION_NOT_SUPPORTED:以非事务的方式执行,若当前存在事务,则把当前事务挂起。
- PROPAGATION_MANDATORY:强制事务执行,若当前不存在事务,则抛出异常.
- PROPAGATION_NEVER:以非事务的方式执行,如果当前存在事务,则抛出异常。
Spring事务传播级别一般不需要定义,默认就是PROPAGATION_REQUIRED,除非在嵌套事务的情况下需要重点了解。
14. 事务注解的本质是什么?
@Transactional 这个注解仅仅是一些(和事务相关的)元数据,在运行时被事务基础设施读取消费,并使用这些元数据来配置 bean 的事务行为。 大致来说具有两方面功能,一是表明该方法要参 与事务,二是配置相关属性来定制事务的参与方式和运行行为
声明式事务主要是得益于Spring AOP。使用一个事务拦截器,在方法调用的前后/周围进行事务性增强(advice),来驱动事务完成。
@Transactional注解既可以标注在类上,也可以标注在方法上。当在类上时,默认应用到类里的所 有方法。如果此时方法上也标注了,则方法上的优先级高。 另外注意方法一定要是public的。
15. spring事务(注解 @Transactional )失效的12种场景
-
访问权限问题 (只有public方法会生效),也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。
-
方法用final修饰,不会生效,
如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
-
(类本身) 未被spring管理
在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。
-
多线程调用,原因:如果事务方法内,开启了新线程去执行其他事务方法也是不受当前事务方法控制的。因为不同线程拥有的threadlocal 不一样。所以:当你需要明确开启新线程,请分开处理。
-
(存储引擎)表不支持事务,周所周知,在mysql5之前,默认的数据库引擎是myisam。
事务不回滚【五种】原因
-
错误的传播特性
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
-
自己吞了异常
事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常,这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。
16. 拦截器的执行顺序
- 根据当前请求,找到
HandlerExecutionChain
(可以处理请求的handler以及handler的所有 拦截器) - 先来顺序执行 所有拦截器的
preHandle()
方法。- 如果当前拦截器
preHandle()
返回为true
。则执行下一个拦截器的preHandle()
- 如果当前拦截器返回为
false
。直接倒序执行所有已经执行了的拦截器的afterCompletion();
。
- 如果当前拦截器
- 如果任何一个拦截器返回
false
,直接跳出不执行目标方法。 - 所有拦截器都返回
true
,才执行目标方法。 - 倒序执行所有拦截器的
postHandle()
方法。 - 前面的步骤有任何异常都会直接倒序触发
afterCompletion()
。 - 页面成功渲染完成以后,也会倒序触发
afterCompletion()
。
17. 过滤器 和 拦截器的区别
过滤器 和 拦截器 均体现了AOP
的编程思想,都可以实现诸如日志记录、登录鉴权等功能,但二者的不同点也是比较多的,接下来一一说明。
1、实现原理不同
过滤器和拦截器 底层实现方式大不相同,过滤器
是基于函数回调的,拦截器
则是基于Java的反射机制(动态代理)实现的。
在我们自定义的过滤器中都会实现一个 doFilter()
方法,这个方法有一个FilterChain
参数,而实际上它是一个回调接口。
2、使用范围不同
- 我们看到过滤器 实现的是
javax.servlet.Filter
接口,而这个接口是在Servlet
规范中定义的,也就是说过滤器Filter
的使用要依赖于Tomcat
等容器,导致它只能在web
程序中使用。 - 而拦截器(
Interceptor
) 它是一个Spring
组件,并由Spring
容器管理,并不依赖Tomcat
等容器,是可以单独使用的。不仅能应用在web
程序中,也可以用于Application
、Swing
等程序中。
3、触发时机不同
过滤器
和 拦截器
的触发时机也不同,我们看下边这张图。
过滤器Filter
是在请求进入容器后,但在进入servlet
之前进行预处理,请求结束是在servlet
处理完以后。
拦截器 Interceptor
是在请求进入servlet
后,在进入Controller
之前进行预处理的,Controller
中渲染了对应的视图之后请求结束。
4、拦截的请求范围不同
过滤器Filter
执行了两次,拦截器Interceptor
只执行了一次。这是因为过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller
中请求或访问static
目录下的资源请求起作用。
5、注入Bean情况不同
在实际的业务场景中,应用到过滤器或拦截器,为处理业务逻辑难免会引入一些service
服务。
6、控制执行顺序不同
实际开发过程中,会出现多个过滤器或拦截器同时存在的情况,不过,有时我们希望某个过滤器或拦截器能优先执行,就涉及到它们的执行顺序。
拦截器默认的执行顺序,就是它的注册顺序,也可以通过Order
手动设置控制,值越小越先执行。
而拦截器:16拦截器的执行顺序