[原创]Swoft源码剖析-Swoft中IOC容器的实现原理

Swoft为应用提供一个完整的IOC容器作为依赖管理方案 ,是Swoft AOP功能,RPC模块等功能的实现基础 。
他主要解决的功能有三个:
1. 避免了麻烦地手工管理对象间种种嵌套依赖。
2. 对象的依赖关系不再在编译期确定,提供了运行期改变行为的更多弹性。
3. 对象可以不再依赖具体实现,而是依赖抽象的接口或者抽象类
对依赖管理有兴趣的同学可以查阅马丁大叔的这篇文章<Inversion of Control Containers and the Dependency Injection pattern>

服务定位器

Bean通过类级别注解@Bean定义,Bean定义后程序可以直接通过App::getBean();获取到一个Bean的实例。

App::getBean()提供 服务定位器 式的依赖管理方式,用于可以通过访问服务定位器获取特定的实例,服务定位器解决了"实例构造,实例间依赖管理,具体实现类选择"的问题,并对用户屏蔽相关细节。

Container->set()方法是App::getBean()底层实际创建bean的方法。原理是通过反射和各种注解(参考注解章节)提供的信息和方法构造Bean的一个代理对象。

//Swoft\Bean\Container.php
    /**
     * 创建Bean
     *
     * @param string           $name             名称
     * @param ObjectDefinition $objectDefinition bean定义
     * @return object
     * @throws \ReflectionException
     * @throws \InvalidArgumentException
     */
    private function set(string $name, ObjectDefinition $objectDefinition)
    {
        // bean创建信息
        $scope = $objectDefinition->getScope();
        $className = $objectDefinition->getClassName();
        $propertyInjects = $objectDefinition->getPropertyInjections();
        $constructorInject = $objectDefinition->getConstructorInjection();

        //ref属性重定向依赖查找,一般用于在Interface这种需要具体实现类的Bean上,用于指定实际使用的实现类
        if (!empty($objectDefinition->getRef())) {
            $refBeanName = $objectDefinition->getRef();
            return $this->get($refBeanName);
        }

       // 构造函数参数注入
        $constructorParameters = [];
        if ($constructorInject !== null) {
            $constructorParameters = $this->injectConstructor($constructorInject);
        }

          
        $reflectionClass = new \ReflectionClass($className);
        $properties = $reflectionClass->getProperties();

        // 通过反射new实例
        $isExeMethod = $reflectionClass->hasMethod($this->initMethod);
        $object = $this->newBeanInstance($reflectionClass, $constructorParameters);

        // 属性注入
        $this->injectProperties($object, $properties, $propertyInjects);

        // 执行Swoft Bean约定的初始化方法`init()`
        if ($isExeMethod) {
            $object->{$this->initMethod}();
        }

        //动态代理,具体见AOP章节
        if (!$object instanceof AopInterface) {
            $object = $this->proxyBean($name, $className, $object);
        }

        // 单例处理
        if ($scope === Scope::SINGLETON) {
            $this->singletonEntries[$name] = $object;
        }

        return $object;
    }

依赖注入

相对于 服务定位器,依赖注入是一种更加先进的依赖管理实践。

服务定位器模式中,客户端需要调用服务定位器本身,对服务定位器本身存在依赖;
依赖注入模式中,客户端和依赖注入管理器之间关系也是控制反转的,客户端并不知道依赖管理器的存在,由依赖管理器调用客户端并注入具体的依赖对象。

Swoft的依赖注入管理方案基于服务定位器。提供的注入方式有三种:

属性注入

    /**
     * @Reference("user")
     * @var \App\Lib\MdDemoInterface
     */
    private $mdDemoService;

    /**
     * @Inject()
     * @var \App\Models\Logic\UserLogic
     */
    private $logic;

    /**
     * the name of pool
     *
     * @Value(name="${config.service.user.name}", env="${USER_POOL_NAME}")
     * @var string
     */
    protected $name = "";

上面@Reference,@Inject,@value三者是典型的属性注入用的注解声明,在一个Bean类中声明这三种注解的属性会分别被注入特定的Rpc客户端代理对象普通的Bean代理对象 ,和配置文件配置值

属性注入元信息的解析

Bean的各个属性的注入信息是在注解搜集阶段完成的,即在Swoft的启动阶段就已经完成

//Swoft\Bean\Wrapper\AbstractWrapper.php
    /**
     * 属性解析
     *
     * @param  array $propertyAnnotations
     * @param string $className
     * @param string $propertyName
     * @param mixed  $propertyValue
     *
     * @return array
     */
    private function parsePropertyAnnotations(array $propertyAnnotations, string $className, string $propertyName, $propertyValue)
    {
       
        $isRef = false;
        $injectProperty = "";

        // 没有任何注解
        if (empty($propertyAnnotations) || !isset($propertyAnnotations[$propertyName])
            || !$this->isParseProperty($propertyAnnotations[$propertyName])
        ) {
            return [null, false];
        }

        // 属性注解解析
        foreach ($propertyAnnotations[$propertyName] as $propertyAnnotation) {
            $annotationClass = get_class($propertyAnnotation);
            if (!in_array($annotationClass, $this->getPropertyAnnotations())) {
                continue;
            }

            // 使用具体的解析器(如ValueParser,ReferenceParser等)解析注入元信息
            $annotationParser = $this->getAnnotationParser($propertyAnnotation);
            if ($annotationParser === null) {
                $injectProperty = null;
                $isRef = false;
                continue;
            }
            list($injectProperty, $isRef) = $annotationParser->parser($className, $propertyAnnotation, $propertyName, "", $propertyValue);
        }
        return [$injectProperty, $isRef];
    }

$isRef 决定属性需要注入一个Bean还是一个标量值
$injectProperty 指代该属性要注入的Bean名或者具体标量值
这两者最终会封装进一个Swoft\Bean\ObjectDefinition对象中并保存在AnnotationResource->$definitions

属性注入

属性注入在调用服务定位器App::getBean()生成Bean的时候进行,此时服务定位器根据之前解析到的$isRef$injectProperty信息注入特定的值到属性中。

// Swoft\Bean\Container.php
    /**
     * 注入属性
     *
     * @param  mixed                $object
     * @param \ReflectionProperty[] $properties $properties
     * @param  mixed                $propertyInjects
     * @throws \InvalidArgumentException
     */
    private function injectProperties($object, array $properties, $propertyInjects)
    {
        foreach ($properties as $property) {
            //...
          
            // 属性是数组
            if (\is_array($injectProperty)) {
                $injectProperty = $this->injectArrayArgs($injectProperty);
            }

            // 属性是bean引用
            if ($propertyInject->isRef()) {
                $injectProperty = $this->get($injectProperty);
            }

            if ($injectProperty !== null) {
                $property->setValue($object, $injectProperty);
            }
      }
}

属性注入依赖于服务定位器,如果一个对象是由用户手动new出来的,将不会获得属性注入功能。

方法参数注入

Swoft有很多框架按照约定直接调用Bean的特定方法的地方,如框架会在收到web请求的时候调用Controllert的某个action方法,如果有合适的AOP连接点会调用对应的通知方法.....
在这些框架调用的种种方法中基本都支持方法参数注入,Swoft会根据参数类型,参数名等规则自动给方法的参数填充合适的值。

<?php
//App\Controllers\RouteController.php;

    /**
     *  这个例子中,除了Request 和Response 是固定的注入特定结构的对象,其他参数都是根据路由规则注入
     * @RequestMapping(route="user/{uid}/book/{bid}/{bool}/{name}")
     *
     * @param bool                $bool  参考RequestMapping
     * @param Request  $request     
     * @param int                 $bid  
     * @param string              $name
     * @param int                 $uid
     * @param Response $response
     *
     * @return array
     */
    public function funcArgs(bool $bool, Request $request, int $bid, string $name, int $uid, Response $response)
    {
        //...
    }

方法注入的实现较为零散,每个方法注入点都会有类似的代码处理注入的数据,这里看一下action的注入处理。action的参数注入处理代码在HandlerAdapter->bindParams()

//Swoft\Http\Server\Route\HandlerAdapter.php
    /**
     * binding params of action method
     *
     * @param ServerRequestInterface $request request object
     * @param mixed $handler handler
     * @param array $matches route params info
     *
     * @return array
     * @throws \ReflectionException
     */
    private function bindParams(ServerRequestInterface $request, $handler, array $matches)
    {
        if (\is_array($handler)) {
            list($controller, $method) = $handler;
            $reflectMethod = new \ReflectionMethod($controller, $method);
            $reflectParams = $reflectMethod->getParameters();
        } else {
            $reflectMethod = new \ReflectionFunction($handler);
            $reflectParams = $reflectMethod->getParameters();
        }

        $bindParams = [];
        // $matches    = $info['matches'] ?? [];
        $response   = RequestContext::getResponse();

        // binding params
        foreach ($reflectParams as $key => $reflectParam) {
            $reflectType = $reflectParam->getType();
            $name        = $reflectParam->getName();

            // 未定义参数类型直接使用$matches对应值
            if ($reflectType === null) {
                if (isset($matches[$name])) {
                    $bindParams[$key] = $matches[$name];
                } else {
                    $bindParams[$key] = null;
                }
                continue;
            }

            /**
             * @notice \ReflectType::getName() is not supported in PHP 7.0, that is why use __toString()
             */
            $type = $reflectType->__toString();
            //若类型的特定类型如Request/Response,直接注入对应对象,否则注入类型转换后的$matches对应值
            if ($type === Request::class) {
                $bindParams[$key] = $request;
            } elseif ($type === Response::class) {
                $bindParams[$key] = $response;
            } elseif (isset($matches[$name])) {
                $bindParams[$key] = $this->parserParamType($type, $matches[$name]);//类型强转处理
            } else {
                $bindParams[$key] = $this->getDefaultValue($type);//提供一个指定类型的默认值(等价于0)
            }
        }

        return $bindParams;
    }

$matches对应的是REST模板型路由特定字段的具体值,举个例子。若实际访问/user/100,其匹配的路由为/user/{uid},则$matches会存储['uid'=>'100']信息。
其他 方法参数注入点 的实现大同小异

构造器注入

swoft当前的构造器注入实现尚不完整,可能还有变动,这里就先不说了。

Swoft源码剖析系列目录:https://www.jianshu.com/p/2f679e0b4d58

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

推荐阅读更多精彩内容