.Net Core實戰之基於角色的訪問控制的設計,.Net微服務實戰之技術架構分層篇,.Net微服務實戰之技術選型篇

前言

  上個月,我寫了兩篇微服務的文章:《.Net微服務實戰之技術架構分層篇》與《.Net微服務實戰之技術選型篇》,微服務系列原有三篇,當我憋第三篇的內容時候一直沒有靈感,因此先打算放一放。

  本篇文章與源碼原本打算實在去年的時候完成併發布的,然而我一直忙於公司項目的微服務的實施,所以該篇文章一拖再拖。如今我花了點時間整理了下代碼,並以此篇文章描述整個實現思路,並開放了源碼給予需要的人一些參考。

  源碼:https://github.com/SkyChenSky/Sikiro.RBAC

RBAC

  Role-Based Access Contro翻譯成中文就是基於角色的訪問控制,文章以下我都用他的簡稱RBAC來描述。

  現信息系統的權限控制大多數採取RBAC的思想進行實現,其本質思想是對系統各種的操作權限不是直接授予具體的某個用戶,而是在用戶集合與權限集合之間建立一個角色,作為間接關聯。每一種角色對應一組相應的權限。一旦用戶被分配了適當的角色后,該用戶就擁有此角色的所有操作權限。

  通過以上的描述,我們可以分析出以下信息:

  •   用戶與權限是通過角色間接關聯的
  •   角色的本質就是權限組(權限集合)

  這樣做的好處在於,不必在每次創建用戶時都進行分配權限的操作,只要分配用戶相應的角色即可,而且角色的權限變更比用戶的權限變更要少得多,這樣將簡化用戶的權限管理,減少系統的開銷。

  

功能分析

權限分類

從權限的作用可以分為三種,功能權限、訪問權限、數據權限

  • 功能權限
    • 功能權限指系統用戶允許在頁面進行按鈕操作的權限。如果有權限則功能按鈕展示,否則隱藏。
  • 訪問權限
    • 訪問權限指系統用戶通過點擊按鈕後進行地址的請求訪問的權限(地址跳轉與接口請求),如果無權限訪問,則由頁面提示無權限訪問。
  • 數據權限
    • 數據權限指用戶可訪問系統的數據權限,不同的用戶可以訪問不同的數據粒度。

數據權限的實現可大可小,大可大到對條件進行動態配置,小可小到只針對某個維度進行硬編碼。不納入這次的討論範圍。

用例圖

非功能性需求

  時效性,直接影響到安全性,既然是權限控制,那麼理應一修改權限后就立刻生效。曾經有同行問過我,是不是每一個請求都得去查一次數據庫是否滿足權限,如果是,數據庫壓力豈不是很大?

  安全性,每一個頁面跳轉,每一個讀寫請求都的進行一次權限驗證,不滿足的權限的功能按鈕就不需要渲染,避免樣式display:none的情況。

  開發效率,權限控制理應是框架層面的,因此盡可能作為非業務的侵入性,讓開發人員保持原有的數據善增改查與頁面渲染。

技術選型

LayUI

  學習門檻極低,開箱即用。其外在極簡,卻又不失飽滿的內在,體積輕盈,組件豐盈,從核心代碼到 API 的每一處細節都經過精心雕琢,非常適合界面的快速開發,它更多是為服務端程序員量身定做,無需涉足各種前端工具的複雜配置,只需面對瀏覽器本身,讓一切你所需要的元素與交互,從這裏信手拈來。作為國人的開源項目,完整的接口文檔與Demo示例讓入門者非常友好的上手,開箱即用的Api讓學習成本盡可能的低,其易用性成為快速開發框架的基礎。

MongoDB

  主要兩大優勢,無模式與橫向擴展。對於權限模塊來說,無需SQL來寫複雜查詢和報表,也不需要使用到多表的強事務,上面提到的時效性的數據庫壓力問題也可以通過分片解決。無模式使得開發人員無需預定義存儲結構,結合MongoDB官方提供的驅動可以做到快速的開發。

數據庫設計

 E-R圖

 

  一個管理員可以擁有多個角色,因此管理員與角色是一對多的關聯;角色作為權限組的存在,又可以選擇多個功能權限值與菜單,所以角色與菜單、功能權限值也是一對多的關係。

類圖

Deparment與Position屬於非核心,可以按照自己的實際業務進行擴展。

功能權限值初始化

  隨着業務發展,需求功能是千奇百怪的,根本無法抽象出來,那麼功能按鈕就要隨着業務進行定義。在我的項目里使用了枚舉值進行定義每個功能權限,通過自定義的PermissionAttribute與響應的action進行綁定,在系統啟動時,通過反射把功能權限的枚舉值與相應的controller、action映射到MenuAction表,枚舉值對應code字段,controller與action拼接后對應url字段。

  已初始化到數據庫的權限值可以到菜單頁把相對應的菜單與權限通過用戶界面關聯起來。

權限值綁定action

1         [HttpPost]
2         [Permission(PermCode.Administrator_Edit)]
3         public IActionResult Edit(EditModel edit)
4         {
5             //do something
6 
7             return Json(result);
8         }

初始化權限值

 1     /// <summary>
 2     /// 功能權限
 3     /// </summary>
 4     public static class PermissionUtil
 5     {
 6         public static readonly Dictionary<string, IEnumerable<int>> PermissionUrls = new Dictionary<string, IEnumerable<int>>();
 7         private static MongoRepository _mongoRepository;
 8 
 9         /// <summary>
10         /// 判斷權限值是否被重複使用
11         /// </summary>
12         public static void ValidPermissions()
13         {
14             var codes = Enum.GetValues(typeof(PermCode)).Cast<int>();
15             var dic = new Dictionary<int, int>();
16             foreach (var code in codes)
17             {
18                 if (!dic.ContainsKey(code))
19                     dic.Add(code, 1);
20                 else
21                     throw new Exception($"權限值 {code} 被重複使用,請檢查 PermCode 的定義");
22             }
23         }
24 
25         /// <summary>
26         /// 初始化添加預定義權限值
27         /// </summary>
28         /// <param name="app"></param>
29         public static void InitPermission(IApplicationBuilder app)
30         {
31             //驗證權限值是否重複
32             ValidPermissions();
33 
34             //反射被標記的Controller和Action
35             _mongoRepository = (MongoRepository)app.ApplicationServices.GetService(typeof(MongoRepository));
36 
37             var permList = new List<MenuAction>();
38             var actions = typeof(PermissionUtil).Assembly.GetTypes()
39                 .Where(t => typeof(Controller).IsAssignableFrom(t) && !t.IsAbstract)
40                 .SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly));
41 
42             //遍歷集合整理信息
43             foreach (var action in actions)
44             {
45                 var permissionAttribute =
46                     action.GetCustomAttributes(typeof(PermissionAttribute), false).ToList();
47                 if (!permissionAttribute.Any())
48                     continue;
49 
50                 var codes = permissionAttribute.Select(a => ((PermissionAttribute)a).Code).ToArray();
51                 var controllerName = action?.ReflectedType?.Name.Replace("Controller", "").ToLower();
52                 var actionName = action.Name.ToLower();
53 
54                 foreach (var item in codes)
55                 {
56                     if (permList.Exists(c => c.Code == item))
57                     {
58                         var menuAction = permList.FirstOrDefault(a => a.Code == item);
59                         menuAction?.Url.Add($"{controllerName}/{actionName}".ToLower());
60                     }
61                     else
62                     {
63                         var perm = new MenuAction
64                         {
65                             Id = item.ToString().EncodeMd5String().ToObjectId(),
66                             CreateDateTime = DateTime.Now,
67                             Url = new List<string> { $"{controllerName}/{actionName}".ToLower() },
68                             Code = item,
69                             Name = ((PermCode)item).GetDisplayName() ?? ((PermCode)item).ToString()
70                         };
71                         permList.Add(perm);
72                     }
73                 }
74                 PermissionUrls.TryAdd($"{controllerName}/{actionName}".ToLower(), codes);
75             }
76 
77             //業務功能持久化
78             _mongoRepository.Delete<MenuAction>(a => true);
79             _mongoRepository.BatchAdd(permList);
80         }
81 
82         /// <summary>
83         /// 獲取當前路徑
84         /// </summary>
85         /// <param name="filterContext"></param>
86         /// <returns></returns>
87         public static string CurrentUrl(HttpContext filterContext)
88         {
89             var url = filterContext.Request.Path.ToString().ToLower().Trim('/');
90             return url;
91         }
92     }

關聯菜單與功能權限

訪問權限

  當所有權限關係關聯上后,用戶訪問系統時,需要對其所有操作進行攔截與實時的權限判斷,我們註冊一個全局的GlobalAuthorizeAttribute,其主要攔截所有已經標識PermissionAttribute的action,查詢該用戶所關聯所有角色的權限是否滿足允許通過。

  我的實現有個細節,給判斷用戶IsSuper==true,也就是超級管理員,如果是超級管理員則繞過所有判斷,可能有人會問為什麼不在角色添加一個名叫超級管理員進行判斷,因為名稱是不可控的,在代碼邏輯里並不知道用戶起的所謂的超級管理員,就是我們需要繞過驗證的超級管理員,假如他叫無敵管理員呢?

 1  /// <summary>
 2     /// 全局的訪問權限控制
 3     /// </summary>
 4     public class GlobalAuthorizeAttribute : System.Attribute, IAuthorizationFilter
 5     {
 6         #region 初始化
 7         private string _currentUrl;
 8         private string _unauthorizedMessage;
 9         private readonly List<string> _noCheckPage = new List<string> { "home/index", "home/indexpage", "/" };
10 
11         private readonly AdministratorService _administratorService;
12         private readonly MenuService _menuService;
13 
14         public GlobalAuthorizeAttribute(AdministratorService administratorService, MenuService menuService)
15         {
16             _administratorService = administratorService;
17             _menuService = menuService;
18         } 
19         #endregion
20 
21         public void OnAuthorization(AuthorizationFilterContext context)
22         {
23             context.ThrowIfNull();
24 
25             _currentUrl = PermissionUtil.CurrentUrl(context.HttpContext);
26 
27             //不需要驗證登錄的直接跳過
28             if (context.Filters.Count(a => a is AllowAnonymousFilter) > 0)
29                 return;
30 
31             var user = GetCurrentUser(context);
32             if (user == null)
33             {
34                 if (_noCheckPage.Contains(_currentUrl))
35                     return;
36 
37                 _unauthorizedMessage = "登錄失效";
38 
39                 if (context.HttpContext.Request.IsAjax())
40                     NoUserResult(context);
41                 else
42                     LogoutResult(context);
43                 return;
44             }
45 
46             //超級管理員跳過
47             if (user.IsSuper)
48                 return;
49 
50             //賬號狀態判斷
51             var administrator = _administratorService.GetById(user.UserId);
52             if (administrator != null && administrator.Status != EAdministratorStatus.Normal)
53             {
54                 if (_noCheckPage.Contains(_currentUrl))
55                     return;
56 
57                 _unauthorizedMessage = "親~您的賬號已被停用,如有需要請您聯繫系統管理員";
58 
59                 if (context.HttpContext.Request.IsAjax())
60                     AjaxResult(context);
61                 else
62                     AuthResult(context, 403, GoErrorPage(true));
63 
64                 return;
65             }
66 
67             if (_noCheckPage.Contains(_currentUrl))
68                 return;
69 
70             var userUrl = _administratorService.GetUserCanPassUrl(user.UserId);
71 
72             // 判斷菜單訪問權限與菜單訪問權限
73             if (IsMenuPass(userUrl) && IsActionPass(userUrl))
74                 return;
75 
76             if (context.HttpContext.Request.IsAjax())
77                 AuthResult(context, 200, GetJsonResult());
78             else
79                 AuthResult(context, 403, GoErrorPage());
80         }
81     }

功能權限

  在權限驗證通過後,返回view之前,還是利用了Filter進行一個實時的權限查詢,主要把該用戶所擁有功能權限值查詢出來通過ViewData[“PermCodes”]傳到頁面,然後通過razor進行按鈕的渲染判斷。

  然而我在項目中封裝了大部分常用的LayUI控件,主要利用.Net Core的TagHelper進行了封裝,TagHelper內部與ViewData[“PermCodes”]進行判斷是否輸出HTML。

全局功能權限值查詢

 1 /// <summary>
 2     /// 全局用戶權限值查詢
 3     /// </summary>
 4     public class GobalPermCodeAttribute : IActionFilter
 5     {
 6         private readonly AdministratorService _administratorService;
 7 
 8         public GobalPermCodeAttribute(AdministratorService administratorService)
 9         {
10             _administratorService = administratorService;
11         }
12 
13         private static AdministratorData GetCurrentUser(HttpContext context)
14         {
15             return context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.UserData)?.Value.FromJson<AdministratorData>();
16         }
17 
18 
19         public void OnActionExecuting(ActionExecutingContext context)
20         {
21             ((Controller)context.Controller).ViewData["PermCodes"] = new List<int>();
22 
23             if (context.HttpContext.Request.IsAjax())
24                 return;
25 
26             var user = GetCurrentUser(context.HttpContext);
27             if (user == null)
28                 return;
29 
30             if (user.IsSuper)
31                 return;
32 
33             ((Controller)context.Controller).ViewData["PermCodes"] = _administratorService.GetActionCode(user.UserId).ToList();
34         }
35 
36         public void OnActionExecuted(ActionExecutedContext context)
37         {
38         }
39     }

LayUI Buttom的TagHelper封裝

 1   [HtmlTargetElement("LayuiButton")]
 2     public class LayuiButtonTag : TagHelper
 3     {
 4         #region 初始化
 5         private const string PermCodeAttributeName = "PermCode";
 6         private const string ClasstAttributeName = "class";
 7         private const string LayEventAttributeName = "lay-event";
 8         private const string LaySubmitAttributeName = "LaySubmit";
 9         private const string LayIdAttributeName = "id";
10         private const string StyleAttributeName = "style";
11 
12         [HtmlAttributeName(StyleAttributeName)]
13         public string Style { get; set; }
14 
15         [HtmlAttributeName(LayIdAttributeName)]
16         public string Id { get; set; }
17 
18         [HtmlAttributeName(LaySubmitAttributeName)]
19         public string LaySubmit { get; set; }
20 
21         [HtmlAttributeName(LayEventAttributeName)]
22         public string LayEvent { get; set; }
23 
24         [HtmlAttributeName(ClasstAttributeName)]
25         public string Class { get; set; }
26 
27         [HtmlAttributeName(PermCodeAttributeName)]
28         public int PermCode { get; set; }
29 
30         [HtmlAttributeNotBound]
31         [ViewContext]
32         public ViewContext ViewContext { get; set; }
33 
34         #endregion
35         public override async void Process(TagHelperContext context, TagHelperOutput output)
36         {
37             context.ThrowIfNull();
38             output.ThrowIfNull();
39 
40             var administrator = ViewContext.HttpContext.GetCurrentUser();
41             if (administrator == null)
42                 return;
43 
44             var childContent = await output.GetChildContentAsync();
45 
46             if (((List<int>)ViewContext.ViewData["PermCodes"]).Contains(PermCode) || administrator.IsSuper)
47             {
48                 foreach (var item in context.AllAttributes)
49                 {
50                     output.Attributes.Add(item.Name, item.Value);
51                 }
52 
53                 output.TagName = "a";
54                 output.TagMode = TagMode.StartTagAndEndTag;
55                 output.Content.SetHtmlContent(childContent.GetContent());
56             }
57             else
58             {
59                 output.TagName = "";
60                 output.TagMode = TagMode.StartTagAndEndTag;
61                 output.Content.SetHtmlContent("");
62             }
63         }
64     }

 

視圖代碼

結尾

  以上就是我本篇分享的內容,項目是以單體應用提供的,方案思路也適用於前後端分離。最後附上幾個系統效果圖

 

 

 

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

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

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

.NetCore對接各大財務軟件憑證API——用友系列(2)

一. 前言

今天我們繼續來分析用友系列的第二個產品–U8Cloud2.5 ,apilink方式的API.官網的API文檔地址如下:U8API文檔 因為我們主要是憑證對接,所以使用到的模塊有總賬、基礎檔案這兩個模塊。

Ps:2.5的財務系統如果不是最新補丁的話,要記得打補丁,不然後續的科目接口會有問題。

二. API參數

2.1 遠程訪問財務系統

如果我們對接的財務系統是公有雲的U8C的話,你會得到一個遠程的財務系統的地址,接着使用UClient工具,即 通過集成友戶通,為企業應用提供了統一的單點登陸支持,支持CA登陸、短信登陸、用戶名/密碼登陸,支持企業用戶系統與友戶通進行綁定,實現統一的用戶登陸服務 的這麼一個工具。

具體的添加應用的步驟為

2.2 全局請求頭

首先,我們必須要在網站內註冊賬號,API集市上有各個接口的詳細說明,我們需要獲取一個apicode參數,每個API模塊在點擊購買后系統會自動分配該模塊的apicode,所以這也就是我們需要兩個不同的apicode.

基本上U8cloud2.5的版本接口,需要涉及到的請求參數就是這個了,接着我們就可以愉快的進行開發工作了。

如圖,固定的全局請求頭參數有以下幾個:

1.authoration–驗證方式;默認是apicode

2.apicode—模塊的apicode.也就是我們上文中購買模塊后得到的參數.

3.system—系統類型. 1—測試. 2—正式.

4.trantype–翻譯類型; 默認為code.即採用編碼模式.

2.3 基礎檔案

基礎檔案,我們主要使用到的API接口有科目查詢.

會計主體賬簿編碼–我們可以從財務系統里獲取,具體的獲取方式如下

打開U8Client,使用正確的用戶名和密碼登錄財務系統.在企業建模平台–》基礎檔案–》組織機構–》會計主體 一欄,可以看到我們使用的會計主體賬簿編碼.如我們要使用的就是40001-9999

其中40001為公司編碼,9999為會計方案.可以看到是採用分頁形式來訪問的,所以如果我們要一次性獲取到所有的會計科目,可以採用以下方法。

        public AccountQueryResponse QueryAccount(string pk_subjscheme, string pageIndex, string glorgBookCode)
        {
            var request = new AccountQueryRequest();
            var pms = new Dictionary<string, object>();
            pms.Add("pk_subjscheme", pk_subjscheme);
            pms.Add("glorgbookcode", glorgBookCode);
            pms.Add("page_now", pageIndex);
            pms.Add("page_size", "100");
            request.SetPostParameters(pms);
            return _Client.Excute(request);
        }

        public List<U8AccountResult> GetAccountQueryResult(string pk_subjscheme, string pageIndex, string glorgBookCode)
        {
            var list = new List<U8AccountResult>();
            var response = QueryAccount(pk_subjscheme, pageIndex, glorgBookCode);
            if (response != null && response.status == "success" && response.data != null)
            {
                var result = JsonConvert.DeserializeObject<AccountQueryResult>(response.data);
                list = result.datas == null ? new List<U8AccountResult>() : result.datas.ToList().Select(x => new U8AccountResult
                {
                    balanorient = x.accsubjParentVO.balanorient,
                    subjcode = x.accsubjParentVO.subjcode,
                    subjname = x.accsubjParentVO.subjname,
                    dispname = x.accsubjParentVO.dispname,
                    remcode = x.accsubjParentVO.remcode,
                    subjId = x.accsubjParentVO.pk_accsubj,
                    endflag = x.accsubjParentVO.endflag,
                    subjectAssInfos = x.subjass == null ? new List<AccSubjectAssInfo>() : x.subjass.ToList().Select(t => new AccSubjectAssInfo
                    {
                        bdcode = t.bdcode,
                        bddispname = t.bddispname,
                        bdname = t.bdname
                    }).ToList()
                }).ToList();
            }
            return list;
        }

///獲取所有的會計科目
        public List<U8AccountResult> GetAllAccount(string pk_subjescheme, string glorgBookCode)
        {
            var pageNo = "1";
            var list = new List<U8AccountResult>();
            var response = QueryAccount(pk_subjescheme, pageNo, glorgBookCode);
            if (response != null && response.status == "success" && response.data != null)
            {
                var result = JsonConvert.DeserializeObject<AccountQueryResult>(response.data);
                var allCount = Math.Ceiling(Convert.ToDouble(result.allcount) / result.retcount);
                if (allCount >= 1)
                {
                    for (int i = 1; i <= allCount; i++)
                    {
                        var resultList = GetAccountQueryResult(pk_subjescheme, i.ToString(), glorgBookCode);
                        list.AddRange(resultList);
                    }
                }
            }
            return list;
        }

allCount為總條數,retCount為當次請求的分頁條數,默認最大值為100,即接口每次只能返回100條數據,超過100條的數據量,我們就要採用分頁的形式來獲取了。

這裏,有兩個隱藏的坑需要注意一下

1.如果沒有打過類似“patch_會計科目查詢api查詢條件增加會計主體賬簿編碼”這樣的補丁,我們無法傳入會計主體賬簿編碼,就默認返回該集團下所有公司的會計科目,這樣顯然達不到我們的目的。

2.返回的會計科目中沒有輔助核算明細,這對於我們傳輸憑證也是有影響的。所以這兩個補丁,如果我們在對接的過程中發現有接口有問題,那麼就要聯繫總部的老師幫忙打相應的補丁了.

2.4 總賬

總賬模塊,主要是我們的憑證傳輸了.

我們先來看憑證的保存,憑證保存要傳入相應的憑證json串.

        public GL_VoucherInsertResponse InsertVoucher(List<object> models)
        {
            var request = new GL_VoucherInsertRequest();
            var pms = new Dictionary<string, object>();
            pms.Add("voucher", models);
            request.SetPostParameters(pms);
            return _Client.Excute(request);
        }
///憑證新增結果
        public List<U8GLVoucherResult> GetVoucherInsertResult(List<object> models)
        {
            var list = new List<U8GLVoucherResult>();
            var response = InsertVoucher(models);
            if (response != null && response.status == "success")
            {
                if (response.data != null && !response.data.IsNullOrEmpty())
                {
                    var result = JsonConvert.DeserializeObject<List<VoucherResult>>(response.data);
                    list = result.Select(x => new U8GLVoucherResult
                    {
                        explanation = x.explanation,
                        glorgbook_code = x.glorgbook_code,
                        glorgbook_name = x.glorgbook_name,
                        no = x.no,
                        pk_glorgbook = x.pk_glorgbook,
                        pk_voucher = x.pk_voucher,
                        totalcredit = x.totalcredit,
                        totaldebit = x.totaldebit,
                        pk_vouchertype = x.pk_vouchertype,
                        vouchertype_code = x.vouchertype_code,
                        vouchertype_name = x.vouchertype_name,
                        prepareddate = Convert.ToDateTime(x.prepareddate),
                        errorMsg = ""
                    }).ToList();
                }
            }
            else
            {
                list.Add(new U8GLVoucherResult { errorMsg = response.errormsg });
            }
            return list;
        }

借貸方,憑證字主要用於我們新增后回執進行憑證記錄的.

接着我們來看憑證保存的實體類.

 public class U8VoucherModel
    {
        /// <summary>
        /// 是否差異憑證
        /// </summary>
        public bool ISDIFFLAG { get; set; }
        /// <summary>
        /// 附單據數
        /// </summary>
        public string attachment { get; set; }
        public Detail[] details { get; set; }
        /// <summary>
        /// 憑證摘要
        /// </summary>
        public string explanation { get; set; }
        /// <summary>
        /// 憑證號
        /// </summary>
        public string no { get; set; }
        /// <summary>
        /// 公司
        /// </summary>
        public string pk_corp { get; set; }
        /// <summary>
        /// 賬簿
        /// </summary>
        public string pk_glorgbook { get; set; }
        /// <summary>
        /// 制單人編碼
        /// </summary>
        public string pk_prepared { get; set; }
        /// <summary>
        /// 憑證類別簡稱
        /// </summary>
        public string pk_vouchertype { get; set; }
        /// <summary>
        /// 制單日期
        /// </summary>
        public string prepareddate { get; set; }
        /// <summary>
        /// 憑證類型
        /// </summary>
        public int voucherkind { get; set; }
    }

    public class Detail
    {
        /// <summary>
        /// 原幣貸方金額
        /// </summary>
        public string creditamount { get; set; }
        /// <summary>
        /// 貸方數量
        /// </summary>
        public string creditquantity { get; set; }
        /// <summary>
        /// 原幣借方金額
        /// </summary>
        public string debitamount { get; set; }
        /// <summary>
        /// 借方數量
        /// </summary>
        public string debitquantity { get; set; }
        /// <summary>
        /// 分錄號
        /// </summary>
        public string detailindex { get; set; }
        /// <summary>
        /// 匯率
        /// </summary>
        public string excrate1 { get; set; }
        /// <summary>
        /// 摘要
        /// </summary>
        public string explanation { get; set; }
        /// <summary>
        /// 本幣貸方金額
        /// </summary>
        public string localcreditamount { get; set; }
        /// <summary>
        /// 本幣借方金額
        /// </summary>
        public string localdebitamount { get; set; }
        /// <summary>
        /// 科目
        /// </summary>
        public string pk_accsubj { get; set; }
        /// <summary>
        /// 幣別編碼
        /// </summary>
        public string pk_currtype { get; set; }
        /// <summary>
        /// 單價
        /// </summary>
        public string price { get; set; }
        public Ass[] ass { get; set; }
        public Cashflow[] cashflow { get; set; }
    }

    public class Ass
    {
        /// <summary>
        /// 輔助核算類型編碼
        /// </summary>
        public string checktypecode { get; set; }
        /// <summary>
        /// 輔助核算值編碼
        /// </summary>
        public string checkvaluecode { get; set; }
    }

    public class Cashflow
    {
        public string cashflow_code { get; set; }
        public string currtype_code { get; set; }
        public int money { get; set; }
    }

整個憑證對接下來,其實坑不是很多,主要在於前期接口文檔的研究,參數的獲取以及測試接口連通性上面.

三.結束語

希望文章能在你開發API接口對接的路上一些幫助和解疑,也希望同樣做API對接的小夥伴,我們可以多多交流。祝你在開發的道路上勇往直前。

我是程序猿貝塔,一個分享自己對接過財務系統API經歷和生活感悟的程序員。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

120行代碼打造.netcore生產力工具-小而美的後台異步組件

相信絕大部分開發者都接觸過用戶註冊的流程,通常情況下大概的流程如下所示:

  1. 接收用戶提交註冊信息
  2. 持久化註冊信息(數據庫+redis)
  3. 發送註冊成功短信(郵件)
  4. 寫操作日誌(可選)

偽代碼如下:

public async Task<IActionResult> Reg([FromBody] User user)
{
    _logger.LogInformation("持久化數據開始");
    await Task.Delay(50);
    _logger.LogInformation("持久化結束");
    _logger.LogInformation("發送短信開始");
    await Task.Delay(100);
    _logger.LogInformation("發送短信結束");
    _logger.LogInformation("操作日誌開始");
    await _logRepository.Insert(new Log { Txt = "註冊日誌" });
    _logger.LogInformation("操作日誌結束");
    return Ok("註冊成功");
}

在以上的代碼中,我使用Task.Delay方法阻塞主線程,用以模擬實際場景中的執行耗時。以上流程應該是包含了絕大部分註冊流程所需要的操作。對於任何開發者來講,以上業務流程沒任何難度,無非是順序的執行各個流程的代碼即可。

稍微有點開發經驗的應該會將以上的流程進行拆分,但有些人可能就要問了,為什麼要拆分呢?拆分之後的代碼應該怎麼寫呢?下面我們就來簡單聊下如此場景的正確打開方式。

首先,註冊成功的依據應該是是否成功的將用戶信息持久化(至於是先持久化到數據庫,異或是先寫到redis不在本篇文章討論的範疇),至於發送註冊短信(郵件)以及寫日誌的操作應該不能成為影響註冊是否成功的因素,而發送短信/郵件等相關操作通常情況下也是比較耗時的,所以在對此接口做性能優化時,可優先考慮將短信/郵件以及寫日誌等相關操作與主流程(持久化數據)拆分,使其不阻塞主流程的執行,從而達到提高響應速度的目的。

知道了為什麼要拆,但具體如何拆分呢?怎樣才能用最少的改動,達到所需的目的呢?

條條大路通羅馬,所以要達成我們的目的也是有很多方案的,具體選擇哪種方案需要根據具體的業務場景,業務體量等多種因素綜合考慮,下面我將一一介紹分析相關方案。

在正式介紹可用方案前,筆者想先介紹一種很多新手容易錯誤使用的一種方案(因為筆者就曾經天真的使用過這種錯誤的方案)。

提到異步,絕大部分.net開發者應該第一想到的就是Task,async,await等,的確,async,await的語法糖簡化了.net開發者異步編程的門檻,減少了很多代碼量。通常一個返回Task類型的方法,在被調用時,會在方法的前面加上await,表示需要等待此方法的執行結果,再繼續執行後面的代碼。但如果不加await時,則不會等待方法的執行結果,進而也不會阻塞主線程。所以,有些人可能就會將發送短信/郵件以及寫日誌的操作如下方式進行改造。

public async Task<IActionResult> Reg1([FromBody] User user)
{
    _logger.LogInformation("持久化數據開始");
    await Task.Delay(50);
    _logger.LogInformation("持久化結束");
    _ = Task.Run(async () =>
     {
         _logger.LogInformation("發送短信開始");
         await Task.Delay(100);
         _logger.LogInformation("發送短信結束");
         _logger.LogInformation("操作日誌開始");
         await _logRepository.Insert(new Log { Txt = "註冊日誌" });
         _logger.LogInformation("操作日誌結束");
     });
    return Ok("註冊成功");
}

然後使用jmeter分別壓測改造前和改造后的接口,結果如下:

有沒有被驚訝到?就這樣一個簡單的改造,吞吐量就提高了三四倍。既然已經提高了三四倍,那為什麼說這是一種錯誤的改造方法嗎?各位看官且往下看。

熟悉.netcore的大佬,應該都知道.netcore的依賴注入的生命周期吧。通常情況下,注入的生命周期包括:Singleton,Scope,Transient。
在以上的流程中,假如寫操作日誌的實例的生命周期是Scope,當在Task中調用Controller獲取到的實例的方法時,因為Task.Run並沒有阻塞主線程,當調用Action return后,當前請求的scope注入的對象會被回收,如果對象被回收之後,Task.Run還未執行完,則會報System.ObjectDisposedException: Cannot access a disposed object. 異常。意思是,不能訪問一個已disposed的對象。正確的做法是使用IServiceScopeFactory創建一個新的作用域,在新的作用域中獲取獲取日誌倉儲服務的實例。這樣就可以避免System.ObjectDisposedException異常了。
改造后的示例代碼如下:

public async Task<IActionResult> Reg1([FromBody] User user)
{
    _logger.LogInformation("持久化數據開始");
    await Task.Delay(50);
    _logger.LogInformation("持久化結束");
    _ = Task.Run(async () =>
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var sp = scope.ServiceProvider;
            var logRepository = sp.GetService<ILogRepository>();
            _logger.LogInformation("發送短信開始");
            await Task.Delay(100);
            _logger.LogInformation("發送短信結束");

            _logger.LogInformation("操作日誌開始");
            await logRepository.Insert(new Log { Txt = "註冊日誌" });
            _logger.LogInformation("操作日誌結束");
        }
    });
    return Ok("註冊成功");
}

雖然得到了正解,但上述的代碼着實有點多,如果一個項目有多個相似的業務場景,就要考慮對CreateScope相關的操作進行封裝。

下面就來一一介紹下筆者覺得實現此業務場景的幾種方案。
1.消息隊列
2.Quartz任務調度組件
3.Hangfire任務調度組件
4.Weshare.TransferJob(推薦)
首先說下消息隊列的方式。準確的說,消息隊列應該是這種場景的最優解決方案,消息隊列的其中一個比較重要的特性就是解耦,從而提高吞吐量。但並不是所有的應用程序都需要上消息隊列。有些業務場景使用消息隊列時,往往會給人一種”殺雞用牛刀”的感覺。

其次Quartz和Hangfire都是任務調度框架,都提供了可實現以上業務場景的邏輯,但Quartz和Hangfire都需要持久化作業數據。雖然Hangfire提供了內存版本,但經過我的測試,發現Hangfire的內存版本特別消耗內存,所以不太推薦使用任務調度框架來實現類似於這樣的業務邏輯。

最後,也就是本文的重點,筆者結合了消息隊列和任務調度的思想,實現了一個輕量級的轉移作業到後台執行的組件。此組件完美的解決了Scope生命周期實例獲取的問題,一行代碼將不需要等待的操作轉移到後台線程執行。
接入步驟如下:
1.使用nuget安裝Weshare.TransferJob
2.在Stratup中注入服務。

services.AddTransferJob();

3.通過構造函數或其他方法獲取到IBackgroundRunService的實例。
4.調用實例的Transfer方法將作業轉移到後台線程。

_backgroundRunService.Transfer(log=>log.Insert(new Log(){Txt = "註冊日誌"}));

就是這麼簡單的實現了這樣的業務場景,不僅簡化了代碼,而且大大提高了系統的吞吐量。

下面再來一起分析下Weshare.TransferJob的核心代碼(畢竟文章要點題)。各位器宇不凡的看官請繼續往下看。
下面的代碼是AddTransferJob方法的實現:

public static IServiceCollection AddTransferJob(this IServiceCollection services)
{
    services.AddSingleton<IBackgroundRunService, BackgroundRunService>();
    services.AddHostedService<TransferJobHostedService>();
    return services;
}

聰明”絕頂”的各位看官應該已經發現上述代碼的關鍵所在。是的, 你沒有看錯,此組件的就是利用.net core提供的HostedService在後台執行被轉移的作業的。
我們再來一起看看TransferJobHostedService的代碼:

public class TransferJobHostedService:BackgroundService
{
    private IBackgroundRunService _runService;
    public TransferJobHostedService(IBackgroundRunService runService)
    {
        _runService = runService;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _runService.Execute(stoppingToken);
        }
    }
}

這個類的代碼也很簡單,重寫了BackgroundService類的ExecuteAsync,循環調用IBackgroundRunService實例的Execute方法。所以,最最關鍵的代碼是IBackgroundRunService的實現類中。
詳細代碼如下:

public class BackgroundRunService : IBackgroundRunService
{
    private readonly SemaphoreSlim _slim;
    private readonly ConcurrentQueue<LambdaExpression> queue;
    private ILogger<BackgroundRunService> _logger;
    private readonly IServiceProvider _serviceProvider;
    public BackgroundRunService(ILogger<BackgroundRunService> logger, IServiceProvider serviceProvider)
    {
        _slim = new SemaphoreSlim(1);
        _logger = logger;
        _serviceProvider = serviceProvider;
        queue = new ConcurrentQueue<LambdaExpression>();
    }
    public async Task Execute(CancellationToken cancellationToken)
    {
        try
        {
            await _slim.WaitAsync(cancellationToken);
            if (queue.TryDequeue(out var job))
            {
                using (var scope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
                {
                    var action = job.Compile();
                    var isTask = action.Method.ReturnType == typeof(Task);
                    var parameters = job.Parameters;
                    var pars = new List<object>();
                    if (parameters.Any())
                    {
                        var type = parameters[0].Type;
                        var param = scope.ServiceProvider.GetRequiredService(type);
                        pars.Add(param);
                    }
                    if (isTask)
                    {
                        await (Task)action.DynamicInvoke(pars.ToArray());
                    }
                    else
                    {
                        action.DynamicInvoke(pars.ToArray());
                    }
                }
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e.ToString());
        }
    }
    public void Transfer<T>(Expression<Func<T, Task>> expression)
    {
        queue.Enqueue(expression);
        _slim.Release();
    }
    public void Transfer(Expression<Action> expression)
    {
        queue.Enqueue(expression);
        _slim.Release();
    }
}

納尼?嫌代碼多看不懂?那咱們一起來剖析下吧。
首先,此類有三個較重要的私有變量,對應的類型分別是SemaphoreSlim, ConcurrentQueue ,IServiceProvider。
其中SemaphoreSlim是為了控制後台作業執行的順序的,在構造函數中初始化了此對象的信號量為1,表示在後台服務的ExecuteAsync方法的循環中每次只能有一個作業執行。
ConcurrentQueue 的對象是用來存儲被轉移到後台服務執行的作業的邏輯,所以使用LambdaExpression作為隊列的類型。
IServiceProvider是為了解決依賴注入的生命周期的。

然後在Execute方法中,第一行代碼如下:

await _slim.WaitAsync(cancellationToken);

作用是等待一個信號量,當沒有可用的信號量時,會阻塞線程的執行,這樣在後台服務的ExecuteAsync方法的死循環就不會一直執行下去,只有獲取到信號量才會繼續執行。
當獲取到信號量后,則說明有新的作業等待執行,所以此時則需要從隊列中讀出要執行的LambdaExpression表達式,創建一個新的Scope后,編譯此表達式樹,判斷返回類型,獲取泛型的具體類型,最後獲取到泛型對應的實例,執行對應的方法。

另外,Transfer方法就是暴露給調用者的方法,用於將表達式樹寫到隊列中,同時釋放信號量。

到此為止,Weshare.TransferJob的實現原理已分析完畢,由於此組件的原理只是將任務轉移到後台進行執行,所以並不是適合對事務有要求的場景。正如本文開頭所假設的場景,TransferJob最適合的場景還是那些和主操作關聯性較低的、失敗或成功並不會影響業務的正常運行。
同時,此組件的定位就是小而美,像延遲執行、定時執行的功能在最初的規劃中其實是有的,後來發現這些功能quartz已經有了,所以沒必要重複造這樣的輪子。
後期會根據使用場景,嘗試加入異常重試機制,以及異常通知回調機制。

最後,不知道有沒有較真的看官想計算下代碼量是否超過120行。
為了證明我不是標題黨,現將此組件進行開源,地址是:
https://github.com/fuluteam/WeShare.TransferJob

橋豆麻袋,筆者辛苦敲的代碼,難道各位看官想白嫖嗎? 點個贊再走唄。點完贊還有力氣的話,如果git上能點個star的話,那也是最好不過的。小生這廂先行謝過。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

心痛!抹香鯨遭漁網纏住痛苦掙扎 潛水員花3天仍割不完

摘錄自2020年7月22日自由時報報導

義大利當地時間18日,一條抹香鯨在利帕里島(Lipari)附近海域被廢棄漁網纏住,義大利海警花了數天時間企圖割開漁網,但由於抹香鯨躁動,導致進度緩慢。

綜合外媒報導,一隻抹香鯨被發現受困於義大利利帕里島海域,牠被廢棄漁網困住無法游離,義大利海警獲報後,即刻出動潛水員救援,他們企圖割開漁網,但由於抹香鯨情緒不穩,相當躁動,令潛水員相當困擾,也因此把牠命為「Fury」(憤怒之意),潛水員花了3天時間才將部分漁網割下。

不料恢復行動力的「Fury」開始下潛失去蹤影,儘管纏在「Fury」身上的大部分漁網已經割開,不過牠的尾巴仍有魚網纏繞住,救難人員正積極尋找其下落。

生物多樣性
海洋
國際新聞
抹香鯨
漁網

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

研究:全球礁鯊數量銳減 部分海域已絕跡

摘錄自2020年7月22日中央社報導

22日發布在科學期刊「自然」(Nature)的一份調查,首度全面了解全球哪裡的礁鯊實際上已滅絕。這份調查為期四年,在近60個國家超過370個暗礁海域進行研究。

加拿大達爾豪希大學(Dalhousie University)副教授麥克尼爾(Aaron MacNeil)表示:「我們原本預期……地球上每個暗礁海域應該都有鯊魚出沒,結果發現我們調查的海域中,有20%沒有任何鯊魚,這令人非常擔心。」

調查顯示,在卡達、印度、越南及肯亞等八個國家的暗礁海域,完全找不到鯊魚。這不代表這些國家的海域沒有鯊魚,但證明當地暗礁海域的鯊魚數量少得可憐。這表示礁鯊在當地生態系統已失去任何角色,也就是牠們已經功能性滅絕。

研究指出,破壞性的捕魚活動最可能是礁鯊數量大減罪魁禍首。「使用流刺網和延繩捕魚,對原本數量相對豐沛的礁鯊帶來最大負面影響。」

生物多樣性
物種保育
土地利用
農林漁牧業
國際新聞
全球
鯊魚
暗礁
滅絕
捕魚
生態系統
流刺網
延繩釣

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

世界經濟論壇:綠色振興將可創造一年300兆元收益

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

印尼帝汶島海灘驚現大鯨魚屍體

摘錄自2020年7月24日法廣印尼報導

印度尼西亞南部帝汶島沙灘附近,發現了一具上百噸重大鯨魚的屍體。當局目前還沒搞懂這頭大型哺乳動物的死因。

當地漁民是在20日早上發現這具長達29米的大鯨魚。海事當局認為這頭海洋生物已有70歲,無生命的漂移,可能已長達數年之久。死因則尚無定論。其屍體上沒有發現任何傷痕,但當地媒體提到了上一個的案例。

2018年,另一頭鯨魚曾擱淺在附近的另個小島Kapota。那頭鯨魚的胃裡發現了眾多塑膠垃圾:115個塑膠杯,還有4個塑膠瓶。這一發現讓人感到震驚,儘管這最終並沒有被確定是造成大鯨魚死亡的原因,但已足以引起科學界的重視。

生物多樣性
海洋
國際新聞
印尼
鯨魚
海洋垃圾

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

漢娜颶風肆虐德州 吹倒美墨邊境圍牆

摘錄自2020年7月27日東森國際新聞美國報導

美國德州(Texas)新冠肺炎疫情持續肆虐,周末又遭颶風漢娜(Hanna)侵襲。漢娜是今年第一個颶風,它已經摧毀許多船隻、淹沒街道、造成電力中斷、甚至吹倒一部分美墨邊境圍牆,災情相當嚴重。

據外媒《WDSU News》報導,國家颶風中心(National Hurricane Center)表示,目前颶風漢娜已降為熱帶低氣壓,以每小時超過50英里的風速橫越美墨邊境,並在德州南部和墨西哥東北部的部分地區降下了超過300毫米的豪雨。

州長格雷格·阿博特 (Greg Abbott)於週六(25日)在一場記者會中表示,「任何颶風都是一場巨大的挑戰,但這次的挑戰非常複雜,必且比以往更加嚴峻,因為這場颶風正在席捲著新冠肺炎的其中一個震央。」

土地利用
國際新聞
美國
颶風
災害

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

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

英國首例 寵物貓確診武漢肺炎

摘錄自2020年7月27日中央社報導

英國廣播公司(BBC)報導,英國出現第一例寵物貓確診2019冠狀病毒疾病(COVID-19,武漢肺炎),專家表示這是英國第一起動物確診病例。衛生官員強調,這起案例非常罕見,民眾不需緊張。

據信是遭到確診的飼主傳染,但這不代表病毒可能透過寵物傳播。目前飼主與貓都已經痊癒。英國首席獸醫官密道米斯(Christine Middlemiss)表示:「沒有證據顯示寵物會直接將病毒傳播給人類,我們將持續密切關注,若情況生變,將告知飼主最新守則。」

英格蘭公共衛生署主任陶艾爾(Yvonne Doyle)建議民眾定期清潔雙手,包括在接觸動物前後。若動物接觸患病的人,牠們的毛髮在短期內可能會攜帶病毒。

國際新聞
英國
武漢肺炎
寵物

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

【Spring註解驅動開發】使用@Import註解給容器中快速導入一個組件

寫在前面

我們可以將一些bean組件交由Spring管理,並且Spring支持單實例bean和多實例bean。我們自己寫的類,可以通過包掃描+標註註解(@Controller、@Servcie、@Repository、@Component)的形式將其註冊到IOC容器中,如果不是我們自己寫的類,比如,我們在項目中引入了一些第三方的類庫,此時,我們需要將這些第三方類庫中的類註冊到Spring容器中,該怎麼辦呢?此時,我們就可以使用@Bean和@Import註解將這些類快速的導入Spring容器中。接下來,我們來一起探討下如何使用@Import註解給容器中快速導入一個組件。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

註冊bean的方式

向Spring容器中註冊bean通常有以下幾種方式:

  • 包掃描+標註註解(@Controller、@Servcie、@Repository、@Component),通常用於自己寫的類。
  • @Bean註解,通常用於導入第三方包中的組件。
  • @Import註解,快速向Spring容器中導入組件。

@Import註解概述

Spring 3.0之前,創建Bean可以通過xml配置文件與掃描特定包下面的類來將類注入到Spring IOC容器內。而在Spring 3.0之後提供了JavaConfig的方式,也就是將IOC容器里Bean的元信息以java代碼的方式進行描述。我們可以通過@Configuration與@Bean這兩個註解配合使用來將原來配置在xml文件里的bean通過java代碼的方式進行描述

@Import註解提供了@Bean註解的功能,同時還有xml配置文件里 標籤組織多個分散的xml文件的功能,當然在這裡是組織多個分散的@Configuration

先看一下@Import註解的源碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    /**
      * {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
      * or regular component classes to import.
      */
     Class<?>[] value();
}

從源碼里可以看出@Import可以配合 Configuration , ImportSelector, ImportBeanDefinitionRegistrar 來使用,下面的or表示也可以把Import當成普通的Bean使用。

@Import只允許放到類上面,不能放到方法上。下面我們來看具體的使用方式。

@Import註解的使用方式

@Import註解的三種用法主要包括:

  • 直接填class數組方式
  • ImportSelector方式【重點】
  • ImportBeanDefinitionRegistrar方式

注意:我們先來看第一種方法:直接填class數組的方式,其他的兩種方式我們後面繼續講。

@Import導入組件的簡單示例

沒有使用@Import註解的效果

首先,我們創建一個Department類,這個類是一個空類,沒有成員變量和方法,如下所示。

package io.mykit.spring.plugins.register.bean;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Import註解的bean
 */
public class Department {
}

接下來,我們先在SpringBeanTest類中創建testAnnotationConfig7()方法,輸出Spring容器中所有的bean,來查看是否存在Department類對應的bean實例,以此來判斷Spring容器中是否註冊有Department類對應的bean實例。

@Test
public void testAnnotationConfig7(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanDefinitionNames();
    Arrays.stream(names).forEach(System.out::println);
}

運行SpringBeanTest類的testAnnotationConfig7()方法,輸出的結果信息如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
person
binghe001

可以看到Spring容器中並沒有Department類對應的bean實例。

使用@Import註解的效果

我們在PersonConfig2類上添加@Import註解,並將Department類標註到註解中,如下所示。

@Configuration
@Import(Department.class)
public class PersonConfig2 {

此時,我們再次運行SpringBeanTest類的testAnnotationConfig7()方法,輸出的結果信息如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
person
binghe001

可以看到,輸出結果中打印了io.mykit.spring.plugins.register.bean.Department,說明使用@Import導入bean時,id默認是組件的全類名。

@Import註解支持同時導入多個類,例如,我們再次創建一個Employee類,如下所示。

package io.mykit.spring.plugins.register.bean;
/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Import註解的bean
 */
public class Employee {
}

接下來,我們也將Employee類添加到@Import註解中,如下所示。

@Configuration
@Import({Department.class, Employee.class})
public class PersonConfig2 {

此時,我們再次運行SpringBeanTest類的testAnnotationConfig7()方法,輸出的結果信息如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
io.mykit.spring.plugins.register.bean.Employee
person
binghe001

可以看到,結果信息中同時輸出了io.mykit.spring.plugins.register.bean.Department和io.mykit.spring.plugins.register.bean.Employee,說明Department類的bean實例和Employee類的bean實例都導入到Spring容器中了。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心