• Springboot启动流程(源码解析)、自动装配流程(源码解析)、总结、SrpringBoot初始化数据扩展


    SpringBoot启动流程

    事先声明:本文对springboot源码讲解不太深,只有皮毛而已,如有需要详细源码流程,请移步!

    本文主要是部分对springboot的源码解析+可扩展的功能(初始化等)

    文尾有图

    启动流程

    先把版本贴出来,不同的版本代码和配置可能会不大相同,但是思想都是通的

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.4version>
    parent>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    先借用下大佬的图,以便对照图来看代码

    在这里插入图片描述

    1.`首先入口处的启动类,都是创建一个Main主函数,调用run方法

    @SpringBootApplication
    public class DispatcherApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DispatcherApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.进去run看一下,返回值是ConfigurableApplicationContext(继承自ApplicationContext),其实返回值就是spring的bean容器

    public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
        return run(new Class[]{primarySource}, args);
    }
    
    • 1
    • 2
    • 3

    题外话:我们甚至可以去接收这个spring bean容器,获取到里面的bean组件,也可能遇到当springboot启动后立即初始化的需求,这里也是一种实现方式。

    @SpringBootApplication
    public class DispatcherApplication {
    
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(DispatcherApplication.class, args);
            // 获取所有bean的名称
            String[] beanDefinitionNames = context.getBeanDefinitionNames();
            // 获取所有bean的数量
            int beanDefinitionCount = context.getBeanDefinitionCount();
            System.out.println("spring bean中有" + beanDefinitionCount + "个bean组件");
            for (String name : beanDefinitionNames) {
                System.out.println(name);
            }
            // 根据bean的class获取此bean组件
            DispatcherService bean = context.getBean(DispatcherService.class);
            System.out.println(bean);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    启动springboot看下控制台日志

    在这里插入图片描述

    187个组件,一大堆哈哈。

    最后根据类型获取bean的日志给贴一下

    com.wlh.rabbitmq.service.DispatcherService@2cc04358
    
    • 1

    3.言归正传,在步骤2中继续往run方法里面看

    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return (new SpringApplication(primarySources)).run(args);
    }
    
    • 1
    • 2
    • 3

    这里是先去创建了一个SpringApplication的应用对象,然后又再次调用了一个run()方法

    4.看一下这个new SpringApplication(primarySources)

    在这里插入图片描述

    主要就是创建SpringApplication的对象,然后进行初始化一些值,如:bannerMode横幅模式(默认输出到控制台),webApplicationType(web应用环境类型,是否是web环境),设置initializers属性、设置listeners属性等(这几个属性都是通过方法的返回值设置的)

    上图中

    initializers属性和listeners属性都是调用了同一个方法getSpringFactoriesInstances实现的,依次点击去SpringFactoriesLoader.loadFactoryNames() ->loadSpringFactories(),发现:其实就是去读取META-INF/spring.factories中的配置

    看下initializers和listeners分别读取的哪些

    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    
    • 1
    • 2

    initializers是读取的ApplicationContextInitializer

    listeners是读取的ApplicationListener的。看下分别读的哪些配置

    # Initializers
    org.springframework.context.ApplicationContextInitializer=\
    org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
    org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
    
    
    # Application Listeners
    org.springframework.context.ApplicationListener=\
    org.springframework.boot.autoconfigure.BackgroundPreinitializer
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    5.回到步骤3,创建完SpringApplication对象后,又调用了run(),点进去看一下

    public ConfigurableApplicationContext run(String... args) {
        // 使用的StopWatch计时器,记录启动总计时间
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
    
        DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
        // 定义了 springbean的容器(ioc容器)
        ConfigurableApplicationContext context = null;
        this.configureHeadlessProperty();
    
        // 创建并初始化监听器SpringApplicationRunListeners,并启动监听,用于监听run方法的执行。
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);
    
        try {
            // 创建并初始化ApplicationArguments,获取run方法传递的args参数
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    
            // 创建并初始化ConfigurableEnvironment(环境配置)。封装main方法的参数,初始化参数,写入到 Environment中,发布 ApplicationEnvironmentPreparedEvent(环境事件),做一些绑定后返回Environment。
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
    
            // 打印banner和版本。
            Banner printedBanner = this.printBanner(environment);
    
            // 构造Spring容器(ApplicationContext)上下文。
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
    
            // 准备spring容器,将启动类注入容器,为后续的自动装配奠定基础
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
    
            // 刷新容器(创建bean)、加载自动化配置信息(很重要)
            this.refreshContext(context);
            // spring容器后置处理
            this.afterRefresh(context, applicationArguments);
    
            // 计时器停止
            stopWatch.stop();
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }
    		// 发出启动结束事件
            listeners.started(context);
            // 执行runner的run方法
            this.callRunners(context, applicationArguments);
            
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, listeners);
            throw new IllegalStateException(var10);
        }
    
        try {
            listeners.running(context);
            // 返回spring容器
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    6.那么刷新容器(创建bean)、加载自动化配置信息(很重要)这一步到底做了什么呢?

    /**
    	 * 刷新应用程序上下文
    	 *
    	 * @param context
    	 */
    private void refreshContext(ConfigurableApplicationContext context) {
        // 注册一个关闭钩子,在jvm停止时会触发,然后退出时执行一定的退出逻辑
        if (this.registerShutdownHook) {
            shutdownHook.registerApplicationContext(context);
        }
    	// ApplicationContext真正开始初始化容器和创建bean的阶段
        this.refresh(context);
    }
    
    protected void refresh(ApplicationContext applicationContext) {
        Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);
        refresh((ConfigurableApplicationContext) applicationContext);
    }
    
    protected void refresh(ConfigurableApplicationContext applicationContext) {
        applicationContext.refresh();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    applicationContext.refresh()又是调用的AbstractApplicationContext类的方法,去看一下

    
    @Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
    
            // 第一步:准备更新上下文时的预备工作
            prepareRefresh();
    
            // 第二步:获取上下文内部BeanFactory
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    
            // 第三步:对BeanFactory做预备工作
            prepareBeanFactory(beanFactory);
    
            try {
                // 第四步:允许在上下文子类中对bean工厂进行post-processing
                postProcessBeanFactory(beanFactory);
    
                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                // 第五步:调用上下文中注册为bean的工厂 BeanFactoryPostProcessor
                invokeBeanFactoryPostProcessors(beanFactory);
    
                // 第六步:注册拦截bean创建的拦截器
                registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();
    
                // 第七步:初始化MessageSource(国际化相关)
                initMessageSource();
    
                // 第八步:初始化容器事件广播器(用来发布事件)
                initApplicationEventMulticaster();
    
                // 第九步:初始化一些特殊的bean
                onRefresh();
    
                // 第十步:将所有监听器注册到前两步创建的事件广播器中
                registerListeners();
    
                // 第十一步:结束bean的初始化工作(主要将所有单例BeanDefinition实例化)
                finishBeanFactoryInitialization(beanFactory);
    
                // 第十二步:afterRefresh(上下文刷新完毕,发布相应事件)
                finishRefresh();
            } catch (BeansException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Exception encountered during context initialization - " +
                                "cancelling refresh attempt: " + ex);
                }
                destroyBeans();
                cancelRefresh(ex);
                throw ex;
            } finally {
                
                resetCommonCaches();
                contextRefresh.end();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    以上第五步调用上下文中注册为bean的工厂 BeanFactoryPostProcessor,则是自动装配的过程。

    7.再回到步骤5,最后有一步

    // 执行runner的run方法
    this.callRunners(context, applicationArguments);
    
    • 1
    • 2

    点进去看一下

    private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList();
        // 将所有spring容器中类型为ApplicationRunner和CommandLineRunner的组件加载进来
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        Iterator var4 = (new LinkedHashSet(runners)).iterator();
    
        while(var4.hasNext()) {
            Object runner = var4.next();
            
            if (runner instanceof ApplicationRunner) {
                this.callRunner((ApplicationRunner)runner, args);
            }
    
            if (runner instanceof CommandLineRunner) {
                this.callRunner((CommandLineRunner)runner, args);
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    将所有spring容器中类型为ApplicationRunner和CommandLineRunner的组件加载后,又调用了callRunner()方法,那么这个方法是干嘛的呢?

    private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
        try {
            runner.run(args);
        } catch (Exception var4) {
            throw new IllegalStateException("Failed to execute ApplicationRunner", var4);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    原来是直接调用run()方法执行了,突然感觉好像可以实现这两个接口上然后重写run方法做一些在spring容器创建完bean后做一些别的初始化任务,毕竟spring容器和里面的bean这个时候肯定已经创建好了,而且spring官方特意留了这么一步,应该就是为了让我们使用者可以在SpringBoot启动后立即做一些初始化数据的扩展!!!

    而文章开头说的那种方式也可以实现初始化任务,但是感觉太low,这种方式更好扩展,并且是在返回spring容器之前就进行初始化了,更接近底层。

    事不宜迟,给个例子

    // 实现 ApplicationRunner
    @Component
    public class MyInitRunner1 implements ApplicationRunner {
        
        @Autowired
        private DispatcherService dispatcherService;
        
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println(dispatcherService);
        }
    }
    
    // 实现 CommandLineRunner
    @Component
    public class MyInitRunner2 implements CommandLineRunner {
    
        @Autowired
        private DispatcherService dispatcherService;
    
        @Override
        public void run(String... args) throws Exception {
            System.out.println(dispatcherService);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    看结果

    在这里插入图片描述

    哦吼,果然可以,第一时间被初始化了,而且dispatcherService这个对象是同一个对象(spring的单例模式)。提一嘴:注意java中的单例模式和spring的单例模式不是一回事,java中的单例模式在整个jvm中有且仅有一个对象,但是如果你一个系统有多个spring的容器,那么每个spring的容器中都有一个对象,广泛意义上讲就不是单例了,当然这种一个系统有多个spring容器极低的概率遇到哈哈~~

    再啰嗦一句,如果我们需要配置多个初始化的内容,但是可能创建了多个类或方法,那么如果想要控制一下加载的顺序,我们可以使用@Order注解来控制类或方法的加载顺序。

    @Order(1)
    @Component
    public class MyInitRunner2 implements CommandLineRunner {
    
        @Autowired
        private DispatcherService dispatcherService;
    
        @Override
        public void run(String... args) throws Exception {
            System.out.println("我是runner2");
            System.out.println(dispatcherService);
        }
    }
    
    @Order(2)
    @Component
    public class MyInitRunner1 implements ApplicationRunner {
    
        @Autowired
        private DispatcherService dispatcherService;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println("我是runner1");
            System.out.println(dispatcherService);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    look结果,按照源码来看,如果不设置优先级,那么实现ApplicationRunner的子类MyInitRunner1肯定先被加载,但是我们设置MyInitRunner1的优先级比MyInitRunner2低(@Order中数值越小优先级越搞)

    在这里插入图片描述

    自动装配

    ok咱们继续,自动装配其实是启动类上的@SpringBootApplication注解在起作用,点击去看一下

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    public @interface SpringBootApplication {
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    前四个都是注解基本的元注解,不用咋看

    • @SpringBootConfiguration,这个注解主要是声明这个类是一个配置类,给贴下源码

      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Configuration
      @Indexed
      public @interface SpringBootConfiguration {
          @AliasFor(
              annotation = Configuration.class
          )
          boolean proxyBeanMethods() default true;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • @ComponentScan,比较常见了吧,就是扫描包下的组件用的

    • @EnableAutoConfiguration,这个比较重要

      @Target(ElementType.TYPE)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Inherited
      @AutoConfigurationPackage	// 自动配置包
      @Import(AutoConfigurationImportSelector.class) // 导入 自动配置选择器(主要)
      public @interface EnableAutoConfiguration {
          
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      其中主要是通过导入这个AutoConfigurationImportSelector来实现的,该类重写了ImportSelectorselectImports方法,实现如下

      @Override
      public String[] selectImports(AnnotationMetadata annotationMetadata) {
          if (!isEnabled(annotationMetadata)) {
              return NO_IMPORTS;
          }
          AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
          return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
      }
      
      protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
          // 是否开启自动装配
          if (!isEnabled(annotationMetadata)) {
              // 未开启,返回空
              return EMPTY_ENTRY;
          }
          AnnotationAttributes attributes = getAttributes(annotationMetadata);
          // 读取所有配置(jar包中的+项目中写的)
          List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
          
          // 去重,可能jar包中的+项目中写的有相同的类,直接去重即可
          configurations = removeDuplicates(configurations);
          // 过滤掉被 exclude的类,比如,@SpringBootApplication(exclude = DataSourceAutoConfiguration.class),那么DataSourceAutoConfiguration会被过滤掉
          Set<String> exclusions = getExclusions(annotationMetadata, attributes);
          // 检查被排除类是否可实例化,是否被自动注册配置所使用,不符合条件则抛出异常
          checkExcludedClasses(configurations, exclusions);
          // 从自动配置类集合中删除被排除的类
          configurations.removeAll(exclusions);
          
      	// 检查配置类的注解是否符合 spring.factories 文件中 AutoConfigurationImportFilter 指定的注解检查条件
          configurations = getConfigurationClassFilter().filter(configurations);
          fireAutoConfigurationImportEvents(configurations, exclusions);
          return new AutoConfigurationEntry(configurations, exclusions);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33

      getCandidateConfigurations方法,里面和上文中一样,调用了SpringFactoriesLoader.loadFactoryNames方法,去读取jar包(autoconfiguration)中的META-INF/spring.factories文件或者项目中创建的META-INF/spring.factories文件。

      然后通过去重、根据条件筛选最后把需要配置的给自动装配好。

    总结

    • springboot启动流程
    • 实现ApplicationRunner或者CommandLineRunner可在spring容器创建且创建完bean后进行初始化操作
    • 同样,可在启动类的run()执行后,获取返回值(spring容器),再进行初始化操作
    • 自动装配的重要注解@EnableAutoConfiguration,里面导入的选择器。

    springboot初始化数据扩展

    最后在提一嘴,如何在springboot项目启动后,做一些初始化操作呢?

    • 实现ApplicationRunner或者CommandLineRunner可在spring容器创建且创建完bean后进行初始化操作

      @Component
      public class MyInitRunner1 implements ApplicationRunner {
      
          @Autowired
          private DispatcherService dispatcherService;
      
          @Override
          public void run(ApplicationArguments args) throws Exception {
              System.out.println("我是runner1");
              System.out.println(dispatcherService);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 可在启动类的run()执行后,获取返回值(spring容器),再进行初始化操作

      @SpringBootApplication
      public class DispatcherApplication {
      
          public static void main(String[] args) {
              ConfigurableApplicationContext context = SpringApplication.run(DispatcherApplication.class, args);
              // 获取所有bean的名称
              String[] beanDefinitionNames = context.getBeanDefinitionNames();
              // 获取所有bean的数量
              int beanDefinitionCount = context.getBeanDefinitionCount();
              System.out.println("spring bean中有" + beanDefinitionCount + "个bean组件");
              for (String name : beanDefinitionNames) {
                  System.out.println(name);
              }
              // 根据bean的class获取此bean组件
              DispatcherService bean = context.getBean(DispatcherService.class);
              System.out.println(bean);
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
    • 使用@PostConstruct注解声明在类中的方法上,表示:当此类被初始化后(被调用了构造函数),执行被@PostConstruct声明的方法(注意必须是无返回值的方法,而且此类必须是被spring管理的)

      @Service
      public class DispatcherService {
      
          @PostConstruct
          public void init() {
              System.out.println("我是DispatcherService的init......");
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    看结果图

    在这里插入图片描述

    @PostConstruct注解优先于实现ApplicationRunnerCommandLineRunner接口

    • 当然还有一种方式,实现InitializingBean接口重写afterPropertiesSet()方法,同样,此类必须是由spring管理

      @Component
      public class MyInit implements InitializingBean {
          @Override
          public void afterPropertiesSet() throws Exception {
              System.out.println("我是MyInit....");
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    再看结果

    在这里插入图片描述

    优先级:

    @PostConstruct > 实现InitializingBean接口 > 实现ApplicationRunnerCommandLineRunner接口


    在这里插入图片描述

    在这里插入图片描述

    参考

    参考自

    SpringBoot启动全流程源码解析

    刨析 SpringBoot 自动装配原理,其实很简单

  • 相关阅读:
    Codeforces Round #684 (Div. 1)
    2023-09-11工作错题集:绝了!BigDecimal计算百分数保留2位小数
    来自阿里P8互联网面试官的夺命连环60问,Git命令面试简直是教科式天花板
    AQS之ReentrantLock源码解析
    Java8新特性之Optional
    share.weiyun.com 微云无法打开 解决办法
    .NetCore+Vue2.0前后端分离的个人博客项目
    vue 预览视频
    训练人工智能机器人的软实力
    认识Ruby
  • 原文地址:https://blog.csdn.net/weixin_45248492/article/details/126272081