详解ASP.NET MVC的筛选器

在ActionInvoker对Action的执行过程中,除了通过利用ActionDescriptor对Action方法的执行,以及之前进行的Model绑定与验证之外,还具有一个重要的工作,那就是对相关筛选器(Filter)的执行。ASP.NET MVC的筛选器是一种基于AOP(面向方面编程)的设计,我们将一些非业务的逻辑实现在相应的筛选器中,然后以一种横切(Crosscutting)的方式应用到对应的Action方法。当Action方法执行前后,这些筛选器会自动执行。ASP.NET MVC提供了四种类型的筛选器(AuthorizationFilter、ActionFilter、ResultFilter和ExceptionFilter),它们对应着相应的筛选器接口(IAuthorizationFilter、IActionFilter、IResultFilter和IExceptionFilter)。[本文已经同步到《How ASP.NET MVC Works?》中]

目录

一、Filter

二、FilterProvider

三、FilterAttribute与FilterAttributeFilterProvider

四、Controller与ControllerInstanceFilterProvider

五、GlobalFilterCollection

六、实例演示:验证Filter的提供机制和执行顺序

一、Filter

虽然ASP.NET MVC提供的四种类型的筛选器具有各自实现的接口,但是对于筛选器的提供体系来说所有的筛选器都通过具有如下定义的Filter类型表示。Filter的核心是Instance属性,因为它代表真正实施筛选功能的对象,该对象实现了一个或者多个基于上述四种筛选器类型的接口。

  public class Filter
  {
    public const int DefaultOrder = -1;
    public Filter(object instance, FilterScope scope, int? order);

    public object Instance { get; protected set; }
    public int Order { get; protected set; }
    public FilterScope Scope { get; protected set; }
  }
  public enum FilterScope
  {
    Action    = 30,
    Controller  = 20,
    First     = 0,
    Global    = 10,
    Last     = 100
  }

注:由于System.Web.Mvc.Filter和实现了IAuthorizationFilter、IActionFilter、IResultFilter和IExceptionFilter的类型均可以被称为“筛选器”,为了不至于造成混淆,在没有做明确说明的情况下,我们使用英文“Filter”和中文“筛选器”分别来表示它们。

Filter的Order和Scope属性最终决定了筛选器的执行顺序。Order属性对应数值越小,执行的优先级越高,该属性的默认值为-1(对应着Filter中定义的常量DefaultOrder)。如果两个Filter具有相同的Order属性值,那么Scope属性最终决定哪个被优先执行。Filter的Scope属性类型是一个类型为FilterScope的枚举。该枚举表示应用Filter的范围,Action和Controller代表Action方法和Controller类级别;First和Last意味着希望被作为第一个和最后一个Filter来执行;Global代表一个全局的Filter。

通过上面的代码片断我们可以看到FilterScope的5个枚举选项均被设置了一个值,这个值决定了Filter的执行顺序,具有更小的枚举值会被优先执行。从FilterScope的定义可以得到这样的结论:对于具有相同Order属性值的多个Filter,应用在Controller上的Filter比应用在Action方法上的Filter具有更高的执行优先级,而一个全局的Filter的执行优先级又高于基于Action的Filter。

二、FilterProvider

Filter的提供机制与之前我们介绍的基于ModelBinder和ModelValidator的提供机制比较类似,均是通过相应的Provider来提供的。提供筛选器的FilterProvider实现了接口IFilterProvider,如下面的代码片断所示,该接口定义了唯一的方法GetFilters根据指定的Controller上下文和用于描述目标Action的ActionDescriptor对象获取一个Filter对象集合。

  public interface IFilterProvider
  {
    IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
  }

我们可以通过静态类型FilterProviders注册或者获取当前应用使用的FilterProvider。如下面的代码片断所示,FilterProviders具有一个类型为FilterProviderCollection的只读属性Providers,表示基于整个Web应用范围内被使用的FilterProvider列表。FilterProviderCollection是元素类型为IFilterProvider的集合,GetFilters方法用于或者该集合中所有FilterProvider对象提供的Filter对象。

  public static class FilterProviders
  {
    public static FilterProviderCollection Providers { get; }
  }
  public class FilterProviderCollection : Collection<IFilterProvider>
  {
    //其他成员
    public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
  }

ASP.NET MVC提供了三种原生的FilterProvider,分别是FilterAttributeFilterProvider、ControllerInstanceFilterProvider和GlobalFilterCollection,接下来我们对它们进行单独介绍。

三、FilterAttribute与FilterAttributeFilterProvider

我们通常将筛选器定义成特性以声明的方式应用到Controller类型或者Action方法上,而抽象类型FilterAttribute是所有筛选器的基类。如下面的代码片断所示,FilterAttribute特性实现了IMvcFilter接口,该接口定义了Order和AllowMultiple两个只读属性,分别用于控制筛选器的执行顺序以及多个同类的筛选器能够同时应用到同一个目标元素(类或者方法)。

  [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
  public abstract class FilterAttribute : Attribute, IMvcFilter
  {
    protected FilterAttribute();

    public bool AllowMultiple { get; }
    public int Order { get; set; }
  }
  public interface IMvcFilter
  {
    bool AllowMultiple { get; }
    int Order { get; }
  }

从应用在FilterAttribute上的AttributeUsageAttribute的定义可以看出该特性可以应用在类型和方法上,这意味着筛选器一般都可以应用在Controller类型和Action方法上。只读属性AllowMultiple实际上返回的是AttributeUsageAttribute的同名属性,通过上面的定义我们可以看到默认情况下该属性值为False。

用于描述Controller和Action的ControllerDescriptor和ActionDescriptor均实现了ICustomAttributeProvider接口,我们可以直接利用它们获取应用在对应的Controller类型或者Action方法上包括FilterAttribute在内的所有特性。实际上,这两个描述类型提供了单独的方法GetFilterAttributes专门用于获取FilterAttribute特性列表。如下面的代码片断所示,该方法具有一个布尔类型的参数useCache,表示是否需要对解析出来的FilterAttribute特性进行缓存以缓解频繁的反射操作对性能造成的影响。

  public abstract class ControllerDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable
  {
    //其他成员
    public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache);
  }
  public abstract class ActionDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable
  {
    //其他成员
    public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache);
  }

针对FilterAttribute特性的Filter通过FilterAttributeFilterProvider对象来提供。FilterAttributeFilterProvider直接调用当前ControllerDescriptor和ActionDescriptor的GetFilterAttributes方法获取所有应用在Controller类型和当前Action方法的FilterAttribute特性,并借此创建相应的Filter对象。FilterAttributeFilterProvider构造函数的参数cacheAttributeInstances表示是否启用针对FilterAttribute的缓存,它将作为调用GetFilterAttributes方法的参数。在默认的情况下(通过调用默认无参的构造函数创建的FilterAttributeFilterProvider)会采用针对FilterAttribute的缓存。

  public class FilterAttributeFilterProvider : IFilterProvider
  {
    public FilterAttributeFilterProvider();
    public FilterAttributeFilterProvider(bool cacheAttributeInstances);
    protected virtual IEnumerable<FilterAttribute> GetActionAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
    protected virtual IEnumerable<FilterAttribute> GetControllerAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
    public virtual IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
  }

对于通过调用GetFilters得到的Filter,对应的FilterAttribute特性作为其Instance属性。Order属性来源于FilterAttribute的同名属性,而Scope属性则取决于FilterAttribute特性是应用在Controller类型上(Scope属性值为Controller)还是当前的Action方法上(Scope属性值为Action)。

四、Controller与ControllerInstanceFilterProvider

提到ASP.NET MVC的筛选器,大部分的都只会想到通过FilterAttribute特性,实际上Controller本身(继承自抽象类Controller)就是一个筛选器。如下面的代码片断所示,抽象类Controller实现了IActionFilter、IAuthorizationFilter、IExceptionFilter和IResultFilter这四个对应着不同筛选器类型的接口。

  public abstract class Controller : ControllerBase,
    IActionFilter,
    IAuthorizationFilter,
    IExceptionFilter,
    IResultFilter,
     ...
  {
    //省略成员
  }

针对Controller对象这种独特筛选器的FilterProvider类型为具有如下定义的ControllerInstanceFilterProvider。在实现的GetFilters方法中,它会根据指定的Controller上下文获取对应的Controller对象,并以此创建一个Filter(Controller对象作为Filter对象的Instance属性值)。该Filter的Scope不是Controller,而是First,而Order的值为-2147483648(Int32.MinValue),毫无疑问这样的Filter肯定第一个被执行。

  public class ControllerInstanceFilterProvider : IFilterProvider
  {
    public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
  }

五、GlobalFilterCollection

通过FilterAttribute的形式定义的筛选器需要显式地标注到目标Controller类型或者Action方法上,而在有些情况下需要一种全局的Filter。所谓全局筛选器,就是不需要显式与某个Controller或者Action进行匹配,而是默认使用到所有的Action执行过程中。用于提供这种全局Filter的FilterProvider对应的类型为具有如下定义的GlobalFilterCollection。

  public sealed class GlobalFilterCollection : IEnumerable<Filter>, IEnumerable, IFilterProvider
  {
    public GlobalFilterCollection();
    public void Add(object filter);
    public void Add(object filter, int order);
    private void AddInternal(object filter, int? order);
    public void Clear();
    public bool Contains(object filter);
    public IEnumerator<Filter> GetEnumerator();
    public void Remove(object filter);
    IEnumerator IEnumerable.GetEnumerator();
    IEnumerable<Filter> IFilterProvider.GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
    public int Count { get; }
  }

通过命名以及上面给出的定义可以看出GlobalFilterCollection就是一个Filter的列表而已,实现的GetFilters方法返回的就是它自己。通过GlobalFilterCollection提供的方法我们可以实现对全局Filter的添加、删除和清除操作。用于添加Filter的Add方法的参数filter不是一个Filter对象,而是一个具体筛选器(实现了相应的筛选器接口),添加的Filter对象根据该筛选器对象创建,其Scope属性被设置成Global。我们通过在Add方法指定添加Filter对象的Order属性,如果没有显示指定Order并且指定的筛选器是一个FilterAttribute特性,那么该特性的Order将会作为Filter对象的Order;否则使用-1作为Order属性值。

针对整个Web应用的全局Filter(或者说全局FilterProvider)的注册和获取可以通过静态类型GlobalFilters来实现。如下面的代码片断所示,GlobalFilters具有一个静态只读属性Filters返回一个GlobalFilterCollection对象。

  public static class GlobalFilters
  {
    public static GlobalFilterCollection Filters { get; }
  }

到目前为止,我们已经介绍了ASP.NET MVC默认提供的三种FilterProvider,以及各自采用得Filter提供机制。当用于注册FilterProvider的静态类型在加载的时候,会默认创建这三种类型的对象并将其作为表示全局FilterProvider集合的Providers属性值,具体的逻辑体现在如下的代码片断中。也就是说,在默认的情况下ASP.NET MVC会采用这三种FilterProvider来提供所有的Filter对象。

  public static class FilterProviders
  {
    static FilterProviders()
    {
      Providers = new FilterProviderCollection();
      Providers.Add(GlobalFilters.Filters);
      Providers.Add(new FilterAttributeFilterProvider());
      Providers.Add(new ControllerInstanceFilterProvider());
    }
    public static FilterProviderCollection Providers{get;private set;}
  }

六、实例演示:验证Filter的提供机制和执行顺序

为了让读者对上面介绍的Filter提供机制具有一个更加深刻的映像,我们来做一个简单的实例演示。在一个通过Visual Studio的ASP.NET MVC项目模板创建的空Web项目中,我们定义了如下一个几个FilterAttribute。FilterBaseAttribute是一个实现了IActionFilter接口的抽象类型,三个具体的FilterAttribute(FooAttribute、BarAttribute和BazAttribute)是它的继承者。

  public abstract class FilterBaseAttribute:FilterAttribute, IActionFilter
  {
    public void OnActionExecuted(ActionExecutedContext filterContext)
    {}
    public void OnActionExecuting(ActionExecutingContext filterContext)
    {}
  }
  public class FooAttribute : FilterBaseAttribute
  {}
  public class BarAttribute : FilterBaseAttribute
  {}
  public class BazAttribute : FilterBaseAttribute
  {} 

我们首先在Global.asax中通过如下的方式将BazAttribute注册为一个全局筛选器。需要注意的是定义在默认创建的Global.asax中的Application_Start方法会调用RegisterGlobalFilters方法注册一个类型为HandleErrorAttribute的ExceptionFilter,我们需要将这行代码注释。

  public class MvcApplication : System.Web.HttpApplication
  {
    //其他成员
    protected void Application_Start()
    {
      //其他操作
      //RegisterGlobalFilters(GlobalFilters.Filters);
      GlobalFilters.Filters.Add(new BazAttribute());
    }
  }

最后我们创建如下一个默认的HomeController,一个空的Action方法Data上应用了我们定义的BarAttribute特性,而HomeController类上则应用了FooAttribute特性。在默认的Action方法Index中,我们通过FilterProviders的静态属性Providers表示的全局FilterProvider列表得到针对于Action方法Data的所有Filter对象,并将它们的基本信息(类型、Order和Scope属性)呈现出来。

  [Foo]
  public class HomeController : Controller
  {
    public void Index()
    {
      ReflectedControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController));
      ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(ControllerContext, "Data");
      foreach (var filter in FilterProviders.Providers.GetFilters(ControllerContext, actionDescriptor))
      {
        Response.Write(string.Format("{0}<br/>",filter.Instance));
        Response.Write(string.Format("    {0}: {1}<br/>", "Order",filter.Order));
        Response.Write(string.Format("    {0}: {1}<br/><br/>", "Scope",filter.Scope));
      }
    }
    [Bar]
    public void Data()
    { }
  }

运行我们的程序之后会在浏览器中呈现如图7-5所示的结果。我们可以清楚地看到,不仅仅应用在自身Action方法的FilterAttribute会应用到目标Action上,应用在Controller类的FilterAttribute、全局注册的Filter以及Controller对象本身体现的Filter都回最终应用在所有的Action上面。

上图将应用于Action方法Data的4个Filter的Order和Scope属性显示出来。我们在前面提到这两个属性决定了同类筛选器执行的顺序,我们现在利用这个程序要证实这一点。为此我们需要对FilterBaseAttribute作如下的修改,在OnActionExecuting中我们将当前执行的FilterAttribute的类型的方法名呈现出来。

  public abstract class FilterBaseAttribute:FilterAttribute, IActionFilter
  {
    public void OnActionExecuted(ActionExecutedContext filterContext)
    {}
    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
      filterContext.HttpContext.Response.Write(string.Format("{0}.OnActionExecuting()<br/>", this.GetType()));
    }
  }

然后我们按照相同的方式重写了HomeController的OnActionExecuting方法,将HomeController自身的类型的当前方法名称呈现出来。

  [Foo]
  public class HomeController : Controller
  {
    //其他成员
    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      Response.Write("HomeController.OnActionExecuting()<br/>");
    }
    [Bar]
    public void Data()
    { }
  }

我们再次运行我们的程序,并在浏览器上指定正确的地址访问定义在HomeController的Action方法Data,会在浏览器中呈现如下图所示的结果。输出的结果体现了应用到Action方法Data上的四个ActionFilter执行的顺序,而这是和Filter对应的Order和Scope属性值是一致的。

关于Filter的提供还另一个值得深究的问题:我们在定义FilterAttribute的时候可以将应用在该类型上的AttributeUsageAttribute的AllowMultiple属性设置为False使它只能在同一个目标元素上应用一次。但是,我们依然可以在Action方法和所在的Controller类型上应用它们,甚至可以将它们注册为全局Filter,那么这些FilterAttribute都将有效吗?

我们现在就来通过实例来验证这一点。现在我们删除所有的FilterAttribute,定义如下一个类型为FooAttribute的ActionFilter,我们将应用在它上面的AttributeUsageAttribute特性的AllowMultiple属性设置为False。

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
  public class FooAttribute : FilterAttribute, IActionFilter
  {
    public void OnActionExecuted(ActionExecutedContext filterContext)
    { }
    public void OnActionExecuting(ActionExecutingContext filterContext)
    { }
  }

现在我们将该FooAttribute特性同时应用在HomeController类型和Action方法Data上,然后在Global.asax中注册一个针对FooAttribute特性的全局Filter。

  [Foo]
  public class HomeController : Controller
  {
    //其他成员
    [Foo]
    public void Data()
    { }
  }
  public class MvcApplication : System.Web.HttpApplication
  {
    //其他成员
    protected void Application_Start()
    {
      //其他操作
      //RegisterGlobalFilters(GlobalFilters.Filters);
      GlobalFilters.Filters.Add(new FooAttribute());
    }
  }

现在我们直接运行我们的程序,开启的浏览器中会呈现出如图7-7所示的结果。可以清楚地看到虽然我们 在三个地方注册了FooAttribute,但是由于该特性的AllowMultiple属性为False,所以只有其中一个FooAttribute最终是有效的。

对于AllowMultiple属性为False的FilterAttribute来说,如果我们以不同的Scope注册了多个,最终有效的是哪个呢?从上图可以看出,应用在Action方法(Scope为Action)上的FooAttribute是有效的。其实具体的逻辑是这样的:所有被创建的Filter按照Order+Scope进行排序(即Filter执行的顺序),取排在最后一个。对于我们的例子来说,提供的三个Filter具有相同的Order属性值(-1),所有最终会按照Scope(Scope、Controller和Action)进行排序,排在最后一个的自然是Scope为Action的Filter。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,同时也希望多多支持我们!

(0)

相关推荐

  • ASP.NET MVC 微信JS-SDK认证

    ASP.NET MVC微信JS-SDK认证,具体内容: 写在前面 前阵子因为有个项目需要做微信自定义分享功能,因而去研究了下微信JS-SDK相关知识. 此文做个简单的记(tu)录(cao)... 开始 所有的东西都从文档开始:微信JSSDK说明文档 项目需要用到的是分享接口不过使用微信JS-SDK之前,需要做JS接口认证. 认证如下: 步骤一:绑定域名 步骤二:引入JS文件 步骤三:通过config接口注入权限验证配置 步骤四:通过ready接口处理成功验证 步骤五:通过error接口处理失败验

  • ASP.NET MVC使用EPPlus,导出数据到Excel中

    好久没写博客了,今天特地来更新一下,今天我们要学习的是如何导出数据到Excel文件中,这里我使用的是免费开源的Epplus组件. 源代码下载:https://github.com/caofangsheng93/ExcelExportInMvc 介绍 这篇文章,介绍的是怎样导出数据到Excel文件中,大多数的后端程序都有报表功能:把显示在Grid中的数据导出到Excel文件中,这篇文章中使用的是EPPlus组件. EPPlus是一个基于OOXML[Open Extended Markup Lang

  • [Asp.Net MVC4]验证用户登录实现实例

    最近我们要做一个仿sina的微博,碰巧的是我最近在学习mvc,就想用mvc技术实现这个项目. 既然是微博,那不用想也应该知道肯定要有用户登陆,但是和常规的asp.NET登陆又不一样,以下是我一下午+一晚上的研究成果~~~ 首先,建好数据库以及表,这就不用说了吧. 下面说一下主要的结构 控制器: HomeController 这是主页的控制器 LoginController 这是登陆的控制器 类: CDBTemplate.cs 这是数据库数据对应的类,里边描述的是数据库的结构 //////////

  • ASP.NET Core MVC 配置全局路由前缀

    ASP.NET Core MVC 配置全局路由前缀 前言 大家好,今天给大家介绍一个 ASP.NET Core MVC 的一个新特性,给全局路由添加统一前缀.严格说其实不算是新特性,不过是Core MVC特有的. 应用背景 不知道大家在做 Web Api 应用程序的时候,有没有遇到过这种场景,就是所有的接口都是以 /api 开头的,也就是我们的api 接口请求地址是像这样的: http://www.example.com/api/order/333 或者是这样的需求 http://www.exa

  • ASP.NET MVC下的四种验证编程方式[续篇]

    在<ASP.NET MVC的四种验证编程方式>一文中我们介绍了ASP.NET MVC支持的四种服务端验证的编程方式("手工验证"."标注ValidationAttribute特性"."让数据类型实现IValidatableObject或者IDataErrorInfo"),那么在ASP.NET MVC框架内部是如何提供针对这四种不同编程方式的支持的呢?接下来我们就来聊聊这背后的故事. 一.ModelValidator与ModelVali

  • Asp.NET MVC中使用SignalR实现推送功能

    一.简介 Signal 是微软支持的一个运行在 Dot NET 平台上的 html websocket 框架.它出现的主要目的是实现服务器主动推送(Push)消息到客户端页面,这样客户端就不必重新发送请求或使用轮询技术来获取消息. 可访问其官方网站:https://github.com/SignalR/ 获取更多资讯. 二.Asp.net SignalR 是个什么东东 Asp.net SignalR是微软为实现实时通信的一个类库.一般情况下,SignalR会使用JavaScript的长轮询(lo

  • ASP.NET MVC阿里大于短信接口开发短信群发能

    互联网上有许多公司提供短信接口服务,诸如网易云信.阿里大于等等.我在自己项目里需要使用到短信服务起到通知作用,实际开发周期三天,完成配置.开发和使用,总的说,阿里大于提供的接口易于开发,非常的方便,短信费用是计数缴纳的,作为个人开发者,我使用的服务业务产生的费用为0.045¥/条(10万条以下). 现在要实现一个例会短信群发通知的功能,所有被通知对象信息均存于Mysql中,应用架构采用asp.net MVC .首先准备好获取的API各项(以下各项服务参数都需要在大于官网上申请), 申请好自己的短

  • ASP.NET MVC后台参数验证的几种方式

    前言 参数验证是一个常见的问题,无论是前端还是后台,都需对用户输入进行验证,以此来保证系统数据的正确性.对于web来说,有些人可能理所当然的想在前端验证就行了,但这样是非常错误的做法,前端代码对于用户来说是透明的,稍微有点技术的人就可以绕过这个验证,直接提交数据到后台.无论是前端网页提交的接口,还是提供给外部的接口,参数验证随处可见,也是必不可少的.总之,一切用户的输入都是不可信的. 参数验证有许多种方式进行,下面以mvc为例,列举几种常见的验证方式,假设有一个用户注册方法 [HttpPost]

  • Asp.net MVC中获取控制器的名称的方法

    1.视图中 string controller = ViewContext.RouteData.Route.GetRouteData(this.Context).Values["controller"].ToString(); string controller = ViewContext.RouteData.Values["controller"].ToString(); 2.控制器的action中 string controller = RouteData.Ro

  • 灵活掌握Asp.net MVC中GridView的使用方法

    本文教程为大家分享了GridView控件的使用方法和具体实现代码,供大家参考,具体内容如下 Models文件下实体类: public class Customer { public int Id { get; set; } public string CompanyName { get; set; } public string ContactTitle { get; set; } public string Address { get; set; } public string City {

  • ASP.NET MVC自定义错误页面真的简单吗?

    如果你在设置asp.net mvc自定义错误页面时遇到问题,这并不止你一个人.惊讶之余你的做法是正确的,没有起到作用的原因是其一部分错误是由asp.net管道处理的,另一部分是由iis直接处理. 通常情况 (我期望是这种情况,在一些其他框架/服务器上) 我们只需要在一个地方配置自定义错误页就可以了,无论怎么哪儿引发的错误.就像这样︰ <customErrors mode="On"> <error code="404" path="404.

随机推荐