[UWP]用Win2D實現鏤空文字

1. 前言

之前用PointLight做了一個番茄鍾,效果還不錯,具體可見這篇文章:

後來試玩了Win2D,這次就用Win2D實現文字的鏤空效果,配合PointLight做一個內斂不張揚的番茄鍾。

實現鏤空文字的核心思想是使用CanvasGeometry.CreateText從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。這篇文章介紹了具體的實現步驟。

2. 參考例子

Win2D Gallery提供了大量Win2D的Sample,這次就參考了其中的文字鏤空效果例子,地址和運行效果如下:

3. 實現步驟

Sample的代碼量雖多,其實核心並不複雜,下面講講需要用到的API:

3.1 CanvasDevice.GetSharedDevice

因為要用到Win2D,所以首先要引用 nuget包。因為我的目標不是輸出到CanvasControl上,而是想要輸出到一個SpriteVisual上,所以使用:

var canvasDevice = CanvasDevice.GetSharedDevice();

3.2 CanvasComposition.CreateCompositionGraphicsDevice

然後創建一個Compositor,並將這個Compositor和CanvasDevice關聯起來,這裏需要使用 創建 :

var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

3.3 CompositionGraphicsDevice.CreateDrawingSurface

然後使用創建一個對象,它是用來繪畫內容的表面:

var drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);

3.4 Compositor.CreateSurfaceBrush

使用創建一個CompositionSurfaceBrush,它的作用是使用像素繪製SpriteVisual,簡單來說它就是一張位圖,然後輸出到SpriteVisual上:

var maskSurfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
spriteTextVisual.Brush = maskSurfaceBrush;

3.5 CanvasComposition.CreateDrawingSession

有了CompositionDrawingSurface就可以為所欲為了,將這個DrawingSurface作為參數,調用創建,DrawingSession提供了多個函數,可以自由地在DrawingSurface上畫文字、形狀、圖片甚至SVG。

using (var session = CanvasComposition.CreateDrawingSession(drawingSurface))
{

}

3.6 CanvasTextFormat和CanvasTextLayout

要再DrawingSurface上寫字,需要,而CanvasTextLayout中的文字大小、格式等則由定義:

using (var textFormat = new CanvasTextFormat()
{
    FontSize = (float)FontSize,
    Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
    VerticalAlignment = CanvasVerticalAlignment.Center,
    HorizontalAlignment = CanvasHorizontalAlignment.Center,

})
{
    using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
    {
        Color fontColor = FontColor;
        session.DrawTextLayout(textLayout, 0, 0, fontColor);
    }
}

3.7 CanvasGeometry.CreateText

因為我的目標是鏤空的文字,所以不能直接使用DrawTextLayout。這裏需要使用從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。CanvasStrokeStyle是可選的,它控制邊框的虛線。

using (var textGeometry = CanvasGeometry.CreateText(textLayout))
{
    var dashedStroke = new CanvasStrokeStyle()
    {
        DashStyle = DashStyle
    };
    session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
}

4. 封裝為控件

將上面的代碼總結一下,封裝為一個OutlineTextControl 控件,它提供了Text、OutlineColor、FontColor等屬性,在控件SizeChanged時,或者各個屬性改變時調用DrawText重新在CompositionDrawingSurface上繪製文字。代碼大致如下:

public class OutlineTextControl : Control
{
    private CompositionDrawingSurface _drawingSurface;

    public OutlineTextControl()
    {
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
        var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, CanvasDevice.GetSharedDevice());
        var spriteTextVisual = compositor.CreateSpriteVisual();

        ElementCompositionPreview.SetElementChildVisual(this, spriteTextVisual);
        SizeChanged += (s, e) =>
        {
            _drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
            DrawText();
            var maskSurfaceBrush = compositor.CreateSurfaceBrush(_drawingSurface);
            spriteTextVisual.Brush = maskSurfaceBrush;
            spriteTextVisual.Size = e.NewSize.ToVector2();
        };
        RegisterPropertyChangedCallback(FontSizeProperty, new DependencyPropertyChangedCallback((s, e) =>
        {
            DrawText();
        }));
    }


    private void DrawText()
    {
        if (ActualHeight == 0 || ActualWidth == 0 || string.IsNullOrWhiteSpace(Text) || _drawingSurface == null)
            return;

        var width = (float)ActualWidth;
        var height = (float)ActualHeight;
        using (var session = CanvasComposition.CreateDrawingSession(_drawingSurface))
        {
            session.Clear(Colors.Transparent);
            using (var textFormat = new CanvasTextFormat()
            {
                FontSize = (float)FontSize,
                Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
                VerticalAlignment = CanvasVerticalAlignment.Center,
                HorizontalAlignment = CanvasHorizontalAlignment.Center,

            })
            {
                using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
                {
                    if (ShowNonOutlineText)
                    {
                        session.DrawTextLayout(textLayout, 0, 0, FontColor);
                    }

                    using (var textGeometry = CanvasGeometry.CreateText(textLayout))
                    {
                        var dashedStroke = new CanvasStrokeStyle()
                        {
                            DashStyle = DashStyle
                        };
                        session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
                    }
                }
            }
        }
    }

//SOME CODE AND PROPERTIES

}

5. 結語

文章開頭的那個番茄鍾源碼可以在這裏查看:

也可以安裝我的番茄鍾應用試玩一下,安裝地址:

6. 參考

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

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

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

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

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

高德服務單元化方案和架構實踐

導讀:本文主要介紹了高德在服務單元化建設方面的一些實踐經驗,服務單元化建設面臨很多共性問題,如請求路由、單元封閉、數據同步,有的有成熟方案可以借鑒和使用,但不同公司的業務不盡相同,要盡可能的結合業務特點,做相應的設計和處理。

一、為什麼要做單元化

  • 單機房資源瓶頸

隨着業務體量和服務用戶群體的增長,單機房或同城雙機房無法支持服務的持續擴容。

  • 服務異地容災

異地容災已經成為核心服務的標配,有的服務雖然進行了多地多機房部署,但數據還是只在中心機房,實現真正意義上的異地多活,就需要對服務進行單元化改造。

二、高德單元化的特點

在做高德單元化項目時,我們首先要考慮的是結合高德的業務特點,看高德的單元化有什麼不一樣的訴求,這樣就清楚哪些經驗和方案是可以直接拿來用的,哪些又是需要我們去解決的。

高德業務和傳統的在線交易業務還是不太一樣,高德為用戶提供以導航為代表的出行服務,很多業務場景對服務的RT要求會很高,所以在做單元化方案時,盡可能減少對整體服務RT的影響就是我們需要重點考慮的問題,盡量做到數據離用戶近一些。轉換到單元化技術層面需要解決兩個問題:

1.用戶設備的單元接入需要盡可能的做到就近接入,用戶真實地理位置接近哪個單元就接入哪個單元,如華北用戶接入到張北,華南接入到深圳。

2.用戶的單元劃分最好能與就近接入的單元保持一致,減少單元間的跨單元路由。如用戶請求從深圳進來,用戶的單元劃分最好就在深圳單元,如果劃到張北單元就會造成跨單元路由。

另外一個區別就是高德很多業務是無須登錄的,所以我們的單元化方案除了用戶ID也要支持基於設備ID。

三、高德單元化實踐

服務的單元化架構改造需要一個至上而下的系統性設計,核心要解決請求路由、單元封閉、數據同步三方面問題。

請求路由:根據高德業務的特點,我們提供了取模路由和路由表路由兩種策略,目前上線應用使用較多的是路由表路由策略。

單元封閉:得益於集團的基礎設施建設,我們使用vipserver、hsf等服務治理能力保證服務同機房調用,從而實現單元封閉(hsf unit模式也是一種可行的方案,但個人認為同機房調用的架構和模式更簡潔且易於維護)。

數據同步:數據部分使用的是集團DB產品提供的DRC數據同步。

單元路由服務採用什麼樣的部署方案是我們另一個要面臨的問題,考慮過以下三種方案:

第一種SDK的方式因為對業務的強侵入性是首先被排除的,統一接入層進行代理和去中心化插件集成兩種方案各有利弊,但當時首批要接入單元化架構的服務很多都還沒有統一接入到gateway,所以基於現狀的考慮使用了去中心化插件集成的方式,通過在應用的nginx集成UnitRouter。

服務單元化架構

目前高德賬號,雲同步、用戶評論系統都完成了單元化改造,採用三地四機房部署,寫入量較高的雲同步服務,單元寫高峰能達到數w+QPS (存儲是mongodb集群)。

以賬號系統為例介紹下高德單元化應用的整體架構。

賬號系統服務是三地四機房部署,數據分別存儲在tair為代表的緩存和XDB里,數據存儲三地集群部署、全量同步。賬號系統服務器的Tengine上安裝UntiRouter,它請求的負責單元識別和路由,用戶單元劃分是通過記錄用戶與單元關係的路由表來控制。

PS:因歷史原因緩存使用了tair和自建的uredis(在redis基礎上添加了基於log的數據同步功能),目前已經在逐步統一到tair。數據同步依賴tair和alisql的數據同步方案,以及自建的uredis數據同步能力。

就近接入實現方案

為滿足高德業務低延時要求,就要想辦法做到數據(單元)離用戶更近,其中有兩個關鍵鏈路,一個是通過aserver接入的外網連接,另一個是服務內部路由(盡可能不產生跨單元路由)。

措施1:客戶端的外網接入通過aserver上的配置,將不同地理區域(七個大區)的設備劃分到對應近的單元,如華北用戶接入張北單元。

措施2:通過記錄用戶和單元關係的路由表來劃分用戶所屬單元,這個關係是通過系統日誌分析出來的,用戶經常從哪個單元入口進來,就會把用戶劃分到哪個單元,從而保證請求入口和單元劃分的相對一致,從而減少跨單元路由。

所以,在最終的單元路由實現上我們提供了傳統的取模路由,和為降延時而設計的基於路由表路由兩種策略。同時,為解無須登錄的業務場景問題,上述兩種策略除了支持用戶ID,我們同時也支持設備ID。

路由表設計

路由表分為兩部分,一個是用戶-分組的關係映射表,另一個是分組-單元的關係映射表。在使用時,通過路由表查對應的分組,再通過分組看用戶所屬單元。分組對應中國大陸的七個大區。

先看“用戶-(大區)分組”:

路由表是定期通過系統日誌分析出來的,看用戶最近IP屬於哪個大區就劃分進哪個分組,同時也對應上了具體單元。當一個北京的用戶長期去了深圳,因IP的變化路由表更新后將划進新大區分組,從而完成用戶從張北單元到深圳單元的遷移。

再看“分組-單元”:

分組與單元的映射有一個默認關係,這是按地理就近來配置的,比如華南對應深圳。除了默認的映射關係,還有幾個用於切流預案的關係映射。

老用戶可以通過路由表來查找單元,新用戶怎麼辦?對於新用戶的處理我們會降級成取模的策略進行單元路由,直至下次路由表的更新。所以整體上看新用戶跨單元路由比例肯定是比老用戶大的多,但因為新用戶是一個相對穩定的增量,所以整體比例在可接受範圍內。

路由計算

有了路由表,接下來就要解工程化應用的問題,性能、空間、靈活性和準確率,以及對服務穩定性的影響這幾個方面是要進行綜合考慮的,首先考慮外部存儲會增加服務的穩定性風險,後面我們在BloomFilter 、BitMap和MapDB多種方案中選擇BloomFilter,萬分之幾的誤命中率導致的跨單元路由在業務可接受範圍內。

通過日誌分析出用戶所屬大區后,我們將不同分組做成多個布隆過濾器,計算時逐層過濾。這個計算有兩種特殊情況:

1) 因為BloomFilter存在誤算率,有可能存在一種情況,華南分組的用戶被計算到華北了,這種情況比例在萬分之3 (生成BloomFilter時可調整),它對業務上沒有什麼影響,這類用戶相當於被劃分到一個非所在大區的分組裡,但這個關係是穩定的,不會影響到業務,只是存在跨單元路由,是可接受的。

2) 新用戶不在分組信息里,所以經過逐層的計算也沒有匹配到對應大區分組,此時會使用取模進行模除分組的計算。

如果業務使用的是取模路由而非路由表路由策略,則直接根據tid或uid計算對應的模除分組,原理簡單不詳表了。

單元切流

在發生單元故障進行切流時,主要分為四步驟

打開單元禁寫 (跨單元寫不敏感業務可以不配置)

檢查業務延時

切換預案

解除單元禁寫

PS:更新路由表時,也需要上述操作,只是第3步的切換預案變成切換新版本路由表;單元禁寫主要是了等待數據同步,避免數據不一致導致的業務問題。

核心指標

單元計算耗時1~2ms

跨單元路由比例底於5%

除了性能外,因就近接入的訴求,跨單元路由比例也是我們比較關心的重要指標。從線上觀察看,路由表策略單元計算基本上在1、2ms內完成,跨單元路由比例3%左右,整體底於5%。

四、後續優化

統一接入集成單元化能力

目前大部分服務都接入了統一接入網關服務,在網關集成單元化能力將大大減少服務單元化部署的成本,通過簡單的配置就可以實現單元路由,服務可將更多的精力放在業務的單元封閉和數據同步上。

分組機制的優化

按大區分組存在三個問題:

通過IP計算大區有一定的誤算率,會導致部分用戶劃分錯誤分組。

分組粒度太大,單元切流時流量不好分配。舉例,假如華東是我們用戶集中的大區,切流時把這個分組切到任意一個指定單元,都會造成單元服務壓力過大。

計算次數多,分多少個大區,理論最大計算次數是有多少次,最後採取取模策略。

針對上述幾個問題我們計劃對分組機製做如下改進

通過用戶進入單元的記錄來確認用戶所屬單元,而非根據用戶IP所在大區來判斷,解上述問題1。

每個單元劃分4個虛擬分組,支持更細粒度單元切流,解上述問題2。

用戶確實單元后,通過取模來劃分到不同的虛擬分組。每個單元只要一次計算就能完成,新用戶只需經過3次計算,解上述問題3。

熱更時的雙表計算

與取模路由策略不同,路由表策略為了把跨單元路由控制在一個較好的水平需要定期更新,目前更新時需要一個短暫的單元禁寫,這對於很多業務來說是不太能接受的。

為優化這個問題,系統將在路由表更新時做雙(路由)表計算,即將新老路由表同時加載進內存,更新時不再對業務做完全的禁寫,我們會分別計算當前用戶(或設備)在新老路由表的單元結果,如果單元一致,則說明路由表的更新沒有導致該用戶(或設備)變更單元,所以請求會被放行,相反如果計算結果是不同單元,說明發生了單元變更,該請求會被攔截,直至到達新路由表的一個完全起用時間。

優化前服務會完全禁寫比如10秒(時間取決於數據同步時間),優化後會變成觸髮禁寫的是這10秒內路由發生變更的用戶,這將大大減少對業務的影響。

服務端數據驅動的單元化場景

前面提到高德在路由策略上結合業務的特別設計,但整體單元劃分還是以用戶(或設備)為維度來進行的,但高德業務還有一個大的場景是我們未來要面對和解決的,就是以數據維度驅動的單元設計,基於終端的服務路由會變成基於數據域的服務路由。

高德很多服務是以服務數據為核心的,像地圖數據等它並非由用戶直接產生。業務的發展數據存儲也將不斷增加,包括5G和自動駕駛,對應數據的爆髮式增長單點全量存儲並不實現,以服務端數據驅動的服務單元化設計,是我們接下來要考慮的重要應用場景。

寫在最後

不同的業務場景對單元化會有不同的訴求,我們提供不同的策略和能力供業務進行選擇,對於多數據服務我們建議使用業務取模路由,簡單且易於維護;對於RT敏感的服務使用路由表的策略來盡可能的降低服務響應時長的影響。另外,要注意的是強依賴性的服務要採用相同的路由策略。

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

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

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

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

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

SpringBoot Application深入學習

本節主要介紹SpringBoot Application類相關源碼的深入學習。

主要包括:

  1. SpringBoot應用自定義啟動配置
  2. SpringBoot應用生命周期,以及在生命周期各個階段自定義配置。

本節採用SpringBoot 2.1.10.RELASE,對應示例源碼在:

SpringBoot應用啟動過程:

SpringApplication application = new SpringApplication(DemoApplication.class);
application.run(args);

一、Application類自定義啟動配置

創建SpringApplication對象后,在調用run方法之前,我們可以使用SpringApplication對象來添加一些配置,比如禁用banner、設置應用類型、設置配置文件(profile)

舉例:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(DemoApplication.class);
        // 設置banner禁用
        application.setBannerMode(Banner.Mode.OFF);
        // 將application-test文件啟用為profile
        application.setAdditionalProfiles("test");
        // 設置應用類型為NONE,即啟動完成后自動關閉
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);
    }

}

​ 也可以使用SpringApplicationBuilder類來創建SpringApplication對象,builder類提供了鏈式調用的API,更方便調用,增強了可讀性。

        new SpringApplicationBuilder(YqManageCenterApplication.class)
                .bannerMode(Banner.Mode.OFF)
                .profiles("test")
                .web(WebApplicationType.NONE)
                .run(args);

二、application生命周期

SpringApplication的生命周期主要包括:

  1. 準備階段:主要包括加載配置、設置主bean源、推斷應用類型(三種)、創建和設置SpringBootInitializer、創建和設置Application監聽器、推斷主入口類
  2. 運行階段:開啟時間監聽、加載運行監聽器、創建Environment、打印banner、創建和裝載context、廣播應用已啟動、廣播應用運行中

我們先來看一下源碼的分析:

SpringBootApplication構造器:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        
        // 設置默認配置
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.addConversionService = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.isCustomEnvironment = false;
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        // 設置主bean源
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        // 推斷和設置應用類型(三種)
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        // 創建和設置SpringBootInitializer
  this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 創建和設置SpringBoot監聽器
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        // 推斷和設置主入口類
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

SpringApplication.run方法源碼:

public ConfigurableApplicationContext run(String... args) {
        // 開啟時間監聽
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
    
        // 加載Spring應用運行監聽器(SpringApplicationRunListenter)
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

        Collection exceptionReporters;
        try {
            // 創建environment(包括PropertySources和Profiles)
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            
            // 打印banner
            Banner printedBanner = this.printBanner(environment);
            
            // 創建context(不同的應用類型對應不同的上下文)
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
            // 裝載context(其中還初始化了IOC容器)
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            // 調用applicationContext.refresh
            this.refreshContext(context);
            // 空方法
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop(); // 關閉時間監聽;這樣可以計算出完整的啟動時間
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            // 廣播SpringBoot應用已啟動,會調用所有SpringBootApplicationRunListener里的started方法
            listeners.started(context);
            
            // 遍歷所有ApplicationRunner和CommadnLineRunner的實現類,執行其run方法
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            // 廣播SpringBoot應用運行中,會調用所有SpringBootApplicationRunListener里的running方法
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            // run出現異常時,處理異常;會調用報錯的listener里的failed方法,廣播應用啟動失敗,將異常擴散出去
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

三、application生命周期自定義配置

在SpringApplication的生命周期中,我們還可以添加一些自定義的配置。

下面的配置,主要是通過實現Spring提供的接口,然後在resources下新建META-INF/spring.factories文件,在裏面添加這個類而實現引入的。

準備階段,可以添加如下自定義配置:

3.1 自定義ApplicationContextInitializer的實現類

@Order(100)
public class MyInitializer implements ApplicationContextInitializer {

@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
    System.out.println("自定義的應用上下文初始化器:" + configurableApplicationContext.toString());
}
}

再定義一個My2Initializer,設置@Order(101)

然後在spring.factories文件里如下配置:

# initializers
org.springframework.context.ApplicationContextInitializer=\
  com.example.applicationdemo.MyInitializer,\
  com.example.applicationdemo.My2Initializer

啟動項目:

3.2 自定義ApplicationListener的實現類

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}![file](https://img2018.cnblogs.com/blog/1860493/201911/1860493-20191125130012982-1676057906.png)

即監聽ApplicationEvents類的ApplicationListener接口的實現類。

首先查看有多少種ApplicationEvents:

裏面還可以進行拆分。

我們這裏設置兩個ApplicationListener,都用於監聽ApplicationEnvironmentPreparedEvent

@Order(200)
public class MyApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
        System.out.println("MyApplicationListener: 應用環境準備完畢" + applicationEnvironmentPreparedEvent.toString());
    }
}

在spring.factories中加入applicationListener的配置:

# application-listeners
org.springframework.context.ApplicationListener=\
  com.example.applicationdemo.MyApplicationListener,\
  com.example.applicationdemo.MyApplicationListener2

啟動階段,可以添加如下自定義配置:

3.3 自定義SpringBootRunListener的實現類

監聽整個SpringBoot應用生命周期

public interface SpringApplicationRunListener {
    // 應用啟動
    void starting();

    // 應用ConfigurableEnvironment準備完畢,此刻可以將其調整
    void environmentPrepared(ConfigurableEnvironment environment);

    // 上下文準備完畢
    void contextPrepared(ConfigurableApplicationContext context);

    // 上下文裝載完畢
    void contextLoaded(ConfigurableApplicationContext context);

    // 啟動完成(Beans已經加載到容器中)
    void started(ConfigurableApplicationContext context);

    // 應用運行中
    void running(ConfigurableApplicationContext context);

    // 應用運行失敗
    void failed(ConfigurableApplicationContext context, Throwable exception);
}

我們可以自定義SpringApplicationRunListener的實現類,通過重寫以上方法來定義自己的listener。

比如:

public class MyRunListener implements SpringApplicationRunListener {

    // 注意要加上這個構造器,兩個參數都不能少,否則啟動會報錯,報錯的詳情可以看這個類的最下面
    public MyRunListener(SpringApplication springApplication, String[] args) {

    }

    @Override
    public void starting() {
        System.out.println("MyRunListener: 程序開始啟動");
    }

    // 其他方法省略,不做修改
}

然後在spring.factories文件中添加這個類:

org.springframework.boot.SpringApplicationRunListener=\
  com.example.applicationdemo.MyRunListener

啟動:

3.4 自定義ApplicationRunner或CommandLineRunner

application的run方法中,有這樣一行:

this.callRunners(context, applicationArguments);

仔細分析源碼,發現這一句的作用是:SpringBoot應用啟動過程中,會遍歷所有的ApplicationRunner和CommandLineRunner,執行其run方法。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        Iterator var4 = (new LinkedHashSet(runners)).iterator();

        while(var4.hasNext()) {
            Object runner = var4.next();
            if (runner instanceof ApplicationRunner) {
                this.callRunner((ApplicationRunner)runner, args);
            }

            if (runner instanceof CommandLineRunner) {
                this.callRunner((CommandLineRunner)runner, args);
            }
        }

    }
@FunctionalInterface
public interface CommandLineRunner {
    void run(String... args) throws Exception;
}
@FunctionalInterface
public interface ApplicationRunner {
    void run(ApplicationArguments args) throws Exception;
}

分別定義一個實現類,添加@Component,這兩個實現類不需要在spring.factories中配置

好了,關於這些自定義配置的具體使用,後續會繼續進行介紹,請持續關注!感謝!

具體示例代碼請去查看。

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

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

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

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

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

結合源碼,重溫 Android View 的事件處理知多少 ?

前言

  • Android View 的 事件處理在我們的編程中,可謂是無處不在了。但對於大多數人而言,一直都是簡單的使用,對其原理缺乏深入地認識。
  • 學 Android 有一段時間了,最近發現,很多基礎知識開始有些遺忘了,所以從新複習了 View 的事件分發。特地整理成了這篇文章分享給大家。
  • 本文不難,可以作為大家茶餘飯後的休閑。

祝大家閱讀愉快!

方便大家學習,我在 GitHub 上建立個 倉庫

  • 倉庫內容與博客同步更新。由於我在 稀土掘金 簡書 CSDN 博客園 等站點,都有新內容發布。所以大家可以直接關注該倉庫,即使獲得精彩內容

  • 倉庫地址:

一、View 的事件回調

  • 我們結合源碼看看 View 的事件分發是個怎樣的過程,首先我們建立一個類 MyButton 類繼承 AppCompatButton 用於測試:
public class MyButton extends AppCompatButton {

    private final String TAG = "DeBugMyButton";
        public MyButton(Context context) {
        super(context);
    }

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

}

1.1 事件分發流程

  • 我們都知道有一個方法叫做 public boolean dispatchTouchEvent(MotionEvent event) 。首先我們要知道,對於我們這個自定義控件,他的觸摸事件都是從我們 dispatchTouchEvent 這個方法開始往下去分發的。所以可以說:這個方法是一個入口方法。

1.1.1 onTouchEvent 作用

  • 現在我們重寫該方法和另一個方法:onTouchEvent ,並且打印一行日誌:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    Log.d(TAG, "----on dispatch Touch Event----");
    return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "----on touch event----");
    }
    return super.onTouchEvent(event);
}
  • 然後我們在 MainActivity 中,設置一個實例化一個 MyButton 控件對象用於測試,並且給他添加一個 onClickListentersetOnTouchListener
public class MainActivity extends AppCompatActivity {

    private final String TAG = "DeBugMainActivity";

    /**
     * 自定義控件 MyButton
     */
    private MyButton mMyButton;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        iniView();
    }

    /**
     * 實例化控件
     */
    private void iniView() {
        mMyButton = findViewById(R.id.my_button);

    mMyButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.d(TAG, "----on touch----");
                    break;
                default:
                    break;
            }
            return false;
        }
    });
    
    mMyButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d(TAG, "----on click----");
        }
    });
    }
}
  • 然後我們運行這個 Demo ,點擊 MyButton 按鈕,會的到如下日誌:
  • 我們可以看到首先回調了這個 dispatchTouchEvent ,然後是它的監聽器 OnTouch ,接着是它的 onTouchEvent,最後又執行了 dispatchTouchEvent ,那麼這是為什麼呢?

  • 這是因為我們這兒只監聽了 ACTION_DOWN 而當手指抬起時它同樣還回去回調 dispatchTouchEvent ,最後我們打印 OnClick 的回調。

  • 總結一下就是:
    dispatchTouchEvent -> setOnTouchListener -> onTouchEvent -> setOnClickListener

  • 說明我們 setOnClickListener 是通過 onTouchEvent 處理,產生了 OnClick 。一會我們再來看看其中的原理。

  • 既然說 dispatchTouchEvent 像一個入口,就先讓我們來看下它是怎麼處理和操作的: 首先,既然我們調用了 super.dispatchTouchEvent(event) ,那麼我們就來看看它父類中是怎麼實現該方法的。不信的是,它的父類 AppCompatButton 也沒有實現該方法 ,最後經過層層搜尋,我們發現這個方法是屬於 View 的方法。

1.1.2 dispatchTouchEvent 的實現

  • 那麼現在我們來看看 ViewdispatchTouchEvent 怎麼實現的:
public boolean dispatchTouchEvent(MotionEvent event) {
    ......
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}
  • dispatchTouchEvent 中,我們可以發現下面這樣一個代碼塊
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}
  • 不難看出:如果執行了這個代碼段,那麼後面的方法就不會執行了,並且 dispatchTouchEvent 會返回 true 。我們再仔細觀察下其中的條件:在 if 條件中我們發現:只有當其滿足 li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) 時才會執行 if 內的操作

  • 經過上面分析,我們可以知道: onTouch 事件必須返回 true 時,才會執行該方法塊。那麼我們就回到 MainActivity 中。我們發現 setOnTouchListeneronTouch 默認返回值是 false( 不滿足返回值為 true ), 這就表明他會繼續去執行下一個代碼塊:

if (!result && onTouchEvent(event)) {
    result = true;
}
  • 執行這個 if 語句的過程中。首先調用了 onTouchEvent 方法。這就解釋了,為什麼它先執行了 mOnTouchListener ,然後再執行 onTouchEvent

  • 現在我們就可以總結一下:首先我們回調了 dispatchTouchEvent ,然後回調 OnTouchListener 。這個時候,如果 TouchListener 沒有 return true ,那麼就會接着去運行 onTouchEvent ( 當然,如果 return true 後面的層級就不會執行了 。一句話說就是:到那個層級 return true 那麼哪個層級就消費掉了這個事件 )。

1.1.3 onTouchEvent 的處理

  • 同時我們還有一個結果:我們 onClick ( 包括我們的 onLongClick ) 是來自於我們 onTouchEvent 這個方法的處理。那麼下面我們就來看看 View 中是怎麼處理 onTouchEvent 的:
public boolean onTouchEvent(MotionEvent event) {
    。。。

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                。。。
                break;

            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                。。。
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    // Remove any future long press/tap checks
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                break;
        }

        return true;
    }

    return false;
}

二、onClick 和 OnLongClick

  • 因為我們是拿 ACTION_DOWN 作為舉例的。那麼我們先來分析一下 case MotionEvent.ACTION_DOWN : 中 onTouchEvent 是怎麼執行的,以及 onClickOnLongClick 是如何產生的:

2.1 onClick 和 OnLongClick 的產生

  • 首先,當我們手指按下時,有一個 mHasPerformedLongPress 標識會先被設為 false 。再往下會執行一行 postDelayed(mPendingCheckForTapViewConfiguration.getTapTimeout()); 我們來看看這一行的作用:

  • 首先,從名字我們就可以猜測,這是個延時執行的方法。我們進一步閱讀發現 mPendingCheckForTap 是一個 Runnable 動作; ViewConfiguration.getTapTimeout() 是一個 100mm 的延時。也就是說延時 100mm 後去執行 mPendingCheckForTap 中的動作。那麼我們就來看看 mPendingCheckForTap 中做了什麼:

private final class CheckForTap implements Runnable {
    public float x;
    public float y;

    @Override
    public void run() {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        setPressed(true, x, y);
        checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
    }
}
  • 也就是說,停一百秒后就開始檢查,用戶的手指是否離開了屏幕。( 就是當前 ACTION_DOWN 之後,有沒有觸發了 ACTION_UP 這個環節 ),但是 ACTION_DOWN 后,我們還有一個 ACTION_MOVE 過程。在這個 ACTION_MOVE 中,如果 100mm 內離開了屏幕、或者離開了這個控件就會觸發 ACTION_UP ,那麼就認為這是一個點擊事件 onClick 。如果沒有觸發 ACTION_UP 的話,就會再延時 400mm

2.2 ACTION_DOWN 之後流程

  • ACTION_DOWN 之後,會先等 100mm
  • 如果沒有離開屏幕或者離開控件,就是沒有觸發 ACTION_UP 的話,就會再延時 400mm。
  • 500mm 后就會觸發 onLongClick 事件。

2.3 那麼我們現在來驗證一下 onLongClick :

  • 首先再 MainActivity 中加上:
mMyButton.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {

        return true;
    }
});
  • 接着,我們發現 OnLongClick 是有返回值的,如果返回值是 false 還會接着去觸發 onClick 事件,如果返回 true 的話,那麼這個長按事件就直接被消費掉了( 也就是這個點擊事件就不會完後傳遞到 OnClickListener 中去了 )。

2.4 總結

  • 100mm 時為點擊,500mm 時為長按,接着觸髮長按事件。
  • 再看長按事件的返回值,如果時 true 就結束。
  • 如果時 false 那麼 OnClickListener 就同樣也被執行。
  • 這就是由 obTouchEvent 產生出來的 onClick/onLongClick 的來龍去脈。

總結

  • 我們 View 的事件方法,基本上就是這麼一個思路,從 dispatchTouchEventOnTouchListener 監聽器,再到 onTouchEvent,接着 onTouchEvent 由產生了 onClick/onLongClick
  • 如果大家感興趣的話可以更深入的去閱讀源碼。
  • 重點:學 Android 有一段時間了,我打算好好的梳理一下所學知識,包括 ActivityServiceBroadcastRecevier 事件分發、滑動衝突、新能優化等所有重要模塊,歡迎大家關注 ,方便及時接收更新
  • 如果有可以補充的知識點,歡迎大家在評論區指出。

碼字不易,你的點贊是我總結的最大動力!

  • 由於我在「稀土掘金」「簡書」「CSDN」「博客園」等站點,都有新內容發布。所以大家可以直接關注我的 GitHub 倉庫,以免錯過精彩內容!

  • 倉庫地址:

  • 一萬多字長文,加上精美思維導圖,記得點贊哦,歡迎關注 ,我們下篇文章見!

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

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

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

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

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

Android DecorView 與 Activity 綁定原理分析

一年多以前,曾經以為自己對 View 的添加显示邏輯已經有所了解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的添加显示。

概論

Android 中 Activity 是作為應用程序的載體存在,代表着一個完整的用戶界面,提供了一個窗口來繪製各種視圖,當 Activity 啟動時,我們會通過 setContentView 方法來設置一個內容視圖,這個內容視圖就是用戶看到的界面。那麼 View 和 activity 是如何關聯在一起的呢 ?

 上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:

  • Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。

  • PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製窗口。並且,該類內部包含了一個 DecorView 對象,該 DectorView 對象是所有應用窗口的根 View。
  • DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(窗口內容的容器)。

  • ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設置它的子 View 。

  • WindowManager : 是一個接口,裏面常用的方法有:添加View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。
  • WindowManagerService (WMS) : 負責管理各 app 窗口的創建,更新,刪除, 显示順序。運行在 system_server 進程。

ViewRootImpl :擁有 DecorView 的實例,通過該實例來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通過調用 IWindow 一些方法,通過 Binder 通信的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來調用 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub,每一個應用進程都有一個唯一的 Session 對象與 WMS 通信。

DecorView 的創建 

先從 Mainactivity 中的代碼看起,首先是調用了 setContentView;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

該方法是父類 AppCompatActivity 的方法,最終會調用 AppCompatDelegateImpl 的 setContentView 方法:

// AppCompatDelegateImpl  
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }

ensureSubDecor 從字面理解就是創建 subDecorView,這個是根據主題來創建的,下文也會講到。創建完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 布局添加到裏面。不過大家注意的是,在添加之前先調用 removeAllViews() 方法,確保沒有其他子 View 的干擾。

    private void ensureSubDecor() {
        if (!this.mSubDecorInstalled) {
            this.mSubDecor = this.createSubDecor(); 
            ......
        }
        ......
    }        

 最終會調用 createSubDecor() ,來看看裏面的具體代碼邏輯:

 private ViewGroup createSubDecor() {
        // 1、獲取主題參數,進行一些設置,包括標題,actionbar 等 
        TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
        if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        } else {
            if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
                this.requestWindowFeature(1);
            } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
                this.requestWindowFeature(108);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                this.requestWindowFeature(109);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                this.requestWindowFeature(10);
            }

            this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
            a.recycle();
            // 2、確保優先初始化 DecorView
            this.mWindow.getDecorView();
            LayoutInflater inflater = LayoutInflater.from(this.mContext);
            ViewGroup subDecor = null;
            // 3、根據不同的設置來對 subDecor 進行初始化
            if (!this.mWindowNoTitle) {
                if (this.mIsFloating) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
                    this.mHasActionBar = this.mOverlayActionBar = false;
                } else if (this.mHasActionBar) {
                    TypedValue outValue = new TypedValue();
                    this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
                    Object themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
                    } else {
                        themedContext = this.mContext;
                    }

                    subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
                    this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                    this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
                    if (this.mOverlayActionBar) {
                        this.mDecorContentParent.initFeature(109);
                    }

                    if (this.mFeatureProgress) {
                        this.mDecorContentParent.initFeature(2);
                    }

                    if (this.mFeatureIndeterminateProgress) {
                        this.mDecorContentParent.initFeature(5);
                    }
                }
            } else {
                if (this.mOverlayActionMode) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
                } else {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
                }

                if (VERSION.SDK_INT >= 21) {
                    ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
                        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                            int top = insets.getSystemWindowInsetTop();
                            int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
                            if (top != newTop) {
                                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
                            }

                            return ViewCompat.onApplyWindowInsets(v, insets);
                        }
                    });
                } else {
                    ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
                        public void onFitSystemWindows(Rect insets) {
                            insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
                        }
                    });
                }
            }

            if (subDecor == null) {
                throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
            } else {
                if (this.mDecorContentParent == null) {
                    this.mTitleView = (TextView)subDecor.findViewById(id.title);
                }

                ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                if (windowContentView != null) {
                    while(windowContentView.getChildCount() > 0) {
                        View child = windowContentView.getChildAt(0);
                        windowContentView.removeViewAt(0);
                        contentView.addView(child);
                    }

                    windowContentView.setId(-1);
                    contentView.setId(16908290);
                    if (windowContentView instanceof FrameLayout) {
                        ((FrameLayout)windowContentView).setForeground((Drawable)null);
                    }
                }
                // 將 subDecor 添加到 DecorView 中
                this.mWindow.setContentView(subDecor);
                contentView.setAttachListener(new OnAttachListener() {
                    public void onAttachedFromWindow() {
                    }

                    public void onDetachedFromWindow() {
                        AppCompatDelegateImpl.this.dismissPopups();
                    }
                });
                return subDecor;
            }
        }
    }
                    

上面的代碼總結來說就是在做一件事,就是創建 subDecor。攤開來說具體如下:

1、根據用戶選擇的主題來設置一些显示特性,包括標題,actionbar 等。

2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。

3、最後添加到 DecorView中。

添加的具體代碼如下:此處是通過調用 

 // AppCompatDelegateImpl   this.mWindow.getDecorView();

 // phoneWindow    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
 

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
 // 生成 DecorView             mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
 // 這樣 DecorView 就持有了window             mDecor.setWindow(this);
        }
      ......
}


   protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
   }

到此,DecorView 的創建就講完了。可是我們似乎並沒有看到 DecorView 是被添加的,什麼時候對用戶可見的。

 WindowManager

View 創建完以後,那 Decorview 是怎麼添加到屏幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。

看 ActivityThread 中的 handleResumeActivity 方法:

// ActivityThread
public
void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) {           // 這裏也會調用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }

上面的代碼主要做了以下幾件事:

1、獲取到 DecorView,設置不可見,然後通過 wm.addView(decor, l) 將 view 添加到 WindowManager;

2、在某些情況下,比如此時點擊了輸入框調起了鍵盤,就會調用 wm.updateViewLayout(decor, l) 來更新 View 的布局。

3、這些做完以後,會調用 activity 的  makeVisible ,讓視圖可見。如果此時 DecorView 沒有添加到 WindowManager,那麼會添加。 

// Activity
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }

 接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:

// WindowManagerGlobal  
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
           // ......
    
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
           // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            } 
}

在這裏,實例化了 ViewRootImpl 。同時調用 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裏還保存了 DecorView ,Params,以及 ViewRootImpl 的實例。

現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。

 ViewRootImpl

實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程序窗口的 DecorView 都有一個與之關聯的 ViewRootImpl 對象,這種關聯關係是由 WindowManager 來維護的。

先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:

   /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
               
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.

                requestLayout();
                ......
            }
        }
    }

這裏先將 mView 保存了 DecorView 的實例,然後調用 requestLayout() 方法,以完成應用程序用戶界面的初次布局。

 public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

因為是 UI 繪製,所以一定要確保是在主線程進行的,checkThread 主要是做一個校驗。接着調用 scheduleTraversals 開始計劃繪製了。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這裏主要關注兩點:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步消息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理異步消息,如果沒有異步消息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設置為異步消息)。

mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。

mTraversalRunnable :TraversalRunnable 的實例,是一個Runnable,最終肯定會調用其 run 方法:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal,如其名,開始繪製了,該方法內部最終會調用 performTraversals 進行繪製。

  void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

到此,DecorView 與 activity 之間的綁定關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。 

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

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

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

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

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

08 決策樹與隨機森林

08 決策樹與隨機森林

決策樹之信息論基礎

認識決策樹

  1. 來源: 決策樹的思想來源非常樸素,程序設計中的條件分支結構就是if – then 結構,最早的決策樹就是利用這類結構分割數據的一種分類學習方法。

  2. 舉例:是否見相親對象

信息的度量和作用

  1. 克勞德 .艾爾伍德 .香農:信息論創始人,密西根大學學士,麻省理工學院博士。 1948年發表了划時代論文 – 通信的數學原理,奠定了現代信息論的基礎。
  2. 信息的單位: 比特 (bit)

  3. 舉例: 以32支球隊爭奪世界杯冠軍
  • 如果不知道任何球隊的信息,每支球隊得冠概率相等。
    以二分法預測,最少需要使用5次才能預測到準確結果。 5 = log32 (以2為底)
    5 = -(1/32log1/32 + 1/32log1/32 + ……)

  • 開放一些信息,則小於5bit, 如1/6 德國,1/6 巴西, 1/10 中國
    5 > -(1/6log1/4 + 1/6log1/4 + ….)

  1. 信息熵:
  • “誰是世界杯冠軍”的信息量應該比5 bit少, 它的準確信息量應該是:
  • H = -(p1logp1 + p2logp2 + p3logp3 +……p32logp32 ) Pi 為第i支球隊獲勝的概率
  • H 的專業術語就是信息熵,單位為比特

決策樹的劃分以及案例

信息增益

  1. 定義: 特徵A對訓練數據集D的信息增益g(D,A), 定義為集合D的信息熵H(D)與特徵A給定條件下D的信息條件熵H(D|A) 之差,即:
    g(D,A) = H(D) – H(D | A)
    注: 信息增益表示得知特徵 X 的信息而使得類 Y的信息的不確定性減少的程度。

  2. 以不同特徵下的信貸成功率為例

  • H(D) = -(9/15log(9/15) + 6/15log(6/15)) = 0.971 # 以類別進行判斷,只有是否兩種類別
  • gD,年紀) = H(D) – H(D’|年紀) = 0.971 – [1/3H(青年)+ 1/3H(中年)+ 1/3H(老年)] # 三種年紀對應的目標值均佔1/3
    – H(青年) = -(2/5log(2/5) + 3/5log(3/5)) # 青年類型中,類別的目標值特徵為(2/5, 3/5)
    – H(中年) = -(2/5log(2/5) + 3/5log(3/5))
    – H(老年) = -(4/5log(2/5) + 1/5log(3/5))

令A1, A2, A3, A4 分別表示年齡,有工作,有房子和信貸情況4個特徵,則對應的信息增益為:
g(D,A1) = H(D) – H(D|A1)
其中,g(D,A2) = 0.324 , g(D,A3) = 0.420 , g(D,A4) = 0.363
相比而言,A3特徵(有房子)的信息增益最大,為最有用特徵。
所以決策樹的實際劃分為:

常見決策樹使用的算法

  1. ID3
  • 信息增益,最大原則
  1. C4.5
  • 信息增益比最大原則 (信息增益占原始信息量的比值)
  1. CART
  • 回歸樹: 平方誤差最小
  • 分類樹: 基尼係數最小原則 (劃分的細緻),sklearn默認的劃分原則

Sklearn決策樹API

  1. sklearn.tree.DecisionTreeClassifier(criterion=’gini’, max_depth=None, random_state=None)
  • criterion (標準): 默認基尼係數,也可以選用信息增益的熵‘entropy’
  • max_depth: 樹的深度大小
  • random_state: 隨機數種子
  1. 決策樹結構
    sklearn.tree.export_graphviz() 導出DOT文件格式
  • estimator: 估算器
  • out_file = “tree.dot” 導出路徑
  • feature_name = [,] 決策樹特徵名

決策樹預測泰坦尼克號案例

import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_graphviz

"""
泰坦尼克數據描述事故后乘客的生存狀態,該數據集包括了很多自建旅客名單,提取的數據集中的特徵包括:
票的類別,存貨,等級,年齡,登錄,目的地,房間,票,船,性別。
乘坐等級(1,2,3)是社會經濟階層的代表,其中age數據存在缺失。
"""


def decision():
    """
    決策樹對泰坦尼克號進行預測生死
    :return: None
    """
    # 1.獲取數據
    titan = pd.read_csv('./titanic_train.csv')

    # 2.處理數據,找出特徵值和目標值
    x = titan[['Pclass', 'Age', 'Sex']]
    y = titan[['Survived']]
    # print(x)

    # 缺失值處理 (使用平均值填充)
    x['Age'].fillna(x['Age'].mean(), inplace=True)
    print(x)
    # 3.分割數據集到訓練集和測試集
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25)

    # 4. 進行處理(特徵工程) 特徵,類別 --> one_hot編碼
    dict = DictVectorizer(sparse=False)
    x_train = dict.fit_transform(x_train.to_dict(orient='records'))
    print(dict.get_feature_names())
    x_test = dict.transform(x_test.to_dict(orient='records'))  # 默認一行一行轉換成字典
    print(x_train)

    # 5. 用決策樹進行預測
    dec = DecisionTreeClassifier()
    dec.fit(x_train, y_train)

    # 預測準確率
    print("預測的準確率:", dec.score(x_test, y_test))

    # 導出決策樹
    export_graphviz(dec, out_file='./tree.dot', feature_names=['Pclass', 'Age', 'Sex'])
    return None


if __name__ == '__main__':
    decision()

隨機森林

集成學習方法

  1. 定義:集成學習通過建立幾個模型組合,來解決單一預測問題。其工作原理是生成多個分類器 / 模型,各組獨立地學習和作出預測。這些預測最後結合成單預測,因此優於任何一個單分類的租出預測。

隨機森林

  1. 定義:在機器學習中,隨機森林是一個包含多個決策樹的分類器,並且其輸出的類別是由個別樹輸出的類別的眾數而定。
    例如: 訓練了5棵樹,其中4棵樹的結果是True, 1棵樹為False, 那麼最終的結果就是True. (投票)

  2. 問題: 如果每棵樹使用相同的特徵,相同的分類器,參數也相同,建立的每棵樹不就是相同的么?

隨機森林建立多個決策樹的過程:

單個樹的建立:(N個樣本,M個特徵)

  1. 隨機在N個樣本中選擇一個樣本,重複N次, 樣本有可能重複
  2. 隨機在M個特徵當中選出m個特徵 m << M
  3. 建立10棵決策樹,樣本,特徵大多不一樣 隨機有放回的抽樣 (bootstrap抽樣)

為什麼要隨機抽樣訓練集?

如果不隨機抽樣,每棵樹的訓練集都一樣,那麼最終訓練處的樹分類結果也是完全一樣的

為什麼要有放回的抽樣?

如果不是有放回的抽樣,那麼每棵樹的訓練樣本都是不同的,都是沒有交集的,這樣的每棵樹都是“有偏的”,“片面的”。即,每棵樹訓練出來都是有很大的差異,而隨機森鈴最後分類取決於多棵樹(弱分類器)的投票表決。

隨機森林 API

  • 分類器:sklearn.ensemble.RandomForestClassifier
    • n_estimators:integer(整數),option, default=10 (森林里數目的數量)
    • criteria: string (default =’gini’) 分割特徵的測量方法
    • max_depth 樹的最大深度
    • max_feature = ‘auto’ 每個決策樹的最大特徵數量
    • bootstrap: default = True 是否放回抽樣

隨機森林的優點

  1. 在當前的所有算法中,具有極好的準確率
  2. 能有有效地運行在大數據集上 (樣本,特徵)
  3. 能夠處理具有高維特徵的輸入樣本,不需要降維
  4. 能夠評估各個特徵在分類問題上的重要性

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

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

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

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

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

eMOVING 極速 100 公里電動機車 iE125 上市,快充 10 分鐘可騎 78 公里

中華汽車在 7 月 19 日發表新款電動機車 eMOVING iE125,極速可達到時速 100 公里,成為市場關注的焦點。

iE 125 的命名除了代表 intelligent electricity,也有 industrial engineering 的含義,125 則象徵 125cc 等級燃油機車的性能與操控。iE125 安全極速達到時速 100 公里,靜止加速到時速 50 公里僅需 3.9 秒,30% 坡度爬坡最高速為時速 32 公里。iE125 在充滿電之後,時速 30 公里之下續航里程為 155 公里,TES 變速續航里程為 82 公里。支援超級快充功能,充電 10 分鐘就能充滿 50% 的電力,可以行駛 78 公里。

iE125 含電池車重為 124 公斤,配備 CBS 連動煞車和 IP67 防水等級。提供 ECO、SPEED 和 BOOST 三種行車模式,讓消費者在不同情境下使用。特別的電動駐車功能只要按下按鈕就能直接立起中柱,車主不再需要為立中柱而困擾。配置 QC 3.0 USB 充電座,方便騎乘時進行充電。

eMOVING iE125 藍色版。

eMOVING iE125 白色版。

eMOVING iE125 橘色版。

eMOVING iE125 灰色版。

iE125 精緻型儀錶板螢幕為彩色液晶螢幕,豪華型和旗艦型則為汽車級 TFT。豪華型和旗艦型搭載車輛診斷系統,能在儀表板上顯示車身、動力、電池和胎壓等資訊,並提供異常提示與保養提醒。儀表板還可以進行個人化設定,自由更換儀表板主題與桌布。

iE125 豪華型和旗艦型具有遙控防盜中控鎖,能夠連結手機進行上鎖、解鎖和座墊開啟。iOS 版本的 App 支援即時來電提醒,來電與訊息通知會即時顯示。旗艦型還特別內建前方行車記錄器和胎壓偵測器,進一步確保行車安全。

儀錶板能顯示各種車身資訊。

eMOVING 將充電分為 3 種類型,分別在不同需求時使用。家用滿足平時充電需求,約 160 分鐘可以充滿 50% 的電力。快速充電站則供在外逛街或用餐時補充電力,約 30 分鐘可以充滿 50% 的電力。超級充電站供臨時路途中繼充電,約 10 分鐘可以充滿 50% 的電力。

消費者可以自行選擇電池租賃方案,eMOVING 提供電池永久保固。基礎型在家充電每月 399 元,輕量型每月 599 元額外提供 100 分鐘的超級充電分鐘數,進階型每月 799 元可以不限時數進行超級充電。為了推廣超級充電,12 月以前輕量型和進階型方案皆以每月 499 元計價,而且享受不限時數的超級充電。

中華汽車預計在 12 月佈建 70 座以上的快速充電站,2020 年 6 月更要佈建超過 150 座快速充電站,早期合作夥伴包括肯德基、家樂福、順益汽車和滙豐汽車。中華汽車也宣布捐贈 5 座超級充電站給桃園市政府,未來讓符合快充共通規格電動機車車主都能免費充電。

中華汽車捐贈 5 座超級充電站給桃園市政府。

eMOVING 推出了 10 月底前購車,就贈送 5,000 元購車金的優惠,可以全額折抵車價或購買配件。iE125 提供藍色、白色、灰色和橘色 4 種顏色讓消費者選擇,精緻型定價為台幣 73,800 元,豪華型定價為台幣 79,800 元,旗艦型定價為台幣 85,800 元。補助最高的桃園市汰換二行程機車換購電動機車補助最高 29,000 元,再加上 10 月底前購車贈送的購車金 5,000 元,精緻型最低台幣 39,800 元起,豪華型最低台幣 45,800 元起,旗艦型最低台幣 51,800 元起。

(合作媒體:。圖片來源:)

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

【其他文章推薦】

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

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

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

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

【設計模式】單例模式的八種姿態寫法分析

目錄

前言
網上泛濫流傳單例模式的寫法種類,有說7種的,也有說6種的,當然也不排除說5種的,他們說的有錯嗎?其實沒有對與錯,刨根問底,寫法終究是寫法,其本質精髓大體一致!因此完全沒必要去追究寫法的多少,有這個時間還不如跟着宜春去網吧偷耳機、去田裡抓青蛙得了,一天天的….

言歸正傳…單例模式是最常用到的設計模式之一,熟悉設計模式的朋友對單例模式絕對不會陌生。同時單例模式也是比較簡單易理解的一種設計模式。

@

何謂單例模式?

專業術語

單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。

單例模式,簡單的說就是 一個類只能有一個實例,並且在整個項目中都能訪問到這個實例。

單例模式的優點

1、在內存中只有一個對象,節省內存空間。
2、避免頻繁的創建銷毀對象,可以提高性能。
3、避免對共享資源的多重佔用。
4、可以全局訪問。

單例模式實現整體思路流程

首先我們要清楚單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱)。

單例模式的常規實現思路大致相同為以下三個步驟:

1、私有構造方法
2、指向自己實例的私有靜態引用
3、以自己實例為返回值的靜態的公有的方法

當然也可以理解為
1、私有化構造方法,讓外部不能new。
2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
3、提供一個公有的static靜態方法(一般該方法使用getInstance這個名稱),返回實例對象。

將該類的構造方法定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用。

單例模式的適用場景

由於單例模式有很多獨特的優點,所以是編程中用的比較多的一種設計模式。我總結了一下我所知道的適合使用單例模式的場景:

1、需要頻繁實例化然後銷毀的對象。
2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象。
3、有狀態的工具類對象。
4、頻繁訪問數據庫或文件的對象。

在後面我將會講到JDK中的Runtime類就是使用的餓漢式單例!在Spring MVC框架中的controller 默認是單例模式的!

單例模式的八種姿態寫法

宜春強烈建議:如果是沒有接觸單例模式的讀者朋友強烈建議你們動手敲一遍,不要複製,不然沒效果!

還有一點就是,要真正輕而易舉的理解單例模式,JVM的類加載知識是不能少的,不然你只是會敲的層次,啥?不懂類加載?放心,宜春就是要你會,要你理解透徹。

其實上面的這篇文章特別重要,上面這篇文章的重要性懂的自然懂,不懂的希望能理解宜春的一片好意,去看一下吧,實在看不懂看不下去在回來看這篇文章就好了,再大不了就把博主一起按在馬桶蓋蓋上….

是不是心裏暖暖的?宜春也不多嗶嗶了,直接擼碼走起….

姿態一:餓漢式1(靜態變量)

package singletonPattern;
//餓漢式(靜態變量)

class Singleton{
    //1、私有化構造方法,讓外部不能new
    private Singleton(){

    }
    //2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
    private final static Singleton instance=new Singleton();

    //3、提供一個公有的static靜態方法,返回實例對象
    public static Singleton getInstance(){
        return instance;
    }
}
//以下是測試代碼=====================

public class SingletenDemo1 {
    public static void main(String[] args) {
        Singleton singleton=Singleton.getInstance();
        Singleton singleton2=Singleton.getInstance();
//驗證一:
        System.out.println(singleton==singleton2);
//驗證二:
        System.out.println(singleton.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

//運行結果:
//        true
//        460141958
//        460141958

/*
餓漢式(靜態變量)方法

優點:寫法簡單,在類加載的時候就完成了實例化,同時也就避免了線程同步問題,因此線程安全
缺點:由於是在類加載時就完成了實例化,沒有達到懶加載的效果。如果一直沒有使用過這個實例,就造成了內存的浪費!

總結:這種方式基於ClassLoader類加載機制避免了多線程的同步問題,只不過instance屬性在類加載就實例化,在單例模式中大多數都是調用getInstance方法,
     由於getInstance方法是static靜態的,調用它肯定會觸發類加載!但是觸發類加載的原因有很多,我們不能保證這個類會通過其他的方式觸發類加載(比如調用了其他的static方法)
     這個時候初始化instance就沒有達到lazy loading 懶加載的效果,可能造成內存的浪費!

     餓漢式(靜態變量)這種方式可以使用但是會造成內存的浪費!

     */

姿態二:餓漢式2(static靜態代碼塊)

package singletonPattern;
//餓漢式2(static靜態代碼塊)

class Singleton2{
    private Singleton2(){

    }

    private static Singleton2 instance;

    static{ //把創建單例對象的操作放進了static靜態代碼塊中==============
        instance = new Singleton2();
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}
//餓漢式2(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!
//唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中

姿態三:懶漢式1(線程不安全)

package singletonPattern;
//懶漢式1(線程不安全)
class Singleton3{
    private Singleton3(){

    }

    private static Singleton3 instance;

    public static Singleton3 getInstance(){
        if(instance == null){
            instance=new Singleton3();
        }
        return instance;
    }
}
/*
懶漢式(線程不安全)的這種方式起到了懶加載的效果,但只能在單線程下使用。
如果在多線程下,一個線程進入了if(singleton==null)判斷語句塊,還沒執行產生實例的句子,另一個線程
又進來了,這時會產生多個實例,所以不安全。

結語:懶漢式(線程不安全)在實際開發中,不要使用這種方式!!存在潛在危險
*/

姿態四:懶漢式2(線程安全)

package singletonPattern;
//懶漢式2(線程安全)
class Singleton4{
    private Singleton4(){

    }

    private static Singleton4 instance;

    public static synchronized Singleton4 getInstance(){
        if(instance == null){
            instance=new Singleton4();
        }
        return instance;
    }
}

/*
懶漢式2(線程安全)方式

優點:線程安全
缺點:效率太低,每次調用getInstance方法都要進行同步

結語:懶漢式2(線程安全)方式在開發中不推薦使用,主要是效率太低了*/

姿態五:餓漢式2(static靜態代碼塊)

package singletonPattern;
//懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,在多線程下依舊會有多個實例
class Singleton5{
    private Singleton5(){

    }

    private static Singleton5 instance;

    public static  Singleton5 getInstance(){
        if(instance == null){   //多線程情況下可能多個線程進入這個if塊
            synchronized (Singleton5.class){  //到這裏只會一個一個創建實例,雖然安全,但是就不再是單例了
                instance=new Singleton5();
            }
        }
        return instance;
    }
}
/*
懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,依舊會有多個實例

結語:懶漢式3 同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個單例設計的有點搞笑*/

姿態六:雙重檢查單例

package singletonPattern;
//雙重檢查應用實例方式
class Singleton6{
    private Singleton6(){}

    private static volatile Singleton6 singleton;

    public static Singleton6 getInstance(){
        if(singleton==null){
            synchronized(Singleton6.class){
                if(singleton == null){
                    singleton= new Singleton6();
                }
            }
        }
        return singleton;
    }
}
/*
雙重檢查應用實例方式:

線程安全、延遲加載、效率較高

結語:開發中推薦使用!
*/

這個時候博主就得嗶嗶幾句了,細心的童鞋會發現有一個Volatile關鍵字,完了,沒見過,小白童鞋慌了!

Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值。

這種實現方式既可以實現線程安全地創建實例,而又不會對性能造成太大的影響。它只是第一次創建實例的時候同步,以後就不需要同步了,從而加快了運行速度。

姿態七:靜態內部類單例

package singletonPattern;
//static靜態內部類單例

class Singleton7{
    private Singleton7(){}

    private static volatile Singleton7 instance;

    //寫一個static靜態內部類,給該類添加一個static靜態instance屬性
    private static class SingletonInstance{
        private static final Singleton7 SINGLETON_7=new Singleton7();
    }

    //
    public static synchronized Singleton7 getInstence(){
        return SingletonInstance.SINGLETON_7;
    }
}
/*
靜態內部類單例方式
        1、這種方式採用了類加載機制來保證初始化實例時只有一個線程
        2、巧妙的將實例化Singleton操作放進getInstance方法中,getInstance方法返回靜態內部類中實例化好的Singleton
        3、類的靜態屬性只會在第一次加載類的時候初始化,也就是只會初始化一次,在這裏,JVM幫我們保證了線程的安全,類在初始化時,別的線程無法進入。
       
        優點:線程安全、利用靜態內部類特點實現延遲加載、效率高
        開發中推薦使用這種靜態內部類單例方式!

static靜態內部特點:
1、外部類加載不會導致內部類加載,保證了其懶加載
*/

這個單例,宜春就不得不嗶嗶兩句了,要清楚這個單例,必須要明白static靜態內部特點,也就是外部類加載不會導致內部類加載!

姿態八:餓漢式2(static靜態代碼塊)

package singletonPattern;
//使用枚舉

import com.sun.xml.internal.bind.v2.runtime.unmarshaller.XsiNilLoader;

enum Singleton8{
    INSTANCE;
    public void methodName(){
        System.out.println("測試數據");
    }
}
/*

枚舉方式的枚舉:
推薦寫法,簡單高效。充分利用枚舉類的特性,只定義了一個實例,且枚舉類是天然支持多線程的。
藉助JDK1.5中添加的枚舉來實現單例模式優點:
         1、不僅能避免多線程同步問題 
         2、還能防止反序列化重新創建新的對象

枚舉方式單例是由Effective java作者Josh Bloch提倡的,結語:推薦使用!
*/

當然也可以測試一下

public class SingletonDemo8 {
    public static void main(String[] args) {
        Singleton8 instance = Singleton8.INSTANCE;
        Singleton8 instance2 = Singleton8.INSTANCE;
        System.out.println(instance==instance2);

        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());

        instance.methodName();
    }
}

運行結果:

true
460141958
460141958
測試數據

屬實沒毛病!

JDK源碼中單例模式的應用

先來看一段Runtime 的源碼吧,並分析一下其使用的是種單例模式!

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

這應該不難看出吧!如果看不出的話只能說明你真的還沒有理解單例模式,我其實想說單例模式其實是23種設計模式中最簡單的一個,只是寫法比較多而已!同時面試官一般都會問單例模式,它已經是很基礎的了,問的稍微有點水平就是問你單例模式在JDK中哪裡運用到了,顯然JDK中的Runtime其實它使用的就是餓漢式單例!正如註釋所說,每一個java應用程序都有一個Runtime實例。Runtime的單例模式是採用餓漢模式創建的,意思是當你加載這個類文件時,這個實例就已經存在了。

Runtime類可以取得JVM系統信息,或者使用gc()方法釋放掉垃圾空間,還可以使用此類運行本機的程序。

==還有就是spring Mvc 中的controller 默認是單例模式的,解析。==

單例模式總結

1、餓漢式(靜態變量)這種方式可以使用,但是沒有達到 lazy loading 懶加載的效果會造成內存的浪費!開發中不建議使用。
2、餓漢式(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中
3、懶漢式(線程不安全)起到了懶加載的效果,但只能在單線程下使用。在實際開發中,不要使用這種方式!!!
4、懶漢式2(線程安全)方式線程安全但是效率太低,每次調用getInstance方法都要進行同步。所以在開發中不推薦使用。 5、懶漢式3
同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個設計有點搞笑哈哈。
6、雙重檢查應用實例方式,線程安全、延遲加載、效率較高。因此開發中推薦使用!
7、靜態內部類單例方式線程安全、利用靜態內部類特點實現延遲加載、效率高。 開發中推薦使用這種靜態內部類單例方式!
8、藉助JDK1.5中添加的枚舉來實現單例模式不僅能避免多線程同步問題還能防止反序列化重新創建新的對象。枚舉方式單例是由Effective java作者Josh Bloch提倡的,開發中推薦使用!

單例模式必須考慮到在多線程的應用場合下的使用,畢竟現在的服務器基本上都是多核的了。

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

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

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

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

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

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

mybatis精講(三)–標籤及TypeHandler使用

目錄

話引

  • 前兩張我們分別介紹了Mybatis環境搭建及其組件的生命周期。這些都是我們Mybatis入門必備技能。有了前兩篇的鋪墊我們今天就來深入下Mybatis, 也為了填下之前埋下的坑。

XML配置標籤

概覽


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration> 
    <!--引入外部配置文件-->
    <properties resource=""/>
    <!--設置-->
    <settings/>
    <!--定義別名-->
    <typeAliases>
        <package name=""/>
    </typeAliases>
    <!--類型處理器-->
    <typeHandlers/>
    <!--對象工廠-->
    <objectFactory/>
    <!--插件-->
    <plugins/>
    <!--定義數據庫信息,默認使用development數據庫構建環境-->
    <environments default="development">
        <environment id="development">
            <!--jdbc事物管理-->
            <transactionManager type="JDBC"/>
            <!--配置數據庫連接信息-->
            <dataSource type="POOLED"/>
        </environment>
    </environments>
    <!--數據庫廠商標識-->
    <databaseIdProvider/>
    <mappers/>
</configuration>

  • 上面模板列出了所有xml可以配置的屬性。這裏plugins是一個讓人哭笑不得的東西。用的好是利器,用的不好就是埋坑。接下來我們來看看各個屬性的作用

properties

  • 該標籤的作用就是引入變量。和maven的properties一樣。在這裏定義的變量或者引入的變量,在下面我們是可以童工${}使用的。

子標籤property


<properties>
  <property name="zxhtom" value="jdbc:mysql://localhost:3306/mybatis"/>
</properties>

<dataSource type="POOLED">
<property name="driver" value="${zxhtom}"/>
<dataSource>
  • 上述的配置就可以直接使用zxhtom這個變量。

resource

  • 除了上述方法我們還可以通過引入其他properties文件,就可以使用文件里的配置變量了。

<properties resource="mybatis.properties"/>

程序注入

  • 最後還有一種我們在構建SqlSessionFactory的時候重新載入我們的Properties對象就可以了。另外三者的優先級是從低到高

settings


configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
  • 上面代碼是我們在XMLConfigBuilder解析settings標籤的代碼。從這段代碼中我們了解到settings子標籤。
參數 功能 可選值 默認值
autoMappingBehavior 指定Mybatis應如何自動映射列到字段上。
NONE : 表示取消自動映射
PARTIAL:只會自動映射沒有定義嵌套結果集映射的結果集
FULL : 自動映射任意複雜的結果集
NONE、PARTIAL、FULL PARTIAL
autoMappingUnknownColumnBehavior 指定識別到位置列或屬性的時間
NONE : 什麼都不做
WARNING:日誌會報警(前提是日誌設置了显示權限)
FAILING : 拋出異常。
NONE, WARNING, FAILING NONE
cacheEnabled 該配置影響的所有映射器中配置的緩存的全局開關 true|false true
proxyFactory 指定Mybatis創建具有延遲加載能力的對象所用到的代理工具未指定時將自動查找 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING not set
lazyLoadingEnabled 延時加載全局開關
開啟時:級聯對象會延時加載;級聯標籤中可以通過fetchType來定製覆蓋此選項
true|false false
aggressiveLazyLoading 啟用時:對任意延遲屬性的調用會使帶有延遲加載屬性的對象分層性質完整加載,反之按需加載 true|false true
multipleResultSetsEnabled 是否允許單一語句返回多結果集 true|false true
useColumnLabel 確切的說當映射找不到參數時會使用列標籤(數據庫列名)代替別名去映射 true|false true
useGeneratedKeys 允許 JDBC 支持自動生成主鍵,需要驅動兼容。 如果設置為 true 則這個設置強制使用自動生成主鍵,儘管一些驅動不能兼容但仍可正常工作(比如 Derby) true|false false
defaultExecutorType 配置默認的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 設置超時時間,決定驅動等待數據庫響應的秒數 整數 null
defaultFetchSize 設置數據庫resultSet讀取數據方式,默認全部加載進內存,設置該屬性可以設置一次性讀多少條數據進內存 整數 null
mapUnderscoreToCamelCase 是否開啟自動駝峰命名規則(camel case)映射,即從經典數據庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似映射。 true|false false
safeRowBoundsEnabled -允許在嵌套語句中使用分頁 true|false false
localCacheScope 一級緩存。mybatis默認對同一個sqlsession中數據是共享的。一個sqlsession調用兩次相同查詢實際只會查詢一次。就是因為該屬性為SESSION , STATEMENT則針對的是每一條sql SESSION|STATEMENT SESSION
jdbcTypeForNull 當沒有為參數提供特定的jdbc類型時,為空值則指定JDBC類型。在新增時我們沒有設置參數,這個時候就會根據此參數天長。加入設置VARCHAR,那麼我們新增的數據沒傳參數則為空字符 NULL|VARCHAR|OTHER OTHER
lazyLoadTriggerMethods 指定具體方法延時加載 方法 equals,clone,hashCode,toString
safeResultHandlerEnabled 允許在嵌套語句中使用分頁 true|false true
defaultScriptingLanguage 動態SQL生成的默認語言 org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
defaultEnumTypeHandler mybatis默認的枚舉處理類
callSettersOnNulls 指定當結果集中值為null的時候是否調用映射對象的setter(put)方法。
useActualParamName 允許使用他們的編譯后名稱來映射,3.4.2后默認true.在xml中#{0}則報錯。設置為false,則#{0}代表第一個參數#{n}第n個 true|false true
returnInstanceForEmptyRow 當返回行的所有列都是空時,MyBatis默認返回 null。 當開啟這個設置時,MyBatis會返回一個空實例。 請注意,它也適用於嵌套的結果集 (如集合或關聯)。(新增於 3.4.2) true|false false
logPrefix 指定 MyBatis 增加到日誌名稱的前綴。

別名

  • 別名是mybatis為我們項目中類起的一個名字,類名往往會很長所以別名就方便我們平時的開發。Mybatis為我們內置了一些類的別名:byte、short、int、long、float、double、boolean、char等基礎類型的別名。還有其的封裝類型、String,Object,Map,List等等常用的類。
    org.apache.ibatis.type.TypeAliasRegistry這個類中幫我們內置了別名。可以看下。自定義別名也是通過這個類進行註冊的。我們可以通過settings中typeAliases配置的方式結合@Alias。或者掃描包也可以的。

TypeHandler

  • 這個接口就四個方法

public interface TypeHandler<T> {

  /**
   * 設置參數是用到的方法
   */
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}
  • 可以理解成攔截器。它主要攔截的是設置參數和獲取結果的兩個節點。這個類的作用就是將Java對象和jdbcType進行相互轉換的一個功能。同樣的在org.apache.ibatis.type.TypeHandlerRegistry這個類中mybatis為我們提供了內置的TypeHandler。基本上是對於基本數據和分裝對象的轉換。
  • 下面我們隨便看一個TypeHandler處理細節
public class BooleanTypeHandler extends BaseTypeHandler<Boolean> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setBoolean(i, parameter);
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    boolean result = rs.getBoolean(columnName);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    boolean result = rs.getBoolean(columnIndex);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    boolean result = cs.getBoolean(columnIndex);
    return !result && cs.wasNull() ? null : result;
  }
}
  • setParameter是PreparedStatement進行設置成boolean類型。getResult分別通過三種不同方式獲取。在這些方法里我們可以根據自己也無需求進行控制。常見的控制是枚舉的轉換。傳遞參數過程可能是枚舉的name,但是傳遞到數據庫中要枚舉的index.這種需求我們就可以在TypeHandler中實現。我們書寫的typeHandler之後並不能被識別,還需要我們在resultMap中的result標籤中通過typeHandler指定我們的自定義Handler.

自定義TypeHandler

  • 承接上文我們說道枚舉的轉換。下面我們還是已學生類為例。學生中性別之前是boolean類型。現在我們採用枚舉類型。但是數據庫中存的還是數據,01.

EnumTypeHandler

  • 在TypeHandlerRegister類中申明了默認的枚舉類處理器是private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
  String s = rs.getString(columnName);
  return s == null ? null : Enum.valueOf(type, s);
}
  • 我們通過這個方法可以看出,這個枚舉處理器適合已枚舉名稱存儲的方式

EnumOrdinalTypeHandler

  • 在Enum中還有一個屬性oridinal。這個表示枚舉中的索引。然後我們通過查看Mybatis提供的處理器發現有個叫EnumOrdinalTypeHandler。我們很容易聯想到的就是這個處理器是通過枚舉的所以作為數據庫內容的。在SexEnum中MALE存儲到數據庫中則為0.注意這個0不是我們的index.而是MALE的索引。如果將MALE和FEMAEL調換。那麼MALE索引則為1.

  • 因為默認的是EnumTypeHandler。所以想用EnumOrdinalTypeHandler的話我們要麼在resultMap中sex字段指定該處理器。要不就通過配置文件typeHandlers註冊進來。(將處理器與Java類進行綁定。mybatis遇到這個Java對象的時候就知道用什麼處理器處理)


<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.github.zxhtom.enums.SexEnum"/>
</typeHandlers>

SexTypeHandler

  • 上面的不管是通過名稱存儲還是通過索引存儲都不太滿足我們的需求。我們想通過我們的index存儲。那麼這時候我們就得自定義處理邏輯了。Mybatis處理器都是繼承BaseTypeHandler。因為BaseTypeHandler實現了TypeHandler.所以我們這裏也就繼承BaseTypeHandler。

public class SexTypeHandler extends BaseTypeHandler<SexEnum> {


    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i,parameter.getIndex());
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int i = rs.getInt(columnName);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int i = rs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int i = cs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }
}

typeHandler注意點

  • 在編寫自定義處理器的時候我們得之處Javatype、jdbctype。兩者不是必填。但至少得有一個。正常我們默認javatype是必填的。
  • 填寫的方式有三種
    • 通過MappedTypes、MappedJdbcTypes分別指定javatype、jdbctype
    • 通過在mybatis-config.xml中配置typeHandlers進行註解。裏面也有這兩個屬性的配置。
    • 通過在mapper.xml的resultmap中再次指定某個字段的typehandler.
  • TypeHandler為我們提供了Java到jdbc數據的轉換橋樑。極大的方便了我們平時的開發。讓我們開發期間忽略數據的轉換這麼糟心的事情。

# 加入戰隊

微信公眾號

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

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

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

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

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

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

從零開始入門 | Kubernetes 中的服務發現與負載均衡

作者 | 阿里巴巴技術專家  溪恆

一、需求來源

為什麼需要服務發現

在 K8s 集群裏面會通過 pod 去部署應用,與傳統的應用部署不同,傳統應用部署在給定的機器上面去部署,我們知道怎麼去調用別的機器的 IP 地址。但是在 K8s 集群裏面應用是通過 pod 去部署的, 而 pod 生命周期是短暫的。在 pod 的生命周期過程中,比如它創建或銷毀,它的 IP 地址都會發生變化,這樣就不能使用傳統的部署方式,不能指定 IP 去訪問指定的應用。

另外在 K8s 的應用部署里,之前雖然學習了 deployment 的應用部署模式,但還是需要創建一個 pod 組,然後這些 pod 組需要提供一個統一的訪問入口,以及怎麼去控制流量負載均衡到這個組裡面。比如說測試環境、預發環境和線上環境,其實在部署的過程中需要保持同樣的一個部署模板以及訪問方式。因為這樣就可以用同一套應用的模板在不同的環境中直接發布。

Service:Kubernetes 中的服務發現與負載均衡

最後應用服務需要暴露到外部去訪問,需要提供給外部的用戶去調用的。我們上節了解到 pod 的網絡跟機器不是同一個段的網絡,那怎麼讓 pod 網絡暴露到去給外部訪問呢?這時就需要服務發現。

在 K8s 裏面,服務發現與負載均衡就是 K8s Service。上圖就是在 K8s 里 Service 的架構,K8s Service 向上提供了外部網絡以及 pod 網絡的訪問,即外部網絡可以通過 service 去訪問,pod 網絡也可以通過 K8s Service 去訪問。

向下,K8s 對接了另外一組 pod,即可以通過 K8s Service 的方式去負載均衡到一組 pod 上面去,這樣相當於解決了前面所說的複發性問題,或者提供了統一的訪問入口去做服務發現,然後又可以給外部網絡訪問,解決不同的 pod 之間的訪問,提供統一的訪問地址。

二、用例解讀

下面進行實際的一個用例解讀,看 pod K8s 的 service 要怎麼去聲明、怎麼去使用?

Service 語法

首先來看 K8s Service 的一個語法,上圖實際就是 K8s 的一個聲明結構。這個結構里有很多語法,跟之前所介紹的 K8s 的一些標準對象有很多相似之處。比如說標籤 label 去做一些選擇、selector 去做一些選擇、label 去聲明它的一些 label 標籤等。

這裡有一個新的知識點,就是定義了用於 K8s Service 服務發現的一個協議以及端口。繼續來看這個模板,聲明了一個名叫 my-service 的一個 K8s Service,它有一個 app:my-service 的 label,它選擇了 app:MyApp 這樣一個 label 的 pod 作為它的後端。

最後是定義的服務發現的協議以及端口,這個示例中我們定義的是 TCP 協議,端口是 80,目的端口是 9376,效果是訪問到這個 service 80 端口會被路由到後端的 targetPort,就是只要訪問到這個 service 80 端口的都會負載均衡到後端 app:MyApp 這種 label 的 pod 的 9376 端口。

創建和查看 Service

如何去創建剛才聲明的這個 service 對象,以及它創建之後是什麼樣的效果呢?通過簡單的命令:

  • kubectl apply -f service.yaml

或者是

  • kubectl created -f service.yaml

上面的命令可以簡單地去創建這樣一個 service。創建好之後,可以通過:

  • kubectl discribe service

去查看 service 創建之後的一個結果。

service 創建好之後,你可以看到它的名字是 my-service。Namespace、Label、Selector 這些都跟我們之前聲明的一樣,這裏聲明完之後會生成一個 IP 地址,這個 IP 地址就是 service 的 IP 地址,這個 IP 地址在集群裏面可以被其它 pod 所訪問,相當於通過這個 IP 地址提供了統一的一個 pod 的訪問入口,以及服務發現。

這裏還有一個 Endpoints 的屬性,就是我們通過 Endpoints 可以看到:通過前面所聲明的 selector 去選擇了哪些 pod?以及這些 pod 都是什麼樣一個狀態?比如說通過 selector,我們看到它選擇了這些 pod 的一個 IP,以及這些 pod 所聲明的 targetPort 的一個端口。

實際的架構如上圖所示。在 service 創建之後,它會在集群裏面創建一個虛擬的 IP 地址以及端口,在集群里,所有的 pod 和 node 都可以通過這樣一個 IP 地址和端口去訪問到這個 service。這個 service 會把它選擇的 pod 及其 IP 地址都掛載到後端。這樣通過 service 的 IP 地址訪問時,就可以負載均衡到後端這些 pod 上面去。

當 pod 的生命周期有變化時,比如說其中一個 pod 銷毀,service 就會自動從後端摘除這個 pod。這樣實現了:就算 pod 的生命周期有變化,它訪問的端點是不會發生變化的。

集群內訪問 Service

在集群裏面,其他 pod 要怎麼訪問到我們所創建的這個 service 呢?有三種方式:

  • 首先我們可以通過 service 的虛擬 IP 去訪問,比如說剛創建的 my-service 這個服務,通過 kubectl get svc 或者 kubectl discribe service 都可以看到它的虛擬 IP 地址是 172.29.3.27,端口是 80,然後就可以通過這個虛擬 IP 及端口在 pod 裏面直接訪問到這個 service 的地址。

  • 第二種方式直接訪問服務名,依靠 DNS 解析,就是同一個 namespace 里 pod 可以直接通過 service 的名字去訪問到剛才所聲明的這個 service。不同的 namespace 裏面,我們可以通過 service 名字加“.”,然後加 service 所在的哪個 namespace 去訪問這個 service,例如我們直接用 curl 去訪問,就是 my- 就可以訪問到這個 service。

  • 第三種是通過環境變量訪問,在同一個 namespace 里的 pod 啟動時,K8s 會把 service 的一些 IP 地址、端口,以及一些簡單的配置,通過環境變量的方式放到 K8s 的 pod 裏面。在 K8s pod 的容器啟動之後,通過讀取系統的環境變量比讀取到 namespace 裏面其他 service 配置的一個地址,或者是它的端口號等等。比如在集群的某一個 pod 裏面,可以直接通過 curl $ 取到一個環境變量的值,比如取到 MY_SERVICE_SERVICE_HOST 就是它的一個 IP 地址,MY_SERVICE 就是剛才我們聲明的 MY_SERVICE,SERVICE_PORT 就是它的端口號,這樣也可以請求到集群裏面的 MY_SERVICE 這個 service。

Headless Service

service 有一個特別的形態就是 Headless Service。service 創建的時候可以指定 clusterIP:None,告訴 K8s 說我不需要 clusterIP(就是剛才所說的集群裏面的一個虛擬 IP),然後 K8s 就不會分配給這個 service 一個虛擬 IP 地址,它沒有虛擬 IP 地址怎麼做到負載均衡以及統一的訪問入口呢?

它是這樣來操作的:pod 可以直接通過 service_name 用 DNS 的方式解析到所有後端 pod 的 IP 地址,通過 DNS 的 A 記錄的方式會解析到所有後端的 Pod 的地址,由客戶端選擇一個後端的 IP 地址,這個 A 記錄會隨着 pod 的生命周期變化,返回的 A 記錄列表也發生變化,這樣就要求客戶端應用要從 A 記錄把所有 DNS 返回到 A 記錄的列表裡面 IP 地址中,客戶端自己去選擇一個合適的地址去訪問 pod。

可以從上圖看一下跟剛才我們聲明的模板的區別,就是在中間加了一個 clusterIP:None,即表明不需要虛擬 IP。實際效果就是集群的 pod 訪問 my-service 時,會直接解析到所有的 service 對應 pod 的 IP 地址,返回給 pod,然後 pod 裏面自己去選擇一個 IP 地址去直接訪問。

向集群外暴露 Service

前面介紹的都是在集群裏面 node 或者 pod 去訪問 service,service 怎麼去向外暴露呢?怎麼把應用實際暴露給公網去訪問呢?這裏 service 也有兩種類型去解決這個問題,一個是 NodePort,一個是 LoadBalancer。

  • NodePort 的方式就是在集群的 node 上面(即集群的節點的宿主機上面)去暴露節點上的一個端口,這樣相當於在節點的一個端口上面訪問到之後就會再去做一層轉發,轉發到虛擬的 IP 地址上面,就是剛剛宿主機上面 service 虛擬 IP 地址。

  • LoadBalancer 類型就是在 NodePort 上面又做了一層轉換,剛才所說的 NodePort 其實是集群裏面每個節點上面一個端口,LoadBalancer 是在所有的節點前又掛一個負載均衡。比如在阿里雲上掛一個 SLB,這個負載均衡會提供一個統一的入口,並把所有它接觸到的流量負載均衡到每一個集群節點的 node pod 上面去。然後 node pod 再轉化成 ClusterIP,去訪問到實際的 pod 上面。

三、操作演示

下面進行實際操作演示,在阿里雲的容器服務上面進去體驗一下如何使用 K8s Service。

創建 Service

我們已經創建好了一個阿里雲的容器集群,然後並且配置好本地終端到阿里雲容器集群的一個連接。

首先可以通過 kubectl get cs ,可以看到我們已經正常連接到了阿里雲容器服務的集群上面去。

今天將通過這些模板實際去體驗阿里雲服務上面去使用 K8s Service。有三個模板,首先是 client,就是用來模擬通過 service 去訪問 K8s 的 service,然後負載均衡到我們的 service 裏面去聲明的一組 pod 上。

K8s Service 的上面,跟剛才介紹一樣,我們創建了一個 K8s Service 模板,裏面 pod,K8s Service 會通過前端指定的 80 端口負載均衡到後端 pod 的 80 端口上面,然後 selector 選擇到 run:nginx 這樣標籤的一些 pod 去作為它的後端。

然後去創建帶有這樣標籤的一組 pod,通過什麼去創建 pod 呢?就是之前所介紹的 K8s deployment,通過 deployment 我們可以輕鬆創建出一組 pod,然後上面聲明 run:nginx 這樣一個label,並且它有兩個副本,會同時跑出來兩個 pod。

先創建一組 pod,就是創建這個 K8s deployment,通過 kubectl create -f service.yaml。這個 deployment 也創建好了,再看一下 pod 有沒有創建出來。如下圖看到這個 deployment 所創建的兩個 pod 都已經在 running 了。通過 kubectl get pod -o wide 可以看到 IP 地址。通過 -l,即 label 去做篩選,run=nginx。如下圖所示可以看到,這兩個 pod 分別是 10.0.0.135 和 10.0.0.12 這樣一個 IP 地址,並且都是帶 run=nginx 這個 label 的。

下面我們去創建 K8s service,就是剛才介紹的通過 service 去選擇這兩個 pod。這個 service 已經創建好了。

根據剛才介紹,通過 kubectl describe svc 可以看到這個 service 實際的一個狀態。如下圖所示,剛才創建的 nginx service,它的選擇器是 run=nginx,通過 run=nginx 這個選擇器選擇到後端的 pod 地址,就是剛才所看到那兩個 pod 的地址:10.0.0.12 和 10.0.0.135。這裏可以看到 K8s 為它生成了集群裏面一個虛擬 IP 地址,通過這個虛擬 IP 地址,它就可以負載均衡到後面的兩個 pod 上面去。

現在去創建一個客戶端的 pod 實際去感受一下如何去訪問這個 K8s Service,我們通過 client.yaml 去創建客戶端的 pod,kubectl get pod 可以看到客戶端 pod 已經創建好並且已經在運行中了。

通過 kubectl exec 到這個 pod 裏面,進入這個 pod 去感受一下剛才所說的三種訪問方式,首先可以直接去訪問這個 K8s 為它生成的這個 ClusterIP,就是虛擬 IP 地址,通過 curl 訪問這個 IP 地址,這個 pod 裏面沒有裝 curl。通過 wget 這個 IP 地址,輸入進去測試一下。可以看到通過這個去訪問到實際的 IP 地址是可以訪問到後端的 nginx 上面的,這個虛擬是一個統一的入口。

第二種方式,可以通過直接 service 名字的方式去訪問到這個 service。同樣通過 wget,訪問我們剛才所創建的 service 名 nginx,可以發現跟剛才看到的結果是一樣的。

在不同的 namespace 時,也可以通過加上 namespace 的一個名字去訪問到 service,比如這裏的 namespace 為 default。

最後我們介紹的訪問方式裏面還可以通過環境變量去訪問,在這個 pod 裏面直接通過執行 env 命令看一下它實際注入的環境變量的情況。看一下 nginx 的 service 的各種配置已經註冊進來了。

可以通過 wget 同樣去訪問這樣一個環境變量,然後可以訪問到我們的一個 service。

介紹完這三種訪問方式,再看一下如何通過 service 外部的網絡去訪問。我們 vim 直接修改一些剛才所創建的 service。

最後我們添加一個 type,就是 LoadBalancer,就是我們前面所介紹的外部訪問的方式。

然後通過 kubectl apply,這樣就把剛剛修改的內容直接生效在所創建的 service 裏面。

現在看一下 service 會有哪些變化呢?通過 kubectl get svc -o wide,我們發現剛剛創建的 nginx service 多了一個 EXTERNAL-IP,就是外部訪問的一個 IP 地址,剛才我們所訪問的都是 CLUSTER-IP,就是在集群裏面的一個虛擬 IP 地址。

然後現在實際去訪問一下這個外部 IP 地址 39.98.21.187,感受一下如何通過 service 去暴露我們的應用服務,直接在終端裏面點一下,這裏可以看到我們直接通過這個應用的外部訪問端點,可以訪問到這個 service,是不是很簡單?

我們最後再看一下用 service 去實現了 K8s 的服務發現,就是 service 的訪問地址跟 pod 的生命周期沒有關係。我們先看一下現在的 service 後面選擇的是這兩個 pod IP 地址。

我們現在把其中的一個 pod 刪掉,通過 kubectl delete 的方式把前面一個 pod 刪掉。

我們知道 deployment 會讓它自動生成一個新的 pod,現在看 IP 地址已經變成 137。

現在再去 describe 一下剛才的 service,如下圖,看到前面訪問端點就是集群的 IP 地址沒有發生變化,對外的 LoadBalancer 的 IP 地址也沒有發生變化。在所有不影響客戶端的訪問情況下,後端的一個 pod IP 已經自動放到了 service 後端裏面。

這樣就相當於在應用的組件調用的時候可以不用關心 pod 在生命周期的一個變化。

以上就是所有演示。

四、架構設計

最後是對 K8s 設計的一個簡單的分析以及實現的一些原理。

Kubernetes 服務發現架構

如上圖所示,K8s 服務發現以及 K8s Service 是這樣整體的一個架構。

K8s 分為 master 節點和 worker 節點:

  • master 裏面主要是 K8s 管控的內容;
  • worker 節點裏面是實際跑用戶應用的一個地方。

在 K8s master 節點裏面有 APIServer,就是統一管理 K8s 所有對象的地方,所有的組件都會註冊到 APIServer 上面去監聽這個對象的變化,比如說我們剛才的組件 pod 生命周期發生變化,這些事件。

這裏面最關鍵的有三個組件:

  • 一個是 Cloud Controller Manager,負責去配置 LoadBalancer 的一個負載均衡器給外部去訪問;
  • 另外一個就是 Coredns,就是通過 Coredns 去觀測 APIServer 裏面的 service 後端 pod 的一個變化,去配置 service 的 DNS 解析,實現可以通過 service 的名字直接訪問到 service 的虛擬 IP,或者是 Headless 類型的 Service 中的 IP 列表的解析;
  • 然後在每個 node 裏面會有 kube-proxy 這個組件,它通過監聽 service 以及 pod 變化,然後實際去配置集群裏面的 node pod 或者是虛擬 IP 地址的一個訪問。

實際訪問鏈路是什麼樣的呢?比如說從集群內部的一個 Client Pod3 去訪問 Service,就類似於剛才所演示的一個效果。Client Pod3 首先通過 Coredns 這裏去解析出 ServiceIP,Coredns 會返回給它 ServiceName 所對應的 service IP 是什麼,這個 Client Pod3 就會拿這個 Service IP 去做請求,它的請求到宿主機的網絡之後,就會被 kube-proxy 所配置的 iptables 或者 IPVS 去做一層攔截處理,之後去負載均衡到每一個實際的後端 pod 上面去,這樣就實現了一個負載均衡以及服務發現。

對於外部的流量,比如說剛才通過公網訪問的一個請求。它是通過外部的一個負載均衡器 Cloud Controller Manager 去監聽 service 的變化之後,去配置的一個負載均衡器,然後轉發到節點上的一個 NodePort 上面去,NodePort 也會經過 kube-proxy 的一個配置的一個 iptables,把 NodePort 的流量轉換成 ClusterIP,緊接着轉換成後端的一個 pod 的 IP 地址,去做負載均衡以及服務發現。這就是整個 K8s 服務發現以及 K8s Service 整體的結構。

後續進階

後續再進階部分我們還會更加深入地去講解 K8s Service 的實現原理,以及在 service 網絡出問題之後,如何去診斷以及去修復的技巧。

本文總結

本文的主要內容就到此為止了,這裏為大家簡單總結一下:

  1. 為什麼雲原生的場景需要服務發現和負載均衡,
  2. 在 Kubernetes 中如何使用 Kubernetes 的 Service 做服務發現和負載均衡
  3. Kubernetes 集群中 Service 涉及到的組件和大概實現原理

相信經過本文的學習與把握,大家能夠通過 Kubernetes Service 將複雜的企業級應用快速並標準地編排起來。

“阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※高價收購3C產品,價格不怕你比較

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