想要成家的90后,12萬元究竟能買到什麼好車?

以上車型都是適合年輕人的,即使是使用貸款的方式購買也不會造成太大的壓力,關鍵的一點是這些車型基本可以陪伴你度過初初成家、孩子長大的過程。

以上車型都是適合年輕人的,即使是使用貸款的方式購買也不會造成太大的壓力,關鍵的一點是這些車型基本可以陪伴你度過初初成家、孩子長大的過程。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

冬至南吃湯圓北吃餃,原來車也分南派北派,你是哪派?

最典型如座椅加熱/方向盤加熱和座椅通風。座椅加熱對於冬至還在穿短袖的廣州人民是一個雞肋的配置,通風功能則要實用得多。相反加熱功能對北方的小夥伴來說簡直是一個“雪中送炭”的配置。大家都知道如今霧霾已經成為威脅人類健康的頭等“殺手”,因此不少主機廠都會在優化車內空氣質量上花心思,比如裝配pM2。

要不是主編大人提醒,叫獸我都忘了昨天居然是冬至。冬至,意味着一年中最寒冷的節氣來臨。此時此刻,你在北方的霧霾里醉生夢死,我在南方的艷陽里穿着短袖···

每逢佳節倍思親,作為一隻“南漂單身狗”,每每到了過節的時候就會感到格外凄涼。而辦公室的另一頭,同事們還在為冬至吃湯圓還是水餃這個問題爭論的時候,叫獸我已經默默提筆,於是有了今天這篇文章。

到底湯圓還是水餃,說白了就是南北的習俗差異。只能說咱們祖國地大物博,不光是過節的習俗有很大不同,就連買車和用車都有很大差異。今天我就化思緒為動力,來和大家探討這個問題。

一.購車習慣差異

都說北方人粗獷大氣,不拘小節,選車習慣也非常符合他們的脾性。相比而言,他們往往對車內的做工和配置等方面不會過於在意,但發動機動力性能要強,車輛高速行駛時的穩定性要好。這裏以德系和美系車最具代表性。

南方人的消費觀念則比較精打細算,平日生活里追求“小而精”。如廣州茶餐廳里精緻的點心一樣,嘗過一回廣式早茶便能體會到其中的精髓。因此他們在選車時會對車輛的做工和油耗等方面比較在意;除此外,性價比也是他們比較看重的部分。這裏則以日系車為代表。

其實從國內幾大主機廠的分佈情況也能看出一二,北方多德系(一汽大眾、奧迪),南方則幾乎成了日系的“大本營”(廣汽旗下的本田、豐田、謳歌)。

二.配置需求不盡相同

這一點很好理解,由於地理環境的影響,南北方消費者對車輛配置方面的要求也有挺大差別。最典型如座椅加熱/方向盤加熱和座椅通風。座椅加熱對於冬至還在穿短袖的廣州人民是一個雞肋的配置,通風功能則要實用得多;相反加熱功能對北方的小夥伴來說簡直是一個“雪中送炭”的配置。

大家都知道如今霧霾已經成為威脅人類健康的頭等“殺手”,因此不少主機廠都會在優化車內空氣質量上花心思,比如裝配pM2.5凈化系統等配置。在這樣的氣候環境下,以往這個看起來毫不起眼的配置變得令不少消費者都重視了起來,北方朋友們則更情有獨鍾。

三.消費觀念不同

*提醒一點,這部分是叫獸以個人經驗得出看法,沒有實際數據作支撐,僅供參考。

同為國內相對富裕地區,廣東與江浙土豪們在汽車消費觀方面也有明顯差異。去過杭州、寧波等地朋友們一定會發現,那裡的超豪車尤其多,繁華地區幾乎隨處可見法拉利、蘭博基尼;相比下廣深兩地的超豪華車的數量則要“理性”得多。

叫獸斗膽以為,廣東人是先富起來的那批人,這裏擁有大量的實業經濟(東莞曾被成為世界工廠),除了政策方面的激勵,他們更多的是靠勤勞和汗水打出來的天下,財富是一點一滴積累起來的;而江浙滬地帶則多金融行業,財富來得有些“大起大落”,因此土豪們花起錢來也要大手大腳一些,畢竟坐一台勞斯萊斯、賓利去談生意可比坐奔馳要容易得多。

四.用車養車的區別

冬季北方氣溫低、雪量大,因此不少注重車輛保養和行車安全的車主都會在冬季來臨前給愛車做一次“大保健”,換粘度更低的機油和凝結點更低的純防凍液;還有最重要的一點,一套雪地胎是必須的。關乎安全,一點也不能放鬆。

夏季南方則是暴雨和暴晒交替進行,所以南方車主對車輛的漆面保養尤為注意,漆面鍍膜則是個不錯的辦法;另外要注意的是,雨季時務必將汽車停在安全地帶防止泡水。據保險公司的朋友介紹,由於天氣的多變性,已經有越來越多的車主會主動購買車輛自燃險和涉水險。

以上是叫獸總結出幾點南方和北方朋友們在選車和用車時的不同之處,如果大家還有更多的看法,歡迎大家留言與叫獸互動。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

微軟、NIKE等9大企業聯盟 盼2050前實現「零碳排放」

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

微軟、Nike、星巴克和其他六家來自各國其他領域的行業巨擘聯手,成立「Transform to Net Zero」,旨在幫助全球企業實現「零碳排放」;該組織目前正在招募更多成員,希望能在2050年以前,達成2019年聯合國氣候行動峰會(UN Climate Action Summit)時,全球66國允諾的二氧化碳淨零排放。

《路透》報導,「Transform to Net Zero」致力於減少全球碳排放,目標包含降低整個供應鏈從產品到服務所產生的碳排放、影響企業對相關投資的實質承諾和意願。

「Transform to Net Zero」在其官網上釋出未來五年將完成的初步研究計畫,以及其他協助各國企業減少碳排放的實質工作規劃。

氣候變遷
國際新聞
碳排放量

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

7.5萬起的大氣SUV車型 車主都說配置高空間大?

而且1。3T發動機配合CVT變速箱,動力也平順。起步的動力不算強,不過挺平順的,像我這些開車不急的人,它的動力夠用了。我的遠景SUV行駛了有3200公里,平均油耗只有8。3L,這個表現讓人滿意。車主:彩色購買車型:1。3T CVT豪華型裸車價格:9。

吉利汽車-遠景SUV

指導價:7.49-10.19萬

車主:南通達人

購買車型:1.8L 手動尊貴型

裸車價格:8.49萬

最滿意的是它的做工質感很不錯,落鎖就自動關閉天窗這個功能很實用!空間夠用,座椅也舒適。動力方面,一擋、二擋起步很有推背感,手動變速箱的手感也不差。

就是發動機怠速時聲音有點大,還有音響的音質一般般,後排座椅不能完全放平,不過空間真的夠大。

目前我的車行駛了2410公里,平均的百公里油耗是8.17L,因為是SUV,所以不算高。

車主:北方的小強

購買車型:1.3T CVT旗艦型

裸車價格:10.19萬

遠景SUV空間大、底盤高。日常溫柔駕駛很舒適!內飾用了很多軟質材料,摸着舒服。而且1.3T發動機配合CVT變速箱,動力也平順。

起步的動力不算強,不過挺平順的,像我這些開車不急的人,它的動力夠用了!

我的遠景SUV行駛了有3200公里,平均油耗只有8.3L,這個表現讓人滿意。

車主:彩色

購買車型:1.3T CVT豪華型

裸車價格:9.49萬

空間大、價格不高、油耗不高!是我選擇它的原因。車子在150km/h內都比較穩,不發飄。轉向也比較精準,回饋力度也合適。

尾部設計有些不夠大氣,不過尾燈卻挺好看的。高速過彎的時候側傾會有一點明顯,不過這麼高的一輛SUV,當然以舒適性位為主。

現在我的車開了2900公里了,平時愛激烈駕駛,所以油耗一般要9.6L,比其他車主都要高!

編者點評:

遠景SUV的空間、用料、配置都表現不錯,加上一個不高的售價讓它性價比表現出色!而1.3T發動機在平順駕駛的情況下還是比較省油的。喜歡這款車型的話,快去4S店試駕一番吧。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

數據源管理 | Kafka集群環境搭建,消息存儲機制詳解

本文源碼:GitHub·點這裏 || GitEE·點這裏

一、Kafka集群環境

1、環境版本

版本:kafka2.11,zookeeper3.4

注意:這裏zookeeper3.4也是基於集群模式部署。

2、解壓重命名

tar -zxvf kafka_2.11-0.11.0.0.tgz
mv kafka_2.11-0.11.0.0 kafka2.11

創建日誌目錄

[root@en-master kafka2.11]# mkdir logs

注意:以上操作需要同步到集群下其他服務上。

3、添加環境變量

vim /etc/profile
export KAFKA_HOME=/opt/kafka2.11
export PATH=$PATH:$KAFKA_HOME/bin
source /etc/profile

4、修改核心配置

[root@en-master /opt/kafka2.11/config]# vim server.properties
-- 核心修改如下
# 唯一編號
broker.id=0
# 開啟topic刪除
delete.topic.enable=true
# 日誌地址
log.dirs=/opt/kafka2.11/logs
# zk集群
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181

注意:broker.id安裝集群服務個數編排即可,集群下不能重複。

5、啟動kafka集群

# 啟動命令
[root@node02 kafka2.11]# bin/kafka-server-start.sh -daemon config/server.properties
# 停止命令
[root@node02 kafka2.11]# bin/kafka-server-stop.sh
# 進程查看
[root@node02 kafka2.11]# jps

注意:這裏默認啟動了zookeeper集群服務,並且集群下的kafka分別啟動。

6、基礎管理命令

創建topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--create --replication-factor 3 --partitions 1 --topic one-topic

參數說明:

  • replication-factor 定義副本個數
  • partitions 定義分區個數
  • topic:定義topic名稱

查看topic列表

bin/kafka-topics.sh --zookeeper zk01:2181 --list

修改topic分區

bin/kafka-topics.sh --zookeeper zk01:2181 --alter --topic one-topic --partitions 5

查看topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--describe --topic one-topic

發送消息

bin/kafka-console-producer.sh \
--broker-list 192.168.72.133:9092 --topic one-topic

消費消息

bin/kafka-console-consumer.sh \
--bootstrap-server 192.168.72.133:9092 --from-beginning --topic one-topic

刪除topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--delete --topic first

7、Zk集群用處

Kafka集群中有一個broker會被選舉為Controller,Controller依賴Zookeeper環境,管理集群broker的上下線,所有topic的分區副本分配和leader選舉等工作。

二、消息攔截案例

1、攔截器簡介

Kafka中間件的Producer攔截器主要用於實現消息發送的自定義控制邏輯。用戶可以在消息發送前以及回調邏輯執行前有機會對消息做一些自定義,比如消息修改等,發送狀態監控等,用戶可以指定多個攔截器按順序執行攔截。

核心方法

  • configure:獲取配置信息和初始化數據時調用;
  • onSend:消息被序列化以及和計算分區前調用該方法,可以對消息做操作;
  • onAcknowledgement:消息發送到Broker之後,或發送過程失敗時調用;
  • close:關閉攔截器調用,執行一些資源清理工作;

注意:這裏說的攔截器是針對消息發送流程。

2、自定義攔截

定義方式:實現ProducerInterceptor接口即可。

攔截器一:在onSend方法中,對攔截的消息進行修改。

@Component
public class SendStartInterceptor implements ProducerInterceptor<String, String> {

    private final Logger LOGGER = LoggerFactory.getLogger("SendStartInterceptor");
    @Override
    public void configure(Map<String, ?> configs) {
        LOGGER.info("configs...");
    }
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        // 修改消息內容
        return new ProducerRecord<>(record.topic(), record.partition(),
                                    record.timestamp(), record.key(),
                              "onSend:{" + record.value()+"}");
    }
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        LOGGER.info("onAcknowledgement...");
    }
    @Override
    public void close() {
        LOGGER.info("SendStart close...");
    }
}

攔截器二:在onAcknowledgement方法中,判斷消息是否發送成功。

@Component
public class SendOverInterceptor implements ProducerInterceptor<String, String> {

    private final Logger LOGGER = LoggerFactory.getLogger("SendOverInterceptor");
    @Override
    public void configure(Map<String, ?> configs) {
        LOGGER.info("configs...");
    }

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        LOGGER.info("record...{}", record.value());
        return record ;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception != null){
            LOGGER.info("Send Fail...exe-msg",exception.getMessage());
        }
        LOGGER.info("Send success...");
    }

    @Override
    public void close() {
        LOGGER.info("SendOver close...");
    }
}

加載攔截器:基於一個KafkaProducer配置Bean,加入攔截器。

@Configuration
public class KafkaConfig {

    @Bean
    public Producer producer (){
        Properties props = new Properties();
        // 省略其他配置...
        // 添加攔截器
        List<String> interceptors = new ArrayList<>();
        interceptors.add("com.kafka.cluster.interceptor.SendStartInterceptor");
        interceptors.add("com.kafka.cluster.interceptor.SendOverInterceptor");
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
        return new KafkaProducer<>(props) ;
    }
}

3、代碼案例

@RestController
public class SendMsgWeb {
    @Resource
    private KafkaProducer<String,String> producer ;
    @GetMapping("/sendMsg")
    public String sendMsg (){
        producer.send(new ProducerRecord<>("one-topic", "msgKey", "msgValue"));
        return "success" ;
    }
}

基於上述自定義Bean類型,進行消息發送,關注攔截器中打印日誌信息。

三、Kafka存儲分析

說明:該過程基於上述案例producer.send方法追蹤的源碼執行流程,源碼中的過程相對清楚,涉及的核心流程如下。

1、消息生成過程

Producer發送消息採用的是異步發送的方式,消息發送過程如下:

  • Producer發送消息之後,經過攔截器,序列化,事務判斷;
  • 流程執行后,消息內容放入容器中;
  • 容器在指定時間內如果裝滿(size),會喚醒Sender線程;
  • 容器如果在指定時間內沒有裝滿,也會執行一次Sender線程喚醒;
  • 喚醒Sender線程之後,把容器數據拉取到topic中;

絮叨一句:讀這些中間件的源碼,不僅能開闊思維,也會讓自己意識到平時寫的代碼可能真的叫搬磚。

2、存儲機制

Kafka中消息是以topic進行標識分類,生產者面向topic生產消息,topic分區(partition)是物理上的存儲,基於消息日誌文件的方式。

  • 每個partition對應於一個log文件,發送的消息不斷追加到該log文件末端;
  • log文件中存儲的就是producer生產的消息數據,採用分片和索引機制;
  • partition分為多個segment。每個segment對應兩個(.index)和(.log)文件;
  • index文件類型存儲的索引信息;
  • log文件存儲消息的數據;
  • 索引文件中的元數據指向對應數據文件中message的物理偏移地址;
  • 消費者組中的每個消費者,都會實時記錄消費的消息offset位置;
  • 當然消息消費出錯時,恢復是從上次的記錄位置繼續消費;

3、事務控制機制

Kafka支持消息的事務控制

Producer事務

跨分區跨會話的事務原理,引入全局唯一的TransactionID,並將Producer獲得的PID和TransactionID綁定。Producer重啟后可以通過正在進行的TransactionID獲得原來的PID。
Kafka基於TransactionCoordinator組件管理Transaction,Producer通過和TransactionCoordinator交互獲得TransactionID對應的任務狀態。TransactionCoordinator將事務狀態寫入Kafka的內部Topic,即使整個服務重啟,進行中的事務狀態可以得到恢復。

Consumer事務

Consumer消息消費,事務的保證強度很低,無法保證消息被精確消費,因為同一事務的消息可能會出現重啟后已經被刪除的情況。

四、源代碼地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent

推薦關聯閱讀:數據源管理系列

序號 標題
01 數據源管理:主從庫動態路由,AOP模式讀寫分離
02 數據源管理:基於JDBC模式,適配和管理動態數據源
03 數據源管理:動態權限校驗,表結構和數據遷移流程
04 數據源管理:關係型分庫分表,列式庫分佈式計算
05 數據源管理:PostGreSQL環境整合,JSON類型應用
06 數據源管理:基於DataX組件,同步數據和源碼分析
07 數據源管理:OLAP查詢引擎,ClickHouse集群化管理

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

挹注氣候30% 歐盟25兆振興方案到底有多綠?

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

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

曹工說JDK源碼(4)–抄了一小段ConcurrentHashMap的代碼,我解決了部分場景下的Redis緩存雪崩問題

曹工說JDK源碼(1)–ConcurrentHashMap,擴容前大家同在一個哈希桶,為啥擴容后,你去新數組的高位,我只能去低位?

曹工說JDK源碼(2)–ConcurrentHashMap的多線程擴容,說白了,就是分段取任務

曹工說JDK源碼(3)–ConcurrentHashMap,Hash算法優化、位運算揭秘

什麼是緩存雪崩

基本概念梳理

這個基本也是redis 面試的經典題目了,然而,網上不少博客對這個詞的定義都含糊不清,各執一詞。

主要有兩類說法:

  • 大量緩存key,由於設置了相同的過期時間,在某個時刻同時失效,導致此刻的查詢請求,全部湧向db,本來db的tps大概是幾千左右,結果湧入了幾十萬的請求,那db肯定直接就扛不住了

    這種說法下面,解決方案一般是,把過期時間增加一個隨機值,這樣,也就不會大批量的key同時失效了

  • 另外一種說法是,本來redis扛下了大部分的請求,但是,由於緩存所在的機器,發生了宕機。此時,緩存這台機器之間就連不上了,redis服務也掛了,此時,你的服務里,發現redis取不到,然後全都跑去查數據庫,那,就發生和前面一樣的情況了,請求全部湧向db,db無響應。

兩類說法,也不用覺得,這個對,那個不對,不過是一個技術名詞,當初發明這個詞的人,估計也沒想那麼多,結果傳播開來之後,就變成了現在這個樣子。

我們這裏主要採用下面那一種說法,因為下面這種說法,其實是已經包含了上面的情景。但是,下面這種場景,要複雜的多,因為redis此時就是一個完全不可信的東西了,你得想好,怎麼不讓它掛掉,那是不是應該部署sentinel、cluster集群?同時,持久化必須要開啟。

這樣呢,掛掉后,短暫的不可用之後,大概幾十s吧,緩存集群就恢復了,就又可用了。

同時,我們還得考慮,假設,現在redis掛了,我們代碼的降級策略是什麼?

大家發現redis掛了,首先,估計是會拋異常了,連接超時;拋了異常后,要直接拋到前端嗎?作為一個穩健的後端程序,那肯定是不行的,你redis掛了,數據庫又沒掛;好吧,那我們就大家一起去查數據庫。

結果,大量的查詢請求,就烏泱泱地跑去查庫了,然後,db卒。這個肯定不行。

所以,我們必須要控制的一點是,當發現某個key失效了,不是大家都去查庫,而是要進行 併發控制

什麼是併發控制?就是不能全部放過去查庫,只能放少部分,免得把脆弱的db打死。

併發控制,基本就是要爭奪去查庫的權利了,這一步,基本就是一個選舉的過程,可以通過搶鎖的方式,比如Reentrentlock,synchronized,cas也可以。

  1. 搶到鎖的線程,有資格去查庫,其他線程要麼被阻塞,要麼自旋

  2. 搶到鎖的線程,去查庫,查到數據后,將數據存放在某個地方,通知其他線程去取(如果其他線程被阻塞的話);或者,如果其他線程沒被阻塞,比如sleep 50ms,再去指定的地方拿數據那種,這種就不需要通知

    總之,如果其他線程要我們通知,我們就通知;不要我們通知,我們就不通知。

搶到鎖的線程,在構建緩存時,其他線程應該干什麼?

  1. 在while(true)里,sleep 50ms,然後再去取數據

    這種類似於忙等待,但是每次sleep一會,所以還不錯

  2. 將自己阻塞,等待搶到鎖的線程,構建完緩存后,來喚醒

  3. 在while(true)里,一直忙循環,期間一直檢查數據是否已經ok了,這種方案呢,要看裏面:檢查數據的操作,是否耗時;如果只是檢查jvm內存里的數據,那還好;否則的話,假設要去檢查redis的話,這種io比較耗時的操作的話,就不合適了,cpu會一直空轉。

本文採用的方案

主線程構建緩存時,其他線程,在while(true)里,sleep 一定時間,然後再檢查數據是否ready。

說了這麼多,好像和題目里的concurrenthashmap沒啥關係,不,是有關係的,因為,這個思路,其實就是來自於concurrentHashMap。

ConcurrentHashMap中,是怎麼去初始化底層數組的

在我們用無參構造函數,去new一個ConcurrentHashMap時,此時還不會去創建底層數組,這個是一個小優化。什麼時候創建數組呢,是在我們第一次去put的時候。

put的時候,會調用putVal。

其中,putVal代碼如下:

    transient volatile Node<K,V>[] table;

	final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
      	// 1
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
          	// 2
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
  • 1處,把field table,賦值給局部變量tab

  • 2處,如果tab為null,則進行initTable初始化

    這個2處,在多線程put的時候,是可能多個線程同時進來的。有併發問題。

我們接下來,看看initTable是怎麼解決這個問題的,畢竟,我們new數組,只new一次即可,new那麼多次,沒用,對性能有損耗。所以,這裏面肯定會多線程爭奪初始化權利的代碼。

	private transient volatile int sizeCtl;
	transient volatile Node<K,V>[] table;

	/**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab;
      	int sc;
      	
      	// 0
        while ((tab = table) == null || tab.length == 0) {
          	// 1
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
          	// 2
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                  	// 3
                    if ((tab = table) == null || tab.length == 0) {
                      	// 4
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                  	// 5
                    sizeCtl = sc;
                }
                break;
              
            }// end if
          
        }// end while
        return tab;
    }
  • 1處,這裏把sizeCtl,賦值給局部變量sc。這裏的sizeCtl是一個很重要的field,當我們new完之後,默認這個字段,要麼為0,要麼為準備創建的底層數組的長度。

    這裏去判斷是否小於0,那肯定不滿足,小於0,會是什麼意思?當某個線程,搶到了這個initTable中的底層數組的創建權利時,就會把sizeCtl改為 -1。

    所以,這裏的意思是,看看是否已經有其他線程在初始化了,如果已經有了,則直接調用:

    Thread.yield();

    這個方法的意思是,暗示操作系統,自己準備放棄cpu;但操作系統,自有它自己的線程調度規則,所以,這個方法可能沒什麼效果;我們業務代碼,這裏一般可以修改為Thread.sleep。

    這個方法調用完成后,後續也沒有其他代碼,所以會直接跳轉到循環開始處(0處代碼),判斷table是否初始化ok了,如果沒有ok,則會繼續進來。

  • 2處,使用cas,如果此時,sizeCtl的值等於sc的值,就修改sizeCtl為 -1;如果成功,則返回true,進入3處

    否則,會跳轉到0處,繼續循環。

  • 3處,雖然搶到了控制權,但是這裏還是要再判斷一下,不然可能出現重複初始化,即,不加這一行,4處的代碼,會被重複執行

  • 4處開始,這裏去執行真正的初始化邏輯。

    // 
    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
    @SuppressWarnings("unchecked")
    // 1
    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
    // 2
    table = tab = nt;
    sc = n - (n >>> 2);
    

    這裏的1處,new數組;2處,賦值給field:table;此時,因為table 這個field是volatile修飾的,所以其他線程會馬上感知到。0處代碼就不會為true了,就不會繼續循環了。

  • 5處,修改sizeCtl為正數。

這裏說下,為啥要加3處的那個判斷。

現在,假設線程A,在初始化完成后,走到了5處,修改了sizeCtl為正數;而線程B,剛好執行1處代碼:

// 1
if ((sc = sizeCtl) < 0)

那肯定,1處就不滿足了;然後就會進到2處,cas修改成功,進行初始化。沒有3處判斷的話,就會重複初始化。

基於concurrentHashmap,實現我們的緩存雪崩方案

我這裏的方案,還是比較簡單那種,就是,n個線程同時爭奪構建緩存的權利;winner線程,構建緩存后,會把緩存設置到redis;其他線程則是一直在while(true)里sleep一段時間,然後檢查redis里的數據是否不為空。

這個方案中,redis掛了這種情況,是沒在考慮中的,但是一個方案,沒辦法立馬各方面全部到位,後續我再完善一下。

不考慮緩存雪崩的代碼

@Override
public Users getUser(long userId) {
    ValueOperations<String, Users> ops = redisTemplate.opsForValue();
  	// 1
    Users s = ops.get(String.valueOf(userId));
    if (s == null) {
        /**
         * 2 這裏要去查庫獲取值
         */
        Users users = getUsersFromDB(userId);
		// 3
        ops.set(String.valueOf(users.getUserId()),users);

        return users;
    }

    return s;
}

private Users getUsersFromDB(long userId) {
    Users users = new Users();

    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("spent 1s to get user from db");
    users.setUserId(userId);
    users.setUserName("zhangsan");

    return users;
}

直接看上面的1,2,3處。就是檢查、構建緩存,設置到緩存的過程。

考慮緩存雪崩的代碼

	// 1
	private volatile int initControl;

	@Override
    public Users getUser(long userId) {
        ValueOperations<String, Users> ops = redisTemplate.opsForValue();

        Users users;
        while (true) {
          	// 2
            users = ops.get(String.valueOf(userId));
            if (users != null) {
              	// 3 
                break;
            }
			
          	// 4
            int initControlLocal = initControl;
            /**
             * 5 如果已經有線程在進行獲取了,則直接放棄cpu
             */
            if (initControlLocal < 0) {
//                log.info("initControlLocal < 0,just yield and wait");
//                Thread.yield();
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    log.warn("e:{}", e);
                }
                continue;
            }


            /**
             * 6 爭奪控制權
             */
            boolean bGotChanceToInit = U.compareAndSwapInt(this,
                    INIT_CONTROL, initControlLocal, -1);
          	// 7
            if (bGotChanceToInit) {
                try {
                  	// 8
                    users = ops.get(String.valueOf(userId));
                    if (users == null) {
                        log.info("got change to init");
                        /**
                         * 9 這裏要去查庫獲取值
                         */
                        users = getUsersFromDB(userId);
                        ops.set(String.valueOf(users.getUserId()), users);
                        log.info("init over");
                    }
                } finally {
                  	// 10
                    initControl = 0;
                }

                break;
            }// end if (bGotChanceToInit)
        }// end while


        return users;
    }
  • 1處,定義了一個field,initControl;默認為0.線程們會去使用cas,修改為-1,成功的線程,即獲得初始化緩存的權利。

    注意,要定義為volatile,保證線程間的可見性

  • 2處,去redis獲取緩存,如果不為null,直接返回

  • 4處,如果沒取到緩存,則進入此處;此處,將field:initControl賦值給局部變量

  • 5處,判斷局部變量initControlLocal,是否小於0;小於0,說明已經有線程在進行初始化了,直接contine,繼續下一次循環

  • 6處,如果當前還沒有線程在初始化,則開始競爭初始化的權利,誰成功地用cas,修改field:initControl為-1,誰就獲得這個權利

  • 7處,如果當前線程獲得了權利,則進入8處,否則,會繼續下一次循環

  • 8處,再次去redis,獲取緩存,如果不為空,則進入9處

  • 9處,查庫,設置緩存

  • 10處,修改field:initControl為0,表示退出初始化

這裏的代碼,整體和hashmap中的initTable是一模一樣的。

如何測試

上面的方案,怎麼測試沒問題呢?我寫了一段測試代碼。

    ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 100,
            60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new RejectedExecutionHandler() 	{
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            log.info("discard:{}",r);
        }
    });
	
	@RequestMapping("/test.do")
    public void test() {
      	// 0
        iUsersService.deleteUser(111L);

        CyclicBarrier barrier = new CyclicBarrier(100);

        for (int i = 0; i < 100; i++) {

            executor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    long start = System.currentTimeMillis();
                  	// 1
                    Users users = iUsersService.getUser(111L);
                    log.info("result:{},spent {} ms", users, System.currentTimeMillis() - start);
                }
            });
        }

    }

上面模擬100併發下,獲取緩存。

0處,把緩存刪了,模擬緩存失效

1處,調用方法,獲取緩存。

效果如下:

可以看到,只有一個線程拿到了初始化權利。

源碼位置

https://gitee.com/ckl111/all-simple-demo-in-work-1/tree/master/redis-cache-avalanche

總結

jdk的併發包,寫得真是有水平,大家仔細研究的話,必有收穫。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

印度空氣污染嚴重致或致八成蜜蜂死亡 糧食安全將受重大威脅

摘錄自2020年8月11日立場新聞報導

蜜蜂可能比人類更強烈地感受到空氣污染帶來的影響。最新刊於《美國國家科學院期刊》(PNAS) 的報告指,即使是輕微的空氣污染也會殺死 80% 的亞洲大蜜蜂(Apis Dorsata) 。學者表示這是首次提供全面證據,證明昆蟲對空氣中的懸浮粒子特別敏感。

印度多個城市都高居全球空氣污染最嚴重名單,不僅是 13.5 億人的家鄉,亦是亞洲大蜜蜂的棲息地。這種蜜蜂是南亞主要的花粉傳播媒介。芬蘭奧盧大學行為生態學家 Olli Loukola 指出,據估計如沒有昆蟲授粉, 53% 芒果會消失,單計印度的出口就因此損失約 8,600 萬美元。

研究發現,在污染更嚴重地點採集的蜜蜂更有可能出現含砷和鉛等有毒重金屬的顆粒。從污染最嚴重地區捕獲的 80% 蜜蜂在採樣後 1 天內死亡,是校園區蜜蜂死亡量的兩倍。團隊也發現,被有毒灰塵覆蓋的蜜蜂接觸花朵採蜜的次數僅是校園區蜜蜂的一半,因此可能減少有花植物成功授粉的機會。

生物多樣性
污染治理
國際新聞
印度
懸浮粒子
糧食安全
蜜蜂
授粉生物
空氣污染

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

格陵蘭冰融已難挽回 全球海平面恐升6公尺

摘錄自2020年08月16日中央通訊社格陵蘭報導

新研究顯示,格陵蘭(Greenland)冰層可能已經融化到無法挽回的地步,無論全球以多快的速度降低溫室氣體排放,冰層仍可能繼續融化。如果格陵蘭的冰全部融化,全球海平面平均將上升6公尺。

路透社報導,科學家研究橫跨北極234個冰川截至2018年的34年數據發現,年降雪量已經不足以補充冰川在夏季融化而流失的雪和冰。冰川融化已造成全球海平面每年平均上升1公釐。如果格陵蘭的冰全部融化,足以淹沒全球許多沿海城市,不過這個過程將花上數十年。

過去30年來,北極暖化的速度比起全球其他區域至少快兩倍,此現象被稱為「北極放大」效應,而極地海冰在今年7月創下40年來最低點。科學家表示,全球仍能透過降低碳排來減緩氣候變遷,即使格陵蘭難再累積覆蓋其200萬平方公里的冰層,遏制全球氣溫上升也能放慢冰層流失的速度。

氣候變遷
國際新聞
格陵蘭
冰川融化
海平面上升

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」

作者:小傅哥
博客:https://bugstack.cn

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

一、前言

你感受到的容易,一定有人為你承擔不容易

這句話更像是描述生活的,許許多多的磕磕絆絆總有人為你提供躲雨的屋檐和避風的港灣。其實編程開發的團隊中也一樣有人只負責CRUD中的簡單調用,去使用團隊中高級程序員開發出來的核心服務和接口。這樣的編程開發對於初期剛進入程序員行業的小夥伴來說鍛煉鍛煉還是不錯的,但隨着開發的日子越來越久一直做這樣的事情就很難得到成長,也想努力的去做一些更有難度的承擔,以此來增強個人的技術能力。

沒有最好的編程語言,語言只是工具

刀槍棍棒、斧鉞鈎叉、包子油條、盒子麻花,是語言。五郎八卦棍、十二路彈腿、洪家鐵線拳,是設計。記得恭弘=叶 恭弘問里有一句台詞是:金山找:今天我北方拳術,輸給你南方拳術了。恭弘=叶 恭弘問:你錯了,不是南北拳的問題,是你的問題。所以當你編程開發寫的久了,就不會再特別在意用的語言,而是為目標服務,用最好的設計能力也就是編程的智慧做出做最完美的服務。這也就是編程人員的價值所在!

設計與反設計以及過渡設計

設計模式是解決程序中不合理、不易於擴展、不易於維護的問題,也是幹掉大部分ifelse的利器,在我們常用的框架中基本都會用到大量的設計模式來構建組件,這樣也能方便框架的升級和功能的擴展。但!如果不能合理的設計以及亂用設計模式,會導致整個編程變得更加複雜難維護,也就是我們常說的;反設計過渡設計。而這部分設計能力也是從實踐的項目中獲取的經驗,不斷的改造優化摸索出的最合理的方式,應對當前的服務體量。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. SpringBoot 2.1.2.RELEASE
  4. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-10-00 場景模擬工程;模擬一個提供接口服務的SpringBoot工程
itstack-demo-design-10-01 使用一坨代碼實現業務需求
itstack-demo-design-10-02 通過設計模式開發為中間件,包裝通用型核心邏輯

三、外觀模式介紹

外觀模式也叫門面模式,主要解決的是降低調用方的使用接口的複雜邏輯組合。這樣調用方與實際的接口提供方提供方提供了一个中間層,用於包裝邏輯提供API接口。有些時候外觀模式也被用在中間件層,對服務中的通用性複雜邏輯進行中間件層包裝,讓使用方可以只關心業務開發。

那麼這樣的模式在我們的所見產品功能中也經常遇到,就像幾年前我們註冊一個網站時候往往要添加很多信息,包括;姓名、昵稱、手機號、QQ、郵箱、住址、單身等等,但現在註冊成為一個網站的用戶只需要一步即可,無論是手機號還是微信也都提供了這樣的登錄服務。而對於服務端應用開發來說以前是提供了一個整套的接口,現在註冊的時候並沒有這些信息,那麼服務端就需要進行接口包裝,在前端調用註冊的時候服務端獲取相應的用戶信息(從各個渠道),如果獲取不到會讓用戶後續進行補全(營銷補全信息給獎勵),以此來拉動用戶的註冊量和活躍度。

四、案例場景模擬

在本案例中我們模擬一個將所有服務接口添加白名單的場景

在項目不斷壯大發展的路上,每一次發版上線都需要進行測試,而這部分測試驗證一般會進行白名單開量或者切量的方式進行驗證。那麼如果在每一個接口中都添加這樣的邏輯,就會非常麻煩且不易維護。另外這是一類具備通用邏輯的共性需求,非常適合開發成組件,以此來治理服務,讓研發人員更多的關心業務功能開發。

一般情況下對於外觀模式的使用通常是用在複雜或多個接口進行包裝統一對外提供服務上,此種使用方式也相對簡單在我們平常的業務開發中也是最常用的。你可能經常聽到把這兩個接口包裝一下,但在本例子中我們把這種設計思路放到中間件層,讓服務變得可以統一控制。

1. 場景模擬工程

itstack-demo-design-10-00
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── domain
    │   │       │	└── UserInfo.java
    │   │       ├── web	
    │   │       │	└── HelloWorldController.java
    │   │       └── HelloWorldApplication.java
    │   └── resources	
    │       └── application.yml	
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java
  • 這是一個SpringBootHelloWorld工程,在工程中提供了查詢用戶信息的接口HelloWorldController.queryUserInfo,為後續擴展此接口的白名單過濾做準備。

2. 場景簡述

2.1 定義基礎查詢接口

@RestController
public class HelloWorldController {

    @Value("${server.port}")
    private int port;

    /**
     * key:需要從入參取值的屬性字段,如果是對象則從對象中取值,如果是單個值則直接使用
     * returnJson:預設攔截時返回值,是返回對象的Json
     *
     * http://localhost:8080/api/queryUserInfo?userId=1001
     * http://localhost:8080/api/queryUserInfo?userId=小團團
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
    }

}
  • 這裏提供了一個基本的查詢服務,通過入參userId,查詢用戶信息。後續就需要在這裏擴展白名單,只有指定用戶才可以查詢,其他用戶不能查詢。

2.2 設置Application啟動類

@SpringBootApplication
@Configuration
public class HelloWorldApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloWorldApplication.class, args);
    }

}
  • 這裡是通用的SpringBoot啟動類。需要添加的是一個配置註解@Configuration,為了後續可以讀取白名單配置。

五、用一坨坨代碼實現

一般對於此種場景最簡單的做法就是直接修改代碼

累加if塊幾乎是實現需求最快也是最慢的方式,是修改當前內容很快,是如果同類的內容幾百個也都需要如此修改擴展和維護會越來越慢。

1. 工程結構

itstack-demo-design-10-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── HelloWorldController.java
  • 以上的實現是模擬一個Api接口類,在裏面添加白名單功能,但類似此類的接口會有很多都需要修改,所以這也是不推薦使用此種方式的重要原因。

2. 代碼實現

public class HelloWorldController {

    public UserInfo queryUserInfo(@RequestParam String userId) {

        // 做白名單攔截
        List<String> userList = new ArrayList<String>();
        userList.add("1001");
        userList.add("aaaa");
        userList.add("ccc");
        if (!userList.contains(userId)) {
            return new UserInfo("1111", "非白名單可訪問用戶攔截!");
        }

        return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
    }

}
  • 在這裏白名單的代碼佔據了一大塊,但它又不是業務中的邏輯,而是因為我們上線過程中需要做的開量前測試驗證。
  • 如果你日常對待此類需求經常是這樣開發,那麼可以按照此設計模式進行優化你的處理方式,讓後續的擴展和摘除更加容易。

六、外觀模式重構代碼

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

這次重構的核心是使用外觀模式也可以說門面模式,結合SpringBoot中的自定義starter中間件開發的方式,統一處理所有需要白名單的地方。

後續接下來的實現中,會涉及的知識;

  1. SpringBoot的starter中間件開發方式。
  2. 面向切面編程和自定義註解的使用。
  3. 外部自定義配置信息的透傳,SpringBoot與Spring不同,對於此類方式獲取白名單配置存在差異。

1. 工程結構

itstack-demo-design-10-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design.door
    │   │       ├── annotation
    │   │       │	└── DoDoor.java	
    │   │       ├── config
    │   │       │	├── StarterAutoConfigure.java
    │   │       │	├── StarterService.java
    │   │       │	└── StarterServiceProperties.java
    │   │       └── DoJoinPoint.java
    │   └── resources	
    │       └── META_INF
    │           └── spring.factories
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

門面模式模型結構

  • 以上是外觀模式的中間件實現思路,右側是為了獲取配置文件,左側是對於切面的處理。
  • 門面模式可以是對接口的包裝提供出接口服務,也可以是對邏輯的包裝通過自定義註解對接口提供服務能力。

2. 代碼實現

2.1 配置服務類

public class StarterService {

    private String userStr;

    public StarterService(String userStr) {
        this.userStr = userStr;
    }

    public String[] split(String separatorChar) {
        return StringUtils.split(this.userStr, separatorChar);
    }

}
  • 以上類的內容較簡單隻是為了獲取配置信息。

2.2 配置類註解定義

@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {

    private String userStr;

    public String getUserStr() {
        return userStr;
    }

    public void setUserStr(String userStr) {
        this.userStr = userStr;
    }

}
  • 用於定義好後續在 application.yml 中添加 itstack.door 的配置信息。

2.3 自定義配置類信息獲取

@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {

    @Autowired
    private StarterServiceProperties properties;

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
    StarterService starterService() {
        return new StarterService(properties.getUserStr());
    }

}
  • 以上代碼是對配置的獲取操作,主要是對註解的定義;@Configuration@ConditionalOnClass@EnableConfigurationProperties,這一部分主要是與SpringBoot的結合使用。

2.4 切面註解定義

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {

    String key() default "";

    String returnJson() default "";

}
  • 定義了外觀模式門面註解,後續就是此註解添加到需要擴展白名單的方法上。
  • 這裏提供了兩個入參,key:獲取某個字段例如用戶ID、returnJson:確定白名單攔截后返回的具體內容。

2.5 白名單切面邏輯

@Aspect
@Component
public class DoJoinPoint {

    private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);

    @Autowired
    private StarterService starterService;

    @Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
    public void aopPoint() {
    }

    @Around("aopPoint()")
    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
        //獲取內容
        Method method = getMethod(jp);
        DoDoor door = method.getAnnotation(DoDoor.class);
        //獲取字段值
        String keyValue = getFiledValue(door.key(), jp.getArgs());
        logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
        if (null == keyValue || "".equals(keyValue)) return jp.proceed();
        //配置內容
        String[] split = starterService.split(",");
        //白名單過濾
        for (String str : split) {
            if (keyValue.equals(str)) {
                return jp.proceed();
            }
        }
        //攔截
        return returnObject(door, method);
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

    private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
        return jp.getTarget().getClass();
    }

    //返回對象
    private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
        Class<?> returnType = method.getReturnType();
        String returnJson = doGate.returnJson();
        if ("".equals(returnJson)) {
            return returnType.newInstance();
        }
        return JSON.parseObject(returnJson, returnType);
    }

    //獲取屬性值
    private String getFiledValue(String filed, Object[] args) {
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (null == filedValue || "".equals(filedValue)) {
                    filedValue = BeanUtils.getProperty(arg, filed);
                } else {
                    break;
                }
            } catch (Exception e) {
                if (args.length == 1) {
                    return args[0].toString();
                }
            }
        }
        return filedValue;
    }

}
  • 這裏包括的內容較多,核心邏輯主要是;Object doRouter(ProceedingJoinPoint jp),接下來我們分別介紹。

@Pointcut(“@annotation(org.itstack.demo.design.door.annotation.DoDoor)”)

定義切面,這裏採用的是註解路徑,也就是所有的加入這個註解的方法都會被切面進行管理。

getFiledValue

獲取指定key也就是獲取入參中的某個屬性,這裏主要是獲取用戶ID,通過ID進行攔截校驗。

returnObject

返回攔截后的轉換對象,也就是說當非白名單用戶訪問時則返回一些提示信息。

doRouter

切面核心邏輯,這一部分主要是判斷當前訪問的用戶ID是否白名單用戶,如果是則放行jp.proceed();,否則返回自定義的攔截提示信息。

3. 測試驗證

這裏的測試我們會在工程:itstack-demo-design-10-00中進行操作,通過引入jar包,配置註解的方式進行驗證。

3.1 引入中間件POM配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>itstack-demo-design-10-02</artifactId>
</dependency>
  • 打包中間件工程,給外部提供jar包服務

3.2 配置application.yml

# 自定義中間件配置
itstack:
  door:
    enabled: true
    userStr: 1001,aaaa,ccc #白名單用戶ID,多個逗號隔開
  • 這裏主要是加入了白名單的開關和白名單的用戶ID,逗號隔開。

3.3 在Controller中添加自定義註解

/**
 * http://localhost:8080/api/queryUserInfo?userId=1001
 * http://localhost:8080/api/queryUserInfo?userId=小團團
 */
@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
    return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯衚衕100號");
}
  • 這裏核心的內容主要是自定義的註解的添加@DoDoor,也就是我們的外觀模式中間件化實現。
  • key:需要從入參取值的屬性字段,如果是對象則從對象中取值,如果是單個值則直接使用。
  • returnJson:預設攔截時返回值,是返回對象的Json。

3.4 啟動SpringBoot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.2.RELEASE)

2020-06-11 23:56:55.451  WARN 65228 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2020-06-11 23:56:55.531  INFO 65228 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-11 23:56:55.533  INFO 65228 --- [           main] o.i.demo.design.HelloWorldApplication    : Started HelloWorldApplication in 1.688 seconds (JVM running for 2.934)
  • 啟動正常,SpringBoot已經啟動可以對外提供服務。

3.5 訪問接口接口測試

白名單用戶訪問

http://localhost:8080/api/queryUserInfo?userId=1001

{"code":"0000","info":"success","name":"蟲蟲:1001","age":19,"address":"天津市南開區旮旯衚衕100號"}
  • 此時的測試結果正常,可以拿到接口數據。

非白名單用戶訪問

http://localhost:8080/api/queryUserInfo?userId=小團團

{"code":"1111","info":"非白名單可訪問用戶攔截!","name":null,"age":null,"address":null}
  • 這次我們把userId換成小團團,此時返回的信息已經是被攔截的信息。而這個攔截信息正式我們自定義註解中的信息:@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")

七、總結

  • 以上我們通過中間件的方式實現外觀模式,這樣的設計可以很好的增強代碼的隔離性,以及復用性,不僅使用上非常靈活也降低了每一個系統都開發這樣的服務帶來的風險。
  • 可能目前你看這隻是非常簡單的白名單控制,是否需要這樣的處理。但往往一個小小的開始會影響着後續無限的擴展,實際的業務開發往往也要複雜的很多,不可能如此簡單。因而使用設計模式來讓代碼結構更加乾淨整潔。
  • 很多時候不是設計模式沒有用,而是自己編程開發經驗不足導致即使學了設計模式也很難駕馭。畢竟這些知識都是經過一些實際操作提煉出來的精華,但如果你可以按照本系列文章中的案例方式進行學習實操,還是可以增強這部分設計能力的。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式(多種類型商品發獎場景)
  • 2. 重學 Java 設計模式:實戰抽象工廠模式(替換Redis雙集群升級場景)
  • 3. 重學 Java 設計模式:實戰建造者模式(裝修物料組合套餐選配場景)
  • 4. 重學 Java 設計模式:實戰原型模式(多套試每人題目和答案亂序場景)
  • 5. 重學 Java 設計模式:實戰橋接模式(多支付渠道「微信、支付寶」與多支付模式「刷臉、指紋」場景)
  • 6. 重學 Java 設計模式:實戰組合模式(營銷差異化人群發券,決策樹引擎搭建場景)

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案