这些Spring的面试题你应该这样答!

6.1、Spring IoC 的容器构建流程?

核心的构建流程如下,也就是 refresh 方法的核心内容:

3cc7704170b2c71bbefb1a8eac662c6.png3753d53c7c29e687ba35e9b7405cc11.png什么是IoC和DI?DI是如何实现的?

答:IoC叫控制反转,是Inversion of Control的缩写,DI(Dependency Injection)叫依赖注⼊,是对IoC更

简单的诠释。控制反转是把传统上由程序代码直接操控的对象的调⽤权交给容器,通过容器来实现对象组件的装配和管理。

所谓的"控制反转"就是对组件对象控制权的转移,从程序代码本⾝转移到了外部容器,由容器来创建对象并管理对象之间的依赖关系。IoC体现了好莱坞原则 - "Don’t call me, we will call you"。

依赖注⼊的基本原则是应⽤组件不应该负责查找资源或者其他依赖的协作对象。配置对象的⼯作应该由容器负责,查找资源的逻辑应该从应⽤组件的代码中抽取出来,交给容器来完成。

DI是对IoC更准确的描述,即组件之间的依赖关系由容器在运⾏期决定,形象的来说,即由容器动态的将某种依赖关系注⼊到组件之中。依赖注⼊可以通过setter⽅法注⼊(设值注⼊)、构造器注⼊和接⼝注⼊三种⽅式来实现,Spring⽀持setter注⼊和构造器注⼊,通常使⽤构造器注⼊来注⼊必须的依赖关系,对于可选的依赖关系,则setter注⼊是更好的选择,setter注⼊需要类提供⽆参构造器或者⽆参的静态⼯⼚⽅法来创建对象。

6.2、Spring Bean 的生命周期

bean 的⽣命周期主要有以下⼏个阶段,深⾊底的5个是⽐较重要的阶段。

c8063f8acfd9b5d096313d20a04ce05.png6.3、BeanFactory 和 FactoryBean 的区别?

BeanFactory:Spring 容器最核⼼也是最基础的接⼝,本质是个⼯⼚类,⽤于管理 bean 的⼯⼚,最核⼼的功能是加载 bean,也就是 getBean ⽅法,通常我们不会直接使⽤该接⼝,⽽是使⽤其⼦接⼝。

FactoryBean:该接⼝以 bean 样式定义,但是它不是⼀种普通的 bean,它是个⼯⼚ bean,实现该接⼝的类可以⾃⼰定义要创建的 bean 实例,只需要实现它的 getObject ⽅法即可。

FactoryBean 被⼴泛应⽤于 Java 相关的中间件中,如果你看过⼀些中间件的源码,⼀定会看到FactoryBean 的⾝影。

⼀般来说,都是通过 FactoryBean#getObject 来返回⼀个代理类,当我们触发调⽤时,会⾛到代理类中,从⽽可以在代理类中实现中间件的⾃定义逻辑,⽐如:RPC 最核⼼的⼏个功能,选址、建⽴连接、远程调⽤,还有⼀些⾃定义的监控、限流等等。

6.4、BeanFactory 和 ApplicationContext 的区别?

BeanFactory:基础 IoC 容器,提供完整的 IoC 服务⽀持。

ApplicationContext:⾼级 IoC 容器,BeanFactory 的⼦接⼝,在 BeanFactory 的基础上进⾏扩展。

包含 BeanFactory 的所有功能,还提供了其他⾼级的特性,⽐如:事件发布、国际化信息⽀持、统⼀

资源加载策略等。正常情况下,我们都是使⽤的 ApplicationContext。

a00a50960bc078f9b3f8232416865aa.png

这边以电话来举个简单的例⼦:

我们家⾥使⽤的 “座机” 就类似于 BeanFactory,可以进⾏电话通讯,满⾜了最基本的需求。⽽现在⾮常普及的智能⼿机,iPhone、⼩⽶等,就类似于 ApplicationContext,除了能进⾏电话通讯,还有其他很多功能:拍照、地图导航、听歌等。

6.5、Spring 的 AOP 是怎么实现的?

本质是通过动态代理来实现的,主要有以下⼏个步骤。

1)获取增强器,例如被 Aspect 注解修饰的类。

2)在创建每⼀个 bean 时,会检查是否有增强器能应⽤于这个 bean,简单理解就是该 bean 是否在该增强器指定的 execution 表达式中。如果是,则将增强器作为拦截器参数,使⽤动态代理创建bean 的代理对象实例。

3)当我们调⽤被增强过的 bean 时,就会⾛到代理类中,从⽽可以触发增强器,本质跟拦截器类似。

你如何理解AOP中的连接点(Joinpoint)、切点(Pointcut)、增强(Advice)、引介(Introduction)、织⼊(Weaving)、切⾯(Aspect)这些概念?

答:

a. 连接点(Joinpoint):程序执⾏的某个特定位置(如:某个⽅法调⽤前、调⽤后,⽅法抛出异常后)。⼀个类或⼀段程序代码拥有⼀些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅⽀持⽅法的连接点。

b. 切点(Pointcut):如果连接点相当于数据中的记录,那么切点相当于查询条件,⼀个切点可以匹配多个连接点。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。

c. 增强(Advice):增强是织⼊到⽬标类连接点上的⼀段程序代码。Spring提供的增强接⼝都是带⽅位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。很多资料上将增强译为“通知”,这明显是个词不达意的翻译,让很多程序员困惑了许久。说明: Advice在国内的很多书⾯资料中都被翻译成"通知",但是很显然这个翻译⽆法表达其本质,有少量的读物上将这个词翻译为"增强",这个翻译是对Advice较为准确的诠释,我们通过AOP将横切关注功能加到原有的业务逻辑上,这就是对原有业务逻辑的⼀种增强,这种增强可以是前置增强、后置增强、返回后增强、抛异常时增强和包围型增强。

d. 引介(Introduction):引介是⼀种特殊的增强,它为类添加⼀些属性和⽅法。这样,即使⼀个业务类原本没有实现某个接⼝,通过引介功能,可以动态的未该业务类添加接⼝的实现逻辑,让业务类成为这个接⼝的实现类。

e. 织⼊(Weaving):织⼊是将增强添加到⽬标类具体连接点上的过程,AOP有三种织⼊⽅式:

①编译期织⼊:需要特殊的Java编译期(例如AspectJ的ajc);

②装载期织⼊:要求使⽤特殊的类加载器,在装载类的时候对类进⾏增强;

③运⾏时织⼊:在运⾏时为⽬标类⽣成代理实现增强。Spring采⽤了动态代理的⽅式实现了运⾏时织⼊,⽽AspectJ采⽤了编译期织⼊和装载期织⼊的⽅式。

f. 切⾯(Aspect):切⾯是由切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义。

补充:代理模式是GoF提出的23种设计模式中最为经典的模式之⼀,代理模式是对象的结构模式,它给某⼀个对象提供⼀个代理对象,并由代理对象控制对原对象的引⽤。

简单的说,代理对象可以完成⽐原对象更多的职责,当需要为原对象添加横切关注功能时,就可以使⽤原对象的代理对象。

我们在打开Office系列的Word⽂档时,如果⽂档中有插图,当⽂档刚加载时,⽂档中的插图都只是⼀个虚框占位符,等⽤⼾真正翻到某⻚要查看该图⽚时,才会真正加载这张图,这其实就是对代理模式的使⽤,代替真正图⽚的虚框就是⼀个虚拟代理;

Hibernate的load⽅法也是返回⼀个虚拟代理对象,等⽤⼾真正需要访问对象的属性时,才向数据库发出SQL语句获得真实对象

6.6、多个AOP的顺序怎么定?

通过 Ordered 和 PriorityOrdered 接⼝进⾏排序。PriorityOrdered 接⼝的优先级⽐ Ordered 更⾼,

如果同时实现 PriorityOrdered 或 Ordered 接⼝,则再按 order 值排序,值越⼩的优先级越⾼。

6.7、Spring 的 AOP 有哪⼏种创建代理的⽅式?

Spring 中的 AOP ⽬前⽀持 JDK 动态代理和 Cglib 代理。

通常来说:如果被代理对象实现了接⼝,则使⽤ JDK 动态代理,否则使⽤ Cglib 代理。另外,也可以

通过指定 proxyTargetClass=true 来实现强制⾛ Cglib 代理。

6.8、JDK 动态代理和 Cglib 代理的区别?

1)JDK 动态代理本质上是实现了被代理对象的接⼝,⽽ Cglib 本质上是继承了被代理对象,覆盖其中的⽅法。

2)JDK 动态代理只能对实现了接⼝的类⽣成代理,Cglib 则没有这个限制。但是 Cglib 因为使⽤继承实现,所以 Cglib ⽆法代理被 final 修饰的⽅法或类。

3)在调⽤代理⽅法上,JDK 是通过反射机制调⽤,Cglib是通过FastClass 机制直接调⽤。FastClass简单的理解,就是使⽤ index 作为⼊参,可以直接定位到要调⽤的⽅法直接进⾏调⽤。

4)在性能上,JDK1.7 之前,由于使⽤了 FastClass 机制,Cglib 在执⾏效率上⽐ JDK 快,但是随着JDK 动态代理的不断优化,从 JDK 1.7 开始,JDK 动态代理已经明显⽐ Cglib 更快了。

6.9、JDK 动态代理为什么只能对实现了接⼝的类⽣成代理?

根本原因是通过 JDK 动态代理⽣成的类已经继承了 Proxy 类,所以⽆法再使⽤继承的⽅式去对类实现代理。

6.10、Spring 的事务传播⾏为有哪些?

1)REQUIRED:Spring 默认的事务传播级别,如果上下⽂中已经存在事务,那么就加⼊到事务中执⾏,如果当前上下⽂中不存在事务,则新建事务执⾏。

2)REQUIRES_NEW:每次都会新建⼀个事务,如果上下⽂中有事务,则将上下⽂的事务挂起,当新建事务执⾏完成以后,上下⽂事务再恢复执⾏。

3)SUPPORTS:如果上下⽂存在事务,则加⼊到事务执⾏,如果没有事务,则使⽤⾮事务的⽅式执⾏。

4)MANDATORY:上下⽂中必须要存在事务,否则就会抛出异常。

5)NOT_SUPPORTED :如果上下⽂中存在事务,则挂起事务,执⾏当前逻辑,结束后恢复上下⽂的事务。

6)NEVER:上下⽂中不能存在事务,否则就会抛出异常。

7)NESTED:嵌套事务。如果上下⽂中存在事务,则嵌套事务执⾏,如果不存在事务,则新建事务。

6.11、Spring 的事务隔离级别?

Spring 的事务隔离级别底层其实是基于数据库的,Spring 并没有⾃⼰的⼀套隔离级别。

DEFAULT:使⽤数据库的默认隔离级别。

READ_UNCOMMITTED:读未提交,最低的隔离级别,会读取到其他事务还未提交的内容,存在脏读。

READ_COMMITTED:读已提交,读取到的内容都是已经提交的,可以解决脏读,但是存在不可重复读。

REPEATABLE_READ:可重复读,在⼀个事务中多次读取时看到相同的内容,可以解决不可重复读,但是存在幻读。

SERIALIZABLE:串⾏化,最⾼的隔离级别,对于同⼀⾏记录,写会加写锁,读会加读锁。在这种情况下,只有读读能并发执⾏,其他并⾏的读写、写读、写写操作都是冲突的,需要串⾏执⾏。可以防⽌脏读、不可重复度、幻读,没有并发事务问题。

6.12、Spring 的事务隔离级别是如何做到和数据库不⼀致的?

⽐如数据库是可重复读,Spring 是读已提交,这是怎么实现的?

Spring 的事务隔离级别本质上还是通过数据库来控制的,具体是在执⾏事务前先执⾏命令修改数据库

隔离级别,命令格式如下:

1 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED

6.13、Spring 事务的实现原理?

Spring 事务的底层实现主要使⽤的技术:AOP(动态代理) + ThreadLocal + try/catch。动态代理:基本所有要进⾏逻辑增强的地⽅都会⽤到动态代理,AOP 底层也是通过动态代理实现。

ThreadLocal:主要⽤于线程间的资源隔离,以此实现不同线程可以使⽤不同的数据源、隔离级别等等。

try/catch:最终是执⾏ commit 还是 rollback,是根据业务逻辑处理是否抛出异常来决定。

Spring 事务的核⼼逻辑伪代码如下:

public void invokeWithinTransaction() {
// 1.事务资源准备
   try {
// 1.业务逻辑处理,也就是调⽤被代理的⽅法
  } catch (Exception e) {
// 2.出现异常,进⾏回滚并将异常抛出
  } finally {
// 现场还原:还原旧的事务信息
  }
// 5.正常执⾏,进⾏事务的提交
// 返回业务逻辑处理结果
}

详细流程如下图所⽰:

89d047ebbc870e509bec5fbf4f58e82.png6.14、Spring 怎么解决循环依赖的问题?

Spring 是通过提前暴露 bean 的引⽤来解决的,具体如下。

Spring ⾸先使⽤构造函数创建⼀个 “不完整” 的 bean 实例(之所以说不完整,是因为此时该 bean

实例还未初始化),并且提前曝光该 bean 实例的 ObjectFactory(提前曝光就是将 ObjectFactory

放到 singletonFactories 缓存).

通过 ObjectFactory 我们可以拿到该 bean 实例的引⽤,如果出现循环引⽤,我们可以通过缓存中的

ObjectFactory 来拿到 bean 实例,从⽽避免出现循环引⽤导致的死循环。

举个例⼦:A 依赖了 B,B 也依赖了 A,那么依赖注⼊过程如下。

• 检查 A 是否在缓存中,发现不存在,进⾏实例化

• 通过构造函数创建 bean A,并通过 ObjectFactory 提前曝光 bean A

• A ⾛到属性填充阶段,发现依赖了 B,所以开始实例化 B。

• ⾸先检查 B 是否在缓存中,发现不存在,进⾏实例化

• 通过构造函数创建 bean B,并通过 ObjectFactory 曝光创建的 bean B

• B ⾛到属性填充阶段,发现依赖了 A,所以开始实例化 A。

• 检查 A 是否在缓存中,发现存在,拿到 A 对应的 ObjectFactory 来获得 bean A,并返回。B 继续接下来的流程,直⾄创建完毕,然后返回 A 的创建流程,A 同样继续接下来的流程,直⾄创建完毕。

这边通过缓存中的 ObjectFactory 拿到的 bean 实例虽然拿到的是 “不完整” 的 bean 实例,但是由于是单例,所以后续初始化完成后,该 bean 实例的引⽤地址并不会变,所以最终我们看到的还是完整 bean 实例。

6.15、Spring 能解决构造函数循环依赖吗?

答案是不⾏的,对于使⽤构造函数注⼊产⽣的循环依赖,Spring 会直接抛异常。

为什么⽆法解决构造函数循环依赖?

上⾯解决逻辑的第⼀句话:“⾸先使⽤构造函数创建⼀个 “不完整” 的 bean 实例”,从这句话可以

看出,构造函数循环依赖是⽆法解决的,因为当构造函数出现循环依赖,我们连 “不完整” 的 bean

实例都构建不出来。

6.16、Spring 三级缓存解决循环依赖

Spring 的三级缓存其实就是解决循环依赖时所⽤到的三个缓存。

singletonObjects:正常情况下的 bean 被创建完毕后会被放到该缓存,key:beanName,value:bean 实例。

singletonFactories:上⾯说的提前曝光的 ObjectFactory 就会被放到该缓存中,key:beanName,

value:ObjectFactory。

earlySingletonObjects:该缓存⽤于存放 ObjectFactory 返回的 bean,也就是说对于⼀个 bean,ObjectFactory 只会被⽤⼀次,之后就通过 earlySingletonObjects 来获取

key:beanName,

value:早期 bean 实例。

6.17、@Resource 和 @Autowire 的区别?

1)@Resource 和 @Autowired 都可以⽤来装配 bean

2)@Autowired 默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false。

3)@Resource 如果指定了 name 或 type,则按指定的进⾏装配;如果都不指定,则优先按名称装配,当找不到与名称匹配的 bean 时才按照类型进⾏装配。

6.18、@Autowire 怎么使⽤名称来注⼊?

配合 @Qualifier 使⽤,如下所⽰:

@Component
public class Test {
  @Autowired
 @Qualifier("userService")
 private UserService userService;
}

6.19、@PostConstruct 修饰的⽅法⾥⽤到了其他 bean 实例,会有问题

吗?

🔊 https://www.bilibili.com/video/BV1dY4y1Y7pC?p=43

该题可以拆解成下⾯3个问题:

1)@PostConstruct 修饰的⽅法被调⽤的时间

2)bean 实例依赖的其他 bean 被注⼊的时间,也可理解为属性的依赖注⼊时间

3)步骤2的时间是否早于步骤1:如果是,则没有问题,如果不是,则有问题

解析

1. PostConstruct 注解被封装在 CommonAnnotationBeanPostProcessor中,具体触发时间是在
postProcessBeforeInitialization ⽅法,从 doCreateBean 维度看,则是在
initializeBean ⽅法⾥,属于初始化 bean 阶段。
1. 属性的依赖注⼊是在 populateBean ⽅法⾥,属于属性填充阶段。
2. 属性填充阶段位于初始化之前,所以本题答案为没有问题。

6.20、bean 的 init-method 属性指定的⽅法⾥⽤到了其他 bean 实例,会有问题吗?

该题同上⾯这题类似,只是将 @PostConstruct 换成了 init-method 属性。

答案是不会有问题。同上⾯⼀样,init-method 属性指定的⽅法也是在 initializeBean ⽅法⾥被触发,属于初始化 bean 阶段。

6.21、要在 Spring IoC 容器构建完毕之后执行⼀些逻辑,怎么实现?

1)⽐较常⻅的⽅法是使⽤事件监听器,实现 ApplicationListener 接⼝,监听ContextRefreshedEvent 事件。

2)还有⼀种⽐较少⻅的⽅法是实现 SmartLifecycle 接⼝,并且 isAutoStartup ⽅法返回 true,则会在 finishRefresh() ⽅法中被触发。

两种⽅式都是在 finishRefresh 中被触发,SmartLifecycle在ApplicationListener之前。

6.22、Spring 中的常见扩展点有哪些?

1)ApplicationContextInitializerinitialize ⽅法,在 Spring 容器刷新前触发,也就是 refresh ⽅法前被触发。

2)BeanFactoryPostProcessorpostProcessBeanFactory ⽅法,在加载完 Bean 定义之后,创建 Bean 实例之前被触发,通常使⽤

该扩展点来加载⼀些⾃⼰的 bean 定义。

3)BeanPostProcessorpostProcessBeforeInitialization ⽅法,执⾏ bean 的初始化⽅法前被触发;postProcessAfterInitialization ⽅法,执⾏ bean 的初始化⽅法后被触发。

4)@PostConstruct该注解被封装在 CommonAnnotationBeanPostProcessor 中,具体触发时间是在postProcessBeforeInitialization ⽅法。

5)InitializingBeanafterPropertiesSet ⽅法,在 bean 的属性填充之后,初始化⽅法(init-method)之前被触发,该⽅法的作⽤基本等同于 init-method,主要⽤于执⾏初始化相关操作。

6)ApplicationListener,事件监听器onApplicationEvent ⽅法,根据事件类型触发时间不同,通常使⽤的 ContextRefreshedEvent 触发时间为上下⽂刷新完毕,通常⽤于 IoC 容器构建结束后处理⼀些⾃定义逻辑。

7)@PreDestroy该注解被封装在 DestructionAwareBeanPostProcessor 中,具体触发时间是在postProcessBeforeDestruction ⽅法,也就是在销毁对象之前触发。

8)DisposableBeandestroy ⽅法,在 bean 的销毁阶段被触发,该⽅法的作⽤基本等同于 destroy-method,主⽤⽤于执⾏销毁相关操作。

6.23、Spring中如何让两个bean按顺序加载?

1)使⽤ @DependsOn、depends-on

2)让后加载的类依赖先加载的类

@Component
public class A {
@Autowire
p

3)使⽤扩展点提前加载,例如:BeanFactoryPostProcessor

@Component
public class TestBean implements BeanFactoryPostProcessor {
   @Override
   public void postProcessBeanFactory(ConfigurableListableBeanFactory
   configurableListableBeanFactory) throws BeansException {
// 加载bean
   beanFactory.getBean("a");
  }
}

6.24、mybatis

iBATIS 的着⼒点,则在于POJO 与SQL之间的映射关系。然后通过映射配置⽂件,将SQL所需的参数,以及返回的结果字段映射到指定POJO。 相对Hibernate“O/R”⽽⾔,iBATIS 是⼀种“SqlMapping”的ORM实现。

Mybatis优势

MyBatis可以进⾏更为细致的SQL优化,可以减少查询字段。

MyBatis容易掌握,⽽Hibernate⻔槛较⾼。

解释⼀下MyBatis中命名空间(namespace)的作⽤。

答:在⼤型项⽬中,可能存在⼤量的SQL语句,这时候为每个SQL语句起⼀个唯⼀的标识(ID)就变得并不容易了。为了解决这个问题,在MyBatis中,可以为每个映射⽂件起⼀个唯⼀的命名空间,这样定义在这个映射⽂件中的每个SQL语句就成了定义在这个命名空间中的⼀个ID。只要我们能够保证每个命名空间中这个ID是唯⼀的,即使在不同映射⽂件中的语句ID相同,也不会再产⽣冲突了。

MyBatis中的动态SQL是什么意思?

答:对于⼀些复杂的查询,我们可能会指定多个查询条件,但是这些条件可能存在也可能不存在,例如在58同城上⾯找房⼦,我们可能会指定⾯积、楼层和所在位置来查找房源,也可能会指定⾯积、价格、⼾型和所在位置来查找房源,此时就需要根据⽤⼾指定的条件动态⽣成SQL语句。如果不使⽤持久层框架我们可能需要⾃⼰拼装SQL语句,还好MyBatis提供了动态SQL的功能来解决

这个问题。MyBatis中⽤于实现动态SQL的元素主要有:

- if

- choose / when / otherwise

- trim

- where

- set

- foreach

Mybatis调优⽅案

MyBatis在Session⽅⾯和Hibernate的Session⽣命周期是⼀致的,同样需要合理的Session管理机

制。MyBatis同样具有⼆级缓存机制。 MyBatis可以进⾏详细的SQL优化设计。MyBatis缓存

MyBatis 包含⼀个⾮常强⼤的查询缓存特性,它可以⾮常⽅便地配置和定制。MyBatis 3 中的缓存实现

的很多改进都已经实现了,使得它更加强⼤⽽且易于配置。默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强变现⽽且处理循环 依赖也是必须的。要开启⼆级缓存,你需要在你的 SQL 映射⽂件中添加⼀⾏: <cache/>

字⾯上看就是这样。这个简单语句的效果如下:

映射语句⽂件中的所有 select 语句将会被缓存。

映射语句⽂件中的所有 insert,update 和 delete 语句会刷新缓存。

缓存会使⽤ Least Recently Used(LRU,最近最少使⽤的)算法来收回。

根据时间表(⽐如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。缓存会存储列表集合或对象(⽆论查询⽅法返回什么)的 1024 个引⽤。

缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,⽽ 且可以安全地被调⽤者修改,⽽不⼲扰其他调⽤者或线程所做的潜在修改。所有的这些属性都可以通过缓存元素的属性来修改。⽐如: <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

这个更⾼级的配置创建了⼀个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引⽤,⽽且返回的对象被认为是只读的,因此在不同线程中的调⽤者之间修改它们会 导致冲突。可⽤的收回策略有,

默认的是 LRU:

LRU ‒ 最近最少使⽤的:移除最⻓时间不被使⽤的对象。

FIFO ‒ 先进先出:按对象进⼊缓存的顺序来移除它们。

SOFT ‒ 软引⽤:移除基于垃圾回收器状态和软引⽤规则的对象。

WEAK ‒ 弱引⽤:更积极地移除基于垃圾收集器状态和弱引⽤规则的对象。

flushInterval(刷新间隔)可以被设置为任意的正整数,⽽且它们代表⼀个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调⽤语句时刷新。

size(引⽤数⽬)可以被设置为任意正整数,要记住你缓存的对象数⽬和你运⾏环境的 可⽤内存资源数⽬。默认值是1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调⽤者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷⻉(通过序列化) 。这会慢⼀些,但是安全,因此默认是 false。

6.25、使⽤ Mybatis 时,调⽤ DAO接⼝时是怎么调⽤到 SQL 的?

简单点说,当我们使⽤ Spring+MyBatis 时:

1、DAO接⼝会被加载到 Spring 容器中,通过动态代理来创建

2、XML中的SQL会被解析并保存到本地缓存中,key是SQL 的 namespace + id,value 是SQL的封装

3、当我们调⽤DAO接⼝时,会⾛到代理类中,通过接⼝的全路径名,从步骤2的缓存中找到对应的SQL,然后执⾏并返回结果

6.26、springmvc的核⼼是什么,请求的流程是怎么处理的,控制反转怎

么实现的

springmvc是基于servlet的前端控制框架,核⼼是ioc和aop(基于spring实现)

核⼼架构的具体流程步骤如下:

1、⾸先⽤⼾发送请求⸺>DispatcherServlet,前端控制器收到请求后⾃⼰不进⾏处理,⽽是委托给

其他的解析器进⾏

处理,作为统⼀访问点,进⾏全局的流程控制;

2、DispatcherServlet⸺>HandlerMapping, HandlerMapping 将会把请求映射为

HandlerExecutionChain 对象(包含⼀个Handler 处理器(⻚⾯控制器)对象、多个

HandlerInterceptor 拦截器)对象,通过这种策略模式,很容易添加新的映射策略;

3、DispatcherServlet⸺>HandlerAdapter,HandlerAdapter 将会把处理器包装为适配器,从⽽⽀

持多种类型的处理器,即适配器设计模式的应⽤,从⽽很容易⽀持很多类型的处理器;

4、HandlerAdapter⸺>处理器功能处理⽅法的调⽤,HandlerAdapter 将会根据适配的结果调⽤真

正的处理器的功能处

理⽅法,完成功能处理;并返回⼀个ModelAndView 对象(包含模型数据、逻辑视图名);

千锋教育 | 初心至善、匠心育人

做真实的自己 用良心做教育

千锋Java面试宝典5、ModelAndView的逻辑视图名⸺> ViewResolver, ViewResolver 将

把逻辑视图名解析为具体的

View,通过这种策

略模式,很容易更换其他视图技术;

6、View⸺>渲染,View会根据传进来的Model模型数据进⾏渲染,此处的Model实际是⼀个Map数

据结构,因此

很容易⽀持其他视图技术;

7、返回控制权给DispatcherServlet,由DispatcherServlet返回响应给⽤⼾,到此⼀个流程结束

IOC控制反转的实现是基于spring的bean⼯⼚,通过获取要创建的类的class全限定名称,反射创建对象

6.27、Hibernate

Hibernate对数据库结构提供了较为完整的封装,Hibernate的O/R Mapping实现了POJO 和数据库表

之间的映射,以及SQL 的⾃动⽣成和执⾏。程序员往往只需定义好了POJO 到数据库表的映射关系,

即可通过Hibernate 提供的⽅法完成持久层操

作。程序员甚⾄不需要对SQL 的熟练掌握, Hibernate/OJB 会根据制定的存储逻辑,⾃动⽣成对应

的SQL 并调⽤JDBC 接⼝加以执⾏。

Hibernate的调优⽅案

制定合理的缓存策略;

尽量使⽤延迟加载特性;

采⽤合理的Session管理机制;

使⽤批量抓取,设定合理的批处理参数(

batch_size);

进⾏合理的O/R映射设计

SQL优化⽅⾯

Hibernate的查询会将表中的所有字段查询出来,这⼀点会有性能消耗。Hibernate也可以⾃⼰写SQL

来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。⽽Mybatis的SQL是⼿动编写的,

所以可以按需求指定查询的字段。

Hibernate HQL语句的调优需要将SQL打印出来,⽽Hibernate的SQL被很多⼈嫌弃因为太丑了。

MyBatis的SQL是⾃⼰⼿动写的所以调整⽅便。但Hibernate具有⾃⼰的⽇志统计。Mybatis本⾝不带

⽇志统计,使⽤Log4j进⾏⽇志记录。

抓取策略

Hibernate对实体关联对象的抓取有着良好的机制。对于每⼀个关联关系都可以详细地设置是否延迟

加载,并且提供关联抓取、查询抓取、⼦查询抓取、批量抓取四种模式。 它是详细配置和处理的。

千锋教育 | 初心至善、匠心育人

做真实的自己 用良心做教育

千锋Java面试宝典Hibernate缓存

Hibernate⼀级缓存是Session缓存,利⽤好⼀级缓存就需要对Session的⽣命周期进⾏管理好。建议

在⼀个Action操作中使⽤⼀个Session。⼀级缓存需要对Session进⾏严格管理。

Hibernate⼆级缓存是SessionFactory级的缓存。 SessionFactory的缓存分为内置缓存和外置缓存。

内置缓存中存放的是SessionFactory对象的⼀些集合属性包含的数据(映射元素据及预定SQL语句等),

对于应⽤程序来说,它是只读的。

外置缓存中存放的是数据库数据的副本,其作⽤和⼀级缓存类似.⼆级缓存除了以内存作为存储介质外,

还可以选⽤硬盘等外部存储设备。⼆级缓存称为进程级缓存或SessionFactory级缓存,

它可以被所有session共享,它的⽣命周期伴随着SessionFactory的⽣命周期存在和消亡。

Hibernate中SessionFactory是线程安全的吗?Session是线程安全的吗(两个线程能够共享同⼀个

Session吗)?

答:SessionFactory对应Hibernate的⼀个数据存储的概念,它是线程安全的,可以被多个线程并发

访问。SessionFactory⼀般只会在启动的时候构建。

对于应⽤程序,最好将SessionFactory通过单例模式进⾏封装以便于访问。Session是⼀个轻量级⾮

线程安全的对象(线程间不能共享session),它表⽰与数据库进⾏交互的⼀个⼯作单元。

Session是由SessionFactory创建的,在任务完成之后它会被关闭。Session是持久层服务对外提供的

主要接⼝。Session会延迟获取数据库连接(也就是在需要的时候才会获取)。

为了避免创建太多的session,可以使⽤ThreadLocal将session和当前线程绑定在⼀起,这样可以让

同⼀个线程获得的总是同⼀个session。Hibernate 3中SessionFactory的getCurrentSession()⽅法就

可以做到。

Hibernate中Session的load和get⽅法的区别是什么?

答:主要有以下三项区别:

① 如果没有找到符合条件的记录,get⽅法返回null,load⽅法抛出异常。

② get⽅法直接返回实体类对象,load⽅法返回实体类对象的代理。

③ 在Hibernate 3之前,get⽅法只在⼀级缓存中进⾏数据查找,如果没有找到对应的数据则越过⼆级

缓存,直接发出SQL语句完成数据读取;load⽅法则可以从⼆级缓存中获取数据;从Hibernate 3开

始,get⽅法不再是对⼆级缓存只写不读,它也是可以访问⼆级缓存的。

说明:对于load()⽅法Hibernate认为该数据在数据库中⼀定存在可以放⼼的使⽤代理来实现延迟加

载,如果没有数据就抛出异常,⽽通过get()⽅法获取的数据可以不存在

Session的save()、update()、merge()、lock()、saveOrUpdate()和

persist()⽅法分别是做什么

的?有什么区别?

答:Hibernate的对象有三种状态:瞬时态(

transient)、持久态(

persistent)和游离态

detached),瞬时态的实例可以通过调⽤save()、persist()或者saveOrUpdate()⽅法变成持久态;

游离态的实例可以通过调⽤ update()、saveOrUpdate()、lock()或者replicate()变成持久态。save()

和persist()将会引发SQL的INSERT语句,⽽update()或merge()会引发UPDATE语句。

save()和update()的区别在于⼀个是将瞬时态对象变成持久态,⼀个是将游离态对象变为持久态。

merge()⽅法可以完成save()和update()⽅法的功能,它的意图是将新的状态合并到已有的持久化对

象上或创建新的持久化对象。

对于persist()⽅法,按照官⽅⽂档的说明:

① persist()⽅法把⼀个瞬时态的实例持久化,但是并不保证标识符被⽴刻填⼊到持久化实例中,标识

符的填⼊可能被推迟到flush的时间;② persist()⽅法保证当它在⼀个事务外部被调⽤的时候并不触

发⼀个INSERT语句,当需要封装⼀个⻓会话流程的时候,persist()⽅法是很有必要的;

③ save()⽅法不保证第②条,它要返回标识符,所以它会⽴即执⾏INSERT语句,不管是在事务内部

还是外部。⾄于lock()⽅法和update()⽅法的区别,update()⽅法是把⼀个已经更改过的脱管状态的

对象变成持久状态;lock()⽅法是把⼀个没有更改过的脱管状态的对象变成持久状态。

阐述Session加载实体对象的过程?

答:Session加载实体对象的步骤是:

① Session在调⽤数据库查询功能之前,⾸先会在⼀级缓存中通过实体类型和主键进⾏查找,如果⼀

级缓存查找命中且数据状态合法,则直接返回;

② 如果⼀级缓存没有命中,接下来Session会在当前NonExists记录(相当于⼀个查询⿊名单,如果

出现重复的⽆效查询可以迅速做出判断,从⽽提升性能)中进⾏查找,如果NonExists中存在同样的

查询条件,则返回null;

③ 如果⼀级缓存查询失败则查询⼆级缓存,如果⼆级缓存命中则直接返回;

④ 如果之前的查询都未命中,则发出SQL语句,如果查询未发现对应记录则将此次查询添加到

Session的NonExists中加以记录,并返回null;

⑤ 根据映射配置和SQL语句得到ResultSet,并创建对应的实体对象;

⑥ 将对象纳⼊Session(⼀级缓存)的管理;

⑦ 如果有对应的拦截器,则执⾏拦截器的onLoad⽅法;

⑧ 如果开启并设置了要使⽤⼆级缓存,则将数据对象纳⼊⼆级缓存;

⑨ 返回数据对象。

Query接⼝的list⽅法和iterate⽅法有什么区别?

答:

千锋教育 | 初心至善、匠心育人

做真实的自己 用良心做教育

千锋Java面试宝典① list()⽅法⽆法利⽤⼀级缓存和⼆级缓存(对缓存只写不读),它只能在

开启查询缓存的前提下使⽤

查询缓存;iterate()⽅法可以充分利⽤缓存,如果⽬标数据只读或者读取频繁,使⽤iterate()⽅法可

以减少性能开销。

② list()⽅法不会引起N+1查询问题,⽽iterate()⽅法可能引起N+1查询问题

如何理解Hibernate的延迟加载机制?在实际应⽤中,延迟加载与Session关闭的⽭盾是如何处理

的?

答:延迟加载就是并不是在读取的时候就把数据加载进来,⽽是等到使⽤时再加载。Hibernate使⽤

了虚拟代理机制实现延迟加载,我们使⽤Session的load()⽅法加载数据或者⼀对多关联映射在使⽤延

迟加载的情况下从⼀的⼀⽅加载多的⼀⽅,

得到的都是虚拟代理,简单的说返回给⽤⼾的并不是实体本⾝,⽽是实体对象的代理。代理对象在用户调⽤getter⽅法时才会去数据库加载数据。但加载数据就需要数据库连接。⽽当我们把会话关闭时,数据库连接就同时关闭了。

延迟加载与session关闭的⽭盾⼀般可以这样处理

① 关闭延迟加载特性。这种⽅式操作起来⽐较简单,因为Hibernate的延迟加载特性是可以通过映射⽂件或者注解进⾏配置的,但这种解决⽅案存在明显的缺陷。⾸先,出现"no session or session was closed"通常说明系统中已经存在主外键关联,如果去掉延迟加载的话,每次查询的开销都会变得很⼤。

② 在session关闭之前先获取需要查询的数据,可以使⽤⼯具⽅法Hibernate.isInitialized()判断对象是否被加载,如果没有被加载则可以使⽤Hibernate.initialize()⽅法加载对象。

③ 使⽤拦截器或过滤器延⻓Session的⽣命周期直到视图获得数据。Spring整合Hibernate提供的OpenSessionInViewFilter和OpenSessionInViewInterceptor就是这种做法。

谈⼀下你对继承映射的理解。

答:继承关系的映射策略有三种:

① 每个继承结构⼀张表(table per class hierarchy),不管多少个⼦类都⽤⼀张表。

② 每个⼦类⼀张表(table per subclass),公共信息放⼀张表,特有信息放单独的表。

③ 每个具体类⼀张表(table per concrete class),有多少个⼦类就有多少张表。

第⼀种⽅式属于单表策略,其优点在于查询⼦类对象的时候⽆需表连接,查询速度快,适合多态查询;缺点是可能导致表很⼤。后两种⽅式属于多表策略,其优点在于数据存储紧凑,其缺点是需要进⾏连接查询,不适合多态查询。

简述Hibernate常⻅优化策略。

答:这个问题应当挑⾃⼰使⽤过的优化策略回答,常⽤的有:

① 制定合理的缓存策略(⼆级缓存、查询缓存)。

② 采⽤合理的Session管理机制。

③ 尽量使⽤延迟加载特性。

④ 设定合理的批处理参数。

⑤ 如果可以,选⽤UUID作为主键⽣成器。

⑥ 如果可以,选⽤基于版本号的乐观锁替代悲观锁。

⑦ 在开发过程中, 开启hibernate.show_sql选项查看⽣成的SQL,从⽽了解底层的状况;开发完成后关闭此选项。

⑧ 考虑数据库本⾝的优化,合理的索引、恰当的数据分区策略等都会对持久层的性能带来可观的提升,但这些需要专业的DBA(数据库管理员)提供⽀持。

谈⼀谈Hibernate的⼀级缓存、⼆级缓存和查询缓存。

答:Hibernate的Session提供了⼀级缓存的功能,默认总是有效的,当应⽤程序保存持久化实体、修改持久化实体时,Session并不会⽴即把这种改变提交到数据库,⽽是缓存在当前的Session中,除⾮显⽰调⽤了Session的flush()⽅法或通过close()⽅法关闭Session。通过⼀级缓存,可以减少程序与数据库的交互,从⽽提⾼数据库访问性能。

SessionFactory级别的⼆级缓存是全局性的,所有的Session可以共享这个⼆级缓存。不过⼆级缓存默认是关闭的,需要显⽰开启并指定需要使⽤哪种⼆级缓存实现类(可以使⽤第三⽅提供的实现)。

⼀旦开启了⼆级缓存并设置了需要使⽤⼆级缓存的实体类,SessionFactory就会缓存访问过的该实体类的每个对象,除⾮缓存的数据超出了指定的缓存空间。

⼀级缓存和⼆级缓存都是对整个实体进⾏缓存,不会缓存普通属性,如果希望对普通属性进⾏缓存,

可以使⽤查询缓存。查询缓存是将HQL或SQL语句以及它们的查询结果作为键值对进⾏缓存,对于同样的查询可以直接从缓存中获取数据。查询缓存默认也是关闭的,需要显⽰开启。

Hibernate中DetachedCriteria类是做什么的?

答:DetachedCriteria和Criteria的⽤法基本上是⼀致的,但Criteria是由Session的createCriteria()⽅法创建的,也就意味着离开创建它的Session,Criteria就⽆法使⽤了。DetachedCriteria不需要Session就可以创建(使⽤DetachedCriteria.forClass()⽅法创建),所以通常也称其为离线的Criteria,在需要进⾏查询操作的时候再和Session绑定(调⽤其getExecutableCriteria(Session)⽅法),这也就意味着⼀个DetachedCriteria可以在需要的时候和不同的Session进⾏绑定。

@OneToMany注解的mappedBy属性有什么作⽤?

答:@OneToMany⽤来配置⼀对多关联映射,但通常情况下,⼀对多关联映射都由多的⼀⽅来维护

关联关系,例如学⽣和班级,应该在学⽣类中添加班级属性来维持学⽣和班级的关联关系(在数据库中是由学⽣表中的外键班级编号来维护学⽣表和班级表的多对⼀关系)如果要使⽤双向关联,在班级类中添加⼀个容器属性来存放学⽣,并使⽤@OneToMany注解进⾏映射,此时mappedBy属性就⾮常重要。如果使⽤XML进⾏配置,可以⽤<set>标签的inverse="true"设置来达到同样的效果。