浅谈 IoC / DI

1.背景

刚开始接触 spring ,一定不会少看到 IoC、DI 这两个单词。

先把 spring 撇开,单独看看 IoC 和 DI 是什么意思呢?

找两个定义:

  • IoC,全称 Inverse of Control,控制反转。是指,由容器/第三方系统(例如 spring )来控制业务对象之间的依赖关系,而不是像传统方式中由代码来直接控制。

  • DI,全称 Dependency Injection,依赖注入。是指,自身对象中的内置对象是通过注入的方式进行创建。

呃,看完了,似懂非懂。。。

2.举个“吃饭”的例子

场景:在沙县小吃/学校食堂吃饭。

沙县小吃:

  1. 顾客到店:老板,我要一份炒米线。
  2. 老板:好嘞,这就给你做。

学校食堂:

  1. 学生到食堂:阿姨,我要这个西红柿炒蛋,加4两米饭。
  2. 食堂阿姨:好嘞,这就给你装盘。

区别在哪呢?

  • “沙县小吃”是由顾客到店后主动要求“我要吃什么”,然后餐馆才去 new 一个你要吃的菜。
  • “学校食堂”是由食堂提前 new 很多可以提供给顾客吃的菜,顾客来了之后直接挑选装盘就可以了。

3.再举个例子

场景:接口参数检查

假设,我们的后台服务拆两个类:MyService 和 Validator 。很明显,MyService 依赖 Validator。

考虑一下, 最简单的实现方式是什么呢?

3.1 传统实现,new

简单介绍一下用到的类和接口:

  • IValidate.java,接口定义了 validate() 方法,对一个 String 类型参数做检查。
  • NotnullValidator.java 实现了 IValidate 接口的validate()方法,要求参数不能为空。
  • MyService.java,定义了一个Serve() 方法,用来接收外部参数,然后对参数做非空检查。
  • Client.java,模拟调用方,请求MyService。

代码示例:

// IValidator.java
interface IValidator.java {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService.java {
    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        NotnullValidator notnullValidator = new NotnullValidator();
        notnullValidator.validate(param);
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        MyService myService = new MyService();
        myService.Serve("");
        myService.Serve("123");
        /**
         * 这样做的弊端是,依赖隐藏在函数内部,模块化、可用性、扩展性、易测性都不好。
         */
    }
}

在这个例子中,MyService 直接在 Serve() 方法内部 new 了一个 NotNullValidator 实例,进行参数检查。

这样做的问题在于:

  1. 扩展性差,如果需要其他校验器,就要在 MyService 内部做大调整了。
  2. 依赖管理困难,IValidator 实例的生命周期在 MyService 之内,无法管理。
  3. 可测性差,耦合太严重,依赖和行为完全堆到一起去了,没法做单元测试。

那么,有什么解决方法呢?
很简单,把 new NotNullValidator 这个操作拿出来就可以了。

3.2 构造子注入

同样的例子,我们可以在 MyService 内部定义一个 IValidator 成员变量,然后再 定义一个 MyService 的带参构造函数。

代码示例:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService {
    private IValidator validator;

    MyService(IValidator validator) {
        System.out.println("构造子注入");
        this.validator = validator;
    }

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        NotnullValidator notnullValidator = new NotnullValidator();
        MyService myService = new MyService(notnullValidator);
        myService.Serve("");
        myService.Serve("123");
        /**
         * 依赖由外部管理,可测性变强了。
         * 这其实就是一种依赖注入方法。
         * 通过构造方法注入依赖,叫做"构造子注入"。
         */
    }
}

在这里,Client 自己 new 了一个 NotNullValidator 实例,通过构造方法传给新的 MyService 实例。

这样,类与类之间的依赖关系由外部管理,可测试性、可复用性都得到了很大的提升。

从应用程序的角度描述,依赖关系不再是 MyService 正向获得一个 IValidator 实现的实例,而是 Client 先实例化一个自己想要的 IValidator 实现,然后注入到 MyService,这就是“控制反转”(IoC)。

从 Client 的角度描述,Client 负责 IValidator 的实例化,并将其注入到 MyService,这就是“依赖注入”(DI)。

IoC其实就是把主动变被动,并有效的分离了对象和它所需要的外部资源,使得它们松散耦合,有利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

除了通过构造器注入外,还有另外两种依赖注入方法。

3.3 设值注入

设值注入是利用类成员变量的 setter 方法进行依赖注入的一种方式。

代码示例:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService {
    private IValidator validator;

    void setValidator(IValidator validator) {
        System.out.println("设值注入");
        this.validator = validator;
    }

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        NotnullValidator notnullValidator = new NotnullValidator();
        MyService myService = new MyService();
        myService.setValidator(notnullValidator);
        myService.Serve("");
        myService.Serve("123");
        /**
         * 通过成员变量的setter方法注入依赖,叫做"设值注入"
         */
    }
}

这里,为 MyService 的成员变量 validator 定义了setValidator() 方法,从而使得外部可以通过这个 setValidator() 方法注入 IValidator 依赖。

3.4 接口注入

接口注入是指,通过实现特定接口的依赖注入方法实现依赖注入的一种方式。

代码示例:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// MyInject.java
interface MyInject {
    public void inject(IValidator validator);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService implements MyInject {
    private IValidator validator;

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }

    @Override
    public void inject(IValidator validator) {
        System.out.println("接口注入");
        this.validator = validator;
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        NotnullValidator notnullValidator = new NotnullValidator();
        MyService myService = new MyService();
        myService.inject(notnullValidator);
        myService.Serve("");
        myService.Serve("123");
        /**
         * 通过实现一个单独的依赖注入接口,实现依赖注入,叫做"接口注入"。
         * 不常见的用法,构造函数和setter就够用了。
         *
         * 通过构造函数、setter、接口注入依赖,仍然存在问题。
         * 加入一个类A依赖非常多的其他类,每一个都要手动new,然后set,太麻烦了。
         * 怎么办呢?上框架。
         */
    }
}

这里定义了一个 MyInject 接口,接口内部参数 inject() 用于注入 IValidator 。
MyService 实现了这个接口,Client 可以通过 inject() 注入IValidator 实例。

上面提到的三种注入方式:构造子注入、设值注入、接口注入,实际上都是提供一个“入口”函数,允许外部调用方向内注入 IValidator 依赖,只是形式不同罢了。

但是,外部系统管理这些 “new” 操作的话,如果接口内有多层传递依赖,或者依赖项比较多的时候,Client 负担就太重了。

怎么办呢?上框架。

IoC/DI 框架要做的事情,就是:new 什么,什么时候 new ,new 完了要给谁。。。

IoC/DI 框架有很多,这里主要介绍两个,Spring 和 Guice。

4. IoC/DI 框架

4.1 spring

IoC是spring的核心,对于spring框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。

想要使用 spring ,至少需要两个核心包:group: 'org.springframework', name: 'spring-core'group: 'org.springframework', name: 'spring-context'

spring 通过一个叫做“容器”的东西来管理所有的 bean 对象。

当 Spring 应用程序被加载到内存中时,spring 框架通过读取 “配置元数据”,来完成对象的实例化、配置和组装。应用程序可以从容器中取bean,方法是getBean()

在这里,接口和类定义同上一章。
只是,额外增加 xml 配置文件用于定义 bean 的生命周期及依赖关系。

PS:“配置元数据” 可以通过 XML、Java注解、Java代码来标识,代码示例只给出xml配置。

代码有点多,我分开说哈。
记得以下几步:

  1. 引入上面说的两个包。
  2. 写 java 代码 定义 bean。
  3. 写 xml 配置文件。
  4. Client。

构建工具用的是gradle,所以这么引包:

// build.gradle
group 'com.ann.javas.topics'
version '1.0-SNAPSHOT'

apply plugin: 'java'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.springframework', name: 'spring-core', version: '4.3.11.RELEASE'
    compile group: 'org.springframework', name: 'spring-context', version: '4.3.11.RELEASE'
}

本例中,给出了5种xml配置方法:构造子注入、设值注入、自动装载byName,自动装载byType,自动装载constructor。

先列下java代码,bean 定义没变,就是Client.java里面的内容多了点:

// IValidator.java
public interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
public class NotnullValidator implements IValidator{
    @Override
    public void validate(String param) {
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
public class MyService {
    private IValidator validator;

    public MyService(IValidator validator) {
        this.validator = validator;
    }

    public void setValidator(IValidator validator) {
        this.validator = validator;
    }

    public MyService() {
    }

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}


// Client.java
public class Client {
    /**
     * 构造子注入 和 设值注入,唯一的区别就是标签中定义依赖的元素不同。
     * 构造子注入使用:constructor-arg,设值注入使用:property。
     *
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("case1:测试spring容器构造子注入(基于XML配置)...");
        demo1();
        System.out.println("..................................................");

        System.out.println("case2:测试spring容器设值注入(基于XML配置)...");
        demo2();
        System.out.println("..................................................");

        System.out.println("case3:测试spring容器自动装载byName(基于XML配置)...");
        demo3();
        System.out.println("..................................................");

        System.out.println("case4:测试spring容器自动装载byType(基于XML配置)...");
        demo4();
        System.out.println("..................................................");

        System.out.println("case5:测试spring容器自动装载constructor(基于XML配置)...");
        demo5();
        System.out.println("..................................................");

    }

    private static void demo1(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case1.xml");
        // 构造子注入实例
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo2(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case2.xml");
        // 设值注入实例
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo3(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case3.xml");
        // 自动装载,byName
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo4(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case4.xml");
        // 自动装载,byType
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo5(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case5.xml");
        // 自动装载,constructor
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }
}

然后是xml配置文件,有5个:

// demo5_configioc-byxml-case1.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 用 constructor-arg 配置,就是构造子注入 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService">
        <constructor-arg ref="p"/>
    </bean>
    <bean id="p" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case2.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 用 property 配置,就是设值注入 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService">
        <property name="validator" ref="p"/>
    </bean>
    <bean id="p" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case3.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 自动装载 byName,根据 id 或 name 查找 bean 并自动装配 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService" autowire="byName">
    </bean>
    <!-- 这里的 id 必须和 MyService 中定义的IValidator 变量名一致,否则 byName 无法识别 -->
    <bean id="validator" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case4.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 自动装载byType,根据 class 查找并自动装配 bean -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService" autowire="byType">
    </bean>
    <bean id="b" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case5.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 自动装载 constructor,根据 bean 的构造函数定义进行查找并自动装配 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService" autowire="constructor">
    </bean>
    <bean id="b" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

代码中的注释有简单介绍,详细的内容就不再赘述~

4.2 guice

guice 是谷歌开发的轻量级 DI 框架,号称比 spring 快10倍,虽然我没测过,但确实蛮好用的就是了。

想要使用guice,需要引包:group: 'com.google.inject', name: 'guice'

guice 允许你使用 @Inject 进行注解构造函数的方式,来进行自动实例化构造参数。也可以通过实现 Module 的 configure(Binder binder) 方法定义依赖注入规则。

在程序启动时,你可以使用 Guice.createInjector(自定义Module) 来生成一个injector,之后就可以从 injector 中获取你需要 guice 为你实例化的类,方法是 injector.getInstance(你想要的类.class)

用guice非常简单:

  1. 引包。
  2. java代码。

同样gradle构建:

// build.gradle
group 'com.ann.javas.topics'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'com.google.inject', name: 'guice', version: '4.0'
}

java代码:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator {
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}


// MyService.java
class MyService  {
    @Inject
    private IValidator validator;

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}


// MyModule.java
public class MyModule implements Module{
    @Override
    public void configure(Binder binder) {
        System.out.println("guice注入规则,绑定 IValidator 到 NotnullValidator");
        binder.bind(IValidator.class).to(NotnullValidator.class);
    }
}


// Client.java
public class Client {
    public static void main(String[] args){
        // 定义依赖注入规则
        MyModule module = new MyModule();

        // 根据注入规则,生成 injector
        Injector injector = Guice.createInjector(module);

        // 获取 MyService 实例
        MyService myService = injector.getInstance(MyService.class);

        // 来两个case
        myService.Serve("");
        myService.Serve("123");
    }
}

如你所见,MyModule中定义了依赖注入规则,将 IValidator 绑定到 NotnullValidator。这样,当某个类需要一个IValidator的时候,guice就会给它一个NotNullValidator实例。

其他的请看注释说明~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容