.Net Core后端架构实战【3-介入IOC控制反转】

2023/6/5 1:23:07

本文主要是介绍.Net Core后端架构实战【3-介入IOC控制反转】,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

摘要:基于.NET Core 7.0WebApi后端架构实战【2-介入IOC控制反转】  2023/04/09, ASP.NET Core 7.0, VS2022

引言

    Inversion of Control,简称IOC,即控制反转。记得当初刚实习的时候公司的带我的人和我提到过IOC这个概念,当初完全不知道是
啥东西。后来有幸写了半年Java,SpringBoot里面业务开发随处可见IOC。再后来我写.Net Core用到的第一个框架Blog.Core项目,它里
面IRepository与Repository和IServices与Services,这种解耦的程度单说它贯彻依赖倒置原则是非常nice的!.NetCore时代更加注
重面向接口编程,并且它内置轻量型的IOC容器可帮助我们灵活的进行开发。

依赖注入

Dependency Injection,何为依赖注入?由容器动态的将对象依赖的资源注入到对象之中。假设容器在构造对象A时,对象A的构造依赖对象B、对象C、对象D这些参数,容器会将这些依赖关系自动构造好,最大限度的完成程序的解耦。依赖注入(Dependency Injection,简称DI)就是构造A对象时,需要依赖B对象,那么就先构造B对象作为参数传递到A对象,这种对象初始化并注入的技术就叫做依赖注入。

.NetCore本身就集成了一个轻量型的IOC容器,我们可以把程序中的抽象(Interface)或业务类根据需要的生命周期配置到容器中。他不是一次把所有的实例都构造出来,当你在用的时候他才会去构造实例!.NetCore内置的IOC容器只支持构造函数注入!

如下三种生命周期的注入:

Singleton:在第一次被请求的时候被创建,对象的构造函数只会执行一次然后一直会存在内存中之后就一直使用这一个实例。如果在多线程中使用了Singleton,要考虑线程安全的问题,保证它不会有冲突。

Scoped:一个请求只创建这一个实例,请求会被当成一个范围,也就是说单个请求对象的构造函数只会执行一次,同一次请求内注入多次也用的是同一个对象。假如一个请求会穿透应用层、领域层、基础设施层等等各层,在穿层的过程中肯定会有同一个类的多次注入,那这些多次注入在这个作用域下维持的就是单例。例如工作单元模式就可以借助这种模式的生命周期来实现,像EF Core的DbContext上下文就被注册为作用域服务。

Transient:每次请求时都会创建一个新的实例,只要注入一次,构造函数就会执行一次即实例化一次。Scoped、Transient的区别是你在同一个上下文中是否期望使用同一个实例,如果是,用Scoped,反之则使用Transient。

😊接下来我们来看下.Net Core中相关IOC核心源码😊

IOC源码

ServiceDescriptor

这个类是用来描述服务注册时的服务类型、实例类型、实现类型、生命周期等信息。就如同他本身的描述:Describes a service with its service type, implementation, and lifetime.       

DI容器ServiceProvider之所以能够根据我们给定的服务类型(一般是一个接口类型)提供一个能够开箱即用的服务实例,是因为我们预先注册了相应的服务描述信息,就是说ServiceDescriptor为容器提供了正确的服务信息以供容器选择对应的实例。

这个类一共有四个构造函数,本质其实就是给描述具体注册的LifetimeServiceTypeImplementationTypeImplementationInstanceImplementationFactory赋值。请看源码:

这里的话只列举其中的一个构造函数,因为我发现在文章中源码不能罗列的太多,不然让人看得嫌烦😅

下面这个构造函数需要两个参数,分别是服务元素的类型,服务元素实例,默认是单例的,其它的几个构造函数都与其很相仿。

/// <summary>
/// Initializes a new instance of <see cref="ServiceDescriptor"/> with the specified <paramref name="instance"/>
/// as a <see cref="ServiceLifetime.Singleton"/>.
/// </summary>
/// <param name="serviceType">The <see cref="Type"/> of the service.</param>
/// <param name="instance">The instance implementing the service.</param>
public ServiceDescriptor(
    Type serviceType,
    object instance)
    : this(serviceType, ServiceLifetime.Singleton)
{
    if (serviceType == null)
    {
        throw new ArgumentNullException(nameof(serviceType));
    }
    if (instance == null)
    {
        throw new ArgumentNullException(nameof(instance));
    }
    ImplementationInstance = instance;
}

 我们再来看下它里面几个重要的函数:

Describe()函数,可以看到这个函数也是用来生成ServiceDescriptor类的,它们分别对应上面的构造函数,也就是说为实例化ServiceDescriptor提供另外一种方式。源码如下:

private static ServiceDescriptor Describe<TService, TImplementation>(ServiceLifetime lifetime)
    where TService : class
    where TImplementation : class, TService
{
    return Describe(
        typeof(TService),
        typeof(TImplementation),
        lifetime: lifetime);
}
/// <summary>
/// Creates an instance of <see cref="ServiceDescriptor"/> with the specified
/// <paramref name="serviceType"/>, <paramref name="implementationType"/>,
/// and <paramref name="lifetime"/>.
/// </summary>
/// <param name="serviceType">The type of the service.</param>
/// <param name="implementationType">The type of the implementation.</param>
/// <param name="lifetime">The lifetime of the service.</param>
/// <returns>A new instance of <see cref="ServiceDescriptor"/>.</returns>
public static ServiceDescriptor Describe(Type serviceType, Type implementationType, ServiceLifetime lifetime)
{
    return new ServiceDescriptor(serviceType, implementationType, lifetime);
}
/// <summary>
/// Creates an instance of <see cref="ServiceDescriptor"/> with the specified
/// <paramref name="serviceType"/>, <paramref name="implementationFactory"/>,
/// and <paramref name="lifetime"/>.
/// </summary>
/// <param name="serviceType">The type of the service.</param>
/// <param name="implementationFactory">A factory to create new instances of the service implementation.</param>
/// <param name="lifetime">The lifetime of the service.</param>
/// <returns>A new instance of <see cref="ServiceDescriptor"/>.</returns>
public static ServiceDescriptor Describe(Type serviceType, Func<IServiceProvider, object> implementationFactory, ServiceLifetime lifetime)
{
    return new ServiceDescriptor(serviceType, implementationFactory, lifetime);
}

Singleton()函数,这个函数一共有七个重载,我只贴出其中三个方法,其它几个都差不多,总之就是一句话用于生成单例化的ServiceDescriptor类。源码如下:

/// <summary>
/// Creates an instance of <see cref="ServiceDescriptor"/> with the specified
/// <typeparamref name="TService"/>, <typeparamref name="TImplementation"/>,
/// and the <see cref="ServiceLifetime.Singleton"/> lifetime.
/// </summary>
/// <typeparam name="TService">The type of the service.</typeparam>
/// <typeparam name="TImplementation">The type of the implementation.</typeparam>
/// <returns>A new instance of <see cref="ServiceDescriptor"/>.</returns>
public static ServiceDescriptor Singleton<TService, TImplementation>()
    where TService : class
    where TImplementation : class, TService
{
    return Describe<TService, TImplementation>(ServiceLifetime.Singleton);
}
/// <summary>
/// Creates an instance of <see cref="ServiceDescriptor"/> with the specified
/// <paramref name="service"/> and <paramref name="implementationType"/>
/// and the <see cref="ServiceLifetime.Singleton"/> lifetime.
/// </summary>
/// <param name="service">The type of the service.</param>
/// <param name="implementationType">The type of the implementation.</param>
/// <returns>A new instance of <see cref="ServiceDescriptor"/>.</returns>
public static ServiceDescriptor Singleton(Type service, Type implementationType)
{
    if (service == null)
    {
        throw new ArgumentNullException(nameof(service));
    }
    if (implementationType == null)
    {
        throw new ArgumentNullException(nameof(implementationType));
    }
    return Describe(service, implementationType, ServiceLifetime.Singleton);
}
/// <summary>
/// Creates an instance of <see cref="ServiceDescriptor"/> with the specified
/// <paramref name="serviceType"/>, <paramref name="implementationInstance"/>,
/// and the <see cref="ServiceLifetime.Scoped"/> lifetime.
/// </summary>
/// <param name="serviceType">The type of the service.</param>
/// <param name="implementationInstance">The instance of the implementation.</param>
/// <returns>A new instance of <see cref="ServiceDescriptor"/>.</returns>
public static ServiceDescriptor Singleton(
    Type serviceType,
    object implementationInstance)
{
    if (serviceType == null)
    {
        throw new ArgumentNullException(nameof(serviceType));
    }
    if (implementationInstance == null)
    {
        throw new ArgumentNullException(nameof(implementationInstance));
    }
    return new ServiceDescriptor(serviceType, implementationInstance);
}

Scoped()函数,这个函数有五个重载,其实他内部还是调用Describe()方法来生成作用域的ServiceDescriptor类。源码如下:

/// <summary>
/// Creates an instance of <see cref="ServiceDescriptor"/> with the specified
/// <typeparamref name="TService"/>, <typeparamref name="TImplementation"/>,
/// and the <see cref="ServiceLifetime.Scoped"/> lifetime.
/// </summary>
/// <typeparam name="TService">The type of the service.</typeparam>
/// <typeparam name="TImplementation">The type of the implementation.</typeparam>
/// <returns>A new instance of <see cref="ServiceDescriptor"/>.</returns>
public static ServiceDescriptor Scoped<TService, TImplementation>()
    where TService : class
    where TImplementation : class, TService
{
    return Describe<TService, TImplementation>(ServiceLifetime.Scoped);
}
/// <summary>
/// Creates an instance of <see cref="ServiceDescriptor"/> with the specified
/// <paramref name="service"/> and <paramref name="implementationType"/>
/// and the <see cref="ServiceLifetime.Scoped"/> lifetime.
/// </summary>
/// <param name="service">The type of the service.</param>
/// <param name="implementationType">The type of the implementation.</param>
/// <returns>A new instance of <see cref="ServiceDescriptor"/>.</returns>
public static ServiceDescriptor Scoped(Type service, Type implementationType)
{
    return Describe(service, implementationType, ServiceLifetime.Scoped);
}

通过Scoped()方法我们可以这样注册:

services.Add(ServiceDescriptor.Scoped<IPersonService, PersonService>());
//services.Add(ServiceDescriptor.Scoped(typeof(IPersonService),typeof(PersonService)));
//或
services.Add(ServiceDescriptor.Transient<IPersonService, PersonService>());
//services.Add(ServiceDescriptor.Transient(typeof(IPersonService), typeof(PersonService)));
//或
services.Add(ServiceDescriptor.Singleton<IPersonService, PersonService>());
//services.Add(ServiceDescriptor.Singleton(typeof(IPersonService), typeof(PersonService)));

这种注册方式是通过ServiceDescriptor自身的操作去注册相关实例,我们拿出来其中一个Transient方法看一下具体实现:

public static ServiceDescriptor Transient<TService, TImplementation>()
    where TService : class
    where TImplementation : class, TService
{
    //都是在调用Describe
    return Describe<TService, TImplementation>(ServiceLifetime.Transient);
}
 
 
public static ServiceDescriptor Transient(Type service, Type implementationType)
{
    //都是在调用Describe
    return Describe(service, implementationType, ServiceLifetime.Transient);
}
 
 
public static ServiceDescriptor Describe(Type serviceType, Type implementationType, ServiceLifetime lifetime)
{
    //还是返回ServiceDescriptor实例
    return new ServiceDescriptor(serviceType, implementationType, lifetime);
}
 
 
public static ServiceDescriptor Describe(Type serviceType, Func<IServiceProvider, object> implementationFactory, ServiceLifetime lifetime)
{
    //还是返回ServiceDescriptor实例
    return new ServiceDescriptor(serviceType, implementationFactory, lifetime);
}

通过这个我们就可以了解到ServiceDescriptor.SingletonServiceDescriptor.ScopedServiceDescriptor.Transient其实是调用的Describe()方法,Describe()的本身还是去实例化ServiceDescriptor,殊途同归,只是多了种写法,最终还是去构建ServiceDescriptor。通过这么多源码的分析得出的结论就一点IServiceCollection注册的本质就是在构建ServiceDescriptor集合。

IServiceCollection

IServiceCollection是一个轻量级的依赖注入容器,来自于Microsoft.Extensions.DependencyInjection这个包。

我们来看一下它的定义:

using System.Collections;
using System.Collections.Generic;

namespace Microsoft.Extensions.DependencyInjection
{
  public interface IServiceCollection : 
    IList<ServiceDescriptor>,
    ICollection<ServiceDescriptor>,
    IEnumerable<ServiceDescriptor>,
    IEnumerable
  {
  }
}

可以看到它已经具备集合的增删改查基本功能了,并且它和ServiceDescriptor进行了挂钩,是保存ServiceDescriptor的数据结构接口。但是里面并没有AddScopedAddTransientAddSingleton这几个方法,说明这几个方法是写在其它类中的扩展方法。IServiceCollection也没有定义其任何成员,而是从IList<ServiceDescriptor>、ICollection<ServiceDescriptor>、IEnumerable<ServiceDescriptor>派生

ServiceCollection

我们接下来看下ServiceCollection的源码,ServiceCollection就是ServiceDescriptor的集合,它全部是对ServiceDescriptor集合进行操作。如下:

/// <summary>
/// Default implementation of <see cref="IServiceCollection"/>.
/// </summary>
public class ServiceCollection : IServiceCollection
{
    private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
    /// <inheritdoc />
    public int Count => _descriptors.Count;
    /// <inheritdoc />
    public bool IsReadOnly => false;
    /// <inheritdoc />
    public ServiceDescriptor this[int index]
    {
        get
        {
            return _descriptors[index];
        }
        set
        {
            _descriptors[index] = value;
        }
    }
    /// <inheritdoc />
    public void Clear()
    {
        _descriptors.Clear();
    }
    /// <inheritdoc />
    public bool Contains(ServiceDescriptor item)
    {
        return _descriptors.Contains(item);
    }
    /// <inheritdoc />
    public void CopyTo(ServiceDescriptor[] array, int arrayIndex)
    {
        _descriptors.CopyTo(array, arrayIndex);
    }
    /// <inheritdoc />
    public bool Remove(ServiceDescriptor item)
    {
        return _descriptors.Remove(item);
    }
    /// <inheritdoc />
    public IEnumerator<ServiceDescriptor> GetEnumerator()
    {
        return _descriptors.GetEnumerator();
    }
    void ICollection<ServiceDescriptor>.Add(ServiceDescriptor item)
    {
        _descriptors.Add(item);
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    /// <inheritdoc />
    public int IndexOf(ServiceDescriptor item)
    {
        return _descriptors.IndexOf(item);
    }
    /// <inheritdoc />
    public void Insert(int index, ServiceDescriptor item)
    {
        _descriptors.Insert(index, item);
    }
    /// <inheritdoc />
    public void RemoveAt(int index)
    {
        _descriptors.RemoveAt(index);
    }
}

ServiceCollection是一个ServiceDescriptor队列实现类,主要作用是保存ServiceDescriptor对象,它默认定义的ServiceDescriptor集合是私有的。ServiceCollection承载了一个List的集合,由于实现了IList接口,所以该类实现了接口的方法,实现了对List集合的操作。总之ServiceCollection就是用来操作ServiceDescriptor对象的。

ServiceCollectionServiceExtensions

这个类里面都是我们平时服务注册用到的常用方法,即:AddTransientAddScopedAddSingleton。它们其实方法实现都差不多,由于篇幅原因我只贴出部分源码:

/// <summary>
/// Adds a scoped service of the type specified in <paramref name="serviceType"/> with an
/// implementation of the type specified in <paramref name="implementationType"/> to the
/// specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="serviceType">The type of the service to register.</param>
/// <param name="implementationType">The implementation type of the service.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <seealso cref="ServiceLifetime.Scoped"/>
public static IServiceCollection AddScoped(
    this IServiceCollection services,
    Type serviceType,
    Type implementationType)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }
    if (serviceType == null)
    {
        throw new ArgumentNullException(nameof(serviceType));
    }
    if (implementationType == null)
    {
        throw new ArgumentNullException(nameof(implementationType));
    }
    return Add(services, serviceType, implementationType, ServiceLifetime.Scoped);
}

private static IServiceCollection Add(
    IServiceCollection collection,
    Type serviceType,
    Type implementationType,
    ServiceLifetime lifetime)
{
    var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
    collection.Add(descriptor);
    return collection;
}
private static IServiceCollection Add(
    IServiceCollection collection,
    Type serviceType,
    Func<IServiceProvider, object> implementationFactory,
    ServiceLifetime lifetime)
{
    var descriptor = new ServiceDescriptor(serviceType, implementationFactory, lifetime);
    collection.Add(descriptor);
    return collection;
}

可以看到AddScoped()方法最终调用的还是Add()方法,一共有两个重载,每个注入的实例的生命周期是通过ServiceLifetime这个枚举来判断的。你看它代码都非常简单,就是构建ServiceDescriptor实例然后添加到IServiceCollectionIList中。下图这种只注册具体类型或者具体实例的方法,其实注册单类型的方法,也是通过调用的注入实例到抽象的方法,只不过是将自己注册给了自己。

来看一下ServiceLifetime的源码,这个枚举是为了我们注册服务实例的声明周期的,非常清晰不在过多讲述:

/// <summary>
/// Specifies the lifetime of a service in an <see cref="IServiceCollection"/>.
/// </summary>
public enum ServiceLifetime
{
    /// <summary>
    /// Specifies that a single instance of the service will be created.
    /// </summary>
    Singleton,
    /// <summary>
    /// Specifies that a new instance of the service will be created for each scope.
    /// </summary>
    /// <remarks>
    /// In ASP.NET Core applications a scope is created around each server request.
    /// </remarks>
    Scoped,
    /// <summary>
    /// Specifies that a new instance of the service will be created every time it is requested.
    /// </summary>
    Transient
}

IServiceProvider

IServiceProvider是依赖注入的核心接口 ,但是它里面的结构比较简单,只有一个方法,用于从容器中获取实例,其他获取实例的方法都是根据这个方法扩展而来:

/// <summary>Defines a mechanism for retrieving a service object; that is, an object that provides custom support to other objects.</summary>
public interface IServiceProvider
{
  /// <summary>Gets the service object of the specified type.</summary>
  /// <param name="serviceType">An object that specifies the type of service object to get.</param>
  /// <returns>A service object of type <paramref name="serviceType" />.
  /// 
  /// -or-
  /// 
  /// <see langword="null" /> if there is no service object of type <paramref name="serviceType" />.</returns>
  object? GetService(Type serviceType);
}

IServiceProvider作为服务提供者,是根据IServiceCollection构建出来的:

IServiceProvider serviceProvider = services.BuildServiceProvider();

BuildServiceProvider来自于ServiceCollectionContainerBuilderExtensions扩展类下的扩展方法,源码如下:

/// <summary>
/// Extension methods for building a <see cref="ServiceProvider"/> from an <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionContainerBuilderExtensions
{
    /// <summary>
    /// Creates a <see cref="ServiceProvider"/> containing services from the provided <see cref="IServiceCollection"/>.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection"/> containing service descriptors.</param>
    /// <returns>The <see cref="ServiceProvider"/>.</returns>
    public static ServiceProvider BuildServiceProvider(this IServiceCollection services)
    {
        return BuildServiceProvider(services, ServiceProviderOptions.Default);
    }
    /// <summary>
    /// Creates a <see cref="ServiceProvider"/> containing services from the provided <see cref="IServiceCollection"/>
    /// optionally enabling scope validation.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection"/> containing service descriptors.</param>
    /// <param name="validateScopes">
    /// <c>true</c> to perform check verifying that scoped services never gets resolved from root provider; otherwise <c>false</c>.
    /// </param>
    /// <returns>The <see cref="ServiceProvider"/>.</returns>
    public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes)
    {
        return services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = validateScopes });
    }
    /// <summary>
    /// Creates a <see cref="ServiceProvider"/> containing services from the provided <see cref="IServiceCollection"/>
    /// optionally enabling scope validation.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection"/> containing service descriptors.</param>
    /// <param name="options">
    /// Configures various service provider behaviors.
    /// </param>
    /// <returns>The <see cref="ServiceProvider"/>.</returns>
    public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }
        return new ServiceProvider(services, options);
    }
}

当我们构建ServiceProvider实例的时候,如果没有传递参数,他会创建一个ServiceProviderOptions的默认实例,ServiceProviderOptions这个类是 DI 容器的配置选项,主要可以用来配置ServiceProvider的行为,体现在其内部几个属性的值上面。

public class ServiceProviderOptions
{
    internal static readonly ServiceProviderOptions Default = new ServiceProviderOptions();
    /// <summary>
    /// 是否在每个作用域中验证服务的依赖关系。默认值为 false。
    /// </summary>
    public bool ValidateScopes { get; set; }
    /// <summary>
    /// 是否在构建 ServiceProvider 时验证所有服务的依赖关系。默认值为 true。
    /// </summary>
    public bool ValidateOnBuild { get; set; }
    /// <summary>
    /// 控制 ServiceProvider 的行为
    /// </summary>
    internal ServiceProviderMode Mode { get; set; }
}

internal enum ServiceProviderMode
{
    Default,//这是默认的服务提供程序模式,它使用基于反射的动态代码生成来创建服务对象。在大多数情况下,这种模式可以满足我们的需求。
    Dynamic,//这种模式也使用基于反射的动态代码生成来创建服务对象,但是它会尝试在编译时生成代码,以提高性能。这种模式需要在应用程序启动时进行编译,因此可能会增加启动时间。
    Runtime,//这种模式使用基于反射的动态代码生成来创建服务对象,但是它会尝试在运行时生成代码。这种模式可以提高性能,但是会增加内存使用量和启动时间。
    Expressions,//这种模式使用表达式树来创建服务对象。它比基于反射的动态代码生成更快,但是可能会增加内存使用量。
    ILEmit,//这种模式使用基于 IL 的代码生成来创建服务对象。它比基于反射的动态代码生成更快,但是需要更多的内存和启动时间。
}

ValidateScopes属性用于控制服务提供程序是否检查服务作用域的范围:

  • 如果开启了范围检查,程序将会在创建服务对象时检查服务对象的作用域是否与其依赖项的作用域匹配。如果不匹配,服务提供程序将会抛出一个异常,以避免出现潜在的 bug。开启作用域范围检查可以帮助我们在开发过程中及时发现问题,确保应用程序的稳定性和可靠性。特别是在使用多个作用域的情况下,开启作用域范围检查可以避免出现一些意想不到的问题。

  • 如果不开启范围检查,程序将不会进行作用域范围检查。这样可以提高服务对象的创建速度,但是也可能会导致一些潜在的 bug,因为服务对象的作用域与其依赖项的作用域不匹配。

ValidateOnBuild属性用于指定 DI 容器在创建服务实例时是否进行作用域范围检查,主要会检查服务实例的依赖关系是否在同一个作用域范围内,以确保服务实例的依赖关系正确:

  • 如果开启了依赖关系检查,容器会在创建服务实例时进行作用域范围检查,如果检查失败,则会抛出异常。

  • 如果不开启依赖关系检查,容器不会进行作用域范围检查,这可能会导致服务实例的依赖关系错误,从而导致应用程序出现问题。

因此,建议在开发过程中开启ValidateScopes 与ValidateOnBuild 属性,以确保应用程序的稳定性和可靠性。而在生产环境中,可以考虑关闭这两个属性以提高性能。

ServiceProvider

ServiceProvider是.NET Core 中依赖注入容器的核心实现,它提供了注册服务、获取服务、创建作用域等基本功能,并支持配置选项和模式,它也是IServiceProvider的默认实现类,这里的话我只贴出提供服务相关的代码:

/// <summary>
/// The default IServiceProvider.
/// </summary>
public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback, IAsyncDisposable
{
    private readonly IServiceProviderEngine _engine;
    private readonly CallSiteValidator _callSiteValidator;
    internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
    {
        IServiceProviderEngineCallback callback = null;
        if (options.ValidateScopes)
        {
            callback = this;
            _callSiteValidator = new CallSiteValidator();
        }
        //通过ServiceProviderOptions中的枚举值判断用哪种方式实例化对象
        switch (options.Mode)
        {
            case ServiceProviderMode.Default:
!NETCOREAPP
                _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);
e
                if (RuntimeFeature.IsSupported("IsDynamicCodeCompiled"))
                {
                    _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);
                }
                else
                {
                    // Don't try to compile Expressions/IL if they are going to get interpreted
                    _engine = new RuntimeServiceProviderEngine(serviceDescriptors, callback);
                }
if
                break;
            case ServiceProviderMode.Dynamic:
                _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);
                break;
            case ServiceProviderMode.Runtime:
                _engine = new RuntimeServiceProviderEngine(serviceDescriptors, callback);
                break;
IL_EMIT
            case ServiceProviderMode.ILEmit:
                _engine = new ILEmitServiceProviderEngine(serviceDescriptors, callback);
                break;
if
            case ServiceProviderMode.Expressions:
                _engine = new ExpressionsServiceProviderEngine(serviceDescriptors, callback);
                break;
            default:
                throw new NotSupportedException(nameof(options.Mode));
        }
        //当前创建服务实例时是否进行作用域范围检查
        if (options.ValidateOnBuild)
        {
            List<Exception> exceptions = null;
            foreach (var serviceDescriptor in serviceDescriptors)
            {
                try
                {
                    _engine.ValidateService(serviceDescriptor);
                }
                catch (Exception e)
                {
                    exceptions = exceptions ?? new List<Exception>();
                    exceptions.Add(e);
                }
            }
            if (exceptions != null)
            {
                throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray());
            }
        }
    }
    /// <summary>
    /// 通过IServiceProviderEngine获取实例
    /// </summary>
    public object GetService(Type serviceType) => _engine.GetService(serviceType);
}

IServiceProviderEngine接口是用于创建和管理依赖项注入容器的引擎,这个接口是为了提高依赖项注入容器的性能而设计的。

IServiceProviderEngine接口定义了一个CreateServiceProvider 方法,该方法使用 IServiceProviderEngine 实现创建一个新的 IServiceProvider 实例。

具体来说,IServiceProviderEngine接口的作用是:

  • 提供了一个可扩展的接口,允许开发人员实现自定义的依赖注入容器。
  • 通过使用编译时生成的代码,提高了依赖注入容器的性能。

  • 提供了一种更灵活的方式来管理依赖注入容器,以支持更高级的场景,例如模块化应用程序和插件式架构。

我们上面说到从容器中获取实例使用IServiceProvider中的GetService()方法,但是远不止这一种,还有我们的子作用域的创建是在哪个类中实现的,接下来我们来看一下ServiceProvider的另一个扩展类ServiceProviderServiceExtensions,源码如下:

/// <summary>
/// Extension methods for getting services from an <see cref="IServiceProvider" />.
/// </summary>
public static class ServiceProviderServiceExtensions
{
    /// <summary>
    /// Get service of type <typeparamref name="T"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    /// <typeparam name="T">The type of service object to get.</typeparam>
    /// <param name="provider">The <see cref="IServiceProvider"/> to retrieve the service object from.</param>
    /// <returns>A service object of type <typeparamref name="T"/> or null if there is no such service.</returns>
    public static T GetService<T>(this IServiceProvider provider)
    {
        if (provider == null)
        {
            throw new ArgumentNullException(nameof(provider));
        }
        return (T)provider.GetService(typeof(T));
    }
    /// <summary>
    /// Get service of type <paramref name="serviceType"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    /// <param name="provider">The <see cref="IServiceProvider"/> to retrieve the service object from.</param>
    /// <param name="serviceType">An object that specifies the type of service object to get.</param>
    /// <returns>A service object of type <paramref name="serviceType"/>.</returns>
    /// <exception cref="System.InvalidOperationException">There is no service of type <paramref name="serviceType"/>.</exception>
    public static object GetRequiredService(this IServiceProvider provider, Type serviceType)
    {
        if (provider == null)
        {
            throw new ArgumentNullException(nameof(provider));
        }
        if (serviceType == null)
        {
            throw new ArgumentNullException(nameof(serviceType));
        }
        var requiredServiceSupportingProvider = provider as ISupportRequiredService;
        if (requiredServiceSupportingProvider != null)
        {
            return requiredServiceSupportingProvider.GetRequiredService(serviceType);
        }
        var service = provider.GetService(serviceType);
        if (service == null)
        {
            throw new InvalidOperationException(Resources.FormatNoServiceRegistered(serviceType));
        }
        return service;
    }
    /// <summary>
    /// Get service of type <typeparamref name="T"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    /// <typeparam name="T">The type of service object to get.</typeparam>
    /// <param name="provider">The <see cref="IServiceProvider"/> to retrieve the service object from.</param>
    /// <returns>A service object of type <typeparamref name="T"/>.</returns>
    /// <exception cref="System.InvalidOperationException">There is no service of type <typeparamref name="T"/>.</exception>
    public static T GetRequiredService<T>(this IServiceProvider provider)
    {
        if (provider == null)
        {
            throw new ArgumentNullException(nameof(provider));
        }
        return (T)provider.GetRequiredService(typeof(T));
    }
    /// <summary>
    /// Get an enumeration of services of type <typeparamref name="T"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    /// <typeparam name="T">The type of service object to get.</typeparam>
    /// <param name="provider">The <see cref="IServiceProvider"/> to retrieve the services from.</param>
    /// <returns>An enumeration of services of type <typeparamref name="T"/>.</returns>
    public static IEnumerable<T> GetServices<T>(this IServiceProvider provider)
    {
        if (provider == null)
        {
            throw new ArgumentNullException(nameof(provider));
        }
        return provider.GetRequiredService<IEnumerable<T>>();
    }
    /// <summary>
    /// Get an enumeration of services of type <paramref name="serviceType"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    /// <param name="provider">The <see cref="IServiceProvider"/> to retrieve the services from.</param>
    /// <param name="serviceType">An object that specifies the type of service object to get.</param>
    /// <returns>An enumeration of services of type <paramref name="serviceType"/>.</returns>
    public static IEnumerable<object> GetServices(this IServiceProvider provider, Type serviceType)
    {
        if (provider == null)
        {
            throw new ArgumentNullException(nameof(provider));
        }
        if (serviceType == null)
        {
            throw new ArgumentNullException(nameof(serviceType));
        }
        var genericEnumerable = typeof(IEnumerable<>).MakeGenericType(serviceType);
        return (IEnumerable<object>)provider.GetRequiredService(genericEnumerable);
    }
    /// <summary>
    /// Creates a new <see cref="IServiceScope"/> that can be used to resolve scoped services.
    /// </summary>
    /// <param name="provider">The <see cref="IServiceProvider"/> to create the scope from.</param>
    /// <returns>A <see cref="IServiceScope"/> that can be used to resolve scoped services.</returns>
    public static IServiceScope CreateScope(this IServiceProvider provider)
    {
        return provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
    }
}

创建子作用域:

using (IServiceScope scope = serviceProvider.CreateScope())
{
    IServiceProvider scopeProvider = scope.ServiceProvider;
}
  • GetService(Type serviceType):从 DI 容器中获取指定类型的服务实例。如果 DI 容器中不存在该类型的服务,则返回 null。
  • GetRequiredService(Type serviceType):从 DI 容器中获取指定类型的服务实例。如果 DI 容器中不存在该类型的服务,则抛出异常。
  • GetServices(Type serviceType):从 DI 容器中获取指定类型的所有服务实例。如果 DI 容器中不存在该类型的服务,则返回一个空的IEnumerable
  • CreateScope():用于创建一个新的作用域(Scope)。作用域是依赖注入容器中的一种机制,它可以用于隔离不同部分之间的依赖关系,同时也可以控制依赖项的生命周期。在一个作用域中,可以创建一个新的IServiceProvider 实例,该实例只能访问当前作用域及其父作用域中注册的服务。这样,就可以在不同的作用域中使用不同的服务实例,从而实现依赖关系的隔离。该方法返回一个IServiceScope 实例,该实例包含了一个新的IServiceProvider 实例和一个作用域对象。可以通过调用IServiceScope 实例的ServiceProvider 属性来获取新的 IServiceProvider 实例,然后使用该实例来解析当前作用域及其父作用域中注册的服务。

IServiceProviderFactory

在.Net Core中提供了IServiceProviderFactory接口来替换默认的容器,通过实现 IServiceProviderFactory接口,可以创建自己的容器并将其与 ASP.NET Core 的依赖注入系统集成实现自定义的依赖注入容器。这个接口定义了一个方法CreateServiceProvider,它接受一个泛型参数TContainer,并返回一个实现了IServiceProvider接口的对象。源码如下:

/// <summary>
/// Provides an extension point for creating a container specific builder and an <see cref="IServiceProvider"/>.
/// </summary>
public interface IServiceProviderFactory<TContainerBuilder>
{
    /// <summary>
    /// Creates a container builder from an <see cref="IServiceCollection"/>.
    /// </summary>
    /// <param name="services">The collection of services</param>
    /// <returns>A container builder that can be used to create an <see cref="IServiceProvider"/>.</returns>
    TContainerBuilder CreateBuilder(IServiceCollection services);
    /// <summary>
    /// Creates an <see cref="IServiceProvider"/> from the container builder.
    /// </summary>
    /// <param name="containerBuilder">The container builder</param>
    /// <returns>An <see cref="IServiceProvider"/></returns>
    IServiceProvider CreateServiceProvider(TContainerBuilder containerBuilder);
}

IServiceProviderFactory接口的默认实现是DefaultServiceProviderFactory类,源码如下:

/// <summary>
/// Default implementation of <see cref="IServiceProviderFactory{TContainerBuilder}"/>.
/// </summary>
public class DefaultServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
{
    private readonly ServiceProviderOptions _options;
    /// <summary>
    /// Initializes a new instance of the <see cref="DefaultServiceProviderFactory"/> class
    /// with default options.
    /// </summary>
    /// <seealso cref="ServiceProviderOptions.Default"/>
    public DefaultServiceProviderFactory() : this(ServiceProviderOptions.Default)
    {
    }
    /// <summary>
    /// Initializes a new instance of the <see cref="DefaultServiceProviderFactory"/> class
    /// with the specified <paramref name="options"/>.
    /// </summary>
    /// <param name="options">The options to use for this instance.</param>
    public DefaultServiceProviderFactory(ServiceProviderOptions options)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }
        _options = options;
    }
    /// <inheritdoc />
    public IServiceCollection CreateBuilder(IServiceCollection services)
    {
        return services;
    }
    /// <inheritdoc />
    public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
    {
        return containerBuilder.BuildServiceProvider(_options);
    }
}

可以看到根本么有什么逻辑,就是把IServiceCollectionIServiceProvider向外提供,方便开发者能够替换默认的容器实现自定义的容器。

我们来看一下Autofac中的源码,AutofacServiceProviderFactory 这个类就是Autofac 对IServiceProviderFactory 的一个实现。源码如下:

/// <summary>
/// A factory for creating a <see cref="ContainerBuilder"/> and an <see cref="IServiceProvider" />.
/// </summary>
public class AutofacServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
{
    private readonly Action<ContainerBuilder> _configurationAction;
    private readonly ContainerBuildOptions _containerBuildOptions = ContainerBuildOptions.None;

    /// <summary>
    /// Initializes a new instance of the <see cref="AutofacServiceProviderFactory"/> class.
    /// </summary>
    /// <param name="containerBuildOptions">The container options to use when building the container.</param>
    /// <param name="configurationAction">Action on a <see cref="ContainerBuilder"/> that adds component registrations to the container.</param>
    public AutofacServiceProviderFactory(
        ContainerBuildOptions containerBuildOptions,
        Action<ContainerBuilder> configurationAction = null)
        : this(configurationAction) =>
        _containerBuildOptions = containerBuildOptions;

    /// <summary>
    /// Initializes a new instance of the <see cref="AutofacServiceProviderFactory"/> class.
    /// </summary>
    /// <param name="configurationAction">Action on a <see cref="ContainerBuilder"/> that adds component registrations to the container..</param>
    public AutofacServiceProviderFactory(Action<ContainerBuilder> configurationAction = null) =>
        _configurationAction = configurationAction ?? (builder => { });

    /// <summary>
    /// Creates a container builder from an <see cref="IServiceCollection" />.
    /// </summary>
    /// <param name="services">The collection of services.</param>
    /// <returns>A container builder that can be used to create an <see cref="IServiceProvider" />.</returns>
    public ContainerBuilder CreateBuilder(IServiceCollection services)
    {
        var builder = new ContainerBuilder();

        builder.Populate(services);

        _configurationAction(builder);

        return builder;
    }

    /// <summary>
    /// Creates an <see cref="IServiceProvider" /> from the container builder.
    /// </summary>
    /// <param name="containerBuilder">The container builder.</param>
    /// <returns>An <see cref="IServiceProvider" />.</returns>
    public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
    {
        if (containerBuilder == null)
        {
            throw new ArgumentNullException(nameof(containerBuilder));
        }

        var container = containerBuilder.Build(_containerBuildOptions);

        return new AutofacServiceProvider(container);
    }
}

CreateBuilder方法接收一个IServiceCollection类型的参数services,用于将服务注册到容器中。在方法中,首先创建一个ContainerBuilder实例,然后调用其 Populate方法将services中的服务注册到容器中。最后,如果存在自定义配置,就调用configurationAction对容器进行配置。

CreateServiceProvider方法接收一个ContainerBuilder类型的参数containerBuilder,用于创建IServiceProvider实例。在方法中,首先调用containerBuilderBuild方法创建容器实例,然后将容器实例传入AutofacServiceProvider的构造函数中创建IServiceProvider实例。

UseServiceProviderFactory

public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull
 {
     if (factory is null)
     {
         throw new ArgumentNullException(nameof(factory));
     }
     _operations.Add(b => b.UseServiceProviderFactory(factory));
     return this;
 }

UseServiceProviderFactory是一个扩展方法,用来注册自定义容器工厂,可以用builder.Host.UseServiceProviderFactory(new XXXFactory())来进行配置,从而实现更加灵活的依赖注入。

具体来说,我们可以通过实现IServiceProviderFactory接口来创建自定义的服务提供程序工厂,其中TContainer是服务提供程序容器的类型。然后,我们可以在UseServiceProviderFactory方法中使用这个工厂来创建服务提供程序。

如果我们想要使用 Autofac 作为服务提供程序容器,就可以实现IServiceProviderFactory接口来创建Autofac容器,然后在WebHostBuilderHostBuilder中使用UseServiceProviderFactory方法来指定使用这个工厂。这样,我们就可以在应用程序中使用 Autofac来管理依赖关系。

扩展

不能在项目中使用AddScopedAddSingletonAddTransient来注册对象至容器中,不是不推荐,是不能🤨。后面项目一旦膨胀起来维护变得很麻烦,所以我们要将项目中的dll进行判断然后将其中的类根据我们自定义的规则判断自动进行注册。这里的话我们还是用.Net Core自带的IOC进行扩展批量注入。

批量注入-使用接口的方式

我们定义三种类型的接口,让其分别代表单例、作用域与瞬时,然后扫描项目中的dll中的类型,判断是否直接或间接继承这三种接口,如果是则找到实例然后进行注册🍉

先定义三种类型的接口:

/// <summary>
/// 实现该接口的对象为作用域生命周期
/// </summary>
public interface IScopeDependency
{
}
/// <summary>
/// 实现该接口的对象为单例生命周期
/// </summary>
public interface ISingletonDependency
{
}
/// <summary>
/// 实现该接口的对象为瞬时生命周期
/// </summary>
public interface ITransientDependency
{
}

主要的实现方法,就是筛选我们需要的Type进行注册:

public static List<Type> BaseTypes { get; set; } = new();
public static IServiceCollection AddDataService(this IServiceCollection services, IConfiguration configuration)
{
    List<string> BaseTypeStrArr = (List<string>)configuration.GetSection("BaseTypes").Get(typeof(List<string>));
    BaseTypeStrArr.ForEach(item => { BaseTypes.Add(Type.GetType(item)); });
    var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
    var getFiles = Directory.GetFiles(path, "*.dll").Where(x => Match(x, configuration));
    var referencedAssemblies = getFiles.Select(Assembly.LoadFrom).ToList();
    var types = referencedAssemblies
                .SelectMany(m => m.DefinedTypes)
                .Select(type => type.AsType())
                .Where(IsMatchType).ToList();
    var implementTypes = types.Where(x => x.IsClass).ToList();
    var interfaceTypes = types.Where(x => x.IsInterface).ToList();
    //循环实例
    foreach (var implementType in implementTypes)
    {
        var type = GetMatchType(implementType);
        if (type != null)
        {
            var interfaceType = interfaceTypes.FirstOrDefault(x => x.IsAssignableFrom(implementType));
            if (interfaceType != null)
            {
                switch (type.Name)
                {
                    case "IScopeDependency":
                        services.AddScoped(interfaceType, implementType);
                        break;
                    case "ISingletonDependency":
                        services.AddSingleton(interfaceType, implementType);
                        break;
                    case "ITransientDependency":
                        services.AddTransient(interfaceType, implementType);
                        break;
                    default:
                        break;
                }
            }
        }
    }
    return services;
}

/// <summary>
/// 判断类型是否匹配
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsMatchType(Type type)
{
    return BaseTypes.Where(m => m != type && m.IsAssignableFrom(type)).Any();
}

/// <summary>
/// 获取BaseType
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static Type GetMatchType(Type type)
{
    return BaseTypes.Where(m => m.IsAssignableFrom(type)).FirstOrDefault();
}

/// <summary>
/// 程序集是否匹配
/// </summary>
public static bool Match(string assemblyName, IConfiguration configuration)
{
    assemblyName = Path.GetFileName(assemblyName);
    //if (assemblyName.StartsWith($"{AppDomain.CurrentDomain.FriendlyName}.XXX"))
    //    return false;
    return Regex.IsMatch(assemblyName, configuration["MatchAssemblies"], RegexOptions.IgnoreCase | RegexOptions.Compiled);
}

批量注入-使用注解的方式

我们先定义一个特性,可传入一个生命周期的枚举参数,然后在需要的类上面标注,如果这个类有接口则将自己和对应的接口进行注册,如果没有接口则只注入自己🍎

定义特性类,用于声明当前标注的类的生命周期

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CoreInjectAttribute : Attribute
{
    public CoreInjectAttribute(ServiceLifetime injectType)
    {
        InjectType = injectType;
    }

    /// <summary>
    /// 生命周期
    /// </summary>
    public ServiceLifetime InjectType { get; set; }
}

主要的实现方法,代码都比较简单,我就不再说了

public static IServiceCollection AddDataService(this IServiceCollection service)
{
    var basePath = ApplicationEnvironment.ApplicationBasePath;
    var path = AppDomain.CurrentDomain.BaseDirectory;
    var assemblies = Directory.GetFiles(path, "ICore.*.dll").Select(Assembly.LoadFrom).ToList();
    foreach (var assembly in assemblies)
    {
        var types = assembly.GetTypes().Where(a => a.GetCustomAttribute<CoreInjectAttribute>() != null)
            .ToList();
        if (types.Count <= 0) continue;
        foreach (var type in types)
        {
            var injectData = (CoreInjectAttribute)type.GetCustomAttribute<CoreInjectAttribute>();
            Type[] interfaces = type.GetInterfaces();
            if (!interfaces.Any())
            {
                service.RegisterType(type, type, injectData.InjectType);
                continue;
            }
            foreach (Type interfaceType in interfaces)
            {
                service.RegisterType(interfaceType, type, injectData.InjectType);
            }
        }
    }
    return service;
}

public static void RegisterType(this IServiceCollection service, Type interfaceType, Type serviceType, ServiceLifetime serviceLifetime)
{
    switch (serviceLifetime)
    {
        case ServiceLifetime.Scoped:
            service.AddScoped(interfaceType, serviceType);
            break;
        case ServiceLifetime.Singleton:
            service.AddSingleton(interfaceType, serviceType);
            break;
        case ServiceLifetime.Transient:
            service.AddTransient(interfaceType, serviceType);
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
}

实现属性注入

因为.Net Core默认是不支持属性注入的,我们可以通过IControllerActivator接口来实现让控制器支持属性注入。IControllerActivator 接口的作用是创建控制器实例,当一个请求到达时,框架需要创建一个控制器实例以处理该请求。我们可以通过实现 IControllerActivator 接口来自定义控制器实例的创建过程。其实很简单,我们定义一个特性,在控制器需要实例的属性上面标记一下,当请求到来时,我们从容器中取出对应的实例对其进行赋值即可。

[AttributeUsage(AttributeTargets.Property)]
public class InjectAttribute : Attribute
{
}

public class CustomControllerActivator : IControllerActivator
{
    /// <summary>
    /// 用于创建控制器实例信息,我们可以通过此方法实例属性信息
    /// <summary>
    public object Create(ControllerContext context)
    {
        var controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();//得到控制器的类型
        var controllerInstance = context.HttpContext.RequestServices.GetService(controllerType);//用容器完成控制器的实例化
        //循环标记Inject特性的属性
        foreach (var prop in controllerType.GetProperties().Where(p => p.IsDefined(typeof(InjectAttribute), true)))
        {
            //从容器中取出对应的实例
            var propValue = context.HttpContext.RequestServices.GetService(prop.PropertyType);
            prop.SetValue(controllerInstance, propValue);
        }

        return controllerInstance;
    }

    /// <summary>
    /// 用于释放控制器实例以便进行垃圾回收,
    /// 当 ASP.NET Core 框架不再需要使用控制器实例时,它会调用Release方法来通知IControllerActivator实现释放控制器实例。
    /// 在释放控制器实例之前,Release 方法可以执行任何必要的清理操作,例如关闭数据库连接、释放未托管资源等。
    /// <summary>
    public void Release(ControllerContext context, object controller)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (controller == null)
        {
            throw new ArgumentNullException(nameof(controller));
        }

        var disposable = controller as IDisposable;
        if (disposable != null)
        {
            disposable.Dispose();
        }
    }
}

我们先是说了一下IOC的理念,再介绍了.Net Core中三种注入的生命周期,然后看源码了解其内部是如何运作的以及怎样替换默认容器,最后用.Net Core自带容器实现批量注入以及扩展属性注入,希望对大家有所帮助🏝️



这篇关于.Net Core后端架构实战【3-介入IOC控制反转】的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程