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 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注……

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

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

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

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

Java多線程系列——多線程方法詳解

Java多線程系列文章是Java多線程的詳解介紹,對多線程還不熟悉的同學可以先去看一下我的這篇博客,這篇博客從宏觀層面介紹了多線程的整體概況,接下來的幾篇文章是對多線程的深入剖析。

 

多線程的常用方法

1、currentThread()方法:

介紹:currentThread()方法可返回該代碼正在被哪個線程調用的信息。

示例

例1:

public class Test01 {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());
	}
	
}

結果:
main

  

結果說明,main方法被名為main的線程調用

 

例2:

class Mythread extends Thread{
	
	public Mythread() {
		System.out.println("構造方法的打印:"+Thread.currentThread().getName());
	}
	
	@Override
	public void run() {
		System.out.println("run方法的打印:"+Thread.currentThread().getName());
	}
}

public class Test01 {

	public static void main(String[] args) {
		Mythread t=new Mythread();
		t.start();//①
	}
	
}

結果:
構造方法的打印:main
run方法的打印:Thread-0

  

從結果可知:Mythread的構造方法是被main線程調用的,而run方法是被名稱為Thread-0的線程調用的,run方法是線程自動調用的

現在我們將①處的代碼改為t.run(),現在的輸出結果如下:

構造方法的打印:main
run方法的打印:main

  

從結果中我們可以看到兩次的結果显示都是main線程調用了方法,因為當你使用t.start()方法的時候是線程自動調用的run()方法,所以輸出的是Thread-0,當你直接調用run()方法時,和調用普通方法沒有什麼區別,所以是main線程調用run()

 

2、isAlive()方法:

介紹:isAlive()方法的功能是判斷當前的線程是否處於活動狀態

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		System.out.println("run =="+this.isAlive());
	}
	
}

public class Test01 {

	public static void main(String[] args) {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
end ==true
run ==true

  

方法isAlive()的作用是測試線程是否處於活動狀態。那麼什麼情況下是活動狀態呢?活動狀態就是線程已經啟動且尚未停止。線程處於正在運行或準備開始運行的狀態,就認為線程是存活的

①處代碼的結果為false,因為此時線程還未啟動;

②處代碼調用了run()方法輸出結果為run ==true,此時線程處於活動狀態;

③處代碼的結果為true,有的同學看到這個輸出可能會不理解,不是說線程處於活動狀態isAlive()方法的結果才是true,現在程序都已經運行結束了為什麼還是true?這裏的輸出結果是不確定的,我們再來看下面一段代碼

我們將例1中的代碼稍做修改,代碼如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		Thread.sleep(1000);//這裏加了一行代碼,讓當前線程沉睡1秒
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
run ==true
end ==false

  

現在我們看到③處的代碼結果為end ==false,因為thread對象已經在1秒內執行完畢,而上面代碼輸出結果為true是因為thread線程未執行完畢。

 

3、sleep()方法:

介紹:

方法sleep()的作用是在指定的毫秒數內讓當前“正在執行的線程”休眠(暫停執行),這個“正在執行的線程”是指this.currentThread()返回的線程。

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run threadName="+this.currentThread().getName()+" begin");
			Thread.sleep(2000);
			System.out.println("run threadName="+this.currentThread().getName()+" end");
			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin ="+System.currentTimeMillis());
		thread.run();//①
		System.out.println("end ="+System.currentTimeMillis());
	}
	
}

結果:
begin =1574660731663
run threadName=main begin
run threadName=main end
end =1574660733665

  

從結果中可以看出main線程暫停了2秒(因為這裏調用的是thread.run())

下面我們將①處的代碼改成thread.start(),再來看下運行結果:

begin =1574661491412
end =1574661491412
run threadName=Thread-0 begin
run threadName=Thread-0 end

  

由於main線程與thread線程是異步執行的,所以首先打印的信息為begin和end,而thread線程是隨後運行的,在最後兩行打印run begin和run end的信息。

 

4、getId()方法:

介紹:getId()方法的作用是取得線程的唯一標識

示例

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread thread=Thread.currentThread();
		System.out.println(thread.getName()+" "+thread.getId());
	}
	
}

結果:main 1

  

從運行結果可以看出,當前執行代碼的線程名稱是main,線程id值為1

 

5、停止線程:

介紹:停止線程是在多線程開發時很重要的技術點,掌握此技術可以對線程的停止進行有效的處理。停止線程在Java語言中並不像break語句那樣乾脆,需要一些技巧性的處理。

在java中有三種方法可以停止線程

  1. 使用退出標誌,讓線程正常退出,也就是當run方法執行完之後終止
  2. 使用stop方法強制終止線程,但是不推薦使用,因為stop和suspend及resume一樣,是java廢棄的方法
  3. 使用interrupt方法中斷線程(推薦使用)

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(2000);
		thread.interrupt();
	}
	
}

  

運行結果:

 

從運行結果我們可以看出最後i=500000,調用interrupt方法沒有停止線程,那麼該如何停止線程呢?

在介紹如何停止線程時,我們先來介紹一下如何判斷線程是否處於停止狀態

Thread類中提供了兩種方法用來判斷線程是否停止:

1、this.interrupted():測試當前線程是否已經中斷,執行后具有將狀態標誌清除為false的功能

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
}

  

2、this.isInterrupted():測試線程Thread對象是否已經中斷,但是不清除狀態標誌

public boolean isInterrupted() {
        return isInterrupted(false);
}

  

讀者可以仔細觀看一下這兩個方法的聲明有什麼不同?

 

例2:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(1000);
		thread.interrupt();
		System.out.println("是否停止1?="+thread.interrupted());
		System.out.println("是否停止2?="+thread.interrupted());
		System.out.println("end!");
	}
	
}

  

結果:

 

 

 輸出結果显示調用了thread.interrupt()方法后線程並未停止,這也就證明了interrupted()方法的解釋:測試當前線程是否已經中斷。這個當前線程是main,它從未斷過,所以打印的結果是兩個false。

如果想讓main線程結束該怎麼做?

將main方法改成如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread.currentThread().interrupt();
		System.out.println("是否停止1?="+Thread.interrupted());
		System.out.println("是否停止2?="+Thread.interrupted());
		System.out.println("end!");
	}
	
}

結果:
是否停止1?=true
是否停止2?=false
end!

  

從輸出結果我們可以看出,方法interrupted()的確判斷出當前線程是否是停止狀態。但為什麼第2個值是false?

查看一下官方文檔的介紹:

測試當前線程是否已經中斷。線程的中斷狀態由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。

 文檔中說明的非常清楚,interrupted()方法具有清除狀態的功能,所以第二次調用interrupted方法返回的值時false。

下面我們來看一下isInterrupted()方法,將main方法改成如下代碼:

public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		thread.interrupt();
		Thread.sleep(1000);
		System.out.println("是否停止1?="+thread.isInterrupted());
		System.out.println("是否停止2?="+thread.isInterrupted());
		System.out.println("end");
}
結果:

是否停止1?=true
是否停止2?=true
end

  

從結果可以看出,方法isInterrrupted()並未清除狀態,所以結果為兩個true。

 

例3:在沉睡中停止

當線程調用sleep()方法后再調用interrupt()方法後會有什麼結果:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run begin");
			Thread.sleep(200000);
			System.out.println("run end");
		} catch (InterruptedException e) {
			System.out.println("在沉睡中被停止,進入catch!"+this.isInterrupted());
			e.printStackTrace();
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		try {
			Mythread thread=new Mythread();
			thread.start();
			Thread.sleep(200);
			thread.interrupt();
		}catch(Exception e) {
			System.out.println("main catch");
			e.printStackTrace();
		}
		System.out.println("end!");
	}
	
}

  

 

 

6、暫停線程:

暫停線程意味着此線程還可以恢復運行。在java多線程中,可以使用suspend()方法暫停線程,使用resume()方法恢複線程的執行

例1:

class Mythread extends Thread{
	
	private long i=0;
	public long getI() {
		return i;
	}
	
    public void setI(long i) {
		this.i = i;
	}
	
	@Override
	public void run() {
		while(true) {
			i++;
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(5000);
		//A段
		thread.suspend();
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		
		//B段
		thread.resume();
		Thread.sleep(5000);
		
		//C段
		thread.suspend();
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		
	}
	
}

  

結果:

 


從控制台打印的時間上來看,線程的確被暫停了,而且還可以恢復成運行狀態。

 

7、yield方法:

介紹:yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		long beginTime=System.currentTimeMillis();
		int count=0;
		for(int i=0;i<500000;i++) {
               //Thread.yield();① count=count+(i+1); } long endTime=System.currentTimeMillis(); System.out.println("用時:"+(endTime-beginTime)+"毫秒!"); } } public class Test01 { public static void main(String[] args) throws InterruptedException { Mythread thread=new Mythread(); thread.start(); } }

結果:用時:2毫秒!

  

現在將①處的代碼取消註釋,我們再來看一下運行結果:

用時:213毫秒!

  

將CPU讓給其他資源導致速度變慢

 

8、線程優先級:

介紹:

在操作系統中,線程可以劃分優先級,優先級較高的線程得到的CPU資源較多,也就是CPU優先執行優先級較高的線程對象中的任務。

設置線程優先級有助於幫“線程規劃器”確定在下一次選擇哪一個線程來優先執行。

設置線程的優先級使用setPriority()方法,此方法在JDK的源代碼如下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
}

  

在Java中,線程的優先級為1-10這10個等級,如果小於1或大於10,則JDK拋出異常throw new IllegalArgumentException()。

通常高優先級的線程總是先執行完,但是並不是一定的,高優先級和低優先級的線程會交替進行,高優先級執行的次數多一些

 

線程優先級的繼承特性:

在Java中,線程的優先級具有繼承性,比如A線程啟動B線程,則B線程的優先級與A是一樣的。

class Mythread2 extends Thread{
	@Override
	public void run() {
		System.out.println("Mythread2 run priority="+this.getPriority());
	}
}


class Mythread1 extends Thread{
	
	@Override
	public void run() {
		System.out.println("Mythread run priority="+this.getPriority());
		Mythread2 thread2=new Mythread2();
		thread2.start();
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		System.out.println("main thread begin priority="+Thread.currentThread().getPriority());
    	        //Thread.currentThread().setPriority(6);①
		System.out.println("main thread end priority="+Thread.currentThread().getPriority());
		Mythread1 thread1=new Mythread1();
		thread1.start();
	}
	
}

結果:
main thread begin priority=5
main thread end priority=5
Mythread run priority=5
Mythread2 run priority=5

  

可以看到上面幾個線程的優先級都為5

現在將①處的代碼註釋掉后的結果是:

main thread begin priority=5
main thread end priority=6
Mythread run priority=6
Mythread2 run priority=6

  

優先級被更改后再繼續繼承

 

9、守護線程:

在java中有兩種線程,一種是用戶線程,另一種是守護線程。

守護線程是一種特殊的線程,它的特性有“陪伴”的含義,當進程中不存在非守護線程了,則守護線程自動銷毀。典型的守護線程就是垃圾回收線程,當進程中沒有非守護線程了,則垃圾回收線程也就沒有存在的必要了,自動銷毀。用個比較通俗的比喻來解釋一下:“守護線程”:任何一個守護線程都是整個JVM中所有非守護線程的“保姆”,只要當前JVM實例中存在任何一個非守護線程沒有結束,守護線程就在工作,只有當最後一個非守護線程結束時,守護線程才隨着JVM一同結束工作。Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是GC(垃圾回收器),它就是一個很稱職的守護者。

 

class Mythread extends Thread{
	
	private int i=0;
	
	
	@Override
	public void run() {
		
		try {
			while(true) {
				i++;
				System.out.println("i="+(i));
				Thread.sleep(1000);
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		try {
			Mythread thread=new Mythread();
			thread.setDaemon(true);
			thread.start();
			Thread.sleep(5000);
			System.out.println("我離開Thread對象就不再打印了,也就是停止了");
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
}

  

 

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

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的誤判問題。

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

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

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

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

Java開發中常用jar包整理及使用

本文整理了我自己在Java開發中常用的jar包以及常用的API記錄。

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

common-lang3

簡介

一個現在最為常用的jar包,封裝了許多常用的工具包

依賴:

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>

主要常見的類如下:

  • 數組工具類 ArrayUtils
  • 日期工具類 DateUtils DateFormatUtils
  • 字符串工具類 StringUtils
  • 数字工具類 NumberUtils
  • 布爾工具類 BooleanUtils
  • 反射相關工具類 FieldUtils、MethodUtils、MemberUtils、TypeUtils、ConstructorUtils
  • 對象工具類 ObjectUtils
  • 序列化工具類 SerializationUtils

API介紹

這裏我只介紹經常使用的幾個工具類及方法,ArrayUtils,StringUtils,NumberUtils,DateUtils,其他的請查看官方API文檔吧

1.ArrayUtils

方法名 說明
add
remove
clone 複製數組
addAll
removeAll 第二個參數傳入需要刪除的下標(可以指定多個下標)
toObject 把數值(int[],double[])轉為包裝類(Int[],Double[])
indexOf 在數組按順序查找,找到第一個滿足對應的數值的下標
lastIndexOf 在數組按順序查找,找到最後一個滿足對應的數值的下標
contains 數組是否包含某個值
isEmpty 判斷數組是否為空
isNotEmpty 判斷數組是否不為空
reverse 數組反轉
subarray 指定區間截取數組,區間為半開區間,不包含末尾
toArray 接收一個多個對象,把這幾個對象轉為對應類型的數組
toMap 將一個二維數組轉為Map

2.NumberUtils

方法名 說明
min 比較三個數,返回最小值 或比較指定的幾個數,返回最小值
max 比較三個數,返回最大值 或比較指定的幾個數,返回最大值
createInt 從傳入的String中創建對應類型的數值,createDouble,createFloat…
toInt 將指定字符串轉為Int類型,可以選擇指定默認數值,如果字符串為null則返回默認數值,除此之外,還有toDouble,toLong…等轉為不同類型的方法
compare 比較兩個同類型數值的大小
isDigits 判斷字符串是否只包含数字
isParsable 判斷字符串是否可轉換為Long,Int等類型
isNumber 判斷字符串是否為數值(0x,0X開頭等進制數值)

3.DateUtils

方法名 說明
parseDate 將Date對象轉為字符串
isSameDay 判斷兩個Dated對象是否為同一天
isSameDay 判斷兩個Dated對象是否為同一天
addHour 將指定的Date對象加上指定小時,除此之外,還有addMonth,addDay..等

DateFormatUtils正如其名,是用來把時間轉為字符串,這裏就不再多說

4.StringUtils

方法名 說明
join 將指定的數組連接成字符串,並添加指定的分割字符
containOnly 字符串是否只包含某個字符串
substringBefore 截取指定字符串前面的內容
substringAfter 截取指定字符串後面的內容(不包括指定字符串)
substringBetween 截取字符串某區間內容,如substringBetween(“abcde”,”a”,”e”)=”bcd”
difference 比較兩個字符串,返回兩個字符串不同的內容,具體可以看API文檔給出的示例
isBlank 判斷字符串是否為空白,null,””,” “這三個結果都是為true
isEmpty 判斷字符串是否為空(只要不為null,或傳入的String對象的長度不為0即為true)
countMatches 判斷指定的字符串在某個字符串中出現的次數
deleteWhitespace 刪除字符串中的空格
defaultIfBlank 如果字符串為空白,則返回一個指定的默認值(null或某個String)
defaultIfEmpty 如果字符串為空,則返回一個指定的默認值(null或某個String)
capitalize 將指定字符串首字母大寫
abbreviate 將指定字符串的後面三位轉為…
swapCase 將字符串中的字母大小寫反轉,如aBc變為AbC
lowerCase 將字符串的字母全部轉為小寫
upperCase 將字符串的字母全部轉為大寫
left 取字符串左邊幾個字符,如left(“hello”,3)=”hel”,right與此相反
leftPad 字符串的長度不夠,則使用指定字符填充指定字符串,如leftPad(“hel”,5,”z”)=”zzhel”,rightPad方法與此相反
prependIfMissing 指定字符串不以某段字符串開頭,則自動添加開頭,如prependIfMissing(“hello”,”li”)=”lihello”
prependIfMissing 指定字符串不以某段字符串開頭(忽略大小寫),則自動添加開頭
getCommonPrefix 獲得多個字符串相同的開頭內容,接收參數為多個字符串
removeEnd 刪除字符串中結尾(滿足是以某段內容結尾),如removeEnd(“hello”,”llo”)=”he”
removeEndIgnoreCase 與上面一樣,忽略大小寫
removeStart 與上面的相反
remove 刪除字符串中的指定內容,如remove(“hello”,”l”)=”heo”
removeIgnoreCase 刪除字符串中的指定內容,如remove(“hello”,”l”)=”heo”
strip 清除字符串開頭和末尾指定的字符(第二個參數為null,用來清除字符串開頭和末尾的空格),如strip(” abcxy”,”xy”)=” abc”,strip(” abcxy”,”yx”)=” abc”
stripStart 清除字符串開頭指定字符
stripEnd 清除字符串末尾指定的字符

common-io

簡介

常用的IO流工具包

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

API

我們主要關心的就是Utils後綴的那幾個類即可,可以看到,common-io庫提供了FileUtils,FileSystemUtils,FileNameUtils,FileFilterUtils,IOUtils

FileUtils

  • 寫出文件
  • 讀取文件
  • 創建一個有父級文件夾的文件夾
  • 複製文件和文件夾
  • 刪除文件和文件夾
  • URL轉文件
  • 通過過濾器和擴展名來篩選文件和文件夾
  • 比較文件內容
  • 文件最後修改時間
  • 文件校驗

FileSystemUtils

關於文件系統的相關操作,如查看C盤的大小,剩餘大小等操作

IOUtils

字面意思,是封裝了IO流的各種操作的工具類

Log4j

簡介

Log4J 是 Apache 的一個開源項目,通過在項目中使用 Log4J,我們可以控制日誌信息輸出到控制台、文件、GUI 組件、甚至是數據庫中。

我們可以控制每一條日誌的輸出格式,通過定義日誌的輸出級別,可以更靈活的控制日誌的輸出過程,方便項目的調試。

依賴:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

結構

Log4J 主要由 Loggers (日誌記錄器)、Appenders(輸出端)和 Layout(日誌格式化器)組成。

其中Loggers 控制日誌的輸出級別與日誌是否輸出;
Appenders 指定日誌的輸出方式(輸出到控制台、文件等);
Layout 控制日誌信息的輸出格式。

日誌級別:

級別 說明
OFF 最高日誌級別,關閉左右日誌
FATAL 將會導致應用程序退出的錯誤
ERROR 發生錯誤事件,但仍不影響系統的繼續運行
WARN 警告,即潛在的錯誤情形
INFO 一般和在粗粒度級別上,強調應用程序的運行全程
DEBUG 一般用於細粒度級別上,對調試應用程序非常有幫助
ALL 最低等級,打開所有日誌記錄

我們主要使用這四個:Error>Warn>Info>Debug

使用

我們可以使用兩種方式來運行Log4j,一種是java代碼方式,另外一種則是配置文件方式

例子(Java方式)

public class Log4JTest {
    public static void main(String[] args) {   
        //獲取Logger對象的實例(傳入當前類)         
        Logger logger = Logger.getLogger(Log4JTest.class);
        //使用默認的配置信息,不需要寫log4j.properties
        BasicConfigurator.configure();
        //設置日誌輸出級別為WARN,這將覆蓋配置文件中設置的級別,只有日誌級別低於WARN的日誌才輸出
        logger.setLevel(Level.WARN);
        logger.debug("這是debug");
        logger.info("這是info");
        logger.warn("這是warn");
        logger.error("這是error");
        logger.fatal("這是fatal");
    }
}

例子(配置文件方式)

上面的例子,我們想要實現打印Log,但是每次都要寫一遍,浪費時間和精力,所以,Log4j提供了另外一種方式來配置好我們的信息

創建一個名為log4j.properties的文件,此文件需要放在項目的根目錄(約定),如果是maven項目,直接放在resources文件夾中即可

log4j.properties

#控制台
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n

#log jdbc
log4j.logger.java.sql.ResultSet=INFO
log4j.logger.org.apache=WARN
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

#log mybatis設置
#log4j.logger.org.apache.ibatis=DEBUG
log4j.logger.org.apache.ibatis.jdbc=error
log4j.logger.org.apache.ibatis.io=info
log4j.logger.org.apache.ibatis.datasource=info

#springMVC日誌
log4j.logger.org.springframework.web=WARN

# 文件輸出配置
log4j.appender.A = org.apache.log4j.DailyRollingFileAppender
log4j.appender.A.File = D:/log.txt #指定日誌的輸出路徑
log4j.appender.A.Append = true
log4j.appender.A.Threshold = DEBUG
log4j.appender.A.layout = org.apache.log4j.PatternLayout #使用自定義日誌格式化器
log4j.appender.A.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n #指定日誌的輸出格式
log4j.appender.A.encoding=UTF-8 #指定日誌的文件編碼

#指定日誌的輸出級別與輸出端
log4j.rootLogger=DEBUG,Console,A

#指定某個包名日誌級別(不能超過上面定義的級別,否則日誌不會輸出)
log4j.logger.com.wan=DEBUG

之後使用的話就比較簡單了

//Logger的初始化(這個推薦定義為全局變量,方便使用)
Logger logger = Logger.getLogger(Log4JTest.class);
//輸出Log
logger.info("這是info");

參考鏈接:

lombok

簡介

平常我們創建實體類的時候,需要get/set方法,極其麻煩,雖然IDEA等IDE都是有提供了快捷生成,不過,最好的解決方法還是省略不寫

而lombok就是這樣的一個框架,實現省略get/set方法,當然,lombok的功能不只有此,還有equal,toString方法也可以由此框架自動生成

lombok的原理是使用註解,之後就會在編譯過程中,給Class文件自動加上get/set等方法

不過IDEA似乎無法識別,代碼檢查還是會報錯,所以,使用IDEA的時候還得安裝一個插件,在plugin搜索lombok,之後安裝重啟即可,如圖

之後為Java項目添加依賴

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>

使用示例

1.實體類省略get/set
估計Kotlin中的data關鍵字就是參照着lombok實現的

//這裏我們只需要為類添加Data註解,就會自動生成對應屬性的get/set方法,toString,equal等方法
@Data
public class User {
    private String username;
    private String password;
}

2.需要無參構造以及get/set方法

@Getter
@Setter
@NoArgsConstructor
public class User {
    private String username;
    private String password;
}

3.鏈式調用set方法

@Data
@Accessors(chain = true)
public class User {
    private String username;
    private String password;
}

//使用
User user = new User();
user.setUsername("helo").setPassword("123");

4.參數不為空

//如果調用此方法,就會抱一個空指針錯誤
public String print(@NotNull String str){
    ...
}

5.只需要toString

@ToString(callSuper=true, includeFieldNames=true)
public class User {
    private String username;
    private String password;
    //省略的get/set方法
}

6.builder模式創建實體類對象

@Data
@Builder
public class User {
    private String username;
    private String password;
}
//使用
User user1 = User.builder().username("user1").password("123").build();

7.工具類

@UtilityClass
public class MyUtils{
    //會將此方法自動轉為靜態方法
    public void print(String str){
        ...
    }
}
//使用
MyUtils.print("hello");

8.自動關閉流

public static void main(String[] args) throws Exception {
    //使用Cleanup會自動調用close方法
    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    byte[] b = new byte[1024];
    while (true) {
        int r = in.read(b);
        if (r == -1) break;
        out.write(b, 0, r);
    }
}

9.省略Logger時的初始化

@Log4j
@Log
public class User{
    //會自動添加此語句
    //Logger logger = Logger.getLogger(User.class);
    ...
}

參考:

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

標準庫bufio個人詳解

本文是我有通俗的語言寫的如果有誤請指出。

先看bufio官方文檔

https://studygolang.com/pkgdoc文檔地址

 

 主要分三部分Reader、Writer、Scanner

分別是讀數據、寫數據和掃描器三種數據類型的相關操作 這個掃描後面會詳細說我開始也沒弄明白其實很簡單。

 

Reader

func 

func NewReaderSize(rd ., size ) *

NewReaderSize創建一個具有最少有size尺寸的緩衝、從r讀取的*Reader。如果參數r已經是一個具有足夠大緩衝的* Reader類型值,會返回r。

 

 

 解釋:看官方解釋這個方法可能不太容易懂,這個意思就是就是你可以給*Reader自定義一個size大小的緩衝區,*Reader每次從底層io.Reader(也就是你那個文件或者流)中預讀size大小的數據到緩衝區中(可能讀不滿),然後你每次讀數據實際是從這個緩衝區中拿數據。

 

 下面是NewReaderSize源碼

func NewReaderSize(rd io.Reader, size int) *Reader {
    // Is it already a Reader?
    b, ok := rd.(*Reader)
    if ok && len(b.buf) >= size {
        return b
    }
    if size < minReadBufferSize { //minReadBufferSize==16
        size = minReadBufferSize
    }
    r := new(Reader)
    r.reset(make([]byte, size), rd)
    return r
}

  r.reset 初始化了一個*Reader 返回大小是size。

func 

func NewReader(rd .) *

NewReader創建一個具有默認大小緩衝、從r讀取的*Reader。

解釋:那這個NewReader就很好解釋了 和NewReaderSize基本一樣就是緩衝區大小是默認設置好的

func (*Reader) 

func (b *) Peek(n ) ([], )

解釋:Peek就是返回緩存的一個切片,該切片引用緩存中的前N個字節的數據,如果n大於總大小,則返回能讀到的字節數的數據。

func (*Reader) 

func (b *) Read(p []) (n , err )

Read讀取數據寫入p。本方法返回寫入p的字節數。本方法一次調用最多會調用下層Reader接口一次Read方法,因此返回值n可能小於len(p)。讀取到達結尾時,返回值n將為0而err將為io.EOF。

解釋:如果緩存不為空則直接從緩存中讀數據不會從底層io.Reader讀,如果緩存為空len(p)>緩存大小,則直接從底層io.Reader讀數據到p。

如果len(p)<緩存大小,則先從底層io.Reader中讀數據到緩存再到p。

 

主要就這幾個 還有幾個文檔寫的都很清楚易懂我就不多寫了。

Writer類型的方法和Reader類型的方法差不多也很易懂主要就一個Flush要注意。

func (*Writer) 

func (b *) Flush() 

Flush方法將緩衝中的數據寫入下層的io.Writer接口。

和Reader是倒過來的,Writer每次寫數據是先寫入緩衝區的,進程緩衝區填滿后,通過進程緩衝寫入到內核緩衝再寫入到磁盤,使用Flush就不等填滿直接走寫入流程了,保證你的數據及時寫入文件。

 

 

 

 解釋:scanner類型掃描器 官方的說法很複雜,我也沒太看懂找了很多資料,其實就是你在數據傳輸的時候時候使用“分隔符”,scanner類型可以通過分隔符逐個迭代你的數據。

上面4個函數func Scan……  就是分隔符的判斷函數這4個是給你預設好的,你也可以按照自己的需求改寫。

怎麼改寫呢,看下面

func (*Scanner) 

func (s *) Split(split )

這個Split方法就是設置你這個scanner的用哪個SplitFunc類型的函數

在看下面這個SpliFunc類型的函數簽名

type SplitFunc func(data [], atEOF ) (advance , token [], err )

照着這個格式寫一個不就得了么,當然具體寫法給出了但是你不會?沒關係咱看一下官方是咋寫的。

https://github.com/golang/go/blob/master/src/bufio/scan.go?name=release#57官方源碼地址

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	if i := bytes.IndexByte(data, '\n'); i >= 0 {
		// We have a full newline-terminated line.
		return i + 1, dropCR(data[0:i]), nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), dropCR(data), nil
	}
	// Request more data.
	return 0, nil, nil
}

   

看bytes.IndexByte(data, ‘\n’);這段不就是在找行尾嘛 比如你想改成以“;”為分隔符的就改成bytes.IndexByte(data, ‘;’);不就得了么

func main(){
    scanner:=bufio.NewScanner(
        strings.NewReader("abcdefg\nhigklmn"),
    )
    scanner.Split(ScanLines) //這裏可以隨意選擇用哪個函數也可以自定義,可以不指定默認為\n做分隔符
  for scanner.Scan(){
    fmt.Println(scanner.Text())
  }
}

  

到此為止拉~

 

 

 

 

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

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

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

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

Elasticsearch從入門到放棄:文檔CRUD要牢記

在Elasticsearch中,文檔(document)是所有可搜索數據的最小單位。它被序列化成JSON存儲在Elasticsearch中。每個文檔都會有一個唯一ID,這個ID你可以自己指定或者交給Elasticsearch自動生成。

如果延續我們之前不恰當的對比RDMS的話,我認為文檔可以類比成關係型數據庫中的表。

元數據

前面我們提到,每個文檔都有一個唯一ID來標識,獲取文檔時,“_id”字段記錄的就是文檔的唯一ID,它是元數據之一。當然,文檔還有一些其他的元數據,下面我們來一一介紹

  • _index:文檔所屬的索引名
  • _type:文檔所屬的type
  • _id:文檔的唯一ID

有了這三個,我們就可以唯一確定一個document了,當然,7.0版本以後我們已經不需要_type了。接下來我們再來看看其他的一些元數據

  • _source:文檔的原始JSON數據
  • _field_names:該字段用於索引文檔中值不為null的字段名,主要用於exists請求查找指定字段是否為空
  • _ignore:這個字段用於索引和存儲文檔中每個由於異常(開啟了ignore_malformed)而被忽略的字段的名稱
  • _meta:該字段用於存儲一些自定義的元數據信息
  • _routing:用來指定數據落在哪個分片上,默認值是Id
  • _version:文檔的版本信息
  • _score:相關性打分

創建文檔

創建文檔有以下4種方法:

  • PUT /<index>/_doc/<_id>
  • POST /<index>/_doc/
  • PUT /<index>/_create/<_id>
  • POST /<index>/_create/<_id>

這四種方法的區別是,如果不指定id,則Elasticsearch會自動生成一個id。如果使用_create的方法,則必須保證文檔不存在,而使用_doc方法的話,既可以創建新的文檔,也可以更新已存在的文檔。

在創建文檔時,還可以選擇一些參數。

請求參數

  • if_seq_no:當文檔的序列號是指定值時才更新
  • if_primary_term:當文檔的primary term是指定值時才更新
  • op_type:如果設置為create則指定id的文檔必須不存在,否則操作失敗。有效值為index或create,默認為index
  • op_type:指定預處理的管道id
  • refresh:如果設置為true,則立即刷新受影響的分片。如果是wait_for,則會等到刷新分片后,此次操作才對搜索可見。如果是false,則不會刷新分片。默認值為false
  • routing:指定路由到的主分片
  • timeout:指定響應時間,默認是30秒
  • master_timeout:連接主節點的響應時長,默認是30秒
  • version:顯式的指定版本號
  • version_type:指定版本號類型:internal、 external、external_gte、force
  • wait_for_active_shards:處理操作之前,必須保持活躍的分片副本數量,可以設置為all或者任意正整數。默認是1,即只需要主分片活躍。

響應包體

  • **_shards**:提供分片的信息
  • **_shards.total**:創建了文檔的總分片數量
  • **_shards.successful**:成功創建文檔分片的數量
  • **_shards.failed**:創建文檔失敗的分片數量
  • **_index**:文檔所屬索引
  • **_type**:文檔所屬type,目前只支持_doc
  • **_id**:文檔的id
  • **_version**:文檔的版本號
  • **_seq_no**:文檔的序列號
  • **_primary_term**:文檔的主要術語
  • result:索引的結果,created或者updated

我們在創建文檔時,如果指定的索引不存在,則ES會自動為我們創建索引。這一操作是可以通過設置中的action.auto_create_index字段來控制的,默認是true。你可以修改這個字段,實現指定某些索引可以自動創建或者所有索引都不能自動創建的目的。

更新文檔

了解了如何創建文檔之後,我們再來看看應該如何更新一個已經存在的文檔。其實在創建文檔時我們就提到過,使用PUT /<index>/_doc/<id>的方法就可以更新一個已存在的文檔。除此之外,我們還有另一種更新文檔的方法:

POST /<index>/_update/<_id>

這兩種更新有所不同。_doc方法是先刪除原有的文檔,再創建新的。而_update方法則是增量更新,它的更新過程是先檢索到文檔,然後運行指定腳本,最後重新索引。

還有一個區別就是_update方法支持使用腳本更新,默認的語言是painless,你可以通過參數lang來進行設置。在請求參數方面,_update相較於_doc多了以下幾個參數:

  • lang:指定腳本語言
  • retry_on_conflict:發生衝突時重試次數,默認是0
  • **_source**:設置為false,則不返回任何檢索字段
  • **_source_excludes**:指定要從檢索結果排除的source字段
  • **_source_includes**:指定要返回的檢索source字段

下面的一個例子是用腳本來更新文檔

curl -X POST "localhost:9200/test/_update/1?pretty" -H 'Content-Type: application/json' -d'
{
    "script" : {
        "source": "ctx._source.counter += params.count",
        "lang": "painless",
        "params" : {
            "count" : 4
        }
    }
}
'

Upsert

curl -X POST "localhost:9200/test/_update/1?pretty" -H 'Content-Type: application/json' -d'
{
    "script" : {
        "source": "ctx._source.counter += params.count",
        "lang": "painless",
        "params" : {
            "count" : 4
        }
    },
    "upsert" : {
        "counter" : 1
    }
}
'

當指定的文檔不存在時,可以使用upsert參數,創建一個新的文檔,而當指定的文檔存在時,該請求會執行script中的腳本。如果不想使用腳本,而只想新增/更新文檔的話,可以使用doc_as_upsert。

curl -X POST "localhost:9200/test/_update/1?pretty" -H 'Content-Type: application/json' -d'
{
    "doc" : {
        "name" : "new_name"
    },
    "doc_as_upsert" : true
}
'

update by query

這個API是用於批量更新檢索出的文檔的,具體可以通過一個例子來了解。

curl -X POST "localhost:9200/twitter/_update_by_query?pretty" -H 'Content-Type: application/json' -d'
{
  "script": {
    "source": "ctx._source.likes++",
    "lang": "painless"
  },
  "query": {
    "term": {
      "user": "kimchy"
    }
  }
}
'

獲取文檔

ES獲取文檔用的是GET API,請求的格式是:

GET /<index>/_doc/<_id>

它會返迴文檔的數據和一些元數據,如果你只想要文檔的內容而不需要元數據時,可以使用

GET /<index>/_source/<_id>

請求參數

獲取文檔的有幾個請求參數之前已經提到過,這裏不再贅述,它們分別是:

  • refresh
  • routing
  • **_source**
  • **_source_excludes**
  • **_source_includes**
  • version
  • version_type

而還有一些之前沒提到過的參數,我們來具體看一下

  • preference:用來 指定執行請求的node或shard,如果設置為_local,則會優先在本地的分片執行
  • realtime:如果設置為true,則請求是實時的而不是近實時。默認是true
  • stored_fields:返回指定的字段中,store為true的字段

mget

mget是批量獲取的方法之一,請求的格式有兩種:

  • GET /_mget
  • GET /<index>/_mget

第一種是在請求體中寫index。第二種是把index放到url中,不過這種方式可能會觸發ES的安全檢查。

mget的請求參數和get相同,只是需要在請求體中指定doc的相關檢索條件

request

GET /_mget
{
    "docs" : [
        {
            "_index" : "jackey",
            "_id" : "1"
        },
        {
            "_index" : "jackey",
            "_id" : "2"
        }
    ]
}

response

{
  "docs" : [
    {
      "_index" : "jackey",
      "_type" : "_doc",
      "_id" : "1",
      "_version" : 5,
      "_seq_no" : 6,
      "_primary_term" : 1,
      "found" : true,
      "_source" : {
        "user" : "ja",
        "tool" : "ES",
        "message" : "qwer"
      }
    },
    {
      "_index" : "jackey",
      "_type" : "_doc",
      "_id" : "2",
      "_version" : 1,
      "_seq_no" : 2,
      "_primary_term" : 1,
      "found" : true,
      "_source" : {
        "user" : "zhe",
        "post_date" : "2019-11-15T14:12:12",
        "message" : "learning Elasticsearch"
      }
    }
  ]
}

刪除文檔

CURD操作只剩下最後一個D了,下面我們就一起來看看ES中如何刪除一個文檔。

刪除指定id使用的請求是

DELETE /<index>/_doc/<_id>

在併發量比較大的情況下,我們在刪除時通常會指定版本,以確定刪除的文檔是我們真正想要刪除的文檔。刪除請求的參數我們在之前也都介紹過,想要具體了解的同學可以直接查看。

delete by query

類似於update,delete也有一個delete by query的API。

POST /<index>/_delete_by_query

它也是要先按照條件來查詢匹配的文檔,然後刪除這些文檔。在執行查詢之前,Elasticsearch會先為指定索引做一個快照,如果在執行刪除過程中,要索引發生改變,則會導致操作衝突,同時返回刪除失敗。

如果刪除的文檔比較多,也可以使這個請求異步執行,只需要設置wait_for_completion=false即可。

這個API的refresh與delete API的refresh參數有所不同,delete中的refresh參數是設置操作是否立即可見,即只刷新一個分片,而這個API中的refresh參數則是需要刷新受影響的所有分片。

Bulk API

最後,我們再來介紹一種特殊的API,批量操作的API。它支持兩種寫法,可以將索引名寫到url中,也可以寫到請求體中。

  • POST /_bulk

  • POST /<index>/_bulk

在這個請求中,你可以任意使用之前的CRUD請求的組合。

curl -X POST "localhost:9200/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
'

請求體中使用的語法是newline delimited JSON(NDJSON)。具體怎麼用呢?其實我們在上面的例子中已經有所展現了,對於index或create這樣的請求,如果請求本身是有包體的,那麼用換行符來表示下面的內容與子請求分隔,即為包體的開始。

例如上面例子中的index請求,它的包體就是{ “field1” : “value1” },所以它會在index請求的下一行出現。

對於批量執行操作來說,單條操作失敗並不會影響其他操作,而最終每條操作的結果也都會返回。

上面的例子執行完之後,我們得到的結果應該是

{
   "took": 30,
   "errors": false,
   "items": [
      {
         "index": {
            "_index": "test",
            "_type": "_doc",
            "_id": "1",
            "_version": 1,
            "result": "created",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "status": 201,
            "_seq_no" : 0,
            "_primary_term": 1
         }
      },
      {
         "delete": {
            "_index": "test",
            "_type": "_doc",
            "_id": "2",
            "_version": 1,
            "result": "not_found",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "status": 404,
            "_seq_no" : 1,
            "_primary_term" : 2
         }
      },
      {
         "create": {
            "_index": "test",
            "_type": "_doc",
            "_id": "3",
            "_version": 1,
            "result": "created",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "status": 201,
            "_seq_no" : 2,
            "_primary_term" : 3
         }
      },
      {
         "update": {
            "_index": "test",
            "_type": "_doc",
            "_id": "1",
            "_version": 2,
            "result": "updated",
            "_shards": {
                "total": 2,
                "successful": 1,
                "failed": 0
            },
            "status": 200,
            "_seq_no" : 3,
            "_primary_term" : 4
         }
      }
   ]
}

批量操作的執行過程相比多次單個操作而言,在性能上會有一定的提升。但同時也會有一定的風險,所以我們在使用的時候要非常的謹慎。

總結

本文我們先介紹了文檔的基本概念和文檔的元數據。接着又介紹了文檔的CRUD操作和Bulk API。相信看完文章你對Elasticsearch的文檔也會有一定的了解。那最後就請你啟動你的Elasticsearch,然後親自動手試一試這些操作,看看各種請求的參數究竟有什麼作用。相信親手實驗過一遍之後你會對這些API有更深的印象。

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

EF Core For MySql查詢中使用DateTime.Now作為查詢條件的一個小問題

背景

最近一直忙於手上澳洲線上項目的整體遷移和升級的準備工作,導致博客和公眾號停更。本周終於艱難的完成了任務,藉此機會,總結一下項目中遇到的一些問題。

EF Core一直是我們團隊中中小型項目常用的ORM框架,在使用SQL Server作為持久化倉儲的場景一下,一直表現還中規中矩。但是在本次項目中,項目使用了MySql作為持久化倉儲。為了與EF Core集成,團隊使用了Pomelo.EntityFrameworkCore.MySql作為EF Core For MySql的擴展。在開發過程中,團隊遇到了各種各樣在SQL Server場景下沒有遇到過的問題,其中最奇怪的,也是隱藏最深的問題,就是將DateTime.Now作為查詢條件,產生了非預期的結果。

問題場景

本周在項目升級的過程中,客戶反饋了一個問題。

在當前系統的Dashboard頁面,有一個消息提醒功能,客戶可以自定義一些消息,並且指定提醒的日期。客戶遇到的問題是通常添加的消息提醒,在指定日期的上午時間段是不會显示,只有在下午時間段才能看到,比如說客戶指定2019年10月26號看到一個的消息提醒,但是在10月26日這天早上8:00-12:00這個時間段,系統總是看不到提醒,只有到了下午的時間段才能看到提醒。

PS:這裏客戶表達的只是個籠統的問題,但問題確實是上午的大部分時間是看不到消息提醒的,但並不是精確到中午12:00點這個時間, 所以此處不必過於糾結於具體的時間。

查看問題代碼

看到這個問題的時候,我自己也很奇怪,難道代碼或者數據庫使用了時區,導致查詢出現了偏差?

於是我就Review了一下此處的查詢, 代碼如下。

var query = DbContext.CRM_Note_Reminders
    .Include(x => x.CRM_Note)
    .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= DateTime.Now.Date)
     .ToList();

PS: 這裏可能有同學會有疑問,為啥不用DbFunctions.DiffDays? 原因是DbFunctions.DiffDays是 EF Core for SQLServer的擴展方法,針對MySql還沒有官方的實現方案。

從這個查詢中,我沒有看出任何問題,於是我直接藉助一些日誌工具,將EF Core生成的查詢語句的輸出了出來。

其中WHERE條件部分如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE))
AND (CONVERT(`x`.`Reminder_Date`, date) 
  <= CONVERT(CURRENT_TIMESTAMP(), date)))

這裏CURRENT_TIMESTAMP()是MySql的內置函數,與SQLServer的內置函數GETDATE()不同,CURRENT_TIMESTAMP()默認返回的是UTC時間。因此我們大概能知道,為什麼澳洲客戶會遇到上面的場景了。

PS: 根據7樓兄弟的反饋,我試了一下,改動Mysql的時區配置之後,果然CURRENT_TIMESTAMP()就改為了對應時區的時間。這裏使用UTC時間的原因應該是我在AWS RDS上創建Mysql實例的時候,忽略了時區配置。

由於澳洲處於東10區,與UTC時間有+10個小時的時差,所以當澳洲上午的10點之前,UTC時間都是在當前澳洲日期的前一天,所以系統中出現了當天的消息提醒在上午時間段不能正常显示的問題。

PS: 由於澳洲是分冬令時和夏令時的,夏令時時間要加一個小時,所以實際上客戶在每天的11點之前都無法看到正確的消息提醒。

深入思考

你這可能會非常奇怪,為什麼DateTime.Now會被轉化成內置函數CURRENT_TIMESTAMP(),而沒有使用我們傳入的值DateTime.Now.Date呢?

其實EF/EF Core在查詢是時候是分2個階段的,一個是組合查詢表達式樹的階段,一個是真正的查詢階段。

在組合查詢表達式樹的階段,EF/EF Core只會去組合表達式,而不會去嘗試計算表達式的值,所以這個階段DateTime.Now.Date的值並沒有被計算出來, 在進入正常查詢階段的時候, EF/EF Core會嘗試將查詢表達式樹翻譯成SQL腳本,這時候由於我們的EF ProviderMySql Provider, 恰巧DateTime.Now可以翻譯成Mysql的內置函數CURRENT_TIMESTAMP(), 所以這裏EF/EF Core就跳過了表達式值的計算,直接將其翻譯成了對應的內置函數,所以導致生成的SQL查詢和我們的預期有偏差。

那麼我們該如何解決這個問題呢?

解決方案

經過了以上的思考,其實解決這個問題也就很簡單了,我們可以將DateTime.Now.Date先計算出來,保存在一個變量中,然後將這個變量傳入查詢中。

var today = DateTime.Now.Date;

var query = DbContext.CRM_Note_Reminders
     .Include(x => x.CRM_Note)
     .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= today)
     .ToList();

由此生成的MySQL腳本如下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE)) 
AND (CONVERT(`x`.`Reminder_Date`, date) <= @__date_0)) 

這樣我們就得到了一個正確的結果,澳洲客戶也就收到了正確的消息。

是不是有種差之毫厘,謬以千里的感覺呢?

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

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

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

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

將Swagger2文檔導出為HTML或markdown等格式離線閱讀

網上有很多《使用swagger2構建API文檔》的文章,該文檔是一個在線文檔,需要使用HTTP訪問。但是在我們日常使用swagger接口文檔的時候,有的時候需要接口文檔離線訪問,如將文檔導出為html、markdown格式。又或者我們不希望應用系統與swagger接口文檔使用同一個服務,而是導出HTML之後單獨部署,這樣做保證了對接口文檔的訪問不影響業務系統,也一定程度提高了接口文檔的安全性。核心的實現過程就是:

  • 在swagger2接口文檔所在的應用內,利用swagger2markup將接口文檔導出為adoc文件,也可以導出markdown文件。
  • 然後將adoc文件轉換為靜態的html格式,可以將html發布到nginx或者其他的web應用容器,提供訪問(本文不會講html靜態部署,只講HTML導出)。

注意:adoc是一種文件格式,不是我的筆誤。不是doc文件也不是docx文件。

一、maven依賴類庫

在已經集成了swagger2的應用內,通過maven坐標引入相關依賴類庫,pom.xml代碼如下:

<dependency>
    <groupId>io.github.swagger2markup</groupId>
    <artifactId>swagger2markup</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-core</artifactId>
    <version>1.5.16</version>
</dependency>
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-models</artifactId>
    <version>1.5.16</version>
</dependency>

swagger2markup用於將swagger2在線接口文檔導出為html,markdown,adoc等格式文檔,用於靜態部署或離線閱讀。其中第一個maven坐標是必須的。后兩個maven坐標,當你在執行後面的代碼過程中報下圖中的ERROR,或者有的類無法import的時候使用。

產生異常的原因已經有人在github的issues上給出解釋了:當你使用swagger-core版本大於等於1.5.11,並且swagger-models版本小於1.5.11就會有異常發生。所以我們顯式的引入這兩個jar,替換掉swagger2默認引入的這兩個jar。

二、生成adoc格式文件

下面的代碼是通過編碼方式實現的生成adoc格式文件的方式

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class DemoApplicationTests {
    @Test
    public void generateAsciiDocs() throws Exception {
        //    輸出Ascii格式
        Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
                .withMarkupLanguage(MarkupLanguage.ASCIIDOC) //設置生成格式
                .withOutputLanguage(Language.ZH)  //設置語言中文還是其他語言
                .withPathsGroupedBy(GroupBy.TAGS)
                .withGeneratedExamples()
                .withoutInlineSchema()
                .build();

        Swagger2MarkupConverter.from(new URL("http://localhost:8888/v2/api-docs"))
                .withConfig(config)
                .build()
                .toFile(Paths.get("src/main/resources/docs/asciidoc"));
    }
}
  • 使用RunWith註解和SpringBootTest註解,啟動應用服務容器。 SpringBootTest.WebEnvironment.DEFINED_PORT表示使用application.yml定義的端口,而不是隨機使用一個端口進行測試,這很重要。
  • Swagger2MarkupConfig 是輸出文件的配置,如文件的格式和文件中的自然語言等
  • Swagger2MarkupConverter的from表示哪一個HTTP服務作為資源導出的源頭(JSON格式),可以自己訪問試一下這個鏈接。8888是我的服務端口,需要根據你自己的應用配置修改。
  • toFile表示將導出文件存放的位置,不用加後綴名。也可以使用toFolder表示文件導出存放的路徑。二者區別在於使用toFolder導出為文件目錄下按標籤TAGS分類的多個文件,使用toFile是導出一個文件(toFolder多個文件的合集)。
@Test
public void generateMarkdownDocsToFile() throws Exception {
    //    輸出Markdown到單文件
    Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
            .withMarkupLanguage(MarkupLanguage.MARKDOWN)
            .withOutputLanguage(Language.ZH)
            .withPathsGroupedBy(GroupBy.TAGS)
            .withGeneratedExamples()
            .withoutInlineSchema()
            .build();

    Swagger2MarkupConverter.from(new URL("http://localhost:8888/v2/api-docs"))
            .withConfig(config)
            .build()
            .toFile(Paths.get("src/main/resources/docs/markdown"));
}

上面的這一段代碼是生成markdown格式接口文件的代碼。執行上面的2段單元測試代碼,就可以生產對應格式的接口文件。

還有一種方式是通過maven插件的方式,生成adoc和markdown格式的接口文件。筆者不常使用這種方式,沒有使用代碼生成的方式配置靈活,很多配置都放到pom.xml感覺很臃腫。但還是介紹一下,首先配置maven插件swagger2markup-maven-plugin。

<plugin>
    <groupId>io.github.swagger2markup</groupId>
    <artifactId>swagger2markup-maven-plugin</artifactId>
    <version>1.3.1</version>
    <configuration>
        <swaggerInput>http://localhost:8888/v2/api-docs</swaggerInput><!---swagger-api-json路徑-->
        <outputDir>src/main/resources/docs/asciidoc</outputDir><!---生成路徑-->
        <config>
            <swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage><!--生成格式-->
        </config>
    </configuration>
</plugin>

然後運行插件就可以了,如下圖:

三、通過maven插件生成HTML文檔

<plugin>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId>
    <version>1.5.6</version>
    <configuration>
         <!--asciidoc文件目錄-->
        <sourceDirectory>src/main/resources/docs</sourceDirectory>
        <!---生成html的路徑-->
        <outputDirectory>src/main/resources/html</outputDirectory>
        <backend>html</backend>
        <sourceHighlighter>coderay</sourceHighlighter>
        <attributes>
            <!--導航欄在左-->
            <toc>left</toc>
            <!--显示層級數-->
            <!--<toclevels>3</toclevels>-->
            <!--自動打数字序號-->
            <sectnums>true</sectnums>
        </attributes>
    </configuration>
</plugin>

adoc的sourceDirectory路徑必須和第三小節中生成的adoc文件路徑一致。然後按照下圖方式運行插件。

HTMl接口文檔显示的效果如下,有了HTML接口文檔你想轉成其他各種格式的文檔就太方便了,有很多工具可以使用。這裏就不一一介紹了。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

新發現一種抗體有望開發為通用型流感疫苗

  新華社華盛頓 10 月 26 日電(記者周舟)美國科研團隊發現一種能“嵌入”流感病毒表面蛋白的抗體,可保護小鼠免遭多種流感病毒毒株的感染,未來有望開發為通用型流感疫苗。

  血凝素(H蛋白)和神經氨酸酶(N蛋白)是流感病毒表面的兩種蛋白,它們將流感病毒分為不同的亞型。目前開發的流感疫苗主要靶向血凝素。2017 年冬,美國華盛頓大學病理學和免疫學助理教授阿里·艾利貝迪發現一個流感患者的血樣不僅含有靶向血凝素的抗體,還含有可靶向其他蛋白的抗體。

  艾利貝迪將其中三種靶向不明的抗體送至芒特西奈伊坎醫學院進行檢測,該院微生物學教授弗洛里安·克拉默發現其中一種被稱為“1G01”的抗體,可阻斷多種流感病毒毒株上幾乎所有已知的神經氨酸酶的活動。

  克拉默團隊讓實驗小鼠感染致命性劑量的流感病毒,發現這種抗體可以對抗 12 種被測試的流感毒株,其中包括三類人類流感病毒毒株、禽流感和其他不在人際間傳播的病毒毒株。實驗發現,所有小鼠都生存了下來,即便在感染 72 小時以後給葯。相比而言,達菲必須在癥狀出現 24 小時內給葯。

  美國斯克里普斯研究所的結構生物學家伊安·威爾遜分析了這種抗體的結構,發現這種抗體將一個環狀結構嵌入神經氨酸酶的活性部位,阻止了神經氨酸酶從細胞表面釋放新的病毒顆粒。

  研究显示,這種抗體只阻斷神經氨酸酶的活性部位,而不同流感毒株間的活性部位幾乎不發生變異,因此它對多種流感病毒均有效。目前研究人員正在以抗體 1G01 為基礎設計新的流感藥物和疫苗。

  這一研究成果日前發表在美國《科學》雜誌上。

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

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

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

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

JSON——IT技術人員都必須要了解的一種數據交換格式

JSON作為目前Web主流的數據交換格式,是每個IT技術人員都必須要了解的一種數據交換格式。尤其是在Ajax和REST技術的大行其道的當今,JSON無疑成為了數據交換格式的首選

今天大家就和豬哥一起來學習一下JSON的相關知識吧!

一、XML

在講JSON之前,我覺得有必要先帶大家了解一下XML(Extensible Markup Language 可擴展標記語言),因為JSON正在慢慢取代XML。

1.XML起源

早期Web發展和負載的數據量並不是很大,所以基本靠HTML(1989誕生)可以解決。但是隨着Web應用的不斷壯大,HTML的一些缺點也慢慢顯現,如:可讀性差、解析時間長、數據描述性差等。

1998年2月10日,W3C(World WideⅥiebConsortium,萬維網聯盟)公布XML 1.0標準,XML誕生了。

XML使用一個簡單而又靈活的標準格式,為基於Web的應用提供了一個描述數據和交換數據的有效手段。但是,XML並非是用來取代HTML的。HTML着重如何描述將文件显示在瀏覽器中,它着重描述如何將數據以結構化方式表示。

XML簡單易於在任何應用程序中讀/寫數據,這使XML很快成為數據交換的唯一公共語言,所以XML被廣泛應用。

注意: XML是一種數據交換的格式,並不是編程語言。而且他是跨語言的數據格式,目前絕大多數編程語言均支持XML。

2.XML實例

XML究竟怎麼用?是什麼樣子的?我們來舉一個簡單的例子吧!

A公司要和B公司業務對接(A公司要獲取B公司的用戶基本信息),B公司提供接口讓A公司調用,A、B公司對接的開發人員會提前溝通好這個接口的:URL、傳參、返回數據、異常等等。

但是也許兩個公司使用的技術棧並不相同,所以支持的據格式也可能不同。為了解決因技術棧不同帶來的數據格式不同問題,A、B公司的開發協商使用一種通用的數據格式來傳輸,於是他們想到了XML。

  1. 假設現在A公司需要名稱叫pig的用戶信息,於是A公司調用B公司的接口,並傳參數name=pig。
  2. 然後B公司接口收到請求后,將用戶信息從數據庫拿出來,然後封裝成下面的XML格式,然後再返回給A公司。
  3. 最後A公司收到返回后,使用XML庫解析數據即可
<?xml version="1.0" encoding="UTF-8"?>
<person>
  <name>pig</name>
  <age>18</age>
  <sex>man</sex>
  <hometown>
    <province>江西省</province>
    <city>撫州市</city>
    <county>崇仁縣</county>
  </hometown>
</person>

3.XML十字路口

雖然XML標準本身簡單,但與XML相關的標準卻種類繁多,W3C制定的相關標準就有二十多個,採用XML制定的重要的电子商務標準就有十多個。這給軟件開發工程師帶來了極大的麻煩!

隨着AJax(之前叫XMLHTTP,2005年後才叫Ajax)技術的流行,XML的弊端也越來越顯現:大家都知道XML實現是基於DOM樹實現的,而DOM在各種瀏覽器中的實現細節不盡相同,所以XML的跨瀏覽器兼容性並不好,所以急需一種新的數據負載格式集成到HTML頁面中以滿足Ajax的要求!

二、JSON

前面我們說了隨着Ajax的流行,而各種瀏覽器對DOM的實現細節不盡相同,所以會出現兼容性問題,這對前端開發同學來講真的是災難。因為一個功能可能需要用代碼去兼容各種不同的瀏覽器,還要調試,工作量巨大。

1.JSON誕生

如何才能將數據整合到HTML中又解決瀏覽器兼容性問題呢?答案就是:利用所有主流瀏覽器中的一種通用組件——JavaScript引擎。這樣只要創造一種JavaScript引擎能識別的數據格式就可以啦!

2001 年 4 月,首個 JSON 格式的消息被發送出來。此消息是從舊金山灣區某車庫的一台計算機發出的,這是計算機歷史上重要的的時刻。道格拉斯·克羅克福特(Douglas Crockford) 和 奇普·莫寧斯達(Chip Morningstar) 是一家名為 State Software 的技術諮詢公司的聯合創始人(後來都在雅虎任職),他們當時聚集在 Morningstar 的車庫里測試某個想法,發出了此消息。

document.domain = 'fudco'; 

parent.session.receive( 
    { to: "session", do: "test", text: "Hello world" } 
) 

熟悉js的同學是不是也很驚訝,第一個 JSON 消息它明顯就是 JavaScript!實際上,Crockford 自己也說過他不是第一個這樣做的人。網景(Netscape )公司的某人早在 1996 年就使用 JavaScript 數組字面量來交換信息。因為消息就是 JavaScript,其不需要任何特殊解析工作,JavaScript 解釋器就可搞定一切。

最初的 JSON 信息實際上與 JavaScript 解釋器發生了衝突。JavaScript 保留了大量的關鍵字(ECMAScript 6 版本就有 64 個保留字),Crockford 和 Morningstar 無意中在其 JSON 中使用了一個保留字:do。因為 JavaScript 使用的保留字太多了,所以Crockford決定:既然不可避免的要使用到這些保留字,那就要求所有的 JSON 鍵名都加上引號。被引起來的鍵名會被 JavaScript 解釋器識別成字符串。這就為什麼今天 JSON 鍵名都要用引號引起來的原因。

這種數據格式既然可以被JavaScript引擎識別,那就解決了XML帶來的各種瀏覽器兼容性問題,所以這種技術完全可以推廣出去,於是Crockford 和 Morningstar 想給其命名為 “JSML”,表示JavaScript 標記語言(JavaScript Markup Language)的意思,但發現這個縮寫已經被一個名為 Java Speech 標記語言的東西所使用了。所以他們決定採用 “JavaScript Object Notation”,縮寫為 JSON,至此JSON正式誕生。

2.JSON發展

2005 年,JSON 有了一次大爆發。那一年,一位名叫 Jesse James Garrett 的網頁設計師和開發者在博客文章中創造了 “AJAX” 一詞。他很謹慎地強調:AJAX 並不是新技術,而是 “好幾種蓬勃發展的技術以某種強大的新方式彙集在一起。” AJAX 是 Garrett 給這種正受到青睞的 Web 應用程序的新開發方法的命名。他的博客文章接着描述了開發人員如何利用 JavaScript 和 XMLHttpRequest 構建新型應用程序,這些應用程序比傳統的網頁更具響應性和狀態性。他還以 Gmail 和 Flickr 網站已經使用 AJAX 技術作為了例子。

當然了,“AJAX” 中的 “X” 代表 XML。但在隨後的問答帖子中,Garrett 指出,JSON 可以完全替代 XML。他寫道:“雖然 XML 是 AJAX 客戶端進行數據輸入、輸出的最完善的技術,但要實現同樣的效果,也可以使用像 JavaScript Object Notation(JSON)或任何類似的結構數據方法等技術。 ”

這時JSON便在國外的博客圈、技術圈慢慢流行起來!

2006 年,Dave Winer,一位高產的博主,他也是許多基於 XML 的技術(如 RSS 和 XML-RPC)背後的開發工程師,他抱怨到 JSON 毫無疑問的正在重新發明 XML。

Crockford 閱讀了 Winer 的這篇文章並留下了評論。為了回應 JSON 重新發明 XML 的指責,Crockford 寫到:“重造輪子的好處是可以得到一個更好的輪子”。

3.JSON實例

還是以上面A、B公司業務對接為例子,兩邊的開發人員協商一種通用的數據交換格式,現在有XML與JSON比較流行的兩種數據格式,於是開發人員又將用戶信息以JSON形式展現出來,然後比較兩種數據格式:

{
  "person": {
    "name": "pig",
    "age": "18",
    "sex": "man",
    "hometown": {
      "province": "江西省",
      "city": "撫州市",
      "county": "崇仁縣"
    }
  }
}

比較XML與JSON的數據格式之後,開發人員發現:JSON可閱讀性、簡易性更好而且相同數據負載JSON字符數更少,所以兩個開發人員一致同意使用JSON作為接口數據格式!

而且還有重要的一點,在編寫XML時,第一行需要定義XML的版本,而JSON不存在版本問題,格式永遠不變!

4.當今JSON地位

當今的JSON 已經佔領了全世界。絕大多數的應用程序彼此通過互聯網通信時,都在使用 JSON。它已被所有大型企業所採用:十大最受歡迎的 web API 接口列表中(主要由 Google、Facebook 和 Twitter 提供),僅僅只有一個 API 接口是以 XML 的格式開放數據的。

JSON 也在程序編碼級別和文件存儲上被廣泛採用:在 Stack Overflow上,關於JSON的問題越來越多,下圖是關於Stack Overflow上不同數據交換格式的問題數和時間的曲線關係圖。
從上圖我們可以看出在Stack Overflow上越來越多JSON的問題,從這裏也可以反映出JSON越來越流行!

更詳細的關於創造JSON的故事可閱讀:

3、總結

由於篇幅原因我們今天只學習了JSON的誕生和起源相關知識,知道了JSON的誕生是因為XML無法滿足Ajax對瀏覽器兼容性問題,所以就有人想創造一種瀏覽器通用組件:JavaScript引擎 能識別的數據格式,這樣就可以解決瀏覽器不兼容問題,所以就從Js數據格式中提取了一個子集,取名為JSON!

我們還知道了為什麼JSON鍵為什麼需要用雙引號引起來,是因為JS中存在許多的關鍵字和保留關鍵字,為了避免與JS關鍵字衝突,所以Crockford就要求在所有的鍵名上加上雙引號,這樣JS引擎會將其識別為字符串,就避免與JS中關鍵字衝突!

下期我們會詳細介紹JSON數據結構、JSON序列化、JSON在Python中的使用等知識。

了解技術誕生與發展背後的故事同樣重要,因為這些可以作為你吹逼的資本!

參考資料:
百度百科:XML
Daniel Rubio:JSON 簡介

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

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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