軟件架構的核心思想

目錄

  • 前言
  • 一、軟件架構的定義
  • 二、軟件架構與設計
  • 三、軟件架構的幾個方面
    • 系統
    • 結構
    • 環境
    • 利益相關者
  • 四、軟件架構的結構特徵
    • 運行時結構
    • 模塊結構
  • 五、軟件架構的質量屬性
    • 可修改性
    • 可測試性
    • 可擴展性
    • 性能
    • 可用性
    • 安全性
    • 可部署性
  • 六、其他常見概念
    • 內聚
    • 耦合
    • 企業架構
    • 系統架構

前言

welcome to chenqionghe’s blog,架構能力其實更像是一種內功,需要我們不斷地去學習,讓我們用一張正能量的圖片開啟美好的學習生活,let’s do it~

一、軟件架構的定義

架構是一個系統的基本組織,涵蓋所包含的組件,組件之間的關係、組件與環境的關係,以及指導架構設計和演進的原則等內容

二、軟件架構與設計

軟件架構和軟件設計中都有“設計”的意思,但與後者相比,前者具有更高的抽象性和更廣的範圍

  • 軟件架構關注如何對系統中的結構和交互進行較高級別的描述,它關注的是那些與系統骨架相關的決策問題,例如:功能、組織 、技術、業務和質量標準等。
  • 軟件設計關注構成系統的部件或組件,以及子系統是如何組織的,關注的問題通常更接近代碼或模塊本身,例如:
    • 將代碼分成哪些模塊?如何組織?
    • 給不同的功能分配哪些類(或模塊)?
    • 對於類“C”應該使用哪種設計模式?
    • 在運行時對象之間是如何交互的?傳遞什麼消息?如何實施交互?

三、軟件架構的幾個方面

系統

系統是以特定方式組織的組件集合,以實現特定的功能。
軟件系統是其軟件組件的集合。一個系統通常可以劃分為若干個子系統。

結構

結構是根據某個指導規則或原則來組成或組織在一起的一組元素的集合,可以是軟件或硬件系統。
軟件架構可以根據觀察者的上下文展示各個層次的結構

環境

軟件系統所在的上下文或環境對其軟件架構有直接的影響。
這樣的上下因素可以是技術、商業、專業、操作等

利益相關者

任何對某個系統及其成功與否感興趣或關心的個體或團體,都是利益相關者。
例如:架構師、開發團隊、客戶、項目經理和營銷團隊等

四、軟件架構的結構特徵

運行時結構

在運行時創建的對象及其之間的交互方式經常決定部署架構。
部署架構與可擴展性、性能、安全性和交互操作性等質量屬性密切相關

模塊結構

為了分解任務,如何拆分代碼並把代碼組織到模塊和包中,這與系統的可維護性和可修改性密切相關,因為

  • 在代碼組織過程中考慮到可擴展性的話,通常會將父類放在單獨定義好的具有恰當文檔和配置的包中,這樣就可以輕易地通過增加外部模塊進行擴展,而不需要處理太多的依賴關係
  • 對於那些依賴於外部或第三方開發者(庫、框架等)的代碼,通過會根據提供的安裝或部署步驟,從外部源手動或自動地獲取並補丁全部各種依賴。此類代碼還提供多種文檔(例如README、INSTALL)等,它們清楚地記錄了這些步驟

五、軟件架構的質量屬性

質量屬性是系統的可度量和可測試的特性,可用於評估系統在其指定環境中的非功能性需求方面的達成情況

可修改性

定義為對系統進行修改的容易程度,以及系統對理髮進行調整的靈活性。
這是討論的修改不光是代碼的修改、部署的修改,而是任何層次上的修改。
一般架構師對可修改性的興趣點如下:

  • 難點:對系統進行修改的難易程度
  • 成本:進行修改需要的時間和資源
  • 風險:任何與系統修改相關的風險

代碼可讀性越強,其可修改性就越強,代碼的可修改性與可讀性成正比

可測試性

指一個軟件系統支持通過測試來檢測故障的程度。
可測試性也可以認為是一個軟件系統向最終用戶和集成測試隱藏了多少bug
一個系統的可測試性越好,它能隱藏的bug就越少。

可擴展性

指系統能夠適應不斷增長的負載需求,但同時要保證可接受的處理性能,一般分為兩大類:

  • 橫向(水平)擴展性。意味着通過向其中添加更多多的計算節點。
  • 縱向(垂直)擴展性。涉及系統單個節點中資源的添加或移除

性能

指系統在給定的計算資源內完成的工作量,完成的工作量和計算資源的比例(work/unit)越高,性能越高。

計算資源的單位有以下幾種

  • 響應時間。一個函數或執行單元運行所需要的時間。
  • 延遲。某個系統被激活並提供響應所需的時間。
  • 吞吐量。系統處理信息的某種比率。

可用性

指系統處於完全可操作狀態的程度,以便在任何時候獲得調用請求時可以執行的能力

  • 可靠性
    系統的可用性和可靠性密切相關,系統越可靠,可用性就越高。
  • 故障恢復能力
    影響可用性的另一個因素是從故障中恢復的能力,包括了故障檢測、故障恢復、故障預防
  • 數據一致性
    CAP定理指出,系統的可用性與其數據一致性有密切聯繫。一致性和可用性一般膛會同時成立,因為可能通信失敗,系統可以在一致性或可用性之間進行選擇

安全性

避免被未經過身份驗證的訪問損害數據和邏輯,同時繼續向通過誰的其他系統和角色提供服務的一種能力。

可部署性

指軟件從開發環境到產品運行環境移交的難易程度。
有以下相關因素:

  • 模塊結構。
    將系統劃分為易於部署的一個個子單元,則部署會容易
  • 產品運行環境與開發環境。與
    開發環境結構非常相似的產品運行環境會使部署
  • 開發生態系統支持。
    為系統提供成熟的工具鏈支持,允許各種依賴關係自動建立和驗證等配置項內容,從而提高可部署性。
  • 標準配置。
    一個好的方式是開發者保持開發環境的配置結構和產品運行環境一致。
  • 標準化基礎設施。
    將部署保持在一個標準化的基礎設施上,提高可部署性
  • 容器使用
    隨着Docker容器技術的普及,可以規範軟件,減少啟動/停止的開銷,從而使部署更容易。

六、其他常見概念

內聚

一個模塊內相關聯程度的度量,描述的是模塊內的功能聯繫
若一個模塊之間各元素聯繫緊密,則內聚性就高(高內聚)
如果能做到將模塊做成一個功能類聚、獨立性強、內部緊密結合才是一個理想的類聚模塊,這對初學都來說,非常不容易,不光是個人技術能力的挑戰,更是對某個領域業務水平的挑戰。

耦合

各模塊間相互聯繫緊密程度的一種度量。
模塊之間聯繫少,耦合性就越低,模塊之間的相對獨立性就越強。

企業架構

企業架構是一個定義企業結構和行為的概念藍圖。它確定了企業結構、流程、人員和信息流動如何與其核心目標相一致,以便有效地實現當前和未來的目標

系統架構

系統架構是系統的基本組織形式,由其結構和行為視圖表示。該結構由兩部分確定:構成系統的組件和組件的行為。組件的行為是指組件之間的交互,以及組件與外部系統之間的交互

以上內容由chenqionghe整理,參考《軟件架構-Python語言實現》,light weight baby~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

聚甘新

ASP.NET Core 對Controller進行單元測試

單元測試對我們的代碼質量非常重要。很多同學都會對業務邏輯或者工具方法寫測試用例,但是往往忽略了對Controller層寫單元測試。我所在的公司沒見過一個對Controller寫過測試的。今天來演示下如果對Controller進行單元測試。以下內容默認您對單元測試有所了解,比如如何mock一個接口。在這裏多叨叨一句,面向接口的好處,除了能夠快速的替換實現類(其實大部分接口不會有多個實現),最大的好處就是可以進行mock,可以進行單元測試。

測試Action

下面的Action非常簡單,非常常見的一種代碼。根據用戶id去獲取用戶信息然後展示出來。下面看看如何對這個Action進行測試。

   public class UserController : Controller
    {
        private readonly IUserService _userService;
        public UserController(IUserService userService)
        {
            _userService = userService;
        }

        public IActionResult UserInfo(string userId)
        {
            if (string.IsNullOrEmpty(userId))
            {
                throw new ArgumentNullException(nameof(userId));
            }

            var user = _userService.Get(userId);
            return View(user);
        }
      
    }

測試代碼:

  [TestMethod()]
        public void UserInfoTest()
        {

            var userService = new Mock<IUserService>();
            userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User());

            var ctrl = new UserController(userService.Object);
            //對空參數進行assert
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo(null);
            });
            //對空參數進行assert
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo("");
            });

            var result = ctrl.UserInfo("1");
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(ViewResult));
        }

我們對一個Action進行測試主要的思路就是模擬各種入參,使測試代碼能夠到達所有的分支,並且Assert輸出是否為空,是否為指定的類型等。

對ViewModel進行測試

我們編寫Action的時候還會涉及ViewModel給視圖傳遞數據,這部分也需要進行測試。修改測試用例,加入對ViewModel的測試代碼:

  [TestMethod()]
        public void UserInfoTest()
        {
            var userService = new Mock<IUserService>();
            userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User()
            {
                Id = "x"
            }) ;

            var ctrl = new UserController(userService.Object);
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo(null);
            });
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo("");
            });

            var result = ctrl.UserInfo("1");
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(ViewResult));
            //對viewModel進行assert
            var vr = result as ViewResult;
            Assert.IsNotNull(vr.Model);
            Assert.IsInstanceOfType(vr.Model, typeof(User));
            var user = vr.Model as User;
            Assert.AreEqual("x", user.Id);
        }

對ViewData進行測試

我們編寫Action的時候還會涉及ViewData給視圖傳遞數據,這部分同樣需要測試。修改Action代碼,對ViewData進行賦值:

   public IActionResult UserInfo(string userId)
        {
            if (string.IsNullOrEmpty(userId))
            {
                throw new ArgumentNullException(nameof(userId));
            }

            var user = _userService.Get(userId);

            ViewData["title"] = "user_info";

            return View(user);
        }
      

修改測試用例,加入對ViewData的測試代碼:

   [TestMethod()]
        public void UserInfoTest()
        {
            var userService = new Mock<IUserService>();
            userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User()
            {
                Id = "x"
            }) ;

            var ctrl = new UserController(userService.Object);
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo(null);
            });
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo("");
            });

            var result = ctrl.UserInfo("1");
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(ViewResult));

            var vr = result as ViewResult;
            Assert.IsNotNull(vr.Model);
            Assert.IsInstanceOfType(vr.Model, typeof(User));
            var user = vr.Model as User;
            Assert.AreEqual("x", user.Id);
            //對viewData進行assert
            Assert.IsTrue(vr.ViewData.ContainsKey("title"));
            var title = vr.ViewData["title"];
            Assert.AreEqual("user_info", title);
        }

對ViewBag進行測試

因為ViewBag事實上是ViewData的dynamic類型的包裝,所以Action代碼不用改,可以直接對ViewBag進行測試:

     [TestMethod()]
        public void UserInfoTest()
        {
            var userService = new Mock<IUserService>();
            userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User()
            {
                Id = "x"
            }) ;

            var ctrl = new UserController(userService.Object);
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo(null);
            });
            Assert.ThrowsException<ArgumentNullException>(() => {
                var result = ctrl.UserInfo("");
            });

            var result = ctrl.UserInfo("1");
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(ViewResult));

            var vr = result as ViewResult;
            Assert.IsNotNull(vr.Model);
            Assert.IsInstanceOfType(vr.Model, typeof(User));
            var user = vr.Model as User;
            Assert.AreEqual("x", user.Id);

            Assert.IsTrue(vr.ViewData.ContainsKey("title"));
            var title = vr.ViewData["title"];
            Assert.AreEqual("user_info", title);
            //對viewBag進行assert
            string title1 = ctrl.ViewBag.title;
            Assert.AreEqual("user_info", title1);
        }

設置HttpContext

我們編寫Action的時候很多時候需要調用基類里的HttpContext,比如獲取Request對象,獲取Path,獲取Headers等等,所以有的時候需要自己實例化HttpContext以進行測試。

    var ctrl = new AccountController();
    ctrl.ControllerContext = new ControllerContext();
    ctrl.ControllerContext.HttpContext = new DefaultHttpContext();

對HttpContext.SignInAsync進行mock

我們使用ASP.NET Core框架進行登錄認證的時候,往往使用HttpContext.SignInAsync進行認證授權,所以單元測試的時候也需要進行mock。下面是一個典型的登錄Action,對密碼進行認證后調用SignInAsync在客戶端生成登錄憑證,否則跳到登錄失敗頁面。

   public async Task<IActionResult> Login(string password)
        {
            if (password == "123")
            {
                var claims = new List<Claim>
                {
                  new Claim("UserName","x")
                };
                var authProperties = new AuthenticationProperties
                {
                };
                var claimsIdentity = new ClaimsIdentity(
                  claims, CookieAuthenticationDefaults.AuthenticationScheme);
                await HttpContext.SignInAsync(
                    CookieAuthenticationDefaults.AuthenticationScheme,
                    new ClaimsPrincipal(claimsIdentity),
                    authProperties);
                return Redirect("login_success");
            }

            return Redirect("login_fail");
        }

HttpContext.SignInAsync其實個時擴展方法,SignInAsync其實最終是調用了IAuthenticationService里的SignInAsync方法。所以我們需要mock的就是IAuthenticationService接口,否者代碼走到HttpContext.SignInAsync會提示找不到IAuthenticationService的service。而IAuthenticationService本身是通過IServiceProvider注入到程序里的,所以同時需要mock接口IServiceProvider。

    [TestMethod()]
        public async Task LoginTest()
        {
            var ctrl = new AccountController();

            var authenticationService = new Mock<IAuthenticationService>();
            var sp = new Mock<IServiceProvider>();
            sp.Setup(s => s.GetService(typeof(IAuthenticationService)))
                .Returns(() => {
                    return authenticationService.Object;
                });
            ctrl.ControllerContext = new ControllerContext();
            ctrl.ControllerContext.HttpContext = new DefaultHttpContext();
            ctrl.ControllerContext.HttpContext.RequestServices = sp.Object;

           var result = await ctrl.Login("123");
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(RedirectResult));
            var rr = result as RedirectResult;
            Assert.AreEqual("login_success", rr.Url);

            result = await ctrl.Login("1");
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(RedirectResult));
            rr = result as RedirectResult;
            Assert.AreEqual("login_fail", rr.Url);
        }

對HttpContext.AuthenticateAsync進行mock

HttpContext.AuthenticateAsync同樣比較常用。這個擴展方法同樣是在IAuthenticationService里,所以測試代碼跟上面的SignInAsync類似,只是需要對AuthenticateAsync繼續mock返回值success or fail。

     public async Task<IActionResult> Login()
        {
            if ((await HttpContext.AuthenticateAsync()).Succeeded)
            {
                return Redirect("/home");
            }

            return Redirect("/login");
        }

測試用例:


        [TestMethod()]
        public async Task LoginTest1()
        {
            var authenticationService = new Mock<IAuthenticationService>();
            //設置AuthenticateAsync為success
            authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
                .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new System.Security.Claims.ClaimsPrincipal(), "")));
            var sp = new Mock<IServiceProvider>();
            sp.Setup(s => s.GetService(typeof(IAuthenticationService)))
                .Returns(() => {
                    return authenticationService.Object;
                });

            var ctrl = new AccountController();
            ctrl.ControllerContext = new ControllerContext();
            ctrl.ControllerContext.HttpContext = new DefaultHttpContext();
            ctrl.ControllerContext.HttpContext.RequestServices = sp.Object;

            var act = await ctrl.Login();
            Assert.IsNotNull(act);
            Assert.IsInstanceOfType(act, typeof(RedirectResult));
            var rd = act as RedirectResult;
            Assert.AreEqual("/home", rd.Url);
            //設置AuthenticateAsync為fail
            authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
               .ReturnsAsync(AuthenticateResult.Fail(""));

            act = await ctrl.Login();
            Assert.IsNotNull(act);
            Assert.IsInstanceOfType(act, typeof(RedirectResult));
            rd = act as RedirectResult;
            Assert.AreEqual("/login", rd.Url);

        }

Filter進行測試

我們寫Controller的時候往往需要配合很多Filter使用,所以Filter的測試也很重要。下面演示下如何對Fitler進行測試。

    public class MyFilter: ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            if (context.HttpContext.Request.Path.Value.Contains("/abc/"))
            {
                context.Result = new ContentResult() {
                    Content = "拒絕訪問"
                };
            }

            base.OnActionExecuting(context);
        }
    }

對Filter的測試最主要的是模擬ActionExecutingContext參數,以及其中的HttpContext等,然後對預期進行Assert。

       [TestMethod()]
        public void OnActionExecutingTest()
        {
            var filter = new MyFilter();
            var actContext = new ActionContext(new DefaultHttpContext(),new RouteData(), new ActionDescriptor());
            actContext.HttpContext.Request.Path = "/abc/123";
            var listFilters = new List<IFilterMetadata>();
            var argDict = new Dictionary<string, object>();
            var actExContext = new ActionExecutingContext(
                actContext ,
                listFilters ,
                argDict ,
                new AccountController()
                );
             filter.OnActionExecuting(actExContext);

            Assert.IsNotNull(actExContext.Result);
            Assert.IsInstanceOfType(actExContext.Result, typeof(ContentResult));
            var cr = actExContext.Result as ContentResult;
            Assert.AreEqual("拒絕訪問", cr.Content);

            actContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
            actContext.HttpContext.Request.Path = "/1/123";
            listFilters = new List<IFilterMetadata>();
            argDict = new Dictionary<string, object>();
            actExContext = new ActionExecutingContext(
                actContext,
                listFilters,
                argDict,
                new AccountController()
                );
            filter.OnActionExecuting(actExContext);
            Assert.IsNull(actExContext.Result);
        }

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

聚甘新

EOS基礎全家桶(十三)智能合約基礎

簡介

智能合約是現在區塊鏈的一大特色,而不同的鏈使用的智能合約的虛擬機各不相同,編碼語言也有很大差異。而今天我們開始學習EOS的智能合約,我也是從EOS初期一直開發合約至今,期間踩過無數坑,也在Stack Overflow上提過問(最後自己解決了),在實際生產中也積累了很多經驗,所以我會連續幾周分多次分享合約開發的經驗,今天先來點基礎的。

一些C++的編程基礎

EOS就是使用C++開發的,這也為它帶來了諸多好處,而合約也沿用C++作為開發語言,雖然合約中無法直接使用Boost等框架(你可以自己引入,但這也意味着合約會很大,會佔用大量賬號的內存),但是我們還是可以使用很多C++的小型庫,並伴隨着eosio.cdt的發展,融入了更多實用的合約功能。

如果你之前沒有使用C系列的開發語言做過開發,比如:C語言、C++或者是C#,那麼你需要先學習下C語言的基本語法和數據結構,這裏我不做展開,在我們的系列文章的開篇就介紹了我推薦的Learn EOS – c/c++ 教程英文版,有一定英語基礎的朋友可以直接看這個,其他朋友也可以在網上找一些C++的入門教程看下。

如果你已經有了一定的C語言基礎,那麼寫合約的話,你會發現需要的基礎也並不多,依葫蘆畫瓢就能寫出各種基礎功能了,所以,你並不需要擔心太多語言上的門檻,畢竟合約只是一個特定環境下運行的程序,你能用到的東西並不會很多。

CDT選擇

EOS的早期版本進行合約開發還沒有CDT工具,那時的合約藉助的是源碼中的工具eosiocpp,所以你看2018年的博客,進行合約編譯都是用它,但你現在是見不到了。隨着官方CDT的迭代,在CDT的1.4版本開始被官方推薦使用,CDT後面也經歷了幾個大的版本更新,逐步改善合約編寫方式,更加趨於簡潔、直觀。

但是不同的CDT版本,也意味着編譯器的不同,所以合約開發也會有所區別,比如一些語法變了,一些庫名稱變了,增加了一些新的標註……

我們的教程側重還是介紹最新的語法,所以推薦使用1.6以上的版本。我也會盡量在後面的介紹中補充說明老的CDT的寫法,方便大家對照網上其他老博客的合約。

來個HelloWorld

學習任何編程,我們都不能少了Mr.HelloWorld,先來給大家打個招呼吧。

#include <eosio/eosio.hpp>

using namespace eosio;

class [[eosio::contract]] hello : public contract
{
public:
    using contract::contract;

    [[eosio::action]] void hi(name user)
    {
        print("Hello, ", user);
    }
};

  

基本合約結構及類型

hello合約就是一個最簡單的合約了,而且還有一個可調用的action為hi。我們首先還是來介紹下一個合約的程序結構吧。

  • 程序頭

包含了引入的頭文件、庫文件等,還有全局的命名空間的引入等。

#include <eosio/eosio.hpp>

using namespace eosio;

  

這裏eosio庫是我們的合約基礎庫,所有和eos相關的類型和方法,都在這個庫裏面,而這個庫裏面eosio.hpp是基礎,包含了contract等的定義,所以所有的合約都要引入。

【CDT老版本】早期cdt版本中庫名稱不是eosio,而是eosiolib

默認的,我們引入了eosio的命名空間,因為eosio的所有內容都是在這個命名空間下的,所以我們全局引入,會方便我們後續的代碼編寫。

  • 合約類定義

其實就是定義了一個class,繼承contract,並通過[[eosio::contract]]標註這個類是一個合約。使用using引入contract也是為了後續代碼可以更簡潔。

class [[eosio::contract]] hello : public contract{
public:
    using contract::contract;
}

  

【CDT老版本】早期cdt版本中直接使用了CONTRACT來定義合約類,比如:CONTRACT hello: public contract {}

  • action定義

寫一個public的方法,參數盡量用簡單或者是eosio內置的類型定義,無返回值(合約調用無法返回任何結果,除非報錯),然後在用[[eosio::action]]標註這個方法是一個合約action就行。

注意:action的名稱要求符合name類型的規則,name規則請看下面的常用類型中的說明。

[[eosio::action]]
void hi( name user ) {
    print( "Hello, ", user);
}

  

因為合約無法調試,所以只能通過print來打印信息,或者直接通過斷言拋出異常來進行調試。

【CDT老版本】早期cdt版本中直接使用ACTION來定義方法,比如:ACTION hi( name user ){}

  • 常用類型
類型 說明 示例
name 名稱類型,賬號名、表名、action名都是該類型,只能使用26個小寫字母和1到5的数字,特殊可以使用小數點,總長不超過13。 name("hi") 或者 "hi"_n
asset 資產類型,Token都是使用該類型,包含了Token符號和小數位,是一個複合類型,字符形式為1.0000 EOS asset(10000, symbol("TADO", 4)就是1.0000 TADO)
uint64_t 無符號64位整型,主要數據類型,表主鍵、name實質都是改類型 uint64_t amount = 10000000;
  • 內置常用對象或方法

在合約中,contract基類提供了一些方便的內置對象。

首先是get_self()或者是_self,這個方法可以獲取到當前合約所在的賬號,比如你把hello合約部署到了helloworld111這個賬號,那麼get_self()就可以獲取到helloworld111。

然後是get_code()或者是_code,這個方法可以獲取到當前交易請求的action方法名,這個在進行內聯action調用時可以用於判斷入口action。

最後是get_datastream()或者_ds,這個方法獲取的是數據流,如果你使用的是複雜類型,或者是自定義類型,那麼你無法在方法的參數上直接獲取到反序列化的變量值,你必須自己通過數據流來解析。

常用的還有獲取當前時間current_time_point(),這個需要引入#include <eosio/transaction.hpp>

數據持久化

當然,合約裏面,我們總會有些功能需要把數據存下來,在鏈上持久化存儲。所以我們就需要定義合約表了。

合約的表存在相應的合約賬號中,可以劃分表範圍(scope),每個表都有一個主鍵,uint64_t類型的,還可以有多個其他索引,表的查詢都是基於索引的。

這裏先提一句,表數據所佔用的內存,默認是合約賬號的內存,也可以使用其他賬號的,但需要權限,這個以後我們再介紹。

我們擴展一下hello合約。

#include <eosio/eosio.hpp>
#include <eosio/transaction.hpp>

using namespace eosio;

class [[eosio::contract]] hello : public contract
{
public:
    using contract::contract;

    hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
    {
    }

    [[eosio::action]] void hi(name user)
    {
        print("Hello, ", user);

        uint32_t now = current_time_point().sec_since_epoch();

        auto friend_itr = friend_table.find(user.value);
        if (friend_itr == friend_table.end())
        {
            friend_table.emplace(get_self(), [&](auto &f) {
                f.friend_name = user;
                f.visit_time = now;
            });
        }
        else
        {
            friend_table.modify(friend_itr, get_self(), [&](auto &f) {
                f.visit_time = now;
            });
        }
    }

    [[eosio::action]] void nevermeet(name user)
    {
        print("Never see you again, ", user);

        auto friend_itr = friend_table.find(user.value);
        check(friend_itr != friend_table.end(), "I don't know who you are.");

        friend_table.erase(friend_itr);
    }

private:
    struct [[eosio::table]] my_friend
    {
        name friend_name;
        uint64_t visit_time;

        uint64_t primary_key() const { return friend_name.value; }
    };

    typedef eosio::multi_index<"friends"_n, my_friend> friends;

    friends friend_table;
};

  

可以看到,我們已經擴充了不少東西了,包括構造函數,表定義,多索引表配置,並完善了原先的hi方法,增加了nevermeet方法。

我們現在模擬的是這樣一個使用場景,我們遇到一個朋友的時候,就會和他打招呼(調用hi),如果這個朋友是一個新朋友,就會插入一條記錄到我們的朋友表中,如果是一個老朋友了,我們就會更新這個朋友的記錄中的訪問時間。當我們決定不再見這個朋友了,就是絕交了(調用nevermeet),我們就會把這個朋友的記錄刪除。

  • 表定義

首先我們需要聲明我們的朋友表。定義一個結構體,然後用[[eosio::table]]標註這個結構體是一個合約表。在結構體里定義一個函數名primary_key,返回uint64_t類型,作為主鍵的定義。

private:
    struct [[eosio::table]] my_friend
    {
        name friend_name;
        uint64_t visit_time;

        uint64_t primary_key() const { return friend_name.value; }
    };

  

我們這裏聲明了一個my_friend的表,合約的表名不在這裏定義,所以結構體的名稱不必滿足name的規則。我們定義了兩個字段,friend_name(朋友的名稱)和visit_time(拜訪時間),主鍵我們直接使用了friend_name,這個字段是name類型的,而name類型的實質就是一個uint64_t的類型(所以name的規則那麼苛刻)。

【CDT老版本】早期cdt版本中直接使用TABLE來定義合約表,比如:TABLE my_friend{}

  • 多索引表配置

合約里的表都是通過多索引來定義的,這是合約表的結構基礎。所以這裏才是定義表名和查詢索引的地方。

typedef eosio::multi_index<"friends"_n, my_friend> friends;

  

我們現在只介紹最簡單的單索引的定義,以後再介紹多索引的定義方式,這裏的"friends"_n就是定義表名,所以使用了name類型,之後my_friend是表的結構類型,typedef實質上就是聲明了一個類型別名,名字是friends的類型。

  • 構造函數

構造函數這裏並不是必須,但是為了我們能在全局直接使用合約表,所以我們要在構造函數進行表對象的實例化。

public:
    hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
    {
    }

private:
    friends friend_table;

  

這一段是標準合約構造函數,hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds),合約類型實例化時會傳入receiver也就是我們的合約賬號(一般情況下),code就是我們的action名稱,ds就是數據流。

friend_table(get_self(), get_self().value)這一段就是對我們定義的friend_table變量的實例化,friend_table變量就是我們定義的多索引表的friends類型的實例。在合約里我們就可以直接使用friend_table變量來進行表操作了。實例化時傳遞的兩個參數正是表所在合約的名稱和表範圍(scope),這裏都使用的是當前合約的名稱。

  • 查詢記錄

查詢有多種方式,也就是多索引表提供了多種查詢的方式,默認的,使用findget方法是直接使用主鍵進行查詢,下次我們會介紹使用第二、第三等索引來進行查詢。find返回的是指針,數據是否存在,需要通過判斷指針是否是指到了表末尾,如果等於表末尾,就說明數據不存在,否則,指針的值就是數據對象。get直接返回的就是數據對象,所以在調用get時,就必須傳遞數據不存在時的錯誤信息。

auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
    //數據不存在
}else
{
    //數據存在
}

  

我們在hi方法中先查詢了user是否存在。如果不存在,我們就添加數據,如果存在了,就修改數據中的visit_time字段的值為當前時間。

  • 添加記錄

多索引的表對象添加記錄使用emplace方法,第一個參數就是內存使用的對象,第二個參數就是添加表對象時的委託方法。

uint32_t now = current_time_point().sec_since_epoch();

auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
    friend_table.emplace(get_self(), [&](auto &f) {
        f.friend_name = user;
        f.visit_time = now;
    });
}
else
{
    //數據存在
}

  

這裏先定義了一個變量now來表示當前時間,正是使用的內置方法current_time_point(),這個還是用了它的sec_since_epoch()方法,是為了直接獲取秒單位的值。

我們查詢后發現這個user的數據不存在,所以就進行插入操作,內存直接使用的合約賬號的,所以使用get_self(),然後對錶數據對象進行賦值。

  • 修改記錄

多索引的表對象修改記錄使用modify方法,第一個參數是傳遞需要修改的數據指針,第二個參數是內存使用的對象,第二個參數就是表對象修改時的委託方法。

friend_table.modify(friend_itr, get_self(), [&](auto &f) {
    f.visit_time = now;
});

  

我們將查詢到的用戶對象的指針friend_itr傳入,然後內存還是使用合約賬號的,委託中,我們只修改visit_time的值(主鍵是不能修改的)。

  • 刪除記錄
  • 多索引的表對象刪除記錄使用erase方法,只有一個參數,就是要刪除的對象指針,有返回值,是刪除數據后的指針偏移,也就是下一條數據的指針。
auto friend_itr = friend_table.find(user.value);
check(friend_itr != friend_table.end(), "I don't know who you are.");

friend_table.erase(friend_itr);

  

我們的示例中,將查詢到的這條數據直接刪除,併為使用變量來接收下一條數據的指針,在連續刪除數據時,你會需要獲取下一條數據的指針,因為已刪除的數據的指針已經失效了。

編譯

編譯我們再之前也有過介紹,安裝了eosio.cdt后,我們就有了eosio-cpp命令,進入到合約文件夾中,直接執行以下命令就會在當前目錄生成wasm和abi文件。

eosio-cpp -abigen hello.cpp -o hello.wasm

注意:替換命令中使用的hello.cpp為實際合約代碼文件名,而hello.wasm為實際合約的wasm文件名。

當然,編譯不通過的時候,你就要看看錯誤是什麼了,這可能會考驗一下你的C++功底。

發布

決定了要發布的賬號后,記得要購買足夠的內存和抵押足夠的資源。合約的內存消耗我們可以大致這樣估算,看下編譯好了的合約wasm文件有多大,然後乘以10,就是你發布到鏈上大概所需的內存大小了。

發布合約我們使用cleos set contract命令,其後跟合約賬號名和合約目錄,為了方便,我建議你把合約的目錄名保持和合約文件名一致。

cleos set contract helloworld111 ./hello -p helloworld111

這裏我們給出的代碼是將hello目錄下的hello合約發布到helloworld111。我這裏的文件夾是hello,裏面的abi和wasm也都是hello,這樣你不用手動指定合約文件了。

總結

至此,我想大家應該對合約的編寫有了一個大致的了解了,至少你可以參照着寫個簡單的合約出來了,這其中還有很多技巧和高級用法,我會在後續的文章中繼續和大家分享。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

聚甘新

Linux nohup命令詳解,終端關閉程序依然可以在執行!

大家好,我是良許。

在工作中,我們很經常跑一個很重要的程序,有時候這個程序需要跑好幾個小時,甚至需要幾天,這個時候如果我們退出終端,或者網絡不好連接中斷,那麼程序就會被中止。而這個情況肯定不是我們想看到的,我們希望即使終端關閉,程序依然可以在跑。

這時我們就可以使用 nohup 這個命令。

nohup 命令是英語詞組 no hangup 的縮寫,意思是不掛斷,也就是指程序不退出。這個命令會使程序忽略 HUP 信號,保證程序能夠正常進行。HUP 信號有些人可能比較陌生,它是在終端被中止的時候向它所關聯的進程所發出的信號,進程收到這個信號后就會中止運行。所以如果你不希望進程被這個信號幹掉的話,就可以忽略這個信號。而 nohup 命令做的就是這個事情。

本文我們將詳細介紹 nohup 命令的具體用法。

nohup命令基本語法

nohup 命令的基本語法如下:

$ nohup command arguments

或者:

$ nohup options

如果你想要得到更多關於 nohup 的用法介紹,可以查看它的幫助頁面:

$ nohup --help

如果你需要查看它的版本號,可以使用 --version 選項。

$ nohup --version

使用nohup命令啟動一個程序

如果你需要運行一個程序,即使對應的 Shell 被退出后依然保持運行,可以這樣使用 nohup 運行這個程序:

$ nohup command

當這個程序進行起來之後,這個程序對應的 log 輸出及其錯誤日誌都將被記錄在 nohup.out 文件里,這個文件一般位於家目錄或者當前目錄。

重定向程序的輸出

如果我不想把程序的輸出保存在家目錄或者當前目錄,我想保存在我指定的路徑,並且自定義文件名,要怎麼操作?這時我們就可以使用重定向操作 >

比如,我現在有個腳本 myScript.sh 我想把它的輸出保存在家目錄下的 output 目錄下,文件名為 myOutput.txt ,可以這樣運行:

$ nohup ./myScript.sh > ~/output/myOutput.txt

使用nohup命令後台啟動一個程序

如果想讓程序在後台運行,可以加上 & 符號。但這樣運行之後,程序就無影無蹤了。想要讓程序重新回到終端,可以使用 fg 命令。

這個命令的輸出 log 將保存在 nohup.out 文件里,你可以使用 cat 或其它命令查看。第二行里 8699 這個数字代表這個命令對應的進程號,也就是 pid 。我們可以使用 ps 命令來找到這個進程。

使用nohup同時運行多個程序

如果你需要同時跑多個程序,沒必要一個個運行,直接使用 && 符號即可。比如,你想同時跑 mkdir ,ping,ls 三個命令,可以這樣運行:

$ nohup bash -c 'mkdir files &&
ping -c 1 baidu.com && ls'> output.txt

終止跑在後台的進程

上面有提到,nohup 命令結合 & 符號可以使進程在後台運行,即使關閉了終端依然不受影響。這時,如果想要終止這個進程,要怎麼操作呢?

最簡單的當屬 kill 命令,相信大家用過很多次了。

$ kill -9 PID

那要如何找到進程對應的 pid 呢?我們可以使用 ps 命令。

$ ps aux | grep myScript.sh

或者你使用 pgrep 命令也行。

接下來,再使用 kill 命令就可以終止該進程了。

$ kill -9 14942

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

聚甘新

SpringBoot + Mybatis + Redis 整合入門項目

這篇文章我決定一改以往的風格,以幽默風趣的故事博文來介紹如何整合 SpringBoot、Mybatis、Redis。

很久很久以前,森林里有一隻可愛的小青蛙,他邁着沉重的步伐走向了找工作的道路,結果發現許多的招聘要求都要會 Redis。

小青蛙就想啥是 Redis 呢,為什麼要用 Redis 呢?難道是因為 Mysql 的幣格不夠高嗎,小青蛙點開了收藏已久的網站:十萬個為什麼

發現原來隨着使用網站的用戶越來越多,表中的數據也越來越多,查詢速度越來越慢。

MySql 的性能遇到了瓶頸,所以許多網站都用 Redis 作緩存。

 

然而能作緩存的不僅只有 Redis,還有 Memcache,那為什麼要用 Redis 呢?

1、性能方面:它們都是將數據存放在內存中,所以性能基本相似。

2、數據類型方面:Redis 支持五種數據數據類型:字符串、散列、列表、集合、有序集合,而 Memcache 僅僅支持簡單的 key-value。

3、數據持久化方面:Redis 可以通過 RDB快照、AOF日誌 等方式進行數據持久化,但是 Memcache 不可以。

4、數據備份方面:Redis  支持 master-slave 主從模式的數據備份。

 

在了解到許多 Redis 的好處后,小青蛙已經迫不及待的想了解它了。

為了更好的使用 Redis,了解 Redis 的五種數據類型適應場景是很有必要的。

1、String 類型:一個 key 對應一個 value,而 value 不僅僅是 String,也可以是数字、甚至是一個序列化對象。

2、Hash 類型:一個 key 對應 多個 field,一個 field 對應 yige value,實際上該類型最適合存儲序列化對象。

           key 相當於數據庫表名字,field  相當於主鍵,value 也就是序列化對象。

3、List 類型:簡單的字符串列表,按照插入順序排序,該結構類似於數據結構中的雙向鏈表,可以從頭部插也可以從尾部插。

4、Set 類型:它是字符串集合,只不過它是無序的且不存在重複的字符串,但是它可以實現 交集、並集、差集。

5、ZSet 類型:它也是字符串集合,它和 Set 的區別是該集合的元素存在 score 屬性,按照 score 屬性的高低排序。可以應用在排行榜上。

值得注意的是,不要習慣性的認為 Redis 字符串只能存字符串,實際上,它可以存儲任何序列化后的對象,當然也可以讀出來。

 

小青蛙知道了 Redis 的五種數據類型應用場景后,迫不及待的想要實踐它了。

為了知道如何讓它作為緩存,以及如何操作數據,小青蛙打開了珍藏已久的視頻網站來學習:青蛙愛學習

在該視頻網站上,小青蛙沉迷其中無法自拔,額,呸呸。緩過神來,發現了一個很好的視頻。小青蛙幽默的說:快進我的收藏夾吃灰去吧。

小青蛙向來都不是一個收藏從未停止,學習從未開始的青蛙。它模仿着視頻建了一個 SpringBoot 項目。

此時,小青蛙想為什麼要勾選 Lombok 呢,因為它可以簡化類,並且提供了 log 等功能。

之後小青蛙為了連上 Mysql 和 Redis 就開始配置 application.properties 文件:

# 數據源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=lemon@mango

# Redis 配置
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=100

配置好該文件后,需要 redisTemplate 模板 Bean,因為自動配置的 redisTemplate Bean 沒有提供序列化操作:(因為是入門版的,所以這樣最好理解)

package com.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
                template.setConnectionFactory(factory);
                GenericJackson2JsonRedisSerializer genericJsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
                template.setKeySerializer(new StringRedisSerializer());
                template.setValueSerializer(genericJsonRedisSerializer);
                template.setHashKeySerializer(genericJsonRedisSerializer);
                template.setHashValueSerializer(genericJsonRedisSerializer);
                template.afterPropertiesSet();
                return template;
    }
}

至此就整合完成了,小青蛙心想這就完事了?!!!,不信?那就來演示一下:(先建一個簡單的類測試一下)

package com.test.serviceImpl;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class JustForTest {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    public void test(String username) {
        
        if(redisTemplate.hasKey(username)) {
            log.info((String)redisTemplate.opsForValue().get(username));
            log.info("get value from redis");
        }else {
            String password = "password";
            log.info(password);
            log.info("get value from mysql");
            log.info("set value to redis");
            redisTemplate.opsForValue().set(username, password);
        }
        
    }

}
package com.test;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;
import com.test.serviceImpl.JustForTest;

@SpringBootTest
class TestApplicationTests {
    
    @Autowired
    private JustForTest justFortest;

    @Test
    void contextLoads() {
        justFortest.test("username");
    }
}

哦嚯,報錯了,原來是 pom.xml 中少了 commons.pool 依賴,咱給它加上:

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

再來一次:

再再來一次:

可以看到確實存入 Redis 了,小青蛙便去 Redis 數據庫中看看有沒有:

事實證明確實整合完畢了,小青蛙仍然表示不解,說好的是SpringBoot、Mybatis、Redis的整合呢,怎麼只看到 Redis 的?

小青蛙剛這麼想,然後視頻里就說了,心急吃不了熱豆腐,需要慢慢來。緊接着,小青蛙就看到了完整的項目結構:

為了讓青蛙們只關注有關整合的部分,視頻里僅僅只給出 serviceImpl 中的代碼:

package com.demo.serviceImpl;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import com.demo.pojo.SimpleUser;
import com.demo.dao.SimpleUserDao;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
@SuppressWarnings({"rawtypes","unchecked"})
public class SimpleUserServiceImpl implements UserDetailsService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SimpleUserDao userDao;
    @Override 
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        if(redisTemplate.opsForHash().hasKey("user",username)) {
            SimpleUser user = (SimpleUser)redisTemplate.opsForHash().get("user",username);
            return new User(user.getUsername(),user.getPassword(),user.getAuthorities());
        }else {
            SimpleUser user = userDao.findUserByUsername(username);
            if(user != null) {
                redisTemplate.opsForHash().put("user", "username", user);
                return new User(user.getUsername(),user.getPassword(),user.getAuthorities());
            }else {
                throw new UsernameNotFoundException("Username or Password is not correct");
            }
        }
    }
    
    public int addSimpleUser(SimpleUser user) {
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        return userDao.addSimpleUser(user);
    }
}

和上面測試的簡單小例子相似,僅僅是把 String 換成了對象。小青蛙對 Mysql 的表結構和Redis中存入的數據比較感心趣:

由於對 String 類型帶有 ” 符號,所以需要對其進行轉義。

小青蛙是一個有分享精神的蛙,每次它覺得有價值的東西,它都會分享給它的朋友們,項目地址為:GitHub

小青娃逐漸的弄懂了怎麼去進行整合,但是它還是不太明白為什麼這樣就能整合,也就是 know how but don’t konw why!

小青蛙知道:萬事開頭難,但它不知道的是,後面也很難…從此,小青蛙踏上了探尋源碼的道路!呱呱呱……

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

聚甘新

基於NACOS和JAVA反射機制動態更新JAVA靜態常量非@Value註解

1.前言

項目中都會使用常量類文件, 這些值如果需要變動需要重新提交代碼,或者基於@Value註解實現動態刷新, 如果常量太多也是很麻煩; 那麼 能不能有更加簡便的實現方式呢?

本文講述的方式是, 一個JAVA類對應NACOS中的一個配置文件,優先使用nacos中的配置,不配置則使用程序中的默認值;

2.正文

nacos的配置如下圖所示,為了滿足大多數情況,配置了 namespace命名空間和group;

 

 

 新建個測試工程 cloud-sm.

bootstrap.yml 中添加nacos相關配置;

為了支持多配置文件需要注意ext-config節點,group對應nacos的添加的配置文件的group; data-id 對應nacos上配置的data-id

配置如下:

server:
  port: 9010
  servlet:
    context-path: /sm
spring:
  application:
    name: cloud-sm
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.100.101:8848 #Nacos服務註冊中心地址
        namespace: 1
      config:
        server-addr: 192.168.100.101:8848 #Nacos作為配置中心地址
        namespace: 1
        ext-config:
          - group: TEST_GROUP
            data-id: cloud-sm.yaml
            refresh: true
          - group: TEST_GROUP
            data-id: cloud-sm-constant.properties
            refresh: true

接下來是本文重點:

1)新建註解ConfigModule,用於在配置類上;一個value屬性;

2)新建個監聽類,用於獲取最新配置,並更新常量值

實現流程:

1)項目初始化時獲取所有nacos的配置

2)遍歷這些配置文件,從nacos上獲取配置

3)遍歷nacos配置文件,獲取MODULE_NAME的值

4)尋找配置文件對應的常量類,從spring容器中尋找 常量類 有註解ConfigModule 且值是 MODULE_NAME對應的

5)使用JAVA反射更改常量類的值

6)增加監聽,用於動態刷新

 

import org.springframework.stereotype.Component;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Component
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ConfigModule {
    /**
     *  對應配置文件裏面key為( MODULE_NAME ) 的值
     * @return
     */
    String value();
}
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.utils.LogUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * nacos 自定義監聽
 *
 * @author zch
 */
@Component
public class NacosConfigListener {
    private Logger LOGGER = LogUtils.logger(NacosConfigListener.class);
    @Autowired
    private NacosConfigProperties configs;
    @Value("${spring.cloud.nacos.config.server-addr:}")
    private String serverAddr;
    @Value("${spring.cloud.nacos.config.namespace:}")
    private String namespace;
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 目前只考慮properties 文件
     */
    private String fileType = "properties";
    /**
     * 需要在配置文件中增加一條 MODULE_NAME 的配置,用於找到對應的 常量類
     */
    private String MODULE_NAME = "MODULE_NAME";

    /**
     * NACOS監聽方法
     *
     * @throws NacosException
     */
    public void listener() throws NacosException {
        if (StringUtils.isBlank(serverAddr)) {
            LOGGER.info("未找到 spring.cloud.nacos.config.server-addr");
            return;
        }
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr.split(":")[0]);
        if (StringUtils.isNotBlank(namespace)) {
            properties.put(PropertyKeyConst.NAMESPACE, namespace);
        }

        ConfigService configService = NacosFactory.createConfigService(properties);
        // 處理每個配置文件
        for (NacosConfigProperties.Config config : configs.getExtConfig()) {
            String dataId = config.getDataId();
            String group = config.getGroup();
            //目前只考慮properties 文件
            if (!dataId.endsWith(fileType)) continue;

            changeValue(configService.getConfig(dataId, group, 5000));

            configService.addListener(dataId, group, new Listener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    changeValue(configInfo);
                }

                @Override
                public Executor getExecutor() {
                    return null;
                }
            });
        }
    }

    /**
     * 改變 常量類的 值
     *
     * @param configInfo
     */
    private void changeValue(String configInfo) {
        if(StringUtils.isBlank(configInfo)) return;
        Properties proper = new Properties();
        try {
            proper.load(new StringReader(configInfo)); //把字符串轉為reader
        } catch (IOException e) {
            e.printStackTrace();
        }
        String moduleName = "";
        Enumeration enumeration = proper.propertyNames();
        //尋找MODULE_NAME的值
        while (enumeration.hasMoreElements()) {
            String strKey = (String) enumeration.nextElement();
            if (MODULE_NAME.equals(strKey)) {
                moduleName = proper.getProperty(strKey);
                break;
            }
        }
        if (StringUtils.isBlank(moduleName)) return;
        Class curClazz = null;
        // 尋找配置文件對應的常量類
        // 從spring容器中 尋找類的註解有ConfigModule 且值是 MODULE_NAME對應的
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            Class clazz = applicationContext.getBean(beanName).getClass();
            ConfigModule configModule = (ConfigModule) clazz.getAnnotation(ConfigModule.class);
            if (configModule != null && moduleName.equals(configModule.value())) {
                curClazz = clazz;
                break;
            }
        }
        if (curClazz == null) return;
        // 使用JAVA反射機制 更改常量
        enumeration = proper.propertyNames();
        while (enumeration.hasMoreElements()) {
            String key = (String) enumeration.nextElement();
            String value = proper.getProperty(key);
            if (MODULE_NAME.equals(key)) continue;
            try {
                Field field = curClazz.getDeclaredField(key);
                //忽略屬性的訪問權限
                field.setAccessible(true);
                Class<?> curFieldType = field.getType();
                //其他類型自行拓展
                if (curFieldType.equals(String.class)) {
                    field.set(null, value);
                } else if (curFieldType.equals(List.class)) { // 集合List元素
                    field.set(null, JSONUtils.parse(value));
                } else if (curFieldType.equals(Map.class)) { //Map
                    field.set(null, JSONUtils.parse(value));
                }
            } catch (NoSuchFieldException | IllegalAccessException e) {
                LOGGER.info("設置屬性失敗:{} {} = {} ", curClazz.toString(), key, value);
            }
        }
    }

    @PostConstruct
    public void init() throws NacosException {
        listener();
    }
}

 3.測試

1)新建常量類Constant,增加註解@ConfigModule(“sm”),盡量測試全面, 添加常量類型有 String, List,Map

@ConfigModule("sm")
public class Constant {

    public static volatile String TEST = new String("test");

    public static volatile List<String> TEST_LIST = new ArrayList<>();
    static {
        TEST_LIST.add("默認值");
    }
    public static volatile Map<String,Object> TEST_MAP = new HashMap<>();
    static {
        TEST_MAP.put("KEY","初始化默認值");
    }
    public static volatile List<Integer> TEST_LIST_INT = new ArrayList<>();
    static {
        TEST_LIST_INT.add(1);
    }
}

2)新建個Controller用於測試這些值

@RestController
public class TestController {

    @GetMapping("/t1")
    public Map<String, Object> test1() {
        Map<String, Object> result = new HashMap<>();

        result.put("string" , Constant.TEST);
        result.put("list" , Constant.TEST_LIST);
        result.put("map" , Constant.TEST_MAP);
        result.put("list_int" , Constant.TEST_LIST_INT);
        result.put("code" , 1);
        return result;
    }
}

3)當前nacos的配置文件cloud-sm-constant.properties為空

 4)訪問測試路徑localhost:9010/sm/t1,返回為默認值

{
    "code": 1,
    "string": "test",
    "list_int": [
        1
    ],
    "list": [
        "默認值"
    ],
    "map": {
        "KEY": "初始化默認值"
    }
}

5)然後更改nacos的配置文件cloud-sm-constant.properties;

 6)再次訪問測試路徑localhost:9010/sm/t1,返回為nacos中的值

{
    "code": 1,
    "string": "12351",
    "list_int": [
        1,
        23,
        4
    ],
    "list": [
        "123",
        "sss"
    ],
    "map": {
        "A": 12,
        "B": 432
    }
}

4.結語

這種實現方式優點如下:

1)動態刷新配置,不需要重啟即可改變程序中的靜態常量值

2)使用簡單,只需在常量類上添加一個註解

3)避免在程序中大量使用@Value,@RefreshScope註解

 不足:

此代碼是個人業餘時間的想法,未經過生產驗證,實現的數據類型暫時只寫幾個,其餘的需要自行拓展

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聚甘新

學習ASP.NET Core(11)-解決跨域問題與程序部署

上一篇我們介紹了系統日誌與測試相關的內容並添加了相關的功能;本章我們將介紹跨域與程序部署相關的內容

一、跨域

1、跨域的概念

1、什麼是跨域?

一個請求的URL由協議,域名,端口號組成,以百度的https://www.baidu.com為例,協議為https,域名由子域名www和主域名baidu組成,端口號若為80會自動隱藏(也可以配置為其它端口,通過代理服務器將80端口請求轉發給實際的端口號)。而當請求的URL的協議,域名,端口號任意一個於當前頁面的URL不同即為跨域

2、什麼是同源策略?

瀏覽器存在一個同源策略,即為了防範跨站腳本的攻擊,出現跨域請求時瀏覽器會限制自身不能執行其它網站的腳本(如JavaScript)。所以說當我們把項目部署到Web服務器后,通過瀏覽器進行請求時就會出現同源策略問題;而像PostMan軟件因其是客戶端形式的,所以不存在此類問題

3、跨域會導致什麼問題?

同源策略會限制以下行為:

  • Cookie、LocalStorage和IndexDb的讀取
  • DOM和JS對象的獲取
  • Ajax請求的發送

2、常用的解決方法

這裏我們將簡單介紹針對跨域問題常用的幾種解決辦法,並就其中的Cors方法進行配置,若對其它方式感興趣,可參照老張的哲學的文章,⅖ 種方法實現完美跨域

2.1、JsonP

1、原理

上面有提到瀏覽器基於其同源策略會限制部分行為,但對於Script標籤是沒有限制的,而JsonP就是基於這一點,它會在頁面種動態的插入Script標籤,其Src屬性對應的就是api接口的地址,前端會以Get方式將處理函數以回調的形式傳遞給後端,後端響應後會再以回調的方式傳遞給前端,最終頁面得以显示

2、優缺點

JsonP出現時間較早,所以對舊版本瀏覽器支持性較好;但自身只支持Get請求,無法確認請求是否成功

2.2、 CORS

1、原理

CORS的全稱是Corss Origin Resource Sharing,即跨域資源共享,它允許將當前域下的資源被其它域的腳本請求訪問。其實現原理就是在響應的head中添加Access-Control-Allow-Origin,只要有該字段就支持跨域請求

2、優缺點

Cors支持所有Http方法,不用考慮接口規則,使用簡單;但是對一些舊版本的瀏覽器支持性欠佳

3、使用

其使用非常簡單,以我們的項目為例,在BlogSystem.Core項目的Startup類的ConfigureServices方法中進行如下配置

同時需要開啟使用中間件,如下:

2.3、Nginx

1、原理

跨域問題是指在一個地址中發起另一個地址的請求,而Nginx可以利用其反向代理的功能,接受請求后直接請求該地址,類似打開了一個新的頁面,所以可以避開跨域的問題

2、優缺點

配置簡單,可以降低開發成本,方便配置負載均衡;靈活性差,每個環境都需要進行不同的配置

二、程序部署

1、部署模式

在.NET Core中,有兩種部署模式,分別為FDD(Framework-dependent)框架依賴發布模式和SCD(Self-contained)自包含獨立發布模式

  • FDD:此類部署需要服務器安裝.NET Core SDK環境,部署的包容量會比較小,但可能因SDK版本存在兼容性問題;
  • SCD:此類部署自包含.NET Core SDK的環境,不同.NET Core版本可以共存,其部署包容量會較大,且需要對服務器進行相關配置

2、常用部署方式

以下內容均參考老張的哲學的文章最全的部署方案 & 最豐富的錯誤分析,有興趣的朋友可以參考原文

2.1、Windows平台

  • 直接運行:發布目標選擇windows時會在文件夾中生成一個exe文件,我們可以直接執行exe文件或使用CLI命令調用dll運行;這種方式雖然方便,卻存在一些弊端,比如說部署多個的情況下會存在很多控制台窗口,如誤操作會導致窗口關閉等;
  • 部署服務:除了上述直接運行的方式外,我們還可以將程序發布為服務,發布后我們可以像控制系統服務一樣控製程序的啟動和關閉

需要注意的是上述兩類方法都需要藉助IIS或者是代理服務器進行服務的轉發,否則只能在本地進行訪問;

2.2、Linux平台

Linux平台常用的部署方式即為程序+代理服務器,但是當我們配置完成后運行程序時,該運行命令會一直佔用操作窗口,所以我們需要使用“守護進程”來解決這個問題,簡單來說就是將程序放到後台運行,不影響我們進行其他操作

綜上,部署模式、部署方式及部署平台有多種組合方式,接下來我們挑選下述3種方法進行演示:

方案 依賴運行時/宿主機 依賴代理服務器 其它配置
Windows程序(SCD)+Nginx
Windows服務(FDD)+IIS 設置為服務
Linux程序(FDD)+Nginx 守護進程

3、程序發布

1、這裏我們右擊BlogSystem.Core項目,選擇發布,選擇文件夾后,點擊高級

2、為了演示後面的發布實例,這裏我們分別選擇3種組合模式,①獨立+win-x64;②框架依賴+win-x64;③框架依賴+linux-x64

3、將發布實例拷貝到單獨的文件夾種,這裏我們使用SCD-Window驗證下程序能否直接運行,運行BlogSystem.Core.exe,報錯:

原來還是老問題,BLL沒有添加到發布文件中,我們到項目的bin文件夾下將BLL和DAL的dll文件分別拷貝至3個文件夾,再次運行,出現404錯誤,經過確認發現,首頁對應的是Swagger文檔頁面,而在配置中間件時我們有添加開發環境才配置swagger的邏輯,所以這裏我們可以根據個人需求決定是否添加。

這裏我為了方便確認發布是否成功,所以將其從判斷邏輯中取出了。重新生成發布文件,拷貝BLL和DAL的dll文件,再次運行,還是報錯。原來時Swagger的XML文件缺失,從bin文件夾下拷貝添加至發布文件,運行后成功显示頁面

4、有的朋友會說了,每次都要拷貝這兩個dll和這兩個xml文件,太麻煩了。其實也是有對應的解決辦法的,我們可以使用dotnet的CLI命令進行發布,選擇引用的發布文件夾為bin文件夾,拷貝至發布文件夾即可,有興趣的朋友可以自行研究

三、服務器發布

這裏我用的是阿里雲服務器,Window系統版本是Window Server2012 R2,Linux系統版本是CentOS 8.0;在操作前記得確認拷貝的發布文件能否在本地正常運行

1、Windows程序(SCD)+Nginx

1、解壓后雙擊exe文件網站可以正常運行,如下:

2、這個時候我們發現了一個問題,服務器上沒有數據庫,所以無法確認功能是否正常,這裏我們先下載安裝一個Microsoft SQL Server 2012 Express數據庫(建項目時沒有考慮到發布后測試的問題,實際上像SQLite數據庫是非常符合這類場景的)

安裝完成后我們新建一個BlogSystem的數據庫,通過Sql文件的形式將數據庫結構和數據導入至服務器數據庫,這時候又發現一個問題,由於我們連接數據庫的邏輯放置在model層的BlogSystemContext文件夾下,所以需要將連接中的DataSource更改為Express數據庫,重新發布后覆蓋舊的發布文件(系統設計有缺陷,可以將EF上下文文件放在應用程序層或單獨一層),再次運行,成功執行查詢,如下:

3、這個時候本地已經可以進行正常的訪問了,但是外部網絡是無法訪問調用接口的,這裏我們藉助Nginx進行服務的轉發。下載Nginx后解壓對conf文件夾下的nginx.conf文件進行如下配置:

4、在nginx.exe文件所在目錄的文件路徑輸入cmd,鍵入nginx啟動服務訪問8081端口,成功显示頁面(確保core程序正常運行)如下:

5、這個時候我們使用其它電腦訪問接口,發現還是無法訪問,經過查詢是阿里雲服務器進行了相關的限制,在阿里雲控制台配置安全組規則后即可正常訪問,如下:

6、配置完成后運行,成功訪問該網站且功能正常。這類方法不需要藉助Core的運行時環境,可以說十分便捷

2、Windows服務(FDD)+IIS

1、首先我們將FDD發布文件壓縮后拷貝至Window Server主機,因FDD的部署方法需要藉助.NET Core運行時環境,所以這裏我們首先到官網https://dotnet.microsoft.com/download/dotnet-core/current/runtime下載安裝.NET Core運行時,這裏我們選擇的是右邊這個,安裝完需要重新啟動

2、上一個方法中桌面显示控制台窗口顯然不是一個較佳的方案,所以這裏我們將其註冊為服務。官方提供了ASP.NET Core服務託管的方法,但使用較為複雜,這裏我們藉助一個名為nssm的工具來達到同樣的目的。我們下載nssm后,在其exe路徑運行cmd命令,執行nssm install,在彈出的窗口中進行如下配置:

3、我們在系統服務中開啟BlogSytem.Core_Server,在控制面版中選擇安裝IIS服務,併發布對應的項目,安裝完成后,添加部署為8082端口,將應用程序池修改為無託管,如下:

4、運行網站,成功显示頁面,但是進行功能試用時發現報錯;經過確認是由於IIS應用程序池的用戶驗證模式和sqlserver的驗證模式不同,解決辦法有三種①修改應用程序池高級設置中的進程模型中的標識②將連接數據庫字符串中的Integrated Security=True去除,並添加數據庫連接對應的賬號密碼③在數據庫的“安全性”>“登錄名”裏面,添加對應IIS程序池的名稱,並在這個用戶的“服務器角色”和“用戶映射”中給他對應的權限

後續嘗試方案一失敗,嘗試方案二成功,方案三由於要安裝SSMS所以沒有嘗試,有遇到相同問題的朋友可以自己試下

3、Linux程序(FDD)+Nginx

1、首先我們使用MobaXterm工具登錄至Linux主機(選擇此工具是由於其)同時支持文件傳送和命令行操作),這裏使用的Linux版本是CentOS 8.0;藉助MobaXterm工具在home文件夾下創建WebSite文件夾,並在其內部創建BlogSystem文件夾,將我們準備好的FDD部署方式的發布文件上傳至此文件夾后,使用命令sudo dnf install dotnet-sdk-3.1安裝.net core sdk,如下圖

2、輸入cd /home/WebSite/BlogSystem切換至項目文件夾后,使用dotnet BlogSystem.Core.dll運行程序,成功執行,但是由於我們沒有數據庫,且未配置代理服務器,所以無法驗證服務是否正常運行;所以這裏我們先參照微軟doc快速入門:在 Red Hat 上安裝 SQL Server 並創建數據庫安裝Sql Server數據庫(阿里雲默認安裝了python3作為解釋器所以無需重複安裝),安裝完成后我們開放在阿里雲實例中開放1433端口,使用可視化工具導入表結構和數據

3、完成上述操作后我們需要配置守護進程,將程序放在後台運行。首先我們在/etc/systemd/system下新建守護進程文件,文件名以.service結尾,這裏我們新建名為BlogSystem.service文件,使用MobaXterm自帶的編輯器打開文件後進行如下配置,注意後面的中文備註需要去除否則會報錯

[Unit]
Description=BlogSystem    #服務描述,隨便填就好

[Service]
WorkingDirectory=/home/WebSite/BlogSystem/   #工作目錄,填你應用的絕對路徑
ExecStart=/usr/bin/dotnet /home/WebSite/BlogSystem/BlogSystem.Core.dll    #啟動:前半截是你dotnet的位置(一般都在這個位置),後半部分是你程序入口的dll,中間用空格隔開
Restart=always  
RestartSec=25 #如果服務出現問題會在25秒后重啟,數值可自己設置
SyslogIdentifier=BlogSystem  #設置日誌標識,此行可以沒有
User=root   #配置服務用戶,越高越好
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target

我們使用cd /etc/systemd/system/切換至BlogSystem.service對應的目錄,使用systemctl enable BlogSystem.service設置為開機運行后,再使用systemctl start BlogSystem.service啟動服務,另外可以使用systemctl status BlogSystem確認服務狀態

4、接下來我們安裝代理Nginx代理默認的5000端口,使用sudo yum install nginx安裝nginx后,我們到\etc\nginx文件夾下打開nginx.conf文件進行如下配置:

配置完成我們進入\etc\nginx文件夾下,使用systemctl enable nginx將nginx設置為開機啟動,並使用systemctl start nginx啟用服務,同樣可以使用systemctl status nginx確認其狀態。確認無誤后在阿里雲中開放8081端口,外網可正常訪問,但功能試用時報錯,原來是數據庫連接錯誤,重新設置后即可正常訪問

本章完~

本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。

本文部分內容參考了網絡上的視頻內容和文章,僅為學習和交流,地址如下:

老張的哲學,系列一、ASP.NET Core 學習視頻教程

solenovex,ASP.NET Core 3.x 入門視頻

聲明

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

聚甘新

【Spring】循環依賴 Java Vs Spring

菜瓜:水稻,這次我特意去看了java的循環依賴

水稻:喲,有什麼收穫

菜瓜:兩種情況,構造器循環依賴,屬性循環依賴

  • 構造器循環依賴在邏輯層面無法通過。對象通過構造函數創建時如果需要創建另一個對象,就會存在遞歸調用。棧內存直接溢出
  • 屬性循環依賴可以解決。在對象創建完成之後通過屬性賦值操作。
  • package club.interview.base;
    
    /**
     * 構造器循環依賴 - Exception in thread "main" java.lang.StackOverflowError
     * toString()循環打印也會異常 - Exception in thread "main" java.lang.StackOverflowError
     * @author QuCheng on 2020/6/18.
     */
    public class Circular {
    
        class A {
            B b;
    
    //        public A() {
    //            b = new B();
    //        }
    
    //        @Override
    //        public String toString() {
    //            return "A{" +
    //                    "b=" + b +
    //                    '}';
    //        }
        }
    
        class B {
            A a;
    
    //        public B() {
    //            a = new A();
    //        }
    
    //        @Override
    //        public String toString() {
    //            return "B{" +
    //                    "a=" + a +
    //                    '}';
    //        }
        }
    
        private void test() {
            B b = new B();
            A a = new A();
            a.b = b;
            b.a = a;
            System.out.println(a);
            System.out.println(b);
        }
    
        public static void main(String[] args) {
            new Circular().test();
        }
    }

水稻:厲害啊,Spring也不支持構造函數的依賴注入,而且也不支持多例的循環依賴。同樣的,它支持屬性的依賴注入。

  • 看效果 – 如果toString()打印同樣會出現棧內存溢出。
  • package com.vip.qc.circular;
    
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    
    /**
     * @author QuCheng on 2020/6/18.
     */
    @Component("a")
    public class CircularA {
    
        @Resource
        private CircularB circularB;
    
    //    @Override
    //    public String toString() {
    //        return "CircularA{" +
    //                "circularB=" + circularB +
    //                '}';
    //    }
    }
    
    
    package com.vip.qc.circular;
    
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    
    /**
     * @author QuCheng on 2020/6/18.
     */
    @Component("b")
    public class CircularB {
    
        @Resource
        private CircularA circularA;
    
    //    @Override
    //    public String toString() {
    //        return "CircularB{" +
    //                "circularA=" + circularA +
    //                '}';
    //    }
    }
    
    
        @Test
        public void testCircular() {
            String basePackages = "com.vip.qc.circular";
            new AnnotationConfigApplicationContext(basePackages);
        }

菜瓜:看來spring的實現應該也是通過屬性注入的吧

水稻:你說的對。先給思路和demo,之後帶你掃一遍源碼,follow me !

  • spring的思路是給已經初始化的bean標記狀態,假設A依賴B,B依賴A,先創建A
    • 先從緩存容器(總共三層,一級拿不到就拿二級,二級拿不到就從三級緩存中拿正在創建的)中獲取A,未獲取到就執行創建邏輯
    • 對象A在創建完成還未將屬性渲染完之前標記為正在創建中,放入三級緩存容器。渲染屬性populateBean()會獲取依賴的對象B。
    • 此時B會走一次getBean邏輯,B同樣會先放入三級緩存,然後渲染屬性,再次走getBean邏輯注入A,此時能從三級緩存中拿到A,並將A放入二級容器。B渲染完成放入一級容器
    • 回到A渲染B的方法populateBean(),拿到B之後能順利執行完自己的創建過程。放入一級緩存
  •  為了證實結果,我把源碼給改了一下,看結果

    • package com.vip.qc.circular;
      
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      
      /**
       * @author QuCheng on 2020/6/18.
       */
      @Component("a")
      public class CircularA {
      
          @Resource
          private CircularB circularB;
      
          @Override
          public String toString() {
              return "CircularA{" +
                      "circularB=" + circularB +
                      '}';
          }
      }
      
      
      package com.vip.qc.circular;
      
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      
      /**
       * @author QuCheng on 2020/6/18.
       */
      @Component("b")
      public class CircularB {
      
          @Resource
          private CircularA circularA;
      
          @Override
          public String toString() {
              return "CircularB{" +
                      "circularA=" + circularA +
                      '}';
          }
      }
      
      
      測試代碼
      @Test
          public void testCircular() {
              String basePackages = "com.vip.qc.circular";
              new AnnotationConfigApplicationContext(basePackages);
      }
      
      測試結果(我改過源碼了)
      ---- 
      將a放入三級緩存
      將b放入三級緩存
      將a放入二級緩存
      將b放入一級緩存
      從二級緩存中拿到了a
      將a放入一級緩存

        

  • 再看源碼
    • 關鍵類處理getSingleton邏輯 – 緩存容器
      • public class DefaultSingletonBeanRegistry 
        
          /** Cache of singleton objects: bean name to bean instance. */
          // 一級緩存
            private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
        
            /** Cache of singleton factories: bean name to ObjectFactory. */
          // 三級緩存
            private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
        
            /** Cache of early singleton objects: bean name to bean instance. */
          // 二級緩存
            private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    • 主流程 AbstractApplicationContext#refresh() -> DefaultListableBeanFactory#preInstantiateSingletons() -> AbstractBeanFactory#getBean() & #doGetBean()
      • protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
              @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
           /**
            * 處理FactoryBean接口名稱轉換 {@link BeanFactory#FACTORY_BEAN_PREFIX }
            */
           final String beanName = transformedBeanName(name);
                ...
           // ①從緩存中拿對象(如果對象正在創建中且被依賴注入,會放入二級緩存)
           Object sharedInstance = getSingleton(beanName);
           if (sharedInstance != null && args == null) {
              ...
           }else {      
                    ...
                 if (mbd.isSingleton()) {
                   // ② 將創建的對象放入一級緩存
                    sharedInstance = getSingleton(beanName, () -> {
                       try {
                             // ③ 具體創建的過程,每個bean創建完成之後都會放入三級緩存,然後渲染屬性
                          return createBean(beanName, mbd, args);
                       }catch (BeansException ex) {
                         ...
           ...
           return (T) bean;
        } 
    • ①getSingleton(beanName) – 二級緩存操作
      • protected Object getSingleton(String beanName, boolean allowEarlyReference) {
           // 實例化已經完成了的放在singletonObjects
           Object singletonObject = this.singletonObjects.get(beanName);
           // 解決循環依賴
           if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
              synchronized (this.singletonObjects) {
                 singletonObject = this.earlySingletonObjects.get(beanName);
                 if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                       singletonObject = singletonFactory.getObject();
                       this.earlySingletonObjects.put(beanName, singletonObject);
                       if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                          System.out.println("將"+beanName+"放入二級緩存");;
                       this.singletonFactories.remove(beanName);
                    }
                 }else if(singletonObject != null){
                    System.out.println("從二級緩存中拿到了"+beanName);
                 }
              }
           }
           return singletonObject;
        }
    • ② getSingleton(beanName,lamdba) – 一級緩存操作
      • public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
              Assert.notNull(beanName, "Bean name must not be null");
              synchronized (this.singletonObjects) {
                 Object singletonObject = this.singletonObjects.get(beanName);
                 if (singletonObject == null) {
                    if (this.singletonsCurrentlyInDestruction) {
                    ...
                    // 正在創建的bean加入singletonsCurrentlyInCreation - 保證只有一個對象創建,阻斷循環依賴
                    beforeSingletonCreation(beanName);
                              ...
                    try {
                       singletonObject = singletonFactory.getObject();
                    ...
                    finally {
                    ...
                       // 從singletonsCurrentlyInCreation中移除
                       afterSingletonCreation(beanName);
                    }
                    if (newSingleton) {
                       // 對象創建完畢 - 放入一級緩存(從其他緩存移除)
                       addSingleton(beanName, singletonObject);
                    }
                 }
                 return singletonObject;
              }
           }
                
         //  -----  內部調用一級緩存操作
            protected void addSingleton(String beanName, Object singletonObject) {
                synchronized (this.singletonObjects) {
                    if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                        System.out.println("將"+beanName+"放入一級緩存");;
                    this.singletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                    this.earlySingletonObjects.remove(beanName);
                    this.registeredSingletons.add(beanName);
                }
            }        
           
    • ③createBean(beanName, mbd, args) – 三級緩存操作
      • protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {
             ...
           if (instanceWrapper == null) {
              // 5* 實例化對象本身
              instanceWrapper = createBeanInstance(beanName, mbd, args);
           }
           ...
           boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                 isSingletonCurrentlyInCreation(beanName));
           if (earlySingletonExposure) {
              ...
              // 將創建好還未渲染屬性的bean 放入三級緩存
              addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
           }
        
           Object exposedObject = bean;
           try {
              // 渲染bean自身和屬性
              populateBean(beanName, mbd, instanceWrapper);
              // 實例化之後的後置處理 - init
              exposedObject = initializeBean(beanName, exposedObject, mbd);
           }
           catch (Throwable ex) {
           ...
           return exposedObject;
        }
          
          
           // ------------- 內部調用三級緩存操作 
           protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
                Assert.notNull(singletonFactory, "Singleton factory must not be null");
                synchronized (this.singletonObjects) {
                    if (!this.singletonObjects.containsKey(beanName)) {
                        if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                            System.out.println("將"+beanName+"放入三級緩存");;
                        this.singletonFactories.put(beanName, singletonFactory);
                        this.earlySingletonObjects.remove(beanName);
                        this.registeredSingletons.add(beanName);
                    }
                }
            }       

菜瓜:demo比較簡單,流程大致明白,源碼我還需要斟酌一下,整體有了概念。這個流程好像是摻雜在bean的創建過程中,結合bean的生命周期整體理解可能會更深入一點

水稻:是的。每個知識點都不是單一的,拿着bean的生命周期再理解一遍可能會更有收穫。

 

討論

  • 為什麼是三級緩存,兩級不行嗎?
    • 猜測:理論上兩級也可以實現。多一個二級緩存可能是為了加快獲取的速度。假如A依賴B,A依賴C,B依賴A,C依賴A,那麼C在獲取A的時候只需要從二級緩存中就能拿到A了

總結

  • Spring的處理方式和java處理的思想一致,構造器依賴本身是破壞語義和規範的
  • 屬性賦值–> 依賴注入 。 先創建對象,再賦值屬性,賦值的時候發現需要創建便生成依賴對象,被依賴對象需要前一個對象就從緩存容器中拿取即可

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

聚甘新

文本挖掘之情感分析(一)

一、文本挖掘  

     文本挖掘則是對文本進行處理,從中挖掘出來文本中有用的信息和關鍵的規則,在文本挖掘領域應用最往廣泛的是對文本進行分類和聚類,其挖掘的方法分為無監督學習和監督學習。文本挖掘還可以劃分為7大類:關鍵詞提取、文本摘要、文本主題模型、文本聚類、文本分類、觀點提取、情感分析。

   關鍵詞提取:對長文本的內容進行分析,輸出能夠反映文本關鍵信息的關鍵詞。

   文本摘要:許多文本挖掘應用程序需要總結文本文檔,以便對大型文檔或某一主題的文檔集合做出簡要概述。

   文本聚類:主要是對未標註的文本進行標註,常見的有 K均值聚類和層次聚類。

   文本分類:文本分類使用監督學習的方法,以對未知數據的分類進行預測的機器學習方法。

   文本主題模型 LDA:LDA(Latent Dirichlet Allocation)是一種文檔主題生成模型,也稱為一個三層貝恭弘=叶 恭弘斯概率模型,包含詞、主題和文檔三層結構,該模型可以用於獲取語料的主題提取和對不同類別的文檔進行分類。

   觀點抽取:對文本(主要針對評論)進行分析,抽取出核心觀點,並判斷極性(正負面),主要用於電商、美食、酒店、汽車等評論進行分析。

   情感分析:對文本進行情感傾向判斷,將文本情感分為正向、負向、中性。用於口碑分析、話題監控、輿情分析。

   因為自己的論文寫的是關於情感分析方面的內容,因此打算接下來主要寫情感分析系列的內容,今天主要寫關於情感分析的介紹以及發展史。

二、情感分析

1. 含義

     情感分析主要是通過分析人們對於服務、產品、事件、話題來挖掘出說話人/作者觀點、情感、情緒等的研究。情感分析按照研究內容的不同,可以分為:意見挖掘 / 意見提取 / 主觀性分析 / 情感傾向分析、情感情緒分析、情感打分等。情感傾向問題,即是指挖掘出一段語料中說話人/作者對於某一話題/事件所持有的態度,如褒義、貶義、中性、兩者兼有。情感情緒,則是將情感傾向進行更進一步的細化,依據“大連理工大學的情感詞彙本體庫”可知,可以將情感傾向可以細化為:“喜歡”、“憤怒”、“討厭”等具體的7個大類——21個小類別。情感打分,即根據情感態度對於某一事物進行評分,如淘寶系統的1-5分。 文本中的情感分析還可以分為:顯式情感、隱式情感,顯式情感是指包含明顯的情感詞語(如:高興、漂亮、討厭等),隱式情感則是指不包含情感詞語的情感文本,如:“這個杯子上面有一層灰”。由於隱式情感分析難度較大,因此目前的工作多集中在顯式情感分析領域。

     情感分析按照不同的分析對象,可以分為:文章級別的情感分析、句子級別的情感分析、詞彙級別的情感分析。按照不同的研究內容以及研究的粒度的不同,其研究情感分析的方法也有很大的變化。

     目前的情感分析研究可歸納為:情感資源構建、情感元素抽取、情感分類及情感分析應用系統;

     情感資源構建:情感資源一般來說有:情感詞典、情感語料庫。情感詞典的構建即是將現有的、整理好的情感詞典資源進行整合,比如中文情感詞典有:大連理工大學的情感詞彙本體庫、知網Hownet情感詞典、台灣大學的NTUSD簡體中文情感詞典等,根據不同的需求,應用這些情感詞典。情感語料庫,則是我們要分析的文本,如關於新聞的文本、微博評論文本、商品評論文本、電影評論文本等,這些語料的獲取可以是尋找已經整理好的數據,或者自己爬蟲獲取。推薦一個比較全的中文語料庫網站:中文NLP語料庫。

    情感元素抽取:情感元素抽取則是從語料中抽取出來能夠代表說話人/作者情感態度問題的詞彙,也稱為細粒度情感分析。語料中的評價對象和表達抽取是情感元素抽取的核心內容。評價對象是指語料中被討論的主題,比如對於商品評論來說,用戶常提起的“外觀”、“快遞”、“包裝”等方面;表達抽取主要針對顯式情感表達的文本,是指文本抽取出來能夠代表說話人/作者情感、情緒、意見等的詞彙,比如“漂亮”、“贊同”、“不贊同”等。一般來說,評價對象和表達抽取也可以作為相互獨立的兩個任務。一般來說,分析這兩者的方法有:基於規則、基於機器學習。對於評價對象來說,現如今使用最多的方法是利用主題模型中的LDA(Latent Dirichlet Allocation)模型進行分析;對於表達抽取則有:深度學習的方法、基於JST (Joint Sentiment/Topic )模型的方法等。

     情感分類:情感分類則是將文本分為一個具體的類別,比如情感傾向分析,則是將文檔分為:褒義、貶義、中性等。一般來說,進行情感分類的方法有,基於情感詞典、基於機器學習。基於情感詞典,最典型的方法則是基於知網Hownet情感詞典的So-Hownet指標進行情感分類,基於機器學習的方法則有監督學習方法、半監督學習方法等。

    針對於情感分析,現已經存在一些專有平台,如:基於Boson 數據的情感分析平台,基於產品評論的平台Google Shopping。情感分析除了在電商平台應用廣泛之外,情感分析技術還被引入到對話機器人領域。例如,微軟的“小冰”機器人 可以通過分析用戶的文本輸入和表情貼圖,理解用戶當前的情緒狀況,並據此回復文本或者語音等情感回應。部分研究機構還將情感分析技術融入實體機器人中。

 2. 發展史

     V. H. 和 K. R. McKeown 於 1997 年發表的論文 [1],該論文使用對數線性回歸模型從大量語料庫中識別形容詞的正面或負面語義,同時藉助該模型對語料中出現的形容詞進行分類預測。

     Peter Turney在 2002年在論文 [2] 提出了一種無監督學習的算法,其可以很好的將語料中的詞語分類成正面情感詞和負面情感詞。

    2002 年 Bo Pang 等人在論文 [3] 中使用了傳統的機器學習方法對電影評論數據進行分類,同時也驗證了機器學習的方法的確要比之前基於規則的方法要優。

    2003 年 Blei 等人在論文 [4] 中提出了 LDA(Latent Dirichlet Allocation)模型,在之後的情感分析領域的工作中,很多學者/研究人員都使用主題模型來進行情感分析,當然也不只是基於主題模型來進行情感分析研究,還有很多利用深度學習方法來進行情感分析的研究。

   Lin 和 He在 2009 年的論文 [5] 提出了一種基於主題模型的模型 —JST(Joint Sentiment/Topic),其有效的將情感加入到了經典的主題模型當中,因此利用該模型可以獲取到不同情感極性標籤下不同主題的分佈情況。傳統的主題模型獲取文檔的主題以及詞的分佈情況,但並沒有關注到情感的存在,因此基於該模型可以對文檔的情感傾向進行分析。利用 JST 模型可以有效的直接將語料的主題、情感信息挖掘出來,同時 JST 模型還考慮到了主題、文檔、情感、詞之間的聯繫。

    基於對於語義和句法的考慮,Jo 和 H.OH 在 2011 年提出了 ASUM(Aspect and Sentiment Unification Model)模型,該模型和 JST 模型很相似都是四層的貝恭弘=叶 恭弘斯網絡結構 [6] 。

    基於神經網絡的語義組合算法被驗證是一種非常有效的特徵學習手段,2013年,Richard Socher和Christopher Potts等人提出多個基於樹結構的Recursive Neural Network,該方法通過迭代運算的方式學習變量長度的句子或短語的語義表示,在斯坦福情感分析樹庫(Stanford Sentiment Treebank)上驗證了該方法的有效性 [7]。Nal Kalchbrenner等人描述了一個卷積體繫結構,稱為動態卷積神經網絡(DCNN),他們採用它來進行句子的語義建模。 該網絡使用動態k-Max池,這是一種線性序列的全局池操作。 該網絡處理不同長度的輸入句子,並在句子上引入能夠明確捕獲短程和長程關係的特徵圖。 網絡不依賴於解析樹,並且很容易適用於任何語言。該模型在句子級情感分類任務上取得了非常出色的效果[8]。2015年,Kai Sheng Tai,Richard Socher, Christopher D. Manning在序列化的LSTM (Long Short-Term Memory)模型的基礎上加入了句法結構的因素,該方法在句法分析的結果上進行語義組合,在句子級情感分類和文本蘊含任務(text entailment)上都取得了很好的效果[9]。

   2016年,Qiao Qian, Xiaoyan Zhu等人在LSTM和Bi-LSTM模型的基礎上加入四種規則約束,這四種規則分別是: Non-Sentiment Regularizer,Sentiment Regularizer, Negation Regularizer, Intensity Regularizer,利用語言資源和神經網絡相結合來提升情感分類問題的精度。

  除了上面的一些研究,關於情感分析領域的應用仍然有很多,比如:2015 年鄭祥雲等人通過主題模型提取出來圖書館用戶的主題信息,最後利用這些信息來進行個性化圖書的有效推薦。將 JST 模型中直接引入了情感詞典作為外部先驗知識來對新聞文本進行分析,獲取其中的主旨句,並對主旨句進行情感打分,同時利用情感主旨句來代替全文,這樣能夠使得用戶更有效、更快速的閱讀文章。

  總的來說,情感分析在很多的領域被應用,當然情感分析也有很多的局限性,就是過多的依賴於語料庫信息,同時還需要使用自然語言、人工智能的方法來能夠最大化的挖掘出來其中的信息。

 

 參考文獻:

 [1] :Predicting the semantic orientation of adjectives

 [2]:  Thumbs up or thumbs down? Semantic orientation applied to unsupervised classification of reviews 

 [3] : Thumbs up? Sentiment Classification using Machine Learning Techniques

 [4] :   Latent Dirichlet Allocation

 [5] : Joint sentiment/topic model for sentiment analysis

 [6] : Aspect and sentiment unification model for online review analysis

 [7] : Recursive Deep Models for Semantic Compositionality Over a Sentiment Treebank

 [8]:  A Convolutional Neural Network for Modelling Sentences

 [9]: Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks

 [10] : Linguistically Regularized LSTMs for Sentiment Classification

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

聚甘新

從一個計算器開始說起——C#中的工廠方法模式

工廠模式作為很常見的設計模式,在日常工作中出鏡率非常高,程序員們一定要掌握它的用法喲,今天跟着老胡一起來看看吧。

舉個例子

現在先讓我們來看一個例子吧,比如,要開發一個簡單的計算器,完成加減功能,通過命令行讀入形如1+1的公式,輸出2這個結果,讓我們看看怎麼實現吧。

 

第一個版本

這個版本裏面,我們不考慮使用模式,就按照最簡單的結構,怎麼方便怎麼來。

思路非常簡單,僅需要實現以下幾個方法

  • 取運算數
  • 取運算符
  • 輸出結果
     class Program
    {
        static int GetOperatorIndex(string input)
        {
            int operatorIndex = 0;
            for (; operatorIndex < input.Length; operatorIndex++)
            {
                if (!char.IsDigit(input[operatorIndex]))
                    break;
            }
            return operatorIndex;
        }

        static int GetOp(string input, int startIndex, int size = -1)
        {
            string subStr;
            if (size == -1)
            {
                subStr = input.Substring(startIndex);
            }
            else
            {
                subStr = input.Substring(startIndex, size);
            }
            return int.Parse(subStr);
        }

        static int CalculateExpression(string input)
        {
            var operatorIndex = GetOperatorIndex(input); //得到運算符索引
            var op1 = GetOp(input, 0, operatorIndex); //得到運算數1
            var op2 = GetOp(input, operatorIndex + 1); //得到運算數2
            switch (input[operatorIndex])
            {
                case '+':
                    return op1 + op2;
                case '-':
                    return op1 - op2;
                default:
                    throw new Exception("not support");
            }
        }

        static void Main(string[] args)
        {
            string input = Console.ReadLine();
            while(!string.IsNullOrEmpty(input))
            {
                var result = CalculateExpression(input);
                Console.WriteLine("={0}", result);
                input = Console.ReadLine();
            }            
        }
}

代碼非常簡單,毋庸置疑,這個運算器是可以正常工作的。這也可能是我們大部分人剛剛踏上工作崗位的時候可能會寫出的代碼。但它有着以下這些缺點:

  • 缺乏起碼的抽象,至少加和減應該能抽象出操作類。
  • 缺乏抽象造成了巨型客戶端,所有的邏輯都嵌套在了客戶端裏面。
  • 使用switch case缺乏擴展性,同時switch case也暗指了這部分代碼是屬於變化可能性比較高的地方,我們應該把它們封裝起來。而且不能把他們放在和客戶端代碼一起

接下來,我們引入我們的主題,工廠方法模式。
 

工廠方法模式版本

工廠方法模式使用一個虛擬的工廠來完成產品構建(在這裡是運算符的構建,因為運算符是我們這個程序中最具有變化的部分),通過把可變化的部分封裝在工廠類中以達到隔離變化的目的。我們看看UML圖:

依葫蘆畫瓢,我們設計思路如下:

  • 設計一個IOperator接口,對應抽象的Product
  • 設計AddOperator和SubtractOperator,對應具體Product
  • 設計IOperatorFactory接口生產Operator
  • 設計OperatorFactory實現抽象IFactory

關鍵代碼如下,其他讀取操作數之類的代碼就不在贅述。

  • IOperator接口
       interface IOperator
       {
           int Calculate(int op1, int p2);
       }
  • 具體Operator
	class AddOperator : IOperator
        {
            public int Calculate(int op1, int op2)
            {
                return op1 + op2;
            }
        }

        class SubtractOperator : IOperator
        {
            public int Calculate(int op1, int op2)
            {
                return op1 - op2;
            }
        }
  • Factory接口
	interface IOperatorFactory
        {
            IOperator CreateOperator(char c);
        }
  • 具體Factory
	class OperatorFactory : IOperatorFactory
        {
            public IOperator CreateOperator(char c)
            {
                switch(c)
                {
                    case '+':
                        return new AddOperator();
                    case '-':
                        return new SubtractOperator();
                    default:
                        throw new Exception("Not support");
                }
            }
        }
  • 在CalculateExpression裏面使用他們
   static IOperator GetOperator(string input, int operatorIndex)
       {
           IOperatorFactory f = new OperatorFactory();
           return f.CreateOperator(input[operatorIndex]);
       }

       static int CalculateExpression(string input)
       {
           var operatorIndex = GetOperatorIndex(input);
           var op1 = GetOp(input, 0, operatorIndex);
           var op2 = GetOp(input, operatorIndex + 1);
           IOperator op = GetOperator(input, operatorIndex);
           return op.Calculate(op1, op2);                    
       }

這樣,我們就用工廠方法重新寫了一次計算器,現在看看,好處有

  • 容易變化的創建部分被工廠封裝了起來,工廠和客戶端以接口的形式依賴,工廠內部邏輯可以隨時變化而不用擔心影響客戶端代碼
  • 工厂部分可以放在另外一個程序集,項目規劃會更加合理
  • 客戶端僅僅需要知道工廠和抽象的產品類,不需要再知道每一個具體的產品(不需要知道如何構建每一個具體運算符),符合迪米特法則
  • 擴展性增強,如果之後需要添加乘法multiple,那麼僅需要添加一個Operator類代表Multiple並且修改Facotry裏面的生成Operator邏輯就可以了,不會影響到客戶端
     

自此,我們已經在代碼裏面實現了工廠方法模式,但可能有朋友就會想,雖然現在擴展性增強了,但是新添加運算符還是需要修改已有的工廠,這不是違反了開閉原則么。。有沒有更好的辦法呢?當然是有的。
 

反射版本

想想工廠方法那個版本,我們為什麼增加新的運算符就會不可避免的修改現有工廠?原因就是我們通過switch case來硬編碼“教導”工廠如何根據用戶輸入產生正確的運算符,那麼如果有一種方法可以讓工廠自動學會發現新的運算符,那麼我們的目的不就達到了?

嗯,我想聰明的朋友們已經知道了,用屬性嘛,在C#中,這種方法完成類的自描述,是最好不過了的。

我們的設計思路如下:

  • 定義一個描述屬性以識別運算符
  • 在運算符中添加該描述屬性
  • 在工廠啟動的時候,掃描程序集以註冊所有運算符

代碼如下:

  • 描述屬性
    class OperatorDescriptionAttribute : Attribute
    {
        public char Symbol { get; }
        public OperatorDescriptionAttribute(char c)
        {
            Symbol = c;
        }
    }
  • 添加描述屬性到運算符
    [OperatorDescription('+')]
    class AddOperator : IOperator
    {
        public int Calculate(int op1, int op2)
        {
            return op1 + op2;
        }
    }

    [OperatorDescription('-')]
    class SubtractOperator : IOperator
    {
        public int Calculate(int op1, int op2)
        {
            return op1 - op2;
        }
    }
  • 讓工廠使用描述屬性
    class OperatorFactory : IOperatorFactory
    {
        private Dictionary<char, IOperator> dict = new Dictionary<char, IOperator>();
        public OperatorFactory()
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            foreach (var type in assembly.GetTypes())
            {
                if (typeof(IOperator).IsAssignableFrom (type) 
                    && !type.IsInterface)
                {
                    var attribute = type.GetCustomAttribute<OperatorDescriptionAttribute>();
                    if(attribute != null)
                    {
                        dict[attribute.Symbol] = Activator.CreateInstance(type) as IOperator;
                    }
                }
            }
        }
        public IOperator CreateOperator(char c)
        {
            if(!dict.ContainsKey(c))
            {
                throw new Exception("Not support");
            }
            return dict[c];
        }
    }

經過這種改造,現在程序對擴展性支持已經很友好了,需要添加Multiple,只需要添加一個Multiple類就可以,其他代碼都不用修改,這樣就完美符合開閉原則了。

  [OperatorDescription('*')]
  class MultipleOperator : IOperator
  {
      public int Calculate(int op1, int op2)
      {
          return op1 * op2;
      }
  }

這就是我們怎麼一步步從最原始的代碼走過來,一點點重構讓代碼實現工廠方法模式,最終再完美支持開閉原則的過程,希望能幫助到大家。

其實關於最後那個通過標記屬性實現擴展,微軟有個MEF框架支持的很好,原理跟這個有點相似,有機會我們再聊聊MEF。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

聚甘新