多应用+多态+容器依赖注入在WorkerMan模式下产生的不确定性

sudoim

环境描述

框架:ThinkPHP6,WorkerMan方式启动(依赖:topthink/think-worker)
环境:PHP8.0.22
场景:多应用模式
目录结构(只简单列举):

├─app           程序根目录
|  ├─admin      应用目录
|  |  ├─provider.php    容器定义文件
│  |  ├─service         服务目录
│  |  |  └─AdminService.php  管理员服务类
│  |  └─ ...            更多类库目录
│  ├─api        应用目录
|  |  ├─provider.php    容器定义文件
│  |  ├─service         服务目录
│  |  |  └─UserService.php   用户服务类
│  |  └─ ...            更多类库目录
|  └─common     公共类库目录
|     ├─contract        接口定义目录
│     |  └─SessionServiceInterface.php 会话服务接口类
|     ├─model           模型目录
│     |  ├─Admin.php    管理员模型
│     |  └─User.php     用户模型
|     ├─middleware      中间件目录
|     |  └─Authorization.php 权限认证中间件
│     └─ ...            更多类库目录
└─ ...

代码部分:

app/admin/provider.php

<?php

use app\admin\service\AdminService;
use app\common\contract\SessionServiceInterface;

return [
    SessionServiceInterface::class => AdminService::class
];

app/api/provider.php

<?php

use app\api\service\UserService;
use app\common\contract\SessionServiceInterface;

return [
    SessionServiceInterface::class => UserService::class
];

app/common/contract/SessionServiceInterface.php

<?php

namespace app\common\contract;

interface SessionServiceInterface
{
    public function access();

    public function right();
}

app/admin/service/AdminService.php

<?php

namespace app\admin\service;

use app\common\contract\SessionServiceInterface;
use app\common\model\Admin;

class AdminService implements SessionServiceInterface
{
    public function access(): Admin
    {
        ...
    }

    public function right(): bool
    {
        ...
    }
}

app/api/service/UserService.php

<?php

namespace app\api\service;

use app\common\contract\SessionServiceInterface;
use app\common\model\User;

class UserService implements SessionServiceInterface
{
    public function access(): User
    {
        ...
    }

    public function right(): bool
    {
        ...
    }
}

app/common/middleware/Authorization.php

<?php

namespace app/common/middleware;

/**
 * 权限认证中间件(全局路由中间件)
 */
class Authorization
{
    public function __contruct(SessionServiceInterface $service)
    {
        // 中间件中打印输出(WorkerMan设置进程数量1可快速复现)
        // 第一次,访问api应用,输出:app\api\service\UserService
        // 第二次,访问admin应用,输出:app\api\service\UserService
        // 第二次预期结果应该输出:app\admin\service\AdminService,但和预期结果不符
        var_dump(get_class($service));
    }
}

问题描述

由于WorkerMan不会在每次请求后释放全局对象及类的静态成员,而容器又是单例模式实现的,因此所有的依赖注入都保存在容器类的静态成员instance中,在请求处理完成后不会被释放

以下是容器代码实现片段

<?php

namespace think;

class Container implements ContainerInterface, ArrayAccess, IteratorAggregate, Countable
{
    /**
     * 容器对象实例
     * @var Container|Closure
     */
    protected static $instance;

    /**
     * 获取当前容器的实例(单例)
     * @access public
     * @return static
     */
    public static function getInstance()
    {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }

        if (static::$instance instanceof Closure) {
            return (static::$instance)();
        }

        return static::$instance;
    }
}

所以,当首次访问两个应用中的其中一个,在使用依赖注入后,再访问另一个时,后者会因为相同的容器标识已经存在,而获取到访问的第一个应用注入的实例对象,即:

首次访问api应用,依赖注入时是UserService类,获取自然是UserService类
而再次访问admin应用,由于WorkerMan的特性,静态成员不会释放,导致容器标识已经存在,所以使用依赖注入获取到的是UserService类,而不是AdminService类。

但实际情况比这个复杂得多,由于是多进程内存常驻,且静态变量不会在请求后释放,就算在每次请求前后主动删除容器标识(这种也有很多问题),也并不能确保在多个应用并发访问时,依赖注入获取的到底是UserService类还是AdminService类。

在查阅容器依赖注入和多态时,由于网上资料非常稀少,一度曾以为是自己强行将依赖注入和多态捆绑到一起使用才产生的问题,但仔细想想,有这样想法的人应该不止我一个人,毕竟在容器标识绑定的说明处,也有和Interface相关的简单示例,且尝试过在传统fpm的情况下不会有此问题产生,至此才一步一步查阅到WorkerMan对单例模式资源释放的说明。

由于本人学识浅薄,对WorkMan的很多底层知识了解得不是很深入,进程之间的交互和原理也知道得并不多,能力有限,希望能有大佬懂得其中的缘由和分享解决的办法,非常感谢!

220 2 0
2个回答

powerbowen

一直在用webman,没有用workerman
首先依赖注入是否可以使用控制器方法传参方式或者使用控制器构造函数传参方式
webman中有个controller_reuse这个配置,不确定workerman中是否有相同的东西,有的话不要开
看你那个写法是lv的写法,在不同应用下注册不同的接口,每个接口只映射唯一实现服务,是否真的有必要这么写,是否可以放到该应用的基类中设置映射列表或直接注入

  • sudoim 13天前

    我目前这样的写法是打算将登录鉴权从控制器中剥离到中间件(框架中的Middleware)。
    在中间件进行鉴权时,通过依赖注入获取到应用下的相关服务类,调用服务类的access方法做登录认证,如果认证成功会返回对应的模型对象,否则抛出异常,而right方法的作用则是实现访问控制器方法的权限认证。
    而且我认为,在同一套系统中,存在多种不同用户体系是比较常见的,最常见的就是admin和user,但是相同的登录鉴权流程可能要每种体系都写一次。而如果能够好好利用中间件和依赖注入的特点,则相同的流程只需要在中间件实现,而具体的逻辑只需要由不同体系应用中的Service服务类实现。
    让我更清晰一点的展开来说吧,中间件使用对SessionServiceInterface的依赖注入来获取对应应用下的Service类,假设User模型和Admin模型都实现于一个新的接口SessionModelInterface,那么access方法的返回可以写成SessionModelInterface,同样的,通过依赖注入,可以在某个应用中通过对SessionModelInterface的依赖注入来获取对应应用登录的用户模型实例,甚至可以将它放到控制器基类中。
    这样的好处,只需要一个中间件,就可以完成任意多种用户体系的登录鉴权流程。
    虽然可以将多态的实例化以Config配置的方式设置到配置文件中实现同样的效果,但依赖注入的带来的便捷更是一种优势。

  • powerbowen 13天前

    按照你的需求,多应用,每个应用单独设置验证用户中间件是否可行,用户验证中间件基类,子中间件应用在多应用中,子应用的中间件设置对应service给到base验证中间件
    依赖注入可以用你这个注册的方式也可以直接在方法里传进去,只是实现不同

  • sudoim 13天前

    虽然登录鉴权对于单独的一个人来说怎么实现都行,但我抛出的却是一个真实存在的问题。
    这篇帖子也并不是着重在如何实现登录鉴权的功能上,根本的问题在于讨论如何在这样的情况下使依赖注入获取到正确的类。

  • powerbowen 13天前

    你当前不就是按照应用来区分的哪个类么,如果不是的话,你上面代码的依赖映射为什么要写在不同模块下,而且依赖注入为什么一定要用映射呢

  • sudoim 13天前

    是按照应用来获取到的,但只用通过容器标识进行映射,而我只需要在一个全局中间件中调用。
    但你的意思是每个应用都有一个中间件,而这些中间件除了使用的Service实例不同,其它的代码基本都是相同的,这样去实现固然是可行的,可重复的代码降低了整体代码的质量。如果我只是为了在我的项目中用上某个功能,又何必去研究现在的方案呢

walkor

这个问题在于容器无法区分多应用。按道理应该是容器根据当前应用提供不同的类实例。

webman的应用插件机制与你的这种情况类似。webman中每个应用插件可以看作一个相对独立的应用,都自己的依赖注入配置。当需要实例化类时,会判断当前请求属于哪个应用,然后会用对应应用的容器去实例化类,这样就做到应用分离了。

你可以给 think/Container::$instance 设置为一个回调函数,think/Container::getInstance 被调用时回调函数根据不同的应用返回不同的容器实例,这样估计可行。

  • sudoim 12天前

    非常感谢!这种的思路我也想到了,也是我正在尝试的方式,虽然由容器标识直接映射类变成了类似容器标识映射闭包的方式,但确实这是相对来说,比较可行且简便的方式。

🔝