Java面试——Spring

Java面试——Spring

一、Spring Bean 作用域


【1】singleton:该属性在 IOC容器仅创建一个 Bean实例(单例),IOC容器每次返回的是同一个 Bean实例。
【2】prototype:该属性在 IOC容器可以创建多个 Bean实例,每次返回的都是一个新的实例。
【3】request:该属性仅对 HTTP请求产生作用,HTTP请求每次都会创建一个新的Bean,适用于 WebApplicationContext 环境。
【4】session:该属性仅用于 HTTP Session,同一个 Session共享一个 Bean实例。不同 Session使用不同的实例。
【5】global-session:该属性仅用于HTTP Session,同 session作用域不同的是,所有的 Session共享一个 Bean实例

二、BeanFactory 和 FactoryBean


【1】BeanFactory:Spring IoC容器设计中,我们可以看到两个主要的容器系列,其中一个就是实现 BeanFactory接口的简单容器系列,这系列容器只实现了容器的基本功能;该接口是 IoC容器的顶级接口,是 IoC容器的最基础实现,也是访问 Spring容器的根接口,负责对 Bean的创建,访问等工作。最典型的容器就是 DefaultListableBeanFactory 。
【2】FactoryBean:是一种工厂 bean,可以返回 bean的实例,可以通过实现该接口对 Bean进行额外的操作,例如根据不同的配置类型返回不同类型的Bean,简化 xml配置等;其在使用上也有些特殊,大家还记得 BeanFactory中有一个字符常量String FACTORY_BEAN_PREFIX = “&”; 当我们去获取 BeanFactory类型的 bean时,如果 beanName不加&则获取到对应 bean的实例;如果 beanName 加上 &,则获取到 BeanFactory本身的实例;FactoryBean 接口对应 Spring框架来说占有重要的地位,Spring 本身就提供了70多个 FactoryBean的实现。他们隐藏了实例化一些复杂的细节,给上层应用带来了便利。从Spring3.0 开始,FactoryBean 开始支持泛型。

三、BeanFactory 和 ApplicationContext


img

【1】BeanFacotry 接口提供了使用 IoC容器的规范,提供了最简单的容器的功能,只提供了实例化对象功能。原始的 BeanFactory无法支持 Spring 的许多插件,如 AOP功能、Web应用等。ApplicationContext 接口,它由 BeanFactory接口派生而来,因而提供 BeanFactory所有的功能。ApplicationContext 以一种面向框架的方式工作以及对上下文进行分层和实现继承,ApplicationContext 包还提供了以下的功能: ① MessageSource, 提供国际化的消息访问;② 访问资源,如 URL和文件(ResourceLoader);③ 事件传播 ④载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的 web层;
【2】BeanFactroy 采用的是延迟加载形式来注入 Bean的(懒加载),即只有在使用到某个 Bean时(调用getBean()),才对该 Bean进行加载实例化,这样,我们就不能发现一些存在的 Spring的配置问题。而 ApplicationContext 则相反,它是在容器启动时,一次性创建了所有的单例Bean。这样,在容器启动时,我们就可以发现 Spring中存在的配置错误。
【3】BeanFactoryApplicationContext 都支持 BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory 需要手动注册,而 ApplicationContext则是自动注册。

1 BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
2 IHelloService helloService = (IHelloService) beanFactory.getBean("helloService");
3 helloService.sayHello();

四、Spring Bean 的生命周期,如何被管理的


对于普通的 Java对象,当 new的时候创建对象,当它没有任何引用的时候被垃圾回收机制回收。而由 Spring IoC容器托管的对象,它们的生命周期完全由容器控制。Spring 中每个 Bean的生命周期如下:
img

主要对几个重要的步骤进行说明:
【1】实例化 Bean:对于 BeanFactory容器,当客户向容器请求一个尚未初始化的 bean时,或初始化 bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用 createBean进行实例化。对于 ApplicationContext容器,当容器启动结束后,便实例化所有的单实例 bean。容器通过获取 BeanDefinition对象中的信息进行实例化。并且这一步仅仅是简单的实例化,并未进行依赖注入。实例化对象被包装在 BeanWrapper 对象中,BeanWrapper 提供了设置对象属性的接口,从而避免了使用反射机制设置属性。通过工厂方法或者执行构造器解析执行即可:创建的对象是个空对象。
【2】设置对象属性(依赖注入):实例化后的对象被封装在 BeanWrapper对象中,并且此时对象仍然是一个原生的状态,并没有进行依赖注入。紧接着获取所有的属性信息通过 populateBean(beanName,mbd,bw,pvs),Spring 根据 BeanDefinition 中的信息进行依赖注入。并且通过 BeanWrapper提供的设置属性的接口完成依赖注入。赋值之前获取所有的 InstantiationAwareBeanPostProcessor 后置处理器的 postProcessAfterInstantiation() 第二次获取InstantiationAwareBeanPostProcessor 后置处理器;执行 postProcessPropertyValues()最后为应用 Bean属性赋值:为属性利用 setter 方法进行赋值 applyPropertyValues(beanName,mbd,bw,pvs)。
【3】bean 初始化:initializeBean(beanName,bean,mbd)。
1)执行xxxAware 接口的方法,调用实现了BeanNameAware、BeanClassLoaderAware、BeanFactoryAware接口的方法。
2)执行后置处理器之前的方法:applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName)所有后置处理器的 BeanPostProcessor.postProcessBeforeInitialization()。
3)执行初始化方法: InitializingBean 与 init-methodinvoke 当 BeanPostProcessor的前置处理完成后就会进入本阶段。先判断是否实现了 InitializingBean接口的实现;执行接口规定的初始化。其次自定义初始化方法。
InitializingBean 接口只有一个函数**:**afterPropertiesSet()这一阶段也可以在 bean正式构造完成前增加我们自定义的逻辑,但它与前置处理不同,由于该函数并不会把当前 bean对象传进来,因此在这一步没办法处理对象本身,只能增加一些额外的逻辑。若要使用它,我们需要让 bean实现该接口,并把要增加的逻辑写在该函数中。然后 Spring会在前置处理完成后检测当前 bean是否实现了该接口,并执行 afterPropertiesSet函数。当然,Spring 为了降低对客户代码的侵入性,给 bean的配置提供了 init-method属性,该属性指定了在这一阶段需要执行的函数名。Spring 便会在初始化阶段执行我们设置的函数。init-method 本质上仍然使用了InitializingBean接口。
4)applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);执行初始化之后的后置处理器的方法。BeanPostProcessor.postProcessAfterInitialization(result, beanName);
【4】Bean的销毁:DisposableBean 和 destroy-method:和 init-method 一样,通过给 destroy-method 指定函数,就可以在bean 销毁前执行指定的逻辑。

Bean 的管理就是通过 IOC容器中的 BeanDefinition信息进行管理的。

五、Spring Bean 的加载和获取过程


Bean 的加载过程,主要是对配置文件的解析,并注册 bean 的过程 。

【1】根据注解或者 XML中 定义 Bean 的基本信息。例如:spring-core.xml

<bean id="myBean" class="com.taobao.pojo"></bean>

【2】获取配置文件:这里使用最原始的方式获取。

Resource resource = new ClassPathResource("spring-core.xml")

【3】 利用 XmlBeanFactory 解析并注册 bean 定义:已经完成将配置文件包装成了 Spring 定义的资源,并触发解析和注册。XmlBeanFactory 实际上是对 DefaultListableBeanFactory(非常核心的类,它包含了基本 IOC 容器所具有的重要功能,是一个 IOC 容器的基本实现。然后是调用了this.reader.loadBeanDefinitions(resource),从这里开始加载配置文件) 和 XmlBeanDefinitionReader 组合使用方式的封装,所以这里我们仍然将继续分析基于 XmlBeanFactory 加载 bean 的过程。

XmlBeanFactory beanFactory = new XmlBeanFactory(resource);

Spring 使用了专门的资源加载器对资源进行加载,这里的 reader 就是 XmlBeanDefinitionReader对象,专门用来加载基于 XML 文件配置的 bean。这里的加载过程为:

①、利用 EncodedResource 二次包装资源文件;
②、获取资源输入流,并构造 InputSource 对象:

1 // 获取资源的输入流
2 InputStream inputStream = encodedResource.getResource().getInputStream();
3 // 构造InputSource对象
4 InputSource inputSource = new InputSource(inputStream);
5 // 真正开始从 XML文件中加载 Bean定义
6 return this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());

这里的 this.doLoadBeanDefinitions(inputSource, encodedResource.getResource()) 就是真正开始加载 XMl 的入口,该方法源码如下:第一步获取 org.w3c.dom.Document 对象,第二步由该对象解析得到 BeanDefinition 对象,并注册到 IOC 容器中。

1 protected intdoLoadBeanDefinitions(InputSource inputSource, Resource resource){
   
2   try {
   
3     // 1. 加载xml文件,获取到对应的Document(包含获取xml文件的实体解析器和验证模式)
4     Document doc = this.doLoadDocument(inputSource, resource);
5     // 2. 解析Document对象,并注册bean
6     return this.registerBeanDefinitions(doc, resource);
7   } 
8 }

③、获取 XML 文件的实体解析器和验证模式:this.doLoadDocument(inputSource, resource)包含了获取实体解析器、验证模式,以及 Document 对象的逻辑,XML 是半结构化数据,XML 的验证模式用于保证结构的正确性,常见的验证模式有 DTD 和 XSD 两种。
④、加载 XML 文件,获取对应的 Document 对象和验证模式与解析器,解析器就可以加载 Document 对象了,这里本质上调用的是 DefaultDocumentLoader 的 loadDocument() 方法,源码如下:整个过程类似于我们平常解析 XML 文件的流程。

1 public DocumentloadDocument(InputSource inputSource, EntityResolver entityResolver,
2                ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
   
3  
4     DocumentBuilderFactory factory = this.createDocumentBuilderFactory(validationMode, namespaceAware);
5     DocumentBuilder builder = this.createDocumentBuilder(factory, entityResolver, errorHandler);
6     return builder.parse(inputSource);
7 }

⑤、由 Document 对象解析并注册 bean:完成了对 XML 文件的到 Document 对象的解析,我们终于可以解析 Document 对象,并注册 bean 了,这一过程发生在 this.registerBeanDefinitions(doc, resource)中,源码如下:

 1 public intregisterBeanDefinitions(Document doc, Resource resource)throwsBeanDefinitionStoreException{
   
 2     // 使用DefaultBeanDefinitionDocumentReader构造
 3     BeanDefinitionDocumentReader documentReader = this.createBeanDefinitionDocumentReader();
 4  
 5     // 记录之前已经注册的BeanDefinition个数
 6     int countBefore = this.getRegistry().getBeanDefinitionCount();
 7  
 8     // 加载并注册bean
 9     documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
10  
11     // 返回本次加载的bean的数量
12     return getRegistry().getBeanDefinitionCount() - countBefore;
13 }

这里方法的作用是创建对应的 BeanDefinitionDocumentReader,并计算返回了过程中新注册的 bean 的数量,而具体的注册过程,则是由 BeanDefinitionDocumentReader 来完成的,具体的实现位于子类 DefaultBeanDefinitionDocumentReader 中:

1 publicvoidregisterBeanDefinitions(Document doc, XmlReaderContext readerContext){
   
2     this.readerContext = readerContext;
3   
4     // 获取文档的root结点
5     Element root = doc.getDocumentElement();
6  
7     this.doRegisterBeanDefinitions(root);
8 }

还是按照 Spring 命名习惯,doRegisterBeanDefinitions 才是真正干活的地方,这也是真正开始解析配置的核心所在:

 1 protectedvoiddoRegisterBeanDefinitions(Element root){
   
 2   BeanDefinitionParserDelegate parent = this.delegate;
 3   this.delegate = this.createDelegate(getReaderContext(), root, parent);
 4 
 5     // 处理profile标签(其作用类比pom.xml中的profile)
 6     String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
 7  
 8     // 解析预处理,留给子类实现
 9     this.preProcessXml(root);
10  
11     // 解析并注册BeanDefinition
12     this.parseBeanDefinitions(root, this.delegate);
13  
14     // 解析后处理,留给子类实现
15     this.postProcessXml(root);
16 }

方法在解析并注册 BeanDefinition 前后各设置一个模板方法,留给子类扩展实现,而在this.parseBeanDefinitions(root, this.delegate)中执行解析和注册逻辑:方法中判断当前标签是默认标签还是自定义标签,并按照不同的策略去解析。

 1 protectedvoidparseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate){
   
 2     if (delegate.isDefaultNamespace(root)) {
   
 3         // 解析默认标签
 4         NodeList nl = root.getChildNodes();
 5         for (int i = 0; i < nl.getLength(); i++) {
   
 6             Node node = nl.item(i);
 7             if (node instanceof Element) {
   
 8                 Element ele = (Element) node;
 9                 if (delegate.isDefaultNamespace(ele)) {
   
10                   // 解析默认标签
11                   this.parseDefaultElement(ele, delegate);
12                 } else {
   
13                   // 解析自定义标签
14                   delegate.parseCustomElement(ele);
15                 }
16             }
17         }
18       } else {
   
19         // 解析自定义标签
20         delegate.parseCustomElement(root);
21    }
22 }

到这里我们已经完成了静态配置到动态 BeanDefinition 的解析,这个时候 bean 的定义已经处于内存中。

【4】 从 IOC容器加载获取 bean:我们可以调用 beanFactory.getBean("myBean")方法来获取目标对象。

MyBean myBean = (MyBean) beanFactory.getBean("myBean");

六、如何实现一个 Spring AOP


AOP 源码】:链接

七、如果实现一个 Spring IOC


IOC 源码】:链接

八、Spring 是如何管理事务的,事务管理机制


事实上 Spring并不是直接管理事务的,只通过事务管理器来实现事务管理。

Spring 事务管理主要包括3个接口,Spring 的事务主要是由他们三个共同完成的,其中 PlatformTransactionManager是 Spring事务管理的核心接口!这三个事务管理器接口通过 getTransaction(TransactionDefinition definition)方法根据指定的传播行为返回当前活动的事务或创建一个新的事务,参数中定义一些基本的事务属性。这个方法里面的参数是TransactionDefinition类,这个类就定义了一些基本的事务属性。在 TransactionDefinition接口中定义了它自己的传播行为和隔离级别。
【1】第一个接口是 PlatformTransactionManager,是 Spring事务管理的核心接口。主要功能是事务管理器,是用于平台相关事务的管理,包括 commit 事务的提交;rollback 事务的回滚;getTransaction 事务状态的获取三种方法。
img
【2】第二个接口是 TransactionDefinition,主要功能是事务定义信息,是用来定义事务相关的属性,给事务管理器PlatformTransactionManager 使用的。而且在 TransactionDefinition 接口中定义了它自己的传播行为隔离级别。包括getIsolationLevel:获取隔离级别;getPropagationBehavior:获取传播行为;getTimeout:获取超时时间;isReadOnly:是否只读,四种方法。
img
【3】第三个接口是 TransactionStatus,主要功能是事务具体运行状态,是事务管理过程中,每个时间点事务的状态信息,它可以封装许多代码,节省我们的工作量。包括 hasSavepoint():返回这个事务内部是否包含一个保存点;isCompleted():返回该事务是否已完成,也就是说,是否已经提交或回滚;isNewTransaction():判断当前事务是否是一个新事, 这三种方法。
img

Spring 中配置事务管理器的两种方式链接

九、Spring 的不同事务传播行为/属性


事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

1 @Transactional(propagation = Propagation.REQUIRES_NEW)
2 public void methodB() {

【1】PROPAGATION_REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
【2】PROPAGATION_SUPPORTS:如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS 与不使用事务有少许不同。
【3】PROPAGATION_MANDATORY:如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
【4】PROPAGATION_REQUIRES_NEW:需要使用 JTATransactionManager 作为事务管理器,它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。
【5】PROPAGATION_NOT_SUPPORTED:总是非事务地执行,并挂起任何存在的事务。也需要使用JTATransactionManager作为事务管理器。
【6】PROPAGATION_NEVER:总是非事务地执行,如果存在一个活动事务,则抛出异常。
【7】PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务,则按REQUIRED 属性执行。

【PROPAGATION_NESTED 与 PROPAGATION_REQUIRES_NEW的区别】:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
● 使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
● 使用 PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用 savepoint支持 PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的 JTATrasactionManager实现可能有不同的支持方式。
● PROPAGATION_REQUIRES_NEW 启动一个新的,不依赖于环境的 “内部” 事务。这个事务将被完全 commited 或 rolled back 而不依赖于外部事务,它拥有自己的隔离范围,自己的锁等等。当内部事务开始执行时,外部事务将被挂起,内部事务结束 时,外部事务将继续执行。另一方面,PROPAGATION_NESTED 开始一个 “嵌套的” 事务,它是已经存在事务的一个真正的子事务。嵌套事务开始执行时,它将取得一个 savepoint。如果这个嵌套事务失败,我们将回滚到此 savepoint。嵌套事务是外部事务的一部分,只有外部事务结束后它才会被提交。
● 由此可见 PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于,PROPAGATION_REQUIRES_NEW 完全是一个新的事务,而 PROPAGATION_NESTED 则是外部事务的子事务,如果外部事务 commit,嵌套事务也会被 commit,这个规则同样适用于 rollback。

十、Spring 中用到了那些设计模式


【1】工厂设计模式:Spring 使用工厂模式通过 BeanFactoryApplicationContext 创建 bean 对象。
【2】代理设计模式:Spring AOP 功能的实现。
【3】单例设计模式:Spring 中的 Bean 默认都是单例的。
【4】模板方法模式:Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
【5】包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
【6】观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用。
【7】适配器模式:Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配。
【8】职责链模式:Spring AOP 中通过拦截器链将 Advice(通知器) 进行封装,与代理模式配合完成 AOP 功能。

十一、Spring MVC 的工作原理


主要从两点进行回答:ContextLoaderListener(Spring MVC 是建立在 IoC容器基础上的,ContextLoaderListener是个监听类,监听 Web 服务器的声明周期相关联的,负责完成 IoC容器在 Web 环境的启动工作),一般会加载整个 Spring容器相关的 Bean配置管理(如: Log, Service, Dao, PropertiesLoader, DataSource Bean, etc) 和 DispatcherServlet(请求分发作用,是很重要的类),一般会加载 MVC相关的 Bean配置管理(如: ViewResolver, Controller, MultipartResolver, ExceptionHandler, etc)。

web.xml 文件配置如下:配置了 ContextLoaderListenerDispatcherServlet

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 5 http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
 6         
 7      <context-param>
 8         <param-name>contextConfigLocation</param-name>
 9         <param-value>/WEB-INF/applicationContext.xml</param-value>
10         <!-- 默认是/WEB-INF/applicationContext.xml,设置根上下文配置文件的位置 -->
11      </context-param>
12      
13      <listener>
14         <listener-class>
15             org.springframework.web.context.ContextLoaderListener
16         </listener-class>
17      </listener>
18   
19     <servlet>
20         <servlet-name>SpringMVC</servlet-name>
21         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
22         <init-param>
23             <param-name>contextConfigLocation</param-name>
24             <param-value>/WEB-INF/SpringMVC-servlet.xml</param-value>
25             <!-- 默认是/WEB-INF/[servlet名字]-servlet.xml -->
26         </init-param>
27         <load-on-startup>1</load-on-startup>
28     </servlet>
29     
30     <servlet-mapping>
31         <servlet-name>SpringMVC</servlet-name>
32         <url-pattern>/</url-pattern>
33     </servlet-mapping>
34   
35 </web-app>

整体流程概述如下:
【1】用户发送请求至前端控制器 DispatcherServlet
【2】DispatcherServlet 收到请求调用 HandlerMapping处理器映射器
【3】处理器映射器根据请求 url找到具体的处理器,生成处理器对象处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
【4】DispatcherServlet通过 HandlerAdapter处理器适配器调用处理器;
【5】HandlerAdapter 执行处理器(handler,也叫后端控制器);
【6】Controller 执行完成返回 ModelAndView
【7】HandlerAdapter将 handler执行结果 ModelAndView返回给 DispatcherServlet
【8】DispatcherServlet将 ModelAndView传给 ViewReslover视图解析器
【9】ViewReslover解析后返回具体 View对象
【10】DispatcherServlet对 View进行渲染视图(即将模型数据填充至视图中)。
【11】DispatcherServlet 响应用户
【SpringBootMVC博客连接

十二、Spring 循环注入的原理


循环依赖就是循环引用,就是两个或多个Bean相互之间的持有对方,比如 A引用B,B引用C,C引用A,则它们最终反映为一个环。

【1】构造器循环依赖:Spring 容器将每一个正在创建的 Bean标识符放在一个“当前创建Bean池”中,Bean 标识符在创建过程中将一直保持在这个池中,因此在创建 Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlylnCreationException 异常表示循环依赖;对于创建完毕的 Bean将从“当前创建Bean池”中清除掉。
案例】:①、Spring容器创建 “A” Bean,首先去 “当前创建Bean池”查找是否当前 Bean正在创建,如果没发现,则继续准备其需要的构造器参数 “B”,并将 “A” 标识符放到“当前创建Bean池”;
②、Spring 容器创建“B” Bean,首先去“当前创建 Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“C”,并将“B” 标识符放到“当前创建Bean池”;
③、Spring 容器创建“C” Bean,首先去“当前创建 Bean池”查找是否当前 Bean正在创建,如果没发现,则继续准备其需要的构造器参数“A”,并将“C” 标识符放到“当前创建Bean池”;
④、到此为止 Spring容器要去创建“A”Bean,发现该 Bean 标识符在“当前创建Bean池”中,因为表示循环依赖,抛出BeanCurrentlyInCreationException;
【2】setter 循环依赖:表示通过setter注入方式构成的循环依赖;
案例】:①、Spring 容器创建单例 “A” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露创建中的Bean,并将“A” 标识符放到“当前创建Bean池”;然后进行setter注入“B”;
②、Spring容器创建单例“B” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory”用于返回一个提前暴露创建中的Bean,并将“B” 标识符放到“当前创建Bean池”,然后进行setter注入“C”;
③、Spring容器创建单例“C” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露创建中的Bean,并将“C” 标识符放到“当前创建Bean池”,然后进行setter注入“A”;进行注入“A”时由于提前暴露了“ObjectFactory”工厂从而使用它返回提前暴露一个创建中的Bean;最后在依赖注入“circleB”和“circleA”,完成setter注入。

对于 “prototype” 作用域Bean,Spring容器无法完成依赖注入,因为 “prototype” 作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。对于 “singleton” 作用域Bean,可以通过 “setAllowCircularReferences(false);”来禁用循环引用:
img

深入博客连接】:链接

十三、Spring AOP 的理解


面向切面编程(AOP)提供另外一种角度来思考程序结构,通过这种方式**弥补了面向对象编程****(OOP)**的不足,除了类(classes)以外,AOP提供了切面。切面对关注点进行模块化,例如横切多个类型和对象的事务管理。Spring 用代理类+拦截器链包裹切面,把它们织入到 Spring管理的 Bean中。也就是说代理类伪装成目标类,它会截取对目标类中方法的调用,让调用者对目标类的调用都先变成调用伪装类,伪装类中就先执行了切面,再把调用转发给真正的目标Bean。

【1】通知器(Advice)**:当我们完成对目标方法的切面增强设计(advice)和关注点的设计(pointcut)以后,需要一个对象把它们结合起来。主要包括:①、@Before:前置通知,方法执行之前执行;②、@After(finally):后置通知,方法执行之后执行,无论成功执行还是抛出异常。③、@AfterReturning:返回通知,方法成功执行之后执行,异常或者错误不执行。可以获取方法返回值。④、@AfterThrowing:**异常通知,方法抛出异常之后才执行,成功时不执行。**⑤、@Around:**环绕通知,包括以上四中注解!能决定目标方法是否执行和执行时间。

 1 @Around("point()")
 2 public void exec(ProceedingJoinPoint point) throws Throwable{
   
 3     System.out.println("...............Before(此处执行的代码相当于-前置通知)...............");
 4     try{
   
 5         point.proceed();//有此代码,被切入的方法体才会执行,如果被切入的方法有返回值,则返回值为null,见3
 6         System.out.println("..........AfterReturning(此处执行的代码相当于-返回通知)..........");
 7     }catch(Exception e){
   
 8         System.out.println("...........AfterThrowing(此处执行的代码相当于-异常通知)..........");
 9     }finally{
   
10         System.out.println("...........After1(此处执行的代码相当于-后置通知).............");
11     }
12     System.out.println("........After2(此处执行的代码相当于-后置通知).........");
13 }

【2】连接点JoinPointJoinPoint 对象封装了 SpringAop中切面方法的信息,在切面方法中添加 JoinPoint 参数,就可以获取到封装了该方法信息的 JoinPoint对象。ProceedingJoinPoint 对象:ProceedingJoinPoint 对象是 JoinPoint 的子接口,该对象只用在 @Around 的切面方法中,添加了 Object proceed()执行目标方法,例如上面的例子。作为方法的参数传入:

 1 /** 2 * 前置方法,在目标方法执行前执行 3 * @param joinPoint 封装了代理方法信息的对象,若用不到则可以忽略不写 4 */
 5 @Before("declareJoinPointerExpression()")
 6 public void beforeMethod(JoinPoint joinPoint){
   
 7     System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
 8     System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
 9     System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
10     System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
11     //获取传入目标方法的参数
12     Object[] args = joinPoint.getArgs();
13     for (int i = 0; i < args.length; i++) {
   
14         System.out.println("第" + (i+1) + "个参数为:" + args[i]);
15     }
16     System.out.println("被代理的对象:" + joinPoint.getTarget());
17     System.out.println("代理对象自己:" + joinPoint.getThis());
18 }

【3】切入点Pointcut上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切入点来筛选连接点,选中那几个你想要的方法。

1 /** 2 * 定义一个切入点表达式,用来确定哪些类需要代理 3 * execution(* aopdemo.*.*(..))代表aopdemo包下所有类的所有方法都会被代理 4 */
5 @Pointcut("execution(* aopdemo.*.*(..))")
6 public void declareJoinPointerExpression() {
   }

【4】切面Aspect切面是通知和切入点的结合。现在发现了吧,没连接点什么事情,连接点就是为了让你好理解切入点,搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过方法名中的before、after、around等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。上面的方法都在此类中定义:

1 @Aspect
2 @Component
3 public class aopAspect {
   

【5】引入introduction允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗。
【6】目标target引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。
【7】织入weaving织入是将增强添加到目标类具体连接点上的过程,AOP有三种织入方式:①编译时织入:需要特殊的Java编译器(例如AspectJ的ajc);②装载期织入:要求使用特殊的类加载器,在装载类的时候对类进行增强;③运行时织入:在运行时为目标类生成代理实现增强。Spring采用了动态代理的方式实现了运行时织入,而AspectJ采用了编译期织入和装载期织入的方式。
【8】在 SpringAopDemoApplication 中增加注解 @EnableAspectJAutoProxy

1 @SpringBootApplication
2 @EnableAspectJAutoProxy
3 public class SpringAopDemoApplication {
   
4     public static void main(String[] args) {
   
5         ApplicationContext applicationContext = SpringApplication.run(SpringAopDemoApplication.class, args);
6     }
7 }

深入博客连接链接

十四、Spring 如何保证 Controller 并发的安全


Controller 默认是单例的,一般情况下,如果用Spring MVC 的 Controller时,尽量不在 Controller中使用实例变量。否则会出现线程不安全性的情况,导致数据逻辑混乱。正因为单例所以不是线程安全的。举个简单例子:

 1 @Controller
 2 public class ScopeTestController {
   
 3 
 4     private int num = 0;
 5 
 6     @RequestMapping("/testScope")
 7     public void testScope() {
   
 8         System.out.println(++num);
 9     }
10 
11     @RequestMapping("/testScope2")
12     public void testScope2() {
   
13         System.out.println(++num);
14     }
15 }

【1】首先访问 http://localhost:8080/testScope,得到的答案是1
【2】然后我们再访问 http://localhost:8080/testScope2,得到的答案是 2。 此时就不是我们想要的答案,num已经被修改。所有 request都访问同一个 Controller时,这里的私有变量就是共用的,也就是说某个 request中如果修改了这个变量,那么在别的请求中也可读到这个修改的内容。

解决办法】:【1】不要在 Controller 中定义成员变量;
【2】万一必须要定义一个非静态成员变量时候,则通过注解 @Scope(“prototype”),将其设置为多例模式。
【3】在 Controller 中使用 ThreadLocal 变量;

十五、说说你对 Spring 的认识


这个问题涉及的面比较广,你可以告诉面试官简单的说下,Spring 的两大核心 IOC 和 AOP 的思想:
【1】控制反转(IOC):传统的 Java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 Spring 开发模式中,Spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用 Spring 提供的对象就可以了,这是控制反转的思想;
【2】依赖注入(DI):Spring 使用 JavaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。可以了解下 autowrite 实现的原理;
【3】面向切面编程(AOP):在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。
IOC 源码链接
AOP 源码链接

十六、Spring 如何解决循环依赖问题


循环依赖问题就是 A->B->A,Spring 在创建A的时候,发现需要依赖B,当去创建B实例时发现B又依赖于A,又去创建A。因此形成一个闭环,无法停止下来就可能会导致 CPU计算飙升。

解决方案:Spring 解决这个问题主要靠巧妙的三层缓存,所谓的缓存主要是指这三个map**,singletonObjects主要存放的是单例对象,属于第一级缓存;**singletonFactories属于单例工厂对象,属于第三级缓存;earlySingletonObjects属于第二级缓存,如何理解 early这个标识呢?它表示只是经过了实例化尚未初始化的对象。Spring 首先从 singletonObjects(一级缓存)中尝试获取,如果获取不到并且对象在创建中,则尝试从 earlySingletonObjects(二级缓存)中获取,如果还是获取不到并且允许从 singletonFactories 通过 getObject获取,则通过 singletonFactory.getObject()(三级缓存)获取。如果获取到了则移除对应的singletonFactory,将 singletonObject 放入到 earlySingletonObjects,其实就是将三级缓存提升到二级缓存,这个就是缓存升级。Spring 在进行对象创建的时候,会依次从一级、二级、三级缓存中寻找对象,如果找到直接返回。由于是初次创建,只能从第三级缓存中找到(实例化阶段放入进去的),创建完实例,然后将缓存放到第一级缓存中。下次循环依赖的再直接从一级缓存中就可以拿到实例对象了。

十七、Spring IOC 的理解,其初始化过程


IOC 的初始化过程是通过 AbstractApplicationContext 中的 refresh 方法完成的,可以查看 IOC 源码进行简要回答。
博客链接】:链接

十八、SpringMVC 的工作流程


【1】用户发送请求至前端控制器 DispatcherServlet;
【2】DispatcherServlet 收到请求调用 HandlerMapping处理器映射器;
【3】处理器映射器找到具体的处理器(可以根据 xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet;
【5】DispatcherServlet 调用 HandlerAdapter处理器适配器;
【6】HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器);
【7】Controller 执行完成返回 ModelAndView;
【8】HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet;
【9】DispatcherServlet 将 ModelAndView 传给 ViewReslover视图解析器;
【10】ViewReslover 解析后返回具体 View;
【11】DispatcherServlet 根据 View进行渲染视图(即将模型数据填充至视图中);
【12】DispatcherServlet 响应用户;

【前端控制器-DispatcherServlet】:接收请求,响应结果,相当于转发器,中央处理器。有了 DispatcherServlet减少了其它组件之间的耦合度。用户请求到达前端控制器,它就相当于 mvc模式中的c,DispatcherServlet 是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性。可以将其看做是 SpringMVC 实现中最为核心的部分;DispatcherServlet 的启动过程就是 SpringMVC 的启动过程。DispatcherServlet 的工作大致分为两个部分:①、初始化部分:由 initServletBean() 启动通过 initApplicationContext() 方法最终调用 DispatcherServlet 的 initStrategies 方法。DispatcherServlet 对 MVC 的其他模块进行初始化。比如:HandlerMapping、ViewResolver 等。②、对 HTTP 请求进行响应:doService() 方法,在这个方法调用中封装了 doDispatch() 这个 doDispatch 是实现 MVC模式的主要部分。不但建立了自己持有的 IOC容器还肩负着请求分发处理的重任。
处理器映射器-HandlerMapping对于不同 Web请求有对应的映射 Spring 提供了不同的 HandlerMapping 作为映射策略。这个策略可以根据需求选择。默认的策略:BeanNameUrlHandlerMapping。根据请求的 url查找 Handler,HandlerMapping 负责根据用户请求找到 Handler即处理器,SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等;
处理器适配器-HandlerAdapter按照特定规则(HandlerAdapter要求的规则)去执行Handler,通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行;
处理器-Handler】(需要工程师开发)
:注意
:编写 Handler 时按照 HandlerAdapter 的要求去做,这样适配器才可以去正确执行Handler,Handler 是继 DispatcherServlet 前端控制器的后端控制器,在 DispatcherServlet的控制下 Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发 Handler;
视图解析器View resolver进行视图解析,根据逻辑视图名解析成真正的视图(view)。View Resolver 负责将处理结果生成View视图,View Resolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View视图对象,最后对 View进行渲染将处理结果通过页面展示给用户。 springmvc 框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等;
视图View】(需要工程师开发)View 是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf…);

 1 <web-app>
 2     <listener>
 3         <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 4     </listener>
 5 
 6     <context-param>
 7         <param-name>contextConfigLocation</param-name>
 8         <param-value>/WEB-INF/app-context.xml</param-value>
 9     </context-param>
10 
11     <servlet>
12         <servlet-name>app</servlet-name>
13         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
14         <init-param>
15             <param-name>contextConfigLocation</param-name>
16             <param-value></param-value>
17         </init-param>
18         <load-on-startup>1</load-on-startup>
19     </servlet>
20 
21     <servlet-mapping>
22         <servlet-name>app</servlet-name>
23         <url-pattern>/app/*</url-pattern>
24     </servlet-mapping>
25 </web-app>

十九、什么情况下Spring事物会失效


二十、谈谈你对 spring IOC和 DI的理解


IoC:Inverse of Control 控制反转的概念,就是将原本在程序中手动创建对象的控制权,交由 Spring框架管理,简单说,就是创建对象控制权被反转到了 Spring框架。DI:Dependency Injection 依赖注入,在 Spring框架负责创建 Bean对象时,动态的将依赖对象注入到 Bean组件 。
img
IoC 和 DI的区别:IoC 控制反转,指将对象的创建权,反转到 Spring容器 , DI 依赖注入,指 Spring创建对象的过程中,将对象依赖属性通过配置进行注入 。

二十一、Spring的核心类有哪些


BeanFactory:产生一个新的实例,可以实现单例模式;
BeanWrapper:提供统一的 get及 set方法;
ApplicationContext:提供框架的实现,包括 BeanFactory的所有功能;

二十二、Spring 如何处理线程并发问题?


Spring 使用 ThreadLocal解决线程安全问题。我们知道只有无状态的 Bean才可以在多线程环境下共享,在 Spring中,绝大部分 Bean都可以声明为 singleton作用域。就是因为 Spring对一些 Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用 ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的 Bean就可以在多线程中共享了。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

而 ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。

由于 ThreadLocal中可以持有任何类型的对象,低版本 JDK所提供的 get()返回的是 Object对象,需要强制类型转换。但 JDK5.0通过泛型很好的解决了这个问题,在一定程度地简化 ThreadLocal的使用。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间的方式,而 ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

二十三、Spring里面 applicationContext.xml文件能不能改成其他文件名?


ContextLoaderListener 是一个 ServletContextListener,它在你的 web应用启动的时候初始化。缺省情况下, 它会在 WEB-INF/applicationContext.xml文件找 Spring的配置。 你可以通过定义一个 元素名字为” contextConfigLocation”来改变 Spring配置文件的 位置。示例如下:

1 <listener> 
2     <listener-class>org.springframework.web.context.ContextLoaderListener
3         <context-param> 
4          <param-name>contextConfigLocation</param-name> 
5          <param-value>/WEB-INF/xyz.xml</param-value> 
6         </context-param>   
7     </listener-class> 
8 </listener> 

二十四、Resource 与 Autowired区别


在使用 Spring的自动的 Annotation注解的时候经常会见到两类的注解:@Resource(这个注解属于J2EE的,个人常用)、@Autowired(这个注解是 Spring的) ,如果你想要了解这两个的区别,最好的做法是先认真学完了 Spring依赖注入的时候讲解过的自动配置操作,在 Spring里面自动配置的模式有两类:按照类型、按照名称。
@Autowired:则表示按照类型进行自动注入,缺点是如果类型相同,则无法注入;
@Resource:具备按照类型自动注入的特点,而后如果现在类型相同,则可以设置一个名称,也就是说你使用@Component、@Service等注解设置自动扫描的时候可以设置一个名字,而这个名字就可在@Resource中使用了;

二十四、Resource 与 Autowired区别


在使用 Spring的自动的 Annotation注解的时候经常会见到两类的注解:@Resource(这个注解属于J2EE的,个人常用)、@Autowired(这个注解是 Spring的) ,如果你想要了解这两个的区别,最好的做法是先认真学完了 Spring依赖注入的时候讲解过的自动配置操作,在 Spring里面自动配置的模式有两类:按照类型、按照名称。
@Autowired:则表示按照类型进行自动注入,缺点是如果类型相同,则无法注入;
@Resource:具备按照类型自动注入的特点,而后如果现在类型相同,则可以设置一个名称,也就是说你使用@Component、@Service等注解设置自动扫描的时候可以设置一个名字,而这个名字就可在@Resource中使用了;
SpringBoot里面,因为其自动支持一些环境配置,如果使用的是 Autowired,那么配置多个相同类型的 Bean的话,将无法进行准确的注入操作。必须使用 @Resource完成。

本文来源程序猿进阶,由javajgs_com转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处:https://javajgs.com/archives/8297

发表评论