如何详细解析 HttpHandler 的映射机制?

2026-05-27 06:592阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2644个文字,预计阅读时间需要11分钟。

如何详细解析 HttpHandler 的映射机制?

在ASP.NET编程模型中,来自客户端的请求需经过一个称为管道的处理过程。在整个请求处理过程中,HttpHandler负责的核心处理是至关重要的。HttpHandler的作用在于处理请求,并将其转换为响应。

在ASP.NET编程模型中,一个来自客户端的请求要经过一个称为管线的处理过程。 在整个处理请求中,相对于其它对象来说,HttpHandler的处理算得上是整个过程的核心部分。 由于HttpHandler的重要地位,我前面已经有二篇博客对它过一些使用上的介绍。 中谈到了它的一般使用方法。 又详细地介绍了异步HttpHandler的使用方式。

今天的博客将着重介绍HttpHandler的配置,创建以及重用过程,还将涉及HttpHandlerFactory的内容。

回顾HttpHandler

HttpHandler其实是一类统称:泛指实现了IHttpHandler接口的一些类型,这些类型有一个共同的功能,那就是可以用来处理HTTP请求。 IHttpHandler的接口定义如下:

// 定义 ASP.NET 为使用自定义 HTTP 处理程序同步处理 HTTP Web 请求而实现的协定。 public interface IHttpHandler { // 获取一个值,该值指示其他请求是否可以使用 System.Web.IHttpHandler 实例。 // // 返回结果: // 如果 System.Web.IHttpHandler 实例可再次使用,则为 true;否则为 false。 bool IsReusable { get; } // 通过实现 System.Web.IHttpHandler 接口的自定义 HttpHandler 启用 HTTP Web 请求的处理。 void ProcessRequest(HttpContext context); }

有关HttpHandler的各类用法,可参考我的博客, 本文将不再重复说明了。它还有一个异步版本:

// 摘要: // 定义 HTTP 异步处理程序对象必须实现的协定。 public interface IHttpAsyncHandler : IHttpHandler { // 摘要: // 启动对 HTTP 处理程序的异步调用。 // // 参数: // context: // 一个 System.Web.HttpContext 对象,该对象提供对用于向 HTTP 请求提供服务的内部服务器对象(如 Request、Response、Session // 和 Server)的引用。 // // extraData: // 处理该请求所需的所有额外数据。 // // cb: // 异步方法调用完成时要调用的 System.AsyncCallback。如果 cb 为 null,则不调用委托。 // // 返回结果: // 包含有关进程状态信息的 System.IAsyncResult。 IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData); // // 摘要: // 进程结束时提供异步处理 End 方法。 // // 参数: // result: // 包含有关进程状态信息的 System.IAsyncResult。 void EndProcessRequest(IAsyncResult result); }

IHttpAsyncHandler接口的二个方法该如何使用,可参考我的博客, 本文也将不再重复讲解。

如果我们创建了一个自定义的HttpHandler,那么为了能让它处理某些HTTP请求,我们还需将它注册到web.config中,就像下面这样:

<localhost:51652/abc.test?id=1 时,会在浏览器中看到以下输出结果:

从这个截图来看,显然:MyTestHandler的实例被重用了。

我想很多人都创建过ashx文件,IDE会为我们创建一个实现了IHttpHandler接口的类型,在实现IsReusable属性时,一般都会这样:

public bool IsReusable { get { return false; } }

有些人看到这个,会想:如果返回true,就可以重用IHttpHandler实例,达到优化性能的目的。 但事实上,即使你在ashx中返回true也是无意义的,因为您可以试着这样去实现这个属性:

public bool IsReusable { get { throw new Exception("这里不起作用。"); } }

如果您访问那个ashx,会发现:根本没有异常出现!
因此,我们可以得出一个结论:默认情况下,IsReusable不能决定一个ashx的实例是否能重用。

这个结果太奇怪了。为什么会这样呢?

前面我们看到*.ashx的请求交给SimpleHandlerFactory来创建相应的HttpHandler对象, 然而当ASP.NET调用SimpleHandlerFactory.GetHandler()方法时, 该方法会直接创建并返回我们实现的类型实例。 换句话说:SimpleHandlerFactory根本不使用IHttpHandler.IsReusable的属性,因此,这种情况下,想重用ashx的实例是不可能的事, 所以,即使我在实现IsReusable属性时,写上抛异常的语句,根本也不会被调用。

同样的事情还发在aspx页面的实例上,所以,在默认情况下,我们不可能重用aspx, ashx的实例。

至于aspx的实例不能重用,除了和PageHandlerFactory有关外,还与Page在实现IHttpHandler.IsReusable有关,以下是Page的实现方式:

public bool IsReusable { get { return false; } }

从代码可以看到微软在Page是否重用上的明确想法:就是不允许重用!

由于Page的IsReusable属性我们平时看不到,我想没人对它的重用性有产生过疑惑,但ashx就不同了, 它的IsReusable属性的代码是摆在我们面前的,任何人都可以看到它,试想一下:当有人发现把它设为true or false时都不起作用,会是个什么想法? 估计很多人会郁闷。

小结:IHttpHandler.IsReusable并不能决定是否重用HttpHanlder !

实现自己的HttpHandlerFactory

通过前面的示例,我们也看到了,虽然IHttpHandler定义了一个IsReusable属性,但它并不能决定此类型的实例是否能得到重用。 重不重用,其实是由HttpHandlerFactory来决定的。ashx的实例不能重用就是一个典型的例子。

下面我就来演示如何实现自己的HttpHandlerFactory来重用ashx的实例。示例代码如下(注意代码中的注释):

internal class ReusableAshxHandlerFactory : IHttpHandlerFactory { private Dictionary<string, IHttpHandler> _cache = new Dictionary<string, IHttpHandler>(200, StringComparer.OrdinalIgnoreCase); public IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string physicalPath) { string cacheKey = requestType + virtualPath; // 检查是否有缓存的实例(或者可理解为:被重用的实例) IHttpHandler handler = null; if( _cache.TryGetValue(cacheKey, out handler) == false ) { // 根据请求路径创建对应的实例 Type handlerType = BuildManager.GetCompiledType(virtualPath); // 确保一定是IHttpHandler类型 if( typeof(IHttpHandler).IsAssignableFrom(handlerType) == false ) throw new HttpException("要访问的资源没有实现IHttpHandler接口。"); // 创建实例,并保存到成员字段中 handler = (IHttpHandler)Activator.CreateInstance(handlerType, true); // 如果handler要求重用,则保存它的引用。 if( handler.IsReusable ) _cache[cacheKey] = handler; } return handler; } public void ReleaseHandler(IHttpHandler handler) { // 不需要处理这个方法。 } }

为了能让HttpHandlerFactory能在ASP.NET中运行,还需要在web.config中注册:

<httpHandlers> <add path="*.ashx" verb="*" validate="false" type="ReusableAshxHandlerFactory"/> </httpHandlers>

有了这个配置后,我们可以创建一个Handler2.ashx来测试效果:

<%@ WebHandler Language="C#" Class="Handler2" %> using System; using System.Web; public class Handler2 : IHttpHandler { private Counter _counter = new Counter(); public bool IsReusable { get { return true; } } public void ProcessRequest(HttpContext context) { _counter.ShowCountAndRequestInfo(context); } }

在多次访问Handler2.ashx后,我们可以看到以下效果:

再来看看按照IDE默认生成的IsReusable会在运行时出现什么结果。示例代码:

<%@ WebHandler Language="C#" Class="Handler1" %> using System; using System.Web; public class Handler1 : IHttpHandler { private Counter _counter = new Counter(); public bool IsReusable { get { // 如果在配置文件中启用ReusableAshxHandlerFactory,那么这里将会被执行。 // 可以尝试切换下面二行代码测试效果。 //throw new Exception("这里不起作用。"); return false; } } public void ProcessRequest(HttpContext context) { _counter.ShowCountAndRequestInfo(context); } }

此时,无论我访问Handler1.ashx多少次,浏览器始终显示如下结果:

如果我启用代码行 throw new Exception("这里不起作用。"); 将会看到以下结果:

终于,我们期待的黄页出现了。
此时,如果我在web.config中将ReusableAshxHandlerFactory的注册配置注释起来,发现Handler1.ashx还是可以访问的。

回想一下前面我们看到的IHttpHandlerFactory接口,它还定义了一个ReleaseHandler方法,这个方法又是做什么的呢? 对于这个方法,MSDN也有一句简单的说明:

使工厂可以重用现有的处理程序实例。

对于这个说明,我认为并不恰当。如果按照HandlerFactoryWrapper的实现方式,那么这个解释是正确的。 但我前面的示例中,我在实现这个方法时,没有任何代码,但一样可以达到重用HttpHandler的目的。 因此,我认为重用的方式取决于具体的实现方式。

小结:IHttpHandler.IsReusable并不能完全决定HttpHandler的实例是否能重用,它只起到一个指示作用。 HttpHandler如何重用,关键还是要由HttpHandlerFactory来实现。

是否需要IsReusable = true ?

经过前面文字讲解以及示例演示,有些人可能会想:我在实现IHttpHandler的IsReusable属性时, 要不要返回true呢?(千万别模仿我的示例代码抛异常哦。

如果返回true,则HttpHandler能得到重用,或许某些场合下,是可以达到性能优化的目的。
但是,它也可能会引发新的问题:HttpHandler实例的一些状态会影响后续的请求。
也正是由于这个原因,aspx, ashx 的实例在默认情况下,都是不重用的。

有些人还可能会担心:被重用的HttpHandler是否有线程安全问题?
理论上,在ASP.NET中,只要使用static的数据成员都会有这个问题。 不过,这里所说的被重用的单个HttpHandler实例在处理请求过程中,只会被一个线程所调用,因此,它的实例成员还是线程安全的。 但有一点需要注意:在HttpHandlerFactory中实现重用HttpHandler时,缓存HttpHandler的容器要保证是线程安全的。

如果您希望重用HttpHandler来提升程序性能,那么我建议应该考虑以下问题:
HttpHandler的所有数据成员都能在处理请求前初始化。(通常会在后期维护时遗忘,尤其是多人维护时)

小结:在通常情况下,当实现IsReusable时返回false,虽然性能上不是最优,但却是最安全的做法。

HttpHandlerFactory的主要用途

前面示例演示了如何使用HttpHandlerFactory来重用HttpHandler,但设计HttpHandlerFactory并不是完全为了这个目的, 它的主要用途还是如何创建HttpHandler,而且定义IHttpHandlerFactory的主要目的是为了扩展性。

我想很多人也许使用过 Web Service ,它运行在ASP.NET平台上,自然也有对应的HttpHandler,我们来看看asmx这个扩展名是如何映射的。

<add path="*.asmx" verb="*" validate="false" type="System.Web.Services.Protocols.WebServiceHandlerFactory, System.Web.Services, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>

接着找WebServiceHandlerFactory,最后发现是这样创建的HttpHandler :

internal IHttpHandler CoreGetHandler(Type type, HttpContext context, HttpRequest request, HttpResponse response) { // ..... 已删除一些无关的代码 bool isAsync = protocol.MethodInfo.IsAsync; bool enableSession = protocol.MethodAttribute.EnableSession; if( isAsync ) { if( enableSession ) { return new AsyncSessionHandler(protocol); } return new AsyncSessionlessHandler(protocol); } if( enableSession ) { return new SyncSessionHandler(protocol); } return new SyncSessionlessHandler(protocol); }

这才是Factory嘛!

老实说,看到这几句话,我是眼前一亮:用HttpHandlerFactory来动态处理实在是太合适了。

这里有必要补充一下:

internal class SyncSessionHandler : SyncSessionlessHandler, IRequiresSessionState { }

小结:HttpHandlerFactory用途并非是为了专门处理HttpHandler的重用,它只是一个Factory, WebServiceHandlerFactory从另一个角度向我们展示了HttpHandlerFactory在扩展性方面所体现的重要作用。

如何详细解析 HttpHandler 的映射机制?

点击此处下载示例代码

如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的按钮。
如果,您希望更容易地发现我的新博客,不妨点击一下右下角的。
因为,我的写作热情也离不开您的肯定支持。

感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是Fish Li 。

本文共计2644个文字,预计阅读时间需要11分钟。

如何详细解析 HttpHandler 的映射机制?

在ASP.NET编程模型中,来自客户端的请求需经过一个称为管道的处理过程。在整个请求处理过程中,HttpHandler负责的核心处理是至关重要的。HttpHandler的作用在于处理请求,并将其转换为响应。

在ASP.NET编程模型中,一个来自客户端的请求要经过一个称为管线的处理过程。 在整个处理请求中,相对于其它对象来说,HttpHandler的处理算得上是整个过程的核心部分。 由于HttpHandler的重要地位,我前面已经有二篇博客对它过一些使用上的介绍。 中谈到了它的一般使用方法。 又详细地介绍了异步HttpHandler的使用方式。

今天的博客将着重介绍HttpHandler的配置,创建以及重用过程,还将涉及HttpHandlerFactory的内容。

回顾HttpHandler

HttpHandler其实是一类统称:泛指实现了IHttpHandler接口的一些类型,这些类型有一个共同的功能,那就是可以用来处理HTTP请求。 IHttpHandler的接口定义如下:

// 定义 ASP.NET 为使用自定义 HTTP 处理程序同步处理 HTTP Web 请求而实现的协定。 public interface IHttpHandler { // 获取一个值,该值指示其他请求是否可以使用 System.Web.IHttpHandler 实例。 // // 返回结果: // 如果 System.Web.IHttpHandler 实例可再次使用,则为 true;否则为 false。 bool IsReusable { get; } // 通过实现 System.Web.IHttpHandler 接口的自定义 HttpHandler 启用 HTTP Web 请求的处理。 void ProcessRequest(HttpContext context); }

有关HttpHandler的各类用法,可参考我的博客, 本文将不再重复说明了。它还有一个异步版本:

// 摘要: // 定义 HTTP 异步处理程序对象必须实现的协定。 public interface IHttpAsyncHandler : IHttpHandler { // 摘要: // 启动对 HTTP 处理程序的异步调用。 // // 参数: // context: // 一个 System.Web.HttpContext 对象,该对象提供对用于向 HTTP 请求提供服务的内部服务器对象(如 Request、Response、Session // 和 Server)的引用。 // // extraData: // 处理该请求所需的所有额外数据。 // // cb: // 异步方法调用完成时要调用的 System.AsyncCallback。如果 cb 为 null,则不调用委托。 // // 返回结果: // 包含有关进程状态信息的 System.IAsyncResult。 IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData); // // 摘要: // 进程结束时提供异步处理 End 方法。 // // 参数: // result: // 包含有关进程状态信息的 System.IAsyncResult。 void EndProcessRequest(IAsyncResult result); }

IHttpAsyncHandler接口的二个方法该如何使用,可参考我的博客, 本文也将不再重复讲解。

如果我们创建了一个自定义的HttpHandler,那么为了能让它处理某些HTTP请求,我们还需将它注册到web.config中,就像下面这样:

<localhost:51652/abc.test?id=1 时,会在浏览器中看到以下输出结果:

从这个截图来看,显然:MyTestHandler的实例被重用了。

我想很多人都创建过ashx文件,IDE会为我们创建一个实现了IHttpHandler接口的类型,在实现IsReusable属性时,一般都会这样:

public bool IsReusable { get { return false; } }

有些人看到这个,会想:如果返回true,就可以重用IHttpHandler实例,达到优化性能的目的。 但事实上,即使你在ashx中返回true也是无意义的,因为您可以试着这样去实现这个属性:

public bool IsReusable { get { throw new Exception("这里不起作用。"); } }

如果您访问那个ashx,会发现:根本没有异常出现!
因此,我们可以得出一个结论:默认情况下,IsReusable不能决定一个ashx的实例是否能重用。

这个结果太奇怪了。为什么会这样呢?

前面我们看到*.ashx的请求交给SimpleHandlerFactory来创建相应的HttpHandler对象, 然而当ASP.NET调用SimpleHandlerFactory.GetHandler()方法时, 该方法会直接创建并返回我们实现的类型实例。 换句话说:SimpleHandlerFactory根本不使用IHttpHandler.IsReusable的属性,因此,这种情况下,想重用ashx的实例是不可能的事, 所以,即使我在实现IsReusable属性时,写上抛异常的语句,根本也不会被调用。

同样的事情还发在aspx页面的实例上,所以,在默认情况下,我们不可能重用aspx, ashx的实例。

至于aspx的实例不能重用,除了和PageHandlerFactory有关外,还与Page在实现IHttpHandler.IsReusable有关,以下是Page的实现方式:

public bool IsReusable { get { return false; } }

从代码可以看到微软在Page是否重用上的明确想法:就是不允许重用!

由于Page的IsReusable属性我们平时看不到,我想没人对它的重用性有产生过疑惑,但ashx就不同了, 它的IsReusable属性的代码是摆在我们面前的,任何人都可以看到它,试想一下:当有人发现把它设为true or false时都不起作用,会是个什么想法? 估计很多人会郁闷。

小结:IHttpHandler.IsReusable并不能决定是否重用HttpHanlder !

实现自己的HttpHandlerFactory

通过前面的示例,我们也看到了,虽然IHttpHandler定义了一个IsReusable属性,但它并不能决定此类型的实例是否能得到重用。 重不重用,其实是由HttpHandlerFactory来决定的。ashx的实例不能重用就是一个典型的例子。

下面我就来演示如何实现自己的HttpHandlerFactory来重用ashx的实例。示例代码如下(注意代码中的注释):

internal class ReusableAshxHandlerFactory : IHttpHandlerFactory { private Dictionary<string, IHttpHandler> _cache = new Dictionary<string, IHttpHandler>(200, StringComparer.OrdinalIgnoreCase); public IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string physicalPath) { string cacheKey = requestType + virtualPath; // 检查是否有缓存的实例(或者可理解为:被重用的实例) IHttpHandler handler = null; if( _cache.TryGetValue(cacheKey, out handler) == false ) { // 根据请求路径创建对应的实例 Type handlerType = BuildManager.GetCompiledType(virtualPath); // 确保一定是IHttpHandler类型 if( typeof(IHttpHandler).IsAssignableFrom(handlerType) == false ) throw new HttpException("要访问的资源没有实现IHttpHandler接口。"); // 创建实例,并保存到成员字段中 handler = (IHttpHandler)Activator.CreateInstance(handlerType, true); // 如果handler要求重用,则保存它的引用。 if( handler.IsReusable ) _cache[cacheKey] = handler; } return handler; } public void ReleaseHandler(IHttpHandler handler) { // 不需要处理这个方法。 } }

为了能让HttpHandlerFactory能在ASP.NET中运行,还需要在web.config中注册:

<httpHandlers> <add path="*.ashx" verb="*" validate="false" type="ReusableAshxHandlerFactory"/> </httpHandlers>

有了这个配置后,我们可以创建一个Handler2.ashx来测试效果:

<%@ WebHandler Language="C#" Class="Handler2" %> using System; using System.Web; public class Handler2 : IHttpHandler { private Counter _counter = new Counter(); public bool IsReusable { get { return true; } } public void ProcessRequest(HttpContext context) { _counter.ShowCountAndRequestInfo(context); } }

在多次访问Handler2.ashx后,我们可以看到以下效果:

再来看看按照IDE默认生成的IsReusable会在运行时出现什么结果。示例代码:

<%@ WebHandler Language="C#" Class="Handler1" %> using System; using System.Web; public class Handler1 : IHttpHandler { private Counter _counter = new Counter(); public bool IsReusable { get { // 如果在配置文件中启用ReusableAshxHandlerFactory,那么这里将会被执行。 // 可以尝试切换下面二行代码测试效果。 //throw new Exception("这里不起作用。"); return false; } } public void ProcessRequest(HttpContext context) { _counter.ShowCountAndRequestInfo(context); } }

此时,无论我访问Handler1.ashx多少次,浏览器始终显示如下结果:

如果我启用代码行 throw new Exception("这里不起作用。"); 将会看到以下结果:

终于,我们期待的黄页出现了。
此时,如果我在web.config中将ReusableAshxHandlerFactory的注册配置注释起来,发现Handler1.ashx还是可以访问的。

回想一下前面我们看到的IHttpHandlerFactory接口,它还定义了一个ReleaseHandler方法,这个方法又是做什么的呢? 对于这个方法,MSDN也有一句简单的说明:

使工厂可以重用现有的处理程序实例。

对于这个说明,我认为并不恰当。如果按照HandlerFactoryWrapper的实现方式,那么这个解释是正确的。 但我前面的示例中,我在实现这个方法时,没有任何代码,但一样可以达到重用HttpHandler的目的。 因此,我认为重用的方式取决于具体的实现方式。

小结:IHttpHandler.IsReusable并不能完全决定HttpHandler的实例是否能重用,它只起到一个指示作用。 HttpHandler如何重用,关键还是要由HttpHandlerFactory来实现。

是否需要IsReusable = true ?

经过前面文字讲解以及示例演示,有些人可能会想:我在实现IHttpHandler的IsReusable属性时, 要不要返回true呢?(千万别模仿我的示例代码抛异常哦。

如果返回true,则HttpHandler能得到重用,或许某些场合下,是可以达到性能优化的目的。
但是,它也可能会引发新的问题:HttpHandler实例的一些状态会影响后续的请求。
也正是由于这个原因,aspx, ashx 的实例在默认情况下,都是不重用的。

有些人还可能会担心:被重用的HttpHandler是否有线程安全问题?
理论上,在ASP.NET中,只要使用static的数据成员都会有这个问题。 不过,这里所说的被重用的单个HttpHandler实例在处理请求过程中,只会被一个线程所调用,因此,它的实例成员还是线程安全的。 但有一点需要注意:在HttpHandlerFactory中实现重用HttpHandler时,缓存HttpHandler的容器要保证是线程安全的。

如果您希望重用HttpHandler来提升程序性能,那么我建议应该考虑以下问题:
HttpHandler的所有数据成员都能在处理请求前初始化。(通常会在后期维护时遗忘,尤其是多人维护时)

小结:在通常情况下,当实现IsReusable时返回false,虽然性能上不是最优,但却是最安全的做法。

HttpHandlerFactory的主要用途

前面示例演示了如何使用HttpHandlerFactory来重用HttpHandler,但设计HttpHandlerFactory并不是完全为了这个目的, 它的主要用途还是如何创建HttpHandler,而且定义IHttpHandlerFactory的主要目的是为了扩展性。

我想很多人也许使用过 Web Service ,它运行在ASP.NET平台上,自然也有对应的HttpHandler,我们来看看asmx这个扩展名是如何映射的。

<add path="*.asmx" verb="*" validate="false" type="System.Web.Services.Protocols.WebServiceHandlerFactory, System.Web.Services, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>

接着找WebServiceHandlerFactory,最后发现是这样创建的HttpHandler :

internal IHttpHandler CoreGetHandler(Type type, HttpContext context, HttpRequest request, HttpResponse response) { // ..... 已删除一些无关的代码 bool isAsync = protocol.MethodInfo.IsAsync; bool enableSession = protocol.MethodAttribute.EnableSession; if( isAsync ) { if( enableSession ) { return new AsyncSessionHandler(protocol); } return new AsyncSessionlessHandler(protocol); } if( enableSession ) { return new SyncSessionHandler(protocol); } return new SyncSessionlessHandler(protocol); }

这才是Factory嘛!

老实说,看到这几句话,我是眼前一亮:用HttpHandlerFactory来动态处理实在是太合适了。

这里有必要补充一下:

internal class SyncSessionHandler : SyncSessionlessHandler, IRequiresSessionState { }

小结:HttpHandlerFactory用途并非是为了专门处理HttpHandler的重用,它只是一个Factory, WebServiceHandlerFactory从另一个角度向我们展示了HttpHandlerFactory在扩展性方面所体现的重要作用。

如何详细解析 HttpHandler 的映射机制?

点击此处下载示例代码

如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的按钮。
如果,您希望更容易地发现我的新博客,不妨点击一下右下角的。
因为,我的写作热情也离不开您的肯定支持。

感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是Fish Li 。