首先,Spring bean的默認加載順序是怎么控制的
工程中有2個bean,A和B,其中必須先初始化A再初始化B,但是沒有depend-on或者Order等方式去保證,只不過恰好剛好這么運行着沒出事,但是突然增加了一個C之后,就先初始化B再初始化A導致問題,但是在主干版本上卻沒問題。
解決這個問題其實很簡單,depend-on即可,但是為什么會分支版本上會增加C后就改變AB的初始化順序?為什么主干版本上同樣添加沒問題呢?可以看spring的源碼 DefaultListableBeanFactory 類中有
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(256);
這個map保存的就是xml中定義的bean結構,spring解析定義的bean之后,保存到這里,其中key為beanid,value為bean的詳細信息,根據beanid算出hashCode分別為1505068002,1682286698,從定義可以看到這是一個ConcurrentHashMap,在獲取到定義后,spring在初始化的時候是怎么初始化的呢?顯然也是從這個定義里取值,為了簡化問題,我們理解為是類似如下方式
for(Entry<String,BeanDefinition> entry : beanDefinitionMap)
去遍歷bean然后根據BeanDefinition來初始化,考慮分支版本的是用JDK1.6,主干是用JDK1.8,不同ConcurrentHashMap的實現是否會導致取出來的順序不一樣呢?
JDK1.8中,ConcurrentHashMap 的實現是這樣的,數組Node<K,V>[]table,每個元素為一個Node,每個元素直接落到Node上,去追加鏈表或者在紅黑樹上,總之是一次hash
JDK1.6中,ConcurrentHashMap 的實現是這樣的,先Segment<K,V>[]segments來進行一個分段,然后每個Segment里再包含元素 HashEntry<K,V>[] table,即,會對每個元素會有2次hash,第一次定位到Segment,第二次在Segment內部定位到自己的位置userService
可以知道,在JDK1.8中2個bean的位置是固定的(所以主干版本同樣添加但是AB初始化順序不變),但是在JDK1.6中可能會發生這種情況:B在第一個Segment中,A在第二個Segment中,導致取出來的時候先獲取到B,后取出來A,所以出現了空指針,同樣,也可以解釋,為什么新增了1個id為C的bean就導致了問題,但是之前沒問題,因為新增bean導致ConcurrentHashMap中部分bean所在的Segment發生變化。
當然,對有依賴關系顯然是顯式指定出來的好,不然像這樣坑后來人就不好了
使用Spring @DependsOn控制bean加載順序
spring容器載入bean順序是不確定的,spring框架沒有約定特定順序邏輯規范。但spring保證如果A依賴B(如beanA中有@Autowired B的變量),那么B將先於A被加載。但如果beanA不直接依賴B,我們如何讓B仍先加載呢?
控制bean初始化順序
可能有些場景中,bean A 間接依賴 bean B。如Bean B應該需要更新一些全局緩存,可能通過單例模式實現且沒有在spring容器注冊,bean A需要使用該緩存;因此,如果bean B沒有准備好,bean A無法訪問。
另一個場景中,bean A是事件發布者(或JMS發布者),bean B (或一些) 負責監聽這些事件,典型的如觀察者模式。我們不想B 錯過任何事件,那么B需要首先被初始化。
簡言之,有很多場景需要bean B應該被先於bean A被初始化,從而避免各種負面影響。我們可以在bean A上使用@DependsOn注解,告訴容器bean B應該先被初始化。下面通過示例來說明。
示例說明
示例通過事件機制說明,發布者和監聽者,然后通過spring配置運行。為了方便說明,示例進行了簡化。
EventManager.java
事件管理類,維護監聽器列表,通過單例方法獲取事件管理器,可以增加監聽器或發布事件。
import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; public class EventManager { private final List<Consumer<String>> listeners = new ArrayList<>(); private EventManager() { } private static class SingletonHolder { private static final EventManager INSTANCE = new EventManager(); } public static EventManager getInstance() { return SingletonHolder.INSTANCE; } public void publish(final String message) { listeners.forEach(l -> l.accept(message)); } public void addListener(Consumer<String> eventConsumer) { listeners.add(eventConsumer); } }
EventPublisherBean.java
事件發布類,通過EventManager類發布事件。
import com.logicbig.example.EventManager; public class EventPublisherBean { public void initialize() { System.out.println("EventPublisherBean initializing"); EventManager.getInstance().publish("event published from EventPublisherBean"); } }
EventListenerBean.java
事件監聽者,可以增加監聽器。
import com.logicbig.example.EventManager; public class EventListenerBean { private void initialize() { EventManager.getInstance(). addListener(s -> System.out.println("event received in EventListenerBean : " + s)); } }
AppConfig.java
配置運行類。
@Configuration @ComponentScan("com.logicbig.example") public class AppConfig { @Bean(initMethod = "initialize") @DependsOn("eventListener") public EventPublisherBean eventPublisherBean () { return new EventPublisherBean(); } @Bean(name = "eventListener", initMethod = "initialize") // @Lazy public EventListenerBean eventListenerBean () { return new EventListenerBean(); } public static void main (String... strings) { new AnnotationConfigApplicationContext(AppConfig.class); } }
運行AppConfig的main方法,輸出結果為:
EventListenerBean initializing
EventPublisherBean initializing
event received in EventListenerBean : event published from EventPublisherBean
總結
如果我們注釋掉@DependsOn("eventListener"),我們可能不確定獲得相同結果。嘗試多次運行main方法,偶爾我們將看到EventListenerBean 沒有收到事件。為什么是偶爾呢?因為容器啟動過程中,spring按任意順序加載bean。
那么當不使用@DependsOn可以讓其100%確定嗎?可以使用@Lazy注解放在eventListenerBean ()上。因為EventListenerBean在啟動階段不加載,當其他bean需要其時才加載。這次我們僅EventListenerBean被初始化。
EventPublisherBean initializing
現在從新增加@DependsOn,也不刪除@Lazy注解,輸出結果和第一次一致,雖然我們使用了@Lazy注解,eventListenerBean在啟動時仍然被加載,因為@DependsOn表明需要EventListenerBean。
@Lazy
@Lazy用於指定該Bean是否取消預初始化。主要用於修飾Spring Bean類,用於指定該Bean的預初始化行為,
使用該Annotation時可以指定一個boolean型的value屬性,該屬性決定是否要預初始化該Bean
- lazy代表延時加載,lazy=false,代表不延時,如果對象A中還有對象B的引用,會在A的xml映射文件中配置b的對象引用,多對一或一對多,不延時代表查詢出對象A的時候,會把B對象也查詢出來放到A對象的引用中,A對象中的B對象是有值的。
- lazy=true代表延時,查詢A對象時,不會把B對象也查詢出來,只會在用到A對象中B對象時才會去查詢,默認好像是false,你可以看看后台的sql語句的變化就明白了,一般需要優化效率的時候會用到
@Lazy(true) @Component public class Chinese implements Person{ //codes here }
@DependsOn用於強制初始化其他Bean。可以修飾Bean類或方法,使用該Annotation時可以指定一個字符串數組作為參數,每個數組元素對應於一個強制初始化的Bean
@DependsOn({"steelAxe","abc"}) @Component public class Chinese implements Person{ //codes here }
@Order
@Order注解的源碼如下,value用例賦值,默認賦值Ordered.LOWEST_PRECEDENCE,即默認優先級最低:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) public @interface Order { /** * The order value. Default is {@link Ordered#LOWEST_PRECEDENCE}. * @see Ordered#getOrder() */ int value() default Ordered.LOWEST_PRECEDENCE; }
再來看下Order接口的源碼如下,一目了然,值越小優先級越高,即被優先加載(precedence即優先級):
public interface Ordered { /** * Useful constant for the highest precedence value. * @see java.lang.Integer#MIN_VALUE */ int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;//值越小,優先級越高 /** * Useful constant for the lowest precedence value. * @see java.lang.Integer#MAX_VALUE */ int LOWEST_PRECEDENCE = Integer.MAX_VALUE;//值越大,優先級越低
/** * Return the order value of this object, with a * higher value meaning greater in terms of sorting. * <p>Normally starting with 0, with {@code Integer.MAX_VALUE} * indicating the greatest value. Same order values will result * in arbitrary positions for the affected objects. * <p>Higher values can be interpreted as lower priority. As a * consequence, the object with the lowest value has highest priority * (somewhat analogous to Servlet "load-on-startup" values). * @return the order value */ int getOrder();
拓展①:為何在類上加@Order就可以對類(映射到容器中就是bean)有效排序了?
先了解下Java強大的注解功能,可以參考Java注解教程及自定義注解
拓展②:給類@Order注解后spring容器具體怎么給bean排序的呢?
在@Order注解的源碼上,作者寫了相關信息,感興趣可以查看下
拓展③:@Order的作用域可以是類、方法、類成員,方法和類成員暫時沒有測試,保留。
Springboot的@AutoConfigureAfter注解,手動的指定Bean的實例化順序
Springboot的@AutoConfigureAfter注解,手動的指定Bean的實例化順序。了解Spring內Bean的解析,加載和實例化順序機制有助於我們更好的使用Spring/Springboot,避免手動的去干預Bean的加載過程,搭建更優雅的框架。
Spring容器在實例化時會加載容器內所有非延遲加載的單例類型Bean,看如下源碼:
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext, DisposableBean { //刷新Spring容器,相當於初始化 public void refresh() throws BeansException, IllegalStateException { ...... // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); } } public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { /** List of bean definition names, in registration order */ private volatile List<String> beanDefinitionNames = new ArrayList<String>(256); public void preInstantiateSingletons() throws BeansException { List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames); for (String beanName : beanNames) { ...... getBean(beanName); //實例化Bean } } }
ApplicationContext內置一個BeanFactory對象,作為實際的Bean工廠,和Bean相關業務都交給BeanFactory去處理。
在BeanFactory實例化所有非延遲加載的單例Bean時,遍歷beanDefinitionNames 集合,按順序實例化指定名稱的Bean。beanDefinitionNames 屬性是Spring在加載Bean Class生成的BeanDefinition時,為這些Bean預先定義好的名稱,看如下代碼:
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { ...... this.beanDefinitionNames.add(beanName); } }
BeanFactory在加載一個BeanDefinition(也就是加載Bean Class)時,將相應的beanName存入beanDefinitionNames屬性中,在加載完所有的BeanDefinition后,執行Bean實例化工作,此時會依據beanDefinitionNames的順序來有序實例化Bean,也就是說Spring容器內Bean的加載和實例化是有順序的,而且近似一致,當然僅是近似。
Spring在初始化容器時,會先解析和加載所有的Bean Class,如果符合要求則通過Class生成BeanDefinition,存入BeanFactory中,在加載完所有Bean Class后,開始有序的通過BeanDefinition實例化Bean。
我們先看加載Bean Class過程,零配置下Spring Bean的加載起始於ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry(BeanDefinitionRegistry)方法,我總結了下其加載解析Bean Class的流程:
配置類可以是Spring容器的起始配置類,也可以是通過@ComponentScan掃描得到的類,也可以是通過@Import引入的類。如果這個類上含有@Configuration,@Component,@ComponentScan,@Import,@ImportResource注解中的一個,或者內部含有@Bean標識的方法,那么這個類就是一個配置類,Spring就會按照一定流程去解析這個類上的信息。
在解析的第一步會校驗當前類是否已經被解析過了,如果是,那么需要按照一定的規則處理(@ComponentScan得到的Bean能覆蓋@Import得到的Bean,@Bean定義的優先級最高)。
如果未解析過,那么開始解析:
解析內部類,查看內部類是否應該被定義成一個Bean,如果是,遞歸解析。
解析@PropertySource,也就是解析被引入的Properties文件。
解析配置類上是否有@ComponentScan注解,如果有則執行掃描動作,通過掃描得到的Bean Class會被立即解析成BeanDefinition,添加進beanDefinitionNames屬性中。之后查看掃描到的Bean Class是否是一個配置類(大部分情況是,因為標識@Component注解),如果是則遞歸解析這個Bean Class。
解析@Import引入的類,如果這個類是一個配置類,則遞歸解析。
解析@Bean標識的方法,此種形式定義的Bean Class不會被遞歸解析
解析父類上的@ComponentScan,@Import,@Bean,父類不會被再次實例化,因為其子類能夠做父類的工作,不需要額外的Bean了。
在1,3,4,6中都有遞歸操作,也就是在解析一個Bean Class A時,發現其上能夠獲取到其他Bean Class B信息,此時會遞歸的解析Bean Class B,在解析完Bean Class B后再接着解析Bean Class A,可能在解析B時能夠獲取到C,那么也會先解析C再解析B,就這樣不斷的遞歸解析。
在第3步中,通過@ComponentScan掃描直接得到的Bean Class會被立即加載入beanDefinitionNames中,但@Import和@Bean形式定義的Bean Class則不會,也就是說正常情況下面@ComponentScan直接得到的Bean其實例化時機比其他兩種形式的要早。
通過@Bean和@Import形式定義的Bean Class不會立即加載,他們會被放入一個ConfigurationClass類中,然后按照解析的順序有序排列,就是圖片上的 “將配置類有序排列“。一個ConfigurationClass代表一個配置類,這個類可能是被@ComponentScan掃描到的,則此類已經被加載過了;也可能是被@Import引入的,則此類還未被加載;此類中可能含有@Bean標識的方法。
Spring在解析完了所有Bean Class后,開始加載ConfigurationClass。如果這個ConfigurationClass是被Import的,也就是說在加載@ComponentScan時其未被加載,那么此時加載ConfigurationClass代表的Bean Class。然后加載ConfigurationClass內的@Bean方法。
順序總結:@ComponentScan > @Import > @Bean
下面看實際的啟動流程:
此圖順序驗證小框架:Spring Bean解析,加載及實例化順序驗證小框架
Bean Class的結構圖如上所示,A是配置類的入口,通過A能直接或間接的引入一個模塊。
此時啟動Spring容器,將A引入容器內。
如果A是通過@ComponentScan掃描到的,那么此時的加載順序是:
A > D > F > B > E > G > C
如果A是通過@Import形式引入的,那么此時的加載順訊是:
D > F > B > E > G > A > C
當然以上僅僅代表着加載Bean Class的順序,實際實例化Bean的順序和加載順序大體相同,但還是會有一些差別。
Spring在通過getBean(beanName)形式實例化Bean時,會通過BeanDefinition去生成Bean對象。在這個過程中,如果BeanDefinition的DependsOn不為空,從字面理解就是依賴某個什么,其值一般是某個或多個beanName,也就是說依賴於其他Bean,此時Spring會將DependsOn指定的這些名稱的Bean先實例化,也就是先調用getBean(dependsOn)方法。我們可以通過在Bean Class或者@Bean的方法上標識@DependsOn注解,來指定當前Bean實例化時需要觸發哪些Bean的提前實例化。
當一個Bean A內部通過@Autowired或者@Resource注入Bean B,那么在實例化A時會觸發B的提前實例化,此時會注冊A>B的dependsOn依賴關系,實質和@DependsOn一樣,這個是Spring自動為我們處理好的。
了解Spring Bean的解析,加載及實例化的順序機制能夠加深對Spring的理解,搭建更優雅簡介的Spring框架。