springboot 啟動分析三

承接上文springboot啟動分析二

上篇文章分析到了SpringApplicationRunListener的starting()方法調(diào)用流程,今天繼續(xù)分析SpringApplication類 run方法的后續(xù)流程

public ConfigurableApplicationContext run(String... args) {

    // 聲明并實例化一個跑表,用于計算springboot應(yīng)用程序啟動時間
    StopWatch stopWatch = new StopWatch();

    // 啟動跑表
    stopWatch.start();

    // 聲明一個應(yīng)用程序上下文,注意這里是一個接口聲明
    ConfigurableApplicationContext context = null;

    // 聲明一個集合,用于記錄springboot異常報告
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();

    // 配置不在意的屬性 java.awt.headless
    configureHeadlessProperty();

    // 獲取用于監(jiān)聽spring應(yīng)用程序run方法的監(jiān)聽器實例
    SpringApplicationRunListeners listeners = getRunListeners(args);

    // 循環(huán)啟動用于run方法的監(jiān)聽器
    listeners.starting();
    try {
        // 封裝應(yīng)用參數(shù)
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);

        // 根據(jù)SpringApplication實例化時候推斷出來的應(yīng)用類型 webApplicationType,
        // 去獲取不同的環(huán)境,然后獲取配置要適用的PropertySource以及激活哪個Profile
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);

        // 根據(jù)環(huán)境配置需要忽略的bean的信息
        configureIgnoreBeanInfo(environment);

        // 根據(jù)環(huán)境配置打印banner,即根據(jù)bannerMode 枚舉值,決定是否打印banner和banner打印的位置
        Banner printedBanner = printBanner(environment);

        // 創(chuàng)建應(yīng)用程序上下文,
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[]{ConfigurableApplicationContext.class}, context);
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    } catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        listeners.running(context);
    } catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

1. ApplicationArguments applicationArguments = new DefaultApplicationArguments(args)

封裝應(yīng)用參數(shù),即封裝命令行傳入springboot應(yīng)用程序的參數(shù)到ApplicationArguments對象中

ApplicationArguments 接口

該接口定義了針對應(yīng)用參數(shù)的一系列操作方法,比如獲取源參數(shù)(即main傳入的參數(shù))方法,獲取操作名集合(比如源參數(shù)為--debug --foo=bar,就會返回["debug","foo"]),是否包含指定操作名方法以及獲取操作命令值方法等

DefaultApplicationArguments 類

該類繼承自ApplicationArguments 接口,實現(xiàn)了相應(yīng)操作main參數(shù)的方法,類內(nèi)部定義了兩個final屬性,source和args

public class DefaultApplicationArguments implements ApplicationArguments {

// 內(nèi)部類Source 繼承 SimpleCommandLinePropertySource
private final Source source;

// main方法傳遞進(jìn)來的參數(shù)
private final String[] args;

// 構(gòu)造方法
public DefaultApplicationArguments(String[] args) {
    Assert.notNull(args, "Args must not be null");
    this.source = new Source(args);
    this.args = args;
}

@Override
public String[] getSourceArgs() {
    return this.args;
}

@Override
public Set<String> getOptionNames() {
    String[] names = this.source.getPropertyNames();
    return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(names)));
}

@Override
public boolean containsOption(String name) {
    return this.source.containsProperty(name);
}

@Override
public List<String> getOptionValues(String name) {
    List<String> values = this.source.getOptionValues(name);
    return (values != null) ? Collections.unmodifiableList(values) : null;
}

@Override
public List<String> getNonOptionArgs() {
    return this.source.getNonOptionArgs();
}

/**
 * DefaultApplicationArguments 類內(nèi)部類,繼承自SimpleCommandLinePropertySource
 * @date 10:53 2018/9/28
 */
private static class Source extends SimpleCommandLinePropertySource {

    Source(String[] args) {
        super(args);
    }

    @Override
    public List<String> getNonOptionArgs() {
        return super.getNonOptionArgs();
    }

    @Override
    public List<String> getOptionValues(String name) {
        return super.getOptionValues(name);
    }

}

}

  • Source source 為內(nèi)部類,繼承自SimpleCommandLinePropertySource
image.png

可以看到SimpleCommandLinePropertySource是PropertySource抽象類的派生類

這里new Source() 調(diào)用了SimpleCommandLinePropertySource的構(gòu)造方法

public SimpleCommandLinePropertySource(String... args) {
    super(new SimpleCommandLineArgsParser().parse(args));
}

這里利用了SimpleCommandLineArgsParser 簡單命令行參數(shù)解析器將mian方法傳入的args參數(shù)解析為CommandLineArgs 對象。為什么需要解析為CommandLineArgs對象,是因為SimpleCommandLineArgsParser類的父類CommandLinePropertySource<CommandLineArgs>是一個泛型抽象類

  • 解析參數(shù)的方法parse

    public CommandLineArgs parse(String... args) {
      CommandLineArgs commandLineArgs = new CommandLineArgs();
      for (String arg : args) {
          if (arg.startsWith("--")) {
              String optionText = arg.substring(2, arg.length());
              String optionName;
              String optionValue = null;
              if (optionText.contains("=")) {
                  optionName = optionText.substring(0, optionText.indexOf('='));
                  optionValue = optionText.substring(optionText.indexOf('=')+1, optionText.length());
              }
              else {
                  optionName = optionText;
              }
              if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) {
                  throw new IllegalArgumentException("Invalid argument syntax: " + arg);
              }
              commandLineArgs.addOptionArg(optionName, optionValue);
          }
          else {
              commandLineArgs.addNonOptionArg(arg);
          }
      }
      return commandLineArgs;
    

    }

就是將args參數(shù)遍歷后,根據(jù)"--" 和"="號分割,取出其optionName 和 optionValue值,存入commandLineArgs 的optionArgs屬性中。

private final Map<String, List<String>> optionArgs = new HashMap<>();

CommandLineArgs對象構(gòu)建成功后,繼續(xù)向上調(diào)用父類的構(gòu)造函數(shù),直到PropertySource抽象類的構(gòu)造

public abstract class CommandLinePropertySource<T> extends EnumerablePropertySource<T> {

/** The default name given to {@link CommandLinePropertySource} instances: {@value}. */
public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs";

/** The default name of the property representing non-option arguments: {@value}. */
public static final String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME = "nonOptionArgs";


private String nonOptionArgsPropertyName = DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME;


/**
 * Create a new {@code CommandLinePropertySource} having the default name
 * {@value #COMMAND_LINE_PROPERTY_SOURCE_NAME} and backed by the given source object.
 */
public CommandLinePropertySource(T source) {
    super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source);
}

}

PropertySource抽象類

spring中,一個用于表述屬性對(name/value)源的抽象類

image.png

即最終的DefaultApplicationArguments 對象中含有一個args參數(shù)數(shù)組和一個name為"commandLineArgs",value為CommandLineArgs 的PropertySource對象

image.png

2. ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);

準(zhǔn)備應(yīng)用環(huán)境,傳入?yún)?shù)一個是SpringApplicationRunListeners ,作用是當(dāng)環(huán)境預(yù)備成功后發(fā)布ApplicationEnvironmentPreparedEvent 事件;第二個參數(shù)是需要根據(jù)應(yīng)用命令行參數(shù)配置應(yīng)用環(huán)境,比如命令行參數(shù)會改變springboot激活哪個配置文件等

  • 預(yù)備環(huán)境

      private ConfigurableEnvironment prepareEnvironment(
          SpringApplicationRunListeners listeners,
          ApplicationArguments applicationArguments) {
      // Create and configure the environment
      // 1.獲取或者創(chuàng)建一個可配置的環(huán)境對象ConfigurableEnvironment
      ConfigurableEnvironment environment = getOrCreateEnvironment();
    
      // 配置環(huán)境
      configureEnvironment(environment, applicationArguments.getSourceArgs());
    
      // 告訴SpringApplicationRunListener 環(huán)境已準(zhǔn)備好,可以做出相應(yīng)的處理了
      listeners.environmentPrepared(environment);
    
      // 將準(zhǔn)備好的環(huán)境綁定到springApplication
      bindToSpringApplication(environment);
      if (!this.isCustomEnvironment) {
          environment = new EnvironmentConverter(getClassLoader())
                  .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
      }
    
      // 將環(huán)境附加到配置屬性源中,name為configurationProperties, value為environment對象中的多個PropertySource對象
      ConfigurationPropertySources.attach(environment);
      return environment;
    

    }

  • 獲取或者創(chuàng)建一個可配置的環(huán)境對象ConfigurableEnvironment

      private ConfigurableEnvironment getOrCreateEnvironment() {
      // 如果SpringApplication已經(jīng)有了環(huán)境對象,直接返回
      if (this.environment != null) {
          return this.environment;
      }
      // 沒有,就根據(jù)webApplicationType類型取判斷實例化哪一個環(huán)境
      if (this.webApplicationType == WebApplicationType.SERVLET) {
          // SERVLET 類型返回StandardServletEnvironment環(huán)境對象
          return new StandardServletEnvironment();
      }
      if (this.webApplicationType == WebApplicationType.REACTIVE) {
          return new StandardReactiveWebEnvironment();
      }
          // 否則就返回標(biāo)準(zhǔn)環(huán)境對象,非web應(yīng)用
      return new StandardEnvironment();
    

    }

StandardServletEnvironment 標(biāo)準(zhǔn)Servlet環(huán)境是 Environment 接口的實現(xiàn)類,用于以Servlet為基礎(chǔ)的web應(yīng)用

image.png

這里注意一下,StandardServletEnvironment類繼承了StandardEnvironment類,StandardEnvironment類又繼承AbstractEnvironment抽象類

這里說一下設(shè)計的原因:由于每一種環(huán)境在初始化時都需要定義自己環(huán)境獨有的一些屬性,那么就有了一個標(biāo)準(zhǔn)JAVA環(huán)境類,該類中會自定義系統(tǒng)屬性以及環(huán)境變量屬性,而Servlet環(huán)境則需要在標(biāo)準(zhǔn)環(huán)境的基礎(chǔ)上增加自己特定的環(huán)境屬性源如Servlet_config 和servlet context等

實現(xiàn)方式:在AbstractEnvironment抽象類中定義了構(gòu)造方法,構(gòu)造器中調(diào)用各個子類覆寫的customizePropertySources(this.propertySources);函數(shù),這樣子類StandardServletEnvironment在實例化new的時候會先調(diào)用父類的構(gòu)造函數(shù),轉(zhuǎn)而調(diào)用自己覆寫的方法實現(xiàn)

  • StandardServletEnvironment 類

      /** Servlet context init parameters property source name: {@value} */
      public static final String     SERVLET_CONTEXT_PROPERTY_SOURCE_NAME =     "servletContextInitParams";
    
      /** Servlet config init parameters property source name:     {@value} */
      public static final String     SERVLET_CONFIG_PROPERTY_SOURCE_NAME =     "servletConfigInitParams";
    
      /** JNDI property source name: {@value} */
      public static final String JNDI_PROPERTY_SOURCE_NAME =     "jndiProperties";
    
      @Override
      protected void customizePropertySources(MutablePropertySources propertySources) {
      //  propertySources 是父類 AbstractEnvironment中的屬性
          propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
      propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
      if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
          propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
      }
      super.customizePropertySources(propertySources);
    

    }

  • super.customizePropertySources(propertySources);

      @Override
      protected void     customizePropertySources(MutablePropertySources propertySources) {
          propertySources.addLast(new     MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_    NAME, getSystemProperties()));
          propertySources.addLast(new     SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROP    ERTY_SOURCE_NAME, getSystemEnvironment()));
      }
    

這樣,當(dāng)我們實例化一個StandardServletEnvironment對象的時候,其實已經(jīng)初始化Servlet環(huán)境默認(rèn)需要的四個屬性源了

getSystemProperties() 內(nèi)部利用System.getProperties()可以獲取到系統(tǒng)屬性,如jdk版本等
getSystemEnvironment() 內(nèi)部利用System.getenv(),可以獲取到系統(tǒng)環(huán)境變量,

  • 配置環(huán)境對象

該方法有兩個參數(shù),一個是上一步中獲取的環(huán)境對象,另一個是ApplicationArguments對象的args屬性,也就是main方法傳入的源String[] args參數(shù)

protected void configureEnvironment(ConfigurableEnvironment environment,
                                    String[] args) {
    // 添加,刪除或重新排序此應(yīng)用環(huán)境的任何propertySources
    configurePropertySources(environment, args);

    // 配置此應(yīng)用程序環(huán)境哪一個配置文件是激活的,只是一個設(shè)置的作用
    configureProfiles(environment, args);
}
  • 告訴SpringApplicationRunListener 環(huán)境已經(jīng)準(zhǔn)備好,這一步是重點

listeners.environmentPrepared(environment);

這個和上一篇博客上分析的關(guān)于starting方法的發(fā)布是一樣的邏輯,只是這里不一樣的是發(fā)布的事件是ApplicationEnvironmentPreparedEvent

這里重點說的是關(guān)心這個事件的監(jiān)聽器

ConfigFileApplicationListener

配置上下文環(huán)境通過從指定位置加載配置文件,默認(rèn)屬性文件被加載從application.properties 或者 application.yml
位置:
classpath:
file:./
classpath:config/
file:./config/:
另外的文件也可以被加載基于激活的profiles,例如如果'dev' profile 被激活,那么application-dev.properties 和 application-dev.yml將會被加載
另外使用spring.config.name可以指定加載配置文件的名字,spring.config.location可以指定加載配置文件的位置

利用觀察者模式將配置文件的加載放在了ConfigFileApplicationListener中處理,解耦了加載的過程

    @Override
    public boolean supportsEventType(Class<? extends     ApplicationEvent> eventType) {
        return     ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventTy    pe)
                ||     ApplicationPreparedEvent.class.isAssignableFrom(eventType);
    }

    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        return true;
    }

可以看到該監(jiān)聽器,支持的事件源類型為所有,事件類型為ApplicationEnvironmentPreparedEvent和ApplicationPreparedEvent

繼續(xù)看其監(jiān)聽到事件的處理方式

@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent(
                (ApplicationEnvironmentPreparedEvent) event);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}

當(dāng)事件是ApplicationEnvironmentPreparedEvent 時調(diào)用私有的onApplicationEnvironmentPreparedEvent方法

繼續(xù)向下看

private void onApplicationEnvironmentPreparedEvent(
        ApplicationEnvironmentPreparedEvent event) {
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    postProcessors.add(this);
    AnnotationAwareOrderComparator.sort(postProcessors);
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(),
                event.getSpringApplication());
    }
}

其中的loadPostProcessors()就是去spring.factories文件中獲取EnvironmentPostProcessor.class作為key對應(yīng)的環(huán)境處理器實例

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor

實例化這三個環(huán)境后置處理器后,將ConfigFileApplicationListener 監(jiān)聽器實例也加入這三個處理器之后,排序后再進(jìn)行循環(huán)調(diào)用各自的postProcessEnvironment方法進(jìn)行處理。

下面一一述說這四個環(huán)境后置處理器各自做了什么事情

  1. SystemEnvironmentPropertySourceEnvironmentPostProcessor
    系統(tǒng)環(huán)境變量屬性源后置處理器

     @Override
     public void postProcessEnvironment(ConfigurableEnvironment environment,
         SpringApplication application) {
     String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
     PropertySource<?> propertySource = environment.getPropertySources()
             .get(sourceName);
         // 從已有的環(huán)境中獲取到name為systemEnvironment的系統(tǒng)環(huán)境變量屬性源,判斷是否為空,不為空就執(zhí)行if里面的replacePropertySource方法
     if (propertySource != null) {
         replacePropertySource(environment, sourceName, propertySource);
     }
     }
    

繼續(xù)看替換屬性源的方法

image.png

實際上就是將原來的系統(tǒng)環(huán)境變量屬性源對象換成了OriginAwareSystemEnvironmentPropertySource對象,內(nèi)部的source沒有變化

  1. SpringApplicationJsonEnvironmentPostProcessor
    spring應(yīng)用Json環(huán)境后置處理器

該處理器就是用來從spring.application.json解析出JSON格式的配置屬性,再添加進(jìn)enviroment中,key為"spring.application.json",source為Map<String, Object>,這個新的屬性比系統(tǒng)的屬性system properties優(yōu)先

  1. CloudFoundryVcapEnvironmentPostProcessor

貌似是一個關(guān)于云平臺的環(huán)境后置處理器,執(zhí)行處理邏輯時先判斷上下文環(huán)境是否時云平臺

判斷依據(jù)是環(huán)境變量中是否包含屬性“VCAP_APPLICATION”或者“VCAP_SERVICES”,不包含不做處理

  1. ConfigFileApplicationListener

最后一個也是最重要的一個環(huán)境后置處理器,重點分析它到底做了什么

public void postProcessEnvironment(ConfigurableEnvironment environment,
        SpringApplication application) {
     // 增加配置文件屬性到特定的環(huán)境中,參數(shù)一為上下文環(huán)境,參數(shù)二為spring應(yīng)用類的資源加載器,這里默認(rèn)為null
    addPropertySources(environment, application.getResourceLoader());
}

表明了springboot是利用ConfigFileApplicationListener將項目中的配置文件屬性添加到上下文環(huán)境中的

protected void addPropertySources(ConfigurableEnvironment environment,
        ResourceLoader resourceLoader) {
    // 處理配置文件中以"random.XX"的隨機數(shù),生成隨機值后加入到環(huán)境中
    RandomValuePropertySource.addToEnvironment(environment);
    // new 一個加載器加載配置文件中的所有屬性到環(huán)境中
    new Loader(environment, resourceLoader).load();
}
image.png

可以看到在environment中的propertySources屬性的propertySourceList中多了name為"random"的隨機值PropertySource

Loader 為ConfigFileApplicationListener 的內(nèi)部類,用于加載候選的屬性源以及配置激活文件

// 構(gòu)造函數(shù)傳入了上下文環(huán)境,以及一個資源加載器
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        this.environment = environment;
        // 資源加載器為null就new 一個默認(rèn)的資源加載器
        this.resourceLoader = (resourceLoader != null) ? resourceLoader
                : new DefaultResourceLoader();
        // 屬性源加載器則是從spring.factories文件中獲取key為PropertySourceLoader的實現(xiàn)類
        this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
                PropertySourceLoader.class, getClass().getClassLoader());
    }

這里貼一下springboot的META-INF下的sprin.factories

org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

會初始化兩個PropertySourceLoader接口的實現(xiàn)類,一個PropertiesPropertySourceLoader,一個YamlPropertySourceLoader,分別對應(yīng)加載properties或xml結(jié)尾的文件資源,和yml或yaml結(jié)尾的文件資源

下面分析一下Load類的load()加載方法

public void load() {
        this.profiles = new LinkedList<>();
        this.processedProfiles = new LinkedList<>();
        this.activatedProfiles = false;
        this.loaded = new LinkedHashMap<>();
        // key 1 初始化profile配置
        initializeProfiles();
        while (!this.profiles.isEmpty()) {
            Profile profile = this.profiles.poll();
                // 判斷激活profile不是null,且不是默認(rèn)profile
            if (profile != null && !profile.isDefaultProfile()) {
                addProfileToEnvironment(profile.getName());
            }
            // 加載激活的profile對應(yīng)的配置
            load(profile, this::getPositiveProfileFilter,
                    addToLoaded(MutablePropertySources::addLast, false));
            this.processedProfiles.add(profile);
        }
        // 加載默認(rèn)配置
        load(null, this::getNegativeProfileFilter,
                addToLoaded(MutablePropertySources::addFirst, true));
        // key2 將已經(jīng)加載的配置屬性添加到environmenth中
        addLoadedPropertySources();
    }

key1 從environment的激活profiles中初始化profile信息

private void initializeProfiles() {
        // The default profile for these purposes is represented as null. We add it
        // first so that it is processed first and has lowest priority.
        this.profiles.add(null);
        // 從環(huán)境中獲取激活的profile
        Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
        this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
        // Any pre-existing active profiles set via property sources (e.g.
        // System properties) take precedence over those added in config files.
        addActiveProfiles(activatedViaProperty);
        if (this.profiles.size() == 1) { // only has null profile
            for (String defaultProfileName : this.environment.getDefaultProfiles()) {
                Profile defaultProfile = new Profile(defaultProfileName, true);
                this.profiles.add(defaultProfile);
            }
        }
    }

key2 將已經(jīng)加載的配置屬性添加到environmenth中

image.png

至此,通過ConfigFileApplicationListener 就可以將所有的應(yīng)用配置文件中的屬性添加到environment中了
以后有事件會細(xì)分析一下這里的配置文件屬性到底怎么解析的


結(jié)尾

environment環(huán)境準(zhǔn)備好的通知事件已經(jīng)處理完畢,接下來的文章會分析關(guān)于applicationContext的創(chuàng)建以及run方法后續(xù)的執(zhí)行流程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,568評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,273評論 6 342
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,942評論 1 92
  • 未來的某年某月某一天 當(dāng)我打開哪些關(guān)于我的過往時 我可能是一個人 但我很希望有人和我共老 愿歲月可回首且以情深待今...
    深珄閱讀 179評論 0 0
  • 在terminal里面執(zhí)行Python代碼
    Foreally閱讀 254評論 0 0

友情鏈接更多精彩內(nèi)容