Laravel 学习笔记之 Bootstrap 源码解析
说明:Laravel在把Request通过管道Pipeline送入中间件Middleware和路由Router之前,还做了程序的启动Bootstrap工作,本文主要学习相关源码,看看Laravel启动程序做了哪些具体工作,并将个人的研究心得分享出来,希望对别人有所帮助。Laravel在入口index.php时先加载Composer加载器:Laravel学习笔记之Composer自动加载,然后进行Application的实例化:Laravel学习笔记之IoC Container实例化源码解析,得到实例化后的Application对象再从容器中解析出Kernel服务,然后进行Request实例化(Request实例化下次再聊),然后进行Bootstrap操作启动程序,再通过Pipeline送到Middleware:Laravel学习笔记之Middleware源码解析,然后经过路由映射找到对该请求的操作action(以后再聊),生成Response对象经过Kernel的send()发送给Client。本文主要聊下程序的启动操作,主要做了哪些准备工作。
开发环境:Laravel5.3 + PHP7 + OS X 10.11
在Laravel学习笔记之Middleware源码解析聊过,Kernel中的sendRequestThroughRouter()处理Request,并把Request交给Pipeline送到Middleware和Router中,看源码:
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
/* 依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数,做了几件准备事情:
1. 环境检测 DetectEnvironment
2. 配置加载 LoadConfiguration
3. 日志配置 ConfigureLogging
4. 异常处理 HandleException
5. 注册Facades RegisterFacades
6. 注册Providers RegisterProviders
7. 启动Providers BootProviders
protected $bootstrappers = [
'Illuminate\Foundation\Bootstrap\DetectEnvironment',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
'Illuminate\Foundation\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\RegisterProviders',
'Illuminate\Foundation\Bootstrap\BootProviders',
];*/
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
在Request被Pipeline送到Middleware前还有一步操作bootstrap()操作,这步操作就是启动程序,看下\Illuminate\Foundation\Http\Kernel中的bootstrap()源码:
protected $hasBeenBootstrapped = false;
...
/**
* Bootstrap the application for HTTP requests.
*
* @return void
*/
public function bootstrap()
{
// 检查程序是否已经启动
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
public function hasBeenBootstrapped()
{
return $this->hasBeenBootstrapped;
}
protected function bootstrappers()
{
return $this->bootstrappers;
}
protected $bootstrappers = [
'Illuminate\Foundation\Bootstrap\DetectEnvironment',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
'Illuminate\Foundation\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\RegisterProviders',
'Illuminate\Foundation\Bootstrap\BootProviders',
];
从以上源码可知道,程序将会依次bootstrapWith()数组$bootstrappers中各个bootstrapper,看下容器中的bootstrapWith()源码:
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}
首先触发'bootstrapping: '.$bootstrapper事件,告知将要启动该bootstrapper,然后从容器中make($bootstrapper)出该$bootstrapper,并执行该$bootstrapper中的bootstrap()方法,最后在触发事件:'bootstrapped: '.$bootstrapper,告知该$bootstrapper已经启动OK了。启动的bootstrappers就是数组$bootstrappers中的7个bootstrapper,看下程序做了哪些启动工作。
-
环境检测
查看Illuminate\Foundation\Bootstrap\DetectEnvironment中的bootstrap()源码:public function bootstrap(Application $app)
{
// 查看bootstrap/cache/config.php缓存文件是否存在
// php artisan config:cache来生成配置缓存文件,就是把config/下的所有文件放在一个缓存文件内,提高性能
// 这里假设没有缓存配置文件
if (! $app->configurationIsCached()) {
$this->checkForSpecificEnvironmentFile($app);try { $env = $_ENV; // 调试添加的,此时为空 // 这里把.env文件值取出存入$_ENV内 (new Dotenv($app->environmentPath(), $app->environmentFile()))->load(); // 这里$_ENV数组有值了 $env = $_ENV; } catch (InvalidPathException $e) { // } }
}
protected function checkForSpecificEnvironmentFile($app)
{
// 读取$_ENV全局变量中'APP_ENV'值,此时是空
if (! env('APP_ENV')) {
return;
}$file = $app->environmentFile().'.'.env('APP_ENV'); // .env.local if (file_exists($app->environmentPath().'/'.$file)) { $app->loadEnvironmentFrom($file); }
}
环境监测核心就是把.env文件内值存入到$_ENV全局变量中\Dotenv\Dotenv::load()函数实现了这个功能,具体不详述了。可以通过Xdebug调试查看:
图片描述 -
配置加载
配置加载就是读取config/文件夹下的所有配置值,然后存入\Illuminate\Config\Repository对象中,而环境检测是读取.env文件存入$_ENV全局变量中,加载环境配置主要是使用\Symfony\Component\Finder\Finder这个组件进行文件查找,看下LoadConfiguration::bootstrap()的源码:public function bootstrap(Application $app)
{
$items = [];
// 查看config有没有缓存文件,缓存文件是在bootstrap/cache/config.php
// 通过php artisan config:cache命令来生成缓存文件,把config/下的所有配置文件打包成一个文件,提高程序执行速度
// 这里假设没有缓存文件
if (file_exists($cached = $app->getCachedConfigPath())) {
$items = require $cached;$loadedFromCache = true; } // 绑定服务'config',服务是\Illuminate\Config\Repository对象 $app->instance('config', $config = new Repository($items)); if (! isset($loadedFromCache)) { // 加载config/*.php所有配置文件,把所有配置存入Repository对象中 $this->loadConfigurationFiles($app, $config); } // 检查'APP_ENV'环境设置,一般也就是'dev','stg','prd'三个环境,即'development', 'staging', 'production' $app->detectEnvironment(function () use ($config) { return $config->get('app.env', 'production'); }); // 设置时区,$config['app.timezone']就是调用Repository::get('app.timezone'),因为Repository实现了ArrayAccess Interface, // '.'语法读取是Arr::get()实现的,很好用的一个方法 date_default_timezone_set($config['app.timezone']); mb_internal_encoding('UTF-8');
}
加载配置文件,就是读取/config/*.php文件,看下源码:protected function loadConfigurationFiles(Application $app, RepositoryContract $repository)
{
foreach ($this->getConfigurationFiles($app) as $key => $path) {
// 存入到Repository对象中,以'key => value'存入到$items[]属性中
$repository->set($key, require $path);
}
}protected function getConfigurationFiles(Application $app)
{
$files = [];
// 就是'config/'这个路径
$configPath = realpath($app->configPath());
// Finder链式接口读取config/.php所有文件,获取所有文件名称,然后依次遍历
foreach (Finder::create()->files()->name('.php')->in($configPath) as $file) {
$nesting = $this->getConfigurationNesting($file, $configPath);$files[$nesting.basename($file->getRealPath(), '.php')] = $file->getRealPath(); } return $files;
}
可以通过Xdebug调试知道$files的返回值是这样的数组:$files = [
'app' => '/vagrant/config/app.php', //文件的绝对路径
'auth' => 'vagrant/config/auth.php',
'broadcasting' => '/vagrant/config/broadcasting.php',
'cache' => '/vagrant/config/cache.php',
'compile' => 'vagrant/config/compile.php',
'database' => '/vagrant/config/databse.php',
'filesystems' => '/vagrant/config/filesystems.php',
'mail' => '/vagrant/config/mail.php',
'queue' => '/vagrant/config/queue.php',
'services' => '/vagrant/config/services.php',
'session' => '/vagrant/config/session.php',
'view' => '/vagrant/config/view.php',
];
然后通过Application的detectEnvironment()方法把app.env的值即app.php中env的值取出来存入Application对象的$env属性中:public function detectEnvironment(Closure $callback)
{
$args = isset($_SERVER['argv']) ? $_SERVER['argv'] : null;return $this['env'] = (new EnvironmentDetector())->detect($callback, $args);
}
public function detect(Closure $callback, $consoleArgs = null)
{
if ($consoleArgs) {
return $this->detectConsoleEnvironment($callback, $consoleArgs);
}return $this->detectWebEnvironment($callback);
}
protected function detectWebEnvironment(Closure $callback)
{
return call_user_func($callback);
}
所以属性检查的时候就存到了$env属性的值了,开发代码中就可以App::environment()得到这个$env属性然后进行一些操作,可以看下environment()的源码,该方法有两个feature:如果不传入值则读取$env值;如果传入值则判断该值是否与$env一样。这里如果对Application没有$env成员属性定义有疑惑,是因为PHP可以后期添加属性,如:
class ClassField
{
}
$class_field = new ClassField();
$class_field->name = 'Laravel';
echo $class_field->name . PHP_EOL;
/* output:
Laravel
-
日志配置
Laravel主要利用Monolog日志库来做日志处理,\Illuminate\Log\Writer相当于Monolog Bridge,把Monolog库接入到Laravel中。看下ConfigureLogging::bootstrap()源码:public function bootstrap(Application $app)
{
// 注册'log'服务
$log = $this->registerLogger($app);// 检查是否已经注册了Monolog // 这里假设开始没有注册 if ($app->hasMonologConfigurator()) { call_user_func( $app->getMonologConfigurator(), $log->getMonolog() ); } else { // $this->configureHandlers($app, $log); }
}
protected function registerLogger(Application $app)
{
// 向容器中绑定'log'服务,即Writer对象
$app->instance('log', $log = new Writer(
new Monolog($app->environment()), $app['events'])
);return $log;
}
Laravel的Log模块中已经内置了几个类型的LogHandler:Single,Daily,Syslog,Errorlog.根据config/app.php文件中'log'的配置选择其中一个handler,看下configureHandlers()源码:
protected function configureHandlers(Application $app, Writer $log)
{
$method = 'configure'.ucfirst($app['config']['app.log']).'Handler';
$this->{$method}($app, $log);
}
configureHandlers()这方法也是一个技巧,找到方法名然后调用,这在Laravel中经常这么用,如Filesystem那一模块中有'create'.ucfirst(xxx).'Driver'这样的源码,是个不错的设计。这里看下configureDailyHandler()的源码,其余三个也类似:
protected function configureDailyHandler(Application $app, Writer $log)
{
// 解析'config'服务
$config = $app->make('config');
// 默认没有设置,就为null
$maxFiles = $config->get('app.log_max_files');
$log->useDailyFiles(
$app->storagePath().'/logs/laravel.log', // storage/log/laravel.log
is_null($maxFiles) ? 5 : $maxFiles, // 5
$config->get('app.log_level', 'debug')
);
}
// Writer.php
public function useDailyFiles($path, $days = 0, $level = 'debug')
{
$this->monolog->pushHandler(
$handler = new RotatingFileHandler($path, $days, $this->parseLevel($level))
);
$handler->setFormatter($this->getDefaultFormatter());
}
利用Mnolog的RotatingFileHandler()来往laravel.log里打印log值,当然在应用程序中经常\Log::info(),\Log::warning(),\Log::debug()来打印变量值,即Writer类中定义的的方法。Log的facade是\Illuminate\Support\Facades\Log:
class Log extends Facade
{
/**
- Get the registered name of the component.
-
@return string
*/
protected static function getFacadeAccessor()
{
return 'log';
}
}
而'log'服务在上文中bootstrap()源码第一步registerLogger()就注册了。当然,至于使用Facade来从容器中获取服务也聊过,也不复杂,看下\Illuminate\Support\Facades\Facade的resolveFacadeInstance()源码就知道了:protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } return static::$resolvedInstance[$name] = static::$app[$name]; // 实际上就是使用$app['log']来获取服务
}
- 异常处理
异常处理是十分重要的,Laravel中异常处理类\App\Exception\Handler中有一个方法report(),该方法可以用来向第三方服务(如Sentry)发送程序异常堆栈(以后在一起聊聊这个Sentry,效率神器),如Production Code线上环境报出个异常,可以很清楚整个堆栈,出错在哪一行:
- 异常处理
图片描述
OK,看下异常设置的启动源代码,HandleExceptions::bootstrap()的源码:
public function bootstrap(Application $app)
{
$this->app = $app;
error_reporting(-1);
// 出现错误,抛出throw new ErrorException
set_error_handler([$this, 'handleError']);
// 处理异常,使用report()方法来报告,可集成第三方服务Sentry来作为异常报告处理器ExceptionReportHandler
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
if (! $app->environment('testing')) {
ini_set('display_errors', 'Off');
}
}
这里重点看下handleException()的源码:
public function handleException($e)
{
if (! $e instanceof Exception) {
$e = new FatalThrowableError($e);
}
// (new App\Exceptions\Handler($container))->report($e)
$this->getExceptionHandler()->report($e);
if ($this->app->runningInConsole()) {
$this->renderForConsole($e);
} else {
$this->renderHttpResponse($e);
}
}
protected function getExceptionHandler()
{
// 解析出App\Exceptions\Handler对象
// 在boostrap/app.php中做过singleton()绑定
return $this->app->make('Illuminate\Contracts\Debug\ExceptionHandler');
}
protected function renderHttpResponse(Exception $e)
{
// 使用(new App\Exceptions\Handler($container))->render(Request $request, $e)
$this->getExceptionHandler()->render($this->app['request'], $e)->send();
}
从源码中知道,重点是使用App\Exceptions\Handler的report()方法报告异常情况,如向Sentry报告异常堆栈和其他有用信息;App\Exceptions\Handler的render()方法通过Request发送到浏览器。关于使用第三方服务Sentry来做异常报告以后详聊,我司每天都在用这样的效率神器,很好用,值得推荐下。
-
注册Facades
在路由文件中经常会出现Route::get()这样的写法,但实际上并没有Route类,Route只是\Illuminate\Support\Facades\Route::class外观类的别名,这样取个别名只是为了简化作用,使用的是PHP内置函数class_alias(string $class, string $alias)来给类设置别名。看下RegisterFacades::bootstrap()的源码:public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();Facade::setFacadeApplication($app); AliasLoader::getInstance($app->make('config')->get('app.aliases', []))->register();
}
// \Illuminate\Support\Facades\Facade
public static function clearResolvedInstances()
{
static::$resolvedInstance = [];
}// \Illuminate\Support\Facades\Facade
public static function setFacadeApplication($app)
{
static::$app = $app;
}
$app->make('config')->get('app.aliases', [])是从config/app.php中读取'aliases'的值,然后注册外观类的别名,注册的外观类有:'aliases' => [
'App' => Illuminate\Support\Facades\App::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class, 'Auth' => Illuminate\Support\Facades\Auth::class, 'Blade' => Illuminate\Support\Facades\Blade::class, 'Cache' => Illuminate\Support\Facades\Cache::class, 'Config' => Illuminate\Support\Facades\Config::class, 'Cookie' => Illuminate\Support\Facades\Cookie::class, 'Crypt' => Illuminate\Support\Facades\Crypt::class, 'DB' => Illuminate\Support\Facades\DB::class, 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Event' => Illuminate\Support\Facades\Event::class, 'File' => Illuminate\Support\Facades\File::class, 'Gate' => Illuminate\Support\Facades\Gate::class, 'Hash' => Illuminate\Support\Facades\Hash::class, 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, 'Mail' => Illuminate\Support\Facades\Mail::class, 'Notification' => Illuminate\Support\Facades\Notification::class, 'Password' => Illuminate\Support\Facades\Password::class, 'Queue' => Illuminate\Support\Facades\Queue::class, 'Redirect' => Illuminate\Support\Facades\Redirect::class, 'Redis' => Illuminate\Support\Facades\Redis::class, 'Request' => Illuminate\Support\Facades\Request::class, 'Response' => Illuminate\Support\Facades\Response::class, 'Route' => Illuminate\Support\Facades\Route::class, 'Schema' => Illuminate\Support\Facades\Schema::class, 'Session' => Illuminate\Support\Facades\Session::class, 'Storage' => Illuminate\Support\Facades\Storage::class, 'URL' => Illuminate\Support\Facades\URL::class, 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class,
],
从以上外观别名数组中知道Route是IlluminateSupportFacadesRoute::class的别名,所以Route::get()实际上就是IlluminateSupportFacadesRoute::get(),看下AliasLoader类的getInstance()和register()方法源码:public static function getInstance(array $aliases = [])
{
if (is_null(static::$instance)) {
// 这里$aliases就是上面传进来的$aliases[],即config/app.php中'aliases'值
return static::$instance = new static($aliases);
}$aliases = array_merge(static::$instance->getAliases(), $aliases); static::$instance->setAliases($aliases); return static::$instance;
}
public function register()
{
if (! $this->registered) {
$this->prependToLoaderStack();$this->registered = true; }
}
protected function prependToLoaderStack()
{
// 把AliasLoader::load()放入自动加载函数堆栈中,堆栈首的位置
spl_autoload_register([$this, 'load'], true, true);
}
而loader()函数的源码:public function load($alias)
{
if (isset($this->aliases[$alias])) {
// @link http://php.net/manual/en/function.class-al...
return class_alias($this->aliases[$alias], $alias);
}
}
就是通过class_alias()给外观类设置一个别名。所以Route::get()的调用过程就是,首先发现没有Route类,就去自动加载函数堆栈中通过AliasLoader::load()函数查找到Route是IlluminateSupportFacadesRoute的别名,那就调用IlluminateSupportFacadesRoute::get(),当然这里IlluminateSupportFacadesRoute没有get()静态方法,那就调用父类Facade的__callStatic()来找到名为router的服务,名为'router'的服务那就是早就注册到容器中的IlluminateRoutingRouter对象,所以最终就是调用IlluminateRoutingRouter::get()方法。这个过程主要使用了两个技术:一个是外观类的别名;一个是PHP的重载,可看这篇:Laravel学习笔记之PHP重载(overloading)。 -
注册Providers
外观注册是注册config/app.php中的$aliases[ ]得值,Providers注册就是注册$providers[ ]的值。看下RegisterProviders::bootstrap()的源码:public function bootstrap(Application $app)
{
$app->registerConfiguredProviders();
}// Application.php
public function registerConfiguredProviders()
{
// 查找bootstrap/cache/services.php有没有这个缓存文件
// services.php这个缓存文件存储的是service providers的数组值:
// return [
// 'providers' => [],
// 'eager' => [],
// 'deferred' => [],
// 'when' => []
// ];
$manifestPath = $this->getCachedServicesPath();// 通过load()方法加载config/app.php中'$providers[ ]'数组值 (new ProviderRepository($this, new Filesystem, $manifestPath)) ->load($this->config['app.providers']);
}
看下load()的源码:public function load(array $providers)
{
// 查看bootstrap/cache/services.php有没有这个缓存文件
// 第一次启动时是没有的
$manifest = $this->loadManifest();
// 开始没有这个缓存文件,那就把$providers[ ]里的值
if ($this->shouldRecompile($manifest, $providers)) {
// 然后根据$providers[ ]编译出services.php这个缓存文件
$manifest = $this->compileManifest($providers);
}foreach ($manifest['when'] as $provider => $events) { // 注册包含有事件监听的service provider // 包含有事件监听的service provider都要有when()函数返回 $this->registerLoadEvents($provider, $events); } foreach ($manifest['eager'] as $provider) { // 把'eager'字段中service provider注册进容器中, // 即遍历每一个service provider,调用其中的register()方法 // 向容器中注册具体的服务 $this->app->register($this->createProvider($provider)); } // 注册延迟的service provider, // deferred的service provider, 一是要设置$defer = true,二是要提供provides()方法返回绑定到容器中服务的名称 $this->app->addDeferredServices($manifest['deferred']);
}
看下编译缓存文件compileManifest()方法的源码:protected function compileManifest($providers)
{
$manifest = $this->freshManifest($providers);foreach ($providers as $provider) { $instance = $this->createProvider($provider); // 根据每一个service provider的defer属性看是否是延迟加载的service provider if ($instance->isDeferred()) { // 延迟加载的,根据provides()方法提供的服务名称,写入到'deferred'字段里 // 所以延迟加载的service provider都要提供provides()方法 foreach ($instance->provides() as $service) { $manifest['deferred'][$service] = $provider; } // 使用when()函数提供的值注册下含有事件的service provider, $manifest['when'][$provider] = $instance->when(); } else { // 不是延迟加载的,就放在'eager'字段里,用$this->app->register()来注册延迟加载的service provider $manifest['eager'][] = $provider; } } // 最后写入到services.php缓存文件中 return $this->writeManifest($manifest);
}
protected function freshManifest(array $providers)
{
return ['providers' => $providers, 'eager' => [], 'deferred' => []];
}
总之,注册providers就是把config/app.php中$providers[ ]定义的所有service provider中,把不是defer的service provider中绑定的服务启动起来,是defer的service provider等到需要里面绑定的服务时再执行绑定。 -
启动Providers
最后一步,就是启动程序了,看下BootProviders::bootstrap()源码:public function bootstrap(Application $app)
{
$app->boot();
}public function boot()
{
// 如果程序已启动则返回,显然还没启动,还在booting状态中
if ($this->booted) {
return;
}
// 执行之前Application实例化的时候在$bootingCallbacks[]注册的回调
$this->fireAppCallbacks($this->bootingCallbacks);
// 之前凡是用Application::register()方法的service provider都写入到了$serviceProviders[]中
// 这里依次执行每一个service provider里的boot()方法,如果存在的话
array_walk($this->serviceProviders, function ($p) {
$this->bootProvider($p);
});$this->booted = true; // 执行之前Application实例化的时候在$bootedCallbacks[]注册的回调 $this->fireAppCallbacks($this->bootedCallbacks);
}
protected function bootProvider(ServiceProvider $provider)
{
if (method_exists($provider, 'boot')) {
return $this->call([$provider, 'boot']);
}
}
从以上源码中知道,第(7)步和第(6)步类似:第(6)是依次执行每一个不是defer的service provider的register()方法;第(7)步是依次执行每一个不是defer的service provider的boot()方法,如果存在的话。所以官网上service provider章节说了这么一句The Boot Method:
This method is called after all other service providers have been registered, meaning you have access to all other services that have been registered by the framework
这里就明白了为啥这句话的含义了。
之前聊过Application::register()方法时里面有个检测程序是否已经启动的代码:
public function register($provider, $options = [], $force = false)
{
...
if ($this->booted) {
$this->bootProvider($provider);
}
return $provider;
}
刚刚开始实例化Application的时候还没有启动,在执行所有非defer的service provider boot()方法后程序就启动了:$this->booted = true;。
OK, 程序启动所做的准备工作就聊完了,过程不复杂,只需一步步拆解就能基本清楚Laravel启动时做了哪些具体工作。
总结:本文主要学习了Laravel启动时做的七步准备工作:1. 环境检测 DetectEnvironment; 2. 配置加载 LoadConfiguratio; 3. 日志配置 ConfigureLogging; 4. 异常处理 HandleException;5. 注册Facades RegisterFacades;6. 注册Providers RegisterProviders;7. 启动Providers BootProviders。下次有好的技术再分享,到时见。
欢迎关注Laravel-China。
本作品采用《CC 协议》,转载必须注明作者和本文链接
好长,格式乱了