SpringBoot自动装配机制探秘

一、什么是SpringBoot自动装配

SpringBoot的核心功能就是自动装配,只有掌握了SpringBoot自动装配机制,才算熟练掌握SpringBoot。早期我们使用SSH或SSM框架进行应用开发的时候,我们在开启某些Spring特性或引入第三方依赖时需要基于XML或注解进行配置,而这就体现了Spring Framework的自动装配。
而SpringBoot是基于Spring的基础上,通过SPI机制如对SPI机制不了解,请看SPI机制探秘这篇文章进行了解,看不懂来砍我)的方式,进行了深层次的优化。
根据SPI机制,SpringBoot定义了一套接口标准规范,这套标准规范规定:SpringBoot 在启动时会通过Spring去扫描引用外部 jar 包中的META-INF/spring.factories的配置类文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装配进 SpringBoot。
因此我们使用Spring Boot进行开发时,只需引入一个 starter 即可,通过少量注解和一些简单的配置就能开启引入组件提供的功能,从而对starter提供好的xxxAutoConfiguration、配置文件进行读取进行开发了,就是这么简单。

二、SpringBoot启动源码分析

基于SpringBoot创建应用程序,首先创建一个应用启动类如下图所示:
在这里插入图片描述
我们知道SpringBoot程序的入口是SpringApplication.run(Application.class, args);方法,因此点击run()方法进去进行分析,直到点击到最里层的run()方法如下源码所示:【SpringBoot版本为2.1.18.RELEASE,不同版本有细微差别,但原理是一样的

public ConfigurableApplicationContext run(String... args) {
		// 1、创建并启动计时监控进行计时
        StopWatch stopWatch = new StopWatch();
		// 启动开始计时
        stopWatch.start();
		// 2、声明应用上下文对象
        ConfigurableApplicationContext context = null;
		// 异常报告集合
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
		// 3、设置系统属性java.awt.headless的值
        this.configureHeadlessProperty();
		// 4、创建所有Spring运行监听器(此处读取META-INF/spring.factories配置文件)
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
		// 发布应用启动事件
        listeners.starting();

        Collection exceptionReporters;
        try {
			// 5、处理启动类获取到的args参数
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 6、准备环境
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
			// 配置忽略的bean信息
            this.configureIgnoreBeanInfo(environment);
			// 7、设置Banner的打印,可以设置打印banner off、console、log	
            Banner printedBanner = this.printBanner(environment);
			// 8、创建应用上下文
            context = this.createApplicationContext();
			// 9、实例化异常报告集合
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
			// 10、准备应用上下文
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 11、刷新应用上下文
            this.refreshContext(context);
			// 12、应用上下文刷新后置事件处理
            this.afterRefresh(context, applicationArguments);
			// 13、停止计时监控
            stopWatch.stop();
			// 14、记录日志,输出执行主类名、时间信息等
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }
			// 15、发布应用上下文启动完成事件
            listeners.started(context);
			// 16、运行所有Runner执行器
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
			// 处理运行错误信息
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
			// 17、发布应用上下文就绪事件
            listeners.running(context);
			// 18、返回应用上下文对象
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

从源码解析中可知SpringBoot启动共分为18个步骤,从中了解SpringBoot的启动流程,从而进行一些自定义的处理过程。

三、SpringBoot如何实现自动装配

基于上文内容,我们分析了run()方法,接着从应用启动类上的核心注解SpringBootApplication开始进行自动装配分析,知道SpringBoot关于自动配置的源码在spring-boot-autoconfigure-x.x.x.x.jar中。
点击@SpringBootApplication进去,如下源码所示:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 点击进去是@Configuration注解,即允许在上下文中注册额外的bean或导入其他配置类
@SpringBootConfiguration
// 启用SpringBoot的自动配置机制
@EnableAutoConfiguration
// 扫描被@Component/@Service/@Controller注解的bean,注解默认会扫描启动类所在的包下所有的类 ,同时可以自定义不扫描的bean。
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};
}

从源码中可知@EnableAutoConfiguration是实现自动装配的重要注解,从该注解继续点击进去,如下源码所示:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 将main包下所有组件注册到容器中
@AutoConfigurationPackage
// 加载自动装配类XxxxAutoConfiguration类
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
	// 用来覆盖自动配置开关的功能
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
	// 根据类排除指定的自动配置
    Class<?>[] exclude() default {};
	// 根据类名排除指定的自动配置
    String[] excludeName() default {};
}

从源码中可知AutoConfigurationImportSelector是负责加载自动装配类的,其中实现了多个接口,我们看下整个继承体系,如下图所示:
在这里插入图片描述
从图中可知,AutoConfigurationImportSelector 类实现了DeferredImportSelector接口从而实现了 ImportSelector接口,也就实现了这个接口中的 selectImports()方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类将被加载到 IoC 容器中。
点击进去,如下源码所示:【这块代码看懂了就没问题了】

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    private static final AutoConfigurationImportSelector.AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationImportSelector.AutoConfigurationEntry();
    private static final String[] NO_IMPORTS = new String[0];
    private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class);
    private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
    private ConfigurableListableBeanFactory beanFactory;
    private Environment environment;
    private ClassLoader beanClassLoader;
    private ResourceLoader resourceLoader;

    public AutoConfigurationImportSelector() {
    }

	// 重写ImportSelector接口的selectImports()方法
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
		// 判断是否开启自动装配配置,默认spring.boot.enableautoconfiguration=true,可在 application.properties或application.yml中进行设置
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
			// 获取所有需要装配的bean
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
			// 负责加载自动配置类【核心代码】
			AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
			// 返回符合条件的配置类的全限定类名数组
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
			// 获取主类中配置的exclude或excludeName注解的属性值
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
			// 获取所有候选的配置类
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
			// 对重复的配置类进行去重
            configurations = this.removeDuplicates(configurations);
			// 获得注解中被exclude或excludeName所指定被排除的类的集合
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
			// 检查被排除的类是否可实例化,是否被自动注册配置所使用,不符合条件则抛出异常
            this.checkExcludedClasses(configurations, exclusions);
			 // 从所有候选的自动配置类集合中移除被排除的类
            configurations.removeAll(exclusions);
			// 过滤配置类的注解是否符合META-INF/spring.factories文件中AutoConfigurationImportFilter指定的注解检查条件
            configurations = this.filter(configurations, autoConfigurationMetadata);    
			// 将候选完成的配置类和被排除的配置类构建为事件类,并通知监听器			this.fireAutoConfigurationImportEvents(configurations, exclusions);
			// 返回候选完成的配置类和被排除的配置类
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

    public Class<? extends Group> getImportGroup() {
        return AutoConfigurationImportSelector.AutoConfigurationGroup.class;
    }

    protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }

    protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
        String name = this.getAnnotationClass().getName();
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
        Assert.notNull(attributes, () -> {
            return "No auto-configuration attributes found. Is " + metadata.getClassName() + " annotated with " + ClassUtils.getShortName(name) + "?";
        });
        return attributes;
    }

    protected Class<?> getAnnotationClass() {
        return EnableAutoConfiguration.class;
    }

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		// 使用SpringFactoriesLoader工具类,查找classpath目录下所有jar包中的META-INF/spring.factories,找出其中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的属性定义的工厂类名称【核心代码】
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }

    private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
        List<String> invalidExcludes = new ArrayList(exclusions.size());
        Iterator var4 = exclusions.iterator();

        while(var4.hasNext()) {
            String exclusion = (String)var4.next();
            if (ClassUtils.isPresent(exclusion, this.getClass().getClassLoader()) && !configurations.contains(exclusion)) {
                invalidExcludes.add(exclusion);
            }
        }

        if (!invalidExcludes.isEmpty()) {
            this.handleInvalidExcludes(invalidExcludes);
        }

    }

    protected void handleInvalidExcludes(List<String> invalidExcludes) {
        StringBuilder message = new StringBuilder();
        Iterator var3 = invalidExcludes.iterator();

        while(var3.hasNext()) {
            String exclude = (String)var3.next();
            message.append("\t- ").append(exclude).append(String.format("%n"));
        }

        throw new IllegalStateException(String.format("The following classes could not be excluded because they are not auto-configuration classes:%n%s", message));
    }

    protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        Set<String> excluded = new LinkedHashSet();
		// 排除配置的自动装配类
        excluded.addAll(this.asList(attributes, "exclude"));
        excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
		// 排除通过配置文件spring.autoconfigure.exclude=全限定类名的方法
		excluded.addAll(this.getExcludeAutoConfigurationsProperty());
        return excluded;
    }

    private List<String> getExcludeAutoConfigurationsProperty() {
        if (this.getEnvironment() instanceof ConfigurableEnvironment) {
            Binder binder = Binder.get(this.getEnvironment());
            return (List)binder.bind("spring.autoconfigure.exclude", String[].class).map(Arrays::asList).orElse(Collections.emptyList());
        } else {
            String[] excludes = (String[])this.getEnvironment().getProperty("spring.autoconfigure.exclude", String[].class);
            return excludes != null ? Arrays.asList(excludes) : Collections.emptyList();
        }
    }

    private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
        long startTime = System.nanoTime();
		// 将所有加载的自动配置类封装成数组
        String[] candidates = StringUtils.toStringArray(configurations);
        boolean[] skip = new boolean[candidates.length];
        boolean skipped = false;
		// 遍历过滤器进行判断自动装配是否生效
        Iterator var8 = this.getAutoConfigurationImportFilters().iterator();

        while(var8.hasNext()) {
            AutoConfigurationImportFilter filter = (AutoConfigurationImportFilter)var8.next();
            this.invokeAwareMethods(filter);
			
        // 主要判断逻辑,candidates是自动装配类数组,autoConfigurationMetadata这个是META-INF/spring-autoconfigure-metadata.properties文件中的值
            boolean[] match = filter.match(candidates, autoConfigurationMetadata);

            for(int i = 0; i < match.length; ++i) {
                if (!match[i]) {
                    skip[i] = true;
					// 将不生效的自动配置类置为null
                    candidates[i] = null;
					// 判断标志位
                    skipped = true;
                }
            }
        }
// 判断标志位,true,则跳过;如果为false,返回配置类
        if (!skipped) {
            return configurations;
        } else {
			// 保存最终的配置类
            List<String> result = new ArrayList(candidates.length);

            int numberFiltered;
			// 遍历所有的自动装配类数组
            for(numberFiltered = 0; numberFiltered < candidates.length; ++numberFiltered) {
                if (!skip[numberFiltered]) {      
					// 将不为null的值加入进去
					result.add(candidates[numberFiltered]);
                }
            }

            if (logger.isTraceEnabled()) {
                numberFiltered = configurations.size() - result.size();
                logger.trace("Filtered " + numberFiltered + " auto configuration class in " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
            }
			// 返回最中的自动装配类集合
            return new ArrayList(result);
        }
    }

    protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
		// 具体的加载META-INF/spring.factories中的 AutoConfigurationImportFilter的值
        return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader);
    }

    protected final <T> List<T> removeDuplicates(List<T> list) {
        return new ArrayList(new LinkedHashSet(list));
    }

    protected final List<String> asList(AnnotationAttributes attributes, String name) {
        String[] value = attributes.getStringArray(name);
        return Arrays.asList(value != null ? value : new String[0]);
    }

    private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
		// 加载META-INF/spring.factories文件中类型为 AutoConfigurationImportListener的监听器
        List<AutoConfigurationImportListener> listeners = this.getAutoConfigurationImportListeners();
        if (!listeners.isEmpty()) {
			// 封装AutoConfigurationImportEvent自动装配导入事件
            AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
			// 遍历监听器
            Iterator var5 = listeners.iterator();
            while(var5.hasNext()) {
                AutoConfigurationImportListener listener = (AutoConfigurationImportListener)var5.next();
                this.invokeAwareMethods(listener);
				// 广播事件,所有监听AutoConfigurationImportEvent 事件的监听器会做出相应的处理
				listener.onAutoConfigurationImportEvent(event);
            }
        }

    }

    protected List<AutoConfigurationImportListener> getAutoConfigurationImportListeners() {
		// 具体加载META-INF/spring.factories中的 AutoConfigurationImportFilter的值
        return SpringFactoriesLoader.loadFactories(AutoConfigurationImportListener.class, this.beanClassLoader);
    }

    private void invokeAwareMethods(Object instance) {
        if (instance instanceof Aware) {
            if (instance instanceof BeanClassLoaderAware) {
                ((BeanClassLoaderAware)instance).setBeanClassLoader(this.beanClassLoader);
            }

            if (instance instanceof BeanFactoryAware) {
                ((BeanFactoryAware)instance).setBeanFactory(this.beanFactory);
            }

            if (instance instanceof EnvironmentAware) {
                ((EnvironmentAware)instance).setEnvironment(this.environment);
            }

            if (instance instanceof ResourceLoaderAware) {
                ((ResourceLoaderAware)instance).setResourceLoader(this.resourceLoader);
            }
        }

    }

    public int getOrder() {
        return 2147483646;
    }

    protected static class AutoConfigurationEntry {
        private final List<String> configurations;
        private final Set<String> exclusions;

        private AutoConfigurationEntry() {
            this.configurations = Collections.emptyList();
            this.exclusions = Collections.emptySet();
        }

        AutoConfigurationEntry(Collection<String> configurations, Collection<String> exclusions) {
            this.configurations = new ArrayList(configurations);
            this.exclusions = new HashSet(exclusions);
        }

        public List<String> getConfigurations() {
            return this.configurations;
        }

        public Set<String> getExclusions() {
            return this.exclusions;
        }
    }

    private static class AutoConfigurationGroup implements Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
        private final Map<String, AnnotationMetadata> entries = new LinkedHashMap();
        private final List<AutoConfigurationImportSelector.AutoConfigurationEntry> autoConfigurationEntries = new ArrayList();
        private ClassLoader beanClassLoader;
        private BeanFactory beanFactory;
        private ResourceLoader resourceLoader;
        private AutoConfigurationMetadata autoConfigurationMetadata;

        private AutoConfigurationGroup() {
        }

        public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
            Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> {
                return String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName());
            });
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(this.getAutoConfigurationMetadata(), annotationMetadata);
            this.autoConfigurationEntries.add(autoConfigurationEntry);
            Iterator var4 = autoConfigurationEntry.getConfigurations().iterator();

            while(var4.hasNext()) {
                String importClassName = (String)var4.next();
                this.entries.putIfAbsent(importClassName, annotationMetadata);
            }

        }

        public Iterable<Entry> selectImports() {
            if (this.autoConfigurationEntries.isEmpty()) {
                return Collections.emptyList();
            } else {
                Set<String> allExclusions = (Set)this.autoConfigurationEntries.stream().map(AutoConfigurationImportSelector.AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
                Set<String> processedConfigurations = (Set)this.autoConfigurationEntries.stream().map(AutoConfigurationImportSelector.AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
                processedConfigurations.removeAll(allExclusions);
                return (Iterable)this.sortAutoConfigurations(processedConfigurations, this.getAutoConfigurationMetadata()).stream().map((importClassName) -> {
                    return new Entry((AnnotationMetadata)this.entries.get(importClassName), importClassName);
                }).collect(Collectors.toList());
            }
        }
		// 。。。。。。删除了部分代码

分析完了源码,在进行一次总结,如下图所示:
在这里插入图片描述

四、SpringBoot条件注解

从源码分析中知道,有些类会被过滤掉,这就得说SpringBoot条件注解,而SpringBoot自动装配通常有一个原则:只有当容器中不存在特定的类型的bean或特定bean时,SpringBoot自动装配才会装配该类型的bean或特定bean。
条件注解可用于修饰@Configuration类或@Bean方法等,表示只有当特定条件有效时,被修饰的配置类或配置方法才会生效。
SpringBoot条件注解分类如下:

1、类条件注解

@ConditionalOnClass:当类路径下存在被修饰的类或方法时生效。可通过value或name属性指定它所要求存在的类,其中value属性值是被检查类的Class对象,name属性值是被检查类的字符串形式的全限定类名,一般使用name属性值居多。
@ConditionalOnMissingClass:当类路径下没有被修饰的类或方法时生效。只能通过value属性指定它所要求不存在的类,value属性值只能是被检查类的字符串形式的全限定类名。

2、Bean条件注解

@ConditionalOnBean:当容器里存在指定 Bean 的条件下生效
@ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下生效
@ConditionalOnSingleCandidate:@ConditionalOnBean的增强版,当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean时才生效。
@ConditionalOnMissingFilterBean:@ConditionalOnMissingBean的特殊版本,专门用于检查容器中是否存在指定类型的javax.servlet.Filter,因此它只能通过value属性指定其要检查的Filter的类型。

3、属性条件注解

@ConditionalOnProperty:指定的属性是否存在指定的属性值

4、Web应用条件注解

@ConditionalOnWebApplication:当前项目是Web应用时,修饰的类或方法才生效。
@ConditionalOnNotWebApplication:当前项目不是Web应用时,修饰的类或方法才生效。
@ConditionalOnWarDeployment:只有当前应用以传统WAR包方式被部署到Web服务器或应用服务器时,修饰的类或方法才生效。

5、资源条件注解

@ConditionalOnResource:当指定的资源存在时,修饰的类或方法才生效。

6、SpEL表达式条件注解

@ConditionalOnExpression:基于指定SpEL 表达式的值为true时,修饰的类或方法才生效。

7、特殊条件注解

@ConditionalOnJava:对部署平台的Java版本进行检测,配置才生效。
@ConditionalOnJndi:在 JNDI 存在的条件下,通过value属性指定要检查的JNDI。
@ConditionalOnCloudPlatform:应用被部署在特定云平台上,修饰的类或方法才生效。
@ConditionalOnRepositoryType:当特定的Spring Data Repository被启用时,修饰的类或方法才生效。

8、自定义注解

只有当满足自定义的注解时,修饰的类或方法才生效。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值