分類
發燒車訊

Gogoro 宣布與新北市政府合作擴充電池交換站,擴充大台北充電網路

Gogoro 致力於電池交換型式的供電系統,提供比傳統機車更為綠能的選擇。13 日 Gogoro 宣布與新北市政府合作建置電池交換站,加上上個月台北市年底前建置的千座電池交換站,大台北地區將逐漸追上加油站數量。

Gogoro 在新北市於明年第一季前預計完成建置 20 座電池交換站、兩座太陽能電池交換站,建置範圍涵蓋三重區、三峽區、中和區、汐止區、板橋區、林口區、新店區、新莊區、樹林區、蘆洲區等新北都會區域。

Gogoro 營運副總潘璟倫表示:「目前雙北市平均每月電池交換次數高達近 28 萬次,為全台灣使用率最高的地區。為了加速提供車主的能源需求,Gogoro 不斷積極的佈建電池交換站以及尋找適合的地點,這次非常感謝新北市政府給予機會讓我們建置足夠的電池換電站提供消費者使用,希望透過這樣的合作持續帶動電動機車的發展。」

Gogoro 在台北市電池交換站已經超越傳統加油站的數量,而與台北市政府合作的 1,000 座電池交換站將拉大差距。本月在大台北地區新開台北天母忠誠、新北新店民權兩家門市。大台北地區將共有 35 家銷售服務據點同時提供完整的服務,完善台北地區服務網路。

配合開學季,Gogoro 也於 9/15 – 10/14 推試騎送限量「All Pass 文具組」,只要消費者到門市體驗試乘,即可獲得此試騎好禮。

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

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

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

分類
發燒車訊

一時技癢,擼了個動態線程池,源碼放Github了

闡述背景

線程池在日常工作中用的還挺多,當需要異步,批量處理一些任務的時候我們會定義一個線程池來處理。

在使用線程池的過程中有一些問題,下面簡單介紹下之前遇到的一些問題。

場景一:實現一些批量處理數據的功能,剛開始線程池的核心線程數設的比較小,然後想調整下,只能改完后重啟應用。

場景二:有一個任務處理的應用,會接收 MQ 的消息進行任務的處理,線程池的隊列也允許緩存一定數量的任務。當任務處理的很慢的時候,想看看到底有多少沒有處理完不是很方便。當時為了快速方便,就直接啟動了一個線程去循環打印線程池隊列的大小。

正好之前在我公眾號有轉發過美團的一篇線程池應用的文章(https://mp.weixin.qq.com/s/tIWAocevZThfbrfWoJGa9w),覺得他們的思路非常好,就是沒有開放源碼,所以自己就抽時間在我的開源項目 Kitty 中增加了一個動態線程池的組件,支持了 Cat 監控,動態變更核心參數,任務堆積告警等。今天就給大家分享一下實現的方式。

項目源代碼地址:https://github.com/yinjihuan/kitty

使用方式

添加依賴

依賴線程池的組件,目前 Kitty 未發布,需要自己下載源碼 install 本地或者私有倉庫。

<dependency>
    <groupId>com.cxytiandi</groupId>
    <artifactId>kitty-spring-cloud-starter-dynamic-thread-pool</artifactId>
</dependency>

添加配置

然後在 Nacos 配置線程池的信息,我的這個整合了 Nacos。推薦一個應用創建一個單獨的線程池配置文件,比如我們這個叫 dataId 為 kitty-cloud-thread-pool.properties,group 為 BIZ_GROUP。

內容如下:

kitty.threadpools.nacosDataId=kitty-cloud-thread-pool.properties
kitty.threadpools.nacosGroup=BIZ_GROUP
kitty.threadpools.accessToken=ae6eb1e9e6964d686d2f2e8127d0ce5b31097ba23deee6e4f833bc0a77d5b71d
kitty.threadpools.secret=SEC6ec6e31d1aa1bdb2f7fd5eb5934504ce09b65f6bdc398d00ba73a9857372de00
kitty.threadpools.owner=尹吉歡
kitty.threadpools.executors[0].threadPoolName=TestThreadPoolExecutor
kitty.threadpools.executors[0].corePoolSize=4
kitty.threadpools.executors[0].maximumPoolSize=4
kitty.threadpools.executors[0].queueCapacity=5
kitty.threadpools.executors[0].queueCapacityThreshold=5
kitty.threadpools.executors[1].threadPoolName=TestThreadPoolExecutor2
kitty.threadpools.executors[1].corePoolSize=2
kitty.threadpools.executors[1].maximumPoolSize=4

nacosDataId,nacosGroup

監聽配置修改的時候需要知道監聽哪個 DataId,值就是當前配置的 DataId。

accessToken,secret

釘釘機器人的驗證信息,用於告警。

owner

這個應用的負責人,告警的消息中會显示。

threadPoolName

線程池的名稱,使用的時候需要關注。

剩下的配置就不一一介紹了,跟線程池內部的參數一致,還有一些可以查看源碼得知。

注入使用

@Autowired
private DynamicThreadPoolManager dynamicThreadPoolManager;
dynamicThreadPoolManager.getThreadPoolExecutor("TestThreadPoolExecutor").execute(() -> {
    log.info("線程池的使用");
    try {
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, "getArticle");

通過 DynamicThreadPoolManager 的 getThreadPoolExecutor 方法獲取線程池對象,然後傳入 Runnable,Callable 等。第二個參數是這個任務的名稱,之所以要擴展一個參數是因為如果任務沒有標識,那麼無法區分任務。

這個線程池組件默認集成了 Cat 打點,設置了名稱可以在 Cat 上查看這個任務相關的監控數據。

擴展功能

任務執行情況監控

在 Cat 的 Transaction 報表中會以線程池的名稱為類型显示。

詳情中會以任務的名稱显示。

核心參數動態修改

核心參數目前只支持 corePoolSize,maximumPoolSize,queueCapacity(隊列類型為 LinkedBlockingDeque 才可以修改),rejectedExecutionType,keepAliveTime,unit 這些參數的修改。

一般 corePoolSize,maximumPoolSize,queueCapacity 是最常要動態改變的。

需要改動的話直接在 Nacos 中將對應的配置值修改即可,客戶端會監聽配置的修改,然後同步修改先線程池的參數。

隊列容量告警

queueCapacityThreshold 是隊列容量告警的閥值,如果隊列中的任務數量超過了 queueCapacityThreshold 就會告警。

拒絕次數告警

當隊列容量滿了后,新進來的任務會根據用戶設置的拒絕策略去選擇對應的處理方式。如果是採用 AbortPolicy 策略,也會進行告警。相當於消費者已經超負荷了。

線程池運行情況

底層對接了 Cat,所以將線程的運行數據上報給了 Cat。我們可以在 Cat 中查看這些信息。

如果你想在自己的平台去展示,我這邊暴露了/actuator/thread-pool 端點,你可以自行拉取數據。

{
	threadPools: [{
		threadPoolName: "TestThreadPoolExecutor",
		activeCount: 0,
		keepAliveTime: 0,
		largestPoolSize: 4,
		fair: false,
		queueCapacity: 5,
		queueCapacityThreshold: 2,
		rejectCount: 0,
		waitTaskCount: 0,
		taskCount: 5,
		unit: "MILLISECONDS",
		rejectedExecutionType: "AbortPolicy",
		corePoolSize: 4,
		queueType: "LinkedBlockingQueue",
		completedTaskCount: 5,
		maximumPoolSize: 4
	}, {
		threadPoolName: "TestThreadPoolExecutor2",
		activeCount: 0,
		keepAliveTime: 0,
		largestPoolSize: 0,
		fair: false,
		queueCapacity: 2147483647,
		queueCapacityThreshold: 2147483647,
		rejectCount: 0,
		waitTaskCount: 0,
		taskCount: 0,
		unit: "MILLISECONDS",
		rejectedExecutionType: "AbortPolicy",
		corePoolSize: 2,
		queueType: "LinkedBlockingQueue",
		completedTaskCount: 0,
		maximumPoolSize: 4
	}]
}

自定義拒絕策略

平時我們使用代碼創建線程池可以自定義拒絕策略,在構造線程池對象的時候傳入即可。這裏由於創建線程池都被封裝好了,我們只能在 Nacos 配置拒絕策略的名稱來使用對應的策略。默認是可以配置 JDK 自帶的 CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy 這四種。

如果你想自定義的話也是支持的,定義方式跟以前一樣,如下:

@Slf4j
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        log.info("進來了。。。。。。。。。");
    }
}

要讓這個策略生效的話使用的是 SPI 的方式,需要在 resources 下面創建一個 META-INF 的文件夾,然後創建一個 services 的文件夾,再創建一個 java.util.concurrent.RejectedExecutionHandler 的文件,內容為你定義的類全路徑。

自定義告警方式

默認是內部集成了釘釘機器人的告警方式,如果你不想用也可以將其關閉。或者將告警信息對接到你的監控平台去。

如果沒有告警平台也可以在項目中實現新的告警方式,比如短信等。

只需要實現 ThreadPoolAlarmNotify 這個類即可。

/**
 * 自定義短信告警通知
 *
 * @作者 尹吉歡
 * @個人微信 jihuan900
 * @微信公眾號 猿天地
 * @GitHub https://github.com/yinjihuan
 * @作者介紹 http://cxytiandi.com/about
 * @時間 2020-05-27 22:26
 */
@Slf4j
@Component
public class ThreadPoolSmsAlarmNotify implements ThreadPoolAlarmNotify {
    @Override
    public void alarmNotify(AlarmMessage alarmMessage) {
        log.info(alarmMessage.toString());
    }
}

代碼實現

具體的就不講的很細了,源碼在https://github.com/yinjihuan/kitty/tree/master/kitty-dynamic-thread-pool,大家自己去看,並不複雜。

創建線程池

根據配置創建線程池,ThreadPoolExecutor 是自定義的,因為需要做 Cat 埋點。

/**
 * 創建線程池
 * @param threadPoolProperties
 */
private void createThreadPoolExecutor(DynamicThreadPoolProperties threadPoolProperties) {
    threadPoolProperties.getExecutors().forEach(executor -> {
        KittyThreadPoolExecutor threadPoolExecutor = new KittyThreadPoolExecutor(
                executor.getCorePoolSize(),
                executor.getMaximumPoolSize(),
                executor.getKeepAliveTime(),
                executor.getUnit(),
                getBlockingQueue(executor.getQueueType(), executor.getQueueCapacity(), executor.isFair()),
                new KittyThreadFactory(executor.getThreadPoolName()),
                getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()), executor.getThreadPoolName());
        threadPoolExecutorMap.put(executor.getThreadPoolName(), threadPoolExecutor);
    });
}

刷新線程池

首先需要監聽 Nacos 的修改。

/**
 * 監聽配置修改,spring-cloud-alibaba 2.1.0版本不支持@NacosConfigListener的監聽
 */
public void initConfigUpdateListener(DynamicThreadPoolProperties dynamicThreadPoolProperties) {
    ConfigService configService = nacosConfigProperties.configServiceInstance();
    try {
        configService.addListener(dynamicThreadPoolProperties.getNacosDataId(), dynamicThreadPoolProperties.getNacosGroup(), new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                new Thread(() -> refreshThreadPoolExecutor()).start();
                log.info("線程池配置有變化,刷新完成");
            }
        });
    } catch (NacosException e) {
        log.error("Nacos配置監聽異常", e);
    }
}

然後再刷新線程池的參數信息,由於監聽事件觸發的時候,這個時候配置其實還沒刷新,所以我就等待了 1 秒鐘,讓配置完成刷新然後直接從配置類取值。

雖然有點挫還是可以用,其實更好的方式是解析 receiveConfigInfo 那個 configInfo,configInfo 就是改變之後的整個配置內容。因為不太好解析成屬性文件,就沒做,後面再改吧。

/**
 * 刷新線程池
 */
private void refreshThreadPoolExecutor() {
    try {
        // 等待配置刷新完成
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    dynamicThreadPoolProperties.getExecutors().forEach(executor -> {
        ThreadPoolExecutor threadPoolExecutor = threadPoolExecutorMap.get(executor.getThreadPoolName());
        threadPoolExecutor.setCorePoolSize(executor.getCorePoolSize());
        threadPoolExecutor.setMaximumPoolSize(executor.getMaximumPoolSize());
        threadPoolExecutor.setKeepAliveTime(executor.getKeepAliveTime(), executor.getUnit());
        threadPoolExecutor.setRejectedExecutionHandler(getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()));
        BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
        if (queue instanceof ResizableCapacityLinkedBlockIngQueue) {
            ((ResizableCapacityLinkedBlockIngQueue<Runnable>) queue).setCapacity(executor.getQueueCapacity());
        }
    });
}

其他的刷新都是線程池自帶的,需要注意的是線程池隊列大小的刷新,目前只支持 LinkedBlockingQueue 隊列,由於 LinkedBlockingQueue 的大小是不允許修改的,所以按照美團那篇文章提供的思路,自定義了一個可以修改的隊列,其實就是把 LinkedBlockingQueue 的代碼複製了一份,改一下就可以。

往 Cat 上報運行信息

往 Cat 的 Heartbeat 報表上傳數據的代碼如下,主要還是 Cat 本身提供了擴展的能力。只需要定時去調用下面的方式上報數據即可。

public void registerStatusExtension(ThreadPoolProperties prop, KittyThreadPoolExecutor executor) {
    StatusExtensionRegister.getInstance().register(new StatusExtension() {
        @Override
        public String getId() {
            return "thread.pool.info." + prop.getThreadPoolName();
        }
        @Override
        public String getDescription() {
            return "線程池監控";
        }
        @Override
        public Map<String, String> getProperties() {
            AtomicLong rejectCount = getRejectCount(prop.getThreadPoolName());
            Map<String, String> pool = new HashMap<>();
            pool.put("activeCount", String.valueOf(executor.getActiveCount()));
            pool.put("completedTaskCount", String.valueOf(executor.getCompletedTaskCount()));
            pool.put("largestPoolSize", String.valueOf(executor.getLargestPoolSize()));
            pool.put("taskCount", String.valueOf(executor.getTaskCount()));
            pool.put("rejectCount", String.valueOf(rejectCount == null ? 0 : rejectCount.get()));
            pool.put("waitTaskCount", String.valueOf(executor.getQueue().size()));
            return pool;
        }
    });
}

定義線程池端點

通過自定義端點來暴露線程池的配置和運行的情況,可以讓外部的監控系統拉取數據做對應的處理。

@Endpoint(id = "thread-pool")
public class ThreadPoolEndpoint {
    @Autowired
    private DynamicThreadPoolManager dynamicThreadPoolManager;
    @Autowired
    private DynamicThreadPoolProperties dynamicThreadPoolProperties;
    @ReadOperation
    public Map<String, Object> threadPools() {
        Map<String, Object> data = new HashMap<>();
        List<Map> threadPools = new ArrayList<>();
        dynamicThreadPoolProperties.getExecutors().forEach(prop -> {
            KittyThreadPoolExecutor executor = dynamicThreadPoolManager.getThreadPoolExecutor(prop.getThreadPoolName());
            AtomicLong rejectCount = dynamicThreadPoolManager.getRejectCount(prop.getThreadPoolName());
            Map<String, Object> pool = new HashMap<>();
            Map config = JSONObject.parseObject(JSONObject.toJSONString(prop), Map.class);
            pool.putAll(config);
            pool.put("activeCount", executor.getActiveCount());
            pool.put("completedTaskCount", executor.getCompletedTaskCount());
            pool.put("largestPoolSize", executor.getLargestPoolSize());
            pool.put("taskCount", executor.getTaskCount());
            pool.put("rejectCount", rejectCount == null ? 0 : rejectCount.get());
            pool.put("waitTaskCount", executor.getQueue().size());
            threadPools.add(pool);
        });
        data.put("threadPools", threadPools);
        return data;
    }
}

Cat 監控線程池中線程的執行時間

本來是將監控放在 KittyThreadPoolExecutor 的 execute,submit 方法里的。後面測試下來發現有問題,數據在 Cat 上確實有了,但是執行時間都是 1 毫秒,也就是沒生效。

不說想必大家也知道,因為線程是後面單獨去執行的,所以再添加任務的地方埋點沒任務意義。

後面還是想到了一個辦法來實現埋點的功能,就是利用線程池提供的 beforeExecute 和 afterExecute 兩個方法,在線程執行之前和執行之後都會觸發這兩個方法。

@Override
protected void beforeExecute(Thread t, Runnable r) {
    String threadName = Thread.currentThread().getName();
    Transaction transaction = Cat.newTransaction(threadPoolName, runnableNameMap.get(r.getClass().getSimpleName()));
    transactionMap.put(threadName, transaction);
    super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
    super.afterExecute(r, t);
    String threadName = Thread.currentThread().getName();
    Transaction transaction = transactionMap.get(threadName);
    transaction.setStatus(Message.SUCCESS);
    if (t != null) {
        Cat.logError(t);
        transaction.setStatus(t);
    }
    transaction.complete();
    transactionMap.remove(threadName);
}

後面的代碼大家自己去看就行了,本文到這裏就結束了。如果感覺本文還不錯的記得轉發下哦!

多謝多謝。

最後感謝美團技術團隊的那篇文章,雖然沒有分享源碼,但是思路什麼的和應用場景都講的很明白。

感興趣的 Star 下唄:https://github.com/yinjihuan/kitty

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

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

分類
發燒車訊

載不動聖誕老人 瑞典夏季乾旱 部分馴鹿餓死

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

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

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

分類
發燒車訊

Python 為什麼推薦蛇形命名法?

關於變量的命名,這又是一個容易引發程序員論戰的話題。如何命名才能更具有可讀性、易寫性與明義性呢?眾說紛紜。

本期“Python為什麼”欄目,我們將聚焦於變量命名中的連接方式,來切入這塊是非之地,想要回答的問題是——Python 為什麼要推薦蛇形命名法?

首先一點,對於單個字符或者單詞 (例如:a、A、PYTHON、Cat),當它們被用作變量名時,大致有全小寫、全大寫和首字母大寫這幾種情況。編程語言中出現這些情況時,它們基本上跟英語的表達習慣是相同的。

但是,編程語言為了令變量名表達出更豐富的含義,通常需要使用多個單詞或符號。 英語習慣使用空格來間隔開單詞,然而這種用法在編程語言中會帶來一些麻煩,所以程序員們就創造出了另外的方法:

  • 蛇形命名法(snake case)
  • 駝峰命名法(camel case)
  • 匈牙利命名法(HN case)
  • 帕斯卡命名法(Pascal case)
  • 脊柱命名法(spinal case)
  • 自由命名法(studly caps)
  • 駝峰蛇形命名法

總體而言,這些命名法都是要克服單詞間的空格,從而把不同單詞串連起來, 最終達到創造出一種新的“單詞”的效果。

我畫了一張思維導圖,大略區分了這幾種命名法:

如果按照受眾量與知名程度排名,毫無疑問排前兩位的是駝峰命名法和蛇形命名法。

我們可以簡單比較一下它們的優缺點:

  • 可讀性:蛇形命名法用下劃線拉大詞距,更清楚易讀;駝峰命名法的變量名緊湊,節省行寬
  • 易寫性:駝峰命名法以大小寫為區分,不引入額外的標識符;蛇形命名法統一小寫,輸入相對方便
  • 明義性:對於某些縮寫成的專有名詞,例如 HTTP、RGB、DNS等等,一般習慣全用大寫表示,但是如果嚴格遵循這兩種命名法的話,須得只留首字母大寫或者全小寫,這樣對原意都會造成一些“破壞”,有時候甚至讓人感覺到彆扭。如果保留全大寫,IDE 可能識別不準,反而會出現波浪提示

由此可見,它們各有優缺點,但哪一方都不具有壓倒性。我個人稍微偏好於蛇形命名法,但是在需要用駝峰命名的時候(比如寫 Java 時),也能無障礙切換。

需要指出的是,Python 也推薦使用駝峰式命名,那是在類名、Type 變量、異常 exception 名這些情況。而在包名、模塊名、方法名和普通變量名 等情況,則是推薦用蛇形命名(lower_case_with_underscores)。

那麼,為什麼 Python 會推薦用蛇形命名法呢?

最大的原因是歷史原因。 蛇形命名方式起源於 1960 年代,那時它甚至還沒有特定的名稱。Python 從 C 語言中借鑒過來后,給它起名為“lower_case_with_underscores”,即帶下劃線的小寫命名。

直到 21 世紀初的幾年,在 Intel 和 Ruby 社區中,才有人開始以“snake_case”即蛇形命名來稱呼它。

現今有不少編程語言在某些場景下會推薦使用蛇形命名法,而 Python 則是其中最早這麼做的之一,並且是使用場景最多的語言之一。

維基百科上統計了一份清單,可以看出 Python 對它的偏好:

其次,還有一個比較重要的原因,那就是 Python 對下劃線“_”的獨特偏愛。

比如類似於 _xx、__xx、xx_、__xx__ 等等的寫法就隨處可見,甚至還有孤零零一個下劃線 _ 作為變量的特殊情況。這樣看來,下劃線作為單詞間的連接,恰恰是這種傳統習慣的一部分。

最後,我還看到過一種解釋:因為 Python 是蟒蛇啊,理所當然是用蛇形命名……

對於這三個解釋,你們是如何感想的呢?對於蛇形命名法,大家是喜歡還是不喜歡呢?歡迎留言交流。

寫在最後:本文屬於“Python為什麼”系列(Python貓出品),該系列主要關注 Python 的語法、設計和發展等話題,以一個個“為什麼”式的問題為切入點,試着展現 Python 的迷人魅力。部分話題會推出視頻版,請在 B 站收看,觀看地址:視頻地址

公眾號【Python貓】, 本號連載優質的系列文章,有Python為什麼系列、喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

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

聚甘新

分類
發燒車訊

福斯拼2025年躍居電動車龍頭、砸500億歐元採購電池

  福斯集團(Volkswagen Group)11月18日宣布,2018~2022年期間旗下核心品牌將在全球投資228億歐元、當中有140億歐元將會投入德國地區工廠。位於德國茲威考(Zwickau)的工廠將獲得10億歐元的挹注、藉此轉型為純電動車生產線。   福斯預估2025年旗下電動車產量至少可達100萬輛。採用MEB(Modular Electric Drive Kit)模組化電動車平台技術的電動車續航力將介於400~600公里之間。首款搭載MEB的Volkswagen I.D.預計在2020年發表、當年產量預估將達10萬輛。   福斯集團11月17日宣布,2018~2022年期間集團將投入340億歐元在電動移動、自主駕駛、新型移動服務以及數位化等領域。福斯9月宣布,2030年底以前旗下所有車款都將具備電動驅動選項。   福斯執行長Matthias Muller表示,上述投資將為集團在2025年躍居全球最大電動車廠商奠定基礎。Muller提到,未來數年整個汽車業將面臨根本性的變化。 為了滿足對電池產能的巨大需求,福斯集團已啟動了旨在建立長期戰略合作夥伴關係的全球(包括中國在內)招標程序、金額預估將超過500億歐元。   福斯集團預期巴西、中國、俄羅斯以及北美將會是未來數年的主要成長來源。   福斯汽車公司(中國)11月16日宣布,從現在到2025年這段期間公司與合資企業夥伴將投資逾100億歐元在電動移動工業化領域。   Thomson Reuters報導,英國財政部19日表示,財長Philip Hammond將在11月22日公布的政府預算書當中宣布提供總額達1.0億英鎊的電動車購買補助金。   (本文內容由授權使用。首圖來源:pixabay)  

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

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

聚甘新

分類
發燒車訊

電動巴士開進太魯閣,花縣繼續推動綠能公車政策

花蓮縣太魯閣客運公司今年繼續辦理增資,以採購新的電動巴士。自9月起,往來於花蓮市區、太魯閣、東華大學等地的公車路線,將有電動巴士陸續開始運行。

花蓮縣政府的「綠能公車」政策於2014年核定太魯閣客運公司的301線、302線兩條公車路線上路,且須採用電動巴士運行。一開始因電動巴士品質不夠穩定,一度改租用柴油巴士,但受到反彈而停駛;之後在地方民代的協調下,301、302線規劃復駛,並透過增資採購7輛低底盤K9電動巴士車輛,重新展開電動巴士服務。

太魯閣客運公司經理表示,本次採購的7輛K9電動巴士為台灣凱勝綠能科技所生產,車體採用全鋁合金打造,並配有緊急自動滅火系統、ABS防鎖剎車等。車輛搭載的電池為台灣長利科技的磷酸鋰鐵電池,每次充飽電的續航力可達250公里以上。

K9巴士的性能佳,目前在苗栗客運、捷順交通、雲林客運、南台灣客運等公司都有同款車輛正在提供服務。太魯閣客運採購K9巴士時,為挑戰巴士的續航力,還安排一輛K9巴士直接從苗栗車體廠繞過北台灣、蘇花公路一路行駛到花蓮東華大學,總里程320.7公里,全程開冷氣;駛達目的地後,電池的電量還有43.9%之多。

目前,301線已配合東華大學開學而正式復駛。太魯閣客運希望新城到天祥的302線也能在年底前復駛,預計將有6輛電動巴士投入服役。

(照片來源:東華大學/太魯閣客運)

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

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

聚甘新

分類
發燒車訊

spring源碼分析——BeanPostProcessor接口

 

  BeanPostProcessor是處理bean的後置接口,beanDefinitionMaps中的BeanDefinition實例化完成后,完成populateBean,屬性設置,完成

初始化后,這個接口支持對bean做自定義的操作。

一:BeanPostProcessor的使用

定義一個測試用的model對象,name屬性默認為hello

public class BeanDemo {

	private String name = "hello";

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		final StringBuffer sb = new StringBuffer("BeanDemo{");
		sb.append("name='").append(name).append('\'');
		sb.append('}');
		return sb.toString();
	}
}

  

自定義一個MyBeanPostProcessor類,實現BeanPostProcessor接口

@Service
public class MyBeanPostProcessor implements BeanPostProcessor {

	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return null;
	}

	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if(beanName.equals("beanDemo")){
			BeanDemo beanDemo = (BeanDemo)bean;
			beanDemo.setName("kitty");
			return beanDemo;
		}
		return bean;
	}
}

  

 

 

從運行結果看,spring中維護的beanName為beanDemo的對象,name屬性為ketty

 

 

二:看看源碼怎麼實現的

1:實例化並且註冊到beanPostProcessors集合中

 

 

 

 

主要的實例化邏輯在這個接口,這個接口的作用就是把所有實現BeanPostProcessor接口的類實例化,然後註冊到 beanPostProcessors這個緩存中

 

 

	public static void registerBeanPostProcessors(
			ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {

		// 獲取所有實現接口BeanPostProcessor的beanName
		String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

		// Register BeanPostProcessorChecker that logs an info message when
		// a bean is created during BeanPostProcessor instantiation, i.e. when
		// a bean is not eligible for getting processed by all BeanPostProcessors.
		int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
		beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

		// Separate between BeanPostProcessors that implement PriorityOrdered,
		// Ordered, and the rest.
		/**
		 * 把實現PriorityOrdered 和 Ordered 和 其他的處理器分開
		 */
		List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
		List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
		List<String> orderedPostProcessorNames = new ArrayList<>();
		List<String> nonOrderedPostProcessorNames = new ArrayList<>();
		/**
		 * 1:遍歷集合postProcessorNames
		 * 2:判斷類型,如果是PriorityOrdered,則實例化對象放入priorityOrderedPostProcessors集合,
		 * Ordered 則放入orderedPostProcessorNames集合,其他的放入nonOrderedPostProcessorNames集合
 		 */
		for (String ppName : postProcessorNames) {
			if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
				BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
				priorityOrderedPostProcessors.add(pp);
				if (pp instanceof MergedBeanDefinitionPostProcessor) {
					internalPostProcessors.add(pp);
				}
			}
			else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
				orderedPostProcessorNames.add(ppName);
			}
			else {
				nonOrderedPostProcessorNames.add(ppName);
			}
		}

		// First, register the BeanPostProcessors that implement PriorityOrdered.
		// 首先對priorityOrderedPostProcessors集合中實例對象排序,然後註冊,放入beanFactory中緩存下來
		sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

		// Next, register the BeanPostProcessors that implement Ordered.
		// 然後再實例化實現Ordered接口的對象,完成註冊
		List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>();
		for (String ppName : orderedPostProcessorNames) {
			BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
			orderedPostProcessors.add(pp);
			if (pp instanceof MergedBeanDefinitionPostProcessor) {
				internalPostProcessors.add(pp);
			}
		}
		sortPostProcessors(orderedPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, orderedPostProcessors);

		// Now, register all regular BeanPostProcessors.
		// 最後實例化什麼都沒有實現的,完成實例化並註冊
		List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>();
		for (String ppName : nonOrderedPostProcessorNames) {
			BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
			nonOrderedPostProcessors.add(pp);
			if (pp instanceof MergedBeanDefinitionPostProcessor) {
				internalPostProcessors.add(pp);
			}
		}
		registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

		// Finally, re-register all internal BeanPostProcessors.
		// 最後再次註冊內部postProcessor
		sortPostProcessors(internalPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, internalPostProcessors);

		// Re-register post-processor for detecting inner beans as ApplicationListeners,
		// moving it to the end of the processor chain (for picking up proxies etc).
		beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
	}

  

 

 

定義四類容器,高優先級有序、有序、無序、內部

 

 分類放入四種容器:

 

 

註冊BeanPostProcessor,將實現BeanPostProcessor接口的對象放入beanPostProcessors緩存中

 

 

 

 

 

 

註冊完PriorityOrdered的實現類后,再處理Ordered的實現類

 

 

註冊什麼都沒有實現的BeanPostProcessor接口實現類,

 

 

最後註冊內部的BeanPostProcessor對象

 

 到這裏BeanPostProcessor的實例化以及註冊工作完成,在beanFactory的beanPostProcessors集合中已經緩存了所有的beanPostProcessor的對象

 

2:BeanPostProcessor的使用

因為這個接口是bean的後置接口,所以需要bean創建並初始化完成,才可以發揮作用,上一步的緩存只是埋好點,以備使用,因為bean的實例化流程我們

還沒有分析,這裏直接看一下怎麼使用的

 

 

我們看一下init方法后的攔截,因為這個時候已經init完成,可以在後置接口中對bean做一下修改的操作

 

 

調用到我們自定義的MyBeanPostProcessor實現類:

 

 

把這個beanDemo對象屬性修改一下,修改完,再返回,將這個對象緩存到spring的一級緩存中。

 

 

總結:

  BeanPostProcessor接口主要是對bean對象做一些自定義的操作,修改bean對象的信息,aop代理也是通過這種方式實現的,

在refresh的registryBeanPostProcessor方法中實例化BeanPostProcessor對象,並且註冊到beanFactory容器的beanPostProcessors的緩存中,

然後在後續的操作中攔截使用。

 

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

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

聚甘新

分類
發燒車訊

菜渣開源一個基於 EMIT 的 AOP 庫(.NET Core)

目錄

  • 1,快速入門
    • 1.1 繼承 ActionAttribute 特性
    • 1.2 標記代理類型
    • 2,如何創建代理類型
      • 2.1 通過API直接創建
  • 2,創建代理類型
    • 通過API
    • 通過 Microsoft.Extensions.DependencyInjection
      • 通過 Autofac
  • 3,深入使用
    • 代理類型
    • 方法、屬性代理
    • 上下文
      • 攔截方法或屬性的參數
    • 非侵入式代理

Nuget 庫地址:https://www.nuget.org/packages/CZGL.AOP/

Github 庫地址:https://github.com/whuanle/CZGL.AOP

CZGL.AOP 是 基於 EMIT 編寫的 一個簡單輕量的AOP框架,支持非侵入式代理,支持.NET Core/ASP.NET Core,以及支持多種依賴注入框架。

1,快速入門

CZGL.AOP 使用比較簡單,你只需要使用 [Interceptor] 特性標記需要代理的類型,然後使用繼承 ActionAttribute 的特性標記要被代理的方法或屬性。

1.1 繼承 ActionAttribute 特性

ActionAttribute 是用於代理方法或屬性的特性標記,不能直接使用,需要繼承后重寫方法。

示例如下:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

Before 會在被代理的方法執行前或被代理的屬性調用時生效,你可以通過 AspectContext 上下文,獲取、修改傳遞的參數。

After 在方法執行后或屬性調用時生效,你可以通過上下文獲取、修改返回值。

1.2 標記代理類型

在被代理的類型中,使用 [Interceptor] 特性來標記,在需要代理的方法中,使用 繼承了 ActionAttribute 的特性來標記。

此方法是侵入式的,需要在編譯前完成。

[Interceptor]
public class Test : ITest
{
    [Log] public virtual string A { get; set; }
    [Log]
    public virtual void MyMethod()
    {
        Console.WriteLine("運行中");
    }
}

注意的是,一個方法或屬性只能設置一個攔截器。

2,如何創建代理類型

CZGL.AOP 有多種生成代理類型的方式,下面介紹簡單的方式。

請預先創建如下代碼:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

    public interface ITest
    {
        void MyMethod();
    }

    [Interceptor]
    public class Test : ITest
    {
        [Log] public virtual string A { get; set; }
        public Test()
        {
            Console.WriteLine("構造函數沒問題");
        }
        [Log]
        public virtual void MyMethod()
        {
            Console.WriteLine("運行中");
        }
    }

2.1 通過API直接創建

通過 CZGL.AOP 中的 AopInterceptor 類,你可以生成代理類型。

示例如下:

            ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
            Test test2 = AopInterceptor.CreateProxyOfClass<Test>();
            test1.MyMethod();
            test2.MyMethod();

CreateProxyOfInterface 通過接口創建代理類型;CreateProxyOfClass 通過類創建代理類型;

默認調用的是無參構造函數。

2,創建代理類型

通過API

你可以參考源碼解決方案

中的 ExampleConsole 項目。

如果要直接使用 AopInterceptor.CreateProxyOfInterfaceAopInterceptor.CreateProxyOfClass 方法,通過接口或類來創建代理類型。

        ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
        Test test2 = AopInterceptor.CreateProxyOfClass<Test>();

如果要指定實例化的構造函數,可以這樣:

            // 指定構造函數
            test2 = AopInterceptor.CreateProxyOfClass<Test>("aaa", "bbb");
            test2.MyMethod();

通過 Microsoft.Extensions.DependencyInjection

Microsoft.Extensions.DependencyInjection 是 .NET Core/ASP.NET Core 默認的依賴注入容器。

如果需要支持 ASP.NET Core 中使用 AOP,你可以在 Nuget 包中安裝 CZGL.AOP.MEDI

如果你在控制台下使用 Microsoft.Extensions.DependencyInjection,你可以使用名為 BuildAopProxyIServiceCollection 拓展方法來為容器中的類型,生成代理類型。

示例如下:

            IServiceCollection _services = new ServiceCollection();
            _services.AddTransient<ITest, Test>();
            var serviceProvider = _services.BuildAopProxy().BuildServiceProvider();
            serviceProvider.GetService<ITest>();
            return serviceProvider;

你可以參考源碼解決方案中的 ExampleMEDI 項目。

如果你要在 ASP.NET Core 中使用,你可以在 Startup 中,ConfigureServices 方法的最後一行代碼使用 services.BuildAopProxy();

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.BuildAopProxy();
        }

還可以在 ProgramIHostBuilder 中使用 .UseServiceProviderFactory(new AOPServiceProxviderFactory()) 來配置使用 CZGL.AOP。

示例:

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .UseServiceProviderFactory(new AOPServiceProxviderFactory())
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });

可以參考解決方案中的 ExampleConsoleExampleWebMEDI 兩個項目。

你不必擔心引入 CZGL.AOP 后,使用依賴注入會使程序變慢或者破壞容器中的原有屬性。CZGL.AOP 只會在創建容器時處理需要被代理的類型,不會影響容器中的服務,也不會幹擾到依賴注入的執行。

通過 Autofac

如果需要在 Autofac 中使用 AOP,則需要引用 CZGL.AOP.Autofac 包。

如果你在控制台程序中使用 Autofac,則可以在 Build() 後面使用 BuildAopProxy()

            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<Test>().As<ITest>();
            var container = builder.Build().BuildAopProxy();

            using (ILifetimeScope scope = container.BeginLifetimeScope())
            {
                // 獲取實例
                ITest myService = scope.Resolve<ITest>();
                myService.MyMethod();
            }

            Console.ReadKey();
        }

要注意的是,在已經完成的組件註冊創建一個新的容器后,才能調用 BuildAopProxy() 方法,

這樣針對一個新的容器你可以考慮是否需要對容器中的組件進行代理。

如果在 ASP.NET Core 中使用 Autofac,你需要在 Program 類的 IHostBuilder 中使用:

.UseServiceProviderFactory(new AutofacServiceProviderFactory())

如果需要代理已經註冊的組件,則將其替換為:

 .UseServiceProviderFactory(new CZGL.AOP.Autofac.AOPServiceProxviderFactory())

請參考 源碼解決方案中的 ExampleAutofacExampleWebAutofac 兩個項目。

3,深入使用

代理類型

要被代理的類型,需要使用 [Interceptor]來標記,例如:

    [Interceptor]
    public class Test : ITest
    {
    }

支持泛型類型。

被代理的類型必須是可被繼承的。

類型的構造函數沒有限制,你可以隨意編寫。

在使用 API 創建代理類型並且實例化時,你可以指定使用哪個構造函數。

例如:

			string a="",b="",c="";
			ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>(a,b,c);

API 會根據參數的多少以及參數的類型自動尋找合適的構造函數。

方法、屬性代理

為了代理方法或屬性,你需要繼承 ActionAttribute 特性,然後為方法或屬性標記此特性,並且將方法或屬性設置為 virtual

一個類型中的不同方法,可以使用不同的攔截器。

        [Log1]
        public virtual void MyMethod1(){}
        
        [Log2]
        public virtual void MyMethod2(){}

對於屬性,可以在屬性上直接使用特性,或者只在 get 或 set 構造器使用。

        [Log] public virtual string A { get; set; }
        
        // 或
        public virtual string A { [Log] get; set; }
        
        // 或
        public virtual string A { get; [Log] set; }

如果在屬性上使用特性,相當於 [Log] get; [Log] set;

上下文

一個簡單的方法或屬性攔截標記是這樣的:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

AspectContext 的屬性說明如下:

字段 說明
Type 當前被代理類型生成的代理類型
ConstructorParamters 類型被實例化時使用的構造函數的參數,如果構造函數沒有參數,則 MethodValues.Length = 0,而不是 MethodValues 為 null。
IsProperty 當前攔截的是屬性
PropertyInfo 當前被執行的屬性的信息,可為 null。
PropertyValue 但調用的是屬性時,返回 get 的結果或 set 的 value 值。
IsMethod 當前攔截的是方法
MethodInfo 當前方法的信息
MethodValues 方法被調用時傳遞的參數,如果此方法沒有參數,則 MethodValues.Length = 0,而不是 MethodValues 為 null
MethodResult 方法執行返回的結果(如果有)

攔截方法或屬性的參數

通過上下文,你可以修改方法或屬性的參數以及攔截返回結果:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            // 攔截並修改方法的參數
            for (int i = 0; i < context.MethodValues.Length; i++)
            {
                context.MethodValues[i] = (int)context.MethodValues[i] + 1;
            }
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");

            // 攔截方法的執行結果
            context.MethodResult = (int)context.MethodResult + 664;

            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

    [Interceptor]
    public class Test
    {
        [Log]
        public virtual int Sum(int a, int b)
        {
            Console.WriteLine("運行中");
            return a + b;
        }
    }
            Test test = AopInterceptor.CreateProxyOfClass<Test>();

            Console.WriteLine(test.Sum(1, 1));

方法的參數支持 inrefout;支持泛型方法泛型屬性;支持異步方法;

非侵入式代理

此種方式不需要改動被代理的類型,你也可以代理程序集中的類型。

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }
    public class TestNo
    {
        public virtual string A { get; set; }
        public virtual void MyMethod()
        {
            Console.WriteLine("運行中");
        }
    }
            TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(new ProxyTypeBuilder()
                .AddProxyMethod(typeof(LogAttribute), typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                .AddProxyMethod(typeof(LogAttribute), typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));

通過 ProxyTypeBuilder 來構建代理類型。

代理方法或屬性都是使用 AddProxyMethod,第一個參數是要使用的攔截器,第二個參數是要攔截的方法。

如果要攔截屬性,請分開設置屬性的 getset 構造。

如果多個方法或屬性使用同一個攔截器,則可以這樣:

            TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(
                new ProxyTypeBuilder(new Type[] { typeof(LogAttribute) })
                .AddProxyMethod("LogAttribute", typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                .AddProxyMethod("LogAttribute", typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));
            TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(
                new ProxyTypeBuilder(new Type[] { typeof(LogAttribute) })
                .AddProxyMethod("LogAttribute", typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                .AddProxyMethod(typeof(LogAttribute2), typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));

在構造函數中傳遞過去所需要的攔截器,然後在攔截時使用。

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

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

聚甘新

分類
發燒車訊

【Flutter實戰】六大布局組件及半圓菜單案例

老孟導讀:Flutter中布局組件有水平 / 垂直布局組件( RowColumn )、疊加布局組件( StackIndexedStack )、流式布局組件( Wrap )和 自定義布局組件(Flow)。

水平、垂直布局組件

Row 是將子組件以水平方式布局的組件, Column 是將子組件以垂直方式布局的組件。項目中 90% 的頁面布局都可以通過 Row 和 Column 來實現。

將3個組件水平排列:

Row(
  children: <Widget>[
    Container(
      height: 50,
      width: 100,
      color: Colors.red,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.green,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.blue,
    ),
  ],
)

將3個組件垂直排列:

Column(
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Container(
      height: 50,
      width: 100,
      color: Colors.red,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.green,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.blue,
    ),
  ],
)

在 Row 和 Column 中有一個非常重要的概念:主軸( MainAxis )交叉軸( CrossAxis ),主軸就是與組件布局方向一致的軸,交叉軸就是與主軸方向垂直的軸。

具體到 Row 組件,主軸 是水平方向,交叉軸 是垂直方向。而 Column 與 Row 正好相反,主軸 是垂直方向,交叉軸 是水平方向。

明白了 主軸 和 交叉軸 概念,我們來看下 mainAxisAlignment 屬性,此屬性表示主軸方向的對齊方式,默認值為 start,表示從組件的開始處布局,此處的開始位置和 textDirection 屬性有關,textDirection 表示文本的布局方向,其值包括 ltr(從左到右) 和 rtl(從右到左),當 textDirection = ltr 時,start 表示左側,當 textDirection = rtl 時,start 表示右側,

Container(
  decoration: BoxDecoration(border: Border.all(color: Colors.black)),
  child: Row(
    children: <Widget>[
      Container(
        height: 50,
        width: 100,
        color: Colors.red,
      ),
      Container(
        height: 50,
        width: 100,
        color: Colors.green,
      ),
      Container(
        height: 50,
        width: 100,
        color: Colors.blue,
      ),
    ],
  ),
)

黑色邊框是Row控件的範圍,默認情況下Row鋪滿父組件。

主軸對齊方式有6種,效果如下圖:

spaceAround 和 spaceEvenly 區別是:

  • spaceAround :第一個子控件距開始位置和最後一個子控件距結尾位置是其他子控件間距的一半。
  • spaceEvenly : 所有間距一樣。

和主軸對齊方式相對應的就是交叉軸對齊方式 crossAxisAlignment ,交叉軸對齊方式默認是居中。Row控件的高度是依賴子控件高度,因此子控件高都一樣時,Row的高和子控件高相同,此時是無法體現交叉軸對齊方式,修改3個顏色塊高分別為50,100,150,這樣Row的高是150,代碼如下:

Container(
      decoration: BoxDecoration(border: Border.all(color: Colors.black)),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Container(
            height: 50,
            width: 100,
            color: Colors.red,
          ),
          Container(
            height: 100,
            width: 100,
            color: Colors.green,
          ),
          Container(
            height: 150,
            width: 100,
            color: Colors.blue,
          ),
        ],
      ),
    )

主軸對齊方式效果如下圖:

mainAxisSize 表示主軸尺寸,有 min 和 max 兩種方式,默認是 maxmin 表示盡可能小,max 表示盡可能大。

Container(
	decoration: BoxDecoration(border: Border.all(color: Colors.black)),
	child: Row(
		mainAxisSize: MainAxisSize.min,
		...
	)
)

看黑色邊框,正好包裹子組件,而 max 效果如下:

textDirection 表示子組件主軸布局方向,值包括 ltr(從左到右) 和 rtl(從右到左)

Container(
  decoration: BoxDecoration(border: Border.all(color: Colors.black)),
  child: Row(
    textDirection: TextDirection.rtl,
    children: <Widget>[
      ...
    ],
  ),
)

verticalDirection 表示子組件交叉軸布局方向:

  • up :從底部開始,並垂直堆疊到頂部,對齊方式的 start 在底部,end 在頂部。
  • down: 與 up 相反。
Container(
  decoration: BoxDecoration(border: Border.all(color: Colors.black)),
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    verticalDirection: VerticalDirection.up,
    children: <Widget>[
      Container(
        height: 50,
        width: 100,
        color: Colors.red,
      ),
      Container(
        height: 100,
        width: 100,
        color: Colors.green,
      ),
      Container(
        height: 150,
        width: 100,
        color: Colors.blue,
      ),
    ],
  ),
)

想一想這種效果完全可以通過對齊方式實現,那麼為什麼還要有 textDirectionverticalDirection 這兩個屬性,官方API文檔已經解釋了這個問題:

This is also used to disambiguate start and end values (e.g. [MainAxisAlignment.start] or [CrossAxisAlignment.end]).

用於消除 MainAxisAlignment.start 和 CrossAxisAlignment.end 值的歧義的。

疊加布局組件

疊加布局組件包含 StackIndexedStack,Stack 組件將子組件疊加显示,根據子組件的順利依次向上疊加,用法如下:

Stack(
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Container(
      height: 140,
      width: 140,
      color: Colors.yellow,
    )
  ],
)

Stack 對未定位(不被 Positioned 包裹)子組件的大小由 fit 參數決定,默認值是 StackFit.loose ,表示子組件自己決定,StackFit.expand 表示盡可能的大,用法如下:

Stack(
  fit: StackFit.expand,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Container(
      height: 140,
      width: 140,
      color: Colors.yellow,
    )
  ],
)

效果只有黃色(最後一個組件的顏色),並不是其他組件沒有繪製,而是另外兩個組件被黃色組件覆蓋。

Stack 對未定位(不被 Positioned 包裹)子組件的對齊方式由 alignment 控制,默認左上角對齊,用法如下:

Stack(
  alignment: AlignmentDirectional.center,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Container(
      height: 140,
      width: 140,
      color: Colors.yellow,
    )
  ],
)

通過 Positioned 定位的子組件:

Stack(
  alignment: AlignmentDirectional.center,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Positioned(
      left: 30,
      right: 40,
      bottom: 50,
      top: 60,
      child: Container(
        color: Colors.yellow,
      ),
    )
  ],
)

topbottomleftright 四種定位屬性,分別表示距離上下左右的距離。

如果子組件超過 Stack 邊界由 overflow 控制,默認是裁剪,下面設置總是显示的用法:

Stack(
  overflow: Overflow.visible,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Positioned(
      left: 100,
      top: 100,
      height: 150,
      width: 150,
      child: Container(
        color: Colors.green,
      ),
    )
  ],
)

IndexedStack 是 Stack 的子類,Stack 是將所有的子組件疊加显示,而 IndexedStack 通過 index 只显示指定索引的子組件,用法如下:

class IndexedStackDemo extends StatefulWidget {
  @override
  _IndexedStackDemoState createState() => _IndexedStackDemoState();
}

class _IndexedStackDemoState extends State<IndexedStackDemo> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        SizedBox(height: 50,),
        _buildIndexedStack(),
        SizedBox(height: 30,),
        _buildRow(),
      ],
    );
  }

  _buildIndexedStack() {
    return IndexedStack(
      index: _index,
      children: <Widget>[
        Center(
          child: Container(
            height: 300,
            width: 300,
            color: Colors.red,
            alignment: Alignment.center,
            child: Icon(
              Icons.fastfood,
              size: 60,
              color: Colors.blue,
            ),
          ),
        ),
        Center(
          child: Container(
            height: 300,
            width: 300,
            color: Colors.green,
            alignment: Alignment.center,
            child: Icon(
              Icons.cake,
              size: 60,
              color: Colors.blue,
            ),
          ),
        ),
        Center(
          child: Container(
            height: 300,
            width: 300,
            color: Colors.yellow,
            alignment: Alignment.center,
            child: Icon(
              Icons.local_cafe,
              size: 60,
              color: Colors.blue,
            ),
          ),
        ),
      ],
    );
  }

  _buildRow() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        IconButton(
          icon: Icon(Icons.fastfood),
          onPressed: () {
            setState(() {
              _index = 0;
            });
          },
        ),
        IconButton(
          icon: Icon(Icons.cake),
          onPressed: () {
            setState(() {
              _index = 1;
            });
          },
        ),
        IconButton(
          icon: Icon(Icons.local_cafe),
          onPressed: () {
            setState(() {
              _index = 2;
            });
          },
        ),
      ],
    );
  }
}

流式布局組件

Wrap 為子組件進行水平或者垂直方向布局,且當空間用完時,Wrap 會自動換行,也就是流式布局。

創建多個子控件做為 Wrap 的子控件,代碼如下:

Wrap(
  children: List.generate(10, (i) {
    double w = 50.0 + 10 * i;
    return Container(
      color: Colors.primaries[i],
      height: 50,
      width: w,
      child: Text('$i'),
    );
  }),
)

direction 屬性控制布局方向,默認為水平方向,設置方向為垂直代碼如下:

Wrap(
  direction: Axis.vertical,
  children: List.generate(4, (i) {
    double w = 50.0 + 10 * i;
    return Container(
      color: Colors.primaries[i],
      height: 50,
      width: w,
      child: Text('$i'),
    );
  }),
)

alignment 屬性控制主軸對齊方式,crossAxisAlignment 屬性控制交叉軸對齊方式,對齊方式只對有剩餘空間的行或者列起作用,例如水平方向上正好填充完整,則不管設置主軸對齊方式為什麼,看上去的效果都是鋪滿。

說明 :主軸就是與當前組件方向一致的軸,而交叉軸就是與當前組件方向垂直的軸,如果Wrap的布局方向為水平方向 Axis.horizontal,那麼主軸就是水平方向,反之布局方向為垂直方向 Axis.vertical ,主軸就是垂直方向。

Wrap(
	alignment: WrapAlignment.spaceBetween,
	...
)

主軸對齊方式有6種,效果如下圖:

spaceAroundspaceEvenly 區別是:

  • spaceAround:第一個子控件距開始位置和最後一個子控件距結尾位置是其他子控件間距的一半。
  • spaceEvenly:所有間距一樣。

設置交叉軸對齊代碼如下:

Wrap(
	crossAxisAlignment: WrapCrossAlignment.center,
	...
)

如果 Wrap 的主軸方向為水平方向,交叉軸方向則為垂直方向,如果想要看到交叉軸對齊方式的效果需要設置子控件的高不一樣,代碼如下:

Wrap(
  spacing: 5,
  runSpacing: 3,
  crossAxisAlignment: WrapCrossAlignment.center,
  children: List.generate(10, (i) {
    double w = 50.0 + 10 * i;
    double h = 50.0 + 5 * i;
    return Container(
      color: Colors.primaries[i],
      height: h,
      alignment: Alignment.center,
      width: w,
      child: Text('$i'),
    );
  }),
)

runAlignment 屬性控制 Wrap 的交叉抽方向上每一行的對齊方式,下面直接看 runAlignment 6中方式對應的效果圖,

runAlignmentalignment 的區別:

  • alignment :是主軸方向上對齊方式,作用於每一行。
  • runAlignment :是交叉軸方向上將每一行看作一個整體的對齊方式。

spacingrunSpacing 屬性控制Wrap主軸方向和交叉軸方向子控件之間的間隙,代碼如下:

Wrap(
	spacing: 5,
    runSpacing: 2,
	...
)

textDirection 屬性表示 Wrap 主軸方向上子組件的方向,取值範圍是 ltr(從左到右) 和 rtl(從右到左),下面是從右到左的代碼:

Wrap(
	textDirection: TextDirection.rtl,
	...
)

verticalDirection 屬性表示 Wrap 交叉軸方向上子組件的方向,取值範圍是 up(向上) 和 down(向下),設置代碼如下:

Wrap(
	verticalDirection: VerticalDirection.up,
	...
)

注意:文字為0的組件是在下面的。

自定義布局組件

大部分情況下,不會使用到 Flow ,但 Flow 可以調整子組件的位置和大小,結合Matrix4繪製出各種酷炫的效果。

Flow 組件對使用轉換矩陣操作子組件經過系統優化,性能非常高效。

基本用法如下:

Flow(
  delegate: SimpleFlowDelegate(),
  children: List.generate(5, (index) {
    return Container(
      height: 100,
      color: Colors.primaries[index % Colors.primaries.length],
    );
  }),
)

delegate 控制子組件的位置和大小,定義如下 :

class SimpleFlowDelegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    for (int i = 0; i < context.childCount; ++i) {
      context.paintChild(i);
    }
  }

  @override
  bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
    return false;
  }
}

delegate 要繼承 FlowDelegate,重寫 paintChildrenshouldRepaint 函數,上面直接繪製子組件,效果如下:

只看到一種顏色並不是只繪製了這一個,而是疊加覆蓋了,和 Stack 類似,下面讓每一個組件有一定的偏移,SimpleFlowDelegate 修改如下:

class SimpleFlowDelegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    for (int i = 0; i < context.childCount; ++i) {
      context.paintChild(i,transform: Matrix4.translationValues(0,i*30.0,0));
    }
  }

  @override
  bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
    return false;
  }
}

每一個子組件比上一個組件向下偏移30。

仿 掘金-我的效果

效果如下:

到拿到一個頁面時,先要將其拆分,上面的效果拆分如下:

總體分為3個部分,水平布局,紅色區域圓形頭像代碼如下:

_buildCircleImg() {
  return Container(
    height: 60,
    width: 60,
    decoration: BoxDecoration(
        shape: BoxShape.circle,
        image: DecorationImage(image: AssetImage('assets/images/logo.png'))),
  );
}

藍色區域代碼如下:

_buildCenter() {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      Text('老孟Flutter', style: TextStyle(fontSize: 20),),
      Text('Flutter、Android', style: TextStyle(color: Colors.grey),)
    ],
  );
}

綠色區域是一個圖標,代碼如下:

Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),

將這3部分組合在一起:

Container(
  color: Colors.grey.withOpacity(.5),
  alignment: Alignment.center,
  child: Container(
    height: 100,
    color: Colors.white,
    child: Row(
      children: <Widget>[
        SizedBox(
          width: 15,
        ),
        _buildCircleImg(),
        SizedBox(
          width: 25,
        ),
        Expanded(
          child: _buildCenter(),
        ),
        Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),
        SizedBox(
          width: 15,
        ),
      ],
    ),
  ),
)

最終的效果就是開始我們看到的效果圖。

水平展開/收起菜單

使用Flow實現水平展開/收起菜單的功能,代碼如下:

class DemoFlowPopMenu extends StatefulWidget {
  @override
  _DemoFlowPopMenuState createState() => _DemoFlowPopMenuState();
}

class _DemoFlowPopMenuState extends State<DemoFlowPopMenu>
    with SingleTickerProviderStateMixin {
  //動畫必須要with這個類
  AnimationController _ctrlAnimationPopMenu; //定義動畫的變量
  IconData lastTapped = Icons.notifications;
  final List<IconData> menuItems = <IconData>[
    //菜單的icon
    Icons.home,
    Icons.new_releases,
    Icons.notifications,
    Icons.settings,
    Icons.menu,
  ];

  void _updateMenu(IconData icon) {
    if (icon != Icons.menu) {
      setState(() => lastTapped = icon);
    } else {
      _ctrlAnimationPopMenu.status == AnimationStatus.completed
          ? _ctrlAnimationPopMenu.reverse() //展開和收攏的效果
          : _ctrlAnimationPopMenu.forward();
    }
  }

  @override
  void initState() {
    super.initState();
    _ctrlAnimationPopMenu = AnimationController(
      //必須初始化動畫變量
      duration: const Duration(milliseconds: 250), //動畫時長250毫秒
      vsync: this, //SingleTickerProviderStateMixin的作用
    );
  }

//生成Popmenu數據
  Widget flowMenuItem(IconData icon) {
    final double buttonDiameter =
        MediaQuery.of(context).size.width * 2 / (menuItems.length * 3);
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: RawMaterialButton(
        fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
        splashColor: Colors.amber[100],
        shape: CircleBorder(),
        constraints: BoxConstraints.tight(Size(buttonDiameter, buttonDiameter)),
        onPressed: () {
          _updateMenu(icon);
        },
        child: Icon(icon, color: Colors.white, size: 30.0),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Flow(
        delegate: FlowMenuDelegate(animation: _ctrlAnimationPopMenu),
        children: menuItems
            .map<Widget>((IconData icon) => flowMenuItem(icon))
            .toList(),
      ),
    );
  }
}

FlowMenuDelegate 定義如下:

class FlowMenuDelegate extends FlowDelegate {
  FlowMenuDelegate({this.animation}) : super(repaint: animation);
  final Animation<double> animation;

  @override
  void paintChildren(FlowPaintingContext context) {
    double x = 50.0; //起始位置
    double y = 50.0; //橫向展開,y不變
    for (int i = 0; i < context.childCount; ++i) {
      x = context.getChildSize(i).width * i * animation.value;
      context.paintChild(
        i,
        transform: Matrix4.translationValues(x, y, 0),
      );
    }
  }

  @override
  bool shouldRepaint(FlowMenuDelegate oldDelegate) =>
      animation != oldDelegate.animation;
}

半圓菜單展開/收起

代碼如下:

import 'dart:math';

import 'package:flutter/material.dart';

class DemoFlowMenu extends StatefulWidget {
  @override
  _DemoFlowMenuState createState() => _DemoFlowMenuState();
}

class _DemoFlowMenuState extends State<DemoFlowMenu>
    with TickerProviderStateMixin {
  //動畫需要這個類來混合
  //動畫變量,以及初始化和銷毀
  AnimationController _ctrlAnimationCircle;

  @override
  void initState() {
    super.initState();
    _ctrlAnimationCircle = AnimationController(
        //初始化動畫變量
        lowerBound: 0,
        upperBound: 80,
        duration: Duration(milliseconds: 300),
        vsync: this);
    _ctrlAnimationCircle.addListener(() => setState(() {}));
  }

  @override
  void dispose() {
    _ctrlAnimationCircle.dispose(); //銷毀變量,釋放資源
    super.dispose();
  }

  //生成Flow的數據
  List<Widget> _buildFlowChildren() {
    return List.generate(
        5,
        (index) => Container(
              child: Icon(
                index.isEven ? Icons.timer : Icons.ac_unit,
                color: Colors.primaries[index % Colors.primaries.length],
              ),
            ));
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned.fill(
          child: Flow(
            delegate: FlowAnimatedCircle(_ctrlAnimationCircle.value),
            children: _buildFlowChildren(),
          ),
        ),
        Positioned.fill(
          child: IconButton(
            icon: Icon(Icons.menu),
            onPressed: () {
              setState(() {
                //點擊后讓動畫可前行或回退
                _ctrlAnimationCircle.status == AnimationStatus.completed
                    ? _ctrlAnimationCircle.reverse()
                    : _ctrlAnimationCircle.forward();
              });
            },
          ),
        ),
      ],
    );
  }
}

FlowAnimatedCircle 代碼如下:

class FlowAnimatedCircle extends FlowDelegate {
  final double radius; //綁定半徑,讓圓動起來
  FlowAnimatedCircle(this.radius);

  @override
  void paintChildren(FlowPaintingContext context) {
    if (radius == 0) {
      return;
    }
    double x = 0; //開始(0,0)在父組件的中心
    double y = 0;
    for (int i = 0; i < context.childCount; i++) {
      x = radius * cos(i * pi / (context.childCount - 1)); //根據數學得出坐標
      y = radius * sin(i * pi / (context.childCount - 1)); //根據數學得出坐標
      context.paintChild(i, transform: Matrix4.translationValues(x, -y, 0));
    } //使用Matrix定位每個子組件
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) => true;
}

交流

老孟Flutter博客地址(330個控件用法):http://laomengit.com

歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

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

聚甘新

分類
發燒車訊

德國小城弗萊堡 交通轉型有成 35年私人汽車減半

環境資訊中心特約記者 陳文姿報導

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

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

聚甘新