.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);
    // 执行解析时的hock
    OnResolve(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_0
            engine = CreateDynamicEngine();
#else
            if (RuntimeFeature.IsDynamicCodeCompiled && !DisableDynamicEngine)
            {
                engine = CreateDynamicEngine();
            }
            else
            {
                // Don't try to compile Expressions/IL if they are going to get interpreted
                engine = RuntimeServiceProviderEngine.Instance;
            }
#endif
            return engine;

            [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",
                Justification = "CreateDynamicEngine won't be called when using NativeAOT.")] // see also https://github.com/dotnet/linker/issues/2715
            ServiceProviderEngine 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 lock
    if (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 paths
            Task.Run(() => ((IAsyncDisposable)localService).DisposeAsync().AsTask()).GetAwaiter().GetResult();
        }
        // 这种case会抛出一个ObjectDisposedException
        ThrowHelper.ThrowObjectDisposedException();
    }
    return service;
}

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

public ValueTask DisposeAsync()
{
    List<object>? toDispose = BeginDispose(); // 获取_disposable
    if (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),在整个应用程序的生命周期内只会创建一次,并且在所有请求中共享。

  1. Scope:表示服务实例应该在当前作用域(Scope)中缓存。对于作用域服务(Scoped),实例会在每个作用域中创建一次,并在该作用域内的所有请求中共享。

  1. Dispose:尽管这个名称可能会让人误解,但在 ResultCache 的上下文中,Dispose 表示着服务是瞬态的(每次请求都创建新实例)。

  1. 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 -> A
    var 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. 查找调用点缓存,存在就直接返回啦

  1. 服务标识符会被转成服务描述符 ServiceDescriptor (key化服务不指定key默认取last)

  1. 计算ServiceCallSite,依次是:

  • TryCreateExact

计算 ResultCache

如果已经有实现实例了,则返回 ConstantCallSite:表示直接返回已经创建的实例的调用点。

如果有实现工厂,则返回 FactoryCallSite:表示通过工厂方法创建服务实例的调用点。

如果有实现类型,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。

  • TryCreateOpenGeneric

根据泛型定义获取服务描述符 ServiceDescriptor

计算 ResultCache

使用服务标识符中的具体泛型参数来构造实现的闭合类型

AOT兼容性测试(因为不能保证值类型泛型的代码已经生成)

如果成功闭合,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。

  • TryCreateEnumerable

确定类型是 IEnumerable<T>

AOT兼容性测试(因为不能保证值类型数组的代码已经生成)

如果 T 不是泛型类型,并且可以找到对应的服务描述符集合,则循环 TryCreateExact

否则,方向循环 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 directly
        return value;
    }
    var lockType = RuntimeResolverLock.Root;
    // 单例都是直接放根作用域的
    ServiceProviderEngineScope serviceProviderEngine = context.Scope.RootProvider.Root;
    lock (callSite)
    {
        // 这里搞了个双检锁来确保在多线程环境中,同一时间只有一个线程可以执行接下来的代码块。
        // Lock the callsite and check if another thread already cached the value
        if (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 root
    return 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的设计,但是我们没有进行任何缓存行为。即,每次都去访问调用点。

文章转载自:xiaolipro

原文链接:https://www.cnblogs.com/xiaolipro/p/17873575.html

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

帆软报表决策报表改变屏幕大小后出现字体大小或滚动条异常解决方案:双向自适应

帆软报表决策报表改变屏幕大小后出现字体大小或滚动条异常。 解决方案&#xff1a;在模板和报表块中配置双向自适应 在每一个报表块中设置&#xff1a;

HGNN复现

python版本&#xff1a;3.6.13 torch版本&#xff1a;http://download.pytorch.org/whl/cpu/torch-0.4.0-cp36-cp36m-win_amd64.whl 安装torch&#xff1a; pip install http://download.pytorch.org/whl/cpu/torch-0.4.0-cp36-cp36m-win_amd64.whl 除了numpy、matplotlib、…

Ubuntu环境下使用GDB调试C语言项目

1. 安装gdb //终端输入 sudo apt-get install gdb 2. 启动gdb gdb GDB常用命令大全&#xff0c;参考此篇博客 使用GDB调试C项目中的makefile 1.在内核配置中启用调试信息&#xff1a; 在内核配置中&#xff0c;确保启用了调试信息。可以通过以下步骤来配置内核&#xff1a…

验证码的多种生成策略

&#x1f60a; 作者&#xff1a; 瓶盖子io &#x1f496; 主页&#xff1a; 瓶盖子io-CSDN博客 第一种 a.导入依赖 <dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.10</ver…

解决Kettle无法传输生偏字的问题

最近在搞数据传输&#xff0c;然后涉及到人名的时候&#xff0c;难免会碰到生僻字&#xff0c;utf8是无法存储的&#xff0c;然后我把目标表改为utf8mb4&#xff0c;但是还是无法传输生僻字&#xff0c;这就很奇怪了&#xff0c;明明两边的数据库都是MySQL 5.7x&#xff0c;但是…

3D热图绘制教程

本期教程 原文链接https://mp.weixin.qq.com/s/EyBs6jn78zOomcTv1aP52g 6 3D热图的绘制教程 基于《热图绘制教程》专栏,本教程已更新了5个章节,不知道大家是否有所收获。对于小杜个人来说,真的需要不断的复习和练习才可以记住,但是在自己使用的时候也需要重复的看自己的学…

【Linux】resolv.conf 文件

resolv.conf resolv.conf 文件 是 DNS 的 client 端使用的文件&#xff0c;用于设置 DNS 服务器的 ip 地址以及 DNS 域名&#xff0c;还可以配置域名搜索顺序等等。主要包含如下关键字&#xff1a;nameserver、domain、search、sortlist、options。设置的格式都是 关键字空格 …

【如何写论文】——写作提效的n个技巧:Word图、表自动编号 +Zotero实现参考文献自动化

目录 一、Word图、表自动编号1.1、单级编号1.2、多级编号1.3、交叉引用1.4、修改题注格式 二、Zotero实现参考文献自动化最后 一、Word图、表自动编号 在论文写作中&#xff0c;通常会包含数十张图片或表格。默认情况下&#xff0c;这些图片和表格都是没有编号的。 然而&…

C语言第十六集(前)

1.关于那个整形存储入char的 是先取好补码,再截断 例: 2.%u是以十进制的形式打印无符号整数 3.注意(背):存储的char类型变量的补码为10000000的直接解析为-128 4.signed char 类型的变量取值范围是-128~127 5.unsigned char 类型的变量取值范围是0~255 6.有符号类型的变量…

Linus:我休假的时候也会带着电脑,否则会感觉很无聊

目录 Linux 内核最新版本动态 关于成为内核维护者 代码好写&#xff0c;人际关系难处理 内核维护者老龄化 内核中 Rust 的使用 关于 AI 的看法 参考 12.5-12.6 日&#xff0c;Linux 基金会组织的开源峰会&#xff08;OSS&#xff0c;Open Source Summit&#xff09;在日…

matplot绘图时图像太大报错但能保存

matplot绘图时&#xff0c;图像太大&#xff0c;可能在jupyter里面报错&#xff0c;但是图像可以保存。 报错&#xff1a;Image size of 12237479x675 pixels is too large. It must be less than 2^16 in each direction. 在这里插入图片描述

编程创意汇聚地,打造个性作品集 | 开源日报 No.97

spring-projects/spring-boot Stars: 70.4k License: Apache-2.0 Spring Boot 是一个用于简化 Spring 应用程序开发的框架&#xff0c;它通过提供默认配置和约定大于配置的方式来减少开发者的工作量。Spring Boot 可以快速地创建独立的、生产级别的基于 Spring 框架的应用程序…

简单的越权

越权漏洞 危害 越权漏洞分为水平越权和垂直越权&#xff1b; 水平越权容易造成敏感信息的泄露&#xff1b;垂直越权容易造成服务器权限丢失。 普通用户执行管理员操作时&#xff0c;可能会执行一些上传操作&#xff0c;例如一句话木马 挖掘 我会在数据包中寻找例如id、us…

报错处理集

这个报错处理集的错误来源于编译arm平台的so文件产生的。但是后续可以补充成linux一个大的错误处理集。 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 第一次整理的时间是2023年12月8日10:05:59&#xff0c;以下错误来源于欧拉系统编译…

单片机第三季-第四课:STM32下载、MDK和调试器

目录 1&#xff0c;扩展板使用的STM32芯片类型 2&#xff0c;使用普中科技软件下载程序 3&#xff0c;keil介绍 4&#xff0c;JLINK调试器介绍 5&#xff0c;使用普中的调试器进行debug 6&#xff0c;使用Simulator仿真 1&#xff0c;扩展板使用的STM32芯片类型 扩展版…

numpy数据读取保存及速度测试

目录 数据保存及读取 速度比对测试 数据保存及读取 代码示例&#xff1a; # 导入必要的库 import numpy as np # 生成测试数据 arr_disk np.arange(8) # 打印生成能的数据 print(arr_disk) # numpy保存数据到本地 np.save("arr_disk", arr_disk) # 加载本地数据…

网络攻击(一)--安全渗透简介

1. 安全渗透概述 目标 了解渗透测试的基本概念了解渗透测试从业人员的注意事项 1.1. 写在前面的话 在了解渗透测试之前&#xff0c;我们先看看&#xff0c;信息安全相关的法律是怎么样的 中华人民共和国网络安全法 《中华人民共和国网络安全法》由全国人民代表大会常务委员会…

Python爬虫代理程序如何应对目标网站反爬策略

玩过python爬虫的都知道&#xff0c;在爬虫程序编写过程中&#xff0c;可能会遇到目标网站的反爬策略&#xff0c;需要不停的和网站做技术抗争&#xff0c;并且需要不停的更新反爬策略。这些策略防止程序过度爬取影响服务器负载。下面就是我总结的一些经验技巧可以看看。 我们…

接龙数列问题

更好的阅读体验请点击 接龙数组。 题目&#xff1a;接龙 对于一个长度为 K 的整数数列&#xff1a;A1,A2,…,AK&#xff0c;我们称之为接龙数列当且仅当 Ai 的首位数字恰好等于 Ai−1的末位数字 (2≤i≤K)。 例如 12,23,35,56,61,1112,23,35,56,61,11 是接龙数列&#xff1b;…

获取拼多多京东淘宝商品数据店铺数据店铺信息最推荐最好用的一种方式就是API接口

随着越来越多的社会资源被网络化和数字化&#xff0c;大数据可以承载的价值也将不断被提及和提高&#xff0c;大数据的应用范围也将不断扩大。因此&#xff0c;在未来的网络时代&#xff0c;大数据本身不仅可以代表价值&#xff0c;而且大数据本身也可以创造价值。 获取淘宝拼…
最新文章