天天看點

Spring底層核心原了解析

作者:喬治龍

先來看看入門使用Spring的代碼:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) context.getBean("userService");
userService.test();           

對于這三行代碼應該,大家都是比較熟悉,這是學習Spring的hello world。可是,這三行代碼底層都做了什麼,比如:

第一行代碼,會構造一個ClassPathXmlApplicationContext對象,ClassPathXmlApplicationContext該如何了解,調用該構造方法除開會執行個體化得到一個對象,還會做哪些事情?

第二行代碼,會調用ClassPathXmlApplicationContext的getBean方法,會得到一個UserService對象,getBean()是如何實作的?傳回的UserService對象和我們自己直接new的UserService對象有差別嗎?

第三行代碼,就是簡單的調用UserService的test()方法,不難了解。

隻看這三行代碼,其實并不能展現出來Spring的強大之處,也不能了解為什麼需要ClassPathXmlApplicationContext和getBean()方法,随着課程的深入将會改變你此時的觀念,而對于上面的這些疑問,也會随着課程深入逐漸得到解決。對于這三行代碼,你現在可以認為:如果你要用Spring,你就得這麼寫。就像你要用Mybatis,你就得寫各種Mapper接口。 ​

但是用ClassPathXmlApplicationContext其實已經過時了,在新版的Spring MVC和Spring Boot的底層主要用的都是AnnotationConfigApplicationContext,比如:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
//ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) context.getBean("userService");
userService.test();
可以看到AnnotationConfigApplicationContext的用法和ClassPathXmlApplicationContext是非常類似的,隻不過需要傳入的是一個class,而不是一個xml檔案。           

而AppConfig.class和spring.xml一樣,表示Spring的配置,比如可以指定掃描路徑,可以直接定義Bean,比如: ​

spring.xml中的内容為:

<context:component-scan base-package="com.zhouyu"/>
<bean id="userService" class="com.zhouyu.service.UserService"/>           

AppConfig中的内容為:

@ComponentScan("com.zhouyu")
public class AppConfig {
  @Bean
  public UserService userService(){
  return new UserService();
  }
}           

是以spring.xml和AppConfig.class本質上是一樣的。 ​

目前,我們基本很少直接使用上面這種方式來用Spring,而是使用Spring MVC,或者SpringBoot,但是它們都是基于上面這種方式的,都需要在内部去建立一個ApplicationContext的,隻不過:

Spring MVC建立的是XmlWebApplicationContext,和ClassPathXmlApplicationContext類似,都是基于XML配置;

Spring Boot建立的是AnnotationConfigApplicationContext;

因為AnnotationConfigApplicationContext是比較重要的,并且AnnotationConfigApplicationContext和ClassPathXmlApplicationContext大部分底層都是共同的。

Spring中是如何建立一個對象?

其實不管是AnnotationConfigApplicationContext還是ClassPathXmlApplicationContext,目前,我們都可以簡單的将它們了解為就是用來建立Java對象的,比如調用getBean()就會去建立對象(此處不嚴謹,getBean可能也不會去建立對象,後續文章會講解)。 ​

在Java語言中,肯定是根據某個類來建立一個對象的。我們在看一下執行個體代碼:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) context.getBean("userService");
userService.test();           

當我們調用context.getBean("userService")時,就會去建立一個對象,但是getBean方法内部怎麼知道"userService"對應的是UserService類呢?

是以,我們就可以分析出來,在調用AnnotationConfigApplicationContext的構造方法時,也就是第一行代碼,會去做一些事情:

解析AppConfig.class,得到掃描路徑;

周遊掃描路徑下的所有Java類,如果發現某個類上存在@Component、@Service等注解,那麼Spring就把這個類記錄下來,存在一個Map中,比如Map<String, Class>。(實際上,Spring源碼中确實存在類似的這麼一個Map,叫做BeanDefinitionMap,後續文章會講解);

Spring會根據某個規則生成目前類對應的beanName,作為key存入Map,目前類作為value;

​這樣,但調用context.getBean("userService")時,就可以根據"userService"找到UserService類,進而就可以去建立對象了。 ​

Bean的建立過程

那麼Spring到底是如何來建立一個Bean的呢,這個就是Bean建立的生命周期,大緻過程如下:

利用該類的構造方法來執行個體化得到一個對象(但是如何一個類中有多個構造方法,Spring則會進行選擇,這個叫做推斷構造方法)

得到一個對象後,Spring會判斷該對象中是否存在被@Autowired注解了的屬性,把這些屬性找出來并由Spring進行指派(依賴注入)

依賴注入後,Spring會判斷該對象是否實作了BeanNameAware接口、BeanClassLoaderAware接口、BeanFactoryAware接口,如果實作了,就表示目前對象必須實作該接口中所定義的setBeanName()、setBeanClassLoader()、setBeanFactory()方法,那Spring就會調用這些方法并傳入相應的參數(Aware回調)

Aware回調後,Spring會判斷該對象中是否存在某個方法被@PostConstruct注解了,如果存在,Spring會調用目前對象的此方法(初始化前)

緊接着,Spring會判斷該對象是否實作了InitializingBean接口,如果實作了,就表示目前對象必須實作該接口中的afterPropertiesSet()方法,那Spring就會調用目前對象中的afterPropertiesSet()方法(初始化)

最後,Spring會判斷目前對象需不需要進行AOP,如果不需要那麼Bean就建立完了,如果需要進行AOP,則會進行動态代理并生成一個代理對象做為Bean(初始化後)

​通過最後一步,我們可以發現,當Spring根據UserService類來建立一個Bean時:

如果不用進行AOP,那麼Bean就是UserService類的構造方法所得到的對象。

如果需要進行AOP,那麼Bean就是UserService的代理類所執行個體化得到的對象,而不是UserService本身所得到的對象。

​Bean對象建立出來後:

如果目前Bean是單例Bean,那麼會把該Bean對象存入一個Map<String, Object>,Map的key為beanName,value為Bean對象。這樣下次getBean時就可以直接從Map中拿到對應的Bean對象了。(實際上,在Spring源碼中,這個Map就是單例池)

如果目前Bean是原型Bean,那麼後續沒有其他動作,不會存入一個Map,下次getBean時會再次執行上述建立過程,得到一個新的Bean對象。

推斷構造方法

Spring在基于某個類生成Bean的過程中,需要利用該類的構造方法來執行個體化得到一個對象,但是如果一個類存在多個構造方法,Spring會使用哪個呢? ​

Spring的判斷邏輯如下:

如果一個類隻存在一個構造方法,不管該構造方法是無參構造方法,還是有參構造方法,Spring都會用這個構造方法

如果一個類存在多個構造方法

這些構造方法中,存在一個無參的構造方法,那麼Spring就會用這個無參的構造方法;

這些構造方法中,不存在一個無參的構造方法,那麼Spring就會報錯;

​Spring的設計思想是這樣的:

如果一個類隻有一個構造方法,那麼沒得選擇,隻能用這個構造方法

如果一個類存在多個構造方法,Spring不知道如何選擇,就會看是否有無參的構造方法,因為無參構造方法本身表示了一種預設的意義

不過如果某個構造方法上加了@Autowired注解,那就表示程式員告訴Spring就用這個加了注解的方法,那Spring就會用這個加了@Autowired注解構造方法了

需要重視的是,如果Spring選擇了一個有參的構造方法,Spring在調用這個有參構造方法時,需要傳入參數,那這個參數是怎麼來的呢? ​

Spring會根據入參的類型和入參的名字去Spring中找Bean對象(以單例Bean為例,Spring會從單例池那個Map中去找):

先根據入參類型找,如果隻找到一個,那就直接用來作為入參;

如果根據類型找到多個,則再根據入參名字來确定唯一一個;

最終如果沒有找到,則會報錯,無法建立目前Bean對象;

确定用哪個構造方法,确定入參的Bean對象,這個過程就叫做推斷構造方法。

AOP大緻流程

AOP就是進行動态代理,在建立一個Bean的過程中,Spring在最後一步會去判斷目前正在建立的這個Bean是不是需要進行AOP,如果需要則會進行動态代理。 ​

如何判斷目前Bean對象需不需要進行AOP:

找出所有的切面Bean

周遊切面中的每個方法,看是否寫了@Before、@After等注解

如果寫了,則判斷所對應的Pointcut是否和目前Bean對象的類是否比對

如果比對則表示目前Bean對象有比對的的Pointcut,表示需要進行AOP

利用cglib進行AOP的大緻流程:

生成代理類UserServiceProxy,代理類繼承UserService

代理類中重寫了父類的方法,比如UserService中的test()方法

代理類中還會有一個target屬性,該屬性的值為被代理對象(也就是通過UserService類推斷構造方法執行個體化出來的對象,進行了依賴注入、初始化等步驟的對象)

代理類中的test()方法被執行時的邏輯如下:

執行切面邏輯(@Before)

調用target.test()

​當我們從Spring容器得到UserService的Bean對象時,拿到的就是UserServiceProxy所生成的對象,也就是代理對象。 ​

UserService代理對象.test()--->執行切面邏輯--->target.test(),注意target對象不是代理對象,而是被代理對象。 ​

Spring事務

當我們在某個方法上加了@Transactional注解後,就表示該方法在調用時會開啟Spring事務,而這個方法所在的類所對應的Bean對象會是該類的代理對象。 ​

Spring事務的代理對象執行某個方法時的步驟:

判斷目前執行的方法是否存在@Transactional注解;

如果存在,則利用事務管理器(TransactionMananger)建立一個資料庫連接配接;

修改資料庫連接配接的autocommit為false;

執行target.test(),執行程式員所寫的業務邏輯代碼,也就是執行sql;

執行完了之後如果沒有出現異常,則送出,否則復原;​

Spring事務是否會失效的判斷标準:

某個加了@Transactional注解的方法被調用時,要判斷到底是不是直接被代理對象調用的,如果是則事務會生效,如果不是則失效。