laravel配置文件的加载过程及门面facade的初始化过程和服务提供者的初始化
之前也写过一些文章,也是在老司机的分析下,学习过来的,现在尝试自己看一看laravel的一些基础配置机器引导过程也没有那么吃力了。目前看的是laravel5.8的版本。
从index.php开始,在加载完composer的配置文件autoload.php文件以后,实例化了$app。
1 $app = require_once __DIR__.'/../bootstrap/app.php';、 2 3 4 //让我们进入这个文件看一看,内容如下 5 //相关注释已经去掉 6 7 $app = new Illuminate\Foundation\Application( 8 $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) 9 ); 10 11 $app->singleton( 12 Illuminate\Contracts\Http\Kernel::class, 13 App\Http\Kernel::class 14 ); 15 16 $app->singleton( 17 Illuminate\Contracts\Console\Kernel::class, 18 App\Console\Kernel::class 19 ); 20 21 $app->singleton( 22 Illuminate\Contracts\Debug\ExceptionHandler::class, 23 App\Exceptions\Handler::class 24 ); 25 26 return $app;
这里注册了三个单例对象,但是还没有实例化,等到需要使用的时候在进行实例化,由于 Illuminate\Foundation\Application 这个类的对象本身就继承了 Illuminate\Container; 这个容器类,所以它也拥有容器一样的特性。
分析上述代码,注册单件就不说了,实际上是为后面需要这个对象而做准备,只是需要稍微注意下第一个单件
1 //Illuminate\Contracts\Http\Kernel::class是一个接口,如果要make它则实际上make(实例化)的是它的实现类App\Http\Kernel::class 2 $app->singleton( 3 Illuminate\Contracts\Http\Kernel::class, 4 App\Http\Kernel::class 5 );
好的进入 Illuminate\Foundation\Application 这个类的构造方法中,代码如下:
1 public function __construct($basePath = null) 2 { 3 if ($basePath) { 4 $this->setBasePath($basePath); 5 } 6 7 $this->registerBaseBindings(); 8 $this->registerBaseServiceProviders(); 9 $this->registerCoreContainerAliases(); 10 }
先指定根路径,然后注册基础绑定和基础的服务提供者,之后在注册容器中对象的一些别名方便引用。这里就直接看第三个方法吧,前两个方法暂时还没研究。
1 public function registerCoreContainerAliases() 2 { 3 foreach ([ 4 'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class], 5 'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class], 6 'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class], 7 'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class], 8 'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class], 9 'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class], 10 'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class], 11 'cookie' => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class], 12 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], 13 'db' => [\Illuminate\Database\DatabaseManager::class], 14 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], 15 'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], 16 'files' => [\Illuminate\Filesystem\Filesystem::class], 17 'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], 18 'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class], 19 'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class], 20 'hash' => [\Illuminate\Hashing\HashManager::class], 21 'hash.driver' => [\Illuminate\Contracts\Hashing\Hasher::class], 22 'translator' => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class], 23 'log' => [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class], 24 'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class], 25 'auth.password' => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class], 26 'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class], 27 'queue' => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class], 28 'queue.connection' => [\Illuminate\Contracts\Queue\Queue::class], 29 'queue.failer' => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class], 30 'redirect' => [\Illuminate\Routing\Redirector::class], 31 'redis' => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class], 32 'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class], 33 'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class], 34 'session' => [\Illuminate\Session\SessionManager::class], 35 'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class], 36 'url' => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class], 37 'validator' => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class], 38 'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class], 39 ] as $key => $aliases) { 40 foreach ($aliases as $alias) { 41 $this->alias($key, $alias); 42 } 43 } 44 }
主要是一些别名对应的对象,之后调用 $this->alias($key, $alias); 依次将这些别名添几件啊加到$this(也就是Illuminate\Foundation\Application类)的aliase属性中,看一下alias方法,做了双向关联。我这就不贴代码了。
一层层返回,Illuminate\Foundation\Application的对象$app实例化完成,回到app.php,之后执行了一段代码 $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 之前提到需要注意的那个单件就是它,实例化这个接口,实际上需要实例化的是 App\Http\Kernel::class 这个类,点进这个类的构造方法,由于这个类没有构造方法,于是将会调用这个类父类的构造方法即 Illuminate\Foundation\Http\Kernel; 这个类的构造方法,这里传入一个 \Illuminate\Contracts\Foundation\Application 对象和 \Illuminate\Routing\Router 对象,怎么实例化的实际上就是容器完成的工作了,不是本次讨论的重点,进入构造方法,主要是对对象中的一些属性进行赋值操作,其它的一些操作也不是本次讨论的重点。本次所关注的就是赋值这个操作, Illuminate\Foundation\Http\Kernel; 已经有了 \Illuminate\Contracts\Foundation\Application 对象,这是我们所关注的,回到index.php,看这段代码:
1 $response = $kernel->handle( 2 $request = Illuminate\Http\Request::capture() 3 );
刚才实例化的类调用了handle方法,并传入了一个对象,这个对象也不是本次的重点,进入这个方法看一看:
1 public function handle($request) 2 { 3 try { 4 $request->enableHttpMethodParameterOverride(); 5 6 $response = $this->sendRequestThroughRouter($request); 7 } catch (Exception $e) { 8 $this->reportException($e); 9 10 $response = $this->renderException($request, $e); 11 } catch (Throwable $e) { 12 $this->reportException($e = new FatalThrowableError($e)); 13 14 $response = $this->renderException($request, $e); 15 } 16 17 $this->app['events']->dispatch( 18 new Events\RequestHandled($request, $response) 19 ); 20 21 return $response; 22 }
第一个方法看一下比较简单,也不是这次的重点,进入第二个方法 $response = $this->sendRequestThroughRouter($request); 代码如下:
protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); }
看到有个facade类调用的静态方法,可以看一下。
1 public static function clearResolvedInstance($name) 2 { 3 unset(static::$resolvedInstance[$name]); 4 }
清除了静态属性中的某个值,这个方法实际上我也不确定是干什么的,但不会影响本次的探讨,回到上个方法,接着进入了一个十分重要的方法 $this->bootstrap(); ,我们进去这个方法看看。
1 public function bootstrap() 2 { 3 if (! $this->app->hasBeenBootstrapped()) { 4 $this->app->bootstrapWith($this->bootstrappers()); 5 } 6 }
还记得刚刚强调的$app对象吗,就在这里起作用了,先判断是否引导完成,没有则开始引导,我们进入这个引导的方法 $this->app->bootstrapWith($this->bootstrappers()); ,带了一些重要的参数过去。
1 protected function bootstrappers() 2 { 3 return $this->bootstrappers; 4 }
参数如下,主要是一些类:
1 protected $bootstrappers = [ 2 \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, 3 \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, 4 \Illuminate\Foundation\Bootstrap\HandleExceptions::class, 5 \Illuminate\Foundation\Bootstrap\RegisterFacades::class, 6 \Illuminate\Foundation\Bootstrap\RegisterProviders::class, 7 \Illuminate\Foundation\Bootstrap\BootProviders::class, 8 ];
然后进入我们的$app类中的 bootstrapWith 方法。
1 public function bootstrapWith(array $bootstrappers) 2 { 3 $this->hasBeenBootstrapped = true; 4 5 foreach ($bootstrappers as $bootstrapper) { 6 $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]); 7 8 $this->make($bootstrapper)->bootstrap($this); 9 10 $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]); 11 } 12 }
再循环中,实例化了参数中每一个类,并调用这个类的bootstrap方法,于是我们在这个参数中看到了Fade: \Illuminate\Foundation\Bootstrap\RegisterFacades::class, ,进入这个类的bootstrap方法:
1 public function bootstrap(Application $app) 2 { 3 Facade::clearResolvedInstances(); 4 5 Facade::setFacadeApplication($app); 6 7 AliasLoader::getInstance(array_merge( 8 $app->make('config')->get('app.aliases', []), 9 $app->make(PackageManifest::class)->aliases() 10 ))->register(); 11 }
初始化了一些东西,同时把$app对象赋值给这个类作为静态属性,之前有说过,其它的facade调用的时候 都会创建一个对象,就是从这个属性中以数组的形式取出对象,细心可以发现\Illuminate\Contracts\Foundation\Application父类Illuminate\Container实际上已经实现了ArrayAccess接口,获取的时候实际上是调用这个方法。
1 public function offsetGet($key) 2 { 3 return $this->make($key); 4 }
直接make的方式获取方法,之后的静态调用实际上是调用了Facade的魔术方法。
1 public static function __callStatic($method, $args) 2 { 3 $instance = static::getFacadeRoot(); 4 5 if (! $instance) { 6 throw new RuntimeException('A facade root has not been set.'); 7 } 8 9 return $instance->$method(...$args); 10 }
由此说明$app对于Facade的重要性,现在我们便知道它实在什么时候被初始化了。
接着是配置加载,本来刚开始还没注意的,主要还是想看服务提供者的加载过程,看的过程中跳过了某些东西于是引起了我的思考,决定退回去看看到底是怎么回事,以下则是过程。
在引导完facade以后,进入下一个类, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, ,进入这个类的bootstrap方法:
1 public function bootstrap(Application $app) 2 { 3 $app->registerConfiguredProviders(); 4 }
实际上调用的还是 Illuminate\Contracts\Foundation\Application 类的registerConfiguredProviders()这个方法,进入这个方法。
1 public function registerConfiguredProviders() 2 { 3 $providers = Collection::make($this->config['app.providers']) 4 ->partition(function ($provider) { 5 return Str::startsWith($provider, 'Illuminate\\'); 6 }); 7 8 $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]); 9 10 (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) 11 ->load($providers->collapse()->toArray()); 12 }
这里使用了集合,集合还是不是很熟悉,但是在make方法中传入一个参数, $this->config['app.providers'] ,我查看了一下,发现这个类包括其父类都没有这个属性,但是我很好奇它是哪里来的,于是才有了标题中才有laravel中相关配置加载这一项,我打印了这个属性实际上它是 /Illuminate/Config/Repository.php 类的对象,但是它是何时被实例化的,又是何时被赋值给 $app->config 属性的,我并不知道,找了半天没有头绪,于是我不厚道的开启了XDEBUG 调试模式,在它的构造方法里打了断点,只要执行完构造方法,我就知道在哪里实例化了,于是我发现实在这个类中被实例化的 /Illuminate/Foundation/Bootstrap/LoadConfiguration.php ,方法如下:
1 public function bootstrap(Application $app) 2 { 3 $items = []; 4 5 // First we will see if we have a cache configuration file. If we do, we'll load 6 // the configuration items from that file so that it is very quick. Otherwise 7 // we will need to spin through every configuration file and load them all. 8 if (file_exists($cached = $app->getCachedConfigPath())) { 9 $items = require $cached; 10 11 $loadedFromCache = true; 12 } 13 14 // Next we will spin through all of the configuration files in the configuration 15 // directory and load each one into the repository. This will make all of the 16 // options available to the developer for use in various parts of this app. 17 $app->instance('config', $config = new Repository($items)); 18 19 if (! isset($loadedFromCache)) { 20 $this->loadConfigurationFiles($app, $config); 21 } 22 23 // Finally, we will set the application's environment based on the configuration 24 // values that were loaded. We will pass a callback which will be used to get 25 // the environment in a web context where an "--env" switch is not present. 26 $app->detectEnvironment(function () use ($config) { 27 return $config->get('app.env', 'production'); 28 }); 29 30 date_default_timezone_set($config->get('app.timezone', 'UTC')); 31 32 mb_internal_encoding('UTF-8'); 33 }
与此同时实例化完以后还把对象赋值给了 $app->config ,其实稍微细心一点就已经发现是怎么回事了,但是我还是没转过弯来,于是在用XDEBUG 再次跳出这个方法,看看到底是谁执行的,跳出后,果然,调用这个类的方法已经贴出过了,就是 bootsrrapWith() 方法,而 /Illuminate/Foundation/Bootstrap/LoadConfiguration 和 \Illuminate\Foundation\Bootstrap\RegisterFacades::class 一样其实都是 $bootstrappers 中的一个属性,只是刚才被忽略掉了。说到这里,我进入到 /Illuminate/Foundation/Bootstrap/LoadConfiguration 类的 bootstrap() 方法中看看配置是怎么加载进来的吧,代码已在上方贴出。
看了看引用属性的方式: $this->config['app.providers'] ,也是通过数组的方式引用,(刚刚有提到过,Facade使用的时候也是以数组形式获取对象),于是,可以看一下这个类的 offsetGet() 方法,其实是调用 $this->get($key); 进入 get() 方法
1 public function get($key, $default = null) 2 { 3 if (is_array($key)) { 4 return $this->getMany($key); 5 } 6 7 return Arr::get($this->items, $key, $default); 8 }
看到这里也就是说,这个类的核心属性就是item,回到刚才实例化的过程,进入了一个if语句,查了一下,item是不存在的,所以不用管,接着实例化对象之后,下面一段代码则是关键。
1 if (! isset($loadedFromCache)) { 2 $this->loadConfigurationFiles($app, $config); 3 }
进入 loadConfigurationFiles($app, $config) 这个方法。
1 protected function loadConfigurationFiles(Application $app, RepositoryContract $repository) 2 { 3 $files = $this->getConfigurationFiles($app); 4 5 if (! isset($files['app'])) { 6 throw new Exception('Unable to load the "app" configuration file.'); 7 } 8 9 foreach ($files as $key => $path) { 10 $repository->set($key, require $path); 11 } 12 }
这里又调用了 $files = $this->getConfigurationFiles($app); ,进入这个方法在看一下。
1 protected function getConfigurationFiles(Application $app) 2 { 3 $files = []; 4 5 $configPath = realpath($app->configPath()); 6 7 foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) { 8 $directory = $this->getNestedDirectory($file, $configPath); 9 10 $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath(); 11 } 12 13 ksort($files, SORT_NATURAL); 14 15 return $files; 16 }
getConfigurationFiles() 这个方法使用工具类遍历了根目录下中config目录的所有以.php结尾的文件,将文件赋值给 $files ,返回值如下:
1 array:13 [ 2 "app" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\app.php" 3 "auth" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\auth.php" 4 "broadcasting" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\broadcasting.php" 5 "cache" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\cache.php" 6 "database" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\database.php" 7 "filesystems" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\filesystems.php" 8 "hashing" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\hashing.php" 9 "logging" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\logging.php" 10 "mail" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\mail.php" 11 "queue" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\queue.php" 12 "services" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\services.php" 13 "session" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\session.php" 14 "view" => "D:\phpstudy_v8.0\laravel\laravel5.8\config\view.php" 15 ]
这个方法的过程不是本次讨论的重点,于是回退到调用这个方法的方法 loadConfigurationFiles() ,接着遍历了刚才的数组,然后给每个属性设置相应的值,我们可以看到配置文件下app.php中又providers这个数组,回到上一层 bootstrap(Application $app) 下面的方法我也没有看过,但是大体上,laravel 的大部分的配置基本上都加载完毕了。
解决了刚才的疑问,继续看服务提供者吧,回到 registerConfiguredProviders() , 方法如下:
1 public function registerConfiguredProviders() 2 { 3 $providers = Collection::make($this->config['app.providers']) 4 ->partition(function ($provider) { 5 return Str::startsWith($provider, 'Illuminate\\'); 6 }); 7 8 $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]); 9 10 (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) 11 ->load($providers->collapse()->toArray()); 12 }
这里对获取到的配置处理了一下,暂时还没分析怎么处理的,打算全分析完了再回去仔细看看是怎么处理的,还有获取配置的文件操作,最后一行实例化了一个类 /Illuminate/Foundation/ProviderRepository ,并调用了这个类的 load() 方法,看了一下控制器,都是些赋值操作,之后再看 load() 方法。
1 public function load(array $providers) 2 { 3 $manifest = $this->loadManifest(); 4 5 // First we will load the service manifest, which contains information on all 6 // service providers registered with the application and which services it 7 // provides. This is used to know which services are "deferred" loaders. 8 if ($this->shouldRecompile($manifest, $providers)) { 9 $manifest = $this->compileManifest($providers); 10 } 11 12 // Next, we will register events to load the providers for each of the events 13 // that it has requested. This allows the service provider to defer itself 14 // while still getting automatically loaded when a certain event occurs. 15 foreach ($manifest['when'] as $provider => $events) { 16 $this->registerLoadEvents($provider, $events); 17 } 18 19 // We will go ahead and register all of the eagerly loaded providers with the 20 // application so their services can be registered with the application as 21 // a provided service. Then we will set the deferred service list on it. 22 foreach ($manifest['eager'] as $provider) { 23 $this->app->register($provider); 24 } 25 26 $this->app->addDeferredServices($manifest['deferred']); 27 }
直奔重点,前面的也暂时不管了,因为有些服务提供者会有些特殊的操作或者配置,先看看普通的吧:
1 foreach ($manifest['eager'] as $provider) { 2 $this->app->register($provider); 3 }
这个 $manifest 实际上就是构造方法传进来的providers数组,之后调用 $this->app->register($provider); 来注册,进入这个方法:
1 public function register($provider, $force = false) 2 { 3 if (($registered = $this->getProvider($provider)) && ! $force) { 4 return $registered; 5 } 6 7 if (is_string($provider)) { 8 $provider = $this->resolveProvider($provider); 9 } 10 11 $provider->register(); 12 13 if (property_exists($provider, 'bindings')) { 14 foreach ($provider->bindings as $key => $value) { 15 $this->bind($key, $value); 16 } 17 } 18 19 if (property_exists($provider, 'singletons')) { 20 foreach ($provider->singletons as $key => $value) { 21 $this->singleton($key, $value); 22 } 23 } 24 25 $this->markAsRegistered($provider); 26 27 if ($this->isBooted()) { 28 $this->bootProvider($provider); 29 } 30 31 return $provider; 32 }
这里调用了 $this->resolveProvider($provider); ,这个方法就是对这个provider进行实例化,之后调用服务提供者的register方法,然后对这个服务提供者对象进行一些初始化操作,这些操作官方文档就有介绍。之后有个判断, $this->isBooted() ,如果框架引导完成,就会调用这个方法,此时是还没有将所有的privider加载完成的,所以引导还没有完成,因此不会调用里面的 $this->bootProvider($provider); 方法,这个方法其实就是调用服务提供者对象的 boot() 方法,也就是说, boot() 方法会在所有服务提供者被实例化了才会调用(这里我暂时还没有考虑延时的服务提供者),正好对应了文档,可以在boot方法中添加指定类型的参数,因为所有的服务提供者都实例化了,相关类的绑定也已经加入到容器的属性中了,所以可以使用依赖注入相关属性。而服务提供者的 register() 不能添加相关类参数,并不是所有的绑定都已经加入到容器中了,所以在这个方法里面使用依赖注入可能会出现问题,这一点文档中也说了,所有服务提供者实例化完成后,接着就是调用boot方法了,回到 Illuminate\Foundation\Http\Kernel 中, $bootstrappers 还有最后一行, \Illuminate\Foundation\Bootstrap\BootProviders::class 进入到这个类的booststrap方法中,这个类就是负责调用所有服务容器的 boot() 方法:
1 public function bootstrap(Application $app) 2 { 3 $app->boot(); 4 }
它调用了$app的boot方法,进入到这个方法:
1 public function boot() 2 { 3 if ($this->isBooted()) { 4 return; 5 } 6 7 $this->fireAppCallbacks($this->bootingCallbacks); 8 9 array_walk($this->serviceProviders, function ($p) { 10 $this->bootProvider($p); 11 }); 12 13 $this->booted = true; 14 15 $this->fireAppCallbacks($this->bootedCallbacks); 16 }
对于每个provider,它又调用了 $this->bootProvider($p); ,进入这个方法
1 protected function bootProvider(ServiceProvider $provider) 2 { 3 if (method_exists($provider, 'boot')) { 4 return $this->call([$provider, 'boot']); 5 } 6 }
于是,我们看见,服务提供者的boot方法被调用了,至此,于是分析也就到此结束了。
浙公网安备 33010602011771号