20
2024
09
23:27:38

分析SpringBoot启动配置原理

目录

一、整体概述

(一)基本整体初步分析

(二)从启动来看整体过程图分析

二、SpringApplication构造过程分析

(一)验证主配置类不为空并且保存主类

(二)推断项目的类型

(三)初始化initializers

(四)加载相关的listeners

(五)决定ApplicationClass主程序

三、SpringApplication启动过程分析

(一)监控器监听容器启动并进行图形化页面处理

(二)监听器SpringApplicationRunListeners开启监听

(三)environmentPrepared环境准备处理

(四)banner打印

(五)创建Spring应用上下文

(六)Spring应用上下文准备阶段

(七)Spring应用上下文刷新阶段

(八)Spring应用上下文收尾阶段

(九)回调工作处理

(十)SpringApplication启动异常处理

四、SpringBoot自动配置分析

(一)自动装配原理分析

(二)条件化自动装配

(三)自动配置原理举例:HttpEncodingAutoConfiguration(HTTP编码自动配置)


干货分享,感谢您的阅读!

一、整体概述

(一)基本整体初步分析

Spring Boot 是一个用于构建独立的、生产级的 Spring 应用程序的框架,它提供了自动化的配置和约定优于配置的原则。在理解 Spring Boot 的启动配置原理之前,我们需要了解几个关键概念。

首先,Spring Boot 使用了基于约定的自动配置机制它通过在 classpath 下查找特定的配置文件和类,根据应用程序所使用的依赖自动配置 Spring 应用程序的各种组件。这样可以大大简化开发者的工作,减少了手动配置的需求。

其次,Spring Boot 使用了条件化配置(Conditional Configuration)的机制。这意味着配置的应用取决于一组条件是否满足。条件可以基于多种因素,如 classpath 中存在特定的类、特定的 bean 是否存在等等。通过条件化配置,Spring Boot 可以根据不同的环境和需求进行动态的配置。

Spring Boot 的启动配置原理可以概括如下:

  1. 在启动过程中,Spring Boot 会加载并解析应用程序的配置文件,其中包括 application.properties 或 application.yml 文件等。这些文件中可以定义各种属性和配置信息,如数据库连接、日志级别等。

  2. Spring Boot 会自动扫描 classpath 下的特定包,寻找带有特定注解的类,如 @SpringBootApplication。这个注解标识了一个 Spring Boot 应用程序的入口点。

  3. 根据配置文件中的属性和条件化配置的机制,Spring Boot 自动配置应用程序的各种组件,包括数据库连接池、消息队列、Web 服务器等。如果需要进行自定义配置,可以使用专门的注解或编写自定义的配置类。

  4. 在应用程序启动时,Spring Boot 会初始化 Spring 容器,并根据配置进行相应的初始化工作。这包括创建和管理 bean、处理依赖注入等。

总的来说,Spring Boot 的启动配置原理是基于自动化的约定和条件化配置机制。它通过读取配置文件、扫描注解、自动配置组件等步骤,简化了应用程序的配置过程,并提供了灵活性和易用性。

(二)从启动来看整体过程图分析

每个SpringBoot程序都有一个主入口main方法,main里面调用SpringApplication.run()启动整个SpringBoot程序,该方法所在类需要使用@SpringBootApplication注解,例如如下

package org.zyf.javabasic;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * 描述:启动入口类
 *
 * @author yanfengzhang
 * @date 2019-12-19 18:11
 */
@SpringBootApplication
@ComponentScan(basePackages = {"org.zyf.javabasic"})
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@EnableSwagger2
public class ZYFApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(ZYFApplication.class, args);
}

其中对@SpringBootApplication进行展开分析:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@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 {
    @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 {};
}

@SpringBootApplication包括三个注解,功能如下:

  • @SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境

  • @ComponentScan:组件扫描,可自动发现和装配Bean(比如@Component和@Configuration),默认扫描SpringApplication的run方法里类所在的包路径下所有文件

  • @EnableAutoConfiguration:激活SpringBoot自动装配的特性

现在对主入口main方法进行展开来给出整体的流程图分析

二、SpringApplication构造过程分析

进入main里面的run方法,创建了一个SpringApplication实例,配置一些基本的环境变量、资源、构造器、监听器,进入这个SpringApplication有参构造函数

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.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

注意:在该构造方法内,做的工作就是把相关的类(主要是initializer和listener)加载进容器中,并没有执行

(一)验证主配置类不为空并且保存主类

(二)推断项目的类型

进入对应方法分析如下:

推断项目的类型可能为reactive、none、servlet三种类型,默认为servlet类型。其使用类加载器判断类型的逻辑如下

类型

判断情况

reactive

存在Spring WebFlux的DispatcherHandler存在,但是Spring MVC的DispatcherServlet不存在

none

二者都不存在

servlet

剩余所有情况

(三)初始化initializers

先进入ApplicationContextInitializer接口,这个接口只有一个方法initialize

package org.springframework.context;

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    void initialize(C var1);
}

查看实现了该这个接口的类进行分析

以SharedMetadataReaderFactoryContextInitializer实现类为例,跳转对应jar包,可以看到里面 spring.factories就在key为ApplicationContextInitializer中指定了对应的实现类,例如:

进入SharedMetadataReaderFactoryContextInitializer,其实现了ApplicationContextInitializer接口,并实现了里面的initialize方法

现在回到一开始再来看getSpringFactoriesInstance()方法,其核心为loadFactoryNames()方法

进入loadFactoryNames()方法,可以看到ApplicationContextInitializer的相关获取内容直接就是从文件“META-INF/spring.factories”中获取保存的

(四)加载相关的listeners

先进入ApplicationListener接口,这个接口只有一个方法onApplicationEvent

package org.springframework.context;

import java.util.EventListener;

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}

查看实现了该这个接口的类进行分析

以BackgroundPreinitializer实现类为例,跳转对应jar包,可以看到里面 spring.factories就在key为ApplicationListener中指定了对应的实现类:

 进入BackgroundPreinitializer,其实现了ApplicationListener,并实现了里面的onApplicationEvent

方法如下:

现在回到一开始再来看getSpringFactoriesInstance()方法,其处理流程和上面的一样,即依然是从类路径下找到META-INF/spring.factories配置的所有ApplicationListener

(五)决定ApplicationClass主程序

进入deduceMainApplicationClass方法

三、SpringApplication启动过程分析

SpringBoot启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块以及后续的收尾回调等内容

(一)监控器监听容器启动并进行图形化页面处理

(二)监听器SpringApplicationRunListeners开启监听

直接开始分析ApringApplicationRunListeners,内含SpringApplicationRunListener的集合,其中starting方法就是对listeners进行遍历,对每个listener都调用starting方法

SpringApplicationRunListener和ApplicationListener都是SpringBoot中的事件监听器,但是它们所监听的事件和触发时机有所不同,其区别如下:

  1. 监听的事件不同
    SpringApplicationRunListener主要监听SpringApplication运行时的各种事件,例如应用程序开始启动、应用程序启动失败、应用程序启动完成等事件。而ApplicationListener主要监听Spring容器中的各种事件,例如Bean加载完成、上下文刷新完成等事件。

  2. 触发时机不同
    SpringApplicationRunListener在SpringApplication启动时就开始工作,可以接收到应用程序开始启动、应用程序启动失败、应用程序启动成功等各种事件。而ApplicationListener则是在Spring容器启动完成后,才能开始工作,监听的是Spring容器中的各种事件。

  3. 使用场景不同
    在实际应用中,SpringApplicationRunListener主要用于监听SpringApplication的启动过程,例如在应用程序启动前后执行某些操作、监听应用程序启动失败事件并做出相应的操作等。而ApplicationListener则用于监听Spring容器中的各种事件,例如在Bean加载完成后做出相应的操作、在上下文刷新完成后更新一些状态等。

总之,尽管SpringApplicationRunListener和ApplicationListener都是SpringBoot中的事件监听器,但是它们所监听的事件、触发时机、使用场景等都有所不同,我们需要根据具体的应用需求,选择合适的监听器来完成应用程序的事件处理。

在进入看一下SpringApplicationRunListener这个类

package org.springframework.boot;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

public interface SpringApplicationRunListener {
    void starting();

    void environmentPrepared(ConfigurableEnvironment environment);

    void contextPrepared(ConfigurableApplicationContext context);

    void contextLoaded(ConfigurableApplicationContext context);

    void started(ConfigurableApplicationContext context);

    void running(ConfigurableApplicationContext context);

    void failed(ConfigurableApplicationContext context, Throwable exception);
}

对应方法说明如下

监听方法

运行阶段说明

SpringBoot起始版本

contextLoaded(ConfigurationApplicationEnvironment)

ConfigurableApplicationContext完成加载,但仍未启动;通知监听器,ApplicationContext已经完成IoC配置

1.0

contextPrepared(ConfigurationApplicationEnvironment)

ConfigurableApplicationContext准备妥当:通知监听器,ApplicationContext已经创建并初始化完成

1.0

environmentPrepared(ConfigurationEnvironment)

ConfigurationEnvironment准备妥当,允许将其调整

1.0

failed(ConfigurationApplicationEnvironment,Throwable)

Spring应用运行失败

2.0

running(ConfigurationApplicationEnvironment)

Spring应用正在运行

2.0

started(ConfigurationApplicationEnvironment)

ConfigurableApplicationContext已启动,此时SpringBean已初始化完成

2.0

starting()

run方法执行的时候立马执行:通知监听器,SpringBoot开始执行

1.0

总的来说就是创建了应用的监听器SpringApplicationRunListeners并调用start()开始监听,先在getRunListeners中获取了所有的监听器,然后starting开启

(三)environmentPrepared环境准备处理

分析这段代码,进入准备环境的方法prepareEnvironment中,可以看到如下:

首先创建了一个环境ConfigurationEnvironment(有的话就获取,没有则创建)

回到该方法返回的地方,通过this.configureEnvironment对环境进行设置,接着如下:

环境配置好以后回调了SpringApplicationRunListener的environmentPrepared函数,进入该方法:

可以看到environmentPrepared环境准备中进行了通知监听器,Environment准备完成。

回到开始处,可以看到环境准备完成后通过bindToSpringApplication将环境绑定到程序中。

(四)banner打印

打印对应的banner信息

也就是启动后的改部分,见如下

实际中,改图可以进行替换,只需要在resources增加banner.txt信息即可,例如如下:

其中的原理可以继续点开对应后续代码分析,这个后续中讲解替换思路。

(五)创建Spring应用上下文

创建应用上下文即IOC过程,IOC容器是驱动整体SpringBoot应用组件的核心,进入该方法:

这里的创建是根据SpringApplication在构造阶段所推断的web应用类型进行IOC容器的创建,IOC容器就是run返回的内容。

(六)Spring应用上下文准备阶段

prepareContext方法将listeners、environment、applicationArguments、banner等重要组件与上下文对象关联,对上下文对象进行进一步配置,进入该方法具体分析:

可以看到首先分别保存了刚才生成的environment、ApplicationContext,接下来的applyInitializers方法是执行初始化,进入该方法

方法内部遍历所有的initializer,然后依次回调里面所有的initialize方法(这些initializer就是在springboot刚启动时构造new springApplication时添加的),设置完当前环境并完成初始化之后,回调了所有Linsteners的contextPrepared方法

接下来将命令行参数和banner注册到IOC容器来,如下:

最后全部操作都完成后,这个方法回调了Listeners的contextLoaded方法,如上。

(七)Spring应用上下文刷新阶段

在该方法中首先注册了一个shutdownHook线程,用来实现SpringBean销毁生命周期回调

在执行完refresh之后的控制台,可以看到tomcat和一些IOC容器的bean都被加载进去了


(八)Spring应用上下文收尾阶段

其中afrerRefresh()并无内容处理,后续的版本中已经没有改方法了

protected void afterRefresh(ConfigurableApplicationContext context,
 ApplicationArguments args) {
    }

收尾计时器停止,同时调用监听器的started()。

(九)回调工作处理

进入该方法分析

ApplicationContext就是IOC容器,这个方法从IOC容器中获取了所有的ApplicationRunner和ConmmandLineRunner,接下来进行遍历和回调。

在callRunners使用的这两个类几乎可以等价,都是用于做一些客户自定义的工作,而且是整个流程完成之后才会调用用户自己定义的实现类的run方法,这两个run方法的实现方法都是在容器基本初始化好的时候调用的。

紧接着,如果无异常代码执行如下:

监听器回调running()方法代表SpringApplication正常启动结束。

(十)SpringApplication启动异常处理

发生异常主要是对异常的处理,我们进入该方法分析

可以看到这个异常报告类也是支持自定义并且自动配置的,配置结束后,Springboot做了一些基本的收尾工作,返回了应用环境上下文(IOC容器)。

四、SpringBoot自动配置分析

Spring Boot 的自动化配置模块是该框架的核心功能之一,它可以大大简化应用程序的配置工作。下面是对 Spring Boot 自动化配置模块的讲解和分析:

  1. 自动化配置的原理:Spring Boot 的自动化配置模块基于约定优于配置的原则。它通过在 classpath 下扫描依赖和配置,自动配置应用程序的各个组件。它使用条件化配置的机制,根据环境和条件自动选择适当的配置。

  2. 自动配置的实现方式:Spring Boot 自动化配置模块使用了 @Conditional 注解和条件注解来实现条件化配置。这些注解可以根据一组条件来决定是否启用某个配置。例如,@ConditionalOnClass 根据 classpath 中是否存在指定的类来判断是否启用配置。

  3. 自动配置的加载顺序Spring Boot 的自动配置是通过在 classpath 下的 META-INF/spring.factories 文件中定义的自动配置类来实现的。这些自动配置类会被自动加载,并根据条件进行初始化和配置。根据条件的不同,可以有多个自动配置类被加载,它们会按照优先级顺序进行配置

  4. 自动配置的自定义:Spring Boot 允许开发者对自动配置进行自定义。你可以使用 @Conditional 注解和条件注解来定义自定义的条件,从而影响自动配置的行为。你还可以使用 @EnableAutoConfiguration 注解来控制自动配置的启用或禁用。

  5. 自动配置的好处:Spring Boot 的自动化配置模块带来了很多好处。它大大减少了手动配置的工作量,提高了开发效率。它提供了合理的默认配置,减少了错误配置的风险。同时,它的条件化配置机制使得应用程序更具灵活性,能够根据不同的环境和需求进行动态的配置。

总的来说,Spring Boot 的自动化配置模块是该框架的重要特性之一。它通过约定优于配置的原则和条件化配置机制,实现了自动加载和配置应用程序的各个组件。这为开发者提供了便利和灵活性,并大大简化了应用程序的配置过程。

现在回到我们一开始的图示分析,该配置模块的主要使用到了SpringFactoriesLoader,即Spring工厂加载器,该对象提供了loadFactoryNames方法,入参为factoryClass和classLoader,即需要传入工厂类名称和对应的类加载器,方法会根据指定的classLoader,加载该类加器搜索路径下的指定文件,即spring.factories文件,传入的工厂类为接口,获取到这些实现类的类名后,loadFactoryNames方法返回类名集合,方法调用方得到这些集合后,再通过反射获取这些类的类对象、构造方法,最终生成实例。

(一)自动装配原理分析

从@SpringBootApplication中的@EnableAutoConfiguration注解中可以看到其import了一个自动配置导入选择器AutoConfigurationImportSelect

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

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

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

    String[] excludeName() default {};
}

其类图如下

可以发现其最终实现了ImportSelector(选择器)和BeanClassLoaderAware(bean类加载器中间件),其中这个选择器的作用就是导入组件。

所有自动装配的逻辑都是在AutoConfigurationImportSelector里面的selectImports方法中实现的

进入getAutoConfigurationEntry()方法

进入getCandidateConfigurations()获取候选配置方法,可以看到核心的SpringFactoriesLoader的loadFactoryNames()方法

其中SpringFactoriesLoader是Spring Framework工厂机制的加载器,loadFactoryNames是其对应的加载方法,进入这个核心的loadFactoryNames方法中查看

该处加载原理如下:

  • 扫描所有jar包路径下 META-INF/spring.factories,这里是通过类加载器生成对应的url路径

  • 把扫描到的内容包装成properties对象,并对这个对象的内容进行遍历,返回map,map的key为接口的全类名,value为接口全部实现类列表(列表里元素去重,防止重复加载),这个value的信息后续会作为loadSpringFactories方法的返回值

  • 在上一步返回的map之中查找并返回指定类名映射的实现类全类名列表

再看刚才getCandidateConfiguration方法中的getSpringFactoriesLoaderFactoryClass方法,返回的就是EnableAutoConfiguration类

也就是说要从刚才properties中再获取这个类对应的值,把它们加到容器中。

选取mybatis-spring-boot-autoconfigure下的spring.factories文件分析一下:

每一个 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中,用它们做自动配置;

只有进入到容器中,这些自动配置类才会起作用——进行自动配置功能

(二)条件化自动装配

对于使用@Configuration的自动配置类,其条件自动化装配以@condition为核心。在spring底层中的@conditional注解会根据不同的满足条件生效整个配置类里面的配置。

条件化装配可以分为以下几类:

Class条件注解

注解

说明

@ConditionalOnClass

指定类存在时生效

@ConditionalOnMissingClass

指定类缺失时生效

Bean条件注解

注解

说明

@ConditionalOnBean

指定Bean存在时生效

@ConditionalOnMissingBean

指定Bean缺失时生效

属性条件注解

注解

说明

@ConditionalOnProperty

使用属性(application.properties)的值判断是否生效

Web应用条件注解

注解

说明

@ConditionalOnWebApplication

是web类型时生效

@ConditionalOnNotWebApplication

不是web类型时生效

其他条件注解

@Conditional扩展注解

作用(判断是否满足当前指定条件)

@ConditionalOnJava

系统的java版本是否符合要求

@ConditionalOnExpression

满足SpEL表达式指定

@ConditionalOnSingleCandidate

容器中只有一个指定的Bean,或者这个Bean是首选Bean

@ConditionalOnResource

类路径下是否存在指定资源文件

@ConditionalOnJndi

JNDI存在指定项

分析@ConditionalOnWebApplication

package org.springframework.boot.autoconfigure.condition;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnWebApplicationCondition.class})
public @interface ConditionalOnWebApplication {
    ConditionalOnWebApplication.Type type() default ConditionalOnWebApplication.Type.ANY;

    public static enum Type {
        ANY,
        SERVLET,
        REACTIVE;

        private Type() {
        }
    }
}

进入OnWebApplicationCondition类,里面有一个getMatchOutcome()方法,就是判断是否符合当前配置条件

该方法首先判断是否使用了这个注释,然后使用isWebApplication来判断当前是否是web应用,这些match的方法判断指定的条件成立,才会给容器添加组件,配置内容才会生效。

(三)自动配置原理举例:HttpEncodingAutoConfiguration(HTTP编码自动配置)

可以看到以下信息:

  • @Configuration 代表这是一个配置类,类似编写的配置文件,也可以给容器中添加组件。

  • @EnableConfigurationProperties 启用指定类的ConfigurationProperties功能,将配置文件application.properties的值和ServerProperties绑定起来,并把ServerProperties加入到IOC容器中。

  • @ConditionalOnClass用来判断当前项目是否含有这个类,这里CharacterEncodingFilter的作用就是springMVC乱码解决的过滤器(以前在spring的xml文件中配置的),如果有这个过滤器则配置生效。

  • @ConditionalOnProperty判断这个配置是否存在,matchIfMissing = true代表即使配置文件中不存在这个属性也是默认生效的。

需要注意的是,在spring.factories中的自动配置类不是都能生效的,都有各自的生效条件。根据当前不同条件判断,来决定这个配置类是否生效;一旦配置类生效,这个配置类就会给容器添加各种组件,这些组件的属性从对应的properties中获取,这些类里面的每一个属性又是和配置文件绑定的。




推荐本站淘宝优惠价购买喜欢的宝贝:

image.png

本文链接:https://www.hqyman.cn/post/8026.html 非本站原创文章欢迎转载,原创文章需保留本站地址!

分享到:
打赏





休息一下~~


« 上一篇 下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

请先 登录 再评论,若不是会员请先 注册

您的IP地址是: