HttpKernel组件

3.3 版本
维护中的版本

HttpKernel组件利用EventDispatcher组件提供了一个结构化的处理进程来将Request转换为Response。该组件的灵活程度足以打造一个全功能的框架(Symfony)、一个微框架(Silex)以及一个高级CMS发布系统(Drupal)。

安装 

你可以通过下述两种方式安装:

然后,包容vendor/autoload.php文件,以开启Composer提供的自动加载机制。否则,你的程序将无法找到这个Symfony组件的类。

Request的工作流 

每一次HTTP web交互,始于一个请求,结束于一个响应。身为开发者你要做的是利用PHP代码读取请求信息(比如URL),然后创建并返回一个响应(如一个HTML页面或JSON串)。

上图中的文字,描述了WEB运行次第:

  1. 用户在浏览器中请求某个资源

  2. 浏览器发送请求到服务器

  3. Symfony向开发者提供一个Request对象

  4. 开发者要将这个Request对象“转换”成一个Response对象

  5. 服务器发送响应给浏览器

  6. 浏览器显示资源给用户

通常,某些类型的框架或者系统,被设计成处理全部的重复劳动(如路由、安全等),这样开发者可以轻松创建程序的每一个页面。但究竟这些系统是如何 构建的则千差万别。HttpKernel组件提供了一个接口,把“始于请求,结束于合适的响应”这一过程,正式定型。本组件是任何程序和框架的心脏,无论那些系统的构建是多么的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Symfony\Component\HttpKernel;
 
use Symfony\Component\HttpFoundation\Request;
 
interface HttpKernelInterface
{
    // ...
 
    /**
     * @return Response A Response instance / 返回一个Response实例
     */
    public function handle(
        Request $request,
        $type = self::MASTER_REQUEST,
        $catch = true
    );
}

HttpKernel::handle() - 该方法是HttpKernelInterface::handle()接口方法的具体实现,定义了一个“始于Request对象,结束于Response对象”的工作流。

这一工作流的具体细节是理解kernel(包括Symfony框架和其他使用了kernel的库)如何工作的关键。

事件驱动的HttpKernel 

HttpKernel::handle()方法在内部是靠“派遣事件”来完成工作的。这不光能令此方法灵活,还能更加抽象,因为在一个框架/程序中,所有用HttpKernel派生出来的“工作”,统统是被event listener(事件监听)来完成的。

为了更好地解释这个过程,本文对进程中的每一步予以关照,讨论HttpKernel的某个具体实现——也就是Symfony框架——究竟是如何工作的。

在内部,使用HttpKernel是极其简单的,涉及一个 event dispatcher和一个控制器及其参数的“解析器”。为了完成你的可用内核(working kernel),你需要为下面讨论中所提及的事件,添加更多的事件监听(译注:本文是要告诉大家,如何去单独地使用HttpKernel组件,并非是对Symfony自身的相关代码进行分析——尽管每一个用本组件构建而成的kernel看起来都很像):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
 
// create the Request object 创建Request对象
$request = Request::createFromGlobals();
 
$dispatcher = new EventDispatcher();
// ... add some event listeners 添加一些监听
 
// create your controller and argument resolvers
// 创建你的控制器及其参数的解析器
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();
 
// instantiate the kernel 对kernel实例化
$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);
 
// actually execute the kernel, which turns the request into a response
// by dispatching events, calling a controller, and returning the response
// 真正执行内核,通过事件派遣、调用控制器、返回response,来把请求转换为响应
$response = $kernel->handle($request);
 
// send the headers and echo the content 发送头信息并输出内容
$response->send();
 
// triggers the kernel.terminate event 触发kernel.terminate事件
$kernel->terminate($request, $response);

参考完整示例以了解更多的具体实现办法。

若要了解关于如何为下文中的事件“添加监听”,可参考创建一个事件监听

截至3.1,HttpKernel接收了一个“第四参”,它必须是ArgumentResolverInterface的一个实例。在4.0版框架中此参数将变为必须。

有个极好的入门系列,讲的是利用HttpKernel组件和Symfony其他组件来创建你自己的微框架。参考这个介绍

1)kernel.request事件 

主要目的:添加更多的信息给Request,对部分系统进行初始化或在可能的情况下返回一个Response(如,安全层[security layer]拒绝访问等)。

Kernel Events信息表

HttpKernel::handle()方法中 第一个被派遣的事件是kernel.request,它可以有多种不同的监听。

这个事件的监听器可以是多种多样的。其中的一些——像是某个security listener——已经有足够多的信息来立即创建一个Response对象。例如,如果一个securty listener决定不让用户访问,那么这个监听会返回一个RedirectResponse到登陆页,或是返回一个403拒绝访问的响应。

如果一个Response对象在这种情况下被返回,则(HttpKernel::handle())进程直接跳到kernel.response事件。

其他的监听,只是对请求进行初始化操作或者添加更多信息进来。例如,一个监听器有可能对Request对象确定和设置locale(区域信息)。

另一个常见的监听器是,路由监听。一个router listener可以处理Request并决定controller是否应该被渲染(参见下一小节)。实际上,Request对象有一个attributes bag(属性包),它是存放“与请求相关的附加数据和程序级特定数据”的好地方。这意味着你的路由监听通过某种方式来控制controller,它可以存储Request对象之attributes(供控制器的resolver[解析器]来使用)。

总地说,kernel.request事件的用意,是“既能直接创建、又可直接返回”一个Response对象,或者,对Request对象添加信息(如,设置locale以及其他一些信息到Request的attributes中)

当(在监听中)为kernel.request事件设置响应时,propagation(宣传)被停止了。这意味着低优先级的监听,将不再继续执行。

Symfony框架中的kernel.request

在Symfony框架中,kernel.request事件的一个极其重要的监听,是 RouterListener。这个类执行了路由层(routing layer),路由层返回一个含有“匹配的请求”之相关信息的数组,包括_controller以及路由pattern中的每一个占位符(如 {slug})等。参考Routing component路由组件。

这个“信息数组”也被存放于Request对象的attributes属性数组中。当前阶段添加进来的路由信息暂未发生任何作用,但却在下面“对控制器进行解析”时被用到。

2)解析controller 

假设kernel.request的监听没有能创建一个Response,HttpKernel的下一步是决定和准备(如,resolve[解析])控制器。控制器是末级程序(end-application,即由Symfony用户所创建的)代码中,对某一特定页面创建和返回Response的那部分。唯一的需求,是一个PHP回调(callable)——它可以是一个函数,对象的方法,或是一个closure

但是对于一个请求,你如何 才能确定它属于哪个控制器,则完全取决于你的程序。这也是“控制器解析器(controller resolver)”所要承担的任务——它是实现了ControllerResolverInterface接口的类,也是HttpKernel的构造参数之一。

你的任务是创建一个类去实现该接口的两个方法:getController()getArguments()。实际上,默认的实现已经存在了,你可以直接使用,或者参考:ControllerResolver。这个实现将在下文中详细解释:

1
2
3
4
5
6
7
8
9
10
namespace Symfony\Component\HttpKernel\Controller;
 
use Symfony\Component\HttpFoundation\Request;
 
interface ControllerResolverInterface
{
    public function getController(Request $request);
 
    public function getArguments(Request $request, $controller);
}

ControllerResolver中的getArguments()方法连同其ControllerResolverInterface接口已经从3.1版开始被弱化(deprecated),并且将从4.0中移除。你可以使用ArgumentResolver来替代,它实现的是ArgumentResolverInterface接口。

在内部,HttpKernel::handle()方法首先调用controller resolver(控制器解析器)中的getController()方法。该方法被传入一个Request对象,其职责是基于请求中的信息,来以某种方式决定并返回一个PHP callable(即控制器)。

第二个方法,getArguments(),将在另外一个事件 - kernel.controller - 被派遣之后被调用。

Symfony框架中的控制器解析过程

Symfony框架使用了内置的ControllerResolver类(实际上,框架使用的是子类,扩展了一些功能,后面会提及)。这个类巧用了在RouterListener触发过程中存放于Request对象之attributes属性中的信息。

getController

ControllerResolverRequest对象的attributes属性(数组)中寻找_controller键(再次强调,这个信息通常被RouterListener存放于Request对象的attributes属性中)。然后,这个字符串会被转换成一个PHP的callable,通过如下步骤可以实现:

  1. _controller键对应的值是AcmeDemoBundle:Default:index这种格式,则会被转换成另一个字符串,含有控制器完成类名和方法名,这是通过以下Symfony命名约定实现的—— Acme\DemoBundle\Controller\DefaultController::indexAction。这种转换是Symfony框架所使用的ControllerResolver子类所独有的。

  2. 你的控制器类的一个全新实例。实例化时,并不带有构造参数。

  3. 如果控制器实现的是ContainerAwareInterface接口,控制器对象的setContainer()方法会被调用以传入容器给它。这一步也是Symfony框架所使用的ControllerResolver子类所独有的。

3)kernel.controller事件 

主要目的:在控制器被执行之前,初始化一些东西,或改变控制器。

Kernel Events信息表

在控制器的callable被确定下来之后,HttpKernel::handle()将派遣kernel.controller事件。监听此事件的listener可能会初始化系统的某些部分,因为它们需要在特定的东西被确定之后(比如controller、routing信息等)同时又在控制器被执行之前被初始化。下文中有一些Symfony框架内部针对此事件进行监听的例子。

此事件的监听,通过调用传入该监听的事件对象中的FilterControllerEvent::setController方法,也可以完全改变控制器的callable。

Symfony框架中的kernel.controller

Symfony框架中有几个小型监听作用于kernel.controller事件,当profiler分析器被开启时,多数是在负责收集分析数据。

一个有意思的监听,来自SensioFrameworkExtraBundle,该Bundle被Symfony标准版框架收入。这个监听的@ParamConverter功能,可以让你传入一个完整对象(如,Post对象)到你的控制器中,而不是使用一个标量值(scalar value,比如一个id路由参数)。这个监听是 - ParamConverterListener - 它使用反射(reflection)来寻找控制器的每一个参数,并且尝试使用不同的方法来把它们转换成对象,对象是被存在Request对象的attributes属性中。请阅读后续小节以了解为什么这是重要的。

4)获取控制器参数 

接下来,HttpKernel::handle()开始调用ArgumentResolverInterface::getArguments()。牢记一点,在getController()中返回的控制器是一个回调(callable)。而getArguments()的作用是要返回“需要传入控制器”的参数(arguments)数组。此过程如何实现取决于你的设计,不过内置的ArgumentResolver是一个好例子。

在这个节点,kernel已经有了一个PHP回调(即controller),以及一个在执行此回调时“应当传入”的参数数组。

在Symfony框架中取得控制器的参数

现在你已经明确了控制器回调(controller callable,通常是一个controller对象中的方法)是什么,ArgumentResolver类对这个回调使用了reflection(反射),为的是返回一个“包含每一个参数名(names)”的数组。然后参数解析器要遍历这些参数,并使用以下技巧来决定这些参数应当被返回什么值:

  1. 如果Request对象的属性包(attributes bag)数组中含有一个键(key),能够匹配参数名(argument name),那么这个值(value)将被采纳。例如,如果控制器的第一个参数是$slug,然后有个slug键存在于Requestattributes属性(该属性是数组)中,那么这个键的值将被使用(通常这个值来自于RouterListener,即路由监听)。

  2. 如果控制器参数被“类型提示”为Symfony的Request对象,则Request对象将被作为参数值来传入。如果你自定义了一个Request类,它将被在你继承Symfony的Request类时被注入。

  3. 如果函数或方法的参数是variadic而且Request对象的attributes bag(属性包)含有一个(那个可变参数的)数组,则那些值透过 variadic argument都能够被使用。

以上解析功能,由实现了ArgumentResolverInterface接口的resolver类来提供。有四个接口方法用于提供Symfony的默认行为,但能够自定义才是本文强调的关键。你可以自行实现ArgumentResolverInterface接口,然后把你的这个类传入ArgumentResolver,即可继承上述这些功能。

5)调用控制器 

下一步就简单了!HttpKernel::handle()要执行控制器了。

控制器的任务是为给定的资源创建响应。它可以是一个HTML页面,JSON串或任何东东。不像handle()进程的其他步骤,这一步是由“末级开发者”(end-developer)来实现的,以构建每一个页面。

通常,控制器要返回一个Response对象。如果是这种情况,那么kernel的工作差不多就完成了!在本例中,下一步是kernel.response事件。

但如果控制器返回的是Response以外的任何东西,则内核(kernel)还有少许工作要做——kernel.view事件(因为kernel乃至框架的终极使命始终是要生成一个Response对象)。

一个控制器必须要返回一些东西。如果控制器返回null,则异常会被立即抛出。

6)kernel.view事件 

主要目的:把控制器的一个非Response的返回值转换成一个Response对象。

Kernel Events信息表

如果控制器没能返回一个Response对象,那么kernel会派遣另一个事件 - kernel.view。监听此事件的listener要做的就是接管控制器的这个返回值(比如,一个data数组或某个对象)来创建一个Response对象。

当你希望使用一个view层时这是非常有用的:毋须从控制器中返回Response,你可以返回渲染页面所需之数据。本事件的监听可以利用这些数据以正确格式(如HTML,JSON等)来创建Response

在这个节点,如果没有监听器利用事件(译注:指的是传入listener的事件对象)来设置Response,会有一个异常抛出:无论是控制器还是针对view的某个监听,必须要返回一个Response

当(在监听中)为kernel.view事件设置响应时,propagation(宣传)被停止了。这意味着低优先级的监听,将不再继续执行。

Symfony框架中的kernel.view

Symfony框架里面没有默认的监听器司职于kernel.view事件。不过,核心bundle之一 - SensioFrameworkExtraBundle - 的确 有添加一个监听到这个事件。如果你的控制器(的action方法)返回一个数组,同时你把@Template annotation添加到方法上方,那么这个监听是要负责渲染模板的,把你在控制器中返回的数组传到那个模板中,然后再创建一个Response响应,里面有从模板返回的内容。

除此之外,大受欢迎的社区bundle:FOSRestBundle,也提供了一个基于此事件的监听,目的是给你一个健壮的view层,使其有能力在控制器中返回“多种不同content-type”的响应(如,HTML,JSON,XML等等)。

7)kernel.response事件 

主要目的:在Response对象被发送之前修改它。

Kernel Events信息表

kernel的最终目标是把Request转换成ResponseResponse可能被创建于kernel.request事件的派遣过程中,也可能是从controller中返回,或者从kernel.view事件的某个监听中返回。

不管是谁来创建Response,另一个事件,也就是kernel.response旋即被派遣。该事件的一个典型监听将以某些方式来修改Response对象,比如修改头信息,添加cookie,甚至改变Response自身的内容(比如在HTML响应的</body>结束标签之前,注入一些JavaScript代码)等等。

本事件被派遣后,最终版Response对象将从handle()方法中被返回。在多数“代表性用法”中,你可以继续调用send()方法,来发送头信息并打出Response中的内容。

Symfony框架中的kernel.response

Symfony框架中有几个小型监听作用于kernel.response事件,多数是以某些方式来修改响应。例如,WebDebugToolbarListener要注入一些Javascript码段在你的页面底部,确保在dev环境下显示web除错工具条(web debug toolbar)。另一个监听是ContextListener,它把当前用户的信息序列化(serializes)到session中,以便在下一次请求中能够加载和使用。

8)kernel.terminate事件 

主要目的:在响应被流化到(streamed to)用户之后,执行一些重载操作。

Kernel Events信息表

HttpKernel进程中的最后一个事件是kernel.terminate,它是独特的,因为它是在HttpKernel::handle()方法之后,以及响应被发送到用户之后才发生。

1
2
3
4
5
// send the headers and echo the content 发送头信息并打出内容
$response->send();
 
// triggers the kernel.terminate event 触发terminate事件
$kernel->terminate($request, $response);

如同你看到的,在发送响应之后去调用$kernel->terminate,将触发kernel.terminate事件,进而可以执行特定的“延时操作”(如,发送邮件),这是为了尽可能快地将响应返回到客户端。

在内部,HttpKernel利用了fastcgi_finish_request PHP函数。这意味着在这一刻,只有PHP FPM server API才能够发送响应给客户端,同时服务器端的PHP进程仍在执行着一些任务。对于所有其他的服务器APIs,针对kernel.terminate事件的监听也仍然会被执行,只是无法发送响应到客户端,直到(监听中的)所有任务被执行完毕(才能发送)。

使用kernel.terminate是可选的,仅当你的kernel实现了TerminableInterface接口时方可调用。

Symfony框架中的kernel.terminate

如果你使用了Symfony中的SwiftMailerBundle并且用的是memory spooling(基于内存的多任务缓冲),则EmailSenderListener会被激活,它会在request过程中真正发送每一封你提前安排好的邮件。

异常处理:kernel.exception事件 

主要目的:用于处理各种类型的异常,并创建一个适当的Response来返回该异常。

Kernel Events信息表

HttpKernel::handle()过程中,若任何一个时间点抛出异常,则kernel.exception事件被触发。在内部,handle()方法体被打包成一个try-catch块儿。当异常抛出时,kernel.exception事件被派遣,以便你的系统能够以某种方式来回应该异常。

针对此事件的每一个监听都将被传入一个GetResponseForExceptionEvent事件,你可以用它的getException()来访问原始异常。本事件的监听通常都会检查异常的所属类型,并创建一个合适的错误Response

例如,为了生成一个404页,你可能要抛出一个特殊类型的异常,然后为exception事件添加监听,利用listener来寻找这个异常,然后创建并返回一个404Response。实际上,HttpKernel组件内置了一个ExceptionListener,你可以选择使用,它将为你做这些事,乃至更多(参考下方区块里的内容以了解细节)。

当(在监听中)为kernel.exception事件设置响应时,propagation(宣传)被停止了。这意味着低优先级的监听,将不再继续执行。

Symfony框架中的kernel.excption

当使用Symfony框架时,有两个主要监听作用于kernel.exception事件。

HttpKernel组件中的ExceptionListener

第一个监听来自于HttpKernel组件,被称为ExceptionListener。这个监听有以下几个目标:

  1. 把抛出的异常转换成一个FlattenException对象,里面包含关于request的所有信息,但却可以被输出(printed)和序列化(serialized)。

  2. 如果原始异常实现的是HttpExceptionInterface接口,则该异常中的getStatusCode()getHeaders()方法会被调用,用来加载FlattenException对象中的头信息和状态码(status code)。用这招是因为在下一步创建“最终响应”时要用到这些信息。如果你希望设置一个自定义的HTTP头,你始终可以使用(该异常的)setHeaders()方法,而这一方法却又源自HttpException

  3. 一个控制器将被执行,传入其中的是flattened exception(译注:即前述的扁平化异常,异常对象中的信息可以被打印和序列化,如同数组)。确定的正待渲染的控制器,被当成一个构造参数传入这个监听器。这个控制器将为这个错误页来返回最终版Response

Security组件中的ExceptionListener

另一个重要的监听是ExceptionListener。它的作用是处理security异常,以及在时机合适时帮助用户进行验证(authenticate。如,跳转到登陆页)。

创建一个事件监听 

至此,你已经能创建一个监听,并将其附于“在HttpKernel::handle()周期内”被派遣的事件之上。通常,监听(listener)就是一个“带有一些需要被执行的方法”的PHP类,但它也可以是任何东西。创建和附着事件监听的详细内容,请参考EventDispatcher组件。

每一个“kernel”事件的名字,被以常量的方式被定义在KernelEvents类中。此外,每一个监听器都要传入一个唯一参数,即KernelEvent的子类。该参数对象(译注:监听器的事件参数)包含了关于系统当前状态的信息,同时每一个事件都有自己的事件对象(event object):

Name
事件名
KernelEvents Constant
KernelEvents类常量
Argument passed to the listener
传入监听器的(事件)参数
kernel.request KernelEvents::REQUEST GetResponseEvent
kernel.controller KernelEvents::CONTROLLER FilterControllerEvent
kernel.view KernelEvents::VIEW GetResponseForControllerResultEvent
kernel.response KernelEvents::RESPONSE FilterResponseEvent
kernel.finish_request KernelEvents::FINISH_REQUEST FinishRequestEvent
kernel.terminate KernelEvents::TERMINATE PostResponseEvent
kernel.exception KernelEvents::EXCEPTION GetResponseForExceptionEvent

完整示例 

当使用HttpKernel组件时,你可以附着任意监听到(上表中的)核心事件;你可以使用任意controller resolver(控制器解析器),只要它实现了ControllerResolverInterface接口;你还可以使用任意argument resolver(参数解析器),只要它实现了 ArgumentResolverInterface接口。但是,HttpKernel组件自带了一些内置监听以及其他一些东东,可以用来创建一个完整示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
 
$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}', array(
    '_controller' => function (Request $request) {
        return new Response(
            sprintf("Hello %s", $request->get('name'))
        );
    })
));
 
$request = Request::createFromGlobals();
 
$matcher = new UrlMatcher($routes, new RequestContext());
 
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack()));
 
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();
 
$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);
 
$response = $kernel->handle($request);
$response->send();
 
$kernel->terminate($request, $response);

子请求/Sub Requests 

除了被传至HttpKernel::handle()的“主要”请求之外,你也可以传入一个被称为“sub-request”的子请求。子请求看上去和用起来都很像其他请求,但通常被用在渲染页面小段内容而不是整个页面。你可以从控制器中发起子请求(或者干脆在模板中发动,而这也是由你的控制器负责渲染)。

为了执行一次子请求,使用HttpKernel::handle()方法,但要改变第二个参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
 
// ...
 
// create some other request manually as needed
// 根据需要,手动创建某个其他请求
$request = new Request();
// for example, possibly set its _controller manually
// 作为示例,可以手动设置其_controller属性
$request->attributes->set('_controller', '...');
 
$response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST);
// do something with this response
// 对这个响应进行操作

这会创建另外一个完整的“请求-响应”周期,来把这个新的Request转换成一个Response。内部唯一的不同,就是某些监听(比如Security的)可能只对master request进行操作。每个监听被传入的都是Kernel.Event的子类,其isMasterRequest()方法可以被用于检查当前的请求是“主”还是“子”请求。

例如,若一个监听仅需对主请求进行操作的话,可能像下面这样:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
// ...
 
public function onKernelRequest(GetResponseEvent $event)
{
    if (!$event->isMasterRequest()) {
        return;
    }
 
    // ...
}

学习更多 

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)