用PHP从零开始写一个编译器(五)

769 阅读7分钟

前面的链接:

用PHP从零开始写一个编译器(一)

用PHP从零开始写一个编译器(二)

用PHP从零开始写一个编译器(三)

用PHP从零开始写一个编译器(四)

前面我们把整个编译器的算法讲解完成了。Parser类编译完成以后是一个PHP程序可用的数组。但是,为了对程序员友好,我们最后把数据封装到了Query类中。为什么要这么做呢,这是因为,对于完整的查询或者是查询语句片断,PHP程序使用时是要区别对待的。其实,任何一个开源,如果你对程序员不友好,那就是死路一条。这也就是为什么Laravel框架能在短时间内成为世界第一,因为,它真的是好用。

其实,Query类,也并没有做什么,它就是实现了QueryInterface这个接口。

declare(strict_types=1);
/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\RqlParser;

/**
 * Interface QueryInterface
 *
 * @package ByteFerry\RqlParser
 *
 * @property array $container
 */
interface QueryInterface
{

    /**
     * @return QueryInterface
     */
    public static function of();

    /**
     * @param array $query
     *
     * @return \ByteFerry\RqlParser\QueryInterface
     */
    public function from($query);

    /**
     * @param $name
     *
     * @return mixed|null
     */
    public function __get($name);


    /**
     * @param $name
     *
     * @return bool
     */
    public function __isset($name);


    /**
     * @return mixed
     */
    public function toArray();
}

而实现这个接口的有两个类,一个叫Fragment,它是RQL查询片段。

declare(strict_types=1);

/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\RqlParser;

use ByteFerry\RqlParser\Abstracts\BaseObject;

/**
 * Class Fragment
 *
 * @package ByteFerry\RqlParser
 */
class Fragment extends BaseObject implements QueryInterface
{

    /**
     * @var array
     */
    protected $container = [];


    /**
     * @param array $query
     *
     * @return \ByteFerry\RqlParser\Fragment
     */
    public function from($query)
    {
        $this->container = $query;
        return $this;
    }

    /**
     * @param $name
     *
     * @return mixed|null
     */
    public function __get($name)
    {
        if (isset($this->container[$name])) {
            return $this->container[$name];
        }
        return null;
    }

    /**
     * @param $name
     *
     * @return bool
     */
    public function __isset($name)
    {
        return isset($this->container[$name]);
    }

    /**
     * @return array|mixed
     */
    public function toArray(){
        return $this->container;
    }
}

另一个则是Query,Query对象提供了

declare(strict_types=1);
/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\RqlParser;

use ByteFerry\RqlParser\Abstracts\BaseObject;

/**
 * Class Query
 *
 * @package ByteFerry\RqlParser
 */
class Query extends BaseObject implements QueryInterface
{

    /**
     * @var array
     */
    protected $container = [];


    /**
     * @param array $query
     *
     * @return \ByteFerry\RqlParser\Query
     */
    public function from($query)
    {
        $this->container = $query;
        return $this;
    }

    /**
     * @param $name
     *
     * @return mixed|null
     */
    public function __get($name){
        if(isset($this->container[$name])){
            return $this->container[$name];
        }
        return null;
    }

    /**
     * @param $name
     *
     * @return bool
     */
    public function __isset($name){
        return isset($this->container[$name]);
    }

    /**
     * @return array|mixed
     */
    public function toArray(){
        return $this->container;
    }

    /**
     * @return mixed|null
     */
    public function  getOperator(){
        return $this->container['operator']??null;
    }

    /**
     * @return mixed|null
     */
    public function  getQueryType(){
        return $this->container['query_type']??null;
    }

    /**
     * @return string
     */
    public function getResourceName(){
        return $this->container['resource']??null;
    }

    /**
     * @return array
     */
    public function getColumns(){
        return $this->container['columns']??null;
    }

    /**
     * @return mixed|null
     */
    public function getColumnsOperator(){
        return $this->container['columns_operator']??null;
    }

    /**
     * @return mixed|null
     */
    public function getGroupBy(){
        return $this->container['group_by']??null;
    }

    /**
     * @return mixed|null
     */
    public function getFilter(){
        return $this->container['filter'][0]??null;
    }

    /**
     * @return mixed|null
     */
    public function getParameters(){
        return $this->container['paramaters']??null;
    }

    /**
     * @return mixed|null
     */
    public function getSearch(){
        return $this->container['search']??null;
    }

    /**
     * @return mixed|null
     */
    public function getSort(){
        return $this->container['sort']??null;
    }

    /**
     * @return mixed|null
     */
    public function getHaving(){
        return $this->container['having'][0]??null;
    }

    /**
     * @return mixed|null
     */
    public function getLimit(){
        return $this->container['limit']??null;
    }

项目当中,剩下的两个类,那就是BaseObject 和 ParseException,无需讲解。接下来,就是单元测试了。

为什么要单元测试?假如你写的组件,那是交给天下人用的。如果未经测试,谁愿意做你的小白鼠?使用单元测试,我可以用composer下载之后,可以通过PhpUnit命令行验证一下。为什么要验证?因为,php版本,关联组件版本可能不一样。就象一些公司,说明,有新版本了,于是,尽快升级,但是,一升级,马上就崩了。这种情况相当常见,毕境,一旦有关联组件,肯定就有兼容问题,比如,thinkphp5.0就不兼容现在的redis,redis把函数名改了,del改成delete了。这还是小事。像thinkphp5.0与thinkphp5.1直接未在composer中设置版本冲突,所以,你在用5.0时,一不小心升级了,于是崩了。再如,thinkphp5系列均不带PhpUnit,所以,当我安装了组件之后,我无法验证可不可运行。只有在开发了,代码出来以后,才开始报错,这就浪费了大量的时间。所以,一般我composer安装后,喜欢用PhpUnit验证一下之后再开发,如果不行,则换版本,或换组件。

由此来看,你把你的想法实现了,交给大家来用,这是开源精神,值得称赞,但是,实际上,github中大量的开源并不是按此规范做的。所以,那些开源基本无人问津。

那么,该怎么测呢?首先我们要安装PhpUnit 和 orchestra/testbench,为什么要安装orchestra/testbench呢?这是为了能和travisCI这个免费的持续集成工具集成方便。另外,写代码也会方便很多。

关于这两个组件的使用,不多讲了,大家还是直接看文档。基本步骤是,配置phpuint.xml,用以告诉它,怎么测。然后,继承一个TestCase,废话少说,上代码:

declare(strict_types=1);
/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\Tests;

use Orchestra\Testbench\TestCase as Orchestra;

/**
 * Class TestCase
 *
 * @package ByteFerry\tests
 */
class TestCase extends Orchestra
{
    /**
     * The base URL to use while testing the application.
     *
     * @var string
     */
    protected $baseUrl = 'http://localhost';

    public function setUp(): void
    {
//        touch('./tests.sqlite');

        parent::setUp();
    }

    public function tearDown(): void
    {
//        unlink('./tests.sqlite');
    }

    /**
     * Define environment setup.
     *
     * @param \Illuminate\Foundation\Application $app
     *
     * @return void
     */
    protected function getEnvironmentSetUp($app)
    {
        // Setup default database to use sqlite :memory:
        $app['config']->set('database.default', 'testbench');
        $app['config']->set('database.connections.testbench', [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ]);
    }
}

这个类的代码基本都是默认的。接下来,要写相关测试了。 首先,我们要测RQL的所有关键字。

/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\Tests\Unit;

use ByteFerry\Tests\TestCase;
use  ByteFerry\RqlParser\Parser;
/**
 * Class KeyTest
 *
 * @package ByteFerry\tests\Unit
 */
final class KeyTest extends TestCase
{
    protected $test_keys = [
        'aggr' =>['aggr(id,sum(amount))','{"columns":["id","sum(amount)"],"columns_operator":"aggr","group_by":["id"]}'],
        'aggregate' =>['aggregate(id,sum(amount))','{"columns":["id","sum(amount)"],"columns_operator":"aggregate","group_by":["id"]}'],
        'and' =>['and(gt(age,20),lt(id,300),is(name,null()))','"( age > 20 )and( id < 300 )and( name is null )"'],
        'arr' =>['in(id,(1,34,2,4))','" id in  (1, 34, 2, 4)  "'],
        'avg' =>['aggr(id,avg(amount))', '{"columns":["id","avg(amount)"],"columns_operator":"aggr","group_by":["id"]}'],
        'between' =>['between(age,18,55)','" age BETWEEN 18 and 55 "'],
        'cols' =>['cols(id,age)','{"columns":["id","age"],"columns_operator":"cols","group_by":[]}'],
        'columns' =>['columns(id,age,name)','{"columns":["id","age","name"],"columns_operator":"columns","group_by":[]}'],
        'data' =>['data(id:17,score:33)','{"data":{"id":"17","score":"33"}}'],
        'empty' => ['is(name,empty())','" name is \"\" "'],
        'eq' =>['eq(age,17)','" age = 17 "'],
        'except' =>['except(password)','{"columns":["password"],"columns_operator":"except","group_by":[]}'],
        'false' => ['filter(false())','{"filter":[0],"paramaters":[]}'],
        'filter' =>['filter(eq(age,11))','{"filter":[" age = 11 "],"paramaters":{"age":"11"}}'],
        'ge' =>['ge(age,35)','" age >= 35 "'],
        'gt' =>['gt(age,15)','" age > 15 "'],
        'having' =>['having(ge(sum(amount),100))','{"having":[" sum(amount) >= 100 "],"paramaters":{"sum(amount)":"100"}}'],
        'in' =>['in(id,(1,3,5))','" id in  (1, 3, 5)  "'],
        'is' => ['is(name,null())','" name is null "'],
        'le' =>['le(age,3)','" age <= 3 "'],
        'like' =>['like(name,"ad%")','" name like \"ad%\" "'],
        'limit' =>['limit(1,20)','{"limit":["1","20"]}'],
        'lt' =>['lt(age,15)','" age < 15 "'],
        'max' =>['aggr(id,max(amount))','{"columns":["id","max(amount)"],"columns_operator":"aggr","group_by":["id"]}'],
        'mean' =>['aggr(id,mean(amount))','{"columns":["id","avg(amount)"],"columns_operator":"aggr","group_by":["id"]}'],
        'min' =>['aggr(id,min(amount))','{"columns":["id","min(amount)"],"columns_operator":"aggr","group_by":["id"]}'],
        'ne' =>['ne(age,15)','" age <> 15 "'],
        'nin' =>['nin(age,(15,25,35))','" age not in  (15, 25, 35)  "'],
        'not' =>['is(name,not(null()))','" name is not null "'],
        'null' =>['is(name,not(null()))','" name is not null "'],
        'only' =>['only(id,name,age,email)','{"columns":["id","name","age","email"],"columns_operator":"only","group_by":[]}'],
        'or' =>['or(eq(age,18),eq(gender,1))','"( age = 18 )or( gender = 1 )"'],
        'out' =>['out(id,(1,3,5))','" id not in  (1, 3, 5)  "'],
        'search' =>['search("jh%")','{"search":"jh%"}'],
        'select' =>['select(id,name,age)','{"columns":["id","name","age"],"columns_operator":"select","group_by":[]}'],
        'sort' =>['sort(+age,-name)','{"sort":[["age","ASC"],["name","DESC"]]}'],
        'sum' =>['aggr(id,sum(amount))','{"columns":["id","sum(amount)"],"columns_operator":"aggr","group_by":["id"]}'],
        'true' => ['eq(deleted,true())','" deleted = 1 "'],
        'values' =>['values(name)','{"columns":["name"],"columns_operator":"values","group_by":[]}'],
    ];

    /** @test */
    public function testKeys(){
        foreach($this->test_keys as $key => $item){
            [$test_str,$compare] = $item;
            $result = Parser::parse($test_str,true);
            $this->assertEquals($compare,json_encode($result[0]->toArray()));
        }
    }
}

这里,我们使用了一个偷懒的办法,那就是,我们把编译的结果用json_encode转换为字符串,与我们相要的字符串比对,如果一致,则OK。(这样做,是让代码行数变少)

第二步,我们要测所有的完整的查询

declare(strict_types=1);
/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\Tests\Unit;

use ByteFerry\Tests\TestCase;

use ByteFerry\RqlParser\Parser;

/**
 * Class SimpleQueryTest
 *
 * @package ByteFerry\tests\Unit
 */
final class SimpleQueryTest extends TestCase
{
    public $query_keys = [
        'all' =>['all(user,only(id,age))', '{"resource":"user","columns":["id","age"],"columns_operator":"only","group_by":[],"operator":"all","query_type":"Q_READ"}'],
        'any' =>['any(user,cols(id,name,age))','{"resource":"user","columns":["id","name","age"],"columns_operator":"cols","group_by":[],"operator":"any","query_type":"Q_READ"}'],
        'count' =>['count(user,cols(id))','{"resource":"user","columns":["id"],"columns_operator":"cols","group_by":[],"operator":"count","query_type":"Q_READ"}'],
        'create' =>['create(user,data(name:jhon,age:17))','{"resource":"user","data":{"name":"jhon","age":"17"},"operator":"create","query_type":"Q_WRITE"}'],
        'decrement' =>['decrement(Article,cols(read_count))','{"resource":"Article","columns":["read_count"],"columns_operator":"cols","group_by":[],"operator":"decrement","query_type":"Q_WRITE"}'],
        'delete' =>['delete(User,filter(eq(id,23)))','{"resource":"User","filter":[" id = 23 "],"paramaters":{"id":"23"},"operator":"delete","query_type":"Q_WRITE"}'],
        'distinct' =>['distinct(User_score,filter(gt(score,95)))','{"columns":["User_score",{"filter":[" score > 95 "],"paramaters":{"score":"95"}}],"columns_operator":"distinct","group_by":[]}'],
        'exists' =>['exists(user,filter(eq(mobile,1111)))','{"resource":"user","filter":[" mobile = 1111 "],"paramaters":{"mobile":"1111"},"operator":"exists","query_type":"Q_READ"}'],
        'first' =>['first(user,filter(gt(age,10)))','{"resource":"user","filter":[" age > 10 "],"paramaters":{"age":"10"},"operator":"first","query_type":"Q_READ"}'],
        'increment' =>['increment(Article,cols(read_count))','{"resource":"Article","columns":["read_count"],"columns_operator":"cols","group_by":[],"operator":"increment","query_type":"Q_WRITE"}'],
        'minus' =>['minus(Article,cols(read_count))','{"resource":"Article","columns":["read_count"],"columns_operator":"cols","group_by":[],"operator":"decrement","query_type":null}'],
        'one' =>['one(user,filter(eq(id,3)),search(ada))','{"resource":"user","filter":[" id = 3 "],"paramaters":{"id":"3"},"search":"ada%","operator":"one","query_type":"Q_READ"}'],
        'plus' =>['plus(Article,cols(read_count))','{"resource":"Article","columns":["read_count"],"columns_operator":"cols","group_by":[],"operator":"increment","query_type":null}'],
        'update' =>['update(user,data(age:18),filter(eq(id,3)))','{"resource":"user","data":{"age":"18"},"filter":[" id = 3 "],"paramaters":{"id":"3"},"operator":"update","query_type":"Q_WRITE"}']
    ];

    /** @test */
    public function testQuery(){
        foreach($this->query_keys as $key => $item){
            [$test_str,$compare] = $item;
            $result = Parser::parse($test_str);
            $this->assertEquals($compare,json_encode($result[0]->toArray()));
        }
    }

}

第三步,我们要测QueryInterface这个接口类

declare(strict_types=1);
/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\Tests\Unit;

use ByteFerry\Tests\TestCase;
use  ByteFerry\RqlParser\Parser;
use  ByteFerry\RqlParser\Fragment;

/**
 * Class QueryInterfaceTest
 *
 * @package ByteFerry\Tests\Unit
 */
final class QueryInterfaceTest extends TestCase
{
    /** @test */
    public function testQueryInterface(){
        $rql_str= 'all(User,aggr(id,name,age,gender,address,avg(age)),filter(is(created_at, null()), search(Jhon),sort(-id,+age),having(gt(sum(amount),0)),limit(0,20)))'; //,    //,

        $result = Parser::parse($rql_str);
        /** @var \ByteFerry\RqlParser\Query $query */
        $query = $result[0];
        $this->assertEquals($query->getResourceName(),'User');
        $this->assertEquals($query->getColumns(),[0 => 'id',
            1 => 'name',
            2 => 'age',
            3 => 'gender',
            4 => 'address',
            5 => 'avg(age)']);
        $this->assertEquals($query->getColumnsOperator(),'aggr');
        $this->assertEquals($query->getGroupBy(),[
            0 => 'id',
            1 => 'name',
            2 => 'age',
            3 => 'gender',
            4 => 'address']);
        $this->assertEquals($query->getFilter(),' created_at is null ');
        $this->assertEquals($query->getParameters(),[
            'created_at' => NULL,
            'sum(amount)' => '0'
        ]);

        $this->assertEquals($query->getSearch(),'Jhon%');
        $this->assertEquals($query->getSort(),[
            [   0 => 'id',
                1 => 'DESC'
                ],
            [
                0 => 'age',
                1 => 'ASC',
            ]
        ]);
        $this->assertEquals($query->getHaving(),' sum(amount) > 0 ');
        $this->assertEquals($query->getLimit(),['0','20']);
        $this->assertEquals($query->getOperator(),'all');
        $this->assertEquals($query->getQueryType(),'Q_READ');
        $this->assertEquals($query->query_type,'Q_READ');
        $this->assertEquals($query->operator,'all');
        $this->assertTrue(isset($query->operator));
        $this->assertEquals($query->name,null);
    }

    /** @test */
    public function testFragment(){
        $rql_str= 'aggregate(id,sum(amount))'; //,    //,

        $result = Parser::parse($rql_str,true);
        /** @var \ByteFerry\RqlParser\Fragment $query */
        $query = $result[0];
        $this->assertTrue($query instanceof Fragment);
        $this->assertEquals($query->columns,["id","sum(amount)"]);
        $this->assertEquals($query->group_by,["id"]);
        $this->assertEquals($query->columns_operator,'aggregate');
        $this->assertFalse(isset($query->operator));
        $this->assertEquals($query->operator,null);
    }
}

最后,我们还要进行异常测试。为什么呢?这是因为,出错时,你要保证抛出异常给程序员看到。同时,也只有经过异常测试,才能保证代码测试的覆盖率。

declare(strict_types=1);
/*
 * This file is part of the ByteFerry/Rql-Parser package.
 *
 * (c) BardoQi <67158925@qq.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace ByteFerry\Tests\Unit;

use ByteFerry\RqlParser\Fragment;
use ByteFerry\Tests\TestCase;
use  ByteFerry\RqlParser\Parser;
use  ByteFerry\RqlParser\Exceptions\ParseException;
/**
 * Class ExceptionTest
 *
 * @package ByteFerry\Tests\Unit
 */
final class ExceptionTest extends TestCase
{
    /** @test */
    public function testExceptions(){
        $rql_str= 'aggregate(id,,sum(amount))'; //,    //,
        try{
        $result = Parser::parse($rql_str,true);
        }catch(\Exception $e){
            $this->assertTrue($e instanceof ParseException);
        }

        $rql_str= 'aggregate(id,sum(amount)'; //,    //,
        try{
            $result = Parser::parse($rql_str,true);
        }catch(\Exception $e){
            $this->assertTrue($e instanceof ParseException);
        }

    }
}

最后,我们要集成到TraviCI , 并且,用CodeCov检查代码测试覆盖率。 如何配,这里,我们不是单独运行,因为,本组件是无任何依赖项的,我们选择集成到Laravel,为什么不选择thinkPHP?原因相当简单,它那里连phpUnit都没有,最后出错,用户认为我错了,岂不坏了我的名声?

其实, 这些东西简单的,官方网站都有详细的文档,并且是免费的。 TraviCI配置tavis.yml如下:(是基于yml的配置)

language: php

php:
  - 7.2

env:
  - LARAVEL_VERSION=5.8.*
  - LARAVEL_VERSION=6.*
  - LARAVEL_VERSION=7.*
  - LARAVEL_VERSION=8.*

before_script:
  - travis_retry composer self-update
  - travis_retry composer install --prefer-source --no-interaction
  - if [ "$LARAVEL_VERSION" != "" ]; then composer require --dev "laravel/laravel:${LARAVEL_VERSION}" --no-update; fi;
  - composer dump-autoload

script:
  - ./vendor/bin/phpunit --coverage-clover coverage.xml

after_success:
  - bash <(curl -s https://codecov.io/bash)

notifications:
email:
  recipients:
    - bardoqi@gmail.com
  on_success: always
  on_failure: always

测试做完了,集成完成,你可以将相关链接图标放到你的readme.md中。如下

image.png

(说明,上面是截图,这里无法正常显示动态的SHELD,只能上截图了)

这些链接告诉用户,你的代码是经过测试的,这其中,

第一个是 TravisCI 编译通过的标识

第二个是SytleCI(代码格式规范)通过的标志,

第三个,最重要的,就是测试代码覆盖率,总共覆盖了多少。我们可是99%

后面三个则是packagist.org版本与下载量,以及你的授权。这些主要是让人一眼可以看出是否可以用。

到此,我们的PHP代码编译器开发算是完成了。最后,别忘了,能够提供详细的文档,我们提供了中英文两个版本的readme,同时还提供了在线的RQL文档。因为,我们修改了一些RQL的关键字。此外,大家可以看出,此代码相当易用,因为,就是一个静态函数。为什么要这么做?越简单,大家用得越爽呀。另一方面,毕境是每个版本,目前不清楚客户会有什么配置项之类的。当然从简。

大家可以到

github.com/byteferry/r…

国内链接: gitee.com/bulo/rql-pa…

下载完整的原码。如果你觉得有用,别忘了star,既是自己收藏,也是一份鼓励。

那么,这个组件的效率如何?我们在内存仅8G, i7CPU的笔记本环境,测试下来,最长的RQL查询的编译一般在2毫秒左右。看起来,这里有10多个文件,但是,对一个懂得设计模式的人来说,每一个文件最多300行,那代码阅读是相当开心的。换个初学者,一样可以一个文件,3000行一个函数,也能完成,但未来就惨了。

所以,一些人认为,代码越多,程序越慢,实际是不对的。代码快与慢,关键是看数据结构与程序结构,不是代码多与少的问题。比如,ElasticSearch代码远比一般全文搜索的代码多得多了,为什么那么快?

同时,我们也可以看到,我们代码中,没有一个函数超过30行的。

那么,有人要问,你这是不是重造轮子?实际不是,因为,第一,目前任何一个PHP的RQL Parser均没有理想的程序结构。第二,更没有支持写查询的。第三,有些效率也不是很高。

rql-parser 是我们的第一步计划,我们是根据组件化的原理,下一步要写ApiBirdge,用来真正增强RestAPi的调用。所以,这个组件中的RQL与现在RQL标准的草案还是有些区别的。

自然,作者水平有限,也希望高手一些参与。RQL作为一项新技术,我相信未来肯定会被广泛运用到基础应用架构以及ORM等领域。作为个人,做开源的力量是有限的,所以,也希望感兴趣的企业共同参与。(全文完)