Spring常见面试问题
# Spring常见面试问题
草稿
# 1. 什么是 Spring 框架?
开源轻量级 Java 开发框架,Spring 框架指的是Spring Framework,它是很多模块的集合,使用这些模块可以很方便的协助我们开发。
核心思想:不重新造轮子,开箱即用。
# 2. Spring Boot、SpringMVC 和 Spring 的区别?
Spring:是一个 IOC 容器,用来创建、管理 Bean 对象,使用依赖注入实现控制反转,可以方便的整合框架;同时,提供给 AOP 弥补 OOP 代码重复问题,可以将不同类不同方法中的公共处理抽取成切面,使用 AOP 对已有代码进行增强。如,日志统计、异常处理、事务等。
SpringMVC:是 Spring 对 Web 框架的一个解决方案,提供了一个总的前端控制器 Servlet,用来接收请求,然后定义了一套路由策略(url 到 handle 的映射)及适配执行 handle,将 handle 结果使用视图解析技术生成视图返回给前端。
SpringBoot:是 Spring 提供一个快速开发工具包,能让程序员更方便、更快速的开发基于 Spring + SpringMVC 应用,简化了配置,整合了一系列的解决方案(starter 机制)。
# 3. 什么是 bean?
简单来说,bean代指的就是那些被IoC容器所管理的对象。我们需要告诉IoC容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是XML文件、注解或Java类。
<!-- Constructor-arg with 'value' attribute -->
<bean id="..." class="...">
<constructor-arg value="..."/>
</bean>
2
3
4
# 4. bean 的作用域有哪些?
- singleton:唯一bean实例,默认是单例的。
- prototype:每次请求都会创建一个新的bean实例。
- request:每次HTTP请求都会创建一个新的bean实例,该bean仅在当前HTTP request内有效。
- session:每一次来自新session的HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。
- global-session:全局session作用域,
# 5. 单例 bean 的线程安全了解吗?
Spring 本身没有针对 Bean 做线程安全处理,所以:
- 如果 Bean 是无状态的,那么 Bean 则是线程安全的
- 如果 Bean 是有状态,那么 Bean 则不是线程安全的
另外,Bean 是不是线程安全,跟 Bean 的作用域没有关系,Bean 的作用域只是表示 Bean 的生命周期范围,对于任何生命周期的 Bean 都是一个对象,这个对象是不是线程安全的,还是得看这个 Bean 对象本身。
线程安全问题的解决办法:
- 在bean中尽量避免定义可变的成员变量。
- 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中。
# 6. @Component 和 @Bean 的区别是什么?
- @Component 注解作用于类,而 @Bean 作用于方法。
- @Component 通常是通过类路径扫描来自动侦测及自动装配到 Spring 容器。(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出表示了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean注解通常是我们在标有该注解的方法中定义产生整个bean,@Bean告诉了Spring这是某个类的实例,当我们需要用它的时候还给我。
- @Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring 容器时,则只能通过 @Bean 来实现。
# 7. @Autowired 与 @Resource 的区别是什么?
- @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
- @Autowired 只能按类型注入,如果存在多个 Bean 时需要使用@Qualifier 指明 Bean 的名称;
- @Resource 默认按名称注入,当找不到对应名称时按照类型注入。
# 8. 将一个类声明为 bean 的注解有哪些?
一般使用 @Autowired 注解自动装配 bean,要想把类表示成可用于 @Autowired 注解自动装配的 bean 的类,需用一下注解:
- @Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component注解标注。
- @Repository:对应持久层的Dao层,数据库相关操作。
- @Service:对应服务层,主要设计一些复杂的逻辑,需要用到Dao层。
- @Controller:对应Spring MVC控制层,接受用户请求并调用Service层返回数据给前端。
- @Bean:通常作用在方法上,该方法会返回一个创建好的对象。
# 9. 说一说对 Spring 容器的了解?
主要包含两种类型的容器:BeanFactory 和 ApplicationContext
- BeanFactory:基础的IoC容器,默认采用延迟初始化策略。只有当客户端对象需要访问容器中的某个受管理对象啊的时候,才对该受管对象进行初始化以及依赖注入的操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,且功能要求不是很严格的场景,BeanFactory是比较合适的IoC选择。
- ApplicationContext:在BeanFactory基础上构建,ApplicationText所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,启动时间会慢一些。在那些系统资源充足,并且要求更多的功能的场景中,ApplicationContext类型的容器是比较合适的选择。
# 10. ApplicationContext 与 BeanFacatory 区别
ApplicationContext 继承 BeanFactory
BeanFactory 是 Spring 中非常核心的组件,表示 Bean 工厂,可以生成 Bean,维护 Bean。
ApplicationContext 继承了 BeanFactory,所以 ApplicationContext 拥有 BeanFactory 所有的特点,也是一个 Bean 工厂,但是 ApplicationContext 除继承了 BeanFactory 外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher 等接口,从而 ApplicationContext 还要获取系统环境变量、国际化、事件发表等功能,这是 BeanFactory 所不具备的。
# 11. Spring 中的设计模式——单例模式
在系统中,有一些对象其实我们只需要一个,比如:线程池、缓冲池、日志对象、驱动等对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序行为异常、资源使用过量、不一致的结果。
使用单例的好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔开销;
- 由于new操作的次数减少,因而对系统内存的使用率也会降低,减轻GC压力,缩短GC停顿时间。
# 12. Spring 事务的传播机制?
- propagation_required:如果外部没有事务,就开启一个事务;如果外部存在一个事务,就加入到该事务中。适用于增删改。(常用)
- propagation_supports:如果外部事务不存在,则不使用事务;如果外部存在一个事务,就加入到该事务中。适用于查询方法。(常用)
- propagation_mandatory:如果外部事务不存在,抛出异常;如果外部存在一个事务,就加入到该事务中。
- propagation_required_new:如果外部没有事务,就开启一个事务;如果外部存在一个事务,挂起外部事物,创建新的事物。
- propagation_not_supported:如果外部没有事务,不开启事务;如果外部存在一个事务,挂起外部事物。
- propagation_never:如果外部事务不存在,则不使用事务;如果外部存在一个事务,则抛出异常。
- propagation_nested:嵌套事务,如果当前事务存在,则嵌套在事务中执行。如果当前事务不存在,则创建一个新事物。如果嵌套事务发送回滚,只回滚嵌套部分的事务。
# 13. @Transactional 注解执行的流程
- 事务管理器新建一个数据库连接 conn,保证方法内的所有操作都使用同一个数据库连接。
- 关闭自动提交:conn.autoCommit = false
- 获取第一步事务管理器创建的链接(是一个 ThreadLocal 对象,如果获取不到的话就自己创建,那么事务就失效了),执行一系列方法 sql
- 未抛异常,conn.commit()
- 抛出异常,conn.rollback()
# 14. Spring 事务什么时候会失效?
Spring事务的原理是 AOP,进行切面增强,那么事务失效的根本原因是 AOP 失效,常见的情况有:
- 方法不是 public 的,@Transactional 只能用于 public 方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式
- 数据库不支持事务
- 没有被 Spring 管理
- 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为 RuntimeException)
- 方法互相调用时注解失效,需要分析是普通方法还是代理方法的调用,只要代理对象调用其他方法时注解才会生效。
下面详细解释一下第五点,以下面这行代码为案例
Class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void test() {
jdbcTemplate.excute("xxxxx1");
a();
}
@Transactional(propagation = Propagation.NEVER)
public void a() {
jdbcTemplate.excute("xxxxx2");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test()
方法开启事务,执行 sql 语句,再调用 a()
方法,该方法也在这个事务当中。需要注意的是,a()
方法也开启了事务,并设置传播机制为 NEVER,表示如果外部事务不存在,则不使用事务;如果外部存在一个事务,则抛出异常。
错误理解:因为带事务的方法 test() 调用方法 a(),由于 a()
的传播机制为 NEVER,所以会抛出异常。
正确理解:首先我们要知道,Spring 内部执行的增强的 test()
方法其实是调用被代理对象的 test()
,被代理对象 test()
方法调用的 a()
方法是普通的方法,是没有经过 AOP 增强的方法,不存在事务,不会抛出异常。
// 模拟代理类
public void test() {
// 事务管理器新建一个数据库连接
// conn.autocommit = false
// target.test(); // 普通对象.test()
// conn.commit() 或者 conn.rollback()
}
2
3
4
5
6
7
那么如何修改呢?
方法1:把两个方法分到两个文件,通过 @Autowired 注入再调用,此时 test()
方法调用的 a()
方法就是动态代理生成的方法。
方法2:自己注入自己,通过 @Autowired 注入自己 UserService,调用 userservice.a()
# 15. Spring 中的设计模式有哪些?
参考:Spring中用到了哪些设计模式 (opens new window)
主要有:工厂模式、单例模式、代理模式、模板方法、观察者模式、适配器模式
工厂模式
Spring 可以通过 BeanFactory 和 Application 创建 bean 对象。
单例模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
使用单例模式的好处
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
代理模式
AOP 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
AOP 就是基于动态代理的,如果要代理的对象实现了某个接口,那么会使用 JDK Proxy 去创建代理对象,而对于没有实现接口的对象,会使用Cglib生成一个被代理对象的子类来作为代理。
模板方法
模板方法是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。
Spring 中 jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
观察者模式
观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。
适配器模式
适配器模式将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
spring AOP中的适配器模式:
我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter
。Advice 常用的类型有:BeforeAdvice
、AfterAdvice
、AfterReturningAdvice
等等。每个类型Advice(通知)都有对应的拦截器: MethodBeforeAdviceInterceptor
、AfterReturningAdviceAdapter
、AfterReturningAdviceInterceptor
。Spring 预定义的通知要通过对应的适配器,适配成 MethodInterceptor
接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor
负责适配 MethodBeforeAdvice
)。
spring MVC中的适配器模式:
在Spring MVC中,DispatcherServlet
根据请求信息调用 HandlerMapping
,解析请求对应的 Handler
。解析到对应的 Handler
(也就是我们平常说的 Controller
控制器)后,开始由 HandlerAdapter
适配器处理。HandlerAdapter
作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller
作为需要适配的类。
草稿
# Aware的应用
工具类,用于获取Bean对象。 为什么不使用注解来注入Bean对象?
- 需要在静态方法中获取到 spring 管理的某个 bean
- 策略模式,枚举类中定义好策略以及对应的实现类,根据类名获取Bean对象进行后续操作
@Component
public class SpringUtil implements ApplicationContextAware {
// 应用上下文环境
private static ApplicationContext context;
@SuppressWarnings("static-access")
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
/**
* 根据bean name 获取对象
*
* @param name
* @return
*/
public static <T> T getBean(String name) {
return (T)context.getBean(name);
}
/**
* 根据Class获取对象
*
* @param clazz
* @param <T>
* @return
* @throws BeansException
*/
public static <T> T getBean(Class<T> clazz) throws BeansException {
return context.getBean(clazz);
}
}
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
# BeanFactoryPostProcess 与
BeanFactoryPostProcess
BeanFactoryPostProcess 可以对 beanDefination 进行修改,比如里面的 property 用的占位符${xxx.xxx}$
,需要执行 invokeBeanFactoryPostProcessors(beanFactory)
来将占位符 ${xxx.xxx}$
换成需要的值。
只要实现 BeanFactoryPostProcess 接口并重写其中的方法,就可以通过 beanFactory 对象对其中的 BeanDefinition 进行修改。
# Spring 如何创建一个 Bean 对象的?
通过 new UserService()
创建的对象和 Spring 容器帮我们创建的 Bean 对象是不一样的,少了依赖注入、属性填充等步骤。依赖注入,指需要给加了 @Autowire 或 @Resource 注解的对象注入到对象中。
也就是说,Spring 在创建 Bean 对象后,再去看看这个对象内部需要哪些对象,并创建内部依赖的对象,并把依赖的对象注入进去。
容器底层就是一个 Map,Key 为 BeanName,Value 为 Bean 对象。单例 Bean 是放在一个单例池中。
依赖注入后,执行初始化前的任务、初始化过程(实现 InitializingBean 接口,实现 afterPropertiesSet 方法)、舒适化后的任务
Bean 实例化:通过构造器得到一个对象
Bean 初始化:对已经实例化的对象,执行初始化方法(自定义的方法)
UserService 类 ---> 推断无参构造器 ---> 对象 ---> 依赖注入 ---> 初始化(@PostConstruct)---> 初始化(InitializingBean)---> 初始化后(AOP)---> 代理对象 ---> 放入单例池 ---> Bean 对象
推断构造方法:Spring 根据类已有的构造方法进行选择,如果有 @Autowired 指定的就用指定的,如果没指定则用无参构造方法。如果有多个有参构造方法,且没有无参构造方法则会报错。当选择好构造方法后,如果构造方法有入参,则会从容器中获取,获取方式先ByType 再 ByName
依赖注入:当对象实例化完成,Spring 会看对象内哪些属性加了 @Autowired 注解,对其进行赋值,先 ByTpe 再 ByName。
当使用 AOP 后,单例池中存放的是代理对象,注意,代理对象后面没有依赖注入了。
OrderService 是一个单例 Bean,那么在 Spring 容器中只能有一个 OrderService 类型的 Bean。这是错的。
当使用 AOP 对被代理类进行增强时生成了代理类对象 UserServiceProxy。普通对象 UserService 里面包含对象 OrderService,Spring 容器会自动将 OrderService 注入到 UserService 内。但 UserServiceProxy 对象内部的 OrderService = null,因为前面说过了,AOP 之后不会再依赖注入了,但使用 UserServiceProxy 的时候依然能够使用 OrderService 里面的方法,因为 UserServiceProxy 里面包含了被代理对象 UserService,而 UserService 对象内部已经注入了 OrderService。代理类增强代理类的方法:
public void test() {
// 前面方法1
target.test(); // 执行被代理类的方法
// 切面方法2
}
2
3
4
5
# Spring 中 Bean 创建的生命周期有哪些步骤?
Spring 创建 BeanDefination 步骤:
- 通过 xml、注解读取 BeanDefination 定义信息
- 通过 BeanFactoryPostProcess 可以对 BeanDefination 进行修改
Spring 中一个 Bean 的创建大概分为以下几个步骤:
- 推断构造方法:使用哪个构造器
- 实例化
- 填充属性,也就是依赖注入
- 处理 Aware 回调,给自定义对象赋值 Spring 的容器
- 初始化前,applyBeanPostProcessorsBeforeInitialization
- 初始化,处理 InitalizingBean 接口
- 初始化后,applyBeanPostProcessorsAfterInitialization