深入探究ASP.NET Core Startup初始化问题
前言
startup类相信大家都比较熟悉,在我们使用asp.net core开发过程中经常用到的类,我们通常使用它进行ioc服务注册,配置中间件信息等。虽然它不是必须的,但是将这些操作统一在startup中做处理,会在实际开发中带来许多方便。当我们谈起startup类的时候你有没有好奇过以下几点
- 为何我们自定义的startup可以正常工作。
- 我们定义的startup类中configureservices和configure只能叫这个名字才能被调用到吗?
- 在使用泛型主机(ihostbuilder)时startup的构造函数,为何只支持注入iwebhostenvironment、ihostenvironment、iconfiguration。
- configureservices方法为何只能传递iservicecollection实例。
- configure方法的参数为何可以是所有在iservicecollection注册服务实例。
- 在asp.net core结合autofac使用的时候为何我们添加的configurecontainer方法会被调用。
- 带着以上几点疑问,我们将在本篇文章中探索startup的源码,来了解startup初始化过程到底为我们做了些什么。
startup的另类指定方式
在日常编码过程中,我们通常使用usestartup的方式来引入startup类。但是这并不是唯一的方式,还有一种方式是在配置节点中指定startup所在的程序集来自动查找startup类,这个我们可以在genericwebhostbuilder的构造函数源码中的找到相关代码[点击查看源码????]相信熟悉asp.net core启动流程的同学对genericwebhostbuilder这个类都比较了解。configurewebhostdefaults方法中其实调用了configurewebhost方法,configurewebhost方法中实例化了genericwebhostbuilder对象,启动流程不是咱们的重点,所以这里只是简单描述一下。直接找到我们需要的代码如下所示
这里我们可以看出来,我们需要配置startupassembly对应的程序集,它可以通过startuploader的findstartuptype方法加载程序集中对应的类。我们还可以看到它还传递了environmentname环境变量,至于它起到了什么作用,我们继续往下看。
首先我们需要找到webhostoptions.startupassembly是如何被初始化的,在webhostoptions的构造函数中我们找到了startupassembly初始化的地方[点击查看源码????]
从这里也可以看出来它的值来于配置,它的key来自webhostdefaults.startupassemblykey这个常量值,最后我们找到了的值为
也就是说只要我们给startupassembly配置startup所在的程序集名称,它就可以在程序集中查找startup类进行初始化,如下所示
回到上面的思路,我们在startuploader类中查看findstartuptype方法,来看下它是通过什么规则来查找startup的[点击查看源码????]精简之后的代码大致如下
通过上述代码我们可以看到在通过配置指定程序集时是如何查找指定规则的startup类的,基本上可以理解为先去查找名称为startup+环境变量的类,如果找不到则继续查找名称为startup的类,最终会返回startup的类型传递给usestartup方法。其实我们最常使用的usestartup
startup的构造函数
相信对startup有所了解的同学们都比较清楚,在使用泛型主机(ihostbuilder)时startup的构造函数只支持注入iwebhostenvironment、ihostenvironment、iconfiguration,这个在微软官方文档中也有介绍,如果还有不熟悉这个操作的请先反思一下自己,然后在查阅微软官方文档。接下来我们就从源码着手,来探究一下它到底是如何做到的。沿着上述的操作,继续查看usestartup里的代码找到了如下的实现[点击查看源码????]
这里的startuptype就是我们传递的startup类型,关于activatorutilities这个类还是比较实用的,它为我们提供了许多帮助我们实例化对象的方法,在日常编程中如果有需要可以使用这个类。上面的activatorutilities的createinstance方法的功能就是根据传递iserviceprovider类型的对象去实例化指定的类型对象,我们这里的类型就是startuptype。它的使用场景就是,如果某个类型需要用过有参构造函数去实例化,而构造函数的参数可以来自于iserviceprovider的实例,那么使用这个方法就在合适不过了。上面的代码传递的iserviceprovider的实例是hostserviceprovider对象,接下来我们找到它的实现源码[点击查看源码????]代码并不多我们就全部粘贴出来
通过这个内部私有类我们就能清晰的看到为何starup的构造函数只能注入iwebhostenvironment、ihostenvironment、iconfiguration相关实例了,hostserviceprovider类实现了iserviceprovider的getservice方法并做了判断,只有满足这几种类型才能返回具体的实例注入,其它不满足条件的类型都会返回null。因此在初始化starup实例的时候,通过构造函数注入的类型也就只能是这几种了。最终通过这个构造函数初始化了startup类的实例。
configureservices的装载
接下来我们就来在usestartup方法里继续查看是如何查找并执行configureservices方法的,继续查看找到如下实现[点击查看源码????]
从上述代码中我们可以了解到查找并执行configureservices方法的具体步骤可分为三步,首先在startuptype类型中根据环境变量名称查找具体方法返回configureservicesbuilder实例,然后构建configureservicesbuilder实例返回configureservices方法的委托,最后传递iservicecollection对象执行委托方法。接下来我们就来查看具体实现源码。
我们在startuploader类中找到了findconfigureservicesdelegate方法的相关实现[点击查看源码????]
通过这里的源码我们可以看到在startuptype类型里去查找名字为environmentname构建的configure{0}services的方法信息,然后根据查找的方法信息即methodinfo对象去构建configureservicesbuilder实例。接下里我们就来查询findmethod方法的实现
通过findmethod方法我们可以得到几个结论,首先configureservices方法的名称可以是包含环境变量的名称比如(configuredevelopmentservices),其次方法可以为共有的静态或非静态方法。findmethod方法是真正执行查找的逻辑所在,如果找到相关方法则返回methodinfo。findmethod查找的方法名称是通过methodname参数传递进来的,我们标注的注释代码都是直接写死了configureservices方法,只是为了便于说明理解,但其实findmethod是通用方法,接下来我们要讲解的内容还会涉及到这个方法,到时候关于这个代码的逻辑我们就不会在进行说明了,因为是同一个方法,希望大家能注意到这一点。
通过上面的相关方法,我们了解到了是通过什么样的规则去查找到configureservices的方法信息的,我们也看到了configureservicesbuilder正是通过查找到的methodinfo去构造实例的,接下来我们就来查看下configureservicesbuilder的实现源码[点击查看源码????]
看完configureservicesbuilder类的实现逻辑,关于通过什么样的逻辑查找并执行configureservices方法的逻辑就非常清晰了。首先是查找configureservices方法,即包含环境变量的configureservices方法名称比如(configuredevelopmentservices)或名为configureservices的方法,返回的是configureservicesbuilder对象。然后执行configureservicesbuilder的build方法,这个方法里包含了执行configureservices的规则,即configureservices只能包含一个参数且类型为iservicecollection,然后将当前程序中存在的iservicecollection实例传递给它。
configure的装载
我们常使用startup的configure方法去配置中间件,默认生成的configure方法为我们添加了iapplicationbuilder和iwebhostenvironment实例,但是其实configure方法不仅仅可以传递这两个参数,它可以通过参数注入在iservicecollection中注册的所有服务,究竟是如何实现的呢,接下来我们继续探究usestartup方法查找源码查看想实现
[点击查看源码????],我们抽离出来核心实现如下
我们通过查看genericwebhostserviceoptions的源码可知configureapplication属性的类型为action
[点击查看源码????]
从这里我们可以看到findconfiguredelegate方法也是调用的findmethod方法,只是传递的方法名字符串为configure或configure+环境变量,关于findmethod的方法实现我们在上面讲解configureservices方法的时候已经非常详细的说过了,这里就不过多的讲解了。总之是通过findmethod去查找名为configure的方法或名为configure+环境变量的方法比如configuredevelopment查找规则和configureservices是完全一致的。但是configure方法却可以通过参数注入注册到iservicecollection中的服务,答案我们同样要在configurebuilder类中去探寻
[点击查看源码????]
通过configurebuilder类的实现逻辑,可以清晰的看到为何configure方法参数可以注入任何在iservicecollection中注册的服务了。接下来我们总结一下configure方法的初始化逻辑,首先在startup中查找方法名为configure或configure+环境变量名称(比如configuredevelopment)的方法,然后查找iapplicationbuilder类型的参数,如果找到则将程序中的iapplicationbuilder实例传递给它。至于为何configure方法能够通过参数注入任何在iservicecollection中注册的服务,则是因为循环configure中的所有参数然后在ioc容器中获取对应实例赋值过来,configure方法的参数一定得是在iservicecollection注册过的类型,否则会抛出异常。
configurecontainer为何会被调用
如果你在asp.net core 3.1中使用过autofac那么你对configurecontainer方法一定不陌生,它和configureservices、configure方法一样的神奇,在几乎没有任何约束的情况下我们只需要定义configurecontainer方法并为方法传递一个containerbuilder参数,那么这个方法就能顺利的被调用了。这一切究竟是如何实现的呢,接下来我们继续探究源码,找到了如下的逻辑
[点击查看源码????]
继续使用老配方,我们查看startuploader的findconfigurecontainerdelegate方法实现
[点击查看源码????]
果然还是这个配方这个味道,废话不多说直接查看configurecontainerbuilder源码
[点击查看源码????]
果不其然千年老方下来还是那个味道,和configureservices、configure方法思路几乎一致。这里需要注意的是getcontainertype获取的容器类型是configurecontainer方法的唯一参数即容器类型,如果传递多个参数则直接抛出异常。其实startup的configurecontainer方法经过花里胡哨的一番操作之后,最终还是转换成了雷士如下的操作方式,这个我们在上面代码中构建actiontype的时候就可以看出,最终通过查找到的容器类型去完成注册等相关操作,这里就不过多的讲解了
总结
本篇文章我们主要是围绕着startup是如何被初始化进行讲解的,分别讲解了startup是如何被实例化的,为何startup的构造函数只能传递iwebhostenvironment、ihostenvironment、iconfiguration类型的参数,以及configureservices、configure、configurecontainer方法是如何查找到并被初始化调用的。其中虽然涉及到的代码比较多,但是整体思路在阅读源码后还是比较清晰的。由于笔者文笔有限,可能许多地方描述的不够清晰,亦或是本人能力有限理解的不够透彻,不过本人在文章中都标记了源码所在位置的链接,如果有感兴趣的同学可以自行点击连接查看源码。startup类比较常用,如果能够更深层次的了解其原理,对我们实际编程过程中会有很大的帮助,同时呼吁更多的小伙伴们深入阅读了解.net core的源码并分享出来。如有各位有疑问或者有了解的更透彻的,欢迎评论区提问或批评指导。