SpringBoot源码分析

SpringBoot 源码分析

一、源码分析 - 自动装配

1、@SpringBootApplication

// 自动装配的开始
@SpringBootApplication
public class BasicProjectApplication {
   
    public static void main(String[] args) {
   
        SpringApplication.run(BasicProjectApplication.class, args);
    }
}
@Target({
   ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// @Configuration的子注解
@SpringBootConfiguration
// 开启自动装配
@EnableAutoConfiguration
// 开启扫描机制,扫描启动类所在包(但是不包含类型排除和自动配置的Filter)
@ComponentScan(excludeFilters = {
    
    @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) 
})
public @interface SpringBootApplication {
   .....}

2、@EnableAutoConfiguration

package org.springframework.boot.autoconfigure;

@Target({
   ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
// 通过 @Import 引入 AutoConfigurationImportSelector 
@Import({
   AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
   
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {
   };

    String[] excludeName() default {
   };
}

3、AutoConfigurationImportSelector

ImportSelector 接口用于实现动态地选择需要被导入到容器中的配置类的逻辑。

public interface ImportSelector {
   

    /**
     * @param importingClassMetadata 用于描述使用了 @Import 注解的配置类的注解元数据。
     * @return 数组中的每个元素都是一个全限定类名,表示需要被导入到容器中的配置类。
     */ 
    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
	default Predicate<String> getExclusionFilter() {
   
		return null;
	}
}

AutoConfigurationImportSelector 实现了 ImportSelector 接口,并在 selectImports 方法中根据特定的条件或逻辑来选择需要导入的配置类,实现更加灵活和动态的配置。

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
   

    // 这里的参数 annotationMetadata,就是 @EnableAutoConfiguration 注解的元数据
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
   
        if (!this.isEnabled(annotationMetadata)) {
   
            return NO_IMPORTS;
        } else {
   
            AutoConfigurationEntry autoConfigurationEntry = 
                // 获取自动配置类
                this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

    protected static class AutoConfigurationEntry {
   
        private final List<String> configurations;	// 需要导入的
        private final Set<String> exclusions;		// 不需要导入的
    }

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
   
        if (!this.isEnabled(annotationMetadata)) {
   
            return EMPTY_ENTRY;
        } else {
   
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = 
                // 获取所有候选的配置类
                this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.getConfigurationClassFilter().filter(configurations);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationEntry(configurations, exclusions);
        }
    }

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   
        // 获取所有配置类的类全名
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            this.getSpringFactoriesLoaderFactoryClass(),	// EnableAutoConfiguration.class
            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;
    }

}

4、SpringFactoriesLoader

// org.springframework.core.io.support.SpringFactoriesLoader

/**
 * 这里的 factoryType 就是 EnableAutoConfiguration.class
 */
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
   
    // org.springframework.boot.autoconfigure.EnableAutoConfiguration
    String factoryTypeName = factoryType.getName();
    // 获取 META-INF/spring.factories 文件中 EnableAutoConfiguration 对应的配置类的类全名
    return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
   
    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
    if (result != null) {
   
        return result;
    } else {
   
        try {
   
            // 扫描jar包路径下的 META-INF/spring.factories 文件
            Enumeration<URL> urls = classLoader != null ? 
                classLoader.getResources("META-INF/spring.factories") : 
            ClassLoader.getSystemResources("META-INF/spring.factories");
            
            // 把 META-INF/spring.factories 解析成 Map
            MultiValueMap<String, String> result = new LinkedMultiValueMap();
            
            // ...

            cache.put(classLoader, result);
            return result;
        } catch (IOException var13) {
   
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
        }
    }
}

5、META-INF/spring.factories

下面看一下 META-INF/spring.factories 里的 EnableAutoConfiguration 对应的配置类

在这里插入图片描述

我们在里面挑一个常用的 DataSourceAutoConfiguration类看一下

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({
   DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(type = {
   "io.r2dbc.spi.ConnectionFactory"})
@EnableConfigurationProperties({
   DataSourceProperties.class})
@Import({
   
    DataSourcePoolMetadataProvidersConfiguration.class, 
    DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
   

}
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
   
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    // ...
}

通过 @ConfigurationProperties + @EnableConfigurationProperties 自动读取配置文件中的配置

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: .....
    username: root
    password: root

6、SpringMVC相关装配

在这里插入图片描述

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({
   Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({
   WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
// 在 DispatcherServletAutoConfiguration 配置类之后配置
@AutoConfigureAfter({
   DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
   
    // MVC的相关的配置.....
}
@AutoConfigureOrder(Integer.MIN_VALUE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({
   DispatcherServlet.class})
// 在 ServletWebServerFactoryAutoConfiguration 配置类之后配置
@AutoConfigureAfter({
   ServletWebServerFactoryAutoConfiguration.class})
public class DispatcherServletAutoConfiguration {
   
    
    @Configuration(proxyBeanMethods = false)
    @Conditional({
   DefaultDispatcherServletCondition.class})
    @ConditionalOnClass({
   ServletRegistration.class})
    @EnableConfigurationProperties({
   WebMvcProperties.class})
    protected static class DispatcherServletConfiguration {
   
        protected DispatcherServletConfiguration() {
   
        }

        // 前端控制器 DispatcherServlet
        @Bean(name = {
   "dispatcherServlet"})
        public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
   
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            // ...
            return dispatcherServlet;
        }

        // 文件上传相关bean
        @Bean
        @ConditionalOnBean({
   MultipartResolver.class})
        @ConditionalOnMissingBean(name = {
   "multipartResolver"})
        public MultipartResolver multipartResolver(MultipartResolver resolver) {
   
            return resolver;
        }
    }
}
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Integer.MIN_VALUE)
@ConditionalOnClass({
   ServletRequest.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties({
   ServerProperties.class})
@Import({
   
    BeanPostProcessorsRegistrar.class, 
    ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,	// 内置Tomcat
    ServletWebServerFactoryConfiguration.EmbeddedJetty.class, 
    ServletWebServerFactoryConfiguration.EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
   ...}

二、源码分析 - 启动加载

1、SpringApplication - 静态run

看完了自动装配,我们继续看一下启动加载,还是从启动类开始

@SpringBootApplication
public class BasicProjectApplication {
   
    public static void main(String[] args) {
   
        SpringApplication.run(BasicProjectApplication.class, args);
    }
}
// org.springframework.boot.SpringApplication

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

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
   
    // 构建 SpringApplication实例 并执行run方法
    return (new SpringApplication(primarySources)).run(args);
}

可以看到,启动类中的 SpringApplication.run ,最终会先构建 SpringApplication实例,再执行实例的run方法。

2、SpringApplication - 构造方法

// org.springframework.boot.SpringApplication

// SpringApplication的构造方法
public SpringApplication(Class<?>... primarySources) {
   
    this((ResourceLoader)null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
   
    this.sources = new LinkedHashSet();
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    this.addCommandLineProperties = true;
    this.addConversionService = true;
    this.headless = true;
    this.registerShutdownHook = true;
    this.additionalProfiles = new HashSet();
    this.isCustomEnvironment = false;
    this.lazyInitialization = false;

    // 资源加载器
    this.resourceLoader = resourceLoader;

    // primarySources 就是 启动类
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));

    // 判断Web程序的类型(无、基于Servlet的、基于响应式编程模型的)
    this.webApplicationType = WebApplicationType.deduceFromClasspath();

    // getSpringFactoriesInstances 就是获取 META-INF/spring.factories 中对应类的实例
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));

    // 推断 Spring Boot 应用程序的主类
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
   
    return this.getSpringFactoriesInstances(type, new Class[0]);
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
   
    ClassLoader classLoader = this.getClassLoader();
    // loadFactoryNames应该很熟悉了,在「自动装配」里边讲过
    Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 构建实例
    List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // 排序返回
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

3、SpringApplication - 实例run

构建完 SpringApplication 实例之后,就开始调用实例的run方法了

// org.springframework.boot.SpringApplication

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

public ConfigurableApplicationContext run(String... args) {
   
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    
    // Spring容器
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
    this.configureHeadlessProperty();

    // 开始监听
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting();

    Collection exceptionReporters;
    try {
   
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 准备Environment
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
        this.configureIgnoreBeanInfo(environment);
        // 打印程序启动时的横幅标语
        Banner printedBanner = this.printBanner(environment);
        // 构建Spring容器
        context = this.createApplicationContext();
        exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{
   ConfigurableApplicationContext.class}, context);
        // 准备IoC容器
        this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        // IoC容器的初始化
        this.refreshContext(context);
        // IoC容器初始化之后的一些操作
        this.afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
   
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
        }

        listeners.started(context);
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
   
        this.handleRunFailure(context, var10, exceptionReporters, listeners);
        throw new IllegalStateException(var10);
    }

    try {
   
        listeners.running(context);
        return context;
    } catch (Throwable var9) {
   
        this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
        throw new IllegalStateException(var9);
    }
}

4、IoC容器的初始化

在执行 SpringApplication 实例的run方法时,通过refreshContext方法进行IoC容器的初始化

// org.springframework.boot.SpringApplication

private void refreshContext(ConfigurableApplicationContext context) {
   
    refresh((ApplicationContext) context);
    if (this.registerShutdownHook) {
   
        try {
   
            context.registerShutdownHook();
        }
        catch (AccessControlException ex) {
   
            // Not allowed in some environments.
        }
    }
}

@Deprecated
protected void refresh(ApplicationContext applicationContext) {
   
    Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);
    refresh((ConfigurableApplicationContext) applicationContext);
}

protected void refresh(ConfigurableApplicationContext applicationContext) {
   
    // IoC容器的初始化
    applicationContext.refresh();
}

然后,就到我们熟悉的 refresh() 方法了,总共有12大步骤(这属于Spring源码的内容,这里就不展开了)

// org.springframework.context.support.AbstractApplicationContext

@Override
public void refresh() throws BeansException, IllegalStateException {
   
    synchronized (this.startupShutdownMonitor) {
   
        // Prepare this context for refreshing.
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
   
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);

            // Register bean processors that intercept bean creation.
            registerBeanPostProcessors(beanFactory);

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            // Initialize other special beans in specific context subclasses.
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // Instantiate all remaining (non-lazy-init) singletons.
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        } 

        catch (BeansException ex) {
   
            // logger...

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset 'active' flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
   
            // Reset common introspection caches in Spring's core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
        }
    }
}

5、内置Tomcat原理

我们看到 refresh() 方法12大步骤中的 onRefresh() 方法,默认是空实现

// org.springframework.context.support.AbstractApplicationContext

protected void onRefresh() throws BeansException {
   
    // For subclasses: do nothing by default.
}

ServletWebServerApplicationContext 重写了onRefresh方法:

// org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext

@Override
protected void onRefresh() {
   
    super.onRefresh();
    try {
   
        // tomcat容器就是在这里创建并启动的
        createWebServer();
    }
    catch (Throwable ex) {
   
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

private void createWebServer() {
   
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
   
        ServletWebServerFactory factory = getWebServerFactory();
        // 创建web容器
        this.webServer = factory.getWebServer(getSelfInitializer());
        getBeanFactory().registerSingleton("webServerGracefulShutdown",
                                           new WebServerGracefulShutdownLifecycle(this.webServer));
        getBeanFactory().registerSingleton("webServerStartStop",
                                           new WebServerStartStopLifecycle(this, this.webServer));
    }
    else if (servletContext != null) {
   
        try {
   
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
   
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}
// org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
   
    if (this.disableMBeanRegistry) {
   
        Registry.disableRegistry();
    }
    // 创建Tomcat
    Tomcat tomcat = new Tomcat();
    File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    Connector connector = new Connector(this.protocol);
    connector.setThrowOnFailure(true);
    tomcat.getService().addConnector(connector);
    customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    configureEngine(tomcat.getEngine());
    for (Connector additionalConnector : this.additionalTomcatConnectors) {
   
        tomcat.getService().addConnector(additionalConnector);
    }
    prepareContext(tomcat.getHost(), initializers);
    // 启动Tomcat
    return getTomcatWebServer(tomcat);
}

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
   
    return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}

public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
   
    Assert.notNull(tomcat, "Tomcat Server must not be null");
    this.tomcat = tomcat;
    // 自启动
    this.autoStart = autoStart;
    this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
    // 初始化
    initialize();
}

private void initialize() throws WebServerException {
   
    logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
    synchronized (this.monitor) {
   
        try {
   
            // ...
            
            this.tomcat.start();	// 启动Tomcat
            
            // ...
        } 
        // ...
    }
}

三、自动装配 - 相关注解

1、自动装配 - 条件注解

@Conditional 是 Spring 框架中的一个元注解,根据注解的条件来决定是否应该应用某个配置类或组件。

Spring 还提供了许多子注解,用于更精细地定义条件。

在这里插入图片描述

常用注解 描述
@ConditionalOnBean 容器中存在指定的 Bean 时,配置才会生效。
@ConditionalOnMissingBean 容器中不存在指定的 Bean 时,配置才会生效。
@ConditionalOnClass 当指定的类存在时,配置才会生效。
@ConditionalOnMissingClass 当指定的类不存在时,配置才会生效。
@ConditionalOnWebApplication 只有在web环境下,配置才会生效。
@ConditionalOnNotWebApplication 只有在非web环境下,配置才会生效。
@ConditionalOnJava 系统的Java版本符合需求,配置才会生效。
@ConditionalOnExpression 满足指定的SpEL表达式,配置才会生效。
@ConditionalOnProperty 指定的属性有指定的值,配置才会生效。
@ConditionalOnResource 类路径下存在指定资源文件,配置才会生效。
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或Bean是首选Bean

2、自动装配 - 顺序注解

注解 描述
@AutoConfigureAfter 在指定配置类加载之后加载
@AutoConfigureBefore 在指定配置类加载之前加载
@AutoConfigureOrder 指定加载配置的优先级,默认0

四、自定义 starter

看完自动装配的源码,我们可以尝试自定义 starter,来实现自动装配。

1、pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <!-- 定义starter的坐标 -->
    <groupId>jwt-utils</groupId>
    <artifactId>jwt-spring-boot-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    
    <name>jwt-spring-boot-starter</name>
    
</project>

2、Properties实体类

@Data
@ConfigurationProperties(prefix = "auth")
public class ClientProperties {
   
    /**
     * 客户端名称
     */
    private String clientId;
    /**
     * 客户端秘钥
     */
    private String secret;
    /**
     * 拦截器拦截路径
     */
    private List<String> includeFilterPaths;
    /**
     * 拦截器放行路径
     */
    private List<String> excludeFilterPaths;
}

3、Config配置类

@Slf4j
@Configuration
// 只有配置了 auth.clientId 和 auth.secret 属性,当前配置才会生效。
@ConditionalOnProperty(prefix = "auth", name = {
   "clientId", "secret"})
@EnableConfigurationProperties(ClientProperties.class)
public class AuthAutoConfiguration {
   
    // 实现一些starter的逻辑....
}

4、spring.factories

resources/META-INF/spring.factories 目录下编写

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.auth.config.AuthAutoConfiguration

5、使用自动装配

在要使用的模块的pom文件中添加starter依赖

<dependency>
    <groupId>jwt-utils</groupId>
    <artifactId>jwt-spring-boot-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

application.yaml文件添加配置

auth:
  clientId: user-service
  secret: 1234
  includeFilterPaths:
  - /path1
  - /path2

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-02-23 09:22:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-23 09:22:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-23 09:22:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-23 09:22:03       20 阅读

热门阅读

  1. 【npm install报错,如何解决记录】讲解

    2024-02-23 09:22:03       32 阅读
  2. 汽车会撞死人,应不应该限制汽车?

    2024-02-23 09:22:03       30 阅读
  3. lua 拓展math库,增加四舍五入函数 math.round

    2024-02-23 09:22:03       29 阅读
  4. Docker的优势及实际应用

    2024-02-23 09:22:03       23 阅读
  5. LeetCode //C - 901. Online Stock Span

    2024-02-23 09:22:03       31 阅读