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

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

服务定位器

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会根据参数类型,参数名等规则自动给方法的参数填充合适的值。

方法注入的实现较为零散,每个方法注入点都会有类似的代码处理注入的数据,这里看一下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

你可能感兴趣的:([原创]Swoft源码剖析-Swoft中IOC容器的实现原理)