- Spring 面试题
- 前言
- 1 Spring简介
- 1.1 Spring是什么
- 1.2 Spring优势
- 2 IOC
- 2.1 常用的依赖注入方式
- 2.1.1 字段注入(@Autowired)IDEA、Spring官方不推荐使用
- 2.1.2 构造器依赖注入(Spring官方推荐)
- 2.1.3 Setter方法注入
- 循环依赖的解决
- 一级缓存、二级缓存、三级缓存是什么,一级二级能不能解决循环依赖
- 为什么用三级缓存解决循环依赖
- 2.2 IOC容器的加载过程
- 2.2.1 资源文件定位
- 2.2.2 解析
- 2.2.3 注册
- 2.2.4 实例化
- 3 Spring Bean
- 3.1 Spring怎么管理Bean的
- 3.2 Bean的生命周期
- 3.3 Spring 中的 bean 的作用域有哪些?
- 3.4 Spring中的Bean是否线程安全
- 3.5 Spring中出现同名bean怎么办?
- 3.6 Spring依赖注入的配置方式
- 3.6.1 注解配置
- 3.6.2 延迟加载 @Lazy(Spring 默认使用预加载)
- 4 AOP
- 4.1 AOP的底层实现
- 常用的动态代理技术
- 4.1.1 JDK动态代理的实现
- 1. java.lang.reflect 包中的 InvocationHandler 接⼝:
- 2. java.lang.reflect 包中的 Proxy 类中的 newProxyInstance ⽅法:
- JDK动态代理要为什么实现接口
- 4.2 AOP的基本概念
- 4.3 通知类型
- 5 Spring 注解
- 5.1 @Component、@Repository、@Service、@Controller区别
- 5.2 @Autowired 与@Resource的区别:
- 5.3 @Controller 和 @RestController
- 6 其他
- 6.1 Spring使用到的设计模式
- 6.2 BeanFactory与ApplicationContext的区别是什么?
前言 本科菜鸡经历了秋招的摧残,发现了自己在很多地方的不足,打算写博客总结一下自己学习的资料,也想为春招加点分,同时也希望能给大家带来一些帮助!!! 如有错误和不足,欢迎各位指出。
1 Spring简介 1.1 Spring是什么
Spring是分层的Java SE/EE应用full-stack轻量级开源框架,以IOC和AOP为内核。
1.2 Spring优势- 方便解耦,简化开发
- 支持AOP编程
- 支持声明式事务
- 方便测试
- 方便集成各种框架
IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
IOC主要通过依赖注入来实现(DI)
2.1 常用的依赖注入方式-
字段注入
-
构造器依赖注入
-
setter 方法注入
基于BeanFactory接口实现(ApplicationContext)
2.1.1 字段注入(@Autowired)IDEA、Spring官方不推荐使用为什么不推荐:
- 最大问题——对象的外部可见性(类和容器的耦合度过高,无法脱离容器来使用目标对象,只能用反射的方式获取,不符合JavaBean的规范)
- 可能导致潜在的循环依赖(两个类互相注入,只有在使用到A、B的时候会报错)
- 无法设置需要注入对象为final,也无法注入那些不可变的对象(类必须在类实例化时进行实例化)
通过类的构造函数来完成
- 可以解决外部对象的可见性问题
- Spring官网有这样一段话:构造器注入能够保证注入的组件不可变(可以使用final修饰对象),并且确保需要的依赖不为空(避免出翔空指针异常)。
- 所以构造器注入可以解决字段注入的三大问题
缺点:
- 当构造函数中存在较多依赖对象的时候,大量的构造器参数会让代码非常的冗长
- 比构造器注入更具有可读性,可以把多个依赖对象用setter方法逐个注入
- 可以选择性的注入想要注入的依赖对象,可以实现按需注入
- 可以解决循环依赖的问题(作用域必须为单例模式)
单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
一级缓存、二级缓存、三级缓存是什么,一级二级能不能解决循环依赖Spring中的一级缓存名为singletonObjects,二级缓存名为earlySingletonObjects,三级缓存名为singletonFactories,除了一级缓存是ConcurrentHashMap之外,二级缓存和三级缓存都是HashMap。它们的定义是在DefaultSingletonBeanRegistry类中。
为什么用三级缓存解决循环依赖缓存解决循环依赖的思路有两种:
第一种:不管有没有循环依赖,都提前创建好代理对象,并将代理对象放入缓存。出现循环依赖时,其它对象直接就可以取到代理对象并注入,两个级别的缓存就够了。
第二种:不提前创建好代理对象,在出现循环依赖被其它对象注入时,才实时生成代理对象。
Spring选择了后一种,**如果使用两个缓存解决循环依赖,意味着Bean在构造完成后就创建代理对象,这样就违背了Spring的设计原则。**Spring结合AOP和Bean的生命周期,是在Bean创建完成之后通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理器的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。使用第一种方案如果出现了循环依赖,那没有办法,只有先给Bean先创建代理,但是在Spring设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化之后就立马完成代理。
2.2 IOC容器的加载过程 2.2.1 资源文件定位其中资源文件定位,一般是在ApplicationContext的实现类里完成的,因为ApplicationContext接口继承ResourcePatternResolver 接口,ResourcePatternResolver接口继承ResourceLoader接口,ResourceLoader其中的getResource()方法,可以将外部的资源,读取为Resource类。
2.2.2 解析解析 DefaultBeanDefinitiondocumentReader
解析主要是在BeanDefinitionReader中完成的。
最常用的实现类是XmlBeanDefinitionReader,其中的loadBeanDefinitions()方法,负责读取Resource,并完成后续的步骤。
ApplicationContext完成资源文件定位之后,是将解析工作委托给XmlBeanDefinitionReader来完成的解析这里涉及到很多步骤,最常见的情况,资源文件来自一个XML配置文件。首先是BeanDefinitionReader,将XML文件读取成w3c的document文档。
DefaultBeanDefinitiondocumentReader对document进行进一步解析。
然后DefaultBeanDefinitiondocumentReader又委托给BeanDefinitionParserDelegate进行解析。如果是标准的xml namespace元素,会在Delegate内部完成解析,如果是非标准的xml namespace元素,则会委托合适的NamespaceHandler进行解析最终解析的结果都封装为BeanDefinitionHolder,至此解析就算完成。
2.2.3 注册bean的注册是在BeanFactory里完成的,BeanFactory接口最常见的一个实现类是DefaultListableBeanFactory,它实现了BeanDefinitionRegistry接口,所以其中的registerBeanDefinition()方法,可以对BeanDefinition进行注册这里附带一提,最常见的XmlWebApplicationContext不是自己持有BeanDefinition的,它继承自AbstractRefreshableApplicationContext,其持有一个DefaultListableBeanFactory的字段,就是用它来保存BeanDefinition所谓的注册,其实就是将BeanDefinition的name和实例,保存到一个Map中。
最常用的实现DefaultListableBeanFactory,其中的字段就是beanDefinitionMap,是一个ConcurrentHashMap。
2.2.4 实例化注册也完成之后,在BeanFactory的getBean()方法之中,会完成初始化,也就是依赖注入的过程大体上的流程就是这样
3 Spring Bean 3.1 Spring怎么管理Bean的以Application Context为例
该类中拥有一个List泛型集合类以及一个Map
a.读取配置文件bean.xml,并根据文件中bean的id,class属性实例化一个BeanDefinition,装入泛型集合中。
b.通过循环+反射,将List中的bean加入到Map
c.提供一个对外的接口,通过传入参数获取该bean。
它在初始化的时候将配置文件中bean以及相对应关系的配置都加入到ApplicationContext,通过一系列的转换将这些bean实例化,bean被它进行了管理,所以ApplicationContext就扮演了一个容器的角色。
3.2 Bean的生命周期主要把握创建过程和销毁过程这两个大的方面;
- 创建过程:
- 首先实例化Bean,并设置Bean的属性,根据其实现的Aware接口(主要是BeanFactoryAware接口,ApplicationContextAware)设置依赖信息
- 接下来调用BeanPostProcess的postProcessBeforeInitialization方法,完成initial前的自定义逻辑;afterPropertiesSet方法做一些属性被设定后的自定义的事情;
- 调用Bean自身定义的init方法,去做一些初始化相关的工作;然后再调用postProcessAfterInitialization去做一些bean初始化之后的自定义工作。
- 此时,Bean初始化完成,可以使用这个Bean了。
- 销毁过程:如果实现了DisposableBean的destroy方法,则调用它,如果实现了自定义的销毁方法,则调用之。
默认是单例,如果要实现多例,可以有两种方式:
-
要实现多例的类加上注解@Scope(“prototype”)
-
在配置文件中配置
-
singleton(单例) : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
- 无状态的Bean使用(只有普通的对数据的操作方法,而没有存储功能,如userDao)
-
prototype (原型): 每次请求都会创建一个新的 bean 实例。
- 有状态的Bean使用(具有数据存储功能,如User)
- 创建过程中会对性能产生影响,因为每次请求都会创建Bean,对那些初始化性能需要消耗巨大资源的对象不推荐使用,如:网络连接、数据库连接。
web开发提供:
- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
- session : :在一个HTTP Session中,一个Bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
- global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。
单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。
如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
有状态Bean:如果有实例变量的对象,可以保存数据,是非线程安全的。
对于有状态Bean,Spring官方提供了Thread Local解决线程安全。
3.5 Spring中出现同名bean怎么办?- 同一个配置文件内同名的Bean,以最上面定义的为准
- 不同配置文件中存在同名Bean,后解析的配置文件会覆盖先解析的配置文件
- 同文件中ComponentScan和@Bean出现同名Bean。同文件下@Bean的会生效,@ComponentScan扫描进来不会生效。通过@ComponentScan扫描进来的优先级是最低的,原因就是它扫描进来的Bean定义是最先被注册的~
- XML
- Java代码
- 注解配置(最主要)
- @Autowired
- @Primary
- 管理针对某种类型的多个实现类
- @Qualifier
- 可以通过设置不同的名称获取不同的实例
只有在使用到Bean的时候才会去初始化,而不是SpringIOC启动时
- 预加载:需要应用程序尽可能块的运行,并更快的为请求提供服务
- 延时加载:需要尽可能块的加载应用程序
AOP(Aspect-Oriented Programming:面向切面编程)
是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP是OOP的延续,利用AOP可以对业务逻辑的各个部分进行隔离,从而降低耦合度,提高程序的可重用性和开发效率。
4.1 AOP的底层实现AOP底层是通过Spring提供的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。
常用的动态代理技术- JDK动态代理:基于接口的动态代理技术
- 通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。
- cglib动态代理:基于父类的动态代理技术
- 如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类 。CGLIB ( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final ,那么它是无法使用 CGLIB 做动态代理的。
JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 。
1. java.lang.reflect 包中的 InvocationHandler 接⼝:public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable;
}
对于被代理的类的操作都会由该接⼝中的 invoke ⽅法实现,其中的参数的含义分别是:
- proxy:被代理的类的实例;
- method:调⽤被代理的类的⽅法;
- args:该⽅法需要的参数。
使⽤⽅法⾸先是需要实现该接⼝,并且我们可以在 invoke ⽅法中调⽤被代理类的⽅法并获得返回值,⾃然也可以 在调⽤该⽅法的前后去做⼀些额外的事情,从⽽实现动态代理。
2. java.lang.reflect 包中的 Proxy 类中的 newProxyInstance ⽅法:public static Object newProxyInstance(ClassLoader loader, Class>[] interfaces, InvocationHandler h) throws IllegalArgumentException
其中的参数含义如下:
- loader:被代理的类的类加载器;
- interfaces:被代理类的接⼝数组;
- invocationHandler:调⽤处理器类的对象实例
该⽅法会返回⼀个被修改过的类的实例,从⽽可以⾃由的调⽤该实例的⽅法
JDK动态代理要为什么实现接口生成的代理类居然继承了Proxy,我们知道java是单继承的,所以JDK动态代理只能代理接口
4.2 AOP的基本概念- 切⾯(Aspect):官⽅的抽象定义为“⼀个关注点的模块化,这个关注点可能会横切多个对象”。
- 连接点(Joinpoint):程序执⾏过程中的某⼀⾏为。
- 通知(Advice):“切⾯”对于某个“连接点”所产⽣的动作。
- 切⼊点(Pointcut):匹配连接点的断⾔,在 AOP 中通知和⼀个切⼊点表达式关联。
- ⽬标对象(Target Object):被⼀个或者多个切⾯所通知的对象。
- AOP 代理(AOP Proxy):在 Spring AOP 中有两种代理⽅式,JDK 动态代理和 CGLIB 代理。
- 前置通知
- 后置通知
- 返回后通知
- 环绕通知
- 抛出异常通知
- @Service用于标注业务层组件
- @Controller用于标注控制层组件(如struts中的action)
- @Repository用于标注数据访问组件,即DAO组件
- @Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
1、 @Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上。
2、 @Autowired默认按类型装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用,
3、@Resource(这个注解属于J2EE的),默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
推荐使用:@Resource注解在字段上,这样就不用写setter方法了,并且这个注解是属于J2EE的,减少了与spring的耦合。这样代码看起就比较优雅。
5.3 @Controller 和 @RestController@RestController注解相当于@ResponseBody + @Controller合在一起的作用。
如果使用@RestController注解Controller,则Controller中的方法无法返回jsp页面,或html,配置的视图解析器 InternalResourceViewResolver不起作用,返回的内容就是Return 里的内容。
如果需要返回到指定页面,则需要用 @Controller配合视图解析器InternalResourceViewResolver才行。
6 其他 6.1 Spring使用到的设计模式工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
: Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
6.2 BeanFactory与ApplicationContext的区别是什么?BeanFactory采用了工厂设计模式,负责读取bean配置文档,管理bean的加载,实例化,维护bean之间的依赖关系,负责bean的声明周期。而ApplicationContext除了提供上述BeanFactory所能提供的功能之外,还提供了更完整的框架功能:国际化支持、aop、事务等。同时BeanFactory在解析配置文件时并不会初始化对象,只有在使用对象getBean()才会对该对象进行初始化,而ApplicationContext在解析配置文件时对配置文件中的所有对象都初始化了,getBean()方法只是获取对象的过程。
因此我们一般在使用的时候尽量使用ApplicationContext。



