• 跟我一起学.NetCore之中间件(Middleware)应用和自定义


    前言

    Asp.NetCore中的请求管道是通过一系列的中间件组成的,使得请求会根据需求进行对应的过滤和加工处理。在平时开发中会时常引用别人定义好的中间件,只需简单进行app.Usexxx就能完成中间件的注册,但是对于一些定制化需求还得自己进行处理和封装,以下说说中间件的注册应用和自定义中间件;

    正文

    在上一小节中有简单提到,当注册第三方封装的中间件时,其实本质还是调用了IApplicationBuilder的Use方法;而在开发过程中,会使用以下三种方式进行中间件的注册:

    • Use:通过Use的方式注册中间件,可以控制是否将请求传递到下一个中间件;
    • Run:通过Run的方式注册中间件,一般用于断路或请求管道末尾,即不会将请求传递下去;
    • Map/MapWhen:请求管道中增加分支,条件满足之后就由分支管道进行处理,而不会切换回主管道;Map用于请求路径匹配,而MapWhen可以有更多的条件进行过滤;
    • UseMiddleWare : 一般用于注册自定义封装的中间件,内部其实是使用Use的方式进行中间件注册;

    相信都知道我的套路了,光说不练假把式,来一个Asp.NetCore API项目进行以上几种中间件注册方式演示:

    img

    图中代码部分将原先默认注册的中间件删除了,用Use和Run的方式分别注册了两个中间件(这里只是简单的显示文字,里面可以根据需求添加相关逻辑),其中用Use注册的方式在上一节中已经提及到,直接将中间件添加链表中,这里就不再赘述了;

    对于使用Run方式注册中间,小伙伴们肯定不甘心止于此吧,所以这里直接看Run是如何实现:

    namespace Microsoft.AspNetCore.Builder
    {
        public static class RunExtensions
        {
            // 也是一个扩展方法,但参数就是一个委托
            public static void Run(this IApplicationBuilder app, RequestDelegate handler)
            {
                // 参数校验,如果null就抛出异常
                if (app == null)
                {
                    throw new ArgumentNullException(nameof(app));
                }
                // 传入的委托校验,如果null也是抛出异常
                if (handler == null)
                {
                    throw new ArgumentNullException(nameof(handler));
                }
                // 这里其实只有一个 RequestDelegate执行逻辑,并没有传递功能
                // 本质也是使用方法Use
                app.Use(_ => handler);
            }
        }
    }
    

    通过代码可知,用Run方式只是将处理逻辑RequestDelegate传入,并没有传递的逻辑,所以Run注册的中间件就会形成断路,导致后面的中间件不能再执行了;

    使用Map和MapWhen注册的方式,其实是给管道开一个分支,就像高速公路一样,有匝道,到了对应出口就进匝道了,就不能倒车回来了(倒回来扣你12分);同样,请求管道也是,当条件满足时,请求就走Map对应的分支管道,就不能重新返回主管道了;

    img

    代码走一波,在注册中间件的地方增加Map的使用:

    img

    Configure全部代码如下:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 使用Use注册中间 
        app.Use(async (context, next) => {
            await context.Response.WriteAsync("Hello Use1
    ");
            // 将请求传递到下一个中间件
            await next();
    ​
            await context.Response.WriteAsync("Hello Use1 Response
    ");
        });
    ​
        // 使用Use注册中间   参数类型不一样
        app.Use(requestDelegate =>
        {
            return async (context) =>
            {
                await context.Response.WriteAsync("Hello Use2
    ");
                // 将请求传递到下一个中间件
                await requestDelegate(context);
    ​
                await context.Response.WriteAsync("Hello Use2 Response
    ");
            };
        });
        // 分支管道,只有匹配到路径才走分支管道
        app.Map("/Hello", builder =>
        {
            builder.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("Hello MapUse
    ");
                // 将请求传递到分支管道的下一个中间件
                await next();
    ​
                await context.Response.WriteAsync("Hello MapUse Response
    ");
            });
            // 注册分支管道中间件
            builder.Run(async context => {
                await context.Response.WriteAsync("Hello MapRun1~~~
    ");
            });
            // 注册分支管道中间件
            builder.Run(async context => {
                await context.Response.WriteAsync("Hello MapRun2~~~
    ");
            });
        });
    ​
         // 使用Run 
         app.Run(async context => {
             await context.Response.WriteAsync("Hello Run~~~
    ");
         });
    ​
         //使用Run注册
         app.Run(async context => {
             await context.Response.WriteAsync("Hello Code综艺圈~~~
    ");
         });
    }
    

    执行看效果:

    img

    Map方式注册的分支管道只有路径匹配了才走,否则都会走主管道;

    仔细的小伙伴肯定会说,那是在分支管道上用了Run注册中间件了,形成了断路,所以导致不能执行主管道剩下的中间件,好,那我们稍微改改代码:

    img

    这样运行访问分支管道时会报错,因为分支管道中没有下一个中间件了,还调用下一个中间件,那肯定有问题;

    img

    改了改,如下运行:

    img

    进入匝道还想倒回来,12分不要了吗,哈哈哈;

    MapWhen注册的分支管道逻辑和Map差不多类似,只是匹配的条件更加灵活而已,可以根据自己需求进行调节匹配,如下:

    img

    看到这,小伙伴应该都知道,接下来肯定不会放过Map/MapWhen的实现:

    • Map

      public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)
      {
          // 进行参数校验 IApplicationBuilder对象不能为空
          if (app == null)
          {
              throw new ArgumentNullException(nameof(app));
          }
          // 进行参数校验 传进的委托对象不能为空
          if (configuration == null)
          {
              throw new ArgumentNullException(nameof(configuration));
          }
          // 匹配的路径末尾不能有"/",否则就抛异常
          if (pathMatch.HasValue && pathMatch.Value.EndsWith("/", StringComparison.Ordinal))
          {
              throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch));
          }
      ​
          // 克隆一个IApplicationBuilder,共用之前的属性,这里其实创建了分支管道
          var branchBuilder = app.New();
          // 将创建出来的branchBuilder进行相关配置
          configuration(branchBuilder);
          // 构造出分支管道
          var branch = branchBuilder.Build();
          // 将构造出来的管道和匹配路径进行封装
          var options = new MapOptions
          {
              Branch = branch,
              PathMatch = pathMatch,
          };
          // 注册中间件
          return app.Use(next => new MapMiddleware(next, options).Invoke);
      }
      ​
      // MapMiddleware 的Invoke方法,及如何进入分支管道处理的
      public async Task Invoke(HttpContext context)
      {
          // 参数判断
          if (context == null)
          {
              throw new ArgumentNullException(nameof(context));
          }
      ​
          PathString matchedPath;
          PathString remainingPath;
      ​
          // 判断是否匹配路径,如果匹配上就进入分支
          if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath))
          {
              // 更新请求地址
              var path = context.Request.Path;
              var pathBase = context.Request.PathBase;
              context.Request.PathBase = pathBase.Add(matchedPath);
              context.Request.Path = remainingPath;
      ​
              try
              {
                  // 进入分支管道
                  await _options.Branch(context);
              }
              finally
              {
                  // 恢复原先请求地址,回到主管道之后,并没有进行主管道也下一个中间件的传递,所以主管道后续不在执行
                  context.Request.PathBase = pathBase;
                  context.Request.Path = path;
              }
          }
          else
          {
              // 匹配不到路径就继续主管道执行
              await _next(context);
          }
      }
      
    • MapWhen:其实和Map差不多,只是传入的匹配规则不一样,比较灵活:

      public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration)
      {
          if (app == null)
          {
              throw new ArgumentNullException(nameof(app));
          }
      ​
          if (predicate == null)
          {
              throw new ArgumentNullException(nameof(predicate));
          }
      ​
          if (configuration == null)
          {
              throw new ArgumentNullException(nameof(configuration));
          }
      ​
          // 构建分支管道,和Map一致
          var branchBuilder = app.New();
          configuration(branchBuilder);
          var branch = branchBuilder.Build();
      ​
          // 封装匹配规则
          var options = new MapWhenOptions
          {
              Predicate = predicate,
              Branch = branch,
          };
          // 注册中间件
          return app.Use(next => new MapWhenMiddleware(next, options).Invoke);
      }
      // MapWhenMiddleware 的Invoke方法
      public async Task Invoke(HttpContext context)
      {
          // 参数校验
          if (context == null)
          {
              throw new ArgumentNullException(nameof(context));
          }
          // 判断是否匹配规则,如果匹配就进入分支管道
          if (_options.Predicate(context))
          {
              await _options.Branch(context);
          }
          else
          {
              // 没有匹配就继续执行主管道
              await _next(context);
          }
      }
      

    现在是不是清晰明了多了,不懵了吧;还没完呢,继续往下;

    上面注册中间件的方式是不是有点不那么好看,当中间件多了时候,可读性很是头疼,维护性也得花点功夫,所以微软肯定想到这了,提供了类的方式进行中间件的封装(但是要按照约定来),从而可以像使用第三方中间件那样简单,如下:

    img

    使用及运行:

    img

    是不是自定义也没想象中那么难,其中注册封装的中间件时,在扩展方法中使用了app.UseMiddleware()进行注册,这个上一节中提到过,就是那段在上一节中有点嫌早的代码,这里就拷过来了(偷个懒):

    // 看着调用的方法
    public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
    {
        // 内部调用了以下方法
        return app.UseMiddleware(typeof(TMiddleware), args);
    }
    // 其实这里是对自定义中间件的注册,这里可以不用太深入了解
    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
    {
        if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
        {
            // IMiddleware doesn't support passing args directly since it's
            // activated from the container
            if (args.Length > 0)
            {
                throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
            }
    ​
            return UseMiddlewareInterface(app, middleware);
        }
        // 取得容器
        var applicationServices = app.ApplicationServices;
        // 反编译进行包装成注册中间件的样子(Func<ReuqestDelegate,RequestDelegate>),但可以看到本质使用IApplicationBuilder中Use方法
        return app.Use(next =>
        {
            // 获取指定类型中的方法列表
            var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
            // 找出名字是Invoke或是InvokeAsync的方法
            var invokeMethods = methods.Where(m =>
                string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
                || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
                ).ToArray();
            // 如果有多个方法 ,就抛出异常,这里保证方法的唯一
            if (invokeMethods.Length > 1)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
            }
            // 如果没有找到,也就抛出异常
            if (invokeMethods.Length == 0)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
            }
            // 取得唯一的方法Invoke或是InvokeAsync方法
            var methodInfo = invokeMethods[0];
            // 判断类型是否返回Task,如果不是就抛出异常,要求返回Task的目的是为了后续包装RequestDelegate
            if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
            }
            // 判断方法的参数,参数的第一个参数必须是HttpContext类型
            var parameters = methodInfo.GetParameters();
            if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
            }
    ​
            // 开始构造RequestDelegate对象
            var ctorArgs = new object[args.Length + 1];
            ctorArgs[0] = next;
            Array.Copy(args, 0, ctorArgs, 1, args.Length);
            // 这里找到参数匹配最多的构造函数进行实例创建
            var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
            // 如果参数只有一个HttpContext 就包装成一个RequestDelegate返回
            if (parameters.Length == 1)
            {
                return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
            }
            // 如果参数有多个的情况就单独处理,这里不详细进去了
            var factory = Compile<object>(methodInfo, parameters);
    ​
            return context =>
            {
                var serviceProvider = context.RequestServices ?? applicationServices;
                if (serviceProvider == null)
                {
                    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
                }
    ​
                return factory(instance, context, serviceProvider);
            };
        });
    }
    

    可以看出,框架将我们封装的中间件类进行了反射获取对应的方法和属性,然后封装成中间件(Func<RequestDelegate,RequestDelegate>)的样子,从而是得编码更加方便,中间件更容易分类管理了;通过以上代码注释也能看出在封装中间件的时候对应的约定,哈哈哈,是不是得重新看一遍代码(如果这样,目标达到了);对了,框架提供了IMiddleware了接口,实现中间件的时候可以实现,但是约定还是一个不能少;

    总结

    我去,不能熬了,再熬明天起不来跑步了;这篇内容有点多,之所以没分开,感觉关联性比较强,一口气看下来比较合适;下一节说说文件相关的点;

    ---------------------------------------------------

    CSDN:Code综艺圈

    知乎:Code综艺圈

    掘金:Code综艺圈

    博客园:Code综艺圈

    bilibili:Code综艺圈

    ---------------------------------------------------

    一个被程序搞丑的帅小伙,关注"Code综艺圈",识别关注跟我一起学~~~

    img

    撸文不易,莫要白瞟,三连走起~~~~

  • 相关阅读:
    JAVA基础——编程练习(二)
    JAVA基础——面向对象三大特性:封装、继承、多态
    JVM内存
    50. Pow(x, n) (JAVA)
    47. Permutations II (JAVA)
    46. Permutations (JAVA)
    45. Jump Game II (JAVA)
    43. Multiply Strings (JAVA)
    42. Trapping Rain Water (JAVA)
    41. First Missing Positive (JAVA)
  • 原文地址:https://www.cnblogs.com/zoe-zyq/p/13631614.html
Copyright © 2020-2023  润新知