燒庫存惹議 Burberry停止銷毀並捨棄真皮草

摘錄自2018年9月6日中央社報導

英國時尚品牌Burberry表示,未來將不再燒毀價值數百萬英鎊計的庫存商品,也不再於產品中使用真皮草。

路透社報導,Burberry在上個財政年度報告中,承認燒毀價值高達2860萬英鎊(約新台幣11.7億元)商品後,表示會致力於讓公司在社會和環境議題更負責任。

Burberry還表示,將追隨Versace、Gucci和道德時尚品牌先驅Stella McCartney的腳步,從系列產品中去除真皮,如兔子,狐狸,貂、亞洲浣熊等的皮毛。Burberry新設計師里卡多提西(Riccardo Tisci)首發系列將不含任何皮草。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

福島災民立場不一的現況

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

幼獸淪名流自拍炫富工具後棄養

摘錄自2018年11月15日公視報導

法國警方19號在香榭大道攔下一輛名車藍寶堅尼,發現裡面有一隻不到兩個月大的獅子寶寶,動保團體表示有越來越多人,會買一隻幼獸自拍炫富,然後棄養,形成歪風。

動保團體「300萬之友」主席胡庭說:「問題就出在馬戲團,他們可以利用野生動物(表演),法國馬戲團大約有1500隻野生動物,可怕的是這些馬戲團有繁殖的權利。」

胡庭認為,買幼獸自拍並上傳這群媒體的歪風,是來自波斯灣阿拉伯國家,這些小動物長大就會被丟棄;他們在約旦撿到三、四十隻被丟棄的小獅子,但更令人擔心的是,現在法國似乎也開始流行,過去半年,他們陸續撿到四隻獅子幼獸。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

【Spring註解驅動開發】在@Import註解中使用ImportBeanDefinitionRegistrar向容器中註冊bean

寫在前面

在前面的文章中,我們學習了如何使用@Import註解向Spring容器中導入bean,可以使用@Import註解快速向容器中導入bean,小夥伴們可以參見《【Spring註解驅動開發】使用@Import註解給容器中快速導入一個組件》。可以在@Import註解中使用ImportSelector接口導入bean,小夥伴們可以參見《【Spring註解驅動開發】在@Import註解中使用ImportSelector接口導入bean》一文。今天,我們就來說說,如何在@Import註解中使用ImportBeanDefinitionRegistrar向容器中註冊bean。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

ImportBeanDefinitionRegistrar概述

概述

我們先來看看ImportBeanDefinitionRegistrar是個什麼鬼,點擊進入ImportBeanDefinitionRegistrar源碼,如下所示。

package org.springframework.context.annotation;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.type.AnnotationMetadata;

public interface ImportBeanDefinitionRegistrar {

	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator) {

		registerBeanDefinitions(importingClassMetadata, registry);
	}

	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}

}

由源碼可以看出,ImportBeanDefinitionRegistrar本質上是一個接口。在ImportBeanDefinitionRegistrar接口中,有一個registerBeanDefinitions()方法,通過registerBeanDefinitions()方法,我們可以向Spring容器中註冊bean實例。

Spring官方在動態註冊bean時,大部分套路其實是使用ImportBeanDefinitionRegistrar接口。

所有實現了該接口的類都會被ConfigurationClassPostProcessor處理,ConfigurationClassPostProcessor實現了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中動態註冊的bean是優先於依賴其的bean初始化的,也能被aop、validator等機制處理。

使用方法

ImportBeanDefinitionRegistrar需要配合@Configuration和@Import註解,@Configuration定義Java格式的Spring配置文件,@Import註解導入實現了ImportBeanDefinitionRegistrar接口的類。

ImportBeanDefinitionRegistrar實例

既然ImportBeanDefinitionRegistrar是一個接口,那我們就創建一個MyImportBeanDefinitionRegistrar類,實現ImportBeanDefinitionRegistrar接口,如下所示。

package io.mykit.spring.plugins.register.condition;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description ImportBeanDefinitionRegistrar的實現類
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * AnnotationMetadata: 當前類的註解信息
     * BeanDefinitionRegistry:BeanDefinition註冊類
     * 通過調用BeanDefinitionRegistry接口的registerBeanDefinition()方法,可以將所有需要添加到容器中的bean注入到容器中。
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){

    }
}

可以看到,這裏,我們先創建了MyImportBeanDefinitionRegistrar類的大體框架。接下來,我們在PersonConfig2類上的@Import註解中,添加MyImportBeanDefinitionRegistrar類,如下所示。

@Configuration
@Import({Department.class, Employee.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})
public class PersonConfig2 {

接下來,創建一個Company類,作為測試測試ImportBeanDefinitionRegistrar接口的bean,如下所示。

package io.mykit.spring.plugins.register.bean;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試ImportBeanDefinitionRegistrar接口的使用
 */
public class Company {
}

接下來,就要實現MyImportBeanDefinitionRegistrar類中的registerBeanDefinitions()方法的邏輯了,添加邏輯后的registerBeanDefinitions()方法如下所示。

    /**
     * AnnotationMetadata: 當前類的註解信息
     * BeanDefinitionRegistry:BeanDefinition註冊類
     * 通過調用BeanDefinitionRegistry接口的registerBeanDefinition()方法,可以將所有需要添加到容器中的bean注入到容器中。
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
        boolean employee = registry.containsBeanDefinition("employee");
        boolean department = registry.containsBeanDefinition("department");
        if (employee && department){
            BeanDefinition beanDefinition = new RootBeanDefinition(Company.class);
            registry.registerBeanDefinition("company", beanDefinition);
        }
    }

registerBeanDefinitions()方法的實現邏輯很簡單,就是判斷Spring容器中是否同時存在以employee命名的bean和以department命名的bean,如果同時存在以employee命名的bean和以department命名的bean,則向Spring容器中注入一個以company命名的bean。

接下來,我們就運行SpringBeanTest類中的testAnnotationConfig7()方法來進行測試,輸出結果信息如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
io.mykit.spring.plugins.register.bean.Employee
io.mykit.spring.plugins.register.bean.User
io.mykit.spring.plugins.register.bean.Role
person
binghe001

可以看到,在輸出結果中,並沒有看到“company”,這是因為輸出結果中存在io.mykit.spring.plugins.register.bean.Department和io.mykit.spring.plugins.register.bean.Employee,並不存在我們代碼邏輯中的department和employee。所以,我們將registerBeanDefinitions()方法的邏輯稍微修改下,修改后的代碼如下所示。

/**
  * AnnotationMetadata: 當前類的註解信息
  * BeanDefinitionRegistry:BeanDefinition註冊類
  * 通過調用BeanDefinitionRegistry接口的registerBeanDefinition()方法,可以將所有需要添加到容器中的bean注入到容器中。
  */
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
    boolean employee = registry.containsBeanDefinition(Employee.class.getName());
    boolean department = registry.containsBeanDefinition(Department.class.getName());
    if (employee && department){
        BeanDefinition beanDefinition = new RootBeanDefinition(Company.class);
        registry.registerBeanDefinition("company", beanDefinition);
    }
}

接下來,我們再次運行SpringBeanTest類中的testAnnotationConfig7()方法來進行測試,輸出結果信息如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig2
io.mykit.spring.plugins.register.bean.Department
io.mykit.spring.plugins.register.bean.Employee
io.mykit.spring.plugins.register.bean.User
io.mykit.spring.plugins.register.bean.Role
person
binghe001
company

可以看到,此時輸出了company,說明Spring容器中已經成功註冊了以company命名的bean。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

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

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

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

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

精美圖文講解Java AQS 共享式獲取同步狀態以及Semaphore的應用

| 好看請贊,養成習慣

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

看到本期內容這麼少,是不是心動了呢?

前言

上一篇萬字長文 Java AQS隊列同步器以及ReentrantLock的應用 為我們讀 JUC 源碼以及其設計思想做了足夠多的鋪墊,接下來的內容我將重點說明差異化,如果有些童鞋不是能很好的理解文中的一些內容,強烈建議回看上一篇文章,搞懂基礎內容,接下來的閱讀真會輕鬆加愉快

AQS 中我們介紹了獨佔式獲取同步狀態的多種情形:

  • 獨佔式獲取鎖
  • 可響應中斷的獨佔式獲取鎖
  • 有超時限制的獨佔式獲取鎖

AQS 提供的模版方法裏面還差共享式獲取同步狀態沒有介紹,所以我們今天來揭開這個看似神秘的面紗

AQS 中的共享式獲取同步狀態

獨佔式是你中沒我,我中沒你的的一種互斥形式,共享式顯然就不是這樣了,所以他們的唯一區別就是:

同一時刻能否有多個線程同時獲取到同步狀態

簡單來說,就是這樣滴:

我們知道同步狀態 state 是維護在 AQS 中的,拋開可重入鎖的概念,我在上篇文章中也提到了,獨佔式和共享式控制同步狀態 state 的區別僅僅是這樣:

所以說想了解 AQS 的 xxxShared 的模版方法,只需要知道它是怎麼控制 state 的就好了

AQS共享式獲取同步狀態源碼分析

為了幫助大家更好的回憶內容,我將上一篇文章的兩個關鍵內容粘貼在此處,幫助大家快速回憶,關於共享式,大家只需要關注【騷紫色】就可以了

自定義同步器需要重寫的方法

AQS 提供的模版方法

故事就從這裏說起吧 (你會發現和獨佔式驚人的相似),關鍵代碼都加了註釋

    public final void acquireShared(int arg) {
      	// 同樣調用自定義同步器需要重寫的方法,非阻塞式的嘗試獲取同步狀態,如果結果小於零,則獲取同步狀態失敗
        if (tryAcquireShared(arg) < 0)
          	// 調用 AQS 提供的模版方法,進入等待隊列
            doAcquireShared(arg);
    }

進入 doAcquireShared 方法:

    private void doAcquireShared(int arg) {
      	// 創建共享節點「SHARED」,加到等待隊列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
          	// 進入“自旋”,這裏並不是純粹意義上的死循環,在獨佔式已經說明過
            for (;;) {
              	// 同樣嘗試獲取當前節點的前驅節點
                final Node p = node.predecessor();
              	// 如果前驅節點為頭節點,嘗試再次獲取同步狀態
                if (p == head) {
                  	// 在此以非阻塞式獲取同步狀態
                    int r = tryAcquireShared(arg);
                  	// 如果返回結果大於等於零,才能跳出外層循環返回
                    if (r >= 0) {
                      	// 這裡是和獨佔式的區別
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面代碼第 18 行我們提到和獨佔式獲取同步狀態的區別,貼心的給大家一個更直觀的對比:

差別只在這裏,所以我們就來看看 setHeadAndPropagate(node, r) 到底幹了什麼,我之前說過 JDK 源碼中的方法命名絕大多數還是非常直觀的,該方法直譯過來就是 【設置頭並且傳播/繁衍】。獨佔式只是設置了頭,共享式除了設置頭還多了一個傳播,你的疑問應該已經來了:

啥是傳播,為什麼會有傳播這個設置呢?

想了解這個問題,你需要先知道非阻塞共享式獲取同步狀態返回值的含義:

這裏說的傳播其實說的是 propagate > 0 的情況,道理也很簡單,當前線程獲取同步狀態成功了,還有剩餘的同步狀態可用於其他線程獲取,那就要通知在等待隊列的線程,讓他們嘗試獲取剩餘的同步狀態

如果要讓等待隊列中的線程獲取到通知,需要線程調用 release 方法實現的。接下來,我們走近 setHeadAndPropagate 一探究竟,驗證一下

  // 入參,node: 當前節點
	// 入參,propagate:獲取同步狀態的結果值,即上面方法中的變量 r
	private void setHeadAndPropagate(Node node, int propagate) {
    		// 記錄舊的頭部節點,用於下面的check
        Node h = head; 
    		// 將當前節點設置為頭節點
        setHead(node);
        
    		// 通過 propagate 的值和 waitStatus 的值來判斷是否可以調用 doReleaseShared 方法
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
          	// 如果後繼節點為空或者後繼節點為共享類型,則進行喚醒後繼節點
    				// 這裏後繼節點為空意思是只剩下當前頭節點了,另外這裏的 s == null 也是判斷空指針的標準寫法
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

上面方法的大方向作用我們了解了,但是代碼中何時調用 doReleaseShared 的判斷邏輯還是挺讓人費解的,為什麼會有這麼一大堆的判斷,我們來逐個分析一下:

這裏的空判斷有點讓人頭大,我們先挑出來說明一下:

排除了其他判斷條件的干擾,接下來我們就專註分析 propagate 和 waitStatus 兩個判斷條件就可以了,這裏再將 waitStatus 的幾種狀態展示在這裏,幫助大家理解,【騷粉色】是我們一會要用到的:

propagate > 0

上面已經說過了,如果成立,直接短路後續判斷,然後根據 doReleaseShared 的判斷條件進行釋放

propagate > 0 不成立, h.waitStatus < 0 成立 (注意這裏的h是舊的頭節點)

什麼時候 h.waitStatus < 0 呢?拋開 CONDITION 的使用,只剩下 SIGNAL 和 PROPAGATE,想知道這個答案,需要提前看一下 doReleaseShared() 方法了:

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                  	// CAS 將頭節點的狀態設置為0                
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 設置成功后才能跳出循環喚醒頭節點的下一個節點
                  	unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         // 將頭節點狀態CAS設置成 PROPAGATE 狀態
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

從 doReleaseShared() 方法中可以看出:

  • 如果讓 h.waitStatus < 0 成立,只能將其設置成 PROPAGATE = -3 的情況,設置成功的前提是 h 頭節點 expected 的狀態是 0;

  • 如果 h.waitStatus = 0,是上述代碼第 8 行 CAS 設置成功,然後喚醒等待中的線程

所以猜測,當前線程執行到 h.waitStatus < 0 的判斷前,有另外一個線程剛好執行了 doReleaseShared() 方法,將 waitStatus 又設置成PROPAGATE = -3

這個理解有點繞,我們還是來畫個圖理解一下吧:

可能有同學還是不太能理解這麼寫的道理,我們一直說 propagate <> = 0 的情況,propagate = 0 代表的是當時/當時/當時 嘗試獲取同步狀態沒成功,但是之後可能又有共享狀態被釋放了,所以上面的邏輯是以防這種萬一,你懂的,嚴謹的併發就是要防止一切萬一,現在結合這個情景再來理解上面的判斷你是否豁然開朗了呢?

繼續向下看,

前序條件不成立,(h = head) == null || h.waitStatus < 0 注意這裏的h是新的頭節點)

有了上面鋪墊,這個就直接畫個圖就更好理解啦,其實就是沒有那麼巧有另外一個線程摻合了

相信到這裏你應該理解共享式獲取同步狀態的全部過程了吧,至於非阻塞共享式獲取同步狀態和帶有超時時間獲取同步狀態,結合本文講的 setHeadAndPropagate 邏輯和獨佔式獲取同步狀態的實現過程過程來看,真是一毛一樣,這裏就不再累述了,趕緊打開你的 IDE 去驗證一下吧

我們分析了AQS 的模版方法,還一直沒說 tryAcquireShared(arg) 這個方法是如何被重寫的,想要了解這個,我們就來看一看共享式獲取同步狀態的經典應用 Semaphore

Semaphore 的應用及源碼分析

Semaphore 概念

Semaphore 中文多翻譯為 【信號量】,我還特意查了一下劍橋辭典的英文解釋:

其實就是信號標誌(two flags),比如紅綠燈,每個交通燈產生兩種不同行為

  • Flag1-紅燈:停車
  • Flag2-綠燈:行車

在 Semaphore 裏面,什麼時候是紅燈,什麼時候是綠燈,其實就是靠 tryAcquireShared(arg) 的結果來表示的

  • 獲取不到共享狀態,即為紅燈
  • 獲取到共享狀態,即為綠燈

所以我們走近 Semaphore ,來看看它到底是怎麼應用 AQS 的,又是怎樣重寫 tryAcquireShared(arg) 方法的

Semaphore 源碼分析

先看一下類結構

看到這裏你是否有點跌眼鏡,和 ReentrantLock 相似的可怕吧,如果你有些陌生,再次強烈建議你回看上一篇文章 Java AQS隊列同步器以及ReentrantLock的應用 ,這裏直接提速對比看公平和非公平兩種重寫的 tryAcquireShared(arg) 方法,沒有意外,公平與否,就是判斷是否有前驅節點

方法內部只是計算 state 的剩餘值,那 state 的初始值是多少怎麼設置呢?當然也就是構造方法了:

		public Semaphore(int permits) {
      	// 默認仍是非公平的同步器,至於為什麼默認是非公平的,在上一篇文章中也特意說明過
        sync = new NonfairSync(permits);
    }
    
    NonfairSync(int permits) {
    		super(permits);
    }

super 方法,就會將初始值給到 AQS 中的 state

也許你發現了,當我們把 permits 設置為1 的時候,不就是 ReentrantLock 的互斥鎖了嘛,說的一點也沒錯,我們用 Semaphore 也能實現基本互斥鎖的效果


static int count;
//初始化信號量
static final Semaphore s 
    = new Semaphore(1);
//用信號量保證互斥    
static void addOne() {
  s.acquire();
  try {
    count+=1;
  } finally {
    s.release();
  }
}

But(英文聽力中的重點),Semaphore 肯定不是為這種特例存在的,它是共享式獲取同步狀態的一種實現。如果使用信號量,我們通常會將 permits 設置成大於1的值,不知道你是否還記得我曾在 為什麼要使用線程池? 一文中說到的池化概念,在同一時刻,允許多個線程使用連接池,每個連接被釋放之前,不允許其他線程使用。所以說 Semaphore 可以允許多個線程訪問一個臨界區,最終很好的做到一個限流/限流/限流 的作用

雖然 Semaphore 能很好的提供限流作用,說實話,Semaphore 的限流作用比較單一,我在實際工作中使用 Semaphore 並不是很多,如果真的要用高性能限流器,Guava RateLimiter 是一個非常不錯的選擇,我們後面會做分析,有興趣的可以提前了解一下

關於 Semaphore 源碼,就這麼三下五除二的結束了

總結

不知你有沒有感覺到,我們的節奏明顯加快了,好多原來分散的點在被瘋狂的串聯起來,如果按照這個方式來閱讀 JUC 源碼,相信你也不會一頭扎進去迷失方向,然後沮喪的退出 JUC 吧,然後面試背誦答案,然後忘記,然後再背誦?

跟上節奏,關於共享式獲取同步狀態,Semaphore 只不過是非常經典的應用,ReadWriteLock 和 CountDownLatch 日常應用還是非常廣泛的,我們接下來就陸續聊聊它們吧

靈魂追問

  1. Semaphore 的 permits 設置成1 “等同於” 簡單的互斥鎖實現,那它和 ReentrantLock 的區別還是挺大的,都有哪些區別呢?
  2. 你在項目中是如何使用 Semaphore 的呢?

參考

  1. Java 併發實戰
  2. Java 併發編程的藝術
  3. https://blog.csdn.net/anlian523/article/details/106319294

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

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

前言

上一篇【.Net Core微服務入門全紀錄(三)——Consul-服務註冊與發現(下)】已經使用Consul完成了服務的註冊與發現,實際中光有服務註冊與發現往往是不夠的,我們需要一個統一的入口來連接客戶端與服務。

Ocelot

官網:https://ocelot.readthedocs.io/
Ocelot正是為.Net微服務體系提供一個統一的入口點,稱為:Gateway(網關)。

  • 上手Ocelot:

首先創建一個空的asp.net core web項目。

注意ocelot.json是我們添加的Ocelot的配置文件,記得設置生成時複製到輸出目錄。ocelot.json的文件名不是固定的,可以自己定義。

NuGet安裝一下Ocelot:

只需簡單的修改幾處默認代碼:
Program.cs:

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config.AddJsonFile("ocelot.json");
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

Startup.cs:

    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            //添加ocelot服務
            services.AddOcelot();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //設置Ocelot中間件
            app.UseOcelot().Wait();
        }
    }

ocelot.json:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 9050
        },
        {
          "Host": "localhost",
          "Port": 9051
        },
        {
          "Host": "localhost",
          "Port": 9052
        }
      ],
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //負載均衡,輪詢機制 LeastConnection/RoundRobin/NoLoadBalancer/CookieStickySessions
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 9060
        },
        {
          "Host": "localhost",
          "Port": 9061
        },
        {
          "Host": "localhost",
          "Port": 9062
        }
      ],
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //負載均衡,輪詢機制 LeastConnection/RoundRobin/NoLoadBalancer/CookieStickySessions
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070"
  }
}

我們先暫時忽略Consul,將服務實例的地址都寫在配置文件中。要知道Consul、Ocelot等組件都是可以獨立存在的。
配置文件中的Routes節點用來配置路由,Downstream代表下游,也就是服務實例,Upstream代表上游,也就是客戶端。我們的路徑比較簡單,只有/products、/orders,路徑中如果有不固定參數則使用{}匹配。我們這個配置的意思呢就是客戶端訪問網關的/orders、/products,網關會轉發給服務實例的/orders、/products,注意這個上游的路徑不一定要和下游一致,比如上游路徑可以配置成/api/orders,/xxx都可以。
LoadBalancerOptions節點用來配置負載均衡,Ocelot內置了 LeastConnection、RoundRobin、NoLoadBalancer、CookieStickySessions 4種負載均衡策略。
BaseUrl節點就是配置我們ocelot網關將要運行的地址。

  • 運行gateway:

目前不考慮網關集群,就不放在docker里了。直接控制台執行:`dotnet Ocelot.APIGateway.dll –urls=”http://*:9070″

用瀏覽器測試一下:

測試正常,我們通過網關可以正常的訪問到服務實例。

  • 接下來繼續改造客戶端代碼:

因為改動太多就直接新建一個GatewayServiceHelper來做。
GatewayServiceHelper:

    /// <summary>
    /// 通過gateway調用服務
    /// </summary>
    public class GatewayServiceHelper : IServiceHelper
    {
        public async Task<string> GetOrder()
        {
            var Client = new RestClient("http://localhost:9070");
            var request = new RestRequest("/orders", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public async Task<string> GetProduct()
        {
            var Client = new RestClient("http://localhost:9070");
            var request = new RestRequest("/products", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public void GetServices()
        {
            throw new NotImplementedException();
        }
    }

然後在Startup中修改一下注入的類型,別的就不用改了,這就是依賴注入的好處之一。。。
Startup.ConfigureServices():

//注入IServiceHelper
//services.AddSingleton<IServiceHelper, ServiceHelper>();
            
//注入IServiceHelper
services.AddSingleton<IServiceHelper, GatewayServiceHelper>();

Startup.Configure():

//程序啟動時 獲取服務列表
//serviceHelper.GetServices();

運行客戶端測試:

好了,現在客戶端對服務的調用都通過網關進行中轉,客戶端再也不用去關心那一堆服務實例的地址,只需要知道網關地址就可以了。另外,服務端也避免了服務地址直接暴露給客戶端。這樣做對客戶端,服務都非常友好。

至於我們的api網關呢,又要說到服務發現的問題了。目前我們的服務地址是寫在ocelot.json配置文件里的,當然這種做法在服務實例不經常變化的情況下是沒有問題的,一旦服務變化,需要人為的修改配置文件,這又顯得不太合理了。

當然,強大的Ocelot為我們提供了服務發現的方案。

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

未完待續…

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

【其他文章推薦】

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

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

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

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

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

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

HashSet擴容機制在時間和空間上的浪費,遠大於你的想象

一:背景

1. 講故事

自從這個純內存項目進了大客戶之後,搞得我現在對內存和CPU特別敏感,跑一點數據內存幾個G的上下,特別沒有安全感,總想用windbg抓幾個dump看看到底是哪一塊導致的,是我的代碼還是同事的代碼? 很多看過我博客的老朋友總是留言讓我出一套windbg的系列或者視頻,我也不會呀,沒辦法,人在江湖飄,遲早得挨上幾刀,逼着也得會幾個花架子,廢話不多說,這一篇就來看看 HashSet 是如何擴容的。

二:HashSet的擴容機制

1. 如何查看

了解如何擴容,最好的辦法就是翻看HashSet底層源碼,最粗暴的入口點就是 HashSet.Add 方法。

從圖中可以看到最後的初始化是用 Initialize 的,而且裏面有這麼一句神奇的代碼: int prime = HashHelpers.GetPrime(capacity);,從字面意思看是獲取一個質數,哈哈,有點意思,什麼叫質數? 簡單說就是只能被 1 和 自身 整除的數就叫做質數,那好奇心就來了,一起看看質數是怎麼算的吧! 再次截圖。

從圖中看,HashSet底層為了加速默認定義好了 72 個質數,最大的一個質數是 719w,換句話就是說當元素個數大於 719w 的時候,就只能使用 IsPrime 方法動態計算質數,如下代碼:


public static bool IsPrime(int candidate)
{
	if ((candidate & 1) != 0)
	{
		int num = (int)Math.Sqrt(candidate);
		for (int i = 3; i <= num; i += 2)
		{
			if (candidate % i == 0)
			{
				return false;
			}
		}
		return true;
	}
	return candidate == 2;
}

看完了整個流程,我想你應該明白了,當你第一次Add的時候,默認的空間佔用是 72 個預定義中最小的一個質數 3,看過我之前文章的朋友知道List的默認大小是4,後面就是簡單粗暴的 * 2 處理,如下代碼。


private void EnsureCapacity(int min)
{
	if (_items.Length < min)
	{
		int num = (_items.Length == 0) ? 4 : (_items.Length * 2);
	}
}

2. HashSet 二次擴容探究

當HashSet的個數達到3之後,很顯然要進行二次擴容,這一點不像List用一個 EnsureCapacity 方法搞定就可以了,然後細看一下怎麼擴容。


public static int ExpandPrime(int oldSize)
{
	int num = 2 * oldSize;
	if ((uint)num > 2146435069u && 2146435069 > oldSize)
	{
		return 2146435069;
	}
	return GetPrime(num);
}

從圖中可以看到,最後的擴容是在 ExpandPrime 方法中完成的,流程就是先 * 2, 再取最接近上限的一個質數,也就是 7 ,然後將 7 作為 HashSet 新的Size,如果你非要看演示,我就寫一小段代碼證明一下吧,如下圖:

2. 您嗅出風險了嗎?

<1> 時間上的風險

為了方便演示,我把 72 個預定義的最後幾個質數显示出來。


public static readonly int[] primes = new int[72]
{
	2009191,
	2411033,
	2893249,
	3471899,
	4166287,
	4999559,
	5999471,
	7199369
};

也就是說,當HashSet的元素個數為 2893249 的時候觸發擴容變成了 2893249 * 2 => 5786498 最接近的一個質數為:5999471,也就是 289w 暴增到了 599w,一下子就是 599w -289w = 310w 的空間虛占,這可是增加了兩倍多哦,嚇人不? 下面寫個代碼驗證下。


        static void Main(string[] args)
        {
            var hashSet = new HashSet<int>(Enumerable.Range(0, 2893249));

            hashSet.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l

000000B8F4DBE500 00007ffaf00132ae ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
    LOCALS:
        0x000000B8F4DBE538 = 0x0000020e0b8fcc08
0:000> !DumpObj /d 0000020e0b8fcc08
Name:        System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
Size:        64(0x40) bytes
File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Collections.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaf0096d10  4000017        8       System.Int32[]  0 instance 0000020e2025e9f8 _buckets
00007ffaf00f7ad0  4000018       10 ...ivate.CoreLib]][]  0 instance 0000020e2bea1020 _slots
00007ffaeffdf828  4000019       28         System.Int32  1 instance          2893250 _count
0:000> !DumpObj /d 0000020e2025e9f8
Name:        System.Int32[]
Size:        23997908(0x16e2dd4) bytes
Array:       Rank 1, Number of elements 5999471, Type Int32 (Print Array)
Fields:
None


而且最重要的是,這裡是一次性擴容的,而非像redis中實現的那樣漸進式擴容,時間開銷也是大家值得注意的。

<2> 空間上的風險

這個有什麼風險呢?可以看一下:289w 和 599w 兩個HashSet的佔用空間大小,這也是我最敏感的。


        static void Main(string[] args)
        {
            var hashSet1 = new HashSet<int>(Enumerable.Range(0, 2893249));

            var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
            hashSet2.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l
OS Thread Id: 0x4a44 (0)
000000B1B4FEE460 00007ffaf00032ea ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 18]
    LOCALS:
        0x000000B1B4FEE4B8 = 0x000001d13363cc08
        0x000000B1B4FEE4B0 = 0x000001d13363d648

0:000> !objsize 0x000001d13363cc08
sizeof(000001D13363CC08) = 46292104 (0x2c25c88) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])
0:000> !objsize 0x000001d13363d648
sizeof(000001D13363D648) = 95991656 (0x5b8b768) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])

可以看到, hashSet1的佔用: 46292104 / 1024 / 1024 = 44.1M, hashSet2 的佔用 : 95991656 / 1024 / 1024 = 91.5M,一下子就浪費了: 91.5 - 44.1 = 47.4M。

如果你真以為僅僅浪費了 47.4M 的話,那你就大錯特錯了,不要忘了底層在擴容的時候,使用新的 size 覆蓋了老的 size,而這個 老的 size 集合在GC還沒有回收的時候會一直佔用堆上空間的,這個能聽得懂嗎? 如下圖:

要驗證的話可以用 windbg 去託管堆上抓一下 Slot[] m_slots 和 int[] m_buckets 兩個數組,我把代碼修改如下:


    static void Main(string[] args)
    {
        var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
        hashSet2.Add(int.MaxValue);
        Console.Read();
    }


0:011> !dumpheap -stat
00007ffaf84f7ad0        3    123455868 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

這裏就拿 Slot[] 說事,從上面代碼可以看到,託管堆上有三個 Slot[] 數組,這就有意思了,怎麼有三個哈,是不是有點懵逼,沒關係,我們將三個 Slot[] 的地址找出來,一個一個看。


0:011> !DumpHeap /d -mt 00007ffaf84f7ad0
         Address               MT     Size
0000016c91308048 00007ffaf84f7ad0 16743180     
0000016c928524b0 00007ffaf84f7ad0 34719012     
0000016ce9e61020 00007ffaf84f7ad0 71993676  

0:011> !gcroot 0000016c91308048
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016c928524b0
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016ce9e61020
Thread 2b0c:
    0000006AFAB7E5F0 00007FFAF84132AE ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 15]
        rbp-18: 0000006afab7e618
            ->  0000016C8000CC08 System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
            ->  0000016CE9E61020 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

從上面可以看到,我通過 gcroot 去找這三個地址的引用根,有兩個是沒有的,最後一個有的自然就是新的 599w 的size,對不對,接下來用 !do 打出這三個地址的值。


0:011> !do 0000016c91308048
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        16743180(0xff7b0c) bytes
Array:       Rank 1, Number of elements 1395263, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016c928524b0
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        34719012(0x211c524) bytes
Array:       Rank 1, Number of elements 2893249, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016ce9e61020
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        71993676(0x44a894c) bytes
Array:       Rank 1, Number of elements 5999471, Type VALUETYPE (Print Array)
Fields:
None

從上面的 Rank 1, Number of elements 信息中可以看到,原來託管堆不僅有擴容前的Size :2893249,還有更前一次的擴容Size: 1395263,所以按這種情況算: 託管堆上的總大小近似為: 23.7M + 47.4M + 91.5M = 162.6M,我去,不簡單把。。。 也就是說:託管堆上有 162.6 - 91.5 =71.1M 的未回收垃圾 剛才的 47.4M 的空間虛佔用,總浪費為:118.5M,但願我沒有算錯。。。

3. 有解決方案嗎?

在List中大家可以通過 Capacity 去控制List的Size,但是很遺憾,在 HashSet 中並沒有類似的解決方案,只有一個很笨拙的裁剪方法: TrimExcess,用於將當前Size擴展到最接近的 質數 值, 如下代碼所示:


public void TrimExcess()
{
	int prime = HashHelpers.GetPrime(m_count);
	Slot[] array = new Slot[prime];
	int[] array2 = new int[prime];
	int num = 0;
	for (int i = 0; i < m_lastIndex; i++)
	{
		if (m_slots[i].hashCode >= 0)
		{
			array[num] = m_slots[i];
			int num2 = array[num].hashCode % prime;
			array[num].next = array2[num2] - 1;
			array2[num2] = num + 1;
			num++;
		}
	}
}

應用到本案例就是將 289w 限制到 347w,仍然有 58w的空間佔用。 如下圖:

三: 總結

HashSet的時間和空間上虛占遠比你想象的大很多,而且實占也不小,因為底層用到了雙數組 m_slots 和 m_buckets,每個Slot還有三個元素: struct Slot { int hashCode;internal int next;internal T value; },所以了解完原理之後謹慎着用吧。

如您有更多問題與我互動,掃描下方進來吧~

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

重學 Java 設計模式:實戰代理模式「模擬mybatis-spring中定義DAO接口,使用代理類方式操作數據庫原理實現場景」

作者:小傅哥

博客:https://bugstack.cn

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

一、前言

難以跨越的瓶頸期,把你拿捏滴死死的!

編程開發學習過程中遇到的瓶頸期,往往是由於看不到前進的方向。這個時候你特別希望能有人告訴你,你還欠缺些什麼朝着哪個方向努力。而導致這一問題的主要原因是由於日常的業務開發太過於複製過去,日復一日的重複。沒有太多的挑戰,也沒參与過較大體量的業務場景,除了這些開發場景因素外,還有缺少組內的技術氛圍和技術分享,沒有人做傳播和佈道者,也缺少自己對各項技術學習的熱情,從而導致一直遊盪在瓶頸之下,難以提升。

小公司與大公司,選擇哪個?

刨除掉薪資以外你會選擇什麼,是不有人建議小公司,因為可以接觸到各個環境,也有人建議大公司,因為正規體量大可以學習到更多。有些時候你的技術成長緩慢也是因為你的不同選擇而導致的,小公司確實要接觸各個環境,但往往如果你所做的業務體量不高,那麼你會用到的技術棧就會相對較少,同時也會技術棧研究的深度也會較淺。大公司中確實有時候你不需要去關心一個集群的部署和維護、一个中間件的開發、全套服務監控等等,但如果你願意了解這些技術在內部都是公開的,你會擁有無限的技術營養可以補充。而這最主要的是提升視野和事業。

除了業務中的CRUD開發,有些技術你真的很難接觸到!

可能很多小夥伴認為技術開發就是承接下產品需求,寫寫CRUD,不會的百度一下,就完事了,總覺得別人問的東西像再造火箭一樣。但在高體量、高併發的業務場景下,每一次的壓測優化,性能提升,都像在研究一道數學題一樣,反覆的錘鍊,壓榨性能。不斷的深究,找到最合適的設計。除了這些優化提升外,還有那麼廣闊的技術體系棧,都可能因為你只是注重CRUD而被忽略;字節碼編程、領域驅動設計架構、代理模式中間件開發、JVM虛擬機實現原理等等。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. Spring 4.3.24.RELEASE
  4. 涉及工程三個,可以通過關注公眾號:bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-12-00 模擬MyBatis開發中間件代理類部分

三、代理模式介紹

代理模式有點像老大和小弟,也有點像分銷商。主要解決的是問題是為某些資源的訪問、對象的類的易用操作上提供方便使用的代理服務。而這種設計思想的模式經常會出現在我們的系統中,或者你用到過的組件中,它們都提供給你一種非常簡單易用的方式控制原本你需要編寫很多代碼的進行使用的服務類。

類似這樣的場景可以想到;

  1. 你的數據庫訪問層面經常會提供一個較為基礎的應用,以此來減少應用服務擴容時不至於數據庫連接數暴增。
  2. 使用過的一些中間件例如;RPC框架,在拿到jar包對接口的描述后,中間件會在服務啟動的時候生成對應的代理類,當調用接口的時候,實際是通過代理類發出的socket信息進行通過。
  3. 另外像我們常用的MyBatis,基本是定義接口但是不需要寫實現類,就可以對xml或者自定義註解里的sql語句進行增刪改查操作。

四、案例場景模擬

在本案例中我們模擬實現mybatis-spring中代理類生成部分

對於Mybatis的使用中只需要定義接口不需要寫實現類就可以完成增刪改查操作,有疑問的小夥伴,在本章節中就可以學習到這部分知識。解析下來我們會通過實現一個這樣的代理類交給spring管理的核心過程,來講述代理類模式。

這樣的案例場景在實際的業務開發中其實不多,因為這是將這種思想運用在中間件開發上,而很多小夥伴經常是做業務開發,所以對Spring的bean定義以及註冊和對代理以及反射調用的知識了解的相對較少。但可以通過本章節作為一個入門學習,逐步了解。

五、代理類模式實現過程

接下來會使用代理類模式來模擬實現一個Mybatis中對類的代理過程,也就是只需要定義接口,就可以關聯到方法註解中的sql語句完成對數據庫的操作。

這裏需要注意一些知識點;

  1. BeanDefinitionRegistryPostProcessor,spring的接口類用於處理對bean的定義註冊。
  2. GenericBeanDefinition,定義bean的信息,在mybatis-spring中使用到的是;ScannedGenericBeanDefinition 略有不同。
  3. FactoryBean,用於處理bean工廠的類,這個類非常見。

1. 工程結構

itstack-demo-design-12-00
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── agent
    │   │       │	├── MapperFactoryBean.java
    │   │       │	├── RegisterBeanFactory.java
    │   │       │	└── Select.java
    │   │       └── IUserDao.java
    │   └── resources	
    │       └── spring-config.xml
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

代理模式中間件模型結構

  • 此模型中涉及的類並不多,但都是抽離出來的核心處理類。主要的事情就是對類的代理和註冊到spring中。
  • 上圖中最上面是關於中間件的實現部分,下面對應的是功能的使用。

2. 代碼實現

2.1 自定義註解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {

    String value() default "";  // sql語句

}
  • 這裏我們定義了一個模擬mybatis-spring中的自定義註解,用於使用在方法層面。

2.2 Dao層接口

public interface IUserDao {

    @Select("select userName from user where id = #{uId}")
    String queryUserInfo(String uId);

}
  • 這裏定義一個Dao層接口,並把自定義註解添加上。這與你使用的mybatis組件是一樣的。
  • 2.1和2.2是我們的準備工作,後面開始實現中間件功能部分。

2.3 代理類定義

public class MapperFactoryBean<T> implements FactoryBean<T> {

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

    private Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        InvocationHandler handler = (proxy, method, args) -> {
            Select select = method.getAnnotation(Select.class);
            logger.info("SQL:{}", select.value().replace("#{uId}", args[0].toString()));
            return args[0] + ",小傅哥,bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!";
        };
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperInterface}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}
  • 如果你有閱讀過mybatis源碼,是可以看到這樣的一個類;MapperFactoryBean,這裏我們也模擬一個這樣的類,在裏面實現我們對代理類的定義。
  • 通過繼承FactoryBean,提供bean對象,也就是方法;T getObject()。
  • 在方法getObject()中提供類的代理以及模擬對sql語句的處理,這裏包含了用戶調用dao層方法時候的處理邏輯。
  • 還有最上面我們提供構造函數來透傳需要被代理類,Class<T> mapperInterface,在mybatis中也是使用這樣的方式進行透傳。
  • 另外getObjectType()提供對象類型反饋,以及isSingleton()返回類是單例的。

2.4 將Bean定義註冊到Spring容器

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {
    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(MapperFactoryBean.class);
        beanDefinition.setScope("singleton");
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(IUserDao.class);

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        // left intentionally blank
    }

}
  • 這裏我們將代理的bean交給spring容器管理,也就可以非常方便讓我們可以獲取到代理的bean。這部分是spring中關於一個bean註冊過程的源碼。
  • GenericBeanDefinition,用於定義一個bean的基本信息setBeanClass(MapperFactoryBean.class);,也包括可以透傳給構造函數信息addGenericArgumentValue(IUserDao.class);
  • 最後使用 BeanDefinitionReaderUtils.registerBeanDefinition,進行bean的註冊,也就是註冊到DefaultListableBeanFactory中。

2.5 配置文件spring-config

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-autowire="byName">

    <bean id="userDao" class="org.itstack.demo.design.agent.RegisterBeanFactory"/>

</beans>
  • 接下來在配置文件中添加我們的bean配置,在mybatis的使用中一般會配置掃描的dao層包,這樣就可以減少這部分的配置。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_IUserDao() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo("100001");
    logger.info("測試結果:{}", res);
}
  • 測試的過程比較簡單,通過加載Bean工廠獲取我們的代理類的實例對象,之後調用方法返回結果。
  • 那麼這個過程你可以看到我們是沒有對接口先一個實現類的,而是使用代理的方式給接口生成一個實現類,並交給spring管理。

3.2 測試結果

23:21:57.551 [main] DEBUG o.s.core.env.StandardEnvironment - Adding PropertySource 'systemProperties' with lowest search precedence
...
23:21:57.858 [main] DEBUG o.s.c.s.ClassPathXmlApplicationContext - Unable to locate LifecycleProcessor with name 'lifecycleProcessor': using default [org.springframework.context.support.DefaultLifecycleProcessor@7bc1a03d]
23:21:57.859 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'lifecycleProcessor'
23:21:57.860 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
23:21:57.861 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userDao'
23:21:57.915 [main] INFO  o.i.d.design.agent.MapperFactoryBean - SQL:select userName from user where id = 100001
23:21:57.915 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:100001,小傅哥,bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!

Process finished with exit code 0
  • 從測試結果可以看到,我們打印了SQL語句,這部分語句是從自定義註解中獲取的;select userName from user where id = 100001,我們做了簡單的適配。在mybatis框架中會交給SqlSession的實現類進行邏輯處理返回操作數據庫數據
  • 而這裏我們的測試結果是一個固定的,如果你願意更加深入的研究可以嘗試與數據庫操作層進行關聯,讓這個框架可以更加完善。

六、總結

  • 關於這部分代理模式的講解我們採用了開發一個關於mybatis-spring中間件中部分核心功能來體現代理模式的強大之處,所以涉及到了一些關於代理類的創建以及spring中bean的註冊這些知識點,可能在平常的業務開發中都是很少用到的,但是在中間件開發中確實非常常見的操作。
  • 代理模式除了開發中間件外還可以是對服務的包裝,物聯網組件等等,讓複雜的各項服務變為輕量級調用、緩存使用。你可以理解為你家裡的電燈開關,我們不能操作220v電線的人肉連接,但是可以使用開關,避免觸電。
  • 代理模式的設計方式可以讓代碼更加整潔、乾淨易於維護,雖然在這部分開發中額外增加了很多類也包括了自己處理bean的註冊等,但是這樣的中間件復用性極高也更加智能,可以非常方便的擴展到各個服務應用中。

七、推薦閱讀

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

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

【其他文章推薦】

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

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

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

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

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

Asp.Net Core入門之自定義中間件

什麼是中間件?

這裏引用官方解釋:

       中間件是用於組成應用程序管道來處理請求和響應的組件。管道內的每一個組件都可以選擇是否將請求交給下一個組件、並在管道中調用下一個組件之前和之後執行某些操作。請求委託被用來建立請求管道,請求委託處理每一個 HTTP 請求。

       請求委託通過使用 IApplicationBuilder 類型的 Run、Map 以及 Use 擴展方法來配置,並在 Startup 類中傳給 Configure 方法 。每個單獨的請求委託都可以被指定為一個內嵌匿名方法,或其定義在一個可重用的類中。這些可重用的類被稱作 中間件 或 中間件組件。每個位於請求管道內的中間件組件負責調用管道中下一個組件,或適時短路調用鏈。

       ASP.NET 請求管道由一系列的請求委託所構成,它們一個接着一個被調用,如圖所示(該執行線程按黑色箭頭的順序執行):

 

ASP.Net Core內置很多中間件,如:靜態文件,身份驗證,MVC等。

這裏需要注意的是,中間件添加的順序非常重要,這會影響他們處理請求的順序。如果順序添加不對,很可能其中某个中間件會造成管道短路,不能觸發下一個請求委託,造成系統異常。

自定義中間件

1.自定義中間件處理邏輯。

 

 1     public class MyMiddleWare
 2     {
 3         private readonly ILogger logger;
 4         private readonly RequestDelegate next;
 5         /// <summary>
 6         /// 通過構造函數 注入Logger日誌和Http請求委託
 7         /// </summary>
 8         /// <param name="_logger"></param>
 9         /// <param name="_next"></param>
10         public MyMiddleWare(ILogger<MyMiddleWare> _logger, RequestDelegate _next)
11         {
12             logger = _logger;
13             next = _next;
14         }
15 
16         /// <summary>
17         /// 在調用下一個組件之前和之後執行某些操作
18         /// </summary>
19         /// <param name="context"></param>
20         /// <returns></returns>
21         public async Task Invoke(HttpContext context)
22         {
23             logger.LogInformation("自定義中間件,開始" + DateTime.Now);
24             await next.Invoke(context);
25             logger.LogInformation("自定義中間件,結束" + DateTime.Now);
26         }
27     }

 

2.使用IApplicationBuilder嚮應用程序的請求管道添加中間件

1     public static class ServiceExtensions
2     {
3         public static IApplicationBuilder UserMyMiddleWare(this IApplicationBuilder app)
4         {
5             return app.UseMiddleware<MyMiddleWare>();
6         }
7     }

3.通過Configure方法添加自定義中間件。

Configure 方法用於指定 ASP.NET 應用程序將如何響應每一個 HTTP 請求。中間件需要最終在這裏添加。

 1         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
 2         {
 3             if (env.IsDevelopment())
 4             {
 5                 app.UseDeveloperExceptionPage();
 6             }
 7             //wwroot目錄下靜態文件
 8             app.UseStaticFiles();
 9             //添加自定義中間件
10             app.UserMyMiddleWare();
11         }

 

當然大家也可以在Configure 方法中直接使用 app.UseMiddleware<MyMiddleWare>()進行添加,我這裡是單獨做了一個派生類,方便管理吧。

 

好了,然後我們啟動程序,大家會發現我們剛才添加的中間件處理請求信息:

 

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

代碼之間為什麼要加空格?

據我觀察,大多數人寫代碼喜歡在符號兩側加空格,而且還會嘲諷不加空格的人。這些支持者認為,加空格可以讓代碼美觀,增強代碼的可讀性。但是我不喜歡加空格,因為這麼寫對代碼含義沒有影響,浪費空間還很麻煩。我不認為多幾個空格就能讓原本凌亂的代碼清晰起來,反而會讓代碼看起來很鬆散,影響閱讀。

以上,是我在知乎上看到的一個題目,感覺蠻清新的,說得似乎有理有據。就好像在說,據我觀察,大多數人喜歡早晚刷牙,而且還會嘲笑不刷牙的人。這些支持者認為,刷牙有益於牙齒健康,空腔清新。但我不喜歡刷牙,因為我牙齒沒壞,吃嘛嘛香。我不認為刷牙能保護牙齒健康,反而很浪費時間。

對吧?我敢保證,這世界上有不少人不刷牙。說出來不怕大家笑話,我上初中之前真沒刷過牙,就算是到現在,很多農村的孩子仍然沒有刷牙的習慣。

在我寫代碼的前一兩年裡,寫代碼也不習慣加空格,覺得很麻煩,並且加不加空格,真的不影響代碼的運行效果。

// 加空格的寫法
int a = 5;
int b = 3;
a = a + b;

// 不加空格的寫法
int a=5;
int b=3;
a=a+b;

想知道為什麼後來我開始刷牙了呢?

記得小學五年級的時候,牙齒疼過一次,真的是喪心病狂地疼,什麼東西也吃不了。那時候,哪有看牙科的,完全沒有,至少在我那時候的成長環境下。

沒辦法,疼了大概有一周的時間,終於打聽到鄰村有一個老先生,他那有秘方,父母就帶我去了。還真的見效,某一種秘制的液體滴在牙齒上,超級冰涼的那種,感覺整個牙齒完全失去了知覺。

有了這段痛苦的記憶,上初中后,看到很多同學都刷牙,於是我也就買了牙刷。初中的條件還是很差勁的,冬天,沒有熱水,洗臉的水和刷牙的水,都是前一天下午飯缸里接的水,放在課桌里。第二天早上到了教室,同學們就互幫互助,端着對方的飯缸倒水洗臉,臉沒凍壞,牙沒凍壞就是萬幸啊。

我在代碼里加空格,和開始刷牙有相似的經歷。

Thisisavarofthings=yourfuckingtype+otherdamnedresult*me;
oh=mygodblessedsweetheart-thesaintangel/cursedcorpsehere;

哦,老天,上面這段代碼里就完全沒有空格,看起來密密麻麻,可讀性差到你感覺自己的雙眼受到了侮辱。

當符號兩側的單詞很簡單的時候,簡單到只有 a 和 b 的時候,的確,不加空格好像看起來更緊湊一些。但考慮到實際的編碼中,命名要有意義,長度都不會太短,這時候,如果沒有空格的輔助,代碼就真的不堪入目了。

Thisisavarofthings = yourfuckingtype + otherdamnedresult * me;
oh = mygodblessedsweetheart - thesaintangel / cursedcorpsehere;

OK,現在加了空格,是不是觀感上就完全不一樣了。單詞與單詞之間,很清楚,很協調,不拖泥帶水。

一般的 IDE 都有自動格式化代碼的快捷鍵,寫完代碼來那麼一下,立馬就會把該有的空格給加上。我現在已經完全習慣手敲空格了,感覺自己很優雅,甚至有點大師範的感覺,畢竟程序員還是要有點追求的,對吧?

說到這,不得不替古人感慨一下,由於紙張稀缺,所以大師們寫出來的文章都是沒有標點符號的,要是沒點文化水平,密密麻麻一團的字擠在一起,真不一定能讀的懂,況且漢語是這麼博大精深。

我們算是幸運的,讀到的文言文都經過了加工,編輯們幫我們加了標點符號,所以看起來不至於那麼吃力。但我就特別討厭一些題目,讓加標點符號斷句的那些,很無聊。

所以,別任性,該加空格就加空格,你寫的代碼可不只是給你一個人看的,負責任點。

換句話說,我也可以寫文章不換行,對吧?寫一行兩行文字真沒必要換行。但段落多了,還不換行,我保證你看不下去。

不過,需要注意一點的是,我看有些視頻課里,有些講課人習慣什麼地方都加空格,我也是有點受不了的,比如說方法名和小括號之間——public void print (),這就有點不合時宜了。

按照規約來說,方法名和小括號之間是不需要空格的,因為沒有空格看起來更自然,更像一個方法,而不是割裂的關鍵字和小括號。

寫代碼不是鑽牛角尖,就像有些人喜歡在 if 語句里使用 == false 一樣——if (isFlase == false) {},他覺得這樣的代碼更具有可讀性,這不是出來搞笑的嗎?

最次,你應該寫成下面這樣子:

if (isTrue) {

} else {

}

這都比在 if 條件中使用 == false 來得讓人舒服。

做程序員嘛,總得有個程序員的樣子,想優雅點的話,把非“!”用上——if (!isFlase),搞那些無病呻吟的代碼,真的很掉檔次。

無規矩不成方圓,你如果不想遵守代碼規約的話,不想團隊協作的話,請隨意處置你的代碼,誰也拿你沒辦法。逃逃逃)

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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