写在前面

关于springboot系列详细分析,可以参考这里

1:使用

在使用springboot的时候,我们可能会有如下的需求,希望创建一个对象,但是呢,对象的属性是动态的,希望可配置,此时我们可能有如下的选择:

  • 1:自己读取配置文件,获取配置的属性,动态赋值
  • 2:使用@PropertySource注解和@Value注解组合
  • 3:使用@ConfigurationProperties注解
    其中1比较麻烦,显然不可取,2如果是在纯spring的环境中,可以采取该方案,具体可以参考这篇文章,3是在springboot环境中提供的方案,也是本文我们要分析的重点。其中,一般我们有两种方式来使用ConfigurationProperties,第一种是使用@Component+@ConfigurationProperties,第二种是使用@EnableConfigurationProperties+@ConfigurationProperties,分别来看下。

1.1:@Component+@ConfigurationProperties

1.1.1:定义bean
@ConfigurationProperties(prefix = "mypreffix1")
@Component
public class MyComponentConfigurationProperties {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MyConfigurationProperties{" +
                "name='" + name + '\'' +
                '}';
    }
}

默认从application.properties/yml文件中读取,如果是需要从其它配置文件中读取的话,可以通过@PropertySource配置,可能如下:

@Component("myBean")
@PropertySource("classpath:myPropertySourceBean.properties")
@ConfigurationProperties(prefix = "mybean")
public class MyPropertySourceBean {}
1.1.2:定义配置

application.properties:

mypreffix1.name="the name by ConfigurationProperties1s"
1.1.3:测试代码
@SpringBootApplication(
        scanBasePackages = { "dongshi.daddy.springboothelloworld",
                             "dongshi.daddy.configurationproperties" }
)
public class SpringbootHelloWorldApplication {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(SpringbootHelloWorldApplication.class);

        ConfigurableApplicationContext ac = springApplication.run(args);
        MyComponentConfigurationProperties myComponentConfigurationProperties = ac.getBean(MyComponentConfigurationProperties.class);
        System.out.println(myComponentConfigurationProperties);
    }

}

运行:

2021-06-26 10:08:14.199  INFO 5258 --- [           main] d.d.s.SpringbootHelloWorldApplication    : Started SpringbootHelloWorldApplication in 2.515 seconds (JVM running for 3.313)
MyConfigurationProperties{name='"the name by ConfigurationProperties1s"'}

1.2:@EnableConfigurationProperties+@ConfigurationProperties

1.2.1:定义bean
@ConfigurationProperties(prefix = "mypreffix")
public class MyConfigurationProperties {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MyConfigurationProperties{" +
                "name='" + name + '\'' +
                '}';
    }
}
1.2.2:EnableAutoConfigurationProperties
@Configuration("myConfigurationPropertiesBean")
@EnableConfigurationProperties(MyConfigurationProperties.class)
public class MyConfigurationPropertiesBean {

    @Resource
    private MyConfigurationProperties myConfigurationProperties;

    public MyConfigurationProperties getMyConfigurationProperties() {
        return myConfigurationProperties;
    }

    public void setMyConfigurationProperties(MyConfigurationProperties myConfigurationProperties) {
        this.myConfigurationProperties = myConfigurationProperties;
    }
}
1.2.3:测试代码
@SpringBootApplication(
        scanBasePackages = { "dongshi.daddy.springboothelloworld",
                             "dongshi.daddy.configurationproperties" }
)
public class SpringbootHelloWorldApplication {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(SpringbootHelloWorldApplication.class);

        ConfigurableApplicationContext ac = springApplication.run(args);
        MyConfigurationPropertiesBean myConfigurationPropertiesBean = ac.getBean(MyConfigurationPropertiesBean.class);
        System.out.println(myConfigurationPropertiesBean);
        System.out.println(myConfigurationPropertiesBean.getMyConfigurationProperties());
    }

}

运行:

2021-06-26 11:30:20.439  INFO 8100 --- [           main] d.d.s.SpringbootHelloWorldApplication    : Started SpringbootHelloWorldApplication in 3.128 seconds (JVM running for 4.085)
dongshi.daddy.configurationproperties.MyConfigurationPropertiesBean$$EnhancerBySpringCGLIB$$de5fbbbe@606fc505
MyConfigurationProperties{name='"the name by ConfigurationProperties"'}

其中我们用到的EnableConfigurationProperies源码如下:

// 启用通过注解ConfigurationProperties注解配置的bean,我们使用常规的方式
// (比如@Bean,@Component等)来注册,或者是为了方便,通过该注解的value
// 来配置作为spring bean
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {

	// 设置使用了ConfigurationProperties注解,并需要注册为spring bean
	// 的类的类型数组,最终的效果是,设置的类会从配置文件中读取相关信息
	// 完成spring bean的创建
	Class<?>[] value() default {};

}

2:执行过程分析

当我们执行run方法,首先执行到如下代码:

org.springframework.boot.SpringApplication#run(java.lang.String...)
// 1:启动应用程序 2:创建并返回spring容器
public ConfigurableApplicationContext run(String... args) {
	...snip...
	try {
		...snip...
		// 刷新容器
		refreshContext(context);
		...snip...
	}
	catch (Throwable ex) {
		...snip...
	}
	...snip...
	return context;
}

refreshContext执行关键源码如下:

org.springframework.boot.SpringApplication#refreshContext
private void refreshContext(ConfigurableApplicationContext context) {
	refresh(context);
	...snip...
}

继续:

org.springframework.boot.SpringApplication#refresh
protected void refresh(ApplicationContext applicationContext) {
	...snip...
	((AbstractApplicationContext) applicationContext).refresh();
}

继续:

org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#refresh
// 注意该方法还是springboot中的方法,重写了
// 加入了刷新异常后停止web服务器的逻辑
public final void refresh() throws BeansException, IllegalStateException {
	try {
		// <202106261716>
		super.refresh();
	}
	catch (RuntimeException ex) {
		// 异常,停止web服务器
		stopAndReleaseWebServer();
		throw ex;
	}
}

继续看<202106261716>处源码,这就是调用spring原有的容器刷新逻辑了,源码如下:

org.springframework.context.support.AbstractApplicationContext#refresh
public void refresh() throws BeansException, IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		...snip...
		try {
			...snip...
			// <202106261722>
			// 执行BeanFactoryPostProcessor,在生成了BeanDefinition
			// 之后,在通过BeanDefinition生成bean之前调用,详细可以参考:https://blog.csdn.net/wang0907/article/details/115440135
			invokeBeanFactoryPostProcessors(beanFactory);
			...snip...
		}
		catch (BeansException ex) {
			...snip...
		}
		...snip...
	}
}

<202106261722>处执行后会一直调用到处理@Configuration注解的方法:

org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
	...snip...
	// 递归处理configuration的类,以及子类
	SourceClass sourceClass = asSourceClass(configClass);
	do {
		sourceClass = doProcessConfigurationClass(configClass, sourceClass);
	} while (sourceClass != null);
	...snip...
}

最终会执行到方法org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector#selectImports,该方法源码如下:

org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector#selectImports
public String[] selectImports(AnnotationMetadata metadata) {
	// <202106262010>
	return IMPORTS;
}

注意到该方法正式在EnableConfigurationProperties注解中的Import注解中配置的EnableConfigurationPropertiesImportSelector<202106262010>的IMPORTS定义如下:

org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector#IMPORTS
private static final String[] IMPORTS = { ConfigurationPropertiesBeanRegistrar.class.getName(),
			ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

这两个类是ConfigurationPropertiesBeanRegistrarConfigurationPropertiesBindingPostProcessorRegistrar,其中ConfigurationPropertiesBeanRegistrar参考3:ConfigurationPropertiesBeanRegistrarConfigurationPropertiesBindingPostProcessorRegistrar参考4:ConfigurationPropertiesBindingPostProcessorRegistrar`。

3:ConfigurationPropertiesBeanRegistrar

该类的作用是引入在EnableConfigurationProperties注解中使用value配置的类为BeanDefinition对象。
源码如下:

org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar#registerBeanDefinitions
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
	// <202106262023>
	getTypes(metadata).forEach((type) -> register(registry, (ConfigurableListableBeanFactory) registry, type));
}

<202106262023>处getTypes(metadata),metadata是封装类的注解信息的对象,比如都使用了哪些注解,注解中都定义了哪些属性等信息,源码如下:

org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar#getTypes
private List<Class<?>> getTypes(AnnotationMetadata metadata) {
	// 获取EnableConfigurationProperties注解配置的信息的结果map
	MultiValueMap<String, Object> attributes = metadata
			.getAllAnnotationAttributes(EnableConfigurationProperties.class.getName(), false);
	// 获取所有属性配置中的value的信息集合,如下配置:
	/*
	@Configuration("myConfigurationPropertiesBean")
@EnableConfigurationProperties(MyConfigurationProperties.class)
public class MyConfigurationPropertiesBean {}
	*/
	// 则结果就是"class dongshi.daddy.configurationproperties.MyConfigurationProperties"
	return collectClasses((attributes != null) ? attributes.get("value") : Collections.emptyList());
}

获取了EnableConfigurationProperties注解配置的数组后,就可以通过<202106262023>处register方法来注册对应的beandefinition了,具体参考3.1:register

3.1:register

源码如下:

org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar#register
private void register(BeanDefinitionRegistry registry, ConfigurableListableBeanFactory beanFactory,
				Class<?> type) {
	// <202106262048>
	String name = getName(type);
	// 如果在bean工厂中不包含bean定义
	if (!containsBeanDefinition(beanFactory, name)) {
		// <202106262102>
		registerBeanDefinition(registry, name, type);
	}
}

<202106262048>处是通过class类型生成bean的名称,源码如下:


org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar#getName
private String getName(Class<?> type) {
	// 获取类上的ConfigurationProperties注解
	ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, ConfigurationProperties.class);
	// 获取注解的prefix属性,如"@ConfigurationProperties(prefix = "mypreffix")",
	// 这此处的结果就是"mypreffix"
	String prefix = (annotation != null) ? annotation.prefix() : "";
	// 如果有前缀则使用"前缀-类型名称"的格式,否则直接适用类型名称,如:
	/*
	@ConfigurationProperties(prefix = "mypreffix")
public class MyConfigurationProperties {}
	*/
	// 我本地结果就是"mypreffix-dongshi.daddy.configurationproperties.MyConfigurationProperties"
	return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
}

<202106262102>处是注册beandefinition,源码如下:

org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar#registerBeanDefinition
private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, Class<?> type) {
	// 要有ConfigurationProperties注解
	assertHasAnnotation(type);
	// 定义GenericBeanDefinition对象
	GenericBeanDefinition definition = new GenericBeanDefinition();
	// 设置class类型
	definition.setBeanClass(type);
	// 注册,即存储到"org.springframework.beans.factory.support.DefaultListableBeanFactory#beanDefinitionMap"中
	registry.registerBeanDefinition(name, definition);
}

注意:到此处,只是注册了类型信息,还没有获取到在配置文件中配置的属性值信息!!!

4:ConfigurationPropertiesBindingPostProcessorRegistrar

源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar#registerBeanDefinitions
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	// <202106270830>
	if (!registry.containsBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
		// <202106270856>
		// 注册ConfigurationPropertiesBindingPostProcessor
		registerConfigurationPropertiesBindingPostProcessor(registry);
		// <202106270905>
		// 注册ConfigurationBeanFactoryMetadata
		registerConfigurationBeanFactoryMetadata(registry);
	}
}

<202106270830>处判断是否包含org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#BEAN_NAME,这是bean处理器ConfigurationPropertiesBindingPostProcessor在容器中bean名称,关于ConfigurationPropertiesBindingPostProcessor参考4.1:ConfigurationPropertiesBindingPostProcessor<202106270856>处源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar#registerConfigurationPropertiesBindingPostProcessor
private void registerConfigurationPropertiesBindingPostProcessor(BeanDefinitionRegistry registry) {
	// 创建GenericBeanDefinition对象实例,并设置相关信息
	GenericBeanDefinition definition = new GenericBeanDefinition();	definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
	definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
	// 注册到bean定义注册器中
	registry.registerBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);
}

<202106270905>是注册ConfigurationBeanFactoryMetadata,源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorRegistrar#registerConfigurationBeanFactoryMetadata
private void registerConfigurationBeanFactoryMetadata(BeanDefinitionRegistry registry) {
	// 创建GenericBeanDefinition对象实例,并设置相关属性
	GenericBeanDefinition definition = new GenericBeanDefinition();
	definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
	definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
	// 注册到bean定义注册器中
	registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME, definition);
}

关于ConfigurationBeanFactoryMetadata具体参考4.2:ConfigurationBeanFactoryMetadata

4.1:ConfigurationPropertiesBindingPostProcessor

该类定义如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
// 绑定PropertySource的信息到使用了ConfigurationProperties注解的bean上
public class ConfigurationPropertiesBindingPostProcessor
		implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {}

可以看到其实现了BeanPostProcessor接口会在bean初始化前调用方法postProcessBeforeInitialization,bean初始化后方法postProcessAfterInitialization并没有实现(因为在接口中定义是default的,所以可以不用实现),关于postProcessBeforeInitialization方法具体参考4.1.1:postProcessBeforeInitialization
还实现了aware接口ApplicationContextAware,因此会通过方法setApplicationContext设置应用程序上下文对象。
最后还实现了InitializingBean,因此会在bean的属性设置完毕后调用方法afterPropertiesSet,关于该方法参考4.1.2:afterPropertiesSet

4.1.1:postProcessBeforeInitialization

源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization
// bean:只进行了实例化,还初始化的bean对象
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
	// <202106270938>
	// 获取ConfigurationProperties注解
	ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class);
	if (annotation != null) {
		// <202106270942>
		bind(bean, beanName, annotation);
	}
	return bean;
}

<202106270938>处源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#getAnnotation
private <A extends Annotation> A getAnnotation(Object bean, String beanName, Class<A> type) {
	// 获得bean上的注解
	A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
	// 从bean上没有获取到注册,则从class上获取
	if (annotation == null) {
		annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
	}
	return annotation;
}

<202106270942>处源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#bind
private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
	// 获取bean的类型,源码如下:
	/*
	org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#getBeanType
	private ResolvableType getBeanType(Object bean, String beanName) {
		// 我测试获取的为null,不直达何时不为null,先忽略
		Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
		if (factoryMethod != null) {
			return ResolvableType.forMethodReturnType(factoryMethod);
		}
		// 通过class获取类型
		return ResolvableType.forClass(bean.getClass());
	}
	*/
	ResolvableTypetype = getBeanType(bean, beanName);
	// 获取Validated注解,认为没有即可
	Validated validated = getAnnotation(bean, beanName, Validated.class);
	// 默认validated注解为null,所以这里的结果是:[ConfigurationProperties]
	Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
			: new Annotation[] { annotation };
	// 创建Bindable对象,内部封装了bean,annotations信息
	Bindable<?> target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations);
	try {
		// <202106271027>
		this.configurationPropertiesBinder.bind(target);
	}
	catch (Exception ex) {
		throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex);
	}
}

<202106271027>处是绑定属性信息到bean中,具体参考4.3:绑定属性信息到bean

4.1.2:afterPropertiesSet

源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#afterPropertiesSet
public void afterPropertiesSet() throws Exception {
	this.beanFactoryMetadata = this.applicationContext.getBean(ConfigurationBeanFactoryMetadata.BEAN_NAME,
			ConfigurationBeanFactoryMetadata.class);
	this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(this.applicationContext,
			VALIDATOR_BEAN_NAME);
}

4.2:ConfigurationBeanFactoryMetadata

该类主要处理通过方法+@Bean方式组合创建bean情况的元信息。

4.2.1:postProcessBeanFactory

源码如下:

org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata#postProcessBeanFactory
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
	this.beanFactory = beanFactory;
	// 遍历bean工厂中所有bean名称
	for (String name : beanFactory.getBeanDefinitionNames()) {
		// 获取当前bean定义的名称
		BeanDefinition definition = beanFactory.getBeanDefinition(name);
		// method是使用了@Bean注解的方法的名字
		// bean是@Configuration所在的类的bean名字
		/*
		package foo.bar;

		@Configuration
		public class XXX {
			
			@Bean
			public Object m1() {
				return new Object();
			}
		}
		*/
		// method的值就是"m1",bean的值就是"foo.bar.XXX"
		String method = definition.getFactoryMethodName();
		String bean = definition.getFactoryBeanName();
		// 如果是method和bean都有值,则存储到bean工厂元信息对象beansFactoryMetadata中
		if (method != null && bean != null) {
			this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method));
		}
	}
}

4.3:绑定属性信息到bean

执行的方法是org.springframework.boot.context.properties.ConfigurationPropertiesBinder#bind,源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBinder#bind
public void bind(Bindable<?> target) {
	// 获取ConfigurationProperties注解
	ConfigurationProperties annotation = target.getAnnotation(ConfigurationProperties.class);
	// 断言,必须有ConfigurationProperties注解
	Assert.state(annotation != null, () -> "Missing @ConfigurationProperties on " + target);
	// <202106271105>
	// 获取Validator集合
	List<Validator> validators = getValidators(target);
	// <202106271158>
	BindHandler bindHandler = getBindHandler(annotation, validators);
	// <202106271207>
	getBinder().bind(annotation.prefix(), target, bindHandler);
}

<202106271105>处是获取Validator集合,源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBinder#getValidators
private List<Validator> getValidators(Bindable<?> target) {
	// 最多3个验证器,所以这里长度设置为3
	List<Validator> validators = new ArrayList<>(3);
	// 第1个验证器的来源,configurationPropertiesValidator
	if (this.configurationPropertiesValidator != null) {
		validators.add(this.configurationPropertiesValidator);
	}
	// 第2个验证器的来源,jsr303Present
	if (this.jsr303Present && target.getAnnotation(Validated.class) != null) {
		validators.add(getJsr303Validator());
	}
	// 第3个验证器,本身实现了Validator接口的情况
	if (target.getValue() != null && target.getValue().get() instanceof Validator) {
		validators.add((Validator) target.getValue().get());
	}
	// 返回验证器集合
	return validators;
}

<202106271158>处是获取BinderHandler,源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBinder#getBindHandler
private BindHandler getBindHandler(ConfigurationProperties annotation, List<Validator> validators) {
	BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
	// 如果有ignoreInvalidFields,则进一步包装
	if (annotation.ignoreInvalidFields()) {
		handler = new IgnoreErrorsBindHandler(handler);
	}
	// 如果有ignoreUnknownFields,则进一步包装
	if (!annotation.ignoreUnknownFields()) {
		UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
		handler = new NoUnboundElementsBindHandler(handler, filter);
	}
	// 如果有验证器,则进一步包装
	if (!validators.isEmpty()) {
		handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
	}
	// 忽略
	for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
		handler = advisor.apply(handler);
	}
	// 返回,可以认为结果就是IgnoreTopLevelConverterNotFoundBindHandler
	return handler;
}

<202106271207>出getBinder方法源码如下:

org.springframework.boot.context.properties.ConfigurationPropertiesBinder#getBinder
private Binder getBinder() {
	if (this.binder == null) {
		// getConfigurationPropertySources():获取封装属性信息的对象
		// getPropertySourcesPlaceholdersResolver():获取处理占位符的对象
		// getPropertyEditorInitializer():获取用于编辑对象属性的对象
		this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(),
				getConversionService(), getPropertyEditorInitializer());
	}
	return this.binder;
}

<202106271207>处的bind方法就是完成绑定配置信息到bean的属性中的工作了,具体是如何绑定的,属于org.springframework.boot.context.properties.bind包中的内容,没有了解,知道即可先。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐