SpringBoot源码解析(三)IOC容器环境准备

1,357 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

接下来让我们进入SpringApplication的run方法

{
   //1 创建StopWatch对象
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
   //2 设置Headless模式
   configureHeadlessProperty();
   //3 创建事件发布器
   SpringApplicationRunListeners listeners = getRunListeners(args);
   listeners.starting();
   try {
      // 将main方法的args参数封装到一个对象中
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      //4 准备容器环境信息
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
      configureIgnoreBeanInfo(environment);
      //5.打印Banner
      Banner printedBanner = printBanner(environment);
      //6.创建IOC容器和Bean工厂
      context = createApplicationContext();
      //7.初始化异常报告器
      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;
}

首先我们先看第一步

一、创建计时器

StopWatch是Spring框架的工具类,通过它可方便的对程序部分代码进行计时(ms级别),适用于同步单线程代码块

用法

public static void main(String[] args) throws InterruptedException {
     StopWatchTest.testStopWatch();
}

public static void testStopWatch() throws InterruptedException {
     StopWatch sw = new StopWatch("testWatch");
     sw.start("TaskA");
     // do something
    Thread.sleep(100);
    sw.stop();
    sw.start("TaskB");
    // do something
    Thread.sleep(200);
    sw.stop();
    System.out.println("sw.prettyPrint()~~~~~~~~~~~~~~~~~");
    System.out.println(sw.prettyPrint());
}

start开始记录,stop停止记录,然后通过StopWatch的prettyPrint方法,可直观的输出代码执行耗时,以及执行时间百分比

运行结果

运行结果:
sw.prettyPrint()~~~~~~~~~~~~~~~~~
StopWatch 'testWatch': running time (millis) = 308
-----------------------------------------
ms     %     Task name
-----------------------------------------
00104  034%  TaskA
00204  066%  TaskB

Springboot在使用计时器记录了容器启动前后的消费时间

image.png

二、设置Headless模式

Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。我们的项目运行大多数是运行在linux操作系统下的,是没有显示器的,SpringBootSpringBoot其实是想设置该应用程序,即使没有检测到显示器,也允许其启动

private void configureHeadlessProperty() {
   System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
         SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}

三、创建事件发布器

我们进入getRunListeners(args)方法中

private SpringApplicationRunListeners getRunListeners(String[] args) {
   //使用Spring的Spi机制获取SpringApplicationRunListener的实现类
   Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
   return new SpringApplicationRunListeners(logger,
         getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

通过Spi机制获取META-INF/spring.factories下所有SpringApplicationRunListener实现类

SpringApplicationRunListener 接口的作用主要就是在Spring Boot 启动初始化的过程中可以通过SpringApplicationRunListener接口回调来让用户在启动的各个流程中可以加入自己的逻辑。 Spring Boot启动过程的关键事件(按照触发顺序)包括:

  • 开始启动

  • Environment构建完成

  • ApplicationContext构建完成

  • ApplicationContext完成加载

  • ApplicationContext完成刷新并启动

  • 启动完成

  • 启动失败

下面是SpringApplicationRunListener接口定义:

public interface SpringApplicationRunListener {

    // 在run()方法开始执行时,该方法就立即被调用,可用于在初始化最早期时做一些工作
    void starting();
    // 当environment构建完成,ApplicationContext创建之前,该方法被调用
    void environmentPrepared(ConfigurableEnvironment environment);
    // 当ApplicationContext构建完成时,该方法被调用
    void contextPrepared(ConfigurableApplicationContext context);
    // 在ApplicationContext完成加载,但没有被刷新前,该方法被调用
    void contextLoaded(ConfigurableApplicationContext context);
    // 在ApplicationContext刷新并启动后,CommandLineRunners和ApplicationRunner未被调用前,该方法被调用
    void started(ConfigurableApplicationContext context);
    // 在run()方法执行完成前该方法被调用
    void running(ConfigurableApplicationContext context);
    // 当应用运行出错时该方法被调用
    void failed(ConfigurableApplicationContext context, Throwable exception);
}

Spirngboot中默认创建了一个实现类EventPublishingRunListener

image.png SpringApplicationRunListeners的starting()方法中调用了所有的监听器的starting()方法;

void starting() {
   for (SpringApplicationRunListener listener : this.listeners) {
      listener.starting();
   }
}

接下来进入EventPublishingRunListener

3.1、EventPublishingRunListener

EventPublishingRunListener是Springboot为我们提供的一套事件发布机制,他会在容器的启动的各个阶段发布对应的事件,我们可以实各个事件的监听器,拿到容器对应阶段的信息,完成自定义逻辑。

接下来看看EventPublishingRunListener是怎么完成事件的发布的

首先看这个类的构造器,这里的application和listener是在源码二启动时创建的。然后会创建一个Spring的事件发布器,并将所有监听器放入其中

public EventPublishingRunListener(SpringApplication application, String[] args) {
   this.application = application;
   this.args = args;
   this.initialMulticaster = new SimpleApplicationEventMulticaster();
   for (ApplicationListener<?> listener : application.getListeners()) {
      this.initialMulticaster.addApplicationListener(listener);
   }
}

接下来进入starting()方法

@Override
public void starting() {
   //发布容器启动事件
   this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}

第一步调用了SimpleApplicationEventMulticaster类的multicastEvent方法并且传入了ApplicationStartingEvent对象。

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            Executor executor = getTaskExecutor();
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                invokeListener(listener, event);
            }
 **       }
    }

紧接着遍历了每个事件是ApplicationStartingEvent的监听器,最后进行回调



    @SuppressWarnings({"unchecked", "rawtypes"})
    private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
        try {
            listener.onApplicationEvent(event);
        }
        catch (ClassCastException ex) {
            String msg = ex.getMessage();
            if (msg == null || matchesClassCastMessage(msg, event.getClass().getName())) {
                // Possibly a lambda-defined listener which we could not resolve the generic event type for
                // -> let's suppress the exception and just log a debug message.
                Log logger = LogFactory.getLog(getClass());
                if (logger.isDebugEnabled()) {
                    logger.debug("Non-matching event type for listener: " + listener, ex);
                }
            }
            else {
                throw ex;
            }**
        }
    }

所以这里就是发布了一个容器启动事件ApplicationStartedEvent

四、准备容器环境信息

获取IOC容器运行时环境信息

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
      ApplicationArguments applicationArguments) {
   //4.1 创建运行时环境
   ConfigurableEnvironment environment = getOrCreateEnvironment();
   //4.2 配置运行时环境
   configureEnvironment(environment, applicationArguments.getSourceArgs());
   ConfigurationPropertySources.attach(environment);
   //4.3 发布容器环境准备就绪事件
   listeners.environmentPrepared(environment);
   //4.4 环境与应用绑定
   bindToSpringApplication(environment);
   if (!this.isCustomEnvironment) {
      environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
            deduceEnvironmentClass());
   }
   ConfigurationPropertySources.attach(environment);
   return environment;
}

4.1 创建运行时环境

根据不同的容器信息创建不同的环境载体

private ConfigurableEnvironment getOrCreateEnvironment() {
   if (this.environment != null) {
      return this.environment;
   }
   switch (this.webApplicationType) {
   case SERVLET:
      return new StandardServletEnvironment();
   case REACTIVE:
      return new StandardReactiveWebEnvironment();
   default:
      return new StandardEnvironment();
   }
}

我们一般用Tomcat容器这里我们看看StandardServletEnvironment

StandardServletEnvironment继承了AbstractEnvironment所以在初始化构造方法时候会默认调用父类构造器

public AbstractEnvironment() {
   customizePropertySources(this.propertySources);
}

加载自定义数据源,这里在数据源中加入了Sevlet配置和上下文信息,并且调用了父类StandardEnvironment的加载数据源方法

protected void customizePropertySources(MutablePropertySources propertySources) {
   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);
}

这里将系统的配置和环境信息放入到数据源中

@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
   propertySources.addLast(
         new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
   propertySources.addLast(
         new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}

image.png

image.png

4.2配置环境信息

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
   if (this.addConversionService) {
      //获取装换器
      ConversionService conversionService = ApplicationConversionService.getSharedInstance();
      //设置转换器
      environment.setConversionService((ConfigurableConversionService) conversionService);
   }
   //配置PropertySource
   configurePropertySources(environment, args);
   //配置Profiles
   configureProfiles(environment, args);
}

SpringBoot会默认创建ApplicationConversionService转换器

public static ConversionService getSharedInstance() {
   ApplicationConversionService sharedInstance = ApplicationConversionService.sharedInstance;
   if (sharedInstance == null) {
      synchronized (ApplicationConversionService.class) {
         sharedInstance = ApplicationConversionService.sharedInstance;
         if (sharedInstance == null) {
            sharedInstance = new ApplicationConversionService();
            ApplicationConversionService.sharedInstance = sharedInstance;
         }
      }
   }
   return sharedInstance;
}

ApplicationConversionService在创建时候会添加各种类型转换器,例如String,Number等

image.png 最后会将刚刚获取到的系统配置和环境信息放入配置源中,然后设置PropertySources和Profiles

4.3 发布容器环境准备就绪事件

接下来会发布容器环境已经好的容器事件ApplicationEnvironmentPreparedEvent

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
   this.initialMulticaster
         .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

这里ConfigFileApplicationListener监听器接收到ApplicationEnvironmentPreparedEvent会将配置文件中的信息放入数据源中

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
   addPropertySources(environment, application.getResourceLoader());
}

加载配置文件

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
   RandomValuePropertySource.addToEnvironment(environment);
   new Loader(environment, resourceLoader).load();
}
void load() {
   FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
         (defaultProperties) -> {
            this.profiles = new LinkedList<>();
            this.processedProfiles = new LinkedList<>();
            this.activatedProfiles = false;
            this.loaded = new LinkedHashMap<>();
            initializeProfiles();
            while (!this.profiles.isEmpty()) {
               Profile profile = this.profiles.poll();
               if (isDefaultProfile(profile)) {
                  addProfileToEnvironment(profile.getName());
               }
               load(profile, this::getPositiveProfileFilter,
                     addToLoaded(MutablePropertySources::addLast, false));
               this.processedProfiles.add(profile);
            }
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
            addLoadedPropertySources();
            applyActiveProfiles(defaultProperties);
         });
}

定义在这些目录下的配置文件下都会被加载到环境当中

  1. classpath:/
  2. classpath:/config/
  3. file:./
  4. file:./config/*/
  5. file:./config/
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
      DocumentConsumer consumer) {
   Resource[] resources = getResources(location);
   for (Resource resource : resources) {
      try {
         if (resource == null || !resource.exists()) {
            if (this.logger.isTraceEnabled()) {
               StringBuilder description = getDescription("Skipped missing config ", location, resource,
                     profile);
               this.logger.trace(description);
            }
            continue;
         }
         if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
            if (this.logger.isTraceEnabled()) {
               StringBuilder description = getDescription("Skipped empty config extension ", location,
                     resource, profile);
               this.logger.trace(description);
            }
            continue;
         }
         if (resource.isFile() && hasHiddenPathElement(resource)) {
            if (this.logger.isTraceEnabled()) {
               StringBuilder description = getDescription("Skipped location with hidden path element ",
                     location, resource, profile);
               this.logger.trace(description);
            }
            continue;
         }
         String name = "applicationConfig: [" + getLocationName(location, resource) + "]";
         List<Document> documents = loadDocuments(loader, name, resource);
         if (CollectionUtils.isEmpty(documents)) {
            if (this.logger.isTraceEnabled()) {
               StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
                     profile);
               this.logger.trace(description);
            }
            continue;
         }
         List<Document> loaded = new ArrayList<>();
         for (Document document : documents) {
            if (filter.match(document)) {
               addActiveProfiles(document.getActiveProfiles());
               addIncludedProfiles(document.getIncludeProfiles());
               loaded.add(document);
            }
         }
         Collections.reverse(loaded);
         if (!loaded.isEmpty()) {
            loaded.forEach((document) -> consumer.accept(profile, document));
            if (this.logger.isDebugEnabled()) {
               StringBuilder description = getDescription("Loaded config file ", location, resource,
                     profile);
               this.logger.debug(description);
            }
         }
      }
      catch (Exception ex) {
         StringBuilder description = getDescription("Failed to load property source from ", location,
               resource, profile);
         throw new IllegalStateException(description.toString(), ex);
      }
   }
}

4.4 绑定容器环境

就是把配置内容绑定到指定的属性配置类中

protected void bindToSpringApplication(ConfigurableEnvironment environment) {
   try {
      Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
   }
   catch (Exception ex) {
      throw new IllegalStateException("Cannot bind to SpringApplication", ex);
   }
}

五、打印Banner

Banner就是我们启动SpringBoot容器时候控制台打印的图标

image.png

private Banner printBanner(ConfigurableEnvironment environment) {
   if (this.bannerMode == Banner.Mode.OFF) {
      return null;
   }
   //5.1获取资源加载器
   ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
         : new DefaultResourceLoader(null);
   SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
   if (this.bannerMode == Mode.LOG) {
      return bannerPrinter.print(environment, this.mainApplicationClass, logger);
   }
   //5.2输出Banner    
   return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}

5.1ResourceLoader

用于加载资源(例如类路径或文件系统资源)的策略接口

主要就是借助URL类来加载资源

@Override
public Resource getResource(String location) {
   Assert.notNull(location, "Location must not be null");

   for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
      Resource resource = protocolResolver.resolve(location, this);
      if (resource != null) {
         return resource;
      }
   }

   if (location.startsWith("/")) {
      return getResourceByPath(location);
   }
   else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
      return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
   }
   else {
      try {
         // Try to parse the location as a URL...
         URL url = new URL(location);
         return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
      }
      catch (MalformedURLException ex) {
         // No URL -> resolve as resource path.
         return getResourceByPath(location);
      }
   }
}

5.2 输出Banner

打印Banner之前会先获取Banner

Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
   Banner banner = getBanner(environment);
   banner.printBanner(environment, sourceClass, out);
   return new PrintedBanner(banner, sourceClass);
}

获取Banner

private Banner getBanner(Environment environment) {
   Banners banners = new Banners();
   //5.2.1获取图片
   banners.addIfNotNull(getImageBanner(environment));
   //5.2.2获取文本
   banners.addIfNotNull(getTextBanner(environment));
   if (banners.hasAtLeastOneBanner()) {
      return banners;
   }
   if (this.fallbackBanner != null) {
      return this.fallbackBanner;
   }
   //5.2.3返回默认的Banner
   return DEFAULT_BANNER;
}

5.2.1获取图片Banner

获取资源路径下Banner图片

private Banner getImageBanner(Environment environment) {
   //获取Banner图片地址spring.banner.image.location
   String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
   if (StringUtils.hasLength(location)) {
      Resource resource = this.resourceLoader.getResource(location);
      return resource.exists() ? new ImageBanner(resource) : null;
   }
   //获取Resouces下,以banner.gif, banner.jpg, banner.png的图片
   for (String ext : IMAGE_EXTENSION) {
      Resource resource = this.resourceLoader.getResource("banner." + ext);
      if (resource.exists()) {
         return new ImageBanner(resource);
      }
   }
   return null;
}

5.2.2获取文本Banner

获取资源路径下Banner文本

private Banner getTextBanner(Environment environment) {
    //获取文本Banner地址spring.banner.image.location
   String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
   //加载文本Banner(默认加载Resources下的banner.txt)
   Resource resource = this.resourceLoader.getResource(location);
   if (resource.exists()) {
      return new ResourceBanner(resource);
   }
   return null;
}

5.2.3获取默认Banner

SpringBoot的默认Banner打印版本信息和启动类

public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
    String[] var4 = BANNER;
    int var5 = var4.length;

    for(int var6 = 0; var6 < var5; ++var6) {
        String line = var4[var6];
        printStream.println(line);
    }

    String version = SpringBootVersion.getVersion();
    version = version != null ? " (v" + version + ")" : "";
    StringBuilder padding = new StringBuilder();

    while(padding.length() < 42 - (version.length() + " :: Spring Boot :: ".length())) {
        padding.append(" ");
    }

    printStream.println(AnsiOutput.toString(new Object[]{AnsiColor.GREEN, " :: Spring Boot :: ", AnsiColor.DEFAULT, padding.toString(), AnsiStyle.FAINT, version}));
    printStream.println();
}

六、创建IOC容器和Bean工厂

public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."+ "annotation.AnnotationConfigApplicationContext";
                        
public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."+"web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
                        
public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."+"boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";
                        
protected ConfigurableApplicationContext createApplicationContext() {
   Class<?> contextClass = this.applicationContextClass;
   if (contextClass == null) {
      try {
         switch (this.webApplicationType) {
         case SERVLET:
            contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
            break;
         case REACTIVE:
            contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
            break;
         default:
            contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
         }
      }
      catch (ClassNotFoundException ex) {
         throw new IllegalStateException(
               "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
      }
   }
   return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

SpringBoot都是基于注解创建的Ioc容器 (如果是非Web环境,创建的 ApplicationContext 与常规用 SpringFramework 时使用的注解驱动IOC容器一致)

Tomcat环境下会创建AnnotationConfigServletWebServerApplicationContext作为IOC容器,需要注意的是Bean工厂(DefaultListableBeanFactory)在GenericApplicationContext父类构造器中就会被创建

public GenericApplicationContext() {
    this.customClassLoader = false;
    this.refreshed = new AtomicBoolean();
    this.beanFactory = new DefaultListableBeanFactory();
}

并且BeanDefinition读取器和扫描器都会被创建出来

public AnnotationConfigServletWebServerApplicationContext() {
   this.reader = new AnnotatedBeanDefinitionReader(this);
   this.scanner = new ClassPathBeanDefinitionScanner(this);
}

七、初始化异常报告器

这里会创建SpirngBoot出现异常分析器,用于收集错误信息,向用户报告错误原因。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
   ClassLoader classLoader = getClassLoader();
   // Use names and ensure unique to protect against duplicates
   Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
   List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
   AnnotationAwareOrderComparator.sort(instances);
   return instances;
}

总结

  1. 创建了事件发布器SpringApplicationRunListeners

  2. 创建了运行环境并向其中那个添加了系统配置和环境信息

  3. 发布了两个事件

    • ApplicationStartingEvent(容器启动事件)
    • ApplicationEnvironmentPreparedEvent (环境准备事件)
  4. 创建IOC容器AnnotationConfigServletWebServerApplicationContext并且创建了默认的Bean工厂DefaultListableBeanFactory,AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner

ConfigFileApplicationListener监听器在在接受ApplicationEnvironmentPreparedEvent事件后通过执行器将Reources下的配置文件里的配置信息添加到运行环境中

image.png