简单上手SpringBean的整个装配过程

打印 上一主题 下一主题

主题 885|帖子 885|积分 2655

你好,这里是codetrend专栏“Spring6全攻略”。
典型的企业级应用程序并非仅由单个对象(在Spring术语中称为bean)组成。即使是最简单的应用程序,也会包含一些协同工作的对象,共同呈现出终端用户眼中连贯一致的应用程序形态。
以下mermaid流程图简单展示了Spring工作过程。
graph LR    A[业务类POJO] --> C[Spring容器ApplicationContext]     B[配置元数据Configuration Metadata] --> C    C --产生--> D[可执行的体系/应用程序]业务类与配置元数据相结合,使得在Spring容器ApplicationContext被创建并初始化后,得到的是一个完全配置好且可执行的体系或应用程序。
下文将从定义一系列独立的bean定义出发,进而构建出一个对象间相互协作以达成目标的完全成型的应用程序。
配置元数据 Configuration Metadata

Spring IoC 容器通过消费一种形式的配置元数据。这些配置元数据代表了您作为应用程序开发者告诉 Spring 容器如何实例化、配置和组装应用程序中的对象。
配置元数据方式如下:

  • 基于 XML 格式配置
  • 基于 Groovy 格式配置
  • 基于Java类和注解进行配置
虽然配置的形式不一样,但是配置内容和api基本一样的。
以下是基于 XML 格式、Groovy 格式和 Java 类与注解的方式来配置 Spring IoC 容器的示例:

  • 基于 XML 格式配置:
  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.        xsi:schemaLocation="http://www.springframework.org/schema/beans
  4.            http://www.springframework.org/schema/beans/spring-beans.xsd">
  5.     <bean id="userService" >
  6.         <property name="userDao" ref="userDao"/>
  7.     </bean>
  8.     <bean id="userDao" />
  9. </beans>
复制代码

  • 基于 Groovy 格式配置:
  1. // applicationContext.groovy
  2. beans {
  3.     userService(com.example.UserService) {
  4.         userDao = ref('userDao')
  5.     }
  6.     userDao(com.example.UserDao)
  7. }
复制代码

  • 基于 Java 类和注解进行配置:
  1. // AppConfig.java
  2. @Configuration
  3. public class AppConfig {
  4.     @Bean
  5.     public UserService userService() {
  6.         UserService userService = new UserService();
  7.         userService.setUserDao(userDao());
  8.         return userService;
  9.     }
  10.     @Bean
  11.     public UserDao userDao() {
  12.         return new UserDao();
  13.     }
  14. }
复制代码
以上示例分别展示了利用 XML、Groovy 和 Java 类与注解的方式来配置 Spring IoC 容器。无论利用哪种配置方式,都可以定义和组装应用程序中的对象,并且相应的 API 在实现上基本一致。
这三种配置方式各有优劣,开发者可以根据项目需求和个人喜好选择合适的方式。
Ioc容器利用初体验

Ioc容器在Spring6框架中也就是各种BeanFactory的实现类来创建和管理对象。
ClassPathXmlApplicationContext就是通过读取xml配置初始化bean的一种方法。
启动类的代码如下:
  1. /**
  2. * 宠物测试app
  3. * @author nine
  4. * @since 1.0
  5. */
  6. public class PetApp {
  7.     public static void main(String[] args) {
  8.         // 创建一个类路径下的XML应用上下文,并指定配置文件
  9.         ApplicationContext context = new ClassPathXmlApplicationContext("s104/services.xml", "s104/daos.xml");
  10.         // 从上下文中获取名为"petStore"的bean,其类型为PetStoreServiceImpl,其中petStoreAlias是别名
  11.         PetStoreService petStoreService = context.getBean("petStoreAlias", PetStoreServiceImpl.class);
  12.         // 调用获取的bean的buyPet方法
  13.         petStoreService.buyPet(new Pet("Tom", "Cat",1));
  14.     }
  15. }
复制代码
bean的配置如下:
  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.        xsi:schemaLocation="http://www.springframework.org/schema/beans
  4.            http://www.springframework.org/schema/beans/spring-beans.xsd">
  5.     <bean id="userService" >
  6.         <property name="userDao" ref="userDao"/>
  7.     </bean>
  8.     <bean id="userDao" />
  9. </beans><beans xmlns="http://www.springframework.org/schema/beans"
  10.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  11.        xsi:schemaLocation="http://www.springframework.org/schema/beans
  12.            http://www.springframework.org/schema/beans/spring-beans.xsd">
  13.     <bean id="userService" >
  14.         <property name="userDao" ref="userDao"/>
  15.     </bean>
  16.     <bean id="userDao" />
  17. </beans>            
复制代码
对应bean如下。其中这些bean也就是简单的业务bean。
  1. @Slf4j
  2. @Data
  3. public class PetStoreServiceImpl implements PetStoreService {
  4.     private  AccountDao accountDao;
  5.     private  ItemDao itemDao;
  6.     @Override
  7.     public boolean buyPet(Pet pet) {
  8.         log.info("buy pet: {}", pet);
  9.         accountDao.store(pet);
  10.         itemDao.minus(pet);
  11.         return true;
  12.     }
  13. }
  14. @Slf4j
  15. public class ItemDao {
  16.     public void minus(Pet pet) {
  17.         log.info("minus pet num: {}", pet.getNum());
  18.     }
  19. }
  20. @Slf4j
  21. public class AccountDao {
  22.     public void store(Pet pet) {
  23.         log.info("增加收入: {}", pet);
  24.     }
  25. }
复制代码
这个过程与开发者编写工具类一样,没有任何注解、导入依靠这些配置。习惯利用springboot的开发者可能对此表示有点不习惯。
但是xml元数据配置+BeanFactory一起,就组合成了一个单独的app。运行起来就和springboot无异。
讲上述代码修改为基于Java类和注解进行配置的代码如下。
  1. public class PetAppJavaConfig {
  2.     public static void main(String[] args) {
  3.         // 创建一个基于 Java Config 的应用上下文
  4.         ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
  5.         // 从上下文中获取名为"petStoreService"的bean,其类型为PetStoreService
  6.         PetStoreService petStoreService = context.getBean(PetStoreService.class);
  7.         // 调用获取的bean的buyPet方法
  8.         petStoreService.buyPet(new Pet("Tom", "Cat", 1));
  9.     }
  10. }
  11. @Configuration
  12. class AppConfig {
  13.     @Bean
  14.     public PetStoreService petStoreService(AccountDao accountDao, ItemDao itemDao) {
  15.         PetStoreServiceImpl petStoreService = new PetStoreServiceImpl();
  16.         petStoreService.setAccountDao(accountDao);
  17.         petStoreService.setItemDao(itemDao);
  18.         return petStoreService;
  19.     }
  20.     @Bean
  21.     public AccountDao accountDao() {
  22.         return new AccountDao();
  23.     }
  24.     @Bean
  25.     public ItemDao itemDao() {
  26.         return new ItemDao();
  27.     }
  28. }
复制代码
可以看出代码简洁明了不少,代码输出结果都是一致的。
  1. 11:38:11.646 [main] INFO io.yulin.learn.spring.s104.PetStoreServiceImpl -- buy pet: Pet(name=Tom, type=Cat, num=1)
  2. 11:38:11.653 [main] INFO io.yulin.learn.spring.s104.AccountDao -- 增加收入: Pet(name=Tom, type=Cat, num=1)
  3. 11:38:11.653 [main] INFO io.yulin.learn.spring.s104.ItemDao -- minus pet num: 1
复制代码
完备项目源码信息检察可以在gitee或者github上搜刮r0ad检察。(外链考核太严酷~木办法)
配置Bean初体验

Spring IoC容器管理一个或多个bean。这些bean是根据您提供给容器的配置元数据创建的(例如,以XML  定义的形式)。
在容器内部,bean 定义被表示为 BeanDefinition 对象,其中包含(除其他信息外)以下元数据:

  • 一个包限定的类名:通常是所定义的 bean 的实际实现类。
  • Bean 行为配置元素,用于说明 bean 在容器中应如何运行(作用域、生命周期回调等)。
  • 引用其他 bean,这些 bean 是该 bean 执行工作所需的。这些引用也称为协作者或依靠项。
  • 其他配置设置用于设置新创建对象中的值,例如,管理连接池的 bean 中的池巨细限定或要利用的连接数。
  1. /**
  2. * 说明备案definition的例子
  3. *
  4. * @author nine
  5. * @since 1.0
  6. */
  7. public class BeanDefinitionProcessDemo {
  8.     public static void main(String[] args) {
  9.         // ️GenericApplicationContext 是一个【干净】的容器
  10.         GenericApplicationContext context = new GenericApplicationContext();
  11.         // 用原始方法注册三个 bean
  12.         context.registerBean("bean1", Bean1.class);
  13.         // 初始化容器
  14.         // 执行beanFactory后处理器, 添加bean后处理器, 初始化所有单例
  15.         context.refresh();
  16.         Bean1 bean = context.getBean(Bean1.class);
  17.         bean.print();
  18.         // 销毁容器
  19.         context.close();
  20.     }
  21. }
  22. @Slf4j
  23. class Bean1 {
  24.     public void print() {
  25.         log.info("I am bean1");
  26.     }
  27. }
复制代码
通过这个代码可以发现,Bean1在调用registerBean接口后从一个普通的pojo类变成了一个bean。
org.springframework.context.support.GenericApplicationContext#registerBean为了方便利用有很多重载方法。
通过源码可以发现,普通类通过ClassDerivedBeanDefinition的构造函数转换为BeanDefinition。也就是该class通过setBeanClass成为BeanDefinition的属性beanClass。
后续通过一些列操作,自定义、名字处理、注册容器等等添加了其他的属性信息或者进行二次处理。
具体源码如下。
  1. public <T> void registerBean(@Nullable String beanName, Class<T> beanClass, @Nullable Supplier<T> supplier, BeanDefinitionCustomizer... customizers) {
  2.     // 创建一个 ClassDerivedBeanDefinition 对象,用于封装 Bean 的定义信息
  3.     ClassDerivedBeanDefinition beanDefinition = new ClassDerivedBeanDefinition(beanClass);
  4.     // 如果存在 supplier,则设置到 BeanDefinition 中
  5.     if (supplier != null) {
  6.         beanDefinition.setInstanceSupplier(supplier);
  7.     }
  8.     // 对 BeanDefinition 进行定制处理
  9.     for (BeanDefinitionCustomizer customizer : customizers) {
  10.         customizer.customize(beanDefinition);
  11.     }
  12.     // 如果指定了 beanName,则使用指定的名称,否则使用 beanClass 的名称
  13.     String nameToUse = (beanName != null ? beanName : beanClass.getName());
  14.     // 将封装好的 BeanDefinition 注册到容器中
  15.     registerBeanDefinition(nameToUse, beanDefinition);
  16. }
复制代码
bean的生命周期回调

Spring6 中 Bean 的生命周期可以通过 InitializingBean 和 DisposableBean 接口、@PostConstruct 和 @PreDestroy 注解以及配置文件中的 init-method 和 destroy-method 方法来管理。
把上面手动注入的bean的demo修改,增长实现 Bean 的初始化和烧毁回调:
  1. public class BeanDefinitionProcessDemo {
  2.     public static void main(String[] args) {
  3.         // GenericApplicationContext 是一个干净的容器
  4.         GenericApplicationContext context = new GenericApplicationContext();
  5.         // 用原始方法注册 bean1,并指定初始化和销毁方法
  6.         context.registerBean("bean1", Bean1.class, Bean1::new, beanDefinition -> {
  7.             beanDefinition.setInitMethodName("init");
  8.             beanDefinition.setDestroyMethodName("destroy");
  9.         });
  10.         // 初始化容器
  11.         context.refresh();
  12.         Bean1 bean = context.getBean(Bean1.class);
  13.         bean.print();
  14.         // 销毁容器
  15.         context.close();
  16.     }
  17. }
  18. @Slf4j
  19. class Bean1 {
  20.     public void print() {
  21.         log.info("I am bean1");
  22.     }
  23.     public void init() {
  24.         log.info("Bean1 is being initialized");
  25.     }
  26.     public void destroy() {
  27.         log.info("Bean1 is being destroyed");
  28.     }
  29. }
复制代码
通过输出可以发现,bean1的初始化和烧毁回调被调用了。
  1. 15:14:23.631 [main] INFO io.yulin.learn.spring.s104.Bean1 -- Bean1 is being initialized
  2. 15:14:23.669 [main] INFO io.yulin.learn.spring.s104.Bean1 -- I am bean1
  3. 15:14:23.670 [main] INFO io.yulin.learn.spring.s104.Bean1 -- Bean1 is being destroyed
复制代码
把整个过程改为更熟悉的基于注解驱动开发的方式,代码如下。
  1. import jakarta.annotation.PostConstruct;
  2. import jakarta.annotation.PreDestroy;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  5. /**
  6. * 通过注解方式配置Bean
  7. * @author nine
  8. * @since 1.0
  9. */
  10. public class BeanDefinitionProcessAnnotationDemo {
  11.     public static void main(String[] args) {
  12.         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  13.         context.register(BeanAnnotation1.class);
  14.         context.refresh();
  15.         BeanAnnotation1 bean = context.getBean(BeanAnnotation1.class);
  16.         bean.print();
  17.         context.close();
  18.     }
  19. }
  20. @Slf4j
  21. class BeanAnnotation1 {
  22.     public void print() {
  23.         log.info("I am BeanAnnotation1");
  24.     }
  25.     @PostConstruct
  26.     public void init() {
  27.         log.info("BeanAnnotation1 is being initialized");
  28.     }
  29.     @PreDestroy
  30.     public void destroy() {
  31.         log.info("BeanAnnotation1 is being destroyed");
  32.     }
  33. }
复制代码
输出结果基本一致的。
通过这个转换过程可以更能清晰的发现,Spring如何从基于xml配置、Java配置、注解配置的转换。也能更加深刻体会到Spring的强大兼容性。
实例化Bean

必要利用bean就必须实例化这个类,最简单的方式就是new 一个对象。
但是在Spring6框架中提供了更多的配置来实现实例化bean。
如果利用基于XML的配置元数据,可以在元素的class属性中指定要实例化的对象的范例(或类)。
这个class属性(在BeanDefinition实例上内部是一个Class属性)通常是必需的。
利用Class属性的两种方式之一:

  • 通常情况下,为了指定要构造的bean类,在容器本身通过调用其构造函数反射性地直接创建bean的情况下,类似于利用new运算符的Java代码。
  • 在较不常见的情况下,为了指定包含静态工厂方法的实际类,容器调用该类上的静态工厂方法来创建bean。从调用静态工厂方法返回的对象范例可以是相同的类,也可以是完全差别的类。
构造器实例化bean

当通过构造函数方式创建一个bean时,所有普通类都可以被Spring利用并与之兼容。
也就是说,正在开发的类不必要实现任何特定的接口或以特定方式编码。只需指定bean类即可。
然而,根据为该特定bean利用的IoC范例,可能必要一个默认(空)构造函数。
Spring IoC容器可以管理险些任何希望它管理的类。它不范围于管理真正的JavaBeans。
大多数Spring用户更喜欢具有仅默认(无参数)构造函数。
Spring容器还可以管理非bean的类。例如,如果必要利用不符合JavaBean规范的传统连接池,Spring也可以进行管理。
下面通过一个例子说明基于构造器实例化bean的例子。
  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.context.ApplicationContext;
  3. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. /**
  7. * SpringBean创建demo
  8. *
  9. * @author nine
  10. * @since 1.0
  11. */
  12. public class SpringBeanCreateDemo {
  13.     public static void main(String[] args) {
  14.         // 创建一个基于 Java Config 的应用上下文
  15.         ApplicationContext context = new AnnotationConfigApplicationContext(AppCreateConfig.class);
  16.         // 从上下文中获取名bean,其类型为PetStoreService
  17.         MyClass bean = context.getBean(MyClass.class);
  18.         // 调用获取的bean的方法
  19.         bean.hello("jack");
  20.     }
  21. }
  22. @Configuration
  23. @Slf4j
  24. class AppCreateConfig {
  25.     @Bean
  26.     public MyClass exampleBean() {
  27.         return new MyClass("exampleConstructorArg");
  28.     }
  29. }
  30. @Slf4j
  31. class MyClass {
  32.     public MyClass(String constructorArg) {
  33.         log.info(constructorArg);
  34.     }
  35.     public void hello(String name) {
  36.         log.info("hello " + name);
  37.     }
  38. }
复制代码
输出结果如下。可以看到MyClass类被正确初始化和被IoC容器管理。
  1. 09:44:57.211 [main] INFO io.yulin.learn.spring.s104.MyClass -- exampleConstructorArg
  2. 09:44:57.262 [main] INFO io.yulin.learn.spring.s104.MyClass -- hello jack
复制代码
静态工厂方法实例化bean

在定义利用静态工厂方法创建的bean时,利用class属性指定包含静态工厂方法的类,并利用名为factory-method的属性指定工厂方法本身的名称。
应该能够调用这个方法(带有可选参数,如后面所述),并返回一个活动对象,随后将其视为通过构造函数创建的对象。
如许一个bean定义的用途之一是在遗留代码中调用静态工厂。
  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.context.ApplicationContext;
  3. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. /**
  7. * SpringBean创建demo
  8. *
  9. * @author nine
  10. * @since 1.0
  11. */
  12. public class SpringBeanFactoryMethodCreateDemo {
  13.     public static void main(String[] args) {
  14.         // 创建一个基于 Java Config 的应用上下文
  15.         ApplicationContext context = new AnnotationConfigApplicationContext(AppFactoryConfig.class);
  16.         // 从上下文中获取名bean,其类型为PetStoreService
  17.         AppFactoryConfig.MyBean bean = context.getBean(AppFactoryConfig.MyBean.class);
  18.         // 调用获取的bean的方法
  19.         bean.hello();
  20.     }
  21. }
  22. @Slf4j
  23. @Configuration
  24. class AppFactoryConfig {
  25.     @Bean
  26.     public MyBean myBean() {
  27.         // 调用带有可选参数的静态工厂方法创建 bean
  28.         return MyBeanFactory.createBean("Tom");
  29.     }
  30.     static class MyBean {
  31.         private String name;
  32.         public MyBean(String name) {
  33.             this.name = name;
  34.         }
  35.         public void hello() {
  36.             log.info("Hello, " + name);
  37.         }
  38.     }
  39.     static class MyBeanFactory {
  40.         public static MyBean createBean(String parameter) {
  41.             return new MyBean(parameter);
  42.         }
  43.     }
  44. }
复制代码
通过实例工厂方法实例化bean

类似于通过静态工厂方法进行实例化,利用实例工厂方法进行实例化会调用容器中现有 bean 的非静态方法来创建一个新的 bean。
要利用这种机制,将class属性留空,在factory-bean属性中指定当前(或父级或祖先)容器中包含要被调用以创建对象的实例方法的 bean 的名称。
利用factory-method属性设置工厂方法本身的名称。
这个例子是基于java config来实现的。
可以发现MyBean通过现著名为MyBeanFactory的bean来创建的。
  1. import org.springframework.context.annotation.Bean;
  2. import org.springframework.context.annotation.Configuration;
  3. @Configuration
  4. public class AppBeanCreateConfig {
  5.     @Bean
  6.     public MyBeanFactory myBeanFactory() {
  7.         return new MyBeanFactory();
  8.     }
  9.     @Bean
  10.     public MyBean myBean(MyBeanFactory myBeanFactory) {
  11.         // 调用实例工厂方法创建 bean
  12.         return myBeanFactory.createBean("optionalParameter");
  13.     }
  14.     static class MyBean {
  15.         private String name;
  16.         public MyBean(String name) {
  17.             this.name = name;
  18.         }
  19.         public void hello() {
  20.             System.out.println("Hello, " + name);
  21.         }
  22.     }
  23.     static class MyBeanFactory {
  24.         public MyBean createBean(String parameter) {
  25.             return new MyBean(parameter);
  26.         }
  27.     }
  28. }
复制代码
确定Bean的运行时范例

确定Spring框架中一个特定bean的运行时范例确实必要思量到多种复杂情况。
在bean元数据定义中指定的类只是一个初始类引用,可能与声明的工厂方法结合,或者是一个可能导致bean具有差别运行时范例的FactoryBean类,或者在实例级工厂方法的情况下根本没有设置(这是通过指定的工厂-bean 名称来剖析的)。
此外,AOP署理可能会用基于接口的署理包装一个bean实例,只袒露目标bean的实际范例(仅袒露其实现的接口)。
查找特定bean的实际运行时范例的推荐方法是利用BeanFactory.getType调用指定的bean名称。
这思量了上述所有情况,并返回BeanFactory.getBean调用将为相同的bean名称返回的对象范例。
以上面的通过实例工厂方法实例化bean为例说明利用BeanFactory.getType。
  1. import lombok.extern.slf4j.Slf4j;
  2. import org.junit.jupiter.api.Test;
  3. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  4. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  5. @Slf4j
  6. public class BeanRunTimeTypeTest {
  7.     @Test
  8.     public void test() {
  9.         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppBeanCreateConfig.class);
  10.         // 获取 BeanFactory 实例
  11.         ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
  12.         // 使用 BeanFactory.getType 方法获取特定 bean 的运行时类型
  13.         Class<?> beanType = beanFactory.getType("myBean");
  14.         log.info("The runtime type of 'myBean' is: " + beanType.getName());
  15.         // 使用 BeanFactory.getBean 方法获取特定 bean 的实例对象
  16.         AppBeanCreateConfig.MyBean myBeanInstance = (AppBeanCreateConfig.MyBean) beanFactory.getBean("myBean");
  17.         myBeanInstance.hello();
  18.         // 关闭应用上下文
  19.         context.close();
  20.     }
  21. }
复制代码
输出结果如下。可以看到实例工厂方法没有设置class,但是运行时范例为MyBean。
  1. 10:51:25.111 [main] INFO io.yulin.learn.spring.s104.BeanRunTimeTypeTest -- The runtime type of 'myBean' is: io.yulin.learn.spring.s104.AppBeanCreateConfig$MyBean
  2. Hello, optionalParameter
复制代码
BeanFactory.getType()一些常见用途:

  • 范例检查:通过调用 getType() 方法,可以获取特定 bean 的实际范例,并根据这些范例信息执行相应的操作。这对于在运行时进行范例检查和验证非常有用。
  • 动态处理:在某些情况下,您可能必要根据 bean 的范例来动态地决定如何处理该 bean。通过 getType() 方法可以获取 bean 的范例信息,并根据必要执行相应的处理逻辑。
  • 条件化配置:在 Spring 应用程序中,有时根据 bean 的范例来进行条件化的配置会很有用。通过 getType() 方法可以获取 bean 的范例,从而根据差别的范例执行差别的配置。
  • 自定义逻辑:某些情况下,可能必要根据 bean 的范例来编写特定的业务逻辑。通过 getType() 方法可以获取 bean 的准确范例信息,并在代码中编写相应的逻辑。
关于作者

来自全栈程序员nine的探索与实践,持续迭代中。
接待关注或者点个小红心~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

天空闲话

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表