分類
發燒車訊

沒了IDE,你的Java項目還能Run起來嗎~

計算機只能識別機器碼0101…編程語言->能執行的機器碼 需要經過 預處理->編譯->彙編->鏈接->機器碼過程。一個語言處理系統的示意圖如下:

編譯器 是將源語言程序一次性翻譯成一個等價的,用目標語言編寫的程序。還存在另一種常見的語言處理器,解釋器:它是逐個語句的執行源語言程序。由一個編譯器產生的目標語言程序通常比一個解釋器快,但解釋器的錯誤診斷效果通常更好。

Java語言處理器結合了編譯和解釋的過程。一個.Java源程序首先被編譯為.class字節碼文件,被加載到虛擬機中,然後由虛擬機將字節碼翻譯成機器碼。

虛擬機的好處在於:一旦一個程序被轉換成 Java 字節碼,那麼它便可以在不同平台上的虛擬機實現里運行。實現一次編寫,到處運行。另外一個好處是它帶來了一個託管環境。這個託管環境能夠代替我們處理一些代碼中冗長而且容易出錯的部分,如自動內存管理與垃圾回收。

在Hotspot中,虛擬機翻譯字節碼有兩種方式:

1.解釋執行
即逐條將字節碼翻譯成機器碼並執行。

2.即時編譯
即將一個方法中包含的所有字節碼編譯成機器碼后再執行。

前者的優勢在於無需等待編譯,而後者的優勢在於實際運行速度更快。HotSpot 默認採用混合模式,綜合了解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而後將其中反覆執行的熱點代碼,以方法為單位進行即時編譯。

即時編譯建立在程序符合二八定律的假設上,也就是百分之二十的代碼佔據了百分之八十的計算資源。

好了,裝X結束。

阿姨知道的編譯知識全在上面了。。(っ╥╯﹏╰╥c)

如題,下面我們來看一下讓Java項目運行起來我們能做什麼。

我們能做的很簡單,當然不是寫虛擬機。我們只需要:

1.執行command javac,將.Java文件變為.class文件。
2.執行command java,讓.class文件運行起來。

也就是 執行command :)

Java程序的運行方式

Java程序可以通過java命令運行.class文件運行可執行Jar文件
我們先看第一種方式:從Hello World開始。

運行.class文件

Step1:編寫Java文件

Step2:執行 command javac

將.Java文件變為.class文件

小貼士:class文件的全路徑名是包名目錄+ 類文件名。

Step3:執行 command java

運行.class文件

神奇,我們沒有用IDE讓Java程序運行起來了 :)

小夥伴先別噴老阿姨,哪特么有這麼簡單的Java項目啊。。我們工作中用的明明都是Jar文件啊…
Jar文件咋運行啊!!

運行可執行Jar文件

Jar文件是基於ZIP文件格式的一種文件格式,它將大量的Java類文件、相關的元數據和資源(文本、圖片等)文件聚合到一個Jar文件中,此外還包含一個可選的META-INF文件夾。這個文件夾下的文件或文件夾主要用來打包和擴展配置信息,包括安全,版本,擴展程序和服務等。如MANIFEST.MF文件定義了擴展和打包的相關數據信息。
一個Jar文件通常在項目中用作第三方類庫使用,也是項目構建的一部分。

生成一個Jar文件大致分為兩步:

1.將源文件編譯為.class文件

2.通過 command jar命令將.class文件,資源文件等等打成一個文件格式的Jar文件。

我們以一個SbDemo項目為例來看Jar文件的打包和運行。項目目錄結構如下:

Test2.java中調用了Test1.java的方法,

我們需要先將Test1.java編譯並打成一個Test1.jar文件,然後通過Test1.jar將Test2.java編譯並打成一個可執行的Test2.jar文件

可執行和不可執行的Jar文件 區別在於是否在Jar文件中指定了main方法的入口,我們後面再看。

Step1:Test1.java的編譯

Step2:將編譯后的classes/com/Test1.class文件打成一個Test1.jar包

Java中和jar包相關的命令是jar命令,生成一個jar包我們需要定義信息文件(manifest-file),它可以定義所生成jar包的classpath類搜索路徑,jar包的入口類等等。可以理解為與Jar包相關的元數據配置信息
Step2.1 書寫信息文件
這裏我們使用resources/manifest-test1.text文件作為信息文件

是的,Test1.java太簡單了,就是打成一個可被他人引用的jar包,信息文件不重要。
Step2.2 執行打包命令

Step3. 編譯Test2.java文件
因為Test2.java中引用了com.Test1類,所以我們需要在編譯時指定Classpath路徑。
Classpath:顧名思義,是指待編譯類依賴的類所在路徑位置。我們可以通過 javac 的 -cp 參數指定。
關於編譯時classpath的值優先級如下:

  • 如果沒有傳入classpath參數,將使用環境變量CLASSPATH的值。(小夥伴不知道環境變量咋查看和設置?去看阿姨的上一篇文章:)
  • 如果沒有發現環境變量CLASSPATH,將使用 執行命令的當前文件夾(.)。
  • 如果javac命令行 通過-classpath or -cp參數指定了類路徑值,則優先級最高。

這裏我們使用-cp指定Test1.jar所在位置

可以看到classes目錄下已經生成了com2/Test2.class文件了。

Step4. 將編譯后的Test2.class和它依賴的Test1.jar一起打成一個可執行的Jar包
Step4.1 書寫信息文件
這時候我們使用信息文件resources/manifest-test2.text文件指定這些信息

Step4.2 執行Jar包生成命令

可以看到在lib目錄下生成了Test2.jar

Step5.運行我們的可執行Jar

大功告成了,我們的SbDemo項目Run起來了…

當然實際項目不可能人肉編譯,打包。我們需要通過Maven/Gradle等構建工具,幫助我們管理代碼之間的Jar包依賴,構建,部署…我們可能大多時候通過點一下IDE就託管了Maven的構建部署命令。

拿Maven舉例子,Maven首先定義了一套項目結構,我們按照它的結構書寫代碼,引入各個模塊所需要的Jar包依賴。然後Maven可以通過自己的生命周期管理項目的清理,構建,打包,部署階段。每個階段有對應的Maven插件執行相應的目標。IDE又整合了Maven,使我們通過點吧點吧按鈕就完成了項目的運行。

但是當一個項目並沒有按照規範的構建工具結構搭建,或者項目沒有成功運行報錯時,了解Java實際的編譯運行過程會對理解、解決這類問題有所幫助。

好啦,限於篇幅,阿姨先不講這些年Maven躺過的坑了,有想看的嗎?關注,在看,轉發三連回應下 >-<

參考資料:
[1].《編譯原理》序 (゚´ω`゚)゚
[2].https://time.geekbang.org/column/article/11289

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

分類
發燒車訊

【asp.net core 系列】8 實戰之 利用 EF Core 完成數據操作層的實現

0. 前言

通過前兩篇,我們創建了一個項目,並規定了一個基本的數據層訪問接口。這一篇,我們將以EF Core為例演示一下數據層訪問接口如何實現,以及實現中需要注意的地方。

1. 添加EF Core

先在數據層實現層引入 EF Core:

cd Domain.Implements
dotnet add package Microsoft.EntityFrameworkCore

當前項目以SqlLite為例,所以再添加一個SqlLite數據庫驅動:

dotnet add package Microsoft.EntityFrameworkCore.SQLite

刪除 Domain.Implements 里默認的Class1.cs 文件,然後添加Insfrastructure目錄,創建一個 DefaultContext:

using Microsoft.EntityFrameworkCore;

namespace Domain.Implements.Insfrastructure
{
    public class DefaultContext : DbContext
    {
        private string ConnectStr { get; }
        public DefaultContext(string connectStr)
        {
            ConnectStr = connectStr;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(ConnectStr);//如果需要別的數據庫,在這裏進行修改
        }
    }
}

2. EF Core 批量加載模型

通常情況下,在使用ORM的時候,我們不希望過度的使用特性來標註實體類。因為如果後期需要變更ORM或者出現其他變動的時候,使用特性來標註實體類的話,會導致遷移變得複雜。而且大部分ORM框架的特性都依賴於框架本身,並非是統一的特性結構,這樣就會造成一個後果:本來應該是對調用方隱藏的實現就會被公開,而且在項目引用關係中容易出現循環引用。

所以,我在開發中會尋找是否支持配置類,如果使用配置類或者在ORM框架中設置映射關係,那麼就可以保證數據層的純凈,也能實現對調用方隱藏實現。

EF Core的配置類我們在《C# 數據訪問系列》中關於EF的文章中介紹過,這裏就不做過多介紹了(沒來得及看的小夥伴們不着急,後續會有一個簡單版的介紹)。

通常情況下,配置類我也會放在Domain.Implements項目中。現在我給大家介紹一下如何快速批量加載配置類:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetAssembly(this.GetType()),
		t => t.GetInterfaces().Any(i => t.Name.Contains("IEntityTypeConfiguration")));
}

現在版本的EF Core支持通過Assembly加載配置類,可以指定加載當前上下文類所在的Assembly,然後篩選實現接口中包含IEntityTypeConfiguration的類即可。

3. 使用EF Core實現數據操作

我們已經創建好了一個EF Context,那麼現在就帶領大家一起看一下,如何使用EF來實現 上一篇《「asp.net core」7 實戰之 數據訪問層定義》中介紹的數據訪問接口:

新建一個BaseRepository類,在Domain.Implements項目的Insfrastructure 目錄下:

using Domain.Infrastructure;
using Microsoft.EntityFrameworkCore;

namespace Domain.Implements.Insfrastructure
{
    public abstract class BaseRepository<T> : ISearchRepository<T>, IModifyRepository<T> where T : class
    {
        public DbContext Context { get; }
        protected BaseRepository(DbContext context)
        {
            Context = context;
        }
    }
}

先創建以上內容,這裏給Repository傳參的時候,使用的是EFCore的默認Context類不是我們自己定義的。這是我個人習慣,實際上並沒有其他影響。主要是為了對實現類隱藏具體的EF 上下文實現類。

在實現各接口方法之前,創建如下屬性:

public DbSet<T> Set { get => Context.Set<T>(); }

這是EF操作數據的核心所在。

3.1 實現IModifyRepository接口

先實現修改接口:

public T Insert(T entity)
{   
    return Set.Add(entity).Entity;
}

public void Insert(params T[] entities)
{
    Set.AddRange(entities);
}

public void Insert(IEnumerable<T> entities)
{
    Set.AddRange(entities);
}
public void Update(T entity)
{
    Set.Update(entity);
}

public void Update(params T[] entities)
{
    Set.UpdateRange(entities);
}

public void Delete(T entity)
{
    Set.Remove(entity);
}

public void Delete(params T[] entities)
{
    Set.RemoveRange(entities);
}

在修改接口裡,我預留了幾個方法沒有實現,因為這幾個方法使用EF Core自身可以實現,但實現會比較麻煩,所以這裏藉助一個EF Core的插件:

dotnet add package Z.EntityFramework.Plus.EFCore

這是一個免費開源的插件,可以直接使用。在Domain.Implements 中添加后,在BaseRepository 中添加如下引用:

using System.Linq;
using System.Linq.Expressions;

實現方法:

public void Update(Expression<Func<T, bool>> predicate, Expression<Func<T, T>> updator)
{
    Set.Where(predicate).UpdateFromQuery(updator);
}

public void Delete(Expression<Func<T, bool>> predicate)
{
    Set.Where(predicate).DeleteFromQuery();
}

public void DeleteByKey(object key)
{
    Delete(Set.Find(key));
}

public void DeleteByKeys(params object[] keys)
{
    foreach (var k in keys)
    {
        DeleteByKey(k);
    }
}

這裏根據主鍵刪除的方法有個問題,我們無法根據條件進行刪除,實際上如果約定泛型T是BaseEntity的子類,我們可以獲取到主鍵,但是這樣又會引入另一個泛型,為了避免引入多個泛型根據主鍵的刪除就採用了這種方式。

3.2 實現ISearchRepository 接口

獲取數據以及基礎統計接口:

public T Get(object key)
{
    return Set.Find(key);
}

public T Get(Expression<Func<T, bool>> predicate)
{
    return Set.SingleOrDefault(predicate);
}

public int Count()
{
    return Set.Count();
}

public long LongCount()
{
    return Set.LongCount();
}

public int Count(Expression<Func<T, bool>> predicate)
{
    return Set.Count(predicate);
}

public long LongCount(Expression<Func<T, bool>> predicate)
{
    return Set.LongCount(predicate);
}

public bool IsExists(Expression<Func<T, bool>> predicate)
{
    return Set.Any(predicate);
}

這裡有一個需要關注的地方,在使用條件查詢單個數據的時候,我使用了SingleOrDefault而不是FirstOrDefault。這是因為我在這裏做了規定,如果使用條件查詢,調用方應該能預期所使用條件是能查詢出最多一條數據的。不過,這裏可以根據實際業務需要修改方法:

  • Single 返回單個數據,如果數據大於1或者等於0,則拋出異常
  • SingleOrDefault 返回單個數據,如果結果集沒有數據,則返回null,如果多於1,則拋出異常
  • First 返回結果集的第一個元素,如果結果集沒有數據,則拋出異常
  • FirstOrDefault 返回結果集的第一個元素,如果沒有元素則返回null

實現查詢方法:

public List<T> Search()
{
    return Query().ToList();
}

public List<T> Search(Expression<Func<T, bool>> predicate)
{
    return Query(predicate).ToList();
}

public IEnumerable<T> Query()
{
    return Set;
}

public IEnumerable<T> Query(Expression<Func<T, bool>> predicate)
{
    return Set.Where(predicate);
}

public List<T> Search<P>(Expression<Func<T, bool>> predicate, Expression<Func<T, P>> order)
{
    return Search(predicate, order, false);
}

public List<T> Search<P>(Expression<Func<T, bool>> predicate, Expression<Func<T, P>> order, bool isDesc)
{
    var source = Set.Where(predicate);
    if (isDesc)
    {
        source = source.OrderByDescending(order);
    }
    else
    {
        source = source.OrderBy(order);
    }
    return source.ToList();
}

這裏我盡量通過調用了參數最多的方法來實現查詢功能,這樣有一個好處,小夥伴們可以想一下哈。當然了,這是我自己覺得這樣會好一點。

實現分頁:

在實現分頁之前,我們知道當時我們定義的分頁參數類的排序字段用的是字符串,而不是lambda表達式,而Linq To EF需要一個Lambda表示才可以進行排序。這裏就有兩種方案,可以自己寫一個方法,實現字符串到Lambda表達式的轉換;第二種就是借用三方庫來實現,正好我們之前引用的EF Core增強插件里有這個功能:

var list = context.Customers.OrderByDescendingDynamic(x => "x.Name").ToList();

這是它給出的示例。

我們可以先依此來寫一份實現方法:

public PageModel<T> Search(PageCondition<T> condition)
{
    var result = new PageModel<T>
    {
        TotalCount = LongCount(condition.Predicate),
        CurrentPage = condition.CurrentPage,
        PerpageSize = condition.PerpageSize,
    };
    var source = Query(condition.Predicate);
    if (condition.Sort.ToUpper().StartsWith("a")) // asc
    {
        source = source.OrderByDynamic(t => $"t.{condition.OrderProperty}");
    }
    else // desc
    {
        source = source.OrderByDescendingDynamic(t => $"t.{condition.OrderProperty}");
    }
    var items = source.Skip((condition.CurrentPage -1)* condition.PerpageSize).Take(condition.PerpageSize);
    result.Items = items.ToList();
    return result;
}

回到第一種方案:

我們需要手動寫一個字符串的處理方法,先在Utils項目創建以下目錄:Extend>Lambda,並在目錄中添加一個ExtLinq類,代碼如下:

using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;

namespace Utils.Extend.Lambda
{
    public static class ExtLinq
    {
        public static IQueryable<T> CreateOrderExpression<T>(this IQueryable<T> source, string orderBy, string orderAsc)
        {
            if (string.IsNullOrEmpty(orderBy)|| string.IsNullOrEmpty(orderAsc)) return source;
            var isAsc = orderAsc.ToLower() == "asc";
            var _order = orderBy.Split(',');
            MethodCallExpression resultExp = null;
            foreach (var item in _order)
            {
                var orderPart = item;
                orderPart = Regex.Replace(orderPart, @"\s+", " ");
                var orderArry = orderPart.Split(' ');
                var orderField = orderArry[0];
                if (orderArry.Length == 2)
                {
                    isAsc = orderArry[1].ToUpper() == "ASC";
                }
                var parameter = Expression.Parameter(typeof(T), "t");
                var property = typeof(T).GetProperty(orderField);
                var propertyAccess = Expression.MakeMemberAccess(parameter, property);
                var orderByExp = Expression.Lambda(propertyAccess, parameter);
                resultExp = Expression.Call(typeof(Queryable), isAsc ? "OrderBy" : "OrderByDescending",
                    new[] {typeof(T), property.PropertyType},
                    source.Expression, Expression.Quote(orderByExp));
            }

            return resultExp == null
                ? source
                : source.Provider.CreateQuery<T>(resultExp);
        }
    }
}

暫時不用關心為什麼這樣寫,後續會為大家分析的。

然後回過頭來再實現我們的分頁,先添加Utils 到Domain.Implements項目中

cd ../Domain.Implements # 進入Domain.Implements 項目目錄
dotnet add reference ../Utils
public PageModel<T> Search(PageCondition<T> condition)
{
    var result = new PageModel<T>
    {
        TotalCount = LongCount(condition.Predicate),
        CurrentPage = condition.CurrentPage,
        PerpageSize = condition.PerpageSize,
    };
    var source = Set.Where(condition.Predicate).CreateOrderExpression(condition.OrderProperty, condition.Sort);
    var items = source.Skip((condition.CurrentPage -1)* condition.PerpageSize).Take(condition.PerpageSize);
    result.Items = items.ToList();
    return result;
}

記得添加引用:

using Utils.Extend.Lambda;

在做分頁的時候,因為前台傳入的參數大多都是字符串的排序字段,所以到後端需要進程字符串到字段的處理。這裏的處理利用了C# Expression的一個技術,這裏就不做過多介紹了。後續在.net core高級篇中會有介紹。

4. 總結

到目前為止,看起來我們已經成功實現了利用EF Core為我們達成 數據操作和查詢的目的。但是,別忘了EF Core需要手動調用一個SaveChanges方法。下一篇,我們將為大家介紹如何優雅的執行SaveChanges方法。

這一篇介紹到這裏,雖然說明不是很多,但是這也是我在開發中總結的經驗。

更多內容煩請關注我的博客《高先生小屋》

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

Python 3.9 beta2 版本發布了,看看這 7 個新的 PEP 都是什麼?

原作:Jake Edge

譯者:豌豆花下貓@Python貓

英文:https://lwn.net/Articles/819853/

隨着 Python 3.9.0b1 的發布,即開發周期中計劃的四個 beta 版本的首個,Python 3.9 的功能已經是完善了。在 10 月發布最終版本之前,還會有許多測試和穩定性方面的工作要做。

(譯註:beta1 版本發佈於 5 月 18 日,作者文章寫於 5 月 20,而到本篇譯文發布時,beta2 剛好在今天即 6 月 9 日發布,這是一個巧合!)

該發布說明中列出了被 3.9 接受的 7 個 Python 增強提案(PEP)。我們研究了其中的一些 PEP,看到有一些更新。現在似乎是一個介紹 Python 3.9 帶來的一些東西的好時機。

1、字符串操作

有時最簡單(表明上的)的事情最困難,或者至少會引起巨大的討論。其中大部分的爭議是關於命名(還能是什麼?),但是給標準字符串對象添加函數,來刪除前綴和後綴,這種想法是毫無爭議的。

是否可以將那些詞綴(前綴和後綴的統稱)指定為序列,以便在一次調用中處理多個詞綴,這一點尚不明確,最後它被從提案中刪除了,等待着其他人再次推動更改。

在 3 月底,Dennis Sweeney 在 python-dev 郵件列表上請求核心開發者支持 PEP 616(“字符串刪除前綴和後綴的方法”)。他指出了自 2019 年 3 月以來關於該話題的 python-ideas 討論。埃里克·史密斯(Eric V. Smith)同意支持該 PEP,這促使 Sweeney 發布並啟動了討論。

在最初版本中,他使用 cutprefix() 和 cutsuffix() 作為要添加給字符串對象的方法名。四種類型的 Python 對象將獲得新的方法:str(Unicode 字符串),byte(二進制序列),bytearray(可變的二進制序列)和 collections.UserString(字符串對象的一種封裝)。

它的寫法如下:

'abcdef'.cutprefix('abc') # 返回'def'
'abcdef'.cutsuffix('ef') # 返回'abcd'

針對命名部分,出現了一大堆的建議。基本上很少有人喜歡“cut”,因此“strip”、“strim”和“remove”被提出來了,並且都獲得了一些支持。

stripprefix() 以及 stripsuffix() 由於 PEP 中指出的一種理由,至少是被部分地反對了;現有的“strip”函數令人困惑,因此應避免重用該名稱。

str.lstrip() 和 str.rstrip() 方法也用於刪除前導字符和尾隨字符,但是它們對於真正在尋找 cutprefix() 功能的程序員來說是一個困惑的來源。

*strip() 在調用時接收一個字符串參數,但會將其視為一組字符,並從字符串開頭或結尾消除:

'abcdef'.lstrip('abc') # 返回“def”,符合預期
'abcbadefed'.lstrip('abc') # 返回'defed',完全不符合預期

最終,removeprefix() 和 removesuffix() 似乎佔據了上風,這正是 Sweeney 最終改成的版本。Guido van Rossum 也支持這些名字。

埃里克·法格倫(Eric Fahlgren)這樣搞笑地總結了命名的爭論:

我認為如果你先寫文檔,則名稱的選擇會更容易些:

cutprefix – 刪除指定的前綴。

trimprefix – 刪除指定的前綴。

stripprefix – 刪除指定的前綴。

removeprefix – 刪除指定的前綴。廢話

Sweeney 更新了 PEP,回應了許多評論,但還增加了提議將字符串元組作為詞綴的功能(可以在 PEP GitHub 倉庫中看到該版本)。

但是史蒂文·達普拉諾(Steven D’Aprano)不確定這樣做是否合理。他指出,唯一接受元組參數的字符串操作是 str.startswith() 和 str.endswith(),而它們不返回字符串(只是一個布爾值)。他懷疑添加這一種接收元組參數卻返回字符串的方法,因為無論選擇何種規則來處理元組,對於某些人來說都是“錯誤的”選擇。

例如:

這裏的困難在於,如果兩個或多個前綴都能匹配,則“剪切這些前綴中的一個”的概念是模稜兩可的。對 startwith 沒有區別:

 "extraordinary".startswith(('ex', 'extra'))

因為是從左到右,從最短到最大,甚至是隨機順序匹配都為True。但是對於 cutprefix,應該刪除哪個前綴?

如他所說,建議的規則是使用從左到右處理元組的第一個匹配字符串,但是有些人可能想要最長的匹配或最後一個匹配;這一切都取決於使用的上下文。他建議在提交添加此類行為之前,要給該功能更多的“浸泡時間”(譯註:即預備時間):“在添加多前綴/後綴的支持之前,我們首先應該對簡單的情況進行一些實際的體驗。”

伊桑·弗曼(Ethan Furman)同意達普拉諾(D’Aprano)的意見。但是維克托·斯汀納(Victor Stinner)強烈贊成元組參數的想法,只不過,他還想知道當傳入的元組有空字符串時,會怎麼處理。根據 PEP 提議,在處理元組時遇到空字符串(實際上可以匹配任何內容)只會返回原始字符串,這會導致令人驚訝的結果:

cutsuffix("Hello World", ("", " World"))    # 返回 "Hello World"
cutsuffix("Hello World", (" World", ""))    # 返回 "Hello"

這個例子不太明顯;詞綴不一定是硬編碼的,因此空字符串可能會溜進意想不到的位置。Stinner 建議如果遇到空字符串,則拋出 ValueError,類似於 str.split()。但是 Sweeney 決定完全刪除元組參數功能,以便“允許對此有更強見解的人在另外的 PEP 中提出並捍衛一系列的語義”。他在 3 月 28 日發布了該 PEP 的最新版本。

4 月 9 日,Sweeney 發起了一個指導委員會 issue,請求對其 PEP 進行評審。4 月 20 日,Stinner 代表委員會接受了該提案。

這是一個很小的更改,但值得花時間確保它具有長期適用的接口(和語義)。我們將在 Python 3.9 中看到 removeprefix() 和removesuffix()。

2、新解析器

並不令人感到驚訝的是,指導委員會已經接受了我們在 4 月中旬介紹過的 CPython 新解析器。PEP 617(“CPython 新的 PEG 解析器”)由 Python 創始人即前仁慈的獨裁者(BDFL) Guido van Rossum 以及 Pablo Galindo Salgado 和 Lysandros Nikolaou 共同提出。

它已經運行良好,並且在現有解析器的速度和內存使用方面提升了 10% 以內的性能。由於解析器是基於解析表達語法(PEG),因此也將簡化語言規範。CPython 現有的 LL(1) 解析器存在諸多缺點和一些 hack,新的解析器將會消除掉。

這一更改為 Python 超越 LL(1) 語法鋪平了道路,儘管現有語言並不完全是 LL(1)。這一更改不會太快,因為計劃是在 Python 3.9 的命令行中提供開關,保持現有解析器可用。

但是 Python 3.10 將刪除現有的解析器,這可能會導致語言變更。如果做了那些更改,那麼,其它的 Python 實現(例如 PyPy 和 MicroPython)就需要切換解析器的 LL(1) 實現,以便跟上語言規範的要求。這可能會使核心開發者暫停進行此類更改。

3、更多內容

我們在三月初查看了 PEP 615(“在標準庫中支持 IANA 時區數據庫”)。它將在標準庫中添加一個zoneinfo 模塊,該模塊將有助於從 IANA 時區數據庫中(也稱為“Olson數據庫”)獲取時區信息,以填充時區對象。在撰寫本文時,它看起來很順利。

在 3 月底,Paul Ganssle 請求就該 PEP 作出決議。他認為在一個有趣的時間範圍內接受它,可能會很有趣:

… 我希望(出於異想天開的原因)在 4 月 5 日(星期日)UTC 時間 02:00-04:00 或 13:00-17:30 之間接受它,因為這些時間代表着地球上某些地方的不明確時間(主要在澳大利亞)。還有另一個時機,那就是在 4 月 19 日星期日 UTC 01:00-03:00 之間,這段時間在西撒哈拉是不明確的。

他意識到這可能難以實現,它當然不是優先考慮的事。指導委員會沒有錯過第二個時間窗太多;Barry Warsaw 於 4 月 20 日宣布接受該 PEP。

Python 現在將具有一種機制來訪問系統的時區數據庫,以創建和處理時區。另外,Python 軟件包索引(PyPI)中有一個 tzdata 模塊,它為缺少 IANA 數據的系統提供這些數據;它將由 Python 核心開發者維護。

PEP 593(“靈活的函數和變量註釋”)添加了一種將上下文特定的(context-specific)元數據與函數和變量關聯的方法。實際上,type hint 註解已擠出了很多年前在 Python 3.0 中實現的 PEP 3107(“函數註釋”)中設想的其它用例。PEP 593 使用註解的(Annotated)類型提示為這些用例創建了一種新的機制。

PEP 585(“標準集合中的類型提示泛型”)提供了另一種清除方法。它將允許刪除在 typing 模塊中維護的一組并行的類型別名,以支持泛型。例如,type.List 類型將不再需要支持諸如“dict[str,list[int]]”之類的註解(例如,一個帶有字符串鍵和整數列表的值的字典)。

字典“加法”的聯合操作也會是 Python 3.9 的一部分。它曾不時引起爭議,但是 2 月中旬,PEP 584(“給字典添加聯合操作符”)被 Van Rossum 推薦採納。指導委員會迅速同意了,該特性於 2 月 24 日合入。

最後一個 PEP 是 PEP 602(“Python 的年度發布周期”)。如提案所書,它將發布節奏從每 18 個月更改為每年一次。但是,開發和發布周期是重疊的,因此整個功能開發需要 12 個月的時間。當第一個 Python 3.9 beta 版本發布時(即現在),Python 3.10 的功能開發就開始了。請繼續關注來年的下一輪 PEP。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

分類
發燒車訊

[原創][開源] SunnyUI.Net 主題

SunnyUI.Net, 基於 C# .Net WinForm 開源控件庫、工具類庫、擴展類庫、多頁面開發框架

  • Blog: https://www.cnblogs.com/yhuse
  • Gitee: https://gitee.com/yhuse/SunnyUI
  • GitHub: https://github.com/yhuse/SunnyUI
  • 幫助文檔目錄: https://www.cnblogs.com/yhuse/p/SunnyUI_Menu.html
  • 歡迎交流,QQ群:  56829229 (SunnyUI技術交流群) 

主題

1、Color 色彩

SunnyUI為了避免視覺傳達差異,使用一套特定的調色板來規定顏色,為你所搭建的產品提供一致的外觀視覺感受。主要顏色參照Element(https://element.eleme.cn/

  • 主色

SunnyUI主要品牌顏色是鮮艷、友好的藍色。

  • 輔助色

除了主色外的場景色,需要在不同的場景中使用(例如紅色表示危險的操作)。

  • 中性色

中性色用於文本、背景和邊框顏色。通過運用不同的中性色,來表現層次結構。

 

2、Rect邊框

我們對邊框進行統一規範,可用於按鈕、卡片、彈窗等組件里。

主要屬性如下:

  • RectColor:邊框顏色
  • RectDisableColor:控件不可用時邊框顏色
  • RectSides:邊框显示方向
  • 無:不显示邊框
  • 全部:显示全部邊框
  • 頂:显示頂部邊框
  • 底:显示底部邊框
  • 左:显示左側邊框
  • 右:显示右側邊框

注:邊框显示和圓角設置相關,如果一側的邊框兩端端點為圓角,則此邊框必定显示。

 

3、Radius圓角

我們提供了以下幾種圓角樣式,以供選擇。默認圓角大小為5px。

主要屬性如下:

Radius:圓角大小

RadiusSides:显示四個角圓角的显示與否

  • 圓角不显示

  • 默認圓角大小為5px

  • 圓角大小與控件高度相等時,显示大圓角

  • 可通過四個角圓角的設置,對控件組合显示

 

4、Font字體

默認字體為:微軟雅黑, 12pt

 

5、Style主題

SunnyUI包含 Element 風格主題 11 個,DotNetBar 主題 3 個,其他主題 2 個,包含主題管理組件 UIStyleManager,可自由切換主題。

  •  UIStyleManager

參考SunnyUI.Demo.exe,將UIStyleManager放置在主窗體上,通過選擇UIStyleManager的屬性Style,或者通過代碼設置統一主題風格。

UIStyleManager.Style = style;

 

  • Style主要屬性如下:

Style:設置主題風格

StyleCustomMode:是否為自定義主題,設置為False時使用UIStyleManager提供的統一主題風格,設置為Ture時可手動調整控件配色,不受UIStyleManager約束。

 

  • UIStyle.Blue

  • UIStyle.Green

  • UIStyle.Orange

  • UIStyle.Red

  • UIStyle.Gray

  • UIStyle.White

  • UIStyle.DarkBlue

  • UIStyle.Black

  • UIStyle.Office2010Blue

  • UIStyle.Office2010Silver

  • UIStyle.Office2010Black

  

原創文章,轉載請保留鏈接 Sunny’s blog

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

分類
發燒車訊

Spring Boot 教程 – Elasticsearch

1. Elasticsearch簡介

Elasticsearch是一個基於Lucene的搜索服務器。它提供了一個分佈式多用戶能力的全文搜索引擎,基於RESTful web接口。Elasticsearch是用Java語言開發的,並作為Apache許可條款下的開放源碼發布,是一種流行的企業級搜索引擎。Elasticsearch用於雲計算中,能夠達到實時搜索,穩定,可靠,快速,安裝使用方便。官方客戶端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和許多其他語言中都是可用的。根據DB-Engines的排名显示,Elasticsearch是最受歡迎的企業搜索引擎,其次是Apache Solr,也是基於Lucene。以後再給大家詳細介紹solr。

它能很方便的使大量數據具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸縮性,能使數據在生產環境變得更有價值。Elasticsearch 的實現原理主要分為以下幾個步驟,首先用戶將數據提交到Elasticsearch 數據庫中,再通過分詞控制器去將對應的語句分詞,將其權重和分詞結果一併存入數據,當用戶搜索數據時候,再根據權重將結果排名,打分,再將返回結果呈現給用戶。

Elasticsearch可以用於搜索各種文檔。它提供可擴展的搜索,具有接近實時的搜索,並支持多租戶。”Elasticsearch是分佈式的,這意味着索引可以被分成分片,每個分片可以有0個或多個副本。每個節點託管一個或多個分片,並充當協調器將操作委託給正確的分片。再平衡和路由是自動完成的。“相關數據通常存儲在同一個索引中,該索引由一個或多個主分片和零個或多個複製分片組成。一旦創建了索引,就不能更改主分片的數量。

Elasticsearch使用Lucene,並試圖通過JSON和Java API提供其所有特性。它支持facetting和percolating,如果新文檔與註冊查詢匹配,這對於通知非常有用。另一個特性稱為“網關”,處理索引的長期持久性;例如,在服務器崩潰的情況下,可以從網關恢復索引。Elasticsearch支持實時GET請求,適合作為NoSQL數據存儲,但缺少分佈式事務。

2. Elasticsearch深入了解

2.1 Elasticsearch的底層實現

  • 2.1.1 lucene

    Es是一個比較複雜的搜索服務器,本身也是使用Java語言編寫的,在上面的簡介中,說明了ES是一個基於lucene的搜索服務器,lucene是什麼呢?Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎。lucene也是使用Java語言編寫的,Java天下第一!

    Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟件基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋。在Java開發環境里Lucene是一個成熟的免費開源工具。就其本身而言,Lucene是當前以及最近幾年最受歡迎的免費Java信息檢索程序庫。至於lucene到底是怎麼實現的,牛牛們可能要自己去百度或者谷歌一下啦。

  • 2.1.2 Elasticsearch的基本概念

    1. 集群(Cluster):就是多台ES服務器在一起構成搜索服務器,現在很多應用基本上都有集群的概念,提高性能,讓應用具有高可用性,一台服務器掛掉,可以很快有另一台ES服務器補上。

    2. 節點(Node):節點就是集群中的某一台ES服務器就稱為一個節點。

    3. 索引庫(Index Indices):就是ES服務器上的某一個索引,相當於Mysql數據庫中的數據庫的概念,一個節點可以有很多個索引庫。

    4. 文檔類型(Type):這個概念就相當於Mysql數據庫中表的概念,一個索引庫可以有很多個文檔類型,但是這個概念現在慢慢淡化了,因為在ES中一個索引庫直接存數據文檔就挺好的,這個概念現在來說有點多餘了,所以ES官方也在淡化這個概念,在ES8中,這個概念將會徹底的消失。

    5. 文檔(Doc):文檔就相當於Mysql是數據庫中某個表的一條數據記錄,現在ES已經到7.7版本了,我們也就忽略type這個概念,直接在索引庫中存文檔即可。另外需要說一下,我們一般把數據文檔存到Es服務器的某個索引庫的這個動作稱之為索引

      最後還有兩個比較重要的概念,但是可能不是那麼直觀的可以感受得到:

      分片(Shards)和副本(Replicas)

      索引可能會存儲大量數據,這些數據可能超過單個節點的硬件限制。例如,十億個文檔的單個索引佔用了1TB的磁盤空間,可能不適合單個節點的磁盤,或者可能太慢而無法單獨滿足來自單個節點的搜索請求。

      為了解決此問題,Elasticsearch提供了將索引細分為多個碎片的功能。創建索引時,只需定義所需的分片數量即可。每個分片本身就是一個功能齊全且獨立的“索引”,可以託管在群集中的任何節點上。

      分片很重要,主要有兩個原因:

      • 它允許您水平分割/縮放內容量
      • 它允許您跨碎片(可能在多個節點上)分佈和并行化操作,從而提高性能/吞吐量

      分片如何分佈以及其文檔如何聚合回到搜索請求中的機制由Elasticsearch完全管理,並且對您作為用戶是透明的。

      在隨時可能發生故障的網絡/雲環境中,非常有用,強烈建議您使用故障轉移機制,以防碎片/節點因某種原因脫機或消失。為此,Elasticsearch允許您將索引分片的一個或多個副本製作為所謂的副本分片(簡稱副本)。

      複製很重要,主要有兩個原因:

      • 如果分片/節點發生故障,它可提供高可用性。因此,重要的是要注意,副本碎片永遠不會與從其複製原始/主要碎片的節點分配在同一節點上。
      • 由於可以在所有副本上并行執行搜索,因此它可以擴展搜索量/吞吐量。

      總而言之,每個索引可以分為多個碎片。索引也可以複製零(表示沒有副本)或多次。複製后,每個索引將具有主碎片(從中進行複製的原始碎片)和副本碎片(主碎片的副本)。可以在創建索引時為每個索引定義分片和副本的數量。創建索引后,您可以隨時動態更改副本數,但不能事後更改分片數。

      默認情況下,Elasticsearch中的每個索引分配有5個主碎片和1個副本,這意味着如果集群中至少有兩個節點,則索引將具有5個主碎片和另外5個副本碎片(1個完整副本),總共每個索引10個碎片。

  • 2.1.3 Elasticsearch的索引原理

    Es作為一個全文檢索服務器,那麼它在搜索方面肯定很在行啦!那它是怎麼做到的呢?

    Es官方有這麼一句話:一切設計都是為了提高搜索的性能!

    Es能夠快速的搜索出我們需要的內容,靠的就是倒排索引的思想,或者說是一種設計!

    在沒有使用倒排索引的情況下,正常思路是根據搜索關鍵字去查找相應的內容,但是使用了倒排索引之後,ES會先將文檔的所有內容拆分成多個詞條,創建一個包含所有不重複詞條的排序列表,然後列出每個詞條出現在哪個文檔。

    例如,假設我們有兩個文檔,每個文檔的 content 域包含如下內容:

    ​ Doc_1:The quick brown fox jumped over the lazy dog

    ​ Doc_2:Quick brown foxes leap over lazy dogs in summer

    ES首先會將這兩個文檔拆分成多個單獨的詞,或者叫做詞條,然後為所有的詞條創建一個排序列表,並記錄每個詞條出現的文檔的信息。就像下面這樣:

    Term      Doc_1  Doc_2
    -------------------------
    Quick   |       |  X                        /*
    The     |   X   |								Term就是詞條,比如第一個Term就是Quick關鍵字,在Doc_1中不存
    brown   |   X   |  X							在,在Doc_2中存在,其他的以此類推。
    dog     |   X   |							*/
    dogs    |       |  X
    fox     |   X   |
    foxes   |       |  X
    in      |       |  X
    jumped  |   X   |
    lazy    |   X   |  X
    leap    |       |  X
    over    |   X   |  X
    quick   |   X   |
    summer  |       |  X
    the     |   X   |
    ------------------------
    

    現在,如果我們想搜索 quickbrown這兩個關鍵字,我們只需要查找包含每個詞條的文檔,就相當於我們查詢的時候,是通過這個索引表找到文檔,在通過文檔去找文檔內容中的搜索關鍵字,與傳統的通過關鍵字去找內容是不同的。

    倒排索引到底是個怎麼實現的,怎麼個思想,我在這裏就不一一說明了,大家可以看下官方的詳細介紹:倒排索引的原理

    還有es官方的一系列的說明也都可以了解一下:什麼是Elasticsearch?

2.2 Elasticsearch的安裝

本演示項目ES版本為7.0.0版本,其他版本的ES的maven依賴與其他的jar包關係請自行查閱官方文檔,保證不衝突。

  • Windows

    Es服務器的安裝很簡單,Windows版本特別的簡單,直接去官網下載,運行 bin/elasticsearch 或者bin\elasticsearch.bat

  • Linux(CentOS7)

    首先我們去官網下載ES的tar.gz包,然後自建一個文件夾放好,然後解壓tar.zg壓縮包:

    tar -xvf elasticsearch-7.0.0.tar.gz
    

    然後進入到bin目錄下:

    cd elasticsearch-7.0.0/bin
    

    然後運行elasticsearch:

    ./elasticsearch
    

    這個時候肯定會報錯的,因為沒有進行配置,所以我們先對es進行一些簡單的配置,保證能單機運行,進入elasticsearch-7.7.0/config目錄,對es的核心配置文件進行編輯:

    vim elasticsearch.yml
    

    進入到了elasticsearch.yml文件的編輯頁面:

    首先我們配置集群名稱,集群名稱自己取一個喜歡的名字就好:

    接下來配置節點名稱,就是在這個集群中,這個es服務器的名稱:

    接下來配置一些必要的參數:

    bootstrap.memory_lock: 是否鎖住內存,避免交換(swapped)帶來的性能損失,默認值是: false。

    bootstrap.system_call_filter: 是否支持過濾掉系統調用。elasticsearch 5.2以後引入的功能,在bootstrap的時候check是否支持seccomp。

    配置network為所有人都可以訪問,因為我們一般是使用ssh連接工具在其他的電腦上操作Linux系統,所以我們需要配置一下:

    到這裏就配置完成了,但是當你重新去運行.elasticsearch的可執行文件的時候,依然會報錯。

    報錯信息中可能包含以下幾個錯誤:

    • max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]

      原因:無法創建本地文件問題,用戶最大可創建文件數太小。

      解決方法:切換到root賬戶下,進入Linux系統文件夾,編輯limits.conf文件:

      vim /etc/security/limits.conf
      

      在文件的末尾加上:

      *                soft    nofile          65536
      *                hard    nofile          65536
      *                soft    nproc           4096
      *                hard    nproc           4096
      
    • max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

      原因:最大虛擬內存太小,需要修改系統變量的最大值。

      解決方法:切換到root賬戶下,進入Linux系統文件夾,編輯sysctl.conf文件:

      vim /etc/sysctl.conf
      

      在文件的末尾加上:

      vm.max_map_count=262144
      
    • max number of threads [1024] for user [es] likely too low, increase to at least [2048]

      原因:無法創建本地線程問題,用戶最大可創建線程數太小。

      解決方法:如果你是CentOS6及以下系統,編輯的文件是90-nproc.conf這個文件,如果你和我一樣使用的是CentOS7的話,編輯的文件是20-nproc.conf文件,其實這兩個文件是一樣的,只是在不同CentOS系統中名稱不一樣而已。

      CentOS7使用這個命令:

      vim /etc/security/limits.d/20-nproc.conf
      

      CentOS6使用這個命令:

      vim /etc/security/limits.d/90-nproc.conf
      

      只需要在文件中加上以下配置:

      *          soft    nproc     4096
      

      這個配置的意思是說賦予其他用戶的可創建本地線程數為4096。在這個文件中本來就有一個配置,意思是說賦予root賬戶創建線程數不受限制。我們就把上面的配置加在本來存在的配置的下面一行就可以了。

      如果是CentOS7的使用者,還需要配置另一個文件,否則這個最大線程數是不會生效的。CentOS 7 使用systemd替換了SysV,Systemd目的是要取代Unix時代以來一直在使用的init系統,兼容SysV和LSB的啟動腳本,而且夠在進程啟動過程中更有效地引導加載服務。在/etc/systemd目錄下有一個系統的默認管理配置,這裡有登陸、日誌、服務、系統等。所以CentOS7的使用者還需要配置下面這個文件:

      vim /etc/systemd/system.conf
      

      對其中的選項進行配置,在文件的末尾加上:

      DefaultLimitNOFILE=65536
      DefaultLimitNPROC=4096
      

    上面的所以錯誤解決完畢之後,我們再運行.elasticsearch可執行文件,es才可以啟動成功。

2.3 Elasticsearch的使用

首先給大家介紹一個谷歌瀏覽器插件,這個插件是用來可視化展示es的索引庫數據的,這個插件叫做ElasticVue,個人感覺挺好用的,展示也比較方便,給大家截個圖看看:

大家可以使用這個建立索引庫,然後調用es官方的es專用的語法操作es服務器進行CRUD操作,但是此處我只介紹Java語言如何調用es服務器API,廢話不多說,我們直接開始下一步。

  • 2.3.1 引入依賴

    搭建工程的過程我就不演示了,直接上pom.xml依賴文件。

    pom.xml

    <!--springboot父工程-->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.2.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <dependencies>
            <!--springboot-web組件-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.2.2.RELEASE</version>
            </dependency>
            <!--elasticsearch-rest-client組件-->
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-client</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--elasticsearch-rest-high-level-client組件-->
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-high-level-client</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--elasticsearch組件-->
            <dependency>
                <groupId>org.elasticsearch</groupId>
                <artifactId>elasticsearch</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--mybatis整合springboot組件-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.0</version>
            </dependency>
            <!--mysql數據庫連接驅動-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.18</version>
            </dependency>
            <!--lombok組件-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.10</version>
            </dependency>
            <!--json組件gson-->
            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>2.8.5</version>
            </dependency>
            <!--springboot-test組件-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-test</artifactId>
            </dependency>
            <!--單元測試junit組件-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
            <!--spring-test組件-->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>5.2.2.RELEASE</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <!--springboot的maven插件-->
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <compilerArgs>
                            <arg>-parameters</arg>
                        </compilerArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
  • 2.3.2 Elasticsearch的配置類和Gson配置類和應用配置文件

    application.yml

    butterflytri:
      databaseurl-port: 127.0.0.1:3306 # 數據庫端口
      database-name: student_db # 數據庫名
      host: 192.168.129.100:9200 # es服務端
    server:
      port: 8080 # 應用端口
      servlet:
        context-path: /butterflytri # 應用映射
    spring:
      application:
        name: mybatis # 應用名稱
      datasource:
        url: jdbc:mysql://${butterflytri.databaseurl-port}/${butterflytri.database-name}?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
    mybatis:
      type-aliases-package: com.butterflytri.entity # entity別名
      mapper-locations: classpath:com/butterflytri/mapper/*Mapper.xml # mapper映射包掃描
    

    注意:yml文件中的192.168.129.100:9200是es對外的端口,使用的http協議進行操作,es服務器還有個9300端口,這個端口是es集群中各個節點進行交流的端口,使用的是tcp協議。所以我們連接的時候,端口要使用9200端口。

    項目啟動類沒有什麼特別的東西,就不展示了。

    ElasticsearchConfig.java

    package com.butterflytri.config;
    
    import org.apache.http.HttpHost;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.beans.factory.FactoryBean;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: ElasticSearchConfig
     */
    @Configuration
    public class ElasticSearchConfig implements FactoryBean<RestHighLevelClient>, InitializingBean, DisposableBean {
    
        /**
         * {@link FactoryBean<T>}:FactoryBean<T>是spring對外提供的對接接口,當向spring對象使用getBean("..")方法時,
         *                         spring會使用FactoryBean<T>的getObject 方法返回對象。所以當一個類實現的factoryBean<T>接口時,
         *                         那麼每次向spring要這個類時,spring就返回T對象。
         *
         * {@link InitializingBean}:InitializingBean接口為bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,
         *                          凡是繼承該接口的類,在初始化bean的時候會執行該方法。在spring初始化bean的時候,如果該bean是
         *                          實現了InitializingBean接口,並且同時在配置文件中指定了init-method,系統則是
         *                          先調用afterPropertiesSet方法,然後在調用init-method中指定的方法。
         *
         * {@link DisposableBean}:DisposableBean接口為bean提供了銷毀方法destroy-method,會在程序關閉前銷毀對象。
         */
    
        @Value("#{'${butterflytri.host}'.split(':')}")
        private String[] host;
    
        private RestHighLevelClient restHighLevelClient;
    
        private RestHighLevelClient restHighLevelClient() {
            restHighLevelClient = new RestHighLevelClient(
    
                    RestClient.builder(new HttpHost(host[0],Integer.valueOf(host[1]),"http"))
    
            );
            return restHighLevelClient;
        }
    
        @Override
        public void destroy() throws Exception {
            restHighLevelClient.close();
        }
    
        @Override
        public RestHighLevelClient getObject() throws Exception {
            return restHighLevelClient;
        }
    
        @Override
        public Class<?> getObjectType() {
            return RestHighLevelClient.class;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            restHighLevelClient();
        }
    
    }
    

    ES的配置類,這個配置類實現了三個接口,三個接口的作用我也寫上了註釋,大家可以看下,需要注意的是FactoryBean這個接口,一但實現了這個接口,每當你需要使用泛型表示的對象T的時候,Spring不會從容器中去拿這個對象,而是會調用這個FactoryBean.getObject()方法去拿對象。其他的就沒有什麼了。

    Gson.java

    Gson是一個操作json數據的類,它的執行效率可能會慢一點,但是它在解析json數據的時候不會出Bug。

    package com.butterflytri.config;
    
    import com.google.gson.Gson;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: GsonConfig
     */
    @Configuration
    public class GsonConfig {
    
        /**
         * {@link Gson}:一個操作json的對象,有比較好的json操作體驗,相對於Alibaba的FastJson來說速度慢一些,但是FastJson在解析
         *              複雜的的json字符串時有可能會出現bug。
         * @return Gson
         */
    
        @Bean
        public Gson gson() {
            return new Gson();
        }
    
    }
    

    Constants.java

    這是我寫的常量類,放一些ES使用的常量,直接寫字符串也行,但是我建議這樣做。

    package com.butterflytri.constants;
    
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: Constants
     */
    public class Constants {
    
        /**
         * es搜索關鍵字
         */
        public static final String KEYWORD = ".keyword";
    
        /**
         * es的type類型:type字段將在 elasticsearch-version:8 中徹底刪除,本來就覺得沒得啥用。
         */
        public static final String DOC_TYPE = "_doc";
    
        /**
         * 學生信息索引類型
         */
        public static final String INDEX_STUDENT = "student_info";
    
    
        /**
         * 自定連接符
         */
        public static final String CONNECTOR = " --> ";
    
    }
    

    Student.java

    package com.butterflytri.entity;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.io.Serializable;
    
    /**
     * @author: WJF
     * @date: 2020/5/16
     * @description: Student
     */
    
    @ToString
    @Getter
    @Setter
    public class Student implements Serializable {
    
        private Long id;
    
        private String studentName;
    
        private String studentNo;
    
        private String sex;
    
        private Integer age;
    
        private String clazz;
    
    }
    

    StudentMapper.java

    package com.butterflytri.mapper;
    
    import com.butterflytri.entity.Student;
    import org.apache.ibatis.annotations.Mapper;
    
    import java.util.List;
    
    /**
     * @author: WJF
     * @date: 2020/5/16
     * @description: StudentMapper
     */
    @Mapper
    public interface StudentMapper {
    
        /**
         * 查詢所有學生信息
         * @return List<Student>
         */
        List<Student> findAll();
    
        /**
         * 通過id查詢學生信息
         * @param id:學生id
         * @return Student
         */
        Student findOne(Long id);
    
        /**
         * 通過學號查詢學生信息
         * @param studentNo:學生學號
         * @return Student
         */
        Student findByStudentNo(String studentNo);
    
    }
    

    mybatis的SQL映射文件我就不展示了,也很簡單,大家看接口方法名就應該可以想象得到SQL語句是怎樣的。

  • 2.3.3 索引數據到ES服務器

    IndexServiceImpl.java

    package com.butterflytri.service.impl;
    
    import com.butterflytri.constants.Constants;
    import com.butterflytri.entity.Student;
    import com.butterflytri.service.IndexService;
    import com.google.gson.Gson;
    import org.elasticsearch.action.ActionListener;
    import org.elasticsearch.action.index.IndexRequest;
    import org.elasticsearch.action.index.IndexResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.common.xcontent.XContentType;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.io.IOException;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: IndexServiceImpl
     */
    @Service
    public class IndexServiceImpl implements IndexService {
    
        @Resource
        private Gson gson;
    
        @Resource
        private RestHighLevelClient restHighLevelClient;
    
        @Override
        public String index(Student student) {
            StringBuilder builder = new StringBuilder();
            IndexRequest indexRequest = this.initIndexRequest(student);
            try {
                // 同步索引到elasticsearch服務器,獲取索引響應IndexResponse
                IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
                String statusName = indexResponse.status().name();
                int statusCode = indexResponse.status().getStatus();
                builder.append(statusName).append(Constants.CONNECTOR).append(statusCode);
            } catch (IOException e) {
                builder.append("Fail").append(Constants.CONNECTOR).append(e.getMessage());
            }
            return builder.toString();
        }
    
    
        @Override
        public String indexAsync(Student student) {
            StringBuilder builder = new StringBuilder();
            IndexRequest indexRequest = this.initIndexRequest(student);
            // 異步索引到elasticsearch服務器,獲取索引響應IndexResponse
            restHighLevelClient.indexAsync(indexRequest, RequestOptions.DEFAULT,actionListener(builder));
            return builder.toString();
        }
    
    
    
        /**
         * 初始化IndexRequest,並設置數據源。
         * @param student
         * @return IndexRequest
         */
        private IndexRequest initIndexRequest(Student student) {
            // 構建IndexRequest,設置索引名稱,索引類型,索引id
            IndexRequest indexRequest = new IndexRequest(Constants.INDEX_STUDENT);
            // 可以不設置,默認就是'_doc'
            indexRequest.type(Constants.DOC_TYPE);
            // 設置索引id為studentId
            indexRequest.id(String.valueOf(student.getId()));
            // 設置數據源
            String studentJson = gson.toJson(student);
            indexRequest.source(studentJson, XContentType.JSON);
            return indexRequest;
        }
    
        /**
         * 異步索引的回調監聽器,根據不同的結果做出不同的處理
         * @param builder
         * @return ActionListener<IndexResponse>
         */
        private ActionListener<IndexResponse> actionListener(StringBuilder builder) {
            return new ActionListener<IndexResponse>() {
                // 當索引數據到es服務器時,返回不同的狀態
                @Override
                public void onResponse(IndexResponse indexResponse) {
                    String statusName = indexResponse.status().name();
                    int statusCode = indexResponse.status().getStatus();
                    builder.append(statusName).append(Constants.CONNECTOR).append(statusCode);
                }
    
                // 當索引數據時出現異常
                @Override
                public void onFailure(Exception e) {
                    builder.append("Fail").append(Constants.CONNECTOR).append(e.getMessage());
                }
            };
        }
    }
    

    上面的內容很簡單,就是將Student對象格式化為Json字符串,然後存到es服務器中,大家只要遵守一個規則就好,就是操作es服務器,不管是什麼操作都是用RestHighLevelClient這個類去操作,上面的就是student對象索引的es服務器中,使用restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT),首先就是構建indexRequest對象,這個對象就是索引請求對象,具體幹了什麼看代碼上的註釋。這裏還有個restHighLevelClient.indexAsync()這個方法,這個方法和上面的index方法一樣的效果,只不過是異步調用。

    接下來我們測試一下這個代碼,請看:

    @Test
        public void indexTest() {
            List<Student> list = studentMapper.findAll();
            for (Student student : list) {
                String message = indexService.index(student);
                System.out.println(message);
            }
        }
    

    我們使用ElasticVue插件連接es服務器即可看到有一個索引庫:

    當我們點擊到show按鈕的時候,可以看到student_info索引庫中有幾條記錄:

    索引數據到數據庫成功了。

  • 2.3.4 獲取Es服務器數據

    獲取數據,是es提供給我們的API,這個Api只能獲取某個索引的某一條文檔,示例如下:

    GetServiceImpl.java

    	@Override
        public Student get(String id) {
            Student student = new Student();
            GetRequest getRequest = new GetRequest(Constants.INDEX_STUDENT, id);
            try {
                GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
                String source = getResponse.getSourceAsString();
                student = gson.fromJson(source, Student.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return student;
        }
    

    接着我們在測試類中,調用這個方法然後打印一下結果:

    GetServiceTest.java

    	@Test
        public void getTest() {
            Student student = getService.get("1");
            System.out.println(student);
        }
    

    結果如下:

    更新數據文檔和刪除數據文檔我就不演示了,都是大同小異,大家可以拉下我的代碼,好好研究一下,都有詳細的註釋,覺得可以的話,給我點下star也是極好的。下面演示一下searchApi,這個Api是我們經常需要使用的,特別重要。

  • 2.3.5 搜索Es服務器數據

    ES的搜索API包含很多,比如說組合搜索,區間搜索,高亮显示,分詞搜索等等。我先給大家演示一下組合搜索,區間搜索其實也是組合搜索的一個子條件,其他的搜索其實也都是,代碼如下:

    SearchServiceImpl.java

    	@Override
        public List<Student> searchRange(Object from, Object to, String field, String index) {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            // 需要搜索的區間字段field
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(field);
            // 左區間
            if (from != null) {
                rangeQueryBuilder.from(from, true);
            }
            // 右區間
            if (to != null) {
                rangeQueryBuilder.to(to, true);
            }
            boolQueryBuilder.must();
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQueryBuilder);
            SearchRequest searchRequest = new SearchRequest(index);
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }
    

    上面的代碼其實很簡單,就是一個區間查詢構建器,查詢指定字段處於區間的所有數據,rangeQueryBuilder.from(from, true)的第一個參數就是字段的下邊界,第二個參數代表是否包含邊界。SearchResponse就是搜索的響應對象,所有的數據都在SearchHit對象中。

    接下來給大家演示一些組合查詢,這個方法搜索年齡在18到19歲並且班級為’G0305’的學生。記得ES默認是分頁的,如果想不分頁,一定要記得給搜索字段加上.keyword(字符串加,数字不支持)。

    SearchServiceImpl.java

    @Override
        public List<Student> searchBool() {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            boolQuery.must(QueryBuilders.rangeQuery("age").gte(18).lte(19));
            boolQuery.must(QueryBuilders.termQuery("clazz" + Constants.KEYWORD,"G0305"));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQuery);
            SearchRequest searchRequest = new SearchRequest(Constants.INDEX_STUDENT);
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }
    

    上面的代碼中的類BoolQueryBuilder就是組合查詢構建器,這個類可以用來構建組合的條件查詢。boolQuery.must()方法就是用來拼接條件的一種方式,使用這個方法代表必須滿足這個條件才會查詢出來,上面的代碼說明必須滿足年齡為18(包含18)到19(包含19)歲,並且班級為’G0305’的學生才會查詢出來。還有其他的一些常見的組合查詢方法,如下:

    • boolQuery.must():必須滿足此條件,相當於=或者&
    • boolQuery.mustNot():必須不滿足此條件,相當於!=
    • boolQuery.should():相當於||或者or
    • boolQuery.filter():過濾。

    然後是聚合查詢,很類似於MySQL中的聚合函數,這個示例我就不再解釋了,代碼註釋很清楚:

    @Override
        public void searchBoolAndAggregation() {
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            boolQuery.must(QueryBuilders.rangeQuery("age").gte(18).lte(19));
            boolQuery.must(QueryBuilders.termQuery("clazz" + Constants.KEYWORD,"G0305"));
            // 聚合分組:按clazz字段分組,並將結果取名為clazz,es默認是分詞的,為了精確配置,需要加上‘.keyword’關鍵詞後綴。
            TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("clazz").field("clazz" + Constants.KEYWORD);
            // 聚合求和:求符合查詢條件的學生的年齡的和,並將結果取名為ageSum,因為不是字符串,所以默認是精確匹配,不支持分詞。
            aggregationBuilder.subAggregation(AggregationBuilders.sum("ageSum").field("age"));
            // 聚合求平均:求符合查詢條件的學生的年齡的平均值,並將結果取名為ageAvg,因為不是字符串,所以默認是精確匹配,不支持分詞。
            aggregationBuilder.subAggregation(AggregationBuilders.avg("ageAvg").field("age"));
            // 聚合求數量:按學號查詢符合查詢條件的學生個數,並將結果取名為count,es默認是分詞的,為了精確配置,需要加上‘.keyword’關鍵詞後綴。
            aggregationBuilder.subAggregation(AggregationBuilders.count("count").field("studentNo" + Constants.KEYWORD));
            SearchSourceBuilder builder = new SearchSourceBuilder();
            builder.query(boolQuery);
            builder.aggregation(aggregationBuilder);
            // 按年齡降序排序。
            builder.sort("age", SortOrder.DESC);
            SearchRequest request = new SearchRequest("student_info");
            request.source(builder);
            try {
                SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    System.out.println(student);
                }
                // 使用Terms對象接收
                Terms clazz = search.getAggregations().get("clazz");
                for (Terms.Bucket bucket : clazz.getBuckets()) {
                    System.out.println(bucket.getDocCount());
    
                    System.out.println("=====================");
                    // 使用ParsedSum對象接收
                    ParsedSum ageCount = bucket.getAggregations().get("ageSum");
                    System.out.println(ageCount.getType());
                    System.out.println(ageCount.getValue());
                    System.out.println(ageCount.getValueAsString());
                    System.out.println(ageCount.getMetaData());
                    System.out.println(ageCount.getName());
    
                    System.out.println("=====================");
                    // 使用ParsedAvg對象接收
                    ParsedAvg ageAvg = bucket.getAggregations().get("ageAvg");
                    System.out.println(ageAvg.getType());
                    System.out.println(ageAvg.getValue());
                    System.out.println(ageAvg.getValueAsString());
                    System.out.println(ageAvg.getMetaData());
                    System.out.println(ageAvg.getName());
    
                    System.out.println("=====================");
                    // 使用ParsedValueCount對象接收
                    ParsedValueCount count = bucket.getAggregations().get("count");
                    System.out.println(count.getType());
                    System.out.println(count.getValue());
                    System.out.println(count.getValueAsString());
                    System.out.println(count.getMetaData());
                    System.out.println(count.getName());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

    最後還有分詞查詢,分詞查詢就不加.keyword關鍵字即可。

    @Override
        public List<Student> searchMatch(String matchStudentName) {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            // 分詞查詢時不加'.keyword'關鍵字
            boolQueryBuilder.must(QueryBuilders.matchQuery("studentName",matchStudentName));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQueryBuilder);
            SearchRequest searchRequest = new SearchRequest("student_info");
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits().getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }
    

    請記住,一般的進行分詞都是字符串才進行分詞搜索,数字等類型只能是精準匹配。

    最後,ES功能很強大,作為搜索界的扛把子,ES的功能遠遠不止這些,它還可以高亮搜索,數據分析等等。我在這裏演示的僅僅只是皮毛,甚至都不是皮毛,僅作為初學者的參考。如有大佬覺得我哪裡寫錯了,或者有不同見解,歡迎留言。

3. 項目地址

本項目傳送門:

  • GitHub —> spring-boot-elasticsearch
  • Gitee —> spring-boot-elasticsearch

此教程會一直更新下去,覺得博主寫的可以的話,關注一下,也可以更方便下次來學習。

  • 作者:Butterfly-Tri
  • 出處:Butterfly-Tri個人博客
  • 版權所有,歡迎保留原文鏈接進行轉載

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

分類
發燒車訊

03 . Prometheus監控容器和HTTP探針應用

Eeporter是什麼及來源?

是什麼?

廣義上講所有可以向Prometheus提供監控樣本數據的程序都可以被稱為一個Exporter。而Exporter的一個實例稱為target,如下所示,Prometheus通過輪詢的方式定期從這些target中獲取樣本數據:

來源有哪些?

社區提供的

Prometheus社區提供了豐富的Exporter實現,涵蓋了從基礎設施,中間件以及網絡等各個方面的監控功能。這些Exporter可以實現大部分通用的監控需求。下錶列舉一些社區中常用的Exporter:

範圍 常用Exporter
數據庫 MySQL Exporter, Redis Exporter, MongoDB Exporter, MSSQL Exporter等
硬件 Apcupsd Exporter,IoT Edison Exporter, IPMI Exporter, Node Exporter等
消息隊列 Beanstalkd Exporter, Kafka Exporter, NSQ Exporter, RabbitMQ Exporter等
存儲 Ceph Exporter, Gluster Exporter, HDFS Exporter, ScaleIO Exporter等
HTTP服務 Apache Exporter, HAProxy Exporter, Nginx Exporter等
API服務 AWS ECS Exporter, Docker Cloud Exporter, Docker Hub Exporter, GitHub Exporter等
日誌 Fluentd Exporter, Grok Exporter等
監控系統 Collectd Exporter, Graphite Exporter, InfluxDB Exporter, Nagios Exporter, SNMP Exporter等
其他 Blockbox Exporter, JIRA Exporter, Jenkins Exporter, Confluence Exporter等

用戶自定義的

除了直接使用社區提供的Exporter程序以外,用戶還可以基於Prometheus提供的Client Library創建自己的Exporter程序,目前Promthues社區官方提供了對以下編程語言的支持:Go、Java/Scala、Python、Ruby。同時還有第三方實現的如:Bash、C++、Common Lisp、Erlang,、Haskeel、Lua、Node.js、PHP、Rust等。

Exporter的運行方式

從Exporter的運行方式來講,又可以分為

獨立使用的

以我們已經使用過的Node Exporter為例,由於操作系統本身並不直接支持Prometheus,同時用戶也無法通過直接從操作系統層面上提供對Prometheus的支持。因此,用戶只能通過獨立運行一個程序的方式,通過操作系統提供的相關接口,將系統的運行狀態數據轉換為可供Prometheus讀取的監控數據。 除了Node Exporter以外,比如MySQL Exporter、Redis Exporter等都是通過這種方式實現的。 這些Exporter程序扮演了一个中間代理人的角色。

集成到應用中的

為了能夠更好的監控系統的內部運行狀態,有些開源項目如Kubernetes,ETCD等直接在代碼中使用了Prometheus的Client Library,提供了對Prometheus的直接支持。這種方式打破的監控的界限,讓應用程序可以直接將內部的運行狀態暴露給Prometheus,適合於一些需要更多自定義監控指標需求的項目。

Exporter規範

所有的Exporter程序都需要按照Prometheus的規範,返回監控的樣本數據。以Node Exporter為例,當訪問/metrics地址時會返回以下內容:

# HELP node_cpu Seconds the cpus spent in each mode.
# TYPE node_cpu counter
node_cpu{cpu="cpu0",mode="idle"} 362812.7890625
# HELP node_load1 1m load average.
# TYPE node_load1 gauge
node_load1 3.0703125

這是一種基於文本的格式規範,在Prometheus 2.0之前的版本還支持Protocol buffer規範。相比於Protocol buffer文本具有更好的可讀性,以及跨平台性。Prometheus 2.0的版本也已經不再支持Protocol buffer。

Exporter返回的樣本數據,主要由三個部分組成:樣本的一般註釋信息(HELP),樣本的類型註釋信息(TYPE)和樣本。Prometheus會對Exporter響應的內容逐行解析:

如果當前行以# HELP開始,Prometheus將會按照以下規則對內容進行解析,得到當前的指標名稱以及相應的說明信息:

# HELP <metrics_name> <doc_string>

如果當前行以# TYPE開始,Prometheus會按照以下規則對內容進行解析,得到當前的指標名稱以及指標類型:

# TYPE <metrics_name> <metrics_type>

TYPE註釋行必須出現在指標的第一個樣本之前。如果沒有明確的指標類型需要返回為untyped。 除了# 開頭的所有行都會被視為是監控樣本數據。 每一行樣本需要滿足以下格式規範:

metric_name [
  "{" label_name "=" `"` label_value `"` { "," label_name "=" `"` label_value `"` } [ "," ] "}"
] value [ timestamp ]

其中metric_name和label_name必須遵循PromQL的格式規範要求。value是一個float格式的數據,timestamp的類型為int64(從1970-01-01 00:00:00以來的毫秒數),timestamp為可選默認為當前時間。具有相同metric_name的樣本必須按照一個組的形式排列,並且每一行必須是唯一的指標名稱和標籤鍵值對組合。

需要特別注意的是對於histogram和summary類型的樣本。需要按照以下約定返回樣本數據:

1 . 類型為summary或者histogram的指標x,該指標所有樣本的值的總和需要使用一個單獨的x_sum指標表示

2 . 類型為summary或者histogram的指標x,該指標所有樣本的總數需要使用一個單獨的x_count指標表示。

3 . 對於類型為summary的指標x,其不同分位數quantile所代表的樣本,需要使用單獨的x{quantile=”y”}表示。

4 . 對於類型histogram的指標x為了表示其樣本的分佈情況,每一個分佈需要使用x_bucket{le=”y”}表示,其中y為當前分佈的上位數。同時必須包含一個樣本x_bucket{le=”+Inf”},並且其樣本值必須和x_count相同。

5 . 對於histogram和summary的樣本,必須按照分位數quantile和分佈le的值的遞增順序排序。

以下是類型為histogram和summary的樣本輸出示例

# A histogram, which has a pretty complex representation in the text format:
# HELP http_request_duration_seconds A histogram of the request duration.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320

# Finally a summary, which has a complex representation, too:
# HELP rpc_duration_seconds A summary of the RPC duration in seconds.
# TYPE rpc_duration_seconds summary
rpc_duration_seconds{quantile="0.01"} 3102
rpc_duration_seconds{quantile="0.05"} 3272
rpc_duration_seconds{quantile="0.5"} 4773
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693

指定樣式格式的版本
在Exporter響應的HTTP頭信息中,可以通過Content-Type指定特定的規範版本,例如:

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 2906
Content-Type: text/plain; version=0.0.4
Date: Sat, 17 Mar 2018 08:47:06 GMT

其中version用於指定Text-based的格式版本,當沒有指定版本的時候,默認使用最新格式規範的版本。同時HTTP響應頭還需要指定壓縮格式為gzip。

容器監控

Docker是一個開源的應用容器引擎,讓開發者可以打包他們的應用以及依賴包到一個可移植的容器中,然後發布到任何流行的Linux/Windows/Mac機器上。容器鏡像正成為一個新的標準化軟件交付方式。

例如,可以通過一下命令快速在本地啟動一個Nginx服務:

安裝docker
# 安裝一些必要的系統工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 添加軟件源信息
# docker 官方源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# 阿里雲源
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

sudo yum makecache fast

# CentOS7安裝 Docker-ce
yum -y install docker-ce   


mkdir /etc/docker
vim /etc/docker/daemon.json
{
"registry-mirrors": ["https://registry.docker-cn.com"]
}

# 啟動Docker後台服務
systemctl start docker && systemctl enable docker
systemctl daemon-reload                 # 守護進程重啟

# 運行一個nginx做測試
docker run -itd nginx

為了能夠獲取到Docker容器的運行狀態,用戶可以通過Docker的stats命令獲取到當前主機上運行容器的統計信息,可以查看容器的CPU利用率、內存使用量、網絡IO總量以及磁盤IO總量等信息。

docker stats
CONTAINER           CPU %      MEM USAGE / LIMIT     MEM %      NET I/O         BLOCK I/O   PIDS
9a1648bec3b2        0.30%      196KiB / 3.855GiB     0.00%      828B / 0B       827kB / 0B  1
# 除了使用命令以外,用戶還可以通過docker提供的http api查看容器的監控統計信息.

使用CAdvisor

CAdvisor是Google開源的一款用於展示和分析容器運行狀態的可視化工具。通過在主機上運行CAdvisor用戶可以輕鬆的獲取到當前主機上容器的運行統計信息,並以圖表的形式向用戶展示。

在本地運行CAdvisor也非常簡單,直接運行一下命令即可:

docker run \
  --volume=/:/rootfs:ro \
  --volume=/var/run:/var/run:rw \
  --volume=/sys:/sys:ro \
  --volume=/var/lib/docker/:/var/lib/docker:ro \
  --publish=8080:8080 \
  --detach=true \
  --name=cadvisor \
  google/cadvisor:latest
# 通過訪問http://localhost:8080可以查看,當前主機上容器的運行狀態.

CAdvisor是一個簡單易用的工具,相比於使用Docker命令行工具,用戶不用再登錄到服務器中即可以可視化圖表的形式查看主機上所有容器的運行狀態。

而在多主機的情況下,在所有節點上運行一個CAdvisor再通過各自的UI查看監控信息顯然不太方便,同時CAdvisor默認只保存2分鐘的監控數據。好消息是CAdvisor已經內置了對Prometheus的支持。訪問http://localhost:8080/metrics即可獲取到標準的Prometheus監控樣本輸出:

下面列舉了一些CAdvisor中獲取的典型監控指標

指標名稱 類型 含義
gauge 再過去10秒內容器CPU的平均負載
container_cpu_usage_seconds_total
指標名稱 類型 含義
container_cpu_load_average_10s gauge 過去10秒內容器CPU的平均負載
container_cpu_usage_seconds_total counter 容器在每個CPU內核上的累積佔用時間 (單位:秒)
container_cpu_system_seconds_total counter System CPU累積佔用時間(單位:秒)
container_cpu_user_seconds_total counter User CPU累積佔用時間(單位:秒)
container_fs_usge_bytes gauge 容器中文件系統的使用量(單位:字節)
container_network_receive_bytes_total counter 容器網絡累計接受數據總量(單位: 字節)
container_network_transmit_bytes_total counter 容器網絡累計傳輸數據總量(單位: 字節)

與Prometheus集成

修改/etc/prometheus/prometheus.yml,將cAdvisor添加監控數據採集任務目標當中:

  - job_name: 'docker'
    static_configs:
    - targets: ['172.19.0.27:8080']

systemctl restart prometheus

啟動Prometheus服務,可以在Prometheus UI中看到當前所有的Target狀態:

當能夠正常採集到cAdvisor的樣本數據后,可以通過一下錶達式計算容器的CPU使用率.

sum(irate(container_cpu_usage_seconds_total{image!=""}[1m])) without (cpu)

查詢容器內存使用量(單位: 字節)

container_memory_usage_bytes{image!=""}

查詢容器網絡接收量速率(單位: 字節/秒)

sum(rate(container_network_receive_bytes_total{image!=""}[1m])) without (interface)

查詢容器網絡傳輸量速率

sum(rate(container_network_transmit_bytes_total{image!=""}[1m])) without (interface)

查詢容器文件系統讀取速率

sum(rate(container_fs_reads_bytes_total{image!=""}[1m])) without (device)

# 為了方便看出效果,我們使用dd命令
docker exec -it 628d /bin/bash
dd if=/dev/zero of=test bs=1M count=1000

  • 查詢容器文件系統寫入速率(單位: 字節/秒)
sum(rate(container_fs_writes_bytes_total{image!=""}[1m])) without (device)

Prometheus網絡探測

接下來我們主要介紹Prometheus下如何進行白盒監控,我們之前監控主機的資源用量、容器的運行狀態、數據庫中間件的運行數據。 這些都是支持業務和服務的基礎設施,通過白盒能夠了解其內部的實際運行狀態,通過對監控指標的觀察能夠預判可能出現的問題,從而對潛在的不確定因素進行優化。而從完整的監控邏輯的角度,除了大量的應用白盒監控以外,還應該添加適當的黑盒監控。
黑盒監控即以用戶的身份測試服務的外部可見性,常見的黑盒監控包括HTTP探針、TCP探針等用於檢測站點或者服務的可訪問性,以及訪問效率等。

黑盒監控相較於白盒監控最大的不同在於黑盒監控是以故障為導向當故障發生時,黑盒監控能快速發現故障,而白盒監控則側重於主動發現或者預測潛在的問題。一個完善的監控目標是要能夠從白盒的角度發現潛在問題,能夠在黑盒的角度快速發現已經發生的問題。

安裝Blackbox Exporter

Blackbox Exporter是Prometheus社區提供的官方黑盒監控解決方案,其允許用戶通過:HTTP、HTTPS、DNS、TCP以及ICMP的方式對網絡進行探測。用戶可以直接使用go get命令獲取Blackbox Exporter源碼並生成本地可執行文件:

下載安裝blackbox_exporter

wget https://github.com/prometheus/blackbox_exporter/releases/download/v0.16.0/blackbox_exporter-0.16.0.linux-amd64.tar.gz

tar xvf blackbox_exporter-0.16.0.linux-amd64.tar.gz -C /usr/local/prometheus/
mv blackbox_exporter-0.16.0.linux-amd64/ blackbox_exporter
useradd prometheus
chown -R prometheus:prometheus /usr/local/prometheus/

vim /usr/lib/systemd/system/blackbox_exporter.service
[Unit]
Description=blackbox_exporter
After=network.target

[Service]
Type=simple
User=prometheus
ExecStart=/usr/local/prometheus/blackbox_exporter/blackbox_exporter --config.file=/usr/local/prometheus/blackbox_exporter/blackbox.yml
Restart=on-failure

[Install]
WantedBy=multi-user.target

systemctl enable blackbox_exporter.service
systemctl start blackbox_exporter.service

運行Blackbox Exporter時,需要用戶提供探針的配置信息,這些配置信息可能是一些自定義的HTTP頭信息,也可能是探測時需要的一些TSL配置,也可能是探針本身的驗證行為。在Blackbox Exporter每一個探針配置稱為一個module,並且以YAML配置文件的形式提供給Blackbox Exporter。 每一個module主要包含以下配置內容,包括探針類型(prober)、驗證訪問超時時間(timeout)、以及當前探針的具體配置項:

# 探針類型:http、 tcp、 dns、 icmp.
prober: <prober_string>
# 超時時間
[ timeout: <duration> ]
# 探針的詳細配置,最多只能配置其中的一個
[ http: <http_probe> ]
[ tcp: <tcp_probe> ]
[ dns: <dns_probe> ]
[ icmp: <icmp_probe> ]

下面是一個簡化的探針配置文件blockbox.yml,包含兩個HTTP探針配置項

modules:
  http_2xx:
    prober: http
    http:
      method: GET
  http_post_2xx:
    prober: http
    http:
      method: POST

通過運行一下命令,並指定使用的探針設置文件啟動Blockbox Exporter實例:

blackbox_exporter --config.file=/etc/prometheus/blackbox.yml
or
systemctl restart blackbox_exporter.service

啟動成功后,就可以通過訪問http://172.19.0.27:9115/probe?module=http_2xx&target=baidu.com對baidu.com進行探測。這裏通過在URL中提供module參數指定了當前使用的探針,target參數指定探測目標,探針的探測結果通過Metrics的形式返回:

# HELP probe_dns_lookup_time_seconds Returns the time taken for probe dns lookup in seconds
# TYPE probe_dns_lookup_time_seconds gauge
probe_dns_lookup_time_seconds 0.004359875
# HELP probe_duration_seconds Returns how long the probe took to complete in seconds
# TYPE probe_duration_seconds gauge
probe_duration_seconds 0.046153996
# HELP probe_failed_due_to_regex Indicates if probe failed due to regex
# TYPE probe_failed_due_to_regex gauge
probe_failed_due_to_regex 0
# HELP probe_http_content_length Length of http content response
# TYPE probe_http_content_length gauge
probe_http_content_length 81
# HELP probe_http_duration_seconds Duration of http request by phase, summed over all redirects
# TYPE probe_http_duration_seconds gauge
probe_http_duration_seconds{phase="connect"} 0.00105657
probe_http_duration_seconds{phase="processing"} 0.039457402
probe_http_duration_seconds{phase="resolve"} 0.004359875
probe_http_duration_seconds{phase="tls"} 0
probe_http_duration_seconds{phase="transfer"} 0.000337184
# HELP probe_http_last_modified_timestamp_seconds Returns the Last-Modified HTTP \
response header in unixtime
# TYPE probe_http_last_modified_timestamp_seconds gauge
probe_http_last_modified_timestamp_seconds 1.26330408e+09
# HELP probe_http_redirects The number of redirects
# TYPE probe_http_redirects gauge
probe_http_redirects 0
# HELP probe_http_ssl Indicates if SSL was used for the final redirect
# TYPE probe_http_ssl gauge
probe_http_ssl 0
# HELP probe_http_status_code Response HTTP status code
# TYPE probe_http_status_code gauge
probe_http_status_code 200
# HELP probe_http_uncompressed_body_length Length of uncompressed response body
# TYPE probe_http_uncompressed_body_length gauge
probe_http_uncompressed_body_length 81
# HELP probe_http_version Returns the version of HTTP of the probe response
# TYPE probe_http_version gauge
probe_http_version 1.1
# HELP probe_ip_protocol Specifies whether probe ip protocol is IP4 or IP6
# TYPE probe_ip_protocol gauge
probe_ip_protocol 4
# HELP probe_success Displays whether or not the probe was a success
# TYPE probe_success gauge
probe_success 1

從返回的樣本中,用戶可以獲取站點的DNS解析耗時,站點響應時間,HTTP響應狀態碼等等和站點訪問質量相關的監控指標,從而幫助管理員主動的發現故障和問題.

Prometheus集成

接下來,只需要在Prometheus下配置對Blockbox Exporter實例的採集任務即可、最直觀的配置方式.

  - job_name: 'baidu_http2xx_probe'
    params:
      module:
      - http_2xx
      target:
      - baidu.com
    metrics_path: /probe
    static_configs:
    - targets: ['172.19.0.27:9115']

  - job_name: 'prometheus_http2xx_probe'
    params:
      module:
      - http_2xx
      target:
      - prometheus.io
    metrics_path: /probe
    static_configs:
    - targets: ['172.19.0.27:9115']

systemctl restart prometheus

這裏分別配置了名為baidu_http2x_probe和prometheus_http2xx_probe的採集任務,並且通過params指定使用的探針(module)以及探測目標(target).

那問題就來了,假如我們有N個目標站點且都需要M種探測方式,那麼Prometheus中將包含N * M個採集任務,從配置管理的角度來說顯然是不可接受的。這裏我們也可以採用Relabling的方式對這些配置進行簡化,配置方式如下:

  - job_name: 'blackbox'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
        - http://prometheus.io    # Target to probe with http.
        - https://prometheus.io   # Target to probe with https.
        - http://example.com:8080 # Target to probe with http on port 8080.
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 172.19.0.27:9115

這裏針對每一個探針服務(如http_2xx)定義一個採集任務,並且直接將任務的採集目標定義為我們需要探測的站點,在採集樣本數據之前通過relabel_configs對採集任務進行動態配置.

* 第一步, 根據Target實例的地址,寫入__param_target標籤中,__param_<name>形式的標籤來表示,
	# 在採集任務時會在請求目標地址中添加<name>參數,等同於params的設置.
* 第二步,  獲取__param_target的值,並覆寫到instance標籤中.
* 第三步,  覆寫Target實例的__address__標籤值為BlockBox Exporter實例的訪問地址.

HTTP探針

HTTP探針是進行黑盒監控時最常用的探針之一,通過HTTP探針能夠網站或者HTTP服務建立有效的監控,包括其本身的可用性,以及用戶體驗相關的如響應時間等等。除了能夠在服務出現異常的時候及時報警,還能幫助系統管理員分析和優化網站體驗。

Blockbox Exporter中所有的探針均是以Module的信息進行配置。如下所示,配置了一個最簡單的HTTP探針:

modules:
  http_2xx_example:
    prober: http
    http:

通過prober配置項指定探針類型。配置項http用於自定義探針的探測方式,這裡有沒對http配置項添加任何配置,表示完全使用HTTP探針的默認配置,該探針將使用HTTP GET的方式對目標服務進行探測,並且驗證返回狀態碼是否為2XX,是則表示驗證成功,否則失敗。

自定義HTTP請求

HTTP服務通常會以不同的形式對外展現,有些可能就是一些簡單的網頁,而有些則可能是一些基於REST的API服務。 對於不同類型的HTTP的探測需要管理員能夠對HTTP探針的行為進行更多的自定義設置,包括:HTTP請求方法、HTTP頭信息、請求參數等。對於某些啟用了安全認證的服務還需要能夠對HTTP探測設置相應的Auth支持。對於HTTPS類型的服務還需要能夠對證書進行自定義設置。

如下所示,這裏通過method定義了探測時使用的請求方法,對於一些需要請求參數的服務,還可以通過headers定義相關的請求頭信息,使用body定義請求內容:

http_post_2xx:
    prober: http
    timeout: 5s
    http:
      method: POST
      headers:
        Content-Type: application/json
      body: '{}'

如果HTTP服務啟用了安全認證,Blockbox Exporter內置了對basic_auth的支持,可以直接設置相關的認證信息即可:

http_basic_auth_example:
    prober: http
    timeout: 5s
    http:
      method: POST
      headers:
        Host: "login.example.com"
      basic_auth:
        username: "username"
        password: "mysecret"

對於使用了Bear Token的服務也可以通過bearer_token配置項直接指定令牌字符串,或者通過bearer_token_file指定令牌文件。

對於一些啟用了HTTPS的服務,但是需要自定義證書的服務,可以通過tls_config指定相關的證書信息:

 http_custom_ca_example:
    prober: http
    http:
      method: GET
      tls_config:
        ca_file: "/certs/my_cert.crt"
  • 自定義探針行為
  • 在默認情況下HTTP探針只會對HTTP返回狀態碼進行校驗,如果狀態碼為2XX(200 <= StatusCode < 300)則表示探測成功,並且探針返回的指標probe_success值為1。
  • 如果用戶需要指定HTTP返回狀態碼,或者對HTTP版本有特殊要求,如下所示,可以使用valid_http_versions和valid_status_codes進行定義:
  http_2xx_example:
    prober: http
    timeout: 5s
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2"]
      valid_status_codes: []

默認情況下,Blockbox返回的樣本數據中也會包含指標probe_http_ssl,用於表明當前探針是否使用了SSL:

# HELP probe_http_ssl Indicates if SSL was used for the final redirect
# TYPE probe_http_ssl gauge
probe_http_ssl 0

而如果用戶對於HTTP服務是否啟用SSL有強制的標準。則可以使用fail_if_ssl和fail_if_not_ssl進行配置。fail_if_ssl為true時,表示如果站點啟用了SSL則探針失敗,反之成功。fail_if_not_ssl剛好相反。

  http_2xx_example:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: []
      method: GET
      no_follow_redirects: false
      fail_if_ssl: false
      fail_if_not_ssl: false

除了基於HTTP狀態碼,HTTP協議版本以及是否啟用SSL作為控制探針探測行為成功與否的標準以外,還可以匹配HTTP服務的響應內容。使用fail_if_matches_regexp和fail_if_not_matches_regexp用戶可以定義一組正則表達式,用於驗證HTTP返回內容是否符合或者不符合正則表達式的內容。

  http_2xx_example:
    prober: http
    timeout: 5s
    http:
      method: GET
      fail_if_matches_regexp:
        - "Could not connect to database"
      fail_if_not_matches_regexp:
        - "Download the latest version here"

最後需要提醒的時,默認情況下HTTP探針會走IPV6的協議。 在大多數情況下,可以使用preferred_ip_protocol=ip4強制通過IPV4的方式進行探測。在Bloackbox響應的監控樣本中,也會通過指標probe_ip_protocol,表明當前的協議使用情況:

# HELP probe_ip_protocol Specifies whether probe ip protocol is IP4 or IP6
# TYPE probe_ip_protocol gauge
probe_ip_protocol 6

除了支持對HTTP協議進行網絡探測以外,Blackbox還支持對TCP、DNS、ICMP等其他網絡協議![]

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

【其他文章推薦】

※回頭車貨運收費標準

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

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

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

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

分類
發燒車訊

最新研究:大氣二氧化碳總量逼近1500萬年來高峰

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

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

【其他文章推薦】

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

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

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

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

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

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

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

分類
發燒車訊

亞馬遜6月毀林創紀錄 巴西太空署總督導下台

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

巴西政府免去國家太空署(INPE)地球觀測站總督導溫海斯(Lubia Vinhas)的職位,免職書由科技部長龐特斯(Marcos Pontes)簽署,刊登在今(13日)的國家公報。

溫海斯接受環球電視(TV Globo)訪問時說,她通過高考進入國家太空署服務23年了,所以就算被免去管理職位,也將繼續待在機關。溫海斯也表示不曉得為何被免職,說自己是通過國家公報才知道被調職。

上週巴西國家太空署公布報告,今年6月份的毀林警報數量創下2015年以來當月最高紀錄。今年上半年累計的警報顯示,亞馬遜遭破壞面積達3069.57平方公里,與2019年上半年相比增加25%。僅6月的毀林警報範圍就達1034.4平方公里。

溫海斯遭免職後,國際環保團體綠色和平組織(Greenpeace)發表聲明表示,基於巴西總統波索納洛(Jair Bolsonaro)政府先前的決定,溫海斯被免職並不意外,只是再次顯示「巴西政府是真理的敵人」。

巴西經濟部長葛德斯(Paulo Guedes)今天在經濟合作暨發展組織(OECD)線上會議中表示,巴西願意配合和幫助保護環境;如果巴西的環境政策出現錯誤或過度行為,也將予以糾正。

生物多樣性
國際新聞
巴西
亞馬遜雨林
森林

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

分類
發燒車訊

邁向碳中合歐盟 擬設統一能源部門

摘錄自2020年7月13日台灣醒報報導

歐洲議會推設立「統一能源部門」,促歐盟各國實現碳中合目標!奧地利的歐洲議員嘉夢日前在一場會議中,提出設立「歐洲統一能源部門」,將協助整合各國現有的能源法規、打造歐盟再生能源中心及促進工業及交通運輸轉型為使用綠色能源,獲得多數議員支持。

歐盟許多成員國包括芬蘭、丹麥已設下2050年以前要實現「碳中合」的目標,即便是將脫歐的英國也曾承諾跟進。根據歐盟調查,為了實現碳中合目標,歐盟整體的用電量將從25%提升到50%,嘉夢說:「實現碳中合,非常需要各國政府機關朝使用綠色能源邁進,因此,我們需要一個部門負責協調整合。」

除了統一各國現有的能源政策及法規外,該部門也將負責打造歐盟再生能源中心、提升各國的太陽能、生物能發電等基礎設施的普及率、研究並部署綠色能源科技,以及協助將乾淨能源的使用範圍擴張到傳統上依賴化石燃料的領域中,例如提倡綠色建築、工業和交通運輸。

能源議題
再生能源
能源轉型
國際新聞
歐盟
碳中和

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

分類
發燒車訊

8000字長文讓你徹底了解 Java 8 的 Lambda、函數式接口、Stream 用法和原理

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。公眾號回復『666』獲取高清大圖。

就在今年 Java 25周歲了,可能比在座的各位中的一些少年年齡還大,但令人遺憾的是,竟然沒有我大,不禁感嘆,Java 還是太小了。(難道我會說是因為我老了?)

而就在上個月,Java 15 的試驗版悄悄發布了,但是在 Java 界一直有個神秘現象,那就是「你發你發任你發,我的最愛 Java 8」.

據 Snyk 和 The Java Magazine 聯合推出發布的 2020 JVM 生態調查報告显示,在所有的 Java 版本中,仍然有 64% 的開發者使用 Java 8。另外一些開發者可能已經開始用 Java 9、Java 11、Java 13 了,當然還有一些神仙開發者還在堅持使用 JDK 1.6 和 1.7。

儘管 Java 8 發布多年,使用者眾多,可神奇的是竟然有很多同學沒有用過 Java 8 的新特性,比如 Lambda表達式、比如方法引用,再比如今天要說的 Stream。其實 Stream 就是以 Lambda 和方法引用為基礎,封裝的簡單易用、函數式風格的 API。

Java 8 是在 2014 年發布的,實話說,風箏我也是在 Java 8 發布后很長一段時間才用的 Stream,因為 Java 8 發布的時候我還在 C# 的世界中掙扎,而使用 Lambda 表達式卻很早了,因為 Python 中用 Lambda 很方便,沒錯,我寫 Python 的時間要比 Java 的時間還長。

要講 Stream ,那就不得不先說一下它的左膀右臂 Lambda 和方法引用,你用的 Stream API 其實就是函數式的編程風格,其中的「函數」就是方法引用,「式」就是 Lambda 表達式。

Lambda 表達式

Lambda 表達式是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象,是一個匿名函數,即沒有函數名的函數。Lambda表達式可以表示閉包。

在 Java 中,Lambda 表達式的格式是像下面這樣

// 無參數,無返回值
() -> log.info("Lambda")

 // 有參數,有返回值
(int a, int b) -> { a+b }

其等價於

log.info("Lambda");

private int plus(int a, int b){
  	return a+b;
}

最常見的一個例子就是新建線程,有時候為了省事,會用下面的方法創建並啟動一個線程,這是匿名內部類的寫法,new Thread需要一個 implements 自Runnable類型的對象實例作為參數,比較好的方式是創建一個新類,這個類 implements Runnable,然後 new 出這個新類的實例作為參數傳給 Thread。而匿名內部類不用找對象接收,直接當做參數。

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("快速新建並啟動一個線程");
    }
}).run();

但是這樣寫是不是感覺看上去很亂、很土,而這時候,換上 Lambda 表達式就是另外一種感覺了。

new Thread(()->{
    System.out.println("快速新建並啟動一個線程");
}).run();

怎麼樣,這樣一改,瞬間感覺清新脫俗了不少,簡潔優雅了不少。

Lambda 表達式簡化了匿名內部類的形式,可以達到同樣的效果,但是 Lambda 要優雅的多。雖然最終達到的目的是一樣的,但其實內部的實現原理卻不相同。

匿名內部類在編譯之後會創建一個新的匿名內部類出來,而 Lambda 是調用 JVM invokedynamic指令實現的,並不會產生新類。

方法引用

方法引用的出現,使得我們可以將一個方法賦給一個變量或者作為參數傳遞給另外一個方法。::雙冒號作為方法引用的符號,比如下面這兩行語句,引用 Integer類的 parseInt方法。

Function<String, Integer> s = Integer::parseInt;
Integer i = s.apply("10");

或者下面這兩行,引用 Integer類的 compare方法。

Comparator<Integer> comparator = Integer::compare;
int result = comparator.compare(100,10);

再比如,下面這兩行代碼,同樣是引用 Integer類的 compare方法,但是返回類型卻不一樣,但卻都能正常執行,並正確返回。

IntBinaryOperator intBinaryOperator = Integer::compare;
int result = intBinaryOperator.applyAsInt(10,100);

相信有的同學看到這裏恐怕是下面這個狀態,完全不可理喻嗎,也太隨便了吧,返回給誰都能接盤。

先別激動,來來來,現在咱們就來解惑,解除蒙圈臉。

Q:什麼樣的方法可以被引用?

A:這麼說吧,任何你有辦法訪問到的方法都可以被引用。

Q:返回值到底是什麼類型?

A:這就問到點兒上了,上面又是 Function、又是Comparator、又是 IntBinaryOperator的,看上去好像沒有規律,其實不然。

返回的類型是 Java 8 專門定義的函數式接口,這類接口用 @FunctionalInterface 註解。

比如 Function這個函數式接口的定義如下:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

還有很關鍵的一點,你的引用方法的參數個數、類型,返回值類型要和函數式接口中的方法聲明一一對應才行。

比如 Integer.parseInt方法定義如下:

public static int parseInt(String s) throws NumberFormatException {
    return parseInt(s,10);
}

首先parseInt方法的參數個數是 1 個,而 Function中的 apply方法參數個數也是 1 個,參數個數對應上了,再來,apply方法的參數類型和返回類型是泛型類型,所以肯定能和 parseInt方法對應上。

這樣一來,就可以正確的接收Integer::parseInt的方法引用,並可以調用Funcitonapply方法,這時候,調用到的其實就是對應的 Integer.parseInt方法了。

用這套標準套到 Integer::compare方法上,就不難理解為什麼即可以用 Comparator<Integer>接收,又可以用 IntBinaryOperator接收了,而且調用它們各自的方法都能正確的返回結果。

Integer.compare方法定義如下:

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

返回值類型 int,兩個參數,並且參數類型都是 int

然後來看ComparatorIntBinaryOperator它們兩個的函數式接口定義和其中對應的方法:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}

對不對,都能正確的匹配上,所以前面示例中用這兩個函數式接口都能正常接收。其實不止這兩個,只要是在某個函數式接口中聲明了這樣的方法:兩個參數,參數類型是 int或者泛型,並且返回值是 int或者泛型的,都可以完美接收。

JDK 中定義了很多函數式接口,主要在 java.util.function包下,還有 java.util.Comparator 專門用作定製比較器。另外,前面說的 Runnable也是一個函數式接口。

自己動手實現一個例子

1. 定義一個函數式接口,並添加一個方法

定義了名稱為 KiteFunction 的函數式接口,使用 @FunctionalInterface註解,然後聲明了具有兩個參數的方法 run,都是泛型類型,返回結果也是泛型。

還有一點很重要,函數式接口中只能聲明一個可被實現的方法,你不能聲明了一個 run方法,又聲明一個 start方法,到時候編譯器就不知道用哪個接收了。而用default 關鍵字修飾的方法則沒有影響。

@FunctionalInterface
public interface KiteFunction<T, R, S> {

    /**
     * 定義一個雙參數的方法
     * @param t
     * @param s
     * @return
     */
    R run(T t,S s);
}

2. 定義一個與 KiteFunction 中 run 方法對應的方法

在 FunctionTest 類中定義了方法 DateFormat,一個將 LocalDateTime類型格式化為字符串類型的方法。

public class FunctionTest {
    public static String DateFormat(LocalDateTime dateTime, String partten) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
        return dateTime.format(dateTimeFormatter);
    }
}

3.用方法引用的方式調用

正常情況下我們直接使用 FunctionTest.DateFormat()就可以了。

而用函數式方式,是這樣的。

KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat;
String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");

而其實我可以不專門在外面定義 DateFormat這個方法,而是像下面這樣,使用匿名內部類。

public static void main(String[] args) throws Exception {
  
    String dateString = new KiteFunction<LocalDateTime, String, String>() {
        @Override
        public String run(LocalDateTime localDateTime, String s) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);
            return localDateTime.format(dateTimeFormatter);
        }
    }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
    System.out.println(dateString);
}

前面第一個 Runnable的例子也提到了,這樣的匿名內部類可以用 Lambda 表達式的形式簡寫,簡寫后的代碼如下:

public static void main(String[] args) throws Exception {

        KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
            return dateTime.format(dateTimeFormatter);
        };
        String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
        System.out.println(dateString);
}

使用(LocalDateTime dateTime, String partten) -> { } 這樣的 Lambda 表達式直接返回方法引用。

Stream API

為了說一下 Stream API 的使用,可以說是大費周章啊,知其然,也要知其所以然嗎,追求技術的態度和姿勢要正確。

當然 Stream 也不只是 Lambda 表達式就厲害了,真正厲害的還是它的功能,Stream 是 Java 8 中集合數據處理的利器,很多本來複雜、需要寫很多代碼的方法,比如過濾、分組等操作,往往使用 Stream 就可以在一行代碼搞定,當然也因為 Stream 都是鏈式操作,一行代碼可能會調用好幾個方法。

Collection接口提供了 stream()方法,讓我們可以在一個集合方便的使用 Stream API 來進行各種操作。值得注意的是,我們執行的任何操作都不會對源集合造成影響,你可以同時在一個集合上提取出多個 stream 進行操作。

我們看 Stream 接口的定義,繼承自 BaseStream,機會所有的接口聲明都是接收方法引用類型的參數,比如 filter方法,接收了一個 Predicate類型的參數,它就是一個函數式接口,常用來作為條件比較、篩選、過濾用,JPA中也使用了這個函數式接口用來做查詢條件拼接。

public interface Stream<T> extends BaseStream<T, Stream<T>> {
  
  Stream<T> filter(Predicate<? super T> predicate);
  
  // 其他接口
}  

下面就來看看 Stream 常用 API。

of

可接收一個泛型對象或可變成泛型集合,構造一個 Stream 對象。

private static void createStream(){
    Stream<String> stringStream = Stream.of("a","b","c");
}

empty

創建一個空的 Stream 對象。

concat

連接兩個 Stream ,不改變其中任何一個 Steam 對象,返回一個新的 Stream 對象。

private static void concatStream(){
    Stream<String> a = Stream.of("a","b","c");
    Stream<String> b = Stream.of("d","e");
    Stream<String> c = Stream.concat(a,b);
}

max

一般用於求数字集合中的最大值,或者按實體中数字類型的屬性比較,擁有最大值的那個實體。它接收一個 Comparator<T>,上面也舉到這個例子了,它是一個函數式接口類型,專門用作定義兩個對象之間的比較,例如下面這個方法使用了 Integer::compareTo這個方法引用。

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Integer max = integerStream.max(Integer::compareTo).get();
    System.out.println(max);
}

當然,我們也可以自己定製一個 Comparator,順便複習一下 Lambda 表達式形式的方法引用。

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Comparator<Integer> comparator =  (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1);
    Integer max = integerStream.max(comparator).get();
    System.out.println(max);
}

min

與 max 用法一樣,只不過是求最小值。

findFirst

獲取 Stream 中的第一個元素。

findAny

獲取 Stream 中的某個元素,如果是串行情況下,一般都會返回第一個元素,并行情況下就不一定了。

count

返回元素個數。

Stream<String> a = Stream.of("a", "b", "c");
long x = a.count();

peek

建立一個通道,在這個通道中對 Stream 的每個元素執行對應的操作,對應 Consumer<T>的函數式接口,這是一個消費者函數式接口,顧名思義,它是用來消費 Stream 元素的,比如下面這個方法,把每個元素轉換成對應的大寫字母並輸出。

private static void peek() {
    Stream<String> a = Stream.of("a", "b", "c");
    List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList());
}

forEach

和 peek 方法類似,都接收一個消費者函數式接口,可以對每個元素進行對應的操作,但是和 peek 不同的是,forEach 執行之後,這個 Stream 就真的被消費掉了,之後這個 Stream 流就沒有了,不可以再對它進行後續操作了,而 peek操作完之後,還是一個可操作的 Stream 對象。

正好藉著這個說一下,我們在使用 Stream API 的時候,都是一串鏈式操作,這是因為很多方法,比如接下來要說到的 filter方法等,返回值還是這個 Stream 類型的,也就是被當前方法處理過的 Stream 對象,所以 Stream API 仍然可以使用。

private static void forEach() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.forEach(e->System.out.println(e.toUpperCase()));
}

forEachOrdered

功能與 forEach是一樣的,不同的是,forEachOrdered是有順序保證的,也就是對 Stream 中元素按插入時的順序進行消費。為什麼這麼說呢,當開啟并行的時候,forEachforEachOrdered的效果就不一樣了。

Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEach(e->System.out.println(e.toUpperCase()));

當使用上面的代碼時,輸出的結果可能是 B、A、C 或者 A、C、B或者A、B、C,而使用下面的代碼,則每次都是 A、 B、C

Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));

limit

獲取前 n 條數據,類似於 MySQL 的limit,只不過只能接收一個參數,就是數據條數。

private static void limit() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.limit(2).forEach(e->System.out.println(e));
}

上述代碼打印的結果是 a、b。

skip

跳過前 n 條數據,例如下面代碼,返回結果是 c。

private static void skip() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.skip(2).forEach(e->System.out.println(e));
}

distinct

元素去重,例如下面方法返回元素是 a、b、c,將重複的 b 只保留了一個。

private static void distinct() {
    Stream<String> a = Stream.of("a", "b", "c","b");
    a.distinct().forEach(e->System.out.println(e));
}

sorted

有兩個重載,一個無參數,另外一個有個 Comparator類型的參數。

無參類型的按照自然順序進行排序,只適合比較單純的元素,比如数字、字母等。

private static void sorted() {
    Stream<String> a = Stream.of("a", "c", "b");
    a.sorted().forEach(e->System.out.println(e));
}

有參數的需要自定義排序規則,例如下面這個方法,按照第二個字母的大小順序排序,最後輸出的結果是 a1、b3、c6。

private static void sortedWithComparator() {
    Stream<String> a = Stream.of("a1", "c6", "b3");
    a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e));
}

為了更好的說明接下來的幾個 API ,我模擬了幾條項目中經常用到的類似數據,10條用戶信息。

private static List<User> getUserData() {
    Random random = new Random();
    List<User> users = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
        User user = new User();
        user.setUserId(i);
        user.setUserName(String.format("古時的風箏 %s 號", i));
        user.setAge(random.nextInt(100));
        user.setGender(i % 2);
        user.setPhone("18812021111");
        user.setAddress("無");
        users.add(user);
    }
    return users;
}

filter

用於條件篩選過濾,篩選出符合條件的數據。例如下面這個方法,篩選出性別為 0,年齡大於 50 的記錄。

private static void filter(){
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e));

    /**
     *等同於下面這種形式 匿名內部類
     */
//    stream.filter(new Predicate<User>() {
//        @Override
//        public boolean test(User user) {
//            return user.getGender().equals(0) && user.getAge()>50;
//        }
//    }).forEach(e->System.out.println(e));
}

map

map方法的接口方法聲明如下,接受一個 Function函數式接口,把它翻譯成映射最合適了,通過原始數據元素,映射出新的類型。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Function的聲明是這樣的,觀察 apply方法,接受一個 T 型參數,返回一個 R 型參數。用於將一個類型轉換成另外一個類型正合適,這也是 map的初衷所在,用於改變當前元素的類型,例如將 Integer 轉為 String類型,將 DAO 實體類型,轉換為 DTO 實例類型。

當然了,T 和 R 的類型也可以一樣,這樣的話,就和 peek方法沒什麼不同了。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

例如下面這個方法,應該是業務系統的常用需求,將 User 轉換為 API 輸出的數據格式。

private static void map(){
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList());
}

private static UserDto dao2Dto(User user){
    UserDto dto = new UserDto();
    BeanUtils.copyProperties(user, dto);
    //其他額外處理
    return dto;
}

mapToInt

將元素轉換成 int 類型,在 map方法的基礎上進行封裝。

mapToLong

將元素轉換成 Long 類型,在 map方法的基礎上進行封裝。

mapToDouble

將元素轉換成 Double 類型,在 map方法的基礎上進行封裝。

flatMap

這是用在一些比較特別的場景下,當你的 Stream 是以下這幾種結構的時候,需要用到 flatMap方法,用於將原有二維結構扁平化。

  1. Stream<String[]>
  2. Stream<Set<String>>
  3. Stream<List<String>>

以上這三類結構,通過 flatMap方法,可以將結果轉化為 Stream<String>這種形式,方便之後的其他操作。

比如下面這個方法,將List<List<User>>扁平處理,然後再使用 map或其他方法進行操作。

private static void flatMap(){
    List<User> users = getUserData();
    List<User> users1 = getUserData();
    List<List<User>> userList = new ArrayList<>();
    userList.add(users);
    userList.add(users1);
    Stream<List<User>> stream = userList.stream();
    List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList());
}

flatMapToInt

用法參考 flatMap,將元素扁平為 int 類型,在 flatMap方法的基礎上進行封裝。

flatMapToLong

用法參考 flatMap,將元素扁平為 Long 類型,在 flatMap方法的基礎上進行封裝。

flatMapToDouble

用法參考 flatMap,將元素扁平為 Double 類型,在 flatMap方法的基礎上進行封裝。

collection

在進行了一系列操作之後,我們最終的結果大多數時候並不是為了獲取 Stream 類型的數據,而是要把結果變為 List、Map 這樣的常用數據結構,而 collection就是為了實現這個目的。

就拿 map 方法的那個例子說明,將對象類型進行轉換后,最終我們需要的結果集是一個 List<UserDto >類型的,使用 collect方法將 Stream 轉換為我們需要的類型。

下面是 collect接口方法的定義:

<R, A> R collect(Collector<? super T, A, R> collector);

下面這個例子演示了將一個簡單的 Integer Stream 過濾出大於 7 的值,然後轉換成 List<Integer>集合,用的是 Collectors.toList()這個收集器。

private static void collect(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList());
}

很多同學表示看不太懂這個 Collector是怎麼一個意思,來,我們看下面這段代碼,這是 collect的另一個重載方法,你可以理解為它的參數是按順序執行的,這樣就清楚了,這就是個 ArrayList 從創建到調用 addAll方法的一個過程。

private static void collect(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add,
            ArrayList::addAll);
}

我們在自定義 Collector的時候其實也是這個邏輯,不過我們根本不用自定義, Collectors已經為我們提供了很多拿來即用的收集器。比如我們經常用到Collectors.toList()Collectors.toSet()Collectors.toMap()。另外還有比如Collectors.groupingBy()用來分組,比如下面這個例子,按照 userId 字段分組,返回以 userId 為key,List 為value 的 Map,或者返回每個 key 的個數。

// 返回 userId:List<User>
Map<String,List<User>> map = user.stream().collect(Collectors.groupingBy(User::getUserId));

// 返回 userId:每組個數
Map<String,Long> map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));

toArray

collection是返回列表、map 等,toArray是返回數組,有兩個重載,一個空參數,返回的是 Object[]

另一個接收一個 IntFunction<R>類型參數。

@FunctionalInterface
public interface IntFunction<R> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    R apply(int value);
}

比如像下面這樣使用,參數是 User[]::new也就是new 一個 User 數組,長度為最後的 Stream 長度。

private static void toArray() {
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new);
}

reduce

它的作用是每次計算的時候都用到上一次的計算結果,比如求和操作,前兩個數的和加上第三個數的和,再加上第四個數,一直加到最後一個數位置,最後返回結果,就是 reduce的工作過程。

private static void reduce(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    Integer sum = integerStream.reduce(0,(x,y)->x+y);
    System.out.println(sum);
}

另外 Collectors好多方法都用到了 reduce,比如 groupingByminBymaxBy等等。

并行 Stream

Stream 本質上來說就是用來做數據處理的,為了加快處理速度,Stream API 提供了并行處理 Stream 的方式。通過 users.parallelStream()或者users.stream().parallel() 的方式來創建并行 Stream 對象,支持的 API 和普通 Stream 幾乎是一致的。

并行 Stream 默認使用 ForkJoinPool線程池,當然也支持自定義,不過一般情況下沒有必要。ForkJoin 框架的分治策略與并行流處理正好契合。

雖然并行這個詞聽上去很厲害,但並不是所有情況使用并行流都是正確的,很多時候完全沒這個必要。

什麼情況下使用或不應使用并行流操作呢?

  1. 必須在多核 CPU 下才使用并行 Stream,聽上去好像是廢話。
  2. 在數據量不大的情況下使用普通串行 Stream 就可以了,使用并行 Stream 對性能影響不大。
  3. CPU 密集型計算適合使用并行 Stream,而 IO 密集型使用并行 Stream 反而會更慢。
  4. 雖然計算是并行的可能很快,但最後大多數時候還是要使用 collect合併的,如果合併代價很大,也不適合用并行 Stream。
  5. 有些操作,比如 limit、 findFirst、forEachOrdered 等依賴於元素順序的操作,都不適合用并行 Stream。

最後

Java 25 周歲了,有多少同學跟我一樣在用 Java 8,還有多少同學再用更早的版本,請說出你的故事。

壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案