BMW 2017年目標售出10萬輛電動車

德國媒體報導,汽車廠商BMW CEO Harald Krueger 在接受採訪時表示,電動車的時代即將到來,這一市場還有巨大的潛力,BMW 將在2017 年力爭把電動車的銷量提升到10 萬台,目前該公司2015 年電動車和混合動力車的銷量大約為6 萬台。

BMW CEO Harald Krueger 表示,該公司希望能夠在2017 年大幅提升電動車的銷量,至少提升六成,達到年銷量10 萬台。BMW 2016 年電動車和混合動力車的銷量合計大約為6 萬台,自2013 年發售電動車以來,BMW 電動車的雷軍銷量已經超過了10 萬台。

Harald Krueger 認為電動車的時代即將到來,這一市場還有很大的潛力,需求還沒有完全被發掘。

BMW 旗下唯一的純電動車車款i3 上市以來市場反響不如預期,2015 年i3 僅售出了2.5 萬台,為了提升這款產品的競爭力,BMW 將在2016 年的升級中把i3 系列電動車的續航里程提升50%。

在高階汽車市場中,BMW 的產品銷量已經落後於Benz,該公司希望在2025 年前將電動車和混合動力車的銷量提升到15%-25%。

(本文由《》授權提供。照片來源:)

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

聚甘新

電動車有害道路安全?美國規定汽車不能太安靜!

根據外電報導,美國政府正式通過一則汽車行駛新規定,也就是未來一些「安靜的汽車」,如電動汽車或混合動力汽車等,在行駛速度超過時速18.6 英里 (約30 公里)的情況下,必須增加噪音警報裝置,以避免傷及行人、盲人和騎車的路人。

根據美國國家公路交通安全管理局(NHTSA)發布的這則新規定表示,該規定將在2019 年9 月1 日起生效。屆時,包括特斯拉 (TESLA)、日產汽車 (Nisson) 和豐田汽車 (TOYOTA) 等廠商都需要為所有汽車增加噪音裝置。美國政府希望通過此規定,能夠防止每年多達2,400 件的行人受傷事故,並為53 萬輛汽車增設噪音專制。

NHTSA 指出,這項新規每年約會給汽車廠商增加3,900 萬美元的花費。因為,他們要為汽車增設一個外設防水喇叭。但是,能夠透過降低交通事故而每年節省約2.5 億到3.2 億美元的醫療費用。

該規定未來將適用於全美各地區,並且涵蓋路上行駛的電動汽車和混合動力汽車等。另外,包括帶有4 個輪子、總重量低於10,000 磅的車輛,也都必須在向後或向前行駛時,在速度達到每小時30 公里之際發出聲響,以警告周遭的路人與騎車的民眾。不過,NHTSA 也認為,規定汽車喇叭可有可無,因為輪胎和風也可以製造出足夠的噪音聲響,只要該聲響能達到警告的標準即可。

該項新的規定雖然受到全美盲人主組織的歡迎,但是卻讓汽車廠商表示擔憂。因為噪音也許會太大,並且實施起來將可能相當複雜。因為,新規只規定了最低的聲音要求,卻沒有制定必須發出什麼聲音。

(本文由《》授權提供。照片來源:特斯拉)

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

聚甘新

.Net Core微服務入門全紀錄(五)——Ocelot-API網關(下)

前言

上一篇【.Net Core微服務入門全紀錄(四)——Ocelot-API網關(上)】已經完成了Ocelot網關的基本搭建,實現了服務入口的統一。當然,這隻是API網關的一個最基本功能,它的進階功能還有很多很多。

服務發現

首先需要解決的就是服務發現的問題,服務發現的優點之前講過,就不說了。
上一篇中我們的服務地址都是寫在ocelot.json配置文件里,現在我們需要結合之前的Consul來實現服務發現。

  • 改造代碼:

首先NuGet安裝Ocelot.Provider.Consul

修改Startup.cs:

        public void ConfigureServices(IServiceCollection services)
        {
            //添加ocelot服務
            services.AddOcelot()
                .AddConsul();//添加consul支持
        }

修改ocelot.json配置:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "OrderService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    }
  }
}

這個配置應該很好理解,就是把我們上次的DownstreamHostAndPorts節點去掉了,然後增加了ServiceDiscoveryProvider服務發現相關配置。
注意,Ocelot除了支持Consul服務發現以外,還有Eureka也可以,Eureka也是一個類似的註冊中心。

好了,代碼修改就差不多了,下面運行程序測試一下:

客戶端正常運行。

至此我們就實現了服務註冊與發現和api網關的基本功能。接下來就要提到:服務治理

服務治理

其實服務治理也沒有一個非常明確的定義。它的作用簡單來說,就是幫助我們更好的管理服務,提升服務的可用性。——緩存,限流,熔斷,鏈路追蹤 等等。。。都屬於常用的服務治理手段。
之前講的負載均衡,服務發現也可以算是服務治理。

  • 緩存:

在Ocelot中啟用緩存,需要NuGet安裝一下Ocelot.Cache.CacheManager

修改Startup.cs中的ConfigureServices()方法:

//添加ocelot服務
services.AddOcelot()
    //添加consul支持
    .AddConsul()
    //添加緩存
    .AddCacheManager(x =>
    {
        x.WithDictionaryHandle();
    });

修改ocelot.json配置文件:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "OrderService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    }
  }
}

在Routes路由配置中增加了FileCacheOptions。TtlSeconds代表緩存的過期時間,Region代表緩衝區名稱,這個我們目前用不到。

好了,代碼修改完需要編譯重啟一下網關項目,然後打開客戶端網站測試一下:

可以看到,5秒之內的請求都是同樣的緩存數據。Ocelot也支持自定義緩存。

  • 限流:

限流就是限制客戶端一定時間內的請求次數。
繼續修改配置:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "ProductService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      },
      "RateLimitOptions": {
        "ClientWhitelist": [ "SuperClient" ],
        "EnableRateLimiting": true,
        "Period": "5s",
        "PeriodTimespan": 2,
        "Limit": 1
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "OrderService",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      },
      "RateLimitOptions": {
        "ClientWhitelist": [ "SuperClient" ],
        "EnableRateLimiting": true,
        "Period": "5s",
        "PeriodTimespan": 2,
        "Limit": 2
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    },
    "RateLimitOptions": {
      "DisableRateLimitHeaders": false,
      "QuotaExceededMessage": "too many requests...",
      "HttpStatusCode": 999,
      "ClientIdHeader": "Test"
    }
  }
}

在Routes路由配置中增加了RateLimitOptions。ClientWhitelist代表客戶端白名單,在白名單中的客戶端可以不受限流的影響;EnableRateLimiting代表是否限流;Period代表限流的單位時間,例如1s,5m,1h,1d等;PeriodTimespan代表客戶端達到請求上限多少秒后可以重試;Limit代表客戶端在定義的時間內可以發出的最大請求數。
在GlobalConfiguration配置中也增加了RateLimitOptions。DisableRateLimitHeaders代表是否禁用X-Rate-Limit和Retry-After標頭(請求達到上限時response header中的限制數和多少秒后能重試);QuotaExceededMessage:代表請求達到上限時返回給客戶端的消息;HttpStatusCode:代表請求達到上限時返回給客戶端的HTTP狀態代碼。ClientIdHeader可以允許自定義用於標識客戶端的標頭。默認情況下為“ ClientId”。
最重要的就是Period,PeriodTimespan,Limit這幾個配置。

重新編譯啟動看一下效果:

  • 超時/熔斷

超時很好理解,就是網關請求服務時可容忍的最長響應時間。熔斷的意思就是當請求某個服務的異常次數達到一定量時,那麼網關在一定時間內就不再對這個服務發起請求了,直接熔斷。
Ocelot中啟用 超時/熔斷 需要NuGet安裝一下Ocelot.Provider.Polly

修改Startup.cs中的ConfigureServices()方法:

//添加ocelot服務
services.AddOcelot()
    //添加consul支持
    .AddConsul()
    //添加緩存
    .AddCacheManager(x =>
    {
        x.WithDictionaryHandle();
    })
    //添加Polly
    .AddPolly();

同樣的在ocelot.json路由配置中增加QoSOptions:

"QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3,
        "DurationOfBreak": 10000,
        "TimeoutValue": 5000
      }

ExceptionsAllowedBeforeBreaking代表發生錯誤的次數,DurationOfBreak代表熔斷時間,TimeoutValue代表超時時間。
以上的配置意思就是當服務發生3次錯誤時,那麼就熔斷10秒,期間客戶端的請求直接返回錯誤,10秒之後恢復。
這個不太好模擬,就不演示了,應該也挺好理解的。

。。。。。。

關於服務治理的學問還有很多,不繼續了。。。就到此為止吧。
想要更深入了解Ocelot的,請看官網:https://ocelot.readthedocs.io/en/latest/
或者看源碼:https://github.com/ThreeMammals/Ocelot

下一篇準備說一下:事件總線。

代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

未完待續…

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

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

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

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

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

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

※超省錢租車方案

聚甘新

重學 Java 設計模式:實戰責任鏈模式「模擬618電商大促期間,項目上線流程多級負責人審批場景」

作者:小傅哥
博客:https://bugstack.cn – 原創系列專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

場地和場景的重要性

射擊需要去靶場學習、滑雪需要去雪場體驗、開車需要能上路實踐,而編程開發除了能完成產品的功能流程,還需要保證系統的可靠性能。就像你能聽到的一些系統監控指標;QPSTPSTP99TP999可用率響應時長等等,而這些指標的總和評估就是一個系統的健康度。但如果你幾乎沒有聽到這樣的技術術語,也沒接觸過類似高併發場景,那麼就很像駕駛證的科目1考了100分,但不能上路。沒有這樣的技術場景給你訓練,讓你不斷的體會系統的脾氣秉性,即便你有再多的想法都沒法實現。所以,如果真的想學習一定要去一個有實操的場景,下水試試才能學會狗刨。

你的視覺盲區有多大

同樣一本書、同樣一條路、同樣一座城,你真的以為生活有選擇嗎?有時候很多選項都是擺設,給你多少次機會你都選的一模一樣。這不是你選不選而是你的認知範圍決定了你下一秒做的事情,另外的一個下一秒又決定了再下一個下一秒。就像管中窺豹一樣,20%的面積在你視覺里都是黑色的,甚至就總是忽略看不到,而這看不到的20%就是生命中的時運!但,人可以學習,可以成長,可以脫胎換骨,可以努力付出,通過一次次的蛻變而看到剩下的20%!

沒有設計圖紙你敢蓋樓嗎

編程開發中最好的什麼,是設計。運用架構思維、經驗心得、才華靈感,構建出最佳的系統。真正的研發會把自己寫的代碼當做作品來欣賞,你說這是一份工作,但在這樣的人眼裡這可不是一份工作,而是一份工匠精神。就像可能時而你也會為自己因為一個niubility的設計而豪邁萬丈,為能上線一個扛得住每秒200萬訪問量的系統會精神煥發。這樣的自豪感就是一次次壘磚一樣墊高腳底,不斷的把你的視野提高,讓你能看到上層設計也能知曉根基建設。可以把控全局,也可以治理細節。這一份份知識的沉澱,來幫助你繪製出一張系統架構藍圖。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-13-00 場景模擬工程;模擬一個上線流程審批的接口。
itstack-demo-design-13-01 使用一坨代碼實現業務需求
itstack-demo-design-13-02 通過設計模式優化改造代碼,產生對比性從而學習

三、責任鏈模式介紹

擊鼓傳雷,看上圖你是否想起周星馳有一個電影,大家坐在海邊圍成一個圈,拿着一個點燃的炸彈,互相傳遞。

責任鏈模式的核心是解決一組服務中的先後執行處理關係,就有點像你沒錢花了,需要家庭財務支出審批,10塊錢以下找閨女審批,100塊錢先閨女審批在媳婦審批。你可以理解想象成當你要跳槽的時候被安排的明明白白的被各個領導簽字放行。

四、案例場景模擬

在本案例中我們模擬在618大促期間的業務系統上線審批流程場景

像是這些一線電商類的互聯網公司,阿里、京東、拼多多等,在618期間都會做一些運營活動場景以及提供的擴容備戰,就像過年期間百度的紅包一樣。但是所有開發的這些系統都需要陸續的上線,因為臨近618有時候也有一些緊急的調整的需要上線,但為了保障線上系統的穩定性是盡可能的減少上線的,也會相應的增強審批力度。就像一級響應、二級響應一樣。

而這審批的過程在隨着特定時間點會增加不同級別的負責人加入,每個人就像責任鏈模式中的每一個核心點。對於研發小夥伴並不需要關心具體的審批流程處理細節,只需要知道這個上線更嚴格,級別也更高,但對於研發人員來說同樣是點擊相同的提審按鈕,等待審核。

接下來我們就模擬這樣一個業務訴求場景,使用責任鏈的設計模式來實現此功能。

1. 場景模擬工程

itstack-demo-design-13-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── AuthService.java
  • 這裏的代碼結構比較簡單,只有一個模擬審核和查詢審核結果的服務類。相當於你可以調用這個類去審核工程和獲取審核結構,這部分結果信息是模擬的寫到緩存實現。

2. 場景簡述

2.1 模擬審核服務

public class AuthService {

    private static Map<String, Date> authMap = new ConcurrentHashMap<String, Date>();

    public static Date queryAuthInfo(String uId, String orderId) {
        return authMap.get(uId.concat(orderId));
    }

    public static void auth(String uId, String orderId) {
        authMap.put(uId.concat(orderId), new Date());
    }

}
  • 這裏面提供了兩個接口一個是查詢審核結果(queryAuthInfo)、另外一個是處理審核(auth)。
  • 這部分是把由誰審核的和審核的單子ID作為唯一key值記錄到內存Map結構中。

五、用一坨坨代碼實現

這裏我們先使用最直接的方式來實現功能

按照我們的需求審批流程,平常系統上線只需要三級負責人審批就可以,但是到了618大促時間點,就需要由二級負責以及一級負責人一起加入審批系統上線流程。在這裏我們使用非常直接的if判斷方式來實現這樣的需求。

1. 工程結構

itstack-demo-design-13-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── AuthController.java
  • 這部分非常簡單的只包含了一個審核的控制類,就像有些夥伴開始寫代碼一樣,一個類寫所有需求。

2. 代碼實現

public class AuthController {

    private SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 時間格式化

    public AuthInfo doAuth(String uId, String orderId, Date authDate) throws ParseException {

        // 三級審批
        Date date = AuthService.queryAuthInfo("1000013", orderId);
        if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態:待三級審批負責人 ", "王工");

        // 二級審批
        if (authDate.after(f.parse("2020-06-01 00:00:00")) && authDate.before(f.parse("2020-06-25 23:59:59"))) {
            date = AuthService.queryAuthInfo("1000012", orderId);
            if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態:待二級審批負責人 ", "張經理");
        }

        // 一級審批
        if (authDate.after(f.parse("2020-06-11 00:00:00")) && authDate.before(f.parse("2020-06-20 23:59:59"))) {
            date = AuthService.queryAuthInfo("1000011", orderId);
            if (null == date) return new AuthInfo("0001", "單號:", orderId, " 狀態:待一級審批負責人 ", "段總");
        }

        return new AuthInfo("0001", "單號:", orderId, " 狀態:審批完成");
    }

}
  • 這裏從上到下分別判斷了在指定時間範圍內由不同的人員進行審批,就像618上線的時候需要三個負責人都審批才能讓系統進行上線。
  • 像是這樣的功能看起來很簡單的,但是實際的業務中會有很多部門,但如果這樣實現就很難進行擴展,並且在改動擴展調整也非常麻煩。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_AuthController() throws ParseException {
    AuthController authController = new AuthController();  

    // 模擬三級負責人審批
    logger.info("測試結果:{}", JSON.toJSONString(authController.doAuth("小傅哥", "1000998004813441", new Date())));
    logger.info("測試結果:{}", "模擬三級負責人審批,王工");
    AuthService.auth("1000013", "1000998004813441");  

    // 模擬二級負責人審批                                 
    logger.info("測試結果:{}", JSON.toJSONString(authController.doAuth("小傅哥", "1000998004813441", new Date())));
    logger.info("測試結果:{}", "模擬二級負責人審批,張經理");
    AuthService.auth("1000012", "1000998004813441");    

    // 模擬一級負責人審批
    logger.info("測試結果:{}", JSON.toJSONString(authController.doAuth("小傅哥", "1000998004813441", new Date())));
    logger.info("測試結果:{}", "模擬一級負責人審批,段總");
    AuthService.auth("1000011", "1000998004813441");            

    logger.info("測試結果:{}", "審批完成");
}
  • 這裏模擬每次查詢是否審批完成,隨着審批的不同節點,之後繼續由不同的負責人進行審批操作。
  • authController.doAuth,是查看審批的流程節點、AuthService.auth,是審批方法用於操作節點流程狀態。

3.2 測試結果

23:25:00.363 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待三級審批負責人 王工"}
23:25:00.366 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬三級負責人審批,王工
23:25:00.367 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待二級審批負責人 張經理"}
23:25:00.367 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬二級負責人審批,張經理
23:25:00.368 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待一級審批負責人 段總"}
23:25:00.368 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬一級負責人審批,段總
23:25:00.368 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:審批完成

Process finished with exit code 0
  • 從測試結果上可以看到一層層的由不同的人員進行審批,審批完成後到下一個人進行處理。單看結果是滿足我們的訴求,只不過很難擴展和調整流程,相當於代碼寫的死死的。

六、責任鏈模式重構代碼

接下來使用裝飾器模式來進行代碼優化,也算是一次很小的重構。

責任鏈模式可以讓各個服務模塊更加清晰,而每一個模塊間可以通過next的方式進行獲取。而每一個next是由繼承的統一抽象類實現的。最終所有類的職責可以動態的進行編排使用,編排的過程可以做成可配置化。

1. 工程結構

itstack-demo-design-13-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │    ├── Level1AuthLink.java
                │    ├── Level2AuthLink.java
                │    └── Level3AuthLink.java
                ├── AuthInfo.java
                └── AuthLink.java

責任鏈模式模型結構

  • 上圖是這個業務模型中責任鏈結構的核心部分,通過三個實現了統一抽象類AuthLink的不同規則,再進行責任編排模擬出一條鏈路。這個鏈路就是業務中的責任鏈。
  • 一般在使用責任鏈時候如果是場景比較固定,可以通過寫死到代碼中進行初始化。但如果業務場景經常變化可以做成xml配置的方式進行處理,也可以落到庫里進行初始化操作。

2. 代碼實現

2.1 責任鏈中返回對象定義

public class AuthInfo {

    private String code;
    private String info = "";

    public AuthInfo(String code, String ...infos) {
        this.code = code;
        for (String str:infos){
            this.info = this.info.concat(str);
        }
    }
    
    // ...get/set
}
  • 這個類的是包裝了責任鏈處理過程中返回結果的類,方面處理每個責任鏈的返回信息。

2.2 鏈路抽象類定義

public abstract class AuthLink {

    protected Logger logger = LoggerFactory.getLogger(AuthLink.class);

    protected SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 時間格式化
    protected String levelUserId;                           // 級別人員ID
    protected String levelUserName;                         // 級別人員姓名
    private AuthLink next;                                  // 責任鏈

    public AuthLink(String levelUserId, String levelUserName) {
        this.levelUserId = levelUserId;
        this.levelUserName = levelUserName;
    }

    public AuthLink next() {
        return next;
    }

    public AuthLink appendNext(AuthLink next) {
        this.next = next;
        return this;
    }

    public abstract AuthInfo doAuth(String uId, String orderId, Date authDate);

}
  • 這部分是責任鏈,鏈接起來的核心部分。AuthLink next,重點在於可以通過next方式獲取下一個鏈路需要處理的節點。
  • levelUserIdlevelUserName,是責任鏈中的公用信息,標記每一個審核節點的人員信息。
  • 抽象類中定義了一個抽象方法,abstract AuthInfo doAuth,這是每一個實現者必須實現的類,不同的審核級別處理不同的業務。

2.3 三個審核實現類

Level1AuthLink

public class Level1AuthLink extends AuthLink {

    public Level1AuthLink(String levelUserId, String levelUserName) {
        super(levelUserId, levelUserName);
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "單號:", orderId, " 狀態:待一級審批負責人 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:一級審批完成負責人", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        return next.doAuth(uId, orderId, authDate);
    }

}

Level2AuthLink

public class Level2AuthLink extends AuthLink {

    private Date beginDate = f.parse("2020-06-11 00:00:00");
    private Date endDate = f.parse("2020-06-20 23:59:59");

    public Level2AuthLink(String levelUserId, String levelUserName) throws ParseException {
        super(levelUserId, levelUserName);
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "單號:", orderId, " 狀態:待二級審批負責人 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:二級審批完成負責人", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        if (authDate.before(beginDate) || authDate.after(endDate)) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:二級審批完成負責人", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        return next.doAuth(uId, orderId, authDate);
    }

}

Level3AuthLink

public class Level3AuthLink extends AuthLink {

    private Date beginDate = f.parse("2020-06-01 00:00:00");
    private Date endDate = f.parse("2020-06-25 23:59:59");

    public Level3AuthLink(String levelUserId, String levelUserName) throws ParseException {
        super(levelUserId, levelUserName);
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "單號:", orderId, " 狀態:待三級審批負責人 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:三級審批負責人完成", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        if (authDate.before(beginDate) || authDate.after(endDate)) {
            return new AuthInfo("0000", "單號:", orderId, " 狀態:三級審批負責人完成", " 時間:", f.format(date), " 審批人:", levelUserName);
        }

        return next.doAuth(uId, orderId, authDate);
    }

}
  • 如上三個類;Level1AuthLinkLevel2AuthLinkLevel3AuthLink,實現了不同的審核級別處理的簡單邏輯。
  • 例如第一個審核類中會先判斷是否審核通過,如果沒有審核通過則返回結果給調用方,引導去審核。(這裏簡單模擬審核後有時間信息不為空,作為判斷條件)
  • 判斷完成后獲取下一個審核節點;super.next();,如果不存在下一個節點,則直接返回結果。
  • 之後是根據不同的業務時間段進行判斷是否需要,二級和一級的審核。
  • 最後返回下一個審核結果;next.doAuth(uId, orderId, authDate);,有點像遞歸調用。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_AuthLink() throws ParseException {
    AuthLink authLink = new Level3AuthLink("1000013", "王工")
            .appendNext(new Level2AuthLink("1000012", "張經理")
                    .appendNext(new Level1AuthLink("1000011", "段總")));

    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

    // 模擬三級負責人審批
    AuthService.auth("1000013", "1000998004813441");
    logger.info("測試結果:{}", "模擬三級負責人審批,王工");
    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

    // 模擬二級負責人審批
    AuthService.auth("1000012", "1000998004813441");
    logger.info("測試結果:{}", "模擬二級負責人審批,張經理");
    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

    // 模擬一級負責人審批
    AuthService.auth("1000011", "1000998004813441");
    logger.info("測試結果:{}", "模擬一級負責人審批,段總");
    logger.info("測試結果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));
}
  • 這裏包括最核心的責任鏈創建,實際的業務中會包裝到控制層; AuthLink authLink = new Level3AuthLink("1000013", "王工") .appendNext(new Level2AuthLink("1000012", "張經理") .appendNext(new Level1AuthLink("1000011", "段總"))); 通過把不同的責任節點進行組裝,構成一條完整業務的責任鏈。
  • 接下里不斷的執行查看審核鏈路authLink.doAuth(...),通過返回結果對數據進行3、2、1級負責人審核,直至最後審核全部完成。

3.2 測試結果

23:49:46.585 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待三級審批負責人 王工"}
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬三級負責人審批,王工
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待二級審批負責人 張經理"}
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬二級負責人審批,張經理
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0001","info":"單號:1000998004813441 狀態:待一級審批負責人 段總"}
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:模擬一級負責人審批,段總
23:49:46.590 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"code":"0000","info":"單號:1000998004813441 狀態:一級審批完成負責人 時間:2020-06-18 23:49:46 審批人:段總"}

Process finished with exit code 0
  • 從上述的結果可以看到我們的責任鏈已經生效,按照責任鏈的結構一層層審批,直至最後輸出審批結束到一級完成的結果。
  • 這樣責任鏈的設計方式可以方便的進行擴展和維護,也把if語句幹掉了。

七、總結

  • 從上面代碼從if語句重構到使用責任鏈模式開發可以看到,我們的代碼結構變得清晰乾淨了,也解決了大量if語句的使用。並不是if語句不好,只不過if語句並不適合做系統流程設計,但是在做判斷和行為邏輯處理中還是非常可以使用的。
  • 在我們前面學習結構性模式中講到過組合模式,它像是一顆組合樹一樣,我們搭建出一個流程決策樹。其實這樣的模式也是可以和責任鏈模型進行組合擴展使用,而這部分的重點在於如何關聯鏈路的關聯,最終的執行都是在執行在中間的關係鏈。
  • 責任鏈模式很好的處理單一職責和開閉原則,簡單了耦合也使對象關係更加清晰,而且外部的調用方並不需要關心責任鏈是如何進行處理的(以上程序中可以把責任鏈的組合進行包裝,在提供給外部使用)。但除了這些優點外也需要是適當的場景才進行使用,避免造成性能以及編排混亂調試測試疏漏問題。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
  • 2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
  • 3. 重學 Java 設計模式:實戰橋接模式「多支付渠道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
  • 4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
  • 5. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

聚甘新

特性速覽| Apache Hudi 0.5.3版本正式發布

1. 下載連接

  • 源代碼下載:Apache Hudi 0.5.3 Source Release (asc, sha512)
  • 0.5.3版本相關jar包地址:https://repository.apache.org/#nexus-search;quick~hudi

2. 遷移指南

  • 這是一個bugfix版本,從0.5.2升級時不需要任何特殊的遷移步驟。如果要從早期版本”X”升級,請閱讀”X”和0.5.3之間的每個後續版本的遷移指南。
  • 0.5.3是Hudi畢業后的第一個版本,因此所有hudi jar的版本名稱中不再帶有”-incubating”。在所有提及hudi版本的地方,請確保不再存在”-incubating”。

例如,hudi-spark-bundle pom依賴如下所示:

<dependency>
	<groupId>org.apache.hudi</groupId>
	<artifactId>hudi-spark-bundle_2.12</artifactId>
	<version>0.5.3</version>
</dependency>

3. 關鍵特性

  • Hudi內置支持 aliyun OSS 對象存儲。

  • 默認情況下將為delta-streamer和spark datasource寫入啟用Embedded Timeline Server。在此版本之前,此功能處於實驗模式,embeddedTimeline Server在Spark Driver中緩存文件列表,並提供Restful接口給Spark Writer任務調用來減少了每次寫入時的list文件列表的操作,此優化對雲上對象存儲非常友好。

  • 默認情況下為delta-streamer和Spark datasource寫入均啟用”增量清理(incremental cleaning)”。在此版本之前,此功能還處於實驗模式,在穩定狀態下,增量清理避免了掃描所有分區的昂貴步驟,而是使用Hudi元數據來查找要清理的文件,此優化也對雲上對象存儲非常友好。

  • 支持將Delta-Streamer配置文件放置在與實際數據不同的文件系統中。

  • Hudi Hive Sync現在支持按日期類型列分區的表。

  • Hudi Hive Sync現在支持直接通過Hive MetaStore進行同步。您只需要設置hoodie.datasource.hive_sync.use_jdbc = false。Hive Metastore Uri將從environment中隱式讀取。例如當通過Spark datasource寫入時,

     spark.write.format(“hudi”)
     .option(…)
     .option(“hoodie.datasource.hive_sync.username”, “<user>”)
     .option(“hoodie.datasource.hive_sync.password”, “<password>”)
     .option(“hoodie.datasource.hive_sync.partition_fields”, “<partition_fields>”)
     .option(“hoodie.datasource.hive_sync.database”, “<db_name>”)
     .option(“hoodie.datasource.hive_sync.table”, “<table_name>”)
     .option(“hoodie.datasource.hive_sync.use_jdbc”, “false”)
     .mode(APPEND)
     .save(“/path/to/dataset”)
    
  • 支持Presto查詢MoR表時Hudi側的改造。

  • 其他與Writer Performance相關的缺陷修復。

    • 現在DataSource Writer避免了寫入后不必要的數據加載。
    • Hudi Writer現在利用spark的併發來加速小文件查找。

4. 感謝

感謝如下貢獻者(排名不分先後): @bhasudha,@yanghua ,@ddong ,@smarthi ,@afilipchik,@zhedoubushishi,@umehrot2,@varadar,@ffcchi,@bschell,@vinothchandar ,@shenh062326,@lamber-ken,@zhaomin1423,@EdwinGuo,@prashantwason ,@pratyakshsharma,@dengziming ,@AakashPradeep,@Jecarm ,@xushiyan ,@cxzl25,@garyli1019 ,@rolandjohann ,@nsivabalan,@leesf ,@jfrazee

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

聚甘新

一起玩轉微服務(5)——分層架構

領域驅動設計DDD(Domain Driven Design)提出了從業務設計到代碼實現一致性的要求,不再對分析模型和實現模型進行區分。也就是說從代碼的結構中我們可以直接理解業務的設計,命名得當的話,非程序人員也可以“讀”代碼。這與微服務設計中的約定優於配置不謀而合,如果你熟悉英文,那麼直接根據包名和類名就可以直接解讀出程序開發者所構建的業務的大概意圖。

領域模型包含一些明確定義的類型:

  • 實體是一個對象,它有固定的身份,具有明確定義的”連續性線索”或生命周期。通常列舉的示例是一個 Person(一個實體)。大多數系統都需要唯一地跟蹤一個 Person,無論姓名、地址或其他屬性是否更改。
  • l值對象沒有明確定義的身份,而僅由它們的屬性定義。它們通常不可變,所以兩個相等的值對象始終保持相等。地址可以是與 Person 關聯的值對象。
  • l集合是一個相關對象集群,這些對象被看作一個整體。它擁有一個特定實體作為它的根,並定義了明確的封裝邊界。它不只是一個列表。
  • l服務用於表示不是實體或值對象的自然部分的操作或活動。

領域模型在實現時可大可小,在業務的早期,在系統比較小的情況下,它有可能是一個類。當系統做大了以後,它可能是個庫。再做更大一點的時候,它可能是一個服務,給不同的應用去調用。

要將領域元素轉換為服務,可按照以下一般準則來完成此操作:

  • 使用值對象的表示作為參數和返回值,將集合和實體轉換為獨立的微服務。
  • 將領域服務(未附加到集合或實體的服務)與獨立的微服務相匹配。
  • 每個微服務應處理一個完整的業務功能。

領域模型又可以分為失血、貧血和充血3種。

  • 失血模型:基於數據庫的領域設計方式就是典型的失血模型,只關注數據的增刪改查。
  • 貧血模型:就是在domain object包含了不依賴於持久化的領域邏輯,而那些依賴持久化的領域邏輯被分離到server層。
  • 充血模型:充血模型跟貧血模型差不多,不同的是如何劃分業務邏輯,就是說,約大部分業務應該放到domain object裏面,而service應該是很薄的一層。

設計原則之分層架構

同一公司使用統一應用分層,以減少開發維護學習成本。應用分層這件事情看起來很簡單,但每個程序員都有自己的一套,哪怕是初學者,所以想實施起來並非那麼容易。

最早接觸分層架構的應該是我們最熟悉的MVC(Model-View-Controller)架構,將應用分成了模型、視圖和控制層,可以說引導了絕大多數開發者,而我們現在的應用中非常多的包括框架,架構設計都使用此模式。這后又演化出了MVP(Model-View-Presenter)和MVVM(Model-View-ViewModel)。這些可以說都是隨着技術的不斷髮展,為了應對不同場景所演化出來的模型。而微服務的每個架構都可以再細分成領域模型,下面看一下經典的領域模型架構。

它包括了Domain,Service Layer和Repositories。核心實體(Entity)和值對象(Value Object)應該在Domain層,定義的領域服務(Domain Service)在Service Layer,而針對實體和值對象的存儲和查詢邏輯都應該在Repositories層。值得注意的是,不要把Entity的屬性和行為分離到Domain和Service兩層中去實現,即所謂的貧血模型,事實證明這樣的實現方式會造成很大的維護問題。基於這種設計,工程的結構可以構造為:

– MicroService-Sample/src/

    domain

    gateways

    interface

    repositories

    services

當然,在微服務的架構中,每個微服務不必嚴格遵照這樣的規定,切忌死搬硬套,最重要的是理解,在不同的業務場合,架構的設計可以適當的做調整,畢竟適合的架構一定要具有靈活性。

分層的原則包括:

  • 文件夾分層法

應用分層採用文件夾方式的優點是可大可小、簡單易用、統一規範,可以包括 5 個項目,也可以包括 50 個項目,以滿足所有業務應用的多種不同場景。

  • 調用規約

在開發過程中,需要遵循分層架構的約束,禁止跨層次的調用。

  • 下層為上層服務

以用戶為中心,以目標為導向。上層(業務邏輯層)需要什麼,下層(數據訪問層)提供什麼,而不是下層(數據訪問層)有什麼,就向上層(業務邏輯層)提供什麼。

  • 實體層規約

Entity是數據表對象,不是數據訪問層對象;DTO 是網絡傳輸對象,不是表現層對象;BO 是內存計算邏輯對象,不是業務邏輯層對象,不是只能給業務邏輯層使用 。如果僅限定在本層訪問,則導致單個應用內大量沒有價值的對象轉換。以用戶為中心來設計實體類,可以減少無價值重複對象和無用轉換。

  • U 型訪問

下行時表現層是 Input,業務邏輯層是 Process,數據訪問層是 Output。上行時數據訪問層是 Input,業務邏輯層是 Process,  表現層就 Output。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

聚甘新

Mybatis Generator逆向工程的使用

一、MyBatis Generator簡介

    MyBatis Generator(MBG)是MyBatis和iBATIS的代碼生成器。它將為所有版本的MyBatis以及版本2.2.0之後的iBATIS版本生成代碼。它將審查數據庫表(或許多表),並將生成可用於訪問表的構件。這減少了設置對象和配置文件以與數據庫表交互的初始麻煩。MBG尋求對簡單CRUD(創建,檢索,更新,刪除)的大部分數據庫操作產生重大影響。您仍然需要為連接查詢或存儲過程手動編寫SQL和對象代碼。MyBatis Generator將生成:

  • 與表結構匹配的Java POJO。這可能包括:

    • 一個匹配表的主鍵的類(如果有主鍵)

    • 一個匹配表的非主鍵字段的類(BLOB字段除外)

    • 包含表的BLOB字段的類(如果表具有BLOB字段)

    • 用於啟用動態選擇,更新和刪除的類

    這些類之間存在適當的繼承關係。請注意,生成器可以配置為生成不同類型的POJO層次結構 – 例如,如果您願意,可以選擇為每個表生成單個域對象。

  • MyBatis/iBATIS兼容的SQL Map XML文件。MBG為配置中的每個表上的簡單CRUD函數生成SQL。生成的SQL語句包括:

    • insert 插入

    • update by primary key  按主鍵更新

    • update by example (using a dynamic where clause)  通過條件更新(使用動態where子句)

    • delete by primary key  按主鍵刪除

    • delete by example (using a dynamic where clause)  按條件刪除(使用動態where子句)

    • select by primary key  按主鍵查詢

    • select by example (using a dynamic where clause)  按條件查詢(使用動態where子句)

    • count by example  按條件查詢記錄總數

    根據表結構的不同,這些語句有不同的變體(例如,如果表沒有主鍵,則MBG不會通過主鍵功能生成更新)。

    • 適當使用上述對象的Java客戶端類。Java客戶端類的生成是可選的。MBG將為MyBatis 3.x生成以下類型的Java客戶端:

      • 適用於MyBatis 3.x映射器基礎結構的映射器接口

MBG將為iBATIS 2.x生成以下類型的Java客戶端:

    • 符合Spring框架的DAO

    • 僅使用iBATIS SQL映射API的DAO。這些DAO可以生成兩種:通過構造函數或setter注入提供SqlMapClient。

    • 符合iBATIS DAO框架的DAO(iBATIS的可選部分,現在不推薦使用此框架,我們建議您使用Spring框架)

    MyBatis生成器設計為在迭代開發環境中運行良好,並且可以作為Ant任務或Maven插件包含在連續構建環境中。迭代運行MBG時需要注意的重要事項包括:

  1. 如果存在與新生成的XML文件同名的現有文件,MBG將自動合併XML文件。MBG不會覆蓋您對其生成的XML文件所做的任何自定義更改。您可以反覆運行它,而不必擔心會丟失對XML的自定義更改。MBG將替換先前運行中生成的任何XML元素。

  2. MBG不會合併Java文件,它可以覆蓋現有文件或使用不同的唯一名稱保存新生成的文件。如果對生成的Java文件進行更改並以迭代方式運行MBG,則必須手動合併更改。當作為Eclipse插件運行時 ,MBG可以自動合併Java文件。

二、MyBatis Generator使用

1、新建MBG的配置文件generatorConfig.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
 
 
    <!--導入屬性配置-->
    <properties resource="generator.properties"></properties>
    <!--指定特定數據庫的jdbc驅動jar包的位置-->
    <!--<classPathEntry location="${jdbc.driverLocation}"/>-->
 
    <context id="default" targetRuntime="MyBatis3">
 
        <!-- optional,旨在創建class時,對註釋進行控制,false生成註釋,true無註釋 -->
        <commentGenerator>
            <property name="suppressDate" value="false"/>
            <property name="suppressAllComments" value="false"/>
        </commentGenerator>
 
        <!--jdbc的數據庫連接 -->
        <jdbcConnection
                driverClass="${jdbc.driverClass}"
                connectionURL="${jdbc.connectionURL}"
                userId="${jdbc.userId}"
                password="${jdbc.password}">
        </jdbcConnection>
 
 
        <!--
         默認false,把JDBC DECIMAL 和 NUMERIC 類型解析為 Integer,
         為 true時把JDBC DECIMAL 和 NUMERIC 類型解析為java.math.BigDecimal
        -->
        <!-- 非必需,類型處理器,在數據庫類型和java類型之間的轉換控制-->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>
 
 
        <!-- Model模型生成器,用來生成含有主鍵key的類,記錄類 以及查詢Example類
            targetPackage     指定生成的model生成所在的包名
            targetProject     指定在該項目下所在的路徑|指定生成到的工程名稱
        -->
        <javaModelGenerator targetPackage="com.test.model"
                            targetProject=".\src\main\java">
            <!-- 是否允許子包,即targetPackage.schemaName.tableName -->
            <property name="enableSubPackages" value="false"/>
            <!-- 是否對model添加 構造函數 true添加,false不添加-->
            <property name="constructorBased" value="false"/>
            <!-- 是否對類CHAR類型的列的數據進行trim操作 -->
            <property name="trimStrings" value="true"/>
            <!-- 建立的Model對象是否 不可改變  即生成的Model對象不會有 setter方法,只有構造方法 -->
            <property name="immutable" value="false"/>
        </javaModelGenerator>
 
        <!--Mapper映射文件生成所在的目錄 為每一個數據庫的表生成對應的SqlMapper文件 -->
        <sqlMapGenerator targetPackage="com.test.mapper"
                         targetProject=".\src\main\java">
            <property name="enableSubPackages" value="false"/>
        </sqlMapGenerator>
 
        <!-- 客戶端代碼,生成易於使用的針對Model對象和XML配置文件 的代碼
                type="ANNOTATEDMAPPER",生成Java Model 和基於註解的Mapper對象
                type="MIXEDMAPPER",生成基於註解的Java Model 和相應的Mapper對象
                type="XMLMAPPER",生成SQLMapper XML文件和獨立的Mapper接口
        -->
        <javaClientGenerator targetPackage="com.test.mapper"
                             targetProject=".\src\main\java" type="XMLMAPPER">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>
 
        <!--需要映射的數據庫的表名-->
        <table tableName="t_userinfo" domainObjectName="UserInfo"
               enableCountByExample="false" enableUpdateByExample="false"
               enableDeleteByExample="false" enableSelectByExample="false"
               selectByExampleQueryId="false">
        </table>
    </context>
</generatorConfiguration>

2、新建generator.properties文件

jdbc.driverLocation=C:\\mysql-connector-java-5.1.43.jar
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://localhost:3306/mybatis
jdbc.userId=root
jdbc.password=tiger

3、配置執行mybatis generator操作,這裡有兩種方式

第1種方式:如果使用maven項目就可以省去編寫Java啟動類,使用maven插件和配置文件pom.xml即可,插件啟動maven-generator,在pom.xml中添加maven-generator插件

<plugins>
    <!--myBatis逆向工程插件-->
    <plugin>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-maven-plugin</artifactId>
        <version>1.3.2</version>
        <configuration>
            <verbose>true</verbose>
            <overwrite>true</overwrite>
            <configurationFile>${project.basedir}/src/main/resources/generatorConfig.xml</configurationFile>
        </configuration>
        <dependencies>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.35</version>
            </dependency>
        </dependencies>
    </plugin>
</plugins>

點擊mybatis-generator:generate就能執行mybatis generator了

第2種方式:

1、如果不是maven項目添加該mybatis-generator-core-1.3.2.jar,編寫main方法指向mybatis逆向工程,我給依賴粘貼到下面了

<!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
<dependency>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-core</artifactId>
    <version>1.3.2</version>
</dependency>

2、修改generatorConfig.xml文件,放開註釋的該配置

<classPathEntry location="${jdbc.driverLocation}"/>

3、然後編寫測試類執行

/**
 * 如果不是maven項目可以這樣生成
 */
public class MybatisGeneratorTest {
    public static void main(String[] args) throws InterruptedException, SQLException, IOException, InvalidConfigurationException, XMLParserException {
        List<String> warnings = new ArrayList<String>();
        //生成的java文件是否覆蓋
        boolean overwrite = true;
        //指定逆向工程配置文件
        //File configFile = new File("E:\\project\\mybatis-generator\\src\\main\\resources\\generatorConfig.xml");
        InputStream resourceAsStream = MybatisGeneratorTest.class.getClassLoader().getResourceAsStream("generatorConfig.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(resourceAsStream);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,callback, warnings);
        myBatisGenerator.generate(null);
    }
}

介紹完這兩種方式,查看數據表:

查看生成的實體類:


TIP:可以看出如果實體類想要遵循駝峰命名規範,數據庫表字段名設計需要用”_”來劃分

查看生成的文件信息:


TIP1:必須在<plugin></plugin>標籤里添加數據庫驅動,在其他地方添加無效,如果不添加會報找不到驅動錯誤,如過不在該插件添加數據庫依賴的話可以使用 <classPathEntry location=”${jdbc.driverLocation}”/> 來指定數據庫驅動位置。

TIP2:如果你在使用mybatis generator插件執行的時候報[ERROR] Failed to execute goal org.mybatis.generator:mybatis-generator-maven-plugin:1.3.2:generate (default-cli) on project mybatis-generator: <properties> resource generator.properties does not exist -> [Help 1]


儘管你的 <properties resource=”generator.properties”></properties>配置的沒有問題,但是還是找不到generator.properties,查看該配置,註釋掉


該配置會改變generatorConfig.xml中讀取generator.properties文件的默認路徑

TIP3:Mybatis Generator反向工程默認不會覆蓋生成的*.java文件。也可以設置覆蓋生成的*.java文件,在反向工程插件mybatis-generator-maven-plugin添加該配置<overwrite>true</overwrite>則會覆蓋生成的*.java文件,如圖


    Mybatis Generator不會覆蓋你的mapper.xml文件,MBG會合併追加到mapper.xml和你自定義的存在一起,但是如果你修改MBG第一次默認生成的SQL(MBG生成的CRUD),MBG會重新把自己生成的CRUD恢復默認,說白了,MBG只會覆蓋他自己生成的SQL,不會覆蓋你自定義的,你自定義的不變。。。如圖,他不會動你的自定義SQL,只會覆蓋Mybatis反向工程自己生成的SQL,前提MBG自動生成SQL語句的註釋要存在。


在最常見的用例中,MyBatis Generator(MBG)由XML配置文件驅動。配置文件告訴MBG

  • 如何連接到數據庫

  • 生成什麼對象,以及如何生成它們

  • 應使用哪些表生成對象

官方MBG配置文件詳解地址:http://mybatis.org/generator/configreference/xmlconfig.html

附帶一個MBG的中文配置文件詳解:https://www.jianshu.com/p/e09d2370b796

 

更多Mybatis逆向工程的使用參考:http://www.mybatis.org/generator/index.html


● XStream學習手冊

● 別在 Java 代碼里亂打日誌了,這才是正確的打日誌姿勢!

● 高可用Redis服務架構分析與搭建

● 8 種方案,幫你解決重複提交問題!請拿走

● IDEA 解決 Maven 依賴衝突的高能神器,這一篇夠不夠?

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

聚甘新

機器學習——打開集成方法的大門,手把手帶你實現AdaBoost模型

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是機器學習專題的第25篇文章,我們一起來聊聊AdaBoost。

我們目前為止已經學過了好幾個模型,光決策樹的生成算法就有三種。但是我們每次進行分類的時候,每次都是採用一個模型進行訓練和預測。我們日常在做一個決策的時候,往往會諮詢好幾個人,綜合採納他們的意見。那麼有沒有可能把這個思路照搬到機器學習領域當中,創建多個模型來綜合得出結果呢?

這當然是可以的,這樣的思路就叫做集成方法(ensemble method)。

集成方法

集成方法本身並不是某種具體的方法或者是算法,只是一種訓練機器學習模型的思路。它的含義只有一點,就是訓練多個模型,然後將它們的結果匯聚在一起。

根據這個思路,業內又衍生出了三種特定的方法,分別是Bagging、Boosting和Stacking。

Bagging

Bagging是bootstrap aggregating的縮寫,我們從字面上很難理解它的含義。我們記住這個名字即可,在Bagging方法當中,我們會通過有放回隨機採樣的方式創建K個數據集。對於每一個數據集來說,可能有一些單個的樣本重複出現,也可能有一些樣本從沒有出現過,但整體而言,每個樣本出現的概率是相同的。

之後,我們用抽樣出來的K個數據集訓練K個模型,這裏的模型沒有做限制,我們可以使用任何機器學習方模型。K個模型自然會得到K個結果,那麼我們採取民主投票的方式對這K個模型進行聚合。

舉個例子說,假設K=25,在一個二分類問題當中。有10個模型預測結果是0,15個模型預測結果是1。那麼最終整個模型的預測結果就是1,相當於K個模型民主投票,每個模型投票權一樣。大名鼎鼎的隨機森林就是採取的這種方式。

Boosting

Boosting的思路和Bagging非常相似,它們對於樣本的採樣邏輯是一致的。不同的是,在Boosting當中,這K個模型並不是同時訓練的,而是串行訓練的。每一個模型在訓練的時候都會基於之前模型的結果,更加關注於被之前模型判斷錯誤的樣本。同樣,樣本也會有一個權值,錯誤判斷率越大的樣本擁有越大的權值。

並且每一個模型根據它能力的不同,會被賦予不同的權重,最後會對所有模型進行加權求和,而不是公平投票。由於這個機制,使得模型在訓練的時候的效率也有差異。因為Bagging所有模型之間是完全獨立的,我們是可以採取分佈式訓練的。而Boosting中每一個模型會依賴之前模型的效果,所以只能串行訓練。

Stacking

Stacking是Kaggle比賽當中經常使用的方法,它的思路也非常簡單。我們選擇K種不同的模型,然後通過交叉驗證的方式,在訓練集上進行訓練和預測。保證每個模型都對所有的訓練樣本產出一個預測結果。那麼對於每一條訓練樣本,我們都能得到K個結果。

之後,我們再創建一個第二層的模型,它的訓練特徵就是這K個結果。也就是說Stacking方法當中會用到多層模型的結構,最後一層模型的訓練特徵是上層模型預測的結果。由模型自己去訓練究竟哪一個模型的結果更值得採納,以及如何組合模型之間的特長。

我們今天介紹的AdaBoost顧名思義,是一個經典的Boosting算法。

模型思路

AdaBoost的核心思路是通過使用Boosting的方法,通過一些弱分類器構建出強分類器來。

強分類器我們都很好理解,就是性能很強的模型,那麼弱分類器應該怎麼理解呢?模型的強弱其實是相對於隨機結果來定義的,比隨機結果越好的模型,它的性能越強。從這點出發,弱分類器也就是只比隨機結果略強的分類器。我們的目的是通過設計樣本和模型的權重,使得可以做出最佳決策,將這些弱分類器的結果綜合出強分類器的效果來。

首先我們會給訓練樣本賦予一個權重,一開始的時候,每一條樣本的權重均相等。根據訓練樣本訓練出一個弱分類器並計算這個分類器的錯誤率。然後在同一個數據集上再次訓練弱分類器,在第二次的訓練當中,我們將會調整每個樣本的權重。其中正確的樣本權重會降低,錯誤的樣本權重會升高

同樣每一個分類器也會分配到一個權重值,權重越高說明它的話語權越大。這些是根據模型的錯誤率來計算的。錯誤率定義為:

這裏的D表示數據集表示分類錯誤的集合,它也就等於錯誤分類的樣本數除以總樣本數。

有了錯誤率之後,我們可以根據下面這個公式得到

得到了之後,我們利用它對樣本的權重進行更新,其中分類正確的權重更改為:

分類錯誤的樣本權重更改為:

這樣,我們所有的權重都更新完了,這也就完成了一輪迭代。AdaBoost會反覆進行迭代和調整權重,直到訓練錯誤率為0或者是弱分類器的數量達到閾值。

代碼實現

首先,我們來獲取數據,這裏我們選擇了sklearn數據集中的乳腺癌預測數據。和之前的例子一樣,我們可以直接import進來使用,非常方便:

import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer

breast = load_breast_cancer()
X, y = breast.data, breast.target
# reshape,將一維向量轉成二維
y = y.reshape((-1, 1))

接着,我們將數據拆分成訓練數據和測試數據,這個也是常規做法了,沒有難度:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=23)

在AdaBoost模型當中,我們選擇的弱分類器是決策樹的樹樁。所謂的樹樁就是樹深為1的決策樹。樹深為1顯然不論我們怎麼選擇閾值,都不會得到特別好的結果,但是由於我們依然會選擇閾值和特徵,所以結果也不會太差,至少要比隨機選擇要好。所以這就保證了,我們可以得到一個比隨機選擇效果略好一些的弱分類器,並且它的實現非常簡單。

在我們實現模型之前,我們先來實現幾個輔助函數。

def loss_error(y_pred, y, weight):
    return weight.T.dot((y_pred != y_train))

def stump_classify(X, idx, threshold, comparator):
    if comparator == 'lt':
        return X[:, idx] <= threshold
    else:
        return X[:, idx] > threshold
    
def get_thresholds(X, i):
    min_val, max_val = X[:, i].min(), X[:, i].max()
    return np.linspace(min_val, max_val, 10)

這三個函數應該都不難理解,第一個函數當中我們計算了模型的誤差。由於我們每一個樣本擁有一個自身的權重,所以我們對誤差進行加權求和。第二個函數是樹樁分類器的預測函數,邏輯非常簡單,根據閾值比較大小。這裡有兩種情況,有可能小於閾值的樣本是正例,也有可能大於閾值的樣本是正例,所以我們還需要第三個參數記錄這個信息。第三個函數是生成閾值的函數,由於我們並不需要樹樁的性能特別好,所以我們也沒有必要去遍歷閾值的所有取值,簡單地把特徵的範圍劃分成10段即可。

接下來是單個樹樁的生成函數,它等價於決策樹當中選擇特徵進行數據拆分的函數,邏輯大同小異,只需要稍作修改即可。

def build_stump(X, y, weight):
    m, n = X.shape
    ret_stump, ret_pred = None, []
    best_error = float('inf')

    # 枚舉特徵
    for i in range(n):
        # 枚舉閾值
        for j in get_thresholds(X, i):
            # 枚舉正例兩種情況
            for c in ['lt', 'gt']:
                # 預測並且求誤差
                pred = stump_classify(X, i, j, c).reshape((-1, 1))
                err = loss_error(pred, y, weight)
                # 記錄下最好的樹樁
                if err < best_error:
                    best_error, ret_pred = err, pred.copy()
                    ret_stump = {'idx': i, 'threshold': j, 'comparator': c} 
    return ret_stump, best_error, ret_pred

接下來要做的就是重複生成樹樁的操作,計算,並且更新每一條樣本的權重。整個過程也沒有太多的難點,基本上就是照着實現公式:

def adaboost_train(X, y, num_stump):
    stumps = []
    m = X.shape[0]
    # 樣本權重初始化,一開始全部相等
    weight = np.ones((y_train.shape[0], 1)) / y_train.shape[0]
    # 生成num_stump個樹樁
    for i in range(num_stump):
        best_stump, err, pred = build_stump(X, y, weight)
        # 計算alpha
        alpha = 0.5 * np.log((1.0 - err) / max(err, 1e-10))
        best_stump['alpha'] = alpha
        stumps.append(best_stump)

        # 更新每一條樣本的權重
        for j in range(m):
            weight[j] = weight[j] * (np.exp(-alpha) if pred[j] == y[j] else np.exp(alpha))
        weight = weight / weight.sum()
        # 如果當前的準確率已經非常高,則退出
        if err < 1e-8:
            break
    return stumps

樹樁生成結束之後,最後就是預測的部分了。整個預測過程依然非常簡單,就是一個加權求和的過程。這裏要注意一下,我們在訓練的時候為了突出錯誤預測的樣本,讓模型擁有更好的能力,維護了樣本的權重。然而在預測的時候,我們是不知道預測樣本的權重的,所以我們只需要對模型的結果進行加權即可。

def adaboost_classify(X, stumps):
    m = X.shape[0]
    pred = np.ones((m, 1))
    alphs = 0.0
    for i, stump in enumerate(stumps):
        y_pred = stump_classify(X, stump['idx'], stump['threshold'], stump['comparator'])
        # 根據alpha加權求和
        pred = y_pred * stump['alpha']
        alphs += stump['alpha']
    pred /= alphs
    # 根據0.5劃分0和1類別
    return np.sign(pred).reshape((-1, 1))

到這裏,我們整個模型就實現完了,我們先來看下單個樹樁在訓練集上的表現:

可以看到準確率只有0.54,只是比隨機預測略好一點點而已。

然而當我們綜合了20個樹樁的結果之後,在訓練集上我們可以得到0.9的準確率。在預測集上,它的表現更好,準確率有接近0.95!

這是因為AdaBoost當中,每一個分類器都是弱分類器,它根本沒有過擬合的能力,畢竟在訓練集的表現都很差,這就保證了分類器學到的都是實在的泛化能力,在訓練集上適用,在測試集上很大概率也適用。這也是集成方法最大的優點之一。

總結

集成方法可以說是機器學習領域一個非常重要的飛躍,集成方法的出現,讓設計出一個強分類器這件事的難度大大降低,並且還保證了模型的效果。

因為在一些領域當中,設計一個強分類器可能非常困難,然而設計一個弱一些的分類器則簡單得多,再加上模型本身性能很好,不容易陷入過擬合。使得在深度學習模型流行之前,集成方法廣泛使用,幾乎所有機器學習領域的比賽的冠軍,都使用了集成學習。

集成學習當中具體的思想或許各有不同,但是核心的思路是一致的。我們理解了AdaBoost之後,再去學習其他的集成模型就要容易多了。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

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

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

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

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

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

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

※超省錢租車方案

聚甘新

Java 從入門到進階之路(二十四)

在之前的文章我們介紹了一下 Java 中的  集合框架中的Collection 的泛型,本章我們來看一下 Java 集合框架中的Collection 的子接口 List。

Collection 接口有 3 種子類型,List、Set 和 Queue,其中 List 和 Set 的區別是 Set 中不能存放相同的元素,而 List 中可以,本章我們就來介紹一下 List。

 

 

 從上圖我們可以知道 List 有兩個實例類,ArrayList 和 LinkedList,

ArrayList 是數組實現,查找快,增上慢,由於是數組實現,在增和刪的時候牽扯到數組的增容,以及靠背元素,所以慢,數組是可以直接按索引查找,所以查找時較快。

LinkedList 是鏈表實現,增刪快,查找慢,由於鏈表實現,增加時只要讓前一個元素記住自己就可以了,刪除時讓前一個元素記住后一個元素,后一個元素記住前一個元素,這樣的增刪效率高但查詢時需要一個一個遍歷,所以效率低。

LinkedList 我們可以形象的比作老式手錶的鏈子,一節扣一節,增刪時只需要打開兩個之間的節扣即可,不需要牽扯到其他節扣。

ArrayList 和 LinkedList 都有各自的優缺點,在用的時候可以根據需求自行選擇,避免性能消耗。在現在計算機計算能力越來越強,做的也不是大型項目的時候,這兩個之間的性能差異我們其實是可以忽略的。

接下來我們就來看一下 List 接口的一些基礎用法,如下:

 1 import java.util.ArrayList;
 2 import java.util.List;
 3 
 4 /**
 5  * java.util.List
 6  * 可重複集合,並且有序
 7  * 特點是可以根據下錶操作元素
 8  * ArrayList:使用數組實現,查詢更快
 9  * LinkedList:使用鏈表實現,增刪更快(收尾增刪效果更明顯)
10  */
11 
12 public class Main {
13     public static void main(String[] args) {
14         List<String> list = new ArrayList<String>();
15         list.add("one");
16         list.add("two");
17         list.add("three");
18         list.add("four");
19         /**
20          * E set(int index, E e)
21          * 將給定元素設置到制定位置上,返回原位置的元素
22          * 所以是替換元素操作
23          * 如果超出了元素的長度,則使用 add 添加,否則編譯錯誤
24          * */
25         String old = list.set(1, "2"); // 將下標為 1 的元素改為 2,返回值則是被替換的元素
26         System.out.println(old); // two
27         System.out.println(list); // [one, 2, three, four]
28 
29         /**
30          * E get(int index)
31          * 獲取給定下標對應的元素
32          * */
33         String two = list.get(1); // 獲取第二個元素
34         System.out.println(two); // 2
35 
36         /**
37          * 可以通過傳統的循環遍歷 List 集合
38          * */
39         for (int i = 0; i < list.size(); i++) {
40             System.out.println(list.get(i)); // one 2 three four
41         }
42     }
43 }

在上面的代碼中,我們通過 set 和 get 方法來設置和獲取我們想要的下標的元素,當然還有其他方法,如下:

 1 /**
 2  * List 集合提供了一對重載的 add,remove 方法
 3  * void add(int index, E e)
 4  * 將給定元素插入到指定位置,
 5  * 如果不指定下標,則插入到末尾
 6  * <p>
 7  * E remove(int index)
 8  * 從集合中刪除指定位置的元素,並將其返回
 9  */
10 
11 public class Main {
12     public static void main(String[] args) {
13         List<String> list = new ArrayList<String>();
14         list.add("one");
15         list.add("two");
16         list.add("three");
17         list.add("four");
18 
19         list.add(1, "2"); // 將下標為 1 的元素插入 2
20         System.out.println(list); // [one, 2, two, three, four]
21 
22         String string = list.remove(1); // 將下標為 1 的元素刪除,返回值為該元素
23         System.out.println(string); // 2
24         System.out.println(list); // [one, two, three, four]
25     }
26 }

我們在將 Collection 的時候講過 add 和 remove 方法,在 List 中這兩個方法被重載了,可以根據需求插入和刪除想要刪除的下標的元素,那如果我們想要獲取兩個下標之間的元素和刪除兩個下標之間的元素該怎麼辦呢,如下:

 1 import java.util.ArrayList;
 2 import java.util.List;
 3 
 4 public class Main {
 5     public static void main(String[] args) {
 6         List<Integer> list = new ArrayList<Integer>();
 7         for (int i = 0; i < 10; i++) {
 8             list.add(i);
 9         }
10         System.out.println(list); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
11         List<Integer> subList = list.subList(2, 5); // 獲取下標從 2 到 5 的元素,含 2 不含 5
12         System.out.println(subList); // [2, 3, 4]
13         // 將 subList 中每個元素擴大 10 倍
14         for (int i = 0; i < subList.size(); i++) {
15             subList.set(i, subList.get(i) * 10);
16         }
17         System.out.println(subList); // [20, 30, 40]
18         /**
19          * 對子集的修改,就是修改原集合相應內容
20          * */
21         System.out.println(list); // [0, 1, 20, 30, 40, 5, 6, 7, 8, 9]
22         /**
23          * 刪除集合中 2-5 的元素
24          * */
25         list.subList(2, 5).clear();
26         System.out.println(list); // [0, 1, 5, 6, 7, 8, 9]
27     }
28 }

我們說集合和數組有很多相似的地方,那課可以進行相互轉換呢,當然是可以的,如下:

 1 import java.util.ArrayList;
 2 import java.util.Collection;
 3 
 4 public class Main {
 5     public static void main(String[] args) {
 6         Collection<String> collection = new ArrayList<String>();
 7         collection.add("one");
 8         collection.add("two");
 9         collection.add("three");
10         collection.add("four");
11         System.out.println(collection); // [one, two, three, four]
12         /**
13          * 集合提供了一個 toArray,可以將當前集合轉換為數組
14          * */
15         // Object[] array = collection.toArray(); // 不常用
16         /**
17          * collection.size() 表示要轉換的數組的 length
18          * 如果大於給定的 collection 的 size,則自動填充完整 array
19          * 如果小於給定的 collection 的 size,則自動創建給你一樣長度的 size
20          * */
21         String[] array = collection.toArray(new String[collection.size()]);
22         System.out.println(array.length); // 4
23         for (String string : array) {
24             System.out.println(string); // one two three four
25         }
26 
27         String[] array1 = collection.toArray(new String[6]);
28         System.out.println(array.length); // 4
29         for (String string : array1) {
30             System.out.println(string); // one two three four null null
31         }
32 
33         String[] array2 = collection.toArray(new String[1]);
34         System.out.println(array.length); // 4
35         for (String string : array2) {
36             System.out.println(string); // one two three four
37         }
38     }
39 }

在上面的代碼中我們實現了集合轉換為數組的方法,接下來我們再看一下數組轉換為集合的方法:

 1 import java.util.ArrayList;
 2 import java.util.Arrays;
 3 import java.util.List;
 4 
 5 /**
 6  * 數組轉換為集合
 7  * 需要注意,轉換隻能轉換為 List 集合
 8  * 使用的是數組的工具類 Arrays 的靜態方法 asList
 9  * 只能轉換為 List 集合的主要原因是:Set 不能存放重複元素
10  * 所以若轉換為 Set 集合可能會出現丟失元素的情況
11  */
12 public class Main {
13     public static void main(String[] args) {
14         String[] array = {"one", "two", "three", "four"};
15         List<String> list = Arrays.asList(array);
16         System.out.println(list); // [one, two, three, four]
17 
18         /**
19          * 向集合中添加元素,會出現編譯錯誤
20          * 相當於在原數組添加元素
21          * 該集合時由數組轉換過來的,那麼該集合就表示原來的數組
22          * 所以對集合的操作就是對數組的操作
23          * 那麼添加元素會導致原數組擴容
24          * 那麼久不能表示原來的數組了
25          * 所以不允許向該集合添加元素
26          */
27         // list.add("five"); // 編譯錯誤 Exception in thread "main" java.lang.UnsupportedOperationException
28 
29         /**
30          * 若希望增刪元素,需要另外創建一個集合
31          * */
32         /**
33          * 所有的集合都提供了一個帶有 Collection 類型參數的構造方法
34          * 該構造方法稱為:複製構造器
35          * 作用是在創建當前集合的同時,
36          * 集合中包含給定集合中的所有元素
37          * */
38         // List<String> list1 = new ArrayList<String>(list); // 複製構造器,一步到位
39         List<String> list1 = new ArrayList<String>();
40         list1.addAll(list);
41         list1.add("five");
42         System.out.println(list1); // [one, 2, three, four, five]
43 
44         /**
45          * 修改集合元素,數組元素也會改變
46          * */
47         list.set(1, "2");
48         System.out.println(list); // [one, 2, three, four]
49         for (String string : array) {
50             System.out.println(string); // one 2 three four
51         }
52     }
53 }

    

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

聚甘新

Spring Boot 2 實戰:利用Redis的Geo功能實現查找附近的位置

1. 前言

老闆突然要上線一個需求,獲取當前位置方圓一公里的業務代理點。明天上線!當接到這個需求的時候我差點吐血,這時間也太緊張了。趕緊去查相關的技術選型。經過一番折騰,終於在晚上十點完成了這個需求。現在把大致實現的思路總結一下。

2. MySQL 不合適

遇到需求,首先要想到現有的東西能不能滿足,成本如何。

MySQL是我首先能夠想到的,畢竟大部分數據要持久化到MySQL。但是使用MySQL需要自行計算Geohash。需要使用大量數學幾何計算,並且需要學習地理相關知識,門檻較高,短時間內不可能完成需求,而且長期來看這也不是MySQL擅長的領域,所以沒有考慮它。

Geohash 參考 https://www.cnblogs.com/LBSer/p/3310455.html

2. Redis 中的GEO

Redis是我們最為熟悉的K-V數據庫,它常被拿來作為高性能的緩存數據庫來使用,大部分項目都會用到它。從3.2版本開始它開始提供了GEO能力,用來實現諸如附近位置、計算距離等這類依賴於地理位置信息的功能。GEO相關的命令如下:

Redis命令 描述
GEOHASH 返回一個或多個位置元素的 Geohash 表示
GEOPOS 從key里返回所有給定位置元素的位置(經度和緯度)
GEODIST 返回兩個給定位置之間的距離
GEORADIUS 以給定的經緯度為中心, 找出某一半徑內的元素
GEOADD 將指定的地理空間位置(緯度、經度、名稱)添加到指定的key中
GEORADIUSBYMEMBER 找出位於指定範圍內的元素,中心點是由給定的位置元素決定

Redis會假設地球為完美的球形, 所以可能有一些位置計算偏差,據說<=0.5%,對於有嚴格地理位置要求的需求來說要經過一些場景測試來檢驗是否能夠滿足需求。

2.1 寫入地理信息

那麼如何實現目標單位半徑內的所有元素呢?我們可以將所有的位置的經緯度通過上表中的GEOADD將這些地理信息轉換為52位的Geohash寫入Redis

該命令格式:

geoadd key longitude latitude member [longitude latitude member ...]

對應例子:

redis> geoadd cities:locs 117.12 39.08 tianjin 114.29 38.02  shijiazhuang 
(integer) 2

意思是將經度為117.12緯度為39.08的地點tianjin和經度為114.29緯度為38.02的地點shijiazhuang加入keycities:locssorted set集合中。可以添加一到多個位置。然後我們就可以藉助於其他命令來進行地理位置的計算了。

有效的經度從-180度到180度。有效的緯度從-85.05112878度到85.05112878度。當坐標位置超出上述指定範圍時,該命令將會返回一個錯誤。

2.2 統計單位半徑內的地區

我們可以藉助於GEORADIUS來找出以給定經緯度,某一半徑內的所有元素。

該命令格式:

georadius key longtitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] 

這個命令比GEOADD要複雜一些:

  • radius 半徑長度,必選項。後面的mkmftmi、是長度單位選項,四選一。
  • WITHCOORD 將位置元素的經度和維度也一併返回,非必選。
  • WITHDIST 在返回位置元素的同時, 將位置元素與中心點的距離也一併返回。 距離的單位和查詢單位一致,非必選。
  • WITHHASH 返回位置的52位精度的Geohash值,非必選。這個我反正很少用,可能其它一些偏向底層的LBS應用服務需要這個。
  • COUNT 返回符合條件的位置元素的數量,非必選。比如返回前10個,以避免出現符合的結果太多而出現性能問題。
  • ASC|DESC 排序方式,非必選。默認情況下返回未排序,但是大多數我們需要進行排序。參照中心位置,從近到遠使用ASC ,從遠到近使用DESC

例如,我們在 cities:locs 中查找以(115.03,38.44)為中心,方圓200km的城市,結果包含城市名稱、對應的坐標和距離中心點的距離(km),並按照從近到遠排列。命令如下:

redis> georadius cities:locs 115.03 38.44 200 km WITHCOORD WITHDIST ASC
1) 1) "shijiazhuang"
   2) "79.7653"
   3) 1) "114.29000169038772583"
      2) "38.01999994251037407"
2) 1) "tianjin"
   2) "186.6937"
   3) 1) "117.02000230550765991"
      2) "39.0800000535766543"

你可以加上 COUNT 1來查找最近的一個位置。

3. 基於Redis GEO實戰

大致的原理思路說完了,接下來就是實操了。結合Spring Boot應用我們應該如何做?

3.1 開發環境

需要具有GEO特性的Redis版本,這裏我使用的是Redis 4 。另外我們客戶端使用 spring-boot-starter-data-redis 。這裏我們會使用到 RedisTemplate對象。

3.2 批量添加位置信息

第一步,我們需要將位置數據初始化到Redis中。在Spring Data Redis中一個位置坐標(lng,lat) 可以封裝到org.springframework.data.geo.Point對象中。然後指定一個名稱,就組成了一個位置Geo信息。RedisTemplate提供了批量添加位置信息的方法。我們可以將章節2.1中的添加命令轉換為下面的代碼:

   Map<String, Point> points = new HashMap<>();
   points.put("tianjin", new Point(117.12, 39.08));
   points.put("shijiazhuang", new Point(114.29, 38.02));
   // RedisTemplate 批量添加 Geo
   redisTemplate.boundGeoOps("cities:locs").add(points);

可以結合Spring Boot 提供的ApplicationRunner接口來實現初始化。

@Bean
public ApplicationRunner cacheActiveAppRunner(RedisTemplate<String, String> redisTemplate) {

    return args -> {
        final String GEO_KEY = "cities:locs";

        // 清理緩存
        redisTemplate.delete(GEO_KEY);
        
        Map<String, Point> points = new HashMap<>();
        points.put("tianjin", new Point(117.12, 39.08));
        points.put("shijiazhuang", new Point(114.29, 38.02));
        // RedisTemplate 批量添加 GeoLocation
        BoundGeoOperations<String, String> geoOps = redisTemplate.boundGeoOps(GEO_KEY);
        geoOps.add(points);
    };
}

地理數據持久化到MySQL,然後同步到Redis中。

3.3 查詢附近的特定位置

RedisTemplate 針對GEORADIUS命令也有封裝:

GeoResults<GeoLocation<M>> radius(K key, Circle within, GeoRadiusCommandArgs args)

Circle對象是封裝覆蓋的面積(圖1),需要的要素為中心點坐標Point對象、半徑(radius)、計量單位(metric), 例如:

Point point = new Point(115.03, 38.44);

Metric metric = RedisGeoCommands.DistanceUnit.KILOMETERS;
Distance distance = new Distance(200, metric);

Circle circle = new Circle(point, distance);

GeoRadiusCommandArgs用來封裝GEORADIUS的一些可選命令參數,參見章節2.2中的WITHCOORDCOUNTASC等,例如我們需要在返回結果中包含坐標、中心距離、由近到遠排序的前5條數據:

RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands
        .GeoRadiusCommandArgs
        .newGeoRadiusArgs()
        .includeDistance()
        .includeCoordinates()
        .sortAscending()
        .limit(limit);

然後執行 radius方法就會拿到GeoResults<RedisGeoCommands.GeoLocation<String>>封裝的結果,我們對這個可迭代對象進行解析就可以拿到我們想要的數據:

GeoResults<RedisGeoCommands.GeoLocation<String>> radius = redisTemplate.opsForGeo()
        .radius(GEO_STAGE, circle, args);

if (radius != null) {
    List<StageDTO> stageDTOS = new ArrayList<>();
    radius.forEach(geoLocationGeoResult -> {
        RedisGeoCommands.GeoLocation<String> content = geoLocationGeoResult.getContent();
        //member 名稱  如  tianjin 
        String name = content.getName();
        // 對應的經緯度坐標
        Point pos = content.getPoint();
        // 距離中心點的距離
        Distance dis = geoLocationGeoResult.getDistance();
    });
}

3.4 刪除元素

有時候我們可能需要刪除某個位置元素,但是RedisGeo並沒有刪除成員的命令。不過由於它的底層是zset,我們可以藉助zrem命令進行刪除,對應的Java代碼為:

redisTemplate.boundZSetOps(GEO_STAGE).remove("tianjin");

4. 總結

今天我們使用RedisGeo特性實現了常見的附近的地理信息查詢需求,簡單易上手。其實使用另一個Nosql數據庫MongoDB也可以實現。在數據量比較小的情況下Redis已經能很好的滿足需要。如果數據量大可使用MongoDB來實現。 文中涉及的DEMO可通過我個人博客獲取。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

聚甘新