解析.NET對象的跨應用程序域訪問(上篇)

   在目前的項目開發中,分佈式開發已經逐漸成爲主流。一個項目要是沒有采用分佈式架構,都很差意思跟別人說這是一個完整的項目。這句話雖然有些過激,可是隨着人們對效率的要求在提升,以及產品須要提高用戶體驗。只有在軟件項目的效率和體驗作到高質量,才能夠贏得用戶和市場。java

   對於.NET項目,咱們使用較多的分佈式結構有Webservice,.Net remoting,MSMQ,WCF,WebAPI等等,咱們在使用這些框架的時候,從這些分佈式框架中獲得了很好的用戶體驗。在.NET項目中,分佈式架構對項目的開發也有很大的效率提高。安全

   不少人會問,這些分佈式框架的底層原理是什麼呢?恐怕誰也不敢輕言幾句就能夠描述完畢,在這個博文系列中,就是簡單的描述一下這些分佈式結構的底層實現原理。架構

   本文主要講解對象在應用程序域中的傳遞。主要講解應用程序域的一些核心對象,對於應用程序域的操做出現的比較少,因此在這裏給出的是程序集的一些基本操做。若有不足之處,還望多多指正。app

一.AppDomain解析:

     AppDomain在不少場合都是被翻譯爲「應用程序域」,在本文中也將採用這一翻譯。對於.NET的開發者,對於CLR應該是最熟悉不過了,CLR相似於java的JVM。在CLR中,AppDomain規定了代碼的執行範圍,提供了錯誤隔離的程度,提供了一個安全隔離度,而且擁有本身的資源。AppDomain的具體功能,有以下圖:框架

  

   1.AppDomain概述:

      AppDomain相似與系統的進程,進程是有操做系統進行建立,AppDomain是由CLR進行建立。一個給定的AppDomain必須駐留在一個操做系統的進程中,而一個給定的進程能夠寄宿多個AppDomain。有以下圖:dom

   

      如上圖所示,一個對象正好存放在一個AppDomain種,值也同樣。一個AppDomain中的對象引用必須是引用同一AppDomain中的對象,AppDomain的行爲就好像擁有本身私有的地址空間。若是兩個AppDomain須要使用一個類型,必須爲每一個AppDomain分別初始化和分配一次類型。必須爲各個用到類型的AppDomain分別加載和初始化一次類型的方法和程序集。進程種的各個AppDomain要維護類型的不一樣拷貝。對於類型的靜態子類,每一個AppDomain都有其本身的私有副本。分佈式

     AppDomain的資源有如圖:ide

      對於應用AppDomain的資源被加載,一直在內存中,卸載AppDomain資源是惟一卸載模塊或者程序集的途徑,卸載AppDomain資源也是回收類型靜態字段所佔內存的惟一方式。ui

     在上面提到過操做系統的線程與AppDomain相似,在CLR中定義了System.Threading.Thread,在AppDomain中表示爲可調度的實體,在這裏提出一個新的概念,那就是「軟線程」和「硬線程」,顧名思義,操做系統的線程被稱爲「硬線程」,CLR中的System.Threading.Thread被稱爲「軟線程」。一個CLR軟線程對象駐留在一個肯定的AppDomain中;一個給定的AppDomain可能有多個軟線程對象。在當前的CLR中,對於給定的AppDomain,硬線程至多有一個軟線程對象屬於他,若是一個硬線程運行在多個AppDomain中,每一個AppDomain都會有一個明顯的軟線程對象屬於該線程。當給定的硬線程進入AppDomain後,就會獲得一樣的軟線程對象。this

   2.AppDomain核心對象解析:

     上面介紹了一些AppDomain的基本概念,接下來咱們來簡單瞭解一下AppDomain的相關操做和核心對象。在.NET種能夠經過System.AppDomain類型訪問AppDomain。在這裏咱們具體瞭解一下System.AppDomain類型的方法和屬性。對於該類的說明:https://msdn.microsoft.com/en-us/library/system.appdomain(v=vs.110).aspx。  

   (1).CurrentDomain:獲取當前Thread 的當前應用程序域。

public static AppDomain CurrentDomain
    {
      get
      {
        return Thread.GetDomain();
      }
    }

    由以上代碼可知,該屬性爲一個靜態屬性,而且只有一個只讀屬性。該屬性只是簡單地提取存儲在硬線程的TLS(線程本地存儲區)中的AppDomain引用。你能夠在Thread.CurrentThread屬性中,從硬線程的TLS中提取當前的軟線程對象。 

   (2).GetData():爲指定名稱獲取存儲在當前應用程序域中的值。

[SecuritySafeCritical]
    public object GetData(string name)
    {
      if (name == null)
        throw new ArgumentNullException("name");
      switch (AppDomainSetup.Locate(name))
      {
        case -1:
          if (name.Equals(AppDomainSetup.LoaderOptimizationKey))
            return (object) this.FusionStore.LoaderOptimization;
          object syncRoot = ((ICollection) this.LocalStore).SyncRoot;
          bool lockTaken = false;
          object[] objArray;
          try
          {
            Monitor.Enter(syncRoot, ref lockTaken);
            this.LocalStore.TryGetValue(name, out objArray);
          }
          finally
          {
            if (lockTaken)
              Monitor.Exit(syncRoot);
          }
          if (objArray == null)
            return (object) null;
          if (objArray[1] != null)
            ((IPermission) objArray[1]).Demand();
          return objArray[0];
        case 0:
          return (object) this.FusionStore.ApplicationBase;
        case 1:
          return (object) this.FusionStore.ConfigurationFile;
        case 2:
          return (object) this.FusionStore.DynamicBase;
        case 3:
          return (object) this.FusionStore.DeveloperPath;
        case 4:
          return (object) this.FusionStore.ApplicationName;
        case 5:
          return (object) this.FusionStore.PrivateBinPath;
        case 6:
          return (object) this.FusionStore.PrivateBinPathProbe;
        case 7:
          return (object) this.FusionStore.ShadowCopyDirectories;
        case 8:
          return (object) this.FusionStore.ShadowCopyFiles;
        case 9:
          return (object) this.FusionStore.CachePath;
        case 10:
          return (object) this.FusionStore.LicenseFile;
        case 11:
          return (object) (bool) (this.FusionStore.DisallowPublisherPolicy ? 1 : 0);
        case 12:
          return (object) (bool) (this.FusionStore.DisallowCodeDownload ? 1 : 0);
        case 13:
          return (object) (bool) (this.FusionStore.DisallowBindingRedirects ? 1 : 0);
        case 14:
          return (object) (bool) (this.FusionStore.DisallowApplicationBaseProbing ? 1 : 0);
        case 15:
          return (object) this.FusionStore.GetConfigurationBytes();
        default:
          return (object) null;
      }
    }

   每個AppDomain有本身的環境屬性集,能夠經過SetData和GetData方法訪問,在這裏給出了GetData()方法的源碼。該方法接收一個string參數,預約義應用程序域屬性的名稱,或已定義的應用程序域屬性的名稱。返回一個屬性的值,或 null(若是屬性不存在)。AppDomainSetup類爲一個封閉類,表示能夠添加到System.AppDomain的實例的程序集綁定信息。

   (3).CreateDomain:使用指定的名稱、證據和應用程序域設置信息建立新的應用程序域。

[SecuritySafeCritical]
    [SecurityPermission(SecurityAction.Demand, ControlAppDomain = true)]
    public static AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup info)
    {
      return AppDomain.InternalCreateDomain(friendlyName, securityInfo, info);
    }

   該方法存在幾個重載,接收三個參數,域的友好名稱。friendlyName:此友好名稱可在用戶界面中顯示以標識域;securityInfo:肯定代碼標識的證據,該代碼在應用程序域中運行。傳遞 null 以使用當前應用程序域的證據。info:包含應用程序域初始化信息的對象。該方法返回一個新建立的應用程序域。

   (4).ExecuteAssembly():使用指定的證據和實參執行指定文件中包含的程序集。

 [Obsolete("Methods which use evidence to sandbox are obsolete and will be removed in a future release of the .NET Framework. Please use an overload of ExecuteAssembly which does not take an Evidence parameter. See http://go.microsoft.com/fwlink/?LinkID=155570 for more information.")]
    public int ExecuteAssembly(string assemblyFile, Evidence assemblySecurity, string[] args)
    {
      if (assemblySecurity != null && !this.IsLegacyCasPolicyEnabled)
        throw new NotSupportedException(Environment.GetResourceString("NotSupported_RequiresCasPolicyImplicit"));
      RuntimeAssembly assembly = (RuntimeAssembly) Assembly.LoadFrom(assemblyFile, assemblySecurity);
      if (args == null)
        args = new string[0];
      return this.nExecuteAssembly(assembly, args);
    }

   當建立一個AppDomain後,可使用一系列技術強制它加載和執行代碼,能夠採用ExecuteAssembly方法。該方法將目標AppDomain加載到程序集中,而且執行其主入口點。在父AppDomain種,ExecuteAssembly方法不會加載或者初始化指定的程序集。ExecuteAssembly是一個同步的例程,這就意味着調用者將被阻塞,直到程序的Main方法把控制權交還運行時。

   ExecuteAssembly方法存在幾個重載版本,在這裏只拿出一個版原本說明。該方法接收三個參數,assemblyFile:包含要執行程序集的文件的名稱;assemblySecurity:爲程序集提供的證據;args:程序集的入口點的實參。該方法返回 程序集的入口點返回的值。該方法使用Assembly.LoadFrom來加載程序集。有關程序集的內容將在下一篇講解。

   (5).DoCallBack():在另外一個應用程序域中執行代碼,該應用程序域由指定的委託標識。

 public void DoCallBack(CrossAppDomainDelegate callBackDelegate)
    {
      if (callBackDelegate == null)
        throw new ArgumentNullException("callBackDelegate");
      callBackDelegate();
    }

   這個指定方法必須是靜態的,而且它的簽名與CrossAppDomainDelegate簽名匹配。

三.程序集操做實例:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace AppDomainToolkit
{

    /// <summary>
    /// 用於肯定加載器應加載哪些加載上下文程序集。
    /// </summary>
    public enum LoadMethod
    {
        /// <summary>
        /// 將程序集加載到LoadFrom上下文中,這將使程序集及其全部引用被發現
                 ///並加載到目標應用程序域中。 儘管它對DLL地獄的傾向,這多是去的方式
                 /// default,只要確保將應用程序的基本目錄傳遞給AssemblyResolver實例等
                 ///能夠正確解析引用。 這也容許同時加載同名的多個程序集
                 ///維護單獨的文件名。 這是推薦的方式。
        /// </summary>
        LoadFrom,

        /// <summary>
        /// 使用原始文件名將組合件加載到內存中。 這將以匿名方式加載程序集,所以它不會有
                 ///一個加載上下文。 使用這個,若是你想要的位加載,但確保經過這個文件所在的目錄
                 /// AssemblyResolver實例,以便您能夠再次找到它。 這是相似於LoadFrom,除非你沒有獲得免費
                 ///經過融合查找已經存在的程序集名稱。 使用它能夠更好地控制彙編文件加載。
        /// </summary>
        LoadFile,

        /// <summary>
        /// 使用原始文件名將目標程序集的位加載到內存中。 這本質上是一個動態組件
                 ///爲全部的CLR關心。 你將永遠不能找到這個與程序集解析器,因此不要使用這,除非你看
                 ///按名稱。 當心這一個。
        /// </summary>
        LoadBits
    }

    /// <summary>
    /// 這個類將會把程序集加載到它加載到的任何應用程序域中。 這只是一個簡單的方便
    /// wrapper環繞靜態Assembly.Load *方法,主要的好處是可以加載程序集
    ///匿名按位。 當您以這種方式加載程序集時,不會有任何DLL文件的鎖定。
    /// </summary>
    public class AssemblyLoader : MarshalByRefObject, IAssemblyLoader
    {
        #region Public Methods

        /// <inheritdoc /> 
        /// <remarks>
        /// 若是此實例的LoadMethod設置爲LoadBits,而且PDB文件的路徑未指定,那麼咱們將嘗試猜想
                 ///到PDB的路徑並加載它。 注意,若是一個程序集被加載到內存中而沒有調試符號,那麼
                 /// image將被拋出。 警戒這個。 使用LoadBits方法加載程序集不會鎖定
                 /// DLL文件,由於整個程序集被加載到內存中而且文件句柄被關閉。 可是,
                 ///以這種方式加載的程序集不會有與之關聯的位置,所以您必須鍵入程序集
                 ///它的強名。 當將同一程序集的多個版本加載到一個程序集時,這可能會致使問題
                 ///應用程序域。
        /// </remarks>
        public Assembly LoadAssembly(LoadMethod loadMethod, string assemblyPath, string pdbPath = null)
        {
            Assembly assembly = null;
            switch (loadMethod)
            {
                case LoadMethod.LoadFrom:
                    assembly = Assembly.LoadFrom(assemblyPath);
                    break;
                case LoadMethod.LoadFile:
                    assembly = Assembly.LoadFile(assemblyPath);
                    break;
                case LoadMethod.LoadBits:

                    // Attempt to load the PDB bits along with the assembly to avoid image exceptions.
                    pdbPath = string.IsNullOrEmpty(pdbPath) ? Path.ChangeExtension(assemblyPath, "pdb") : pdbPath;

                    // Only load the PDB if it exists--we may be dealing with a release assembly.
                    if (File.Exists(pdbPath))
                    {
                        assembly = Assembly.Load(
                            File.ReadAllBytes(assemblyPath),
                            File.ReadAllBytes(pdbPath));
                    }
                    else
                    {
                        assembly = Assembly.Load(File.ReadAllBytes(assemblyPath));
                    }

                    break;
                default:
                    // In case we upadate the enum but forget to update this logic.
                    throw new NotSupportedException("The target load method isn't supported!");
            }

            return assembly;
        }

        /// <inheritdoc />
        /// <remarks>
        /// 這個實現將執行目標程序集的盡力負載,它是必需的引用
                 ///進入當前應用程序域。 .NET框架在咱們容許使用的調用上鎖定咱們
                 ///當加載這些程序集時,因此咱們須要依賴於AssemblyResolver實例附加的
                 /// AppDomain爲了加載咱們想要的方式。
        /// </remarks>
        public IList<Assembly> LoadAssemblyWithReferences(LoadMethod loadMethod, string assemblyPath)
        {
            var list = new List<Assembly>();
            var assembly = this.LoadAssembly(loadMethod, assemblyPath);
            list.Add(assembly);

            foreach (var reference in assembly.GetReferencedAssemblies())
            {
                list.Add(Assembly.Load(reference));
            }

            return list;
        }

        /// <inheritdoc />
        /// <remarks>
        /// Just a simple call to AppDomain.CurrentDomain.GetAssemblies(), nothing more.
        /// </remarks>
        public Assembly[] GetAssemblies()
        {
            return AppDomain.CurrentDomain.GetAssemblies();
        }

        #endregion
    }
}

四.總結:

   本文主要講解了應用程序域的相關概念,本系列主要講解.NET對象的跨應用程序域的傳遞,因爲設計應用程序域的內容,因此本文主要講解了一些基本概念,以及一些基本的對象,對於應用程序域包含的程序集的相關內容將在下面進行操做。在實際的項目中,不多直接取操做應用程序域,比較多的是直接操做程序集,因此在本文的最後給出了一個就暗淡的程序集的操做方法。

相關文章
相關標籤/搜索