使用Lua指令碼語言開發出高擴充套件性的系統,AgileEAS.NET SOA中介軟體Lua指令碼引擎介紹

魏瓊東發表於2014-01-09

一、前言

     AgileEAS.NET SOA 中介軟體平臺是一款基於基於敏捷並行開發思想和Microsoft .Net構件(元件)開發技術而構建的一個快速開發應用平臺。用於幫助中小型軟體企業建立一條適合市場快速變化的開發團隊,以達到節省開發成本、縮短開發時間,快速適應市場變化的目的。

     AgileEAS.NET SOA中介軟體平臺提供了敏捷快速開發軟體工程的最佳實踐,通過提供大量的基礎支撐功能如IOC、ORM、SOA、分散式體系及敏捷併發開發方法所支撐的外掛開發體系,以及提供了大量的實體、資料模型設計生成工具、程式碼生成工具,用於幫助中小軟體開發商快速成長。

     AgileEAS.NET平臺充分把握目前軟體行業快速發展的新趨勢,基於敏捷並行開發、快速適應市場這樣淳樸的軟體工程實踐,採用業界廣泛使用的Microsoft .Net構件(元件)開發技術實踐了這種開發思想,幫助軟體企業實現“敏捷變化、快速適合”的目標,從而幫助軟體企業在激烈的市場競爭中贏得先機並獲得更高的回報。

二、關於Lua語言

     Lua 是一個小巧的指令碼語言。是巴西里約熱內盧天主教大學(Pontifical Catholic University of Rio de Janeiro)裡的一個研究小組,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所組成並於1993年開發。 其設計目的是為了嵌入應用程式中,從而為應用程式提供靈活的擴充套件和定製功能。

     Lua語言目前應用最廣泛的領域是遊戲程式設計領域,最早接觸和認識Lua也是因為在2008年玩金山的劍網3,通過解包讀過其中的一些遊戲指令碼,慢慢也看過一些其他遊戲的Lua指令碼,在我們開發自己的電子病歷系統的過程之中,引入了Lua指令碼語言實現那些可變性非常高的場景。

    .NET通過LuaInterface開源專案類庫實現對Lua的指令碼呼叫以及Lua與C#的相互繫結,有關於這此方面的內容請大家搜尋相關文章以獲得幫助。

     在AgileEAS.NET SOA5.0版本之中,我們決定把在開發過程之中形成的Lua指令碼引擎一併整合入AgileEAS.NET SOA中介軟體平臺,目前Lua指令碼語言被我們廣泛的應用電子病歷系統這中的病案自動評分、簡訊系統之中的互動式應答、和一些計劃任系統之中的計劃任務定義。

三、AgileEAS.NET SOA平臺Lua引擎

     AgileEAS.NET SOA5.0平臺目前使用的是Lua5.1版本,使用LuaInterface實現C#與Lua的相互繫結處理,平臺已經對其進行了二次封裝以提供統一的API支援,目前由EAS.LuaScript.dll程式集承載所有業務。

     AgileEAS.NET SOA平臺Lua引擎提供了以下介面或型別的API:

     ILuaEngine,定義為一個Lua指令碼引擎:

   1: using System;
   2:  
   3: namespace EAS.LuaScript
   4: {
   5:     /// <summary>  
   6:     /// Lua指令碼解析引擎。  
   7:     /// </summary>  
   8:     /// <remarks>
   9:     /// 用於完成程式之中嵌入的動態Lua指令碼,Lua指令碼在AgileEAS.NET SOA 中介軟體之中主要用於環境的各種引數的動態解析處理之中。
  10:     /// </remarks>
  11:     public interface ILuaEngine:IDisposable
  12:     {
  13:         /// <summary>
  14:         /// 指令碼路徑。
  15:         /// </summary>
  16:         string ScriptDirectory
  17:         {
  18:             get;
  19:             set;
  20:         }
  21:  
  22:         /// <summary>
  23:         /// 輸出重定向方法/用於除錯。
  24:         /// </summary>
  25:         Action<object> OutAction
  26:         {
  27:             get;
  28:             set;
  29:         }
  30:  
  31:         /// <summary>  
  32:         /// 註冊lua函式,實現Lua繫結。
  33:         /// </summary>  
  34:         /// <param name="luaAPIClass">lua函式類</param>  
  35:         void BindLuaFunctions(object luaAPIClass);
  36:  
  37:         /// <summary>  
  38:         /// 執行lua指令碼檔案。 
  39:         /// </summary>  
  40:         /// <param name="luaFileName">指令碼檔名。</param>  
  41:         /// <returns>lua指令碼執行結果。</returns>
  42:         object[] DoFile(string luaFileName);
  43:  
  44:         /// <summary>  
  45:         /// 執行lua指令碼文字。
  46:         /// </summary>  
  47:         /// <param name="chunk">lua指令。</param>  
  48:         /// <returns>lua指令碼執行結果。</returns>
  49:         object[] DoString(string chunk);
  50:  
  51:         /// <summary>
  52:         /// 呼叫Lua函式。
  53:         /// </summary>
  54:         /// <param name="luaFunction">函式名稱。</param>
  55:         /// <param name="args">呼叫引數。</param>
  56:         /// <returns>lua指令碼執行結果。</returns>
  57:         object[] Invoke(string luaFunction, params object[] args);
  58:     }
  59: }

     其中指令碼路徑ScriptDirectory指示Lua指令碼檔案的儲存位置,在DoFile執行指令碼文過程之中如果傳入為相關路徑則可以自動在ScriptDirectory之中定址,ScriptDirectory預設為當前程式目錄。

     DoFile:執行指令碼檔案並且返回執行結果。

     DoString:執行指令碼程式碼並且返回執行結果。

     Invoke:執行指定的指令碼函式。

      LuaFunctionAttribute,Lua指令碼函式特性:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5:  
   6: namespace EAS.LuaScript
   7: {
   8:     /// <summary>  
   9:     /// Lua函式描述特性。
  10:     /// </summary>  
  11:     /// <remarks>
  12:     /// 用於標記Lua介面方法。
  13:     /// </remarks>
  14:     [AttributeUsage(AttributeTargets.Method,
  15:                     Inherited = false, AllowMultiple = false)]
  16:     [Serializable]
  17:     public class LuaFunctionAttribute : Attribute
  18:     {
  19:         /// <summary>
  20:         /// 初始化LuaFunctionAttribute物件例項。
  21:         /// </summary>
  22:         /// <param name="name">函式名稱。</param>
  23:         public LuaFunctionAttribute(string name)
  24:             : this(name, string.Empty)
  25:         {
  26:  
  27:         }
  28:  
  29:         /// <summary>
  30:         /// 初始化LuaFunctionAttribute物件例項。
  31:         /// </summary>
  32:         /// <param name="name">函式名稱。</param>
  33:         /// <param name="description">函式描述。</param>
  34:         public LuaFunctionAttribute(string name, string description)
  35:             : this(name, description,null)
  36:         {
  37:  
  38:         }
  39:  
  40:         /// <summary>
  41:         /// 初始化LuaFunctionAttribute物件例項。
  42:         /// </summary>
  43:         /// <param name="name">函式名稱。</param>
  44:         /// <param name="description">函式描述。</param>
  45:         /// <param name="funcParams">引數說明。</param>
  46:         public LuaFunctionAttribute(string name, string description, params string[] funcParams)
  47:         {
  48:             this.Name = name;
  49:             this.Description = description;
  50:             this.Params = funcParams;
  51:         }
  52:  
  53:         /// <summary>
  54:         /// 函式名稱。
  55:         /// </summary>
  56:         public string Name
  57:         {
  58:             get;
  59:             internal set;
  60:         }
  61:  
  62:         /// <summary>
  63:         /// 函式描述。
  64:         /// </summary>
  65:         public string Description
  66:         {
  67:             get;
  68:             internal set;
  69:         }
  70:  
  71:         /// <summary>
  72:         /// 引數說明。
  73:         /// </summary>
  74:         public string[] Params
  75:         {
  76:             get;
  77:             internal set;
  78:         }
  79:     }
  80: }

     用於C#向Lua暴露函式介面,實現C#方法與Lua指令碼函式的繫結,也可以實現為把C#的方法暴露給Lua指令碼呼叫,如以下程式碼:

   1: /// <summary>
   2: /// 執行指定的非查詢命令。
   3: /// </summary>
   4: /// <param name="commandText">要執行的命令語句。該語句必須是標準的資料庫語句。</param>
   5: /// <returns>返回命令影響的行數。</returns>
   6: [LuaFunction("ExecuteSql", "執行指定的非查詢命令,返回執行所影響的行數。", "要執行的SQL語句")]
   7: public int Execute(string commandText)
   8: {
   9:     IDataAccessor da = this.GetComponent();
  10:     if (da != null)
  11:     {
  12:         return da.Execute(commandText);
  13:     }
  14:     else
  15:     {
  16:         return -1;
  17:     }
  18: }

     其目標早實現C#方法Execute與Lua指令碼函式ExecuteSql之間的繫結,當在Lua教本之中呼叫ExecuteSql,即會執行Execute方法。

     LuaFramework,Lua框架,為Lua指令碼引用最重要的API:

   1: #region 程式集 EAS.LuaScript.dll, v4.0.30319
   2: // G:\Health.Work\AgileEMR4.0\Publish\EAS.LuaScript.dll
   3: #endregion
   4:  
   5: using System;
   6:  
   7: namespace EAS.LuaScript
   8: {
   9:     // 摘要:
  10:     //     Lua指令碼執行框架/上下文。
  11:     //
  12:     // 備註:
  13:     //     應用系統之中呼叫Lua指令碼的入口。
  14:     public sealed class LuaFramework
  15:     {
  16:         // 摘要:
  17:         //     LuaFramework物件的唯一例項。
  18:         public static LuaFramework Instance { get; }
  19:  
  20:         // 摘要:
  21:         //     註冊lua函式,實現Lua繫結。
  22:         //
  23:         // 引數:
  24:         //   luaAPIClass:
  25:         //     lua函式類
  26:         public static void BindLuaFunctions(object luaAPIClass);
  27:         //
  28:         // 摘要:
  29:         //     從應用程式上下文環境之中獲取指定名稱的物件例項。
  30:         //
  31:         // 引數:
  32:         //   componentKey:
  33:         //     元件名稱。
  34:         //
  35:         // 返回結果:
  36:         //     物件例項。
  37:         public object GetComponent(string componentKey);
  38:         //
  39:         // 摘要:
  40:         //     Lua指令碼引擎。
  41:         public static ILuaEngine GetLuaEngine();
  42:         //
  43:         // 摘要:
  44:         //     Lua指令碼引擎。
  45:         public static ILuaEngine GetLuaEngine(string scriptDirectory);
  46:         //
  47:         // 摘要:
  48:         //     取系統時間。
  49:         [LuaFunction("GetTime", "取系統時間")]
  50:         public DateTime GetTime();
  51:     }
  52: }

     其中BindLuaFunctions方法實現C#方法與Lua函式的繫結註冊,呼叫時傳入需要註冊方法所在的C#類物件例項即可。
     GetLuaEngine獲取一個ILuaEngine,為之後的執行指令碼作準備,呼叫GetLuaEngine可選傳入scriptDirectory引數,scriptDirectory為Lua指令碼儲存目錄。

     AgileEAS.NET SOA中介軟體已經為大家提供了少數的幾個預定義API繫結:

   1: helpcmd(cmdName) - Show help for a given command or package
   2: help() - List available commands.
   3: Include(luaFileName) - 預載入指定的指令碼檔案
   4: echo(message) - 顯示Lua除錯資訊,用於Lua的動態除錯。
   5: Echo(message) - 顯示Lua除錯資訊,用於Lua的動態除錯。
   6: WriteLog(message) - 寫日誌記錄,用於Lua指令碼的動態除錯。
   7: GetTime() - 取系統時間
   8: GetComponent(componentKey) - 從應用程式上下文環境之中獲取指定名稱的物件例項
   9: GetAccount() - 求當前賬戶資訊
  10: ExecuteSql(commandText) - 執行指定的非查詢命令,返回執行所影響的行數。
  11: QuerySql(commandText, resultType) - 執行給定的資料庫查詢命令,返回執行結果,返回結果由resultType引數決定。
  12: QuerySqlScalar(commandText) - 執行給定的資料庫查詢命令,僅返回第一行第一列結果
  13: QuerySqlMatrix(commandText) - 執行給定的資料庫查詢命令,返回第一個查詢結果Matrix
  14: QuerySqlDictionary(commandText) - 執行給定的資料庫查詢命令,返回第一行的Key-Value
  15: QuerySqlList(commandText) - 執行給定的資料庫查詢命令,返回第一行的Key-Value
  16: QuerySqlDataSet(commandText) - 執行給定的資料庫查詢命令,返回查詢結果集
  17: QuerySqlDataTable(commandText) - 執行給定的資料庫查詢命令,返回查詢結果集的第一個DataTable
  18: GetDataSet() - 求當前系統的帳套資訊

     大家也可以基於AgileEAS.NET SOA 平臺Lua引擎擴充套件自己的LuaAPI。

四、AgileEAS.NET SOA平臺Lua引擎應用案例

     在我12年的醫療行業開發之中,做過多年的電子病歷,開發、指導開發過多套電子病歷系統,在電子病歷系統之中,有以下幾個問題一直得不到好的解決方案:

     在電子病歷質控之中有一個自動質控評分,其由程式自動的預先為某個病案打出一個得分:

]3L0{AR35LX4WKWXUAU}(2S

     在沒有使用Lua教本之前呢,也是能實現這種自動評分,但是為了實現這樣的功能,系統設計之中就會要增加一些輔助的表和大量的輔助設計以幫助完成這樣的功能,因為畢竟不存在統一的評分規則,所以這個設計會極其的複雜,以致於很難把這樣的功能做好,或者說在成本和效益估量之中,得不償失。

     另外還有比較簡單的例子就是電子病歷之間的巨集替換問題,在書寫電子病歷的過程之中需要動態的插入如病人基本資訊、醫院名稱這樣的東西進入病歷,但是因為其資料來源並不是唯一確定的,如果不使用動態指令碼設計,那麼其程式這中就會存在大量的硬編碼,並且如果想要在後期實施階段增加巨集,那麼則必須要修改程式才能完成。

     下面我們就以自動評分案例向大家講解一下是如何實現功能的呢:

     首先,系統之中必須管理和維護用於自動評分的所有評分規則:

BF$CE1XPYCZES13DJ%Z4XP5

     這裡與其他很多開發者的做法不一樣的是,對於每個缺陷專案,我們都為其提供了一個評分規則的指令碼的設計,例如對於缺陷“*缺入院記錄”的評分指令碼如下:

   1: -----------------------------------------------------------------------------------------------
   2: -- 建立人   :  魏瓊東
   3: -- 建立時間    : 2013-09-05
   4: -- 效果備註    : *缺入院記錄評分指令碼。
   5: --               GetErrorNumber,求缺陷數量,由指令碼計算出本缺陷數量
   6: ----------------------------------------------------------------------------------------------
   7: ------------------------不怎麼華麗的分割線--------------------------------------
   8: function GetErrorNumber(patient,errorItem)
   9:  
  10:     --呼叫C#,求監控專案
  11:     local monitorItem = GetMonitorItem("B01")    
  12:     --監控表示式,無表示式返回0
  13:     local script = monitorItem.Expression
  14:     if script == nil then
  15:         return 0
  16:     elseif script == "" then
  17:         return 0
  18:     end    
  19:     
  20:     --載入監控指令碼
  21:     ExecuteScript(script)
  22:     --執行監控函式NeedWrite,如果需要寫則預標存在此缺陷
  23:     local need = NeedWrite(patient,monitorItem)
  24:     
  25:     if need == 1 then
  26:         return 1
  27:     else
  28:         return 0
  29:     end
  30:  
  31: end

這樣的設計的目標是將缺陷的評分動作都由Lua指令碼實現,而在應用程式之中並不存在針對缺陷制定不同的評分規則的情況,只有呼叫指令碼進行評分的程式碼:

   1: /// <summary>
   2: /// 求指定患者、指定缺陷的缺陷數。
   3: /// </summary>
   4: /// <param name="pRoot"></param>
   5: /// <param name="errorItem"></param>
   6: /// <returns></returns>
   7: public static int GetErrorNumber(ILuaEngine m_Lua, PatientRoot pRoot, ErrorDict errorItem)
   8: {
   9:     if (string.IsNullOrEmpty(errorItem.Expression))  //無評分指令碼
  10:     {
  11:         return 0;
  12:     }
  13:  
  14:     string m_file = System.IO.Path.Combine(LuaEx.ScriptDirectory, string.Format("emr\\error\\{0}.lua", errorItem.ErrorCode));
  15:     if (!System.IO.File.Exists(m_file))
  16:     {
  17:         return 0;
  18:     }
  19:  
  20:     try
  21:     {
  22:         m_Lua.DoFile(m_file);  //載入指令碼
  23:  
  24:         object[] vs = m_Lua.Invoke("GetErrorNumber", pRoot, errorItem);  //執行函式。
  25:         if (vs != null && vs.Length > 0)
  26:         {
  27:             return int.Parse(vs[0].ToString());
  28:         }
  29:         else
  30:         {
  31:             return 0;
  32:         }
  33:     }
  34:     catch
  35:     {
  36:         return 0;
  37:     }
  38: }

這樣的好處在於,主體程式變的極其的簡單一致,不需要各種判斷和各種規則,並且靈活自動,可以做到政策變化或者客戶需求變化導到的各種評分規則的修改,則會靈活自如。

     實踐證明,使用動態指令碼語言擴充套件現在系統的系統是一種低投入高產出的工作,對於應對這種高擴充套件性和高定製性的專案是一個非常好的選擇,這個可以解釋遊戲程式設計之中大量使用Lua語言的事實,針對NPC、劇情書寫相關的處理指令碼,讓遊戲的後期變更,調整變得簡單可行。

四、聯絡我們

     為完善、改進和推廣AgileEAS.NET而成立了敏捷軟體工程實驗室,是一家研究、推廣和發展新技術,並致力於提供具有自主智慧財產權的業務基礎平臺軟體,以及基於業務基礎平臺開發的管理軟體的專業軟體提供商。主要業務是為客戶提供軟體企業研發管理解決方案、企業管理軟體開發,以及相關的技術支援,管理及技術諮詢與培訓業務。

     AgileEAS.NET SOA中介軟體平臺自2004年秋呱呱落地一來,我就一直在逐步完善和改進,也被應用於保險、醫療、電子商務、房地產、鐵路、教育等多個應用,但一直都是以我個人在推廣,2010年因為我辭職休息,我就想到把AgileEAS.NET推向市場,讓更多的人使用。

     我的技術團隊成員都是合作多年的老朋友,因為這個平臺是免費的,所以也沒有什麼收入,都是由程式設計師的那種理想與信念堅持,在此我感謝一起奮鬥的朋友。

團隊網站:http://www.agilelab.cn

AgileEAS.NET網站:http://www.smarteas.net

官方部落格:http://eastjade.cnblogs.com

QQ:47920381,AgileEAS.NET

QQ群:113723486(AgileEAS SOA 平臺)/上限1000人

199463175(AgileEAS SOA 交流)/上限1000人

212867943(AgileEAS.NET研究)/上限500人

147168308(AgileEAS.NET應用)/上限500人

172060626(深度AgileEAS.NET平臺)/上限500人

116773358(AgileEAS.NET 平臺)/上限500人

125643764(AgileEAS.NET探討)/上限500人

193486983(AgileEAS.NET 平臺)/上限500人

郵件:james@agilelab.cn,mail.james@qq.com,

電話:18629261335。

相關文章