springboot 启动分析三

承接上文springboot启动分析二

上篇文章分析到了SpringApplicationRunListener的starting()方法调用流程,今天继续分析SpringApplication类 run方法的后续流程

public ConfigurableApplicationContext run(String... args) {

    // 声明并实例化一个跑表,用于计算springboot应用程序启动时间
    StopWatch stopWatch = new StopWatch();

    // 启动跑表
    stopWatch.start();

    // 声明一个应用程序上下文,注意这里是一个接口声明
    ConfigurableApplicationContext context = null;

    // 声明一个集合,用于记录springboot异常报告
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();

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

    // 获取用于监听spring应用程序run方法的监听器实例
    SpringApplicationRunListeners listeners = getRunListeners(args);

    // 循环启动用于run方法的监听器
    listeners.starting();
    try {
        // 封装应用参数
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);

        // 根据SpringApplication实例化时候推断出来的应用类型 webApplicationType,
        // 去获取不同的环境,然后获取配置要适用的PropertySource以及激活哪个Profile
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);

        // 根据环境配置需要忽略的bean的信息
        configureIgnoreBeanInfo(environment);

        // 根据环境配置打印banner,即根据bannerMode 枚举值,决定是否打印banner和banner打印的位置
        Banner printedBanner = printBanner(environment);

        // 创建应用程序上下文,
        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)

封装应用参数,即封装命令行传入springboot应用程序的参数到ApplicationArguments对象中

ApplicationArguments 接口

该接口定义了针对应用参数的一系列操作方法,比如获取源参数(即main传入的参数)方法,获取操作名集合(比如源参数为--debug --foo=bar,就会返回["debug","foo"]),是否包含指定操作名方法以及获取操作命令值方法等

DefaultApplicationArguments 类

该类继承自ApplicationArguments 接口,实现了相应操作main参数的方法,类内部定义了两个final属性,source和args

public class DefaultApplicationArguments implements ApplicationArguments {

// 内部类Source 继承 SimpleCommandLinePropertySource
private final Source source;

// main方法传递进来的参数
private final String[] args;

// 构造方法
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 类内部类,继承自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 为内部类,继承自SimpleCommandLinePropertySource
image.png

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

这里new Source() 调用了SimpleCommandLinePropertySource的构造方法

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

这里利用了SimpleCommandLineArgsParser 简单命令行参数解析器将mian方法传入的args参数解析为CommandLineArgs 对象。为什么需要解析为CommandLineArgs对象,是因为SimpleCommandLineArgsParser类的父类CommandLinePropertySource<CommandLineArgs>是一个泛型抽象类

  • 解析参数的方法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参数遍历后,根据"--" 和"="号分割,取出其optionName 和 optionValue值,存入commandLineArgs 的optionArgs属性中。

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

CommandLineArgs对象构建成功后,继续向上调用父类的构造函数,直到PropertySource抽象类的构造

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参数数组和一个name为"commandLineArgs",value为CommandLineArgs 的PropertySource对象

image.png

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

准备应用环境,传入参数一个是SpringApplicationRunListeners ,作用是当环境预备成功后发布ApplicationEnvironmentPreparedEvent 事件;第二个参数是需要根据应用命令行参数配置应用环境,比如命令行参数会改变springboot激活哪个配置文件等

  • 预备环境

      private ConfigurableEnvironment prepareEnvironment(
          SpringApplicationRunListeners listeners,
          ApplicationArguments applicationArguments) {
      // Create and configure the environment
      // 1.获取或者创建一个可配置的环境对象ConfigurableEnvironment
      ConfigurableEnvironment environment = getOrCreateEnvironment();
    
      // 配置环境
      configureEnvironment(environment, applicationArguments.getSourceArgs());
    
      // 告诉SpringApplicationRunListener 环境已准备好,可以做出相应的处理了
      listeners.environmentPrepared(environment);
    
      // 将准备好的环境绑定到springApplication
      bindToSpringApplication(environment);
      if (!this.isCustomEnvironment) {
          environment = new EnvironmentConverter(getClassLoader())
                  .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
      }
    
      // 将环境附加到配置属性源中,name为configurationProperties, value为environment对象中的多个PropertySource对象
      ConfigurationPropertySources.attach(environment);
      return environment;
    

    }

  • 获取或者创建一个可配置的环境对象ConfigurableEnvironment

      private ConfigurableEnvironment getOrCreateEnvironment() {
      // 如果SpringApplication已经有了环境对象,直接返回
      if (this.environment != null) {
          return this.environment;
      }
      // 没有,就根据webApplicationType类型取判断实例化哪一个环境
      if (this.webApplicationType == WebApplicationType.SERVLET) {
          // SERVLET 类型返回StandardServletEnvironment环境对象
          return new StandardServletEnvironment();
      }
      if (this.webApplicationType == WebApplicationType.REACTIVE) {
          return new StandardReactiveWebEnvironment();
      }
          // 否则就返回标准环境对象,非web应用
      return new StandardEnvironment();
    

    }

StandardServletEnvironment 标准Servlet环境是 Environment 接口的实现类,用于以Servlet为基础的web应用

image.png

这里注意一下,StandardServletEnvironment类继承了StandardEnvironment类,StandardEnvironment类又继承AbstractEnvironment抽象类

这里说一下设计的原因:由于每一种环境在初始化时都需要定义自己环境独有的一些属性,那么就有了一个标准JAVA环境类,该类中会自定义系统属性以及环境变量属性,而Servlet环境则需要在标准环境的基础上增加自己特定的环境属性源如Servlet_config 和servlet context等

实现方式:在AbstractEnvironment抽象类中定义了构造方法,构造器中调用各个子类覆写的customizePropertySources(this.propertySources);函数,这样子类StandardServletEnvironment在实例化new的时候会先调用父类的构造函数,转而调用自己覆写的方法实现

  • 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()));
      }
    

这样,当我们实例化一个StandardServletEnvironment对象的时候,其实已经初始化Servlet环境默认需要的四个属性源了

getSystemProperties() 内部利用System.getProperties()可以获取到系统属性,如jdk版本等
getSystemEnvironment() 内部利用System.getenv(),可以获取到系统环境变量,

  • 配置环境对象

该方法有两个参数,一个是上一步中获取的环境对象,另一个是ApplicationArguments对象的args属性,也就是main方法传入的源String[] args参数

protected void configureEnvironment(ConfigurableEnvironment environment,
                                    String[] args) {
    // 添加,删除或重新排序此应用环境的任何propertySources
    configurePropertySources(environment, args);

    // 配置此应用程序环境哪一个配置文件是激活的,只是一个设置的作用
    configureProfiles(environment, args);
}
  • 告诉SpringApplicationRunListener 环境已经准备好,这一步是重点

listeners.environmentPrepared(environment);

这个和上一篇博客上分析的关于starting方法的发布是一样的逻辑,只是这里不一样的是发布的事件是ApplicationEnvironmentPreparedEvent

这里重点说的是关心这个事件的监听器

ConfigFileApplicationListener

配置上下文环境通过从指定位置加载配置文件,默认属性文件被加载从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;
    }

可以看到该监听器,支持的事件源类型为所有,事件类型为ApplicationEnvironmentPreparedEvent和ApplicationPreparedEvent

继续看其监听到事件的处理方式

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

当事件是ApplicationEnvironmentPreparedEvent 时调用私有的onApplicationEnvironmentPreparedEvent方法

继续向下看

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对应的环境处理器实例

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

实例化这三个环境后置处理器后,将ConfigFileApplicationListener 监听器实例也加入这三个处理器之后,排序后再进行循环调用各自的postProcessEnvironment方法进行处理。

下面一一述说这四个环境后置处理器各自做了什么事情

  1. SystemEnvironmentPropertySourceEnvironmentPostProcessor
    系统环境变量属性源后置处理器

     @Override
     public void postProcessEnvironment(ConfigurableEnvironment environment,
         SpringApplication application) {
     String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
     PropertySource<?> propertySource = environment.getPropertySources()
             .get(sourceName);
         // 从已有的环境中获取到name为systemEnvironment的系统环境变量属性源,判断是否为空,不为空就执行if里面的replacePropertySource方法
     if (propertySource != null) {
         replacePropertySource(environment, sourceName, propertySource);
     }
     }
    

继续看替换属性源的方法

image.png

实际上就是将原来的系统环境变量属性源对象换成了OriginAwareSystemEnvironmentPropertySource对象,内部的source没有变化

  1. SpringApplicationJsonEnvironmentPostProcessor
    spring应用Json环境后置处理器

该处理器就是用来从spring.application.json解析出JSON格式的配置属性,再添加进enviroment中,key为"spring.application.json",source为Map<String, Object>,这个新的属性比系统的属性system properties优先

  1. CloudFoundryVcapEnvironmentPostProcessor

貌似是一个关于云平台的环境后置处理器,执行处理逻辑时先判断上下文环境是否时云平台

判断依据是环境变量中是否包含属性“VCAP_APPLICATION”或者“VCAP_SERVICES”,不包含不做处理

  1. ConfigFileApplicationListener

最后一个也是最重要的一个环境后置处理器,重点分析它到底做了什么

public void postProcessEnvironment(ConfigurableEnvironment environment,
        SpringApplication application) {
     // 增加配置文件属性到特定的环境中,参数一为上下文环境,参数二为spring应用类的资源加载器,这里默认为null
    addPropertySources(environment, application.getResourceLoader());
}

表明了springboot是利用ConfigFileApplicationListener将项目中的配置文件属性添加到上下文环境中的

protected void addPropertySources(ConfigurableEnvironment environment,
        ResourceLoader resourceLoader) {
    // 处理配置文件中以"random.XX"的随机数,生成随机值后加入到环境中
    RandomValuePropertySource.addToEnvironment(environment);
    // new 一个加载器加载配置文件中的所有属性到环境中
    new Loader(environment, resourceLoader).load();
}
image.png

可以看到在environment中的propertySources属性的propertySourceList中多了name为"random"的随机值PropertySource

Loader 为ConfigFileApplicationListener 的内部类,用于加载候选的属性源以及配置激活文件

// 构造函数传入了上下文环境,以及一个资源加载器
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        this.environment = environment;
        // 资源加载器为null就new 一个默认的资源加载器
        this.resourceLoader = (resourceLoader != null) ? resourceLoader
                : new DefaultResourceLoader();
        // 属性源加载器则是从spring.factories文件中获取key为PropertySourceLoader的实现类
        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接口的实现类,一个PropertiesPropertySourceLoader,一个YamlPropertySourceLoader,分别对应加载properties或xml结尾的文件资源,和yml或yaml结尾的文件资源

下面分析一下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,且不是默认profile
            if (profile != null && !profile.isDefaultProfile()) {
                addProfileToEnvironment(profile.getName());
            }
            // 加载激活的profile对应的配置
            load(profile, this::getPositiveProfileFilter,
                    addToLoaded(MutablePropertySources::addLast, false));
            this.processedProfiles.add(profile);
        }
        // 加载默认配置
        load(null, this::getNegativeProfileFilter,
                addToLoaded(MutablePropertySources::addFirst, true));
        // key2 将已经加载的配置属性添加到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);
        // 从环境中获取激活的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 将已经加载的配置属性添加到environmenth中

image.png

至此,通过ConfigFileApplicationListener 就可以将所有的应用配置文件中的属性添加到environment中了
以后有事件会细分析一下这里的配置文件属性到底怎么解析的


结尾

environment环境准备好的通知事件已经处理完毕,接下来的文章会分析关于applicationContext的创建以及run方法后续的执行流程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,629评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,773评论 6 342
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,353评论 1 92
  • 未来的某年某月某一天 当我打开哪些关于我的过往时 我可能是一个人 但我很希望有人和我共老 愿岁月可回首且以情深待今...
    深珄阅读 161评论 0 0
  • 在terminal里面执行Python代码
    Foreally阅读 235评论 0 0