從EFCore上下文的使用到深入剖析DI的生命周期最後實現自動屬性注入

故事背景

最近在把自己的一個老項目從Framework遷移到.Net Core 3.0,數據訪問這塊選擇的是EFCore+Mysql。使用EF的話不可避免要和DbContext打交道,在Core中的常規用法一般是:創建一個XXXContext類繼承自DbContext,實現一個擁有DbContextOptions參數的構造器,在啟動類StartUp中的ConfigureServices方法里調用IServiceCollection的擴展方法AddDbContext,把上下文注入到DI容器中,然後在使用的地方通過構造函數的參數獲取實例。OK,沒任何毛病,官方示例也都是這麼來用的。但是,通過構造函數這種方式來獲取上下文實例其實很不方便,比如在Attribute或者靜態類中,又或者是系統啟動時初始化一些數據,更多的是如下一種場景:

    public class BaseController : Controller
    {
        public BloggingContext _dbContext;
        public BaseController(BloggingContext dbContext)
        {
            _dbContext = dbContext;
        }

        public bool BlogExist(int id)
        {
            return _dbContext.Blogs.Any(x => x.BlogId == id);
        }
    }

    public class BlogsController : BaseController
    {
        public BlogsController(BloggingContext dbContext) : base(dbContext) { }
    }

從上面的代碼可以看到,任何要繼承BaseController的類都要寫一個“多餘”的構造函數,如果參數再多幾個,這將是無法忍受的(就算只有一個參數我也忍受不了)。那麼怎樣才能更優雅的獲取數據庫上下文實例呢,我想到以下幾種辦法。


DbContext從哪來

1、  直接開new

回歸原始,既然要創建實例,沒有比直接new一個更好的辦法了,在Framework中沒有DI的時候也差不多都這麼干。但在EFCore中不同的是,DbContext不再提供無參構造函數,取而代之的是必須傳入一個DbContextOptions類型的參數,這個參數通常是做一些上下文選項配置例如使用什麼類型數據庫連接字符串是多少。

        public BloggingContext(DbContextOptions<BloggingContext> options) : base(options)
        {
        }

默認情況下,我們已經在StartUp中註冊上下文的時候做了配置,DI容器會自動幫我們把options傳進來。如果要手動new一個上下文,那豈不是每次都要自己傳?不行,這太痛苦了。那有沒有辦法不傳這個參數?肯定也是有的。我們可以去掉有參構造函數,然後重寫DbContext中的OnConfiguring方法,在這個方法中做數據庫配置: 

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite("Filename=./efcoredemo.db");
        }

即使是這樣,依然有不夠優雅的地方,那就是連接字符串被硬編碼在代碼中,不能做到從配置文件讀取。反正我忍受不了,只能再尋找其他方案。

2、  從DI容器手動獲取

既然前面已經在啟動類中註冊了上下文,那麼從DI容器中獲取實例肯定是沒問題的。於是我寫了這樣一句測試代碼用來驗證猜想:

    var context = app.ApplicationServices.GetService<BloggingContext>();

不過很遺憾拋出了異常:

報錯信息說的很明確,不能從root provider中獲取這個服務。我從G站下載了DI框架的源碼(地址是),拿報錯信息進行反向追溯,發現異常來自於CallSiteValidator類的ValidateResolution方法:

        public void ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope)
        {
            if (ReferenceEquals(scope, rootScope)
                && _scopedServices.TryGetValue(serviceType, out var scopedService))
            {
                if (serviceType == scopedService)
                {
                    throw new InvalidOperationException(
                        Resources.FormatDirectScopedResolvedFromRootException(serviceType,
                            nameof(ServiceLifetime.Scoped).ToLowerInvariant()));
                }

                throw new InvalidOperationException(
                    Resources.FormatScopedResolvedFromRootException(
                        serviceType,
                        scopedService,
                        nameof(ServiceLifetime.Scoped).ToLowerInvariant()));
            }
        }

View Code

繼續往上,看到了GetService方法的實現:

        internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
        {
            if (_disposed)
            {
                ThrowHelper.ThrowObjectDisposedException();
            }

            var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
            _callback?.OnResolve(serviceType, serviceProviderEngineScope);
            DependencyInjectionEventSource.Log.ServiceResolved(serviceType);
            return realizedService.Invoke(serviceProviderEngineScope);
        }

View Code

可以看到,_callback在為空的情況下是不會做驗證的,於是猜想有參數能對它進行配置。把追溯對象換成_callback繼續往上翻,在DI框架的核心類ServiceProvider中找到如下方法:

        internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
        {
            IServiceProviderEngineCallback callback = null;
            if (options.ValidateScopes)
            {
                callback = this;
                _callSiteValidator = new CallSiteValidator();
            }
            //省略....
        }    

說明我的猜想沒錯,驗證是受ValidateScopes控制的。這樣來看,把ValidateScopes設置成False就可以解決了,這也是網上普遍的解決方案:

      .UseDefaultServiceProvider(options =>
       {
              options.ValidateScopes = false;
       })

但這樣做是極其危險的。

為什麼危險?到底什麼是root provider?那就要從原生DI的生命周期說起。我們知道,DI容器被封裝成一個IServiceProvider對象,服務都是從這裏來獲取。不過這並不是一個單一對象,它是具有層級結構的,最頂層的即前面提到的root provider,可以理解為僅屬於系統層面的DI控制中心。在Asp.Net Core中,內置的DI有3種服務模式,分別是SingletonTransientScoped,Singleton服務實例是保存在root provider中的,所以它才能做到全局單例。相對應的Scoped,是保存在某一個provider中的,它能保證在這個provider中是單例的,而Transient服務則是隨時需要隨時創建,用完就丟棄。由此可知,除非是在root provider中獲取一個單例服務,否則必須要指定一個服務範圍(Scope),這個驗證是通過ServiceProviderOptionsValidateScopes來控制的。默認情況下,Asp.Net Core框架在創建HostBuilder的時候會判定當前是否開發環境,在開發環境下會開啟這個驗證:

所以前面那種關閉驗證的方式是錯誤的。這是因為,root provider只有一個,如果恰好有某個singleton服務引用了一個scope服務,這會導致這個scope服務也變成singleton,仔細看一下註冊DbContext的擴展方法,它實際上提供的是scope服務:

如果發生這種情況,數據庫連接會一直得不到釋放,至於有什麼後果大家應該都明白。

所以前面的測試代碼應該這樣寫:

     using (var serviceScope = app.ApplicationServices.CreateScope())
     {
         var context = serviceScope.ServiceProvider.GetService<BloggingContext>();
     }

與之相關的還有一個ValidateOnBuild屬性,也就是說在構建IServiceProvider的時候就會做驗證,從源碼中也能體現出來:

            if (options.ValidateOnBuild)
            {
                List<Exception> exceptions = null;
                foreach (var serviceDescriptor in serviceDescriptors)
                {
                    try
                    {
                        _engine.ValidateService(serviceDescriptor);
                    }
                    catch (Exception e)
                    {
                        exceptions = exceptions ?? new List<Exception>();
                        exceptions.Add(e);
                    }
                }

                if (exceptions != null)
                {
                    throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray());
                }
            }

View Code

正因為如此,Asp.Net Core在設計的時候為每個請求創建獨立的Scope,這個Scope的provider被封裝在HttpContext.RequestServices中。

 [小插曲]

通過代碼提示可以看到,IServiceProvider提供了2種獲取service的方式:

這2個有什麼區別呢?分別查看各自的方法摘要可以看到,通過GetService獲取一個沒有註冊的服務時會返回null,而GetRequiredService會拋出一個InvalidOperationException,僅此而已。

        // 返回結果:
        //     A service object of type T or null if there is no such service.
        public static T GetService<T>(this IServiceProvider provider);

        // 返回結果:
        //     A service object of type T.
        //
        // 異常:
        //   T:System.InvalidOperationException:
        //     There is no service of type T.
        public static T GetRequiredService<T>(this IServiceProvider provider);

 

終極大招

到現在為止,儘管找到了一種看起來合理的方案,但還是不夠優雅,使用過其他第三方DI框架的朋友應該知道,屬性注入的快感無可比擬。那原生DI有沒有實現這個功能呢,我滿心歡喜上G站搜Issue,看到這樣一個回復():

官方明確表示沒有開發屬性注入的計劃,沒辦法,只能靠自己了。

我的思路大概是:創建一個自定義標籤(Attribute),用來給需要注入的屬性打標籤,然後寫一個服務激活類,用來解析給定實例需要注入的屬性並賦值,在某個類型被創建實例的時候也就是構造函數中調用這個激活方法實現屬性注入。這裡有個核心點要注意的是,從DI容器獲取實例的時候一定要保證是和當前請求是同一個Scope,也就是說,必須要從當前的HttpContext中拿到這個IServiceProvider

先創建一個自定義標籤:

    [AttributeUsage(AttributeTargets.Property)]
    public class AutowiredAttribute : Attribute
    {

    }

解析屬性的方法:

        public void PropertyActivate(object service, IServiceProvider provider)
        {
            var serviceType = service.GetType();
            var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_"));
            foreach (PropertyInfo property in properties)
            {
                var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
                if (autowiredAttr != null)
                {
                    //從DI容器獲取實例
                    var innerService = provider.GetService(property.PropertyType);
                    if (innerService != null)
                    {
                        //遞歸解決服務嵌套問題
                        PropertyActivate(innerService, provider);
                        //屬性賦值
                        property.SetValue(service, innerService);
                    }
                }
            }
        }

然後在控制器中激活屬性:

        [Autowired]
        public IAccountService _accountService { get; set; }

        public LoginController(IHttpContextAccessor httpContextAccessor)
        {
            var pro = new AutowiredServiceProvider();
            pro.PropertyActivate(this, httpContextAccessor.HttpContext.RequestServices);
        }

這樣子下來,雖然功能實現了,但是裏面存着幾個問題。第一個是由於控制器的構造函數中不能直接使用ControllerBaseHttpContext屬性,所以必須要通過注入IHttpContextAccessor對象來獲取,貌似問題又回到原點。第二個是每個構造函數中都要寫這麼一堆代碼,不能忍。於是想有沒有辦法在控制器被激活的時候做一些操作?沒考慮引入AOP框架,感覺為了這一個功能引入AOP有點重。經過網上搜索,發現Asp.Net Core框架激活控制器是通過IControllerActivator接口實現的,它的默認實現是DefaultControllerActivator):

       /// <inheritdoc />
        public object Create(ControllerContext controllerContext)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException(nameof(controllerContext));
            }

            if (controllerContext.ActionDescriptor == null)
            {
                throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(ControllerContext.ActionDescriptor),
                    nameof(ControllerContext)));
            }

            var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;

            if (controllerTypeInfo == null)
            {
                throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(controllerContext.ActionDescriptor.ControllerTypeInfo),
                    nameof(ControllerContext.ActionDescriptor)));
            }

            var serviceProvider = controllerContext.HttpContext.RequestServices;
            return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
        }

View Code

這樣一來,我自己實現一個Controller激活器不就可以接管控制器激活了,於是有如下這個類:

    public class HosControllerActivator : IControllerActivator
    {
        public object Create(ControllerContext actionContext)
        {
            var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType();
            var instance = actionContext.HttpContext.RequestServices.GetRequiredService(controllerType);
            PropertyActivate(instance, actionContext.HttpContext.RequestServices);
            return instance;
        }

        public virtual void Release(ControllerContext context, object controller)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            if (controller == null)
            {
                throw new ArgumentNullException(nameof(controller));
            }
            if (controller is IDisposable disposable)
            {
                disposable.Dispose();
            }
        }

        private void PropertyActivate(object service, IServiceProvider provider)
        {
            var serviceType = service.GetType();
            var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_"));
            foreach (PropertyInfo property in properties)
            {
                var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
                if (autowiredAttr != null)
                {
                    //從DI容器獲取實例
                    var innerService = provider.GetService(property.PropertyType);
                    if (innerService != null)
                    {
                        //遞歸解決服務嵌套問題
                        PropertyActivate(innerService, provider);
                        //屬性賦值
                        property.SetValue(service, innerService);
                    }
                }
            }
        }
    }

View Code

需要注意的是,DefaultControllerActivator中的控制器實例是從TypeActivatorCache獲取的,而自己的激活器是從DI獲取的,所以必須額外把系統所有控制器註冊到DI中,封裝成如下的擴展方法:

        /// <summary>
        /// 自定義控制器激活,並手動註冊所有控制器
        /// </summary>
        /// <param name="services"></param>
        /// <param name="obj"></param>
        public static void AddHosControllers(this IServiceCollection services, object obj)
        {
            services.Replace(ServiceDescriptor.Transient<IControllerActivator, HosControllerActivator>());
            var assembly = obj.GetType().GetTypeInfo().Assembly;
            var manager = new ApplicationPartManager();
            manager.ApplicationParts.Add(new AssemblyPart(assembly));
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
            var feature = new ControllerFeature();
            manager.PopulateFeature(feature);
            feature.Controllers.Select(ti => ti.AsType()).ToList().ForEach(t =>
            {
                services.AddTransient(t);
            });
        }

View Code

ConfigureServices中調用:

services.AddHosControllers(this);

到此,大功告成!可以愉快的繼續CRUD了。

 

結尾

市面上好用的DI框架一堆一堆的,集成到Core裏面也很簡單,為啥還要這麼折騰?沒辦法,這不就是造輪子的樂趣嘛。上面這些東西從頭到尾也折騰了不少時間,屬性注入那裡也還有優化的空間,歡迎探討。

推薦閱讀:

 

 

 

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

工信部:投入5億元組建動力電池研發平臺

工信部部長苗圩25日表示,工信部已聯合行業內外的九家企業投入5億元資本金,正組建動力電池研究院,或者動力電池的研究研發平臺。

日前召開的國務院常務會議提出,要加快實現動力電池革命性突破。中國汽車工業協會常務副會長董揚認為,這是“補短板”之舉,“對中國來說,要逐步達到世界平均水準,需要材料、裝備、生產、市場等各方協同發展”。

據瞭解,目前我國新能源汽車累計產量接近50萬輛,但車用動力電池與一些發達國家有相隔一代的差距。除比亞迪外,其他生產新能源汽車的企業都沒有電池研發。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

英國建成總長1099公里電動汽車快速充電網 30分鐘充電80%

日前,一條總長為1099公里的電動汽車快速充電網在英國建成,為電動汽車的推廣創造更大的便利。

據悉,該充電網路擁有74個快速充電網站,僅需30分鐘就可以為電動汽車充電 80%。該網路的建立給車主提供了更多選擇,讓長途駕駛更加便捷。此外,網站在多個重點交通樞紐建立,電動車駕駛員可以順利過境。據紐卡斯爾大學研究表明,72%的電動汽車駕駛員會使用快速充電站。據悉,該網路覆蓋了英國大部分地區。

據悉,該專案總投資為580萬英鎊,由歐盟和日產、寶馬、雷諾以及大眾這四家汽車製造商出資籌建。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

2016 第二屆亞太新能源汽車國際峰會 聚焦智慧網聯電動交通

本次由中國汽車工程研究院支援並由上海領研商務諮詢有限公司主辦的 “2016第二屆亞太新能源汽車國際峰會”將於2016年6月2日(星期四)至3日(星期五)在北京召開。

隨著全球能源危機的加重及汽車排放引起的環境問題日益得到世界各國的重視,智慧化,網聯化和電動化已成為未來全球汽車行業發展的必然趨勢。汽車行業是能源、資源消耗較大的產業,同時汽車排放也是環境污染的重要原因。中國已經步入汽車生產和消費大國,節能、環保、低碳是中國汽車產業面臨的長期任務。

據中汽協資料顯示,2015年新能源汽車生產34.04萬輛,銷售33.1萬輛,同比分別增長3.3倍、3.4倍。根據機動車整車出廠合格證統計,2016年1月,中國新能源汽車生產1.61萬輛,同比增長144%。根據全國乘用車市場訊息聯席會剛發佈的統計資料,2016年1月新能源乘用車銷量達13748輛,較之2015年1月同比增長1.8倍。

2015年11月18日,中國國家發改委發佈了《電動汽車充電基礎設施發展指南(2015-2020年)》 ,明確提出到2020年,中國將新增集中式充換電站1.2萬座,分散式充電樁480萬個,以滿足全國500萬輛電動汽車充電需求。今年5月19日中國務院發佈的《中國製造2025》中提出將“節能與新能源汽車”作為重點發展領域,明確了繼續支持電動汽車、燃料電池汽車發展,掌握汽車低碳化、資訊化、智慧化核心技術等的發展戰略。

面臨重大發展機遇的同時,新能源汽車的大規模商業化仍然還存在一些挑戰和瓶頸,如價格偏高、性能不完善、充電基礎設施不完善、充電標準不統一、關鍵零部件技術尚未成熟等等。

在此背景下,2016第二屆亞太新能源汽車國際峰會以 “助力智慧網聯電動交通”為主題,旨在為全球新能源汽車參與者打造一個瞭解中國新能源汽車發展現狀,商業模式和技術發展趨勢的前瞻性產業交流平臺。   

相信本屆在北京召開的”2016第二屆亞太新能源汽車國際峰會”構建的交流平臺和溝通管道,對於借鑒先進理念、整合政策資源、利用各方智慧,推進新能源汽車產業發展、節能減排等各項工作將會產生深遠影響和積極作用,並為中國綠色交通產業起到巨大的推動作用。

期待您的支持和參與。

更多關於2016ANEVS的資訊,請登錄會議官方網站:

組委會聯繫方式:
連絡人:Fiona Hu           
電  話:+86 21-58560121
郵  箱:

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

結合參數接收響應轉換原理講解SpringBoot常用註解

一、常用註解回顧

1.1 @RequestBody與@ResponseBody


//注意並不要求@RequestBody與@ResponseBody成對使用。
public @ResponseBody  AjaxResponse saveArticle(@RequestBody ArticleVO article)

如上代碼所示:

  • @RequestBody修飾請求參數,註解用於接收HTTP的body,默認是使用JSON的格式
  • @ResponseBody修飾返回值,註解用於在HTTP的body中攜帶響應數據,默認是使用JSON的格式。如果不加該註解,spring響應字符串類型,是跳轉到模板頁面或jsp頁面的開發模式。說白了:加上這個註解你開發的是一個數據接口,不加這個註解你開發的是一個頁面跳轉控制器。

那麼我們有一個問題:如果我們想接收或XML數據該怎麼辦?我們想響應excel的數據格式該怎麼辦?我們後文來回答這個問題。

1.2. @RequestMapping註解

@RequestMapping註解是所有常用註解中,最有看點的一個註解,用於標註HTTP服務端點。它的很多屬性對於豐富我們的應用開發方式方法,都有很重要的作用。如:

  • value: 應用請求端點,最核心的屬性,用於標誌請求處理方法的唯一性;
  • method: HTTP協議的method類型, 如:GET、POST、PUT、DELETE等;
  • consumes: HTTP協議請求內容的數據類型(Content-Type),例如application/json, text/html;
  • produces: HTTP協議響應內容的數據類型。下文會詳細講解。
  • params: HTTP請求中必須包含某些參數值的時候,才允許被註解標註的方法處理請求。
  • headers: HTTP請求中必須包含某些指定的header值,才允許被註解標註的方法處理請求。

@RequestMapping(value = "/article", method = POST)
@PostMapping(value = "/article")

上面代碼中兩種寫法起到的是一樣的效果,也就是PostMapping等同於@RequestMapping的method等於POST。同理:@GetMapping、@PutMapping、@DeleteMapping也都是簡寫的方式。

1.3. @RestController與@Controller

@Controller註解是開發中最常使用的註解,它的作用有兩層含義:

  • 一是告訴Spring,被該註解標註的類是一個Spring的Bean,需要被注入到Spring的上下文環境中。
  • 二是該類裏面所有被RequestMapping標註的註解都是HTTP服務端點。

@RestController相當於 @Controller和@ResponseBody結合。它有兩層含義:

  • 一是作為Controller的作用,將控制器類注入到Spring上下文環境,該類RequestMapping標註方法為HTTP服務端點。
  • 二是作為ResponseBody的作用,請求響應默認使用的序列化方式是JSON,而不是跳轉到jsp或模板頁面。

1.4. @PathVariable 與@RequestParam

PathVariable用於URI上的{參數},如下方法用於刪除一篇文章,其中id為文章id。如:我們的請求URL為“/article/1”,那麼將匹配DeleteMapping並且PathVariable接收參數id=1。而RequestParam用於接收普通表單方式或者ajax模擬表單提交的參數數據。

@DeleteMapping("/article/{id}")
public @ResponseBody AjaxResponse deleteArticle(@PathVariable Long id) {

@PostMapping("/article")
public @ResponseBody AjaxResponse deleteArticle(@RequestParam Long id) {

二、接收複雜嵌套對象參數

有一些朋友可能還無法理解RequestBody註解存在的真正意義,表單數據提交用RequestParam就好了,為什麼還要搞出來一個RequestBody註解呢?RequestBody註解的真正意義在於能夠使用對象或者嵌套對象接收前端數據。

仔細看上面的代碼,是一個paramData對象裡面包含了一個bestFriend對象。這種數據結構使用RequestParam就無法接收了,RequestParam只能接收平面的、一對一的參數。像上文中這種數據結構的參數,就需要我們在java服務端定義兩個類,一個類是ParamData,一個類是BestFriend.

public class ParamData {
    private String name;
    private int id;
    private String phone;
    private BestFriend bestFriend;
    
    public static class BestFriend {
        private String address;
        private String sex;
    }
}
  • 注意上面代碼中省略了GET、SET方法等必要的java plain model元素。
  • 注意成員變量名稱一定要和JSON屬性名稱對應上。
  • 注意接收不同類型的參數,使用不同的成員變量類型

完成以上動作,我們就可以使用@RequestBody ParamData paramData,一次性的接收以上所有的複雜嵌套對象參數了,參數對象的所有屬性都將被賦值。

三、Http數據轉換的原理

大家現在使用JSON都比較普遍了,其方便易用、表達能力強,是絕大部分數據接口式應用的首選。那麼如何響應其他的類型的數據?其中的判別原理又是什麼?下面就來給大家介紹一下:

  • 當一個HTTP請求到達時是一個InputStream,通過HttpMessageConverter轉換為java對象,從而進行參數接收。
  • 當對一個HTTP請求進行響應時,我們首先輸出的是一個java對象,然後由HttpMessageConverter轉換為OutputStream輸出。

當我們在Spring Boot應用中集成了jackson的類庫之後,如下的一些HttpMessageConverter將會被加載。

實現類 功能說明
StringHttpMessageConverter 將請求信息轉為字符串
FormHttpMessageConverter 將表單數據讀取到MultiValueMap中
XmlAwareFormHttpMessageConverter 擴展與FormHttpMessageConverter,如果部分表單屬性是XML數據,可用該轉換器進行讀取
ResourceHttpMessageConverter 讀寫org.springframework.core.io.Resource對象
BufferedImageHttpMessageConverter 讀寫BufferedImage對象
ByteArrayHttpMessageConverter 讀寫二進制數據
SourceHttpMessageConverter 讀寫java.xml.transform.Source類型的對象
MarshallingHttpMessageConverter 通過Spring的org.springframework,xml.Marshaller和Unmarshaller讀寫XML消息
Jaxb2RootElementHttpMessageConverter 通過JAXB2讀寫XML消息,將請求消息轉換為標註的XmlRootElement和XmlType連接的類中
MappingJacksonHttpMessageConverter 利用Jackson開源包的ObjectMapper讀寫JSON數據
RssChannelHttpMessageConverter 讀寫RSS種子消息
AtomFeedHttpMessageConverter 和RssChannelHttpMessageConverter能夠讀寫RSS種子消息

根據HTTP協議的Accept和Content-Type屬性,以及參數數據類型來判別使用哪一種HttpMessageConverter。當使用RequestBody或ResponseBody時,再結合前端發送的Accept數據類型,會自動判定優先使用MappingJacksonHttpMessageConverter作為數據轉換器。但是,不僅JSON可以表達對象數據類型,XML也可以。如果我們希望使用XML格式該怎麼告知Spring呢,那就要使用到produces屬性了。

@GetMapping(value ="/demo",produces = MediaType.APPLICATION_XML_VALUE)

這裏我們明確的告知了返回的數據類型是xml,就會使用Jaxb2RootElementHttpMessageConverter作為默認的數據轉換器。當然實現XML數據響應比JSON還會更複雜一些,還需要結合@XmlRootElement、@XmlElement等註解實體類來使用。同理consumes屬性你是不是也會用了呢。

四、自定義HttpMessageConverter

其實絕大多數的數據格式都不需要我們自定義HttpMessageConverter,都有第三方類庫可以幫助我們實現(包括下文代碼中的Excel格式)。但有的時候,有些數據的輸出格式並沒有類似於Jackson這種類庫幫助我們處理,需要我們自定義數據格式。該怎麼做?下面代碼只是幫助我們理解的一個例子,不要用於生產:

@Service
public class TeamToXlsConverter extends AbstractHttpMessageConverter<Team> {

    private static final MediaType EXCEL_TYPE = MediaType.valueOf("application/vnd.ms-excel");

    TeamToXlsConverter() {
        super(EXCEL_TYPE);
    }

    @Override
    protected Team readInternal(final Class<? extends Team> clazz, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    protected boolean supports(final Class<?> clazz) {
        return (Team.class == clazz);
    }

    @Override
    protected void writeInternal(final Team team, final HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        try (final Workbook workbook = new HSSFWorkbook()) {
            final Sheet sheet = workbook.createSheet();
            int rowNo = 0;
            for (final TeamMember member : team.getMembers()) {
                final Row row = sheet.createRow(rowNo++);
                row.createCell(0)
                   .setCellValue(member.getName());
            }
            workbook.write(outputMessage.getBody());
        }
    }
}
  • 實現AbstractHttpMessageConverter接口
  • 指定該轉換器是針對哪種數據格式的?如上文代碼中的”application/vnd.ms-excel”
  • 指定該轉換器針對那些對象數據類型?如上文代碼中的supports函數
  • 使用writeInternal對數據進行輸出處理,上例中是輸出為Excel格式。

    期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

[ASP.NET Core 3框架揭秘] 文件系統[1]:抽象的“文件系統”

ASP.NET Core應用 具有很多讀取文件的場景,比如配置文件、靜態Web資源文件(比如CSS、JavaScript和圖片文件等)以及MVC應用的View文件,甚至是直接編譯到程序集中的內嵌資源文件。這些文件的讀取都需要使用到一個IFileProvider對象。IFileProvider對象構建了一個抽象的文件系統,我們不僅可以利用它提供的統一API來讀取各種類型的文件,還能及時監控目標文件的變化。

一、樹形層次結構

IFileProvider對象為我們構建了一個具有層次化目錄結構的文件系統。由於IFileProvider是一個接口,所以由它構建的是一個抽象化的文件系統,這裏所謂的目錄和文件都是一個抽象的概念。具體的文件可能對應一個物理文件,也可能保存在數據庫中,或者來源於網絡,甚至有可能根本就不存在,其內容需要在讀取時動態生成。目錄也僅僅是組織文件的邏輯容器。為了讓讀者朋友們對這個文件系統有一個大體認識,我們先來演示幾個簡單的實例。

文件系統管理的所有文件以目錄的形式進行組織,一個IFileProvider對象可以視為針對一個根目錄的映射。目錄除了可以存放文件之外,還可以包含子目錄,所以目錄/文件在整體上呈現出樹形化層次化結構。接下來我們將一個IFileProvider對象映射到一個物理目錄,並利用它將所在目錄的結構呈現出來。

我們演示實例是一個普通的控制台程序。我們在演示實例中定義了如下一個IFileManager接口,它利用一個唯一的ShowStructure方法將文件系統的整體結構显示出來。該方法具有一個類型為Action<int, string>的參數負責將文件系統的節點(目錄或者文件)名稱呈現出來。這個Action<int, string>對象的兩個參數分別代表縮進的層級和目錄/文件的名稱。

public interface IFileManager
{
    void ShowStructure(Action<int, string> render);
}

我們定義如下這個FileManager類作為對IFileManager接口的默認實現,它利用只讀_fileProvider字段表示的IFileProvider對象來提取目錄結構。目標文件系統的整體結構通過Render方法以遞歸的方式呈現出來,其中涉及到對IFileProvider對象的GetDirectoryContents方法的調用。該方法返回一個IDirectoryContents對象表示指定目錄的內容,如果對應的目錄存在,我們可以遍歷該對象得到它的子目錄和文件。目錄和文件最終體現為一個IFileInfo對象來,至於IFileInfo對象對應的就是一個目錄還是一個文件,則通過其IsDirectory屬性來區分。

public class FileManager : IFileManager
{
    private readonly IFileProvider _fileProvider;
    public FileManager(IFileProvider fileProvider) => _fileProvider = fileProvider;
    public void ShowStructure(Action<int, string> render)
    {
        int indent = -1;
        Render("");
        void Render(string subPath)
        {
            indent++;
            foreach (var fileInfo in _fileProvider.GetDirectoryContents(subPath))
            {
                render(indent, fileInfo.Name);
                if (fileInfo.IsDirectory)
                {
                    Render($@"{subPath}\{fileInfo.Name}".TrimStart('\\'));
                }
            }
            indent--;
        }
    }        
}

接下來我們構建一個本地物理目錄“c:\test\”,並按照如下圖所示的結構在它下面創建相應的子目錄和文件。我們會將這個目錄映射到一個IFileProvider對象上,並進一步利用它創建出上面這個FileManager對象。我們最終調用這個FileManager對象的ShowStructure方法將目錄結構呈現出來。

整個演示程序體現在如下的代碼片段中。我們針對目錄“c:\test\”創建了一個表示物理文件系統的PhysicalFileProvider對象,並將其註冊到創建的ServiceCollection對象上。除此之外,ServiceCollection對象上還添加了針對IFileManager/FileManager的服務註冊。

class Program
{
    static void Main()
    {
        static void Print(int layer, string name)  => Console.WriteLine($"{new string(' ', layer * 4)}{name}");        
        new ServiceCollection()
            .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test"))
            .AddSingleton<IFileManager, FileManager>()
            .BuildServiceProvider()
            .GetRequiredService<IFileManager>()
            .ShowStructure(Print);
    }
}

我們最終利用ServiceCollection生成的IServiceProvider對象得到FileManager對象,並調用該對象的ShowStructure方法將PhysicalFileProvider對象映射的目錄結構呈現出來。當我們運行該程序之後,控制台上將呈現出如下圖所示的輸出結果,該結果為我們展示了映射物理目錄的真實結構。(S501)

二、讀取文件內容

前面我們演示了如何利用IFileProvider對象將文件系統的結構完整地呈現出來,接下來我們來演示如何利用它來讀取一個物理文件的內容。我們為IFileManager定義如下一個ReadAllTextAsync方法以異步的方式讀取指定文件內容,方法的參數表示文件的路徑。如下面的代碼片段所示,ReadAllTextAsync方法將指定的文件路徑作為參數調用IFileProvider對象的GetFileInfo方法得到一個IFileInfo對象。我們最終調用這個IFileInfo對象的CreateReadStream方法得到讀取文件的輸出流,進而得到文件的真實內容。

public interface IFileManager
{
    ...
    Task<string> ReadAllTextAsync(string path);
}

public class FileManager : IFileManager
{
    ...
    public async Task<string> ReadAllTextAsync(string path)
    {
        byte[] buffer;
        using (var stream = _fileProvider.GetFileInfo(path).CreateReadStream())
        {
            buffer = new byte[stream.Length];
            await stream.ReadAsync(buffer, 0, buffer.Length);
        }
        return Encoding.Default.GetString(buffer);
    }
}

假設我們依然將FileManager使用的IFileProvider映射為目錄“c:\test\”,現在我們在該目錄中創建一個名為data.txt的文本文件,並在該文件中任意寫入一些內容。接下來我們在Main方法中編寫了如下的程序利用依賴注入的方式得到FileManager對象,並讀取文件data.txt的內容。最終的調試斷言旨在確定通過IFileProvider讀取的確實就是目標文件的真實內容。(S502)

class Program
{
    static async Task Main()
    {
        var content = await new ServiceCollection()
            .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test"))
            .AddSingleton<IFileManager, FileManager>()
            .BuildServiceProvider()
            .GetRequiredService<IFileManager>()
            .ReadAllTextAsync("data.txt");

        Debug.Assert(content == File.ReadAllText(@"c:\test\data.txt"));
    }
}

三、內嵌文件系統

我們一直在強調由IFileProvider結構構建的是一個抽象的具有目錄結構的文件系統,具體文件的提供方式取決於對具體的IFileProvider對象是怎樣一個類型。我們演示實例定義的FileManager並沒有限定具體使用何種類型的IFileProvider,該對象是在應用中通過依賴注入的方式指定的。由於上面的應用程序注入的是一個PhysicalFileProvider對象,所以我們可以利用它來讀取對應物理目錄下的某個文件。假設現在將這個data.txt直接以資源文件的形式編譯到程序集中,我們就需要使用另一個名為EmbeddedFileProvider的實現類型。現在我們直接將這個data.txt文件添加到控制台應用的項目根目錄下。在默認的情況下,當我們編譯項目的時候這樣的文件並不能成為內嵌到目標程序集的資源文件,我們需要利用VS將該文件的“Build Action”屬性按照如下所示的方式設置為“Embedded resource”。

上圖所示的設置將會體現在項目文件(.csproj文件)上。具體來說,項目文件會以如下的形式添加一個<EmbeddedResource>元素將文件data.txt設置為內嵌到編譯後生成的程序集的內嵌資源文件。

<Project Sdk="Microsoft.NET.Sdk">
  ...
  <ItemGroup>
      <EmbeddedResource Include="data.txt"/>   
  </ItemGroup>
</Project>

我們編寫了如下的程序來演示針對內嵌於程序集中的資源文件的讀取。我們首先得到當前入口程序集,並利用它創建了一個EmbeddedFileProvider對象,它代替原來的PhysicalFileProvider對象被註冊到ServiceCollection之中。我們接下來採用了完全一致的編程方式得到FileManager對象並利用它讀取內嵌文件data.txt的內容。為了驗證讀取的目標文件準確無誤,我們採用直接讀取資源文件的方式得到了內嵌文件data.txt的內容,並利用一個調試斷言確定兩者的一致性。(S503)

class Program
{
    static async Task Main()
    {
        var assembly = Assembly.GetEntryAssembly();

        var content1 = await new ServiceCollection()
            .AddSingleton<IFileProvider>(new EmbeddedFileProvider(assembly))
            .AddSingleton<IFileManager, FileManager>()
            .BuildServiceProvider()
            .GetRequiredService<IFileManager>()
            .ReadAllTextAsync("data.txt");

        var stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.data.txt");
        var buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
        var content2 = Encoding.Default.GetString(buffer);

        Debug.Assert(content1 == content2);
    }
}

四、監控文件的變化

在文件讀取場景中,確定加載到內存中的數據與源文件的一致性並自動同步是一個很常見的需求。比如說我們將配置定義在一個JSON文件中,應用啟動的時候會讀取該文件並將其轉換成對應的Options對象。在很多情況下,如果我們改動了配置文件, 最新的配置數據只有在應用重啟之後才能生效。如果我們能夠以一種高效的方式對配置文件進行監控,並在其發生改變的情況下嚮應用發送通知,那麼應用就能在不用重啟的情況下重新讀取配置文件,進而實現Options對象承載的內容和原始配置文件完全同步。

對文件系統實施監控並在其發生改變時發送通知也是IFileProvider對象提供的核心功能之一。接下來我們依然使用前面這個程序來演示如何使用PhysicalFileProvider對某個物理文件實施監控,並在目標文件的內容發生改變的時候重新讀取新的內容。

class Program
{
    static async Task Main()
    {
        using (var fileProvider = new PhysicalFileProvider(@"c:\test"))
        {
            string original = null;
            ChangeToken.OnChange(() => fileProvider.Watch("data.txt"), Callback);
            while (true)
            {
                File.WriteAllText(@"c:\test\data.txt", DateTime.Now.ToString());
                await Task.Delay(5000);
            }

            async void Callback()
            {
                var stream = fileProvider.GetFileInfo("data.txt").CreateReadStream();
                {
                    var buffer = new byte[stream.Length];
                    await stream.ReadAsync(buffer, 0, buffer.Length);
                    string current = Encoding.Default.GetString(buffer);
                    if (current != original)
                    {
                        Console.WriteLine(original = current);
                    }
                }
            }
        }
    }
}

如上面的代碼片段所示,我們針對目錄“c:\test”創建了一個PhysicalFileProvider對象,並調用其Watch方法對指定的文件data.txt實施監控。該方法的返回一個IChangeToken對象,我們正是利用這個對象接收文件改變的通知。我們調用ChangeToken的靜態方法OnChange針對這個對象註冊了一個回調實現對源文件的重新讀取和显示,當源文件發生改變的時候,註冊的回調會自動執行。我們以每隔5秒的間隔對文件data.txt作一次修改,而文件的內容為當前時間。所以當我們的程序啟動之後,每隔5秒鐘當前時間就會以如下圖的方式呈現在控制台上。

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

疫情衝擊助減碳 學者籲國際合作

摘錄自2020年3月8日台灣醒報報導

新冠肺炎(COVID-19,武漢肺炎)肆虐全球,但長期來看有助於對抗氣候變遷?美國外交關係協會主任傑飛指出,諸如在家工作、視訊會議或上下班錯開等防疫行為,均可以減緩氣候變遷。此外,為了因應全球供應鏈斷裂,企業或許考慮將生產「在地化」,此舉也可避免受到氣候變遷的衝擊。

不過,赫爾辛基大學能源和清潔空氣中心的梅利維爾擔心,當疫情過去後工商業復甦又恢復到以往的排碳量。以中國為例,2008年金融海嘯一度造成排碳量大減,但之後政府大興土木以刺激經濟,使得排碳量不減反增。

佛蒙特大學岡德環境研究所埃里克森指出,與其在面對危機時被迫減碳,我們還有5至10年的時間,將經濟轉型成低碳產業,同時令經濟衝擊降到最低。對此,埃里克森認為,對抗氣候變遷需要與抗疫相同程度的國際合作。

生活環境
公共衛生
全球變遷
國際新聞
武漢肺炎
減碳

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

企鵝出沒澳南島嶼 不時迷路跑到本土

摘錄自2020年3月10日公視報導

南半球的澳洲靠近南極洲附近的島嶼,經常會有野生企鵝出沒,有些企鵝迷路,就會不小心跑到澳洲本土的海灘上,吸引民眾圍觀。野生動物專家呼籲,千萬不要餵食企鵝,否則可能有害健康,也會妨礙牠們回歸大自然。

澳洲南部靠近南極洲的島嶼,總共有八種企鵝出沒,專家近年來發現經常有迷路的企鵝,跑到不該出現的澳洲本土岸邊。在伯斯附近的瑪格麗特河小鎮,就有一處企鵝保育基地,專門收容這些需要幫助的企鵝。

專家說,一般民眾如果發現落單或迷路的企鵝,可以趕快通知野生動物管理人員,但最好不要亂餵食。莫爾解釋,「在牠們獲得照料前,你不需要給牠們水或食物,只有照護人員知道什麼是適當的食物。」

生態保育
國際新聞
澳洲
企鵝

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

陸充電樁建設料加速,充換電設備規模上看千億RMB

大陸工信部部長近日表示,今(2016)年新能源車產銷有望成長一倍以上,並著力突破充電樁等環節發展瓶頸。根據在此之前出臺的電動汽車充電基礎設施發展指南提出,到2020年新增分散式充電樁超過480萬個,滿足500萬輛電動車的充電需求。陸媒引述機構估算,到2020年,充換電設備市場規模將達到1,240億元(人民幣,下同),且在政策推動和市場需求雙重推動下,充電網路建設運營發展可望進一步加速,相關訂單也將進入落實階段。

充電網路作為大陸新能源車推廣的基礎設施,獲得系列政策扶持。今年1月,財政部等五部委聯合發佈十三五充電基礎設施獎勵政策通知,明確2016-2020年中央財政將繼續安排資金對充電基礎設施建設、運營給予獎補。《通知》中要求獎補資金應當專門用於支持充電設施建設運營、改造升級、充換電服務網路運營監控系統建設等相關領域。其中,對於今年大氣污染治理重點城市,在標準車推廣量不低於3萬輛時,可獲得充電設施獎勵9,000萬元。

去年11月,大陸發改委印發的2015-2020年電動汽車充電基礎設施發展指南,按照適度超前原則明確充電基礎設施建設目標,到2020年滿足全國500萬輛電動汽車充電需求。同時,優先建設公交、出租及環衛與物流等公共服務領域充電基礎設施,新增超過3,850座公車充換電站、2,500座計程車充換電站、2,450座環衛物流等專用車充電站;另將結合骨幹高速公路網,建設「四縱四橫」的城際快充網路,新增超過800座城際快充站,以滿足城際出行需要。

而大陸各地方政府也在積極落實充電網路建設,京滬等重點城市進展最快。根據京津冀充電設施協同建設的需求,國網北京市電力公司今年計畫將建設14座高速公路快充站、5,880個城市快充樁,今年年底前實現北京區域內高速公路服務區全覆蓋。日前,上海提出到今年底,新能源汽車分時租賃服務網點超過1,000個,純電動汽車超過3,000輛,充電樁超過5,000個。

機構認為,大陸新能源車高速發展將迫使充電設施建設加速,預計今年充電設施投資將超過百億元,增速達到400%以上,未來幾年均有望保持高速增長;同時,充電樁管理APP等互聯網工具在充電運營網路的應用,加強充電樁統一管理的同時,也有望構建新的獲利模式。

(本文內容由授權使用;首圖來源: CC BY 2.0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

思源:秒級體驗百億級數據量監控鑽取

編者薦語:

當業務量快速增長的時候,業務保障平台就要應運而生,預判問題發出告警,越快越好,從宏觀到微觀一路下鑽響應越快越好,尤其是交易量暴漲的高峰時段。怎麼做到?看思源的現身說法:

以下文章來源於雲縱達摩院 ,作者劉勤紅

 

——業務保障平台性能提升走過的那些路
禧雲信息/研發中心/劉勤紅(思源) 2019年11月
 
業務保障平台需從多維度去監控業務的可靠性,快速定位問題並自動解決或推薦出解決方案,所以時效性顯得非常重要。接下來我將從以下三個維度展開如何將百億級數據量監控鑽取從十幾秒提升到秒級體驗:
· 工具升級:OpenTSDB–>Druid
· 調用鏈收集的優化
· 聚合查詢的優化
 

一、背景回顧

· 禧雲的團餐業務發展非常迅速,短短几個月的時間,日交易量就從數百萬拉高到千萬級,隨着調用鏈追蹤得愈發細緻,業務數據量也從億級上升到百億級別。
· 團餐的高峰交易量比外賣更集中,外賣可以提前預訂,而團餐的早中晚就餐非常集中,全國的學生幾乎都是一個點兒下課,從下單到吃完也就集中在20-30分鐘內,下圖0為交易量的曲線圖,坡度幾乎是直線上升,真是爭分奪秒!

圖0 團餐高峰期爭分奪秒不容有失
在這種高強度業務場景下,業務保障平台應運而生,它主要是多場景多維度實時監控大盤,從設備終端到服務端全鏈路監控,讓技術團隊從事後追查和整改,轉變為事前預警和快速判定根因。它主要涉及交易、業務調用鏈路、網絡監控、設備運行指標、業務日誌等維度數據。架構如下圖1所示。
圖1 基於OpenTSDB版的大致架構
· 千萬級日交易數據:從各業務系統或支付網關迴流的交易數據—>交易迴流服務—>多維度聚合入庫OpenTSDB和ElasticSearch集群;
· 十億級業務調用鏈數據:基於Jaeger Trace植入–>Nginx多路分發–>經Jaeger-collector收集到Kafka–>被多組消費者處理(Flink實時寫入OpenTSDB,批量寫入ES);
· 百億級網絡和設備指標數據:主要是智能設備上報的各種監控指標:基於HTTP/HTTPS層監視–>Nginx–>太空橋(我司IoT平台)設備監控–>批量入庫ES。
技術棧為:
· 鏈路採集:Uber開源的Jaeger
· 基礎數據存儲:ES
· 指標數據存儲:OpenTSDB
· 削峰填谷的消息隊列:Kafka
· 實時計算:Flink  

二、工具升級:OpenTSDB升級到Druid

在禧雲業務保障平台上,OpenTSDB確實沒有太好的性能表現,查詢數據量在百萬級的時候,速度還是可以的,在暑假交易額低谷期並未暴露出OpenTSDB的性能問題。但是進入9月開學季之後,數據延時和查詢緩慢的問題立馬就暴露出來了,中途也寄希望於升級OpenTSDB版本,但依然效果不佳。
 

A. OpenTSDB性能表現不佳的原因

第一個原因,Rowkey設計過長的問題。Rowkey第一大設計原則是保證唯一性,否則原先的數據會被覆蓋掉,第二大設計原則是長度原則,Rowkey是一個二進制,它的長度建議設計在10~100個字節,越短越好。Rowkey如果過長,對性能有以下影響:
1)HBase的持久化文件HFile是按照KeyValue存儲的,如果Rowkey過長,比如500個字節,1000萬列數據,光Rowkey就要佔用500*1000萬=50億個字節,將近1G數據,這會極大影響HFile的存儲效率。
2)MemStore緩存部分數據到內存,如果Rowkey字段過長,內存的有效利用率會降低,系統無法緩存更多的數據,這會降低檢索效率。
需要指出的是不僅Rowkey的長度越短越好,列族名、列名等也盡量使用短名字,因為這些名字都是會寫入HFile中的,過長的Rowkey、列族名、列名都會導致整體的存儲量成倍增加。
 
第二個原因,業務監控業務場景非常多,涉及到不同業務線的多個維度,比如多機房、多支付渠道、商戶/門店/設備/碼等多維度聚合,很難設計出一個滿足全場景的Rowkey方案。在任意維度的組合查詢下,OpenTSDB 查詢效率會明顯降低。
比如對於組合條件查詢使用的就是scan方式,在使用時有以下幾點值得注意:
1)通過setCaching、setBatch方法提高速度,以空間換時間;
2)通過setStartRow與setEndRow來限定範圍,範圍越小,性能越高;
3)通過setFilter方法添加過濾器,這也是分頁、多條件查詢的基礎,比如使用SingleColumnValueFilter。
以上優化當遇到真正海量數據時,會消耗很大的資源,每次都需要花較長的時間處理。
 

B. 升級為Druid

我司其他技術團隊已經試用了Druid, 其核心是通過數據預先聚合提高查詢性能,針對預先定義好的Schema,因此適合實時分析的場景,結果返回時間在亞秒級。
我們切換到Druid之後,響應速度上確實有一個數量級的提升,查詢千萬級的數據範圍基本秒級響應。
 

三、調用鏈收集的優化

調用鏈收集的數據流為:jaeger-collector–>Kafka–>jaeger-ingester 消費–>入庫ES。下面講一下如何優化。
A. 第一階段:Kafka消息堆積高峰期由千萬級降到百萬級
強調一點,合理的分區設置很重要。
剛開始我們嘗試調整每次拉取的消息條數,將ingester.parallelism由1000調整為6000,消息堆積似乎好了一點,但效果不明顯。
考慮到可能是因為併發數不夠,所以通過擴充Kafka的分區數去提高併發。先將分區數由5個擴到8個,消息堆積由千萬級降到百萬級。但繼續將分區擴到10個,就幾乎沒有什麼效果了。  
B. 第二階段:Kafka的版本選擇不可忽視
莫名其妙的是jaeger-ingester消費非常不穩定,頻繁與kafka斷開重連,嘗試去消費其他分區消息,導致消費速率上不去。
後來發現,當Kafka版本為v2.3時,多個jaeger-ingester節點會反覆觸發kafka的消費再平衡機制,結果導致jaeger-ingester只能單點消費。
所以我們又將Kafka版本回退到了v2.2,調整jaeger-ingester實例個數和Kafka分區數為1:1,可橫向擴容支持高併發。
 
C. 第三階段:消息堆積高峰期由百萬降到萬級,延時秒級已可接受
我們觀察到ES的負載已經很高了,單節點高峰期CPU負載達到16。之前為了方便定位問題,給網絡請求實時加上了traceId標記,調用的是jaeger原生的trace鏈路計算。現在分析發現查詢QPS太高,所以嘗試優化查詢邏輯,一方面改為自定義的邏輯直接查詢ES,一方面調整好批量閾值查詢(指的是1次查多少,目前是按時間10ms和數據條數100條一個批次去查ES)。
優化完成后,消息堆積又下降一個數量級。目前高峰期堆積非常少,秒級消費已可接受。
 

四、聚合查詢的優化

A. 散點圖 vs 柱狀圖

對於調用鏈宏觀展示來說,慣常使用散點圖,它可以表達出一段時間內請求的耗時分佈情況,如下圖2所示。

圖2 調用鏈散點圖
但是存在一個問題,當時間區間選得比較大的時候,服務端查詢的數據過多而響應變慢,客戶端要渲染的點太多也會非常卡。所以點要抽樣。抽樣方式能想得到的有:
a.過濾出耗時長的數據;
b.取最近一段時間的數據;
c.只取指定數量的數據;
d.對相同耗時進行聚合展示等等。
這些調整可謂犧牲了監控者的真實需求,因為有些數據監控者看不了,或者很難看全。
我們對比了阿里雲日誌服務的設計,他山之石可以攻玉,借鑒了以下兩點:
第一點,人們看問題的方式總是從宏觀一路下鑽到微觀,所以我們加了柱狀圖做為時間段聚合,方便從宏觀上看到請求量的規模分佈。下圖3是某區域的訂單趨勢柱狀圖。

圖3 訂單趨勢柱狀圖
第二點,如果數據量非常大,可繼續點擊柱圖,展開當前時間條件下的子柱狀圖。系統根據數據量規模,自動展示出散點圖,方便用戶瀏覽耗時分佈。這樣一層層穿刺下鑽,既滿足了人的操作習慣,又提高了處理速度,做到了秒級響應。

圖4柱狀圖下鑽
 

B. 從宏觀到微觀,化繁為簡

就宏觀到微觀的監控下鑽,下面講三個案例。
案例一,單一維度下鑽
其實多數時候人們只想關注某一維度的分佈情況,比如按應用、按工程、按服務IP、按請求URL、按商戶、按門店、按設備看分佈。對於ES來說,單一條件聚合速度很快,秒級響應。
案例二,網絡質量監察
我司在全國大江南北分佈着大量餐飲中心(即食堂),如何快速定位出哪些食堂網絡環境糟糕呢?不能等客戶告訴我們。
我們從請求量級篩選(系統推薦和自定義)、dns/http/ssl/tcp/mqtt耗時情況、趨勢發展等諸多因子中分析出餐飲中心網絡情況,哪怕是學校食堂的一次網絡抖動,都可以被我們偵查到。不僅能分析出網絡問題,而且還能下鑽到請求鏈路上的任何階段,比如是DNS、TCP、SSL、首包等,並分析出受影響的終端設備。依然是秒級響應,如下圖5所示。

圖5 設備耗時下鑽
 
案例三,找出掉單
當用戶已支付而商戶未收款時,如何從千萬級訂單中快速找到丟失的那一筆訂單呢?內部稱sos訂單:

  • 秒級偵查出來;
  • 快速定位在哪個環節出了問題;
  • 系統自我修復能力

具體做法為:
第一步,將內部可能發生的業務場景圈出來,通過Flink實時計算形成閉環,當其中一個鏈條斷了,就會立馬把待排查的sos訂單壓入隊列任務,然後不斷主動查詢第三方支付渠道確認支付狀態。
第二步,對於一些疑難問題如短時間內系統無法解決時,比如第三方支付渠道出現了故障,就會發出告警消息給客服,客服預先跟進,減少用戶投訴處理時間。
 
總結一下:
OpenTSDB切換為Druid,明顯提升了從宏觀下鑽到微觀的響應時間,基本能做到百億級數據量秒級響應。高峰時段Kafka消息堆積也大幅降低。下鑽方式做了優化,更符合工程師探查習慣。
 
-完-

歡迎關注公眾號:老兵筆記

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

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?