资讯专栏INFORMATION COLUMN

Yii2中的依赖注入

harriszh / 2739人阅读

摘要:构造器注入实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。

基本概念

1.依赖倒置(反转)原则(DIP):一种软件架构设计的原则(抽象概念,是一种思想)
在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

1.高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。

2.抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

在上图中,高层对象A依赖于底层对象B的实现;图2中,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。

该原则颠倒了一部分人对于面向对象设计的认识方式。如高层次和低层次对象都应该依赖于相同的抽象接口。它转换了依赖,高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口。通俗的讲,就是高层模块定义接口,低层模块负责实现。

2.控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式,一种设计原则)
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。

实现控制反转主要有两种方式:
1.依赖注入:
2.依赖查找

两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。

3.依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)
依赖注入有如下实现方式:

接口注入(Interface Injection):实现特定接口以供外部容器注入所依赖类型的对象。

设值注入(Setter Injection): 实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。

构造器注入(Constructor Injection): 实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。

基于注解 : 基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。

3.依赖查找(DL):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)
依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态

小结
依赖倒置原则(DIP):一种软件架构设计的原则(抽象概念,一种思想)。
控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式)。
依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。
IoC容器(也称DI Container):提供了动态地(自动化)创建、注入依赖单元,映射依赖关系等功能,减少了许多代码量(DI框架)。

需要注意的一些地方
1.控制反转的层面
在传统的应用中,程序流程的顺序是由开发者主导的,由于IoC,主导权转移到了框架的手里(因为IoC容器)

2.控制反转需要解决的问题
查找,生成所需的实例,返回给需要者(因此又叫依赖注入)

3.实现依赖注入的目的
尽管一个类A对它所依赖的类B是如何实现的一无所知,类A依然能够与类B通信(通过定义一些通用接口)。类B在开发中可能会有多种实现,依赖注入(同时也是IoC)解决的问题就是自动地将这些类B的实现在需要的时候传递给类A。

4.如何实现依赖注入
最基本的思路是构造一个独立的类,它的功能就是统一为其他所有类的依赖生成所需的实例(assembler,类似容器),然后构造并返回这个类

依赖注入的具体实现方式(主要介绍设值注入和构造器注入)

备注:对类A,类B,类C的定义如下
类A (需要通过容器获取的)
类B (类A的依赖,广义上的接口)
类C (类B的具体实现)

1.构造器注入

a)在类A的构造器参数列表中定义了该类所有需要被依赖注入的东西(类B)

b)在容器中需要先定义好某个接口(广义上的interface,即类B)关联的某个具体实现类(有时还需要配置一些具体参数,即类C),这些容器配置在不同的开发中很可能是不一样的。通常这些配置会是一个独立的文件

c)在需要某个类A的时候通过容器来生成而不是直接new

class MovieLister...     (MovieLister相当于类A,MovieFinder相当于类B)
public MovieLister(MovieFinder finder) {
    this.finder = finder;       
}

class ColonMovieFinder...   (这个相当于类c)
public ColonMovieFinder(String filename) {
    this.filename = filename;
}

(这里返回的pico就是IoC容器)
private MutablePicoContainer configureContainer() {
    MutablePicoContainer pico = new DefaultPicoContainer();
    Parameter[] finderParams =  {new ConstantParameter("movies1.txt")};
    //在使用容器前需要先配置,下面的代码就是对容器的配置
    pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
    pico.registerComponentImplementation(MovieLister.class);
    return pico;
}

下面是通过容器来获得类A的过程
public void testWithPico() {
    MutablePicoContainer pico = configureContainer();//获得一个配置好的容器
    MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);//通过容器来获得类A
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

2.setter注入

a)在类A中为所有需要注入的依赖类(类B)创建setter方法

b)在独立的文件配置类A中的依赖的具体实现(即配置类B的具体实现类C)

c)通过容器生成生成类A

class MovieLister...  (同样的MovieLister为类A,MovieFinder为类B) 
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
  this.finder = finder;
}

class ColonMovieFinder...  (这个同样的相当于类C)
public void setFilename(String filename) {
   this.filename = filename;
}


//下面是容器的配置

    
        
            
        
    
    
        
            movies1.txt
        
    



public void testWithSpring() throws Exception {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");//获得配置好的容器
    MovieLister lister = (MovieLister) ctx.getBean("MovieLister");  //通过容器获取类A
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
Service Locator

与DI类似,Service Locator也是用来打破依赖的

基本思想
提供一个独立的类(即Service Locator),它能够为整个应用提供所需的所有service(也可以理解为component)。

具体实现
1.在类A中,依赖的所有类都是通过Service Locator获取的

    MovieFinder finder = ServiceLocator.movieFinder();

2.通过配置可以定制在Service Locator中实现如何返回一个特定实例,这个与DI类似

小结
1.实际上可以将Service Locator和DI结合使用,在类A中通过Service Locator获取依赖,而在Service Locator中则可以通过DI来实现获取具体的实例(或者将Service Locator与DI互换也可以?)

2.动态的Service Locator:使用一张映射表,通过查表实现(或直接获取)具体的实例

3.Service Locator与DI 的区别:使用Service Locator时是显式地调用Locator,而Di并没有显式地调用

Yii2中的依赖注入

相关的类:

yiidiContainer 容器

yiidiinstance 容器或Service Locator中的东西: 本质上是对于某一个类实例的引用

yiidiServiceLocator

1.yiidiinstance
主要用在两个地方:

1.在配置DI容器的时候,使用Instance来引用一个类名接口名或者是别名(即Instance的id属性)。因此后续DI容器可以将这个引用解析成相应的对象

2.用在那些使用service locator获取依赖对象的类中

对于 yiidiInstance:
1.表示的是容器中的内容,代表的是对于实际对象的引用。
2.DI容器可以通过他获取所引用的实际对象。
3.Instance类仅有的一个属性id一般表示的是实例的类型(即component ID, class name, interface name or alias name)。

2.yiidiContainer
注意:下面所说的“对象类型”的具体定义为“类名,接口名,别名”
对于yiidiContainer

a) 5个私有属性(都是数组):$_singletons,$_definitions,$_params,$_reflections,$_dependencies

b) $_singletons // 用于保存单例Singleton对象,以对象类型为键

c) $_definitions // 用于保存依赖的定义,以对象类型为键

d) $_params // 用于保存构造函数的参数,以对象类型为键

e) $_reflections // 用于缓存ReflectionClass对象,以对象类型为键

f) $_dependencies // 用于缓存依赖信息,以对象类型为键

注意
1.在DI容器中,依赖关系的定义是唯一的。 后定义的同名依赖,会覆盖前面定义好的依赖。
2.上面的键具体就是:带命名空间的类名,接口名,或者是一个别名
3.对于 $_definitions 数组中的元素,它要么是一个包含了”class” 元素的数组,要么是一个PHP callable, 再要么就是一个具体对象。这就是规范化后的最终结果
4.对于$_singletons数组中的元素,要不就是null(表示还未实例化),要不就是一个具体的实例
5.对于$_params数组中的元素,就是一个数组,包含构造函数的所有参数
6.对于$_reflections数组中的元素,就是一个ReflectionClass对象
7.setter注入可以在实例化后

yiidiContainer使用的具体过程
一个简单的例子

namespace appmodels;

use yiiaseObject;
use yiidbConnection;
use yiidiContainer;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends Object implements UserFinderInterface
{
    public $db;

    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends Object
{
    public $finder;

    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

$container = new Container;
$container->set("yiidbConnection", [
    "dsn" => "...",
]);
$container->set("appmodelsUserFinderInterface", [
    "class" => "appmodelsUserFinder",
]);
$container->set("userLister", "appmodelsUserLister");

$lister = $container->get("userLister");

// which is equivalent to:

$db = new yiidbConnection(["dsn" => "..."]);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

1.在类A的构造器参数列表中定义了该类所有需要被依赖注入的东西(类B)

2.注册依赖:
a)yiidiContainer::set()
b)yiidiContainer::setSinglton()
使用到了$_definitions ,$_params, $_singletons

3.对象的实例化
a)解析依赖信息
yiidiContainer::getDependencies() (会被后续的build()调用)
getDependencies():操作$_reflections与$_dependencies
1.会向$_reflections 和 $_dependencies写入信息
2.使用PHP的反射机制来获取类的有关信息,主要就是为了从构造器中获取依赖信息,会将反射得到的信息写入$_reflections
3.将从构造器中获取的依赖信息(即构造函数的参数列表)写入$_dependencies
4.返回值: 数组[$reflection, $dependencies]

yiidiContainer::resolveDependencies() (同样的会被后续的build()调用)
resolveDependencies()利用getDependencies()获得的信息进一步具体处理(递归调用)。处理依赖信息, 将依赖信息中保存的Instance实例所引用的类或接口进行实例化。

b)创建实例
yiidiContainer::build()
由getDependencies()获得第一层依赖
由resolveDependencies()递归分析依赖,最终生成所有依赖的实例
$reflection->newInstanceArgs($dependencies);//生成所有依赖后生成这个实例

注意:DI容器只支持 yiibaseObject 类,也就是说如果你想你的类可以放在DI容器里,那么必须继承自 yiibaseObject 类。

4.获取依赖实例化对象

yiidiContainer::get()
a)如果是已经实例化的单例,直接返回($_singletons)

b)如果是尚未定义(不存在于$_definition),则说明其实例化没有依赖,调用build()

c)存在$_definition
    i.$definition为callable,直接调用
    ii.$definition为数组,根据$definition数组中的‘class’,递归调用get(),递归终止的条件是(当具体实现类就是当前的依赖类时),递归结束时调用build()进行实例化
    iii.$definition为对象,直接返回该对象,并将该对象设置为单例
    

setSinglton()类似
public function set($class, $definition = [], array $params = [])
{
    //normalizeDefinition()处理后,返回值要么是一个包含了”class” 元素的数组,要么是一个PHP callable, 再要么就是一个具体对象
    $this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
    $this->_params[$class] = $params;
    unset($this->_singletons[$class]);
    return $this;
}

public function get($class, $params = [], $config = [])
{
    if (isset($this->_singletons[$class])) {//是单例,且已经实例化(不为null)
        // singleton
        return $this->_singletons[$class];
    } elseif (!isset($this->_definitions[$class])) {//还没有定义过,需要build
        return $this->build($class, $params, $config);
    }

    $definition = $this->_definitions[$class];

    if (is_callable($definition, true)) {
        $params = $this->resolveDependencies($this->mergeParams($class, $params));
        $object = call_user_func($definition, $this, $params, $config);
    } elseif (is_array($definition)) {
        $concrete = $definition["class"];
        unset($definition["class"]);

        $config = array_merge($definition, $config);
        $params = $this->mergeParams($class, $params);

        if ($concrete === $class) {//$concrete相当于之前提到的具体实现类C,而$class则相当于接口类B
            $object = $this->build($class, $params, $config);
        } else {
            $object = $this->get($concrete, $params, $config);//递归,直到找到具体的实现类C
        }
    } elseif (is_object($definition)) {
        return $this->_singletons[$class] = $definition;
    } else {
        throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition));
    }

    if (array_key_exists($class, $this->_singletons)) {
        // singleton
        $this->_singletons[$class] = $object;
    }

    return $object;
}


protected function build($class, $params, $config)
{
    /* @var $reflection ReflectionClass */
    list ($reflection, $dependencies) = $this->getDependencies($class); //获取第一层依赖关系

    foreach ($params as $index => $param) {
        $dependencies[$index] = $param; //额外提供的构造函数参数,添加到依赖中
    }

    $dependencies = $this->resolveDependencies($dependencies, $reflection);//递归解析依赖,并会在此返回依赖的实例
    if (!$reflection->isInstantiable()) {
        throw new NotInstantiableException($reflection->name);
    }
    if (empty($config)) {
        return $reflection->newInstanceArgs($dependencies);//通过反射实例生成对象
    }
    
    //config中的对象作为该类的property使用
    if (!empty($dependencies) && $reflection->implementsInterface("yiiaseConfigurable")) {
        // set $config as the last parameter (existing one will be overwritten)
        $dependencies[count($dependencies) - 1] = $config;
        return $reflection->newInstanceArgs($dependencies);
    } else {
        $object = $reflection->newInstanceArgs($dependencies);
        foreach ($config as $name => $value) {
            $object->$name = $value;
        }
        return $object;
    }
}


protected function getDependencies($class)
{
    if (isset($this->_reflections[$class])) {//如果已经反射解析过则直接返回
        return [$this->_reflections[$class], $this->_dependencies[$class]];
    }

    $dependencies = [];
    $reflection = new ReflectionClass($class);
    
    //构造函数的参数即这个类的依赖
    $constructor = $reflection->getConstructor();
    if ($constructor !== null) {    
        foreach ($constructor->getParameters() as $param) {
            if ($param->isDefaultValueAvailable()) {
                $dependencies[] = $param->getDefaultValue();
            } else {
                $c = $param->getClass();//这里要能获取到类名需要在构造函数中用类型限制参数,否则获取到null,而且注意对于php的基本类型,获取到的也是null
                $dependencies[] = Instance::of($c === null ? null : $c->getName());
            }
        }
    }

    $this->_reflections[$class] = $reflection;
    $this->_dependencies[$class] = $dependencies;

    return [$reflection, $dependencies];
}



protected function resolveDependencies($dependencies, $reflection = null)
{
    foreach ($dependencies as $index => $dependency) {
        if ($dependency instanceof Instance) {
            if ($dependency->id !== null) { //这里的dependency是Instance的实例
                $dependencies[$index] = $this->get($dependency->id);
            } elseif ($reflection !== null) {
                $name = $reflection->getConstructor()->getParameters()[$index]->getName();
                $class = $reflection->getName();
                throw new InvalidConfigException("Missing required parameter "$name" when instantiating "$class".");
            }
        }
    }
    return $dependencies;
}

递归调用的示意图

在Yii2中调用组件

先看一下各个类的继承关系

下面以Yii::$app->db为例
1.配置组件
配置的内容:

"components" => [
    "db" => [
        "class" => "yiidbConnection",
        "dsn" => "mysql:host=localhost;dbname=wechat",
        "username" => "root",
        "password" => "michael",
        "charset" => "utf8",
    ],

2.在框架的启动过程中加载组件的定义
Yii2的启动
入口脚本:

(new yiiwebApplication($config))->run();
1.new yiiwebApplication($config)
2.run()

yiiwebApplication的构造函数

public function __construct($config = [])
{
    Yii::$app = $this;
    static::setInstance($this);   //将当前module存到Yii::$app->loadedModules[]
    $this->state = self::STATE_BEGIN;

    //1.通过$config配置别名,基本参数
    //2.配置核心组件(仅仅是配置,将Yii框架写好的配置与自己的配置合并)
    $this->preInit($config); 

    $this->registerErrorHandler($config);

    //下面这一行是重点,Component是当前类的祖先
    //在下面的构造函数中执行了Yii::configure($this, $config),将$config中的配置作为属性添加到$app中
    Component::__construct($config);
}

Component::__construct($config)

//实际上下面这个构造函数的定义在yiiaseObject中
public function __construct($config = [])
    {
        if (!empty($config)) {
            Yii::configure($this, $config);
        }
        $this->init();
    }

Yii::configure($this, $config)

public static function configure($object, $properties)
    {
        foreach ($properties as $name => $value) {
            //下面这行代码会触发魔术方法 ($object->components = $value)
            //实际执行的代码是ServiceLocator::setComponents($components)
            $object->$name = $value;
        }

        return $object;
    }

ServiceLocator::setComponents($components)

public function setComponents($components)
{
    foreach ($components as $id => $component) {
        $this->set($id, $component);
    }
}

最终加载组件配置的代码

//从下面的代码中可以看到,最终组件的配置被存储在$app->_definitions数组中
public function set($id, $definition)
{
    if ($definition === null) {
        unset($this->_components[$id], $this->_definitions[$id]);
        return;
    }

    unset($this->_components[$id]);

    if (is_object($definition) || is_callable($definition, true)) {
        // an object, a class name, or a PHP callable
        $this->_definitions[$id] = $definition;
    } elseif (is_array($definition)) {
        // a configuration array
        if (isset($definition["class"])) {
            $this->_definitions[$id] = $definition;
        } else {
            throw new InvalidConfigException("The configuration for the "$id" component must contain a "class" element.");
        }
    } else {
        throw new InvalidConfigException("Unexpected configuration type for the "$id" component: " . gettype($definition));
    }
}

3.获取组件
Yii::$app->db会触发魔术方法,调用ServiceLocator::__get()

public function __get($name)
{
    if ($this->has($name)) {
        //已经定义过组件
        return $this->get($name);
    } else {
        //没有定义过组件
        return parent::__get($name);
    }
}

public function has($id, $checkInstance = false)
{
    //这里因为在框架启动过程中将组件的配置加载到$_definitions中了,所以会返回true
    return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}


//通过下面的代码可以看到,如果组件已经实例化过存储在$_components中了,就直接返回
//否则通过Yii::createObject($definition)来生成组件实例,并存储到$_components中
public function get($id, $throwException = true)
{
    if (isset($this->_components[$id])) {
        return $this->_components[$id];
    }

    if (isset($this->_definitions[$id])) {
        $definition = $this->_definitions[$id];
        if (is_object($definition) && !$definition instanceof Closure) {
            return $this->_components[$id] = $definition;
        } else {
            return $this->_components[$id] = Yii::createObject($definition);
        }
    } elseif ($throwException) {
        throw new InvalidConfigException("Unknown component ID: $id");
    } else {
        return null;
    }
}

Yii::createObject($definition)

//在Yii2框架中要使用DI来生成对象的话,可以通过调用Yii::createObject($definition)实现
public static function createObject($type, array $params = [])
{
    if (is_string($type)) {
        //使用容器
        return static::$container->get($type, $params);
    } elseif (is_array($type) && isset($type["class"])) {
        $class = $type["class"];
        unset($type["class"]);
        return static::$container->get($class, $params, $type);
    } elseif (is_callable($type, true)) {
        return static::$container->invoke($type, $params);
    } elseif (is_array($type)) {
        throw new InvalidConfigException("Object configuration must be an array containing a "class" element.");
    }

    throw new InvalidConfigException("Unsupported configuration type: " . gettype($type));
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/25699.html

相关文章

  • 使用Yii2依赖注入简化开发

    摘要:本文代码什么是依赖注入对象由框架来创建而不是程序员通过创建。解除了调用者与被调用者之间的依赖。的依赖注入通过提供容器特性。灵活使用可以使我们从依赖关系中解脱出来,专注于业务逻辑。 本文代码 https://github.com/xialeistudio/yii2-di-demo 什么是依赖注入(DI)? 对象由框架来创建而不是程序员通过 new 创建。跟IoC差不多一个意思。 为什么要...

    Luosunce 评论0 收藏0
  • 图解Yii2框架依赖注入容器、服务定位器

    摘要:调用方法创建类得实例化对象,实际上又调用了依赖注入容器获取每一个类的实例化对象。依赖注入容器自动解决待实例化类的依赖关系,并返回待实例化类的实例对象。 以下是Yii2源码中,ServiceLocator(服务定位器)与Container(依赖注入容器)的关系解析图。 一句话总结 Application继承了ServiceLocator,是一个服务器定位器,ServiceLocator用...

    AlphaGooo 评论0 收藏0
  • yii2框架中的di容器源码中了解反射的作用

    摘要:反射简介参考官方简介的话,具有完整的反射,添加了对类接口函数方法和扩展进行反向工程的能力。此外,反射提供了方法来取出函数类和方法中的文档注释。 反射简介 参考官方简介的话,PHP 5 具有完整的反射 API,添加了对类、接口、函数、方法和扩展进行反向工程的能力。 此外,反射 API 提供了方法来取出函数、类和方法中的文档注释。 YII2框架中示例 对于yii2框架,应该都知道di容器,...

    dantezhao 评论0 收藏0
  • yii过滤xss代码,防止sql注入教程

    摘要:好啦,我们看看在框架的不同版本中是怎么处理攻击,注入等问题的。那要是,又是怎样处理的喃考虑目前国内网站大部分采集文章十分频繁,更有甚者不注明原文出处,原作者更希望看客们查看原文,以防有任何问题不能更新所有文章,避免误导继续阅读 作者:白狼 出处:http://www.manks.top/yii2_filter_xss_code_or_safe_to_database.html 本文版权...

    Shonim 评论0 收藏0
  • Yii2 完整框架分析(详细)

    摘要:行为是如何注册到组件的呢通过注册行为之后,实际上是添加到了的属性中那么行为中的属性,就添加到了,中进行直接调用行为里面的方法的时候,实际上触发了里面的魔术方法继承链图解 Yii2 框架Trace 准备 了解composer的autoload psr0 psr4 加载机制 了解spl_autoload_register 了解依赖注入的实现原理反射 了解常用魔术方法__set,__get...

    spademan 评论0 收藏0

发表评论

0条评论

harriszh

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<