.NET8 依赖注入

依赖注入(Dependency Injection,简称DI)是一种设计模式,用于解耦组件(服务)之间的依赖关系。它通过将依赖关系的创建和管理交给外部容器来实现,而不是在组件(服务)内部直接创建依赖对象。

​ 咱就是通过 IServiceCollection 和 IServiceProvider 来实现的,他们直接被收入到了runtime libraries,在整个.NET平台下通用!

3.1 ServiceCollection

​ IServiceCollection 本质是一个 ServiceDescriptor 而 ServiceDescriptor 则是用于描述服务类型,实现和生命周期

public interface IServiceCollection : IList<ServiceDescriptor>,ICollection<ServiceDescriptor>,IEnumerable<ServiceDescriptor>,IEnumerable;

​ 官方提供一些列拓展帮助我们向服务容器中添加服务描述,具体在 ServiceCollectionServiceExtensions

builder.Services.AddTransient<StudentService>();
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository>("a");
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository2>("b");
builder.Services.AddTransient<TransientService>();
builder.Services.AddScoped<ScopeService>();
builder.Services.AddSingleton<SingletonService>();

3.2 ServiceProvider

​ IServiceProvider 定义了一个方法 GetService,帮助我们通过给定的服务类型,获取其服务实例

public interface IServiceProvider
{object? GetService(Type serviceType);
}

​ 下面是 GetService 的默认实现(如果不给定engine scope,则默认是root)

public object? GetService(Type serviceType) => GetService(ServiceIdentifier.FromServiceType(serviceType), Root);

​ 也就是

internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
{if (_disposed){ThrowHelper.ThrowObjectDisposedException();}// 获取服务标识符对应的服务访问器ServiceAccessor serviceAccessor = _serviceAccessors.GetOrAdd(serviceIdentifier, _createServiceAccessor);// 执行解析时的hockOnResolve(serviceAccessor.CallSite, serviceProviderEngineScope);DependencyInjectionEventSource.Log.ServiceResolved(this, serviceIdentifier.ServiceType);// 通过服务访问器提供的解析服务,得到服务实例object? result = serviceAccessor.RealizedService?.Invoke(serviceProviderEngineScope);System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceIdentifier));return result;
}

​ 其中,服务标识符 ServiceIdentifier 其实就是包了一下服务类型,和服务Key(为了.NET8的键化服务)

internal readonly struct ServiceIdentifier : IEquatable<ServiceIdentifier>
{public object? ServiceKey { get; }public Type ServiceType { get; }
}

​ 显而易见的,我们的服务解析是由 serviceAccessor.RealizedService 提供,而创建服务访问器 serviceAccessor 只有一个实现 CreateServiceAccessor

private ServiceAccessor CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
{// 通过 CallSiteFactory 获取服务的调用点(CallSite),这是服务解析的一个表示形式。ServiceCallSite? callSite = CallSiteFactory.GetCallSite(serviceIdentifier, new CallSiteChain());// 如果调用站点不为空,则继续构建服务访问器。if (callSite != null){DependencyInjectionEventSource.Log.CallSiteBuilt(this, serviceIdentifier.ServiceType, callSite);// 触发创建调用站点的相关事件。OnCreate(callSite);// 如果调用站点的缓存位置是根(Root),即表示这是一个单例服务。if (callSite.Cache.Location == CallSiteResultCacheLocation.Root){// 直接拿缓存内容object? value = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root);return new ServiceAccessor { CallSite = callSite, RealizedService = scope => value };}// 通过引擎解析Func<ServiceProviderEngineScope, object?> realizedService = _engine.RealizeService(callSite);return new ServiceAccessor { CallSite = callSite, RealizedService = realizedService };}// 如果调用点为空,则它的实现服务函数总是返回 null。return new ServiceAccessor { CallSite = callSite, RealizedService = _ => null };
}
3.2.1 ServiceProviderEngine

​ ServiceProviderEngine 是服务商解析服务的执行引擎,它在服务商被初始化时建立。有两种引擎,分别是动态引擎运行时引擎,在 NETFRAMEWORK || NETSTANDARD2_0 默认使用动态引擎。

        private ServiceProviderEngine GetEngine(){ServiceProviderEngine engine;#if NETFRAMEWORK || NETSTANDARD2_0engine = CreateDynamicEngine();
#elseif (RuntimeFeature.IsDynamicCodeCompiled && !DisableDynamicEngine){engine = CreateDynamicEngine();}else{// Don't try to compile Expressions/IL if they are going to get interpretedengine = RuntimeServiceProviderEngine.Instance;}
#endifreturn engine;[UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",Justification = "CreateDynamicEngine won't be called when using NativeAOT.")] // see also https://github.com/dotnet/linker/issues/2715ServiceProviderEngine CreateDynamicEngine() => new DynamicServiceProviderEngine(this);}

​ 由于.NET Aot技术与dynamic技术冲突,因此Aot下只能使用运行时引擎,但动态引擎在大多情况下仍然是默认的。

动态引擎使用了emit技术,这是一个动态编译技术,而aot的所有代码都需要在部署前编译好,因此运行时无法生成新的代码。那运行时引擎主要使用反射,目标是在不牺牲太多性能的情况下,提供一个在aot环境中可行的解决方案。

​ 我们展开动态引擎来看看它是如何解析服务的。

public override Func<ServiceProviderEngineScope, object?> RealizeService(ServiceCallSite callSite)
{// 定义一个局部变量来跟踪委托被调用的次数int callCount = 0;return scope =>{// 当委托被调用时,先使用CallSiteRuntimeResolver.Instance.Resolve方法来解析服务。这是一个同步操作,确保在编译优化之前,服务可以被正常解析。var result = CallSiteRuntimeResolver.Instance.Resolve(callSite, scope);// 委托第二次被调用,通过UnsafeQueueUserWorkItem在后台线程上启动编译优化if (Interlocked.Increment(ref callCount) == 2){// 将一个工作项排队到线程池,但不捕获当前的执行上下文。_ = ThreadPool.UnsafeQueueUserWorkItem(_ =>{try{// 用编译优化后的委托替换当前的服务访问器,主要用到emit/expression技术_serviceProvider.ReplaceServiceAccessor(callSite, base.RealizeService(callSite));}catch (Exception ex){DependencyInjectionEventSource.Log.ServiceRealizationFailed(ex, _serviceProvider.GetHashCode());Debug.Fail($"We should never get exceptions from the background compilation.{Environment.NewLine}{ex}");}},null);}return result;};
}

这个实现的关键思想是,第一次解析服务时使用一个简单的运行时解析器,这样可以快速返回服务实例。然后,当服务再被解析,它会在后台线程上启动一个编译过程,生成一个更高效的服务解析委托。一旦编译完成,新的委托会替换掉原来的委托,以后的服务解析将使用这个新的、更高效的委托。这种方法可以在不影响应用程序启动时间的情况下,逐渐优化服务解析的性能。

3.2.2 ServiceProviderEngineScope

​ ServiceProviderEngineScope 闪亮登场,他是我们服务商的代言人,从定义不难看出他对外提供了服务商所具备的一切能力

internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, 			IAsyncDisposable, IServiceScopeFactory
{// this scope中所有实现IDisposable or IAsyncDisposable的服务private List<object>? _disposables;// 解析过的服务缓存(其实就是scope生命周期的服务缓存,singleton生命周期的服务缓存都直接挂在调用点上了)internal Dictionary<ServiceCacheKey, object?> ResolvedServices { get; }// 实锤服务商代言人public IServiceProvider ServiceProvider => this;// 没错啦,通过root scope我们又能继续创建无数个scope,他们彼此独立public IServiceScope CreateScope() => RootProvider.CreateScope();
}

​ 我们来观察他获取服务的逻辑,可以发现他就是很朴实无华的用着我们根服务商 ServiceProvider,去解析服务,那 engine scope 呢,就是 this。现在我们已经隐约可以知道engine scope,就是为了满足scope生命周期而生。而 ResolvedServices 中存的呢,就是该scope中的所有scope生命周期服务实例啦,在这个scope中他们是唯一的。

public object? GetService(Type serviceType)
{if (_disposed){ThrowHelper.ThrowObjectDisposedException();}return RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this);
}

​ 再来看另一个核心的方法:CaptureDisposable,实现disposable的服务会被添加到 _disposables。

internal object? CaptureDisposable(object? service)
{// 如果服务没有实现 IDisposable or IAsyncDisposable,那么不需要捕获,直接原路返回if (ReferenceEquals(this, service) || !(service is IDisposable || service is IAsyncDisposable)){return service;}bool disposed = false;lock (Sync){if (_disposed) // 如果scope已经销毁则进入销毁流程{disposed = true;}else{_disposables ??= new List<object>();_disposables.Add(service);}}// Don't run customer code under the lockif (disposed) // 这表示我们在试图捕获可销毁服务时,scope就已经被销毁{if (service is IDisposable disposable){disposable.Dispose();}else{// sync over async, for the rare case that an object only implements IAsyncDisposable and may end up starving the thread pool.object? localService = service; // copy to avoid closure on other pathsTask.Run(() => ((IAsyncDisposable)localService).DisposeAsync().AsTask()).GetAwaiter().GetResult();}// 这种case会抛出一个ObjectDisposedExceptionThrowHelper.ThrowObjectDisposedException();}return service;
}

​ 在engine scope销毁时,其作用域中所有scope生命周期且实现了disposable的服务(其实就是_disposable)呢,也会被一同的销毁。

public ValueTask DisposeAsync()
{List<object>? toDispose = BeginDispose(); // 获取_disposableif (toDispose != null){// 从后往前,依次销毁服务}
}

​ 那么有同学可能就要问了:单例实例既然不存在root scope中,而是单独丢到了调用点上,那他是咋销毁的?压根没看到啊,那不得泄露了?

​ 其实呀,同学们并不用担心这个问题。首先,单例服务的实例确实是缓存在调用点上,但 disable 服务仍然会被 scope 捕获呀(在下文会详细介绍)。在 BeginDispose 中的,我们会去判断,如果是 singleton case,且root scope 没有被销毁过,我们会主动去销毁喔~

if (IsRootScope && !RootProvider.IsDisposed()) RootProvider.Dispose();

3.3 ServiceCallSite

​ ServiceCallSite 的主要职责是封装服务解析的逻辑,它可以代表一个构造函数调用、属性注入、工厂方法调用等。DI系统使用这个抽象来表示服务的各种解析策略,并且可以通过它来生成服务实例。

internal abstract class ServiceCallSite
{protected ServiceCallSite(ResultCache cache){Cache = cache;}public abstract Type ServiceType { get; }public abstract Type? ImplementationType { get; }public abstract CallSiteKind Kind { get; }public ResultCache Cache { get; }public object? Value { get; set; }public object? Key { get; set; }public bool CaptureDisposable => ImplementationType == null ||typeof(IDisposable).IsAssignableFrom(ImplementationType) ||typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
}
3.3.1 ResultCache

​ 其中 ResultCache 定义了我们如何缓存解析后的结果

public CallSiteResultCacheLocation Location { get; set; } // 缓存位置
public ServiceCacheKey Key { get; set; } // 服务key(键化服务用的)

​ CallSiteResultCacheLocation 是一个枚举,定义了几个值

  1. Root:表示服务实例应该在根级别的 IServiceProvider 中缓存。这通常意味着服务实例是单例的(Singleton),在整个应用程序的生命周期内只会创建一次,并且在所有请求中共享。
  2. Scope:表示服务实例应该在当前作用域(Scope)中缓存。对于作用域服务(Scoped),实例会在每个作用域中创建一次,并在该作用域内的所有请求中共享。
  3. Dispose:尽管这个名称可能会让人误解,但在 ResultCache 的上下文中,Dispose 表示着服务是瞬态的(每次请求都创建新实例)。
  4. None:表示没有缓存服务实例。

​ ServiceCacheKey 结构体就是包了一下服务标识符和一个slot,用于适配多实现的

internal readonly struct ServiceCacheKey : IEquatable<ServiceCacheKey>
{public ServiceIdentifier ServiceIdentifier { get; }public int Slot { get; } // 那最后一个实现的slot是0
}
3.3.2 CallSiteFactory.GetCallSite

​ 那我们来看看调用点是怎么创建的吧,其实上面已经出现过一次了:

private ServiceCallSite? CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
{if (!_stackGuard.TryEnterOnCurrentStack()) // 防止栈溢出{return _stackGuard.RunOnEmptyStack(CreateCallSite, serviceIdentifier, callSiteChain);}// 获取服务标识符对应的锁,以确保在创建调用点时的线程安全。// 是为了保证并行解析下的调用点也只会被创建一次,例如:// C -> D -> A// E -> D -> Avar callsiteLock = _callSiteLocks.GetOrAdd(serviceIdentifier, static _ => new object());lock (callsiteLock){// 检查当前服务标识符是否会导致循环依赖callSiteChain.CheckCircularDependency(serviceIdentifier);// 首先尝试创建精确匹配的服务调用站点,如果失败,则尝试创建开放泛型服务调用站点,如果还是失败,则尝试创建枚举服务调用站点。如果所有尝试都失败了,callSite将为null。ServiceCallSite? callSite = TryCreateExact(serviceIdentifier, callSiteChain) ??TryCreateOpenGeneric(serviceIdentifier, callSiteChain) ??TryCreateEnumerable(serviceIdentifier, callSiteChain);return callSite;}
}

​ 那服务点的创建过程我就简单概述一下啦

  1. 查找调用点缓存,存在就直接返回啦
  2. 服务标识符会被转成服务描述符 ServiceDescriptor (key化服务不指定key默认取last)
  3. 计算ServiceCallSite,依次是:
    1. TryCreateExact
      1. 计算 ResultCache
      2. 如果已经有实现实例了,则返回 ConstantCallSite:表示直接返回已经创建的实例的调用点。
      3. 如果有实现工厂,则返回 FactoryCallSite:表示通过工厂方法创建服务实例的调用点。
      4. 如果有实现类型,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
    2. TryCreateOpenGeneric
      1. 根据泛型定义获取服务描述符 ServiceDescriptor
      2. 计算 ResultCache
      3. 使用服务标识符中的具体泛型参数来构造实现的闭合类型
      4. AOT兼容性测试(因为不能保证值类型泛型的代码已经生成)
      5. 如果成功闭合,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
    3. TryCreateEnumerable
      1. 确定类型是 IEnumerable<T>
      2. AOT兼容性测试(因为不能保证值类型数组的代码已经生成)
      3. 如果 T 不是泛型类型,并且可以找到对应的服务描述符集合,则循环 TryCreateExact
      4. 否则,方向循环 TryCreateExact,然后方向循环 TryCreateOpenGeneric

3.4 CallSiteVisitor

​ 好了,有了上面的了解我们可以开始探索服务解析的内幕了。服务解析说白了就是引擎围着 CallSiteVisitor 转圈圈,所谓的解析服务,其实就是访问调用点了。

protected virtual TResult VisitCallSite(ServiceCallSite callSite, TArgument argument)
{if (!_stackGuard.TryEnterOnCurrentStack()) // 一些校验,分栈啥的{return _stackGuard.RunOnEmptyStack(VisitCallSite, callSite, argument);}switch (callSite.Cache.Location){case CallSiteResultCacheLocation.Root: // 单例return VisitRootCache(callSite, argument);case CallSiteResultCacheLocation.Scope: // 作用域return VisitScopeCache(callSite, argument);case CallSiteResultCacheLocation.Dispose: // 瞬态return VisitDisposeCache(callSite, argument);case CallSiteResultCacheLocation.None: // 不缓存(ConstantCallSite)return VisitNoCache(callSite, argument);default:throw new ArgumentOutOfRangeException();}
}

​ 为了方便展示,我们这里的解析器都拿运行时来说,因为内部是反射,而emit、expression实在是难以观看。

3.4.1 VisitRootCache

​ 那我们来看看单例的情况下,是如何访问的:

protected override object? VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
{if (callSite.Value is object value){// Value already calculated, return it directlyreturn value;}var lockType = RuntimeResolverLock.Root;// 单例都是直接放根作用域的ServiceProviderEngineScope serviceProviderEngine = context.Scope.RootProvider.Root;lock (callSite){// 这里搞了个双检锁来确保在多线程环境中,同一时间只有一个线程可以执行接下来的代码块。// Lock the callsite and check if another thread already cached the valueif (callSite.Value is object callSiteValue){return callSiteValue;}object? resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext{Scope = serviceProviderEngine,AcquiredLocks = context.AcquiredLocks | lockType});// 捕获可销毁的服务serviceProviderEngine.CaptureDisposable(resolved);// 缓存解析结果到调用点上callSite.Value = resolved;return resolved;}
}

​ 好,可以看到真正解析调用点的主角出来了 VisitCallSiteMain,那这里的 CallSiteKind 上面计算 ServiceCallSite 时呢已经讲的很清楚啦,咱对号入座就行了

protected virtual TResult VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
{switch (callSite.Kind){case CallSiteKind.Factory:return VisitFactory((FactoryCallSite)callSite, argument);case  CallSiteKind.IEnumerable:return VisitIEnumerable((IEnumerableCallSite)callSite, argument);case CallSiteKind.Constructor:return VisitConstructor((ConstructorCallSite)callSite, argument);case CallSiteKind.Constant:return VisitConstant((ConstantCallSite)callSite, argument);case CallSiteKind.ServiceProvider:return VisitServiceProvider((ServiceProviderCallSite)callSite, argument);default:throw new NotSupportedException(SR.Format(SR.CallSiteTypeNotSupported, callSite.GetType()));}
}

​ 我们就看看最经典的通过构造函数创建服务实例的调用点 ConstructorCallSite,很显然就是new嘛,只不过可能构造中依赖其它服务,那就递归创建呗。easy,其它几种太简单了大家自己去看看吧。

protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
{object?[] parameterValues;if (constructorCallSite.ParameterCallSites.Length == 0){parameterValues = Array.Empty<object>();}else{parameterValues = new object?[constructorCallSite.ParameterCallSites.Length];for (int index = 0; index < parameterValues.Length; index++){// 递归构建依赖的服务parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context);}}// new (xxx)return constructorCallSite.ConstructorInfo.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: parameterValues, culture: null);
}
3.4.2 VisitScopeCache

​ 在访问单例缓存的时候呢,仅仅通过了一个double check lock就搞定了,因为人家全局的嘛,咱再来看看访问作用域缓存,会不会有什么不一样

protected override object? VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
{// Check if we are in the situation where scoped service was promoted to singleton// and we need to lock the rootreturn context.Scope.IsRootScope ?VisitRootCache(callSite, context) :VisitCache(callSite, context, context.Scope, RuntimeResolverLock.Scope);
}

​ 哈哈,它果然很不一般啊,上来就来检查我们是否是 root scope。如果是这种case呢,则走 VisitRootCache。但是奇怪啊,为什么访问 scope cache,所在 engine scope 能是 root scope?

​ 还记得 ServiceProvider 获取的服务实例的核心方法吗?engine scope 他是传进来的,如果我们给一个 root scope,自然就出现的这种case,只是这种 case 特别罕见。

internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)

​ VisitCache 的同步模型写的实在是酷,我们看 RuntimeResolverLock 的枚举就两个:Scope = 1 和 Root = 2

  • AcquiredLocks=Scope时

  • 那 AcquiredLocks&false==0 显然成立,申请锁,也就是尝试独占改作用域的ResolvedServices

  • 申请成功进入同步块,重新计算AcquiredLocks|true=1

  • 如此,在该engine scope 中这条链路上的调用点都被占有,直到结束

  • AcquiredLocks=Root 时

    • 那显然 engine scope 也应该是 root scope,那么走 VisitRootCache case
    • 在 VisitRootCache 通过DCL锁住 root scope 上链路涉及的服务点,直至结束

​ 至此我们应该不难看出这个设计的精妙之处,即在非 root scope(scope生命周期)中,scope之间是互相隔离的,没有必要像 root scope(singleton生命周期)那样,在所有scope中独占服务点。

private object? VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine
{bool lockTaken = false;object sync = serviceProviderEngine.Sync;Dictionary<ServiceCacheKey, object?> resolvedServices = serviceProviderEngine.ResolvedServices;if ((context.AcquiredLocks & lockType) == 0){Monitor.Enter(sync, ref lockTaken);}try{// Note: This method has already taken lock by the caller for resolution and access synchronization.// For scoped: takes a dictionary as both a resolution lock and a dictionary access lock.if (resolvedServices.TryGetValue(callSite.Cache.Key, out object? resolved)){return resolved;}// scope服务的解析结果是放在engine scope的ResolvedServices上的,而非调用点resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext{Scope = serviceProviderEngine,AcquiredLocks = context.AcquiredLocks | lockType});serviceProviderEngine.CaptureDisposable(resolved);resolvedServices.Add(callSite.Cache.Key, resolved);return resolved;}finally{if (lockTaken){Monitor.Exit(sync);}}
}
3.4.3 VisitDisposeCache

​ 我们看最后一个,也就是 Transient case

protected override object? VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
{return context.Scope.CaptureDisposable(VisitCallSiteMain(transientCallSite, context));
}

​ 异常的简单,我们沿用了scope的设计,但是我们没有进行任何缓存行为。即,每次都去访问调用点。

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

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

相关文章

等保测评报价相差很大,里面有什么门道

等保测评报价的差异主要源于以下几点&#xff1a; 服务质量评估标准不同&#xff1a;不同的测评机构在测评过程中所提供的服务范围、深度、细节等方面可能存在差异&#xff0c;因此导致报价有所不同。一些机构可能提供全面且细致的测评服务&#xff0c;致力于提供高质量的等保测…

openGauss学习笔记-139 openGauss 数据库运维-例行维护-检查应用连接数

文章目录 openGauss学习笔记-139 openGauss 数据库运维-例行维护-检查应用连接数139.1 操作步骤139.2 异常处理 openGauss学习笔记-139 openGauss 数据库运维-例行维护-检查应用连接数 如果应用程序与数据库的连接数超过最大值&#xff0c;则新的连接无法建立。建议每天检查连…

一种结构新颖的双通带超导滤波器设计

闫鑫1,2&#xff0c;季来运1&#xff0c;张浩1,2&#xff0c;李颢毅1,2&#xff0c;王昭月1,2&#xff0c;曹凤莹1,2 &#xff08;1.天津海芯电子有限公司&#xff0c;天津300380&#xff1b;2.天津师范大学物理与材料科学学院&#xff0c;天津 300387.&#xff09; 摘要&…

前端dark主题的快速构建与切换

首先在全局css样式中增加一个 dark 模式即可&#xff0c;主要就是filter这个属性&#xff0c; invert(1);则表示100%完全反转样式&#xff0c;通俗点就是颠倒黑白&#xff0c;白的让它变成黑的&#xff0c;黑的让它变成白的。 css中的filter函数总结 filter:invert(1);数值范围…

Leetcode题库(数据库合集)_ 难度:简单

目录 难度&#xff1a;简单1. 组合两个表2. 第二高的薪水3. 第N高的薪水4. 分数排名5. 连续出现的数字6. 超过经理收入的员工7. 重新8. 寻找用户推荐人9. 销售员10. 排名靠前的旅行者11. 患某种疾病的患者12. 修复表中的名字13. 求关注者的数量14. 可回收且低脂的产品15. 计算特…

前后端参数传递总结

1、 页面参数 js传递参数 渲染表格 页面控制器&#xff08;前端&#xff09; 后端控制器 后端服务 实体赋值 2、跟踪情况

场景实践 | 法大大落地业财一体化,优化流程结构

2023 年&#xff0c;法大大作为中国电子签行业唯一上榜《2023胡润全球未来独角兽》企业&#xff0c;同时上榜“2022深圳市潜在科技独角兽企业榜单”。作为高速发展的高科技服务企业&#xff0c;法大大自2021年完成9亿元腾讯D轮融资后&#xff0c;建立了长期主义发展计划&#x…

计算机基础知识63

Django的条件查询&#xff1a;查询函数 exclude exclude&#xff1a;返回不满足条件的数据 res Author.objects.exclude(pk1) print(res) # <QuerySet [<Author: Author object (2)>, <Author: Author object (3)>]> order_by 1、按照 id 升序排序 res …

【Seata源码学习 】篇六 全局事务提交与回滚

【Seata源码学习 】篇六 全局事务提交与回滚 全局事务提交 TM在RPC远程调用RM后,如果没有出现异常&#xff0c;将向TC发送提交全局事务请求io.seata.tm.api.TransactionalTemplate#execute public Object execute(TransactionalExecutor business) throws Throwable {// 1. …

Naco安装、配置、交互

1. Docker安装Naco 官方文档https://nacos.io/zh-cn/docs/quick-start-docker.html&#xff0c;然而自己部署的时候遇到了“Database not set”的问题。有可能是因为环境中已经部署了3306的mysql服务导致的。&#xff08;虽然我尝试修改了naco的docker-compose&#xff0c;但是…

【离散数学】——期末刷题题库(集合)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

【FPGA】Verilog:二进制并行加法器 | 超前进位 | 实现 4 位二进制并行加法器和减法器 | MSI/LSI 运算电路

Ⅰ. 前置知识 0x00 并行加法器和减法器 如果我们要对 4 位加法器和减法器进行关于二进制并行运算功能&#xff0c;可以通过将加法器和减法器以 N 个并行连接的方式&#xff0c;创建一个执行 N 位加法和减法运算的电路。 4 位二进制并行加法器 4 位二进制并行减法器 换…

内存是如何工作的

一、什么是内存 从外观上辨识&#xff0c;它就是内存条&#xff1b;从硬件上讲&#xff0c;它叫RAM&#xff0c;翻译过来叫随机存储器。英文全称&#xff1a;Random Access Memory。它也叫主存&#xff0c;是与CPU直接交换数据的内部存储器。其特点是读写速度快&#xff0c;不…

JSX语法

文章目录 1.JSX是什么2.JSX书写规范3.JSX中显示数据4.添加样式隔离作用域 5.条件渲染6.列表渲染7.响应事件8.更新页面 1.JSX是什么 JSX是一种JavaScript的语法扩展用于描述页面,并且可以和JavaScript融合在一起不同于Vue的模板语法,没有Vue中的一些指令(v-if,v-show,v-for)在r…

SpringBoot嵌入式容器(自动配置原理、自定义嵌入式容器)

目录 1. 自动配置原理2. 自定义嵌入式容器3. 最佳实践 Servlet容器&#xff1a;管理、运行Servlet组件&#xff08;Servlet、Filter、Listener&#xff09;的环境&#xff0c;一般指服务器 1. 自动配置原理 SpringBoot默认嵌入Tomcat作为Servlet容器自动配置类是ServletWebSer…

抽象类与接口

抽象类&#xff1a; 父类某些方法需要声明&#xff0c;但又不确定如何实现&#xff0c;可将其声明抽象方法&#xff0c; 【抽象类主要是为了防止创建父类对象】 抽象类与抽象方法 抽象方法没有方法体 类中有抽象方法&#xff0c;类必须声明为抽象类 …

java开发之个微机器人的实现

简要描述&#xff1a; 二次登录 请求URL&#xff1a; http://域名地址/secondLogin 请求方式&#xff1a; POST 请求头Headers&#xff1a; Content-Type&#xff1a;application/jsonAuthorization&#xff1a;login接口返回 参数&#xff1a; 参数名必选类型说明wcId…

k8s之Pod常用命令详解、镜像拉取策略(imagePullPolicy)

常用命令 kubectl get pod #查看默认命名空间下所有pod kubectl describe pod podname #获取默认命名空间下POD详情# 如果要查看制定命名空间则使用 -n nsname kubectl get pod -n ns kubectl describe pod podname -n ns# 以YAML格式提供比 kubectl describe pod 更加详细的信…

【每日一题】从二叉搜索树到更大和树

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;中序遍历的反序方法二&#xff1a;后缀数组 写在最后 Tag 【中序遍历】【二叉树】【2023-12-04】 题目来源 1038. 从二叉搜索树到更大和树 题目解读 在二叉搜索树中&#xff0c;将每一个节点的值替换成树中大于等于该…

DDR详解

DDR也就是常称的内存在一般使用过程中都是透明的&#xff0c;此文从多方面对DDR进行详解。 DDR训练 高可靠性是系统级芯片SoC重要的质量和性能要求之一。SoC的复杂在于各个IP模块都对其产生至关重要的影响。从芯耀辉长期服务客户的经验来看&#xff0c;在客户的SoC设计中&…