诊断日志知多少 | DiagnosticSource 在.NET上的应用

1. 引言

最近为了解决ABP集成CAP时无法通过拦截器启用工作单元的问题,从小伙伴那里学了一招。借助DiagnossticSource,可以最小改动完成需求。关于DiagnosticSource晓东大佬18年在文章 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 记录跟踪信息就有介绍,文章开头就说明了Diagnostics 一直是一个被大多数开发者忽视的东西。是的,我也忽略了,这个好东西,有必要学习一下,下面就和大家简单聊一聊System.Diagnostics.DiagnosticSource在.NET上的应用。

2. System.Diagnostics.DiagnosticSource

Diagnostics位于System命名空间下,由此可见Diagnostics在.NET 运行时中的地位不可小觑。其中System.Diagnostics命名空间下又包含不同类库,提供了允许与系统进程,事件日志和性能计数器进行交互的类。如下图所示:

Diagnostic Namespace

其中System.Diagnostics.DiagnosticSource模块,它允许对代码进行检测,以在生产时记录丰富的数据负载(可以传递不可序列化的数据类型),以便在进程内进行消耗。消费者可以在运行时动态发现数据源并订阅感兴趣的数据源。

在展开之前,有必要先梳理下涉及的以下核心概念:

  • IObservable:可观测对象

  • IObserver:观察者

  • DiagnosticSource :诊断来源

  • DiagnosticListener:诊断监听器

  • Activity:活动

3. 观察者模式(IObservable & IObserver)

IObservableIObserver位于System命名空间下,是.NET中对观察者模式的抽象。

观察者设计模式使观察者能够从可观察对象订阅并接收通知。 它适用于需要基于推送通知的任何方案。 此模式定义可观察对象,以及零个、一个或多个观察者。 观察者订阅可观察对象,并且每当预定义的条件、事件或状态发生更改时,该可观察对象会通过调用其方法之一来自动通知所有观察者。 在此方法调用中,该可观察对象还可向观察者提供当前状态信息。 在 .NET Framework 中,通过实现泛型 System.IObservable 和 System.IObserver 接口来应用观察者设计模式。泛型类型参数表示提供通知信息的类型。 泛型类型参数表示提供通知信息的类型。

第一次学习观察者模式,应该是大学课本中基于事件烧水的例子,咱们就基于此实现个简单的Demo吧。首先执行dotnet new web -n Dotnet.Diagnostic.Demo创建示例项目。

3.1. 定义可观察对象(实现IObservable接口)

对于烧水的示例,主要关注水温的变化,因此先定义Temperature来表示温度变化:

public class Temperature
{public Temperature(decimal temperature, DateTime date){Degree = temperature;Date = date;}public decimal Degree { get;  }public DateTime Date { get;  }
}

接下来通过实现IObservable<T>接口来定义可观察对象。

public interface IObservable<out T>
{IDisposable Subscribe(IObserver<T> observer);
}

从接口申明来看,只定义了一个Subscribe方法,从观察者模式讲,观察者应该既能订阅又能取消订阅消息。为什么没有定义一个UnSubscribe方法呢?其实这里方法申明已经说明,期望通过返回IDisposable对象的Dispose方法来达到这个目的。

/// <summary>
/// 热水壶
/// </summary>
public class Kettle : IObservable<Temperature>
{private List<IObserver<Temperature>> observers;private decimal temperature = 0;public Kettle(){observers = new List<IObserver<Temperature>>();}public decimal Temperature{get => temperature;private set{temperature = value;observers.ForEach(observer => observer.OnNext(new Temperature(temperature, DateTime.Now)));if (temperature == 100)observers.ForEach(observer => observer.OnCompleted());}}public IDisposable Subscribe(IObserver<Temperature> observer){if (!observers.Contains(observer)){Console.WriteLine("Subscribed!");observers.Add(observer);}//使用UnSubscriber包装,返回IDisposable对象,用于观察者取消订阅return new UnSubscriber<Temperature>(observers, observer);}/// <summary>/// 烧水方法/// </summary>public async Task StartBoilWaterAsync(){var random = new Random(DateTime.Now.Millisecond);while (Temperature < 100){Temperature += 10;await Task.Delay(random.Next(5000));}}
}//定义泛型取消订阅对象,用于取消订阅
internal class UnSubscriber<T> : IDisposable
{private List<IObserver<T>> _observers;private IObserver<T> _observer;internal UnSubscriber(List<IObserver<T>> observers, IObserver<T> observer){this._observers = observers;this._observer = observer;}public void Dispose(){if (_observers.Contains(_observer)){Console.WriteLine("Unsubscribed!");_observers.Remove(_observer);}}
}

以上代码中List<IObserver>存在线程安全问题,因为简单Demo,就不予优化了。

3.2. 定义观察者(实现IObserver接口)

比如定义一个报警器,实时播报温度。

public class Alter : IObserver<Temperature>
{public void OnCompleted(){Console.WriteLine("du du du !!!");}public void OnError(Exception error){//Nothing to do}public void OnNext(Temperature value){Console.WriteLine($"{value.Date.ToString()}: Current temperature is {value.Degree}.");}
}

添加测试代码,访问localhost:5000/subscriber控制台输出结果如下:

endpoints.MapGet("/subscriber", async context =>
{var kettle = new Kettle();//初始化热水壶var subscribeRef = kettle.Subscribe(new Alter());//订阅var boilTask = kettle.StartBoilWaterAsync();//启动开始烧水任务var timoutTask = Task.Delay(TimeSpan.FromSeconds(15));//定义15s超时任务//等待,如果超时任务先返回则取消订阅var firstReturnTask = await Task.WhenAny(boilTask, timoutTask);if (firstReturnTask == timoutTask)subscribeRef.Dispose();await context.Response.WriteAsync("Hello subscriber!");
});------------------------------------------------------------------Subscribed!
10/2/2020 4:53:20 PM: Current temperature is 10.
10/2/2020 4:53:20 PM: Current temperature is 20.
10/2/2020 4:53:21 PM: Current temperature is 30.
10/2/2020 4:53:21 PM: Current temperature is 40.
10/2/2020 4:53:24 PM: Current temperature is 50.
10/2/2020 4:53:25 PM: Current temperature is 60.
10/2/2020 4:53:26 PM: Current temperature is 70.
10/2/2020 4:53:30 PM: Current temperature is 80.
Unsubscribed!

4. DiagnosticSource & DiagnosticListener

4.1. 概念讲解

DiagnosticSource直译就是诊断源,也就是它是诊断日志的来源入口。DiagnosticSource是一个抽象类主要定义了以下方法:

//Provides a generic way of logging complex payloads
public abstract void Write(string name, object value);
//Verifies if the notification event is enabled.
public abstract bool IsEnabled(string name);

DiagnosticListener直译就是诊断监听器,继承自DiagnosticSource,同时实现了IObservable<KeyValuePair<string, object>>接口,因此其本质是一个可观察对象。小结以下:

  1. DiagnosticSource 作为诊断日志来源,提供接口,用于写入诊断日志。

  2. 诊断日志的可观察数据类型为KeyValuePair<string, object>

  3. DiagnosticListener 继承自DiagnosticSource,作为可观察对象,可由其他观察者订阅,以获取诊断日志。

DiagnosticListener 其构造函数接收一个name参数。

private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");

可以通过下面这种方式记录诊断日志:

if (httpLogger.IsEnabled("RequestStart"))httpLogger.Write("RequestStart", new { Url="http://clr", Request=aRequest });

然后需要实现IObserver<KeyValuePair<string, object>>接口,以便消费诊断数据。定义DiagnosticObserver,进行诊断日志消费:

public class DiagnosticObserver : IObserver<KeyValuePair<string, object>>
{public void OnCompleted(){//Noting to do}public void OnError(Exception error){Console.WriteLine($"{error.Message}");}public void OnNext(KeyValuePair<string, object> pair){ // 这里消费诊断数据Console.WriteLine($"{pair.Key}-{pair.Value}");}
}

ASP.NET Core 项目中默认就依赖了System.Diagnostics.DiagnosticSourceNuget包,同时在构建通用Web主机时,就注入了名为Microsoft.AspNetCoreDiagnosticListener

//GenericWebHostBuilder.cs
DiagnosticListener instance = new DiagnosticListener("Microsoft.AspNetCore");
services.TryAddSingleton<DiagnosticListener>(instance);
services.TryAddSingleton<DiagnosticSource>((DiagnosticSource) instance);

因此我们可以直接通过注入DiagnosticListener进行诊断日志的订阅:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DiagnosticListener diagnosticListener)
{diagnosticListener.Subscribe(new DiagnosticObserver());//订阅诊断日志
}

当然也可以直接使用DiagnosticListener.AllListeners.Subscribe(IObserver<DiagnosticListener> observer);进行订阅,不过区别是,接收的参数类型为IObserver<DiagnosticListener>

运行项目输出:

Microsoft.AspNetCore.Hosting.HttpRequestIn.Start-Microsoft.AspNetCore.Http.DefaultHttpContext
Microsoft.AspNetCore.Hosting.BeginRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300014352 }
Microsoft.AspNetCore.Routing.EndpointMatched-Microsoft.AspNetCore.Http.DefaultHttpContext
Microsoft.AspNetCore.Hosting.EndRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300319214 }
Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop-Microsoft.AspNetCore.Http.DefaultHttpContext

从中可以看出,ASP.NET Core Empty Web Project在一次正常的Http请求过程中分别在请求进入、请求处理、路由匹配都埋了点,除此之外还有请求异常、Action处理都有埋点。因此,根据需要,可以实现比如请求拦截、耗时统计等系列操作。

4.2. 耗时统计

基于以上知识,下面尝试完成一个简单的耗时统计。从上面的内容可知,ASP.NET Core在BeginRequest和EndRequest返回的诊断数据类型如下所示:

Microsoft.AspNetCore.Hosting.BeginRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300014352 }
Microsoft.AspNetCore.Hosting.EndRequest-{ httpContext = Microsoft.AspNetCore.Http.DefaultHttpContext, timestamp = 7526300319214 }

因此只要拿到两个timestamp就可以直接计算耗时,修改DiagnosticObserverOnNext方法如下:

private ConcurrentDictionary<string, long> startTimes = new ConcurrentDictionary<string, long>();
public void OnNext(KeyValuePair<string, object> pair)
{//Console.WriteLine($"{pair.Key}-{pair.Value}");//获取httpContextvar context = pair.Value.GetType().GetTypeInfo().GetDeclaredProperty("httpContext")?.GetValue(pair.Value) as DefaultHttpContext;//获取timestampvar timestamp = pair.Value.GetType().GetTypeInfo().GetDeclaredProperty("timestamp")?.GetValue(pair.Value) as long?;switch (pair.Key){case "Microsoft.AspNetCore.Hosting.BeginRequest":Console.WriteLine($"Request {context.TraceIdentifier} Begin:{context.Request.GetUri()}");startTimes.TryAdd(context.TraceIdentifier, timestamp.Value);//记录请求开始时间break;case "Microsoft.AspNetCore.Hosting.EndRequest":startTimes.TryGetValue(context.TraceIdentifier, out long startTime);var elapsedMs = (timestamp - startTime) / TimeSpan.TicksPerMillisecond;//计算耗时Console.WriteLine($"Request {context.TraceIdentifier} End: Status Code is {context.Response.StatusCode},Elapsed {elapsedMs}ms");startTimes.TryRemove(context.TraceIdentifier, out _);break;}
}

输出如下,大功告成:

Request 0HM37UNERKGF0:00000001 Begin:https://localhost:44330
Request 0HM37UNERKGF0:00000001 End: Status Code is 200,Elapsed 38ms

上面有通过反射去获取诊断数据属性的代码(var timestamp = pair.Value.GetType().GetTypeInfo().GetDeclaredProperty("timestamp") ?.GetValue(pair.Value) as long?;),非常不优雅。但我们可以安装**Microsoft.Extensions.DiagnosticAdapter**包来简化诊断数据的消费。安装后,添加HttpContextDiagnosticObserver,通过添加DiagnosticName指定监听的诊断名称,即可进行诊断数据消费。

public sealed class HttpContextDiagnosticObserver
{private ConcurrentDictionary<string, long> startTimes = new ConcurrentDictionary<string, long>();[DiagnosticName("Microsoft.AspNetCore.Hosting.BeginRequest")]public void BeginRequest(HttpContext httpContext,long timestamp){Console.WriteLine($"Request {httpContext.TraceIdentifier} Begin:{httpContext.Request.GetUri()}");startTimes.TryAdd(httpContext.TraceIdentifier, timestamp);//记录请求开始时间}[DiagnosticName("Microsoft.AspNetCore.Hosting.EndRequest")]public void EndRequest(HttpContext httpContext,long timestamp){startTimes.TryGetValue(httpContext.TraceIdentifier, out long startTime);var elapsedMs = (timestamp - startTime) / TimeSpan.TicksPerMillisecond;//计算耗时Console.WriteLine($"Request {httpContext.TraceIdentifier} End: Status Code is {httpContext.Response.StatusCode},Elapsed {elapsedMs}ms");startTimes.TryRemove(httpContext.TraceIdentifier, out _);}
}

然后使用SubscribeWithAdapter进行订阅即可。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DiagnosticListener diagnosticListener)
{// diagnosticListener.Subscribe(new DiagnosticObserver());diagnosticListener.SubscribeWithAdapter(new HttpContextDiagnosticObserver());
}

到这里可能也有小伙伴说,我用ActionFilter也可以实现,没错,但这两种方式是完全不同的,DiagnosticSource是完全异步的。

4.3. 应用场景思考

根据DiagnosticSource的特性,可以运用于以下场景 :

  1. AOP:因为Diagnostics命名事件一般是成对出现的,因此可以做些拦截操作。比如在Abp集成Cap时,若想默认启用Uow,就可以消费DotNetCore.CAP.WriteSubscriberInvokeBefore命名事件,创建Uow,再在命名事件DotNetCore.CAP.WriteSubscriberInvokeAfter中提交事务,并Dispose。

  2. APM:SkyAPM-dotnet的实现就是通过消费诊断日志,进行链路跟踪。 

  3. EventBus:充分利用其发布订阅模式,可将其用于进程内事件的发布与消费。

5. Activity(活动)

5.1. Activity 概述

那Activity又是何方神圣,用于解决什么问题呢?关于Activity官方只有一句简要介绍:Represents an operation with context to be used for logging。(表示包含上下文的操作,用于日志记录。)

Activity用来存储和访问诊断上下文,并由日志系统进行消费。当应用程序开始处理操作时,例如HTTP请求或队列中的任务,它会在处理请求时创建Activity以在系统中跟踪该Activity。Activity中存储的上下文可以是HTTP请求路径,方法,用户代理或关联ID:所有重要信息都应与每个跟踪一起记录。当应用程序调用外部依赖关系以完成操作时,它可能需要传递一些上下文(例如,关联ID)以及依赖关系调用,以便能够关联来自多个服务的日志。

先来看下Activity主要以下核心属性:

  1. Tags(标签)IEnumerable<KeyValuePair<string, string>> Tags { get; } - 表示与活动一起记录的信息。标签的好例子是实例/机器名称,传入请求HTTP方法,路径,用户/用户代理等。标签不传递给子活动。典型的标签用法包括添加一些自定义标签,并通过它们进行枚举以填充日志事件的有效负载。可通过Activity AddTag(string key, string value)添加Tag,但不支持通过Key检索标签。

  2. Baggage(行李)IEnumerable<KeyValuePair<string, string>> Baggage { get; } - 表示要与活动一起记录并传递给其子项的信息。行李的例子包括相关ID,采样和特征标记。Baggage被序列化并与外部依赖项请求一起传递。典型的Baggage用法包括添加一些Baggage属性,并通过它们进行枚举以填充日志事件的有效负载。可通过Activity AddBaggage(string key, string value)添加Baggage。并通过string GetBaggageItem(string key)获取指定Key的Baggage。

  3. OperationName(操作名称)string OperationName { get; } - 活动名称,必须在构造函数中指定。

  4. StartTimeUtcDateTime StartTimeUtc { get; private set; }  - UTC格式的启动时间,如果不指定,则在启动时默认指定为DateTime.UtcNow。可通过Activity SetStartTime(DateTime startTimeUtc)指定。

  5. DurationTimeSpan Duration { get; private set; } - 如果活动已停止,则代表活动持续时间,否则为0。

  6. Idstring Id { get; private set; } - 表示特定的活动标识符。过滤特定ID可确保您仅获得与操作中特定请求相关的日志记录。该Id在活动开始时生成。Id传递给外部依赖项,并被视为新的外部活动的[ParentId]。

  7. ParentIdstring ParentId { get; private set; } - 如果活动是根据请求反序列化的,则该活动可能具有进程中的[Parent]或外部Parent。ParentId和Id代表日志中的父子关系,并允许您关联传出和传入请求。

  8. RootIdstring RootId { get; private set; } - 代表根Id

  9. Currentstatic Activity Current { get; } - 返回在异步调用之间流动的当前Activity。

  10. ParentActivity Parent { get; private set; } - 如果活动是在同一过程中从另一个活动创建的,则可以使用Partent获得该活动。但是,如果“活动”是根活动或父项来自流程外部,则此字段可能为null。

  11. Start()Activity Start() - 启动活动:设置活动的Activity.Current和Parent,生成唯一的ID并设置StartTimeUtc(如果尚未设置)。

  12. Stop()void Stop() - 停止活动:设置活动的Activity.Current,并使用Activity SetEndTime(DateTime endTimeUtc)或DateTime.UtcNow中提供的时间戳计算Duration。

另外DiagnosticSource中也定义了两个相关方法:

  1. StartActivityActivity StartActivity(Activity activity, object args) - 启动给定的Activity,并将DiagnosticSource事件消息写入OperationName.Start格式的命名事件中。

  2. StopActivityvoid StopActivity(Activity activity, object args) - 停止给定的Activity,并将DiagnosticSource事件消息写入{OperationName}.Stop格式的命名事件中。

5.2. Activity在ASP.NET Core中的应用

要想弄懂Activity,我们还是得向源码学习,看一下HostingApplicationDiagnostics的实现。首先来看下BeginRequst中的StartActivity方法。

private Activity StartActivity(HttpContext httpContext, out bool hasDiagnosticListener)
{Activity activity = new Activity("Microsoft.AspNetCore.Hosting.HttpRequestIn");hasDiagnosticListener = false;IHeaderDictionary headers = httpContext.Request.Headers;StringValues stringValues1;if (!headers.TryGetValue(HeaderNames.TraceParent, out stringValues1))headers.TryGetValue(HeaderNames.RequestId, out stringValues1);if (!StringValues.IsNullOrEmpty(stringValues1)){activity.SetParentId((string) stringValues1);StringValues stringValues2;if (headers.TryGetValue(HeaderNames.TraceState, out stringValues2))activity.TraceStateString = (string) stringValues2;string[] commaSeparatedValues = headers.GetCommaSeparatedValues(HeaderNames.CorrelationContext);if (commaSeparatedValues.Length != 0){foreach (string str in commaSeparatedValues){NameValueHeaderValue parsedValue;if (NameValueHeaderValue.TryParse((StringSegment) str, out parsedValue))activity.AddBaggage(parsedValue.Name.ToString(), parsedValue.Value.ToString());}}}this._diagnosticListener.OnActivityImport(activity, (object) httpContext);if (this._diagnosticListener.IsEnabled("Microsoft.AspNetCore.Hosting.HttpRequestIn.Start")){hasDiagnosticListener = true;this.StartActivity(activity, httpContext);}elseactivity.Start();return activity;
}

从中可以看出,在ASP.NET Core 开始处理请求之前:

  1. 首先,创建了名为Microsoft.AspNetCore.Hosting.HttpRequestIn的Activity,该Activity首先尝试从HTTP请求头中获取TraceParent/euqstId作为当前Activity的ParentId,这个很显然,是用来链路跟踪的。

  2. 其次,尝试从CorrelationContext中获取关联上下文信息,然后将其添加到创建的Activity的Baggage中,进行关联上下文的继续传递。

  3. 然后,启动Activity,然后向Name为Microsoft.AspNetCore.Hosting.HttpRequestIn.Start中写入诊断日志。

这里大家可能有个疑问,这个关联上下文信息CorrelationContext又是何时添加到Http请求头中的呢?在System.Net.Http中的DiagnosticsHandler中添加的。因此我们应该明白了,整个关联上下文的传递机制。

紧接着再来看一看RequestEnd中的StopActivity方法。

private void StopActivity(Activity activity, HttpContext httpContext)
{if (activity.Duration == TimeSpan.Zero)activity.SetEndTime(DateTime.UtcNow);this._diagnosticListener.Write("Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", (object) httpContext);activity.Stop();
}

从中可以看出主要是先SetEndTime,再写入Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop命名事件;最后调用Stop方法停止当前Activity。

简单总结一下,借助Activity中附加的Baggage信息可以实现请求链路上上下文数据的共享。

5.3. 应用场景思考

从上面的命名事件中可以看出,其封送的数据类型是特定的,因此可以借助Activity的Tags或Baggage添加自定义的数据进行共享。

按照上面我们的耗时统计,只能统计到整个http请求的耗时,但对于我们定位问题来说还是有困难,比如,某个api即有调用redis,又操作了消息队列,同时又访问了数据库,那到底是那一段超时了呢?显然不好直接定位,借助activity,我们就可以很好的实现细粒度的链路跟踪。通过activity携带的信息,可以将一系列的操作关联起来,记录日志,再借助AMP进行可视化快速定位跟踪。

6. 参考资料

  1. 在 .NET Core 中使用 Diagnostics (Diagnostic Source) 记录跟踪信息

  2. Logging using DiagnosticSource in ASP.NET Core

  3. .Net Core中的诊断日志DiagnosticSource讲解

  4. Observer Design Pattern

  5. DiagnosticSource User Guide

  6. Activity User Guide

  7. DiagnosticSourcery 101 - Mark Rendle

  8. Improvements in .NET Core 3.0 for troubleshooting and monitoring distributed apps 

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

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

相关文章

leetcode110. 平衡二叉树(java详解)

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

IdentityServer4系列 | 常见术语说明

一、前言在上一篇中&#xff0c;我们IdentityServer4的说明&#xff0c;认识到是一个基于OpenID Connect协议标准的身份认证和授权程序&#xff0c;并简单的对基础知识的认识以及区别说明&#xff0c;从OAuth、OpenID、OpenID Connect以及JWT等进行对比区别说明。而在这一篇中&…

网易年薪40W架构师面试题,欢迎自测!

一个月前被拉进了一个微信群&#xff0c;名字叫《明日都是大佬》&#xff0c;群里有20多个人&#xff0c;都是正在跳槽的&#xff0c;目标是年薪30w&#xff01;投简历、笔试、面试后都相互分享&#xff0c;互通有无你懂的。拉我进群是帮忙解答一些难题&#xff0c;很多题目还是…

leetcode257. 二叉树的所有路径(java递归详解)

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

leetcode617. 合并二叉树

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

跟我一起学.NetCore之熟悉的接口权限验证不能少(Jwt)

前言权限管控对于一个系统来说是非常重要的&#xff0c;最熟悉不过的是菜单权限和数据权限&#xff0c;上一节通过Jwt实现了认证&#xff0c;接下来用它实现接口权限的验证&#xff0c;为什么不是菜单权限呢&#xff1f;对于前后端分离而言&#xff0c;称其为接口权限感觉比较符…

.NET 云原生架构师训练营(模块一 架构师与云原生)--学习笔记

目录什么是软件架构软件架构的基本思路单体向分布式演进、云原生、技术中台1.1 什么是软件架构1.1.1 什么是架构&#xff1f;Software architecture {Elements, Forms, Rationale/Constraints}元素、形式/模式、基本原理和限制为什么需要软件架构&#xff1f;软件架构的终极目…

leetcode530. 二叉搜索树的最小绝对差

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

再分享 5 个 vs 调试技巧

前言 之前在《5 个非常实用的 vs 调试技巧》和《继续分享 5 个实用的 vs 调试技巧》中分享了 10 个我认为非常值得了解的 vs 调试技巧&#xff0c;本周继续分享 5 个很实用的调试技巧。1. 显示下一条语句 在调试时&#xff0c;遇到断点中断后&#xff0c;为了更加清楚的了解程…

leetcode501. 二叉搜索树中的众数

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

网络知识 | 《图解HTTP》读书笔记(上)

【网络知识】| 作者 / Edison Zhou这是EdisonTalk的第293篇原创内容作为一个专业的IT技术人&#xff0c;一个Web应用开发者&#xff0c;不了解网络基础和协议&#xff0c;怎么能行&#xff1f;本文是我2016年阅读《图解HTTP》一书的读书笔记&#xff0c;希望对你有所帮助&#…

leetcode292周赛--6057. 统计值等于子树平均值的节点数

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

C# 中 System.Range 结构体

翻译自 John Demetriou 2020年4月6日 的文章 《C# 8 Is Introducing Ranges》我们之前讨论过的 C# 中的一个特性 System.Index 结构体&#xff0c;与另一特性 System.Range 结构体 是携手并进的。在 C# 8.0 之前&#xff0c;没有访问集合中任意部分的范围或切片的语法。开发人员…

leetcode292周赛--6056. 字符串中最大的 3 位相同数字

一:题目 二:上码 class Solution { public:string largestGoodInteger(string num) {int slowIndex 0;int count 0;vector<int> ans;for (int fastIndex 1; fastIndex < num.size(); fastIndex) {if (num[fastIndex] ! num[slowIndex]) {slowIndex;} else {//相等…

GDB 调试 .NET 程序实录 - .NET 调用 .so 出现问题怎么解决

注&#xff1a;本文重要信息使用 *** 屏蔽关键字。最近国庆前&#xff0c;项目碰到一个很麻烦的问题&#xff0c;这个问题让我们加班到凌晨三点。大概背景&#xff1a;客户给了一些 C语言 写的 SDK 库&#xff0c;这些库打包成 .so 文件&#xff0c;然后我们使用 C# 调用这些库…

leetcode236. 二叉树的最近公共祖先(java递归)

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode(int x) { val x; }* }*/ class Solution {/**思路:1.后序遍历 从下往上遍历 找到目标结点后然后做逻辑判断处理1…

WPF MVVM 弹框之等待框

WPF MVVM 弹框之等待框目录一、效果二、弹框主体改造三、等待动画用户控件四、弹窗 ViewModel 和帮助类的改造五、使用方法和代码地址独立观察员 2020年10月13日之前写过一篇《WPF MVVM 模式下的弹窗》&#xff0c;里面实现了确认框和消息框&#xff0c;经过一段时间的演化&…

设计一个具有等待队列的连接池

说到连接池相关很多人都使用过&#xff0c;常见的有数据连接池&#xff0c;HttpClient连接池等。连接池的作用是保持一定量的连接让交互过程复用这些连接&#xff0c;从而大大节省连接创建过程或过多的损耗。在连接池策略中往往当池没有连接的情况都会抛出异常告诉使用者资源无…

leetcode701. 二叉搜索树中的插入操作

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

你不该错过的2020中国开源年报,填开源开发者问卷,成为国内开源的见证者

点击上方“开源社”关注我们| 作者&#xff1a;王伟| 编辑&#xff1a;黄欣宜| 设计&#xff1a;冯艺怡| 责编&#xff1a;王玥敏卷首语一年一度的中国开源年报再度启动&#xff5e;中国开源年报由开源社发起。旨在从多种维度&#xff0c;多种方式&#xff0c;多种协作来呈现国…