成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Spring Boot 啟動優化實踐

開發
本文系統性分析并優化了一個Spring Boot項目啟動耗時高達 280 秒的問題。通過識別瓶頸、優化分庫分表加載邏輯、異步初始化耗時任務等手段,最終將啟動耗時縮短至 159 秒,提升近 50%。文章涵蓋啟動流程分析、性能熱點識別、異步初始化設計等關鍵技術細節,適用于大型Spring Boot項目的性能優化參考。

一、前言

隨著業務的發展,筆者項目對應的Spring Boot工程的依賴越來越多。隨著依賴數量的增長,Spring 容器需要加載更多組件、解析復雜依賴并執行自動裝配,導致項目啟動時間顯著增長。在日常開發或測試過程中,一旦因為配置變更或者其他熱部署不生效的變更時,項目重啟就需要等待很長的時間影響代碼的交付。加快Spring項目的啟動可以更好的投入項目中,提升開發效率。

整體環境介紹:

  • Spring版本:4.3.22
  • Spring Boot版本:1.5.19
  • CPU:i5-9500
  • 內存:24GB
  • 優化前啟動耗時:280秒

二、Spring Boot項目啟動流程介紹

Spring Boot項目主要啟動流程都在org.spring-framework.boot.SpringApplication#run(java.lang.String...)方法中:

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // Spring上下文
    ConfigurableApplicationContext context = null;
    FailureAnalyzers analyzers = null;
    configureHeadlessProperty();
    // 初始化SpringApplicationRunListener監聽器
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        // 環境準備
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
         // 打印banner
        Banner printedBanner = printBanner(environment);
        // 創建上下文
        context = createApplicationContext();
        analyzers = new FailureAnalyzers(context);
        // 容器初始化
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        // 刷新容器內容
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        // 結束監聽廣播
        listeners.finished(context, null);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        return context;
    } catch (Throwable ex) {
        handleRunFailure(context, listeners, analyzers, ex);
        throw new IllegalStateException(ex);
    }
}

可以看到在啟動流程中,監聽器應用在了應用的多個生命周期中。并且Spring Boot中也預留了針對listener的擴展點。我們可以借此實現一個自己的擴展點去監聽Spring Boot的每個階段的啟動耗時,實現如下:

@Slf4j
public class MySpringApplicationRunListener implements SpringApplicationRunListener{
    private Long startTime;
    public MySpringApplicationRunListener(SpringApplication application, String[] args){
    }
    @Override
    public void starting(){
        startTime = System.currentTimeMillis();
        log.info("MySpringListener啟動開始 {}", LocalTime.now());
    }
    @Override
    public void environmentPrepared(ConfigurableEnvironment environment){
        log.info("MySpringListener環境準備 準備耗時:{}毫秒", (System.currentTimeMillis() - startTime));
        startTime = System.currentTimeMillis();
    }
    @Override
    public void contextPrepared(ConfigurableApplicationContext context){
        log.info("MySpringListener上下文準備 耗時:{}毫秒", (System.currentTimeMillis() - startTime));
        startTime = System.currentTimeMillis();
    }
    @Override
    public void contextLoaded(ConfigurableApplicationContext context){
        log.info("MySpringListener上下文載入 耗時:{}毫秒", (System.currentTimeMillis() - startTime));
        startTime = System.currentTimeMillis();
    }
   @Override
   public void finished(ConfigurableApplicationContext context, Throwable exception){
        log.info("MySpringListener結束 耗時:{}毫秒", (System.currentTimeMillis() - startTime));
        startTime = System.currentTimeMillis();
    }
}

接著還需要在classpath/META-INF目錄下新建spring.factories文件,并添加如下文件內容:

org.springframework.boot.SpringApplicationRunListener=com.vivo.internet.gameactivity.api.web.MySpringApplicationRunListener

至此,借助Listener機制,我們能夠追蹤Spring Boot啟動各階段的耗時分布,為后續性能優化提供數據支撐。

contextLoaded事件是在run方法中的prepareContext()結束時調用的,因此contextLoaded事件和finished事件之間僅存在兩個語句:refreshContext(context)和afterRefresh(context,applicationArguements)消耗了285秒的時間,調試一下就能發現主要耗時在refreshContext()中。

三、AbstractApplicationContext#refresh

refreshContext()最終調用到org.spring-framework.context.support.AbstractApplicationContext#refresh方法中,這個方法主要是beanFactory的預準備、對beanFactory完成創建并進行后置處理、向容器添加bean并且給bean添加屬性、實例化所有bean。通過調試發現,finishBeanFactoryInitialization(beanFactory) 方法耗時最久。該方法負責實例化容器中所有的單例 Bean,是啟動性能的關鍵影響點。

四、找出實例化耗時的Bean

Spring Boot也是利用的Spring的加載流程。在Spring中可以實現InstantiationAwareBeanPost-Processor接口去在Bean的實例化和初始化的過程中加入擴展點。因此我們可以實現該接口并添加自己的擴展點找到處理耗時的Bean。

@Service
public class TimeCostCalBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
    private Map<String, Long> costMap = Maps.newConcurrentMap();


    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        if (!costMap.containsKey(beanName)) {
            costMap.put(beanName, System.currentTimeMillis());
        }
        return null;
    }
    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        return true;
    }
    @Override
    public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
        return pvs;
    }
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
         if (costMap.containsKey(beanName)) {
            Long start = costMap.get(beanName);
            long cost = System.currentTimeMillis() - start;
            // 只打印耗時長的bean
             if (cost > 5000) {
                System.out.println("bean: " + beanName + "\ttime: " + cost + "ms");
            }
        }
         return bean;
    }
}

具體原理就是在Bean開始實例化之前記錄時間,在Bean初始化完成后記錄結束時間,打印實例化到初始化的時間差獲得Bean的加載總體耗時。結果如圖:

可以看到有許多耗時在10秒以上的類,接下來可以針對性的做優化。值得注意的是,統計方式為單點耗時計算,未考慮依賴鏈上下文對整體加載順序的影響,實際優化還需結合依賴關系分析。

五、singletonDataSource

@Bean(name = "singletonDataSource")
public DataSource singletonDataSource(DefaultDataSourceWrapper dataSourceWrapper) throws SQLException {
    //先初始化連接
    dataSourceWrapper.getMaster().init();
    //構建分庫分表數據源
    String dataSource0 = "ds0";
    Map<String, DataSource> dataSourceMap = new HashMap<>();
    dataSourceMap.put(dataSource0, dataSourceWrapper.getMaster());
    //分庫分表數據源
    DataSource shardingDataSource = ShardingDataSourceFactory.createDataSource
    (dataSourceMap,shardingRuleConfiguration, prop);
    return shardingDataSource;    
    }

singletonDataSource是一個分庫分表的數據源,連接池采用的是Druid,分庫分表組件采用的是公司內部優化后的中間件。通過簡單調試代碼發現,整個Bean耗時的過程發生在createDataSource方法,該方法中會調用createMetaData方法去獲取數據表的元數據,最終運行到loadDefaultTables方法。該方法如下圖,會遍歷數據庫中所有的表。因此數據庫中表越多,整體就越耗時。

筆者的測試環境數據庫中有很多的分表,這些分表為了和線上保持一致,分表的數量都和線上是一樣的。

因此在測試環境啟動時,為了加載這些分表會更加的耗時。可通過將分表數量配置化,使測試環境在不影響功能驗證的前提下減少分表數量,從而加快啟動速度。

六、初始化異步

activityServiceImpl啟動中,主要會進行活動信息的查詢初始化,這是一個耗時的操作。類似同樣的操作在工程的其他類中也存在。

@Service
public class ActivityServiceImpl implements ActivityService, InitializingBean{
     // 省略無關代碼
     @Override
     public void afterPropertiesSet() throws Exception {
        initActivity();
    }
     // 省略無關代碼
}

可以通過將afterPropertiesSet()異步化的方式加速項目的啟動。

觀察Spring源碼可以注意到afterPropertiesSet方法是在AbstractAutowireCapableBeanFactory#invokeInitMethods中調用的。在這個方法中,不光處理了afterPropertiesSet方法,也處理了init-method。

因此我們可以寫一個自己的BeanFactory繼承AbstractAutowireCapableBeanFactory,將invokeInitMethods方法進行異步化重寫。考慮到AbstractAutowireCapableBeanFactory是個抽象類,有額外的抽象方法需要實現,因此繼承該抽象類的子類DefaultListableBeanFactory。具體實現代碼如下:

public class AsyncInitListableBeanFactory extends DefaultListableBeanFactory{
     public AsyncInitBeanFactory(ConfigurableListableBeanFactory beanFactory){
         super(beanFactory);
    }
     @Override
     protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd)throws Throwable {
        if (beanName.equals("activityServiceImpl")) {
            AsyncTaskExecutor.submitTask(() -> {
                try {
                      super.invokeInitMethods(beanName, bean, mbd);
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            });
        } else {
              super.invokeInitMethods(beanName, bean, mbd);
        }
    }
}

又因為Spring在refreshContext()方法之前的prepareContext()發放中針對initialize方法提供了接口擴展(applyInitializers())。因此我們可以通過實現該接口并將我們的新的BeanFactory通過反射的方式更新到Spring的初始化流程之前。

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
     /**
     * Initialize the given application context.
     * @param applicationContext the application to configure
     */
    void initialize(C applicationContext);


}

改造后的代碼如下,新增AsyncAccelerate-Initializer類實現ApplicationContextInitializer接口:

public class AsyncBeanFactoryInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @SneakyThrows
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext){
        if (applicationContext instanceof GenericApplicationContext) {
            AsyncInitListableBeanFactory beanFactory = new AsyncInitListableBeanFactory(applicationContext.getBeanFactory());
            Field field = GenericApplicationContext.class.getDeclaredField("beanFactory");
            field.setAccessible(true);
            field.set(applicationContext, beanFactory);
        }
    }
}
public class AsyncBeanInitExecutor{
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final AtomicReference<ThreadPoolExecutor> THREAD_POOL_REF = new AtomicReference<>();
    private static final List<Future<?>> FUTURES = new ArrayList<>();
     /**
      * 創建線程池實例
      */
     private static ThreadPoolExecutor createThreadPoolExecutor(){
         int poolSize = CPU_COUNT + 1;
         return new ThreadPoolExecutor(poolSize, poolSize, 50L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
    /**
     * 確保線程池已初始化(線程安全)
     */
     private static void ensureThreadPoolExists(){
         if (THREAD_POOL_REF.get() != null) {
              return;
        }
        ThreadPoolExecutor executor = createThreadPoolExecutor();
         if (!THREAD_POOL_REF.compareAndSet(null, executor)) {
            executor.shutdown(); // 另一線程已初始化成功
        }
    }
    /**
     * 提交異步初始化任務
     *
     * @param task 初始化任務
     * @return 提交后的 Future 對象
     */
    public static Future<?> submitInitTask(Runnable task) {
        ensureThreadPoolExists();
        Future<?> future = THREAD_POOL_REF.get().submit(task);
        FUTURES.add(future);
        return future;
    }
    /**
     * 等待所有初始化任務完成并釋放資源
     */
    public static void waitForInitTasks(){
        try {
            for (Future<?> future : FUTURES) {
                future.get();
            }
        } catch (Exception ex) {
            throw new RuntimeException("Async init task failed", ex);
        } finally {
            FUTURES.clear();
            shutdownThreadPool();
        }
    }
     /**
     * 關閉線程池并重置引用
     */
     private static void shutdownThreadPool(){
        ThreadPoolExecutor executor = THREAD_POOL_REF.getAndSet(null);
         if (executor != null) {
            executor.shutdown();
        }
    }
}

實現類后,還需要在META-INF/spring.factories下新增說明org.springframework.context.Applicatinotallow=com.xxx.AsyncAccelerateInitializer,這樣這個類才能真正生效。

這樣異步化以后還有一個點需要注意,如果該初始化方法執行耗時很長,那么會存在Spring容器已經啟動完成,但是異步初始化任務沒執行完的情況,可能會導致空指針等異常。為了避免這種問題的發生,還要借助于Spring容器啟動中finishRefresh()方法,監聽對應事件,確保異步任務執行完成之后,再啟動容器。

public class AsyncInitCompletionListener implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, PriorityOrdered{
    private ApplicationContext currentContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)throws BeansException {
         this.currentContext = applicationContext;
    }
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event){
        if (event.getApplicationContext() == currentContext) {
            AsyncBeanInitExecutor.waitForInitTasks();
        }
    }
    @Override
    public int getOrder(){
         return Ordered.HIGHEST_PRECEDENCE;
    }
}

七、總結

啟動優化后的項目實際測試結果如下:

通過異步化初始化和分庫分表加載優化,項目啟動時間從 280 秒縮短至 159 秒,提升約 50%。這對于提升日常開發效率、加快測試與聯調流程具有重要意義。

責任編輯:龐桂玉 來源: vivo互聯網技術
相關推薦

2023-09-27 08:14:56

2017-01-23 21:05:00

AndroidApp啟動優化

2024-05-31 14:06:55

SpringCDSGraalVM

2023-06-02 16:24:46

SpringBootSSM

2024-12-16 08:10:00

Spring開發

2022-03-29 13:27:22

Android優化APP

2024-12-25 16:01:01

2024-11-21 14:42:31

2024-11-28 09:43:04

2024-12-03 11:12:47

2024-07-26 07:59:25

2019-04-28 09:00:15

開發者技能工具

2022-09-02 08:41:20

Spring項目微服務

2019-07-24 10:34:28

Spring Boot項目模板

2023-09-22 10:12:57

2021-09-02 10:10:59

技術VS Code實踐

2017-03-06 15:43:33

Springboot啟動

2024-09-09 05:30:00

數據庫Spring

2020-02-26 15:35:17

Spring Boot項目優化JVM調優

2022-10-11 14:58:00

性能優化Java
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产一区二区在线视频 | 99这里只有精品视频 | 99精品视频一区二区三区 | 91精品国产一区二区三区香蕉 | 超碰在线网站 | 午夜视频在线免费观看 | 久久99精品久久 | 久久久久久久电影 | 精品国产乱码久久久久久丨区2区 | 欧美综合一区二区三区 | 欧美在线观看一区 | www.久久 | 第四色影音先锋 | 孰女乱色一区二区三区 | 成人免费视频网站 | 中文字幕 在线观看 | 伊人春色成人网 | 国产99小视频 | 一区二区中文 | 亚洲社区在线 | 99国产精品99久久久久久粉嫩 | 91黄色免费看 | 色眯眯视频在线观看 | 国产成人精品一区 | 日韩在线观看网站 | 中文在线一区二区 | 欧美亚洲国产一区 | 中文字幕一区二区三区日韩精品 | 欧美网址在线观看 | 久久在线| 久久久久成人精品 | 黄色骚片| 亚洲一区二区三区视频 | 国产综合精品一区二区三区 | 久久久网 | 成人久久久 | 成人亚洲 | 日日夜夜精品免费视频 | 中文字幕精品一区久久久久 | 欧美黑人一区二区三区 | 亚洲国产一区二区三区在线观看 |