SpringBoot Application深入學習

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

主要包括:

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

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

SpringBoot應用啟動過程:

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

一、Application類自定義啟動配置

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

舉例:

@SpringBootApplication
public class DemoApplication {

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

}

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

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

二、application生命周期

SpringApplication的生命周期主要包括:

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

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

SpringBootApplication構造器:

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

SpringApplication.run方法源碼:

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

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

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

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

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

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

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

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

3.1 自定義ApplicationContextInitializer的實現類

@Order(100)
public class MyInitializer implements ApplicationContextInitializer {

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

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

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

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

啟動項目:

3.2 自定義ApplicationListener的實現類

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

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

首先查看有多少種ApplicationEvents:

裏面還可以進行拆分。

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

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

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

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

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

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

3.3 自定義SpringBootRunListener的實現類

監聽整個SpringBoot應用生命周期

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

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

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

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

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

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

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

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

比如:

public class MyRunListener implements SpringApplicationRunListener {

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

    }

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

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

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

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

啟動:

3.4 自定義ApplicationRunner或CommandLineRunner

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

this.callRunners(context, applicationArguments);

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

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

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

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

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

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

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

具體示例代碼請去查看。

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

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

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

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

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

你必須知道的容器日誌 (2) 開源日誌管理方案 ELK/EFK

本篇已加入《》,可以點擊查看更多容器化技術相關係列文章。上一篇《》中介紹了Docker自帶的logs子命令以及其Logging driver,本篇將會介紹一個流行的開源日誌管理方案ELK。

一、關於ELK

1.1 ELK簡介

  ELK 是Elastic公司提供的一套完整的日誌收集以及展示的解決方案,是三個產品的首字母縮寫,分別是ElasticSearchLogstashKibana

  • Elasticsearch是實時全文搜索和分析引擎,提供搜集、分析、存儲數據三大功能
  • Logstash是一個用來搜集、分析、過濾日誌的工具
  • Kibana是一個基於Web的圖形界面,用於搜索、分析和可視化存儲在 Elasticsearch指標中的日誌數據   

1.2 ELK日誌處理流程

   上圖展示了在Docker環境下,一個典型的ELK方案下的日誌收集處理流程:

  • Logstash從各個Docker容器中提取日誌信息
  • Logstash將日誌轉發到ElasticSearch進行索引和保存
  • Kibana負責分析和可視化日誌信息

  由於Logstash在數據收集上並不出色,而且作為Agent,其性能並不達標。基於此,Elastic發布了beats系列輕量級採集組件。

  這裏我們要實踐的Beat組件是Filebeat,Filebeat是構建於beats之上的,應用於日誌收集場景的實現,用來替代 Logstash Forwarder 的下一代 Logstash 收集器,是為了更快速穩定輕量低耗地進行收集工作,它可以很方便地與 Logstash 還有直接與 Elasticsearch 進行對接。

  本次實驗直接使用Filebeat作為Agent,它會收集我們在第一篇《》中介紹的json-file的log文件中的記錄變動,並直接將日誌發給ElasticSearch進行索引和保存,其處理流程變為下圖,你也可以認為它可以稱作 EFK。

二、ELK套件的安裝

  本次實驗我們採用Docker方式部署一個最小規模的ELK運行環境,當然,實際環境中我們或許需要考慮高可用和負載均衡。

  首先拉取一下sebp/elk這個集成鏡像,這裏選擇的tag版本是640(最新版本已經是7XX了):

docker pull sebp/elk:640

  注:由於其包含了整個ELK方案,所以需要耐心等待一會。

  通過以下命令使用sebp/elk這個集成鏡像啟動運行ELK:

docker run -it -d --name elk \
    -p 5601:5601 \
    -p 9200:9200 \
    -p 5044:5044 \
    sebp/elk:640

  運行完成之後就可以先訪問一下 http://[Your-HostIP]:5601 看看Kibana的效果:  

  Kibana管理界面

Kibana Index Patterns界面

  當然,目前沒有任何可以显示的ES的索引和數據,再訪問一下http://[Your-HostIP]:9200 看看ElasticSearch的API接口是否可用:

ElasticSearch API

  Note:如果啟動過程中發現一些錯誤,導致ELK容器無法啟動,可以參考《》及《》一文。如果你的主機內存低於4G,建議增加配置設置ES內存使用大小,以免啟動不了。例如下面增加的配置,限制ES內存使用最大為1G:

docker run -it -d --name elk \
    -p 5601:5601 \
    -p 9200:9200 \
    -p 5044:5044 \
  -e ES_MIN_MEM=512m \ -e ES_MAX_MEM=1024m \ sebp/elk:640

三、Filebeat配置

3.1 安裝Filebeat

  這裏我們通過rpm的方式下載Filebeat,注意這裏下載和我們ELK對應的版本(ELK是6.4.0,這裏也是下載6.4.0,避免出現錯誤):

wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.4.0-x86_64.rpm
rpm -ivh filebeat-6.4.0-x86_64.rpm

3.2 配置Filebeat  

   這裏我們需要告訴Filebeat要監控哪些日誌文件 及 將日誌發送到哪裡去,因此我們需要修改一下Filebeat的配置:

cd /etc/filebeat
vim filebeat.yml

  要修改的內容為:

  (1)監控哪些日誌?

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /var/lib/docker/containers/*/*.log - /var/log/syslog

  這裏指定paths:/var/lib/docker/containers/*/*.log,另外需要注意的是將 enabled 設為 true。

  (2)將日誌發到哪裡?

#-------------------------- Elasticsearch output ------------------------------
output.elasticsearch:
  # Array of hosts to connect to.
  hosts: ["192.168.16.190:9200"]

  # Optional protocol and basic auth credentials.
  #protocol: "https"
  #username: "elastic"
  #password: "changeme"

  這裏指定直接發送到ElasticSearch,配置一下ES的接口地址即可。

  Note:如果要發到Logstash,請使用後面這段配置,將其取消註釋進行相關配置即可:

#----------------------------- Logstash output --------------------------------
#output.logstash:
  # The Logstash hosts
  #hosts: ["localhost:5044"]

  # Optional SSL. By default is off.
  # List of root certificates for HTTPS server verifications
  #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"]

  # Certificate for SSL client authentication
  #ssl.certificate: "/etc/pki/client/cert.pem"

  # Client Certificate Key
  #ssl.key: "/etc/pki/client/cert.key"

3.3 啟動Filebeat

  由於Filebeat在安裝時已經註冊為systemd的服務,所以只需要直接啟動即可:

systemctl start filebeat.service

  檢查Filebeat啟動狀態:

systemctl status filebeat.service

3.4 驗證Filebeat

  通過訪問ElasticSearch API可以發現以下變化:ES建立了以filebeat-開頭的索引,我們還能夠看到其來源及具體的message。

四、Kibana配置

  接下來我們就要告訴Kibana,要查詢和分析ElasticSearch中的哪些日誌,因此需要配置一個Index Pattern。從Filebeat中我們知道Index是filebeat-timestamp這種格式,因此這裏我們定義Index Pattern為 filebeat-*

  點擊Next Step,這裏我們選擇Time Filter field name為@timestamp:

  單擊Create index pattern按鈕,即可完成配置。

  這時我們單擊Kibana左側的Discover菜單,即可看到容器的日誌信息啦:

  仔細看看細節,我們關注一下message字段:

  可以看到,我們重點要關注的是message,因此我們也可以篩選一下只看這個字段的信息:

  此外,Kibana還提供了搜索關鍵詞的日誌功能,例如這裏我關注一下日誌中包含unhandled exception(未處理異常)的日誌信息:

  這裏只是樸素的展示了導入ELK的日誌信息,實際上ELK還有很多很豐富的玩法,例如分析聚合、炫酷Dashboard等等。筆者在這裏也是初步使用,就介紹到這裏啦。

五、Fluentd引入

5.1 關於Fluentd

  前面我們採用的是Filebeat收集Docker的日誌信息,基於Docker默認的json-file這個logging driver,這裏我們改用Fluentd這個開源項目來替換json-file收集容器的日誌。

  Fluentd是一個開源的數據收集器,專為處理數據流設計,使用JSON作為數據格式。它採用了插件式的架構,具有高可擴展性高可用性,同時還實現了高可靠的信息轉發。Fluentd也是雲原生基金會 (CNCF) 的成員項目之一,遵循Apache 2 License協議,其github地址為:。Fluentd與Logstash相比,比佔用內存更少、社區更活躍,兩者的對比可以參考這篇文章《》。

  因此,整個日誌收集與處理流程變為下圖,我們用 Filebeat 將 Fluentd 收集到的日誌轉發給 Elasticsearch。

   當然,我們也可以使用Fluentd的插件(fluent-plugin-elasticsearch)直接將日誌發送給 Elasticsearch,可以根據自己的需要替換掉Filebeat,從而形成Fluentd => ElasticSearch => Kibana 的架構,也稱作EFK。

5.2 運行Fluentd

  這裏我們通過容器來運行一個Fluentd採集器:

docker run -d -p 24224:24224 -p 24224:24224/udp -v /edc/fluentd/log:/fluentd/log fluent/fluentd

  默認Fluentd會使用24224端口,其日誌會收集在我們映射的路徑下。

  此外,我們還需要修改Filebeat的配置文件,將/edc/fluentd/log加入監控目錄下:

#=========================== Filebeat inputs =============================

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /edc/fluentd/log/*.log

  添加監控配置之後,需要重新restart一下filebeat:

systemctl restart filebeat

5.3 運行測試容器

  為了驗證效果,這裏我們Run兩個容器,並分別制定其log-dirver為fluentd:

docker run -d \
           --log-driver=fluentd \
           --log-opt fluentd-address=localhost:24224 \
           --log-opt tag="test-docker-A" \
           busybox sh -c 'while true; do echo "This is a log message from container A"; sleep 10; done;'

docker run -d \
           --log-driver=fluentd \
           --log-opt fluentd-address=localhost:24224 \
           --log-opt tag="test-docker-B" \
           busybox sh -c 'while true; do echo "This is a log message from container B"; sleep 10; done;'

  這裏通過指定容器的log-driver,以及為每個容器設立了tag,方便我們後面驗證查看日誌。

5.4 驗證EFK效果

  這時再次進入Kibana中查看日誌信息,便可以通過剛剛設置的tag信息篩選到剛剛添加的容器的日誌信息了:

六、小結

  本文從ELK的基本組成入手,介紹了ELK的基本處理流程,以及從0開始搭建了一個ELK環境,演示了基於Filebeat收集容器日誌信息的案例。然後,通過引入Fluentd這個開源數據收集器,演示了如何基於EFK的日誌收集案例。當然,ELK/EFK有很多的知識點,筆者也還只是初步使用,希望未來能夠分享更多的實踐總結。

參考資料

CloudMan,《》

一杯甜酒,《》

於老三,《》

zpei0411,《》

曹林華,《》

 

作者:

出處:

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

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

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Lombok 使用詳解,簡化Java編程

前言

在 Java 應用程序中存在許多重複相似的、生成之後幾乎不對其做更改的代碼,但是我們還不得不花費很多精力編寫它們來滿足 Java 的編譯需求

比如,在 Java 應用程序開發中,我們幾乎要為所有 Bean 的成員變量添加 get() ,set() 等方法,這些相對固定但又不得不編寫的代碼浪費程序員很多精力,同時讓類內容看着更雜亂,我們希望將有限的精力關注在更重要的地方。

Lombok 已經誕生很久了,甚至在 Spring Boot Initalizr 中都已加入了 Lombok 選項,

這裏我們將 Lombok 做一下詳細說明:

Lombok

官網的介紹:Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or equals method again. Early access to future java features such as val, and much more.

直白的說: Lombok 是一種 Java™ 實用工具,可用來幫助開發人員消除 Java 的冗長,尤其是對於簡單的 Java 對象(POJO)。它通過註解實現這一目的,且看:

Bean 的對比

傳統的 POJO 類是這樣的

通過Lombok改造后的 POJO 類是這樣的

一眼可以觀察出來我們在編寫 Employee 這個類的時候通過 @Data 註解就已經實現了所有成員變量的 get()set() 方法等,同時 Employee 類看起來更加清晰簡潔。Lombok 的神奇之處不止這些,豐富的註解滿足了我們開發的多數需求。

Lombok的安裝

查看下圖,@Data的實現,我們發現這個註解是應用在編譯階段的

這和我們大多數使用的註解,如 Spring 的註解(在運行時,通過反射來實現業務邏輯)是有很大差別的,如Spring 的@RestController 註解

一個更直接的體現就是,普通的包在引用之後一般的 IDE 都能夠自動識別語法,但是 Lombok 的這些註解,一般的 IDE 都無法自動識別,因此如果要使用 Lombok 的話還需要配合安裝相應的插件來支持 IDE 的編譯,防止IDE 的自動檢查報錯,下面以 IntelliJ IDEA 舉例安裝插件。

在Repositories中搜索Lombok,安裝后重啟IDE即可

在Maven或Gradle工程中添加依賴

至此我們就可以應用 Lombok 提供的註解幹些事情了。

Lombok註解詳解

Lombok官網提供了許多註解,但是 “勁酒雖好,可不要貪杯哦”,接下來逐一講解官網推薦使用的註解(有些註解和原有Java編寫方式沒太大差別的也沒有在此處列舉,如@ Synchronized等)

@Getter和@Setter

該註解可應用在類或成員變量之上,和我們預想的一樣,@Getter@Setter 就是為成員變量自動生成 get 和 set 方法,默認生成訪問權限為 public 方法,當然我們也可以指定訪問權限 protected 等,如下圖:

成員變量name指定生成set方法,並且訪問權限為protected;boolean類型的成員變量 female 只生成get方法,並修改方法名稱為 isFemale()。當把該註解應用在類上,默認為所有非靜態成員變量生成 get 和 set 方法,也可以通過 AccessLevel.NONE 手動禁止生成get或set方法,如下圖:

@ToString

該註解需應用在類上,為我們生成 Object 的 toString 方法,而該註解裏面的幾個屬性能更加豐富我們想要的內容, exclude 屬性禁止在 toString 方法中使用某字段,而of屬性可以指定需要使用的字段,如下圖:

查看編譯后的Employee.class得到我們預期的結果,如下圖

@EqualsAndHashCode

該註解需應用在類上,使用該註解,lombok會為我們生成 equals(Object other) 和 hashcode() 方法,包括所有非靜態屬性和非transient的屬性,同樣該註解也可以通過 exclude 屬性排除某些字段,of 屬性指定某些字段,也可以通過 callSuper 屬性在重寫的方法中使用父類的字段,這樣我們可以更靈活的定義bean的比對,如下圖:

查看編譯后的Employee.class文件,如下圖:

@NonNull

該註解需應用在方法或構造器的參數上或屬性上,用來判斷參數的合法性,默認拋出 NullPointerException 異常

查看NonNullExample.class文件,會為我們拋出空指針異常,如下圖:

當然我們可以通過指定異常類型拋出其他異常,lombok.nonNull.exceptionType = [NullPointerException | IllegalArgumentException] , 為實現此功能我們需要在項目的根目錄新建lombok.config文件:

重新編譯NonNullExample類,已經為我們拋出非法參數異常:

@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

以上三個註解分別為我們生成無參構造器,指定參數構造器和包含所有參數的構造器,默認情況下,@RequiredArgsConstructor, @AllArgsConstructor 生成的構造器會對所有標記 @NonNull 的屬性做非空校驗。

無參構造器很好理解,我們主要看看后兩種,先看 @RequiredArgsConstructor

從上圖中我們可以看出, @RequiredArgsConstructor 註解生成有參數構造器時只會包含有 final 和 @NonNull 標識的 field,同時我們可以指定 staticName 通過生成靜態方法來構造對象

查看Employee.class文件

當我們把 staticName 屬性去掉我們來看遍以後的文件:

相信你已經注意到細節

@AllArgsConstructor 就更簡單了,請大家自行查看吧

@Data

介紹了以上的註解,再來介紹 @Data 就非常容易懂了,@Data 註解應用在類上,是@ToString, @EqualsAndHashCode, @Getter / @Setter@RequiredArgsConstructor合力的體現,如下圖:

@Builder

函數式編程或者說流式的操作越來越流行,應用在大多數語言中,讓程序更具更簡介,可讀性更高,編寫更連貫,@Builder就帶來了這個功能,生成一系列的builder API,該註解也需要應用在類上,看下面的例子就會更加清晰明了。

編譯后的Employee.class文件如下:

媽媽再也不用擔心我 set 值那麼麻煩了,流式操作搞定:

@Log

該註解需要應用到類上,在編寫服務層,需要添加一些日誌,以便定位問題,我們通常會定義一個靜態常量Logger,然後應用到我們想日誌的地方,現在一個註解就可以實現:

查看class文件,和我們預想的一樣:

Log有很多變種,CommonLog,Log4j,Log4j2,Slf4j等,lombok依舊良好的通過變種註解做良好的支持:

我實際使用的是 @Slf4j 註解

val

熟悉 Javascript 的同學都知道,var 可以定義任何類型的變量,而在 java 的實現中我們需要指定具體變量的類型,而 val 讓我們擺脫指定,編譯之後就精準匹配上類型,默認是 final 類型,就像 java8 的函數式表達式,()->System.out.println(“hello lombok”); 就可以解析到Runnable函數式接口。

查看解析后的class文件:

@Cleanup

當我們對流進行操作,我們通常需要調用 close 方法來關閉或結束某資源,而 @Cleanup 註解可以幫助我們調用 close 方法,並且放到 try/finally 處理塊中,如下圖:

編譯后的class文件如下,我們發現被try/finally包圍處理,並調用了流的close方法

其實在 JDK1.7 之後就有了 try-with-resource,不用我們顯式的關閉流,這個請大家自行看吧

總結

Lombok的基本操作流程是這樣的:

  1. 定義編譯期的註解
  2. 利用JSR269 api(Pluggable Annotation Processing API )創建編譯期的註解處理器
  3. 利用tools.jar的javac api處理AST(抽象語法樹)
  4. 將功能註冊進jar包

Lombok 當然還有很多註解,我推薦使用以上就足夠了,這個工具是帶來便利的,而不能被其捆綁,“弱水三千隻取一瓢飲,代碼千萬需抓重點看”,Lombok 能讓我更加專註有效代碼排除意義微小的障眼代碼(get,set等),另外Lombok生成的代碼還能像使用工具類一樣方便(@Builder)。

更多內容請查看官網:https://www.projectlombok.org/

靈魂追問

  1. 為什麼只有一個整體 @EqualsAndHashCode 註解?而不是 @Equals@HashCode?這涉及到一個規範哦
  2. 如果把三種構造器方式同時應用又加上了 @Builder 註解,會發生什麼?
  3. 你的燈還亮着嗎?

歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具匯總 | 回復「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回復「資料」

以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注……

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

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

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

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

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

前言

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

祝大家閱讀愉快!

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

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

  • 倉庫地址:

一、View 的事件回調

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

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

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

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

}

1.1 事件分發流程

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

1.1.1 onTouchEvent 作用

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

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

    private final String TAG = "DeBugMainActivity";

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

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

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

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

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

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

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

1.1.2 dispatchTouchEvent 的實現

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

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

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

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

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

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

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

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

1.1.3 onTouchEvent 的處理

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

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

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

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

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

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

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

            case MotionEvent.ACTION_CANCEL:
                。。。
                break;

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

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

        return true;
    }

    return false;
}

二、onClick 和 OnLongClick

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

2.1 onClick 和 OnLongClick 的產生

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

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

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

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

2.2 ACTION_DOWN 之後流程

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

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

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

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

2.4 總結

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

總結

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

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

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

  • 倉庫地址:

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

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

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

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

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

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

.NET core3.0 使用Jwt保護api

摘要:

本文演示如何向有效用戶提供jwt,以及如何在webapi中使用該token通過JwtBearerMiddleware中間件對用戶進行身份認證。

認證和授權區別?

首先我們要弄清楚認證(Authentication)和授權(Authorization)的區別,以免混淆了。認證是確認的過程中你是誰,而授權圍繞是你被允許做什麼,即權限。顯然,在確認允許用戶做什麼之前,你需要知道他們是誰,因此,在需要授權時,還必須以某種方式對用戶進行身份驗證。 

什麼是JWT?

根據維基百科的定義,JSON WEB Token(JWT),是一種基於JSON的、用於在網絡上聲明某種主張的令牌(token)。JWT通常由三部分組成:頭信息(header),消息體(payload)和簽名(signature)。

頭信息指定了該JWT使用的簽名算法:

header = '{"alg":"HS256","typ":"JWT"}'

HS256表示使用了HMAC-SHA256來生成簽名。

消息體包含了JWT的意圖:

payload = '{"loggedInAs":"admin","iat":1422779638}'//iat表示令牌生成的時間

未簽名的令牌由base64url編碼的頭信息和消息體拼接而成(使用”.”分隔),簽名則通過私有的key計算而成:

key = 'secretkey'  
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)  
signature = HMAC-SHA256(key, unsignedToken)

最後在未簽名的令牌尾部拼接上base64url編碼的簽名(同樣使用”.”分隔)就是JWT了:

token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)

# token看起來像這樣: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

JWT常常被用作保護服務端的資源(resource),客戶端通常將JWT通過HTTP的Authorization header發送給服務端,服務端使用自己保存的key計算、驗證簽名以判斷該JWT是否可信:

Authorization: Bearer eyJhbGci*...<snip>...*yu5CSpyHI

準備工作

使用vs2019創建webapi項目,並且安裝nuget包

Microsoft.AspNetCore.Authentication.JwtBearer

Startup類
  • ConfigureServices 添加認證服務

services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = "https://www.cnblogs.com/chengtian",
                    ValidIssuer = "https://www.cnblogs.com/chengtian",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SecureKeySecureKeySecureKeySecureKeySecureKeySecureKey"))
                };
            });
  • Configure 配置認證中間件

 app.UseAuthentication();//認證中間件

創建一個token

  • 添加一個登錄model命名為LoginInput

public class LoginInput
    {

        public string Username { get; set; }

        public string Password { get; set; }
    }
  • 添加一個認證控制器命名為AuthenticateController

[Route("api/[controller]")]
    public class AuthenticateController : Controller
    {
        [HttpPost]
        [Route("login")]
        public IActionResult Login([FromBody]LoginInput input)
        {
            //從數據庫驗證用戶名,密碼 
            //驗證通過 否則 返回Unauthorized

            //創建claim
            var authClaims = new[] {
                new Claim(JwtRegisteredClaimNames.Sub,input.Username),
                new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
            };
            IdentityModelEventSource.ShowPII = true;
            //簽名秘鑰 可以放到json文件中
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SecureKeySecureKeySecureKeySecureKeySecureKeySecureKey"));

            var token = new JwtSecurityToken(
                   issuer: "https://www.cnblogs.com/chengtian",
                   audience: "https://www.cnblogs.com/chengtian",
                   expires: DateTime.Now.AddHours(2),
                   claims: authClaims,
                   signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                   );

            //返回token和過期時間
            return Ok(new
            {
                token = new JwtSecurityTokenHandler().WriteToken(token),
                expiration = token.ValidTo
            });
        }
    }
添加api資源

利用默認的控制器WeatherForecastController

    • 添加個Authorize標籤

    • 路由調整為:[Route(“api/[controller]”)] 代碼如下

 [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class WeatherForecastController : ControllerBase

到此所有的代碼都已經准好了,下面進行運行測試

運行項目

使用postman進行模擬

  • 輸入url:https://localhost:44364/api/weatherforecast

     

     發現返回時401未認證,下面獲取token

  • 通過用戶和密碼獲取token

    如果我們的憑證正確,將會返回一個token和過期日期,然後利用該令牌進行訪問

  • 利用token進行請求

    ok,最後發現請求狀態200!

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

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

徹底搞懂CSS偽類選擇器:is、not

本文介紹一下Css偽類:is和:not,並解釋一下is、not、matches、any之前的關係

:not

The :not() CSS pseudo-class represents elements that do not match a list of selectors. Since it prevents specific items from being selected, it is known as the negation pseudo-class.

以上是MDN對not的解釋

單從名字上我們應該能對它有大概的認知,非選擇,排除括號內的其它元素

最簡單的例子,用CSS將div內,在不改變html的前提下,除了P標籤,其它的字體顏色變成藍色,

<div>
    <span>我是藍色</span>
    <p>我是黑色</p>
    <h1>我是藍色</h2>
    <h2>我是藍色</h2>
    <h3>我是藍色</h3>
    <h4>我是藍色</h4>
    <h5>我是藍色</h5>
</div>

之前的做法

div span,div h2,div h3, div h4,{
  color: blue;
}

not寫法

div:not(p){
  color: blue;
}

從上面的例子可以明顯體會到not偽類選擇器的作用

下面升級一下,問:將div內除了span和p,其它字體顏色變藍色

div:not(p):not(span){
  color: blue;
}

還有更為簡潔的方法,如下,但是目前兼容不太好,不建議使用

div:not(p,span){
  color: blue;
}

兼容

除IE8,目前所有主流瀏覽器都支持,可以放心使用

:is

The :is() CSS pseudo-class function takes a selector list as its argument, and selects any element that can be selected by one of the selectors in that list. This is useful for writing large selectors in a more compact form.

以上是MDN的解釋

在說is前,需要先了解一下matches

matches跟is是什麼關係?

matches是is的前世,但是本質上確實一個東西,用法完全一樣

matches這個單詞意思跟它的作用非常匹配,但是它跟not作用恰好相反,作為not的對立面,matches這個次看起來確實格格不入,而且單詞不夠簡潔,所以它被改名了,這裏還有一個issue

好了,現在知道matches和is其實是一個東西,那麼is的用法是怎樣的呢?

舉例:將header和main下的p標籤,在鼠標hover時文字變藍色

<header>
  <ul>
    <li><p>鼠標放上去變藍色</p></li>
    <li><p>鼠標放上去變藍色</p></li>
  </ul>
  <p>正常字體</p>
</header>

<main>
  <ul>
    <li><p>鼠標放上去變藍色</p></li>
    <li><p>鼠標放上去變藍色</p></li>
    <p>正常字體</p>
  </ul>
</main>

<footer>
  <ul>
    <li><p>正常字體</p></li>
    <li><p>正常字體</p></li>
  </ul>
</footer>

之前的做法

header ul p:hover,main ul p:hover{
  color: blue;
}

is寫法

:is(header, main) ul p:hover{
  color: blue;
}

從上面的例子大概能看出is的左右,但是並沒有完全體現出is的強大之處,但是當選擇的內容變多之後,特別是那種層級較多的,會發現is的寫法有多簡潔,拿MDN的一個例子看下

之前的寫法

/* Level 0 */
h1 {
  font-size: 30px;
}
/* Level 1 */
section h1, article h1, aside h1, nav h1 {
  font-size: 25px;
}
/* Level 2 */
section section h1, section article h1, section aside h1, section nav h1,
article section h1, article article h1, article aside h1, article nav h1,
aside section h1, aside article h1, aside aside h1, aside nav h1,
nav section h1, nav article h1, nav aside h1, nav nav h1 {
  font-size: 20px;
}

is寫法

/* Level 0 */
h1 {
  font-size: 30px;
}
/* Level 1 */
:is(section, article, aside, nav) h1 {
  font-size: 25px;
}
/* Level 2 */
:is(section, article, aside, nav)
:is(section, article, aside, nav) h1 {
  font-size: 20px;
}

可以看出,隨着嵌套層級的增加,is的優勢越來越明顯

說完了is,那就必須認識一下any,前面說到is是matches的替代者,

any跟is又是什麼關係呢?

是的,is也是any的替代品,它解決了any的一些弊端,比如瀏覽器前綴、選擇性能等

any作用跟is完全一樣,唯一不同的是它需要加瀏覽器前綴,用法如下

:-moz-any(.b, .c) {

}
:-webkit-any(.b, .c) {
    
}

結論

通過上面的介紹大概講述了css偽類is,not,matches,any它們三者的關係

is+not組合是大勢所趨

最後附上我的個人網站 ,轉載請著名出處

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

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

【故障公告】docker swarm 集群問題造成新版博客後台故障

非常抱歉,今天下午 16:55~17:05 左右,由於 docker swarm 集群的突發不穩定問題造成(目前處於灰度發布階段)無法正常使用,由此給您帶來麻煩,請您諒解。

出故障期時,新版博客後台的2個容器都無法正常啟動。

AME                NODE                DESIRED STATE       CURRENT STATE 
i_web.1             prod-swarm-w3       Running             Assigned 5 minutes ago                       
i_web.2             prod-swarm-w4       Running             Assigned 2 hours ago       

發現問題后,我們進行了刪除 stack 並重新部署的操作。

docker stack rm i
./deploy-production.sh 2.0.6
NAME                NODE                DESIRED STATE       CURRENT STATE
i_web.1             prod-swarm-w3       Running             Assigned 42 seconds ago                       
i_web.2             prod-swarm-w7       Running             Starting 42 seconds ago

重新部署后發現 prod-swarm-w7  節點上的容器可以正常啟動,而 prod-swarm-w3 節點上的容器問題依舊,由此確認是 prod-swarm-w3 節點出了問題,於是立即卸載該節點。

docker node update --availability drain prod-swarm-w3

卸載后,新版博客後台很快恢復了正常。

我們已經決定用 k8s 取代 docker swarm ,但目前 k8s 集群還沒部署好,在這即將與 docker swarm 說 88 的時刻,又被 docker swarm 坑了一次,都怪我們當時貪圖省事,選對了集裝箱(docker 容器)卻上錯了船(docker swarm),我們會深刻吸取這次上錯船的教訓。

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

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

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

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

BloomFilter在Hudi中的應用

Bloom Filter在Hudi中的應用

介紹

Bloom Filter可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,主要缺點是存在一定的誤判率:當其判斷元素存在時,實際上元素可能並不存在。而當判定不存在時,則元素一定不存在,Bloom Filter在對精確度要求不太嚴格的大數據量場景下運用十分廣泛。

引入

為何要引入Bloom Filter?這是Hudi為加快數據upsert採用的一種解決方案,即判斷record是否已經在文件中存在,若存在,則更新,若不存在,則插入。對於upsert顯然無法容忍出現誤判,否則可能會出現應該插入和變成了更新的錯誤,那麼Hudi是如何解決誤判問題的呢?一種簡單辦法是當Bloom Filter判斷該元素存在時,再去文件里二次確認該元素是否真的存在;而當Bloom Filter判斷該元素不存在時,則無需讀文件,通過二次確認的方法來規避Bloom Filter的誤判問題,實際上這也是Hudi採取的方案,值得一提的是,現在Delta暫時還不支持Bloom Filter,其判斷一條記錄是否存在是直接通過一次全表join來實現,效率比較低下。接下來我們來分析Bloom Filter在Hudi中的應用。

流程

Hudi從上游系統(Kafka、DFS等)消費一批數據后,會根據用戶配置的寫入模式(insert、upsert、bulkinsert)寫入Hudi數據集。而當配置為upsert時,意味着需要將數據插入更新至Hudi數據集,而第一步是需要標記哪些記錄已經存在,哪些記錄不存在,然後,對於存在的記錄進行更新,不存在記錄進行插入。

HoodieWriteClient中提供了對應三種寫入模式的方法(#insert、#upsert、#bulkinsert),對於使用了Bloom Filter的#upsert方法而言,其核心源代碼如下

public JavaRDD<WriteStatus> upsert(JavaRDD<HoodieRecord<T>> records, final String commitTime) {
    ...
    // perform index loop up to get existing location of records
    JavaRDD<HoodieRecord<T>> taggedRecords = index.tagLocation(dedupedRecords, jsc, table);
    ...
    return upsertRecordsInternal(taggedRecords, commitTime, table, true);
}

可以看到首先利用索引給記錄打標籤,然後再進行更新,下面主要分析打標籤的過程。

對於索引,Hudi提供了四種索引方式的實現:HBaseIndexHoodieBloomIndexHoodieGlobalBloomIndexInMemoryHashIndex,默認使用HoodieBloomIndex。其中HoodieGlobalBloomIndex與HoodieBloomIndex的區別是前者會讀取所有分區文件,而後者只讀取記錄所存在的分區下的文件。下面以HoodieBloomIndex為例進行分析。

HoodieBloomIndex#tagLocation核心代碼如下

public JavaRDD<HoodieRecord<T>> tagLocation(JavaRDD<HoodieRecord<T>> recordRDD, JavaSparkContext jsc,
      HoodieTable<T> hoodieTable) {

    // Step 0: cache the input record RDD
    if (config.getBloomIndexUseCaching()) {
      recordRDD.persist(config.getBloomIndexInputStorageLevel());
    }

    // Step 1: Extract out thinner JavaPairRDD of (partitionPath, recordKey)
    JavaPairRDD<String, String> partitionRecordKeyPairRDD =
        recordRDD.mapToPair(record -> new Tuple2<>(record.getPartitionPath(), record.getRecordKey()));

    // Lookup indexes for all the partition/recordkey pair
    JavaPairRDD<HoodieKey, HoodieRecordLocation> keyFilenamePairRDD =
        lookupIndex(partitionRecordKeyPairRDD, jsc, hoodieTable);

    // Cache the result, for subsequent stages.
    if (config.getBloomIndexUseCaching()) {
      keyFilenamePairRDD.persist(StorageLevel.MEMORY_AND_DISK_SER());
    }

    // Step 4: Tag the incoming records, as inserts or updates, by joining with existing record keys
    // Cost: 4 sec.
    JavaRDD<HoodieRecord<T>> taggedRecordRDD = tagLocationBacktoRecords(keyFilenamePairRDD, recordRDD);

    if (config.getBloomIndexUseCaching()) {
      recordRDD.unpersist(); // unpersist the input Record RDD
      keyFilenamePairRDD.unpersist();
    }

    return taggedRecordRDD;
  }

該過程會緩存記錄以便優化數據的加載。首先從記錄中解析出對應的分區路徑 -> key,接着查看索引,然後將位置信息(存在於哪個文件)回推到記錄中。

HoodieBloomIndex#lookup核心代碼如下

private JavaPairRDD<HoodieKey, HoodieRecordLocation> lookupIndex(
      JavaPairRDD<String, String> partitionRecordKeyPairRDD, final JavaSparkContext jsc,
      final HoodieTable hoodieTable) {
    // Obtain records per partition, in the incoming records
    Map<String, Long> recordsPerPartition = partitionRecordKeyPairRDD.countByKey();
    List<String> affectedPartitionPathList = new ArrayList<>(recordsPerPartition.keySet());

    // Step 2: Load all involved files as <Partition, filename> pairs
    List<Tuple2<String, BloomIndexFileInfo>> fileInfoList =
        loadInvolvedFiles(affectedPartitionPathList, jsc, hoodieTable);
    final Map<String, List<BloomIndexFileInfo>> partitionToFileInfo =
        fileInfoList.stream().collect(groupingBy(Tuple2::_1, mapping(Tuple2::_2, toList())));

    // Step 3: Obtain a RDD, for each incoming record, that already exists, with the file id,
    // that contains it.
    Map<String, Long> comparisonsPerFileGroup =
        computeComparisonsPerFileGroup(recordsPerPartition, partitionToFileInfo, partitionRecordKeyPairRDD);
    int safeParallelism = computeSafeParallelism(recordsPerPartition, comparisonsPerFileGroup);
    int joinParallelism = determineParallelism(partitionRecordKeyPairRDD.partitions().size(), safeParallelism);
    return findMatchingFilesForRecordKeys(partitionToFileInfo, partitionRecordKeyPairRDD, joinParallelism, hoodieTable,
        comparisonsPerFileGroup);
  }

該方法首先會計算出每個分區有多少條記錄和影響的分區有哪些,然後加載影響的分區的文件,最後計算并行度后,開始找記錄真正存在的文件。

對於#loadInvolvedFiles方法而言,其會查詢指定分區分區下所有的數據文件(parquet格式),並且如果開啟了hoodie.bloom.index.prune.by.ranges,還會讀取文件中的最小key和最大key(為加速後續的查找)。

HoodieBloomIndex#findMatchingFilesForRecordKeys核心代碼如下

JavaPairRDD<HoodieKey, HoodieRecordLocation> findMatchingFilesForRecordKeys(
      final Map<String, List<BloomIndexFileInfo>> partitionToFileIndexInfo,
      JavaPairRDD<String, String> partitionRecordKeyPairRDD, int shuffleParallelism, HoodieTable hoodieTable,
      Map<String, Long> fileGroupToComparisons) {
    JavaRDD<Tuple2<String, HoodieKey>> fileComparisonsRDD =
        explodeRecordRDDWithFileComparisons(partitionToFileIndexInfo, partitionRecordKeyPairRDD);

    if (config.useBloomIndexBucketizedChecking()) {
      Partitioner partitioner = new BucketizedBloomCheckPartitioner(shuffleParallelism, fileGroupToComparisons,
          config.getBloomIndexKeysPerBucket());

      fileComparisonsRDD = fileComparisonsRDD.mapToPair(t -> new Tuple2<>(Pair.of(t._1, t._2.getRecordKey()), t))
          .repartitionAndSortWithinPartitions(partitioner).map(Tuple2::_2);
    } else {
      fileComparisonsRDD = fileComparisonsRDD.sortBy(Tuple2::_1, true, shuffleParallelism);
    }

    return fileComparisonsRDD.mapPartitionsWithIndex(new HoodieBloomIndexCheckFunction(hoodieTable, config), true)
        .flatMap(List::iterator).filter(lr -> lr.getMatchingRecordKeys().size() > 0)
        .flatMapToPair(lookupResult -> lookupResult.getMatchingRecordKeys().stream()
            .map(recordKey -> new Tuple2<>(new HoodieKey(recordKey, lookupResult.getPartitionPath()),
                new HoodieRecordLocation(lookupResult.getBaseInstantTime(), lookupResult.getFileId())))
            .collect(Collectors.toList()).iterator());
  }

該方法首先會查找記錄需要進行比對的文件,然後再查詢的記錄的位置信息。

其中,對於#explodeRecordRDDWithFileComparisons方法而言,其會藉助樹/鏈表結構構造的文件過濾器來加速記錄對應文件的查找(每個record可能會對應多個文件)。

而使用Bloom Filter的核心邏輯承載在HoodieBloomIndexCheckFunction,HoodieBloomIndexCheckFunction$LazyKeyCheckIterator該迭代器完成了記錄對應文件的實際查找過程,查詢的核心邏輯在computeNext`中,其核心代碼如下

protected List<HoodieKeyLookupHandle.KeyLookupResult> computeNext() {

      List<HoodieKeyLookupHandle.KeyLookupResult> ret = new ArrayList<>();
      try {
        // process one file in each go.
        while (inputItr.hasNext()) {
          Tuple2<String, HoodieKey> currentTuple = inputItr.next();
          String fileId = currentTuple._1;
          String partitionPath = currentTuple._2.getPartitionPath();
          String recordKey = currentTuple._2.getRecordKey();
          Pair<String, String> partitionPathFilePair = Pair.of(partitionPath, fileId);

          // lazily init state
          if (keyLookupHandle == null) {
            keyLookupHandle = new HoodieKeyLookupHandle(config, hoodieTable, partitionPathFilePair);
          }

          // if continue on current file
          if (keyLookupHandle.getPartitionPathFilePair().equals(partitionPathFilePair)) {
            keyLookupHandle.addKey(recordKey);
          } else {
            // do the actual checking of file & break out
            ret.add(keyLookupHandle.getLookupResult());
            keyLookupHandle = new HoodieKeyLookupHandle(config, hoodieTable, partitionPathFilePair);
            keyLookupHandle.addKey(recordKey);
            break;
          }
        }

        // handle case, where we ran out of input, close pending work, update return val
        if (!inputItr.hasNext()) {
          ret.add(keyLookupHandle.getLookupResult());
        }
      } catch (Throwable e) {
        if (e instanceof HoodieException) {
          throw e;
        }
        throw new HoodieIndexException("Error checking bloom filter index. ", e);
      }

      return ret;
    }

該方法每次迭代只會處理一個文件,每次處理時都會生成HoodieKeyLookupHandle,然後會添加recordKey,處理完后再獲取查詢結果。

其中HoodieKeyLookupHandle#addKey方法核心代碼如下

public void addKey(String recordKey) {
    // check record key against bloom filter of current file & add to possible keys if needed
    if (bloomFilter.mightContain(recordKey)) {
      ...
      candidateRecordKeys.add(recordKey);
    }
    totalKeysChecked++;
  }

可以看到,這裏使用到了Bloom Filter來判斷該記錄是否存在,如果存在,則加入到候選隊列中,等待進一步判斷;若不存在,則無需額外處理,其中Bloom Filter會在創建HoodieKeyLookupHandle實例時初始化(從指定文件中讀取Bloom Filter)。

HoodieKeyLookupHandle#getLookupResult方法核心代碼如下

public KeyLookupResult getLookupResult() {
    ...
    HoodieDataFile dataFile = getLatestDataFile();
    List<String> matchingKeys =
        checkCandidatesAgainstFile(hoodieTable.getHadoopConf(), candidateRecordKeys, new Path(dataFile.getPath()));
    ...
    return new KeyLookupResult(partitionPathFilePair.getRight(), partitionPathFilePair.getLeft(),
        dataFile.getCommitTime(), matchingKeys);
  }

該方法首先獲取指定分區下的最新數據文件,然後判斷數據文件存在哪些recordKey,並將其封裝進KeyLookupResult后返回。其中#checkCandidatesAgainstFile會讀取文件中所有的recordKey,判斷是否存在於candidateRecordKeys,這便完成了進一步確認。

到這裏即完成了record存在於哪些文件的所有查找,查找完後會進行進一步處理,後續再給出分析。

總結

Hudi引入Bloom Filter是為了加速upsert過程,並將其存入parquet數據文件中的Footer中,在讀取文件時會從Footer中讀取該BloomFilter。在利用Bloom Filter來判斷記錄是否存在時,會採用二次確認的方式規避Bloom Filter的誤判問題。

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

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

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

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

坑~夏令時冬令時引發的時間換算問題

 

起因

最近接觸到一些國外的項目,由於國內外有時差這個東西,對於某些基礎數據存到數據庫的時候需要記錄時間,為了方便,這裏採用了時間戳(int或者timestamp)記錄。由於時間戳全球都是一樣的,需要的時候根據時區進行轉換就能夠拿到當地的時間。

嗯~ o(* ̄▽ ̄*)o,這樣看起來確實沒什麼毛病。眾所周知,一天有24小時,換算成秒就是:24*60*60=86400秒。

然而,我在某次使用 MySql 的 FROM_UNIXTIME 發現一個問題,兩個時間相差86400秒,但是格式化之後卻不是相差一天!!!

假設北京時間2019年11月25日 12:00:00,對應的時間戳是:1574654400,照理說這個時間戳加上一天86400秒,理論上就是北京時間2019年11月26日 12:00:00,事實上確實如此,國內的話這麼算確實沒什麼問題,但是如果是國外時區的話,那可能會出問題。

由於國外部分國家有夏令時冬令時之分(具體下面會細說),直接加上86400秒可能會有問題。

感興趣的可以拿1572764400(太平洋時間2019-11-03 00:00:00,單位:)這個時間戳驗證下

拿代碼演示下:

PHP:

<?php

echo "PST時區的時間\n";
date_default_timezone_set('PST8PDT');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

//換個時區
echo "換成上海時區看看\n";
date_default_timezone_set('Asia/Shanghai');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

運行結果:

PST時區的時間
2019-11-03 00:00:00
2019-11-03 23:00:00
換成上海時區看看
2019-11-03 15:00:00
2019-11-04 15:00:00

明明是同一個時間戳,都是加上86400(一天),為什麼在上海這個時區是第二天,而在PST(美國太平洋時區)只加了23小時?神不神奇!意不意外!

為了弄清楚這個問題,首先得先了解下什麼是夏令時,什麼是冬令時

 

夏令時

夏令時,表示為了節約能源,人為規定時間的意思。也叫夏時制,夏時令(Daylight Saving Time:DST),又稱“日光節約時制”和“夏令時間”,在這一制度實行期間所採用的統一時間稱為“夏令時間”。

一般在天亮早的夏季人為將時間調快一小時,可以使人早起早睡,減少照明量,以充分利用光照資源,從而節約照明用電。各個採納夏時制的國家具體規定不同。目前全世界有近110個國家每年要實行夏令時。[1]

 

冬令時

有夏令時就會有冬令時。高緯度和中緯度的許多國家在夏季到來前,把時針撥快一小時,新的時間就是夏令時,到下半季秋季來臨前,再把時針撥回一小時,即形成冬令時。 [2] 

 

夏令時和冬令時的影響

拿美國來說,美國各個地區的時間都不同,不像中國一樣統一使用北京時間,美國一般以三月份第二個周日凌晨兩點當成夏季的開始,十一月份第一個周日的凌晨兩點當成冬季的開始。

所以在每年的三月份第二個周日凌晨兩點過後,時間就會往前調快一個小時;同理,十一月份第一個周日把這一個小時調回來

你也可以理解成美國那邊,一年裡面有一天只有23小時(夏天開始那一天),有一天有25小時(冬天開始那一天),其他時間每天都是24小時。

所以你會發現,夏天的時候,中國的北京時間(東八區)與美國太平洋時區(西八區)的時差是15小時,而到了冬天卻變成16小時

 

解決方案

回到開頭那個問題,如果我們想直接算第二天,直接加上86400(一天)可能在其他國家就會有我上面那個夏令時和冬令時時間換算的問題,要如何避免呢?首先能夠確定的是,直接加上86400是不可取的,如果加上一天能否行得通

PHP:

<?php

echo "PST時區的時間\n";
date_default_timezone_set('PST8PDT');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

echo "--------------------------\n";
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',strtotime('+1 day',1572764400));
echo "\n";

運行結果:

PST時區的時間
2019-11-03 00:00:00
2019-11-03 23:00:00
--------------------------
2019-11-03 00:00:00
2019-11-04 00:00:00

可以看出,不直接加上86400,直接在日期上加上一天是完全沒問題的。

JavaScript:

var date = new Date(1572764400*1000);
date.setDate(date.getDate()+1);
var timestamp = Math.round(date.getTime()/1000);

注意:JS的時間戳是毫秒!!!

 

結論

在經濟全球化快速發展的今天,在軟件開發的過程中,盡量養成習慣,由於夏令時和冬令時不是固定的,開發在時間計算上應該慎用86400進行加減運算,時間計算請直接對日期進行加減,展示時間給用戶看的時候盡量結合當地時間,結合夏令時和冬令時計算出準確的當地時間,避免產生不必要的分歧。

 

參考:

[1]. 

[2]. 

 

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

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

.NET高級特性-Emit(2)類的定義,.NET高級特性-Emit(1)

  在上一篇博文發了一天左右的時間,就收到了博客園許多讀者的評論和推薦,非常感謝,我也會及時回復讀者的評論。之後我也將繼續撰寫博文,梳理相關.NET的知識,希望.NET的圈子能越來越大,開發者能了解/深入.NET的本質,將工作做的簡單又高效,拒絕重複勞動,拒絕CRUD。

  ok,咱們開始繼續Emit的探索。在這之前,我先放一下我往期關於Emit的文章,方便讀者閱讀。

  《》

一、基礎知識

  既然C#作為一門面向對象的語言,所以首當其沖的我們需要讓Emit為我們動態構建類。

  廢話不多說,首先,我們先來回顧一下C#類的內部由什麼東西組成:

  (1) 字段-C#類中保存數據的地方,由訪問修飾符、類型和名稱組成;

  (2) 屬性-C#類中特有的東西,由訪問修飾符、類型、名稱和get/set訪問器組成,屬性的是用來控制類中字段數據的訪問,以實現類的封裝性;在Java當中寫作getXXX()和setXXX(val),C#當中將其變成了屬性這種語法糖;

  (3) 方法-C#類中對邏輯進行操作的基本單元,由訪問修飾符、方法名、泛型參數、入參、出參構成;

  (4) 構造器-C#類中一種特殊的方法,該方法是專門用來創建對象的方法,由訪問修飾符、與類名相同的方法名、入參構成。

  接着,我們再觀察C#類本身又具備哪些東西:

  (1) 訪問修飾符-實現對C#類的訪問控制

  (2) 繼承-C#類可以繼承一個父類,並需要實現父類當中所有抽象的方法以及選擇實現父類的虛方法,還有就是子類需要調用父類的構造器以實現對象的創建

  (3) 實現-C#類可以實現多個接口,並實現接口中的所有方法

  (4) 泛型-C#類可以包含泛型參數,此外,類還可以對泛型實現約束

  以上就是C#類所具備的一些元素,以下為樣例:

public abstract class Bar
{
    public abstract void PrintName();
}
public interface IFoo<T> { public T Name { get; set; } } //繼承Bar基類,實現IFoo接口,泛型參數T
public class Foo<T> : Bar, IFoo<T>
  //泛型約束
  where T : struct {
//構造器 public Foo(T name):base() { _name = name; } //字段 private T _name; //屬性 public T Name { get => _name; set => _name = value; } //方法 public override void PrintName() {
    Console.WriteLine(_name.ToString()); }
}

  在探索完了C#類及其定義后,我們要來了解C#的項目結構組成。我們知道C#的一個csproj項目最終會對應生成一個dll文件或者exe文件,這一個文件我們稱之為程序集Assembly;而在一個程序集中,我們內部包含和定義了許多命名空間,這些命令空間在C#當中被稱為模塊Module,而模塊正是由一個一個的C#類Type組成。

 

 

 

   所以,當我們需要定義C#類時,就必須首先定義Assembly以及Module,如此才能進行下一步工作。

二、IL概覽

   由於Emit實質是通過IL來生成C#代碼,故我們可以反向生成,先將寫好的目標代碼寫成cs文件,通過編譯器生成dll,再通過ildasm查看IL代碼,即可依葫蘆畫瓢的編寫出Emit代碼。所以我們來查看以下上節Foo所生成的IL代碼。

  

 

 

   從上圖我們可以很清晰的看到.NET的層級結構,位於樹頂層淺藍色圓點表示一個程序集Assembly,第二層藍色表示模塊Module,在模塊下的均為我們所定義的類,類中包含類的泛型參數、繼承類信息、實現接口信息,類的內部包含構造器、方法、字段、屬性以及它的get/set方法,由此,我們可以開始編寫Emit代碼了

三、Emit編寫

  有了以上的對C#類的解讀和IL的解讀,我們知道了C#類本身所需要哪些元素,我們就開始根據這些元素來開始編寫Emit代碼了。這裏的代碼量會比較大,請讀者慢慢閱讀,也可以參照以上我寫的類生成il代碼進行比對。

  在Emit當中所有創建類型的幫助類均以Builder結尾,從下錶中我們可以看的非常清楚

元素中文 元素名稱 對應Emit構建器名稱
程序集  Assembly AssemblyBuilder
模塊  Module ModuleBuilder
 Type TypeBuilder
構造器  Constructor ConstructorBuilder
屬性  Property PropertyBuilder
字段  Field FieldBuilder
方法  Method MethodBuilder

  由於創建類需要從Assembly開始創建,所以我們的入口是AssemblyBuilder

  (1) 首先,我們先引入命名空間,我們以上節Foo類為樣例進行編寫

using System.Reflection.Emit;

  (2) 獲取基類和接口的類型

var barType = typeof(Bar);
var interfaceType = typeof(IFoo<>);

  (3) 定義Foo類型,我們可以看到在定義類之前我們需要創建Assembly和Module

//定義類
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Edwin.Blog.Emit");
var typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

  (4) 定義泛型參數T,並添加約束

//定義泛型參數
var genericTypeBuilder = typeBuilder.DefineGenericParameters("T")[0];
//設置泛型約束
genericTypeBuilder.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

  (5) 繼承和實現接口,注意當實現類的泛型參數需傳遞給接口時,需要將泛型接口添加泛型參數后再調用AddInterfaceImplementation方法

//繼承基類
typeBuilder.SetParent(barType);
//實現接口
typeBuilder.AddInterfaceImplementation(interfaceType.MakeGenericType(genericTypeBuilder));

  (6) 定義字段,因為字段在構造器值需要使用,故先創建

//定義字段
var fieldBuilder = typeBuilder.DefineField("_name", genericTypeBuilder, FieldAttributes.Private);

  (7) 定義構造器,並編寫內部邏輯

//定義構造器
var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { genericTypeBuilder });
var ctorIL = ctorBuilder.GetILGenerator();
//Ldarg_0在實例方法中表示this,在靜態方法中表示第一個參數
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
//為field賦值
ctorIL.Emit(OpCodes.Stfld, fieldBuilder);
ctorIL.Emit(OpCodes.Ret);

  (8) 定義Name屬性

//定義屬性
var propertyBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None, genericTypeBuilder, Type.EmptyTypes);

  (9) 編寫Name屬性的get/set訪問器

//定義get方法
var getMethodBuilder = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, genericTypeBuilder, Type.EmptyTypes);
var getIL = getMethodBuilder.GetILGenerator();
getIL.Emit(OpCodes.Ldarg_0);
getIL.Emit(OpCodes.Ldfld, fieldBuilder);
getIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(getMethodBuilder, interfaceType.GetProperty("Name").GetGetMethod()); //實現對接口方法的重載
propertyBuilder.SetGetMethod(getMethodBuilder); //設置為屬性的get方法
//定義set方法
var setMethodBuilder = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { genericTypeBuilder });
var setIL = setMethodBuilder.GetILGenerator();
setIL.Emit(OpCodes.Ldarg_0);
setIL.Emit(OpCodes.Ldarg_1);
setIL.Emit(OpCodes.Stfld, fieldBuilder);
setIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(setMethodBuilder, interfaceType.GetProperty("Name").GetSetMethod()); //實現對接口方法的重載
propertyBuilder.SetSetMethod(setMethodBuilder); //設置為屬性的set方法

   (10) 定義並實現PrintName方法

//定義方法
var printMethodBuilder = typeBuilder.DefineMethod("PrintName", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.Standard, null, Type.EmptyTypes);
var printIL = printMethodBuilder.GetILGenerator();
printIL.Emit(OpCodes.Ldarg_0);
printIL.Emit(OpCodes.Ldflda, fieldBuilder);
printIL.Emit(OpCodes.Constrained, genericTypeBuilder);
printIL.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
printIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
printIL.Emit(OpCodes.Ret);
//實現對基類方法的重載
typeBuilder.DefineMethodOverride(printMethodBuilder, barType.GetMethod("PrintName", Type.EmptyTypes));

  (11) 創建類

var type = typeBuilder.CreateType(); //netstandard中請使用CreateTypeInfo().AsType()

  (12) 調用

var obj = Activator.CreateInstance(type.MakeGenericType(typeof(DateTime)), DateTime.Now);
(obj as Bar).PrintName();
Console.WriteLine((obj as IFoo<DateTime>).Name);

四、應用

  上面的樣例僅供學習只用,無法運用在實際項目當中,那麼,Emit構建類在實際項目中我們可以有什麼應用,提高我們的編碼效率

  (1) 動態DTO-當我們需要將實體映射到某個DTO時,可以用動態DTO來代替你手寫的DTO,選擇你需要的字段回傳給前端,或者前端把他想要的字段傳給後端

  (2) DynamicLinq-我的第一篇博文有個讀者提到了表達式樹,而linq使用的正是表達式樹,當表達式樹+Emit時,我們就可以用像SQL或者GraphQL那樣的查詢語句實現動態查詢

  (3) 對象合併-我們可以編寫實現一個像js當中Object.assign()一樣的方法,實現對兩個實體的合併

  (4) AOP動態代理-AOP的核心就是代理模式,但是與其對應的是需要手寫代理類,而Emit就可以幫你動態創建代理類,實現切面編程

  (5) …

五、小結

  對於Emit,確實初學者會對其感到複雜和難以學習,但是只要搞懂其中的原理,其實最終就是C#和.NET語言的本質所在,在學習Emit的同時,也是在鍛煉你的基本功是否紮實,你是否對這門語言精通,是否有各種簡化代碼的應用。

  保持學習,勇於實踐;Write Less,Do More;作者之後還會繼續.NET高級特性系列,感謝閱讀!

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

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

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

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