Java学习,一文掌握Java之SpringBoot框架学习文集(3)

发布时间:2024年01月04日

在这里插入图片描述

🏆作者简介,普修罗双战士,一直追求不断学习和成长,在技术的道路上持续探索和实践。
🏆多年互联网行业从业经验,历任核心研发工程师,项目技术负责人。
🎉欢迎 👍点赞?评论?收藏

SpringBoot知识专栏学习

SpringBoot知识云集访问地址备注
SpringBoot知识点(1)https://blog.csdn.net/m0_50308467/article/details/135322153SpringBoot专栏
SpringBoot知识点(2)https://blog.csdn.net/m0_50308467/article/details/135323253SpringBoot专栏
SpringBoot知识点(3)https://blog.csdn.net/m0_50308467/article/details/135373512SpringBoot专栏

🏆 学习Java框架之Spring Boot

🔎 Java框架之Spring Boot学习(3)

🍁🍁 01、Spring Boot 中的自动配置是怎样实现的?

在 Spring Boot 中,自动配置(Auto-configuration)是一种基于条件化(Conditional)的配置方式。它可以根据应用的 classpath、依赖和配置来自动配置 Spring 应用程序,而无需进行过多的手动配置。

Spring Boot 的自动配置实际上是通过 Spring 的条件化注解来实现的,主要有 @ConditionalOnClass@ConditionalOnMissingClass@ConditionalOnBean@ConditionalOnMissingBean@ConditionalOnProperty@ConditionalOnResource@ConditionalOnWebApplication 等。

这些注解通过判断应用所引入的类、Bean 等是否存在,或者应用的配置属性、资源、Web 应用类型是否符合特定的条件,来决定是否生效该自动配置。如果满足条件,就会创建对应的 Bean、配置信息等。如果不满足,对应的 Bean、配置信息就会忽略或移除。

Spring Boot 还提供了 @EnableAutoConfiguration 注解,它可以根据应用的 classpath 来自动启动并配置 Spring Boot 应用程序的各种组件和功能。具体来说,它会从 META-INF/spring.factories 中加载自动配置类,并根据自动配置的条件判断来决定是否生效。

通过自动配置,Spring Boot 可以让开发者专注于业务开发而无需关心繁琐的配置。同时也能提高开发效率、降低开发成本和错误率。因此,自动配置是 Spring Boot 框架的一个重要特性。

🍁🍁 02、Spring Boot 中的条件注解是如何工作的?

在 Spring Boot 中,条件注解(Conditional Annotations)是一种用于根据特定条件来装配 Bean 和配置组件的机制。条件注解允许我们基于运行时环境、配置属性或其他条件来动态地确定是否应该创建特定的 Bean。

Spring Boot 提供了一系列内置的条件注解,例如:

  1. @ConditionalOnClass:当类路径中存在指定的类时,条件匹配。

  2. @ConditionalOnMissingClass:当类路径中不存在指定的类时,条件匹配。

  3. @ConditionalOnBean:当 Spring 上下文中存在指定的 Bean 时,条件匹配。

  4. @ConditionalOnMissingBean:当 Spring 上下文中不存在指定的 Bean 时,条件匹配。

  5. @ConditionalOnProperty:当指定的配置属性存在且值满足条件时,条件匹配。

  6. @ConditionalOnResource:当指定的资源存在时,条件匹配。

  7. @ConditionalOnWebApplication:当应用程序是一个 Web 应用程序时,条件匹配。

这些条件注解可以根据应用的具体情况以及自定义的条件进行组合和定制。它们可以应用在配置类、Bean 方法上,通过条件判断来决定是否加载、注册相关组件。当满足条件时,Bean 或配置会被创建和应用;当不满足条件时,它们会被忽略或者被移除。

条件注解的工作原理是基于 Spring 的条件化(Conditional)机制。条件注解本质上是一个条件化注解,它会在应用启动时进行条件判断,并根据条件结果来决定是否启用相关的 Bean 或配置。只有满足特定条件的注解才会生效。这样可以根据不同的条件来定制化地配置应用,提供更灵活的自动装配机制。

以下给你举几个条件注解的示例。

1. @ConditionalOnClass:当类路径中存在指定的类时,条件匹配。

@Configuration
@ConditionalOnClass(MyClass.class)
public class MyConfiguration {
    // ... 定义需要配置的 Bean 或组件 ...
}

在这个例子中,MyConfiguration 类只有在类路径中存在 MyClass.class 类时才会被装配为配置类。否则,该配置类将被忽略。

2. @ConditionalOnBean:当 Spring 上下文中存在指定的 Bean 时,条件匹配。

@Configuration
@ConditionalOnBean(DataSource.class)
public class MyConfiguration {
    // ... 定义需要配置的 Bean 或组件 ...
}

在这个例子中,MyConfiguration 类只有当 Spring 上下文中存在 DataSource Bean 时,才会被装配为配置类。如果不存在该 Bean,该配置类将被忽略。

3. @ConditionalOnProperty:当指定的配置属性存在且值满足条件时,条件匹配。

@Configuration
@ConditionalOnProperty(prefix = "myapp", name = "enabled", havingValue = "true")
public class MyConfiguration {
    // ... 定义需要配置的 Bean 或组件 ...
}

在这个例子中,MyConfiguration 类只有当名为 myapp.enabled 的配置属性值为 true 时,才会被装配为配置类。如果配置属性不存在或者值不满足条件,该配置类将被忽略。

🍁🍁 03、@ConditionalOnBean 和 @ConditionalOnMissingBean 有什么区别?

在 Spring Boot 中,@ConditionalOnBean@ConditionalOnMissingBean 是两个条件注解,用于在装配 Bean 或配置组件时进行条件判断。它们的区别如下所示:

1. @ConditionalOnBean:该注解的条件是判断 Spring 上下文中是否存在指定的 Bean。如果存在指定的 Bean,则条件匹配,相关的 Bean 或配置组件会被加载和应用。

@Configuration
@ConditionalOnBean(DataSource.class)
public class MyConfiguration {
    // ... 定义需要配置的 Bean 或组件 ...
}

上面的例子中,只有当 Spring 上下文中存在 DataSource Bean 时,MyConfiguration 类才会被装配为配置类。

2. @ConditionalOnMissingBean:该注解的条件是判断 Spring 上下文中是否不存在指定的 Bean。如果不存在指定的 Bean,则条件匹配,相关的 Bean 或配置组件会被加载和应用。

@Configuration
@ConditionalOnMissingBean(DataSource.class)
public class MyConfiguration {
    // ... 定义需要配置的 Bean 或组件 ...
}

上面的例子中,只有当 Spring 上下文中不存在 DataSource Bean 时,MyConfiguration 类才会被装配为配置类。

简而言之,@ConditionalOnBean 用于判断 Bean 是否存在,条件匹配时加载和应用相关的组件;而 @ConditionalOnMissingBean 则用于判断 Bean 是否不存在,条件匹配时加载和应用相关的组件。

总结一下它们的区别,并列出一个简单的表格:

条件注解条件判断说明条件匹配时是否加载配置类
@ConditionalOnBean判断是否存在指定的 Bean,存在则条件匹配是,加载相关的 Bean 或配置类
@ConditionalOnMissingBean判断是否不存在指定的 Bean,不存在则条件匹配是,加载相关的 Bean 或配置类

总之,这两个注解是常用的 Spring Boot 条件注解之一,它们通常与其它条件注解、自动配置类等一起使用,帮助您更加灵活地控制应用程序的自动配置和启动过程。

🍁🍁 04、Spring Boot 的自动配置是如何解析属性的?

在 Spring Boot 中,自动配置属性的解析是通过 @ConfigurationProperties 注解实现的。@ConfigurationProperties 注解用于指定属性的前缀,并根据属性的命名规则自动映射到对应的实体类中。

以下是 Spring Boot 自动配置属性解析的流程:

1. 在自动配置类中,使用 @ConfigurationProperties 注解来指定属性的前缀。

@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
    private String name;
    private int age;
    // ... 其他属性的getter和setter方法 ...
}

上面的例子中,所有以 myapp 开头的属性都会映射到 MyAppProperties 类中。

2. 在应用的 application.propertiesapplication.yml 文件中,设置对应的属性值。

myapp.name=My Application
myapp.age=20

3. 在启动时,自动配置会将应用配置文件中的属性值解析并映射到 MyAppProperties 实体类的相关属性中。

4. 在启动过程中,可以通过 @Autowired@Resource 等注解将 MyAppProperties 注入到其他需要使用它的类中。

@RestController
public class MyController {
    @Autowired
    private MyAppProperties myAppProperties;
}

上面的例子中,MyAppProperties 的实例会被注入到 MyController 类中,从而可以直接使用其中定义的属性。

通过以上流程,Spring Boot 实现了自动配置属性的解析。使用 @ConfigurationProperties 注解,结合应用配置文件,可以方便地将应用的配置信息自动映射到指定的实体类中,从而实现属性的自动配置。

🍁🍁 05、如何自定义 Spring Boot 的自动配置类?

想要自定义 Spring Boot 的自动配置类,有三种常用的方式:

  1. 最简单的方式是通过在 @Configuration 注解的类中定义 @Bean@Conditional 方法来重写 Spring Boot 的默认配置。这种方式可以覆盖某些默认配置,而不会影响其它自动配置。

  2. 另外一种方式是通过创建一个新的自动配置类来覆盖默认的自动配置类。这种方式可以更改更多的默认配置,但也可能会影响其它自动配置。

  3. 最后一种方式是通过创建一个外部的配置类来修改默认的配置。外部配置类可以通过 SpringApplication.setDefaultProperties(...) 方法来设置默认配置,或者通过在 application.propertiesapplication.yml 中添加配置来修改默认配置。

下面我们一一介绍这三种方式:

1. 通过定义 @Bean@Conditional 方法来重写默认配置

这种方式是最简单的方式,只需要在 @Configuration 注解的类中定义特定的 @Bean@Conditional 方法即可。例如,下面的示例代码定义了一个新的 DataSource Bean,并覆盖默认的 DataSourceAutoConfiguration 自动配置:

@Configuration
public class DataSourceConfig {
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        // ... 创建并返回一个新的 DataSource ...
    }
}

这里使用 @ConditionalOnMissingBean 注解来判断当前应用程序上下文中是否已存在 DataSource Bean。如果已存在,则不会创建新的 DataSource,否则将会创建一个新的 DataSource Bean,并使用它来替换默认的 DataSourceAutoConfiguration 自动配置。这里的 dataSource() 方法只是一个示例,实际上可以根据需要定义任意数量的 Bean@Conditional 方法来覆盖默认的自动配置。

2. 通过创建新的自动配置类来覆盖默认的自动配置类

这种方式需要创建一个新的自动配置类,并使用 @ConditionalOnClass@ConditionalOnBean@ConditionalOnProperty 等注解来控制该自动配置类的生效范围。例如,下面的示例代码创建了一个新的 AwesomeServiceAutoConfiguration 自动配置类,并覆盖了默认的 MyServiceAutoConfiguration 自动配置:

@Configuration
@ConditionalOnClass(AwesomeService.class)
@EnableConfigurationProperties(AwesomeServiceProperties.class)
public class AwesomeServiceAutoConfiguration {
    @Autowired
    private AwesomeServiceProperties properties;

    @Bean
    @ConditionalOnMissingBean
    public AwesomeService awesomeService() {
        // ... 根据配置创建并返回 AwesomeService ...
    }
}

这里通过 @ConditionalOnClass 注解来判断当前类路径下是否存在 AwesomeService 类。如果存在,则自动配置将会生效,否则自动配置将不会生效。然后使用 @EnableConfigurationProperties 注解来启用该自动配置类所需的配置类,最后定义 awesomeService() 方法来创建 AwesomeService Bean,并使用 @ConditionalOnMissingBean 注解来判断当前应用程序上下文中是否已存在该 Bean。如果已存在,则不会创建新的 AwesomeService Bean,否则将会创建一个新的 AwesomeService Bean,并用它来替换默认的 MyServiceAutoConfiguration 自动配置。

3. 通过创建外部的配置类来修改默认的配置

这种方式需要在 application.propertiesapplication.yml 中添加配置。例如,下面的示例代码使用 myapp.server.port 属性来修改默认的服务端口号配置:

server.port=8080
myapp.server.port=8888

在这个示例中,我们添加了一个名为 myapp.server.port 的新属性,它将会覆盖默认的 server.port 服务端口号配置。

总之,以上三种方式都是常用的自定义 Spring Boot 自动配置的方法。选择哪种方式,应该根据具体情况和需求来决定。

🍁🍁 06、Spring Boot 如何处理循环依赖?

Spring Boot 可以处理简单的循环依赖,但是对于复杂的循环依赖则需要使用特殊的处理方式。

简单的循环依赖是指基于属性的 setter 依赖或构造函数注入时的循环依赖。Spring Boot 可以通过提前暴露其中一个 bean 实例,例如通过将某个依赖的 bean 实例包装在一个 ObjectHolder 中来暴露,从而解决简单的循环依赖。对于最简单的情况,例如一个类 A 依赖于另一个类 B,而 B 又依赖于 A 实例的一个属性,在构造函数注入时,Spring Boot 会自动将一个不完整的 A 实例暴露给 B,然后在构造函数完成之后,再将完整的 A 实例注入到 B 中。

但是,如果存在复杂的循环依赖,例如循环依赖环,即 A -> B -> C -> A,Spring Boot 就不能处理了。在这种情况下,通常需要使用延迟注入来解决循环依赖。延迟注入是指不在初始化时立即注入 bean,而是在使用时进行注入。Spring Boot 中可以使用 @Lazy 注解来实现延迟注入。

例如:

@Component
public class A {
    @Autowired
    @Lazy
    private B b;
    // ...
}

@Component
public class B {
    @Autowired
    private C c;
    // ...
}

@Component
public class C {
    @Autowired
    private A a;
    // ...
}

在上面的示例中,A 依赖于 B,B 依赖于 C,而 C 又依赖于 A。这种循环依赖环无法通过默认的 Spring Boot 依赖注入机制处理。但是,通过使用 @Lazy 注解,我们可以将 A 中的 B 延迟注入,直到需要使用它时再进行注入。这样就可以解决循环依赖问题了。

总之,Spring Boot 可以处理简单的循环依赖,但对于复杂的循环依赖,需要使用特殊的处理方式,例如延迟注入。在实际应用中,应该尽量避免使用复杂的循环依赖,保持依赖注入的单向性,有助于提高应用程序的可维护性。

除了使用延迟注入,还有一些其他的方法可以处理 Spring Boot 中的复杂循环依赖问题,例如使用构造函数注入、通过接口实现注入等。下面是一些解决循环依赖问题的常用方法:

1. 使用构造函数注入

对于复杂的循环依赖问题,使用构造函数注入是一种常见的解决方案。通过使用构造函数注入,可以强制 Spring Boot 在初始化 bean 的过程中显式指定所有必需的依赖项,从而解决循环依赖问题。示例代码如下:

@Component
public class A {
    private B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }
    // ...
}

@Component
public class B {
    private C c;

    @Autowired
    public B(C c) {
        this.c = c;
    }
    // ...
}

@Component
public class C {
    private A a;

    @Autowired
    public C(A a) {
        this.a = a;
    }
    // ...
}

在上面的示例中,每个 bean 都将其依赖项通过构造函数注入进来。这样就可以避免循环依赖问题。

2. 通过接口实现注入

另一种处理复杂循环依赖的方法是使用接口实现注入。通过使用接口注入,可以实现循环依赖的两个 bean 分别依赖于自己的扩展接口,而不是依赖于对方的类实现。示例代码如下:

public interface AInterface {
    void setB(BInterface b);
}

@Component
public class A implements AInterface {
    private BInterface b;

    @Override
    @Autowired
    public void setB(BInterface b) {
        this.b = b;
    }
    // ...
}

public interface BInterface {
    void setC(CInterface c);
}

@Component
public class B implements BInterface {
    private CInterface c;

    @Override
    @Autowired
    public void setC(CInterface c) {
        this.c = c;
    }
    // ...
}

public interface CInterface {
    void setA(AInterface a);
}

@Component
public class C implements CInterface {
    private AInterface a;

    @Override
    @Autowired
    public void setA(AInterface a) {
        this.a = a;
    }
    // ...
}

在上面的示例中,A、B 和 C 分别实现了自己的扩展接口,然后通过接口注入各自的依赖关系。这种方法通过抽象和分离依赖关系,使得 Spring Boot 可以轻松地处理循环依赖问题。

总之,处理 Spring Boot 中的循环依赖问题需要根据具体情况进行选择。常用的解决方法包括使用延迟注入、构造函数注入和通过接口实现注入等。在实际应用中,应该尽量避免出现复杂的循环依赖问题,保证依赖注入的单向性,有助于提高应用程序的可维护性。

🍁🍁 07、Spring Boot 中的热部署是如何实现的?

在 Spring Boot 中实现热部署有多种方法,下面介绍其中两种常用的方法:

1. 使用 Spring Boot DevTools

Spring Boot DevTools 是一个开发工具,可以帮助实现热部署。它使用了两个关键技术:

  • 应用程序快速重启:DevTools 监听文件系统的更改,当项目文件发生变化时,它会自动触发应用程序的重启。
  • 类加载器触发器:DevTools 使用另一个类加载器加载应用程序,这样应用程序代码的改变就可以在不重启应用程序的情况下生效。

要启用 Spring Boot DevTools,只需要将以下依赖项添加到项目的 pom.xml 文件中:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

使用 DevTools 启动应用程序后,当你修改了任何源代码文件、资源文件或配置文件时,它会自动重新加载应用程序。你可以在 IDE 中编辑文件并保存,或者使用命令行工具进行相应的更改。

2. 使用 Spring Loaded

Spring Loaded 是一个开源项目,用于支持在运行时动态加载修改后的类。它通过 Java 的 HotSwap 技术实现热交换。

要使用 Spring Loaded,首先需要将其引入项目的开发依赖中:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>springloaded</artifactId>
    <version>1.2.8.RELEASE</version>
    <scope>provided</scope>
</dependency>

然后,在该项目的启动脚本中加入 -javaagent 参数,指定 Spring Loaded 的代理模块:

-javaagent:path/to/springloaded-{version}.jar

启动项目后,当你修改了相应的类文件时,Spring Loaded 会在后台自动加载并重新定义该类,实现热部署。

需要注意的是,Spring Boot DevTools 和 Spring Loaded 都是用于开发环境,在生产环境中并不推荐使用。在生产环境中,可以使用 Docker、Kubernetes 等部署技术来实现无缝的应用程序更新和滚动升级。

🍁🍁 08、Spring Boot 中的配置优先级是怎样的?

在 Spring Boot 中,配置的优先级是按照以下规则确定的:

1. 命令行参数(Command Line Arguments):命令行参数提供了最高的优先级。可以在运行 Spring Boot 应用程序时使用 --<option>=<value> 的格式来传递参数,例如 java -jar myproject.jar --server.port=8080

2. 系统属性(System Properties):可以通过在 JVM 启动时使用 -D<option>=<value> 的方式来设置系统属性。例如:java -Dserver.port=8080 -jar myproject.jar

3. 环境变量(Environment Variables):可以通过设置环境变量来配置应用程序。Spring Boot 将环境变量作为默认的属性源,同时支持使用下划线(_)或点号(.)来分隔层次结构。例如,设置环境变量 SERVER_PORT=8080

4. 配置文件(Application Properties):Spring Boot 默认支持使用 .properties.yml 文件来配置应用程序。可以通过在 src/main/resourcesclasspath:/config 目录下添加相应的配置文件来覆盖默认配置。针对不同的环境,可以使用不同的文件名,例如 application-dev.propertiesapplication-prod.yml,或者使用 spring.profiles.active 属性来指定活动的配置文件。可以使用 spring.config.namespring.config.location 属性来定制配置文件的名称和位置。

5. 默认配置(Default Configuration):Spring Boot 提供了一组默认的配置,例如默认的数据库连接池、默认的端口号等。当没有其他配置来源时,应用程序将使用这些默认配置。

在优先级较高的配置选项中,更具体的配置会覆盖更一般的配置。例如,命令行参数会覆盖配置文件中的参数,而配置文件中的参数会覆盖默认配置。

🍁🍁 09、如何在 Spring Boot 中实现异步调用?

在 Spring Boot 中实现异步调用有多种方法,下面介绍其中两种常用的方法:

1. 使用 @Async 注解

Spring Boot 提供了 @Async 注解,它可以应用在方法上,用来指示该方法是一个异步方法。在使用 @Async 注解时,需要注意以下几点:

  • 在 Spring Boot 应用程序的启动类上添加 @EnableAsync 注解,以启用异步执行功能。
  • 在异步方法上添加 @Async 注解,以标识该方法应该在单独的线程中执行。
  • 如果异步方法需要返回结果,可以将其返回类型设置为 Future<T>,其中 T 是返回结果的类型。

下面是一个示例:

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.Future;

@Component
public class MyService {

    @Async
    public void asyncTask() {
        // 异步执行的任务
    }

    @Async
    public Future<String> asyncTaskWithResult() {
        // 异步执行的任务,并返回结果
        return new AsyncResult<>("Task completed");
    }
}

2. 使用 CompletableFuture

Java 8 引入了 CompletableFuture 类,它提供了更灵活的方式来处理异步操作。在 Spring Boot 中,可以使用 CompletableFuture 来实现异步调用。下面是一个示例:

import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;

@Component
public class MyService {

    public CompletableFuture<String> asyncTask() {
        return CompletableFuture.supplyAsync(() -> {
            // 异步执行的任务
            return "Task completed";
        });
    }
}

在调用该方法时,会立即返回一个 CompletableFuture 对象,可以通过该对象获取异步操作的结果。

需要注意的是,上述方法需要在 Spring Boot 启动类上添加 @EnableAsync 注解启用异步执行功能,或者在配置类中使用 @EnableAsync 注解进行相应配置。

使用异步调用可以提高系统的吞吐量和响应性,但也需要注意合理使用线程池,避免线程资源的过度消耗。

🍁🍁 10、Spring Boot 中如何实现跨域请求?

在 Spring Boot 中实现跨域请求有多种方法,下面介绍其中两种常用的方法:

1. 使用 @CrossOrigin 注解

@CrossOrigin 注解是 Spring Framework 提供的一种简单的方式,用于处理跨域请求。通过在控制器类或方法上添加 @CrossOrigin 注解,可以指定允许的跨域请求的来源、方法和其他选项。

下面是一些示例:

@RestController
public class MyController {

    @GetMapping("/api/data")
    @CrossOrigin(origins = "http://example.com")
    public String getData() {
        // 处理请求并返回数据
    }

    @PostMapping("/api/save")
    @CrossOrigin(origins = {"http://example.com", "http://another-domain.com"})
    public void saveData(@RequestBody Data data) {
        // 处理请求并保存数据
    }
}

在上述示例中,@CrossOrigin 注解指定了允许跨域请求的来源,可以用字符串或字符串数组形式指定多个来源。还可以通过其他选项,如 allowedHeadersallowCredentialsexposedHeaders 等来自定义跨域请求的行为。

2. 使用 Web 安全配置类

除了使用 @CrossOrigin 注解,还可以通过 Web 安全配置类进行全局配置,以处理跨域请求。可以创建一个继承自 WebSecurityConfigurerAdapter 的配置类,并覆盖 addCorsMappings 方法,并在其中配置跨域请求的细节。

下面是一个示例:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

在上述示例中,通过覆盖 configure 方法并调用 http.cors(),启用了跨域请求的支持。此外,通过创建一个 CorsConfigurationSource 的 bean,并在其中设置允许跨域请求的来源和方法。

在这里插入图片描述

文章来源:https://blog.csdn.net/m0_50308467/article/details/135373512
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。