Spring 源码部分接口实践

1.自定义条件注解

1.1 @conditional注解

@Conditional注解是Spring框架中的一个条件注解,可以根据不同的条件来决定是否需要创建一个Bean对象。其工作原理是,在Bean创建之前根据条件判断进行筛选,只有当条件满足时才会创建Bean对象。

1.2自定义条件注解

(1)实现Condition接口

public class RedisCustomCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("redis.configuration.off");
        if (Strings.isBlank(property)){
            return false;
        }
        return property.equalsIgnoreCase("true");
    }
}

(2)自定义注解,这样当我们在@Configuration类上使用@CustomConditionAnnotation注解时,框架会判断CustomCondition类的matches()方法是否返回true,如果是,则创建这个Bean对象。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.ANNOTATION_TYPE})
@Conditional(value = RedisCustomCondition.class)
public @interface RedisCustomConditionAnnotation {
}

(3)在条件装配的场景使用该注解

@Configuration
@RedisCustomConditionAnnotation
public class RedisConfig {
	....
}

(4)配置

redis.configuration.off = false

应用:可以用作某个配置是否交给spring管理的开关

2. ApplicationContextInitializer

2.1作用

通常用于需要在应用程序上下文中进行一些程序化初始化的 Web 应用程序。例如,注册属性源或激活与上下文环境相关的配置文件。

2.2 加载和初始化时机

通过实现ApplicationContextInitializer接口,可以在ApplicationContext创建之前对其进行一些定制化的修改

源码使用位置

org.springframework.boot.SpringApplication#getSpringFactoriesInstances(java.lang.Class<T>)

来获取 META-INF/spring.factories 中配置 key 为 org.springframework.context.ApplicationContextInitializer 的数据

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

ApplicationContextInitializer 的初始化时机

查看 SpringApplicationrun(String... args) 方法,如下所示:

	public ConfigurableApplicationContext run(String... args) {
        //监控器监听容器启动并进行图形化页面处理
		Startup startup = Startup.create();
		if (this.registerShutdownHook) {
			SpringApplication.shutdownHook.enableShutdownHookAddition();
		}
        //创建默认的引导上下文
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
        //图形化页面开启
		configureHeadlessProperty();
        //添加监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
            //加载参数
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            //环境准备
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            //banner打印
			Banner printedBanner = printBanner(environment);
            //创建Spring应用上下文
			context = createApplicationContext();
            //设置引导器
			context.setApplicationStartup(this.applicationStartup);
			/**
			* 准备上下文: 期间就会调用ApplicationContextInitializer 
			*/
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            //刷新上下文(包括单实例创建,前置处理和后置处理器,代理对象创建(aop)等一系列过程)
			refreshContext(context);
            //Spring应用上下文收尾阶段
			afterRefresh(context, applicationArguments);
			startup.started();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startup);
			}
			listeners.started(context, startup.timeTakenToStarted());
            //回调工作处理: 处理实现了ApplicationRunner 或 CommandLineRunner 接口的类,做一些容器启动成功后的预处理工作
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			throw handleRunFailure(context, ex, listeners);
		}
		try {
			if (context.isRunning()) {
				listeners.ready(context, startup.ready());
			}
		}
		catch (Throwable ex) {
			throw handleRunFailure(context, ex, null);
		}
		return context;
	}

进入上述 prepareContext 方法,可以看到applyInitializers(context)方法

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
		context.setEnvironment(environment);
		postProcessApplicationContext(context);
		addAotGeneratedInitializerIfNecessary(this.initializers);
		applyInitializers(context);
		listeners.contextPrepared(context);
		bootstrapContext.close(context);
}

进入上述 applyInitializers(context) 方法,可以看到这里循环处理实现了ApplicationContextInitializer接口的类

	protected void applyInitializers(ConfigurableApplicationContext context) {
		for (ApplicationContextInitializer initializer : getInitializers()) {
			Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
					ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
			initializer.initialize(context);
		}
	}
2.3 应用:
  1. 修改Spring Boot默认的environment属性。使用configurableApplicationContext.getEnvironment()方法获取到environment对象,从而修改环境变量,例如添加自定义配置文件路径。

  2. 添加自定义的PropertySource。使用environment.getPropertySources().addLast(propertySource)方法,可以添加自定义的属性源,从而实现更灵活的配置。

  3. 注册自定义bean。使用configurableApplicationContext.getBeanFactory().registerSingleton(beanName, bean)方法,可以注册自定义的bean,从而实现更灵活的依赖注入。

举例:

(1)添加自定义配置文件

/**
 * 修改Spring Boot默认的environment属性。
 * 使用configurableApplicationContext.getEnvironment()方法获取到environment对象,从而修改环境变量,例如添加自定义配置文件路径。
 *
 */
public class InterveneApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        // 添加自定义配置文件路径
        try {
            System.out.println("InterveneApplicationContextInitializer initialize :" + environment);
            environment.getPropertySources().addFirst(new ResourcePropertySource("classpath:liugp.properties"));
            System.out.println("InterveneApplicationContextInitializer initialize add FirstResourcePropertySource  classpath:zyftest.properties");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述代码中的InterveneApplicationContextInitializer需要被注册才能生效。可以通过在src/main/resources/META-INF/spring.factories文件中指定注册项的方式来注册:

org.springframework.context.ApplicationContextInitializer=\
com.example.demo17.config.hbase.InterveneApplicationContextInitializer

(2)添加自定义的属性源,从而实现更灵活的配置

/**
 * 添加自定义的PropertySource。
 * 使用environment.getPropertySources().addLast(propertySource)方法,可以添加自定义的属性源,从而实现更灵活的配置。
 */
public class InterveneApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        // 添加自定义的PropertySource
        PropertySource<?> propertySource = new MyPropertySource("myPropertySource");
        ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment();
        environment.getPropertySources().addLast(propertySource);
        System.out.println("InterveneApplicationContextInitializer initialize add PropertySource  myPropertySource");
    }

    // 自定义PropertySource
    private static class MyPropertySource extends PropertySource<String> {
        private static final String MY_PROPERTY_SOURCE_KEY = "my.property.source.key";

        public MyPropertySource(String name) {
            super(name);
        }

        @Override
        public Object getProperty(String name) {
            if (MY_PROPERTY_SOURCE_KEY.equals(name)) {
                return "myPropertySourceValue";
            }
            return null;
        }
    }
}

这样就可以通过@Value("${my.property.source.key}")的方式在应用程序中获取到它的值了;

同上:上述代码中的InterveneApplicationContextInitializer需要被注册才能生效

(3)注册自定义bean

/**
 * 注册自定义bean。
 * 使用configurableApplicationContext.getBeanFactory().registerSingleton(beanName, bean)方法,可以注册自定义的bean,从而实现更灵活的依赖注入。
 */
public class InterveneApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {

        ConfigurableListableBeanFactory beanFactory = configurableApplicationContext.getBeanFactory();
        // 注册自定义Bean
        MyBean myBean = new MyBean();
        beanFactory.registerSingleton("myBean", myBean);
        System.out.println("InterveneApplicationContextInitializer initialize registerSingleton  myBean");
    }
    
}

同上:上述代码中的InterveneApplicationContextInitializer需要被注册才能生效

3. ApplicationRunner 接口

应用:在Spring容器启动期间,容器刷新完成后做一些初始化操作(比如:预加载一些元数据,环境预热等)

用法:实现ApplicationRunner接口,交给Spring管理

/**
 * 项目启动时执行,预先加载初始化数据
 * */
@Component
public class ApplicationRunnerTest implements ApplicationRunner {
    @Override
    public void run(String... args) throws Exception {
        new Thread(()->{
            System.out.println("Hello ApplicationRunner!");
        }).start();
    }
}

4. CommandLineRunner 接口

用法作用和ApplicationRunner 类似

/**
 * 项目启动时执行,预先加载命令行相关数据
 * */
@Component
public class CommandLineRunnerTest implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        new Thread(()->{
            System.out.println("Hello CommandLineRunner!");
        }).start();
    }
}

5. SpringApplicationRunListener

SpringApplicationRunListener是Spring Boot的一个事件监听器,用于在应用程序启动和停止时执行一些操作。

/**
 * SpringApplicationRunListener是Spring Boot的一个事件监听器,用于在应用程序启动和停止时执行一些操作。
 * 可能需要自定义SpringApplicationRunListener来执行某些特定操作。
 * 下面是一个示例,演示如何扩展SpringApplicationRunListener以添加自定义操作
 * */
public class InterveneRunListener implements SpringApplicationRunListener {

    private final SpringApplication application;

    private final String[] args;

    public InterveneRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
    }

    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        System.out.println("SpringApplicationRunListener ... starting ");
        SpringApplicationRunListener.super.starting(bootstrapContext);
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        System.out.println("SpringApplicationRunListener ... environmentPrepared ");

        SpringApplicationRunListener.super.environmentPrepared(bootstrapContext, environment);
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("SpringApplicationRunListener ... contextPrepared ");

        SpringApplicationRunListener.super.contextPrepared(context);
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println("SpringApplicationRunListener ... contextLoaded ");

        SpringApplicationRunListener.super.contextLoaded(context);
    }

    @Override
    public void started(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("SpringApplicationRunListener ... started ");

        SpringApplicationRunListener.super.started(context, timeTaken);
    }

    @Override
    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("SpringApplicationRunListener ... ready ");

        SpringApplicationRunListener.super.ready(context, timeTaken);
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println("SpringApplicationRunListener ... failed ");

        SpringApplicationRunListener.super.failed(context, exception);
    }
}

上述代码中的InterveneRunListener需要被注册才能生效。可以通过在src/main/resources/META-INF/spring.factories文件中指定注册项的方式来注册:

org.springframework.boot.SpringApplicationRunListener=\
com.example.demo17.config.hbase.InterveneRunListener

6. BeanFactoryAware

使用场景:可以在bean实例化之后,但还未初始化之前,拿到 BeanFactory,在这个时候,可以对每个bean进行特殊化的定制。也或者可以把BeanFactory拿到进行缓存,日后使用。

  • 获取其他 bean 实例:通过 BeanFactory 引用,这个 bean 可以动态地获取容器中的其他 bean 实例。

  • 访问容器的配置信息:通过 BeanFactory,这个 bean 可以访问容器的配置信息,例如属性文件、环境变量等。

  • 控制 bean 的生命周期:通过 BeanFactory,这个 bean 可以在需要时自定义自己的初始化逻辑,或者在销毁时执行一些清理操作。

注册Bean

@Component
public class ApplicationListenerTest implements BeanFactoryAware {

    private ConfigurableListableBeanFactory configurableListableBeanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        configurableListableBeanFactory = (ConfigurableListableBeanFactory) beanFactory;
        configurableListableBeanFactory.registerSingleton("object",new Object());
    }
}

7. ApplicationContextAware

7.1应用场景

ApplicationContextAware可以用于各种场景,其中一些常见的包括:

访问其他Bean: 可以通过ApplicationContextAware获取其他Bean的实例,以便在Bean中调用其他Bean的方法或访问其他Bean的属性。 执行特定逻辑: 可以在获取到Spring容器上下文后执行特定的逻辑,例如执行一些初始化任务或后处理任务。 获取Spring容器信息: 可以获取Spring容器的各种信息,例如Bean的定义、环境属性、配置信息等。

@Component
public class ApplicationListenerTest implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        //通过注解获取bean
        Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(CustomConditionAnnotation.class);

        //通过接口获取bean
        Map<String, Condition> beansOfType = applicationContext.getBeansOfType(Condition.class);

    }
}

8. InitializingBean 和 @PostConstruct

主要适用于处理非静态成员的初始化,所以这个接口的用途就是用来实现初始化数据用的。

比如某些init操作

public class RetryTemplateTest {
    private RetryTemplate retryTemplate;
    static Long RETRY_TIME = 1000L;
 
    @PostConstruct
    public void init() {
        rateLimiter = RateLimiter.create(Double.parseDouble(globalArkConfig.getRateLimiterRate()));
        this.retryTemplate = createRetryTemplate();
    }
 
    /**
    * 定义重试策略:可以定义重试的次数、每次重试之间的间隔时间,甚至可以设置不同的重试策略(如指数退避策略)
    */
    private static RetryTemplate createRetryTemplate() {
 
        RetryTemplate retryTemplate = new RetryTemplate();
 
        // 设置重试策略,无线重试
        AlwaysRetryPolicy alwaysRetryPolicy = new AlwaysRetryPolicy();
        retryTemplate.setRetryPolicy(alwaysRetryPolicy);
        //设置重试策略,最高重试100次
        SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
        simpleRetryPolicy.setMaxAttempts(5);
        retryTemplate.setRetryPolicy(simpleRetryPolicy);
        // 设置回退策略-重试的间隔策略
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        //重试间隔ms
        backOffPolicy.setBackOffPeriod(1000);
        retryTemplate.setBackOffPolicy(backOffPolicy);
        //设置监听器
        RetryListener retryListener = new RetryListener() {
            @Override
            public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
                System.out.println("---open----在第一次重试时调用");
                return true;
            }
 
            @Override
            public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
                System.out.println("close----在最后一次重试后调用(无论成功与失败)。" + context.getRetryCount());
            }
 
            @Override
            public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
                System.out.println("error----在每次调用异常时调用。" + context.getRetryCount());
            }
        };
        retryTemplate.registerListener(retryListener);
        return retryTemplate;
    }
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool(16);
        try {
 
            ArrayList<Integer> list = new ArrayList<>(1000);
            for (int i = 0; i < 101; i++) {
                list.add(i);
            }
            System.out.println("数据准备完成。" + Thread.currentThread().getName());
 
            forkJoinPool.submit(() -> {
                list.parallelStream().forEach(res -> {
 
                    /**
                     * 通过 RetryTemplate 的 execute 方法来执行需要重试的操作。如果操作成功,则返回结果;
                     * 如果失败,则根据定义的重试策略进行重试。
                     */
                    retryTemplate.execute(
                            //每次重试的操作
                            retryContext -> {
                                if (res == 100) {
                                    throw new RuntimeException("偶数异常 => " + res);
                                }
                                return 0;
                            },
                            //达到最大重试次数后执行,并以其返回结果作为最终的返回结果
                            retryContext -> {
                                throw new RuntimeException("达到最大重试次数后 => " + res);
                            }
                    );
 
                });
            }).get();
 
        } catch (Exception e) {
            throw new RuntimeException("捕获到线程内部异常 => " + e);
        } finally {
            forkJoinPool.shutdown();
        }
    }
}
 

9. InstantiationAwareBeanPostProcessor

  • InstantiationAwareBeanPostProcessor接口继承BeanPostProcessor接口,它内部提供了3个方法,再加上BeanPostProcessor接口内部的2个方法,所以实现这个接口需要实现5个方法。InstantiationAwareBeanPostProcessor接口的主要作用在于目标对象的实例化过程中需要处理的事情,包括实例化对象的前后过程以及实例的属性设置

  • postProcessBeforeInstantiation方法是最先执行的方法,它在目标对象实例化之前调用,该方法的返回值类型是Object,我们可以返回任何类型的值。由于这个时候目标对象还未实例化,所以这个返回值可以用来代替原本该生成的目标对象的实例(比如代理对象)。如果该方法的返回值代替原本该生成的目标对象,后续只有postProcessAfterInitialization方法会调用,其它方法不再调用;否则按照正常的流程走

  • postProcessAfterInstantiation方法在目标对象实例化之后调用,这个时候对象已经被实例化,但是该实例的属性还未被设置,都是null。因为它的返回值是决定要不要调用postProcessPropertyValues方法的其中一个因素(因为还有一个因素是mbd.getDependencyCheck());如果该方法返回false,并且不需要check,那么postProcessPropertyValues就会被忽略不执行;如果返回true, postProcessPropertyValues就会被执行

  • postProcessPropertyValues方法对属性值进行修改(这个时候属性值还未被设置,但是我们可以修改原本该设置进去的属性值)。如果postProcessAfterInstantiation方法返回false,该方法可能不会被调用。可以在该方法内对属性值进行修改

  • 父接口BeanPostProcessor的2个方法postProcessBeforeInitialization和postProcessAfterInitialization都是在目标对象被实例化之后,并且属性也被设置之后调用的

10. BeanPostProcessor

BeanProcessor主要应用在以下几个方面:

  • 依赖注入的简化: 通过BeanPostProcessor,我们可以轻松实现依赖注入,减少了手动管理对象之间的依赖关系的复杂性,提高了代码的可测试性和可维护性。

  • 面向切面编程(AOP)的支持: BeanPostProcessor提供了AOP的支持,使得我们可以更加灵活地实现横切关注点,如日志记录、事务管理等,将这些与核心业务逻辑分离开来,提高了代码的模块化程度。

  • 配置管理的优化: 通过BeanPostProcessor,我们可以将配置信息与代码分离,实现了配置的集中管理和动态加载,降低了系统的耦合度,使得系统更易于维护和扩展。

执行顺序

ApplicationContextInitializer>静态代码块 > 构造方法 > BeanFactoryAware > ApplicationContextAware > @PostConstruct > InitializingBean > InstantiationAwareBeanPostProcessor > BeanPostProcessor > ApplicationRunner

11. Spring Boot 程序的启动时间优化

  • 减少依赖项:评估项目的依赖项,并确保只引入必要的依赖。较多的依赖项可能会增加启动时间,因为它们需要被扫描和初始化。通过删除不需要的依赖项或仅引入必要的模块,可以减少类路径的扫描和初始化时间。

  • 调整自动配置:Spring Boot 的自动配置是一个强大的特性,但有时可能会引入不必要的组件和功能。通过调整自动配置,可以精确地指定所需的配置,避免加载不必要的组件,从而减少启动时间。

  • 启用懒加载:将一些不常用的组件设置为懒加载,即在需要时才进行初始化。通过懒加载,可以避免在启动阶段初始化不必要的组件,从而加快启动时间。可以使用 Spring Framework 的 @Lazy 注解或在配置类中进行相应的配置。

    使用 @Lazy 注解:在需要懒加载的组件上使用 Spring Framework 的 @Lazy 注解。将 @Lazy 注解应用于组件的声明处,以指示该组件应该在需要时才进行初始化。

    @Component
    @Lazy
    public class MyLazyComponent {
        // ...
    }

    在配置类中进行配置:如果你使用的是配置类来进行组件的配置,你可以在配置类的方法上使用 @Lazy 注解,将需要懒加载的组件进行标记

    @Configuration
    public class MyConfig {
        @Bean
        @Lazy
        public MyLazyComponent myLazyComponent() {
            return new MyLazyComponent();
        }
    }

  • 启用编译时优化:使用 Spring Boot 2.4 及更高版本,你可以通过启用编译时优化来加快启动时间。通过在 pom.xml 文件中设置 <compilerArgs> 属性,使用 --add-opens 选项来启用编译时优化。这可以减少反射操作的开销,从而提高启动性能。

    确认使用的 Spring Boot 版本:确保你的项目使用的是 Spring Boot 2.4 或更高版本。编译时优化功能是在 Spring Boot 2.4 中引入的。 配置 pom.xml 文件:在项目的 pom.xml 文件中,找到 <build> 元素,并在其中的 <plugins> 元素下添加 Maven Compiler 插件配置。在 Maven Compiler 插件配置中,使用 <compilerArgs> 属性来设置编译器选项。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerArgs>
                        <arg>--add-opens</arg>
                        <arg>java.base/java.lang=ALL-UNNAMED</arg>
                        <arg>--add-opens</arg>
                        <arg>java.base/java.util=ALL-UNNAMED</arg>
                        <!-- 添加其他需要的 --add-opens 选项 -->
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <compilerArgs> 属性中,我们使用 --add-opens 选项来指定需要开放的包和模块。上述示例中,我们设置了两个 --add-opens 选项,分别是 java.lang 和 java.util 包。你还可以根据需要添加其他的 --add-opens 选项,以开放其他需要的包和模块。 重新构建应用程序:保存更改后,重新构建应用程序。在编译过程中,编译器将使用指定的编译器选项,启用编译时优化功能。

  • 调整日志级别:Spring Boot 默认启用了相对较高的日志级别,这可能会导致大量的日志输出,从而增加启动时间。通过将日志级别调整为更低的级别,如将 INFO 调整为 WARN,可以减少日志输出,从而缩短启动时间。

  • 使用缓存:Spring Boot 在启动过程中会进行多个步骤的扫描和初始化。通过使用缓存机制,可以缓存一些元数据和初始化结果,避免重复的扫描和初始化操作,从而提高启动性能。可以使用 Spring Boot 的缓存机制或其他缓存库来实现。

参考原文链接:

分析SpringBoot启动配置原理_spring启动加载顺序及原理-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值