[Abp 源码分析]DTO 自动验证

点击上方蓝字关注我们


0.简介

在平时开发 API 接口的时候需要对前端传入的参数进行校验之后才能进入业务逻辑进行处理,否则一旦前端传入一些非法/无效数据到 API 当中,轻则导致程序报错,重则导致整个业务流程出现问题。

用过传统 ASP.NET MVC 数据注解的同学应该知道,我们可以通过在 Model 上面指定各种数据特性,然后在前端调用 API 的时候就会根据这些注解来校验 Model 内部的字段是否合法。

1.启动流程

Abp 针对于数据校验分为两个地方进行,第一个是 MVC 的过滤器,也是我们最常使用的。第二个则是借助于 Castle 的拦截器实现的 DTO 数据校验功能,前者只能用于控制器方法,而后者则支持普通方法。

1.1 过滤器注入

在注入 Abp 的时候,通过 AddAbp() 方法内部的 ConfigureAspNetCore() 配置了诸多过滤器。

private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{// ... 其他代码//Configure MVCservices.Configure<MvcOptions>(mvcOptions =>{mvcOptions.AddAbp(services);});// ... 其他代码
}

过滤器注入方法:

internal static class AbpMvcOptionsExtensions
{public static void AddAbp(this MvcOptions options, IServiceCollection services){// ... 其他代码AddFilters(options);// ... 其他代码}// ... 其他代码private static void AddFilters(MvcOptions options){// ... 其他过滤器注入// 注入参数验证过滤器options.Filters.AddService(typeof(AbpValidationActionFilter));// ... 其他过滤器注入}// ... 其他代码
}

1.2 拦截器注入

Abp 针对于验证拦截器的注册始于 AbpBootstrapper 类,该基类在之前曾经多次出现过,也就是在用户调用 IServiceCollection.AddAbp<TStartupModule>() 方法的时候会初始化该类的一个实例对象。在该类的构造函数当中,会调用一个 AddInterceptorRegistrars() 方法用于添加各种拦截器的注册类实例。代码如下:

public class AbpBootstrapper : IDisposable
{private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null){// ... 其他代码if (!options.DisableAllInterceptors){AddInterceptorRegistrars();}}// ... 其他代码// 添加各种拦截器private void AddInterceptorRegistrars(){ValidationInterceptorRegistrar.Initialize(IocManager);AuditingInterceptorRegistrar.Initialize(IocManager);EntityHistoryInterceptorRegistrar.Initialize(IocManager);UnitOfWorkRegistrar.Initialize(IocManager);AuthorizationInterceptorRegistrar.Initialize(IocManager);}// ... 其他代码\
}

来到 ValidationInterceptorRegistrar 类型定义当中可以看到,其内部就是通过 Castle 的 IocContainer 来针对每次注入的应用服务应用上参数验证拦截器。

internal static class ValidationInterceptorRegistrar
{public static void Initialize(IIocManager iocManager){iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered;}private static void Kernel_ComponentRegistered(string key, IHandler handler){// 判断是否实现了 IApplicationService 接口,如果实现了,则为该对象添加拦截器if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation)){handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(ValidationInterceptor)));}}
}

2.代码分析

从 Abp 库代码当中我们可以知道其拦截器与过滤器是在何时被注入的,下面我们就来具体分析一下他们的处理逻辑。

2.1 过滤器代码分析

Abp 在框架初始化的时候就将 AbpValidationActionFilter 添加到 MVC 的配置当中,其自定义实现的拦截器实现了 IAsyncActionFilter 接口,也就是说当每次接口被调用的时候都会进入该拦截器的内部。

public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency
{// Ioc 解析器,用于解析各种注入的组件private readonly IIocResolver _iocResolver;// Abp 针对与 ASP.NET Core 的配置项,主要作用是判断用户是否需要检测控制器方法private readonly IAbpAspNetCoreConfiguration _configuration;public AbpValidationActionFilter(IIocResolver iocResolver, IAbpAspNetCoreConfiguration configuration){_iocResolver = iocResolver;_configuration = configuration;}public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){// ... 处理逻辑}
}

在内部首先是结合配置项判断用户是否禁用了 MVC Controller 的参数验证功能,禁用了则不进行任何操作。

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{// 判断是否禁用了控制器检测if (!_configuration.IsValidationEnabledForControllers || !context.ActionDescriptor.IsControllerAction()){await next();return;}// 针对应用服务增加一个验证完成标识using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Validation)){// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>()){validator.Object.Initialize(context);validator.Object.Validate();}await next();}
}

其实我们这里看到有一个 AbpCrossCuttingConcerns.Applying() 方法,那么该方法的作用是什么呢?

在这里我先大体讲述一下该方法的作用,该方法主要是向应用服务对象 (也就是继承了 ApplicationService 类的对象) 内部的 AppliedCrossCuttingConcerns 属性增加一个常量值,在这里也就是 AbpCrossCuttingConcerns.Validation 的值,也就是一个字符串。

那么其作用是什么呢,就是防止重复验证。从启动流程一节我们就已经知道 Abp 框架在启动的时候除了注入过滤器之外,还会注入拦截器进行接口参数验证,当过滤器验证过之后,其实没必要再使用拦截器进行二次验证。

所以在拦截器的 Intercept() 方法内部会有这样一句代码:

public void Intercept(IInvocation invocation)
{// 判断是否拥有处理过的标识if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)){invocation.Proceed();return;}// ... 其他代码
}

解释完 AbpCrossCuttingConcerns.Applying() 之后,我们继续往下看代码。

// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法
using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>())
{validator.Object.Initialize(context);validator.Object.Validate();
}await next();

这里就比较简单了,过滤器通过 IocResolver 解析出来了一个 MvcActionInvocationValidator 对象,使用该对象来校验具体的参数内容。

2.2 拦截器代码分析

看完过滤器代码之后,其实拦截器代码更加简单。整体逻辑上面与过滤器差不多,只不过针对于拦截器,它是通过一个 MethodInvocationValidator 对象来校验传入的参数内容。

public class ValidationInterceptor : IInterceptor
{// Ioc 解析器,用于解析各种注入的组件private readonly IIocResolver _iocResolver;public ValidationInterceptor(IIocResolver iocResolver){_iocResolver = iocResolver;}public void Intercept(IInvocation invocation){// 判断过滤器是否已经处理过if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)){// 处理过则直接进入具体方法内部,执行业务逻辑invocation.Proceed();return;}// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法using (var validator = _iocResolver.ResolveAsDisposable<MethodInvocationValidator>()){validator.Object.Initialize(invocation.MethodInvocationTarget, invocation.Arguments);validator.Object.Validate();}invocation.Proceed();}
}

可以看到两个过滤器与拦截器业务逻辑相似,但都是通过验证器来进行处理的,那么验证器又是个什么鬼东西呢?

2.3 参数验证器

验证器即是用来具体执行验证逻辑的工具,从上述代码里面我们可以看到过滤器和拦截器都是通过解析出 MethodInvocationValidator/MvcActionInvocationValidator 之后调用其验证方法进行验证的。

首先我们来看一下 MVC 的验证器是如何进行处理的,看方法类型的定义,可以看到其继承了一个基类,叫 ActionInvocationValidatorBase,而这个基类呢,又继承自 MethodInvocationValidator

public class MvcActionInvocationValidator : ActionInvocationValidatorBase
{// ... 其他代码
}
public abstract class ActionInvocationValidatorBase : MethodInvocationValidator
{// ... 其他代码
}

所以我们分析代码的顺序调整一下,先看一下 MethodInvocationValidator 的内部是如何做处理的吧,这个类型内部还是比较简单的,可能除了有一个递归有点绕之外。

其主要功能就是拿着传递进来的参数值,通过在 Abp 框架启动的时候注入的具体验证器(用户自定义验证器)来递归校验每个参数的值。

/// <summary>
/// 本类用于需要参数验证的方法.
/// </summary>
public class MethodInvocationValidator : ITransientDependency
{// 最大迭代验证次数private const int MaxRecursiveParameterValidationDepth = 8;// 待验证的方法信息protected MethodInfo Method { get; private set; }// 传入的参数值protected object[] ParameterValues { get; private set; }// 方法参数信息protected ParameterInfo[] Parameters { get; private set; }protected List<ValidationResult> ValidationErrors { get; }protected List<IShouldNormalize> ObjectsToBeNormalized { get; }private readonly IValidationConfiguration _configuration;private readonly IIocResolver _iocResolver;public MethodInvocationValidator(IValidationConfiguration configuration, IIocResolver iocResolver){_configuration = configuration;_iocResolver = iocResolver;ValidationErrors = new List<ValidationResult>();ObjectsToBeNormalized = new List<IShouldNormalize>();}// 初始化拦截器参数public virtual void Initialize(MethodInfo method, object[] parameterValues){Check.NotNull(method, nameof(method));Check.NotNull(parameterValues, nameof(parameterValues));Method = method;ParameterValues = parameterValues;Parameters = method.GetParameters();}// 开始验证参数的有效性public void Validate(){// 检测是否初始化,没有初始化则抛出系统级异常CheckInitialized();// 检测方法是否有参数if (Parameters.IsNullOrEmpty()){return;}// 检测方法是否为公开方法if (!Method.IsPublic){return;}// 如果没有开启方法参数检测,则直接返回if (IsValidationDisabled()){return;                }// 如果方法所定义的参数数量与传入的参数值数量匹配不上,则抛出系统级异常if (Parameters.Length != ParameterValues.Length){throw new Exception("Method parameter count does not match with argument count!");}// 遍历方法的参数列表,使用传入的参数值进行校验for (var i = 0; i < Parameters.Length; i++){ValidateMethodParameter(Parameters[i], ParameterValues[i]);}// 如果校验的错误结果集合有任意一条数据,则抛出用户异常,返回给前端展示if (ValidationErrors.Any()){ThrowValidationError();}foreach (var objectToBeNormalized in ObjectsToBeNormalized){objectToBeNormalized.Normalize();}}// ... 忽略的代码// 校验调用方法时传递的参数与参数值protected virtual void ValidateMethodParameter(ParameterInfo parameterInfo, object parameterValue){// 如果参数值为空的情况下,做一系列特殊判断if (parameterValue == null){if (!parameterInfo.IsOptional && !parameterInfo.IsOut && !TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType, includeEnums: true)){ValidationErrors.Add(new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name }));}return;}// 递归校验参数ValidateObjectRecursively(parameterValue, 1);}protected virtual void ValidateObjectRecursively(object validatingObject, int currentDepth){// 验证层级是否超过了最大层级(8)if (currentDepth > MaxRecursiveParameterValidationDepth){return;}// 值是否为空,为空则不继续进行校验if (validatingObject == null){return;}// 判断其类型是否是用户配置的忽略类型,忽略则不进行校验if (_configuration.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject))){return;}// 判断参数类型是否为基本类型if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObject.GetType())){return;}SetValidationErrors(validatingObject);// 判定参数类型是否实现了 IEnumerabe 接口,如果实现了,则递归遍历校验其内部的元素if (IsEnumerable(validatingObject)){foreach (var item in (IEnumerable) validatingObject){ValidateObjectRecursively(item, currentDepth + 1);}}// 如果实现了标准化接口,则进行标准化操作if (validatingObject is IShouldNormalize){ObjectsToBeNormalized.Add(validatingObject as IShouldNormalize);}// 是否还需要继续递归校验if (ShouldMakeDeepValidation(validatingObject)){var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();foreach (var property in properties){// 如果有禁止校验的特性则忽略if (property.Attributes.OfType<DisableValidationAttribute>().Any()){continue;}ValidateObjectRecursively(property.GetValue(validatingObject), currentDepth + 1);}}}// ... 其他代码protected virtual bool ShouldValidateUsingValidator(object validatingObject, Type validatorType){return true;}// 是否进行深度验证protected virtual bool ShouldMakeDeepValidation(object validatingObject){// 不需要递归集合对象if (validatingObject is IEnumerable){return false;}var validatingObjectType = validatingObject.GetType();// 不需要递归基础类型的对象if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType)){return false;}return true;}// ... 其他代码
}

有朋友可能会奇怪,在方法内部不是通过 IEnumerable 判断之后来进行递归校验么,为什么在最后面还有一个深度验证呢?

这是因为当前对象除了是一个集合的情况之外,还有可能其内部某个对象是另外一个用户所自定义的复杂对象,这个时候就必须要通过深度验证来校验各个参数的值。不过这个递归也是有限度的,通过 MaxRecursiveParameterValidationDepth 来控制这个迭代层数为 8 层。如果不加以限制的话,那么很有可能出现循环引用而产生死循环的情况,或者是层级过深导致接口相应缓慢。

那么在这里执行具体校验操作的则是那些实现了 IMethodParameterValidator 接口的对象,这些对象在 Abp 核心模块(AbpKernelModule)的预加载的时候被添加到了 Configuration.Validation.Validators 属性当中。

当然用户也可以在自己的模块预加载方法当中增加自己的参数验证器,只要实现该接口即可。

public sealed class AbpKernelModule : AbpModule
{public override void PreInitialize(){// ... 其他代码// 增加需要忽略的类型AddIgnoredTypes();// 增加参数校验器AddMethodParameterValidators();}private void AddMethodParameterValidators(){Configuration.Validation.Validators.Add<DataAnnotationsValidator>();Configuration.Validation.Validators.Add<ValidatableObjectValidator>();Configuration.Validation.Validators.Add<CustomValidator>();}// Abp 默认需要忽略的对象private void AddIgnoredTypes(){var commonIgnoredTypes = new[]{typeof(Stream),typeof(Expression)};foreach (var ignoredType in commonIgnoredTypes){Configuration.Auditing.IgnoredTypes.AddIfNotContains(ignoredType);Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);}var validationIgnoredTypes = new[] { typeof(Type) };foreach (var ignoredType in validationIgnoredTypes){Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);}}
}

之后呢,回到之前的校验方法,可以看到在 SetValidationErrors(object validatingObject) 方法里面遍历了之前被注入的验证器集合,然后调用其 Validate() 方法来进行具体的参数校验。

protected virtual void SetValidationErrors(object validatingObject)
{foreach (var validatorType in _configuration.Validators){if (ShouldValidateUsingValidator(validatingObject, validatorType)){using (var validator = _iocResolver.ResolveAsDisposable<IMethodParameterValidator>(validatorType)){var validationResults = validator.Object.Validate(validatingObject);ValidationErrors.AddRange(validationResults);}}}
}

2.4 具体的参数验证器

这里以 Abp 默认实现的 DataAnnotationValidator 类型为例,可以看看他是怎么来根据参数的数据注解来验证参数是否正确的。

public class DataAnnotationsValidator : IMethodParameterValidator
{public virtual IReadOnlyList<ValidationResult> Validate(object validatingObject){return GetDataAnnotationAttributeErrors(validatingObject);}protected virtual List<ValidationResult> GetDataAnnotationAttributeErrors(object validatingObject){var validationErrors = new List<ValidationResult>();var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();// 获得参数值的所有属性,如果传入的是一个 DTO 对象的话,他内部肯定会有很多属性的foreach (var property in properties){var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();// 没有数据注解特性,跳过当前属性处理if (validationAttributes.IsNullOrEmpty()){continue;}// 创建一个错误信息上下文,用户数据注解工具进行校验var validationContext = new ValidationContext(validatingObject){DisplayName = property.DisplayName,MemberName = property.Name};// 根据特性来校验参数结果foreach (var attribute in validationAttributes){var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext);if (result != null){validationErrors.Add(result);}}}return validationErrors;}
}

3. 后记

最近工作较忙,可能更新速度不会像原来那么快,不过我尽可能在国庆结束后完成剩余文章,谢谢大家的支持。

作者:myzony

出处:https://www.cnblogs.com/myzony/p/9716742.html

公众号“码侠江湖”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

扫描二维码

获取更多精彩

码侠江湖


喜欢就点个在看再走吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305025.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

五分钟搞懂并查集

并查集是我暑假从高手那里学到的一招&#xff0c;觉得真是太精妙的设计了。来看一个实例&#xff0c;杭电1232畅通工程。首先在地图上给你若干个城镇&#xff0c;这些城镇都可以看作点&#xff0c;然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问…

[Abp 源码分析]多语言(本地化)处理

点击上方蓝字关注我们0.简介如果你所开发的需要走向世界的话&#xff0c;那么肯定需要针对每一个用户进行不同的本地化处理&#xff0c;有可能你的客户在日本&#xff0c;需要使用日语作为显示文本&#xff0c;也有可能你的客户在美国&#xff0c;需要使用英语作为显示文本。如…

Kaggle 发布首份数据科学从业报告 | 不及美国同行1/3,中国数据科学家平均年薪约3万美元

Kaggle 是互联网上最著名的数据科学竞赛平台之一&#xff0c;今年 3 月 8 日&#xff0c;这家机构被谷歌收购&#xff0c;6 月 6 日又宣布用户数量超过了 100 万人。互联网创业方兴未艾&#xff0c;人工智能的浪潮又接踵而来&#xff0c;而贯穿其中的数据科学则在这更迭交替中显…

mysql设置表名字为占位符_这可能是把MySQL存储引擎讲解的最清楚的一篇文章了

存储引擎是MySQL的组件&#xff0c;用于处理不同表类型的SQL操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能&#xff0c;使用不同的存储引擎&#xff0c;还可以获得特定的功能。使用哪一种引擎可以灵活选择&#xff0c;一个数据库中多个表可以使用不同引擎…

linux开发板显示横向彩虹,给 Linux 终端的输出添加彩虹特效的命令

原标题&#xff1a;给 Linux 终端的输出添加彩虹特效的命令正文如果认为Linux命令行很无聊并且没有任何乐趣&#xff0c;那么您错了&#xff0c;真实的Linux多么有趣和淘气。在本文&#xff0c;我将介绍一个名为“ lolcat ”的实用小工具&#xff0c;该实用工具在终端中产生彩虹…

Delphi 与 DirectX 之 DelphiX(46): TDIB.DoAntiAlias;

本例效果图:代码文件:unit Unit1;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, DIB, StdCtrls;typeTForm1 class(TForm)DXPaintBox1: TDXPaintBox;Button1: TButton;Button2: TButton;procedure Button1Click(Sender: T…

Web API实现微信公众平台开发-服务器验证

背景最近开发微信公众号相关接口&#xff0c;在这里记录下微信公众号相关各项功能的实现。先决条件1、一台可部署web服务的服务器或者云平台&#xff08;本地可以搞个花生壳域名&#xff09;。2、一个可以正常使用的微信公众账号&#xff0c;开始的时候使用它的测试号。3、Visu…

每天20分钟,只需一年,一年级学生英语听力达到六年级水平!关键是坚持一点都不难!

导读&#xff1a; 除了语文数学这种常规科目&#xff0c;最让家长们焦虑的就是英语。现在的孩子&#xff0c;英语启蒙都很早&#xff0c;但是对英语的兴趣总是开始还可以&#xff0c;越往后越没动力和兴趣&#xff0c;稍微遇到点挫折就不想坚持了。钱也花了&#xff0c;好老师也…

python爬取论坛付费内容_Python爬虫抓取论坛关键字过程解析

前言&#xff1a; 之前学习了用python爬虫的基本知识&#xff0c;现在计划用爬虫去做一些实际的数据统计功能。由于前段时间演员的诞生带火了几个年轻的实力派演员&#xff0c;想用爬虫程序搜索某论坛中对于某些演员的讨论热度&#xff0c;并按照日期统计每天的讨论量。 这个项…

相对完善的Java通过JDBC操纵mysql的例子

工具类: Code1import java.sql.Connection; 2import java.sql.DriverManager; 3import java.sql.ResultSet; 4import java.sql.SQLException; 5import java.sql.Statement; 6 7 8public final class JDBCUtils { 910 private JDBCUtils(){}11 12 private static Strin…

如何在 ASP.Net Core 中使用 File Providers

ASP.Net Core 为了便于获取文件和文件夹信息&#xff0c;监视文件变更&#xff0c; 在文件系统中提供了一个抽象层&#xff1a;File Providers&#xff0c; 这篇文章将会讨论如何使用 File Providers 。File Provider 抽象层 file prodivers 实现了 IFileProvider 接口&#xf…

对5种主流编程语言的吐槽

不可否认&#xff0c;想要成为一名优秀的程序员确实是需要掌握多种编程语言。通过这几年的自虐式学习&#xff0c;小编也慢慢的掌握了这些编程语言。接下来要为大家&#xff0c;介绍五款让人又爱又恨的编程语言&#xff01; 1.C 语言 C 语言给人的感觉&#xff0c;就是一位神秘…

WM中的OutLook开发和操作

昨天闲来无视&#xff0c;学习了一下WM的基本开发。看WM有约的那套教程心里痒痒&#xff0c;于是下载了SDK&#xff0c;看看DEMO&#xff0c;在Sample中的示例进行加工。小有一点心得。其实总的来说难度也不是很大&#xff0c;以前没有做过FORM的程序&#xff0c;都是WEB上面的…

苹果手机运行python_iPhone是卖的最好的手机?用Python照样把他玩弄鼓掌之间!

关于 iOS 的技术解读有很多&#xff0c;但是却鲜有设备可视化同步的介绍文章。本文一起了解下这个酷炫的 iOS 黑科技。我们的任务很简单——如上图所示&#xff0c;实时获取设备的当前方向。 UIDevice.current.orientation 首先&#xff0c;需要调用 beginGeneratingDeviceOrie…

这几个动图告诉你科学的神奇,看完瞬间觉得智商都提高了

生活中简单平常的事物和现象背后&#xff0c;往往有着奇妙的原理&#xff0c;赶快跟着一起来看看涨点知识吧&#xff01; 夹心雪糕的制作原理 ▼ 难怪雪糕大小&#xff0c;厚度都一模一样 原来都是从一个模子里出来的 ▼ 煎饼可以统一翻面 再也不用担心烤焦了 ▼ 冰淇淋蛋筒的制…

自定义 ocelot 中间件输出自定义错误信息

自定义 ocelot 中间件输出自定义错误信息Introocelot 中默认的 Response 中间件在出错的时候只会设置 StatusCode 没有具体的信息&#xff0c;想要展示自己定义的错误信息的时候就需要做一些自定义了&#xff0c;对 ocelot 中的 Response 中间件做了一些小改动&#xff0c;实现…

盘点小坏蛋的礼物

我们家小坏蛋一天天长大了&#xff0c;妈妈从觉得很辛苦过渡到习惯了很辛苦&#xff0c;苦中作乐的妈妈终于抽出空来写博客啦&#xff01; 为什么叫他小坏蛋呢&#xff1f;因为他吃饭不乖。没满月的时候蛮乖的&#xff0c;每顿奶都吃的很香&#xff0c;咕咚咕咚的喝下去&#x…

为什么对gRPC做负载均衡会很棘手?

在过去的几年中&#xff0c;随着微服务的增长&#xff0c;gRPC在这些较小的服务之间的相互通信中获得了很大的普及,在后台&#xff0c;gRPC使用http/2在同一连接和双工流中复用许多请求。使用具有结构化数据的快速&#xff0c;轻便的二进制协议作为服务之间的通信介质确实很有吸…

给新手程序猿的16个必备小妙招

写在前面&#xff1a; 这个文章核心并不是程序优化的具体技巧&#xff0c;而是拿到一个问题如何思考和利用工具的通用方法。比如即使我们不知道 profiler 这个东西&#xff0c;通过搜索"代码 每一行 时间"也可以很快知道有这样的工具叫做 profiler&#xff0c;并且学…

python字符串设置字体_python怎么更改字符串后几位

python更改字符串后几位的方法&#xff1a;可以利用replace()函数来实现。replace()函数可以把字符串中的旧字符串替换成新字符串&#xff0c;并返回替换后的新字符串。具体使用方法如&#xff1a;【str.replace("is", "was")】。可以利用replace()函数来修…