【死磕-springboot第三篇】prepareEnvironment

【死磕-第三篇】prepareEnvironment


导读

  • 上一篇讲述了,获取运行监听器,然后调用starting方法,干了些什么
  • 这一篇主要讲springApplication.run这个流程中准备环境的这个过程

1. 源码详解

/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * 翻译:运行一个spring程序,创建并且刷新一个新的ApplicationContext
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
    // 创建一个计时器
		StopWatch stopWatch = new StopWatch();
    // 开始计时
		stopWatch.start();
    // 创建一个可以配置的ConfigurableApplicationContext 变量
		ConfigurableApplicationContext context = null;
    // 创建一个异常报告集合
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 配置Headless属性 (这个就不详细讲了,大致意思是开启一个Headless模式,此模式会对没有显示器等输入输出设备的程序做一些处理)
		configureHeadlessProperty();
    // 获取运行监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
    // 调用监听器的开始方法,内部会批量的调用监听器的starting方法,以发送事件等来间接调用ApplicationListener
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      // 1.1 准备环境
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			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;
	}
  • 进入正题, prepareEnvironment(listeners, applicationArguments);
// 1.2 准备环境
// 参数1: listeners 此内部有之前获得的eventPublishRunListener
// 参数2: applicationArguments 启动传入的参数
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
  • 进入 prepareEnvironment 内部
	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// 获取或者创建一个环境(根据appliation的类型,SERVLET,REACTIVE,NONE)
		ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 配置环境
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}
  • 进入getOrCreateEnvironment()内部
	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();
		}
	}
  • 进入new StandardServletEnvironment()内部
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
	
  @Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
	  // SERVLET_CONFIG_PROPERTY_SOURCE_NAME servletConfigInitParams
    // SERVLET_CONTEXT_PROPERTY_SOURCE_NAME servletContextInitParams
    // 向StandardServletEnvironment对象中添加二个propertySources,目前是空的
    // Stub 存根,站位,差不多意思就是目前是空的,占个位置
    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);
	}
  
  // 这是初始化servletProperties方法,此方法会在启动流程的刷新容器方法中被调用,会将上面二个propertySource 的值进行填充
	@Override
	public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
		WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
	}

}
  • 跳转进入StandardServletEnvironment的父类,StandardEnvironment内部
public class StandardEnvironment extends AbstractEnvironment {

	/** System environment property source name: {@value}. */
	public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

	/** JVM system properties property source name: {@value}. */
	public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

	@Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
    // 向propertySources 添加二个propertySource,一个是系统的属性,一个是系统的环境,直接就是有值的
    // getSystemProperties() 会获得系统的属性值
    // getSystemEnvironment() 会获得系统的环境值
		propertySources.addLast(
				new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
		propertySources.addLast(
				new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
	}
}
  • OK,至此就将ConfigurableEnvironment 新对象创建完了,接着就是配置里面的其他参数了
  • 跳转进入configureEnvironment内部
	protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
		if (this.addConversionService) {
      // 采用单例模式获取一个ConversionService 对象
      // ConversionService用于类型转化,我看了下加载完之后有136个转换器,例如String-》Number
      // 在此之前EventPublishingRunListener中发送的starting() 事件触发了一个BackgroundPreinitializer 
      // 对此进行了预初始化
			ConversionService conversionService = ApplicationConversionService.getSharedInstance();
      // 将类型转换对象设置到环境中去
			environment.setConversionService((ConfigurableConversionService) conversionService);
		}
    // 配置属性资源
		configurePropertySources(environment, args);
    // 配置哪个配置文件生效
		configureProfiles(environment, args);
	}
  • 跳转进入 configurePropertySources
	protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
    // 获取环境对象中的PropertySources,刚看了这个,不会忘了吧,应该是有四个对吧
    // 系统属性 系统环境 servletConfigInitParams servletContextInitParams,前二个是有的,二个是空的
		MutablePropertySources sources = environment.getPropertySources();
    // 看看启动SpringApplicaiton的时候,有没有设置默认的属性以键值对的形式,有的话就加进去
		if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
			sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
		}
    // addCommandLineProperties 默认是true
    // 看看启动的时候有没有传入参数
		if (this.addCommandLineProperties && args.length > 0) {
      // COMMAND_LINE_PROPERTY_SOURCE_NAME commandLineArgs
			String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
      // 看看资源中是否包含 commandLineArgs
			if (sources.contains(name)) {
        // 从资源获取 commandLineArgs
				PropertySource<?> source = sources.get(name);
        // 以commandLineArgs 为key 创建一个CompositePropertySource对象
				CompositePropertySource composite = new CompositePropertySource(name);
        // 向composite里加入 一个以springApplicationCommandLineArgs为key,传入的参数为值的PropertySource
				composite.addPropertySource(
						new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
				// 并且将source 也加入到 composite中
        composite.addPropertySource(source);
        // 将sources中的 commandLineArgs 替换为 包含commandLineArgs 和 springApplicationCommandLineArgs的一个
				// CompositePropertySource 对象
				sources.replace(name, composite);
        // 总结: 简单的说就是将属性中的命令行参数和spring启动的参数都放进composite中,然后将属性中原本的替换为现在的
        // composite
			}
			else {
        // 如果只有spring启动传入的参数的话,则以commandLineArgs为key,参数为值添加到最sources的最前面
				sources.addFirst(new SimpleCommandLinePropertySource(args));
			}
		}
	}
  • 刚这个过程这么复杂,其实要是不传参数啥的,以上代码都不会执行,所以不用管,主要就是将启动参数加入到属性资源里面来
  • 至此属性资源中要是没有传参数的话,propertySource中还是有4个source,而且二个还是没有值
  • 跳转进入configureProfiles()内部
	protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
		Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
    // 从遍历PropertySources,从中获取属性为spring.profiles.active的属性
		profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
    // 设置环境中生效的profiles,可以同时生效多个
		environment.setActiveProfiles(StringUtils.toStringArray(profiles));
	}
  • 到这里configureEnvironment(environment, applicationArguments.getSourceArgs());已经解析完毕了

  • 接着回到prepareEnvironment()中,继续下一个流程

ConfigurationPropertySources.attach(environment);
  • 进入attach方法内部
	/**
	 * Attach a {@link ConfigurationPropertySource} support to the specified
	 * {@link Environment}. 
	 * 
	 * 翻译:给指定的环境添加一个ConfigurationPropertySource的支持
	 * 
	 * Adapts each {@link PropertySource} managed by the environment
	 * to a {@link ConfigurationPropertySource} and allows classic
	 * {@link PropertySourcesPropertyResolver} calls to resolve using
	 * {@link ConfigurationPropertyName configuration property names}.
	 * <p>
	 *
	 * 翻译:将环境管理的每个PropertySource调整为ConfigurationPropertySource,
	 * 并允许经典的PropertySourcesPropertyResolver调用来解决使用
	 * ConfigurationPropertyName配置属性名称
	 * 
	 * The attached resolver will dynamically track any additions or removals from the
	 * underlying {@link Environment} property sources.
	 * 
	 * 翻译:所附的解析器将动态地跟踪底层环境属性源的任何添加或删除。
	 *
	 * @param environment the source environment (must be an instance of
	 * {@link ConfigurableEnvironment})
	 * @see #get(Environment)
	 */
	// 翻译:
	// 给指定的环境添加一个ConfigurationPropertySource的支持
	public static void attach(Environment environment) {
		Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
    // 获取环境中的PropertySources
		MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
		// ATTACHED_PROPERTY_SOURCE_NAME configurationProperties
    // 获取sources中的configurationProperties
    PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
    // 如果sources 中含有 configurationProperties,并且其中的source不是最新的,则移除他
		if (attached != null && attached.getSource() != sources) {
			sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
			attached = null;
		}
		if (attached == null) {
      // 在sources的最前面添加ConfigurationPropertySourcesPropertySource类型的configurationProperties,
      // 内容就是环境中sources
			sources.addFirst(new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
					new SpringConfigurationPropertySources(sources)));
		}
	}
  • 目前按我的理解这个绑定就是给所有的sources换了个类型,这个类型提供一些方法可以更好的追踪,删除加入等操作
  • 返回prepareEnvironment()流程 进入listeners.environmentPrepared(environment);内部
	void environmentPrepared(ConfigurableEnvironment environment) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.environmentPrepared(environment);
		}
	}
  • 此方法就是使用EventPublishingRunListener调用environmentPrepared函数发送环境已经准备好的事件,从而触发监听器执行方法
  • 我们来看看会触发哪些监听器
// 一共会触发 7 个监听器
0 = {ConfigFileApplicationListener@2420} 
1 = {AnsiOutputApplicationListener@2421} 
2 = {LoggingApplicationListener@2422} 
3 = {ClasspathLoggingApplicationListener@2423} 
4 = {BackgroundPreinitializer@2424} 
5 = {DelegatingApplicationListener@2425} 
6 = {FileEncodingApplicationListener@2426} 

1.1 ConfigFileApplicationListener

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
         // 加载环境后置处理器
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
         // 并且加上自己,因为ConfigFileApplicationListener本身也是环境后置处理器
		postProcessors.add(this);
         // 排序
		AnnotationAwareOrderComparator.sort(postProcessors);
         // 分别调用他们的后置处理环境函数
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}
  • 进入loadPostProcessors();内部
	List<EnvironmentPostProcessor> loadPostProcessors() {
         // 从spring.factories中加载EnvironmentPostProcessor
		return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class.class, getClass().getClassLoader());
	}

// 返回一共有4个,加上自己一共5个
// 0 = {SystemEnvironmentPropertySourceEnvironmentPostProcessor@2450} 
// 1 = {SpringApplicationJsonEnvironmentPostProcessor@2451} 
// 2 = {CloudFoundryVcapEnvironmentPostProcessor@2452} 
// 3 = {ConfigFileApplicationListener@2286} 
// 4 = {DebugAgentEnvironmentPostProcessor@2453} 
  • 好吧,一个一个来
  • 进入SystemEnvironmentPropertySourceEnvironmentPostProcessor内部,看看是干嘛的
/**
 * An {@link EnvironmentPostProcessor} that replaces the systemEnvironment
 * {@link SystemEnvironmentPropertySource} with an
 * {@link OriginAwareSystemEnvironmentPropertySource} that can track the
 * {@link SystemEnvironmentOrigin} for every system environment property.
 *
 * 翻译:一个环境后置处理器,用于替换系统环境的属性资源,以便可以追踪,删除啥的,就是像之前的
 * ConfigurationPropertySources.attach(environment) 的作用
 * 将原本的 SystemEnvironmentPropertySource 替换成了 OriginAwareSystemEnvironmentPropertySource,值还是不变
 * @author Madhura Bhave
 * @since 2.0.0
 */
public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
    
	/**
	 * The default order for the processor.
	 */
	public static final int DEFAULT_ORDER = SpringApplicationJsonEnvironmentPostProcessor.DEFAULT_ORDER - 1;

	private int order = DEFAULT_ORDER;

	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
         // SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
		String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
         // 获取“systemEnvironment”
		PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
		if (propertySource != null) {
             // 将systemEnvironment的值换一个类进行封装
			replacePropertySource(environment, sourceName, propertySource);
		}
	}

	@SuppressWarnings("unchecked")
	private void replacePropertySource(ConfigurableEnvironment environment, String sourceName,
			PropertySource<?> propertySource) {
         // 获取值
		Map<String, Object> originalSource = (Map<String, Object>) propertySource.getSource();
         // 将值重新封装
		SystemEnvironmentPropertySource source = new OriginAwareSystemEnvironmentPropertySource(sourceName,
				originalSource);
         // 将环境中原本的SystemEnvironmentPropertySource 换成 OriginAwareSystemEnvironmentPropertySource
		environment.getPropertySources().replace(sourceName, source);
	}

    ...
    ...
}
  • 进入第2个 SpringApplicationJsonEnvironmentPostProcessor内部
/**
 * An {@link EnvironmentPostProcessor} that parses JSON from
 * {@code spring.application.json} or equivalently {@code SPRING_APPLICATION_JSON} and
 * adds it as a map property source to the {@link Environment}. The new properties are
 * added with higher priority than the system properties.
 *
 * 翻译:
 * 
 * @author Dave Syer
 * @author Phillip Webb
 * @author Madhura Bhave
 * @author Artsiom Yudovin
 * @since 1.3.0
 */
public class SpringApplicationJsonEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
         // 获取所有的PropertySource
		MutablePropertySources propertySources = environment.getPropertySources();
         // 查找是否有spring.application.json,有的话解析此值,然后存储到环境中
		propertySources.stream().map(JsonPropertyValue::get).filter(Objects::nonNull).findFirst()
				.ifPresent((v) -> processJson(environment, v));
	}

	private void processJson(ConfigurableEnvironment environment, JsonPropertyValue propertyValue) {
		JsonParser parser = JsonParserFactory.getJsonParser();
         // 获取spring.application.json 的值,转换成map
		Map<String, Object> map = parser.parseMap(propertyValue.getJson());
		if (!map.isEmpty()) {
             // 添加到环境中去
			addJsonPropertySource(environment, new JsonPropertySource(propertyValue, flatten(map)));
		}
	}
    
	...
	...
}
  • 简单的使用方法是,添加一个spring.application.json的系统属性,然后值是json形式的,就可以向propertysource中加入属性了
  • 进入第3个 CloudFoundryVcapEnvironmentPostProcessor内部
不知道这个是什么东西
  • 进入第4个ConfigFileApplicationListener内部
// 提取ConfigFileApplicationListener中后置处理器部分
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
	
    ...
	...
        
	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		addPropertySources(environment, application.getResourceLoader());
	}
    
	protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        // 添加一个RandomValuePropertySource到propertysource中
		RandomValuePropertySource.addToEnvironment(environment);
        // 创建一个Loader,并执行load()方法
		new Loader(environment, resourceLoader).load();
	}
    
	...
	...
}
  • 进入new Loader()内部(LoaderConfigFileApplicationListener的内部类)
		Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
			this.environment = environment;
            // 创建一个占位符解析器,内部是 PropertyPlaceholderHelper,默认占位符 ${}
			this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
			this.resourceLoader = (resourceLoader != null) ? resourceLoader
					: new DefaultResourceLoader(getClass().getClassLoader());
            // 从spring.factories中加载 PropertySourceLoader, 加载了以下二个
            // 0 = {PropertiesPropertySourceLoader@2621}  用于加载后缀为properties的资源
            // 1 = {YamlPropertySourceLoader@2622} 用于加载后缀为yaml的资源
			this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
					getClass().getClassLoader());
		}
  • 创建好了加载器,那就使用它来加载资源吧
  • 进入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);
					});
		}
  • 进入FilteredPropertySource.apply()内部
	static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
			Consumer<PropertySource<?>> operation) {
        // 获取所有的propetiesource,目前应该是只有6个,最近刚添加了个RandomValuePropertySource
		MutablePropertySources propertySources = environment.getPropertySources();
        // 获取资源中名为defaultProperties的资源
		PropertySource<?> original = propertySources.get(propertySourceName);
		if (original == null) {
            // 没有的话,则直接调用传入的匿名方法
			operation.accept(null);
			return;
		}
        // 如果有的话,则将资源替换为FilteredPropertySource,添加2个额外的属性,spring.profiles.active,spring.profiles.include用于筛选
		propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
		try {
			operation.accept(original);
		}
		finally {
			propertySources.replace(propertySourceName, original);
		}
	}
  • 总结下来就是根据传入的参数,以及生效的profile 来加载配置文件
  • 最后一个就是DebugAgentEnvironmentPostProcessor, 进入DebugAgentEnvironmentPostProcessor内部
	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		if (ClassUtils.isPresent(REACTOR_DEBUGAGENT_CLASS, null)) {
			Boolean agentEnabled = environment.getProperty(DEBUGAGENT_ENABLED_CONFIG_KEY, Boolean.class);
			if (agentEnabled != Boolean.FALSE) {
				try {
					Class<?> debugAgent = Class.forName(REACTOR_DEBUGAGENT_CLASS);
					debugAgent.getMethod("init").invoke(null);
				}
				catch (Exception ex) {
					throw new RuntimeException("Failed to init Reactor's debug agent");
				}
			}
		}
	}
  • 用于快速加载此类

1.2 AnsiOutputApplicationListener

  • 用于配置ansi编码

1.3 LoggingApplicationListener

  • 根据配置的属性设置配置日志

1.4 ClasspathLoggingApplicationListener

  • 根据日志的输出等级打印classpath的信息

1.5 BackgroundPreinitializer

  • 在此事件中不会执行操作

1.6 DelegatingApplicationListener

  • 在此事件中不会执行操作

1.7 FileEncodingApplicationListener

  • 根据配置来判断文件的类型是否符合配置
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值